PC6 完善 PC 工作台与审批信息架构
这个提交包含在:
@@ -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>
|
||||
|
||||
在新工单中引用
屏蔽一个用户