FastAPI backend with auth, user, instance, ticket, query, dashboard APIs. React + TypeScript frontend with mock data UI (10 pages). SQLite + async SQLAlchemy, JWT auth, role-based access control. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
551 lines
29 KiB
TypeScript
551 lines
29 KiB
TypeScript
import { useState } from 'react';
|
|
import {
|
|
Plus, Edit2, Eye, ToggleLeft, ToggleRight, GitBranch, Users, CheckCircle,
|
|
ArrowRight, Shield, Trash2, X, ChevronDown, ChevronUp,
|
|
Save, Play
|
|
} from 'lucide-react';
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
import { workflows as initialWorkflows, dbTypeColorMap } from '@/lib/mock';
|
|
|
|
type StepType = 'submit' | 'audit' | 'execute';
|
|
|
|
type FlowStep = {
|
|
id: string;
|
|
name: string;
|
|
type: StepType;
|
|
assignees: string[];
|
|
mode?: 'any' | 'all';
|
|
description?: string;
|
|
};
|
|
|
|
type WorkflowDetail = {
|
|
id: number;
|
|
name: string;
|
|
dbTypes: string[];
|
|
steps: FlowStep[];
|
|
description: string;
|
|
enabled: boolean;
|
|
conditions?: string[];
|
|
};
|
|
|
|
const stepTypeLabels: Record<StepType, string> = {
|
|
submit: '提交阶段',
|
|
audit: '审核阶段',
|
|
execute: '执行阶段',
|
|
};
|
|
|
|
const stepTypeColors: Record<StepType, string> = {
|
|
submit: 'bg-blue-500/15 text-blue-400 border-blue-500/30',
|
|
audit: 'bg-purple-500/15 text-purple-400 border-purple-500/30',
|
|
execute: 'bg-green-500/15 text-green-400 border-green-500/30',
|
|
};
|
|
|
|
const stepTypeIcons: Record<StepType, React.ElementType> = {
|
|
submit: Users,
|
|
audit: Shield,
|
|
execute: Play,
|
|
};
|
|
|
|
const defaultSteps: FlowStep[] = [
|
|
{ id: 'step-1', name: '提交阶段', type: 'submit', assignees: ['工单提交人'], description: '申请人提交 SQL 变更工单' },
|
|
{ id: 'step-2', name: '一级审批', type: 'audit', assignees: ['部门负责人'], mode: 'any', description: '部门负责人进行一级审批' },
|
|
{ id: 'step-3', name: '二级审批', type: 'audit', assignees: ['DBA 团队'], mode: 'any', description: 'DBA 进行二级审批' },
|
|
{ id: 'step-4', name: 'SQL 执行', type: 'execute', assignees: ['DBA 执行人'], description: '审批通过后执行 SQL 变更' },
|
|
];
|
|
|
|
export default function WorkflowPage() {
|
|
const [items, setItems] = useState<WorkflowDetail[]>(
|
|
initialWorkflows.map(w => ({
|
|
...w,
|
|
steps: defaultSteps.slice(0, Math.min(w.steps + 2, defaultSteps.length)),
|
|
}))
|
|
);
|
|
const [previewWorkflow, setPreviewWorkflow] = useState<WorkflowDetail | null>(null);
|
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
|
const [editingWorkflow, setEditingWorkflow] = useState<WorkflowDetail | null>(null);
|
|
const [expandedId, setExpandedId] = useState<number | null>(null);
|
|
|
|
// 新建/编辑表单状态
|
|
const [formName, setFormName] = useState('');
|
|
const [formDesc, setFormDesc] = useState('');
|
|
const [formDbTypes, setFormDbTypes] = useState<string[]>(['mysql']);
|
|
const [formSteps, setFormSteps] = useState<FlowStep[]>([defaultSteps[0]]);
|
|
const [formConditions, setFormConditions] = useState('');
|
|
|
|
const dbTypes = ['mysql', 'postgres', 'redis', 'mongo'];
|
|
|
|
const resetForm = () => {
|
|
setFormName('');
|
|
setFormDesc('');
|
|
setFormDbTypes(['mysql']);
|
|
setFormSteps([{ ...defaultSteps[0] }]);
|
|
setFormConditions('');
|
|
};
|
|
|
|
const openCreate = () => {
|
|
resetForm();
|
|
setEditingWorkflow(null);
|
|
setShowCreateModal(true);
|
|
};
|
|
|
|
const openEdit = (wf: WorkflowDetail) => {
|
|
setFormName(wf.name);
|
|
setFormDesc(wf.description);
|
|
setFormDbTypes(wf.dbTypes);
|
|
setFormSteps([...wf.steps]);
|
|
setFormConditions(wf.conditions?.join(', ') || '');
|
|
setEditingWorkflow(wf);
|
|
setShowCreateModal(true);
|
|
};
|
|
|
|
const toggleWorkflow = (id: number) => {
|
|
setItems(prev => prev.map(w => w.id === id ? { ...w, enabled: !w.enabled } : w));
|
|
};
|
|
|
|
const deleteWorkflow = (id: number) => {
|
|
setItems(prev => prev.filter(w => w.id !== id));
|
|
};
|
|
|
|
const addStep = () => {
|
|
const types: StepType[] = ['audit', 'execute'];
|
|
const nextType = types[Math.min(formSteps.length - 1, types.length - 1)];
|
|
setFormSteps(prev => [...prev, {
|
|
id: `step-${Date.now()}`,
|
|
name: nextType === 'audit' ? `第 ${prev.filter(s => s.type === 'audit').length + 1} 级审批` : '执行阶段',
|
|
type: nextType,
|
|
assignees: nextType === 'audit' ? ['审批人'] : ['DBA 执行人'],
|
|
mode: 'any',
|
|
description: '',
|
|
}]);
|
|
};
|
|
|
|
const removeStep = (stepId: string) => {
|
|
if (formSteps.length <= 1) return;
|
|
setFormSteps(prev => prev.filter(s => s.id !== stepId));
|
|
};
|
|
|
|
const updateStep = (stepId: string, updates: Partial<FlowStep>) => {
|
|
setFormSteps(prev => prev.map(s => s.id === stepId ? { ...s, ...updates } : s));
|
|
};
|
|
|
|
const moveStep = (index: number, direction: 'up' | 'down') => {
|
|
if (direction === 'up' && index === 0) return;
|
|
if (direction === 'down' && index === formSteps.length - 1) return;
|
|
const newIndex = direction === 'up' ? index - 1 : index + 1;
|
|
setFormSteps(prev => {
|
|
const arr = [...prev];
|
|
[arr[index], arr[newIndex]] = [arr[newIndex], arr[index]];
|
|
return arr;
|
|
});
|
|
};
|
|
|
|
const saveWorkflow = () => {
|
|
if (!formName.trim()) return;
|
|
const conditions = formConditions ? formConditions.split(',').map(s => s.trim()).filter(Boolean) : [];
|
|
if (editingWorkflow) {
|
|
setItems(prev => prev.map(w => w.id === editingWorkflow.id ? {
|
|
...w,
|
|
name: formName,
|
|
description: formDesc,
|
|
dbTypes: formDbTypes,
|
|
steps: [...formSteps],
|
|
conditions: conditions.length > 0 ? conditions : undefined,
|
|
} : w));
|
|
} else {
|
|
const newWf: WorkflowDetail = {
|
|
id: Date.now(),
|
|
name: formName,
|
|
description: formDesc,
|
|
dbTypes: formDbTypes,
|
|
steps: [...formSteps],
|
|
enabled: true,
|
|
conditions: conditions.length > 0 ? conditions : undefined,
|
|
};
|
|
setItems(prev => [...prev, newWf]);
|
|
}
|
|
setShowCreateModal(false);
|
|
};
|
|
|
|
const toggleDbType = (dt: string) => {
|
|
setFormDbTypes(prev =>
|
|
prev.includes(dt) ? prev.filter(d => d !== dt) : [...prev, dt]
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-5">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-semibold">审批流程配置</h1>
|
|
<p className="text-sm text-[var(--dg-text-secondary)] mt-1">配置多级审批流程模板,支持提交阶段、审核阶段、执行阶段的自定义配置</p>
|
|
</div>
|
|
<button
|
|
onClick={openCreate}
|
|
className="flex items-center gap-2 px-4 py-2.5 bg-gradient-to-r from-blue-500 to-purple-600 rounded-lg text-sm font-medium text-white hover:opacity-90 transition-opacity"
|
|
>
|
|
<Plus className="w-4 h-4" /> 新建流程
|
|
</button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-4 gap-4">
|
|
{[
|
|
{ label: '流程总数', value: items.length, icon: GitBranch, color: 'text-blue-400', bg: 'bg-blue-400/10' },
|
|
{ label: '已启用', value: items.filter(w => w.enabled).length, icon: CheckCircle, color: 'text-green-400', bg: 'bg-green-400/10' },
|
|
{ label: '多级审批', value: items.filter(w => w.steps.filter(s => s.type === 'audit').length > 1).length, icon: Users, color: 'text-purple-400', bg: 'bg-purple-400/10' },
|
|
{ label: '单级审批', value: items.filter(w => w.steps.filter(s => s.type === 'audit').length === 1).length, icon: Shield, color: 'text-orange-400', bg: 'bg-orange-400/10' },
|
|
].map((s, i) => (
|
|
<Card key={i} className="dg-card">
|
|
<CardContent className="p-4 flex items-center gap-3">
|
|
<div className={`w-10 h-10 rounded-lg ${s.bg} flex items-center justify-center`}><s.icon className={`w-5 h-5 ${s.color}`} /></div>
|
|
<div><div className="text-2xl font-bold">{s.value}</div><div className="text-xs text-[var(--dg-text-muted)]">{s.label}</div></div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
{items.map(wf => {
|
|
const isExpanded = expandedId === wf.id;
|
|
return (
|
|
<Card key={wf.id} className={`dg-card overflow-hidden ${!wf.enabled ? 'opacity-50' : ''}`}>
|
|
<CardContent className="p-5">
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-3 mb-2">
|
|
<h3 className="text-lg font-semibold">{wf.name}</h3>
|
|
<span className={`px-2 py-0.5 rounded text-xs font-medium ${wf.enabled ? 'bg-green-500/15 text-green-400' : 'bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-muted)]'}`}>
|
|
{wf.enabled ? '已启用' : '已禁用'}
|
|
</span>
|
|
<span className="px-2 py-0.5 bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-muted)] text-xs rounded">
|
|
{wf.steps.length} 个阶段
|
|
</span>
|
|
</div>
|
|
<p className="text-sm text-[var(--dg-text-secondary)] mb-3">{wf.description}</p>
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<span className="text-xs text-[var(--dg-text-muted)]">适用数据库:</span>
|
|
{wf.dbTypes.map(dt => (
|
|
<span key={dt} className={`inline-flex px-2 py-0.5 rounded text-[11px] font-medium ${dbTypeColorMap[dt] || ''}`}>{dt.toUpperCase()}</span>
|
|
))}
|
|
</div>
|
|
|
|
{/* 流程可视化 */}
|
|
<div className="flex items-center gap-1 mt-4 p-4 bg-[var(--dg-bg-tertiary)] rounded-lg overflow-x-auto">
|
|
{wf.steps.map((step, i) => {
|
|
const StepIcon = stepTypeIcons[step.type];
|
|
return (
|
|
<div key={step.id} className="flex items-center gap-1 flex-shrink-0">
|
|
<div className={`px-3 py-2 rounded-lg border ${stepTypeColors[step.type]} flex items-center gap-2`}>
|
|
<StepIcon className="w-4 h-4" />
|
|
<div>
|
|
<div className="text-xs font-medium whitespace-nowrap">{step.name}</div>
|
|
<div className="text-[10px] opacity-80 whitespace-nowrap">{step.assignees.join(' / ')}</div>
|
|
</div>
|
|
</div>
|
|
{i < wf.steps.length - 1 && <ArrowRight className="w-4 h-4 text-[var(--dg-text-muted)] flex-shrink-0" />}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* 展开详情 */}
|
|
{isExpanded && (
|
|
<div className="mt-4 space-y-3 pt-4 border-t border-[var(--dg-border)]">
|
|
<h4 className="text-sm font-semibold">流程步骤详情</h4>
|
|
{wf.steps.map((step) => {
|
|
const StepIcon = stepTypeIcons[step.type];
|
|
return (
|
|
<div key={step.id} className="flex items-start gap-3 p-3 bg-[var(--dg-bg-primary)] rounded-lg border border-[var(--dg-border)]">
|
|
<div className={`w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 ${stepTypeColors[step.type].split(' ')[0]}`}>
|
|
<StepIcon className={`w-4 h-4 ${stepTypeColors[step.type].split(' ')[1]}`} />
|
|
</div>
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<span className="text-sm font-medium">{step.name}</span>
|
|
<span className={`text-[11px] px-2 py-0.5 rounded ${stepTypeColors[step.type]}`}>{stepTypeLabels[step.type]}</span>
|
|
{step.mode && (
|
|
<span className="text-[11px] px-2 py-0.5 rounded bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-muted)]">
|
|
{step.mode === 'any' ? '或签(任一人)' : '会签(全部人)'}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<p className="text-xs text-[var(--dg-text-secondary)]">{step.description}</p>
|
|
<div className="flex items-center gap-1 mt-1">
|
|
<span className="text-xs text-[var(--dg-text-muted)]">执行人:</span>
|
|
{step.assignees.map((a, j) => (
|
|
<span key={j} className="text-xs text-blue-400 bg-blue-500/10 px-1.5 py-0.5 rounded">{a}</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
{wf.conditions && (
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-xs text-[var(--dg-text-muted)]">触发条件:</span>
|
|
{wf.conditions.map(c => (
|
|
<span key={c} className="px-2 py-0.5 bg-yellow-500/10 text-yellow-400 text-xs rounded">{c}</span>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1 ml-4">
|
|
<button onClick={() => setPreviewWorkflow(wf)} className="w-8 h-8 rounded-lg flex items-center justify-center text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-hover)] hover:text-[var(--dg-text-primary)] transition-colors" title="预览">
|
|
<Eye className="w-4 h-4" />
|
|
</button>
|
|
<button onClick={() => openEdit(wf)} className="w-8 h-8 rounded-lg flex items-center justify-center text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-hover)] hover:text-[var(--dg-text-primary)] transition-colors" title="编辑">
|
|
<Edit2 className="w-4 h-4" />
|
|
</button>
|
|
<button onClick={() => toggleWorkflow(wf.id)} className="w-8 h-8 rounded-lg flex items-center justify-center transition-colors" title={wf.enabled ? '禁用' : '启用'}>
|
|
{wf.enabled ? <ToggleRight className="w-5 h-5 text-green-400" /> : <ToggleLeft className="w-5 h-5 text-[var(--dg-text-muted)]" />}
|
|
</button>
|
|
<button onClick={() => deleteWorkflow(wf.id)} className="w-8 h-8 rounded-lg flex items-center justify-center text-[var(--dg-text-secondary)] hover:bg-red-500/15 hover:text-red-400 transition-colors" title="删除">
|
|
<Trash2 className="w-4 h-4" />
|
|
</button>
|
|
<button onClick={() => setExpandedId(isExpanded ? null : wf.id)} className="w-8 h-8 rounded-lg flex items-center justify-center text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-hover)] transition-colors" title="展开">
|
|
{isExpanded ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* 预览弹窗 */}
|
|
{previewWorkflow && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" onClick={() => setPreviewWorkflow(null)}>
|
|
<div className="bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded-xl p-6 max-w-2xl w-full mx-4" onClick={e => e.stopPropagation()}>
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h3 className="text-xl font-semibold">{previewWorkflow.name} - 流程预览</h3>
|
|
<button onClick={() => setPreviewWorkflow(null)} className="w-8 h-8 rounded-lg flex items-center justify-center hover:bg-[var(--dg-bg-hover)] transition-colors">
|
|
<X className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
<div className="space-y-4">
|
|
{previewWorkflow.steps.map((step, i) => {
|
|
const StepIcon = stepTypeIcons[step.type];
|
|
return (
|
|
<div key={step.id} className="flex items-start gap-4">
|
|
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${stepTypeColors[step.type].split(' ')[0]}`}>
|
|
<StepIcon className={`w-5 h-5 ${stepTypeColors[step.type].split(' ')[1]}`} />
|
|
</div>
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<span className="font-medium">{step.name}</span>
|
|
<span className={`text-[11px] px-2 py-0.5 rounded ${stepTypeColors[step.type]}`}>{stepTypeLabels[step.type]}</span>
|
|
</div>
|
|
<p className="text-sm text-[var(--dg-text-secondary)]">{step.description || '该阶段暂无详细描述'}</p>
|
|
<div className="flex items-center gap-2 mt-1">
|
|
<span className="text-xs text-[var(--dg-text-muted)]">负责人:</span>
|
|
{step.assignees.map((a, j) => (
|
|
<span key={j} className="text-xs text-blue-400">{a}{j < step.assignees.length - 1 ? ' / ' : ''}</span>
|
|
))}
|
|
{step.mode && (
|
|
<span className="text-[11px] px-2 py-0.5 rounded bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-muted)] ml-2">
|
|
{step.mode === 'any' ? '或签' : '会签'}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
{i < previewWorkflow.steps.length - 1 && <ArrowRight className="w-5 h-5 text-[var(--dg-text-muted)] mt-2" />}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
<div className="flex justify-end mt-6">
|
|
<button onClick={() => setPreviewWorkflow(null)} className="px-5 py-2.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm hover:bg-[var(--dg-bg-hover)] transition-colors">
|
|
关闭
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 新建/编辑流程弹窗 */}
|
|
{showCreateModal && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" onClick={() => setShowCreateModal(false)}>
|
|
<div className="bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded-xl p-6 max-w-3xl w-full mx-4 max-h-[90vh] overflow-y-auto" onClick={e => e.stopPropagation()}>
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h3 className="text-xl font-semibold">{editingWorkflow ? '编辑流程' : '新建流程'}</h3>
|
|
<button onClick={() => setShowCreateModal(false)} className="w-8 h-8 rounded-lg flex items-center justify-center hover:bg-[var(--dg-bg-hover)] transition-colors">
|
|
<X className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="space-y-5">
|
|
{/* 基本信息 */}
|
|
<div className="space-y-4">
|
|
<h4 className="text-sm font-semibold text-[var(--dg-text-secondary)] uppercase tracking-wider">基本信息</h4>
|
|
<div>
|
|
<label className="text-xs text-[var(--dg-text-muted)] mb-1.5 block">流程名称 <span className="text-red-400">*</span></label>
|
|
<input
|
|
type="text"
|
|
value={formName}
|
|
onChange={e => setFormName(e.target.value)}
|
|
className="w-full px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm focus:outline-none focus:border-blue-500"
|
|
placeholder="例如:钱包平台 SQL 变更流程"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="text-xs text-[var(--dg-text-muted)] mb-1.5 block">流程描述</label>
|
|
<textarea
|
|
value={formDesc}
|
|
onChange={e => setFormDesc(e.target.value)}
|
|
className="w-full px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm focus:outline-none focus:border-blue-500 resize-none"
|
|
rows={2}
|
|
placeholder="描述该流程的用途和适用范围"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="text-xs text-[var(--dg-text-muted)] mb-2 block">适用数据库</label>
|
|
<div className="flex flex-wrap gap-2">
|
|
{dbTypes.map(dt => (
|
|
<button
|
|
key={dt}
|
|
onClick={() => toggleDbType(dt)}
|
|
className={`px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors ${
|
|
formDbTypes.includes(dt)
|
|
? 'bg-blue-500/15 text-blue-400 border-blue-500/30'
|
|
: 'bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-muted)] border-[var(--dg-border)] hover:border-[var(--dg-text-secondary)]'
|
|
}`}
|
|
>
|
|
{dt.toUpperCase()}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className="text-xs text-[var(--dg-text-muted)] mb-1.5 block">触发条件(用逗号分隔)</label>
|
|
<input
|
|
type="text"
|
|
value={formConditions}
|
|
onChange={e => setFormConditions(e.target.value)}
|
|
className="w-full px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm focus:outline-none focus:border-blue-500"
|
|
placeholder="例如:影响行数>1000, 执行时间>1s"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 步骤配置 */}
|
|
<div className="space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<h4 className="text-sm font-semibold text-[var(--dg-text-secondary)] uppercase tracking-wider">流程步骤</h4>
|
|
<button
|
|
onClick={addStep}
|
|
className="flex items-center gap-1 px-3 py-1.5 bg-blue-500/15 text-blue-400 rounded-lg text-xs hover:bg-blue-500/25 transition-colors"
|
|
>
|
|
<Plus className="w-3.5 h-3.5" /> 添加步骤
|
|
</button>
|
|
</div>
|
|
|
|
{formSteps.map((step, index) => (
|
|
<Card key={step.id} className="dg-card border-l-2 border-l-blue-500">
|
|
<CardContent className="p-4">
|
|
<div className="flex items-start gap-3">
|
|
<div className="flex flex-col gap-1 pt-1">
|
|
<button onClick={() => moveStep(index, 'up')} disabled={index === 0} className="w-6 h-6 rounded flex items-center justify-center hover:bg-[var(--dg-bg-hover)] disabled:opacity-30 transition-colors">
|
|
<ChevronUp className="w-3.5 h-3.5" />
|
|
</button>
|
|
<button onClick={() => moveStep(index, 'down')} disabled={index === formSteps.length - 1} className="w-6 h-6 rounded flex items-center justify-center hover:bg-[var(--dg-bg-hover)] disabled:opacity-30 transition-colors">
|
|
<ChevronDown className="w-3.5 h-3.5" />
|
|
</button>
|
|
</div>
|
|
<div className="flex-1 space-y-3">
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">步骤名称</label>
|
|
<input
|
|
type="text"
|
|
value={step.name}
|
|
onChange={e => updateStep(step.id, { name: e.target.value })}
|
|
className="w-full px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm focus:outline-none focus:border-blue-500"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">步骤类型</label>
|
|
<select
|
|
value={step.type}
|
|
onChange={e => updateStep(step.id, { type: e.target.value as StepType })}
|
|
className="w-full px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm focus:outline-none focus:border-blue-500"
|
|
>
|
|
<option value="submit">提交阶段</option>
|
|
<option value="audit">审核阶段</option>
|
|
<option value="execute">执行阶段</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">负责人(用/分隔)</label>
|
|
<input
|
|
type="text"
|
|
value={step.assignees.join(' / ')}
|
|
onChange={e => updateStep(step.id, { assignees: e.target.value.split('/').map(s => s.trim()).filter(Boolean) })}
|
|
className="w-full px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm focus:outline-none focus:border-blue-500"
|
|
placeholder="例如:张三 / 李四"
|
|
/>
|
|
</div>
|
|
{step.type === 'audit' && (
|
|
<div>
|
|
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">审批模式</label>
|
|
<select
|
|
value={step.mode || 'any'}
|
|
onChange={e => updateStep(step.id, { mode: e.target.value as 'any' | 'all' })}
|
|
className="w-full px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm focus:outline-none focus:border-blue-500"
|
|
>
|
|
<option value="any">或签(任一人通过)</option>
|
|
<option value="all">会签(全部人通过)</option>
|
|
</select>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div>
|
|
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">步骤描述</label>
|
|
<input
|
|
type="text"
|
|
value={step.description || ''}
|
|
onChange={e => updateStep(step.id, { description: e.target.value })}
|
|
className="w-full px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm focus:outline-none focus:border-blue-500"
|
|
placeholder="描述该步骤的具体操作..."
|
|
/>
|
|
</div>
|
|
</div>
|
|
{formSteps.length > 1 && (
|
|
<button
|
|
onClick={() => removeStep(step.id)}
|
|
className="w-7 h-7 rounded-lg flex items-center justify-center text-[var(--dg-text-muted)] hover:bg-red-500/15 hover:text-red-400 transition-colors"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end gap-3 mt-6 pt-4 border-t border-[var(--dg-border)]">
|
|
<button onClick={() => setShowCreateModal(false)} className="px-5 py-2.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm hover:bg-[var(--dg-bg-hover)] transition-colors">
|
|
取消
|
|
</button>
|
|
<button
|
|
onClick={saveWorkflow}
|
|
disabled={!formName.trim() || formDbTypes.length === 0}
|
|
className="flex items-center gap-2 px-5 py-2.5 bg-gradient-to-r from-blue-500 to-purple-600 rounded-lg text-sm font-medium text-white hover:opacity-90 transition-opacity disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<Save className="w-4 h-4" /> {editingWorkflow ? '保存修改' : '创建流程'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|