feat(ai-chat): 支持长按复制消息内容
This commit is contained in:
@@ -39,8 +39,13 @@
|
|||||||
<view class="message-nickname" v-else>
|
<view class="message-nickname" v-else>
|
||||||
<text class="message-time">{{ formatTime(message.timestamp) }}</text>
|
<text class="message-time">{{ formatTime(message.timestamp) }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="message-bubble">
|
<view class="message-bubble"
|
||||||
<text class="message-text">{{ message.content }}</text>
|
@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>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -63,7 +68,12 @@
|
|||||||
<text class="ai-nickname-text">智能助手</text>
|
<text class="ai-nickname-text">智能助手</text>
|
||||||
<text class="message-time">{{ formatTime(message.timestamp) }}</text>
|
<text class="message-time">{{ formatTime(message.timestamp) }}</text>
|
||||||
</view>
|
</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">
|
<view v-if="message.type === 'text'" class="text-content">
|
||||||
<rich-text :nodes="parseMarkdown(message.content)" />
|
<rich-text :nodes="parseMarkdown(message.content)" />
|
||||||
@@ -512,7 +522,11 @@ export default {
|
|||||||
lastScrollTop: 0,
|
lastScrollTop: 0,
|
||||||
historyOffset: 0,
|
historyOffset: 0,
|
||||||
isFetchingHistory: false,
|
isFetchingHistory: false,
|
||||||
keyboardHeight: 0
|
keyboardHeight: 0,
|
||||||
|
|
||||||
|
// 手动长按检测相关(touch 计时器)
|
||||||
|
_longPressTimer: null,
|
||||||
|
_longPressMessage: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -1756,6 +1770,125 @@ _handleStreamError(msg) {
|
|||||||
this.$emit('action-click', { action, message })
|
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) {
|
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>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user