统一首批页面成熟设计基线(05-30-unified-page-design-review)

这个提交包含在:
2026-05-30 17:47:00 +08:00
父节点 6ae6d82355
当前提交 218ea9ad10
共修改 24 个文件,包含 837 行新增和 326 行删除
+172 -150
查看文件
@@ -115,31 +115,7 @@ function isAlreadyProcessedTaskError(error: unknown) {
return isApiError(error) && error.details?.task === "already_processed";
}
function resolveWorkflowNextActionText(input: {
taskStatus: string;
entityType?: string | null;
allowedActions: string[];
}) {
if (input.taskStatus !== "pending") {
return "当前审批任务已结束,本页用于查看摘要、依据和处理历史。";
}
const entityName = entityTitle(input.entityType ?? undefined);
if (input.allowedActions.includes("approve")) {
return `先核对${entityName}摘要、金额/时间和附件依据;确认无误后通过,存在缺口时退回补充。`;
}
if (input.allowedActions.includes("return")) {
return `当前节点可退回,请说明需要补充的${entityName}材料或字段。`;
}
if (input.allowedActions.includes("reject")) {
return `当前节点可驳回,请写清业务原因,便于申请人和后续审计理解。`;
}
if (input.allowedActions.includes("transfer")) {
return "当前节点可转审,请选择更合适的审批人并说明原因。";
}
return "当前账号没有可执行动作,请返回待办列表或联系管理员核对权限。";
}
type SecondaryWorkflowAction = "reject" | "return" | "transfer";
export function WorkflowTaskDetailPage() {
const { taskId = "" } = useParams();
@@ -152,7 +128,9 @@ export function WorkflowTaskDetailPage() {
const [transferReason, setTransferReason] = useState("当前事项需转交对应审批人处理,请尽快处理。");
const [selectedApprover, setSelectedApprover] = useState("");
const [actionError, setActionError] = useState("");
const [activeSecondaryAction, setActiveSecondaryAction] = useState<SecondaryWorkflowAction | null>(null);
const [confirmAction, setConfirmAction] = useState<WorkflowConfirmAction | null>(null);
const [transferConfirmOpen, setTransferConfirmOpen] = useState(false);
const detailQuery = useQuery({
enabled: Boolean(taskId),
@@ -200,6 +178,8 @@ export function WorkflowTaskDetailPage() {
queryClient.invalidateQueries({ queryKey: ["workflow", "task", taskId] }),
]);
setActionError("");
setActiveSecondaryAction(null);
setTransferConfirmOpen(false);
const messages: Record<string, string> = { approve: "已通过", reject: "已驳回", return: "已退回", transfer: "已转审" };
toast.success(messages[action] ?? "操作成功");
},
@@ -240,6 +220,20 @@ export function WorkflowTaskDetailPage() {
setConfirmAction(action);
}
function requestWorkflowTransfer() {
if (!selectedApprover) {
setActionError("请选择转审目标");
return;
}
if (transferReason.trim().length < 10) {
setActionError("转审理由至少 10 个字");
return;
}
setActionError("");
setTransferConfirmOpen(true);
}
if (!taskId) {
return (
<div className="card-std p-5">
@@ -283,23 +277,18 @@ export function WorkflowTaskDetailPage() {
const canTransfer = allowedActions.has("transfer");
const allowedActionLabels = detail.allowedActions.map(workflowActionLabel).join(" / ");
const businessStatusLabel = workflowInstanceStatusLabel(detail.workflowInstance?.currentStatus);
const nextActionText = resolveWorkflowNextActionText({
taskStatus: detail.task.status,
entityType: detail.workflowInstance?.entityType,
allowedActions: detail.allowedActions,
});
return (
<>
<PageScaffold>
<EntityOverviewHero
title={`任务详情 · ${detail.task.stepName}`}
subtitle="先核对业务摘要和证据,再处理通过、退回、驳回或转审。"
title="审批详情"
meta={(
<>
<StatusBadge status={detail.task.status} />
<span className="info-chip">业务状态:{businessStatusLabel}</span>
<span className="info-chip">类型:{entityTitle(detail.workflowInstance?.entityType)}</span>
<span className="info-chip">节点:{detail.task.stepName}</span>
</>
)}
actions={(
@@ -311,20 +300,8 @@ export function WorkflowTaskDetailPage() {
<div className="detail-workspace-grid">
<div className="space-y-5">
<SectionPanel title="当前状态与审批对象" description="首屏只放判断审批所需的状态、摘要和下一步。">
<SectionPanel title="当前状态与审批对象">
<div className="space-y-4" data-testid="workflow-task-overview">
<div className="detail-command-panel" data-testid="workflow-task-next-action">
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
<div className="min-w-0">
<div className="text-xs font-semibold text-zinc-500">下一步</div>
<p className="mt-1 text-sm leading-6 text-zinc-700">{nextActionText}</p>
</div>
<div className="shrink-0 rounded-lg border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-700">
可执行:<span className="font-semibold text-zinc-900">{allowedActionLabels || "无"}</span>
</div>
</div>
</div>
<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>} />
@@ -444,107 +421,6 @@ export function WorkflowTaskDetailPage() {
</SectionPanel>
) : null}
<SectionPanel title="审批动作" description="高风险动作会再次确认;驳回、退回和转审需要留下可复核说明。">
<div className="detail-action-stack">
{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}
<div className="detail-action-card flex flex-wrap gap-2">
<Button
data-testid="workflow-task-approve"
disabled={!allowedActions.has("approve") || mutation.isPending}
onClick={() => requestWorkflowAction("approve")}
type="button"
>
通过
</Button>
</div>
<div className="detail-action-card space-y-2">
<p className="text-sm font-medium text-zinc-800">驳回</p>
<Textarea
data-testid="workflow-task-reject-comment"
onChange={(event) => setRejectComment(event.target.value)}
rows={2}
value={rejectComment}
/>
<Button
data-testid="workflow-task-reject"
variant="secondary"
disabled={!allowedActions.has("reject") || mutation.isPending || rejectComment.trim().length < 1}
onClick={() => requestWorkflowAction("reject")}
type="button"
>
提交驳回
</Button>
</div>
<div className="detail-action-card space-y-2">
<p className="text-sm font-medium text-zinc-800">退回</p>
<Textarea
data-testid="workflow-task-return-comment"
onChange={(event) => setReturnComment(event.target.value)}
rows={2}
value={returnComment}
/>
<Button
data-testid="workflow-task-return"
variant="secondary"
disabled={!allowedActions.has("return") || mutation.isPending || returnComment.trim().length < 1}
onClick={() => requestWorkflowAction("return")}
type="button"
>
提交退回
</Button>
</div>
{canTransfer ? (
<div className="detail-action-card space-y-2">
<p className="text-sm font-medium text-zinc-800">转审</p>
<select
className="select-std"
data-testid="workflow-task-transfer-approver"
onChange={(event) => setSelectedApprover(event.target.value)}
value={selectedApprover}
>
<option value="">请选择目标审批人</option>
{(approverQuery.data?.items ?? []).map((item) => (
<option key={item.id} value={item.id}>
{item.name} ({item.employeeNo})
</option>
))}
</select>
<Textarea
data-testid="workflow-task-transfer-reason"
onChange={(event) => setTransferReason(event.target.value)}
rows={2}
value={transferReason}
/>
<Button
data-testid="workflow-task-transfer"
variant="secondary"
disabled={
!allowedActions.has("transfer") ||
mutation.isPending ||
!selectedApprover ||
transferReason.trim().length < 10
}
onClick={() => mutation.mutate("transfer")}
type="button"
>
提交转审
</Button>
</div>
) : null}
</div>
</SectionPanel>
<SectionPanel title="处理历史">
<div className="detail-timeline">
{detail.history.length ? (
@@ -565,16 +441,148 @@ export function WorkflowTaskDetailPage() {
</div>
<div className="space-y-5">
<DetailSideMeta title="任务摘要" description="保留审批留痕所需的节点、角色和时间。">
<SectionPanel title="处理" className="workflow-action-panel">
<div className="detail-action-stack">
{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}
<div className="workflow-action-bar">
<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>
) : null}
</div>
{activeSecondaryAction === "reject" ? (
<div className="detail-action-card space-y-2">
<p className="text-sm font-medium text-zinc-800">驳回意见</p>
<Textarea
data-testid="workflow-task-reject-comment"
onChange={(event) => setRejectComment(event.target.value)}
rows={3}
value={rejectComment}
/>
<Button
data-testid="workflow-task-reject"
variant="secondary"
disabled={!allowedActions.has("reject") || mutation.isPending || rejectComment.trim().length < 1}
onClick={() => requestWorkflowAction("reject")}
type="button"
>
确认驳回
</Button>
</div>
) : null}
{activeSecondaryAction === "return" ? (
<div className="detail-action-card space-y-2">
<p className="text-sm font-medium text-zinc-800">退回意见</p>
<Textarea
data-testid="workflow-task-return-comment"
onChange={(event) => setReturnComment(event.target.value)}
rows={3}
value={returnComment}
/>
<Button
data-testid="workflow-task-return"
variant="secondary"
disabled={!allowedActions.has("return") || mutation.isPending || returnComment.trim().length < 1}
onClick={() => requestWorkflowAction("return")}
type="button"
>
确认退回
</Button>
</div>
) : null}
{activeSecondaryAction === "transfer" && canTransfer ? (
<div className="detail-action-card space-y-2">
<p className="text-sm font-medium text-zinc-800">转审</p>
<select
className="select-std"
data-testid="workflow-task-transfer-approver"
onChange={(event) => setSelectedApprover(event.target.value)}
value={selectedApprover}
>
<option value="">请选择目标审批人</option>
{(approverQuery.data?.items ?? []).map((item) => (
<option key={item.id} value={item.id}>
{item.name} ({item.employeeNo})
</option>
))}
</select>
<Textarea
data-testid="workflow-task-transfer-reason"
onChange={(event) => setTransferReason(event.target.value)}
rows={3}
value={transferReason}
/>
<Button
data-testid="workflow-task-transfer"
variant="secondary"
disabled={
!allowedActions.has("transfer") ||
mutation.isPending ||
!selectedApprover ||
transferReason.trim().length < 10
}
onClick={requestWorkflowTransfer}
type="button"
>
确认转审
</Button>
</div>
) : null}
</div>
</SectionPanel>
<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>
<div className="rounded-lg border border-zinc-200 bg-zinc-50 p-3 text-zinc-700">
{nextActionText}
</div>
</DetailSideMeta>
</div>
</div>
@@ -600,6 +608,20 @@ export function WorkflowTaskDetailPage() {
await mutation.mutateAsync(confirmAction);
}}
/>
<ConfirmDialog
open={transferConfirmOpen}
onOpenChange={setTransferConfirmOpen}
title="确认转审审批?"
description={`事项:${entityTitle(detail.workflowInstance?.entityType)} · ${detail.task.stepName}。转审后当前任务会交给目标审批人处理,系统会写入审批历史。`}
confirmLabel="确认转审"
variant="default"
testId="workflow-transfer-confirm-dialog"
confirmTestId="workflow-transfer-confirm-submit"
cancelTestId="workflow-transfer-confirm-cancel"
onConfirm={async () => {
await mutation.mutateAsync("transfer");
}}
/>
</>
);
}