From cc650762f84f24c291d8fe35656b3b51eccfb39b Mon Sep 17 00:00:00 2001 From: XingQue Date: Mon, 27 Jul 2026 17:20:24 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8A=A2=E5=8D=95=E6=B1=A0=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E7=BA=A2=E8=A7=92=E6=A0=87=20+=20=E7=BB=84=E9=98=9F?= =?UTF-8?q?=20Tab=20=E5=BE=85=E7=BB=84=E9=98=9F=E8=A7=92=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pages/accept-order/accept-order.js | 29 ++++++-- pages/accept-order/accept-order.wxml | 15 +++- pages/accept-order/accept-order.wxss | 25 +++++++ pages/fighter-orders/fighter-orders.js | 95 ++++++++++++++++++++++++-- pages/fighter/fighter.js | 3 + pages/team-hall/team-hall.js | 53 +++++++++++--- tab-bar/index.js | 40 ++++++++++- tab-bar/index.wxml | 7 +- utils/dashou-badges.js | 83 ++++++++++++++++++++++ utils/dashou-profile.js | 2 + 10 files changed, 325 insertions(+), 27 deletions(-) create mode 100644 utils/dashou-badges.js diff --git a/pages/accept-order/accept-order.js b/pages/accept-order/accept-order.js index 4b4231f..5c53ef2 100644 --- a/pages/accept-order/accept-order.js +++ b/pages/accept-order/accept-order.js @@ -14,6 +14,10 @@ import { ensureRoleAgreement } from '../../utils/role-agreement-gate.js'; import { fetchMyZhidingOrders, submitZhidingResponse, processZhidingList, filterZhidingBannerList, ZHIDING_HF_MAP } from '../../utils/zhiding-order.js'; import { isFighterOnly, FIGHTER_MINE, isDashouCertified, canEnterAcceptOrder, kickToLoginPage } from '../../utils/fighter-only.js'; +/** 成交指标芯片:有真实样本才展示,过滤假 0% */ +import { buildDealStatChips } from '../../utils/deal-stat-display.js'; +import { consumeBadgeFields, mergeTypePendingCounts } from '../../utils/dashou-badges.js'; + /** 抢单池轮播 page_key(勿与点单页 order_pool 混用) */ const ACCEPT_ORDER_PAGE_KEY = 'accept_order'; @@ -207,10 +211,10 @@ Page({ return; } - if (!app.globalData._dashouPhoneChecked) { + // 每次进入抢单池都校验手机号认证 + { const phoneOk = await ensurePhoneAuth({ redirect: true, role: 'dashou' }); if (!phoneOk) return; - app.globalData._dashouPhoneChecked = true; } this.registerNotificationComponent(); @@ -367,7 +371,8 @@ Page({ else if (Array.isArray(data.list)) list = data.list; else if (Array.isArray(data)) list = data; } - const processedList = this.processTupianUrl(list); + const processedList = mergeTypePendingCounts(this.processTupianUrl(list), data.data?.type_pending_counts); + consumeBadgeFields(data.data || {}); const firstId = processedList[0]?.id || null; const keepPrev = preserveSelection && prevId && processedList.some((i) => i.id === prevId); const selectedId = keepPrev ? prevId : firstId; @@ -513,9 +518,15 @@ Page({ this._listRequesting = false; if (res.data.code === 200 || res.data.code === 0) { - const newList = res.data.data.list || []; - const hasMore = res.data.data.has_more || false; - const serverClumber = res.data.data.clumber; + const payload = res.data.data || {}; + consumeBadgeFields(payload); + if (payload.type_pending_counts) { + const merged = mergeTypePendingCounts(this.data.shangpinleixing, payload.type_pending_counts); + this.setData({ shangpinleixing: merged }); + } + const newList = payload.list || []; + const hasMore = payload.has_more || false; + const serverClumber = payload.clumber; if (Array.isArray(serverClumber) && serverClumber.length) { app.globalData.clumber = serverClumber; this.setData({ huiyuanList: serverClumber }); @@ -601,6 +612,11 @@ Page({ const tags = normalizeOrderTags(item) + const dealStatChips = buildDealStatChips(item.shangjia_deal_stat, { + dealLabel: '成交率', + fineLabel: '罚款率', + }); + return { ...item, full_tupian_url: fullTupianUrl, @@ -620,6 +636,7 @@ Page({ shangjia_youzhi: !!item.shangjia_youzhi, // 🆕 是否有查看价格的权限 hasRequiredMember: hasRequiredMember, + dealStatChips, }; }, diff --git a/pages/accept-order/accept-order.wxml b/pages/accept-order/accept-order.wxml index 9531fdc..8506e4e 100644 --- a/pages/accept-order/accept-order.wxml +++ b/pages/accept-order/accept-order.wxml @@ -136,7 +136,10 @@ - + + + {{item.pending_badge}} + {{item.jieshao}} @@ -366,6 +369,9 @@ {{item.sjnicheng || '未知商家'}} 发布于 {{item.creat_time}} + + {{c.t}} + {{item.isMyZhiding ? '立即接待' : '抢单'}} @@ -411,7 +417,12 @@ - {{item.sjnicheng || '未知商家'}} + + {{item.sjnicheng || '未知商家'}} + + {{c.t}} + + diff --git a/pages/accept-order/accept-order.wxss b/pages/accept-order/accept-order.wxss index d645434..569a3b5 100644 --- a/pages/accept-order/accept-order.wxss +++ b/pages/accept-order/accept-order.wxss @@ -122,6 +122,31 @@ page { transition: all 0.3s ease; border: 2rpx solid rgba(0, 200, 255, 0.4); } + + .leixing-pic-wrap { + position: relative; + width: 120rpx; + height: 120rpx; + z-index: 1; + } + + .leixing-badge { + position: absolute; + top: -8rpx; + right: -12rpx; + min-width: 32rpx; + height: 32rpx; + padding: 0 8rpx; + box-sizing: border-box; + border-radius: 16rpx; + background: #fa3534; + color: #fff; + font-size: 20rpx; + line-height: 32rpx; + text-align: center; + font-weight: 600; + z-index: 3; + } .leixing-active .leixing-tupian { box-shadow: 0 16rpx 40rpx rgba(0, 200, 255, 0.5); diff --git a/pages/fighter-orders/fighter-orders.js b/pages/fighter-orders/fighter-orders.js index fba3e26..528e76b 100644 --- a/pages/fighter-orders/fighter-orders.js +++ b/pages/fighter-orders/fighter-orders.js @@ -2,8 +2,16 @@ const app = getApp(); import { createPage, request } from '../../utils/base-page.js'; import { reconnectForRole } from '../../utils/role-tab-bar.js'; import { getOrderStatusText } from '../../utils/api-helper.js'; -import { attachAutoSettleFields } from '../../utils/auto-settle-display.js'; +import { + attachAutoSettleFields, + buildAutoSettleDisplay, + startAutoSettleTicker, + stopAutoSettleTicker, +} from '../../utils/auto-settle-display.js'; +import { sanitizeMyDealStat, DEAL_STAT_HELP } from '../../utils/deal-stat-display.js'; +import { consumeBadgeFields } from '../../utils/dashou-badges.js'; import { isFighterOnly, canEnterAcceptOrder, getLoginPageUrl } from '../../utils/fighter-only.js'; +import { ensurePhoneAuth } from '../../utils/phone-auth.js'; Page(createPage({ data: { @@ -50,6 +58,8 @@ Page(createPage({ defaultImg: '/images/default-order.png', ossImageUrl: app.globalData.ossImageUrl || '', dealStat: null, + dealStatChips: [], + showDealStatHelp: false, }, onLoad() { @@ -69,11 +79,14 @@ Page(createPage({ this.registerNotificationComponent(); }, - onShow() { + async onShow() { if (isFighterOnly() && !canEnterAcceptOrder()) { wx.reLaunch({ url: getLoginPageUrl() }); return; } + const phoneOk = await ensurePhoneAuth({ redirect: true, role: 'dashou' }); + if (!phoneOk) return; + this.registerNotificationComponent(); this.loadDealStat(); const app = getApp(); @@ -178,13 +191,26 @@ Page(createPage({ header: { 'content-type': 'application/json' }, }); if (res.data && (res.data.code === 0 || res.data.code === 200)) { - this.setData({ dealStat: res.data.data || null }); + const cleaned = sanitizeMyDealStat(res.data.data, 'dashou'); + this.setData({ + dealStat: cleaned, + dealStatChips: (cleaned && cleaned.chips) || [], + }); } } catch (e) { /* 无样本静默 */ } }, + onDealStatHelpTap() { + wx.showModal({ + title: '指标说明', + content: DEAL_STAT_HELP.dashou, + showCancel: false, + confirmText: '知道了', + }); + }, + switchStatus(e) { const key = e.currentTarget.dataset.key; if (key === this.data.currentStatusKey) return; @@ -241,8 +267,10 @@ Page(createPage({ const code = res.data.code; if (code === 200 || code === 0) { - const newList = res.data.data.list || []; - const hasMore = res.data.data.has_more || false; + const payload = res.data.data || {}; + consumeBadgeFields(payload); + const newList = payload.list || []; + const hasMore = payload.has_more || false; // ✅ 图片URL拼接(核心) const processed = newList.map(item => attachAutoSettleFields({ @@ -297,6 +325,53 @@ Page(createPage({ hasMore: tabData.hasMore, isLoading: tabData.isLoading }); + this.syncAutoSettleListTicker(); + }, + + syncAutoSettleListTicker() { + const key = this.data.currentStatusKey; + const has = (this.data.currentList || []).some((it) => it && it.auto_settle_eligible); + if (!has) { + stopAutoSettleTicker(this); + return; + } + startAutoSettleTicker(this, { + onTick: () => { + const k = this.data.currentStatusKey; + const list = this.data.currentList || []; + const now = Date.now(); + let changed = false; + const next = list.map((it) => { + if (!it || !it.auto_settle_eligible) return it; + const disp = buildAutoSettleDisplay(it, now); + if (!disp) { + if (it.autoSettleClock || it.autoSettleTip) { + changed = true; + return { ...it, autoSettleTip: '', autoSettleClock: '', autoSettleRemainText: '' }; + } + return it; + } + if (it.autoSettleClock !== disp.clockText || it.autoSettleTip !== disp.tip) { + changed = true; + return { + ...it, + autoSettleTip: disp.tip, + autoSettleExpireAt: disp.expireAt, + autoSettleRemainText: disp.remainText, + autoSettleClock: disp.clockText, + autoSettleRemainSeconds: disp.remainSeconds, + }; + } + return it; + }); + if (changed) { + this.setData({ + currentList: next, + [`dsDingdanShuju.${k}.list`]: next, + }); + } + }, + }); }, resetCurrentStatusData() { @@ -355,5 +430,13 @@ Page(createPage({ } }, - onImageError() {} + onImageError() {}, + + onHide() { + stopAutoSettleTicker(this); + }, + + onUnload() { + stopAutoSettleTicker(this); + }, })) diff --git a/pages/fighter/fighter.js b/pages/fighter/fighter.js index 5b5f7fb..0e06f92 100644 --- a/pages/fighter/fighter.js +++ b/pages/fighter/fighter.js @@ -19,6 +19,7 @@ import { import { getDefaultAvatarUrl } from '../../utils/avatar.js' import { ensureRoleAgreement } from '../../utils/role-agreement-gate.js' import { isFighterOnly, FIGHTER_ONLY_DEFAULT_INVITE, FIGHTER_HOME, shouldSkipAutoAuth, clearSkipAutoAuth, isShangjiaCertified, LOGIN_HINT_KEY, clearAuthSession, kickToLoginPage } from '../../utils/fighter-only.js' +import { consumeBadgeFields } from '../../utils/dashou-badges.js' const PLATFORM_INVITE_CODE = FIGHTER_ONLY_DEFAULT_INVITE || '0000008VfJKPu4Spmcjt' const app = getApp() @@ -603,6 +604,7 @@ Page(createPage({ const res = await request({ url: '/yonghu/dashouxinxi', method: 'POST' }); if (res && res.data.code == 200) { const data = res.data.data; + consumeBadgeFields(data); const app = getApp(); const updateData = { dashouNicheng: data.dashounicheng || '', @@ -1334,6 +1336,7 @@ Page(createPage({ const res = await request({ url: '/yonghu/dashouxinxi', method: 'POST' }); if (res && res.data.code == 200) { const data = res.data.data; + consumeBadgeFields(data); const app = getApp(); const updateData = { dashouNicheng: data.dashounicheng || '', diff --git a/pages/team-hall/team-hall.js b/pages/team-hall/team-hall.js index fcb2f9b..5384849 100644 --- a/pages/team-hall/team-hall.js +++ b/pages/team-hall/team-hall.js @@ -2,7 +2,11 @@ const app = getApp(); import request from '../../utils/request.js'; import { attachAutoSettleFields } from '../../utils/auto-settle-display.js'; +import { buildDealStatChips } from '../../utils/deal-stat-display.js'; import { isFighterOnly, canEnterAcceptOrder, getLoginPageUrl } from '../../utils/fighter-only.js'; +import { ensurePhoneAuth } from '../../utils/phone-auth.js'; +import { getDefaultAvatarUrl } from '../../utils/avatar.js'; +import { consumeBadgeFields, mergeTypePendingCounts } from '../../utils/dashou-badges.js'; Page({ data: { @@ -23,6 +27,7 @@ Page({ joinGameId: '', joining: false, ossImageUrl: '', + defaultAvatarUrl: '/images/default-avatar.png', lastSwitchTime: 0, switchCooldown: 800, }, @@ -32,15 +37,21 @@ Page({ wx.reLaunch({ url: getLoginPageUrl() }); return; } - this.setData({ ossImageUrl: app.globalData.ossImageUrl || '' }); + this.setData({ + ossImageUrl: app.globalData.ossImageUrl || '', + defaultAvatarUrl: getDefaultAvatarUrl(app), + }); this.loadShangpinLeixing(true); }, - onShow() { + async onShow() { if (isFighterOnly() && !canEnterAcceptOrder()) { wx.reLaunch({ url: getLoginPageUrl() }); return; } + const phoneOk = await ensurePhoneAuth({ redirect: true, role: 'dashou' }); + if (!phoneOk) return; + if (typeof this.getTabBar === 'function' && this.getTabBar()) { this.getTabBar().refresh && this.getTabBar().refresh(); } @@ -77,7 +88,8 @@ Page({ else if (data.data && Array.isArray(data.data)) list = data.data; else if (Array.isArray(data.list)) list = data.list; } - const processed = this.processTupianUrl(list); + const processed = mergeTypePendingCounts(this.processTupianUrl(list), (res.data && res.data.data && res.data.data.type_pending_counts) || {}); + consumeBadgeFields((res && res.data && res.data.data) || {}); const firstId = processed[0] ? processed[0].id : null; this.setData({ shangpinleixing: processed, @@ -160,17 +172,41 @@ Page({ : Math.max(0, (item.need_count || 0) - (item.joined_count || 0)); const isPingtai = Number(item.fadanpingtai) === 1; const isShangjia = Number(item.fadanpingtai) === 2; + const defaultAv = this.data.defaultAvatarUrl || '/images/default-avatar.png'; + const fullTupian = this._fullImg(item.tupian) || '/images/default-type.png'; + const reqParts = []; + if (item.min_deal_rate != null && item.min_deal_rate !== '') { + reqParts.push(`成交≥${(Number(item.min_deal_rate) * 100).toFixed(0)}%`); + } + if (item.max_fine_rate != null && item.max_fine_rate !== '') { + reqParts.push(`罚款≤${(Number(item.max_fine_rate) * 100).toFixed(0)}%`); + } + const leaderChips = buildDealStatChips(item.leader_deal_stat, { + dealLabel: '成交率', + fineLabel: '被罚款率', + }); + const merchantChips = isShangjia + ? buildDealStatChips(item.shangjia_deal_stat, { + dealLabel: '商家成交率', + fineLabel: '商家罚款率', + }) + : []; return { ...item, isPingtai, isShangjia, shareText: share, - remainText: `还差 ${remain} / 要 ${item.need_count || 0}`, + remainText: `还差${remain}/${item.need_count || 0}人`, platformText: item.fadanpingtai_text || (isPingtai ? '平台单' : isShangjia ? '商家单' : ''), - fullTupian: this._fullImg(item.tupian), - leaderStatText: item.leader_deal_stat - ? `队长 结算率 ${item.leader_deal_stat.deal_rate_text || '-'} · 罚款率 ${item.leader_deal_stat.fined_rate_text || '-'}` - : '', + creat_time: item.opened_at || '组队招募中', + full_tupian_url: fullTupian, + beizhu: item.remark || '', + leader_avatar_full: this._fullImg(item.leader_avatar) || defaultAv, + leader_deal_stat: item.leader_deal_stat || null, + shangjia_deal_stat: item.shangjia_deal_stat || null, + leaderStatChips: leaderChips, + merchantStatChips: merchantChips, + requireStatText: reqParts.join(' · '), autoSettleTip: item.autoSettleTip || '', openedText: item.opened_at || '', }; @@ -194,6 +230,7 @@ Page({ }); if (res && res.data && res.data.code === 0) { const data = res.data.data || {}; + consumeBadgeFields(data); const rows = (data.list || []).map((r) => this._mapHallItem(r)); this.setData({ list: reset ? rows : this.data.list.concat(rows), diff --git a/tab-bar/index.js b/tab-bar/index.js index 0efdc0f..da73b1d 100644 --- a/tab-bar/index.js +++ b/tab-bar/index.js @@ -48,6 +48,8 @@ Component({ tabList: [], currentRole: 'normal', badgeText: '', + teamBadgeText: '', + themeGreen: false, }, lifetimes: { @@ -59,12 +61,19 @@ Component({ this.setData({ badgeText: data.badgeText }); } }; + this._teamBadgeListener = (data) => { + if (data.teamBadgeText !== undefined) { + this.setData({ teamBadgeText: data.teamBadgeText }); + } + }; this._roleListener = () => this.refresh(); app.on('tabBarBadgeChanged', this._badgeListener); + app.on('tabBarTeamBadgeChanged', this._teamBadgeListener); app.on('currentRoleChanged', this._roleListener); }, detached() { if (this._badgeListener) app.off('tabBarBadgeChanged', this._badgeListener); + if (this._teamBadgeListener) app.off('tabBarTeamBadgeChanged', this._teamBadgeListener); if (this._roleListener) app.off('currentRoleChanged', this._roleListener); }, }, @@ -98,6 +107,15 @@ Component({ if (badgeText && badgeText !== this.data.badgeText) { this.setData({ badgeText }); } + let teamBadgeText = (app.globalData && app.globalData.teamTabBadgeText) || ''; + if (!teamBadgeText) { + try { + teamBadgeText = wx.getStorageSync('tabBarTeamBadge') || ''; + } catch (e) {} + } + if (teamBadgeText !== this.data.teamBadgeText) { + this.setData({ teamBadgeText }); + } if (app.restoreTabBarBadge) app.restoreTabBarBadge(); }, @@ -139,7 +157,9 @@ Component({ const curRoute = pages.length ? pages[pages.length - 1].route : ''; let idx = list.findIndex((t) => t.pagePath === curRoute); if (idx === -1) idx = 0; - this.setData({ currentRole: role, tabList: list, selectedIndex: idx }); + // 组队大厅:底栏跟页内绿系衔接,减轻切换突兀感 + const themeGreen = curRoute === 'pages/team-hall/team-hall'; + this.setData({ currentRole: role, tabList: list, selectedIndex: idx, themeGreen }); }, switchTab(e) { @@ -150,9 +170,23 @@ Component({ // tabBar 注册页用 switchTab;其余页用 reLaunch(redirectTo 对 tab 页会失败) if (TAB_BAR_NATIVE_PAGES.has(path)) { wx.switchTab({ url }); - } else { - wx.reLaunch({ url }); + return; } + // 商家端 Tab 互切:redirectTo 比 reLaunch 轻,减少整页重载卡顿/入驻闪一下 + const merchantPages = ( + path === 'pages/merchant-home/merchant-home' + || path === 'pages/merchant-orders/merchant-orders' + || path === 'pages/merchant/merchant' + || path === 'pages/messages/messages' + ); + if (this.data.currentRole === 'shangjia' && merchantPages) { + wx.redirectTo({ + url, + fail: () => wx.reLaunch({ url }), + }); + return; + } + wx.reLaunch({ url }); }, }, }); diff --git a/tab-bar/index.wxml b/tab-bar/index.wxml index ef4cbe2..2ce24da 100644 --- a/tab-bar/index.wxml +++ b/tab-bar/index.wxml @@ -1,5 +1,5 @@ - + {{badgeText}} + + {{teamBadgeText}} + - {{item.text}} + {{item.text}} diff --git a/utils/dashou-badges.js b/utils/dashou-badges.js new file mode 100644 index 0000000..0453fc9 --- /dev/null +++ b/utils/dashou-badges.js @@ -0,0 +1,83 @@ +/** + * 打手端角标:抢单池类型 pending_count + 组队 Tab team_pending_count + * 仅消费接口新增字段,不影响旧逻辑。 + */ +const app = getApp(); + +function fmtBadge(n) { + const num = Number(n) || 0; + if (num <= 0) return ''; + return num > 99 ? '99+' : String(num); +} + +export function applyTeamTabBadge(count) { + const text = fmtBadge(count); + try { + if (app && app.globalData) { + app.globalData.teamTabBadgeText = text; + app.globalData.teamPendingCount = Number(count) || 0; + } + } catch (e) {} + try { + wx.setStorageSync('tabBarTeamBadge', text); + } catch (e) {} + if (app && typeof app.emitEvent === 'function') { + app.emitEvent('tabBarTeamBadgeChanged', { teamBadgeText: text }); + } + try { + const pages = getCurrentPages(); + const page = pages && pages[pages.length - 1]; + const tabBar = page && typeof page.getTabBar === 'function' ? page.getTabBar() : null; + if (tabBar && tabBar.setData) { + tabBar.setData({ teamBadgeText: text }); + } + } catch (e) {} +} + +/** 从任意接口 data 吃角标字段并刷新组队 Tab */ +export function consumeBadgeFields(data) { + if (!data || typeof data !== 'object') return null; + if (data.team_pending_count != null) { + applyTeamTabBadge(data.team_pending_count); + } + return { + typePendingCounts: data.type_pending_counts || null, + poolPendingTotal: data.pool_pending_total, + teamPendingCount: data.team_pending_count, + }; +} + +/** 把 type_pending_counts 合并进商品类型列表 */ +export function mergeTypePendingCounts(list, counts) { + if (!Array.isArray(list)) return list || []; + const map = counts && typeof counts === 'object' ? counts : {}; + return list.map((item) => { + const id = item && item.id; + const key = id != null ? String(id) : ''; + const fromItem = item && item.pending_count != null ? Number(item.pending_count) : null; + const fromMap = key && map[key] != null ? Number(map[key]) : (map[id] != null ? Number(map[id]) : null); + const pending = fromItem != null && !Number.isNaN(fromItem) + ? fromItem + : (fromMap != null && !Number.isNaN(fromMap) ? fromMap : 0); + return { + ...item, + pending_count: pending, + pending_badge: fmtBadge(pending), + }; + }); +} + +export function restoreTeamTabBadge() { + let text = ''; + try { + if (app && app.globalData && app.globalData.teamTabBadgeText) { + text = app.globalData.teamTabBadgeText; + } + } catch (e) {} + if (!text) { + try { + text = wx.getStorageSync('tabBarTeamBadge') || ''; + } catch (e) {} + } + if (text) applyTeamTabBadge(text === '99+' ? 99 : Number(text) || 0); +} diff --git a/utils/dashou-profile.js b/utils/dashou-profile.js index 7927ea5..c482fe2 100644 --- a/utils/dashou-profile.js +++ b/utils/dashou-profile.js @@ -2,6 +2,7 @@ * 从服务端刷新打手会员/押金/积分等到 globalData(抢单、考试等页共用) */ import request from './request.js'; +import { consumeBadgeFields } from './dashou-badges.js'; export async function refreshDashouMembership(app) { app = app || getApp(); @@ -9,6 +10,7 @@ export async function refreshDashouMembership(app) { const res = await request({ url: '/yonghu/dashouxinxi', method: 'POST' }); if (res && res.data && res.data.code == 200) { const data = res.data.data || {}; + consumeBadgeFields(data); if (data.zhanghaostatus !== undefined && Number(data.zhanghaostatus) !== 1) { try { const { kickToLoginPage } = require('./fighter-only.js');