refactor: 拆分ApprovalFlow.tsx(666→412行)提取ApprovalDetailDialog+ApprovalCreateDialog子模块
This commit is contained in:
1 parent
8da6ca15db
commit
d76c23b875
3 files changed
+376
-278
No files matched your search
@@ -0,0 +1,212 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { Loader2 } from 'lucide-react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { approvalApi } from '@/services/api';
|
||||||
|
import { parseFields, APPROVAL_TYPE_OPTIONS } from './helpers';
|
||||||
|
|
||||||
|
interface ApprovalCreateDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
templates: any[];
|
||||||
|
onSuccess: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新建资源申请弹窗 — 选择申请类型 + 动态表单填写,状态自包含 // Logan.Li
|
||||||
|
export function ApprovalCreateDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
templates,
|
||||||
|
onSuccess,
|
||||||
|
}: ApprovalCreateDialogProps) {
|
||||||
|
const [selectedTemplate, setSelectedTemplate] = useState<any>(null);
|
||||||
|
const [formData, setFormData] = useState<Record<string, any>>({});
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// 提交新建申请 // Logan.Li
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!selectedTemplate) {
|
||||||
|
toast.error('请先选择申请类型');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const fields = parseFields(selectedTemplate.form_schema);
|
||||||
|
for (const f of fields) {
|
||||||
|
if (f.required && !formData[f.key]) {
|
||||||
|
toast.error(`请填写${f.label}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setSubmitting(true);
|
||||||
|
try {
|
||||||
|
await approvalApi.createProcess({
|
||||||
|
template_id: selectedTemplate.id,
|
||||||
|
title: formData.app_name || selectedTemplate.name,
|
||||||
|
form_data: JSON.stringify(formData),
|
||||||
|
});
|
||||||
|
toast.success('申请已提交');
|
||||||
|
onOpenChange(false);
|
||||||
|
setSelectedTemplate(null);
|
||||||
|
setFormData({});
|
||||||
|
onSuccess();
|
||||||
|
} catch (e: any) {
|
||||||
|
toast.error('提交失败', { description: e?.message });
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
onOpenChange={(v) => {
|
||||||
|
// 关闭弹窗时重置内部状态 // Logan.Li
|
||||||
|
if (!v) {
|
||||||
|
setSelectedTemplate(null);
|
||||||
|
setFormData({});
|
||||||
|
}
|
||||||
|
onOpenChange(v);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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)]">
|
||||||
|
{selectedTemplate ? '请填写申请信息' : '请选择申请类型'}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{!selectedTemplate ? (
|
||||||
|
/* 步骤一:选择申请类型 */
|
||||||
|
<div className="grid grid-cols-2 gap-4 py-2">
|
||||||
|
{APPROVAL_TYPE_OPTIONS.map((opt) => {
|
||||||
|
const tpl = templates.find(
|
||||||
|
(t) => t.type === opt.value && (t.enabled !== false)
|
||||||
|
);
|
||||||
|
const Icon = opt.icon;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={opt.value}
|
||||||
|
type="button"
|
||||||
|
disabled={!tpl}
|
||||||
|
onClick={() => { setSelectedTemplate(tpl); setFormData({}); }}
|
||||||
|
className={cn(
|
||||||
|
'flex flex-col items-start gap-3 p-5 rounded-xl text-left transition-colors',
|
||||||
|
'bg-[var(--dg-bg-tertiary)] hover:bg-[var(--dg-bg-hover)]',
|
||||||
|
!tpl && 'opacity-40 cursor-not-allowed hover:bg-[var(--dg-bg-tertiary)]'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="w-10 h-10 rounded-lg flex items-center justify-center"
|
||||||
|
style={{ backgroundColor: `color-mix(in srgb, ${opt.color} 15%, transparent)`, color: opt.color }}
|
||||||
|
>
|
||||||
|
<Icon className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-[var(--dg-text-primary)]">{opt.label}</p>
|
||||||
|
<p className="text-xs text-[var(--dg-text-muted)] mt-0.5">
|
||||||
|
{tpl ? tpl.description || `共 ${parseFields(tpl.form_schema).length} 个字段` : '暂无可用模板'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
/* 步骤二:动态表单 */
|
||||||
|
<div className="py-2">
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => { setSelectedTemplate(null); setFormData({}); }}
|
||||||
|
className="text-xs text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)]"
|
||||||
|
>
|
||||||
|
← 返回选择类型
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-x-4 gap-y-4">
|
||||||
|
{parseFields(selectedTemplate.form_schema).map((field: any) => {
|
||||||
|
const isTextarea = field.type === 'textarea';
|
||||||
|
return (
|
||||||
|
<div key={field.key} className={cn('space-y-1.5', isTextarea && 'col-span-2')}>
|
||||||
|
<Label className="text-sm font-medium text-[var(--dg-text-secondary)]">
|
||||||
|
{field.label}
|
||||||
|
{field.required && <span className="text-red-400 ml-0.5">*</span>}
|
||||||
|
</Label>
|
||||||
|
{field.type === 'select' ? (
|
||||||
|
<Select
|
||||||
|
value={String(formData[field.key] ?? '')}
|
||||||
|
onValueChange={(val) => setFormData((prev) => ({ ...prev, [field.key]: val }))}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="bg-[var(--dg-bg-primary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
|
||||||
|
<SelectValue placeholder={field.placeholder || `请选择${field.label}`} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||||
|
{(field.options || []).map((opt: string) => (
|
||||||
|
<SelectItem key={opt} value={opt} className="text-[var(--dg-text-primary)] focus:bg-[var(--dg-bg-hover)]">
|
||||||
|
{opt}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
) : field.type === 'textarea' ? (
|
||||||
|
<Textarea
|
||||||
|
placeholder={field.placeholder || `请输入${field.label}`}
|
||||||
|
value={formData[field.key] ?? ''}
|
||||||
|
onChange={(e) => setFormData((prev) => ({ ...prev, [field.key]: e.target.value }))}
|
||||||
|
className="bg-[var(--dg-bg-primary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)] min-h-[80px]"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
type={field.type === 'number' ? 'number' : 'text'}
|
||||||
|
placeholder={field.placeholder || `请输入${field.label}`}
|
||||||
|
value={formData[field.key] ?? ''}
|
||||||
|
onChange={(e) => setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[field.key]: e.target.value,
|
||||||
|
}))}
|
||||||
|
className="bg-[var(--dg-bg-primary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="ghost" onClick={() => onOpenChange(false)}>取消</Button>
|
||||||
|
{selectedTemplate && (
|
||||||
|
<Button
|
||||||
|
className="bg-[var(--dg-blue)] hover:opacity-90 text-white"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={submitting}
|
||||||
|
>
|
||||||
|
{submitting && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
||||||
|
提交申请
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
interface ApprovalDetailDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
instance: any;
|
||||||
|
templates: any[];
|
||||||
|
onApprove: (instance: any) => void;
|
||||||
|
onReject: (instance: any) => 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: any, 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,18 +1,9 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Input } from '@/components/ui/input';
|
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { Label } from '@/components/ui/label';
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@/components/ui/select';
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -30,23 +21,20 @@ import {
|
|||||||
Plus,
|
Plus,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useConfirm } from '@/components/ui/confirm-dialog';
|
|
||||||
import { approvalApi } from '@/services/api';
|
import { approvalApi } from '@/services/api';
|
||||||
import { ApprovalTemplateTab } from './ApprovalTemplateTab';
|
|
||||||
import { ApprovalProcessTab } from './ApprovalProcessTab';
|
import { ApprovalProcessTab } from './ApprovalProcessTab';
|
||||||
|
import { ApprovalTemplateTab } from './ApprovalTemplateTab';
|
||||||
|
import { ApprovalDetailDialog } from './ApprovalDetailDialog';
|
||||||
|
import { ApprovalCreateDialog } from './ApprovalCreateDialog';
|
||||||
import {
|
import {
|
||||||
parseFields,
|
|
||||||
renderFormData,
|
|
||||||
renderStepProgress,
|
renderStepProgress,
|
||||||
getProgressInfo,
|
getProgressInfo,
|
||||||
getApprovalTypeLabel,
|
getApprovalTypeLabel,
|
||||||
getApprovalTypeColor,
|
getApprovalTypeColor,
|
||||||
getInstanceType,
|
getInstanceType,
|
||||||
APPROVAL_TYPE_OPTIONS,
|
|
||||||
} from './helpers';
|
} from './helpers';
|
||||||
|
|
||||||
export function ApprovalFlow() {
|
export function ApprovalFlow() {
|
||||||
const confirm = useConfirm();
|
|
||||||
const [activeTab, setActiveTab] = useState<'instances' | 'flows' | 'my-approvals'>('instances');
|
const [activeTab, setActiveTab] = useState<'instances' | 'flows' | 'my-approvals'>('instances');
|
||||||
|
|
||||||
const [processes, setProcesses] = useState<any[]>([]);
|
const [processes, setProcesses] = useState<any[]>([]);
|
||||||
@@ -70,9 +58,6 @@ export function ApprovalFlow() {
|
|||||||
|
|
||||||
// 新建申请弹窗
|
// 新建申请弹窗
|
||||||
const [createOpen, setCreateOpen] = useState(false);
|
const [createOpen, setCreateOpen] = useState(false);
|
||||||
const [selectedTemplate, setSelectedTemplate] = useState<any>(null);
|
|
||||||
const [formData, setFormData] = useState<Record<string, any>>({});
|
|
||||||
const [submitting, setSubmitting] = useState(false);
|
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -109,13 +94,13 @@ export function ApprovalFlow() {
|
|||||||
return templateTypeFilter === 'all' || template.type === templateTypeFilter;
|
return templateTypeFilter === 'all' || template.type === templateTypeFilter;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 打开详情弹窗
|
// 打开详情弹窗 // Logan.Li
|
||||||
const handleView = (instance: any) => {
|
const handleView = (instance: any) => {
|
||||||
setSelectedInstance(instance);
|
setSelectedInstance(instance);
|
||||||
setDetailOpen(true);
|
setDetailOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 打开审批操作弹窗
|
// 打开审批操作弹窗 // Logan.Li
|
||||||
const openActionDialog = (type: 'approve' | 'reject', instance: any) => {
|
const openActionDialog = (type: 'approve' | 'reject', instance: any) => {
|
||||||
setActionType(type);
|
setActionType(type);
|
||||||
setSelectedInstance(instance);
|
setSelectedInstance(instance);
|
||||||
@@ -123,7 +108,7 @@ export function ApprovalFlow() {
|
|||||||
setActionDialogOpen(true);
|
setActionDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 确认审批操作
|
// 确认审批操作 // Logan.Li
|
||||||
const handleConfirmAction = async () => {
|
const handleConfirmAction = async () => {
|
||||||
if (!selectedInstance) return;
|
if (!selectedInstance) return;
|
||||||
try {
|
try {
|
||||||
@@ -144,36 +129,6 @@ export function ApprovalFlow() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 提交新建申请
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
if (!selectedTemplate) {
|
|
||||||
toast.error('请先选择申请类型');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const fields = parseFields(selectedTemplate.form_schema);
|
|
||||||
for (const f of fields) {
|
|
||||||
if (f.required && !formData[f.key]) {
|
|
||||||
toast.error(`请填写${f.label}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setSubmitting(true);
|
|
||||||
try {
|
|
||||||
await approvalApi.createProcess({
|
|
||||||
template_id: selectedTemplate.id,
|
|
||||||
title: formData.app_name || selectedTemplate.name,
|
|
||||||
form_data: JSON.stringify(formData),
|
|
||||||
});
|
|
||||||
toast.success('申请已提交');
|
|
||||||
setCreateOpen(false);
|
|
||||||
loadData();
|
|
||||||
} catch (e: any) {
|
|
||||||
toast.error('提交失败', { description: e?.message });
|
|
||||||
} finally {
|
|
||||||
setSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 统计数据
|
// 统计数据
|
||||||
const pendingCount = processes.filter((p) => p.status === 'pending').length;
|
const pendingCount = processes.filter((p) => p.status === 'pending').length;
|
||||||
const approvedCount = processes.filter((p) => p.status === 'approved').length;
|
const approvedCount = processes.filter((p) => p.status === 'approved').length;
|
||||||
@@ -190,7 +145,7 @@ export function ApprovalFlow() {
|
|||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
className="bg-[var(--dg-blue)] hover:opacity-90 text-white"
|
className="bg-[var(--dg-blue)] hover:opacity-90 text-white"
|
||||||
onClick={() => { setSelectedTemplate(null); setFormData({}); setCreateOpen(true); }}
|
onClick={() => setCreateOpen(true)}
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
新建申请
|
新建申请
|
||||||
@@ -377,107 +332,15 @@ export function ApprovalFlow() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 详情弹窗 */}
|
{/* 详情弹窗 — 提取到 ApprovalDetailDialog */}
|
||||||
<Dialog open={detailOpen} onOpenChange={setDetailOpen}>
|
<ApprovalDetailDialog
|
||||||
<DialogContent className="max-w-2xl bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] max-h-[90vh] overflow-y-auto">
|
open={detailOpen}
|
||||||
<DialogHeader>
|
onOpenChange={setDetailOpen}
|
||||||
<DialogTitle className="text-[var(--dg-text-primary)]">审批详情</DialogTitle>
|
instance={selectedInstance}
|
||||||
<DialogDescription className="text-[var(--dg-text-muted)]">查看审批流程详细信息</DialogDescription>
|
templates={templates}
|
||||||
</DialogHeader>
|
onApprove={(instance) => openActionDialog('approve', instance)}
|
||||||
{selectedInstance && (
|
onReject={(instance) => openActionDialog('reject', instance)}
|
||||||
<div className="space-y-4 py-2">
|
/>
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-medium text-[var(--dg-text-primary)]">{selectedInstance.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(selectedInstance)) }}>
|
|
||||||
{getApprovalTypeLabel(getInstanceType(selectedInstance))}
|
|
||||||
</Badge>
|
|
||||||
<span className="text-xs text-[var(--dg-text-muted)]">
|
|
||||||
申请人:{selectedInstance.applicant_name || selectedInstance.applicant || '--'}
|
|
||||||
</span>
|
|
||||||
<span className="text-xs text-[var(--dg-text-muted)]">
|
|
||||||
提交于 {selectedInstance.created_at ? new Date(selectedInstance.created_at).toLocaleString() : '--'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 审批内容 */}
|
|
||||||
{selectedInstance.content && (
|
|
||||||
<div className="bg-[var(--dg-bg-primary)] rounded-lg p-4">
|
|
||||||
<p className="text-sm text-[var(--dg-text-secondary)] whitespace-pre-wrap">{selectedInstance.content}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 申请表单 */}
|
|
||||||
{(() => {
|
|
||||||
const tpl = templates.find((t) => t.id === selectedInstance.template_id);
|
|
||||||
const rendered = renderFormData(selectedInstance.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(selectedInstance)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 操作记录 */}
|
|
||||||
{selectedInstance.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">
|
|
||||||
{selectedInstance.actions.map((action: any, 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>
|
|
||||||
{selectedInstance?.status === 'pending' && (
|
|
||||||
<>
|
|
||||||
<Button variant="outline" className="border-red-500/30 text-red-400 hover:bg-red-500/10" onClick={() => { setDetailOpen(false); openActionDialog('reject', selectedInstance); }}>
|
|
||||||
<XCircle className="h-4 w-4 mr-2" />拒绝
|
|
||||||
</Button>
|
|
||||||
<Button className="bg-emerald-500/90 hover:bg-emerald-500 text-white" onClick={() => { setDetailOpen(false); openActionDialog('approve', selectedInstance); }}>
|
|
||||||
<CheckCircle2 className="h-4 w-4 mr-2" />通过
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Button variant="ghost" onClick={() => setDetailOpen(false)}>关闭</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
{/* 审批操作弹窗 */}
|
{/* 审批操作弹窗 */}
|
||||||
<Dialog open={actionDialogOpen} onOpenChange={setActionDialogOpen}>
|
<Dialog open={actionDialogOpen} onOpenChange={setActionDialogOpen}>
|
||||||
@@ -534,130 +397,13 @@ export function ApprovalFlow() {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* 新建申请弹窗 */}
|
{/* 新建申请弹窗 — 提取到 ApprovalCreateDialog,状态自包含 */}
|
||||||
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
|
<ApprovalCreateDialog
|
||||||
<DialogContent className="max-w-2xl bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] max-h-[90vh] overflow-y-auto">
|
open={createOpen}
|
||||||
<DialogHeader>
|
onOpenChange={setCreateOpen}
|
||||||
<DialogTitle className="text-[var(--dg-text-primary)]">新建资源申请</DialogTitle>
|
templates={templates}
|
||||||
<DialogDescription className="text-[var(--dg-text-muted)]">
|
onSuccess={loadData}
|
||||||
{selectedTemplate ? '请填写申请信息' : '请选择申请类型'}
|
/>
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
{!selectedTemplate ? (
|
|
||||||
/* 步骤一:选择申请类型 */
|
|
||||||
<div className="grid grid-cols-2 gap-4 py-2">
|
|
||||||
{APPROVAL_TYPE_OPTIONS.map((opt) => {
|
|
||||||
const tpl = templates.find(
|
|
||||||
(t) => t.type === opt.value && (t.enabled !== false)
|
|
||||||
);
|
|
||||||
const Icon = opt.icon;
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={opt.value}
|
|
||||||
type="button"
|
|
||||||
disabled={!tpl}
|
|
||||||
onClick={() => { setSelectedTemplate(tpl); setFormData({}); }}
|
|
||||||
className={cn(
|
|
||||||
'flex flex-col items-start gap-3 p-5 rounded-xl text-left transition-colors',
|
|
||||||
'bg-[var(--dg-bg-tertiary)] hover:bg-[var(--dg-bg-hover)]',
|
|
||||||
!tpl && 'opacity-40 cursor-not-allowed hover:bg-[var(--dg-bg-tertiary)]'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="w-10 h-10 rounded-lg flex items-center justify-center"
|
|
||||||
style={{ backgroundColor: `color-mix(in srgb, ${opt.color} 15%, transparent)`, color: opt.color }}
|
|
||||||
>
|
|
||||||
<Icon className="h-5 w-5" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-[var(--dg-text-primary)]">{opt.label}</p>
|
|
||||||
<p className="text-xs text-[var(--dg-text-muted)] mt-0.5">
|
|
||||||
{tpl ? tpl.description || `共 ${parseFields(tpl.form_schema).length} 个字段` : '暂无可用模板'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
/* 步骤二:动态表单 */
|
|
||||||
<div className="py-2">
|
|
||||||
<div className="flex items-center gap-2 mb-4">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => { setSelectedTemplate(null); setFormData({}); }}
|
|
||||||
className="text-xs text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)]"
|
|
||||||
>
|
|
||||||
← 返回选择类型
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-x-4 gap-y-4">
|
|
||||||
{parseFields(selectedTemplate.form_schema).map((field: any) => {
|
|
||||||
const isTextarea = field.type === 'textarea';
|
|
||||||
return (
|
|
||||||
<div key={field.key} className={cn('space-y-1.5', isTextarea && 'col-span-2')}>
|
|
||||||
<Label className="text-sm font-medium text-[var(--dg-text-secondary)]">
|
|
||||||
{field.label}
|
|
||||||
{field.required && <span className="text-red-400 ml-0.5">*</span>}
|
|
||||||
</Label>
|
|
||||||
{field.type === 'select' ? (
|
|
||||||
<Select
|
|
||||||
value={String(formData[field.key] ?? '')}
|
|
||||||
onValueChange={(val) => setFormData((prev) => ({ ...prev, [field.key]: val }))}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="bg-[var(--dg-bg-primary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
|
|
||||||
<SelectValue placeholder={field.placeholder || `请选择${field.label}`} />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
|
||||||
{(field.options || []).map((opt: string) => (
|
|
||||||
<SelectItem key={opt} value={opt} className="text-[var(--dg-text-primary)] focus:bg-[var(--dg-bg-hover)]">
|
|
||||||
{opt}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
) : field.type === 'textarea' ? (
|
|
||||||
<Textarea
|
|
||||||
placeholder={field.placeholder || `请输入${field.label}`}
|
|
||||||
value={formData[field.key] ?? ''}
|
|
||||||
onChange={(e) => setFormData((prev) => ({ ...prev, [field.key]: e.target.value }))}
|
|
||||||
className="bg-[var(--dg-bg-primary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)] min-h-[80px]"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Input
|
|
||||||
type={field.type === 'number' ? 'number' : 'text'}
|
|
||||||
placeholder={field.placeholder || `请输入${field.label}`}
|
|
||||||
value={formData[field.key] ?? ''}
|
|
||||||
onChange={(e) => setFormData((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[field.key]: field.type === 'number' ? e.target.value : e.target.value,
|
|
||||||
}))}
|
|
||||||
className="bg-[var(--dg-bg-primary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button variant="ghost" onClick={() => setCreateOpen(false)}>取消</Button>
|
|
||||||
{selectedTemplate && (
|
|
||||||
<Button
|
|
||||||
className="bg-[var(--dg-blue)] hover:opacity-90 text-white"
|
|
||||||
onClick={handleSubmit}
|
|
||||||
disabled={submitting}
|
|
||||||
>
|
|
||||||
{submitting && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
|
||||||
提交申请
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user