feat(flow): 落地 FL2.0 原型——部门选人抽屉 / 授权管理页 / 退回重提入口(基线 origin/main)
build / build (push) Successful in 1m31s

在 c93313c 之上增量落地原型与前端规范,保留既有流程图入口/失败提示/指派预览/线级停发:

- 审批/抄送抽屉按原型重构(422→1026 / 239→742 行):科室树+成员多选、SYSTEM/CUSTOM 分支、
  CUSTOM 分类导航自动定位、跨节点导航重置、保存后重开回显
- 新增授权管理页 /admin/usage-grants(UsageGrants.vue,417 行):校域清单、按流程线筛选、
  授予/撤销后回读,失败不宣称成功
- 路由与 API:新增 usage-grant(schoolList/summary/grant/revoke)、department(groups/options/resolve/members)、
  instance/resubmit;TaskHandle 增加「退回待修改」入口(仅 status=50 显示)
- 错误处理沿用 flowError 统一出口,不透出后端原文
- 组件/契约测试:flow-error、flow2-contract、department-roundtrip、task-result

不含构建产物(dist-verify)与依赖目录
This commit is contained in:
chenxing
2026-10-30 10:15:47 +08:00
parent c93313cdb7
commit bed57846f4
28 changed files with 2829 additions and 253 deletions
@@ -0,0 +1,27 @@
import { it, expect, vi, afterEach } from 'vitest';
import { mount, flushPromises, enableAutoUnmount } from '@vue/test-utils';
import ViewUIPlus from 'view-ui-plus';
import TaskHandle from '@/views/flow/TaskHandle.vue';
const { api } = vi.hoisted(() => ({ api: { taskDetail: vi.fn(), approve: vi.fn() } }));
vi.mock('@/api/module/flow', () => ({ flowApi: api, INSTANCE_STATUS: { 10: '审批中' } }));
vi.mock('vue-router', () => ({ useRoute: () => ({ params: { taskId: '1' }, path: '/task/1', query: { embed: '1' } }), useRouter: () => ({ push: vi.fn() }) }));
vi.mock('@/utils/taskEmbed', () => ({ isBareTaskRoute: () => true, postToParent: vi.fn() }));
enableAutoUnmount(afterEach);
const detail = { instanceId: 9, status: 10, myPendingTaskIds: [1], trace: [{ taskId: 1, status: 0 }], outlets: [] };
async function click(text: string) {
const b = Array.from(document.querySelectorAll('button')).find(x => x.textContent?.trim() === text);
expect(b).toBeTruthy(); b!.click(); await flushPromises();
}
for (const lost of [true, false]) it(lost ? 'POST 丢响应不声称未生效' : 'POST 成功但刷新失败保留成功并关闭办理入口', async () => {
document.body.innerHTML = '';
let reads = 0;
api.taskDetail.mockImplementation(() => ({ send: async () => { if (++reads > 1) throw new Error('refresh failed'); return detail; } }));
api.approve.mockImplementation(() => ({ send: async () => { if (lost) throw new TypeError('Failed to fetch'); return {}; } }));
mount(TaskHandle, { global: { plugins: [ViewUIPlus], stubs: { TraceTimeline: true } }, attachTo: document.body });
await flushPromises(); await click('同意'); await click('确定');
const text = document.body.textContent || '';
expect(text).not.toContain('本次办理未生效');
expect(text).not.toContain('任务仍是你的待办');
if (lost) expect(text).toContain('结果待确认');
else { expect(text).toContain('已提交成功'); expect(document.querySelector('.actions')).toBeNull(); }
});