统一PC前端布局密度(05-28-pc-frontend-layout-unification)

这个提交包含在:
2026-05-28 00:58:29 +08:00
父节点 f926905c57
当前提交 2229449e66
共修改 18 个文件,包含 308 行新增和 83 行删除
@@ -44,6 +44,12 @@ classes in `client/src/index.css`:
secondary actions. secondary actions.
- `ops-table-focus` for the primary desktop table container. - `ops-table-focus` for the primary desktop table container.
- `ops-inline-alert` for temporary mode hints such as batch operation notices. - `ops-inline-alert` for temporary mode hints such as batch operation notices.
- `PrimaryTable` already applies `ops-table-focus` by default; page code may
add responsive visibility classes such as `hidden md:block`, but should not
recreate a separate table frame.
- Finance/admin status summaries should prefer the shared `finance-status-strip`
and `finance-metric-card` classes unless the task introduces a new reusable
metric component.
**Why**: This keeps high-frequency PC pages dense and predictable without **Why**: This keeps high-frequency PC pages dense and predictable without
recreating page-local Tailwind command bars. It also lets `frontend-design` recreating page-local Tailwind command bars. It also lets `frontend-design`
@@ -67,7 +73,7 @@ change the visual treatment once while preserving component contracts.
</div> </div>
</FilterBar> </FilterBar>
<PrimaryTable className="hidden md:block ops-table-focus">...</PrimaryTable> <PrimaryTable className="hidden md:block">...</PrimaryTable>
``` ```
**Tests Required**: **Tests Required**:
@@ -0,0 +1,4 @@
{"file": ".trellis/spec/hyb-oa/project-governance.md", "reason": "Validate Trellis workflow, scope and no production side effects"}
{"file": ".trellis/spec/hyb-oa/frontend-react-vite-design.md", "reason": "Validate frontend design contracts, tests and UI governance"}
{"file": ".trellis/tasks/archive/2026-05/05-27-oa-product-flow-test-policy-audit/research/overall-audit-matrices.md", "reason": "Validate implementation remains aligned with governance review roadmap"}
{"file": ".trellis/tasks/archive/2026-05/05-27-oa-frontend-layout-page-fit-review/research/page-layout-and-fit-review.md", "reason": "Validate page taxonomy and design direction are followed"}
@@ -0,0 +1,4 @@
{"file": ".trellis/spec/hyb-oa/project-governance.md", "reason": "Trellis task, approval and commit boundaries for implementation"}
{"file": ".trellis/spec/hyb-oa/frontend-react-vite-design.md", "reason": "Primary frontend design and verification rules for PC OA pages"}
{"file": ".trellis/tasks/archive/2026-05/05-27-oa-product-flow-test-policy-audit/research/overall-audit-matrices.md", "reason": "Governance review conclusions and execution roadmap"}
{"file": ".trellis/tasks/archive/2026-05/05-27-oa-frontend-layout-page-fit-review/research/page-layout-and-fit-review.md", "reason": "Page taxonomy, fit matrix and frontend design direction"}
@@ -0,0 +1,60 @@
# 实施方案
## 推荐方案
采用“共享模板先行 + 重点页面收敛”的方式,而不是逐页重画。
1. 盘点现有 PC 页面是否已经使用 `ListWorkspace`、`WorkspaceHeader`、`FilterBar`、`PrimaryTable`、`PageScaffold` 和 `ops-*` / `finance-*` / `admin-*`。
2. 先补共享层:
- 统一页面宽度、标题层级、标题动作区、统计条密度、命令区间距、表格容器边界。
- 保持现有 class contract,不破坏已经通过 Wave2-Wave4 的页面。
3. 再改重点页面:
- 工作台:突出今日动作和待办,折叠低频入口。
- 报销/审批:列表统一筛选和主表,详情统一摘要、票据/附件、审批、付款/证据四区。
- 财务/AP/票据池:保留高密,但统一统计、操作面板和表格焦点。
- C 类管理页:统一统计条、命令区、表格、空状态,减少页面说明。
4. 最后做视觉与交互验证:
- typecheck/build
- UI design rules
- focused Playwright
- 必要时用截图检查桌面和移动响应式。
## 风险控制
- 不动业务数据流、接口、权限、审批状态机和数据库。
- 不把“视觉优化”变成流程重构。
- 不新增重复 UI 系统;优先复用已有 pattern 和 CSS class。
- 高风险动作只调整位置和呈现,不取消二次确认。
## 第一批文件候选
实际实现前再按代码确认,候选区域包括:
- `client/src/components/patterns/`
- `client/src/index.css`
- `client/src/pages/PortalHomePage.tsx`
- `client/src/pages/ReimbursementsPage.tsx`
- `client/src/pages/ReimbursementDetailPage.tsx`
- `client/src/pages/WorkflowTasksPage.tsx`
- `client/src/pages/WorkflowTaskDetailPage.tsx`
- `client/src/pages/InvoicePoolPage.tsx`
- `client/src/pages/PaymentBatchesPage.tsx`
- `client/src/pages/ApOperationsPage.tsx`
- `client/src/pages/UserManagementPage.tsx`
- `client/src/pages/MasterDataPage.tsx`
- `client/src/pages/FeatureFlagsPage.tsx`
- `client/src/pages/NotificationChannelsPage.tsx`
- `client/src/pages/AuditLogsPage.tsx`
## 不做事项
- 不新增登录、邮箱、企业微信功能。
- 不接发票验真真实 provider。
- 不改制度签收、用印台账、采购比价或审批阈值。
- 不部署公网。
## 完成记录
- 共享层:收敛 `theme.css` 半径、`ListWorkspace` 标题/筛选/表格密度、`index.css` 的 `ops-*`、`finance-*`、`admin-*` 和 Portal 类。
- 页面层:压缩 Portal 工作台文案与面板密度,系统设置改为带图标的管理入口卡片,报销批量操作、详情错误态、主数据/功能开关/通知渠道间距统一。
- 验证:typecheck、build、UI design rules、Trellis governance、Wave2/Wave4 focused Playwright 均通过。
@@ -0,0 +1,105 @@
# PC 前端布局统一改造
## Goal
在 OAOPS1 真实试运行并行推进的前提下,启动第一批实施类改造:统一 PC 页面布局、信息密度、命令区、表格/列表和详情区体验,降低“页面啰嗦、操作复杂、风格不统一”的用户体感问题。视觉裁决以 `frontend-design` 为准,但 OA 业务系统保持克制、密集、清晰的工作台风格。
## What I Already Know
- OAOPS1 七天真实试运行仍是独立并行主线,不因本任务暂停。
- 已归档审查结论建议第一批实施优先做 PC 前端布局统一,移动和流程规则后置。
- 前端已有 Wave2-Wave4 基础,不能再做一次大而全重构。
- 现有代码已经有可复用模式:`ListWorkspace`、`WorkspaceHeader`、`FilterBar`、`PrimaryTable`、`PageScaffold`、`EntityOverviewHero`、`SectionPanel`、`ops-*`、`detail-*`、`finance-*`、`admin-*`。
- `frontend-design` 要求界面有明确设计方向;本任务的方向是“安静、精密、运营型 OA 工作台”,不是营销页、装饰页或大面积视觉噱头。
## Assumptions
- 第一批优先 PC,不改移动主体验,除非发现 PC 改动破坏移动响应式。
- 优先通过共享组件和共享 CSS class 收敛,不给每个页面各写一套新样式。
- 本任务不改变审批、报销、付款、发票、权限、企业微信或后端业务规则。
- 不做生产部署,除非完成本地验证后另行获得明确批准。
## Scope
- PC 工作台、列表页、详情页、财务密集页和 C 类管理页的布局统一。
- 共享前端 pattern、CSS class、页面容器、命令区、表格/列表容器和视觉密度调整。
- 必要的 focused Playwright、typecheck、build 和 UI governance 验证。
- 仅限前端展示层,不触碰业务规则、后端接口、数据库和生产配置。
## Requirements
- 固化 PC 页面模板:
- 工作台:今日动作、待办、常用入口、异常提示,不做模块橱窗。
- 列表页:`ListWorkspace + WorkspaceHeader + FilterBar + PrimaryTable + ops-*`。
- 详情页:`PageScaffold + EntityOverviewHero + SectionPanel + detail-*`。
- 财务密集页:`finance-* + ops-table-focus`,保留高密度但减少说明型文案。
- 管理 C 类页:`admin-* + ops-*`,统一命令区和表格密度。
- 第一批页面范围优先覆盖:
- `/portal`
- 报销列表与详情
- 审批中心与审批详情
- 票据池、付款批次、AP
- 系统设置、用户管理、主数据、功能开关、通知渠道、审计日志
- 压缩页面文案:删除重复说明、弱化低频帮助文本,把复杂说明移到空状态、错误态或二次确认。
- 统一操作区:主动作靠近页面标题或当前记录,危险/高风险动作必须保留二次确认。
- 统一表格密度和状态展示:状态、金额、时间、人员、异常原因位置稳定。
- 保持现有权限、路由、接口、feature flag、测试数据过滤和安全边界不变。
## Acceptance Criteria
- [x] 关键 PC 页面采用统一页面骨架、命令区、表格/列表和详情模式。
- [x] 页面文案明显收敛,不再出现重复说明型段落或页面级帮助堆叠。
- [x] 财务/AP/票据池仍保持高密度操作效率,危险操作保留确认和审计提示。
- [x] 管理页命令区、统计条、表格容器和空状态保持一致。
- [x] 不改变后端接口、数据库、权限模型、审批规则、企业微信配置或生产配置。
- [x] `cd client && npm run typecheck && npm run build` 通过。
- [x] `node scripts/qa/check-ui-design-rules.mjs` 通过。
- [x] 至少运行 focused Playwright 覆盖 PC 关键页,必要时补截图/交互证据。
## Definition of Done
- 改造范围内页面在桌面宽度下没有明显布局漂移、文字挤压、操作区混乱或卡片套卡片。
- 移动响应式没有被 PC 改造破坏。
- 本地验证通过;如果用户要求可测,再获得部署批准后执行公网验证。
- Trellis task 校验和治理检查通过。
- 提交信息使用中文并引用本任务 ID。
## Out of Scope
- 不重写整套设计系统。
- 不改移动端业务主体验。
- 不改业务流程、审批阈值、付款规则、发票验真 provider、权限模型或数据库 schema。
- 不采购、部署或改生产配置。
- 不把官网营销视觉照搬到 OA 工作台。
## Test Plan
```bash
cd client && npm run typecheck
cd client && npm run build
node scripts/qa/check-ui-design-rules.mjs
python3 ./.trellis/scripts/task.py validate 05-28-pc-frontend-layout-unification
node scripts/qa/check-trellis-governance.mjs
git diff --check
```
Focused Playwright 根据最终修改页面选择,优先覆盖:
```bash
cd client && npx playwright test tests/e2e/login-portal-ux.spec.ts
cd client && npx playwright test tests/e2e/workflow-*.spec.ts
cd client && npx playwright test tests/e2e/reimbursement-payment-batch-flow.spec.ts
cd client && npx playwright test tests/e2e/invoice-ocr-flow.spec.ts
```
## Technical Notes
- Frontend spec: `.trellis/spec/hyb-oa/frontend-react-vite-design.md`
- Project governance: `.trellis/spec/hyb-oa/project-governance.md`
- Prior审查: `.trellis/tasks/archive/2026-05/05-27-oa-product-flow-test-policy-audit/research/overall-audit-matrices.md`
- 页面专项审查: `.trellis/tasks/archive/2026-05/05-27-oa-frontend-layout-page-fit-review/research/page-layout-and-fit-review.md`
- Likely shared code: `client/src/components/patterns/`、`client/src/index.css`、`client/src/design/`
## Completion Note
2026-05-28:已完成第一批 PC 布局统一改造。改动集中在共享 pattern/CSS、Portal、报销/审批详情、系统设置、主数据、功能开关和通知渠道等展示层;没有修改后端接口、数据库、权限、审批规则或生产配置。
@@ -0,0 +1,26 @@
{
"id": "pc-frontend-layout-unification",
"name": "pc-frontend-layout-unification",
"title": "PC前端布局统一改造",
"description": "在OAOPS1真实试运行并行推进的前提下,统一PC页面布局、信息密度、命令区、表格/列表和详情区体验。",
"status": "in_progress",
"dev_type": null,
"scope": "frontend-pc-layout",
"package": "client",
"priority": "P2",
"creator": "shunli",
"assignee": "shunli",
"createdAt": "2026-05-28",
"completedAt": null,
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}
@@ -18,7 +18,7 @@ export function EntityOverviewHero({
{subtitle ? <p className="workbench-hero__subtitle">{subtitle}</p> : null} {subtitle ? <p className="workbench-hero__subtitle">{subtitle}</p> : null}
{meta ? <div className="mt-3 flex flex-wrap gap-2">{meta}</div> : null} {meta ? <div className="mt-3 flex flex-wrap gap-2">{meta}</div> : null}
</div> </div>
{actions ? <div className="shrink-0 rounded-2xl border border-white/70 bg-white/55 p-2 shadow-sm backdrop-blur">{actions}</div> : null} {actions ? <div className="shrink-0 rounded-lg border border-white/70 bg-white/55 p-2 shadow-sm backdrop-blur">{actions}</div> : null}
</section> </section>
); );
} }
@@ -21,20 +21,20 @@ export function ListWorkspace({ className, children }: WorkspaceChildrenProps) {
export function WorkspaceHeader({ eyebrow, title, subtitle, meta, actions, className }: WorkspaceHeaderProps) { export function WorkspaceHeader({ eyebrow, title, subtitle, meta, actions, className }: WorkspaceHeaderProps) {
return ( return (
<header className={cn("flex flex-col gap-4 md:flex-row md:items-end md:justify-between", className)}> <header className={cn("flex flex-col gap-3 md:flex-row md:items-end md:justify-between", className)}>
<div className="min-w-0 max-w-3xl"> <div className="min-w-0 max-w-3xl">
{eyebrow ? <div className="text-xs font-bold uppercase text-primary">{eyebrow}</div> : null} {eyebrow ? <div className="text-xs font-bold uppercase text-primary">{eyebrow}</div> : null}
<h1 className="mt-1 text-2xl font-semibold text-[var(--color-text-primary)] md:text-3xl">{title}</h1> <h1 className="mt-1 text-2xl font-semibold text-[var(--color-text-primary)] md:text-[1.65rem]">{title}</h1>
{subtitle ? <div className="mt-2 text-sm leading-6 text-[var(--color-text-secondary)]">{subtitle}</div> : null} {subtitle ? <div className="mt-1.5 text-sm leading-6 text-[var(--color-text-secondary)]">{subtitle}</div> : null}
{meta ? <div className="mt-3 flex flex-wrap gap-2">{meta}</div> : null} {meta ? <div className="mt-2.5 flex flex-wrap gap-2">{meta}</div> : null}
</div> </div>
{actions ? <div className="flex shrink-0 flex-wrap items-center gap-2">{actions}</div> : null} {actions ? <div className="flex shrink-0 flex-wrap items-center gap-2 md:justify-end">{actions}</div> : null}
</header> </header>
); );
} }
export function FilterBar({ className, children }: WorkspaceChildrenProps) { export function FilterBar({ className, children }: WorkspaceChildrenProps) {
return <section className={cn("card-std p-4 md:p-5", className)}>{children}</section>; return <section className={cn("card-std p-3 md:p-3.5", className)}>{children}</section>;
} }
export function FilterGrid({ className, children }: WorkspaceChildrenProps) { export function FilterGrid({ className, children }: WorkspaceChildrenProps) {
@@ -66,7 +66,7 @@ export function WorkspaceSection({
actions?: ReactNode; actions?: ReactNode;
}) { }) {
return ( return (
<section className={cn("card-std space-y-4 p-4 md:p-5", className)}> <section className={cn("card-std space-y-3 p-3.5 md:p-4", className)}>
{title || description || actions ? ( {title || description || actions ? (
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between"> <div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
<div className="min-w-0"> <div className="min-w-0">
@@ -86,7 +86,7 @@ export function ActionToolbar({ className, children }: WorkspaceChildrenProps) {
} }
export function PrimaryTable({ className, children }: WorkspaceChildrenProps) { export function PrimaryTable({ className, children }: WorkspaceChildrenProps) {
return <section className={cn("table-container p-0", className)}>{children}</section>; return <section className={cn("table-container ops-table-focus p-0", className)}>{children}</section>;
} }
export function WorkspaceEmptyState({ export function WorkspaceEmptyState({
@@ -97,7 +97,7 @@ export function WorkspaceEmptyState({
children?: ReactNode; children?: ReactNode;
}) { }) {
return ( return (
<div className={cn("rounded-xl border border-dashed border-zinc-300 px-4 py-8 text-center text-sm text-zinc-500", className)}> <div className={cn("rounded-lg border border-dashed border-zinc-300 px-4 py-7 text-center text-sm text-zinc-500", className)}>
{children} {children}
</div> </div>
); );
@@ -123,7 +123,7 @@ export function WorkspaceNotice({
}[tone]; }[tone];
return ( return (
<section className={cn("rounded-xl border px-4 py-3 text-sm", toneClass, className)}> <section className={cn("rounded-lg border px-4 py-3 text-sm", toneClass, className)}>
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between"> <div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
<div className="min-w-0"> <div className="min-w-0">
<div className="font-semibold">{title}</div> <div className="font-semibold">{title}</div>
+3 -3
查看文件
@@ -20,10 +20,10 @@
--color-info: #00398a; --color-info: #00398a;
--radius-sm: 0.5rem; --radius-sm: 0.5rem;
--radius-md: 0.75rem; --radius-md: 0.75rem;
--radius-lg: 1rem; --radius-lg: 0.75rem;
--radius-xl: 1rem; --radius-xl: 0.875rem;
--radius-2xl: 1.25rem; --radius-2xl: 1.25rem;
--radius-panel: 1rem; --radius-panel: 0.75rem;
--shadow-xs: 0 1px 2px rgba(15, 23, 42, 0.05); --shadow-xs: 0 1px 2px rgba(15, 23, 42, 0.05);
--shadow-sm: 0 4px 14px rgba(15, 23, 42, 0.055), 0 1px 2px rgba(15, 23, 42, 0.04); --shadow-sm: 0 4px 14px rgba(15, 23, 42, 0.055), 0 1px 2px rgba(15, 23, 42, 0.04);
--shadow-md: 0 14px 34px rgba(0, 57, 138, 0.10); --shadow-md: 0 14px 34px rgba(0, 57, 138, 0.10);
+38 -25
查看文件
@@ -46,7 +46,7 @@
.page-scaffold { .page-scaffold {
animation: page-reveal 320ms ease-out both; animation: page-reveal 320ms ease-out both;
@apply space-y-5; @apply space-y-4 md:space-y-5;
} }
.auth-shell { .auth-shell {
@@ -119,7 +119,7 @@
border: 1px solid rgba(226, 232, 240, 0.92); border: 1px solid rgba(226, 232, 240, 0.92);
border-radius: var(--radius-panel); border-radius: var(--radius-panel);
box-shadow: var(--shadow-xs); box-shadow: var(--shadow-xs);
@apply flex flex-col gap-4 px-5 py-5 md:flex-row md:items-end md:justify-between md:px-6; @apply flex flex-col gap-4 px-4 py-4 md:flex-row md:items-end md:justify-between md:px-5 md:py-4;
} }
.ops-status-strip { .ops-status-strip {
@@ -129,7 +129,7 @@
.ops-status-strip .metric-card { .ops-status-strip .metric-card {
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
box-shadow: var(--shadow-xs); box-shadow: var(--shadow-xs);
@apply px-3.5 py-3.5; @apply min-h-[92px] px-3.5 py-3;
} }
.ops-status-strip .metric-card::before { .ops-status-strip .metric-card::before {
@@ -153,7 +153,7 @@
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
background: rgba(255, 255, 255, 0.94); background: rgba(255, 255, 255, 0.94);
box-shadow: var(--shadow-xs); box-shadow: var(--shadow-xs);
@apply p-3 md:p-3.5; @apply p-3;
} }
.ops-command-bar__row { .ops-command-bar__row {
@@ -168,6 +168,11 @@
@apply flex w-full flex-wrap items-center gap-2 xl:w-auto xl:justify-end; @apply flex w-full flex-wrap items-center gap-2 xl:w-auto xl:justify-end;
} }
.ops-command-bar__actions > .select-sm,
.ops-command-bar__actions > .select-std {
min-width: 9rem;
}
.ops-filter-label { .ops-filter-label {
color: var(--color-text-secondary); color: var(--color-text-secondary);
@apply text-xs font-semibold; @apply text-xs font-semibold;
@@ -181,6 +186,8 @@
.ops-table-focus { .ops-table-focus {
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
box-shadow: var(--shadow-xs); box-shadow: var(--shadow-xs);
border: 1px solid rgba(226, 232, 240, 0.94);
background: rgba(255, 255, 255, 0.98);
} }
.ops-table-focus table { .ops-table-focus table {
@@ -195,13 +202,13 @@
.ops-table-focus th, .ops-table-focus th,
.ops-table-focus .table-th { .ops-table-focus .table-th {
color: var(--color-text-tertiary); color: var(--color-text-tertiary);
@apply px-4 py-3 text-xs font-semibold; @apply px-3.5 py-2.5 text-xs font-semibold;
} }
.ops-table-focus td, .ops-table-focus td,
.ops-table-focus .table-td, .ops-table-focus .table-td,
.ops-table-focus .table-td-strong { .ops-table-focus .table-td-strong {
@apply px-4 py-3 text-sm; @apply px-3.5 py-2.5 text-sm;
} }
.ops-table-focus tbody tr:hover { .ops-table-focus tbody tr:hover {
@@ -238,7 +245,7 @@
} }
.workbench-hero--compact { .workbench-hero--compact {
@apply gap-4 px-5 py-5 md:px-6 md:py-6; @apply gap-4 px-4 py-4 md:px-5 md:py-5;
} }
.workbench-hero--compact .workbench-hero__title { .workbench-hero--compact .workbench-hero__title {
@@ -275,7 +282,7 @@
.section-panel { .section-panel {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
@apply p-4 md:p-5; @apply p-3.5 md:p-4;
} }
.section-panel::before { .section-panel::before {
@@ -311,7 +318,7 @@
border: 1px solid rgba(226, 232, 240, 0.96); border: 1px solid rgba(226, 232, 240, 0.96);
border-radius: var(--radius-panel); border-radius: var(--radius-panel);
box-shadow: var(--shadow-xs); box-shadow: var(--shadow-xs);
@apply block px-4 py-4 transition duration-150 hover:border-slate-300 hover:bg-white; @apply block px-3.5 py-3.5 transition duration-150 hover:border-slate-300 hover:bg-white;
} }
.metric-card::before { .metric-card::before {
@@ -348,12 +355,12 @@
.metric-card__value { .metric-card__value {
color: var(--color-text-primary); color: var(--color-text-primary);
@apply mt-2 text-xl font-semibold tracking-tight; @apply mt-1.5 text-xl font-semibold tracking-tight;
} }
.metric-card__note { .metric-card__note {
color: var(--color-text-secondary); color: var(--color-text-secondary);
@apply mt-2 text-sm leading-6; @apply mt-1.5 text-sm leading-6;
} }
.info-chip { .info-chip {
@@ -393,7 +400,7 @@
.table-container { .table-container {
border-color: rgba(226, 232, 240, 0.92); border-color: rgba(226, 232, 240, 0.92);
background: var(--color-bg-surface-raised); background: var(--color-bg-surface-raised);
@apply overflow-hidden rounded-2xl border shadow-sm backdrop-blur; @apply overflow-hidden rounded-lg border shadow-sm backdrop-blur;
} }
.table-header { .table-header {
@@ -537,7 +544,7 @@
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(248, 250, 252, 0.88)); linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(248, 250, 252, 0.88));
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
box-shadow: var(--shadow-xs); box-shadow: var(--shadow-xs);
@apply border p-4; @apply min-h-[92px] border p-3.5;
} }
.finance-metric-card__label { .finance-metric-card__label {
@@ -547,7 +554,7 @@
.finance-metric-card__value { .finance-metric-card__value {
color: var(--color-text-primary); color: var(--color-text-primary);
@apply mt-2 text-2xl font-bold; @apply mt-1.5 text-xl font-semibold;
} }
.finance-workspace-grid { .finance-workspace-grid {
@@ -559,7 +566,7 @@
background: rgba(255, 255, 255, 0.97); background: rgba(255, 255, 255, 0.97);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
box-shadow: var(--shadow-xs); box-shadow: var(--shadow-xs);
@apply border p-4 md:p-5; @apply border p-3.5 md:p-4;
} }
.finance-list-card { .finance-list-card {
@@ -580,11 +587,11 @@
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(248, 250, 252, 0.9)); linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(248, 250, 252, 0.9));
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
box-shadow: var(--shadow-xs); box-shadow: var(--shadow-xs);
@apply border p-4 md:p-5; @apply border p-3.5 md:p-4;
} }
.admin-tile-grid { .admin-tile-grid {
@apply grid gap-4 md:grid-cols-2 xl:grid-cols-3; @apply grid gap-3 md:grid-cols-2 xl:grid-cols-4;
} }
.admin-tile-card { .admin-tile-card {
@@ -592,7 +599,13 @@
background: rgba(255, 255, 255, 0.96); background: rgba(255, 255, 255, 0.96);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
box-shadow: var(--shadow-xs); box-shadow: var(--shadow-xs);
@apply border p-4 transition-colors hover:border-slate-300 hover:bg-slate-50/60 md:p-5; @apply border p-3.5 transition-colors hover:border-slate-300 hover:bg-slate-50/60 md:p-4;
}
.admin-tile-card__icon {
background: rgba(0, 57, 138, 0.08);
color: var(--color-accent-primary);
@apply flex size-9 shrink-0 items-center justify-center rounded-lg;
} }
.admin-workspace-grid { .admin-workspace-grid {
@@ -622,11 +635,11 @@
} }
.portal-home-grid { .portal-home-grid {
@apply grid gap-5 xl:grid-cols-[minmax(0,2fr),minmax(320px,0.92fr)] xl:items-stretch; @apply grid gap-4 xl:grid-cols-[minmax(0,1.72fr),minmax(300px,0.86fr)] xl:items-stretch;
} }
.portal-home-grid--collaboration { .portal-home-grid--collaboration {
@apply xl:grid-cols-[minmax(0,1.55fr),minmax(320px,0.9fr)]; @apply xl:grid-cols-[minmax(0,1.42fr),minmax(300px,0.92fr)];
} }
.portal-panel-fill { .portal-panel-fill {
@@ -638,7 +651,7 @@
border: 1px solid rgba(226, 232, 240, 0.94); border: 1px solid rgba(226, 232, 240, 0.94);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
box-shadow: var(--shadow-xs); box-shadow: var(--shadow-xs);
@apply block px-3.5 py-3 transition duration-150; @apply block px-3 py-2.5 transition duration-150;
} }
.portal-list-card:hover { .portal-list-card:hover {
@@ -666,7 +679,7 @@
border: 1px solid rgba(226, 232, 240, 0.94); border: 1px solid rgba(226, 232, 240, 0.94);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
box-shadow: var(--shadow-xs); box-shadow: var(--shadow-xs);
@apply px-3 py-3 text-left transition duration-150; @apply px-3 py-2.5 text-left transition duration-150;
} }
.portal-action-tile:hover { .portal-action-tile:hover {
@@ -700,7 +713,7 @@
background: rgba(255, 255, 255, 0.92); background: rgba(255, 255, 255, 0.92);
border: 1px solid rgba(226, 232, 240, 0.94); border: 1px solid rgba(226, 232, 240, 0.94);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
@apply px-3.5 py-3; @apply px-3 py-2.5;
} }
.portal-team-list { .portal-team-list {
@@ -728,7 +741,7 @@
} }
.portal-secondary-drawer { .portal-secondary-drawer {
@apply p-3.5 md:p-4; @apply p-3 md:p-3.5;
} }
.portal-secondary-summary { .portal-secondary-summary {
@@ -749,7 +762,7 @@
} }
.portal-compact-group { .portal-compact-group {
@apply rounded-lg border border-slate-200 bg-white/80 px-3.5 py-3; @apply rounded-lg border border-slate-200 bg-white/80 px-3 py-2.5;
} }
.portal-compact-group__title { .portal-compact-group__title {
+2 -2
查看文件
@@ -685,7 +685,7 @@ export function FeatureFlagsPage() {
</p> </p>
) : null} ) : null}
{!impactQuery.isLoading && !impactQuery.isError && impactQuery.data ? ( {!impactQuery.isLoading && !impactQuery.isError && impactQuery.data ? (
<div className="space-y-6"> <div className="space-y-4">
<div className="space-y-3"> <div className="space-y-3">
<h4 className="text-sm font-semibold text-zinc-900">前端导航</h4> <h4 className="text-sm font-semibold text-zinc-900">前端导航</h4>
{impactQuery.data.navigationItems.length ? ( {impactQuery.data.navigationItems.length ? (
@@ -793,7 +793,7 @@ export function FeatureFlagsPage() {
</Dialog> </Dialog>
{batchMode ? ( {batchMode ? (
<div className="fixed bottom-6 left-1/2 z-50 w-[min(960px,calc(100vw-2rem))] -translate-x-1/2 rounded-2xl border border-zinc-200 bg-white/95 p-4 shadow-xl backdrop-blur"> <div className="fixed bottom-6 left-1/2 z-50 w-[min(960px,calc(100vw-2rem))] -translate-x-1/2 rounded-lg border border-zinc-200 bg-white/95 p-3.5 shadow-xl backdrop-blur">
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between"> <div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div className="text-sm text-zinc-700"> <div className="text-sm text-zinc-700">
已选择 <span className="font-semibold text-zinc-900">{selectedItems.length}</span> 个功能开关 已选择 <span className="font-semibold text-zinc-900">{selectedItems.length}</span> 个功能开关
+5 -5
查看文件
@@ -341,7 +341,7 @@ export function MasterDataPage() {
<TabsTrigger value="suppliers">供应商</TabsTrigger> <TabsTrigger value="suppliers">供应商</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="legal-entities" className="space-y-6"> <TabsContent value="legal-entities" className="space-y-4">
<FilterBar className="ops-command-bar !p-3 md:!p-3.5"> <FilterBar className="ops-command-bar !p-3 md:!p-3.5">
<div className="flex flex-wrap items-center justify-between gap-3"> <div className="flex flex-wrap items-center justify-between gap-3">
<h3 className="text-base font-semibold text-zinc-900">法体筛选</h3> <h3 className="text-base font-semibold text-zinc-900">法体筛选</h3>
@@ -454,7 +454,7 @@ export function MasterDataPage() {
</div> </div>
</TabsContent> </TabsContent>
<TabsContent value="cost-centers" className="space-y-6"> <TabsContent value="cost-centers" className="space-y-4">
<FilterBar className="ops-command-bar !p-3 md:!p-3.5"> <FilterBar className="ops-command-bar !p-3 md:!p-3.5">
<div className="grid gap-3 md:grid-cols-3"> <div className="grid gap-3 md:grid-cols-3">
<select <select
@@ -613,7 +613,7 @@ export function MasterDataPage() {
</div> </div>
</TabsContent> </TabsContent>
<TabsContent value="document-categories" className="space-y-6"> <TabsContent value="document-categories" className="space-y-4">
<FilterBar className="ops-command-bar !p-3 md:!p-3.5"> <FilterBar className="ops-command-bar !p-3 md:!p-3.5">
<div className="grid gap-3 md:grid-cols-3"> <div className="grid gap-3 md:grid-cols-3">
<select <select
@@ -780,14 +780,14 @@ export function MasterDataPage() {
</div> </div>
</TabsContent> </TabsContent>
<TabsContent value="holiday-calendars" className="space-y-6"> <TabsContent value="holiday-calendars" className="space-y-4">
<HolidayCalendarTab <HolidayCalendarTab
canManage={canManage} canManage={canManage}
includeArchived={includeArchived} includeArchived={includeArchived}
legalEntities={legalEntities} legalEntities={legalEntities}
/> />
</TabsContent> </TabsContent>
<TabsContent value="suppliers" className="space-y-6"> <TabsContent value="suppliers" className="space-y-4">
<SupplierTab <SupplierTab
canManage={canManage} canManage={canManage}
includeArchived={includeArchived} includeArchived={includeArchived}
@@ -535,7 +535,7 @@ export function NotificationChannelsPage() {
) : null} ) : null}
</div> </div>
<div className="grid gap-6 xl:grid-cols-[minmax(0,2fr)_minmax(320px,1fr)]"> <div className="grid gap-4 xl:grid-cols-[minmax(0,2fr)_minmax(320px,1fr)]">
<div className="admin-command-panel space-y-4"> <div className="admin-command-panel space-y-4">
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
<div> <div>
+15 -15
查看文件
@@ -524,7 +524,7 @@ function AdoptionSummaryPanel({ adoptionSummary }: { adoptionSummary: NonNullabl
<div className="mt-2 text-2xl font-semibold text-zinc-900">{adoptionSummary.announcementCount}</div> <div className="mt-2 text-2xl font-semibold text-zinc-900">{adoptionSummary.announcementCount}</div>
</div> </div>
</div> </div>
<div className="mt-4 rounded-2xl border border-slate-200 bg-slate-50 px-4 py-4 text-sm text-slate-600"> <div className="mt-4 rounded-lg border border-slate-200 bg-slate-50 px-3.5 py-3 text-sm text-slate-600">
<div className="font-medium text-zinc-900">本周观测提示</div> <div className="font-medium text-zinc-900">本周观测提示</div>
<ul className="mt-2 list-disc space-y-1 pl-5"> <ul className="mt-2 list-disc space-y-1 pl-5">
{adoptionSummary.hints.map((hint) => ( {adoptionSummary.hints.map((hint) => (
@@ -589,7 +589,7 @@ function GettingStartedCard({
return ( return (
<SectionPanel <SectionPanel
title="快速上手" title="快速上手"
description="基础资料" description="基础设置"
actions={( actions={(
<button <button
type="button" type="button"
@@ -622,7 +622,7 @@ function GettingStartedCard({
</Link> </Link>
))} ))}
<div className="rounded-2xl border border-slate-200 bg-slate-50 px-4 py-4"> <div className="rounded-lg border border-slate-200 bg-slate-50 px-3.5 py-3">
<div className="flex items-center justify-between gap-3 text-sm"> <div className="flex items-center justify-between gap-3 text-sm">
<span className="font-medium text-zinc-900">完成进度</span> <span className="font-medium text-zinc-900">完成进度</span>
<span className="text-zinc-500">{completedCount}/{items.length}</span> <span className="text-zinc-500">{completedCount}/{items.length}</span>
@@ -726,10 +726,10 @@ function TodoAggregatePanel({ items }: { items: TodoAggregateItem[] }) {
<Link key={`${item.title}-${item.href}-${item.sourceLabel}`} to={item.href} className="portal-list-card"> <Link key={`${item.title}-${item.href}-${item.sourceLabel}`} to={item.href} className="portal-list-card">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<div className={item.severity === "high" <div className={item.severity === "high"
? "mt-0.5 flex size-9 items-center justify-center rounded-2xl bg-red-50 text-red-600" ? "mt-0.5 flex size-9 items-center justify-center rounded-lg bg-red-50 text-red-600"
: item.severity === "warning" : item.severity === "warning"
? "mt-0.5 flex size-9 items-center justify-center rounded-2xl bg-amber-50 text-amber-600" ? "mt-0.5 flex size-9 items-center justify-center rounded-lg bg-amber-50 text-amber-600"
: "mt-0.5 flex size-9 items-center justify-center rounded-2xl bg-zinc-100 text-zinc-600"} : "mt-0.5 flex size-9 items-center justify-center rounded-lg bg-zinc-100 text-zinc-600"}
> >
<span className="material-symbols-outlined text-[18px]">{item.icon}</span> <span className="material-symbols-outlined text-[18px]">{item.icon}</span>
</div> </div>
@@ -896,14 +896,14 @@ export function PortalHomePage() {
const gettingStartedCompletedCount = gettingStartedItems.filter((item) => item.completed).length; const gettingStartedCompletedCount = gettingStartedItems.filter((item) => item.completed).length;
if (portalQuery.isLoading) { if (portalQuery.isLoading) {
return <div className="card-std p-6 text-sm text-zinc-500">加载工作台中...</div>; return <div className="card-std p-5 text-sm text-zinc-500">加载工作台中...</div>;
} }
if (portalQuery.isError || !portalQuery.data) { if (portalQuery.isError || !portalQuery.data) {
const message = isApiError(portalQuery.error) const message = isApiError(portalQuery.error)
? `${portalQuery.error.code}: ${mapApiErrorMessage(portalQuery.error.code, portalQuery.error.details, portalQuery.error.message)}` ? `${portalQuery.error.code}: ${mapApiErrorMessage(portalQuery.error.code, portalQuery.error.details, portalQuery.error.message)}`
: "工作台加载失败"; : "工作台加载失败";
return <div className="card-std p-6 text-sm text-red-600">{message}</div>; return <div className="card-std p-5 text-sm text-red-600">{message}</div>;
} }
const payload = portalQuery.data; const payload = portalQuery.data;
@@ -1096,8 +1096,8 @@ export function PortalHomePage() {
quickActions.push({ quickActions.push({
key: "attendance", key: "attendance",
icon: "schedule", icon: "schedule",
label: "考勤汇总", label: "考勤",
description: "企微摘要 / OA 修正", description: "企微摘要",
to: "/attendance", to: "/attendance",
}); });
if (payload.weeklyReport || payload.weeklySummary) { if (payload.weeklyReport || payload.weeklySummary) {
@@ -1105,7 +1105,7 @@ export function PortalHomePage() {
key: "weekly", key: "weekly",
icon: "article", icon: "article",
label: "提交周报", label: "提交周报",
description: "填写 / 查看缺口", description: "填写 / 缺口",
to: "/weekly-reports", to: "/weekly-reports",
}); });
} }
@@ -1161,7 +1161,7 @@ export function PortalHomePage() {
fallback={( fallback={(
<PortalSectionFallback <PortalSectionFallback
title="快速上手" title="快速上手"
description="基础资料" description="基础设置"
/> />
)} )}
> >
@@ -1205,9 +1205,9 @@ export function PortalHomePage() {
<> <>
<SecondaryWorkspacePanel <SecondaryWorkspacePanel
title="角色重点" title="角色重点"
description="常用入口" description="入口"
> >
<div className="grid gap-6 xl:grid-cols-[minmax(0,1fr),minmax(0,1fr)] 2xl:grid-cols-[minmax(0,1fr),minmax(0,1fr),360px]"> <div className="grid gap-4 xl:grid-cols-[minmax(0,1fr),minmax(0,1fr)] 2xl:grid-cols-[minmax(0,1fr),minmax(0,1fr),340px]">
{roleCapabilities.canApproveTasks ? ( {roleCapabilities.canApproveTasks ? (
<SummaryPanel title="审批处理" items={approvalSummaryItems} /> <SummaryPanel title="审批处理" items={approvalSummaryItems} />
) : null} ) : null}
@@ -1243,7 +1243,7 @@ export function PortalHomePage() {
) : ( ) : (
<SecondaryWorkspacePanel <SecondaryWorkspacePanel
title="更多入口" title="更多入口"
description="申请、资产、最近访问和项目指标" description="申请、资产、最近访问"
> >
<div className="portal-secondary-compact-grid"> <div className="portal-secondary-compact-grid">
<CompactSummaryLinks title="我的申请与资产" items={employeeSummaryItems} /> <CompactSummaryLinks title="我的申请与资产" items={employeeSummaryItems} />
+4 -4
查看文件
@@ -740,7 +740,7 @@ export function ReimbursementDetailPage() {
if (!reimbursementId) { if (!reimbursementId) {
return ( return (
<div className="card-std p-6"> <div className="card-std p-5">
<p className="text-sm text-red-600">缺少 reimbursementId 参数。</p> <p className="text-sm text-red-600">缺少 reimbursementId 参数。</p>
</div> </div>
); );
@@ -760,7 +760,7 @@ export function ReimbursementDetailPage() {
: "加载报销详情失败"; : "加载报销详情失败";
return ( return (
<div className="card-std space-y-3 p-6"> <div className="card-std space-y-3 p-5">
<p className="text-sm text-red-600">{message}</p> <p className="text-sm text-red-600">{message}</p>
<Button asChild variant="secondary"> <Button asChild variant="secondary">
<Link to="/reimbursements">返回报销列表</Link> <Link to="/reimbursements">返回报销列表</Link>
@@ -1230,7 +1230,7 @@ export function ReimbursementDetailPage() {
</SectionPanel> </SectionPanel>
<SectionPanel title="明细与票据"> <SectionPanel title="明细与票据">
<div className="space-y-6"> <div className="space-y-4">
<div className="space-y-4" data-testid="reimbursement-items"> <div className="space-y-4" data-testid="reimbursement-items">
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between"> <div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div className="space-y-1"> <div className="space-y-1">
@@ -1465,7 +1465,7 @@ export function ReimbursementDetailPage() {
</SectionPanel> </SectionPanel>
<SectionPanel title="预算与审核提示"> <SectionPanel title="预算与审核提示">
<div className="space-y-6"> <div className="space-y-4">
{budgetHint ? ( {budgetHint ? (
<div className="detail-command-panel text-sm" data-testid="reimbursement-budget-hint"> <div className="detail-command-panel text-sm" data-testid="reimbursement-budget-hint">
<h3 className="text-base font-semibold text-zinc-900">预算提示</h3> <h3 className="text-base font-semibold text-zinc-900">预算提示</h3>
+4 -4
查看文件
@@ -838,7 +838,7 @@ export function ReimbursementsPage() {
{batchMode ? ( {batchMode ? (
<div className="fixed bottom-6 left-1/2 z-40 w-[min(760px,calc(100vw-2rem))] -translate-x-1/2"> <div className="fixed bottom-6 left-1/2 z-40 w-[min(760px,calc(100vw-2rem))] -translate-x-1/2">
<div className="rounded-2xl border border-zinc-200 bg-white p-4 shadow-lg"> <div className="rounded-lg border border-zinc-200 bg-white p-3.5 shadow-lg">
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between"> <div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div className="text-sm text-zinc-700">已选 {selectedItems.length} 项可处理报销待办</div> <div className="text-sm text-zinc-700">已选 {selectedItems.length} 项可处理报销待办</div>
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
@@ -869,7 +869,7 @@ export function ReimbursementsPage() {
</DialogHeader> </DialogHeader>
<div className="space-y-4"> <div className="space-y-4">
<div className="rounded-2xl border border-zinc-200 bg-zinc-50 p-4 text-sm text-zinc-700"> <div className="rounded-lg border border-zinc-200 bg-zinc-50 p-3.5 text-sm text-zinc-700">
共 {selectedItems.length} 项: 共 {selectedItems.length} 项:
<div className="mt-2 max-h-40 space-y-1 overflow-auto"> <div className="mt-2 max-h-40 space-y-1 overflow-auto">
{selectedItems.map((item) => ( {selectedItems.map((item) => (
@@ -923,9 +923,9 @@ export function ReimbursementsPage() {
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="max-h-[60vh] space-y-2 overflow-auto rounded-2xl border border-zinc-200 bg-zinc-50 p-4"> <div className="max-h-[60vh] space-y-2 overflow-auto rounded-lg border border-zinc-200 bg-zinc-50 p-3.5">
{batchResult?.results.map((item) => ( {batchResult?.results.map((item) => (
<div key={`${item.taskId}-${item.outcome}`} className="rounded-xl border border-zinc-200 bg-white px-4 py-3"> <div key={`${item.taskId}-${item.outcome}`} className="rounded-lg border border-zinc-200 bg-white px-4 py-3">
<div className="flex flex-col gap-2 md:flex-row md:items-center md:justify-between"> <div className="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
<div> <div>
<div className="font-medium text-zinc-900"> <div className="font-medium text-zinc-900">
+17 -10
查看文件
@@ -2,14 +2,14 @@ import { Link } from "react-router-dom";
import { ListWorkspace, WorkspaceHeader } from "../components/patterns/ListWorkspace"; import { ListWorkspace, WorkspaceHeader } from "../components/patterns/ListWorkspace";
const ITEMS = [ const ITEMS = [
{ to: "/system/workspace", title: "工作区设置", description: "调整小团队模式的导航与模块显隐。" }, { to: "/system/workspace", icon: "dashboard_customize", title: "工作区设置", description: "导航与模块显隐。" },
{ to: "/people", title: "人员管理", description: "处理入职、转正、调岗、离职与档案。" }, { to: "/people", icon: "badge", title: "人员管理", description: "入转调离与档案。" },
{ to: "/system/approval-rules", title: "审批规则", description: "维护仍在使用的简单审批链与规则。" }, { to: "/system/approval-rules", icon: "account_tree", title: "审批规则", description: "审批链与规则。" },
{ to: "/system/expense-standards", title: "费用标准", description: "维护报销费用类型上限与 warn/block 管控模式。" }, { to: "/system/expense-standards", icon: "rule_settings", title: "费用标准", description: "费用上限与管控。" },
{ to: "/system/reimbursement-templates", title: "报销模板", description: "维护差旅费、招待费、办公用品等常见报销模板。" }, { to: "/system/reimbursement-templates", icon: "receipt_long", title: "报销模板", description: "常见报销模板。" },
{ to: "/system/master-data", title: "主数据", description: "维护费用、法体、成本中心等主数据。" }, { to: "/system/master-data", icon: "database", title: "主数据", description: "法体、成本中心、供应商。" },
{ to: "/system/notification-channels", title: "通知渠道", description: "继续保留 IM/邮件等补充通知渠道。" }, { to: "/system/notification-channels", icon: "notifications", title: "通知渠道", description: "IM / 邮件补充通知。" },
{ to: "/system/integrations/erp", title: "外部系统桥接", description: "查看财务与外部系统桥接能力及出入口。" }, { to: "/system/integrations/erp", icon: "hub", title: "外部系统桥接", description: "财务出入口与桥接。" },
]; ];
export function SystemSettingsPage() { export function SystemSettingsPage() {
@@ -26,8 +26,15 @@ export function SystemSettingsPage() {
<div className="admin-tile-grid"> <div className="admin-tile-grid">
{ITEMS.map((item) => ( {ITEMS.map((item) => (
<Link key={item.to} to={item.to} className="admin-tile-card"> <Link key={item.to} to={item.to} className="admin-tile-card">
<div className="text-base font-semibold text-zinc-900">{item.title}</div> <div className="flex items-start gap-3">
<div className="mt-2 text-sm text-zinc-600">{item.description}</div> <span className="admin-tile-card__icon">
<span className="material-symbols-outlined text-[20px]">{item.icon}</span>
</span>
<span className="min-w-0">
<span className="block text-base font-semibold text-zinc-900">{item.title}</span>
<span className="mt-1 block text-sm leading-5 text-zinc-600">{item.description}</span>
</span>
</div>
</Link> </Link>
))} ))}
</div> </div>
+2 -2
查看文件
@@ -216,7 +216,7 @@ export function WorkflowTaskDetailPage() {
if (!taskId) { if (!taskId) {
return ( return (
<div className="card-std p-6"> <div className="card-std p-5">
<p className="text-sm text-red-600">缺少 taskId 参数。</p> <p className="text-sm text-red-600">缺少 taskId 参数。</p>
</div> </div>
); );
@@ -240,7 +240,7 @@ export function WorkflowTaskDetailPage() {
: "加载失败,请返回列表重试"; : "加载失败,请返回列表重试";
return ( return (
<div className="card-std space-y-3 p-6"> <div className="card-std space-y-3 p-5">
<p className="text-sm text-red-600">{message}</p> <p className="text-sm text-red-600">{message}</p>
<Button <Button
variant="secondary" variant="secondary"