PC6 完善 PC 工作台与审批信息架构

这个提交包含在:
2026-05-12 02:34:15 +08:00
父节点 e7bf57380e
当前提交 aec660a8fe
共修改 13 个文件,包含 518 行新增和 74 行删除
+108 -43
查看文件
@@ -14,6 +14,35 @@ import { Tabs, TabsList, TabsTrigger } from "../components/ui/tabs";
import { TEST_IDS } from "../lib/testIds";
const PAGE_SIZE = 20;
const WORKFLOW_STATUS_OPTIONS = [
{ value: "pending", label: "待处理" },
{ value: "completed", label: "已处理" },
{ value: "rejected", label: "已驳回" },
{ value: "cancelled", label: "已取消" },
{ value: "skipped", label: "已跳过" },
] as const;
const WORKFLOW_ENTITY_OPTIONS = [
{ value: "all", label: "全部业务" },
{ value: "reimbursement", label: "报销" },
{ value: "leave", label: "请假" },
{ value: "purchase_request", label: "采购" },
{ value: "travel_request", label: "差旅" },
] as const;
type WorkflowStatusFilter = (typeof WORKFLOW_STATUS_OPTIONS)[number]["value"];
type WorkflowEntityFilter = (typeof WORKFLOW_ENTITY_OPTIONS)[number]["value"];
function normalizeStatusFilter(value: string | null): WorkflowStatusFilter {
return WORKFLOW_STATUS_OPTIONS.some((item) => item.value === value)
? (value as WorkflowStatusFilter)
: "pending";
}
function normalizeEntityFilter(value: string | null): WorkflowEntityFilter {
return WORKFLOW_ENTITY_OPTIONS.some((item) => item.value === value)
? (value as WorkflowEntityFilter)
: "all";
}
function entityLabel(entityType: string | undefined) {
if (entityType === "leave") {
@@ -82,19 +111,36 @@ export function WorkflowTasksPage() {
const [searchParams, setSearchParams] = useSearchParams();
const queryClient = useQueryClient();
const status =
(searchParams.get("status") as
| "pending"
| "completed"
| "rejected"
| "cancelled"
| "skipped"
| null) ?? "pending";
const status = normalizeStatusFilter(searchParams.get("status"));
const entityType = normalizeEntityFilter(searchParams.get("entityType"));
const page = Number(searchParams.get("page") ?? "1") || 1;
function updateSearchParams(next: {
status?: WorkflowStatusFilter;
entityType?: WorkflowEntityFilter;
page?: number;
}) {
const nextStatus = next.status ?? status;
const nextEntityType = next.entityType ?? entityType;
const params: Record<string, string> = {
status: nextStatus,
page: String(next.page ?? page),
};
if (nextEntityType !== "all") {
params.entityType = nextEntityType;
}
setSearchParams(params);
}
const query = useQuery({
queryKey: ["workflow", "tasks", status, page],
queryFn: () => fetchWorkflowTasks({ status, page, pageSize: PAGE_SIZE }),
queryKey: ["workflow", "tasks", status, entityType, page],
queryFn: () =>
fetchWorkflowTasks({
status,
entityType: entityType === "all" ? undefined : entityType,
page,
pageSize: PAGE_SIZE,
}),
});
const approveMutation = useMutation({
@@ -125,32 +171,47 @@ export function WorkflowTasksPage() {
return (
<div className="space-y-4 md:space-y-6">
<PageHeader title="待我处理" subtitle="待处理审批。" />
<PageHeader title="审批中心" subtitle="集中处理待办、查看已办和业务审批历史。" />
<div className="card-std space-y-4 p-4 md:p-5">
<Tabs
data-testid="workflow-tasks-status-tabs"
value={status}
onValueChange={(value) => setSearchParams({ status: value, page: "1" })}
>
<TabsList className="flex w-full flex-wrap justify-start gap-1">
{[
{ value: "pending", label: "待处理" },
{ value: "completed", label: "已处理" },
{ value: "rejected", label: "已驳回" },
{ value: "cancelled", label: "已取消" },
{ value: "skipped", label: "已跳过" },
].map((item) => (
<TabsTrigger
data-testid={`workflow-tasks-tab-${item.value}`}
key={item.value}
value={item.value}
>
{item.label}
</TabsTrigger>
))}
</TabsList>
</Tabs>
<div className="flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between">
<Tabs
data-testid="workflow-tasks-status-tabs"
value={status}
onValueChange={(value) => updateSearchParams({ status: value as WorkflowStatusFilter, page: 1 })}
>
<TabsList className="flex w-full flex-wrap justify-start gap-1 xl:w-auto">
{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 items-center gap-2">
<label className="text-sm font-medium text-zinc-700" htmlFor="workflow-entity-type-filter">
业务类型
</label>
<select
id="workflow-entity-type-filter"
className="select-std min-w-36"
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>
{query.isLoading ? <TableSkeleton columns={7} rows={5} /> : null}
@@ -184,6 +245,7 @@ export function WorkflowTasksPage() {
<div className="mt-3 grid gap-1 rounded-lg bg-zinc-50 px-3 py-2 text-xs text-zinc-600">
<div>申请人:{displayApplicant(task)}</div>
<div>金额/天数:{displayMetric(task)}</div>
<div>当前节点:{task.stepName}</div>
<div>业务状态:{businessStatusLabel(task.businessSummary?.businessStatus)}</div>
</div>
<div className="mt-3 grid grid-cols-2 gap-2">
@@ -224,12 +286,13 @@ export function WorkflowTasksPage() {
<Table data-testid="workflow-tasks-table">
<TableHeader>
<tr>
<TableHead>审批事项</TableHead>
<TableHead>业务类型 / 编号</TableHead>
<TableHead>标题</TableHead>
<TableHead>申请人</TableHead>
<TableHead>金额/天数</TableHead>
<TableHead>当前节点</TableHead>
<TableHead>业务状态</TableHead>
<TableHead>任务状态</TableHead>
<TableHead>创建时间</TableHead>
<TableHead>更新时间</TableHead>
<TableHead className="w-24">操作</TableHead>
</tr>
</TableHeader>
@@ -237,16 +300,18 @@ export function WorkflowTasksPage() {
{taskItems.length ? (
taskItems.map((task) => (
<TableRow data-testid="workflow-task-row" key={task.id}>
<TableCell>
<div className="font-medium text-zinc-900">{entityLabel(task.workflowInstance?.entityType)}</div>
<div className="text-xs text-zinc-500">{displayReference(task)}</div>
</TableCell>
<TableCell>
<div className="font-medium text-zinc-900">{displayTitle(task)}</div>
<div className="text-xs text-zinc-500">
{entityLabel(task.workflowInstance?.entityType)} · {displayReference(task)}
</div>
<div className="mt-1"><StatusBadge status={task.status} /></div>
</TableCell>
<TableCell>{displayApplicant(task)}</TableCell>
<TableCell>{displayMetric(task)}</TableCell>
<TableCell>{task.stepName}</TableCell>
<TableCell>{businessStatusLabel(task.businessSummary?.businessStatus)}</TableCell>
<TableCell><StatusBadge status={task.status} /></TableCell>
<TableCell>{new Date(task.createdAt).toLocaleString()}</TableCell>
<TableCell>
<div className="flex flex-wrap gap-2">
@@ -275,7 +340,7 @@ export function WorkflowTasksPage() {
))
) : (
<TableRow>
<TableCell className="py-3 text-zinc-500" colSpan={7}>
<TableCell className="py-3 text-zinc-500" colSpan={8}>
暂无数据
</TableCell>
</TableRow>
@@ -289,7 +354,7 @@ export function WorkflowTasksPage() {
<Pagination
page={page}
totalPages={totalPages}
onPageChange={(p) => setSearchParams({ status, page: String(p) })}
onPageChange={(p) => updateSearchParams({ page: p })}
testIdPrefix="workflow-tasks"
/>
</div>