diff --git a/App.vue b/App.vue index 576a6e8..11994f9 100644 --- a/App.vue +++ b/App.vue @@ -3,6 +3,8 @@ // 多语言引入并初始化 import i18n from './locale/index'; const env = process.env.NODE_ENV; + + import socket from '@/utils/socket'; export default { globalData: { data: { @@ -3107,6 +3109,12 @@ onLaunch(params) { //隐藏系统tabbar this.globalData.system_hide_tabbar(); + + const userInfo = uni.getStorageSync('cache_shop_user_info_key'); + if (userInfo.id) { + // 应用启动时连接socket + socket.connectSocket(); + } }, // 启动,或从后台进入前台显示 diff --git a/components/diy/footer.vue b/components/diy/footer.vue index d62d8aa..ca5dc54 100644 --- a/components/diy/footer.vue +++ b/components/diy/footer.vue @@ -95,6 +95,7 @@ // 角标链接定义 let badge_arr = { '/pages/cart/cart': 'cart', + '/pages/user/user': 'user', '/pages/cart-page/cart-page': 'cart', }; for (var i in nav_content) { diff --git a/pages.json b/pages.json index 5b4b358..86d2773 100644 --- a/pages.json +++ b/pages.json @@ -12,6 +12,14 @@ "enablePullDownRefresh": true, "navigationBarTitleText": "" } + }, { + "path": "pages/contact/contact", + "style": { + // #ifdef MP-WEIXIN || MP-BAIDU || MP-QQ || MP-KUAISHOU || H5 || APP + "navigationStyle": "custom", + // #endif + "enablePullDownRefresh": true + } }, { "path": "pages/goods-category/goods-category", diff --git a/pages/contact/contact.vue b/pages/contact/contact.vue new file mode 100644 index 0000000..eb04382 --- /dev/null +++ b/pages/contact/contact.vue @@ -0,0 +1,441 @@ + + + + + diff --git a/pages/goods-detail/goods-detail.vue b/pages/goods-detail/goods-detail.vue index 6e3c795..a401398 100644 --- a/pages/goods-detail/goods-detail.vue +++ b/pages/goods-detail/goods-detail.vue @@ -452,6 +452,10 @@ + + + 客服 + @@ -1460,7 +1464,24 @@ // url事件 url_event(e) { - app.globalData.url_event(e); + var login = e.currentTarget.dataset.login; + if (login === undefined || login == 1) { + if (this.is_login()) { + app.globalData.url_event(e); + } + } else { + app.globalData.url_event(e); + } + }, + + // 是否登录 + is_login() { + const user = app.globalData.get_user_cache_info() || null; + if ((user || null) == null) { + app.globalData.url_open('/pages/login/login?event_callback=init'); + return false; + } + return true; }, // 底部导航操作返回事件 diff --git a/pages/login/login.vue b/pages/login/login.vue index ad5a4f4..1ee3fde 100644 --- a/pages/login/login.vue +++ b/pages/login/login.vue @@ -15,9 +15,7 @@ --> - - - + @@ -415,6 +413,7 @@ diff --git a/static/icon_contact_avatar.svg b/static/icon_contact_avatar.svg new file mode 100644 index 0000000..4be612b --- /dev/null +++ b/static/icon_contact_avatar.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/utils/socket.js b/utils/socket.js new file mode 100644 index 0000000..55770f6 --- /dev/null +++ b/utils/socket.js @@ -0,0 +1,547 @@ +// socket.js - 优化后的全局唯一socket管理模块(支付宝小程序兼容版) + +// 事件总线实现 +class EventEmitter { + constructor() { + this.events = {}; + } + + on(event, callback) { + if (!this.events[event]) { + this.events[event] = []; + } + this.events[event].push(callback); + return () => this.off(event, callback); + } + + off(event, callback) { + if (!this.events[event]) return; + + if (callback) { + this.events[event] = this.events[event].filter(cb => cb !== callback); + } else { + this.events[event] = []; + } + } + + emit(event, data) { + if (!this.events[event]) return; + this.events[event].forEach(callback => callback(data)); + } +} + +// 创建全局事件总线 +const eventBus = new EventEmitter(); + +// 唯一的socket实例 +let socketTask = null; +let connectTimer = null; +let heartbeatTimer = null; +let reconnectCount = 0; +let isConnected = false; +let lastUrl = ''; +let isConnecting = false; +let isAlipayMiniProgram = false; + +// 支付宝小程序特有状态 +let globalEventHandlersRegistered = false; + +// 初始化时检测环境 +(function detectEnvironment() { + try { + const systemInfo = uni.getSystemInfoSync(); + isAlipayMiniProgram = systemInfo.platform === 'alipay' || systemInfo.environment === 'alipay' || systemInfo + .app == 'alipay'; + console.log('当前运行平台===', systemInfo); + console.log(`当前运行环境: ${isAlipayMiniProgram ? '支付宝小程序' : '其他平台'}`); + } catch (e) { + console.error('检测运行环境失败:', e); + isAlipayMiniProgram = false; + } +})(); + +const MAX_RECONNECT_COUNT = 5; +const RECONNECT_DELAY = 3000; // 3秒重连间隔 +const HEARTBEAT_INTERVAL = 10000; // 10秒心跳间隔 + +// 连接socket +function connectSocket(url = 'wss://store.sxczgkj.com/shopser') { + // 如果已经连接到相同URL,直接返回 + if (isConnected && lastUrl === url) { + console.log('socket已连接到:', url); + return Promise.resolve(); + } + + // 如果正在连接中,返回pending状态的Promise + if (isConnecting) { + console.log('socket连接中,等待连接完成...'); + return new Promise((resolve, reject) => { + const unsubscribeConnected = eventBus.on('connected', () => { + unsubscribeConnected(); + unsubscribeError(); + resolve(); + }); + + const unsubscribeError = eventBus.on('error', (err) => { + unsubscribeConnected(); + unsubscribeError(); + reject(err); + }); + }); + } + + // 如果已有不同URL的连接,先关闭 + if ((socketTask || globalEventHandlersRegistered) && lastUrl !== url) { + console.log('关闭旧连接,准备连接新URL:', url); + closeSocket(); + } + + // 记录当前URL + lastUrl = url; + isConnecting = true; + reconnectCount = 0; // 重置重连计数 + + console.log('开始连接socket:', url); + + return new Promise((resolve, reject) => { + // 支付宝小程序特殊处理 + if (isAlipayMiniProgram) { + // 确保只注册一次全局事件处理程序 + if (!globalEventHandlersRegistered) { + registerGlobalEventHandlers(); + globalEventHandlersRegistered = true; + } + + // 调用连接方法 + uni.connectSocket({ + url, + success: () => { + console.log('socket连接请求已发送'); + }, + fail: (err) => { + console.error('socket连接失败:', err); + isConnecting = false; + + // 特殊处理:如果是"WebSocket已存在"错误,尝试重用现有连接 + if (err.errMsg.includes('WebSocket 已存在')) { + console.warn('检测到重复连接,尝试重用现有连接'); + // 触发已连接事件(模拟连接成功) + handleSocketOpen(null); + resolve(); + return; + } + + eventBus.emit('error', err); + handleReconnect(url); + reject(err); + } + }); + } else { + // H5和其他平台使用socketTask对象方法 + socketTask = uni.connectSocket({ + url, + success: () => { + console.log('socket连接请求已发送'); + }, + fail: (err) => { + console.error('socket连接失败:', err); + isConnecting = false; + eventBus.emit('error', err); + handleReconnect(url); + reject(err); + } + }); + + // 检查socketTask是否正确返回 + if (!socketTask) { + console.error('uni.connectSocket() 未返回 socketTask 对象'); + isConnecting = false; + eventBus.emit('error', new Error('无法创建socket连接')); + handleReconnect(url); + reject(new Error('无法创建socket连接')); + return; + } + + // 监听socket连接成功 + if (socketTask.onOpen) { + socketTask.onOpen((res) => { + handleSocketOpen(res, resolve); + }); + } else { + console.error('socketTask对象缺少onOpen方法'); + isConnecting = false; + eventBus.emit('error', new Error('socketTask对象不完整')); + handleReconnect(url); + reject(new Error('socketTask对象不完整')); + } + + // 监听socket消息 + if (socketTask.onMessage) { + socketTask.onMessage((res) => { + handleSocketMessage(res); + }); + } + + // 监听socket关闭 + if (socketTask.onClose) { + socketTask.onClose((res) => { + handleSocketClose(res); + }); + } + + // 监听socket错误 + if (socketTask.onError) { + socketTask.onError((err) => { + handleSocketError(err, reject); + }); + } + } + }); +} + +// 注册支付宝小程序全局事件监听 +function registerGlobalEventHandlers() { + console.log('注册支付宝小程序全局socket事件监听'); + + // 连接成功 + uni.onSocketOpen((res) => { + console.log('支付宝小程序: socket连接成功'); + handleSocketOpen(res); + }); + + // 接收消息 + uni.onSocketMessage((res) => { + handleSocketMessage(res); + }); + + // 连接关闭 + uni.onSocketClose((res) => { + handleSocketClose(res); + }); + + // 连接错误 + uni.onSocketError((err) => { + handleSocketError(err); + }); +} + +// 处理socket连接成功 +function handleSocketOpen(res, resolve) { + console.log('socket连接成功:', res); + reconnectCount = 0; + isConnected = true; + isConnecting = false; + + // 标记socket已创建(支付宝小程序无socketTask对象) + if (isAlipayMiniProgram) { + socketTask = true; // 使用布尔值标记连接状态 + } + + eventBus.emit('connected'); + + + const userInfo = uni.getStorageSync('cache_shop_user_info_key'); + if (userInfo.id) { + sendMessage({ + operate_type: 'init', + type: 'user_msg', + user_id: userInfo.id, + }); + } + + startHeartbeat(); + + if (resolve) { + resolve(); + } +} + +// 处理socket消息 +function handleSocketMessage(res) { + try { + let data = JSON.parse(res.data); + console.log('收到socket消息:', data); + eventBus.emit('message', data); + } catch (e) { + console.error('解析socket消息失败:', e); + eventBus.emit('error', e); + } +} + +// 处理socket关闭 +function handleSocketClose(res) { + console.log('socket已关闭:', res); + isConnected = false; + eventBus.emit('closed', res); + stopHeartbeat(); + + // 仅在非支付宝小程序环境下重置socketTask + if (!isAlipayMiniProgram) { + socketTask = null; + } + + handleReconnect(lastUrl); +} + +// 处理socket错误 +function handleSocketError(err, reject) { + console.error('socket发生错误:', err); + isConnected = false; + isConnecting = false; + eventBus.emit('error', err); + stopHeartbeat(); + + // 仅在非支付宝小程序环境下重置socketTask + if (!isAlipayMiniProgram) { + socketTask = null; + } + + handleReconnect(lastUrl); + + if (reject) { + reject(err); + } +} + +// 发送消息 +function sendMessage(data) { + if (!isConnected) { + console.error('socket未连接,无法发送消息'); + // 尝试重连 + connectSocket().then(() => { + sendMessage(data); + }).catch(err => { + console.error('重连失败,无法发送消息:', err); + }); + return; + } + + try { + const messageData = JSON.stringify(data); + + if (isAlipayMiniProgram) { + uni.sendSocketMessage({ + data: messageData, + success: () => { + console.log('消息发送成功'); + }, + fail: (err) => { + console.error('消息发送失败:', err); + eventBus.emit('error', err); + } + }); + } else { + if (!socketTask) { + console.error('socketTask不存在,无法发送消息'); + return; + } + + // 检查socketTask是否有send方法 + if (socketTask.send) { + socketTask.send({ + data: messageData, + success: () => { + console.log('消息发送成功'); + }, + fail: (err) => { + console.error('消息发送失败:', err); + eventBus.emit('error', err); + } + }); + } else { + console.error('socketTask缺少send方法'); + eventBus.emit('error', new Error('socketTask对象不完整')); + } + } + } catch (e) { + console.error('消息序列化失败:', e); + eventBus.emit('error', e); + } +} + +// 发送心跳 +function sendHeartbeat() { + sendMessage({ + type: 'ping_interval' + }); +} + +// 开始心跳 +function startHeartbeat() { + stopHeartbeat(); + heartbeatTimer = setInterval(sendHeartbeat, HEARTBEAT_INTERVAL); +} + +// 停止心跳 +function stopHeartbeat() { + if (heartbeatTimer) { + clearInterval(heartbeatTimer); + heartbeatTimer = null; + } +} + +// 处理重连 +function handleReconnect(url) { + if (reconnectCount >= MAX_RECONNECT_COUNT) { + console.error('达到最大重连次数,停止重连'); + return; + } + + // 检查是否需要重连(避免重复触发连接) + if (isConnecting || isConnected) { + return; + } + + reconnectCount++; + console.log(`准备第${reconnectCount}次重连,${RECONNECT_DELAY/1000}秒后...`); + + if (connectTimer) { + clearTimeout(connectTimer); + } + + connectTimer = setTimeout(() => { + connectSocket(url); + }, RECONNECT_DELAY); +} + +// 关闭socket +function closeSocket() { + console.log('正在关闭socket连接...'); + + try { + if (isAlipayMiniProgram) { + uni.closeSocket({ + success: () => { + console.log('主动关闭socket成功'); + }, + fail: (err) => { + console.error('主动关闭socket失败:', err); + } + }); + } else { + if (!socketTask) return; + + if (socketTask.close) { + socketTask.close({ + success: () => { + console.log('主动关闭socket成功'); + }, + fail: (err) => { + console.error('主动关闭socket失败:', err); + } + }); + } else { + console.error('socketTask缺少close方法'); + isConnected = false; + } + } + } catch (e) { + console.error('关闭socket时发生异常:', e); + isConnected = false; + } + + stopHeartbeat(); + if (connectTimer) { + clearTimeout(connectTimer); + connectTimer = null; + } + + isConnected = false; + isConnecting = false; + + // 仅在非支付宝小程序环境下重置socketTask + if (!isAlipayMiniProgram) { + socketTask = null; + } + + // 重置支付宝小程序的全局事件标记 + globalEventHandlersRegistered = false; +} + +// 监听消息 +function onMessage(callback) { + return eventBus.on('message', callback); +} + +// 监听连接状态 +function onConnected(callback) { + return eventBus.on('connected', callback); +} + +// 监听断开状态 +function onClosed(callback) { + return eventBus.on('closed', callback); +} + +// 监听错误 +function onError(callback) { + return eventBus.on('error', callback); +} + +// 提供一个自动管理生命周期的组件选项 +function createSocketMixin() { + return { + data() { + return { + socketUnsubscribes: [] + }; + }, + beforeDestroy() { + // 自动取消所有订阅 + this.socketUnsubscribes.forEach(unsubscribe => unsubscribe()); + this.socketUnsubscribes = []; + }, + methods: { + // 订阅消息并自动管理生命周期 + socketOnMessage(callback) { + const unsubscribe = onMessage(callback); + this.socketUnsubscribes.push(unsubscribe); + return unsubscribe; + }, + socketOnConnected(callback) { + const unsubscribe = onConnected(callback); + this.socketUnsubscribes.push(unsubscribe); + return unsubscribe; + }, + socketOnClosed(callback) { + const unsubscribe = onClosed(callback); + this.socketUnsubscribes.push(unsubscribe); + return unsubscribe; + }, + socketOnError(callback) { + const unsubscribe = onError(callback); + this.socketUnsubscribes.push(unsubscribe); + return unsubscribe; + } + } + }; +} + +// 导出状态检查函数 +function isSocketConnected() { + return isConnected; +} + +// 导出连接状态 +function getConnectionStatus() { + return { + isConnected, + isConnecting, + lastUrl, + reconnectCount, + platform: isAlipayMiniProgram ? 'alipay' : 'other' + }; +} + +export default { + connectSocket, + sendMessage, + closeSocket, + onMessage, + onConnected, + onClosed, + onError, + createSocketMixin, + isSocketConnected, + getConnectionStatus +}; \ No newline at end of file diff --git a/utils/uploadFile.js b/utils/uploadFile.js new file mode 100644 index 0000000..27071ca --- /dev/null +++ b/utils/uploadFile.js @@ -0,0 +1,83 @@ +const app = getApp(); + +/** + * 选择并上传图片 + * @param {number} count - 最多选择的图片数量 + * @param {boolean} compressed - 是否压缩图片 + * @returns {Promise} - 返回上传成功后的图片URL数组 + */ +export function uploadImage(count = 1, compressed = true) { + return new Promise((resolve, reject) => { + // 选择图片 + uni.chooseImage({ + count, + sizeType: compressed ? ['compressed'] : ['original'], + sourceType: ['album', 'camera'], + success: (res) => { + const tempFilePaths = res.tempFilePaths; + const uploadPromises = tempFilePaths.map((tempFilePath) => + uploadSingleImage(tempFilePath) + ); + + // 并行上传所有图片 + Promise.all(uploadPromises) + .then(results => resolve(results)) + .catch(error => reject(error)); + }, + fail: (err) => { + console.error('选择图片失败:', err); + reject(err); + } + }); + }); +} + +/** + * 上传单张图片 + * @param {string} filePath - 本地图片路径 + * @returns {Promise} - 返回上传成功后的图片URL + */ +function uploadSingleImage(filePath) { + return new Promise((resolve, reject) => { + uni.uploadFile({ + url: app.globalData.get_request_url('excel', 'xo'), // 上传地址 + filePath, + name: 'file', // 服务器接收的文件字段名 + success: (res) => { + if (res.statusCode === 200) { + try { + const data = JSON.parse(res.data); + if (data.code === 0) { // 根据实际接口返回格式调整 + resolve(data.data.url); // 返回图片URL + } else { + reject(new Error(data.message || '上传失败')); + } + } catch (e) { + reject(new Error('解析响应数据失败')); + } + } else { + reject(new Error(`服务器响应错误: ${res.statusCode}`)); + } + }, + fail: (err) => { + console.error('上传图片失败:', err); + reject(err); + } + }); + }); +} + +/** + * 预览图片 + * @param {Array} urls - 图片URL数组 + * @param {number} current - 当前预览的图片索引 + */ +export function previewImage(urls, current = 0) { + uni.previewImage({ + urls, + current, + fail: (err) => { + console.error('预览图片失败:', err); + } + }); +} \ No newline at end of file