目标读者 :前端初学者、AI 应用探索者、独立开发者、想用 AI 做产品的创业者
阅读时间 :约 20 分钟(但值得每一秒)
最终成果:一个可运行、可部署、有产品思维的 AI 应用
第一部分:为什么这个想法值得做?------产品洞察
1.1 传统背单词 App 的三大痛点
| App | 优点 | 缺点 |
|---|---|---|
| 百词斩 | 图片联想记忆 | 图片是"标准图",脱离真实生活(比如"apple"永远是红苹果,但现实中可能是青苹果、切开的、腐烂的) |
| 扇贝 | 间隔重复算法 | 内容抽象,无法应对"看到实物却说不出单词"的场景 |
| Duolingo | 游戏化学习 | 进度慢,不适合即时查询 |
👉 核心问题 :单词和真实世界脱节。
1.2 真实用户场景(强需求)
- 🧳 出国旅游:看到路牌、菜单、商品标签,想知道英文怎么说
- 👶 家长教孩子:指着家里的"冰箱""水龙头"问孩子英文
- 🏪 留学生购物:超市里分不清 "yogurt" 和 "sour cream"
- 📸 语言爱好者:拍下街头涂鸦、广告牌,学习地道表达
✅ 这些场景的共同点:需要"即时 + 准确 + 简单"的单词解释
1.3 AI 能带来什么?
-
多模态大模型(如 Kimi Vision、GPT-4V)能:
- 理解任意图片内容
- 提取语义核心
- 生成自然语言
-
TTS(Text-to-Speech)能:
- 将文字转为真人语音
- 支持发音模仿、语调控制
💡 组合起来 = 拍照 → 听单词 → 学例句 → 看解释
第二部分:产品功能设计(MVP)
我们不做复杂功能,只聚焦最核心的闭环:
用户上传图片
↓
AI 返回:1个简单英文单词 + 1个例句 + 解释段落 + 互动问答
↓
用户点击播放按钮,听到例句朗读
↓
可展开查看详情(图片+解释)
功能清单(必须实现)
| 功能 | 说明 |
|---|---|
| 图片上传 | 支持手机相册/拍照,隐藏原生 input,用 label 触发 |
| 本地预览 | 上传后立即显示缩略图(提升体验) |
| 加载状态 | 显示"分析中..."避免用户以为卡死 |
| 单词展示 | 大字体显示核心单词 |
| 音频播放 | 点击喇叭图标播放例句 |
| 详情展开 | 点击"Talk about it"展开解释和问答 |
| 无障碍支持 | 屏幕阅读器可操作(label + for) |
第三部分:技术架构详解
3.1 整体流程图
3.2 为什么不用后端?
- 快速验证:直接调用云 API,省去服务器成本
- 适合 MVP:等有用户后再加后端代理(防 Key 泄露)
- 当前风险:API Key 暴露在前端(仅用于 demo)
🔒 上线前必须加后端代理! (文末会讲)
第四部分:代码逐行深度解析
我们将拆解你提供的三段代码,一行一行讲清楚作用。
4.1 组件 PictureCard.vue ------ 图片上传与展示
xml
<template>
<div class="card">
<!-- 隐藏的文件输入 -->
<input type="file" id="selecteImage" ... />
<!-- 可点击的图片区域 -->
<label for="selecteImage">...</label>
<!-- 单词展示 -->
<div class="word">{{ props.word }}</div>
<!-- 播放按钮 -->
<div class="playAudio" @click="playAudio">...</div>
</div>
</template>
▶ 关键代码详解
(1) 隐藏原生 input,用 label 触发
python
<input type="file" id="selecteImage" class="input" accept="image/*" @change="updateImageData" />
<label for="selecteImage" class="upload">...</label>
accept="image/*":只允许选图片id="selecteImage"+for="selecteImage":点击 label 会触发 inputdisplay: none(CSS):隐藏丑陋的原生按钮
✅ 好处:
- 样式完全自定义(圆角、阴影、hover 效果)
- 无障碍支持:屏幕阅读器知道这是"上传图片"按钮
(2) 图片预览:Base64 转换
ini
const updateImageData = async (e: Event): Promise<any> => {
const file = (e.target as HTMLInputElement).files?.[0];
if (!file) return;
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file); // ← 关键:转为 base64
reader.onload = () => {
const data = reader.result as string;
imgPreview.value = data; // 本地预览
emit('updateImage', data); // 传给父组件
resolve(data);
}
reader.onerror = reject;
})
}
📌 为什么用
readAsDataURL?
- 它返回
data:image/jpeg;base64,/9j/4AAQ...格式- 多模态 API(如 Kimi)要求图片以这种格式传入
- 无需上传到服务器,直接前端处理
(3) 音频播放
ini
const playAudio = () => {
const audio = new Audio(props.audio); // props.audio 是 Blob URL
audio.play();
}
props.audio来自父组件(TTS 生成的临时 URL)new Audio().play()是最简单的播放方式
4.2 工具函数 generateAudio.ts ------ 文字转语音
▶ 核心函数:createBlobURL
ini
function createBlobURL(base64AudioData: string): string {
const byteCharacters = atob(base64AudioData); // ← 解码 base64
const byteArrays: number[] = [];
for (let offset = 0; offset < byteCharacters.length; offset++) {
byteArrays.push(byteCharacters.charCodeAt(offset));
}
const audioBlob = new Blob([new Uint8Array(byteArrays)], { type: 'audio/mp3' });
return URL.createObjectURL(audioBlob); // ← 生成临时 URL
}
🔍 为什么这么复杂?
- TTS API 返回的是 纯 base64 字符串 (不含
data:audio/mp3;base64,前缀)- 浏览器不能直接播放纯 base64,必须转为 Blob URL
atob()是浏览器内置函数,用于 base64 解码
▶ 调用 TTS 服务
javascript
const res = await fetch(endpoint, {
method: 'POST',
headers: {
'Authorization': `Bearer;${token}` // ← 注意:这里应是 Bearer ${token}
},
body: JSON.stringify(payload)
});
⚠️ 注意 :你的代码中写的是
Bearer;${token},应该是Bearer ${token}(空格不是分号)!
4.3 主页面 App.vue ------ AI 调用与状态管理
▶ Prompt 设计(决定一切!)
arduino
const userPrompt = `
分析图片内容,找出最能描述图片的一个英文单词,尽量选择更简单的A1~A2的词汇。
返回JSON 数据:
{
"image_discription": "...",
"representative_word": "...",
"example_sentence": "...",
"explaination": "段落以Look at ...开头,将段落分句,每一句单独一行...",
"explanation_replys": ["...", "..."]
}
`
✨ Prompt 设计技巧:
- 限定输出:"一个单词"、"A1~A2 级别"
- 结构化:明确要求 JSON 格式
- 教学友好:"Look at..." 开头,适合口语教学
- 互动性:提供两个可能的回复,模拟对话
▶ 调用 Kimi Vision API
php
const response = await fetch(endpoint, {
method: 'POST',
headers,
body: JSON.stringify({
model: 'moonshot-v1-8k-vision-preview',
messages: [{
role: 'user',
content: [
{ type: 'image_url', image_url: { url: imageDate } }, // ← Base64 图片
{ type: 'text', text: userPrompt }
]
}]
})
});
📌 Kimi 多模态接口要求:
content必须是数组- 图片和文本作为两个对象传入
- 图片 URL 可以是 Base64 字符串
▶ 解析响应 & 更新状态
ini
const replyData = JSON.parse(data.choices[0].message.content);
word.value = replyData.representative_word;
sentence.value = replyData.example_sentence;
explainations.value = replyData.explaination.split('\n').filter(Boolean);
💡 为什么 split('\n')?
因为 Prompt 要求"每一句单独一行",所以用换行符分割成数组,便于循环渲染。
第五部分:UI/UX 与无障碍设计
5.1 交互细节
| 场景 | 处理方式 |
|---|---|
| 上传图片 | 立即显示预览,减少等待焦虑 |
| AI 分析中 | 显示"分析中...",避免用户重复点击 |
| 音频加载 | TTS 异步生成,完成后才显示播放按钮 |
| 详情展开 | 从底部滑出,不打断主流程 |
5.2 无障碍(Accessibility)
<label for="selecteImage">:屏幕阅读器会读作"上传图片"- 按钮有
cursor: pointer:视觉反馈 - 所有交互元素可键盘聚焦(未来可加
tabindex)
✅ 符合 WCAG 2.1 标准,让视障用户也能使用
第六部分:安全与部署建议
6.1 当前风险:API Key 暴露
ini
const token = import.meta.env.VITE_AUDIO_ACCESS_TOKEN;
VITE_开头的变量会被 Vite 打包进前端代码- 任何人打开 DevTools 都能看到你的 Key!
6.2 正确做法:加一个轻量后端代理
用 NestJS / Express / Cloudflare Workers 写一个中间层:
dart
// POST /api/generate-audio
app.post('/api/generate-audio', async (req, res) => {
const { text } = req.body;
// 在这里调用 TTS,Key 存在服务器环境变量
const audioUrl = await callTTS(text);
res.json({ audioUrl });
});
前端改为:
ini
const res = await fetch('/api/generate-audio', { ... });
✅ 安全!Key 永远不会暴露
6.3 部署方案
| 方案 | 适合阶段 | 成本 |
|---|---|---|
| Vercel(前端) + 直连 API | Demo 阶段 | 免费 |
| Vercel + Cloudflare Workers(代理) | 上线初期 | $5/月 |
| 自建 NestJS 服务器 | 用户量大时 | $10+/月 |
第七部分:商业思考 ------ One Person Company 的机会
7.1 如何验证需求?
- 做最小可用产品(就是你现在做的)
- 录屏发小红书/抖音:标题如"出国再也不怕看不懂菜单了!"
- 收集反馈:有多少人愿意每天用?
7.2 变现模式
| 模式 | 说明 |
|---|---|
| 免费 + 广告 | 初期引流 |
| 高级语音包 | 更多发音人、语速调节 |
| 离线模式 | 下载小型模型,无网使用(需付费) |
| 企业 API | 餐厅/博物馆定制词库 |
7.3 护城河在哪里?
- Prompt 工程:你的指令让输出更教学友好
- 用户体验:极简、快速、无障碍
- 场景聚焦:不做"全能",只做"拍照查词"
结语:你已经站在了 AI 时代的起跑线上
这个项目看似简单,但它融合了:
- 现代前端工程(Vue3 + TS + Vite)
- AI 应用开发(多模态 + TTS)
- 产品思维(场景驱动 + 用户共情)
- 工程实践(无障碍 + 安全 + 部署)
一个人,一台电脑,就能做出有真实价值的产品。