From 137af44ae8be69bbd3f25a768ce4a1ca0397c5cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Sat, 5 Sep 2026 17:18:47 +0800 Subject: [PATCH] =?UTF-8?q?task-111(=E4=BB=BB=E5=8A=A1=E4=B8=8E=E9=87=8D?= =?UTF-8?q?=E5=A4=8D=E5=88=86=E6=9E=90=E4=B8=AD=E5=BF=83):=20=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E5=BA=97=E9=93=BA=E6=95=B0=E6=8D=AE=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E5=88=86=E9=A1=B5=E5=88=B7=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 shop-data-list-state.ts:店铺数据任务分页信息派生与可批量选择结果行 (result_id)展平,供后续批量下载复用。 TDD: task-111.test.ts 8 用例先 RED 后 GREEN。 --- .../src/pages/tasks/shop-data-list-state.ts | 54 ++++++++++++++++ admin-frontend-vue/tests/task-111.test.ts | 64 +++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 admin-frontend-vue/src/pages/tasks/shop-data-list-state.ts create mode 100644 admin-frontend-vue/tests/task-111.test.ts diff --git a/admin-frontend-vue/src/pages/tasks/shop-data-list-state.ts b/admin-frontend-vue/src/pages/tasks/shop-data-list-state.ts new file mode 100644 index 00000000..16c3265e --- /dev/null +++ b/admin-frontend-vue/src/pages/tasks/shop-data-list-state.ts @@ -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 +} diff --git a/admin-frontend-vue/tests/task-111.test.ts b/admin-frontend-vue/tests/task-111.test.ts new file mode 100644 index 00000000..ecd641b9 --- /dev/null +++ b/admin-frontend-vue/tests/task-111.test.ts @@ -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/) +})