merge: 合并 origin/main(排行榜/手机号校验)并保留本地体验会员与UI改动

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-08 00:17:07 +08:00
3 changed files with 284 additions and 112 deletions

View File

@@ -1,68 +1,125 @@
/**
* 排行榜 POST /yonghu/phbhqsj
* 参数: shenfen(dashou|guanshi|zuzhang|shangjia), riqi(今日|本周|本月|总榜|昨日|上周|上月)
*/
/**
* 鎺掕姒? * 榛樿 POST /yonghu/phbhqsj
* 鏄熶箣鐣?xzj) POST /yonghu/xzjphbhqsj锛堥個璇蜂汉鏁版帓搴忕瓑涓撶敤瑙勫垯锛? */
import request from '../../utils/request.js'
import { resolveAvatarUrl, getDefaultAvatarUrl } from '../../utils/avatar.js'
import { CLUB_ID } from '../../config/club-config.js'
const IS_XZJ = CLUB_ID === 'xzj'
const RANK_API = IS_XZJ ? '/yonghu/xzjphbhqsj' : '/yonghu/phbhqsj'
const CLOSED_DATES = ['鏄ㄦ棩', '涓婂懆', '涓婃湀']
const ROLE_LIST = [
{ key: 'dashou', label: '接单员' },
{ key: 'guanshi', label: '管事' },
{ key: 'zuzhang', label: '组长' },
{ key: 'shangjia', label: '商家' },
{ key: 'dashou', label: '鎺ュ崟鍛? },
{ key: 'guanshi', label: '' },
{ key: 'zuzhang', label: '缁勯暱' },
{ key: 'shangjia', label: '鍟嗗' },
]
const DATE_LIST = [
{ key: '今日', label: '今日' },
{ key: '本周', label: '本周' },
{ key: '本月', label: '本月' },
{ key: '总榜', label: '总榜' },
{ key: '昨日', label: '昨日' },
{ key: '上周', label: '上周' },
{ key: '上月', label: '上月' },
{ key: '浠婃棩', label: '浠婃棩' },
{ key: '鏄ㄦ棩', label: '鏄ㄦ棩' },
{ key: '', label: '' },
{ key: '涓婂懆', label: '涓婂懆' },
{ key: '', label: '' },
{ key: '涓婃湀', label: '涓婃湀' },
{ key: '鎬绘', label: '鎬绘' },
]
const ROLE_META = {
dashou: {
title: '接单员排行榜',
title: '鎺ュ崟鍛樻帓琛屾',
sortField: 'chengjiao_zonge',
sortLabel: '分红总额',
sortLabel: '鍒嗙孩鎬婚',
mainType: 'money',
metrics: [
{ field: 'jiedan_zongliang', label: '接单量', type: 'int' },
{ field: 'jiedan_zonge', label: '接单额', type: 'money' },
{ field: 'chengjiao_zongliang', label: '成交量', type: 'int' },
{ field: 'jiedan_zongliang', label: '鎺ュ崟閲?, type: 'int' },
{ field: 'jiedan_zonge', label: '鎺ュ崟棰?, type: 'money' },
{ field: 'chengjiao_zongliang', label: '鎴愪氦閲?, type: 'int' },
],
},
guanshi: {
title: '管事排行榜',
title: '绠′簨鎺掕姒?,
sortField: 'shouru_zonge',
sortLabel: '收入总额',
sortLabel: '鏀跺叆鎬婚',
mainType: 'money',
metrics: [
{ field: 'yaoqing_dashou_shu', label: '邀请', type: 'int' },
{ field: 'chongzhi_dashou_shu', label: '充值', type: 'int' },
{ field: 'yaoqing_dashou_shu', label: '?, type: 'int' },
{ field: 'chongzhi_dashou_shu', label: '鍏呭€?, type: 'int' },
],
},
zuzhang: {
title: '组长排行榜',
title: '缁勯暱鎺掕?,
sortField: 'shouru_zonge',
sortLabel: '收入总额',
sortLabel: '鏀跺叆鎬婚',
mainType: 'money',
metrics: [
{ field: 'yaoqing_guanshi_shu', label: '邀请', type: 'int' },
{ field: 'fenyong_jine', label: '分佣', type: 'money' },
{ field: 'yaoqing_guanshi_shu', label: '閭€璇?, type: 'int' },
{ field: 'fenyong_jine', label: '鍒嗕剑', type: 'money' },
],
},
shangjia: {
title: '商家排行榜',
title: '鍟嗗鎺掕?,
sortField: 'jiesuan_jine',
sortLabel: '成交总额',
sortLabel: '鎴愪氦鎬婚',
mainType: 'money',
metrics: [
{ field: 'jiesuan_dingdan_shu', label: '成交量', type: 'int' },
{ field: 'paifa_dingdan_shu', label: '派单量', type: 'int' },
{ field: 'paifa_jine', label: '派单额', type: 'money' },
{ field: 'jiesuan_dingdan_shu', label: '鎴愪氦閲?, type: 'int' },
{ field: 'paifa_dingdan_shu', label: '娲惧崟閲?, type: 'int' },
{ field: 'paifa_jine', label: '娲惧崟棰?, type: 'money' },
],
},
}
/** 鏄熶箣鐣屼笓鐢ㄥ睍绀轰笌鎺掑簭瀛楁 */
const XZJ_ROLE_META = {
dashou: {
title: '鎺ュ崟鍛樻帓琛屾',
sortField: 'chengjiao_zonge',
sortLabel: '鎴愪氦閲戦',
mainType: 'money',
metrics: [
{ field: 'jiedan_zongliang', label: '鎺ュ崟閲?, type: 'int' },
{ field: 'jiedan_zonge', label: '鎺ュ崟棰?, type: 'money' },
{ field: 'chengjiao_zongliang', label: '鎴愪氦閲?, type: 'int' },
],
},
guanshi: {
title: '绠′簨鎺掕姒?,
sortField: 'chongzhi_dashou_shu',
sortLabel: '鏈夋晥浜烘暟',
mainType: 'int',
metrics: [
{ field: 'wuxiao_yaoqing_dashou_shu', label: '鏃犳晥閭?, type: 'int' },
{ field: 'yaoqing_dashou_shu', label: '閭€璇蜂汉鏁?, type: 'int' },
{ field: 'shouru_zonge', label: '鏀剁泭鎬婚', type: 'money' },
],
},
zuzhang: {
title: '缁勯暱鎺掕?,
sortField: 'shouru_zonge',
sortLabel: '鏀剁泭鎬婚',
mainType: 'money',
metrics: [
{ field: 'wuxiao_yaoqing_guanshi_shu', label: '鏃犳晥閭€璇?, type: 'int' },
{ field: 'youxiao_guanshi_shu', label: '鏈夋晥浜烘暟', type: 'int' },
{ field: 'yaoqing_guanshi_shu', label: '璇蜂汉鏁?, type: 'int' },
],
},
shangjia: {
title: '鍟嗗鎺掕姒?,
sortField: 'paifa_jine',
sortLabel: '娲惧崟娴佹按',
mainType: 'money',
metrics: [
{ field: 'paifa_dingdan_shu', label: '娲惧崟閲?, type: 'int' },
{ field: 'paifa_jine', label: '娲惧崟娴佹按', type: 'money' },
],
},
}
const ACTIVE_ROLE_META = IS_XZJ ? XZJ_ROLE_META : ROLE_META
function isInvalidAvatarPath(path) {
if (path === null || path === undefined) return true
if (typeof path !== 'string') return true
@@ -86,20 +143,25 @@ Page({
roleList: ROLE_LIST,
dateList: DATE_LIST,
currentRole: 'dashou',
currentDate: '今日',
currentDate: '浠婃棩',
fanwei: 'club',
showJituanScope: false, // 涓存椂闅愯棌闆嗗洟鎬绘锛屾仮澶嶆椂鏀?true 骞惰皟鏁?fanwei 榛樿鍊? myClubId: CLUB_ID || 'xq',
roleMeta: ROLE_META.dashou,
sortLabel: '分红总额',
sortLabel: '鍒嗙孩鎬婚',
isLoading: true,
rankList: [],
topThree: [],
restList: [],
isEmpty: false,
defaultAvatar: '',
rewardInfo: null,
showClaimBar: false,
claimLoading: false,
},
onLoad(options) {
const type = options.type || options.rankType || 'dashou'
const validRole = ROLE_META[type] ? type : 'dashou'
const validRole = ACTIVE_ROLE_META[type] ? type : 'dashou'
this.initPage()
this.applyRole(validRole, false)
this.fetchRankList()
@@ -115,15 +177,13 @@ Page({
const base = (g.ossImageUrl || '') + 'beijing/paihangbang/'
this.setData({
imgUrls: {
pageBg: base + 'page-bg.jpg',
cardBg: base + 'list-card-bg.png',
emptyIcon: base + 'empty.png',
refreshIcon: base + 'icon-refresh.png',
},
})
},
/** app.js 一致:ossImageUrl + morentouxiang,配置异步加载后 onShow 再同步 */
/** 涓?app.js 涓€鑷达細ossImageUrl + morentouxiang锛岄厤缃紓姝ュ姞杞藉悗 onShow 鍐嶅悓姝?*/
syncDefaultAvatar() {
const def = getDefaultAvatar()
if (def && def !== this.data.defaultAvatar) {
@@ -133,7 +193,7 @@ Page({
}
},
/** 配置晚于页面加载时,补全空头像 */
/** 閰嶇疆鏅氫簬椤甸潰鍔犺浇鏃讹紝琛ュ叏绌哄ご鍍?*/
ensureAvatars() {
const def = getDefaultAvatar()
if (!def) return
@@ -162,8 +222,8 @@ Page({
},
applyRole(role, reload = true) {
const meta = ROLE_META[role]
wx.setNavigationBarTitle({ title: meta.title })
const meta = ACTIVE_ROLE_META[role]
wx.setNavigationBarTitle({ title: '鎬绘帓琛屾' })
this.setData({ currentRole: role, roleMeta: meta, sortLabel: meta.sortLabel })
if (reload) this.fetchRankList()
},
@@ -181,6 +241,61 @@ Page({
this.fetchRankList()
},
onScopeTap(e) {
const fanwei = e.currentTarget.dataset.fanwei
if (!fanwei || fanwei === this.data.fanwei) return
if (fanwei === 'jituan' && !this.data.showJituanScope) return
this.setData({ fanwei })
this.fetchRankList()
},
onShowRules() {
const r = this.data.rewardInfo
if (!r || !r.tiers || !r.tiers.length) {
wx.showToast({ title: '鏆傛棤濂栧姳瑙勫垯', icon: 'none' })
return
}
const lines = r.tiers.map((t) => {
const label = t.label || `绗?{t.rank_from}${t.rank_to > t.rank_from ? '-' + t.rank_to : ''}鍚峘
return `${label}锛毬?{t.amount}`
})
const tip = r.period_status === 'open' ? '锛堟湰鏈熻繘琛屼腑锛岀粨鏉熷悗鍙鍙栵級' : ''
wx.showModal({
title: `${r.title || '鎺掕姒滃鍔?}${tip}`,
content: [
`淇变箰閮細${r.club_id || this.data.myClubId}`,
`鎺掑簭锛?{r.sort_label || ''}`,
...lines,
r.description || '',
].filter(Boolean).join('\n'),
showCancel: false,
})
},
async onClaimTap() {
const claim = this.data.rewardInfo && this.data.rewardInfo.my_claim
if (!claim || !claim.claim_id || this.data.claimLoading) return
this.setData({ claimLoading: true })
try {
const res = await request({
url: '/yonghu/phbjllq',
method: 'POST',
data: { claim_id: claim.claim_id },
})
const body = res?.data || {}
if (body.code === 200) {
wx.showToast({ title: `宸查鍙?楼${claim.amount}`, icon: 'success' })
await this.fetchRewardInfo()
} else {
wx.showToast({ title: body.msg || '棰嗗彇澶辫触', icon: 'none' })
}
} catch (e) {
wx.showToast({ title: '棰嗗彇澶辫触', icon: 'none' })
} finally {
this.setData({ claimLoading: false })
}
},
onRefreshTap() {
this.fetchRankList()
},
@@ -196,10 +311,10 @@ Page({
},
normalizeItem(raw, index, role) {
const meta = ROLE_META[role]
const meta = ACTIVE_ROLE_META[role]
const app = getApp()
const uid = raw.yonghuid || raw.uid || ''
const nicheng = raw.nicheng || raw.nick || '用户'
const nicheng = raw.nicheng || raw.nick || (role === 'dashou' ? '鎵撴墜' : role === 'guanshi' ? '' : role === 'zuzhang' ? '缁勯暱' : role === 'shangjia' ? '鍟嗗' : '鐢ㄦ埛')
const def = getDefaultAvatar(app)
const avatar = buildAvatar(raw.touxiang || raw.avatar, app) || def
const metrics = meta.metrics.map((m) => {
@@ -209,33 +324,120 @@ Page({
value: m.type === 'money' ? this.formatMoney(v) : this.formatInt(v),
}
})
const hasBackendMetric = raw.metric_value !== undefined && raw.metric_value !== null
const sortRaw = IS_XZJ
? (raw[meta.sortField] ?? raw.metric_value)
: (hasBackendMetric ? raw.metric_value : (raw[meta.sortField] ?? raw.metric_value))
let mainType = meta.mainType || 'money'
if (hasBackendMetric && raw.metric_is_int !== undefined) {
mainType = raw.metric_is_int ? 'int' : 'money'
} else if (raw.metric_label) {
mainType = (Number.isInteger(Number(sortRaw)) && !String(sortRaw).includes('.')) ? 'int' : 'money'
}
const mainValue = mainType === 'money'
? this.formatMoney(sortRaw)
: this.formatInt(sortRaw)
return {
mingci: raw.mingci || index + 1,
uid,
nicheng,
avatar,
mainValue: this.formatMoney(raw[meta.sortField]),
mainLabel: meta.sortLabel,
clubName: raw.club_name || raw.club_id || '',
rewardAmount: raw.reward_amount || 0,
mainType,
mainPrefix: mainType === 'money' ? '' : '',
mainValue,
mainLabel: (IS_XZJ ? meta.sortLabel : (raw.metric_label || meta.sortLabel)),
metrics,
}
},
async fetchRankList() {
this.setData({ isLoading: true, isEmpty: false })
applyRewardToList(list, rewardInfo, fanwei) {
if (fanwei !== 'club') return list
if (!rewardInfo || !rewardInfo.rank_rewards) return list
const map = rewardInfo.rank_rewards
return list.map((item) => ({
...item,
rewardAmount: map[item.mingci] || item.rewardAmount || 0,
}))
},
async fetchRewardInfo() {
try {
const res = await request({
url: '/yonghu/phbhqsj',
url: '/yonghu/phbjlxx',
method: 'POST',
data: { shenfen: this.data.currentRole, riqi: this.data.currentDate },
data: {
shenfen: this.data.currentRole,
riqi: this.data.currentDate,
club_id: this.data.myClubId,
},
})
const body = res?.data || {}
const ok = body.code === 200 || body.code === 0
let list = ok && body.data
? (body.data.list || body.data.paihang_list || body.data.rank_list || [])
: []
if (body.code !== 200 || !body.data) return
const rewardInfo = body.data
const showClaimBar = !!(rewardInfo.my_claim && rewardInfo.my_claim.status === 'pending')
const { rankList, fanwei } = this.data
const merged = this.applyRewardToList(rankList, rewardInfo, fanwei)
this.setData({
rewardInfo,
showClaimBar,
rankList: merged,
topThree: merged.slice(0, 3),
restList: merged.slice(3),
})
} catch (e) {
console.warn('濂栧姳淇伅鍔犺浇澶辫触', e)
}
},
async fetchRankList() {
this.setData({ isLoading: true, isEmpty: false, showClaimBar: false })
try {
const { currentRole, currentDate, fanwei, myClubId } = this.data
let list = []
let rewardFromRank = null
if (fanwei === 'club') {
const res = await request({
url: '/yonghu/phbjlphb',
method: 'POST',
data: {
shenfen: currentRole,
riqi: currentDate,
fanwei: 'club',
club_id: myClubId,
},
})
const body = res?.data || {}
if (body.code === 200 && body.data) {
list = body.data.list || []
rewardFromRank = body.data.reward || null
if (body.data.sort_label) {
this.setData({ sortLabel: body.data.sort_label })
}
}
} else {
const res = await request({
url: RANK_API,
method: 'POST',
data: { shenfen: currentRole, riqi: currentDate },
})
const body = res?.data || {}
const ok = body.code === 200 || body.code === 0
list = ok && body.data
? (body.data.list || body.data.paihang_list || body.data.rank_list || [])
: []
}
if (!Array.isArray(list)) list = []
const role = this.data.currentRole
const normalized = list.slice(0, 50).map((item, i) => this.normalizeItem(item, i, role))
const role = currentRole
// 鍚嶆椤哄簭瀹屽叏娌跨敤鍚庣杩斿洖锛屽墠绔笉鍋?sort / 閲嶆帓
let normalized = list.slice(0, 50).map((item, i) => this.normalizeItem(item, i, role))
if (rewardFromRank) {
normalized = this.applyRewardToList(normalized, rewardFromRank, 'club')
this.setData({ rewardInfo: rewardFromRank, showClaimBar: !!(rewardFromRank.my_claim && rewardFromRank.my_claim.status === 'pending') })
}
this.setData({
rankList: normalized,
topThree: normalized.slice(0, 3),
@@ -244,9 +446,12 @@ Page({
isLoading: false,
})
this.ensureAvatars()
if (!rewardFromRank) {
await this.fetchRewardInfo()
}
} catch (e) {
console.error('排行榜加载失败', e)
wx.showToast({ title: '加载失败', icon: 'none' })
console.error('鎺掕姒滃姞杞藉け璐?, e)
wx.showToast({ title: '鍔犺浇澶辫触', icon: 'none' })
this.setData({ rankList: [], topThree: [], restList: [], isEmpty: true, isLoading: false })
}
},

View File

@@ -1,5 +1,7 @@
/**
* 手机号强制认证:每次调用 check / ensurePhoneAuth 均请求后端 /peizhi/yhbdsjh
* 手机号强制认证 - 完全自包含逻辑模块
*
* check / ensurePhoneAuth 每次均请求后端 /peizhi/yhbdsjh 判断 need_auth
*/
const API_BASE = 'https://www.abas.asia/hqhd';
@@ -7,7 +9,6 @@ const AUTH_CHECK_URL = '/peizhi/yhbdsjh';
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';
function innerRequest(url, data = {}) {
return new Promise((resolve, reject) => {
@@ -91,29 +92,10 @@ function parseCheckResponse(res) {
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';
}
/**
* 询问后端是否需要手机号认证
* @returns {Promise<boolean>} true=需要认证
*/
async function check() {
const token = wx.getStorageSync('token');
if (!token) return false;
@@ -128,13 +110,9 @@ async function check() {
}
}
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);
function redirectToPhoneAuth(returnRole) {
if (returnRole) {
wx.setStorageSync(RETURN_ROLE_KEY, returnRole);
}
if (!isOnPhoneAuthPage()) {
wx.reLaunch({ url: AUTH_PAGE });
@@ -142,32 +120,26 @@ function redirectToPhoneAuth(returnRole, returnPage) {
}
/**
* 向后端确认是否需要手机号认证;需要则跳转认证页
* @returns {Promise<boolean>} true=无需认证或已满足false=已跳转认证页
* 打手端等场景:向后端确认,未认证则跳转认证页
* @returns {Promise<boolean>} true=已通过或无需认证false=已跳转认证页
*/
async function ensurePhoneAuth(options = {}) {
const { redirect = true, role, returnPage } = options;
const { redirect = true, role = 'dashou' } = options;
const token = wx.getStorageSync('token');
if (!token) return true;
const needAuth = await check();
if (!needAuth) return true;
if (redirect) {
redirectToPhoneAuth(role || resolveReturnRole(), returnPage);
}
if (redirect) redirectToPhoneAuth(role);
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();
const role = wx.getStorageSync(RETURN_ROLE_KEY) || '';
wx.removeStorageSync(RETURN_ROLE_KEY);
return defaultReturnPage(role);
if (role === 'dashou') return '/pages/accept-order/accept-order';
if (role === 'shangjia') return '/pages/merchant-orders/merchant-orders';
return '/pages/mine/mine';
}
async function submit(phoneCode, avatarPath) {
@@ -194,5 +166,4 @@ module.exports = {
ensurePhoneAuth,
redirectToPhoneAuth,
getPhoneAuthReturnPage,
resolveReturnRole,
};

View File

@@ -1,6 +1,5 @@
/** 三端固定normal | dashou | shangjia认证后不可回退 */
import { isStaffMode } from './staff-api.js';
import { ensurePhoneAuth } from './phone-auth.js';
const VALID = ['normal', 'dashou', 'shangjia'];
@@ -56,9 +55,6 @@ export function getPrimaryRole(app) {
if (isRoleStatusActive(wx.getStorageSync('shangjiastatus'))) {
return 'shangjia';
}
if (isStaffMode()) {
return 'shangjia';
}
return 'normal';
}
@@ -89,7 +85,7 @@ export function clearPrimaryRole(app) {
export const PRIMARY_DEFAULT_PAGES = {
normal: '/pages/mine/mine',
dashou: '/pages/accept-order/accept-order',
shangjia: '/pages/merchant-home/merchant-home',
shangjia: '/pages/merchant-orders/merchant-orders',
};
/** 锁定主端并跳转到该端默认首页,同时建立 IM 长连接 */
@@ -111,8 +107,8 @@ function finishEnterLockedRole(role, app) {
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 (role === 'dashou') {
ensurePhoneAuth({ redirect: true, role: 'dashou' }).then((ok) => {
if (ok) finishEnterLockedRole(role, app);
});
return true;