629 lines
24 KiB
HTML
629 lines
24 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>FLNXI 素材系统 - 管理面板</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background: #f5f5f5;
|
|
min-height: 100vh;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 20px;
|
|
}
|
|
.container {
|
|
background: white;
|
|
border-radius: 8px;
|
|
padding: 32px;
|
|
max-width: 520px;
|
|
width: 100%;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
}
|
|
.header {
|
|
text-align: center;
|
|
margin-bottom: 24px;
|
|
}
|
|
h1 {
|
|
color: #333;
|
|
font-size: 20px;
|
|
font-weight: 600;
|
|
margin-bottom: 4px;
|
|
}
|
|
.subtitle {
|
|
color: #666;
|
|
font-size: 14px;
|
|
}
|
|
.tabs {
|
|
display: flex;
|
|
border-bottom: 1px solid #e9ecef;
|
|
margin-bottom: 20px;
|
|
}
|
|
.tab {
|
|
flex: 1;
|
|
padding: 12px;
|
|
text-align: center;
|
|
cursor: pointer;
|
|
color: #666;
|
|
font-size: 14px;
|
|
border-bottom: 2px solid transparent;
|
|
transition: all 0.2s;
|
|
}
|
|
.tab:hover { color: #1890ff; }
|
|
.tab.active {
|
|
color: #1890ff;
|
|
border-bottom-color: #1890ff;
|
|
font-weight: 500;
|
|
}
|
|
.tab-content { display: none; }
|
|
.tab-content.active { display: block; }
|
|
.info-card {
|
|
background: #f8f9fa;
|
|
border: 1px solid #e9ecef;
|
|
border-radius: 6px;
|
|
padding: 16px;
|
|
margin-bottom: 20px;
|
|
}
|
|
.info-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
padding: 6px 0;
|
|
font-size: 13px;
|
|
border-bottom: 1px solid #e9ecef;
|
|
}
|
|
.info-row:last-child { border-bottom: none; }
|
|
.info-label { color: #666; }
|
|
.info-value { color: #333; font-weight: 500; }
|
|
.form-group { margin-bottom: 16px; }
|
|
label {
|
|
display: block;
|
|
margin-bottom: 6px;
|
|
color: #333;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
}
|
|
.label-hint {
|
|
font-weight: normal;
|
|
color: #999;
|
|
font-size: 12px;
|
|
}
|
|
input, select {
|
|
width: 100%;
|
|
padding: 10px 12px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 6px;
|
|
font-size: 14px;
|
|
transition: border-color 0.2s;
|
|
}
|
|
input:focus, select:focus {
|
|
outline: none;
|
|
border-color: #1890ff;
|
|
}
|
|
input::placeholder { color: #bbb; }
|
|
.btn {
|
|
width: 100%;
|
|
padding: 12px;
|
|
background: #1890ff;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 6px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: background 0.2s;
|
|
margin-top: 8px;
|
|
}
|
|
.btn:hover { background: #40a9ff; }
|
|
.btn:disabled { background: #d9d9d9; cursor: not-allowed; }
|
|
.btn-success { background: #52c41a; }
|
|
.btn-success:hover { background: #73d13d; }
|
|
.btn-warning { background: #faad14; }
|
|
.btn-warning:hover { background: #ffc53d; }
|
|
.progress { display: none; margin-top: 20px; }
|
|
.progress-bar {
|
|
height: 6px;
|
|
background: #f0f0f0;
|
|
border-radius: 3px;
|
|
overflow: hidden;
|
|
}
|
|
.progress-fill {
|
|
height: 100%;
|
|
background: #1890ff;
|
|
width: 0%;
|
|
transition: width 0.3s;
|
|
}
|
|
.progress-text {
|
|
margin-top: 8px;
|
|
color: #666;
|
|
font-size: 13px;
|
|
text-align: center;
|
|
}
|
|
.status {
|
|
padding: 12px;
|
|
border-radius: 6px;
|
|
margin-top: 16px;
|
|
display: none;
|
|
font-size: 14px;
|
|
}
|
|
.status.success { background: #f6ffed; color: #52c41a; border: 1px solid #b7eb8f; }
|
|
.status.error { background: #fff2f0; color: #ff4d4f; border: 1px solid #ffccc7; }
|
|
.status.info { background: #e6f7ff; color: #1890ff; border: 1px solid #91d5ff; }
|
|
.tip {
|
|
font-size: 12px;
|
|
color: #999;
|
|
text-align: center;
|
|
margin-top: 16px;
|
|
}
|
|
.version-badge {
|
|
display: inline-block;
|
|
padding: 2px 8px;
|
|
border-radius: 4px;
|
|
font-size: 12px;
|
|
margin-left: 8px;
|
|
}
|
|
.version-current { background: #e6f7ff; color: #1890ff; }
|
|
.version-new { background: #f6ffed; color: #52c41a; }
|
|
.update-available {
|
|
background: #fffbe6;
|
|
border: 1px solid #ffe58f;
|
|
border-radius: 6px;
|
|
padding: 12px;
|
|
margin-bottom: 16px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
.update-info { font-size: 13px; color: #d48806; }
|
|
.container-list {
|
|
margin-top: 16px;
|
|
}
|
|
.container-item {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 12px;
|
|
background: #f8f9fa;
|
|
border: 1px solid #e9ecef;
|
|
border-radius: 6px;
|
|
margin-bottom: 8px;
|
|
}
|
|
.container-name { font-weight: 500; color: #333; }
|
|
.container-status {
|
|
font-size: 12px;
|
|
padding: 2px 8px;
|
|
border-radius: 4px;
|
|
}
|
|
.container-status.running { background: #f6ffed; color: #52c41a; }
|
|
.container-status.stopped { background: #fff2f0; color: #ff4d4f; }
|
|
.container-actions button {
|
|
padding: 4px 12px;
|
|
font-size: 12px;
|
|
margin-left: 8px;
|
|
border: 1px solid #d9d9d9;
|
|
background: white;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
}
|
|
.container-actions button:hover { border-color: #1890ff; color: #1890ff; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>FLNXI 素材系统</h1>
|
|
<p class="subtitle">管理面板</p>
|
|
</div>
|
|
|
|
<div class="tabs">
|
|
<div class="tab active" data-tab="status">系统状态</div>
|
|
<div class="tab" data-tab="install">安装</div>
|
|
<div class="tab" data-tab="update">更新</div>
|
|
</div>
|
|
|
|
<!-- 系统状态 Tab -->
|
|
<div class="tab-content active" id="tab-status">
|
|
<div class="info-card" id="sysInfo">
|
|
<div class="info-row">
|
|
<span class="info-label">系统信息</span>
|
|
<span class="info-value">加载中...</span>
|
|
</div>
|
|
</div>
|
|
<div class="container-list" id="containerList">
|
|
<div style="text-align: center; color: #999; padding: 20px;">加载中...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 安装 Tab -->
|
|
<div class="tab-content" id="tab-install">
|
|
<form id="installForm">
|
|
<div class="form-group">
|
|
<label>许可证密钥 <span class="label-hint">(可选,留空免费使用)</span></label>
|
|
<input type="text" id="licenseKey" placeholder="XXXX-XXXX-XXXX-XXXX">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>数据存储路径</label>
|
|
<input type="text" id="dataPath" value="/opt/flnxi" placeholder="/opt/flnxi">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Web 端口</label>
|
|
<input type="number" id="webPort" value="3000" min="1" max="65535">
|
|
</div>
|
|
<button type="submit" class="btn" id="installBtn">开始安装</button>
|
|
</form>
|
|
<div class="progress" id="installProgress">
|
|
<div class="progress-bar"><div class="progress-fill" id="installProgressFill"></div></div>
|
|
<p class="progress-text" id="installProgressText">准备中...</p>
|
|
</div>
|
|
<div class="status" id="installStatus"></div>
|
|
<p class="tip">安装过程需要几分钟,请耐心等待</p>
|
|
</div>
|
|
|
|
<!-- 更新 Tab -->
|
|
<div class="tab-content" id="tab-update">
|
|
<div class="info-card" id="versionInfo">
|
|
<div class="info-row">
|
|
<span class="info-label">当前版本</span>
|
|
<span class="info-value" id="currentVersion">检测中...</span>
|
|
</div>
|
|
<div class="info-row">
|
|
<span class="info-label">最新版本</span>
|
|
<span class="info-value" id="latestVersion">检测中...</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="update-available" id="updateAvailable" style="display: none;">
|
|
<span class="update-info">🎉 发现新版本,建议更新</span>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>目标版本</label>
|
|
<select id="targetVersion">
|
|
<option value="">检测中...</option>
|
|
</select>
|
|
</div>
|
|
|
|
<button class="btn btn-success" id="updateBtn" disabled>检查更新中...</button>
|
|
|
|
<div class="progress" id="updateProgress">
|
|
<div class="progress-bar"><div class="progress-fill" id="updateProgressFill"></div></div>
|
|
<p class="progress-text" id="updateProgressText">准备中...</p>
|
|
</div>
|
|
<div class="status" id="updateStatus"></div>
|
|
<p class="tip">更新过程中服务会短暂中断,请确保没有用户正在使用</p>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const API = '';
|
|
let currentVersion = '';
|
|
let latestVersion = '';
|
|
|
|
// Tab 切换
|
|
document.querySelectorAll('.tab').forEach(tab => {
|
|
tab.addEventListener('click', () => {
|
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
|
tab.classList.add('active');
|
|
document.getElementById('tab-' + tab.dataset.tab).classList.add('active');
|
|
});
|
|
});
|
|
|
|
// 获取系统信息
|
|
async function getSystemInfo() {
|
|
try {
|
|
const res = await fetch(API + '/api/info');
|
|
const data = await res.json();
|
|
document.getElementById('sysInfo').innerHTML = `
|
|
<div class="info-row">
|
|
<span class="info-label">操作系统</span>
|
|
<span class="info-value">${data.os} / ${data.arch}</span>
|
|
</div>
|
|
<div class="info-row">
|
|
<span class="info-label">Docker</span>
|
|
<span class="info-value">${data.dockerVersion}</span>
|
|
</div>
|
|
<div class="info-row">
|
|
<span class="info-label">CPU</span>
|
|
<span class="info-value">${data.cpus} 核</span>
|
|
</div>
|
|
<div class="info-row">
|
|
<span class="info-label">内存</span>
|
|
<span class="info-value">${(data.memTotal / 1024 / 1024 / 1024).toFixed(1)} GB</span>
|
|
</div>
|
|
`;
|
|
} catch (e) {
|
|
document.getElementById('sysInfo').innerHTML = `
|
|
<div class="info-row">
|
|
<span class="info-label">状态</span>
|
|
<span class="info-value" style="color:#ff4d4f">无法连接到 Agent</span>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
// 获取容器列表
|
|
async function getContainers() {
|
|
try {
|
|
const res = await fetch(API + '/api/containers');
|
|
const containers = await res.json();
|
|
|
|
if (containers.length === 0) {
|
|
document.getElementById('containerList').innerHTML = `
|
|
<div style="text-align: center; color: #999; padding: 20px;">
|
|
未检测到已安装的服务,请先安装
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
let html = '<h4 style="margin-bottom: 12px; color: #333; font-size: 14px;">运行中的服务</h4>';
|
|
containers.forEach(c => {
|
|
const name = c.name.replace(/^\//, '');
|
|
const isRunning = c.state === 'running';
|
|
html += `
|
|
<div class="container-item">
|
|
<div>
|
|
<span class="container-name">${name}</span>
|
|
<span class="container-status ${isRunning ? 'running' : 'stopped'}">${isRunning ? '运行中' : '已停止'}</span>
|
|
</div>
|
|
<div class="container-actions">
|
|
${isRunning
|
|
? `<button onclick="restartContainer('${name}')">重启</button>`
|
|
: `<button onclick="startContainer('${name}')">启动</button>`
|
|
}
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// 获取素材系统版本
|
|
if (name === 'flnxi-material' && c.image) {
|
|
const match = c.image.match(/:([^:]+)$/);
|
|
currentVersion = match ? match[1] : 'unknown';
|
|
}
|
|
});
|
|
document.getElementById('containerList').innerHTML = html;
|
|
} catch (e) {
|
|
document.getElementById('containerList').innerHTML = `
|
|
<div style="text-align: center; color: #ff4d4f; padding: 20px;">
|
|
获取容器列表失败
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
// 检查版本更新
|
|
async function checkVersion() {
|
|
try {
|
|
// 获取当前版本
|
|
await getContainers();
|
|
document.getElementById('currentVersion').innerHTML = currentVersion
|
|
? `${currentVersion} <span class="version-badge version-current">当前</span>`
|
|
: '未安装';
|
|
|
|
// 从 Agent API 获取可用版本列表
|
|
let versions = [];
|
|
latestVersion = '';
|
|
|
|
try {
|
|
const versionRes = await fetch(API + '/api/versions');
|
|
if (versionRes.ok) {
|
|
const versionData = await versionRes.json();
|
|
if (versionData.versions && versionData.versions.length > 0) {
|
|
versions = versionData.versions;
|
|
}
|
|
if (versionData.latest) {
|
|
latestVersion = versionData.latest;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.warn('获取版本列表失败:', e);
|
|
}
|
|
|
|
document.getElementById('latestVersion').innerHTML = latestVersion
|
|
? `${latestVersion} <span class="version-badge version-new">最新</span>`
|
|
: '未配置';
|
|
|
|
const select = document.getElementById('targetVersion');
|
|
select.innerHTML = versions.map(v =>
|
|
`<option value="${v}" ${v === latestVersion ? 'selected' : ''}>${v}${v === latestVersion ? ' (推荐)' : ''}</option>`
|
|
).join('');
|
|
|
|
const btn = document.getElementById('updateBtn');
|
|
if (versions.length === 0) {
|
|
btn.textContent = '未配置版本来源';
|
|
btn.disabled = true;
|
|
btn.classList.remove('btn-success');
|
|
return;
|
|
}
|
|
if (currentVersion && latestVersion && currentVersion !== latestVersion) {
|
|
document.getElementById('updateAvailable').style.display = 'flex';
|
|
btn.textContent = '立即更新';
|
|
btn.disabled = false;
|
|
} else if (currentVersion && latestVersion && currentVersion === latestVersion) {
|
|
btn.textContent = '已是最新版本';
|
|
btn.disabled = true;
|
|
btn.classList.remove('btn-success');
|
|
} else {
|
|
btn.textContent = '开始更新';
|
|
btn.disabled = false;
|
|
}
|
|
} catch (e) {
|
|
console.error('检查版本失败:', e);
|
|
}
|
|
}
|
|
|
|
// 执行更新
|
|
document.getElementById('updateBtn').addEventListener('click', async () => {
|
|
const version = document.getElementById('targetVersion').value;
|
|
if (!version) return;
|
|
|
|
const btn = document.getElementById('updateBtn');
|
|
const progress = document.getElementById('updateProgress');
|
|
const status = document.getElementById('updateStatus');
|
|
|
|
btn.disabled = true;
|
|
btn.textContent = '更新中...';
|
|
progress.style.display = 'block';
|
|
status.style.display = 'none';
|
|
|
|
try {
|
|
await fetch(API + '/api/update', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ version, authToken: '' })
|
|
});
|
|
pollUpdateStatus();
|
|
} catch (e) {
|
|
showUpdateError('更新请求失败: ' + e.message);
|
|
btn.disabled = false;
|
|
btn.textContent = '重试更新';
|
|
}
|
|
});
|
|
|
|
async function pollUpdateStatus() {
|
|
try {
|
|
const res = await fetch(API + '/api/update/status');
|
|
const data = await res.json();
|
|
|
|
document.getElementById('updateProgressFill').style.width = data.progress + '%';
|
|
document.getElementById('updateProgressText').textContent = data.message || data.step;
|
|
|
|
if (data.status === 'running') {
|
|
setTimeout(pollUpdateStatus, 1000);
|
|
} else if (data.status === 'success') {
|
|
showUpdateSuccess();
|
|
} else if (data.status === 'failed') {
|
|
showUpdateError(data.error);
|
|
}
|
|
} catch (e) {
|
|
showUpdateError('获取状态失败');
|
|
}
|
|
}
|
|
|
|
function showUpdateSuccess() {
|
|
const status = document.getElementById('updateStatus');
|
|
status.className = 'status success';
|
|
status.innerHTML = '✓ 更新完成!页面将在 3 秒后刷新...';
|
|
status.style.display = 'block';
|
|
setTimeout(() => location.reload(), 3000);
|
|
}
|
|
|
|
function showUpdateError(msg) {
|
|
const status = document.getElementById('updateStatus');
|
|
status.className = 'status error';
|
|
status.textContent = msg;
|
|
status.style.display = 'block';
|
|
document.getElementById('updateBtn').disabled = false;
|
|
document.getElementById('updateBtn').textContent = '重试更新';
|
|
}
|
|
|
|
// 容器操作
|
|
async function restartContainer(name) {
|
|
if (!confirm(`确定要重启 ${name} 吗?`)) return;
|
|
try {
|
|
await fetch(API + `/api/containers/${name}/restart`, { method: 'POST' });
|
|
alert('重启成功');
|
|
getContainers();
|
|
} catch (e) {
|
|
alert('重启失败: ' + e.message);
|
|
}
|
|
}
|
|
|
|
async function startContainer(name) {
|
|
try {
|
|
await fetch(API + `/api/containers/${name}/start`, { method: 'POST' });
|
|
alert('启动成功');
|
|
getContainers();
|
|
} catch (e) {
|
|
alert('启动失败: ' + e.message);
|
|
}
|
|
}
|
|
|
|
// 安装表单
|
|
document.getElementById('installForm').onsubmit = async (e) => {
|
|
e.preventDefault();
|
|
const btn = document.getElementById('installBtn');
|
|
const progress = document.getElementById('installProgress');
|
|
const status = document.getElementById('installStatus');
|
|
|
|
btn.disabled = true;
|
|
progress.style.display = 'block';
|
|
status.style.display = 'none';
|
|
|
|
try {
|
|
await fetch(API + '/api/install', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
licenseKey: document.getElementById('licenseKey').value || '',
|
|
dataPath: document.getElementById('dataPath').value,
|
|
webPort: parseInt(document.getElementById('webPort').value)
|
|
})
|
|
});
|
|
pollInstallStatus();
|
|
} catch (e) {
|
|
showInstallError('安装请求失败: ' + e.message);
|
|
btn.disabled = false;
|
|
}
|
|
};
|
|
|
|
async function pollInstallStatus() {
|
|
try {
|
|
const res = await fetch(API + '/api/install/status');
|
|
const data = await res.json();
|
|
|
|
document.getElementById('installProgressFill').style.width = data.progress + '%';
|
|
document.getElementById('installProgressText').textContent = data.message || data.step;
|
|
|
|
if (data.status === 'running') {
|
|
setTimeout(pollInstallStatus, 1000);
|
|
} else if (data.status === 'success') {
|
|
showInstallSuccess();
|
|
} else if (data.status === 'failed') {
|
|
showInstallError(data.error);
|
|
}
|
|
} catch (e) {
|
|
showInstallError('获取状态失败');
|
|
}
|
|
}
|
|
|
|
function showInstallSuccess() {
|
|
const status = document.getElementById('installStatus');
|
|
const port = document.getElementById('webPort').value;
|
|
status.className = 'status success';
|
|
status.innerHTML = `
|
|
<div style="margin-bottom: 12px; font-weight: 500;">✓ 安装完成!</div>
|
|
<div style="background: #fff; padding: 12px; border-radius: 4px; margin-bottom: 12px;">
|
|
<div style="margin-bottom: 8px; color: #333;">访问地址:<a href="http://${location.hostname}:${port}" target="_blank" style="color: #1890ff;">http://${location.hostname}:${port}</a></div>
|
|
<div style="border-top: 1px solid #e9ecef; padding-top: 8px; margin-top: 8px;">
|
|
<div style="color: #666; font-size: 12px; margin-bottom: 4px;">管理员账号说明:</div>
|
|
<div style="color: #333;">初始管理员信息请以当前安装流程返回结果为准。</div>
|
|
<div style="color: #333;">系统不再内置固定默认密码。</div>
|
|
</div>
|
|
</div>
|
|
<div style="font-size: 12px; color: #52c41a;">首次登录后请立即完成密码修改</div>
|
|
`;
|
|
status.style.display = 'block';
|
|
document.getElementById('installBtn').disabled = false;
|
|
getContainers();
|
|
}
|
|
|
|
function showInstallError(msg) {
|
|
const status = document.getElementById('installStatus');
|
|
status.className = 'status error';
|
|
status.textContent = msg;
|
|
status.style.display = 'block';
|
|
document.getElementById('installBtn').disabled = false;
|
|
}
|
|
|
|
// 初始化
|
|
getSystemInfo();
|
|
getContainers();
|
|
checkVersion();
|
|
</script>
|
|
</body>
|
|
</html>
|