Spaces:
Sleeping
Sleeping
| <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')">×</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')">×</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')">×</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> |