WuYongJie ba98bf22cc feat: 添加许可证系统、Agent部署模块和Harbor镜像管理
- 新增许可证服务器 (license-server/)
- 新增 Agent 部署模块 (flnxi-agent/)
- 添加许可证验证和激活页面
- 添加 Harbor 镜像同步脚本
- 更新素材系统支持许可证验证
2025-12-15 21:19:46 +08:00

157 lines
7.2 KiB
TypeScript

'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
interface Version {
id: string;
version: string;
releaseNotes: string | null;
minRequiredVersion: string | null;
isSecurityUpdate: boolean;
isCriticalUpdate: boolean;
downloadSize: string | null;
releaseDate: string;
}
export default function VersionsPage() {
const router = useRouter();
const [versions, setVersions] = useState<Version[]>([]);
const [loading, setLoading] = useState(true);
const [showModal, setShowModal] = useState(false);
useEffect(() => {
fetchVersions();
}, []);
const fetchVersions = async () => {
try {
const res = await fetch('/api/admin/versions');
if (res.status === 401) {
router.push('/admin/login');
return;
}
const data = await res.json();
setVersions(data.versions || []);
} catch (error) {
console.error('获取版本列表失败:', error);
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-gray-100">
<nav className="bg-white shadow">
<div className="max-w-7xl mx-auto px-4 py-4 flex justify-between items-center">
<div className="flex items-center gap-4">
<Link href="/admin" className="text-gray-600 hover:text-gray-900">← 返回</Link>
<h1 className="text-xl font-bold">版本管理</h1>
</div>
</div>
</nav>
<main className="max-w-7xl mx-auto px-4 py-8">
<div className="bg-white rounded-lg shadow">
<div className="p-6 border-b flex justify-between items-center">
<h2 className="font-medium">版本列表</h2>
<button onClick={() => setShowModal(true)} className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
发布新版本
</button>
</div>
{loading ? (
<div className="p-8 text-center text-gray-500">加载中...</div>
) : (
<table className="w-full">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">版本号</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">发布日期</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">最低要求版本</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">标签</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">更新说明</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{versions.map((v) => (
<tr key={v.id}>
<td className="px-6 py-4 font-mono font-medium">{v.version}</td>
<td className="px-6 py-4 text-gray-600">{new Date(v.releaseDate).toLocaleDateString()}</td>
<td className="px-6 py-4 text-gray-600">{v.minRequiredVersion || '-'}</td>
<td className="px-6 py-4">
{v.isSecurityUpdate && <span className="px-2 py-1 bg-red-100 text-red-700 rounded text-xs mr-1">安全更新</span>}
{v.isCriticalUpdate && <span className="px-2 py-1 bg-yellow-100 text-yellow-700 rounded text-xs">重要更新</span>}
</td>
<td className="px-6 py-4 text-gray-600 max-w-xs truncate">{v.releaseNotes || '-'}</td>
</tr>
))}
{!versions.length && (
<tr><td colSpan={5} className="px-6 py-8 text-center text-gray-500">暂无版本数据</td></tr>
)}
</tbody>
</table>
)}
</div>
</main>
{showModal && <VersionModal onClose={() => setShowModal(false)} onSuccess={() => { setShowModal(false); fetchVersions(); }} />}
</div>
);
}
function VersionModal({ onClose, onSuccess }: { onClose: () => void; onSuccess: () => void }) {
const [form, setForm] = useState({ version: '', releaseNotes: '', minRequiredVersion: '', isSecurityUpdate: false, isCriticalUpdate: false });
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError('');
try {
const res = await fetch('/api/admin/versions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(form),
});
const data = await res.json();
if (data.success) onSuccess();
else setError(data.error || '发布失败');
} catch { setError('网络错误'); }
finally { setLoading(false); }
};
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl w-full max-w-md">
<div className="p-6 border-b"><h2 className="text-lg font-medium">发布新版本</h2></div>
<form onSubmit={handleSubmit} className="p-6 space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">版本号 *</label>
<input type="text" value={form.version} onChange={(e) => setForm({ ...form, version: e.target.value })} className="w-full px-3 py-2 border rounded-lg" placeholder="1.0.0" required />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">更新说明</label>
<textarea value={form.releaseNotes} onChange={(e) => setForm({ ...form, releaseNotes: e.target.value })} className="w-full px-3 py-2 border rounded-lg" rows={3} />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">最低要求版本</label>
<input type="text" value={form.minRequiredVersion} onChange={(e) => setForm({ ...form, minRequiredVersion: e.target.value })} className="w-full px-3 py-2 border rounded-lg" placeholder="留空表示不强制更新" />
</div>
<div className="flex gap-4">
<label className="flex items-center gap-2"><input type="checkbox" checked={form.isSecurityUpdate} onChange={(e) => setForm({ ...form, isSecurityUpdate: e.target.checked })} /><span className="text-sm">安全更新</span></label>
<label className="flex items-center gap-2"><input type="checkbox" checked={form.isCriticalUpdate} onChange={(e) => setForm({ ...form, isCriticalUpdate: e.target.checked })} /><span className="text-sm">重要更新</span></label>
</div>
{error && <div className="text-red-600 text-sm bg-red-50 p-3 rounded-lg">{error}</div>}
<div className="flex gap-3 pt-4">
<button type="button" onClick={onClose} className="flex-1 py-2 border rounded-lg hover:bg-gray-50">取消</button>
<button type="submit" disabled={loading} className="flex-1 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50">{loading ? '发布中...' : '发布'}</button>
</div>
</form>
</div>
</div>
);
}