🎨 docs: 更新部署文档和口语页面样式
This commit is contained in:
parent
c8229ced26
commit
00f4447bc9
64
DEPLOY.md
64
DEPLOY.md
|
|
@ -39,12 +39,13 @@ npm install
|
|||
|
||||
| 配置项 | 说明 |
|
||||
|--------|------|
|
||||
| `GRS_API_KEY` | GRS AI 接口密钥(作文批改 / 试题分析) |
|
||||
| `GRS_API_KEY` | GRS AI 接口密钥(作文批改) |
|
||||
| `DOUBAO_KEY` | 豆包 API 密钥 |
|
||||
| `DOUBAO_APP_ID` | 豆包语音 App ID |
|
||||
| `DOUBAO_ACCESS_TOKEN` | 豆包语音访问令牌 |
|
||||
| `DOUBAO_RESOURCE_ID` | 豆包 TTS 资源 ID |
|
||||
| `DOUBAO_ASR_RESOURCE_ID` | 豆包 ASR 资源 ID |
|
||||
| `ARK_API_KEY` | 火山引擎 Ark 大模型密钥 |
|
||||
| `ARK_API_KEY` | 火山引擎 Ark 大模型密钥(口语对话) |
|
||||
| `ARK_MODEL` | Ark 模型名称(如 doubao-pro-4k) |
|
||||
|
||||
### 2.2 跨域响应头要求(重要)
|
||||
|
|
@ -290,7 +291,7 @@ DOUBAO_APP_ID=xxx DOUBAO_ACCESS_TOKEN=xxx node server.js
|
|||
|
||||
### 方案 C:EdgeOne Pages / Vercel / Netlify(纯静态托管)
|
||||
|
||||
> **注意**:此类平台**不支持服务端代理**,`/tts-api`、`/ark-api`、`/dashscope-api`、`/asr-ws` 等接口将因跨域或缺少鉴权 Header 而失败。
|
||||
> **注意**:此类平台**不支持服务端代理**,`/tts-api`、`/ark-api`、`/dashscope-api`、`/asr-ws` 等接口将因跨域或缺少鉴权 Header 而失败。
|
||||
> 建议仅用于演示或配合独立后端服务使用。
|
||||
|
||||
#### 部署步骤(以 Vercel 为例)
|
||||
|
|
@ -321,14 +322,14 @@ DOUBAO_APP_ID=xxx DOUBAO_ACCESS_TOKEN=xxx node server.js
|
|||
|
||||
| 验证项 | 检查方法 |
|
||||
|--------|----------|
|
||||
| 页面正常加载 | 访问首页,确认 7 个功能卡片显示正常 |
|
||||
| 页面正常加载 | 访问首页,确认 6 个功能卡片显示正常 |
|
||||
| 路由刷新不 404 | 直接访问 `/pronunciation`、`/speaking` 等子路由 |
|
||||
| COOP/COEP 响应头 | 浏览器 DevTools → Network → 查看 index.html 响应头 |
|
||||
| TTS 语音合成 | 进入发音练习页,测试语音播放 |
|
||||
| ASR 语音识别 | 进入口语对话页,测试麦克风录音识别 |
|
||||
| 作文批改 | 进入作文批改页,提交一段英文作文 |
|
||||
| 视频讲解(FFmpeg) | 进入视频讲解页,上传视频文件测试 |
|
||||
| HTTPS 证书 | 确认浏览器地址栏显示锁形图标 |
|
||||
| 试题分析 | 进入试题分析页,上传图片或输入题目 |
|
||||
| 单词拼写练习 | 进入单词拼写页,测试拼写检测功能 |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -336,27 +337,27 @@ DOUBAO_APP_ID=xxx DOUBAO_ACCESS_TOKEN=xxx node server.js
|
|||
|
||||
### Q1:视频讲解页面报错 `SharedArrayBuffer is not defined`
|
||||
|
||||
**原因**:服务器未返回 `Cross-Origin-Opener-Policy: same-origin` 和 `Cross-Origin-Embedder-Policy: require-corp` 响应头。
|
||||
**原因**:服务器未返回 `Cross-Origin-Opener-Policy: same-origin` 和 `Cross-Origin-Embedder-Policy: require-corp` 响应头。
|
||||
**解决**:参考第二节 2.2,在 Nginx 或 Node.js 服务器中添加对应响应头。
|
||||
|
||||
### Q2:刷新页面出现 404
|
||||
|
||||
**原因**:Vue Router 使用 History 模式,服务器未配置路由回退。
|
||||
**原因**:Vue Router 使用 History 模式,服务器未配置路由回退。
|
||||
**解决**:Nginx 添加 `try_files $uri $uri/ /index.html;`,Node.js 添加通配路由返回 `index.html`。
|
||||
|
||||
### Q3:API 请求跨域报错(CORS)
|
||||
|
||||
**原因**:生产环境未配置反向代理,浏览器直接请求第三方 API 被跨域拦截。
|
||||
**原因**:生产环境未配置反向代理,浏览器直接请求第三方 API 被跨域拦截。
|
||||
**解决**:参考第二节 2.3,在 Nginx 或 Node.js 中配置对应代理规则。
|
||||
|
||||
### Q4:ASR 语音识别 WebSocket 连接失败
|
||||
|
||||
**原因**:代理未注入豆包 ASR 鉴权 Header,或 WebSocket 升级配置缺失。
|
||||
**原因**:代理未注入豆包 ASR 鉴权 Header,或 WebSocket 升级配置缺失。
|
||||
**解决**:确认 Nginx 配置中包含 `proxy_set_header Upgrade $http_upgrade;` 及三个 `X-Api-*` Header。
|
||||
|
||||
### Q5:构建时内存不足
|
||||
|
||||
**原因**:项目包含 FFmpeg WASM,构建产物较大。
|
||||
**原因**:项目包含 FFmpeg WASM,构建产物较大。
|
||||
**解决**:
|
||||
|
||||
```bash
|
||||
|
|
@ -366,7 +367,35 @@ NODE_OPTIONS=--max-old-space-size=4096 npm run build
|
|||
|
||||
---
|
||||
|
||||
## 七、目录结构参考
|
||||
## 七、页面路由清单
|
||||
|
||||
| 路由路径 | 页面名称 | 功能说明 |
|
||||
|----------|----------|----------|
|
||||
| `/` | 首页 | 功能导航卡片 |
|
||||
| `/pronunciation` | 发音练习 | TTS 语音合成,音标学习 |
|
||||
| `/speaking` | 口语对话 | ASR 语音识别,AI 对话 |
|
||||
| `/essay-correction` | 作文批改 | 图片/文本作文批改 |
|
||||
| `/exam-analysis` | 试题分析 | 图片题目分析解答 |
|
||||
| `/spell-practice` | 单词拼写 | 单词拼写练习检测 |
|
||||
|
||||
---
|
||||
|
||||
## 八、依赖版本参考
|
||||
|
||||
| 依赖包 | 版本 |
|
||||
|--------|------|
|
||||
| vue | ^3.5.30 |
|
||||
| vue-router | ^5.0.3 |
|
||||
| vite | ^8.0.0 |
|
||||
| @ffmpeg/ffmpeg | ^0.12.15 |
|
||||
| @ffmpeg/util | ^0.12.2 |
|
||||
| axios | ^1.13.6 |
|
||||
| marked | ^17.0.5 |
|
||||
| pako | ^2.1.0 |
|
||||
|
||||
---
|
||||
|
||||
## 九、目录结构参考
|
||||
|
||||
```
|
||||
AI_Demo/
|
||||
|
|
@ -374,7 +403,16 @@ AI_Demo/
|
|||
├── src/
|
||||
│ ├── config/index.js # API 密钥配置(部署前确认)
|
||||
│ ├── router/index.js # 路由定义
|
||||
│ └── views/ # 页面组件
|
||||
│ ├── views/ # 页面组件
|
||||
│ │ ├── HomePage.vue # 首页
|
||||
│ │ ├── Pronunciation.vue # 发音练习
|
||||
│ │ ├── Speaking.vue # 口语对话
|
||||
│ │ ├── EssayCorrection.vue # 作文批改
|
||||
│ │ ├── ExamAnalysis.vue # 试题分析
|
||||
│ │ └── SpellPractice.vue # 单词拼写
|
||||
│ ├── components/ # 公共组件
|
||||
│ ├── assets/ # 静态资源
|
||||
│ └── MainLayout.vue # 布局组件
|
||||
├── vite.config.js # 开发代理配置(生产环境需在服务器复现)
|
||||
├── package.json
|
||||
└── DEPLOY.md # 本文档
|
||||
|
|
|
|||
|
|
@ -189,7 +189,8 @@ const scrollToBottom = async () => {
|
|||
};
|
||||
|
||||
// TTS 合成并播放
|
||||
const synthesizeAndPlay = async (text, msgId) => {
|
||||
// autoPlay: 是否自动播放,默认为 true
|
||||
const synthesizeAndPlay = async (text, msgId, autoPlay = true) => {
|
||||
const payload = {
|
||||
user: { uid: "speaking_" + Date.now() },
|
||||
req_params: {
|
||||
|
|
@ -264,7 +265,9 @@ const synthesizeAndPlay = async (text, msgId) => {
|
|||
const msg = messages.value.find((m) => m.id === msgId);
|
||||
if (msg) msg.audioUrl = url;
|
||||
|
||||
playAudio(url, msgId);
|
||||
if (autoPlay) {
|
||||
playAudio(url, msgId);
|
||||
}
|
||||
};
|
||||
|
||||
// 播放音频
|
||||
|
|
@ -345,8 +348,8 @@ const sendMessage = async () => {
|
|||
messages.value.push(userMsg);
|
||||
await scrollToBottom();
|
||||
|
||||
// 用户消息 TTS(不阻塞主流程)
|
||||
synthesizeAndPlay(text, userMsgId).catch((e) => console.error("User TTS error:", e));
|
||||
// 用户消息 TTS(不阻塞主流程,不自动播放)
|
||||
synthesizeAndPlay(text, userMsgId, false).catch((e) => console.error("User TTS error:", e));
|
||||
|
||||
// 插入 AI loading 占位
|
||||
const aiMsgId = ++msgIdCounter;
|
||||
|
|
@ -643,8 +646,17 @@ const startRecording = async () => {
|
|||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
if (currentAudioInstance) { currentAudioInstance.pause(); currentAudioInstance = null; }
|
||||
// 停止音频播放
|
||||
if (currentAudioInstance) {
|
||||
currentAudioInstance.pause();
|
||||
currentAudioInstance = null;
|
||||
}
|
||||
// 重置所有消息的播放状态
|
||||
messages.value.forEach((m) => (m.isPlaying = false));
|
||||
// 释放 blob URL
|
||||
blobUrls.forEach((url) => URL.revokeObjectURL(url));
|
||||
blobUrls.length = 0;
|
||||
// 停止录音
|
||||
stopRecording();
|
||||
});
|
||||
</script>
|
||||
|
|
@ -703,21 +715,20 @@ onUnmounted(() => {
|
|||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
<span v-else>{{ msg.content }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 消息播放按钮(AI 和用户消息均显示) -->
|
||||
<div v-if="!msg.isLoading && msg.content" class="audio-controls" :class="{ 'user-audio-controls': msg.role === 'user' }">
|
||||
<span v-if="!msg.audioUrl" class="tts-hint">合成中...</span>
|
||||
<button v-else class="play-btn" :class="{ playing: msg.isPlaying }" @click="replayMessage(msg)" :title="msg.isPlaying ? '暂停' : '播放'">
|
||||
<!-- 播放中:音波动画 -->
|
||||
<div v-if="msg.isPlaying" class="wave-bars">
|
||||
<span></span><span></span><span></span><span></span>
|
||||
</div>
|
||||
<!-- 未播放:播放图标 -->
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.347a1.125 1.125 0 0 1 0 1.972l-11.54 6.347a1.125 1.125 0 0 1-1.667-.986V5.653Z" />
|
||||
</svg>
|
||||
</button>
|
||||
<!-- 消息播放按钮(AI 和用户消息均显示) -->
|
||||
<div v-if="!msg.isLoading && msg.content" class="audio-controls" :class="{ 'user-audio-controls': msg.role === 'user' }">
|
||||
<span v-if="!msg.audioUrl" class="tts-hint">合成中...</span>
|
||||
<button v-else class="play-btn" :class="{ playing: msg.isPlaying }" @click="replayMessage(msg)" :title="msg.isPlaying ? '暂停' : '播放'">
|
||||
<!-- 播放中:音波动画 -->
|
||||
<div v-if="msg.isPlaying" class="wave-bars">
|
||||
<span></span><span></span><span></span><span></span>
|
||||
</div>
|
||||
<!-- 未播放:播放图标 -->
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.347a1.125 1.125 0 0 1 0 1.972l-11.54 6.347a1.125 1.125 0 0 1-1.667-.986V5.653Z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -901,7 +912,11 @@ onUnmounted(() => {
|
|||
align-items: flex-end;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.message-row.user { flex-direction: row-reverse; }
|
||||
.message-row.user {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.message-row.user .avatar { order: 2; }
|
||||
.message-row.user .bubble-wrap { order: 1; }
|
||||
|
||||
.avatar {
|
||||
width: 38px;
|
||||
|
|
@ -914,24 +929,27 @@ onUnmounted(() => {
|
|||
justify-content: center;
|
||||
font-size: 1.2rem;
|
||||
flex-shrink: 0;
|
||||
order: 0;
|
||||
}
|
||||
.user-avatar { background: rgba(139,92,246,0.15); border-color: rgba(139,92,246,0.3); }
|
||||
|
||||
.bubble-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
max-width: 70%;
|
||||
order: 1;
|
||||
}
|
||||
.message-row.user .bubble-wrap { align-items: flex-end; }
|
||||
|
||||
.bubble {
|
||||
padding: 0.875rem 1.125rem;
|
||||
padding-bottom: 2.25rem;
|
||||
border-radius: 18px;
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
word-break: break-word;
|
||||
animation: bubbleIn 0.25s ease-out;
|
||||
position: relative;
|
||||
}
|
||||
.bubble.assistant {
|
||||
background: var(--card-bg);
|
||||
|
|
@ -972,6 +990,9 @@ onUnmounted(() => {
|
|||
|
||||
/* Audio Controls */
|
||||
.audio-controls {
|
||||
position: absolute;
|
||||
bottom: 0.5rem;
|
||||
right: 0.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
|
|
|||
Loading…
Reference in New Issue