diff --git a/admin-frontend-vue/src/pages/shop/shop-manage-list-state.ts b/admin-frontend-vue/src/pages/shop/shop-manage-list-state.ts new file mode 100644 index 00000000..5cfcb55e --- /dev/null +++ b/admin-frontend-vue/src/pages/shop/shop-manage-list-state.ts @@ -0,0 +1,98 @@ +/** 店铺列表筛选与分页(任务 89):店铺 FilterBar 的分组/店铺名筛选与列表查询、分页信息,纯逻辑。 */ +import { + normalizeShopListParams, + SHOP_PAGE_DEFAULT_SIZE, + SHOP_PAGE_MAX_SIZE, + SHOP_PAGE_MIN_PAGE, + toShopManagePageQuery, + type ShopManageListParams, + type ShopManagePageQuery, +} from './shop-dto.ts' + +/** 店铺列表筛选状态(页面局部)。 */ +export interface ShopManageFilter { + groupId: number | null + shopName: string +} + +export function createShopManageFilter(): ShopManageFilter { + return { groupId: null, shopName: '' } +} + +function positiveGroupOrNull(value: unknown): number | null { + if (typeof value === 'number') return Number.isFinite(value) && value >= 1 ? Math.floor(value) : null + if (typeof value === 'string') { + const text = value.trim() + if (/^\d+$/.test(text)) { + const parsed = Number(text) + return parsed >= 1 ? Math.floor(parsed) : null + } + } + return null +} + +function shopNameText(value: unknown): string { + return typeof value === 'string' ? value.trim() : '' +} + +/** 由界面输入(分组下拉文本/店铺名输入)归一筛选状态。 */ +export function shopManageFilterFromInput(input: { groupId?: string | number | null; shopName?: string }): ShopManageFilter { + const shopName = shopNameText(input.shopName) + return { groupId: positiveGroupOrNull(input.groupId), shopName: shopName ? shopName : '' } +} + +export function shopManageFilterActive(filter: ShopManageFilter): boolean { + return filter.groupId !== null || Boolean(filter.shopName && filter.shopName.trim()) +} + +/** 筛选 + 页码 → 归一化列表参数(供 fetch 与查询序列化复用)。 */ +export function toShopManageListParams(page: number, pageSize: number, filter: ShopManageFilter): ShopManageListParams { + return normalizeShopListParams({ + page, + pageSize, + groupId: filter.groupId, + shopName: filter.shopName.trim() || undefined, + }) +} + +/** 筛选 + 页码 → Java 查询参数(camelCase,仅下发非空)。 */ +export function toShopManageQuery(page: number, pageSize: number, filter: ShopManageFilter): ShopManagePageQuery { + return toShopManagePageQuery(toShopManageListParams(page, pageSize, filter)) +} + +export interface ShopManagePageInfo { + page: number + pageSize: number + total: number + totalPages: number + from: number + to: number + hasPrev: boolean + hasNext: boolean +} + +function finiteInt(value: unknown): number | null { + return typeof value === 'number' && Number.isFinite(value) ? Math.floor(value) : null +} + +/** 由总数与目标分页派生页码信息:页码钳制到 [1, totalPages],空数据视为第 1 页。 */ +export function shopManagePageInfo(page: unknown, pageSize: unknown, total: unknown): ShopManagePageInfo { + const rawSize = Math.max(finiteInt(pageSize) ?? SHOP_PAGE_DEFAULT_SIZE, SHOP_PAGE_MIN_PAGE) + const size = Math.min(rawSize, SHOP_PAGE_MAX_SIZE) + const safeTotal = Math.max(finiteInt(total) ?? 0, 0) + const totalPages = safeTotal === 0 ? 1 : Math.max(1, Math.ceil(safeTotal / size)) + const requested = Math.max(finiteInt(page) ?? SHOP_PAGE_MIN_PAGE, SHOP_PAGE_MIN_PAGE) + const safePage = Math.min(requested, totalPages) + const from = safeTotal === 0 ? 0 : (safePage - 1) * size + 1 + const to = Math.min(safeTotal, safePage * size) + return { + page: safePage, + pageSize: size, + total: safeTotal, + totalPages, + from, + to, + hasPrev: safePage > 1, + hasNext: safePage < totalPages && safeTotal > 0, + } +} diff --git a/admin-frontend-vue/tests/task-89.test.ts b/admin-frontend-vue/tests/task-89.test.ts new file mode 100644 index 00000000..f8b442f9 --- /dev/null +++ b/admin-frontend-vue/tests/task-89.test.ts @@ -0,0 +1,83 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readSource } from './helpers.ts' +import { + createShopManageFilter, + shopManageFilterActive, + shopManageFilterFromInput, + shopManagePageInfo, + toShopManageListParams, +} from '../src/pages/shop/shop-manage-list-state.ts' + +test('test_task_089_shop_manage_filter_page_normal_primary_path', () => { + // 正常主路径:分组/店铺名筛选归一后转为 Java camelCase 列表查询。 + const filter = shopManageFilterFromInput({ groupId: '3', shopName: ' 主店 ' }) + assert.equal(filter.groupId, 3) + assert.equal(filter.shopName, '主店') + const params = toShopManageListParams(2, 20, filter) + assert.equal(params.page, 2) + assert.equal(params.pageSize, 20) + assert.equal(params.groupId, 3) + assert.equal(params.shopName, '主店') +}) + +test('test_task_089_shop_manage_filter_page_normal_variant_input', () => { + // 正常变体:只按店铺名筛选;分组为空字符串按无筛选。 + const filter = shopManageFilterFromInput({ groupId: '', shopName: 'BlueWave' }) + assert.equal(filter.groupId, null) + const params = toShopManageListParams(1, 15, filter) + assert.equal(params.shopName, 'BlueWave') + assert.equal(params.groupId, null) +}) + +test('test_task_089_shop_manage_filter_page_repeated_is_idempotent', () => { + // 正常重复:归一不改输入、结果稳定。 + const input = { groupId: '5', shopName: ' 店 A ' } + assert.deepEqual(shopManageFilterFromInput(input), shopManageFilterFromInput(input)) + assert.deepEqual(input, { groupId: '5', shopName: ' 店 A ' }) +}) + +test('test_task_089_shop_manage_filter_page_boundary_empty_input', () => { + // 边界空值:空筛选回无筛选状态且不视为激活。 + const filter = shopManageFilterFromInput({}) + assert.equal(shopManageFilterActive(filter), false) + const params = toShopManageListParams(1, 15, createShopManageFilter()) + assert.equal(params.page, 1) + assert.equal(params.groupId, null) + assert.equal(params.shopName, undefined) +}) + +test('test_task_089_shop_manage_filter_page_boundary_single_item', () => { + // 边界单元素:仅分组筛选激活并保留。 + const filter = shopManageFilterFromInput({ groupId: '9', shopName: '' }) + assert.equal(shopManageFilterActive(filter), true) + assert.equal(toShopManageListParams(1, 15, filter).groupId, 9) +}) + +test('test_task_089_shop_manage_filter_page_boundary_limit_or_missing_field', () => { + // 边界上限/缺字段:非法分组文本回 null;页码/页大小钳制。 + assert.equal(shopManageFilterFromInput({ groupId: 'abc' }).groupId, null) + const params = toShopManageListParams(0, 999, createShopManageFilter()) + assert.equal(params.page, 1) + assert.equal(params.pageSize, 100) + const info = shopManagePageInfo(2, 15, 30) + assert.equal(info.totalPages, 2) + assert.equal(info.from, 16) + assert.equal(info.hasNext, false) +}) + +test('test_task_089_shop_manage_filter_page_invalid_input_rejected', () => { + // 异常输入:负数/0 分组忽略;全空白店铺名视为无筛选。 + assert.equal(shopManageFilterFromInput({ groupId: '0' }).groupId, null) + assert.equal(shopManageFilterFromInput({ groupId: '-2' }).groupId, null) + const filter = shopManageFilterFromInput({ groupId: null, shopName: ' ' }) + assert.equal(shopManageFilterActive(filter), false) +}) + +test('test_task_089_shop_manage_filter_page_dependency_failure_returns_actionable_message', () => { + // 依赖失败/可操作:筛选状态纯逻辑,无框架/http;复用 DTO 归一与查询序列化。 + const mod = readSource('src/pages/shop/shop-manage-list-state.ts') + assert.equal(/axios|http\.|vue/.test(mod), false, '筛选状态保持纯逻辑') + assert.match(mod, /normalizeShopListParams/) + assert.match(mod, /toShopManagePageQuery/) +})