18 Commits

Author SHA1 Message Date
54c39259a7 chore:代码合并,整理功能 2026-02-02 18:04:59 +08:00
15361211b1 Merge pull request 'tjh/merge' (#3) from tjh/merge into dev/1.0
Reviewed-on: #3
2026-02-02 09:52:19 +00:00
e301ddc6ec chore:微信小程序标签栏点击可以跳转 2026-01-31 17:04:50 +08:00
a560ad4a5d chore:后台点击什么页面就显示什么 2026-01-31 10:24:12 +08:00
7f7a18803f chore:智能客服正常运行 2026-01-31 10:06:59 +08:00
42563d7184 chore:改了pages-tool/ai-chat/ai-chat-message.vue 2026-01-31 09:47:41 +08:00
14a903f42a chore:改了components/chat-message/chat-message.vue 2026-01-31 09:44:05 +08:00
c9f8614927 chore:改了common/js/lang.js 2026-01-31 09:23:36 +08:00
fe9303bf4c chore:改了store/index.js 2026-01-31 09:20:31 +08:00
e4dfd0ae11 chore:改了project.config.json 2026-01-31 09:17:57 +08:00
7037d0b083 chore:改了pages-tool/ai-chat/index.vue 2026-01-31 09:14:00 +08:00
e8a79bd245 chore:改了golbalconfig.js 2026-01-31 09:11:00 +08:00
047cf765da chore:改了pages.json 2026-01-31 09:07:48 +08:00
3a30b0817d chore:替换了pages/index/index.vue 2026-01-31 09:03:58 +08:00
a464a8731f chore:替换了customer-service.js 2026-01-31 08:59:30 +08:00
7599a80943 chore:替换了ai-service.js 2026-01-31 08:55:57 +08:00
992a01cc76 chore:替了chat-message.vue 2026-01-31 08:52:55 +08:00
dd09c66215 chore:替了hover 2026-01-31 08:48:18 +08:00
12 changed files with 1799 additions and 1639 deletions

View File

@@ -42,6 +42,6 @@ const localDevConfig = ({
uniacid: 2,
domain: 'http://localhost:8050/',
},
})['2811']; // 选择要使用的环境配置
})['1']; // 选择要使用的环境配置
export default localDevConfig;

View File

