- 新增许可证服务器 (license-server/) - 新增 Agent 部署模块 (flnxi-agent/) - 添加许可证验证和激活页面 - 添加 Harbor 镜像同步脚本 - 更新素材系统支持许可证验证
157 lines
7.2 KiB
TypeScript
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>
|
|
);
|
|
}
|