This commit is contained in:
cnlimiter
2026-03-14 17:36:55 +08:00
parent 9d3099fcd8
commit 0688f4ca7e
8 changed files with 2066 additions and 882 deletions

View File

@@ -35,6 +35,7 @@ class RegistrationTaskCreate(BaseModel):
email_service_type: str = "tempmail"
proxy: Optional[str] = None
email_service_config: Optional[dict] = None
email_service_id: Optional[int] = None # 使用数据库中已配置的邮箱服务 ID
class BatchRegistrationRequest(BaseModel):
@@ -43,6 +44,7 @@ class BatchRegistrationRequest(BaseModel):
email_service_type: str = "tempmail"
proxy: Optional[str] = None
email_service_config: Optional[dict] = None
email_service_id: Optional[int] = None # 使用数据库中已配置的邮箱服务 ID
interval_min: int = 5 # 最小间隔秒数
interval_max: int = 30 # 最大间隔秒数
@@ -97,7 +99,7 @@ def task_to_response(task: RegistrationTask) -> RegistrationTaskResponse:
)
async def run_registration_task(task_uuid: str, email_service_type: str, proxy: Optional[str], email_service_config: Optional[dict]):
async def run_registration_task(task_uuid: str, email_service_type: str, proxy: Optional[str], email_service_config: Optional[dict], email_service_id: Optional[int] = None):
"""异步执行注册任务"""
with get_db() as db:
try:
@@ -116,21 +118,66 @@ async def run_registration_task(task_uuid: str, email_service_type: str, proxy:
service_type = EmailServiceType(email_service_type)
settings = get_settings()
if service_type == EmailServiceType.TEMPMAIL:
config = {
"base_url": settings.tempmail_base_url,
"timeout": settings.tempmail_timeout,
"max_retries": settings.tempmail_max_retries,
"proxy_url": proxy,
}
elif service_type == EmailServiceType.CUSTOM_DOMAIN:
config = {
"base_url": settings.custom_domain_base_url,
"api_key": settings.custom_domain_api_key.get_secret_value() if settings.custom_domain_api_key else "",
"proxy_url": proxy,
}
# 优先使用数据库中配置的邮箱服务
if email_service_id:
from ...database.models import EmailService as EmailServiceModel
db_service = db.query(EmailServiceModel).filter(
EmailServiceModel.id == email_service_id,
EmailServiceModel.enabled == True
).first()
if db_service:
config = db_service.config.copy() if db_service.config else {}
# 更新任务关联的邮箱服务
crud.update_registration_task(db, task_uuid, email_service_id=db_service.id)
logger.info(f"使用数据库邮箱服务: {db_service.name} (ID: {db_service.id})")
else:
raise ValueError(f"邮箱服务不存在或已禁用: {email_service_id}")
else:
config = email_service_config or {}
# 使用默认配置或传入的配置
if service_type == EmailServiceType.TEMPMAIL:
config = {
"base_url": settings.tempmail_base_url,
"timeout": settings.tempmail_timeout,
"max_retries": settings.tempmail_max_retries,
"proxy_url": proxy,
}
elif service_type == EmailServiceType.CUSTOM_DOMAIN:
# 检查数据库中是否有可用的自定义域名服务
from ...database.models import EmailService as EmailServiceModel
db_service = db.query(EmailServiceModel).filter(
EmailServiceModel.service_type == "custom_domain",
EmailServiceModel.enabled == True
).order_by(EmailServiceModel.priority.asc()).first()
if db_service and db_service.config:
config = db_service.config.copy()
crud.update_registration_task(db, task_uuid, email_service_id=db_service.id)
logger.info(f"使用数据库自定义域名服务: {db_service.name}")
elif settings.custom_domain_base_url and settings.custom_domain_api_key:
config = {
"base_url": settings.custom_domain_base_url,
"api_key": settings.custom_domain_api_key.get_secret_value() if settings.custom_domain_api_key else "",
"proxy_url": proxy,
}
else:
raise ValueError("没有可用的自定义域名邮箱服务,请先在设置中配置")
elif service_type == EmailServiceType.OUTLOOK:
# 检查数据库中是否有可用的 Outlook 账户
from ...database.models import EmailService as EmailServiceModel
db_service = db.query(EmailServiceModel).filter(
EmailServiceModel.service_type == "outlook",
EmailServiceModel.enabled == True
).order_by(EmailServiceModel.priority.asc()).first()
if db_service and db_service.config:
config = db_service.config.copy()
crud.update_registration_task(db, task_uuid, email_service_id=db_service.id)
logger.info(f"使用数据库 Outlook 账户: {db_service.name}")
else:
raise ValueError("没有可用的 Outlook 账户,请先在设置中导入账户")
else:
config = email_service_config or {}
email_service = EmailServiceFactory.create(service_type, config)
@@ -194,6 +241,7 @@ async def run_batch_registration(
email_service_type: str,
proxy: Optional[str],
email_service_config: Optional[dict],
email_service_id: Optional[int],
interval_min: int,
interval_max: int
):
@@ -223,7 +271,7 @@ async def run_batch_registration(
# 运行单个注册任务
await run_registration_task(
task_uuid, email_service_type, proxy, email_service_config
task_uuid, email_service_type, proxy, email_service_config, email_service_id
)
# 更新统计
@@ -289,7 +337,8 @@ async def start_registration(
task_uuid,
request.email_service_type,
request.proxy,
request.email_service_config
request.email_service_config,
request.email_service_id
)
return task_to_response(task)
@@ -350,6 +399,7 @@ async def start_batch_registration(
request.email_service_type,
request.proxy,
request.email_service_config,
request.email_service_id,
request.interval_min,
request.interval_max
)
@@ -498,3 +548,94 @@ async def get_registration_stats():
"by_status": {status: count for status, count in status_stats},
"today_count": today_count
}
@router.get("/available-services")
async def get_available_email_services():
"""
获取可用于注册的邮箱服务列表
返回所有已启用的邮箱服务,包括:
- tempmail: 临时邮箱(无需配置)
- outlook: 已导入的 Outlook 账户
- custom_domain: 已配置的自定义域名服务
"""
from ...database.models import EmailService as EmailServiceModel
from ...config.settings import get_settings
settings = get_settings()
result = {
"tempmail": {
"available": True,
"count": 1,
"services": [{
"id": None,
"name": "Tempmail.lol",
"type": "tempmail",
"description": "临时邮箱,自动创建"
}]
},
"outlook": {
"available": False,
"count": 0,
"services": []
},
"custom_domain": {
"available": False,
"count": 0,
"services": []
}
}
with get_db() as db:
# 获取 Outlook 账户
outlook_services = db.query(EmailServiceModel).filter(
EmailServiceModel.service_type == "outlook",
EmailServiceModel.enabled == True
).order_by(EmailServiceModel.priority.asc()).all()
for service in outlook_services:
config = service.config or {}
result["outlook"]["services"].append({
"id": service.id,
"name": service.name,
"type": "outlook",
"has_oauth": bool(config.get("client_id") and config.get("refresh_token")),
"priority": service.priority
})
result["outlook"]["count"] = len(outlook_services)
result["outlook"]["available"] = len(outlook_services) > 0
# 获取自定义域名服务
custom_services = db.query(EmailServiceModel).filter(
EmailServiceModel.service_type == "custom_domain",
EmailServiceModel.enabled == True
).order_by(EmailServiceModel.priority.asc()).all()
for service in custom_services:
config = service.config or {}
result["custom_domain"]["services"].append({
"id": service.id,
"name": service.name,
"type": "custom_domain",
"default_domain": config.get("default_domain"),
"priority": service.priority
})
result["custom_domain"]["count"] = len(custom_services)
result["custom_domain"]["available"] = len(custom_services) > 0
# 如果数据库中没有自定义域名服务,检查 settings
if not result["custom_domain"]["available"]:
if settings.custom_domain_base_url and settings.custom_domain_api_key:
result["custom_domain"]["available"] = True
result["custom_domain"]["count"] = 1
result["custom_domain"]["services"].append({
"id": None,
"name": "默认自定义域名服务",
"type": "custom_domain",
"from_settings": True
})
return result

File diff suppressed because it is too large Load Diff

View File

@@ -1,36 +1,37 @@
/**
* 账号管理页面 JavaScript
* 使用 utils.js 中的工具库
*/
// API 基础路径
const API_BASE = '/api';
// 状态
let currentPage = 1;
let pageSize = 20;
let totalAccounts = 0;
let selectedAccounts = new Set();
let isLoading = false;
// DOM 元素
const accountsTable = document.getElementById('accounts-table');
const totalAccountsEl = document.getElementById('total-accounts');
const activeAccountsEl = document.getElementById('active-accounts');
const expiredAccountsEl = document.getElementById('expired-accounts');
const failedAccountsEl = document.getElementById('failed-accounts');
const filterStatus = document.getElementById('filter-status');
const filterService = document.getElementById('filter-service');
const searchInput = document.getElementById('search-input');
const refreshBtn = document.getElementById('refresh-btn');
const batchDeleteBtn = document.getElementById('batch-delete-btn');
const exportBtn = document.getElementById('export-btn');
const exportMenu = document.getElementById('export-menu');
const selectAllCheckbox = document.getElementById('select-all');
const prevPageBtn = document.getElementById('prev-page');
const nextPageBtn = document.getElementById('next-page');
const pageInfo = document.getElementById('page-info');
const detailModal = document.getElementById('detail-modal');
const modalBody = document.getElementById('modal-body');
const closeModalBtn = document.getElementById('close-modal');
const elements = {
table: document.getElementById('accounts-table'),
totalAccounts: document.getElementById('total-accounts'),
activeAccounts: document.getElementById('active-accounts'),
expiredAccounts: document.getElementById('expired-accounts'),
failedAccounts: document.getElementById('failed-accounts'),
filterStatus: document.getElementById('filter-status'),
filterService: document.getElementById('filter-service'),
searchInput: document.getElementById('search-input'),
refreshBtn: document.getElementById('refresh-btn'),
batchDeleteBtn: document.getElementById('batch-delete-btn'),
exportBtn: document.getElementById('export-btn'),
exportMenu: document.getElementById('export-menu'),
selectAll: document.getElementById('select-all'),
prevPage: document.getElementById('prev-page'),
nextPage: document.getElementById('next-page'),
pageInfo: document.getElementById('page-info'),
detailModal: document.getElementById('detail-modal'),
modalBody: document.getElementById('modal-body'),
closeModal: document.getElementById('close-modal')
};
// 初始化
document.addEventListener('DOMContentLoaded', () => {
@@ -42,38 +43,44 @@ document.addEventListener('DOMContentLoaded', () => {
// 事件监听
function initEventListeners() {
// 筛选
filterStatus.addEventListener('change', () => {
elements.filterStatus.addEventListener('change', () => {
currentPage = 1;
loadAccounts();
});
filterService.addEventListener('change', () => {
elements.filterService.addEventListener('change', () => {
currentPage = 1;
loadAccounts();
});
// 搜索
let searchTimeout;
searchInput.addEventListener('input', () => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
currentPage = 1;
// 搜索(防抖)
elements.searchInput.addEventListener('input', debounce(() => {
currentPage = 1;
loadAccounts();
}, 300));
// 快捷键聚焦搜索
elements.searchInput.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
elements.searchInput.blur();
elements.searchInput.value = '';
loadAccounts();
}, 300);
}
});
// 刷新
refreshBtn.addEventListener('click', () => {
elements.refreshBtn.addEventListener('click', () => {
loadStats();
loadAccounts();
toast.info('已刷新');
});
// 批量删除
batchDeleteBtn.addEventListener('click', handleBatchDelete);
elements.batchDeleteBtn.addEventListener('click', handleBatchDelete);
// 全选
selectAllCheckbox.addEventListener('change', (e) => {
const checkboxes = accountsTable.querySelectorAll('input[type="checkbox"]');
elements.selectAll.addEventListener('change', (e) => {
const checkboxes = elements.table.querySelectorAll('input[type="checkbox"][data-id]');
checkboxes.forEach(cb => {
cb.checked = e.target.checked;
const id = parseInt(cb.dataset.id);
@@ -87,124 +94,189 @@ function initEventListeners() {
});
// 分页
prevPageBtn.addEventListener('click', () => {
if (currentPage > 1) {
elements.prevPage.addEventListener('click', () => {
if (currentPage > 1 && !isLoading) {
currentPage--;
loadAccounts();
}
});
nextPageBtn.addEventListener('click', () => {
elements.nextPage.addEventListener('click', () => {
const totalPages = Math.ceil(totalAccounts / pageSize);
if (currentPage < totalPages) {
if (currentPage < totalPages && !isLoading) {
currentPage++;
loadAccounts();
}
});
// 导出
exportBtn.addEventListener('click', (e) => {
elements.exportBtn.addEventListener('click', (e) => {
e.stopPropagation();
exportMenu.classList.toggle('active');
elements.exportMenu.classList.toggle('active');
});
document.querySelectorAll('#export-menu .dropdown-item').forEach(item => {
item.addEventListener('click', (e) => {
e.preventDefault();
const format = e.target.dataset.format;
exportAccounts(format);
exportMenu.classList.remove('active');
});
delegate(elements.exportMenu, 'click', '.dropdown-item', (e, target) => {
e.preventDefault();
const format = target.dataset.format;
exportAccounts(format);
elements.exportMenu.classList.remove('active');
});
// 关闭模态框
closeModalBtn.addEventListener('click', () => {
detailModal.classList.remove('active');
elements.closeModal.addEventListener('click', () => {
elements.detailModal.classList.remove('active');
});
detailModal.addEventListener('click', (e) => {
if (e.target === detailModal) {
detailModal.classList.remove('active');
elements.detailModal.addEventListener('click', (e) => {
if (e.target === elements.detailModal) {
elements.detailModal.classList.remove('active');
}
});
// 点击其他地方关闭下拉菜单
document.addEventListener('click', () => {
exportMenu.classList.remove('active');
elements.exportMenu.classList.remove('active');
});
}
// 加载统计信息
async function loadStats() {
try {
const response = await fetch(`${API_BASE}/accounts/stats/summary`);
const data = await response.json();
const data = await api.get('/accounts/stats/summary');
totalAccountsEl.textContent = data.total || 0;
activeAccountsEl.textContent = data.by_status?.active || 0;
expiredAccountsEl.textContent = data.by_status?.expired || 0;
failedAccountsEl.textContent = data.by_status?.failed || 0;
elements.totalAccounts.textContent = format.number(data.total || 0);
elements.activeAccounts.textContent = format.number(data.by_status?.active || 0);
elements.expiredAccounts.textContent = format.number(data.by_status?.expired || 0);
elements.failedAccounts.textContent = format.number(data.by_status?.failed || 0);
// 添加动画效果
animateValue(elements.totalAccounts, data.total || 0);
} catch (error) {
console.error('加载统计信息失败:', error);
}
}
// 数字动画
function animateValue(element, value) {
element.style.transition = 'transform 0.2s ease';
element.style.transform = 'scale(1.1)';
setTimeout(() => {
element.style.transform = 'scale(1)';
}, 200);
}
// 加载账号列表
async function loadAccounts() {
if (isLoading) return;
isLoading = true;
// 显示加载状态
elements.table.innerHTML = `
<tr>
<td colspan="7">
<div class="empty-state">
<div class="skeleton skeleton-text" style="width: 60%;"></div>
<div class="skeleton skeleton-text" style="width: 80%;"></div>
<div class="skeleton skeleton-text" style="width: 40%;"></div>
</div>
</td>
</tr>
`;
const params = new URLSearchParams({
page: currentPage,
page_size: pageSize,
});
if (filterStatus.value) {
params.append('status', filterStatus.value);
if (elements.filterStatus.value) {
params.append('status', elements.filterStatus.value);
}
if (filterService.value) {
params.append('email_service', filterService.value);
if (elements.filterService.value) {
params.append('email_service', elements.filterService.value);
}
if (searchInput.value.trim()) {
params.append('search', searchInput.value.trim());
if (elements.searchInput.value.trim()) {
params.append('search', elements.searchInput.value.trim());
}
try {
const response = await fetch(`${API_BASE}/accounts?${params}`);
const data = await response.json();
const data = await api.get(`/accounts?${params}`);
totalAccounts = data.total;
renderAccounts(data.accounts);
updatePagination();
} catch (error) {
console.error('加载账号列表失败:', error);
accountsTable.innerHTML = '<tr><td colspan="7" style="text-align: center;">加载失败</td></tr>';
elements.table.innerHTML = `
<tr>
<td colspan="7">
<div class="empty-state">
<div class="empty-state-icon">❌</div>
<div class="empty-state-title">加载失败</div>
<div class="empty-state-description">请检查网络连接后重试</div>
</div>
</td>
</tr>
`;
} finally {
isLoading = false;
}
}
// 渲染账号列表
function renderAccounts(accounts) {
if (accounts.length === 0) {
accountsTable.innerHTML = '<tr><td colspan="7" style="text-align: center;">暂无数据</td></tr>';
elements.table.innerHTML = `
<tr>
<td colspan="7">
<div class="empty-state">
<div class="empty-state-icon">📭</div>
<div class="empty-state-title">暂无数据</div>
<div class="empty-state-description">没有找到符合条件的账号记录</div>
</div>
</td>
</tr>
`;
return;
}
accountsTable.innerHTML = accounts.map(account => `
<tr>
<td><input type="checkbox" data-id="${account.id}" ${selectedAccounts.has(account.id) ? 'checked' : ''}></td>
<td>${account.id}</td>
<td>${escapeHtml(account.email)}</td>
<td>${escapeHtml(account.email_service)}</td>
<td><span class="status-badge ${account.status}">${getStatusText(account.status)}</span></td>
<td>${formatDate(account.registered_at)}</td>
elements.table.innerHTML = accounts.map(account => `
<tr data-id="${account.id}">
<td>
<button class="btn btn-sm btn-secondary" onclick="viewAccount(${account.id})">查看</button>
<button class="btn btn-sm btn-danger" onclick="deleteAccount(${account.id}, '${escapeHtml(account.email)}')">删除</button>
<input type="checkbox" data-id="${account.id}"
${selectedAccounts.has(account.id) ? 'checked' : ''}>
</td>
<td>${account.id}</td>
<td>
<span class="email-cell" title="${escapeHtml(account.email)}">
${escapeHtml(account.email)}
</span>
</td>
<td>${getServiceTypeText(account.email_service)}</td>
<td>
<span class="status-badge ${getStatusClass('account', account.status)}">
${getStatusText('account', account.status)}
</span>
</td>
<td>${format.date(account.registered_at)}</td>
<td>
<div class="action-buttons">
<button class="btn btn-ghost btn-sm" onclick="viewAccount(${account.id})" title="查看详情">
👁️
</button>
<button class="btn btn-ghost btn-sm" onclick="copyEmail('${escapeHtml(account.email)}')" title="复制邮箱">
📋
</button>
<button class="btn btn-ghost btn-sm" onclick="deleteAccount(${account.id}, '${escapeHtml(account.email)}')" title="删除">
🗑️
</button>
</div>
</td>
</tr>
`).join('');
// 绑定复选框事件
accountsTable.querySelectorAll('input[type="checkbox"]').forEach(cb => {
elements.table.querySelectorAll('input[type="checkbox"][data-id]').forEach(cb => {
cb.addEventListener('change', (e) => {
const id = parseInt(e.target.dataset.id);
if (e.target.checked) {
@@ -219,91 +291,107 @@ function renderAccounts(accounts) {
// 更新分页
function updatePagination() {
const totalPages = Math.ceil(totalAccounts / pageSize);
const totalPages = Math.max(1, Math.ceil(totalAccounts / pageSize));
prevPageBtn.disabled = currentPage <= 1;
nextPageBtn.disabled = currentPage >= totalPages;
elements.prevPage.disabled = currentPage <= 1;
elements.nextPage.disabled = currentPage >= totalPages;
pageInfo.textContent = `${currentPage} 页 / 共 ${totalPages}`;
elements.pageInfo.textContent = `${currentPage} 页 / 共 ${totalPages}`;
}
// 更新批量操作按钮
function updateBatchButtons() {
batchDeleteBtn.disabled = selectedAccounts.size === 0;
const count = selectedAccounts.size;
elements.batchDeleteBtn.disabled = count === 0;
elements.batchDeleteBtn.textContent = count > 0 ? `🗑️ 删除选中 (${count})` : '🗑️ 批量删除';
}
// 查看账号详情
async function viewAccount(id) {
try {
const response = await fetch(`${API_BASE}/accounts/${id}`);
const account = await response.json();
const account = await api.get(`/accounts/${id}`);
const tokens = await api.get(`/accounts/${id}/tokens`);
const tokensResponse = await fetch(`${API_BASE}/accounts/${id}/tokens`);
const tokens = await tokensResponse.json();
modalBody.innerHTML = `
elements.modalBody.innerHTML = `
<div class="info-grid">
<div class="info-item">
<span class="label">邮箱</span>
<span class="value">${escapeHtml(account.email)}</span>
<span class="value">
${escapeHtml(account.email)}
<button class="btn btn-ghost btn-sm" onclick="copyToClipboard('${escapeHtml(account.email)}')" title="复制">
📋
</button>
</span>
</div>
<div class="info-item">
<span class="label">邮箱服务</span>
<span class="value">${escapeHtml(account.email_service)}</span>
<span class="value">${getServiceTypeText(account.email_service)}</span>
</div>
<div class="info-item">
<span class="label">状态</span>
<span class="value">${getStatusText(account.status)}</span>
<span class="value">
<span class="status-badge ${getStatusClass('account', account.status)}">
${getStatusText('account', account.status)}
</span>
</span>
</div>
<div class="info-item">
<span class="label">注册时间</span>
<span class="value">${formatDate(account.registered_at)}</span>
<span class="value">${format.date(account.registered_at)}</span>
</div>
<div class="info-item">
<div class="info-item" style="grid-column: span 2;">
<span class="label">Account ID</span>
<span class="value">${escapeHtml(account.account_id || '-')}</span>
<span class="value" style="font-size: 0.75rem; word-break: break-all;">
${escapeHtml(account.account_id || '-')}
</span>
</div>
<div class="info-item">
<div class="info-item" style="grid-column: span 2;">
<span class="label">Workspace ID</span>
<span class="value">${escapeHtml(account.workspace_id || '-')}</span>
<span class="value" style="font-size: 0.75rem; word-break: break-all;">
${escapeHtml(account.workspace_id || '-')}
</span>
</div>
<div class="info-item">
<div class="info-item" style="grid-column: span 2;">
<span class="label">Access Token</span>
<span class="value" style="font-size: 0.75rem; word-break: break-all;">${escapeHtml(tokens.access_token || '-')}</span>
<div class="value" style="font-size: 0.7rem; word-break: break-all; font-family: var(--font-mono); background: var(--surface-hover); padding: 8px; border-radius: 4px;">
${escapeHtml(tokens.access_token || '-')}
${tokens.access_token ? `<button class="btn btn-ghost btn-sm" onclick="copyToClipboard('${escapeHtml(tokens.access_token)}')" style="margin-left: 8px;">📋</button>` : ''}
</div>
</div>
<div class="info-item">
<div class="info-item" style="grid-column: span 2;">
<span class="label">Refresh Token</span>
<span class="value" style="font-size: 0.75rem; word-break: break-all;">${escapeHtml(tokens.refresh_token || '-')}</span>
<div class="value" style="font-size: 0.7rem; word-break: break-all; font-family: var(--font-mono); background: var(--surface-hover); padding: 8px; border-radius: 4px;">
${escapeHtml(tokens.refresh_token || '-')}
${tokens.refresh_token ? `<button class="btn btn-ghost btn-sm" onclick="copyToClipboard('${escapeHtml(tokens.refresh_token)}')" style="margin-left: 8px;">📋</button>` : ''}
</div>
</div>
</div>
`;
detailModal.classList.add('active');
elements.detailModal.classList.add('active');
} catch (error) {
alert('加载账号详情失败: ' + error.message);
toast.error('加载账号详情失败: ' + error.message);
}
}
// 复制邮箱
function copyEmail(email) {
copyToClipboard(email);
}
// 删除账号
async function deleteAccount(id, email) {
if (!confirm(`确定要删除账号 ${email} 吗?`)) {
return;
}
const confirmed = await confirm(`确定要删除账号 ${email} 吗?此操作不可恢复。`);
if (!confirmed) return;
try {
const response = await fetch(`${API_BASE}/accounts/${id}`, {
method: 'DELETE',
});
if (response.ok) {
loadStats();
loadAccounts();
} else {
const data = await response.json();
alert('删除失败: ' + (data.detail || '未知错误'));
}
await api.delete(`/accounts/${id}`);
toast.success('账号已删除');
selectedAccounts.delete(id);
loadStats();
loadAccounts();
} catch (error) {
alert('删除失败: ' + error.message);
toast.error('删除失败: ' + error.message);
}
}
@@ -311,33 +399,20 @@ async function deleteAccount(id, email) {
async function handleBatchDelete() {
if (selectedAccounts.size === 0) return;
if (!confirm(`确定要删除选中的 ${selectedAccounts.size} 个账号吗?`)) {
return;
}
const confirmed = await confirm(`确定要删除选中的 ${selectedAccounts.size} 个账号吗?此操作不可恢复。`);
if (!confirmed) return;
try {
const response = await fetch(`${API_BASE}/accounts/batch-delete`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
ids: Array.from(selectedAccounts),
}),
const result = await api.post('/accounts/batch-delete', {
ids: Array.from(selectedAccounts)
});
const data = await response.json();
if (response.ok) {
alert(`成功删除 ${data.deleted_count} 个账号`);
selectedAccounts.clear();
loadStats();
loadAccounts();
} else {
alert('删除失败: ' + (data.detail || '未知错误'));
}
toast.success(`成功删除 ${result.deleted_count} 个账号`);
selectedAccounts.clear();
loadStats();
loadAccounts();
} catch (error) {
alert('删除失败: ' + error.message);
toast.error('删除失败: ' + error.message);
}
}
@@ -345,37 +420,22 @@ async function handleBatchDelete() {
function exportAccounts(format) {
const params = new URLSearchParams();
if (filterStatus.value) {
params.append('status', filterStatus.value);
if (elements.filterStatus.value) {
params.append('status', elements.filterStatus.value);
}
if (filterService.value) {
params.append('email_service', filterService.value);
if (elements.filterService.value) {
params.append('email_service', elements.filterService.value);
}
window.location.href = `${API_BASE}/accounts/export/${format}?${params}`;
window.location.href = `/api/accounts/export/${format}?${params}`;
toast.info('正在导出...');
}
// 工具函数
// HTML 转义
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function getStatusText(status) {
const statusMap = {
'active': '活跃',
'expired': '过期',
'banned': '封禁',
'failed': '失败',
};
return statusMap[status] || status;
}
function formatDate(dateStr) {
if (!dateStr) return '-';
const date = new Date(dateStr);
return date.toLocaleString('zh-CN');
}

View File

@@ -1,160 +1,303 @@
/**
* 注册页面 JavaScript
* 使用 utils.js 中的工具库
*/
// API 基础路径
const API_BASE = '/api';
// 状态
let currentTask = null;
let currentBatch = null;
let logPollingInterval = null;
let batchPollingInterval = null;
let isBatchMode = false;
let availableServices = {
tempmail: { available: true, services: [] },
outlook: { available: false, services: [] },
custom_domain: { available: false, services: [] }
};
// DOM 元素
const registrationForm = document.getElementById('registration-form');
const emailServiceSelect = document.getElementById('email-service');
const proxyInput = document.getElementById('proxy');
const regModeSelect = document.getElementById('reg-mode');
const batchCountGroup = document.getElementById('batch-count-group');
const batchCountInput = document.getElementById('batch-count');
const batchOptions = document.getElementById('batch-options');
const intervalMinInput = document.getElementById('interval-min');
const intervalMaxInput = document.getElementById('interval-max');
const startBtn = document.getElementById('start-btn');
const cancelBtn = document.getElementById('cancel-btn');
const taskStatusCard = document.getElementById('task-status-card');
const batchStatusCard = document.getElementById('batch-status-card');
const consoleLog = document.getElementById('console-log');
const clearLogBtn = document.getElementById('clear-log-btn');
const elements = {
form: document.getElementById('registration-form'),
emailService: document.getElementById('email-service'),
proxy: document.getElementById('proxy'),
regMode: document.getElementById('reg-mode'),
batchCountGroup: document.getElementById('batch-count-group'),
batchCount: document.getElementById('batch-count'),
batchOptions: document.getElementById('batch-options'),
intervalMin: document.getElementById('interval-min'),
intervalMax: document.getElementById('interval-max'),
startBtn: document.getElementById('start-btn'),
cancelBtn: document.getElementById('cancel-btn'),
taskStatusCard: document.getElementById('task-status-card'),
batchStatusCard: document.getElementById('batch-status-card'),
consoleLog: document.getElementById('console-log'),
clearLogBtn: document.getElementById('clear-log-btn'),
// 任务状态
taskId: document.getElementById('task-id'),
taskEmail: document.getElementById('task-email'),
taskStatus: document.getElementById('task-status'),
taskService: document.getElementById('task-service'),
taskStatusBadge: document.getElementById('task-status-badge'),
// 批量状态
batchProgress: document.getElementById('batch-progress'),
progressBar: document.getElementById('progress-bar'),
batchSuccess: document.getElementById('batch-success'),
batchFailed: document.getElementById('batch-failed'),
batchRemaining: document.getElementById('batch-remaining')
};
// 初始化
document.addEventListener('DOMContentLoaded', () => {
initEventListeners();
loadSavedProxy();
loadAvailableServices();
});
// 事件监听
function initEventListeners() {
// 注册表单提交
registrationForm.addEventListener('submit', handleStartRegistration);
elements.form.addEventListener('submit', handleStartRegistration);
// 注册模式切换
regModeSelect.addEventListener('change', handleModeChange);
elements.regMode.addEventListener('change', handleModeChange);
// 邮箱服务切换
elements.emailService.addEventListener('change', handleServiceChange);
// 取消按钮
cancelBtn.addEventListener('click', handleCancelTask);
elements.cancelBtn.addEventListener('click', handleCancelTask);
// 清空日志
clearLogBtn.addEventListener('click', () => {
consoleLog.innerHTML = '<div class="log-line info">[*] 日志已清空</div>';
elements.clearLogBtn.addEventListener('click', () => {
elements.consoleLog.innerHTML = '<div class="log-line info">[系统] 日志已清空</div>';
});
}
// 加载保存的代理设置
async function loadSavedProxy() {
try {
const settings = await api.get('/settings');
if (settings.proxy?.host) {
elements.proxy.value = `${settings.proxy.type}://${settings.proxy.host}:${settings.proxy.port}`;
}
} catch (error) {
// 忽略错误
}
}
// 加载可用的邮箱服务
async function loadAvailableServices() {
try {
const data = await api.get('/registration/available-services');
availableServices = data;
// 更新邮箱服务选择框
updateEmailServiceOptions();
addLog('info', '[系统] 邮箱服务列表已加载');
} catch (error) {
console.error('加载邮箱服务列表失败:', error);
addLog('warning', '[警告] 加载邮箱服务列表失败');
}
}
// 更新邮箱服务选择框
function updateEmailServiceOptions() {
const select = elements.emailService;
select.innerHTML = '';
// Tempmail
if (availableServices.tempmail.available) {
const optgroup = document.createElement('optgroup');
optgroup.label = '🌐 临时邮箱';
availableServices.tempmail.services.forEach(service => {
const option = document.createElement('option');
option.value = `tempmail:${service.id || 'default'}`;
option.textContent = service.name;
option.dataset.type = 'tempmail';
optgroup.appendChild(option);
});
select.appendChild(optgroup);
}
// Outlook
if (availableServices.outlook.available) {
const optgroup = document.createElement('optgroup');
optgroup.label = `📧 Outlook (${availableServices.outlook.count} 个账户)`;
availableServices.outlook.services.forEach(service => {
const option = document.createElement('option');
option.value = `outlook:${service.id}`;
option.textContent = service.name + (service.has_oauth ? ' (OAuth)' : '');
option.dataset.type = 'outlook';
option.dataset.serviceId = service.id;
optgroup.appendChild(option);
});
select.appendChild(optgroup);
} else {
const optgroup = document.createElement('optgroup');
optgroup.label = '📧 Outlook (未配置)';
const option = document.createElement('option');
option.value = '';
option.textContent = '请先在设置中导入账户';
option.disabled = true;
optgroup.appendChild(option);
select.appendChild(optgroup);
}
// 自定义域名
if (availableServices.custom_domain.available) {
const optgroup = document.createElement('optgroup');
optgroup.label = `🔗 自定义域名 (${availableServices.custom_domain.count} 个服务)`;
availableServices.custom_domain.services.forEach(service => {
const option = document.createElement('option');
option.value = `custom_domain:${service.id || 'default'}`;
option.textContent = service.name;
option.dataset.type = 'custom_domain';
if (service.id) {
option.dataset.serviceId = service.id;
}
optgroup.appendChild(option);
});
select.appendChild(optgroup);
} else {
const optgroup = document.createElement('optgroup');
optgroup.label = '🔗 自定义域名 (未配置)';
const option = document.createElement('option');
option.value = '';
option.textContent = '请先在设置中添加服务';
option.disabled = true;
optgroup.appendChild(option);
select.appendChild(optgroup);
}
}
// 处理邮箱服务切换
function handleServiceChange(e) {
const value = e.target.value;
if (!value) return;
const [type, id] = value.split(':');
const selectedOption = e.target.options[e.target.selectedIndex];
// 显示服务信息
if (type === 'outlook') {
const service = availableServices.outlook.services.find(s => s.id == id);
if (service) {
addLog('info', `[系统] 已选择 Outlook 账户: ${service.name}`);
}
} else if (type === 'custom_domain') {
const service = availableServices.custom_domain.services.find(s => s.id == id);
if (service) {
addLog('info', `[系统] 已选择自定义域名服务: ${service.name}`);
}
}
}
// 模式切换
function handleModeChange(e) {
const mode = e.target.value;
isBatchMode = mode === 'batch';
batchCountGroup.style.display = isBatchMode ? 'block' : 'none';
batchOptions.style.display = isBatchMode ? 'block' : 'none';
elements.batchCountGroup.style.display = isBatchMode ? 'block' : 'none';
elements.batchOptions.style.display = isBatchMode ? 'block' : 'none';
}
// 开始注册
async function handleStartRegistration(e) {
e.preventDefault();
const emailService = emailServiceSelect.value;
const proxy = proxyInput.value.trim() || null;
const selectedValue = elements.emailService.value;
if (!selectedValue) {
toast.error('请选择一个邮箱服务');
return;
}
const [emailServiceType, serviceId] = selectedValue.split(':');
const proxy = elements.proxy.value.trim() || null;
// 禁用开始按钮
startBtn.disabled = true;
cancelBtn.disabled = false;
elements.startBtn.disabled = true;
elements.cancelBtn.disabled = false;
// 清空日志
consoleLog.innerHTML = '';
elements.consoleLog.innerHTML = '';
// 构建请求数据
const requestData = {
email_service_type: emailServiceType,
proxy: proxy
};
// 如果选择了数据库中的服务,传递 service_id
if (serviceId && serviceId !== 'default') {
requestData.email_service_id = parseInt(serviceId);
}
if (isBatchMode) {
await handleBatchRegistration(emailService, proxy);
await handleBatchRegistration(requestData);
} else {
await handleSingleRegistration(emailService, proxy);
await handleSingleRegistration(requestData);
}
}
// 单次注册
async function handleSingleRegistration(emailService, proxy) {
addLog('info', '[*] 正在启动注册任务...');
async function handleSingleRegistration(requestData) {
addLog('info', '[系统] 正在启动注册任务...');
try {
const response = await fetch(`${API_BASE}/registration/start`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email_service_type: emailService,
proxy: proxy,
}),
});
const data = await api.post('/registration/start', requestData);
const data = await response.json();
currentTask = data;
addLog('info', `[系统] 任务已创建: ${data.task_uuid}`);
showTaskStatus(data);
updateTaskStatus('running');
if (response.ok) {
currentTask = data;
addLog('info', `[*] 任务已创建: ${data.task_uuid}`);
showTaskStatus(data);
// 开始轮询日志
startLogPolling(data.task_uuid);
// 开始轮询日志
startLogPolling(data.task_uuid);
} else {
addLog('error', `[Error] 启动失败: ${data.detail || '未知错误'}`);
resetButtons();
}
} catch (error) {
addLog('error', `[Error] 网络错误: ${error.message}`);
addLog('error', `[错误] 启动失败: ${error.message}`);
toast.error(error.message);
resetButtons();
}
}
// 批量注册
async function handleBatchRegistration(emailService, proxy) {
const count = parseInt(batchCountInput.value) || 5;
const intervalMin = parseInt(intervalMinInput.value) || 5;
const intervalMax = parseInt(intervalMaxInput.value) || 30;
async function handleBatchRegistration(requestData) {
const count = parseInt(elements.batchCount.value) || 5;
const intervalMin = parseInt(elements.intervalMin.value) || 5;
const intervalMax = parseInt(elements.intervalMax.value) || 30;
addLog('info', `[*] 正在启动批量注册任务 (数量: ${count})...`);
requestData.count = count;
requestData.interval_min = intervalMin;
requestData.interval_max = intervalMax;
addLog('info', `[系统] 正在启动批量注册任务 (数量: ${count})...`);
try {
const response = await fetch(`${API_BASE}/registration/batch`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
count: count,
email_service_type: emailService,
proxy: proxy,
interval_min: intervalMin,
interval_max: intervalMax,
}),
});
const data = await api.post('/registration/batch', requestData);
const data = await response.json();
currentBatch = data;
addLog('info', `[系统] 批量任务已创建: ${data.batch_id}`);
addLog('info', `[系统] 共 ${data.count} 个任务已加入队列`);
showBatchStatus(data);
if (response.ok) {
currentBatch = data;
addLog('info', `[*] 批量任务已创建: ${data.batch_id}`);
addLog('info', `[*] 共 ${data.count} 个任务已加入队列`);
showBatchStatus(data);
// 开始轮询批量状态
startBatchPolling(data.batch_id);
// 开始轮询批量状态
startBatchPolling(data.batch_id);
} else {
addLog('error', `[Error] 启动失败: ${data.detail || '未知错误'}`);
resetButtons();
}
} catch (error) {
addLog('error', `[Error] 网络错误: ${error.message}`);
addLog('error', `[错误] 启动失败: ${error.message}`);
toast.error(error.message);
resetButtons();
}
}
@@ -163,68 +306,70 @@ async function handleBatchRegistration(emailService, proxy) {
async function handleCancelTask() {
if (isBatchMode && currentBatch) {
try {
const response = await fetch(`${API_BASE}/registration/batch/${currentBatch.batch_id}/cancel`, {
method: 'POST',
});
if (response.ok) {
addLog('warning', '[!] 批量任务取消请求已提交');
stopBatchPolling();
resetButtons();
}
await api.post(`/registration/batch/${currentBatch.batch_id}/cancel`);
addLog('warning', '[警告] 批量任务取消请求已提交');
toast.info('任务取消请求已提交');
stopBatchPolling();
resetButtons();
} catch (error) {
addLog('error', `[Error] 取消失败: ${error.message}`);
addLog('error', `[错误] 取消失败: ${error.message}`);
toast.error(error.message);
}
} else if (currentTask) {
try {
const response = await fetch(`${API_BASE}/registration/tasks/${currentTask.task_uuid}/cancel`, {
method: 'POST',
});
if (response.ok) {
addLog('warning', '[!] 任务已取消');
stopLogPolling();
resetButtons();
}
await api.post(`/registration/tasks/${currentTask.task_uuid}/cancel`);
addLog('warning', '[警告] 任务已取消');
toast.info('任务已取消');
stopLogPolling();
resetButtons();
} catch (error) {
addLog('error', `[Error] 取消失败: ${error.message}`);
addLog('error', `[错误] 取消失败: ${error.message}`);
toast.error(error.message);
}
}
}
// 开始轮询日志
function startLogPolling(taskUuid) {
let lastLogLine = '';
let lastLogIndex = 0;
logPollingInterval = setInterval(async () => {
try {
const response = await fetch(`${API_BASE}/registration/tasks/${taskUuid}/logs`);
const data = await response.json();
const data = await api.get(`/registration/tasks/${taskUuid}/logs`);
if (response.ok) {
// 更新任务状态
updateTaskStatus(data.status);
// 更新任务状态
updateTaskStatus(data.status);
// 添加新日志
const logs = data.logs || [];
logs.forEach(log => {
if (log !== lastLogLine) {
const logType = getLogType(log);
addLog(logType, log);
lastLogLine = log;
}
});
// 更新邮箱信息
if (data.email) {
elements.taskEmail.textContent = data.email;
}
if (data.email_service) {
elements.taskService.textContent = getServiceTypeText(data.email_service);
}
// 检查任务是否完成
if (['completed', 'failed', 'cancelled'].includes(data.status)) {
stopLogPolling();
resetButtons();
// 添加新日志
const logs = data.logs || [];
for (let i = lastLogIndex; i < logs.length; i++) {
const log = logs[i];
const logType = getLogType(log);
addLog(logType, log);
}
lastLogIndex = logs.length;
if (data.status === 'completed') {
addLog('success', '[*] 注册成功!');
} else if (data.status === 'failed') {
addLog('error', '[Error] 注册失败');
}
// 检查任务是否完成
if (['completed', 'failed', 'cancelled'].includes(data.status)) {
stopLogPolling();
resetButtons();
if (data.status === 'completed') {
addLog('success', '[成功] 注册成功!');
toast.success('注册成功!');
} else if (data.status === 'failed') {
addLog('error', '[错误] 注册失败');
toast.error('注册失败');
} else if (data.status === 'cancelled') {
addLog('warning', '[警告] 任务已取消');
}
}
} catch (error) {
@@ -245,18 +390,19 @@ function stopLogPolling() {
function startBatchPolling(batchId) {
batchPollingInterval = setInterval(async () => {
try {
const response = await fetch(`${API_BASE}/registration/batch/${batchId}`);
const data = await response.json();
const data = await api.get(`/registration/batch/${batchId}`);
updateBatchProgress(data);
if (response.ok) {
updateBatchProgress(data);
// 检查是否完成
if (data.finished) {
stopBatchPolling();
resetButtons();
// 检查是否完成
if (data.finished) {
stopBatchPolling();
resetButtons();
addLog('info', `[*] 批量任务完成!成功: ${data.success}, 失败: ${data.failed}`);
addLog('info', `[完成] 批量任务完成!成功: ${data.success}, 失败: ${data.failed}`);
if (data.success > 0) {
toast.success(`批量注册完成,成功 ${data.success}`);
} else {
toast.warning('批量注册完成,但没有成功注册任何账号');
}
}
} catch (error) {
@@ -275,54 +421,67 @@ function stopBatchPolling() {
// 显示任务状态
function showTaskStatus(task) {
taskStatusCard.style.display = 'block';
batchStatusCard.style.display = 'none';
document.getElementById('task-id').textContent = task.task_uuid;
updateTaskStatus(task.status);
elements.taskStatusCard.style.display = 'block';
elements.batchStatusCard.style.display = 'none';
elements.taskId.textContent = task.task_uuid;
elements.taskEmail.textContent = '-';
elements.taskService.textContent = '-';
}
// 更新任务状态
function updateTaskStatus(status) {
const statusBadge = document.getElementById('task-status-badge');
const statusText = document.getElementById('task-status');
const statusMap = {
'pending': { text: '等待中', class: '' },
'running': { text: '运行中', class: 'running' },
'completed': { text: '已完成', class: 'completed' },
'failed': { text: '失败', class: 'failed' },
'cancelled': { text: '已取消', class: '' },
const statusInfo = {
pending: { text: '等待中', class: 'pending' },
running: { text: '运行中', class: 'running' },
completed: { text: '已完成', class: 'completed' },
failed: { text: '失败', class: 'failed' },
cancelled: { text: '已取消', class: 'disabled' }
};
const info = statusMap[status] || { text: status, class: '' };
statusBadge.textContent = info.text;
statusBadge.className = 'status-badge ' + info.class;
statusText.textContent = info.text;
const info = statusInfo[status] || { text: status, class: '' };
elements.taskStatusBadge.textContent = info.text;
elements.taskStatusBadge.className = `status-badge ${info.class}`;
elements.taskStatus.textContent = info.text;
}
// 显示批量状态
function showBatchStatus(batch) {
batchStatusCard.style.display = 'block';
taskStatusCard.style.display = 'none';
document.getElementById('batch-progress').textContent = `0/${batch.count}`;
document.getElementById('progress-bar').style.width = '0%';
document.getElementById('batch-success').textContent = '0';
document.getElementById('batch-failed').textContent = '0';
document.getElementById('batch-remaining').textContent = batch.count;
elements.batchStatusCard.style.display = 'block';
elements.taskStatusCard.style.display = 'none';
elements.batchProgress.textContent = `0/${batch.count}`;
elements.progressBar.style.width = '0%';
elements.batchSuccess.textContent = '0';
elements.batchFailed.textContent = '0';
elements.batchRemaining.textContent = batch.count;
// 重置计数器
elements.batchSuccess.dataset.last = '0';
elements.batchFailed.dataset.last = '0';
}
// 更新批量进度
function updateBatchProgress(data) {
const progress = data.completed / data.total * 100;
document.getElementById('batch-progress').textContent = data.progress;
document.getElementById('progress-bar').style.width = `${progress}%`;
document.getElementById('batch-success').textContent = data.success;
document.getElementById('batch-failed').textContent = data.failed;
document.getElementById('batch-remaining').textContent = data.total - data.completed;
const progress = (data.completed / data.total * 100).toFixed(0);
elements.batchProgress.textContent = data.progress || `${data.completed}/${data.total}`;
elements.progressBar.style.width = `${progress}%`;
elements.batchSuccess.textContent = data.success;
elements.batchFailed.textContent = data.failed;
elements.batchRemaining.textContent = data.total - data.completed;
// 记录日志
// 记录日志(避免重复)
if (data.completed > 0) {
addLog('info', `[*] 进度: ${data.progress}, 成功: ${data.success}, 失败: ${data.failed}`);
const lastSuccess = parseInt(elements.batchSuccess.dataset.last || '0');
const lastFailed = parseInt(elements.batchFailed.dataset.last || '0');
if (data.success > lastSuccess) {
addLog('success', `[成功] 第 ${data.success} 个账号注册成功`);
}
if (data.failed > lastFailed) {
addLog('error', `[失败] 第 ${data.failed} 个账号注册失败`);
}
elements.batchSuccess.dataset.last = data.success;
elements.batchFailed.dataset.last = data.failed;
}
}
@@ -330,22 +489,39 @@ function updateBatchProgress(data) {
function addLog(type, message) {
const line = document.createElement('div');
line.className = `log-line ${type}`;
line.textContent = message;
consoleLog.appendChild(line);
// 添加时间戳
const timestamp = new Date().toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
line.innerHTML = `<span class="timestamp">[${timestamp}]</span>${escapeHtml(message)}`;
elements.consoleLog.appendChild(line);
// 自动滚动到底部
consoleLog.scrollTop = consoleLog.scrollHeight;
elements.consoleLog.scrollTop = elements.consoleLog.scrollHeight;
// 限制日志行数
const lines = elements.consoleLog.querySelectorAll('.log-line');
if (lines.length > 500) {
lines[0].remove();
}
}
// 获取日志类型
function getLogType(log) {
if (log.includes('[Error]') || log.includes('失败') || log.includes('错误')) {
if (typeof log !== 'string') return 'info';
const lowerLog = log.toLowerCase();
if (lowerLog.includes('error') || lowerLog.includes('失败') || lowerLog.includes('错误')) {
return 'error';
}
if (log.includes('[!]') || log.includes('警告')) {
if (lowerLog.includes('warning') || lowerLog.includes('警告')) {
return 'warning';
}
if (log.includes('成功') || log.includes('完成')) {
if (lowerLog.includes('success') || lowerLog.includes('成功') || lowerLog.includes('完成')) {
return 'success';
}
return 'info';
@@ -353,8 +529,16 @@ function getLogType(log) {
// 重置按钮状态
function resetButtons() {
startBtn.disabled = false;
cancelBtn.disabled = true;
elements.startBtn.disabled = false;
elements.cancelBtn.disabled = true;
currentTask = null;
currentBatch = null;
}
// HTML 转义
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}

View File

@@ -1,41 +1,38 @@
/**
* 设置页面 JavaScript
* 使用 utils.js 中的工具库
*/
// API 基础路径
const API_BASE = '/api';
// DOM 元素
const tabBtns = document.querySelectorAll('.tab-btn');
const tabContents = document.querySelectorAll('.tab-content');
const proxyForm = document.getElementById('proxy-form');
const registrationForm = document.getElementById('registration-form');
const testProxyBtn = document.getElementById('test-proxy-btn');
const backupBtn = document.getElementById('backup-btn');
const cleanupBtn = document.getElementById('cleanup-btn');
const addEmailServiceBtn = document.getElementById('add-email-service-btn');
const addServiceModal = document.getElementById('add-service-modal');
const addServiceForm = document.getElementById('add-service-form');
const closeServiceModalBtn = document.getElementById('close-service-modal');
const cancelAddServiceBtn = document.getElementById('cancel-add-service');
const serviceTypeSelect = document.getElementById('service-type');
const serviceConfigFields = document.getElementById('service-config-fields');
const emailServicesTable = document.getElementById('email-services-table');
// Outlook 批量导入相关
const toggleImportBtn = document.getElementById('toggle-import-btn');
const outlookImportBody = document.getElementById('outlook-import-body');
const outlookImportBtn = document.getElementById('outlook-import-btn');
const clearImportBtn = document.getElementById('clear-import-btn');
const outlookImportData = document.getElementById('outlook-import-data');
const importResult = document.getElementById('import-result');
// 批量操作
const batchDeleteBtn = document.getElementById('batch-delete-btn');
const selectAllCheckbox = document.getElementById('select-all-services');
const elements = {
tabs: document.querySelectorAll('.tab-btn'),
tabContents: document.querySelectorAll('.tab-content'),
proxyForm: document.getElementById('proxy-form'),
registrationForm: document.getElementById('registration-settings-form'),
testProxyBtn: document.getElementById('test-proxy-btn'),
backupBtn: document.getElementById('backup-btn'),
cleanupBtn: document.getElementById('cleanup-btn'),
addEmailServiceBtn: document.getElementById('add-email-service-btn'),
addServiceModal: document.getElementById('add-service-modal'),
addServiceForm: document.getElementById('add-service-form'),
closeServiceModal: document.getElementById('close-service-modal'),
cancelAddService: document.getElementById('cancel-add-service'),
serviceType: document.getElementById('service-type'),
serviceConfigFields: document.getElementById('service-config-fields'),
emailServicesTable: document.getElementById('email-services-table'),
// Outlook 导入
toggleImportBtn: document.getElementById('toggle-import-btn'),
outlookImportBody: document.getElementById('outlook-import-body'),
outlookImportBtn: document.getElementById('outlook-import-btn'),
clearImportBtn: document.getElementById('clear-import-btn'),
outlookImportData: document.getElementById('outlook-import-data'),
importResult: document.getElementById('import-result'),
// 批量操作
selectAllServices: document.getElementById('select-all-services')
};
// 选中的服务 ID
let selectedServiceIds = [];
let selectedServiceIds = new Set();
// 初始化
document.addEventListener('DOMContentLoaded', () => {
@@ -48,12 +45,12 @@ document.addEventListener('DOMContentLoaded', () => {
// 初始化标签页
function initTabs() {
tabBtns.forEach(btn => {
elements.tabs.forEach(btn => {
btn.addEventListener('click', () => {
const tab = btn.dataset.tab;
tabBtns.forEach(b => b.classList.remove('active'));
tabContents.forEach(c => c.classList.remove('active'));
elements.tabs.forEach(b => b.classList.remove('active'));
elements.tabContents.forEach(c => c.classList.remove('active'));
btn.classList.add('active');
document.getElementById(`${tab}-tab`).classList.add('active');
@@ -64,90 +61,84 @@ function initTabs() {
// 事件监听
function initEventListeners() {
// 代理表单
proxyForm.addEventListener('submit', handleSaveProxy);
elements.proxyForm.addEventListener('submit', handleSaveProxy);
// 测试代理
testProxyBtn.addEventListener('click', handleTestProxy);
elements.testProxyBtn.addEventListener('click', handleTestProxy);
// 注册配置表单
registrationForm.addEventListener('submit', handleSaveRegistration);
elements.registrationForm.addEventListener('submit', handleSaveRegistration);
// 备份数据库
backupBtn.addEventListener('click', handleBackup);
elements.backupBtn.addEventListener('click', handleBackup);
// 清理数据
cleanupBtn.addEventListener('click', handleCleanup);
elements.cleanupBtn.addEventListener('click', handleCleanup);
// 添加邮箱服务
addEmailServiceBtn.addEventListener('click', () => {
addServiceModal.classList.add('active');
loadServiceConfigFields(serviceTypeSelect.value);
elements.addEmailServiceBtn.addEventListener('click', () => {
elements.addServiceModal.classList.add('active');
loadServiceConfigFields(elements.serviceType.value);
});
closeServiceModalBtn.addEventListener('click', () => {
addServiceModal.classList.remove('active');
elements.closeServiceModal.addEventListener('click', () => {
elements.addServiceModal.classList.remove('active');
});
cancelAddServiceBtn.addEventListener('click', () => {
addServiceModal.classList.remove('active');
elements.cancelAddService.addEventListener('click', () => {
elements.addServiceModal.classList.remove('active');
});
addServiceModal.addEventListener('click', (e) => {
if (e.target === addServiceModal) {
addServiceModal.classList.remove('active');
elements.addServiceModal.addEventListener('click', (e) => {
if (e.target === elements.addServiceModal) {
elements.addServiceModal.classList.remove('active');
}
});
// 服务类型切换
serviceTypeSelect.addEventListener('change', (e) => {
elements.serviceType.addEventListener('change', (e) => {
loadServiceConfigFields(e.target.value);
});
// 添加服务表单
addServiceForm.addEventListener('submit', handleAddService);
elements.addServiceForm.addEventListener('submit', handleAddService);
// Outlook 批量导入展开/折叠
if (toggleImportBtn) {
toggleImportBtn.addEventListener('click', () => {
const isHidden = outlookImportBody.style.display === 'none';
outlookImportBody.style.display = isHidden ? 'block' : 'none';
toggleImportBtn.textContent = isHidden ? '收起' : '展开';
if (elements.toggleImportBtn) {
elements.toggleImportBtn.addEventListener('click', () => {
const isHidden = elements.outlookImportBody.style.display === 'none';
elements.outlookImportBody.style.display = isHidden ? 'block' : 'none';
elements.toggleImportBtn.textContent = isHidden ? '收起' : '展开';
});
}
// Outlook 批量导入
if (outlookImportBtn) {
outlookImportBtn.addEventListener('click', handleOutlookBatchImport);
if (elements.outlookImportBtn) {
elements.outlookImportBtn.addEventListener('click', handleOutlookBatchImport);
}
// 清空导入数据
if (clearImportBtn) {
clearImportBtn.addEventListener('click', () => {
outlookImportData.value = '';
importResult.style.display = 'none';
if (elements.clearImportBtn) {
elements.clearImportBtn.addEventListener('click', () => {
elements.outlookImportData.value = '';
elements.importResult.style.display = 'none';
});
}
// 全选/取消全选
if (selectAllCheckbox) {
selectAllCheckbox.addEventListener('change', (e) => {
if (elements.selectAllServices) {
elements.selectAllServices.addEventListener('change', (e) => {
const checkboxes = document.querySelectorAll('.service-checkbox');
checkboxes.forEach(cb => cb.checked = e.target.checked);
updateSelectedServices();
});
}
// 批量删除
if (batchDeleteBtn) {
batchDeleteBtn.addEventListener('click', handleBatchDelete);
}
}
// 加载设置
async function loadSettings() {
try {
const response = await fetch(`${API_BASE}/settings`);
const data = await response.json();
const data = await api.get('/settings');
// 代理设置
document.getElementById('proxy-enabled').checked = data.proxy?.enabled || false;
@@ -165,61 +156,88 @@ async function loadSettings() {
} catch (error) {
console.error('加载设置失败:', error);
toast.error('加载设置失败');
}
}
// 加载邮箱服务
async function loadEmailServices() {
try {
const response = await fetch(`${API_BASE}/email-services`);
const data = await response.json();
const data = await api.get('/email-services');
renderEmailServices(data.services);
} catch (error) {
console.error('加载邮箱服务失败:', error);
elements.emailServicesTable.innerHTML = `
<tr>
<td colspan="7">
<div class="empty-state">
<div class="empty-state-icon">❌</div>
<div class="empty-state-title">加载失败</div>
</div>
</td>
</tr>
`;
}
}
// 渲染邮箱服务
function renderEmailServices(services) {
if (services.length === 0) {
emailServicesTable.innerHTML = '<tr><td colspan="7" style="text-align: center;">暂无配置</td></tr>';
batchDeleteBtn.style.display = 'none';
elements.emailServicesTable.innerHTML = `
<tr>
<td colspan="7">
<div class="empty-state">
<div class="empty-state-icon">📭</div>
<div class="empty-state-title">暂无配置</div>
<div class="empty-state-description">点击上方"添加服务"按钮添加邮箱服务</div>
</div>
</td>
</tr>
`;
return;
}
emailServicesTable.innerHTML = services.map(service => `
elements.emailServicesTable.innerHTML = services.map(service => `
<tr data-service-id="${service.id}">
<td><input type="checkbox" class="service-checkbox" data-id="${service.id}" onchange="updateSelectedServices()"></td>
<td>
<input type="checkbox" class="service-checkbox" data-id="${service.id}"
onchange="updateSelectedServices()">
</td>
<td>${escapeHtml(service.name)}</td>
<td>${getServiceTypeText(service.service_type)}</td>
<td><span class="status-badge ${service.enabled ? 'completed' : ''}">${service.enabled ? '已启用' : '已禁用'}</span></td>
<td>${service.priority}</td>
<td>${formatDate(service.last_used)}</td>
<td>
<button class="btn btn-sm btn-secondary" onclick="testService(${service.id})">测试</button>
<button class="btn btn-sm ${service.enabled ? 'btn-warning' : 'btn-primary'}" onclick="toggleService(${service.id}, ${!service.enabled})">
${service.enabled ? '禁用' : '启用'}
</button>
<button class="btn btn-sm btn-danger" onclick="deleteService(${service.id})">删除</button>
<span class="status-badge ${service.enabled ? 'active' : 'disabled'}">
${service.enabled ? '已启用' : '已禁用'}
</span>
</td>
<td>${service.priority}</td>
<td>${format.date(service.last_used)}</td>
<td>
<div class="action-buttons">
<button class="btn btn-ghost btn-sm" onclick="testService(${service.id})" title="测试">
🔌
</button>
<button class="btn btn-ghost btn-sm" onclick="toggleService(${service.id}, ${!service.enabled})" title="${service.enabled ? '禁用' : '启用'}">
${service.enabled ? '🔒' : '🔓'}
</button>
<button class="btn btn-ghost btn-sm" onclick="deleteService(${service.id})" title="删除">
🗑️
</button>
</div>
</td>
</tr>
`).join('');
// 更新批量删除按钮状态
updateSelectedServices();
}
// 加载数据库信息
async function loadDatabaseInfo() {
try {
const response = await fetch(`${API_BASE}/settings/database`);
const data = await response.json();
const data = await api.get('/settings/database');
document.getElementById('db-size').textContent = `${data.database_size_mb} MB`;
document.getElementById('db-accounts').textContent = data.accounts_count;
document.getElementById('db-services').textContent = data.email_services_count;
document.getElementById('db-tasks').textContent = data.tasks_count;
document.getElementById('db-accounts').textContent = format.number(data.accounts_count);
document.getElementById('db-services').textContent = format.number(data.email_services_count);
document.getElementById('db-tasks').textContent = format.number(data.tasks_count);
} catch (error) {
console.error('加载数据库信息失败:', error);
@@ -240,38 +258,25 @@ async function handleSaveProxy(e) {
};
try {
const response = await fetch(`${API_BASE}/settings/proxy`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (response.ok) {
alert('代理设置已保存');
} else {
const result = await response.json();
alert('保存失败: ' + (result.detail || '未知错误'));
}
await api.post('/settings/proxy', data);
toast.success('代理设置已保存');
} catch (error) {
alert('保存失败: ' + error.message);
toast.error('保存失败: ' + error.message);
}
}
// 测试代理
async function handleTestProxy() {
testProxyBtn.disabled = true;
testProxyBtn.textContent = '测试中...';
elements.testProxyBtn.disabled = true;
elements.testProxyBtn.innerHTML = '<span class="loading-spinner"></span> 测试中...';
try {
// 这里应该调用一个测试代理的 API
// 暂时模拟
await new Promise(resolve => setTimeout(resolve, 1000));
alert('代理测试功能待实现');
// TODO: 实现代理测试 API
await new Promise(resolve => setTimeout(resolve, 1500));
toast.info('代理测试功能待实现');
} finally {
testProxyBtn.disabled = false;
testProxyBtn.textContent = '测试连接';
elements.testProxyBtn.disabled = false;
elements.testProxyBtn.textContent = '🔌 测试连接';
}
}
@@ -288,96 +293,68 @@ async function handleSaveRegistration(e) {
};
try {
const response = await fetch(`${API_BASE}/settings/registration`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (response.ok) {
alert('注册配置已保存');
} else {
const result = await response.json();
alert('保存失败: ' + (result.detail || '未知错误'));
}
await api.post('/settings/registration', data);
toast.success('注册配置已保存');
} catch (error) {
alert('保存失败: ' + error.message);
toast.error('保存失败: ' + error.message);
}
}
// 备份数据库
async function handleBackup() {
backupBtn.disabled = true;
backupBtn.textContent = '备份中...';
elements.backupBtn.disabled = true;
elements.backupBtn.innerHTML = '<span class="loading-spinner"></span> 备份中...';
try {
const response = await fetch(`${API_BASE}/settings/database/backup`, {
method: 'POST',
});
const data = await response.json();
if (response.ok) {
alert(`备份成功: ${data.backup_path}`);
} else {
alert('备份失败: ' + (data.detail || '未知错误'));
}
const data = await api.post('/settings/database/backup');
toast.success(`备份成功: ${data.backup_path}`);
} catch (error) {
alert('备份失败: ' + error.message);
toast.error('备份失败: ' + error.message);
} finally {
backupBtn.disabled = false;
backupBtn.textContent = '备份数据库';
elements.backupBtn.disabled = false;
elements.backupBtn.textContent = '💾 备份数据库';
}
}
// 清理数据
async function handleCleanup() {
if (!confirm('确定要清理过期数据吗?此操作不可恢复。')) {
return;
}
const confirmed = await confirm('确定要清理过期数据吗?此操作不可恢复。');
if (!confirmed) return;
cleanupBtn.disabled = true;
cleanupBtn.textContent = '清理中...';
elements.cleanupBtn.disabled = true;
elements.cleanupBtn.innerHTML = '<span class="loading-spinner"></span> 清理中...';
try {
const response = await fetch(`${API_BASE}/settings/database/cleanup?days=30`, {
method: 'POST',
});
const data = await response.json();
if (response.ok) {
alert(data.message);
loadDatabaseInfo();
} else {
alert('清理失败: ' + (data.detail || '未知错误'));
}
const data = await api.post('/settings/database/cleanup?days=30');
toast.success(data.message);
loadDatabaseInfo();
} catch (error) {
alert('清理失败: ' + error.message);
toast.error('清理失败: ' + error.message);
} finally {
cleanupBtn.disabled = false;
cleanupBtn.textContent = '清理过期数据';
elements.cleanupBtn.disabled = false;
elements.cleanupBtn.textContent = '🧹 清理过期数据';
}
}
// 加载服务配置字段
async function loadServiceConfigFields(serviceType) {
try {
const response = await fetch(`${API_BASE}/email-services/types`);
const data = await response.json();
const data = await api.get('/email-services/types');
const typeInfo = data.types.find(t => t.value === serviceType);
if (!typeInfo) return;
serviceConfigFields.innerHTML = typeInfo.config_fields.map(field => `
if (!typeInfo) {
elements.serviceConfigFields.innerHTML = '';
return;
}
elements.serviceConfigFields.innerHTML = typeInfo.config_fields.map(field => `
<div class="form-group">
<label for="config-${field.name}">${field.label}</label>
<input type="${field.name.includes('password') || field.name.includes('token') ? 'password' : 'text'}"
id="config-${field.name}"
name="${field.name}"
value="${field.default || ''}"
placeholder="${field.label}"
${field.required ? 'required' : ''}>
</div>
`).join('');
@@ -391,10 +368,10 @@ async function loadServiceConfigFields(serviceType) {
async function handleAddService(e) {
e.preventDefault();
const formData = new FormData(addServiceForm);
const formData = new FormData(elements.addServiceForm);
const config = {};
serviceConfigFields.querySelectorAll('input').forEach(input => {
elements.serviceConfigFields.querySelectorAll('input').forEach(input => {
config[input.name] = input.value;
});
@@ -407,44 +384,27 @@ async function handleAddService(e) {
};
try {
const response = await fetch(`${API_BASE}/email-services`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (response.ok) {
addServiceModal.classList.remove('active');
addServiceForm.reset();
loadEmailServices();
alert('邮箱服务已添加');
} else {
const result = await response.json();
alert('添加失败: ' + (result.detail || '未知错误'));
}
await api.post('/email-services', data);
toast.success('邮箱服务已添加');
elements.addServiceModal.classList.remove('active');
elements.addServiceForm.reset();
loadEmailServices();
} catch (error) {
alert('添加失败: ' + error.message);
toast.error('添加失败: ' + error.message);
}
}
// 测试服务
async function testService(id) {
try {
const response = await fetch(`${API_BASE}/email-services/${id}/test`, {
method: 'POST',
});
const data = await response.json();
const data = await api.post(`/email-services/${id}/test`);
if (data.success) {
alert('服务连接正常');
toast.success('服务连接正常');
} else {
alert('服务连接失败: ' + data.message);
toast.warning('服务连接失败: ' + data.message);
}
} catch (error) {
alert('测试失败: ' + error.message);
toast.error('测试失败: ' + error.message);
}
}
@@ -452,62 +412,134 @@ async function testService(id) {
async function toggleService(id, enabled) {
try {
const endpoint = enabled ? 'enable' : 'disable';
const response = await fetch(`${API_BASE}/email-services/${id}/${endpoint}`, {
method: 'POST',
});
if (response.ok) {
loadEmailServices();
} else {
const data = await response.json();
alert('操作失败: ' + (data.detail || '未知错误'));
}
await api.post(`/email-services/${id}/${endpoint}`);
toast.success(enabled ? '服务已启用' : '服务已禁用');
loadEmailServices();
} catch (error) {
alert('操作失败: ' + error.message);
toast.error('操作失败: ' + error.message);
}
}
// 删除服务
async function deleteService(id) {
if (!confirm('确定要删除此邮箱服务配置吗?')) {
return;
}
const confirmed = await confirm('确定要删除此邮箱服务配置吗?');
if (!confirmed) return;
try {
const response = await fetch(`${API_BASE}/email-services/${id}`, {
method: 'DELETE',
});
if (response.ok) {
loadEmailServices();
} else {
const data = await response.json();
alert('删除失败: ' + (data.detail || '未知错误'));
}
await api.delete(`/email-services/${id}`);
toast.success('服务已删除');
loadEmailServices();
} catch (error) {
alert('删除失败: ' + error.message);
toast.error('删除失败: ' + error.message);
}
}
// 工具函数
// 更新选中的服务
function updateSelectedServices() {
selectedServiceIds.clear();
document.querySelectorAll('.service-checkbox:checked').forEach(cb => {
selectedServiceIds.add(parseInt(cb.dataset.id));
});
}
// Outlook 批量导入
async function handleOutlookBatchImport() {
const data = elements.outlookImportData.value.trim();
if (!data) {
toast.warning('请输入要导入的数据');
return;
}
const enabled = document.getElementById('outlook-import-enabled').checked;
const priority = parseInt(document.getElementById('outlook-import-priority').value) || 0;
// 解析数据
const lines = data.split('\n').filter(line => line.trim() && !line.trim().startsWith('#'));
const accounts = [];
const errors = [];
lines.forEach((line, index) => {
const parts = line.split('----').map(p => p.trim());
if (parts.length < 2) {
errors.push(`${index + 1} 行格式错误`);
return;
}
const account = {
email: parts[0],
password: parts[1],
client_id: parts[2] || null,
refresh_token: parts[3] || null,
enabled: enabled,
priority: priority
};
if (!account.email.includes('@')) {
errors.push(`${index + 1} 行邮箱格式错误: ${account.email}`);
return;
}
accounts.push(account);
});
if (errors.length > 0) {
elements.importResult.style.display = 'block';
elements.importResult.innerHTML = `
<div class="import-errors">${errors.map(e => `<div>${e}</div>`).join('')}</div>
`;
return;
}
elements.outlookImportBtn.disabled = true;
elements.outlookImportBtn.innerHTML = '<span class="loading-spinner"></span> 导入中...';
let successCount = 0;
let failCount = 0;
try {
for (const account of accounts) {
try {
await api.post('/email-services', {
service_type: 'outlook',
name: account.email,
config: {
email: account.email,
password: account.password,
client_id: account.client_id,
refresh_token: account.refresh_token
},
enabled: account.enabled,
priority: account.priority
});
successCount++;
} catch {
failCount++;
}
}
elements.importResult.style.display = 'block';
elements.importResult.innerHTML = `
<div class="import-stats">
<span>✅ 成功: ${successCount}</span>
<span>❌ 失败: ${failCount}</span>
</div>
`;
toast.success(`导入完成,成功 ${successCount}`);
loadEmailServices();
} catch (error) {
toast.error('导入失败: ' + error.message);
} finally {
elements.outlookImportBtn.disabled = false;
elements.outlookImportBtn.textContent = '📥 开始导入';
}
}
// HTML 转义
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function getServiceTypeText(type) {
const typeMap = {
'tempmail': 'Tempmail.lol',
'outlook': 'Outlook',
'custom_domain': '自定义域名',
};
return typeMap[type] || type;
}
function formatDate(dateStr) {
if (!dateStr) return '-';
const date = new Date(dateStr);
return date.toLocaleString('zh-CN');
}

View File

@@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>账号管理 - OpenAI 注册系统</title>
<link rel="stylesheet" href="/static/css/style.css">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📋</text></svg>">
</head>
<body>
<div class="container">
@@ -18,6 +19,9 @@
<a href="/accounts" class="nav-link active">账号管理</a>
<a href="/settings" class="nav-link">设置</a>
</div>
<button class="theme-toggle" onclick="theme.toggle()" title="切换主题">
🌙
</button>
</nav>
<!-- 主内容区 -->
@@ -66,15 +70,19 @@
<option value="custom_domain">自定义域名</option>
</select>
<input type="text" id="search-input" class="form-input" placeholder="搜索邮箱...">
<input type="text" id="search-input" class="form-input" placeholder="🔍 搜索邮箱..." style="min-width: 200px;">
</div>
<div class="toolbar-right">
<button class="btn btn-secondary" id="refresh-btn">刷新</button>
<button class="btn btn-danger" id="batch-delete-btn" disabled>批量删除</button>
<button class="btn btn-ghost" id="refresh-btn" title="刷新">
🔄 刷新
</button>
<button class="btn btn-danger" id="batch-delete-btn" disabled>
🗑️ 批量删除
</button>
<div class="dropdown">
<button class="btn btn-primary dropdown-toggle" id="export-btn">
导出
<button class="btn btn-primary" id="export-btn">
📥 导出
</button>
<div class="dropdown-menu" id="export-menu">
<a href="#" class="dropdown-item" data-format="json">导出 JSON</a>
@@ -87,29 +95,43 @@
<!-- 账号列表 -->
<div class="card">
<div class="card-body">
<table class="data-table">
<thead>
<tr>
<th><input type="checkbox" id="select-all"></th>
<th>ID</th>
<th>邮箱</th>
<th>邮箱服务</th>
<th>状态</th>
<th>注册时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="accounts-table">
<!-- 动态加载 -->
</tbody>
</table>
<div class="card-body" style="padding: 0;">
<div class="table-container">
<table class="data-table">
<thead>
<tr>
<th style="width: 40px;"><input type="checkbox" id="select-all"></th>
<th style="width: 60px;">ID</th>
<th>邮箱</th>
<th style="width: 120px;">邮箱服务</th>
<th style="width: 100px;">状态</th>
<th style="width: 160px;">注册时间</th>
<th style="width: 140px;">操作</th>
</tr>
</thead>
<tbody id="accounts-table">
<tr>
<td colspan="7">
<div class="empty-state">
<div class="skeleton skeleton-text" style="width: 60%;"></div>
<div class="skeleton skeleton-text" style="width: 80%;"></div>
<div class="skeleton skeleton-text" style="width: 40%;"></div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页 -->
<div class="pagination" id="pagination">
<button class="btn btn-sm" id="prev-page" disabled>上一页</button>
<span id="page-info">第 1 页</span>
<button class="btn btn-sm" id="next-page">下一页</button>
<button class="btn btn-secondary btn-sm" id="prev-page" disabled>
← 上一页
</button>
<span id="page-info">第 1 页 / 共 1 页</span>
<button class="btn btn-secondary btn-sm" id="next-page">
下一页 →
</button>
</div>
</div>
</div>
@@ -120,7 +142,7 @@
<div class="modal" id="detail-modal">
<div class="modal-content">
<div class="modal-header">
<h3>账号详情</h3>
<h3>📋 账号详情</h3>
<button class="modal-close" id="close-modal">&times;</button>
</div>
<div class="modal-body" id="modal-body">
@@ -129,6 +151,7 @@
</div>
</div>
<script src="/static/js/utils.js"></script>
<script src="/static/js/accounts.js"></script>
</body>
</html>

View File

@@ -3,8 +3,9 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OpenAI/Codex CLI 自动注册系统</title>
<title>注册控制台 - OpenAI 注册系统</title>
<link rel="stylesheet" href="/static/css/style.css">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🚀</text></svg>">
</head>
<body>
<div class="container">
@@ -18,6 +19,9 @@
<a href="/accounts" class="nav-link">账号管理</a>
<a href="/settings" class="nav-link">设置</a>
</div>
<button class="theme-toggle" onclick="theme.toggle()" title="切换主题">
🌙
</button>
</nav>
<!-- 主内容区 -->
@@ -30,7 +34,7 @@
<!-- 注册表单 -->
<div class="card">
<div class="card-header">
<h3>新建注册任务</h3>
<h3>📝 新建注册任务</h3>
</div>
<div class="card-body">
<form id="registration-form">
@@ -80,8 +84,8 @@
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary" id="start-btn">
开始注册
<button type="submit" class="btn btn-primary btn-lg" id="start-btn">
🚀 开始注册
</button>
<button type="button" class="btn btn-secondary" id="cancel-btn" disabled>
取消任务
@@ -94,7 +98,7 @@
<!-- 批量任务状态 -->
<div class="card" id="batch-status-card" style="display: none;">
<div class="card-header">
<h3>批量任务进度</h3>
<h3>📊 批量任务进度</h3>
<span id="batch-progress" class="status-badge">0/0</span>
</div>
<div class="card-body">
@@ -102,9 +106,9 @@
<div id="progress-bar" class="progress-bar" style="width: 0%"></div>
</div>
<div class="batch-stats">
<span>成功: <strong id="batch-success">0</strong></span>
<span>失败: <strong id="batch-failed">0</strong></span>
<span>剩余: <strong id="batch-remaining">0</strong></span>
<span>成功: <strong id="batch-success">0</strong></span>
<span>失败: <strong id="batch-failed">0</strong></span>
<span>剩余: <strong id="batch-remaining">0</strong></span>
</div>
</div>
</div>
@@ -112,23 +116,27 @@
<!-- 任务状态 -->
<div class="card" id="task-status-card" style="display: none;">
<div class="card-header">
<h3>任务状态</h3>
<span id="task-status-badge" class="status-badge">等待中</span>
<h3>任务状态</h3>
<span id="task-status-badge" class="status-badge pending">等待中</span>
</div>
<div class="card-body">
<div class="task-info">
<div class="info-row">
<span class="label">任务 ID:</span>
<div class="info-grid">
<div class="info-item">
<span class="label">任务 ID</span>
<span id="task-id" class="value">-</span>
</div>
<div class="info-row">
<span class="label">邮箱:</span>
<div class="info-item">
<span class="label">邮箱</span>
<span id="task-email" class="value">-</span>
</div>
<div class="info-row">
<span class="label">状态:</span>
<div class="info-item">
<span class="label">状态</span>
<span id="task-status" class="value">-</span>
</div>
<div class="info-item">
<span class="label">邮箱服务</span>
<span id="task-service" class="value">-</span>
</div>
</div>
</div>
</div>
@@ -136,18 +144,19 @@
<!-- 控制台日志 -->
<div class="card">
<div class="card-header">
<h3>控制台日志</h3>
<button class="btn btn-sm" id="clear-log-btn">清空</button>
<h3>💻 控制台日志</h3>
<button class="btn btn-ghost btn-sm" id="clear-log-btn">清空</button>
</div>
<div class="card-body">
<div class="card-body" style="padding: 0;">
<div id="console-log" class="console-log">
<div class="log-line info">[*] 系统就绪,等待开始注册...</div>
<div class="log-line info">[系统] 准备就绪,等待开始注册...</div>
</div>
</div>
</div>
</main>
</div>
<script src="/static/js/utils.js"></script>
<script src="/static/js/app.js"></script>
</body>
</html>

View File

@@ -3,8 +3,9 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>设置 - OpenAI 注册系统</title>
<title>系统设置 - OpenAI 注册系统</title>
<link rel="stylesheet" href="/static/css/style.css">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⚙️</text></svg>">
</head>
<body>
<div class="container">
@@ -18,6 +19,9 @@
<a href="/accounts" class="nav-link">账号管理</a>
<a href="/settings" class="nav-link active">设置</a>
</div>
<button class="theme-toggle" onclick="theme.toggle()" title="切换主题">
🌙
</button>
</nav>
<!-- 主内容区 -->
@@ -29,10 +33,10 @@
<!-- 设置标签页 -->
<div class="tabs">
<button class="tab-btn active" data-tab="proxy">代理设置</button>
<button class="tab-btn" data-tab="email">邮箱服务</button>
<button class="tab-btn" data-tab="registration">注册配置</button>
<button class="tab-btn" data-tab="database">数据库</button>
<button class="tab-btn active" data-tab="proxy">🌐 代理设置</button>
<button class="tab-btn" data-tab="email">📧 邮箱服务</button>
<button class="tab-btn" data-tab="registration">⚙️ 注册配置</button>
<button class="tab-btn" data-tab="database">💾 数据库</button>
</div>
<!-- 代理设置 -->
@@ -73,18 +77,18 @@
<div class="form-row">
<div class="form-group">
<label for="proxy-username">用户名 (可选)</label>
<input type="text" id="proxy-username" name="username">
<input type="text" id="proxy-username" name="username" autocomplete="off">
</div>
<div class="form-group">
<label for="proxy-password">密码 (可选)</label>
<input type="password" id="proxy-password" name="password">
<input type="password" id="proxy-password" name="password" autocomplete="new-password">
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">保存设置</button>
<button type="button" class="btn btn-secondary" id="test-proxy-btn">测试连接</button>
<button type="submit" class="btn btn-primary">💾 保存设置</button>
<button type="button" class="btn btn-secondary" id="test-proxy-btn">🔌 测试连接</button>
</div>
</form>
</div>
@@ -96,8 +100,8 @@
<!-- Outlook 批量导入 -->
<div class="card" id="outlook-import-card">
<div class="card-header">
<h3>Outlook 批量导入</h3>
<button class="btn btn-sm btn-secondary" id="toggle-import-btn">展开</button>
<h3>📥 Outlook 批量导入</h3>
<button class="btn btn-ghost btn-sm" id="toggle-import-btn">展开</button>
</div>
<div class="card-body" id="outlook-import-body" style="display: none;">
<div class="import-info">
@@ -125,41 +129,44 @@
</div>
</div>
<div class="form-actions">
<button type="button" class="btn btn-primary" id="outlook-import-btn">开始导入</button>
<button type="button" class="btn btn-primary" id="outlook-import-btn">📥 开始导入</button>
<button type="button" class="btn btn-secondary" id="clear-import-btn">清空</button>
</div>
<div id="import-result" style="display: none; margin-top: 16px;">
<div class="import-stats"></div>
<div class="import-errors" style="margin-top: 8px;"></div>
</div>
<div id="import-result" style="display: none; margin-top: var(--spacing-md);"></div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3>邮箱服务配置</h3>
<div class="toolbar-left">
<button class="btn btn-sm btn-primary" id="add-email-service-btn">添加服务</button>
<button class="btn btn-sm btn-danger" id="batch-delete-btn" style="display: none;">批量删除</button>
</div>
<button class="btn btn-primary btn-sm" id="add-email-service-btn"> 添加服务</button>
</div>
<div class="card-body">
<table class="data-table">
<thead>
<tr>
<th><input type="checkbox" id="select-all-services"></th>
<th>名称</th>
<th>类型</th>
<th>状态</th>
<th>优先级</th>
<th>最后使用</th>
<th>操作</th>
</tr>
</thead>
<tbody id="email-services-table">
<!-- 动态加载 -->
</tbody>
</table>
<div class="card-body" style="padding: 0;">
<div class="table-container">
<table class="data-table">
<thead>
<tr>
<th style="width: 40px;"><input type="checkbox" id="select-all-services"></th>
<th>名称</th>
<th style="width: 120px;">类型</th>
<th style="width: 100px;">状态</th>
<th style="width: 80px;">优先级</th>
<th style="width: 160px;">最后使用</th>
<th style="width: 180px;">操作</th>
</tr>
</thead>
<tbody id="email-services-table">
<tr>
<td colspan="7">
<div class="empty-state">
<div class="skeleton skeleton-text"></div>
<div class="skeleton skeleton-text" style="width: 80%;"></div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
@@ -171,7 +178,7 @@
<h3>注册配置</h3>
</div>
<div class="card-body">
<form id="registration-form">
<form id="registration-settings-form">
<div class="form-row">
<div class="form-group">
<label for="max-retries">最大重试次数</label>
@@ -202,7 +209,7 @@
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">保存设置</button>
<button type="submit" class="btn btn-primary">💾 保存设置</button>
</div>
</form>
</div>
@@ -216,28 +223,28 @@
<h3>数据库信息</h3>
</div>
<div class="card-body">
<div class="info-grid">
<div class="info-grid" style="margin-bottom: var(--spacing-lg);">
<div class="info-item">
<span class="label">数据库大小:</span>
<span class="label">数据库大小</span>
<span id="db-size" class="value">-</span>
</div>
<div class="info-item">
<span class="label">账号数量:</span>
<span class="label">账号数量</span>
<span id="db-accounts" class="value">-</span>
</div>
<div class="info-item">
<span class="label">邮箱服务数量:</span>
<span class="label">邮箱服务数量</span>
<span id="db-services" class="value">-</span>
</div>
<div class="info-item">
<span class="label">任务记录数量:</span>
<span class="label">任务记录数量</span>
<span id="db-tasks" class="value">-</span>
</div>
</div>
<div class="form-actions">
<button class="btn btn-secondary" id="backup-btn">备份数据库</button>
<button class="btn btn-warning" id="cleanup-btn">清理过期数据</button>
<button class="btn btn-secondary" id="backup-btn">💾 备份数据库</button>
<button class="btn btn-warning" id="cleanup-btn">🧹 清理过期数据</button>
</div>
</div>
</div>
@@ -249,7 +256,7 @@
<div class="modal" id="add-service-modal">
<div class="modal-content">
<div class="modal-header">
<h3>添加邮箱服务</h3>
<h3> 添加邮箱服务</h3>
<button class="modal-close" id="close-service-modal">&times;</button>
</div>
<div class="modal-body">
@@ -265,7 +272,7 @@
<div class="form-group">
<label for="service-name">服务名称</label>
<input type="text" id="service-name" name="name" required>
<input type="text" id="service-name" name="name" required placeholder="例如:我的 Outlook 账号">
</div>
<div id="service-config-fields">
@@ -281,6 +288,7 @@
</div>
</div>
<script src="/static/js/utils.js"></script>
<script src="/static/js/settings.js"></script>
</body>
</html>