From 95a3864250c7b4dc87a68711fb96f5ed928b5468 Mon Sep 17 00:00:00 2001 From: Peanut Date: Sun, 26 Jul 2026 15:31:01 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9AScriptDetailView=20=E6=94=B9?= =?UTF-8?q?=E9=80=A0=E4=B8=BA=20chat-bubble=20=E6=B5=81=E7=A8=8B=E8=A7=86?= =?UTF-8?q?=E5=9B=BE=E5=A4=8D=E5=88=BB=E7=94=9F=E6=88=90=E6=97=B6=E5=B1=95?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/main/ScriptDetailView.vue | 456 ++++++++---------- 1 file changed, 204 insertions(+), 252 deletions(-) diff --git a/mini-program/src/pages/main/ScriptDetailView.vue b/mini-program/src/pages/main/ScriptDetailView.vue index 20374b7..6bc51d0 100644 --- a/mini-program/src/pages/main/ScriptDetailView.vue +++ b/mini-program/src/pages/main/ScriptDetailView.vue @@ -10,55 +10,57 @@ - - PARALLEL LIFE - {{ script?.title || '未命名剧本' }} - {{ script?.summary || '命运正在整理章节。' }} - - 💭 用户心愿 - {{ userWish }} - - - - {{ script?.style || '爽文' }} - 风格 + + + + + {{ msg.content }} - - {{ lengthText }} - 篇幅 + + + + + {{ msg.card.question }} + 已回答:{{ msg.answer || '未回答' }} + - - {{ script?.wordCount || 0 }} - 字数 + + + + + + {{ msg.outline.title }} + + + {{ msg.outline.logline }} + + + + {{ beat.order || idx + 1 }} + + {{ beat.title || '' }} + {{ beat.summary }} + + + + + 结局 + {{ msg.outline.ending }} + + + + + + + - - - 正文 - 大纲 - - - - - - - - - - {{ index + 1 }} - - {{ item.title }} - {{ item.text }} - - - - - {{ index + 1 }} - - {{ item.title }} - {{ item.text }} - + + 暂无剧本内容 @@ -81,49 +83,21 @@ import Markdown from '../../components/Markdown.vue' import analytics from '../../services/analytics.js' import { useTtsPlayer } from '../../composables/useTtsPlayer.js' import { useMenuButtonSafeArea } from '../../composables/useMenuButtonSafeArea.js' +import { listMessagesByConversation } from '../../services/scriptChat.js' const store = useAppStore() -const activeTab = ref('content') const scriptId = ref('') const script = ref(null) +const resultMessages = ref([]) const pagePath = '/pages/main/ScriptDetailView' const ttsPlayer = useTtsPlayer({ pagePath }) const { capsuleTopReservePx, topbarStyle } = useMenuButtonSafeArea({ extraTopPx: 10 }) -const fullContent = computed(() => script.value?.content || '暂无正文内容。') const lengthText = computed(() => { const map = { short: '短篇', medium: '中篇', long: '长篇' } return map[script.value?.length] || script.value?.length || '中篇' }) -/** - * 用户心愿原文(从 EpicScript.theme 字段读取) - */ -const userWish = computed(() => script.value?.theme || '') - -/** - * 大纲(从 plotJson.stages.outline 读取后端真实保存的 outline JSON) - * 后端 ShortNovelServiceImpl 在 novel_done 时会把 outline_created 事件累积到 metadata.stages.outline - */ -const outlineFromMeta = computed(() => { - const stages = script.value?.plotJson?.stages - if (!stages || !stages.outline) return [] - const outline = stages.outline - const items = [] - if (outline.title) items.push({ title: '标题', text: outline.title }) - if (outline.logline) items.push({ title: '简介', text: outline.logline }) - if (Array.isArray(outline.beats)) { - outline.beats.forEach((beat, i) => { - items.push({ - title: beat.title || `章节 ${i + 1}`, - text: beat.text || beat.description || JSON.stringify(beat) - }) - }) - } - if (outline.ending) items.push({ title: '结局', text: outline.ending }) - return items -}) - const detailTtsActionText = computed(() => { if (!script.value?.id) return '播放' if (ttsPlayer.loading.value) return '生成中' @@ -134,39 +108,63 @@ const detailTtsIcon = computed(() => { return ttsPlayer.playing.value ? 'Ⅱ' : '▶' }) -const outline = computed(() => { - const text = fullContent.value - const parts = text.split(/\n{2,}/).filter(Boolean) - if (parts.length > 1) { - return parts.slice(0, 8).map((part, index) => { - const clean = part.replace(/[#>*_`]/g, '').trim() - const lines = clean.split('\n').filter(Boolean) - return { - title: lines[0]?.replace(/[【】]/g, '') || `章节 ${index + 1}`, - text: lines.slice(1).join(' ').slice(0, 120) || clean.slice(0, 120) - } +/** + * 从后端 message 列表重建统一对话消息流 + * 复刻 ScriptView 生成时的 chat-bubble 展示结构 + */ +const rebuildResultMessages = (scriptData, messages) => { + const result = [] + + // 1. 用户心愿作为首条 user 消息(如果 messages 中没有 user 心愿消息则用 theme 补) + const hasUserWishInMessages = messages.some(m => m.sender === 'user' && m.type === 'chat') + if (!hasUserWishInMessages && scriptData?.theme) { + result.push({ + id: `wish-${scriptData.id}`, + role: 'user', + kind: 'text', + content: scriptData.theme }) } - return [ - { title: '起点', text: script.value?.theme || '从真实的人生经验出发。' }, - { title: '转折', text: '关键机会出现,旧有困境开始松动。' }, - { title: '高光', text: '主角完成一次真正的选择,并看见新的自我。' }, - { title: '回响', text: '剧本落回现实,转化为可执行路径。' } - ] -}) + // 2. 按 messageOrder 遍历 messages,转换为对应 kind + messages.forEach(m => { + if (m.type === 'clarification_question') { + let card = {} + try { card = JSON.parse(m.content || '{}') } catch (e) { card = {} } + result.push({ id: m.id, role: 'assistant', kind: 'card', card, answer: '', submitted: true }) + } else if (m.type === 'clarification_answer') { + const lastCard = [...result].reverse().find(x => x.kind === 'card') + if (lastCard) { + lastCard.answer = m.content || '未回答' + } else { + // 无对应 question,作为独立 user 消息 + result.push({ id: m.id, role: 'user', kind: 'text', content: m.content || '' }) + } + } else if (m.type === 'outline') { + let outline = {} + try { outline = JSON.parse(m.content || '{}') } catch (e) { outline = {} } + result.push({ id: m.id, role: 'assistant', kind: 'outline', outline }) + } else if (m.type === 'script') { + result.push({ id: m.id, role: 'assistant', kind: 'novel', content: m.content || '' }) + } else if (m.type === 'system') { + // 跳过系统欢迎消息,不展示 + } else if (m.type === 'chat' && m.sender === 'user') { + result.push({ id: m.id, role: 'user', kind: 'text', content: m.content || '' }) + } + }) -const loadScript = async () => { - if (!scriptId.value) return - ttsPlayer.reset() - script.value = store.getScriptById(scriptId.value) - if (!script.value) { - await store.fetchScripts() - script.value = store.getScriptById(scriptId.value) - } - if (script.value?.id) { - ttsPlayer.prewarmSource(script.value.id) + // 3. 如果 messages 中没有 script 类型消息,fallback 到 plotJson.fullContent + const hasNovel = result.some(m => m.kind === 'novel') + if (!hasNovel && scriptData?.plotJson?.fullContent) { + result.push({ + id: `fallback-novel-${scriptData.id}`, + role: 'assistant', + kind: 'novel', + content: scriptData.plotJson.fullContent + }) } + + return result } const continueCurrent = () => { @@ -194,9 +192,32 @@ const goBack = () => { } onMounted(async () => { + // scriptId 从路由参数获取 const pages = getCurrentPages() - scriptId.value = pages[pages.length - 1]?.options?.id || '' - await loadScript() + const currentPage = pages[pages.length - 1] + scriptId.value = currentPage?.options?.id || currentPage?.$page?.options?.id || '' + + // 1. 加载 script 数据 + await store.fetchScripts() + script.value = store.getScriptById(scriptId.value) + + // 2. 加载 conversation messages + const conversationId = script.value?.conversationId + if (conversationId) { + try { + const res = await listMessagesByConversation({ conversationId, includeVersions: false }) + const messages = Array.isArray(res?.data) ? res.data : [] + resultMessages.value = rebuildResultMessages(script.value, messages) + } catch (e) { + console.error('[ScriptDetailView] 加载 messages 失败:', e) + // 降级:仅展示心愿 + 正文 + resultMessages.value = rebuildResultMessages(script.value, []) + } + } else { + // 无 conversationId,降级展示 + resultMessages.value = rebuildResultMessages(script.value, []) + } + analytics.trackPageView(pagePath, { script_id: scriptId.value }) analytics.track('script_detail_view', { script_id: scriptId.value, @@ -283,168 +304,99 @@ onUnmounted(() => { padding: 0 30rpx; } -.hero-card { - border-radius: 32rpx; - padding: 36rpx 30rpx; +/* chat-bubble 流程视图样式 */ +.detail-flow { + padding: 20rpx 0 160rpx; } - -.eyebrow { - display: block; - color: #c186ff; - font-size: 18rpx; - letter-spacing: 6rpx; +.chat-bubble { + margin-bottom: 24rpx; + padding: 24rpx; + border-radius: 24rpx; } - -.script-title { - display: block; - margin-top: 14rpx; - color: #fff; - font-size: 44rpx; - line-height: 1.18; - font-weight: 900; +.chat-bubble.user { + background: rgba(168, 85, 247, 0.12); + align-self: flex-end; + margin-left: 80rpx; } - -.script-summary { - display: block; - margin-top: 18rpx; - color: rgba(226, 216, 246, 0.7); - font-size: 25rpx; - line-height: 1.62; -} - -.wish-block { - margin-top: 24rpx; - padding: 20rpx 22rpx; - border-radius: 22rpx; - background: linear-gradient(135deg, rgba(168, 85, 247, 0.16), rgba(232, 121, 249, 0.08)); - border: 1rpx solid rgba(168, 85, 247, 0.24); -} - -.wish-label { - display: block; - font-size: 22rpx; - color: #c186ff; - font-weight: 700; - letter-spacing: 2rpx; - margin-bottom: 8rpx; -} - -.wish-text { - display: block; - font-size: 26rpx; - line-height: 1.62; - color: rgba(255, 255, 255, 0.92); -} - -.stats { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 16rpx; - margin-top: 28rpx; -} - -.stat { - padding: 18rpx 10rpx; - border-radius: 20rpx; - text-align: center; +.chat-bubble.system { background: rgba(255, 255, 255, 0.06); + margin-right: 40rpx; } - -.stat-value, -.stat-label { - display: block; -} - -.stat-value { - color: #fff; - font-size: 24rpx; - font-weight: 800; -} - -.stat-label { - margin-top: 6rpx; - color: rgba(219, 207, 243, 0.58); - font-size: 19rpx; -} - -.tabs { - height: 76rpx; - margin-top: 24rpx; - border-radius: 999rpx; - padding: 6rpx; - display: grid; - grid-template-columns: 1fr 1fr; -} - -.tab { - border-radius: 999rpx; - display: flex; - align-items: center; - justify-content: center; - color: rgba(224, 214, 243, 0.64); - font-size: 26rpx; - font-weight: 700; -} - -.tab.active { - color: #fff; - background: linear-gradient(135deg, #a63cff, #6530ff); - box-shadow: 0 0 26rpx rgba(162, 71, 255, 0.48); -} - -.article-card { - margin: 24rpx 0 34rpx; - border-radius: 30rpx; - padding: 30rpx; -} - -.outline-list { +.card-answered { display: flex; flex-direction: column; - gap: 18rpx; - margin: 24rpx 0 34rpx; + gap: 12rpx; } - -.outline-card { - border-radius: 26rpx; - padding: 26rpx; - display: flex; - gap: 20rpx; +.card-question-text { + font-size: 28rpx; + color: rgba(255, 255, 255, 0.9); + font-weight: 500; } - -.outline-node { - width: 48rpx; - height: 48rpx; - flex-shrink: 0; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - color: #fff; - font-size: 22rpx; - font-weight: 900; - background: linear-gradient(135deg, #a855ff, #4f46e5); - box-shadow: 0 0 22rpx rgba(168, 85, 255, 0.48); +.card-answer-text { + font-size: 26rpx; + color: rgba(193, 134, 255, 0.88); } - -.outline-body { - flex: 1; - min-width: 0; -} - .outline-title { - display: block; + font-size: 32rpx; + font-weight: 600; color: #fff; - font-size: 27rpx; - font-weight: 900; + margin-bottom: 12rpx; } - -.outline-text { - display: block; - margin-top: 10rpx; - color: rgba(223, 211, 245, 0.7); +.outline-logline { + font-size: 26rpx; + color: rgba(255, 255, 255, 0.7); + margin-bottom: 16rpx; +} +.outline-beats { + display: flex; + flex-direction: column; + gap: 16rpx; +} +.beat-item { + display: flex; + gap: 16rpx; +} +.beat-order { font-size: 24rpx; - line-height: 1.6; + color: rgba(193, 134, 255, 0.88); + width: 40rpx; +} +.beat-content { + flex: 1; + display: flex; + flex-direction: column; + gap: 6rpx; +} +.beat-title { + font-size: 28rpx; + color: rgba(255, 255, 255, 0.9); +} +.beat-summary { + font-size: 24rpx; + color: rgba(255, 255, 255, 0.6); +} +.outline-ending { + margin-top: 16rpx; + padding-top: 16rpx; + border-top: 1rpx solid rgba(255, 255, 255, 0.1); +} +.ending-label { + font-size: 24rpx; + color: rgba(255, 255, 255, 0.5); + margin-right: 12rpx; +} +.ending-text { + font-size: 26rpx; + color: rgba(255, 255, 255, 0.8); +} +.novel-bubble { + padding: 32rpx; +} +.empty-flow { + padding: 80rpx 0; + text-align: center; + color: rgba(255, 255, 255, 0.4); + font-size: 28rpx; } .bottom-actions {