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

12 KiB
Raw Blame History

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 条 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 metadataclarification_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 为空:至少展示 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 章标题:

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 端到端验收(强制)

  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 保存)
  • 不新增澄清问答的轮数限制(保持上游服务决定的轮数)
  • 不改造列表页的搜索、排序、筛选功能