修复详情页重复展示问题,列表页增加大纲预览,后端保存澄清问答消息。 详情页复刻生成时 ScriptView 的 chat-bubble 展示结构。
12 KiB
author, created_at, purpose
| author | created_at | purpose |
|---|---|---|
| huazhongmin | 2026-07-26 | 小说列表页和详情页展示改造设计 — 修复详情页重复展示、列表页增加大纲预览、后端保存澄清问答消息 |
小说列表页 / 详情页展示改造设计
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 列表。
后端新增接口:
@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),让它完整还原生成时的消息流:
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 章标题:
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 之间新增大纲预览行:
<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 样式
.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 端到端验收(强制)
- 生成新小说:在 ScriptView 页面生成一篇新小说,观察生成时的展示(心愿 → 澄清 → 大纲 → 正文)
- 返回列表:进入 ScriptLibraryView,确认新小说卡片显示大纲预览行
- 进入详情:点击新小说进入 ScriptDetailView,确认展示与生成时一致(chat-bubble 流程视图)
- 老剧本兼容:点击一个改造前生成的老小说,确认不报错、正常展示(可跳过澄清步骤)
- 浏览器 Console:确认无任何新增错误
7.2 mp-weixin 产物重建
代码改造完成后,执行 npm run dev:mp-weixin 重新构建 mp-weixin 产物,确保微信开发者工具中的真实用户也能看到改造效果。
8. 不在范围内
- 不改造 ScriptView.vue 的生成流程 UI(保持现状)
- 不改造后端 SSE 转发逻辑(只在关键事件点新增 message 保存)
- 不新增澄清问答的轮数限制(保持上游服务决定的轮数)
- 不改造列表页的搜索、排序、筛选功能