@@ -60,55 +60,107 @@ export default {
/**
* 流式消息入口(自动适配平台)
*/
async sendStreamMessage(message, onChunk, onComplete) {
// #ifdef MP-WEIXIN
// 微信小程序:降级为普通请求 + 前端打字模拟
try {
const result = await this.sendMessage(message);
const content = result.content || '';
const conversationId = result.conversationId || '';
async sendStreamMessage(message, onChunk, onComplete) {
// #ifdef MP-WEIXIN
return new Promise((resolve, reject) => {
const socketTask = wx.connectSocket({
url: 'wss://dev.aigc-quickapp.com/ws/aikefu',
header: {}
});
// 保存会话ID确保连续对话
if (conversationId) {
this.setConversationId(conversationId);
}
let content = '';
let conversationId = '';
let isAuthenticated = false;
// 模拟打字效果
let index = 0;
const chunkSize = 2; // 每次显示2个字符
return new Promise((resolve) => {
const timer = setInterval(() => {
if (index < content.length) {
const chunk = content.substring(index, index + chunkSize);
index += chunkSize;
if (onChunk) onChunk(chunk);
} else {
clearInterval(timer);
if (onComplete) {
onComplete({
content: content,
conversation_id: conversationId
});
}
resolve({ content, conversation_id: conversationId });
}
}, 80); // 打字速度80ms/次
});
} catch (error) {
console.error('小程序流式消息降级失败:', error);
if (onComplete) {
onComplete({ error: error.message || '发送失败' });
}
throw error;
}
socketTask.onOpen(() => {
console.log('WebSocket 连接成功,开始认证...');
socketTask.send({
data: JSON.stringify({
action: 'auth',
uniacid: store.state.uniacid || '1',
token: store.state.token || 'test_token',
user_id: store.state.memberInfo?.id || 'anonymous'
})
});
});
socketTask.onMessage((res) => {
try {
const data = JSON.parse(res.data);
console.log('收到 WebSocket 消息:', data);
if (data.type === 'auth_success') {
console.log('认证成功,发送聊天消息...');
isAuthenticated = true;
socketTask.send({
data: JSON.stringify({
action: 'chat',
uniacid: store.state.uniacid || '1',
query: message,
user_id: store.state.memberInfo?.id || 'anonymous',
conversation_id: this.getConversationId() || ''
})
});
} else if (data.type === 'auth_failed') {
const errorMsg = '认证失败,请重新登录';
console.error(errorMsg, data);
reject(new Error(errorMsg));
if (onComplete) onComplete({ error: errorMsg });
socketTask.close();
}
// 处理流式消息块
else if (data.type === 'message' || data.event === 'message') {
const text = data.answer || data.content || data.text || '';
content += text;
if (onChunk) onChunk(text);
}
// 处理流结束
else if (data.event === 'message_end' || data.type === 'message_end') {
conversationId = data.conversation_id || '';
if (conversationId) {
this.setConversationId(conversationId);
}
if (onComplete) {
onComplete({ content, conversation_id: conversationId });
}
resolve({ content, conversation_id: conversationId });
socketTask.close();
}
// 可选:处理 done
else if (data.type === 'done') {
console.log('对话完成:', data);
}
} catch (e) {
console.error('WebSocket 消息解析失败:', e, '原始数据:', res.data);
}
});
socketTask.onError((err) => {
const errorMsg = 'WebSocket 连接失败';
console.error(errorMsg, err);
reject(new Error(errorMsg));
if (onComplete) onComplete({ error: errorMsg });
});
socketTask.onClose(() => {
console.log('WebSocket 连接已关闭');
});
const timeout = setTimeout(() => {
if (!isAuthenticated || content === '') {
console.warn('WebSocket 超时,强制关闭');
socketTask.close();
reject(new Error('AI服务响应超时'));
if (onComplete) onComplete({ error: 'AI服务响应超时' });
}
}, 10000);
});
// #endif
// #ifdef H5
// H5使用真实流式EventSource / Fetch
return this.sendHttpStream(message, onChunk, onComplete);
// #endif
},
},
/**
* HTTP 流式请求(仅 H5 使用)
*/
@@ -132,11 +184,8 @@ export default {
})
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
if (!response.body) {
throw new Error('响应体不可用');
if (!response.ok || !response.body) {
throw new Error('无效响应');
}
const reader = response.body.getReader();
@@ -145,50 +194,64 @@ export default {
let content = '';
let conversationId = '';
function processBuffer(buf, callback) {
const lines = buf.split('\n');
buf = lines.pop() || '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// 按行分割,保留不完整的最后一行
const lines = buffer.split('\n');
buffer = lines.pop() || ''; // 未完成的行留到下次
for (const line of lines) {
const trimmed = line.trim();
if (trimmed.startsWith('data:')) {
const jsonStr = trimmed.slice(5).trim();
if (jsonStr) {
try {
try {
const jsonStr = trimmed.slice(5).trim();
if (jsonStr && jsonStr !== '[DONE]') {
const data = JSON.parse(jsonStr);
if (data.event === 'message') {
const text = data.answer || data.text || '';
content += text;
callback(text);
if (onChunk) onChunk(text);
}
if (data.conversation_id) {
conversationId = data.conversation_id;
}
if (data.event === 'message_end') {
// 可选:提前完成
// 可提前结束
}
} catch (e) {
console.warn('解析流数据失败:', e);
}
} catch (e) {
console.warn('解析失败:', e, line);
}
}
}
return buf;
}
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
buffer = processBuffer(buffer, (chunk) => {
if (onChunk) onChunk(chunk);
});
// 处理最后残留的 buffer如果有
if (buffer.trim().startsWith('data:')) {
try {
const jsonStr = buffer.trim().slice(5);
if (jsonStr) {
const data = JSON.parse(jsonStr);
if (data.event === 'message') {
const text = data.answer || '';
content += text;
if (onChunk) onChunk(text);
}
if (data.conversation_id) {
conversationId = data.conversation_id;
}
}
} catch (e) {
console.warn('最后 buffer 解析失败:', e);
}
}
if (onComplete) {
onComplete({
content,
conversation_id: conversationId
});
onComplete({ content, conversation_id: conversationId });
}
return { content, conversation_id: conversationId };
} catch (error) {
@@ -196,11 +259,6 @@ export default {
throw error;
}
// #endif
// #ifdef MP-WEIXIN
// 理论上不会执行到这里,但防止 fallback
return this.sendStreamMessage(message, onChunk, onComplete);
// #endif
},
/**

View File

@@ -2,68 +2,13 @@
* 客服统一处理服务
* 整合各种客服方式,提供统一的调用接口
*/
class CustomerService {
constructor(vueInstance, externalConfig = {}) {
if (!vueInstance.$lang) {
throw new Error('CustomerService 必须在 Vue 实例中初始化');
}
export class CustomerService {
constructor(vueInstance, externalConfig = null) {
this.vm = vueInstance;
this.externalConfig = externalConfig; // 外部传入的最新配置(优先级最高)
this.latestPlatformConfig = null;
}
getSupoortKeFuList() {
if (!this.vm) return [];
const vm = this.vm;
return [
{
id: 'weixin-official',
name: vm.$lang('customer.weChatKefu'),
isOfficial: true,
type: 'weapp'
},
{
id: 'custom-kefu',
name: vm.$lang('customer.systemKefu'),
isOfficial: false
},
{
id: 'qyweixin-kefu',
name: vm.$lang('customer.weChatWorkKefu'),
isOfficial: false
},
]
}
/**
* 打开客服选择弹窗
*/
openCustomerSelectPopupDialog() {
const kefu_list = this.getSupoortKeFuList();
const kefuNames = kefu_list.map(item => item.name);
uni.showActionSheet({
itemList: kefuNames,
success: (res) => {
const kefu = kefu_list[res.tapIndex];
this.externalConfig = kefu ?? this.externalConfig ?? {};
if (kefu.isOfficial) {
uni.openCustomerServiceConversation({
sessionFrom: 'weapp',
showMessageCard: true
});
} else if (kefu.id === 'custom-kefu') {
this.handleCustomerClick();
} else if (kefu.id === 'qyweixin-kefu') {
this.handleQyWeixinKefuClick();
}
}
});
}
/**
* 强制刷新配置(支持传入外部配置)
* @param {Object} externalConfig 外部最新配置
@@ -83,28 +28,29 @@ class CustomerService {
return this.latestPlatformConfig;
}
// 优先级:外部传入的最新配置 > vuex配置 > 空对象
// 优先级:外部传入 > vuex store > 空对象
const servicerConfig = this.externalConfig || this.vm.$store.state.servicerConfig || {};
console.log(`【实时客服配置】`, servicerConfig);
let platformConfig = null;
// #ifdef H5
platformConfig = servicerConfig.h5 ? (typeof servicerConfig.h5 === 'object' ? servicerConfig.h5 : null) : null;
platformConfig = servicerConfig.h5 && typeof servicerConfig.h5 === 'object' ? servicerConfig.h5 : null;
// #endif
// #ifdef MP-WEIXIN
platformConfig = servicerConfig.weapp ? (typeof servicerConfig.weapp === 'object' ? servicerConfig.weapp : null) : null;
platformConfig = servicerConfig.weapp && typeof servicerConfig.weapp === 'object' ? servicerConfig.weapp : null;
// #endif
// #ifdef MP-ALIPAY
platformConfig = servicerConfig.aliapp ? (typeof servicerConfig.aliapp === 'object' ? servicerConfig.aliapp : null) : null;
platformConfig = servicerConfig.aliapp && typeof servicerConfig.aliapp === 'object' ? servicerConfig.aliapp : null;
// #endif
// #ifdef PC
platformConfig = servicerConfig.pc ? (typeof servicerConfig.pc === 'object' ? servicerConfig.pc : null) : null;
platformConfig = servicerConfig.pc && typeof servicerConfig.pc === 'object' ? servicerConfig.pc : null;
// #endif
// 处理空数组情况你的配置中pc/aliapp是空数组转为null
// 防止空数组被当作有效配置
if (Array.isArray(platformConfig)) {
platformConfig = null;
}
@@ -144,32 +90,18 @@ class CustomerService {
warnings: []
};
if (!config) {
result.isValid = false;
result.errors.push('客服配置不存在');
return result;
}
if (config.type === 'aikefu') {
return result;
}
if (!config.type) {
if (!config || !config.type) {
result.isValid = false;
result.errors.push('客服类型未配置');
return result;
}
if (config.type === 'wxwork') {
if (!wxworkConfig) {
result.isValid = false;
result.errors.push('企业微信配置不存在');
} else {
if (!wxworkConfig.enable) {
result.warnings.push('企业微信功能未启用');
}
if (!wxworkConfig.contact_url) {
result.warnings.push('企业微信活码链接未配置,将使用原有客服方式');
}
if (!wxworkConfig || !wxworkConfig.enable) {
result.warnings.push('企业微信未启用');
}
if (!wxworkConfig.contact_url) {
result.warnings.push('企业微信活码链接未配置');
}
}
@@ -177,16 +109,39 @@ class CustomerService {
}
/**
* 跳转到AI客服页面
* 跳转到 AI 客服页面Dify
*/
openAIKeFuService() {
const vm = this.vm;
vm.$util.redirectTo(vm.$util.AI_CHAT_PAGE_URL);
openDifyService() {
try {
// 清除未读数(如果存在)
if (typeof this.vm.setAiUnreadCount === 'function') {
this.vm.setAiUnreadCount(0);
}
// ✅ 修正路径:必须与 pages.json 中注册的路径一致
const aiChatUrl = '/pages_tool/ai-chat/index';
// ✅ 使用 navigateTo 保留返回栈(体验更好)
uni.navigateTo({
url: aiChatUrl,
fail: (err) => {
console.error('跳转 AI 客服失败:', err);
// H5 兜底
// #ifdef H5
window.location.href = aiChatUrl;
// #endif
uni.showToast({ title: '打开客服失败', icon: 'none' });
}
});
} catch (e) {
console.error('跳转 AI 客服异常:', e);
uni.showToast({ title: '打开客服失败', icon: 'none' });
}
}
/**
* 处理客服点击事件
* @param {Object} options 选项参数
* 处理客服点击事件(统一入口)
* @param {Object} options 选项参数(用于消息卡片等)
*/
handleCustomerClick(options = {}) {
const validation = this.validateConfig();
@@ -201,51 +156,61 @@ class CustomerService {
}
const config = this.getPlatformConfig();
const { niushop = {}, sendMessageTitle = '', sendMessagePath = '', sendMessageImg = '' } = options || {};
console.log('【当前客服配置】', config);
console.log('【客服类型】', config.type);
const { niushop = {}, sendMessageTitle = '', sendMessagePath = '', sendMessageImg = '' } = options;
if (config.type === 'none') {
this.showNoServicePopup();
return;
}
// 核心分支:根据最新的type处理
// 核心路由:根据 type 决定行为
switch (config.type) {
case 'aikefu':
this.openAIKeFuService();
console.log('【跳转 AI 客服】目标路径: /pages_tool/ai-chat/index');
this.openDifyService();
break;
case 'wxwork':
console.log('【跳转企业微信客服】');
this.openWxworkService(false, config, options);
break;
case 'third':
console.log('【跳转第三方客服】');
this.openThirdService(config);
break;
case 'miniprogram':
console.log('【跳转第三方小程序客服】');
this.openThirdService(config);
break;
case 'niushop':
console.log('【跳转牛商客服】');
this.openNiushopService(niushop);
break;
case 'weapp':
console.log('【跳转微信官方客服】');
this.openWeappService(config, options);
break;
case 'aliapp':
console.log('【跳转支付宝客服】');
this.openAliappService(config);
break;
default:
console.error('【未知客服类型】', config.type);
this.makePhoneCall();
}
}
/**
* 打开企业微信客服
* @param {boolean} useOriginalService 是否使用原有客服方式
* @param {Object} servicerConfig 客服配置
* @param {Object} options 选项参数
*/
// ================== 各类型客服实现 ==================
openWxworkService(useOriginalService = false, servicerConfig = null, options = {}) {
const config = servicerConfig || this.getPlatformConfig();
const wxworkConfig = this.getWxworkConfig();
const { sendMessageTitle = '', sendMessagePath = '', sendMessageImg = '' } = options || {};
const { sendMessageTitle = '', sendMessagePath = '', sendMessageImg = '' } = options;
// #ifdef MP-WEIXIN
if (wxworkConfig?.enable && wxworkConfig?.contact_url && !useOriginalService) {
if (!useOriginalService && wxworkConfig?.enable && wxworkConfig?.contact_url) {
wx.navigateToMiniProgram({
appId: 'wxeb490c6f9b154ef9',
path: `pages/contacts/externalContactDetail?url=${encodeURIComponent(wxworkConfig.contact_url)}`,
@@ -256,9 +221,16 @@ class CustomerService {
}
});
} else {
// 检查是否有企业微信配置
if (!config.wxwork_url && !config.corpid) {
console.error('企业微信配置不完整,缺少 wxwork_url 或 corpid');
uni.showToast({ title: '企业微信配置不完整', icon: 'none' });
this.fallbackToPhoneCall();
return;
}
wx.openCustomerServiceChat({
extInfo: { url: config.wxwork_url },
corpId: config.corpid,
extInfo: { url: config.wxwork_url || '' },
corpId: config.corpid || '',
showMessageCard: true,
sendMessageTitle,
sendMessagePath,
@@ -268,120 +240,125 @@ class CustomerService {
// #endif
// #ifdef H5
if (wxworkConfig?.enable && wxworkConfig?.contact_url) {
if (!useOriginalService && wxworkConfig?.enable && wxworkConfig?.contact_url) {
window.location.href = wxworkConfig.contact_url;
} else if (config.wxwork_url) {
location.href = config.wxwork_url;
window.location.href = config.wxwork_url;
} else {
this.fallbackToPhoneCall();
}
// #endif
}
/**
* 打开第三方客服
* @param {Object} config 客服配置
*/
openThirdService(config) {
console.log('【第三方客服配置】', config);
console.log('【配置字段】', Object.keys(config));
// 支持多种可能的字段名
const miniAppId = config.mini_app_id || config.miniAppId || config.appid || config.appId || config.app_id;
const miniAppPath = config.mini_app_path || config.miniAppPath || config.path || config.page_path || '';
console.log('【解析后的小程序配置】AppID:', miniAppId, 'Path:', miniAppPath);
// 优先处理第三方微信小程序客服
if (miniAppId) {
console.log('【跳转第三方小程序】AppID:', miniAppId, 'Path:', miniAppPath);
// #ifdef MP-WEIXIN
wx.navigateToMiniProgram({
appId: miniAppId,
path: miniAppPath,
success: () => {
console.log('【跳转第三方小程序成功】');
},
fail: (err) => {
console.error('【跳转第三方小程序失败】', err);
uni.showToast({ title: '跳转失败,请稍后重试', icon: 'none' });
}
});
// #endif
// #ifdef H5
uni.showToast({ title: '第三方小程序客服仅在微信小程序中可用', icon: 'none' });
// #endif
return;
}
// 处理第三方链接客服
if (config.third_url) {
console.log('【跳转第三方链接】', config.third_url);
// #ifdef H5
window.location.href = config.third_url;
// #endif
// #ifdef MP-WEIXIN
uni.setClipboardData({
data: config.third_url,
success: () => {
uni.showToast({ title: '链接已复制,请在浏览器打开', icon: 'none' });
}
});
// #endif
} else {
console.error('【第三方客服配置不完整】缺少 mini_app_id 或 third_url');
this.fallbackToPhoneCall();
}
}
/**
* 打开牛商客服
* @param {Object} niushop 牛商参数
*/
openNiushopService(niushop) {
if (Object.keys(niushop).length > 0) {
if (Object.keys(niushop).length > 0 && this.vm.$util?.redirectTo) {
this.vm.$util.redirectTo('/pages_tool/chat/room', niushop);
} else {
this.makePhoneCall();
}
}
/**
* 打开微信小程序客服
* @param {Object} config 客服配置
* @param {Object} options 选项参数
*/
openWeappService(config, options = {}) {
if (!this.shouldUseCustomService(config)) {
console.log('使用官方微信小程序客服');
// 如果 useOfficial 为 true 或 undefined则使用原生系统客服由 button open-type="contact" 触发)
// 此方法仅用于自定义跳转(如 useOfficial: false
if (config.useOfficial !== false) {
// 不做任何事,应由 <button open-type="contact"> 触发
console.log('使用微信官方客服,请确保按钮为 <button open-type="contact">');
return;
}
console.log('使用自定义微信小程序客服');
this.handleCustomWeappService(config, options);
}
/**
* 处理自定义微信小程序客服
* @param {Object} config 客服配置
* @param {Object} options 选项参数
*/
handleCustomWeappService(config, options = {}) {
const { sendMessageTitle = '', sendMessagePath = '', sendMessageImg = '' } = options || {};
if (config.customServiceUrl) {
let url = config.customServiceUrl;
const params = [];
const { sendMessageTitle, sendMessagePath, sendMessageImg } = options;
if (sendMessageTitle) params.push(`title=${encodeURIComponent(sendMessageTitle)}`);
if (sendMessagePath) params.push(`path=${encodeURIComponent(sendMessagePath)}`);
if (sendMessageImg) params.push(`img=${encodeURIComponent(sendMessageImg)}`);
if (params.length > 0) {
url += (url.includes('?') ? '&' : '?') + params.join('&');
}
uni.navigateTo({
url: url,
fail: (err) => {
console.error('跳转自定义客服页面失败:', err);
this.tryThirdPartyService(config, options);
}
});
uni.navigateTo({ url });
return;
}
this.tryThirdPartyService(config, options);
}
/**
* 尝试使用第三方客服
* @param {Object} config 客服配置
* @param {Object} options 选项参数
*/
tryThirdPartyService(config, options = {}) {
if (config.thirdPartyServiceUrl) {
// #ifdef H5
window.open(config.thirdPartyServiceUrl, '_blank');
// #endif
// 支持第三方微信小程序客服
if (config.thirdPartyMiniAppId || config.mini_app_id) {
// #ifdef MP-WEIXIN
if (config.thirdPartyMiniAppId) {
wx.navigateToMiniProgram({
appId: config.thirdPartyMiniAppId,
path: config.thirdPartyMiniAppPath || '',
fail: (err) => {
console.error('跳转第三方小程序失败:', err);
this.fallbackToPhoneCall();
}
});
} else {
uni.setClipboardData({
data: config.thirdPartyServiceUrl,
success: () => {
uni.showModal({
title: '客服链接已复制',
content: '客服链接已复制到剪贴板,请在浏览器中粘贴访问',
showCancel: false
});
}
});
}
wx.navigateToMiniProgram({
appId: config.thirdPartyMiniAppId || config.mini_app_id,
path: config.thirdPartyMiniAppPath || config.mini_app_path || ''
});
// #endif
return;
}
// 支持第三方链接客服
if (config.thirdPartyServiceUrl || config.third_url) {
const serviceUrl = config.thirdPartyServiceUrl || config.third_url;
// #ifdef H5
window.open(serviceUrl, '_blank');
// #endif
// #ifdef MP-WEIXIN
uni.setClipboardData({ data: serviceUrl });
uni.showToast({ title: '客服链接已复制', icon: 'none' });
// #endif
return;
}
@@ -389,106 +366,55 @@ class CustomerService {
this.fallbackToPhoneCall();
}
/**
* 降级到电话客服
*/
fallbackToPhoneCall() {
uni.showModal({
title: '联系客服',
content: '在线客服暂时不可用,是否拨打电话联系客服?',
success: (res) => {
if (res.confirm) {
this.makePhoneCall();
}
}
});
}
/**
* 打开支付宝小程序客服
* @param {Object} config 客服配置
*/
openAliappService(config) {
console.log('支付宝小程序客服', config);
switch (config.type) {
case 'aikefu':
this.openAIKeFuService();
break;
case 'third':
this.openThirdService(config);
break;
default:
console.log('使用支付宝官方客服');
break;
if (config.type === 'aikefu') {
this.openDifyService();
} else if (config.type === 'third') {
this.openThirdService(config);
} else {
// 支付宝原生客服由 button open-type="contact" 触发,此处不处理
console.log('使用支付宝官方客服');
}
}
/**
* 拨打电话
*/
makePhoneCall(mobileNumber) {
if (mobileNumber) {
return uni.makePhoneCall({
phoneNumber: mobileNumber
});
}
// ================== 辅助方法 ==================
// 从缓存中获取电话信息
uni.getStorage({
key: 'shopInfo',
success: (res) => {
const shopInfo = res.data;
const mobile = shopInfo?.mobile ?? '';
if (mobile) {
uni.makePhoneCall({
phoneNumber: mobile
});
makePhoneCall() {
this.vm.$api.sendRequest({
url: '/api/site/shopcontact',
success: res => {
if (res.code === 0 && res.data?.mobile) {
uni.makePhoneCall({ phoneNumber: res.data.mobile });
} else {
uni.showToast({
title: '暂无客服电话',
icon: 'none'
});
uni.showToast({ title: '暂无客服电话', icon: 'none' });
}
},
fail: () => {
uni.showToast({ title: '获取客服电话失败', icon: 'none' });
}
});
}
/**
* 显示无客服弹窗
*/
showNoServicePopup() {
const siteInfo = this.vm.$store.state.siteInfo || {};
const message = siteInfo?.site_tel
? `请联系客服,客服电话是${siteInfo.site_tel}`
? `请联系客服,客服电话是 ${siteInfo.site_tel}`
: '抱歉,商家暂无客服,请线下联系';
uni.showModal({
title: '联系客服',
content: message,
showCancel: false
});
uni.showModal({ title: '联系客服', content: message, showCancel: false });
}
/**
* 显示配置错误弹窗
* @param {Array} errors 错误列表
*/
showConfigErrorPopup(errors) {
const message = errors.join('\n');
uni.showModal({
title: '配置错误',
content: `客服配置有误:\n${message}`,
title: '客服配置错误',
content: `配置有误:\n${errors.join('\n')}`,
showCancel: false
});
}
/**
* 降级处理:使用原有客服方式
*/
fallbackToOriginalService() {
uni.showModal({
title: '提示',
content: '无法直接添加企业微信客服,是否使用其他方式联系客服?',
content: '无法直接添加企业微信,是否使用其他方式联系客服?',
success: (res) => {
if (res.confirm) {
this.openWxworkService(true);
@@ -497,9 +423,19 @@ class CustomerService {
});
}
fallbackToPhoneCall() {
uni.showModal({
title: '提示',
content: '在线客服不可用,是否拨打电话联系客服?',
success: (res) => {
if (res.confirm) this.makePhoneCall();
}
});
}
/**
* 获取客服按钮配置
* @returns {Object} 按钮配置
* 获取按钮配置(用于 template 中 v-if / open-type 判断)
* @returns {Object}
*/
getButtonConfig() {
const config = this.getPlatformConfig();
@@ -507,39 +443,24 @@ class CustomerService {
let openType = '';
// #ifdef MP-WEIXIN
if (config.type === 'weapp') {
openType = config.useOfficial !== false ? 'contact' : '';
if (config.type === 'weapp' && config.useOfficial !== false) {
openType = 'contact';
}
// #endif
// #ifdef MP-ALIPAY
if (config.type === 'aliapp') openType = 'contact';
// #endif
return { ...config, openType };
}
/**
* 判断是否应该使用自定义客服处理
* @param {Object} config 客服配置
* @returns {boolean} 是否使用自定义客服
*/
shouldUseCustomService(config) {
// #ifdef MP-WEIXIN
if (config?.type === 'weapp') {
return config.useOfficial === false;
}
// #endif
return true;
}
}
/**
* 创建客服服务实例
* @param {Object} vueInstance Vue实例
* @param {Object} externalConfig 外部最新配置
* @returns {CustomerService} 客服服务实例
* @param {Object} vueInstance Vue 实例(通常是 this
* @param {Object} externalConfig 可选:外部传入的最新配置(如从 DIY 数据中提取)
* @returns {CustomerService}
*/
export function createCustomerService(vueInstance, externalConfig = {}) {
export function createCustomerService(vueInstance, externalConfig = null) {
return new CustomerService(vueInstance, externalConfig);
}

View File

@@ -65,6 +65,10 @@ export default {
componentRefresh() {
return this.$store.state.componentRefresh;
},
// AI客服配置
globalAIKefuConfig() {
return this.$store.state.globalAIKefuConfig;
},
// 客服配置
servicerConfig() {
return this.$store.state.servicerConfig;

View File

@@ -179,6 +179,7 @@
.chat-message {
width: 100%;
height: 100%;
background: white; /* 白色 */
.message {
padding: 13rpx 20rpx;
@@ -270,7 +271,7 @@
flex-direction: row-reverse;
.content {
background-color: #4cd964;
background-color: #c4e0ff; /* 浅蓝色 */
margin-right: 28rpx;
word-break: break-all;
line-height: 36rpx;

View File

@@ -1,43 +1,57 @@
<template>
<!-- 悬浮按钮 -->
<view v-if="pageCount == 1 || need" class="fixed-box" :style="[customContainerStyle, {
height: fixBtnShow ? '400rpx' : '320rpx',
backgroundImage: bgUrl ? `url(${bgUrl})` : '',
backgroundSize: 'cover'
}]">
<view v-if="pageCount == 1 || need" class="fixed-box"
:style="[customContainerStyle, {
height: containerHeight,
backgroundImage: bgUrl ? `url( $ {bgUrl})` : '',
backgroundSize: 'cover'
}]">
<!-- 统一客服入口根据后台配置自动适配 AI / 企业微信 / 第三方等 -->
<!-- 微信官方客服需要使用 button open-type="contact" -->
<button
v-if="fixBtnShow && isWeappOfficialKefu"
class="btn-item common-bg"
open-type="contact"
:style="{ backgroundImage: currentKefuImg ? `url( $ {currentKefuImg})` : '', backgroundSize: '100% 100%' }"
>
<text class="ai-icon" v-if="!currentKefuImg">🤖</text>
</button>
<!-- 其他类型客服使用普通 view -->
<view
v-else-if="fixBtnShow"
class="btn-item common-bg"
@click="handleUnifiedKefuClick"
:style="{ backgroundImage: currentKefuImg ? `url( $ {currentKefuImg})` : '', backgroundSize: '100% 100%' }"
>
<text class="ai-icon" v-if="!currentKefuImg">🤖</text>
</view>
<!-- 新增小程序系统客服按钮当附加设置开启时显示 -->
<button
v-if="fixBtnShow && showWeappSystemKefu"
class="btn-item common-bg"
open-type="contact"
:style="{ backgroundImage: currentKefuImg ? `url( $ {currentKefuImg})` : '', backgroundSize: '100% 100%' }"
>
<text class="ai-icon" v-if="!currentKefuImg">💬</text>
</button>
<!-- 中英文切换按钮 -->
<view v-if="isLanguageSwitchEnabled && fixBtnShow" class="btn-item common-bg" @click="toggleLanguage">
<view
v-if="isLanguageSwitchEnabled && fixBtnShow"
class="btn-item common-bg"
@click="toggleLanguage"
>
<text>{{ currentLangDisplayName }}</text>
</view>
<!-- AI 智能助手 -->
<view v-if="fixBtnShow && enableAIChat" class="btn-item common-bg" @click="openAIChat"
:style="{ backgroundImage: aiAgentimg ? `url(${aiAgentimg})` : '', backgroundSize: '100% 100%' }">
<text class="ai-icon" v-if="!aiAgentimg">🤖</text>
</view>
<!-- 微信小程序客服按钮 -->
<!-- #ifdef MP-WEIXIN -->
<button class="btn-item common-bg" hoverClass="none" openType="contact" sessionFrom="weapp" showMessageCard="true"
:style="[{ backgroundImage: kefuimg ? `url(${kefuimg})` : '', backgroundSize: '100% 100%' }, customButtonStyle]">
<text class="icox icox-kefu" v-if="!kefuimg"></text>
</button>
<!-- #endif -->
<!-- 普通客服仅当未启用 AI 时显示 -->
<!-- #ifdef H5 -->
<template v-if="fixBtnShow">
<button class="btn-item common-bg" hoverClass="none" @click="openCustomerSelectPopup"
:style="[{ backgroundImage: kefuimg ? `url(${kefuimg})` : '', backgroundSize: '100% 100%' }, customButtonStyle]">
<text class="icox icox-kefu" v-if="!kefuimg"></text>
</button>
</template>
<!-- #endif -->
<!-- 电话按钮始终显示 -->
<view v-if="fixBtnShow" class="btn-item common-bg" @click="call()"
:style="[{ backgroundImage: phoneimg ? `url(${phoneimg})` : '', backgroundSize: '100% 100%' }, customButtonStyle]">
<view
v-if="fixBtnShow"
class="btn-item common-bg"
@click="call()"
:style="[{ backgroundImage: phoneimg ? `url( $ {phoneimg})` : '', backgroundSize: '100% 100%' }, customButtonStyle]"
>
<text class="iconfont icon-dianhua" v-if="!phoneimg"></text>
</view>
@@ -56,16 +70,18 @@ export default {
return {
pageCount: 0,
fixBtnShow: true,
shopInfo: null,
currentLangIndex: 0,
langIndexMap: {},
customerService: null,
kefuList: [
{ id: 'weixin-official', name: '微信官方客服', isOfficial: true, type: 'weapp' },
{ id: 'custom-kefu', name: '自定义在线客服', isOfficial: false, type: 'custom' },
{ id: 'qyweixin-kefu', name: '企业微信客服', isOfficial: false, type: 'qyweixin' }
],
selectedKefu: null,
};
},
computed: {
// 安全读取 shopInfo 中的字段,避免 undefined 报错
bgUrl() {
return this.shopInfo?.bgUrl || '';
},
@@ -73,10 +89,10 @@ export default {
return this.shopInfo?.aiAgentimg || '';
},
kefuimg() {
return this.shopInfo?.kefuimg || this.$util.getDefaultImage().kefu;
return this.shopInfo?.kefuimg || this. $util.getDefaultImage().kefu;
},
phoneimg() {
return this.shopInfo?.phoneimg || this.$util.getDefaultImage().phone;
return this.shopInfo?.phoneimg || this. $util.getDefaultImage().phone;
},
tel() {
return this.shopInfo?.mobile || '';
@@ -84,9 +100,6 @@ export default {
isLanguageSwitchEnabled() {
return !!this.shopInfo?.ischina;
},
enableAIChat() {
return !!this.shopInfo?.enableAIChat;
},
currentLangDisplayName() {
const lang = this.langIndexMap[this.currentLangIndex];
return lang === 'zh-cn' ? 'EN' : 'CN';
@@ -96,15 +109,76 @@ export default {
},
customButtonStyle() {
return this.shopInfo?.floatingButton?.button || {};
},
unreadCount() {
return this. $store.state.aiUnreadCount || 0;
},
// ✅ 新增:根据当前客服类型动态返回图标
currentKefuImg() {
if (!this.shopInfo) return '';
const customerService = createCustomerService(this);
const config = customerService.getPlatformConfig();
if (config?.type === 'aikefu') {
return this.aiAgentimg;
} else if (config?.type === 'wxwork' || config?.type === 'qyweixin') {
// 企业微信客服专用图标
return this.aiAgentimg;
}
// 默认客服图标
return this.kefuimg;
},
// ✅ 新增:判断是否为微信官方客服
isWeappOfficialKefu() {
if (!this.shopInfo) return false;
const customerService = createCustomerService(this);
const config = customerService.getPlatformConfig();
return config?.type === 'weapp';
},
// ✅ 新增:判断是否需要同时显示小程序系统客服
showWeappSystemKefu() {
if (!this.shopInfo) return false;
const customerService = createCustomerService(this);
const config = customerService.getPlatformConfig();
// 检查附加设置是否开启了同时显示小程序系统客服,且当前客服类型不是小程序系统客服
return (config?.show_system_service === true || config?.show_system_service === '1') && config?.type !== 'weapp';
},
//根据显示的按钮数量动态计算容器高度
containerHeight() {
if (!this.fixBtnShow) return '320rpx';
let buttonCount = 1;
if (this.isLanguageSwitchEnabled) buttonCount++;
if (this.showWeappSystemKefu) buttonCount++;
buttonCount++;
const totalRpx = 94 * buttonCount - 14;
const pxValue = Math.round(totalRpx * 0.5);
return ` $ {pxValue}px`;
}
},
watch: {
shopInfo: {
handler(newVal) {
// 可在此添加额外逻辑(如埋点、通知等),当前无需操作
},
immediate: true
}
},
created() {
this.customerService = createCustomerService(this);
this.initLanguage();
this.pageCount = getCurrentPages().length;
uni.getStorage({
key: 'shopInfo',
success: (e) => {
console.log('【调试】当前 shopInfo:', e.data);
this.shopInfo = e.data;
},
fail: () => {
console.warn('未获取到 shopInfo使用默认设置');
}
});
},
@@ -162,6 +236,95 @@ export default {
*/
openCustomerSelectPopup() {
this.customerService.openCustomerSelectPopupDialog();
},
// ✅ 核心方法:统一客服入口
handleUnifiedKefuClick() {
const customerService = createCustomerService(this);
const validation = customerService.validateConfig();
console.log('【客服配置验证】', validation);
if (!validation.isValid) {
console.error('客服配置无效:', validation.errors);
uni.showToast({ title: '客服暂不可用', icon: 'none' });
return;
}
if (validation.warnings.length > 0) {
console.warn('客服配置警告:', validation.warnings);
}
const platformConfig = customerService.getPlatformConfig();
console.log('【当前客服配置】', platformConfig);
// 检查企业微信配置
if (platformConfig.type === 'wxwork') {
const wxworkConfig = customerService.getWxworkConfig();
console.log('【企业微信配置】', wxworkConfig);
// #ifdef MP-WEIXIN
if (!wxworkConfig?.enable || !wxworkConfig?.contact_url) {
console.warn('企业微信配置不完整,使用原生客服');
uni.showToast({ title: '企业微信配置不完整', icon: 'none' });
}
// #endif
// #ifdef H5
if (!wxworkConfig?.contact_url && !platformConfig.wxwork_url) {
console.error('企业微信链接未配置');
uni.showToast({ title: '企业微信链接未配置', icon: 'none' });
return;
}
// #endif
}
// 直接调用统一处理方法,由 CustomerService 内部根据配置路由
try {
customerService.handleCustomerClick({
sendMessageTitle: '来自悬浮按钮的咨询',
sendMessagePath: '/pages/index/index'
});
} catch (error) {
console.error('客服处理失败:', error);
uni.showToast({ title: '打开客服失败', icon: 'none' });
}
},
// 以下方法保留用于 actionSheet如仍需手动选择
openKefuSelectPopup() {
const kefuNames = this.kefuList.map(item => item.name);
uni.showActionSheet({
itemList: kefuNames,
success: (res) => {
this.selectedKefu = this.kefuList[res.tapIndex];
const cs = createCustomerService(this, this.selectedKefu);
if (this.selectedKefu.isOfficial) {
uni.openCustomerServiceConversation({
sessionFrom: 'weapp',
showMessageCard: true
});
} else if (this.selectedKefu.id === 'qyweixin-kefu') {
// 处理企业微信客服
if (uni.getSystemInfoSync().platform === 'wechat') {
// 小程序端:跳转到企业微信客服
uni.navigateTo({
url: '/pages_tool/qyweixin-kefu/index'
});
} else {
// H5端跳转到企业微信链接
const qyweixinUrl = this.shopInfo.qyweixinUrl; // 后端返回的企业微信链接
if (qyweixinUrl) {
window.location.href = qyweixinUrl;
} else {
uni.showToast({ title: '企业微信客服未配置', icon: 'none' });
}
}
} else {
cs.handleCustomerClick();
}
}
});
}
}
}

View File

@@ -104,7 +104,7 @@
},
"router" : {
"mode" : "history",
"base" : "/hwappx/2811/"
"base" : "/hwappx/common/"
},
"title" : "",
"devServer" : {

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
<template>
<view class="ai-chat-page" :style="wrapperPageStyle">
<view class="container" :style="wrapperPageStyle">
<!--自定义导航头部 -->
<view class="custom-navbar" ref="pageHeader" v-if="showCustomNavbar">
<view class="header-left">
@@ -51,12 +51,11 @@
import { mapGetters, mapMutations } from 'vuex'
import navigationHelper from '@/common/js/navigation';
import { EventSafety } from '@/common/js/event-safety';
import aiChatMessage from './ai-chat-message.vue';
export default {
components: {
aiChatMessage,
aiChatMessage
},
data() {
return {
@@ -86,11 +85,11 @@ export default {
// --- 聊天内容区域 ---
scrollViewHeight: '0px',
// 事件处理器引用(用于清理)
safeEventHandlers: new Map()
}
},
computed: {
...mapGetters([
'globalAIKefuConfig'
@@ -109,7 +108,7 @@ export default {
},
wrapperPageStyle() {
// #ifdef H5
return `top: ${this.navBarHeight + 'px'};`
return `top: $ {this.navBarHeight + 'px'};`
// #endif
// #ifdef MP-WEIXIN
@@ -121,35 +120,42 @@ export default {
wrapperChatContentStyle() {
return {
height: this.containerHeight,
paddingTop: this.showCustomNavbar ? '0' : (this.navHeight + 'px')
paddingTop: this.showCustomNavbar ? '0' : (this.navBarHeight + 'px')
}
}
},
async onLoad(options) {
await this.initPage(options)
// ✅ 修改:取消权限校验,允许所有客服类型访问此页面
// const hasAccess = this.checkAccessPermission();
// if (!hasAccess) {
// // 如果校验失败,不继续初始化
// return;
// }
this. $langConfig.title('AI智能客服');
this.initChat();
},
async onReady() {
await this.initNavigation()
await this.initNavigation();
},
onShow() {
// 可在此补充逻辑(如刷新未读数)
},
onHide() {
// 页面隐藏时可暂停音频等
},
onUnload() {
this.cleanup()
this.cleanup();
},
methods: {
// ========== 安全事件处理 ==========
setupSafeEventListeners() {
// 使用 EventSafety 包装事件处理器
const safeHandlers = {
serviceRequest: EventSafety.wrapEventHandler(
this.handleServiceRequest.bind(this),
@@ -165,124 +171,100 @@ export default {
)
}
// 注册事件监听
this.$on('service.requestComponentInfo', safeHandlers.serviceRequest)
this.$on('navigation.requestInfo', safeHandlers.navigationRequest)
this.$on('component.interaction', safeHandlers.componentInteraction)
this. $on('service.requestComponentInfo', safeHandlers.serviceRequest);
this. $on('navigation.requestInfo', safeHandlers.navigationRequest);
this. $on('component.interaction', safeHandlers.componentInteraction);
// 保存处理器引用用于清理
this.safeEventHandlers.set('serviceRequest', safeHandlers.serviceRequest)
this.safeEventHandlers.set('navigationRequest', safeHandlers.navigationRequest)
this.safeEventHandlers.set('componentInteraction', safeHandlers.componentInteraction)
this.safeEventHandlers.set('serviceRequest', safeHandlers.serviceRequest);
this.safeEventHandlers.set('navigationRequest', safeHandlers.navigationRequest);
this.safeEventHandlers.set('componentInteraction', safeHandlers.componentInteraction);
},
setupNavigationEvents() {
// 监听窗口大小变化
uni.onWindowResize((res) => {
console.log('窗口大小变化:', res.size)
this.calculateScrollViewHeight()
})
console.log('窗口大小变化:', res.size);
this.calculateScrollViewHeight();
});
},
// ========== 事件处理方法 ==========
async handleServiceRequest(event) {
console.log('处理服务请求:', EventSafety.extractEventData(event))
console.log('处理服务请求:', EventSafety.extractEventData(event));
// 安全地检查事件目标
if (event.matches('.service-component') ||
event.detail?.componentType === 'service') {
await this.processServiceRequest(event)
await this.processServiceRequest(event);
}
},
async handleNavigationRequest(event) {
console.log('处理导航请求:', event.type)
// 提供导航信息
this.emitNavigationInfo(event)
console.log('处理导航请求:', event.type);
this.emitNavigationInfo(event);
},
handleComponentInteraction(event) {
console.log('处理组件交互:', event.detail)
// 安全地处理组件交互
this.processComponentInteraction(event)
console.log('处理组件交互:', event.detail);
this.processComponentInteraction(event);
},
handleEventError(error, event) {
console.error('事件处理错误:', {
error: error.message,
eventType: event?.type,
component: this.$options.name
})
component: this. $options.name
});
this.showError('操作失败,请重试')
this.showError('操作失败,请重试');
},
// ========== 初始化页面 ==========
async initPage(options = {}) {
this.$langConfig.title('AI智能客服');
this.initChat()
initChat() {
console.log('AI聊天页面初始化');
},
// 初始化导航栏相关配置
async initNavigation() {
try {
// 获取导航栏高度
this.navBarHeight = await navigationHelper.getNavigationHeight(this, {forceRefresh: false})
// 获取状态栏高度
this.statusBarHeight = navigationHelper.getStatusBarHeight()
// 计算滚动视图高度
this.calculateScrollViewHeight()
// 注册导航相关事件
this.setupNavigationEvents()
this.navBarHeight = await navigationHelper.getNavigationHeight(this, {forceRefresh: false});
this.statusBarHeight = navigationHelper.getStatusBarHeight();
this.calculateScrollViewHeight();
this.setupNavigationEvents();
} catch (error) {
console.error('初始化导航栏失败:', error)
this.setFallbackNavigationValues()
console.error('初始化导航栏失败:', error);
this.setFallbackNavigationValues();
}
},
// 计算滚动视图高度
calculateScrollViewHeight() {
const safeArea = navigationHelper.getSafeAreaInsets()
const bottomInset = safeArea.bottom || 0
const inputHeight = 120 // 输入区域高度
this.scrollViewHeight = `calc(100vh - ${this.navBarHeight + this.statusBarHeight + inputHeight + bottomInset}px)`
const safeArea = navigationHelper.getSafeAreaInsets();
const bottomInset = safeArea.bottom || 0;
const inputHeight = 120;
this.scrollViewHeight = `calc(100vh - $ {this.navBarHeight + this.statusBarHeight + inputHeight + bottomInset}px)`;
},
// 备用高度设置
setFallbackNavigationValues() {
// #ifdef MP-WEIXIN
this.navBarHeight = 44
this.statusBarHeight = 20
this.navBarHeight = 44;
this.statusBarHeight = 20;
// #endif
// #ifdef H5
this.navBarHeight = 44
this.statusBarHeight = 0
this.navBarHeight = 44;
this.statusBarHeight = 0;
// #endif
// #ifdef APP-PLUS
this.navBarHeight = 88
this.statusBarHeight = 44
this.navBarHeight = 88;
this.statusBarHeight = 44;
// #endif
},
cleanup() {
// 清理事件监听器
this.safeEventHandlers.forEach((handler, eventType) => {
this.$off(eventType, handler)
})
this.safeEventHandlers.clear()
console.log('组件清理完成')
this. $off(eventType, handler);
});
this.safeEventHandlers.clear();
console.log('组件清理完成');
},
showError(message) {
@@ -290,19 +272,12 @@ export default {
title: message,
icon: 'none',
duration: 2000
})
},
// 初始化聊天
initChat() {
// 可以在这里加载历史消息
console.log('AI聊天页面初始化')
});
},
// 返回上一页
goBack() {
uni.navigateBack()
uni.navigateBack();
},
// 显示菜单
@@ -312,59 +287,54 @@ export default {
success: (res) => {
switch (res.tapIndex) {
case 0:
this.clearChat()
break
this.clearChat();
break;
case 1:
this.exportChat()
break
this.exportChat();
break;
case 2:
this.showSettings()
break
this.showSettings();
break;
case 3:
this.showHelp()
break
this.showHelp();
break;
}
}
})
});
},
// 清空聊天
clearChat() {
uni.showModal({
title: '提示',
content: '确定要清空聊天记录吗?',
success: (res) => {
if (res.confirm) {
this.$refs.chat.clearMessages()
// 重新添加欢迎消息
this.$refs.chat.addMessage({
this. $refs.chat.clearMessages();
this. $refs.chat.addMessage({
id: Date.now(),
role: 'ai',
type: 'text',
content: '聊天记录已清空,有什么可以帮助您的吗?',
timestamp: Date.now()
})
});
}
}
})
});
},
// 导出聊天记录
exportChat() {
uni.showToast({
title: '功能开发中',
icon: 'none'
})
});
},
// 显示设置
showSettings() {
uni.navigateTo({
url: '/pages/settings/index'
})
});
},
// 显示帮助
showHelp() {
const helpMessage = {
id: Date.now(),
@@ -391,27 +361,18 @@ export default {
- **复制**: 长按消息可复制内容
- **转发**: 支持消息转发功能`,
timestamp: Date.now()
}
this.$refs.chat.addMessage(helpMessage)
};
this. $refs.chat.addMessage(helpMessage);
},
// 用户发送消息
onMessageSent(message) {
console.log('用户发送消息:', message)
// 使用AI服务获取回复
// AI聊天组件内部已经集成了AI服务这里只需要监听事件
console.log('用户发送消息:', message);
},
// AI回复消息
onAIResponse(message) {
console.log('AI回复消息:', message)
// 可以在这里处理AI回复后的逻辑
// 比如记录对话、更新状态等
console.log('AI回复消息:', message);
},
// 生成AI回复
generateAIResponse(userMessage) {
const responses = [
'我理解您的需求,让我为您详细解答。',
@@ -419,133 +380,142 @@ export default {
'根据您的问题,我建议您可以考虑以下几个方面:',
'这个问题很常见,让我为您提供一些解决方案。',
'我明白您的困惑,让我帮您分析一下。'
]
];
const randomResponse = responses[Math.floor(Math.random() * responses.length)]
const randomResponse = responses[Math.floor(Math.random() * responses.length)];
const aiMessage = {
id: Date.now(),
role: 'ai',
type: 'text',
content: `${randomResponse}\n\n您的问题"${userMessage.content}"我已经收到,正在为您处理中...`,
content: ` $ {randomResponse}\n\n您的问题" $ {userMessage.content}"我已经收到,正在为您处理中...`,
timestamp: Date.now(),
actions: [
{ type: 'like', count: 0 },
{ type: 'dislike', count: 0 }
]
}
};
this.$refs.chat.addMessage(aiMessage)
this.$emit('ai-response', aiMessage)
this. $refs.chat.addMessage(aiMessage);
this. $emit('ai-response', aiMessage);
},
// 操作按钮点击
onActionClick({ action, message }) {
console.log('操作点击:', action, message)
switch (action.type) {
case 'like':
uni.showToast({
title: '感谢您的反馈!',
icon: 'success'
})
break
uni.showToast({ title: '感谢您的反馈!', icon: 'success' });
break;
case 'dislike':
uni.showToast({
title: '我们会改进服务',
icon: 'none'
})
break
uni.showToast({ title: '我们会改进服务', icon: 'none' });
break;
}
},
// 历史消息加载完成
onHistoryLoaded(messages) {
console.log('历史消息加载完成:', messages.length)
console.log('历史消息加载完成:', messages.length);
},
// 文件预览
onFilePreview(message) {
console.log('文件预览:', message)
uni.showToast({
title: '打开文件: ' + message.fileName,
icon: 'none'
})
uni.showToast({ title: '打开文件: ' + message.fileName, icon: 'none' });
},
// 音频播放
onAudioPlay(message) {
console.log('音频播放:', message)
console.log('音频播放:', message);
},
// 音频暂停
onAudioPause(message) {
console.log('音频暂停:', message)
console.log('音频暂停:', message);
},
// 视频播放
onVideoPlay(message) {
console.log('视频播放:', message)
console.log('视频播放:', message);
},
// 视频暂停
onVideoPause(message) {
console.log('视频暂停:', message)
console.log('视频暂停:', message);
},
// 链接打开
onLinkOpen(message) {
console.log('链接打开:', message)
uni.showModal({
title: '打开链接',
content: `确定要打开链接:${message.url} 吗?`,
content: `确定要打开链接: $ {message.url} 吗?`,
success: (res) => {
if (res.confirm) {
// #ifdef H5
window.open(message.url, '_blank')
window.open(message.url, '_blank');
// #endif
// #ifdef APP-PLUS
plus.runtime.openURL(message.url)
plus.runtime.openURL(message.url);
// #endif
}
}
})
});
},
// 商品查看
onProductView(message) {
console.log('商品查看:', message)
uni.showToast({
title: '查看商品: ' + message.title,
icon: 'none'
})
uni.showToast({ title: '查看商品: ' + message.title, icon: 'none' });
},
// 输入内容变化
onInputChange(value) {
console.log('输入内容:', value)
console.log('输入内容:', value);
},
// ✅ 新增:权限校验(核心修复)
checkAccessPermission() {
const servicerConfig = this. $store.state.servicerConfig;
// 如果配置未加载延迟重试最多3次防白屏
if (!servicerConfig) {
console.warn('【AI客服】servicerConfig 未加载200ms后重试...');
setTimeout(() => {
this.checkAccessPermission();
}, 200);
return false;
}
let currentType = 'none';
// #ifdef MP-WEIXIN
currentType = servicerConfig?.weapp?.type || 'none';
// #endif
// #ifdef H5
currentType = servicerConfig?.h5?.type || 'none';
// #endif
if (currentType !== 'aikefu') {
uni.showToast({ title: '当前客服类型不支持此页面', icon: 'none' });
setTimeout(() => {
const pages = getCurrentPages();
if (pages.length > 1) {
uni.navigateBack();
} else {
// 回到首页(假设首页是 tab 页)
uni.switchTab({ url: '/pages/index/index' });
}
}, 1500);
return false;
}
return true;
}
}
}
</script>
<style lang="scss">
/* 引入图标字体 */
@import url('@/common/css/iconfont.css');
@import url('/common/css/iconfont.css');
/* 页面样式 */
.ai-chat-page {
display: flex;
flex-direction: column;
background-color: #f8f8f8;
overflow: hidden;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.container {
display: flex;
flex-direction: column;
background-color: white; /* 白色 */
overflow: hidden;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
/* 自定义导航头部 */
.custom-navbar {
@@ -553,7 +523,7 @@ export default {
align-items: center;
justify-content: space-between;
padding: 20rpx 30rpx;
background-color: white;
background-color: #c4e0ff; /* 浅蓝色 */
border-bottom: 2rpx solid #eeeeee;
.header-left {
@@ -565,7 +535,7 @@ export default {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
background-color: #f8f8f8;
background-color: #ffffff; /* 白色按钮 */
display: flex;
align-items: center;
justify-content: center;
@@ -610,7 +580,7 @@ export default {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
background-color: #f8f8f8;
background-color: #ffffff; /* 白色按钮 */
display: flex;
align-items: center;
justify-content: center;
@@ -634,18 +604,11 @@ export default {
display: flex;
flex-direction: column;
overflow: hidden;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
/* 底部tabBar占位样式 */
.page-bottom {
width: 100%;
background-color: transparent;
}
</style>

View File

@@ -1,4 +1,4 @@
module.exports = {
baseUrl: "https://xcx6.aigc-quickapp.com/",//修改域名
uniacid: 2811,//后台对应uniacid
baseUrl: "https://dev.aigc-quickapp.com/",//修改域名
uniacid: 1,//后台对应uniacid
};

View File

@@ -65,6 +65,7 @@ const store = new Vuex.Store({
bottomNavHidden: false, // 底部导航是否隐藏true隐藏false显示
aiUnreadCount: 10, // AI未读消息数量
globalAIKefuConfig: null, // AI客服配置
customerServiceType: 'ai',
globalStoreConfig: null, // 门店配置
globalStoreInfo: null, // 门店信息
defaultStoreInfo: null, // 默认门店
@@ -159,6 +160,9 @@ const store = new Vuex.Store({
state.globalAIKefuConfig = value;
uni.setStorageSync('globalAIKefuConfig', value); // 初始化数据调用
},
setCustomerServiceType(state, type) {
state.customerServiceType = type;
},
setGlobalStoreConfig(state, value) {
state.globalStoreConfig = value;
uni.setStorageSync('globalStoreConfig', value); // 初始化数据调用