// 审批分类通用页面 — 资源申请/应用申请/权限审批三个子页面共用 import { useState, useEffect, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; import { Label } from '@/components/ui/label'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Plus, Search, RefreshCw, Clock, CheckCircle, XCircle, FileText, Eye, Check, XIcon, User, ChevronRight, Play } from 'lucide-react'; import { approvalApi } from '@/api'; import { toast } from 'sonner'; import { PageShell } from '@/components/layout/PageShell'; import { ContentPanel } from '@/components/layout/ContentPanel'; import { PageEmpty } from '@/components/layout/PageEmpty'; import type { JsonData } from '@/types/common'; export interface FormField { key: string; label: string; type: 'text' | 'number' | 'select' | 'textarea'; options?: string[]; placeholder?: string; defaultValue?: string | number; required?: boolean; } interface Props { category: 'resource' | 'app' | 'permission'; title: string; description: string; fields: FormField[]; templateCode: string; } interface StepItem { id: number; step: number; step_type?: string; approver: number; status: string; comment: string; approved_at: string | null; approver_user?: { username?: string; real_name?: string }; } interface ProcessItem { id: number; title: string; status: string; form_data: string; created_at: string; applicant: number; applicant_user?: { username?: string; real_name?: string }; template?: { name?: string }; current_step?: number; total_steps?: number; steps?: StepItem[]; } const statusBadge = (status: string, t: (k: string) => string) => { switch (status) { case 'approved': return {t('approval.status.approved')}; case 'rejected': return {t('approval.status.rejected')}; default: return {t('approval.status.pending')}; } }; function parseFormData(data: string): Record { try { return JSON.parse(data) || {}; } catch { return {}; } } function getApplicant(p: JsonData): string { const u = p?.applicant_user; return u ? (u.real_name || u.username || `#${p.applicant}`) : (p?.applicant ? `#${p.applicant}` : '--'); } function getCurrentApprover(p: ProcessItem, t: (k: string) => string): string { if (p.status !== 'pending') return '--'; const steps = p.steps || []; if (steps.length === 0) return t('approval.detail.adminPending'); const cs = steps.find(s => s.step === p.current_step && s.status === 'pending'); if (!cs) return t('approval.detail.adminPending'); if (cs.approver_user) return cs.approver_user.real_name || cs.approver_user.username || `#${cs.approver}`; return t('approval.detail.admin'); } export function ApprovalCategoryPage({ category, title, description, fields, templateCode }: Props) { const { t } = useTranslation(); const [processes, setProcesses] = useState([]); const [loading, setLoading] = useState(true); const [keyword, setKeyword] = useState(''); const [statusFilter, setStatusFilter] = useState(''); const [dialogOpen, setDialogOpen] = useState(false); const [submitting, setSubmitting] = useState(false); const [templateId, setTemplateId] = useState(null); const [formData, setFormData] = useState>({}); const [detailItem, setDetailItem] = useState(null); const [acting, setActing] = useState(false); const fetchProcesses = useCallback(async () => { setLoading(true); try { const params: JsonData = { page_size: '100' }; if (category) params.category = category; if (statusFilter) params.status = statusFilter; const resp = (await approvalApi.getProcesses(params)) as JsonData; setProcesses(resp?.list ?? resp?.data?.list ?? []); } catch { setProcesses([]); } finally { setLoading(false); } }, [category, statusFilter]); const fetchTemplateId = useCallback(async () => { try { const resp = (await approvalApi.getTemplates({ type: templateCode })) as JsonData; const list = resp?.list ?? resp?.data?.list ?? []; if (list.length > 0) setTemplateId(list[0].id); } catch { // 模板缺失时保留空状态,由创建流程提示用户。// Logan.Li } }, [templateCode]); useEffect(() => { fetchProcesses(); }, [fetchProcesses]); useEffect(() => { fetchTemplateId(); }, [fetchTemplateId]); const handleCreate = async () => { if (!templateId) { toast.error(t('approval.msg.templateNotLoaded')); return; } for (const f of fields) { if (f.required && !formData[f.key]) { toast.error(t('approval.msg.pleaseFill', { label: f.label })); return; } } setSubmitting(true); try { const firstVal = formData[fields[0]?.key] || ''; const secondVal = fields[1] ? formData[fields[1].key] || '' : ''; await approvalApi.createProcess({ template_id: templateId, title: `${title} - ${firstVal}${secondVal ? ' / ' + secondVal : ''}`, form_data: JSON.stringify(formData), }); toast.success(t('approval.msg.submitSuccess')); setDialogOpen(false); setFormData({}); fetchProcesses(); } catch (err) { toast.error(err?.message || t('approval.msg.submitFailed')); } finally { setSubmitting(false); } }; const openCreateDialog = () => { const init: Record = {}; fields.forEach((f) => { init[f.key] = f.defaultValue?.toString() || ''; }); setFormData(init); setDialogOpen(true); }; const handleApprove = async (id: number) => { setActing(true); try { await approvalApi.approveProcess(id); toast.success(t('approval.msg.approveSuccess')); fetchProcesses(); } catch (err) { toast.error(err?.response?.data?.message || t('approval.msg.approveFailed')); } finally { setActing(false); } }; const handleReject = async (id: number) => { setActing(true); try { await approvalApi.rejectProcess(id, t('approval.flow.rejectTitle')); toast.success(t('approval.msg.rejectSuccess')); fetchProcesses(); } catch (err) { toast.error(err?.response?.data?.message || t('common.operationFailed')); } finally { setActing(false); } }; const openDetail = async (p: ProcessItem) => { // Load full detail including steps try { const resp: JsonData = await approvalApi.getProcesses({ page_size: 200 } as JsonData); const list = resp?.list ?? resp?.data?.list ?? []; const full = list.find((x: JsonData) => x.id === p.id); setDetailItem(full || p); } catch { setDetailItem(p); } }; const filtered = processes.filter((p) => !keyword || (p.title || '').toLowerCase().includes(keyword.toLowerCase())); // 工具栏:搜索 + 状态筛选 // Logan.Li const toolbar = (
setKeyword(e.target.value)} className="pl-9 h-9 dg-input" />
); return ( <> } toolbar={toolbar} loading={loading} actions={( )} > {filtered.length === 0 ? ( } title={t('approval.noRecords')} action={( )} /> ) : ( {t('approval.col.submitTime')} {t('approval.col.submitAccount')} {t('approval.col.currentApprover')} {t('approval.col.status')} {t('approval.col.actions')} {filtered.map((p) => ( {p.created_at ? new Date(p.created_at).toLocaleString('zh-CN') : '--'} {getApplicant(p)} {getCurrentApprover(p, t)} {statusBadge(p.status, t)}
{p.status === 'pending' ? ( <> ) : ( )}
))}
)}
{/* Create Dialog */} {t('approval.newRequest')} {title} {description}
{fields.map((field) => (
{field.type === 'select' ? ( ) : field.type === 'textarea' ? (