feat(08-05-interaction-pattern-unification): 统一前端交互与反馈模式

这个提交包含在:
2026-08-07 06:50:40 +08:00
父节点 d4e9597b37
当前提交 94f1bad336
共修改 97 个文件,包含 784 行新增和 836 行删除
+67 -9
查看文件
@@ -189,7 +189,7 @@ function mockNavigation(page: import("@playwright/test").Page, mode: "employee"
? {
main: [
{ key: "portal", href: "/portal", label: "工作台", icon: "home_app_logo", placement: "main", permissionCodes: [], featureFlagKey: null },
{ key: "workflow", href: "/workflow/tasks", label: "待我处理", icon: "fact_check", placement: "main", permissionCodes: [], featureFlagKey: null },
{ key: "workflow", href: "/workflow/tasks", label: "待我审批", icon: "fact_check", placement: "main", permissionCodes: [], featureFlagKey: null },
{ key: "expense", href: "/reimbursements", label: "报销", icon: "request_quote", placement: "main", permissionCodes: [], featureFlagKey: null },
{ key: "purchase", href: "/purchase-requests", label: "采购", icon: "shopping_cart", placement: "main", permissionCodes: [], featureFlagKey: null },
{ key: "contracts", href: "/contracts-registry", label: "合同", icon: "description", placement: "main", permissionCodes: [], featureFlagKey: null },
@@ -262,14 +262,37 @@ function mockAttendanceToday(page: import("@playwright/test").Page) {
});
}
function mockPortalHome(page: import("@playwright/test").Page, mode: "employee" | "manager") {
function mockPortalHome(
page: import("@playwright/test").Page,
mode: "employee" | "manager",
options: { failuresBeforeSuccess?: number } = {},
) {
let requestCount = 0;
return page.route("**/api/portal/home", async (route) => {
requestCount += 1;
if (requestCount <= (options.failuresBeforeSuccess ?? 0)) {
await route.fulfill({
status: 500,
contentType: "application/json",
body: JSON.stringify({
requestId: "mock-request",
error: {
code: "SYS_001",
message: "GET /api/portal/home failed",
details: {},
},
}),
});
return;
}
const data = mode === "manager"
? {
workspace: { id: "workspace-1", persisted: true, profile: "small_team_internal" },
greeting: { name: "李研发" },
cards: [
{ key: "pending_approvals", title: "待我处理", value: 3, href: "/workflow/tasks", severity: "warning" },
{ key: "pending_approvals", title: "待我审批", value: 3, href: "/workflow/tasks", severity: "warning" },
{ key: "team_presence", title: "今日团队状态", value: "6/8 在岗", href: "/team", severity: "warning" },
{ key: "weekly_missing", title: "未提交周报", value: 2, href: "/weekly-reports", severity: "warning" },
{ key: "weekly_help", title: "需协助员工", value: 1, href: "/weekly-reports", severity: "high" },
@@ -307,7 +330,7 @@ function mockPortalHome(page: import("@playwright/test").Page, mode: "employee"
workspace: { id: "workspace-1", persisted: true, profile: "small_team_internal" },
greeting: { name: "陈工程" },
cards: [
{ key: "my_todos", title: "我的待办", value: 4, href: "/todos", severity: "warning" },
{ key: "my_todos", title: "我的事项", value: 4, href: "/todos", severity: "warning" },
{ key: "today_presence", title: "今日状态", value: "在岗", href: "/team", severity: "normal" },
{ key: "my_weekly_report", title: "本周周报", value: "待填写", href: "/weekly-reports", severity: "warning" },
{ key: "my_projects", title: "我的项目", value: 2, href: "/projects", severity: "warning" },
@@ -524,12 +547,47 @@ test("Portal 员工视角优先显示今日动作顺序", async ({ page }) => {
await expect(page.getByTestId("portal-work-summary")).toContainText("待处理 7");
await expect(page.getByRole("heading", { name: "今日待处理" })).toBeVisible();
await expect(page.getByRole("link", { name: /待办事项 4 条.*查看待办/ })).toBeVisible();
await expect(page.getByRole("link", { name: /我的事项 4 条.*查看事项/ })).toBeVisible();
await expect(page.getByRole("link", { name: /本周周报待填写.*填写周报/ })).toBeVisible();
await expect(page.getByText("当前焦点")).not.toBeVisible();
await expect(page.getByText("下一步", { exact: true })).not.toBeVisible();
await expect(page.locator("summary").filter({ hasText: "团队与通知" })).not.toBeVisible();
await expect(page.locator("summary").filter({ hasText: "更多入口" })).not.toBeVisible();
const portalSectionOrder = await page.getByRole("heading", { name: /今日待处理|常用入口/ }).evaluateAll((headings) =>
headings.map((heading) => heading.textContent?.trim()),
);
expect(portalSectionOrder).toEqual(["今日待处理", "常用入口"]);
const todoLink = page.getByRole("link", { name: /我的事项 4 条.*查看事项/ });
const weeklyLink = page.getByRole("link", { name: /本周周报待填写.*填写周报/ });
await todoLink.focus();
await expect(todoLink).toBeFocused();
await page.keyboard.press("Tab");
await expect(weeklyLink).toBeFocused();
});
test("Portal 加载失败持久显示自然中文并可重新加载", async ({ page }) => {
await mockPasswordLogin(page);
await mockNavigation(page, "employee");
await mockPortalHome(page, "employee", { failuresBeforeSuccess: 2 });
await mockAttendanceToday(page);
await mockNotificationList(page);
await mockMe(page, ["employee"], "陈工程");
await login(page, EMPLOYEE);
const errorRegion = page.getByTestId("portal-load-error");
await expect(errorRegion).toContainText("服务暂不可用,请稍后重试或联系管理员");
await expect(errorRegion).not.toContainText("SYS_001");
await expect(errorRegion).not.toContainText("/api/portal/home");
const retryButton = page.getByTestId("portal-load-retry");
await retryButton.focus();
await expect(retryButton).toBeFocused();
await retryButton.click();
await expect(page.getByRole("heading", { name: "今日待处理" })).toBeVisible();
await expect(errorRegion).toHaveCount(0);
});
test("Portal 管理视角按角色分发待处理和团队卡点", async ({ page }) => {
@@ -543,7 +601,7 @@ test("Portal 管理视角按角色分发待处理和团队卡点", async ({ page
await login(page, MANAGER);
await expect(page.getByRole("heading", { name: "今日待处理" })).toBeVisible();
await expect(page.getByRole("link", { name: /待我处理 3 条.*去处理/ })).toBeVisible();
await expect(page.getByRole("link", { name: /待我审批 3 条.*去处理/ })).toBeVisible();
await expect(page.getByRole("heading", { name: "团队卡点" })).toBeVisible();
await expect(page.getByRole("link", { name: /项目风险未关闭.*处理风险/ })).toBeVisible();
await expect(page.locator("summary").filter({ hasText: "团队与通知" })).not.toBeVisible();
@@ -653,9 +711,9 @@ test("Portal 新员工只显示轻量欢迎入口,不再显示快速上手清
const welcomeDialog = page.getByTestId(TEST_IDS.onboarding.welcomeDialog);
await expect(welcomeDialog).toBeVisible();
await expect(welcomeDialog.getByRole("heading", { name: "常用入口" })).toBeVisible();
await expect(welcomeDialog.getByRole("link")).toHaveCount(3);
await expect(welcomeDialog.getByRole("link", { name: /处理待办/ })).toBeVisible();
await expect(welcomeDialog.getByRole("link", { name: /提交报销/ })).toBeVisible();
await expect(welcomeDialog.getByRole("link")).toHaveCount(2);
await expect(welcomeDialog.getByRole("link", { name: /待我审批/ })).toHaveCount(0);
await expect(welcomeDialog.getByRole("link", { name: /报销/ })).toBeVisible();
await expect(welcomeDialog.getByRole("link", { name: /请假/ })).toBeVisible();
await expect(welcomeDialog.getByRole("link", { name: /个人设置/ })).not.toBeVisible();
await expect(welcomeDialog.getByRole("link", { name: /通知中心/ })).not.toBeVisible();