init
This commit is contained in:
282
templates/admin_apis.html
Normal file
282
templates/admin_apis.html
Normal file
@@ -0,0 +1,282 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>接口管理 - 管理后台</title>
|
||||
<link rel="stylesheet" href="/static/css/ui-components.css?v=3">
|
||||
<link rel="stylesheet" href="/static/css/admin.css">
|
||||
</head>
|
||||
|
||||
<body class="admin-layout">
|
||||
<header class="admin-header">
|
||||
<div class="header-container">
|
||||
<a href="/admin/dashboard" class="brand">
|
||||
<span style="font-size: 1.5rem;">⚡</span> JieXi Admin
|
||||
</a>
|
||||
<nav class="nav-links">
|
||||
<a href="/admin/dashboard" class="nav-item">仪表板</a>
|
||||
<a href="/admin/users" class="nav-item">用户管理</a>
|
||||
<a href="/admin/apis" class="nav-item active">接口管理</a>
|
||||
<a href="/admin/config" class="nav-item">系统配置</a>
|
||||
<a href="/admin/logs" class="nav-item">日志审计</a>
|
||||
</nav>
|
||||
<div class="user-actions">
|
||||
<a href="/admin/profile" class="ui-btn ui-btn-secondary ui-btn-sm">账号设置</a>
|
||||
<button class="ui-btn ui-btn-secondary ui-btn-sm" onclick="logout()">退出登录</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="main-container">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">接口管理</h1>
|
||||
<div class="actions">
|
||||
<button class="ui-btn ui-btn-primary" onclick="showAddModal()">
|
||||
<span style="margin-right: 4px;">+</span> 添加接口
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui-card">
|
||||
<div id="tableContainer">
|
||||
<div class="loading">加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- API Modal -->
|
||||
<div id="apiModal" class="ui-modal">
|
||||
<div class="ui-modal-overlay" onclick="closeModal()"></div>
|
||||
<div class="ui-modal-content">
|
||||
<h3 id="modalTitle">添加接口</h3>
|
||||
<form id="apiForm">
|
||||
<input type="hidden" id="apiId">
|
||||
<div class="form-group">
|
||||
<label>接口名称 *</label>
|
||||
<input type="text" id="apiName" class="ui-input" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>平台类型 *</label>
|
||||
<select id="apiPlatform" class="ui-input" required>
|
||||
<option value="douyin">抖音</option>
|
||||
<option value="tiktok">TikTok</option>
|
||||
<option value="bilibili">哔哩哔哩</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>API地址 *</label>
|
||||
<input type="url" id="apiUrl" class="ui-input" required placeholder="https://api.example.com">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>API密钥</label>
|
||||
<input type="text" id="apiKey" class="ui-input" placeholder="可选">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>权重</label>
|
||||
<input type="number" id="apiWeight" class="ui-input" value="1" min="0" max="100">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>是否启用</label>
|
||||
<select id="apiEnabled" class="ui-input">
|
||||
<option value="true">启用</option>
|
||||
<option value="false">禁用</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="ui-modal-actions">
|
||||
<button type="button" class="ui-btn ui-btn-secondary" onclick="closeModal()">取消</button>
|
||||
<button type="submit" class="ui-btn ui-btn-primary">保存</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/ui-components.js"></script>
|
||||
<script>
|
||||
let apisData = [];
|
||||
|
||||
async function loadApis() {
|
||||
try {
|
||||
const response = await fetch('/admin/api/apis');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
apisData = result.data;
|
||||
renderTable();
|
||||
} else {
|
||||
UI.notify('加载失败: ' + result.message, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
UI.notify('加载失败: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function renderTable() {
|
||||
const container = document.getElementById('tableContainer');
|
||||
if (apisData.length === 0) {
|
||||
container.innerHTML = '<div style="text-align: center; padding: 2rem; color: var(--text-muted);">暂无接口数据</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>名称</th>
|
||||
<th>平台</th>
|
||||
<th>API地址</th>
|
||||
<th>权重</th>
|
||||
<th>状态</th>
|
||||
<th>健康</th>
|
||||
<th>调用统计</th>
|
||||
<th>响应时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${apisData.map(a => `
|
||||
<tr>
|
||||
<td>#${a.id}</td>
|
||||
<td><span class="font-medium">${a.name}</span></td>
|
||||
<td><span class="badge badge-neutral">${a.platform}</span></td>
|
||||
<td style="max-width: 200px; overflow: hidden; text-overflow: ellipsis;" title="${a.api_url}" class="text-muted text-sm">${a.api_url}</td>
|
||||
<td>${a.weight}</td>
|
||||
<td><span class="badge ${a.is_enabled ? 'badge-info' : 'badge-neutral'}">${a.is_enabled ? '启用' : '禁用'}</span></td>
|
||||
<td><span class="badge ${a.health_status ? 'badge-success' : 'badge-error'}">${a.health_status ? '正常' : '异常'}</span></td>
|
||||
<td class="text-sm">${a.success_calls}/${a.total_calls}</td>
|
||||
<td class="text-sm text-muted">${(a.avg_response_time / 1000).toFixed(2)}s</td>
|
||||
<td>
|
||||
<div class="flex gap-2">
|
||||
<button class="ui-btn ui-btn-secondary ui-btn-sm" onclick='editApi(${JSON.stringify(a)})'>编辑</button>
|
||||
<button class="ui-btn ui-btn-secondary ui-btn-sm" onclick="testApi(${a.id}, '${a.platform}')">测试</button>
|
||||
<button class="ui-btn ui-btn-danger ui-btn-sm" onclick="deleteApi(${a.id})">删除</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function showAddModal() {
|
||||
document.getElementById('modalTitle').textContent = '添加接口';
|
||||
document.getElementById('apiForm').reset();
|
||||
document.getElementById('apiId').value = '';
|
||||
document.getElementById('apiModal').classList.add('show');
|
||||
}
|
||||
|
||||
function editApi(api) {
|
||||
document.getElementById('modalTitle').textContent = '编辑接口';
|
||||
document.getElementById('apiId').value = api.id;
|
||||
document.getElementById('apiName').value = api.name;
|
||||
document.getElementById('apiPlatform').value = api.platform;
|
||||
document.getElementById('apiUrl').value = api.api_url;
|
||||
document.getElementById('apiKey').value = api.api_key || '';
|
||||
document.getElementById('apiWeight').value = api.weight;
|
||||
document.getElementById('apiEnabled').value = api.is_enabled;
|
||||
document.getElementById('apiModal').classList.add('show');
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('apiModal').classList.remove('show');
|
||||
}
|
||||
|
||||
async function deleteApi(id) {
|
||||
UI.confirm('确认删除', '确定要删除此接口吗?', async () => {
|
||||
try {
|
||||
const response = await fetch(`/admin/api/apis/${id}`, { method: 'DELETE' });
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
UI.notify('删除成功', 'success');
|
||||
loadApis();
|
||||
} else {
|
||||
UI.notify('删除失败: ' + result.message, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
UI.notify('删除失败: ' + error.message, 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('apiForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const apiId = document.getElementById('apiId').value;
|
||||
const data = {
|
||||
name: document.getElementById('apiName').value,
|
||||
platform: document.getElementById('apiPlatform').value,
|
||||
api_url: document.getElementById('apiUrl').value,
|
||||
api_key: document.getElementById('apiKey').value || null,
|
||||
weight: parseInt(document.getElementById('apiWeight').value),
|
||||
is_enabled: document.getElementById('apiEnabled').value === 'true'
|
||||
};
|
||||
|
||||
try {
|
||||
const url = apiId ? `/admin/api/apis/${apiId}` : '/admin/api/apis';
|
||||
const method = apiId ? 'PUT' : 'POST';
|
||||
const response = await fetch(url, {
|
||||
method: method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
UI.notify(apiId ? '更新成功' : '创建成功', 'success');
|
||||
closeModal();
|
||||
loadApis();
|
||||
} else {
|
||||
UI.notify('操作失败: ' + result.message, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
UI.notify('操作失败: ' + error.message, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
async function testApi(apiId, platform) {
|
||||
const testUrls = {
|
||||
'douyin': 'https://v.douyin.com/iRNBho6u/',
|
||||
'tiktok': 'https://www.tiktok.com/@username/video/1234567890',
|
||||
'bilibili': 'https://www.bilibili.com/video/BV1vrU6B4ELQ/?share_source=copy_web&vd_source=8977adbddf938cc18f327c3c21c5120c'
|
||||
};
|
||||
|
||||
UI.prompt('测试接口', `请输入测试链接 (${platform})`, testUrls[platform] || '', async (testUrl) => {
|
||||
try {
|
||||
const response = await fetch(`/admin/api/apis/${apiId}/test`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ test_url: testUrl })
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
UI.notify(`测试成功!响应时间: ${result.response_time}ms`, 'success');
|
||||
loadApis();
|
||||
} else {
|
||||
UI.notify('测试失败: ' + result.message, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
UI.notify('测试失败: ' + error.message, 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
await fetch('/admin/logout', { method: 'POST' });
|
||||
window.location.href = '/admin/login';
|
||||
} catch (error) {
|
||||
UI.notify('退出失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
loadApis();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
181
templates/admin_config.html
Normal file
181
templates/admin_config.html
Normal file
@@ -0,0 +1,181 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>站点配置 - 管理后台</title>
|
||||
<link rel="stylesheet" href="/static/css/ui-components.css?v=3">
|
||||
<link rel="stylesheet" href="/static/css/admin.css">
|
||||
</head>
|
||||
|
||||
<body class="admin-layout">
|
||||
<header class="admin-header">
|
||||
<div class="header-container">
|
||||
<a href="/admin/dashboard" class="brand">
|
||||
<span style="font-size: 1.5rem;">⚡</span> JieXi Admin
|
||||
</a>
|
||||
<nav class="nav-links">
|
||||
<a href="/admin/dashboard" class="nav-item">仪表板</a>
|
||||
<a href="/admin/users" class="nav-item">用户管理</a>
|
||||
<a href="/admin/apis" class="nav-item">接口管理</a>
|
||||
<a href="/admin/config" class="nav-item active">系统配置</a>
|
||||
<a href="/admin/logs" class="nav-item">日志审计</a>
|
||||
</nav>
|
||||
<div class="user-actions">
|
||||
<a href="/admin/profile" class="ui-btn ui-btn-secondary ui-btn-sm">账号设置</a>
|
||||
<button class="ui-btn ui-btn-secondary ui-btn-sm" onclick="logout()">退出登录</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="main-container">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">系统配置</h1>
|
||||
<div class="actions">
|
||||
<a href="/admin/smtp" class="ui-btn ui-btn-secondary">SMTP设置</a>
|
||||
<a href="/admin/health-checks" class="ui-btn ui-btn-secondary">健康检查</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-card" style="max-width: 800px;">
|
||||
<form id="configForm">
|
||||
<div class="form-group">
|
||||
<label>网站标题</label>
|
||||
<input type="text" id="site_title" class="ui-input" placeholder="短视频解析平台">
|
||||
<small class="text-muted text-sm">显示在浏览器标签</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>主页大标题</label>
|
||||
<input type="text" id="home_title" class="ui-input" placeholder="JieXi Pro">
|
||||
<small class="text-muted text-sm">显示在首页的主标题</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>主页副标题</label>
|
||||
<input type="text" id="home_subtitle" class="ui-input" placeholder="新一代全能短视频去水印解析工具">
|
||||
<small class="text-muted text-sm">显示在首页主标题下方</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>网站Logo URL</label>
|
||||
<input type="url" id="site_logo" class="ui-input" placeholder="https://example.com/logo.png">
|
||||
<small class="text-muted text-sm">留空则不显示Logo</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>网站图标 (Favicon) URL</label>
|
||||
<input type="url" id="site_favicon" class="ui-input" placeholder="https://example.com/favicon.ico">
|
||||
<small class="text-muted text-sm">浏览器标签页显示的小图标,留空则使用Logo</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>网站公告</label>
|
||||
<textarea id="site_notice" class="ui-input" style="min-height: 100px;"
|
||||
placeholder="欢迎使用短视频解析平台"></textarea>
|
||||
<small class="text-muted text-sm">显示在首页顶部(留空则不显示)</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>网站底部信息</label>
|
||||
<textarea id="site_footer" class="ui-input" style="min-height: 80px;"
|
||||
placeholder="© 2024 短视频解析平台. All rights reserved."></textarea>
|
||||
<small class="text-muted text-sm">显示在页面底部</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>最大并发解析数</label>
|
||||
<input type="number" id="max_concurrent" class="ui-input" min="1" max="20" value="3">
|
||||
<small class="text-muted text-sm">同时处理的最大解析任务数(1-20)</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>游客每日解析次数</label>
|
||||
<input type="number" id="guest_daily_limit" class="ui-input" min="0" max="100" value="5">
|
||||
<small class="text-muted text-sm">未登录用户每天可解析的次数</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>普通用户每日解析次数</label>
|
||||
<input type="number" id="user_daily_limit" class="ui-input" min="0" max="500" value="10">
|
||||
<small class="text-muted text-sm">已注册用户每天可解析的次数</small>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex justify-between">
|
||||
<div></div>
|
||||
<button type="submit" class="ui-btn ui-btn-primary">保存配置</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script src="/static/js/ui-components.js"></script>
|
||||
<script>
|
||||
async function loadConfig() {
|
||||
try {
|
||||
const response = await fetch('/admin/api/config');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
const data = result.data;
|
||||
Object.keys(data).forEach(key => {
|
||||
const element = document.getElementById(key);
|
||||
if (element) {
|
||||
element.value = data[key] || '';
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
UI.notify('加载失败: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('configForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const data = {
|
||||
site_title: document.getElementById('site_title').value,
|
||||
home_title: document.getElementById('home_title').value,
|
||||
home_subtitle: document.getElementById('home_subtitle').value,
|
||||
site_logo: document.getElementById('site_logo').value,
|
||||
site_favicon: document.getElementById('site_favicon').value,
|
||||
site_notice: document.getElementById('site_notice').value,
|
||||
site_footer: document.getElementById('site_footer').value,
|
||||
max_concurrent: document.getElementById('max_concurrent').value,
|
||||
guest_daily_limit: document.getElementById('guest_daily_limit').value,
|
||||
user_daily_limit: document.getElementById('user_daily_limit').value
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/admin/api/config', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
UI.notify('配置保存成功', 'success');
|
||||
} else {
|
||||
UI.notify('保存失败: ' + result.message, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
UI.notify('保存失败: ' + error.message, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
await fetch('/admin/logout', { method: 'POST' });
|
||||
window.location.href = '/admin/login';
|
||||
} catch (error) {
|
||||
UI.notify('退出失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
loadConfig();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
150
templates/admin_dashboard.html
Normal file
150
templates/admin_dashboard.html
Normal file
@@ -0,0 +1,150 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>仪表板 - 管理后台</title>
|
||||
<link rel="stylesheet" href="/static/css/ui-components.css?v=3">
|
||||
<link rel="stylesheet" href="/static/css/admin.css">
|
||||
</head>
|
||||
|
||||
<body class="admin-layout">
|
||||
<header class="admin-header">
|
||||
<div class="header-container">
|
||||
<a href="/admin/dashboard" class="brand">
|
||||
<span style="font-size: 1.5rem;">⚡</span> JieXi Admin
|
||||
</a>
|
||||
<nav class="nav-links">
|
||||
<a href="/admin/dashboard" class="nav-item active">仪表板</a>
|
||||
<a href="/admin/users" class="nav-item">用户管理</a>
|
||||
<a href="/admin/apis" class="nav-item">接口管理</a>
|
||||
<a href="/admin/config" class="nav-item">系统配置</a>
|
||||
<a href="/admin/logs" class="nav-item">日志审计</a>
|
||||
</nav>
|
||||
<div class="user-actions">
|
||||
<a href="/admin/profile" class="ui-btn ui-btn-secondary ui-btn-sm">账号设置</a>
|
||||
<button class="ui-btn ui-btn-secondary ui-btn-sm" onclick="logout()">退出登录</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="main-container">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">仪表板</h1>
|
||||
<div class="actions">
|
||||
<a href="/" target="_blank" class="ui-btn ui-btn-primary ui-btn-sm">
|
||||
访问前台
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">今日解析总数</div>
|
||||
<div class="stat-value" id="todayTotal">-</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">今日成功解析</div>
|
||||
<div class="stat-value" id="todaySuccess" style="color: var(--success);">-</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">总注册用户</div>
|
||||
<div class="stat-value" id="totalUsers">-</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">活跃接口节点</div>
|
||||
<div class="stat-value" id="activeApis" style="color: var(--warning);">-</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui-card">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-bold">实时解析动态</h3>
|
||||
<a href="/admin/logs" class="text-sm text-muted" style="text-decoration: none;">查看全部 →</a>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<table id="logsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>时间</th>
|
||||
<th>平台</th>
|
||||
<th>状态</th>
|
||||
<th>耗时</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="4" style="text-align: center; color: var(--text-muted);">加载中...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script src="/static/js/ui-components.js"></script>
|
||||
<script>
|
||||
async function loadDashboard() {
|
||||
try {
|
||||
const response = await fetch('/admin/api/dashboard');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
const data = result.data;
|
||||
document.getElementById('todayTotal').textContent = data.today.total.toLocaleString();
|
||||
document.getElementById('todaySuccess').textContent = data.today.success.toLocaleString();
|
||||
document.getElementById('totalUsers').textContent = data.total_users.toLocaleString();
|
||||
document.getElementById('activeApis').textContent = data.active_apis;
|
||||
}
|
||||
} catch (error) {
|
||||
UI.notify('加载数据失败: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRecentLogs() {
|
||||
try {
|
||||
const response = await fetch('/admin/api/logs?page=1&per_page=10');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success && result.data.length > 0) {
|
||||
const tbody = document.querySelector('#logsTable tbody');
|
||||
tbody.innerHTML = result.data.map(log => {
|
||||
let badgeClass = 'badge-neutral';
|
||||
if (log.status === 'success') badgeClass = 'badge-success';
|
||||
else if (log.status === 'failed') badgeClass = 'badge-error';
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td>${new Date(log.created_at).toLocaleString('zh-CN')}</td>
|
||||
<td><span class="font-medium">${log.platform}</span></td>
|
||||
<td><span class="badge ${badgeClass}">${log.status}</span></td>
|
||||
<td class="text-muted">${log.response_time ? (log.response_time / 1000).toFixed(2) + 's' : '-'}</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
} else {
|
||||
document.querySelector('#logsTable tbody').innerHTML =
|
||||
'<tr><td colspan="4" style="text-align: center; padding: 2rem; color: var(--text-muted);">暂无记录</td></tr>';
|
||||
}
|
||||
} catch (error) {
|
||||
UI.notify('加载日志失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
await fetch('/admin/logout', { method: 'POST' });
|
||||
window.location.href = '/admin/login';
|
||||
} catch (error) {
|
||||
UI.notify('退出失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
loadDashboard();
|
||||
loadRecentLogs();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
232
templates/admin_health_checks.html
Normal file
232
templates/admin_health_checks.html
Normal file
@@ -0,0 +1,232 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>健康检查配置 - 管理后台</title>
|
||||
<link rel="stylesheet" href="/static/css/ui-components.css?v=3">
|
||||
<link rel="stylesheet" href="/static/css/admin.css">
|
||||
</head>
|
||||
|
||||
<body class="admin-layout">
|
||||
<header class="admin-header">
|
||||
<div class="header-container">
|
||||
<a href="/admin/dashboard" class="brand">
|
||||
<span style="font-size: 1.5rem;">⚡</span> JieXi Admin
|
||||
</a>
|
||||
<nav class="nav-links">
|
||||
<a href="/admin/dashboard" class="nav-item">仪表板</a>
|
||||
<a href="/admin/users" class="nav-item">用户管理</a>
|
||||
<a href="/admin/apis" class="nav-item">接口管理</a>
|
||||
<a href="/admin/config" class="nav-item active">系统配置</a>
|
||||
<a href="/admin/logs" class="nav-item">日志审计</a>
|
||||
</nav>
|
||||
<div class="user-actions">
|
||||
<a href="/admin/profile" class="ui-btn ui-btn-secondary ui-btn-sm">账号设置</a>
|
||||
<button class="ui-btn ui-btn-secondary ui-btn-sm" onclick="logout()">退出登录</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="main-container">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">健康检查配置</h1>
|
||||
<div class="actions">
|
||||
<a href="/admin/config" class="ui-btn ui-btn-secondary">返回配置</a>
|
||||
<button class="ui-btn ui-btn-primary" onclick="showAddModal()">+ 添加配置</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui-card">
|
||||
<div class="table-container">
|
||||
<table id="healthTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>平台</th>
|
||||
<th>测试链接</th>
|
||||
<th>检查间隔</th>
|
||||
<th>告警邮箱</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="7" style="text-align: center; color: var(--text-muted);">加载中...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Health Check Modal -->
|
||||
<div id="healthModal" class="ui-modal">
|
||||
<div class="ui-modal-overlay" onclick="closeModal()"></div>
|
||||
<div class="ui-modal-content">
|
||||
<h3 id="modalTitle">添加健康检查配置</h3>
|
||||
<form id="healthForm">
|
||||
<input type="hidden" id="configId">
|
||||
<div class="form-group">
|
||||
<label>平台类型 *</label>
|
||||
<select id="platform" class="ui-input" required>
|
||||
<option value="douyin">抖音 (douyin)</option>
|
||||
<option value="tiktok">TikTok (tiktok)</option>
|
||||
<option value="bilibili">哔哩哔哩 (bilibili)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>测试视频链接 *</label>
|
||||
<input type="url" id="testUrl" class="ui-input" required placeholder="https://...">
|
||||
<small class="text-muted text-sm">用于健康检查的测试视频链接</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>检查间隔(秒)*</label>
|
||||
<input type="number" id="checkInterval" class="ui-input" value="300" min="60" max="3600" required>
|
||||
<small class="text-muted text-sm">建议300秒(5分钟)</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>告警邮箱</label>
|
||||
<input type="email" id="alertEmail" class="ui-input" placeholder="admin@example.com">
|
||||
<small class="text-muted text-sm">接口异常时发送告警邮件</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>是否启用</label>
|
||||
<select id="isEnabled" class="ui-input">
|
||||
<option value="true">启用</option>
|
||||
<option value="false">禁用</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="ui-modal-actions">
|
||||
<button type="button" class="ui-btn ui-btn-secondary" onclick="closeModal()">取消</button>
|
||||
<button type="submit" class="ui-btn ui-btn-primary">保存</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/ui-components.js"></script>
|
||||
<script>
|
||||
async function loadHealthChecks() {
|
||||
try {
|
||||
const response = await fetch('/admin/api/health-checks');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
const tbody = document.querySelector('#healthTable tbody');
|
||||
if (result.data.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="7" style="text-align: center; padding: 2rem; color: var(--text-muted);">暂无配置</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = result.data.map(c => `
|
||||
<tr>
|
||||
<td>${c.id}</td>
|
||||
<td><span class="badge badge-neutral">${c.platform}</span></td>
|
||||
<td style="max-width: 300px; overflow: hidden; text-overflow: ellipsis;" class="text-sm text-muted" title="${c.test_url}">${c.test_url}</td>
|
||||
<td>${c.check_interval}秒</td>
|
||||
<td class="text-sm">${c.alert_email || '-'}</td>
|
||||
<td><span class="badge ${c.is_enabled ? 'badge-success' : 'badge-neutral'}">${c.is_enabled ? '启用' : '禁用'}</span></td>
|
||||
<td>
|
||||
<div class="flex gap-2">
|
||||
<button class="ui-btn ui-btn-secondary ui-btn-sm" onclick='editConfig(${JSON.stringify(c)})'>编辑</button>
|
||||
<button class="ui-btn ui-btn-danger ui-btn-sm" onclick="deleteConfig(${c.id})">删除</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
} catch (error) {
|
||||
UI.notify('加载失败: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function showAddModal() {
|
||||
document.getElementById('modalTitle').textContent = '添加健康检查配置';
|
||||
document.getElementById('healthForm').reset();
|
||||
document.getElementById('configId').value = '';
|
||||
document.getElementById('checkInterval').value = '300';
|
||||
document.getElementById('healthModal').classList.add('show');
|
||||
}
|
||||
|
||||
function editConfig(config) {
|
||||
document.getElementById('modalTitle').textContent = '编辑健康检查配置';
|
||||
document.getElementById('configId').value = config.id;
|
||||
document.getElementById('platform').value = config.platform;
|
||||
document.getElementById('testUrl').value = config.test_url;
|
||||
document.getElementById('checkInterval').value = config.check_interval;
|
||||
document.getElementById('alertEmail').value = config.alert_email || '';
|
||||
document.getElementById('isEnabled').value = config.is_enabled;
|
||||
document.getElementById('healthModal').classList.add('show');
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('healthModal').classList.remove('show');
|
||||
}
|
||||
|
||||
async function deleteConfig(id) {
|
||||
UI.confirm('确认删除', '确定要删除此配置吗?', async () => {
|
||||
try {
|
||||
const response = await fetch(`/admin/api/health-checks/${id}`, { method: 'DELETE' });
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
UI.notify('删除成功', 'success');
|
||||
loadHealthChecks();
|
||||
} else {
|
||||
UI.notify('删除失败: ' + result.message, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
UI.notify('删除失败: ' + error.message, 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('healthForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const configId = document.getElementById('configId').value;
|
||||
const data = {
|
||||
platform: document.getElementById('platform').value,
|
||||
test_url: document.getElementById('testUrl').value,
|
||||
check_interval: parseInt(document.getElementById('checkInterval').value),
|
||||
alert_email: document.getElementById('alertEmail').value || null,
|
||||
is_enabled: document.getElementById('isEnabled').value === 'true'
|
||||
};
|
||||
|
||||
try {
|
||||
const url = configId ? `/admin/api/health-checks/${configId}` : '/admin/api/health-checks';
|
||||
const method = configId ? 'PUT' : 'POST';
|
||||
const response = await fetch(url, {
|
||||
method: method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
UI.notify(configId ? '更新成功' : '创建成功', 'success');
|
||||
closeModal();
|
||||
loadHealthChecks();
|
||||
} else {
|
||||
UI.notify('操作失败: ' + result.message, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
UI.notify('操作失败: ' + error.message, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
await fetch('/admin/logout', { method: 'POST' });
|
||||
window.location.href = '/admin/login';
|
||||
} catch (error) {
|
||||
UI.notify('退出失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
loadHealthChecks();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
161
templates/admin_login.html
Normal file
161
templates/admin_login.html
Normal file
@@ -0,0 +1,161 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>管理员登录 - JieXi Admin</title>
|
||||
<link rel="stylesheet" href="/static/css/ui-components.css">
|
||||
<link rel="stylesheet" href="/static/css/admin.css">
|
||||
<style>
|
||||
body {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, var(--primary-50) 0%, var(--secondary-100) 100%);
|
||||
}
|
||||
|
||||
.login-container {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
background: white;
|
||||
border-radius: var(--radius-xl);
|
||||
box-shadow: var(--shadow-xl);
|
||||
padding: 2.5rem;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.login-header .logo {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.login-header h1 {
|
||||
font-size: 1.5rem;
|
||||
color: var(--secondary-900);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.login-header p {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.login-form .form-group {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.login-form label {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--secondary-800);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.login-form .ui-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login-form .submit-btn {
|
||||
width: 100%;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
text-align: center;
|
||||
margin-top: 1.5rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.login-footer a {
|
||||
color: var(--primary-600);
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.login-footer a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<div class="login-card">
|
||||
<div class="login-header">
|
||||
<div class="logo">⚡</div>
|
||||
<h1>JieXi Admin</h1>
|
||||
<p>管理员登录</p>
|
||||
</div>
|
||||
|
||||
<form id="loginForm" class="login-form">
|
||||
<div class="form-group">
|
||||
<label for="username">用户名</label>
|
||||
<input type="text" id="username" class="ui-input" placeholder="请输入用户名" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">密码</label>
|
||||
<input type="password" id="password" class="ui-input" placeholder="请输入密码" required>
|
||||
</div>
|
||||
<div class="form-group" id="2faGroup" style="display:none;">
|
||||
<label for="code_2fa">2FA 验证码</label>
|
||||
<input type="text" id="code_2fa" class="ui-input" placeholder="请输入6位验证码" maxlength="6">
|
||||
</div>
|
||||
<button type="submit" class="ui-btn ui-btn-primary submit-btn">登录</button>
|
||||
</form>
|
||||
|
||||
<div class="login-footer">
|
||||
<a href="/">返回首页</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/ui-components.js"></script>
|
||||
<script>
|
||||
document.getElementById('loginForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const username = document.getElementById('username').value;
|
||||
const password = document.getElementById('password').value;
|
||||
const code_2fa = document.getElementById('code_2fa').value;
|
||||
|
||||
try {
|
||||
const response = await fetch('/admin/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password, code_2fa })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
UI.notify('登录成功!跳转中...', 'success');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/admin/dashboard';
|
||||
}, 1000);
|
||||
} else {
|
||||
if (data.require_2fa) {
|
||||
document.getElementById('2faGroup').style.display = 'block';
|
||||
document.getElementById('code_2fa').focus();
|
||||
}
|
||||
UI.notify(data.message, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
UI.notify('登录失败,请重试', 'error');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
157
templates/admin_logs.html
Normal file
157
templates/admin_logs.html
Normal file
@@ -0,0 +1,157 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>日志审计 - 管理后台</title>
|
||||
<link rel="stylesheet" href="/static/css/ui-components.css?v=3">
|
||||
<link rel="stylesheet" href="/static/css/admin.css">
|
||||
</head>
|
||||
|
||||
<body class="admin-layout">
|
||||
<header class="admin-header">
|
||||
<div class="header-container">
|
||||
<a href="/admin/dashboard" class="brand">
|
||||
<span style="font-size: 1.5rem;">⚡</span> JieXi Admin
|
||||
</a>
|
||||
<nav class="nav-links">
|
||||
<a href="/admin/dashboard" class="nav-item">仪表板</a>
|
||||
<a href="/admin/users" class="nav-item">用户管理</a>
|
||||
<a href="/admin/apis" class="nav-item">接口管理</a>
|
||||
<a href="/admin/config" class="nav-item">系统配置</a>
|
||||
<a href="/admin/logs" class="nav-item active">日志审计</a>
|
||||
</nav>
|
||||
<div class="user-actions">
|
||||
<a href="/admin/profile" class="ui-btn ui-btn-secondary ui-btn-sm">账号设置</a>
|
||||
<button class="ui-btn ui-btn-secondary ui-btn-sm" onclick="logout()">退出登录</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="main-container">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">日志审计</h1>
|
||||
<div class="actions flex gap-2">
|
||||
<select id="platformFilter" class="ui-input" style="width: auto; margin-bottom: 0;"
|
||||
onchange="loadLogs(1)">
|
||||
<option value="">全部平台</option>
|
||||
<option value="douyin">抖音</option>
|
||||
<option value="tiktok">TikTok</option>
|
||||
<option value="bilibili">哔哩哔哩</option>
|
||||
</select>
|
||||
<select id="statusFilter" class="ui-input" style="width: auto; margin-bottom: 0;"
|
||||
onchange="loadLogs(1)">
|
||||
<option value="">全部状态</option>
|
||||
<option value="success">成功</option>
|
||||
<option value="failed">失败</option>
|
||||
<option value="queued">排队中</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui-card">
|
||||
<div class="table-container">
|
||||
<table id="logsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>时间</th>
|
||||
<th>用户ID</th>
|
||||
<th>IP地址</th>
|
||||
<th>平台</th>
|
||||
<th>视频链接</th>
|
||||
<th>状态</th>
|
||||
<th>响应时间</th>
|
||||
<th>错误信息</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="9" style="text-align: center; color: var(--text-muted);">加载中...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="pagination mt-4 flex justify-between items-center" id="pagination"></div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script src="/static/js/ui-components.js"></script>
|
||||
<script>
|
||||
let currentPage = 1;
|
||||
|
||||
async function loadLogs(page = 1) {
|
||||
const platform = document.getElementById('platformFilter').value;
|
||||
const status = document.getElementById('statusFilter').value;
|
||||
|
||||
let url = `/admin/api/logs?page=${page}&per_page=50`;
|
||||
if (platform) url += `&platform=${platform}`;
|
||||
if (status) url += `&status=${status}`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
const tbody = document.querySelector('#logsTable tbody');
|
||||
if (result.data.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="9" style="text-align: center; padding: 2rem; color: var(--text-muted);">暂无日志记录</td></tr>';
|
||||
} else {
|
||||
tbody.innerHTML = result.data.map(log => {
|
||||
let badgeClass = 'badge-neutral';
|
||||
if (log.status === 'success') badgeClass = 'badge-success';
|
||||
else if (log.status === 'failed') badgeClass = 'badge-error';
|
||||
else if (log.status === 'queued') badgeClass = 'badge-warning';
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td>#${log.id}</td>
|
||||
<td class="text-sm text-muted">${new Date(log.created_at).toLocaleString('zh-CN')}</td>
|
||||
<td>${log.user_id || '<span class="text-muted">游客</span>'}</td>
|
||||
<td class="text-sm">${log.ip_address}</td>
|
||||
<td><span class="font-medium">${log.platform}</span></td>
|
||||
<td><button class="ui-btn ui-btn-secondary ui-btn-sm" onclick="copyUrl('${log.video_url}')" title="${log.video_url}">复制链接</button></td>
|
||||
<td><span class="badge ${badgeClass}">${log.status}</span></td>
|
||||
<td class="text-sm">${log.response_time ? (log.response_time / 1000).toFixed(2) + 's' : '-'}</td>
|
||||
<td class="text-sm text-muted" style="max-width: 150px; overflow: hidden; text-overflow: ellipsis;" title="${log.error_message || ''}">${log.error_message || '-'}</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
const pagination = document.getElementById('pagination');
|
||||
pagination.innerHTML = `
|
||||
<button class="ui-btn ui-btn-secondary ui-btn-sm" ${page === 1 ? 'disabled' : ''} onclick="loadLogs(${page - 1})">上一页</button>
|
||||
<span class="text-sm text-muted">第 ${page} / ${result.pagination.pages} 页 (共 ${result.pagination.total} 条)</span>
|
||||
<button class="ui-btn ui-btn-secondary ui-btn-sm" ${page === result.pagination.pages ? 'disabled' : ''} onclick="loadLogs(${page + 1})">下一页</button>
|
||||
`;
|
||||
currentPage = page;
|
||||
}
|
||||
} catch (error) {
|
||||
UI.notify('加载失败: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function copyUrl(url) {
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
UI.notify('链接已复制', 'success');
|
||||
}).catch(() => {
|
||||
UI.notify('复制失败', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
await fetch('/admin/logout', { method: 'POST' });
|
||||
window.location.href = '/admin/login';
|
||||
} catch (error) {
|
||||
UI.notify('退出失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
loadLogs();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
292
templates/admin_profile.html
Normal file
292
templates/admin_profile.html
Normal file
@@ -0,0 +1,292 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>账号管理 - 管理后台</title>
|
||||
<link rel="stylesheet" href="/static/css/ui-components.css?v=3">
|
||||
<link rel="stylesheet" href="/static/css/admin.css">
|
||||
</head>
|
||||
|
||||
<body class="admin-layout">
|
||||
<header class="admin-header">
|
||||
<div class="header-container">
|
||||
<a href="/admin/dashboard" class="brand">
|
||||
<span style="font-size: 1.5rem;">⚡</span> JieXi Admin
|
||||
</a>
|
||||
<nav class="nav-links">
|
||||
<a href="/admin/dashboard" class="nav-item">仪表板</a>
|
||||
<a href="/admin/users" class="nav-item">用户管理</a>
|
||||
<a href="/admin/apis" class="nav-item">接口管理</a>
|
||||
<a href="/admin/config" class="nav-item">系统配置</a>
|
||||
<a href="/admin/logs" class="nav-item">日志审计</a>
|
||||
</nav>
|
||||
<div class="user-actions">
|
||||
<a href="/admin/profile" class="ui-btn ui-btn-secondary ui-btn-sm">账号设置</a>
|
||||
<button class="ui-btn ui-btn-secondary ui-btn-sm" onclick="logout()">退出登录</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="main-container">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">账号管理</h1>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid" style="grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));">
|
||||
<!-- Basic Info Card -->
|
||||
<div class="ui-card">
|
||||
<h3 class="text-lg font-bold mb-4">基本信息</h3>
|
||||
<div class="form-group">
|
||||
<label>用户名</label>
|
||||
<div class="text-lg font-medium" id="username">-</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>邮箱</label>
|
||||
<div class="text-lg" id="email">-</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>2FA状态</label>
|
||||
<div id="2faStatus">-</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>最后登录</label>
|
||||
<div class="text-sm text-muted">
|
||||
<span id="lastLoginAt">-</span> (IP: <span id="lastLoginIp">-</span>)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Change Email Card -->
|
||||
<div class="ui-card">
|
||||
<h3 class="text-lg font-bold mb-4">修改邮箱</h3>
|
||||
<form id="emailForm">
|
||||
<div class="form-group">
|
||||
<label>新邮箱地址</label>
|
||||
<input type="email" id="newEmail" class="ui-input" required placeholder="admin@example.com">
|
||||
</div>
|
||||
<button type="submit" class="ui-btn ui-btn-primary w-full">保存邮箱</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Change Password Card -->
|
||||
<div class="ui-card">
|
||||
<h3 class="text-lg font-bold mb-4">修改密码</h3>
|
||||
<form id="passwordForm">
|
||||
<div class="form-group">
|
||||
<label>原密码</label>
|
||||
<input type="password" id="oldPassword" class="ui-input" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>新密码</label>
|
||||
<input type="password" id="newPassword" class="ui-input" required minlength="8">
|
||||
<small class="text-muted text-sm">密码长度至少8位</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>确认新密码</label>
|
||||
<input type="password" id="confirmPassword" class="ui-input" required minlength="8">
|
||||
</div>
|
||||
<button type="submit" class="ui-btn ui-btn-primary w-full">修改密码</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 2FA Card -->
|
||||
<div class="ui-card">
|
||||
<h3 class="text-lg font-bold mb-4">两步验证 (2FA)</h3>
|
||||
<p class="text-sm text-muted mb-4">启用2FA可以提高账号安全性,需要使用Google Authenticator等应用扫描二维码。</p>
|
||||
|
||||
<div id="2faButtons">
|
||||
<button class="ui-btn ui-btn-success w-full" onclick="enable2FA()">启用2FA</button>
|
||||
</div>
|
||||
|
||||
<div id="2faSetup" style="display: none;">
|
||||
<div style="text-align: center; margin-bottom: 1.5rem;">
|
||||
<img id="qrCode" style="max-width: 200px; border-radius: 8px;">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>验证码</label>
|
||||
<input type="text" id="2faCode" class="ui-input" placeholder="请输入6位验证码" maxlength="6">
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button class="ui-btn ui-btn-success flex-1" onclick="verify2FA()">验证并启用</button>
|
||||
<button class="ui-btn ui-btn-secondary flex-1" onclick="cancel2FA()">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script src="/static/js/ui-components.js"></script>
|
||||
<script>
|
||||
async function loadProfile() {
|
||||
try {
|
||||
const response = await fetch('/admin/api/profile');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
const data = result.data;
|
||||
document.getElementById('username').textContent = data.username;
|
||||
document.getElementById('email').textContent = data.email || '未设置';
|
||||
document.getElementById('newEmail').value = data.email || '';
|
||||
|
||||
const is2faEnabled = data.is_2fa_enabled;
|
||||
document.getElementById('2faStatus').innerHTML = is2faEnabled
|
||||
? '<span class="badge badge-success">已启用</span>'
|
||||
: '<span class="badge badge-neutral">未启用</span>';
|
||||
|
||||
document.getElementById('lastLoginIp').textContent = data.last_login_ip || '-';
|
||||
document.getElementById('lastLoginAt').textContent = data.last_login_at
|
||||
? new Date(data.last_login_at).toLocaleString('zh-CN')
|
||||
: '-';
|
||||
|
||||
document.getElementById('2faButtons').innerHTML = is2faEnabled
|
||||
? '<button class="ui-btn ui-btn-danger w-full" onclick="disable2FA()">禁用2FA</button>'
|
||||
: '<button class="ui-btn ui-btn-success w-full" onclick="enable2FA()">启用2FA</button>';
|
||||
}
|
||||
} catch (error) {
|
||||
UI.notify('加载失败: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('emailForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const email = document.getElementById('newEmail').value;
|
||||
|
||||
try {
|
||||
const response = await fetch('/admin/api/profile/email', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email })
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
UI.notify('邮箱修改成功', 'success');
|
||||
loadProfile();
|
||||
} else {
|
||||
UI.notify(result.message, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
UI.notify('修改失败: ' + error.message, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('passwordForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const oldPassword = document.getElementById('oldPassword').value;
|
||||
const newPassword = document.getElementById('newPassword').value;
|
||||
const confirmPassword = document.getElementById('confirmPassword').value;
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
UI.notify('两次输入的密码不一致', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/admin/api/profile/password', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ old_password: oldPassword, new_password: newPassword })
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
UI.notify('密码修改成功', 'success');
|
||||
document.getElementById('passwordForm').reset();
|
||||
} else {
|
||||
UI.notify(result.message, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
UI.notify('修改失败: ' + error.message, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
async function enable2FA() {
|
||||
try {
|
||||
const response = await fetch('/admin/api/2fa/enable', { method: 'POST' });
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
document.getElementById('qrCode').src = result.qr_code;
|
||||
document.getElementById('2faSetup').style.display = 'block';
|
||||
document.getElementById('2faButtons').style.display = 'none';
|
||||
} else {
|
||||
UI.notify(result.message, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
UI.notify('操作失败: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function verify2FA() {
|
||||
const code = document.getElementById('2faCode').value;
|
||||
if (!code || code.length !== 6) {
|
||||
UI.notify('请输入6位验证码', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/admin/api/2fa/verify', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ code })
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
UI.notify('2FA已启用', 'success');
|
||||
cancel2FA();
|
||||
loadProfile();
|
||||
} else {
|
||||
UI.notify(result.message, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
UI.notify('操作失败: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function cancel2FA() {
|
||||
document.getElementById('2faSetup').style.display = 'none';
|
||||
document.getElementById('2faButtons').style.display = 'block';
|
||||
document.getElementById('2faCode').value = '';
|
||||
}
|
||||
|
||||
async function disable2FA() {
|
||||
UI.prompt('禁用2FA', '请输入2FA验证码', '', async (code) => {
|
||||
if (!code) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/admin/api/2fa/disable', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ code })
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
UI.notify('2FA已禁用', 'success');
|
||||
loadProfile();
|
||||
} else {
|
||||
UI.notify(result.message, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
UI.notify('操作失败: ' + error.message, 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
await fetch('/admin/logout', { method: 'POST' });
|
||||
window.location.href = '/admin/login';
|
||||
} catch (error) {
|
||||
UI.notify('退出失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
loadProfile();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
335
templates/admin_smtp.html
Normal file
335
templates/admin_smtp.html
Normal file
@@ -0,0 +1,335 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SMTP配置 - 管理后台</title>
|
||||
<link rel="stylesheet" href="/static/css/ui-components.css?v=3">
|
||||
<link rel="stylesheet" href="/static/css/admin.css">
|
||||
</head>
|
||||
|
||||
<body class="admin-layout">
|
||||
<header class="admin-header">
|
||||
<div class="header-container">
|
||||
<a href="/admin/dashboard" class="brand">
|
||||
<span style="font-size: 1.5rem;">⚡</span> JieXi Admin
|
||||
</a>
|
||||
<nav class="nav-links">
|
||||
<a href="/admin/dashboard" class="nav-item">仪表板</a>
|
||||
<a href="/admin/users" class="nav-item">用户管理</a>
|
||||
<a href="/admin/apis" class="nav-item">接口管理</a>
|
||||
<a href="/admin/config" class="nav-item active">系统配置</a>
|
||||
<a href="/admin/logs" class="nav-item">日志审计</a>
|
||||
</nav>
|
||||
<div class="user-actions">
|
||||
<a href="/admin/profile" class="ui-btn ui-btn-secondary ui-btn-sm">账号设置</a>
|
||||
<button class="ui-btn ui-btn-secondary ui-btn-sm" onclick="logout()">退出登录</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="main-container">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">SMTP配置</h1>
|
||||
<div class="actions">
|
||||
<a href="/admin/config" class="ui-btn ui-btn-secondary">返回配置</a>
|
||||
<button class="ui-btn ui-btn-primary" onclick="showAddModal()">+ 添加SMTP</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui-card">
|
||||
<div class="table-container">
|
||||
<table id="smtpTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>名称</th>
|
||||
<th>服务器</th>
|
||||
<th>端口</th>
|
||||
<th>用户名</th>
|
||||
<th>发件邮箱</th>
|
||||
<th>权重</th>
|
||||
<th>状态</th>
|
||||
<th>统计</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="10" style="text-align: center; color: var(--text-muted);">加载中...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- SMTP Modal -->
|
||||
<div id="smtpModal" class="ui-modal">
|
||||
<div class="ui-modal-overlay" onclick="closeModal()"></div>
|
||||
<div class="ui-modal-content">
|
||||
<h3 id="modalTitle">添加SMTP配置</h3>
|
||||
<form id="smtpForm">
|
||||
<input type="hidden" id="smtpId">
|
||||
<div class="form-group">
|
||||
<label>配置名称 *</label>
|
||||
<input type="text" id="smtpName" class="ui-input" required placeholder="例如:阿里云邮件">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>SMTP服务器 *</label>
|
||||
<input type="text" id="smtpHost" class="ui-input" required placeholder="smtp.example.com">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>端口 *</label>
|
||||
<input type="number" id="smtpPort" class="ui-input" required value="587" min="1" max="65535">
|
||||
<small class="text-muted text-sm">常用端口:25 (不加密), 587 (TLS), 465 (SSL)</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>用户名 *</label>
|
||||
<input type="text" id="smtpUsername" class="ui-input" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>密码 *</label>
|
||||
<input type="password" id="smtpPassword" class="ui-input" required placeholder="SMTP授权码/密码">
|
||||
<small class="text-muted text-sm" id="smtpPasswordHint">创建时必填,编辑时留空表示不修改</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>发件邮箱 *</label>
|
||||
<input type="email" id="smtpFromEmail" class="ui-input" required placeholder="noreply@example.com">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>发件人名称</label>
|
||||
<input type="text" id="smtpFromName" class="ui-input" placeholder="短视频解析平台">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>使用TLS</label>
|
||||
<select id="smtpUseTls" class="ui-input">
|
||||
<option value="true">是</option>
|
||||
<option value="false">否</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>权重</label>
|
||||
<input type="number" id="smtpWeight" class="ui-input" value="1" min="0" max="100">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>是否启用</label>
|
||||
<select id="smtpEnabled" class="ui-input">
|
||||
<option value="true">启用</option>
|
||||
<option value="false">禁用</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="ui-modal-actions">
|
||||
<button type="button" class="ui-btn ui-btn-secondary" onclick="closeModal()">取消</button>
|
||||
<button type="submit" class="ui-btn ui-btn-primary">保存</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Test Modal -->
|
||||
<div id="testModal" class="ui-modal">
|
||||
<div class="ui-modal-overlay" onclick="closeTestModal()"></div>
|
||||
<div class="ui-modal-content">
|
||||
<h3>测试SMTP配置</h3>
|
||||
<form id="testForm">
|
||||
<div class="form-group">
|
||||
<label>测试邮箱地址</label>
|
||||
<input type="email" id="testEmail" class="ui-input" required placeholder="test@example.com">
|
||||
</div>
|
||||
<div class="ui-modal-actions">
|
||||
<button type="button" class="ui-btn ui-btn-secondary" onclick="closeTestModal()">取消</button>
|
||||
<button type="submit" class="ui-btn ui-btn-success">发送测试邮件</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/ui-components.js"></script>
|
||||
<script>
|
||||
async function loadSmtp() {
|
||||
try {
|
||||
const response = await fetch('/admin/api/smtp');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
const tbody = document.querySelector('#smtpTable tbody');
|
||||
if (result.data.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="10" style="text-align: center; padding: 2rem; color: var(--text-muted);">暂无SMTP配置</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = result.data.map(s => `
|
||||
<tr>
|
||||
<td>${s.id}</td>
|
||||
<td><span class="font-medium">${s.name}</span> ${s.is_default ? '<span class="badge badge-info">默认</span>' : ''}</td>
|
||||
<td class="text-sm">${s.host}</td>
|
||||
<td>${s.port}</td>
|
||||
<td class="text-sm text-muted">${s.username}</td>
|
||||
<td class="text-sm">${s.from_email}</td>
|
||||
<td>${s.weight}</td>
|
||||
<td><span class="badge ${s.is_enabled ? 'badge-success' : 'badge-neutral'}">${s.is_enabled ? '启用' : '禁用'}</span></td>
|
||||
<td class="text-sm">${s.send_count} / <span class="text-error">${s.fail_count}失败</span></td>
|
||||
<td>
|
||||
<div class="flex gap-2">
|
||||
<button class="ui-btn ui-btn-secondary ui-btn-sm" onclick='editSmtp(${JSON.stringify(s)})'>编辑</button>
|
||||
<button class="ui-btn ui-btn-secondary ui-btn-sm" onclick="testSmtp()">测试</button>
|
||||
<button class="ui-btn ui-btn-danger ui-btn-sm" onclick="deleteSmtp(${s.id})">删除</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
} catch (error) {
|
||||
UI.notify('加载失败: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function showAddModal() {
|
||||
const passwordInput = document.getElementById('smtpPassword');
|
||||
const passwordHint = document.getElementById('smtpPasswordHint');
|
||||
document.getElementById('modalTitle').textContent = '添加SMTP配置';
|
||||
document.getElementById('smtpForm').reset();
|
||||
document.getElementById('smtpId').value = '';
|
||||
document.getElementById('smtpPort').value = '587';
|
||||
document.getElementById('smtpWeight').value = '1';
|
||||
passwordInput.value = '';
|
||||
passwordInput.required = true;
|
||||
passwordHint.textContent = '创建时必填,编辑时留空表示不修改';
|
||||
document.getElementById('smtpModal').classList.add('show');
|
||||
}
|
||||
|
||||
function editSmtp(smtp) {
|
||||
const passwordInput = document.getElementById('smtpPassword');
|
||||
const passwordHint = document.getElementById('smtpPasswordHint');
|
||||
document.getElementById('modalTitle').textContent = '编辑SMTP配置';
|
||||
document.getElementById('smtpId').value = smtp.id;
|
||||
document.getElementById('smtpName').value = smtp.name;
|
||||
document.getElementById('smtpHost').value = smtp.host;
|
||||
document.getElementById('smtpPort').value = smtp.port;
|
||||
document.getElementById('smtpUsername').value = smtp.username;
|
||||
passwordInput.value = '';
|
||||
passwordInput.required = false;
|
||||
passwordHint.textContent = '留空表示保留原密码/授权码';
|
||||
document.getElementById('smtpFromEmail').value = smtp.from_email;
|
||||
document.getElementById('smtpFromName').value = smtp.from_name || '';
|
||||
document.getElementById('smtpUseTls').value = smtp.use_tls;
|
||||
document.getElementById('smtpWeight').value = smtp.weight;
|
||||
document.getElementById('smtpEnabled').value = smtp.is_enabled;
|
||||
document.getElementById('smtpModal').classList.add('show');
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('smtpModal').classList.remove('show');
|
||||
}
|
||||
|
||||
function testSmtp() {
|
||||
document.getElementById('testModal').classList.add('show');
|
||||
}
|
||||
|
||||
function closeTestModal() {
|
||||
document.getElementById('testModal').classList.remove('show');
|
||||
}
|
||||
|
||||
async function deleteSmtp(id) {
|
||||
UI.confirm('确认删除', '确定要删除此SMTP配置吗?', async () => {
|
||||
try {
|
||||
const response = await fetch(`/admin/api/smtp/${id}`, { method: 'DELETE' });
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
UI.notify('删除成功', 'success');
|
||||
loadSmtp();
|
||||
} else {
|
||||
UI.notify('删除失败: ' + result.message, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
UI.notify('删除失败: ' + error.message, 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('smtpForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const smtpId = document.getElementById('smtpId').value;
|
||||
const passwordInput = document.getElementById('smtpPassword');
|
||||
const password = passwordInput.value.trim();
|
||||
const data = {
|
||||
name: document.getElementById('smtpName').value.trim(),
|
||||
host: document.getElementById('smtpHost').value.trim(),
|
||||
port: parseInt(document.getElementById('smtpPort').value, 10),
|
||||
username: document.getElementById('smtpUsername').value.trim(),
|
||||
from_email: document.getElementById('smtpFromEmail').value.trim(),
|
||||
from_name: document.getElementById('smtpFromName').value.trim(),
|
||||
use_tls: document.getElementById('smtpUseTls').value === 'true',
|
||||
weight: parseInt(document.getElementById('smtpWeight').value, 10),
|
||||
is_enabled: document.getElementById('smtpEnabled').value === 'true'
|
||||
};
|
||||
|
||||
if (password) {
|
||||
data.password = password;
|
||||
} else if (!smtpId) {
|
||||
UI.notify('请输入SMTP密码/授权码', 'error');
|
||||
passwordInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = smtpId ? `/admin/api/smtp/${smtpId}` : '/admin/api/smtp';
|
||||
const method = smtpId ? 'PUT' : 'POST';
|
||||
const response = await fetch(url, {
|
||||
method: method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
UI.notify(smtpId ? '更新成功' : '创建成功', 'success');
|
||||
closeModal();
|
||||
loadSmtp();
|
||||
} else {
|
||||
UI.notify('操作失败: ' + result.message, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
UI.notify('操作失败: ' + error.message, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('testForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const email = document.getElementById('testEmail').value;
|
||||
|
||||
try {
|
||||
const response = await fetch('/admin/api/smtp/test', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email })
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
UI.notify('测试邮件已发送,请检查收件箱', 'success');
|
||||
closeTestModal();
|
||||
} else {
|
||||
UI.notify('发送失败: ' + result.message, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
UI.notify('发送失败: ' + error.message, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
await fetch('/admin/logout', { method: 'POST' });
|
||||
window.location.href = '/admin/login';
|
||||
} catch (error) {
|
||||
UI.notify('退出失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
loadSmtp();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
400
templates/admin_users.html
Normal file
400
templates/admin_users.html
Normal file
@@ -0,0 +1,400 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>用户管理 - 管理后台</title>
|
||||
<link rel="stylesheet" href="/static/css/ui-components.css?v=3">
|
||||
<link rel="stylesheet" href="/static/css/admin.css">
|
||||
</head>
|
||||
|
||||
<body class="admin-layout">
|
||||
<header class="admin-header">
|
||||
<div class="header-container">
|
||||
<a href="/admin/dashboard" class="brand">
|
||||
<span style="font-size: 1.5rem;">⚡</span> JieXi Admin
|
||||
</a>
|
||||
<nav class="nav-links">
|
||||
<a href="/admin/dashboard" class="nav-item">仪表板</a>
|
||||
<a href="/admin/users" class="nav-item active">用户管理</a>
|
||||
<a href="/admin/apis" class="nav-item">接口管理</a>
|
||||
<a href="/admin/config" class="nav-item">系统配置</a>
|
||||
<a href="/admin/logs" class="nav-item">日志审计</a>
|
||||
</nav>
|
||||
<div class="user-actions">
|
||||
<a href="/admin/profile" class="ui-btn ui-btn-secondary ui-btn-sm">账号设置</a>
|
||||
<button class="ui-btn ui-btn-secondary ui-btn-sm" onclick="logout()">退出登录</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="main-container">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">用户管理</h1>
|
||||
<div class="actions">
|
||||
<button class="ui-btn ui-btn-primary" onclick="showGroupsModal()">管理用户组</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="stats-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 2rem;">
|
||||
<div class="stat-card" style="background: white; border-radius: 12px; padding: 1.5rem; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
||||
<div style="font-size: 0.875rem; color: #6b7280; margin-bottom: 0.5rem;">总用户数</div>
|
||||
<div style="font-size: 2rem; font-weight: 700; color: #1f2937;" id="totalUsers">-</div>
|
||||
</div>
|
||||
<div class="stat-card" style="background: white; border-radius: 12px; padding: 1.5rem; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
||||
<div style="font-size: 0.875rem; color: #6b7280; margin-bottom: 0.5rem;">普通用户</div>
|
||||
<div style="font-size: 2rem; font-weight: 700; color: #6b7280;" id="normalUsers">-</div>
|
||||
</div>
|
||||
<div class="stat-card" style="background: white; border-radius: 12px; padding: 1.5rem; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
||||
<div style="font-size: 0.875rem; color: #6b7280; margin-bottom: 0.5rem;">VIP用户</div>
|
||||
<div style="font-size: 2rem; font-weight: 700; color: #f59e0b;" id="vipUsers">-</div>
|
||||
</div>
|
||||
<div class="stat-card" style="background: white; border-radius: 12px; padding: 1.5rem; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
||||
<div style="font-size: 0.875rem; color: #6b7280; margin-bottom: 0.5rem;">SVIP用户</div>
|
||||
<div style="font-size: 2rem; font-weight: 700; color: #8b5cf6;" id="svipUsers">-</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 筛选器 -->
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<select id="groupFilter" class="ui-input" style="width: 200px;" onchange="loadUsers(1)">
|
||||
<option value="">全部用户组</option>
|
||||
<option value="2">普通用户</option>
|
||||
<option value="3">VIP</option>
|
||||
<option value="4">SVIP</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="ui-card">
|
||||
<div class="table-container">
|
||||
<table id="usersTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>用户名</th>
|
||||
<th>邮箱</th>
|
||||
<th>用户组</th>
|
||||
<th>解析次数</th>
|
||||
<th>状态</th>
|
||||
<th>注册时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="8" style="text-align: center; color: var(--text-muted);">加载中...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="pagination mt-4 flex justify-between items-center" id="pagination">
|
||||
<!-- Pagination controls will be injected here -->
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Edit User Modal -->
|
||||
<div id="editModal" class="ui-modal">
|
||||
<div class="ui-modal-overlay" onclick="closeModal()"></div>
|
||||
<div class="ui-modal-content">
|
||||
<h3>编辑用户</h3>
|
||||
<form id="editForm">
|
||||
<input type="hidden" id="editUserId">
|
||||
<div class="form-group">
|
||||
<label>用户分组</label>
|
||||
<select id="editGroupId" class="ui-input"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>账号状态</label>
|
||||
<select id="editIsActive" class="ui-input">
|
||||
<option value="true">激活</option>
|
||||
<option value="false">禁用</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="ui-modal-actions">
|
||||
<button type="button" class="ui-btn ui-btn-secondary" onclick="closeModal()">取消</button>
|
||||
<button type="submit" class="ui-btn ui-btn-primary">保存更改</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Groups Management Modal -->
|
||||
<div id="groupsModal" class="ui-modal">
|
||||
<div class="ui-modal-overlay" onclick="closeGroupsModal()"></div>
|
||||
<div class="ui-modal-content" style="max-width: 700px;">
|
||||
<h3>用户组管理</h3>
|
||||
<div class="table-container" style="margin-top: 1rem;">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>分组名称</th>
|
||||
<th>每日解析次数</th>
|
||||
<th>描述</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="groupsTableBody">
|
||||
<tr><td colspan="4" style="text-align: center;">加载中...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="ui-modal-actions" style="margin-top: 1.5rem;">
|
||||
<button type="button" class="ui-btn ui-btn-secondary" onclick="closeGroupsModal()">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Group Modal -->
|
||||
<div id="editGroupModal" class="ui-modal">
|
||||
<div class="ui-modal-overlay" onclick="closeEditGroupModal()"></div>
|
||||
<div class="ui-modal-content">
|
||||
<h3>编辑用户组</h3>
|
||||
<form id="editGroupForm">
|
||||
<input type="hidden" id="editGroupIdInput">
|
||||
<div class="form-group">
|
||||
<label>分组名称</label>
|
||||
<input type="text" id="editGroupName" class="ui-input" readonly>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>每日解析次数</label>
|
||||
<input type="number" id="editGroupLimit" class="ui-input" min="0" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>描述</label>
|
||||
<textarea id="editGroupDesc" class="ui-input" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="ui-modal-actions">
|
||||
<button type="button" class="ui-btn ui-btn-secondary" onclick="closeEditGroupModal()">取消</button>
|
||||
<button type="submit" class="ui-btn ui-btn-primary">保存更改</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/ui-components.js"></script>
|
||||
<script>
|
||||
let currentPage = 1;
|
||||
let groups = [];
|
||||
|
||||
async function loadGroups() {
|
||||
try {
|
||||
const response = await fetch('/admin/api/groups');
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
groups = result.data;
|
||||
const select = document.getElementById('editGroupId');
|
||||
select.innerHTML = groups.map(g => `<option value="${g.id}">${g.name} (${g.daily_limit}次/天)</option>`).join('');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load groups', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadUserStats() {
|
||||
try {
|
||||
const response = await fetch('/admin/api/users?page=1&per_page=1000');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
const users = result.data;
|
||||
const total = users.length;
|
||||
const normal = users.filter(u => u.group_id === 2).length;
|
||||
const vip = users.filter(u => u.group_id === 3).length;
|
||||
const svip = users.filter(u => u.group_id === 4).length;
|
||||
|
||||
document.getElementById('totalUsers').textContent = total;
|
||||
document.getElementById('normalUsers').textContent = normal;
|
||||
document.getElementById('vipUsers').textContent = vip;
|
||||
document.getElementById('svipUsers').textContent = svip;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load user stats', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadUsers(page = 1) {
|
||||
try {
|
||||
const groupFilter = document.getElementById('groupFilter').value;
|
||||
let url = `/admin/api/users?page=${page}&per_page=20`;
|
||||
if (groupFilter) {
|
||||
url += `&group_id=${groupFilter}`;
|
||||
}
|
||||
|
||||
const response = await fetch(url);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
const tbody = document.querySelector('#usersTable tbody');
|
||||
tbody.innerHTML = result.data.map(u => `
|
||||
<tr>
|
||||
<td>#${u.id}</td>
|
||||
<td><span class="font-medium">${u.username}</span></td>
|
||||
<td class="text-muted">${u.email}</td>
|
||||
<td><span class="badge badge-info">${u.group_name}</span></td>
|
||||
<td>${u.total_parse_count}</td>
|
||||
<td>
|
||||
<span class="badge ${u.is_active ? 'badge-success' : 'badge-error'}">
|
||||
${u.is_active ? '正常' : '禁用'}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-muted text-sm">${new Date(u.created_at).toLocaleString('zh-CN')}</td>
|
||||
<td>
|
||||
<button class="ui-btn ui-btn-secondary ui-btn-sm"
|
||||
onclick="editUser(${u.id}, ${u.group_id}, ${u.is_active})">
|
||||
编辑
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
|
||||
const pagination = document.getElementById('pagination');
|
||||
pagination.innerHTML = `
|
||||
<button class="ui-btn ui-btn-secondary ui-btn-sm"
|
||||
${page === 1 ? 'disabled' : ''}
|
||||
onclick="loadUsers(${page - 1})">上一页</button>
|
||||
<span class="text-sm text-muted">第 ${page} / ${result.pagination.pages} 页</span>
|
||||
<button class="ui-btn ui-btn-secondary ui-btn-sm"
|
||||
${page === result.pagination.pages ? 'disabled' : ''}
|
||||
onclick="loadUsers(${page + 1})">下一页</button>
|
||||
`;
|
||||
currentPage = page;
|
||||
}
|
||||
} catch (error) {
|
||||
UI.notify('加载用户列表失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function editUser(id, groupId, isActive) {
|
||||
document.getElementById('editUserId').value = id;
|
||||
document.getElementById('editGroupId').value = groupId;
|
||||
document.getElementById('editIsActive').value = isActive;
|
||||
document.getElementById('editModal').classList.add('show');
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('editModal').classList.remove('show');
|
||||
}
|
||||
|
||||
async function showGroupsModal() {
|
||||
document.getElementById('groupsModal').classList.add('show');
|
||||
await loadGroupsTable();
|
||||
}
|
||||
|
||||
function closeGroupsModal() {
|
||||
document.getElementById('groupsModal').classList.remove('show');
|
||||
}
|
||||
|
||||
async function loadGroupsTable() {
|
||||
try {
|
||||
const response = await fetch('/admin/api/groups');
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
const tbody = document.getElementById('groupsTableBody');
|
||||
tbody.innerHTML = result.data.map(g => `
|
||||
<tr>
|
||||
<td><span class="font-medium">${g.name}</span></td>
|
||||
<td><span class="badge badge-info">${g.daily_limit}次/天</span></td>
|
||||
<td class="text-sm text-muted">${g.description || '-'}</td>
|
||||
<td>
|
||||
<button class="ui-btn ui-btn-secondary ui-btn-sm"
|
||||
onclick='editGroup(${JSON.stringify(g)})'>
|
||||
编辑
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
} catch (error) {
|
||||
UI.notify('加载用户组失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function editGroup(group) {
|
||||
document.getElementById('editGroupIdInput').value = group.id;
|
||||
document.getElementById('editGroupName').value = group.name;
|
||||
document.getElementById('editGroupLimit').value = group.daily_limit;
|
||||
document.getElementById('editGroupDesc').value = group.description || '';
|
||||
document.getElementById('editGroupModal').classList.add('show');
|
||||
}
|
||||
|
||||
function closeEditGroupModal() {
|
||||
document.getElementById('editGroupModal').classList.remove('show');
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
await fetch('/admin/logout', { method: 'POST' });
|
||||
window.location.href = '/admin/login';
|
||||
} catch (error) {
|
||||
UI.notify('退出失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('editForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const userId = document.getElementById('editUserId').value;
|
||||
const data = {
|
||||
group_id: parseInt(document.getElementById('editGroupId').value),
|
||||
is_active: document.getElementById('editIsActive').value === 'true'
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(`/admin/api/users/${userId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
UI.notify('更新成功', 'success');
|
||||
closeModal();
|
||||
loadUsers(currentPage);
|
||||
} else {
|
||||
UI.notify(result.message || '更新失败', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
UI.notify('网络错误', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('editGroupForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const groupId = document.getElementById('editGroupIdInput').value;
|
||||
const data = {
|
||||
daily_limit: parseInt(document.getElementById('editGroupLimit').value),
|
||||
description: document.getElementById('editGroupDesc').value
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(`/admin/api/groups/${groupId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
UI.notify('用户组更新成功', 'success');
|
||||
closeEditGroupModal();
|
||||
await loadGroupsTable();
|
||||
await loadGroups(); // 刷新用户编辑表单中的分组列表
|
||||
} else {
|
||||
UI.notify(result.message || '更新失败', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
UI.notify('网络错误', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
loadGroups();
|
||||
loadUsers();
|
||||
loadUserStats();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
460
templates/index.html
Normal file
460
templates/index.html
Normal file
@@ -0,0 +1,460 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>短视频解析平台 - 免费高清无水印</title>
|
||||
<link rel="stylesheet" href="/static/css/ui-components.css?v=3">
|
||||
<style>
|
||||
body {
|
||||
background: linear-gradient(135deg, #f0f9ff 0%, #e0e7ff 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.hero-container {
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
animation: fadeIn 0.8s ease-out;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 3rem;
|
||||
font-weight: 800;
|
||||
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
margin-bottom: 0.5rem;
|
||||
letter-spacing: -0.05em;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.125rem;
|
||||
color: var(--secondary-500);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.main-card {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 24px;
|
||||
padding: 3rem;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.15);
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||
animation: slideUp 0.8s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.url-input {
|
||||
width: 100%;
|
||||
padding: 1.25rem 1.5rem;
|
||||
font-size: 1rem;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 16px;
|
||||
background: #f1f5f9;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.url-input:focus {
|
||||
outline: none;
|
||||
background: white;
|
||||
border-color: var(--primary-500);
|
||||
box-shadow: 0 0 0 4px var(--primary-100);
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 100%;
|
||||
padding: 1.25rem;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 10px 20px -5px rgba(79, 70, 229, 0.4);
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 15px 30px -5px rgba(79, 70, 229, 0.5);
|
||||
}
|
||||
|
||||
.action-btn:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.action-btn:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.result-card {
|
||||
margin-top: 2rem;
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
display: none;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
animation: fadeIn 0.5s ease;
|
||||
}
|
||||
|
||||
.result-card.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.video-preview {
|
||||
width: 100%;
|
||||
aspect-ratio: 16/9;
|
||||
background: black;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.result-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.video-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: var(--secondary-900);
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.video-meta {
|
||||
font-size: 0.875rem;
|
||||
color: var(--secondary-500);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.download-actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.download-actions {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
color: var(--secondary-500);
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
color: var(--primary-600);
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 3px solid rgba(255,255,255,0.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: white;
|
||||
animation: spin 1s linear infinite;
|
||||
display: none;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.btn-text { display: inline; }
|
||||
.action-btn.loading .btn-text { display: none; }
|
||||
.action-btn.loading .loading-spinner { display: block; }
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{% if config.get('site_notice') %}
|
||||
<div style="background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%); padding: 1rem; text-align: center; border-radius: 12px; margin: 1rem auto; max-width: 700px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
|
||||
<div style="color: #92400e; font-weight: 500; margin: 0;">{{ config.get('site_notice') | safe }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="hero-container">
|
||||
<h1 class="logo-text">{{ config.get('home_title', 'JieXi Pro') }}</h1>
|
||||
<p class="subtitle">{{ config.get('home_subtitle', '新一代全能短视频去水印解析工具') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="main-card">
|
||||
<div class="input-wrapper">
|
||||
<input type="text" class="url-input" id="videoUrl" placeholder="在此粘贴 抖音/TikTok/B站 视频链接...">
|
||||
</div>
|
||||
<button class="action-btn" onclick="parseVideo()" id="parseBtn">
|
||||
<span class="btn-text">立即解析</span>
|
||||
<div class="loading-spinner"></div>
|
||||
</button>
|
||||
|
||||
<div id="result" class="result-card">
|
||||
<video id="videoPlayer" class="video-preview" controls></video>
|
||||
<div class="result-content">
|
||||
<h3 class="video-title" id="title"></h3>
|
||||
<p class="video-meta" id="description"></p>
|
||||
<div class="download-actions">
|
||||
<a id="videoLink" class="ui-btn ui-btn-primary" href="" download target="_blank">
|
||||
下载视频
|
||||
</a>
|
||||
<button class="ui-btn ui-btn-primary" onclick="downloadCover()">
|
||||
下载封面
|
||||
</button>
|
||||
<button class="ui-btn ui-btn-secondary" onclick="copyLink()">
|
||||
复制链接
|
||||
</button>
|
||||
</div>
|
||||
<a id="coverLink" href="" style="display: none;"></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nav-links" id="navLinks">
|
||||
<a href="/auth/login" class="nav-link">登录账号</a>
|
||||
<a href="/auth/register" class="nav-link">注册新用户</a>
|
||||
</div>
|
||||
|
||||
{% if config.get('site_footer') %}
|
||||
<footer style="margin-top: 3rem; text-align: center; color: var(--secondary-500); font-size: 0.875rem; line-height: 1.8;">
|
||||
{{ config.get('site_footer') | safe }}
|
||||
</footer>
|
||||
{% endif %}
|
||||
|
||||
<script src="/static/js/ui-components.js"></script>
|
||||
<script>
|
||||
function extractVideoUrl(text) {
|
||||
const patterns = [
|
||||
/https?:\/\/v\.douyin\.com\/[A-Za-z0-9_-]+/,
|
||||
/https?:\/\/www\.douyin\.com\/video\/\d+/,
|
||||
/https?:\/\/(?:www\.)?bilibili\.com\/video\/[A-Za-z0-9?&=]+/,
|
||||
/https?:\/\/b23\.tv\/[A-Za-z0-9]+/,
|
||||
/https?:\/\/(?:www\.)?tiktok\.com\/@[^\/]+\/video\/\d+/
|
||||
];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const match = text.match(pattern);
|
||||
if (match) return match[0];
|
||||
}
|
||||
return text.trim();
|
||||
}
|
||||
|
||||
async function parseVideo() {
|
||||
const input = document.getElementById('videoUrl');
|
||||
const btn = document.getElementById('parseBtn');
|
||||
const resultCard = document.getElementById('result');
|
||||
|
||||
const rawUrl = input.value.trim();
|
||||
if (!rawUrl) {
|
||||
UI.notify('请先粘贴视频链接', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const url = extractVideoUrl(rawUrl);
|
||||
if (!url) {
|
||||
UI.notify('无法识别有效的视频链接', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset UI
|
||||
resultCard.classList.remove('show');
|
||||
btn.disabled = true;
|
||||
btn.classList.add('loading');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/parse', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ url: url })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
if (data.status === 'completed') {
|
||||
showResult(data.data);
|
||||
} else if (data.status === 'queued') {
|
||||
pollTaskResult(data.task_id);
|
||||
}
|
||||
} else {
|
||||
throw new Error(data.message || '解析失败');
|
||||
}
|
||||
} catch (error) {
|
||||
UI.notify(error.message, 'error');
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('loading');
|
||||
}
|
||||
}
|
||||
|
||||
async function pollTaskResult(taskId) {
|
||||
const maxAttempts = 30;
|
||||
let attempts = 0;
|
||||
const btn = document.getElementById('parseBtn');
|
||||
|
||||
const poll = async () => {
|
||||
if (attempts >= maxAttempts) {
|
||||
UI.notify('解析超时,请稍后重试', 'error');
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('loading');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/task/${taskId}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 'completed') {
|
||||
showResult(data.data);
|
||||
} else if (data.status === 'failed') {
|
||||
throw new Error(data.message || '解析失败');
|
||||
} else {
|
||||
attempts++;
|
||||
setTimeout(poll, 2000);
|
||||
}
|
||||
} catch (error) {
|
||||
UI.notify(error.message, 'error');
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('loading');
|
||||
}
|
||||
};
|
||||
|
||||
poll();
|
||||
}
|
||||
|
||||
function showResult(data) {
|
||||
const btn = document.getElementById('parseBtn');
|
||||
const resultCard = document.getElementById('result');
|
||||
|
||||
document.getElementById('title').textContent = data.title || '无标题';
|
||||
document.getElementById('description').textContent = data.description || '';
|
||||
|
||||
const player = document.getElementById('videoPlayer');
|
||||
player.src = data.video_url;
|
||||
player.poster = data.cover;
|
||||
|
||||
document.getElementById('videoLink').href = data.video_url;
|
||||
document.getElementById('coverLink').href = data.cover;
|
||||
|
||||
resultCard.classList.add('show');
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('loading');
|
||||
|
||||
UI.notify('解析成功', 'success');
|
||||
}
|
||||
|
||||
async function downloadCover() {
|
||||
const coverUrl = document.getElementById('coverLink').href;
|
||||
if (!coverUrl) {
|
||||
UI.notify('封面链接无效', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
UI.notify('正在下载封面...', 'info');
|
||||
|
||||
// 使用 fetch 下载,设置 no-referrer 策略
|
||||
const response = await fetch(coverUrl, {
|
||||
method: 'GET',
|
||||
referrerPolicy: 'no-referrer'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('下载失败');
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'cover_' + Date.now() + '.jpg';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
UI.notify('封面下载成功', 'success');
|
||||
} catch (error) {
|
||||
UI.notify('下载失败: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function copyLink() {
|
||||
const url = document.getElementById('videoLink').href;
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
UI.notify('链接已复制', 'success');
|
||||
}).catch(() => {
|
||||
UI.notify('复制失败', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('videoUrl').addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') parseVideo();
|
||||
});
|
||||
|
||||
// 检查登录状态并更新导航
|
||||
async function checkLoginStatus() {
|
||||
try {
|
||||
const response = await fetch('/auth/user-info');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
document.getElementById('navLinks').innerHTML = `
|
||||
<a href="/auth/profile" class="nav-link">个人中心</a>
|
||||
<a href="#" class="nav-link" onclick="logout(); return false;">退出登录</a>
|
||||
`;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// 未登录,保持默认导航
|
||||
}
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
await fetch('/auth/logout', { method: 'POST' });
|
||||
UI.notify('已退出登录', 'success');
|
||||
setTimeout(() => location.reload(), 1000);
|
||||
} catch (e) {
|
||||
UI.notify('退出失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
checkLoginStatus();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
243
templates/login.html
Normal file
243
templates/login.html
Normal file
@@ -0,0 +1,243 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>用户登录 - JieXi Pro</title>
|
||||
<link rel="stylesheet" href="/static/css/ui-components.css">
|
||||
<style>
|
||||
body {
|
||||
background: linear-gradient(135deg, #f0f9ff 0%, #e0e7ff 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.auth-container {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 24px;
|
||||
padding: 3rem;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.15);
|
||||
width: 100%;
|
||||
max-width: 450px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.auth-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.auth-title {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.auth-subtitle {
|
||||
color: var(--secondary-500);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--secondary-700);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 0.875rem 1rem;
|
||||
font-size: 0.9375rem;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 12px;
|
||||
background: #f1f5f9;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
background: white;
|
||||
border-color: var(--primary-500);
|
||||
box-shadow: 0 0 0 4px var(--primary-100);
|
||||
}
|
||||
|
||||
.form-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.remember-me {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--secondary-600);
|
||||
}
|
||||
|
||||
.forgot-link {
|
||||
font-size: 0.875rem;
|
||||
color: var(--primary-600);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.forgot-link:hover {
|
||||
color: var(--primary-700);
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 10px 20px -5px rgba(79, 70, 229, 0.4);
|
||||
}
|
||||
|
||||
.submit-btn:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 15px 30px -5px rgba(79, 70, 229, 0.5);
|
||||
}
|
||||
|
||||
.submit-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.auth-footer {
|
||||
text-align: center;
|
||||
margin-top: 1.5rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid var(--secondary-200);
|
||||
}
|
||||
|
||||
.auth-link {
|
||||
color: var(--primary-600);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.auth-link:hover {
|
||||
color: var(--primary-700);
|
||||
}
|
||||
|
||||
.back-home {
|
||||
display: inline-block;
|
||||
margin-top: 1rem;
|
||||
color: var(--secondary-500);
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.back-home:hover {
|
||||
color: var(--secondary-700);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="auth-container">
|
||||
<div class="auth-header">
|
||||
<h1 class="auth-title">欢迎回来</h1>
|
||||
<p class="auth-subtitle">登录您的账号继续使用</p>
|
||||
</div>
|
||||
|
||||
<form id="loginForm">
|
||||
<div class="form-group">
|
||||
<label class="form-label">邮箱地址</label>
|
||||
<input type="email" id="email" class="form-input" placeholder="your@email.com" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">密码</label>
|
||||
<input type="password" id="password" class="form-input" placeholder="请输入密码" required>
|
||||
</div>
|
||||
|
||||
<div class="form-footer">
|
||||
<label class="remember-me">
|
||||
<input type="checkbox" id="rememberMe">
|
||||
<span>记住我</span>
|
||||
</label>
|
||||
<a href="#" class="forgot-link" onclick="showForgotPassword(); return false;">忘记密码?</a>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="submit-btn">立即登录</button>
|
||||
</form>
|
||||
|
||||
<div class="auth-footer">
|
||||
<span class="text-muted text-sm">还没有账号?</span>
|
||||
<a href="/auth/register" class="auth-link">立即注册</a>
|
||||
<br>
|
||||
<a href="/" class="back-home">← 返回首页</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/ui-components.js"></script>
|
||||
<script>
|
||||
function showForgotPassword() {
|
||||
UI.notify('密码重置功能开发中...', 'info');
|
||||
}
|
||||
|
||||
document.getElementById('loginForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const email = document.getElementById('email').value.trim();
|
||||
const password = document.getElementById('password').value;
|
||||
|
||||
if (!email || !password) {
|
||||
UI.notify('请填写完整信息', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const submitBtn = e.target.querySelector('.submit-btn');
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = '登录中...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
UI.notify('登录成功!即将跳转...', 'success');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
}, 1000);
|
||||
} else {
|
||||
UI.notify(result.message || '登录失败', 'error');
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = '立即登录';
|
||||
}
|
||||
} catch (error) {
|
||||
UI.notify('网络错误,请稍后重试', 'error');
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = '立即登录';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
447
templates/profile.html
Normal file
447
templates/profile.html
Normal file
@@ -0,0 +1,447 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>个人中心 - 短视频解析平台</title>
|
||||
<link rel="stylesheet" href="/static/css/ui-components.css?v=3">
|
||||
<style>
|
||||
body {
|
||||
background: linear-gradient(135deg, #f0f9ff 0%, #e0e7ff 100%);
|
||||
min-height: 100vh;
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--secondary-900);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 1.5rem;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: var(--secondary-900);
|
||||
}
|
||||
|
||||
.stat-value.primary {
|
||||
color: var(--primary-600);
|
||||
}
|
||||
|
||||
.stat-value.success {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.stat-value.warning {
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.info-card {
|
||||
background: white;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 1.5rem;
|
||||
box-shadow: var(--shadow-md);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.info-card h3 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: var(--secondary-900);
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 0;
|
||||
border-bottom: 1px solid var(--secondary-100);
|
||||
}
|
||||
|
||||
.info-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-weight: 500;
|
||||
color: var(--secondary-800);
|
||||
}
|
||||
|
||||
.group-badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.group-badge.normal {
|
||||
background: var(--secondary-100);
|
||||
color: var(--secondary-700);
|
||||
}
|
||||
|
||||
.group-badge.vip {
|
||||
background: linear-gradient(135deg, #fbbf24, #f59e0b);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.group-badge.svip {
|
||||
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.logs-card {
|
||||
background: white;
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-md);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.logs-header {
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.logs-header h3 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: var(--secondary-900);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.logs-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.logs-table th,
|
||||
.logs-table td {
|
||||
padding: 0.875rem 1rem;
|
||||
text-align: left;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.logs-table th {
|
||||
background: var(--secondary-50);
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.logs-table tr:not(:last-child) td {
|
||||
border-bottom: 1px solid var(--secondary-100);
|
||||
}
|
||||
|
||||
.logs-table .platform {
|
||||
font-weight: 500;
|
||||
color: var(--secondary-800);
|
||||
}
|
||||
|
||||
.logs-table .url {
|
||||
color: var(--primary-600);
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.logs-table .url:hover {
|
||||
color: var(--primary-700);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-badge.success {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.status-badge.failed {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 8px;
|
||||
background: var(--secondary-100);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, var(--primary-500), var(--primary-600));
|
||||
border-radius: 4px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.logs-table {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">个人中心</h1>
|
||||
<div class="header-actions">
|
||||
<a href="/" class="ui-btn ui-btn-secondary">返回首页</a>
|
||||
<button class="ui-btn ui-btn-danger" onclick="logout()">退出登录</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 使用统计 -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">今日已用</div>
|
||||
<div class="stat-value primary" id="todayUsed">-</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">今日剩余</div>
|
||||
<div class="stat-value success" id="todayRemaining">-</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">每日限额</div>
|
||||
<div class="stat-value" id="dailyLimit">-</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">累计解析</div>
|
||||
<div class="stat-value" id="totalCount">-</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户信息 -->
|
||||
<div class="info-card">
|
||||
<h3>账户信息</h3>
|
||||
<div class="info-row">
|
||||
<span class="info-label">用户名</span>
|
||||
<span class="info-value" id="username">-</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">邮箱</span>
|
||||
<span class="info-value" id="email">-</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">用户组</span>
|
||||
<span class="info-value" id="groupName">-</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">注册时间</span>
|
||||
<span class="info-value" id="createdAt">-</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">今日使用进度</span>
|
||||
<span class="info-value" id="usageText">-</span>
|
||||
</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="usageProgress" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 解析记录 -->
|
||||
<div class="logs-card">
|
||||
<div class="logs-header">
|
||||
<h3>解析记录(最近20条)</h3>
|
||||
</div>
|
||||
<table class="logs-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>平台</th>
|
||||
<th>视频链接</th>
|
||||
<th>状态</th>
|
||||
<th>耗时</th>
|
||||
<th>时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="logsBody">
|
||||
<tr>
|
||||
<td colspan="5" class="empty-state">加载中...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/ui-components.js"></script>
|
||||
<script>
|
||||
async function loadProfile() {
|
||||
try {
|
||||
const response = await fetch('/auth/api/profile');
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
if (response.status === 401) {
|
||||
window.location.href = '/auth/login';
|
||||
return;
|
||||
}
|
||||
throw new Error(result.message || '加载失败');
|
||||
}
|
||||
|
||||
const data = result.data;
|
||||
|
||||
// 更新统计数据
|
||||
document.getElementById('todayUsed').textContent = data.usage.today_used;
|
||||
document.getElementById('todayRemaining').textContent = data.usage.today_remaining;
|
||||
document.getElementById('dailyLimit').textContent = data.usage.daily_limit;
|
||||
document.getElementById('totalCount').textContent = data.usage.total_parse_count;
|
||||
|
||||
// 更新用户信息
|
||||
document.getElementById('username').textContent = data.user.username;
|
||||
document.getElementById('email').textContent = data.user.email;
|
||||
document.getElementById('createdAt').textContent = data.user.created_at || '-';
|
||||
|
||||
// 用户组徽章
|
||||
const groupBadge = getGroupBadge(data.group.name);
|
||||
document.getElementById('groupName').innerHTML = groupBadge;
|
||||
|
||||
// 使用进度
|
||||
const usagePercent = data.usage.daily_limit > 0
|
||||
? Math.round((data.usage.today_used / data.usage.daily_limit) * 100)
|
||||
: 0;
|
||||
document.getElementById('usageText').textContent = `${data.usage.today_used} / ${data.usage.daily_limit}`;
|
||||
document.getElementById('usageProgress').style.width = `${Math.min(usagePercent, 100)}%`;
|
||||
|
||||
// 更新解析记录
|
||||
const logsBody = document.getElementById('logsBody');
|
||||
if (data.parse_logs.length === 0) {
|
||||
logsBody.innerHTML = '<tr><td colspan="5" class="empty-state">暂无解析记录</td></tr>';
|
||||
} else {
|
||||
logsBody.innerHTML = data.parse_logs.map((log, index) => `
|
||||
<tr>
|
||||
<td class="platform">${getPlatformName(log.platform)}</td>
|
||||
<td class="url" title="点击复制链接" data-url="${log.video_url}" onclick="copyUrlFromElement(this)">${log.video_url}</td>
|
||||
<td><span class="status-badge ${log.status === 'success' ? 'success' : 'failed'}">${log.status === 'success' ? '成功' : '失败'}</span></td>
|
||||
<td>${log.response_time ? (log.response_time / 1000).toFixed(2) + 's' : '-'}</td>
|
||||
<td>${log.created_at}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
UI.notify('加载失败: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function getGroupBadge(groupName) {
|
||||
const name = groupName.toLowerCase();
|
||||
if (name.includes('svip')) {
|
||||
return `<span class="group-badge svip">${groupName}</span>`;
|
||||
} else if (name.includes('vip')) {
|
||||
return `<span class="group-badge vip">${groupName}</span>`;
|
||||
}
|
||||
return `<span class="group-badge normal">${groupName}</span>`;
|
||||
}
|
||||
|
||||
function getPlatformName(platform) {
|
||||
const names = {
|
||||
'douyin': '抖音',
|
||||
'tiktok': 'TikTok',
|
||||
'bilibili': 'B站'
|
||||
};
|
||||
return names[platform] || platform;
|
||||
}
|
||||
|
||||
function copyUrlFromElement(element) {
|
||||
const url = element.getAttribute('data-url');
|
||||
if (!url) {
|
||||
UI.notify('无法获取链接', 'error');
|
||||
return;
|
||||
}
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
UI.notify('链接已复制', 'success');
|
||||
}).catch(() => {
|
||||
UI.notify('复制失败', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
await fetch('/auth/logout', { method: 'POST' });
|
||||
UI.notify('已退出登录', 'success');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
UI.notify('退出失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
loadProfile();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
305
templates/register.html
Normal file
305
templates/register.html
Normal file
@@ -0,0 +1,305 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>用户注册 - JieXi Pro</title>
|
||||
<link rel="stylesheet" href="/static/css/ui-components.css">
|
||||
<style>
|
||||
body {
|
||||
background: linear-gradient(135deg, #f0f9ff 0%, #e0e7ff 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.auth-container {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 24px;
|
||||
padding: 3rem;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.15);
|
||||
width: 100%;
|
||||
max-width: 450px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.auth-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.auth-title {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.auth-subtitle {
|
||||
color: var(--secondary-500);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--secondary-700);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 0.875rem 1rem;
|
||||
font-size: 0.9375rem;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 12px;
|
||||
background: #f1f5f9;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
background: white;
|
||||
border-color: var(--primary-500);
|
||||
box-shadow: 0 0 0 4px var(--primary-100);
|
||||
}
|
||||
|
||||
.code-group {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.code-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.code-btn {
|
||||
padding: 0.875rem 1.5rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 10px 20px -5px rgba(79, 70, 229, 0.4);
|
||||
}
|
||||
|
||||
.submit-btn:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 15px 30px -5px rgba(79, 70, 229, 0.5);
|
||||
}
|
||||
|
||||
.submit-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.auth-footer {
|
||||
text-align: center;
|
||||
margin-top: 1.5rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid var(--secondary-200);
|
||||
}
|
||||
|
||||
.auth-link {
|
||||
color: var(--primary-600);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.auth-link:hover {
|
||||
color: var(--primary-700);
|
||||
}
|
||||
|
||||
.back-home {
|
||||
display: inline-block;
|
||||
margin-top: 1rem;
|
||||
color: var(--secondary-500);
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.back-home:hover {
|
||||
color: var(--secondary-700);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="auth-container">
|
||||
<div class="auth-header">
|
||||
<h1 class="auth-title">创建账号</h1>
|
||||
<p class="auth-subtitle">注册后享受更多解析次数</p>
|
||||
</div>
|
||||
|
||||
<form id="registerForm">
|
||||
<div class="form-group">
|
||||
<label class="form-label">用户名</label>
|
||||
<input type="text" id="username" class="form-input" placeholder="请输入用户名" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">邮箱地址</label>
|
||||
<input type="email" id="email" class="form-input" placeholder="your@email.com" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">邮箱验证码</label>
|
||||
<div class="code-group">
|
||||
<input type="text" id="code" class="form-input code-input" placeholder="请输入验证码" required>
|
||||
<button type="button" class="ui-btn ui-btn-secondary code-btn" id="sendCodeBtn" onclick="sendCode()">
|
||||
发送验证码
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">密码</label>
|
||||
<input type="password" id="password" class="form-input" placeholder="至少6位字符" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">确认密码</label>
|
||||
<input type="password" id="confirmPassword" class="form-input" placeholder="再次输入密码" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="submit-btn">立即注册</button>
|
||||
</form>
|
||||
|
||||
<div class="auth-footer">
|
||||
<span class="text-muted text-sm">已有账号?</span>
|
||||
<a href="/auth/login" class="auth-link">立即登录</a>
|
||||
<br>
|
||||
<a href="/" class="back-home">← 返回首页</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/ui-components.js"></script>
|
||||
<script>
|
||||
let countdown = 0;
|
||||
let timer = null;
|
||||
|
||||
async function sendCode() {
|
||||
const email = document.getElementById('email').value.trim();
|
||||
if (!email) {
|
||||
UI.notify('请先输入邮箱地址', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
UI.notify('邮箱格式不正确', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('sendCodeBtn');
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
const response = await fetch('/auth/send-code', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, purpose: 'register' })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
UI.notify('验证码已发送,请查收邮件', 'success');
|
||||
startCountdown();
|
||||
} else {
|
||||
UI.notify(result.message || '发送失败', 'error');
|
||||
btn.disabled = false;
|
||||
}
|
||||
} catch (error) {
|
||||
UI.notify('网络错误,请稍后重试', 'error');
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function startCountdown() {
|
||||
countdown = 60;
|
||||
const btn = document.getElementById('sendCodeBtn');
|
||||
|
||||
timer = setInterval(() => {
|
||||
countdown--;
|
||||
btn.textContent = `${countdown}秒后重试`;
|
||||
|
||||
if (countdown <= 0) {
|
||||
clearInterval(timer);
|
||||
btn.textContent = '发送验证码';
|
||||
btn.disabled = false;
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
document.getElementById('registerForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const username = document.getElementById('username').value.trim();
|
||||
const email = document.getElementById('email').value.trim();
|
||||
const code = document.getElementById('code').value.trim();
|
||||
const password = document.getElementById('password').value;
|
||||
const confirmPassword = document.getElementById('confirmPassword').value;
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
UI.notify('两次输入的密码不一致', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
UI.notify('密码长度至少6位', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const submitBtn = e.target.querySelector('.submit-btn');
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = '注册中...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/auth/register', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, email, password, code })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
UI.notify('注册成功!即将跳转到登录页...', 'success');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/auth/login';
|
||||
}, 1500);
|
||||
} else {
|
||||
UI.notify(result.message || '注册失败', 'error');
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = '立即注册';
|
||||
}
|
||||
} catch (error) {
|
||||
UI.notify('网络错误,请稍后重试', 'error');
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = '立即注册';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user