feat: K8s集群拓扑图接入真实后端数据 — 分层可视化Cluster→Ingress→Service→Deployment→Node + http.ts类型修复

This commit is contained in:
attacker committed 2026-06-14 20:12:42 +08:00
1 parent 4b094f4645
commit 9f65faff6f
17 files changed
+3021 -1205

No files matched your search

@@ -1,9 +1,8 @@
import { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
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 { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import {
Table,
TableBody,
@@ -15,448 +14,378 @@ import {
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
Layers,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
Plus,
Search,
Database,
Network,
Server,
HardDrive,
Settings,
Edit,
Trash2,
Eye,
Link,
Copy,
ArrowRight,
GitBranch,
Pencil,
Layers,
GripVertical,
X,
Loader2,
} from 'lucide-react';
import { resourceModels, modelInstances } from '@/mocks/data';
import type { ResourceModel, ModelInstance } from '@/types';
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 [activeTab, setActiveTab] = useState('models');
const [models, setModels] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState('');
const [selectedModel, setSelectedModel] = useState<ResourceModel | null>(null);
const [selectedInstance, setSelectedInstance] = useState<ModelInstance | null>(null);
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 filteredModels = resourceModels.filter(
(model) =>
model.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
model.code.toLowerCase().includes(searchQuery.toLowerCase())
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 getModelTypeBadge = (type: ResourceModel['type']) => {
const configs = {
normal: { label: '普通', className: 'border-blue-500/30 text-blue-400 bg-blue-500/10' },
parent: { label: '父模型', className: 'border-purple-500/30 text-purple-400 bg-purple-500/10' },
child: { label: '子模型', className: 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10' },
};
const config = configs[type];
return (
<Badge variant="outline" className={config.className}>
{config.label}
</Badge>
);
};
const getAttributeTypeIcon = (type: string) => {
switch (type) {
case 'string':
return <span className="text-xs text-slate-400">ABC</span>;
case 'number':
return <span className="text-xs text-slate-400">123</span>;
case 'boolean':
return <span className="text-xs text-slate-400">T/F</span>;
case 'date':
return <span className="text-xs text-slate-400">📅</span>;
case 'enum':
return <span className="text-xs text-slate-400">▼</span>;
default:
return null;
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="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">CMDB资源模型定义与实例管理</p>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" className="border-slate-700 text-slate-300 hover:text-slate-100 hover:bg-slate-800">
<Link className="h-4 w-4 mr-2" />
关系管理
</Button>
<Button className="bg-blue-600 hover:bg-blue-700 text-white">
<Plus className="h-4 w-4 mr-2" />
新建模型
</Button>
</div>
</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">{resourceModels.length}</p>
</div>
<div className="p-3 rounded-lg bg-blue-500/10 text-blue-400">
<Layers 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">
{resourceModels.reduce((acc, m) => acc + m.instanceCount, 0)}
</p>
</div>
<div className="p-3 rounded-lg bg-emerald-500/10 text-emerald-400">
<Database 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-purple-400">
{resourceModels.filter((m) => m.type === 'parent').length}
</p>
</div>
<div className="p-3 rounded-lg bg-purple-500/10 text-purple-400">
<GitBranch 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-blue-400">
{resourceModels.reduce((acc, m) => acc + m.attributes.length, 0)}
</p>
</div>
<div className="p-3 rounded-lg bg-cyan-500/10 text-cyan-400">
<Settings className="h-5 w-5" />
</div>
</div>
</CardContent>
</Card>
</div>
{/* Filters */}
<div className="flex items-center gap-3">
<div className="relative w-64">
<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="搜索模型..."
placeholder="搜索模型名称或编码..."
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"
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>
{/* Tabs */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="bg-slate-800 border border-slate-700">
<TabsTrigger value="models" className="data-[state=active]:bg-slate-700">
<Layers className="h-4 w-4 mr-2" />
模型列表
</TabsTrigger>
<TabsTrigger value="instances" className="data-[state=active]:bg-slate-700">
<Database className="h-4 w-4 mr-2" />
实例管理
</TabsTrigger>
<TabsTrigger value="relations" className="data-[state=active]:bg-slate-700">
<Link className="h-4 w-4 mr-2" />
关系视图
</TabsTrigger>
</TabsList>
<TabsContent value="models" className="mt-4">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{filteredModels.map((model) => (
<Card key={model.id} className="bg-slate-900 border-slate-800">
<CardHeader className="pb-2">
<div className="flex items-start justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-blue-500/10 flex items-center justify-center">
<Layers className="h-5 w-5 text-blue-400" />
</div>
<div>
<CardTitle className="text-base text-slate-100">{model.name}</CardTitle>
<CardDescription className="text-slate-500">{model.code}</CardDescription>
</div>
</div>
<div className="flex items-center gap-2">
{getModelTypeBadge(model.type)}
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-slate-400 hover:text-slate-100"
onClick={() => setSelectedModel(model)}
>
<Eye 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>
</CardHeader>
<CardContent>
<p className="text-sm text-slate-400 mb-3">{model.description}</p>
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-slate-500">属性数量</span>
<span className="text-slate-300">{model.attributes.length}</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-slate-500">实例数量</span>
<span className="text-slate-300">{model.instanceCount}</span>
</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 className="mt-4 pt-4 border-t border-slate-800">
<p className="text-xs text-slate-500 mb-2">属性预览</p>
<div className="flex flex-wrap gap-1">
{model.attributes.slice(0, 5).map((attr) => (
<Badge key={attr.id} variant="outline" className="border-slate-700 text-slate-400 text-xs">
{getAttributeTypeIcon(attr.type)}
<span className="ml-1">{attr.name}</span>
{attr.required && <span className="text-red-400 ml-0.5">*</span>}
</Badge>
))}
{model.attributes.length > 5 && (
<Badge variant="outline" className="border-slate-700 text-slate-400 text-xs">
+{model.attributes.length - 5}
</Badge>
)}
</div>
</div>
</CardContent>
</Card>
))}
</div>
</TabsContent>
<TabsContent value="instances" className="mt-4">
<Card className="bg-slate-900 border-slate-800">
<CardContent className="p-0">
<div className="border-t border-slate-800">
<Table>
<TableHeader>
<TableRow className="border-slate-800 hover:bg-transparent">
<TableHead className="text-slate-400">实例名称</TableHead>
<TableHead className="text-slate-400">所属模型</TableHead>
<TableHead className="text-slate-400">关键属性</TableHead>
<TableHead className="text-slate-400">关系数</TableHead>
<TableHead className="text-slate-400">更新时间</TableHead>
<TableHead className="text-slate-400 text-right">操作</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{modelInstances.map((instance) => (
<TableRow key={instance.id} className="border-slate-800 hover:bg-slate-800/50">
<TableCell>
<span className="text-sm font-medium text-slate-200">{instance.name}</span>
</TableCell>
<TableCell>
<Badge variant="outline" className="border-slate-700 text-slate-400">
{resourceModels.find((m) => m.id === instance.modelId)?.name}
</Badge>
</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
{Object.entries(instance.data).slice(0, 3).map(([key, value]) => (
<code key={key} className="text-xs text-slate-400 bg-slate-800 px-1.5 py-0.5 rounded">
{key}: {String(value).slice(0, 15)}
</code>
))}
</div>
</TableCell>
<TableCell>
<span className="text-sm text-slate-300">{instance.relations.length}</span>
</TableCell>
<TableCell>
<span className="text-sm text-slate-400">
{new Date(instance.updatedAt).toLocaleString()}
</span>
</TableCell>
<TableCell className="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={() => setSelectedInstance(instance)}
>
<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>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</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>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="relations" className="mt-4">
<Card className="bg-slate-900 border-slate-800">
<CardContent className="p-8">
<div className="text-center">
<Link className="h-12 w-12 mx-auto text-slate-500 mb-4" />
<h3 className="text-lg font-medium text-slate-300 mb-2">关系视图</h3>
<p className="text-sm text-slate-500 mb-4">可视化展示资源实例之间的关系</p>
<Button className="bg-blue-600 hover:bg-blue-700 text-white">
<Plus className="h-4 w-4 mr-2" />
创建关系
{/* 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>
</CardContent>
</Card>
</TabsContent>
</Tabs>
{/* Model Detail Dialog */}
<Dialog open={!!selectedModel} onOpenChange={() => setSelectedModel(null)}>
<DialogContent className="max-w-2xl bg-slate-900 border-slate-800">
<DialogHeader>
<DialogTitle className="text-slate-100 flex items-center gap-2">
<Layers className="h-5 w-5 text-blue-400" />
{selectedModel?.name}
</DialogTitle>
<DialogDescription className="text-slate-500">
{selectedModel?.code} · {selectedModel?.description}
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<h4 className="text-sm font-medium text-slate-300 mb-2">属性定义</h4>
<div className="border rounded-lg border-slate-700 overflow-hidden">
<table className="w-full">
<thead className="bg-slate-800">
<tr>
<th className="px-4 py-2 text-left text-xs font-medium text-slate-400">属性名</th>
<th className="px-4 py-2 text-left text-xs font-medium text-slate-400">字段名</th>
<th className="px-4 py-2 text-left text-xs font-medium text-slate-400">类型</th>
<th className="px-4 py-2 text-left text-xs font-medium text-slate-400">必填</th>
<th className="px-4 py-2 text-left text-xs font-medium text-slate-400">唯一</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800">
{selectedModel?.attributes.map((attr) => (
<tr key={attr.id} className="hover:bg-slate-800/50">
<td className="px-4 py-2 text-sm text-slate-200">{attr.name}</td>
<td className="px-4 py-2 text-sm text-slate-400">{attr.code}</td>
<td className="px-4 py-2">
<Badge variant="outline" className="border-slate-700 text-slate-400 text-xs">
{attr.type}
</Badge>
</td>
<td className="px-4 py-2 text-sm text-slate-400">{attr.required ? '是' : '否'}</td>
<td className="px-4 py-2 text-sm text-slate-400">{attr.unique ? '是' : '否'}</td>
</tr>
))}
</tbody>
</table>
<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="outline" className="border-slate-700 text-slate-300">
<Copy className="h-4 w-4 mr-2" />
复制模型
</Button>
<Button className="bg-blue-600 hover:bg-blue-700 text-white">
<Edit className="h-4 w-4 mr-2" />
编辑模型
<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>
{/* Instance Detail Dialog */}
<Dialog open={!!selectedInstance} onOpenChange={() => setSelectedInstance(null)}>
<DialogContent className="max-w-2xl bg-slate-900 border-slate-800">
{/* 删除确认 */}
<Dialog open={deleteId !== null} onOpenChange={(v) => !v && setDeleteId(null)}>
<DialogContent className="max-w-sm">
<DialogHeader>
<DialogTitle className="text-slate-100 flex items-center gap-2">
<Database className="h-5 w-5 text-emerald-400" />
{selectedInstance?.name}
</DialogTitle>
<DialogDescription className="text-slate-500">
模型: {resourceModels.find((m) => m.id === selectedInstance?.modelId)?.name}
</DialogDescription>
<DialogTitle>确认删除</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<h4 className="text-sm font-medium text-slate-300 mb-2">属性数据</h4>
<div className="grid grid-cols-2 gap-3 p-4 rounded-lg bg-slate-800/50">
{selectedInstance && Object.entries(selectedInstance.data).map(([key, value]) => (
<div key={key}>
<p className="text-xs text-slate-500 mb-1">{key}</p>
<p className="text-sm text-slate-200">{String(value)}</p>
</div>
))}
</div>
</div>
{selectedInstance && selectedInstance.relations.length > 0 && (
<div>
<h4 className="text-sm font-medium text-slate-300 mb-2">关联关系</h4>
<div className="space-y-2">
{selectedInstance.relations.map((rel, index) => (
<div key={index} className="flex items-center gap-2 p-2 rounded-lg bg-slate-800/50">
<span className="text-sm text-slate-300">{rel.relationType}</span>
<ArrowRight className="h-4 w-4 text-slate-500" />
<Badge variant="outline" className="border-slate-700 text-slate-400">
{rel.targetInstanceId}
</Badge>
</div>
))}
</div>
</div>
)}
</div>
<p className="text-sm text-slate-400">确定删除此资源模型?此操作不可撤销。</p>
<DialogFooter>
<Button variant="outline" className="border-slate-700 text-slate-300">
<Link className="h-4 w-4 mr-2" />
管理关系
</Button>
<Button className="bg-blue-600 hover:bg-blue-700 text-white">
<Edit className="h-4 w-4 mr-2" />
编辑实例
</Button>
<Button variant="ghost" onClick={() => setDeleteId(null)}>取消</Button>
<Button variant="destructive" onClick={confirmDelete}>删除</Button>
</DialogFooter>
</DialogContent>
</Dialog>