Add project files: Outlook mail manager with Docker support
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
337
static/admin.html
Normal file
337
static/admin.html
Normal file
@@ -0,0 +1,337 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Outlook邮件系统 - 账号管理</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet">
|
||||
<style>
|
||||
/* 背景与容器风格对齐用户页 */
|
||||
body {
|
||||
background: #f5f7fa;
|
||||
}
|
||||
.admin-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
.admin-card {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
max-width: 1100px;
|
||||
animation: slideIn 0.5s ease-out;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@keyframes slideIn {
|
||||
from { opacity: 0; transform: translateY(30px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.admin-header {
|
||||
background: #0078d4;
|
||||
color: white;
|
||||
padding: 16px 18px;
|
||||
text-align: center;
|
||||
}
|
||||
.admin-header h2 { font-size: 18px; font-weight: 600; margin: 0 0 4px 0; }
|
||||
.admin-header p { margin: 0; opacity: .9; }
|
||||
.admin-content { padding: 16px; }
|
||||
.login-section { max-width: 420px; margin: 0 auto; }
|
||||
.management-section { display: none; }
|
||||
|
||||
/* 账号卡片风格 */
|
||||
.account-item {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
padding: 14px;
|
||||
margin-bottom: 12px;
|
||||
background: #ffffff;
|
||||
transition: box-shadow .2s ease, border-color .2s ease;
|
||||
}
|
||||
.account-item:hover {
|
||||
border-color: #cbd5e0;
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* 按钮与输入对齐用户页 */
|
||||
.btn-admin {
|
||||
background: #0078d4;
|
||||
border: none;
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
padding: 6px 12px;
|
||||
transition: all 0.2s ease;
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
}
|
||||
.btn-admin:hover { background: #005a9e; color: #fff; transform: translateY(-1px); }
|
||||
|
||||
.input-group-custom { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
|
||||
.input-group-custom input {
|
||||
flex: 1; min-width: 200px; padding: 6px 10px; border: 1px solid #e2e8f0; border-radius: 6px; font-size: 13px; outline: none; transition: all .3s;
|
||||
}
|
||||
.input-group-custom input:focus { border-color: #0078d4; box-shadow: 0 0 0 3px rgba(0, 120, 212, 0.1); }
|
||||
.btn-custom { padding: 6px 10px; border: none; border-radius: 6px; font-weight: 500; cursor: pointer; transition: all .3s; display: inline-flex; align-items: center; gap: 6px; font-size: 13px; }
|
||||
.btn-primary-custom { background: #0078d4; color: #fff; }
|
||||
.btn-primary-custom:hover { background: #005a9e; }
|
||||
.btn-outline-secondary { border: 1px solid #e2e8f0; color: #718096; padding: 6px 8px; border-radius: 6px; font-size: 12px; }
|
||||
|
||||
/* 选项卡与内容区域 */
|
||||
.nav-tabs .nav-link { border: 1px solid transparent; border-radius: 8px 8px 0 0; color: #0078d4; padding: 6px 10px; font-size: 13px; }
|
||||
.nav-tabs .nav-link.active { background: #0078d4; color: white; border-color: #0078d4; }
|
||||
.tab-content { border: 1px solid #e2e8f0; border-top: none; border-radius: 0 0 8px 8px; padding: 12px; background: white; max-height: 55vh; overflow-y: auto; }
|
||||
.admin-content { padding: 16px; flex: 1; overflow-y: auto; max-height: calc(100vh - 160px); }
|
||||
|
||||
/* 模态风格对齐用户页 */
|
||||
.modal-content { border: none; border-radius: 12px; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2); }
|
||||
.modal-header { background: #0078d4; color: white; border-radius: 12px 12px 0 0; border-bottom: none; padding: 15px 20px; }
|
||||
.modal-title { font-size: 16px; font-weight: 600; }
|
||||
.modal-header .btn-close { filter: brightness(0) invert(1); opacity: 0.8; }
|
||||
.modal-header .btn-close:hover { opacity: 1; }
|
||||
.modal-body .form-label { font-weight: 600; color: #2d3748; margin-bottom: 6px; font-size: 14px; }
|
||||
.modal-body .form-control { border: 1px solid #e2e8f0; border-radius: 6px; padding: 8px 12px; transition: all 0.3s; font-size: 14px; }
|
||||
.modal-body .form-control:focus { border-color: #0078d4; box-shadow: 0 0 0 3px rgba(0, 120, 212, 0.1); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin-container">
|
||||
<div class="admin-card">
|
||||
<div class="admin-header P-3">
|
||||
<h2><i class="bi bi-gear me-2"></i>Outlook邮件系统 - 账号管理</h2>
|
||||
</div>
|
||||
|
||||
<div class="admin-content">
|
||||
<!-- 登录验证区域 -->
|
||||
<div id="loginSection" class="login-section">
|
||||
<div class="text-center mb-4">
|
||||
<i class="bi bi-shield-lock display-4 text-primary"></i>
|
||||
<h4 class="mt-3">身份验证</h4>
|
||||
<p class="text-muted">请输入管理令牌以访问账号管理功能</p>
|
||||
</div>
|
||||
|
||||
<form id="loginForm">
|
||||
<div class="mb-3">
|
||||
<label for="tokenInput" class="form-label">管理令牌</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">
|
||||
<i class="bi bi-key"></i>
|
||||
</span>
|
||||
<input type="password" class="form-control" id="tokenInput"
|
||||
placeholder="请输入管理令牌" required>
|
||||
</div>
|
||||
<div class="form-text">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
默认令牌:admin123(可通过环境变量 ADMIN_TOKEN 修改)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-admin w-100">
|
||||
<i class="bi bi-unlock me-2"></i>验证并登录
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="text-center mt-3">
|
||||
<a href="/" class="text-decoration-none">
|
||||
<i class="bi bi-arrow-left me-1"></i>返回邮件管理
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 管理功能区域(单页紧凑版) -->
|
||||
<div id="managementSection" class="management-section">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div class="d-flex justify-content-end gap-2">
|
||||
<button class="btn btn-outline-secondary btn-sm" id="openImportModalBtn" title="导入">
|
||||
<i class="bi bi-upload"></i> 导入
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" id="exportDataBtn" title="导出">
|
||||
<i class="bi bi-download"></i> 导出
|
||||
</button>
|
||||
</div>
|
||||
<div class="d-flex gap-2 align-items-center">
|
||||
<div class="input-group-custom" style="width: 280px;">
|
||||
<input type="text" id="accountSearchInput" placeholder="搜索邮箱...">
|
||||
<button class="btn btn-outline-secondary btn-sm" id="searchAccountsBtn" title="搜索">
|
||||
<i class="bi bi-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn btn-outline-secondary btn-sm" id="refreshAccountsBtn" title="刷新">
|
||||
<i class="bi bi-arrow-clockwise"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="accountsList" class="accounts-list">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="small text-muted" style="width:50%">邮箱</th>
|
||||
<th class="small text-muted">标签</th>
|
||||
<th class="text-end small text-muted" style="width:120px">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="accountsTbody">
|
||||
<tr>
|
||||
<td colspan="3" class="text-center py-4">
|
||||
<div class="spinner-border text-primary" role="status"></div>
|
||||
<div class="mt-2 small text-muted">正在加载账号列表...</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mt-3">
|
||||
<div class="small text-muted" id="accountsPagerInfo">第 1 / 1 页,共 0 条</div>
|
||||
<div class="btn-group" role="group">
|
||||
<button class="btn btn-outline-secondary btn-sm" id="prevPageBtn" title="上一页"><i class="bi bi-chevron-left"></i></button>
|
||||
<button class="btn btn-outline-secondary btn-sm" id="nextPageBtn" title="下一页"><i class="bi bi-chevron-right"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- 退出登录 -->
|
||||
<div class="text-center mt-4">
|
||||
<button class="btn btn-sm btn-outline-danger" id="logoutBtn">
|
||||
<i class="bi bi-box-arrow-right me-2"></i>退出管理
|
||||
</button>
|
||||
<a href="/" class="btn btn-outline-secondary ms-2">
|
||||
<i class="bi bi-arrow-left me-2"></i>返回邮件管理
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 成功提示模态框 -->
|
||||
<div class="modal fade" id="successModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header border-0">
|
||||
<h5 class="modal-title">
|
||||
<i class="bi bi-check-circle me-2"></i>操作成功
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p id="successMessage" class="mb-0"></p>
|
||||
</div>
|
||||
<div class="modal-footer border-0">
|
||||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">确定</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误提示模态框 -->
|
||||
<div class="modal fade" id="errorModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header border-0">
|
||||
<h5 class="modal-title text-danger">
|
||||
<i class="bi bi-exclamation-triangle me-2"></i>错误提示
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p id="errorMessage" class="mb-0"></p>
|
||||
</div>
|
||||
<div class="modal-footer border-0">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">确定</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 标签管理模态框(保留用于行内编辑) -->
|
||||
<div class="modal fade" id="tagManagementModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="tagManagementModalTitle">
|
||||
<i class="bi bi-tags me-2"></i>管理标签
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="tagManagementInput" class="form-label">标签</label>
|
||||
<input type="text" class="form-control" id="tagManagementInput"
|
||||
placeholder="输入标签,用逗号分隔,如:微软,谷歌,苹果">
|
||||
<div class="form-text">
|
||||
<i class="bi bi-lightbulb me-1"></i>
|
||||
多个标签用逗号分隔,如:微软,谷歌,苹果
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<small class="text-muted">当前标签:</small>
|
||||
<div id="tagManagementCurrentTags" class="mt-1">
|
||||
<span class="text-muted">暂无标签</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" id="tagManagementSaveBtn">
|
||||
<i class="bi bi-save me-1"></i>保存标签
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 导入对话框 -->
|
||||
<div class="modal fade" id="importModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="bi bi-upload me-2"></i>批量导入账号
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-info" role="alert" style="font-size: 12px;">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
支持:完整格式「邮箱----密码----client_id----refresh_token」或简化格式「邮箱----refresh_token」。
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label for="importTextarea" class="form-label small mb-1">账户数据</label>
|
||||
<textarea class="form-control" id="importTextarea" rows="8" placeholder="每行一个账户,使用四个短横线(----)分隔字段"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label for="mergeMode" class="form-label small mb-1">合并模式</label>
|
||||
<select class="form-select form-select-sm" id="mergeMode">
|
||||
<option value="update">更新现有账户</option>
|
||||
<option value="skip">跳过重复账户</option>
|
||||
<option value="replace">替换所有数据</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">取消</button>
|
||||
<button type="button" class="btn btn-admin btn-sm" id="executeImportBtn">
|
||||
<i class="bi bi-upload me-1"></i>导入
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
743
static/admin.js
Normal file
743
static/admin.js
Normal file
@@ -0,0 +1,743 @@
|
||||
// 账号管理页面JavaScript
|
||||
|
||||
class AdminManager {
|
||||
constructor() {
|
||||
this.isAuthenticated = false;
|
||||
this.token = '';
|
||||
// 分页与搜索状态
|
||||
this.page = 1;
|
||||
this.pageSize = 10;
|
||||
this.total = 0;
|
||||
this.query = '';
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.bindEvents();
|
||||
this.checkStoredAuth();
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
// 登录表单
|
||||
document.getElementById('loginForm').addEventListener('submit', this.handleLogin.bind(this));
|
||||
|
||||
// 刷新账号列表
|
||||
document.getElementById('refreshAccountsBtn').addEventListener('click', () => {
|
||||
this.page = 1;
|
||||
this.loadAccounts();
|
||||
});
|
||||
|
||||
// 搜索
|
||||
const searchInput = document.getElementById('accountSearchInput');
|
||||
const searchBtn = document.getElementById('searchAccountsBtn');
|
||||
if (searchBtn) {
|
||||
searchBtn.addEventListener('click', () => {
|
||||
this.query = searchInput.value.trim();
|
||||
this.page = 1;
|
||||
this.loadAccounts();
|
||||
});
|
||||
}
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
this.query = searchInput.value.trim();
|
||||
this.page = 1;
|
||||
this.loadAccounts();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 分页
|
||||
const prevBtn = document.getElementById('prevPageBtn');
|
||||
const nextBtn = document.getElementById('nextPageBtn');
|
||||
if (prevBtn) {
|
||||
prevBtn.addEventListener('click', () => {
|
||||
if (this.page > 1) {
|
||||
this.page -= 1;
|
||||
this.loadAccounts();
|
||||
}
|
||||
});
|
||||
}
|
||||
if (nextBtn) {
|
||||
nextBtn.addEventListener('click', () => {
|
||||
const maxPage = Math.max(1, Math.ceil(this.total / this.pageSize));
|
||||
if (this.page < maxPage) {
|
||||
this.page += 1;
|
||||
this.loadAccounts();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 导入相关(存在才绑定)
|
||||
const executeImportBtn = document.getElementById('executeImportBtn');
|
||||
if (executeImportBtn) {
|
||||
executeImportBtn.addEventListener('click', this.executeImport.bind(this));
|
||||
}
|
||||
const clearImportBtn = document.getElementById('clearImportBtn');
|
||||
if (clearImportBtn) {
|
||||
clearImportBtn.addEventListener('click', this.clearImport.bind(this));
|
||||
}
|
||||
|
||||
// 导出
|
||||
document.getElementById('exportDataBtn').addEventListener('click', this.exportData.bind(this));
|
||||
|
||||
// 退出登录
|
||||
document.getElementById('logoutBtn').addEventListener('click', this.logout.bind(this));
|
||||
|
||||
// 单页:不使用tabs,直接加载列表
|
||||
setTimeout(() => this.loadAccounts(), 100);
|
||||
|
||||
// 打开导入对话框
|
||||
const openImportBtn = document.getElementById('openImportModalBtn');
|
||||
if (openImportBtn) {
|
||||
openImportBtn.addEventListener('click', () => {
|
||||
const modal = new bootstrap.Modal(document.getElementById('importModal'));
|
||||
modal.show();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
checkStoredAuth() {
|
||||
// 检查是否有保存的认证信息(仅在当前会话有效)
|
||||
const storedToken = sessionStorage.getItem('admin_token');
|
||||
if (storedToken) {
|
||||
this.token = storedToken;
|
||||
this.showManagement();
|
||||
}
|
||||
}
|
||||
|
||||
async handleLogin(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const tokenInput = document.getElementById('tokenInput');
|
||||
const enteredToken = tokenInput.value.trim();
|
||||
|
||||
if (!enteredToken) {
|
||||
this.showError('请输入管理令牌');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 验证令牌
|
||||
const isValid = await this.verifyToken(enteredToken);
|
||||
|
||||
if (isValid) {
|
||||
this.token = enteredToken;
|
||||
this.isAuthenticated = true;
|
||||
|
||||
// 保存到会话存储
|
||||
sessionStorage.setItem('admin_token', enteredToken);
|
||||
|
||||
this.showManagement();
|
||||
this.showSuccess('登录成功');
|
||||
} else {
|
||||
this.showError('令牌验证失败,请检查输入的令牌是否正确');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error);
|
||||
this.showError('登录失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async verifyToken(token) {
|
||||
try {
|
||||
// 发送验证请求到后端
|
||||
const response = await fetch('/api/admin/verify', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ token: token })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
return result.success;
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('令牌验证错误:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
showManagement() {
|
||||
document.getElementById('loginSection').style.display = 'none';
|
||||
document.getElementById('managementSection').style.display = 'block';
|
||||
|
||||
// 自动加载账号列表
|
||||
this.loadAccounts();
|
||||
}
|
||||
|
||||
async loadAccounts() {
|
||||
const accountsList = document.getElementById('accountsList');
|
||||
|
||||
// 显示加载状态(表格内)
|
||||
const tbodyLoading = document.getElementById('accountsTbody');
|
||||
if (tbodyLoading) {
|
||||
tbodyLoading.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="3" class="text-center py-4">
|
||||
<div class="spinner-border spinner-border-sm text-primary" role="status"></div>
|
||||
<div class="mt-2 small text-muted">正在加载账号列表...</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
params.set('page', String(this.page));
|
||||
params.set('page_size', String(this.pageSize));
|
||||
if (this.query) params.set('q', this.query);
|
||||
const response = await fetch(`/api/accounts/paged?${params.toString()}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
const items = (result.data && result.data.items) ? result.data.items : [];
|
||||
this.total = (result.data && typeof result.data.total === 'number') ? result.data.total : items.length;
|
||||
this.page = (result.data && typeof result.data.page === 'number') ? result.data.page : this.page;
|
||||
this.pageSize = (result.data && typeof result.data.page_size === 'number') ? result.data.page_size : this.pageSize;
|
||||
this.renderAccounts(items);
|
||||
this.renderPager();
|
||||
} else {
|
||||
throw new Error(result.message);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载账号列表失败:', error);
|
||||
accountsList.innerHTML = `
|
||||
<div class="text-center py-4 text-danger">
|
||||
<i class="bi bi-exclamation-triangle display-4"></i>
|
||||
<div class="mt-2">加载失败: ${error.message}</div>
|
||||
<button class="btn btn-outline-primary btn-sm mt-2" onclick="adminManager.loadAccounts()">
|
||||
<i class="bi bi-arrow-clockwise me-1"></i>重试
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
async renderAccounts(accounts) {
|
||||
const tbody = document.getElementById('accountsTbody');
|
||||
if (!tbody) return;
|
||||
|
||||
if (accounts.length === 0) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="3" class="text-center py-4 text-muted">
|
||||
<i class="bi bi-inbox"></i>
|
||||
<div class="mt-2 small">暂无账号数据</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取所有账户的标签信息
|
||||
const accountsWithTags = await this.loadAccountsWithTags();
|
||||
|
||||
const accountsHtml = accounts.map((account, index) => {
|
||||
const tags = accountsWithTags[account.email] || [];
|
||||
const tagsHtml = tags.length > 0 ?
|
||||
tags.map(tag => `<span class="badge bg-secondary me-1 mb-1">${this.escapeHtml(tag)}</span>`).join('') :
|
||||
'<span class="text-muted small">无标签</span>';
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td class="small"><i class="bi bi-envelope me-1"></i>${account.email}</td>
|
||||
<td>${tagsHtml}</td>
|
||||
<td class="text-end">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button class="btn btn-outline-secondary btn-sm"
|
||||
onclick="adminManager.showTagManagementDialog('${account.email}')"
|
||||
title="管理标签">
|
||||
<i class="bi bi-tags"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
tbody.innerHTML = accountsHtml;
|
||||
}
|
||||
|
||||
// 已移除测试与系统配置相关功能
|
||||
|
||||
// 删除账户功能不在精简范围内,已移除调用
|
||||
|
||||
clearImport() {
|
||||
document.getElementById('importTextarea').value = '';
|
||||
document.getElementById('mergeMode').value = 'update';
|
||||
this.showSuccess('已清空导入内容');
|
||||
}
|
||||
|
||||
async executeImport() {
|
||||
const textarea = document.getElementById('importTextarea');
|
||||
const mergeMode = document.getElementById('mergeMode');
|
||||
|
||||
const importText = textarea.value.trim();
|
||||
if (!importText) {
|
||||
this.showError('请输入要导入的账户数据');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 解析文本
|
||||
const parseResponse = await fetch('/api/parse-import-text', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${this.token}`
|
||||
},
|
||||
body: JSON.stringify({ text: importText })
|
||||
});
|
||||
|
||||
if (!parseResponse.ok) {
|
||||
throw new Error(`解析失败: HTTP ${parseResponse.status}`);
|
||||
}
|
||||
|
||||
const parseResult = await parseResponse.json();
|
||||
if (!parseResult.success) {
|
||||
throw new Error(parseResult.message);
|
||||
}
|
||||
|
||||
// 执行导入
|
||||
const importResponse = await fetch('/api/import', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${this.token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
accounts: parseResult.data.accounts, // 只提取accounts数组
|
||||
merge_mode: mergeMode.value
|
||||
})
|
||||
});
|
||||
|
||||
if (!importResponse.ok) {
|
||||
throw new Error(`导入失败: HTTP ${importResponse.status}`);
|
||||
}
|
||||
|
||||
const importResult = await importResponse.json();
|
||||
|
||||
if (importResult.success) {
|
||||
this.showSuccess(`导入完成! 新增: ${importResult.added_count}, 更新: ${importResult.updated_count}, 跳过: ${importResult.skipped_count}`);
|
||||
// 清空导入内容
|
||||
document.getElementById('importTextarea').value = '';
|
||||
this.loadAccounts(); // 刷新账号列表
|
||||
} else {
|
||||
this.showError(`导入失败: ${importResult.message}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('导入失败:', error);
|
||||
this.showError(`导入失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async exportData() {
|
||||
try {
|
||||
const response = await fetch('/api/export');
|
||||
|
||||
if (response.ok) {
|
||||
// 直接获取文本内容
|
||||
const content = await response.text();
|
||||
|
||||
// 从响应头获取文件名
|
||||
const contentDisposition = response.headers.get('Content-Disposition');
|
||||
let filename = 'outlook_accounts_config.txt';
|
||||
if (contentDisposition) {
|
||||
const match = contentDisposition.match(/filename=(.+)/);
|
||||
if (match) {
|
||||
filename = match[1];
|
||||
}
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
this.downloadTextFile(content, filename);
|
||||
this.showSuccess('数据导出成功');
|
||||
} else {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('导出失败:', error);
|
||||
this.showError(`导出失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
downloadTextFile(content, filename) {
|
||||
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.isAuthenticated = false;
|
||||
this.token = '';
|
||||
sessionStorage.removeItem('admin_token');
|
||||
|
||||
document.getElementById('managementSection').style.display = 'none';
|
||||
document.getElementById('loginSection').style.display = 'block';
|
||||
|
||||
// 清空表单
|
||||
document.getElementById('tokenInput').value = '';
|
||||
|
||||
this.showSuccess('已安全退出管理');
|
||||
}
|
||||
|
||||
showSuccess(message) {
|
||||
document.getElementById('successMessage').textContent = message;
|
||||
const modal = new bootstrap.Modal(document.getElementById('successModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
document.getElementById('errorMessage').textContent = message;
|
||||
const modal = new bootstrap.Modal(document.getElementById('errorModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
// ==================== 标签管理功能 ====================
|
||||
|
||||
async loadAccountsWithTags() {
|
||||
try {
|
||||
const response = await fetch('/api/accounts/tags', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.token}`
|
||||
}
|
||||
});
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
return result.data.accounts || {};
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载账户标签失败:', error);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
async showTagManagementDialog(email) {
|
||||
// 显示标签管理对话框
|
||||
const modal = new bootstrap.Modal(document.getElementById('tagManagementModal'));
|
||||
const modalTitle = document.getElementById('tagManagementModalTitle');
|
||||
const tagInput = document.getElementById('tagManagementInput');
|
||||
const currentTagsDisplay = document.getElementById('tagManagementCurrentTags');
|
||||
|
||||
modalTitle.textContent = `管理标签 - ${email}`;
|
||||
|
||||
// 加载当前标签
|
||||
try {
|
||||
const response = await fetch(`/api/account/${encodeURIComponent(email)}/tags`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
const tags = result.data.tags || [];
|
||||
tagInput.value = tags.join(',');
|
||||
|
||||
if (tags.length > 0) {
|
||||
const tagsHtml = tags.map(tag =>
|
||||
`<span class="badge bg-secondary me-1">${this.escapeHtml(tag)}</span>`
|
||||
).join('');
|
||||
currentTagsDisplay.innerHTML = tagsHtml;
|
||||
} else {
|
||||
currentTagsDisplay.innerHTML = '<span class="text-muted">暂无标签</span>';
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载账户标签失败:', error);
|
||||
}
|
||||
|
||||
// 设置保存按钮事件
|
||||
const saveBtn = document.getElementById('tagManagementSaveBtn');
|
||||
saveBtn.onclick = () => this.saveAccountTagsFromDialog(email);
|
||||
|
||||
modal.show();
|
||||
}
|
||||
|
||||
async saveAccountTagsFromDialog(email) {
|
||||
const tagInput = document.getElementById('tagManagementInput');
|
||||
const tagsText = tagInput.value.trim();
|
||||
const tags = tagsText ? tagsText.split(',').map(tag => tag.trim()).filter(tag => tag) : [];
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/account/${encodeURIComponent(email)}/tags`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${this.token}`
|
||||
},
|
||||
body: JSON.stringify({ email: email, tags: tags })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
this.showSuccess('标签保存成功');
|
||||
// 关闭对话框
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('tagManagementModal'));
|
||||
modal.hide();
|
||||
// 刷新账户列表
|
||||
this.loadAccounts();
|
||||
} else {
|
||||
this.showError('保存失败: ' + result.message);
|
||||
}
|
||||
} else {
|
||||
this.showError(`保存失败: HTTP ${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存标签失败:', error);
|
||||
this.showError('保存标签失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async loadAllTags() {
|
||||
const allTagsList = document.getElementById('allTagsList');
|
||||
|
||||
allTagsList.innerHTML = `
|
||||
<div class="text-center py-3">
|
||||
<div class="spinner-border spinner-border-sm text-primary" role="status"></div>
|
||||
<div class="mt-2 small">加载中...</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/accounts/tags', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.token}`
|
||||
}
|
||||
});
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
this.renderAllTags(result.data.tags || []);
|
||||
} else {
|
||||
throw new Error(result.message);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载标签失败:', error);
|
||||
allTagsList.innerHTML = `
|
||||
<div class="text-center py-3 text-danger">
|
||||
<i class="bi bi-exclamation-triangle"></i>
|
||||
<div class="mt-2 small">加载失败: ${error.message}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
renderAllTags(tags) {
|
||||
const allTagsList = document.getElementById('allTagsList');
|
||||
|
||||
if (tags.length === 0) {
|
||||
allTagsList.innerHTML = `
|
||||
<div class="text-center py-3 text-muted">
|
||||
<i class="bi bi-tags"></i>
|
||||
<div class="mt-2 small">暂无标签</div>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const tagsHtml = tags.map(tag => `
|
||||
<span class="badge bg-primary me-1 mb-1">${this.escapeHtml(tag)}</span>
|
||||
`).join('');
|
||||
|
||||
allTagsList.innerHTML = `
|
||||
<div class="mb-2">
|
||||
<small class="text-muted">共 ${tags.length} 个标签:</small>
|
||||
</div>
|
||||
<div>${tagsHtml}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async loadAccountsForTags() {
|
||||
const tagAccountSelect = document.getElementById('tagAccountSelect');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/accounts');
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
tagAccountSelect.innerHTML = '<option value="">请选择账户...</option>';
|
||||
result.data.forEach(account => {
|
||||
const option = document.createElement('option');
|
||||
option.value = account.email;
|
||||
option.textContent = account.email;
|
||||
tagAccountSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载账户列表失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async loadAccountTags() {
|
||||
const tagAccountSelect = document.getElementById('tagAccountSelect');
|
||||
const accountTagsInput = document.getElementById('accountTagsInput');
|
||||
const currentAccountTags = document.getElementById('currentAccountTags');
|
||||
const currentTagsDisplay = document.getElementById('currentTagsDisplay');
|
||||
|
||||
const selectedEmail = tagAccountSelect.value;
|
||||
if (!selectedEmail) {
|
||||
accountTagsInput.value = '';
|
||||
currentAccountTags.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/account/${encodeURIComponent(selectedEmail)}/tags`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.token}`
|
||||
}
|
||||
});
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
const tags = result.data.tags || [];
|
||||
accountTagsInput.value = tags.join(',');
|
||||
|
||||
if (tags.length > 0) {
|
||||
const tagsHtml = tags.map(tag =>
|
||||
`<span class="badge bg-secondary me-1">${this.escapeHtml(tag)}</span>`
|
||||
).join('');
|
||||
currentTagsDisplay.innerHTML = tagsHtml;
|
||||
currentAccountTags.style.display = 'block';
|
||||
} else {
|
||||
currentAccountTags.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载账户标签失败:', error);
|
||||
this.showError('加载账户标签失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async saveAccountTags() {
|
||||
const tagAccountSelect = document.getElementById('tagAccountSelect');
|
||||
const accountTagsInput = document.getElementById('accountTagsInput');
|
||||
|
||||
const selectedEmail = tagAccountSelect.value;
|
||||
if (!selectedEmail) {
|
||||
this.showError('请先选择账户');
|
||||
return;
|
||||
}
|
||||
|
||||
const tagsText = accountTagsInput.value.trim();
|
||||
const tags = tagsText ? tagsText.split(',').map(tag => tag.trim()).filter(tag => tag) : [];
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/account/${encodeURIComponent(selectedEmail)}/tags`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${this.token}`
|
||||
},
|
||||
body: JSON.stringify({ email: selectedEmail, tags: tags })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
this.showSuccess('标签保存成功');
|
||||
this.loadAccountTags(); // 刷新显示
|
||||
this.loadAllTags(); // 刷新所有标签列表
|
||||
} else {
|
||||
this.showError('保存失败: ' + result.message);
|
||||
}
|
||||
} else {
|
||||
this.showError(`保存失败: HTTP ${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存标签失败:', error);
|
||||
this.showError('保存标签失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
clearAccountTags() {
|
||||
document.getElementById('accountTagsInput').value = '';
|
||||
document.getElementById('currentAccountTags').style.display = 'none';
|
||||
}
|
||||
|
||||
// ==================== 分页信息渲染 ====================
|
||||
renderPager() {
|
||||
const info = document.getElementById('accountsPagerInfo');
|
||||
if (!info) return;
|
||||
const maxPage = Math.max(1, Math.ceil(this.total / this.pageSize));
|
||||
info.textContent = `第 ${this.page} / ${maxPage} 页,共 ${this.total} 条`;
|
||||
const prevBtn = document.getElementById('prevPageBtn');
|
||||
const nextBtn = document.getElementById('nextPageBtn');
|
||||
if (prevBtn) prevBtn.disabled = this.page <= 1;
|
||||
if (nextBtn) nextBtn.disabled = this.page >= maxPage;
|
||||
}
|
||||
|
||||
// ==================== 辅助方法 ====================
|
||||
|
||||
escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '未知时间';
|
||||
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
if (isNaN(date.getTime())) {
|
||||
return dateString;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const messageDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
||||
|
||||
const timeDiff = today.getTime() - messageDate.getTime();
|
||||
const daysDiff = Math.floor(timeDiff / (1000 * 3600 * 24));
|
||||
|
||||
if (daysDiff === 0) {
|
||||
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
|
||||
} else if (daysDiff === 1) {
|
||||
return '昨天';
|
||||
} else if (daysDiff < 7) {
|
||||
return `${daysDiff}天前`;
|
||||
} else {
|
||||
return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Date formatting error:', error);
|
||||
return dateString;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载完成后初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
window.adminManager = new AdminManager();
|
||||
});
|
||||
350
static/index.html
Normal file
350
static/index.html
Normal file
@@ -0,0 +1,350 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Outlook 邮件管理器</title>
|
||||
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.1.3/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.bootcdn.net/ajax/libs/bootstrap-icons/1.13.1/font/bootstrap-icons.css" rel="stylesheet">
|
||||
<link href="style.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ====================================================================
|
||||
视图1:账号管理(默认主页)
|
||||
==================================================================== -->
|
||||
<div id="accountView" class="view-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<nav class="navbar">
|
||||
<div class="navbar-container">
|
||||
<a href="#" class="logo">
|
||||
<div class="logo-icon"><i class="bi bi-envelope-fill"></i></div>
|
||||
<span class="logo-text">邮箱助手</span>
|
||||
</a>
|
||||
<div class="nav-actions">
|
||||
<div class="search-box">
|
||||
<i class="bi bi-search"></i>
|
||||
<input type="text" id="accountSearch" placeholder="搜索邮箱..." autocomplete="off">
|
||||
</div>
|
||||
<button class="btn btn-primary" id="importBtn">
|
||||
<i class="bi bi-clipboard-plus"></i>
|
||||
<span>粘贴导入</span>
|
||||
</button>
|
||||
<button class="btn btn-primary" id="exportBtn">
|
||||
<i class="bi bi-download"></i>
|
||||
<span>导出</span>
|
||||
</button>
|
||||
<button class="btn btn-primary" id="claudePaymentBtn">
|
||||
<i class="bi bi-credit-card"></i>
|
||||
<span>Claude检测</span>
|
||||
</button>
|
||||
<button class="btn btn-icon" id="refreshAccountsBtn" title="刷新">
|
||||
<i class="bi bi-arrow-clockwise"></i>
|
||||
</button>
|
||||
<a href="/admin" class="btn btn-icon" title="管理后台">
|
||||
<i class="bi bi-gear"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<div class="main-container">
|
||||
<!-- 表格容器 -->
|
||||
<div class="table-container">
|
||||
<div class="table-header">
|
||||
<h2 class="table-title">
|
||||
<i class="bi bi-people"></i>
|
||||
邮箱账号列表
|
||||
</h2>
|
||||
</div>
|
||||
<div class="table-scroll">
|
||||
<table class="data-table" id="accountTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-num">#</th>
|
||||
<th>邮箱</th>
|
||||
<th>密码</th>
|
||||
<th class="col-hide-mobile">客户ID</th>
|
||||
<th>令牌</th>
|
||||
<th>支付状态</th>
|
||||
<th>退款状态</th>
|
||||
<th>支付时间</th>
|
||||
<th>退款时间</th>
|
||||
<th>封号时间</th>
|
||||
<th>备注/卡号</th>
|
||||
<th>代理</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="accountTableBody">
|
||||
<tr>
|
||||
<td colspan="13">
|
||||
<div class="no-data">
|
||||
<i class="bi bi-inbox"></i>
|
||||
<div>暂无邮箱数据,点击"粘贴导入"添加</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container" id="paginationContainer">
|
||||
<div class="pagination-info">
|
||||
<span id="pagerInfo">共 0 条</span>
|
||||
<span class="divider">|</span>
|
||||
<span id="pagerCurrent">1/1 页</span>
|
||||
</div>
|
||||
<div class="pagination-controls" id="pagerControls">
|
||||
<button class="pagination-btn" id="prevPageBtn" disabled title="上一页">
|
||||
<i class="bi bi-chevron-left"></i>
|
||||
</button>
|
||||
<button class="pagination-btn active">1</button>
|
||||
<button class="pagination-btn" id="nextPageBtn" disabled title="下一页">
|
||||
<i class="bi bi-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ====================================================================
|
||||
视图2:邮件查看器
|
||||
==================================================================== -->
|
||||
<div id="emailView" class="view-container" style="display:none;">
|
||||
<!-- 顶部导航 -->
|
||||
<nav class="navbar">
|
||||
<div class="navbar-container">
|
||||
<button class="btn btn-primary" id="backToAccounts">
|
||||
<i class="bi bi-arrow-left"></i>
|
||||
<span>返回邮箱管理</span>
|
||||
</button>
|
||||
<div class="current-email-info">
|
||||
<div class="info-icon"><i class="bi bi-envelope"></i></div>
|
||||
<div class="info-text">
|
||||
<div class="info-label">当前查看</div>
|
||||
<div class="info-value">
|
||||
<span id="topbarEmail"></span>
|
||||
<span class="folder-badge" id="topbarFolder"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 移动端邮件列表按钮 -->
|
||||
<button class="btn btn-icon d-md-none" id="mobileMailToggle" title="邮件列表">
|
||||
<i class="bi bi-list-ul"></i>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 双栏主体 -->
|
||||
<div class="email-main">
|
||||
<!-- 左侧邮件列表 -->
|
||||
<div class="email-list-panel" id="emailListPanel">
|
||||
<div class="email-list-header">
|
||||
<span id="emailListTitle">邮件列表</span>
|
||||
<span class="email-count-badge" id="emailCountBadge" style="display:none;">0</span>
|
||||
</div>
|
||||
<div class="email-list-content" id="emailListContent">
|
||||
<div class="empty-state">
|
||||
<i class="bi bi-envelope"></i>
|
||||
<p>加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧邮件详情 -->
|
||||
<div class="email-detail-panel" id="emailDetailPanel">
|
||||
<div class="detail-content" id="detailContent">
|
||||
<div class="empty-detail">
|
||||
<i class="bi bi-envelope-open"></i>
|
||||
<h6>选择一封邮件查看详情</h6>
|
||||
<p>从左侧列表中选择邮件</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 移动端遮罩 -->
|
||||
<div class="mobile-overlay" id="mobileOverlay"></div>
|
||||
</div>
|
||||
|
||||
<!-- ====================================================================
|
||||
导入模态框
|
||||
==================================================================== -->
|
||||
<div class="paste-modal" id="importModal">
|
||||
<div class="paste-modal-content">
|
||||
<div class="paste-modal-header">
|
||||
<div class="paste-modal-icon"><i class="bi bi-clipboard-plus"></i></div>
|
||||
<div>
|
||||
<h3 class="paste-modal-title">粘贴导入邮箱</h3>
|
||||
<p class="paste-modal-subtitle">将邮箱数据粘贴到下方输入框中</p>
|
||||
</div>
|
||||
</div>
|
||||
<textarea class="paste-textarea" id="importText"
|
||||
placeholder="每行一个账户 格式: 邮箱----密码----客户ID----令牌 简化: 邮箱----令牌"></textarea>
|
||||
<div class="paste-modal-hint">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
<span>使用 <code>----</code> 分隔各字段,每行一个邮箱</span>
|
||||
</div>
|
||||
<div class="merge-mode-section">
|
||||
<label class="input-label">合并模式</label>
|
||||
<div class="merge-options">
|
||||
<label class="merge-option">
|
||||
<input type="radio" name="mergeMode" value="update" checked>
|
||||
<span>更新已有</span>
|
||||
</label>
|
||||
<label class="merge-option">
|
||||
<input type="radio" name="mergeMode" value="skip">
|
||||
<span>跳过重复</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="paste-modal-buttons">
|
||||
<button class="btn btn-cancel" id="cancelImportBtn">取消</button>
|
||||
<button class="btn btn-primary" id="doImportBtn">
|
||||
<i class="bi bi-check2"></i>
|
||||
<span>确定导入</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 备注/卡号编辑弹窗 -->
|
||||
<div class="paste-modal" id="noteModal">
|
||||
<div class="paste-modal-content" style="max-width:420px;">
|
||||
<div class="paste-modal-header">
|
||||
<div class="paste-modal-icon"><i class="bi bi-pencil-square"></i></div>
|
||||
<div>
|
||||
<h3 class="paste-modal-title">编辑备注</h3>
|
||||
<p class="paste-modal-subtitle" id="noteModalEmail"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="note-fields">
|
||||
<div class="note-field-group">
|
||||
<label class="input-label">标题</label>
|
||||
<input type="text" class="proxy-input" id="noteTitle" placeholder="如:淘宝、开台" autocomplete="off">
|
||||
</div>
|
||||
<div class="note-field-group">
|
||||
<label class="input-label">卡号</label>
|
||||
<input type="text" class="proxy-input" id="noteCardNumber" placeholder="银行卡号" autocomplete="off">
|
||||
</div>
|
||||
<div class="note-field-group">
|
||||
<label class="input-label">备注</label>
|
||||
<textarea class="proxy-input" id="noteRemark" rows="3" placeholder="其他备注信息" style="resize:vertical;min-height:60px;"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="paste-modal-buttons">
|
||||
<button class="btn btn-cancel" id="cancelNoteBtn">取消</button>
|
||||
<button class="btn btn-primary" id="saveNoteBtn">
|
||||
<i class="bi bi-check2"></i>
|
||||
<span>保存</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 代理编辑弹窗 -->
|
||||
<div class="paste-modal" id="proxyModal">
|
||||
<div class="paste-modal-content" style="max-width:480px;">
|
||||
<div class="paste-modal-header">
|
||||
<div class="paste-modal-icon"><i class="bi bi-globe"></i></div>
|
||||
<div>
|
||||
<h3 class="paste-modal-title">编辑代理</h3>
|
||||
<p class="paste-modal-subtitle" id="proxyModalEmail"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="proxy-format-section">
|
||||
<label class="input-label">协议类型</label>
|
||||
<div class="proxy-format-options">
|
||||
<label class="merge-option"><input type="radio" name="proxyProtocol" value="http" checked><span>HTTP</span></label>
|
||||
<label class="merge-option"><input type="radio" name="proxyProtocol" value="https"><span>HTTPS</span></label>
|
||||
<label class="merge-option"><input type="radio" name="proxyProtocol" value="socks5"><span>SOCKS5</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="proxy-fields">
|
||||
<div class="proxy-row">
|
||||
<div class="proxy-field">
|
||||
<label class="input-label">IP地址</label>
|
||||
<input type="text" class="proxy-input" id="proxyHost" placeholder="127.0.0.1" autocomplete="off">
|
||||
</div>
|
||||
<div class="proxy-field proxy-field-sm">
|
||||
<label class="input-label">端口</label>
|
||||
<input type="text" class="proxy-input" id="proxyPort" placeholder="1080" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
<div class="proxy-row">
|
||||
<div class="proxy-field">
|
||||
<label class="input-label">账号 <span style="color:#94a3b8;font-weight:400">(可选)</span></label>
|
||||
<input type="text" class="proxy-input" id="proxyUser" placeholder="username" autocomplete="off">
|
||||
</div>
|
||||
<div class="proxy-field">
|
||||
<label class="input-label">密码 <span style="color:#94a3b8;font-weight:400">(可选)</span></label>
|
||||
<input type="text" class="proxy-input" id="proxyPass" placeholder="password" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="proxy-extra-section">
|
||||
<div class="proxy-row">
|
||||
<div class="proxy-field">
|
||||
<label class="input-label">IP有效期</label>
|
||||
<div class="proxy-expire-options">
|
||||
<label class="merge-option"><input type="radio" name="proxyExpire" value="10"><span>10天</span></label>
|
||||
<label class="merge-option"><input type="radio" name="proxyExpire" value="30" checked><span>30天</span></label>
|
||||
<label class="merge-option"><input type="radio" name="proxyExpire" value="custom"><span>自定义</span></label>
|
||||
<input type="number" class="proxy-input proxy-input-mini" id="proxyExpireCustom" placeholder="天" min="1" style="display:none;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="proxy-field">
|
||||
<label class="input-label">类型</label>
|
||||
<div class="proxy-share-options">
|
||||
<label class="merge-option"><input type="radio" name="proxyShare" value="exclusive" checked><span>独享</span></label>
|
||||
<label class="merge-option"><input type="radio" name="proxyShare" value="shared"><span>共享</span></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="proxy-row">
|
||||
<div class="proxy-field">
|
||||
<label class="input-label">购买日期</label>
|
||||
<input type="date" class="proxy-input" id="proxyPurchaseDate">
|
||||
</div>
|
||||
<div class="proxy-field">
|
||||
<label class="input-label">到期日期 <span style="color:#94a3b8;font-weight:400">(自动计算)</span></label>
|
||||
<input type="text" class="proxy-input" id="proxyExpireDate" readonly style="background:#f8fafc;color:#64748b;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="paste-modal-hint" style="margin-top:10px;">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
<span>也可直接粘贴:<code>ip:端口:账号:密码</code> 或 <code>socks5://user:pass@host:port</code></span>
|
||||
</div>
|
||||
<div class="proxy-raw-section">
|
||||
<label class="input-label">快速粘贴</label>
|
||||
<input type="text" class="proxy-input" id="proxyRaw" placeholder="粘贴完整代理字符串,自动解析" autocomplete="off">
|
||||
</div>
|
||||
<div class="proxy-preview" id="proxyPreview"></div>
|
||||
<div class="paste-modal-buttons">
|
||||
<button class="btn btn-cancel" id="clearProxyBtn" style="margin-right:auto;">清除代理</button>
|
||||
<button class="btn btn-cancel" id="cancelProxyBtn">取消</button>
|
||||
<button class="btn btn-primary" id="saveProxyBtn">
|
||||
<i class="bi bi-check2"></i>
|
||||
<span>保存</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast 提示 -->
|
||||
<div class="toast-container" id="toastContainer"></div>
|
||||
|
||||
<!-- Loading 遮罩 -->
|
||||
<div class="loading-overlay" id="loadingOverlay">
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
1165
static/script.js
Normal file
1165
static/script.js
Normal file
File diff suppressed because it is too large
Load Diff
1432
static/style.css
Normal file
1432
static/style.css
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user