重构审批中心与报销详情降噪(approval-detail-simplification-refactor)
这个提交包含在:
@@ -21,7 +21,6 @@ import {
|
||||
StatStrip,
|
||||
WorkspaceEmptyState,
|
||||
WorkspaceHeader,
|
||||
WorkspaceNotice,
|
||||
} from "../components/patterns/ListWorkspace";
|
||||
import { MetricCard } from "../components/patterns/MetricCard";
|
||||
import { Button } from "../components/ui/button";
|
||||
@@ -137,12 +136,12 @@ function isAlreadyProcessedTaskError(error: unknown) {
|
||||
|
||||
export function WorkflowTasksPage() {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [confirmApproveTaskId, setConfirmApproveTaskId] = useState<string | null>(null);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const status = normalizeStatusFilter(searchParams.get("status"));
|
||||
const entityType = normalizeEntityFilter(searchParams.get("entityType"));
|
||||
const page = Number(searchParams.get("page") ?? "1") || 1;
|
||||
const [confirmApproveTaskId, setConfirmApproveTaskId] = useState<string | null>(null);
|
||||
const [showAdvanced, setShowAdvanced] = useState(() => status !== "pending" || entityType !== "all");
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
function updateSearchParams(next: {
|
||||
status?: WorkflowStatusFilter;
|
||||
@@ -203,6 +202,15 @@ export function WorkflowTasksPage() {
|
||||
const totalPages = Math.max(1, Math.ceil((query.data?.total ?? 0) / PAGE_SIZE));
|
||||
const taskItems = query.data?.items ?? [];
|
||||
const confirmCopy = confirmApproveTaskId ? getWorkflowConfirmCopy("approve") : null;
|
||||
const isDefaultTaskView = status === "pending" && entityType === "all";
|
||||
const advancedVisible = showAdvanced || !isDefaultTaskView;
|
||||
const pendingTotal = summaryQuery.data?.pendingTotal ?? "-";
|
||||
const overdueTotal = summaryQuery.data?.overdueTotal ?? "-";
|
||||
const pendingEntitySummary =
|
||||
(summaryQuery.data?.byEntityType ?? [])
|
||||
.filter((item) => item.pendingTotal > 0)
|
||||
.map((item) => `${entityLabel(item.key)} ${item.pendingTotal}`)
|
||||
.join(" / ") || "-";
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -210,73 +218,107 @@ export function WorkflowTasksPage() {
|
||||
<WorkspaceHeader
|
||||
eyebrow="审批"
|
||||
title="审批中心"
|
||||
subtitle="先看待处理,再进入详情核对依据;快速通过前仍会弹出确认。"
|
||||
actions={
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
data-testid="workflow-tasks-advanced-toggle"
|
||||
onClick={() => setShowAdvanced((value) => !value)}
|
||||
>
|
||||
{advancedVisible ? "收起高级" : "高级 / 历史"}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<div data-testid="workflow-task-summary">
|
||||
<StatStrip columns={4} className="ops-status-strip">
|
||||
<MetricCard title="待办总数" value={summaryQuery.data?.pendingTotal ?? "-"} />
|
||||
<MetricCard title="超时待办" value={summaryQuery.data?.overdueTotal ?? "-"} tone="warning" />
|
||||
<MetricCard title="最早待处理" value={formatSummaryTime(summaryQuery.data?.earliestPendingAt)} />
|
||||
<MetricCard
|
||||
title="业务分布"
|
||||
value={
|
||||
<span className="block truncate text-base">
|
||||
{(summaryQuery.data?.byEntityType ?? [])
|
||||
.filter((item) => item.pendingTotal > 0)
|
||||
.map((item) => `${entityLabel(item.key)} ${item.pendingTotal}`)
|
||||
.join(" / ") || "-"}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</StatStrip>
|
||||
</div>
|
||||
|
||||
<FilterBar className="ops-command-bar !p-3 md:!p-3.5">
|
||||
<div className="ops-command-bar__row">
|
||||
<Tabs
|
||||
data-testid="workflow-tasks-status-tabs"
|
||||
value={status}
|
||||
onValueChange={(value) => updateSearchParams({ status: value as WorkflowStatusFilter, page: 1 })}
|
||||
>
|
||||
<TabsList className="ops-command-bar__tabs !h-auto !justify-start !bg-slate-50">
|
||||
{WORKFLOW_STATUS_OPTIONS.map((item) => (
|
||||
<TabsTrigger
|
||||
data-testid={`workflow-tasks-tab-${item.value}`}
|
||||
key={item.value}
|
||||
value={item.value}
|
||||
>
|
||||
{item.label}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-3" data-testid="workflow-tasks-focus">
|
||||
<span className="badge-neutral">待处理</span>
|
||||
<span className="text-2xl font-semibold text-zinc-900">{pendingTotal}</span>
|
||||
<span className="text-sm text-zinc-500">
|
||||
最早:{formatSummaryTime(summaryQuery.data?.earliestPendingAt)}
|
||||
</span>
|
||||
{summaryQuery.data?.overdueTotal ? (
|
||||
<span className="rounded-full border border-amber-200 bg-amber-50 px-2.5 py-1 text-xs font-medium text-amber-700">
|
||||
超时 {summaryQuery.data.overdueTotal}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="ops-command-bar__actions">
|
||||
<label className="ops-filter-label" htmlFor="workflow-entity-type-filter">
|
||||
业务类型
|
||||
</label>
|
||||
<select
|
||||
id="workflow-entity-type-filter"
|
||||
className="select-sm min-w-32"
|
||||
data-testid="workflow-tasks-entity-filter"
|
||||
value={entityType}
|
||||
onChange={(event) => updateSearchParams({ entityType: event.target.value as WorkflowEntityFilter, page: 1 })}
|
||||
>
|
||||
{WORKFLOW_ENTITY_OPTIONS.map((item) => (
|
||||
<option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{!isDefaultTaskView ? (
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setShowAdvanced(false);
|
||||
updateSearchParams({ status: "pending", entityType: "all", page: 1 });
|
||||
}}
|
||||
>
|
||||
回到待处理
|
||||
</Button>
|
||||
) : null}
|
||||
<span className="text-xs text-zinc-500">业务:{pendingEntitySummary}</span>
|
||||
</div>
|
||||
</div>
|
||||
</FilterBar>
|
||||
|
||||
{status === "pending" ? (
|
||||
<WorkspaceNotice title="处理原则" tone="neutral">
|
||||
先看业务编号、申请人、金额/天数和当前节点;需要查看附件、审批历史或驳回原因时进入详情页。
|
||||
</WorkspaceNotice>
|
||||
{advancedVisible ? (
|
||||
<div className="space-y-3" data-testid="workflow-tasks-advanced-panel">
|
||||
<div data-testid="workflow-task-summary">
|
||||
<StatStrip columns={4} className="ops-status-strip">
|
||||
<MetricCard title="待办总数" value={pendingTotal} />
|
||||
<MetricCard title="超时待办" value={overdueTotal} tone="warning" />
|
||||
<MetricCard title="最早待处理" value={formatSummaryTime(summaryQuery.data?.earliestPendingAt)} />
|
||||
<MetricCard
|
||||
title="业务分布"
|
||||
value={<span className="block truncate text-base">{pendingEntitySummary}</span>}
|
||||
/>
|
||||
</StatStrip>
|
||||
</div>
|
||||
|
||||
<FilterBar className="ops-command-bar !p-3 md:!p-3.5">
|
||||
<div className="ops-command-bar__row">
|
||||
<Tabs
|
||||
data-testid="workflow-tasks-status-tabs"
|
||||
value={status}
|
||||
onValueChange={(value) => updateSearchParams({ status: value as WorkflowStatusFilter, page: 1 })}
|
||||
>
|
||||
<TabsList className="ops-command-bar__tabs !h-auto !justify-start !bg-slate-50">
|
||||
{WORKFLOW_STATUS_OPTIONS.map((item) => (
|
||||
<TabsTrigger
|
||||
data-testid={`workflow-tasks-tab-${item.value}`}
|
||||
key={item.value}
|
||||
value={item.value}
|
||||
>
|
||||
{item.label}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
<div className="ops-command-bar__actions">
|
||||
<label className="ops-filter-label" htmlFor="workflow-entity-type-filter">
|
||||
业务类型
|
||||
</label>
|
||||
<select
|
||||
id="workflow-entity-type-filter"
|
||||
className="select-sm min-w-32"
|
||||
data-testid="workflow-tasks-entity-filter"
|
||||
value={entityType}
|
||||
onChange={(event) =>
|
||||
updateSearchParams({ entityType: event.target.value as WorkflowEntityFilter, page: 1 })
|
||||
}
|
||||
>
|
||||
{WORKFLOW_ENTITY_OPTIONS.map((item) => (
|
||||
<option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</FilterBar>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{query.isLoading ? <TableSkeleton columns={7} rows={5} /> : null}
|
||||
@@ -348,13 +390,15 @@ export function WorkflowTasksPage() {
|
||||
variant="secondary"
|
||||
data-testid={TEST_IDS.workflow.detailLink}
|
||||
>
|
||||
<Link to={`/workflow/tasks/${task.id}`}>详情</Link>
|
||||
<Link to={`/workflow/tasks/${task.id}`}>查看</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</article>
|
||||
))
|
||||
) : (
|
||||
<WorkspaceEmptyState>当前筛选下没有审批任务。</WorkspaceEmptyState>
|
||||
<WorkspaceEmptyState>
|
||||
{isDefaultTaskView ? "当前没有待处理任务。" : "当前筛选下没有审批任务。"}
|
||||
</WorkspaceEmptyState>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -405,10 +449,10 @@ export function WorkflowTasksPage() {
|
||||
<Button
|
||||
asChild
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
variant={status === "pending" && (task.allowedActions ?? []).includes("approve") ? "secondary" : "default"}
|
||||
data-testid={TEST_IDS.workflow.detailLink}
|
||||
>
|
||||
<Link to={`/workflow/tasks/${task.id}`}>详情</Link>
|
||||
<Link to={`/workflow/tasks/${task.id}`}>查看</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
@@ -417,7 +461,7 @@ export function WorkflowTasksPage() {
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell className="px-6 py-8 text-center text-zinc-500" colSpan={8}>
|
||||
当前筛选下没有审批任务
|
||||
{isDefaultTaskView ? "当前没有待处理任务" : "当前筛选下没有审批任务"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
|
||||
在新工单中引用
屏蔽一个用户