From e16402c9c116b7d482433b477d0cbe1f1ae6c0f6 Mon Sep 17 00:00:00 2001
From: ZF sun <34314687@qq.com>
Date: Mon, 11 May 2026 17:14:26 +0800
Subject: [PATCH 01/13] =?UTF-8?q?fix(ai):=20=E4=BF=AE=E5=A4=8DWebSocket?=
=?UTF-8?q?=E9=87=8D=E5=A4=8D=E5=9B=9E=E8=B0=83=E5=8F=8A=E6=84=8F=E5=A4=96?=
=?UTF-8?q?=E5=85=B3=E9=97=AD=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
common/js/ai-service.js | 60 +++++++++++++++++++++-----
pages_tool/ai-chat/ai-chat-message.vue | 8 +++-
2 files changed, 55 insertions(+), 13 deletions(-)
diff --git a/common/js/ai-service.js b/common/js/ai-service.js
index 9bda135..1b3a278 100644
--- a/common/js/ai-service.js
+++ b/common/js/ai-service.js
@@ -88,6 +88,16 @@ export default {
return new Promise((resolve, reject) => {
const match = Config.baseUrl.match(/^(?:https?:\/\/|wss?:\/\/)([^\/\s:]+)/);
const domain = match ? match[1] : '';
+
+ // 修复 5:Domain 提取失败时提前拒绝
+ if (!domain) {
+ const errorMsg = 'WebSocket 域名配置无效';
+ console.error(errorMsg);
+ reject(new Error(errorMsg));
+ if (onComplete) onComplete({ error: errorMsg });
+ return;
+ }
+
const wssUrl = `wss://${domain}/ws/aikefu`;
const socketTask = wx.connectSocket({
url: wssUrl,
@@ -97,6 +107,29 @@ export default {
let content = '';
let conversationId = '';
let isAuthenticated = false;
+ let closedNormally = false;
+ let settled = false;
+
+ // 安全地拒绝(防止重复调用)
+ function safeReject(error) {
+ if (settled) return;
+ settled = true;
+ clearTimeout(timeout);
+ reject(error);
+ }
+
+ // 安全地完成
+ function safeResolve(result) {
+ if (settled) return;
+ settled = true;
+ clearTimeout(timeout);
+ resolve(result);
+ }
+
+ // 安全地触发 onComplete
+ function safeComplete(result) {
+ if (onComplete) onComplete(result);
+ }
socketTask.onOpen(() => {
socketTask.send({
@@ -128,8 +161,8 @@ export default {
} else if (data.type === 'auth_failed') {
const errorMsg = '认证失败,请重新登录';
console.error(errorMsg, data);
- reject(new Error(errorMsg));
- if (onComplete) onComplete({ error: errorMsg });
+ safeComplete({ error: errorMsg });
+ safeReject(new Error(errorMsg));
socketTask.close();
}
@@ -146,10 +179,9 @@ export default {
if (conversationId) {
this.setConversationId(conversationId);
}
- if (onComplete) {
- onComplete({ content, conversation_id: conversationId });
- }
- resolve({ content, conversation_id: conversationId });
+ closedNormally = true;
+ safeComplete({ content, conversation_id: conversationId });
+ safeResolve({ content, conversation_id: conversationId });
socketTask.close();
}
@@ -165,21 +197,27 @@ export default {
socketTask.onError((err) => {
const errorMsg = 'WebSocket 连接失败';
console.error(errorMsg, err);
- reject(new Error(errorMsg));
- if (onComplete) onComplete({ error: errorMsg });
+ safeComplete({ error: errorMsg });
+ safeReject(new Error(errorMsg));
});
socketTask.onClose(() => {
+ if (!closedNormally && !settled) {
+ const errorMsg = 'WebSocket 连接意外关闭';
+ console.error(errorMsg);
+ safeComplete({ error: errorMsg });
+ safeReject(new Error(errorMsg));
+ }
});
const timeout = setTimeout(() => {
if (!isAuthenticated || content === '') {
console.warn('WebSocket 超时,强制关闭');
socketTask.close();
- reject(new Error('AI服务响应超时'));
- if (onComplete) onComplete({ error: 'AI服务响应超时' });
+ safeComplete({ error: 'AI服务响应超时' });
+ safeReject(new Error('AI服务响应超时'));
}
- }, 10000);
+ }, 20000);
});
// #endif
},
diff --git a/pages_tool/ai-chat/ai-chat-message.vue b/pages_tool/ai-chat/ai-chat-message.vue
index ac1e8df..2eddd4d 100644
--- a/pages_tool/ai-chat/ai-chat-message.vue
+++ b/pages_tool/ai-chat/ai-chat-message.vue
@@ -1010,8 +1010,10 @@ async loadChatHistoryIfExist() {
await aiService.sendHttpStream(
userMessage,
(chunk) => {
- // 实时更新内容(不频繁滚动)
+ // 实时更新内容
streamMessage.content += chunk;
+ // 流式更新时滚动到底部显示最新内容(防抖)
+ this.scrollToBottom();
},
(completeResult) => {
// 流结束回调
@@ -1054,8 +1056,10 @@ async loadChatHistoryIfExist() {
await aiService.sendStreamMessage(
userMessage,
(chunk) => {
- // 实时更新内容(不频繁滚动)
+ // 实时更新内容
streamMessage.content += chunk;
+ // 流式更新时滚动到底部显示最新内容(防抖)
+ this.scrollToBottom();
},
(completeResult) => {
// 流结束回调
From 875cf0b42803bc0fa9381b80a4a4cf48100a83af Mon Sep 17 00:00:00 2001
From: ZF sun <34314687@qq.com>
Date: Tue, 12 May 2026 08:30:26 +0800
Subject: [PATCH 02/13] =?UTF-8?q?feat(ai-chat):=20=E5=A2=9E=E5=BC=BAMarkdo?=
=?UTF-8?q?wn=E8=A7=A3=E6=9E=90=E5=8F=8A=E8=B0=83=E6=95=B4=E8=81=8A?=
=?UTF-8?q?=E5=A4=A9=E6=B0=94=E6=B3=A1=E5=B8=83=E5=B1=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.local.config.js | 2 +-
pages_tool/ai-chat/ai-chat-message.vue | 198 +++++++++++++++++++++++--
2 files changed, 184 insertions(+), 16 deletions(-)
diff --git a/.local.config.js b/.local.config.js
index 1902e69..4d86284 100644
--- a/.local.config.js
+++ b/.local.config.js
@@ -62,5 +62,5 @@ const localDevConfig = ({
uniacid: 2075,
domain: 'https://xcx.aigc-quickapp.com/',
},
-})['2811']; // 选择要使用的环境配置
+})['1']; // 选择要使用的环境配置
export default localDevConfig;
\ No newline at end of file
diff --git a/pages_tool/ai-chat/ai-chat-message.vue b/pages_tool/ai-chat/ai-chat-message.vue
index 2eddd4d..60ce73a 100644
--- a/pages_tool/ai-chat/ai-chat-message.vue
+++ b/pages_tool/ai-chat/ai-chat-message.vue
@@ -1126,20 +1126,190 @@ async loadChatHistoryIfExist() {
}
return `感谢您的咨询!关于"${userMessage}"的问题,我们的专业客服会尽快为您解答。您也可以查看我们的帮助文档获取更多信息。`;
},
-
-
- // 解析Markdown
+
+
+ // 增强版Markdown解析(无第三方依赖)
parseMarkdown(content) {
if (!content) return ''
-
- // 简单的Markdown解析
+
+ // 辅助:内联格式处理
+ const parseInline = (text) => {
+ return text
+ // 行内图片
+ .replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '
')
+ // 链接
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1')
+ // 行内代码
+ .replace(/`([^`]+)`/g, '$1')
+ // 粗体(优先匹配**)
+ .replace(/\*\*(.+?)\*\*/g, '$1')
+ // 斜体
+ .replace(/\*(.+?)\*/g, '$1')
+ // 删除线
+ .replace(/~~(.+?)~~/g, '$1')
+ // 手机号
+ .replace(/\b(1[3-9]\d{9})\b/g, '$1')
+ // 400电话
+ .replace(/\b(400[\d-]{7,10})\b/g, '$1')
+ }
+
+ // 1. 转义HTML特殊字符
let html = content
- .replace(/\*\*(.*?)\*\*/g, '$1') // 粗体
- .replace(/\*(.*?)\*/g, '$1') // 斜体
- .replace(/`(.*?)`/g, '$1') // 代码
- .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1') // 链接
- .replace(/\n/g, '
') // 换行
-
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+
+ // 2. 提取代码块(保护其内容不被后续处理污染)
+ const codeBlocks = []
+ html = html.replace(/```(\w*)\n?([\s\S]*?)```/g, (m, lang, code) => {
+ const idx = codeBlocks.length
+ const safe = code.replace(//g, '>')
+ codeBlocks.push(
+ `
` +
+ `${safe}
`
+ )
+ return `%%CODEBLOCK_${idx}%%`
+ })
+
+ // 3. 按行处理,用
连接单换行,双换行分割段落块
+ const lines = html.split('\n')
+ const out = []
+ let inList = false
+ let buf = [] // 普通文本行缓冲区
+ let inTable = false
+ let tableRows = [] // 表格行缓冲区
+
+ const closeList = () => {
+ if (inList === 'ul') { inList = false; return '' }
+ if (inList === 'ol') { inList = false; return '' }
+ return ''
+ }
+ const flushBuf = () => {
+ if (buf.length === 0) return
+ // 去掉首尾的空串,避免产生尾随
+ while (buf.length > 0 && buf[0] === '') buf.shift()
+ while (buf.length > 0 && buf[buf.length - 1] === '') buf.pop()
+ if (buf.length === 0) return
+ const content = buf.map(l => parseInline(l)).join('
')
+ out.push(`${content}
`)
+ buf = []
+ }
+ const flushTable = () => {
+ if (!inTable || tableRows.length === 0) return
+ const thead = tableRows[0].map(c =>
+ `${parseInline(c)} | `
+ ).join('')
+ const tbody = tableRows.slice(1).map(row =>
+ '' + row.map(c =>
+ `| ${parseInline(c)} | `
+ ).join('') + '
'
+ ).join('')
+ out.push(
+ ``
+ )
+ tableRows = []
+ inTable = false
+ }
+
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i]
+ const trimmed = line.trim()
+
+ // 代码块占位符
+ if (/^%%CODEBLOCK_\d+%%$/.test(trimmed)) {
+ flushBuf(); flushTable(); out.push(closeList())
+ out.push(line)
+ continue
+ }
+
+ // 表格行(以 | 开头)
+ if (trimmed.charAt(0) === '|') {
+ flushBuf(); out.push(closeList())
+ // 去除首尾空 cell
+ const rawCells = trimmed.split('|').filter((c, j, a) => j > 0 && j < a.length - 1)
+ if (rawCells.length === 0) continue
+ // 跳过分隔行 | --- | :--- | :---: | ---: |
+ const isSep = rawCells.every(c => /^[\s:\-|]+$/.test(c.trim()))
+ if (isSep) continue
+ if (!inTable) { inTable = true; tableRows = [] }
+ tableRows.push(rawCells.map(c => c.trim()))
+ continue
+ }
+ // 非表格行 → 刷新已积累的表格
+ if (inTable) flushTable()
+
+ // 标题
+ const hMatch = line.match(/^(#{1,6})\s+(.+)$/)
+ if (hMatch) {
+ flushBuf(); out.push(closeList())
+ const lv = hMatch[1].length
+ const size = ['', '40rpx', '36rpx', '32rpx', '28rpx', '26rpx', '24rpx'][lv]
+ const wgt = ['', '700', '700', '600', '600', '500', '500'][lv]
+ const mg = ['', '20rpx 0 10rpx', '16rpx 0 8rpx', '14rpx 0 6rpx', '12rpx 0 6rpx', '10rpx 0 4rpx', '8rpx 0 4rpx'][lv]
+ out.push(`${parseInline(hMatch[2])}`)
+ continue
+ }
+
+ // 分隔线
+ if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(trimmed)) {
+ flushBuf(); out.push(closeList())
+ out.push('
')
+ continue
+ }
+
+ // 引用
+ const bqMatch = line.match(/^>\s?(.+)$/)
+ if (bqMatch) {
+ flushBuf(); out.push(closeList())
+ out.push(`${parseInline(bqMatch[1])}
`)
+ continue
+ }
+
+ // 无序列表
+ const ulMatch = line.match(/^[-*+]\s+(.+)$/)
+ if (ulMatch) {
+ flushBuf()
+ if (!inList) { out.push(''); inList = 'ul' }
+ out.push(`- ${parseInline(ulMatch[1])}
`)
+ continue
+ }
+
+ // 有序列表
+ const olMatch = line.match(/^\d+\.\s+(.+)$/)
+ if (olMatch) {
+ flushBuf()
+ if (!inList) { out.push(''); inList = 'ol' }
+ out.push(`- ${parseInline(olMatch[1])}
`)
+ continue
+ }
+
+ // 空行 → 加入缓冲区作段落分隔(
),同时关闭列表
+ if (trimmed === '') {
+ // 关闭列表(标准Markdown行为)
+ if (inList) {
+ flushBuf()
+ out.push(closeList())
+ }
+ // 避免连续空行推入多个空串
+ if (buf.length > 0 && buf[buf.length - 1] !== '') {
+ buf.push('')
+ }
+ continue
+ }
+
+ // 普通文本行 => 加入缓冲区
+ buf.push(line)
+ }
+
+ // 扫尾
+ flushBuf(); flushTable(); out.push(closeList())
+
+ html = out.join('')
+
+ // 4. 还原代码块
+ html = html.replace(/%%CODEBLOCK_(\d+)%%/g, (m, idx) => codeBlocks[parseInt(idx)])
+
return html
},
@@ -2045,7 +2215,7 @@ $radius-lg: 36rpx;
.message-content {
flex: 1;
margin-left: 24rpx;
- max-width: calc(100% - 112rpx);
+ max-width: calc(100% - 96rpx);
min-width: 0;
width: 0; /* 添加这个属性防止flex元素溢出 */
@@ -2543,7 +2713,6 @@ $radius-lg: 36rpx;
height: auto;
padding: 16rpx 20rpx; /* 微信风格内边距 */
display: inline-block;
- max-width: 70%; /* 微信风格宽度 */
word-break: break-word;
white-space: pre-wrap;
line-height: 1.5;
@@ -2580,7 +2749,6 @@ $radius-lg: 36rpx;
height: auto;
padding: 16rpx 20rpx; /* 微信风格内边距 */
display: inline-block;
- max-width: 70%; /* 微信风格宽度 */
word-break: break-word;
white-space: pre-wrap;
line-height: 1.5;
@@ -3513,7 +3681,7 @@ $radius-lg: 36rpx;
.message-content {
margin-left: 16rpx;
- max-width: calc(100% - 88rpx);
+ max-width: calc(100% - 72rpx);
.message-bubble {
padding: 16rpx 24rpx;
From 0d18041037c762a0fbeb3cbd831001c4ea8bcbf3 Mon Sep 17 00:00:00 2001
From: ZF sun <34314687@qq.com>
Date: Tue, 12 May 2026 08:48:55 +0800
Subject: [PATCH 03/13] =?UTF-8?q?refactor:=20=E4=BD=BF=E7=94=A8=20Intl.Dat?=
=?UTF-8?q?eTimeFormat=20=E7=94=9F=E6=88=90=E6=97=B6=E9=97=B4=E6=88=B3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
vue.config.js | 19 +++++++++----------
1 file changed, 9 insertions(+), 10 deletions(-)
diff --git a/vue.config.js b/vue.config.js
index ad9dbf7..fa26f85 100644
--- a/vue.config.js
+++ b/vue.config.js
@@ -217,16 +217,15 @@ class VersionInjectPlugin {
console.log(`[H5Zip] 输出目录名正确: ${outputDirName}`);
}
- // 上海时区时间戳格式: 20260511_1058
- const now = new Date();
- const shanghaiOffset = 8 * 60;
- const shanghaiTime = new Date(now.getTime() + shanghaiOffset * 60 * 1000);
- const y = shanghaiTime.getFullYear();
- const M = String(shanghaiTime.getMonth() + 1).padStart(2, '0');
- const d = String(shanghaiTime.getDate()).padStart(2, '0');
- const h = String(shanghaiTime.getHours()).padStart(2, '0');
- const m = String(shanghaiTime.getMinutes()).padStart(2, '0');
- const ts = `${y}${M}${d}_${h}${m}`;
+ // 标准时区时间戳格式: 20260511_1058
+ const fmt = new Intl.DateTimeFormat('en-CA', {
+ timeZone: 'Asia/Shanghai',
+ year: 'numeric', month: '2-digit', day: '2-digit',
+ hour: '2-digit', minute: '2-digit', hour12: false
+ });
+ const parts = fmt.formatToParts(new Date());
+ const get = (t) => parts.find(p => p.type === t).value;
+ const ts = `${get('year')}${get('month')}${get('day')}_${get('hour')}${get('minute')}`;
const zipFileName = `${outputDirName}-${this._cachedCommitId}-${ts}.zip`;
const zipFilePath = path.join(h5Dir, zipFileName);
From 5e837711932e7a12b22c0970f1fb435ab8e7023c Mon Sep 17 00:00:00 2001
From: ZF sun <34314687@qq.com>
Date: Tue, 12 May 2026 09:59:43 +0800
Subject: [PATCH 04/13] =?UTF-8?q?chore:=20=E4=BF=9D=E7=95=99H5=E6=89=93?=
=?UTF-8?q?=E5=8C=85=E5=90=8E=E7=9A=84=E6=BA=90=E7=9B=AE=E5=BD=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
vue.config.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/vue.config.js b/vue.config.js
index fa26f85..563bf9a 100644
--- a/vue.config.js
+++ b/vue.config.js
@@ -247,8 +247,8 @@ class VersionInjectPlugin {
execSync(command, { stdio: 'inherit' });
console.log(`✅ H5 ZIP: ${zipFileName}`);
// 删除已打包的源目录
- fs.rmSync(targetDir, { recursive: true, force: true });
- console.log(`[H5Zip] 已删除源目录: ${outputDirName}`);
+ // fs.rmSync(targetDir, { recursive: true, force: true });
+ // console.log(`[H5Zip] 已删除源目录: ${outputDirName}`);
} catch (e) {
console.error('H5 ZIP 打包失败:', e.message);
}
From e789ab2ca36091e891e47fe853e325db1f0b9780 Mon Sep 17 00:00:00 2001
From: ZF sun <34314687@qq.com>
Date: Tue, 12 May 2026 11:39:43 +0800
Subject: [PATCH 05/13] =?UTF-8?q?fix(H5):=20=E4=BB=8E=20package.json=20?=
=?UTF-8?q?=E8=AF=BB=E5=8F=96=20UNI=5FAPP=5FDOMAIN=20=E5=85=9C=E5=BA=95?=
=?UTF-8?q?=EF=BC=8C=E7=A1=AE=E4=BF=9D=20H5=20=E5=8F=91=E8=A1=8C=E5=9F=9F?=
=?UTF-8?q?=E5=90=8D=E6=AD=A3=E5=B8=B8=E6=B3=A8=E5=85=A5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
common/js/config.js | 4 ++--
vue.config.js | 17 +++++++++++++++++
2 files changed, 19 insertions(+), 2 deletions(-)
diff --git a/common/js/config.js b/common/js/config.js
index 2c7e3e1..89ec42c 100644
--- a/common/js/config.js
+++ b/common/js/config.js
@@ -21,9 +21,9 @@ try {
let defaultDomain = '';
// #ifdef H5
-// 域名由 package.json 中对应脚本的 env.UNI_APP_DOMAIN 传入
+// 域名由 vue.config.js 中 DefinePlugin 注入的 process.env.UNI_APP_DOMAIN 传入
try {
- if (process?.env?.UNI_APP_DOMAIN) {
+ if (process.env.UNI_APP_DOMAIN) {
defaultDomain = process.env.UNI_APP_DOMAIN;
}
} catch (e) {}
diff --git a/vue.config.js b/vue.config.js
index 563bf9a..7da20a9 100644
--- a/vue.config.js
+++ b/vue.config.js
@@ -5,6 +5,23 @@ const { zionUniMpLoadPackagePlugin } = require('zion-uniapp-mp-load-package/webp
console.log('[vue.config] 已加载');
+// 确保 UNI_APP_DOMAIN 有值(从 package.json 脚本配置中读取)
+if (!process.env.UNI_APP_DOMAIN && process.env.UNI_SCRIPT) {
+ try {
+ const pkg = require('./package.json');
+ const scriptCfg = pkg['uni-app']?.scripts?.[process.env.UNI_SCRIPT];
+ if (scriptCfg?.env?.UNI_APP_DOMAIN) {
+ process.env.UNI_APP_DOMAIN = scriptCfg.env.UNI_APP_DOMAIN;
+ }
+ } catch (e) {}
+}
+
+// 输出 H5 域名信息到控制台
+if (process.env.UNI_PLATFORM === 'h5') {
+ console.log(`\n[H5 发行] 脚本: ${process.env.UNI_SCRIPT || '(默认)'}`);
+ console.log(`[H5 发行] 域名: ${process.env.UNI_APP_DOMAIN || '(未配置)'}\n`);
+}
+
// 打印出process.env
// for (let key in process.env) {
// console.log(key, process.env[key]);
From d5ca77e2d87753896773554b25be5c5ec952d6e5 Mon Sep 17 00:00:00 2001
From: ZF sun <34314687@qq.com>
Date: Wed, 13 May 2026 08:40:01 +0800
Subject: [PATCH 06/13] =?UTF-8?q?fix(ai-chat):=20=E8=B7=B3=E8=BF=87?=
=?UTF-8?q?=E5=81=A5=E5=BA=B7=E6=A3=80=E6=9F=A5=E9=81=BF=E5=85=8D=E6=9C=8D?=
=?UTF-8?q?=E5=8A=A1=E8=AF=AF=E5=88=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
pages_tool/ai-chat/ai-chat-message.vue | 11 ++---------
1 file changed, 2 insertions(+), 9 deletions(-)
diff --git a/pages_tool/ai-chat/ai-chat-message.vue b/pages_tool/ai-chat/ai-chat-message.vue
index 60ce73a..babb4a2 100644
--- a/pages_tool/ai-chat/ai-chat-message.vue
+++ b/pages_tool/ai-chat/ai-chat-message.vue
@@ -906,15 +906,8 @@ async loadChatHistoryIfExist() {
this.messages.push(loadingMessage)
try {
- // 检查AI服务状态
- const status = await aiService.getServiceStatus()
- this.isAIServiceAvailable = status.available
-
- if (!this.isAIServiceAvailable) {
- throw new Error('AI服务暂不可用')
- }
-
- // 使用AI服务获取回复
+ // 直接发送消息(跳过健康检查,避免健康检查接口格式不匹配导致误判)
+ // 如果服务不可用,实际发送时会抛出异常,由 catch 块处理
const enhancedMessage = this.enhanceEmojiMessage(userMessage.content)
if (this.enableStreaming) {
await this.sendStreamMessage(enhancedMessage)
From b2688dc8a1fc4c8d714a275278179994ddeb047f Mon Sep 17 00:00:00 2001
From: ZF sun <34314687@qq.com>
Date: Wed, 13 May 2026 09:00:20 +0800
Subject: [PATCH 07/13] =?UTF-8?q?fix(ai-chat):=20=E4=BF=AE=E5=A4=8D?=
=?UTF-8?q?=E5=8E=86=E5=8F=B2=E6=B6=88=E6=81=AF=E5=8A=A0=E8=BD=BD=E9=80=BB?=
=?UTF-8?q?=E8=BE=91=E5=8F=8A=E8=B6=85=E6=97=B6=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
common/js/ai-service.js | 8 -
pages_tool/ai-chat/ai-chat-message.vue | 197 +++++++++++++------------
2 files changed, 99 insertions(+), 106 deletions(-)
diff --git a/common/js/ai-service.js b/common/js/ai-service.js
index 1b3a278..c99000c 100644
--- a/common/js/ai-service.js
+++ b/common/js/ai-service.js
@@ -210,14 +210,6 @@ export default {
}
});
- const timeout = setTimeout(() => {
- if (!isAuthenticated || content === '') {
- console.warn('WebSocket 超时,强制关闭');
- socketTask.close();
- safeComplete({ error: 'AI服务响应超时' });
- safeReject(new Error('AI服务响应超时'));
- }
- }, 20000);
});
// #endif
},
diff --git a/pages_tool/ai-chat/ai-chat-message.vue b/pages_tool/ai-chat/ai-chat-message.vue
index babb4a2..fa9d3c6 100644
--- a/pages_tool/ai-chat/ai-chat-message.vue
+++ b/pages_tool/ai-chat/ai-chat-message.vue
@@ -534,28 +534,30 @@ export default {
}
},
onShow() {
- // 如果不是 AI 客服,立即退出并跳转
- if (customerServiceType !== 'ai') {
- uni.showToast({ title: '当前客服类型不支持此页面', icon: 'none' });
- setTimeout(() => {
- uni.navigateBack({ delta: 1 }); // 或 redirectTo 首页
- }, 1000);
- return; // 关键:阻止后续初始化
- }
- // 优先读取本地缓存的会话 ID
- const localConvId = this.getConversationIdFromLocal();
- if (localConvId) {
- this.currentConversationId = localConvId;
- aiService.setConversationId(localConvId);
- } else {
- this.currentConversationId = aiService.getConversationId();
- }
- // 加载初始历史后,同步historyOffset
- this.loadChatHistoryIfExist().then(() => {
- // 初始历史条数作为偏移量
- this.historyOffset = this.messages.length;
- });
- },
+ // 如果不是 AI 客服,立即退出并跳转
+ if (customerServiceType !== 'ai') {
+ uni.showToast({ title: '当前客服类型不支持此页面', icon: 'none' });
+ setTimeout(() => {
+ uni.navigateBack({ delta: 1 });
+ }, 1000);
+ return;
+ }
+ // 只在初次渲染或会话ID发生变化时才加载历史
+ const localConvId = this.getConversationIdFromLocal();
+ const shouldReload = this.currentConversationId !== localConvId;
+
+ if (localConvId && shouldReload) {
+ // 切换到已有的历史会话 → 重新加载历史
+ this.currentConversationId = localConvId;
+ aiService.setConversationId(localConvId);
+ this.loadChatHistoryIfExist();
+ // 注意: historyOffset 和 hasMoreHistory 在 loadChatHistoryIfExist() 内部设置
+ } else if (!localConvId && !this.currentConversationId) {
+ // 全新会话,初始化偏移量
+ this.historyOffset = this.messages.length;
+ }
+ // 会话未变化且已有消息 → 跳过加载,保留当前消息
+ },
// 新增:组件销毁时清除防抖定时器
beforeDestroy() {
if (this.scrollTimer) clearTimeout(this.scrollTimer);
@@ -631,8 +633,8 @@ export default {
// 防抖处理:50ms内只执行一次,避免频繁触发
if (this.scrollTimer) clearTimeout(this.scrollTimer);
this.scrollTimer = setTimeout(() => {
- // 滚动到顶部附近(<20px)、不在加载中、显示加载更多时,触发加载
- if (currentScrollTop < 20 && !this.isLoadingHistory && this.showLoadMore) {
+ // 滚动到顶部附近(<20px)、不在加载中、还有更多历史时,触发加载
+ if (currentScrollTop < 20 && !this.isLoadingHistory && this.hasMoreHistory) {
this.loadMoreHistory();
}
}, 50);
@@ -682,7 +684,7 @@ async loadChatHistoryIfExist() {
const realTimestamp = msg.created_at || msg.create_time || msg.time;
return {
id: msg.message_id || (Date.now() + Math.random() * 10000),
- role: msg.role === 'user' ? 'user' : 'assistant',
+ role: msg.role === 'user' ? 'user' : 'ai',
content: content,
timestamp: realTimestamp,
actions: msg.role !== 'user' ? [
@@ -691,6 +693,10 @@ async loadChatHistoryIfExist() {
] : []
};
}).filter(msg => msg.content.trim() !== ''); // 过滤掉内容为空的消息
+ // 根据服务端 total 判断是否还有更多历史可加载
+ const msgCount = this.messages.length;
+ this.hasMoreHistory = response.total > msgCount;
+ this.historyOffset = msgCount;
} catch (error) {
console.error('加载历史记录失败:', error);
// 只需调用服务层的清除方法,它会处理本地存储
@@ -1393,80 +1399,74 @@ async loadChatHistoryIfExist() {
},
// 加载更多历史消息
- async loadMoreHistory() {
- if (!this.currentConversationId) {
- this.currentConversationId = this.getConversationIdFromLocal();
- aiService.setConversationId(this.currentConversationId);
- }
- // 防止重复加载
- if (!this.showLoadMore || !this.currentConversationId || this.isLoadingHistory) {
- return;
- }
- this.isFetchingHistory = true;
- this.isLoadingHistory = true;
- this.loadingText = '加载历史消息中...';
- this.hasMoreHistory = true;
-
- try {
- // 关键修改:用独立的historyOffset作为偏移量,不再依赖messages.length
- const response = await aiService.getConversationHistory({
- conversation_id: this.currentConversationId,
- limit: 20,
- offset: this.historyOffset // 改用独立偏移量
- });
-
+ async loadMoreHistory() {
+ if (!this.currentConversationId) {
+ this.currentConversationId = this.getConversationIdFromLocal();
+ aiService.setConversationId(this.currentConversationId);
+ }
+ // 防止重复加载
+ if (!this.hasMoreHistory || !this.currentConversationId || this.isLoadingHistory) {
+ return;
+ }
+ this.isFetchingHistory = true;
+ this.isLoadingHistory = true;
+ this.loadingText = '加载历史消息中...';
+
+ try {
+ const response = await aiService.getConversationHistory({
+ conversation_id: this.currentConversationId,
+ limit: 20,
+ offset: this.historyOffset
+ });
+
if (response.success && Array.isArray(response.messages) && response.messages.length > 0) {
const historyMessages = response.messages.map(msg => {
- // 只使用后端返回的真实时间,不使用假时间
- const realTimestamp = msg.create_time || msg.created_at || msg.time;
- return {
- id: msg.id || `history-${Date.now()}-${Math.random().toString(36).slice(2)}`,
- role: msg.role === 'user' ? 'user' : 'ai',
- type: 'text',
- content: msg.content,
- timestamp: realTimestamp
- };
- });
-
- // 1. 记录新增消息数量
- const newMsgCount = historyMessages.length;
- // 2. 添加到消息列表开头
- this.messages = [...historyMessages, ...this.messages];
- // 3. 更新历史偏移量(累加,不是覆盖)
- this.historyOffset += newMsgCount;
-
- // 4. 等待DOM渲染后,精准计算滚动位置
- await this.$nextTick();
- // 获取新增消息的实际DOM高度
- const query = uni.createSelectorQuery().in(this);
- const heightPromises = [];
- for (let i = 0; i < newMsgCount; i++) {
- heightPromises.push(new Promise(resolve => {
- query.select(`.message-item:nth-child(${i + 1})`).boundingClientRect(rect => {
- resolve(rect ? rect.height + 32 : 0); // 加上消息间距(32rpx)
- }).exec();
- }));
- }
- // 计算总高度
- const heights = await Promise.all(heightPromises);
- const totalNewHeight = heights.reduce((sum, h) => sum + h, 0);
- // 5. 设置scrollTop,保持滚动条在顶部附近
- this.scrollTop = totalNewHeight;
- } else {
- // 无更多历史
- this.showLoadMore = false;
- }
- } catch (error) {
- console.error('加载历史失败:', error);
- uni.showToast({ title: '加载历史失败', icon: 'none' });
- this.showLoadMore = false;
- } finally {
- this.isLoadingHistory = false;
- this.isFetchingHistory = false;
- this.hasMoreHistory = false;
- this.loadingText = '加载更多历史消息';
- }
- },
+ const realTimestamp = msg.create_time || msg.created_at || msg.time;
+ return {
+ id: msg.id || `history-${Date.now()}-${Math.random().toString(36).slice(2)}`,
+ role: msg.role === 'user' ? 'user' : 'ai',
+ type: 'text',
+ content: msg.content,
+ timestamp: realTimestamp
+ };
+ });
+
+ const newMsgCount = historyMessages.length;
+ this.messages = [...historyMessages, ...this.messages];
+ this.historyOffset += newMsgCount;
+
+ // 根据服务端 total 判断是否还有更多
+ if (response.total !== undefined) {
+ this.hasMoreHistory = this.historyOffset < response.total;
+ }
+
+ await this.$nextTick();
+ const query = uni.createSelectorQuery().in(this);
+ const heightPromises = [];
+ for (let i = 0; i < newMsgCount; i++) {
+ heightPromises.push(new Promise(resolve => {
+ query.select(`.message-item:nth-child(${i + 1})`).boundingClientRect(rect => {
+ resolve(rect ? rect.height + 32 : 0);
+ }).exec();
+ }));
+ }
+ const heights = await Promise.all(heightPromises);
+ const totalNewHeight = heights.reduce((sum, h) => sum + h, 0);
+ this.scrollTop = totalNewHeight;
+ } else {
+ // 无更多历史
+ this.hasMoreHistory = false;
+ }
+ } catch (error) {
+ console.error('加载历史失败:', error);
+ uni.showToast({ title: '加载历史失败', icon: 'none' });
+ this.hasMoreHistory = false;
+ } finally {
+ this.isLoadingHistory = false;
+ this.isFetchingHistory = false;
+ this.loadingText = '加载更多历史消息';
+ }
+ },
// 显示更多工具
showMoreTools() {
this.showToolsPanel = true
@@ -1908,8 +1908,9 @@ async loadChatHistoryIfExist() {
this.currentConversationId = null
aiService.clearConversationId()
this.clearConversationIdFromLocal()
- // 重置历史偏移量
+ // 重置历史偏移量和加载状态
this.historyOffset = 0
+ this.hasMoreHistory = false
// 触发清空事件
this.$emit('messages-cleared')
},
From 80b18cee0f409eba3d5d842eded8669bb790e337 Mon Sep 17 00:00:00 2001
From: ZF sun <34314687@qq.com>
Date: Wed, 13 May 2026 09:19:38 +0800
Subject: [PATCH 08/13] =?UTF-8?q?feat(ai-chat):=20=E4=BC=98=E5=8C=96?=
=?UTF-8?q?=E5=8A=A0=E8=BD=BD=E7=8A=B6=E6=80=81=E5=8A=A8=E7=94=BB=E4=B8=8E?=
=?UTF-8?q?=E6=B5=81=E5=BC=8F=E6=B6=88=E6=81=AF=E8=BF=87=E6=B8=A1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
pages_tool/ai-chat/ai-chat-message.vue | 463 ++++++++++++++++---------
1 file changed, 302 insertions(+), 161 deletions(-)
diff --git a/pages_tool/ai-chat/ai-chat-message.vue b/pages_tool/ai-chat/ai-chat-message.vue
index fa9d3c6..6f7381c 100644
--- a/pages_tool/ai-chat/ai-chat-message.vue
+++ b/pages_tool/ai-chat/ai-chat-message.vue
@@ -17,7 +17,7 @@
-
+
-
-
-
-
+
+
+
+
+
+
+
+ {{ message.loadingText || 'AI正在思考' }}
+
+ .
+ .
+ .
+
+
- AI正在思考中...
@@ -558,9 +567,15 @@ export default {
}
// 会话未变化且已有消息 → 跳过加载,保留当前消息
},
- // 新增:组件销毁时清除防抖定时器
+ // 新增:组件销毁时清除所有定时器
beforeDestroy() {
if (this.scrollTimer) clearTimeout(this.scrollTimer);
+ // 清理所有加载状态定时器
+ this.messages.forEach(msg => {
+ if (msg._rotationTimer) {
+ clearInterval(msg._rotationTimer);
+ }
+ });
},
created() {
this.messages = [...this.initialMessages]
@@ -900,17 +915,25 @@ async loadChatHistoryIfExist() {
this.messages.push(userMessage)
this.inputText = ''
- // 显示AI正在输入
+ // 显示AI正在处理 - 带状态轮换提示
const loadingMessage = {
id: ++this.messageId,
role: 'ai',
type: 'loading',
content: '',
- timestamp: Date.now()
+ timestamp: Date.now(),
+ loadingText: 'AI正在思考',
+ _loadingIndex: 0
}
this.shouldScrollToBottom = true
this.messages.push(loadingMessage)
+ // 启动处理状态文字轮换
+ this._startLoadingTextRotation(loadingMessage)
+
+ // 等待 Vue 先渲染 loading 动画,再发起请求
+ await this.$nextTick()
+
try {
// 直接发送消息(跳过健康检查,避免健康检查接口格式不匹配导致误判)
// 如果服务不可用,实际发送时会抛出异常,由 catch 块处理
@@ -925,7 +948,7 @@ async loadChatHistoryIfExist() {
console.error('AI服务调用失败:', error)
// 移除加载状态
- this.messages = this.messages.filter(msg => msg.type !== 'loading')
+ this._removeLoadingMessages()
// 显示错误消息
const errorMessage = {
@@ -955,7 +978,7 @@ async loadChatHistoryIfExist() {
})
// 移除加载状态
- this.messages = this.messages.filter(msg => msg.type !== 'loading')
+ this._removeLoadingMessages()
// 更新会话ID
if (response.conversationId) {
@@ -982,134 +1005,193 @@ async loadChatHistoryIfExist() {
this.$emit('ai-response', aiMessage)
},
// 发送流式消息(自动适配 H5 / 微信小程序)
- async sendStreamMessage(userMessage) {
- // 创建流式消息对象
- const streamMessage = {
- id: ++this.messageId,
- role: 'ai',
- type: 'text',
- content: '',
- timestamp: Date.now(),
- isStreaming: true
- };
- // 标记正在流式响应
- this.isStreaming = true;
- // 移除加载状态,添加流式消息
- this.messages = this.messages.filter(msg => msg.type !== 'loading');
- this.shouldScrollToBottom = true;
- this.messages.push(streamMessage);
-
- // 添加消息后立即滚动一次
- this.$nextTick(() => {
- this.scrollToBottom();
- });
-
- try {
- // #ifdef H5
- await aiService.sendHttpStream(
- userMessage,
- (chunk) => {
- // 实时更新内容
- streamMessage.content += chunk;
- // 流式更新时滚动到底部显示最新内容(防抖)
- this.scrollToBottom();
+async sendStreamMessage(userMessage) {
+ // 找到已有的 loading 消息,复用它在 DOM 中的位置,实现无缝过渡
+ const loadingIdx = this.messages.findIndex(msg => msg.type === 'loading')
+ if (loadingIdx === -1) {
+ // 安全回退:没有 loading 消息时创建新消息
+ const fallback = {
+ id: ++this.messageId,
+ role: 'ai',
+ type: 'text',
+ content: '',
+ timestamp: Date.now(),
+ isStreaming: true
+ }
+ this.isStreaming = true
+ this.messages.push(fallback)
+ this.scrollToBottom()
+
+ try {
+ // #ifdef H5
+ await aiService.sendHttpStream(userMessage,
+ (chunk) => { fallback.content += chunk; this.scrollToBottom() },
+ (result) => this._finishStream(fallback, result))
+ // #endif
+ // #ifdef MP-WEIXIN
+ await aiService.sendStreamMessage(userMessage,
+ (chunk) => { fallback.content += chunk; this.scrollToBottom() },
+ (result) => this._finishStream(fallback, result))
+ // #endif
+ } catch (error) {
+ this._handleStreamError(fallback)
+ }
+ return
+ }
+
+ this.isStreaming = true
+ const loadingMsg = this.messages[loadingIdx]
+ this._stopLoadingTextRotation(loadingMsg)
+ loadingMsg.loadingText = '正在接收回复'
+
+ let transformed = false
+
+ const transformOnFirstChunk = () => {
+ if (transformed) return this.messages[loadingIdx]
+ transformed = true
+ const msg = this.messages[loadingIdx]
+ msg.type = 'text'
+ msg.content = ''
+ this.$set(msg, 'isStreaming', true)
+ this.$set(msg, 'actions', [])
+ delete msg.loadingText
+ delete msg._loadingIndex
+ return msg
+ }
+
+ try {
+ // #ifdef H5
+ await aiService.sendHttpStream(
+ userMessage,
+ (chunk) => {
+ const msg = transformOnFirstChunk()
+ msg.content += chunk
+ this.scrollToBottom()
},
- (completeResult) => {
- // 流结束回调
- let convId = '';
-
- if (typeof completeResult !== 'string') {
- convId = completeResult.conversation_id || '';
- }
-
- // 标记流式结束
- streamMessage.isStreaming = false;
- this.isStreaming = false;
-
- // 检查内容是否为空,如果为空则移除该消息
- if (!streamMessage.content || streamMessage.content.trim() === '') {
- this.messages = this.messages.filter(msg => msg.id !== streamMessage.id);
- return;
- }
-
- // 保存会话 ID
- if (convId) {
- this.currentConversationId = convId;
- aiService.setConversationId(convId);
- this.saveConversationIdToLocal(convId);
- }
-
- // 流结束后再滚动到底部
- this.$nextTick(() => {
- this.scrollToBottom();
- });
-
- // 触发事件
- this.$emit('ai-response', streamMessage);
- }
- );
- // #endif
-
- // #ifdef MP-WEIXIN
- // ===== 微信小程序: 使用 WebSocket =====
- await aiService.sendStreamMessage(
- userMessage,
- (chunk) => {
- // 实时更新内容
- streamMessage.content += chunk;
- // 流式更新时滚动到底部显示最新内容(防抖)
- this.scrollToBottom();
- },
- (completeResult) => {
- // 流结束回调
- let convId = completeResult?.conversation_id || '';
-
- // 标记流式结束
- streamMessage.isStreaming = false;
- this.isStreaming = false;
-
- // 检查内容是否为空,如果为空则移除该消息
- if (!streamMessage.content || streamMessage.content.trim() === '') {
- this.messages = this.messages.filter(msg => msg.id !== streamMessage.id);
- return;
- }
-
- // 保存会话 ID
- if (convId) {
- this.currentConversationId = convId;
- aiService.setConversationId(convId);
- this.saveConversationIdToLocal(convId);
- }
-
- // 流结束后再滚动到底部
- this.$nextTick(() => {
- this.scrollToBottom();
- });
-
- // 触发事件
- this.$emit('ai-response', streamMessage);
+ (completeResult) => {
+ const msg = this.messages[loadingIdx]
+ if (!this._finishStream(msg, completeResult)) return
}
- );
- // #endif
- } catch (error) {
- console.error('流式请求失败:', error);
- // 标记流式结束
- this.isStreaming = false;
- // 移除流式消息
- this.messages = this.messages.filter(msg => msg.id !== streamMessage.id);
- // 显示错误
- const errorMsg = {
- id: ++this.messageId,
- role: 'ai',
- type: 'text',
- content: '抱歉,服务暂时不可用,请稍后重试。',
- timestamp: Date.now(),
- actions: [{ id: 1, text: '重试', type: 'retry' }]
- };
- this.messages.push(errorMsg);
- this.$emit('ai-response', errorMsg);
- }
- },
+ )
+ // #endif
+
+ // #ifdef MP-WEIXIN
+ await aiService.sendStreamMessage(
+ userMessage,
+ (chunk) => {
+ const msg = transformOnFirstChunk()
+ msg.content += chunk
+ this.scrollToBottom()
+ },
+ (completeResult) => {
+ const msg = this.messages[loadingIdx]
+ if (!this._finishStream(msg, completeResult)) return
+ }
+ )
+ // #endif
+ } catch (error) {
+ console.error('流式请求失败:', error)
+ this.isStreaming = false
+
+ if (!transformed) {
+ // 在第一个 chunk 到达前出错 → 移除 loading 消息
+ this._removeLoadingMessages()
+ } else {
+ // 流传输中出错 → 移除空的流式消息
+ const msg = this.messages[loadingIdx]
+ if (msg && (!msg.content || msg.content.trim() === '')) {
+ this.messages.splice(loadingIdx, 1)
+ }
+ }
+
+ const errorMsg = {
+ id: ++this.messageId,
+ role: 'ai',
+ type: 'text',
+ content: '抱歉,服务暂时不可用,请稍后重试。',
+ timestamp: Date.now(),
+ actions: [{ id: 1, text: '重试', type: 'retry' }]
+ }
+ this.messages.push(errorMsg)
+ this.$emit('ai-response', errorMsg)
+ }
+},
+
+// 完成流式消息处理(共用逻辑)
+_finishStream(msg, result) {
+ msg.isStreaming = false
+ this.isStreaming = false
+
+ if (!msg.content || msg.content.trim() === '') {
+ this._removeLoadingMessages()
+ return false
+ }
+
+ if (result) {
+ const convId = typeof result === 'string' ? '' : (result.conversation_id || '')
+ if (convId) {
+ this.currentConversationId = convId
+ aiService.setConversationId(convId)
+ this.saveConversationIdToLocal(convId)
+ }
+ }
+
+ msg.actions = [
+ { id: 1, text: '有帮助', type: 'like' },
+ { id: 2, text: '没帮助', type: 'dislike' }
+ ]
+
+ this.$nextTick(() => this.scrollToBottom())
+ this.$emit('ai-response', msg)
+ return true
+},
+
+// 流式请求出错处理
+_handleStreamError(msg) {
+ console.error('流式请求失败')
+ this.isStreaming = false
+ this.messages = this.messages.filter(m => m.id !== msg.id)
+
+ const errorMsg = {
+ id: ++this.messageId,
+ role: 'ai',
+ type: 'text',
+ content: '抱歉,服务暂时不可用,请稍后重试。',
+ timestamp: Date.now(),
+ actions: [{ id: 1, text: '重试', type: 'retry' }]
+ }
+ this.messages.push(errorMsg)
+ this.$emit('ai-response', errorMsg)
+},
+ // 加载状态文字轮换 - 让AI看起来在处理不同阶段
+ _startLoadingTextRotation(loadingMsg) {
+ const statusTexts = [
+ 'AI正在思考',
+ 'AI正在分析',
+ 'AI正在检索',
+ 'AI正在生成'
+ ]
+ loadingMsg._rotationTimer = setInterval(() => {
+ loadingMsg._loadingIndex = (loadingMsg._loadingIndex + 1) % statusTexts.length
+ loadingMsg.loadingText = statusTexts[loadingMsg._loadingIndex]
+ }, 2000)
+ },
+
+ // 停止加载状态文字轮换
+ _stopLoadingTextRotation(loadingMsg) {
+ if (loadingMsg && loadingMsg._rotationTimer) {
+ clearInterval(loadingMsg._rotationTimer)
+ loadingMsg._rotationTimer = null
+ }
+ },
+
+ // 移除加载消息并清理定时器
+ _removeLoadingMessages() {
+ const loadingMsgs = this.messages.filter(msg => msg.type === 'loading');
+ loadingMsgs.forEach(msg => this._stopLoadingTextRotation(msg));
+ this.messages = this.messages.filter(msg => msg.type !== 'loading');
+ },
+
generateAIResponse(userMessage) {
const responses = {
'你好': '您好!我是AI智能客服,很高兴为您服务!有什么可以帮助您的吗?',
@@ -1713,11 +1795,17 @@ async loadChatHistoryIfExist() {
role: 'ai',
type: 'loading',
content: '',
- timestamp: Date.now()
+ timestamp: Date.now(),
+ loadingText: 'AI正在思考',
+ _loadingIndex: 0
}
this.messages.push(loadingMessage)
-
+ this._startLoadingTextRotation(loadingMessage)
+
+ // 等待 Vue 渲染 loading 动画
+ await this.$nextTick()
+
try {
// 使用AI服务获取回复
if (this.enableStreaming) {
@@ -1732,7 +1820,7 @@ async loadChatHistoryIfExist() {
console.error('AI服务调用失败:', error)
// 移除加载状态
- this.messages = this.messages.filter(msg => msg.type !== 'loading')
+ this._removeLoadingMessages()
// 显示错误消息
const errorMessage = {
@@ -1900,6 +1988,13 @@ async loadChatHistoryIfExist() {
// 清空聊天记录
clearMessages() {
+ // 清理所有加载状态定时器
+ this.messages.forEach(msg => {
+ if (msg._rotationTimer) {
+ clearInterval(msg._rotationTimer)
+ msg._rotationTimer = null
+ }
+ })
// 清空消息数组
this.messages = []
// 重置消息 ID
@@ -2562,31 +2657,68 @@ $radius-lg: 36rpx;
align-items: center;
padding: 32rpx 0;
- .loading-dots {
+ .loading-typing {
display: flex;
- margin-right: 16rpx;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 12rpx;
- .dot {
- width: 14rpx;
- height: 14rpx;
- border-radius: 50%;
- background-color: $color-primary;
- margin: 0 6rpx;
- animation: dotPulse 1.5s infinite ease-in-out;
+ .typing-dots {
+ display: flex;
+ align-items: center;
- &:nth-child(2) {
- animation-delay: 0.2s;
- }
-
- &:nth-child(3) {
- animation-delay: 0.4s;
+ .dot {
+ width: 14rpx;
+ height: 14rpx;
+ border-radius: 50%;
+ background-color: $color-primary;
+ margin: 0 6rpx;
+ animation: dotPulse 1.5s infinite ease-in-out;
+
+ &:nth-child(2) {
+ animation-delay: 0.2s;
+ }
+
+ &:nth-child(3) {
+ animation-delay: 0.4s;
+ }
+ }
+ }
+
+ .loading-text {
+ color: $color-text-light;
+ font-size: 24rpx;
+ display: flex;
+ align-items: center;
+
+ .loading-status {
+ color: $color-primary;
+ font-weight: 500;
+ transition: opacity 0.3s ease;
+ }
+
+ .loading-ellipsis {
+ display: inline-flex;
+ margin-left: 4rpx;
+
+ .ellipsis-dot {
+ animation: ellipsisBlink 1.4s infinite ease-in-out;
+ opacity: 0;
+
+ &:nth-child(1) {
+ animation-delay: 0s;
+ }
+
+ &:nth-child(2) {
+ animation-delay: 0.2s;
+ }
+
+ &:nth-child(3) {
+ animation-delay: 0.4s;
+ }
+ }
}
}
- }
-
- .loading-text {
- color: $color-text-light;
- font-size: 26rpx;
}
}
@@ -3478,6 +3610,15 @@ $radius-lg: 36rpx;
}
}
+@keyframes ellipsisBlink {
+ 0%, 100% {
+ opacity: 0;
+ }
+ 50% {
+ opacity: 1;
+ }
+}
+
@keyframes wave {
0%, 100% {
transform: scaleY(0.5);
From 39245dfcacee927eaf7873247eab0e2caaefc84a Mon Sep 17 00:00:00 2001
From: ZF sun <34314687@qq.com>
Date: Wed, 13 May 2026 14:08:58 +0800
Subject: [PATCH 09/13] =?UTF-8?q?fix(ai-chat):=20=E4=BF=AE=E5=A4=8D?=
=?UTF-8?q?=E8=BE=93=E5=85=A5=E6=A1=86=E5=AE=9A=E4=BD=8D=E5=8F=8A=E6=BB=9A?=
=?UTF-8?q?=E5=8A=A8=E9=97=AE=E9=A2=98=E5=B9=B6=E6=9B=B4=E6=96=B0=E5=AE=A2?=
=?UTF-8?q?=E6=9C=8D=E5=90=8D=E7=A7=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
common/js/customer-service.js | 10 ++--
pages.json | 2 +-
pages_tool/ai-chat/ai-chat-message.vue | 83 ++++++++------------------
3 files changed, 30 insertions(+), 65 deletions(-)
diff --git a/common/js/customer-service.js b/common/js/customer-service.js
index 288770f..13a2ded 100644
--- a/common/js/customer-service.js
+++ b/common/js/customer-service.js
@@ -119,7 +119,7 @@ export class CustomerService {
}
/**
- * 跳转到 AI 客服页面(Dify)
+ * 跳转到 AI智能客服页面(Dify)
*/
openDifyService() {
try {
@@ -135,7 +135,7 @@ export class CustomerService {
uni.navigateTo({
url: aiChatUrl,
fail: (err) => {
- console.error('跳转 AI 客服失败:', err);
+ console.error('跳转 AI智能客服失败:', err);
// H5 兜底
// #ifdef H5
window.location.href = aiChatUrl;
@@ -144,7 +144,7 @@ export class CustomerService {
}
});
} catch (e) {
- console.error('跳转 AI 客服异常:', e);
+ console.error('跳转 AI智能客服异常:', e);
uni.showToast({ title: '打开客服失败', icon: 'none' });
}
}
@@ -565,7 +565,7 @@ export class CustomerService {
uni.navigateTo({
url: aiChatUrl,
fail: (err) => {
- console.error('跳转 AI 客服失败:', err);
+ console.error('跳转 AI智能客服失败:', err);
// #ifdef H5
window.location.href = aiChatUrl;
// #endif
@@ -573,7 +573,7 @@ export class CustomerService {
}
});
} catch (e) {
- console.error('跳转 AI 客服异常:', e);
+ console.error('跳转 AI智能客服异常:', e);
uni.showToast({ title: '打开客服失败', icon: 'none' });
}
}
diff --git a/pages.json b/pages.json
index 0c468d2..a2ea398 100644
--- a/pages.json
+++ b/pages.json
@@ -318,7 +318,7 @@
{
"path": "ai-chat/index",
"style": {
- "navigationBarTitleText": "AI 客服"
+ "navigationBarTitleText": "AI智能客服"
}
},
{
diff --git a/pages_tool/ai-chat/ai-chat-message.vue b/pages_tool/ai-chat/ai-chat-message.vue
index 6f7381c..16e5ec8 100644
--- a/pages_tool/ai-chat/ai-chat-message.vue
+++ b/pages_tool/ai-chat/ai-chat-message.vue
@@ -358,7 +358,7 @@
-