perf(frontend): 工具页分页/历史截断/轻量轮询/共享纯逻辑(审查 F5/F6/F9/F12/G3)
- F9 新增 useTablePaging composable 并接入 7 个工具页"匹配结果"表(只切渲染窗口, 不加选择语义;每页 100 条)+ 深色分页样式 - F6 历史任务抽屉渲染截断(默认 50 条 + "显示全部/收起"),全选口径改为当前可见条目 - F5 新增 task-progress-polling 适配器:等待任务终态的紧循环走 /tasks/progress/light, 异常或空响应回退重型 batch;已接入跟价/定时匹配的 waitForTaskTerminal - F12 背景图 bg.jpg 251KB → 166KB(1920 宽 + quality 80,image-set 仍优先 webp) - G3 抽出共享纯逻辑 task-queue-state(开始时间表序列化校验 + 记录缺失错误判定), 5 个工具页删除逐字重复实现 - 新增 4 个单测文件(分页切片/历史截断/轻量轮询归一/任务队列纯逻辑)
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { HISTORY_VISIBLE_LIMIT, sliceHistoryItems } from '../src/shared/utils/history-paging.ts'
|
||||
|
||||
test('未超上限时全量渲染且不显示展开入口', () => {
|
||||
const items = Array.from({ length: 10 }, (_, i) => ({ key: `k${i}` }))
|
||||
|
||||
const slice = sliceHistoryItems(items, false)
|
||||
|
||||
assert.equal(slice.visible.length, 10)
|
||||
assert.equal(slice.hiddenCount, 0)
|
||||
assert.equal(slice.capped, false)
|
||||
})
|
||||
|
||||
test('超过上限时只渲染前 N 条并给出剩余条数', () => {
|
||||
const items = Array.from({ length: HISTORY_VISIBLE_LIMIT + 7 }, (_, i) => ({ key: `k${i}` }))
|
||||
|
||||
const slice = sliceHistoryItems(items, false)
|
||||
|
||||
assert.equal(slice.visible.length, HISTORY_VISIBLE_LIMIT)
|
||||
assert.equal(slice.hiddenCount, 7)
|
||||
assert.equal(slice.capped, true)
|
||||
assert.equal(slice.visible[0].key, 'k0')
|
||||
assert.equal(slice.visible.at(-1)?.key, `k${HISTORY_VISIBLE_LIMIT - 1}`)
|
||||
})
|
||||
|
||||
test('展开全部后不再截断', () => {
|
||||
const items = Array.from({ length: HISTORY_VISIBLE_LIMIT + 3 }, (_, i) => ({ key: `k${i}` }))
|
||||
|
||||
const slice = sliceHistoryItems(items, true)
|
||||
|
||||
assert.equal(slice.visible.length, items.length)
|
||||
assert.equal(slice.hiddenCount, 0)
|
||||
assert.equal(slice.capped, false)
|
||||
})
|
||||
|
||||
test('恰好等于上限时不算截断', () => {
|
||||
const items = Array.from({ length: HISTORY_VISIBLE_LIMIT }, (_, i) => ({ key: `k${i}` }))
|
||||
|
||||
const slice = sliceHistoryItems(items, false)
|
||||
|
||||
assert.equal(slice.capped, false)
|
||||
assert.equal(slice.visible.length, HISTORY_VISIBLE_LIMIT)
|
||||
})
|
||||
|
||||
test('空值与非法上限安全降级', () => {
|
||||
assert.deepEqual(sliceHistoryItems(null, false), { visible: [], hiddenCount: 0, capped: false })
|
||||
assert.deepEqual(sliceHistoryItems(undefined, false), { visible: [], hiddenCount: 0, capped: false })
|
||||
assert.deepEqual(sliceHistoryItems([], false), { visible: [], hiddenCount: 0, capped: false })
|
||||
|
||||
const items = [{ key: 'a' }, { key: 'b' }]
|
||||
const slice = sliceHistoryItems(items, false, 0)
|
||||
assert.equal(slice.visible.length, 2, '非法上限回退到默认上限')
|
||||
assert.equal(slice.capped, false)
|
||||
})
|
||||
|
||||
test('自定义上限生效', () => {
|
||||
const items = Array.from({ length: 5 }, (_, i) => i)
|
||||
|
||||
const slice = sliceHistoryItems(items, false, 2)
|
||||
|
||||
assert.deepEqual(slice.visible, [0, 1])
|
||||
assert.equal(slice.hiddenCount, 3)
|
||||
})
|
||||
@@ -0,0 +1,38 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { DEFAULT_TABLE_PAGE_SIZE, clampPage, paginateSlice } from '../src/shared/composables/useTablePaging.ts'
|
||||
|
||||
const items = (n) => Array.from({ length: n }, (_, i) => i + 1)
|
||||
|
||||
test('切片返回当前页窗口', () => {
|
||||
assert.deepEqual(paginateSlice(items(250), 1, 100), items(100))
|
||||
assert.deepEqual(paginateSlice(items(250), 3, 100), items(250).slice(200, 250), '末页只到最后一行为止')
|
||||
assert.deepEqual(paginateSlice(items(5), 2, 100), items(5), '只有一页时任何页码都落在首页')
|
||||
})
|
||||
|
||||
test('越界页码自动收敛到有效范围', () => {
|
||||
assert.deepEqual(paginateSlice(items(250), 99, 100), items(250).slice(200, 250), '超出末页回到末页')
|
||||
assert.deepEqual(paginateSlice(items(250), 0, 100), items(100), '小于 1 回到首页')
|
||||
assert.deepEqual(paginateSlice(items(250), -3, 100), items(100))
|
||||
})
|
||||
|
||||
test('非法分页参数安全降级', () => {
|
||||
assert.deepEqual(paginateSlice(null, 1, 100), [])
|
||||
assert.deepEqual(paginateSlice(undefined, 1, 100), [])
|
||||
assert.equal(paginateSlice(items(10), 1, 0).length, 10, '非法 pageSize 回退默认值')
|
||||
assert.equal(paginateSlice(items(DEFAULT_TABLE_PAGE_SIZE + 1), 1, Number.NaN).length, DEFAULT_TABLE_PAGE_SIZE)
|
||||
})
|
||||
|
||||
test('clampPage 与切片口径一致', () => {
|
||||
assert.equal(clampPage(1, 250, 100), 1)
|
||||
assert.equal(clampPage(3, 250, 100), 3)
|
||||
assert.equal(clampPage(9, 250, 100), 3)
|
||||
assert.equal(clampPage(0, 0, 100), 1, '空列表仍有 1 页')
|
||||
assert.equal(clampPage(5, 0, 100), 1)
|
||||
})
|
||||
|
||||
test('恰好整除时不多出空页', () => {
|
||||
assert.equal(clampPage(2, 200, 100), 2)
|
||||
assert.equal(clampPage(3, 200, 100), 2)
|
||||
assert.deepEqual(paginateSlice(items(200), 2, 100).length, 100)
|
||||
})
|
||||
@@ -0,0 +1,50 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { pickPollingBatch, toBatchItems } from '../src/shared/api/task-progress-polling.ts'
|
||||
|
||||
test('轻量项归一为重型 batch 的嵌套形状', () => {
|
||||
const items = toBatchItems([
|
||||
{ taskId: 7, status: 'RUNNING', fileStatus: 'PENDING', fileReady: false, updatedAt: '2026-09-14T06:00:00' },
|
||||
])
|
||||
|
||||
assert.equal(items.length, 1)
|
||||
assert.deepEqual(items[0].task, {
|
||||
id: 7,
|
||||
status: 'RUNNING',
|
||||
statusCode: null,
|
||||
fileStatus: 'PENDING',
|
||||
fileError: null,
|
||||
fileReady: false,
|
||||
updatedAt: '2026-09-14T06:00:00',
|
||||
})
|
||||
})
|
||||
|
||||
test('缺少 taskId 的项被丢弃,非法 taskId 同样丢弃', () => {
|
||||
const items = toBatchItems([{ status: 'RUNNING' }, { taskId: 0, status: 'RUNNING' }, { taskId: 3 }])
|
||||
|
||||
assert.equal(items.length, 1)
|
||||
assert.equal(items[0].task?.id, 3)
|
||||
})
|
||||
|
||||
test('有条目时返回可用结果', () => {
|
||||
const picked = pickPollingBatch([{ taskId: 9, status: 'SUCCESS' }], [])
|
||||
|
||||
assert.notEqual(picked, null)
|
||||
assert.equal(picked?.items.length, 1)
|
||||
assert.deepEqual(picked?.missingTaskIds, [])
|
||||
})
|
||||
|
||||
test('只有 missing 列表也算可用结果(任务已删除场景)', () => {
|
||||
const picked = pickPollingBatch([], [11, 12])
|
||||
|
||||
assert.notEqual(picked, null)
|
||||
assert.equal(picked?.items.length, 0)
|
||||
assert.deepEqual(picked?.missingTaskIds, [11, 12])
|
||||
})
|
||||
|
||||
test('空响应返回 null,调用方据此回退重型端点', () => {
|
||||
assert.equal(pickPollingBatch([], []), null)
|
||||
assert.equal(pickPollingBatch(null, null), null)
|
||||
assert.equal(pickPollingBatch(undefined, undefined), null)
|
||||
assert.equal(pickPollingBatch([{ status: 'RUNNING' }], []), null, '全是非法项时同样回退')
|
||||
})
|
||||
@@ -0,0 +1,57 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import {
|
||||
isRecordMissingError,
|
||||
parseTaskStartTimes,
|
||||
removeTaskStartTime,
|
||||
upsertTaskStartTime,
|
||||
} from '../src/shared/utils/task-queue-state.ts'
|
||||
|
||||
test('解析开始时间表:保留正整数 id 与非空时间戳', () => {
|
||||
const parsed = parseTaskStartTimes(JSON.stringify({ '12': '2026-09-14T00:00:00Z', '0': 'x', '-3': 'y', 'abc': 'z', '15': ' ' }))
|
||||
|
||||
assert.deepEqual(parsed, { 12: '2026-09-14T00:00:00Z' })
|
||||
})
|
||||
|
||||
test('解析开始时间表:坏 JSON 与非字符串值安全降级', () => {
|
||||
assert.deepEqual(parseTaskStartTimes('{bad json'), {})
|
||||
assert.deepEqual(parseTaskStartTimes(null), {})
|
||||
assert.deepEqual(parseTaskStartTimes(undefined), {})
|
||||
assert.deepEqual(parseTaskStartTimes('{"7": 123, "8": null}'), {})
|
||||
})
|
||||
|
||||
test('写入开始时间:非法 taskId 不改动内容且不共享引用', () => {
|
||||
const base = { 1: 'a' }
|
||||
const next = upsertTaskStartTime(base, 0, 'b')
|
||||
|
||||
assert.deepEqual(next, { 1: 'a' })
|
||||
assert.notEqual(next, base)
|
||||
|
||||
assert.deepEqual(upsertTaskStartTime(base, 2, 'b'), { 1: 'a', 2: 'b' })
|
||||
})
|
||||
|
||||
test('移除开始时间:不存在时返回内容相同的新对象', () => {
|
||||
const base = { 1: 'a' }
|
||||
const next = removeTaskStartTime(base, 9)
|
||||
|
||||
assert.deepEqual(next, { 1: 'a' })
|
||||
assert.notEqual(next, base)
|
||||
assert.deepEqual(removeTaskStartTime(base, 1), {})
|
||||
assert.deepEqual(removeTaskStartTime(base, null), { 1: 'a' })
|
||||
})
|
||||
|
||||
test('记录缺失错误判定:中文关键词与 404 命中', () => {
|
||||
assert.equal(isRecordMissingError(new Error('记录不存在')), true)
|
||||
assert.equal(isRecordMissingError(new Error('任务不存在')), true)
|
||||
assert.equal(isRecordMissingError(new Error('该记录已删除')), true)
|
||||
assert.equal(isRecordMissingError(new Error('request failed with 404')), true)
|
||||
assert.equal(isRecordMissingError(new Error('Not Found')), true)
|
||||
assert.equal(isRecordMissingError('不存在'), true)
|
||||
})
|
||||
|
||||
test('记录缺失错误判定:其它错误不误判', () => {
|
||||
assert.equal(isRecordMissingError(new Error('服务器繁忙,请稍后重试')), false)
|
||||
assert.equal(isRecordMissingError(new Error('网络超时')), false)
|
||||
assert.equal(isRecordMissingError(null), false)
|
||||
assert.equal(isRecordMissingError(undefined), false)
|
||||
})
|
||||
Reference in New Issue
Block a user