Files
2026-07-24 11:34:38 +08:00

268 lines
12 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>文件传输自动化 - 管理后台</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f6fa; color: #2d3436; }
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
h1 { font-size: 24px; margin-bottom: 20px; color: #2d3436; }
.tabs { display: flex; gap: 8px; margin-bottom: 20px; }
.tab { padding: 8px 16px; border: none; background: #dfe6e9; border-radius: 6px; cursor: pointer; font-size: 14px; }
.tab.active { background: #0984e3; color: white; }
.panel { display: none; }
.panel.active { display: block; }
.card { background: white; border-radius: 8px; padding: 20px; margin-bottom: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.card h3 { margin-bottom: 12px; font-size: 16px; }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
th, td { padding: 8px 12px; text-align: left; border-bottom: 1px solid #eee; }
th { background: #f8f9fa; font-weight: 600; }
.status-success { color: #00b894; }
.status-failed { color: #d63031; }
.status-running { color: #fdcb6e; }
.filters { display: flex; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; }
.filters input, .filters select { padding: 6px 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 13px; }
.btn { padding: 6px 14px; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; }
.btn-primary { background: #0984e3; color: white; }
.btn-danger { background: #d63031; color: white; }
.btn-sm { padding: 4px 8px; font-size: 12px; }
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; margin-bottom: 20px; }
.stat-item { background: white; padding: 16px; border-radius: 8px; text-align: center; }
.stat-item .num { font-size: 28px; font-weight: 700; color: #0984e3; }
.stat-item .label { font-size: 12px; color: #636e72; margin-top: 4px; }
.login-box { max-width: 320px; margin: 100px auto; }
.login-box input { width: 100%; padding: 10px; margin-bottom: 12px; border: 1px solid #ddd; border-radius: 6px; }
.login-box button { width: 100%; padding: 10px; background: #0984e3; color: white; border: none; border-radius: 6px; cursor: pointer; }
.form-row { display: flex; gap: 8px; margin-bottom: 8px; }
.form-row input { flex: 1; padding: 6px 10px; border: 1px solid #ddd; border-radius: 4px; }
.pagination { display: flex; gap: 8px; justify-content: center; margin-top: 12px; }
</style>
</head>
<body>
<div class="container" id="app" style="display:none">
<h1>文件传输自动化 · 管理后台</h1>
<div class="tabs">
<button class="tab active" onclick="switchTab('dashboard')">仪表盘</button>
<button class="tab" onclick="switchTab('logs')">操作日志</button>
<button class="tab" onclick="switchTab('commands')">预设命令</button>
<button class="tab" onclick="switchTab('config')">API配置</button>
</div>
<!-- 仪表盘 -->
<div class="panel active" id="panel-dashboard">
<div class="stats" id="stats"></div>
<div class="card">
<h3>系统信息</h3>
<div id="sysinfo"></div>
</div>
</div>
<!-- 操作日志 -->
<div class="panel" id="panel-logs">
<div class="card">
<div class="filters">
<select id="filter-type">
<option value="">全部类型</option>
<option value="file_in">文件传入</option>
<option value="file_out">文件传出</option>
<option value="group_permission">红区组权限</option>
</select>
<select id="filter-status">
<option value="">全部状态</option>
<option value="success">成功</option>
<option value="failed">失败</option>
<option value="running">执行中</option>
</select>
<input type="date" id="filter-start">
<input type="date" id="filter-end">
<input type="text" id="filter-keyword" placeholder="搜索关键词">
<button class="btn btn-primary" onclick="loadLogs()">查询</button>
</div>
<table>
<thead><tr><th>时间</th><th>类型</th><th>申请人</th><th>操作</th><th>状态</th><th>详情/错误</th></tr></thead>
<tbody id="logs-body"></tbody>
</table>
<div class="pagination" id="logs-pagination"></div>
</div>
</div>
<!-- 预设命令 -->
<div class="panel" id="panel-commands">
<div class="card">
<h3>添加预设命令</h3>
<div class="form-row">
<input type="text" id="cmd-name" placeholder="命令名称">
<input type="text" id="cmd-command" placeholder="命令内容">
<input type="text" id="cmd-target" placeholder="目标(默认ic1)" value="ic1">
<button class="btn btn-primary" onclick="addCommand()">添加</button>
</div>
</div>
<div class="card">
<table>
<thead><tr><th>ID</th><th>名称</th><th>命令</th><th>目标</th><th>状态</th><th>操作</th></tr></thead>
<tbody id="commands-body"></tbody>
</table>
</div>
</div>
<!-- API配置 -->
<div class="panel" id="panel-config">
<div class="card">
<h3>添加/更新配置</h3>
<div class="form-row">
<input type="text" id="cfg-key" placeholder="配置键名">
<input type="text" id="cfg-value" placeholder="配置值">
<input type="text" id="cfg-desc" placeholder="说明">
<button class="btn btn-primary" onclick="saveConfig()">保存</button>
</div>
</div>
<div class="card">
<table>
<thead><tr><th>键名</th><th></th><th>说明</th><th>更新时间</th></tr></thead>
<tbody id="config-body"></tbody>
</table>
</div>
</div>
</div>
<!-- 登录框 -->
<div class="login-box" id="login-box">
<h2 style="text-align:center;margin-bottom:20px">管理后台登录</h2>
<input type="password" id="login-password" placeholder="输入管理密码" onkeydown="if(event.key==='Enter')doLogin()">
<button onclick="doLogin()">登录</button>
</div>
<script>
let adminPassword = '';
let currentPage = 1;
function api(path, method = 'GET', body = null) {
const opts = {
method,
headers: { 'Content-Type': 'application/json', 'X-Admin-Password': adminPassword },
};
if (body) opts.body = JSON.stringify(body);
return fetch(`/api/admin${path}`, opts).then(r => {
if (r.status === 401) { alert('密码错误'); location.reload(); }
return r.json();
});
}
function doLogin() {
adminPassword = document.getElementById('login-password').value;
api('/status').then(data => {
if (data.uptime) {
document.getElementById('login-box').style.display = 'none';
document.getElementById('app').style.display = 'block';
loadDashboard();
}
});
}
function switchTab(name) {
document.querySelectorAll('.tab').forEach((t, i) => t.classList.toggle('active', t.textContent.includes({dashboard:'仪表盘',logs:'操作日志',commands:'预设命令',config:'API配置'}[name])));
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
document.getElementById(`panel-${name}`).classList.add('active');
if (name === 'dashboard') loadDashboard();
if (name === 'logs') loadLogs();
if (name === 'commands') loadCommands();
if (name === 'config') loadConfig();
}
async function loadDashboard() {
const data = await api('/status');
const s = data.todayStats || {};
document.getElementById('stats').innerHTML = `
<div class="stat-item"><div class="num">${s.total||0}</div><div class="label">今日总操作</div></div>
<div class="stat-item"><div class="num" style="color:#00b894">${s.success||0}</div><div class="label">成功</div></div>
<div class="stat-item"><div class="num" style="color:#d63031">${s.failed||0}</div><div class="label">失败</div></div>
<div class="stat-item"><div class="num" style="color:#fdcb6e">${s.running||0}</div><div class="label">执行中</div></div>
`;
document.getElementById('sysinfo').innerHTML = `
<p>运行时间: ${Math.floor(data.uptime/3600)}小时${Math.floor((data.uptime%3600)/60)}分钟</p>
<p>内存使用: ${Math.round(data.memory?.heapUsed/1024/1024||0)}MB</p>
<p>版本: ${data.version}</p>
`;
}
async function loadLogs(page = 1) {
currentPage = page;
const params = new URLSearchParams({ page, pageSize: 15 });
const type = document.getElementById('filter-type').value;
const status = document.getElementById('filter-status').value;
const start = document.getElementById('filter-start').value;
const end = document.getElementById('filter-end').value;
const keyword = document.getElementById('filter-keyword').value;
if (type) params.set('workflowType', type);
if (status) params.set('status', status);
if (start) params.set('startDate', start);
if (end) params.set('endDate', end);
if (keyword) params.set('keyword', keyword);
const data = await api(`/logs?${params}`);
const typeMap = { file_in: '文件传入', file_out: '文件传出', group_permission: '红区组权限' };
document.getElementById('logs-body').innerHTML = (data.rows || []).map(r => `
<tr>
<td>${r.created_at}</td>
<td>${typeMap[r.workflow_type] || r.workflow_type}</td>
<td>${r.applicant_name || '-'}</td>
<td>${r.action}</td>
<td class="status-${r.status}">${r.status === 'success' ? '成功' : r.status === 'failed' ? '失败' : '执行中'}</td>
<td title="${r.error_message || r.detail || ''}">${(r.error_message || r.detail || '').substring(0, 50)}</td>
</tr>
`).join('');
const totalPages = Math.ceil((data.total || 0) / 15);
document.getElementById('logs-pagination').innerHTML = Array.from({length: Math.min(totalPages, 7)}, (_, i) =>
`<button class="btn btn-sm ${i+1===page?'btn-primary':''}" onclick="loadLogs(${i+1})">${i+1}</button>`
).join('');
}
async function loadCommands() {
const data = await api('/commands');
document.getElementById('commands-body').innerHTML = (data || []).map(c => `
<tr>
<td>${c.id}</td><td>${c.name}</td><td><code>${c.command}</code></td><td>${c.target}</td>
<td>${c.enabled ? '启用' : '禁用'}</td>
<td><button class="btn btn-sm btn-danger" onclick="deleteCommand(${c.id})">删除</button></td>
</tr>
`).join('');
}
async function addCommand() {
const name = document.getElementById('cmd-name').value;
const command = document.getElementById('cmd-command').value;
const target = document.getElementById('cmd-target').value;
if (!name || !command) return alert('请填写名称和命令');
await api('/commands', 'POST', { name, command, target });
loadCommands();
}
async function deleteCommand(id) {
if (!confirm('确认删除?')) return;
await api(`/commands/${id}`, 'DELETE');
loadCommands();
}
async function loadConfig() {
const data = await api('/config');
document.getElementById('config-body').innerHTML = (data || []).map(c => `
<tr><td>${c.key}</td><td>${c.value}</td><td>${c.description || '-'}</td><td>${c.updated_at}</td></tr>
`).join('');
}
async function saveConfig() {
const key = document.getElementById('cfg-key').value;
const value = document.getElementById('cfg-value').value;
const description = document.getElementById('cfg-desc').value;
if (!key || !value) return alert('请填写键名和值');
await api('/config', 'PUT', { key, value, description });
loadConfig();
}
</script>
</body>
</html>