From e0d7398d37be1d1693b5f94aef279731a20e3f7c Mon Sep 17 00:00:00 2001 From: yml2213 Date: Fri, 2 Oct 2026 17:13:59 +0800 Subject: [PATCH] =?UTF-8?q?fix(admin):=20=E9=A3=8E=E6=8E=A7=20tab=20?= =?UTF-8?q?=E7=82=B9=E4=BA=86=E6=B2=A1=E5=8F=8D=E5=BA=94=20=E2=80=94?= =?UTF-8?q?=E2=80=94=20tab=20key=20=E7=99=BD=E5=90=8D=E5=8D=95=E6=BC=8F?= =?UTF-8?q?=E7=99=BB=E8=AE=B0=EF=BC=8C=E5=B9=B6=E5=90=88=E5=B9=B6=E4=B8=BA?= =?UTF-8?q?=E5=94=AF=E4=B8=80=E6=9D=A5=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 现象与根因 风控 tab 渲染得出来、但怎么点都切不过去,且不报错、不白屏。 原因是 tab key 名单在同一文件里写了两遍: - isWorkerPlatformTab()(changeTab 的守卫) - loadActiveTabPreference()(localStorage 旧选择校验) 新增风控 tab 时只把 'risk-control' 加了 TAB_QUERY_KEYS 与 Tabs items,漏了上面两处, 于是 changeTab('risk-control') 在第一行 `if (!isWorkerPlatformTab(nextTab)) return` 就返回了。 这类漏改没有任何报错信号,只能靠点一下才发现——上一轮的验证只查了构建产物里 是否含面板代码,没有验证 tab 真的能切换,所以没拦住。 ## 修法 把名单抽到 utils/admin-worker-platform-tabs.ts,只保留唯一一份 WORKER_PLATFORM_TAB_KEYS;查询缓存映射与「只登记缓存但不是 tab」的例外(hall-config, 它是「杂项配置」tab 内部的区块)也一并显式化,避免下次再靠人记。 ## 回归测试 新增 tests/admin-worker-platform-tabs.test.ts(5 个用例): - 风控 tab 必须被 isWorkerPlatformTab 接受 - 每个登记了切页缓存的 tab 都必须在合法 tab 名单里 ← 正是本次漏改的形态 - 名单无重复项 / 非法 key 被拒 / 名单内自洽 --- .../pages/admin/AdminWorkerPlatformPage.tsx | 65 ++--------------- .../src/utils/admin-worker-platform-tabs.ts | 70 +++++++++++++++++++ .../tests/admin-worker-platform-tabs.test.ts | 46 ++++++++++++ 3 files changed, 121 insertions(+), 60 deletions(-) create mode 100644 apps/frontend/src/utils/admin-worker-platform-tabs.ts create mode 100644 apps/frontend/tests/admin-worker-platform-tabs.test.ts diff --git a/apps/frontend/src/pages/admin/AdminWorkerPlatformPage.tsx b/apps/frontend/src/pages/admin/AdminWorkerPlatformPage.tsx index b70bc412..e4cdf293 100644 --- a/apps/frontend/src/pages/admin/AdminWorkerPlatformPage.tsx +++ b/apps/frontend/src/pages/admin/AdminWorkerPlatformPage.tsx @@ -7,6 +7,10 @@ import PageHeader from '@/components/admin/PageHeader' import type { WorkOrder } from '@/types/worker-platform' import { fetchAdminWorkerPlatformSummary } from '@/services/admin' import { getAdminRole, hasAdminPermission } from '@/utils/admin-auth' +import { + WORKER_PLATFORM_TAB_QUERY_KEYS as TAB_QUERY_KEYS, + isWorkerPlatformTab, +} from '@/utils/admin-worker-platform-tabs' import AfterSalesPanel, { type AfterSalesPanelHandle } from './panels/AfterSalesPanel' import AnnouncementPanel from './panels/AnnouncementPanel' import CancelRequestsPanel from './panels/CancelRequestsPanel' @@ -28,29 +32,6 @@ type AfterSalesLocationState = { createOrder?: WorkOrder } -const TAB_QUERY_KEYS: Record = { - 'consignment-inventory': [['admin-consignment-inventories'], ['admin-consignment-stats']], - 'consignment-submissions': [ - ['admin-consignment-submissions'], - ['admin-consignment-submissions-pending-total'], - ], - orders: [['admin-work-orders'], ['admin-work-orders-stats']], - 'cancel-requests': [['admin-work-order-cancel-requests']], - 'after-sales': [['admin-after-sales-orders'], ['admin-after-sales-stats']], - feedbacks: [['admin-feedbacks']], - rules: [['admin-product-rules']], - 'product-match': [['admin-product-match']], - categories: [['admin-categories']], - workers: [['admin-workers'], ['admin-worker-users']], - leaderboard: [['admin-leaderboard']], - finance: [['admin-finance-settlements'], ['admin-finance-withdrawals']], - levels: [['admin-worker-levels']], - notifications: [['admin-notifications']], - announcement: [['admin-announcements']], - 'hall-config': [['admin-hall-config']], - 'risk-control': [['admin-worker-risk-overview'], ['admin-worker-risk-events']], -} - export default function AdminWorkerPlatformPage() { const queryClient = useQueryClient() const [searchParams, setSearchParams] = useSearchParams() @@ -207,49 +188,13 @@ const ACTIVE_TAB_STORAGE_KEY = 'admin-worker-platform-tab' function loadActiveTabPreference(): string { const saved = localStorage.getItem(ACTIVE_TAB_STORAGE_KEY) - return saved === 'orders' || - saved === 'consignment-inventory' || - saved === 'consignment-submissions' || - saved === 'leaderboard' || - saved === 'cancel-requests' || - saved === 'feedbacks' || - saved === 'after-sales' || - saved === 'announcement' || - saved === 'rules' || - saved === 'product-match' || - saved === 'categories' || - saved === 'workers' || - saved === 'finance' || - saved === 'levels' || - saved === 'notifications' - ? saved - : 'orders' + return isWorkerPlatformTab(saved) ? saved : 'orders' } function loadActiveTab(tab: string | null): string { return isWorkerPlatformTab(tab) ? tab : loadActiveTabPreference() } -function isWorkerPlatformTab(tab: string | null): tab is string { - return [ - 'orders', - 'consignment-inventory', - 'consignment-submissions', - 'leaderboard', - 'cancel-requests', - 'feedbacks', - 'after-sales', - 'announcement', - 'rules', - 'product-match', - 'categories', - 'workers', - 'finance', - 'levels', - 'notifications', - ].includes(String(tab || '')) -} - function SummaryCards() { const summaryQuery = useQuery({ queryKey: ['admin-worker-platform-summary'], diff --git a/apps/frontend/src/utils/admin-worker-platform-tabs.ts b/apps/frontend/src/utils/admin-worker-platform-tabs.ts new file mode 100644 index 00000000..544bd0f4 --- /dev/null +++ b/apps/frontend/src/utils/admin-worker-platform-tabs.ts @@ -0,0 +1,70 @@ +/** + * 接单平台(AdminWorkerPlatformPage)的 tab key 名单与切页缓存刷新映射。 + * + * 抽成独立模块的原因:这些名单原先散落在页面组件里,且**同一份名单写了两遍** + * (isWorkerPlatformTab 与 loadActiveTabPreference 各一份)。新增 tab 时漏改的后果 + * 不是报错、也不是白屏,而是**点了没反应**——changeTab() 会在 isWorkerPlatformTab() + * 返回 false 时直接 return,tab 渲染得出来却永远切不过去。 + * + * 因此这里只保留唯一一份 WORKER_PLATFORM_TAB_KEYS,并配套单测守住 + * 「登记了查询缓存却没登记合法 tab」这类漏改。 + */ + +/** 所有可作为 tab 的 key(新增 tab 只改这里)。 */ +export const WORKER_PLATFORM_TAB_KEYS: readonly string[] = [ + 'orders', + 'consignment-inventory', + 'consignment-submissions', + 'leaderboard', + 'cancel-requests', + 'feedbacks', + 'after-sales', + 'announcement', + 'rules', + 'product-match', + 'categories', + 'workers', + 'finance', + 'levels', + 'notifications', + 'risk-control', +] + +/** + * 切到某个 tab 时需要失效的 react-query key,用于「切入即为最新状态」。 + * + * 注意:这里的 key 未必都是 tab——例如 `hall-config` 是「杂项配置」tab 内部的一个区块。 + * 未登记的 tab 切换时不额外失效任何查询(仍会刷新概览)。 + */ +export const WORKER_PLATFORM_TAB_QUERY_KEYS: Record = { + 'consignment-inventory': [['admin-consignment-inventories'], ['admin-consignment-stats']], + 'consignment-submissions': [ + ['admin-consignment-submissions'], + ['admin-consignment-submissions-pending-total'], + ], + orders: [['admin-work-orders'], ['admin-work-orders-stats']], + 'cancel-requests': [['admin-work-order-cancel-requests']], + 'after-sales': [['admin-after-sales-orders'], ['admin-after-sales-stats']], + feedbacks: [['admin-feedbacks']], + rules: [['admin-product-rules']], + 'product-match': [['admin-product-match']], + categories: [['admin-categories']], + workers: [['admin-workers'], ['admin-worker-users']], + leaderboard: [['admin-leaderboard']], + finance: [['admin-finance-settlements'], ['admin-finance-withdrawals']], + levels: [['admin-worker-levels']], + notifications: [['admin-notifications']], + announcement: [['admin-announcements']], + 'hall-config': [['admin-hall-config']], + 'risk-control': [['admin-worker-risk-overview'], ['admin-worker-risk-events']], +} + +/** + * 只登记了查询缓存、但本身不是 tab 的 key。 + * 目前只有 hall-config:它是「杂项配置」tab 内部的一个区块。 + */ +export const WORKER_PLATFORM_NON_TAB_QUERY_KEYS: readonly string[] = ['hall-config'] + +export function isWorkerPlatformTab(tab: string | null): tab is string { + return WORKER_PLATFORM_TAB_KEYS.includes(String(tab || '')) +} diff --git a/apps/frontend/tests/admin-worker-platform-tabs.test.ts b/apps/frontend/tests/admin-worker-platform-tabs.test.ts new file mode 100644 index 00000000..38739609 --- /dev/null +++ b/apps/frontend/tests/admin-worker-platform-tabs.test.ts @@ -0,0 +1,46 @@ +import assert from 'node:assert/strict' +import test from 'node:test' + +import { + WORKER_PLATFORM_NON_TAB_QUERY_KEYS, + WORKER_PLATFORM_TAB_KEYS, + WORKER_PLATFORM_TAB_QUERY_KEYS, + isWorkerPlatformTab, +} from '../src/utils/admin-worker-platform-tabs.ts' + +/** + * 这组用例守住一个「不报错、不白屏,只是点了没反应」的坑: + * changeTab() 在 isWorkerPlatformTab() 返回 false 时直接 return, + * 于是 tab 渲染得出来却永远切不过去。生产上风控 tab 就这样失效过一次。 + */ + +test('风控 tab 必须被 isWorkerPlatformTab 接受(漏登记会导致点击无反应)', () => { + assert.equal(isWorkerPlatformTab('risk-control'), true) +}) + +test('每个登记了切页缓存的 tab 都必须在合法 tab 名单里', () => { + const nonTabs = new Set(WORKER_PLATFORM_NON_TAB_QUERY_KEYS) + for (const key of Object.keys(WORKER_PLATFORM_TAB_QUERY_KEYS)) { + if (nonTabs.has(key)) continue + assert.ok( + isWorkerPlatformTab(key), + `"${key}" 登记了 WORKER_PLATFORM_TAB_QUERY_KEYS 却不在 WORKER_PLATFORM_TAB_KEYS 中 —— changeTab() 会直接 return,表现为点了没反应`, + ) + } +}) + +test('合法 tab 名单无重复项', () => { + assert.equal(new Set(WORKER_PLATFORM_TAB_KEYS).size, WORKER_PLATFORM_TAB_KEYS.length) +}) + +test('非法 tab key 一律被拒(含 null 与空串)', () => { + for (const bad of ['', null, 'nope', 'riskcontrol', 'risk_control', 'hall-config']) { + assert.equal(isWorkerPlatformTab(bad), false, `${String(bad)} 不应被当作合法 tab`) + } +}) + +test('名单里的每个 key 都能被自身校验通过', () => { + for (const key of WORKER_PLATFORM_TAB_KEYS) { + assert.equal(isWorkerPlatformTab(key), true, `${key} 应可通过校验`) + } +})