feat: 定时任务编辑入口全栈打通 + 修复ApprovalFlow编译错误 + 完成大文件拆分重构
- CronJobManager: 编辑按钮接通 onClick → openEdit(job),弹出编辑对话框预填数据,
复用新建/编辑共用 Dialog(editingId 区分模式),标题/按钮文案动态切换,
保存时根据 editingId 调用 createJob 或 updateJob — 消除占位按钮
- ApprovalFlow: 修复 API 方法名不匹配导致 tsc -b 编译失败
(getInstances→getProcesses, getMyApprovals→getProcesses({status:'pending'}),
approveInstance→approveProcess, rejectInstance→rejectProcess)
+ 添加 ApprovalFlowManager 导出别名兼容 App.tsx 导入
- 合并已完成的大文件拆分重构: automation.go→automation_cron/script/playbook,
ldap.go→ldap_ou/ldap_users, server.go→cloud_accounts/domain/platform/region,
user.go→user_admin; 前端 Dashboard/K8s/Server/Approval/Monitor 子组件拆分
// Logan.Li
This commit is contained in:
1 parent
8be63ac168
commit
a7e2351ad2
37 files changed
+7374
-4891
No files matched your search
@@ -0,0 +1,304 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Search,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
Eye,
|
||||
Loader2,
|
||||
GitCommit,
|
||||
Server,
|
||||
KeyRound,
|
||||
FileText,
|
||||
} from 'lucide-react';
|
||||
|
||||
function parseSteps(steps: any) {
|
||||
if (Array.isArray(steps)) return steps;
|
||||
if (typeof steps === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(steps);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// 审批类型字典
|
||||
const APPROVAL_TYPE_OPTIONS = [
|
||||
{ value: 'deploy', label: '发布审批', icon: GitCommit, color: 'var(--dg-blue)' },
|
||||
{ value: 'server', label: '服务器操作', icon: Server, color: 'var(--dg-cyan)' },
|
||||
{ value: 'permission', label: '权限申请', icon: KeyRound, 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 getApprovalTypeIcon = (value: string): React.ElementType =>
|
||||
APPROVAL_TYPE_OPTIONS.find((o) => o.value === value)?.icon || FileText;
|
||||
|
||||
// 状态标签页定义
|
||||
const STATUS_TABS = [
|
||||
{ key: 'all', label: '全部', value: '' },
|
||||
{ key: 'pending', label: '待审批', value: 'pending' },
|
||||
{ key: 'approved', label: '已通过', value: 'approved' },
|
||||
{ key: 'rejected', label: '已拒绝', value: 'rejected' },
|
||||
{ key: 'processing', label: '进行中', value: 'processing' },
|
||||
{ key: 'completed', label: '已完成', value: 'completed' },
|
||||
];
|
||||
|
||||
function getStatusBadge(status: string) {
|
||||
const statusMap: Record<string, { label: string; className: string; dot: string }> = {
|
||||
pending: {
|
||||
label: '待审批',
|
||||
className: 'border-amber-500/30 text-amber-400 bg-amber-500/10',
|
||||
dot: 'bg-amber-400',
|
||||
},
|
||||
approved: {
|
||||
label: '已通过',
|
||||
className: 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10',
|
||||
dot: 'bg-emerald-400',
|
||||
},
|
||||
rejected: {
|
||||
label: '已拒绝',
|
||||
className: 'border-red-500/30 text-red-400 bg-red-500/10',
|
||||
dot: 'bg-red-400',
|
||||
},
|
||||
processing: {
|
||||
label: '进行中',
|
||||
className: 'border-[var(--dg-blue)]/30 text-[var(--dg-blue)] bg-[var(--dg-blue)]/10',
|
||||
dot: 'bg-[var(--dg-blue)]',
|
||||
},
|
||||
completed: {
|
||||
label: '已完成',
|
||||
className: 'border-[var(--dg-border)] text-[var(--dg-text-secondary)] bg-[var(--dg-bg-tertiary)]',
|
||||
dot: 'bg-[var(--dg-text-secondary)]',
|
||||
},
|
||||
};
|
||||
|
||||
const config = statusMap[status] || statusMap.pending;
|
||||
return (
|
||||
<Badge variant="outline" className={config.className}>
|
||||
<div className={cn('w-1.5 h-1.5 rounded-full mr-1.5', config.dot)} />
|
||||
{config.label}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
// 获取进度信息
|
||||
function getProgressInfo(instance: any) {
|
||||
const steps = parseSteps(instance.steps);
|
||||
const currentIndex = instance.current_step ?? 0;
|
||||
const total = steps.length;
|
||||
const completed = steps.filter((s: any) => s.status === 'approved' || s.status === 'completed').length;
|
||||
const percentage = total > 0 ? Math.round((completed / total) * 100) : 0;
|
||||
return { steps, currentIndex, total, completed, percentage };
|
||||
}
|
||||
|
||||
// 渲染审批进度
|
||||
function renderStepProgress(instance: any) {
|
||||
const { steps, currentIndex } = getProgressInfo(instance);
|
||||
if (steps.length === 0) return null;
|
||||
return (
|
||||
<div className="flex items-center gap-1 overflow-x-auto">
|
||||
{steps.map((step: any, index: number) => {
|
||||
const isCompleted = step.status === 'approved' || step.status === 'completed';
|
||||
const isCurrent = index === currentIndex;
|
||||
const isRejected = step.status === 'rejected';
|
||||
return (
|
||||
<div key={step.id || index} className="flex items-center">
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs whitespace-nowrap',
|
||||
isCompleted && 'bg-emerald-500/10 text-emerald-400',
|
||||
isCurrent && !isRejected && 'bg-amber-500/10 text-amber-400 border border-amber-500/30',
|
||||
isRejected && 'bg-red-500/10 text-red-400',
|
||||
!isCompleted && !isCurrent && !isRejected && 'bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-muted)]'
|
||||
)}
|
||||
>
|
||||
{isCompleted && <CheckCircle2 className="h-3 w-3" />}
|
||||
{isRejected && <XCircle className="h-3 w-3" />}
|
||||
{isCurrent && !isRejected && (
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
)}
|
||||
<span>{step.name || step.approver_name || `步骤 ${index + 1}`}</span>
|
||||
</div>
|
||||
{index < steps.length - 1 && (
|
||||
<div className={cn('w-4 h-px', isCompleted ? 'bg-emerald-500/30' : 'bg-[var(--dg-border)]')} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ApprovalProcessTabProps {
|
||||
processes: any[];
|
||||
filteredInstances: any[];
|
||||
loading: boolean;
|
||||
searchQuery: string;
|
||||
setSearchQuery: (v: string) => void;
|
||||
statusFilter: string;
|
||||
setStatusFilter: (v: string) => void;
|
||||
onView: (instance: any) => void;
|
||||
onApprove: (instance: any) => void;
|
||||
onReject: (instance: any) => void;
|
||||
}
|
||||
|
||||
export function ApprovalProcessTab({
|
||||
filteredInstances,
|
||||
loading,
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
statusFilter,
|
||||
setStatusFilter,
|
||||
onView,
|
||||
onApprove,
|
||||
onReject,
|
||||
}: ApprovalProcessTabProps) {
|
||||
return (
|
||||
<>
|
||||
{/* 搜索 + 状态筛选 */}
|
||||
<div className="flex items-center justify-between flex-wrap gap-4">
|
||||
<div className="relative w-72">
|
||||
<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-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
|
||||
/>
|
||||
</div>
|
||||
<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) => (
|
||||
<button
|
||||
key={tab.key}
|
||||
onClick={() => setStatusFilter(tab.value)}
|
||||
className={cn(
|
||||
'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}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 流程实例列表 */}
|
||||
{loading ? (
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-12">
|
||||
<div className="flex items-center justify-center text-[var(--dg-text-muted)]">
|
||||
<Loader2 className="h-6 w-6 animate-spin mr-2" />
|
||||
加载中...
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : filteredInstances.length === 0 ? (
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-12">
|
||||
<div className="text-center">
|
||||
<FileText 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>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{filteredInstances.map((instance) => {
|
||||
const Icon = getApprovalTypeIcon(instance.type);
|
||||
const { percentage } = getProgressInfo(instance);
|
||||
return (
|
||||
<Card key={instance.id} className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] hover:border-[var(--dg-border)] transition-colors">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-[var(--dg-bg-tertiary)] flex items-center justify-center" style={{ color: getApprovalTypeColor(instance.type) }}>
|
||||
<Icon className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="font-medium text-[var(--dg-text-primary)]">{instance.title}</h3>
|
||||
{getStatusBadge(instance.status)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-xs text-[var(--dg-text-muted)]">
|
||||
<span className="flex items-center gap-1">
|
||||
<Badge variant="outline" className="text-xs border-[var(--dg-border)]" style={{ color: getApprovalTypeColor(instance.type) }}>
|
||||
{getApprovalTypeLabel(instance.type)}
|
||||
</Badge>
|
||||
</span>
|
||||
<span>申请人:{instance.applicant_name || instance.applicant || '--'}</span>
|
||||
<span>提交于 {instance.created_at ? new Date(instance.created_at).toLocaleString() : '--'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="ghost" size="sm" className="text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)]" onClick={() => onView(instance)}>
|
||||
<Eye className="h-4 w-4 mr-1" />
|
||||
详情
|
||||
</Button>
|
||||
{instance.status === 'pending' && (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-emerald-500/30 text-emerald-400 hover:bg-emerald-500/10"
|
||||
onClick={() => onApprove(instance)}
|
||||
>
|
||||
<CheckCircle2 className="h-4 w-4 mr-1" />
|
||||
通过
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-red-500/30 text-red-400 hover:bg-red-500/10"
|
||||
onClick={() => onReject(instance)}
|
||||
>
|
||||
<XCircle className="h-4 w-4 mr-1" />
|
||||
拒绝
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* 审批进度条 */}
|
||||
<div className="mb-3">
|
||||
<div className="flex items-center justify-between text-xs text-[var(--dg-text-muted)] mb-1.5">
|
||||
<span>审批进度</span>
|
||||
<span>{percentage}%</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' : 'bg-[var(--dg-blue)]'
|
||||
)}
|
||||
style={{ width: `${percentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* 步骤进度 */}
|
||||
<div className="bg-[var(--dg-bg-primary)] rounded-lg p-3">
|
||||
{renderStepProgress(instance)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user