test / templates /index.html
22333Misaka's picture
Update templates/index.html
0a9d754 verified
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>系统</title>
<style>
:root {
--primary-color: #5B9BD5;
--primary-light: rgba(91, 155, 213, 0.1);
--primary-dark: #4A8BC4;
--bg-color: #F0F4F8;
--card-bg: rgba(255, 255, 255, 0.85);
--accent-color: #E0E0E0;
--text-color: #333;
--text-muted: #666;
--success-color: #4CAF50;
--warning-color: #FF9800;
--error-color: #F44336;
--border-radius: 12px;
--shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
--shadow-hover: 0 8px 30px rgba(0, 0, 0, 0.15);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
background: linear-gradient(135deg, var(--bg-color) 0%, #E8EEF5 100%);
min-height: 100vh;
color: var(--text-color);
}
/* 登录页面 */
.login-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 20px;
}
.login-card {
background: var(--card-bg);
backdrop-filter: blur(10px);
border-radius: var(--border-radius);
padding: 40px;
width: 100%;
max-width: 400px;
box-shadow: var(--shadow);
}
.login-card h1 {
text-align: center;
color: var(--primary-color);
margin-bottom: 30px;
font-size: 24px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: var(--text-muted);
font-size: 14px;
}
.form-group input {
width: 100%;
padding: 12px 16px;
border: 2px solid #E0E0E0;
border-radius: 8px;
font-size: 16px;
transition: border-color 0.3s;
}
.form-group input:focus {
outline: none;
border-color: var(--primary-color);
}
/* 密码输入框容器 */
.password-wrapper {
position: relative;
display: flex;
align-items: center;
}
.password-wrapper input {
padding-right: 45px;
}
.password-toggle {
position: absolute;
right: 12px;
background: none;
border: none;
cursor: pointer;
padding: 5px;
font-size: 18px;
color: var(--text-muted);
transition: color 0.3s;
}
.password-toggle:hover {
color: var(--primary-color);
}
.password-toggle .eye-open {
display: none;
}
.password-toggle .eye-closed {
display: inline;
}
.password-toggle.visible .eye-open {
display: inline;
}
.password-toggle.visible .eye-closed {
display: none;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
gap: 8px;
}
.btn-primary {
background: var(--primary-color);
color: white;
width: 100%;
}
.btn-primary:hover {
background: var(--primary-dark);
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(91, 155, 213, 0.4);
}
.btn-secondary {
background: var(--accent-color);
color: var(--text-color);
}
.btn-secondary:hover {
background: #D0D0D0;
}
.btn-success {
background: var(--success-color);
color: white;
}
.btn-success:hover {
background: #43A047;
}
.btn-warning {
background: var(--warning-color);
color: white;
}
.btn-warning:hover {
background: #F57C00;
}
.btn-danger {
background: var(--error-color);
color: white;
}
.btn-danger:hover {
background: #D32F2F;
}
.btn-small {
padding: 6px 12px;
font-size: 13px;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* 主布局 */
.app-container {
display: none;
min-height: 100vh;
}
.header {
background: var(--card-bg);
backdrop-filter: blur(10px);
padding: 16px 24px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
position: sticky;
top: 0;
z-index: 100;
}
.header h1 {
color: var(--primary-color);
font-size: 20px;
}
.header-actions {
display: flex;
align-items: center;
gap: 16px;
}
.color-picker-wrapper {
position: relative;
}
.color-picker-btn {
width: 36px;
height: 36px;
border-radius: 50%;
border: 2px solid #E0E0E0;
cursor: pointer;
transition: transform 0.3s;
}
.color-picker-btn:hover {
transform: scale(1.1);
}
.color-picker-input {
position: absolute;
opacity: 0;
width: 36px;
height: 36px;
cursor: pointer;
}
.main-content {
padding: 24px;
max-width: 1400px;
margin: 0 auto;
}
/* 统计卡片 */
.stats-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 24px;
}
.stat-card {
background: var(--card-bg);
backdrop-filter: blur(10px);
border-radius: var(--border-radius);
padding: 24px;
box-shadow: var(--shadow);
transition: transform 0.3s, box-shadow 0.3s;
}
.stat-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-hover);
}
.stat-card .stat-value {
font-size: 36px;
font-weight: bold;
margin-bottom: 8px;
}
.stat-card .stat-label {
color: var(--text-muted);
font-size: 14px;
}
.stat-card.total .stat-value {
color: var(--primary-color);
}
.stat-card.success .stat-value {
color: var(--success-color);
}
.stat-card.creating .stat-value {
color: var(--warning-color);
}
.stat-card.failed .stat-value {
color: var(--error-color);
}
/* 标签页 */
.tabs-container {
background: var(--card-bg);
backdrop-filter: blur(10px);
border-radius: var(--border-radius);
box-shadow: var(--shadow);
overflow: hidden;
}
.tabs-header {
display: flex;
border-bottom: 1px solid #E0E0E0;
}
.tab-btn {
flex: 1;
padding: 16px 24px;
background: none;
border: none;
font-size: 15px;
font-weight: 500;
color: var(--text-muted);
cursor: pointer;
transition: all 0.3s;
position: relative;
}
.tab-btn:hover {
color: var(--primary-color);
background: var(--primary-light);
}
.tab-btn.active {
color: var(--primary-color);
}
.tab-btn.active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 3px;
background: var(--primary-color);
}
.tabs-content {
padding: 24px;
}
.tab-panel {
display: none;
animation: fadeIn 0.3s ease;
}
.tab-panel.active {
display: block;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 工具栏 */
.toolbar {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 20px;
align-items: center;
}
.search-box {
flex: 1;
min-width: 200px;
position: relative;
}
.search-box input {
width: 100%;
padding: 10px 16px 10px 40px;
border: 2px solid #E0E0E0;
border-radius: 8px;
font-size: 14px;
transition: border-color 0.3s;
}
.search-box input:focus {
outline: none;
border-color: var(--primary-color);
}
.search-box::before {
content: '🔍';
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
}
.filter-select {
padding: 10px 16px;
border: 2px solid #E0E0E0;
border-radius: 8px;
font-size: 14px;
background: white;
cursor: pointer;
}
/* 表格 */
.table-container {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
padding: 14px 16px;
text-align: left;
border-bottom: 1px solid #E0E0E0;
}
th {
background: #F5F7FA;
font-weight: 600;
color: var(--text-muted);
font-size: 13px;
text-transform: uppercase;
}
tr:hover {
background: var(--primary-light);
}
.status-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
}
.status-success {
background: rgba(76, 175, 80, 0.1);
color: var(--success-color);
}
.status-creating {
background: rgba(255, 152, 0, 0.1);
color: var(--warning-color);
}
.status-failed {
background: rgba(244, 67, 54, 0.1);
color: var(--error-color);
}
.status-updating {
background: rgba(91, 155, 213, 0.1);
color: var(--primary-color);
}
.actions-cell {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
/* 分页 */
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
margin-top: 20px;
}
.pagination button {
padding: 8px 16px;
border: 1px solid #E0E0E0;
background: white;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s;
}
.pagination button:hover:not(:disabled) {
background: var(--primary-light);
border-color: var(--primary-color);
}
.pagination button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.pagination .page-info {
color: var(--text-muted);
font-size: 14px;
}
/* 设置面板 */
.settings-section {
margin-bottom: 32px;
}
.settings-section h3 {
margin-bottom: 16px;
color: var(--primary-color);
font-size: 16px;
display: flex;
align-items: center;
gap: 8px;
}
.settings-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 16px;
}
.setting-item {
display: flex;
flex-direction: column;
gap: 8px;
}
.setting-item label {
font-size: 14px;
color: var(--text-muted);
}
.setting-item input,
.setting-item select {
padding: 10px 14px;
border: 2px solid #E0E0E0;
border-radius: 8px;
font-size: 14px;
}
.setting-item input:focus,
.setting-item select:focus {
outline: none;
border-color: var(--primary-color);
}
/* 邮箱配置列表 */
.email-config-list {
background: #F5F7FA;
border-radius: 8px;
padding: 16px;
}
.email-config-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: white;
border-radius: 8px;
margin-bottom: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.email-config-item:last-child {
margin-bottom: 0;
}
.email-config-item .domain {
flex: 1;
font-weight: 500;
}
/* 模态框 */
.modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-overlay.active {
display: flex;
}
.modal {
background: white;
border-radius: var(--border-radius);
padding: 24px;
width: 90%;
max-width: 600px;
max-height: 90vh;
overflow-y: auto;
box-shadow: var(--shadow-hover);
animation: modalIn 0.3s ease;
}
@keyframes modalIn {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.modal-header h2 {
font-size: 18px;
color: var(--primary-color);
}
.modal-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: var(--text-muted);
}
.modal-body {
margin-bottom: 20px;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
/* 详情视图 */
.detail-section {
margin-bottom: 20px;
}
.detail-section h4 {
color: var(--primary-color);
margin-bottom: 12px;
font-size: 14px;
border-bottom: 1px solid #E0E0E0;
padding-bottom: 8px;
}
.detail-item {
display: flex;
margin-bottom: 12px;
}
.detail-label {
width: 140px;
color: var(--text-muted);
font-size: 14px;
flex-shrink: 0;
}
.detail-value {
flex: 1;
word-break: break-all;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 13px;
background: #F5F7FA;
padding: 8px 12px;
border-radius: 6px;
max-height: 100px;
overflow-y: auto;
}
.detail-value.empty {
color: var(--text-muted);
font-style: italic;
}
.detail-value.copyable {
cursor: pointer;
position: relative;
}
.detail-value.copyable:hover {
background: #E8EEF5;
}
.detail-value.copyable::after {
content: '📋';
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
opacity: 0;
transition: opacity 0.2s;
}
.detail-value.copyable:hover::after {
opacity: 1;
}
/* Toast 通知 */
.toast-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 2000;
}
.toast {
background: white;
border-radius: 8px;
padding: 16px 20px;
margin-bottom: 10px;
box-shadow: var(--shadow);
display: flex;
align-items: center;
gap: 12px;
animation: toastIn 0.3s ease;
max-width: 350px;
}
.toast.success {
border-left: 4px solid var(--success-color);
}
.toast.error {
border-left: 4px solid var(--error-color);
}
.toast.warning {
border-left: 4px solid var(--warning-color);
}
@keyframes toastIn {
from {
opacity: 0;
transform: translateX(100%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
/* 加载动画 */
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid #E0E0E0;
border-top-color: var(--primary-color);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 40px 20px;
color: var(--text-muted);
}
.empty-state .icon {
font-size: 48px;
margin-bottom: 16px;
}
/* 响应式 */
@media (max-width: 768px) {
.stats-container {
grid-template-columns: repeat(2, 1fr);
}
.toolbar {
flex-direction: column;
}
.search-box {
width: 100%;
}
.header h1 {
font-size: 16px;
}
.tabs-header {
flex-direction: column;
}
.tab-btn {
text-align: center;
}
.actions-cell {
flex-direction: column;
}
.detail-item {
flex-direction: column;
}
.detail-label {
width: 100%;
margin-bottom: 4px;
}
}
</style>
</head>
<body>
<!-- 登录页面 -->
<div class="login-container" id="loginPage">
<div class="login-card">
<h1>🔐 管理员登录</h1>
<form id="loginForm">
<div class="form-group">
<label>用户名</label>
<input type="text" id="username" required placeholder="请输入用户名">
</div>
<div class="form-group">
<label>密码</label>
<div class="password-wrapper">
<input type="password" id="password" required placeholder="请输入密码">
<button type="button" class="password-toggle" onclick="togglePassword('password', this)">
<span class="eye-open">👁️</span>
<span class="eye-closed">👁️‍🗨️</span>
</button>
</div>
</div>
<button type="submit" class="btn btn-primary">登录</button>
</form>
</div>
</div>
<!-- 主应用 -->
<div class="app-container" id="appContainer">
<!-- 头部 -->
<header class="header">
<h1>📊 Gemini Business 账号管理系统</h1>
<div class="header-actions">
<div class="color-pickers-group" style="display: flex; gap: 8px;">
<div class="color-picker-wrapper" title="主色调">
<input type="color" class="color-picker-input" id="primaryColorPicker" value="#5B9BD5">
<div class="color-picker-btn" id="primaryColorBtn" style="background: var(--primary-color)">
</div>
</div>
<div class="color-picker-wrapper" title="背景色调">
<input type="color" class="color-picker-input" id="bgColorPicker" value="#F0F4F8">
<div class="color-picker-btn" id="bgColorBtn" style="background: var(--bg-color)"></div>
</div>
<div class="color-picker-wrapper" title="其他色调">
<input type="color" class="color-picker-input" id="accentColorPicker" value="#E0E0E0">
<div class="color-picker-btn" id="accentColorBtn" style="background: var(--accent-color)"></div>
</div>
</div>
<button class="btn btn-secondary btn-small" onclick="logout()">退出</button>
</div>
</header>
<!-- 主内容 -->
<main class="main-content">
<!-- 统计卡片 -->
<div class="stats-container">
<div class="stat-card total">
<div class="stat-value" id="statTotal">0</div>
<div class="stat-label">总账号数</div>
</div>
<div class="stat-card success">
<div class="stat-value" id="statSuccess">0</div>
<div class="stat-label">注册成功</div>
</div>
<div class="stat-card creating">
<div class="stat-value" id="statCreating">0</div>
<div class="stat-label">正在创建</div>
</div>
<div class="stat-card failed">
<div class="stat-value" id="statFailed">0</div>
<div class="stat-label">创建失败</div>
</div>
</div>
<!-- 标签页 -->
<div class="tabs-container">
<div class="tabs-header">
<button class="tab-btn active" data-tab="accounts">📋 账号管理</button>
<button class="tab-btn" data-tab="settings">⚙️ 系统设置</button>
</div>
<div class="tabs-content">
<!-- 账号管理 -->
<div class="tab-panel active" id="accountsPanel">
<div class="toolbar">
<div class="search-box">
<input type="text" id="searchInput" placeholder="搜索邮箱..." autocomplete="chrome-off"
autocorrect="off" autocapitalize="off" spellcheck="false" name="search_email_filter"
readonly onfocus="this.removeAttribute('readonly');">
</div>
<select class="filter-select" id="statusFilter">
<option value="">全部状态</option>
<option value="success">成功</option>
<option value="creating">创建中</option>
<option value="updating">更新中</option>
<option value="failed">失败</option>
</select>
<button class="btn btn-primary btn-small" onclick="createAccount()">
➕ 创建账号
</button>
<button class="btn btn-warning btn-small" onclick="refreshAllAccounts()">
🔄 更新全部
</button>
<button class="btn btn-danger btn-small" onclick="stopAll()">
⏹ 停止全部
</button>
<button class="btn btn-success btn-small" onclick="exportAccounts()">
📥 导出数据
</button>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th>邮箱</th>
<th>状态</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="accountsTable">
<tr>
<td colspan="4">
<div class="empty-state">
<div class="icon">📭</div>
<p>暂无账号数据</p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="pagination" id="pagination">
<button id="prevPage" disabled>上一页</button>
<span class="page-info" id="pageInfo">第 1 页 / 共 1 页</span>
<button id="nextPage" disabled>下一页</button>
</div>
</div>
<!-- 系统设置 -->
<div class="tab-panel" id="settingsPanel">
<!-- 浏览器设置 -->
<div class="settings-section">
<h3>🌐 浏览器设置</h3>
<div class="settings-grid">
<div class="setting-item">
<label>User-Agent</label>
<input type="text" id="settingUA">
</div>
<div class="setting-item">
<label>最大并发数</label>
<input type="number" id="settingMaxWorkers" min="1" max="10">
</div>
<div class="setting-item">
<label>无头模式</label>
<select id="settingHeadless">
<option value="true">开启</option>
<option value="false">关闭</option>
</select>
</div>
</div>
</div>
<!-- 指纹设置 -->
<div class="settings-section">
<h3>🎭 浏览器指纹</h3>
<div class="settings-grid">
<div class="setting-item">
<label>窗口大小</label>
<input type="text" id="settingWindowSize" placeholder="1920x1080">
</div>
<div class="setting-item">
<label>时区</label>
<input type="text" id="settingTimezone" placeholder="Asia/Shanghai">
</div>
<div class="setting-item">
<label>语言</label>
<input type="text" id="settingLocale" placeholder="zh-CN">
</div>
</div>
</div>
<!-- 邮箱配置 -->
<div class="settings-section">
<h3>📧 邮箱域配置</h3>
<div class="email-config-list" id="emailConfigList">
<div class="empty-state">
<p>暂无配置</p>
</div>
</div>
<button class="btn btn-primary btn-small" style="margin-top: 12px;"
onclick="showAddEmailConfig()">
➕ 添加配置
</button>
</div>
<button class="btn btn-primary" onclick="saveSettings()">
💾 保存设置
</button>
</div>
</div>
</div>
</main>
</div>
<!-- 模态框:账号详情 -->
<div class="modal-overlay" id="detailModal">
<div class="modal">
<div class="modal-header">
<h2>📄 账号详情</h2>
<button class="modal-close" onclick="closeModal('detailModal')">&times;</button>
</div>
<div class="modal-body" id="detailContent">
<!-- 动态填充 -->
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeModal('detailModal')">关闭</button>
</div>
</div>
</div>
<!-- 模态框:添加邮箱配置 -->
<div class="modal-overlay" id="emailConfigModal">
<div class="modal">
<div class="modal-header">
<h2>📧 添加邮箱配置</h2>
<button class="modal-close" onclick="closeModal('emailConfigModal')">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>Worker Domain</label>
<input type="text" id="newWorkerDomain" placeholder="apimail.example.com">
</div>
<div class="form-group">
<label>Email Domain</label>
<input type="text" id="newEmailDomain" placeholder="example.com">
</div>
<div class="form-group">
<label>Admin Password</label>
<div class="password-wrapper">
<input type="password" id="newAdminPassword" placeholder="管理员密码">
<button type="button" class="password-toggle"
onclick="togglePassword('newAdminPassword', this)">
<span class="eye-open">👁️</span>
<span class="eye-closed">👁️‍🗨️</span>
</button>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeModal('emailConfigModal')">取消</button>
<button class="btn btn-primary" onclick="addEmailConfig()">添加</button>
</div>
</div>
</div>
<!-- 模态框:创建账号 -->
<div class="modal-overlay" id="createModal">
<div class="modal">
<div class="modal-header">
<h2>➕ 创建新账号</h2>
<button class="modal-close" onclick="closeModal('createModal')">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>邮箱用户名 (可选,留空自动生成)</label>
<input type="text" id="newUsername" placeholder="留空自动生成随机用户��">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeModal('createModal')">取消</button>
<button class="btn btn-primary" onclick="submitCreateAccount()">创建</button>
</div>
</div>
</div>
<!-- Toast 容器 -->
<div class="toast-container" id="toastContainer"></div>
<script>
// 全局状态
let authToken = localStorage.getItem('authToken') || '';
let currentPage = 1;
let totalPages = 1;
let refreshInterval = null;
// 初始化
document.addEventListener('DOMContentLoaded', () => {
if (authToken) {
showApp();
}
initEventListeners();
});
// 切换密码显示
function togglePassword(inputId, button) {
const input = document.getElementById(inputId);
const isPassword = input.type === 'password';
input.type = isPassword ? 'text' : 'password';
button.classList.toggle('visible', isPassword);
}
// 事件监听
function initEventListeners() {
// 登录表单
document.getElementById('loginForm').addEventListener('submit', handleLogin);
// 标签页切换
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', () => switchTab(btn.dataset.tab));
});
// 搜索和筛选
document.getElementById('searchInput').addEventListener('input', debounce(loadAccounts, 300));
document.getElementById('statusFilter').addEventListener('change', loadAccounts);
// 分页
document.getElementById('prevPage').addEventListener('click', () => changePage(-1));
document.getElementById('nextPage').addEventListener('click', () => changePage(1));
// 颜色选择器
setupColorPicker('primaryColorPicker', 'primaryColorBtn', 'primary');
setupColorPicker('bgColorPicker', 'bgColorBtn', 'bg');
setupColorPicker('accentColorPicker', 'accentColorBtn', 'accent');
}
// 登录处理
async function handleLogin(e) {
e.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (data.success) {
authToken = data.token;
localStorage.setItem('authToken', authToken);
localStorage.setItem('auth-password', password);
showApp();
showToast('登录成功', 'success');
} else {
showToast(data.message || '登录失败', 'error');
}
} catch (error) {
showToast('网络错误', 'error');
}
}
// 显示应用
function showApp() {
document.getElementById('loginPage').style.display = 'none';
document.getElementById('appContainer').style.display = 'block';
loadAccounts();
loadSettings();
startAutoRefresh();
}
// 退出登录
function logout() {
authToken = '';
localStorage.removeItem('authToken');
document.getElementById('loginPage').style.display = 'flex';
document.getElementById('appContainer').style.display = 'none';
stopAutoRefresh();
}
// API 请求封装
async function apiRequest(url, options = {}) {
const headers = {
'Content-Type': 'application/json',
...options.headers
};
// Auth
const token = localStorage.getItem('auth-password');
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const response = await fetch(url, { ...options, headers });
return response;
}
// 加载账号列表
async function loadAccounts() {
try {
const search = document.getElementById('searchInput').value;
const status = document.getElementById('statusFilter').value;
const params = new URLSearchParams({
page: currentPage,
per_page: 20,
search,
status
});
const response = await apiRequest(`/api/accounts?${params}`);
const data = await response.json();
if (data.success) {
renderAccounts(data.accounts);
updateStats(data.stats);
updatePagination(data.page, data.total_pages);
}
} catch (error) {
console.error('加载账号失败:', error);
}
}
// 渲染账号表格
function renderAccounts(accounts) {
const tbody = document.getElementById('accountsTable');
if (!accounts || accounts.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="4">
<div class="empty-state">
<div class="icon">📭</div>
<p>暂无账号数据</p>
</div>
</td>
</tr>
`;
return;
}
tbody.innerHTML = accounts.map(acc => `
<tr>
<td>${acc.email || '-'}</td>
<td>
<span class="status-badge status-${getStatusClass(acc.status)}">
${getStatusText(acc.status)}
</span>
</td>
<td>${formatDate(acc.created_at)}</td>
<td class="actions-cell">
<button class="btn btn-secondary btn-small" onclick="viewDetail('${acc.email}')">
👁️ 查看
</button>
${acc.status === 'success' ? `
<button class="btn btn-primary btn-small" onclick="refreshAccount('${acc.email}')">
🔄 刷新
</button>
` : ''}
${acc.status === 'failed' ? `
<button class="btn btn-warning btn-small" onclick="retryAccount('${acc.email}')">
🔁 重试
</button>
` : ''}
${isProcessing(acc.status) ? `
<button class="btn btn-danger btn-small" onclick="stopAccount('${acc.email}')">
⏹ 停止
</button>
` : ''}
<button class="btn btn-danger btn-small" onclick="deleteAccount('${acc.email}')">
🗑️ 删除
</button>
</td>
</tr>
`).join('');
}
// 检查是否正在处理中
function isProcessing(status) {
return ['pending', 'creating_email', 'opening_page', 'entering_email',
'waiting_code', 'entering_code', 'verifying', 'entering_name',
'agreeing', 'waiting_redirect', 'completing', 'extracting_data',
'updating'].includes(status);
}
// 获取状态样式类
function getStatusClass(status) {
if (status === 'success') return 'success';
if (status === 'failed') return 'failed';
if (status === 'updating') return 'updating';
return 'creating';
}
// 获取状态文本
function getStatusText(status) {
const statusMap = {
'pending': '待处理',
'creating_email': '创建邮箱',
'opening_page': '打开页面',
'entering_email': '输入邮箱',
'waiting_code': '等待验证码',
'entering_code': '输入验证码',
'verifying': '验证中',
'entering_name': '输入姓名',
'agreeing': '同意条款',
'waiting_redirect': '等待跳转',
'completing': '完成设置',
'extracting_data': '提取数据',
'success': '成功',
'failed': '失败',
'updating': '更新中'
};
return statusMap[status] || status;
}
// 更新统计
function updateStats(stats) {
if (!stats) return;
document.getElementById('statTotal').textContent = stats.total || 0;
document.getElementById('statSuccess').textContent = stats.success || 0;
document.getElementById('statCreating').textContent = (stats.creating || 0) + (stats.updating || 0);
document.getElementById('statFailed').textContent = stats.failed || 0;
}
// 更新分页
function updatePagination(page, total) {
currentPage = page || 1;
totalPages = total || 1;
document.getElementById('pageInfo').textContent = `第 ${currentPage} 页 / 共 ${totalPages} 页`;
document.getElementById('prevPage').disabled = currentPage <= 1;
document.getElementById('nextPage').disabled = currentPage >= totalPages;
}
// 切换页面
function changePage(delta) {
const newPage = currentPage + delta;
if (newPage >= 1 && newPage <= totalPages) {
currentPage = newPage;
loadAccounts();
}
}
// 切换标签页
function switchTab(tabId) {
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.tab === tabId);
});
document.querySelectorAll('.tab-panel').forEach(panel => {
panel.classList.toggle('active', panel.id === tabId + 'Panel');
});
}
// 创建账号
function createAccount() {
openModal('createModal');
}
async function submitCreateAccount() {
const username = document.getElementById('newUsername').value.trim();
try {
const response = await apiRequest('/api/accounts', {
method: 'POST',
body: JSON.stringify({ username })
});
const data = await response.json();
if (data.success) {
showToast(`账号创建已开始: ${data.email}`, 'success');
closeModal('createModal');
document.getElementById('newUsername').value = '';
loadAccounts();
} else {
showToast(data.error || '创建失败', 'error');
}
} catch (error) {
showToast('网络错误', 'error');
}
}
// 查看详情
async function viewDetail(email) {
try {
const response = await apiRequest(`/api/accounts?email=${encodeURIComponent(email)}`);
const data = await response.json();
if (data.success && data.account) {
const acc = data.account;
document.getElementById('detailContent').innerHTML = `
<div class="detail-section">
<h4>📧 基本信息</h4>
<div class="detail-item">
<span class="detail-label">邮箱</span>
<span class="detail-value copyable" onclick="copyToClipboard('${acc.email}')">${acc.email || '-'}</span>
</div>
<div class="detail-item">
<span class="detail-label">状态</span>
<span class="detail-value">
<span class="status-badge status-${getStatusClass(acc.status)}">${getStatusText(acc.status)}</span>
</span>
</div>
${acc.error_message ? `
<div class="detail-item">
<span class="detail-label">错误信息</span>
<span class="detail-value" style="color: var(--error-color);">${acc.error_message}</span>
</div>
` : ''}
<div class="detail-item">
<span class="detail-label">创建时间</span>
<span class="detail-value">${formatDate(acc.created_at)}</span>
</div>
${acc.updated_at ? `
<div class="detail-item">
<span class="detail-label">更新时间</span>
<span class="detail-value">${formatDate(acc.updated_at)}</span>
</div>
` : ''}
</div>
<div class="detail-section">
<h4>🔐 Cookie & 组织信息</h4>
<div class="detail-item">
<span class="detail-label">Team ID</span>
<span class="detail-value ${acc.config_id ? 'copyable' : 'empty'}"
${acc.config_id ? `onclick="copyToClipboard('${acc.config_id}')"` : ''}>
${acc.config_id || '(未获取)'}
</span>
</div>
<div class="detail-item">
<span class="detail-label">csesidx</span>
<span class="detail-value ${acc.csesidx ? 'copyable' : 'empty'}"
${acc.csesidx ? `onclick="copyToClipboard('${acc.csesidx}')"` : ''}>
${acc.csesidx || '(未获取)'}
</span>
</div>
<div class="detail-item">
<span class="detail-label">__Host-C_OSES</span>
<span class="detail-value ${acc.c_oses ? 'copyable' : 'empty'}"
${acc.c_oses ? `onclick="copyToClipboard('${acc.c_oses}')"` : ''}>
${acc.c_oses || '(未获取)'}
</span>
</div>
<div class="detail-item">
<span class="detail-label">__Secure-C_SES</span>
<span class="detail-value ${acc.c_ses ? 'copyable' : 'empty'}"
${acc.c_ses ? `onclick="copyToClipboard('${acc.c_ses}')"` : ''}>
${acc.c_ses || '(未获取)'}
</span>
</div>
</div>
`;
openModal('detailModal');
}
} catch (error) {
showToast('获取详情失败', 'error');
}
}
// 复制到剪贴板
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => {
showToast('已复制到剪贴板', 'success');
}).catch(() => {
showToast('复制失败', 'error');
});
}
// 刷新账号
async function refreshAccount(email) {
try {
const response = await apiRequest(`/api/accounts/${encodeURIComponent(email)}/refresh`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
showToast('刷新已开始', 'success');
loadAccounts();
} else {
showToast(data.error || '刷新失败', 'error');
}
} catch (error) {
showToast('网络错误', 'error');
}
}
// 重试失败的账号
async function retryAccount(email) {
try {
const response = await apiRequest(`/api/accounts/${encodeURIComponent(email)}/retry`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
showToast('重试已开始', 'success');
loadAccounts();
} else {
showToast(data.error || '重试失败', 'error');
}
} catch (error) {
showToast('网络错误', 'error');
}
}
// 刷新所有账号
async function refreshAllAccounts() {
if (!confirm('确定要刷新所有成功的账号吗?')) return;
try {
const response = await apiRequest('/api/accounts/refresh-all', {
method: 'POST'
});
const data = await response.json();
if (data.success) {
showToast(data.message, 'success');
loadAccounts();
} else {
showToast(data.error || '操作失败', 'error');
}
} catch (error) {
showToast('网络错误', 'error');
}
}
// 停止账号
async function stopAccount(email) {
try {
const response = await apiRequest(`/api/accounts/${encodeURIComponent(email)}/stop`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
showToast('已停止', 'success');
loadAccounts();
} else {
showToast(data.error || '停止失败', 'error');
}
} catch (error) {
showToast('网络错误', 'error');
}
}
// 停止所有
async function stopAll() {
if (!confirm('确定要停止所有正在进行的任务吗?')) return;
try {
const response = await apiRequest('/api/accounts/stop-all', {
method: 'POST'
});
const data = await response.json();
if (data.success) {
showToast(data.message, 'success');
loadAccounts();
} else {
showToast(data.error || '操作失败', 'error');
}
} catch (error) {
showToast('网络错误', 'error');
}
}
// 删除账号
async function deleteAccount(email) {
if (!confirm(`确定要删除账号 ${email} 吗?`)) return;
try {
const response = await apiRequest(`/api/accounts/${encodeURIComponent(email)}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
showToast('删除成功', 'success');
loadAccounts();
} else {
showToast(data.error || '删除失败', 'error');
}
} catch (error) {
showToast('网络错误', 'error');
}
}
// 导出账号
async function exportAccounts() {
try {
const response = await apiRequest('/api/accounts/export');
const data = await response.json();
if (!data.accounts || data.accounts.length === 0) {
showToast('没有可导出的账号', 'warning');
return;
}
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `gemini_accounts_${new Date().toISOString().slice(0, 10)}.json`;
a.click();
URL.revokeObjectURL(url);
showToast(`成功导出 ${data.accounts.length} 个账号`, 'success');
} catch (error) {
showToast('导出失败', 'error');
}
}
// 加载设置
async function loadSettings() {
try {
const response = await apiRequest('/api/settings');
const data = await response.json();
if (data.success) {
const settings = data.settings;
document.getElementById('settingUA').value = settings.user_agent || '';
document.getElementById('settingMaxWorkers').value = settings.max_workers || 1;
document.getElementById('settingHeadless').value = (settings.headless !== false).toString();
if (settings.browser_fingerprint) {
document.getElementById('settingWindowSize').value = settings.browser_fingerprint.window_size || '';
document.getElementById('settingTimezone').value = settings.browser_fingerprint.timezone || '';
document.getElementById('settingLocale').value = settings.browser_fingerprint.locale || '';
}
renderEmailConfigs(settings.email_configs);
}
} catch (error) {
console.error('加载设置失败:', error);
}
}
// 渲染邮箱配置
function renderEmailConfigs(configs) {
const container = document.getElementById('emailConfigList');
if (!configs || configs.length === 0) {
container.innerHTML = '<div class="empty-state"><p>暂无配置</p></div>';
return;
}
container.innerHTML = configs.map((config, index) => `
<div class="email-config-item">
<span class="domain">${config.worker_domain} / ${config.email_domain}</span>
<button class="btn btn-danger btn-small" onclick="deleteEmailConfig(${index})">删除</button>
</div>
`).join('');
}
// 保存设置
async function saveSettings() {
try {
const settings = {
user_agent: document.getElementById('settingUA').value,
max_workers: parseInt(document.getElementById('settingMaxWorkers').value) || 1,
headless: document.getElementById('settingHeadless').value === 'true',
browser_fingerprint: {
window_size: document.getElementById('settingWindowSize').value,
timezone: document.getElementById('settingTimezone').value,
locale: document.getElementById('settingLocale').value
}
};
const response = await apiRequest('/api/settings', {
method: 'POST',
body: JSON.stringify(settings)
});
const data = await response.json();
if (data.success) {
showToast('设置已保存', 'success');
} else {
showToast(data.error || '保存失败', 'error');
}
} catch (error) {
showToast('网络错误', 'error');
}
}
// 添加邮箱配置
function showAddEmailConfig() {
openModal('emailConfigModal');
}
async function addEmailConfig() {
const workerDomain = document.getElementById('newWorkerDomain').value.trim();
const emailDomain = document.getElementById('newEmailDomain').value.trim();
const adminPassword = document.getElementById('newAdminPassword').value;
if (!workerDomain || !emailDomain || !adminPassword) {
showToast('请填写所有字段', 'error');
return;
}
try {
const response = await apiRequest('/api/email-configs', {
method: 'POST',
body: JSON.stringify({
worker_domain: workerDomain,
email_domain: emailDomain,
admin_password: adminPassword
})
});
const data = await response.json();
if (data.success) {
showToast('配置已添加', 'success');
closeModal('emailConfigModal');
document.getElementById('newWorkerDomain').value = '';
document.getElementById('newEmailDomain').value = '';
document.getElementById('newAdminPassword').value = '';
loadSettings();
} else {
showToast(data.error || '添加失败', 'error');
}
} catch (error) {
showToast('网络错误', 'error');
}
}
// 删除邮箱配置
async function deleteEmailConfig(index) {
if (!confirm('确定要删除此配置吗?')) return;
try {
const response = await apiRequest(`/api/email-configs/${index}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
showToast('配置已删除', 'success');
loadSettings();
} else {
showToast(data.error || '删除失败', 'error');
}
} catch (error) {
showToast('网络错误', 'error');
}
}
// 自动刷新
function startAutoRefresh() {
refreshInterval = setInterval(loadAccounts, 5000);
}
function stopAutoRefresh() {
if (refreshInterval) {
clearInterval(refreshInterval);
refreshInterval = null;
}
}
// 设置颜色选择器
function setupColorPicker(inputId, btnId, type) {
const input = document.getElementById(inputId);
const btn = document.getElementById(btnId);
input.addEventListener('input', (e) => {
updateThemeColor(type, e.target.value);
});
btn.addEventListener('click', () => {
input.click();
});
}
// 更新主题色
function updateThemeColor(type, color) {
if (type === 'primary') {
document.documentElement.style.setProperty('--primary-color', color);
// 计算浅色和深色变体
const r = parseInt(color.slice(1, 3), 16);
const g = parseInt(color.slice(3, 5), 16);
const b = parseInt(color.slice(5, 7), 16);
document.documentElement.style.setProperty('--primary-light', `rgba(${r}, ${g}, ${b}, 0.1)`);
document.documentElement.style.setProperty('--primary-dark', `rgb(${Math.max(0, r - 20)}, ${Math.max(0, g - 20)}, ${Math.max(0, b - 20)})`);
document.getElementById('primaryColorBtn').style.background = color;
} else if (type === 'bg') {
document.documentElement.style.setProperty('--bg-color', color);
document.getElementById('bgColorBtn').style.background = color;
} else if (type === 'accent') {
document.documentElement.style.setProperty('--accent-color', color);
document.getElementById('accentColorBtn').style.background = color;
}
// 保存到本地存储
saveThemeColors();
}
// 保存颜色配置
function saveThemeColors() {
const colors = {
primary: document.documentElement.style.getPropertyValue('--primary-color').trim(),
bg: document.documentElement.style.getPropertyValue('--bg-color').trim(),
accent: document.documentElement.style.getPropertyValue('--accent-color').trim()
};
localStorage.setItem('themeColors', JSON.stringify(colors));
}
// 加载保存的主题色
const savedColors = localStorage.getItem('themeColors');
if (savedColors) {
try {
const colors = JSON.parse(savedColors);
if (colors.primary) {
updateThemeColor('primary', colors.primary);
document.getElementById('primaryColorPicker').value = colors.primary;
}
if (colors.bg) {
updateThemeColor('bg', colors.bg);
document.getElementById('bgColorPicker').value = colors.bg;
}
if (colors.accent) {
updateThemeColor('accent', colors.accent);
document.getElementById('accentColorPicker').value = colors.accent;
}
} catch (e) {
console.error('Failed to load theme colors', e);
}
} else {
// 兼容旧版
const oldColor = localStorage.getItem('themeColor');
if (oldColor) {
updateThemeColor('primary', oldColor);
document.getElementById('primaryColorPicker').value = oldColor;
localStorage.removeItem('themeColor'); // 迁移后删除
}
}
// 模态框操作
function openModal(modalId) {
document.getElementById(modalId).classList.add('active');
}
function closeModal(modalId) {
document.getElementById(modalId).classList.remove('active');
}
// Toast 通知
function showToast(message, type = 'success') {
const container = document.getElementById('toastContainer');
const toast = document.createElement('div');
toast.className = `toast ${type}`;
const icons = {
success: '✅',
error: '❌',
warning: '⚠️'
};
toast.innerHTML = `
<span>${icons[type] || '📢'}</span>
<span>${message}</span>
`;
container.appendChild(toast);
setTimeout(() => {
toast.style.animation = 'toastIn 0.3s ease reverse';
setTimeout(() => toast.remove(), 300);
}, 3000);
}
// 工具函数
function formatDate(dateStr) {
if (!dateStr) return '-';
try {
const date = new Date(dateStr);
if (isNaN(date.getTime())) return '-';
return date.toLocaleString('zh-CN');
} catch {
return '-';
}
}
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
</script>
</body>
</html>