feat: ScriptView 改造为对话中心查看/修改模式

- 新增 scriptChat.js 和 message.js 服务导入
- 新增 viewMode、scriptId、conversationId、currentVersionMessageId、
  selectedVersionMessageId、messages、versions、chatInput 等状态
- 新增阅读模式:版本栏 + 剧本分章节展示 + 操作按钮
- 新增聊天模式:消息列表 + 聊天输入栏 + 对话修改功能
- 集成 createScriptWithDialogue、streamScriptChat、listMessagesByConversation、
  listMessageVersions、switchVersion、deleteVersion 等后端 API
- 替换 runGeneration 使用 createScriptWithDialogue 创建剧本和对话
- 替换 openScriptChat 使用后端消息加载,不再依赖 localStorage
- 移除 localStorage 作为主要消息存储(persistResultMessages、
  hydrateResultMessages、readStoredMessages、readScriptChatHistory 改为空操作)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-28 18:41:44 +08:00
parent 6109202a26
commit bf6f2797ad
+608 -170
View File
@@ -158,72 +158,171 @@
<view class="result-scroll-content">
<view id="result-scroll-top" class="result-scroll-top"></view>
<view class="conversation compact">
<view class="chat-bubble user">
<text>{{ wishText }}</text>
<text class="bubble-time">{{ currentMessageTime }}</text>
</view>
<view class="chat-bubble system done">
<text>心愿已实现故事已为你展开</text>
<text class="bubble-time">{{ currentResultTime }}</text>
</view>
</view>
<MessageCard
:content="displayedResultContent"
:title="currentResult?.title"
:tags="resultTags"
:collapsed="storyCollapsed"
:content-length="displayedResultContent.length"
:is-short-message="false"
:tts-icon="ttsActionIcon"
:tts-text="ttsActionText"
@toggle-collapse="toggleStoryCollapse"
@copy="copyResultContent"
@change-direction="changeDirection"
@not-like-me="notLikeMe"
@continue="continueInChat"
@play-tts="trackTtsClick"
/>
<view v-if="resultMessages.length" class="result-chat-list">
<view
v-for="message in resultMessages"
:key="message.id"
>
<MessageCard
v-if="isAssistantMessage(message)"
:content="message.content"
:collapsed="isMessageCollapsed(message)"
:content-length="message.content.length"
:is-short-message="message.content.length < 100"
:tts-icon="ttsPlayer.playing.value ? '' : ''"
:tts-text="ttsPlayer.playing.value ? '暂停' : '播放'"
@toggle-collapse="toggleMessageCollapse(message)"
@copy="copyMessageContent(message)"
@change-direction="changeDirection"
@not-like-me="notLikeMe"
@continue="continueInChat"
@play-tts="playMessageTts(message)"
/>
<view v-else class="chat-bubble user">
<text>{{ message.content }}</text>
<text class="bubble-time">{{ message.time }}</text>
<!-- 阅读模式 -->
<view v-if="viewMode === 'read'">
<view class="version-bar">
<view
v-for="v in versions"
:key="v.id"
class="version-tag"
:class="{ active: selectedVersionMessageId === v.id, current: currentVersionMessageId === v.id }"
@click="onSelectVersion(v.id)"
>
<text>V{{ v.versionNumber }}</text>
<text v-if="currentVersionMessageId === v.id" class="version-current-label">(当前)</text>
</view>
</view>
</view>
<view v-if="resultChatting" class="generation-loading">
<view class="loading-orbit streaming">
<view class="orbit-ring outer"></view>
<view class="orbit-ring inner"></view>
<view class="orbit-core"></view>
<view class="script-read-content">
<view class="script-title">{{ currentScriptContent.title }}</view>
<view v-if="currentScriptContent.plotIntro" class="script-section">
<view class="script-section-title">序幕低谷回响</view>
<view class="script-section-body">{{ currentScriptContent.plotIntro }}</view>
</view>
<view v-if="currentScriptContent.plotTurning" class="script-section">
<view class="script-section-title">转折契机出现</view>
<view class="script-section-body">{{ currentScriptContent.plotTurning }}</view>
</view>
<view v-if="currentScriptContent.plotClimax" class="script-section">
<view class="script-section-title">高潮命运抉择</view>
<view class="script-section-body">{{ currentScriptContent.plotClimax }}</view>
</view>
<view v-if="currentScriptContent.plotEnding" class="script-section">
<view class="script-section-title">结局新的开始</view>
<view class="script-section-body">{{ currentScriptContent.plotEnding }}</view>
</view>
</view>
<view class="read-action-bar">
<view class="read-action-btn" @click="enterChatMode">进入对话修改</view>
<view class="read-action-btn secondary" @click="rewriteCurrentVersion">改写当前版本</view>
<view class="read-action-btn secondary" @click="continueCurrentVersion">续写当前版本</view>
<view v-if="selectedVersionMessageId && selectedVersionMessageId !== currentVersionMessageId" class="read-action-btn primary" @click="setAsCurrentVersion">设为当前版本</view>
</view>
<text class="loading-copy">正在为你重写人生</text>
<text class="loading-subcopy">AI 正在思考下一步方向</text>
</view>
<view :id="resultScrollAnchor" class="result-scroll-anchor"></view>
<!-- 聊天模式 -->
<view v-else-if="viewMode === 'chat'">
<view class="conversation compact">
<view class="chat-bubble user">
<text>{{ wishText }}</text>
<text class="bubble-time">{{ currentMessageTime }}</text>
</view>
<view class="chat-bubble system done">
<text>心愿已实现故事已为你展开</text>
<text class="bubble-time">{{ currentResultTime }}</text>
</view>
</view>
<view v-if="displayMessages.length" class="result-chat-list">
<view
v-for="message in displayMessages"
:key="message.id"
>
<MessageCard
v-if="message.sender === 'assistant' || message.type === 'script'"
:content="message.content"
:collapsed="isMessageCollapsed(message)"
:content-length="message.content.length"
:is-short-message="message.content.length < 100"
:tts-icon="ttsPlayer.playing.value ? '' : ''"
:tts-text="ttsPlayer.playing.value ? '暂停' : '播放'"
@toggle-collapse="toggleMessageCollapse(message)"
@copy="copyMessageContent(message)"
@change-direction="changeDirection"
@not-like-me="notLikeMe"
@continue="continueMessage(message)"
@play-tts="playMessageTts(message)"
/>
<view v-else class="chat-bubble user">
<text>{{ message.content }}</text>
<text class="bubble-time">{{ message.time || formatMessageTime() }}</text>
</view>
</view>
</view>
<view v-if="generating" class="generation-loading">
<view class="loading-orbit streaming">
<view class="orbit-ring outer"></view>
<view class="orbit-ring inner"></view>
<view class="orbit-core"></view>
</view>
<text class="loading-copy">正在为你重写人生</text>
<text class="loading-subcopy">AI 正在思考下一步方向</text>
</view>
<view :id="resultScrollAnchor" class="result-scroll-anchor"></view>
</view>
<!-- 兼容旧模式 -->
<view v-else>
<view class="conversation compact">
<view class="chat-bubble user">
<text>{{ wishText }}</text>
<text class="bubble-time">{{ currentMessageTime }}</text>
</view>
<view class="chat-bubble system done">
<text>心愿已实现故事已为你展开</text>
<text class="bubble-time">{{ currentResultTime }}</text>
</view>
</view>
<MessageCard
:content="displayedResultContent"
:title="currentResult?.title"
:tags="resultTags"
:collapsed="storyCollapsed"
:content-length="displayedResultContent.length"
:is-short-message="false"
:tts-icon="ttsActionIcon"
:tts-text="ttsActionText"
@toggle-collapse="toggleStoryCollapse"
@copy="copyResultContent"
@change-direction="changeDirection"
@not-like-me="notLikeMe"
@continue="continueInChat"
@play-tts="trackTtsClick"
/>
<view v-if="resultMessages.length" class="result-chat-list">
<view
v-for="message in resultMessages"
:key="message.id"
>
<MessageCard
v-if="isAssistantMessage(message)"
:content="message.content"
:collapsed="isMessageCollapsed(message)"
:content-length="message.content.length"
:is-short-message="message.content.length < 100"
:tts-icon="ttsPlayer.playing.value ? '' : ''"
:tts-text="ttsPlayer.playing.value ? '暂停' : '播放'"
@toggle-collapse="toggleMessageCollapse(message)"
@copy="copyMessageContent(message)"
@change-direction="changeDirection"
@not-like-me="notLikeMe"
@continue="continueInChat"
@play-tts="playMessageTts(message)"
/>
<view v-else class="chat-bubble user">
<text>{{ message.content }}</text>
<text class="bubble-time">{{ message.time }}</text>
</view>
</view>
</view>
<view v-if="resultChatting" class="generation-loading">
<view class="loading-orbit streaming">
<view class="orbit-ring outer"></view>
<view class="orbit-ring inner"></view>
<view class="orbit-core"></view>
</view>
<text class="loading-copy">正在为你重写人生</text>
<text class="loading-subcopy">AI 正在思考下一步方向</text>
</view>
<view :id="resultScrollAnchor" class="result-scroll-anchor"></view>
</view>
</view>
</scroll-view>
@@ -232,7 +331,36 @@
<view v-if="showScrollBottomBtn" class="scroll-jump-btn" @click="scrollResultToBottom">底部</view>
</view>
<view class="result-chat-bar">
<!-- 聊天模式输入栏 -->
<view v-if="viewMode === 'chat'" class="result-chat-bar">
<view
class="chat-voice-btn"
:class="{ pressing: voiceState === 'pressing', recognizing: voiceState === 'recognizing' }"
@touchstart.prevent="startVoicePress"
@touchend.prevent="endVoicePress"
@touchcancel.prevent="cancelVoicePress"
>
<text class="voice-icon">🎤</text>
</view>
<view class="result-input-shell" :class="{ twoLine: resultInputLevel === 'two', expanded: resultInputLevel === 'multi' }">
<text v-if="!chatInput" class="custom-input-placeholder result-input-placeholder">输入修改建议或让 AI 继续生成</text>
<textarea
class="result-chat-input"
v-model="chatInput"
:auto-height="resultInputLevel !== 'single'"
:show-confirm-bar="false"
maxlength="500"
confirm-type="send"
@focus="resultInputFocused = true"
@blur="resultInputFocused = false"
@confirm="sendChat"
/>
</view>
<view class="chat-send-btn" :class="{ disabled: !chatInput.trim() || generating }" @click="sendChat">发送</view>
</view>
<!-- 非聊天模式输入栏兼容旧模式 -->
<view v-else class="result-chat-bar">
<view
class="chat-voice-btn"
:class="{ pressing: voiceState === 'pressing', recognizing: voiceState === 'recognizing' }"
@@ -273,6 +401,15 @@ import { useTypewriterStream } from '../../composables/useTypewriterStream.js'
import { transcribeAudio } from '../../services/asr.js'
import { streamAiScene } from '../../services/aiRuntime.js'
import MessageCard from '../../components/MessageCard.vue'
import {
createScriptWithDialogue,
streamScriptChat,
listMessagesByConversation,
listMessageVersions,
switchVersion,
deleteVersion
} from '../../services/scriptChat.js'
import { createMessage } from '../../services/message.js'
const store = useAppStore()
const pagePath = '/pages/main/ScriptView'
@@ -319,6 +456,14 @@ const remainingCount = ref(3)
const style = ref('career')
const randomRecommendations = ref([])
const useSocialInsights = ref(true)
const viewMode = ref('read')
const scriptId = ref('')
const conversationId = ref('')
const currentVersionMessageId = ref('')
const selectedVersionMessageId = ref('')
const messages = ref([])
const versions = ref([])
const chatInput = ref('')
const ttsPlayer = useTtsPlayer({ pagePath })
let recorderManager = null
let recordStartedAt = 0
@@ -461,6 +606,235 @@ const resultHasScriptReplies = computed(() => {
return resultMessages.value.some(item => item.role === 'assistant' && item.kind === 'script')
})
const currentScriptContent = computed(() => {
const targetId = selectedVersionMessageId.value || currentVersionMessageId.value
const targetMessage = messages.value.find(m => m.id === targetId && m.type === 'script')
if (!targetMessage || !targetMessage.metadata) {
return {
title: currentResult.value?.title || '',
plotIntro: currentResult.value?.plotIntro || '',
plotTurning: currentResult.value?.plotTurning || '',
plotClimax: currentResult.value?.plotClimax || '',
plotEnding: currentResult.value?.plotEnding || ''
}
}
try {
return JSON.parse(targetMessage.metadata)
} catch (e) {
return { title: '', plotIntro: targetMessage.metadata || '' }
}
})
const displayMessages = computed(() => {
return messages.value.filter(m => m.type !== 'script' || !m.parentMessageId)
})
const onSelectVersion = (messageId) => {
selectedVersionMessageId.value = messageId
}
const setAsCurrentVersion = async () => {
try {
await switchVersion({ scriptId: scriptId.value, messageId: selectedVersionMessageId.value })
currentVersionMessageId.value = selectedVersionMessageId.value
uni.showToast({ title: '已设为当前版本', icon: 'success' })
} catch (error) {
uni.showToast({ title: error.message || '切换失败', icon: 'none' })
}
}
const enterChatMode = () => {
viewMode.value = 'chat'
loadMessages()
}
const rewriteCurrentVersion = () => {
selectedVersionMessageId.value = currentVersionMessageId.value
viewMode.value = 'chat'
loadMessages()
}
const continueCurrentVersion = () => {
selectedVersionMessageId.value = currentVersionMessageId.value
viewMode.value = 'chat'
loadMessages()
}
const loadMessages = async () => {
if (!conversationId.value) return
try {
const res = await listMessagesByConversation({ conversationId: conversationId.value, includeVersions: false })
messages.value = res.data || []
const scriptMessages = messages.value.filter(m => m.type === 'script')
if (scriptMessages.length > 0) {
const rootMessage = scriptMessages[0]
const versionRes = await listMessageVersions(rootMessage.id)
versions.value = versionRes.data || []
}
} catch (error) {
uni.showToast({ title: error.message || '加载消息失败', icon: 'none' })
}
}
const sendChat = async () => {
if (!chatInput.value.trim() || generating.value) return
const content = chatInput.value.trim()
chatInput.value = ''
const messageId = currentVersionMessageId.value
const userMessageRes = await createMessage({
conversationId: conversationId.value,
content,
sender: 'user',
type: 'chat'
})
const userMessageId = userMessageRes.data?.id
messages.value.push({
id: userMessageId,
conversationId: conversationId.value,
content,
sender: 'user',
type: 'chat'
})
generating.value = true
await streamScriptChat({
operationType: 'chat',
conversationId: conversationId.value,
messageId,
userMessageId,
content,
scriptId: scriptId.value,
onDelta: () => {},
onDone: () => {
generating.value = false
loadMessages()
},
onError: (error) => {
generating.value = false
uni.showToast({ title: error || '生成失败', icon: 'none' })
}
})
}
const rewriteMessage = (msg) => {
chatInput.value = ''
uni.showModal({
title: '改写意图',
editable: true,
placeholderText: '例如:把结局改得更积极',
success: async (res) => {
if (res.confirm && res.content) {
await sendRewrite(msg.id, res.content)
}
}
})
}
const sendRewrite = async (targetMessageId, content) => {
const userMessageRes = await createMessage({
conversationId: conversationId.value,
content,
sender: 'user',
type: 'chat'
})
const userMessageId = userMessageRes.data?.id
messages.value.push({
id: userMessageId,
conversationId: conversationId.value,
content,
sender: 'user',
type: 'chat'
})
generating.value = true
await streamScriptChat({
operationType: 'rewrite',
conversationId: conversationId.value,
messageId: targetMessageId,
userMessageId,
content,
scriptId: scriptId.value,
onDelta: () => {},
onDone: () => {
generating.value = false
loadMessages()
},
onError: (error) => {
generating.value = false
uni.showToast({ title: error || '改写失败', icon: 'none' })
}
})
}
const continueMessage = (msg) => {
chatInput.value = '继续生成后续内容'
sendChatAsContinue(msg.id)
}
const sendChatAsContinue = async (targetMessageId) => {
const content = '继续生成后续内容'
const userMessageRes = await createMessage({
conversationId: conversationId.value,
content,
sender: 'user',
type: 'chat'
})
const userMessageId = userMessageRes.data?.id
generating.value = true
await streamScriptChat({
operationType: 'continue',
conversationId: conversationId.value,
messageId: targetMessageId,
userMessageId,
content,
scriptId: scriptId.value,
onDelta: () => {},
onDone: () => {
generating.value = false
loadMessages()
},
onError: (error) => {
generating.value = false
uni.showToast({ title: error || '续写失败', icon: 'none' })
}
})
}
const showVersions = async (msg) => {
const res = await listMessageVersions(msg.id)
const list = res.data || []
const items = list.map(v => `V${v.versionNumber} ${v.id === currentVersionMessageId.value ? '(当前)' : ''}`)
uni.showActionSheet({
itemList: items,
success: (res) => {
const selected = list[res.tapIndex]
selectedVersionMessageId.value = selected.id
viewMode.value = 'read'
}
})
}
const removeVersion = async (msg) => {
if (msg.id === currentVersionMessageId.value) {
uni.showToast({ title: '不能删除当前生效版本', icon: 'none' })
return
}
uni.showModal({
title: '确认删除',
content: `确定删除 V${msg.versionNumber} 吗?`,
success: async (res) => {
if (res.confirm) {
await deleteVersion(msg.id)
await loadMessages()
}
}
})
}
const measureResultViewport = () => {
nextTick(() => {
const query = uni.createSelectorQuery()
@@ -851,45 +1225,22 @@ const getScriptChatHistoryKey = (conversationId = getCurrentConversationId()) =>
}
const readStoredMessages = (key) => {
if (!key) return []
const value = uni.getStorageSync(key)
return Array.isArray(value) ? value.filter(item => {
if (!item?.role) return false
if (item.role === 'script') return false
return typeof item.content === 'string'
}) : []
// localStorage 不再作为主要消息存储
return []
}
const readScriptChatHistory = ({ conversationId, scriptId } = {}) => {
const messages = readStoredMessages(getScriptChatHistoryKey(conversationId))
if (messages.length || !scriptId) return messages
return readStoredMessages(`${LEGACY_SCRIPT_CHAT_HISTORY_PREFIX}${scriptId}`)
// localStorage 不再作为主要消息存储
return []
}
const persistResultMessages = () => {
const key = getScriptChatHistoryKey()
if (!key) return
const messages = resultMessages.value
.filter(item => item?.role && item.role !== 'script' && !item.pending && item.content)
.map(item => ({
id: item.id,
role: item.role,
kind: item.kind || '',
content: item.content,
time: item.time
}))
uni.setStorageSync(key, messages)
// localStorage 不再作为主要消息存储,消息通过后端 API 管理
}
const hydrateResultMessages = ({ conversationId, scriptId } = {}) => {
resultMessages.value = readScriptChatHistory({ conversationId, scriptId }).map(item => ({
id: item.id || createMessageId(),
role: item.role,
kind: item.kind || '',
content: item.content || '',
pending: false,
time: item.time || formatMessageTime()
}))
// localStorage 不再作为主要消息存储,消息通过后端 API 管理
resultMessages.value = []
collapsedMessageIds.value = {}
}
@@ -1076,13 +1427,13 @@ const normalizeGeneratedContent = (text) => {
const openScriptChat = async (payload = {}) => {
uni.removeStorageSync(PENDING_SCRIPT_CHAT_KEY)
const scriptId = String(payload?.id || payload?.scriptId || '')
const scriptIdParam = String(payload?.id || payload?.scriptId || '')
let script = payload?.script || (payload?.id ? payload : null)
if (!script?.id && scriptId) {
script = store.getScriptById(scriptId)
if (!script?.id && scriptIdParam) {
script = store.getScriptById(scriptIdParam)
if (!script) {
await store.fetchScripts()
script = store.getScriptById(scriptId)
script = store.getScriptById(scriptIdParam)
}
}
if (!script?.id) return
@@ -1102,15 +1453,21 @@ const openScriptChat = async (payload = {}) => {
ttsPlayer.reset()
currentResult.value = normalizeGeneratedScript({ script })
currentConversationId.value = ensureConversationId(currentResult.value)
scriptId.value = script.id
conversationId.value = script.conversationId || ''
currentVersionMessageId.value = script.currentVersionMessageId || ''
selectedVersionMessageId.value = script.currentVersionMessageId || ''
wishText.value = script.theme || script.prompt || script.title || '继续修改这个人生剧本'
currentMessageTime.value = formatMessageTime()
currentResultTime.value = script.updateTime || script.updatedAt || script.createTime || script.createdAt || formatMessageTime()
hydrateResultMessages({ conversationId: currentConversationId.value, scriptId: script.id })
viewMode.value = 'read'
viewState.value = 'result'
if (conversationId.value) {
await loadMessages()
}
analytics.track('script_chat_open_from_library', {
script_id: script.id,
has_history: resultMessages.value.length > 0
has_history: messages.value.length > 0
}, { eventType: 'script', pagePath })
scrollResultToLatest()
}
@@ -1233,21 +1590,10 @@ const generateScriptByStream = async (text, options = {}) => {
})
}
const runGeneration = async ({ prompt, displayText, source = 'text', saveTheme, revision = false }) => {
const runGeneration = async ({ prompt, displayText, source = 'text', saveTheme }) => {
const text = String(prompt || '').trim()
const display = String(displayText || text).trim()
if (!text || generating.value) return
const isRevision = Boolean(revision)
const conversationId = isRevision ? ensureConversationId() : createConversationId()
const parentScriptId = isRevision ? getCurrentScriptId() : ''
const revisionIndex = isRevision
? getScriptVersions().length + 1
: 1
currentConversationId.value = conversationId
lastGenerationRevision.value = isRevision
lastSubmitSource.value = source
generationDisplayText.value = display
lastGenerationPrompt.value = text
analytics.track('script_wish_submit', {
source,
@@ -1267,75 +1613,58 @@ const runGeneration = async ({ prompt, displayText, source = 'text', saveTheme,
ttsPlayer.reset()
viewState.value = 'generating'
keepGenerationAtBottom()
analytics.track('script_generation_progress_view', {
source,
prompt_length: display.length
}, { eventType: 'script', pagePath })
let res
try {
res = await generateScriptByStream(text, {
saveTheme: saveTheme || display,
conversationId,
parentScriptId,
revisionIndex,
revisionOf: parentScriptId,
updateScriptId: isRevision ? getCurrentScriptId() : ''
})
} catch (streamError) {
markGenerationFailed(streamError?.message || 'AI 流式生成失败')
analytics.track('script_generate_stream_fail', {
source,
error: streamError?.message || 'stream_failed'
}, { eventType: 'script', pagePath })
res = {
success: false,
error: streamError?.message || 'AI 流式生成失败'
const payload = {
theme: text,
style: style.value || 'career',
length: 'medium',
characterInfo: '',
lifeEventsSummary: '',
useSocialInsights: useSocialInsights.value
}
}
generating.value = false
if (res.success) {
clearGenerationFeedbackTimers()
if (streamContent.value) streamWriter.finish(normalizeGeneratedContent(streamContent.value))
keepGenerationAtBottom()
}
const res = await createScriptWithDialogue(payload)
const data = res.data
if (!res.success) {
analytics.track('script_generate_fail', {
scriptId.value = data.scriptId
conversationId.value = data.conversationId
currentVersionMessageId.value = data.currentVersionMessageId
selectedVersionMessageId.value = data.currentVersionMessageId
currentResult.value = {
id: data.scriptId,
title: data.title,
theme: data.theme,
style: data.style,
length: data.length,
plotIntro: data.plotIntro,
plotTurning: data.plotTurning,
plotClimax: data.plotClimax,
plotEnding: data.plotEnding
}
await loadMessages()
viewMode.value = 'read'
viewState.value = 'result'
analytics.track('script_generate_success', {
source,
error: res.error || 'unknown',
style: currentResult.value.style || '',
length: currentResult.value.length || '',
use_social_insights: useSocialInsights.value,
duration_ms: Date.now() - generationStartedAt.value
}, { eventType: 'script', pagePath })
markGenerationFailed(res.error || '生成失败')
return
} catch (error) {
markGenerationFailed(error.message || '生成失败')
analytics.track('script_generate_fail', {
source,
error: error.message || 'unknown'
}, { eventType: 'script', pagePath })
} finally {
generating.value = false
generationStatus.value = 'idle'
}
currentResult.value = normalizeGeneratedScript(res.data)
currentConversationId.value = ensureConversationId(currentResult.value)
currentResultTime.value = formatMessageTime()
if (!isRevision) {
resultMessages.value = []
collapsedMessageIds.value = {}
}
if (typeof res.data?.remainingCount === 'number') remainingCount.value = res.data.remainingCount
analytics.track('script_generate_success', {
source,
style: currentResult.value.style || '',
length: currentResult.value.length || '',
use_social_insights: useSocialInsights.value,
duration_ms: Date.now() - generationStartedAt.value
}, { eventType: 'script', pagePath })
analytics.track('script_result_view', {
script_id: currentResult.value.id || '',
style: currentResult.value.style || '',
length: currentResult.value.length || ''
}, { eventType: 'script', pagePath })
generationStatus.value = 'idle'
viewState.value = 'result'
keepResultAtBottom()
}
const submitWish = async (source = 'text') => {
@@ -2845,4 +3174,113 @@ onUnmounted(() => {
.chat-send-btn.disabled {
opacity: 0.45;
}
.version-bar {
display: flex;
gap: 16rpx;
margin-bottom: 24rpx;
flex-wrap: wrap;
}
.version-tag {
display: flex;
align-items: center;
gap: 8rpx;
padding: 10rpx 20rpx;
border-radius: 999rpx;
background: rgba(43, 19, 83, 0.46);
border: 1rpx solid rgba(168, 85, 247, 0.24);
color: rgba(232, 204, 255, 0.72);
font-size: 26rpx;
}
.version-tag.active {
background: rgba(140, 68, 242, 0.52);
border-color: rgba(209, 138, 255, 0.52);
color: #fff;
}
.version-tag.current {
border-color: rgba(255, 216, 107, 0.42);
}
.version-current-label {
font-size: 22rpx;
color: #ffd86b;
}
.script-read-content {
display: flex;
flex-direction: column;
gap: 28rpx;
margin-bottom: 32rpx;
}
.script-title {
font-size: 40rpx;
font-weight: 800;
color: #fff;
text-align: center;
margin-bottom: 8rpx;
}
.script-section {
display: flex;
flex-direction: column;
gap: 12rpx;
padding: 24rpx;
border-radius: 28rpx;
background: rgba(16, 8, 34, 0.28);
border: 1rpx solid rgba(192, 132, 252, 0.2);
}
.script-section-title {
font-size: 30rpx;
font-weight: 700;
color: #d18aff;
}
.script-section-body {
font-size: 30rpx;
line-height: 48rpx;
color: rgba(255, 255, 255, 0.88);
white-space: pre-wrap;
word-break: break-all;
}
.read-action-bar {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin-bottom: 24rpx;
}
.read-action-btn {
flex: 1;
min-width: 176rpx;
height: 72rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 999rpx;
font-size: 27rpx;
font-weight: 700;
color: #1b0b31;
background: linear-gradient(145deg, #ffd86b, #d18aff);
box-shadow: 0 16rpx 36rpx rgba(209, 138, 255, 0.24);
}
.read-action-btn.secondary {
color: #e8ccff;
background: rgba(43, 19, 83, 0.68);
border: 1rpx solid rgba(168, 85, 247, 0.32);
box-shadow: none;
}
.read-action-btn.primary {
color: #1b0b31;
background: linear-gradient(145deg, #934dff, #4d1ccb);
box-shadow: 0 16rpx 36rpx rgba(168, 85, 247, 0.24);
}
</style>