优化聊天:配对群组、稳定连接、消息列表与订单详情跳转

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-06-23 22:48:42 +08:00
parent 5e90f7c87c
commit 1ea106f101
8 changed files with 365 additions and 365 deletions

View File

@@ -1,6 +1,8 @@
const app = getApp();
import { formatDate } from '../../static/lib/utils';
import { sendGroupMessage } from '../../utils/message-sender';
import { showConfirmByScene } from '../../utils/scriptService.js';
import { normalizeGroupMessage } from '../../utils/group-chat.js';
Page({
data: {
@@ -89,33 +91,45 @@ Page({
},*/
onShow() {
this.autoConnect();
this.ensureChatConnection();
},
onHide() {
this.clearAllListeners();
this.clearPageListeners();
},
autoConnect() {
ensureChatConnection() {
const role = app.globalData.currentRole || 'normal';
const uid = wx.getStorageSync('uid');
const prefixMap = { normal:'Boss', dashou:'Ds', shangjia:'Sj', guanshi:'Gs', zuzhang:'Zz' };
const prefix = prefixMap[role] || 'Boss';
const targetUserId = prefix + uid;
const targetUserId = (prefixMap[role] || 'Boss') + uid;
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.subscribeGroupIfNeeded();
this.setupAllListeners();
this.loadHistory(true);
return;
} else {
wx.goEasy.disconnect();
}
const onReady = () => {
this.subscribeGroupIfNeeded();
this.setupAllListeners();
this.loadHistory(true).then(() => this.markGroupMessageAsRead());
};
const status = wx.goEasy?.getConnectionStatus?.() || 'disconnected';
const currentUserId = wx.goEasy?.im?.userId;
if ((status === 'connected' || status === 'reconnected') && currentUserId === targetUserId) {
onReady();
return;
}
this.connectGoEasy(targetUserId);
if (app.connectWithIdentity) {
app.connectWithIdentity(role, targetUserId, true).then(onReady).catch(() => {
const s = wx.goEasy?.getConnectionStatus?.();
if (s === 'connected' || s === 'reconnected') onReady();
});
} else {
this.connectGoEasy(targetUserId);
}
},
clearPageListeners() {
this.clearAllListeners();
},
connectGoEasy(userId) {
@@ -130,10 +144,7 @@ Page({
onSuccess: () => {
this.subscribeGroupIfNeeded();
this.setupAllListeners();
this.loadHistory(true).then(()=>{
this.markGroupMessageAsRead
});
this.loadHistory(true).then(() => this.markGroupMessageAsRead());
},
onFailed: (error) => {
if (error.code === 408) {
@@ -213,6 +224,7 @@ Page({
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;
@@ -248,6 +260,7 @@ Page({
this._msgHandler = (msg) => {
if (msg.groupId !== this.data.groupId) return;
if (msg.senderId === this.data.currentUser.id) return;
normalizeGroupMessage(msg);
msg.formattedTime = formatDate(msg.timestamp);
const msgs = this.data.messages;
const last = msgs.length ? msgs[msgs.length-1] : null;
@@ -301,8 +314,11 @@ Page({
sendMessage() {
if (this.data.pendingImage) {
this.sendImageMsg(this.data.pendingImage);
this.setData({ pendingImage: '' });
const file = this.data.pendingImage;
showConfirmByScene('chat_image_confirm', () => {
this.sendImageMsg(file);
this.setData({ pendingImage: '' });
});
return;
}
const text = this.data.inputText.trim();

View File

@@ -1,6 +1,7 @@
// pages/sjddxq/sjddxq.js - 商家订单详情(完整版,已添加罚款/退款操作及教程按钮)
const app = getApp()
import request from '../../utils/request.js'
import connectionManager from '../../utils/xiaoxilj.js'
import PopupService from '../../services/popupService.js' // 新增:导入弹窗服务
Page({
@@ -94,55 +95,17 @@ Page({
ensureIMConnection() {
const uid = wx.getStorageSync('uid')
if (!uid) return
app.globalData.currentRole = 'shangjia'
wx.setStorageSync('currentRole', 'shangjia')
const targetUserId = 'Sj' + uid
if (wx.goEasy && wx.goEasy.getConnectionStatus) {
const status = wx.goEasy.getConnectionStatus()
if (status === 'connected' || status === 'reconnected') {
const currentUserId = wx.goEasy.im ? wx.goEasy.im.userId : null
if (currentUserId === targetUserId) {
this.setData({ currentIMUserId: targetUserId })
return
} else {
wx.goEasy.disconnect()
}
}
const status = wx.goEasy?.getConnectionStatus?.() || 'disconnected'
const currentUserId = wx.goEasy?.im?.userId
if ((status === 'connected' || status === 'reconnected') && currentUserId === targetUserId) {
this.setData({ currentIMUserId: targetUserId })
return
}
this.connectGoEasy(targetUserId)
},
connectGoEasy(userId) {
let currentUser = app.globalData.currentUser
if (!currentUser) {
const uid = wx.getStorageSync('uid')
currentUser = {
id: uid,
name: '用户' + (uid ? uid.substring(0, 6) : ''),
avatar: app.globalData.ossImageUrl + app.globalData.morentouxiang
}
}
wx.goEasy.connect({
id: userId,
data: {
name: currentUser.name,
avatar: currentUser.avatar || (app.globalData.ossImageUrl + app.globalData.morentouxiang)
},
onSuccess: () => {
this.setData({ currentIMUserId: userId })
try {
wx.setStorageSync('savedGoEasyConnection', JSON.stringify({
userId, identityType: 'shangjia', lastConnectTime: Date.now()
}))
wx.setStorageSync('goEasyUserId', userId)
} catch (e) {}
},
onFailed: (error) => {
if (error.code === 408) {
this.setData({ currentIMUserId: userId })
} else {
console.error('IM连接失败', error)
}
}
})
if (app.ensureConnection) app.ensureConnection()
this.setData({ currentIMUserId: targetUserId })
},
jiexiTiaozhuanCanshu(options) {
@@ -265,7 +228,7 @@ Page({
})
},
// ===== 联系打手(替打手订阅 + 发机器人消息 + 跳转) =====
// ===== 联系打手(后端准备配对群 + 稳定跳转) =====
goToChatWithDashou() {
const uid = wx.getStorageSync('uid')
if (!uid) return
@@ -276,110 +239,14 @@ Page({
return
}
const targetUserId = 'Sj' + uid
if (this.data.currentIMUserId !== targetUserId) {
this.ensureIMConnection()
wx.showToast({ title: '连接中,请稍后再试', icon: 'none' })
return
}
const that = this
const groupName = this.data.jibenShuju.nicheng + '的订单群'
const isCross = this.data.jibenShuju.fadanpingtai || 0
const dashouUid = this.data.dashouInfo.yonghuid
// 第一步:商家自己订阅群组
wx.goEasy.im.subscribeGroup({
groupIds: [orderId],
onSuccess: () => {
// 如果有打手,则需要帮打手订阅并发送消息
if (dashouUid) {
that.bangDashouDingyue(orderId, dashouUid)
.then(() => {
that.tiaozhuanQunLiao(orderId, groupName, isCross)
})
.catch((err) => {
console.error('替打手订阅失败,仍可跳转群聊', err)
wx.showToast({ title: '通知打手可能失败', icon: 'none' })
that.tiaozhuanQunLiao(orderId, groupName, isCross)
})
} else {
// 无打手,直接跳转
that.tiaozhuanQunLiao(orderId, groupName, isCross)
}
},
onFailed: (error) => {
console.error('订阅群组失败', error)
wx.showToast({ title: '连接群聊失败', icon: 'none' })
}
})
},
// 帮打手订阅群组 + 发送机器人消息
bangDashouDingyue(orderId, dashouUid) {
return new Promise((resolve, reject) => {
const dashouImId = 'Ds' + dashouUid
const appkey = 'BC-32e2991c69bc47e1b538e73aaed37fc1' // 你的GoEasy AppKey
const restHost = 'https://rest-hz.goeasy.io'
// 1. 通过 REST API 为打手订阅群组
wx.request({
url: restHost + '/v2/im/subscribe-groups',
method: 'POST',
header: { 'content-type': 'application/json' },
data: {
appkey: appkey,
userIds: [dashouImId],
groupIds: [orderId]
},
success: (res) => {
if (res.data.code === 200) {
// 2. 订阅成功后,以打手身份发送一条系统消息
wx.request({
url: restHost + '/v2/im/message',
method: 'POST',
header: { 'content-type': 'application/json' },
data: {
appkey: appkey,
senderId: dashouImId,
senderData: { name: '系统通知', avatar: '' },
to: {
type: 'group',
id: orderId,
data: { name: '订单群', avatar: '' }
},
type: 'text',
payload: '此消息为系统自动发送,打手看到后会尽快回复您。'
},
success: (msgRes) => {
if (msgRes.data.code === 200) {
resolve()
} else {
reject(new Error('发送消息失败'))
}
},
fail: reject
})
} else {
reject(new Error('订阅打手失败'))
}
},
fail: reject
})
})
},
// 跳转群聊页面
tiaozhuanQunLiao(orderId, groupName, isCross) {
const param = {
groupId: orderId,
orderId: orderId,
groupName: groupName,
groupAvatar: '',
isCross: isCross
}
wx.navigateTo({
url: '/pages/qunliaotian/qunliaotian?data=' + encodeURIComponent(JSON.stringify(param))
connectionManager.connectToGroupChat({
identityType: 'shangjia',
userId: 'Sj' + uid,
orderId,
groupName: (this.data.dashouInfo.nicheng || this.data.jibenShuju.nicheng) + '的订单群',
isCross: this.data.jibenShuju.fadanpingtai || 0
}).catch(err => {
console.error('跳转群聊失败', err)
})
},

View File

@@ -2,6 +2,7 @@
const app = getApp();
import { formatDate } from '../../static/lib/utils';
const { jianquanxian } = require('../../utils/imAuth/jianquanxian');
import { enrichGroupConversation, isOrderGroupConversation } from '../../utils/group-chat.js';
Page({
data: {
@@ -9,7 +10,7 @@ Page({
filteredConversations: [],
activeTab: 0,
tabUnread: [0, 0, 0],
displayCount: 10,
displayCount: 4,
searchKeyword: '',
actionPopup: { visible: false, conversation: null },
currentUser: null,
@@ -81,26 +82,42 @@ Page({
return true;
},
// ========== 自动连接 ==========
// ========== 自动连接(复用全局连接,避免反复 disconnect ==========
autoConnect() {
const role = app.globalData.currentRole || 'normal';
const uid = wx.getStorageSync('uid');
const prefixMap = { normal: 'Boss', dashou: 'Ds', shangjia: 'Sj', guanshi: 'Gs', zuzhang: 'Zz' };
const prefix = prefixMap[role] || 'Boss';
const targetUserId = prefix + uid;
const targetUserId = (prefixMap[role] || 'Boss') + uid;
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.setupConversationListener();
this.loadConversations();
return;
} else {
wx.goEasy.disconnect();
}
const onReady = () => {
this.setupConversationListener();
this.loadConversations();
};
const status = wx.goEasy?.getConnectionStatus?.() || 'disconnected';
const currentUserId = wx.goEasy?.im?.userId;
if ((status === 'connected' || status === 'reconnected') && currentUserId === targetUserId) {
onReady();
return;
}
this.connectGoEasy(targetUserId);
if (currentUserId && currentUserId !== targetUserId && app.disconnectGoEasy) {
app.disconnectGoEasy().then(() => {
app.connectWithIdentity(role, targetUserId, true).then(onReady).catch(() => {});
});
return;
}
if (!app.connectWithIdentity) {
this.connectGoEasy(targetUserId);
return;
}
app.connectWithIdentity(role, targetUserId, true).then(onReady).catch(() => {
const s = wx.goEasy?.getConnectionStatus?.();
if (s === 'connected' || s === 'reconnected') onReady();
});
},
connectGoEasy(userId) {
@@ -168,12 +185,27 @@ Page({
}
let list = content.conversations;
const defaultAvatar = app.globalData.ossImageUrl + app.globalData.morentouxiang;
const myGoEasyId = wx.goEasy?.im?.userId || '';
const ossBase = app.globalData.ossImageUrl || '';
const groupMeta = app.globalData.groupInfoMap || {};
list.forEach(item => {
if (item.data && item.data.avatar) {
if (!item.data.avatar.startsWith('http')) {
item.data.avatar = app.globalData.ossImageUrl + item.data.avatar;
if (item.type === 'group') {
const meta = groupMeta[item.groupId];
if (meta) {
if (!item.data) item.data = {};
if (meta.name) item.data.name = meta.name;
if (meta.avatar) item.data.avatar = meta.avatar;
if (meta.orderDesc) item.data.orderDesc = meta.orderDesc;
if (meta.orderId) item.data.orderId = meta.orderId;
if (meta.orderZhuangtai != null) item.data.orderZhuangtai = meta.orderZhuangtai;
}
} else {
enrichGroupConversation(item, myGoEasyId, defaultAvatar, ossBase);
} else if (item.data && item.data.avatar) {
if (!item.data.avatar.startsWith('http')) {
item.data.avatar = ossBase + item.data.avatar;
}
} else if (item.data) {
item.data.avatar = defaultAvatar;
}
if (item.lastMessage && item.lastMessage.timestamp) {
@@ -189,7 +221,7 @@ Page({
return (b.lastMessage.timestamp || 0) - (a.lastMessage.timestamp || 0);
});
this.setData({ allConversations: list, displayCount: 10 }, () => {
this.setData({ allConversations: list, displayCount: 4 }, () => {
this.applyFilter();
});
@@ -234,7 +266,7 @@ Page({
const { allConversations, activeTab, searchKeyword } = this.data;
let filtered = [];
if (activeTab === 0) {
filtered = allConversations.filter(c => c.type === 'group' && c.data && c.data.orderId);
filtered = allConversations.filter(c => isOrderGroupConversation(c));
} else if (activeTab === 1) {
filtered = allConversations.filter(c => c.type === 'private');
} else if (activeTab === 2) {
@@ -253,7 +285,7 @@ Page({
const tabUnread = [0, 0, 0];
allConversations.forEach(c => {
if (c.type === 'group' && c.data && c.data.orderId) tabUnread[0] += c.unread || 0;
if (isOrderGroupConversation(c)) tabUnread[0] += c.unread || 0;
else if (c.type === 'private') tabUnread[1] += c.unread || 0;
else if (c.type === 'cs') tabUnread[2] += c.unread || 0;
});
@@ -263,17 +295,17 @@ Page({
switchTab(e) {
const index = parseInt(e.currentTarget.dataset.index);
this.setData({ activeTab: index, displayCount: 10, searchKeyword: '' }, () => this.applyFilter());
this.setData({ activeTab: index, displayCount: 4, searchKeyword: '' }, () => this.applyFilter());
},
onSearchInput(e) {
this.setData({ searchKeyword: e.detail.value, displayCount: 10 }, () => this.applyFilter());
this.setData({ searchKeyword: e.detail.value, displayCount: 4 }, () => this.applyFilter());
},
onScrollToLower() {
const { displayCount, filteredConversations } = this.data;
if (displayCount < filteredConversations.length) {
this.setData({ displayCount: Math.min(displayCount + 10, filteredConversations.length) });
this.setData({ displayCount: Math.min(displayCount + 4, filteredConversations.length) });
}
},

View File

@@ -29,14 +29,25 @@
<!-- 会话列表 (上拉加载更多) -->
<scroll-view class="conversation-list" scroll-y="true" bindscrolltolower="onScrollToLower" style="height: {{scrollHeight}}px">
<block wx:for="{{filteredConversations}}" wx:key="key" wx:for-index="i" wx:if="{{i < displayCount}}">
<view class="conversation-item {{item.unread > 0 ? 'has-unread' : ''}}" data-conversation="{{item}}" bindtap="chat" bindlongpress="showAction">
<image class="avatar" src="{{item.data.avatar}}" mode="aspectFill" />
<view class="conversation-item {{item.unread > 0 ? 'has-unread' : ''}} {{item.type === 'group' ? 'order-group-item' : ''}}" data-conversation="{{item}}" bindtap="chat" bindlongpress="showAction">
<image class="avatar {{item.type === 'group' ? 'avatar-round' : ''}}" src="{{item.data.avatar}}" mode="aspectFill" />
<view class="info">
<view class="top-line">
<text class="name">{{item.data.name}}</text>
<text class="time">{{item.lastMessage.date}}</text>
</view>
<view class="bottom-line">
<block wx:if="{{item.type === 'group'}}">
<view class="order-block" wx:if="{{item.data.orderId || item.data.orderDesc}}">
<text class="order-id" wx:if="{{item.data.orderId}}">单号 {{item.data.orderId}}</text>
<text class="order-desc" wx:if="{{item.data.orderDesc}}">{{item.data.orderDesc}}</text>
<text class="order-status" wx:if="{{item.data.orderZhuangtaiText}}">{{item.data.orderZhuangtaiText}}</text>
</view>
<view class="bottom-line">
<text class="last-msg">{{item.displayLastMsg || item.lastMessage.payload.text}}</text>
<view class="unread-dot" wx:if="{{item.unread > 0}}">{{item.unread > 99 ? '99+' : item.unread}}</view>
</view>
</block>
<view class="bottom-line" wx:else>
<text class="last-msg">{{item.lastMessage.type === 'text' ? item.lastMessage.payload.text : '[其他消息]'}}</text>
<view class="unread-dot" wx:if="{{item.unread > 0}}">{{item.unread > 99 ? '99+' : item.unread}}</view>
</view>

View File

@@ -14,13 +14,19 @@
.tab-badge { background: #fa5151; color: #fff; font-size: 20rpx; border-radius: 20rpx; padding: 2rpx 12rpx; margin-left: 8rpx; min-width: 32rpx; text-align: center; }
.conversation-list { background: #fff; }
.conversation-item { display: flex; align-items: center; padding: 20rpx 24rpx; border-bottom: 1rpx solid #f0f0f0; }
.conversation-item { display: flex; align-items: flex-start; padding: 24rpx 24rpx; border-bottom: 1rpx solid #f0f0f0; }
.order-group-item { min-height: 160rpx; padding: 28rpx 24rpx; }
.has-unread { background: #fafafa; }
.avatar { width: 96rpx; height: 96rpx; border-radius: 12rpx; margin-right: 20rpx; background: #eee; }
.info { flex: 1; }
.avatar { width: 96rpx; height: 96rpx; border-radius: 12rpx; margin-right: 20rpx; background: #eee; flex-shrink: 0; }
.avatar-round { border-radius: 50%; }
.info { flex: 1; min-width: 0; }
.top-line { display: flex; justify-content: space-between; margin-bottom: 8rpx; }
.name { font-size: 30rpx; color: #1a1a1a; font-weight: 500; }
.time { font-size: 22rpx; color: #999; }
.time { font-size: 22rpx; color: #999; flex-shrink: 0; margin-left: 12rpx; }
.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; }
.bottom-line { display: flex; justify-content: space-between; align-items: center; }
.last-msg { font-size: 26rpx; color: #888; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.unread-dot { background: #fa5151; color: #fff; font-size: 20rpx; border-radius: 50%; min-width: 36rpx; height: 36rpx; line-height: 36rpx; text-align: center; margin-left: 12rpx; }