feat(ai-chat): 支持长按复制消息内容

This commit is contained in:
2026-05-16 11:08:46 +08:00
parent 0bc1b5e90c
commit 76afe5f433

View File

@@ -39,8 +39,13 @@
<view class="message-nickname" v-else>
<text class="message-time">{{ formatTime(message.timestamp) }}</text>
</view>
<view class="message-bubble">
<text class="message-text">{{ message.content }}</text>
<view class="message-bubble"
@contextmenu.prevent="onMessageLongPress(message)"
@touchstart="onBubbleTouchStart($event, message)"
@touchmove="onBubbleTouchMove"
@touchend="onBubbleTouchEnd"
@touchcancel="onBubbleTouchEnd">
<text class="message-text" selectable>{{ message.content }}</text>
</view>
</view>
</view>
@@ -63,7 +68,12 @@
<text class="ai-nickname-text">智能助手</text>
<text class="message-time">{{ formatTime(message.timestamp) }}</text>
</view>
<view class="message-bubble">
<view class="message-bubble"
@contextmenu.prevent="onMessageLongPress(message)"
@touchstart="onBubbleTouchStart($event, message)"
@touchmove="onBubbleTouchMove"
@touchend="onBubbleTouchEnd"
@touchcancel="onBubbleTouchEnd">
<!-- 文本消息 -->
<view v-if="message.type === 'text'" class="text-content">
<rich-text :nodes="parseMarkdown(message.content)" />
@@ -512,7 +522,11 @@ export default {
lastScrollTop: 0,
historyOffset: 0,
isFetchingHistory: false,
keyboardHeight: 0
keyboardHeight: 0,
// 手动长按检测相关(touch 计时器)
_longPressTimer: null,
_longPressMessage: null
}
},
computed: {
@@ -1755,7 +1769,126 @@ _handleStreamError(msg) {
this.$emit('action-click', { action, message })
},
// 长按消息→直接复制
onMessageLongPress(message) {
// loading 消息不允许操作
if (message.type === 'loading') return
// #ifdef MP-WEIXIN
// 微信小程序:震动反馈提升手感(小米等 Android 设备也可用)
try {
wx.vibrateShort({ type: 'medium' })
} catch (e) { /* 不支持震动的设备忽略 */ }
// #endif
this.copyMessage(message)
},
// 手动长按检测(touch 事件方案,兼容所有平台)
// 在 H5 desktop 上用 @contextmenu 触发,
// 在 H5 mobile / 小米微信小程序等场景靠 touch 计时器兜底
onBubbleTouchStart(event, message) {
// 单指长按才触发,忽略多点触控
if (event.touches && event.touches.length > 1) return
this._longPressMessage = message
this._longPressTimer = setTimeout(() => {
this._longPressTimer = null
this.onMessageLongPress(message)
}, 500) // 和系统长按接近的 500ms
},
onBubbleTouchMove() {
// 手指移动(滚动列表)→ 取消长按
if (this._longPressTimer) {
clearTimeout(this._longPressTimer)
this._longPressTimer = null
}
this._longPressMessage = null
},
onBubbleTouchEnd() {
// 手指提前松开 → 取消长按
if (this._longPressTimer) {
clearTimeout(this._longPressTimer)
this._longPressTimer = null
}
this._longPressMessage = null
},
// 复制消息内容到剪贴板(兼容 H5 / 微信小程序)
copyMessage(message) {
let copyText = message.content || ''
if (!copyText && message.type === 'text') {
copyText = message.content || ''
}
if (!copyText) {
uni.showToast({ title: '无可复制的内容', icon: 'none' })
return
}
// #ifdef H5
// H5:检测是否有局部文本被选中,有则复制选区文本,无则复制全文
const selection = window.getSelection()
const selectedText = selection && selection.toString().trim()
const finalText = selectedText || copyText
// H5 优先使用 Clipboard API,降级使用 document.execCommand
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(finalText).then(() => {
uni.showToast({ title: selectedText ? '已复制选中内容' : '已复制', icon: 'success' })
}).catch(() => {
this._fallbackCopyH5(finalText)
})
} else {
this._fallbackCopyH5(finalText)
}
} catch (e) {
this._fallbackCopyH5(finalText)
}
// #endif
// #ifdef MP-WEIXIN
// 微信小程序专用(含小米等 Android 兼容)
console.log('[AI Chat] 开始复制, 内容长度:', copyText.length, '内容前50字:', copyText.substring(0, 50))
uni.setClipboardData({
data: copyText,
success: () => {
console.log('[AI Chat] 复制成功')
uni.showToast({ title: '已复制', icon: 'success' })
},
fail: (err) => {
console.error('[AI Chat] 复制失败, 详细错误:', JSON.stringify(err))
console.error('[AI Chat] 复制失败 errMsg:', err.errMsg || err.message || '无错误消息')
uni.showToast({ title: '复制失败: ' + (err.errMsg || '未知错误'), icon: 'none', duration: 3000 })
}
})
// #endif
},
// H5 降级复制方案(document.execCommand + textarea)
_fallbackCopyH5(text) {
try {
const textarea = document.createElement('textarea')
textarea.value = text
textarea.style.position = 'fixed'
textarea.style.left = '-9999px'
textarea.style.top = '-9999px'
textarea.style.opacity = '0'
document.body.appendChild(textarea)
textarea.select()
textarea.setSelectionRange(0, text.length)
const success = document.execCommand('copy')
document.body.removeChild(textarea)
if (success) {
uni.showToast({ title: '已复制', icon: 'success' })
} else {
uni.showToast({ title: '复制失败', icon: 'none' })
}
} catch (e) {
uni.showToast({ title: '复制失败', icon: 'none' })
}
},
// 重试消息
async retryMessage(message) {
// 找到对应的用户消息
@@ -4066,5 +4199,12 @@ $radius-lg: 36rpx;
}
}
}
// H5 下允许 rich-text 内容文本选择(鼠标拖选复制)
.text-content,
.markdown-body {
user-select: text;
-webkit-user-select: text;
}
}
</style>