diff --git a/admin-frontend-vue/src/pages/asin/asin-filter.ts b/admin-frontend-vue/src/pages/asin/asin-filter.ts new file mode 100644 index 00000000..ae26f30e --- /dev/null +++ b/admin-frontend-vue/src/pages/asin/asin-filter.ts @@ -0,0 +1,81 @@ +/** ASIN 数据中心共享分页/筛选类型(任务 61):与 Java 各列表接口(snake)对齐,纯逻辑。 */ + +export const ASIN_PAGE_DEFAULT_SIZE = 15 +export const ASIN_PAGE_MIN_PAGE = 1 +export const ASIN_PAGE_MAX_SIZE = 200 + +/** ISO 日期(yyyy-MM-dd)或空。 */ +export function validIsoDateOrUndefined(value: unknown): string | undefined { + if (typeof value !== 'string') return undefined + const text = value.trim() + return /^\d{4}-\d{2}-\d{2}$/.test(text) ? text : undefined +} + +/** ASIN 中心列表页共享筛选/分页状态。 */ +export interface AsinListParams { + page: number + pageSize: number + /** 值/商品模糊关键字。 */ + keyword?: string + /** 上传用户名模糊搜索。 */ + username?: string + startDate?: string + endDate?: string + groupId?: number | null + /** 国家代码(如 DE、UK)。 */ + country?: string +} + +/** 序列化到 Java 分页接口的查询参数(snake_case)。 */ +export interface AsinPageQuery { + page: number + page_size: number + keyword?: string + username?: string + start_date?: string + end_date?: string + group_id?: number + country?: string +} + +function finiteInt(value: unknown): number | null { + return typeof value === 'number' && Number.isFinite(value) ? Math.floor(value) : null +} + +/** 归一化共享分页/筛选:页码下限 1、页大小 1..上限,空条件视为无筛选。 */ +export function normalizeAsinPageParams(raw: Partial): AsinListParams { + const page = Math.max(finiteInt(raw.page) ?? ASIN_PAGE_MIN_PAGE, ASIN_PAGE_MIN_PAGE) + const rawSize = finiteInt(raw.pageSize) + const pageSize = + rawSize === null || rawSize < ASIN_PAGE_MIN_PAGE + ? ASIN_PAGE_DEFAULT_SIZE + : Math.min(rawSize, ASIN_PAGE_MAX_SIZE) + const textOrUndefined = (value: unknown) => + typeof value === 'string' && value.trim() ? value.trim() : undefined + const keyword = textOrUndefined(raw.keyword) + const username = textOrUndefined(raw.username) + const country = textOrUndefined(raw.country) + const groupId = finiteInt(raw.groupId) + return { + page, + pageSize, + keyword, + username, + startDate: validIsoDateOrUndefined(raw.startDate), + endDate: validIsoDateOrUndefined(raw.endDate), + groupId: groupId === null || groupId < 1 ? null : groupId, + country, + } +} + +/** 前端状态 → Java 查询参数;仅下发非空字段。 */ +export function toAsinPageQuery(params: AsinListParams): AsinPageQuery { + const query: AsinPageQuery = { page: params.page, page_size: params.pageSize } + if (params.keyword) query.keyword = params.keyword + if (params.username) query.username = params.username + if (params.startDate) query.start_date = params.startDate + if (params.endDate) query.end_date = params.endDate + if (params.groupId != null) query.group_id = params.groupId + if (params.country) query.country = params.country + return query +} diff --git a/admin-frontend-vue/tests/task-61.test.ts b/admin-frontend-vue/tests/task-61.test.ts new file mode 100644 index 00000000..b23e5fb3 --- /dev/null +++ b/admin-frontend-vue/tests/task-61.test.ts @@ -0,0 +1,87 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readSource } from './helpers.ts' +import { + ASIN_PAGE_DEFAULT_SIZE, + normalizeAsinPageParams, + toAsinPageQuery, + validIsoDateOrUndefined, +} from '../src/pages/asin/asin-filter.ts' + +test('test_task_061_asin_shared_filter_normal_primary_path', () => { + // 正常主路径:完整筛选条件归一并序列化为 Java snake_case 查询参数。 + const params = normalizeAsinPageParams({ + page: 2, + pageSize: 20, + keyword: 'B0ABC', + startDate: '2026-01-01', + endDate: '2026-01-31', + groupId: 3, + country: 'DE', + }) + assert.equal(params.page, 2) + const query = toAsinPageQuery(params) + assert.equal(query.page, 2) + assert.equal(query.page_size, 20) + assert.equal(query.keyword, 'B0ABC') + assert.equal(query.start_date, '2026-01-01') + assert.equal(query.end_date, '2026-01-31') + assert.equal(query.group_id, 3) + assert.equal(query.country, 'DE') +}) + +test('test_task_061_asin_shared_filter_normal_variant_input', () => { + // 正常变体:username 维度可选;空条件不下发字段。 + const query = toAsinPageQuery(normalizeAsinPageParams({ page: 1, pageSize: 15, username: ' 张伟恒 ' })) + assert.equal(query.username, '张伟恒') + assert.equal('keyword' in query, false) +}) + +test('test_task_061_asin_shared_filter_normal_repeated_operation_is_idempotent', () => { + // 正常重复:归一不改输入、结果稳定。 + const input = { page: 3, pageSize: 30, keyword: 'K' } + assert.deepEqual(normalizeAsinPageParams(input), normalizeAsinPageParams(input)) + assert.equal((input as { keyword: string }).keyword, 'K') +}) + +test('test_task_061_asin_shared_filter_boundary_empty_input', () => { + // 边界空值:空入参回默认分页(第 1 页/15 条),无其它筛选。 + const params = normalizeAsinPageParams({}) + assert.equal(params.page, 1) + assert.equal(params.pageSize, ASIN_PAGE_DEFAULT_SIZE) + const query = toAsinPageQuery(params) + assert.deepEqual(Object.keys(query).sort(), ['page', 'page_size']) +}) + +test('test_task_061_asin_shared_filter_boundary_single_item', () => { + // 边界单元素:country 单筛选保留。 + const query = toAsinPageQuery(normalizeAsinPageParams({ page: 1, country: 'UK' })) + assert.equal(query.country, 'UK') +}) + +test('test_task_061_asin_shared_filter_boundary_limit_or_missing_field', () => { + // 边界上限:页码/页大小钳制;非法 ISO 日期按无日期处理;负 groupId 忽略。 + const params = normalizeAsinPageParams({ page: 0, pageSize: 999, startDate: 'not-a-date', groupId: -1 }) + assert.equal(params.page, 1) + assert.equal(params.pageSize, 200) + assert.equal(params.startDate, undefined) + assert.equal(params.groupId, null) + assert.equal(validIsoDateOrUndefined('2026-02-01'), '2026-02-01') +}) + +test('test_task_061_asin_shared_filter_invalid_input_rejected', () => { + // 异常输入:非数字/NaN 回默认;空字符串条件不下发。 + const query = toAsinPageQuery(normalizeAsinPageParams({ page: 'x' as never, pageSize: NaN, keyword: ' ' })) + assert.equal(query.page, 1) + assert.equal(query.page_size, ASIN_PAGE_DEFAULT_SIZE) + assert.equal('keyword' in query, false) +}) + +test('test_task_061_asin_shared_filter_dependency_failure_returns_actionable_message', () => { + // 依赖失败/可操作:共享筛选为纯逻辑、无框架/http,日期与国家可空。 + const mod = readSource('src/pages/asin/asin-filter.ts') + assert.equal(/axios|http\.|vue/.test(mod), false, '共享筛选保持纯逻辑') + assert.match(mod, /start_date/) + assert.match(mod, /end_date/) + assert.match(mod, /country/) +})