feat(web): A6 任务中心页面与可复用进度组件(方案 A)
背景:A6-FE-01/02,用户选定方案 A · 经典管理台;后端任务协议 / SSE / 取消 / 仅重试失败项已就绪,需要可用页面与可复用进度体验。 改动: - 路由 /material/tasks、/material/tasks/:id(material:read;写操作按 material:render 渲染);侧栏「生产任务」更名「任务中心」;AppLayout 满高布局;详情面包屑与标题对齐。 - 页面:任务中心列表(统计、类型 chips、状态/客户/时间/关键字筛选 + URL 同步、游标加载更多、三态、行内取消/重试)、详情(任务头、失败优先进度、失败明细与行号、逐项表 + items 续拉、任务信息/时间线/进度更新/操作说明)、提交演练任务弹层(实时预估 + 复用 Idempotency-Key + 提交中禁用)、取消/重试二次确认。 - 组件:TaskTypeTag / TaskStatusTag / TaskItemStatusTag / TaskProgressBar / TaskMiniProgress / TaskProgressRing / TaskSseIndicator / TaskBadge + useTaskStream(fetch 流 + Last-Event-ID + 重连 + 5s 轮询兜底);顶栏角标含待处理失败直达,完成/失败通知。 - API/hooks:media.ts(提交 202 + Idempotency-Key、列表、详情、取消、重试、SSE 流)、use-media-tasks(游标 / 详情 / 角标 / 待处理失败 / 提交取消重试 mutation / 预估配置);client 增加流式请求与 401 刷新;后台轮询失败不弹全局 toast。 - 样式:tasks.css / task-detail.css 全部使用 --lx-* token;行高 56、表格撑满、1024 保留失败统计;按独立视觉评审完成两轮修正(失败数前置、去工程化文案、状态色统一、去重复入口与计数、去掉禁用入口)。 影响面:新增 2 个受权限保护的路由与 1 个顶栏角标入口;未改其他页面行为。 回滚:撤销提交即可,后端接口与旧页面不受影响。 验证:web typecheck / lint / test(37 文件 238 用例,覆盖率 91.7%)/ build 全绿;A6 浏览器 e2e 6/6;真实 API 截图见 design/a6-ui-v1/shots/impl/real。
This commit is contained in:
1 parent
b377618ff9
commit
a9ee5e6383
40 files changed
+7299
-13
No files matched your search
@@ -0,0 +1,51 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { parseSseChunk, parseSseEvent } from './task-sse';
|
||||
|
||||
const TASK_ID = '01JCNB2M8K4Q7T3F9V6W1X0Y5Z';
|
||||
|
||||
describe('SSE 帧解析', () => {
|
||||
it('按空行切分多帧,处理心跳注释与 retry 字段', () => {
|
||||
const chunk = [
|
||||
': ping',
|
||||
'',
|
||||
`id: 7\nevent: progress\ndata: {"taskId":"${TASK_ID}","status":"running","progress":25,"itemTotal":8,"itemSucceeded":2,"itemFailed":0,"itemSkipped":0}`,
|
||||
'',
|
||||
'',
|
||||
].join('\n');
|
||||
const parsed = parseSseChunk(chunk);
|
||||
expect(parsed.frames).toHaveLength(1);
|
||||
expect(parsed.frames[0]?.id).toBe('7');
|
||||
expect(parsed.frames[0]?.event).toBe('progress');
|
||||
expect(parsed.rest).toBe('');
|
||||
});
|
||||
|
||||
it('未收完的尾帧留在 rest,下一 chunk 拼接后解析', () => {
|
||||
const first = parseSseChunk('id: 1\nevent: progress\ndata: {"a":');
|
||||
expect(first.frames).toHaveLength(0);
|
||||
expect(first.rest).toContain('"a":');
|
||||
const second = parseSseChunk(`${first.rest}1}\n\n`);
|
||||
expect(second.frames).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('多行 data 用换行拼接,未知字段忽略,兼容 CRLF', () => {
|
||||
const chunk = 'id: 2\r\nevent: item\r\ndata: {"taskId":"a",\r\ndata: "seq":1}\r\n\r\n';
|
||||
const parsed = parseSseChunk(chunk);
|
||||
expect(parsed.frames[0]?.data).toBe(`{"taskId":"a",\n"seq":1}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SSE 事件契约解析', () => {
|
||||
it('合法帧 → 契约事件;非法 JSON / 未知 event / 缺 id 返回 null', () => {
|
||||
const valid = parseSseEvent({
|
||||
id: '3',
|
||||
event: 'done',
|
||||
data: `{"taskId":"${TASK_ID}","status":"pending_review","progress":100}`,
|
||||
});
|
||||
expect(valid?.event).toBe('done');
|
||||
|
||||
expect(parseSseEvent({ id: '4', event: 'progress', data: '{oops' })).toBeNull();
|
||||
expect(parseSseEvent({ id: '5', event: 'unknown', data: '{}' })).toBeNull();
|
||||
expect(parseSseEvent({ event: 'done', data: '{}' })).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user