/blog/post-1785857613946/博客
READY
技术笔记

把 Vue 3 桌面翻译工具搬进微信小程序:一场架构、Prompt 与妥协的 48 小时实战

2026-08-04约 9 分钟

一句话总结:把年初写的一个 Vue 3 桌面翻译工具,完整搬进了微信小程序。
项目叫“译文岛”(Translation Island)。用户对着考研英语真题练翻译,AI 自动评分批改,错题自动沉淀,还能导出学习报告海报。原本是纯 Web + Capacitor 打包 APK,今天刚刚跑通了 uni-app 小程序版本的完整闭环。


背景:这个工具怎么来的

年初心血来潮想刷考研英语,发现市面上的翻译练习工具要么是机翻 + 死记硬背,要么缺乏系统性的批改反馈。于是花了两周搭了个原型:

  • Vue 3 + Element Plus 做了个三栏桌面端(范文列表 | 翻译区 | 评分面板)

  • 接 DeepSeek API 做了四维评分(准确性 + 语法 + 词汇 + 流畅度)

  • 移动端用 Capacitor 打包成 APK,当 window.innerWidth < 768 时自动切到 MobileApp.vue

为了省钱(DeepSeek API 按 token 计费),花了大量时间打磨 Prompt——要求 AI 输出结构化 JSON,用 mistakeWaves 数组记录每个错误的位置和类型,errorSpans 标红具体出错片段,而不是让 AI 写小作文。

原型跑起来后,朋友试了说“好用是好用,但要装 APK 太麻烦了,有没有小程序版?”——于是有了今天的故事。


架构:一套状态,三套壳

整个项目的架构一度让我很纠结,最后选了一个“非主流但很实用”的方案:

text

App.vue(4500 行 — 唯一状态持有者)
├── 所有数据、API 调用、评分逻辑、生词池……
├── provide('ett', 全量状态)          ← 注入给子组件
├── <template v-if="!isMobile">       ← 桌面端三栏布局
└── <MobileApp v-if="isMobile" />     ← 移动端三 Tab

没有用 Pinia、没有用 Vuex。 因为 MobileApp 是唯一消费者,provide/inject 零依赖、零配置,$ 前缀访问足够简洁。代价是高度耦合——改 App.vue 的 provide 接口必须同步检查 MobileApp.vue。

弹窗适配也是一个大坑。桌面端的 el-dialog 在 375dp 手机上根本没法用,键盘弹出后更灾难。最后搞了个“键盘感知二分法”:

  • 有 textarea 输入的 → 全屏遮罩

  • 纯浏览/搜索的 → 底部抽屉

这个二分法在后面的小程序迁移中居然又可以原样搬过去,因为小程序的交互模式天然就是全屏页面 + 底部抽屉。


今天的核心任务:小程序迁移

模板选的是 uni-app(Vue 3 模式),原因很简单:这个项目的状态管理、算法逻辑都是纯 JS,语法几乎零改动就能搬。

代码复用情况

仔细拆了下 App.vue 4265 行代码的复用度:

类别占比示例直接复制~45%LCS 句子对齐、JSON 提取、评分解析、Prompt 常量改造后复用~30%AI 调用(fetch → 云函数)、存储(localStorage → uni.storage)必须重写~25%桌面端模板、el-calendar、拖拽排序、图片导入

对于那 45% 的代码,基本是原样文件复制——utils/align.js、utils/parse.js、utils/format.js 直接拖进 miniapp/src/utils/。

最大的改造:AI 调用的安全兜底

原来 App.vue 里,用户的 DeepSeek API Key 是明文存在 localStorage 里的,前端直接 fetch DeepSeek:

javascript

// 原始版本(前端直接调 DeepSeek)
const res = await fetch('https://api.deepseek.com/v1/chat/completions', {
  headers: { 'Authorization': `Bearer ${apiKey}` },
  body: JSON.stringify({ messages, ... })
})

小程序版改成了双密钥 + 云函数代理的架构:

text

小程序 → uni.cloud.callFunction('callAI') → 云函数 → DeepSeek
                        ↑
              用户 Key 优先 → 平台兜底 Key
              (存云数据库 ai_keys,按 openid 隔离)

关键设计点:

  1. Key 永不回传客户端。 保存、测试、删除全走云函数,前端只知道“已配置/未配置”。

  2. 演示模式。 DEMO_MODE = true 时,所有 AI 调用返回内置模拟数据——无后端也能完整体验,比赛演示特别有用。

  3. Token 用量统计。 云函数每次调用后写入 ai_usage 集合,标记 keySource(user/platform),方便后续计费分析。

