chore: 抢单端UI回退前备份当前完整工作区

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-06-29 17:59:23 +08:00
parent 6e2f7bc39f
commit e8fb32c1fe
53 changed files with 3410 additions and 1065 deletions

35
app.js
View File

@@ -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 | minionLaunch 重置为 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,
};
}
},

View File

@@ -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考核称号等特殊形状

View File

@@ -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();

View File

@@ -15,14 +15,15 @@
<!-- 通知内容 -->
<view class="notification-content">
<!-- 左侧头像 -->
<image class="notification-avatar" src="{{avatar}}" mode="aspectFill"></image>
<image class="notification-avatar" src="{{avatar}}" mode="aspectFill" binderror="onAvatarError"></image>
<!-- 中间内容区域 -->
<view class="notification-body">
<view class="notification-tag">新消息</view>
<view class="notification-title">{{title}}</view>
<view class="notification-message">{{message}}</view>
<view class="notification-time" wx:if="{{showTime}}">
{{formatTime(notificationData ? notificationData.timestamp : '')}}
<view class="notification-time" wx:if="{{show && showTime && formattedTime}}">
{{formattedTime}}
</view>
</view>

View File

@@ -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;

View File

@@ -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 });
}
},
});
},
},
});

View File

@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}

View File

@@ -0,0 +1,47 @@
<movable-area class="kefu-movable-area" wx:if="{{!sessionHidden}}">
<movable-view
class="kefu-movable-view"
direction="vertical"
y="{{floatY}}"
damping="40"
friction="2"
bindchange="onFloatMove"
>
<block wx:if="{{mode === 'full'}}">
<view class="kefu-wrap">
<view class="kefu-float kefu-float--full">
<view class="kefu-unread-badge" wx:if="{{csUnread > 0}}">{{csUnread > 99 ? '99+' : csUnread}}</view>
<view class="kefu-float-main" catchtap="onContact">
<text class="kefu-float-icon">💬</text>
<view class="kefu-text-col">
<text class="kefu-float-text">联系客服</text>
<text class="kefu-float-sub" wx:if="{{csUnread > 0}}">客服有新消息</text>
<text class="kefu-float-sub kefu-float-sub--muted" wx:else>咨询订单 / 账号问题</text>
</view>
</view>
<view class="kefu-float-close" catchtap="onHideTap">
<text>×</text>
</view>
</view>
<view class="kefu-permanent-row" catchtap="onSessionDismiss">
<text class="kefu-permanent-label">本次不再显示</text>
<text class="kefu-permanent-x">×</text>
</view>
</view>
</block>
<block wx:else>
<view class="kefu-wrap kefu-wrap--mini">
<view class="kefu-float kefu-float--mini" catchtap="onContact" bindlongpress="onExpand">
<view class="kefu-unread-badge kefu-unread-badge--mini" wx:if="{{csUnread > 0}}">{{csUnread > 99 ? '99+' : csUnread}}</view>
<text class="kefu-float-mini-icon">💬</text>
<text class="kefu-mini-label">联系客服</text>
</view>
<view class="kefu-permanent-row kefu-permanent-row--mini" catchtap="onSessionDismiss">
<text class="kefu-permanent-x">×</text>
<text class="kefu-permanent-label">本次隐藏</text>
</view>
</view>
</block>
</movable-view>
</movable-area>

View File

@@ -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;
}

View File

@@ -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');
}
}
});
},
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();
},
},
});

View File

@@ -1,12 +1,28 @@
<view class="order-sender-mask" wx:if="{{visible}}" catchtap="close"></view>
<view class="order-sender {{visible ? 'show' : ''}}">
<view class="header">
<text class="title">提示</text>
<text class="title">选择订单发送</text>
<text class="close" bindtap="close">✕</text>
</view>
<view class="content">
<view class="icon">📋</view>
<text class="notice">此功能暂未开放</text>
<text class="sub">敬请期待</text>
<view class="search-row">
<input class="search-input" placeholder="搜索订单号/内容" value="{{keyword}}" bindinput="onSearchInput" confirm-type="search" />
</view>
</view>
<scroll-view scroll-y class="order-list" enable-flex>
<view class="loading-tip" wx:if="{{loading}}">加载中...</view>
<block wx:for="{{filteredOrders}}" wx:key="dingdan_id">
<view class="order-item" data-index="{{index}}" bindtap="onSelectOrder">
<view class="order-item-top">
<text class="order-id">{{item.dingdan_id}}</text>
<text class="order-status">{{item.zhuangtaiText}}</text>
</view>
<text class="order-desc">{{item.jieshao || '暂无介绍'}}</text>
<view class="order-item-bottom">
<text class="order-price">¥{{item.jine || '0'}}</text>
<text class="order-time">{{item.create_time}}</text>
</view>
<text class="order-send-hint">点击发送订单卡片</text>
</view>
</block>
<view class="empty-tip" wx:if="{{!loading && !filteredOrders.length}}">暂无相关订单</view>
</scroll-view>
</view>

View File

@@ -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;
}
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;
}

View File

@@ -9,7 +9,7 @@
<text class="pindao-channel-no">{{channelNo}}</text>
</view>
<scroll-view class="pindao-scroll" scroll-y enhanced show-scrollbar="{{false}}">
<view wx:if="{{images.length === 0}}" class="pindao-empty">暂无频道内容</view>
<view wx:if="{{!images || images.length === 0}}" class="pindao-empty">暂无频道内容</view>
<image
wx:for="{{images}}"
wx:key="index"

View File

