Compare commits

...

50 Commits

Author SHA1 Message Date
5703405598 chore:添加了快应用打包步骤 2026-04-08 15:51:10 +08:00
e19fef785f chore:改了commom 2026-04-08 14:39:23 +08:00
92ae114c5c chore: 2026-04-08 11:47:07 +08:00
b25c366a5b chore:增加了弹窗用户隐私 2026-04-08 11:37:29 +08:00
4ad93ceace refactor: 将视频标题样式绑定从对象改为数组 2026-02-07 18:11:42 +08:00
294856d9ef Merge branch 'test/diy-tabs' into dev/1.0 2026-02-07 17:53:30 +08:00
ba2c7d6d64 Revert "chore:点击中英文按钮不跳到‘我的’了"
This reverts commit 19a53ef414.
2026-02-07 17:51:10 +08:00
ec9e76fda7 chore:改了中英文模式 2026-02-07 16:53:40 +08:00
5dba8071e4 refactor(diy-tab): 统一样式绑定语法为数组形式
将模板中的样式绑定从对象展开语法改为数组形式,保持代码风格一致
2026-02-07 16:16:43 +08:00
19a53ef414 chore:点击中英文按钮不跳到‘我的’了 2026-02-07 16:10:55 +08:00
e943b11c8f fix(组件): 修复tab组件中子组件moduleIndex冲突问题
为tab组件中的子组件添加tabIndex前缀,避免不同tab页中子组件的moduleIndex冲突
2026-02-07 15:57:49 +08:00
e9db54980e refactor(diy-tab): 使用模板字符串和可选链优化key生成逻辑 2026-02-07 14:59:26 +08:00
3ddcc665b8 refactor(diy-tab): 统一使用生成函数获取tab和panel的key
使用getTabKey和getPanelKey方法统一生成key,提高代码可维护性
2026-02-07 14:11:51 +08:00
a981c02baf chore:智能客服页面右上角改成齿轮图标,更多功能图标改为(...) 2026-02-07 13:59:43 +08:00
1c3f759b35 feat: 为DIY标签组件添加可配置的动画效果 2026-02-07 11:41:59 +08:00
bb58866192 feat: 为标签组件添加悬停动画和对齐选项 2026-02-07 11:34:45 +08:00
069b3c2c07 feat: 为DIY标签页组件添加滚动容器和样式 2026-02-06 17:55:44 +08:00
c6fd13956a chore:H5和微信小程序的第三方客服悬浮按钮应该会显示 👤 2026-02-06 17:49:48 +08:00
a489e24988 chore:改了企业微信客服,智能客服的悬浮按钮 2026-02-06 17:18:58 +08:00
6897fc631b feat: 重构DIY标签页组件,支持多种布局和样式配置 2026-02-06 16:47:32 +08:00
6d23a64aef chore:智能客服的页面全部显示出来 2026-02-06 16:30:33 +08:00
f34533b0c7 chore:进入智能客服的页面加了智能客服的头像 2026-02-06 16:08:50 +08:00
72c74f44f0 chore:删除弹出的框的一个叉 2026-02-06 16:02:11 +08:00
7f3d9c0f10 chore:将更改头像和昵称的按钮放在...的设置中 2026-02-06 15:56:00 +08:00
37f95e7267 chore:改了客服的图标 2026-02-05 18:30:16 +08:00
d3c630e860 chore:后台点击什么,页面显示什么 2026-02-05 16:58:54 +08:00
c5b57cedeb chore:后台点击不启用,H5和微信小程序不显示 2026-02-05 16:34:33 +08:00
bf24746a7c chore;更改了客服和中英文按钮的顺序 2026-02-05 10:14:27 +08:00
5ff2925ba1 chore: 2026-02-05 09:56:37 +08:00
3dc81e7f53 chore:优化了hover 2026-02-05 09:50:36 +08:00
0417c5c5ed chore:优化了hover 2026-02-05 09:27:43 +08:00
db26a85c8b chore:商品,名片,我的都能正常跳转了 2026-02-04 17:09:57 +08:00
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
18 changed files with 3542 additions and 2379 deletions

View File

@@ -42,6 +42,14 @@ const localDevConfig = ({
uniacid: 2, uniacid: 2,
domain: 'http://localhost:8050/', domain: 'http://localhost:8050/',
}, },
})['2811']; // 选择要使用的环境配置 '47': { // 修改弹窗
uniacid: 47,
domain: 'https://5g.aigc-quickapp.com/',
},
'2075': { //
uniacid: 2075,
domain: 'https://xcx.aigc-quickapp.com/',
},
})['47']; // 选择要使用的环境配置
export default localDevConfig; export default localDevConfig;

View File

