chore: 抢单端UI回退前备份当前完整工作区
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
35
app.js
35
app.js
@@ -7,6 +7,7 @@ import { setClubId, getConfiguredClubId, buildClubHeaders, getClubId } from './u
|
||||
import { CLUB_ID, WX_APP_ID } from './config/club-config';
|
||||
import { applyMiniappAssetsFromConfig, warmupPindaoConfig } from './utils/miniapp-icons.js';
|
||||
import { refreshDashouMembership } from './utils/dashou-profile.js';
|
||||
const { getDefaultAvatarUrl, resolveAvatarFromStorage } = require('./utils/avatar.js');
|
||||
|
||||
// 三端固定首页
|
||||
const roleDefaultPage = PRIMARY_DEFAULT_PAGES;
|
||||
@@ -163,11 +164,21 @@ App({
|
||||
debugMode: false,
|
||||
currentUser: null,
|
||||
currentRole: 'normal',
|
||||
primaryRole: 'normal'
|
||||
},
|
||||
primaryRole: 'normal',
|
||||
/** 客服浮钮:full | mini,onLaunch 重置为 full */
|
||||
kefuFloatMode: 'full',
|
||||
/** 本次启动内隐藏客服浮钮(不持久化,杀进程后恢复) */
|
||||
kefuFloatSessionHidden: false,
|
||||
kefuFloatY: null,
|
||||
/** 从「我的」跳抢单页时高亮指定单 */
|
||||
_acceptOrderHighlight: '',
|
||||
},
|
||||
|
||||
// 核心启动流程
|
||||
async onLaunch() {
|
||||
this.globalData.kefuFloatMode = 'full';
|
||||
this.globalData.kefuFloatSessionHidden = false;
|
||||
try { wx.removeStorageSync('kefuFloatPermanentHidden'); } catch (e) {}
|
||||
setClubId(getConfiguredClubId(), this);
|
||||
this.globalData.clubId = CLUB_ID;
|
||||
// ① 隐藏返回首页按钮
|
||||
@@ -258,8 +269,13 @@ App({
|
||||
async onShow() {
|
||||
if (!wx.getStorageSync('token')) return;
|
||||
await ensurePhoneAuth({ redirect: true });
|
||||
if (this.globalData.chatEnabled && typeof this.ensureConnection === 'function') {
|
||||
if (this.globalData.chatEnabled && typeof this.startImWhenReady === 'function') {
|
||||
this.startImWhenReady();
|
||||
} else if (this.globalData.chatEnabled && typeof this.ensureConnection === 'function') {
|
||||
this.globalData.goEasyConnection.autoReconnect = true;
|
||||
if (typeof this.restoreTabBarBadge === 'function') {
|
||||
this.restoreTabBarBadge();
|
||||
}
|
||||
this.ensureConnection();
|
||||
if (typeof this.syncConnectionStatus === 'function') {
|
||||
setTimeout(() => this.syncConnectionStatus(), 800);
|
||||
@@ -386,6 +402,13 @@ App({
|
||||
} catch (e) {
|
||||
console.warn('miniapp assets apply failed', e);
|
||||
}
|
||||
|
||||
if (this.emitEvent) {
|
||||
this.emitEvent('configApplied', {
|
||||
ossImageUrl: this.globalData.ossImageUrl,
|
||||
morentouxiang: this.globalData.morentouxiang,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 仅在有效 appkey 时初始化
|
||||
@@ -422,10 +445,12 @@ App({
|
||||
initCurrentUser() {
|
||||
const uid = wx.getStorageSync('uid');
|
||||
if (uid) {
|
||||
const def = getDefaultAvatarUrl(this);
|
||||
const avatar = resolveAvatarFromStorage(this) || def;
|
||||
this.globalData.currentUser = {
|
||||
id: uid,
|
||||
name: '用户' + uid.substring(0, 6),
|
||||
avatar: this.globalData.ossImageUrl + this.globalData.morentouxiang
|
||||
name: wx.getStorageSync('nicheng') || ('用户' + uid.substring(0, 6)),
|
||||
avatar: avatar || def,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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:考核称号等特殊形状
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
126
components/kefu-float/kefu-float.js
Normal file
126
components/kefu-float/kefu-float.js
Normal 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 });
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
4
components/kefu-float/kefu-float.json
Normal file
4
components/kefu-float/kefu-float.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
47
components/kefu-float/kefu-float.wxml
Normal file
47
components/kefu-float/kefu-float.wxml
Normal 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>
|
||||
179
components/kefu-float/kefu-float.wxss
Normal file
179
components/kefu-float/kefu-float.wxss
Normal 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;
|
||||
}
|
||||
@@ -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();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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));">
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -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>
|
||||
|
||||
<!-- 底部输入区域(保持不变) -->
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
});
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -114,6 +114,7 @@ Page(createPage({
|
||||
if (this.data.isShangjia) {
|
||||
ensureRoleOnCenterPage(this, 'shangjia');
|
||||
}
|
||||
if (app.startImWhenReady) app.startImWhenReady();
|
||||
|
||||
if (this._skipShowRefresh) {
|
||||
this._skipShowRefresh = false;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}}"/>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
106
utils/chat-history.js
Normal 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),
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
12
utils/kefu-nav.js
Normal 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)),
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 || '客服') : '',
|
||||
|
||||
@@ -1,269 +1,296 @@
|
||||
/**
|
||||
* 订单群聊跳转:先确定群 ID,IM 连接失败也尽量能进聊天页
|
||||
*/
|
||||
import request from './request';
|
||||
import { persistGroupMeta } from './im-user.js';
|
||||
import { resolveLocalGroupId } from './group-chat.js';
|
||||
|
||||
const app = getApp();
|
||||
|
||||
function formatError(err) {
|
||||
if (!err) return '进入聊天失败';
|
||||
if (typeof err === 'string') return err;
|
||||
if (err.message) return err.message;
|
||||
if (err.content) return String(err.content);
|
||||
if (err.msg) return String(err.msg);
|
||||
if (err.code) return `连接失败(${err.code})`;
|
||||
try {
|
||||
return JSON.stringify(err);
|
||||
} catch (e) {
|
||||
return '进入聊天失败';
|
||||
}
|
||||
}
|
||||
|
||||
function withTimeout(promise, ms, errMsg) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => reject(new Error(errMsg || '操作超时')), ms);
|
||||
promise
|
||||
.then((v) => { clearTimeout(timer); resolve(v); })
|
||||
.catch((e) => { clearTimeout(timer); reject(e); });
|
||||
});
|
||||
}
|
||||
|
||||
function getConnectedUserId() {
|
||||
const fromState = app.globalData?.goEasyConnection?.userId;
|
||||
const fromIm = wx.goEasy?.im?.userId;
|
||||
return fromState || fromIm || '';
|
||||
}
|
||||
|
||||
function isImConnected() {
|
||||
const status = wx.goEasy?.getConnectionStatus?.() || 'disconnected';
|
||||
return status === 'connected' || status === 'reconnected';
|
||||
}
|
||||
|
||||
async function ensureIdentityConnection(identityType, userId) {
|
||||
app.globalData.currentRole = identityType;
|
||||
wx.setStorageSync('currentRole', identityType);
|
||||
|
||||
const uid = userId.replace(/^(Ds|Sj|Boss|Gs|Zz|Kh)/, '');
|
||||
const avatar = wx.getStorageSync('touxiang') ||
|
||||
(app.globalData.ossImageUrl + app.globalData.morentouxiang);
|
||||
app.globalData.currentUser = {
|
||||
id: userId,
|
||||
name: app.globalData.currentUser?.name || `用户${uid}`,
|
||||
avatar: avatar,
|
||||
};
|
||||
|
||||
if (!app.globalData.chatEnabled || !wx.goEasy?.im) {
|
||||
throw new Error('聊天服务未就绪,请稍后重试');
|
||||
}
|
||||
|
||||
if (isImConnected() && getConnectedUserId() === userId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const connectedId = getConnectedUserId();
|
||||
if (connectedId && connectedId !== userId && app.disconnectGoEasy) {
|
||||
await app.disconnectGoEasy();
|
||||
}
|
||||
|
||||
if (!app.connectWithIdentity) {
|
||||
throw new Error('聊天功能未初始化');
|
||||
}
|
||||
|
||||
await withTimeout(
|
||||
app.connectWithIdentity(identityType, userId, true),
|
||||
12000,
|
||||
'IM连接超时'
|
||||
);
|
||||
|
||||
if (!isImConnected()) {
|
||||
throw new Error('IM连接失败,请检查网络后重试');
|
||||
}
|
||||
}
|
||||
|
||||
async function prepareGroupChat(identityType, userId, orderId, partnerUid, fadanPingtai, groupName, isCross) {
|
||||
let chatData = null;
|
||||
|
||||
try {
|
||||
const res = await withTimeout(
|
||||
request({
|
||||
url: '/dingdan/ltdhzb',
|
||||
method: 'POST',
|
||||
data: {
|
||||
dingdan_id: orderId,
|
||||
identityType,
|
||||
push_order_card: false,
|
||||
},
|
||||
}),
|
||||
12000,
|
||||
'准备群聊超时'
|
||||
);
|
||||
const body = res?.data || {};
|
||||
if (body.code === 0 && body.data?.groupId) {
|
||||
chatData = body.data;
|
||||
} else if (body.msg) {
|
||||
console.warn('ltdhzb:', body.msg);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('ltdhzb 请求失败,尝试本地 groupId', e);
|
||||
}
|
||||
|
||||
if (!chatData?.groupId) {
|
||||
const myUid = userId.replace(/^(Ds|Sj|Boss)/, '');
|
||||
let localGroupId = resolveLocalGroupId(
|
||||
identityType, myUid, partnerUid, fadanPingtai, orderId, isCross
|
||||
);
|
||||
if (!localGroupId && orderId) {
|
||||
localGroupId = `group_${orderId}`;
|
||||
}
|
||||
if (!localGroupId) {
|
||||
throw new Error('无法确定群聊,请确认订单已接单且对方信息完整');
|
||||
}
|
||||
chatData = {
|
||||
groupId: localGroupId,
|
||||
orderId,
|
||||
groupName: groupName || '订单群聊',
|
||||
isCross: isCross || fadanPingtai || 0,
|
||||
};
|
||||
try {
|
||||
await request({
|
||||
url: '/dingdan/ltdhzb',
|
||||
method: 'POST',
|
||||
data: { dingdan_id: orderId, identityType, push_order_card: false },
|
||||
});
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
return chatData;
|
||||
}
|
||||
|
||||
class ConnectionManager {
|
||||
async connectAndChat(params) {
|
||||
const { identityType, userId, targetUser } = params;
|
||||
if (!identityType || !userId || !targetUser || !targetUser.id) {
|
||||
throw new Error('参数不完整');
|
||||
}
|
||||
|
||||
await ensureIdentityConnection(identityType, userId);
|
||||
|
||||
const currentUser = {
|
||||
id: userId,
|
||||
name: app.globalData.currentUser?.name || '用户',
|
||||
avatar: app.globalData.currentUser?.avatar || (app.globalData.ossImageUrl + app.globalData.morentouxiang),
|
||||
};
|
||||
|
||||
const to = {
|
||||
id: targetUser.id,
|
||||
name: targetUser.name || '用户',
|
||||
avatar: targetUser.avatar || (app.globalData.ossImageUrl + app.globalData.morentouxiang),
|
||||
};
|
||||
|
||||
const param = { to, currentUser };
|
||||
wx.navigateTo({ url: '/pages/chat/chat?data=' + encodeURIComponent(JSON.stringify(param)) });
|
||||
}
|
||||
|
||||
async connectToGroupChat(params) {
|
||||
const {
|
||||
identityType, userId, orderId, groupName, groupAvatar, isCross,
|
||||
partnerUid, fadanPingtai,
|
||||
} = params;
|
||||
|
||||
if (!identityType || !userId || !orderId) {
|
||||
throw new Error('参数不完整');
|
||||
}
|
||||
|
||||
app.globalData.currentRole = identityType;
|
||||
wx.setStorageSync('currentRole', identityType);
|
||||
|
||||
wx.showLoading({ title: '建立联系中...', mask: true });
|
||||
|
||||
try {
|
||||
const chatData = await prepareGroupChat(
|
||||
identityType, userId, orderId, partnerUid, fadanPingtai || isCross, groupName, isCross
|
||||
);
|
||||
|
||||
const realGroupId = chatData.groupId;
|
||||
let avatar = chatData.counterpartAvatar || chatData.groupAvatar || groupAvatar || '';
|
||||
if (avatar && !avatar.startsWith('http')) {
|
||||
avatar = app.globalData.ossImageUrl + avatar.replace(/^\//, '');
|
||||
}
|
||||
|
||||
if (!app.globalData.groupInfoMap) app.globalData.groupInfoMap = {};
|
||||
const meta = {
|
||||
name: chatData.counterpartName || chatData.groupName || groupName,
|
||||
avatar,
|
||||
orderId: chatData.orderId || orderId,
|
||||
orderDesc: chatData.orderDesc || '',
|
||||
orderZhuangtai: chatData.orderZhuangtai,
|
||||
dashouGoEasyId: chatData.dashouGoEasyId,
|
||||
partnerGoEasyId: chatData.partnerGoEasyId,
|
||||
dashouYonghuid: chatData.dashouYonghuid,
|
||||
partnerYonghuid: chatData.partnerYonghuid,
|
||||
dashouName: chatData.dashouName,
|
||||
partnerName: chatData.partnerName,
|
||||
dashouAvatar: chatData.dashouAvatar,
|
||||
partnerAvatar: chatData.partnerAvatar,
|
||||
counterpartId: chatData.counterpartId,
|
||||
counterpartYonghuid: chatData.counterpartYonghuid,
|
||||
};
|
||||
app.globalData.groupInfoMap[realGroupId] = meta;
|
||||
persistGroupMeta(app, realGroupId, meta);
|
||||
|
||||
let imReady = false;
|
||||
try {
|
||||
await ensureIdentityConnection(identityType, userId);
|
||||
imReady = true;
|
||||
try {
|
||||
await withTimeout(
|
||||
new Promise((resolve, reject) => {
|
||||
wx.goEasy.im.subscribeGroup({
|
||||
groupIds: [realGroupId],
|
||||
onSuccess: () => resolve(),
|
||||
onFailed: (error) => reject(error),
|
||||
});
|
||||
}),
|
||||
8000,
|
||||
'订阅群聊超时'
|
||||
);
|
||||
} catch (subErr) {
|
||||
console.warn('订阅群聊失败,仍尝试进入页面', subErr);
|
||||
}
|
||||
} catch (imErr) {
|
||||
console.warn('IM连接失败,仍进入聊天页由页面重连', imErr);
|
||||
}
|
||||
|
||||
const uid = userId.replace(/^(Ds|Sj|Boss)/, '');
|
||||
const param = {
|
||||
groupId: realGroupId,
|
||||
orderId: chatData.orderId || orderId,
|
||||
groupName: chatData.counterpartName || chatData.groupName || groupName || '订单群聊',
|
||||
groupAvatar: avatar,
|
||||
isCross: chatData.isCross != null ? chatData.isCross : (isCross || 0),
|
||||
currentUserId: userId,
|
||||
currentUserName: app.globalData.currentUser?.name || `用户${uid}`,
|
||||
currentUserAvatar: app.globalData.currentUser?.avatar ||
|
||||
(wx.getStorageSync('touxiang') || (app.globalData.ossImageUrl + app.globalData.morentouxiang)),
|
||||
orderZhuangtai: chatData.orderZhuangtai,
|
||||
orderJine: chatData.orderJine,
|
||||
orderDesc: chatData.orderDesc || '',
|
||||
imReady,
|
||||
};
|
||||
|
||||
wx.hideLoading();
|
||||
wx.navigateTo({
|
||||
url: '/pages/group-chat/group-chat?data=' + encodeURIComponent(JSON.stringify(param)),
|
||||
fail: (navErr) => {
|
||||
wx.showToast({ title: formatError(navErr), icon: 'none', duration: 2500 });
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
wx.hideLoading();
|
||||
console.error('跳转群聊失败:', err);
|
||||
wx.showToast({ title: formatError(err), icon: 'none', duration: 2500 });
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new ConnectionManager();
|
||||
/**
|
||||
* 订单群聊跳转:与文赫一致,新群用打手+商家/老板配对 groupId;旧 group_{订单号} 会话保留
|
||||
*/
|
||||
import request from './request';
|
||||
import { persistGroupMeta } from './im-user.js';
|
||||
import { resolveLocalGroupId } from './group-chat.js';
|
||||
|
||||
const app = getApp();
|
||||
|
||||
function formatError(err) {
|
||||
if (!err) return '进入聊天失败';
|
||||
if (typeof err === 'string') return err;
|
||||
if (err.message) return err.message;
|
||||
if (err.msg) return String(err.msg);
|
||||
if (err.content) return String(err.content);
|
||||
if (err.code) return `连接失败(${err.code})`;
|
||||
try {
|
||||
return JSON.stringify(err);
|
||||
} catch (e) {
|
||||
return '进入聊天失败';
|
||||
}
|
||||
}
|
||||
|
||||
function withTimeout(promise, ms, errMsg) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => reject(new Error(errMsg || '操作超时')), ms);
|
||||
promise
|
||||
.then((v) => { clearTimeout(timer); resolve(v); })
|
||||
.catch((e) => { clearTimeout(timer); reject(e); });
|
||||
});
|
||||
}
|
||||
|
||||
function mapIdentityForApi(identityType) {
|
||||
if (identityType === 'dashou') return 'dashou';
|
||||
if (identityType === 'shangjia') return 'shangjia';
|
||||
return 'normal';
|
||||
}
|
||||
|
||||
function validateIdentity(identityType, userId) {
|
||||
if (identityType === 'dashou' && !userId.startsWith('Ds')) {
|
||||
throw new Error('接单员 ID 必须以 Ds 开头');
|
||||
}
|
||||
if (identityType === 'shangjia' && !userId.startsWith('Sj')) {
|
||||
throw new Error('商家 ID 必须以 Sj 开头');
|
||||
}
|
||||
if ((identityType === 'boss' || identityType === 'normal') && !userId.startsWith('Boss')) {
|
||||
throw new Error('老板 ID 必须以 Boss 开头');
|
||||
}
|
||||
}
|
||||
|
||||
function getConnectedUserId() {
|
||||
const fromState = app.globalData?.goEasyConnection?.userId;
|
||||
const fromIm = wx.goEasy?.im?.userId;
|
||||
return fromState || fromIm || '';
|
||||
}
|
||||
|
||||
function isImConnected() {
|
||||
const status = wx.goEasy?.getConnectionStatus?.() || 'disconnected';
|
||||
return status === 'connected' || status === 'reconnected';
|
||||
}
|
||||
|
||||
async function ensureIdentityConnection(identityType, userId) {
|
||||
validateIdentity(identityType, userId);
|
||||
|
||||
app.globalData.currentRole = identityType;
|
||||
wx.setStorageSync('currentRole', identityType);
|
||||
|
||||
const uid = userId.replace(/^(Ds|Sj|Boss)/, '');
|
||||
const avatar = wx.getStorageSync('touxiang') ||
|
||||
(app.globalData.ossImageUrl + app.globalData.morentouxiang);
|
||||
app.globalData.currentUser = {
|
||||
id: userId,
|
||||
name: app.globalData.currentUser?.name || `用户${uid}`,
|
||||
avatar,
|
||||
};
|
||||
|
||||
if (!app.globalData.chatEnabled || !wx.goEasy?.im) {
|
||||
throw new Error('聊天服务未就绪,请稍后重试');
|
||||
}
|
||||
|
||||
if (isImConnected() && getConnectedUserId() === userId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const connectedId = getConnectedUserId();
|
||||
if (connectedId && connectedId !== userId && app.disconnectGoEasy) {
|
||||
await app.disconnectGoEasy();
|
||||
}
|
||||
|
||||
if (!app.connectWithIdentity) {
|
||||
throw new Error('聊天功能未初始化');
|
||||
}
|
||||
|
||||
await withTimeout(
|
||||
app.connectWithIdentity(identityType, userId, true),
|
||||
12000,
|
||||
'IM连接超时'
|
||||
);
|
||||
|
||||
if (!isImConnected()) {
|
||||
throw new Error('IM连接失败,请检查网络后重试');
|
||||
}
|
||||
}
|
||||
|
||||
async function prepareGroupChat(identityType, userId, orderId, partnerUid, fadanPingtai, groupName, isCross) {
|
||||
const orderIdStr = String(orderId || '').replace(/^group_/, '');
|
||||
let chatData = null;
|
||||
|
||||
try {
|
||||
const res = await withTimeout(
|
||||
request({
|
||||
url: '/dingdan/ltdhzb',
|
||||
method: 'POST',
|
||||
data: {
|
||||
dingdan_id: orderIdStr,
|
||||
identityType: mapIdentityForApi(identityType),
|
||||
push_order_card: false,
|
||||
},
|
||||
}),
|
||||
12000,
|
||||
'准备群聊超时'
|
||||
);
|
||||
const body = res?.data || {};
|
||||
if ((body.code === 0 || body.code === 200) && body.data?.groupId) {
|
||||
chatData = body.data;
|
||||
} else if (body.msg) {
|
||||
console.warn('ltdhzb:', body.msg);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('ltdhzb 请求失败,尝试本地配对 groupId', e);
|
||||
}
|
||||
|
||||
if (!chatData?.groupId) {
|
||||
const myUid = userId.replace(/^(Ds|Sj|Boss)/, '');
|
||||
let localGroupId = resolveLocalGroupId(
|
||||
identityType, myUid, partnerUid, fadanPingtai, orderIdStr, isCross
|
||||
);
|
||||
if (!localGroupId && orderIdStr) {
|
||||
localGroupId = `group_${orderIdStr}`;
|
||||
}
|
||||
if (!localGroupId) {
|
||||
throw new Error('无法确定群聊,请确认订单已接单且对方信息完整');
|
||||
}
|
||||
chatData = {
|
||||
groupId: localGroupId,
|
||||
orderId: orderIdStr,
|
||||
groupName: groupName || '订单群聊',
|
||||
isCross: isCross || fadanPingtai || 0,
|
||||
};
|
||||
try {
|
||||
await request({
|
||||
url: '/dingdan/ltdhzb',
|
||||
method: 'POST',
|
||||
data: {
|
||||
dingdan_id: orderIdStr,
|
||||
identityType: mapIdentityForApi(identityType),
|
||||
push_order_card: false,
|
||||
},
|
||||
});
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
return chatData;
|
||||
}
|
||||
|
||||
class ConnectionManager {
|
||||
async connectAndChat(params) {
|
||||
const { identityType, userId, targetUser } = params;
|
||||
if (!identityType || !userId || !targetUser || !targetUser.id) {
|
||||
throw new Error('参数不完整');
|
||||
}
|
||||
|
||||
await ensureIdentityConnection(identityType, userId);
|
||||
|
||||
const currentUser = {
|
||||
id: userId,
|
||||
name: app.globalData.currentUser?.name || '用户',
|
||||
avatar: app.globalData.currentUser?.avatar || (app.globalData.ossImageUrl + app.globalData.morentouxiang),
|
||||
};
|
||||
|
||||
const to = {
|
||||
id: targetUser.id,
|
||||
name: targetUser.name || '用户',
|
||||
avatar: targetUser.avatar || (app.globalData.ossImageUrl + app.globalData.morentouxiang),
|
||||
};
|
||||
|
||||
wx.navigateTo({
|
||||
url: '/pages/chat/chat?data=' + encodeURIComponent(JSON.stringify({ to, currentUser })),
|
||||
});
|
||||
}
|
||||
|
||||
async connectToGroupChat(params) {
|
||||
const {
|
||||
identityType, userId, orderId, groupName, groupAvatar, isCross,
|
||||
partnerUid, fadanPingtai,
|
||||
} = params;
|
||||
|
||||
if (!identityType || !userId || !orderId) {
|
||||
throw new Error('参数不完整');
|
||||
}
|
||||
|
||||
app.globalData.currentRole = identityType;
|
||||
wx.setStorageSync('currentRole', identityType);
|
||||
|
||||
wx.showLoading({ title: '建立联系中...', mask: true });
|
||||
|
||||
try {
|
||||
const chatData = await prepareGroupChat(
|
||||
identityType, userId, orderId, partnerUid, fadanPingtai || isCross, groupName, isCross
|
||||
);
|
||||
|
||||
const realGroupId = chatData.groupId;
|
||||
let avatar = chatData.counterpartAvatar || chatData.groupAvatar || groupAvatar || '';
|
||||
if (avatar && !avatar.startsWith('http')) {
|
||||
avatar = app.globalData.ossImageUrl + avatar.replace(/^\//, '');
|
||||
}
|
||||
|
||||
if (!app.globalData.groupInfoMap) app.globalData.groupInfoMap = {};
|
||||
const meta = {
|
||||
name: chatData.counterpartName || chatData.groupName || groupName,
|
||||
avatar,
|
||||
orderId: chatData.orderId || String(orderId).replace(/^group_/, ''),
|
||||
orderDesc: chatData.orderDesc || '',
|
||||
orderZhuangtai: chatData.orderZhuangtai,
|
||||
orderJine: chatData.orderJine,
|
||||
dashouGoEasyId: chatData.dashouGoEasyId,
|
||||
partnerGoEasyId: chatData.partnerGoEasyId,
|
||||
dashouYonghuid: chatData.dashouYonghuid,
|
||||
partnerYonghuid: chatData.partnerYonghuid,
|
||||
dashouName: chatData.dashouName,
|
||||
partnerName: chatData.partnerName,
|
||||
dashouAvatar: chatData.dashouAvatar,
|
||||
partnerAvatar: chatData.partnerAvatar,
|
||||
counterpartId: chatData.counterpartId,
|
||||
counterpartYonghuid: chatData.counterpartYonghuid,
|
||||
};
|
||||
app.globalData.groupInfoMap[realGroupId] = meta;
|
||||
persistGroupMeta(app, realGroupId, meta);
|
||||
|
||||
let imReady = false;
|
||||
try {
|
||||
await ensureIdentityConnection(identityType, userId);
|
||||
imReady = true;
|
||||
try {
|
||||
await withTimeout(
|
||||
new Promise((resolve, reject) => {
|
||||
wx.goEasy.im.subscribeGroup({
|
||||
groupIds: [realGroupId],
|
||||
onSuccess: () => resolve(),
|
||||
onFailed: (error) => reject(error),
|
||||
});
|
||||
}),
|
||||
8000,
|
||||
'订阅群聊超时'
|
||||
);
|
||||
} catch (subErr) {
|
||||
console.warn('订阅群聊失败,仍尝试进入页面', subErr);
|
||||
}
|
||||
} catch (imErr) {
|
||||
console.warn('IM连接失败,仍进入聊天页由页面重连', imErr);
|
||||
}
|
||||
|
||||
const uid = userId.replace(/^(Ds|Sj|Boss)/, '');
|
||||
const param = {
|
||||
groupId: realGroupId,
|
||||
orderId: chatData.orderId || String(orderId).replace(/^group_/, ''),
|
||||
groupName: chatData.counterpartName || chatData.groupName || groupName || '订单群聊',
|
||||
groupAvatar: avatar,
|
||||
isCross: chatData.isCross != null ? chatData.isCross : (isCross || 0),
|
||||
currentUserId: userId,
|
||||
currentUserName: app.globalData.currentUser?.name || `用户${uid}`,
|
||||
currentUserAvatar: app.globalData.currentUser?.avatar ||
|
||||
(wx.getStorageSync('touxiang') || (app.globalData.ossImageUrl + app.globalData.morentouxiang)),
|
||||
orderZhuangtai: chatData.orderZhuangtai,
|
||||
orderJine: chatData.orderJine,
|
||||
orderDesc: chatData.orderDesc || '',
|
||||
imReady,
|
||||
};
|
||||
|
||||
wx.hideLoading();
|
||||
wx.navigateTo({
|
||||
url: '/pages/group-chat/group-chat?data=' + encodeURIComponent(JSON.stringify(param)),
|
||||
fail: (navErr) => {
|
||||
wx.showToast({ title: formatError(navErr), icon: 'none', duration: 2500 });
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
wx.hideLoading();
|
||||
console.error('跳转群聊失败:', err);
|
||||
wx.showToast({ title: formatError(err), icon: 'none', duration: 2500 });
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new ConnectionManager();
|
||||
|
||||
53
utils/zhiding-order.js
Normal file
53
utils/zhiding-order.js
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user