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 @@
+
+
+
+
+
+
+
+ 下拉加载更多历史消息
+ 没有更多消息了~
+
+
+ {{ contactInfo }}
+
+
+
+
+
+ {{ item.content }}
+
+
+
+ {{ item.add_time | formatTime }}
+
+
+
+
+
+
+
+
+
+
+
+
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