Files
xingque/pages/fighter-rank/fighter-rank.js
2026-06-27 07:38:01 +08:00

344 lines
9.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 排行榜
* 默认 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 ROLE_LIST = [
{ 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: '上月' },
]
const 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: 'shouru_zonge',
sortLabel: '收入总额',
mainType: 'money',
metrics: [
{ field: 'yaoqing_dashou_shu', label: '邀请', type: 'int' },
{ field: 'chongzhi_dashou_shu', label: '充值', type: 'int' },
],
},
zuzhang: {
title: '组长排行榜',
sortField: 'shouru_zonge',
sortLabel: '收入总额',
mainType: 'money',
metrics: [
{ field: 'yaoqing_guanshi_shu', label: '邀请', type: 'int' },
{ field: 'fenyong_jine', label: '分佣', type: 'money' },
],
},
shangjia: {
title: '商家排行榜',
sortField: 'jiesuan_jine',
sortLabel: '成交总额',
mainType: 'money',
metrics: [
{ 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: 'yaoqing_dashou_shu',
sortLabel: '邀请人数',
mainType: 'int',
metrics: [
{ field: 'wuxiao_yaoqing_dashou_shu', label: '无效邀请', type: 'int' },
{ field: 'chongzhi_dashou_shu', label: '有效人数', type: 'int' },
{ field: 'shouru_zonge', label: '收入金额', type: 'money' },
],
},
zuzhang: {
title: '组长排行榜',
sortField: 'yaoqing_guanshi_shu',
sortLabel: '邀请人数',
mainType: 'int',
metrics: [
{ field: 'wuxiao_yaoqing_guanshi_shu', label: '无效邀请', type: 'int' },
{ field: 'youxiao_guanshi_shu', label: '有效人数', type: 'int' },
{ field: 'shouru_zonge', label: '收入金额', type: 'money' },
],
},
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
const s = path.trim()
return !s || s === 'null' || s === 'undefined' || s === 'none'
}
function getDefaultAvatar(app) {
return getDefaultAvatarUrl(app)
}
function buildAvatar(path, app) {
app = app || getApp()
if (isInvalidAvatarPath(path)) return getDefaultAvatar(app)
return resolveAvatarUrl(path, app) || getDefaultAvatar(app)
}
Page({
data: {
imgUrls: {},
roleList: ROLE_LIST,
dateList: DATE_LIST,
currentRole: 'dashou',
currentDate: '今日',
roleMeta: ROLE_META.dashou,
sortLabel: '分红总额',
isLoading: true,
rankList: [],
topThree: [],
restList: [],
isEmpty: false,
defaultAvatar: '',
},
onLoad(options) {
const type = options.type || options.rankType || 'dashou'
const validRole = ACTIVE_ROLE_META[type] ? type : 'dashou'
this.initPage()
this.applyRole(validRole, false)
this.fetchRankList()
},
onPullDownRefresh() {
this.fetchRankList().finally(() => wx.stopPullDownRefresh())
},
initPage() {
this.syncDefaultAvatar()
const g = getApp().globalData
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 再同步 */
syncDefaultAvatar() {
const def = getDefaultAvatar()
if (def && def !== this.data.defaultAvatar) {
this.setData({ defaultAvatar: def })
} else if (!this.data.defaultAvatar && def) {
this.setData({ defaultAvatar: def })
}
},
/** 配置晚于页面加载时,补全空头像 */
ensureAvatars() {
const def = getDefaultAvatar()
if (!def) return
const g = getApp().globalData
const oss = g.ossImageUrl || ''
const { rankList, topThree, restList } = this.data
if (!rankList.length) {
if (def !== this.data.defaultAvatar) this.setData({ defaultAvatar: def })
return
}
const isValidUrl = (url) => {
if (!url || typeof url !== 'string') return false
if (url.startsWith('http://') || url.startsWith('https://')) return true
return !!(oss && url.indexOf(oss) === 0)
}
const fix = (item) => ({
...item,
avatar: isValidUrl(item.avatar) ? item.avatar : def,
})
this.setData({
defaultAvatar: def,
rankList: rankList.map(fix),
topThree: topThree.map(fix),
restList: restList.map(fix),
})
},
applyRole(role, reload = true) {
const meta = ACTIVE_ROLE_META[role]
wx.setNavigationBarTitle({ title: meta.title })
this.setData({ currentRole: role, roleMeta: meta, sortLabel: meta.sortLabel })
if (reload) this.fetchRankList()
},
onRoleTap(e) {
const role = e.currentTarget.dataset.role
if (!role || role === this.data.currentRole) return
this.applyRole(role)
},
onDateTap(e) {
const date = e.currentTarget.dataset.date
if (!date || date === this.data.currentDate) return
this.setData({ currentDate: date })
this.fetchRankList()
},
onRefreshTap() {
this.fetchRankList()
},
formatMoney(val) {
const n = parseFloat(val)
return Number.isNaN(n) ? '0.00' : n.toFixed(2)
},
formatInt(val) {
const n = parseInt(val, 10)
return Number.isNaN(n) ? '0' : String(n)
},
normalizeItem(raw, index, role) {
const meta = ACTIVE_ROLE_META[role]
const app = getApp()
const uid = raw.yonghuid || raw.uid || ''
const nicheng = raw.nicheng || raw.nick || '用户'
const def = getDefaultAvatar(app)
const avatar = buildAvatar(raw.touxiang || raw.avatar, app) || def
const metrics = meta.metrics.map((m) => {
const v = raw[m.field]
return {
label: m.label,
value: m.type === 'money' ? this.formatMoney(v) : this.formatInt(v),
}
})
const sortRaw = raw[meta.sortField]
const mainType = meta.mainType || 'money'
const mainValue = mainType === 'money'
? this.formatMoney(sortRaw)
: this.formatInt(sortRaw)
return {
mingci: raw.mingci || index + 1,
uid,
nicheng,
avatar,
mainType,
mainPrefix: mainType === 'money' ? '¥' : '',
mainValue,
mainLabel: meta.sortLabel,
metrics,
}
},
async fetchRankList() {
this.setData({ isLoading: true, isEmpty: false })
try {
const res = await request({
url: RANK_API,
method: 'POST',
data: { shenfen: this.data.currentRole, riqi: this.data.currentDate },
})
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 (!Array.isArray(list)) list = []
const role = this.data.currentRole
const normalized = list.slice(0, 50).map((item, i) => this.normalizeItem(item, i, role))
this.setData({
rankList: normalized,
topThree: normalized.slice(0, 3),
restList: normalized.slice(3),
isEmpty: normalized.length === 0,
isLoading: false,
})
this.ensureAvatars()
} catch (e) {
console.error('排行榜加载失败', e)
wx.showToast({ title: '加载失败', icon: 'none' })
this.setData({ rankList: [], topThree: [], restList: [], isEmpty: true, isLoading: false })
}
},
onAvatarError(e) {
const { zone, index } = e.currentTarget.dataset
const fallback = getDefaultAvatar()
if (!fallback) return
const key = zone === 'top' ? `topThree[${index}].avatar` : `restList[${index}].avatar`
this.setData({ [key]: fallback })
},
registerNotificationComponent() {
const app = getApp()
const comp = this.selectComponent('#global-notification')
if (comp?.showNotification) {
app.globalData.globalNotification = {
show: (d) => comp.showNotification(d),
hide: () => comp.hideNotification(),
}
}
},
onShow() {
this.syncDefaultAvatar()
this.ensureAvatars()
this.registerNotificationComponent()
},
})