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:
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user