docs:小说列表页/详情页展示改造设计文档
修复详情页重复展示问题,列表页增加大纲预览,后端保存澄清问答消息。 详情页复刻生成时 ScriptView 的 chat-bubble 展示结构。
This commit is contained in:
@@ -0,0 +1,298 @@
|
||||
---
|
||||
author: huazhongmin
|
||||
created_at: 2026-07-26
|
||||
purpose: 小说列表页和详情页展示改造设计 — 修复详情页重复展示、列表页增加大纲预览、后端保存澄清问答消息
|
||||
---
|
||||
|
||||
# 小说列表页 / 详情页展示改造设计
|
||||
|
||||
## 1. 背景与问题
|
||||
|
||||
### 1.1 当前问题
|
||||
|
||||
**详情页重复展示**:`ScriptDetailView.vue` 当前结构存在内容重复:
|
||||
- hero-card 显示 `script.summary`(正文前 90 字 — 小说正文开头)
|
||||
- tabs 正文 tab 显示 `script.content`(完整小说正文)
|
||||
- hero-card 显示 `userWish = script.theme`(用户心愿)
|
||||
- 结果:用户看到小说正文展示了两次(summary + fullContent),加上用户心愿,看起来"重复展示了两遍"
|
||||
|
||||
**列表页缺少大纲**:`ScriptLibraryView.vue` 当前只显示标题、标签、正文摘要,没有展示小说的大纲结构。
|
||||
|
||||
**澄清问答未保存**:后端 `saveNovelResult` 只创建 3 条 message(system 欢迎、user 心愿、assistant 小说正文)。生成过程中的澄清问答(3 轮 Q/A)没有保存为 message,导致详情页无法还原完整生成流程。
|
||||
|
||||
### 1.2 核心设计原则
|
||||
|
||||
> **"生成的时候是什么样展示的,后续进去详情页面就要怎么样展示"**
|
||||
|
||||
详情页必须完全复刻生成时 `ScriptView.vue` 的 chat-bubble 展示结构,不发明新 UI。
|
||||
|
||||
---
|
||||
|
||||
## 2. 改造范围
|
||||
|
||||
| 模块 | 改造内容 | 复杂度 |
|
||||
|---|---|---|
|
||||
| 后端 `ShortNovelServiceImpl` | 保存 clarification_question / clarification_answer / outline 消息到 conversation.messages | 高 |
|
||||
| 后端 新增 API | `/conversation/{conversationId}/messages` 返回消息列表 | 低 |
|
||||
| 前端 `ScriptDetailView.vue` | 移除 hero-card + tabs,改为 chat-bubble 流程视图(复用 ScriptView 结构) | 高 |
|
||||
| 前端 `ScriptLibraryView.vue` | 新增大纲预览行 | 低 |
|
||||
| 前端 store/service | 新增 `fetchConversationMessages` 函数 | 低 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 数据层改造
|
||||
|
||||
### 3.1 后端 Message 保存(ShortNovelServiceImpl)
|
||||
|
||||
**现状**:`saveNovelResult` 只在 `novel_done` 时创建 3 条 message。澄清问答没有保存。
|
||||
|
||||
**改造**:在 SSE 转发逻辑中增加 message 保存:
|
||||
|
||||
| SSE 事件 | 触发的 message 保存 |
|
||||
|---|---|
|
||||
| `clarification_card` | 创建 assistant message(type=`clarification_question`, sender=`assistant`, content=问题 JSON) |
|
||||
| followup `answer_clarification` | 创建 user message(type=`clarification_answer`, sender=`user`, content=用户答案) |
|
||||
| `outline_created` | 创建 assistant message(type=`outline`, sender=`assistant`, content=大纲 JSON 摘要) |
|
||||
| `novel_done` | 保持现有 3 条 message 不变(system 欢迎 + user 心愿 + assistant 小说正文) |
|
||||
|
||||
**messageOrder 策略**:按事件到达顺序递增。每个 message 用 `snowflakeIdGenerator.nextIdAsString()` 生成 ID。
|
||||
|
||||
**message metadata**:clarification_question 消息的 metadata 字段存储完整的 card JSON(包含 question、options 等),用于详情页还原 ClarificationCard 组件。
|
||||
|
||||
### 3.2 前端 API 层
|
||||
|
||||
**新增**:`getMessagesByConversation(conversationId)` — 调用 `/conversation/{conversationId}/messages` 返回按 messageOrder 排序的 message 列表。
|
||||
|
||||
**后端新增接口**:
|
||||
```java
|
||||
@GetMapping("/conversation/{conversationId}/messages")
|
||||
public Result<List<MessageResponse>> getMessages(@PathVariable String conversationId)
|
||||
```
|
||||
|
||||
**注意**:需检查是否已有类似接口(如 `listMessagesByConversation` in scriptChat.js)。如已存在则复用。
|
||||
|
||||
### 3.3 前端 Store 层
|
||||
|
||||
**新增**:`fetchConversationMessages(conversationId)` — 调用 API 并返回消息列表。
|
||||
|
||||
---
|
||||
|
||||
## 4. 详情页改造(ScriptDetailView.vue)
|
||||
|
||||
### 4.1 核心思路
|
||||
|
||||
**不发明新 UI,直接复用 ScriptView.vue 的 chat-bubble 结构**:
|
||||
|
||||
| 生成时 ScriptView | 详情页 ScriptDetailView |
|
||||
|---|---|
|
||||
| `ClarificationCard` 组件(可交互) | 复用 `ClarificationCard` 组件(只读模式) |
|
||||
| `kind: 'outline'` beats 渲染 | 复用相同 beats 渲染逻辑 |
|
||||
| `kind: 'novel'` novel-text 渲染 | 升级为 Markdown 渲染(详情页不需要流式) |
|
||||
| chat-bubble user/system 样式 | 完全复用相同样式 |
|
||||
| chat-input-bar(用户输入) | 移除(详情页只读) |
|
||||
| outline 修改/确认按钮 | 移除(详情页只读) |
|
||||
|
||||
### 4.2 数据加载与消息重建
|
||||
|
||||
扩展 ScriptView.vue 已有的"从历史剧本重建 resultMessages"逻辑(line 1449-1500),让它完整还原生成时的消息流:
|
||||
|
||||
```javascript
|
||||
const rebuildResultMessages = async (script, messages) => {
|
||||
const result = []
|
||||
|
||||
// 1. 用户心愿(首条 user 消息)
|
||||
if (script.theme) {
|
||||
result.push({ role: 'user', kind: 'text', content: script.theme })
|
||||
}
|
||||
|
||||
// 2. 按 messageOrder 遍历 messages,转换为对应 kind
|
||||
messages.forEach(m => {
|
||||
if (m.type === 'clarification_question') {
|
||||
result.push({ role: 'assistant', kind: 'card', card: parseCard(m) })
|
||||
} else if (m.type === 'clarification_answer') {
|
||||
const lastCard = [...result].reverse().find(x => x.kind === 'card')
|
||||
if (lastCard) {
|
||||
lastCard.answer = m.content
|
||||
lastCard.submitted = true
|
||||
}
|
||||
} else if (m.type === 'outline') {
|
||||
result.push({ role: 'assistant', kind: 'outline', outline: parseOutline(m) })
|
||||
} else if (m.type === 'script') {
|
||||
result.push({ role: 'assistant', kind: 'novel', content: m.content })
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 UI 布局
|
||||
|
||||
```
|
||||
┌─ 顶部栏 ───────────────────────────┐
|
||||
│ ‹ 人生剧本 ✦ 继续 │
|
||||
├──────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─ chat-bubble user ────────────┐ │
|
||||
│ │ 我想开一家自己的咖啡馆... │ │
|
||||
│ └──────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─ chat-bubble system (card) ───┐ │
|
||||
│ │ [ClarificationCard 只读模式] │ │
|
||||
│ │ Q: 你的咖啡馆有什么特色? │ │
|
||||
│ │ 已回答:安静的阅读空间 │ │
|
||||
│ └──────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─ chat-bubble system (outline) ┐ │
|
||||
│ │ 咖啡与书页之间 │ │
|
||||
│ │ 简介:一个爱书人的咖啡馆故事 │ │
|
||||
│ │ ① 辞职的决定 │ │
|
||||
│ │ ② 街角邂逅 │ │
|
||||
│ │ 结局:新的开始 │ │
|
||||
│ └──────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─ chat-bubble system (novel) ──┐ │
|
||||
│ │ [Markdown 渲染小说正文] │ │
|
||||
│ │ 你把那份商业计划书放在... │ │
|
||||
│ └──────────────────────────────┘ │
|
||||
│ │
|
||||
├──────────────────────────────────────┤
|
||||
│ 返回列表 ▶ 播放 继续生成 │
|
||||
└──────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 4.4 只读模式处理
|
||||
|
||||
- **ClarificationCard**:所有卡片都显示"已回答"状态,隐藏选项按钮
|
||||
- **Outline**:隐藏"修改大纲"/"确认大纲"按钮
|
||||
- **Novel**:用 Markdown 组件渲染(比生成时的纯文本更美观)
|
||||
- **无 chat-input-bar**:详情页底部不显示输入框
|
||||
|
||||
### 4.5 降级策略
|
||||
|
||||
- **老剧本无 clarification 消息**:流程中跳过澄清步骤,只展示 wish → outline → novel
|
||||
- **messages API 失败**:降级用 `plotJson.stages` 重建(只有最后 1 轮澄清)
|
||||
- **messages 为空**:至少展示 wish(theme)+ novel(fullContent)
|
||||
|
||||
---
|
||||
|
||||
## 5. 列表页增强(ScriptLibraryView.vue)
|
||||
|
||||
### 5.1 现状卡片结构
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────┐
|
||||
│ [封面] │ 标题 [中篇] │
|
||||
│ 字 │ [标签] [标签] │
|
||||
│ │ 正文前90字摘要... │
|
||||
│ │ 5章 | 3.2k字 | 07-26 │
|
||||
└──────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 5.2 增强后的卡片结构
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────┐
|
||||
│ [封面] │ 标题 [中篇] │
|
||||
│ 字 │ [标签] [标签] │
|
||||
│ │ 📋 第1章:辞职 · 第2章:邂逅 │ ← 新增大纲预览行
|
||||
│ │ 正文前80字摘要... │
|
||||
│ │ 5章 | 3.2k字 | 07-26 │
|
||||
└──────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 5.3 大纲预览数据源
|
||||
|
||||
从 `script.plotJson.stages.outline.beats` 取前 2 章标题:
|
||||
|
||||
```javascript
|
||||
const getOutlinePreview = (script) => {
|
||||
const beats = script.plotJson?.stages?.outline?.beats
|
||||
if (!Array.isArray(beats) || beats.length === 0) return ''
|
||||
const parts = beats.slice(0, 2).map((beat, i) => {
|
||||
return `第${i + 1}章:${beat.title || ''}`
|
||||
})
|
||||
return `📋 ${parts.join(' · ')}`
|
||||
}
|
||||
```
|
||||
|
||||
### 5.4 模板变更
|
||||
|
||||
在 tag-row 和 summary 之间新增大纲预览行:
|
||||
|
||||
```vue
|
||||
<view v-if="getOutlinePreview(script)" class="outline-preview-row">
|
||||
<text class="outline-preview-text">{{ getOutlinePreview(script) }}</text>
|
||||
</view>
|
||||
<text class="summary">{{ script.summary || '一段正在生成中的平行人生剧本。' }}</text>
|
||||
```
|
||||
|
||||
### 5.5 样式
|
||||
|
||||
```css
|
||||
.outline-preview-row {
|
||||
margin-top: 10rpx;
|
||||
padding: 10rpx 14rpx;
|
||||
border-radius: 14rpx;
|
||||
background: rgba(168, 85, 247, 0.08);
|
||||
border: 1rpx solid rgba(168, 85, 247, 0.16);
|
||||
}
|
||||
|
||||
.outline-preview-text {
|
||||
font-size: 22rpx;
|
||||
color: rgba(193, 134, 255, 0.88);
|
||||
line-height: 1.4;
|
||||
}
|
||||
```
|
||||
|
||||
### 5.6 降级
|
||||
|
||||
- **无大纲数据**:不显示大纲预览行(v-if 控制)
|
||||
- **beats 不足 2 章**:有几章显示几章
|
||||
- **beats 标题为空**:显示"第1章"(无冒号后缀)
|
||||
|
||||
---
|
||||
|
||||
## 6. 错误处理与兼容性
|
||||
|
||||
### 6.1 老剧本兼容
|
||||
|
||||
老剧本(改造前生成的)conversation.messages 只有 3 条(system 欢迎、user 心愿、assistant 小说正文),没有 clarification 和 outline 消息。
|
||||
|
||||
**处理**:详情页重建消息流时,跳过缺失的 clarification/outline 步骤。对于大纲数据,fallback 到 `plotJson.stages.outline`(如果存在)。
|
||||
|
||||
### 6.2 API 失败降级
|
||||
|
||||
- messages API 失败 → 用 `plotJson.stages` 重建(只有最后 1 轮澄清)
|
||||
- 整个 script API 失败 → 显示错误页面,不渲染流程
|
||||
|
||||
### 6.3 空数据降级
|
||||
|
||||
- 无 theme → 不显示心愿气泡
|
||||
- 无 outline → 不显示大纲气泡
|
||||
- 无 content/fullContent → 显示"暂无正文"占位
|
||||
|
||||
---
|
||||
|
||||
## 7. 验收标准
|
||||
|
||||
### 7.1 H5 端到端验收(强制)
|
||||
|
||||
1. **生成新小说**:在 ScriptView 页面生成一篇新小说,观察生成时的展示(心愿 → 澄清 → 大纲 → 正文)
|
||||
2. **返回列表**:进入 ScriptLibraryView,确认新小说卡片显示大纲预览行
|
||||
3. **进入详情**:点击新小说进入 ScriptDetailView,确认展示与生成时一致(chat-bubble 流程视图)
|
||||
4. **老剧本兼容**:点击一个改造前生成的老小说,确认不报错、正常展示(可跳过澄清步骤)
|
||||
5. **浏览器 Console**:确认无任何新增错误
|
||||
|
||||
### 7.2 mp-weixin 产物重建
|
||||
|
||||
代码改造完成后,执行 `npm run dev:mp-weixin` 重新构建 mp-weixin 产物,确保微信开发者工具中的真实用户也能看到改造效果。
|
||||
|
||||
---
|
||||
|
||||
## 8. 不在范围内
|
||||
|
||||
- 不改造 ScriptView.vue 的生成流程 UI(保持现状)
|
||||
- 不改造后端 SSE 转发逻辑(只在关键事件点新增 message 保存)
|
||||
- 不新增澄清问答的轮数限制(保持上游服务决定的轮数)
|
||||
- 不改造列表页的搜索、排序、筛选功能
|
||||
Reference in New Issue
Block a user