mirror of
https://github.com/cnlimiter/codex-register.git
synced 2026-05-07 04:12:43 +08:00
3
This commit is contained in:
@@ -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
|
||||
|
||||
1087
static/css/style.css
1087
static/css/style.css
File diff suppressed because it is too large
Load Diff
@@ -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');
|
||||
}
|
||||
|
||||
546
static/js/app.js
546
static/js/app.js
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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">×</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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">×</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>
|
||||
|
||||
Reference in New Issue
Block a user