533 lines
19 KiB
JavaScript
533 lines
19 KiB
JavaScript
// 后台管理 JavaScript
|
|
|
|
// 获取 Cookie
|
|
function getCookie(name) {
|
|
const value = `; ${document.cookie}`;
|
|
const parts = value.split(`; ${name}=`);
|
|
if (parts.length === 2) return parts.pop().split(';').shift();
|
|
return null;
|
|
}
|
|
|
|
// API 请求封装
|
|
async function apiRequest(url, options = {}) {
|
|
const token = getCookie('token');
|
|
const headers = {
|
|
'Content-Type': 'application/json',
|
|
...options.headers,
|
|
};
|
|
|
|
if (token) {
|
|
headers['Authorization'] = `Bearer ${token}`;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
...options,
|
|
headers,
|
|
credentials: 'include',
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (!response.ok) {
|
|
// 如果是401,跳转到登录页
|
|
if (response.status === 401) {
|
|
window.location.href = '/admin/login';
|
|
return;
|
|
}
|
|
throw new Error(data.error || '请求失败');
|
|
}
|
|
|
|
return data;
|
|
} catch (error) {
|
|
console.error('API Error:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// 加载当前用户信息
|
|
async function loadCurrentUser() {
|
|
try {
|
|
const data = await apiRequest('/admin/me');
|
|
const userEl = document.getElementById('current-user');
|
|
if (userEl && data.user) {
|
|
userEl.textContent = `${data.user.username}${data.user.is_admin ? ' (管理员)' : ''}`;
|
|
}
|
|
} catch (error) {
|
|
console.error('Load user error:', error);
|
|
}
|
|
}
|
|
|
|
// 登出
|
|
document.getElementById('logout-btn')?.addEventListener('click', async () => {
|
|
try {
|
|
await apiRequest('/admin/logout', { method: 'POST' });
|
|
document.cookie = 'token=; path=/; max-age=0';
|
|
window.location.href = '/admin/login';
|
|
} catch (error) {
|
|
console.error('Logout error:', error);
|
|
// 即使失败也跳转
|
|
document.cookie = 'token=; path=/; max-age=0';
|
|
window.location.href = '/admin/login';
|
|
}
|
|
});
|
|
|
|
// 页面导航
|
|
document.querySelectorAll('.nav-item').forEach(item => {
|
|
item.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
const page = item.dataset.page;
|
|
|
|
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
|
|
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
|
|
|
|
item.classList.add('active');
|
|
document.getElementById(`page-${page}`).classList.add('active');
|
|
|
|
// 加载对应页面数据
|
|
if (page === 'routes') {
|
|
loadRoutes();
|
|
} else if (page === 'logs') {
|
|
loadLogs();
|
|
} else if (page === 'files') {
|
|
loadFiles();
|
|
} else if (page === 'config') {
|
|
loadConfig();
|
|
} else if (page === 'database') {
|
|
loadDatabaseInfo();
|
|
}
|
|
});
|
|
});
|
|
|
|
// 加载数据库信息
|
|
async function loadDatabaseInfo() {
|
|
try {
|
|
const data = await apiRequest('/admin/api/database');
|
|
const dbDetails = document.getElementById('db-details');
|
|
const osDetails = document.getElementById('os-details');
|
|
const dbConfig = document.getElementById('db-config');
|
|
const passwordSection = document.getElementById('database-password-section');
|
|
|
|
dbDetails.innerHTML = `
|
|
<p><strong>类型:</strong> ${data.database.type}</p>
|
|
<p><strong>状态:</strong> ${data.database.status}</p>
|
|
${data.database.file ? `<p><strong>文件:</strong> ${data.database.file}</p>` : ''}
|
|
${data.database.cgo_support !== undefined ? `<p><strong>CGO 支持:</strong> ${data.database.cgo_support ? '是' : '否'}</p>` : ''}
|
|
`;
|
|
|
|
osDetails.innerHTML = `
|
|
<p><strong>操作系统:</strong> ${data.os.os}</p>
|
|
<p><strong>架构:</strong> ${data.os.arch}</p>
|
|
`;
|
|
|
|
// 加载数据库配置
|
|
try {
|
|
const configData = await apiRequest('/admin/api/database/config');
|
|
dbConfig.innerHTML = `
|
|
<p><strong>类型:</strong> ${configData.config.type}</p>
|
|
<p><strong>主机:</strong> ${configData.config.host}</p>
|
|
<p><strong>端口:</strong> ${configData.config.port}</p>
|
|
<p><strong>用户:</strong> ${configData.config.user}</p>
|
|
<p><strong>数据库:</strong> ${configData.config.database}</p>
|
|
<p><strong>已设置密码:</strong> ${configData.config.has_password ? '是' : '否'}</p>
|
|
`;
|
|
|
|
// 如果是 MySQL,显示密码修改界面
|
|
if (configData.config.type === 'mysql') {
|
|
passwordSection.style.display = 'block';
|
|
} else {
|
|
passwordSection.style.display = 'none';
|
|
}
|
|
} catch (error) {
|
|
console.error('Load database config error:', error);
|
|
dbConfig.innerHTML = '<p>无法加载配置信息</p>';
|
|
}
|
|
} catch (error) {
|
|
console.error('Load database info error:', error);
|
|
}
|
|
}
|
|
|
|
// 刷新数据库信息
|
|
document.getElementById('refresh-db-btn')?.addEventListener('click', loadDatabaseInfo);
|
|
|
|
// 转换数据库
|
|
document.getElementById('convert-db-btn')?.addEventListener('click', async () => {
|
|
const targetType = document.getElementById('target-db-type').value;
|
|
|
|
if (!confirm(`确定要转换数据库类型吗?\n\n目标类型: ${targetType.toUpperCase()}\n\n此操作会导出当前数据并导入到新数据库。请确保已备份数据!`)) {
|
|
return;
|
|
}
|
|
|
|
const resultEl = document.getElementById('convert-result');
|
|
resultEl.className = '';
|
|
resultEl.textContent = '正在转换...';
|
|
resultEl.style.display = 'block';
|
|
|
|
try {
|
|
const result = await apiRequest('/admin/api/database/convert', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
target_type: targetType,
|
|
}),
|
|
});
|
|
|
|
resultEl.className = 'success';
|
|
resultEl.textContent = result.message || '数据库转换成功!';
|
|
loadDatabaseInfo();
|
|
loadLogs();
|
|
} catch (error) {
|
|
resultEl.className = 'error';
|
|
resultEl.textContent = '转换失败: ' + error.message;
|
|
}
|
|
});
|
|
|
|
// 更新数据库密码
|
|
document.getElementById('password-form')?.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
|
|
const currentPassword = document.getElementById('current-password').value;
|
|
const newPassword = document.getElementById('new-password').value;
|
|
const confirmPassword = document.getElementById('confirm-password').value;
|
|
|
|
if (newPassword !== confirmPassword) {
|
|
alert('新密码和确认密码不一致!');
|
|
return;
|
|
}
|
|
|
|
if (!confirm('确定要更新数据库 root 密码吗?\n\n更新后需要修改环境变量 DB_PASSWORD 并重启服务器!')) {
|
|
return;
|
|
}
|
|
|
|
const resultEl = document.getElementById('password-result');
|
|
resultEl.className = '';
|
|
resultEl.textContent = '正在更新密码...';
|
|
resultEl.style.display = 'block';
|
|
|
|
try {
|
|
const result = await apiRequest('/admin/api/database/password', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
current_password: currentPassword,
|
|
new_password: newPassword,
|
|
confirm_password: confirmPassword,
|
|
}),
|
|
});
|
|
|
|
resultEl.className = 'success';
|
|
resultEl.textContent = result.message || '密码更新成功!';
|
|
|
|
// 清空表单
|
|
document.getElementById('password-form').reset();
|
|
|
|
loadLogs();
|
|
} catch (error) {
|
|
resultEl.className = 'error';
|
|
resultEl.textContent = '更新失败: ' + error.message;
|
|
}
|
|
});
|
|
|
|
// 加载系统信息
|
|
async function loadSystemInfo() {
|
|
try {
|
|
const data = await apiRequest('/admin/api/system');
|
|
document.getElementById('stat-users').textContent = data.users;
|
|
document.getElementById('stat-routes').textContent = data.routes;
|
|
document.getElementById('stat-logs').textContent = data.logs;
|
|
document.getElementById('stat-time').textContent = data.server_time;
|
|
} catch (error) {
|
|
console.error('Load system info error:', error);
|
|
}
|
|
}
|
|
|
|
// 加载路由
|
|
async function loadRoutes() {
|
|
try {
|
|
const data = await apiRequest('/admin/api/routes');
|
|
const tbody = document.getElementById('routes-table-body');
|
|
if (!tbody) return;
|
|
|
|
tbody.innerHTML = '';
|
|
|
|
if (data.routes && data.routes.length > 0) {
|
|
data.routes.forEach(route => {
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = `
|
|
<td>${route.id}</td>
|
|
<td><span class="badge">${route.method}</span></td>
|
|
<td>${route.path}</td>
|
|
<td>${route.type}</td>
|
|
<td>${route.description || '-'}</td>
|
|
<td>${route.is_active ? '✅' : '❌'}</td>
|
|
<td>
|
|
<button class="btn btn-secondary" onclick="editRoute(${route.id})">编辑</button>
|
|
<button class="btn btn-danger" onclick="deleteRoute(${route.id})">删除</button>
|
|
</td>
|
|
`;
|
|
tbody.appendChild(tr);
|
|
});
|
|
} else {
|
|
tbody.innerHTML = '<tr><td colspan="7" class="empty-state">暂无路由</td></tr>';
|
|
}
|
|
} catch (error) {
|
|
console.error('Load routes error:', error);
|
|
const tbody = document.getElementById('routes-table-body');
|
|
if (tbody) {
|
|
tbody.innerHTML = '<tr><td colspan="7" class="empty-state">加载失败: ' + error.message + '</td></tr>';
|
|
}
|
|
}
|
|
}
|
|
|
|
// 添加路由
|
|
document.getElementById('add-route-btn')?.addEventListener('click', () => {
|
|
document.getElementById('route-modal-title').textContent = '添加路由';
|
|
document.getElementById('route-form').reset();
|
|
document.getElementById('route-id').value = '';
|
|
document.getElementById('route-modal').classList.add('show');
|
|
});
|
|
|
|
// 编辑路由
|
|
window.editRoute = async function(id) {
|
|
try {
|
|
const data = await apiRequest('/admin/api/routes');
|
|
const route = data.routes.find(r => r.id === id);
|
|
|
|
if (route) {
|
|
document.getElementById('route-modal-title').textContent = '编辑路由';
|
|
document.getElementById('route-id').value = route.id;
|
|
document.getElementById('route-method').value = route.method;
|
|
document.getElementById('route-path').value = route.path;
|
|
document.getElementById('route-type').value = route.type;
|
|
document.getElementById('route-handler').value = route.handler;
|
|
document.getElementById('route-description').value = route.description || '';
|
|
document.getElementById('route-active').checked = route.is_active;
|
|
document.getElementById('route-order').value = route.order;
|
|
document.getElementById('route-modal').classList.add('show');
|
|
}
|
|
} catch (error) {
|
|
console.error('Edit route error:', error);
|
|
}
|
|
};
|
|
|
|
// 删除路由
|
|
window.deleteRoute = async function(id) {
|
|
if (!confirm('确定要删除这个路由吗?')) return;
|
|
|
|
try {
|
|
await apiRequest(`/admin/api/routes/${id}`, { method: 'DELETE' });
|
|
loadRoutes();
|
|
} catch (error) {
|
|
alert('删除失败: ' + error.message);
|
|
}
|
|
};
|
|
|
|
// 保存路由
|
|
document.getElementById('route-save-btn')?.addEventListener('click', async () => {
|
|
const id = document.getElementById('route-id').value;
|
|
const routeData = {
|
|
method: document.getElementById('route-method').value,
|
|
path: document.getElementById('route-path').value,
|
|
type: document.getElementById('route-type').value,
|
|
handler: document.getElementById('route-handler').value,
|
|
description: document.getElementById('route-description').value,
|
|
is_active: document.getElementById('route-active').checked,
|
|
order: parseInt(document.getElementById('route-order').value) || 0,
|
|
};
|
|
|
|
try {
|
|
if (id) {
|
|
await apiRequest(`/admin/api/routes/${id}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(routeData),
|
|
});
|
|
} else {
|
|
await apiRequest('/admin/api/routes', {
|
|
method: 'POST',
|
|
body: JSON.stringify(routeData),
|
|
});
|
|
}
|
|
document.getElementById('route-modal').classList.remove('show');
|
|
loadRoutes();
|
|
} catch (error) {
|
|
alert('保存失败: ' + error.message);
|
|
}
|
|
});
|
|
|
|
// 关闭模态框
|
|
document.querySelectorAll('.modal-close, #route-cancel-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
document.getElementById('route-modal').classList.remove('show');
|
|
});
|
|
});
|
|
|
|
// 加载日志
|
|
async function loadLogs() {
|
|
try {
|
|
const data = await apiRequest('/admin/api/logs?limit=100');
|
|
const logsEl = document.getElementById('logs-content');
|
|
if (!logsEl) return;
|
|
|
|
logsEl.innerHTML = '';
|
|
|
|
if (data.logs && data.logs.length > 0) {
|
|
data.logs.forEach(log => {
|
|
const entry = document.createElement('div');
|
|
entry.className = 'log-entry';
|
|
entry.innerHTML = `
|
|
<span class="log-time">${log.time || ''}</span>
|
|
<span class="log-level ${log.level || 'INFO'}">${log.level || 'INFO'}</span>
|
|
<span class="log-message">${log.message || ''}</span>
|
|
`;
|
|
logsEl.appendChild(entry);
|
|
});
|
|
|
|
logsEl.scrollTop = logsEl.scrollHeight;
|
|
} else {
|
|
logsEl.innerHTML = '<div class="empty-state">暂无日志</div>';
|
|
}
|
|
} catch (error) {
|
|
console.error('Load logs error:', error);
|
|
const logsEl = document.getElementById('logs-content');
|
|
if (logsEl) {
|
|
logsEl.innerHTML = '<div class="empty-state">加载失败: ' + error.message + '</div>';
|
|
}
|
|
}
|
|
}
|
|
|
|
// 刷新日志
|
|
document.getElementById('refresh-logs-btn')?.addEventListener('click', loadLogs);
|
|
|
|
// 清空日志
|
|
document.getElementById('clear-logs-btn')?.addEventListener('click', () => {
|
|
const logsEl = document.getElementById('logs-content');
|
|
if (logsEl) {
|
|
logsEl.innerHTML = '<div class="empty-state">日志已清空</div>';
|
|
}
|
|
});
|
|
|
|
// 加载文件
|
|
async function loadFiles() {
|
|
const fileBrowser = document.getElementById('file-browser');
|
|
if (!fileBrowser) return;
|
|
|
|
try {
|
|
const data = await apiRequest('/admin/api/files?dir=public/downloads');
|
|
fileBrowser.innerHTML = '';
|
|
|
|
if (data.files && data.files.length > 0) {
|
|
data.files.forEach(file => {
|
|
const item = document.createElement('div');
|
|
item.className = 'file-item';
|
|
item.innerHTML = `
|
|
<div>
|
|
<strong>${file.name}</strong>
|
|
<small style="display: block; color: #86868b;">
|
|
${file.is_dir ? '📁 目录' : `📄 ${formatBytes(file.size)}`} • ${file.mod_time || ''}
|
|
</small>
|
|
</div>
|
|
${!file.is_dir ? `<button class="btn btn-secondary" onclick="readFile('${file.name}')">查看</button>` : ''}
|
|
`;
|
|
fileBrowser.appendChild(item);
|
|
});
|
|
} else {
|
|
fileBrowser.innerHTML = '<div class="empty-state">暂无文件</div>';
|
|
}
|
|
} catch (error) {
|
|
console.error('Load files error:', error);
|
|
fileBrowser.innerHTML = '<div class="empty-state">加载失败: ' + error.message + '</div>';
|
|
}
|
|
}
|
|
|
|
document.getElementById('refresh-files-btn')?.addEventListener('click', loadFiles);
|
|
|
|
// 读取文件
|
|
window.readFile = async function(filename) {
|
|
const fullPath = `public/downloads/${filename}`;
|
|
|
|
try {
|
|
const data = await apiRequest(`/admin/api/file?path=${encodeURIComponent(fullPath)}`);
|
|
// 显示文件内容在模态框中或新窗口
|
|
const content = data.content.substring(0, 5000) + (data.content.length > 5000 ? '\n\n... (内容过长,已截断)' : '');
|
|
alert('文件内容:\n\n' + content);
|
|
} catch (error) {
|
|
alert('读取文件失败: ' + error.message);
|
|
}
|
|
};
|
|
|
|
// 加载配置
|
|
async function loadConfig() {
|
|
const file = document.getElementById('config-select').value;
|
|
try {
|
|
const response = await fetch(`/public/${file}`);
|
|
const data = await response.json();
|
|
document.getElementById('config-editor').value = JSON.stringify(data, null, 2);
|
|
} catch (error) {
|
|
console.error('Load config error:', error);
|
|
}
|
|
}
|
|
|
|
document.getElementById('load-config-btn')?.addEventListener('click', loadConfig);
|
|
|
|
// 保存配置
|
|
document.getElementById('save-config-btn')?.addEventListener('click', async () => {
|
|
const file = document.getElementById('config-select').value;
|
|
const content = document.getElementById('config-editor').value;
|
|
|
|
try {
|
|
const jsonData = JSON.parse(content);
|
|
const result = await apiRequest('/admin/api/config', {
|
|
method: 'PUT',
|
|
body: JSON.stringify({
|
|
file: file,
|
|
content: jsonData,
|
|
reload: false, // 仅保存
|
|
}),
|
|
});
|
|
alert(result.message || '配置保存成功!');
|
|
} catch (error) {
|
|
alert('保存失败: ' + error.message);
|
|
}
|
|
});
|
|
|
|
// 保存并立即加载配置
|
|
document.getElementById('save-reload-config-btn')?.addEventListener('click', async () => {
|
|
const file = document.getElementById('config-select').value;
|
|
const content = document.getElementById('config-editor').value;
|
|
|
|
try {
|
|
const jsonData = JSON.parse(content);
|
|
const result = await apiRequest('/admin/api/config', {
|
|
method: 'PUT',
|
|
body: JSON.stringify({
|
|
file: file,
|
|
content: jsonData,
|
|
reload: true, // 保存并立即加载
|
|
}),
|
|
});
|
|
alert(result.message || '配置已保存并立即生效!');
|
|
// 刷新日志以显示加载信息
|
|
if (document.getElementById('page-logs')?.classList.contains('active')) {
|
|
loadLogs();
|
|
}
|
|
} catch (error) {
|
|
alert('保存失败: ' + error.message);
|
|
}
|
|
});
|
|
|
|
// 工具函数
|
|
function formatBytes(bytes) {
|
|
if (bytes === 0) return '0 B';
|
|
const k = 1024;
|
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
|
}
|
|
|
|
// 页面加载时初始化
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadCurrentUser();
|
|
loadSystemInfo();
|
|
|
|
// 定期更新系统信息
|
|
setInterval(loadSystemInfo, 30000); // 每30秒更新一次
|
|
});
|