feat: 完善告警规则配置页面 — 增删改查弹窗 + 修复白屏bug + UpdateRule notify_channels序列化修复

- AlertManager: 新建/编辑告警规则弹窗(规则名称/描述/类型/数据源/表达式/级别/持续时间/通知渠道Switch)
- AlertManager: 修复白屏bug — getRules返回{list,total}未解包导致rules.map崩溃(Frontend Pitfall #1)
- AlertManager: 修复规则列表字段显示 — 原引用不存在的rule.metric/operator/threshold字段,改为rule.source/expression
- AlertManager: 新增规则删除按钮(带确认)
- alert.go: 修复UpdateRule notify_channels序列化bug — 原直接赋值[]string到text列导致500,改用toJSON()
- 同步提交前序未提交改动(WebShell/K8s/Server/Login等组件改进)
This commit is contained in:
attacker committed 2026-06-14 03:07:44 +08:00
1 parent 170818c141
commit 00d5510366
13 files changed
+2170 -496

No files matched your search

+441 -157
View File
@@ -3,6 +3,8 @@ import { cn } from '@/lib/utils';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Badge } from '@/components/ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import {
@@ -21,6 +23,13 @@ import {
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
CheckSquare,
Plus,
@@ -35,6 +44,7 @@ import {
Edit,
GitBranch,
Loader2,
Filter,
} from 'lucide-react';
import { approvalApi } from '@/services/api';
@@ -51,15 +61,44 @@ function parseSteps(steps: any) {
return [];
}
// 审批类型字典(参考 sql-platform 审批流程)
const APPROVAL_TYPE_OPTIONS = [
{ value: 'deploy', label: '发布审批', color: 'var(--dg-blue)' },
{ value: 'server', label: '服务器操作', color: 'var(--dg-cyan)' },
{ value: 'permission', label: '权限申请', color: 'var(--dg-purple)' },
];
const getApprovalTypeLabel = (value: string): string =>
APPROVAL_TYPE_OPTIONS.find((o) => o.value === value)?.label || value || '通用审批';
const getApprovalTypeColor = (value: string): string =>
APPROVAL_TYPE_OPTIONS.find((o) => o.value === value)?.color || 'var(--dg-text-secondary)';
// 状态筛选标签
const STATUS_TABS = [
{ value: 'all', label: '全部' },
{ value: 'pending', label: '待审批' },
{ value: 'approved', label: '已通过' },
{ value: 'rejected', label: '已驳回' },
];
export function ApprovalFlowManager() {
const [activeTab, setActiveTab] = useState('instances');
const [searchQuery, setSearchQuery] = useState('');
const [statusFilter, setStatusFilter] = useState<string>('all');
const [templateTypeFilter, setTemplateTypeFilter] = useState<string>('all');
const [processes, setProcesses] = useState<any[]>([]);
const [templates, setTemplates] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [selectedInstance, setSelectedInstance] = useState<any | null>(null);
// 审批操作 Dialog 状态
const [actionDialogOpen, setActionDialogOpen] = useState(false);
const [actionType, setActionType] = useState<'approve' | 'reject'>('approve');
const [actionTarget, setActionTarget] = useState<any | null>(null);
const [actionComment, setActionComment] = useState('');
const [actionSubmitting, setActionSubmitting] = useState(false);
const loadData = async () => {
try {
setLoading(true);
@@ -68,7 +107,7 @@ export function ApprovalFlowManager() {
approvalApi.getTemplates(),
]);
setProcesses((processesData as any)?.list || (processesData as any) || []);
setTemplates(templatesData || []);
setTemplates((templatesData as any)?.list || (templatesData as any) || []);
} catch (error) {
console.error('Failed to load approval data:', error);
} finally {
@@ -88,11 +127,17 @@ export function ApprovalFlowManager() {
return matchesSearch && matchesStatus;
});
// 按类型筛选的模板
const filteredTemplates = templates.filter((template) => {
if (templateTypeFilter === 'all') return true;
return template.type === templateTypeFilter || template.category === templateTypeFilter;
});
const getStatusBadge = (status: any) => {
const configs: Record<string, any> = {
pending: { label: '审批中', className: 'border-amber-500/30 text-amber-400 bg-amber-500/10', icon: Clock },
pending: { label: '待审批', className: 'border-amber-500/30 text-amber-400 bg-amber-500/10', icon: Clock },
approved: { label: '已通过', className: 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10', icon: CheckCircle },
rejected: { label: '已拒绝', className: 'border-red-500/30 text-red-400 bg-red-500/10', icon: XCircle },
rejected: { label: '已驳回', className: 'border-red-500/30 text-red-400 bg-red-500/10', icon: XCircle },
};
const config = configs[status] || configs.pending;
const Icon = config.icon;
@@ -104,31 +149,50 @@ export function ApprovalFlowManager() {
);
};
const handleApprove = async (id: any) => {
// 打开审批操作 Dialog
const openActionDialog = (instance: any, type: 'approve' | 'reject') => {
setActionTarget(instance);
setActionType(type);
setActionComment('');
setActionDialogOpen(true);
};
// 执行审批操作
const handleConfirmAction = async () => {
if (!actionTarget) return;
try {
await approvalApi.approveProcess(id);
setActionSubmitting(true);
if (actionType === 'approve') {
await approvalApi.approveProcess(actionTarget.id, actionComment);
} else {
await approvalApi.rejectProcess(actionTarget.id, actionComment);
}
setActionDialogOpen(false);
setSelectedInstance(null);
setActionTarget(null);
loadData();
} catch (error) {
console.error('Failed to approve process:', error);
console.error('Failed to process approval action:', error);
} finally {
setActionSubmitting(false);
}
};
const handleReject = async (id: any) => {
try {
await approvalApi.rejectProcess(id);
setSelectedInstance(null);
loadData();
} catch (error) {
console.error('Failed to reject process:', error);
}
// 计算审批进度
const getProgressInfo = (instance: any) => {
const steps = parseSteps(instance.steps);
const total = steps.length || instance.total_steps || 1;
const current = instance.current_step_index ?? instance.current_step ?? 1;
const currentNum = typeof current === 'number' ? current : 1;
const percent = total > 0 ? Math.min(100, Math.round((currentNum / total) * 100)) : 0;
return { current: currentNum, total, percent };
};
// 模拟审批流程图
const renderFlowChart = (flow: any) => {
const nodes = parseSteps(flow.steps);
if (nodes.length === 0) {
return <div className="text-slate-500 text-center py-4">暂无流程节点</div>;
return <div className="text-[var(--dg-text-muted)] text-center py-4">暂无流程节点</div>;
}
return (
<div className="flex items-center gap-2 overflow-x-auto py-4">
@@ -138,20 +202,20 @@ export function ApprovalFlowManager() {
className={cn(
'px-4 py-2 rounded-lg border min-w-[100px] text-center',
node.type === 'start' && 'bg-emerald-500/10 border-emerald-500/30 text-emerald-400',
node.type === 'approval' && 'bg-blue-500/10 border-blue-500/30 text-blue-400',
node.type === 'approval' && 'bg-[var(--dg-blue)]/10 border-[var(--dg-blue)]/30 text-[var(--dg-blue)]',
node.type === 'condition' && 'bg-amber-500/10 border-amber-500/30 text-amber-400',
node.type === 'end' && 'bg-slate-500/10 border-slate-500/30 text-slate-400'
node.type === 'end' && 'bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-secondary)]'
)}
>
<p className="text-sm font-medium">{node.name}</p>
{node.approvers && (
<p className="text-xs text-slate-500 mt-1">
<p className="text-xs text-[var(--dg-text-muted)] mt-1">
{Array.isArray(node.approvers) ? node.approvers.join(', ') : node.approvers}
</p>
)}
</div>
{index < nodes.length - 1 && (
<ArrowRight className="h-4 w-4 text-slate-600 mx-2" />
<ArrowRight className="h-4 w-4 text-[var(--dg-text-muted)] mx-2" />
)}
</div>
))}
@@ -159,20 +223,48 @@ export function ApprovalFlowManager() {
);
};
// 审批步骤可视化(进度条)
const renderStepProgress = (instance: any) => {
const { current, total, percent } = getProgressInfo(instance);
return (
<div className="space-y-2">
<div className="flex items-center justify-between text-xs">
<span className="text-[var(--dg-text-secondary)]">
当前步骤 <span className="font-medium text-[var(--dg-text-primary)]">{current}</span> / {total}
</span>
<span className="text-[var(--dg-text-muted)]">{percent}%</span>
</div>
<div className="w-full h-1.5 bg-[var(--dg-bg-tertiary)] rounded-full overflow-hidden">
<div
className={cn(
'h-full rounded-full transition-all',
instance.status === 'rejected'
? 'bg-red-400'
: instance.status === 'approved'
? 'bg-emerald-400'
: 'bg-[var(--dg-blue)]'
)}
style={{ width: `${instance.status === 'rejected' ? 100 : percent}%` }}
/>
</div>
</div>
);
};
return (
<div className="space-y-6">
{/* Page Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-slate-100">审批流程</h1>
<p className="text-sm text-slate-500 mt-1">工作流管理与审批处理</p>
<h1 className="text-2xl font-bold text-[var(--dg-text-primary)]">审批流程</h1>
<p className="text-sm text-[var(--dg-text-muted)] mt-1">工作流管理与审批处理</p>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" className="border-slate-700 text-slate-300 hover:text-slate-100 hover:bg-slate-800">
<Button variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)]">
<Settings className="h-4 w-4 mr-2" />
流程设计器
</Button>
<Button className="bg-blue-600 hover:bg-blue-700 text-white">
<Button className="bg-[var(--dg-blue)] hover:opacity-90 text-white">
<Plus className="h-4 w-4 mr-2" />
新建流程
</Button>
@@ -181,11 +273,11 @@ export function ApprovalFlowManager() {
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card className="bg-slate-900 border-slate-800">
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">待审批</p>
<p className="text-sm text-[var(--dg-text-muted)]">待审批</p>
<p className="text-2xl font-bold text-amber-400">
{processes.filter((i) => i.status === 'pending').length}
</p>
@@ -196,11 +288,11 @@ export function ApprovalFlowManager() {
</div>
</CardContent>
</Card>
<Card className="bg-slate-900 border-slate-800">
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">已通过</p>
<p className="text-sm text-[var(--dg-text-muted)]">已通过</p>
<p className="text-2xl font-bold text-emerald-400">
{processes.filter((i) => i.status === 'approved').length}
</p>
@@ -211,11 +303,11 @@ export function ApprovalFlowManager() {
</div>
</CardContent>
</Card>
<Card className="bg-slate-900 border-slate-800">
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">已拒绝</p>
<p className="text-sm text-[var(--dg-text-muted)]">已驳回</p>
<p className="text-2xl font-bold text-red-400">
{processes.filter((i) => i.status === 'rejected').length}
</p>
@@ -226,14 +318,14 @@ export function ApprovalFlowManager() {
</div>
</CardContent>
</Card>
<Card className="bg-slate-900 border-slate-800">
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">流程模板</p>
<p className="text-2xl font-bold text-slate-100">{templates.length}</p>
<p className="text-sm text-[var(--dg-text-muted)]">流程模板</p>
<p className="text-2xl font-bold text-[var(--dg-text-primary)]">{templates.length}</p>
</div>
<div className="p-3 rounded-lg bg-blue-500/10 text-blue-400">
<div className="p-3 rounded-lg bg-[var(--dg-blue)]/10 text-[var(--dg-blue)]">
<GitBranch className="h-5 w-5" />
</div>
</div>
@@ -243,104 +335,139 @@ export function ApprovalFlowManager() {
{/* Tabs */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="bg-slate-800 border border-slate-700">
<TabsTrigger value="instances" className="data-[state=active]:bg-slate-700">
<TabsList className="bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)]">
<TabsTrigger value="instances" className="data-[state=active]:bg-[var(--dg-bg-hover)] data-[state=active]:text-[var(--dg-text-primary)] text-[var(--dg-text-secondary)]">
审批实例
</TabsTrigger>
<TabsTrigger value="flows" className="data-[state=active]:bg-slate-700">
<TabsTrigger value="flows" className="data-[state=active]:bg-[var(--dg-bg-hover)] data-[state=active]:text-[var(--dg-text-primary)] text-[var(--dg-text-secondary)]">
流程模板
</TabsTrigger>
<TabsTrigger value="my-approvals" className="data-[state=active]:bg-slate-700">
<TabsTrigger value="my-approvals" className="data-[state=active]:bg-[var(--dg-bg-hover)] data-[state=active]:text-[var(--dg-text-primary)] text-[var(--dg-text-secondary)]">
我的审批
</TabsTrigger>
</TabsList>
{/* 审批实例 Tab */}
<TabsContent value="instances" className="mt-4 space-y-4">
{/* Filters */}
<div className="flex items-center gap-3 flex-wrap">
{/* 状态筛选标签 + 搜索 */}
<div className="flex items-center justify-between flex-wrap gap-3">
{/* 状态筛选标签 */}
<div className="flex items-center gap-1 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg p-1">
{STATUS_TABS.map((tab) => {
const count =
tab.value === 'all'
? processes.length
: processes.filter((i) => i.status === tab.value).length;
return (
<button
key={tab.value}
onClick={() => setStatusFilter(tab.value)}
className={cn(
'flex items-center gap-1.5 px-3 py-1.5 rounded-md text-sm transition-colors',
statusFilter === tab.value
? 'bg-[var(--dg-bg-hover)] text-[var(--dg-text-primary)] font-medium'
: 'text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)]'
)}
>
{tab.label}
<span
className={cn(
'text-xs px-1.5 py-0.5 rounded-full',
statusFilter === tab.value
? 'bg-[var(--dg-blue)]/20 text-[var(--dg-blue)]'
: 'bg-[var(--dg-bg-hover)] text-[var(--dg-text-muted)]'
)}
>
{count}
</span>
</button>
);
})}
</div>
<div className="relative w-64">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-500" />
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[var(--dg-text-muted)]" />
<Input
placeholder="搜索流程名称或申请人..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9 h-9 bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-500"
className="pl-9 h-9 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
/>
</div>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="h-9 px-3 rounded-md bg-slate-800/50 border border-slate-700 text-slate-200 text-sm"
>
<option value="all">全部状态</option>
<option value="pending">审批中</option>
<option value="approved">已通过</option>
<option value="rejected">已拒绝</option>
</select>
</div>
{/* Instances Table */}
<Card className="bg-slate-900 border-slate-800">
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-0">
<div className="border-t border-slate-800">
<Table>
<TableHeader>
<TableRow className="border-slate-800 hover:bg-transparent">
<TableHead className="text-slate-400">流程名称</TableHead>
<TableHead className="text-slate-400">申请人</TableHead>
<TableHead className="text-slate-400">状态</TableHead>
<TableHead className="text-slate-400">当前节点</TableHead>
<TableHead className="text-slate-400">创建时间</TableHead>
<TableHead className="text-slate-400">完成时间</TableHead>
<TableHead className="text-slate-400 text-right">操作</TableHead>
<Table>
<TableHeader>
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
<TableHead className="text-[var(--dg-text-secondary)]">流程名称</TableHead>
<TableHead className="text-[var(--dg-text-secondary)]">申请人</TableHead>
<TableHead className="text-[var(--dg-text-secondary)]">状态</TableHead>
<TableHead className="text-[var(--dg-text-secondary)]">审批进度</TableHead>
<TableHead className="text-[var(--dg-text-secondary)]">当前节点</TableHead>
<TableHead className="text-[var(--dg-text-secondary)]">创建时间</TableHead>
<TableHead className="text-[var(--dg-text-secondary)] text-right">操作</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow className="border-[var(--dg-border)]">
<TableCell colSpan={7} className="text-center text-[var(--dg-text-muted)] py-8">
<Loader2 className="h-5 w-5 animate-spin inline mr-2" />
加载中...
</TableCell>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow className="border-slate-800">
<TableCell colSpan={7} className="text-center text-slate-500 py-8">
<Loader2 className="h-5 w-5 animate-spin inline mr-2" />
加载中...
</TableCell>
</TableRow>
) : filteredInstances.map((instance) => (
<TableRow key={instance.id} className="border-slate-800 hover:bg-slate-800/50">
) : filteredInstances.length === 0 ? (
<TableRow className="border-[var(--dg-border)]">
<TableCell colSpan={7} className="text-center text-[var(--dg-text-muted)] py-8">
暂无审批实例
</TableCell>
</TableRow>
) : (
filteredInstances.map((instance) => (
<TableRow key={instance.id} className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/50">
<TableCell>
<div className="flex items-center gap-2">
<GitBranch className="h-4 w-4 text-slate-500" />
<span className="text-sm font-medium text-slate-200">{instance.title}</span>
<GitBranch className="h-4 w-4 text-[var(--dg-text-muted)]" />
<div>
<span className="text-sm font-medium text-[var(--dg-text-primary)]">{instance.title}</span>
{instance.type && (
<p className="text-xs mt-0.5" style={{ color: getApprovalTypeColor(instance.type) }}>
{getApprovalTypeLabel(instance.type)}
</p>
)}
</div>
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-1 text-sm text-slate-300">
<User className="h-3.5 w-3.5 text-slate-500" />
<div className="flex items-center gap-1 text-sm text-[var(--dg-text-secondary)]">
<User className="h-3.5 w-3.5 text-[var(--dg-text-muted)]" />
{instance.applicant_name || instance.applicant}
</div>
</TableCell>
<TableCell>{getStatusBadge(instance.status)}</TableCell>
{/* 审批进度可视化 */}
<TableCell className="min-w-[160px]">
{renderStepProgress(instance)}
</TableCell>
<TableCell>
<Badge variant="outline" className="border-slate-700 text-slate-400">
<Badge variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">
{instance.current_step || '--'}
</Badge>
</TableCell>
<TableCell>
<span className="text-sm text-slate-400">
<span className="text-sm text-[var(--dg-text-secondary)]">
{instance.created_at ? new Date(instance.created_at).toLocaleString() : '--'}
</span>
</TableCell>
<TableCell>
<span className="text-sm text-slate-400">
{instance.completed_at
? new Date(instance.completed_at).toLocaleString()
: '--'}
</span>
</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end gap-1">
<Button
variant="ghost"
size="sm"
className="h-8 text-slate-400 hover:text-slate-100"
className="h-8 text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)]"
onClick={() => setSelectedInstance(instance)}
>
<Eye className="h-4 w-4 mr-1" />
@@ -352,7 +479,7 @@ export function ApprovalFlowManager() {
variant="ghost"
size="sm"
className="h-8 text-emerald-400 hover:text-emerald-400"
onClick={() => handleApprove(instance.id)}
onClick={() => openActionDialog(instance, 'approve')}
>
<CheckCircle className="h-4 w-4 mr-1" />
通过
@@ -361,117 +488,191 @@ export function ApprovalFlowManager() {
variant="ghost"
size="sm"
className="h-8 text-red-400 hover:text-red-400"
onClick={() => handleReject(instance.id)}
onClick={() => openActionDialog(instance, 'reject')}
>
<XCircle className="h-4 w-4 mr-1" />
拒绝
驳回
</Button>
</>
)}
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
))
)}
</TableBody>
</Table>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="flows" className="mt-4">
{/* 流程模板 Tab */}
<TabsContent value="flows" className="mt-4 space-y-4">
{/* 类型筛选 */}
<div className="flex items-center gap-3 flex-wrap">
<div className="flex items-center gap-1.5 text-sm text-[var(--dg-text-secondary)]">
<Filter className="h-4 w-4" />
<span>类型筛选:</span>
</div>
<div className="flex items-center gap-1 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg p-1">
<button
onClick={() => setTemplateTypeFilter('all')}
className={cn(
'px-3 py-1.5 rounded-md text-sm transition-colors',
templateTypeFilter === 'all'
? 'bg-[var(--dg-bg-hover)] text-[var(--dg-text-primary)] font-medium'
: 'text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)]'
)}
>
全部类型
</button>
{APPROVAL_TYPE_OPTIONS.map((opt) => (
<button
key={opt.value}
onClick={() => setTemplateTypeFilter(opt.value)}
className={cn(
'px-3 py-1.5 rounded-md text-sm transition-colors flex items-center gap-1.5',
templateTypeFilter === opt.value
? 'bg-[var(--dg-bg-hover)] text-[var(--dg-text-primary)] font-medium'
: 'text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)]'
)}
>
<span style={{ color: opt.color }}>●</span>
{opt.label}
</button>
))}
</div>
</div>
<div className="grid grid-cols-1 gap-4">
{templates.map((flow) => (
<Card key={flow.id} className="bg-slate-900 border-slate-800">
<CardHeader className="pb-2">
<div className="flex items-start justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-blue-500/10 flex items-center justify-center">
<GitBranch className="h-5 w-5 text-blue-400" />
</div>
<div>
<CardTitle className="text-base text-slate-100">{flow.name}</CardTitle>
<CardDescription className="text-slate-500">{flow.description}</CardDescription>
</div>
</div>
<div className="flex items-center gap-2">
<Badge
variant="outline"
className={
flow.status === 1 || flow.status === 'active' || flow.status === true
? 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10'
: 'border-slate-500/30 text-slate-400 bg-slate-500/10'
}
>
{flow.status === 1 || flow.status === 'active' || flow.status === true ? '已启用' : '已停用'}
</Badge>
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-slate-100">
<Edit className="h-4 w-4" />
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<div className="bg-slate-950 rounded-lg p-4 overflow-x-auto">
{renderFlowChart(flow)}
</div>
<div className="flex items-center justify-between mt-4 text-sm text-slate-500">
<span>创建于 {flow.created_at ? new Date(flow.created_at).toLocaleDateString() : '--'}</span>
<span>{parseSteps(flow.steps).length} 个节点</span>
{filteredTemplates.length === 0 ? (
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-8">
<div className="text-center">
<GitBranch className="h-12 w-12 mx-auto text-[var(--dg-text-muted)] mb-4" />
<h3 className="text-lg font-medium text-[var(--dg-text-secondary)] mb-2">暂无流程模板</h3>
<p className="text-sm text-[var(--dg-text-muted)]">当前筛选条件下没有匹配的模板</p>
</div>
</CardContent>
</Card>
))}
) : (
filteredTemplates.map((flow) => (
<Card key={flow.id} className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardHeader className="pb-2">
<div className="flex items-start justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-[var(--dg-blue)]/10 flex items-center justify-center">
<GitBranch className="h-5 w-5 text-[var(--dg-blue)]" />
</div>
<div>
<div className="flex items-center gap-2">
<CardTitle className="text-base text-[var(--dg-text-primary)]">{flow.name}</CardTitle>
{flow.type && (
<Badge
variant="outline"
className="text-xs border-[var(--dg-border)]"
style={{ color: getApprovalTypeColor(flow.type) }}
>
{getApprovalTypeLabel(flow.type)}
</Badge>
)}
</div>
<CardDescription className="text-[var(--dg-text-muted)]">{flow.description}</CardDescription>
</div>
</div>
<div className="flex items-center gap-2">
<Badge
variant="outline"
className={
flow.status === 1 || flow.status === 'active' || flow.status === true
? 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10'
: 'border-[var(--dg-border)] text-[var(--dg-text-secondary)] bg-[var(--dg-bg-tertiary)]'
}
>
{flow.status === 1 || flow.status === 'active' || flow.status === true ? '已启用' : '已停用'}
</Badge>
<Button variant="ghost" size="icon" className="h-8 w-8 text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)]">
<Edit className="h-4 w-4" />
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<div className="bg-[var(--dg-bg-primary)] rounded-lg p-4 overflow-x-auto">
{renderFlowChart(flow)}
</div>
<div className="flex items-center justify-between mt-4 text-sm text-[var(--dg-text-muted)]">
<span>创建于 {flow.created_at ? new Date(flow.created_at).toLocaleDateString() : '--'}</span>
<span>{parseSteps(flow.steps).length} 个节点</span>
</div>
</CardContent>
</Card>
))
)}
</div>
</TabsContent>
{/* 我的审批 Tab */}
<TabsContent value="my-approvals" className="mt-4">
<Card className="bg-slate-900 border-slate-800">
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-8">
<div className="text-center">
<CheckSquare className="h-12 w-12 mx-auto text-slate-500 mb-4" />
<h3 className="text-lg font-medium text-slate-300 mb-2">暂无待审批事项</h3>
<p className="text-sm text-slate-500">当前没有需要您审批的流程</p>
<CheckSquare className="h-12 w-12 mx-auto text-[var(--dg-text-muted)] mb-4" />
<h3 className="text-lg font-medium text-[var(--dg-text-secondary)] mb-2">暂无待审批事项</h3>
<p className="text-sm text-[var(--dg-text-muted)]">当前没有需要您审批的流程</p>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
{/* Instance Detail Dialog */}
<Dialog open={!!selectedInstance} onOpenChange={() => setSelectedInstance(null)}>
<DialogContent className="max-w-2xl bg-slate-900 border-slate-800">
{/* 审批实例详情 Dialog */}
<Dialog open={!!selectedInstance} onOpenChange={(open) => !open && setSelectedInstance(null)}>
<DialogContent className="max-w-2xl bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<DialogHeader>
<DialogTitle className="text-slate-100 flex items-center gap-2">
<GitBranch className="h-5 w-5 text-blue-400" />
<DialogTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
<GitBranch className="h-5 w-5 text-[var(--dg-blue)]" />
{selectedInstance?.title}
</DialogTitle>
<DialogDescription className="text-slate-500">
<DialogDescription className="text-[var(--dg-text-muted)]">
申请人: {selectedInstance?.applicant_name || selectedInstance?.applicant} · 创建于{' '}
{selectedInstance && selectedInstance.created_at ? new Date(selectedInstance.created_at).toLocaleString() : '--'}
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{/* 审批进度条可视化 */}
{selectedInstance && (
<div className="bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<span className="text-sm font-medium text-[var(--dg-text-secondary)]">审批进度</span>
<span className="text-xs text-[var(--dg-text-muted)]">
步骤 {getProgressInfo(selectedInstance).current} / {getProgressInfo(selectedInstance).total}
</span>
</div>
{renderStepProgress(selectedInstance)}
</div>
)}
{/* Approval Timeline */}
<div className="space-y-4">
<h4 className="text-sm font-medium text-slate-300">审批进度</h4>
<h4 className="text-sm font-medium text-[var(--dg-text-secondary)]">审批记录</h4>
<div className="space-y-4">
<div className="flex gap-4">
<div className="flex flex-col items-center">
<div className="w-8 h-8 rounded-full bg-emerald-500/20 text-emerald-400 flex items-center justify-center">
<CheckCircle className="h-4 w-4" />
</div>
<div className="w-0.5 h-full bg-slate-700 mt-2" />
<div className="w-0.5 h-full bg-[var(--dg-border)] mt-2" />
</div>
<div className="flex-1 pb-4">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-slate-200">提交申请</span>
<span className="text-xs text-slate-500">
<span className="text-sm font-medium text-[var(--dg-text-primary)]">提交申请</span>
<span className="text-xs text-[var(--dg-text-muted)]">
{selectedInstance && selectedInstance.created_at ? new Date(selectedInstance.created_at).toLocaleString() : '--'}
</span>
</div>
<p className="text-sm text-slate-400 mt-1">申请人: {selectedInstance?.applicant_name || selectedInstance?.applicant}</p>
<p className="text-sm text-[var(--dg-text-secondary)] mt-1">申请人: {selectedInstance?.applicant_name || selectedInstance?.applicant}</p>
</div>
</div>
@@ -498,19 +699,19 @@ export function ApprovalFlowManager() {
</div>
<div className="flex-1">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-slate-200">{selectedInstance?.current_step || '审批节点'}</span>
<span className="text-xs text-slate-500">
<span className="text-sm font-medium text-[var(--dg-text-primary)]">{selectedInstance?.current_step || '审批节点'}</span>
<span className="text-xs text-[var(--dg-text-muted)]">
{selectedInstance?.completed_at
? new Date(selectedInstance.completed_at).toLocaleString()
: '进行中'}
</span>
</div>
<p className="text-sm text-slate-400 mt-1">
<p className="text-sm text-[var(--dg-text-secondary)] mt-1">
{selectedInstance?.status === 'pending'
? '等待审批人处理'
: selectedInstance?.status === 'approved'
? '审批已通过'
: '审批已拒绝'}
: '审批已驳回'}
</p>
</div>
</div>
@@ -523,15 +724,15 @@ export function ApprovalFlowManager() {
<>
<Button
variant="outline"
className="border-red-700 text-red-400 hover:bg-red-500/10"
onClick={() => handleReject(selectedInstance.id)}
className="border-red-500/30 text-red-400 hover:bg-red-500/10"
onClick={() => openActionDialog(selectedInstance, 'reject')}
>
<XCircle className="h-4 w-4 mr-2" />
拒绝
驳回
</Button>
<Button
className="bg-emerald-600 hover:bg-emerald-700 text-white"
onClick={() => handleApprove(selectedInstance.id)}
onClick={() => openActionDialog(selectedInstance, 'approve')}
>
<CheckCircle className="h-4 w-4 mr-2" />
通过
@@ -541,6 +742,89 @@ export function ApprovalFlowManager() {
</DialogFooter>
</DialogContent>
</Dialog>
{/* 审批操作 Dialog(含审批意见 textarea) */}
<Dialog open={actionDialogOpen} onOpenChange={setActionDialogOpen}>
<DialogContent className="max-w-md bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<DialogHeader>
<DialogTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
{actionType === 'approve' ? (
<>
<CheckCircle className="h-5 w-5 text-emerald-400" />
确认通过审批
</>
) : (
<>
<XCircle className="h-5 w-5 text-red-400" />
确认驳回审批
</>
)}
</DialogTitle>
<DialogDescription className="text-[var(--dg-text-muted)]">
{actionTarget?.title ? `流程:${actionTarget.title}` : '请填写审批意见后确认操作'}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-2">
<div className="space-y-1.5">
<Label className="text-[var(--dg-text-secondary)]">
审批意见 <span className="text-[var(--dg-text-muted)] text-xs">(选填)</span>
</Label>
<Textarea
placeholder={
actionType === 'approve'
? '请输入审批通过意见...'
: '请输入驳回原因...'
}
value={actionComment}
onChange={(e) => setActionComment(e.target.value)}
rows={4}
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)] resize-none"
/>
</div>
{actionType === 'reject' && !actionComment.trim() && (
<p className="text-xs text-amber-400">建议填写驳回原因以便申请人了解</p>
)}
</div>
<DialogFooter className="gap-2">
<Button
variant="outline"
onClick={() => setActionDialogOpen(false)}
className="border-[var(--dg-border)] text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-hover)] hover:text-[var(--dg-text-primary)]"
>
取消
</Button>
{actionType === 'approve' ? (
<Button
className="bg-emerald-600 hover:bg-emerald-700 text-white"
onClick={handleConfirmAction}
disabled={actionSubmitting}
>
{actionSubmitting ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : (
<CheckCircle className="h-4 w-4 mr-2" />
)}
确认通过
</Button>
) : (
<Button
className="bg-red-600 hover:bg-red-700 text-white"
onClick={handleConfirmAction}
disabled={actionSubmitting}
>
{actionSubmitting ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : (
<XCircle className="h-4 w-4 mr-2" />
)}
确认驳回
</Button>
)}
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}