图片导入的两次踩坑

图片导入是这个项目最折腾的功能。用户拍照导入教辅资料,AI 自动提取英文原文和考点。

第一版方案:微信 OCR → DeepSeek 结构化

这个方案在标准印刷体截图上表现不错,但遇到百词斩长截图就翻车了:

  • 长截图压缩后文字模糊,OCR 精度急剧下降

  • UI 控件(播放器按钮、“完成阅读”按钮)被当作噪点识别

  • 图片中间嵌入的插图打断了上下文

最难受的是,cloud.openapi.ocr.printedText 本身就针对标准文档设计,对 App 截图里的 UI 文字兼容性很差。

第二版方案(今天加的):通义千问 VL 多模态模型

思路很简单——既然 OCR 读不了 UI 上的文字,那就让能“看懂图片”的模型直接读:

javascript

// 云函数新增 visionExtract 动作
async function handleVisionExtract(event) {
  const { images, userPrompt } = event
  // cloud:// fileID 转临时 HTTPS 链接
  const tempUrls = await resolveImageFileIDs(images)
  // 构建多模态消息
  const content = [{ type: 'text', text: userPrompt }]
  for (const url of tempUrls) {
    content.push({ type: 'image_url', image_url: { url, detail: 'high' } })
  }
  // 调用 qwen3-vl-plus,失败自动降级到 OCR
  try {
    const data = await callAI(apiKey, messages, { model: 'qwen3-vl-plus' })
    return { content: data.choices[0].message.content }
  } catch (e) {
    return handleImageOcr(event)  // 自动降级
  }
}

现在图片导入走了三级降级:视觉模型 → OCR → 报错。实测 qwen3-vl-plus 对 UI 截图中的文字识别率远超 OCR,还能理解图片布局(知道哪些是文章正文、哪些是 UI 控件)。


Prompt 工程:AI 输出的“类型安全”

这个项目最引以为豪的地方不是代码,而是 6 套精心打磨的 Prompt。每套 Prompt 都在追求同一个目标:让 AI 输出结构稳定、可程序化解析的 JSON。

举一个例子——图片导入的 Prompt 里有 9 条“核心铁律”:

text

1. raw_text_archive 必须 100% 还原截图所有文字(含 UI 文本、编号笔误)
2. segments 按意群切分,但严禁把完整句子拆碎
3. keyPoints 只放考点关键词(短语形式),禁止复制教辅长篇解析
4. 截图底部英文被截断时,必须根据句法逻辑自动补全
...
9. 若截图无英文文章内容,必须输出 {"error":"no_article"},严禁编造

这些规则的迭代过程非常血泪——早期版本 AI 会把选择题的 ABCD 选项混进正文、会把“参考译文”和原文搞反、会在 keyPoints 里塞 200 字的教辅解析。每踩一个坑就加一条铁律,现在的 Prompt 已经迭代到 v3。

评分 Prompt 的设计也很有讲究。早期让 AI 输出自由文本的反馈,用户看得累,程序也没法结构化处理。后来改成:

json

{
  "accuracy": 20, "grammar": 18, "vocabulary": 19, "fluency": 21,
  "total": 78,
  "mistakeWaves": [
    {
      "sentenceIndex": 0,
      "errorType": "修饰位置错误",
      "studentError": "学生写的错译片段",
      "patternEN": "对应的英文原文",
      "whereStuck": "为什么会卡住的白话解释",
      "nextTime": "下次怎么拆"
    }
  ],
  "translationErrors": [
    {
      "originalEN": "immediate family",
      "correctZH": "直系亲属",
      "studentZH": "短暂的家庭",
      "note": "固定搭配,immediate 不是'立即的'"
    }
  ]
}

这下前端就可以:

  • 根据 sentenceIndex 定位到具体句子标红

  • 在 errorSpans 里高亮对应字符

  • 把 translationErrors 自动收录进错题本

  • 按 errorType 分类统计薄弱点

一个重要经验:Prompt 工程本质上是在给 AI 输出做“类型约束”——就像 TypeScript 约束变量类型一样,你的 Prompt 就是 AI 输出的“类型系统”。


小程序版新增的“惊喜功能”

迁移过程中发现有些功能在小程序上反而比 APK 更好实现:

Canvas 海报生成

原来的 APK 版本用 ECharts 做图表,移动端卡得要命。小程序版直接用 Canvas 2D API 手绘了一张“动森风”的学习报告海报——周打卡曲线、四维评分雷达图、错题分布、名言警句,全部用一个 750×1334 的 Canvas 画出来。

