Files
happy-life-star/docs/superpowers/specs/2026-07-26-novel-list-detail-display-design.md
T
peanut 6f4d9ba0d8 docs:小说列表页/详情页展示改造设计文档
修复详情页重复展示问题,列表页增加大纲预览,后端保存澄清问答消息。
详情页复刻生成时 ScriptView 的 chat-bubble 展示结构。
2026-07-26 14:37:57 +08:00

299 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 条 messagesystem 欢迎、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 messagetype=`clarification_question`, sender=`assistant`, content=问题 JSON |
| followup `answer_clarification` | 创建 user messagetype=`clarification_answer`, sender=`user`, content=用户答案) |
| `outline_created` | 创建 assistant messagetype=`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 为空**:至少展示 wishtheme+ novelfullContent
---
## 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 保存)
- 不新增澄清问答的轮数限制(保持上游服务决定的轮数)
- 不改造列表页的搜索、排序、筛选功能