PC6 完善 PC 工作台与审批信息架构

这个提交包含在:
2026-05-12 02:34:15 +08:00
父节点 e7bf57380e
当前提交 aec660a8fe
共修改 13 个文件,包含 518 行新增和 74 行删除
+14
查看文件
@@ -71,6 +71,20 @@ export type PortalHomePayload = {
}
| null;
peopleLifecycle: { onboardingCount: number; offboardingCount: number; assetReturnPendingCount: number; href: string } | null;
roleCapabilities?: {
canSubmitApplications: boolean;
canApproveTasks: boolean;
canUseFinance: boolean;
canManageSystem: boolean;
canViewTeam: boolean;
isAdminOnly: boolean;
};
financeSummary?: {
pendingBatchCount: number;
failedItemCount: number;
riskCount: number;
href: string;
} | null;
adoptionSummary:
| {
activeEmployeeCount: number;
+1
查看文件
@@ -203,6 +203,7 @@ export type WorkflowApproverListResponse = {
export type WorkflowTaskListQuery = {
status?: "pending" | "completed" | "rejected" | "cancelled" | "skipped";
entityType?: "reimbursement" | "leave" | "purchase_request" | "travel_request";
page?: number;
pageSize?: number;
};
+4 -4
查看文件
@@ -480,7 +480,7 @@ export function ApOperationsPage() {
}),
onSuccess: (response, format) => {
downloadBlob(response.blob, response.filename ?? `ap-operations.${format}`);
toast.success(`AP 运营数据已导出为 ${format.toUpperCase()}`);
toast.success(`财务处理数据已导出为 ${format.toUpperCase()}`);
},
onError: (error) => {
toast.error(resolveErrorMessage(error, "导出失败"));
@@ -533,14 +533,14 @@ export function ApOperationsPage() {
};
if (summaryQuery.isLoading && paymentBatchesQuery.isLoading && riskFlagsQuery.isLoading) {
return <div className="card-std p-6 text-sm text-zinc-500">加载 AP 运营工作台...</div>;
return <div className="card-std p-6 text-sm text-zinc-500">加载财务工作台...</div>;
}
return (
<div className="space-y-6">
<PageHeader
title="AP 运营"
subtitle="将待付款批次、异常付款、风险标记和对账异常收口在同一工作台内处理。"
title="财务付款与对账"
subtitle="集中处理待付款批次、异常付款、风险标记和对账异常。"
actions={
<>
<Button
+176 -8
查看文件
@@ -74,6 +74,11 @@ type GettingStartedChecklistItem = {
const LEAVE_PORTAL_PERMISSIONS = ["leave.view_own", "leave.view_all", "leave.create", "leave.submit"];
const EXPENSE_PORTAL_PERMISSIONS = ["expense.view_own", "expense.view_all", "expense.create", "expense.proxy_manage"];
const PROCUREMENT_PORTAL_PERMISSIONS = ["procurement.view_own", "procurement.view_all", "procurement.create", "procurement.submit"];
const TRAVEL_PORTAL_PERMISSIONS = ["travel.view_own", "travel.view_all", "travel.create", "travel.submit"];
const APPROVAL_PORTAL_PERMISSIONS = ["expense.approve", "leave.approve", "procurement.approve", "travel.approve"];
const FINANCE_PORTAL_PERMISSIONS = ["payment.batch.manage", "risk.review.manage", "expense.finance.pay"];
const SYSTEM_PORTAL_PERMISSIONS = ["system.users", "system.settings", "system.audit.view", "notification.channel.manage"];
const TODO_SEVERITY_RANK = {
high: 3,
warning: 2,
@@ -90,6 +95,28 @@ function severityTone(severity?: "normal" | "warning" | "high") {
return "neutral" as const;
}
function buildFallbackRoleCapabilities(permissions: string[], roleCodes: string[]) {
const hasAdminRole = roleCodes.includes("admin") || hasAnyPermission(permissions, ["*"]);
const hasBusinessApproverRole = roleCodes.some((roleCode) =>
["manager", "ceo", "finance", "finance_mgr", "hr"].includes(roleCode),
);
const hasFinanceRole = roleCodes.some((roleCode) => ["finance", "finance_mgr"].includes(roleCode));
return {
canSubmitApplications: hasAnyPermission(permissions, [
...LEAVE_PORTAL_PERMISSIONS,
...EXPENSE_PORTAL_PERMISSIONS,
...PROCUREMENT_PORTAL_PERMISSIONS,
...TRAVEL_PORTAL_PERMISSIONS,
]),
canApproveTasks: hasBusinessApproverRole && hasAnyPermission(permissions, APPROVAL_PORTAL_PERMISSIONS),
canUseFinance: hasFinanceRole && hasAnyPermission(permissions, FINANCE_PORTAL_PERMISSIONS),
canManageSystem: hasAdminRole || hasAnyPermission(permissions, SYSTEM_PORTAL_PERMISSIONS),
canViewTeam: hasBusinessApproverRole || hasAnyPermission(permissions, ["report.dashboard.view", "system.users", "project.manage"]),
isAdminOnly: hasAdminRole && !hasBusinessApproverRole,
};
}
function pickCards(cards: PortalHomePayload["cards"], keys: string[]) {
const map = new Map(cards.map((card) => [card.key, card]));
return keys.map((key) => map.get(key)).filter((card): card is PortalHomePayload["cards"][number] => Boolean(card));
@@ -376,6 +403,38 @@ function SummaryPanel({ title, items }: { title: string; items: SummaryItem[] })
);
}
function FinanceSummaryPanel({ summary }: { summary: PortalHomePayload["financeSummary"] }) {
if (!summary) {
return null;
}
const items: SummaryItem[] = [
{
key: "finance-pending-batches",
href: summary.href,
label: "待付款批次",
value: `${summary.pendingBatchCount} 个`,
tone: summary.pendingBatchCount > 0 ? "warning" : "normal",
},
{
key: "finance-failed-items",
href: summary.href,
label: "异常付款",
value: `${summary.failedItemCount} 项`,
tone: summary.failedItemCount > 0 ? "warning" : "normal",
},
{
key: "finance-risk-flags",
href: summary.href,
label: "待复核风险",
value: `${summary.riskCount} 项`,
tone: summary.riskCount > 0 ? "warning" : "normal",
},
];
return <SummaryPanel title="财务待处理" items={items} />;
}
function AnnouncementsList({ announcements }: { announcements: PortalHomePayload["announcements"] }) {
const safeAnnouncements = Array.isArray(announcements) ? announcements : [];
@@ -817,7 +876,6 @@ export function PortalHomePage() {
retry: (failureCount, error) => !isAttendanceFeatureDisabled(error) && failureCount < 2,
});
const canSetupWorkspace = hasAnyPermission(permissions, ["system.settings"]);
const isManagerView = isManagerRole(permissions);
useEffect(() => {
setShowWelcome(shouldShowWelcomeDialog());
@@ -910,6 +968,20 @@ export function PortalHomePage() {
const quickLinkItems = Array.isArray(payload.quickLinks) ? payload.quickLinks : [];
const recentActivities = Array.isArray(payload.recentActivities) ? payload.recentActivities : [];
const teamStatusBoard = Array.isArray(payload.teamStatusBoard) ? payload.teamStatusBoard : [];
const roleCapabilities = payload.roleCapabilities ?? buildFallbackRoleCapabilities(permissions, me?.roleCodes ?? []);
const isManagerView =
roleCapabilities.canViewTeam ||
roleCapabilities.canApproveTasks ||
roleCapabilities.canUseFinance ||
roleCapabilities.canManageSystem ||
isManagerRole(permissions);
const workspaceEyebrow = roleCapabilities.canUseFinance
? "财务工作台"
: roleCapabilities.canManageSystem
? "管理工作台"
: roleCapabilities.canApproveTasks
? "审批工作台"
: "个人工作台";
const portalSchema = isManagerView ? managerPortalSchema : employeePortalSchema;
const primaryCards = pickCards(cards, portalSchema.heroCards);
const primaryCardKeySet = primaryCards.map((card) => card.key);
@@ -986,8 +1058,87 @@ export function PortalHomePage() {
: null,
] as Array<SummaryItem | null>).filter(isSummaryItem);
const approvalSummaryItems = ([
roleCapabilities.canApproveTasks
? {
key: "approval-pending",
href: payload.pendingTasks.href,
label: "审批待办",
value: `${payload.pendingTasks.count} 条`,
note: payload.pendingTasks.count > 0 ? "进入审批中心处理。" : "暂无待处理审批。",
tone: payload.pendingTasks.count > 0 ? "warning" as const : "normal" as const,
}
: null,
roleCapabilities.canApproveTasks
? {
key: "approval-completed",
href: "/workflow/tasks?status=completed",
label: "已处理",
value: "查看",
note: "复核历史处理记录。",
}
: null,
] as Array<SummaryItem | null>).filter(isSummaryItem);
const systemSummaryItems = ([
roleCapabilities.canManageSystem
? {
key: "system-users",
href: "/people",
label: "账号与人员",
value: "管理",
note: "维护账号、角色和组织信息。",
}
: null,
roleCapabilities.canManageSystem
? {
key: "system-workspace",
href: "/system/workspace",
label: "工作区设置",
value: "配置",
note: "检查入口、通知和基础配置。",
}
: null,
roleCapabilities.canManageSystem
? {
key: "system-audit",
href: "/audit",
label: "审计日志",
value: "查看",
note: "查询关键操作留痕。",
}
: null,
] as Array<SummaryItem | null>).filter(isSummaryItem);
const quickActions: QuickActionConfig[] = [];
if (hasAnyPermission(permissions, LEAVE_PORTAL_PERMISSIONS)) {
if (roleCapabilities.canApproveTasks) {
quickActions.push({
key: "approval",
icon: "fact_check",
label: "处理审批",
description: "进入审批中心查看待办",
to: "/workflow/tasks",
});
}
if (roleCapabilities.canUseFinance && payload.financeSummary) {
quickActions.push({
key: "finance",
icon: "payments",
label: "财务处理",
description: "查看付款和风险待处理",
to: payload.financeSummary.href,
});
}
if (roleCapabilities.canManageSystem) {
quickActions.push({
key: "system",
icon: "admin_panel_settings",
label: "系统管理",
description: "账号、配置和审计入口",
to: "/people",
});
}
if (!roleCapabilities.isAdminOnly && hasAnyPermission(permissions, LEAVE_PORTAL_PERMISSIONS)) {
quickActions.push({
key: "leave",
icon: "event_available",
@@ -996,7 +1147,7 @@ export function PortalHomePage() {
to: "/leave-requests?action=create",
});
}
if (hasAnyPermission(permissions, EXPENSE_PORTAL_PERMISSIONS)) {
if (!roleCapabilities.isAdminOnly && hasAnyPermission(permissions, EXPENSE_PORTAL_PERMISSIONS)) {
quickActions.push({
key: "expense",
icon: "request_quote",
@@ -1036,10 +1187,16 @@ export function PortalHomePage() {
</ErrorBoundary>
<WorkbenchHero
eyebrow={isManagerView ? "管理工作台" : "个人工作台"}
eyebrow={workspaceEyebrow}
title={`你好,${payload.greeting.name}`}
className="workbench-hero--compact"
subtitle={isManagerView ? "审批、团队、周报。" : "待办、打卡、申请。"}
subtitle={roleCapabilities.canUseFinance
? "付款、风险、审批。"
: roleCapabilities.canManageSystem
? "配置、审计、通知。"
: isManagerView
? "审批、团队、周报。"
: "待办、打卡、申请。"}
actions={
!payload.workspace.persisted && canSetupWorkspace ? (
<Button asChild variant="secondary" type="button">
@@ -1117,11 +1274,22 @@ export function PortalHomePage() {
{isManagerView ? (
<>
<SecondaryWorkspacePanel
title="今日管理重点 / 常用入口"
title="角色重点 / 常用入口"
description="按需展开。"
>
<div className="grid gap-6 xl:grid-cols-[minmax(0,1fr),360px]">
<SummaryPanel title="今日管理重点" items={managerSummaryItems} />
<div className="grid gap-6 xl:grid-cols-[minmax(0,1fr),minmax(0,1fr)] 2xl:grid-cols-[minmax(0,1fr),minmax(0,1fr),360px]">
{roleCapabilities.canApproveTasks ? (
<SummaryPanel title="审批处理" items={approvalSummaryItems} />
) : null}
{roleCapabilities.canUseFinance ? (
<FinanceSummaryPanel summary={payload.financeSummary ?? null} />
) : null}
{roleCapabilities.canManageSystem ? (
<SummaryPanel title="系统管理" items={systemSummaryItems} />
) : null}
{roleCapabilities.canViewTeam ? (
<SummaryPanel title="今日团队状态" items={managerSummaryItems} />
) : null}
<QuickLinksPanel items={quickLinkItems} />
</div>
+25 -1
查看文件
@@ -981,6 +981,25 @@ export function PurchaseRequestsPage() {
</div>
</div>
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
<div className="rounded-lg border border-zinc-200 bg-white px-4 py-3 text-sm">
<div className="text-zinc-500">采购状态</div>
<div className="mt-2"><StatusBadge status={detail.status} /></div>
</div>
<div className="rounded-lg border border-zinc-200 bg-white px-4 py-3 text-sm">
<div className="text-zinc-500">总金额</div>
<div className="mt-2 font-semibold text-zinc-900">{totalAmountPreview}</div>
</div>
<div className="rounded-lg border border-zinc-200 bg-white px-4 py-3 text-sm">
<div className="text-zinc-500">当前节点</div>
<div className="mt-2 font-medium text-zinc-900">{detail.pendingWorkflow?.task?.stepName ?? "-"}</div>
</div>
<div className="rounded-lg border border-zinc-200 bg-white px-4 py-3 text-sm">
<div className="text-zinc-500">附件</div>
<div className="mt-2 font-medium text-zinc-900">{detail.attachments.length} 个</div>
</div>
</div>
<RiskFlagsCard items={detail.riskFlags} title="待处理风险项" />
<div className="grid gap-4 md:grid-cols-2">
@@ -1059,10 +1078,15 @@ export function PurchaseRequestsPage() {
<Textarea rows={4} value={reason} disabled={!canEditDraft} onChange={(event) => setReason(event.target.value)} />
</div>
<div className="rounded-lg border border-zinc-200 p-4 text-sm text-zinc-600">
<div className="rounded-lg border border-zinc-200 bg-zinc-50 p-4 text-sm text-zinc-600">
<div>预算池:{detail.budgetPool?.name ?? "待提交时自动解析"}</div>
<div>待办节点:{detail.pendingWorkflow?.task?.stepName ?? "-"}</div>
<div>总金额:<span className="font-semibold text-zinc-900">{totalAmountPreview}</span></div>
{!detail.budgetPool ? (
<div className="mt-2 rounded-md border border-amber-200 bg-amber-50 px-2 py-1 text-amber-700">
未匹配预算池时仍会按当前规则提交审批;如需强预算校验,请由管理员补充预算配置。
</div>
) : null}
</div>
<div className="space-y-3">
+108 -43
查看文件
@@ -14,6 +14,35 @@ import { Tabs, TabsList, TabsTrigger } from "../components/ui/tabs";
import { TEST_IDS } from "../lib/testIds";
const PAGE_SIZE = 20;
const WORKFLOW_STATUS_OPTIONS = [
{ value: "pending", label: "待处理" },
{ value: "completed", label: "已处理" },
{ value: "rejected", label: "已驳回" },
{ value: "cancelled", label: "已取消" },
{ value: "skipped", label: "已跳过" },
] as const;
const WORKFLOW_ENTITY_OPTIONS = [
{ value: "all", label: "全部业务" },
{ value: "reimbursement", label: "报销" },
{ value: "leave", label: "请假" },
{ value: "purchase_request", label: "采购" },
{ value: "travel_request", label: "差旅" },
] as const;
type WorkflowStatusFilter = (typeof WORKFLOW_STATUS_OPTIONS)[number]["value"];
type WorkflowEntityFilter = (typeof WORKFLOW_ENTITY_OPTIONS)[number]["value"];
function normalizeStatusFilter(value: string | null): WorkflowStatusFilter {
return WORKFLOW_STATUS_OPTIONS.some((item) => item.value === value)
? (value as WorkflowStatusFilter)
: "pending";
}
function normalizeEntityFilter(value: string | null): WorkflowEntityFilter {
return WORKFLOW_ENTITY_OPTIONS.some((item) => item.value === value)
? (value as WorkflowEntityFilter)
: "all";
}
function entityLabel(entityType: string | undefined) {
if (entityType === "leave") {
@@ -82,19 +111,36 @@ export function WorkflowTasksPage() {
const [searchParams, setSearchParams] = useSearchParams();
const queryClient = useQueryClient();
const status =
(searchParams.get("status") as
| "pending"
| "completed"
| "rejected"
| "cancelled"
| "skipped"
| null) ?? "pending";
const status = normalizeStatusFilter(searchParams.get("status"));
const entityType = normalizeEntityFilter(searchParams.get("entityType"));
const page = Number(searchParams.get("page") ?? "1") || 1;
function updateSearchParams(next: {
status?: WorkflowStatusFilter;
entityType?: WorkflowEntityFilter;
page?: number;
}) {
const nextStatus = next.status ?? status;
const nextEntityType = next.entityType ?? entityType;
const params: Record<string, string> = {
status: nextStatus,
page: String(next.page ?? page),
};
if (nextEntityType !== "all") {
params.entityType = nextEntityType;
}
setSearchParams(params);
}
const query = useQuery({
queryKey: ["workflow", "tasks", status, page],
queryFn: () => fetchWorkflowTasks({ status, page, pageSize: PAGE_SIZE }),
queryKey: ["workflow", "tasks", status, entityType, page],
queryFn: () =>
fetchWorkflowTasks({
status,
entityType: entityType === "all" ? undefined : entityType,
page,
pageSize: PAGE_SIZE,
}),
});
const approveMutation = useMutation({
@@ -125,32 +171,47 @@ export function WorkflowTasksPage() {
return (
<div className="space-y-4 md:space-y-6">
<PageHeader title="待我处理" subtitle="待处理审批。" />
<PageHeader title="审批中心" subtitle="集中处理待办、查看已办和业务审批历史。" />
<div className="card-std space-y-4 p-4 md:p-5">
<Tabs
data-testid="workflow-tasks-status-tabs"
value={status}
onValueChange={(value) => setSearchParams({ status: value, page: "1" })}
>
<TabsList className="flex w-full flex-wrap justify-start gap-1">
{[
{ value: "pending", label: "待处理" },
{ value: "completed", label: "已处理" },
{ value: "rejected", label: "已驳回" },
{ value: "cancelled", label: "已取消" },
{ value: "skipped", label: "已跳过" },
].map((item) => (
<TabsTrigger
data-testid={`workflow-tasks-tab-${item.value}`}
key={item.value}
value={item.value}
>
{item.label}
</TabsTrigger>
))}
</TabsList>
</Tabs>
<div className="flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between">
<Tabs
data-testid="workflow-tasks-status-tabs"
value={status}
onValueChange={(value) => updateSearchParams({ status: value as WorkflowStatusFilter, page: 1 })}
>
<TabsList className="flex w-full flex-wrap justify-start gap-1 xl:w-auto">
{WORKFLOW_STATUS_OPTIONS.map((item) => (
<TabsTrigger
data-testid={`workflow-tasks-tab-${item.value}`}
key={item.value}
value={item.value}
>
{item.label}
</TabsTrigger>
))}
</TabsList>
</Tabs>
<div className="flex items-center gap-2">
<label className="text-sm font-medium text-zinc-700" htmlFor="workflow-entity-type-filter">
业务类型
</label>
<select
id="workflow-entity-type-filter"
className="select-std min-w-36"
data-testid="workflow-tasks-entity-filter"
value={entityType}
onChange={(event) => updateSearchParams({ entityType: event.target.value as WorkflowEntityFilter, page: 1 })}
>
{WORKFLOW_ENTITY_OPTIONS.map((item) => (
<option key={item.value} value={item.value}>
{item.label}
</option>
))}
</select>
</div>
</div>
{query.isLoading ? <TableSkeleton columns={7} rows={5} /> : null}
@@ -184,6 +245,7 @@ export function WorkflowTasksPage() {
<div className="mt-3 grid gap-1 rounded-lg bg-zinc-50 px-3 py-2 text-xs text-zinc-600">
<div>申请人:{displayApplicant(task)}</div>
<div>金额/天数:{displayMetric(task)}</div>
<div>当前节点:{task.stepName}</div>
<div>业务状态:{businessStatusLabel(task.businessSummary?.businessStatus)}</div>
</div>
<div className="mt-3 grid grid-cols-2 gap-2">
@@ -224,12 +286,13 @@ export function WorkflowTasksPage() {
<Table data-testid="workflow-tasks-table">
<TableHeader>
<tr>
<TableHead>审批事项</TableHead>
<TableHead>业务类型 / 编号</TableHead>
<TableHead>标题</TableHead>
<TableHead>申请人</TableHead>
<TableHead>金额/天数</TableHead>
<TableHead>当前节点</TableHead>
<TableHead>业务状态</TableHead>
<TableHead>任务状态</TableHead>
<TableHead>创建时间</TableHead>
<TableHead>更新时间</TableHead>
<TableHead className="w-24">操作</TableHead>
</tr>
</TableHeader>
@@ -237,16 +300,18 @@ export function WorkflowTasksPage() {
{taskItems.length ? (
taskItems.map((task) => (
<TableRow data-testid="workflow-task-row" key={task.id}>
<TableCell>
<div className="font-medium text-zinc-900">{entityLabel(task.workflowInstance?.entityType)}</div>
<div className="text-xs text-zinc-500">{displayReference(task)}</div>
</TableCell>
<TableCell>
<div className="font-medium text-zinc-900">{displayTitle(task)}</div>
<div className="text-xs text-zinc-500">
{entityLabel(task.workflowInstance?.entityType)} · {displayReference(task)}
</div>
<div className="mt-1"><StatusBadge status={task.status} /></div>
</TableCell>
<TableCell>{displayApplicant(task)}</TableCell>
<TableCell>{displayMetric(task)}</TableCell>
<TableCell>{task.stepName}</TableCell>
<TableCell>{businessStatusLabel(task.businessSummary?.businessStatus)}</TableCell>
<TableCell><StatusBadge status={task.status} /></TableCell>
<TableCell>{new Date(task.createdAt).toLocaleString()}</TableCell>
<TableCell>
<div className="flex flex-wrap gap-2">
@@ -275,7 +340,7 @@ export function WorkflowTasksPage() {
))
) : (
<TableRow>
<TableCell className="py-3 text-zinc-500" colSpan={7}>
<TableCell className="py-3 text-zinc-500" colSpan={8}>
暂无数据
</TableCell>
</TableRow>
@@ -289,7 +354,7 @@ export function WorkflowTasksPage() {
<Pagination
page={page}
totalPages={totalPages}
onPageChange={(p) => setSearchParams({ status, page: String(p) })}
onPageChange={(p) => updateSearchParams({ page: p })}
testIdPrefix="workflow-tasks"
/>
</div>
+15 -3
查看文件
@@ -1,6 +1,6 @@
import { expect, test, type Page, type TestInfo } from "@playwright/test";
import { EMPLOYEE, login, logout, type E2EUser } from "./helpers/auth";
import { EMPLOYEE, FINANCE, login, logout, type E2EUser } from "./helpers/auth";
const ADMIN: E2EUser = {
identifier: "[email protected]",
@@ -34,15 +34,27 @@ test.describe("visual portal", () => {
await page.setViewportSize({ width: 1440, height: 1200 });
await login(page, ADMIN);
await expect(page.locator('[aria-label="portal-card-pending_approvals"]')).toBeVisible();
await expect(page.locator('[aria-label="portal-card-pending_approvals"]')).toHaveCount(0);
await expect(page.locator('[aria-label="portal-card-team_presence"]')).toBeVisible();
await expect(page.locator('[aria-label="portal-card-weekly_missing"]')).toBeVisible();
await expect(page.locator('[aria-label="portal-card-weekly_help"]')).toBeVisible();
await expect(page.locator("summary").filter({ hasText: "今日管理重点 / 常用入口" })).toBeVisible();
await expect(page.locator("summary").filter({ hasText: "角色重点 / 常用入口" })).toBeVisible();
await expect(page.getByText("试点观测摘要")).toBeVisible();
await expect(page.getByRole("tab", { name: "公告" })).toBeVisible();
await attachScreenshot(page, testInfo, "portal-admin.png");
await logout(page, ADMIN.name);
});
test("财务 Portal 显示财务待处理入口", async ({ page }, testInfo) => {
await page.setViewportSize({ width: 1440, height: 1200 });
await login(page, FINANCE);
await expect(page.getByText("财务工作台")).toBeVisible();
await expect(page.getByText("财务处理")).toBeVisible();
await expect(page.locator("summary").filter({ hasText: "角色重点 / 常用入口" })).toBeVisible();
await attachScreenshot(page, testInfo, "portal-finance.png");
await logout(page, FINANCE.name);
});
});
@@ -84,6 +84,7 @@ test("E2E 审批列表快捷通过:经理可在待办列表行内直接通过
await login(page, MANAGER);
await (await openNavLink(page, "待我处理")).click();
await expect(page.getByTestId(TEST_IDS.workflow.tasksTable)).toBeVisible();
await page.getByTestId("workflow-tasks-entity-filter").selectOption("leave");
const taskRows = page.locator(
`[data-testid="${TEST_IDS.workflow.taskRow}"]:has(a[href="/workflow/tasks/${submitResult.currentTaskId}"])`,