task-111(任务与重复分析中心): 实现店铺数据任务分页刷新
新增 shop-data-list-state.ts:店铺数据任务分页信息派生与可批量选择结果行 (result_id)展平,供后续批量下载复用。 TDD: task-111.test.ts 8 用例先 RED 后 GREEN。
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
/** 店铺数据任务列表分页与选择派生(任务 111/112):分页信息 + 可批量选择的结果行,纯逻辑。 */
|
||||
import { SHOP_DATA_DEFAULT_PAGE_SIZE, SHOP_DATA_MAX_PAGE_SIZE, SHOP_DATA_PAGE_MIN } from './shop-data-filter.ts'
|
||||
import type { ShopDataTaskGroup } from './shop-data-model.ts'
|
||||
|
||||
export interface ShopDataPageInfo {
|
||||
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 页。 */
|
||||
export function shopDataPageInfo(page: unknown, pageSize: unknown, total: unknown): ShopDataPageInfo {
|
||||
const rawSize = finiteInt(pageSize)
|
||||
const size =
|
||||
rawSize === null || rawSize < 1
|
||||
? SHOP_DATA_DEFAULT_PAGE_SIZE
|
||||
: Math.min(Math.max(rawSize, 10), SHOP_DATA_MAX_PAGE_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_DATA_PAGE_MIN, SHOP_DATA_PAGE_MIN)
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
/** 展平本页所有可选中的结果行(result_id),供批量下载/勾选;缺行忽略。 */
|
||||
export function selectableResultRows(groups: readonly ShopDataTaskGroup[]): string[] {
|
||||
const ids: string[] = []
|
||||
for (const group of groups) {
|
||||
for (const row of group.results) {
|
||||
if (row.resultId) ids.push(row.resultId)
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readSource } from './helpers.ts'
|
||||
import { selectableResultRows, shopDataPageInfo } from '../src/pages/tasks/shop-data-list-state.ts'
|
||||
|
||||
test('test_task_111_shop_data_page_normal_primary_path', () => {
|
||||
// 正常主路径:分页信息派生(页码/首末/前后页)。
|
||||
const info = shopDataPageInfo(2, 20, 55)
|
||||
assert.equal(info.page, 2)
|
||||
assert.equal(info.totalPages, 3)
|
||||
assert.equal(info.from, 21)
|
||||
assert.equal(info.to, 40)
|
||||
assert.equal(info.hasPrev, true)
|
||||
assert.equal(info.hasNext, true)
|
||||
})
|
||||
|
||||
test('test_task_111_shop_data_page_normal_variant_input', () => {
|
||||
// 正常变体:末页边界。
|
||||
const info = shopDataPageInfo(3, 20, 55)
|
||||
assert.equal(info.page, 3)
|
||||
assert.equal(info.to, 55)
|
||||
assert.equal(info.hasNext, false)
|
||||
})
|
||||
|
||||
test('test_task_111_shop_data_page_repeated_is_idempotent', () => {
|
||||
// 正常重复:分页计算稳定。
|
||||
assert.deepEqual(shopDataPageInfo(1, 20, 10), shopDataPageInfo(1, 20, 10))
|
||||
})
|
||||
|
||||
test('test_task_111_shop_data_page_boundary_empty_input', () => {
|
||||
// 边界空值:空列表回第 1 页空态。
|
||||
const info = shopDataPageInfo(1, 20, 0)
|
||||
assert.equal(info.totalPages, 1)
|
||||
assert.equal(info.page, 1)
|
||||
assert.equal(info.to, 0)
|
||||
})
|
||||
|
||||
test('test_task_111_shop_data_page_boundary_single_item', () => {
|
||||
// 边界单元素:单组单行可选结果派生。
|
||||
const rows = selectableResultRows([{ results: [{ resultId: '5' }] } as never])
|
||||
assert.equal(rows.length, 1)
|
||||
assert.equal(rows[0], '5')
|
||||
})
|
||||
|
||||
test('test_task_111_shop_data_page_boundary_limit_or_missing_field', () => {
|
||||
// 边界上限/缺字段:页码越界钳到末页;无结果组不产出行。
|
||||
const info = shopDataPageInfo(99, 20, 55)
|
||||
assert.equal(info.page, 3)
|
||||
assert.deepEqual(selectableResultRows([]), [])
|
||||
})
|
||||
|
||||
test('test_task_111_shop_data_page_invalid_input_rejected', () => {
|
||||
// 异常输入:NaN 页码按第 1 页处理。
|
||||
const info = shopDataPageInfo(Number.NaN, 20, 10)
|
||||
assert.equal(info.page, 1)
|
||||
assert.equal(info.from, 1)
|
||||
})
|
||||
|
||||
test('test_task_111_shop_data_page_dependency_failure_returns_actionable_message', () => {
|
||||
// 依赖失败/可操作:分页状态纯逻辑、无框架/http。
|
||||
const mod = readSource('src/pages/tasks/shop-data-list-state.ts')
|
||||
assert.equal(/axios|http\.|vue/.test(mod), false, '分页状态保持纯逻辑')
|
||||
assert.match(mod, /selectableResultRows/)
|
||||
})
|
||||
Reference in New Issue
Block a user