feat: 资源申请模块重构——清理816行死代码+提取共享helpers+修复类型标签bug
- 删除 CabinetView.tsx(422行) + ResourceModelManager.tsx(394行) 死代码 - 提取 approval/helpers.tsx 共享辅助函数(parseSteps/parseFields/renderFormData/ getProgressInfo/renderStepProgress/APPROVAL_TYPE_OPTIONS/getInstanceType) - 修复审批类型标签bug:instance无type字段,需从template.type取 (getApprovalTypeLabel/Color/Icon 全部改用 getInstanceType(instance)) - ApprovalFlow.tsx 771→666行(低于700限制) - ApprovalProcessTab.tsx 移除重复helpers+过期类型字典(deploy/server/permission→server_apply/app_deploy)
This commit is contained in:
1 parent
8c4b59510d
commit
f70feb360c
5 files changed
+155
-1020
No files matched your search
@@ -22,133 +22,28 @@ import {
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
GitBranch,
|
||||
FileText,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
Loader2,
|
||||
Clock,
|
||||
Plus,
|
||||
X,
|
||||
GitCommit,
|
||||
Server,
|
||||
KeyRound,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||||
import { approvalApi } from '@/services/api';
|
||||
import { ApprovalTemplateTab } from './ApprovalTemplateTab';
|
||||
import { ApprovalProcessTab } from './ApprovalProcessTab';
|
||||
|
||||
// ---- Shared helpers (kept in main for dialog usage) ----
|
||||
|
||||
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 [];
|
||||
}
|
||||
|
||||
// 解析模板的 form_schema,返回字段定义数组
|
||||
function parseFields(formSchema: any): any[] {
|
||||
if (!formSchema) return [];
|
||||
const schema = typeof formSchema === 'string' ? (() => { try { return JSON.parse(formSchema); } catch { return null; } })() : formSchema;
|
||||
if (!schema) return [];
|
||||
return Array.isArray(schema.fields) ? schema.fields : [];
|
||||
}
|
||||
|
||||
// 在详情弹窗中渲染表单数据为键值对展示
|
||||
function renderFormData(formDataStr: string, template?: any) {
|
||||
if (!formDataStr) return null;
|
||||
let data: Record<string, any>;
|
||||
try {
|
||||
data = typeof formDataStr === 'string' ? JSON.parse(formDataStr) : formDataStr;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (!data || typeof data !== 'object') return null;
|
||||
|
||||
// 优先使用模板的 form_schema 标签,否则退化为 key
|
||||
const fields = parseFields(template?.form_schema);
|
||||
const labelMap = new Map<string, string>();
|
||||
fields.forEach((f) => labelMap.set(f.key, f.label));
|
||||
|
||||
const entries = Object.entries(data).filter(([, v]) => v !== null && v !== undefined && v !== '');
|
||||
if (entries.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-3">
|
||||
{entries.map(([key, value]) => (
|
||||
<div key={key} className="min-w-0">
|
||||
<p className="text-xs text-[var(--dg-text-muted)] mb-0.5">{labelMap.get(key) || key}</p>
|
||||
<p className="text-sm text-[var(--dg-text-primary)] break-words">{String(value)}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const APPROVAL_TYPE_OPTIONS = [
|
||||
{ value: 'server_apply', label: '云服务器申请', icon: Server, color: 'var(--dg-cyan)' },
|
||||
{ value: 'app_deploy', label: '应用部署申请', icon: GitCommit, color: 'var(--dg-blue)' },
|
||||
];
|
||||
|
||||
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)';
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
import {
|
||||
parseFields,
|
||||
renderFormData,
|
||||
renderStepProgress,
|
||||
getProgressInfo,
|
||||
getApprovalTypeLabel,
|
||||
getApprovalTypeColor,
|
||||
getInstanceType,
|
||||
APPROVAL_TYPE_OPTIONS,
|
||||
} from './helpers';
|
||||
|
||||
export function ApprovalFlow() {
|
||||
const confirm = useConfirm();
|
||||
@@ -427,14 +322,14 @@ export function ApprovalFlow() {
|
||||
<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) }}>
|
||||
<div className="w-10 h-10 rounded-lg bg-[var(--dg-bg-tertiary)] flex items-center justify-center" style={{ color: getApprovalTypeColor(getInstanceType(instance)) }}>
|
||||
<FileText 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>
|
||||
<Badge variant="outline" className="text-xs border-[var(--dg-border)]" style={{ color: getApprovalTypeColor(instance.type) }}>
|
||||
{getApprovalTypeLabel(instance.type)}
|
||||
<Badge variant="outline" className="text-xs border-[var(--dg-border)]" style={{ color: getApprovalTypeColor(getInstanceType(instance)) }}>
|
||||
{getApprovalTypeLabel(getInstanceType(instance))}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-xs text-[var(--dg-text-muted)]">
|
||||
@@ -494,8 +389,8 @@ export function ApprovalFlow() {
|
||||
<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(selectedInstance.type) }}>
|
||||
{getApprovalTypeLabel(selectedInstance.type)}
|
||||
<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 || '--'}
|
||||
|
||||
@@ -9,40 +9,16 @@ import {
|
||||
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;
|
||||
import {
|
||||
getProgressInfo,
|
||||
renderStepProgress,
|
||||
getApprovalTypeLabel,
|
||||
getApprovalTypeColor,
|
||||
getApprovalTypeIcon,
|
||||
getInstanceType,
|
||||
} from './helpers';
|
||||
|
||||
// 状态标签页定义
|
||||
const STATUS_TABS = [
|
||||
@@ -92,54 +68,6 @@ function getStatusBadge(status: string) {
|
||||
);
|
||||
}
|
||||
|
||||
// 获取进度信息
|
||||
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[];
|
||||
@@ -218,14 +146,14 @@ export function ApprovalProcessTab({
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{filteredInstances.map((instance) => {
|
||||
const Icon = getApprovalTypeIcon(instance.type);
|
||||
const Icon = getApprovalTypeIcon(getInstanceType(instance));
|
||||
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) }}>
|
||||
<div className="w-10 h-10 rounded-lg bg-[var(--dg-bg-tertiary)] flex items-center justify-center" style={{ color: getApprovalTypeColor(getInstanceType(instance)) }}>
|
||||
<Icon className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
@@ -235,8 +163,8 @@ export function ApprovalProcessTab({
|
||||
</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 variant="outline" className="text-xs border-[var(--dg-border)]" style={{ color: getApprovalTypeColor(getInstanceType(instance)) }}>
|
||||
{getApprovalTypeLabel(getInstanceType(instance))}
|
||||
</Badge>
|
||||
</span>
|
||||
<span>申请人:{instance.applicant_name || instance.applicant || '--'}</span>
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
// 资源申请模块共享辅助函数(从 ApprovalFlow.tsx 提取) // Logan.Li
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
Loader2,
|
||||
Server,
|
||||
GitCommit,
|
||||
FileText,
|
||||
} from 'lucide-react';
|
||||
|
||||
// 解析审批步骤数组(兼容 JSON 字符串) // Logan.Li
|
||||
export 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 [];
|
||||
}
|
||||
|
||||
// 解析模板的 form_schema,返回字段定义数组 // Logan.Li
|
||||
export function parseFields(formSchema: any): any[] {
|
||||
if (!formSchema) return [];
|
||||
const schema = typeof formSchema === 'string' ? (() => { try { return JSON.parse(formSchema); } catch { return null; } })() : formSchema;
|
||||
if (!schema) return [];
|
||||
return Array.isArray(schema.fields) ? schema.fields : [];
|
||||
}
|
||||
|
||||
// 在详情弹窗中渲染表单数据为键值对展示 // Logan.Li
|
||||
export function renderFormData(formDataStr: string, template?: any) {
|
||||
if (!formDataStr) return null;
|
||||
let data: Record<string, any>;
|
||||
try {
|
||||
data = typeof formDataStr === 'string' ? JSON.parse(formDataStr) : formDataStr;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (!data || typeof data !== 'object') return null;
|
||||
|
||||
const fields = parseFields(template?.form_schema);
|
||||
const labelMap = new Map<string, string>();
|
||||
fields.forEach((f) => labelMap.set(f.key, f.label));
|
||||
|
||||
const entries = Object.entries(data).filter(([, v]) => v !== null && v !== undefined && v !== '');
|
||||
if (entries.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-3">
|
||||
{entries.map(([key, value]) => (
|
||||
<div key={key} className="min-w-0">
|
||||
<p className="text-xs text-[var(--dg-text-muted)] mb-0.5">{labelMap.get(key) || key}</p>
|
||||
<p className="text-sm text-[var(--dg-text-primary)] break-words">{String(value)}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 审批类型选项 // Logan.Li
|
||||
export const APPROVAL_TYPE_OPTIONS = [
|
||||
{ value: 'server_apply', label: '云服务器申请', icon: Server, color: 'var(--dg-cyan)' },
|
||||
{ value: 'app_deploy', label: '应用部署申请', icon: GitCommit, color: 'var(--dg-blue)' },
|
||||
];
|
||||
|
||||
export const getApprovalTypeLabel = (value: string): string =>
|
||||
APPROVAL_TYPE_OPTIONS.find((o) => o.value === value)?.label || value || '通用审批';
|
||||
|
||||
export const getApprovalTypeColor = (value: string): string =>
|
||||
APPROVAL_TYPE_OPTIONS.find((o) => o.value === value)?.color || 'var(--dg-text-secondary)';
|
||||
|
||||
// 根据审批类型返回对应图标组件 // Logan.Li
|
||||
export const getApprovalTypeIcon = (value: string) =>
|
||||
APPROVAL_TYPE_OPTIONS.find((o) => o.value === value)?.icon || FileText;
|
||||
|
||||
// 获取审批实例的类型(优先从关联模板取) // Logan.Li
|
||||
export const getInstanceType = (instance: any): string =>
|
||||
instance?.template?.type || instance?.type || '';
|
||||
|
||||
// 计算审批进度信息 // Logan.Li
|
||||
export 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 };
|
||||
}
|
||||
|
||||
// 渲染审批进度(用于详情弹窗) // Logan.Li
|
||||
export 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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user