三方系统密码加密

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

View File

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

View File

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