修复聊天头像与列表:配对群元数据、排序与连接稳定(miniprogram目录)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
511
miniprogram/pages/qunliaotian/qunliaotian.js
Normal file
511
miniprogram/pages/qunliaotian/qunliaotian.js
Normal file
@@ -0,0 +1,511 @@
|
||||
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';
|
||||
import { getFreshImUser } from '../../utils/im-user.js';
|
||||
|
||||
Page({
|
||||
data: {
|
||||
groupId: '',
|
||||
groupName: '',
|
||||
groupAvatar: '',
|
||||
orderId: '',
|
||||
isCross: 0,
|
||||
currentUser: null,
|
||||
messages: [],
|
||||
inputText: '',
|
||||
showEmojiPanel: false,
|
||||
showPlusPanel: false,
|
||||
showOrderSender: false,
|
||||
loadingHistory: false,
|
||||
isRefreshing: false,
|
||||
lastTimestamp: null,
|
||||
hasMore: true,
|
||||
scrollToView: '',
|
||||
detailText: '',
|
||||
showDetailModal: false,
|
||||
lastTapTime: 0,
|
||||
lastTapMsgId: '',
|
||||
emojiList: ['😀','😁','😂','🤣','😃','😄','😅','😆','😉','😊','😋','😎','😍','😘','😗','😙','😚','🙂','🤗','🤩','🤔','🤨','😐','😑','😶','🙄','😏','😣','😥','😮','🤐','😯','😪','😫','😴','😌','😛','😜','😝','🤤','😒','😓','😔','😕','🙃','🤑','😲','☹','🙁','😖','😞','😟','😤','😢','😭','😦','😧','😨','😩','🤯','😬','😰','😱','🥵','🥶','😳','🤪','😵','😡','😠','🤬','😷','🤒','🤕','🤢','🤮','🤧','😇','🤠','🤡','🤥','🤫','🤭','🧐','🤓','😈','👿','👹','👺','💀','👻','👽','🤖','💩','😺','😸','😹','😻','😼','😽','🙀','😿','😾'],
|
||||
pendingImage: '',
|
||||
keyboardHeight: 0,
|
||||
bottomSafeHeight: 10
|
||||
},
|
||||
onLoad(options) {
|
||||
let p = { groupId: '', groupName: '订单群聊', groupAvatar: '', orderId: '', isCross: 0,
|
||||
currentUserId: '', currentUserName: '', currentUserAvatar: '' };
|
||||
if (options.data) {
|
||||
try {
|
||||
const d = JSON.parse(decodeURIComponent(options.data));
|
||||
p = { ...p, ...d };
|
||||
} catch (e) {}
|
||||
}
|
||||
this.setData({
|
||||
groupId: p.groupId,
|
||||
groupName: p.groupName,
|
||||
groupAvatar: p.groupAvatar,
|
||||
orderId: p.orderId,
|
||||
isCross: p.isCross
|
||||
});
|
||||
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.initCurrentUser();
|
||||
}
|
||||
},
|
||||
|
||||
/*onLoad(options) {
|
||||
if (options.data) {
|
||||
try {
|
||||
const p = JSON.parse(decodeURIComponent(options.data));
|
||||
this.setData({
|
||||
groupId: p.groupId || '',
|
||||
groupName: p.groupName || '订单群聊',
|
||||
groupAvatar: p.groupAvatar || '',
|
||||
orderId: p.orderId || '',
|
||||
isCross: p.isCross || 0
|
||||
});
|
||||
} catch (e) {}
|
||||
}
|
||||
wx.setNavigationBarTitle({ title: this.data.groupName });
|
||||
this.initCurrentUser();
|
||||
},*/
|
||||
|
||||
onShow() {
|
||||
this.ensureChatConnection();
|
||||
},
|
||||
|
||||
onHide() {
|
||||
this.clearPageListeners();
|
||||
},
|
||||
|
||||
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 targetUserId = (prefixMap[role] || 'Boss') + uid;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
const { currentUser } = this.data;
|
||||
if (!currentUser) return;
|
||||
wx.goEasy.connect({
|
||||
id: userId,
|
||||
data: {
|
||||
name: currentUser.name,
|
||||
avatar: currentUser.avatar || (app.globalData.ossImageUrl + app.globalData.morentouxiang)
|
||||
},
|
||||
onSuccess: () => {
|
||||
this.subscribeGroupIfNeeded();
|
||||
this.setupAllListeners();
|
||||
this.loadHistory(true).then(() => this.markGroupMessageAsRead());
|
||||
},
|
||||
onFailed: (error) => {
|
||||
if (error.code === 408) {
|
||||
this.subscribeGroupIfNeeded();
|
||||
this.setupAllListeners();
|
||||
this.loadHistory(true);
|
||||
} else {
|
||||
wx.showToast({ title: '连接失败,请重试', icon: 'none' });
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
subscribeGroupIfNeeded() {
|
||||
const { groupId } = this.data;
|
||||
if (!groupId) return;
|
||||
wx.goEasy.im.subscribeGroup({
|
||||
groupIds: [groupId],
|
||||
onSuccess: () => console.log(`[群聊页] 订阅成功: ${groupId}`),
|
||||
onFailed: (err) => console.error(`[群聊页] 订阅失败: ${groupId}`, err)
|
||||
});
|
||||
},
|
||||
|
||||
setupAllListeners() {
|
||||
this.clearAllListeners();
|
||||
this.listenNewMsg();
|
||||
this.listenRecall();
|
||||
this.listenDelete();
|
||||
},
|
||||
|
||||
clearAllListeners() {
|
||||
if (this._msgHandler) { wx.goEasy.im.off(wx.GoEasy.IM_EVENT.GROUP_MESSAGE_RECEIVED, this._msgHandler); this._msgHandler = null; }
|
||||
if (this._recallHandler) { wx.goEasy.im.off(wx.GoEasy.IM_EVENT.MESSAGE_RECALLED, this._recallHandler); this._recallHandler = null; }
|
||||
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';
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
if (this.data.loadingHistory) return;
|
||||
this.setData({ isRefreshing: true });
|
||||
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: '' };
|
||||
});
|
||||
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(); }
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
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;
|
||||
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;
|
||||
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(); // ← 收到新消息就标记已读
|
||||
},
|
||||
|
||||
markGroupMessageAsRead() {
|
||||
if (!this.data.groupId) return;
|
||||
wx.goEasy.im.markMessageAsRead({
|
||||
type: wx.GoEasy.IM_SCENE.GROUP,
|
||||
id: this.data.groupId
|
||||
});
|
||||
},
|
||||
|
||||
listenRecall() {
|
||||
if (this._recallHandler) wx.goEasy.im.off(wx.GoEasy.IM_EVENT.MESSAGE_RECALLED, this._recallHandler);
|
||||
this._recallHandler = (msgs) => {
|
||||
if (!msgs || !msgs.length) return;
|
||||
const ids = new Set(msgs.map(m => m.messageId));
|
||||
this.setData({ messages: this.data.messages.filter(m => !ids.has(m.messageId)) });
|
||||
};
|
||||
wx.goEasy.im.on(wx.GoEasy.IM_EVENT.MESSAGE_RECALLED, this._recallHandler);
|
||||
},
|
||||
|
||||
listenDelete() {
|
||||
if (this._deleteHandler) wx.goEasy.im.off(wx.GoEasy.IM_EVENT.MESSAGE_DELETED, this._deleteHandler);
|
||||
this._deleteHandler = (msgs) => {
|
||||
if (!msgs || !msgs.length) return;
|
||||
const ids = new Set(msgs.map(m => m.messageId));
|
||||
this.setData({ messages: this.data.messages.filter(m => !ids.has(m.messageId)) });
|
||||
};
|
||||
wx.goEasy.im.on(wx.GoEasy.IM_EVENT.MESSAGE_DELETED, this._deleteHandler);
|
||||
},
|
||||
|
||||
onInput(e) { this.setData({ inputText: e.detail.value }); },
|
||||
|
||||
chooseImage() {
|
||||
const that = this;
|
||||
wx.chooseImage({
|
||||
count: 1, sizeType: ['compressed'], sourceType: ['album', 'camera'],
|
||||
success(res) { that.setData({ pendingImage: res.tempFilePaths[0] }); }
|
||||
});
|
||||
},
|
||||
|
||||
clearPendingImage() { this.setData({ pendingImage: '' }); },
|
||||
|
||||
sendMessage() {
|
||||
if (this.data.pendingImage) {
|
||||
const file = this.data.pendingImage;
|
||||
showConfirmByScene('chat_image_confirm', () => {
|
||||
this.sendImageMsg(file);
|
||||
this.setData({ pendingImage: '' });
|
||||
});
|
||||
return;
|
||||
}
|
||||
const text = this.data.inputText.trim();
|
||||
if (!text) return;
|
||||
this.sendText(text);
|
||||
this.setData({ inputText: '' });
|
||||
},
|
||||
|
||||
sendText(text) {
|
||||
const that = this;
|
||||
sendGroupMessage({
|
||||
msgData: {
|
||||
type: 'text',
|
||||
text,
|
||||
currentUser: that.data.currentUser,
|
||||
groupId: that.data.groupId,
|
||||
orderId: that.data.orderId,
|
||||
isCross: that.data.isCross,
|
||||
groupName: that.data.groupName,
|
||||
groupAvatar: that.data.groupAvatar
|
||||
},
|
||||
onSendStart: (localMsg) => {
|
||||
const msgs = that.data.messages;
|
||||
if (msgs.length) {
|
||||
localMsg.showTime = (localMsg.timestamp - msgs[msgs.length-1].timestamp) / 60000 > 5;
|
||||
} else {
|
||||
localMsg.showTime = true;
|
||||
}
|
||||
that.setData({ messages: [...msgs, localMsg], scrollToView: 'msg-bottom' });
|
||||
},
|
||||
onSuccess: (messageId, status) => {
|
||||
that.updateMsg(messageId, status);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
sendImageMsg(file) {
|
||||
const that = this;
|
||||
sendGroupMessage({
|
||||
msgData: {
|
||||
type: 'image',
|
||||
filePath: file,
|
||||
currentUser: that.data.currentUser,
|
||||
groupId: that.data.groupId,
|
||||
orderId: that.data.orderId,
|
||||
isCross: that.data.isCross,
|
||||
groupName: that.data.groupName,
|
||||
groupAvatar: that.data.groupAvatar
|
||||
},
|
||||
onSendStart: (localMsg) => {
|
||||
const msgs = that.data.messages;
|
||||
if (msgs.length) {
|
||||
localMsg.showTime = (localMsg.timestamp - msgs[msgs.length-1].timestamp) / 60000 > 5;
|
||||
} else {
|
||||
localMsg.showTime = true;
|
||||
}
|
||||
that.setData({ messages: [...msgs, localMsg], scrollToView: 'msg-bottom' });
|
||||
},
|
||||
onSuccess: (messageId, status) => {
|
||||
that.updateMsg(messageId, status);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
updateMsg(id, status) {
|
||||
this.setData({ messages: this.data.messages.map(m => m.messageId === id ? { ...m, status } : m) });
|
||||
},
|
||||
|
||||
needShow(ts) {
|
||||
const ms = this.data.messages;
|
||||
if (!ms.length) return true;
|
||||
const last = ms[ms.length-1];
|
||||
return (ts - last.timestamp) / 60000 > 5;
|
||||
},
|
||||
|
||||
onBubbleTap(e) {
|
||||
const msgId = e.currentTarget.dataset.messageid;
|
||||
const text = e.currentTarget.dataset.text;
|
||||
const now = Date.now();
|
||||
if (msgId === this.data.lastTapMsgId && now - this.data.lastTapTime < 350) {
|
||||
this.setData({ detailText: text, showDetailModal: true, lastTapTime:0, lastTapMsgId:'' });
|
||||
} else {
|
||||
this.data.lastTapTime = now;
|
||||
this.data.lastTapMsgId = msgId;
|
||||
}
|
||||
},
|
||||
|
||||
hideDetail() { this.setData({ showDetailModal: false }); },
|
||||
copyDetail() { wx.setClipboardData({ data: this.data.detailText }); },
|
||||
|
||||
showAction(e) {
|
||||
const msgId = e.currentTarget.dataset.messageid;
|
||||
if (!msgId) return;
|
||||
const msg = this.data.messages.find(m => m.messageId === msgId);
|
||||
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('撤回');
|
||||
}
|
||||
itemList.push('删除');
|
||||
wx.showActionSheet({
|
||||
itemList,
|
||||
success: (res) => {
|
||||
if (itemList[res.tapIndex] === '复制') {
|
||||
if (msg.type === 'text') wx.setClipboardData({ data: msg.payload.text });
|
||||
else if (msg.type === 'order') wx.setClipboardData({ data: JSON.stringify(msg.payload) });
|
||||
} else if (itemList[res.tapIndex] === '删除') {
|
||||
this.deleteMsg(msg);
|
||||
} else if (itemList[res.tapIndex] === '撤回') {
|
||||
this.recallMsg(msg);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
deleteMsg(msg) {
|
||||
wx.showModal({
|
||||
title: '提示', content: '仅自己看不到',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
wx.goEasy.im.deleteMessage({
|
||||
messages: [msg],
|
||||
onSuccess: () => { this.setData({ messages: this.data.messages.filter(m => m.messageId !== msg.messageId) }); },
|
||||
onFailed: () => { this.setData({ messages: this.data.messages.filter(m => m.messageId !== msg.messageId) }); }
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
recallMsg(msg) {
|
||||
wx.showModal({
|
||||
title: '提示', content: '撤回后群内所有人都看不到该消息',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.setData({ messages: this.data.messages.filter(m => m.messageId !== msg.messageId) });
|
||||
wx.goEasy.im.recallMessage({ messages: [msg], onFailed:()=>{} });
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
previewImage(e) { wx.previewImage({ urls: [e.currentTarget.dataset.url], current: e.currentTarget.dataset.url }); },
|
||||
|
||||
togglePlusPanel() { this.setData({ showPlusPanel: !this.data.showPlusPanel, showEmojiPanel: false }); },
|
||||
closePlusPanel() { this.setData({ showPlusPanel: false }); },
|
||||
|
||||
openEmojiFromPlus() { this.setData({ showPlusPanel: false, showEmojiPanel: true }); },
|
||||
toggleEmoji() { this.setData({ showEmojiPanel: !this.data.showEmojiPanel }); },
|
||||
insertEmoji(e) { this.setData({ inputText: this.data.inputText + e.currentTarget.dataset.emoji }); },
|
||||
closeEmojiPanel() { this.setData({ showEmojiPanel: false }); },
|
||||
|
||||
openOrderSender() { this.setData({ showOrderSender: true, showPlusPanel: false }); },
|
||||
closeOrderSender() { this.setData({ showOrderSender: false }); },
|
||||
|
||||
onSendOrder(e) {
|
||||
const order = e.detail.order;
|
||||
if (!order) return;
|
||||
const that = this;
|
||||
const payload = {
|
||||
orderId: order.dingdan_id,
|
||||
jieshao: order.jieshao,
|
||||
jine: order.jine,
|
||||
beizhu: order.beizhu
|
||||
};
|
||||
sendGroupMessage({
|
||||
msgData: {
|
||||
type: 'order',
|
||||
orderPayload: payload,
|
||||
currentUser: that.data.currentUser,
|
||||
groupId: that.data.groupId,
|
||||
orderId: that.data.orderId,
|
||||
isCross: that.data.isCross,
|
||||
groupName: that.data.groupName,
|
||||
groupAvatar: that.data.groupAvatar
|
||||
},
|
||||
onSendStart: (localMsg) => {
|
||||
const msgs = that.data.messages;
|
||||
if (msgs.length) {
|
||||
localMsg.showTime = (localMsg.timestamp - msgs[msgs.length-1].timestamp) / 60000 > 5;
|
||||
} else {
|
||||
localMsg.showTime = true;
|
||||
}
|
||||
that.setData({ messages: [...msgs, localMsg], scrollToView: 'msg-bottom' });
|
||||
},
|
||||
onSuccess: (messageId, status) => {
|
||||
that.updateMsg(messageId, status);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
viewOrderDetail(e) {
|
||||
const payload = e.currentTarget.dataset.payload;
|
||||
if (!payload) return;
|
||||
const text = `订单ID: ${payload.orderId}\n内容: ${payload.jieshao}\n金额: ¥${payload.jine}\n备注: ${payload.beizhu}`;
|
||||
this.setData({ detailText: text, showDetailModal: true });
|
||||
},
|
||||
|
||||
onKeyboardHeightChange(e) { this.setData({ keyboardHeight: e.detail.height }); }
|
||||
});
|
||||
436
miniprogram/pages/xiaoxi/xiaoxi.js
Normal file
436
miniprogram/pages/xiaoxi/xiaoxi.js
Normal file
@@ -0,0 +1,436 @@
|
||||
// pages/xiaoxi/xiaoxi.js
|
||||
const app = getApp();
|
||||
import { formatDate } from '../../static/lib/utils';
|
||||
const { jianquanxian } = require('../../utils/imAuth/jianquanxian');
|
||||
import { enrichGroupConversation, isOrderGroupConversation, sortConversations, recordConversationOpen } from '../../utils/group-chat.js';
|
||||
import { loadGroupMetaCache } from '../../utils/im-user.js';
|
||||
|
||||
Page({
|
||||
data: {
|
||||
allConversations: [],
|
||||
filteredConversations: [],
|
||||
activeTab: 0,
|
||||
tabUnread: [0, 0, 0],
|
||||
displayCount: 4,
|
||||
searchKeyword: '',
|
||||
actionPopup: { visible: false, conversation: null },
|
||||
currentUser: null,
|
||||
notificationMuted: false,
|
||||
scrollHeight: 0
|
||||
},
|
||||
|
||||
_permissionSeq: 0,
|
||||
|
||||
onLoad() {
|
||||
this.calculateScrollHeight();
|
||||
const muted = wx.getStorageSync('notificationMuted') || false;
|
||||
this.setData({ notificationMuted: muted });
|
||||
loadGroupMetaCache(app);
|
||||
this._onGlobalConvUpdated = (result) => {
|
||||
const content = result?.content || result;
|
||||
if (content && content.conversations) {
|
||||
this.renderConversations(content);
|
||||
}
|
||||
};
|
||||
app.on('conversationsUpdated', this._onGlobalConvUpdated);
|
||||
app.on('unreadCountChanged', this._onGlobalConvUpdated);
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
if (this._onGlobalConvUpdated) {
|
||||
app.off('conversationsUpdated', this._onGlobalConvUpdated);
|
||||
app.off('unreadCountChanged', this._onGlobalConvUpdated);
|
||||
}
|
||||
},
|
||||
|
||||
onShow() {
|
||||
if (!this.checkLoginStatus()) return;
|
||||
loadGroupMetaCache(app);
|
||||
if (app.globalData.messageManager?.unreadTotal != null) {
|
||||
app.emitEvent('tabBarBadgeChanged', {
|
||||
badgeText: app.globalData.messageManager.unreadTotal > 0
|
||||
? String(app.globalData.messageManager.unreadTotal) : ''
|
||||
});
|
||||
}
|
||||
this.checkPermissionAndAutoConnect();
|
||||
},
|
||||
|
||||
// ❌ 原版注释保留:删除所有清理逻辑,监听器永久存活
|
||||
onHide() {},
|
||||
onUnload() {},
|
||||
|
||||
// ========== 鉴权检查 ==========
|
||||
async checkPermissionAndAutoConnect() {
|
||||
const seq = ++this._permissionSeq;
|
||||
let quanxian;
|
||||
try {
|
||||
quanxian = await jianquanxian(app);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
if (seq !== this._permissionSeq) return;
|
||||
|
||||
if (!quanxian.allowed) {
|
||||
const currentRole = app.globalData.currentRole || 'normal';
|
||||
let content = '您没有权限使用消息功能';
|
||||
if (currentRole === 'dashou') content = '您需要先充值会员才能使用消息功能';
|
||||
wx.showModal({
|
||||
title: '权限不足',
|
||||
content,
|
||||
showCancel: false,
|
||||
confirmText: '我知道了'
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.autoConnect();
|
||||
},
|
||||
|
||||
// ========== 登录状态检查 ==========
|
||||
checkLoginStatus() {
|
||||
const uid = wx.getStorageSync('uid');
|
||||
if (!uid) {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' });
|
||||
setTimeout(() => { wx.redirectTo({ url: '/pages/wode/wode' }); }, 1500);
|
||||
return false;
|
||||
}
|
||||
let currentUser = app.globalData.currentUser;
|
||||
if (!currentUser) {
|
||||
currentUser = {
|
||||
id: uid,
|
||||
name: '用户' + uid.substring(0, 6),
|
||||
avatar: app.globalData.ossImageUrl + app.globalData.morentouxiang
|
||||
};
|
||||
app.globalData.currentUser = currentUser;
|
||||
}
|
||||
this.setData({ currentUser });
|
||||
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 targetUserId = (prefixMap[role] || 'Boss') + uid;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
const { currentUser } = this.data;
|
||||
if (!currentUser) return;
|
||||
wx.goEasy.connect({
|
||||
id: userId,
|
||||
data: {
|
||||
name: currentUser.name,
|
||||
avatar: currentUser.avatar || (app.globalData.ossImageUrl + app.globalData.morentouxiang)
|
||||
},
|
||||
onSuccess: () => {
|
||||
try {
|
||||
const connection = {
|
||||
userId: userId,
|
||||
identityType: app.globalData.currentRole,
|
||||
lastConnectTime: Date.now(),
|
||||
autoReconnect: true,
|
||||
status: 'connected'
|
||||
};
|
||||
wx.setStorageSync('savedGoEasyConnection', JSON.stringify(connection));
|
||||
wx.setStorageSync('goEasyUserId', userId);
|
||||
} catch (e) {}
|
||||
this.setupConversationListener();
|
||||
this.loadConversations();
|
||||
},
|
||||
onFailed: (error) => {
|
||||
if (error.code === 408) {
|
||||
this.setupConversationListener();
|
||||
this.loadConversations();
|
||||
} else {
|
||||
wx.showToast({ title: '连接失败,请重试', icon: 'none' });
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// ========== 会话监听(永不清理) ==========
|
||||
setupConversationListener() {
|
||||
if (this.conversationsUpdatedListener) {
|
||||
wx.goEasy.im.off(wx.GoEasy.IM_EVENT.CONVERSATIONS_UPDATED, this.conversationsUpdatedListener);
|
||||
}
|
||||
this.conversationsUpdatedListener = (result) => {
|
||||
this.renderConversations(result);
|
||||
};
|
||||
wx.goEasy.im.on(wx.GoEasy.IM_EVENT.CONVERSATIONS_UPDATED, this.conversationsUpdatedListener);
|
||||
},
|
||||
|
||||
loadConversations() {
|
||||
wx.goEasy.im.latestConversations({
|
||||
onSuccess: (result) => {
|
||||
this.renderConversations(result.content);
|
||||
},
|
||||
onFailed: () => {
|
||||
wx.showToast({ title: '获取会话失败', icon: 'none' });
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// ========== 会话渲染与排序 ==========
|
||||
renderConversations(content) {
|
||||
if (!content || !content.conversations) {
|
||||
this.setData({ allConversations: [] }, () => this.applyFilter());
|
||||
return;
|
||||
}
|
||||
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.type === 'group') {
|
||||
const meta = groupMeta[item.groupId];
|
||||
if (meta) {
|
||||
if (!item.data) item.data = {};
|
||||
Object.assign(item.data, meta);
|
||||
}
|
||||
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.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) {
|
||||
item.lastMessage.date = formatDate(item.lastMessage.timestamp);
|
||||
}
|
||||
});
|
||||
|
||||
const openTimes = wx.getStorageSync('conversationOpenTimes') || {};
|
||||
sortConversations(list, openTimes);
|
||||
|
||||
const unreadTotal = content.unreadTotal !== undefined ? content.unreadTotal : list.reduce((sum, c) => sum + (c.unread || 0), 0);
|
||||
if (app.globalData.messageManager) {
|
||||
app.globalData.messageManager.unreadTotal = unreadTotal;
|
||||
}
|
||||
if (app && app.emitEvent) {
|
||||
app.emitEvent('tabBarBadgeChanged', { badgeText: unreadTotal > 0 ? String(unreadTotal) : '' });
|
||||
app.emitEvent('unreadCountChanged', { unreadTotal });
|
||||
}
|
||||
|
||||
this.setData({ allConversations: list, displayCount: 4 }, () => {
|
||||
this.applyFilter();
|
||||
});
|
||||
|
||||
// 群组订阅(不重复断开连接)
|
||||
this.subscribeGroupsIfNeeded(list);
|
||||
},
|
||||
|
||||
/**
|
||||
* 群组订阅(依赖:GoEasy IM)
|
||||
* 根据官方文档,需要主动 subscribeGroup 才能接收群聊消息。
|
||||
* 为了应对切后台、网络波动等导致的订阅丢失,这里不再缓存订阅状态,
|
||||
* 每次获取到群组列表时都直接订阅,保证消息不丢失。
|
||||
*/
|
||||
subscribeGroupsIfNeeded(conversations) {
|
||||
if (!wx.goEasy?.im || typeof wx.goEasy.im.subscribeGroup !== 'function') return;
|
||||
|
||||
const groupIds = conversations
|
||||
.filter(c => c.type === 'group' && c.groupId)
|
||||
.map(c => c.groupId);
|
||||
|
||||
if (groupIds.length === 0) return;
|
||||
|
||||
// 不再判断缓存,直接订阅当前所有群组
|
||||
wx.goEasy.im.subscribeGroup({
|
||||
groupIds: groupIds,
|
||||
onSuccess: () => {
|
||||
// 可选的日志输出,不影响功能
|
||||
// console.log('群组订阅成功', groupIds);
|
||||
},
|
||||
onFailed: (error) => {
|
||||
console.error('群组订阅失败:', error);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
applyFilter() {
|
||||
const { allConversations, activeTab, searchKeyword } = this.data;
|
||||
let filtered = [];
|
||||
if (activeTab === 0) {
|
||||
filtered = allConversations.filter(c => isOrderGroupConversation(c));
|
||||
} else if (activeTab === 1) {
|
||||
filtered = allConversations.filter(c => c.type === 'private');
|
||||
} else if (activeTab === 2) {
|
||||
filtered = allConversations.filter(c => c.type === 'cs');
|
||||
}
|
||||
|
||||
if (searchKeyword.trim()) {
|
||||
const kw = searchKeyword.trim().toLowerCase();
|
||||
filtered = filtered.filter(c => {
|
||||
const name = (c.data && c.data.name || '').toLowerCase();
|
||||
const orderId = (c.data && c.data.orderId || '').toLowerCase();
|
||||
const id = (c.userId || c.groupId || '').toLowerCase();
|
||||
return name.includes(kw) || id.includes(kw) || orderId.includes(kw);
|
||||
});
|
||||
}
|
||||
|
||||
const tabUnread = [0, 0, 0];
|
||||
allConversations.forEach(c => {
|
||||
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;
|
||||
});
|
||||
|
||||
this.setData({ filteredConversations: filtered, tabUnread });
|
||||
},
|
||||
|
||||
switchTab(e) {
|
||||
const index = parseInt(e.currentTarget.dataset.index);
|
||||
this.setData({ activeTab: index, displayCount: 4, searchKeyword: '' }, () => this.applyFilter());
|
||||
},
|
||||
|
||||
onSearchInput(e) {
|
||||
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 + 4, filteredConversations.length) });
|
||||
}
|
||||
},
|
||||
|
||||
onRefresh() {
|
||||
this.loadConversations();
|
||||
},
|
||||
|
||||
chat(e) {
|
||||
const conversation = e.currentTarget.dataset.conversation;
|
||||
if (!conversation) return;
|
||||
recordConversationOpen(conversation);
|
||||
|
||||
if (conversation.type === 'private') {
|
||||
const param = {
|
||||
toUserId: conversation.userId,
|
||||
toName: conversation.data.name,
|
||||
toAvatar: conversation.data.avatar
|
||||
};
|
||||
wx.navigateTo({ url: '/pages/liaotian/liaotian?data=' + encodeURIComponent(JSON.stringify(param)) });
|
||||
} else if (conversation.type === 'group') {
|
||||
const param = {
|
||||
groupId: conversation.groupId,
|
||||
orderId: conversation.data.orderId || '',
|
||||
groupName: conversation.data.name,
|
||||
groupAvatar: conversation.data.avatar,
|
||||
isCross: conversation.data.isCross || 0
|
||||
};
|
||||
wx.navigateTo({ url: '/pages/qunliaotian/qunliaotian?data=' + encodeURIComponent(JSON.stringify(param)) });
|
||||
} else if (conversation.type === 'cs') {
|
||||
const param = { teamId: conversation.teamId || conversation.id };
|
||||
wx.navigateTo({ url: '/pages/kefuliaotian/kefuliaotian?data=' + encodeURIComponent(JSON.stringify(param)) });
|
||||
}
|
||||
},
|
||||
|
||||
chatKefu() {
|
||||
const param = { teamId: 'support_team' };
|
||||
wx.navigateTo({ url: '/pages/kefuliaotian/kefuliaotian?data=' + encodeURIComponent(JSON.stringify(param)) });
|
||||
},
|
||||
|
||||
showAction(e) {
|
||||
const conversation = e.currentTarget.dataset.conversation;
|
||||
this.setData({ ['actionPopup.conversation']: conversation, ['actionPopup.visible']: true });
|
||||
},
|
||||
closeMask() {
|
||||
this.setData({ ['actionPopup.visible']: false });
|
||||
},
|
||||
topConversation() {
|
||||
let conversation = this.data.actionPopup.conversation;
|
||||
if (!conversation) return;
|
||||
const top = !conversation.top;
|
||||
wx.showLoading({ title: '处理中...' });
|
||||
if (conversation.type === 'private') {
|
||||
wx.goEasy.im.topPrivateConversation({
|
||||
userId: conversation.userId, top,
|
||||
onSuccess: () => {
|
||||
wx.hideLoading(); this.loadConversations();
|
||||
wx.showToast({ title: top ? '已置顶' : '已取消置顶' });
|
||||
},
|
||||
onFailed: () => { wx.hideLoading(); wx.showToast({ title: '操作失败', icon: 'none' }); }
|
||||
});
|
||||
} else {
|
||||
wx.goEasy.im.topGroupConversation({
|
||||
groupId: conversation.groupId, top,
|
||||
onSuccess: () => {
|
||||
wx.hideLoading(); this.loadConversations();
|
||||
wx.showToast({ title: top ? '已置顶' : '已取消置顶' });
|
||||
},
|
||||
onFailed: () => { wx.hideLoading(); wx.showToast({ title: '操作失败', icon: 'none' }); }
|
||||
});
|
||||
}
|
||||
this.closeMask();
|
||||
},
|
||||
removeConversation() {
|
||||
let conversation = this.data.actionPopup.conversation;
|
||||
if (!conversation) return;
|
||||
wx.showLoading({ title: '删除中...' });
|
||||
wx.goEasy.im.removeConversation({
|
||||
conversation: conversation,
|
||||
onSuccess: () => { wx.hideLoading(); this.loadConversations(); wx.showToast({ title: '已删除' }); },
|
||||
onFailed: () => { wx.hideLoading(); wx.showToast({ title: '删除失败', icon: 'none' }); }
|
||||
});
|
||||
this.closeMask();
|
||||
},
|
||||
|
||||
toggleNotification() {
|
||||
const newMuted = !this.data.notificationMuted;
|
||||
this.setData({ notificationMuted: newMuted });
|
||||
wx.setStorageSync('notificationMuted', newMuted);
|
||||
wx.showToast({ title: newMuted ? '已静音' : '已开启通知', icon: 'none' });
|
||||
},
|
||||
|
||||
calculateScrollHeight() {
|
||||
const systemInfo = wx.getSystemInfoSync();
|
||||
const windowHeight = systemInfo.windowHeight;
|
||||
const windowWidth = systemInfo.windowWidth;
|
||||
const rpxRatio = 750 / windowWidth;
|
||||
const headerHeight = 150 / rpxRatio;
|
||||
const tabBarHeight = 130 / rpxRatio;
|
||||
const safeAreaBottom = systemInfo.safeArea ? (windowHeight - systemInfo.safeArea.bottom) : 0;
|
||||
this.setData({ scrollHeight: windowHeight - headerHeight - tabBarHeight - safeAreaBottom });
|
||||
}
|
||||
});
|
||||
88
miniprogram/pages/xiaoxi/xiaoxi.wxml
Normal file
88
miniprogram/pages/xiaoxi/xiaoxi.wxml
Normal file
@@ -0,0 +1,88 @@
|
||||
<view class="msg-page">
|
||||
<!-- 顶部搜索 + 通知 -->
|
||||
<view class="header">
|
||||
<view class="search-bar">
|
||||
<input class="search-input" placeholder="搜索订单/用户ID/昵称" bindinput="onSearchInput" value="{{searchKeyword}}" />
|
||||
<text class="search-icon">🔍</text>
|
||||
</view>
|
||||
<view class="notify-switch" bindtap="toggleNotification">
|
||||
<text class="notify-text">{{notificationMuted ? '静音' : '通知'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 标签栏 -->
|
||||
<view class="tab-row">
|
||||
<view class="tab-item {{activeTab === 0 ? 'active' : ''}}" data-index="0" bindtap="switchTab">
|
||||
<text class="tab-label">订单消息</text>
|
||||
<text class="tab-badge" wx:if="{{tabUnread[0] > 0}}">{{tabUnread[0]}}</text>
|
||||
</view>
|
||||
<view class="tab-item {{activeTab === 1 ? 'active' : ''}}" data-index="1" bindtap="switchTab">
|
||||
<text class="tab-label">用户消息</text>
|
||||
<text class="tab-badge" wx:if="{{tabUnread[1] > 0}}">{{tabUnread[1]}}</text>
|
||||
</view>
|
||||
<view class="tab-item {{activeTab === 2 ? 'active' : ''}}" data-index="2" bindtap="switchTab">
|
||||
<text class="tab-label">客服消息</text>
|
||||
<text class="tab-badge" wx:if="{{tabUnread[2] > 0}}">{{tabUnread[2]}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 会话列表 (上拉加载更多) -->
|
||||
<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' : ''}} {{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>
|
||||
<block wx:if="{{item.type === 'group'}}">
|
||||
<view class="user-meta-row" wx:if="{{item.data.counterpartYonghuid}}">
|
||||
<text class="user-meta">对方 {{item.data.counterpartYonghuid}} {{item.data.name}}</text>
|
||||
</view>
|
||||
<view class="user-meta-row" wx:if="{{item.data.dashouYonghuid}}">
|
||||
<text class="user-meta sub">打手 {{item.data.dashouYonghuid}} {{item.data.dashouNicheng}}</text>
|
||||
</view>
|
||||
<view class="user-meta-row" wx:if="{{item.data.partnerYonghuid}}">
|
||||
<text class="user-meta sub">{{item.data.partnerRoleLabel || '商家'}} {{item.data.partnerYonghuid}} {{item.data.partnerNicheng}}</text>
|
||||
</view>
|
||||
<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>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 客服标签页空状态时显示联系客服按钮 -->
|
||||
<view class="kefu-entry" wx:if="{{activeTab === 2 && filteredConversations.length === 0}}">
|
||||
<button class="kefu-btn" bindtap="chatKefu">联系客服</button>
|
||||
</view>
|
||||
<view class="load-tip" wx:if="{{filteredConversations.length > displayCount}}">上拉加载更多</view>
|
||||
<view class="empty" wx:if="{{filteredConversations.length === 0 && activeTab !== 2}}">
|
||||
<text>暂无相关消息</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 操作菜单 -->
|
||||
<view class="action-mask" wx:if="{{actionPopup.visible}}" catchtap="closeMask">
|
||||
<view class="action-sheet">
|
||||
<view class="action-btn" bindtap="topConversation">{{actionPopup.conversation.top ? '取消置顶' : '置顶聊天'}}</view>
|
||||
<view class="action-btn" bindtap="removeConversation">删除聊天</view>
|
||||
<view class="action-btn cancel" catchtap="closeMask">取消</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部自定义TabBar -->
|
||||
<custom-tab-bar />
|
||||
</view>
|
||||
47
miniprogram/pages/xiaoxi/xiaoxi.wxss
Normal file
47
miniprogram/pages/xiaoxi/xiaoxi.wxss
Normal file
@@ -0,0 +1,47 @@
|
||||
.msg-page { background: #f5f5f5; min-height: 100vh; }
|
||||
|
||||
.header { display: flex; align-items: center; padding: 20rpx 24rpx; background: #fff; border-bottom: 1rpx solid #e8e8e8; }
|
||||
.search-bar { flex: 1; display: flex; align-items: center; background: #f0f0f0; border-radius: 40rpx; padding: 0 20rpx; height: 68rpx; }
|
||||
.search-input { flex: 1; height: 68rpx; font-size: 28rpx; }
|
||||
.search-icon { font-size: 32rpx; margin-left: 10rpx; color: #999; }
|
||||
.notify-switch { margin-left: 20rpx; }
|
||||
.notify-text { font-size: 26rpx; color: #666; padding: 10rpx 16rpx; background: #f0f0f0; border-radius: 30rpx; }
|
||||
|
||||
.tab-row { display: flex; background: #fff; border-bottom: 1rpx solid #e8e8e8; }
|
||||
.tab-item { flex: 1; display: flex; justify-content: center; align-items: center; padding: 24rpx 0; position: relative; color: #666; font-size: 28rpx; }
|
||||
.tab-item.active { color: #07c160; font-weight: 600; }
|
||||
.tab-item.active::after { content: ''; position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 60rpx; height: 6rpx; background: #07c160; border-radius: 3rpx; }
|
||||
.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: 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; 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; flex-shrink: 0; margin-left: 12rpx; }
|
||||
.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; }
|
||||
.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; }
|
||||
.load-tip { text-align: center; padding: 24rpx; color: #999; font-size: 24rpx; }
|
||||
.empty { text-align: center; padding: 200rpx 0; color: #bbb; font-size: 28rpx; }
|
||||
|
||||
/* 客服入口 */
|
||||
.kefu-entry { text-align: center; padding: 100rpx 0; }
|
||||
.kefu-btn { width: 300rpx; background: #07c160; color: #fff; border-radius: 40rpx; font-size: 28rpx; }
|
||||
|
||||
/* 操作弹窗 */
|
||||
.action-mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.4); z-index: 10000; }
|
||||
.action-sheet { position: absolute; bottom: 0; left: 0; right: 0; background: #fff; border-radius: 24rpx 24rpx 0 0; padding: 20rpx 20rpx 150rpx 20rpx; }
|
||||
.action-btn { text-align: center; padding: 28rpx 0; font-size: 32rpx; border-bottom: 1rpx solid #eee; }
|
||||
.cancel { color: #999; margin-top: 12rpx; border-bottom: none; }
|
||||
Reference in New Issue
Block a user