- Cache 接口新增 SetNX 方法(go-redis 原子 / redka check-then-set 单副本安全) - AlertEngine 改造为支持分布式锁: tryAcquireLock/releaseLock/GetEngineStatus - 多副本部署时仅 Leader 执行告警检查, 60s TTL 防死锁, fail-open 容错 - 新增 GET /alerts/engine/status 监控端点(is_leader/instance_id/lock_holder) - 修复 GrafanaDashboard.tsx Panel.options 类型不匹配导致构建中断 - 同步未提交的前端类型安全迁移(140+ 文件) - ROADMAP/TDD_PLAN/PROJECT_EVALUATION 三文档同步更新 验证: go build 零错误, Docker 部署 healthy, 引擎状态端点正常, 锁竞争测试通过(非 Leader 跳过检查), 5/5 核心 API 回归通过
142 lines
5.8 KiB
TypeScript
142 lines
5.8 KiB
TypeScript
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { CheckCircle2, XCircle, Clock } from 'lucide-react';
|
|
import {
|
|
renderFormData,
|
|
renderStepProgress,
|
|
getApprovalTypeLabel,
|
|
getApprovalTypeColor,
|
|
getInstanceType,
|
|
} from './helpers';
|
|
import type { JsonData } from '@/types/common';
|
|
|
|
interface ApprovalDetailDialogProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
instance?: JsonData;
|
|
templates: JsonData[];
|
|
onApprove: (instance: JsonData) => void;
|
|
onReject: (instance: JsonData) => void;
|
|
}
|
|
|
|
// 审批详情弹窗 — 展示流程详情/表单/进度/操作记录 // Logan.Li
|
|
export function ApprovalDetailDialog({
|
|
open,
|
|
onOpenChange,
|
|
instance,
|
|
templates,
|
|
onApprove,
|
|
onReject,
|
|
}: ApprovalDetailDialogProps) {
|
|
if (!instance) return null;
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="max-w-2xl bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] max-h-[90vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-[var(--dg-text-primary)]">审批详情</DialogTitle>
|
|
<DialogDescription className="text-[var(--dg-text-muted)]">查看审批流程详细信息</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="space-y-4 py-2">
|
|
<div>
|
|
<h3 className="text-lg font-medium text-[var(--dg-text-primary)]">{instance.title}</h3>
|
|
<div className="flex items-center gap-2 mt-1">
|
|
<Badge variant="outline" className="text-xs border-[var(--dg-border)]" style={{ color: getApprovalTypeColor(getInstanceType(instance)) }}>
|
|
{getApprovalTypeLabel(getInstanceType(instance))}
|
|
</Badge>
|
|
<span className="text-xs text-[var(--dg-text-muted)]">
|
|
申请人:{instance.applicant_name || instance.applicant || '--'}
|
|
</span>
|
|
<span className="text-xs text-[var(--dg-text-muted)]">
|
|
提交于 {instance.created_at ? new Date(instance.created_at).toLocaleString() : '--'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 审批内容 */}
|
|
{instance.content && (
|
|
<div className="bg-[var(--dg-bg-primary)] rounded-lg p-4">
|
|
<p className="text-sm text-[var(--dg-text-secondary)] whitespace-pre-wrap">{instance.content}</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* 申请表单 */}
|
|
{(() => {
|
|
const tpl = templates.find((t) => t.id === instance.template_id);
|
|
const rendered = renderFormData(instance.form_data, tpl);
|
|
if (!rendered) return null;
|
|
return (
|
|
<div className="bg-[var(--dg-bg-primary)] rounded-lg p-4">
|
|
<p className="text-sm font-medium text-[var(--dg-text-secondary)] mb-3">申请表单</p>
|
|
{rendered}
|
|
</div>
|
|
);
|
|
})()}
|
|
|
|
{/* 审批进度 */}
|
|
<div className="bg-[var(--dg-bg-primary)] rounded-lg p-4">
|
|
<p className="text-sm font-medium text-[var(--dg-text-secondary)] mb-3">审批进度</p>
|
|
{renderStepProgress(instance)}
|
|
</div>
|
|
|
|
{/* 操作记录 */}
|
|
{instance.actions && (
|
|
<div className="bg-[var(--dg-bg-primary)] rounded-lg p-4">
|
|
<p className="text-sm font-medium text-[var(--dg-text-secondary)] mb-3">操作记录</p>
|
|
<div className="space-y-2">
|
|
{instance.actions.map((action: JsonData, index: number) => (
|
|
<div key={index} className="flex items-center gap-3 text-sm">
|
|
{action.action === 'approve' ? (
|
|
<CheckCircle2 className="h-4 w-4 text-emerald-400" />
|
|
) : action.action === 'reject' ? (
|
|
<XCircle className="h-4 w-4 text-red-400" />
|
|
) : (
|
|
<Clock className="h-4 w-4 text-[var(--dg-text-muted)]" />
|
|
)}
|
|
<div>
|
|
<span className="text-[var(--dg-text-secondary)]">
|
|
{action.operator_name || action.operator}
|
|
</span>
|
|
<span className="text-[var(--dg-text-muted)] mx-1">·</span>
|
|
<span className="text-[var(--dg-text-secondary)]">
|
|
{action.action === 'approve' ? '通过' : action.action === 'reject' ? '拒绝' : '提交'}
|
|
</span>
|
|
{action.comment && (
|
|
<p className="text-xs text-[var(--dg-text-muted)] mt-0.5">{action.comment}</p>
|
|
)}
|
|
</div>
|
|
<span className="text-xs text-[var(--dg-text-muted)] ml-auto">
|
|
{action.created_at ? new Date(action.created_at).toLocaleString() : ''}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<DialogFooter>
|
|
{instance.status === 'pending' && (
|
|
<>
|
|
<Button variant="outline" className="border-red-500/30 text-red-400 hover:bg-red-500/10" onClick={() => { onOpenChange(false); onReject(instance); }}>
|
|
<XCircle className="h-4 w-4 mr-2" />拒绝
|
|
</Button>
|
|
<Button className="bg-emerald-500/90 hover:bg-emerald-500 text-white" onClick={() => { onOpenChange(false); onApprove(instance); }}>
|
|
<CheckCircle2 className="h-4 w-4 mr-2" />通过
|
|
</Button>
|
|
</>
|
|
)}
|
|
<Button variant="ghost" onClick={() => onOpenChange(false)}>关闭</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|