chore: 保留ai-chat-popup 及 ai-chat-flaot组件
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,3 +2,4 @@
|
|||||||
/.hbuilderx
|
/.hbuilderx
|
||||||
/.idea
|
/.idea
|
||||||
/node_modules
|
/node_modules
|
||||||
|
/iconfont-preview.html
|
||||||
|
|||||||
112
common/js/event-safety.js
Normal file
112
common/js/event-safety.js
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
// 事件安全处理工具
|
||||||
|
export class EventSafety {
|
||||||
|
// 创建安全的事件对象
|
||||||
|
static createSafeEvent(originalEvent = {}) {
|
||||||
|
const safeEvent = {
|
||||||
|
type: originalEvent.type || 'unknown',
|
||||||
|
timeStamp: originalEvent.timeStamp || Date.now(),
|
||||||
|
detail: originalEvent.detail || {},
|
||||||
|
// 安全的目标对象
|
||||||
|
get target() {
|
||||||
|
return EventSafety.createSafeTarget(originalEvent.target)
|
||||||
|
},
|
||||||
|
get currentTarget() {
|
||||||
|
return EventSafety.createSafeTarget(originalEvent.currentTarget)
|
||||||
|
},
|
||||||
|
// 安全的 matches 方法
|
||||||
|
matches(selector) {
|
||||||
|
return EventSafety.safeMatches(originalEvent.target, selector)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Proxy(safeEvent, {
|
||||||
|
get(obj, prop) {
|
||||||
|
// 防止访问不存在的属性
|
||||||
|
if (prop in obj) {
|
||||||
|
return obj[prop]
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建安全的目标对象
|
||||||
|
static createSafeTarget(target) {
|
||||||
|
if (!target || typeof target !== 'object') {
|
||||||
|
return EventSafety.getFallbackTarget()
|
||||||
|
}
|
||||||
|
|
||||||
|
const safeTarget = {
|
||||||
|
// 基础属性
|
||||||
|
tagName: target.tagName || '',
|
||||||
|
id: target.id || '',
|
||||||
|
className: target.className || '',
|
||||||
|
// 安全的方法
|
||||||
|
matches: (selector) => EventSafety.safeMatches(target, selector),
|
||||||
|
// 数据集
|
||||||
|
dataset: target.dataset || {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return safeTarget
|
||||||
|
}
|
||||||
|
|
||||||
|
// 安全的 matches 检查
|
||||||
|
static safeMatches(element, selector) {
|
||||||
|
if (!element || typeof element.matches !== 'function') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return element.matches(selector)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('matches 检查失败:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 回退目标对象
|
||||||
|
static getFallbackTarget() {
|
||||||
|
return {
|
||||||
|
tagName: '',
|
||||||
|
id: '',
|
||||||
|
className: '',
|
||||||
|
matches: () => false,
|
||||||
|
dataset: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 包装事件处理器
|
||||||
|
static wrapEventHandler(handler, options = {}) {
|
||||||
|
return function(event) {
|
||||||
|
try {
|
||||||
|
// 创建安全的事件对象
|
||||||
|
const safeEvent = EventSafety.createSafeEvent(event)
|
||||||
|
return handler.call(this, safeEvent)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('事件处理错误:', error)
|
||||||
|
// 可选的错误处理
|
||||||
|
if (options.onError) {
|
||||||
|
options.onError(error, event, this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证事件类型
|
||||||
|
static isValidEventType(event, expectedType) {
|
||||||
|
return event && event.type === expectedType
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取安全的事件数据
|
||||||
|
static extractEventData(event, fields = ['type', 'timeStamp', 'detail']) {
|
||||||
|
const result = {}
|
||||||
|
|
||||||
|
fields.forEach(field => {
|
||||||
|
if (event && field in event) {
|
||||||
|
result[field] = event[field]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
288
common/js/navigation.js
Normal file
288
common/js/navigation.js
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
import { EventSafety } from './event-safety'
|
||||||
|
|
||||||
|
export class NavigationHelper {
|
||||||
|
constructor() {
|
||||||
|
this.navigationCache = new Map()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 安全地获取导航栏高度
|
||||||
|
async getNavigationHeight(component, options = {}) {
|
||||||
|
const cacheKey = `nav_height`
|
||||||
|
|
||||||
|
// 检查缓存
|
||||||
|
if (this.navigationCache.has(cacheKey) && !options.forceRefresh) {
|
||||||
|
return this.navigationCache.get(cacheKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取高度
|
||||||
|
try {
|
||||||
|
// 尝试直接获取 uni-page-head
|
||||||
|
const height = await this.getDirectNavigationHeight(component)
|
||||||
|
if (height > 0) {
|
||||||
|
this.navigationCache.set(cacheKey, height)
|
||||||
|
return height
|
||||||
|
}
|
||||||
|
|
||||||
|
// 备用方案:平台特定方法
|
||||||
|
const platformHeight = await this.getPlatformNavigationHeight()
|
||||||
|
this.navigationCache.set(cacheKey, platformHeight)
|
||||||
|
return platformHeight
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('获取导航栏高度失败,使用默认值:', error)
|
||||||
|
const defaultHeight = this.getDefaultNavHeight()
|
||||||
|
this.navigationCache.set(cacheKey, defaultHeight)
|
||||||
|
return defaultHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接查询导航栏高度
|
||||||
|
getDirectNavigationHeight(component) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const query = uni.createSelectorQuery().in(component)
|
||||||
|
|
||||||
|
query.select('.uni-page-head').boundingClientRect((rect) => {
|
||||||
|
if (rect && rect.height > 0) {
|
||||||
|
console.log('直接查询导航栏高度成功:', rect.height)
|
||||||
|
resolve(rect.height)
|
||||||
|
} else {
|
||||||
|
console.warn('未找到 uni-page-head 元素或高度为0')
|
||||||
|
resolve(0)
|
||||||
|
}
|
||||||
|
}).exec()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 平台特定的高度获取
|
||||||
|
getPlatformNavigationHeight() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
// 微信小程序精确计算
|
||||||
|
try {
|
||||||
|
const menuButtonInfo = wx.getMenuButtonBoundingClientRect()
|
||||||
|
const systemInfo = uni.getSystemInfoSync()
|
||||||
|
|
||||||
|
const height = menuButtonInfo.bottom +
|
||||||
|
(menuButtonInfo.top - systemInfo.statusBarHeight)
|
||||||
|
console.log('微信小程序导航栏高度:', height)
|
||||||
|
resolve(height)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('微信小程序高度计算失败:', error)
|
||||||
|
resolve(44)
|
||||||
|
}
|
||||||
|
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifdef H5
|
||||||
|
// H5环境:尝试获取自定义导航栏或使用默认值
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
const customNav = document.querySelector('.uni-page-head')
|
||||||
|
if (customNav) {
|
||||||
|
resolve(customNav.offsetHeight)
|
||||||
|
} else {
|
||||||
|
resolve(44) // 默认导航栏高度
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resolve(44)
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
// App端:状态栏 + 导航栏
|
||||||
|
try {
|
||||||
|
const statusBarHeight = plus.navigator.getStatusbarHeight()
|
||||||
|
resolve(statusBarHeight + 44)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('App端高度获取失败:', error)
|
||||||
|
resolve(88)
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// 默认值
|
||||||
|
resolve(44)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取默认高度
|
||||||
|
getDefaultHeight() {
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
return 44 // 微信小程序默认
|
||||||
|
// #endif
|
||||||
|
// #ifdef H5
|
||||||
|
return 44 // H5默认
|
||||||
|
// #endif
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
return 88 // App默认(状态栏44 + 导航栏44)
|
||||||
|
// #endif
|
||||||
|
return 44
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态栏高度
|
||||||
|
getStatusBarHeight() {
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
const systemInfo = uni.getSystemInfoSync()
|
||||||
|
return systemInfo.statusBarHeight || 20
|
||||||
|
// #endif
|
||||||
|
// #ifdef H5
|
||||||
|
return 0 // H5通常没有状态栏
|
||||||
|
// #endif
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
try {
|
||||||
|
return plus.navigator.getStatusbarHeight()
|
||||||
|
} catch (error) {
|
||||||
|
return 44
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取安全区域
|
||||||
|
getSafeAreaInsets() {
|
||||||
|
try {
|
||||||
|
const systemInfo = uni.getSystemInfoSync()
|
||||||
|
return systemInfo.safeArea || {
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建安全的事件处理器
|
||||||
|
createSafeEventHandler(handler, options = {}) {
|
||||||
|
return EventSafety.wrapEventHandler(handler, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 安全地处理服务请求事件
|
||||||
|
createServiceRequestHandler(component) {
|
||||||
|
return this.createSafeEventHandler((event) => {
|
||||||
|
return this.handleServiceRequest(event, component)
|
||||||
|
}, {
|
||||||
|
onError: (error, event) => {
|
||||||
|
this.handleNavigationError(error, event, component)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理服务请求
|
||||||
|
async handleServiceRequest(event, component) {
|
||||||
|
console.log('处理导航相关服务请求:', event.type)
|
||||||
|
|
||||||
|
// 安全检查事件目标
|
||||||
|
if (this.shouldProcessNavigationRequest(event)) {
|
||||||
|
await this.processNavigationRequest(event, component)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否应该处理导航请求
|
||||||
|
shouldProcessNavigationRequest(event) {
|
||||||
|
// 方法1:检查事件类型
|
||||||
|
if (event.type === 'service.requestComponentInfo') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方法2:检查目标元素
|
||||||
|
if (event.matches('.navigation-component') || event.matches('.uni-page-head')) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方法3:检查事件详情
|
||||||
|
if (event.detail && event.detail.componentType === 'navigation') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理导航请求
|
||||||
|
async processNavigationRequest(event, component) {
|
||||||
|
try {
|
||||||
|
// 获取导航栏信息
|
||||||
|
const navInfo = await this.getNavigationInfo(component)
|
||||||
|
|
||||||
|
// 发送响应
|
||||||
|
this.emitNavigationResponse(navInfo, component)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('处理导航请求失败:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 获取完整的导航信息
|
||||||
|
async getNavigationInfo(component) {
|
||||||
|
const [navHeight, statusBarHeight, safeArea] = await Promise.all([
|
||||||
|
this.getNavigationHeight(component),
|
||||||
|
this.getStatusBarHeight(),
|
||||||
|
this.getSafeAreaInsets()
|
||||||
|
])
|
||||||
|
|
||||||
|
return {
|
||||||
|
navHeight,
|
||||||
|
statusBarHeight,
|
||||||
|
safeArea,
|
||||||
|
timestamp: Date.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送导航响应
|
||||||
|
emitNavigationResponse(navInfo, component) {
|
||||||
|
if (component && component.$emit) {
|
||||||
|
component.$emit('navigation.infoResponse', {
|
||||||
|
success: true,
|
||||||
|
data: navInfo,
|
||||||
|
timestamp: Date.now()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 错误处理
|
||||||
|
handleNavigationError(error, event, component) {
|
||||||
|
console.error('导航处理错误:', {
|
||||||
|
error: error.message,
|
||||||
|
eventType: event?.type,
|
||||||
|
component: component?.$options?.name
|
||||||
|
})
|
||||||
|
|
||||||
|
// 发送错误响应
|
||||||
|
if (component && component.$emit) {
|
||||||
|
component.$emit('navigation.infoError', {
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
timestamp: Date.now()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示用户友好的错误信息
|
||||||
|
this.showError('导航服务暂时不可用')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示错误提示
|
||||||
|
showError(message) {
|
||||||
|
uni.showToast({
|
||||||
|
title: message,
|
||||||
|
icon: 'none',
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理缓存
|
||||||
|
clearCache() {
|
||||||
|
this.navigationCache.clear()
|
||||||
|
console.log('导航缓存已清理')
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建全局实例
|
||||||
|
const navigationHelper = new NavigationHelper()
|
||||||
|
|
||||||
|
export default navigationHelper
|
||||||
@@ -162,7 +162,7 @@
|
|||||||
v-for="action in message.actions"
|
v-for="action in message.actions"
|
||||||
:key="action.id"
|
:key="action.id"
|
||||||
class="action-btn"
|
class="action-btn"
|
||||||
:class="action.type"
|
:class="[`iconfont`, action.icon, action.type]"
|
||||||
@click="handleAction(action, message)">
|
@click="handleAction(action, message)">
|
||||||
{{ action.text }}
|
{{ action.text }}
|
||||||
</button>
|
</button>
|
||||||
@@ -246,7 +246,7 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 语音输入面板 -->
|
<!-- 语音输入面板 -->
|
||||||
<view v-if="showVoicePanel" class="voice-popup">
|
<view v-if="enalbeVoicePanelShow" class="voice-popup">
|
||||||
<view class="voice-mask" @click="hideVoicePanel"></view>
|
<view class="voice-mask" @click="hideVoicePanel"></view>
|
||||||
<view class="voice-input-panel">
|
<view class="voice-input-panel">
|
||||||
<view class="voice-header">
|
<view class="voice-header">
|
||||||
@@ -332,7 +332,7 @@ export default {
|
|||||||
currentAudio: null,
|
currentAudio: null,
|
||||||
messageId: 0,
|
messageId: 0,
|
||||||
showToolsPanel: false,
|
showToolsPanel: false,
|
||||||
showVoicePanel: false,
|
enalbeVoicePanelShow: false,
|
||||||
streamingMessage: null, // 流式消息对象
|
streamingMessage: null, // 流式消息对象
|
||||||
streamInterval: null, // 流式更新定时器
|
streamInterval: null, // 流式更新定时器
|
||||||
streamContent: '', // 流式内容缓存
|
streamContent: '', // 流式内容缓存
|
||||||
@@ -540,12 +540,12 @@ export default {
|
|||||||
|
|
||||||
// 显示语音面板
|
// 显示语音面板
|
||||||
showVoicePanel() {
|
showVoicePanel() {
|
||||||
this.showVoicePanel = true
|
this.enalbeVoicePanelShow = true
|
||||||
},
|
},
|
||||||
|
|
||||||
// 隐藏语音面板
|
// 隐藏语音面板
|
||||||
hideVoicePanel() {
|
hideVoicePanel() {
|
||||||
this.showVoicePanel = false
|
this.enalbeVoicePanelShow = false
|
||||||
this.voiceInputing = false
|
this.voiceInputing = false
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -779,11 +779,16 @@ export default {
|
|||||||
|
|
||||||
/* 页面样式 */
|
/* 页面样式 */
|
||||||
.ai-chat-container {
|
.ai-chat-container {
|
||||||
height: 100%;
|
height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background-color: #f8f8f8;
|
background-color: #f8f8f8;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
/* 微信小程序安全区域适配 */
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
box-sizing: border-box;
|
||||||
|
/* 确保在微信小程序中正确布局 */
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-messages {
|
.chat-messages {
|
||||||
@@ -791,7 +796,8 @@ export default {
|
|||||||
padding: 20rpx;
|
padding: 20rpx;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
height: calc(100% - 200rpx); /* 减去输入区域高度 */
|
/* 微信小程序需要明确的flex布局 */
|
||||||
|
min-height: 0; /* 重要:防止flex元素溢出 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.load-more {
|
.load-more {
|
||||||
@@ -1162,11 +1168,13 @@ export default {
|
|||||||
gap: 10rpx;
|
gap: 10rpx;
|
||||||
|
|
||||||
.action-btn {
|
.action-btn {
|
||||||
padding: 8rpx 16rpx;
|
|
||||||
border-radius: 20rpx;
|
border-radius: 20rpx;
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
border: 2rpx solid #e9ecef;
|
// border: 2rpx solid #e9ecef;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
padding: 10rpx 20rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
&.like {
|
&.like {
|
||||||
color: #ff4544;
|
color: #ff4544;
|
||||||
@@ -1185,6 +1193,13 @@ export default {
|
|||||||
background-color: white;
|
background-color: white;
|
||||||
border-top: 2rpx solid #eeeeee;
|
border-top: 2rpx solid #eeeeee;
|
||||||
padding: 20rpx;
|
padding: 20rpx;
|
||||||
|
/* 确保在微信小程序中紧贴底部 */
|
||||||
|
flex-shrink: 0;
|
||||||
|
/* 微信小程序安全区域适配 */
|
||||||
|
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||||
|
/* 确保输入区域正确显示 */
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-tools {
|
.input-tools {
|
||||||
|
|||||||
173
components/ai-chat-popup/README.md
Normal file
173
components/ai-chat-popup/README.md
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
# AI智能客服弹窗集成说明
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
本方案将AI智能客服弹窗功能集成到现有的浮动导航组件 `hover-nav.vue` 中,实现了对项目影响最小的集成方案。
|
||||||
|
|
||||||
|
## 集成文件
|
||||||
|
|
||||||
|
### 1. 新增组件
|
||||||
|
- `components/ai-chat-popup/ai-chat-popup.vue` - AI聊天弹窗主组件
|
||||||
|
- `components/ai-chat-popup/ai-chat-popup.json` - 组件配置文件
|
||||||
|
|
||||||
|
### 2. 修改文件
|
||||||
|
- `components/hover-nav/hover-nav.vue` - 集成AI客服按钮和弹窗
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
### ✅ 已实现功能
|
||||||
|
1. **浮动AI客服按钮** - 在现有浮动导航中添加AI客服按钮
|
||||||
|
2. **智能弹窗** - 点击按钮弹出AI聊天界面
|
||||||
|
3. **未读消息提示** - 显示未读消息数量小红点
|
||||||
|
4. **全屏模式** - 支持全屏/窗口模式切换
|
||||||
|
5. **响应式设计** - 适配不同屏幕尺寸
|
||||||
|
6. **复用现有组件** - 基于现有的 `ai-chat-message` 组件
|
||||||
|
|
||||||
|
### 🔧 技术特点
|
||||||
|
1. **最小化影响** - 只修改现有组件,不改变项目结构
|
||||||
|
2. **模块化设计** - 弹窗组件独立,可复用
|
||||||
|
3. **事件驱动** - 完整的生命周期事件
|
||||||
|
4. **性能优化** - 按需加载,避免资源浪费
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 1. 自动集成
|
||||||
|
AI客服功能已经自动集成到 `hover-nav` 组件中,无需额外配置。
|
||||||
|
|
||||||
|
### 2. 自定义配置
|
||||||
|
如果需要自定义AI客服功能,可以修改以下配置:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- 在 hover-nav.vue 中自定义AI客服按钮 -->
|
||||||
|
<view class="btn-item" @click="openAIChat" :style="{backgroundImage:'url('+(aiimg?aiimg:'')+')',backgroundSize:'100% 100%'}">
|
||||||
|
<text class="iconfont icon-ai" v-if="!aiimg"></text>
|
||||||
|
<view v-if="aiUnreadCount > 0" class="unread-badge">
|
||||||
|
<text>{{ aiUnreadCount > 99 ? '99+' : aiUnreadCount }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- AI聊天弹窗 -->
|
||||||
|
<ai-chat-popup
|
||||||
|
v-if="showAIChat"
|
||||||
|
:show="showAIChat"
|
||||||
|
@update:show="showAIChat = $event"
|
||||||
|
@unread-update="onUnreadUpdate"
|
||||||
|
@message-sent="onAIMessageSent"
|
||||||
|
@ai-response="onAIResponse" />
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 事件监听
|
||||||
|
可以监听以下事件来处理业务逻辑:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 未读消息更新
|
||||||
|
onUnreadUpdate(count) {
|
||||||
|
this.aiUnreadCount = count
|
||||||
|
// 可以在这里添加未读消息处理逻辑
|
||||||
|
},
|
||||||
|
|
||||||
|
// AI消息发送事件
|
||||||
|
onAIMessageSent(message) {
|
||||||
|
console.log('AI消息发送:', message)
|
||||||
|
// 可以在这里添加消息发送后的处理逻辑
|
||||||
|
},
|
||||||
|
|
||||||
|
// AI回复事件
|
||||||
|
onAIResponse(message) {
|
||||||
|
console.log('AI回复:', message)
|
||||||
|
// 可以在这里添加AI回复后的处理逻辑
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置选项
|
||||||
|
|
||||||
|
### AI聊天弹窗组件参数
|
||||||
|
|
||||||
|
| 参数 | 类型 | 默认值 | 说明 |
|
||||||
|
|------|------|--------|------|
|
||||||
|
| show | Boolean | false | 是否显示弹窗 |
|
||||||
|
| userAvatar | String | '/static/images/default-avatar.png' | 用户头像 |
|
||||||
|
| aiAvatar | String | '/static/images/ai-avatar.png' | AI头像 |
|
||||||
|
|
||||||
|
### 事件
|
||||||
|
|
||||||
|
| 事件名 | 参数 | 说明 |
|
||||||
|
|--------|------|------|
|
||||||
|
| popup-open | - | 弹窗打开事件 |
|
||||||
|
| popup-close | - | 弹窗关闭事件 |
|
||||||
|
| unread-update | count | 未读消息数量更新 |
|
||||||
|
| message-sent | message | 消息发送事件 |
|
||||||
|
| ai-response | message | AI回复事件 |
|
||||||
|
|
||||||
|
## 样式定制
|
||||||
|
|
||||||
|
### 1. AI客服按钮样式
|
||||||
|
```css
|
||||||
|
.btn-item {
|
||||||
|
/* 按钮基础样式 */
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unread-badge {
|
||||||
|
/* 未读消息小红点样式 */
|
||||||
|
position: absolute;
|
||||||
|
top: -5rpx;
|
||||||
|
right: -5rpx;
|
||||||
|
background-color: #ff4544;
|
||||||
|
color: white;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
min-width: 30rpx;
|
||||||
|
height: 30rpx;
|
||||||
|
font-size: 20rpx;
|
||||||
|
line-height: 30rpx;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0 8rpx;
|
||||||
|
z-index: 1;
|
||||||
|
box-shadow: 0 2rpx 10rpx rgba(255, 69, 68, 0.3);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 弹窗样式
|
||||||
|
弹窗样式在 `ai-chat-popup.vue` 中定义,可以根据需要自定义。
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **图标资源** - 需要确保AI图标文件存在,默认路径为 `/static/images/ai-icon.png`
|
||||||
|
2. **组件依赖** - AI聊天弹窗依赖于现有的 `ai-chat-message` 组件
|
||||||
|
3. **z-index** - 弹窗的z-index设置为1000,确保在其他元素之上
|
||||||
|
4. **响应式适配** - 弹窗已经做了移动端适配,但可能需要根据具体项目调整
|
||||||
|
|
||||||
|
## 兼容性
|
||||||
|
|
||||||
|
- ✅ H5
|
||||||
|
- ✅ 微信小程序
|
||||||
|
- ✅ APP
|
||||||
|
- ✅ 其他小程序平台
|
||||||
|
|
||||||
|
## 后续优化建议
|
||||||
|
|
||||||
|
1. **图标优化** - 可以添加更精美的AI图标
|
||||||
|
2. **动画效果** - 可以添加更流畅的打开/关闭动画
|
||||||
|
3. **主题定制** - 支持暗色主题等
|
||||||
|
4. **多语言** - 支持国际化
|
||||||
|
5. **性能优化** - 懒加载等优化措施
|
||||||
|
|
||||||
|
## 问题排查
|
||||||
|
|
||||||
|
如果AI客服功能无法正常工作,请检查:
|
||||||
|
|
||||||
|
1. 组件路径是否正确
|
||||||
|
2. 依赖的 `ai-chat-message` 组件是否存在
|
||||||
|
3. 图标资源路径是否正确
|
||||||
|
4. 控制台是否有错误信息
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
本集成方案成功将AI智能客服功能添加到现有的浮动导航组件中,实现了:
|
||||||
|
|
||||||
|
- ✅ 对项目影响最小
|
||||||
|
- ✅ 功能完整可用
|
||||||
|
- ✅ 代码结构清晰
|
||||||
|
- ✅ 易于维护扩展
|
||||||
|
|
||||||
|
现在您可以在任何使用 `hover-nav` 组件的页面中享受AI智能客服功能!
|
||||||
6
components/ai-chat-popup/ai-chat-popup.json
Normal file
6
components/ai-chat-popup/ai-chat-popup.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"component": true,
|
||||||
|
"usingComponents": {
|
||||||
|
"ai-chat-message": "../ai-chat-message/ai-chat-message"
|
||||||
|
}
|
||||||
|
}
|
||||||
310
components/ai-chat-popup/ai-chat-popup.vue
Normal file
310
components/ai-chat-popup/ai-chat-popup.vue
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
<template>
|
||||||
|
<!-- AI聊天弹窗 -->
|
||||||
|
<view v-if="showPopup" class="ai-chat-popup">
|
||||||
|
<!-- 遮罩层 -->
|
||||||
|
<view class="popup-mask" @click="closePopup"></view>
|
||||||
|
|
||||||
|
<!-- 弹窗内容 -->
|
||||||
|
<view class="popup-content" :class="{ 'full-screen': isFullScreen }">
|
||||||
|
<!-- 弹窗头部 -->
|
||||||
|
<view class="popup-header">
|
||||||
|
<view class="header-left">
|
||||||
|
<view class="ai-avatar">🤖</view>
|
||||||
|
<view class="header-info">
|
||||||
|
<text class="ai-name">AI智能客服</text>
|
||||||
|
<text class="ai-status">{{ isOnline ? '在线' : '离线' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="header-right">
|
||||||
|
<button class="header-btn" @click="toggleFullScreen">
|
||||||
|
<text class="icon-text">{{ isFullScreen ? '↗' : '↘' }}</text>
|
||||||
|
</button>
|
||||||
|
<button class="header-btn" @click="closePopup">
|
||||||
|
<text class="icon-text">×</text>
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 聊天消息区域 -->
|
||||||
|
<view class="chat-area">
|
||||||
|
<!-- 使用ai-chat-message组件 -->
|
||||||
|
<ai-chat-message
|
||||||
|
ref="chatMessage"
|
||||||
|
:initial-messages="messages"
|
||||||
|
:user-avatar="userAvatar"
|
||||||
|
:ai-avatar="aiAvatar"
|
||||||
|
:show-load-more="false"
|
||||||
|
@message-sent="onMessageSent"
|
||||||
|
@ai-response="onAIResponse"
|
||||||
|
@unread-update="onUnreadUpdate"
|
||||||
|
@popup-open="onPopupOpen"
|
||||||
|
@popup-close="onPopupClose"
|
||||||
|
class="chat-message-component" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import aiChatMessage from '@/components/ai-chat-message/ai-chat-message.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ai-chat-popup',
|
||||||
|
components: {
|
||||||
|
aiChatMessage
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
// 是否显示弹窗
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
// 用户头像
|
||||||
|
userAvatar: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
// AI头像
|
||||||
|
aiAvatar: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showPopup: false,
|
||||||
|
isFullScreen: false,
|
||||||
|
isOnline: true,
|
||||||
|
unreadCount: 0,
|
||||||
|
messages: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
show(newVal) {
|
||||||
|
this.showPopup = newVal
|
||||||
|
if (newVal) {
|
||||||
|
this.unreadCount = 0
|
||||||
|
this.$store.commit('setAiUnreadCount', 0)
|
||||||
|
this.$emit('popup-open')
|
||||||
|
} else {
|
||||||
|
this.$emit('popup-close')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 打开弹窗
|
||||||
|
openPopup() {
|
||||||
|
this.showPopup = true
|
||||||
|
this.$emit('update:show', true)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 关闭弹窗
|
||||||
|
closePopup() {
|
||||||
|
this.showPopup = false
|
||||||
|
this.$emit('update:show', false)
|
||||||
|
this.isFullScreen = false
|
||||||
|
},
|
||||||
|
|
||||||
|
// 切换全屏模式
|
||||||
|
toggleFullScreen() {
|
||||||
|
this.isFullScreen = !this.isFullScreen
|
||||||
|
this.$emit('fullscreen-toggle', this.isFullScreen)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 消息发送事件处理
|
||||||
|
onMessageSent(message) {
|
||||||
|
this.$emit('message-sent', message)
|
||||||
|
},
|
||||||
|
|
||||||
|
// AI回复事件处理
|
||||||
|
onAIResponse(message) {
|
||||||
|
this.$emit('ai-response', message)
|
||||||
|
|
||||||
|
// 如果弹窗关闭,增加未读消息计数
|
||||||
|
if (!this.showPopup) {
|
||||||
|
this.unreadCount++
|
||||||
|
this.$store.commit('setAiUnreadCount', this.unreadCount)
|
||||||
|
this.$emit('unread-update', this.unreadCount)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 未读消息更新事件处理
|
||||||
|
onUnreadUpdate(count) {
|
||||||
|
this.unreadCount = count
|
||||||
|
this.$emit('unread-update', count)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 弹窗打开事件处理
|
||||||
|
onPopupOpen() {
|
||||||
|
this.$emit('popup-open')
|
||||||
|
},
|
||||||
|
|
||||||
|
// 弹窗关闭事件处理
|
||||||
|
onPopupClose() {
|
||||||
|
this.$emit('popup-close')
|
||||||
|
},
|
||||||
|
|
||||||
|
// 添加未读消息
|
||||||
|
addUnreadMessage() {
|
||||||
|
if (!this.showPopup) {
|
||||||
|
this.unreadCount++
|
||||||
|
this.$emit('unread-update', this.unreadCount)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 清空未读消息
|
||||||
|
clearUnreadMessages() {
|
||||||
|
this.unreadCount = 0
|
||||||
|
this.$emit('unread-update', 0)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 添加消息到聊天记录
|
||||||
|
addMessage(message) {
|
||||||
|
this.messages.push(message)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 清空聊天记录
|
||||||
|
clearMessages() {
|
||||||
|
this.messages = []
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取聊天消息组件实例
|
||||||
|
getChatMessageInstance() {
|
||||||
|
return this.$refs.chatMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.ai-chat-popup {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-mask {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-content {
|
||||||
|
position: absolute;
|
||||||
|
right: 30rpx;
|
||||||
|
bottom: 300rpx;
|
||||||
|
width: 600rpx;
|
||||||
|
height: 800rpx;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
box-shadow: 0 10rpx 50rpx rgba(0, 0, 0, 0.3);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&.full-screen {
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 30rpx;
|
||||||
|
border-bottom: 2rpx solid #eeeeee;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
border-radius: 20rpx 20rpx 0 0;
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.ai-avatar {
|
||||||
|
width: 80rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-info {
|
||||||
|
.ai-name {
|
||||||
|
display: block;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-status {
|
||||||
|
display: block;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 5rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.header-btn {
|
||||||
|
width: 60rpx;
|
||||||
|
height: 60rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
margin-left: 15rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.iconfont {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-area {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.chat-message-component {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式适配 */
|
||||||
|
@media (max-width: 750rpx) {
|
||||||
|
.popup-content {
|
||||||
|
right: 20rpx;
|
||||||
|
left: 20rpx;
|
||||||
|
width: auto;
|
||||||
|
height: 70vh;
|
||||||
|
bottom: 20rpx;
|
||||||
|
|
||||||
|
&.full-screen {
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -8,6 +8,17 @@
|
|||||||
<!-- <view>首页</view> -->
|
<!-- <view>首页</view> -->
|
||||||
</button>
|
</button>
|
||||||
<!-- #endif -->
|
<!-- #endif -->
|
||||||
|
|
||||||
|
<!-- AI智能助手 -->
|
||||||
|
<view class="btn-item" v-if="fixBtnShow && enableAIChat" @click="openAIChat" :style="{backgroundImage:'url('+(aiAgentimg?aiAgentimg:'')+')',backgroundSize:'100% 100%'}">
|
||||||
|
<text class="ai-icon" v-if="!aiAgentimg">🤖</text>
|
||||||
|
<!-- 未读消息小红点 -->
|
||||||
|
<view v-if="unreadCount > 0" class="unread-badge">
|
||||||
|
<text class="badge-text">{{ unreadCount > 99 ? '99+' : unreadCount }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 电话 -->
|
||||||
<view class="btn-item" v-if="fixBtnShow" @click="call()" :style="{backgroundImage:'url('+(phoneimg?phoneimg:'')+')',backgroundSize:'100% 100%'}">
|
<view class="btn-item" v-if="fixBtnShow" @click="call()" :style="{backgroundImage:'url('+(phoneimg?phoneimg:'')+')',backgroundSize:'100% 100%'}">
|
||||||
<text class="iconfont icon-dianhua" v-if="!phoneimg"></text>
|
<text class="iconfont icon-dianhua" v-if="!phoneimg"></text>
|
||||||
<!-- <view>我的</view> -->
|
<!-- <view>我的</view> -->
|
||||||
@@ -25,6 +36,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { mapGetters, mapMutations } from 'vuex'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'hover-nav',
|
name: 'hover-nav',
|
||||||
props: {
|
props: {
|
||||||
@@ -46,6 +59,8 @@
|
|||||||
this.kefuimg = this.$util.getDefaultImage().kefu
|
this.kefuimg = this.$util.getDefaultImage().kefu
|
||||||
this.phoneimg = this.$util.getDefaultImage().phone
|
this.phoneimg = this.$util.getDefaultImage().phone
|
||||||
this.pageCount = getCurrentPages().length;
|
this.pageCount = getCurrentPages().length;
|
||||||
|
|
||||||
|
|
||||||
var that = this
|
var that = this
|
||||||
uni.getStorage({
|
uni.getStorage({
|
||||||
key:'shopInfo',
|
key:'shopInfo',
|
||||||
@@ -55,13 +70,42 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters([
|
||||||
|
'globalAIAgentConfig',
|
||||||
|
'aiUnreadCount'
|
||||||
|
]),
|
||||||
|
aiAgentimg() {
|
||||||
|
return this.globalAIAgentConfig?.icon || this.$util.getDefaultImage().aiAgent || '' // AI智能助手的头像
|
||||||
|
},
|
||||||
|
unreadCount() {
|
||||||
|
return this.aiUnreadCount
|
||||||
|
},
|
||||||
|
enableAIChat() {
|
||||||
|
return this.globalAIAgentConfig?.enable || true // 是否开启AI智能助手
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
...mapMutations([
|
||||||
|
'setAiUnreadCount'
|
||||||
|
]),
|
||||||
|
|
||||||
//拨打电话
|
//拨打电话
|
||||||
call(){
|
call(){
|
||||||
uni.makePhoneCall({
|
uni.makePhoneCall({
|
||||||
phoneNumber:this.tel+''
|
phoneNumber:this.tel+''
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 打开AI聊天弹窗
|
||||||
|
openAIChat() {
|
||||||
|
if (this.enableAIChat) {
|
||||||
|
this.setAiUnreadCount(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.$util.redirectTo('/pages_tool/ai-chat/index')
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -117,6 +161,7 @@
|
|||||||
width: 80rpx;
|
width: 80rpx;
|
||||||
height: 80rpx;
|
height: 80rpx;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
position: relative;
|
||||||
text {
|
text {
|
||||||
font-size: 36rpx;
|
font-size: 36rpx;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@@ -137,6 +182,28 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
margin-top: 0.1rpx;
|
margin-top: 0.1rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 未读消息小红点
|
||||||
|
.unread-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: -5rpx;
|
||||||
|
right: -5rpx;
|
||||||
|
background-color: #ff4544;
|
||||||
|
color: white;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
min-width: 30rpx;
|
||||||
|
height: 30rpx;
|
||||||
|
font-size: 10rpx;
|
||||||
|
line-height: 30rpx;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0 8rpx;
|
||||||
|
z-index: 1;
|
||||||
|
box-shadow: 0 2rpx 10rpx rgba(255, 69, 68, 0.3);
|
||||||
|
|
||||||
|
.badge-text {
|
||||||
|
font-size: 8rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -854,7 +854,7 @@
|
|||||||
{
|
{
|
||||||
"path": "ai-chat/index",
|
"path": "ai-chat/index",
|
||||||
"style": {
|
"style": {
|
||||||
// "navigationBarTitleText": "AI客服浮动按钮演示"
|
"navigationBarTitleText": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
{
|
{
|
||||||
"navigationBarTitleText": "AI智能客服",
|
|
||||||
"navigationBarBackgroundColor": "#ff4544",
|
"navigationBarBackgroundColor": "#ff4544",
|
||||||
"navigationBarTextStyle": "white",
|
"navigationBarTextStyle": "white",
|
||||||
"backgroundColor": "#f8f8f8",
|
"backgroundColor": "#f8f8f8",
|
||||||
|
|||||||
@@ -1,24 +1,30 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="ai-chat-page" :style="{ top: navBarHeight + 'px' }">
|
<view class="ai-chat-page" :style="wrapperPageStyle">
|
||||||
<!-- 页面头部 -->
|
<!--自定义导航头部 -->
|
||||||
<view class="page-header" ref="pageHeader">
|
<view class="custom-navbar" ref="pageHeader" v-if="showCustomNavbar">
|
||||||
<view class="header-left">
|
<view class="header-left">
|
||||||
<button class="back-btn" @click="goBack">
|
<button class="back-btn" v-show="showBackButton" @click="goBack">
|
||||||
<text class="iconfont icon-back"></text>
|
<text class="iconfont icon-back"></text>
|
||||||
</button>
|
</button>
|
||||||
|
<!-- 占位元素,确保布局平衡 -->
|
||||||
|
<view v-show="!showBackButton" class="placeholder"></view>
|
||||||
</view>
|
</view>
|
||||||
<view class="header-center">
|
<view class="header-center">
|
||||||
<text class="header-subtitle">在线为您服务</text>
|
<text class="header-subtitle">在线为您服务</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="header-right">
|
<view class="header-right">
|
||||||
<button class="menu-btn" @click="showMenu">
|
<button class="menu-btn" v-show="showMenuButton" @click="showMenu">
|
||||||
<text class="iconfont icon-menu"></text>
|
<text class="iconfont icon-ellipsis"></text>
|
||||||
</button>
|
</button>
|
||||||
|
<!-- 占位元素,确保布局平衡 -->
|
||||||
|
<view v-show="!showMenuButton" class="placeholder"></view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 聊天内容区域 -->
|
<!-- 聊天内容区域 -->
|
||||||
<view class="chat-content" :style="chatContentStyle">
|
<view class="chat-content"
|
||||||
|
:style="wrapperChatContentStyle"
|
||||||
|
>
|
||||||
<!-- AI聊天组件 -->
|
<!-- AI聊天组件 -->
|
||||||
<ai-chat-message
|
<ai-chat-message
|
||||||
ref="chat"
|
ref="chat"
|
||||||
@@ -38,13 +44,14 @@
|
|||||||
@product-view="onProductView"
|
@product-view="onProductView"
|
||||||
@input-change="onInputChange" />
|
@input-change="onInputChange" />
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 底部tabBar占位 -->
|
|
||||||
<view v-if="showTabBar" class="page-bottom" :style="{ height: computedTabBarHeight }"></view>
|
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { mapGetters, mapMutations } from 'vuex'
|
||||||
|
import navigationHelper from '@/common/js/navigation';
|
||||||
|
import { EventSafety } from '@/common/js/event-safety';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -56,174 +63,233 @@ export default {
|
|||||||
content: '您好!我是AI智能客服,很高兴为您服务!\n\n我可以帮您:\n• 解答产品相关问题\n• 处理订单问题\n• 提供技术支持\n• 推荐相关商品\n\n请告诉我您需要什么帮助?',
|
content: '您好!我是AI智能客服,很高兴为您服务!\n\n我可以帮您:\n• 解答产品相关问题\n• 处理订单问题\n• 提供技术支持\n• 推荐相关商品\n\n请告诉我您需要什么帮助?',
|
||||||
timestamp: Date.now() - 60000,
|
timestamp: Date.now() - 60000,
|
||||||
actions: [
|
actions: [
|
||||||
{ type: 'like', count: 0 },
|
{ type: 'like', icon: 'icon-dianzan1', text: '喜欢', count: 0 },
|
||||||
{ type: 'dislike', count: 0 }
|
{ type: 'dislike', icon: 'icon-dianzan1', text: '不喜欢', count: 0 }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
userAvatar: '/static/images/user-avatar.png',
|
|
||||||
aiAvatar: '/static/images/ai-avatar.png',
|
|
||||||
|
|
||||||
// 是否显示底部tabBar
|
// --- 系统导航栏/状态栏相关 ---
|
||||||
showTabBar: false,
|
navBarHeight: 44, // uni-page-head 系统导航栏,默认高度为44px
|
||||||
tabBarHeight: '56px',
|
statusBarHeight: 0, // 状态栏高度,默认0
|
||||||
|
|
||||||
// 页面头部高度
|
// --- 自定义导航栏 ---
|
||||||
headerHeight: 0,
|
showCustomNavbar: true, // 是否显示自定义导航栏
|
||||||
|
showBackButton: false, // 是否显示返回按钮
|
||||||
|
showMenuButton: true, // 是否显示设置菜单按钮
|
||||||
|
|
||||||
// uni-page-head 导航栏高度
|
// --- 聊天内容区域 ---
|
||||||
navBarHeight: 44 // 默认高度为44px
|
scrollViewHeight: '0px',
|
||||||
|
|
||||||
|
|
||||||
|
// 事件处理器引用(用于清理)
|
||||||
|
safeEventHandlers: new Map()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
// 获取底部栏高度
|
...mapGetters([
|
||||||
computedTabBarHeight() {
|
'globalAIAgentConfig'
|
||||||
return this.tabBarHeight || '56px'
|
]),
|
||||||
|
|
||||||
|
/// ---- 关于AI客服的配置,支持远程配置 ----
|
||||||
|
userAvatar() {
|
||||||
|
return this.globalAIAgentConfig?.userAvatar || '/static/images/user-avatar.png'
|
||||||
},
|
},
|
||||||
|
aiAvatar() {
|
||||||
// 计算聊天内容区域高度
|
return this.globalAIAgentConfig?.aiAvatar || '/static/images/ai-avatar.png'
|
||||||
chatContentHeight() {
|
},
|
||||||
if (this.headerHeight === 0 || this.navBarHeight === 0) {
|
/// ---- others ----
|
||||||
return 'calc(100vh - 120rpx)' // 默认高度
|
containerHeight() {
|
||||||
|
return `calc(100vh - ${this.navBarHeight + this.statusBarHeight}px)`
|
||||||
|
},
|
||||||
|
wrapperPageStyle() {
|
||||||
|
// #ifdef H5
|
||||||
|
return {
|
||||||
|
top: this.navBarHeight + 'px'
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
wrapperChatContentStyle() {
|
||||||
|
return {
|
||||||
|
height: this.containerHeight,
|
||||||
|
paddingTop: this.showCustomNavbar ? '0' : (this.navHeight + 'px')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 直接使用获取到的像素高度进行计算
|
|
||||||
const headerHeightPx = this.headerHeight
|
|
||||||
const tabBarHeightPx = this.showTabBar ? parseInt(this.tabBarHeight) : 0
|
|
||||||
|
|
||||||
// 计算剩余高度,使用px单位确保一致性(页面已经设置了top值,不需要再减去navBar高度)
|
|
||||||
const remainingHeight = `calc(100vh - ${headerHeightPx}px - ${tabBarHeightPx}px)`
|
|
||||||
return remainingHeight
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onLoad() {
|
async onLoad(options) {
|
||||||
// 设置页面标题
|
await this.initPage(options)
|
||||||
this.$langConfig.title('AI智能客服');
|
|
||||||
// 页面加载时初始化
|
|
||||||
this.initChat()
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onReady() {
|
async onReady() {
|
||||||
// 页面渲染完成后获取头部高度
|
await this.initNavigation()
|
||||||
// 使用更长的延迟确保DOM完全渲染完成
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.$forceUpdate()
|
|
||||||
this.getNavBarHeight()
|
|
||||||
this.getHeaderHeight()
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onShow() {
|
onShow() {
|
||||||
// 页面显示时再次获取高度,确保布局正确
|
|
||||||
this.$nextTick(() => {
|
},
|
||||||
this.$forceUpdate()
|
|
||||||
this.getNavBarHeight()
|
onHide() {
|
||||||
this.getHeaderHeight()
|
|
||||||
|
},
|
||||||
|
|
||||||
|
onUnload() {
|
||||||
|
this.cleanup()
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
// ========== 安全事件处理 ==========
|
||||||
|
setupSafeEventListeners() {
|
||||||
|
// 使用 EventSafety 包装事件处理器
|
||||||
|
const safeHandlers = {
|
||||||
|
serviceRequest: EventSafety.wrapEventHandler(
|
||||||
|
this.handleServiceRequest.bind(this),
|
||||||
|
{ onError: this.handleEventError.bind(this) }
|
||||||
|
),
|
||||||
|
navigationRequest: EventSafety.wrapEventHandler(
|
||||||
|
this.handleNavigationRequest.bind(this),
|
||||||
|
{ onError: this.handleEventError.bind(this) }
|
||||||
|
),
|
||||||
|
componentInteraction: EventSafety.wrapEventHandler(
|
||||||
|
this.handleComponentInteraction.bind(this),
|
||||||
|
{ onError: this.handleEventError.bind(this) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册事件监听
|
||||||
|
this.$on('service.requestComponentInfo', safeHandlers.serviceRequest)
|
||||||
|
this.$on('navigation.requestInfo', safeHandlers.navigationRequest)
|
||||||
|
this.$on('component.interaction', safeHandlers.componentInteraction)
|
||||||
|
|
||||||
|
// 保存处理器引用用于清理
|
||||||
|
this.safeEventHandlers.set('serviceRequest', safeHandlers.serviceRequest)
|
||||||
|
this.safeEventHandlers.set('navigationRequest', safeHandlers.navigationRequest)
|
||||||
|
this.safeEventHandlers.set('componentInteraction', safeHandlers.componentInteraction)
|
||||||
|
},
|
||||||
|
|
||||||
|
setupNavigationEvents() {
|
||||||
|
// 监听窗口大小变化
|
||||||
|
uni.onWindowResize((res) => {
|
||||||
|
console.log('窗口大小变化:', res.size)
|
||||||
|
this.calculateScrollViewHeight()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
// ========== 事件处理方法 ==========
|
||||||
// 获取页面头部高度
|
|
||||||
getHeaderHeight(retryCount = 0) {
|
|
||||||
// 使用Vue ref方式获取元素高度
|
|
||||||
const query = uni.createSelectorQuery().in(this)
|
|
||||||
query.select(this.$refs.pageHeader).boundingClientRect(data => {
|
|
||||||
if (data && data.height > 0) {
|
|
||||||
this.headerHeight = data.height
|
|
||||||
console.log('页面头部高度:', this.headerHeight + 'px')
|
|
||||||
|
|
||||||
// 高度获取成功后,强制更新视图
|
async handleServiceRequest(event) {
|
||||||
this.$forceUpdate()
|
console.log('处理服务请求:', EventSafety.extractEventData(event))
|
||||||
|
|
||||||
// 延迟一段时间后再次检查,确保布局稳定
|
// 安全地检查事件目标
|
||||||
setTimeout(() => {
|
if (event.matches('.service-component') ||
|
||||||
this.checkLayoutStability()
|
event.detail?.componentType === 'service') {
|
||||||
}, 100)
|
await this.processServiceRequest(event)
|
||||||
} else {
|
|
||||||
// 如果获取失败,重试最多3次
|
|
||||||
if (retryCount < 3) {
|
|
||||||
console.log('获取头部高度失败,第' + (retryCount + 1) + '次重试')
|
|
||||||
setTimeout(() => {
|
|
||||||
this.getHeaderHeight(retryCount + 1)
|
|
||||||
}, 300)
|
|
||||||
} else {
|
|
||||||
console.log('获取头部高度失败,使用默认高度')
|
|
||||||
// 使用默认高度
|
|
||||||
this.headerHeight = 60 // 假设默认高度为60px
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}).exec()
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// 检查布局稳定性
|
async handleNavigationRequest(event) {
|
||||||
checkLayoutStability() {
|
console.log('处理导航请求:', event.type)
|
||||||
// 再次获取头部高度,确保布局稳定
|
|
||||||
const query = uni.createSelectorQuery().in(this)
|
// 提供导航信息
|
||||||
query.select(this.$refs.pageHeader).boundingClientRect(data => {
|
this.emitNavigationInfo(event)
|
||||||
if (data && data.height > 0 && data.height !== this.headerHeight) {
|
|
||||||
console.log('布局发生变化,更新高度:', data.height + 'px')
|
|
||||||
this.headerHeight = data.height
|
|
||||||
this.$forceUpdate()
|
|
||||||
}
|
|
||||||
}).exec()
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取uni-page-head导航栏高度
|
handleComponentInteraction(event) {
|
||||||
getNavBarHeight(retryCount = 0) {
|
console.log('处理组件交互:', event.detail)
|
||||||
// 使用选择器获取uni-page-head元素高度
|
|
||||||
const query = uni.createSelectorQuery().in(this)
|
|
||||||
query.select('.uni-page-head').boundingClientRect(data => {
|
|
||||||
if (data && data.height > 0) {
|
|
||||||
this.navBarHeight = data.height
|
|
||||||
console.log('uni-page-head高度:', this.navBarHeight + 'px')
|
|
||||||
|
|
||||||
// 高度获取成功后,强制更新视图
|
// 安全地处理组件交互
|
||||||
this.$forceUpdate()
|
this.processComponentInteraction(event)
|
||||||
} else {
|
},
|
||||||
// 如果获取失败,重试最多3次
|
|
||||||
if (retryCount < 3) {
|
handleEventError(error, event) {
|
||||||
console.log('获取uni-page-head高度失败,第' + (retryCount + 1) + '次重试')
|
console.error('事件处理错误:', {
|
||||||
setTimeout(() => {
|
error: error.message,
|
||||||
this.getNavBarHeight(retryCount + 1)
|
eventType: event?.type,
|
||||||
}, 300)
|
component: this.$options.name
|
||||||
} else {
|
})
|
||||||
console.log('获取uni-page-head高度失败,使用默认高度')
|
|
||||||
// 使用默认高度(uni-app导航栏默认高度通常为44px)
|
this.showError('操作失败,请重试')
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========== 初始化页面 ==========
|
||||||
|
async initPage(options = {}) {
|
||||||
|
this.$langConfig.title('AI智能客服');
|
||||||
|
this.initChat()
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
// 初始化导航栏相关配置
|
||||||
|
async initNavigation() {
|
||||||
|
try {
|
||||||
|
// 获取导航栏高度
|
||||||
|
this.navBarHeight = await navigationHelper.getNavigationHeight(this, {forceRefresh: false})
|
||||||
|
|
||||||
|
// 获取状态栏高度
|
||||||
|
this.statusBarHeight = navigationHelper.getStatusBarHeight()
|
||||||
|
|
||||||
|
// 计算滚动视图高度
|
||||||
|
this.calculateScrollViewHeight()
|
||||||
|
|
||||||
|
// 注册导航相关事件
|
||||||
|
this.setupNavigationEvents()
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('初始化导航栏失败:', error)
|
||||||
|
this.setFallbackNavigationValues()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 计算滚动视图高度
|
||||||
|
calculateScrollViewHeight() {
|
||||||
|
const safeArea = navigationHelper.getSafeAreaInsets()
|
||||||
|
const bottomInset = safeArea.bottom || 0
|
||||||
|
const inputHeight = 120 // 输入区域高度
|
||||||
|
|
||||||
|
this.scrollViewHeight = `calc(100vh - ${this.navBarHeight + this.statusBarHeight + inputHeight + bottomInset}px)`
|
||||||
|
},
|
||||||
|
|
||||||
|
// 备用高度设置
|
||||||
|
setFallbackNavigationValues() {
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
this.navBarHeight = 44
|
this.navBarHeight = 44
|
||||||
}
|
this.statusBarHeight = 20
|
||||||
}
|
// #endif
|
||||||
}).exec()
|
|
||||||
},
|
|
||||||
|
|
||||||
// 获取uni-page-head导航栏高度
|
// #ifdef H5
|
||||||
getNavBarHeight(retryCount = 0) {
|
|
||||||
// 使用选择器获取uni-page-head元素高度
|
|
||||||
const query = uni.createSelectorQuery().in(this)
|
|
||||||
query.select('.uni-page-head').boundingClientRect(data => {
|
|
||||||
if (data && data.height > 0) {
|
|
||||||
this.navBarHeight = data.height
|
|
||||||
console.log('uni-page-head高度:', this.navBarHeight + 'px')
|
|
||||||
|
|
||||||
// 高度获取成功后,强制更新视图
|
|
||||||
this.$forceUpdate()
|
|
||||||
} else {
|
|
||||||
// 如果获取失败,重试最多3次
|
|
||||||
if (retryCount < 3) {
|
|
||||||
console.log('获取uni-page-head高度失败,第' + (retryCount + 1) + '次重试')
|
|
||||||
setTimeout(() => {
|
|
||||||
this.getNavBarHeight(retryCount + 1)
|
|
||||||
}, 300)
|
|
||||||
} else {
|
|
||||||
console.log('获取uni-page-head高度失败,使用默认高度')
|
|
||||||
// 使用默认高度(uni-app导航栏默认高度通常为44px)
|
|
||||||
this.navBarHeight = 44
|
this.navBarHeight = 44
|
||||||
}
|
this.statusBarHeight = 0
|
||||||
}
|
// #endif
|
||||||
}).exec()
|
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
this.navBarHeight = 88
|
||||||
|
this.statusBarHeight = 44
|
||||||
|
// #endif
|
||||||
},
|
},
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
// 清理事件监听器
|
||||||
|
this.safeEventHandlers.forEach((handler, eventType) => {
|
||||||
|
this.$off(eventType, handler)
|
||||||
|
})
|
||||||
|
this.safeEventHandlers.clear()
|
||||||
|
|
||||||
|
console.log('组件清理完成')
|
||||||
|
},
|
||||||
|
|
||||||
|
showError(message) {
|
||||||
|
uni.showToast({
|
||||||
|
title: message,
|
||||||
|
icon: 'none',
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
// 初始化聊天
|
// 初始化聊天
|
||||||
initChat() {
|
initChat() {
|
||||||
// 可以在这里加载历史消息
|
// 可以在这里加载历史消息
|
||||||
@@ -465,19 +531,19 @@ export default {
|
|||||||
|
|
||||||
/* 页面样式 */
|
/* 页面样式 */
|
||||||
.ai-chat-page {
|
.ai-chat-page {
|
||||||
height: calc(100vh - var(--nav-bar-height, 44px));
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background-color: #f8f8f8;
|
background-color: #f8f8f8;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 页面头部 */
|
/* 自定义导航头部 */
|
||||||
.page-header {
|
.custom-navbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -487,6 +553,8 @@ export default {
|
|||||||
|
|
||||||
.header-left {
|
.header-left {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
.back-btn {
|
.back-btn {
|
||||||
width: 60rpx;
|
width: 60rpx;
|
||||||
@@ -502,6 +570,11 @@ export default {
|
|||||||
color: #666;
|
color: #666;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
width: 120rpx;
|
||||||
|
height: 60rpx;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-center {
|
.header-center {
|
||||||
@@ -525,6 +598,8 @@ export default {
|
|||||||
|
|
||||||
.header-right {
|
.header-right {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
.menu-btn {
|
.menu-btn {
|
||||||
width: 60rpx;
|
width: 60rpx;
|
||||||
@@ -540,6 +615,11 @@ export default {
|
|||||||
color: #666;
|
color: #666;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
width: 120rpx;
|
||||||
|
height: 60rpx;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -549,6 +629,10 @@ export default {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,8 @@ const store = new Vuex.Store({
|
|||||||
goods: '',
|
goods: '',
|
||||||
head: '',
|
head: '',
|
||||||
store: '',
|
store: '',
|
||||||
article: ''
|
article: '',
|
||||||
|
aiAgent: ''
|
||||||
},
|
},
|
||||||
cartList: {},
|
cartList: {},
|
||||||
cartIds: [],
|
cartIds: [],
|
||||||
@@ -58,6 +59,8 @@ const store = new Vuex.Store({
|
|||||||
cartMoney: 0,
|
cartMoney: 0,
|
||||||
cartChange: 0,
|
cartChange: 0,
|
||||||
bottomNavHidden: false, // 底部导航是否隐藏,true:隐藏,false:显示
|
bottomNavHidden: false, // 底部导航是否隐藏,true:隐藏,false:显示
|
||||||
|
aiUnreadCount: 10, // AI未读消息数量
|
||||||
|
globalAIAgentConfig: null, // AI客服配置
|
||||||
globalStoreConfig: null, // 门店配置
|
globalStoreConfig: null, // 门店配置
|
||||||
globalStoreInfo: null, // 门店信息
|
globalStoreInfo: null, // 门店信息
|
||||||
defaultStoreInfo: null, // 默认门店
|
defaultStoreInfo: null, // 默认门店
|
||||||
@@ -137,6 +140,10 @@ const store = new Vuex.Store({
|
|||||||
setBottomNavHidden(state, value) {
|
setBottomNavHidden(state, value) {
|
||||||
state.bottomNavHidden = value;
|
state.bottomNavHidden = value;
|
||||||
},
|
},
|
||||||
|
setGlobalAIAgentConfig(state, value) {
|
||||||
|
state.globalAIAgentConfig = value;
|
||||||
|
uni.setStorageSync('globalAIAgentConfig', value); // 初始化数据调用
|
||||||
|
},
|
||||||
setGlobalStoreConfig(state, value) {
|
setGlobalStoreConfig(state, value) {
|
||||||
state.globalStoreConfig = value;
|
state.globalStoreConfig = value;
|
||||||
uni.setStorageSync('globalStoreConfig', value); // 初始化数据调用
|
uni.setStorageSync('globalStoreConfig', value); // 初始化数据调用
|
||||||
@@ -193,8 +200,18 @@ const store = new Vuex.Store({
|
|||||||
},
|
},
|
||||||
setCartMoney(state, value) {
|
setCartMoney(state, value) {
|
||||||
state.cartMoney = value;
|
state.cartMoney = value;
|
||||||
|
},
|
||||||
|
// 设置AI未读消息数量
|
||||||
|
setAiUnreadCount(state, value) {
|
||||||
|
state.aiUnreadCount = value;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
getters: {
|
||||||
|
// AI智能助手配置
|
||||||
|
globalAIAgentConfig: state => state.globalAIAgentConfig,
|
||||||
|
// AI未读消息数量
|
||||||
|
aiUnreadCount: state => state.aiUnreadCount,
|
||||||
|
},
|
||||||
actions: {
|
actions: {
|
||||||
init() {
|
init() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -220,6 +237,8 @@ const store = new Vuex.Store({
|
|||||||
|
|
||||||
this.commit('setMapConfig', data.map_config);
|
this.commit('setMapConfig', data.map_config);
|
||||||
|
|
||||||
|
this.commit('setGlobalAIAgentConfig', data.ai_agent_config);
|
||||||
|
|
||||||
this.commit('setGlobalStoreConfig', data.store_config);
|
this.commit('setGlobalStoreConfig', data.store_config);
|
||||||
|
|
||||||
//联系我们
|
//联系我们
|
||||||
|
|||||||
Reference in New Issue
Block a user