三方系统密码加密

This commit is contained in:
dengqichen 2025-11-12 13:18:14 +08:00
parent adf83458a5
commit 92eb82583f
3 changed files with 257 additions and 216 deletions

View File

@ -1,4 +1,7 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -8,8 +11,15 @@ import {
DialogBody, DialogBody,
} from '@/components/ui/dialog'; } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Loader2 } from 'lucide-react'; import { Loader2 } from 'lucide-react';
@ -26,18 +36,41 @@ interface EditModalProps {
record?: WorkflowDefinition; record?: WorkflowDefinition;
} }
// Zod 验证 Schema
const workflowFormSchema = z.object({
name: z.string()
.min(1, '请输入流程名称')
.max(100, '流程名称不能超过100个字符'),
key: z.string()
.min(1, '请输入流程标识')
.regex(/^[a-zA-Z_][a-zA-Z0-9_-]*$/, '流程标识只能包含字母、数字、下划线和连字符,且必须以字母或下划线开头')
.refine((val) => !/^xml/i.test(val), '流程标识不能以xml开头'),
categoryId: z.number({
required_error: '请选择流程分类',
invalid_type_error: '请选择流程分类',
}),
formDefinitionId: z.number().optional().nullable(),
description: z.string().max(500, '描述不能超过500个字符').optional(),
});
type WorkflowFormValues = z.infer<typeof workflowFormSchema>;
const EditModal: React.FC<EditModalProps> = ({ visible, onClose, onSuccess, record }) => { const EditModal: React.FC<EditModalProps> = ({ visible, onClose, onSuccess, record }) => {
const { toast } = useToast(); const { toast } = useToast();
const isEdit = !!record; const isEdit = !!record;
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [categories, setCategories] = useState<WorkflowCategoryResponse[]>([]); const [categories, setCategories] = useState<WorkflowCategoryResponse[]>([]);
const [formDefinitions, setFormDefinitions] = useState<FormDefinitionResponse[]>([]); const [formDefinitions, setFormDefinitions] = useState<FormDefinitionResponse[]>([]);
const [formData, setFormData] = useState({
const form = useForm<WorkflowFormValues>({
resolver: zodResolver(workflowFormSchema),
defaultValues: {
name: '', name: '',
key: '', key: '',
categoryId: undefined as number | undefined, categoryId: undefined as any,
formDefinitionId: undefined as number | undefined, formDefinitionId: undefined,
description: '', description: '',
},
}); });
useEffect(() => { useEffect(() => {
@ -45,18 +78,18 @@ const EditModal: React.FC<EditModalProps> = ({ visible, onClose, onSuccess, reco
loadCategories(); loadCategories();
loadFormDefinitions(); loadFormDefinitions();
if (record) { if (record) {
setFormData({ form.reset({
name: record.name, name: record.name,
key: record.key, key: record.key,
categoryId: record.categoryId, categoryId: record.categoryId,
formDefinitionId: record.formDefinitionId, formDefinitionId: record.formDefinitionId || undefined,
description: record.description || '', description: record.description || '',
}); });
} else { } else {
setFormData({ form.reset({
name: '', name: '',
key: '', key: '',
categoryId: undefined, categoryId: undefined as any,
formDefinitionId: undefined, formDefinitionId: undefined,
description: '', description: '',
}); });
@ -95,80 +128,38 @@ const EditModal: React.FC<EditModalProps> = ({ visible, onClose, onSuccess, reco
} }
}; };
const handleSubmit = async () => { const handleSubmit = async (values: WorkflowFormValues) => {
// 验证
if (!formData.name.trim()) {
toast({
title: '验证失败',
description: '请输入流程名称',
variant: 'destructive',
});
return;
}
if (!formData.key.trim()) {
toast({
title: '验证失败',
description: '请输入流程标识',
variant: 'destructive',
});
return;
}
if (!/^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(formData.key)) {
toast({
title: '验证失败',
description: '流程标识只能包含字母、数字、下划线和连字符,且必须以字母或下划线开头',
variant: 'destructive',
});
return;
}
if (/^xml/i.test(formData.key)) {
toast({
title: '验证失败',
description: '流程标识不能以xml开头',
variant: 'destructive',
});
return;
}
if (!formData.categoryId) {
toast({
title: '验证失败',
description: '请选择流程分类',
variant: 'destructive',
});
return;
}
setSubmitting(true); setSubmitting(true);
try { try {
if (isEdit && record) { if (isEdit && record) {
// 更新时需要传递完整的数据和版本号 // 更新时需要传递完整的数据和版本号
const submitData: WorkflowDefinitionRequest = { const submitData: WorkflowDefinitionRequest = {
name: formData.name, name: values.name,
key: formData.key, key: values.key,
categoryId: formData.categoryId, categoryId: values.categoryId,
formDefinitionId: formData.formDefinitionId, formDefinitionId: values.formDefinitionId,
flowVersion: record.flowVersion, // 流程版本 flowVersion: record.flowVersion,
description: formData.description, description: values.description,
graph: record.graph, graph: record.graph,
bpmnXml: record.bpmnXml, bpmnXml: record.bpmnXml,
status: record.status, status: record.status,
version: record.version, // 乐观锁版本号 version: record.version,
}; };
await updateDefinition(record.id, submitData); await updateDefinition(record.id, submitData);
toast({ toast({
title: '更新成功', title: '更新成功',
description: `工作流 "${formData.name}" 已更新`, description: `工作流 "${values.name}" 已更新`,
}); });
} else { } else {
// 新建时初始化基本数据 // 新建时初始化基本数据
const submitData: WorkflowDefinitionRequest = { const submitData: WorkflowDefinitionRequest = {
name: formData.name, name: values.name,
key: formData.key, key: values.key,
categoryId: formData.categoryId, categoryId: values.categoryId,
formDefinitionId: formData.formDefinitionId, formDefinitionId: values.formDefinitionId,
flowVersion: 1, // 新建时流程版本为1 flowVersion: 1,
description: formData.description, description: values.description,
graph: { nodes: [], edges: [] }, graph: { nodes: [], edges: [] },
status: 'DRAFT', status: 'DRAFT',
}; };
@ -176,7 +167,7 @@ const EditModal: React.FC<EditModalProps> = ({ visible, onClose, onSuccess, reco
await saveDefinition(submitData); await saveDefinition(submitData);
toast({ toast({
title: '创建成功', title: '创建成功',
description: `工作流 "${formData.name}" 已创建`, description: `工作流 "${values.name}" 已创建`,
}); });
} }
onSuccess?.(); onSuccess?.();
@ -200,25 +191,29 @@ const EditModal: React.FC<EditModalProps> = ({ visible, onClose, onSuccess, reco
<DialogHeader> <DialogHeader>
<DialogTitle>{isEdit ? '编辑流程' : '新建流程'}</DialogTitle> <DialogTitle>{isEdit ? '编辑流程' : '新建流程'}</DialogTitle>
</DialogHeader> </DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)}>
<DialogBody className="grid gap-6"> <DialogBody className="grid gap-6">
{/* 流程分类 */} {/* 流程分类 */}
<div className="space-y-2"> <FormField
<Label htmlFor="categoryId"> control={form.control}
name="categoryId"
render={({ field }) => (
<FormItem>
<FormLabel>
<span className="text-destructive">*</span> <span className="text-destructive">*</span>
</Label> </FormLabel>
<Select <Select
value={formData.categoryId?.toString() || undefined} value={field.value?.toString()}
onValueChange={(value) => { onValueChange={(value) => field.onChange(Number(value))}
setFormData(prev => ({
...prev,
categoryId: Number(value),
}));
}}
disabled={isEdit} disabled={isEdit}
> >
<SelectTrigger id="categoryId" className="h-10"> <FormControl>
<SelectTrigger className="h-10">
<SelectValue placeholder="请选择流程分类" /> <SelectValue placeholder="请选择流程分类" />
</SelectTrigger> </SelectTrigger>
</FormControl>
<SelectContent> <SelectContent>
{categories.map(cat => ( {categories.map(cat => (
<SelectItem key={cat.id} value={cat.id.toString()}> <SelectItem key={cat.id} value={cat.id.toString()}>
@ -232,41 +227,43 @@ const EditModal: React.FC<EditModalProps> = ({ visible, onClose, onSuccess, reco
</p> </p>
)} )}
</div> <FormMessage />
</FormItem>
)}
/>
{/* 启动表单 */} {/* 启动表单 */}
<div className="space-y-2"> <FormField
<Label htmlFor="formDefinitionId"> control={form.control}
name="formDefinitionId"
</Label> render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<div className="flex gap-2"> <div className="flex gap-2">
<Select <Select
value={formData.formDefinitionId?.toString() || ''} value={field.value?.toString() || ''}
onValueChange={(value) => { onValueChange={(value) => field.onChange(value ? Number(value) : undefined)}
setFormData(prev => ({
...prev,
formDefinitionId: value ? Number(value) : undefined,
}));
}}
> >
<SelectTrigger id="formDefinitionId" className="h-10 flex-1"> <FormControl>
<SelectTrigger className="h-10 flex-1">
<SelectValue placeholder="请选择启动表单(可选)" /> <SelectValue placeholder="请选择启动表单(可选)" />
</SelectTrigger> </SelectTrigger>
</FormControl>
<SelectContent> <SelectContent>
{formDefinitions.map(form => ( {formDefinitions.map(formDef => (
<SelectItem key={form.id} value={form.id.toString()}> <SelectItem key={formDef.id} value={formDef.id.toString()}>
{form.name} ({form.key}) {formDef.name} ({formDef.key})
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
{formData.formDefinitionId && ( {field.value && (
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
size="icon" size="icon"
className="h-10 w-10 shrink-0" className="h-10 w-10 shrink-0"
onClick={() => setFormData(prev => ({ ...prev, formDefinitionId: undefined }))} onClick={() => field.onChange(undefined)}
> >
× ×
</Button> </Button>
@ -275,68 +272,91 @@ const EditModal: React.FC<EditModalProps> = ({ visible, onClose, onSuccess, reco
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
</p> </p>
</div> <FormMessage />
</FormItem>
)}
/>
{/* 流程标识 */} {/* 流程标识 */}
<div className="space-y-2"> <FormField
<Label htmlFor="key"> control={form.control}
name="key"
render={({ field }) => (
<FormItem>
<FormLabel>
<span className="text-destructive">*</span> <span className="text-destructive">*</span>
{isEdit && <span className="text-xs text-muted-foreground ml-1">()</span>} {isEdit && <span className="text-xs text-muted-foreground ml-1">()</span>}
</Label> </FormLabel>
<FormControl>
<Input <Input
id="key"
placeholder="例如jenkins_build_workflow" placeholder="例如jenkins_build_workflow"
value={formData.key}
onChange={(e) => setFormData(prev => ({ ...prev, key: e.target.value }))}
disabled={isEdit} disabled={isEdit}
className="h-10 font-mono" className="h-10 font-mono"
{...field}
/> />
</FormControl>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
线使线 线使线
</p> </p>
</div> <FormMessage />
</FormItem>
)}
/>
{/* 流程名称 */} {/* 流程名称 */}
<div className="space-y-2"> <FormField
<Label htmlFor="name"> control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>
<span className="text-destructive">*</span> <span className="text-destructive">*</span>
{isEdit && <span className="text-xs text-muted-foreground ml-1">()</span>} {isEdit && <span className="text-xs text-muted-foreground ml-1">()</span>}
</Label> </FormLabel>
<FormControl>
<Input <Input
id="name"
placeholder="例如Jenkins 构建流程" placeholder="例如Jenkins 构建流程"
value={formData.name}
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
disabled={isEdit} disabled={isEdit}
className="h-10" className="h-10"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/> />
</div>
{/* 描述 */} {/* 描述 */}
<div className="space-y-2"> <FormField
<Label htmlFor="description"> control={form.control}
name="description"
</Label> render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Textarea <Textarea
id="description"
placeholder="简要说明此工作流的用途..." placeholder="简要说明此工作流的用途..."
value={formData.description}
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
className="min-h-[100px] resize-none" className="min-h-[100px] resize-none"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/> />
</div>
</DialogBody> </DialogBody>
<DialogFooter> <DialogFooter>
<Button variant="outline" onClick={handleClose} disabled={submitting}> <Button type="button" variant="outline" onClick={handleClose} disabled={submitting}>
</Button> </Button>
<Button onClick={handleSubmit} disabled={submitting}> <Button type="submit" disabled={submitting}>
{submitting && <Loader2 className="h-4 w-4 mr-2 animate-spin" />} {submitting && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
{isEdit ? '更新' : '创建'} {isEdit ? '更新' : '创建'}
</Button> </Button>
</DialogFooter> </DialogFooter>
</form>
</Form>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );

View File

@ -414,19 +414,20 @@ const WorkflowDefinitionList: React.FC = () => {
<Table minWidth="1000px"> <Table minWidth="1000px">
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead width="200px"></TableHead> <TableHead width="180px"></TableHead>
<TableHead width="150px"></TableHead> <TableHead width="140px"></TableHead>
<TableHead width="120px"></TableHead> <TableHead width="100px"></TableHead>
<TableHead width="80px"></TableHead> <TableHead width="140px"></TableHead>
<TableHead width="120px"></TableHead> <TableHead width="70px"></TableHead>
<TableHead width="150px"></TableHead> <TableHead width="100px"></TableHead>
<TableHead width="150px"></TableHead>
<TableHead width="180px" sticky></TableHead> <TableHead width="180px" sticky></TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{loading ? ( {loading ? (
<TableRow> <TableRow>
<TableCell colSpan={7} className="h-24 text-center"> <TableCell colSpan={8} className="h-24 text-center">
<div className="flex items-center justify-center"> <div className="flex items-center justify-center">
<Loader2 className="h-6 w-6 animate-spin mr-2" /> <Loader2 className="h-6 w-6 animate-spin mr-2" />
<span className="text-sm text-muted-foreground">...</span> <span className="text-sm text-muted-foreground">...</span>
@ -435,29 +436,47 @@ const WorkflowDefinitionList: React.FC = () => {
</TableRow> </TableRow>
) : data?.content && data.content.length > 0 ? ( ) : data?.content && data.content.length > 0 ? (
data.content.map((record) => { data.content.map((record) => {
const categoryInfo = categories.find(c => c.id === record.categoryId);
const isDraft = record.status === 'DRAFT'; const isDraft = record.status === 'DRAFT';
// 优先使用后端返回的 category 对象fallback 到前端查找
const categoryInfo = record.category || categories.find(c => c.id === record.categoryId);
return ( return (
<TableRow key={record.id} className="hover:bg-muted/50"> <TableRow key={record.id} className="hover:bg-muted/50">
<TableCell width="200px" className="font-medium">{record.name}</TableCell> <TableCell width="180px" className="font-medium">{record.name}</TableCell>
<TableCell width="150px"> <TableCell width="140px">
<code className="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold"> <code className="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold">
{record.key} {record.key}
</code> </code>
</TableCell> </TableCell>
<TableCell width="120px"> <TableCell width="100px">
{categoryInfo ? ( {categoryInfo ? (
<Badge variant="outline">{categoryInfo.name}</Badge> <Badge variant="outline">{categoryInfo.name}</Badge>
) : ( ) : (
<Badge variant="outline"></Badge> <Badge variant="outline"></Badge>
)} )}
</TableCell> </TableCell>
<TableCell width="80px"> <TableCell width="140px">
{record.formDefinitionName ? (
<Badge variant="secondary" className="font-normal">
{record.formDefinitionName}
</Badge>
) : (
<span className="text-xs text-muted-foreground"></span>
)}
</TableCell>
<TableCell width="70px">
<span className="font-mono text-sm">{record.flowVersion || 1}</span> <span className="font-mono text-sm">{record.flowVersion || 1}</span>
</TableCell> </TableCell>
<TableCell width="120px">{getStatusBadge(record.status || 'DRAFT')}</TableCell> <TableCell width="100px">{getStatusBadge(record.status || 'DRAFT')}</TableCell>
<TableCell width="150px"> <TableCell width="150px">
<span className="text-sm line-clamp-1">{record.description || '-'}</span> <span className="text-xs text-muted-foreground">
{record.createTime ? new Date(record.createTime).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
}) : '-'}
</span>
</TableCell> </TableCell>
<TableCell width="180px" sticky> <TableCell width="180px" sticky>
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
@ -526,7 +545,7 @@ const WorkflowDefinitionList: React.FC = () => {
}) })
) : ( ) : (
<TableRow> <TableRow>
<TableCell colSpan={7} className="h-24 text-center"> <TableCell colSpan={8} className="h-24 text-center">
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground"> <div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
<Workflow className="w-16 h-16 mb-4 text-muted-foreground/50" /> <Workflow className="w-16 h-16 mb-4 text-muted-foreground/50" />
<div className="text-lg font-semibold mb-2"></div> <div className="text-lg font-semibold mb-2"></div>

View File

@ -12,7 +12,9 @@ export interface WorkflowDefinition extends BaseResponse {
name: string; name: string;
key: string; key: string;
categoryId?: number; // 分类ID categoryId?: number; // 分类ID
category?: WorkflowCategoryResponse | null; // 分类对象(后端返回)
formDefinitionId?: number; // 启动表单ID formDefinitionId?: number; // 启动表单ID
formDefinitionName?: string | null; // 启动表单名称(后端返回)
processDefinitionId?: string; // 流程定义ID processDefinitionId?: string; // 流程定义ID
flowVersion: number; // 流程版本 flowVersion: number; // 流程版本
bpmnXml?: string; // BPMN XML内容 bpmnXml?: string; // BPMN XML内容