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:
1 parent
170818c141
commit
00d5510366
13 files changed
+2170
-496
No files matched your search
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user