From 2a0489d4b26b26c6c241344fb30ebd2c0d8821a2 Mon Sep 17 00:00:00 2001
From: jinhhanhan <1683105490@qq.com>
Date: Wed, 17 Dec 2025 11:17:50 +0800
Subject: [PATCH] =?UTF-8?q?chore=EF=BC=9A=E8=83=BD=E6=AD=A3=E5=B8=B8?=
=?UTF-8?q?=E8=BF=90=E8=A1=8C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.gitignore | 1 +
common/js/ai-service.js | 489 +++
common/js/config.js | 10 +-
common/js/event-safety.js | 112 +
common/js/navigation.js | 288 ++
components/ai-chat-message/README.md | 271 ++
.../ai-chat-message/ai-chat-message.json | 6 +
.../ai-chat-message/ai-chat-message.vue | 2766 +++++++++++++++++
components/ai-chat-message/demo.vue | 288 ++
components/hover-nav/hover-nav.vue | 320 +-
lang/zh-cn/ai/ai-chat.js | 4 +
manifest.json | 2 +-
pages.json | 7 +
pages/dify-chat/dify-chat.vue | 20 +
pages_tool/ai-chat/index.json | 10 +
pages_tool/ai-chat/index.vue | 650 ++++
pages_tool/kefu-demo/index.json | 6 +
pages_tool/kefu-demo/index.vue | 704 +++++
project.private.config.json | 23 +
scripts/iconfontcss-generate-preview.js | 57 +
store/index.js | 21 +-
21 files changed, 5928 insertions(+), 127 deletions(-)
create mode 100644 common/js/ai-service.js
create mode 100644 common/js/event-safety.js
create mode 100644 common/js/navigation.js
create mode 100644 components/ai-chat-message/README.md
create mode 100644 components/ai-chat-message/ai-chat-message.json
create mode 100644 components/ai-chat-message/ai-chat-message.vue
create mode 100644 components/ai-chat-message/demo.vue
create mode 100644 lang/zh-cn/ai/ai-chat.js
create mode 100644 pages/dify-chat/dify-chat.vue
create mode 100644 pages_tool/ai-chat/index.json
create mode 100644 pages_tool/ai-chat/index.vue
create mode 100644 pages_tool/kefu-demo/index.json
create mode 100644 pages_tool/kefu-demo/index.vue
create mode 100644 project.private.config.json
create mode 100644 scripts/iconfontcss-generate-preview.js
diff --git a/.gitignore b/.gitignore
index 14d7d69..db83a01 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,4 @@
/.hbuilderx
/.idea
/node_modules
+/iconfont-preview.html
diff --git a/common/js/ai-service.js b/common/js/ai-service.js
new file mode 100644
index 0000000..7b47481
--- /dev/null
+++ b/common/js/ai-service.js
@@ -0,0 +1,489 @@
+import Config from './config.js'
+import http from './http.js'
+import store from '@/store/index.js'
+let currentConversationId = null;
+const CONVERSATION_KEY = 'ai_conversation_id';
+
+// 初始化时从本地读取
+try {
+ const saved = uni.getStorageSync(CONVERSATION_KEY);
+ if (saved) {
+ currentConversationId = saved;
+ }
+} catch (e) {
+ console.warn('读取会话ID失败:', e);
+}
+export default {
+ /**
+ * 发送消息到Dify API
+ * @param {string} message 用户消息内容
+ * @param {Object} options 配置选项
+ * @returns {Promise}
+ */
+ async sendMessage(message, options = {}) {
+ try {
+ const aiConfig = store.getters.globalAIKefuConfig
+ const params = {
+ url: '/api/kefu/chat',
+ data: {
+ user_id: store.state.memberInfo?.id || 'anonymous',
+ stream: false,
+ inputs: {},
+ query: message,
+ response_mode: options.stream ? 'streaming' : 'blocking',
+ user: store.state.memberInfo?.id || 'anonymous',
+ conversation_id: this.getConversationId() || ''
+ },
+ header: {
+ 'Content-Type': 'application/json'
+ }
+ }
+ console.log('Sending request to:', params.url);
+ console.log('Request data:', params.data);
+
+ const response = await http.sendRequest({
+ ...params,
+ async: false
+ })
+
+ const result = this.handleResponse(response, options);
+ if (result.conversationId) {
+ this.setConversationId(result.conversationId);
+ }
+ return result;
+ } catch (error) {
+ console.error('Dify API请求失败:', error)
+ throw new Error('AI服务暂时不可用,请稍后重试')
+ }
+ },
+
+ /**
+ * 流式消息处理
+ */
+ async sendStreamMessage(message, onChunk, onComplete) {
+ try {
+ return this.sendHttpStream(message, onChunk, onComplete)
+ } catch (error) {
+ console.error('流式消息发送失败:', error)
+ throw error
+ }
+ },
+
+ /**
+ * EventSource 流式聊天
+ */
+ chatWithEventSource(message, conversationId = '', onMessage, onComplete, onError) {
+ if (window.currentEventSource) {
+ window.currentEventSource.close();
+ }
+ const params = new URLSearchParams({
+ uniacid: store.state.uniacid || '1',
+ user_id: store.state.memberInfo?.id || '123456',
+ query: message,
+ conversation_id: conversationId || '',
+ stream: 'true'
+ });
+ const url = `/api/kefu/chat?${params.toString()}`;
+ try {
+ const eventSource = new EventSource(url);
+ window.currentEventSource = eventSource;
+ let aiMessage = '';
+ eventSource.addEventListener('message', (event) => {
+ try {
+ const data = JSON.parse(event.data);
+ if (data.event === 'message') {
+ aiMessage += data.answer || '';
+ if (onMessage) onMessage(data.answer || '');
+ }
+ if (data.event === 'message_end') {
+ if (onComplete) onComplete({
+ conversation_id: data.conversation_id,
+ message: aiMessage
+ });
+ }
+ if (data.conversation_id) {
+ conversationId = data.conversation_id;
+ }
+ } catch (error) {
+ console.error('解析消息失败:', error);
+ }
+ });
+ eventSource.addEventListener('done', (event) => {
+ try {
+ const data = JSON.parse(event.data);
+ if (onComplete) onComplete(data);
+ } catch (error) {
+ console.error('解析完成事件失败:', error);
+ }
+ });
+ eventSource.addEventListener('close', (event) => {
+ try {
+ const data = JSON.parse(event.data);
+ console.log('连接正常结束:', data);
+ } catch (error) {
+ console.error('解析关闭事件失败:', error);
+ }
+ window.currentEventSource = null;
+ });
+ eventSource.addEventListener('error', (error) => {
+ console.error('EventSource错误:', error);
+ if (onError) onError({ error: 'EventSource连接错误' });
+ window.currentEventSource = null;
+ });
+ return eventSource;
+ } catch (error) {
+ console.error('创建EventSource失败:', error);
+ if (onError) onError({ error: error.message });
+ return null;
+ }
+ },
+
+ /**
+ * Fetch API 流式聊天
+ */
+ async chatWithFetchStream(message, conversationId = '', onMessage, onComplete, onError) {
+ try {
+ let requestData;
+ let headers = {
+ 'Accept': 'text/event-stream'
+ };
+ requestData = {
+ uniacid: store.state.uniacid || '1',
+ user_id: store.state.memberInfo?.id || '123456',
+ query: message,
+ conversation_id: conversationId || '',
+ stream: true,
+ inputs: {},
+ response_mode: 'streaming',
+ user: store.state.memberInfo?.id || '123456'
+ };
+ headers['Content-Type'] = 'application/json';
+ const response = await fetch('/api/kefu/chat', {
+ method: 'POST',
+ headers: headers,
+ body: JSON.stringify(requestData)
+ });
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ if (!response.body) {
+ throw new Error('响应体不可用');
+ }
+ const reader = response.body.getReader();
+ const decoder = new TextDecoder('utf-8');
+ let buffer = '';
+ let aiMessage = '';
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+ buffer += decoder.decode(value, { stream: true });
+ let lineEnd;
+ while ((lineEnd = buffer.indexOf('\n')) !== -1) {
+ const line = buffer.substring(0, lineEnd);
+ buffer = buffer.substring(lineEnd + 1);
+ if (line.startsWith('data: ')) {
+ try {
+ const data = JSON.parse(line.substring(6));
+ if (data.event === 'message' || data.event === 'text_message') {
+ const textContent = data.answer || data.text || '';
+ aiMessage += textContent;
+ if (onMessage) onMessage(textContent);
+ } else if (data.event === 'message_end' || data.event === 'done') {
+ if (onComplete) onComplete({
+ conversation_id: data.conversation_id,
+ message: aiMessage,
+ ...data
+ });
+ } else if (data.event === 'error' && onError) {
+ onError(data);
+ }
+ if (data.conversation_id) {
+ conversationId = data.conversation_id;
+ }
+ } catch (e) {
+ console.warn('解析流式数据失败:', e);
+ }
+ }
+ }
+ }
+ if (buffer.startsWith('data: ')) {
+ try {
+ const data = JSON.parse(buffer.substring(6));
+ if ((data.event === 'message' || data.event === 'text_message') && onMessage) {
+ const textContent = data.answer || data.text || '';
+ onMessage(textContent);
+ } else if ((data.event === 'message_end' || data.event === 'done') && onComplete) {
+ onComplete({
+ conversation_id: data.conversation_id,
+ message: aiMessage,
+ ...data
+ });
+ }
+ } catch (e) {
+ console.warn('解析剩余数据失败:', e);
+ }
+ }
+ } catch (error) {
+ console.error('Fetch流式聊天请求失败:', error);
+ if (onError) onError({ error: error.message });
+ }
+ },
+
+ /**
+ * HTTP流式请求(修复conversationId未定义)
+ */
+ async sendHttpStream(message, onChunk, onComplete) {
+ const params = {
+ url: '/api/kefu/chat',
+ data: {
+ query: message,
+ conversation_id: '',
+ user_id: store.state.memberInfo?.id || 'anonymous',
+ stream: true,
+ uniacid: store.state.uniacid,
+ inputs: {},
+ response_mode: 'streaming',
+ user: store.state.memberInfo?.id || 'anonymous'
+ },
+ header: {
+ 'Content-Type': 'application/json'
+ }
+ }
+ // #ifdef H5
+ try {
+ const url = Config.baseUrl + `/api/kefu/chat`;
+ const response = await fetch(url, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Accept': 'text/event-stream',
+ },
+ body: JSON.stringify({
+ uniacid: store.state.uniacid || '1',
+ inputs: {},
+ query: message,
+ response_mode: 'streaming',
+ user_id: store.state.memberInfo?.id || 'anonymous'
+ })
+ })
+ const reader = response.body.getReader()
+ const decoder = new TextDecoder()
+ let content = ''
+ let buffer = ''
+ // 修复:声明conversationId变量
+ let conversationId = '';
+
+ function processStreamData(buffer, callback) {
+ const lines = buffer.split('\n');
+ buffer = lines.pop() || '';
+ lines.forEach(line => {
+ line = line.trim();
+ if (!line) return;
+ if (line.startsWith('data:')) {
+ const dataPart = line.slice(5).trim();
+ if (dataPart) {
+ try {
+ const data = JSON.parse(dataPart);
+ if (data.event === 'message') {
+ callback(data.answer || '');
+ }
+ if (data.conversation_id) {
+ conversationId = data.conversation_id;
+ }
+ if (data.event === 'message_end') {
+ console.log('对话完成');
+ }
+ } catch (error) {
+ console.error('解析流式数据失败:', error);
+ }
+ }
+ }
+ });
+ return buffer;
+ }
+
+ while (true) {
+ const { done, value } = await reader.read()
+ if (done) break
+ buffer += decoder.decode(value, { stream: true });
+ buffer = processStreamData(buffer, (newData) => {
+ if (newData) {
+ content += newData;
+ if (onChunk) onChunk(newData);
+ }
+ });
+ }
+
+ if (onComplete) onComplete({ content, conversation_id: conversationId });
+ return { content, conversation_id: conversationId };
+ } catch (error) {
+ console.error('HTTP流式请求失败:', error)
+ throw error
+ }
+ // #endif
+ // #ifndef H5
+ const response = await http.sendRequest({
+ ...params,
+ async: false
+ })
+ if (response.success && response.data) {
+ const content = response.data
+ const chunkSize = 3
+ let index = 0
+ const streamInterval = setInterval(() => {
+ if (index < content.length) {
+ const chunk = content.substring(index, index + chunkSize)
+ index += chunkSize
+ if (onChunk) onChunk(chunk)
+ } else {
+ clearInterval(streamInterval)
+ if (onComplete) onComplete(content)
+ }
+ }, 100)
+ }
+ // #endif
+ },
+
+ /**
+ * 处理API响应
+ */
+ handleResponse(response, options) {
+ if (response.code === 0 || response.success) {
+ return {
+ success: true,
+ content: response.data?.answer || response.data?.content || response.data?.reply|| response.data,
+ conversationId: response.data?.conversation_id,
+ messageId: response.data?.message_id
+ }
+ } else {
+ throw new Error(response.message || 'AI服务返回错误')
+ }
+ },
+
+ /**
+ * 生成会话ID
+ */
+ async generateConversationId() {
+ const params = {
+ url: '/api/kefu/createConversation',
+ data: {
+ uniacid: store.state.uniacid,
+ user_id: store.state.memberInfo?.id || 'anonymous',
+ member_id: store.state.memberInfo?.id || '',
+ },
+ header: {
+ 'Content-Type': 'application/json'
+ }
+ }
+ const response = await http.sendRequest({
+ ...params,
+ async: false
+ })
+ return this.handleResponse(response)
+ },
+
+ /**
+ * 获取AI服务状态
+ */
+ async getServiceStatus() {
+ try {
+ const response = await http.sendRequest({
+ url: '/api/kefu/health',
+ async: false,
+ data: {}
+ })
+ const available = [response?.data?.status, response?.data?.components?.ai_service_config?.status].filter(item => item === 'healthy').length > 0;
+ return {
+ available,
+ reason: available ? '服务正常' : '服务异常'
+ }
+ } catch (error) {
+ return {
+ available: false,
+ reason: '服务检查失败'
+ }
+ }
+ },
+
+ /**
+ * 清除会话历史
+ */
+ async clearConversation(conversationId) {
+ try {
+ const response = await http.sendRequest({
+ url: '/api/kefu/clear-conversation',
+ data: { conversation_id: conversationId },
+ async: false
+ })
+ return response.success
+ } catch (error) {
+ console.error('清除会话失败:', error)
+ return false
+ }
+ },
+ getConversationId() {
+ return currentConversationId;
+ },
+
+ setConversationId(id) {
+ if (id && id !== currentConversationId) {
+ currentConversationId = id;
+ try {
+ uni.setStorageSync(CONVERSATION_KEY, id);
+ } catch (e) {
+ console.error('保存会话ID失败:', e);
+ }
+ }
+ },
+
+ clearConversationId() {
+ currentConversationId = null;
+ try {
+ uni.removeStorageSync(CONVERSATION_KEY);
+ } catch (e) {
+ console.error('清除会话ID失败:', e);
+ }
+ },
+
+ /**
+ * 获取会话历史
+ */
+ async getConversationHistory(params = {}) {
+ try {
+ if (!params.conversation_id) {
+ throw new Error('会话ID(conversation_id)是必填参数');
+ }
+ const requestData = {
+ uniacid: params.uniacid || store.state.uniacid || '1',
+ conversation_id: params.conversation_id,
+ user_id: params.user_id || store.state.memberInfo?.id || 'anonymous',
+ limit: params.limit || 20,
+ offset: params.offset || 0,
+ member_id: params.member_id || store.state.memberInfo?.id || '',
+ token: params.token || ''
+ };
+ const response = await http.sendRequest({
+ url: '/api/kefu/getHistory',
+ data: requestData,
+ header: {
+ 'Content-Type': 'application/json'
+ },
+ async: false
+ });
+ if (response.code === 0 || response.success) {
+ return {
+ success: true,
+ data: response.data,
+ messages: response.data?.messages || [],
+ total: response.data?.total || 0,
+ page_info: response.data?.page_info || { limit: 20, offset: 0 }
+ };
+ } else {
+ throw new Error(response.message || '获取会话历史失败');
+ }
+ } catch (error) {
+ console.error('获取会话历史失败:', error);
+ throw new Error(error.message || '获取会话历史时发生错误,请稍后重试');
+ }
+ }
+}
\ No newline at end of file
diff --git a/common/js/config.js b/common/js/config.js
index e97d99d..a8b5e0c 100644
--- a/common/js/config.js
+++ b/common/js/config.js
@@ -14,16 +14,18 @@ try {
// 调试版本,配置说明
const devCfg = {
// 商户ID
- uniacid: 460, //825
+ uniacid: 1, //825
//api请求地址
- baseUrl: 'https://xcx30.5g-quickapp.com/',
+ baseUrl: 'https://dev.aigc-quickapp.com/',
+ // baseUrl: 'http://localhost:8010/',
// 图片域名
- imgDomain: 'https://xcx30.5g-quickapp.com/',
+ imgDomain: 'https://dev.aigc-quickapp.com/',
+ //imgDomain: 'http://localhost:8010/',
// H5端域名
- h5Domain: 'https://xcx30.5g-quickapp.com/',
+ h5Domain: 'https://dev.aigc-quickapp.com/',
// // api请求地址
// baseUrl: 'https://tsaas.liveplatform.cn/',
diff --git a/common/js/event-safety.js b/common/js/event-safety.js
new file mode 100644
index 0000000..7798f8e
--- /dev/null
+++ b/common/js/event-safety.js
@@ -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
+ }
+}
\ No newline at end of file
diff --git a/common/js/navigation.js b/common/js/navigation.js
new file mode 100644
index 0000000..0550cd0
--- /dev/null
+++ b/common/js/navigation.js
@@ -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
\ No newline at end of file
diff --git a/components/ai-chat-message/README.md b/components/ai-chat-message/README.md
new file mode 100644
index 0000000..0f999a8
--- /dev/null
+++ b/components/ai-chat-message/README.md
@@ -0,0 +1,271 @@
+# AI智能客服组件
+
+一个功能完整的AI智能客服对话组件,支持多种消息类型和交互功能。
+
+## 功能特性
+
+- ✅ 支持对话上下文管理
+- ✅ 支持多种消息类型:文本、Markdown、文件、音频、视频、链接、商品卡片
+- ✅ 支持语音输入和录音
+- ✅ 支持图片、文件、位置等附件发送
+- ✅ 支持消息操作按钮(点赞、踩等)
+- ✅ 支持历史消息加载
+- ✅ 响应式设计,适配多端
+
+## 安装使用
+
+### 1. 引入组件
+
+在 `pages.json` 中注册组件:
+
+```json
+{
+ "usingComponents": {
+ "ai-chat-message": "/components/ai-chat-message/ai-chat-message"
+ }
+}
+```
+
+### 2. 在页面中使用
+
+```vue
+
+