🎨 docs: 更新部署文档和口语页面样式

This commit is contained in:
cc 2026-03-23 20:29:04 +08:00
parent c8229ced26
commit 00f4447bc9
2 changed files with 94 additions and 35 deletions

View File

@ -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_APP_ID` | 豆包语音 App ID |
| `DOUBAO_ACCESS_TOKEN` | 豆包语音访问令牌 | | `DOUBAO_ACCESS_TOKEN` | 豆包语音访问令牌 |
| `DOUBAO_RESOURCE_ID` | 豆包 TTS 资源 ID | | `DOUBAO_RESOURCE_ID` | 豆包 TTS 资源 ID |
| `DOUBAO_ASR_RESOURCE_ID` | 豆包 ASR 资源 ID | | `DOUBAO_ASR_RESOURCE_ID` | 豆包 ASR 资源 ID |
| `ARK_API_KEY` | 火山引擎 Ark 大模型密钥 | | `ARK_API_KEY` | 火山引擎 Ark 大模型密钥(口语对话) |
| `ARK_MODEL` | Ark 模型名称(如 doubao-pro-4k | | `ARK_MODEL` | Ark 模型名称(如 doubao-pro-4k |
### 2.2 跨域响应头要求(重要) ### 2.2 跨域响应头要求(重要)
@ -321,14 +322,14 @@ DOUBAO_APP_ID=xxx DOUBAO_ACCESS_TOKEN=xxx node server.js
| 验证项 | 检查方法 | | 验证项 | 检查方法 |
|--------|----------| |--------|----------|
| 页面正常加载 | 访问首页,确认 7 个功能卡片显示正常 | | 页面正常加载 | 访问首页,确认 6 个功能卡片显示正常 |
| 路由刷新不 404 | 直接访问 `/pronunciation`、`/speaking` 等子路由 | | 路由刷新不 404 | 直接访问 `/pronunciation`、`/speaking` 等子路由 |
| COOP/COEP 响应头 | 浏览器 DevTools → Network → 查看 index.html 响应头 | | COOP/COEP 响应头 | 浏览器 DevTools → Network → 查看 index.html 响应头 |
| TTS 语音合成 | 进入发音练习页,测试语音播放 | | TTS 语音合成 | 进入发音练习页,测试语音播放 |
| ASR 语音识别 | 进入口语对话页,测试麦克风录音识别 | | ASR 语音识别 | 进入口语对话页,测试麦克风录音识别 |
| 作文批改 | 进入作文批改页,提交一段英文作文 | | 作文批改 | 进入作文批改页,提交一段英文作文 |
| 视频讲解FFmpeg | 进入视频讲解页,上传视频文件测试 | | 试题分析 | 进入试题分析页,上传图片或输入题目 |
| HTTPS 证书 | 确认浏览器地址栏显示锁形图标 | | 单词拼写练习 | 进入单词拼写页,测试拼写检测功能 |
--- ---
@ -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/ AI_Demo/
@ -374,7 +403,16 @@ AI_Demo/
├── src/ ├── src/
│ ├── config/index.js # API 密钥配置(部署前确认) │ ├── config/index.js # API 密钥配置(部署前确认)
│ ├── router/index.js # 路由定义 │ ├── router/index.js # 路由定义
│ └── views/ # 页面组件 │ ├── views/ # 页面组件
│ │ ├── HomePage.vue # 首页
│ │ ├── Pronunciation.vue # 发音练习
│ │ ├── Speaking.vue # 口语对话
│ │ ├── EssayCorrection.vue # 作文批改
│ │ ├── ExamAnalysis.vue # 试题分析
│ │ └── SpellPractice.vue # 单词拼写
│ ├── components/ # 公共组件
│ ├── assets/ # 静态资源
│ └── MainLayout.vue # 布局组件
├── vite.config.js # 开发代理配置(生产环境需在服务器复现) ├── vite.config.js # 开发代理配置(生产环境需在服务器复现)
├── package.json ├── package.json
└── DEPLOY.md # 本文档 └── DEPLOY.md # 本文档

View File

@ -189,7 +189,8 @@ const scrollToBottom = async () => {
}; };
// TTS // TTS
const synthesizeAndPlay = async (text, msgId) => { // autoPlay: true
const synthesizeAndPlay = async (text, msgId, autoPlay = true) => {
const payload = { const payload = {
user: { uid: "speaking_" + Date.now() }, user: { uid: "speaking_" + Date.now() },
req_params: { req_params: {
@ -264,7 +265,9 @@ const synthesizeAndPlay = async (text, msgId) => {
const msg = messages.value.find((m) => m.id === msgId); const msg = messages.value.find((m) => m.id === msgId);
if (msg) msg.audioUrl = url; if (msg) msg.audioUrl = url;
if (autoPlay) {
playAudio(url, msgId); playAudio(url, msgId);
}
}; };
// //
@ -345,8 +348,8 @@ const sendMessage = async () => {
messages.value.push(userMsg); messages.value.push(userMsg);
await scrollToBottom(); await scrollToBottom();
// TTS // TTS
synthesizeAndPlay(text, userMsgId).catch((e) => console.error("User TTS error:", e)); synthesizeAndPlay(text, userMsgId, false).catch((e) => console.error("User TTS error:", e));
// AI loading // AI loading
const aiMsgId = ++msgIdCounter; const aiMsgId = ++msgIdCounter;
@ -643,8 +646,17 @@ const startRecording = async () => {
}; };
onUnmounted(() => { 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.forEach((url) => URL.revokeObjectURL(url));
blobUrls.length = 0;
//
stopRecording(); stopRecording();
}); });
</script> </script>
@ -703,8 +715,6 @@ onUnmounted(() => {
<span></span><span></span><span></span> <span></span><span></span><span></span>
</div> </div>
<span v-else>{{ msg.content }}</span> <span v-else>{{ msg.content }}</span>
</div>
<!-- 消息播放按钮AI 和用户消息均显示 --> <!-- 消息播放按钮AI 和用户消息均显示 -->
<div v-if="!msg.isLoading && msg.content" class="audio-controls" :class="{ 'user-audio-controls': msg.role === 'user' }"> <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> <span v-if="!msg.audioUrl" class="tts-hint">合成中...</span>
@ -720,6 +730,7 @@ onUnmounted(() => {
</button> </button>
</div> </div>
</div> </div>
</div>
<!-- 用户头像 --> <!-- 用户头像 -->
<div v-if="msg.role === 'user'" class="avatar user-avatar">🧑💻</div> <div v-if="msg.role === 'user'" class="avatar user-avatar">🧑💻</div>
@ -901,7 +912,11 @@ onUnmounted(() => {
align-items: flex-end; align-items: flex-end;
gap: 0.75rem; 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 { .avatar {
width: 38px; width: 38px;
@ -914,24 +929,27 @@ onUnmounted(() => {
justify-content: center; justify-content: center;
font-size: 1.2rem; font-size: 1.2rem;
flex-shrink: 0; flex-shrink: 0;
order: 0;
} }
.user-avatar { background: rgba(139,92,246,0.15); border-color: rgba(139,92,246,0.3); } .user-avatar { background: rgba(139,92,246,0.15); border-color: rgba(139,92,246,0.3); }
.bubble-wrap { .bubble-wrap {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.4rem;
max-width: 70%; max-width: 70%;
order: 1;
} }
.message-row.user .bubble-wrap { align-items: flex-end; } .message-row.user .bubble-wrap { align-items: flex-end; }
.bubble { .bubble {
padding: 0.875rem 1.125rem; padding: 0.875rem 1.125rem;
padding-bottom: 2.25rem;
border-radius: 18px; border-radius: 18px;
font-size: 1rem; font-size: 1rem;
line-height: 1.6; line-height: 1.6;
word-break: break-word; word-break: break-word;
animation: bubbleIn 0.25s ease-out; animation: bubbleIn 0.25s ease-out;
position: relative;
} }
.bubble.assistant { .bubble.assistant {
background: var(--card-bg); background: var(--card-bg);
@ -972,6 +990,9 @@ onUnmounted(() => {
/* Audio Controls */ /* Audio Controls */
.audio-controls { .audio-controls {
position: absolute;
bottom: 0.5rem;
right: 0.75rem;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;