From e8fb32c1fed67081dd4ae088ee3383b5b0a287ce Mon Sep 17 00:00:00 2001 From: XingQue Date: Mon, 29 Jun 2026 17:59:23 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E6=8A=A2=E5=8D=95=E7=AB=AFUI=E5=9B=9E?= =?UTF-8?q?=E9=80=80=E5=89=8D=E5=A4=87=E4=BB=BD=E5=BD=93=E5=89=8D=E5=AE=8C?= =?UTF-8?q?=E6=95=B4=E5=B7=A5=E4=BD=9C=E5=8C=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- app.js | 35 +- components/chenghao-tag/chenghao-tag.js | 17 +- .../global-notification.js | 16 +- .../global-notification.wxml | 7 +- .../global-notification.wxss | 61 +- components/kefu-float/kefu-float.js | 126 ++++ components/kefu-float/kefu-float.json | 4 + components/kefu-float/kefu-float.wxml | 47 ++ components/kefu-float/kefu-float.wxss | 179 +++++ components/order-sender/order-sender.js | 83 ++- components/order-sender/order-sender.wxml | 28 +- components/order-sender/order-sender.wxss | 183 +++-- components/pindao-modal/pindao-modal.wxml | 2 +- pages/accept-order/accept-order.js | 163 +++- pages/accept-order/accept-order.json | 3 +- pages/accept-order/accept-order.wxml | 94 ++- pages/accept-order/accept-order.wxss | 177 +++++ pages/chat/chat.js | 212 ++++-- pages/chat/chat.wxml | 8 +- pages/cs-chat/cs-chat.js | 244 ++++-- pages/cs-chat/cs-chat.wxml | 6 +- pages/fighter/fighter.js | 68 +- pages/fighter/fighter.json | 3 +- pages/fighter/fighter.wxml | 13 +- pages/fighter/fighter.wxss | 36 + pages/group-chat/group-chat.js | 705 +++++++++++------- pages/group-chat/group-chat.json | 3 +- pages/group-chat/group-chat.wxml | 26 +- pages/group-chat/group-chat.wxss | 62 ++ pages/merchant-home/merchant-home.js | 1 + pages/merchant-orders/merchant-orders.js | 49 +- pages/merchant-orders/merchant-orders.wxml | 131 +--- pages/merchant-orders/merchant-orders.wxss | 85 ++- pages/merchant/merchant.js | 27 +- pages/merchant/merchant.wxml | 7 +- pages/messages/messages.js | 308 ++++++-- pages/messages/messages.wxml | 43 +- pages/messages/messages.wxss | 140 +++- tab-bar/index.js | 41 +- utils/avatar.js | 73 +- utils/base-page.js | 4 + utils/chat-core.js | 77 +- utils/chat-history.js | 106 +++ utils/group-chat.js | 38 +- utils/im-user.js | 5 +- utils/imAuth/dashoujianquan.js | 29 +- utils/imAuth/jianquanxian.js | 35 +- utils/imAuth/laobanjianquan.js | 29 +- utils/kefu-nav.js | 12 + utils/role-tab-bar.js | 3 +- utils/staff-api.js | 3 +- utils/xiaoxilj.js | 565 +++++++------- utils/zhiding-order.js | 53 ++ 53 files changed, 3410 insertions(+), 1065 deletions(-) create mode 100644 components/kefu-float/kefu-float.js create mode 100644 components/kefu-float/kefu-float.json create mode 100644 components/kefu-float/kefu-float.wxml create mode 100644 components/kefu-float/kefu-float.wxss create mode 100644 utils/chat-history.js create mode 100644 utils/kefu-nav.js create mode 100644 utils/zhiding-order.js diff --git a/app.js b/app.js index 18fdaef..be37b63 100644 --- a/app.js +++ b/app.js @@ -7,6 +7,7 @@ import { setClubId, getConfiguredClubId, buildClubHeaders, getClubId } from './u import { CLUB_ID, WX_APP_ID } from './config/club-config'; import { applyMiniappAssetsFromConfig, warmupPindaoConfig } from './utils/miniapp-icons.js'; import { refreshDashouMembership } from './utils/dashou-profile.js'; +const { getDefaultAvatarUrl, resolveAvatarFromStorage } = require('./utils/avatar.js'); // 三端固定首页 const roleDefaultPage = PRIMARY_DEFAULT_PAGES; @@ -163,11 +164,21 @@ App({ debugMode: false, currentUser: null, currentRole: 'normal', - primaryRole: 'normal' - }, + primaryRole: 'normal', + /** 客服浮钮:full | mini,onLaunch 重置为 full */ + kefuFloatMode: 'full', + /** 本次启动内隐藏客服浮钮(不持久化,杀进程后恢复) */ + kefuFloatSessionHidden: false, + kefuFloatY: null, + /** 从「我的」跳抢单页时高亮指定单 */ + _acceptOrderHighlight: '', + }, // 核心启动流程 async onLaunch() { + this.globalData.kefuFloatMode = 'full'; + this.globalData.kefuFloatSessionHidden = false; + try { wx.removeStorageSync('kefuFloatPermanentHidden'); } catch (e) {} setClubId(getConfiguredClubId(), this); this.globalData.clubId = CLUB_ID; // ① 隐藏返回首页按钮 @@ -258,8 +269,13 @@ App({ async onShow() { if (!wx.getStorageSync('token')) return; await ensurePhoneAuth({ redirect: true }); - if (this.globalData.chatEnabled && typeof this.ensureConnection === 'function') { + if (this.globalData.chatEnabled && typeof this.startImWhenReady === 'function') { + this.startImWhenReady(); + } else if (this.globalData.chatEnabled && typeof this.ensureConnection === 'function') { this.globalData.goEasyConnection.autoReconnect = true; + if (typeof this.restoreTabBarBadge === 'function') { + this.restoreTabBarBadge(); + } this.ensureConnection(); if (typeof this.syncConnectionStatus === 'function') { setTimeout(() => this.syncConnectionStatus(), 800); @@ -386,6 +402,13 @@ App({ } catch (e) { console.warn('miniapp assets apply failed', e); } + + if (this.emitEvent) { + this.emitEvent('configApplied', { + ossImageUrl: this.globalData.ossImageUrl, + morentouxiang: this.globalData.morentouxiang, + }); + } }, // 仅在有效 appkey 时初始化 @@ -422,10 +445,12 @@ App({ initCurrentUser() { const uid = wx.getStorageSync('uid'); if (uid) { + const def = getDefaultAvatarUrl(this); + const avatar = resolveAvatarFromStorage(this) || def; this.globalData.currentUser = { id: uid, - name: '用户' + uid.substring(0, 6), - avatar: this.globalData.ossImageUrl + this.globalData.morentouxiang + name: wx.getStorageSync('nicheng') || ('用户' + uid.substring(0, 6)), + avatar: avatar || def, }; } }, diff --git a/components/chenghao-tag/chenghao-tag.js b/components/chenghao-tag/chenghao-tag.js index 2edff62..37fcf1d 100644 --- a/components/chenghao-tag/chenghao-tag.js +++ b/components/chenghao-tag/chenghao-tag.js @@ -6,7 +6,7 @@ const PILL_SHAPES = ['pill', 'rectangle', 'rounded']; Component({ properties: { mingcheng: { type: String, value: '' }, - texiaoJson: { type: Object, value: {} }, + texiaoJson: { type: null, value: null }, }, data: { inlineStyle: '', @@ -29,7 +29,20 @@ Component({ }, methods: { _applyConfig(raw) { - const cfg = raw || {}; + let cfg = raw || {}; + if (typeof cfg === 'string') { + try { + cfg = JSON.parse(cfg); + } catch (e) { + cfg = {}; + } + } + if (Array.isArray(cfg)) { + cfg = cfg[0] || {}; + } + if (!cfg || typeof cfg !== 'object') { + cfg = {}; + } const ossImageUrl = app.globalData.ossImageUrl || ''; // 无 shape:身份装饰标签 / 自定义色圆角标;有 shape:考核称号等特殊形状 diff --git a/components/global-notification/global-notification.js b/components/global-notification/global-notification.js index 29820d6..139fce1 100644 --- a/components/global-notification/global-notification.js +++ b/components/global-notification/global-notification.js @@ -1,4 +1,4 @@ -const { resolveAvatarUrl } = require('../../utils/avatar.js'); +const { resolveAvatarUrl, getDefaultAvatarUrl } = require('../../utils/avatar.js'); Component({ properties: { @@ -53,7 +53,8 @@ Component({ // 静音状态 isMuted: false, - + formattedTime: '', + // 自动隐藏计时器 autoHideTimeout: null }, @@ -123,7 +124,7 @@ Component({ let avatar = resolveAvatarUrl( data.avatar || data.message?.senderData?.avatar, app - ); + ) || getDefaultAvatarUrl(app); this.setData({ show: true, @@ -131,6 +132,7 @@ Component({ message: data.content || '[消息内容]', avatar: avatar, notificationData: data, + formattedTime: this.formatTime(data.timestamp), progress: 100, swipingClass: '', swipeStyle: '' @@ -149,6 +151,7 @@ Component({ this.clearTimers(); this.setData({ show: false, + formattedTime: '', progress: 100, swipingClass: '', swipeStyle: '' @@ -265,6 +268,13 @@ Component({ this.triggerEvent('close'); this.hideNotification(); }, + + onAvatarError() { + const def = getDefaultAvatarUrl(getApp()); + if (def && this.data.avatar !== def) { + this.setData({ avatar: def }); + } + }, onMute(e) { e.stopPropagation(); diff --git a/components/global-notification/global-notification.wxml b/components/global-notification/global-notification.wxml index d76b58c..3f7cea1 100644 --- a/components/global-notification/global-notification.wxml +++ b/components/global-notification/global-notification.wxml @@ -15,14 +15,15 @@ - + + 新消息 {{title}} {{message}} - - {{formatTime(notificationData ? notificationData.timestamp : '')}} + + {{formattedTime}} diff --git a/components/global-notification/global-notification.wxss b/components/global-notification/global-notification.wxss index fc67be6..80e8f3a 100644 --- a/components/global-notification/global-notification.wxss +++ b/components/global-notification/global-notification.wxss @@ -1,29 +1,40 @@ .global-notification { position: fixed; - left: 20rpx; - right: 20rpx; - top: 0rpx; + left: 24rpx; + right: 24rpx; + top: calc(env(safe-area-inset-top) + 108rpx); z-index: 99999; opacity: 0; - transform: translateY(-120%); - transition: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55); + transform: translateY(-160%); + transition: all 0.42s cubic-bezier(0.34, 1.2, 0.64, 1); pointer-events: none; overflow: hidden; - border-radius: 24rpx; - background: linear-gradient(135deg, - rgba(41, 47, 61, 0.98) 0%, - rgba(31, 36, 48, 0.98) 100%); - backdrop-filter: blur(30rpx) saturate(180%); - -webkit-backdrop-filter: blur(30rpx) saturate(180%); - border: 1rpx solid rgba(255, 255, 255, 0.08); - box-shadow: - 0 20rpx 60rpx rgba(0, 0, 0, 0.4), - 0 0 0 1rpx rgba(255, 255, 255, 0.03), - inset 0 1rpx 0 rgba(255, 255, 255, 0.1); - min-height: 140rpx; + border-radius: 28rpx; + background: linear-gradient(145deg, + rgba(28, 32, 42, 0.97) 0%, + rgba(22, 26, 36, 0.98) 100%); + backdrop-filter: blur(40rpx) saturate(180%); + -webkit-backdrop-filter: blur(40rpx) saturate(180%); + border: 1rpx solid rgba(255, 255, 255, 0.1); + box-shadow: + 0 24rpx 64rpx rgba(0, 0, 0, 0.45), + 0 0 0 1rpx rgba(255, 255, 255, 0.04), + inset 0 1rpx 0 rgba(255, 255, 255, 0.12); + min-height: 148rpx; touch-action: pan-y; } +.global-notification::before { + content: ''; + position: absolute; + left: 0; + top: 24rpx; + bottom: 24rpx; + width: 6rpx; + border-radius: 0 6rpx 6rpx 0; + background: linear-gradient(180deg, #07c160 0%, #D4AF37 100%); +} + .global-notification.show { opacity: 1; transform: translateY(0); @@ -58,8 +69,8 @@ .notification-content { display: flex; align-items: center; - padding: 32rpx; - min-height: 140rpx; + padding: 28rpx 28rpx 28rpx 36rpx; + min-height: 148rpx; position: relative; } @@ -84,6 +95,18 @@ justify-content: center; } +.notification-tag { + display: inline-block; + align-self: flex-start; + font-size: 20rpx; + color: #07c160; + background: rgba(7, 193, 96, 0.15); + padding: 4rpx 14rpx; + border-radius: 20rpx; + margin-bottom: 8rpx; + font-weight: 500; +} + .notification-title { font-size: 34rpx; font-weight: 600; diff --git a/components/kefu-float/kefu-float.js b/components/kefu-float/kefu-float.js new file mode 100644 index 0000000..e9dc0ee --- /dev/null +++ b/components/kefu-float/kefu-float.js @@ -0,0 +1,126 @@ +import { openCustomerServiceChat } from '../../utils/kefu-nav.js'; + +Component({ + properties: { + bottom: { type: String, value: '200rpx' }, + }, + + data: { + mode: 'full', + sessionHidden: false, + csUnread: 0, + floatY: 520, + }, + + lifetimes: { + attached() { + this._syncFromGlobal(); + this._initFloatY(); + this._convHandler = () => this.syncCsUnread(); + const app = getApp(); + if (app.on) app.on('conversationsUpdated', this._convHandler); + if (app.on) app.on('unreadCountChanged', this._convHandler); + this.syncCsUnread(); + }, + detached() { + const app = getApp(); + if (this._convHandler && app.off) { + app.off('conversationsUpdated', this._convHandler); + app.off('unreadCountChanged', this._convHandler); + } + }, + }, + + pageLifetimes: { + show() { + this._syncFromGlobal(); + this.syncCsUnread(); + }, + }, + + methods: { + _initFloatY() { + const app = getApp(); + if (app.globalData.kefuFloatY != null) { + this.setData({ floatY: app.globalData.kefuFloatY }); + return; + } + try { + const sys = wx.getSystemInfoSync(); + const defaultY = Math.floor((sys.windowHeight || 600) * 0.55); + this.setData({ floatY: defaultY }); + } catch (e) { + this.setData({ floatY: 520 }); + } + }, + + _syncFromGlobal() { + const app = getApp(); + const sessionHidden = !!app.globalData.kefuFloatSessionHidden; + const mode = app.globalData.kefuFloatMode || 'full'; + this.setData({ sessionHidden, mode }); + }, + + onFloatMove(e) { + const y = e.detail && e.detail.y; + if (y == null) return; + const app = getApp(); + app.globalData.kefuFloatY = y; + }, + + syncCsUnread() { + if (this.data.sessionHidden) return; + if (!wx.goEasy?.im?.latestConversations) return; + wx.goEasy.im.latestConversations({ + onSuccess: (res) => { + const list = res.content?.conversations || res.conversations || []; + let unread = 0; + list.forEach((c) => { + if (c.type === 'cs' || c.teamId === 'support_team') { + unread += c.unread || 0; + } + }); + if (unread !== this.data.csUnread) { + this.setData({ csUnread: unread }); + } + }, + onFailed: () => {}, + }); + }, + + onContact() { + openCustomerServiceChat(); + }, + + onHideTap(e) { + if (e && e.stopPropagation) e.stopPropagation(); + const app = getApp(); + app.globalData.kefuFloatMode = 'mini'; + this.setData({ mode: 'mini' }); + }, + + onExpand(e) { + if (e && e.stopPropagation) e.stopPropagation(); + const app = getApp(); + app.globalData.kefuFloatMode = 'full'; + this.setData({ mode: 'full' }); + }, + + onSessionDismiss(e) { + if (e && e.stopPropagation) e.stopPropagation(); + wx.showModal({ + title: '隐藏联系客服', + content: '本次使用期间不再显示此按钮。清空后台重新进入小程序后会再次出现。', + confirmText: '确定隐藏', + cancelText: '取消', + success: (res) => { + if (res.confirm) { + const app = getApp(); + app.globalData.kefuFloatSessionHidden = true; + this.setData({ sessionHidden: true }); + } + }, + }); + }, + }, +}); diff --git a/components/kefu-float/kefu-float.json b/components/kefu-float/kefu-float.json new file mode 100644 index 0000000..a89ef4d --- /dev/null +++ b/components/kefu-float/kefu-float.json @@ -0,0 +1,4 @@ +{ + "component": true, + "usingComponents": {} +} diff --git a/components/kefu-float/kefu-float.wxml b/components/kefu-float/kefu-float.wxml new file mode 100644 index 0000000..48ce5ee --- /dev/null +++ b/components/kefu-float/kefu-float.wxml @@ -0,0 +1,47 @@ + + + + + + {{csUnread > 99 ? '99+' : csUnread}} + + 💬 + + 联系客服 + 客服有新消息 + 咨询订单 / 账号问题 + + + + × + + + + 本次不再显示 + × + + + + + + + + {{csUnread > 99 ? '99+' : csUnread}} + 💬 + 联系客服 + + + × + 本次隐藏 + + + + + diff --git a/components/kefu-float/kefu-float.wxss b/components/kefu-float/kefu-float.wxss new file mode 100644 index 0000000..852b86e --- /dev/null +++ b/components/kefu-float/kefu-float.wxss @@ -0,0 +1,179 @@ +.kefu-movable-area { + position: fixed; + left: 0; + top: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 900; +} + +.kefu-movable-view { + width: auto; + height: auto; + pointer-events: auto; + position: absolute; + right: 24rpx; + top: 0; +} + +.kefu-wrap { + display: flex; + flex-direction: column; + align-items: flex-end; + pointer-events: auto; +} + +.kefu-float { + position: relative; +} + +.kefu-float--full { + display: flex; + align-items: stretch; + background: linear-gradient(135deg, #07c160 0%, #06ad56 100%); + border-radius: 20rpx; + box-shadow: 0 8rpx 28rpx rgba(7, 193, 96, 0.35); + overflow: visible; +} + +.kefu-unread-badge { + position: absolute; + top: -12rpx; + right: -8rpx; + min-width: 36rpx; + height: 36rpx; + line-height: 36rpx; + padding: 0 8rpx; + background: #fa5151; + color: #fff; + font-size: 20rpx; + text-align: center; + border-radius: 18rpx; + border: 2rpx solid #fff; + z-index: 2; +} + +.kefu-unread-badge--mini { + top: -8rpx; + right: -4rpx; +} + +.kefu-float-main { + display: flex; + align-items: center; + padding: 20rpx 24rpx; + flex: 1; +} + +.kefu-float-main:active { + opacity: 0.88; +} + +.kefu-float-icon { + font-size: 36rpx; + margin-right: 14rpx; + flex-shrink: 0; +} + +.kefu-text-col { + display: flex; + flex-direction: column; + min-width: 0; +} + +.kefu-float-text { + font-size: 28rpx; + color: #fff; + font-weight: 600; + white-space: nowrap; +} + +.kefu-float-sub { + font-size: 20rpx; + color: rgba(255, 255, 255, 0.95); + margin-top: 4rpx; +} + +.kefu-float-sub--muted { + color: rgba(255, 255, 255, 0.75); +} + +.kefu-float-close { + width: 64rpx; + display: flex; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.12); + color: rgba(255, 255, 255, 0.95); + font-size: 38rpx; + line-height: 1; +} + +.kefu-float-close:active { + background: rgba(0, 0, 0, 0.2); +} + +.kefu-permanent-row { + display: flex; + align-items: center; + justify-content: flex-end; + margin-top: 10rpx; + padding: 8rpx 12rpx; + background: rgba(255, 255, 255, 0.92); + border-radius: 24rpx; + box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08); +} + +.kefu-permanent-row:active { + opacity: 0.85; +} + +.kefu-permanent-row--mini { + margin-top: 8rpx; + padding: 6rpx 14rpx; +} + +.kefu-permanent-label { + font-size: 22rpx; + color: #888; + margin-right: 8rpx; +} + +.kefu-permanent-x { + font-size: 28rpx; + color: #bbb; + line-height: 1; +} + +.kefu-wrap--mini { + align-items: flex-end; +} + +.kefu-float--mini { + width: auto; + min-width: 160rpx; + padding: 16rpx 24rpx; + border-radius: 48rpx; + background: linear-gradient(135deg, #07c160 0%, #06ad56 100%); + box-shadow: 0 6rpx 20rpx rgba(7, 193, 96, 0.3); + display: flex; + flex-direction: column; + align-items: center; +} + +.kefu-float--mini:active { + transform: scale(0.96); +} + +.kefu-float-mini-icon { + font-size: 36rpx; +} + +.kefu-mini-label { + font-size: 22rpx; + color: #fff; + font-weight: 500; + margin-top: 4rpx; + white-space: nowrap; +} diff --git a/components/order-sender/order-sender.js b/components/order-sender/order-sender.js index f4c9444..5e0ce70 100644 --- a/components/order-sender/order-sender.js +++ b/components/order-sender/order-sender.js @@ -1,16 +1,87 @@ +import request from '../../utils/request'; + const app = getApp(); +function mapIdentityForApi() { + const role = app.globalData.currentRole || wx.getStorageSync('currentRole') || 'normal'; + if (role === 'dashou') return 'dashou'; + if (role === 'shangjia') return 'shangjia'; + return 'normal'; +} + Component({ properties: { - visible: Boolean, - showDetail: { type: Boolean, value: false } + visible: { type: Boolean, value: false }, + groupId: { type: String, value: '' }, + orderId: { type: String, value: '' }, }, - data: {}, + data: { + keyword: '', + loading: false, + orders: [], + filteredOrders: [], + }, + + observers: { + visible(v) { + if (v) { + this.setData({ keyword: '' }); + this.loadOrders(''); + } + }, + }, methods: { close() { this.triggerEvent('close'); - } - } -}); \ No newline at end of file + }, + + onSearchInput(e) { + const keyword = (e.detail.value || '').trim(); + this.setData({ keyword }); + if (this._searchTimer) clearTimeout(this._searchTimer); + this._searchTimer = setTimeout(() => { + this.loadOrders(keyword); + }, 350); + }, + + async loadOrders(keyword) { + const { groupId, orderId } = this.properties; + if (!groupId && !orderId) return; + + this.setData({ loading: true }); + try { + const res = await request({ + url: '/dingdan/ltpddd', + method: 'POST', + data: { + groupId, + dingdan_id: orderId, + keyword: keyword || '', + identityType: mapIdentityForApi(), + }, + }); + const body = res?.data || {}; + const list = body.data?.list || []; + this.setData({ + orders: list, + filteredOrders: list, + loading: false, + }); + } catch (e) { + console.warn('加载历史订单失败', e); + this.setData({ loading: false, orders: [], filteredOrders: [] }); + wx.showToast({ title: '加载订单失败', icon: 'none' }); + } + }, + + onSelectOrder(e) { + const index = e.currentTarget.dataset.index; + const order = this.data.filteredOrders[index]; + if (!order) return; + this.triggerEvent('send', { order }); + this.close(); + }, + }, +}); diff --git a/components/order-sender/order-sender.wxml b/components/order-sender/order-sender.wxml index ae53ed5..3e5dd08 100644 --- a/components/order-sender/order-sender.wxml +++ b/components/order-sender/order-sender.wxml @@ -1,12 +1,28 @@ - 提示 + 选择订单发送 - - 📋 - 此功能暂未开放 - 敬请期待 + + - \ No newline at end of file + + 加载中... + + + + {{item.dingdan_id}} + {{item.zhuangtaiText}} + + {{item.jieshao || '暂无介绍'}} + + ¥{{item.jine || '0'}} + {{item.create_time}} + + 点击发送订单卡片 + + + 暂无相关订单 + + diff --git a/components/order-sender/order-sender.wxss b/components/order-sender/order-sender.wxss index c0ffc48..304f2cd 100644 --- a/components/order-sender/order-sender.wxss +++ b/components/order-sender/order-sender.wxss @@ -1,45 +1,140 @@ .order-sender-mask { - position: fixed; top: 0; left: 0; right: 0; bottom: 0; - background: rgba(0,0,0,0.5); - z-index: 2000; - } - .order-sender { - position: fixed; bottom: 0; left: 0; right: 0; - height: 360rpx; - background: #fff; - border-radius: 24rpx 24rpx 0 0; - z-index: 2001; - transform: translateY(100%); - transition: 0.25s; - display: flex; - flex-direction: column; - } - .order-sender.show { transform: translateY(0); } - - .header { - display: flex; justify-content: space-between; - padding: 24rpx 30rpx; - border-bottom: 1rpx solid #f0f0f0; - } - .title { font-size: 32rpx; font-weight: 600; } - .close { font-size: 44rpx; color: #999; padding: 0 12rpx; } - - .content { - flex: 1; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding-bottom: 40rpx; - } - .icon { font-size: 80rpx; margin-bottom: 20rpx; } - .notice { - font-size: 34rpx; - font-weight: 600; - color: #333; - margin-bottom: 10rpx; - } - .sub { - font-size: 24rpx; - color: #999; - } \ No newline at end of file + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 2000; +} + +.order-sender { + position: fixed; + bottom: 0; + left: 0; + right: 0; + height: 72vh; + max-height: 900rpx; + background: #fff; + border-radius: 24rpx 24rpx 0 0; + z-index: 2001; + transform: translateY(100%); + transition: transform 0.25s; + display: flex; + flex-direction: column; +} + +.order-sender.show { + transform: translateY(0); +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 24rpx 30rpx; + border-bottom: 1rpx solid #f0f0f0; + flex-shrink: 0; +} + +.title { + font-size: 32rpx; + font-weight: 600; +} + +.close { + font-size: 44rpx; + color: #999; + padding: 0 12rpx; +} + +.search-row { + padding: 16rpx 24rpx; + flex-shrink: 0; +} + +.search-input { + background: #f5f5f5; + border-radius: 32rpx; + padding: 16rpx 28rpx; + font-size: 28rpx; +} + +.order-list { + flex: 1; + height: 0; + padding: 0 24rpx 24rpx; + box-sizing: border-box; +} + +.loading-tip, +.empty-tip { + text-align: center; + color: #999; + font-size: 28rpx; + padding: 60rpx 0; +} + +.order-item { + background: #fafafa; + border-radius: 16rpx; + padding: 20rpx 24rpx; + margin-bottom: 16rpx; + border: 1rpx solid #eee; +} + +.order-item:active { + background: #f0f7ff; +} + +.order-item-top { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8rpx; +} + +.order-id { + font-size: 26rpx; + font-weight: 600; + color: #333; +} + +.order-status { + font-size: 22rpx; + color: #07c160; + background: #e8f8ee; + padding: 4rpx 14rpx; + border-radius: 20rpx; +} + +.order-desc { + font-size: 26rpx; + color: #666; + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-bottom: 8rpx; +} + +.order-item-bottom { + display: flex; + justify-content: space-between; + align-items: center; +} + +.order-price { + font-size: 28rpx; + color: #ff6b00; + font-weight: 600; +} + +.order-time { + font-size: 22rpx; + color: #aaa; +} + +.order-send-hint { + display: block; + font-size: 22rpx; + color: #007aff; + margin-top: 10rpx; +} diff --git a/components/pindao-modal/pindao-modal.wxml b/components/pindao-modal/pindao-modal.wxml index 628c411..9e7d8a3 100644 --- a/components/pindao-modal/pindao-modal.wxml +++ b/components/pindao-modal/pindao-modal.wxml @@ -9,7 +9,7 @@ {{channelNo}} - 暂无频道内容 + 暂无频道内容 o.dingdan_id === highlight)) { + this.setData({ highlightOrderId: highlight }); + this._pendingHighlight = ''; + } + } catch (e) { + if (!silent) console.warn('loadMyZhidingOrders', e); + } + }, + + openZhidingDetail(e) { + const item = e.currentTarget.dataset.item; + if (!item) return; + this.setData({ showZhidingDetail: true, zhidingDetailOrder: item }); + }, + + closeZhidingDetail() { + this.setData({ showZhidingDetail: false, zhidingDetailOrder: null }); + }, + + async onZhidingReject(e) { + const item = e.currentTarget.dataset.item; + if (!item) return; + wx.showModal({ + title: '确认婉拒', + content: '婉拒后订单将退回抢单大厅,确定不想接此单吗?', + confirmText: '不想接', + confirmColor: '#e64340', + success: async (res) => { + if (!res.confirm) return; + wx.showLoading({ title: '提交中...', mask: true }); + try { + const body = await submitZhidingResponse(item.dingdan_id, 2); + wx.hideLoading(); + if (body && (body.code === 200 || body.code === 0)) { + wx.showToast({ title: body.msg || '已婉拒', icon: 'none' }); + this.closeZhidingDetail(); + await this.loadMyZhidingOrders(true); + await this.loadDingdanList(true, true); + } else { + wx.showToast({ title: body?.msg || '操作失败', icon: 'none' }); + } + } catch (err) { + wx.hideLoading(); + wx.showToast({ title: '网络错误', icon: 'none' }); + } + }, + }); + }, + + onZhidingLater(e) { + const item = e.currentTarget.dataset.item; + if (!item) return; + this.setData({ + showZhidingDetail: false, + zhidingDetailOrder: null, + showLaterModal: true, + laterOrderId: item.dingdan_id, + laterNote: item.zhiding_dhj_sm || '', + }); + }, + + closeLaterModal() { + this.setData({ showLaterModal: false, laterOrderId: '', laterNote: '' }); + }, + + onLaterNoteInput(e) { + this.setData({ laterNote: e.detail.value }); + }, + + async confirmZhidingLater() { + const { laterOrderId, laterNote } = this.data; + if (!laterNote.trim()) { + wx.showToast({ title: '请填写预计接待时间', icon: 'none' }); + return; + } + wx.showLoading({ title: '提交中...', mask: true }); + try { + const body = await submitZhidingResponse(laterOrderId, 3, laterNote.trim()); + wx.hideLoading(); + if (body && (body.code === 200 || body.code === 0)) { + const note = laterNote.trim(); + wx.showToast({ title: `已记录:${note}`, icon: 'none', duration: 3500 }); + this.closeLaterModal(); + this.closeZhidingDetail(); + await this.loadMyZhidingOrders(true); + await this.loadDingdanList(true, true); + } else { + wx.showToast({ title: body?.msg || '操作失败', icon: 'none' }); + } + } catch (e) { + wx.hideLoading(); + wx.showToast({ title: '网络错误', icon: 'none' }); + } + }, + + async onZhidingAccept(e) { + const item = e.currentTarget.dataset.item; + if (!item) return; + wx.showLoading({ title: '处理中...', mask: true }); + try { + await submitZhidingResponse(item.dingdan_id, 1); + wx.hideLoading(); + } catch (e) { + wx.hideLoading(); + } + const processed = this.processDingdanItem(item); + this.onQiangdanTap({ currentTarget: { dataset: { item: processed } } }); + }, + + noop() {}, + onReachBottom() { if (this.data.isLoading || this.data.isLoadingMore) return; if (this.data.hasMore && !this.data.isLoading && this.data.xuanzhongLeixingId) { @@ -744,6 +902,7 @@ Page({ await this.loadBankuaiBiaoqian(); await this.loadDingdanList(true); } + await this.loadMyZhidingOrders(true); this.loadGlobalStatus(); this.persistPageCache(banner); } catch (e) { diff --git a/pages/accept-order/accept-order.json b/pages/accept-order/accept-order.json index 024a2e6..fc97bc8 100644 --- a/pages/accept-order/accept-order.json +++ b/pages/accept-order/accept-order.json @@ -9,6 +9,7 @@ "backgroundColor": "#f5f3ff", "usingComponents": { "chenghao-tag": "/components/chenghao-tag/chenghao-tag", - "global-notification": "/components/global-notification/global-notification" + "global-notification": "/components/global-notification/global-notification", + "kefu-float": "/components/kefu-float/kefu-float" } } \ No newline at end of file diff --git a/pages/accept-order/accept-order.wxml b/pages/accept-order/accept-order.wxml index d0a2cc6..5bad74b 100644 --- a/pages/accept-order/accept-order.wxml +++ b/pages/accept-order/accept-order.wxml @@ -44,6 +44,32 @@ {{gonggao}} + + + + + 指定给您 + {{myZhidingCount || myZhidingList.length}}单 + + 请确认是否接待 + + + + + 单号 {{item.dingdan_id}} + ¥{{item.dashou_fencheng || 0}} + + {{item.jieshao || '暂无介绍'}} + 您的回复:{{item.zhiding_hf_text}}{{item.zhiding_dhj_sm ? ' · ' + item.zhiding_dhj_sm : ''}} + + + 不想接 + 等会接 + 立即接待 + + + + @@ -182,7 +208,7 @@ - + @@ -212,7 +238,7 @@ 指定 - + {{item.zhiding_nicheng || '指定接单员'}} @@ -258,7 +284,7 @@ 平台派单 - 抢单 + {{item.isMyZhiding ? '立即接待' : '抢单'}} @@ -271,7 +297,7 @@ - + @@ -292,7 +318,7 @@ 指定 - + {{item.zhiding_nicheng || '指定接单员'}} @@ -336,7 +362,7 @@ 发布于 {{item.creat_time}} - 抢单 + {{item.isMyZhiding ? '立即接待' : '抢单'}} 商家派单 @@ -347,7 +373,7 @@ - + @@ -365,7 +391,7 @@ 指定 - + {{item.zhiding_nicheng || '指定接单员'}} @@ -442,7 +468,7 @@ 商家派单 - 抢单 + {{item.isMyZhiding ? '立即接待' : '抢单'}} @@ -482,8 +508,58 @@ + + + + + + + 指定订单详情 + × + + + 单号{{zhidingDetailOrder.dingdan_id}} + 分成¥{{zhidingDetailOrder.dashou_fencheng || 0}} + 介绍{{zhidingDetailOrder.jieshao || '暂无'}} + 备注{{zhidingDetailOrder.beizhu}} + 您的回复{{zhidingDetailOrder.zhiding_hf_text}} + + + 不想接 + 等会接 + 立即接待 + + + + + + + + + 等会接 + × + + + 请填写预计接待时间,例如「30分钟后」「今晚8点」 + + + 确认 + + + diff --git a/pages/accept-order/accept-order.wxss b/pages/accept-order/accept-order.wxss index c31f877..73b1ba6 100644 --- a/pages/accept-order/accept-order.wxss +++ b/pages/accept-order/accept-order.wxss @@ -770,3 +770,180 @@ page { margin-left: auto; max-width: 50%; } + + /* 被指定订单 */ + .my-zhiding-banner { + margin: 16rpx 24rpx 0; + padding: 22rpx; + border-radius: 20rpx; + background: linear-gradient(135deg, #ff9500 0%, #ff6b00 100%); + border: 3rpx solid #ff4500; + box-shadow: 0 12rpx 32rpx rgba(255, 69, 0, 0.45); + position: relative; + z-index: 20; + } + .my-zhiding-banner-head { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16rpx; + } + .my-zhiding-head-left { + display: flex; + align-items: center; + gap: 12rpx; + } + .my-zhiding-badge { + font-size: 26rpx; + font-weight: 800; + color: #ff4500; + background: #fff; + padding: 8rpx 18rpx; + border-radius: 999rpx; + } + .my-zhiding-count-num { + font-size: 32rpx; + font-weight: 800; + color: #fff; + } + .my-zhiding-sub { + font-size: 26rpx; + color: #fff; + font-weight: 700; + } + .my-zhiding-card { + background: #fff; + border-radius: 16rpx; + padding: 18rpx; + margin-bottom: 12rpx; + border: 2rpx solid #ffe0b2; + } + .my-zhiding-card--highlight { + border-color: #ff8c00; + box-shadow: 0 0 0 4rpx rgba(255, 140, 0, 0.15); + } + .my-zhiding-card-top { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8rpx; + } + .my-zhiding-order-id { + font-size: 24rpx; + color: #666; + } + .my-zhiding-reward { + font-size: 30rpx; + font-weight: 700; + color: #e64340; + } + .my-zhiding-desc { + font-size: 28rpx; + color: #333; + line-height: 1.5; + } + .my-zhiding-status { + display: block; + margin-top: 8rpx; + font-size: 24rpx; + color: #059669; + } + .my-zhiding-actions { + display: flex; + gap: 12rpx; + margin-top: 16rpx; + } + .zhiding-act { + flex: 1; + text-align: center; + padding: 14rpx 0; + border-radius: 999rpx; + font-size: 26rpx; + font-weight: 600; + } + .zhiding-act--ghost { + background: #f3f4f6; + color: #666; + } + .zhiding-act--primary { + background: linear-gradient(90deg, #ff8c00, #ff6b00); + color: #fff; + } + .order-my-zhiding { + border: 3rpx solid #ff4500 !important; + box-shadow: 0 0 0 6rpx rgba(255, 69, 0, 0.25), 0 12rpx 36rpx rgba(255, 69, 0, 0.35) !important; + } + .order-zhiding-highlight { + animation: zhidingPulse 1.5s ease-in-out 2; + } + @keyframes zhidingPulse { + 0%, 100% { box-shadow: 0 0 0 0 rgba(255, 140, 0, 0.3); } + 50% { box-shadow: 0 0 0 12rpx rgba(255, 140, 0, 0); } + } + .zhiding-modal-mask { + position: fixed; + left: 0; right: 0; top: 0; bottom: 0; + background: rgba(0, 0, 0, 0.62); + z-index: 10050; + display: flex; + align-items: center; + justify-content: center; + padding: 40rpx; + } + .zhiding-modal-mask--top { + z-index: 10060; + } + .zhiding-modal { + width: 100%; + max-width: 640rpx; + background: #ffffff; + border-radius: 24rpx; + overflow: hidden; + box-shadow: 0 24rpx 64rpx rgba(0, 0, 0, 0.35); + } + .zhiding-modal--sm { max-width: 580rpx; } + .zhiding-modal-head { + display: flex; + justify-content: space-between; + align-items: center; + padding: 24rpx 28rpx; + border-bottom: 1rpx solid #eee; + background: #fff; + } + .zhiding-modal-title { font-size: 32rpx; font-weight: 700; color: #222; } + .zhiding-modal-close { font-size: 44rpx; color: #666; line-height: 1; padding: 0 8rpx; } + .zhiding-modal-body { padding: 24rpx 28rpx; background: #fff; } + .zhiding-modal-row { + display: flex; + justify-content: space-between; + margin-bottom: 16rpx; + font-size: 28rpx; + color: #333; + } + .zhiding-modal-row.col { flex-direction: column; gap: 8rpx; } + .zhiding-modal-row .lbl { color: #999; font-size: 24rpx; } + .zhiding-modal-row .reward { color: #e64340; font-weight: 700; } + .zhiding-modal-actions { padding: 0 28rpx 28rpx; margin-top: 0; background: #fff; } + .later-hint { font-size: 26rpx; color: #444; display: block; margin-bottom: 16rpx; } + .later-input { + width: 100%; + box-sizing: border-box; + border: 2rpx solid #ddd; + border-radius: 12rpx; + padding: 22rpx 20rpx; + font-size: 28rpx; + color: #222; + background: #f9fafb; + min-height: 88rpx; + } + .later-input-ph { color: #aaa; } + .later-confirm { + margin: 0 28rpx 28rpx; + text-align: center; + padding: 24rpx; + background: linear-gradient(90deg, #ff8c00, #ff4500); + color: #fff; + border-radius: 999rpx; + font-size: 30rpx; + font-weight: 700; + } diff --git a/pages/chat/chat.js b/pages/chat/chat.js index 9178c6c..47b8d07 100644 --- a/pages/chat/chat.js +++ b/pages/chat/chat.js @@ -1,7 +1,14 @@ const app = getApp(); import { formatDate } from '../../static/lib/utils'; -import { resolveAvatarUrl, getDefaultAvatarUrl } from '../../utils/avatar.js'; +import { resolveAvatarUrl, getDefaultAvatarUrl, resolveAvatarFromStorage, subscribeConfigAvatarRefresh } from '../../utils/avatar.js'; import { getLocalImUserId } from '../../utils/im-user.js'; +import { recordConversationOpen } from '../../utils/group-chat.js'; +import { + fetchHistoryMessages, + getOldestHistoryTimestamp, + mergeHistoryMessages, + waitImConnected, +} from '../../utils/chat-history.js'; Page({ data: { @@ -25,29 +32,58 @@ Page({ pendingImage: '', pendingImageFile: null, keyboardHeight: 0, - bottomSafeHeight: 10 + bottomSafeHeight: 140, + defaultAvatarUrl: '', }, onLoad(options) { + const defAvatar = getDefaultAvatarUrl(app); if (options.data) { try { const p = JSON.parse(decodeURIComponent(options.data)); this.setData({ toUserId: p.toUserId || '', toName: p.toName || '聊天', - toAvatar: resolveAvatarUrl(p.toAvatar, app), + toAvatar: resolveAvatarUrl(p.toAvatar, app) || defAvatar, + defaultAvatarUrl: defAvatar, }); - } catch (e) {} + } catch (e) { + this.setData({ defaultAvatarUrl: defAvatar }); + } + } else { + this.setData({ defaultAvatarUrl: defAvatar }); } wx.setNavigationBarTitle({ title: this.data.toName }); this.initCurrentUser(); + this._unsubConfigAvatar = subscribeConfigAvatarRefresh(this, () => { + const def = getDefaultAvatarUrl(app); + this.setData({ + defaultAvatarUrl: def, + toAvatar: resolveAvatarUrl(this.data.toAvatar, app) || def, + }); + this.refreshCurrentUserAvatar(); + }); }, onShow() { + const defAvatar = getDefaultAvatarUrl(app); + this.setData({ + defaultAvatarUrl: defAvatar, + toAvatar: resolveAvatarUrl(this.data.toAvatar, app) || defAvatar, + }); + this.refreshCurrentUserAvatar(); + if (this.data.toUserId) { + if (!app.globalData.pageState) app.globalData.pageState = {}; + app.globalData.pageState.lastOpenedConvId = this.data.toUserId; + recordConversationOpen({ userId: this.data.toUserId }); + } this.ensureChatReady(); }, onHide() { + if (this.data.toUserId) { + recordConversationOpen({ userId: this.data.toUserId }); + } this.clearPageListeners(); this.markPrivateMessageAsRead(); setTimeout(() => { @@ -55,6 +91,13 @@ Page({ }, 250); }, + onUnload() { + if (this._unsubConfigAvatar) { + this._unsubConfigAvatar(); + this._unsubConfigAvatar = null; + } + }, + ensureChatReady() { const targetUserId = getLocalImUserId(app); if (!targetUserId) return; @@ -133,14 +176,26 @@ Page({ const imId = getLocalImUserId(app); const uid = wx.getStorageSync('uid'); const nick = app.globalData.nicheng || app.globalData.dashouNicheng || wx.getStorageSync('nicheng') || ''; + const defAvatar = getDefaultAvatarUrl(app); + const avatar = resolveAvatarFromStorage(app) || defAvatar; this.setData({ currentUser: { id: imId || ('Ds' + uid), name: nick || `用户${uid}`, - avatar: resolveAvatarUrl(wx.getStorageSync('touxiang'), app), + avatar, }, }); }, + + refreshCurrentUserAvatar() { + const defAvatar = getDefaultAvatarUrl(app); + const cu = this.data.currentUser; + if (!cu) return; + const avatar = resolveAvatarUrl(cu.avatar || wx.getStorageSync('touxiang'), app) || defAvatar; + if (avatar !== cu.avatar) { + this.setData({ currentUser: { ...cu, avatar } }); + } + }, fixAvatar(url) { return resolveAvatarUrl(url, app); }, @@ -156,6 +211,19 @@ Page({ normalizeMessage(msg) { if (!msg) return msg; if (!msg.payload) msg.payload = {}; + const defAvatar = this.data.defaultAvatarUrl || getDefaultAvatarUrl(app); + const myId = this.data.currentUser?.id; + if (myId && msg.senderId === myId) { + msg.senderData = { + name: this.data.currentUser.name, + avatar: resolveAvatarUrl(this.data.currentUser.avatar, app) || defAvatar, + }; + } else { + msg.senderData = { + name: this.data.toName, + avatar: resolveAvatarUrl(this.data.toAvatar, app) || defAvatar, + }; + } if (msg.type === 'image') { const p = msg.payload; if (!p.url) p.url = p.thumbnail || p.thumb || p.imageUrl || ''; @@ -188,63 +256,91 @@ Page({ onPullDownRefresh() { if (this.data.loadingHistory) return; + if (!this.data.hasMore) { + wx.showToast({ title: '没有更早的消息了', icon: 'none' }); + this.setData({ isRefreshing: false }); + wx.stopPullDownRefresh(); + return; + } this.setData({ isRefreshing: true }); - this.loadHistory(false).finally(() => { this.setData({ isRefreshing: false }); wx.stopPullDownRefresh(); }); + this.loadHistory(false).finally(() => { + this.setData({ isRefreshing: false }); + wx.stopPullDownRefresh(); + }); }, - loadHistory(refresh) { - if (!refresh && !this.data.hasMore) return Promise.resolve(); - return new Promise((resolve) => { - this.setData({ loadingHistory: true }); - const { toUserId, lastTimestamp } = this.data; - const ts = refresh ? null : lastTimestamp; - wx.goEasy.im.history({ - type: wx.GoEasy.IM_SCENE.PRIVATE, id: toUserId, lastTimestamp: ts, limit: 20, - onSuccess: (res) => { - let list = res.content || []; - list.sort((a, b) => a.timestamp - b.timestamp); - list.forEach((m, idx) => { - m = this.normalizeMessage(m); - list[idx] = m; - m.formattedTime = formatDate(m.timestamp); - if (idx === 0) m.showTime = true; - else m.showTime = (m.timestamp - list[idx-1].timestamp) / 60000 > 5; - if (m.senderId === this.data.currentUser.id) { - m.senderData = { - name: this.data.currentUser.name, - avatar: resolveAvatarUrl(this.data.currentUser.avatar, app), - }; - } else { - m.senderData = { - name: this.data.toName, - avatar: resolveAvatarUrl(this.data.toAvatar, app), - }; - } - }); - let final = refresh ? list : [...list, ...this.data.messages]; - if (!refresh) { - for (let i=0; i 5; - } - } - const ids = new Set(); - const unique = []; - for (const m of final) { if (!ids.has(m.messageId)) { ids.add(m.messageId); unique.push(m); } } - const update = { - messages: unique, - hasMore: list.length >= 20, - lastTimestamp: unique.length ? unique[0].timestamp : lastTimestamp, - loadingHistory: false - }; - if (refresh) update.scrollToView = 'msg-bottom'; - this.setData(update); - if (refresh) this.markPrivateMessageAsRead(); - resolve(); - }, - onFailed: () => { this.setData({ loadingHistory: false }); resolve(); } + mapHistoryMessage(m, idx, list) { + m = this.normalizeMessage(m); + m.formattedTime = formatDate(m.timestamp); + if (idx === 0) m.showTime = true; + else m.showTime = (m.timestamp - list[idx - 1].timestamp) / 60000 > 5; + if (m.senderId === this.data.currentUser?.id) { + m.senderData = { + name: this.data.currentUser.name, + avatar: resolveAvatarUrl(this.data.currentUser.avatar, app), + }; + } else { + m.senderData = { + name: this.data.toName, + avatar: resolveAvatarUrl(this.data.toAvatar, app), + }; + } + return m; + }, + + async loadHistory(refresh) { + if (!refresh && !this.data.hasMore) return; + if (this.data.loadingHistory) return; + if (!this.data.toUserId) return; + + this.setData({ loadingHistory: true }); + try { + const ready = await waitImConnected(app); + if (!ready) { + wx.showToast({ title: '消息服务未连接,请稍后重试', icon: 'none' }); + return; + } + + const ts = refresh + ? null + : getOldestHistoryTimestamp(this.data.messages, this.data.lastTimestamp); + if (!refresh && ts == null) { + this.setData({ hasMore: false }); + wx.showToast({ title: '没有更早的消息了', icon: 'none' }); + return; + } + + const raw = await fetchHistoryMessages({ + scene: wx.GoEasy.IM_SCENE.PRIVATE, + id: this.data.toUserId, + lastTimestamp: ts, + limit: 20, }); - }); + + const merged = mergeHistoryMessages({ + incoming: raw, + existing: this.data.messages, + refresh, + limit: 20, + mapMessage: (m, idx, list) => this.mapHistoryMessage(m, idx, list), + }); + + const update = { + messages: merged.messages, + hasMore: merged.hasMore, + lastTimestamp: merged.lastTimestamp, + }; + if (refresh) update.scrollToView = 'msg-bottom'; + this.setData(update); + if (refresh) this.markPrivateMessageAsRead(); + } catch (e) { + console.warn('[私聊] 历史消息加载失败', e); + if (!refresh) { + wx.showToast({ title: '加载历史消息失败', icon: 'none' }); + } + } finally { + this.setData({ loadingHistory: false }); + } }, listenNewMsg() { diff --git a/pages/chat/chat.wxml b/pages/chat/chat.wxml index 5dd1fb6..c64b50a 100644 --- a/pages/chat/chat.wxml +++ b/pages/chat/chat.wxml @@ -4,13 +4,13 @@ refresher-enabled="{{true}}" refresher-triggered="{{isRefreshing}}" bindrefresherrefresh="onPullDownRefresh" - style="top: 20rpx; bottom: calc({{bottomSafeHeight}}rpx + {{keyboardHeight}}px + env(safe-area-inset-bottom) + 20rpx);"> + style="top: 20rpx; bottom: calc({{bottomSafeHeight}}rpx + {{keyboardHeight}}px + env(safe-area-inset-bottom) + 24rpx);"> 加载更早消息... {{item.formattedTime}} - + ID: {{item.payload.orderId}} - + {{item.read ? '已读' : '未读'}} - + diff --git a/pages/cs-chat/cs-chat.js b/pages/cs-chat/cs-chat.js index 645fcd6..c8cbe93 100644 --- a/pages/cs-chat/cs-chat.js +++ b/pages/cs-chat/cs-chat.js @@ -1,7 +1,13 @@ const app = getApp(); import { formatDate } from '../../static/lib/utils'; import { getLocalImUserId } from '../../utils/im-user.js'; -import { resolveAvatarUrl } from '../../utils/avatar.js'; +import { resolveAvatarUrl, getDefaultAvatarUrl, resolveAvatarFromStorage, subscribeConfigAvatarRefresh, ensureAvatarUrl } from '../../utils/avatar.js'; +import { + fetchHistoryMessages, + getOldestHistoryTimestamp, + mergeHistoryMessages, + waitImConnected, +} from '../../utils/chat-history.js'; Page({ data: { @@ -29,11 +35,13 @@ Page({ pendingImage: '', pendingImageFile: null, keyboardHeight: 0, - bottomSafeHeight: 10 + bottomSafeHeight: 140, + defaultAvatarUrl: '', }, onLoad(options) { let teamId = '', teamName = '客服', teamAvatar = ''; + const defAvatar = getDefaultAvatarUrl(app); if (options.data) { try { const p = JSON.parse(decodeURIComponent(options.data)); @@ -43,18 +51,51 @@ Page({ } catch (e) { } } - if (!teamAvatar) { - teamAvatar = app.globalData.ossImageUrl + app.globalData.morentouxiang; + let teamAvatarResolved = teamAvatar ? ensureAvatarUrl(teamAvatar, app) : defAvatar; + if (!teamAvatarResolved || !teamAvatarResolved.startsWith('http')) { + teamAvatarResolved = defAvatar; } - this.setData({ teamId, teamName, teamAvatar }); + this.setData({ teamId, teamName, teamAvatar: teamAvatarResolved, defaultAvatarUrl: defAvatar }); wx.setNavigationBarTitle({ title: teamName }); this.initCurrentUser(); + this._unsubConfigAvatar = subscribeConfigAvatarRefresh(this, () => { + const def = getDefaultAvatarUrl(app); + this.setData({ + defaultAvatarUrl: def, + teamAvatar: def, + }); + this.refreshCurrentUserAvatar(); + }); + }, + + onUnload() { + if (this._unsubConfigAvatar) { + this._unsubConfigAvatar(); + this._unsubConfigAvatar = null; + } }, onShow() { + const defAvatar = getDefaultAvatarUrl(app); + const patch = { + defaultAvatarUrl: defAvatar, + teamAvatar: defAvatar, + }; + this.setData(patch); + this.refreshCurrentUserAvatar(); this.autoConnect(); }, + refreshCurrentUserAvatar() { + const defAvatar = getDefaultAvatarUrl(app); + const cu = this.data.currentUser; + if (!cu) return; + const avatar = resolveAvatarUrl(cu.avatar || wx.getStorageSync('touxiang'), app) || defAvatar; + if (avatar !== cu.avatar) { + this.setData({ currentUser: { ...cu, avatar } }); + } + }, + onHide() { this.clearAllListeners(); }, @@ -63,16 +104,25 @@ Page({ const targetUserId = getLocalImUserId(app); if (!targetUserId) return; + const ready = () => { + this.setupAllListeners(); + this.loadHistory(true); + }; + + if (app.ensureConnection) { + app.ensureConnection(); + setTimeout(ready, 400); + return; + } + const status = wx.goEasy.getConnectionStatus ? wx.goEasy.getConnectionStatus() : 'disconnected'; if (status === 'connected' || status === 'reconnected') { const currentUserId = wx.goEasy.im ? wx.goEasy.im.userId : null; if (currentUserId === targetUserId) { - this.setupAllListeners(); - this.loadHistory(true); + ready(); return; - } else { - wx.goEasy.disconnect(); } + wx.goEasy.disconnect(); } this.connectGoEasy(targetUserId); }, @@ -84,7 +134,7 @@ Page({ id: userId, data: { name: currentUser.name, - avatar: resolveAvatarUrl(currentUser.avatar, app) + avatar: resolveAvatarUrl(currentUser.avatar, app) || getDefaultAvatarUrl(app) }, onSuccess: () => { this.setupAllListeners(); @@ -119,11 +169,13 @@ Page({ initCurrentUser() { const imId = getLocalImUserId(app); const uid = wx.getStorageSync('uid'); + const defAvatar = getDefaultAvatarUrl(app); + const avatar = resolveAvatarFromStorage(app) || defAvatar; this.setData({ currentUser: { id: imId || ('Boss' + uid), - name: app.globalData.currentUser?.name || `用户${uid}`, - avatar: resolveAvatarUrl(wx.getStorageSync('touxiang'), app), + name: app.globalData.currentUser?.name || wx.getStorageSync('nicheng') || `用户${uid}`, + avatar, }, }); }, @@ -169,66 +221,89 @@ Page({ this.setData({ messages: msgs }); }, + mapHistoryMessage(m, idx, list) { + m = this.normalizeMessage(m); + m.formattedTime = formatDate(m.timestamp); + if (idx === 0) m.showTime = true; + else m.showTime = (m.timestamp - list[idx - 1].timestamp) / 60000 > 5; + if (m.senderId === this.data.currentUser?.id) { + const av = resolveAvatarUrl(this.data.currentUser.avatar, app) || this.data.defaultAvatarUrl; + m.senderData = { name: this.data.currentUser.name, avatar: av }; + } else { + const av = resolveAvatarUrl(this.data.teamAvatar, app) || this.data.defaultAvatarUrl; + m.senderData = { name: this.data.teamName, avatar: av }; + } + return m; + }, + + injectWelcomeMessage(messages) { + const list = Array.isArray(messages) ? [...messages] : []; + const welcomeId = 'welcome-cs'; + if (!list.some((m) => m.messageId === welcomeId)) { + const welcome = this.createWelcomeMessage(); + welcome.messageId = welcomeId; + welcome.showTime = true; + list.unshift(welcome); + } + return list; + }, + // 历史消息 - loadHistory(refresh) { - if (!refresh && !this.data.hasMore) return Promise.resolve(); - return new Promise((resolve) => { - this.setData({ loadingHistory: true }); - const { teamId, lastTimestamp } = this.data; - const ts = refresh ? null : lastTimestamp; - wx.goEasy.im.history({ - type: wx.GoEasy.IM_SCENE.CS, - id: teamId, + async loadHistory(refresh) { + if (!refresh && !this.data.hasMore) return; + if (this.data.loadingHistory) return; + if (!this.data.teamId) return; + + this.setData({ loadingHistory: true }); + try { + const ready = await waitImConnected(app); + if (!ready) { + wx.showToast({ title: '消息服务未连接,请稍后重试', icon: 'none' }); + return; + } + + const ts = refresh + ? null + : getOldestHistoryTimestamp(this.data.messages, this.data.lastTimestamp); + if (!refresh && ts == null) { + this.setData({ hasMore: false }); + wx.showToast({ title: '没有更早的消息了', icon: 'none' }); + return; + } + + const raw = await fetchHistoryMessages({ + scene: wx.GoEasy.IM_SCENE.CS, + id: this.data.teamId, lastTimestamp: ts, limit: 20, - onSuccess: (res) => { - let list = res.content || []; - list.sort((a, b) => a.timestamp - b.timestamp); - list.forEach((m, idx) => { - m = this.normalizeMessage(m); - list[idx] = m; - m.formattedTime = formatDate(m.timestamp); - if (idx === 0) m.showTime = true; - else m.showTime = (m.timestamp - list[idx-1].timestamp) / 60000 > 5; - if (m.senderId === this.data.currentUser.id) { - m.senderData = { name: this.data.currentUser.name, avatar: this.data.currentUser.avatar }; - } else { - m.senderData = { name: this.data.teamName, avatar: this.data.teamAvatar }; - } - }); - let final = refresh ? list : [...list, ...this.data.messages]; - if (!refresh) { - for (let i=0; i 5; - } - } - const ids = new Set(); - const unique = []; - for (const m of final) { if (!ids.has(m.messageId)) { ids.add(m.messageId); unique.push(m); } } - - // 插入欢迎语 - const welcomeId = 'welcome-cs'; - if (!unique.some(m => m.messageId === welcomeId)) { - const welcome = this.createWelcomeMessage(); - welcome.messageId = welcomeId; - welcome.showTime = true; - unique.unshift(welcome); - } - - const update = { - messages: unique, - hasMore: list.length >= 20, - lastTimestamp: unique.length ? unique[0].timestamp : lastTimestamp, - loadingHistory: false - }; - if (refresh) update.scrollToView = 'msg-bottom'; - this.setData(update); - resolve(); - }, - onFailed: () => { this.setData({ loadingHistory: false }); resolve(); } }); - }); + + let merged = mergeHistoryMessages({ + incoming: raw, + existing: this.data.messages, + refresh, + limit: 20, + mapMessage: (m, idx, list) => this.mapHistoryMessage(m, idx, list), + }); + + merged.messages = this.injectWelcomeMessage(merged.messages); + merged.lastTimestamp = getOldestHistoryTimestamp(merged.messages, merged.lastTimestamp); + + const update = { + messages: merged.messages, + hasMore: merged.hasMore, + lastTimestamp: merged.lastTimestamp, + }; + if (refresh) update.scrollToView = 'msg-bottom'; + this.setData(update); + } catch (e) { + console.warn('[客服] 历史消息加载失败', e); + if (!refresh) { + wx.showToast({ title: '加载历史消息失败', icon: 'none' }); + } + } finally { + this.setData({ loadingHistory: false }); + } }, createWelcomeMessage() { @@ -238,7 +313,10 @@ Page({ timestamp: Date.now(), senderId: 'system', receiverId: this.data.currentUser?.id, - senderData: { name: '系统消息', avatar: '' }, + senderData: { + name: this.data.teamName || '客服', + avatar: resolveAvatarUrl(this.data.teamAvatar, app) || this.data.defaultAvatarUrl, + }, payload: { text: '你好,请问有什么可以帮到您的?' }, status: 'success', read: false, @@ -255,7 +333,10 @@ Page({ if (msg.senderId === this.data.currentUser.id) return; msg = this.normalizeMessage(msg); msg.formattedTime = formatDate(msg.timestamp); - msg.senderData = { name: this.data.teamName, avatar: this.data.teamAvatar }; + msg.senderData = { + name: this.data.teamName, + avatar: resolveAvatarUrl(this.data.teamAvatar, app) || this.data.defaultAvatarUrl, + }; const msgs = this.data.messages; const last = msgs.length ? msgs[msgs.length-1] : null; msg.showTime = last ? (msg.timestamp - last.timestamp) / 60000 > 5 : true; @@ -521,9 +602,30 @@ Page({ onKeyboardHeightChange(e) { this.setData({ keyboardHeight: e.detail.height }); }, + onTeamAvatarError() { + this.setData({ teamAvatar: getDefaultAvatarUrl(app) }); + }, + + onSelfAvatarError() { + const def = getDefaultAvatarUrl(app); + const cu = this.data.currentUser; + if (cu) { + this.setData({ currentUser: { ...cu, avatar: def } }); + } + }, + onPullDownRefresh() { if (this.data.loadingHistory) return; + if (!this.data.hasMore) { + wx.showToast({ title: '没有更早的消息了', icon: 'none' }); + this.setData({ isRefreshing: false }); + wx.stopPullDownRefresh(); + return; + } this.setData({ isRefreshing: true }); - this.loadHistory(false).finally(() => { this.setData({ isRefreshing: false }); wx.stopPullDownRefresh(); }); + this.loadHistory(false).finally(() => { + this.setData({ isRefreshing: false }); + wx.stopPullDownRefresh(); + }); } }); \ No newline at end of file diff --git a/pages/cs-chat/cs-chat.wxml b/pages/cs-chat/cs-chat.wxml index f805235..bc8bc09 100644 --- a/pages/cs-chat/cs-chat.wxml +++ b/pages/cs-chat/cs-chat.wxml @@ -11,7 +11,7 @@ {{item.formattedTime}} - + {{item.payload.text || '[未知消息]'}} - + - + diff --git a/pages/fighter/fighter.js b/pages/fighter/fighter.js index 37d72c3..ff16bca 100644 --- a/pages/fighter/fighter.js +++ b/pages/fighter/fighter.js @@ -1,6 +1,7 @@ // pages/dashouzhongxin/dashouzhongxin.js import { createPage, request, isRoleStatusActive, isCenterPageActive, syncRoleStatuses, syncProfileFields, syncGroupFields, ensureRoleOnCenterPage, clearCacheAndEnterNormal, lockPrimaryRole, migrateLegacyCenterRole, enterLockedRole, parseSceneOptions } from '../../utils/base-page.js' -import { resolveAvatarUrl } from '../../utils/avatar.js' +import { resolveAvatarUrl, getDefaultAvatarUrl } from '../../utils/avatar.js' +import { fetchMyZhidingOrders, processZhidingList, filterZhidingBannerList } from '../../utils/zhiding-order.js' import { openPrivateChat, buildInviterPeerId } from '../../utils/im-user.js' import { isStaffMode, getStaffContext } from '../../utils/staff-api.js' import { ensurePhoneAuth } from '../../utils/phone-auth.js' @@ -92,6 +93,20 @@ Page(createPage({ scrollViewRefreshing: false, showRechargeBanners: false, showPindaoEntry: true, + + myZhidingList: [], + myZhidingCount: 0, + showBadgeZone: false, + }, + + _syncBadgeZoneFlags(patch = {}) { + const d = { ...this.data, ...patch }; + const showBadgeZone = !!( + (d.chenghaoList && d.chenghaoList.length > 0) + || (d.isGuanshi && d.guanshiChenghaoList && d.guanshiChenghaoList.length > 0) + || (d.identityTagList && d.identityTagList.length > 0) + ); + return { ...patch, showBadgeZone }; }, _buildImgUrls(ossImageUrl) { @@ -246,6 +261,7 @@ Page(createPage({ if (!this.data.isDashou) this.setData({ isDashou: true }); ensureRoleOnCenterPage(this, 'dashou'); this.loadExamStatus(); + this.loadMyZhidingOrders(); } else if (isRoleStatusActive(wx.getStorageSync('guanshistatus')) || isRoleStatusActive(wx.getStorageSync('zuzhangstatus')) || isRoleStatusActive(wx.getStorageSync('kaoheguanstatus'))) { @@ -266,6 +282,7 @@ Page(createPage({ }, 300); } } + if (app.startImWhenReady) app.startImWhenReady(); }, checkRoleStatuses() { @@ -283,6 +300,7 @@ Page(createPage({ }, () => { this._updateTotalAssets(); this._updatePendingAuth(); + this.setData(this._syncBadgeZoneFlags()); }); if (isDashou) { const uid = wx.getStorageSync('uid') || ''; @@ -361,6 +379,7 @@ Page(createPage({ this._fetchZuzhangInfoSilent(), this._fetchKaoheguanInfoSilent(), this.loadIdentityTags(), + this.loadMyZhidingOrders(), ]); this.checkRoleStatuses(); const dashouOk = results[0].status === 'fulfilled'; @@ -410,7 +429,7 @@ Page(createPage({ ...(d.zuzhang || []), ...(d.guanshi || []), ]; - this.setData({ identityTagList: merged }); + this.setData(this._syncBadgeZoneFlags({ identityTagList: merged })); } } catch (e) { /* 静默 */ @@ -548,7 +567,7 @@ Page(createPage({ data: { role: 'guanshi' }, }); if (res && res.data.code === 0 && res.data.data) { - this.setData({ guanshiChenghaoList: res.data.data.chenghao_list || [] }); + this.setData(this._syncBadgeZoneFlags({ guanshiChenghaoList: res.data.data.chenghao_list || [] })); } } catch (e) {} }, @@ -752,11 +771,11 @@ Page(createPage({ }); if (res && res.data.code === 0 && res.data.data) { const list = res.data.data.chenghao_list || []; - this.setData({ chenghaoList: list }); + this.setData(this._syncBadgeZoneFlags({ chenghaoList: list })); const app = getApp(); app.globalData.chenghaoList = list; } else { - this.setData({ chenghaoList: [] }); + this.setData(this._syncBadgeZoneFlags({ chenghaoList: [] })); } } catch (e) { } @@ -1130,7 +1149,44 @@ Page(createPage({ } wx.navigateTo({ url: '/pages/verify/verify?type=shangjia' }); }, - goToReceiveOrder() { wx.navigateTo({ url: '/pages/accept-order/accept-order' }) }, + goToReceiveOrder() { + wx.switchTab({ url: '/pages/accept-order/accept-order' }); + }, + + async loadMyZhidingOrders() { + if (!isCenterPageActive(this, 'isDashou', 'dashoustatus')) return; + try { + const raw = await fetchMyZhidingOrders(); + const uid = this.data.uid || wx.getStorageSync('uid'); + const list = filterZhidingBannerList(processZhidingList(raw, getApp(), uid)); + this.setData({ myZhidingList: list, myZhidingCount: list.length }); + } catch (e) { + console.warn('fighter loadMyZhidingOrders', e); + } + }, + + goToZhidingFromMine() { + const app = getApp(); + const first = (this.data.myZhidingList || [])[0]; + if (first && first.dingdan_id) { + app.globalData._acceptOrderHighlight = first.dingdan_id; + } + wx.switchTab({ + url: '/pages/accept-order/accept-order', + fail: () => wx.showToast({ title: '跳转失败', icon: 'none' }), + }); + }, + + openZhidingDetailFromMine(e) { + const item = e.currentTarget.dataset.item; + const id = item?.dingdan_id || e.currentTarget.dataset.id; + if (!id) return; + getApp().globalData._acceptOrderHighlight = id; + wx.switchTab({ + url: '/pages/accept-order/accept-order', + fail: () => wx.showToast({ title: '跳转失败', icon: 'none' }), + }); + }, goToDashouExam() { wx.navigateTo({ url: '/pages/dashou-exam/dashou-exam' }) }, async loadExamStatus() { diff --git a/pages/fighter/fighter.json b/pages/fighter/fighter.json index 0f3a586..b4b393c 100644 --- a/pages/fighter/fighter.json +++ b/pages/fighter/fighter.json @@ -3,7 +3,8 @@ "global-notification": "/components/global-notification/global-notification", "chenghao-tag": "/components/chenghao-tag/chenghao-tag", "tab-bar": "/tab-bar/index", - "pindao-modal": "/components/pindao-modal/pindao-modal" + "pindao-modal": "/components/pindao-modal/pindao-modal", + "kefu-float": "/components/kefu-float/kefu-float" }, "navigationStyle": "custom", "backgroundColor": "#f5f3ff", diff --git a/pages/fighter/fighter.wxml b/pages/fighter/fighter.wxml index 198e4e6..1c46343 100644 --- a/pages/fighter/fighter.wxml +++ b/pages/fighter/fighter.wxml @@ -18,6 +18,7 @@ + @@ -70,8 +71,17 @@ + + + + 指定 + {{myZhidingCount}}单待接待 + + 去处理 › + + - + @@ -253,6 +263,7 @@ + diff --git a/pages/fighter/fighter.wxss b/pages/fighter/fighter.wxss index b70fa4f..29ede51 100644 --- a/pages/fighter/fighter.wxss +++ b/pages/fighter/fighter.wxss @@ -652,3 +652,39 @@ page { @keyframes fighterSpin { to { transform: rotate(360deg); } } + +.my-zhiding-mine-strip { + margin: 0 24rpx 16rpx; + padding: 20rpx 24rpx; + border-radius: 16rpx; + background: linear-gradient(90deg, #ff6b00, #ff4500); + border: 2rpx solid #ff3300; + box-shadow: 0 8rpx 24rpx rgba(255, 69, 0, 0.4); + display: flex; + align-items: center; + justify-content: space-between; +} +.my-zhiding-mine-strip:active { opacity: 0.92; } +.my-zhiding-mine-strip-left { + display: flex; + align-items: center; + gap: 12rpx; +} +.my-zhiding-mine-badge { + font-size: 22rpx; + font-weight: 800; + color: #ff4500; + background: #fff; + padding: 4rpx 14rpx; + border-radius: 999rpx; +} +.my-zhiding-mine-count { + font-size: 28rpx; + color: #fff; + font-weight: 700; +} +.my-zhiding-mine-arrow { + font-size: 26rpx; + color: #fff; + font-weight: 700; +} diff --git a/pages/group-chat/group-chat.js b/pages/group-chat/group-chat.js index 348cae3..6930909 100644 --- a/pages/group-chat/group-chat.js +++ b/pages/group-chat/group-chat.js @@ -1,10 +1,16 @@ const app = getApp(); import { formatDate } from '../../static/lib/utils'; import { sendGroupMessage } from '../../utils/message-sender'; -import { showConfirmByScene } from '../../utils/scriptService.js'; -import { normalizeGroupMessage, getZhuangtaiText } from '../../utils/group-chat.js'; -import { persistGroupMeta } from '../../utils/im-user.js'; -import { getFreshImUser } from '../../utils/im-user.js'; +import { getLocalImUserId } from '../../utils/im-user.js'; +import { resolveAvatarUrl, getDefaultAvatarUrl, resolveAvatarFromStorage } from '../../utils/avatar.js'; +import { getZhuangtaiText, isPairGroupId, normalizeGroupMessage, recordConversationOpen } from '../../utils/group-chat.js'; +import request from '../../utils/request'; +import { + fetchHistoryMessages, + getOldestHistoryTimestamp, + mergeHistoryMessages, + waitImConnected, +} from '../../utils/chat-history.js'; Page({ data: { @@ -15,8 +21,8 @@ Page({ isCross: 0, orderZhuangtai: null, orderZhuangtaiText: '', - orderJine: '', orderJieshao: '', + orderJine: '', currentUser: null, messages: [], inputText: '', @@ -34,8 +40,10 @@ Page({ lastTapMsgId: '', emojiList: ['😀','😁','😂','🤣','😃','😄','😅','😆','😉','😊','😋','😎','😍','😘','😗','😙','😚','🙂','🤗','🤩','🤔','🤨','😐','😑','😶','🙄','😏','😣','😥','😮','🤐','😯','😪','😫','😴','😌','😛','😜','😝','🤤','😒','😓','😔','😕','🙃','🤑','😲','☹','🙁','😖','😞','😟','😤','😢','😭','😦','😧','😨','😩','🤯','😬','😰','😱','🥵','🥶','😳','🤪','😵','😡','😠','🤬','😷','🤒','🤕','🤢','🤮','🤧','😇','🤠','🤡','🤥','🤫','🤭','🧐','🤓','😈','👿','👹','👺','💀','👻','👽','🤖','💩','😺','😸','😹','😻','😼','😽','🙀','😿','😾'], pendingImage: '', + pendingImageFile: null, keyboardHeight: 0, - bottomSafeHeight: 10 + bottomSafeHeight: 140, + defaultAvatarUrl: '', }, onLoad(options) { let p = { groupId: '', groupName: '订单群聊', groupAvatar: '', orderId: '', isCross: 0, @@ -46,182 +54,278 @@ Page({ p = { ...p, ...d }; } catch (e) {} } + const orderIdStr = String(p.orderId || '').replace(/^group_/, '') || + (p.groupId && !isPairGroupId(p.groupId) ? String(p.groupId).replace(/^group_/, '') : ''); + const groupId = p.groupId + ? String(p.groupId) + : (orderIdStr ? `group_${orderIdStr}` : ''); + const zt = p.orderZhuangtai; + const defAvatar = getDefaultAvatarUrl(app); this.setData({ - groupId: p.groupId, + groupId, groupName: p.groupName, - groupAvatar: p.groupAvatar, - orderId: p.orderId, - isCross: p.isCross + groupAvatar: resolveAvatarUrl(p.groupAvatar, app) || defAvatar, + orderId: orderIdStr, + isCross: p.isCross, + orderJieshao: p.orderDesc || p.orderJieshao || '', + orderJine: p.orderJine || '', + orderZhuangtai: zt, + orderZhuangtaiText: zt != null ? getZhuangtaiText(zt) : '', + defaultAvatarUrl: defAvatar, }); wx.setNavigationBarTitle({ title: this.data.groupName }); - // 如果有外部传入的 currentUser 信息,直接用,不再依赖全局状态 - if (p.currentUserId) { - this.setData({ - currentUser: { - id: p.currentUserId, - name: p.currentUserName || `用户${p.currentUserId.replace(/^[A-Za-z]+/,'')}`, - avatar: p.currentUserAvatar || (app.globalData.ossImageUrl + app.globalData.morentouxiang) - } - }); - } else { + this.ensureCurrentUser(p); + }, + + ensureCurrentUser(p) { + p = p || {}; + const localImId = getLocalImUserId(app); + const uid = wx.getStorageSync('uid') || ''; + const userId = localImId || p.currentUserId || ''; + if (!userId) { this.initCurrentUser(); - } - - const meta = (app.globalData.groupInfoMap && app.globalData.groupInfoMap[p.groupId]) || {}; - const initZt = p.orderZhuangtai != null ? p.orderZhuangtai : meta.orderZhuangtai; - if (initZt != null) { - this.applyOrderStatus(initZt, { - orderId: p.orderId || meta.orderId, - jine: p.orderJine || meta.orderJine, - jieshao: p.orderDesc || meta.orderDesc, - }); - } - }, - - onShow() { - app.globalData.pageState.isInChatPage = true; - app.globalData.pageState.currentChatId = this.data.groupId; - app.globalData.pageState.isInGroupChat = true; - this.ensureChatConnection(); - }, - - onHide() { - app.globalData.pageState.isInChatPage = false; - app.globalData.pageState.currentChatId = ''; - app.globalData.pageState.isInGroupChat = false; - this.clearPageListeners(); - }, - - applyOrderStatus(zhuangtai, extra) { - if (zhuangtai == null && zhuangtai !== 0) return; - const patch = { - orderZhuangtai: zhuangtai, - orderZhuangtaiText: getZhuangtaiText(zhuangtai), - }; - if (extra) { - if (extra.jine) patch.orderJine = extra.jine; - if (extra.jieshao) patch.orderJieshao = extra.jieshao; - if (extra.orderId) patch.orderId = extra.orderId; - } - this.setData(patch); - - const groupId = this.data.groupId; - if (!groupId) return; - if (!app.globalData.groupInfoMap) app.globalData.groupInfoMap = {}; - const meta = { ...(app.globalData.groupInfoMap[groupId] || {}), orderZhuangtai: zhuangtai }; - if (extra && extra.orderId) meta.orderId = extra.orderId; - app.globalData.groupInfoMap[groupId] = meta; - persistGroupMeta(app, groupId, meta); - }, - - applyOrderCardPayload(payload) { - if (!payload) return; - const cardOrderId = payload.orderId || payload.dingdan_id; - if (cardOrderId && this.data.orderId && cardOrderId !== this.data.orderId) return; - const zt = payload.zhuangtai != null ? payload.zhuangtai : this.data.orderZhuangtai; - payload.zhuangtaiText = getZhuangtaiText(zt); - this.applyOrderStatus(zt, { - orderId: payload.orderId || payload.dingdan_id || this.data.orderId, - jine: payload.jine, - jieshao: payload.jieshao, - }); - this.updateOrderBubblesInList(payload); - }, - - updateOrderBubblesInList(payload) { - if (!payload) return; - const orderId = payload.orderId || payload.dingdan_id; - if (!orderId) return; - const messages = this.data.messages.map((m) => { - if (m.type !== 'order' || !m.payload) return m; - const oid = m.payload.orderId || m.payload.dingdan_id; - if (oid !== orderId) return m; - return { ...m, payload: { ...m.payload, ...payload, orderId: oid } }; - }); - this.setData({ messages }); - }, - - mergeServerMessage(localId, status, serverMsg) { - if (!serverMsg) { - this.updateMsg(localId, status); return; } - normalizeGroupMessage(serverMsg); - serverMsg.formattedTime = formatDate(serverMsg.timestamp); - if (serverMsg.type === 'order') { - this.applyOrderCardPayload(serverMsg.payload); + const defAvatar = getDefaultAvatarUrl(app); + const rawAvatar = p.currentUserAvatar + || wx.getStorageSync('touxiang') + || app.globalData.currentUser?.avatar + || ''; + const name = p.currentUserName + || app.globalData.currentUser?.name + || wx.getStorageSync('nicheng') + || app.globalData.dashouNicheng + || app.globalData.nicheng + || `用户${uid}`; + const avatar = resolveAvatarUrl(rawAvatar, app) || defAvatar; + const currentUser = { id: userId, name, avatar: avatar || defAvatar }; + app.globalData.currentUser = { ...app.globalData.currentUser, ...currentUser }; + this.setData({ currentUser }); + }, + + getMyChatProfile() { + const cu = this.data.currentUser; + const defAvatar = getDefaultAvatarUrl(app); + if (cu && cu.id) { + return { + id: cu.id, + name: cu.name || `用户${wx.getStorageSync('uid') || ''}`, + avatar: resolveAvatarUrl(cu.avatar, app) || defAvatar, + }; + } + const imId = getLocalImUserId(app); + const uid = wx.getStorageSync('uid') || ''; + return { + id: imId || '', + name: wx.getStorageSync('nicheng') || app.globalData.nicheng || `用户${uid}`, + avatar: resolveAvatarFromStorage(app) || defAvatar, + }; + }, + + initCurrentUser() { + const profile = this.getMyChatProfile(); + if (!profile.id) return; + this.setData({ currentUser: profile }); + app.globalData.currentUser = { ...app.globalData.currentUser, ...profile }; + }, + + buildImageFile(tempPath, size) { + return { + path: tempPath, + tempFilePath: tempPath, + size: size || 0, + }; + }, + + normalizeMessage(msg) { + if (!msg) return msg; + msg = normalizeGroupMessage(msg); + if (!msg.payload) msg.payload = {}; + const defAvatar = getDefaultAvatarUrl(app); + const me = this.getMyChatProfile(); + if (me.id && msg.senderId === me.id) { + msg.senderData = { + name: me.name, + avatar: me.avatar || defAvatar, + }; + } else if (msg.senderData?.avatar) { + msg.senderData.avatar = resolveAvatarUrl(msg.senderData.avatar, app) || defAvatar; + } else if (msg.senderData) { + msg.senderData.avatar = defAvatar; + } + if (msg.type === 'image' && msg.payload?.url && !String(msg.payload.url).startsWith('http')) { + msg.payload.url = resolveAvatarUrl(msg.payload.url, app) || msg.payload.url; + } + return msg; + }, + + mapIdentityForApi() { + const role = app.globalData.currentRole || wx.getStorageSync('currentRole') || 'normal'; + if (role === 'dashou') return 'dashou'; + if (role === 'shangjia') return 'shangjia'; + return 'normal'; + }, + + async refreshLatestOrderBar() { + const { groupId, orderId } = this.data; + if (!groupId && !orderId) return; + try { + const res = await request({ + url: '/dingdan/ltpddd', + method: 'POST', + data: { + groupId, + dingdan_id: orderId, + identityType: this.mapIdentityForApi(), + }, + }); + const body = res?.data || {}; + const latest = body.data?.latest; + if (!latest) return; + this.setData({ + orderId: latest.dingdan_id, + orderJieshao: latest.jieshao || '', + orderJine: latest.jine || '', + orderZhuangtai: latest.zhuangtai, + orderZhuangtaiText: getZhuangtaiText(latest.zhuangtai), + }); + } catch (e) { + console.warn('刷新最新订单条失败', e); + } + }, + + async refreshOrderStatus() { + const { orderId } = this.data; + if (!orderId) return; + try { + const res = await request({ + url: '/dingdan/ltddzt', + method: 'POST', + data: { + dingdan_id: orderId, + identityType: this.mapIdentityForApi(), + }, + }); + const body = res?.data || {}; + const st = body.data; + if (!st) return; + this.setData({ + orderJieshao: st.jieshao || this.data.orderJieshao, + orderJine: st.jine || this.data.orderJine, + orderZhuangtai: st.zhuangtai, + orderZhuangtaiText: getZhuangtaiText(st.zhuangtai), + }); + } catch (e) { + console.warn('刷新订单状态失败', e); + } + }, + + startOrderStatusPoll() { + this.stopOrderStatusPoll(); + this._orderPollTimer = setInterval(() => this.refreshOrderStatus(), 15000); + }, + + stopOrderStatusPoll() { + if (this._orderPollTimer) { + clearInterval(this._orderPollTimer); + this._orderPollTimer = null; + } + }, + + mergeSentMessage(localId, sent) { + if (!sent) { + this.updateMsg(localId, 'success'); + return; } const msgs = this.data.messages.map((m) => { if (m.messageId !== localId) return m; - return { - ...serverMsg, - showTime: m.showTime, - status: status || 'success', - formattedTime: serverMsg.formattedTime || m.formattedTime, - }; + return this.normalizeMessage({ + ...m, + ...sent, + status: 'success', + messageId: sent.messageId || m.messageId, + payload: sent.payload || m.payload, + senderData: m.senderData || sent.senderData, + }); }); this.setData({ messages: msgs }); }, - async ensureChatConnection() { - const role = app.globalData.currentRole || 'normal'; - const uid = wx.getStorageSync('uid'); - const prefixMap = { normal:'Boss', dashou:'Ds', shangjia:'Sj', guanshi:'Gs', zuzhang:'Zz', kaoheguan:'Kh' }; - const pageUserId = this.data.currentUser?.id; - const targetUserId = pageUserId || (prefixMap[role] || 'Boss') + uid; - - const onReady = () => { - this.subscribeGroupIfNeeded() - .then(() => { - this.setupAllListeners(); - return this.loadHistory(true); - }) - .then(() => this.markGroupMessageAsRead()); - }; - - const connectedId = app.globalData?.goEasyConnection?.userId || wx.goEasy?.im?.userId; - const status = wx.goEasy?.getConnectionStatus?.() || 'disconnected'; - const imOk = status === 'connected' || status === 'reconnected'; - - if (imOk && connectedId === targetUserId) { - onReady(); - return; - } - - try { - if (app.ensureImForRole) { - await app.ensureImForRole(role); - } else if (app.connectWithIdentity) { - await app.connectWithIdentity(role, targetUserId, true); - } - onReady(); - } catch (e) { - const s = wx.goEasy?.getConnectionStatus?.(); - if (s === 'connected' || s === 'reconnected') onReady(); - else wx.showToast({ title: '连接失败,请重试', icon: 'none' }); + onShow() { + const defAvatar = getDefaultAvatarUrl(app); + this.setData({ + defaultAvatarUrl: defAvatar, + groupAvatar: resolveAvatarUrl(this.data.groupAvatar, app) || defAvatar, + }); + this.ensureCurrentUser({}); + if (this.data.groupId) { + if (!app.globalData.pageState) app.globalData.pageState = {}; + app.globalData.pageState.lastOpenedConvId = this.data.groupId; + recordConversationOpen({ groupId: this.data.groupId }); } + this.refreshLatestOrderBar(); + this.startOrderStatusPoll(); + this.autoConnect(); }, - clearPageListeners() { + onHide() { + if (this.data.groupId) { + recordConversationOpen({ groupId: this.data.groupId }); + } + this.stopOrderStatusPoll(); this.clearAllListeners(); }, + onUnload() { + this.stopOrderStatusPoll(); + }, + + autoConnect() { + const targetUserId = this.data.currentUser?.id || getLocalImUserId(app); + if (!targetUserId) return; + + const afterReady = () => { + this.subscribeGroupIfNeeded().then(() => { + this.setupAllListeners(); + this.loadHistory(true); + this.markGroupMessageAsRead(); + }); + }; + + if (app.ensureConnection) { + app.ensureConnection(); + setTimeout(afterReady, 400); + return; + } + + const status = wx.goEasy.getConnectionStatus ? wx.goEasy.getConnectionStatus() : 'disconnected'; + if (status === 'connected' || status === 'reconnected') { + const currentUserId = wx.goEasy.im ? wx.goEasy.im.userId : null; + if (currentUserId === targetUserId) { + afterReady(); + return; + } + wx.goEasy.disconnect(); + } + this.connectGoEasy(targetUserId); + }, + connectGoEasy(userId) { - const { currentUser } = this.data; - if (!currentUser) return; + const me = this.getMyChatProfile(); + if (!me.id) return; + const defAvatar = getDefaultAvatarUrl(app); wx.goEasy.connect({ id: userId, data: { - name: currentUser.name, - avatar: currentUser.avatar || (app.globalData.ossImageUrl + app.globalData.morentouxiang) + name: me.name, + avatar: me.avatar || defAvatar, }, onSuccess: () => { - this.subscribeGroupIfNeeded() - .then(() => { - this.setupAllListeners(); - return this.loadHistory(true); - }) - .then(() => this.markGroupMessageAsRead()); + this.subscribeGroupIfNeeded().then(() => { + this.setupAllListeners(); + this.loadHistory(true); + this.markGroupMessageAsRead(); + }); }, onFailed: (error) => { if (error.code === 408) { @@ -238,17 +342,14 @@ Page({ subscribeGroupIfNeeded() { const { groupId } = this.data; - if (!groupId || !wx.goEasy?.im) return Promise.resolve(); + if (!groupId || !wx.goEasy?.im?.subscribeGroup) return Promise.resolve(); return new Promise((resolve) => { wx.goEasy.im.subscribeGroup({ - groupIds: [groupId], - onSuccess: () => { - console.log(`[群聊页] 订阅成功: ${groupId}`); - resolve(); - }, - onFailed: (err) => { - console.error(`[群聊页] 订阅失败: ${groupId}`, err); - resolve(); + groupIds: [String(groupId)], + onSuccess: () => resolve(true), + onFailed: (error) => { + console.warn('subscribeGroup failed', error); + resolve(false); }, }); }); @@ -267,23 +368,6 @@ Page({ if (this._deleteHandler) { wx.goEasy.im.off(wx.GoEasy.IM_EVENT.MESSAGE_DELETED, this._deleteHandler); this._deleteHandler = null; } }, - initCurrentUser() { - const uid = wx.getStorageSync('uid'); - const role = app.globalData.currentRole || 'normal'; - const fresh = getFreshImUser(app, role, uid); - this.setData({ currentUser: { - id: fresh.id, - name: fresh.name, - avatar: fresh.avatar, - }}); - }, - - fixAvatar(url) { - if (!url) return app.globalData.ossImageUrl + app.globalData.morentouxiang; - if (url.startsWith('http')) return url; - return app.globalData.ossImageUrl + url; - }, - isConnected() { const s = wx.goEasy.getConnectionStatus ? wx.goEasy.getConnectionStatus() : 'disconnected'; return s === 'connected' || s === 'reconnected'; @@ -291,78 +375,110 @@ Page({ onPullDownRefresh() { if (this.data.loadingHistory) return; + if (!this.data.hasMore) { + wx.showToast({ title: '没有更早的消息了', icon: 'none' }); + this.setData({ isRefreshing: false }); + wx.stopPullDownRefresh(); + return; + } this.setData({ isRefreshing: true }); - this.loadHistory(false).finally(() => { this.setData({ isRefreshing: false }); wx.stopPullDownRefresh(); }); + this.loadHistory(false).finally(() => { + this.setData({ isRefreshing: false }); + wx.stopPullDownRefresh(); + }); }, - loadHistory(refresh) { - if (!refresh && !this.data.hasMore) return Promise.resolve(); - return new Promise((resolve) => { - this.setData({ loadingHistory: true }); - const { groupId, lastTimestamp } = this.data; - const ts = refresh ? null : lastTimestamp; - wx.goEasy.im.history({ - type: wx.GoEasy.IM_SCENE.GROUP, - id: groupId, - lastTimestamp: ts, - limit: 20, - onSuccess: (res) => { - let list = res.content || []; - list.sort((a, b) => a.timestamp - b.timestamp); - list.forEach((m, idx) => { - normalizeGroupMessage(m); - m.formattedTime = formatDate(m.timestamp); - if (idx === 0) m.showTime = true; - else m.showTime = (m.timestamp - list[idx-1].timestamp) / 60000 > 5; - if (!m.senderData) m.senderData = { name: '未知用户', avatar: '' }; - if (m.type === 'order' && m.payload) { - this.applyOrderCardPayload(m.payload); - } - }); - let final = refresh ? list : [...list, ...this.data.messages]; - if (!refresh) { - for (let i = 0; i < final.length; i++) { - if (i === 0) final[i].showTime = true; - else final[i].showTime = (final[i].timestamp - final[i-1].timestamp) / 60000 > 5; - } - } - const ids = new Set(); - const unique = []; - for (const m of final) { if (!ids.has(m.messageId)) { ids.add(m.messageId); unique.push(m); } } - const update = { - messages: unique, - hasMore: list.length >= 20, - lastTimestamp: unique.length ? unique[0].timestamp : lastTimestamp, - loadingHistory: false - }; - if (refresh) update.scrollToView = 'msg-bottom'; - this.setData(update); - resolve(); - }, - onFailed: () => { this.setData({ loadingHistory: false }); resolve(); } - }); + mapHistoryMessage(m, idx, list) { + m = this.normalizeMessage(m); + m.formattedTime = formatDate(m.timestamp); + if (idx === 0) m.showTime = true; + else m.showTime = (m.timestamp - list[idx - 1].timestamp) / 60000 > 5; + if (!m.senderData) m.senderData = { name: '未知用户', avatar: '' }; + return m; + }, + + async fetchGroupHistoryPage(gid, lastTimestamp) { + return fetchHistoryMessages({ + scene: wx.GoEasy.IM_SCENE.GROUP, + id: gid, + lastTimestamp, + limit: 20, }); }, + async loadHistory(refresh) { + if (!refresh && !this.data.hasMore) return; + if (this.data.loadingHistory) return; + if (!this.data.groupId) return; + + this.setData({ loadingHistory: true }); + try { + const ready = await waitImConnected(app); + if (!ready) { + wx.showToast({ title: '消息服务未连接,请稍后重试', icon: 'none' }); + return; + } + + await this.subscribeGroupIfNeeded(); + + const ts = refresh + ? null + : getOldestHistoryTimestamp(this.data.messages, this.data.lastTimestamp); + if (!refresh && ts == null) { + this.setData({ hasMore: false }); + wx.showToast({ title: '没有更早的消息了', icon: 'none' }); + return; + } + + let raw = await this.fetchGroupHistoryPage(this.data.groupId, ts); + + if (refresh && raw.length === 0 && isPairGroupId(this.data.groupId) && this.data.orderId) { + const legacyId = `group_${this.data.orderId}`; + if (legacyId !== this.data.groupId) { + raw = await this.fetchGroupHistoryPage(legacyId, ts); + } + } + + const merged = mergeHistoryMessages({ + incoming: raw, + existing: this.data.messages, + refresh, + limit: 20, + mapMessage: (m, idx, list) => this.mapHistoryMessage(m, idx, list), + }); + + const update = { + messages: merged.messages, + hasMore: merged.hasMore, + lastTimestamp: merged.lastTimestamp, + }; + if (refresh) update.scrollToView = 'msg-bottom'; + this.setData(update); + } catch (e) { + console.warn('[群聊] 历史消息加载失败', e); + if (!refresh) { + wx.showToast({ title: '加载历史消息失败', icon: 'none' }); + } + } finally { + this.setData({ loadingHistory: false }); + } + }, + listenNewMsg() { if (this._msgHandler) wx.goEasy.im.off(wx.GoEasy.IM_EVENT.GROUP_MESSAGE_RECEIVED, this._msgHandler); this._msgHandler = (msg) => { if (msg.groupId !== this.data.groupId) return; - normalizeGroupMessage(msg); - if (msg.type === 'order' && msg.payload) { - this.applyOrderCardPayload(msg.payload); - } - if (msg.senderId === this.data.currentUser.id && msg.type !== 'order') return; + if (msg.senderId === this.data.currentUser.id) return; + msg = this.normalizeMessage(msg); msg.formattedTime = formatDate(msg.timestamp); const msgs = this.data.messages; - if (msgs.some((m) => m.messageId === msg.messageId)) return; const last = msgs.length ? msgs[msgs.length-1] : null; msg.showTime = last ? (msg.timestamp - last.timestamp) / 60000 > 5 : true; this.setData({ messages: [...msgs, msg], scrollToView: 'msg-bottom' }); this.markGroupMessageAsRead(); }; wx.goEasy.im.on(wx.GoEasy.IM_EVENT.GROUP_MESSAGE_RECEIVED, this._msgHandler); - this.markGroupMessageAsRead(); // ← 收到新消息就标记已读 + this.markGroupMessageAsRead(); }, markGroupMessageAsRead() { @@ -397,21 +513,31 @@ Page({ chooseImage() { const that = this; - wx.chooseImage({ - count: 1, sizeType: ['compressed'], sourceType: ['album', 'camera'], - success(res) { that.setData({ pendingImage: res.tempFilePaths[0] }); } + wx.chooseMedia({ + count: 1, + mediaType: ['image'], + sizeType: ['compressed'], + sourceType: ['album', 'camera'], + success(res) { + const file = (res.tempFiles && res.tempFiles[0]) || null; + if (!file || !file.tempFilePath) return; + that.setData({ + pendingImage: file.tempFilePath, + pendingImageFile: that.buildImageFile(file.tempFilePath, file.size), + }); + }, + fail() { + wx.showToast({ title: '选择图片失败', icon: 'none' }); + }, }); }, - clearPendingImage() { this.setData({ pendingImage: '' }); }, + clearPendingImage() { this.setData({ pendingImage: '', pendingImageFile: null }); }, sendMessage() { if (this.data.pendingImage) { - const file = this.data.pendingImage; - showConfirmByScene('chat_image_confirm', () => { - this.sendImageMsg(file); - this.setData({ pendingImage: '' }); - }); + this.sendImageMsg(this.data.pendingImage, this.data.pendingImageFile); + this.setData({ pendingImage: '', pendingImageFile: null }); return; } const text = this.data.inputText.trim(); @@ -442,18 +568,61 @@ Page({ } that.setData({ messages: [...msgs, localMsg], scrollToView: 'msg-bottom' }); }, - onSuccess: (messageId, status, serverMsg) => { - that.mergeServerMessage(messageId, status, serverMsg); + onSuccess: (messageId, status) => { + that.updateMsg(messageId, status); } }); }, - sendImageMsg(file) { + sendImageMsg(filePath, fileObj) { const that = this; + const { currentUser, groupId, orderId, isCross, groupName, groupAvatar } = that.data; + if (!filePath || !currentUser || !groupId) return; + + if (isCross === 0) { + if (!wx.goEasy || !wx.goEasy.im) { + wx.showToast({ title: '聊天未连接', icon: 'none' }); + return; + } + const avatar = resolveAvatarUrl(currentUser.avatar, app); + const name = currentUser.name || '用户'; + const localId = `image-${Date.now()}`; + const imageFile = fileObj || this.buildImageFile(filePath, 0); + const localMsg = { + messageId: localId, + type: 'image', + timestamp: Date.now(), + senderId: currentUser.id, + groupId, + senderData: { name, avatar }, + payload: { url: filePath }, + status: 'sending', + formattedTime: formatDate(Date.now()), + showTime: this.needShow(Date.now()), + }; + that.setData({ messages: [...that.data.messages, localMsg], scrollToView: 'msg-bottom' }); + const toData = { name: groupName || '订单群聊', avatar: groupAvatar || avatar, isCross: 0 }; + if (orderId) toData.orderId = orderId; + const imgMsg = wx.goEasy.im.createImageMessage({ + file: imageFile, + to: { type: wx.GoEasy.IM_SCENE.GROUP, id: groupId, data: toData }, + }); + wx.goEasy.im.sendMessage({ + message: imgMsg, + onSuccess: (sent) => that.mergeSentMessage(localId, sent), + onFailed: (error) => { + console.error('[群聊] 图片发送失败', error); + that.updateMsg(localId, 'failed'); + wx.showToast({ title: '图片发送失败', icon: 'none' }); + }, + }); + return; + } + sendGroupMessage({ msgData: { type: 'image', - filePath: file, + filePath, currentUser: that.data.currentUser, groupId: that.data.groupId, orderId: that.data.orderId, @@ -470,8 +639,11 @@ Page({ } that.setData({ messages: [...msgs, localMsg], scrollToView: 'msg-bottom' }); }, - onSuccess: (messageId, status, serverMsg) => { - that.mergeServerMessage(messageId, status, serverMsg); + onSuccess: (messageId, status) => { + that.updateMsg(messageId, status); + if (status === 'failed') { + wx.showToast({ title: '图片发送失败', icon: 'none' }); + } } }); }, @@ -506,7 +678,7 @@ Page({ const msgId = e.currentTarget.dataset.messageid; if (!msgId) return; const msg = this.data.messages.find(m => m.messageId === msgId); - if (!msg || String(msg.messageId).startsWith('local-')) return; + if (!msg || msg.messageId.startsWith('local-')) return; const itemList = ['复制']; if (msg.senderId === this.data.currentUser.id && (Date.now() - msg.timestamp < 120000) && !msg.recalled) { itemList.push('撤回'); @@ -575,7 +747,9 @@ Page({ orderId: order.dingdan_id, jieshao: order.jieshao, jine: order.jine, - beizhu: order.beizhu + beizhu: order.beizhu, + zhuangtai: order.zhuangtai, + nicheng: order.nicheng, }; sendGroupMessage({ msgData: { @@ -599,6 +773,10 @@ Page({ }, onSuccess: (messageId, status) => { that.updateMsg(messageId, status); + that.closeOrderSender(); + if (status === 'success') { + wx.showToast({ title: '订单已发送', icon: 'success' }); + } } }); }, @@ -610,5 +788,24 @@ Page({ this.setData({ detailText: text, showDetailModal: true }); }, - onKeyboardHeightChange(e) { this.setData({ keyboardHeight: e.detail.height }); } + onKeyboardHeightChange(e) { this.setData({ keyboardHeight: e.detail.height }); }, + + onAvatarError(e) { + const def = getDefaultAvatarUrl(app); + const idx = e.currentTarget.dataset.index; + if (idx != null && idx !== '') { + this.setData({ [`messages[${idx}].senderData.avatar`]: def }); + } + }, + + onSelfAvatarError() { + const def = getDefaultAvatarUrl(app); + if (this.data.currentUser) { + this.setData({ 'currentUser.avatar': def }); + } + }, + + tapOrderBar() { + this.openOrderSender(); + }, }); \ No newline at end of file diff --git a/pages/group-chat/group-chat.json b/pages/group-chat/group-chat.json index d5bfa0e..3e7741a 100644 --- a/pages/group-chat/group-chat.json +++ b/pages/group-chat/group-chat.json @@ -1,6 +1,7 @@ { "navigationBarTitleText": "群聊", "usingComponents": { - "order-card": "/components/order-card/order-card" + "order-card": "/components/order-card/order-card", + "order-sender": "/components/order-sender/order-sender" } } \ No newline at end of file diff --git a/pages/group-chat/group-chat.wxml b/pages/group-chat/group-chat.wxml index 2abee15..4e6555a 100644 --- a/pages/group-chat/group-chat.wxml +++ b/pages/group-chat/group-chat.wxml @@ -1,18 +1,28 @@ + + + + 订单 {{orderId}} + ¥{{orderJine}} + + {{orderZhuangtaiText}} + + {{orderJieshao}} + 点击查看历史订单并发送 + + style="top: {{orderId ? '120rpx' : '20rpx'}}; bottom: calc({{bottomSafeHeight}}rpx + {{keyboardHeight}}px + env(safe-area-inset-bottom) + 24rpx);"> 加载更早消息... - + {{item.formattedTime}} - - + {{item.senderData.name || item.senderId}} ID: {{item.payload.orderId}} 内容: {{item.payload.jieshao}} 金额: ¥{{item.payload.jine}} + 备注: {{item.payload.beizhu}} 点击查看详情 - - + - + @@ -88,5 +98,5 @@ - + diff --git a/pages/group-chat/group-chat.wxss b/pages/group-chat/group-chat.wxss index dab11fa..81654bb 100644 --- a/pages/group-chat/group-chat.wxss +++ b/pages/group-chat/group-chat.wxss @@ -1,5 +1,67 @@ /* 使用全局 chat-* 样式 */ +.order-status-bar { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 11; + background: #fff; + padding: 16rpx 24rpx; + border-bottom: 1rpx solid #e8e8e8; + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.04); +} + +.order-status-main { + display: flex; + align-items: center; + justify-content: space-between; +} + +.order-status-id { + font-size: 26rpx; + color: #333; + font-weight: 600; +} + +.order-status-tag { + font-size: 22rpx; + color: #07c160; + background: #e8f8ee; + padding: 4rpx 16rpx; + border-radius: 20rpx; +} + +.order-status-left { + display: flex; + flex-direction: column; + flex: 1; + min-width: 0; +} + +.order-status-price { + font-size: 24rpx; + color: #ff6b00; + margin-top: 4rpx; +} + +.order-status-hint { + font-size: 22rpx; + color: #007aff; + margin-top: 6rpx; + display: block; +} + +.order-status-desc { + font-size: 24rpx; + color: #888; + margin-top: 8rpx; + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + /* 发送者昵称样式(群聊特有) */ .sender-info { display: flex; diff --git a/pages/merchant-home/merchant-home.js b/pages/merchant-home/merchant-home.js index b6e59a7..e5d2ffb 100644 --- a/pages/merchant-home/merchant-home.js +++ b/pages/merchant-home/merchant-home.js @@ -114,6 +114,7 @@ Page(createPage({ if (this.data.isShangjia) { ensureRoleOnCenterPage(this, 'shangjia'); } + if (app.startImWhenReady) app.startImWhenReady(); if (this._skipShowRefresh) { this._skipShowRefresh = false; diff --git a/pages/merchant-orders/merchant-orders.js b/pages/merchant-orders/merchant-orders.js index 65e3e32..a1a899a 100644 --- a/pages/merchant-orders/merchant-orders.js +++ b/pages/merchant-orders/merchant-orders.js @@ -5,8 +5,6 @@ import { reconnectForRole } from '../../utils/role-tab-bar.js'; import { getOrderStatusText } from '../../utils/api-helper.js'; import { STAFF_API, isStaffMode, refreshStaffContext, syncStaffUi } from '../../utils/staff-api.js'; import { ensurePhoneAuth } from '../../utils/phone-auth.js'; -import { normalizeOrderTags } from '../../utils/order-tags.js'; -import { ICON_KEYS, resolveMiniappIcon, MERCHANT_GOLD_BANNER_FALLBACK } from '../../utils/miniapp-icons.js'; import { fetchMerchantOrderStats } from '../../utils/merchant-order-stats.js'; const EMPTY_TAB = { list: [], page: 1, hasMore: true, isLoading: false }; @@ -14,6 +12,8 @@ const EMPTY_TAB = { list: [], page: 1, hasMore: true, isLoading: false }; function buildEmptyStatusData() { return { all: { ...EMPTY_TAB }, + daifuwu: { ...EMPTY_TAB }, + zhidingzhong: { ...EMPTY_TAB }, jinxingzhong: { ...EMPTY_TAB }, tuikuanzhong: { ...EMPTY_TAB }, yituikuan: { ...EMPTY_TAB }, @@ -23,6 +23,17 @@ function buildEmptyStatusData() { }; } +const STATUS_TAG_STYLE = { + 1: { color: '#E65100', bg: '#FFF3E0' }, + 2: { color: '#1565C0', bg: '#E3F2FD' }, + 3: { color: '#2E7D32', bg: '#E8F5E9' }, + 4: { color: '#EF6C00', bg: '#FFF8E1' }, + 5: { color: '#616161', bg: '#F5F5F5' }, + 6: { color: '#C62828', bg: '#FFEBEE' }, + 7: { color: '#BF360C', bg: '#FFE0B2' }, + 8: { color: '#D84315', bg: '#FBE9E7' }, +}; + Page(createPage({ data: { shangpinleixing: [], @@ -35,6 +46,8 @@ Page(createPage({ statusList: [ { name: '全部', key: 'all', zhuangtaiList: [1, 2, 3, 4, 5, 6, 7, 8], color: '#333333' }, + { name: '待服务', key: 'daifuwu', zhuangtaiList: [1], color: '#E65100' }, + { name: '指定中', key: 'zhidingzhong', zhuangtaiList: [7], color: '#BF360C' }, { name: '进行中', key: 'jinxingzhong', zhuangtaiList: [2], color: '#2196F3' }, { name: '退款中', key: 'tuikuanzhong', zhuangtaiList: [4], color: '#FF9800' }, { name: '已退款', key: 'yituikuan', zhuangtaiList: [5], color: '#9E9E9E' }, @@ -56,7 +69,6 @@ Page(createPage({ ossImageUrl: app.globalData.ossImageUrl || '', pendingCount: 0, - goldBannerUrl: '', staffList: [], selectedStaffMemberId: '', selectedStaffLabel: '全部客服', @@ -77,9 +89,6 @@ Page(createPage({ this._listInitialized = false; this._savedScrollTop = 0; this._skipShowRefresh = false; - this.setData({ - goldBannerUrl: resolveMiniappIcon(app, ICON_KEYS.MERCHANT_GOLD_BANNER, MERCHANT_GOLD_BANNER_FALLBACK), - }); wx.setNavigationBarTitle({ title: isStaffMode() ? '客服派单' : '我的派单' }); syncStaffUi(this); if (isStaffMode()) { @@ -476,33 +485,15 @@ Page(createPage({ }, processOrderItem(item, zhuangtaiColor) { - const oss = this.data.ossImageUrl || ''; - const leixing = this.data.shangpinleixing.find((l) => l.id === item.leixing_id); - const leixingIconUrl = leixing ? leixing.full_tupian_url : '/images/default-type.png'; - - let sjAvatarFull = '/images/default-avatar.png'; - if (item.sj_avatar) { - sjAvatarFull = item.sj_avatar.startsWith('http') ? item.sj_avatar : oss + item.sj_avatar; - } - - let zhidingAvatarFull = ''; - if (item.zhiding_avatar) { - zhidingAvatarFull = item.zhiding_avatar.startsWith('http') ? item.zhiding_avatar : oss + item.zhiding_avatar; - } - - const tags = normalizeOrderTags(item); + const st = Number(item.zhuangtai); + const tagStyle = STATUS_TAG_STYLE[st] || { color: zhuangtaiColor || '#333', bg: '#F5F5F5' }; return { ...item, - zhuangtaiZh: getOrderStatusText(item.zhuangtai), - zhuangtaiColor: zhuangtaiColor || '#333333', - leixing_icon_url: leixingIconUrl, - sj_avatar_full: sjAvatarFull, - zhiding_avatar_full: zhidingAvatarFull, - isZhiding: item.zhuangtai === 7, + zhuangtaiZh: getOrderStatusText(st), + zhuangtaiColor: tagStyle.color, + zhuangtaiBg: tagStyle.bg, creat_time: item.creat_time || item.create_time || '', - ...tags, - shangjia_youzhi: !!item.shangjia_youzhi, }; }, diff --git a/pages/merchant-orders/merchant-orders.wxml b/pages/merchant-orders/merchant-orders.wxml index f438e51..254b813 100644 --- a/pages/merchant-orders/merchant-orders.wxml +++ b/pages/merchant-orders/merchant-orders.wxml @@ -65,8 +65,9 @@ - - + + + {{item.name}} @@ -75,7 +76,8 @@ {{pendingCount}} - + + @@ -113,123 +115,20 @@ 快去派发订单吧 - + - - - - - - + + + 单号 {{item.dingdan_id}} + {{item.zhuangtaiZh}} - - - - - 指定 - - {{item.zhiding_nicheng || '指定接单员'}} - - - - - - - - - - {{item.jieshao || '暂无介绍'}} - - 优质商家 - - - - - - {{item.jine || '0.00'}} - - - 商家备注:{{item.beizhu || '暂无'}} - - - - - - - - - - - - - - - - {{item.sjnicheng || '未知商家'}} - ID:{{item.shangjia_id}} 发布于 {{item.creat_time}} - - - - 订单金额 ¥{{item.jine || '0.00'}} - {{item.zhuangtaiZh}} - - - - - - - - - - - - 指定 - - {{item.zhiding_nicheng || '指定接单员'}} - - - - - - - - - - {{item.jieshao || '暂无介绍'}} - - - - - {{item.jine || '0.00'}} - - - 商家备注:{{item.beizhu || '暂无'}} - - - - - - - - - - - - - - - - {{item.sjnicheng || '未知商家'}} - ID:{{item.shangjia_id}} 发布于 {{item.creat_time}} - - - - 订单金额 ¥{{item.jine || '0.00'}} - {{item.zhuangtaiZh}} - - + {{item.jieshao || '暂无介绍'}} + + ¥{{item.jine || '0.00'}} + {{item.creat_time}} + 备注:{{item.beizhu}} diff --git a/pages/merchant-orders/merchant-orders.wxss b/pages/merchant-orders/merchant-orders.wxss index 2f7e06c..dc68610 100644 --- a/pages/merchant-orders/merchant-orders.wxss +++ b/pages/merchant-orders/merchant-orders.wxss @@ -13,6 +13,8 @@ page { display: flex; flex-direction: column; background: #f5f3ff; + overflow: hidden; + box-sizing: border-box; } /* ========== 商品类型区 ========== */ @@ -267,29 +269,38 @@ page { flex: 1; display: flex; overflow: hidden; + min-height: 0; } /* 左侧状态栏 */ .left-status { width: 180rpx; + height: 100%; background: #fff; border-right: 1rpx solid #f0f0f0; - padding: 28rpx 0; flex-shrink: 0; + box-sizing: border-box; + } + .left-status-inner { + padding: 12rpx 0 48rpx; } .status-item { - padding: 28rpx 20rpx; + padding: 22rpx 16rpx; display: flex; align-items: center; justify-content: space-between; position: relative; + flex-wrap: wrap; + gap: 6rpx; } .status-item.status-active { background: linear-gradient(to right, #e3f2fd, #fff); } .status-name { - font-size: 28rpx; + font-size: 26rpx; color: #555; + flex: 1; + min-width: 0; } .status-active .status-name { color: #1976D2; @@ -564,4 +575,72 @@ page { color: #6d28d9; border: 2rpx solid #fff; box-shadow: 0 6rpx 16rpx rgba(245, 213, 99, 0.3); + } + + /* 简洁订单卡片 */ + .sj-order-card { + background: #fff; + border-radius: 16rpx; + padding: 24rpx; + margin-bottom: 16rpx; + box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06); + border: 1rpx solid #eee; + } + .sj-order-card:active { + opacity: 0.92; + } + .sj-order-card-head { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12rpx; + gap: 16rpx; + } + .sj-order-id { + font-size: 24rpx; + color: #888; + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .sj-status-tag { + flex-shrink: 0; + font-size: 24rpx; + font-weight: 700; + padding: 8rpx 20rpx; + border-radius: 999rpx; + } + .sj-order-desc { + font-size: 30rpx; + color: #222; + line-height: 1.5; + font-weight: 500; + margin-bottom: 12rpx; + } + .sj-order-meta { + display: flex; + align-items: center; + justify-content: space-between; + } + .sj-order-amount { + font-size: 32rpx; + font-weight: 700; + color: #e53935; + } + .sj-order-time { + font-size: 22rpx; + color: #aaa; + } + .sj-order-remark { + margin-top: 10rpx; + font-size: 24rpx; + color: #666; + line-height: 1.4; + } + .line2 { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; } \ No newline at end of file diff --git a/pages/merchant/merchant.js b/pages/merchant/merchant.js index 76fb870..54ff719 100644 --- a/pages/merchant/merchant.js +++ b/pages/merchant/merchant.js @@ -36,6 +36,7 @@ Page(createPage({ iconCopy: '', avatarFrame: '', iconClear: '', + iconKefuList: '', }, // 商家状态 @@ -139,18 +140,22 @@ Page(createPage({ async onShow() { this.registerNotificationComponent(); + if (!this._ossImagesReady && app.globalData.ossImageUrl) { + this.setupImageUrls(); + this._ossImagesReady = true; + } this.syncRoleStatusFromStorage(); syncStaffUi(this); - if (app.globalData.ossImageUrl) { - this.setupImageUrls(); - } - // 每次进入刷新经营/处罚统计(避免罚单数量卡住不更新) - if (this.data.isShangjia || isMerchantPortalUser()) { - if (!isStaffMode()) { - this.loadDashboardStats(); + + if (this._showRefreshTimer) clearTimeout(this._showRefreshTimer); + this._showRefreshTimer = setTimeout(() => { + if (this.data.isShangjia || isMerchantPortalUser()) { + if (!isStaffMode()) { + this.loadDashboardStats(); + } } - } - this.loadMerchantDataIfNeeded(); + this.loadMerchantDataIfNeeded(); + }, 200); }, syncRoleStatusFromStorage() { @@ -159,8 +164,8 @@ Page(createPage({ const portal = isMerchantPortalUser(); const staffFlag = Number(wx.getStorageSync('staffstatus')) === 1; if (portal || staffFlag) { - this.setData({ isShangjia: portal || staffFlag, uid: uid || '', staffBooting: false, isLoading: false }); - } else { + this.setData({ isShangjia: true, uid: uid || '', staffBooting: false, isLoading: false }); + } else if (!this.data.isShangjia) { this.setData({ isShangjia: false, staffBooting: false, isLoading: false }); } } catch (error) { diff --git a/pages/merchant/merchant.wxml b/pages/merchant/merchant.wxml index 0e3586d..0d35507 100644 --- a/pages/merchant/merchant.wxml +++ b/pages/merchant/merchant.wxml @@ -56,10 +56,11 @@ {{nicheng || '商家'}} - {{isStaffMode ? (staffRoleName || '商家客服') : '商家'}} + {{staffRoleName || '商家客服'}} + 商家 修改昵称 - + @@ -75,7 +76,7 @@ - + diff --git a/pages/messages/messages.js b/pages/messages/messages.js index b9db0ba..9cfcf94 100644 --- a/pages/messages/messages.js +++ b/pages/messages/messages.js @@ -3,8 +3,9 @@ const app = getApp(); import { formatDate } from '../../static/lib/utils'; import { resolveAvatarUrl, getDefaultAvatarUrl, resolveAvatarFromStorage } from '../../utils/avatar.js'; import { getLocalImUserId, getLocalImIdentity, getImUserIdForRole, loadGroupMetaCache, getDefaultAvatar } from '../../utils/im-user.js'; -import { enrichGroupConversation, isOrderGroupConversation, sortConversations, recordConversationOpen } from '../../utils/group-chat.js'; -import { reconnectForRole } from '../../utils/role-tab-bar.js'; +import { enrichGroupConversation, isOrderGroupConversation, sortConversations, recordConversationOpen, getZhuangtaiText } from '../../utils/group-chat.js'; +import request from '../../utils/request'; +import { openCustomerServiceChat } from '../../utils/kefu-nav.js'; import { getPrimaryRole } from '../../utils/primary-role.js'; import { isStaffMode } from '../../utils/staff-api.js'; import { createPage } from '../../utils/base-page.js'; @@ -21,14 +22,23 @@ Page(createPage({ actionPopup: { visible: false, conversation: null }, currentUser: null, notificationMuted: false, - scrollHeight: 0 + scrollHeight: 0, + imStatus: 'connecting', + imStatusText: '', + defaultAvatarUrl: '', }, _permissionSeq: 0, _peerProfileCache: {}, + _lastConvSignature: '', + _lastRefreshTs: 0, + _showRefreshTimer: null, + _statusRefreshTimer: null, + _chatAllowed: true, onLoad() { this.calculateScrollHeight(); + this.setData({ defaultAvatarUrl: getDefaultAvatarUrl(app) }); const muted = wx.getStorageSync('notificationMuted') || false; this.setData({ notificationMuted: muted }); if (app.globalData.messageManager) { @@ -42,22 +52,61 @@ Page(createPage({ app.loadConversations(); } }; + this._onUnreadChanged = (data) => { + const unreadTotal = data?.unreadTotal ?? app.globalData.messageManager?.unreadTotal ?? 0; + if (app.globalData.messageManager) { + app.globalData.messageManager.unreadTotal = unreadTotal; + } + if (app.emitEvent) { + app.emitEvent('tabBarBadgeChanged', { + badgeText: unreadTotal > 0 ? String(unreadTotal) : '', + }); + } + }; + this._onConnectionChanged = (data) => { + const status = data?.status || 'disconnected'; + const textMap = { + connected: '', + connecting: '正在连接消息服务…', + reconnecting: '正在重新连接…', + disconnected: '消息未连接,点击重试', + disabled: '消息服务未配置', + }; + this.setData({ + imStatus: status, + imStatusText: textMap[status] || (status === 'connected' ? '' : '消息未连接,点击重试'), + }); + if (status === 'connected') { + this.setupConversationListener(); + this.loadConversations(); + } + }; app.on('conversationsUpdated', this._onGlobalConvUpdated); + app.on('unreadCountChanged', this._onUnreadChanged); + app.on('connectionChanged', this._onConnectionChanged); }, onUnload() { if (this._onGlobalConvUpdated) { app.off('conversationsUpdated', this._onGlobalConvUpdated); } + if (this._onUnreadChanged) { + app.off('unreadCountChanged', this._onUnreadChanged); + } + if (this._onConnectionChanged) { + app.off('connectionChanged', this._onConnectionChanged); + } if (this.conversationsUpdatedListener && wx.goEasy?.im) { wx.goEasy.im.off(wx.GoEasy.IM_EVENT.CONVERSATIONS_UPDATED, this.conversationsUpdatedListener); this.conversationsUpdatedListener = null; } }, - onShow() { + async onShow() { if (!this.checkLoginStatus()) return; + this.setData({ defaultAvatarUrl: getDefaultAvatarUrl(app) }); loadGroupMetaCache(app); + if (app.restoreTabBarBadge) app.restoreTabBarBadge(); this.registerNotificationComponent(); if (app.globalData.messageManager?.unreadTotal != null && app.emitEvent) { app.emitEvent('tabBarBadgeChanged', { @@ -65,13 +114,61 @@ Page(createPage({ ? String(app.globalData.messageManager.unreadTotal) : '', }); } - this.checkPermissionAndAutoConnect(); - if (app.ensureConnection) app.ensureConnection(); - this.setupConversationListener(); - setTimeout(() => { + this.patchLocalReadState(); + await this.checkPermissionAndAutoConnect(); + if (this._chatAllowed) { + if (app.ensureConnection) app.ensureConnection(); + this.setupConversationListener(); + if (app.syncConnectionStatus) app.syncConnectionStatus(); + this.scheduleSoftRefresh(); + } + }, + + scheduleSoftRefresh() { + if (this._showRefreshTimer) clearTimeout(this._showRefreshTimer); + if (this._statusRefreshTimer) clearTimeout(this._statusRefreshTimer); + this._showRefreshTimer = setTimeout(() => { + this._showRefreshTimer = null; + const now = Date.now(); + if (now - this._lastRefreshTs < 1800 && this.data.allConversations.length) { + return; + } + this._lastRefreshTs = now; if (app.loadConversations) app.loadConversations(); else if (wx.goEasy?.im) this.loadConversations(); - }, 200); + }, 350); + }, + + patchLocalReadState() { + const openId = app.globalData.pageState?.lastOpenedConvId; + if (!openId || !this.data.allConversations.length) return; + + const list = this.data.allConversations.map((c) => { + const id = c.groupId || c.userId; + if (id === openId && (c.unread || 0) > 0) { + return { ...c, unread: 0 }; + } + return c; + }); + + const openTimes = wx.getStorageSync('conversationOpenTimes') || {}; + sortConversations(list, openTimes); + this.setData({ allConversations: list }, () => this.applyFilter()); + }, + + retryImConnection() { + if (!app.globalData.chatEnabled) { + wx.showToast({ title: '消息服务未就绪', icon: 'none' }); + return; + } + this.setData({ imStatus: 'connecting', imStatusText: '正在连接消息服务…' }); + app.globalData.goEasyConnection.autoReconnect = true; + app.globalData.goEasyConnection.reconnectAttempts = 0; + if (app.ensureConnection) app.ensureConnection(); + setTimeout(() => { + if (app.syncConnectionStatus) app.syncConnectionStatus(); + if (app.loadConversations) app.loadConversations(); + }, 2000); }, onHide() {}, @@ -81,24 +178,30 @@ Page(createPage({ const seq = ++this._permissionSeq; let quanxian; try { - quanxian = await jianquanxian(app); + quanxian = await jianquanxian(app, { silent: true }); } catch (e) { + this._chatAllowed = true; return; } if (seq !== this._permissionSeq) return; if (!quanxian.allowed) { + this._chatAllowed = false; const currentRole = app.globalData.currentRole || 'normal'; - let content = '您没有权限使用消息功能'; - if (currentRole === 'dashou') content = '您需要先充值会员才能使用消息功能'; - wx.showModal({ - title: '权限不足', - content, - showCancel: false, - confirmText: '我知道了' - }); + let hint = '消息连接暂不可用,列表可正常浏览'; + if (currentRole === 'dashou') { + hint = '开通会员后可收发消息(列表可正常浏览)'; + } + const prev = this.data.imStatusText; + if (prev !== hint) { + this.setData({ imStatusText: hint }); + } return; } + this._chatAllowed = true; + if (this.data.imStatusText && this.data.imStatusText.includes('会员')) { + this.setData({ imStatusText: '' }); + } this.autoConnect(); }, @@ -221,56 +324,157 @@ Page(createPage({ // 会话渲染与排序 renderConversations(content) { - const conversations = (content && content.conversations) ? content.conversations : []; - if (!conversations.length) { + if (!content || !content.conversations) { this.setData({ allConversations: [] }, () => this.applyFilter()); return; } - const myGoEasyId = wx.goEasy?.im?.userId || getLocalImUserId(app); + let list = content.conversations; const defaultAvatar = getDefaultAvatar(app); + const myGoEasyId = wx.goEasy?.im?.userId || getLocalImUserId(app); const ossBase = app.globalData.ossImageUrl || ''; + const groupMeta = app.globalData.groupInfoMap || {}; - let list = conversations.map((item) => { + list.forEach((item, idx) => { + item.key = item.groupId || item.userId || item.teamId || `${item.type || 'c'}_${idx}`; if (item.type === 'group') { - return enrichGroupConversation(item, myGoEasyId, defaultAvatar, ossBase); + if (!item.data) item.data = {}; + if (!item.data.orderId && item.groupId) { + const m = /^group_(.+)$/.exec(String(item.groupId)); + if (m) item.data.orderId = m[1]; + } + const meta = groupMeta[item.groupId]; + if (meta) { + if (!item.data) item.data = {}; + Object.assign(item.data, meta); + } + if (item.data.orderJieshao && !item.data.orderDesc) { + item.data.orderDesc = item.data.orderJieshao; + } + if (item.data && item.data.dashouGoEasyId) { + if (!app.globalData.groupInfoMap) app.globalData.groupInfoMap = {}; + app.globalData.groupInfoMap[item.groupId] = { + ...app.globalData.groupInfoMap[item.groupId], + ...item.data, + }; + } + enrichGroupConversation(item, myGoEasyId, defaultAvatar, ossBase); + } else { + if (!item.data) item.data = {}; + item.data.avatar = resolveAvatarUrl(item.data.avatar, app) || defaultAvatar; } - if (!item.data) item.data = {}; - if (item.data.avatar) { - item.data.avatar = resolveAvatarUrl(item.data.avatar, app); - } - return item; - }); - - list.forEach(item => { if (!item.lastMessage) { item.lastMessage = { type: 'text', payload: { text: '' }, timestamp: 0 }; } if (item.lastMessage.timestamp) { item.lastMessage.date = formatDate(item.lastMessage.timestamp); } - if (item.type === 'group' && item.displayLastMsg) { - item.lastMessage.preview = item.displayLastMsg; - } }); const openTimes = wx.getStorageSync('conversationOpenTimes') || {}; - list = sortConversations(list, openTimes); - - this.setData({ allConversations: list, displayCount: 10 }, () => { - this.applyFilter(); - }); + sortConversations(list, openTimes); const unreadTotal = content.unreadTotal !== undefined ? content.unreadTotal : list.reduce((sum, c) => sum + (c.unread || 0), 0); + + const signature = list.map((c) => { + const id = c.groupId || c.userId || c.teamId || ''; + const unread = c.unread || 0; + const ts = c.lastMessage?.timestamp || 0; + const preview = c.displayLastMsg || c.lastMessage?.payload?.text || ''; + return `${id}:${unread}:${ts}:${preview.slice(0, 20)}`; + }).join('|'); + + if (signature === this._lastConvSignature && this.data.allConversations.length) { + if (app.globalData.messageManager) { + app.globalData.messageManager.unreadTotal = unreadTotal; + } + if (app.updateUnreadCount) { + app.updateUnreadCount(unreadTotal); + } else if (app.emitEvent) { + app.emitEvent('tabBarBadgeChanged', { badgeText: unreadTotal > 0 ? String(unreadTotal) : '' }); + } + this.applyFilter(); + return; + } + this._lastConvSignature = signature; + + if (app.globalData.messageManager) { + app.globalData.messageManager.unreadTotal = unreadTotal; + } if (app.updateUnreadCount) { app.updateUnreadCount(unreadTotal); } else if (app.emitEvent) { app.emitEvent('tabBarBadgeChanged', { badgeText: unreadTotal > 0 ? String(unreadTotal) : '' }); } - // 重新订阅群组 + const prevDisplay = this.data.displayCount; + this.setData({ + allConversations: list, + displayCount: prevDisplay > 10 ? prevDisplay : 10, + }, () => { + this.applyFilter(); + }); + this.subscribeGroupsIfNeeded(list); + this.scheduleOrderStatusRefresh(list); + }, + + scheduleOrderStatusRefresh(conversations) { + if (this._statusRefreshTimer) clearTimeout(this._statusRefreshTimer); + this._statusRefreshTimer = setTimeout(() => { + this._statusRefreshTimer = null; + this.refreshOrderStatuses(conversations); + }, 400); + }, + + async refreshOrderStatuses(conversations) { + const orderIds = []; + (conversations || this.data.allConversations).forEach((c) => { + if (!isOrderGroupConversation(c)) return; + const oid = c.data?.orderId; + if (oid && !orderIds.includes(oid)) orderIds.push(oid); + }); + if (!orderIds.length) return; + + try { + const res = await request({ + url: '/dingdan/ltddztpl', + method: 'POST', + data: { order_ids: orderIds.slice(0, 20) }, + }); + const body = res?.data || {}; + const map = body.data || {}; + if (!Object.keys(map).length) return; + + let changed = false; + const list = this.data.allConversations.map((c) => { + const oid = c.data?.orderId; + if (!oid || !map[oid]) return c; + const st = map[oid]; + changed = true; + const data = { + ...c.data, + orderId: oid, + orderDesc: st.jieshao || c.data.orderDesc, + orderJine: st.jine != null ? st.jine : c.data.orderJine, + orderZhuangtai: st.zhuangtai, + orderZhuangtaiText: getZhuangtaiText(st.zhuangtai), + }; + if (c.groupId && app.globalData.groupInfoMap) { + app.globalData.groupInfoMap[c.groupId] = { + ...(app.globalData.groupInfoMap[c.groupId] || {}), + ...data, + }; + } + return { ...c, data }; + }); + if (changed) { + this.setData({ allConversations: list }, () => this.applyFilter()); + } + } catch (e) { + console.warn('刷新订单状态失败', e); + } }, /** @@ -352,7 +556,16 @@ Page(createPage({ chat(e) { const conversation = e.currentTarget.dataset.conversation; if (!conversation) return; + if (this._chatAllowed === false) { + wx.showToast({ title: '请先开通会员后再聊天', icon: 'none' }); + return; + } recordConversationOpen(conversation); + const convId = conversation.groupId || conversation.userId; + if (convId) { + if (!app.globalData.pageState) app.globalData.pageState = {}; + app.globalData.pageState.lastOpenedConvId = convId; + } if (conversation.type === 'private') { const param = { @@ -361,17 +574,23 @@ Page(createPage({ toAvatar: resolveAvatarUrl(conversation.data.avatar, app), }; wx.navigateTo({ url: '/pages/chat/chat?data=' + encodeURIComponent(JSON.stringify(param)) }); - } else if (conversation.type === 'group') { + } else if (conversation.type === 'group' || (conversation.groupId && isOrderGroupConversation(conversation))) { const d = conversation.data || {}; + const rawGid = conversation.groupId || ''; + const oid = String(d.orderId || rawGid).replace(/^group_/, ''); + const groupId = rawGid.startsWith('group_') ? rawGid : (oid ? `group_${oid}` : rawGid); const param = { - groupId: conversation.groupId, - orderId: d.orderId || '', + groupId, + orderId: oid, groupName: d.name || '订单群聊', groupAvatar: d.avatar, isCross: d.isCross || 0, orderZhuangtai: d.orderZhuangtai, orderJine: d.orderJine, orderDesc: d.orderDesc || d.orderJieshao || '', + currentUserId: wx.goEasy?.im?.userId || getLocalImUserId(app), + currentUserName: this.data.currentUser?.name || '', + currentUserAvatar: this.data.currentUser?.avatar || '', }; wx.navigateTo({ url: '/pages/group-chat/group-chat?data=' + encodeURIComponent(JSON.stringify(param)) }); } else if (conversation.type === 'cs') { @@ -381,8 +600,7 @@ Page(createPage({ }, chatKefu() { - const param = { teamId: 'support_team' }; - wx.navigateTo({ url: '/pages/cs-chat/cs-chat?data=' + encodeURIComponent(JSON.stringify(param)) }); + openCustomerServiceChat(); }, showAction(e) { diff --git a/pages/messages/messages.wxml b/pages/messages/messages.wxml index 1b0373b..1aecc48 100644 --- a/pages/messages/messages.wxml +++ b/pages/messages/messages.wxml @@ -10,41 +10,62 @@ + + + {{imStatusText}},点击重连 + + 订单消息 - {{tabUnread[0]}} + {{tabUnread[0]}} 用户消息 - {{tabUnread[1]}} + {{tabUnread[1]}} 客服消息 - {{tabUnread[2]}} + {{tabUnread[2]}} - - + + - {{item.data.name}} + + {{item.data.name || '用户'}} + ID {{item.data.counterpartYonghuid}} + {{item.lastMessage.date}} - + + + {{item.displayLastMsg || item.lastMessage.payload.text}} + {{item.unread > 99 ? '99+' : item.unread}} + + + + 单号 + {{item.data.orderId}} + {{item.data.orderZhuangtaiText}} + + {{item.data.orderDesc}} + + + {{item.lastMessage.type === 'text' ? item.lastMessage.payload.text : (item.lastMessage.type === 'image' ? '[图片]' : '[其他消息]')}} {{item.unread > 99 ? '99+' : item.unread}} - - + @@ -54,7 +75,6 @@ - {{actionPopup.conversation.top ? '取消置顶' : '置顶聊天'}} @@ -63,7 +83,6 @@ - - \ No newline at end of file + diff --git a/pages/messages/messages.wxss b/pages/messages/messages.wxss index 99eb09f..2ab93ed 100644 --- a/pages/messages/messages.wxss +++ b/pages/messages/messages.wxss @@ -11,6 +11,31 @@ align-items: center; padding: 20rpx 24rpx; background: #fff; + border-bottom: 1rpx solid #e8e8e8; +} + +.im-status-bar { + display: flex; + align-items: center; + padding: 16rpx 24rpx; + background: #fff1f0; + border-bottom: 1rpx solid #ffccc7; +} + +.im-status-bar--warn { + background: linear-gradient(90deg, #fff7e6 0%, #fff1f0 100%); +} + +.im-status-icon { + font-size: 28rpx; + margin-right: 12rpx; + color: #fa541c; +} + +.im-status-text { + font-size: 26rpx; + color: #cf1322; + font-weight: 500; } .search-bar { @@ -105,12 +130,61 @@ .conversation-item { display: flex; - align-items: center; + align-items: flex-start; padding: 24rpx; border-bottom: 1rpx solid #f0f0f0; transition: background 0.15s; } +.order-group-item { + padding: 22rpx 24rpx; +} + +.order-compact { + margin-top: 8rpx; + padding-top: 8rpx; + border-top: 1rpx solid #f3f3f3; +} + +.order-compact-row { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8rpx; + margin-bottom: 4rpx; +} + +.order-id-tag { + font-size: 20rpx; + color: #999; + background: #f0f0f0; + padding: 2rpx 10rpx; + border-radius: 6rpx; +} + +.order-id-text { + font-size: 22rpx; + color: #576b95; +} + +.order-status-inline { + font-size: 20rpx; + color: #07c160; + background: #e8f8ef; + padding: 2rpx 10rpx; + border-radius: 6rpx; +} + +.order-desc-compact { + display: block; + font-size: 24rpx; + color: #888; + line-height: 1.4; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .conversation-item:active { background: #f5f5f5; } @@ -128,6 +202,56 @@ flex-shrink: 0; } +.avatar-round { + border-radius: 50%; +} + +.user-meta-row { + margin-bottom: 4rpx; +} + +.user-meta { + font-size: 24rpx; + color: #333; +} + +.user-meta.sub { + color: #666; + font-size: 22rpx; +} + +.order-block { + background: #f7f8fa; + border-radius: 12rpx; + padding: 16rpx 18rpx; + margin-bottom: 10rpx; +} + +.order-id { + display: block; + font-size: 24rpx; + color: #576b95; + margin-bottom: 6rpx; +} + +.order-desc { + display: block; + font-size: 26rpx; + color: #333; + line-height: 1.5; + word-break: break-all; +} + +.order-status { + display: inline-block; + font-size: 22rpx; + color: #07c160; + margin-top: 8rpx; + padding: 2rpx 12rpx; + background: #e8f8ef; + border-radius: 8rpx; +} + .info { flex: 1; min-width: 0; @@ -140,11 +264,23 @@ margin-bottom: 8rpx; } +.name-col { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; +} + +.uid-tag { + font-size: 22rpx; + color: #999; + margin-top: 2rpx; +} + .name { font-size: 30rpx; color: #1a1a1a; font-weight: 500; - flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; diff --git a/tab-bar/index.js b/tab-bar/index.js index dca25af..f16bc59 100644 --- a/tab-bar/index.js +++ b/tab-bar/index.js @@ -50,6 +50,7 @@ Component({ lifetimes: { attached() { this.refresh(); + this._restoreBadgeFromCache(); this._badgeListener = (data) => { if (data.badgeText !== undefined) { this.setData({ badgeText: data.badgeText }); @@ -68,20 +69,48 @@ Component({ pageLifetimes: { show() { this.refresh(); - const globalBadge = app.globalData.messageManager?.tabBarBadgeText || ''; - if (this.data.badgeText !== globalBadge) { - this.setData({ badgeText: globalBadge }); - } + this._restoreBadgeFromCache(); this._syncUnreadWithDebounce(); }, }, methods: { + _restoreBadgeFromCache() { + const mm = app.globalData.messageManager || {}; + let badgeText = mm.tabBarBadgeText || ''; + if (!badgeText) { + try { + badgeText = wx.getStorageSync('tabBarMessageBadge') || ''; + } catch (e) {} + } + if (!badgeText && mm.unreadTotal > 0) { + badgeText = mm.unreadTotal > 99 ? '99+' : String(mm.unreadTotal); + } + if (!badgeText) { + try { + const saved = wx.getStorageSync('messageUnreadTotal'); + if (saved > 0) badgeText = saved > 99 ? '99+' : String(saved); + } catch (e) {} + } + if (badgeText && badgeText !== this.data.badgeText) { + this.setData({ badgeText }); + } + if (app.restoreTabBarBadge) app.restoreTabBarBadge(); + }, + _syncUnreadWithDebounce() { if (this._syncTimer) clearTimeout(this._syncTimer); this._syncTimer = setTimeout(() => { - if (app && app.loadConversations) app.loadConversations(); - }, 300); + if (app.globalData.goEasyConnection) { + app.globalData.goEasyConnection.autoReconnect = true; + } + if (app.startImWhenReady) { + app.startImWhenReady(); + } else { + if (app.ensureConnection) app.ensureConnection(); + if (app.loadConversations) app.loadConversations(); + } + }, 200); }, refresh() { diff --git a/utils/avatar.js b/utils/avatar.js index c5efd6b..0c0c725 100644 --- a/utils/avatar.js +++ b/utils/avatar.js @@ -1,4 +1,4 @@ -/** 统一头像 URL:无有效头像时始终返回 ossImageUrl + morentouxiang */ +/** 统一头像 URL:无有效头像时始终返回 ossImageUrl + morentouxiang(后端配置) */ function isBlankPath(p) { if (p == null) return true; @@ -7,28 +7,71 @@ function isBlankPath(p) { return !s || s === 'null' || s === 'undefined' || s === 'none'; } +function readConfigFallback(app) { + if (!app || typeof app.readConfigFromStorage !== 'function') return null; + try { + return app.readConfigFromStorage(); + } catch (e) { + return null; + } +} + +function getOssBase(app) { + let oss = app.globalData?.ossImageUrl || ''; + if (!oss) { + const cfg = readConfigFallback(app); + oss = cfg?.cos?.ossImageUrl || ''; + } + return oss; +} + +function getDefaultRelative(app) { + let def = app.globalData?.morentouxiang || ''; + if (!def) { + const cfg = readConfigFallback(app); + def = cfg?.otherConfig?.morentouxiang || ''; + } + return def || 'avatar/default.jpg'; +} + export function getDefaultAvatarUrl(app) { return resolveAvatarUrl('', app); } export function resolveAvatarUrl(rawPath, app) { app = app || getApp(); - const oss = app.globalData.ossImageUrl || ''; - const def = app.globalData.morentouxiang || 'avatar/default.jpg'; + const oss = getOssBase(app); + const def = getDefaultRelative(app); if (!isBlankPath(rawPath)) { if (rawPath.startsWith('http://') || rawPath.startsWith('https://')) { return rawPath; } - const path = rawPath.startsWith('/') ? rawPath : '/' + rawPath; - return oss + path; + const path = rawPath.startsWith('/') ? rawPath : `/${rawPath}`; + if (oss) return oss + path; + return path; } if (def.startsWith('http://') || def.startsWith('https://')) { return def; } - const defPath = def.startsWith('/') ? def : '/' + def; - return oss + defPath; + const defPath = def.startsWith('/') ? def : `/${def}`; + if (oss) return oss + defPath; + // 配置未就绪时仍返回相对路径,由 binderror 回退;有 oss 时必须拼完整 URL + return defPath; +} + +/** 是否已是可加载的完整头像 URL */ +export function isUsableAvatarUrl(url) { + return url && (url.startsWith('http://') || url.startsWith('https://')); +} + +export function ensureAvatarUrl(rawPath, app) { + app = app || getApp(); + const resolved = resolveAvatarUrl(rawPath, app); + if (isUsableAvatarUrl(resolved)) return resolved; + const def = getDefaultAvatarUrl(app); + return isUsableAvatarUrl(def) ? def : resolved; } export function resolveAvatarFromStorage(app) { @@ -36,3 +79,19 @@ export function resolveAvatarFromStorage(app) { const tx = wx.getStorageSync('touxiang') || ''; return resolveAvatarUrl(tx, app); } + +/** 配置加载后刷新页面头像(聊天页等订阅 configApplied) */ +export function subscribeConfigAvatarRefresh(page, refreshFn) { + const app = getApp(); + if (!app || typeof refreshFn !== 'function') return null; + const handler = () => refreshFn(); + if (app.on) app.on('configApplied', handler); + return () => { if (app.off) app.off('configApplied', handler); }; +} + +/** 消息列表/聊天页 image 加载失败时回退默认头像 */ +export function onAvatarImageError(page, dataKey, app) { + if (!page || !dataKey) return; + const def = getDefaultAvatarUrl(app || getApp()); + page.setData({ [dataKey]: def }); +} diff --git a/utils/base-page.js b/utils/base-page.js index 6531b92..bb13c82 100644 --- a/utils/base-page.js +++ b/utils/base-page.js @@ -272,6 +272,10 @@ const BASE_METHODS = { const result = {}; Object.keys(urlMap).forEach((key) => { const path = urlMap[key]; + if (!path || typeof path !== 'string') { + result[key] = ''; + return; + } result[key] = path.startsWith('http') || path.startsWith('/') ? path : ossImageUrl + path; }); return result; diff --git a/utils/chat-core.js b/utils/chat-core.js index fd0192d..a75548a 100644 --- a/utils/chat-core.js +++ b/utils/chat-core.js @@ -42,6 +42,8 @@ function initGlobalMessageSystem(app) { app.handleNewMessage = (message) => handleNewMessage(app, message); app.ensureImForRole = (role) => ensureImForRole(app, role); app.syncConnectionStatus = () => syncConnectionStatus(app); + app.restoreTabBarBadge = () => restoreCachedBadge(app); + app.startImWhenReady = () => startImWhenReady(app); loadUserSettings(app); initNetworkListener(app); @@ -132,6 +134,7 @@ function initAppStateListener(app) { wx.onAppShow(() => { updateCurrentPageState(app); app.globalData.goEasyConnection.autoReconnect = true; + restoreCachedBadge(app); ensureConnection(app); setTimeout(() => loadConversations(app), 400); }); @@ -205,6 +208,9 @@ function syncConnectionStatus(app) { app.globalData.goEasyConnection.status = 'connected'; app.globalData.goEasyConnection.userId = cur || expectedId; app.globalData.goEasyConnection.autoReconnect = true; + setupMessageListeners(app); + loadConversations(app); + restoreCachedBadge(app); app.emitEvent('connectionChanged', { status: 'connected' }); return; } @@ -607,6 +613,7 @@ function handleNewMessage(app, message) { } cacheMessage(app, message); + loadConversations(app); if (shouldShowNotification(app, message)) { let notificationType = 'private'; @@ -687,8 +694,57 @@ function showNotificationDirect(app, data) { app.emitEvent('showNotification', data); } +function startImWhenReady(app) { + if (!wx.getStorageSync('uid')) return; + app.globalData.goEasyConnection.autoReconnect = true; + restoreCachedBadge(app); + ensureConnection(app); + setTimeout(() => { + if (isSdkConnected()) { + setupMessageListeners(app); + loadConversations(app); + } + restoreCachedBadge(app); + }, 450); +} + +function restoreCachedBadge(app) { + const { messageManager } = app.globalData; + let unread = messageManager.unreadTotal; + if (unread == null || unread === '') { + try { + const saved = wx.getStorageSync(messageManager.cacheKeys.unreadTotal); + if (saved !== '' && saved != null) unread = Number(saved) || 0; + } catch (e) {} + } + if (!unread) { + try { + const badgeStr = wx.getStorageSync('tabBarMessageBadge'); + if (badgeStr && badgeStr !== '0') { + unread = badgeStr === '99+' ? 100 : (parseInt(badgeStr, 10) || 0); + } + } catch (e) {} + } + if (unread > 0) { + updateTabBarBadge(app, unread); + } else if (messageManager.tabBarBadgeText) { + app.emitEvent('tabBarBadgeChanged', { + index: messageManager.tabBarIndex, + badgeText: messageManager.tabBarBadgeText, + showRedDot: true, + forceUpdate: true, + }); + updateTabBarDirectly(app, messageManager.tabBarBadgeText); + } +} + function applyUnreadTotal(app, unreadTotal) { const prev = app.globalData.messageManager.unreadTotal; + const connected = isSdkConnected(); + if (unreadTotal === 0 && !connected && prev > 0) { + restoreCachedBadge(app); + return; + } app.globalData.messageManager.unreadTotal = unreadTotal; updateTabBarBadge(app, unreadTotal); wx.setStorageSync(app.globalData.messageManager.cacheKeys.unreadTotal, unreadTotal); @@ -789,7 +845,13 @@ function loadConversations(app) { function loadConversationsNow(app) { if (_loadConvInFlight) return; - if (!isSdkConnected() || !wx.goEasy?.im?.latestConversations) return; + if (!isSdkConnected() || !wx.goEasy?.im?.latestConversations) { + restoreCachedBadge(app); + if (app.globalData.goEasyConnection.autoReconnect) { + ensureConnection(app); + } + return; + } _loadConvInFlight = true; wx.goEasy.im.latestConversations({ onSuccess: (result) => { @@ -815,6 +877,7 @@ function loadConversationsNow(app) { onFailed: (error) => { _loadConvInFlight = false; console.error('加载会话列表失败:', error); + restoreCachedBadge(app); if (!isSdkConnected() && app.globalData.goEasyConnection.autoReconnect) { attemptReconnect(app); } @@ -842,7 +905,14 @@ function updateCurrentPageState(app) { const pages = getCurrentPages(); if (pages.length > 0) { const currentPage = pages[pages.length - 1]; - const chatPages = ['pages/liaotian/liaotian', 'pages/qunliaotian/qunliaotian', 'pages/kefuliaotian/kefuliaotian']; + const chatPages = [ + 'pages/chat/chat', + 'pages/group-chat/group-chat', + 'pages/cs-chat/cs-chat', + 'pages/liaotian/liaotian', + 'pages/qunliaotian/qunliaotian', + 'pages/kefuliaotian/kefuliaotian', + ]; app.globalData.pageState.currentPage = currentPage.route; app.globalData.pageState.isInChatPage = chatPages.indexOf(currentPage.route) >= 0; app.globalData.pageState.lastPageUpdate = Date.now(); @@ -907,7 +977,8 @@ function updateTabBarDirectly(app, badgeText) { try { const pages = getCurrentPages(); for (let i = pages.length - 1; i >= 0; i--) { - const tabBar = pages[i].selectComponent('#custom-tab-bar'); + const tabBar = pages[i].selectComponent('#custom-tab-bar') + || pages[i].selectComponent('tab-bar'); if (tabBar && tabBar.setData) { tabBar.setData({ badgeText }); break; diff --git a/utils/chat-history.js b/utils/chat-history.js new file mode 100644 index 0000000..4946c9e --- /dev/null +++ b/utils/chat-history.js @@ -0,0 +1,106 @@ +/** + * 聊天历史消息加载(私聊 / 群聊 / 客服共用) + * - 排除本地/欢迎语等合成消息的 timestamp,避免下拉加载错位 + * - 统一 IM 连接等待与 history API 封装 + */ + +const SYNTHETIC_MSG_ID = /^(local-|img-|order-|welcome-)/; + +export function isHistorySyntheticMessage(msg) { + return !!(msg && msg.messageId && SYNTHETIC_MSG_ID.test(String(msg.messageId))); +} + +/** 取当前列表里最早一条「真实」服务端消息的时间戳,供 GoEasy history 分页 */ +export function getOldestHistoryTimestamp(messages, fallback = null) { + if (!Array.isArray(messages)) return fallback; + let oldest = null; + for (const m of messages) { + if (!m || m.timestamp == null) continue; + if (isHistorySyntheticMessage(m)) continue; + if (oldest == null || m.timestamp < oldest) oldest = m.timestamp; + } + return oldest != null ? oldest : fallback; +} + +export function isImConnected() { + const status = wx.goEasy?.getConnectionStatus?.() || 'disconnected'; + return (status === 'connected' || status === 'reconnected') && !!wx.goEasy?.im; +} + +/** 等待 IM 连接就绪(下拉加载历史前必须先连上) */ +export function waitImConnected(app, timeoutMs = 8000) { + return new Promise((resolve) => { + if (isImConnected()) { + resolve(true); + return; + } + if (app?.ensureConnection) app.ensureConnection(); + const start = Date.now(); + const timer = setInterval(() => { + if (isImConnected()) { + clearInterval(timer); + resolve(true); + } else if (Date.now() - start >= timeoutMs) { + clearInterval(timer); + resolve(false); + } + }, 200); + }); +} + +export function fetchHistoryMessages({ scene, id, lastTimestamp, limit = 20 }) { + return new Promise((resolve, reject) => { + if (!wx.goEasy?.im?.history) { + reject(new Error('IM未初始化')); + return; + } + if (!id) { + reject(new Error('会话ID为空')); + return; + } + wx.goEasy.im.history({ + type: scene, + id: String(id), + lastTimestamp: lastTimestamp ?? null, + limit, + onSuccess: (res) => resolve(res?.content || []), + onFailed: (err) => reject(err || new Error('加载历史消息失败')), + }); + }); +} + +/** + * 合并一页历史消息 + * @returns {{ messages, hasMore, lastTimestamp }} + */ +export function mergeHistoryMessages({ incoming, existing, refresh, limit = 20, mapMessage }) { + let list = Array.isArray(incoming) ? [...incoming] : []; + list.sort((a, b) => a.timestamp - b.timestamp); + if (typeof mapMessage === 'function') { + list = list.map((m, idx) => mapMessage(m, idx, list)); + } + + const prev = Array.isArray(existing) ? existing : []; + let final = refresh ? list : [...list, ...prev]; + + if (!refresh) { + for (let i = 0; i < final.length; i += 1) { + if (i === 0) final[i].showTime = true; + else final[i].showTime = (final[i].timestamp - final[i - 1].timestamp) / 60000 > 5; + } + } + + const ids = new Set(); + const unique = []; + for (const m of final) { + if (!m?.messageId || ids.has(m.messageId)) continue; + ids.add(m.messageId); + unique.push(m); + } + + return { + messages: unique, + hasMore: list.length >= limit, + lastTimestamp: getOldestHistoryTimestamp(unique, null), + }; +} diff --git a/utils/group-chat.js b/utils/group-chat.js index f259133..d354a4a 100644 --- a/utils/group-chat.js +++ b/utils/group-chat.js @@ -4,8 +4,9 @@ export const ORDER_CARD_PREFIX = '[ORDER_CARD]'; const ZHUANGTAI_MAP = { - 1: '已下单', 2: '进行中', 3: '已完成', 4: '退款中', + 1: '待抢单', 2: '进行中', 3: '已完成', 4: '退款中', 5: '已退款', 6: '退款失败', 7: '指定中', 8: '结算中', + 9: '未支付', 14: '支付失败', 25: '已废弃', }; export function getZhuangtaiText(zhuangtai) { @@ -38,6 +39,12 @@ export function resolveLocalGroupId(identityType, myUid, partnerUid, fadanPingta } return buildDashouBossGroupId(myUid, partnerUid); } + if (identityType === 'boss' || identityType === 'normal') { + if (partnerUid && myUid) { + return buildDashouBossGroupId(partnerUid, myUid); + } + return orderId ? `group_${orderId}` : null; + } return orderId ? `group_${orderId}` : null; } @@ -105,6 +112,32 @@ export function applyPairMetaToConversation(item, myGoEasyId, defaultAvatar, oss return; } + const parsed = parsePairGroupId(item.groupId); + if (parsed && myGoEasyId) { + const isDashou = myGoEasyId === parsed.dashouId; + const isPartner = myGoEasyId === parsed.partnerId; + if (isDashou) { + d.counterpartId = parsed.partnerId; + d.counterpartYonghuid = parsed.partnerId.replace(/^(Sj|Boss)/, ''); + d.partnerRoleLabel = parsed.partnerRole === 'boss' ? '老板' : '商家'; + if (!d.name || d.name === item.groupId) { + d.name = d.partnerNicheng || `${d.partnerRoleLabel}${d.counterpartYonghuid.slice(-6)}`; + } + if (!d.avatar || d.avatar === defaultAvatar) { + d.avatar = d.partnerAvatar || defaultAvatar; + } + } else if (isPartner) { + d.counterpartId = parsed.dashouId; + d.counterpartYonghuid = parsed.dashouId.replace(/^Ds/, ''); + if (!d.name || d.name === item.groupId) { + d.name = d.dashouNicheng || `打手${d.counterpartYonghuid.slice(-6)}`; + } + if (!d.avatar || d.avatar === defaultAvatar) { + d.avatar = d.dashouAvatar || defaultAvatar; + } + } + } + const counterpartId = getCounterpartGoEasyId(item.groupId, myGoEasyId); if (counterpartId) { d.counterpartId = counterpartId; @@ -215,6 +248,7 @@ export function getConversationSortTime(conversation, openTimes) { const id = conversation.groupId || conversation.userId || ''; const msgTs = conversation.lastMessage?.timestamp || 0; const openTs = (openTimes && id && openTimes[id]) ? openTimes[id] : 0; + // QQ/微信:取「最后消息时间」与「最近进入会话时间」较大值;新消息到达会更新 msgTs 并置顶 return Math.max(msgTs, openTs); } @@ -222,8 +256,6 @@ export function sortConversations(list, openTimes) { return list.sort((a, b) => { if (a.top && !b.top) return -1; if (!a.top && b.top) return 1; - if (a.unread > 0 && b.unread <= 0) return -1; - if (a.unread <= 0 && b.unread > 0) return 1; const ta = getConversationSortTime(a, openTimes); const tb = getConversationSortTime(b, openTimes); return tb - ta; diff --git a/utils/im-user.js b/utils/im-user.js index 29a2c56..20bcc83 100644 --- a/utils/im-user.js +++ b/utils/im-user.js @@ -24,9 +24,10 @@ export const IM_ROLE_PREFIX = { kaoheguan: 'Ds', }; +import { getDefaultAvatarUrl } from './avatar.js'; + export function getDefaultAvatar(app) { - const oss = app.globalData.ossImageUrl || ''; - return oss + (app.globalData.morentouxiang || ''); + return getDefaultAvatarUrl(app || getApp()); } export function getImPrefixForRole(role) { diff --git a/utils/imAuth/dashoujianquan.js b/utils/imAuth/dashoujianquan.js index b1fc2be..025cc03 100644 --- a/utils/imAuth/dashoujianquan.js +++ b/utils/imAuth/dashoujianquan.js @@ -4,9 +4,9 @@ const imRequest = require('./imRequest.js'); /** * 打手 IM 权限检测 * POST /yonghu/dshqltqx - * 后端返回 { code: 0, allow: 1 } 或 { code: 0, allow: 0 } */ -async function dashoujianquan(app) { +async function dashoujianquan(app, options = {}) { + const silent = options && options.silent === true; const uid = wx.getStorageSync('uid'); if (!uid) return { allowed: false, reason: '未登录' }; @@ -21,7 +21,8 @@ async function dashoujianquan(app) { const allow = res.data.allow; if (allow === 1) { return { allowed: true }; - } else { + } + if (!silent) { await app.disconnectGoEasy(); wx.showToast({ title: '当前身份已不支持消息聊天功能,请充值会员或联系管理员后尝试', @@ -30,22 +31,24 @@ async function dashoujianquan(app) { mask: true }); await new Promise(r => setTimeout(r, 2100)); - return { allowed: false, reason: '接单员消息权限被限制' }; } + return { allowed: false, reason: '接单员消息权限被限制' }; } throw new Error(res.data?.msg || '鉴权接口异常'); } catch (err) { console.error('打手鉴权失败:', err); - await app.disconnectGoEasy(); - wx.showToast({ - title: '消息服务暂不可用,请稍后重试', - icon: 'none', - duration: 2000, - mask: true - }); - await new Promise(r => setTimeout(r, 2100)); + if (!silent) { + await app.disconnectGoEasy(); + wx.showToast({ + title: '消息服务暂不可用,请稍后重试', + icon: 'none', + duration: 2000, + mask: true + }); + await new Promise(r => setTimeout(r, 2100)); + } return { allowed: false, reason: '鉴权请求失败' }; } } -module.exports = { dashoujianquan }; \ No newline at end of file +module.exports = { dashoujianquan }; diff --git a/utils/imAuth/jianquanxian.js b/utils/imAuth/jianquanxian.js index f373451..351b338 100644 --- a/utils/imAuth/jianquanxian.js +++ b/utils/imAuth/jianquanxian.js @@ -14,21 +14,21 @@ const { dashoujianquan } = require('./dashoujianquan.js'); * @param {Object} app 全局 App 实例 * @returns {Promise<{ allowed: boolean, reason?: string }>} */ -async function jianquanxian(app) { +async function jianquanxian(app, options = {}) { + const silent = options && options.silent === true; // 1. 检测登录态(用 uid 或 token) const token = wx.getStorageSync('token') || wx.getStorageSync('uid'); if (!token) { - // 没登录:先强制断开(如果已连接) - await app.disconnectGoEasy(); - // 弹窗提示,强制阅读 2 秒 - wx.showToast({ - title: '您当前处于未登录状态,消息功能暂不可用', - icon: 'none', - duration: 2000, - mask: true - }); - // 等待提示结束,确保用户看到 - await new Promise(resolve => setTimeout(resolve, 2100)); + if (!silent) { + await app.disconnectGoEasy(); + wx.showToast({ + title: '您当前处于未登录状态,消息功能暂不可用', + icon: 'none', + duration: 2000, + mask: true + }); + await new Promise(resolve => setTimeout(resolve, 2100)); + } return { allowed: false, reason: '未登录' }; } @@ -37,17 +37,16 @@ async function jianquanxian(app) { // 3. 根据角色分发到具体鉴权模块 switch (role) { - case 'normal': // 点单老板 - return laobanjianquan(app); + case 'normal': + return laobanjianquan(app, { silent }); - case 'dashou': // 打手 - return dashoujianquan(app); + case 'dashou': + return dashoujianquan(app, { silent }); - // 其他角色暂未细分,统一放行(后续可新增对应文件) case 'shangjia': case 'guanshi': case 'zuzhang': - case 'kaoheguan': + case 'kaoheguan': return { allowed: true }; default: diff --git a/utils/imAuth/laobanjianquan.js b/utils/imAuth/laobanjianquan.js index a29361a..275358f 100644 --- a/utils/imAuth/laobanjianquan.js +++ b/utils/imAuth/laobanjianquan.js @@ -4,9 +4,9 @@ const imRequest = require('./imRequest.js'); /** * 老板(点单用户)IM 权限检测 * POST /yonghu/lbhqltqx - * 后端返回 { code: 0, allow: 1 } 或 { code: 0, allow: 0 } */ -async function laobanjianquan(app) { +async function laobanjianquan(app, options = {}) { + const silent = options && options.silent === true; const uid = wx.getStorageSync('uid'); if (!uid) return { allowed: false, reason: '未登录' }; @@ -21,7 +21,8 @@ async function laobanjianquan(app) { const allow = res.data.allow; if (allow === 1) { return { allowed: true }; - } else { + } + if (!silent) { await app.disconnectGoEasy(); wx.showToast({ title: '当前身份已不支持使用消息功能', @@ -30,22 +31,24 @@ async function laobanjianquan(app) { mask: true }); await new Promise(r => setTimeout(r, 1100)); - return { allowed: false, reason: '当前身份无消息权限' }; } + return { allowed: false, reason: '当前身份无消息权限' }; } throw new Error(res.data?.msg || '鉴权接口异常'); } catch (err) { console.error('老板鉴权失败:', err); - await app.disconnectGoEasy(); - wx.showToast({ - title: '消息服务暂不可用,请稍后重试', - icon: 'none', - duration: 2000, - mask: true - }); - await new Promise(r => setTimeout(r, 2100)); + if (!silent) { + await app.disconnectGoEasy(); + wx.showToast({ + title: '消息服务暂不可用,请稍后重试', + icon: 'none', + duration: 2000, + mask: true + }); + await new Promise(r => setTimeout(r, 2100)); + } return { allowed: false, reason: '鉴权请求失败' }; } } -module.exports = { laobanjianquan }; \ No newline at end of file +module.exports = { laobanjianquan }; diff --git a/utils/kefu-nav.js b/utils/kefu-nav.js new file mode 100644 index 0000000..e5a1e38 --- /dev/null +++ b/utils/kefu-nav.js @@ -0,0 +1,12 @@ +/** 与消息页「联系客服」一致:跳转客服会话 */ +export function openCustomerServiceChat() { + const app = getApp(); + if (app.globalData.chatEnabled && app.ensureConnection) { + app.globalData.goEasyConnection.autoReconnect = true; + app.ensureConnection(); + } + const param = { teamId: 'support_team' }; + wx.navigateTo({ + url: '/pages/cs-chat/cs-chat?data=' + encodeURIComponent(JSON.stringify(param)), + }); +} diff --git a/utils/role-tab-bar.js b/utils/role-tab-bar.js index 8584da4..39f766f 100644 --- a/utils/role-tab-bar.js +++ b/utils/role-tab-bar.js @@ -249,8 +249,9 @@ export function reconnectForRole(role) { const connected = status === 'connected' || status === 'reconnected'; if (connected && currentUserId === targetUserId) { - if (app.loadConversations) app.loadConversations(); + if (app.restoreTabBarBadge) app.restoreTabBarBadge(); if (app.ensureConnection) app.ensureConnection(); + if (app.loadConversations) app.loadConversations(); return; } diff --git a/utils/staff-api.js b/utils/staff-api.js index 24f18c9..b964f86 100644 --- a/utils/staff-api.js +++ b/utils/staff-api.js @@ -224,8 +224,9 @@ export function syncStaffUi(page) { const ctx = staff ? (getStaffContext() || {}) : null; const wallet = (ctx && ctx.wallet) || {}; const portal = isMerchantPortalUser(); + const staffFlag = Number(wx.getStorageSync('staffstatus')) === 1; page.setData({ - isShangjia: portal, + isShangjia: portal || staffFlag || !!(page.data && page.data.isShangjia), isStaffMode: staff, isMerchantOwner: !staff && Number(wx.getStorageSync('shangjiastatus')) === 1, staffRoleName: staff ? (ctx.role_name || '客服') : '', diff --git a/utils/xiaoxilj.js b/utils/xiaoxilj.js index e4eda3f..a8c7c95 100644 --- a/utils/xiaoxilj.js +++ b/utils/xiaoxilj.js @@ -1,269 +1,296 @@ -/** - * 订单群聊跳转:先确定群 ID,IM 连接失败也尽量能进聊天页 - */ -import request from './request'; -import { persistGroupMeta } from './im-user.js'; -import { resolveLocalGroupId } from './group-chat.js'; - -const app = getApp(); - -function formatError(err) { - if (!err) return '进入聊天失败'; - if (typeof err === 'string') return err; - if (err.message) return err.message; - if (err.content) return String(err.content); - if (err.msg) return String(err.msg); - if (err.code) return `连接失败(${err.code})`; - try { - return JSON.stringify(err); - } catch (e) { - return '进入聊天失败'; - } -} - -function withTimeout(promise, ms, errMsg) { - return new Promise((resolve, reject) => { - const timer = setTimeout(() => reject(new Error(errMsg || '操作超时')), ms); - promise - .then((v) => { clearTimeout(timer); resolve(v); }) - .catch((e) => { clearTimeout(timer); reject(e); }); - }); -} - -function getConnectedUserId() { - const fromState = app.globalData?.goEasyConnection?.userId; - const fromIm = wx.goEasy?.im?.userId; - return fromState || fromIm || ''; -} - -function isImConnected() { - const status = wx.goEasy?.getConnectionStatus?.() || 'disconnected'; - return status === 'connected' || status === 'reconnected'; -} - -async function ensureIdentityConnection(identityType, userId) { - app.globalData.currentRole = identityType; - wx.setStorageSync('currentRole', identityType); - - const uid = userId.replace(/^(Ds|Sj|Boss|Gs|Zz|Kh)/, ''); - const avatar = wx.getStorageSync('touxiang') || - (app.globalData.ossImageUrl + app.globalData.morentouxiang); - app.globalData.currentUser = { - id: userId, - name: app.globalData.currentUser?.name || `用户${uid}`, - avatar: avatar, - }; - - if (!app.globalData.chatEnabled || !wx.goEasy?.im) { - throw new Error('聊天服务未就绪,请稍后重试'); - } - - if (isImConnected() && getConnectedUserId() === userId) { - return; - } - - const connectedId = getConnectedUserId(); - if (connectedId && connectedId !== userId && app.disconnectGoEasy) { - await app.disconnectGoEasy(); - } - - if (!app.connectWithIdentity) { - throw new Error('聊天功能未初始化'); - } - - await withTimeout( - app.connectWithIdentity(identityType, userId, true), - 12000, - 'IM连接超时' - ); - - if (!isImConnected()) { - throw new Error('IM连接失败,请检查网络后重试'); - } -} - -async function prepareGroupChat(identityType, userId, orderId, partnerUid, fadanPingtai, groupName, isCross) { - let chatData = null; - - try { - const res = await withTimeout( - request({ - url: '/dingdan/ltdhzb', - method: 'POST', - data: { - dingdan_id: orderId, - identityType, - push_order_card: false, - }, - }), - 12000, - '准备群聊超时' - ); - const body = res?.data || {}; - if (body.code === 0 && body.data?.groupId) { - chatData = body.data; - } else if (body.msg) { - console.warn('ltdhzb:', body.msg); - } - } catch (e) { - console.warn('ltdhzb 请求失败,尝试本地 groupId', e); - } - - if (!chatData?.groupId) { - const myUid = userId.replace(/^(Ds|Sj|Boss)/, ''); - let localGroupId = resolveLocalGroupId( - identityType, myUid, partnerUid, fadanPingtai, orderId, isCross - ); - if (!localGroupId && orderId) { - localGroupId = `group_${orderId}`; - } - if (!localGroupId) { - throw new Error('无法确定群聊,请确认订单已接单且对方信息完整'); - } - chatData = { - groupId: localGroupId, - orderId, - groupName: groupName || '订单群聊', - isCross: isCross || fadanPingtai || 0, - }; - try { - await request({ - url: '/dingdan/ltdhzb', - method: 'POST', - data: { dingdan_id: orderId, identityType, push_order_card: false }, - }); - } catch (e) { /* ignore */ } - } - - return chatData; -} - -class ConnectionManager { - async connectAndChat(params) { - const { identityType, userId, targetUser } = params; - if (!identityType || !userId || !targetUser || !targetUser.id) { - throw new Error('参数不完整'); - } - - await ensureIdentityConnection(identityType, userId); - - const currentUser = { - id: userId, - name: app.globalData.currentUser?.name || '用户', - avatar: app.globalData.currentUser?.avatar || (app.globalData.ossImageUrl + app.globalData.morentouxiang), - }; - - const to = { - id: targetUser.id, - name: targetUser.name || '用户', - avatar: targetUser.avatar || (app.globalData.ossImageUrl + app.globalData.morentouxiang), - }; - - const param = { to, currentUser }; - wx.navigateTo({ url: '/pages/chat/chat?data=' + encodeURIComponent(JSON.stringify(param)) }); - } - - async connectToGroupChat(params) { - const { - identityType, userId, orderId, groupName, groupAvatar, isCross, - partnerUid, fadanPingtai, - } = params; - - if (!identityType || !userId || !orderId) { - throw new Error('参数不完整'); - } - - app.globalData.currentRole = identityType; - wx.setStorageSync('currentRole', identityType); - - wx.showLoading({ title: '建立联系中...', mask: true }); - - try { - const chatData = await prepareGroupChat( - identityType, userId, orderId, partnerUid, fadanPingtai || isCross, groupName, isCross - ); - - const realGroupId = chatData.groupId; - let avatar = chatData.counterpartAvatar || chatData.groupAvatar || groupAvatar || ''; - if (avatar && !avatar.startsWith('http')) { - avatar = app.globalData.ossImageUrl + avatar.replace(/^\//, ''); - } - - if (!app.globalData.groupInfoMap) app.globalData.groupInfoMap = {}; - const meta = { - name: chatData.counterpartName || chatData.groupName || groupName, - avatar, - orderId: chatData.orderId || orderId, - orderDesc: chatData.orderDesc || '', - orderZhuangtai: chatData.orderZhuangtai, - dashouGoEasyId: chatData.dashouGoEasyId, - partnerGoEasyId: chatData.partnerGoEasyId, - dashouYonghuid: chatData.dashouYonghuid, - partnerYonghuid: chatData.partnerYonghuid, - dashouName: chatData.dashouName, - partnerName: chatData.partnerName, - dashouAvatar: chatData.dashouAvatar, - partnerAvatar: chatData.partnerAvatar, - counterpartId: chatData.counterpartId, - counterpartYonghuid: chatData.counterpartYonghuid, - }; - app.globalData.groupInfoMap[realGroupId] = meta; - persistGroupMeta(app, realGroupId, meta); - - let imReady = false; - try { - await ensureIdentityConnection(identityType, userId); - imReady = true; - try { - await withTimeout( - new Promise((resolve, reject) => { - wx.goEasy.im.subscribeGroup({ - groupIds: [realGroupId], - onSuccess: () => resolve(), - onFailed: (error) => reject(error), - }); - }), - 8000, - '订阅群聊超时' - ); - } catch (subErr) { - console.warn('订阅群聊失败,仍尝试进入页面', subErr); - } - } catch (imErr) { - console.warn('IM连接失败,仍进入聊天页由页面重连', imErr); - } - - const uid = userId.replace(/^(Ds|Sj|Boss)/, ''); - const param = { - groupId: realGroupId, - orderId: chatData.orderId || orderId, - groupName: chatData.counterpartName || chatData.groupName || groupName || '订单群聊', - groupAvatar: avatar, - isCross: chatData.isCross != null ? chatData.isCross : (isCross || 0), - currentUserId: userId, - currentUserName: app.globalData.currentUser?.name || `用户${uid}`, - currentUserAvatar: app.globalData.currentUser?.avatar || - (wx.getStorageSync('touxiang') || (app.globalData.ossImageUrl + app.globalData.morentouxiang)), - orderZhuangtai: chatData.orderZhuangtai, - orderJine: chatData.orderJine, - orderDesc: chatData.orderDesc || '', - imReady, - }; - - wx.hideLoading(); - wx.navigateTo({ - url: '/pages/group-chat/group-chat?data=' + encodeURIComponent(JSON.stringify(param)), - fail: (navErr) => { - wx.showToast({ title: formatError(navErr), icon: 'none', duration: 2500 }); - }, - }); - } catch (err) { - wx.hideLoading(); - console.error('跳转群聊失败:', err); - wx.showToast({ title: formatError(err), icon: 'none', duration: 2500 }); - throw err; - } - } -} - -export default new ConnectionManager(); +/** + * 订单群聊跳转:与文赫一致,新群用打手+商家/老板配对 groupId;旧 group_{订单号} 会话保留 + */ +import request from './request'; +import { persistGroupMeta } from './im-user.js'; +import { resolveLocalGroupId } from './group-chat.js'; + +const app = getApp(); + +function formatError(err) { + if (!err) return '进入聊天失败'; + if (typeof err === 'string') return err; + if (err.message) return err.message; + if (err.msg) return String(err.msg); + if (err.content) return String(err.content); + if (err.code) return `连接失败(${err.code})`; + try { + return JSON.stringify(err); + } catch (e) { + return '进入聊天失败'; + } +} + +function withTimeout(promise, ms, errMsg) { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error(errMsg || '操作超时')), ms); + promise + .then((v) => { clearTimeout(timer); resolve(v); }) + .catch((e) => { clearTimeout(timer); reject(e); }); + }); +} + +function mapIdentityForApi(identityType) { + if (identityType === 'dashou') return 'dashou'; + if (identityType === 'shangjia') return 'shangjia'; + return 'normal'; +} + +function validateIdentity(identityType, userId) { + if (identityType === 'dashou' && !userId.startsWith('Ds')) { + throw new Error('接单员 ID 必须以 Ds 开头'); + } + if (identityType === 'shangjia' && !userId.startsWith('Sj')) { + throw new Error('商家 ID 必须以 Sj 开头'); + } + if ((identityType === 'boss' || identityType === 'normal') && !userId.startsWith('Boss')) { + throw new Error('老板 ID 必须以 Boss 开头'); + } +} + +function getConnectedUserId() { + const fromState = app.globalData?.goEasyConnection?.userId; + const fromIm = wx.goEasy?.im?.userId; + return fromState || fromIm || ''; +} + +function isImConnected() { + const status = wx.goEasy?.getConnectionStatus?.() || 'disconnected'; + return status === 'connected' || status === 'reconnected'; +} + +async function ensureIdentityConnection(identityType, userId) { + validateIdentity(identityType, userId); + + app.globalData.currentRole = identityType; + wx.setStorageSync('currentRole', identityType); + + const uid = userId.replace(/^(Ds|Sj|Boss)/, ''); + const avatar = wx.getStorageSync('touxiang') || + (app.globalData.ossImageUrl + app.globalData.morentouxiang); + app.globalData.currentUser = { + id: userId, + name: app.globalData.currentUser?.name || `用户${uid}`, + avatar, + }; + + if (!app.globalData.chatEnabled || !wx.goEasy?.im) { + throw new Error('聊天服务未就绪,请稍后重试'); + } + + if (isImConnected() && getConnectedUserId() === userId) { + return; + } + + const connectedId = getConnectedUserId(); + if (connectedId && connectedId !== userId && app.disconnectGoEasy) { + await app.disconnectGoEasy(); + } + + if (!app.connectWithIdentity) { + throw new Error('聊天功能未初始化'); + } + + await withTimeout( + app.connectWithIdentity(identityType, userId, true), + 12000, + 'IM连接超时' + ); + + if (!isImConnected()) { + throw new Error('IM连接失败,请检查网络后重试'); + } +} + +async function prepareGroupChat(identityType, userId, orderId, partnerUid, fadanPingtai, groupName, isCross) { + const orderIdStr = String(orderId || '').replace(/^group_/, ''); + let chatData = null; + + try { + const res = await withTimeout( + request({ + url: '/dingdan/ltdhzb', + method: 'POST', + data: { + dingdan_id: orderIdStr, + identityType: mapIdentityForApi(identityType), + push_order_card: false, + }, + }), + 12000, + '准备群聊超时' + ); + const body = res?.data || {}; + if ((body.code === 0 || body.code === 200) && body.data?.groupId) { + chatData = body.data; + } else if (body.msg) { + console.warn('ltdhzb:', body.msg); + } + } catch (e) { + console.warn('ltdhzb 请求失败,尝试本地配对 groupId', e); + } + + if (!chatData?.groupId) { + const myUid = userId.replace(/^(Ds|Sj|Boss)/, ''); + let localGroupId = resolveLocalGroupId( + identityType, myUid, partnerUid, fadanPingtai, orderIdStr, isCross + ); + if (!localGroupId && orderIdStr) { + localGroupId = `group_${orderIdStr}`; + } + if (!localGroupId) { + throw new Error('无法确定群聊,请确认订单已接单且对方信息完整'); + } + chatData = { + groupId: localGroupId, + orderId: orderIdStr, + groupName: groupName || '订单群聊', + isCross: isCross || fadanPingtai || 0, + }; + try { + await request({ + url: '/dingdan/ltdhzb', + method: 'POST', + data: { + dingdan_id: orderIdStr, + identityType: mapIdentityForApi(identityType), + push_order_card: false, + }, + }); + } catch (e) { /* ignore */ } + } + + return chatData; +} + +class ConnectionManager { + async connectAndChat(params) { + const { identityType, userId, targetUser } = params; + if (!identityType || !userId || !targetUser || !targetUser.id) { + throw new Error('参数不完整'); + } + + await ensureIdentityConnection(identityType, userId); + + const currentUser = { + id: userId, + name: app.globalData.currentUser?.name || '用户', + avatar: app.globalData.currentUser?.avatar || (app.globalData.ossImageUrl + app.globalData.morentouxiang), + }; + + const to = { + id: targetUser.id, + name: targetUser.name || '用户', + avatar: targetUser.avatar || (app.globalData.ossImageUrl + app.globalData.morentouxiang), + }; + + wx.navigateTo({ + url: '/pages/chat/chat?data=' + encodeURIComponent(JSON.stringify({ to, currentUser })), + }); + } + + async connectToGroupChat(params) { + const { + identityType, userId, orderId, groupName, groupAvatar, isCross, + partnerUid, fadanPingtai, + } = params; + + if (!identityType || !userId || !orderId) { + throw new Error('参数不完整'); + } + + app.globalData.currentRole = identityType; + wx.setStorageSync('currentRole', identityType); + + wx.showLoading({ title: '建立联系中...', mask: true }); + + try { + const chatData = await prepareGroupChat( + identityType, userId, orderId, partnerUid, fadanPingtai || isCross, groupName, isCross + ); + + const realGroupId = chatData.groupId; + let avatar = chatData.counterpartAvatar || chatData.groupAvatar || groupAvatar || ''; + if (avatar && !avatar.startsWith('http')) { + avatar = app.globalData.ossImageUrl + avatar.replace(/^\//, ''); + } + + if (!app.globalData.groupInfoMap) app.globalData.groupInfoMap = {}; + const meta = { + name: chatData.counterpartName || chatData.groupName || groupName, + avatar, + orderId: chatData.orderId || String(orderId).replace(/^group_/, ''), + orderDesc: chatData.orderDesc || '', + orderZhuangtai: chatData.orderZhuangtai, + orderJine: chatData.orderJine, + dashouGoEasyId: chatData.dashouGoEasyId, + partnerGoEasyId: chatData.partnerGoEasyId, + dashouYonghuid: chatData.dashouYonghuid, + partnerYonghuid: chatData.partnerYonghuid, + dashouName: chatData.dashouName, + partnerName: chatData.partnerName, + dashouAvatar: chatData.dashouAvatar, + partnerAvatar: chatData.partnerAvatar, + counterpartId: chatData.counterpartId, + counterpartYonghuid: chatData.counterpartYonghuid, + }; + app.globalData.groupInfoMap[realGroupId] = meta; + persistGroupMeta(app, realGroupId, meta); + + let imReady = false; + try { + await ensureIdentityConnection(identityType, userId); + imReady = true; + try { + await withTimeout( + new Promise((resolve, reject) => { + wx.goEasy.im.subscribeGroup({ + groupIds: [realGroupId], + onSuccess: () => resolve(), + onFailed: (error) => reject(error), + }); + }), + 8000, + '订阅群聊超时' + ); + } catch (subErr) { + console.warn('订阅群聊失败,仍尝试进入页面', subErr); + } + } catch (imErr) { + console.warn('IM连接失败,仍进入聊天页由页面重连', imErr); + } + + const uid = userId.replace(/^(Ds|Sj|Boss)/, ''); + const param = { + groupId: realGroupId, + orderId: chatData.orderId || String(orderId).replace(/^group_/, ''), + groupName: chatData.counterpartName || chatData.groupName || groupName || '订单群聊', + groupAvatar: avatar, + isCross: chatData.isCross != null ? chatData.isCross : (isCross || 0), + currentUserId: userId, + currentUserName: app.globalData.currentUser?.name || `用户${uid}`, + currentUserAvatar: app.globalData.currentUser?.avatar || + (wx.getStorageSync('touxiang') || (app.globalData.ossImageUrl + app.globalData.morentouxiang)), + orderZhuangtai: chatData.orderZhuangtai, + orderJine: chatData.orderJine, + orderDesc: chatData.orderDesc || '', + imReady, + }; + + wx.hideLoading(); + wx.navigateTo({ + url: '/pages/group-chat/group-chat?data=' + encodeURIComponent(JSON.stringify(param)), + fail: (navErr) => { + wx.showToast({ title: formatError(navErr), icon: 'none', duration: 2500 }); + }, + }); + } catch (err) { + wx.hideLoading(); + console.error('跳转群聊失败:', err); + wx.showToast({ title: formatError(err), icon: 'none', duration: 2500 }); + throw err; + } + } +} + +export default new ConnectionManager(); diff --git a/utils/zhiding-order.js b/utils/zhiding-order.js new file mode 100644 index 0000000..e3a8981 --- /dev/null +++ b/utils/zhiding-order.js @@ -0,0 +1,53 @@ +import request from './request.js'; +import { getDefaultAvatarUrl } from './avatar.js'; + +export const ZHIDING_HF_MAP = { 1: '想接', 2: '已婉拒', 3: '等会接' }; + +export async function fetchMyZhidingOrders() { + try { + const res = await request({ url: '/dingdan/zdcx', method: 'POST', data: {} }); + const body = res?.data; + if (body && (body.code === 200 || body.code === 0) && body.data) { + return body.data.list || []; + } + } catch (e) { + console.warn('fetchMyZhidingOrders failed', e); + } + return []; +} + +export async function submitZhidingResponse(dingdanId, huifu, dengHuiNote = '') { + const res = await request({ + url: '/dingdan/zdhf', + method: 'POST', + data: { + dingdan_id: dingdanId, + huifu, + deng_hui_note: dengHuiNote, + }, + }); + return res?.data; +} + +export function processZhidingList(list, app, uid, highlightOrderId = '') { + const defAvatar = getDefaultAvatarUrl(app); + const oss = app.globalData?.ossImageUrl || ''; + return (list || []).map((item) => { + let avatar = defAvatar; + if (item.zhiding_avatar) { + avatar = item.zhiding_avatar.startsWith('http') ? item.zhiding_avatar : oss + item.zhiding_avatar; + } + return { + ...item, + isMyZhiding: true, + zhiding_avatar_full: avatar, + zhiding_hf_text: item.zhiding_hf_text || ZHIDING_HF_MAP[item.zhiding_hf] || '', + isHighlighted: highlightOrderId && item.dingdan_id === highlightOrderId, + }; + }).filter((item) => String(item.zhiding_uid) === String(uid)); +} + +/** 横幅只展示尚未回复的指定单(已等会接/不想接的不占横幅) */ +export function filterZhidingBannerList(list) { + return (list || []).filter((item) => !item.zhiding_hf); +}