319 lines
12 KiB
TypeScript
319 lines
12 KiB
TypeScript
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 {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
import {
|
|
Plus,
|
|
Search,
|
|
Trash2,
|
|
Pencil,
|
|
CloudCog,
|
|
CheckCircle2,
|
|
XCircle,
|
|
ExternalLink,
|
|
Loader2,
|
|
} from 'lucide-react';
|
|
import { platformApi } from '@/services/api';
|
|
|
|
const typeLabels: Record<string, string> = {
|
|
public: '公有云',
|
|
private: '私有云',
|
|
hybrid: '混合云',
|
|
};
|
|
|
|
const typeColors: Record<string, string> = {
|
|
public: 'text-blue-400',
|
|
private: 'text-purple-400',
|
|
hybrid: 'text-cyan-400',
|
|
};
|
|
|
|
export function PlatformManager() {
|
|
const [platforms, setPlatforms] = useState<any[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const [selectedType, setSelectedType] = useState('all');
|
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
const [editingItem, setEditingItem] = useState<any>(null);
|
|
const [submitting, setSubmitting] = useState(false);
|
|
const [formData, setFormData] = useState({
|
|
name: '', code: '', type: 'public', status: 'active',
|
|
website: '', description: '',
|
|
});
|
|
|
|
const loadData = useCallback(async () => {
|
|
setLoading(true);
|
|
try {
|
|
const res = await platformApi.list();
|
|
const items = (res as any)?.list || [];
|
|
setPlatforms(items);
|
|
} catch {
|
|
setPlatforms([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => { loadData(); }, [loadData]);
|
|
|
|
const filtered = platforms.filter(p => {
|
|
const matchSearch = p.name?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
p.code?.toLowerCase().includes(searchQuery.toLowerCase());
|
|
const matchType = selectedType === 'all' || p.type === selectedType;
|
|
return matchSearch && matchType;
|
|
});
|
|
|
|
const openCreate = () => {
|
|
setEditingItem(null);
|
|
setFormData({ name: '', code: '', type: 'public', status: 'active', website: '', description: '' });
|
|
setDialogOpen(true);
|
|
};
|
|
|
|
const openEdit = (item: any) => {
|
|
setEditingItem(item);
|
|
setFormData({
|
|
name: item.name || '', code: item.code || '', type: item.type || 'public',
|
|
status: item.status || 'active', website: item.website || '', description: item.description || '',
|
|
});
|
|
setDialogOpen(true);
|
|
};
|
|
|
|
const handleSubmit = async () => {
|
|
if (!formData.name.trim() || !formData.code.trim()) return;
|
|
setSubmitting(true);
|
|
try {
|
|
if (editingItem) {
|
|
await platformApi.update(editingItem.id, formData);
|
|
} else {
|
|
await platformApi.create(formData);
|
|
}
|
|
setDialogOpen(false);
|
|
loadData();
|
|
} catch {
|
|
// error handled by http interceptor
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
};
|
|
|
|
const [deleteId, setDeleteId] = useState<number | null>(null);
|
|
const handleDelete = (id: number) => setDeleteId(id);
|
|
const confirmDelete = async () => {
|
|
if (deleteId === null) return;
|
|
await platformApi.delete(deleteId);
|
|
setDeleteId(null);
|
|
loadData();
|
|
};
|
|
|
|
return (
|
|
<div className="p-6 space-y-4">
|
|
{/* Toolbar */}
|
|
<div className="flex items-center justify-between gap-4">
|
|
<div className="flex items-center gap-3 flex-1">
|
|
<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>
|
|
<Select value={selectedType} onValueChange={setSelectedType}>
|
|
<SelectTrigger className="w-32 h-9 bg-slate-800/50 border-slate-700">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">全部类型</SelectItem>
|
|
<SelectItem value="public">公有云</SelectItem>
|
|
<SelectItem value="private">私有云</SelectItem>
|
|
<SelectItem value="hybrid">混合云</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</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">
|
|
<CloudCog 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 gap-4">
|
|
{filtered.map((p) => (
|
|
<div
|
|
key={p.id}
|
|
className="bg-slate-800/30 rounded-lg p-4 hover:bg-slate-800/50 transition-colors group"
|
|
>
|
|
<div className="flex items-start justify-between mb-3">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 rounded-lg bg-blue-500/20 flex items-center justify-center">
|
|
<CloudCog className="h-5 w-5 text-blue-400" />
|
|
</div>
|
|
<div>
|
|
<h3 className="font-medium text-slate-100">{p.name}</h3>
|
|
<span className="text-xs text-slate-500 font-mono">{p.code}</span>
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
{p.website && (
|
|
<a href={p.website} target="_blank" rel="noreferrer" className="p-1.5 rounded hover:bg-slate-700 text-slate-400 hover:text-slate-200">
|
|
<ExternalLink className="h-3.5 w-3.5" />
|
|
</a>
|
|
)}
|
|
<button onClick={() => openEdit(p)} 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={() => handleDelete(p.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>
|
|
|
|
{p.description && (
|
|
<p className="text-sm text-slate-400 mb-3 line-clamp-2">{p.description}</p>
|
|
)}
|
|
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
<Badge variant="outline" className={typeColors[p.type] || 'text-slate-400'}>
|
|
{typeLabels[p.type] || p.type}
|
|
</Badge>
|
|
{p.status === 'active' ? (
|
|
<Badge className="bg-emerald-500/20 text-emerald-400 gap-1">
|
|
<CheckCircle2 className="h-3 w-3" /> 启用
|
|
</Badge>
|
|
) : (
|
|
<Badge className="bg-slate-500/20 text-slate-400 gap-1">
|
|
<XCircle className="h-3 w-3" /> 禁用
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Dialog */}
|
|
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>{editingItem ? '编辑云平台' : '添加云平台'}</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="space-y-4 py-2">
|
|
<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="如:aliyun"
|
|
className="bg-slate-800/50 border-slate-700"
|
|
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="public">公有云</SelectItem>
|
|
<SelectItem value="private">私有云</SelectItem>
|
|
<SelectItem value="hybrid">混合云</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label>状态</Label>
|
|
<Select value={formData.status} onValueChange={(v) => setFormData({ ...formData, status: v })}>
|
|
<SelectTrigger className="bg-slate-800/50 border-slate-700">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="active">启用</SelectItem>
|
|
<SelectItem value="inactive">禁用</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label>官网地址</Label>
|
|
<Input
|
|
value={formData.website}
|
|
onChange={(e) => setFormData({ ...formData, website: e.target.value })}
|
|
placeholder="https://..."
|
|
className="bg-slate-800/50 border-slate-700"
|
|
/>
|
|
</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>
|
|
<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>
|
|
);
|
|
}
|