backup: 紫色UI换肤前完整备份(当前橙色逍遥梦主题)

保留改造前全部页面样式与功能代码,便于回滚。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-06-29 01:59:42 +08:00
parent a751708d6e
commit c03d22776f
49 changed files with 6025 additions and 3784 deletions

839
app.js
View File

@@ -1,414 +1,427 @@
// app.js // app.js
import GoEasy from './static/lib/goeasy-2.13.24.esm.min'; import GoEasy from './static/lib/goeasy-2.13.24.esm.min';
const ChatCore = require('./utils/chat-core'); const ChatCore = require('./utils/chat-core');
import { check } from './utils/phone-auth'; import { ensurePhoneAuth } from './utils/phone-auth';
import { getPrimaryRole, lockPrimaryRole, migrateLegacyCenterRole, PRIMARY_DEFAULT_PAGES } from './utils/primary-role'; import { getPrimaryRole, lockPrimaryRole, migrateLegacyCenterRole, PRIMARY_DEFAULT_PAGES } from './utils/primary-role';
// 三端固定首页 // 三端固定首页
const roleDefaultPage = PRIMARY_DEFAULT_PAGES; const roleDefaultPage = PRIMARY_DEFAULT_PAGES;
App({ App({
globalData: { globalData: {
hasShownPopupOnColdStart: false, hasShownPopupOnColdStart: false,
apiBaseUrl: 'https://www.abas.asia/hqhd', apiBaseUrl: 'https://www.abas.asia/hqhd',
ossImageUrl: '', ossImageUrl: '',
morentouxiang: '', morentouxiang: '',
dashouguize: '', dashouguize: '',
shangpinliebiao: [], shangpinliebiao: [],
shangpinzhuanqu: [], shangpinzhuanqu: [],
shangpinleixing: [], shangpinleixing: [],
shangpinlunbo: [], shangpinlunbo: [],
shangpingonggao: '', shangpingonggao: '',
lunbozhanwei: '/images/lunbozhanwei.jpg', lunbozhanwei: '/images/lunbozhanwei.jpg',
dingdanTiaoshu: { dingdanTiaoshu: {
daifuwu: 0, daifuwu: 0,
fuwuzhong: 0, fuwuzhong: 0,
yiwancheng: 0, yiwancheng: 0,
yituikuan: 0 yituikuan: 0
}, },
dashouqun: '', dashouqun: '',
dashouqunid: '', dashouqunid: '',
guanshiqun: '', guanshiqun: '',
guanshiqunid: '', guanshiqunid: '',
appId:'wx0e4be86faac4a8d1', appId:'wx0e4be86faac4a8d1',
shangjiastatus: 0, shangjiastatus: 0,
dashoustatus: 0, dashoustatus: 0,
guanshistatus: 0, guanshistatus: 0,
dashouNicheng: '', dashouNicheng: '',
zhanghaoStatus: null, zhanghaoStatus: null,
dashouzhuangtai: null, dashouzhuangtai: null,
yongjin: null, yongjin: null,
zonge: null, zonge: null,
yajin: null, yajin: null,
chenghao: '', chenghao: '',
jinfen: null, jinfen: null,
clumber: [{ clumber: [{
huiyuanid: '', huiyuanid: '',
huiyuanming: '', huiyuanming: '',
daoqi: '' daoqi: ''
}], }],
chengjiaoliang: null, chengjiaoliang: null,
zaixianZhuangtai: null, zaixianZhuangtai: null,
guanshi: { guanshi: {
nicheng: '', nicheng: '',
uid: '', uid: '',
touxiang: '', touxiang: '',
gszhstatus: '', gszhstatus: '',
yaoqingzongshu: 0, yaoqingzongshu: 0,
fenyongzonge: '0.00', fenyongzonge: '0.00',
fenyongtixian: '0.00' fenyongtixian: '0.00'
}, },
shangjia: { shangjia: {
sjzhzhuangtai: '', sjzhzhuangtai: '',
sjyue: '', sjyue: '',
fadanzong: null, fadanzong: null,
tuikuanzong: null, tuikuanzong: null,
riliushui: '', riliushui: '',
yueliushui: '' yueliushui: ''
}, },
liaotian_liebiao: [], liaotian_liebiao: [],
xshenfen: 1, xshenfen: 1,
goEasyConfig: null, goEasyConfig: null,
kefuConfig: { kefuConfig: {
link: '', link: '',
enterpriseId: '' enterpriseId: ''
}, },
cosConfig: { cosConfig: {
bucket: '', bucket: '',
region: 'ap-shanghai', region: 'ap-shanghai',
uploadPathPrefix: 'order/' uploadPathPrefix: 'order/'
}, },
goEasyConnection: { goEasyConnection: {
status: 'disconnected', status: 'disconnected',
userId: '', userId: '',
identityType: '', identityType: '',
lastConnectTime: 0, lastConnectTime: 0,
autoReconnect: true, autoReconnect: true,
reconnectAttempts: 0, reconnectAttempts: 0,
maxReconnectAttempts: 5, maxReconnectAttempts: 5,
heartbeatInterval: null, heartbeatInterval: null,
cacheKeys: { cacheKeys: {
savedConnection: 'savedGoEasyConnection', savedConnection: 'savedGoEasyConnection',
userId: 'goEasyUserId', userId: 'goEasyUserId',
identityType: 'currentGoEasyIdentity', identityType: 'currentGoEasyIdentity',
connectTime: 'goEasyConnectTime' connectTime: 'goEasyConnectTime'
}, },
config: { config: {
heartbeatInterval: 20000, heartbeatInterval: 20000,
reconnectDelay: 3000, reconnectDelay: 3000,
offlineTimeout: 30000, offlineTimeout: 30000,
cacheValidityHours: 12 cacheValidityHours: 12
} }
}, },
messageManager: { messageManager: {
unreadTotal: 0, unreadTotal: 0,
unreadMap: {}, unreadMap: {},
latestMessages: [], latestMessages: [],
notificationQueue: [], notificationQueue: [],
notificationVisible: false, notificationVisible: false,
lastNotificationTime: 0, lastNotificationTime: 0,
notificationCooldown: 3000, notificationCooldown: 3000,
tabBarIndex: 2, tabBarIndex: 2,
showTabBarBadge: true, showTabBarBadge: true,
customTabBar: true, customTabBar: true,
tabBarBadgeText: 0, tabBarBadgeText: 0,
soundEnabled: true, soundEnabled: true,
vibrationEnabled: true, vibrationEnabled: true,
doNotDisturb: false, doNotDisturb: false,
doNotDisturbStart: '22:00', doNotDisturbStart: '22:00',
doNotDisturbEnd: '08:00', doNotDisturbEnd: '08:00',
notificationStyle: { notificationStyle: {
position: 'top', position: 'top',
duration: 3000, duration: 3000,
backgroundColor: '#00f7ff', backgroundColor: '#00f7ff',
textColor: '#ffffff', textColor: '#ffffff',
borderRadius: '16rpx', borderRadius: '16rpx',
boxShadow: '0 10rpx 30rpx rgba(0, 247, 255, 0.3)' boxShadow: '0 10rpx 30rpx rgba(0, 247, 255, 0.3)'
}, },
currentNotification: null, currentNotification: null,
cacheKeys: { cacheKeys: {
messageSettings: 'messageSettings', messageSettings: 'messageSettings',
unreadTotal: 'messageUnreadTotal', unreadTotal: 'messageUnreadTotal',
latestMessages: 'latestMessages', latestMessages: 'latestMessages',
notificationMuted: 'notificationMuted' notificationMuted: 'notificationMuted'
} }
}, },
pageState: { pageState: {
currentPage: '', currentPage: '',
isInChatPage: false, isInChatPage: false,
currentChatId: '', currentChatId: '',
lastPageUpdate: 0 lastPageUpdate: 0
}, },
eventListeners: {}, eventListeners: {},
debugMode: false, debugMode: false,
currentUser: null, currentUser: null,
currentRole: 'normal', currentRole: 'normal',
primaryRole: 'normal' primaryRole: 'normal'
}, },
// 核心启动流程 // 核心启动流程
async onLaunch() { async onLaunch() {
// ① 隐藏返回首页按钮 // ① 隐藏返回首页按钮
wx.hideHomeButton(); wx.hideHomeButton();
wx.onAppRoute((res) => { wx.onAppRoute((res) => {
wx.hideHomeButton(); wx.hideHomeButton();
const path = (res && res.path) || ''; const path = (res && res.path) || '';
if (path === 'pages/manager/manager' || path === 'pages/leader/leader') { if (path === 'pages/manager/manager' || path === 'pages/leader/leader') {
migrateLegacyCenterRole(this); migrateLegacyCenterRole(this);
lockPrimaryRole('dashou', this); lockPrimaryRole('dashou', this);
} }
}); });
// ② 恢复主端并跳转 // ② 已登录时先恢复子客服身份,再决定主端 Tab避免客服被当成未入驻
migrateLegacyCenterRole(this); if (wx.getStorageSync('token')) {
const savedRole = getPrimaryRole(this); try {
lockPrimaryRole(savedRole, this); const { restoreStaffContextAfterAuth } = require('./utils/staff-api.js');
await restoreStaffContextAfterAuth();
const targetPage = roleDefaultPage[savedRole] || '/pages/index/index'; } catch (e) {}
if (savedRole !== 'normal') { }
setTimeout(() => {
wx.reLaunch({ url: targetPage }); migrateLegacyCenterRole(this);
}, 0); const savedRole = getPrimaryRole(this);
} lockPrimaryRole(savedRole, this);
// ③ 应用本地缓存配置 // ②b 冷启动:有 token 时先问后端是否需要手机号认证
const cachedConfig = this.readConfigFromStorage(); if (wx.getStorageSync('token')) {
if (cachedConfig) { const phoneOk = await ensurePhoneAuth({ redirect: true });
this.applyDynamicConfig(cachedConfig); if (!phoneOk) return;
} }
// ④ 初始化 const targetPage = roleDefaultPage[savedRole] || '/pages/index/index';
this.initGoEasyWithConfig(); // 只有appkey有效时才真正初始化 if (savedRole !== 'normal') {
ChatCore.initGlobalMessageSystem(this); setTimeout(() => {
this.initCurrentUser(); wx.reLaunch({ url: targetPage });
}, 0);
// ⑤ 建立连接 }
const saved = this.getSavedConnection();
console.log('【启动】缓存身份:', saved ? saved.identityType : '无', // ③ 应用本地缓存配置
'userId:', saved ? saved.userId : '无'); const cachedConfig = this.readConfigFromStorage();
setTimeout(() => { if (cachedConfig) {
if (this.startImWhenReady) this.startImWhenReady(); this.applyDynamicConfig(cachedConfig);
else if (this.ensureConnection) this.ensureConnection(); }
}, 300);
// ④ 初始化
// ⑥ 获取远程配置 this.initGoEasyWithConfig(); // 只有appkey有效时才真正初始化
this.fetchConfigSafely() ChatCore.initGlobalMessageSystem(this);
.then(latestConfig => { this.initCurrentUser();
this.saveConfigToStorage(latestConfig); if (wx.getStorageSync('token') && this.emitEvent) {
this.applyDynamicConfig(latestConfig); this.emitEvent('staffContextChanged', {});
this.initGoEasyWithConfig(); }
this.initCurrentUser();
if (this.startImWhenReady) this.startImWhenReady(); // ⑤ 建立连接
else if (this.connectForCurrentRole) this.connectForCurrentRole(); const saved = this.getSavedConnection();
console.log('远程配置更新完成'); console.log('【启动】缓存身份:', saved ? saved.identityType : '无',
}) 'userId:', saved ? saved.userId : '无');
.catch(err => { setTimeout(() => {
console.warn('远程配置获取失败,继续使用本地缓存', err); if (this.startImWhenReady) this.startImWhenReady();
}); else if (this.ensureConnection) this.ensureConnection();
}, 300);
// ⑦ 手机号认证检查
try { // ⑥ 获取远程配置
const needAuth = await check(); this.fetchConfigSafely()
if (needAuth) { .then(latestConfig => {
wx.reLaunch({ url: '/pages/phone-auth/phone-auth' }); this.saveConfigToStorage(latestConfig);
return; this.applyDynamicConfig(latestConfig);
} this.initGoEasyWithConfig();
} catch (e) {} this.initCurrentUser();
}, if (this.startImWhenReady) this.startImWhenReady();
else if (this.connectForCurrentRole) this.connectForCurrentRole();
// 连接管理方法 console.log('远程配置更新完成');
getSavedConnection() { })
try { .catch(err => {
const saved = wx.getStorageSync( console.warn('远程配置获取失败,继续使用本地缓存', err);
this.globalData.goEasyConnection.cacheKeys.savedConnection });
); },
return saved || null;
} catch (e) { /** 从后台切回前台时再次向后端校验 */
return null; async onShow() {
} if (!wx.getStorageSync('token')) return;
}, await ensurePhoneAuth({ redirect: true });
},
// 配置缓存读写
CONFIG_CACHE_KEY: 'app_dynamic_config', // 连接管理方法
getSavedConnection() {
readConfigFromStorage() { try {
try { const saved = wx.getStorageSync(
const data = wx.getStorageSync(this.CONFIG_CACHE_KEY); this.globalData.goEasyConnection.cacheKeys.savedConnection
if (data && typeof data === 'object') { );
return data; return saved || null;
} } catch (e) {
} catch (e) {} return null;
return null; }
}, },
saveConfigToStorage(config) { // 配置缓存读写
try { CONFIG_CACHE_KEY: 'app_dynamic_config',
if (config && typeof config === 'object') {
wx.setStorageSync(this.CONFIG_CACHE_KEY, config); readConfigFromStorage() {
} try {
} catch (e) {} const data = wx.getStorageSync(this.CONFIG_CACHE_KEY);
}, if (data && typeof data === 'object') {
return data;
// 获取远程配置 }
fetchConfigSafely() { } catch (e) {}
return new Promise((resolve) => { return null;
wx.request({ },
url: this.globalData.apiBaseUrl + '/peizhi/qdpzhq',
method: 'POST', saveConfigToStorage(config) {
header: { 'content-type': 'application/json' }, try {
success: (res) => { if (config && typeof config === 'object') {
if (res.statusCode === 200 && res.data && res.data.code === 0 && res.data.data) { wx.setStorageSync(this.CONFIG_CACHE_KEY, config);
resolve(res.data.data); }
} else { } catch (e) {}
console.warn('远程配置接口返回异常,使用已有配置'); },
resolve(this.readConfigFromStorage() || {});
} // 获取远程配置
}, fetchConfigSafely() {
fail: (err) => { return new Promise((resolve) => {
console.warn('远程配置网络请求失败,使用已有配置', err); wx.request({
resolve(this.readConfigFromStorage() || {}); url: this.globalData.apiBaseUrl + '/peizhi/qdpzhq',
} method: 'POST',
}); header: { 'content-type': 'application/json' },
}); success: (res) => {
}, if (res.statusCode === 200 && res.data && res.data.code === 0 && res.data.data) {
resolve(res.data.data);
// 获取远程配置 } else {
fetchDynamicConfig() { console.warn('远程配置接口返回异常,使用已有配置');
return new Promise((resolve, reject) => { resolve(this.readConfigFromStorage() || {});
wx.request({ }
url: this.globalData.apiBaseUrl + '/peizhi/qdpzhq', },
method: 'POST', fail: (err) => {
header: { 'content-type': 'application/json' }, console.warn('远程配置网络请求失败,使用已有配置', err);
success: (res) => { resolve(this.readConfigFromStorage() || {});
if (res.statusCode === 200 && res.data && res.data.code === 0) { }
resolve(res.data.data); });
} else { });
reject(new Error(res.data?.msg || '接口返回错误')); },
}
}, // 获取远程配置
fail: reject fetchDynamicConfig() {
}); return new Promise((resolve, reject) => {
}); wx.request({
}, url: this.globalData.apiBaseUrl + '/peizhi/qdpzhq',
method: 'POST',
applyDynamicConfig(config) { header: { 'content-type': 'application/json' },
if (!config || typeof config !== 'object') return; success: (res) => {
if (res.statusCode === 200 && res.data && res.data.code === 0) {
if (config.cos) { resolve(res.data.data);
this.globalData.ossImageUrl = config.cos.ossImageUrl || ''; } else {
this.globalData.cosConfig = { reject(new Error(res.data?.msg || '接口返回错误'));
bucket: config.cos.bucket || '', }
region: config.cos.region || 'ap-shanghai', },
uploadPathPrefix: config.cos.uploadPathPrefix || 'order/' fail: reject
}; });
} });
},
if (config.goEasy) {
this.globalData.goEasyConfig = { applyDynamicConfig(config) {
host: config.goEasy.host || 'hangzhou.goeasy.io', if (!config || typeof config !== 'object') return;
appkey: config.goEasy.appkey || ''
}; if (config.cos) {
} this.globalData.ossImageUrl = config.cos.ossImageUrl || '';
this.globalData.cosConfig = {
if (config.otherConfig) { bucket: config.cos.bucket || '',
this.globalData.morentouxiang = config.otherConfig.morentouxiang || ''; region: config.cos.region || 'ap-shanghai',
this.globalData.dashouguize = config.otherConfig.dashouguize || ''; uploadPathPrefix: config.cos.uploadPathPrefix || 'order/'
} };
}
if (config.kefu) {
this.globalData.kefuConfig = { if (config.goEasy) {
link: config.kefu.link || '', this.globalData.goEasyConfig = {
enterpriseId: config.kefu.enterpriseId || '' host: config.goEasy.host || 'hangzhou.goeasy.io',
}; appkey: config.goEasy.appkey || ''
} };
}, }
// 仅在有效 appkey 时初始化 if (config.otherConfig) {
initGoEasyWithConfig() { this.globalData.morentouxiang = config.otherConfig.morentouxiang || '';
const cfg = this.globalData.goEasyConfig; this.globalData.dashouguize = config.otherConfig.dashouguize || '';
if (!cfg || !cfg.appkey) { }
console.warn('GoEasy appkey 未配置,聊天功能将在配置就绪后自动恢复');
return; if (config.kefu) {
} this.globalData.kefuConfig = {
link: config.kefu.link || '',
// 避免重复初始化 enterpriseId: config.kefu.enterpriseId || ''
if (wx.goEasy && wx.GoEasy) { };
console.log('GoEasy 已初始化,跳过'); }
return; },
}
// 仅在有效 appkey 时初始化
try { initGoEasyWithConfig() {
wx.goEasy = GoEasy.getInstance({ const cfg = this.globalData.goEasyConfig;
host: cfg.host || 'hangzhou.goeasy.io', if (!cfg || !cfg.appkey) {
appkey: cfg.appkey, console.warn('GoEasy appkey 未配置,聊天功能将在配置就绪后自动恢复');
modules: ['im', 'pubsub'] return;
}); }
wx.GoEasy = GoEasy;
console.log('GoEasy 初始化成功'); // 避免重复初始化
if (this.startImWhenReady && wx.getStorageSync('uid')) { if (wx.goEasy && wx.GoEasy) {
setTimeout(() => this.startImWhenReady(), 100); console.log('GoEasy 已初始化,跳过');
} return;
} catch (error) { }
console.error('GoEasy 初始化失败:', error);
} try {
}, wx.goEasy = GoEasy.getInstance({
host: cfg.host || 'hangzhou.goeasy.io',
initCurrentUser() { appkey: cfg.appkey,
const uid = wx.getStorageSync('uid'); modules: ['im', 'pubsub']
if (uid) { });
this.globalData.currentUser = { wx.GoEasy = GoEasy;
id: uid, console.log('GoEasy 初始化成功');
name: '用户' + uid.substring(0, 6), if (this.startImWhenReady && wx.getStorageSync('uid')) {
avatar: this.globalData.ossImageUrl + this.globalData.morentouxiang setTimeout(() => this.startImWhenReady(), 100);
}; }
} } catch (error) {
}, console.error('GoEasy 初始化失败:', error);
}
emitEvent(eventName, data) { },
if (this.globalData.eventListeners[eventName]) {
this.globalData.eventListeners[eventName].forEach(callback => { initCurrentUser() {
try { callback(data); } catch (error) { console.error(error); } const uid = wx.getStorageSync('uid');
}); if (uid) {
} this.globalData.currentUser = {
}, id: uid,
name: '用户' + uid.substring(0, 6),
/** 切换角色并通知刷新 */ avatar: this.globalData.ossImageUrl + this.globalData.morentouxiang
setCurrentRole(role) { };
if (!role) return; }
this.globalData.currentRole = role; },
wx.setStorageSync('currentRole', role);
this.emitEvent('currentRoleChanged', { role }); emitEvent(eventName, data) {
}, if (this.globalData.eventListeners[eventName]) {
this.globalData.eventListeners[eventName].forEach(callback => {
on(eventName, callback) { try { callback(data); } catch (error) { console.error(error); }
if (!this.globalData.eventListeners[eventName]) { });
this.globalData.eventListeners[eventName] = []; }
} },
this.globalData.eventListeners[eventName].push(callback);
}, /** 切换角色并通知刷新 */
setCurrentRole(role) {
off(eventName, callback) { if (!role) return;
if (this.globalData.eventListeners[eventName]) { this.globalData.currentRole = role;
const index = this.globalData.eventListeners[eventName].indexOf(callback); wx.setStorageSync('currentRole', role);
if (index > -1) { this.emitEvent('currentRoleChanged', { role });
this.globalData.eventListeners[eventName].splice(index, 1); },
}
} on(eventName, callback) {
} if (!this.globalData.eventListeners[eventName]) {
this.globalData.eventListeners[eventName] = [];
}
this.globalData.eventListeners[eventName].push(callback);
},
off(eventName, callback) {
if (this.globalData.eventListeners[eventName]) {
const index = this.globalData.eventListeners[eventName].indexOf(callback);
if (index > -1) {
this.globalData.eventListeners[eventName].splice(index, 1);
}
}
}
}); });

View File

@@ -13,6 +13,9 @@
"pages/merchant-home/merchant-home", "pages/merchant-home/merchant-home",
"pages/merchant-kefu-key/merchant-kefu-key", "pages/merchant-kefu-key/merchant-kefu-key",
"pages/merchant-kefu-list/merchant-kefu-list", "pages/merchant-kefu-list/merchant-kefu-list",
"pages/staff-join/staff-join",
"pages/merchant-staff-audit/merchant-staff-audit",
"pages/merchant-staff-role/merchant-staff-role",
"pages/manager/manager", "pages/manager/manager",
"pages/orders/orders", "pages/orders/orders",
"pages/order-detail/order-detail", "pages/order-detail/order-detail",

View File

