test-w / web_template.py
letterm's picture
Upload 13 files
47d8ca8 verified
raw
history blame
39.1 kB
"""
Web界面模板模块
包含管理界面的HTML模板
"""
def get_admin_login_template():
"""返回管理员登录页面模板"""
return """
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🔐 管理员登录 - Warp API 管理中心</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 0;
background: linear-gradient(135deg, #0f1419 0%, #1a2332 25%, #2d3748 50%, #1a2332 75%, #0f1419 100%);
background-attachment: fixed;
min-height: 100vh;
color: #e2e8f0;
display: flex;
justify-content: center;
align-items: center;
}
.login-container {
max-width: 400px;
width: 90%;
background: rgba(30, 41, 59, 0.9);
backdrop-filter: blur(20px);
border-radius: 20px;
padding: 40px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.05);
border: 1px solid rgba(148, 163, 184, 0.1);
text-align: center;
}
h1 {
color: #cbd5e1;
margin-bottom: 30px;
font-size: 2rem;
text-shadow: 0 2px 10px rgba(59, 130, 246, 0.3);
}
.form-group {
margin-bottom: 25px;
text-align: left;
}
label {
display: block;
margin-bottom: 8px;
color: #cbd5e1;
font-weight: 600;
}
input {
width: 100%;
padding: 12px 16px;
background: rgba(30, 41, 59, 0.7);
border: 2px solid rgba(71, 85, 105, 0.3);
border-radius: 10px;
box-sizing: border-box;
color: #e2e8f0;
font-size: 14px;
transition: all 0.3s ease;
}
input:focus {
outline: none;
border-color: rgba(59, 130, 246, 0.5);
background: rgba(30, 41, 59, 0.9);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
button {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.8) 0%, rgba(29, 78, 216, 0.8) 100%);
color: white;
padding: 14px 28px;
border: none;
border-radius: 10px;
cursor: pointer;
font-weight: 600;
font-size: 16px;
width: 100%;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
border: 1px solid rgba(59, 130, 246, 0.3);
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px -5px rgba(59, 130, 246, 0.4);
background: linear-gradient(135deg, rgba(59, 130, 246, 1) 0%, rgba(29, 78, 216, 1) 100%);
}
.status {
padding: 12px 16px;
margin: 15px 0;
border-radius: 8px;
font-weight: 500;
display: none;
}
.status.success {
background: rgba(34, 197, 94, 0.15);
color: #86efac;
border: 1px solid rgba(34, 197, 94, 0.3);
}
.status.error {
background: rgba(239, 68, 68, 0.15);
color: #fca5a5;
border: 1px solid rgba(239, 68, 68, 0.3);
}
.note {
margin-top: 20px;
padding: 15px;
background: rgba(79, 70, 229, 0.1);
border: 1px solid rgba(79, 70, 229, 0.2);
border-radius: 8px;
font-size: 14px;
color: #a5b4fc;
}
</style>
</head>
<body>
<div class="login-container">
<h1>🔐 管理员登录</h1>
<form id="loginForm">
<div class="form-group">
<label for="adminKey">管理员密钥</label>
<input type="password" id="adminKey" name="adminKey" placeholder="请输入管理员密钥" required>
</div>
<button type="submit">🚀 登录</button>
<div id="status" class="status"></div>
</form>
<div class="note">
💡 管理员密钥通过环境变量 <code>ADMIN_KEY</code> 设置
</div>
</div>
<script>
document.getElementById('loginForm').addEventListener('submit', async function(e) {
e.preventDefault();
const adminKey = document.getElementById('adminKey').value;
const statusDiv = document.getElementById('status');
if (!adminKey) {
showStatus('请输入管理员密钥', 'error');
return;
}
try {
const response = await fetch('/admin/auth', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ admin_key: adminKey })
});
const data = await response.json();
if (data.success) {
showStatus('登录成功,正在跳转...', 'success');
setTimeout(() => {
window.location.href = data.redirect || '/';
}, 1000);
} else {
showStatus(data.message || '登录失败', 'error');
}
} catch (error) {
showStatus('登录请求失败: ' + error.message, 'error');
}
});
function showStatus(message, type) {
const statusDiv = document.getElementById('status');
statusDiv.textContent = message;
statusDiv.className = `status ${type}`;
statusDiv.style.display = 'block';
}
</script>
</body>
</html>
"""
def get_html_template():
"""返回优化的HTML模板"""
return """
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🚀 Warp API 管理中心</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 20px;
background: linear-gradient(135deg, #0f1419 0%, #1a2332 25%, #2d3748 50%, #1a2332 75%, #0f1419 100%);
background-attachment: fixed;
min-height: 100vh;
color: #e2e8f0;
}
.container {
max-width: 1400px;
margin: 0 auto;
background: rgba(30, 41, 59, 0.85);
backdrop-filter: blur(20px);
border-radius: 20px;
padding: 40px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.05);
border: 1px solid rgba(148, 163, 184, 0.1);
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 40px;
}
h1 {
color: #cbd5e1;
margin: 0;
font-size: 2.5rem;
text-shadow: 0 2px 10px rgba(59, 130, 246, 0.3);
letter-spacing: -0.025em;
}
.logout-btn {
background: linear-gradient(135deg, rgba(239, 68, 68, 0.8) 0%, rgba(185, 28, 28, 0.8) 100%);
color: white;
padding: 10px 20px;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
font-size: 14px;
transition: all 0.3s ease;
border: 1px solid rgba(239, 68, 68, 0.3);
}
.logout-btn:hover {
background: linear-gradient(135deg, rgba(239, 68, 68, 1) 0%, rgba(185, 28, 28, 1) 100%);
transform: translateY(-1px);
}
.section {
margin-bottom: 40px;
padding: 30px;
background: rgba(51, 65, 85, 0.6);
backdrop-filter: blur(10px);
border-radius: 16px;
border: 1px solid rgba(148, 163, 184, 0.1);
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
.section h2 {
color: #f1f5f9;
margin-top: 0;
display: flex;
align-items: center;
gap: 12px;
font-size: 1.5rem;
margin-bottom: 25px;
}
.status-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.status-card {
padding: 20px;
border-radius: 12px;
text-align: center;
font-weight: 600;
background: rgba(59, 130, 246, 0.15);
border: 1px solid rgba(59, 130, 246, 0.2);
backdrop-filter: blur(10px);
color: #e2e8f0;
transition: all 0.3s ease;
}
.status-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px -5px rgba(59, 130, 246, 0.2);
background: rgba(59, 130, 246, 0.2);
}
.status-card.total { background: rgba(59, 130, 246, 0.15); border-color: rgba(59, 130, 246, 0.3); }
.status-card.active { background: rgba(34, 197, 94, 0.15); border-color: rgba(34, 197, 94, 0.3); }
.status-card.with-access { background: rgba(168, 85, 247, 0.15); border-color: rgba(168, 85, 247, 0.3); }
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 25px;
background: rgba(30, 41, 59, 0.5);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
th, td {
padding: 16px;
text-align: left;
border-bottom: 1px solid rgba(71, 85, 105, 0.3);
}
th {
background: rgba(51, 65, 85, 0.8);
color: #f1f5f9;
font-weight: 600;
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
tr:hover {
background-color: rgba(71, 85, 105, 0.3);
transition: background-color 0.2s ease;
}
input, textarea {
width: 100%;
padding: 12px 16px;
background: rgba(30, 41, 59, 0.7);
border: 2px solid rgba(71, 85, 105, 0.3);
border-radius: 10px;
box-sizing: border-box;
color: #e2e8f0;
font-size: 14px;
transition: all 0.3s ease;
}
input:focus, textarea:focus {
outline: none;
border-color: rgba(59, 130, 246, 0.5);
background: rgba(30, 41, 59, 0.9);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
input::placeholder, textarea::placeholder {
color: #94a3b8;
}
button {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.8) 0%, rgba(29, 78, 216, 0.8) 100%);
color: white;
padding: 12px 24px;
border: none;
border-radius: 10px;
cursor: pointer;
margin: 6px;
font-weight: 600;
font-size: 14px;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
border: 1px solid rgba(59, 130, 246, 0.3);
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px -5px rgba(59, 130, 246, 0.4);
background: linear-gradient(135deg, rgba(59, 130, 246, 1) 0%, rgba(29, 78, 216, 1) 100%);
}
button:active {
transform: translateY(0);
}
.btn-success {
background: linear-gradient(135deg, rgba(34, 197, 94, 0.8) 0%, rgba(21, 128, 61, 0.8) 100%);
border-color: rgba(34, 197, 94, 0.3);
}
.btn-success:hover {
background: linear-gradient(135deg, rgba(34, 197, 94, 1) 0%, rgba(21, 128, 61, 1) 100%);
box-shadow: 0 10px 25px -5px rgba(34, 197, 94, 0.4);
}
.btn-danger {
background: linear-gradient(135deg, rgba(239, 68, 68, 0.8) 0%, rgba(185, 28, 28, 0.8) 100%);
border-color: rgba(239, 68, 68, 0.3);
}
.btn-danger:hover {
background: linear-gradient(135deg, rgba(239, 68, 68, 1) 0%, rgba(185, 28, 28, 1) 100%);
box-shadow: 0 10px 25px -5px rgba(239, 68, 68, 0.4);
}
.btn-info {
background: linear-gradient(135deg, rgba(6, 182, 212, 0.8) 0%, rgba(8, 145, 178, 0.8) 100%);
border-color: rgba(6, 182, 212, 0.3);
}
.btn-info:hover {
background: linear-gradient(135deg, rgba(6, 182, 212, 1) 0%, rgba(8, 145, 178, 1) 100%);
box-shadow: 0 10px 25px -5px rgba(6, 182, 212, 0.4);
}
.btn-warning {
background: linear-gradient(135deg, rgba(245, 158, 11, 0.8) 0%, rgba(217, 119, 6, 0.8) 100%);
border-color: rgba(245, 158, 11, 0.3);
}
.btn-warning:hover {
background: linear-gradient(135deg, rgba(245, 158, 11, 1) 0%, rgba(217, 119, 6, 1) 100%);
box-shadow: 0 10px 25px -5px rgba(245, 158, 11, 0.4);
}
.btn-small {
padding: 8px 16px;
font-size: 12px;
margin: 2px;
}
.status {
padding: 16px 20px;
margin: 20px 0;
border-radius: 12px;
font-weight: 600;
backdrop-filter: blur(10px);
}
.status.success {
background: rgba(34, 197, 94, 0.15);
color: #86efac;
border: 1px solid rgba(34, 197, 94, 0.3);
}
.status.error {
background: rgba(239, 68, 68, 0.15);
color: #fca5a5;
border: 1px solid rgba(239, 68, 68, 0.3);
}
.api-info {
background: linear-gradient(135deg, rgba(79, 70, 229, 0.15) 0%, rgba(59, 130, 246, 0.15) 100%);
border: 1px solid rgba(79, 70, 229, 0.2);
color: #e2e8f0;
padding: 25px;
border-radius: 16px;
margin-bottom: 30px;
backdrop-filter: blur(10px);
}
.api-info code {
background: rgba(30, 41, 59, 0.6);
padding: 6px 12px;
border-radius: 8px;
color: #a78bfa;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
border: 1px solid rgba(71, 85, 105, 0.3);
}
.tabs {
display: flex;
margin-bottom: 30px;
background: rgba(30, 41, 59, 0.5);
border-radius: 12px;
padding: 4px;
backdrop-filter: blur(10px);
}
.tab {
padding: 14px 28px;
cursor: pointer;
border: none;
background: none;
color: #94a3b8;
font-weight: 600;
border-radius: 8px;
transition: all 0.3s ease;
flex: 1;
text-align: center;
}
.tab.active {
color: #f1f5f9;
background: rgba(59, 130, 246, 0.3);
backdrop-filter: blur(10px);
}
.tab:hover:not(.active) {
color: #cbd5e1;
background: rgba(71, 85, 105, 0.3);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.loading {
display: none;
text-align: center;
padding: 30px;
color: #94a3b8;
}
.spinner {
border: 4px solid rgba(71, 85, 105, 0.3);
border-top: 4px solid #3b82f6;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 15px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.action-buttons {
display: flex;
gap: 8px;
justify-content: center;
}
.token-id {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
background: rgba(30, 41, 59, 0.6);
padding: 4px 8px;
border-radius: 6px;
font-size: 0.85rem;
color: #a78bfa;
}
.env-info {
background: rgba(34, 197, 94, 0.1);
border: 1px solid rgba(34, 197, 94, 0.2);
color: #86efac;
padding: 15px;
border-radius: 12px;
margin-bottom: 20px;
font-size: 14px;
}
.export-section {
background: rgba(245, 158, 11, 0.1);
border: 1px solid rgba(245, 158, 11, 0.2);
padding: 20px;
border-radius: 12px;
margin-bottom: 20px;
}
.export-form {
display: flex;
gap: 15px;
align-items: end;
}
.export-form input {
flex: 1;
}
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(10px);
}
.modal-content {
background: rgba(30, 41, 59, 0.95);
margin: 15% auto;
padding: 30px;
border-radius: 16px;
width: 80%;
max-width: 500px;
border: 1px solid rgba(148, 163, 184, 0.2);
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.8);
}
.close {
color: #94a3b8;
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
line-height: 1;
}
.close:hover {
color: #e2e8f0;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🚀 Warp API 管理中心</h1>
<button class="logout-btn" onclick="logout()">🚪 退出登录</button>
</div>
<div class="api-info">
<h3>📡 API 端点信息</h3>
<p><strong>Base URL:</strong> <code>http://localhost:7860</code></p>
<p><strong>Chat:</strong> <code>/v1/chat/completions</code> | <strong>Models:</strong> <code>/v1/models</code></p>
<div class="env-info">
🔐 <strong>安全提示:</strong> API密钥和敏感信息已隐藏,仅在服务器端可见
</div>
</div>
<div class="tabs">
<button class="tab active" onclick="showTab('token-status')">🔑 Token状态</button>
<button class="tab" onclick="showTab('add-tokens')">➕ 添加Token</button>
<button class="tab" onclick="showTab('get-tokens')">📧 获取Token</button>
</div>
<div id="token-status" class="tab-content active">
<div class="section">
<h2>🔑 Token 状态管理</h2>
<div class="env-info" id="tokenEnvInfo" style="display: none;">
🎯 <strong>环境变量Token:</strong> 已通过 <code>WARP_REFRESH_TOKEN</code> 设置<br>
💡 <strong>多Token支持:</strong> 可使用分号(;)分割多个token:<code>token1;token2;token3</code>
</div>
<div class="export-section">
<h3>🔒 导出 Refresh Token (超级管理员功能)</h3>
<p>导出所有refresh token为分号分割的文本文件,需要超级管理员密钥验证</p>
<div class="export-form">
<div style="flex: 1;">
<label>超级管理员密钥</label>
<input type="password" id="superAdminKey" placeholder="请输入超级管理员密钥">
</div>
<button onclick="exportTokens()" class="btn-warning">📥 导出Token</button>
</div>
<div class="env-info" style="margin-top: 15px;">
💡 <strong>提示:</strong> 点击导出后,浏览器会提示您选择保存位置<br>
📁 <strong>文件格式:</strong> 文本文件,token间用分号(;)分割,文件名包含时间戳
</div>
<div id="exportStatus"></div>
</div>
<div class="status-grid">
<div class="status-card total"><div>总Token数</div><div id="totalTokens">-</div></div>
<div class="status-card active"><div>活跃Token</div><div id="activeTokens">-</div></div>
<div class="status-card with-access"><div>可用Token</div><div id="tokensWithAccess">-</div></div>
</div>
<button onclick="refreshTokenStatus()" class="btn-info">🔄 刷新状态</button>
<button onclick="refreshAllTokens()" class="btn-success">⚡ 刷新所有Token</button>
<div style="max-height: 500px; overflow-y: auto; margin-top: 25px;">
<table>
<thead><tr><th>Token ID</th><th>状态</th><th>访问Token</th><th>刷新次数</th><th>使用次数</th><th>操作</th></tr></thead>
<tbody id="tokenTableBody"></tbody>
</table>
</div>
</div>
</div>
<div id="add-tokens" class="tab-content">
<div class="section">
<h2>➕ 添加 Refresh Token</h2>
<div class="env-info">
💡 <strong>提示:</strong> 也可通过环境变量 <code>WARP_REFRESH_TOKEN</code> 设置refresh token<br>
🔀 <strong>多Token支持:</strong> 环境变量中可使用分号(;)分割多个token:<code>token1;token2;token3</code>
</div>
<textarea id="tokensInput" rows="5" placeholder="请输入refresh token,每行一个&#10;或者在环境变量中使用分号分割:token1;token2;token3"></textarea>
<div style="margin-top: 20px;">
<button onclick="addTokens()" class="btn-success">➕ 添加Token</button>
<button onclick="clearTokensInput()" class="btn-danger">🗑️ 清空</button>
</div>
<div id="addTokensStatus"></div>
</div>
</div>
<div id="get-tokens" class="tab-content">
<div class="section">
<h2>📧 批量获取 Refresh Token</h2>
<div style="margin-bottom: 25px;">
<label style="color: #cbd5e1; margin-right: 10px;">并发线程数:</label>
<input type="number" id="maxWorkers" min="1" max="20" value="5" style="width: 100px; display: inline-block;">
<button onclick="addEmailRow()">➕ 添加邮箱</button>
<button onclick="clearEmails()" class="btn-danger">🗑️ 清空</button>
</div>
<table id="emailTable">
<thead><tr><th>邮箱地址</th><th>登录URL</th><th>操作</th></tr></thead>
<tbody id="emailTableBody">
<tr>
<td><input type="email" placeholder="example@gmail.com"></td>
<td><input type="url" placeholder="https://..."></td>
<td><button onclick="removeEmailRow(this)" class="btn-danger btn-small">❌</button></td>
</tr>
</tbody>
</table>
<button onclick="processEmails()" class="btn-success">🚀 开始处理</button>
<div class="loading" id="emailLoading"><div class="spinner"></div><p id="loadingText">正在处理邮箱,获取Token并创建用户...</p></div>
<div id="emailResults"></div>
</div>
</div>
</div>
<script>
function showTab(tabName) {
document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
event.target.classList.add('active');
document.getElementById(tabName).classList.add('active');
if (tabName === 'token-status') refreshTokenStatus();
}
async function refreshTokenStatus() {
try {
const response = await fetch('/token/status');
const data = await response.json();
if (data.success !== false) {
document.getElementById('totalTokens').textContent = data.total_tokens;
document.getElementById('activeTokens').textContent = data.active_tokens;
document.getElementById('tokensWithAccess').textContent = data.tokens_with_access;
// 如果有token,检查是否是环境变量设置的
if (data.total_tokens > 0) {
document.getElementById('tokenEnvInfo').style.display = 'block';
}
const tbody = document.getElementById('tokenTableBody');
tbody.innerHTML = '';
data.tokens.forEach(token => {
const row = tbody.insertRow();
row.innerHTML = `
<td><span class="token-id">${token.refresh_token}</span></td>
<td>${token.is_active ? '✅ 活跃' : '❌ 失效'}</td>
<td>${token.has_access_token ? '✅ 有效' : '❌ 无效'}</td>
<td>${token.refresh_count}</td>
<td>${token.usage_count}</td>
<td>
<div class="action-buttons">
<button onclick="deleteToken('${token.refresh_token}')" class="btn-danger btn-small">🗑️ 删除</button>
</div>
</td>
`;
});
}
} catch (error) {
console.error('刷新状态失败:', error);
showStatus('刷新状态失败: ' + error.message, 'error');
}
}
async function deleteToken(tokenId) {
if (!confirm('确定要删除这个Token吗?')) return;
try {
const response = await fetch('/token/remove', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: tokenId })
});
const data = await response.json();
if (data.success) {
showStatus('Token删除成功', 'success');
refreshTokenStatus();
} else {
showStatus(data.message || 'Token删除失败', 'error');
}
} catch (error) {
showStatus('删除请求失败: ' + error.message, 'error');
}
}
async function exportTokens() {
const superAdminKey = document.getElementById('superAdminKey').value;
if (!superAdminKey) {
showStatus('请输入超级管理员密钥', 'error', 'exportStatus');
return;
}
try {
showStatus('正在准备导出...', 'success', 'exportStatus');
const response = await fetch('/token/export', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ super_admin_key: superAdminKey })
});
const data = await response.json();
if (data.success) {
// 使用浏览器下载API
downloadTextFile(data.content, data.suggested_filename);
showStatus(`导出成功!包含 ${data.token_count} 个token`, 'success', 'exportStatus');
document.getElementById('superAdminKey').value = '';
} else {
showStatus(data.message || '导出失败', 'error', 'exportStatus');
}
} catch (error) {
showStatus('导出请求失败: ' + error.message, 'error', 'exportStatus');
}
}
function downloadTextFile(content, filename) {
try {
// 创建Blob对象
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
// 创建下载链接
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
// 触发下载
document.body.appendChild(link);
link.click();
// 清理
document.body.removeChild(link);
URL.revokeObjectURL(url);
console.log(`文件 ${filename} 下载完成`);
} catch (error) {
console.error('下载文件时出错:', error);
showStatus('下载文件时出错: ' + error.message, 'error', 'exportStatus');
}
}
async function refreshAllTokens() {
try {
const response = await fetch('/token/refresh', { method: 'POST' });
const data = await response.json();
if (data.success) {
showStatus('所有Token刷新已触发', 'success');
setTimeout(refreshTokenStatus, 2000);
} else {
showStatus(data.message, 'error');
}
} catch (error) { showStatus('刷新失败: ' + error.message, 'error'); }
}
async function addTokens() {
const tokens = document.getElementById('tokensInput').value.split('\\n').map(t => t.trim()).filter(t => t);
if (tokens.length === 0) { showStatus('请输入至少一个token', 'error', 'addTokensStatus'); return; }
try {
const response = await fetch('/token/add', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tokens })
});
const data = await response.json();
if (data.success) {
showStatus(`成功添加 ${data.added_tokens} 个token`, 'success', 'addTokensStatus');
document.getElementById('tokensInput').value = '';
} else {
showStatus(data.message, 'error', 'addTokensStatus');
}
} catch (error) { showStatus('添加失败: ' + error.message, 'error', 'addTokensStatus'); }
}
function clearTokensInput() { document.getElementById('tokensInput').value = ''; }
function addEmailRow() {
const tbody = document.getElementById('emailTableBody');
const row = tbody.insertRow();
row.innerHTML = `<td><input type="email" placeholder="example@gmail.com"></td><td><input type="url" placeholder="https://..."></td><td><button onclick="removeEmailRow(this)" class="btn-danger btn-small">❌</button></td>`;
}
function removeEmailRow(button) { button.closest('tr').remove(); }
function clearEmails() {
document.getElementById('emailTableBody').innerHTML = `<tr><td><input type="email" placeholder="example@gmail.com"></td><td><input type="url" placeholder="https://..."></td><td><button onclick="removeEmailRow(this)" class="btn-danger btn-small">❌</button></td></tr>`;
}
async function processEmails() {
const rows = document.querySelectorAll('#emailTableBody tr');
const emailUrlPairs = [];
rows.forEach(row => {
const inputs = row.querySelectorAll('input');
const email = inputs[0].value.trim();
const url = inputs[1].value.trim();
if (email && url) emailUrlPairs.push({email, url});
});
if (emailUrlPairs.length === 0) { showStatus('请至少添加一个邮箱和URL', 'error', 'emailResults'); return; }
document.getElementById('emailLoading').style.display = 'block';
document.getElementById('emailResults').innerHTML = '';
try {
const response = await fetch('/login/batch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email_url_pairs: emailUrlPairs,
max_workers: parseInt(document.getElementById('maxWorkers').value)
})
});
const data = await response.json();
document.getElementById('emailLoading').style.display = 'none';
if (data.success) {
let resultsHtml = `<div class="status success">批量处理完成!成功: ${data.success_count}/${data.total_count}</div>`;
resultsHtml += '<table><tr><th>邮箱</th><th>状态</th><th>Token</th><th>用户创建</th></tr>';
Object.entries(data.results).forEach(([email, result]) => {
let userCreationStatus = '';
if (result.status.includes('成功并已创建用户')) {
userCreationStatus = '✅ 已创建';
} else if (result.status.includes('创建用户失败')) {
userCreationStatus = '❌ 创建失败';
} else if (result.status.includes('获取access_token失败')) {
userCreationStatus = '⚠️ Token失败';
} else if (result.refresh_token) {
userCreationStatus = '🔄 未尝试';
} else {
userCreationStatus = '-';
}
resultsHtml += `<tr><td>${email}</td><td>${result.status}</td><td>${result.refresh_token ? '✅ 已获取' : '❌ 失败'}</td><td>${userCreationStatus}</td></tr>`;
});
resultsHtml += '</table>';
document.getElementById('emailResults').innerHTML = resultsHtml;
} else {
showStatus(data.message, 'error', 'emailResults');
}
} catch (error) {
document.getElementById('emailLoading').style.display = 'none';
showStatus('处理失败: ' + error.message, 'error', 'emailResults');
}
}
async function logout() {
if (!confirm('确定要退出登录吗?')) return;
try {
const response = await fetch('/admin/logout', { method: 'POST' });
const data = await response.json();
if (data.success) {
window.location.href = '/admin/login';
}
} catch (error) {
console.error('登出失败:', error);
}
}
function showStatus(message, type, targetId = null) {
const status = `<div class="status ${type}">${message}</div>`;
if (targetId) {
document.getElementById(targetId).innerHTML = status;
} else {
const container = document.querySelector('.container');
const statusDiv = document.createElement('div');
statusDiv.innerHTML = status;
container.insertBefore(statusDiv, container.firstChild);
setTimeout(() => statusDiv.remove(), 5000);
}
}
// 页面加载时刷新状态
document.addEventListener('DOMContentLoaded', function() {
refreshTokenStatus();
});
</script>
</body>
</html>
"""