task-89(店铺中心): 实现店铺筛选和分页

新增 shop-manage-list-state.ts:分组/店铺名筛选归一与激活判断、列表参数/
查询序列化及分页信息派生;与 DTO 分页钳制对齐。

TDD: task-89.test.ts 8 用例先 RED 后 GREEN。
This commit is contained in:
2026-09-05 16:43:37 +08:00
parent 42722d0cc6
commit 6ccd5ce1af
2 changed files with 181 additions and 0 deletions
@@ -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,
}
}