统一详情页操作区模板(05-31-unify-business-detail-ant-design)

这个提交包含在:
2026-05-31 12:21:42 +08:00
父节点 7b5476a70e
当前提交 2bcd9012f7
共修改 11 个文件,包含 513 行新增和 178 行删除
@@ -326,6 +326,19 @@ matrices and actions explicit in each page.
lists, prefer `BusinessListToolbar`, `BusinessTable`, `BusinessTableFrame`, lists, prefer `BusinessListToolbar`, `BusinessTable`, `BusinessTableFrame`,
`BusinessMobileList` and shared empty states from `BusinessMobileList` and shared empty states from
`client/src/components/patterns/BusinessListTemplate.tsx`. `client/src/components/patterns/BusinessListTemplate.tsx`.
- For PC business detail pages, prefer the thin components in
`client/src/components/patterns/BusinessDetailTemplate.tsx`:
`BusinessDetailWorkspace`, `BusinessDetailMain`, `BusinessDetailAside`,
`BusinessDetailActionPanel`, `BusinessDetailFactGrid` and
`BusinessDetailFact`. They define the Ant Design Pro style coordinates only:
main facts/evidence/history on the left, status/actions/compact summary on
the right. Keep the page's business fields, permission checks, state
conditions and API calls explicit in the page component.
- `BusinessDetailActionPanel` is the default PC detail operation surface.
Use one primary action for save/create when the detail owns editing; use a
horizontal action bar for approval actions; move low-frequency actions into
`更多` or a clearly named grouped section. Do not add a second page-local
`操作` card or a separate `摘要` card under the same right column.
- Do not create a configuration-driven page generator for reimbursement, - Do not create a configuration-driven page generator for reimbursement,
procurement, contracts, projects, assets, payments, AP or invoices. These procurement, contracts, projects, assets, payments, AP or invoices. These
pages have different permissions, state transitions, attachment rules and pages have different permissions, state transitions, attachment rules and
@@ -0,0 +1,3 @@
{"file": ".trellis/spec/hyb-oa/frontend-react-vite-design.md", "reason": "Verify detail pages follow shared action placement and template contracts."}
{"file": ".trellis/spec/hyb-oa/project-governance.md", "reason": "Verify task scope, validation and commit governance."}
{"file": ".trellis/tasks/05-31-unify-business-detail-ant-design/research/detail-page-pattern.md", "reason": "Verify implementation matches the selected task slice."}
@@ -0,0 +1,3 @@
{"file": ".trellis/spec/hyb-oa/frontend-react-vite-design.md", "reason": "Frontend page template, action placement and mature enterprise design rules for detail pages."}
{"file": ".trellis/spec/hyb-oa/project-governance.md", "reason": "Trellis task, solo developer scope and commit rules."}
{"file": ".trellis/tasks/05-31-unify-business-detail-ant-design/research/detail-page-pattern.md", "reason": "Task-specific detail page pattern findings and implementation slice."}
@@ -0,0 +1,35 @@
# Implementation Notes
## Slice
This task implements the first reusable PC business detail slice:
* Add `BusinessDetailTemplate` as a thin layout component family.
* Apply it to `AssetRegistryPage.tsx` detail mode.
* Apply it to `WorkflowTaskDetailPage.tsx` action/summary area.
* Keep business fields, permission checks, state rules and API calls in the
owning page components.
## Design Rules
* Follow mature Ant Design Pro style coordinates: main facts and evidence on
the left, status/actions/compact summary on the right.
* Do not introduce a configuration-driven page generator.
* Do not duplicate page-local `操作` and `摘要` cards in the same right column.
* Approval details keep one horizontal action bar: `通过`, `退回`, `驳回`,
`更多 -> 转审`.
* Asset details keep save/create as the primary action; transfer actions stay
in the same right-side operation panel as a grouped section.
## Verification Notes
* `cd client && npm run typecheck` passed.
* `cd client && npm run build` passed.
* `workflow-action-confirm-ui.spec.ts` passed.
* `pc-detail-mobile-visual-wave3.spec.ts -g "Wave3 PC"` passed and screenshot
review confirmed the approval action area no longer renders an empty divider.
* `assets-flow.spec.ts` was rerun with local backend started. It passed login
and asset creation, then local auth refresh returned `401` after a full
`page.goto(/assets?id=...)`, redirecting to `/login`. This is recorded as a
local authentication-refresh/test-environment blocker, not a detail-template
regression.
@@ -0,0 +1,92 @@
# 统一业务详情页操作区 Ant Design 风格
## Goal
在上一阶段统一业务列表之后,继续统一 PC 详情页和详情操作区。目标是采用成熟 Ant Design / Ant Design Pro 的详情页心智:顶部摘要、主内容事实区、右侧状态/操作面板、底部历史或关联信息,减少自由堆叠的大卡片和页面级例外,让用户在资产、审批、报销、采购、项目、付款等详情页里始终知道“信息在哪看、动作在哪做”。
## What I Already Know
* 用户明确要求页面、操作等参考成熟系统,不做创新控件。
* 当前产品面向国内小于 10 人科技初创公司,功能要少出错、低维护成本。
* 上一任务已落地 `BusinessListTemplate`,并沉淀“薄模板 + 页面契约,不做配置化生成器”的规则。
* 现有规范要求:PC 详情页使用 Ant Design Pro 风格详情工作区;主事实/证据在左,状态和高风险操作在右;审批详情使用一条操作栏,不使用竖向操作卡片。
* 代码中已有 `detail-workspace-grid`、`detail-command-panel`、`detail-fact-grid`、`detail-action-stack`、`DetailSideMeta`、`EntityOverviewHero` 等基础样式和组件。
* `ReimbursementDetailPage.tsx`、`ProjectDetailPage.tsx`、`WorkflowTaskDetailPage.tsx` 已部分符合详情工作区;`AssetRegistryPage.tsx` 详情页仍较复杂,操作区和摘要表现与其他详情页不一致。
## Current Findings
* `AssetRegistryPage.tsx` 详情页是当前最明显的首批改造对象:主内容和右侧操作区虽然已有两列,但右侧 `操作` + `资产摘要` 是页面局部拼装,资产流转字段常驻在操作面板内,视觉密度和其他详情页不一致。
* `WorkflowTaskDetailPage.tsx` 已有横向 `workflow-action-bar`,但还没有复用通用详情操作组件;应沉淀为模板契约,避免后续页面再出现竖排动作卡。
* `ReimbursementDetailPage.tsx` 和 `ProjectDetailPage.tsx` 已是较好的参照,不应在本轮大幅改动。
* `PurchaseRequestsPage.tsx` 详情模式仍混有“当前状态与下一步”解释文案,但采购详情结构较大,本轮先不重排全部采购表单。
* `PaymentBatchesPage.tsx` 付款详情仍有财务工作台特性,涉及真实付款动作,不在首批详情模板改造里做行为调整。
## Scope
本任务修改前端详情页体验与相关前端测试,范围包括共享详情模板组件、资产详情页首批改造、审批详情动作区模板化复核,以及稳定设计规则沉淀。后端接口、数据库、权限模型、审批引擎、真实付款能力、公网部署不在范围内。
## Requirements
* 采用“模板组件 + 页面契约”的方式推进,不做配置驱动详情页生成器。
* 新增轻量 `BusinessDetailTemplate` 组件族,复用现有 detail CSS 类,不重建视觉系统:
* `BusinessDetailWorkspace`:统一 PC 两列详情布局。
* `BusinessDetailMain` / `BusinessDetailAside`:约束主内容和右侧操作/摘要区域。
* `BusinessDetailActionPanel`:统一右侧状态、主操作、次级操作、更多操作和摘要。
* `BusinessDetailFactGrid` / `BusinessDetailFact`:统一详情摘要事实卡。
* 首批落地页面:
* `AssetRegistryPage.tsx`:把详情页右侧操作区改为统一操作面板;保存/创建作为主操作;领用、归还、调拨作为流转操作区;资产摘要合并到同一右侧面板或标准事实区,避免独立局部卡片风格。
* `WorkflowTaskDetailPage.tsx`:保持一条审批操作栏,使用统一详情操作组件承载;`通过` 主按钮,`退回`、`驳回` 次级按钮,`转审` 放入更多;原因输入仍在点击后出现。
* 保持已有业务状态和权限判断显式写在页面内,不把状态矩阵隐藏进模板配置。
* 不改变资产保存、领用、归还、调拨、审批通过/退回/驳回/转审的后端调用和权限条件。
* PC 详情默认两列;移动端不在本轮重构,但不得破坏现有移动路由。
* 不新增解释普通流程的默认可见文案;异常、敏感字段、不可逆动作说明可以保留。
## Acceptance Criteria
* [x] 新增详情模板组件,页面不再各自拼装右侧操作/摘要面板。
* [x] 资产详情页使用统一右侧操作面板,保存/创建是主操作,流转动作在同一操作区内有清晰分组。
* [x] 资产摘要不再作为独立局部风格卡片堆在操作区下方,而是进入统一事实/摘要结构。
* [x] 审批详情页保持横向一条动作栏,不出现多个竖向主操作卡片。
* [ ] 既有资产流转、保存和审批动作测试仍通过。审批动作已通过;资产主链路在本地后端重跑时通过登录和创建,随后因本地 auth refresh `401` 重定向登录,未进入详情断言。
* [x] 设计规范记录详情模板组件契约和首批适用范围。
## Test Plan
* `cd client && npm run typecheck`
* `cd client && npm run build`
* `cd client && E2E_BASE_URL=http://127.0.0.1:5173 npx playwright test tests/e2e/assets-flow.spec.ts tests/e2e/workflow-action-confirm-ui.spec.ts --workers=1 --reporter=list`
* `cd client && E2E_BASE_URL=http://127.0.0.1:5173 npx playwright test tests/e2e/pc-detail-mobile-visual-wave3.spec.ts -g "Wave3 PC" --workers=1 --reporter=list`
* `git diff --check`
* `python3 ./.trellis/scripts/task.py validate 05-31-unify-business-detail-ant-design`
* `node scripts/qa/check-trellis-governance.mjs`
## Definition Of Done
* 前端类型检查和构建通过。
* 聚焦资产和审批动作 E2E 通过。
* 详情页视觉冒烟覆盖通过。
* Trellis task 校验和治理检查通过。
* 稳定设计规则更新到 `.trellis/spec/hyb-oa/frontend-react-vite-design.md`。
## Verification Results
* `cd client && npm run typecheck` passed.
* `cd client && npm run build` passed.
* `workflow-action-confirm-ui.spec.ts` passed.
* `pc-detail-mobile-visual-wave3.spec.ts -g "Wave3 PC"` passed; screenshot review confirmed the right-side approval action panel has one horizontal action bar and no empty divider.
* `assets-flow.spec.ts` blocked by local auth refresh after a full page reload: the test passed login and `POST /api/assets`, then `/api/auth/refresh` returned `401` and the app redirected to `/login`.
## Out Of Scope
* 不一次性重构所有详情页、编辑页和移动详情页。
* 不引入 Ant Design 组件库替换当前 UI 基座。
* 不做配置化详情页生成器。
* 不调整后端权限、审批状态机、资产状态机、真实付款执行逻辑或数据库结构。
* 不做公网部署,除非后续单独获得明确批准。
## Technical Notes
* 视觉基线:成熟 Ant Design / Ant Design Pro 详情页模式,但使用项目现有 React/Tailwind 组件。
* 现有详情样式:`detail-workspace-grid`、`detail-command-panel`、`detail-fact-grid`、`detail-fact-card`、`detail-action-stack`。
* 现有参照页面:`ReimbursementDetailPage.tsx`、`ProjectDetailPage.tsx`、`WorkflowTaskDetailPage.tsx`。
* 首批主要改造页面:`AssetRegistryPage.tsx`、`WorkflowTaskDetailPage.tsx`。
@@ -0,0 +1,27 @@
# Detail Page Pattern Notes
## Mature Enterprise Baseline
For hyb-oa, the practical baseline is Ant Design Pro style rather than a novel visual design:
* Top summary identifies the object and status.
* Main column contains facts, editable fields, evidence, relations and history.
* Right column contains the current state, allowed actions and compact metadata.
* Primary action appears once. Secondary actions stay nearby; risky or low-frequency actions move to `更多` or a grouped action section.
* Explanatory copy is reserved for exceptions, irreversible actions, sensitive data or external-system boundaries.
## Repo Findings
* Existing CSS already supports the target: `detail-workspace-grid`, `detail-command-panel`, `detail-fact-grid`, `detail-fact-card`, `detail-action-stack`.
* `ReimbursementDetailPage.tsx` and `ProjectDetailPage.tsx` are good references for a two-column detail workspace.
* `WorkflowTaskDetailPage.tsx` already has a horizontal approval action bar and should be protected from regressing into vertical action cards.
* `AssetRegistryPage.tsx` is the clearest first implementation target because the right-side operation and summary area is page-local and dense.
## Recommended Task Slice
Implement a thin `BusinessDetailTemplate` component family first, then apply it to:
1. Asset detail right-side action/summary area.
2. Workflow task detail action panel.
Leave reimbursement, project, purchase and payment detail pages for later slices unless the shared template can be adopted without changing behavior.
@@ -0,0 +1,26 @@
{
"id": "unify-business-detail-ant-design",
"name": "unify-business-detail-ant-design",
"title": "统一业务详情页操作区 Ant Design 风格",
"description": "统一 PC 业务详情页和详情操作区为成熟 Ant Design Pro 风格,新增轻量详情模板并首批改造资产详情和审批详情动作区。",
"status": "in_progress",
"dev_type": null,
"scope": "client",
"package": "hyb-oa",
"priority": "P2",
"creator": "shunli",
"assignee": "shunli",
"createdAt": "2026-05-31",
"completedAt": null,
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}
@@ -0,0 +1,117 @@
import { Children, type ReactNode } from "react";
import { cn } from "../../lib/utils";
import { SectionPanel } from "./SectionPanel";
export function BusinessDetailWorkspace({
children,
className,
testId,
}: {
children: ReactNode;
className?: string;
testId?: string;
}) {
return (
<div className={cn("detail-workspace-grid", className)} data-testid={testId}>
{children}
</div>
);
}
export function BusinessDetailMain({
children,
className,
}: {
children: ReactNode;
className?: string;
}) {
return <div className={cn("space-y-5", className)}>{children}</div>;
}
export function BusinessDetailAside({
children,
className,
}: {
children: ReactNode;
className?: string;
}) {
return <div className={cn("space-y-5", className)}>{children}</div>;
}
export function BusinessDetailFactGrid({
children,
className,
testId,
}: {
children: ReactNode;
className?: string;
testId?: string;
}) {
return (
<div className={cn("detail-fact-grid", className)} data-testid={testId}>
{children}
</div>
);
}
export function BusinessDetailFact({
label,
children,
className,
}: {
label: ReactNode;
children: ReactNode;
className?: string;
}) {
return (
<div className={cn("detail-fact-card", className)}>
<div className="detail-fact-card__label">{label}</div>
<div className="detail-fact-card__value">{children}</div>
</div>
);
}
export function BusinessDetailActionPanel({
title = "操作",
status,
primaryAction,
secondaryActions,
moreActions,
summary,
children,
className,
testId,
}: {
title?: string;
status?: ReactNode;
primaryAction?: ReactNode;
secondaryActions?: ReactNode;
moreActions?: ReactNode;
summary?: ReactNode;
children?: ReactNode;
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);
return (
<SectionPanel title={title} actions={status} className={cn("detail-action-panel xl:sticky xl: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}
{hasChildren ? (
<div className={cn(hasActionContent && "border-t border-zinc-200 pt-3")}>{children}</div>
) : null}
{summary ? (
<div className={cn(hasContentBeforeSummary && "border-t border-zinc-200 pt-3")}>
<div className="space-y-2 text-sm text-zinc-600">{summary}</div>
</div>
) : null}
</div>
</SectionPanel>
);
}
+4
查看文件
@@ -653,6 +653,10 @@
@apply grid gap-3 overflow-visible; @apply grid gap-3 overflow-visible;
} }
.detail-action-panel {
overflow: visible;
}
.detail-action-card { .detail-action-card {
border-color: rgba(226, 232, 240, 0.94); border-color: rgba(226, 232, 240, 0.94);
background: rgba(255, 255, 255, 0.96); background: rgba(255, 255, 255, 0.96);
+56 -50
查看文件
@@ -29,14 +29,20 @@ import { hasAnyPermission, hasPermission } from "../auth/permissions";
import { DateTextInput } from "../components/common/DateTextInput"; import { DateTextInput } from "../components/common/DateTextInput";
import { StatusBadge } from "../components/common/StatusBadge"; import { StatusBadge } from "../components/common/StatusBadge";
import { TableSkeleton } from "../components/common/TableSkeleton"; import { TableSkeleton } from "../components/common/TableSkeleton";
import { DetailSideMeta } from "../components/patterns/DetailSideMeta"; import {
BusinessDetailActionPanel,
BusinessDetailAside,
BusinessDetailFact,
BusinessDetailFactGrid,
BusinessDetailMain,
BusinessDetailWorkspace,
} from "../components/patterns/BusinessDetailTemplate";
import { ListWorkspace, PrimaryTable, WorkspaceHeader } from "../components/patterns/ListWorkspace"; import { ListWorkspace, PrimaryTable, WorkspaceHeader } from "../components/patterns/ListWorkspace";
import { import {
BusinessListToolbar, BusinessListToolbar,
BusinessMobileList, BusinessMobileList,
BusinessTable, BusinessTable,
} from "../components/patterns/BusinessListTemplate"; } from "../components/patterns/BusinessListTemplate";
import { SectionPanel } from "../components/patterns/SectionPanel";
import { Button } from "../components/ui/button"; import { Button } from "../components/ui/button";
import { Input } from "../components/ui/input"; import { Input } from "../components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table";
@@ -1044,7 +1050,8 @@ export function AssetRegistryPage() {
</div> </div>
</div> </div>
) : ( ) : (
<div className="grid gap-5 xl:grid-cols-[minmax(0,1fr)_320px]"> <BusinessDetailWorkspace>
<BusinessDetailMain>
<div className="card-std space-y-4 p-4 md:p-5"> <div className="card-std space-y-4 p-4 md:p-5">
{selectedAssetId && !isCreatingAsset && assetDetailQuery.isLoading ? <TableSkeleton columns={2} rows={8} /> : null} {selectedAssetId && !isCreatingAsset && assetDetailQuery.isLoading ? <TableSkeleton columns={2} rows={8} /> : null}
{selectedAssetId && !isCreatingAsset && assetDetailQuery.isError ? ( {selectedAssetId && !isCreatingAsset && assetDetailQuery.isError ? (
@@ -1078,24 +1085,20 @@ export function AssetRegistryPage() {
) : null} ) : null}
{!isCreatingAsset && currentAsset ? ( {!isCreatingAsset && currentAsset ? (
<div className="grid gap-3 md:grid-cols-4" data-testid="asset-detail-summary"> <BusinessDetailFactGrid className="xl:grid-cols-4" testId="asset-detail-summary">
<div className="rounded-lg border border-zinc-200 bg-zinc-50 p-3 text-sm"> <BusinessDetailFact label="责任人">
<div className="text-xs text-zinc-500">责任人</div> {formatEmployeeSummary(currentAsset.owner ?? currentAsset.assignee)}
<div className="mt-1 font-medium text-zinc-900">{formatEmployeeSummary(currentAsset.owner ?? currentAsset.assignee)}</div> </BusinessDetailFact>
</div> <BusinessDetailFact label="保管/位置">
<div className="rounded-lg border border-zinc-200 bg-zinc-50 p-3 text-sm"> {currentAsset.keeper ? formatEmployeeSummary(currentAsset.keeper) : currentAsset.location ?? "待确认"}
<div className="text-xs text-zinc-500">保管/位置</div> </BusinessDetailFact>
<div className="mt-1 font-medium text-zinc-900">{currentAsset.keeper ? formatEmployeeSummary(currentAsset.keeper) : currentAsset.location ?? "待确认"}</div> <BusinessDetailFact label={isElectronicAssetType(currentAsset.assetType) ? "最近验证" : "最近盘点"}>
</div> {currentAsset.lastVerifiedAt ? formatDisplayDateTime(currentAsset.lastVerifiedAt) : "未登记"}
<div className="rounded-lg border border-zinc-200 bg-zinc-50 p-3 text-sm"> </BusinessDetailFact>
<div className="text-xs text-zinc-500">{isElectronicAssetType(currentAsset.assetType) ? "最近验证" : "最近盘点"}</div> <BusinessDetailFact label="状态">
<div className="mt-1 font-medium text-zinc-900">{currentAsset.lastVerifiedAt ? formatDisplayDateTime(currentAsset.lastVerifiedAt) : "未登记"}</div> <StatusBadge status={currentAsset.status} />
</div> </BusinessDetailFact>
<div className="rounded-lg border border-zinc-200 bg-zinc-50 p-3 text-sm"> </BusinessDetailFactGrid>
<div className="text-xs text-zinc-500">状态</div>
<div className="mt-1"><StatusBadge status={currentAsset.status} /></div>
</div>
</div>
) : null} ) : null}
<div className="space-y-6"> <div className="space-y-6">
@@ -1302,12 +1305,14 @@ export function AssetRegistryPage() {
</> </>
) : null} ) : null}
</div> </div>
</BusinessDetailMain>
{(isCreatingAsset || currentAsset) && !assetDetailQuery.isLoading ? ( {(isCreatingAsset || currentAsset) && !assetDetailQuery.isLoading ? (
<div className="space-y-5"> <BusinessDetailAside>
<SectionPanel title="操作" className="xl:sticky xl:top-24"> <BusinessDetailActionPanel
<div className="space-y-4"> status={!isCreatingAsset && currentAsset ? <StatusBadge status={currentAsset.status} /> : null}
{canManage ? ( primaryAction={canManage ? (
<Button <Button
className="w-full justify-center"
type="button" type="button"
data-testid="asset-save" data-testid="asset-save"
disabled={saveAssetMutation.isPending || !canEditCurrent} disabled={saveAssetMutation.isPending || !canEditCurrent}
@@ -1316,9 +1321,30 @@ export function AssetRegistryPage() {
{saveAssetMutation.isPending ? "保存中..." : isCreatingAsset ? "创建资产卡片" : "保存资产卡片"} {saveAssetMutation.isPending ? "保存中..." : isCreatingAsset ? "创建资产卡片" : "保存资产卡片"}
</Button> </Button>
) : null} ) : null}
summary={(
<>
<div>状态:{isCreatingAsset ? "新建中" : assetStatusLabel(currentAsset?.status)}</div>
<div>类型:{assetTypeLabel(assetForm.assetType)}</div>
<div>责任人:{formatEmployeeSummary(currentAsset?.owner ?? currentAsset?.assignee)}</div>
<div>归属人:{formatEmployeeSummary(currentAsset?.assignee)}</div>
<div>保管人:{formatEmployeeSummary(currentAsset?.keeper)}</div>
<div>位置:{(currentAsset?.location ?? assetForm.location) || "-"}</div>
<div>
{isElectronicAssetType(assetForm.assetType) ? "最近验证" : "最近盘点"}:
{currentAsset?.lastVerifiedAt ? formatDisplayDateTime(currentAsset.lastVerifiedAt) : assetForm.lastVerifiedAt || "-"}
</div>
<div>
采购来源:
{currentAsset?.purchaseRequest
? formatTextWithCompactLongNumbers(currentAsset.purchaseRequest.requestNo ?? currentAsset.purchaseRequest.id)
: "-"}
</div>
</>
)}
testId="asset-detail-action-panel"
>
{currentAsset && canTransfer && !isCreatingAsset && currentAsset.assetType === "fixed_asset" ? ( {currentAsset && canTransfer && !isCreatingAsset && currentAsset.assetType === "fixed_asset" ? (
<div className="space-y-3 border-t border-zinc-200 pt-4"> <div className="space-y-3">
<div className="text-sm font-semibold text-zinc-900">资产流转</div> <div className="text-sm font-semibold text-zinc-900">资产流转</div>
<div className="rounded-lg bg-zinc-50 px-3 py-2 text-xs text-zinc-600"> <div className="rounded-lg bg-zinc-50 px-3 py-2 text-xs text-zinc-600">
归属人:{formatEmployeeSummary(currentAsset.assignee)} · 位置:{currentAsset.location ?? "未设置"} 归属人:{formatEmployeeSummary(currentAsset.assignee)} · 位置:{currentAsset.location ?? "未设置"}
@@ -1399,30 +1425,10 @@ export function AssetRegistryPage() {
</div> </div>
</div> </div>
) : null} ) : null}
</div> </BusinessDetailActionPanel>
</SectionPanel> </BusinessDetailAside>
<DetailSideMeta title="资产摘要">
<div>状态:{isCreatingAsset ? "新建中" : assetStatusLabel(currentAsset?.status)}</div>
<div>类型:{assetTypeLabel(assetForm.assetType)}</div>
<div>责任人:{formatEmployeeSummary(currentAsset?.owner ?? currentAsset?.assignee)}</div>
<div>归属人:{formatEmployeeSummary(currentAsset?.assignee)}</div>
<div>保管人:{formatEmployeeSummary(currentAsset?.keeper)}</div>
<div>位置:{(currentAsset?.location ?? assetForm.location) || "-"}</div>
<div>
{isElectronicAssetType(assetForm.assetType) ? "最近验证" : "最近盘点"}:
{currentAsset?.lastVerifiedAt ? formatDisplayDateTime(currentAsset.lastVerifiedAt) : assetForm.lastVerifiedAt || "-"}
</div>
<div>
采购来源:
{currentAsset?.purchaseRequest
? formatTextWithCompactLongNumbers(currentAsset.purchaseRequest.requestNo ?? currentAsset.purchaseRequest.id)
: "-"}
</div>
</DetailSideMeta>
</div>
) : null} ) : null}
</div> </BusinessDetailWorkspace>
)} )}
</ListWorkspace> </ListWorkspace>
); );
+40 -31
查看文件
@@ -19,7 +19,12 @@ import { buildMobileApprovalsPath, shouldPreferMobileApprovalExperience } from "
import { CardSkeleton } from "../components/common/CardSkeleton"; import { CardSkeleton } from "../components/common/CardSkeleton";
import { ConfirmDialog } from "../components/common/ConfirmDialog"; import { ConfirmDialog } from "../components/common/ConfirmDialog";
import { StatusBadge } from "../components/common/StatusBadge"; import { StatusBadge } from "../components/common/StatusBadge";
import { DetailSideMeta } from "../components/patterns/DetailSideMeta"; import {
BusinessDetailActionPanel,
BusinessDetailAside,
BusinessDetailMain,
BusinessDetailWorkspace,
} from "../components/patterns/BusinessDetailTemplate";
import { EntityOverviewHero } from "../components/patterns/EntityOverviewHero"; import { EntityOverviewHero } from "../components/patterns/EntityOverviewHero";
import { StatStrip } from "../components/patterns/ListWorkspace"; import { StatStrip } from "../components/patterns/ListWorkspace";
import { MetricCard } from "../components/patterns/MetricCard"; import { MetricCard } from "../components/patterns/MetricCard";
@@ -298,8 +303,8 @@ export function WorkflowTaskDetailPage() {
)} )}
/> />
<div className="detail-workspace-grid"> <BusinessDetailWorkspace>
<div className="space-y-5"> <BusinessDetailMain>
<SectionPanel title="当前状态与审批对象"> <SectionPanel title="当前状态与审批对象">
<div className="space-y-4" data-testid="workflow-task-overview"> <div className="space-y-4" data-testid="workflow-task-overview">
<StatStrip columns={4} className="ops-status-strip"> <StatStrip columns={4} className="ops-status-strip">
@@ -438,21 +443,14 @@ export function WorkflowTaskDetailPage() {
)} )}
</div> </div>
</SectionPanel> </SectionPanel>
</div> </BusinessDetailMain>
<div className="space-y-5"> <BusinessDetailAside>
<SectionPanel title="处理" className="workflow-action-panel"> <BusinessDetailActionPanel
<div className="detail-action-stack"> title="处理"
{actionError ? ( className="workflow-action-panel xl:top-20"
<p secondaryActions={(
className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600" <>
data-testid="workflow-task-action-error"
>
{actionError}
</p>
) : null}
<div className="workflow-action-bar">
<Button <Button
data-testid="workflow-task-approve" data-testid="workflow-task-approve"
disabled={!allowedActions.has("approve") || mutation.isPending} disabled={!allowedActions.has("approve") || mutation.isPending}
@@ -491,7 +489,28 @@ export function WorkflowTaskDetailPage() {
</button> </button>
</details> </details>
) : null} ) : null}
</div> </>
)}
summary={(
<>
<div>节点:{detail.task.stepName}</div>
<div>实例状态:{workflowInstanceStatusLabel(detail.workflowInstance?.currentStatus)}</div>
<div>当前审批角色:{detail.task.assigneeRole ?? "-"}</div>
<div>创建时间:{formatDateTime(detail.task.createdAt)}</div>
<div>完成时间:{formatDateTime(detail.task.completedAt)}</div>
<div>发起时间:{formatDateTime(detail.workflowInstance?.startedAt)}</div>
</>
)}
testId="workflow-task-action-panel"
>
{actionError ? (
<p
className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600"
data-testid="workflow-task-action-error"
>
{actionError}
</p>
) : null}
{activeSecondaryAction === "reject" ? ( {activeSecondaryAction === "reject" ? (
<div className="detail-action-card space-y-2"> <div className="detail-action-card space-y-2">
@@ -573,19 +592,9 @@ export function WorkflowTaskDetailPage() {
</Button> </Button>
</div> </div>
) : null} ) : null}
</div> </BusinessDetailActionPanel>
</SectionPanel> </BusinessDetailAside>
</BusinessDetailWorkspace>
<DetailSideMeta title="任务摘要">
<div>节点:{detail.task.stepName}</div>
<div>实例状态:{workflowInstanceStatusLabel(detail.workflowInstance?.currentStatus)}</div>
<div>当前审批角色:{detail.task.assigneeRole ?? "-"}</div>
<div>创建时间:{formatDateTime(detail.task.createdAt)}</div>
<div>完成时间:{formatDateTime(detail.task.completedAt)}</div>
<div>发起时间:{formatDateTime(detail.workflowInstance?.startedAt)}</div>
</DetailSideMeta>
</div>
</div>
</PageScaffold> </PageScaffold>
<ConfirmDialog <ConfirmDialog
open={Boolean(confirmCopy)} open={Boolean(confirmCopy)}