🎨 docs: 更新部署文档和口语页面样式
This commit is contained in:
parent
c8229ced26
commit
00f4447bc9
52
DEPLOY.md
52
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_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 # 本文档
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue