fix(pc-detail-responsive-approval-polish): 协调详情断点并收敛审批展示
这个提交包含在:
@@ -3,7 +3,7 @@ import { NavLinks } from "./NavLinks";
|
||||
|
||||
export function Sidebar() {
|
||||
return (
|
||||
<aside className="app-shell-sidebar fixed inset-y-0 z-20 hidden w-72 overflow-hidden border-r border-slate-200 lg:flex lg:flex-col">
|
||||
<aside className="app-shell-sidebar fixed inset-y-0 z-20 hidden w-72 overflow-hidden border-r border-slate-200 xl:flex xl:flex-col">
|
||||
<div className="border-b border-slate-200 px-6 py-5">
|
||||
<AppBrand />
|
||||
</div>
|
||||
|
||||
@@ -143,7 +143,7 @@ export function Topbar() {
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<Dialog open={mobileNavOpen} onOpenChange={setMobileNavOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="lg:hidden" size="icon" variant="secondary" aria-label="打开菜单" type="button">
|
||||
<Button className="xl:hidden" size="icon" variant="secondary" aria-label="打开菜单" type="button">
|
||||
<MenuIcon />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Children, type ReactNode } from "react";
|
||||
import { Children, Fragment, isValidElement, type ReactNode } from "react";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { SectionPanel } from "./SectionPanel";
|
||||
|
||||
@@ -71,6 +71,15 @@ export function BusinessDetailFact({
|
||||
);
|
||||
}
|
||||
|
||||
function hasVisibleNode(node: ReactNode): boolean {
|
||||
return Children.toArray(node).some((child) => {
|
||||
if (isValidElement(child) && child.type === Fragment) {
|
||||
return hasVisibleNode(child.props.children);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
export function BusinessDetailActionPanel({
|
||||
title = "操作",
|
||||
status,
|
||||
@@ -92,21 +101,29 @@ export function BusinessDetailActionPanel({
|
||||
className?: string;
|
||||
testId?: string;
|
||||
}) {
|
||||
const visibleChildren = Children.toArray(children);
|
||||
const hasActionContent = Boolean(primaryAction || secondaryActions || moreActions);
|
||||
const hasChildren = visibleChildren.length > 0;
|
||||
const hasContentBeforeSummary = Boolean(primaryAction || secondaryActions || moreActions || hasChildren);
|
||||
const hasStatus = hasVisibleNode(status);
|
||||
const hasPrimaryAction = hasVisibleNode(primaryAction);
|
||||
const hasSecondaryActions = hasVisibleNode(secondaryActions);
|
||||
const hasMoreActions = hasVisibleNode(moreActions);
|
||||
const hasSummary = hasVisibleNode(summary);
|
||||
const hasChildren = hasVisibleNode(children);
|
||||
const hasActionContent = hasPrimaryAction || hasSecondaryActions || hasMoreActions;
|
||||
const hasContentBeforeSummary = hasActionContent || hasChildren;
|
||||
|
||||
if (!hasStatus && !hasActionContent && !hasSummary && !hasChildren) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SectionPanel title={title} actions={status} className={cn("detail-action-panel lg:sticky lg:top-24", className)}>
|
||||
<div className="detail-action-stack" data-testid={testId}>
|
||||
{primaryAction ? <div className="grid gap-2">{primaryAction}</div> : null}
|
||||
{secondaryActions ? <div className="workflow-action-bar">{secondaryActions}</div> : null}
|
||||
{moreActions ? <div className="border-t border-zinc-200 pt-3">{moreActions}</div> : null}
|
||||
{hasPrimaryAction ? <div className="grid gap-2">{primaryAction}</div> : null}
|
||||
{hasSecondaryActions ? <div className="workflow-action-bar">{secondaryActions}</div> : null}
|
||||
{hasMoreActions ? <div className="border-t border-zinc-200 pt-3">{moreActions}</div> : null}
|
||||
{hasChildren ? (
|
||||
<div className={cn(hasActionContent && "border-t border-zinc-200 pt-3")}>{children}</div>
|
||||
) : null}
|
||||
{summary ? (
|
||||
{hasSummary ? (
|
||||
<div className={cn(hasContentBeforeSummary && "border-t border-zinc-200 pt-3")}>
|
||||
<div className="space-y-2 text-sm text-zinc-600">{summary}</div>
|
||||
</div>
|
||||
|
||||
@@ -66,7 +66,7 @@ export function AppShellLayout() {
|
||||
{isMobileRoute ? null : <Sidebar />}
|
||||
|
||||
<main
|
||||
className={`app-shell-main flex min-w-0 flex-1 flex-col ${isMobileRoute ? "hyb-mobile-shell overflow-hidden" : "lg:ml-72"}`}
|
||||
className={`app-shell-main flex min-w-0 flex-1 flex-col ${isMobileRoute ? "hyb-mobile-shell overflow-hidden" : "xl:ml-72"}`}
|
||||
style={isMobileRoute ? { height: "var(--mobile-viewport-height, 100vh)", maxHeight: "var(--mobile-viewport-height, 100vh)", minHeight: "var(--mobile-viewport-height, 100vh)" } : undefined}
|
||||
>
|
||||
{isMobileRoute ? null : <Topbar />}
|
||||
|
||||
@@ -52,6 +52,22 @@ export function workflowActionLabel(action: string | null | undefined) {
|
||||
return action ? labels[action] ?? action : "-";
|
||||
}
|
||||
|
||||
export function approvalRoleLabel(role: string | null | undefined) {
|
||||
if (!role?.trim()) {
|
||||
return "-";
|
||||
}
|
||||
|
||||
const labels: Record<string, string> = {
|
||||
manager: "主管",
|
||||
ceo: "负责人",
|
||||
finance: "财务",
|
||||
finance_mgr: "财务经理",
|
||||
hr: "人事",
|
||||
};
|
||||
|
||||
return labels[role.trim().toLowerCase()] ?? "审批人";
|
||||
}
|
||||
|
||||
export function formatApprovalHistoryTime(value: string | null | undefined) {
|
||||
return value ? value.slice(0, 19).replace("T", " ") : "-";
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ import { PageScaffold } from "../components/patterns/PageScaffold";
|
||||
import { SectionPanel } from "../components/patterns/SectionPanel";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Textarea } from "../components/ui/textarea";
|
||||
import { resolvePurchaseDisplayCode, workflowActionLabel } from "../lib/approvalDisplay";
|
||||
import { approvalRoleLabel, resolvePurchaseDisplayCode, workflowActionLabel } from "../lib/approvalDisplay";
|
||||
import { formatDisplayDateTime } from "../lib/displayFormat";
|
||||
import {
|
||||
buildPcDetailSource,
|
||||
@@ -254,13 +254,16 @@ export function WorkflowTaskDetailPage() {
|
||||
onClick={() => navigate(backSource, { replace: true })}
|
||||
type="button"
|
||||
>
|
||||
{backSource.startsWith("/reimbursements") ? "返回报销列表" : "返回待办列表"}
|
||||
返回审批列表
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const detail = detailQuery.data;
|
||||
const canApprove = allowedActions.has("approve");
|
||||
const canReturn = allowedActions.has("return");
|
||||
const canReject = allowedActions.has("reject");
|
||||
const canTransfer = allowedActions.has("transfer");
|
||||
const allowedActionLabels = detail.allowedActions.map(workflowActionLabel).join(" / ");
|
||||
const businessStatusLabel = workflowInstanceStatusLabel(detail.workflowInstance?.currentStatus);
|
||||
@@ -281,7 +284,7 @@ export function WorkflowTaskDetailPage() {
|
||||
actions={(
|
||||
<Button asChild variant="secondary" data-testid="workflow-task-back">
|
||||
<Link replace to={backSource}>
|
||||
{backSource.startsWith("/reimbursements") ? "返回报销列表" : "返回待办列表"}
|
||||
返回审批列表
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
@@ -294,7 +297,9 @@ export function WorkflowTaskDetailPage() {
|
||||
<StatStrip columns={4} className="ops-status-strip">
|
||||
<MetricCard title="当前节点" value={<span className="text-base">{detail.task.stepName}</span>} />
|
||||
<MetricCard title="业务类型" value={<span className="text-base">{entityTitle(detail.workflowInstance?.entityType)}</span>} />
|
||||
<MetricCard title="可执行动作" value={<span className="text-base">{allowedActionLabels || "-"}</span>} />
|
||||
{allowedActionLabels ? (
|
||||
<MetricCard title="可执行动作" value={<span className="text-base">{allowedActionLabels}</span>} />
|
||||
) : null}
|
||||
<MetricCard title="创建时间" value={<span className="text-base">{formatDisplayDateTime(detail.task.createdAt)}</span>} />
|
||||
</StatStrip>
|
||||
|
||||
@@ -431,7 +436,7 @@ export function WorkflowTaskDetailPage() {
|
||||
{workflowActionLabel(item.action)} · {item.actorName || item.actorId}
|
||||
</div>
|
||||
<div className="text-zinc-600">{item.comment || "-"}</div>
|
||||
<div className="text-xs text-zinc-500">{item.createdAt || "-"}</div>
|
||||
<div className="text-xs text-zinc-500">{formatDisplayDateTime(item.createdAt)}</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
@@ -445,53 +450,59 @@ export function WorkflowTaskDetailPage() {
|
||||
<BusinessDetailActionPanel
|
||||
title="处理"
|
||||
className="workflow-action-panel"
|
||||
secondaryActions={(
|
||||
primaryAction={canApprove ? (
|
||||
<Button
|
||||
data-testid="workflow-task-approve"
|
||||
disabled={mutation.isPending}
|
||||
onClick={() => requestWorkflowAction("approve")}
|
||||
type="button"
|
||||
>
|
||||
通过
|
||||
</Button>
|
||||
) : null}
|
||||
secondaryActions={canReturn || canReject ? (
|
||||
<>
|
||||
<Button
|
||||
data-testid="workflow-task-approve"
|
||||
disabled={!allowedActions.has("approve") || mutation.isPending}
|
||||
onClick={() => requestWorkflowAction("approve")}
|
||||
type="button"
|
||||
>
|
||||
通过
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="workflow-task-return-toggle"
|
||||
variant="secondary"
|
||||
disabled={!allowedActions.has("return") || mutation.isPending}
|
||||
onClick={() => setActiveSecondaryAction(activeSecondaryAction === "return" ? null : "return")}
|
||||
type="button"
|
||||
>
|
||||
退回
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="workflow-task-reject-toggle"
|
||||
variant="secondary"
|
||||
disabled={!allowedActions.has("reject") || mutation.isPending}
|
||||
onClick={() => setActiveSecondaryAction(activeSecondaryAction === "reject" ? null : "reject")}
|
||||
type="button"
|
||||
>
|
||||
驳回
|
||||
</Button>
|
||||
{canTransfer ? (
|
||||
<details className="workflow-action-more">
|
||||
<summary>更多</summary>
|
||||
<button
|
||||
data-testid="workflow-task-transfer-menu-item"
|
||||
type="button"
|
||||
onClick={() => setActiveSecondaryAction("transfer")}
|
||||
>
|
||||
转审
|
||||
</button>
|
||||
</details>
|
||||
{canReturn ? (
|
||||
<Button
|
||||
data-testid="workflow-task-return-toggle"
|
||||
variant="secondary"
|
||||
disabled={mutation.isPending}
|
||||
onClick={() => setActiveSecondaryAction(activeSecondaryAction === "return" ? null : "return")}
|
||||
type="button"
|
||||
>
|
||||
退回
|
||||
</Button>
|
||||
) : null}
|
||||
{canReject ? (
|
||||
<Button
|
||||
data-testid="workflow-task-reject-toggle"
|
||||
variant="secondary"
|
||||
disabled={mutation.isPending}
|
||||
onClick={() => setActiveSecondaryAction(activeSecondaryAction === "reject" ? null : "reject")}
|
||||
type="button"
|
||||
>
|
||||
驳回
|
||||
</Button>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
) : null}
|
||||
moreActions={canTransfer ? (
|
||||
<details className="workflow-action-more">
|
||||
<summary>更多</summary>
|
||||
<button
|
||||
data-testid="workflow-task-transfer-menu-item"
|
||||
type="button"
|
||||
onClick={() => setActiveSecondaryAction("transfer")}
|
||||
>
|
||||
转审
|
||||
</button>
|
||||
</details>
|
||||
) : null}
|
||||
summary={(
|
||||
<>
|
||||
<div>节点:{detail.task.stepName}</div>
|
||||
<div>实例状态:{workflowInstanceStatusLabel(detail.workflowInstance?.currentStatus)}</div>
|
||||
<div>当前审批角色:{detail.task.assigneeRole ?? "-"}</div>
|
||||
<div>当前审批角色:{approvalRoleLabel(detail.task.assigneeRole)}</div>
|
||||
<div>创建时间:{formatDisplayDateTime(detail.task.createdAt)}</div>
|
||||
<div>完成时间:{formatDisplayDateTime(detail.task.completedAt)}</div>
|
||||
<div>发起时间:{formatDisplayDateTime(detail.workflowInstance?.startedAt)}</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { mkdirSync, readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { Page, Route } from "@playwright/test";
|
||||
|
||||
@@ -7,10 +7,6 @@ import { expect, test } from "./helpers/fixtures";
|
||||
test.use({ serviceWorkers: "block", timezoneId: "America/Los_Angeles" });
|
||||
|
||||
const outputDir = path.resolve(process.cwd(), "../output/playwright/pc-detail-mobile-visual-wave3");
|
||||
const taskOutputDir = path.resolve(
|
||||
process.cwd(),
|
||||
"../output/playwright/07-18-small-team-internal-readiness",
|
||||
);
|
||||
|
||||
const visualUser = {
|
||||
identifier: "[email protected]",
|
||||
@@ -24,7 +20,7 @@ const visualMe = {
|
||||
name: visualUser.name,
|
||||
email: visualUser.identifier,
|
||||
phone: null,
|
||||
roleCodes: ["employee", "manager", "finance"],
|
||||
roleCodes: ["employee", "manager", "finance_mgr"],
|
||||
permissions: [
|
||||
"expense.create",
|
||||
"expense.submit",
|
||||
@@ -39,6 +35,10 @@ const visualMe = {
|
||||
"invoice.manage",
|
||||
"payment.batch.manage",
|
||||
"risk.review.manage",
|
||||
"asset.view_own",
|
||||
"asset.view_all",
|
||||
"asset.manage",
|
||||
"asset.transfer",
|
||||
"leave.view_own",
|
||||
"attendance.view_own",
|
||||
],
|
||||
@@ -255,6 +255,77 @@ const workflowDetail = {
|
||||
],
|
||||
};
|
||||
|
||||
const completedWorkflowDetail = {
|
||||
...workflowDetail,
|
||||
task: {
|
||||
...workflowDetail.task,
|
||||
id: "task-wave3-completed",
|
||||
assigneeRole: "finance",
|
||||
status: "completed",
|
||||
outcome: "approved",
|
||||
completedAt: "2026-05-27T10:10:00.000Z",
|
||||
},
|
||||
workflowInstance: {
|
||||
...workflowDetail.workflowInstance,
|
||||
currentStatus: "approved",
|
||||
completedAt: "2026-05-27T10:10:00.000Z",
|
||||
},
|
||||
allowedActions: [],
|
||||
history: [
|
||||
...workflowDetail.history,
|
||||
{
|
||||
id: "workflow-history-wave3-completed",
|
||||
taskId: "task-wave3-completed",
|
||||
actorId: visualMe.id,
|
||||
actorName: visualMe.name,
|
||||
actorEmployeeNo: visualMe.employeeNo,
|
||||
action: "approve",
|
||||
comment: "资料已核对,同意通过",
|
||||
payload: null,
|
||||
createdAt: "2026-05-27T10:10:00.000Z",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const assetDetail = {
|
||||
id: "asset-wave3",
|
||||
assetNo: "ZC-2026-研发中心-000018",
|
||||
name: "研发中心高性能移动工作站与现场采集设备",
|
||||
assetType: "fixed_asset",
|
||||
sensitivityLevel: "internal",
|
||||
categoryId: "asset-category-wave3",
|
||||
legalEntityId: "legal-wave3",
|
||||
costCenterId: "cost-wave3",
|
||||
purchaseRequestId: null,
|
||||
serialNo: "SN-WAVE3-20260527-000018",
|
||||
location: "研发中心设备区 A-018",
|
||||
status: "in_use",
|
||||
assigneeEmployeeId: visualMe.id,
|
||||
ownerEmployeeId: visualMe.id,
|
||||
keeperEmployeeId: visualMe.id,
|
||||
archiveLocation: null,
|
||||
externalRef: null,
|
||||
sourceHash: null,
|
||||
verificationNo: "VER-2026-WAVE3-0018",
|
||||
confidentialityNote: null,
|
||||
metadata: null,
|
||||
sensitiveFieldsRedacted: false,
|
||||
lastVerifiedAt: "2026-05-27T09:20:00.000Z",
|
||||
expiresAt: null,
|
||||
activatedAt: "2026-05-20",
|
||||
retiredAt: null,
|
||||
createdAt: "2026-05-20T08:00:00.000Z",
|
||||
updatedAt: "2026-05-27T09:20:00.000Z",
|
||||
category: { id: "asset-category-wave3", code: "DEV", name: "研发设备", documentCategoryId: null, documentCategory: null },
|
||||
legalEntity: { id: "legal-wave3", code: "HYB", name: "本公司" },
|
||||
costCenter: { id: "cost-wave3", code: "RD", name: "研发中心", legalEntityId: "legal-wave3" },
|
||||
purchaseRequest: null,
|
||||
assignee: { id: visualMe.id, employeeNo: visualMe.employeeNo, name: visualMe.name, department: { id: "dept-wave3", name: "研发部" } },
|
||||
owner: { id: visualMe.id, employeeNo: visualMe.employeeNo, name: visualMe.name, department: { id: "dept-wave3", name: "研发部" } },
|
||||
keeper: { id: visualMe.id, employeeNo: visualMe.employeeNo, name: visualMe.name, department: { id: "dept-wave3", name: "研发部" } },
|
||||
permissions: { canEdit: true },
|
||||
};
|
||||
|
||||
const notifications = [
|
||||
{
|
||||
id: "notification-wave3-1",
|
||||
@@ -536,11 +607,6 @@ function screenshotPath(fileName: string) {
|
||||
return path.join(outputDir, fileName);
|
||||
}
|
||||
|
||||
function taskScreenshotPath(fileName: string) {
|
||||
mkdirSync(taskOutputDir, { recursive: true });
|
||||
return path.join(taskOutputDir, fileName);
|
||||
}
|
||||
|
||||
function navigationItem(key: string, href: string, label: string, icon: string, placement = "main") {
|
||||
return { key, href, label, icon, placement, permissionCodes: [], featureFlagKey: null };
|
||||
}
|
||||
@@ -960,6 +1026,11 @@ async function mockWave3Apis(page: Page) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathName === "/api/workflow/tasks/task-wave3-completed") {
|
||||
await fulfillJson(route, completedWorkflowDetail);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathName === "/api/workflow/tasks") {
|
||||
await fulfillJson(route, {
|
||||
page: 1,
|
||||
@@ -988,6 +1059,44 @@ async function mockWave3Apis(page: Page) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathName === "/api/asset-categories") {
|
||||
await fulfillJson(route, {
|
||||
includeArchived: false,
|
||||
items: [assetDetail.category],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathName === "/api/assets/assignees") {
|
||||
await fulfillJson(route, { items: [assetDetail.assignee] });
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathName === "/api/assets/asset-wave3") {
|
||||
await fulfillJson(route, { asset: assetDetail });
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathName === "/api/assets") {
|
||||
await fulfillJson(route, { mine: false, items: [assetDetail] });
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathName === "/api/master-data/legal-entities") {
|
||||
await fulfillJson(route, { includeArchived: false, items: [assetDetail.legalEntity] });
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathName === "/api/master-data/cost-centers") {
|
||||
await fulfillJson(route, { includeArchived: false, items: [assetDetail.costCenter] });
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathName === "/api/master-data/document-categories") {
|
||||
await fulfillJson(route, { includeArchived: false, items: [] });
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathName === "/api/portal/home") {
|
||||
await fulfillJson(route, {
|
||||
workspace: { id: "workspace-wave3", persisted: true, profile: "small_team_internal" },
|
||||
@@ -1085,6 +1194,71 @@ test.beforeEach(async ({ page }) => {
|
||||
await login(page);
|
||||
});
|
||||
|
||||
test("PC 壳层与详情断点保持结构一致", async () => {
|
||||
const appShell = readFileSync(path.resolve(process.cwd(), "src/layouts/AppShellLayout.tsx"), "utf8");
|
||||
const sidebar = readFileSync(path.resolve(process.cwd(), "src/components/navigation/Sidebar.tsx"), "utf8");
|
||||
const topbar = readFileSync(path.resolve(process.cwd(), "src/components/navigation/Topbar.tsx"), "utf8");
|
||||
const detailTemplate = readFileSync(path.resolve(process.cwd(), "src/components/patterns/BusinessDetailTemplate.tsx"), "utf8");
|
||||
const styles = readFileSync(path.resolve(process.cwd(), "src/index.css"), "utf8");
|
||||
|
||||
expect(appShell).toContain('"xl:ml-72"');
|
||||
expect(appShell).not.toContain('"lg:ml-72"');
|
||||
expect(sidebar).toContain("xl:flex xl:flex-col");
|
||||
expect(sidebar).not.toContain("lg:flex lg:flex-col");
|
||||
expect(topbar).toContain('className="xl:hidden"');
|
||||
expect(topbar).not.toContain('className="lg:hidden" size="icon" variant="secondary" aria-label="打开菜单"');
|
||||
expect(detailTemplate).toContain("detail-action-panel lg:sticky lg:top-24");
|
||||
expect(detailTemplate).toMatch(/if \(!hasStatus && !hasActionContent && !hasSummary && !hasChildren\) \{\s*return null;/u);
|
||||
expect(styles).toMatch(/\.detail-workspace-grid\s*\{[\s\S]*?lg:grid-cols-/u);
|
||||
expect(styles).toMatch(/\.workflow-action-panel\s*\{[\s\S]*?relative z-30/u);
|
||||
});
|
||||
|
||||
test("已完成审批只保留中文只读摘要", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1280, height: 800 });
|
||||
await page.goto("/workflow/tasks/task-wave3-completed");
|
||||
|
||||
await expect(page.getByTestId("workflow-task-action-panel")).toBeVisible();
|
||||
await expect(page.getByTestId("workflow-task-action-panel")).toContainText("当前审批角色:财务");
|
||||
await expect(page.getByTestId("workflow-task-action-panel")).toContainText("完成时间:2026年5月27日 18:10");
|
||||
await expect(page.getByTestId("workflow-task-overview").getByText("可执行动作", { exact: true })).toHaveCount(0);
|
||||
await expect(page.getByTestId("workflow-task-approve")).toHaveCount(0);
|
||||
await expect(page.getByTestId("workflow-task-return-toggle")).toHaveCount(0);
|
||||
await expect(page.getByTestId("workflow-task-reject-toggle")).toHaveCount(0);
|
||||
await expect(page.getByText("更多", { exact: true })).toHaveCount(0);
|
||||
await expect(page.getByText("通过 · 王顺理", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText("2026年5月27日 18:10", { exact: true })).toBeVisible();
|
||||
await expect(page.getByTestId("workflow-task-back")).toContainText("返回审批列表");
|
||||
});
|
||||
|
||||
test("1280px 资产详情保持可用双栏和可点击操作区", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1280, height: 800 });
|
||||
await page.goto("/assets?id=asset-wave3");
|
||||
|
||||
await expect(page.getByTestId("asset-name")).toHaveValue(assetDetail.name);
|
||||
await expect(page.getByTestId("asset-detail-action-panel")).toBeVisible();
|
||||
await expect(page.locator(".detail-action-panel")).toHaveCSS("position", "sticky");
|
||||
const assetLayout = await page.locator(".detail-workspace-grid").evaluate((workspace) => {
|
||||
const [main, aside] = Array.from(workspace.children) as HTMLElement[];
|
||||
return {
|
||||
mainWidth: main.getBoundingClientRect().width,
|
||||
asideWidth: aside.getBoundingClientRect().width,
|
||||
columns: getComputedStyle(workspace).gridTemplateColumns.split(" ").filter(Boolean).length,
|
||||
overflow: document.documentElement.scrollWidth - document.documentElement.clientWidth,
|
||||
};
|
||||
});
|
||||
expect(assetLayout.columns).toBe(2);
|
||||
expect(assetLayout.mainWidth).toBeGreaterThanOrEqual(560);
|
||||
expect(assetLayout.asideWidth).toBeGreaterThanOrEqual(280);
|
||||
expect(assetLayout.overflow).toBeLessThanOrEqual(2);
|
||||
|
||||
const saveButtonHitTest = await page.getByTestId("asset-save").evaluate((button) => {
|
||||
const rect = button.getBoundingClientRect();
|
||||
const hit = document.elementFromPoint(rect.left + rect.width / 2, rect.top + rect.height / 2);
|
||||
return hit === button || button.contains(hit);
|
||||
});
|
||||
expect(saveButtonHitTest).toBe(true);
|
||||
});
|
||||
|
||||
test("报销详情固定使用上海时区并收敛只读信息", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1440, height: 960 });
|
||||
await page.goto("/reimbursements/reim-wave3");
|
||||
@@ -1151,8 +1325,16 @@ test("Wave3 PC 详情和通知中心视觉截图", async ({ page }) => {
|
||||
await page.screenshot({ path: screenshotPath("01-pc-reimbursement-detail.png"), fullPage: true });
|
||||
|
||||
await page.goto("/workflow/tasks/task-wave3");
|
||||
await page.setViewportSize({ width: 1024, height: 768 });
|
||||
await page.setViewportSize({ width: 1100, height: 768 });
|
||||
await expect(page.getByRole("heading", { name: "审批详情" })).toBeVisible();
|
||||
await expect(page.locator(".app-shell-sidebar")).toBeHidden();
|
||||
await expect(page.getByRole("button", { name: "打开菜单" })).toBeVisible();
|
||||
await expect(page.locator(".detail-workspace-grid")).toHaveCSS("grid-template-columns", /.+ .+/);
|
||||
await expectNoHorizontalOverflow(page);
|
||||
|
||||
await page.setViewportSize({ width: 1280, height: 800 });
|
||||
await expect(page.locator(".app-shell-sidebar")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "打开菜单" })).toBeHidden();
|
||||
await expect(page.getByTestId("workflow-task-next-action")).toHaveCount(0);
|
||||
await expect(page.getByText("审批动作")).toHaveCount(0);
|
||||
await expect(page.getByRole("heading", { name: "处理", exact: true })).toBeVisible();
|
||||
@@ -1161,6 +1343,25 @@ test("Wave3 PC 详情和通知中心视觉截图", async ({ page }) => {
|
||||
await expect(page.locator(".workflow-action-panel")).toHaveCSS("position", "sticky");
|
||||
await expect(page.locator(".workflow-action-panel")).toHaveCSS("top", "96px");
|
||||
await expect(page.locator(".workflow-action-panel")).toHaveCSS("z-index", "30");
|
||||
const workflowLayout = await page.locator(".detail-workspace-grid").evaluate((workspace) => {
|
||||
const [main, aside] = Array.from(workspace.children) as HTMLElement[];
|
||||
return {
|
||||
mainWidth: main.getBoundingClientRect().width,
|
||||
asideWidth: aside.getBoundingClientRect().width,
|
||||
columns: getComputedStyle(workspace).gridTemplateColumns.split(" ").filter(Boolean).length,
|
||||
overflow: document.documentElement.scrollWidth - document.documentElement.clientWidth,
|
||||
};
|
||||
});
|
||||
expect(workflowLayout.columns).toBe(2);
|
||||
expect(workflowLayout.mainWidth).toBeGreaterThanOrEqual(560);
|
||||
expect(workflowLayout.asideWidth).toBeGreaterThanOrEqual(280);
|
||||
expect(workflowLayout.overflow).toBeLessThanOrEqual(2);
|
||||
const formNumberLineCount = await page.getByText(reimbursementDetail.formNumber, { exact: true }).evaluate((element) => {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(element);
|
||||
return range.getClientRects().length;
|
||||
});
|
||||
expect(formNumberLineCount).toBe(1);
|
||||
await page.setViewportSize({ width: 1440, height: 960 });
|
||||
await page.getByTestId("workflow-task-reimbursement-detail-link").click();
|
||||
await expect(page).toHaveURL(/\/reimbursements\/reim-wave3$/);
|
||||
@@ -1217,7 +1418,7 @@ test("PC 报销详情按来源返回并保留列表筛选", async ({ page }) =>
|
||||
await page.goto("/reimbursements?status=reviewing&page=1&scope=all");
|
||||
await page.getByTestId("reimbursement-detail-link").first().click();
|
||||
await expect(page).toHaveURL(/\/workflow\/tasks\/task-wave3$/);
|
||||
await expect(page.getByTestId("workflow-task-back")).toContainText("返回报销列表");
|
||||
await expect(page.getByTestId("workflow-task-back")).toContainText("返回审批列表");
|
||||
await page.getByTestId("workflow-task-back").click();
|
||||
await expect(page).toHaveURL(/\/reimbursements\?status=reviewing&page=1&scope=all$/);
|
||||
|
||||
@@ -1255,7 +1456,7 @@ test("/contracts-registry 在 768x1024 与 1024x768 下操作列可达", async (
|
||||
await expect(operation).toBeVisible();
|
||||
await operation.click({ trial: true });
|
||||
await page.screenshot({
|
||||
path: taskScreenshotPath(`contracts-registry-${viewport.width}x${viewport.height}.png`),
|
||||
path: screenshotPath(`contracts-registry-${viewport.width}x${viewport.height}.png`),
|
||||
fullPage: true,
|
||||
});
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户