feat(mini-program): ScriptDetailView 新增小说消息操作按钮组

- 新增 collapsedMessageIds ref + 5 个函数(isMessageCollapsed、getMessageDisplayContent、toggleMessageCollapse、copyMessageContent、playMessageTts)
- 为 kind === 'novel' 的消息添加折叠/展开、复制、播放按钮
- 添加 .collapse-toggle-row、.message-actions、.action-btn 样式
- H5 验收通过:复制按钮触发 Toast、播放按钮切换 TTS 状态(▶播放 ↔ Ⅱ暂停)、折叠按钮切换文案

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-28 21:53:24 +08:00
parent 77ab1aee85
commit 44fe10950d
@@ -72,7 +72,27 @@
<!-- 小说正文Markdown 渲染 --> <!-- 小说正文Markdown 渲染 -->
<view v-else-if="msg.kind === 'novel'" class="chat-bubble system novel-bubble"> <view v-else-if="msg.kind === 'novel'" class="chat-bubble system novel-bubble">
<Markdown :content="msg.content" /> <Markdown :content="getMessageDisplayContent(msg)" />
<!-- 折叠/展开按钮内容超过预览长度时显示 -->
<view v-if="msg.content && msg.content.length > ASSISTANT_MESSAGE_PREVIEW_LENGTH" class="collapse-toggle-row" @click="toggleMessageCollapse(msg)">
<text class="collapse-toggle-text">{{ isMessageCollapsed(msg) ? '展开全文' : '收起全文' }}</text>
<view class="collapse-chevron" :class="{ down: isMessageCollapsed(msg) }">
<view></view>
<view></view>
</view>
</view>
<!-- 操作按钮组 -->
<view class="message-actions">
<button class="action-btn" @click="copyMessageContent(msg)">
<text>复制</text>
</button>
<button class="action-btn primary" @click="playMessageTts(msg)">
<text class="action-icon">{{ ttsPlayer.playing.value ? 'Ⅱ' : '▶' }}</text>
<text>{{ ttsPlayer.playing.value ? '暂停' : '播放' }}</text>
</button>
</view>
</view> </view>
</view> </view>
@@ -106,10 +126,14 @@ const store = useAppStore()
const scriptId = ref('') const scriptId = ref('')
const script = ref(null) const script = ref(null)
const resultMessages = ref([]) const resultMessages = ref([])
const collapsedMessageIds = ref({})
const pagePath = '/pages/main/ScriptDetailView' const pagePath = '/pages/main/ScriptDetailView'
const ttsPlayer = useTtsPlayer({ pagePath }) const ttsPlayer = useTtsPlayer({ pagePath })
const { capsuleTopReservePx, topbarStyle } = useMenuButtonSafeArea({ extraTopPx: 10 }) const { capsuleTopReservePx, topbarStyle } = useMenuButtonSafeArea({ extraTopPx: 10 })
// 小说正文折叠时的预览长度(字符)
const ASSISTANT_MESSAGE_PREVIEW_LENGTH = 120
const lengthText = computed(() => { const lengthText = computed(() => {
const map = { short: '短篇', medium: '中篇', long: '长篇' } const map = { short: '短篇', medium: '中篇', long: '长篇' }
return map[script.value?.length] || script.value?.length || '中篇' return map[script.value?.length] || script.value?.length || '中篇'
@@ -125,6 +149,70 @@ const detailTtsIcon = computed(() => {
return ttsPlayer.playing.value ? 'Ⅱ' : '▶' return ttsPlayer.playing.value ? 'Ⅱ' : '▶'
}) })
/**
* 判断消息是否处于折叠状态
*/
const isMessageCollapsed = (message) => {
return Boolean(collapsedMessageIds.value[message.id])
}
/**
* 获取消息显示内容(折叠时截断到预览长度)
*/
const getMessageDisplayContent = (message) => {
const content = String(message?.content || '')
if (!isMessageCollapsed(message)) return content
if (content.length <= ASSISTANT_MESSAGE_PREVIEW_LENGTH) return content
return `${content.slice(0, ASSISTANT_MESSAGE_PREVIEW_LENGTH)}...`
}
/**
* 切换消息折叠/展开状态
*/
const toggleMessageCollapse = (message) => {
collapsedMessageIds.value = {
...collapsedMessageIds.value,
[message.id]: !isMessageCollapsed(message)
}
analytics.track('script_message_collapse_toggle', {
message_id: message?.id || '',
collapsed: collapsedMessageIds.value[message.id]
}, { eventType: 'script', pagePath })
}
/**
* 复制消息内容到剪贴板
*/
const copyMessageContent = (message) => {
const content = String(message?.content || '')
if (!content.trim()) {
uni.showToast({ title: '暂无可复制内容', icon: 'none' })
return
}
uni.setClipboardData({
data: content,
success: () => {
uni.showToast({ title: '已复制', icon: 'success' })
}
})
analytics.track('script_message_copy_click', {
message_id: message?.id || '',
content_length: content.length
}, { eventType: 'script', pagePath })
}
/**
* 播放消息 TTS 音频(播放整个剧本的音频)
*/
const playMessageTts = (message) => {
const scriptIdValue = script.value?.id || ''
ttsPlayer.playSource(scriptIdValue)
analytics.track('script_message_tts_click', {
message_id: message?.id || '',
script_id: scriptIdValue
}, { eventType: 'script', pagePath })
}
/** /**
* 从后端 message 列表重建统一对话消息流 * 从后端 message 列表重建统一对话消息流
* 复刻 ScriptView 生成时的 chat-bubble 展示结构 * 复刻 ScriptView 生成时的 chat-bubble 展示结构
@@ -522,6 +610,110 @@ onUnmounted(() => {
.novel-bubble { .novel-bubble {
padding: 32rpx; padding: 32rpx;
} }
/* 折叠/展开按钮行 */
.collapse-toggle-row {
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
margin-top: 20rpx;
padding: 16rpx 0;
border-radius: 999rpx;
background: rgba(88, 28, 135, 0.12);
border: 1rpx solid rgba(192, 132, 252, 0.28);
color: rgba(246, 230, 255, 0.96);
font-size: 26rpx;
font-weight: 700;
}
.collapse-toggle-row:active {
transform: scale(0.98);
opacity: 0.92;
}
.collapse-toggle-text {
line-height: 1;
}
/* 折叠箭头图标 */
.collapse-chevron {
position: relative;
width: 20rpx;
height: 16rpx;
flex-shrink: 0;
transition: transform 0.18s ease;
}
.collapse-chevron.down {
transform: rotate(180deg);
}
.collapse-chevron view {
position: absolute;
top: 7rpx;
width: 12rpx;
height: 4rpx;
border-radius: 999rpx;
background: linear-gradient(90deg, #fff3b0, #ffd86b);
box-shadow: 0 0 12rpx rgba(255, 216, 107, 0.5);
}
.collapse-chevron view:first-child {
left: 0;
transform: rotate(-38deg);
transform-origin: right center;
}
.collapse-chevron view:last-child {
right: 0;
transform: rotate(38deg);
transform-origin: left center;
}
/* 操作按钮组 */
.message-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14rpx;
margin-top: 20rpx;
}
.action-btn {
height: 72rpx;
min-height: 72rpx;
padding: 0 8rpx;
border-radius: 28rpx;
display: flex;
align-items: center;
justify-content: center;
color: #e8ccff;
font-size: 26rpx;
font-weight: 700;
line-height: 1.15;
text-align: center;
white-space: normal;
box-sizing: border-box;
background: rgba(88, 28, 135, 0.18);
border: 1rpx solid rgba(192, 132, 252, 0.35);
}
.action-btn.primary {
color: #fff;
background: linear-gradient(145deg, #8c44f2, #5f1db8);
}
.action-icon {
margin-right: 8rpx;
font-size: 24rpx;
font-weight: 900;
line-height: 1;
}
.action-btn::after {
border: 0;
}
.empty-flow { .empty-flow {
padding: 80rpx 0; padding: 80rpx 0;
text-align: center; text-align: center;