refactor: 标准化项目结构 frontend/ + backend/ + SQLite + run.sh

- 目录重构: app/ -> frontend/,与 backend/ 并列
- 数据库: 默认 SQLite(零配置),修复 DSN() bug
- run.sh: 一键启动前后端,支持 start/stop/restart/status
- Makefile: install/dev/build/lint/clean 快捷命令
- vite.config.ts: 配置 dev server 端口 5173 + API 代理到 8080
- config.yaml: SQLite 默认配置,开箱即用
- docs/: 完整开发文档(项目结构/API/数据库/规范/部署)
- 修复: GORM foreign key 关联声明
- 修复: go get gorm.io/driver/sqlite 依赖
This commit is contained in:
attacker committed 2026-06-13 23:34:23 +08:00
1 parent 7d6674bddd
commit 97c4207ff8
132 files changed
+1572 -34

No files matched your search

@@ -0,0 +1,338 @@
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Checkbox } from '@/components/ui/checkbox';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from '@/components/ui/dialog';
import {
Cloud,
Key,
CheckCircle,
ChevronRight,
ChevronLeft,
Server,
RefreshCw,
Check,
} from 'lucide-react';
import { cloudAccounts } from '@/mocks/data';
interface CloudServerImportProps {
open: boolean;
onClose: () => void;
}
const cloudProviders = [
{ id: 'aws', name: 'AWS', color: 'bg-orange-500', icon: 'AWS' },
{ id: 'aliyun', name: '阿里云', color: 'bg-orange-600', icon: 'AL' },
{ id: 'tencent', name: '腾讯云', color: 'bg-blue-500', icon: 'TX' },
{ id: 'huawei', name: '华为云', color: 'bg-red-500', icon: 'HW' },
{ id: 'azure', name: 'Azure', color: 'bg-blue-600', icon: 'AZ' },
];
// Mock cloud servers data
const mockCloudServers = [
{ id: 'ecs-001', name: 'i-bp1a2b3c4d5e6f7g8', hostname: 'web-server-01', ip: '47.98.123.45', region: 'cn-hangzhou', status: 'running', os: 'CentOS 7.9', spec: '2核4G' },
{ id: 'ecs-002', name: 'i-bp2b3c4d5e6f7g8h9', hostname: 'web-server-02', ip: '47.98.123.46', region: 'cn-hangzhou', status: 'running', os: 'CentOS 7.9', spec: '2核4G' },
{ id: 'ecs-003', name: 'i-bp3c4d5e6f7g8h9i0', hostname: 'app-server-01', ip: '47.98.123.47', region: 'cn-shanghai', status: 'stopped', os: 'Ubuntu 20.04', spec: '4核8G' },
{ id: 'ecs-004', name: 'i-bp4d5e6f7g8h9i0j1', hostname: 'db-server-01', ip: '47.98.123.48', region: 'cn-beijing', status: 'running', os: 'CentOS 8.2', spec: '8核16G' },
{ id: 'ecs-005', name: 'i-bp5e6f7g8h9i0j1k2', hostname: 'cache-server-01', ip: '47.98.123.49', region: 'cn-hangzhou', status: 'running', os: 'CentOS 7.9', spec: '2核8G' },
];
export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
const [step, setStep] = useState(1);
const [selectedProvider, setSelectedProvider] = useState<string>('');
const [selectedAccount, setSelectedAccount] = useState<string>('');
const [selectedServers, setSelectedServers] = useState<string[]>([]);
const [isLoading, setIsLoading] = useState(false);
const filteredAccounts = cloudAccounts.filter(
acc => acc.provider === selectedProvider && acc.status === 'active'
);
const handleNext = () => {
if (step < 3) {
if (step === 2) {
setIsLoading(true);
setTimeout(() => setIsLoading(false), 1000);
}
setStep(step + 1);
}
};
const handleBack = () => {
if (step > 1) {
setStep(step - 1);
}
};
const handleServerToggle = (serverId: string) => {
setSelectedServers(prev =>
prev.includes(serverId)
? prev.filter(id => id !== serverId)
: [...prev, serverId]
);
};
const handleSelectAll = () => {
if (selectedServers.length === mockCloudServers.length) {
setSelectedServers([]);
} else {
setSelectedServers(mockCloudServers.map(s => s.id));
}
};
const handleImport = () => {
// Mock import action
setTimeout(() => {
onClose();
setStep(1);
setSelectedProvider('');
setSelectedAccount('');
setSelectedServers([]);
}, 1500);
};
const handleClose = () => {
onClose();
setStep(1);
setSelectedProvider('');
setSelectedAccount('');
setSelectedServers([]);
};
const steps = [
{ id: 1, title: '公有云', icon: Cloud },
{ id: 2, title: '访问凭据', icon: Key },
{ id: 3, title: '导入确认', icon: CheckCircle },
];
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="max-w-3xl bg-slate-900 border-slate-700 text-slate-100">
<DialogHeader>
<DialogTitle className="text-xl">导入云主机</DialogTitle>
</DialogHeader>
{/* Step Indicator */}
<div className="flex items-center justify-center mb-6">
{steps.map((s, index) => (
<div key={s.id} className="flex items-center">
<div className={`flex items-center gap-2 px-4 py-2 rounded-lg ${
step === s.id
? 'bg-blue-600 text-white'
: step > s.id
? 'bg-emerald-600/20 text-emerald-400'
: 'bg-slate-800 text-slate-500'
}`}>
<s.icon className="h-4 w-4" />
<span className="text-sm font-medium">{s.title}</span>
{step > s.id && <Check className="h-4 w-4 ml-1" />}
</div>
{index < steps.length - 1 && (
<ChevronRight className="h-5 w-5 text-slate-600 mx-2" />
)}
</div>
))}
</div>
{/* Step 1: Select Cloud Provider */}
{step === 1 && (
<div className="space-y-4">
<p className="text-slate-400 text-sm">请选择要导入云主机的云平台</p>
<div className="grid grid-cols-3 gap-4">
{cloudProviders.map((provider) => (
<Card
key={provider.id}
className={`cursor-pointer transition-all ${
selectedProvider === provider.id
? 'border-blue-500 ring-2 ring-blue-500/30 bg-slate-800'
: 'border-slate-700 hover:border-slate-600 bg-slate-800/50'
}`}
onClick={() => setSelectedProvider(provider.id)}
>
<CardContent className="p-4 flex items-center gap-3">
<div className={`w-10 h-10 rounded-lg ${provider.color} flex items-center justify-center text-white font-bold text-sm`}>
{provider.icon}
</div>
<span className="font-medium text-slate-200">{provider.name}</span>
</CardContent>
</Card>
))}
</div>
</div>
)}
{/* Step 2: Select Account */}
{step === 2 && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<p className="text-slate-400 text-sm">请选择访问凭据(AccessKey)</p>
<Button variant="outline" size="sm" className="border-slate-700 text-slate-300">
<RefreshCw className="h-3 w-3 mr-1" />
刷新
</Button>
</div>
{filteredAccounts.length > 0 ? (
<div className="space-y-2 max-h-[300px] overflow-y-auto">
{filteredAccounts.map((account) => (
<Card
key={account.id}
className={`cursor-pointer transition-all ${
selectedAccount === account.id
? 'border-blue-500 ring-1 ring-blue-500/30 bg-slate-800'
: 'border-slate-700 hover:border-slate-600 bg-slate-800/50'
}`}
onClick={() => setSelectedAccount(account.id)}
>
<CardContent className="p-3 flex items-center justify-between">
<div className="flex items-center gap-3">
<Key className="h-5 w-5 text-blue-400" />
<div>
<p className="font-medium text-slate-200">{account.name}</p>
<p className="text-xs text-slate-500">{account.accessKey.slice(0, 20)}...</p>
</div>
</div>
<div className="text-right">
<Badge className="bg-emerald-500/20 text-emerald-400 border-0">
{account.regionCount} 区域
</Badge>
<p className="text-xs text-slate-500 mt-1">{account.serverCount} 服务器</p>
</div>
</CardContent>
</Card>
))}
</div>
) : (
<div className="text-center py-8 text-slate-500">
<Key className="h-12 w-12 mx-auto mb-3 opacity-50" />
<p>该云平台暂无有效的访问凭据</p>
<Button variant="outline" className="mt-3 border-slate-700 text-slate-300">
添加访问凭据
</Button>
</div>
)}
</div>
)}
{/* Step 3: Import Confirmation */}
{step === 3 && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<p className="text-slate-400 text-sm">请选择要导入的云主机</p>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" className="border-slate-700 text-slate-300" onClick={handleSelectAll}>
{selectedServers.length === mockCloudServers.length ? '取消全选' : '全选'}
</Button>
<Badge className="bg-blue-500/20 text-blue-400 border-0">
已选 {selectedServers.length} 台
</Badge>
</div>
</div>
{isLoading ? (
<div className="text-center py-12">
<RefreshCw className="h-8 w-8 mx-auto mb-3 animate-spin text-blue-400" />
<p className="text-slate-400">正在获取云主机列表...</p>
</div>
) : (
<div className="border rounded-lg border-slate-700 overflow-hidden max-h-[350px] overflow-y-auto">
<table className="w-full">
<thead className="bg-slate-800 sticky top-0">
<tr>
<th className="px-3 py-2 text-left text-xs font-medium text-slate-400 w-10">
<Checkbox
checked={selectedServers.length === mockCloudServers.length && mockCloudServers.length > 0}
onCheckedChange={handleSelectAll}
/>
</th>
<th className="px-3 py-2 text-left text-xs font-medium text-slate-400">实例ID</th>
<th className="px-3 py-2 text-left text-xs font-medium text-slate-400">主机名</th>
<th className="px-3 py-2 text-left text-xs font-medium text-slate-400">IP地址</th>
<th className="px-3 py-2 text-left text-xs font-medium text-slate-400">区域</th>
<th className="px-3 py-2 text-left text-xs font-medium text-slate-400">规格</th>
<th className="px-3 py-2 text-left text-xs font-medium text-slate-400">状态</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800">
{mockCloudServers.map((server) => (
<tr
key={server.id}
className={`hover:bg-slate-800/50 ${selectedServers.includes(server.id) ? 'bg-blue-500/10' : ''}`}
>
<td className="px-3 py-2">
<Checkbox
checked={selectedServers.includes(server.id)}
onCheckedChange={() => handleServerToggle(server.id)}
/>
</td>
<td className="px-3 py-2 text-sm text-slate-300">{server.name}</td>
<td className="px-3 py-2 text-sm text-slate-200">{server.hostname}</td>
<td className="px-3 py-2 text-sm text-slate-400">{server.ip}</td>
<td className="px-3 py-2 text-sm text-slate-400">{server.region}</td>
<td className="px-3 py-2 text-sm text-slate-400">{server.spec}</td>
<td className="px-3 py-2">
<Badge
variant="outline"
className={server.status === 'running'
? 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10'
: 'border-slate-500/30 text-slate-400 bg-slate-500/10'
}
>
{server.status === 'running' ? '运行中' : '已停止'}
</Badge>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
)}
{/* Footer */}
<DialogFooter className="flex justify-between items-center">
<div>
{step > 1 && (
<Button variant="outline" onClick={handleBack} className="border-slate-700 text-slate-300">
<ChevronLeft className="h-4 w-4 mr-1" />
上一步
</Button>
)}
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={handleClose} className="border-slate-700 text-slate-300">
取消
</Button>
{step < 3 ? (
<Button
onClick={handleNext}
disabled={(step === 1 && !selectedProvider) || (step === 2 && !selectedAccount)}
className="bg-blue-600 hover:bg-blue-700"
>
下一步
<ChevronRight className="h-4 w-4 ml-1" />
</Button>
) : (
<Button
onClick={handleImport}
disabled={selectedServers.length === 0}
className="bg-emerald-600 hover:bg-emerald-700"
>
<Check className="h-4 w-4 mr-1" />
确认导入 ({selectedServers.length})
</Button>
)}
</div>
</DialogFooter>
</DialogContent>
</Dialog>
);
}