fix(admin): 风控 tab 点了没反应 —— tab key 白名单漏登记,并合并为唯一来源
## 现象与根因
风控 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 被拒 / 名单内自洽
This commit is contained in:
@@ -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<string, string[][]> = {
|
||||
'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'],
|
||||
|
||||
@@ -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<string, string[][]> = {
|
||||
'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 || ''))
|
||||
}
|
||||
@@ -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} 应可通过校验`)
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user