task-89(店铺中心): 实现店铺筛选和分页
新增 shop-manage-list-state.ts:分组/店铺名筛选归一与激活判断、列表参数/ 查询序列化及分页信息派生;与 DTO 分页钳制对齐。 TDD: task-89.test.ts 8 用例先 RED 后 GREEN。
This commit is contained in:
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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/)
|
||||
})
|
||||
Reference in New Issue
Block a user