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

新增 shop-keys-list-state.ts:密钥页本地关键词过滤(备注名/紫鸟账号)与分页
信息派生(页码钳制/首末/前后页)。Java 密钥接口无服务端筛选,搜索在已加载
当前页行上本地过滤,纯逻辑可测。

TDD: task-83.test.ts 8 用例先 RED 后 GREEN。
This commit is contained in:
2026-09-05 16:36:19 +08:00
parent 8c6088bdc9
commit 119095a57b
2 changed files with 171 additions and 0 deletions
@@ -0,0 +1,67 @@
/** 店铺密钥列表筛选与分页(任务 83):密钥页本地关键词过滤与分页信息派生,纯逻辑。
* 说明:Java 店铺密钥接口只支持分页,无服务端筛选,页面 FilterBar 的备注/紫鸟账号
* 搜索在已加载的当前页行上做本地过滤,避免误导用户以为会跨页检索。 */
import { SHOP_PAGE_DEFAULT_SIZE, SHOP_PAGE_MAX_SIZE, SHOP_PAGE_MIN_PAGE, type ShopKeyItem } from './shop-dto.ts'
/** 店铺密钥列表本地筛选状态。 */
export interface ShopKeyListFilter {
keyword?: string
}
function finiteInt(value: unknown): number | null {
return typeof value === 'number' && Number.isFinite(value) ? Math.floor(value) : null
}
/** 归一本地关键词:去首尾空白,空白视为无筛选。 */
export function normalizeShopKeyFilter(raw: Partial<ShopKeyListFilter>): ShopKeyListFilter {
const keyword = typeof raw.keyword === 'string' ? raw.keyword.trim() || undefined : undefined
return keyword ? { keyword } : {}
}
export function shopKeyFilterActive(filter: ShopKeyListFilter): boolean {
return Boolean(filter.keyword && filter.keyword.trim())
}
/** 在已加载行上做备注名/紫鸟账号名子串过滤(大小写不敏感);无关键词原样返回副本。 */
export function filterShopKeyRows(items: readonly ShopKeyItem[], filter: ShopKeyListFilter): ShopKeyItem[] {
const keyword = (filter.keyword || '').trim().toLowerCase()
if (!keyword) return [...items]
return items.filter(
(item) =>
item.remarkName.toLowerCase().includes(keyword) ||
item.ziniaoAccountName.toLowerCase().includes(keyword),
)
}
export interface ShopKeyPageInfo {
page: number
pageSize: number
total: number
totalPages: number
from: number
to: number
hasPrev: boolean
hasNext: boolean
}
/** 由总数与目标分页派生页码信息:页码钳制到 [1, totalPages],空数据视为第 1 页。 */
export function shopKeyPageInfo(page: unknown, pageSize: unknown, total: unknown): ShopKeyPageInfo {
const safePageSize = Math.max(finiteInt(pageSize) ?? SHOP_PAGE_DEFAULT_SIZE, SHOP_PAGE_MIN_PAGE)
const clampedPageSize = Math.min(safePageSize, SHOP_PAGE_MAX_SIZE)
const safeTotal = Math.max(finiteInt(total) ?? 0, 0)
const totalPages = safeTotal === 0 ? 1 : Math.max(1, Math.ceil(safeTotal / clampedPageSize))
const pageNumber = Math.max(finiteInt(page) ?? SHOP_PAGE_MIN_PAGE, SHOP_PAGE_MIN_PAGE)
const safePage = Math.min(pageNumber, totalPages)
const from = safeTotal === 0 ? 0 : (safePage - 1) * clampedPageSize + 1
const to = Math.min(safeTotal, safePage * clampedPageSize)
return {
page: safePage,
pageSize: clampedPageSize,
total: safeTotal,
totalPages,
from,
to,
hasPrev: safePage > 1,
hasNext: safePage < totalPages && safeTotal > 0,
}
}