Canvas 2D API 有个坑:必须先设宽高再 getContext('2d'),顺序反了画布就花。这个 bug 调了一上午。

错题本闭环

原来 Web 版的错题记录只是 localStorage 里的一个数组。小程序版设计了一个完整间隔复习系统:

  • 翻译错误 / 致命表达 / 生词 三种分类

  • 掌握标记后按 3/7 天间隔重新出现

  • 支持导出文本 / 长图 / PDF(云函数 pdfkit 生成)

  • PDF 里塞了 1.2MB 的 Noto Sans SC 中文字体,保证在任何设备上不乱码

云存储备份

原来用 @capacitor/filesystem 写文件备份,用户体验一般。小程序版直接用云存储 uploadFile,备份的 JSON 存云端,恢复时下载回来,不再依赖本地文件系统。


踩过的坑,希望你别踩

  1. uni-app 的构建权限问题
    在 Windows 受管环境下构建 uni 项目,偶尔会报 EPERM: operation not permitted, mkdir ...。解决方案是先关闭微信开发者工具(它的热重载监视器会锁住 dist/build 目录),再以管理员权限运行构建。

  2. 微信开发者工具的增量缓存
    遇到过最诡异的事:代码改了,但构建产物里的报错行号还是旧的。最后发现是微信开发者工具的增量缓存没更新。现在养成了“遇事不决先清缓存”的肌肉记忆。

  3. Canvas 2D 的 context 获取顺序

javascript

// ❌ 错误顺序:先设宽高再 getContext → 花屏
const ctx = canvas.getContext('2d')
canvas.width = 750
canvas.height = 1334

// ✅ 正确顺序
canvas.width = 750
canvas.height = 1334
const ctx = canvas.getContext('2d')
  1. wot-design-uni 的字体引用
    微信小程序 wxss 不支持本地 @font-face,而 wot-design-uni 的组件默认引用了图标字体。解决方案是写了个构建后脚本 fix-fonts.cjs,在每次 npm run build 后自动清理失效的 @font-face 声明。

  2. 云函数的图片临时链接
    cloud:// 格式的 fileID 不能直接发给视觉模型,需要先用 getTempFileURL 转成 HTTPS 临时链接。这个链接有时效性,不要缓存。


项目文件一瞥

text

miniapp/
├── cloudfunctions/
│   ├── callAI/            ← DeepSeek 代理 + 视觉模型 + OCR 降级
│   ├── sendCheckin/       ← 打卡定时推送
│   └── exportMistakesPdf/ ← 错题 PDF(内置中文子集字体)
├── src/
│   ├── pages/             ← essays(范文库)+ practice(练习)+ mine(我的)
│   ├── components/        ← 15+ 组件:错题本/学习报告/短语默写/词根分析……
│   ├── store/index.js     ← 全局状态(44KB,接管了原来 4500 行 App.vue 的能力)
│   ├── api/ai.js          ← 云函数调用封装 + 用户 Key 管理
│   ├── utils/             ← 纯 JS 工具(45% 代码原样搬运)
│   └── common/            ← Prompt 常量 + 内置范文 + 演示数据
└── project.config.json    ← AppID: wx833a895526e332d3

下一步计划

  • □

    范文库从 20 篇扩充到 50 篇(覆盖近十年真题)

  • □

    图片导入增加“裁切指导”提示,减少用户传整屏长截图

  • □

    错题本社区分享功能(学习报告一键生成分享卡片)

  • □

    接入更多视觉模型做 A/B 对比(qwen-vl vs glm-4v vs gpt-4o)

  • □

    小程序审核上线


一点感受

今天最大的体会是:好的代码架构可以被“降维打击”,差的架构会被“搬家累死”。

这个项目之所以能在 48 小时内完成小程序迁移,不是因为写得快,而是因为从一开始就把 UI 和逻辑分得够开——App.vue 里 70% 的纯逻辑代码直接复制过来就能跑。如果当初图省事把数据请求、评分计算、DOM 操作全搅在一起,今天就要面对完全重写的噩梦。

另一个感受:Prompt 工程是一门值得投入时间的技能。 一个好的 Prompt 可以省掉成百上千行的解析代码、容错代码、格式化代码。当 AI 的输出变得“类型安全”之后,整个系统的可靠性就上了一个台阶。


写于 2026 年 8 月 4 日深夜,翻译工具体验终于从“能用”走向“好用”。

关联项目

EnglishTranslationTool-mobile

基于Vue3+Capacitor的考研英语翻译练习Android应用,支持AI评分、多种训练模式。