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:
attacker committed 2026-06-15 08:07:10 +08:00
1 parent 8c4b59510d
commit f70feb360c
5 files changed
+155 -1020

No files matched your search

@@ -1,422 +0,0 @@
import { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
Server,
Plus,
Search,
Grid3X3,
List,
MapPin,
Cpu,
MemoryStick,
HardDrive,
Eye,
Edit,
} from 'lucide-react';
import { serverApi } from '@/services/api';
import type { Server as ServerType } from '@/types';
export function CabinetView() {
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
const [statusFilter, setStatusFilter] = useState<string>('all');
const [searchQuery, setSearchQuery] = useState('');
const [servers, setServers] = useState<ServerType[]>([]);
const [loading, setLoading] = useState(true);
const [selectedServer, setSelectedServer] = useState<ServerType | null>(null);
// 拉取服务器列表 // Logan.Li
const fetchServers = async () => {
setLoading(true);
try {
const resp = await serverApi.getServers({ page: 1, page_size: 200 });
setServers((resp?.list ?? []) as ServerType[]);
} catch {
setServers([]);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchServers();
}, []);
const filteredServers = servers.filter((srv) => {
const matchesStatus = statusFilter === 'all' || srv.status === statusFilter;
const matchesSearch =
srv.hostname?.toLowerCase().includes(searchQuery.toLowerCase()) ||
srv.ip?.toLowerCase().includes(searchQuery.toLowerCase());
return matchesStatus && matchesSearch;
});
const getStatusColor = (status: string) => {
const colors: Record<string, string> = {
running: 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10',
stopped: 'border-red-500/30 text-red-400 bg-red-500/10',
maintenance: 'border-amber-500/30 text-amber-400 bg-amber-500/10',
offline: 'border-slate-500/30 text-slate-400 bg-slate-500/10',
};
return colors[status] ?? colors.offline;
};
const getStatusText = (status: string) => {
const map: Record<string, string> = {
running: '运行中',
stopped: '已停止',
maintenance: '维护中',
offline: '离线',
};
return map[status] ?? status;
};
const getUsageColor = (usage: number) =>
usage > 80 ? 'bg-red-400' : usage > 60 ? 'bg-amber-400' : 'bg-emerald-400';
return (
<div className="space-y-6">
{/* Page Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-slate-100">机房机柜视图</h1>
<p className="text-sm text-slate-500 mt-1">数据中心服务器资产概览</p>
</div>
<Button className="bg-blue-600 hover:bg-blue-700 text-white">
<Plus className="h-4 w-4 mr-2" />
添加设备
</Button>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card className="bg-slate-900 border-slate-800">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">服务器总数</p>
<p className="text-2xl font-bold text-slate-100">{servers.length}</p>
</div>
<div className="p-3 rounded-lg bg-blue-500/10 text-blue-400">
<Server className="h-5 w-5" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-slate-900 border-slate-800">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">运行中</p>
<p className="text-2xl font-bold text-emerald-400">
{servers.filter((s) => s.status === 'running').length}
</p>
</div>
<div className="p-3 rounded-lg bg-emerald-500/10 text-emerald-400">
<Grid3X3 className="h-5 w-5" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-slate-900 border-slate-800">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">维护中</p>
<p className="text-2xl font-bold text-amber-400">
{servers.filter((s) => s.status === 'maintenance').length}
</p>
</div>
<div className="p-3 rounded-lg bg-amber-500/10 text-amber-400">
<Cpu className="h-5 w-5" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-slate-900 border-slate-800">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">离线</p>
<p className="text-2xl font-bold text-slate-400">
{servers.filter((s) => s.status === 'offline' || s.status === 'stopped').length}
</p>
</div>
<div className="p-3 rounded-lg bg-slate-500/10 text-slate-400">
<HardDrive className="h-5 w-5" />
</div>
</div>
</CardContent>
</Card>
</div>
{/* Filters & View Toggle */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="relative w-64">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-500" />
<Input
placeholder="搜索主机名或 IP..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9 h-9 bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-500"
/>
</div>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-40 h-9 bg-slate-800/50 border-slate-700 text-slate-200">
<SelectValue placeholder="选择状态" />
</SelectTrigger>
<SelectContent className="bg-slate-800 border-slate-700">
<SelectItem value="all">全部状态</SelectItem>
<SelectItem value="running">运行中</SelectItem>
<SelectItem value="stopped">已停止</SelectItem>
<SelectItem value="maintenance">维护中</SelectItem>
<SelectItem value="offline">离线</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-1 bg-slate-800 rounded-lg p-1">
<Button
variant={viewMode === 'grid' ? 'secondary' : 'ghost'}
size="sm"
className="h-7"
onClick={() => setViewMode('grid')}
>
<Grid3X3 className="h-4 w-4 mr-1" />
网格视图
</Button>
<Button
variant={viewMode === 'list' ? 'secondary' : 'ghost'}
size="sm"
className="h-7"
onClick={() => setViewMode('list')}
>
<List className="h-4 w-4 mr-1" />
列表视图
</Button>
</div>
</div>
{/* View Content */}
{loading ? (
<div className="flex items-center justify-center py-16">
<div className="w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
</div>
) : filteredServers.length === 0 ? (
<div className="flex flex-col items-center justify-center py-20 text-slate-500">
<MapPin className="h-10 w-10 mb-3 opacity-40" />
<p className="text-sm">暂无服务器数据</p>
</div>
) : viewMode === 'grid' ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{filteredServers.map((srv) => (
<Card
key={srv.id}
className="bg-slate-900 border-slate-800 cursor-pointer hover:border-slate-700 transition-all"
onClick={() => setSelectedServer(srv)}
>
<CardContent className="p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<div className="p-2 rounded-lg bg-blue-500/10">
<Server className="h-4 w-4 text-blue-400" />
</div>
<div>
<p className="text-sm font-medium text-slate-200">{srv.hostname || '--'}</p>
<p className="text-xs text-slate-500">{srv.ip || '--'}</p>
</div>
</div>
<Badge variant="outline" className={getStatusColor(srv.status)}>
{getStatusText(srv.status)}
</Badge>
</div>
<div className="space-y-2">
<div>
<div className="flex justify-between text-xs text-slate-500 mb-1">
<span className="flex items-center gap-1">
<Cpu className="h-3 w-3" /> CPU
</span>
<span>{Math.round(srv.cpuUsage ?? 0)}%</span>
</div>
<div className="h-1.5 bg-slate-800 rounded-full overflow-hidden">
<div
className={`h-full rounded-full ${getUsageColor(srv.cpuUsage ?? 0)}`}
style={{ width: `${srv.cpuUsage ?? 0}%` }}
/>
</div>
</div>
<div>
<div className="flex justify-between text-xs text-slate-500 mb-1">
<span className="flex items-center gap-1">
<MemoryStick className="h-3 w-3" /> 内存
</span>
<span>{Math.round(srv.memoryUsage ?? 0)}%</span>
</div>
<div className="h-1.5 bg-slate-800 rounded-full overflow-hidden">
<div
className={`h-full rounded-full ${getUsageColor(srv.memoryUsage ?? 0)}`}
style={{ width: `${srv.memoryUsage ?? 0}%` }}
/>
</div>
</div>
<div>
<div className="flex justify-between text-xs text-slate-500 mb-1">
<span className="flex items-center gap-1">
<HardDrive className="h-3 w-3" /> 磁盘
</span>
<span>{Math.round(srv.diskUsage ?? 0)}%</span>
</div>
<div className="h-1.5 bg-slate-800 rounded-full overflow-hidden">
<div
className={`h-full rounded-full ${getUsageColor(srv.diskUsage ?? 0)}`}
style={{ width: `${srv.diskUsage ?? 0}%` }}
/>
</div>
</div>
</div>
<div className="mt-3 flex flex-wrap items-center gap-1">
<Badge variant="outline" className="text-xs border-slate-700 text-slate-400">
{srv.cpu ?? 0} 核
</Badge>
<Badge variant="outline" className="text-xs border-slate-700 text-slate-400">
{srv.memory ?? 0} GB
</Badge>
{srv.region && (
<Badge variant="outline" className="text-xs border-slate-700 text-slate-400">
{srv.region}
</Badge>
)}
</div>
</CardContent>
</Card>
))}
</div>
) : (
<Card className="bg-slate-900 border-slate-800">
<CardContent className="p-0">
<div className="border-t border-slate-800">
<table className="w-full">
<thead className="bg-slate-800">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">主机名</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">IP地址</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">系统</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">区域</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">配置</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">状态</th>
<th className="px-4 py-3 text-right text-xs font-medium text-slate-400">操作</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800">
{filteredServers.map((srv) => (
<tr key={srv.id} className="hover:bg-slate-800/50">
<td className="px-4 py-3">
<div className="flex items-center gap-2">
<Server className="h-4 w-4 text-blue-400" />
<span className="text-sm font-medium text-slate-200">{srv.hostname || '--'}</span>
</div>
</td>
<td className="px-4 py-3 text-sm text-slate-300">{srv.ip || '--'}</td>
<td className="px-4 py-3 text-sm text-slate-300">{srv.os || '--'}</td>
<td className="px-4 py-3 text-sm text-slate-300">{srv.region || '--'}</td>
<td className="px-4 py-3 text-sm text-slate-400">
{srv.cpu ?? 0}核 / {srv.memory ?? 0}GB
</td>
<td className="px-4 py-3">
<Badge variant="outline" className={getStatusColor(srv.status)}>
{getStatusText(srv.status)}
</Badge>
</td>
<td className="px-4 py-3 text-right">
<div className="flex items-center justify-end gap-1">
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-slate-400 hover:text-slate-100"
onClick={() => setSelectedServer(srv)}
>
<Eye className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-slate-100">
<Edit className="h-4 w-4" />
</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
)}
{/* Server Detail Dialog */}
<Dialog open={!!selectedServer} onOpenChange={() => setSelectedServer(null)}>
<DialogContent className="bg-slate-900 border-slate-800">
<DialogHeader>
<DialogTitle className="text-slate-100 flex items-center gap-2">
<Server className="h-5 w-5 text-blue-400" />
{selectedServer?.hostname}
</DialogTitle>
<DialogDescription className="text-slate-500">
{selectedServer?.ip} · {selectedServer?.os}
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4 p-4 rounded-lg bg-slate-800/50">
<div>
<p className="text-xs text-slate-500 mb-1">IP 地址</p>
<p className="text-sm text-slate-200">{selectedServer?.ip || '--'}</p>
</div>
<div>
<p className="text-xs text-slate-500 mb-1">操作系统</p>
<p className="text-sm text-slate-200">{selectedServer?.os || '--'}</p>
</div>
<div>
<p className="text-xs text-slate-500 mb-1">配置</p>
<p className="text-sm text-slate-200">
{selectedServer?.cpu ?? 0} 核 / {selectedServer?.memory ?? 0} GB
</p>
</div>
<div>
<p className="text-xs text-slate-500 mb-1">状态</p>
<Badge variant="outline" className={selectedServer ? getStatusColor(selectedServer.status) : ''}>
{selectedServer ? getStatusText(selectedServer.status) : ''}
</Badge>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" className="border-slate-700 text-slate-300">
<Edit className="h-4 w-4 mr-2" />
编辑
</Button>
<Button className="bg-blue-600 hover:bg-blue-700 text-white">
<Server className="h-4 w-4 mr-2" />
查看详情
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}
@@ -1,394 +0,0 @@
import { useState, useEffect, useCallback } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
Plus,
Search,
Trash2,
Pencil,
Layers,
GripVertical,
X,
Loader2,
} from 'lucide-react';
import { resourceModelApi } from '@/services/api';
interface AttrDef {
name: string;
code: string;
type: string;
required: boolean;
unique?: boolean;
}
const attrTypes = [
{ value: 'string', label: '字符串' },
{ value: 'number', label: '数字' },
{ value: 'bool', label: '布尔' },
{ value: 'date', label: '日期' },
{ value: 'enum', label: '枚举' },
];
const typeLabels: Record<string, string> = {
normal: '普通模型',
parent: '父模型',
};
export function ResourceModelManager() {
const [models, setModels] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState('');
const [dialogOpen, setDialogOpen] = useState(false);
const [editingItem, setEditingItem] = useState<any>(null);
const [submitting, setSubmitting] = useState(false);
const [selectedModel, setSelectedModel] = useState<any>(null);
const [formData, setFormData] = useState({
name: '', code: '', description: '', type: 'normal', icon: '',
});
const [attrs, setAttrs] = useState<AttrDef[]>([]);
const loadData = useCallback(async () => {
setLoading(true);
try {
const res = await resourceModelApi.list();
const items = (res as any)?.list || [];
setModels(items);
} catch {
setModels([]);
} finally {
setLoading(false);
}
}, []);
useEffect(() => { loadData(); }, [loadData]);
const filtered = models.filter(m =>
m.name?.toLowerCase().includes(searchQuery.toLowerCase()) ||
m.code?.toLowerCase().includes(searchQuery.toLowerCase())
);
const parseAttrs = (attrsStr: string): AttrDef[] => {
if (!attrsStr) return [];
try {
const parsed = JSON.parse(attrsStr);
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
}
};
const openCreate = () => {
setEditingItem(null);
setFormData({ name: '', code: '', description: '', type: 'normal', icon: '' });
setAttrs([]);
setDialogOpen(true);
};
const openEdit = (item: any) => {
setEditingItem(item);
setFormData({
name: item.name || '', code: item.code || '', description: item.description || '',
type: item.type || 'normal', icon: item.icon || '',
});
setAttrs(parseAttrs(item.attributes));
setDialogOpen(true);
};
const addAttr = () => {
setAttrs([...attrs, { name: '', code: '', type: 'string', required: false }]);
};
const updateAttr = (index: number, field: keyof AttrDef, value: any) => {
const next = [...attrs];
(next[index] as any)[field] = value;
setAttrs(next);
};
const removeAttr = (index: number) => {
setAttrs(attrs.filter((_, i) => i !== index));
};
const handleSubmit = async () => {
if (!formData.name.trim() || !formData.code.trim()) return;
setSubmitting(true);
try {
const payload = { ...formData, attributes: attrs.filter(a => a.name.trim() && a.code.trim()) };
if (editingItem) {
await resourceModelApi.update(editingItem.id, payload);
} else {
await resourceModelApi.create(payload);
}
setDialogOpen(false);
loadData();
} catch {
} finally {
setSubmitting(false);
}
};
const [deleteId, setDeleteId] = useState<number | null>(null);
const handleDelete = (id: number) => setDeleteId(id);
const confirmDelete = async () => {
if (deleteId === null) return;
await resourceModelApi.delete(deleteId);
if (selectedModel?.id === deleteId) setSelectedModel(null);
setDeleteId(null);
loadData();
};
const getAttrCount = (item: any) => parseAttrs(item.attributes).length;
return (
<div className="p-6 space-y-4">
{/* Toolbar */}
<div className="flex items-center justify-between gap-4">
<div className="relative w-72">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-500" />
<Input
placeholder="搜索模型名称或编码..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9 h-9 bg-slate-800/50 border-slate-700"
/>
</div>
<Button onClick={openCreate} className="gap-2">
<Plus className="h-4 w-4" />
添加模型
</Button>
</div>
{/* List */}
{loading ? (
<div className="flex justify-center py-20">
<Loader2 className="h-6 w-6 animate-spin text-slate-500" />
</div>
) : filtered.length === 0 ? (
<div className="text-center py-20 text-slate-500">
<Layers className="h-12 w-12 mx-auto mb-3 opacity-40" />
<p>暂无资源模型</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{filtered.map((m) => (
<div
key={m.id}
className="bg-slate-800/30 rounded-lg p-4 hover:bg-slate-800/50 transition-colors group cursor-pointer"
onClick={() => setSelectedModel(selectedModel?.id === m.id ? null : m)}
>
<div className="flex items-start justify-between mb-2">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-cyan-500/20 flex items-center justify-center">
<Layers className="h-5 w-5 text-cyan-400" />
</div>
<div>
<h3 className="font-medium text-slate-100">{m.name}</h3>
<span className="text-xs text-slate-500 font-mono">{m.code}</span>
</div>
</div>
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<button onClick={(e) => { e.stopPropagation(); openEdit(m); }} className="p-1.5 rounded hover:bg-slate-700 text-slate-400 hover:text-slate-200">
<Pencil className="h-3.5 w-3.5" />
</button>
<button onClick={(e) => { e.stopPropagation(); handleDelete(m.id); }} className="p-1.5 rounded hover:bg-slate-700 text-slate-400 hover:text-red-400">
<Trash2 className="h-3.5 w-3.5" />
</button>
</div>
</div>
{m.description && (
<p className="text-sm text-slate-400 mb-3 line-clamp-2">{m.description}</p>
)}
<div className="flex items-center gap-2">
<Badge variant="outline" className="text-slate-400">
{typeLabels[m.type] || m.type}
</Badge>
<Badge variant="outline" className="text-slate-400">
{getAttrCount(m)} 个属性
</Badge>
</div>
{/* Expanded attributes */}
{selectedModel?.id === m.id && (
<div className="mt-3 pt-3 border-t border-slate-700/50 space-y-1.5">
{parseAttrs(m.attributes).map((attr, i) => (
<div key={i} className="flex items-center justify-between text-xs">
<span className="text-slate-300">{attr.name}</span>
<div className="flex items-center gap-2">
<span className="text-slate-500 font-mono">{attr.code}</span>
<span className="text-slate-600">{attr.type}</span>
{attr.required && <span className="text-red-400">*</span>}
</div>
</div>
))}
{getAttrCount(m) === 0 && (
<p className="text-xs text-slate-600">暂无属性定义</p>
)}
</div>
)}
</div>
))}
</div>
)}
{/* Dialog */}
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>{editingItem ? '编辑资源模型' : '添加资源模型'}</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-2 max-h-[60vh] overflow-y-auto">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1.5">
<Label>模型名称 *</Label>
<Input
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="如:服务器"
className="bg-slate-800/50 border-slate-700"
/>
</div>
<div className="space-y-1.5">
<Label>编码 *</Label>
<Input
value={formData.code}
onChange={(e) => setFormData({ ...formData, code: e.target.value })}
placeholder="如:server"
className="bg-slate-800/50 border-slate-700 font-mono"
disabled={!!editingItem}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1.5">
<Label>模型类型</Label>
<Select value={formData.type} onValueChange={(v) => setFormData({ ...formData, type: v })}>
<SelectTrigger className="bg-slate-800/50 border-slate-700">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="normal">普通模型</SelectItem>
<SelectItem value="parent">父模型</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<Label>描述</Label>
<Input
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder="模型描述..."
className="bg-slate-800/50 border-slate-700"
/>
</div>
</div>
{/* Attributes editor */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label>属性定义</Label>
<Button type="button" variant="ghost" size="sm" onClick={addAttr} className="h-7 text-xs gap-1">
<Plus className="h-3 w-3" /> 添加属性
</Button>
</div>
<div className="space-y-2">
{attrs.map((attr, i) => (
<div key={i} className="flex items-center gap-2">
<GripVertical className="h-4 w-4 text-slate-600 shrink-0" />
<Input
placeholder="属性名"
value={attr.name}
onChange={(e) => updateAttr(i, 'name', e.target.value)}
className="h-8 bg-slate-800/50 border-slate-700 flex-1"
/>
<Input
placeholder="编码"
value={attr.code}
onChange={(e) => updateAttr(i, 'code', e.target.value)}
className="h-8 bg-slate-800/50 border-slate-700 flex-1 font-mono text-sm"
/>
<Select value={attr.type} onValueChange={(v) => updateAttr(i, 'type', v)}>
<SelectTrigger className="h-8 w-28 bg-slate-800/50 border-slate-700">
<SelectValue />
</SelectTrigger>
<SelectContent>
{attrTypes.map(t => (
<SelectItem key={t.value} value={t.value}>{t.label}</SelectItem>
))}
</SelectContent>
</Select>
<Button
type="button"
variant="ghost"
size="icon"
className={`h-8 w-8 shrink-0 ${attr.required ? 'text-red-400' : 'text-slate-500'}`}
onClick={() => updateAttr(i, 'required', !attr.required)}
title={attr.required ? '必填' : '选填'}
>
*
</Button>
<Button
type="button"
variant="ghost"
size="icon"
className="h-8 w-8 shrink-0 text-slate-500 hover:text-red-400"
onClick={() => removeAttr(i)}
>
<X className="h-3.5 w-3.5" />
</Button>
</div>
))}
{attrs.length === 0 && (
<p className="text-sm text-slate-600 py-2">暂无属性,点击"添加属性"开始定义</p>
)}
</div>
</div>
</div>
<DialogFooter>
<Button variant="ghost" onClick={() => setDialogOpen(false)}>取消</Button>
<Button onClick={handleSubmit} disabled={submitting || !formData.name.trim() || !formData.code.trim()}>
{submitting && <Loader2 className="h-4 w-4 mr-1 animate-spin" />}
{editingItem ? '保存' : '创建'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 删除确认 */}
<Dialog open={deleteId !== null} onOpenChange={(v) => !v && setDeleteId(null)}>
<DialogContent className="max-w-sm">
<DialogHeader>
<DialogTitle>确认删除</DialogTitle>
</DialogHeader>
<p className="text-sm text-slate-400">确定删除此资源模型?此操作不可撤销。</p>
<DialogFooter>
<Button variant="ghost" onClick={() => setDeleteId(null)}>取消</Button>
<Button variant="destructive" onClick={confirmDelete}>删除</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}