Files
xingque/pages/fighter-rank/fighter-rank.js
2026-07-09 00:17:03 +08:00

487 lines
15 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 CLOSED_DATES = ['昨日', '上周', '上月']
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: '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
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: '今日',
fanwei: 'club',
showJituanScope: false, // 临时隐藏集团总榜,恢复时改 true 并调整 fanwei 默认值
myClubId: CLUB_ID || 'xq',
roleMeta: ROLE_META.dashou,
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 = 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: {
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: '总排行榜' })
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()
},
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()
},
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 || (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) => {
const v = raw[m.field]
return {
label: m.label,
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,
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,
}
},
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/phbjlxx',
method: 'POST',
data: {
shenfen: this.data.currentRole,
riqi: this.data.currentDate,
club_id: this.data.myClubId,
},
})
const body = res?.data || {}
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 = 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),
restList: normalized.slice(3),
isEmpty: normalized.length === 0,
isLoading: false,
})
this.ensureAvatars()
if (!rewardFromRank) {
await this.fetchRewardInfo()
}
} 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()
},
})