/** * 排行榜 * 默认 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() }, })