@@ -62,53 +62,105 @@ export default {
*/ */
async sendStreamMessage(message, onChunk, onComplete) { async sendStreamMessage(message, onChunk, onComplete) {
// #ifdef MP-WEIXIN // #ifdef MP-WEIXIN
// 微信小程序:降级为普通请求 + 前端打字模拟 return new Promise((resolve, reject) => {
try { const socketTask = wx.connectSocket({
const result = await this.sendMessage(message); url: 'wss://dev.aigc-quickapp.com/ws/aikefu',
const content = result.content || ''; header: {}
const conversationId = result.conversationId || ''; });
// 保存会话ID(确保连续对话) let content = '';
let conversationId = '';
let isAuthenticated = false;
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) { if (conversationId) {
this.setConversationId(conversationId); this.setConversationId(conversationId);
} }
// 模拟打字效果
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) { if (onComplete) {
onComplete({ onComplete({ content, conversation_id: conversationId });
content: content,
conversation_id: conversationId
});
} }
resolve({ content, conversation_id: conversationId }); resolve({ content, conversation_id: conversationId });
socketTask.close();
} }
}, 80); // 打字速度:80ms/次
});
} catch (error) {
console.error('小程序流式消息降级失败:', error);
if (onComplete) {
onComplete({ error: error.message || '发送失败' });
}
throw error;
}
// #endif
// #ifdef H5 // 可选:处理 done
// H5:使用真实流式(EventSource / Fetch) else if (data.type === 'done') {
return this.sendHttpStream(message, onChunk, onComplete); 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 // #endif
}, },
/** /**
* HTTP 流式请求(仅 H5 使用) * HTTP 流式请求(仅 H5 使用)
*/ */
@@ -132,11 +184,8 @@ export default {
}) })
}); });
if (!response.ok) { if (!response.ok || !response.body) {
throw new Error(`HTTP ${response.status}`); throw new Error('无效响应');
}
if (!response.body) {
throw new Error('响应体不可用');
} }
const reader = response.body.getReader(); const reader = response.body.getReader();
@@ -145,50 +194,64 @@ export default {
let content = ''; let content = '';
let conversationId = ''; let conversationId = '';
function processBuffer(buf, callback) { while (true) {
const lines = buf.split('\n'); const { done, value } = await reader.read();
buf = lines.pop() || ''; if (done) break;
buffer += decoder.decode(value, { stream: true });
// 按行分割,保留不完整的最后一行
const lines = buffer.split('\n');
buffer = lines.pop() || ''; // 未完成的行留到下次
for (const line of lines) { for (const line of lines) {
const trimmed = line.trim(); const trimmed = line.trim();
if (trimmed.startsWith('data:')) { 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); const data = JSON.parse(jsonStr);
if (data.event === 'message') { if (data.event === 'message') {
const text = data.answer || data.text || ''; const text = data.answer || data.text || '';
content += text; content += text;
callback(text); if (onChunk) onChunk(text);
} }
if (data.conversation_id) { if (data.conversation_id) {
conversationId = data.conversation_id; conversationId = data.conversation_id;
} }
if (data.event === 'message_end') { if (data.event === 'message_end') {
// 可选:提前完成 // 可提前结束
}
} }
} catch (e) { } catch (e) {
console.warn('解析流数据失败:', e); console.warn('解析失败:', e, line);
} }
} }
} }
} }
return buf;
}
while (true) { // 处理最后残留的 buffer(如果有)
const { done, value } = await reader.read(); if (buffer.trim().startsWith('data:')) {
if (done) break; try {
buffer += decoder.decode(value, { stream: true }); const jsonStr = buffer.trim().slice(5);
buffer = processBuffer(buffer, (chunk) => { if (jsonStr) {
if (onChunk) onChunk(chunk); 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) { if (onComplete) {
onComplete({ onComplete({ content, conversation_id: conversationId });
content,
conversation_id: conversationId
});
} }
return { content, conversation_id: conversationId }; return { content, conversation_id: conversationId };
} catch (error) { } catch (error) {
@@ -196,11 +259,6 @@ export default {
throw error; throw error;
} }
// #endif // #endif
// #ifdef MP-WEIXIN
// 理论上不会执行到这里,但防止 fallback
return this.sendStreamMessage(message, onChunk, onComplete);
// #endif
}, },
/** /**

View File

@@ -2,68 +2,13 @@
* 客服统一处理服务 * 客服统一处理服务
* 整合各种客服方式,提供统一的调用接口 * 整合各种客服方式,提供统一的调用接口
*/ */
class CustomerService { export class CustomerService {
constructor(vueInstance, externalConfig = {}) { constructor(vueInstance, externalConfig = null) {
if (!vueInstance.$lang) {
throw new Error('CustomerService 必须在 Vue 实例中初始化');
}
this.vm = vueInstance; this.vm = vueInstance;
this.externalConfig = externalConfig; // 外部传入的最新配置(优先级最高) this.externalConfig = externalConfig; // 外部传入的最新配置(优先级最高)
this.latestPlatformConfig = null; 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 外部最新配置 * @param {Object} externalConfig 外部最新配置
@@ -83,28 +28,29 @@ class CustomerService {
return this.latestPlatformConfig; return this.latestPlatformConfig;
} }
// 优先级:外部传入的最新配置 > vuex配置 > 空对象 // 优先级:外部传入 > vuex store > 空对象
const servicerConfig = this.externalConfig || this.vm.$store.state.servicerConfig || {}; const servicerConfig = this.externalConfig || this.vm.$store.state.servicerConfig || {};
console.log(`【实时客服配置】`, servicerConfig); console.log(`【实时客服配置】`, servicerConfig);
let platformConfig = null; let platformConfig = null;
// #ifdef H5 // #ifdef H5
platformConfig = servicerConfig.h5 ? (typeof servicerConfig.h5 === 'object' ? servicerConfig.h5 : null) : null; platformConfig = servicerConfig.h5 && typeof servicerConfig.h5 === 'object' ? servicerConfig.h5 : null;
// #endif // #endif
// #ifdef MP-WEIXIN // #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 // #endif
// #ifdef MP-ALIPAY // #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 // #endif
// #ifdef PC // #ifdef PC
platformConfig = servicerConfig.pc ? (typeof servicerConfig.pc === 'object' ? servicerConfig.pc : null) : null; platformConfig = servicerConfig.pc && typeof servicerConfig.pc === 'object' ? servicerConfig.pc : null;
// #endif // #endif
// 处理空数组情况(你的配置中pc/aliapp是空数组,转为null) // 防止空数组被当作有效配置
if (Array.isArray(platformConfig)) { if (Array.isArray(platformConfig)) {
platformConfig = null; platformConfig = null;
} }
@@ -130,6 +76,15 @@ class CustomerService {
return config && typeof config === 'object' && config.type; return config && typeof config === 'object' && config.type;
} }
/**
* 检查是否有有效的客服配置(用于判断是否显示客服按钮)
* @returns {Boolean}
*/
hasValidKefuConfig() {
const config = this.getPlatformConfig();
return config && typeof config === 'object' && config.type && config.type !== 'none';
}
/** /**
* 验证客服配置完整性 * 验证客服配置完整性
* @returns {Object} 验证结果 * @returns {Object} 验证结果
@@ -144,32 +99,18 @@ class CustomerService {
warnings: [] warnings: []
}; };
if (!config) { if (!config || !config.type) {
result.isValid = false;
result.errors.push('客服配置不存在');
return result;
}
if (config.type === 'aikefu') {
return result;
}
if (!config.type) {
result.isValid = false; result.isValid = false;
result.errors.push('客服类型未配置'); result.errors.push('客服类型未配置');
return result;
} }
if (config.type === 'wxwork') { if (config.type === 'wxwork') {
if (!wxworkConfig) { if (!wxworkConfig || !wxworkConfig.enable) {
result.isValid = false; result.warnings.push('企业微信未启用');
result.errors.push('企业微信配置不存在');
} else {
if (!wxworkConfig.enable) {
result.warnings.push('企业微信功能未启用');
} }
if (!wxworkConfig.contact_url) { if (!wxworkConfig.contact_url) {
result.warnings.push('企业微信活码链接未配置,将使用原有客服方式'); result.warnings.push('企业微信活码链接未配置');
}
} }
} }
@@ -177,16 +118,39 @@ class CustomerService {
} }
/** /**
* 跳转到AI客服页面 * 跳转到 AI 客服页面(Dify)
*/ */
openAIKeFuService() { openDifyService() {
const vm = this.vm; try {
vm.$util.redirectTo(vm.$util.AI_CHAT_PAGE_URL); // 清除未读数(如果存在)
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 = {}) { handleCustomerClick(options = {}) {
const validation = this.validateConfig(); const validation = this.validateConfig();
@@ -201,51 +165,61 @@ class CustomerService {
} }
const config = this.getPlatformConfig(); 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') { if (config.type === 'none') {
this.showNoServicePopup(); this.showNoServicePopup();
return; return;
} }
// 核心分支:根据最新的type处理 // 核心路由:根据 type 决定行为
switch (config.type) { switch (config.type) {
case 'aikefu': case 'aikefu':
this.openAIKeFuService(); console.log('【跳转 AI 客服】目标路径: /pages_tool/ai-chat/index');
this.openDifyService();
break; break;
case 'wxwork': case 'wxwork':
console.log('【跳转企业微信客服】');
this.openWxworkService(false, config, options); this.openWxworkService(false, config, options);
break; break;
case 'third': case 'third':
console.log('【跳转第三方客服】');
this.openThirdService(config);
break;
case 'miniprogram':
console.log('【跳转第三方小程序客服】');
this.openThirdService(config); this.openThirdService(config);
break; break;
case 'niushop': case 'niushop':
console.log('【跳转牛商客服】');
this.openNiushopService(niushop); this.openNiushopService(niushop);
break; break;
case 'weapp': case 'weapp':
console.log('【跳转微信官方客服】');
this.openWeappService(config, options); this.openWeappService(config, options);
break; break;
case 'aliapp': case 'aliapp':
console.log('【跳转支付宝客服】');
this.openAliappService(config); this.openAliappService(config);
break; break;
default: default:
console.error('【未知客服类型】', config.type);
this.makePhoneCall(); this.makePhoneCall();
} }
} }
/** // ================== 各类型客服实现 ==================
* 打开企业微信客服
* @param {boolean} useOriginalService 是否使用原有客服方式
* @param {Object} servicerConfig 客服配置
* @param {Object} options 选项参数
*/
openWxworkService(useOriginalService = false, servicerConfig = null, options = {}) { openWxworkService(useOriginalService = false, servicerConfig = null, options = {}) {
const config = servicerConfig || this.getPlatformConfig(); const config = servicerConfig || this.getPlatformConfig();
const wxworkConfig = this.getWxworkConfig(); const wxworkConfig = this.getWxworkConfig();
const { sendMessageTitle = '', sendMessagePath = '', sendMessageImg = '' } = options || {}; const { sendMessageTitle = '', sendMessagePath = '', sendMessageImg = '' } = options;
// #ifdef MP-WEIXIN // #ifdef MP-WEIXIN
if (wxworkConfig?.enable && wxworkConfig?.contact_url && !useOriginalService) { if (!useOriginalService && wxworkConfig?.enable && wxworkConfig?.contact_url) {
wx.navigateToMiniProgram({ wx.navigateToMiniProgram({
appId: 'wxeb490c6f9b154ef9', appId: 'wxeb490c6f9b154ef9',
path: `pages/contacts/externalContactDetail?url=${encodeURIComponent(wxworkConfig.contact_url)}`, path: `pages/contacts/externalContactDetail?url=${encodeURIComponent(wxworkConfig.contact_url)}`,
@@ -256,9 +230,16 @@ class CustomerService {
} }
}); });
} else { } else {
// 检查是否有企业微信配置
if (!config.wxwork_url && !config.corpid) {
console.error('企业微信配置不完整,缺少 wxwork_url 或 corpid');
uni.showToast({ title: '企业微信配置不完整', icon: 'none' });
this.fallbackToPhoneCall();
return;
}
wx.openCustomerServiceChat({ wx.openCustomerServiceChat({
extInfo: { url: config.wxwork_url }, extInfo: { url: config.wxwork_url || '' },
corpId: config.corpid, corpId: config.corpid || '',
showMessageCard: true, showMessageCard: true,
sendMessageTitle, sendMessageTitle,
sendMessagePath, sendMessagePath,
@@ -268,120 +249,125 @@ class CustomerService {
// #endif // #endif
// #ifdef H5 // #ifdef H5
if (wxworkConfig?.enable && wxworkConfig?.contact_url) { if (!useOriginalService && wxworkConfig?.enable && wxworkConfig?.contact_url) {
window.location.href = wxworkConfig.contact_url; window.location.href = wxworkConfig.contact_url;
} else if (config.wxwork_url) { } else if (config.wxwork_url) {
location.href = config.wxwork_url; window.location.href = config.wxwork_url;
} else { } else {
this.fallbackToPhoneCall(); this.fallbackToPhoneCall();
} }
// #endif // #endif
} }
/**
* 打开第三方客服
* @param {Object} config 客服配置
*/
openThirdService(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) { if (config.third_url) {
console.log('【跳转第三方链接】', config.third_url);
// #ifdef H5
window.location.href = config.third_url; window.location.href = config.third_url;
// #endif
// #ifdef MP-WEIXIN
uni.setClipboardData({
data: config.third_url,
success: () => {
uni.showToast({ title: '链接已复制,请在浏览器打开', icon: 'none' });
}
});
// #endif
} else { } else {
console.error('【第三方客服配置不完整】缺少 mini_app_id 或 third_url');
this.fallbackToPhoneCall(); this.fallbackToPhoneCall();
} }
} }
/**
* 打开牛商客服
* @param {Object} niushop 牛商参数
*/
openNiushopService(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); this.vm.$util.redirectTo('/pages_tool/chat/room', niushop);
} else { } else {
this.makePhoneCall(); this.makePhoneCall();
} }
} }
/**
* 打开微信小程序客服
* @param {Object} config 客服配置
* @param {Object} options 选项参数
*/
openWeappService(config, options = {}) { openWeappService(config, options = {}) {
if (!this.shouldUseCustomService(config)) { // 如果 useOfficial 为 true 或 undefined,则使用原生系统客服(由 button open-type="contact" 触发)
console.log('使用官方微信小程序客服'); // 此方法仅用于自定义跳转(如 useOfficial: false)
if (config.useOfficial !== false) {
// 不做任何事,应由 <button open-type="contact"> 触发
console.log('使用微信官方客服,请确保按钮为 <button open-type="contact">');
return; return;
} }
console.log('使用自定义微信小程序客服');
this.handleCustomWeappService(config, options); this.handleCustomWeappService(config, options);
} }
/**
* 处理自定义微信小程序客服
* @param {Object} config 客服配置
* @param {Object} options 选项参数
*/
handleCustomWeappService(config, options = {}) { handleCustomWeappService(config, options = {}) {
const { sendMessageTitle = '', sendMessagePath = '', sendMessageImg = '' } = options || {};
if (config.customServiceUrl) { if (config.customServiceUrl) {
let url = config.customServiceUrl; let url = config.customServiceUrl;
const params = []; const params = [];
const { sendMessageTitle, sendMessagePath, sendMessageImg } = options;
if (sendMessageTitle) params.push(`title=${encodeURIComponent(sendMessageTitle)}`); if (sendMessageTitle) params.push(`title=${encodeURIComponent(sendMessageTitle)}`);
if (sendMessagePath) params.push(`path=${encodeURIComponent(sendMessagePath)}`); if (sendMessagePath) params.push(`path=${encodeURIComponent(sendMessagePath)}`);
if (sendMessageImg) params.push(`img=${encodeURIComponent(sendMessageImg)}`); if (sendMessageImg) params.push(`img=${encodeURIComponent(sendMessageImg)}`);
if (params.length > 0) { if (params.length > 0) {
url += (url.includes('?') ? '&' : '?') + params.join('&'); url += (url.includes('?') ? '&' : '?') + params.join('&');
} }
uni.navigateTo({ url });
uni.navigateTo({
url: url,
fail: (err) => {
console.error('跳转自定义客服页面失败:', err);
this.tryThirdPartyService(config, options);
}
});
return; return;
} }
this.tryThirdPartyService(config, options); this.tryThirdPartyService(config, options);
} }
/**
* 尝试使用第三方客服
* @param {Object} config 客服配置
* @param {Object} options 选项参数
*/
tryThirdPartyService(config, options = {}) { tryThirdPartyService(config, options = {}) {
if (config.thirdPartyServiceUrl) { // 支持第三方微信小程序客服
// #ifdef H5 if (config.thirdPartyMiniAppId || config.mini_app_id) {
window.open(config.thirdPartyServiceUrl, '_blank');
// #endif
// #ifdef MP-WEIXIN // #ifdef MP-WEIXIN
if (config.thirdPartyMiniAppId) {
wx.navigateToMiniProgram({ wx.navigateToMiniProgram({
appId: config.thirdPartyMiniAppId, appId: config.thirdPartyMiniAppId || config.mini_app_id,
path: config.thirdPartyMiniAppPath || '', path: config.thirdPartyMiniAppPath || config.mini_app_path || ''
fail: (err) => {
console.error('跳转第三方小程序失败:', err);
this.fallbackToPhoneCall();
}
});
} else {
uni.setClipboardData({
data: config.thirdPartyServiceUrl,
success: () => {
uni.showModal({
title: '客服链接已复制',
content: '客服链接已复制到剪贴板,请在浏览器中粘贴访问',
showCancel: false
});
}
}); });
// #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 // #endif
return; return;
} }
@@ -389,106 +375,55 @@ class CustomerService {
this.fallbackToPhoneCall(); this.fallbackToPhoneCall();
} }
/**
* 降级到电话客服
*/
fallbackToPhoneCall() {
uni.showModal({
title: '联系客服',
content: '在线客服暂时不可用,是否拨打电话联系客服?',
success: (res) => {
if (res.confirm) {
this.makePhoneCall();
}
}
});
}
/**
* 打开支付宝小程序客服
* @param {Object} config 客服配置
*/
openAliappService(config) { openAliappService(config) {
console.log('支付宝小程序客服', config); if (config.type === 'aikefu') {
switch (config.type) { this.openDifyService();
case 'aikefu': } else if (config.type === 'third') {
this.openAIKeFuService();
break;
case 'third':
this.openThirdService(config); this.openThirdService(config);
break;
default:
console.log('使用支付宝官方客服');
break;
}
}
/**
* 拨打电话
*/
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
});
} else { } else {
uni.showToast({ // 支付宝原生客服由 button open-type="contact" 触发,此处不处理
title: '暂无客服电话', console.log('使用支付宝官方客服');
icon: 'none'
});
} }
} }
// ================== 辅助方法 ==================
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' });
}
},
fail: () => {
uni.showToast({ title: '获取客服电话失败', icon: 'none' });
}
}); });
} }
/**
* 显示无客服弹窗
*/
showNoServicePopup() { showNoServicePopup() {
const siteInfo = this.vm.$store.state.siteInfo || {}; const siteInfo = this.vm.$store.state.siteInfo || {};
const message = siteInfo?.site_tel 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) { showConfigErrorPopup(errors) {
const message = errors.join('\n');
uni.showModal({ uni.showModal({
title: '配置错误', title: '客服配置错误',
content: `客服配置有误:\n${message}`, content: `配置有误:\n${errors.join('\n')}`,
showCancel: false showCancel: false
}); });
} }
/**
* 降级处理:使用原有客服方式
*/
fallbackToOriginalService() { fallbackToOriginalService() {
uni.showModal({ uni.showModal({
title: '提示', title: '提示',
content: '无法直接添加企业微信客服,是否使用其他方式联系客服?', content: '无法直接添加企业微信,是否使用其他方式联系客服?',
success: (res) => { success: (res) => {
if (res.confirm) { if (res.confirm) {
this.openWxworkService(true); this.openWxworkService(true);
@@ -497,9 +432,19 @@ class CustomerService {
}); });
} }
fallbackToPhoneCall() {
uni.showModal({
title: '提示',
content: '在线客服不可用,是否拨打电话联系客服?',
success: (res) => {
if (res.confirm) this.makePhoneCall();
}
});
}
/** /**
* 获取客服按钮配置 * 获取按钮配置(用于 template 中 v-if / open-type 判断)
* @returns {Object} 按钮配置 * @returns {Object}
*/ */
getButtonConfig() { getButtonConfig() {
const config = this.getPlatformConfig(); const config = this.getPlatformConfig();
@@ -507,11 +452,10 @@ class CustomerService {
let openType = ''; let openType = '';
// #ifdef MP-WEIXIN // #ifdef MP-WEIXIN
if (config.type === 'weapp') { if (config.type === 'weapp' && config.useOfficial !== false) {
openType = config.useOfficial !== false ? 'contact' : ''; openType = 'contact';
} }
// #endif // #endif
// #ifdef MP-ALIPAY // #ifdef MP-ALIPAY
if (config.type === 'aliapp') openType = 'contact'; if (config.type === 'aliapp') openType = 'contact';
// #endif // #endif
@@ -520,26 +464,241 @@ class CustomerService {
} }
/** /**
* 判断是否应该使用自定义客服处理 * 获取客服按钮图标
* @param {Object} config 客服配置 * @returns {String} 图标URL
* @returns {boolean} 是否使用自定义客服
*/ */
shouldUseCustomService(config) { getKefuButtonIcon() {
const shopInfo = this.vm.shopInfo || {};
const config = this.getPlatformConfig();
if (config?.type === 'aikefu') {
return shopInfo.aiAgentimg || '';
} else if (config?.type === 'wxwork' || config?.type === 'qyweixin') {
return shopInfo.aiAgentimg || '';
} else if (config?.type === 'third') {
return '';
}
return shopInfo.kefuimg || this.vm.$util?.getDefaultImage()?.kefu || '';
}
/**
* 判断是否为微信官方客服
* @returns {Boolean}
*/
isWeappOfficialKefu() {
const config = this.getPlatformConfig();
return config?.type === 'weapp';
}
/**
* 判断是否为智能客服
* @returns {Boolean}
*/
isAIKefu() {
const config = this.getPlatformConfig();
return config?.type === 'aikefu';
}
/**
* 判断是否为企业微信客服
* @returns {Boolean}
*/
isWxworkKefu() {
const config = this.getPlatformConfig();
return config?.type === 'wxwork' || config?.type === 'qyweixin';
}
/**
* 判断是否为第三方客服
* @returns {Boolean}
*/
isThirdKefu() {
const config = this.getPlatformConfig();
return config?.type === 'third';
}
/**
* 判断是否需要同时显示小程序系统客服
* @returns {Boolean}
*/
shouldShowExtraWeappSystemKefu() {
const config = this.getPlatformConfig();
return (config?.show_system_service === true || config?.show_system_service === '1') && config?.type !== 'weapp';
}
/**
* 计算容器高度
* @param {Boolean} isLanguageSwitchEnabled 是否启用语言切换
* @param {Boolean} showWeappSystemKefu 是否显示小程序系统客服
* @param {Boolean} fixBtnShow 是否显示按钮
* @returns {String} 高度值(px)
*/
getContainerHeight(isLanguageSwitchEnabled, showWeappSystemKefu, fixBtnShow) {
if (!fixBtnShow) return '160px';
let buttonCount = 1;
if (isLanguageSwitchEnabled) buttonCount++;
if (showWeappSystemKefu) buttonCount++;
const totalRpx = 94 * buttonCount - 14;
const pxValue = Math.round(totalRpx * 0.5);
return `${pxValue}px`;
}
/**
* 获取未读消息数量
* @returns {Number}
*/
getUnreadCount() {
return this.vm.$store?.state?.aiUnreadCount || 0;
}
/**
* 拨打电话联系客服
* @param {String} phoneNumber 电话号码
*/
makePhoneCallByNumber(phoneNumber) {
if (phoneNumber) {
uni.makePhoneCall({ phoneNumber });
} else {
this.makePhoneCall();
}
}
/**
* 打开 AI 智能助手
*/
openAIChat() {
try {
if (typeof this.vm.setAiUnreadCount === 'function') {
this.vm.setAiUnreadCount(0);
}
const aiChatUrl = '/pages_tool/ai-chat/index';
uni.navigateTo({
url: aiChatUrl,
fail: (err) => {
console.error('跳转 AI 客服失败:', err);
// #ifdef H5
window.location.href = aiChatUrl;
// #endif
uni.showToast({ title: '打开客服失败', icon: 'none' });
}
});
} catch (e) {
console.error('跳转 AI 客服异常:', e);
uni.showToast({ title: '打开客服失败', icon: 'none' });
}
}
/**
* 打开客服选择对话框(预留方法)
*/
openCustomerSelectPopupDialog() {
uni.showToast({ title: '客服选择功能开发中', icon: 'none' });
}
/**
* 打开客服选择弹出层(ActionSheet)
* @param {Array} kefuList 客服列表
*/
openKefuSelectPopup(kefuList) {
const kefuNames = kefuList.map(item => item.name);
uni.showActionSheet({
itemList: kefuNames,
success: (res) => {
const selectedKefu = kefuList[res.tapIndex];
const cs = createCustomerService(this.vm, selectedKefu);
if (selectedKefu.isOfficial) {
uni.openCustomerServiceConversation({
sessionFrom: 'weapp',
showMessageCard: true
});
} else if (selectedKefu.id === 'qyweixin-kefu') {
if (uni.getSystemInfoSync().platform === 'wechat') {
uni.navigateTo({
url: '/pages_tool/qyweixin-kefu/index'
});
} else {
const qyweixinUrl = this.vm.shopInfo?.qyweixinUrl;
if (qyweixinUrl) {
window.location.href = qyweixinUrl;
} else {
uni.showToast({ title: '企业微信客服未配置', icon: 'none' });
}
}
} else {
cs.handleCustomerClick();
}
}
});
}
/**
* 核心方法:统一客服入口(带验证和配置检查)
* @param {Object} options 选项参数
*/
handleUnifiedKefuClick(options = {}) {
const validation = this.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 = this.getPlatformConfig();
console.log('【当前客服配置】', platformConfig);
// 检查企业微信配置
if (platformConfig.type === 'wxwork') {
const wxworkConfig = this.getWxworkConfig();
console.log('【企业微信配置】', wxworkConfig);
// #ifdef MP-WEIXIN // #ifdef MP-WEIXIN
if (config?.type === 'weapp') { if (!wxworkConfig?.enable || !wxworkConfig?.contact_url) {
return config.useOfficial === false; console.warn('企业微信配置不完整,使用原生客服');
uni.showToast({ title: '企业微信配置不完整', icon: 'none' });
} }
// #endif // #endif
return true;
// #ifdef H5
if (!wxworkConfig?.contact_url && !platformConfig.wxwork_url) {
console.error('企业微信链接未配置');
uni.showToast({ title: '企业微信链接未配置', icon: 'none' });
return;
}
// #endif
}
// 直接调用统一处理方法,由 CustomerService 内部根据配置路由
try {
this.handleCustomerClick({
sendMessageTitle: options.sendMessageTitle || '来自悬浮按钮的咨询',
sendMessagePath: options.sendMessagePath || '/pages/index/index'
});
} catch (error) {
console.error('客服处理失败:', error);
uni.showToast({ title: '打开客服失败', icon: 'none' });
}
} }
} }
/** /**
* 创建客服服务实例 * 创建客服服务实例
* @param {Object} vueInstance Vue实例 * @param {Object} vueInstance Vue 实例(通常是 this)
* @param {Object} externalConfig 外部最新配置 * @param {Object} externalConfig 可选:外部传入的最新配置(如从 DIY 数据中提取)
* @returns {CustomerService} 客服服务实例 * @returns {CustomerService}
*/ */
export function createCustomerService(vueInstance, externalConfig = {}) { export function createCustomerService(vueInstance, externalConfig = null) {
return new CustomerService(vueInstance, externalConfig); return new CustomerService(vueInstance, externalConfig);
} }

View File

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

View File

@@ -54,6 +54,11 @@ function loadLangPackSync(lang, path) {
} }
} }
function getCurrentLocale() {
return uni.getStorageSync('lang') || "zh-cn";
}
export default { export default {
langList: langConfig.langList, langList: langConfig.langList,
@@ -61,9 +66,7 @@ export default {
* 获得当前本地语言 * 获得当前本地语言
* @returns * @returns
*/ */
getCurrentLocale() { getCurrentLocale,
return uni.getStorageSync('lang') || "zh-cn";
},
/** /**
* * 解析多语言 * * 解析多语言
@@ -73,7 +76,7 @@ export default {
let _page = getCurrentPages()[getCurrentPages().length - 1]; let _page = getCurrentPages()[getCurrentPages().length - 1];
if (!_page) return; if (!_page) return;
const locale = this.getCurrentLocale(); // 获得当前本地语言 const locale = getCurrentLocale(); // 获得当前本地语言
let value = ''; // 存放解析后的语言值 let value = ''; // 存放解析后的语言值
let langPath = ''; // 存放当前页面语言包路径 let langPath = ''; // 存放当前页面语言包路径
@@ -141,7 +144,7 @@ export default {
if (!_page) return; if (!_page) return;
uni.setStorageSync("lang", value); uni.setStorageSync("lang", value);
const locale = this.getCurrentLocale(); const locale = getCurrentLocale();
// 清空已加载的语言包缓存 // 清空已加载的语言包缓存
for (let key in loadedLangPacks) { for (let key in loadedLangPacks) {
@@ -161,7 +164,7 @@ export default {
let _page = getCurrentPages()[getCurrentPages().length - 1]; let _page = getCurrentPages()[getCurrentPages().length - 1];
if (!_page) return; if (!_page) return;
const locale = this.getCurrentLocale(); const locale = getCurrentLocale();
this.title(this.lang("title")); this.title(this.lang("title"));

View File

@@ -14,7 +14,7 @@
</view> </view>
<view class="video-info"> <view class="video-info">
<!-- 视频标题,支持多行显示控制 --> <!-- 视频标题,支持多行显示控制 -->
<view class="video-title" :style="{ '--title-line-clamp': titleLineClamp }">{{ value.videoTitle }} <view class="video-title" :style="[{ '--title-line-clamp': titleLineClamp }]">{{ value.videoTitle }}
</view> </view>
<!-- 视频统计信息 --> <!-- 视频统计信息 -->
<view class="video-stats" v-if="showViewCount && value.showViewCount">{{ value.viewCount }}次观看</view> <view class="video-stats" v-if="showViewCount && value.showViewCount">{{ value.viewCount }}次观看</view>
@@ -34,7 +34,7 @@
</view> </view>
<view class="video-info"> <view class="video-info">
<!-- 视频标题,支持多行显示控制 --> <!-- 视频标题,支持多行显示控制 -->
<view class="video-title" :style="{ '--title-line-clamp': titleLineClamp }">{{ value.videoTitle }} <view class="video-title" :style="[{ '--title-line-clamp': titleLineClamp }]">{{ value.videoTitle }}
</view> </view>
<!-- 视频统计信息,非列表模式下显示 --> <!-- 视频统计信息,非列表模式下显示 -->
<view class="video-stats" v-if="showViewCount && value.showViewCount && !listMode">{{ value.viewCount }}次观看</view> <view class="video-stats" v-if="showViewCount && value.showViewCount && !listMode">{{ value.viewCount }}次观看</view>

View File

@@ -317,7 +317,12 @@ export default {
this.diyGlobalData.value.forEach((item, index) => { this.diyGlobalData.value.forEach((item, index) => {
item.pageStyle = ''; item.pageStyle = '';
// 给每个组件增加位置属性,用于定位,搜索、分类导航等定位 // 给每个组件增加位置属性,用于定位,搜索、分类导航等定位
// 如果是Tab组件内部的子组件,添加tabIndex前缀避免moduleIndex冲突
if (this.diyData.tabIndex !== undefined) {
item.moduleIndex = `${this.diyData.tabIndex}-${index + 1}`;
} else {
item.moduleIndex = index + 1; item.moduleIndex = index + 1;
}
// 特殊处理搜索框 当显示位置为滚动至顶部固定时,只设置背景颜色 // 特殊处理搜索框 当显示位置为滚动至顶部固定时,只设置背景颜色
if (item.componentName == 'Search' && item.positionWay == 'fixed') { if (item.componentName == 'Search' && item.positionWay == 'fixed') {

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,43 +1,71 @@
<template> <template>
<!-- 悬浮按钮 --> <view v-if="pageCount === 1 || need" class="fixed-box"
<view v-if="pageCount == 1 || need" class="fixed-box" :style="[customContainerStyle, { :style="[customContainerStyle, {
height: fixBtnShow ? '400rpx' : '320rpx', height: containerHeight,
backgroundImage: bgUrl ? `url(${bgUrl})` : '', backgroundImage: bgUrl ? `url(${bgUrl})` : '',
backgroundSize: 'cover' backgroundSize: 'cover'
}]"> }]">
<!-- 中英文切换按钮 --> <!-- 中英文切换按钮 -->
<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> <text>{{ currentLangDisplayName }}</text>
</view> </view>
<!-- 微信官方客服按钮-->
<button
v-if="fixBtnShow && hasValidKefuConfig && isWeappOfficialKefu"
class="btn-item common-bg"
open-type="contact"
:style="{ backgroundImage: currentKefuImg ? `url(${currentKefuImg})` : '', backgroundSize: '100% 100%' }"
>
<text class="icox icox-kefu" v-if="!kefuimg"></text>
</button>
<!-- AI 智能助手 --> <!-- 其他类型客服按钮(智能客服、企业微信等)-->
<view v-if="fixBtnShow && enableAIChat" class="btn-item common-bg" @click="openAIChat" <view
:style="{ backgroundImage: aiAgentimg ? `url(${aiAgentimg})` : '', backgroundSize: '100% 100%' }"> v-if="fixBtnShow && hasValidKefuConfig && !isWeappOfficialKefu && !showWeappSystemKefu"
<text class="ai-icon" v-if="!aiAgentimg">🤖</text> class="btn-item common-bg"
@click="handleUnifiedKefuClick"
:style="{ backgroundImage: currentKefuImg ? `url(${currentKefuImg})` : '', backgroundSize: '100% 100%' }"
>
<text class="ai-icon" v-if="isAIKefu && !currentKefuImg">🤖</text>
<text class="ai-icon" v-if="isWxworkKefu && !currentKefuImg">💬</text>
<text class="ai-icon" v-if="!isAIKefu && !isWxworkKefu && !currentKefuImg">👤</text>
</view> </view>
<!-- 微信小程序客服按钮 --> <!-- 智能客服按钮(当同时显示小程序系统客服时)-->
<!-- #ifdef MP-WEIXIN --> <view
<button class="btn-item common-bg" hoverClass="none" openType="contact" sessionFrom="weapp" showMessageCard="true" v-if="fixBtnShow && hasValidKefuConfig && !isWeappOfficialKefu && showWeappSystemKefu"
:style="[{ backgroundImage: kefuimg ? `url(${kefuimg})` : '', backgroundSize: '100% 100%' }, customButtonStyle]"> class="btn-item common-bg"
@click="handleUnifiedKefuClick"
:style="{ backgroundImage: currentKefuImg ? `url(${currentKefuImg})` : '', backgroundSize: '100% 100%' }"
>
<text class="ai-icon" v-if="isAIKefu && !currentKefuImg">🤖</text>
<text class="ai-icon" v-if="isWxworkKefu && !currentKefuImg">💬</text>
<text class="ai-icon" v-if="!isAIKefu && !isWxworkKefu && !currentKefuImg">👤</text>
</view>
<!-- 小程序系统客服按钮 -->
<button
v-if="fixBtnShow && hasValidKefuConfig && showWeappSystemKefu"
class="btn-item common-bg"
open-type="contact"
:style="{ backgroundImage: currentKefuImg ? `url(${currentKefuImg})` : '', backgroundSize: '100% 100%' }"
>
<text class="icox icox-kefu" v-if="!kefuimg"></text> <text class="icox icox-kefu" v-if="!kefuimg"></text>
</button> </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> <text class="iconfont icon-dianhua" v-if="!phoneimg"></text>
</view> </view>
@@ -56,16 +84,13 @@ export default {
return { return {
pageCount: 0, pageCount: 0,
fixBtnShow: true, fixBtnShow: true,
shopInfo: null, shopInfo: null,
currentLangIndex: 0, currentLangIndex: 0,
langIndexMap: {}, langIndexMap: {},
customerService: null, customerService: null,
}; };
}, },
computed: { computed: {
// 安全读取 shopInfo 中的字段,避免 undefined 报错
bgUrl() { bgUrl() {
return this.shopInfo?.bgUrl || ''; return this.shopInfo?.bgUrl || '';
}, },
@@ -84,9 +109,6 @@ export default {
isLanguageSwitchEnabled() { isLanguageSwitchEnabled() {
return !!this.shopInfo?.ischina; return !!this.shopInfo?.ischina;
}, },
enableAIChat() {
return !!this.shopInfo?.enableAIChat;
},
currentLangDisplayName() { currentLangDisplayName() {
const lang = this.langIndexMap[this.currentLangIndex]; const lang = this.langIndexMap[this.currentLangIndex];
return lang === 'zh-cn' ? 'EN' : 'CN'; return lang === 'zh-cn' ? 'EN' : 'CN';
@@ -96,23 +118,77 @@ export default {
}, },
customButtonStyle() { customButtonStyle() {
return this.shopInfo?.floatingButton?.button || {}; return this.shopInfo?.floatingButton?.button || {};
},
unreadCount() {
const customerService = createCustomerService(this);
return customerService.getUnreadCount();
},
currentKefuImg() {
const customerService = createCustomerService(this);
return customerService.getKefuButtonIcon();
},
isWeappOfficialKefu() {
const customerService = createCustomerService(this);
return customerService.isWeappOfficialKefu();
},
isAIKefu() {
const customerService = createCustomerService(this);
return customerService.isAIKefu();
},
isWxworkKefu() {
const customerService = createCustomerService(this);
return customerService.isWxworkKefu();
},
isThirdKefu() {
const customerService = createCustomerService(this);
return customerService.isThirdKefu();
},
showWeappSystemKefu() {
const customerService = createCustomerService(this);
return customerService.shouldShowExtraWeappSystemKefu();
},
hasValidKefuConfig() {
const customerService = createCustomerService(this);
return customerService.hasValidKefuConfig();
},
//根据显示的按钮数量动态计算容器高度
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() { created() {
this.customerService = createCustomerService(this);
this.initLanguage(); this.initLanguage();
this.pageCount = getCurrentPages().length;
uni.getStorage({ uni.getStorage({
key: 'shopInfo', key: 'shopInfo',
success: (e) => { success: (e) => {
console.log('【调试】当前 shopInfo:', e.data);
this.shopInfo = e.data; this.shopInfo = e.data;
},
fail: () => {
console.warn('未获取到 shopInfo,使用默认设置');
} }
}); });
// 初始化 customerService 实例
this.customerService = createCustomerService(this);
}, },
methods: { methods: {
/**
* 初始化多语言配置
*/
initLanguage() { initLanguage() {
this.langList = this.$langConfig.list(); this.langList = this.$langConfig.list();
this.langIndexMap = {}; this.langIndexMap = {};
@@ -132,36 +208,30 @@ export default {
} }
}, },
/**
* 电话联系客服
*/
call() { call() {
this.customerService.makePhoneCall(this.tel); this.customerService.makePhoneCallByNumber(this.tel);
}, },
/**
* 切换中英文语言,并刷新当前页面(保留所有参数)
*/
toggleLanguage() { toggleLanguage() {
this.currentLangIndex = this.currentLangIndex === 0 ? 1 : 0; this.currentLangIndex = this.currentLangIndex === 0 ? 1 : 0;
const targetLang = this.langIndexMap[this.currentLangIndex]; const targetLang = this.langIndexMap[this.currentLangIndex];
// 调用语言切换逻辑(设置 storage + 清空缓存) uni.setStorageSync("lang", targetLang);
this.$langConfig.change(targetLang);
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
const currentRoute = currentPage.route;
uni.reLaunch({
url: '/' + currentRoute
});
}, },
/** handleUnifiedKefuClick() {
* 打开 AI 智能助手 this.customerService.handleUnifiedKefuClick({
*/ sendMessageTitle: '来自悬浮按钮的咨询',
openAIChat() { sendMessagePath: '/pages/index/index'
this.$util.redirectTo(this.$util.AI_CHAT_PAGE_URL); });
},
/**
* 打开客服选择对话框
*/
openCustomerSelectPopup() {
this.customerService.openCustomerSelectPopupDialog();
} }
} }
} }
@@ -196,17 +266,15 @@ export default {
margin: 14rpx 0; margin: 14rpx 0;
transition: 0.1s; transition: 0.1s;
color: var(--hover-nav-text-color); color: var(--hover-nav-text-color);
border-radius: 40rpx; border-radius: 50%;
width: 80rpx; width: 80rpx;
height: 80rpx; height: 80rpx;
padding: 0; padding: 0;
overflow: hidden; overflow: hidden;
} }
/* 定义共同的背景颜色 */
.common-bg { .common-bg {
background-color: var(--hover-nav-bg-color); background-color: var(--hover-nav-bg-color);
/* 使用变量以保持一致性 */
} }
.btn-item text { .btn-item text {
@@ -236,8 +304,6 @@ export default {
font-weight: bold; font-weight: bold;
} }
.ai-icon { .ai-icon {
font-size: 40rpx; font-size: 40rpx;
width: 100%; width: 100%;

View File

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

View File

@@ -17,10 +17,6 @@
}}</view> }}</view>
</view> </view>
<!-- <view class="page-header" v-if="diyData.global && diyData.global.navBarSwitch" :style="{ backgroundImage: bgImg }">
<ns-navbar :title-color="textNavColor" :data="diyData.global" :scrollTop="scrollTop" :isBack="false"/>
</view> -->
<diy-index-page v-if="topIndexValue" ref="indexPage" :value="topIndexValue" :bgUrl="bgUrl" <diy-index-page v-if="topIndexValue" ref="indexPage" :value="topIndexValue" :bgUrl="bgUrl"
:scrollTop="scrollTop" :diyGlobal="diyData.global" class="diy-index-page"> :scrollTop="scrollTop" :diyGlobal="diyData.global" class="diy-index-page">
<template v-slot:components> <template v-slot:components>
@@ -43,35 +39,17 @@
<view @touchmove.prevent.stop> <view @touchmove.prevent.stop>
<uni-popup ref="uniPopupWindow" type="center" class="wap-floating" :maskClick="false"> <uni-popup ref="uniPopupWindow" type="center" class="wap-floating" :maskClick="false">
<view class="small-bot"> <view class="small-bot">
<!-- <view class="adver-time" wx:if="{{startadv.params.style=='default'&&startadv.params.canclose==1}}">
<view bindtap="adverclose">跳过</view>
<view class="time">{{clock}}s</view>
</view>
<view class="adver-time" wx:if="{{startadv.params.style=='default'&&startadv.params.canclose==0}}">
<view class="time" style="line-height: 64rpx;">{{clock}}s</view>
</view> -->
<swiper autoplay="true" :circular="true" indicator-active-color="#fff" <swiper autoplay="true" :circular="true" indicator-active-color="#fff"
indicator-color="rgba(255,255,255,0.6)" :indicator-dots="true" interval="3000"> indicator-color="rgba(255,255,255,0.6)" :indicator-dots="true" interval="3000">
<swiper-item v-for="(item, index) in adv.list" :key="index"> <swiper-item v-for="(item, index) in adv.list" :key="index">
<image class="slide-image" @click="$util.diyRedirectTo(item.link)" height="100%" <image class="slide-image" @click="$util.diyRedirectTo(item.link)" height="100%"
:src="$util.img(item.imageUrl)" width="100%"></image> :src="$util.img(item.imageUrl)" width="100%"></image>
<!-- <image bindtap="adverclose" class="slide-image" height="100%" src="{{item.imgurl}}" width="100%" wx:if="{{item.click==1}}"></image> -->
</swiper-item> </swiper-item>
</swiper> </swiper>
<view bindtap="adverclose" class="small-bot-close" @click="closePopupWindow"> <view bindtap="adverclose" class="small-bot-close" @click="closePopupWindow">
<i class="iconfont icon-round-close" style="color:#fff"></i> <i class="iconfont icon-round-close" style="color:#fff"></i>
</view> </view>
</view> </view>
<!-- <view class="image-wrap">
<swiper class="swiper" style="width:100%;height: 1200rpx;" :autoplay="true" interval="3000" circular="true" :indicator-dots="true" indicator-color="#000" indicator-active-color="red">
<swiper-item class="swiper-item" v-for="(item, index) in adv.list" :key="index" v-if="item.imageUrl" @click="$util.diyRedirectTo(item.link)">
<view class="item">
<image :src="$util.img(item.imageUrl)" mode="aspectFit" :show-menu-by-longpress="true"/>
</view>
</swiper-item>
</swiper>
</view>
<text class="iconfont icon-round-close" @click="closePopupWindow"></text> -->
</uni-popup> </uni-popup>
</view> </view>
</template> </template>
@@ -101,7 +79,7 @@
</view> </view>
</uni-popup> </uni-popup>
<!-- 选择门店弹出框,定位当前位置,展示最近的一个门店 --> <!-- 选择门店弹出框 -->
<view @touchmove.prevent.stop class="choose-store"> <view @touchmove.prevent.stop class="choose-store">
<uni-popup ref="chooseStorePopup" type="center" :maskClick="false" class="choose-store"> <uni-popup ref="chooseStorePopup" type="center" :maskClick="false" class="choose-store">
<view class="choose-store-popup"> <view class="choose-store-popup">
@@ -136,8 +114,27 @@
</view> </view>
</uni-popup> </uni-popup>
</view> </view>
<hover-nav :need="true"></hover-nav> <hover-nav :need="true"></hover-nav>
<!-- 隐私协议 -->
<!-- 隐私协议弹窗 UI (新增) -->
<view v-if="showPrivacyModal" class="privacy-modal-mask" @touchmove.prevent>
<view class="privacy-modal-container">
<view class="modal-header">用户隐私保护提示</view>
<view class="modal-body">
<text>感谢您使用本应用,在使用前您应当阅读并同意</text>
<text class="link" @click="viewPrivacy('privacy')">《隐私条款》</text>
<text class="link" @click="viewPrivacy('agreement')">《用户服务协议》</text>
<text>,当点击同意并继续时,即表示您已理解并同意该条款内容,该条款将对您产生法律约束力;如您不同意,将无法继续使用相关功能。</text>
</view>
<view class="modal-footer">
<view class="btn btn-disagree" @click="handleDisagree">不同意</view>
<view class="btn btn-agree" @click="handleAgree">同意并继续</view>
</view>
</view>
</view>
<!-- 隐私协议组件 (原有) -->
<privacy-popup ref="privacyPopup"></privacy-popup> <privacy-popup ref="privacyPopup"></privacy-popup>
<to-top v-if="showTop" @toTop="scrollToTopNative()"></to-top> <to-top v-if="showTop" @toTop="scrollToTopNative()"></to-top>
<ns-login ref="login"></ns-login> <ns-login ref="login"></ns-login>
@@ -146,14 +143,132 @@
</template> </template>
<script> <script>
import diyJs from '@/common/js/diy.js'; import diyJs from '@/common/js/diy.js';
import scroll from '@/common/js/scroll-view.js'; import scroll from '@/common/js/scroll-view.js';
import indexJs from './public/js/index.js'; import indexJs from './public/js/index.js';
export default { export default {
mixins: [diyJs, scroll, indexJs] mixins: [diyJs, scroll, indexJs],
data() {
return {
minScrollTop: 100,
wechatQrcode: '',
diyRoute: '/pages/index/index',
showPrivacyModal: false // 新增:控制隐私弹窗显示
};
},
onLoad(option) {
// #ifdef H5
if (option.module && option.module == 'locationPicker') {
option.name = '';
}
// #endif
this.name = option.name || 'DIY_VIEW_INDEX';
},
onShow() {
this.getFollowQrcode();
// 页面显示时检查隐私设置
this.checkPrivacySetting();
},
mounted() {
this.unsubscribe = this.$eventBus?.onMany([
'rubik-cube-tap',
'picture-tap',
'text-tap',
'goods-list-tap',
'presale-tap',
'notes-tap',
'merch-list-tap',
'map-tap',
'img-ads-tap',
'many-goods-list-tap',
'search-tap',
], this._handleDiyGroupInteractionEvent);
},
beforeDestroy() {
if (this.unsubscribe) this.unsubscribe();
},
methods: {
// 检查隐私弹窗逻辑
checkPrivacySetting() {
// 模拟检查本地存储,看用户是否已经同意过
// 实际项目中请替换为你真实的存储逻辑
const hasAgreed = uni.getStorageSync('HAS_AGREED_PRIVACY');
if (!hasAgreed) {
// 延迟显示,防止遮挡页面渲染
setTimeout(() => {
this.showPrivacyModal = true;
}, 500);
}
},
// 用户点击同意
handleAgree() {
// 保存同意状态
uni.setStorageSync('HAS_AGREED_PRIVACY', true);
this.showPrivacyModal = false;
},
// 用户点击不同意
handleDisagree() {
// 微信小程序环境下,不同意通常退出小程序
// #ifdef MP-WEIXIN
uni.exitMiniProgram();
// #endif
// H5 或 App 下可以提示或直接不显示内容
// #ifndef MP-WEIXIN
uni.showToast({
title: '您已拒绝协议,无法使用',
icon: 'none',
duration: 2000
});
// 这里可以选择关闭页面或保持弹窗
// this.showPrivacyModal = false;
// #endif
},
// 查看具体条款(可选功能)
viewPrivacy(type) {
// 这里可以跳转到具体的条款页面,或者使用你现有的 privacy-popup 组件
// 假设你有现成的组件逻辑:
if (this.$refs.privacyPopup) {
// 假设 privacy-popup 组件有一个 show 方法
// this.$refs.privacyPopup.show(type);
}
uni.showToast({
title: '请在此处实现查看条款逻辑',
icon: 'none'
});
},
getFollowQrcode() {
if (!this.$util.isWeiXin()) return;
this.$api.sendRequest({
url: '/wechat/api/wechat/followqrcode',
success: res => {
if (res.code >= 0 && res.data) {
this.wechatQrcode = res.data.qrcode;
}
}
});
},
officialAccountsOpen() {
this.$refs.officialAccountsPopup.open();
},
officialAccountsClose() {
this.$refs.officialAccountsPopup.close();
},
_handleDiyGroupInteractionEvent(payload) {
// #ifdef H5
return true;
// #endif
if (this.storeToken) {
return true;
}
this.$refs.login?.open();
return false;
}
}
}; };
</script> </script>
@@ -199,6 +314,74 @@ export default {
.small-bot-close i { .small-bot-close i {
font-size: 60rpx; font-size: 60rpx;
} }
/* 新增:隐私弹窗样式 */
.privacy-modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.6); // 半透明背景
z-index: 9999; // 确保层级最高
display: flex;
justify-content: center;
align-items: center;
}
.privacy-modal-container {
width: 80%;
background-color: #ffffff;
border-radius: 12rpx;
padding: 40rpx 30rpx;
text-align: center;
}
.modal-header {
font-size: 36rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
}
.modal-body {
font-size: 28rpx;
color: #666;
line-height: 1.6;
text-align: left;
margin-bottom: 30rpx;
}
.link {
color: #007aff; // 蓝色链接
margin: 0 4rpx;
}
.modal-footer {
display: flex;
justify-content: space-between;
padding: 0 20rpx;
}
.btn {
flex: 1;
height: 80rpx;
line-height: 80rpx;
border-radius: 40rpx;
font-size: 30rpx;
text-align: center;
margin: 0 15rpx;
}
.btn-disagree {
background-color: #f5f5f5;
color: #333;
}
.btn-agree {
background-color: #07c160; // 微信绿
color: #fff;
}
</style> </style>
<style scoped> <style scoped>
.swiper ::v-deep .uni-swiper-dots-horizontal { .swiper ::v-deep .uni-swiper-dots-horizontal {

View File

@@ -3,7 +3,8 @@ export default {
return { return {
minScrollTop: 100, // 设置回到顶端按钮显示要求,最小页面滚动距离 minScrollTop: 100, // 设置回到顶端按钮显示要求,最小页面滚动距离
wechatQrcode: '', // 公众号二维码 wechatQrcode: '', // 公众号二维码
diyRoute: '/pages/index/index' diyRoute: '/pages/index/index',
showPrivacyModal: false // 新增:控制隐私弹窗显示的变量
}; };
}, },
onLoad(option) { onLoad(option) {
@@ -19,6 +20,9 @@ export default {
onShow() { onShow() {
// 获取关注公众号二维码 // 获取关注公众号二维码
this.getFollowQrcode(); this.getFollowQrcode();
// 打开隐私弹窗
this.openPrivacyModal();
}, },
mounted() { mounted() {
// 监听图片、文字、商品列表等组件的点击事件 // 监听图片、文字、商品列表等组件的点击事件
@@ -41,6 +45,39 @@ export default {
if (this.unsubscribe) this.unsubscribe(); if (this.unsubscribe) this.unsubscribe();
}, },
methods: { methods: {
// 检查隐私设置(保留原有逻辑,如果不需要可删除)
checkPrivacySetting() {
setTimeout(() => {
if (this.$refs.privacyPopup) {
this.$refs.privacyPopup.proactive();
}
}, 100);
},
// 新增:打开隐私弹窗的方法
openPrivacyModal() {
// 延迟一点执行,防止页面加载卡顿
setTimeout(() => {
this.showPrivacyModal = true;
}, 500);
},
// 新增:处理用户点击“同意并继续”
handlePrivacyAgree() {
this.showPrivacyModal = false;
// 这里可以添加保存到本地存储的逻辑,表示用户已同意
uni.setStorageSync('hasAgreedPrivacy', true);
},
// 新增:处理用户点击“不同意”
handlePrivacyDisagree() {
this.showPrivacyModal = false;
// 如果不同意,通常退出小程序或关闭页面
uni.navigateBack({
delta: 1
});
},
// 关注公众号 // 关注公众号
getFollowQrcode() { getFollowQrcode() {
if (!this.$util.isWeiXin()) return; if (!this.$util.isWeiXin()) return;
@@ -79,4 +116,4 @@ export default {
return false; return false;
} }
} }
} };

View File

@@ -1,7 +1,11 @@
<template> <template>
<view class="ai-chat-container"> <view class="ai-chat-container">
<!-- 聊天消息列表 --> <!-- 聊天消息列表 -->
<scroll-view class="chat-messages" scroll-y :scroll-top="scrollTop" @scroll="onScroll" <scroll-view
class="chat-messages"
scroll-y
:scroll-top="scrollTop"
@scroll="onScroll"
:scroll-with-animation="false"> :scroll-with-animation="false">
<!-- 加载更多历史消息 --> <!-- 加载更多历史消息 -->
@@ -10,7 +14,11 @@
</view> </view>
<!-- 消息列表 --> <!-- 消息列表 -->
<view v-for="item in messagesWithKey" :key="item.__renderKey" class="message-item" :class="[item.role, { 'first-message': item.__index === 0 }]"> <view
v-for="(message, index) in messages"
:key="`msg-${message.id || message.timestamp}-${index}`"
class="message-item"
:class="[message.role, { 'first-message': index === 0 }]">
<!-- 用户消息 --> <!-- 用户消息 -->
<view v-if="message.role === 'user'" class="user-message"> <view v-if="message.role === 'user'" class="user-message">
@@ -30,8 +38,8 @@
<!-- AI消息 --> <!-- AI消息 -->
<view v-else class="ai-message"> <view v-else class="ai-message">
<view class="avatar"> <view class="avatar">
<!-- 直接使用内部固定的AI头像 --> <!-- AI头像使用🤖 -->
<image :src="aiAvatar" mode="aspectFill" /> <text>🤖</text>
</view> </view>
<view class="message-content"> <view class="message-content">
<!-- AI昵称固定,不可修改 - 左移到气泡上方 --> <!-- AI昵称固定,不可修改 - 左移到气泡上方 -->
@@ -77,12 +85,21 @@
<text class="audio-duration">{{ formatDuration(message.duration) }}</text> <text class="audio-duration">{{ formatDuration(message.duration) }}</text>
</view> </view>
<view class="audio-controls"> <view class="audio-controls">
<button class="play-btn" :class="{ playing: message.playing }" @click="toggleAudio(message)"> <button
class="play-btn"
:class="{ playing: message.playing }"
@click="toggleAudio(message)">
<text class="iconfont" :class="message.playing ? 'icon-pause' : 'icon-play'"></text> <text class="iconfont" :class="message.playing ? 'icon-pause' : 'icon-play'"></text>
</button> </button>
<slider class="audio-slider" :value="message.currentTime || 0" :max="message.duration || 0" <slider
@changing="onAudioSliderChange" @change="onAudioSliderChangeEnd" activeColor="#8a9fb8" class="audio-slider"
backgroundColor="#e8edf3" block-size="12" /> :value="message.currentTime || 0"
:max="message.duration || 0"
@changing="onAudioSliderChange"
@change="onAudioSliderChangeEnd"
activeColor="#8a9fb8"
backgroundColor="#e8edf3"
block-size="12" />
</view> </view>
</view> </view>
</view> </view>
@@ -90,8 +107,14 @@
<!-- 视频消息 --> <!-- 视频消息 -->
<view v-else-if="message.type === 'video'" class="video-content"> <view v-else-if="message.type === 'video'" class="video-content">
<view class="video-player"> <view class="video-player">
<video :src="message.url" :poster="message.cover" :controls="true" :autoplay="false" <video
class="video-element" @play="onVideoPlay(message)" @pause="onVideoPause(message)"> :src="message.url"
:poster="message.cover"
:controls="true"
:autoplay="false"
class="video-element"
@play="onVideoPlay(message)"
@pause="onVideoPause(message)">
</video> </video>
<view class="video-info"> <view class="video-info">
<text class="video-title">{{ message.title || '视频消息' }}</text> <text class="video-title">{{ message.title || '视频消息' }}</text>
@@ -140,8 +163,12 @@
<!-- 操作按钮 --> <!-- 操作按钮 -->
<view class="message-actions" v-if="message.actions && message.actions.length > 0"> <view class="message-actions" v-if="message.actions && message.actions.length > 0">
<button v-for="action in message.actions" :key="action.id" class="action-btn" <button
:class="[`iconfont`, action.icon, action.type]" @click="handleAction(action, message)"> v-for="action in message.actions"
:key="action.id"
class="action-btn"
:class="[`iconfont`, action.icon, action.type]"
@click="handleAction(action, message)">
{{ action.text }} {{ action.text }}
</button> </button>
</view> </view>
@@ -156,27 +183,29 @@
<view class="input-area"> <view class="input-area">
<view class="input-tools"> <view class="input-tools">
<button class="tool-btn" @click="showMoreTools"> <button class="tool-btn" @click="showMoreTools">
<text class="iconfont icon-plus">📌</text> <text class="iconfont icon-plus">...</text>
</button> </button>
<button class="tool-btn" @click="toggleVoiceInput"> <button class="tool-btn" @click="toggleVoiceInput">
<text class="iconfont" :class="voiceInputing ? 'icon-stop' : 'icon-voice'">🎤</text> <text class="iconfont" :class="voiceInputing ? 'icon-stop' : 'icon-voice'">🎤</text>
</button> </button>
<!-- 更换用户头像按钮 -->
<button class="tool-btn" @click="openAvatarSelector">
<text class="iconfont icon-user">📷</text>
</button>
<!-- 新增:更换用户昵称按钮 -->
<button class="tool-btn" @click="openNicknameEditor">
<text class="iconfont icon-edit">✏️</text>
</button>
</view> </view>
<view class="input-container"> <view class="input-container">
<textarea v-model="inputText" class="message-input" placeholder="请输入您的问题..." :maxlength="500" <textarea
:auto-height="true" :show-confirm-bar="false" @confirm="sendMessage" @input="onInput" /> v-model="inputText"
class="message-input"
placeholder="请输入您的问题..."
:maxlength="500"
:auto-height="true"
:show-confirm-bar="false"
@confirm="sendMessage"
@input="onInput" />
<!-- 发送按钮 --> <!-- 发送按钮 -->
<button class="send-btn" :class="{ disabled: !inputText.trim() }" @click="sendMessage" <button
class="send-btn"
:class="{ disabled: !inputText.trim() }"
@click="sendMessage"
:disabled="!inputText.trim()"> :disabled="!inputText.trim()">
<text>发送</text> <text>发送</text>
</button> </button>
@@ -234,7 +263,11 @@
</view> </view>
<view class="voice-content"> <view class="voice-content">
<view class="voice-wave" :class="{ recording: voiceInputing }"> <view class="voice-wave" :class="{ recording: voiceInputing }">
<view v-for="i in 8" :key="i" class="wave-bar" :style="{ animationDelay: (i * 0.1) + 's' }"></view> <view
v-for="i in 8"
:key="i"
class="wave-bar"
:style="{ animationDelay: (i * 0.1) + 's' }"></view>
</view> </view>
<text class="voice-tip">{{ voiceInputing ? '正在录音,松开结束' : '点击开始录音' }}</text> <text class="voice-tip">{{ voiceInputing ? '正在录音,松开结束' : '点击开始录音' }}</text>
</view> </view>
@@ -290,15 +323,23 @@
</view> </view>
<view class="nickname-editor-content"> <view class="nickname-editor-content">
<view class="nickname-input-container"> <view class="nickname-input-container">
<input v-model="tempNickname" class="nickname-input" placeholder="请输入您的新昵称" :maxlength="12" type="text" /> <input
v-model="tempNickname"
class="nickname-input"
placeholder="请输入您的新昵称"
:maxlength="12"
type="text" />
</view> </view>
<view class="nickname-tip">昵称长度限制1-12个字符,仅自己可见</view> <view class="nickname-tip">昵称长度限制1-12个字符,仅自己可见</view>
<view class="nickname-editor-actions"> <view class="nickname-editor-actions">
<button class="nickname-action-btn cancel" @click="closeNicknameEditor"> <button class="nickname-action-btn cancel" @click="closeNicknameEditor">
<text>取消</text> <text>取消</text>
</button> </button>
<button class="nickname-action-btn confirm" :class="{ disabled: !tempNickname.trim() }" <button
:disabled="!tempNickname.trim()" @click="saveNickname"> class="nickname-action-btn confirm"
:class="{ disabled: !tempNickname.trim() }"
:disabled="!tempNickname.trim()"
@click="saveNickname">
<text>保存</text> <text>保存</text>
</button> </button>
</view> </view>
@@ -391,20 +432,15 @@ export default {
isFetchingHistory: false isFetchingHistory: false
} }
}, },
computed: {
// 为每条消息生成兼容小程序的唯一 key
messagesWithKey() {
return this.messages.map((msg, idx) => {
const uniqueId = msg.id || msg.timestamp || idx;
return {
...msg,
__renderKey: `msg_${uniqueId}_${idx}`,
__index: idx // 保留原始索引,用于判断 first-message 等
};
});
}
},
onShow() { onShow() {
// 如果不是 AI 客服,立即退出并跳转
if (customerServiceType !== 'ai') {
uni.showToast({ title: '当前客服类型不支持此页面', icon: 'none' });
setTimeout(() => {
uni.navigateBack({ delta: 1 }); // 或 redirectTo 首页
}, 1000);
return; // ⚠️ 关键:阻止后续初始化
}
// 优先读取本地缓存的会话 ID // 优先读取本地缓存的会话 ID
const localConvId = this.getConversationIdFromLocal(); const localConvId = this.getConversationIdFromLocal();
if (localConvId) { if (localConvId) {
@@ -774,6 +810,7 @@ export default {
}, },
// 发送流式消息 // 发送流式消息
// 发送流式消息(自动适配 H5 / 微信小程序)
async sendStreamMessage(userMessage) { async sendStreamMessage(userMessage) {
// 创建流式消息对象 // 创建流式消息对象
const streamMessage = { const streamMessage = {
@@ -783,35 +820,36 @@ export default {
content: '', content: '',
timestamp: Date.now(), timestamp: Date.now(),
isStreaming: true isStreaming: true
} };
// 移除加载状态,添加流式消息 // 移除加载状态,添加流式消息
this.messages = this.messages.filter(msg => msg.type !== 'loading') this.messages = this.messages.filter(msg => msg.type !== 'loading');
this.shouldScrollToBottom = true this.shouldScrollToBottom = true;
this.messages.push(streamMessage) this.messages.push(streamMessage);
// 开始流式响应 try {
await aiService.sendStreamMessage( // #ifdef H5
// ===== H5: 使用 POST + 流式 (fetch readable stream) =====
await aiService.sendHttpStream(
userMessage, userMessage,
// 流式数据回调
(chunk) => { (chunk) => {
streamMessage.content += chunk // 实时更新内容
this.$forceUpdate() // 或 this.$nextTick() streamMessage.content += chunk;
this.$forceUpdate(); // 强制更新视图
}, },
// 完成回调:处理对象或字符串
(completeResult) => { (completeResult) => {
// 流结束回调
let finalContent = ''; let finalContent = '';
let convId = ''; let convId = '';
// 判断是对象还是字符串
if (typeof completeResult === 'string') { if (typeof completeResult === 'string') {
finalContent = completeResult; finalContent = completeResult;
} else { } else {
finalContent = completeResult.content || ''; finalContent = completeResult.content || '';
convId = completeResult.conversation_id || ''; // 👈 关键:提取 conversation_id convId = completeResult.conversation_id || '';
} }
// 更新消息状态 // 更新最终内容
streamMessage.isStreaming = false; streamMessage.isStreaming = false;
streamMessage.content = finalContent; streamMessage.content = finalContent;
@@ -821,17 +859,71 @@ export default {
{ id: 2, text: '没帮助', type: 'dislike' } { id: 2, text: '没帮助', type: 'dislike' }
]; ];
// 保存 conversation_id 到本地 // 保存会话 ID
if (convId) { if (convId) {
this.currentConversationId = convId;
aiService.setConversationId(convId); aiService.setConversationId(convId);
this.saveConversationIdToLocal(convId);
} }
// 触发事件 // 触发事件
this.$emit('ai-response', streamMessage); this.$emit('ai-response', streamMessage);
this.saveConversationIdToLocal(convId);
this.currentConversationId = convId;
} }
); );
// #endif
// #ifdef MP-WEIXIN
// ===== 微信小程序: 使用 WebSocket =====
await aiService.sendStreamMessage(
userMessage,
(chunk) => {
// 实时更新内容
streamMessage.content += chunk;
this.$forceUpdate();
},
(completeResult) => {
// 流结束回调
let finalContent = completeResult?.content || '';
let convId = completeResult?.conversation_id || '';
// 更新最终内容
streamMessage.isStreaming = false;
streamMessage.content = finalContent;
// 添加操作按钮
streamMessage.actions = [
{ id: 1, text: '有帮助', type: 'like' },
{ id: 2, text: '没帮助', type: 'dislike' }
];
// 保存会话 ID
if (convId) {
this.currentConversationId = convId;
aiService.setConversationId(convId);
this.saveConversationIdToLocal(convId);
}
// 触发事件
this.$emit('ai-response', streamMessage);
}
);
// #endif
} catch (error) {
console.error('流式请求失败:', error);
// 移除流式消息
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);
}
}, },
generateAIResponse(userMessage) { generateAIResponse(userMessage) {
const responses = { const responses = {
@@ -1352,7 +1444,7 @@ $radius-lg: 36rpx;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
/* 仅新增:科技感粉蓝渐变背景 + 流动动画 */ /* 仅新增:科技感粉蓝渐变背景 + 流动动画 */
background: linear-gradient(135deg, #fde6f7 0%, #f8b7e8 20%, #c4e0ff 50%, #8cb4ff 80%, #ffffff 100%); background: white; /* 白色 */
background-size: 200% 200%; background-size: 200% 200%;
animation: gradient-flow 15s ease infinite; animation: gradient-flow 15s ease infinite;
/* 原有样式保持不变 */ /* 原有样式保持不变 */
@@ -1366,8 +1458,7 @@ $radius-lg: 36rpx;
padding: 24rpx 32rpx; padding: 24rpx 32rpx;
overflow-y: auto; overflow-y: auto;
box-sizing: border-box; box-sizing: border-box;
min-height: 0; min-height: 0; /* 重要:防止flex元素溢出 */
/* 重要:防止flex元素溢出 */
.load-more { .load-more {
text-align: center; text-align: center;
@@ -1382,8 +1473,7 @@ $radius-lg: 36rpx;
margin-top: 24rpx; margin-top: 24rpx;
} }
.user-message, .user-message, .ai-message {
.ai-message {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
@@ -1397,6 +1487,9 @@ $radius-lg: 36rpx;
background-color: $bg-card; background-color: $bg-card;
border: 2rpx solid #f0f4f8; border: 2rpx solid #f0f4f8;
transition: transform 0.2s ease; transition: transform 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
&:hover { &:hover {
transform: scale(1.03); transform: scale(1.03);
@@ -1407,6 +1500,11 @@ $radius-lg: 36rpx;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
} }
text {
font-size: 56rpx;
line-height: 1;
}
} }
.message-content { .message-content {
@@ -1414,25 +1512,7 @@ $radius-lg: 36rpx;
margin-left: 24rpx; margin-left: 24rpx;
max-width: calc(100% - 112rpx); max-width: calc(100% - 112rpx);
min-width: 0; min-width: 0;
width: 0; width: 0; /* 添加这个属性防止flex元素溢出 */
/* 添加这个属性防止flex元素溢出 */
.message-nickname {
font-size: 24rpx;
color: $color-text-light;
margin-bottom: 8rpx;
letter-spacing: 0.5rpx;
text-align: right;
}
// AI昵称专属样式 - 左移到气泡上方
.ai-nickname {
text-align: left;
margin-left: 0;
padding-left: 0;
align-self: flex-start;
margin-bottom: 4rpx;
}
.message-bubble { .message-bubble {
padding: 24rpx 32rpx; padding: 24rpx 32rpx;
@@ -1853,6 +1933,28 @@ $radius-lg: 36rpx;
} }
} }
/* ========== 关键修复:拆分 .message-nickname 样式 ========== */
.message-nickname {
font-size: 24rpx;
color: $color-text-light;
margin-bottom: 8rpx;
letter-spacing: 0.5rpx;
}
/* 用户昵称:右对齐 */
.user-message .message-nickname {
text-align: right;
}
/* AI 昵称:使用 .ai-nickname 类控制 */
.ai-nickname {
text-align: left;
margin-left: 0;
padding-left: 0;
align-self: flex-start;
margin-bottom: 4rpx;
}
/* 用户消息特有样式 */ /* 用户消息特有样式 */
.user-message { .user-message {
flex-direction: row-reverse; flex-direction: row-reverse;
@@ -1863,27 +1965,20 @@ $radius-lg: 36rpx;
text-align: right; text-align: right;
.message-bubble { .message-bubble {
background: linear-gradient(135deg, #ffadd2, #f783ac) !important; background: #c4e0ff !important; /* 浅蓝色 */
/* 粉色渐变 */ color: black !important;
color: white !important; border-radius: 16rpx 16rpx 4rpx 16rpx; /* 右对齐气泡尖角适配 */
border-radius: 16rpx 16rpx 4rpx 16rpx; box-shadow: 0 8rpx 20rpx rgba(196, 224, 255, 0.3) !important;
/* 右对齐气泡尖角适配 */
box-shadow: 0 8rpx 20rpx rgba(247, 131, 172, 0.3) !important;
border: none !important; border: none !important;
/* ✅ 关键:允许内容撑开高度 */ /* ✅ 关键:允许内容撑开高度 */
min-height: auto; min-height: auto;
height: auto; height: auto;
padding: 24rpx 32rpx; padding: 24rpx 32rpx; /* 保留内边距 */
/* 保留内边距 */ display: inline-block; /* 让宽度也随内容收缩(可选) */
display: inline-block; max-width: 80%; /* 防止过宽 */
/* 让宽度也随内容收缩(可选) */
max-width: 80%;
/* 防止过宽 */
word-break: break-word; word-break: break-word;
white-space: pre-wrap; white-space: pre-wrap; /* 保留用户输入的换行符 */
/* 保留用户输入的换行符 */
line-height: 1.6; line-height: 1.6;
&::before { &::before {
content: ''; content: '';
position: absolute; position: absolute;
@@ -1891,8 +1986,7 @@ $radius-lg: 36rpx;
right: -14rpx; right: -14rpx;
width: 24rpx; width: 24rpx;
height: 24rpx; height: 24rpx;
background: #ffffff !important; background: #ffffff !important; /* 菱形改为白色 */
/* 菱形改为白色 */
border-radius: 6rpx; border-radius: 6rpx;
transform: rotate(45deg); transform: rotate(45deg);
box-shadow: 4rpx -4rpx 4rpx rgba(0, 0, 0, 0.05); box-shadow: 4rpx -4rpx 4rpx rgba(0, 0, 0, 0.05);
@@ -1905,9 +1999,11 @@ $radius-lg: 36rpx;
left: -100%; left: -100%;
width: 50%; width: 50%;
height: 100%; height: 100%;
background: linear-gradient(to right, background: linear-gradient(
to right,
rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.15) 100%); rgba(255, 255, 255, 0.15) 100%
);
animation: shine 4s infinite linear; animation: shine 4s infinite linear;
} }
} }
@@ -1923,13 +2019,11 @@ $radius-lg: 36rpx;
align-items: flex-start; align-items: flex-start;
.message-bubble { .message-bubble {
background: linear-gradient(135deg, #dbeafe, #bfdbfe) !important; background: white !important; /* 白色 */
/* 蓝色浅渐变 */ color: black !important;
color: #2c3e50 !important; border-radius: 16rpx 16rpx 16rpx 4rpx; /* 左对齐气泡尖角适配 */
border-radius: 16rpx 16rpx 16rpx 4rpx; box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.1) !important;
/* 左对齐气泡尖角适配 */ border: 1rpx solid #e0e0e0 !important;
box-shadow: 0 8rpx 20rpx rgba(191, 219, 254, 0.4) !important;
border: 1rpx solid #dbeafe !important;
min-height: auto; min-height: auto;
height: auto; height: auto;
padding: 24rpx 32rpx; padding: 24rpx 32rpx;
@@ -1938,7 +2032,6 @@ $radius-lg: 36rpx;
word-break: break-word; word-break: break-word;
white-space: pre-wrap; white-space: pre-wrap;
line-height: 1.6; line-height: 1.6;
&::before { &::before {
content: ''; content: '';
position: absolute; position: absolute;
@@ -1946,8 +2039,7 @@ $radius-lg: 36rpx;
left: -14rpx; left: -14rpx;
width: 24rpx; width: 24rpx;
height: 24rpx; height: 24rpx;
background: #ffffff !important; background: #ffffff !important; /* 菱形改为白色 */
/* 菱形改为白色 */
border-radius: 6rpx; border-radius: 6rpx;
transform: rotate(45deg); transform: rotate(45deg);
box-shadow: -4rpx 4rpx 4rpx rgba(0, 0, 0, 0.05); box-shadow: -4rpx 4rpx 4rpx rgba(0, 0, 0, 0.05);
@@ -1963,7 +2055,7 @@ $radius-lg: 36rpx;
/* 输入区域 */ /* 输入区域 */
.input-area { .input-area {
background: linear-gradient(135deg, #fde6f7 0%, #f8b7e8 20%, #c4e0ff 50%, #8cb4ff 80%, #ffffff 100%); background: #c4e0ff; /* 浅蓝色 */
border-top: 2rpx solid #f0f4f8; border-top: 2rpx solid #f0f4f8;
padding: 24rpx 32rpx; padding: 24rpx 32rpx;
/* 确保在微信小程序中紧贴底部 */ /* 确保在微信小程序中紧贴底部 */
@@ -1978,6 +2070,7 @@ $radius-lg: 36rpx;
.input-tools { .input-tools {
display: flex; display: flex;
margin-bottom: 16rpx; margin-bottom: 16rpx;
align-items: flex-start;
.tool-btn { .tool-btn {
width: 64rpx; width: 64rpx;
@@ -1985,6 +2078,7 @@ $radius-lg: 36rpx;
border-radius: 50%; border-radius: 50%;
background-color: $bg-main; background-color: $bg-main;
margin-right: 16rpx; margin-right: 16rpx;
margin-top: -8rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -2047,12 +2141,9 @@ $radius-lg: 36rpx;
font-size: 28rpx; font-size: 28rpx;
font-weight: 500; font-weight: 500;
border: none; border: none;
text-align: center; text-align: center; /* 兼容多端文字居中 */
/* 兼容多端文字居中 */ white-space: nowrap; /* 强制文字单行横向显示 */
white-space: nowrap; line-height: 1; /* 重置行高,避免文字垂直偏移 */
/* 强制文字单行横向显示 */
line-height: 1;
/* 重置行高,避免文字垂直偏移 */
top: -10px; top: -10px;
&.disabled { &.disabled {
@@ -2076,10 +2167,8 @@ $radius-lg: 36rpx;
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
pointer-events: none; pointer-events: none; /* 让鼠标事件穿透容器,不影响主内容交互 */
/* 让鼠标事件穿透容器,不影响主内容交互 */ z-index: 999; /* 基础弹窗层级 */
z-index: 999;
/* 基础弹窗层级 */
/* 子弹窗需要开启 pointer-events,否则点击无效 */ /* 子弹窗需要开启 pointer-events,否则点击无效 */
> .tools-popup, > .tools-popup,
@@ -2450,45 +2539,14 @@ $radius-lg: 36rpx;
border-radius: 4rpx; border-radius: 4rpx;
animation: none; animation: none;
&:nth-child(1) { &:nth-child(1) { height: 60rpx; animation-delay: 0s; }
height: 60rpx; &:nth-child(2) { height: 90rpx; animation-delay: 0.1s; }
animation-delay: 0s; &:nth-child(3) { height: 50rpx; animation-delay: 0.2s; }
} &:nth-child(4) { height: 120rpx; animation-delay: 0.3s; }
&:nth-child(5) { height: 70rpx; animation-delay: 0.4s; }
&:nth-child(2) { &:nth-child(6) { height: 100rpx; animation-delay: 0.5s; }
height: 90rpx; &:nth-child(7) { height: 80rpx; animation-delay: 0.6s; }
animation-delay: 0.1s; &:nth-child(8) { height: 40rpx; animation-delay: 0.7s; }
}
&:nth-child(3) {
height: 50rpx;
animation-delay: 0.2s;
}
&:nth-child(4) {
height: 120rpx;
animation-delay: 0.3s;
}
&:nth-child(5) {
height: 70rpx;
animation-delay: 0.4s;
}
&:nth-child(6) {
height: 100rpx;
animation-delay: 0.5s;
}
&:nth-child(7) {
height: 80rpx;
animation-delay: 0.6s;
}
&:nth-child(8) {
height: 40rpx;
animation-delay: 0.7s;
}
} }
} }
@@ -2656,13 +2714,10 @@ $radius-lg: 36rpx;
/* 动画定义 */ /* 动画定义 */
@keyframes dotPulse { @keyframes dotPulse {
0%, 100% {
0%,
100% {
opacity: 0.4; opacity: 0.4;
transform: scale(0.8); transform: scale(0.8);
} }
50% { 50% {
opacity: 1; opacity: 1;
transform: scale(1.2); transform: scale(1.2);
@@ -2670,13 +2725,10 @@ $radius-lg: 36rpx;
} }
@keyframes wave { @keyframes wave {
0%, 100% {
0%,
100% {
transform: scaleY(0.5); transform: scaleY(0.5);
opacity: 0.7; opacity: 0.7;
} }
50% { 50% {
transform: scaleY(1); transform: scaleY(1);
opacity: 1; opacity: 1;
@@ -2687,7 +2739,6 @@ $radius-lg: 36rpx;
0% { 0% {
left: -100%; left: -100%;
} }
100% { 100% {
left: 200%; left: 200%;
} }
@@ -2697,11 +2748,9 @@ $radius-lg: 36rpx;
0% { 0% {
background-position: 0% 50%; background-position: 0% 50%;
} }
50% { 50% {
background-position: 100% 50%; background-position: 100% 50%;
} }
100% { 100% {
background-position: 0% 50%; background-position: 0% 50%;
} }
@@ -2738,8 +2787,7 @@ $radius-lg: 36rpx;
.message-item { .message-item {
margin-bottom: 24rpx; margin-bottom: 24rpx;
.user-message, .user-message, .ai-message {
.ai-message {
.avatar { .avatar {
width: 72rpx; width: 72rpx;
height: 72rpx; height: 72rpx;

View File

@@ -1,5 +1,5 @@
<template> <template>
<view class="ai-chat-page" :style="wrapperPageStyle"> <view class="container" :style="wrapperPageStyle">
<!--自定义导航头部 --> <!--自定义导航头部 -->
<view class="custom-navbar" ref="pageHeader" v-if="showCustomNavbar"> <view class="custom-navbar" ref="pageHeader" v-if="showCustomNavbar">
<view class="header-left"> <view class="header-left">
@@ -14,7 +14,7 @@
</view> </view>
<view class="header-right"> <view class="header-right">
<button class="menu-btn" v-show="showMenuButton" @click="showMenu"> <button class="menu-btn" v-show="showMenuButton" @click="showMenu">
<text class="iconfont icon-ellipsis"></text> <text>⚙️</text>
</button> </button>
<!-- 占位元素,确保布局平衡 --> <!-- 占位元素,确保布局平衡 -->
<view v-show="!showMenuButton" class="placeholder"></view> <view v-show="!showMenuButton" class="placeholder"></view>
@@ -44,6 +44,41 @@
@product-view="onProductView" @product-view="onProductView"
@input-change="onInputChange" /> @input-change="onInputChange" />
</view> </view>
<!-- 设置面板 -->
<view v-if="showSettingsPanel" class="settings-popup">
<view class="settings-mask" @click="hideSettings"></view>
<view class="settings-panel">
<view class="settings-header">
<text class="settings-title">设置</text>
<view class="close-btn" @click="hideSettings">
<text>✕</text>
</view>
</view>
<view class="settings-content">
<view class="setting-item" @click="openAvatarSelector">
<view class="setting-icon">
<text class="iconfont icon-user">📷</text>
</view>
<view class="setting-info">
<text class="setting-label">更换头像</text>
<text class="setting-desc">自定义您的聊天头像</text>
</view>
<text class="iconfont icon-right">›</text>
</view>
<view class="setting-item" @click="openNicknameEditor">
<view class="setting-icon">
<text class="iconfont icon-edit">✏️</text>
</view>
<view class="setting-info">
<text class="setting-label">更换昵称</text>
<text class="setting-desc">自定义您的昵称</text>
</view>
<text class="iconfont icon-right">›</text>
</view>
</view>
</view>
</view>
</view> </view>
</template> </template>
@@ -51,12 +86,11 @@
import { mapGetters, mapMutations } from 'vuex' import { mapGetters, mapMutations } from 'vuex'
import navigationHelper from '@/common/js/navigation'; import navigationHelper from '@/common/js/navigation';
import { EventSafety } from '@/common/js/event-safety'; import { EventSafety } from '@/common/js/event-safety';
import aiChatMessage from './ai-chat-message.vue'; import aiChatMessage from './ai-chat-message.vue';
export default { export default {
components: { components: {
aiChatMessage, aiChatMessage
}, },
data() { data() {
return { return {
@@ -82,15 +116,16 @@ export default {
showCustomNavbar: true, // 是否显示自定义导航栏 showCustomNavbar: true, // 是否显示自定义导航栏
showBackButton: false, // 是否显示返回按钮 showBackButton: false, // 是否显示返回按钮
showMenuButton: true, // 是否显示设置菜单按钮 showMenuButton: true, // 是否显示设置菜单按钮
showSettingsPanel: false, // 是否显示设置面板
// --- 聊天内容区域 --- // --- 聊天内容区域 ---
scrollViewHeight: '0px', scrollViewHeight: '0px',
// 事件处理器引用(用于清理) // 事件处理器引用(用于清理)
safeEventHandlers: new Map() safeEventHandlers: new Map()
} }
}, },
computed: { computed: {
...mapGetters([ ...mapGetters([
'globalAIKefuConfig' 'globalAIKefuConfig'
@@ -121,35 +156,42 @@ export default {
wrapperChatContentStyle() { wrapperChatContentStyle() {
return { return {
height: this.containerHeight, height: this.containerHeight,
paddingTop: this.showCustomNavbar ? '0' : (this.navHeight + 'px') paddingTop: this.showCustomNavbar ? '0' : (this.navBarHeight + 'px')
} }
} }
}, },
async onLoad(options) { async onLoad(options) {
await this.initPage(options) // ✅ 修改:取消权限校验,允许所有客服类型访问此页面
// const hasAccess = this.checkAccessPermission();
// if (!hasAccess) {
// // 如果校验失败,不继续初始化
// return;
// }
this. $langConfig.title('AI智能客服');
this.initChat();
}, },
async onReady() { async onReady() {
await this.initNavigation() await this.initNavigation();
}, },
onShow() { onShow() {
// 可在此补充逻辑(如刷新未读数)
}, },
onHide() { onHide() {
// 页面隐藏时可暂停音频等
}, },
onUnload() { onUnload() {
this.cleanup() this.cleanup();
}, },
methods: { methods: {
// ========== 安全事件处理 ========== // ========== 安全事件处理 ==========
setupSafeEventListeners() { setupSafeEventListeners() {
// 使用 EventSafety 包装事件处理器
const safeHandlers = { const safeHandlers = {
serviceRequest: EventSafety.wrapEventHandler( serviceRequest: EventSafety.wrapEventHandler(
this.handleServiceRequest.bind(this), this.handleServiceRequest.bind(this),
@@ -165,49 +207,41 @@ export default {
) )
} }
// 注册事件监听 this. $on('service.requestComponentInfo', safeHandlers.serviceRequest);
this.$on('service.requestComponentInfo', safeHandlers.serviceRequest) this. $on('navigation.requestInfo', safeHandlers.navigationRequest);
this.$on('navigation.requestInfo', safeHandlers.navigationRequest) this. $on('component.interaction', safeHandlers.componentInteraction);
this.$on('component.interaction', safeHandlers.componentInteraction)
// 保存处理器引用用于清理 this.safeEventHandlers.set('serviceRequest', safeHandlers.serviceRequest);
this.safeEventHandlers.set('serviceRequest', safeHandlers.serviceRequest) this.safeEventHandlers.set('navigationRequest', safeHandlers.navigationRequest);
this.safeEventHandlers.set('navigationRequest', safeHandlers.navigationRequest) this.safeEventHandlers.set('componentInteraction', safeHandlers.componentInteraction);
this.safeEventHandlers.set('componentInteraction', safeHandlers.componentInteraction)
}, },
setupNavigationEvents() { setupNavigationEvents() {
// 监听窗口大小变化
uni.onWindowResize((res) => { uni.onWindowResize((res) => {
console.log('窗口大小变化:', res.size) console.log('窗口大小变化:', res.size);
this.calculateScrollViewHeight() this.calculateScrollViewHeight();
}) });
}, },
// ========== 事件处理方法 ========== // ========== 事件处理方法 ==========
async handleServiceRequest(event) { async handleServiceRequest(event) {
console.log('处理服务请求:', EventSafety.extractEventData(event)) console.log('处理服务请求:', EventSafety.extractEventData(event));
// 安全地检查事件目标
if (event.matches('.service-component') || if (event.matches('.service-component') ||
event.detail?.componentType === 'service') { event.detail?.componentType === 'service') {
await this.processServiceRequest(event) await this.processServiceRequest(event);
} }
}, },
async handleNavigationRequest(event) { async handleNavigationRequest(event) {
console.log('处理导航请求:', event.type) console.log('处理导航请求:', event.type);
this.emitNavigationInfo(event);
// 提供导航信息
this.emitNavigationInfo(event)
}, },
handleComponentInteraction(event) { handleComponentInteraction(event) {
console.log('处理组件交互:', event.detail) console.log('处理组件交互:', event.detail);
this.processComponentInteraction(event);
// 安全地处理组件交互
this.processComponentInteraction(event)
}, },
handleEventError(error, event) { handleEventError(error, event) {
@@ -215,74 +249,58 @@ export default {
error: error.message, error: error.message,
eventType: event?.type, eventType: event?.type,
component: this. $options.name component: this. $options.name
}) });
this.showError('操作失败,请重试') this.showError('操作失败,请重试');
}, },
// ========== 初始化页面 ========== // ========== 初始化页面 ==========
async initPage(options = {}) { initChat() {
this.$langConfig.title('AI智能客服'); console.log('AI聊天页面初始化');
this.initChat()
}, },
// 初始化导航栏相关配置
async initNavigation() { async initNavigation() {
try { try {
// 获取导航栏高度 this.navBarHeight = await navigationHelper.getNavigationHeight(this, {forceRefresh: false});
this.navBarHeight = await navigationHelper.getNavigationHeight(this, {forceRefresh: false}) this.statusBarHeight = navigationHelper.getStatusBarHeight();
this.calculateScrollViewHeight();
// 获取状态栏高度 this.setupNavigationEvents();
this.statusBarHeight = navigationHelper.getStatusBarHeight()
// 计算滚动视图高度
this.calculateScrollViewHeight()
// 注册导航相关事件
this.setupNavigationEvents()
} catch (error) { } catch (error) {
console.error('初始化导航栏失败:', error) console.error('初始化导航栏失败:', error);
this.setFallbackNavigationValues() this.setFallbackNavigationValues();
} }
}, },
// 计算滚动视图高度
calculateScrollViewHeight() { calculateScrollViewHeight() {
const safeArea = navigationHelper.getSafeAreaInsets() const safeArea = navigationHelper.getSafeAreaInsets();
const bottomInset = safeArea.bottom || 0 const bottomInset = safeArea.bottom || 0;
const inputHeight = 120 // 输入区域高度 const inputHeight = 120;
this.scrollViewHeight = `calc(100vh - $ {this.navBarHeight + this.statusBarHeight + inputHeight + bottomInset}px)`;
this.scrollViewHeight = `calc(100vh - ${this.navBarHeight + this.statusBarHeight + inputHeight + bottomInset}px)`
}, },
// 备用高度设置
setFallbackNavigationValues() { setFallbackNavigationValues() {
// #ifdef MP-WEIXIN // #ifdef MP-WEIXIN
this.navBarHeight = 44 this.navBarHeight = 44;
this.statusBarHeight = 20 this.statusBarHeight = 20;
// #endif // #endif
// #ifdef H5 // #ifdef H5
this.navBarHeight = 44 this.navBarHeight = 44;
this.statusBarHeight = 0 this.statusBarHeight = 0;
// #endif // #endif
// #ifdef APP-PLUS // #ifdef APP-PLUS
this.navBarHeight = 88 this.navBarHeight = 88;
this.statusBarHeight = 44 this.statusBarHeight = 44;
// #endif // #endif
}, },
cleanup() { cleanup() {
// 清理事件监听器
this.safeEventHandlers.forEach((handler, eventType) => { this.safeEventHandlers.forEach((handler, eventType) => {
this.$off(eventType, handler) this. $off(eventType, handler);
}) });
this.safeEventHandlers.clear() this.safeEventHandlers.clear();
console.log('组件清理完成');
console.log('组件清理完成')
}, },
showError(message) { showError(message) {
@@ -290,19 +308,12 @@ export default {
title: message, title: message,
icon: 'none', icon: 'none',
duration: 2000 duration: 2000
}) });
},
// 初始化聊天
initChat() {
// 可以在这里加载历史消息
console.log('AI聊天页面初始化')
}, },
// 返回上一页 // 返回上一页
goBack() { goBack() {
uni.navigateBack() uni.navigateBack();
}, },
// 显示菜单 // 显示菜单
@@ -312,59 +323,66 @@ export default {
success: (res) => { success: (res) => {
switch (res.tapIndex) { switch (res.tapIndex) {
case 0: case 0:
this.clearChat() this.clearChat();
break break;
case 1: case 1:
this.exportChat() this.exportChat();
break break;
case 2: case 2:
this.showSettings() this.showSettings();
break break;
case 3: case 3:
this.showHelp() this.showHelp();
break break;
} }
} }
}) });
}, },
// 清空聊天
clearChat() { clearChat() {
uni.showModal({ uni.showModal({
title: '提示', title: '提示',
content: '确定要清空聊天记录吗?', content: '确定要清空聊天记录吗?',
success: (res) => { success: (res) => {
if (res.confirm) { if (res.confirm) {
this.$refs.chat.clearMessages() this. $refs.chat.clearMessages();
// 重新添加欢迎消息
this. $refs.chat.addMessage({ this. $refs.chat.addMessage({
id: Date.now(), id: Date.now(),
role: 'ai', role: 'ai',
type: 'text', type: 'text',
content: '聊天记录已清空,有什么可以帮助您的吗?', content: '聊天记录已清空,有什么可以帮助您的吗?',
timestamp: Date.now() timestamp: Date.now()
}) });
} }
} }
}) });
}, },
// 导出聊天记录
exportChat() { exportChat() {
uni.showToast({ uni.showToast({
title: '功能开发中', title: '功能开发中',
icon: 'none' icon: 'none'
}) });
}, },
// 显示设置
showSettings() { showSettings() {
uni.navigateTo({ this.showSettingsPanel = true;
url: '/pages/settings/index' },
})
hideSettings() {
this.showSettingsPanel = false;
},
openAvatarSelector() {
this.hideSettings();
this.$refs.chat.openAvatarSelector();
},
openNicknameEditor() {
this.hideSettings();
this.$refs.chat.openNicknameEditor();
}, },
// 显示帮助
showHelp() { showHelp() {
const helpMessage = { const helpMessage = {
id: Date.now(), id: Date.now(),
@@ -391,27 +409,18 @@ export default {
- **复制**: 长按消息可复制内容 - **复制**: 长按消息可复制内容
- **转发**: 支持消息转发功能`, - **转发**: 支持消息转发功能`,
timestamp: Date.now() timestamp: Date.now()
} };
this.$refs.chat.addMessage(helpMessage) this. $refs.chat.addMessage(helpMessage);
}, },
// 用户发送消息
onMessageSent(message) { onMessageSent(message) {
console.log('用户发送消息:', message) console.log('用户发送消息:', message);
// 使用AI服务获取回复
// AI聊天组件内部已经集成了AI服务,这里只需要监听事件
}, },
// AI回复消息
onAIResponse(message) { onAIResponse(message) {
console.log('AI回复消息:', message) console.log('AI回复消息:', message);
// 可以在这里处理AI回复后的逻辑
// 比如记录对话、更新状态等
}, },
// 生成AI回复
generateAIResponse(userMessage) { generateAIResponse(userMessage) {
const responses = [ const responses = [
'我理解您的需求,让我为您详细解答。', '我理解您的需求,让我为您详细解答。',
@@ -419,9 +428,9 @@ export default {
'根据您的问题,我建议您可以考虑以下几个方面:', '根据您的问题,我建议您可以考虑以下几个方面:',
'这个问题很常见,让我为您提供一些解决方案。', '这个问题很常见,让我为您提供一些解决方案。',
'我明白您的困惑,让我帮您分析一下。' '我明白您的困惑,让我帮您分析一下。'
] ];
const randomResponse = responses[Math.floor(Math.random() * responses.length)] const randomResponse = responses[Math.floor(Math.random() * responses.length)];
const aiMessage = { const aiMessage = {
id: Date.now(), id: Date.now(),
@@ -433,115 +442,124 @@ export default {
{ type: 'like', count: 0 }, { type: 'like', count: 0 },
{ type: 'dislike', count: 0 } { type: 'dislike', count: 0 }
] ]
} };
this.$refs.chat.addMessage(aiMessage) this. $refs.chat.addMessage(aiMessage);
this.$emit('ai-response', aiMessage) this. $emit('ai-response', aiMessage);
}, },
// 操作按钮点击
onActionClick({ action, message }) { onActionClick({ action, message }) {
console.log('操作点击:', action, message)
switch (action.type) { switch (action.type) {
case 'like': case 'like':
uni.showToast({ uni.showToast({ title: '感谢您的反馈!', icon: 'success' });
title: '感谢您的反馈!', break;
icon: 'success'
})
break
case 'dislike': case 'dislike':
uni.showToast({ uni.showToast({ title: '我们会改进服务', icon: 'none' });
title: '我们会改进服务', break;
icon: 'none'
})
break
} }
}, },
// 历史消息加载完成
onHistoryLoaded(messages) { onHistoryLoaded(messages) {
console.log('历史消息加载完成:', messages.length) console.log('历史消息加载完成:', messages.length);
}, },
// 文件预览
onFilePreview(message) { onFilePreview(message) {
console.log('文件预览:', message) uni.showToast({ title: '打开文件: ' + message.fileName, icon: 'none' });
uni.showToast({
title: '打开文件: ' + message.fileName,
icon: 'none'
})
}, },
// 音频播放
onAudioPlay(message) { onAudioPlay(message) {
console.log('音频播放:', message) console.log('音频播放:', message);
}, },
// 音频暂停
onAudioPause(message) { onAudioPause(message) {
console.log('音频暂停:', message) console.log('音频暂停:', message);
}, },
// 视频播放
onVideoPlay(message) { onVideoPlay(message) {
console.log('视频播放:', message) console.log('视频播放:', message);
}, },
// 视频暂停
onVideoPause(message) { onVideoPause(message) {
console.log('视频暂停:', message) console.log('视频暂停:', message);
}, },
// 链接打开
onLinkOpen(message) { onLinkOpen(message) {
console.log('链接打开:', message)
uni.showModal({ uni.showModal({
title: '打开链接', title: '打开链接',
content: `确定要打开链接: $ {message.url} 吗?`, content: `确定要打开链接: $ {message.url} 吗?`,
success: (res) => { success: (res) => {
if (res.confirm) { if (res.confirm) {
// #ifdef H5 // #ifdef H5
window.open(message.url, '_blank') window.open(message.url, '_blank');
// #endif // #endif
// #ifdef APP-PLUS // #ifdef APP-PLUS
plus.runtime.openURL(message.url) plus.runtime.openURL(message.url);
// #endif // #endif
} }
} }
}) });
}, },
// 商品查看
onProductView(message) { onProductView(message) {
console.log('商品查看:', message) uni.showToast({ title: '查看商品: ' + message.title, icon: 'none' });
uni.showToast({
title: '查看商品: ' + message.title,
icon: 'none'
})
}, },
// 输入内容变化
onInputChange(value) { 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> </script>
<style lang="scss"> <style lang="scss">
/* 引入图标字体 */ /* 引入图标字体 */
@import url('@/common/css/iconfont.css'); @import url('/common/css/iconfont.css');
/* 页面样式 */ /* 页面样式 */
.ai-chat-page { .container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background-color: #f8f8f8; background-color: white; /* 白色 */
overflow: hidden; overflow: hidden;
position: fixed; position: fixed;
top: 0; top: 48px;
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
@@ -553,7 +571,7 @@ export default {
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 20rpx 30rpx; padding: 20rpx 30rpx;
background-color: white; background-color: #c4e0ff; /* 浅蓝色 */
border-bottom: 2rpx solid #eeeeee; border-bottom: 2rpx solid #eeeeee;
.header-left { .header-left {
@@ -565,7 +583,7 @@ export default {
width: 60rpx; width: 60rpx;
height: 60rpx; height: 60rpx;
border-radius: 50%; border-radius: 50%;
background-color: #f8f8f8; background-color: #ffffff; /* 白色按钮 */
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -610,7 +628,7 @@ export default {
width: 60rpx; width: 60rpx;
height: 60rpx; height: 60rpx;
border-radius: 50%; border-radius: 50%;
background-color: #f8f8f8; background-color: #ffffff; /* 白色按钮 */
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -634,18 +652,135 @@ export default {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
top: 0;
left: 0;
right: 0;
bottom: 0;
} }
/* 底部tabBar占位样式 */ /* 底部tabBar占位样式 */
.page-bottom { .page-bottom {
width: 100%; width: 100%;
background-color: transparent; background-color: transparent;
} }
/* 设置面板 */
.settings-popup {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
.settings-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(6rpx);
}
.settings-panel {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: #ffffff;
border-radius: 24rpx;
padding: 32rpx;
width: calc(100% - 80rpx);
max-width: 520rpx;
box-shadow: 0 8rpx 36rpx rgba(0, 0, 0, 0.15);
box-sizing: border-box;
.settings-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
border-bottom: 2rpx solid #f0f4f8;
padding-bottom: 16rpx;
.settings-title {
font-size: 34rpx;
font-weight: 600;
color: #333;
}
.close-btn {
width: 56rpx;
height: 56rpx;
border-radius: 50%;
background-color: #f8fafc;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
color: #999;
transition: all 0.2s ease;
&:hover {
background-color: #e9edf2;
}
}
}
.settings-content {
display: flex;
flex-direction: column;
gap: 16rpx;
.setting-item {
display: flex;
align-items: center;
padding: 24rpx;
border-radius: 16rpx;
background-color: #f8fafc;
border: 2rpx solid #f0f4f8;
transition: all 0.2s ease;
&:hover {
background-color: #e9edf2;
border-color: #d9e3ed;
}
.setting-icon {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
background-color: #c4e0ff;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
flex-shrink: 0;
}
.setting-info {
flex: 1;
margin-left: 20rpx;
display: flex;
flex-direction: column;
gap: 4rpx;
.setting-label {
font-size: 28rpx;
font-weight: 500;
color: #333;
}
.setting-desc {
font-size: 22rpx;
color: #999;
}
}
.iconfont.icon-right {
font-size: 36rpx;
color: #ccc;
flex-shrink: 0;
}
}
}
}
}
</style> </style>

View File

@@ -78,20 +78,23 @@
1. 使用 HBuilderX 打开项目 1. 使用 HBuilderX 打开项目
2. 打开项目根目录的 `manifest.json` 文件,切换至可视化配置界面: 2. 打开项目根目录的 `manifest.json` 文件,切换至可视化配置界面:
1. 依次点击「Web 配置」→「运行的基础配置」 1. 依次点击「Web 配置」→「运行的基础配置」
2. 修改路径中的客户编号 2. 根据情况查看是否修改路径中/hwappx/????/的客户编号
3. 📌 **通用版**保留原有 `/hwappx/common/` 📌 **通用版**保留原有 `/hwappx/common/`
📌 **测试版**保留原有 `/hwappx/test/`
💡 **个性化定制示例**:`/hwappx/2811/`,其中 2811 为定制化版客户编号
💡 **示例**:`/hwappx/2811/`,其中 2811 为定制化版客户编号 3. 选择菜单栏「发行」 → 「自定义发行」 →(「H5-xcx.aigc-quickapp.com」查看.local.config.js中站点的网址前缀,站点的网址前缀是什么就选自定义发行中相同的格式)
例如:网址是5g.aigc-quickapp.com,自定义发行就选H5-5g.aigc-quickapp.com
3. 选择菜单栏「发行」 → 「自定义发行」 →「H5-xcx.aigc-quickapp.com」,修改以下配置: 网址是dev.aigc-quickapp.com,自定义发行就选H5-xcx.aigc-quickapp.com
4. 修改以下配置:
1. **网站标题**:快应用 1. **网站标题**:快应用
2. **网站域名**:当前客户域名(示例:xcx.aigc-quickapp.com) 2. **网站域名**:当前客户域名(示例:xcx.aigc-quickapp.com或者5g.aigc-quickapp.com)
3. 确认后点击「发行」等待构建完成 3. 确认后点击「发行」等待构建完成
⚠️ **注意**:底部控制台弹出“项目 lucky_shop 导出Web成功,路径为:D:\项目文件\项目根目录\unpackage\dist\build\web”后再执行下一步 ⚠️ **注意**:底部控制台弹出“项目 lucky_shop 导出Web成功,路径为:D:\项目文件\项目根目录\unpackage\dist\build\web”后再执行下一步
💡 **示例**:`项目 lucky_shop 导出Web成功,路径为:D:\0.项目源码\lucky_shop\unpackage\dist\build\web` 💡 **示例**:`项目 lucky_shop 导出Web成功,路径为:D:\0.项目源码\lucky_shop\unpackage\dist\build\web`
4. 按控制台提示的路径找到 `web` 目录,将该目录下所有文件手动打包成一个 `.zip` 压缩包 5. 按控制台提示的路径找到 `web` 目录,将该目录下所有文件手动打包成一个 `.zip` 压缩包
*仅打包文件,不包含外层 web 目录* *仅打包文件,不包含外层 web 目录*
##### 4.2.1.2 【重命名】 ##### 4.2.1.2 【重命名】

View File

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

View File

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