一句话总结:把年初写的一个 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 隔离)
关键设计点:
-
Key 永不回传客户端。 保存、测试、删除全走云函数,前端只知道“已配置/未配置”。
-
演示模式。
DEMO_MODE = true时,所有 AI 调用返回内置模拟数据——无后端也能完整体验,比赛演示特别有用。 -
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 存云端,恢复时下载回来,不再依赖本地文件系统。
踩过的坑,希望你别踩
-
uni-app 的构建权限问题
在 Windows 受管环境下构建 uni 项目,偶尔会报EPERM: operation not permitted, mkdir ...。解决方案是先关闭微信开发者工具(它的热重载监视器会锁住dist/build目录),再以管理员权限运行构建。 -
微信开发者工具的增量缓存
遇到过最诡异的事:代码改了,但构建产物里的报错行号还是旧的。最后发现是微信开发者工具的增量缓存没更新。现在养成了“遇事不决先清缓存”的肌肉记忆。 -
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')
-
wot-design-uni 的字体引用
微信小程序 wxss 不支持本地@font-face,而wot-design-uni的组件默认引用了图标字体。解决方案是写了个构建后脚本fix-fonts.cjs,在每次npm run build后自动清理失效的@font-face声明。 -
云函数的图片临时链接
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 日深夜,翻译工具体验终于从“能用”走向“好用”。