From 2daaa258a08a9af013f1a2bb6278141e1b6c63b2 Mon Sep 17 00:00:00 2001
From: jing <1683105490@qq.com>
Date: Fri, 11 Sep 2026 14:43:23 +0800
Subject: [PATCH] =?UTF-8?q?chore=EF=BC=9AAI=E6=99=BA=E8=83=BD=E5=8F=AF?=
=?UTF-8?q?=E4=BB=A5=E5=A4=8D=E5=88=B6=E8=AF=AD=E5=8F=A5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
lang-bundle.json | 4 +
lang/en-us/ai-chat/index.js | 12 +-
lang/zh-cn/ai-chat/index.js | 4 +
pages_tool/ai-chat/ai-chat-message.vue | 395 ++++++++++++++++++++++++-
4 files changed, 402 insertions(+), 13 deletions(-)
diff --git a/lang-bundle.json b/lang-bundle.json
index 5a26840..7322e33 100644
--- a/lang-bundle.json
+++ b/lang-bundle.json
@@ -2652,6 +2652,10 @@
"noContentToCopy": "无可复制的内容",
"copySuccess": "已复制",
"copySelected": "已复制选中内容",
+ "selectTextToCopy": "选取文字复制",
+ "selectedText": "已选文字",
+ "copyAll": "复制全部",
+ "selectTextTip": "长按文字拖动两端手柄,可自由选择复制范围",
"copyFail": "复制失败",
"copyFailWithReason": "复制失败:{reason}",
"openingFile": "打开文件中...",
diff --git a/lang/en-us/ai-chat/index.js b/lang/en-us/ai-chat/index.js
index c3b6939..f41d45d 100644
--- a/lang/en-us/ai-chat/index.js
+++ b/lang/en-us/ai-chat/index.js
@@ -33,6 +33,10 @@ export const lang = {
noContentToCopy: 'No content to copy',
copySuccess: 'Copied',
copySelected: 'Copied selected content',
+ selectTextToCopy: 'Select text to copy',
+ selectedText: 'Selected',
+ copyAll: 'Copy all',
+ selectTextTip: 'Long press and drag the handles to choose any range',
copyFail: 'Copy failed',
copyFailWithReason: 'Copy failed: {reason}',
openingFile: 'Opening file...',
@@ -136,8 +140,8 @@ export const lang = {
logInitNavbarFailed: 'Failed to initialize navigation bar',
logChatComponentNotInit: 'Chat component not initialized',
logServicerConfigNotLoaded: '[AI Customer Service] servicerConfig not loaded, retrying after 200ms...'
-
- // ====== AI Agent general copy (customer service is the first agent) ======
- aiAgentDefaultTitle: 'AI Agent',
- aiAgentOpenFailed: 'Failed to open AI Agent, please try again later',
+
+ // ====== AI Agent general copy (customer service is the first agent) ======
+ aiAgentDefaultTitle: 'AI Agent',
+ aiAgentOpenFailed: 'Failed to open AI Agent, please try again later',
aiAgentNotConfigured: 'This AI Agent is not enabled'}
diff --git a/lang/zh-cn/ai-chat/index.js b/lang/zh-cn/ai-chat/index.js
index 752f15b..6cb8f12 100644
--- a/lang/zh-cn/ai-chat/index.js
+++ b/lang/zh-cn/ai-chat/index.js
@@ -33,6 +33,10 @@ export const lang = {
noContentToCopy: '无可复制的内容',
copySuccess: '已复制',
copySelected: '已复制选中内容',
+ selectTextToCopy: '选取文字复制',
+ selectedText: '已选文字',
+ copyAll: '复制全部',
+ selectTextTip: '长按文字拖动两端手柄,可自由选择复制范围',
copyFail: '复制失败',
copyFailWithReason: '复制失败:{reason}',
openingFile: '打开文件中...',
diff --git a/pages_tool/ai-chat/ai-chat-message.vue b/pages_tool/ai-chat/ai-chat-message.vue
index 906a77d..c5a0337 100644
--- a/pages_tool/ai-chat/ai-chat-message.vue
+++ b/pages_tool/ai-chat/ai-chat-message.vue
@@ -44,7 +44,11 @@
@touchend="onBubbleTouchEnd"
@touchcancel="onBubbleTouchEnd">
- {{ message.content }}
+ {{ message.content }}
@@ -78,7 +82,17 @@
@touchcancel="onBubbleTouchEnd">
+
+
+ {{ _stripMarkdown(message.content) }}
+
+
+
@@ -341,6 +355,41 @@
+
+
+
@@ -457,6 +506,12 @@ export default {
localUserNickname: '', // 当前用户昵称
defaultNickname: '', // 默认昵称
showNicknameEditor: false, // 昵称编辑弹窗控制
+ // 选取文字复制弹窗
+ showSelectTextModal: false,
+ selectTextValue: '', // 弹窗内文本域当前内容
+ selectStart: 0, // 当前选中起点
+ selectEnd: 0, // 当前选中终点
+ selectedPreview: '', // 选中文字预览
// AI头像错误状态
avatarError: false,
// 当前选中的 AI 模型, 显示在输入框左侧(如 Auto)
@@ -1811,7 +1866,7 @@ _handleStreamError(msg) {
.trim()
},
- // 长按消息→直接复制
+ // 长按/右键消息 → 弹出操作菜单(复制 / 分享 / 选取文字复制)
onMessageLongPress(message) {
// loading 消息不允许操作
if (message.type === 'loading') return
@@ -1823,7 +1878,164 @@ _handleStreamError(msg) {
} catch (e) { /* 不支持震动的设备忽略 */ }
// #endif
- this.copyMessage(message)
+ this.openMessageMenu(message)
+ },
+
+ // 打开消息操作菜单:复制 / 分享 / 选取文字复制
+ openMessageMenu(message) {
+ const text = message && message.content ? message.content : ''
+ if (!text) return
+
+ // #ifdef H5
+ // H5 优先使用原生浏览器上下文菜单体验(右键已出系统菜单时避免重复弹层)
+ // 仍提供底部操作菜单,保持与小程序一致
+ // #endif
+
+ uni.showActionSheet({
+ itemList: ['复制', '分享', this.$lang('ai-chat.index.selectTextToCopy', '选取文字复制')],
+ success: (res) => {
+ if (res.tapIndex === 0) {
+ this.copyMessage(message)
+ } else if (res.tapIndex === 1) {
+ this.shareMessage(message)
+ } else if (res.tapIndex === 2) {
+ this.openSelectTextModal(message)
+ }
+ },
+ fail: () => { /* 用户取消 */ }
+ })
+ },
+
+ // 分享消息内容(兼容微信小程序与 H5)
+ shareMessage(message) {
+ const text = message && message.content ? message.content : ''
+ if (!text) return
+
+ // #ifdef MP-WEIXIN
+ // 微信小程序:调起分享功能
+ try {
+ wx.showShareMenu({
+ withShareTicket: true,
+ menus: ['shareAppMessage', 'shareTimeline']
+ })
+ } catch (e) {
+ // 若当前页面未配置分享,降级为提示复制
+ uni.showToast({ title: '请长按使用系统分享', icon: 'none' })
+ }
+ // #endif
+
+ // #ifdef H5
+ // H5:调用 Web Share API,降级为复制到剪贴板
+ if (navigator.share) {
+ navigator.share({
+ title: '聊天记录',
+ text: text
+ }).catch(() => { /* 用户取消分享 */ })
+ } else {
+ this.copyMessage(message)
+ uni.showToast({ title: '已复制,可直接粘贴分享', icon: 'none' })
+ }
+ // #endif
+ },
+
+ // 打开"选取文字复制"弹窗:长按后可自由选取任意范围(含单个字符/标点)复制
+ openSelectTextModal(message) {
+ const text = message && message.content ? message.content : ''
+ if (!text) return
+ this.selectTextValue = text
+ this.selectStart = 0
+ this.selectEnd = 0
+ this.selectedPreview = ''
+ this.showSelectTextModal = true
+
+ // 下一帧自动全选,方便用户直接拖动手柄缩小范围(H5 / 微信小程序均适用)
+ this.$nextTick(() => {
+ this.selectStart = 0
+ this.selectEnd = text.length
+ // H5:绑定 textarea DOM 选区变化事件,实时同步选中范围
+ // #ifdef H5
+ try {
+ const el = document.querySelector('.select-text-textarea textarea') || document.querySelector('.select-text-textarea')
+ if (el) {
+ const sync = () => {
+ try {
+ const s = el.selectionStart || 0
+ const e = el.selectionEnd || 0
+ this.selectStart = s
+ this.selectEnd = e
+ this.selectedPreview = (el.value || '').substring(s, e)
+ } catch (err) { /* noop */ }
+ }
+ el.addEventListener('select', sync)
+ el.addEventListener('keyup', sync)
+ el.addEventListener('mouseup', sync)
+ el.addEventListener('touchend', sync)
+ el.focus()
+ el.select && el.select()
+ el.setSelectionRange && el.setSelectionRange(0, el.value.length)
+ sync()
+ }
+ } catch (e) { /* 部分环境不支持 */ }
+ // #endif
+ })
+ },
+
+ // 文本域输入:同步内容(用户若误编辑也能拿到最新文本)
+ onTextInput(event) {
+ const val = (event && event.detail && event.detail.value) || (event && event.target && event.target.value) || ''
+ this.selectTextValue = val
+ },
+
+ // 微信小程序:文本域选区变化(拖动两端手柄选择范围)
+ onTextSelectionChange(event) {
+ try {
+ const detail = event && event.detail
+ if (detail) {
+ this.selectStart = detail.start || 0
+ this.selectEnd = detail.end || 0
+ this.selectedPreview = (this.selectTextValue || '').substring(this.selectStart, this.selectEnd)
+ }
+ } catch (e) { /* noop */ }
+ },
+
+ // 失焦时兜底同步一次选区(H5 拖选结束时)
+ onTextBlur(event) {
+ try {
+ // #ifdef H5
+ const el = document.querySelector('.select-text-textarea textarea') || document.querySelector('.select-text-textarea')
+ if (el) {
+ const s = el.selectionStart || 0
+ const e = el.selectionEnd || 0
+ this.selectStart = s
+ this.selectEnd = e
+ this.selectedPreview = (el.value || '').substring(s, e)
+ }
+ // #endif
+ } catch (e) { /* noop */ }
+ },
+
+ // 复制选中范围(未选则复制全部)
+ copySelectedText() {
+ const text = this.selectTextValue || ''
+ if (!text) {
+ uni.showToast({ title: this.$lang('ai-chat.index.noContentToCopy'), icon: 'none' })
+ return
+ }
+ const start = Math.min(this.selectStart, this.selectEnd)
+ const end = Math.max(this.selectStart, this.selectEnd)
+ const selected = start < end ? text.substring(start, end) : ''
+ const copyText = selected || text
+ this.copyMessage({ content: copyText, _selected: !!selected })
+ this.closeSelectTextModal()
+ },
+
+ // 关闭"选取文字复制"弹窗
+ closeSelectTextModal() {
+ this.showSelectTextModal = false
+ this.selectTextValue = ''
+ this.selectStart = 0
+ this.selectEnd = 0
+ this.selectedPreview = ''
},
// 手动长按检测(touch 事件方案,兼容所有平台)
@@ -1866,17 +2078,28 @@ _handleStreamError(msg) {
return
}
+ // 由"选取文字复制"弹窗传入的已选范围,直接按传入内容复制,不再读取浏览器选区
+ const isSelected = !!message._selected
+
// #ifdef H5
// H5:检测是否有局部文本被选中,有则复制选区文本,无则复制全文
- const selection = window.getSelection()
- const selectedText = selection && selection.toString().trim()
- const finalText = selectedText || copyText
+ let finalText = copyText
+ let selectedText = ''
+ if (!isSelected) {
+ try {
+ const selection = window.getSelection()
+ selectedText = selection && selection.toString().trim()
+ finalText = selectedText || copyText
+ } catch (e) { /* noop */ }
+ } else {
+ selectedText = copyText
+ }
// H5 优先使用 Clipboard API,降级使用 document.execCommand
try {
if (typeof navigator !== 'undefined' && navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(finalText).then(() => {
- uni.showToast({ title: selectedText ? this.$lang('ai-chat.index.copySelected') : this.$lang('ai-chat.index.copySuccess'), icon: 'success' })
+ uni.showToast({ title: selectedText ? this.$lang('ai-chat.index.copySelected', '已复制选中内容') : this.$lang('ai-chat.index.copySuccess'), icon: 'success' })
}).catch(() => {
this._fallbackCopyH5(finalText)
})
@@ -1895,7 +2118,7 @@ _handleStreamError(msg) {
data: copyText,
success: () => {
console.log('[AI Chat] 复制成功')
- uni.showToast({ title: this.$lang('ai-chat.index.copySuccess'), icon: 'success' })
+ uni.showToast({ title: isSelected ? this.$lang('ai-chat.index.copySelected', '已复制选中内容') : this.$lang('ai-chat.index.copySuccess'), icon: 'success' })
},
fail: (err) => {
console.error('[AI Chat] 复制失败, 详细错误:', JSON.stringify(err))
@@ -2536,6 +2759,15 @@ _handleStreamError(msg) {
letter-spacing: 0.3rpx;
color: #1f1f1f;
}
+
+ // 微信小程序: AI 文本消息降级为 selectable text,支持长按选择部分文字
+ .ai-message-text {
+ font-size: 30rpx;
+ line-height: 1.6;
+ color: #1f1f1f;
+ white-space: pre-wrap;
+ word-break: break-word;
+ }
}
.message-time {
@@ -3880,7 +4112,152 @@ _handleStreamError(msg) {
}
}
}
-
+
+/* 选取文字复制弹窗 */
+.select-text-popup {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 1001;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: flex-end;
+
+ .select-text-mask {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: rgba(0, 0, 0, 0.4);
+ }
+
+ .select-text-panel {
+ position: relative;
+ width: 100%;
+ height: 70%;
+ display: flex;
+ flex-direction: column;
+ background-color: #fff;
+ border-radius: 24rpx 24rpx 0 0;
+ overflow: hidden;
+
+ .select-text-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 24rpx 32rpx;
+ border-bottom: 1rpx solid #e5e5e5;
+ background-color: #f9f9f9;
+
+ text {
+ font-size: 32rpx;
+ font-weight: 500;
+ color: #333;
+ }
+
+ .close-btn {
+ width: 64rpx;
+ height: 64rpx;
+ border-radius: 50%;
+ background-color: transparent;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: none;
+ padding: 0;
+
+ text {
+ font-size: 48rpx;
+ color: #999;
+ line-height: 1;
+ }
+ }
+ }
+
+ .select-text-tip {
+ padding: 16rpx 32rpx 0;
+ font-size: 24rpx;
+ color: #999;
+ background-color: #f9f9f9;
+ }
+
+ .select-text-textarea {
+ flex: 1;
+ width: 100%;
+ padding: 32rpx;
+ font-size: 30rpx;
+ line-height: 1.6;
+ color: #333;
+ background-color: #fff;
+ border: none;
+ box-sizing: border-box;
+ }
+
+ .select-text-preview {
+ max-height: 200rpx;
+ overflow-y: auto;
+ padding: 20rpx 32rpx;
+ border-top: 1rpx dashed #e5e5e5;
+ background-color: #f6f8fa;
+ font-size: 26rpx;
+ color: #555;
+ word-break: break-all;
+
+ .select-text-preview-label {
+ color: #999;
+ margin-right: 8rpx;
+ }
+
+ .select-text-preview-content {
+ color: #07c160;
+ font-weight: 500;
+ }
+ }
+
+ .select-text-footer {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 24rpx 32rpx;
+ padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
+ border-top: 1rpx solid #e5e5e5;
+ background-color: #f9f9f9;
+
+ .select-text-copy-btn {
+ flex: 1;
+ height: 80rpx;
+ border-radius: 40rpx;
+ background-color: #07c160;
+ color: #fff;
+ font-size: 28rpx;
+ font-weight: 500;
+ border: none;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-right: 20rpx;
+ }
+
+ .select-text-close-btn {
+ width: 160rpx;
+ height: 80rpx;
+ border-radius: 40rpx;
+ background-color: #f2f2f2;
+ color: #666;
+ font-size: 28rpx;
+ border: none;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+ }
+ }
+}
+
// H5 下允许 rich-text 内容文本选择(鼠标拖选复制)
.text-content,
.markdown-body {