From 25ebd667f1f63617792cc1e3c004d36cceb45649 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Sat, 5 Sep 2026 16:52:46 +0800 Subject: [PATCH] =?UTF-8?q?task-98(=E5=BA=97=E9=93=BA=E4=B8=AD=E5=BF=83):?= =?UTF-8?q?=20=E5=AE=9E=E7=8E=B0=E5=BA=97=E9=93=BA=E4=B8=AD=E5=BF=83?= =?UTF-8?q?=E9=80=9A=E7=94=A8=E7=AD=9B=E9=80=89=E6=A0=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 shop-filter-bar-state.ts:筛选草稿→已应用快照与重置语义,筛选变更标记 回到第 1 页重新加载;纯逻辑可测。 TDD: task-98.test.ts 8 用例先 RED 后 GREEN。 --- .../src/pages/shop/shop-filter-bar-state.ts | 48 +++++++++++++ admin-frontend-vue/tests/task-98.test.ts | 72 +++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 admin-frontend-vue/src/pages/shop/shop-filter-bar-state.ts create mode 100644 admin-frontend-vue/tests/task-98.test.ts diff --git a/admin-frontend-vue/src/pages/shop/shop-filter-bar-state.ts b/admin-frontend-vue/src/pages/shop/shop-filter-bar-state.ts new file mode 100644 index 00000000..bda20e05 --- /dev/null +++ b/admin-frontend-vue/src/pages/shop/shop-filter-bar-state.ts @@ -0,0 +1,48 @@ +/** 店铺中心通用筛选栏状态(任务 98):筛选草稿/已应用快照与“变更回首页”语义,纯逻辑。 */ +import type { ShopManageFilter } from './shop-manage-list-state.ts' + +export interface ShopFilterBarState { + /** 已应用(触发过加载)的筛选快照;null 表示尚未应用任何筛选。 */ + applied: ShopManageFilter | null + /** 本次操作是否要求回到第 1 页重新加载。 */ + returnFirstPage: boolean +} + +export function createShopFilterBarState(): ShopFilterBarState { + return { applied: null, returnFirstPage: false } +} + +function positiveGroupOrNull(value: string | number | null | undefined): number | null { + if (typeof value === 'number') return Number.isFinite(value) && value >= 1 ? Math.floor(value) : null + if (typeof value === 'string' && /^\d+$/.test(value.trim())) { + const parsed = Number(value.trim()) + return parsed >= 1 ? Math.floor(parsed) : null + } + return null +} + +/** 由界面输入(分组下拉文本/店铺名输入)生成筛选草稿。 */ +export function shopFilterDraftFromInput(input: { groupId?: string | number | null; shopName?: string }): ShopManageFilter { + const shopName = typeof input.shopName === 'string' ? input.shopName.trim() : '' + return { groupId: positiveGroupOrNull(input.groupId), shopName } +} + +function cloneFilter(filter: ShopManageFilter): ShopManageFilter { + return { groupId: filter.groupId, shopName: typeof filter.shopName === 'string' ? filter.shopName.trim() : '' } +} + +/** 提交筛选草稿:固化为已应用并标记回到第 1 页。 */ +export function commitShopFilterDraft(_state: ShopFilterBarState, draft: ShopManageFilter): ShopFilterBarState { + return { applied: cloneFilter(draft), returnFirstPage: true } +} + +/** 重置筛选:清空已应用条件并标记回到第 1 页。 */ +export function clearShopFilterState(_state: ShopFilterBarState): ShopFilterBarState { + return { applied: null, returnFirstPage: true } +} + +/** 草稿是否与已应用筛选一致(无差异则无需重新加载)。 */ +export function isShopFilterDraftApplied(draft: ShopManageFilter, applied: ShopManageFilter | null): boolean { + if (!applied) return false + return cloneFilter(draft).groupId === applied.groupId && cloneFilter(draft).shopName === applied.shopName +} diff --git a/admin-frontend-vue/tests/task-98.test.ts b/admin-frontend-vue/tests/task-98.test.ts new file mode 100644 index 00000000..f6a6f3d8 --- /dev/null +++ b/admin-frontend-vue/tests/task-98.test.ts @@ -0,0 +1,72 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readSource } from './helpers.ts' +import { + clearShopFilterState, + commitShopFilterDraft, + createShopFilterBarState, + isShopFilterDraftApplied, + shopFilterDraftFromInput, +} from '../src/pages/shop/shop-filter-bar-state.ts' + +test('test_task_098_shop_filter_bar_normal_primary_path', () => { + // 正常主路径:草稿输入提交后成为已应用筛选并触发回到首页加载。 + const state = commitShopFilterDraft( + createShopFilterBarState(), + shopFilterDraftFromInput({ groupId: '3', shopName: ' 主店 ' }), + ) + assert.equal(state.applied!.groupId, 3) + assert.equal(state.applied!.shopName, '主店') + assert.equal(state.returnFirstPage, true, '筛选变更应回到第 1 页') +}) + +test('test_task_098_shop_filter_bar_normal_variant_input', () => { + // 正常变体:重置筛选后无已应用条件且触发首页加载。 + const base = createShopFilterBarState() + const cleared = clearShopFilterState(base) + assert.equal(cleared.applied, null) + assert.equal(cleared.returnFirstPage, true) +}) + +test('test_task_098_shop_filter_bar_repeated_is_idempotent', () => { + // 正常重复:提交同一草稿结果稳定、不改草稿入参。 + const draft = shopFilterDraftFromInput({ groupId: '2', shopName: '店' }) + const a = commitShopFilterDraft(createShopFilterBarState(), draft) + const b = commitShopFilterDraft(createShopFilterBarState(), draft) + assert.deepEqual(a.applied, b.applied) + assert.equal((draft as { shopName: string }).shopName, '店') +}) + +test('test_task_098_shop_filter_bar_boundary_empty_input', () => { + // 边界空值:无筛选条件提交后应用为空且不再标记变更。 + const applied = commitShopFilterDraft(createShopFilterBarState(), { groupId: null, shopName: '' }).applied + assert.deepEqual(applied, { groupId: null, shopName: '' }) + assert.equal(isShopFilterDraftApplied({ groupId: null, shopName: '' }, applied), true) +}) + +test('test_task_098_shop_filter_bar_boundary_single_item', () => { + // 边界单元素:仅分组筛选提交应用。 + const state = commitShopFilterDraft(createShopFilterBarState(), shopFilterDraftFromInput({ groupId: '9', shopName: '' })) + assert.equal(state.applied!.groupId, 9) + assert.equal(isShopFilterDraftApplied({ groupId: 9, shopName: '' }, state.applied), true) +}) + +test('test_task_098_shop_filter_bar_boundary_limit_or_missing_field', () => { + // 边界上限/缺字段:非法/空分组输入归一为无分组筛选。 + const draft = shopFilterDraftFromInput({ groupId: 'abc', shopName: ' ' }) + assert.equal(draft.groupId, null) + assert.equal(draft.shopName, '') +}) + +test('test_task_098_shop_filter_bar_invalid_input_rejected', () => { + // 异常输入:已应用筛选与草稿不一致时视为有变更(需重新加载)。 + const state = commitShopFilterDraft(createShopFilterBarState(), { groupId: 1, shopName: 'A店' }) + assert.equal(isShopFilterDraftApplied({ groupId: 1, shopName: 'B店' }, state.applied), false) +}) + +test('test_task_098_shop_filter_bar_dependency_failure_returns_actionable_message', () => { + // 依赖失败/可操作:筛选栏状态纯逻辑、无框架/http。 + const mod = readSource('src/pages/shop/shop-filter-bar-state.ts') + assert.equal(/axios|http\.|vue/.test(mod), false, '筛选栏状态保持纯逻辑') + assert.match(mod, /returnFirstPage/) +})