@@ -3,6 +3,7 @@ const app = getApp();
import request from '../../utils/request.js'; import request from '../../utils/request.js';
import PopupService from '../../services/popupService.js'; import PopupService from '../../services/popupService.js';
import { reconnectForRole } from '../../utils/role-tab-bar.js'; import { reconnectForRole } from '../../utils/role-tab-bar.js';
import { ensurePhoneAuth } from '../../utils/phone-auth.js';
Page({ Page({
data: { data: {
@@ -62,7 +63,10 @@ Page({
} }
}, },
onShow() { async onShow() {
const phoneOk = await ensurePhoneAuth({ redirect: true, role: 'dashou' });
if (!phoneOk) return;
this.syncShopBannerFromGlobal(); this.syncShopBannerFromGlobal();
this.registerNotificationComponent(); this.registerNotificationComponent();
this.loadGlobalStatus(); this.loadGlobalStatus();

File diff suppressed because it is too large Load Diff

View File

@@ -2,12 +2,13 @@
<view class="page-container sj-form-theme"> <view class="page-container sj-form-theme">
<!-- 余额卡片 --> <!-- 余额卡片 -->
<view class="balance-card"> <view class="balance-card">
<text class="balance-label">可用余额</text> <text class="balance-label">{{isStaffMode ? '可用额度' : '可用余额'}}</text>
<view class="balance-amount"> <view class="balance-amount">
<text class="amount-value">{{sjyue}}</text> <text class="amount-value">{{sjyue}}</text>
<text class="amount-unit">元</text> <text class="amount-unit">元</text>
</view> </view>
<text class="balance-tip">生成链接从此额扣除</text> <text class="balance-tip" wx:if="{{isStaffMode}}">当前身份:{{staffRoleName || '客服'}} · 生成链接从此额扣除</text>
<text class="balance-tip" wx:else>生成链接将从此余额扣除</text>
</view> </view>
<!-- 商品类型选择 --> <!-- 商品类型选择 -->
@@ -126,6 +127,7 @@
<block wx:for="{{detailLinkList}}" wx:key="id"> <block wx:for="{{detailLinkList}}" wx:key="id">
<view class="link-item"> <view class="link-item">
<text class="link-url" selectable="true">{{item.lianjie}}</text> <text class="link-url" selectable="true">{{item.lianjie}}</text>
<text class="link-operator" wx:if="{{item.roleName || item.displayName}}">{{item.roleName || item.displayName}}</text>
<text class="link-status">{{item.is_used ? '已使用' : '未使用'}}</text> <text class="link-status">{{item.is_used ? '已使用' : '未使用'}}</text>
<view wx:if="{{!item.is_used}}" class="copy-link-btn" bindtap="onCopyLinkItem" data-url="{{item.lianjie}}">复制</view> <view wx:if="{{!item.is_used}}" class="copy-link-btn" bindtap="onCopyLinkItem" data-url="{{item.lianjie}}">复制</view>
</view> </view>

File diff suppressed because it is too large Load Diff

View File

@@ -56,7 +56,7 @@
<view class="vip-price"> <view class="vip-price">
<text class="price-symbol">¥</text> <text class="price-symbol">¥</text>
<text class="price-number">{{item.jiage}}</text> <text class="price-number">{{item.jiage}}</text>
<text class="price-unit">/30天</text> <text class="price-unit">/{{item.formal_days || 30}}天</text>
</view> </view>
<view class="vip-features"> <view class="vip-features">
@@ -107,20 +107,24 @@
<!-- 🔥 将“机甲能源”改为“价格” --> <!-- 🔥 将“机甲能源”改为“价格” -->
<text class="price-label">价格:</text> <text class="price-label">价格:</text>
<text class="price-value">¥{{currentHuiyuan.jiage}}</text> <text class="price-value">¥{{currentHuiyuan.jiage}}</text>
<text class="price-days">/{{currentHuiyuan.formal_days || 30}}天</text>
</view> </view>
<text class="buy-desc" wx:if="{{currentHuiyuan.isBought && currentHuiyuan.buyInfo}}"> <text class="buy-desc" wx:if="{{currentHuiyuan.isBought && currentHuiyuan.buyInfo}}">
已激活,<text class="expire-date">{{currentHuiyuan.buyInfo.daoqi}}</text>后到期 已激活,<text class="expire-date">{{currentHuiyuan.buyInfo.daoqi}}</text>后到期
</text> </text>
<text class="buy-desc" wx:else>激活会员特权</text> <text class="buy-desc" wx:else>正式会员支持微信或余额抵扣</text>
<text class="buy-desc trial-hint" wx:if="{{currentHuiyuan.can_buy_trial}}">
体验版仅微信支付 ¥{{currentHuiyuan.trial_price}}/{{currentHuiyuan.trial_days}}天终身1次
</text>
</view> </view>
<view class="buy-action"> <view class="buy-action">
<view class="tech-buy-btn {{currentHuiyuan.isBought ? 'renew' : ''}}" <view class="tech-buy-btn {{currentHuiyuan.isBought ? 'renew' : ''}}"
bindtap="onBuyHuiyuanClick" data-id="{{currentHuiyuan.id}}"> bindtap="onBuyHuiyuanClick" data-id="{{currentHuiyuan.id}}" data-trial="false">
<view class="buy-btn-bg"></view> <view class="buy-btn-bg"></view>
<view class="buy-btn-glow"></view> <view class="buy-btn-glow"></view>
<text class="buy-btn-text"> <text class="buy-btn-text">
{{currentHuiyuan.isBought ? '立即续费' : '立即激活'}} {{currentHuiyuan.isBought ? '正式续费' : '购买正式版'}}
</text> </text>
<view class="buy-btn-energy"> <view class="buy-btn-energy">
<view class="energy-dot"></view> <view class="energy-dot"></view>
@@ -128,6 +132,16 @@
<view class="energy-dot"></view> <view class="energy-dot"></view>
</view> </view>
</view> </view>
<view
wx:if="{{currentHuiyuan.can_buy_trial}}"
class="tech-buy-btn trial-buy-btn"
bindtap="onBuyHuiyuanClick"
data-id="{{currentHuiyuan.id}}"
data-trial="true"
>
<view class="buy-btn-bg"></view>
<text class="buy-btn-text">购买体验版</text>
</view>
</view> </view>
</view> </view>
</view> </view>

View File

@@ -855,11 +855,29 @@
/* 购买区域 */ /* 购买区域 */
.buy-section { .buy-section {
display: flex; display: flex;
align-items: center; flex-direction: column;
justify-content: space-between; align-items: stretch;
gap: 20rpx;
padding-top: 25rpx; padding-top: 25rpx;
border-top: 1px solid rgba(64, 156, 255, 0.2); border-top: 1px solid rgba(64, 156, 255, 0.2);
} }
.buy-action {
display: flex;
flex-direction: column;
gap: 16rpx;
align-items: flex-end;
}
.price-days {
font-size: 22rpx;
color: #a0c8ff;
}
.trial-hint {
margin-top: 6rpx;
color: #ffb347;
}
.buy-info { .buy-info {
display: flex; display: flex;
@@ -928,6 +946,11 @@
background: linear-gradient(135deg, #1e4b8f, #409cff); background: linear-gradient(135deg, #1e4b8f, #409cff);
} }
.tech-buy-btn.trial-buy-btn .buy-btn-bg {
background: linear-gradient(135deg, rgba(255, 179, 71, 0.35), rgba(255, 120, 80, 0.45));
border: 1px solid rgba(255, 179, 71, 0.6);
}
.tech-buy-btn.renew .buy-btn-bg { .tech-buy-btn.renew .buy-btn-bg {
background: linear-gradient(135deg, #0f2c5c, #36cfc9); background: linear-gradient(135deg, #0f2c5c, #36cfc9);
} }

View File

@@ -2,6 +2,8 @@
import { createPage, request, isRoleStatusActive, isCenterPageActive, syncRoleStatuses, syncProfileFields, syncGroupFields, ensureRoleOnCenterPage, clearCacheAndEnterNormal, lockPrimaryRole, migrateLegacyCenterRole, enterLockedRole, parseSceneOptions } from '../../utils/base-page.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 } from '../../utils/avatar.js'
import { openPrivateChat, buildInviterPeerId } from '../../utils/im-user.js' import { openPrivateChat, buildInviterPeerId } from '../../utils/im-user.js'
import { isStaffMode, getStaffContext } from '../../utils/staff-api.js'
import { ensurePhoneAuth } from '../../utils/phone-auth.js'
const PLATFORM_INVITE_CODE = '0000008RffVgKHMj7kQC' const PLATFORM_INVITE_CODE = '0000008RffVgKHMj7kQC'
@@ -179,7 +181,10 @@ Page(createPage({
this.checkColdStartPopup('dashouduan'); this.checkColdStartPopup('dashouduan');
}, },
onShow() { async onShow() {
const phoneOk = await ensurePhoneAuth({ redirect: true, role: 'dashou' });
if (!phoneOk) return;
migrateLegacyCenterRole(getApp()); migrateLegacyCenterRole(getApp());
lockPrimaryRole('dashou', getApp()); lockPrimaryRole('dashou', getApp());
wx.setStorageSync('isJinpai', 0); wx.setStorageSync('isJinpai', 0);
@@ -277,6 +282,13 @@ Page(createPage({
tag: d.shangjiaCertified ? '进入商家端' : '去认证', tag: d.shangjiaCertified ? '进入商家端' : '去认证',
done: d.shangjiaCertified, done: d.shangjiaCertified,
}); });
list.push({
type: 'staff',
name: '商家客服',
icon: img.iconKefu || img.iconShangjia,
tag: isStaffMode() ? '进入工作台' : '去入驻',
done: isStaffMode(),
});
if (!d.isGuanshi) { if (!d.isGuanshi) {
list.push({ type: 'guanshi', name: '管事', icon: img.iconGuanshiAuth, tag: '去认证' }); list.push({ type: 'guanshi', name: '管事', icon: img.iconGuanshiAuth, tag: '去认证' });
} }
@@ -1015,6 +1027,14 @@ Page(createPage({
onTapAuthItem(e) { onTapAuthItem(e) {
const type = e.currentTarget.dataset.type; const type = e.currentTarget.dataset.type;
if (type === 'staff') {
if (isStaffMode()) {
enterLockedRole('shangjia', getApp());
} else {
wx.navigateTo({ url: '/pages/staff-join/staff-join' });
}
return;
}
if (type === 'shangjia') { if (type === 'shangjia') {
this.onTapShangjiaAuth(); this.onTapShangjiaAuth();
return; return;

View File

@@ -1,11 +1,15 @@
// pages/merchant-dispatch/merchant-dispatch.js // pages/merchant-dispatch/merchant-dispatch.js
import { createPage, request } from '../../utils/base-page.js' import { createPage, request } from '../../utils/base-page.js'
import PopupService from '../../services/popupService.js' // 引入弹窗服务 import PopupService from '../../services/popupService.js' // 引入弹窗服务
import { STAFF_API, isStaffMode, refreshStaffContext, getStaffContext, restoreStaffContextAfterAuth, isMerchantPortalUser } from '../../utils/staff-api.js'
Page(createPage({ Page(createPage({
data: { data: {
// 商家余额 (从全局变量获取) // 可用余额 / 客服额度
sjyue: '0.00', sjyue: '0.00',
isStaffMode: false,
balanceLabel: '可用余额',
balanceTip: '派单将从此余额扣除',
// 商品类型列表 // 商品类型列表
shangpinList: [], shangpinList: [],
@@ -42,7 +46,15 @@ Page(createPage({
bgImageUrl: '' bgImageUrl: ''
}, },
onLoad() { async onLoad() {
if (wx.getStorageSync('token')) {
await restoreStaffContextAfterAuth()
}
if (!isMerchantPortalUser()) {
wx.showToast({ title: '请先成为商家或绑定客服', icon: 'none' })
setTimeout(() => wx.navigateBack(), 1500)
return
}
this.loadBgImage() // 拼接背景图 this.loadBgImage() // 拼接背景图
this.loadShangjiaYue() // 从全局变量获取商家余额 this.loadShangjiaYue() // 从全局变量获取商家余额
this.loadShangpinTypes() // 加载商品类型 this.loadShangpinTypes() // 加载商品类型
@@ -50,15 +62,9 @@ Page(createPage({
}, },
onShow() { onShow() {
// 每次显示页面时刷新余额(可能从其他页面充值后返回)
this.loadShangjiaYue() this.loadShangjiaYue()
// 重新拼接背景图(确保最新)
this.loadBgImage() this.loadBgImage()
// 注册通知组件(用于全局消息提示)
this.registerNotificationComponent() this.registerNotificationComponent()
// 重置按钮缩进定时器重新开始5秒倒计时
this.startTutorialBtnTimer() this.startTutorialBtnTimer()
}, },
@@ -87,6 +93,22 @@ Page(createPage({
// ==================== 基础数据加载 ==================== // ==================== 基础数据加载 ====================
loadShangjiaYue() { loadShangjiaYue() {
const staff = isStaffMode()
this.setData({
isStaffMode: staff,
balanceLabel: staff ? '可用额度' : '可用余额',
balanceTip: staff ? '派单将从您的客服额度扣除(需老板先划额度)' : '派单将从此余额扣除',
})
if (staff) {
const ctx = getStaffContext() || {}
const wallet = ctx.wallet || {}
this.setData({ sjyue: wallet.quota_available || '0.00' })
refreshStaffContext(request).then((fresh) => {
const w = (fresh && fresh.wallet) || wallet
this.setData({ sjyue: w.quota_available || '0.00' })
}).catch(() => {})
return
}
const app = getApp() const app = getApp()
const shangjiaData = app.globalData.shangjia || {} const shangjiaData = app.globalData.shangjia || {}
this.setData({ this.setData({
@@ -97,13 +119,16 @@ Page(createPage({
// 加载商品类型(与极速派单完全一致) // 加载商品类型(与极速派单完全一致)
async loadShangpinTypes() { async loadShangpinTypes() {
this.setData({ isLoading: true }) this.setData({ isLoading: true })
const staff = isStaffMode()
try { try {
const res = await request({ const res = await request({
url: '/dingdan/sjspleixing', url: staff ? STAFF_API.productTypes : '/dingdan/sjspleixing',
method: 'POST' method: 'POST'
}) })
if (res && res.data.code === 200) { const code = res && res.data && res.data.code
let list = res.data.data || [] if (code === 200 || code === 0) {
let list = (res.data.data || [])
if (!Array.isArray(list) && list.list) list = list.list
// 强制倒序排列(与极速派单保持一致) // 强制倒序排列(与极速派单保持一致)
list.reverse() list.reverse()
this.setData({ this.setData({
@@ -115,14 +140,14 @@ Page(createPage({
this.setSelectedType(list[0]) this.setSelectedType(list[0])
} }
} else { } else {
throw new Error(res.data.msg || '加载失败') throw new Error((res.data && res.data.msg) || '加载失败')
} }
} catch (error) { } catch (error) {
console.error('加载商品类型失败:', error) console.error('加载商品类型失败:', error)
this.setData({ isLoading: false }) this.setData({ isLoading: false })
wx.showToast({ wx.showToast({
title: '加载类型失败', title: error.message || '加载类型失败',
icon: 'error', icon: 'none',
duration: 2000 duration: 2000
}) })
} }
@@ -282,21 +307,29 @@ Page(createPage({
} }
} }
// 4. 验证余额是否足够 // 4. 验证余额/额度是否足够
const balance = parseFloat(sjyue) const balance = parseFloat(sjyue)
if (price > balance) { if (price > balance) {
wx.showModal({ if (this.data.isStaffMode) {
title: '余额不足', wx.showModal({
content: `当前余额${sjyue}元,需要${jiage}元。请先充值。`, title: '额度不足',
confirmText: '去充值', content: `当前可用额度${sjyue}元,需要${jiage}元。请联系商家老板划额度。`,
success: (res) => { showCancel: false,
if (res.confirm) { })
wx.navigateTo({ } else {
url: '/pages/merchant-recharge/merchant-recharge' wx.showModal({
}) title: '余额不足',
content: `当前余额${sjyue}元,需要${jiage}元。请先充值。`,
confirmText: '去充值',
success: (res) => {
if (res.confirm) {
wx.navigateTo({
url: '/pages/merchant-recharge/merchant-recharge'
})
}
} }
} })
}) }
return false return false
} }
@@ -349,29 +382,31 @@ Page(createPage({
try { try {
const res = await request({ const res = await request({
url: '/dingdan/sjpaifa', url: isStaffMode() ? STAFF_API.orderDispatch : '/dingdan/sjpaifa',
method: 'POST', method: 'POST',
data: postData data: postData
}) })
if (res && res.data.code === 200) { if (res && res.data.code === 200) {
// 派单成功
wx.showToast({ wx.showToast({
title: '派单成功', title: '派单成功',
icon: 'success', icon: 'success',
duration: 2000 duration: 2000
}) })
// 更新全局变量中的商家信息(余额、发单量、流水)
const orderAmount = parseFloat(this.data.jiage) const orderAmount = parseFloat(this.data.jiage)
const app = getApp() if (this.data.isStaffMode) {
if (app.globalData.shangjia) { this.loadShangjiaYue()
const newBalance = parseFloat(app.globalData.shangjia.sjyue || '0') - orderAmount } else {
app.globalData.shangjia.sjyue = newBalance.toFixed(2) const app = getApp()
app.globalData.shangjia.fadanzong = (parseInt(app.globalData.shangjia.fadanzong || '0') + 1).toString() if (app.globalData.shangjia) {
app.globalData.shangjia.riliushui = (parseFloat(app.globalData.shangjia.riliushui || '0') + orderAmount).toFixed(2) const newBalance = parseFloat(app.globalData.shangjia.sjyue || '0') - orderAmount
app.globalData.shangjia.yueliushui = (parseFloat(app.globalData.shangjia.yueliushui || '0') + orderAmount).toFixed(2) app.globalData.shangjia.sjyue = newBalance.toFixed(2)
this.setData({ sjyue: app.globalData.shangjia.sjyue }) app.globalData.shangjia.fadanzong = (parseInt(app.globalData.shangjia.fadanzong || '0') + 1).toString()
app.globalData.shangjia.riliushui = (parseFloat(app.globalData.shangjia.riliushui || '0') + orderAmount).toFixed(2)
app.globalData.shangjia.yueliushui = (parseFloat(app.globalData.shangjia.yueliushui || '0') + orderAmount).toFixed(2)
this.setData({ sjyue: app.globalData.shangjia.sjyue })
}
} }
// 延迟重置表单,避免用户连续提交 // 延迟重置表单,避免用户连续提交

View File

@@ -2,12 +2,12 @@
<view class="page sj-form-theme"> <view class="page sj-form-theme">
<!-- 余额卡片 --> <!-- 余额卡片 -->
<view class="balance-card"> <view class="balance-card">
<text class="balance-label">可用余额</text> <text class="balance-label">{{balanceLabel}}</text>
<view class="balance-row"> <view class="balance-row">
<text class="balance-value">{{sjyue}}</text> <text class="balance-value">{{sjyue}}</text>
<text class="balance-unit">元</text> <text class="balance-unit">元</text>
</view> </view>
<text class="balance-tip">派单将从此余额扣除</text> <text class="balance-tip">{{balanceTip}}</text>
</view> </view>
<!-- 订单类型 --> <!-- 订单类型 -->

View File

@@ -1,5 +1,10 @@
import { createPage, request, ensureRoleOnCenterPage } from '../../utils/base-page.js'; import { createPage, request, ensureRoleOnCenterPage } from '../../utils/base-page.js';
import { getOrderStatusText } from '../../utils/api-helper.js'; import { getOrderStatusText } from '../../utils/api-helper.js';
import {
STAFF_API, isStaffMode, isMerchantPortalUser, refreshStaffContext, syncStaffUi,
restoreStaffContextAfterAuth, applyStaffStatsToPage, getStaffContext,
} from '../../utils/staff-api.js';
import { ensurePhoneAuth } from '../../utils/phone-auth.js';
const app = getApp(); const app = getApp();
@@ -12,6 +17,11 @@ Page(createPage({
statusBar: 20, statusBar: 20,
navBar: 44, navBar: 44,
isShangjia: false, isShangjia: false,
isStaffMode: false,
staffRoleName: '',
quotaAvailable: '0.00',
canDispatch: true,
canStaffManage: true,
lunboList: [], lunboList: [],
lunbozhanwei: '/images/lunbozhanwei.jpg', lunbozhanwei: '/images/lunbozhanwei.jpg',
gonggao: '', gonggao: '',
@@ -32,21 +42,42 @@ Page(createPage({
recentLoading: false, recentLoading: false,
}, },
onLoad() { async onLoad() {
const sys = wx.getSystemInfoSync(); const sys = wx.getSystemInfoSync();
this.setData({ this.setData({
statusBar: sys.statusBarHeight || 20, statusBar: sys.statusBarHeight || 20,
navBar: (sys.statusBarHeight || 20) + 44, navBar: (sys.statusBarHeight || 20) + 44,
lunbozhanwei: app.globalData.lunbozhanwei || '/images/lunbozhanwei.jpg', lunbozhanwei: app.globalData.lunbozhanwei || '/images/lunbozhanwei.jpg',
}); });
if (wx.getStorageSync('token')) {
await restoreStaffContextAfterAuth();
}
this.checkRole(); this.checkRole();
syncStaffUi(this);
this.waitForConfigAndLoadBanner(); this.waitForConfigAndLoadBanner();
this.checkColdStartPopup('shangjiaduan'); this.checkColdStartPopup('shangjiaduan');
}, },
onShow() { async onShow() {
const phoneOk = await ensurePhoneAuth({ redirect: true, role: 'shangjia' });
if (!phoneOk) return;
this.registerNotificationComponent(); this.registerNotificationComponent();
// 先用本地缓存渲染,再向服务端同步子客服身份
this.checkRole(); this.checkRole();
syncStaffUi(this);
const token = wx.getStorageSync('token');
const isOwner = Number(wx.getStorageSync('shangjiastatus')) === 1;
if (token && !isOwner) {
await restoreStaffContextAfterAuth();
this.checkRole();
syncStaffUi(this);
} else if (isStaffMode()) {
await refreshStaffContext(request);
this.checkRole();
syncStaffUi(this);
applyStaffStatsToPage(this, getStaffContext());
}
if (this.data.isShangjia) { if (this.data.isShangjia) {
ensureRoleOnCenterPage(this, 'shangjia'); ensureRoleOnCenterPage(this, 'shangjia');
this.loadDashboard(); this.loadDashboard();
@@ -78,8 +109,7 @@ Page(createPage({
}, },
checkRole() { checkRole() {
const ok = Number(wx.getStorageSync('shangjiastatus')) === 1; this.setData({ isShangjia: isMerchantPortalUser() });
this.setData({ isShangjia: ok });
}, },
loadBannerData(forceFetch) { loadBannerData(forceFetch) {
@@ -132,6 +162,12 @@ Page(createPage({
}, },
async loadShangjiaBrief() { async loadShangjiaBrief() {
if (isStaffMode()) {
const ctx = await refreshStaffContext(request);
syncStaffUi(this);
applyStaffStatsToPage(this, ctx);
return;
}
try { try {
const res = await request({ const res = await request({
url: '/yonghu/shangjiaxinxi', url: '/yonghu/shangjiaxinxi',
@@ -156,7 +192,7 @@ Page(createPage({
async loadPendingStats() { async loadPendingStats() {
try { try {
const res = await request({ const res = await request({
url: '/dingdan/sjdingdanhq', url: isStaffMode() ? STAFF_API.orderList : '/dingdan/sjdingdanhq',
method: 'POST', method: 'POST',
data: { zhuangtai_list: [8], page: 1, page_size: 20 }, data: { zhuangtai_list: [8], page: 1, page_size: 20 },
}); });
@@ -187,7 +223,7 @@ Page(createPage({
this.setData({ recentLoading: true }); this.setData({ recentLoading: true });
try { try {
const res = await request({ const res = await request({
url: '/dingdan/sjdingdanhq', url: isStaffMode() ? STAFF_API.orderList : '/dingdan/sjdingdanhq',
method: 'POST', method: 'POST',
data: { data: {
zhuangtai_list: ALL_ORDER_ZHUANGTAI, zhuangtai_list: ALL_ORDER_ZHUANGTAI,
@@ -206,7 +242,7 @@ Page(createPage({
return { return {
dingdan_id: item.dingdan_id, dingdan_id: item.dingdan_id,
title: item.jieshao || '暂无描述', title: item.jieshao || '暂无描述',
time: item.create_time || '', time: item.create_time || item.CreateTime || '',
status: getOrderStatusText(item.zhuangtai), status: getOrderStatusText(item.zhuangtai),
money: item.jine || '0.00', money: item.jine || '0.00',
raw, raw,

View File

@@ -7,7 +7,7 @@
</view> </view>
</view> </view>
<block wx:if="{{!isShangjia}}"> <block wx:if="{{!isShangjia && !isStaffMode}}">
<view class="guest-box sj-mx20 sj-panel"> <view class="guest-box sj-mx20 sj-panel">
<text class="guest-title">商家入驻</text> <text class="guest-title">商家入驻</text>
<text class="guest-desc">完成入驻后可发单、管理订单</text> <text class="guest-desc">完成入驻后可发单、管理订单</text>
@@ -39,7 +39,11 @@
<text class="sj-notice-txt">{{gonggao}}</text> <text class="sj-notice-txt">{{gonggao}}</text>
</view> </view>
<view class="sj-fadan-row"> <view class="staff-banner sj-mx20" wx:if="{{isStaffMode}}">
<text class="staff-banner-txt">商家客服 · {{staffRoleName}} · 可用额度 ¥{{quotaAvailable}}</text>
</view>
<view class="sj-fadan-row" wx:if="{{canDispatch}}">
<view class="sj-fadan-img-btn sj-fadan-img-btn--left" bindtap="goExpress"/> <view class="sj-fadan-img-btn sj-fadan-img-btn--left" bindtap="goExpress"/>
<view class="sj-fadan-img-btn sj-fadan-img-btn--right" bindtap="goDispatch"/> <view class="sj-fadan-img-btn sj-fadan-img-btn--right" bindtap="goDispatch"/>
</view> </view>
@@ -73,7 +77,7 @@
<text class="stat-num">{{stats.todayRefund}}</text> <text class="stat-num">{{stats.todayRefund}}</text>
</view> </view>
</view> </view>
<view class="stats-kefu-wrap"> <view class="stats-kefu-wrap" wx:if="{{canStaffManage}}">
<view class="kefu-row-inset flexm"> <view class="kefu-row-inset flexm">
<view class="kefu-btn-inset" bindtap="goKefuKey"> <view class="kefu-btn-inset" bindtap="goKefuKey">
<image class="kefu-inset-img" src="{{imgUrls.kefuKeyBtn}}" mode="widthFix"/> <image class="kefu-inset-img" src="{{imgUrls.kefuKeyBtn}}" mode="widthFix"/>

View File

@@ -307,3 +307,16 @@ page {
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
} }
.staff-banner {
margin-top: 16rpx;
padding: 16rpx 24rpx;
background: #fff8e8;
border-radius: 12rpx;
border: 1rpx solid #f0d89a;
}
.staff-banner-txt {
font-size: 24rpx;
color: #8a6d2b;
}

View File

@@ -1,60 +1,128 @@
/** 客服邀请码/秘钥 - UI 预览,后端接入前使用 mock 数据 */ /** 客服邀请码管理 — 对接 merchant-staff API */
import request from '../../utils/request.js';
import { STAFF_API } from '../../utils/staff-api.js';
Page({ Page({
data: { data: {
statusBar: 20, statusBar: 20,
gudingCode: 'XQKF2026', gudingCode: 'XQKF2026',
kefuTips: '客服秘钥用于绑定子客服账号,后端接口就绪后将自动替换为真实数据。', kefuTips: '一次性邀请码绑定子客服,固定码仅展示不可绑定。',
sublist: [{ name: '全部' }, { name: '未使用' }, { name: '已使用' }], sublist: [{ name: '全部', status: null }, { name: '未使用', status: 0 }, { name: '已使用', status: 1 }],
subCurrent: 0, subCurrent: 0,
list: [ list: [],
{ id: 1, typeName: '财务', code: 'CW-8X2K9P', status: 0, adddate: '2026-06-10' }, roles: [],
{ id: 2, typeName: '总管', code: 'ZG-3M7N1Q', status: 1, adddate: '2026-06-08', usedate: '2026-06-12', user: { nickname: '客服小张', avatar: '/images/default-avatar.png' } },
],
showQrcodeTips: false, showQrcodeTips: false,
loading: false,
}, },
onLoad() { onLoad() {
const sys = wx.getSystemInfoSync(); const sys = wx.getSystemInfoSync();
this.setData({ statusBar: sys.statusBarHeight || 20 }); this.setData({ statusBar: sys.statusBarHeight || 20 });
this.loadRoles();
this.loadList();
}, },
goBack() { onShow() {
wx.navigateBack(); this.loadList();
}, },
async loadRoles() {
try {
const res = await request({ url: STAFF_API.roleList, method: 'POST' });
if (res.data && res.data.code === 0) {
this.setData({ roles: res.data.data.roles || [] });
}
} catch (e) {}
},
async loadList() {
const tab = this.data.sublist[this.data.subCurrent];
try {
const res = await request({
url: STAFF_API.inviteList,
method: 'POST',
data: tab.status === null ? {} : { status: tab.status },
});
if (res.data && res.data.code === 0) {
const d = res.data.data || {};
this.setData({
list: d.list || [],
gudingCode: d.gudingCode || 'XQKF2026',
});
}
} catch (e) {
wx.showToast({ title: '加载失败', icon: 'none' });
}
},
goBack() { wx.navigateBack(); },
copyFixed() { copyFixed() {
wx.setClipboardData({ data: this.data.gudingCode }); wx.setClipboardData({ data: this.data.gudingCode });
}, },
switchSub(e) { switchSub(e) {
this.setData({ subCurrent: Number(e.currentTarget.dataset.idx) }); this.setData({ subCurrent: Number(e.currentTarget.dataset.idx) }, () => this.loadList());
}, },
addKey() { async addKey() {
wx.showToast({ title: '后端接入后可添加', icon: 'none' }); const roles = this.data.roles;
if (!roles.length) {
wx.showToast({ title: '暂无角色,请稍后重试', icon: 'none' });
return;
}
const names = roles.map(r => r.role_name);
wx.showActionSheet({
itemList: names,
success: async (r) => {
const role = roles[r.tapIndex];
wx.showLoading({ title: '生成中' });
try {
const res = await request({
url: STAFF_API.inviteCreate,
method: 'POST',
data: { role_id: role.id },
});
if (res.data && res.data.code === 0) {
wx.showToast({ title: '已生成', icon: 'success' });
this.loadList();
} else {
wx.showToast({ title: res.data.msg || '失败', icon: 'none' });
}
} finally {
wx.hideLoading();
}
},
});
}, },
copyCode(e) { copyCode(e) {
wx.setClipboardData({ data: e.currentTarget.dataset.code || '' }); wx.setClipboardData({ data: e.currentTarget.dataset.code || '' });
}, },
delKey() { async delKey(e) {
wx.showToast({ title: '预览数据不可删除', icon: 'none' }); const id = e.currentTarget.dataset.id;
}, if (!id) return;
const res = await request({
showTips() { url: STAFF_API.inviteRevoke,
this.setData({ showQrcodeTips: true }); method: 'POST',
}, data: { id },
closeTips() {
this.setData({ showQrcodeTips: false });
},
previewQr() {
wx.previewImage({
urls: ['https://xy-1314784552.cos.ap-guangzhou.myqcloud.com/uploads/images/ewm.png'],
}); });
if (res.data && res.data.code === 0) {
wx.showToast({ title: '已作废' });
this.loadList();
} else {
wx.showToast({ title: res.data.msg || '失败', icon: 'none' });
}
}, },
showTips() { this.setData({ showQrcodeTips: true }); },
closeTips() { this.setData({ showQrcodeTips: false }); },
goAudit() {
wx.navigateTo({ url: '/pages/merchant-staff-audit/merchant-staff-audit' });
},
goToRoleManage() {
wx.navigateTo({ url: '/pages/merchant-staff-role/merchant-staff-role' });
},
noop() {}, noop() {},
}); });

View File

@@ -26,6 +26,16 @@
<view class="add-btn" bindtap="addKey">添加</view> <view class="add-btn" bindtap="addKey">添加</view>
</view> </view>
<view class="help-row flexb" bindtap="goToRoleManage">
<text class="help-link">角色与权限管理(资金池/划额度在此配置角色)</text>
<text class="help-arrow"></text>
</view>
<view class="help-row flexb" bindtap="goAudit">
<text class="help-link">操作日志与客服排行</text>
<text class="help-arrow"></text>
</view>
<view class="help-row flexb" bindtap="showTips"> <view class="help-row flexb" bindtap="showTips">
<text class="help-link">秘钥使用方法</text> <text class="help-link">秘钥使用方法</text>
<text class="help-arrow"></text> <text class="help-arrow"></text>
@@ -50,7 +60,6 @@
</view> </view>
<view wx:if="{{!list.length}}" class="empty-tip">暂无卡密</view> <view wx:if="{{!list.length}}" class="empty-tip">暂无卡密</view>
<view class="load-tip">预览 · 接口就绪后对接真实数据</view>
</scroll-view> </scroll-view>
</view> </view>

View File

@@ -1,47 +1,398 @@
/** 客服列表 - UI 预览mock 数据 */ /** 客服列表 — 派单量 / 剩余额度 / 封禁解封 */
import request from '../../utils/request.js';
import { STAFF_API, isStaffMode } from '../../utils/staff-api.js';
const app = getApp();
function avatarUrl(path) {
if (!path) return '/images/default-avatar.png';
if (path.startsWith('http')) return path;
const oss = app.globalData.ossImageUrl || '';
const p = path.startsWith('/') ? path.slice(1) : path;
return oss ? oss + p : '/images/default-avatar.png';
}
Page({ Page({
data: { data: {
statusBar: 20, statusBar: 20,
list: [
{ list: [],
id: 1,
nickname: '客服总管-阿星', isOwner: false,
avatar: '/images/default-avatar.png',
uid: '1008601',
isChief: true,
isFinance: false,
orderCount: 128,
settleCount: 96,
settleAmount: '12800.00',
},
{
id: 2,
nickname: '财务-小李',
avatar: '/images/default-avatar.png',
uid: '1008602',
isChief: false,
isFinance: true,
orderCount: 56,
settleCount: 52,
settleAmount: '6800.00',
},
],
}, },
onLoad() { onLoad() {
const sys = wx.getSystemInfoSync(); const sys = wx.getSystemInfoSync();
this.setData({ statusBar: sys.statusBarHeight || 20 });
this.setData({ statusBar: sys.statusBarHeight || 20, isOwner: !isStaffMode() });
this.loadList();
}, },
goBack() {
wx.navigateBack();
onShow() {
this.loadList();
}, },
formatList(raw) {
return (raw || []).map((item) => ({
...item,
avatar: avatarUrl(item.avatar),
todayDispatchCount: item.todayDispatchCount != null ? item.todayDispatchCount : 0,
todayDispatchAmount: item.todayDispatchAmount || '0.00',
dispatchCount: item.dispatchCount != null ? item.dispatchCount : (item.orderCount || 0),
dispatchAmount: item.dispatchAmount || '0.00',
settleCount: item.settleCount != null ? item.settleCount : 0,
settleAmount: item.settleAmount || '0.00',
refundCount: item.refundCount != null ? item.refundCount : 0,
refundAmount: item.refundAmount || '0.00',
quotaAvailable: item.quota_available || '0.00',
quotaRevokable: item.quota_available || '0.00',
isSelf: !!item.isSelf,
}));
},
rejectSelf(id) {
const row = (this.data.list || []).find((x) => String(x.id) === String(id));
if (row && row.isSelf) {
wx.showToast({ title: '不能对本人操作', icon: 'none' });
return true;
}
return false;
},
async loadList() {
try {
const res = await request({ url: STAFF_API.memberList, method: 'POST' });
if (res.data && res.data.code === 0) {
const raw = (res.data.data && res.data.data.list) || [];
this.setData({ list: this.formatList(raw) });
}
} catch (e) {
wx.showToast({ title: '加载失败', icon: 'none' });
}
},
goBack() { wx.navigateBack(); },
copyUid(e) { copyUid(e) {
wx.setClipboardData({ data: e.currentTarget.dataset.uid || '' }); wx.setClipboardData({ data: e.currentTarget.dataset.uid || '' });
}, },
onAction(e) {
wx.showToast({ title: '后端接入后生效', icon: 'none' });
async onAllocate(e) {
const id = e.currentTarget.dataset.id;
if (this.rejectSelf(id)) return;
wx.showModal({
title: '划额度',
editable: true,
placeholderText: '输入金额',
success: async (r) => {
if (!r.confirm || !r.content) return;
const res = await request({
url: STAFF_API.walletAllocate,
method: 'POST',
data: { member_id: id, amount: r.content },
});
wx.showToast({
title: (res.data && res.data.code === 0) ? '成功' : (res.data.msg || '失败'),
icon: 'none',
});
this.loadList();
},
});
}, },
async onRevoke(e) {
const id = e.currentTarget.dataset.id;
if (this.rejectSelf(id)) return;
const available = e.currentTarget.dataset.available || '0';
wx.showModal({
title: '收回额度',
editable: true,
placeholderText: `最多可收回 ${available}`,
success: async (r) => {
if (!r.confirm || !r.content) return;
const res = await request({
url: STAFF_API.walletRevoke,
method: 'POST',
data: { member_id: id, amount: r.content },
});
wx.showToast({
title: (res.data && res.data.code === 0) ? '已收回' : (res.data.msg || '失败'),
icon: 'none',
});
if (res.data && res.data.code === 0) this.loadList();
},
});
},
async onToggleDisable(e) {
const id = e.currentTarget.dataset.id;
if (this.rejectSelf(id)) return;
const disabled = e.currentTarget.dataset.disabled;
const action = disabled ? '解封' : '封禁';
wx.showModal({
title: `${action}客服`,
content: disabled ? '解封后该客服可继续登录操作' : '封禁后该客服无法使用商家端功能',
success: async (r) => {
if (!r.confirm) return;
const res = await request({
url: STAFF_API.memberDisable,
method: 'POST',
data: { member_id: id, disable: !disabled },
});
wx.showToast({ title: (res.data && res.data.msg) || action, icon: 'none' });
this.loadList();
},
});
},
async onRemove(e) {
const id = e.currentTarget.dataset.id;
wx.showModal({
title: '移除客服',
content: '移除后该用户可认证商家或绑定其他商家',
success: async (r) => {
if (!r.confirm) return;
const res = await request({
url: STAFF_API.memberRemove,
method: 'POST',
data: { member_id: id },
});
wx.showToast({ title: res.data.msg || '完成', icon: 'none' });
this.loadList();
},
});
},
async onAssignRole(e) {
const memberId = e.currentTarget.dataset.id;
if (this.rejectSelf(memberId)) return;
const rolesRes = await request({ url: STAFF_API.roleList, method: 'POST' });
const roles = (rolesRes.data && rolesRes.data.data && rolesRes.data.data.roles) || [];
if (!roles.length) {
wx.showToast({ title: '请先创建角色', icon: 'none' });
return;
}
wx.showActionSheet({
itemList: roles.map((r) => r.role_name),
success: async (r) => {
const role = roles[r.tapIndex];
const res = await request({
url: STAFF_API.memberUpdateRole,
method: 'POST',
data: { member_id: memberId, role_id: role.id },
});
const ok = res.data && res.data.code === 0;
const roleName = (res.data && res.data.data && res.data.data.role_name) || role.role_name;
wx.showToast({
title: ok ? `已设为${roleName}` : (res.data.msg || '失败'),
icon: 'none',
});
if (ok) this.loadList();
},
});
},
goRank() {
wx.navigateTo({ url: '/pages/merchant-staff-audit/merchant-staff-audit?tab=rank' });
},
goAudit() {
wx.navigateTo({ url: '/pages/merchant-staff-audit/merchant-staff-audit' });
},
}); });

View File

@@ -1,47 +1,162 @@
<view class="sub-page sj-page"> <view class="sub-page sj-page">
<view class="status-bar" style="height:{{statusBar}}px"></view> <view class="status-bar" style="height:{{statusBar}}px"></view>
<view class="sub-nav flexb"> <view class="sub-nav flexb">
<text class="back" bindtap="goBack"></text> <text class="back" bindtap="goBack"></text>
<text class="sub-nav-title">客服列表</text> <text class="sub-nav-title">客服列表</text>
<text class="back placeholder"></text> <text class="back placeholder"></text>
</view> </view>
<scroll-view scroll-y class="sub-scroll" enhanced show-scrollbar="{{false}}"> <scroll-view scroll-y class="sub-scroll" enhanced show-scrollbar="{{false}}">
<view wx:for="{{list}}" wx:key="id" class="kefu-card sj-mx20">
<view wx:for="{{list}}" wx:key="id" class="kefu-card sj-mx20 {{item.isDisabled ? 'disabled-card' : ''}}">
<view class="kefu-head flex"> <view class="kefu-head flex">
<image class="kefu-av" src="{{item.avatar}}" mode="aspectFill"/> <image class="kefu-av" src="{{item.avatar}}" mode="aspectFill"/>
<view class="kefu-info"> <view class="kefu-info">
<view class="flex"> <view class="flex">
<text class="kefu-name">{{item.nickname}}</text> <text class="kefu-name">{{item.nickname}}</text>
<text class="badge badge-role">{{item.role_name || '客服'}}</text>
<text wx:if="{{item.isChief}}" class="badge badge-chief">总管</text> <text wx:if="{{item.isChief}}" class="badge badge-chief">总管</text>
<text wx:if="{{item.isSelf}}" class="badge badge-self">本人</text>
<text wx:if="{{item.isFinance}}" class="badge badge-finance">财务</text> <text wx:if="{{item.isFinance}}" class="badge badge-finance">财务</text>
<text wx:if="{{item.isDisabled}}" class="badge badge-off">已封禁</text>
</view> </view>
<view class="uid-row flex"> <view class="uid-row flex">
<text class="kefu-uid">ID: {{item.uid}}</text> <text class="kefu-uid">ID: {{item.uid}}</text>
<text class="copy-link" bindtap="copyUid" data-uid="{{item.uid}}">复制</text> <text class="copy-link" bindtap="copyUid" data-uid="{{item.uid}}">复制</text>
</view> </view>
</view> </view>
</view> </view>
<view class="quota-row">
<text class="quota-label">剩余额度</text>
<text class="quota-val">¥{{item.quotaAvailable}}</text>
</view>
<view class="kefu-stats flexa"> <view class="kefu-stats flexa">
<view class="stat-box flexmc"> <view class="stat-box flexmc">
<text class="stat-n">{{item.orderCount}}</text>
<text class="stat-l">接单数</text> <text class="stat-n">{{item.todayDispatchCount}}</text>
<text class="stat-l">今日派单</text>
</view> </view>
<view class="stat-box flexmc"> <view class="stat-box flexmc">
<text class="stat-n">{{item.dispatchCount}}</text>
<text class="stat-l">累计派单</text>
</view>
<view class="stat-box flexmc">
<text class="stat-n">{{item.settleCount}}</text> <text class="stat-n">{{item.settleCount}}</text>
<text class="stat-l">结算数</text>
<text class="stat-l">结单数</text>
</view> </view>
<view class="stat-box flexmc"> <view class="stat-box flexmc">
<text class="stat-n">¥{{item.settleAmount}}</text>
<text class="stat-l">结算金额</text> <text class="stat-n">{{item.refundCount}}</text>
<text class="stat-l">退款数</text>
</view> </view>
</view> </view>
<view class="kefu-actions flexa">
<text class="act" bindtap="onAction">查看订单</text> <view class="kefu-stats amount-stats flexa">
<text class="act" bindtap="onAction">权限管理</text>
<text class="act act-del" bindtap="onAction">删除</text> <view class="stat-box flexmc">
<text class="stat-n stat-amt">¥{{item.todayDispatchAmount}}</text>
<text class="stat-l">今日派单额</text>
</view>
<view class="stat-box flexmc">
<text class="stat-n stat-amt">¥{{item.dispatchAmount}}</text>
<text class="stat-l">派单金额</text>
</view>
<view class="stat-box flexmc">
<text class="stat-n stat-amt">¥{{item.settleAmount}}</text>
<text class="stat-l">结算金额</text>
</view>
<view class="stat-box flexmc">
<text class="stat-n stat-amt">¥{{item.refundAmount}}</text>
<text class="stat-l">退款金额</text>
</view>
</view> </view>
<view wx:if="{{!item.isSelf}}" class="kefu-actions flexa">
<text class="act" bindtap="onAllocate" data-id="{{item.id}}">划额度</text>
<text class="act" bindtap="onRevoke" data-id="{{item.id}}" data-available="{{item.quotaRevokable}}">收回额度</text>
<text class="act" bindtap="onAssignRole" data-id="{{item.id}}">换角色</text>
<text class="act" bindtap="onToggleDisable" data-id="{{item.id}}" data-disabled="{{item.isDisabled}}">{{item.isDisabled ? '解封' : '封禁'}}</text>
<text wx:if="{{isOwner}}" class="act act-del" bindtap="onRemove" data-id="{{item.id}}">移除</text>
</view>
<view wx:else class="self-tip sj-mx20">本人账号不可在此操作,如需调整请联系商家老板</view>
</view> </view>
<view class="load-tip">预览 · 接口就绪后对接真实数据</view>
<view wx:if="{{!list.length}}" class="load-tip">暂无客服成员</view>
<view class="load-tip link-tip" bindtap="goAudit">操作日志 </view>
<view class="load-tip link-tip" bindtap="goRank">客服排行 </view>
</scroll-view> </scroll-view>
</view> </view>

View File

@@ -31,6 +31,34 @@
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06); box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
} }
.kefu-card.disabled-card { opacity: 0.72; }
.badge-role {
font-size: 20rpx;
padding: 2rpx 10rpx;
background: #f0f0f0;
color: #666;
border-radius: 6rpx;
margin-left: 8rpx;
}
.badge-off {
background: #ffebee;
color: #c62828;
}
.quota-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12rpx 0 8rpx;
border-bottom: 1rpx solid #f0f0f0;
margin-bottom: 12rpx;
}
.quota-label { font-size: 24rpx; color: #888; }
.quota-val { font-size: 30rpx; color: #C9A962; font-weight: 600; }
.kefu-av { .kefu-av {
width: 88rpx; width: 88rpx;
height: 88rpx; height: 88rpx;
@@ -55,6 +83,15 @@
.badge-chief { background: #ffb508; } .badge-chief { background: #ffb508; }
.badge-finance { background: #1b6ef3; } .badge-finance { background: #1b6ef3; }
.badge-self { background: #9e9e9e; }
.self-tip {
margin-top: 16rpx;
padding: 16rpx 0 4rpx;
font-size: 24rpx;
color: #999;
text-align: center;
}
.kefu-uid { .kefu-uid {
font-size: 24rpx; font-size: 24rpx;
@@ -75,12 +112,23 @@
border-bottom: 1rpx solid #f5f5f5; border-bottom: 1rpx solid #f5f5f5;
} }
.amount-stats {
margin-top: 0;
border-top: none;
padding-top: 0;
}
.stat-n { .stat-n {
font-size: 28rpx; font-size: 28rpx;
font-weight: 700; font-weight: 700;
color: #e67e22; color: #e67e22;
} }
.stat-amt {
font-size: 24rpx;
color: #c0392b;
}
.stat-l { .stat-l {
font-size: 22rpx; font-size: 22rpx;
color: #999; color: #999;

File diff suppressed because it is too large Load Diff

View File

@@ -1,354 +1,373 @@
<!--pages/merchant-order-detail/merchant-order-detail.wxml - 商家订单详情页完整结构(正方形九宫格,圆形头像)--> <!--pages/merchant-order-detail/merchant-order-detail.wxml - 商家订单详情页完整结构(正方形九宫格,圆形头像)-->
<!-- 页面根容器 --> <!-- 页面根容器 -->
<view class="sjddxq-page"> <view class="sjddxq-page">
<!-- 加载状态区域,当 isLoading 为 true 时显示 --> <!-- 加载状态区域,当 isLoading 为 true 时显示 -->
<view class="loading-container" wx:if="{{isLoading}}"> <view class="loading-container" wx:if="{{isLoading}}">
<!-- 旋转的加载图标 --> <!-- 旋转的加载图标 -->
<view class="loading-spinner"></view> <view class="loading-spinner"></view>
<!-- 加载文字提示 --> <!-- 加载文字提示 -->
<text class="loading-text">加载中...</text> <text class="loading-text">加载中...</text>
</view> </view>
<!-- 主要内容区域,数据加载完成后显示 --> <!-- 主要内容区域,数据加载完成后显示 -->
<view class="content-container" wx:if="{{!isLoading}}"> <view class="content-container" wx:if="{{!isLoading}}">
<!-- 商品图片和介绍卡片 --> <!-- 商品图片和介绍卡片 -->
<view class="card shangpin-card"> <view class="card shangpin-card">
<!-- 商品图片固定宽高180rpx已是正方形 --> <!-- 商品图片固定宽高180rpx已是正方形 -->
<image class="shangpin-img" src="{{jibenShuju.tupian}}" mode="aspectFill"/> <image class="shangpin-img" src="{{jibenShuju.tupian}}" mode="aspectFill"/>
<!-- 商品介绍文本区域 --> <!-- 商品介绍文本区域 -->
<view class="shangpin-text"> <view class="shangpin-text">
<!-- 订单介绍文字 --> <!-- 订单介绍文字 -->
<text class="jieshao-text">{{jibenShuju.jieshao || '无描述'}}</text> <text class="jieshao-text">{{jibenShuju.jieshao || '无描述'}}</text>
</view> </view>
</view> </view>
<!-- 订单信息卡片,左侧带状态颜色边框 --> <!-- 订单信息卡片,左侧带状态颜色边框 -->
<view class="card info-card" style="border-left: 8rpx solid {{zhuangtaiYanseMap[jibenShuju.zhuangtai]}}"> <view class="card info-card" style="border-left: 8rpx solid {{zhuangtaiYanseMap[jibenShuju.zhuangtai]}}">
<view class="card-title">订单信息</view> <view class="card-title">订单信息</view>
<!-- 游戏昵称行 --> <!-- 游戏昵称行 -->
<view class="info-row" wx:if="{{jibenShuju.nicheng}}"><text class="label">游戏昵称</text><text class="value">{{jibenShuju.nicheng}}</text></view> <view class="info-row" wx:if="{{jibenShuju.nicheng}}"><text class="label">游戏昵称</text><text class="value">{{jibenShuju.nicheng}}</text></view>
<!-- 备注信息行 --> <!-- 备注信息行 -->
<view class="info-row" wx:if="{{jibenShuju.beizhu}}"><text class="label">备注</text><text class="value">{{jibenShuju.beizhu}}</text></view> <view class="info-row" wx:if="{{jibenShuju.beizhu}}"><text class="label">备注</text><text class="value">{{jibenShuju.beizhu}}</text></view>
<!-- 订单ID行可复制 --> <!-- 订单ID行可复制 -->
<view class="info-row"><text class="label">订单ID</text><view class="value-with-copy"><text class="value">{{jibenShuju.dingdan_id}}</text><text class="copy-btn" bindtap="fuzhiWenben" data-text="{{jibenShuju.dingdan_id}}">📋</text></view></view> <view class="info-row"><text class="label">订单ID</text><view class="value-with-copy"><text class="value">{{jibenShuju.dingdan_id}}</text><text class="copy-btn" bindtap="fuzhiWenben" data-text="{{jibenShuju.dingdan_id}}">📋</text></view></view>
<!-- 创建时间行 --> <!-- 创建时间行 -->
<view class="info-row"><text class="label">创建时间</text><text class="value">{{jibenShuju.create_time}}</text></view> <view class="info-row"><text class="label">创建时间</text><text class="value">{{jibenShuju.create_time}}</text></view>
<!-- 订单金额行,红色高亮 --> <!-- 订单金额行,红色高亮 -->
<view class="info-row"><text class="label">订单金额</text><text class="value price-red">¥{{jibenShuju.jine}}</text></view> <view class="info-row"><text class="label">订单金额</text><text class="value price-red">¥{{jibenShuju.jine}}</text></view>
<!-- 订单状态行 --> <!-- 订单状态行 -->
<view class="info-row"><text class="label">状态</text><text class="value zhuangtai-text">{{zhuangtaiWenziMap[jibenShuju.zhuangtai]}}</text></view> <view class="info-row"><text class="label">状态</text><text class="value zhuangtai-text">{{zhuangtaiWenziMap[jibenShuju.zhuangtai]}}</text></view>
<!-- 新增:退款操作按钮(仅退款中状态 --> <!-- 派单客服(有记录才展示 -->
<view class="refund-actions" wx:if="{{jibenShuju.zhuangtai == 4}}"> <view class="card dispatch-staff-card" wx:if="{{dispatchStaff}}">
<view class="action-btn action-cancel" bindtap="showCancelTuikuan">撤销退款申请</view> <view class="card-title">派单客服</view>
<view class="action-btn action-modify" bindtap="showModifyTuikuan">修改退款理由</view> <view class="dashou-header">
</view> <image class="dashou-avatar" src="{{dispatchStaffAvatar || '/images/default-avatar.png'}}" mode="aspectFill"/>
</view> <view class="dashou-detail">
<text class="dashou-nicheng">{{dispatchStaff.dispatch_staff_name || '未知'}}</text>
<!-- 接单员信息卡片仅当接单员ID存在时显示 --> <view class="dashou-id-row">
<view class="card dashou-card" wx:if="{{dashouInfo.yonghuid}}"> <text class="label">用户ID</text>
<view class="card-title">服务接单员</view> <text class="value">{{dispatchStaff.dispatch_staff_uid}}</text>
<view class="dashou-header"> <text class="copy-btn" bindtap="fuzhiWenben" data-text="{{dispatchStaff.dispatch_staff_uid}}">📋</text>
<!-- 接单员头像,圆形 --> </view>
<image class="dashou-avatar" src="{{dashouInfo.avatar || '/images/default-avatar.png'}}" mode="aspectFill"/> <view class="dashou-id-row" wx:if="{{dispatchStaff.dispatch_role_name}}">
<view class="dashou-detail"> <text class="label">角色</text>
<!-- 接单员昵称 --> <text class="value">{{dispatchStaff.dispatch_role_name}}</text>
<text class="dashou-nicheng">{{dashouInfo.nicheng || '未知昵称'}}</text> </view>
<!-- 接单员用户ID行可复制 --> </view>
<view class="dashou-id-row"><text class="label">用户ID</text><text class="value">{{dashouInfo.yonghuid}}</text><text class="copy-btn" bindtap="fuzhiWenben" data-text="{{dashouInfo.yonghuid}}">📋</text></view> </view>
</view> </view>
</view> <!-- 新增:退款操作按钮(仅退款中状态) -->
</view> <view class="refund-actions" wx:if="{{jibenShuju.zhuangtai == 4}}">
<view class="action-btn action-cancel" bindtap="showCancelTuikuan">撤销退款申请</view>
<!-- 服务详情卡片,展示交付图片、留言、退款理由等 --> <view class="action-btn action-modify" bindtap="showModifyTuikuan">修改退款理由</view>
<view class="card service-card" wx:if="{{xiangxiShuju.dashou_jiaofu.length > 0 || xiangxiShuju.dashou_liuyan || xiangxiShuju.tuikuan_liyou || xiangxiShuju.shangjia_liuyan}}"> </view>
<view class="card-title">服务详情</view> </view>
<!-- 交付图片区域:使用 grid-img-wrapper 实现正方形九宫格 -->
<block wx:if="{{xiangxiShuju.dashou_jiaofu.length > 0}}"> <!-- 接单员信息卡片仅当接单员ID存在时显示 -->
<view class="sub-title">交付图片</view> <view class="card dashou-card" wx:if="{{dashouInfo.yonghuid}}">
<view class="image-grid"> <view class="card-title">服务接单员</view>
<!-- 循环渲染每张交付图片 --> <view class="dashou-header">
<block wx:for="{{xiangxiShuju.dashou_jiaofu}}" wx:key="index"> <!-- 接单员头像,圆形 -->
<!-- 正方形容器 --> <image class="dashou-avatar" src="{{dashouInfo.avatar || '/images/default-avatar.png'}}" mode="aspectFill"/>
<view class="grid-img-wrapper"> <view class="dashou-detail">
<!-- 图片绝对定位填充容器 --> <!-- 接单员昵称 -->
<image class="grid-img" src="{{item}}" mode="aspectFill" bindtap="yulanTupian" data-url="{{item}}" data-urls="{{xiangxiShuju.dashou_jiaofu}}"/> <text class="dashou-nicheng">{{dashouInfo.nicheng || '未知昵称'}}</text>
</view> <!-- 接单员用户ID行可复制 -->
</block> <view class="dashou-id-row"><text class="label">用户ID</text><text class="value">{{dashouInfo.yonghuid}}</text><text class="copy-btn" bindtap="fuzhiWenben" data-text="{{dashouInfo.yonghuid}}">📋</text></view>
</view> </view>
</block> </view>
<!-- 接单员留言 --> </view>
<block wx:if="{{xiangxiShuju.dashou_liuyan}}">
<view class="sub-title">接单员留言</view> <!-- 服务详情卡片,展示交付图片、留言、退款理由等 -->
<view class="content-box">{{xiangxiShuju.dashou_liuyan}}</view> <view class="card service-card" wx:if="{{xiangxiShuju.dashou_jiaofu.length > 0 || xiangxiShuju.dashou_liuyan || xiangxiShuju.tuikuan_liyou || xiangxiShuju.shangjia_liuyan}}">
</block> <view class="card-title">服务详情</view>
<!-- 退款理由 --> <!-- 交付图片区域:使用 grid-img-wrapper 实现正方形九宫格 -->
<block wx:if="{{xiangxiShuju.tuikuan_liyou}}"> <block wx:if="{{xiangxiShuju.dashou_jiaofu.length > 0}}">
<view class="sub-title">退款理由</view> <view class="sub-title">交付图片</view>
<view class="content-box">{{xiangxiShuju.tuikuan_liyou}}</view> <view class="image-grid">
</block> <!-- 循环渲染每张交付图片 -->
<!-- 商家留言 --> <block wx:for="{{xiangxiShuju.dashou_jiaofu}}" wx:key="index">
<block wx:if="{{xiangxiShuju.shangjia_liuyan}}"> <!-- 正方形容器 -->
<view class="sub-title">商家留言</view> <view class="grid-img-wrapper">
<view class="content-box">{{xiangxiShuju.shangjia_liuyan}}</view> <!-- 图片绝对定位填充容器 -->
</block> <image class="grid-img" src="{{item}}" mode="aspectFill" bindtap="yulanTupian" data-url="{{item}}" data-urls="{{xiangxiShuju.dashou_jiaofu}}"/>
</view> </view>
</block>
<!-- 罚款信息卡片,仅当罚单数据存在时显示 --> </view>
<view class="card fakuan-card" wx:if="{{fadanData}}"> </block>
<view class="card-title">罚款信息</view> <!-- 接单员留言 -->
<!-- 罚款金额,红色显示 --> <block wx:if="{{xiangxiShuju.dashou_liuyan}}">
<view class="info-row"><text class="label">罚款金额</text><text class="value price-red">¥{{fadanData.fakuanjine || '0.00'}}</text></view> <view class="sub-title">接单员留言</view>
<!-- 罚款理由 --> <view class="content-box">{{xiangxiShuju.dashou_liuyan}}</view>
<view class="info-row"><text class="label">罚款理由</text><text class="value">{{fadanData.chufaliyou || '-'}}</text></view> </block>
<!-- 是否影响抢单 --> <!-- 退款理由 -->
<view class="info-row"><text class="label">是否影响抢单</text><text class="value">{{fadanData.yingxiang_qiangdan == 1 ? '是' : '否'}}</text></view> <block wx:if="{{xiangxiShuju.tuikuan_liyou}}">
<!-- 罚单状态 --> <view class="sub-title">退款理由</view>
<view class="info-row"><text class="label">罚单状态</text><text class="value">{{fadanData.zhuangtai == 1 ? '待缴纳' : fadanData.zhuangtai == 2 ? '已缴纳' : fadanData.zhuangtai == 3 ? '申诉中' : '已驳回'}}</text></view> <view class="content-box">{{xiangxiShuju.tuikuan_liyou}}</view>
<!-- 如果有申诉理由,显示 --> </block>
<block wx:if="{{fadanData.shensuliyou}}"><view class="sub-title">申诉理由</view><view class="content-box">{{fadanData.shensuliyou}}</view></block> <!-- 商家留言 -->
<!-- 如果有同意处罚理由,显示 --> <block wx:if="{{xiangxiShuju.shangjia_liuyan}}">
<block wx:if="{{fadanData.tongyi_chufaliyou}}"><view class="sub-title">同意处罚理由</view><view class="content-box">{{fadanData.tongyi_chufaliyou}}</view></block> <view class="sub-title">商家留言</view>
<!-- 如果有驳回理由,显示 --> <view class="content-box">{{xiangxiShuju.shangjia_liuyan}}</view>
<block wx:if="{{fadanData.bohuiliyou}}"><view class="sub-title">驳回理由</view><view class="content-box">{{fadanData.bohuiliyou}}</view></block> </block>
<!-- 创建时间 --> </view>
<view class="info-row"><text class="label">创建时间</text><text class="value">{{fadanData.create_time}}</text></view>
<!-- 更新时间 --> <!-- 罚款信息卡片,仅当罚单数据存在时显示 -->
<view class="info-row"><text class="label">更新时间</text><text class="value">{{fadanData.update_time}}</text></view> <view class="card fakuan-card" wx:if="{{fadanData}}">
<!-- 新增:罚单操作按钮区域(根据状态显示) --> <view class="card-title">罚款信息</view>
<view class="fakuan-actions" wx:if="{{fadanData}}"> <!-- 罚款金额,红色显示 -->
<!-- 待缴纳(1) 或 申诉中(3):显示修改和撤销按钮 --> <view class="info-row"><text class="label">罚款金额</text><text class="value price-red">¥{{fadanData.fakuanjine || '0.00'}}</text></view>
<block wx:if="{{fadanData.zhuangtai == 1 || fadanData.zhuangtai == 3}}"> <!-- 罚款理由 -->
<view class="action-btn action-modify" bindtap="showModifyFakuan">修改罚款</view> <view class="info-row"><text class="label">罚款理由</text><text class="value">{{fadanData.chufaliyou || '-'}}</text></view>
<view class="action-btn action-cancel" bindtap="showCancelFakuan">撤销罚款</view> <!-- 是否影响抢单 -->
</block> <view class="info-row"><text class="label">是否影响抢单</text><text class="value">{{fadanData.yingxiang_qiangdan == 1 ? '是' : '否'}}</text></view>
<!-- 已驳回(4):显示再次申请按钮 --> <!-- 罚单状态 -->
<block wx:if="{{fadanData.zhuangtai == 4}}"> <view class="info-row"><text class="label">罚单状态</text><text class="value">{{fadanData.zhuangtai == 1 ? '待缴纳' : fadanData.zhuangtai == 2 ? '已缴纳' : fadanData.zhuangtai == 3 ? '申诉中' : '已驳回'}}</text></view>
<view class="action-btn action-resubmit" bindtap="showResubmitFakuan">再次申请罚款</view> <!-- 如果有申诉理由,显示 -->
</block> <block wx:if="{{fadanData.shensuliyou}}"><view class="sub-title">申诉理由</view><view class="content-box">{{fadanData.shensuliyou}}</view></block>
</view> <!-- 如果有同意处罚理由,显示 -->
</view> <block wx:if="{{fadanData.tongyi_chufaliyou}}"><view class="sub-title">同意处罚理由</view><view class="content-box">{{fadanData.tongyi_chufaliyou}}</view></block>
<!-- 如果有驳回理由,显示 -->
<!-- 更换接单员后生成的新订单ID展示卡片 --> <block wx:if="{{fadanData.bohuiliyou}}"><view class="sub-title">驳回理由</view><view class="content-box">{{fadanData.bohuiliyou}}</view></block>
<view class="card new-order-card" wx:if="{{xinDingdanId}}"> <!-- 创建时间 -->
<view class="card-title">更换接单员成功</view> <view class="info-row"><text class="label">创建时间</text><text class="value">{{fadanData.create_time}}</text></view>
<view class="info-row"><text class="label">新订单ID</text><view class="value-with-copy"><text class="value">{{xinDingdanId}}</text><text class="copy-btn" bindtap="fuzhiWenben" data-text="{{xinDingdanId}}">📋</text></view></view> <!-- 更新时间 -->
</view> <view class="info-row"><text class="label">更新时间</text><text class="value">{{fadanData.update_time}}</text></view>
<!-- 新增:罚单操作按钮区域(根据状态显示) -->
<!-- 结算区域卡片仅当订单状态为8结算中时显示 --> <view class="fakuan-actions" wx:if="{{fadanData && canPenaltyManage}}">
<view class="card jiesuan-card" wx:if="{{jibenShuju.zhuangtai == 8}}"> <!-- 待缴纳(1) 或 申诉中(3):显示修改和撤销按钮 -->
<view class="card-title">订单结算</view> <block wx:if="{{fadanData.zhuangtai == 1 || fadanData.zhuangtai == 3}}">
<view class="star-row"> <view class="action-btn action-modify" bindtap="showModifyFakuan">修改罚款</view>
<text class="label">打星评分</text> <view class="action-btn action-cancel" bindtap="showCancelFakuan">撤销罚款</view>
<view class="stars"> </block>
<!-- 循环渲染五颗星星 --> <!-- 已驳回(4):显示再次申请按钮 -->
<block wx:for="{{xingxingData}}" wx:key="id"> <block wx:if="{{fadanData.zhuangtai == 4}}">
<!-- 点击星星触发评分 --> <view class="action-btn action-resubmit" bindtap="showResubmitFakuan">再次申请罚款</view>
<text class="star {{item.active ? 'star-active' : ''}}" bindtap="dianjiXingxing" data-index="{{index+1}}">{{item.active ? '★' : '☆'}}</text> </block>
</block> </view>
</view> </view>
</view>
<!-- 结算留言输入框 --> <!-- 更换接单员后生成的新订单ID展示卡片 -->
<textarea class="liuyan-input" placeholder="留言选填50字" maxlength="50" value="{{jiesuanLiuyan}}" bindinput="shuruLiuyan"/> <view class="card new-order-card" wx:if="{{xinDingdanId}}">
<!-- 结算按钮 --> <view class="card-title">更换接单员成功</view>
<view class="jiesuan-btn" bindtap="showJiesuanConfirm">立即结算</view> <view class="info-row"><text class="label">新订单ID</text><view class="value-with-copy"><text class="value">{{xinDingdanId}}</text><text class="copy-btn" bindtap="fuzhiWenben" data-text="{{xinDingdanId}}">📋</text></view></view>
</view> </view>
</view>
<!-- 结算区域卡片仅当订单状态为8结算中时显示 -->
<!-- 底部固定操作栏 --> <view class="card jiesuan-card" wx:if="{{jibenShuju.zhuangtai == 8 && canSettle}}">
<view class="bottom-bar"> <view class="card-title">订单结算</view>
<!-- 第一行按钮:联系客服、联系接单员 --> <view class="star-row">
<view class="bottom-btn-group"> <text class="label">打星评分</text>
<view class="btn-outline" bindtap="goToKefu">联系客服</view> <view class="stars">
<view class="btn-outline" bindtap="goToChatWithDashou" wx:if="{{dashouInfo.yonghuid}}">联系接单员</view> <!-- 循环渲染五颗星星 -->
</view> <block wx:for="{{xingxingData}}" wx:key="id">
<!-- 第二行按钮:各种操作按钮,根据订单状态显示 --> <!-- 点击星星触发评分 -->
<view class="bottom-btn-group"> <text class="star {{item.active ? 'star-active' : ''}}" bindtap="dianjiXingxing" data-index="{{index+1}}">{{item.active ? '★' : '☆'}}</text>
<!-- 撤销按钮状态为1或7时显示 --> </block>
<view class="btn-warn" bindtap="showChexiaoBtn" wx:if="{{jibenShuju.zhuangtai == 1 || jibenShuju.zhuangtai == 7}}">撤销订单</view> </view>
<!-- 【暂时隐藏】更换接单员按钮:恢复时取消下面整段注释即可 --> </view>
<!-- <view class="btn-primary" bindtap="showGhDashouConfirm" wx:if="{{(jibenShuju.zhuangtai == 2 || jibenShuju.zhuangtai == 8) && dashouInfo.yonghuid}}">更换接单员</view> --> <!-- 结算留言输入框 -->
<!-- 申请退款按钮状态为2或8时显示 --> <textarea class="liuyan-input" placeholder="留言选填50字" maxlength="50" value="{{jiesuanLiuyan}}" bindinput="shuruLiuyan"/>
<view class="btn-danger" bindtap="showTuikuanBtn" wx:if="{{jibenShuju.zhuangtai == 2 || jibenShuju.zhuangtai == 8}}">申请退款</view> <!-- 结算按钮 -->
</view> <view class="jiesuan-btn" bindtap="showJiesuanConfirm">立即结算</view>
</view> </view>
</view>
<!-- 撤销订单弹窗 -->
<view class="modal" wx:if="{{showChexiaoModal}}"> <!-- 底部固定操作栏 -->
<view class="modal-mask" bindtap="closeChexiaoModal"></view> <view class="bottom-bar">
<view class="modal-content"> <!-- 第一行按钮:联系客服、联系接单员 -->
<view class="modal-hd">撤销订单<text class="modal-close" bindtap="closeChexiaoModal">×</text></view> <view class="bottom-btn-group">
<textarea class="modal-input" placeholder="输入撤销原因最多50字" maxlength="50" value="{{chexiaoLiyou}}" bindinput="shuruChexiaoLiyou"/> <view class="btn-outline" bindtap="goToKefu">联系客服</view>
<view class="modal-btns"> <view class="btn-outline" bindtap="goToChatWithDashou" wx:if="{{dashouInfo.yonghuid}}">联系接单员</view>
<view class="modal-btn cancel" bindtap="closeChexiaoModal">取消</view> </view>
<view class="modal-btn confirm" bindtap="tijiaoChexiao">{{isChexiaoLoading ? '处理中' : '确认撤销'}}</view> <!-- 第二行按钮:各种操作按钮,根据订单状态显示 -->
</view> <view class="bottom-btn-group">
</view> <!-- 撤销按钮状态为1或7时显示 -->
</view> <view class="btn-warn" bindtap="showChexiaoBtn" wx:if="{{canCancel && (jibenShuju.zhuangtai == 1 || jibenShuju.zhuangtai == 7)}}">撤销订单</view>
<!-- 【暂时隐藏】更换接单员按钮:恢复时取消下面整段注释即可 -->
<!-- 申请退款弹窗 --> <!-- <view class="btn-primary" bindtap="showGhDashouConfirm" wx:if="{{canChangePlayer && (jibenShuju.zhuangtai == 2 || jibenShuju.zhuangtai == 8) && dashouInfo.yonghuid}}">更换接单员</view> -->
<view class="modal" wx:if="{{showTuikuanModal}}"> <!-- 申请退款按钮状态为2或8时显示 -->
<view class="modal-mask" bindtap="closeTuikuanModal"></view> <view class="btn-danger" bindtap="showTuikuanBtn" wx:if="{{canRefund && (jibenShuju.zhuangtai == 2 || jibenShuju.zhuangtai == 8)}}">申请退款</view>
<view class="modal-content"> </view>
<view class="modal-hd">申请退款<text class="modal-close" bindtap="closeTuikuanModal">×</text></view> </view>
<textarea class="modal-input" placeholder="输入退款原因最多50字" maxlength="50" value="{{tuikuanLiyou}}" bindinput="shuruTuikuanLiyou"/>
<view class="modal-btns"> <!-- 撤销订单弹窗 -->
<view class="modal-btn cancel" bindtap="closeTuikuanModal">取消</view> <view class="modal" wx:if="{{showChexiaoModal}}">
<view class="modal-btn confirm" bindtap="tijiaoTuikuan">{{isTuikuanLoading ? '处理中' : '确认申请'}}</view> <view class="modal-mask" bindtap="closeChexiaoModal"></view>
</view> <view class="modal-content">
</view> <view class="modal-hd">撤销订单<text class="modal-close" bindtap="closeChexiaoModal">×</text></view>
</view> <textarea class="modal-input" placeholder="输入撤销原因最多50字" maxlength="50" value="{{chexiaoLiyou}}" bindinput="shuruChexiaoLiyou"/>
<view class="modal-btns">
<!-- 更换接单员确认弹窗 --> <view class="modal-btn cancel" bindtap="closeChexiaoModal">取消</view>
<view class="modal" wx:if="{{showGhDashouModal}}"> <view class="modal-btn confirm" bindtap="tijiaoChexiao">{{isChexiaoLoading ? '处理中' : '确认撤销'}}</view>
<view class="modal-mask" bindtap="closeGhDashouModal"></view> </view>
<view class="modal-content"> </view>
<view class="modal-hd">更换接单员<text class="modal-close" bindtap="closeGhDashouModal">×</text></view> </view>
<view class="modal-body">
<text class="modal-tip">确认更换接单员吗?当前订单将被撤销,并生成一条相同内容的新订单(接单员清空)。</text> <!-- 申请退款弹窗 -->
</view> <view class="modal" wx:if="{{showTuikuanModal}}">
<view class="modal-btns"> <view class="modal-mask" bindtap="closeTuikuanModal"></view>
<view class="modal-btn cancel" bindtap="closeGhDashouModal">取消</view> <view class="modal-content">
<view class="modal-btn confirm" bindtap="tijiaoGhDashou">{{isGhDashouLoading ? '处理中' : '确认更换'}}</view> <view class="modal-hd">申请退款<text class="modal-close" bindtap="closeTuikuanModal">×</text></view>
</view> <textarea class="modal-input" placeholder="输入退款原因最多50字" maxlength="50" value="{{tuikuanLiyou}}" bindinput="shuruTuikuanLiyou"/>
</view> <view class="modal-btns">
</view> <view class="modal-btn cancel" bindtap="closeTuikuanModal">取消</view>
<view class="modal-btn confirm" bindtap="tijiaoTuikuan">{{isTuikuanLoading ? '处理中' : '确认申请'}}</view>
<!-- 申请罚款弹窗 --> </view>
<view class="modal" wx:if="{{showFakuanModal}}"> </view>
<view class="modal-mask" bindtap="closeFakuanModal"></view> </view>
<view class="modal-content">
<view class="modal-hd">申请罚款<text class="modal-close" bindtap="closeFakuanModal">×</text></view> <!-- 更换接单员确认弹窗 -->
<view class="modal-body"> <view class="modal" wx:if="{{showGhDashouModal}}">
<text class="modal-tip">罚款原因</text> <view class="modal-mask" bindtap="closeGhDashouModal"></view>
<textarea class="modal-input" placeholder="输入罚款原因最多50字" maxlength="50" value="{{fakuanLiyou}}" bindinput="inputFakuanLiyou"/> <view class="modal-content">
<text class="modal-tip">罚款金额</text> <view class="modal-hd">更换接单员<text class="modal-close" bindtap="closeGhDashouModal">×</text></view>
<input class="modal-input-num" type="digit" placeholder="0.00" value="{{fakuanJine}}" bindinput="inputFakuanJine"/> <view class="modal-body">
<view class="toggle-row" bindtap="toggleQiangdan"> <text class="modal-tip">确认更换接单员吗?当前订单将被撤销,并生成一条相同内容的新订单(接单员清空)。</text>
<text class="label">是否影响抢单</text> </view>
<view class="toggle {{yingxiangQiangdan ? 'toggle-on' : ''}}"> <view class="modal-btns">
<view class="toggle-knob"></view> <view class="modal-btn cancel" bindtap="closeGhDashouModal">取消</view>
</view> <view class="modal-btn confirm" bindtap="tijiaoGhDashou">{{isGhDashouLoading ? '处理中' : '确认更换'}}</view>
</view> </view>
<view class="fenhong-tip">此次罚款应得分红:¥{{jisuanFenhong()}}(利率 {{fenhongLilv*100}}%</view> </view>
</view> </view>
<view class="modal-btns">
<view class="modal-btn cancel" bindtap="closeFakuanModal">取消</view> <!-- 申请罚款弹窗 -->
<view class="modal-btn confirm" bindtap="tijiaoFakuan">{{isFakuanLoading ? '处理中' : '确认申请'}}</view> <view class="modal" wx:if="{{showFakuanModal}}">
</view> <view class="modal-mask" bindtap="closeFakuanModal"></view>
</view> <view class="modal-content">
</view> <view class="modal-hd">申请罚款<text class="modal-close" bindtap="closeFakuanModal">×</text></view>
<view class="modal-body">
<!-- 结算确认弹窗 --> <text class="modal-tip">罚款原因</text>
<view class="modal" wx:if="{{showJiesuanConfirm}}"> <textarea class="modal-input" placeholder="输入罚款原因最多50字" maxlength="50" value="{{fakuanLiyou}}" bindinput="inputFakuanLiyou"/>
<view class="modal-mask" bindtap="closeJiesuanConfirm"></view> <text class="modal-tip">罚款金额</text>
<view class="modal-content"> <input class="modal-input-num" type="digit" placeholder="0.00" value="{{fakuanJine}}" bindinput="inputFakuanJine"/>
<view class="modal-hd">确认结算<text class="modal-close" bindtap="closeJiesuanConfirm">×</text></view> <view class="toggle-row" bindtap="toggleQiangdan">
<view class="modal-body"> <text class="label">是否影响抢单</text>
<text class="modal-tip">确认结算该订单吗?结算后状态变为“已完成”。</text> <view class="toggle {{yingxiangQiangdan ? 'toggle-on' : ''}}">
</view> <view class="toggle-knob"></view>
<view class="modal-btns"> </view>
<view class="modal-btn cancel" bindtap="closeJiesuanConfirm">取消</view> </view>
<view class="modal-btn confirm" bindtap="tijiaoJiesuan">{{isJiesuanLoading ? '处理中' : '确认结算'}}</view> <view class="fenhong-tip">此次罚款应得分红:¥{{jisuanFenhong()}}(利率 {{fenhongLilv*100}}%</view>
</view> </view>
</view> <view class="modal-btns">
</view> <view class="modal-btn cancel" bindtap="closeFakuanModal">取消</view>
<view class="modal-btn confirm" bindtap="tijiaoFakuan">{{isFakuanLoading ? '处理中' : '确认申请'}}</view>
<!-- ========== 新增:修改罚款弹窗 ========== --> </view>
<view class="modal" wx:if="{{showModifyFakuanModal}}"> </view>
<view class="modal-mask" bindtap="closeModifyFakuanModal"></view> </view>
<view class="modal-content">
<view class="modal-hd">修改罚款<text class="modal-close" bindtap="closeModifyFakuanModal">×</text></view> <!-- 结算确认弹窗 -->
<view class="modal-body"> <view class="modal" wx:if="{{showJiesuanConfirm}}">
<text class="modal-tip">罚款理由</text> <view class="modal-mask" bindtap="closeJiesuanConfirm"></view>
<textarea class="modal-input" placeholder="输入罚款理由" maxlength="100" value="{{modifyFakuanLiyou}}" bindinput="inputModifyFakuanLiyou"/> <view class="modal-content">
<text class="modal-tip">罚款金额</text> <view class="modal-hd">确认结算<text class="modal-close" bindtap="closeJiesuanConfirm">×</text></view>
<input class="modal-input-num" type="digit" placeholder="0.00" value="{{modifyFakuanJine}}" bindinput="inputModifyFakuanJine"/> <view class="modal-body">
</view> <text class="modal-tip">确认结算该订单吗?结算后状态变为“已完成”。</text>
<view class="modal-btns"> </view>
<view class="modal-btn cancel" bindtap="closeModifyFakuanModal">取消</view> <view class="modal-btns">
<view class="modal-btn confirm" bindtap="submitModifyFakuan">确认修改</view> <view class="modal-btn cancel" bindtap="closeJiesuanConfirm">取消</view>
</view> <view class="modal-btn confirm" bindtap="tijiaoJiesuan">{{isJiesuanLoading ? '处理中' : '确认结算'}}</view>
</view> </view>
</view> </view>
</view>
<!-- 撤销罚款弹窗 -->
<view class="modal" wx:if="{{showCancelFakuanModal}}"> <!-- ========== 新增:修改罚款弹窗 ========== -->
<view class="modal-mask" bindtap="closeCancelFakuanModal"></view> <view class="modal" wx:if="{{showModifyFakuanModal}}">
<view class="modal-content"> <view class="modal-mask" bindtap="closeModifyFakuanModal"></view>
<view class="modal-hd">撤销罚款<text class="modal-close" bindtap="closeCancelFakuanModal">×</text></view> <view class="modal-content">
<view class="modal-body"> <view class="modal-hd">修改罚款<text class="modal-close" bindtap="closeModifyFakuanModal">×</text></view>
<text class="modal-tip">驳回理由</text> <view class="modal-body">
<textarea class="modal-input" placeholder="输入驳回理由" maxlength="100" value="{{cancelFakuanLiyou}}" bindinput="inputCancelFakuanLiyou"/> <text class="modal-tip">罚款理由</text>
</view> <textarea class="modal-input" placeholder="输入罚款理由" maxlength="100" value="{{modifyFakuanLiyou}}" bindinput="inputModifyFakuanLiyou"/>
<view class="modal-btns"> <text class="modal-tip">罚款金额</text>
<view class="modal-btn cancel" bindtap="closeCancelFakuanModal">取消</view> <input class="modal-input-num" type="digit" placeholder="0.00" value="{{modifyFakuanJine}}" bindinput="inputModifyFakuanJine"/>
<view class="modal-btn confirm" bindtap="submitCancelFakuan">确认撤销</view> </view>
</view> <view class="modal-btns">
</view> <view class="modal-btn cancel" bindtap="closeModifyFakuanModal">取消</view>
</view> <view class="modal-btn confirm" bindtap="submitModifyFakuan">确认修改</view>
</view>
<!-- 再次申请罚款弹窗 --> </view>
<view class="modal" wx:if="{{showResubmitFakuanModal}}"> </view>
<view class="modal-mask" bindtap="closeResubmitFakuanModal"></view>
<view class="modal-content"> <!-- 撤销罚款弹窗 -->
<view class="modal-hd">再次申请罚款<text class="modal-close" bindtap="closeResubmitFakuanModal">×</text></view> <view class="modal" wx:if="{{showCancelFakuanModal}}">
<view class="modal-body"> <view class="modal-mask" bindtap="closeCancelFakuanModal"></view>
<text class="modal-tip">罚款理由</text> <view class="modal-content">
<textarea class="modal-input" placeholder="输入罚款理由" maxlength="100" value="{{resubmitFakuanLiyou}}" bindinput="inputResubmitFakuanLiyou"/> <view class="modal-hd">撤销罚款<text class="modal-close" bindtap="closeCancelFakuanModal">×</text></view>
<text class="modal-tip">罚款金额</text> <view class="modal-body">
<input class="modal-input-num" type="digit" placeholder="0.00" value="{{resubmitFakuanJine}}" bindinput="inputResubmitFakuanJine"/> <text class="modal-tip">驳回理由</text>
</view> <textarea class="modal-input" placeholder="输入驳回理由" maxlength="100" value="{{cancelFakuanLiyou}}" bindinput="inputCancelFakuanLiyou"/>
<view class="modal-btns"> </view>
<view class="modal-btn cancel" bindtap="closeResubmitFakuanModal">取消</view> <view class="modal-btns">
<view class="modal-btn confirm" bindtap="submitResubmitFakuan">确认申请</view> <view class="modal-btn cancel" bindtap="closeCancelFakuanModal">取消</view>
</view> <view class="modal-btn confirm" bindtap="submitCancelFakuan">确认撤销</view>
</view> </view>
</view> </view>
</view>
<!-- 撤销退款申请弹窗 -->
<view class="modal" wx:if="{{showCancelTuikuanModal}}"> <!-- 再次申请罚款弹窗 -->
<view class="modal-mask" bindtap="closeCancelTuikuanModal"></view> <view class="modal" wx:if="{{showResubmitFakuanModal}}">
<view class="modal-content"> <view class="modal-mask" bindtap="closeResubmitFakuanModal"></view>
<view class="modal-hd">撤销退款申请<text class="modal-close" bindtap="closeCancelTuikuanModal">×</text></view> <view class="modal-content">
<view class="modal-body"> <view class="modal-hd">再次申请罚款<text class="modal-close" bindtap="closeResubmitFakuanModal">×</text></view>
<text class="modal-tip">撤销理由</text> <view class="modal-body">
<textarea class="modal-input" placeholder="输入撤销理由" maxlength="100" value="{{cancelTuikuanLiyou}}" bindinput="inputCancelTuikuanLiyou"/> <text class="modal-tip">罚款理由</text>
</view> <textarea class="modal-input" placeholder="输入罚款理由" maxlength="100" value="{{resubmitFakuanLiyou}}" bindinput="inputResubmitFakuanLiyou"/>
<view class="modal-btns"> <text class="modal-tip">罚款金额</text>
<view class="modal-btn cancel" bindtap="closeCancelTuikuanModal">取消</view> <input class="modal-input-num" type="digit" placeholder="0.00" value="{{resubmitFakuanJine}}" bindinput="inputResubmitFakuanJine"/>
<view class="modal-btn confirm" bindtap="submitCancelTuikuan">确认撤销</view> </view>
</view> <view class="modal-btns">
</view> <view class="modal-btn cancel" bindtap="closeResubmitFakuanModal">取消</view>
</view> <view class="modal-btn confirm" bindtap="submitResubmitFakuan">确认申请</view>
</view>
<!-- 修改退款理由弹窗 --> </view>
<view class="modal" wx:if="{{showModifyTuikuanModal}}"> </view>
<view class="modal-mask" bindtap="closeModifyTuikuanModal"></view>
<view class="modal-content"> <!-- 撤销退款申请弹窗 -->
<view class="modal-hd">修改退款理由<text class="modal-close" bindtap="closeModifyTuikuanModal">×</text></view> <view class="modal" wx:if="{{showCancelTuikuanModal}}">
<view class="modal-body"> <view class="modal-mask" bindtap="closeCancelTuikuanModal"></view>
<text class="modal-tip">退款理由</text> <view class="modal-content">
<textarea class="modal-input" placeholder="输入新的退款理由" maxlength="100" value="{{modifyTuikuanLiyou}}" bindinput="inputModifyTuikuanLiyou"/> <view class="modal-hd">撤销退款申请<text class="modal-close" bindtap="closeCancelTuikuanModal">×</text></view>
</view> <view class="modal-body">
<view class="modal-btns"> <text class="modal-tip">撤销理由</text>
<view class="modal-btn cancel" bindtap="closeModifyTuikuanModal">取消</view> <textarea class="modal-input" placeholder="输入撤销理由" maxlength="100" value="{{cancelTuikuanLiyou}}" bindinput="inputCancelTuikuanLiyou"/>
<view class="modal-btn confirm" bindtap="submitModifyTuikuan">确认修改</view> </view>
</view> <view class="modal-btns">
</view> <view class="modal-btn cancel" bindtap="closeCancelTuikuanModal">取消</view>
</view> <view class="modal-btn confirm" bindtap="submitCancelTuikuan">确认撤销</view>
</view>
<!-- 全局通知组件 --> </view>
<global-notification id="global-notification" /> </view>
<!-- 教程按钮紧贴右侧右上角5秒后缩进 --> <!-- 修改退款理由弹窗 -->
<view <view class="modal" wx:if="{{showModifyTuikuanModal}}">
class="tutorial-btn {{tutorialBtnHidden ? 'tutorial-btn-hidden' : ''}}" <view class="modal-mask" bindtap="closeModifyTuikuanModal"></view>
bindtap="onTutorialBtnTap" <view class="modal-content">
hover-class="tutorial-btn-hover" <view class="modal-hd">修改退款理由<text class="modal-close" bindtap="closeModifyTuikuanModal">×</text></view>
> <view class="modal-body">
<text class="tutorial-btn-text">教程</text> <text class="modal-tip">退款理由</text>
</view> <textarea class="modal-input" placeholder="输入新的退款理由" maxlength="100" value="{{modifyTuikuanLiyou}}" bindinput="inputModifyTuikuanLiyou"/>
</view>
<!-- 使用教程弹窗组件 --> <view class="modal-btns">
<popup-notice id="popupNotice" /> <view class="modal-btn cancel" bindtap="closeModifyTuikuanModal">取消</view>
<view class="modal-btn confirm" bindtap="submitModifyTuikuan">确认修改</view>
</view>
</view>
</view>
<!-- 全局通知组件 -->
<global-notification id="global-notification" />
<!-- 教程按钮紧贴右侧右上角5秒后缩进 -->
<view
class="tutorial-btn {{tutorialBtnHidden ? 'tutorial-btn-hidden' : ''}}"
bindtap="onTutorialBtnTap"
hover-class="tutorial-btn-hover"
>
<text class="tutorial-btn-text">教程</text>
</view>
<!-- 使用教程弹窗组件 -->
<popup-notice id="popupNotice" />
</view> </view>

View File

@@ -3,6 +3,8 @@ const app = getApp();
import { createPage, request } from '../../utils/base-page.js'; import { createPage, request } from '../../utils/base-page.js';
import { reconnectForRole } from '../../utils/role-tab-bar.js'; import { reconnectForRole } from '../../utils/role-tab-bar.js';
import { getOrderStatusText } from '../../utils/api-helper.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';
Page(createPage({ Page(createPage({
data: { data: {
@@ -49,12 +51,19 @@ Page(createPage({
}, },
onLoad() { onLoad() {
wx.setNavigationBarTitle({ title: '我的派单' }); wx.setNavigationBarTitle({ title: isStaffMode() ? '客服派单' : '我的派单' });
syncStaffUi(this);
if (isStaffMode()) {
refreshStaffContext(request).then(() => syncStaffUi(this));
}
this.loadShangpinLeixing(); this.loadShangpinLeixing();
this.registerNotificationComponent(); this.registerNotificationComponent();
}, },
onShow() { async onShow() {
const phoneOk = await ensurePhoneAuth({ redirect: true, role: 'shangjia' });
if (!phoneOk) return;
this.registerNotificationComponent(); this.registerNotificationComponent();
this.loadPendingCount(); this.loadPendingCount();
if (wx.getStorageSync('uid')) { if (wx.getStorageSync('uid')) {
@@ -169,7 +178,7 @@ Page(createPage({
}; };
const res = await request({ const res = await request({
url: '/dingdan/sjdingdanhq', // 商家订单接口(和原来一样) url: isStaffMode() ? STAFF_API.orderList : '/dingdan/sjdingdanhq',
method: 'POST', method: 'POST',
data: params, data: params,
header: { 'content-type': 'application/json' } header: { 'content-type': 'application/json' }
@@ -247,7 +256,7 @@ Page(createPage({
async loadPendingCount() { async loadPendingCount() {
try { try {
const res = await request({ const res = await request({
url: '/dingdan/sjdingdanhq', url: isStaffMode() ? STAFF_API.orderList : '/dingdan/sjdingdanhq',
method: 'POST', method: 'POST',
data: { data: {
zhuangtai_list: [8], zhuangtai_list: [8],

View File

@@ -1,5 +1,6 @@
// pages/shangjia-chongzhi/index.js // pages/shangjia-chongzhi/index.js
import request from '../../utils/request'; import request from '../../utils/request';
import { guardMerchantOwnerAction } from '../../utils/staff-api.js';
Page({ Page({
data: { data: {
@@ -37,6 +38,10 @@ Page({
}, },
onLoad() { onLoad() {
if (!guardMerchantOwnerAction('仅商家老板可充值')) {
setTimeout(() => wx.navigateBack(), 1200);
return;
}
this.initPage(); this.initPage();
}, },

View File

@@ -0,0 +1,99 @@
import request from '../../utils/request.js';
import { STAFF_API } from '../../utils/staff-api.js';
const ORDER_AUDIT_ACTIONS = [
'DISPATCH', 'LINK_GENERATE', 'SETTLE', 'CANCEL', 'REFUND_APPLY',
'CHANGE_PLAYER', 'PENALTY_APPLY', 'PENALTY_MANAGE',
];
Page({
data: {
tab: 'audit',
auditList: [],
rankList: [],
page: 1,
hasMore: true,
loading: false,
total: 0,
},
onLoad(options) {
if (options.tab === 'rank') this.setData({ tab: 'rank' });
wx.setNavigationBarTitle({
title: options.tab === 'rank' ? '客服排行' : '操作日志',
});
this.loadData(true);
},
onPullDownRefresh() {
this.loadData(true).finally(() => wx.stopPullDownRefresh());
},
onReachBottom() {
if (this.data.tab === 'audit' && this.data.hasMore && !this.data.loading) {
this.loadAudit(false);
}
},
switchTab(e) {
const tab = e.currentTarget.dataset.tab;
wx.setNavigationBarTitle({ title: tab === 'rank' ? '客服排行' : '操作日志' });
this.setData({ tab, page: 1, hasMore: true }, () => this.loadData(true));
},
loadData(reset) {
if (this.data.tab === 'audit') return this.loadAudit(reset);
return this.loadRank();
},
async loadAudit(reset) {
if (this.data.loading) return;
const page = reset ? 1 : this.data.page;
this.setData({ loading: true });
try {
const res = await request({
url: STAFF_API.auditList,
method: 'POST',
data: { page, page_size: 20 },
});
if (res.data && res.data.code === 0) {
const d = res.data.data || {};
const list = (d.list || []).map((item) => ({
...item,
canJumpOrder: ORDER_AUDIT_ACTIONS.indexOf(item.action) >= 0 && !!item.resource_id,
}));
this.setData({
auditList: reset ? list : [...this.data.auditList, ...list],
page: page + 1,
hasMore: !!d.has_more,
total: d.total || 0,
loading: false,
});
} else {
this.setData({ loading: false });
wx.showToast({ title: res.data?.msg || '加载失败', icon: 'none' });
}
} catch (e) {
this.setData({ loading: false });
}
},
async loadRank() {
const res = await request({
url: STAFF_API.rank,
method: 'POST',
data: { period: 'month', sort_by: 'dispatch_count' },
});
if (res.data && res.data.code === 0) {
this.setData({ rankList: (res.data.data && res.data.data.list) || [] });
}
},
goOrderDetail(e) {
const item = e.currentTarget.dataset.item;
if (!item || !item.canJumpOrder || !item.resource_id) return;
wx.navigateTo({
url: `/pages/merchant-order-detail/merchant-order-detail?dingdan_id=${encodeURIComponent(item.resource_id)}`,
});
},
});

View File

@@ -0,0 +1,5 @@
{
"navigationBarTitleText": "操作日志",
"enablePullDownRefresh": true,
"usingComponents": {}
}

View File

@@ -0,0 +1,39 @@
<view class="page">
<view class="head-tip">仅记录派单、退款、模板等重要操作,不含普通查看</view>
<view class="tabs">
<view class="tab {{tab === 'audit' ? 'on' : ''}}" data-tab="audit" bindtap="switchTab">操作日志</view>
<view class="tab {{tab === 'rank' ? 'on' : ''}}" data-tab="rank" bindtap="switchTab">客服排行</view>
</view>
<block wx:if="{{tab === 'audit'}}">
<view class="summary" wx:if="{{total > 0}}">共 {{total}} 条重要操作</view>
<view class="item {{item.canJumpOrder ? 'item-link' : ''}}" wx:for="{{auditList}}" wx:key="id"
bindtap="goOrderDetail" data-item="{{item}}">
<view class="item-hd flexb">
<text class="act">{{item.action_label || item.action}}</text>
<text class="amount" wx:if="{{item.amount}}">¥{{item.amount}}</text>
</view>
<view class="operator">
<text class="tag {{item.operator_type === 'STAFF' ? 'tag-staff' : 'tag-owner'}}">
{{item.role_name || (item.operator_type === 'STAFF' ? '客服' : '老板')}}
</text>
<text class="name">{{item.operator_name}}</text>
<text class="uid">ID:{{item.operator_user_id}}</text>
</view>
<text class="meta">{{item.time}}</text>
<text class="res" wx:if="{{item.resource_id}}">订单:{{item.resource_id}}</text>
<text class="jump-tip" wx:if="{{item.canJumpOrder}}">点击查看订单详情 </text>
<text class="remark" wx:if="{{item.remark}}">{{item.remark}}</text>
</view>
<view class="load-tip" wx:if="{{loading}}">加载中...</view>
<view class="load-tip" wx:elif="{{hasMore && auditList.length}}">上拉加载更多</view>
<view class="empty" wx:if="{{!loading && auditList.length === 0}}">暂无操作日志</view>
</block>
<block wx:else>
<view class="item" wx:for="{{rankList}}" wx:key="member_id">
<text class="rank">#{{item.rank}} {{item.display_name}}</text>
<text class="meta">派单 {{item.dispatch_count}} 单 · ¥{{item.dispatch_amount}}</text>
<text class="meta">结单 {{item.settle_count}} 单 · ¥{{item.settle_amount}}</text>
</view>
<view class="empty" wx:if="{{rankList.length === 0}}">暂无排行数据</view>
</block>
</view>

View File

@@ -0,0 +1,25 @@
.page { padding: 24rpx; background: #f5f5f5; min-height: 100vh; box-sizing: border-box; }
.head-tip { font-size: 24rpx; color: #888; margin-bottom: 16rpx; line-height: 1.5; }
.tabs { display: flex; margin-bottom: 24rpx; }
.tab { flex: 1; text-align: center; padding: 20rpx; background: #fff; margin-right: 8rpx; border-radius: 8rpx; font-size: 28rpx; }
.tab.on { background: #C9A962; color: #fff; }
.summary { font-size: 24rpx; color: #666; margin-bottom: 16rpx; }
.item { background: #fff; padding: 24rpx; margin-bottom: 16rpx; border-radius: 12rpx; }
.item-hd { margin-bottom: 12rpx; }
.act { font-size: 30rpx; color: #333; font-weight: 600; }
.amount { font-size: 28rpx; color: #C9A962; }
.operator { display: flex; align-items: center; flex-wrap: wrap; gap: 12rpx; margin-bottom: 8rpx; }
.tag { font-size: 22rpx; padding: 4rpx 12rpx; border-radius: 6rpx; }
.tag-staff { background: #e8f4ff; color: #1976d2; }
.tag-owner { background: #fff3e0; color: #e65100; }
.name { font-size: 26rpx; color: #333; }
.uid { font-size: 22rpx; color: #999; }
.meta { display: block; font-size: 24rpx; color: #999; margin-top: 6rpx; }
.res { display: block; font-size: 24rpx; color: #666; margin-top: 8rpx; word-break: break-all; }
.remark { display: block; font-size: 24rpx; color: #888; margin-top: 6rpx; }
.rank { font-weight: 600; font-size: 28rpx; display: block; }
.load-tip { text-align: center; color: #999; padding: 24rpx; font-size: 24rpx; }
.empty { text-align: center; color: #999; padding: 80rpx; }
.item-link { border-left: 6rpx solid #C9A962; }
.jump-tip { display: block; font-size: 24rpx; color: #C9A962; margin-top: 8rpx; }
.flexb { display: flex; justify-content: space-between; align-items: center; }

View File

@@ -0,0 +1,236 @@
/** 商家老板:角色与权限管理 */
import request from '../../utils/request.js';
import { STAFF_API, isStaffMode, isMerchantOwnerOnly } from '../../utils/staff-api.js';
/** 与后端 constants.PERMISSION_DEFINITIONS 一致,接口为空时前端兜底 */
const FALLBACK_PERMISSIONS = [
{ perm_code: 'staff_manage', perm_name: '子客服管理' },
{ perm_code: 'role_manage', perm_name: '角色管理' },
{ perm_code: 'wallet_allocate', perm_name: '划额度' },
{ perm_code: 'wallet_revoke', perm_name: '收回额度' },
{ perm_code: 'wallet_view', perm_name: '查看额度' },
{ perm_code: 'finance_view', perm_name: '查看商家余额' },
{ perm_code: 'order_dispatch', perm_name: '派单发单' },
{ perm_code: 'order_view_all', perm_name: '查看全部订单' },
{ perm_code: 'order_view_self', perm_name: '只看自己派的单' },
{ perm_code: 'order_detail', perm_name: '订单详情' },
{ perm_code: 'order_settle', perm_name: '结单' },
{ perm_code: 'order_refund_apply', perm_name: '申请退款' },
{ perm_code: 'order_cancel', perm_name: '撤销订单' },
{ perm_code: 'order_change_player', perm_name: '换打手' },
{ perm_code: 'penalty_apply', perm_name: '申请罚单' },
{ perm_code: 'penalty_manage', perm_name: '改撤罚单' },
{ perm_code: 'penalty_view', perm_name: '查看罚单' },
{ perm_code: 'staff_disable', perm_name: '封禁解封客服' },
{ perm_code: 'template_manage', perm_name: '派单模板' },
{ perm_code: 'audit_view', perm_name: '操作日志' },
{ perm_code: 'rank_view', perm_name: '客服排行榜' },
{ perm_code: 'im_chat', perm_name: '订单IM' },
{ perm_code: 'stats_view', perm_name: '统计数据' },
];
Page({
data: {
statusBar: 20,
permScrollHeight: 360,
loading: false,
roles: [],
rawPermissions: [],
permissionOptions: [],
selectedPerms: [],
showEditPanel: false,
showCreatePanel: false,
editingRoleId: null,
editingRoleName: '',
newRoleName: '',
},
onLoad() {
const sys = wx.getSystemInfoSync();
const h = sys.windowHeight || 667;
this.setData({
statusBar: sys.statusBarHeight || 20,
permScrollHeight: Math.floor(h * 0.45),
});
if (!this.ensureOwnerAccess()) return;
this.loadRoles();
},
onShow() {
if (!isMerchantOwnerOnly()) return;
if (!this.data.showEditPanel && !this.data.showCreatePanel) {
this.loadRoles();
}
},
ensureOwnerAccess() {
if (isStaffMode() || Number(wx.getStorageSync('shangjiastatus')) !== 1) {
wx.showToast({ title: '仅商家老板可管理角色权限', icon: 'none' });
setTimeout(() => wx.navigateBack(), 1200);
return false;
}
return true;
},
/** allPerms: [{perm_code, perm_name}] selectedCodes: ['order_dispatch', ...] */
buildPermOptions(allPerms, selectedCodes) {
const list = (allPerms && allPerms.length) ? allPerms : FALLBACK_PERMISSIONS;
const set = new Set(selectedCodes || []);
return list.map((p) => {
const code = p.perm_code || p;
const name = p.perm_name || code;
return {
perm_code: code,
perm_name: name,
checked: set.has(code),
};
});
},
normalizeRoles(list) {
return (list || []).map((r) => ({
...r,
permCount: (r.permissions && r.permissions.length) || 0,
}));
},
resolveAllPermissions(apiList) {
if (apiList && apiList.length) return apiList;
return FALLBACK_PERMISSIONS;
},
async loadRoles() {
this.setData({ loading: true });
try {
const res = await request({ url: STAFF_API.roleList, method: 'POST' });
if (res.data && res.data.code === 0) {
const d = res.data.data || {};
const rawPermissions = this.resolveAllPermissions(d.all_permissions);
this.setData({
roles: this.normalizeRoles(d.roles),
rawPermissions,
});
return rawPermissions;
}
wx.showToast({ title: (res.data && res.data.msg) || '加载失败', icon: 'none' });
} catch (e) {
wx.showToast({ title: '加载失败', icon: 'none' });
} finally {
this.setData({ loading: false });
}
const fallback = FALLBACK_PERMISSIONS;
this.setData({ rawPermissions: fallback });
return fallback;
},
goBack() { wx.navigateBack(); },
openCreate() {
const raw = this.data.rawPermissions.length
? this.data.rawPermissions
: FALLBACK_PERMISSIONS;
this.setData({
showCreatePanel: true,
showEditPanel: false,
newRoleName: '',
selectedPerms: [],
permissionOptions: this.buildPermOptions(raw, []),
});
},
closeCreate() {
this.setData({ showCreatePanel: false, permissionOptions: [], selectedPerms: [] });
},
onNameInput(e) {
this.setData({ newRoleName: e.detail.value });
},
togglePermItem(e) {
const code = e.currentTarget.dataset.code;
if (!code) return;
const opts = (this.data.permissionOptions || []).map((p) => (
p.perm_code === code ? { ...p, checked: !p.checked } : p
));
this.setData({
permissionOptions: opts,
selectedPerms: opts.filter((x) => x.checked).map((x) => x.perm_code),
});
},
async editRole(e) {
const id = e.currentTarget.dataset.id;
let role = this.data.roles.find((r) => String(r.id) === String(id));
if (!role) return;
let rawPermissions = this.data.rawPermissions;
if (!rawPermissions.length) {
rawPermissions = await this.loadRoles();
role = this.data.roles.find((r) => String(r.id) === String(id));
if (!role) return;
}
const perms = role.permissions || [];
this.setData({
showEditPanel: true,
showCreatePanel: false,
editingRoleId: role.id,
editingRoleName: role.role_name || '',
selectedPerms: [...perms],
permissionOptions: this.buildPermOptions(rawPermissions, perms),
});
},
closeEdit() {
this.setData({
showEditPanel: false,
editingRoleId: null,
editingRoleName: '',
selectedPerms: [],
permissionOptions: [],
});
},
async saveRolePerms() {
const { editingRoleId, selectedPerms } = this.data;
if (!editingRoleId) return;
wx.showLoading({ title: '保存中' });
try {
const res = await request({
url: STAFF_API.roleSetPerms,
method: 'POST',
data: { role_id: editingRoleId, permissions: selectedPerms },
});
wx.showToast({ title: (res.data && res.data.msg) || '已保存', icon: 'none' });
if (res.data && res.data.code === 0) {
this.closeEdit();
this.loadRoles();
}
} finally {
wx.hideLoading();
}
},
async createRole() {
const name = (this.data.newRoleName || '').trim();
if (!name) {
wx.showToast({ title: '请输入角色名', icon: 'none' });
return;
}
wx.showLoading({ title: '创建中' });
try {
const res = await request({
url: STAFF_API.roleCreate,
method: 'POST',
data: { role_name: name, permissions: this.data.selectedPerms },
});
wx.showToast({ title: (res.data && res.data.msg) || '完成', icon: 'none' });
if (res.data && res.data.code === 0) {
this.closeCreate();
this.loadRoles();
}
} finally {
wx.hideLoading();
}
},
});

View File

@@ -0,0 +1,4 @@
{
"navigationBarTitleText": "角色与权限",
"navigationStyle": "custom"
}

View File

@@ -0,0 +1,75 @@
<view class="role-page">
<view class="status-bar" style="height:{{statusBar}}px"></view>
<view class="role-nav flexb">
<text class="nav-back" bindtap="goBack"></text>
<text class="nav-title">角色与权限</text>
<text class="nav-link" bindtap="openCreate">新建</text>
</view>
<scroll-view scroll-y class="role-scroll" enhanced show-scrollbar="{{false}}">
<view class="role-tip">可自由创建角色并勾选权限;预置角色也可改权限。</view>
<view wx:if="{{!roles.length && !loading}}" class="role-empty">暂无角色,请先新建或联系管理员同步权限</view>
<view wx:for="{{roles}}" wx:key="id" class="role-card" bindtap="editRole" data-id="{{item.id}}">
<view class="flexb">
<text class="role-name">{{item.role_name}}</text>
<text class="role-tag">{{item.is_system ? '系统' : '自定义'}}</text>
</view>
<text class="role-sub">{{item.role_hint || ''}}</text>
<text class="role-sub">已授权 {{item.permCount}} 项 · 点击编辑</text>
</view>
</scroll-view>
</view>
<view wx:if="{{showEditPanel}}" class="role-mask">
<view class="role-mask-bg" bindtap="closeEdit"></view>
<view class="role-panel">
<view class="panel-title">编辑角色权限</view>
<text class="panel-role-name">{{editingRoleName}}</text>
<scroll-view scroll-y class="perm-scroll" style="height:{{permScrollHeight}}px">
<view
wx:for="{{permissionOptions}}"
wx:key="perm_code"
class="perm-row flexb {{item.checked ? 'perm-on' : ''}}"
bindtap="togglePermItem"
data-code="{{item.perm_code}}"
>
<view class="perm-text">
<text class="perm-name">{{item.perm_name}}</text>
<text class="perm-code">{{item.perm_code}}</text>
</view>
<view class="perm-check">{{item.checked ? '✓' : ''}}</view>
</view>
</scroll-view>
<view class="panel-btns flexa">
<button class="panel-btn cancel" bindtap="closeEdit" hover-class="btn-hover">取消</button>
<button class="panel-btn ok" bindtap="saveRolePerms" hover-class="btn-hover">保存</button>
</view>
</view>
</view>
<view wx:if="{{showCreatePanel}}" class="role-mask">
<view class="role-mask-bg" bindtap="closeCreate"></view>
<view class="role-panel">
<view class="panel-title">新建角色</view>
<input class="name-input" placeholder="角色名称" value="{{newRoleName}}" bindinput="onNameInput"/>
<scroll-view scroll-y class="perm-scroll" style="height:{{permScrollHeight}}px">
<view
wx:for="{{permissionOptions}}"
wx:key="perm_code"
class="perm-row flexb {{item.checked ? 'perm-on' : ''}}"
bindtap="togglePermItem"
data-code="{{item.perm_code}}"
>
<view class="perm-text">
<text class="perm-name">{{item.perm_name}}</text>
<text class="perm-code">{{item.perm_code}}</text>
</view>
<view class="perm-check">{{item.checked ? '✓' : ''}}</view>
</view>
</scroll-view>
<view class="panel-btns flexa">
<button class="panel-btn cancel" bindtap="closeCreate" hover-class="btn-hover">取消</button>
<button class="panel-btn ok" bindtap="createRole" hover-class="btn-hover">创建</button>
</view>
</view>
</view>

View File

@@ -0,0 +1,221 @@
@import '../../styles/shangjia-xym-common.wxss';
.role-page {
min-height: 100vh;
background: linear-gradient(180deg, #f7dc51 0%, #fff 28%, #fff8e1 100%);
box-sizing: border-box;
}
.role-nav {
padding: 0 24rpx 16rpx;
}
.nav-back {
width: 60rpx;
font-size: 48rpx;
color: #333;
}
.nav-title {
font-size: 34rpx;
font-weight: 700;
color: #343434;
}
.nav-link {
width: 60rpx;
text-align: right;
font-size: 28rpx;
color: #c9a962;
}
.role-scroll {
height: calc(100vh - 120rpx);
}
.role-tip {
font-size: 24rpx;
color: #888;
margin: 20rpx 24rpx;
}
.role-empty {
text-align: center;
padding: 80rpx 24rpx;
font-size: 26rpx;
color: #999;
}
.role-card {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin: 0 24rpx 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
}
.role-name {
font-size: 30rpx;
font-weight: 600;
color: #333;
}
.role-tag {
font-size: 22rpx;
color: #999;
}
.role-sub {
display: block;
margin-top: 8rpx;
font-size: 24rpx;
color: #666;
}
/* 弹层:独立根节点,全屏 fixed */
.role-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
}
.role-mask-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
}
.role-panel {
position: absolute;
left: 0;
right: 0;
bottom: 0;
background: #fff;
border-radius: 24rpx 24rpx 0 0;
padding: 32rpx 24rpx calc(24rpx + env(safe-area-inset-bottom));
z-index: 1001;
max-height: 85vh;
box-sizing: border-box;
}
.panel-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.panel-role-name {
display: block;
margin-top: 8rpx;
margin-bottom: 16rpx;
font-size: 26rpx;
color: #888;
}
.perm-scroll {
width: 100%;
}
.perm-row {
padding: 20rpx 8rpx;
border-bottom: 1rpx solid #f0f0f0;
min-height: 88rpx;
box-sizing: border-box;
}
.perm-row.perm-on {
background: #fffbf0;
}
.perm-text {
flex: 1;
padding-right: 16rpx;
}
.perm-name {
display: block;
font-size: 28rpx;
color: #333;
line-height: 1.4;
}
.perm-code {
display: block;
margin-top: 4rpx;
font-size: 22rpx;
color: #999;
}
.perm-check {
width: 48rpx;
height: 48rpx;
line-height: 48rpx;
text-align: center;
font-size: 32rpx;
color: #c9a962;
font-weight: 700;
border: 2rpx solid #ddd;
border-radius: 8rpx;
flex-shrink: 0;
}
.perm-on .perm-check {
border-color: #c9a962;
background: #c9a962;
color: #fff;
}
.perm-empty {
padding: 40rpx 0;
text-align: center;
font-size: 26rpx;
color: #999;
}
.name-input {
border: 1rpx solid #eee;
padding: 20rpx;
margin: 16rpx 0;
border-radius: 8rpx;
font-size: 28rpx;
}
.panel-btns {
margin-top: 24rpx;
gap: 16rpx;
}
.panel-btn {
flex: 1;
margin: 0;
padding: 0;
height: 88rpx;
line-height: 88rpx;
font-size: 30rpx;
border-radius: 12rpx;
border: none;
}
.panel-btn::after {
border: none;
}
.panel-btn.cancel {
background: #f5f5f5;
color: #666;
}
.panel-btn.ok {
background: #c9a962;
color: #fff;
}
.btn-hover {
opacity: 0.85;
}

View File

@@ -2,6 +2,11 @@
import { createPage, ensureRoleOnCenterPage, lockPrimaryRole, parseSceneOptions, request } from '../../utils/base-page.js'; import { createPage, ensureRoleOnCenterPage, lockPrimaryRole, parseSceneOptions, request } from '../../utils/base-page.js';
import { ensureLogin } from '../../utils/login.js'; import { ensureLogin } from '../../utils/login.js';
import {
STAFF_API, isStaffMode, isMerchantPortalUser, refreshStaffContext, syncStaffUi, getStaffContext,
restoreStaffContextAfterAuth, applyStaffStatsToPage, guardMerchantOwnerAction, clearStaffSession,
} from '../../utils/staff-api.js';
import { clearCacheAndEnterNormal } from '../../utils/base-page.js';
const app = getApp(); const app = getApp();
@@ -32,7 +37,15 @@ Page(createPage({
// 商家状态 // 商家状态
isShangjia: false, isShangjia: false,
isStaffMode: false,
staffRoleName: '',
quotaAvailable: '0.00',
canDispatch: true,
canStaffManage: true,
canAudit: true,
showRechargeWithdraw: true,
isAutoRegistering: false, isAutoRegistering: false,
staffBooting: false,
// 用户基础信息 // 用户基础信息
uid: '', uid: '',
@@ -57,7 +70,7 @@ Page(createPage({
navBar: 44, navBar: 44,
}, },
onLoad(options) { async onLoad(options) {
const sys = wx.getSystemInfoSync(); const sys = wx.getSystemInfoSync();
this.setData({ this.setData({
statusBar: sys.statusBarHeight || 20, statusBar: sys.statusBarHeight || 20,
@@ -65,6 +78,9 @@ Page(createPage({
}); });
const parsed = parseSceneOptions(options); const parsed = parseSceneOptions(options);
this.setupImageUrls(); this.setupImageUrls();
if (wx.getStorageSync('token')) {
await restoreStaffContextAfterAuth();
}
this.checkRoleStatus(); this.checkRoleStatus();
const inviteCode = parsed.inviteCode; const inviteCode = parsed.inviteCode;
@@ -97,13 +113,33 @@ Page(createPage({
this.checkColdStartPopup('shangjiaduan'); this.checkColdStartPopup('shangjiaduan');
}, },
onShow() { async onShow() {
this.registerNotificationComponent(); this.registerNotificationComponent();
if (this.data.isShangjia) { try {
ensureRoleOnCenterPage(this, 'shangjia'); const token = wx.getStorageSync('token');
this.refreshShangjiaInfo(); const isOwner = Number(wx.getStorageSync('shangjiastatus')) === 1;
this.loadPendingCount(); if (token && !isOwner) {
this.loadPunishPending(); await restoreStaffContextAfterAuth();
} else if (isStaffMode()) {
await refreshStaffContext(request);
}
this.checkRoleStatus();
syncStaffUi(this);
if (this.data.isShangjia) {
ensureRoleOnCenterPage(this, 'shangjia');
if (isStaffMode()) {
await this.loadStaffProfile();
} else {
this.refreshShangjiaInfo();
}
this.loadPendingCount();
if (!isStaffMode()) {
this.loadPunishPending();
}
}
} catch (err) {
console.error('商家个人中心 onShow', err);
this.setData({ isLoading: false, staffBooting: false });
} }
}, },
@@ -137,27 +173,102 @@ Page(createPage({
}); });
}, },
// 商家状态检查(覆盖基类 checkRoleStatus增加 getShangjiaInfo 调用 // 商家状态检查(子客服与老板均可进入;子客服绝不走商家注册接口
checkRoleStatus() { checkRoleStatus() {
try { try {
const shangjiastatus = wx.getStorageSync('shangjiastatus');
const uid = wx.getStorageSync('uid'); const uid = wx.getStorageSync('uid');
if (shangjiastatus) { const portal = isMerchantPortalUser();
this.setData({ isShangjia: true, uid: uid || '' }); const staffFlag = Number(wx.getStorageSync('staffstatus')) === 1;
this.loadAvatar();
this.getShangjiaInfo(); if (portal) {
this.setData({ isShangjia: true, uid: uid || '', staffBooting: false, isLoading: false });
syncStaffUi(this);
if (isStaffMode()) {
this.loadStaffProfile();
} else if (Number(wx.getStorageSync('shangjiastatus')) === 1) {
this.loadAvatar();
this.getShangjiaInfo();
} else if (staffFlag) {
this.bootStaffPortal(uid);
}
ensureRoleOnCenterPage(this, 'shangjia'); ensureRoleOnCenterPage(this, 'shangjia');
} else { return;
this.setData({ isShangjia: false });
} }
if (staffFlag) {
this.bootStaffPortal(uid);
return;
}
this.setData({ isShangjia: false, staffBooting: false, isLoading: false });
syncStaffUi(this);
} catch (error) { } catch (error) {
console.error('读取商家状态失败', error); console.error('读取商家状态失败', error);
this.setData({ isShangjia: false }); this.setData({ isShangjia: false, staffBooting: false, isLoading: false });
syncStaffUi(this);
} }
}, },
// 获取商家信息 /** 已绑定客服、尚未拉到 context 时恢复身份(不展示商家邀请码) */
async bootStaffPortal(uid) {
this.setData({ staffBooting: true, isLoading: true, uid: uid || '' });
syncStaffUi(this);
await restoreStaffContextAfterAuth();
if (isMerchantPortalUser()) {
this.setData({ isShangjia: true, staffBooting: false, isLoading: false });
syncStaffUi(this);
if (isStaffMode()) {
await this.loadStaffProfile();
}
ensureRoleOnCenterPage(this, 'shangjia');
return;
}
this.setData({ staffBooting: false, isLoading: false, isShangjia: false });
syncStaffUi(this);
wx.showToast({ title: '客服身份已失效,请联系商家老板', icon: 'none' });
},
onInviteCodeInput(e) {
this.setData({ inviteCode: (e.detail.value || '').trim() });
},
goToStaffJoin() {
wx.navigateTo({ url: '/pages/staff-join/staff-join' });
},
clearCache() {
wx.showModal({
title: '清除缓存',
content: '将清除所有登录与身份数据,并回到点单端,确定继续?',
confirmText: '确定',
cancelText: '取消',
success: (r) => {
if (!r.confirm) return;
clearStaffSession();
clearCacheAndEnterNormal(this);
},
});
},
async loadStaffProfile() {
const ctx = await refreshStaffContext(request);
const profile = wx.getStorageSync('nicheng') || (ctx && ctx.display_name) || '商家客服';
this.loadAvatar();
this.setData({
nicheng: (ctx && ctx.display_name) || profile,
sjyue: (ctx && ctx.wallet && ctx.wallet.quota_available) || '0.00',
chenghaoList: [],
isLoading: false,
});
syncStaffUi(this);
applyStaffStatsToPage(this, ctx);
},
// 获取商家信息(仅商家老板;子客服禁止调用)
async getShangjiaInfo() { async getShangjiaInfo() {
if (isStaffMode() || Number(wx.getStorageSync('staffstatus')) === 1) {
return this.loadStaffProfile();
}
await this.loadData({ await this.loadData({
url: '/yonghu/shangjiaxinxi', url: '/yonghu/shangjiaxinxi',
method: 'POST', method: 'POST',
@@ -192,13 +303,17 @@ Page(createPage({
}, },
refreshShangjiaInfo() { refreshShangjiaInfo() {
if (isStaffMode()) {
this.throttledRefresh(() => this.loadStaffProfile());
return;
}
this.throttledRefresh(() => this.getShangjiaInfo()); this.throttledRefresh(() => this.getShangjiaInfo());
}, },
async loadPendingCount() { async loadPendingCount() {
try { try {
const res = await request({ const res = await request({
url: '/dingdan/sjdingdanhq', url: isStaffMode() ? STAFF_API.orderList : '/dingdan/sjdingdanhq',
method: 'POST', method: 'POST',
data: { zhuangtai_list: [8], page: 1, page_size: 1 }, data: { zhuangtai_list: [8], page: 1, page_size: 1 },
}); });
@@ -273,8 +388,16 @@ Page(createPage({
goToHome() { wx.redirectTo({ url: '/pages/merchant-home/merchant-home' }); }, goToHome() { wx.redirectTo({ url: '/pages/merchant-home/merchant-home' }); },
goToKefuKey() { wx.navigateTo({ url: '/pages/merchant-kefu-key/merchant-kefu-key' }); }, goToKefuKey() { wx.navigateTo({ url: '/pages/merchant-kefu-key/merchant-kefu-key' }); },
goToKefuList() { wx.navigateTo({ url: '/pages/merchant-kefu-list/merchant-kefu-list' }); }, goToKefuList() { wx.navigateTo({ url: '/pages/merchant-kefu-list/merchant-kefu-list' }); },
goToRecharge() { wx.navigateTo({ url: '/pages/merchant-recharge/merchant-recharge' }); }, goToAuditLog() { wx.navigateTo({ url: '/pages/merchant-staff-audit/merchant-staff-audit' }); },
goToWithdraw() { wx.navigateTo({ url: '/pages/withdraw/withdraw' }); }, goToStaffRank() { wx.navigateTo({ url: '/pages/merchant-staff-audit/merchant-staff-audit?tab=rank' }); },
goToRecharge() {
if (!guardMerchantOwnerAction('子客服无法充值,请联系商家老板')) return;
wx.navigateTo({ url: '/pages/merchant-recharge/merchant-recharge' });
},
goToWithdraw() {
if (!guardMerchantOwnerAction('子客服无法提现,请联系商家老板')) return;
wx.navigateTo({ url: '/pages/withdraw/withdraw' });
},
goToRanking() { wx.navigateTo({ url: '/pages/fighter-rank/fighter-rank?type=shangjia' }); }, goToRanking() { wx.navigateTo({ url: '/pages/fighter-rank/fighter-rank?type=shangjia' }); },
goToMessages() { wx.navigateTo({ url: '/pages/merchant-msg/merchant-msg' }); }, goToMessages() { wx.navigateTo({ url: '/pages/merchant-msg/merchant-msg' }); },
}, { }, {

View File

@@ -1,5 +1,21 @@
<!-- 商家端个人中心 - 逍遥梦布局,逻辑/接口不变 --> <!-- 商家端个人中心 - 逍遥梦布局,逻辑/接口不变 -->
<block wx:if="{{!isShangjia}}"> <block wx:if="{{staffBooting}}">
<view class="sj-page user-page">
<view class="sj-page-header">
<view class="status-bar" style="height:{{statusBar}}px"></view>
<view class="nav-bar flexm" style="height:{{navBar - statusBar}}px"><text class="sj-nav-title">我的</text></view>
</view>
<view class="unreg-area sj-mx20">
<view class="sj-panel unreg-card">
<text class="unreg-title">客服身份加载中</text>
<text class="unreg-desc">请稍候,正在同步您的派单员权限…</text>
</view>
</view>
<tab-bar/>
</view>
</block>
<block wx:elif="{{!isShangjia && !isStaffMode}}">
<view class="sj-page user-page"> <view class="sj-page user-page">
<view class="sj-page-header"> <view class="sj-page-header">
<view class="status-bar" style="height:{{statusBar}}px"></view> <view class="status-bar" style="height:{{statusBar}}px"></view>
@@ -8,8 +24,10 @@
<view class="unreg-area sj-mx20"> <view class="unreg-area sj-mx20">
<view class="sj-panel unreg-card"> <view class="sj-panel unreg-card">
<text class="unreg-title">商家入驻</text> <text class="unreg-title">商家入驻</text>
<input class="unreg-input" placeholder="请输入邀请码" maxlength="100" value="{{inviteCode}}" bindinput="onInviteCodeInput"/> <text class="unreg-desc">商家老板请填写商家邀请码;若您是客服/派单员,请走下方入口</text>
<view class="sj-btn-bg unreg-btn flexm" bindtap="onRegister">立即注册</view> <input class="unreg-input" placeholder="商家邀请码(老板专用)" maxlength="100" value="{{inviteCode}}" bindinput="onInviteCodeInput"/>
<view class="sj-btn-bg unreg-btn flexm" bindtap="onRegister">商家注册</view>
<view class="unreg-link staff-link" bindtap="goToStaffJoin">我是客服 / 派单员,绑定客服秘钥</view>
<view class="unreg-link" bindtap="switchToNormal">返回点单端</view> <view class="unreg-link" bindtap="switchToNormal">返回点单端</view>
<view class="unreg-link" bindtap="clearCache">清除缓存,重新登录</view> <view class="unreg-link" bindtap="clearCache">清除缓存,重新登录</view>
</view> </view>
@@ -36,7 +54,7 @@
<view> <view>
<view class="nickname-row flex"> <view class="nickname-row flex">
<text class="nickname">{{nicheng || '商家'}}</text> <text class="nickname">{{nicheng || '商家'}}</text>
<view class="sj-identity-tag">商家</view> <view class="sj-identity-tag">{{isStaffMode ? (staffRoleName || '商家客服') : '商家'}}</view>
</view> </view>
<view class="sn-row flex" bindtap="copyUid"> <view class="sn-row flex" bindtap="copyUid">
<text class="sn">ID: {{uid || '--'}}</text> <text class="sn">ID: {{uid || '--'}}</text>
@@ -56,11 +74,12 @@
<view class="sj-wallet-card flexb"> <view class="sj-wallet-card flexb">
<view> <view>
<view class="sj-w-label">商家余额(元)</view> <view class="sj-w-label">{{isStaffMode ? '可用派单额度(元)' : '商家余额(元)'}}</view>
<view class="sj-w-num">¥{{sjyue}}</view> <view class="sj-w-num">¥{{sjyue}}</view>
<view class="sj-w-sub">发单总量 {{fabu}} · 退款 {{tuikuan}}</view> <view class="sj-w-sub" wx:if="{{!isStaffMode}}">发单总量 {{fabu}} · 退款 {{tuikuan}}</view>
<view class="sj-w-sub" wx:else>角色:{{staffRoleName}}</view>
</view> </view>
<view class="wallet-btns flex"> <view class="wallet-btns flex" wx:if="{{showRechargeWithdraw}}">
<view class="sj-btn-outline" catchtap="goToRecharge">充值</view> <view class="sj-btn-outline" catchtap="goToRecharge">充值</view>
<view class="sj-btn-outline" catchtap="goToWithdraw">去提现</view> <view class="sj-btn-outline" catchtap="goToWithdraw">去提现</view>
</view> </view>
@@ -106,27 +125,35 @@
<view class="sj-panel sj-mx20"> <view class="sj-panel sj-mx20">
<view class="sj-section-hd">交易与发单</view> <view class="sj-section-hd">交易与发单</view>
<view class="sj-func-grid"> <view class="sj-func-grid">
<view class="sj-func-item" bindtap="goToPaiDan"> <view class="sj-func-item" bindtap="goToPaiDan" wx:if="{{canDispatch}}">
<image class="sj-func-icon" src="{{imgUrls.iconRelease}}" mode="aspectFit"/> <image class="sj-func-icon" src="{{imgUrls.iconRelease}}" mode="aspectFit"/>
<text class="sj-func-txt">自定义单</text> <text class="sj-func-txt">自定义单</text>
</view> </view>
<view class="sj-func-item" bindtap="goToJiSuPaiDan"> <view class="sj-func-item" bindtap="goToJiSuPaiDan" wx:if="{{canDispatch}}">
<image class="sj-func-icon" src="{{imgUrls.iconFast}}" mode="aspectFit"/> <image class="sj-func-icon" src="{{imgUrls.iconFast}}" mode="aspectFit"/>
<text class="sj-func-txt">常规单</text> <text class="sj-func-txt">常规单</text>
</view> </view>
<view class="sj-func-item" bindtap="goToKefuKey"> <view class="sj-func-item" bindtap="goToKefuKey" wx:if="{{canStaffManage}}">
<image class="sj-func-icon" src="{{imgUrls.iconService}}" mode="aspectFit"/> <image class="sj-func-icon" src="{{imgUrls.iconService}}" mode="aspectFit"/>
<text class="sj-func-txt">客服邀请码</text> <text class="sj-func-txt">客服邀请码</text>
</view> </view>
<view class="sj-func-item" bindtap="goToKefuList"> <view class="sj-func-item" bindtap="goToKefuList" wx:if="{{canStaffManage}}">
<image class="sj-func-icon" src="{{imgUrls.iconService}}" mode="aspectFit"/> <image class="sj-func-icon" src="{{imgUrls.iconService}}" mode="aspectFit"/>
<text class="sj-func-txt">客服列表</text> <text class="sj-func-txt">客服列表</text>
</view> </view>
<view class="sj-func-item" bindtap="goToMessages"> <view class="sj-func-item" bindtap="goToAuditLog" wx:if="{{canAudit}}">
<image class="sj-func-icon" src="{{imgUrls.iconOrders}}" mode="aspectFit"/>
<text class="sj-func-txt">操作日志</text>
</view>
<view class="sj-func-item" bindtap="goToStaffRank" wx:if="{{canAudit}}">
<image class="sj-func-icon" src="{{imgUrls.iconRank}}" mode="aspectFit"/>
<text class="sj-func-txt">客服排行</text>
</view>
<view class="sj-func-item" bindtap="goToMessages" wx:if="{{!isStaffMode}}">
<image class="sj-func-icon" src="{{imgUrls.iconPunish}}" mode="aspectFit"/> <image class="sj-func-icon" src="{{imgUrls.iconPunish}}" mode="aspectFit"/>
<text class="sj-func-txt">处罚记录</text> <text class="sj-func-txt">处罚记录</text>
</view> </view>
<view class="sj-func-item" bindtap="goToRanking"> <view class="sj-func-item" bindtap="goToRanking" wx:if="{{!isStaffMode}}">
<image class="sj-func-icon" src="{{imgUrls.iconRank}}" mode="aspectFit"/> <image class="sj-func-icon" src="{{imgUrls.iconRank}}" mode="aspectFit"/>
<text class="sj-func-txt">商家排行</text> <text class="sj-func-txt">商家排行</text>
</view> </view>

View File

@@ -149,6 +149,20 @@ page {
margin-bottom: 24rpx; margin-bottom: 24rpx;
} }
.unreg-desc {
display: block;
font-size: 24rpx;
color: #888;
text-align: center;
line-height: 1.5;
margin-bottom: 24rpx;
}
.unreg-link.staff-link {
color: #1976d2;
font-weight: 600;
}
.unreg-input { .unreg-input {
height: 80rpx; height: 80rpx;
border-radius: 12rpx; border-radius: 12rpx;

View File

@@ -4,11 +4,14 @@ import { ensureLogin } from '../../utils/login';
import { import {
backfillUserProfileCache, backfillUserProfileCache,
getUserProfileForDisplay, getUserProfileForDisplay,
getSessionToken,
isRoleStatusActive, isRoleStatusActive,
ensureRoleOnCenterPage, ensureRoleOnCenterPage,
} from '../../utils/role-tab-bar'; } from '../../utils/role-tab-bar';
import { enterLockedRole } from '../../utils/primary-role.js'; import { enterLockedRole } from '../../utils/primary-role.js';
import { isStaffMode, getStaffContext, restoreStaffContextAfterAuth } from '../../utils/staff-api.js';
import { createPage } from '../../utils/base-page.js'; import { createPage } from '../../utils/base-page.js';
import { ensurePhoneAuth } from '../../utils/phone-auth.js';
Page(createPage({ Page(createPage({
data: { data: {
@@ -23,6 +26,8 @@ Page(createPage({
zuzhangIcon: '', guanzhualongIcon: '', arrowRightIcon: '', qingchuIcon: '', zuzhangIcon: '', guanzhualongIcon: '', arrowRightIcon: '', qingchuIcon: '',
dashouCertified: false, dashouCertified: false,
shangjiaCertified: false, shangjiaCertified: false,
staffCertified: false,
staffTag: '入驻',
bottomSafePadding: 0 bottomSafePadding: 0
}, },
@@ -32,13 +37,26 @@ Page(createPage({
this.setBottomSafe() this.setBottomSafe()
}, },
onShow() { async onShow() {
const phoneOk = await ensurePhoneAuth({
redirect: true,
returnPage: '/pages/mine/mine',
});
if (!phoneOk) return;
this.initIconUrls(); this.initIconUrls();
this.registerNotificationComponent(); this.registerNotificationComponent();
this.checkAvatarPrompt(); this.checkAvatarPrompt();
backfillUserProfileCache(app); backfillUserProfileCache(app);
this.loadUserData(); if (getSessionToken(app)) {
this.updateOrderCounts(); restoreStaffContextAfterAuth().finally(() => {
this.loadUserData();
this.updateOrderCounts();
});
} else {
this.loadUserData();
this.updateOrderCounts();
}
}, },
setBottomSafe() { setBottomSafe() {
@@ -84,6 +102,8 @@ Page(createPage({
loadUserData() { loadUserData() {
const profile = getUserProfileForDisplay(app); const profile = getUserProfileForDisplay(app);
const ctx = getStaffContext();
const staffActive = isStaffMode();
this.setData({ this.setData({
avatarUrl: profile.avatarUrl, avatarUrl: profile.avatarUrl,
userNicheng: profile.nick, userNicheng: profile.nick,
@@ -91,6 +111,8 @@ Page(createPage({
showLoginBtn: !profile.isLoggedIn, showLoginBtn: !profile.isLoggedIn,
dashouCertified: isRoleStatusActive(wx.getStorageSync('dashoustatus')), dashouCertified: isRoleStatusActive(wx.getStorageSync('dashoustatus')),
shangjiaCertified: isRoleStatusActive(wx.getStorageSync('shangjiastatus')), shangjiaCertified: isRoleStatusActive(wx.getStorageSync('shangjiastatus')),
staffCertified: staffActive,
staffTag: staffActive ? (ctx.role_name || '已入驻') : '入驻',
}); });
}, },
@@ -107,6 +129,7 @@ Page(createPage({
handleWechatLogin() { handleWechatLogin() {
wx.showLoading({ title: '登录中...', mask: true }); wx.showLoading({ title: '登录中...', mask: true });
ensureLogin({ silent: false, page: this }) ensureLogin({ silent: false, page: this })
.then(() => restoreStaffContextAfterAuth())
.then(() => { .then(() => {
this.loadUserData(); this.loadUserData();
this.updateOrderCounts(); this.updateOrderCounts();
@@ -122,8 +145,13 @@ Page(createPage({
const type = e.currentTarget.dataset.type; const type = e.currentTarget.dataset.type;
if (!type || !['dashou', 'shangjia'].includes(type)) return; if (!type || !['dashou', 'shangjia'].includes(type)) return;
ensureLogin({ silent: false, page: this }) ensureLogin({ silent: false, page: this })
.then(() => restoreStaffContextAfterAuth())
.then(() => { .then(() => {
this.loadUserData(); this.loadUserData();
if (type === 'shangjia' && isStaffMode()) {
enterLockedRole('shangjia', app);
return;
}
const statusKey = type === 'dashou' ? 'dashoustatus' : 'shangjiastatus'; const statusKey = type === 'dashou' ? 'dashoustatus' : 'shangjiastatus';
if (isRoleStatusActive(wx.getStorageSync(statusKey))) { if (isRoleStatusActive(wx.getStorageSync(statusKey))) {
ensureRoleOnCenterPage(this, type); ensureRoleOnCenterPage(this, type);
@@ -136,5 +164,20 @@ Page(createPage({
}, },
goToCustomService() { this.goToKefu() }, goToCustomService() { this.goToKefu() },
goToStaffJoin() {
ensureLogin({ silent: false, page: this })
.then(() => restoreStaffContextAfterAuth())
.then(() => {
this.loadUserData();
if (isStaffMode()) {
enterLockedRole('shangjia', app);
return;
}
wx.navigateTo({ url: '/pages/staff-join/staff-join' });
})
.catch(() => {});
},
goToGuanzhuA() { wx.navigateTo({ url: '/pages/manager-assign/manager-assign' }) }, goToGuanzhuA() { wx.navigateTo({ url: '/pages/manager-assign/manager-assign' }) },
})) }))

View File

@@ -88,6 +88,13 @@
<text class="service-name">商家</text> <text class="service-name">商家</text>
<text class="auth-tag {{shangjiaCertified ? 'auth-tag-done' : ''}}">{{shangjiaCertified ? '已认证' : '去认证'}}</text> <text class="auth-tag {{shangjiaCertified ? 'auth-tag-done' : ''}}">{{shangjiaCertified ? '已认证' : '去认证'}}</text>
</view> </view>
<view class="service-unit" bindtap="goToStaffJoin">
<view class="icon-bg icon-bg-sm">
<image src="{{kefuIcon}}" mode="aspectFit" class="icon-dark icon-size-sm" />
</view>
<text class="service-name">商家客服</text>
<text class="auth-tag {{staffCertified ? 'auth-tag-done' : ''}}">{{staffTag}}</text>
</view>
</view> </view>
</view> </view>

View File

@@ -1,4 +1,4 @@
import { submit } from '../../utils/phone-auth'; import { submit, getPhoneAuthReturnPage } from '../../utils/phone-auth';
Page({ Page({
data: { data: {
@@ -27,8 +27,7 @@ Page({
wx.showLoading({ title: '认证中...', mask: true }); wx.showLoading({ title: '认证中...', mask: true });
await submit(phoneCode, avatarFile); await submit(phoneCode, avatarFile);
wx.hideLoading(); wx.hideLoading();
// 成功重启小程序此时check返回false用户无感进入 wx.reLaunch({ url: getPhoneAuthReturnPage() });
wx.reLaunch({ url: '/pages/index/index' });
} catch (e) { } catch (e) {
wx.hideLoading(); wx.hideLoading();
wx.showToast({ title: e.message, icon: 'none' }); wx.showToast({ title: e.message, icon: 'none' });
@@ -37,4 +36,4 @@ Page({
exit() { exit() {
wx.exitMiniProgram(); wx.exitMiniProgram();
} }
}); });

View File

@@ -0,0 +1,55 @@
import request from '../../utils/request.js';
import { STAFF_API, applyStaffFromLoginData } from '../../utils/staff-api.js';
import { enterLockedRole, lockPrimaryRole } from '../../utils/primary-role.js';
const app = getApp();
Page({
data: {
inviteCode: '',
loading: false,
},
onLoad(options) {
const code = options.code || options.inviteCode || '';
if (code) this.setData({ inviteCode: decodeURIComponent(code) });
},
onInput(e) {
this.setData({ inviteCode: (e.detail.value || '').trim() });
},
async onBind() {
const inviteCode = this.data.inviteCode;
if (!inviteCode) {
wx.showToast({ title: '请输入邀请码', icon: 'none' });
return;
}
this.setData({ loading: true });
try {
const res = await request({
url: STAFF_API.bind,
method: 'POST',
data: { inviteCode },
});
const d = res.data || {};
if (d.code === 0 && d.data && d.data.staff_context) {
applyStaffFromLoginData({
staffstatus: 1,
staff_context: d.data.staff_context,
});
lockPrimaryRole('shangjia', app);
wx.showToast({ title: '绑定成功', icon: 'success' });
setTimeout(() => {
enterLockedRole('shangjia', app);
}, 800);
} else {
wx.showToast({ title: d.msg || '绑定失败', icon: 'none' });
}
} catch (e) {
wx.showToast({ title: '网络错误', icon: 'none' });
} finally {
this.setData({ loading: false });
}
},
});

View File

@@ -0,0 +1,4 @@
{
"navigationBarTitleText": "客服入驻",
"usingComponents": {}
}

View File

@@ -0,0 +1,13 @@
<view class="page">
<view class="card">
<text class="title">成为商家客服</text>
<text class="desc">输入商家提供的一次性邀请码,绑定后可代商家派单(受权限与额度限制)</text>
<input class="input" placeholder="例如 PD-ABC123" value="{{inviteCode}}" bindinput="onInput" />
<button class="btn" loading="{{loading}}" bindtap="onBind">确认绑定</button>
</view>
<view class="tips">
<text>· 已是商家账号无法绑定客服</text>
<text>· 同时只能绑定一个商家</text>
<text>· 被移除后可重新绑定或认证商家</text>
</view>
</view>

View File

@@ -0,0 +1,8 @@
.page { min-height: 100vh; background: #f5f5f5; padding: 40rpx; box-sizing: border-box; }
.card { background: #fff; border-radius: 16rpx; padding: 40rpx; }
.title { display: block; font-size: 36rpx; font-weight: 600; color: #333; margin-bottom: 16rpx; }
.desc { display: block; font-size: 26rpx; color: #666; line-height: 1.6; margin-bottom: 32rpx; }
.input { border: 1px solid #ddd; border-radius: 12rpx; padding: 24rpx; font-size: 30rpx; margin-bottom: 32rpx; }
.btn { background: #C9A962; color: #fff; border-radius: 12rpx; }
.tips { margin-top: 32rpx; font-size: 24rpx; color: #999; line-height: 2; }
.tips text { display: block; }

View File

@@ -1,6 +1,7 @@
// pages/verify/verify.js // pages/verify/verify.js
import { createPage, request } from '../../utils/base-page.js' import { createPage, request } from '../../utils/base-page.js'
import { enterLockedRole } from '../../utils/primary-role.js' import { enterLockedRole } from '../../utils/primary-role.js'
import { isStaffMode, restoreStaffContextAfterAuth } from '../../utils/staff-api.js'
const API_MAP = { const API_MAP = {
dashou: '/yonghu/dashouzhuce', dashou: '/yonghu/dashouzhuce',
@@ -46,8 +47,17 @@ Page(createPage({
this.checkStatus() this.checkStatus()
}, },
onShow() { async onShow() {
if (this.data.authType) this.checkStatus() if (this.data.authType) {
if (this.data.authType === 'shangjia' && wx.getStorageSync('token')) {
await restoreStaffContextAfterAuth();
if (isStaffMode()) {
enterLockedRole('shangjia', getApp());
return;
}
}
this.checkStatus();
}
}, },
checkStatus() { checkStatus() {

View File

@@ -12,6 +12,7 @@ import {
refreshTabBar, refreshTabBar,
} from './role-tab-bar.js'; } from './role-tab-bar.js';
import { lockPrimaryRole, migrateLegacyCenterRole, enterLockedRole } from './primary-role.js'; import { lockPrimaryRole, migrateLegacyCenterRole, enterLockedRole } from './primary-role.js';
import { isMerchantPortalUser } from './staff-api.js';
import { resolveAvatarFromStorage } from './avatar.js'; import { resolveAvatarFromStorage } from './avatar.js';
import { isApiSuccess, getApiMsg, extractUserData, parseSceneOptions } from './api-helper.js'; import { isApiSuccess, getApiMsg, extractUserData, parseSceneOptions } from './api-helper.js';
@@ -296,7 +297,10 @@ function createRoleCenterMixin(roleConfig) {
try { try {
const status = wx.getStorageSync(statusKey); const status = wx.getStorageSync(statusKey);
const uid = wx.getStorageSync('uid'); const uid = wx.getStorageSync('uid');
if (isRoleStatusActive(status)) { const active = role === 'shangjia'
? isMerchantPortalUser()
: isRoleStatusActive(status);
if (active) {
this.setData({ [dataFlag]: true, uid: uid || '' }); this.setData({ [dataFlag]: true, uid: uid || '' });
this.loadAvatar(); this.loadAvatar();
if (this.checkGlobalData) this.checkGlobalData(); if (this.checkGlobalData) this.checkGlobalData();

View File

@@ -5,6 +5,7 @@ import {
getSessionToken, getSessionToken,
isWechatLoginSuccess, isWechatLoginSuccess,
} from './role-tab-bar'; } from './role-tab-bar';
import { restoreStaffContextAfterAuth } from './staff-api.js';
const app = getApp(); const app = getApp();
@@ -14,6 +15,7 @@ export function ensureLogin(options = {}) {
const token = getSessionToken(app); const token = getSessionToken(app);
if (token) { if (token) {
backfillUserProfileCache(app); backfillUserProfileCache(app);
restoreStaffContextAfterAuth();
return Promise.resolve(true); return Promise.resolve(true);
} }

View File

@@ -1,57 +1,31 @@
/** /**
* 手机号强制认证 - 完全自包含逻辑模块 * 手机号强制认证:每次调用 check / ensurePhoneAuth 均请求后端 /peizhi/yhbdsjh
*
* 特点:
* 1. 不引用项目中的 request.js / upload.js避免其顶层 getApp() 带来的加载顺序错误
* 2. 所有请求直接使用 wx.request / wx.uploadFile基础 URL 写死(与 app.globalData.apiBaseUrl 保持一致)
* 3. check() 内部捕获所有异常,永远返回 false 或 true不会抛出错误
* 4. submit() 抛出错误由认证页面自己处理,不影响主流程
*
* 用法:
* app.js 中:
* try {
* const { check } = require('./utils/phone-auth');
* if (await check()) { wx.reLaunch(...); return; }
* } catch(e) {}
*
* 认证页面中:
* const { submit } = require('../../utils/phone-auth');
* await submit(phoneCode, avatarPath);
*/ */
// ==================== 基础配置 ==================== const API_BASE = 'https://www.abas.asia/hqhd';
const API_BASE = 'https://www.abas.asia/hqhd'; // 与你 app.js 里的 apiBaseUrl 一致 const AUTH_CHECK_URL = '/peizhi/yhbdsjh';
const AUTH_CHECK_URL = '/peizhi/yhbdsjh'; // 检查是否需要认证 const AUTH_SUBMIT_URL = '/yonghu/xiugai';
const AUTH_SUBMIT_URL = '/yonghu/xiugai'; // 提交认证(复用修改信息接口) const AUTH_PAGE = '/pages/phone-auth/phone-auth';
const RETURN_ROLE_KEY = 'phone_auth_return_role';
const RETURN_PAGE_KEY = 'phone_auth_return_page';
// ==================== 内部封装wx.request ====================
function innerRequest(url, data = {}) { function innerRequest(url, data = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const token = wx.getStorageSync('token'); // 运行时获取 token不提前取 const token = wx.getStorageSync('token');
const header = { const header = { 'Content-Type': 'application/json' };
'Content-Type': 'application/json' if (token) header['Authorization'] = 'Bearer ' + token;
};
if (token) {
header['Authorization'] = 'Bearer ' + token;
}
wx.request({ wx.request({
url: API_BASE + url, url: API_BASE + url,
method: 'POST', method: 'POST',
data, data,
header, header,
success: (res) => { success: (res) => resolve(res),
// 正常响应直接 resolve 整个响应对象,让上层自己解析 fail: (err) => reject(err),
resolve(res);
},
fail: (err) => {
reject(err);
}
}); });
}); });
} }
// ==================== 内部封装wx.uploadFile ====================
function innerUpload(url, filePath, formData = {}, fileName = 'avatar') { function innerUpload(url, filePath, formData = {}, fileName = 'avatar') {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const token = wx.getStorageSync('token'); const token = wx.getStorageSync('token');
@@ -60,7 +34,6 @@ function innerUpload(url, filePath, formData = {}, fileName = 'avatar') {
return; return;
} }
// 先验证文件是否存在
wx.getFileInfo({ wx.getFileInfo({
filePath, filePath,
success: () => { success: () => {
@@ -69,15 +42,11 @@ function innerUpload(url, filePath, formData = {}, fileName = 'avatar') {
filePath, filePath,
name: fileName, name: fileName,
formData, formData,
header: { header: { Authorization: 'Bearer ' + token },
'Authorization': 'Bearer ' + token
},
success: (res) => { success: (res) => {
// ✅ 修复wx.uploadFile 成功时 statusCode 为 200
if (res.statusCode === 200) { if (res.statusCode === 200) {
try { try {
const data = JSON.parse(res.data); resolve(JSON.parse(res.data));
resolve(data); // 直接返回解析后的对象,方便下游使用
} catch (e) { } catch (e) {
reject(new Error('解析响应数据失败')); reject(new Error('解析响应数据失败'));
} }
@@ -88,78 +57,142 @@ function innerUpload(url, filePath, formData = {}, fileName = 'avatar') {
reject(new Error(`上传失败,状态码:${res.statusCode}`)); reject(new Error(`上传失败,状态码:${res.statusCode}`));
} }
}, },
fail: (err) => { fail: () => reject(new Error('网络请求失败')),
reject(new Error('网络请求失败'));
}
}); });
}, },
fail: () => { fail: () => reject(new Error('文件不存在或无法访问')),
reject(new Error('文件不存在或无法访问'));
}
}); });
}); });
} }
// ==================== 公开方法 ==================== function isOnPhoneAuthPage() {
const pages = getCurrentPages();
const cur = pages[pages.length - 1];
return !!(cur && cur.route === 'pages/phone-auth/phone-auth');
}
function parseCheckResponse(res) {
if (!res || res.statusCode === 401) {
return { ok: false, needAuth: false };
}
if (res.statusCode !== 200) {
return { ok: false, needAuth: false };
}
const body = res.data;
if (!body || typeof body !== 'object') {
return { ok: false, needAuth: false };
}
const code = Number(body.code);
if (code !== 0 && code !== 200) {
return { ok: false, needAuth: false };
}
const needAuth = body.need_auth === true
|| (body.data && body.data.need_auth === true);
return { ok: true, needAuth: !!needAuth };
}
/** 根据当前主端/身份推断认证完成后回跳角色 */
function resolveReturnRole() {
const primary = wx.getStorageSync('primaryRole') || wx.getStorageSync('currentRole') || '';
if (primary === 'dashou' || primary === 'shangjia') return primary;
if (Number(wx.getStorageSync('shangjiastatus')) === 1) return 'shangjia';
if (Number(wx.getStorageSync('staffstatus')) === 1) return 'shangjia';
if (
Number(wx.getStorageSync('dashoustatus')) === 1
|| Number(wx.getStorageSync('guanshistatus')) === 1
|| Number(wx.getStorageSync('zuzhangstatus')) === 1
|| Number(wx.getStorageSync('kaoheguanstatus')) === 1
) {
return 'dashou';
}
return 'mine';
}
function defaultReturnPage(role) {
if (role === 'dashou') return '/pages/accept-order/accept-order';
if (role === 'shangjia') return '/pages/merchant-home/merchant-home';
return '/pages/mine/mine';
}
/**
* 检查是否需要手机号认证
* - 无 token 或 任何异常 → 返回 false
* - 后端明确 need_auth: true → 返回 true
*/
async function check() { async function check() {
const token = wx.getStorageSync('token'); const token = wx.getStorageSync('token');
if (!token) return false; if (!token) return false;
try { try {
const res = await innerRequest(AUTH_CHECK_URL, {}); const res = await innerRequest(AUTH_CHECK_URL, {});
// 假设后端返回格式:{ data: { need_auth: true/false } } 或直接在顶层 const parsed = parseCheckResponse(res);
const body = res.data; if (!parsed.ok) return false;
return body && body.need_auth === true; return parsed.needAuth;
} catch (e) { } catch (e) {
// 任何错误(网络、后端异常)都吞掉,不影响主流程
return false; return false;
} }
} }
function redirectToPhoneAuth(returnRole, returnPage) {
const role = returnRole || resolveReturnRole();
wx.setStorageSync(RETURN_ROLE_KEY, role);
if (returnPage) {
wx.setStorageSync(RETURN_PAGE_KEY, returnPage);
} else {
wx.removeStorageSync(RETURN_PAGE_KEY);
}
if (!isOnPhoneAuthPage()) {
wx.reLaunch({ url: AUTH_PAGE });
}
}
/** /**
* 提交认证(手机号 code + 头像文件) * 向后端确认是否需要手机号认证;需要则跳转认证页
* @param {string} phoneCode 微信 getPhoneNumber 返回的 code * @returns {Promise<boolean>} true=无需认证或已满足false=已跳转认证页
* @param {string} avatarPath 头像临时路径
* @returns {Promise<object>} 后端返回的完整数据
*/ */
async function ensurePhoneAuth(options = {}) {
const { redirect = true, role, returnPage } = options;
const token = wx.getStorageSync('token');
if (!token) return true;
const needAuth = await check();
if (!needAuth) return true;
if (redirect) {
redirectToPhoneAuth(role || resolveReturnRole(), returnPage);
}
return false;
}
function getPhoneAuthReturnPage() {
const savedPage = wx.getStorageSync(RETURN_PAGE_KEY);
wx.removeStorageSync(RETURN_PAGE_KEY);
if (savedPage) {
wx.removeStorageSync(RETURN_ROLE_KEY);
return savedPage;
}
const role = wx.getStorageSync(RETURN_ROLE_KEY) || resolveReturnRole();
wx.removeStorageSync(RETURN_ROLE_KEY);
return defaultReturnPage(role);
}
async function submit(phoneCode, avatarPath) { async function submit(phoneCode, avatarPath) {
if (!phoneCode || !avatarPath) { if (!phoneCode || !avatarPath) {
throw new Error('缺少参数'); throw new Error('缺少参数');
} }
// 将手机号 code 放入 formData const data = await innerUpload(AUTH_SUBMIT_URL, avatarPath, {
const formData = { shoujihao_code: phoneCode,
shoujihao_code: phoneCode });
};
// 调用上传
const data = await innerUpload(AUTH_SUBMIT_URL, avatarPath, formData);
// ✅ 修复:正确判断后端返回的 code 字段,后端成功时 code === 0
if (data && data.code === 0) { if (data && data.code === 0) {
// data.data 包含 { touxiang: '相对路径', phone: '...' }
const respData = data.data || data; const respData = data.data || data;
if (respData.phone) wx.setStorageSync('phone', respData.phone);
// 存储手机号 if (respData.touxiang) wx.setStorageSync('touxiang', respData.touxiang);
if (respData.phone) {
wx.setStorageSync('phone', respData.phone);
}
// ✅ 修复:存储头像相对路径,与 xiugai 页面处理方式一致
if (respData.touxiang) {
wx.setStorageSync('touxiang', respData.touxiang);
}
return respData; return respData;
} else {
throw new Error((data && data.msg) || '认证失败');
} }
throw new Error((data && data.msg) || '认证失败');
} }
module.exports = { check, submit }; module.exports = {
check,
submit,
ensurePhoneAuth,
redirectToPhoneAuth,
getPhoneAuthReturnPage,
resolveReturnRole,
};

View File

@@ -1,5 +1,8 @@
/** 三端固定normal | dashou | shangjia认证后不可回退 */ /** 三端固定normal | dashou | shangjia认证后不可回退 */
import { isStaffMode } from './staff-api.js';
import { ensurePhoneAuth } from './phone-auth.js';
const VALID = ['normal', 'dashou', 'shangjia']; const VALID = ['normal', 'dashou', 'shangjia'];
const LEGACY_CENTER_ROLES = ['guanshi', 'zuzhang', 'kaoheguan']; const LEGACY_CENTER_ROLES = ['guanshi', 'zuzhang', 'kaoheguan'];
@@ -53,6 +56,9 @@ export function getPrimaryRole(app) {
if (isRoleStatusActive(wx.getStorageSync('shangjiastatus'))) { if (isRoleStatusActive(wx.getStorageSync('shangjiastatus'))) {
return 'shangjia'; return 'shangjia';
} }
if (isStaffMode()) {
return 'shangjia';
}
return 'normal'; return 'normal';
} }
@@ -87,8 +93,7 @@ export const PRIMARY_DEFAULT_PAGES = {
}; };
/** 锁定主端并跳转到该端默认首页,同时建立 IM 长连接 */ /** 锁定主端并跳转到该端默认首页,同时建立 IM 长连接 */
export function enterLockedRole(role, app) { function finishEnterLockedRole(role, app) {
if (!VALID.includes(role) || role === 'normal') return false;
app = app || getApp(); app = app || getApp();
lockPrimaryRole(role, app); lockPrimaryRole(role, app);
wx.reLaunch({ url: PRIMARY_DEFAULT_PAGES[role] }); wx.reLaunch({ url: PRIMARY_DEFAULT_PAGES[role] });
@@ -103,3 +108,14 @@ export function enterLockedRole(role, app) {
}, 600); }, 600);
return true; return true;
} }
export function enterLockedRole(role, app) {
if (!VALID.includes(role) || role === 'normal') return false;
if (role === 'dashou' || role === 'shangjia') {
ensurePhoneAuth({ redirect: true, role }).then((ok) => {
if (ok) finishEnterLockedRole(role, app);
});
return true;
}
return finishEnterLockedRole(role, app);
}

View File

@@ -1,74 +1,73 @@
// utils/request.js // utils/request.js
const app = getApp(); function request(options) {
const app = getApp();
function request(options) { const token = wx.getStorageSync('token');
const token = wx.getStorageSync('token');
// 🔥 没有 token 时不发送请求,直接返回一个“空响应”对象,不影响后续代码
// 🔥 没有 token 时不发送请求,直接返回一个“空响应”对象,不影响后续代码 if (!token) {
if (!token) { // 返回一个模拟的响应,状态码为 401但不会触发任何弹窗或跳转
// 返回一个模拟的响应,状态码为 401但不会触发任何弹窗或跳转 return Promise.resolve({
return Promise.resolve({ statusCode: 401,
statusCode: 401, data: { code: 401, msg: '未登录' },
data: { code: 401, msg: '未登录' }, errMsg: 'request:ok'
errMsg: 'request:ok' });
}); }
}
const header = {
const header = { 'Content-Type': 'application/json',
'Content-Type': 'application/json', ...options.header,
...options.header, };
}; if (token) {
if (token) { header['Authorization'] = 'Bearer ' + token;
header['Authorization'] = 'Bearer ' + token; }
}
return new Promise((resolve, reject) => {
return new Promise((resolve, reject) => { wx.request({
wx.request({ url: app.globalData.apiBaseUrl + options.url,
url: app.globalData.apiBaseUrl + options.url, method: options.method,
method: options.method, data: options.data || {},
data: options.data || {}, header,
header, success: async (res) => {
success: async (res) => { if (res.statusCode === 401 || res.statusCode === 403) {
if (res.statusCode === 401 || res.statusCode === 403) { // 注意:这里的弹窗逻辑只会在有 token 且 token 失效时触发
// 注意:这里的弹窗逻辑只会在有 token 且 token 失效时触发 // 无 token 的情况已经在上面直接返回,不会走到这里
// 无 token 的情况已经在上面直接返回,不会走到这里 if (app.globalData.loginPromise) {
if (app.globalData.loginPromise) { try {
try { await app.globalData.loginPromise;
await app.globalData.loginPromise; const retryRes = await request(options);
const retryRes = await request(options); resolve(retryRes);
resolve(retryRes); } catch (err) {
} catch (err) { wx.showModal({
wx.showModal({ content: '操作失败,请先登录',
content: '操作失败,请先登录', success: (modalRes) => {
success: (modalRes) => { if (modalRes.confirm) {
if (modalRes.confirm) { wx.removeStorageSync('token');
wx.removeStorageSync('token'); wx.switchTab({ url: '/pages/mine/mine' });
wx.switchTab({ url: '/pages/mine/mine' }); }
} },
}, });
}); resolve(res);
resolve(res); }
} return;
return; }
}
wx.showModal({
wx.showModal({ content: '操作失败,请先登录',
content: '操作失败,请先登录', success: (modalRes) => {
success: (modalRes) => { if (modalRes.confirm) {
if (modalRes.confirm) { wx.removeStorageSync('token');
wx.removeStorageSync('token'); wx.reLaunch({ url: '/pages/mine/mine' });
wx.reLaunch({ url: '/pages/mine/mine' }); }
} },
}, });
}); resolve(res);
resolve(res); return;
return; }
} resolve(res);
resolve(res); },
}, fail: reject,
fail: reject, });
}); });
}); }
}
export default request; export default request;

View File

@@ -1,255 +1,266 @@
import { clearPrimaryRole } from './primary-role'; import { clearPrimaryRole } from './primary-role';
import { getLocalImUserId } from './im-user.js'; import { getLocalImUserId } from './im-user.js';
export function isWechatLoginSuccess(body) { import { applyStaffFromLoginData, isStaffMode, restoreStaffContextAfterAuth, clearStaffSession } from './staff-api.js';
if (!body) return false; export function isWechatLoginSuccess(body) {
const code = Number(body.code); if (!body) return false;
return code === 0 || code === 200; const code = Number(body.code);
} return code === 0 || code === 200;
}
const SESSION_STATUS_KEYS = ['dashoustatus', 'guanshistatus', 'shangjiastatus', 'zuzhangstatus', 'kaoheguanstatus'];
const SESSION_STATUS_KEYS = ['dashoustatus', 'guanshistatus', 'shangjiastatus', 'zuzhangstatus', 'kaoheguanstatus'];
/** 读取 tokenStorage 优先globalData 兜底并回写 Storage */
export function getSessionToken(app) { /** 读取 tokenStorage 优先globalData 兜底并回写 Storage */
app = app || getApp(); export function getSessionToken(app) {
let token = wx.getStorageSync('token') || app.globalData.token || ''; app = app || getApp();
if (token && !wx.getStorageSync('token')) { let token = wx.getStorageSync('token') || app.globalData.token || '';
wx.setStorageSync('token', token); if (token && !wx.getStorageSync('token')) {
} wx.setStorageSync('token', token);
return token; }
} return token;
}
export function isUserLoggedIn(app) {
return !!getSessionToken(app); export function isUserLoggedIn(app) {
} return !!getSessionToken(app);
}
/** 从 Storage / globalData 汇总展示用用户信息(兼容其他端注册后 nick 未落盘) */
export function getUserProfileForDisplay(app) { /** 从 Storage / globalData 汇总展示用用户信息(兼容其他端注册后 nick 未落盘) */
app = app || getApp(); export function getUserProfileForDisplay(app) {
const token = getSessionToken(app); app = app || getApp();
const uid = wx.getStorageSync('uid') || app.globalData.uid || ''; const token = getSessionToken(app);
let nick = wx.getStorageSync('nicheng') || app.globalData.nicheng || ''; const uid = wx.getStorageSync('uid') || app.globalData.uid || '';
if (!nick && app.globalData.dashouNicheng) { let nick = wx.getStorageSync('nicheng') || app.globalData.nicheng || '';
nick = app.globalData.dashouNicheng; if (!nick && app.globalData.dashouNicheng) {
} nick = app.globalData.dashouNicheng;
const tx = wx.getStorageSync('touxiang') || ''; }
const def = app.globalData.morentouxiang || ''; const tx = wx.getStorageSync('touxiang') || '';
const oss = app.globalData.ossImageUrl || ''; const def = app.globalData.morentouxiang || '';
let avatarUrl = ''; const oss = app.globalData.ossImageUrl || '';
if (tx) { let avatarUrl = '';
avatarUrl = tx.startsWith('http') ? tx : oss + tx; if (tx) {
} else if (def) { avatarUrl = tx.startsWith('http') ? tx : oss + tx;
avatarUrl = def.startsWith('http') ? def : oss + def; } else if (def) {
} avatarUrl = def.startsWith('http') ? def : oss + def;
return { }
nick, return {
uid, nick,
avatarUrl, uid,
isLoggedIn: !!token, avatarUrl,
}; isLoggedIn: !!token,
} };
}
/** 其他端登录/注册成功后,把 globalData 里已有字段补写回 Storage */
export function backfillUserProfileCache(app) { /** 其他端登录/注册成功后,把 globalData 里已有字段补写回 Storage */
app = app || getApp(); export function backfillUserProfileCache(app) {
if (!isUserLoggedIn()) return; app = app || getApp();
const profile = getUserProfileForDisplay(app); if (!isUserLoggedIn()) return;
if (profile.nick && !wx.getStorageSync('nicheng')) { const profile = getUserProfileForDisplay(app);
wx.setStorageSync('nicheng', profile.nick); if (profile.nick && !wx.getStorageSync('nicheng')) {
app.globalData.nicheng = profile.nick; wx.setStorageSync('nicheng', profile.nick);
} app.globalData.nicheng = profile.nick;
if (profile.uid && !wx.getStorageSync('uid')) { }
wx.setStorageSync('uid', profile.uid); if (profile.uid && !wx.getStorageSync('uid')) {
} wx.setStorageSync('uid', profile.uid);
} }
}
export function applyWechatLoginData(data, page) {
if (!data || typeof data !== 'object') return; export function applyWechatLoginData(data, page) {
const app = getApp(); if (!data || typeof data !== 'object') return;
syncProfileFields(data); const app = getApp();
if (data.token) { syncProfileFields(data);
wx.setStorageSync('token', data.token); if (data.token) {
} wx.setStorageSync('token', data.token);
syncRoleStatuses(data); }
syncGroupFields(data); syncRoleStatuses(data);
if (data.zuzhangstatus !== undefined) { syncGroupFields(data);
wx.setStorageSync('zuzhangstatus', data.zuzhangstatus); if (data.zuzhangstatus !== undefined) {
app.globalData.zuzhangstatus = data.zuzhangstatus; wx.setStorageSync('zuzhangstatus', data.zuzhangstatus);
} app.globalData.zuzhangstatus = data.zuzhangstatus;
if (data.kaoheguanstatus !== undefined) { }
wx.setStorageSync('kaoheguanstatus', data.kaoheguanstatus); if (data.kaoheguanstatus !== undefined) {
app.globalData.kaoheguanstatus = data.kaoheguanstatus; wx.setStorageSync('kaoheguanstatus', data.kaoheguanstatus);
} app.globalData.kaoheguanstatus = data.kaoheguanstatus;
if (data.dingdantiaoshu) { }
app.globalData.dingdanTiaoshu = { if (data.dingdantiaoshu) {
daifuwu: data.dingdantiaoshu.daifuwu || 0, app.globalData.dingdanTiaoshu = {
fuwuzhong: data.dingdantiaoshu.fuwuzhong || 0, daifuwu: data.dingdantiaoshu.daifuwu || 0,
yiwancheng: data.dingdantiaoshu.yiwancheng || 0, fuwuzhong: data.dingdantiaoshu.fuwuzhong || 0,
yituikuan: data.dingdantiaoshu.yituikuan || 0, yiwancheng: data.dingdantiaoshu.yiwancheng || 0,
}; yituikuan: data.dingdantiaoshu.yituikuan || 0,
} };
if (data.isJinpai !== undefined) { }
wx.setStorageSync('isJinpai', Number(data.isJinpai)); if (data.isJinpai !== undefined) {
} wx.setStorageSync('isJinpai', Number(data.isJinpai));
}
const targetPage = page || _getCurrentPage();
refreshTabBar(targetPage); applyStaffFromLoginData(data);
}
const targetPage = page || _getCurrentPage();
function _getCurrentPage() { refreshTabBar(targetPage);
const pages = getCurrentPages(); restoreStaffContextAfterAuth().finally(() => {
return pages.length ? pages[pages.length - 1] : null; refreshTabBar(targetPage);
} if (app.emitEvent) {
app.emitEvent('staffContextChanged', {});
export function isRoleStatusActive(status) { }
return Number(status) === 1; });
} }
export function isCenterPageActive(page, dataFlag, statusKey) { function _getCurrentPage() {
return page.data[dataFlag] || isRoleStatusActive(wx.getStorageSync(statusKey)); const pages = getCurrentPages();
} return pages.length ? pages[pages.length - 1] : null;
}
export function syncRoleStatuses(data) {
if (!data || typeof data !== 'object') return; export function isRoleStatusActive(status) {
const app = getApp(); return Number(status) === 1;
const roleFields = ['dashoustatus', 'guanshistatus', 'shangjiastatus', 'zuzhangstatus', 'kaoheguanstatus']; }
roleFields.forEach((field) => {
if (Object.prototype.hasOwnProperty.call(data, field)) { export function isCenterPageActive(page, dataFlag, statusKey) {
const val = Number(data[field]); return page.data[dataFlag] || isRoleStatusActive(wx.getStorageSync(statusKey)) || isStaffMode();
const normalized = Number.isNaN(val) ? 0 : val; }
wx.setStorageSync(field, normalized);
app.globalData[field] = normalized; export function syncRoleStatuses(data) {
} if (!data || typeof data !== 'object') return;
}); const app = getApp();
} const roleFields = ['dashoustatus', 'guanshistatus', 'shangjiastatus', 'zuzhangstatus', 'kaoheguanstatus'];
roleFields.forEach((field) => {
export function syncProfileFields(data) { if (Object.prototype.hasOwnProperty.call(data, field)) {
if (!data || typeof data !== 'object') return; const val = Number(data[field]);
const app = getApp(); const normalized = Number.isNaN(val) ? 0 : val;
['nicheng', 'uid', 'touxiang', 'token'].forEach((field) => { wx.setStorageSync(field, normalized);
if (data[field] !== undefined && data[field] !== null) { app.globalData[field] = normalized;
wx.setStorageSync(field, data[field]); }
app.globalData[field] = data[field]; });
} }
});
} export function syncProfileFields(data) {
if (!data || typeof data !== 'object') return;
/** 清除缓存:清空登录与身份,回到点单端「我的」 */ const app = getApp();
export function clearCacheAndEnterNormal(page) { ['nicheng', 'uid', 'touxiang', 'token'].forEach((field) => {
resetGuestSession(page, { skipPageReset: true }); if (data[field] !== undefined && data[field] !== null) {
wx.reLaunch({ url: '/pages/mine/mine' }); wx.setStorageSync(field, data[field]);
} app.globalData[field] = data[field];
}
/** 清除缓存并重置为点单端(不跳转,供当前已在点单端时使用) */ });
export function resetGuestSession(page, options = {}) { }
const app = getApp();
const configKey = app.CONFIG_CACHE_KEY || 'app_dynamic_config'; /** 清除缓存:清空登录与身份,回到点单端「我的」 */
let configCache = null; export function clearCacheAndEnterNormal(page) {
try { resetGuestSession(page, { skipPageReset: true });
configCache = wx.getStorageSync(configKey); wx.reLaunch({ url: '/pages/mine/mine' });
} catch (e) {} }
wx.clearStorageSync(); /** 清除缓存并重置为点单端(不跳转,供当前已在点单端时使用) */
export function resetGuestSession(page, options = {}) {
if (configCache && typeof configCache === 'object') { const app = getApp();
wx.setStorageSync(configKey, configCache); const configKey = app.CONFIG_CACHE_KEY || 'app_dynamic_config';
} let configCache = null;
try {
clearPrimaryRole(app); configCache = wx.getStorageSync(configKey);
app.globalData.token = ''; } catch (e) {}
app.globalData.nicheng = '';
app.globalData.uid = ''; wx.clearStorageSync();
app.globalData.loginPromise = null;
app.globalData.currentUser = null; clearStaffSession();
app.globalData.dashouNicheng = '';
app.globalData.dingdanTiaoshu = { daifuwu: 0, fuwuzhong: 0, yiwancheng: 0, yituikuan: 0 }; if (configCache && typeof configCache === 'object') {
app.globalData.dashouqun = ''; wx.setStorageSync(configKey, configCache);
app.globalData.dashouqunid = ''; }
app.globalData.guanshiqun = '';
app.globalData.guanshiqunid = ''; clearPrimaryRole(app);
SESSION_STATUS_KEYS.forEach((key) => { app.globalData.token = '';
app.globalData[key] = 0; app.globalData.nicheng = '';
}); app.globalData.uid = '';
app.globalData.shangpinleixing = []; app.globalData.loginPromise = null;
app.globalData.shangpinzhuanqu = []; app.globalData.currentUser = null;
app.globalData.shangpinliebiao = []; app.globalData.dashouNicheng = '';
app.globalData.dingdanTiaoshu = { daifuwu: 0, fuwuzhong: 0, yiwancheng: 0, yituikuan: 0 };
app.emitEvent('currentRoleChanged', { role: 'normal' }); app.globalData.dashouqun = '';
refreshTabBar(page); app.globalData.dashouqunid = '';
app.globalData.guanshiqun = '';
if (!options.skipPageReset && page && page.setData) { app.globalData.guanshiqunid = '';
page.setData({ SESSION_STATUS_KEYS.forEach((key) => {
avatarUrl: '', app.globalData[key] = 0;
userNicheng: '', });
userUid: '', app.globalData.shangpinleixing = [];
showLoginBtn: true, app.globalData.shangpinzhuanqu = [];
orderCounts: { daifuwu: 0, fuwuzhong: 0, yiwancheng: 0, yituikuan: 0 }, app.globalData.shangpinliebiao = [];
dashouCertified: false,
shangjiaCertified: false, app.emitEvent('currentRoleChanged', { role: 'normal' });
}); refreshTabBar(page);
}
} if (!options.skipPageReset && page && page.setData) {
page.setData({
export function syncGroupFields(data) { avatarUrl: '',
if (!data || typeof data !== 'object') return; userNicheng: '',
const app = getApp(); userUid: '',
['dashouqun', 'dashouqunid', 'guanshiqun', 'guanshiqunid'].forEach((field) => { showLoginBtn: true,
if (data[field] !== undefined) { orderCounts: { daifuwu: 0, fuwuzhong: 0, yiwancheng: 0, yituikuan: 0 },
wx.setStorageSync(field, data[field]); dashouCertified: false,
app.globalData[field] = data[field]; shangjiaCertified: false,
} });
}); }
} }
export function refreshTabBar(page) { export function syncGroupFields(data) {
const tryRefresh = () => { if (!data || typeof data !== 'object') return;
const tabBar = page.getTabBar && page.getTabBar(); const app = getApp();
if (tabBar && tabBar.refresh) tabBar.refresh(); ['dashouqun', 'dashouqunid', 'guanshiqun', 'guanshiqunid'].forEach((field) => {
}; if (data[field] !== undefined) {
tryRefresh(); wx.setStorageSync(field, data[field]);
[50, 150, 300, 500, 800].forEach((delay) => setTimeout(tryRefresh, delay)); app.globalData[field] = data[field];
} }
});
export function setRoleAndRefreshTabBar(page, role) { }
const app = getApp();
if (app.setCurrentRole) { export function refreshTabBar(page) {
app.setCurrentRole(role); const tryRefresh = () => {
} else { const tabBar = page.getTabBar && page.getTabBar();
app.globalData.currentRole = role; if (tabBar && tabBar.refresh) tabBar.refresh();
wx.setStorageSync('currentRole', role); };
app.emitEvent('currentRoleChanged', { role }); tryRefresh();
} [50, 150, 300, 500, 800].forEach((delay) => setTimeout(tryRefresh, delay));
refreshTabBar(page); }
}
export function setRoleAndRefreshTabBar(page, role) {
export function reconnectForRole(role) { const app = getApp();
const app = getApp(); if (app.setCurrentRole) {
if (!wx.getStorageSync('uid')) return; app.setCurrentRole(role);
} else {
const targetUserId = getLocalImUserId(app); app.globalData.currentRole = role;
if (!targetUserId) return; wx.setStorageSync('currentRole', role);
app.emitEvent('currentRoleChanged', { role });
const status = wx.goEasy?.getConnectionStatus?.() || 'disconnected'; }
const currentUserId = wx.goEasy?.im?.userId || ''; refreshTabBar(page);
const connected = status === 'connected' || status === 'reconnected'; }
if (connected && currentUserId === targetUserId) { export function reconnectForRole(role) {
if (app.loadConversations) app.loadConversations(); const app = getApp();
if (app.ensureConnection) app.ensureConnection(); if (!wx.getStorageSync('uid')) return;
return;
} const targetUserId = getLocalImUserId(app);
if (!targetUserId) return;
if (app.switchRoleAndReconnect) {
app.switchRoleAndReconnect(role).catch(() => { const status = wx.goEasy?.getConnectionStatus?.() || 'disconnected';
if (app.ensureConnection) app.ensureConnection(); const currentUserId = wx.goEasy?.im?.userId || '';
}); const connected = status === 'connected' || status === 'reconnected';
} else if (app.connectForCurrentRole) {
app.connectForCurrentRole(); if (connected && currentUserId === targetUserId) {
} else if (app.ensureConnection) { if (app.loadConversations) app.loadConversations();
app.ensureConnection(); if (app.ensureConnection) app.ensureConnection();
} return;
} }
export function ensureRoleOnCenterPage(page, role) { if (app.switchRoleAndReconnect) {
setRoleAndRefreshTabBar(page, role); app.switchRoleAndReconnect(role).catch(() => {
reconnectForRole(role); if (app.ensureConnection) app.ensureConnection();
} });
} else if (app.connectForCurrentRole) {
app.connectForCurrentRole();
} else if (app.ensureConnection) {
app.ensureConnection();
}
}
export function ensureRoleOnCenterPage(page, role) {
setRoleAndRefreshTabBar(page, role);
reconnectForRole(role);
}

253
utils/staff-api.js Normal file
View File

@@ -0,0 +1,253 @@
/**
* 商家子客服 API 与身份上下文
*
* 身份说明(互斥,不冲突):
* - 商家老板shangjiastatus=1走原商家接口扣商家余额
* - 子客服(含总管/财务/派单员staff_context.active走 merchant-staff 接口,扣个人额度
* - 同账号不能既是老板又是子客服;换角色只改权限,额度/统计仍归属同一 member 记录
*/
import { isApiSuccess } from './api-helper.js';
const STAFF_PREFIX = '/yonghu/merchant-staff';
export const STAFF_API = {
me: `${STAFF_PREFIX}/me`,
bind: `${STAFF_PREFIX}/bind`,
inviteCreate: `${STAFF_PREFIX}/invite/create`,
inviteList: `${STAFF_PREFIX}/invite/list`,
inviteRevoke: `${STAFF_PREFIX}/invite/revoke`,
memberList: `${STAFF_PREFIX}/member/list`,
memberUpdateRole: `${STAFF_PREFIX}/member/update-role`,
memberRemove: `${STAFF_PREFIX}/member/remove`,
memberDisable: `${STAFF_PREFIX}/member/disable`,
roleList: `${STAFF_PREFIX}/role/list`,
roleSetPerms: `${STAFF_PREFIX}/role/set-perms`,
walletAllocate: `${STAFF_PREFIX}/wallet/allocate`,
walletRevoke: `${STAFF_PREFIX}/wallet/revoke`,
walletLedger: `${STAFF_PREFIX}/wallet/ledger`,
auditList: `${STAFF_PREFIX}/audit/list`,
rank: `${STAFF_PREFIX}/rank`,
roleCreate: `${STAFF_PREFIX}/role/create`,
roleSetPerms: `${STAFF_PREFIX}/role/set-perms`,
memberUpdatePerms: `${STAFF_PREFIX}/member/update-perms`,
templateList: `${STAFF_PREFIX}/template/list`,
templateCreate: `${STAFF_PREFIX}/template/create`,
templateUpdate: `${STAFF_PREFIX}/template/update`,
templateDelete: `${STAFF_PREFIX}/template/delete`,
linkGenerate: `${STAFF_PREFIX}/link/generate`,
linkList: `${STAFF_PREFIX}/link/list`,
orderList: `${STAFF_PREFIX}/order/list`,
orderDetail: `${STAFF_PREFIX}/order/detail`,
orderDispatch: `${STAFF_PREFIX}/order/dispatch`,
productTypes: `${STAFF_PREFIX}/order/product-types`,
orderSettle: `${STAFF_PREFIX}/order/settle`,
orderCancel: `${STAFF_PREFIX}/order/cancel`,
orderRefund: `${STAFF_PREFIX}/order/refund`,
penaltyApply: `${STAFF_PREFIX}/order/penalty-apply`,
penaltyManage: `${STAFF_PREFIX}/order/penalty-manage`,
changePlayer: `${STAFF_PREFIX}/order/change-player`,
};
export function getStaffContext() {
return wx.getStorageSync('staff_context') || null;
}
export function setStaffContext(ctx) {
if (ctx && ctx.active) {
wx.setStorageSync('staff_context', ctx);
wx.setStorageSync('staffstatus', 1);
} else {
wx.removeStorageSync('staff_context');
wx.setStorageSync('staffstatus', 0);
}
}
export function isStaffMode() {
// 商家老板与子客服互斥:已认证商家时一律按老板身份
if (Number(wx.getStorageSync('shangjiastatus')) === 1) {
return false;
}
const ctx = getStaffContext();
if (ctx && ctx.active) {
return true;
}
// 绑定/登录已写入 staffstatuscontext 异步刷新期间仍视为客服
return Number(wx.getStorageSync('staffstatus')) === 1;
}
export function clearStaffSession() {
wx.removeStorageSync('staff_context');
wx.setStorageSync('staffstatus', 0);
}
/** 是否仅为商家老板(非子客服) */
export function isMerchantOwnerOnly() {
return Number(wx.getStorageSync('shangjiastatus')) === 1 && !isStaffMode();
}
/** 子客服/非老板禁止进入商家充值提现等页面 */
export function guardMerchantOwnerAction(msg) {
if (isMerchantOwnerOnly()) return true;
wx.showToast({
title: msg || '仅商家老板可充值或提现,子客服请联系老板',
icon: 'none',
});
return false;
}
export function staffHasPerm(code) {
const ctx = getStaffContext();
if (!ctx || !ctx.permissions) return false;
return ctx.permissions.indexOf(code) >= 0;
}
function loadRequest() {
// 延迟加载,避免 App 未注册时 request 模块被提前初始化
return require('./request.js').default;
}
export async function refreshStaffContext(req) {
const cached = getStaffContext();
const token = wx.getStorageSync('token');
if (!token) return null;
const doRequest = req || loadRequest();
try {
const res = await doRequest({ url: STAFF_API.me, method: 'POST' });
const body = res.data || {};
if (res.statusCode === 401) {
return cached;
}
if (isApiSuccess(body) && body.data && body.data.active) {
setStaffContext(body.data);
return body.data;
}
if (body.code === 404 || body.code === 403) {
// 已有本地客服身份时勿清空,避免误入「商家入驻」
if (cached && cached.active) {
return cached;
}
if (Number(wx.getStorageSync('staffstatus')) === 1) {
return cached;
}
clearStaffSession();
return null;
}
return cached;
} catch (e) {
return cached;
}
}
/** 登录/注册响应写入子客服身份 */
export function applyStaffFromLoginData(data) {
if (!data || typeof data !== 'object') return;
if (data.staff_context && data.staff_context.active) {
setStaffContext(data.staff_context);
return;
}
if (Number(data.staffstatus) === 1) {
wx.setStorageSync('staffstatus', 1);
return;
}
if (Object.prototype.hasOwnProperty.call(data, 'staffstatus') && Number(data.staffstatus) !== 1) {
clearStaffSession();
}
}
/** 登录成功后从 /me 拉取最新子客服身份 */
export async function restoreStaffContextAfterAuth() {
const token = wx.getStorageSync('token');
if (!token) return null;
return refreshStaffContext(loadRequest());
}
/** 客服模式下返回客服专用 URL否则返回商家原 URL */
export function merchantApi(staffPath, ownerPath) {
return isStaffMode() ? staffPath : ownerPath;
}
export function staffCan(action) {
if (!isStaffMode()) return true;
const map = {
dispatch: 'order_dispatch',
detail: 'order_detail',
settle: 'order_settle',
cancel: 'order_cancel',
refund: 'order_refund_apply',
penalty: 'penalty_apply',
penaltyManage: 'penalty_manage',
changePlayer: 'order_change_player',
staffManage: 'staff_manage',
audit: 'audit_view',
finance: 'finance_view',
walletAllocate: 'wallet_allocate',
walletRevoke: 'wallet_revoke',
template: 'template_manage',
imChat: 'im_chat',
};
return staffHasPerm(map[action] || action);
}
/** 商家老板或有效子客服,均可进入商家端工作台 */
export function isMerchantPortalUser() {
return Number(wx.getStorageSync('shangjiastatus')) === 1 || isStaffMode();
}
/** 将 /me 返回的 stats 写入页面 */
export function applyStaffStatsToPage(page, ctx) {
const st = (ctx && ctx.stats) || {};
if (!st || typeof page.setData !== 'function') return;
const patch = {};
if (page.data.stats) {
patch.stats = {
...page.data.stats,
todayDispatch: st.today_dispatch_count || 0,
todayRefund: st.today_refund_count || 0,
};
}
if ('jinridingdan' in page.data) {
patch.jinridingdan = st.today_dispatch_count || 0;
patch.jinrituikuan = st.today_refund_count || 0;
patch.fabu = st.dispatch_count || 0;
patch.tuikuan = st.refund_count || 0;
}
if (Object.keys(patch).length) {
page.setData(patch);
}
}
/** 同步子客服 UI 权限到页面 data并确保 isShangjia 对客服为 true */
export function syncStaffUi(page) {
const staff = isStaffMode();
const ctx = staff ? (getStaffContext() || {}) : null;
const wallet = (ctx && ctx.wallet) || {};
const portal = isMerchantPortalUser();
page.setData({
isShangjia: portal,
isStaffMode: staff,
isMerchantOwner: !staff && Number(wx.getStorageSync('shangjiastatus')) === 1,
staffRoleName: staff ? (ctx.role_name || '客服') : '',
staffMerchantId: staff ? (ctx.merchant_id || '') : '',
quotaAvailable: staff ? (wallet.quota_available || '0.00') : '',
canDispatch: staffCan('dispatch'),
canSettle: staffCan('settle'),
canCancel: staffCan('cancel'),
canRefund: staffCan('refund'),
canPenalty: staffCan('penalty'),
canPenaltyManage: staffCan('penaltyManage'),
canChangePlayer: staffCan('changePlayer'),
canStaffManage: staffCan('staffManage'),
canWalletAllocate: staffCan('walletAllocate'),
canWalletRevoke: staffCan('walletRevoke'),
canFinance: staffCan('finance'),
canTemplate: staffCan('template'),
canAudit: !staff || staffCan('audit'),
canImChat: !staff || staffCan('imChat'),
showRechargeWithdraw: isMerchantOwnerOnly(),
});
}
/** 客服模式走 staff 专用 URL老板走原商家 URL */
export function staffOrOwner(staffUrl, ownerUrl) {
return isStaffMode() ? staffUrl : ownerUrl;
}