feat: 抢单池类型红角标 + 组队 Tab 待组队角标

This commit is contained in:
XingQue committed 2026-07-27 17:20:24 +08:00
1 parent 684f392cb6
commit cc650762f8
10 files changed
+325 -27

No files matched your search

+23 -6
View File
@@ -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,
};
},
+13 -2
View File
@@ -136,7 +136,10 @@
<view wx:if="{{xuanzhongLeixingId == item.id}}" class="guangyun-effect"></view>
<image class="leixing-tupian" src="{{item.full_tupian_url}}" mode="aspectFill"/>
<view class="leixing-pic-wrap">
<image class="leixing-tupian" src="{{item.full_tupian_url}}" mode="aspectFill"/>
<view class="leixing-badge" wx:if="{{item.pending_badge}}">{{item.pending_badge}}</view>
</view>
<text class="leixing-jieshao">{{item.jieshao}}</text>
@@ -366,6 +369,9 @@
<view class="m-info">
<view class="m-name m-name-light line1">{{item.sjnicheng || '未知商家'}}</view>
<view class="m-sn m-sn-light">发布于 {{item.creat_time}}</view>
<view class="merchant-stat-row" wx:if="{{item.dealStatChips.length}}">
<text class="merchant-stat {{c.hl ? 'highlight' : ''}}" wx:for="{{item.dealStatChips}}" wx:for-item="c" wx:key="k">{{c.t}}</text>
</view>
</view>
</view>
<view class="qiangdan flexa btn-bg btn-qiang" data-item="{{item}}" bindtap="onQiangdanTap">{{item.isMyZhiding ? '立即接待' : '抢单'}}</view>
@@ -411,7 +417,12 @@
<image class="m-avatar-sm" src="{{item.shangjia_avatar_full}}" mode="aspectFill"/>
<text class="m-name-sm">{{item.sjnicheng || '未知商家'}}</text>
<view class="m-meta-col">
<text class="m-name-sm">{{item.sjnicheng || '未知商家'}}</text>
<view class="merchant-stat-row" wx:if="{{item.dealStatChips.length}}">
<text class="merchant-stat {{c.hl ? 'highlight' : ''}}" wx:for="{{item.dealStatChips}}" wx:for-item="c" wx:key="k">{{c.t}}</text>
</view>
</view>
</view>
+25
View File
@@ -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);
+89 -6
View File
@@ -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);
},
}))
+3
View File
@@ -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 || '',
+45 -8
View File
@@ -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),
+37 -3
View File
@@ -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 });
},
},
});
+5 -2
View File
@@ -1,5 +1,5 @@
<view class="c-tab-spacer"></view>
<view class="c-tab-outer">
<view class="c-tab-outer {{themeGreen ? 'c-tab-outer-green' : ''}}">
<view class="c-tab-bar">
<block wx:for="{{tabList}}" wx:key="index">
<view
@@ -11,12 +11,15 @@
<view class="c-tab-dot" wx:if="{{item.pagePath === 'pages/messages/messages' && badgeText}}">
<text class="c-tab-dot-t">{{badgeText}}</text>
</view>
<view class="c-tab-dot" wx:if="{{item.pagePath === 'pages/team-hall/team-hall' && teamBadgeText}}">
<text class="c-tab-dot-t">{{teamBadgeText}}</text>
</view>
<image
class="c-tab-ico"
mode="aspectFit"
src="{{selectedIndex === index ? item.selectedIconPath : item.iconPath}}"
/>
<text class="c-tab-txt {{selectedIndex === index ? 'c-tab-txt-on' : ''}}">{{item.text}}</text>
<text class="c-tab-txt {{selectedIndex === index ? (themeGreen ? 'c-tab-txt-on-green' : 'c-tab-txt-on') : ''}}">{{item.text}}</text>
</view>
</block>
</view>
+83
View File
@@ -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);
}
+2
View File
@@ -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');