diff --git a/src/config/settings.py b/src/config/settings.py index ae37520..e875eb2 100644 --- a/src/config/settings.py +++ b/src/config/settings.py @@ -119,6 +119,11 @@ class Settings(BaseSettings): default=SecretStr("your-encryption-key-change-in-production") ) + # CPA 上传配置 + cpa_enabled: bool = Field(default=False) + cpa_api_url: str = Field(default="") # 例如: https://cpa.example.com + cpa_api_token: SecretStr = Field(default=SecretStr("")) + # 全局配置实例 _settings: Optional[Settings] = None diff --git a/src/web/routes/settings.py b/src/web/routes/settings.py index b3b9242..329d1c4 100644 --- a/src/web/routes/settings.py +++ b/src/web/routes/settings.py @@ -624,3 +624,74 @@ async def disable_proxy(proxy_id: int): if not proxy: raise HTTPException(status_code=404, detail="代理不存在") return {"success": True, "message": "代理已禁用"} + + +# ============== CPA 设置 ============== + +class CPASettings(BaseModel): + """CPA 设置""" + enabled: bool = False + api_url: str = "" + api_token: str = "" + + +class CPATestRequest(BaseModel): + """CPA 测试请求""" + api_url: str + api_token: str + + +@router.get("/cpa") +async def get_cpa_settings(): + """获取 CPA 设置""" + settings = get_settings() + + return { + "enabled": settings.cpa_enabled, + "api_url": settings.cpa_api_url, + "has_token": bool(settings.cpa_api_token and settings.cpa_api_token.get_secret_value()), + } + + +@router.post("/cpa") +async def update_cpa_settings(request: CPASettings): + """更新 CPA 设置""" + update_dict = { + "cpa_enabled": request.enabled, + "cpa_api_url": request.api_url, + } + + # 只有提供了 token 才更新 + if request.api_token: + update_dict["cpa_api_token"] = request.api_token + + update_settings(**update_dict) + + return {"success": True, "message": "CPA 设置已更新"} + + +@router.post("/cpa/test") +async def test_cpa_connection(request: CPATestRequest): + """测试 CPA 连接""" + from ...core.cpa_upload import test_cpa_connection as do_test + + settings = get_settings() + proxy = settings.proxy_url + + # 如果传入 'use_saved_token',使用已保存的 token + api_token = request.api_token + if api_token == 'use_saved_token' or not api_token: + if settings.cpa_api_token: + api_token = settings.cpa_api_token.get_secret_value() + else: + return { + "success": False, + "message": "未配置 API Token" + } + + success, message = do_test(request.api_url, api_token, proxy) + + return { + "success": success, + "message": message + } diff --git a/static/css/style.css b/static/css/style.css index 2f711e7..b6e7818 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -280,6 +280,15 @@ body { padding: var(--spacing-lg); } +/* 工具栏卡片允许下拉菜单溢出 */ +.card.toolbar-card { + overflow: visible; +} + +.card-body.toolbar { + overflow: visible; +} + /* ============================================ 表单元素 ============================================ */ @@ -710,6 +719,7 @@ body { align-items: center; flex-wrap: wrap; gap: var(--spacing-md); + overflow: visible; } .toolbar-left, diff --git a/static/js/accounts.js b/static/js/accounts.js index 2373b14..edfd81c 100644 --- a/static/js/accounts.js +++ b/static/js/accounts.js @@ -23,6 +23,7 @@ const elements = { refreshBtn: document.getElementById('refresh-btn'), batchRefreshBtn: document.getElementById('batch-refresh-btn'), batchValidateBtn: document.getElementById('batch-validate-btn'), + batchUploadCpaBtn: document.getElementById('batch-upload-cpa-btn'), batchDeleteBtn: document.getElementById('batch-delete-btn'), exportBtn: document.getElementById('export-btn'), exportMenu: document.getElementById('export-menu'), @@ -40,6 +41,7 @@ document.addEventListener('DOMContentLoaded', () => { loadStats(); loadAccounts(); initEventListeners(); + updateBatchButtons(); // 初始化按钮状态 }); // 事件监听 @@ -83,6 +85,9 @@ function initEventListeners() { // 批量验证Token elements.batchValidateBtn.addEventListener('click', handleBatchValidate); + // 批量上传CPA + elements.batchUploadCpaBtn.addEventListener('click', handleBatchUploadCpa); + // 批量删除 elements.batchDeleteBtn.addEventListener('click', handleBatchDelete); @@ -181,7 +186,7 @@ async function loadAccounts() { // 显示加载状态 elements.table.innerHTML = ` - +
@@ -217,7 +222,7 @@ async function loadAccounts() { console.error('加载账号列表失败:', error); elements.table.innerHTML = ` - +
加载失败
@@ -236,7 +241,7 @@ function renderAccounts(accounts) { if (accounts.length === 0) { elements.table.innerHTML = ` - +
📭
暂无数据
@@ -271,12 +276,22 @@ function renderAccounts(accounts) { ${getStatusText('account', account.status)} + +
+ ${account.cpa_uploaded + ? `` + : `-`} +
+ ${format.date(account.last_refresh) || '-'}
+ @@ -335,10 +350,13 @@ function updateBatchButtons() { elements.batchDeleteBtn.disabled = count === 0; elements.batchRefreshBtn.disabled = count === 0; elements.batchValidateBtn.disabled = count === 0; + elements.batchUploadCpaBtn.disabled = count === 0; + elements.exportBtn.disabled = count === 0; elements.batchDeleteBtn.textContent = count > 0 ? `🗑️ 删除 (${count})` : '🗑️ 批量删除'; elements.batchRefreshBtn.textContent = count > 0 ? `🔄 刷新 (${count})` : '🔄 刷新Token'; elements.batchValidateBtn.textContent = count > 0 ? `✅ 验证 (${count})` : '✅ 验证Token'; + elements.batchUploadCpaBtn.textContent = count > 0 ? `☁️ 上传 (${count})` : '☁️ 上传CPA'; } // 刷新单个账号Token @@ -538,19 +556,57 @@ async function handleBatchDelete() { } // 导出账号 -function exportAccounts(format) { - const params = new URLSearchParams(); - - if (elements.filterStatus.value) { - params.append('status', elements.filterStatus.value); +async function exportAccounts(format) { + if (selectedAccounts.size === 0) { + toast.warning('请先选择要导出的账号'); + return; } - if (elements.filterService.value) { - params.append('email_service', elements.filterService.value); - } + toast.info(`正在导出 ${selectedAccounts.size} 个账号...`); - window.location.href = `/api/accounts/export/${format}?${params}`; - toast.info('正在导出...'); + try { + const response = await fetch('/api/accounts/export/' + format, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + ids: Array.from(selectedAccounts) + }) + }); + + if (!response.ok) { + throw new Error(`导出失败: HTTP ${response.status}`); + } + + // 获取文件内容 + const blob = await response.blob(); + + // 从 Content-Disposition 获取文件名 + const disposition = response.headers.get('Content-Disposition'); + let filename = `accounts_${Date.now()}.${format === 'cpa' ? 'json' : format}`; + if (disposition) { + const match = disposition.match(/filename=(.+)/); + if (match) { + filename = match[1]; + } + } + + // 创建下载链接 + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + a.remove(); + + toast.success('导出成功'); + } catch (error) { + console.error('导出失败:', error); + toast.error('导出失败: ' + error.message); + } } // HTML 转义 @@ -560,3 +616,52 @@ function escapeHtml(text) { div.textContent = text; return div.innerHTML; } + +// 上传单个账号到CPA +async function uploadToCpa(id) { + try { + toast.info('正在上传到CPA...'); + const result = await api.post(`/accounts/${id}/upload-cpa`); + + if (result.success) { + toast.success('上传成功'); + loadAccounts(); + } else { + toast.error('上传失败: ' + (result.error || '未知错误')); + } + } catch (error) { + toast.error('上传失败: ' + error.message); + } +} + +// 批量上传到CPA +async function handleBatchUploadCpa() { + if (selectedAccounts.size === 0) return; + + const confirmed = await confirm(`确定要将选中的 ${selectedAccounts.size} 个账号上传到CPA吗?`); + if (!confirmed) return; + + elements.batchUploadCpaBtn.disabled = true; + elements.batchUploadCpaBtn.textContent = '上传中...'; + + try { + const result = await api.post('/accounts/batch-upload-cpa', { + ids: Array.from(selectedAccounts) + }); + + let message = `成功: ${result.success_count}`; + if (result.failed_count > 0) { + message += `, 失败: ${result.failed_count}`; + } + if (result.skipped_count > 0) { + message += `, 跳过: ${result.skipped_count}`; + } + + toast.success(message); + loadAccounts(); + } catch (error) { + toast.error('批量上传失败: ' + error.message); + } finally { + updateBatchButtons(); + } +} diff --git a/static/js/settings.js b/static/js/settings.js index 338395c..4b50437 100644 --- a/static/js/settings.js +++ b/static/js/settings.js @@ -37,7 +37,10 @@ const elements = { proxyItemForm: document.getElementById('proxy-item-form'), closeProxyModal: document.getElementById('close-proxy-modal'), cancelProxyBtn: document.getElementById('cancel-proxy-btn'), - proxyModalTitle: document.getElementById('proxy-modal-title') + proxyModalTitle: document.getElementById('proxy-modal-title'), + // CPA 设置 + cpaForm: document.getElementById('cpa-form'), + testCpaBtn: document.getElementById('test-cpa-btn') }; // 选中的服务 ID @@ -194,6 +197,15 @@ function initEventListeners() { if (elements.proxyItemForm) { elements.proxyItemForm.addEventListener('submit', handleSaveProxyItem); } + + // CPA 设置 + if (elements.cpaForm) { + elements.cpaForm.addEventListener('submit', handleSaveCpa); + } + + if (elements.testCpaBtn) { + elements.testCpaBtn.addEventListener('click', handleTestCpa); + } } // 加载设置 @@ -215,6 +227,9 @@ async function loadSettings() { document.getElementById('sleep-min').value = data.registration?.sleep_min || 5; document.getElementById('sleep-max').value = data.registration?.sleep_max || 30; + // 加载 CPA 设置 + loadCpaSettings(); + } catch (error) { console.error('加载设置失败:', error); toast.error('加载设置失败'); @@ -823,3 +838,84 @@ async function handleTestAllProxies() { elements.testAllProxiesBtn.textContent = '🔌 测试全部'; } } + + +// ============================================================================ +// CPA 设置管理 +// ============================================================================ + +// 加载 CPA 设置 +async function loadCpaSettings() { + try { + const data = await api.get('/settings/cpa'); + + document.getElementById('cpa-enabled').checked = data.enabled || false; + document.getElementById('cpa-api-url').value = data.api_url || ''; + // 不填充 token,只显示是否有值 + document.getElementById('cpa-api-token').value = ''; + document.getElementById('cpa-api-token').placeholder = data.has_token ? '已配置,留空保持不变' : '请输入 API Token'; + + } catch (error) { + console.error('加载 CPA 设置失败:', error); + } +} + +// 保存 CPA 设置 +async function handleSaveCpa(e) { + e.preventDefault(); + + const data = { + enabled: document.getElementById('cpa-enabled').checked, + api_url: document.getElementById('cpa-api-url').value, + api_token: document.getElementById('cpa-api-token').value || '' + }; + + try { + await api.post('/settings/cpa', data); + toast.success('CPA 设置已保存'); + loadCpaSettings(); + } catch (error) { + toast.error('保存失败: ' + error.message); + } +} + +// 测试 CPA 连接 +async function handleTestCpa() { + const apiUrl = document.getElementById('cpa-api-url').value; + const apiToken = document.getElementById('cpa-api-token').value; + + if (!apiUrl) { + toast.warning('请输入 API URL'); + return; + } + + // 如果 token 为空,尝试使用已保存的 token 进行测试 + if (!apiToken) { + const cpaSettings = await api.get('/settings/cpa'); + if (!cpaSettings.has_token) { + toast.warning('请输入 API Token'); + return; + } + } + + elements.testCpaBtn.disabled = true; + elements.testCpaBtn.innerHTML = ' 测试中...'; + + try { + const result = await api.post('/settings/cpa/test', { + api_url: apiUrl, + api_token: apiToken || 'use_saved_token' + }); + + if (result.success) { + toast.success(result.message); + } else { + toast.error(result.message); + } + } catch (error) { + toast.error('测试失败: ' + error.message); + } finally { + elements.testCpaBtn.disabled = false; + elements.testCpaBtn.textContent = '🔌 测试连接'; + } +} diff --git a/templates/accounts.html b/templates/accounts.html index 683bc7b..1a625fa 100644 --- a/templates/accounts.html +++ b/templates/accounts.html @@ -32,6 +32,18 @@ .token-status .dot.healthy { background: var(--success-color); } .token-status .dot.warning { background: var(--warning-color); } .token-status .dot.expired { background: var(--danger-color); } + .cpa-status { + display: flex; + align-items: center; + justify-content: center; + } + .cpa-status .badge { + font-size: 0.7rem; + padding: 2px 6px; + border-radius: 10px; + } + .cpa-status .badge.uploaded { background: var(--success-color); color: white; } + .cpa-status .badge.pending { background: var(--border-color); color: var(--text-secondary); } @@ -80,7 +92,7 @@
-
+
+ 启用 CPA 上传 + +

启用后可在账号管理页面上传账号到 CPA 管理平台

+
+ +
+ + +

CPA 管理平台的 API 地址

+
+ +
+ + +

CPA 管理平台的认证 Token

+
+ +
+ + +
+ +
+
+
+