--- 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> 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 {{ getOutlinePreview(script) }} {{ script.summary || '一段正在生成中的平行人生剧本。' }} ``` ### 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 保存) - 不新增澄清问答的轮数限制(保持上游服务决定的轮数) - 不改造列表页的搜索、排序、筛选功能