文件
hyb-oa/client/src/pages/WorkflowDefinitionsPage.tsx
T
wangsli 9905c1c79d feat(P25-2/P25-3/P26-2/P28-1/P24-2/P29-3): 落地内部OA够用版前端入口与运营页面
关联 Issue: P25-2, P25-3, P25-4, P26-2, P26-4, P28-1, P28-2, P28-3, P28-4, P24-2, P29-3
2026-03-21 18:03:48 +08:00

169 行
8.7 KiB
TypeScript

import { useEffect, useMemo, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
archiveWorkflowDefinition,
createWorkflowDefinition,
fetchWorkflowDefinitions,
publishWorkflowDefinition,
updateWorkflowDefinition,
type WorkflowDefinitionEditorModel,
} from "../api/workflowDefinitions";
import { PageHeader } from "../components/common/PageHeader";
import { Button } from "../components/ui/button";
import { Input } from "../components/ui/input";
import { Label } from "../components/ui/label";
import { toast } from "sonner";
const ENTITY_TYPES: WorkflowDefinitionEditorModel["entityType"][] = ["reimbursement", "leave", "purchase_request", "travel_request"];
type DraftStep = WorkflowDefinitionEditorModel["steps"][number];
function emptyStep(): DraftStep {
return { id: `step_${Date.now()}`, name: "审批节点", assigneeRule: { type: "department_manager" } };
}
export function WorkflowDefinitionsPage() {
const queryClient = useQueryClient();
const [entityType, setEntityType] = useState<WorkflowDefinitionEditorModel["entityType"]>("leave");
const [selectedId, setSelectedId] = useState("");
const [name, setName] = useState("");
const [notes, setNotes] = useState("");
const [steps, setSteps] = useState<DraftStep[]>([emptyStep()]);
const listQuery = useQuery({ queryKey: ["workflow-definitions"], queryFn: () => fetchWorkflowDefinitions() });
const items = listQuery.data?.items ?? [];
const scopedItems = useMemo(() => items.filter((item) => item.entityType === entityType), [items, entityType]);
const selected = useMemo(() => scopedItems.find((item) => item.id === selectedId) ?? scopedItems[0] ?? null, [scopedItems, selectedId]);
useEffect(() => {
if (!selected) return;
setSelectedId(selected.id);
setName(selected.name);
setNotes(selected.notes ?? "");
setSteps(selected.steps.length ? selected.steps : [emptyStep()]);
}, [selected?.id]);
const refresh = () => queryClient.invalidateQueries({ queryKey: ["workflow-definitions"] });
const createMutation = useMutation({
mutationFn: () => createWorkflowDefinition({ entityType, name: name.trim() || `${entityType}-workflow`, notes, steps }),
onSuccess: async ({ item }) => {
setSelectedId(item.id);
await refresh();
toast.success("流程定义草稿已创建");
},
});
const saveMutation = useMutation({
mutationFn: (id: string) => updateWorkflowDefinition(id, { name, notes, steps }),
onSuccess: async () => {
await refresh();
toast.success("流程定义已保存");
},
});
const publishMutation = useMutation({
mutationFn: publishWorkflowDefinition,
onSuccess: async () => {
await refresh();
toast.success("流程定义已发布");
},
});
const archiveMutation = useMutation({
mutationFn: archiveWorkflowDefinition,
onSuccess: async () => {
await refresh();
toast.success("流程定义已归档");
},
});
return (
<div className="space-y-6">
<PageHeader title="流程定义" subtitle="线性流程的版本化治理;报销继续由审批规则投影。" />
<div className="grid gap-6 xl:grid-cols-[220px,280px,1fr]">
<div className="card-std space-y-3 p-6">
<Label>业务域</Label>
<select className="select-std" value={entityType} onChange={(event) => setEntityType(event.target.value as WorkflowDefinitionEditorModel["entityType"])}>
{ENTITY_TYPES.map((item) => <option key={item} value={item}>{item}</option>)}
</select>
{entityType !== "reimbursement" ? <Button type="button" onClick={() => createMutation.mutate()}>创建草稿</Button> : null}
</div>
<div className="card-std space-y-3 p-6">
<h3 className="text-base font-semibold text-zinc-900">版本列表</h3>
<div className="space-y-2">
{scopedItems.map((item) => (
<button key={item.id} type="button" className={selected?.id === item.id ? "w-full rounded-lg border border-primary bg-primary/5 px-3 py-3 text-left" : "w-full rounded-lg border border-zinc-200 px-3 py-3 text-left hover:bg-zinc-50"} onClick={() => setSelectedId(item.id)}>
<div className="font-medium text-zinc-900">v{item.version} · {item.name}</div>
<div className="text-xs text-zinc-500">{item.status} · {item.source}</div>
</button>
))}
{!scopedItems.length ? <div className="text-sm text-zinc-500">当前实体暂无流程定义</div> : null}
</div>
</div>
<div className="card-std space-y-4 p-6">
{selected ? (
<>
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<div className="text-xs text-zinc-500">{selected.source}</div>
<div className="text-lg font-semibold text-zinc-900">{selected.name}</div>
</div>
{selected.status !== "derived_readonly" ? (
<div className="flex gap-2">
<Button type="button" variant="secondary" onClick={() => saveMutation.mutate(selected.id)} disabled={selected.status !== "draft"}>保存</Button>
<Button type="button" onClick={() => publishMutation.mutate(selected.id)} disabled={selected.status === "archived"}>发布</Button>
<Button type="button" variant="secondary" onClick={() => archiveMutation.mutate(selected.id)} disabled={selected.status === "archived"}>归档</Button>
</div>
) : null}
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label>名称</Label>
<Input value={name} onChange={(event) => setName(event.target.value)} disabled={selected.status !== "draft"} />
</div>
<div className="space-y-2">
<Label>备注</Label>
<Input value={notes} onChange={(event) => setNotes(event.target.value)} disabled={selected.status !== "draft"} />
</div>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<h3 className="text-base font-semibold text-zinc-900">步骤</h3>
{selected.status === "draft" ? <Button type="button" variant="secondary" onClick={() => setSteps((prev) => [...prev, emptyStep()])}>新增步骤</Button> : null}
</div>
<div className="space-y-3">
{steps.map((step, index) => (
<div key={step.id} className="rounded-lg border border-zinc-200 p-4">
<div className="grid gap-3 md:grid-cols-3">
<Input value={step.id} disabled={selected.status !== "draft"} onChange={(event) => setSteps((prev) => prev.map((item, itemIndex) => itemIndex === index ? { ...item, id: event.target.value } : item))} />
<Input value={step.name} disabled={selected.status !== "draft"} onChange={(event) => setSteps((prev) => prev.map((item, itemIndex) => itemIndex === index ? { ...item, name: event.target.value } : item))} />
<select className="select-std" value={step.assigneeRule.type} disabled={selected.status !== "draft"} onChange={(event) => setSteps((prev) => prev.map((item, itemIndex) => itemIndex === index ? { ...item, assigneeRule: event.target.value === "role" ? { type: "role", value: "manager" } : event.target.value === "specific_user" ? { type: "specific_user", value: "" } : { type: "department_manager" } } : item))}>
<option value="department_manager">department_manager</option>
<option value="role">role</option>
<option value="specific_user">specific_user</option>
</select>
</div>
{step.assigneeRule.type !== "department_manager" ? (
<div className="mt-3">
<Input value={step.assigneeRule.value} disabled={selected.status !== "draft"} onChange={(event) => setSteps((prev) => prev.map((item, itemIndex) => itemIndex === index ? { ...item, assigneeRule: { ...item.assigneeRule, value: event.target.value } as DraftStep["assigneeRule"] } : item))} placeholder={step.assigneeRule.type === "role" ? "角色编码" : "用户 ID"} />
</div>
) : null}
</div>
))}
</div>
</div>
</>
) : (
<div className="text-sm text-zinc-500">暂无可编辑流程定义</div>
)}
</div>
</div>
</div>
);
}