This commit is contained in:
2025-11-28 21:20:40 +08:00
commit f940b95b67
73 changed files with 15721 additions and 0 deletions

282
templates/admin_apis.html Normal file
View 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
View 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>

View 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;">查看全部 &rarr;</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>

View 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
View 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
View 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>

View 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
View 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
View 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
View 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
View 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
View 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
View 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>