用 Vue3 + fetch-event-source 打造流式 AI 翻译平台:OCR + 大模型 + SSE 全链路实战

从 UI 工程师到 AI 应用架构者

13 年前,我的工作是让按钮在 IE6 上对齐;
13 年后,我用 fetch-event-source 订阅大模型的“思维流”,用 OCR 解锁图片中的文字——前端,正在成为 AI 产品的第一道体验防线

最近,我基于 Vue 3 + Vite + TypeScript + Ant Design Vue 开发了一款企业级 AI 多语翻译平台。它支持:

    • ✅ 文本/图片/文档多模态输入
    • ✅ 大模型动态切换(通义千问、DeepSeek 等)
    • ✅ 行业领域定制(医疗、法律、金融)
    • SSE 流式输出(使用 fetch-event-source

今天,我将逐行拆解核心代码,带你复现这一高价值 AI 应用的前端架构。


一、技术选型:为什么是 fetch-event-source

虽然浏览器原生支持 EventSource,但在现代前端工程中,我们更倾向使用 @microsoft/fetch-event-source

    • ✅ 基于 fetch,天然支持 AbortController(可取消请求)
    • ✅ 支持 自定义 headers(用于鉴权)
    • ✅ 更好的 TypeScript 类型支持
    • ✅ 与 Axios/Vite 生态无缝集成

安装:

npm install @microsoft/fetch-event-source

二、OCR 图片识别:前端只管“传”和“显”

用户上传一张英文菜单截图,系统自动提取文字并翻译。前端不碰 OCR 算法,但要管理全流程状态

🔧 真实代码实现(来自 文本_r9uv.txt

1. 系统配置加载(含文件大小限制)

// src/views/translation/index.vue
const loadSystemPublicConfig = async () => {
  try {
    const response = await findSystemPublicConfig({ configKey: 'upload_file_max_limit_mb' });
    const config = response.data[0];
    systemConfig.value.uploadFileMaxLimit = parseInt(config.value) || 10; // 默认10MB
  } catch (error) {
    ElMessage.error('加载系统配置失败');
  }
};

2. 文件选择与校验

const handleFileSelect = (event: Event) => {
  const input = event.target as HTMLInputElement;
  if (input.files && input.files.length > 0) {
    const file = input.files[0];
    const maxSize = systemConfig.value.uploadFileMaxLimit * 1024 * 1024;
    if (file.size > maxSize) {
      message.error(`文件不能超过 ${systemConfig.value.uploadFileMaxLimit} MB`);
      return;
    }
    currentFile.value = file;
    startParsing(file);
  }
};

3. 调用 OCR 接口(Ant Design Vue 组件反馈)

const handleObtainFileContentText = async () => {
  if (!currentFile.value) return;
  
  const formData = new FormData();
  formData.append('file', currentFile.value);
  
  try {
    const response = await obtainFileContentText(formData);
    if (response.success) {
      sourceLanguageText.value = response.data.contentText;
      currentUploadStatus.value = 'success';
    } else {
      currentUploadStatus.value = 'failed';
      message.error(response.message || '解析失败');
    }
  } catch (error) {
    currentUploadStatus.value = 'failed';
    message.error('网络错误,请重试');
  }
};

💡 关键设计:状态分为 'idle' | 'parsing' | 'success' | 'failed',对应四个 UI 区块,避免用户困惑。


三、大模型动态集成:让选择“无感而智能”

AI 模型不是越多越好,而是要默认最优 + 动态适配

🔧 真实代码实现

1. 加载模型与语种字典

// 加载大模型列表
const loadLargeModelDictionary = async () => {
  const response = await findLargeModelDictionary({ keyword: '' });
  largeModelList.value = response.data;
  if (largeModelList.value.length > 0) {
    const firstModel = largeModelList.value[0];
    selectedModelId.value = firstModel.id;
    await loadLargeModelLanguageSupport(firstModel.id); // 关键:联动语种
  }
};

// 加载某模型支持的语种
const loadLargeModelLanguageSupport = async (modelId: string) => {
  const response = await findLargeModelLanguageSupport({ largeModelId: modelId });
  languageSupportList.value = response.data;
};

2. 行业领域初始化

const loadIndustrySectorDictionary = async () => {
  const response = await findIndustrySectorDictionary({ keyword: '' });
  industrySectorList.value = response.data;
  // 业务逻辑:若默认为 'general',则跳过
  if (defaultIndustrySector.value === 'general' && industrySectorList.value.length > 1) {
    defaultIndustrySector.value = industrySectorList.value[1].code;
  }
};

3. Ant Design Vue 下拉框绑定

<!-- 模型选择 -->
<a-select
  v-model:value="selectedModelId"
  style="width: 100%"
  @change="handleModelChange"
>
  <a-select-option
    v-for="item in largeModelList"
    :key="item.id"
    :value="item.id"
  >
    {{ item.name }}
  </a-select-option>
</a-select>

四、SSE 流式翻译:用 fetch-event-source 实现“打字机效果”

这是体验升级的核心!我们使用 @microsoft/fetch-event-source 替代原生 EventSource

🔧 完整流式翻译实现(结合你的项目结构)

import { fetchEventSource } from '@microsoft/fetch-event-source';

// 存储控制器,用于取消请求
let abortController: AbortController | null = null;

const handleTranslate = async () => {
  if (isTranslating.value) return;
  
  // 重置结果
  translationResult.value = '';
  isTranslating.value = true;
  
  // 创建 AbortController
  abortController = new AbortController();
  
  const params = new URLSearchParams({
    largeModelId: selectedModelId.value,
    sourceLangCode: selectedSourceLangCode.value,
    targetLangCode: selectedTargetLangCode.value,
    industrySector: defaultIndustrySector.value,
    enableDeepThinking: String(enableDeepThinking.value),
    content: sourceLanguageText.value.trim(),
  });

  try {
    await fetchEventSource(`/api/v1/large-model/translate/stream?${params}`, {
      method: 'GET',
      headers: {
        'Authorization': `Bearer ${getToken()}`, // 从 Pinia 或 localStorage 获取
      },
      signal: abortAssistant.signal, // 支持取消
      onmessage(event) {
        if (event.data) {
          translationResult.value += event.data;
          nextTick(() => {
            scrollToBottom();
          });
        }
      },
      onclose() {
        isTranslating.value = false;
      },
      onerror(err) {
        console.error('SSE Error:', err);
        message.error('翻译服务异常,请稍后重试');
        isTranslating.value = false;
        abortController?.abort(); // 主动关闭
      },
    });
  } catch (error) {
    if (abortController?.signal.aborted) {
      console.log('翻译已取消');
    } else {
      message.error('连接失败');
    }
    isTranslating.value = false;
  }
};

// 取消翻译
const cancelTranslation = () => {
  if (abortController) {
    abortController.abort();
    isTranslating.value = false;
    message.info('翻译已取消');
  }
};

🔧 自动滚动到底部

const scrollToBottom = () => {
  const container = document.querySelector('.translation-result-content');
  if (container) {
    container.scrollTop = container.scrollHeight;
  }
};

💡 为什么不用 WebSocket?

    • SSE 是 单向流(服务端 → 客户端),完美匹配“AI 生成文本”场景
    • fetch-event-source 提供 Promise 风格 API,更符合现代前端习惯

五、工程化亮点:13年经验的沉淀

✅ 1. 响应式布局(PC/Mobile 适配)

const updateMainHeight = () => {
  const mainEl = document.querySelector('.main') as HTMLElement;
  if (!mainEl) return;
  
  if (isMobile.value) {
    mainEl.style.minHeight = '100vh';
    mainEl.style.height = 'auto';
  } else {
    mainEl.style.height = '100vh';
    mainEl.style.minHeight = 'auto';
  }
};

✅ 2. 内存清理(防止泄漏)

onUnmounted(() => {
  if (abortController) {
    abortController.abort();
  }
  window.removeEventListener('resize', checkIsMobile);
});

✅ 3. 防重复提交 + 加载状态

<a-button
  type="primary"
  :loading="isTranslating"
  @click="handleTranslate"
  :disabled="!sourceLanguageText.trim()"
>
  {{ isTranslating ? '翻译中...' : '开始翻译' }}
</a-button>

结语:前端的价值,在“AI 与人之间”

这个项目让我确信:AI 时代,前端工程师的不可替代性在于“体验设计”

    • 我们用 fetch-event-source 把冰冷的 token 流变成温暖的“打字机”;
    • 我们用 Ant Design Vue 让复杂配置变得简单;
    • 我们用状态机管理 OCR 的每一步,不让用户迷失。

如果你也想从“切图仔”转型为“AI 应用架构者”,不妨从一个流式翻译工具开始。技术会过时,但解决问题的能力永远稀缺

项目技术栈:Vue 3.4 + Vite 5 + TypeScript + Ant Design Vue 4 + fetch-event-source
代码来源:本文所有逻辑均来自实际生产项目(已脱敏)
互动:你用过 fetch-event-source 吗?欢迎分享你的 SSE 实践!

 

posted @ 2026-02-02 14:14  奔跑的小蚂蚁9538  阅读(29)  评论(0)    收藏  举报