@@ -10,6 +10,7 @@ import { reconnectForRole } from '../../utils/role-tab-bar.js';
import { ensurePhoneAuth } from '../../utils/phone-auth.js';
import { fetchGonggaoLunbo, isGonggaoCacheValid } from '../../utils/display-config.js';
import { checkPenaltyForGrab } from '../../utils/grab-order-gate.js';
import { fetchMyZhidingOrders, submitZhidingResponse, processZhidingList, filterZhidingBannerList, ZHIDING_HF_MAP } from '../../utils/zhiding-order.js';
Page({
data: {
@@ -58,9 +59,25 @@ Page({
examRequired: false,
examPassed: false,
_examChecked: false,
myZhidingList: [],
highlightOrderId: '',
myZhidingCount: 0,
defaultAvatarUrl: '',
showZhidingDetail: false,
zhidingDetailOrder: null,
showLaterModal: false,
laterOrderId: '',
laterNote: '',
},
async onLoad() {
async onLoad(options) {
const defAvatar = getDefaultAvatarUrl(app);
const highlight = (options && options.highlight) ? String(options.highlight) : '';
this.setData({ defaultAvatarUrl: defAvatar, highlightOrderId: highlight });
if (options && options.highlight) {
this._pendingHighlight = String(options.highlight);
}
if (app.globalData._acceptOrderSessionReady && app.globalData._acceptOrderPageCache) {
const cache = { ...app.globalData._acceptOrderPageCache };
this.data.shangpinleixing = cache.shangpinleixing || [];
@@ -90,6 +107,7 @@ Page({
}
this.persistPageCache();
this._skipShowRefresh = true;
await this.loadMyZhidingOrders(true);
return;
}
@@ -97,6 +115,7 @@ Page({
const banner = await this.loadGonggaoAndLunbo(false);
await this.syncDashouProfileFromServer();
await this.loadShangpinLeixing(true, false, false);
await this.loadMyZhidingOrders(true);
this.persistPageCache(banner);
app.globalData._acceptOrderSessionReady = true;
@@ -149,6 +168,16 @@ Page({
await this.checkExamStatus(false);
this.setData({ defaultAvatarUrl: getDefaultAvatarUrl(app) });
await this.loadMyZhidingOrders(true);
const hl = app.globalData._acceptOrderHighlight;
if (hl) {
app.globalData._acceptOrderHighlight = '';
this.setData({ highlightOrderId: hl });
await this.loadMyZhidingOrders(true);
}
if (this._skipShowRefresh) {
this._skipShowRefresh = false;
return;
@@ -172,6 +201,7 @@ Page({
await this.loadDingdanList(true, true);
await this.loadBankuaiBiaoqian();
}
await this.loadMyZhidingOrders(true);
this.loadGlobalStatus();
this.persistPageCache(banner);
} catch (e) {
@@ -464,8 +494,11 @@ Page({
}
const isZhiding = item.zhuangtai === 7;
const uid = this.data.uid;
const isMyZhiding = isZhiding && uid && String(item.zhiding_uid) === String(uid);
let zhidingAvatar = '';
const defAvatar = getDefaultAvatarUrl(app);
let zhidingAvatar = defAvatar;
if (item.zhiding_avatar) {
zhidingAvatar = !item.zhiding_avatar.startsWith('http')
? ossUrl + item.zhiding_avatar
@@ -493,6 +526,11 @@ Page({
leixing_icon_url: fullTupianUrl,
shangjia_avatar_full: shangjiaAvatar,
isZhiding: isZhiding,
isMyZhiding,
isHighlighted: this.data.highlightOrderId && item.dingdan_id === this.data.highlightOrderId,
zhiding_hf: item.zhiding_hf,
zhiding_hf_text: item.zhiding_hf_text || ZHIDING_HF_MAP[item.zhiding_hf] || '',
zhiding_dhj_sm: item.zhiding_dhj_sm || '',
isPingtai: item.pingtai == 1,
isShangjia: item.pingtai == 2,
zhiding_avatar_full: zhidingAvatar,
@@ -684,6 +722,7 @@ Page({
);
that.setData({ dingdanList: newList });
that.persistPageCache();
that.loadMyZhidingOrders(true);
} else {
wx.showToast({
title: qiangdanRes.data.msg || '抢单失败',
@@ -714,6 +753,125 @@ Page({
});
},
async loadMyZhidingOrders(silent = false) {
if (!this.data.uid && !wx.getStorageSync('uid')) return;
try {
const raw = await fetchMyZhidingOrders();
const uid = this.data.uid || wx.getStorageSync('uid');
const highlight = this.data.highlightOrderId || this._pendingHighlight || '';
const list = filterZhidingBannerList(processZhidingList(raw, app, uid, highlight));
this.setData({ myZhidingList: list, myZhidingCount: list.length });
if (highlight && list.some((o) => 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) {

View File

@@ -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"
}
}

View File

@@ -44,6 +44,32 @@
<text class="xym-gonggao-txt">{{gonggao}}</text>
</view>
<!-- 被指定订单提醒 -->
<view class="my-zhiding-banner" wx:if="{{myZhidingList.length > 0}}">
<view class="my-zhiding-banner-head">
<view class="my-zhiding-head-left">
<text class="my-zhiding-badge">指定给您</text>
<text class="my-zhiding-count-num">{{myZhidingCount || myZhidingList.length}}单</text>
</view>
<text class="my-zhiding-sub">请确认是否接待</text>
</view>
<view wx:for="{{myZhidingList}}" wx:key="dingdan_id" class="my-zhiding-card {{item.isHighlighted ? 'my-zhiding-card--highlight' : ''}}">
<view class="my-zhiding-card-main" data-item="{{item}}" bindtap="openZhidingDetail">
<view class="my-zhiding-card-top">
<text class="my-zhiding-order-id">单号 {{item.dingdan_id}}</text>
<text class="my-zhiding-reward">¥{{item.dashou_fencheng || 0}}</text>
</view>
<text class="my-zhiding-desc line2">{{item.jieshao || '暂无介绍'}}</text>
<text class="my-zhiding-status" wx:if="{{item.zhiding_hf_text}}">您的回复:{{item.zhiding_hf_text}}{{item.zhiding_dhj_sm ? ' · ' + item.zhiding_dhj_sm : ''}}</text>
</view>
<view class="my-zhiding-actions">
<view class="zhiding-act zhiding-act--ghost" data-item="{{item}}" catchtap="onZhidingReject">不想接</view>
<view class="zhiding-act zhiding-act--ghost" data-item="{{item}}" catchtap="onZhidingLater">等会接</view>
<view class="zhiding-act zhiding-act--primary" data-item="{{item}}" catchtap="onZhidingAccept">立即接待</view>
</view>
</view>
</view>
<!-- 商品轮播(只读 globalData与点单端同源 -->
<view class="xym-lunbo-container" wx:if="{{lunboList.length > 0}}">
@@ -182,7 +208,7 @@
<!-- 平台订单(逍遥梦卡片布局) -->
<view wx:if="{{item.isPingtai}}" class="xym-order-item xym-pingtai-order" data-item="{{item}}">
<view wx:if="{{item.isPingtai}}" class="xym-order-item xym-pingtai-order {{item.isMyZhiding ? 'order-my-zhiding' : ''}} {{item.isHighlighted ? 'order-zhiding-highlight' : ''}}" data-item="{{item}}">
<view class="xym-pingtai-head flexb">
@@ -212,7 +238,7 @@
<view class="zhiding-strip light">
<view class="zhiding-strip-top myflex">
<text class="zhiding-label">指定</text>
<image wx:if="{{item.zhiding_avatar_full}}" class="zhiding-avatar" src="{{item.zhiding_avatar_full}}" mode="aspectFill"/>
<image class="zhiding-avatar" src="{{item.zhiding_avatar_full || defaultAvatarUrl}}" mode="aspectFill"/>
<text class="zhiding-name">{{item.zhiding_nicheng || '指定接单员'}}</text>
</view>
<view wx:if="{{item.dashou_biaoqian.length > 0 || item.zhiding_identity_biaoqian.length > 0}}" class="zhiding-strip-tags">
@@ -258,7 +284,7 @@
<text class="o-amt light">平台派单</text>
<view class="qiangdan flexa btn-bg btn-qiang btn-qiang-brown" data-item="{{item}}" bindtap="onQiangdanTap">抢单</view>
<view class="qiangdan flexa btn-bg btn-qiang btn-qiang-brown" data-item="{{item}}" bindtap="onQiangdanTap">{{item.isMyZhiding ? '立即接待' : '抢单'}}</view>
</view>
@@ -271,7 +297,7 @@
<!-- 优质商家订单(金牌卡片) -->
<view wx:elif="{{item.isShangjia && item.shangjia_youzhi}}" class="xym-order-item gold-card" data-item="{{item}}">
<view wx:elif="{{item.isShangjia && item.shangjia_youzhi}}" class="xym-order-item gold-card {{item.isMyZhiding ? 'order-my-zhiding' : ''}} {{item.isHighlighted ? 'order-zhiding-highlight' : ''}}" data-item="{{item}}">
<view class="gold-head flexb">
<view class="kehuduan-banner-wrap">
<image class="gold-banner" src="https://bintao.xmxym88.com/xcx/bintao/82.png" mode="widthFix"/>
@@ -292,7 +318,7 @@
<view class="zhiding-strip">
<view class="zhiding-strip-top myflex">
<text class="zhiding-label">指定</text>
<image wx:if="{{item.zhiding_avatar_full}}" class="zhiding-avatar" src="{{item.zhiding_avatar_full}}" mode="aspectFill"/>
<image class="zhiding-avatar" src="{{item.zhiding_avatar_full || defaultAvatarUrl}}" mode="aspectFill"/>
<text class="zhiding-name">{{item.zhiding_nicheng || '指定接单员'}}</text>
</view>
<view wx:if="{{item.dashou_biaoqian.length > 0 || item.zhiding_identity_biaoqian.length > 0}}" class="zhiding-strip-tags">
@@ -336,7 +362,7 @@
<view class="m-sn m-sn-light">发布于 {{item.creat_time}}</view>
</view>
</view>
<view class="qiangdan flexa btn-bg btn-qiang" data-item="{{item}}" bindtap="onQiangdanTap">抢单</view>
<view class="qiangdan flexa btn-bg btn-qiang" data-item="{{item}}" bindtap="onQiangdanTap">{{item.isMyZhiding ? '立即接待' : '抢单'}}</view>
</view>
<view class="o-foot flexb kehuduan-foot gold-foot-text-only">
<text class="o-amt-grey">商家派单</text>
@@ -347,7 +373,7 @@
<!-- 商家订单(逍遥梦卡片布局) -->
<view wx:elif="{{item.isShangjia}}" class="xym-order-item xym-shangjia-order" data-item="{{item}}">
<view wx:elif="{{item.isShangjia}}" class="xym-order-item xym-shangjia-order {{item.isMyZhiding ? 'order-my-zhiding' : ''}} {{item.isHighlighted ? 'order-zhiding-highlight' : ''}}" data-item="{{item}}">
<view class="xym-shangjia-head flexb">
@@ -365,7 +391,7 @@
<view class="zhiding-strip">
<view class="zhiding-strip-top myflex">
<text class="zhiding-label">指定</text>
<image wx:if="{{item.zhiding_avatar_full}}" class="zhiding-avatar" src="{{item.zhiding_avatar_full}}" mode="aspectFill"/>
<image class="zhiding-avatar" src="{{item.zhiding_avatar_full || defaultAvatarUrl}}" mode="aspectFill"/>
<text class="zhiding-name">{{item.zhiding_nicheng || '指定接单员'}}</text>
</view>
<view wx:if="{{item.dashou_biaoqian.length > 0 || item.zhiding_identity_biaoqian.length > 0}}" class="zhiding-strip-tags">
@@ -442,7 +468,7 @@
<text class="o-amt">商家派单</text>
<view class="qiangdan flexa btn-bg btn-qiang" data-item="{{item}}" bindtap="onQiangdanTap">抢单</view>
<view class="qiangdan flexa btn-bg btn-qiang" data-item="{{item}}" bindtap="onQiangdanTap">{{item.isMyZhiding ? '立即接待' : '抢单'}}</view>
</view>
@@ -482,8 +508,58 @@
<global-notification id="global-notification" />
<kefu-float bottom="200rpx" />
<popup-notice id="popupNotice" />
<!-- 指定单详情 -->
<view class="zhiding-modal-mask" wx:if="{{showZhidingDetail}}" catchtap="closeZhidingDetail">
<view class="zhiding-modal" catchtap="noop">
<view class="zhiding-modal-head">
<text class="zhiding-modal-title">指定订单详情</text>
<text class="zhiding-modal-close" catchtap="closeZhidingDetail">×</text>
</view>
<view class="zhiding-modal-body" wx:if="{{zhidingDetailOrder}}">
<view class="zhiding-modal-row"><text class="lbl">单号</text><text>{{zhidingDetailOrder.dingdan_id}}</text></view>
<view class="zhiding-modal-row"><text class="lbl">分成</text><text class="reward">¥{{zhidingDetailOrder.dashou_fencheng || 0}}</text></view>
<view class="zhiding-modal-row col"><text class="lbl">介绍</text><text>{{zhidingDetailOrder.jieshao || '暂无'}}</text></view>
<view class="zhiding-modal-row col" wx:if="{{zhidingDetailOrder.beizhu}}"><text class="lbl">备注</text><text>{{zhidingDetailOrder.beizhu}}</text></view>
<view class="zhiding-modal-row" wx:if="{{zhidingDetailOrder.zhiding_hf_text}}"><text class="lbl">您的回复</text><text>{{zhidingDetailOrder.zhiding_hf_text}}</text></view>
</view>
<view class="my-zhiding-actions zhiding-modal-actions" wx:if="{{zhidingDetailOrder}}">
<view class="zhiding-act zhiding-act--ghost" data-item="{{zhidingDetailOrder}}" catchtap="onZhidingReject">不想接</view>
<view class="zhiding-act zhiding-act--ghost" data-item="{{zhidingDetailOrder}}" catchtap="onZhidingLater">等会接</view>
<view class="zhiding-act zhiding-act--primary" data-item="{{zhidingDetailOrder}}" catchtap="onZhidingAccept">立即接待</view>
</view>
</view>
</view>
<!-- 等会接时间 -->
<view class="zhiding-modal-mask zhiding-modal-mask--top" wx:if="{{showLaterModal}}" catchtap="closeLaterModal">
<view class="zhiding-modal zhiding-modal--sm" catchtap="noop">
<view class="zhiding-modal-head">
<text class="zhiding-modal-title">等会接</text>
<text class="zhiding-modal-close" catchtap="closeLaterModal">×</text>
</view>
<view class="zhiding-modal-body">
<text class="later-hint">请填写预计接待时间例如「30分钟后」「今晚8点」</text>
<input
class="later-input"
placeholder="预计接待时间"
placeholder-class="later-input-ph"
value="{{laterNote}}"
bindinput="onLaterNoteInput"
maxlength="50"
focus="{{showLaterModal}}"
adjust-position="{{true}}"
confirm-type="done"
bindconfirm="confirmZhidingLater"
/>
</view>
<view class="later-confirm" catchtap="confirmZhidingLater">确认</view>
</view>
</view>
<tab-bar />
</view>

View File

@@ -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;
}

View File

@@ -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<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);
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() {

View File

@@ -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);">
<view class="chat-loading-more" wx:if="{{loadingHistory}}">加载更早消息...</view>
<block wx:for="{{messages}}" wx:key="messageId">
<view class="chat-msg-wrapper" data-messageid="{{item.messageId}}" bindlongpress="showAction">
<view class="chat-time-tag" wx:if="{{item.showTime}}">{{item.formattedTime}}</view>
<view class="chat-msg-row {{item.senderId === currentUser.id ? 'chat-msg-row--right' : 'chat-msg-row--left'}}">
<image wx:if="{{item.senderId !== currentUser.id}}" class="chat-avatar" src="{{item.senderData.avatar || toAvatar}}" mode="aspectFill" binderror="onPeerAvatarError" />
<image wx:if="{{item.senderId !== currentUser.id}}" class="chat-avatar" src="{{toAvatar || defaultAvatarUrl}}" mode="aspectFill" binderror="onPeerAvatarError" />
<view class="chat-bubble {{item.senderId === currentUser.id ? 'chat-bubble--right' : 'chat-bubble--left'}}"
data-messageid="{{item.messageId}}"
data-text="{{item.type==='text' ? item.payload.text : ''}}"
@@ -23,14 +23,14 @@
<text class="chat-order-info">ID: {{item.payload.orderId}}</text>
</view>
</view>
<image wx:if="{{item.senderId === currentUser.id}}" class="chat-avatar" src="{{item.senderData.avatar || currentUser.avatar}}" mode="aspectFill" binderror="onSelfAvatarError" data-index="{{index}}" />
<image wx:if="{{item.senderId === currentUser.id}}" class="chat-avatar" src="{{currentUser.avatar || defaultAvatarUrl}}" mode="aspectFill" binderror="onSelfAvatarError" data-index="{{index}}" />
</view>
<view wx:if="{{item.senderId === currentUser.id && item.status === 'success'}}" class="chat-read-tag">
{{item.read ? '已读' : '未读'}}
</view>
</view>
</block>
<view id="msg-bottom" style="height:20rpx;"></view>
<view id="msg-bottom" style="height:56rpx;"></view>
</scroll-view>
<view class="chat-input-area" style="bottom: {{keyboardHeight}}px; padding-bottom: calc(10rpx + env(safe-area-inset-bottom));">

View File

@@ -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<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 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();
});
}
});

View File

@@ -11,7 +11,7 @@
<view class="chat-time-tag" wx:if="{{item.showTime}}">{{item.formattedTime}}</view>
<view class="chat-msg-row {{item.senderId === currentUser.id ? 'chat-msg-row--right' : 'chat-msg-row--left'}}">
<!-- 对方头像(客服) -->
<image wx:if="{{item.senderId !== currentUser.id}}" class="chat-avatar" src="{{teamAvatar}}" mode="aspectFill" />
<image wx:if="{{item.senderId !== currentUser.id}}" class="chat-avatar" src="{{defaultAvatarUrl}}" mode="aspectFill" binderror="onTeamAvatarError" />
<view class="chat-bubble {{item.senderId === currentUser.id ? 'chat-bubble--right' : 'chat-bubble--left'}}"
data-messageid="{{item.messageId}}"
data-text="{{item.type==='text' ? item.payload.text : ''}}"
@@ -31,12 +31,12 @@
<text wx:else class="chat-msg-text">{{item.payload.text || '[未知消息]'}}</text>
</view>
<!-- 自己头像 -->
<image wx:if="{{item.senderId === currentUser.id}}" class="chat-avatar" src="{{currentUser.avatar}}" mode="aspectFill" />
<image wx:if="{{item.senderId === currentUser.id}}" class="chat-avatar" src="{{currentUser.avatar || defaultAvatarUrl}}" mode="aspectFill" binderror="onSelfAvatarError" />
</view>
<!-- 🔥 已读/未读标签已彻底移除 -->
</view>
</block>
<view id="msg-bottom" style="height:20rpx;"></view>
<view id="msg-bottom" style="height:56rpx;"></view>
</scroll-view>
<!-- 底部输入区域(保持不变) -->

View File

@@ -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() {

View File

@@ -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",

View File

@@ -18,6 +18,7 @@
</view>
</view>
<tab-bar/>
<kefu-float bottom="200rpx" />
</view>
</block>
@@ -70,8 +71,17 @@
</view>
</view>
<!-- 被指定订单(紧凑条,不占满屏) -->
<view class="my-zhiding-mine-strip" wx:if="{{myZhidingCount > 0 && isDashou}}" bindtap="goToZhidingFromMine">
<view class="my-zhiding-mine-strip-left">
<text class="my-zhiding-mine-badge">指定</text>
<text class="my-zhiding-mine-count">{{myZhidingCount}}单待接待</text>
</view>
<text class="my-zhiding-mine-arrow">去处理 </text>
</view>
<!-- 称号 -->
<view class="badge-zone" wx:if="{{chenghaoList.length > 0 || (isGuanshi && guanshiChenghaoList.length > 0) || identityTagList.length > 0}}">
<view class="badge-zone" wx:if="{{showBadgeZone}}">
<scroll-view scroll-x class="badge-scroll" wx:if="{{chenghaoList.length > 0}}">
<view class="badge-tag-wrap" wx:for="{{chenghaoList}}" wx:key="index">
<chenghao-tag mingcheng="{{item.mingcheng}}" texiaoJson="{{item.texiao_json}}"/>
@@ -253,6 +263,7 @@
<view class="content-bottom"></view>
</scroll-view>
<tab-bar/>
<kefu-float bottom="200rpx" />
</view>
</block>

View File

@@ -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;
}

View File

@@ -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();
},
});

View File

@@ -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"
}
}

View File

@@ -1,18 +1,28 @@
<view class="chat-page">
<view class="order-status-bar" wx:if="{{orderId}}" bindtap="tapOrderBar">
<view class="order-status-main">
<view class="order-status-left">
<text class="order-status-id">订单 {{orderId}}</text>
<text class="order-status-price" wx:if="{{orderJine}}">¥{{orderJine}}</text>
</view>
<text class="order-status-tag" wx:if="{{orderZhuangtaiText}}">{{orderZhuangtaiText}}</text>
</view>
<text class="order-status-desc" wx:if="{{orderJieshao}}">{{orderJieshao}}</text>
<text class="order-status-hint">点击查看历史订单并发送</text>
</view>
<scroll-view class="chat-msg-list" scroll-y="true"
scroll-into-view="{{scrollToView}}"
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: {{orderId ? '120rpx' : '20rpx'}}; bottom: calc({{bottomSafeHeight}}rpx + {{keyboardHeight}}px + env(safe-area-inset-bottom) + 24rpx);">
<view class="chat-loading-more" wx:if="{{loadingHistory}}">加载更早消息...</view>
<block wx:for="{{messages}}" wx:key="messageId">
<block wx:for="{{messages}}" wx:key="messageId" wx:for-index="msgIdx">
<view class="chat-msg-wrapper" data-messageid="{{item.messageId}}" bindlongpress="showAction">
<view class="chat-time-tag" wx:if="{{item.showTime}}">{{item.formattedTime}}</view>
<view class="chat-msg-row {{item.senderId === currentUser.id ? 'chat-msg-row--right' : 'chat-msg-row--left'}}">
<!-- 对方头像及昵称 -->
<view wx:if="{{item.senderId !== currentUser.id}}" class="sender-info">
<image class="chat-avatar" src="{{item.senderData.avatar || '/images/default-avatar.png'}}" mode="aspectFill" />
<image class="chat-avatar" src="{{item.senderData.avatar || defaultAvatarUrl}}" mode="aspectFill" binderror="onAvatarError" data-index="{{msgIdx}}" />
<text class="sender-name">{{item.senderData.name || item.senderId}}</text>
</view>
<view class="chat-bubble {{item.senderId === currentUser.id ? 'chat-bubble--right' : 'chat-bubble--left'}}"
@@ -27,15 +37,15 @@
<text class="chat-order-info">ID: {{item.payload.orderId}}</text>
<text class="chat-order-info">内容: {{item.payload.jieshao}}</text>
<text class="chat-order-info">金额: ¥{{item.payload.jine}}</text>
<text class="chat-order-info" wx:if="{{item.payload.beizhu}}">备注: {{item.payload.beizhu}}</text>
<view class="chat-order-info" style="color:#007aff;margin-top:8rpx">点击查看详情</view>
</view>
</view>
<!-- 自己的头像 -->
<image wx:if="{{item.senderId === currentUser.id}}" class="chat-avatar" src="{{currentUser.avatar}}" mode="aspectFill" />
<image wx:if="{{item.senderId === currentUser.id}}" class="chat-avatar" src="{{currentUser.avatar || defaultAvatarUrl}}" mode="aspectFill" binderror="onSelfAvatarError" />
</view>
</view>
</block>
<view id="msg-bottom" style="height:20rpx;"></view>
<view id="msg-bottom" style="height:56rpx;"></view>
</scroll-view>
<view class="chat-input-area" style="bottom: {{keyboardHeight}}px; padding-bottom: calc(10rpx + env(safe-area-inset-bottom));">
@@ -88,5 +98,5 @@
</view>
</view>
<order-sender visible="{{showOrderSender}}" bind:send="onSendOrder" bind:close="closeOrderSender" />
<order-sender visible="{{showOrderSender}}" group-id="{{groupId}}" order-id="{{orderId}}" bind:send="onSendOrder" bind:close="closeOrderSender" />
</view>

View File

@@ -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;

View File

@@ -114,6 +114,7 @@ Page(createPage({
if (this.data.isShangjia) {
ensureRoleOnCenterPage(this, 'shangjia');
}
if (app.startImWhenReady) app.startImWhenReady();
if (this._skipShowRefresh) {
this._skipShowRefresh = false;

View File

@@ -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,
};
},

View File

@@ -65,8 +65,9 @@
<!-- 主体:左右布局 -->
<view class="main-container">
<!-- 左侧状态栏(固定 -->
<view class="left-status">
<!-- 左侧状态栏(可纵向滑动 -->
<scroll-view class="left-status" scroll-y enhanced show-scrollbar="{{false}}">
<view class="left-status-inner">
<block wx:for="{{statusList}}" wx:key="key">
<view class="status-item {{currentStatusKey == item.key ? 'status-active' : ''}}" data-key="{{item.key}}" bindtap="switchStatus">
<text class="status-name">{{item.name}}</text>
@@ -75,7 +76,8 @@
<view wx:if="{{item.key === 'jiesuanzhong' && pendingCount > 0}}" class="sj-status-badge">{{pendingCount}}</view>
</view>
</block>
</view>
</view>
</scroll-view>
<!-- 右侧包裹:撑出右边距,里面是 scroll-view -->
<view class="right-wrapper">
@@ -113,123 +115,20 @@
<text class="empty-subtext">快去派发订单吧</text>
</view>
<!-- 订单卡片:逍遥梦 ui-preview 布局 -->
<!-- 订单列表:简洁卡片,突出状态 -->
<view class="sj-order-list">
<block wx:for="{{currentList}}" wx:key="dingdan_id">
<!-- 优质商家金牌卡 -->
<view wx:if="{{item.shangjia_youzhi}}" class="xym-order-item gold-card" data-item="{{item}}" bindtap="goToSjDingdanXiangqing">
<view class="gold-head flexb">
<view class="kehuduan-banner-wrap">
<image class="gold-banner" src="{{goldBannerUrl}}" mode="widthFix"/>
</view>
<view class="sj-order-card" data-item="{{item}}" bindtap="goToSjDingdanXiangqing">
<view class="sj-order-card-head">
<text class="sj-order-id">单号 {{item.dingdan_id}}</text>
<view class="sj-status-tag" style="color:{{item.zhuangtaiColor}};background:{{item.zhuangtaiBg}}">{{item.zhuangtaiZh}}</view>
</view>
<view class="gold-body-wrap">
<view class="order-con gold-inner">
<view wx:if="{{item.isZhiding}}" class="zhiding-strip">
<view class="zhiding-strip-top myflex">
<text class="zhiding-label">指定</text>
<image wx:if="{{item.zhiding_avatar_full}}" class="zhiding-avatar" src="{{item.zhiding_avatar_full}}" mode="aspectFill"/>
<text class="zhiding-name">{{item.zhiding_nicheng || '指定接单员'}}</text>
</view>
<view wx:if="{{item.dashou_biaoqian.length > 0 || item.zhiding_identity_biaoqian.length > 0}}" class="zhiding-strip-tags">
<chenghao-tag wx:for="{{item.dashou_biaoqian}}" wx:for-item="tag" wx:key="id" mingcheng="{{tag.mingcheng}}" texiaoJson="{{tag.texiao_json}}" />
<chenghao-tag wx:for="{{item.zhiding_identity_biaoqian}}" wx:for-item="tag" wx:key="id" mingcheng="{{tag.mingcheng}}" texiaoJson="{{tag.texiao_json}}" />
</view>
</view>
<view class="order-row myflex">
<image class="o-logo" src="{{item.leixing_icon_url}}" mode="aspectFill"/>
<view class="o-col">
<view class="o-goods line2">{{item.jieshao || '暂无介绍'}}</view>
<view class="tag-row myflex">
<text class="tag-pill tag-youzhi">优质商家</text>
</view>
</view>
</view>
<view class="reward-badge normal-reward">
<text class="reward-yen sm">¥</text>
<text class="reward-num dark">{{item.jine || '0.00'}}</text>
</view>
<view class="remark-block dark">
<text class="remark-label dark-t">商家备注:</text>{{item.beizhu || '暂无'}}
</view>
<view wx:if="{{item.shangjia_biaoqian.length || item.shangjia_identity_biaoqian.length || item.xuqiu_biaoqian.length}}" class="merchant-tags-container">
<scroll-view wx:if="{{item.shangjia_biaoqian.length}}" class="biaoqian-inline-scroll" scroll-x>
<chenghao-tag wx:for="{{item.shangjia_biaoqian}}" wx:for-item="tag" wx:key="id" mingcheng="{{tag.mingcheng}}" texiaoJson="{{tag.texiao_json}}" />
</scroll-view>
<scroll-view wx:if="{{item.shangjia_identity_biaoqian.length}}" class="biaoqian-inline-scroll" scroll-x style="margin-top:8rpx">
<chenghao-tag wx:for="{{item.shangjia_identity_biaoqian}}" wx:for-item="tag" wx:key="id" mingcheng="{{tag.mingcheng}}" texiaoJson="{{tag.texiao_json}}" />
</scroll-view>
<scroll-view wx:if="{{item.xuqiu_biaoqian.length}}" class="biaoqian-inline-scroll" scroll-x style="margin-top:8rpx">
<chenghao-tag wx:for="{{item.xuqiu_biaoqian}}" wx:for-item="tag" wx:key="id" mingcheng="{{tag.mingcheng}}" texiaoJson="{{tag.texiao_json}}" />
</scroll-view>
</view>
<view class="merchant-strip myflex">
<image class="m-avatar" src="{{item.sj_avatar_full}}" mode="aspectFill"/>
<view class="m-info">
<view class="m-name light-t">{{item.sjnicheng || '未知商家'}}</view>
<view class="m-sn light-sub">ID:{{item.shangjia_id}} 发布于 {{item.creat_time}}</view>
</view>
</view>
<view class="o-foot flexb">
<text class="o-amt-grey">订单金额 ¥{{item.jine || '0.00'}}</text>
<view class="sj-status-pill" style="color:{{item.zhuangtaiColor}}">{{item.zhuangtaiZh}}</view>
</view>
</view>
</view>
</view>
<!-- 普通商家订单 -->
<view wx:else class="xym-order-item normal-card" data-item="{{item}}" bindtap="goToSjDingdanXiangqing">
<view class="normal-body-wrap">
<view class="order-con">
<view wx:if="{{item.isZhiding}}" class="zhiding-strip">
<view class="zhiding-strip-top myflex">
<text class="zhiding-label">指定</text>
<image wx:if="{{item.zhiding_avatar_full}}" class="zhiding-avatar" src="{{item.zhiding_avatar_full}}" mode="aspectFill"/>
<text class="zhiding-name">{{item.zhiding_nicheng || '指定接单员'}}</text>
</view>
<view wx:if="{{item.dashou_biaoqian.length > 0 || item.zhiding_identity_biaoqian.length > 0}}" class="zhiding-strip-tags">
<chenghao-tag wx:for="{{item.dashou_biaoqian}}" wx:for-item="tag" wx:key="id" mingcheng="{{tag.mingcheng}}" texiaoJson="{{tag.texiao_json}}" />
<chenghao-tag wx:for="{{item.zhiding_identity_biaoqian}}" wx:for-item="tag" wx:key="id" mingcheng="{{tag.mingcheng}}" texiaoJson="{{tag.texiao_json}}" />
</view>
</view>
<view class="order-row myflex">
<image class="o-logo" src="{{item.leixing_icon_url}}" mode="aspectFill"/>
<view class="o-col">
<view class="o-goods line2">{{item.jieshao || '暂无介绍'}}</view>
</view>
</view>
<view class="reward-badge normal-reward">
<text class="reward-yen sm">¥</text>
<text class="reward-num dark">{{item.jine || '0.00'}}</text>
</view>
<view class="remark-block dark">
<text class="remark-label dark-t">商家备注:</text>{{item.beizhu || '暂无'}}
</view>
<view wx:if="{{item.shangjia_biaoqian.length || item.shangjia_identity_biaoqian.length || item.xuqiu_biaoqian.length}}" class="merchant-tags-container">
<scroll-view wx:if="{{item.shangjia_biaoqian.length}}" class="biaoqian-inline-scroll" scroll-x>
<chenghao-tag wx:for="{{item.shangjia_biaoqian}}" wx:for-item="tag" wx:key="id" mingcheng="{{tag.mingcheng}}" texiaoJson="{{tag.texiao_json}}" />
</scroll-view>
<scroll-view wx:if="{{item.shangjia_identity_biaoqian.length}}" class="biaoqian-inline-scroll" scroll-x style="margin-top:8rpx">
<chenghao-tag wx:for="{{item.shangjia_identity_biaoqian}}" wx:for-item="tag" wx:key="id" mingcheng="{{tag.mingcheng}}" texiaoJson="{{tag.texiao_json}}" />
</scroll-view>
<scroll-view wx:if="{{item.xuqiu_biaoqian.length}}" class="biaoqian-inline-scroll" scroll-x style="margin-top:8rpx">
<chenghao-tag wx:for="{{item.xuqiu_biaoqian}}" wx:for-item="tag" wx:key="id" mingcheng="{{tag.mingcheng}}" texiaoJson="{{tag.texiao_json}}" />
</scroll-view>
</view>
<view class="merchant-strip myflex">
<image class="m-avatar" src="{{item.sj_avatar_full}}" mode="aspectFill"/>
<view class="m-info">
<view class="m-name light-t">{{item.sjnicheng || '未知商家'}}</view>
<view class="m-sn light-sub">ID:{{item.shangjia_id}} 发布于 {{item.creat_time}}</view>
</view>
</view>
<view class="o-foot flexb">
<text class="o-amt-grey">订单金额 ¥{{item.jine || '0.00'}}</text>
<view class="sj-status-pill" style="color:{{item.zhuangtaiColor}}">{{item.zhuangtaiZh}}</view>
</view>
</view>
<text class="sj-order-desc line2">{{item.jieshao || '暂无介绍'}}</text>
<view class="sj-order-meta">
<text class="sj-order-amount">¥{{item.jine || '0.00'}}</text>
<text class="sj-order-time">{{item.creat_time}}</text>
</view>
<text class="sj-order-remark line2" wx:if="{{item.beizhu}}">备注:{{item.beizhu}}</text>
</view>
</block>
</view>

View File

@@ -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;
}

View File

@@ -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) {

View File

@@ -56,10 +56,11 @@
<view>
<view class="nickname-row flex">
<text class="nickname">{{nicheng || '商家'}}</text>
<view class="sj-identity-tag">{{isStaffMode ? (staffRoleName || '商家客服') : '商家'}}</view>
<view class="sj-identity-tag" wx:if="{{isStaffMode}}">{{staffRoleName || '商家客服'}}</view>
<view class="sj-identity-tag" wx:else>商家</view>
</view>
<view wx:if="{{!isStaffMode}}" class="nickname-edit-btn flexm" bindtap="editNicheng">修改昵称</view>
<view class="identity-tags-row flex" wx:if="{{identityTagList.length > 0}}">
<view class="identity-tags-row flex" wx:if="{{identityTagList && identityTagList.length > 0}}">
<view class="badge-tag-wrap" wx:for="{{identityTagList}}" wx:key="id">
<chenghao-tag mingcheng="{{item.mingcheng}}" texiaoJson="{{item.texiao_json}}"/>
</view>
@@ -75,7 +76,7 @@
</view>
</view>
<view class="badge-zone" wx:if="{{chenghaoList.length > 0}}">
<view class="badge-zone" wx:if="{{chenghaoList && chenghaoList.length > 0}}">
<scroll-view scroll-x class="badge-scroll">
<view class="badge-tag-wrap" wx:for="{{chenghaoList}}" wx:key="index">
<chenghao-tag mingcheng="{{item.mingcheng}}" texiaoJson="{{item.texiao_json}}"/>

View File

@@ -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) {

View File

@@ -10,41 +10,62 @@
</view>
</view>
<view class="im-status-bar im-status-bar--warn" wx:if="{{imStatusText}}" bindtap="retryImConnection">
<text class="im-status-icon">⚠</text>
<text class="im-status-text">{{imStatusText}},点击重连</text>
</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>
<text class="tab-badge" wx:if="{{tabUnread && 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>
<text class="tab-badge" wx:if="{{tabUnread && 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>
<text class="tab-badge" wx:if="{{tabUnread && 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' : ''}}" data-conversation="{{item}}" bindtap="chat" bindlongpress="showAction">
<image class="avatar" src="{{item.data.avatar}}" mode="aspectFill" binderror="onAvatarError" data-index="{{i}}" />
<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 || defaultAvatarUrl}}" mode="aspectFill" binderror="onAvatarError" data-index="{{i}}" />
<view class="info">
<view class="top-line">
<text class="name">{{item.data.name}}</text>
<view class="name-col">
<text class="name">{{item.data.name || '用户'}}</text>
<text class="uid-tag" wx:if="{{item.data.counterpartYonghuid}}">ID {{item.data.counterpartYonghuid}}</text>
</view>
<text class="time">{{item.lastMessage.date}}</text>
</view>
<view class="bottom-line">
<block wx:if="{{item.type === 'group'}}">
<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>
<view class="order-compact" wx:if="{{item.data.orderId || item.data.orderDesc}}">
<view class="order-compact-row" wx:if="{{item.data.orderId}}">
<text class="order-id-tag">单号</text>
<text class="order-id-text">{{item.data.orderId}}</text>
<text class="order-status-inline" wx:if="{{item.data.orderZhuangtaiText}}">{{item.data.orderZhuangtaiText}}</text>
</view>
<text class="order-desc-compact" wx:if="{{item.data.orderDesc}}">{{item.data.orderDesc}}</text>
</view>
</block>
<view class="bottom-line" wx:else>
<text class="last-msg">{{item.lastMessage.type === 'text' ? item.lastMessage.payload.text : (item.lastMessage.type === 'image' ? '[图片]' : '[其他消息]')}}</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>
@@ -54,7 +75,6 @@
</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>
@@ -63,7 +83,6 @@
</view>
</view>
<!-- 底部自定义TabBar -->
<tab-bar />
<global-notification id="global-notification" />
</view>
</view>

View File

@@ -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;

View File

@@ -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() {

View File

@@ -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 });
}

View File

@@ -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;

View File

@@ -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;

106
utils/chat-history.js Normal file
View File

@@ -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),
};
}

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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 };
module.exports = { dashoujianquan };

View File

@@ -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:

View File

@@ -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 };
module.exports = { laobanjianquan };

12
utils/kefu-nav.js Normal file
View File

@@ -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)),
});
}

View File

@@ -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;
}

View File

@@ -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 || '客服') : '',

View File

@@ -1,269 +1,296 @@
/**
* 订单群聊跳转:先确定群 IDIM 连接失败也尽量能进聊天页
*/
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();

53
utils/zhiding-order.js Normal file
View File

@@ -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);
}