mirror of
https://github.com/cnlimiter/codex-register.git
synced 2026-05-07 06:13:01 +08:00
Merge branch 'master' into master
This commit is contained in:
@@ -9,6 +9,8 @@ let pageSize = 20;
|
||||
let totalAccounts = 0;
|
||||
let selectedAccounts = new Set();
|
||||
let isLoading = false;
|
||||
let selectAllPages = false; // 是否选中了全部页
|
||||
let currentFilters = { status: '', email_service: '', search: '' }; // 当前筛选条件
|
||||
|
||||
// DOM 元素
|
||||
const elements = {
|
||||
@@ -24,6 +26,8 @@ const elements = {
|
||||
batchRefreshBtn: document.getElementById('batch-refresh-btn'),
|
||||
batchValidateBtn: document.getElementById('batch-validate-btn'),
|
||||
batchUploadCpaBtn: document.getElementById('batch-upload-cpa-btn'),
|
||||
batchCheckSubBtn: document.getElementById('batch-check-sub-btn'),
|
||||
batchUploadTmBtn: document.getElementById('batch-upload-tm-btn'),
|
||||
batchDeleteBtn: document.getElementById('batch-delete-btn'),
|
||||
exportBtn: document.getElementById('export-btn'),
|
||||
exportMenu: document.getElementById('export-menu'),
|
||||
@@ -42,6 +46,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
loadAccounts();
|
||||
initEventListeners();
|
||||
updateBatchButtons(); // 初始化按钮状态
|
||||
renderSelectAllBanner();
|
||||
});
|
||||
|
||||
// 事件监听
|
||||
@@ -49,17 +54,20 @@ function initEventListeners() {
|
||||
// 筛选
|
||||
elements.filterStatus.addEventListener('change', () => {
|
||||
currentPage = 1;
|
||||
resetSelectAllPages();
|
||||
loadAccounts();
|
||||
});
|
||||
|
||||
elements.filterService.addEventListener('change', () => {
|
||||
currentPage = 1;
|
||||
resetSelectAllPages();
|
||||
loadAccounts();
|
||||
});
|
||||
|
||||
// 搜索(防抖)
|
||||
elements.searchInput.addEventListener('input', debounce(() => {
|
||||
currentPage = 1;
|
||||
resetSelectAllPages();
|
||||
loadAccounts();
|
||||
}, 300));
|
||||
|
||||
@@ -68,6 +76,7 @@ function initEventListeners() {
|
||||
if (e.key === 'Escape') {
|
||||
elements.searchInput.blur();
|
||||
elements.searchInput.value = '';
|
||||
resetSelectAllPages();
|
||||
loadAccounts();
|
||||
}
|
||||
});
|
||||
@@ -88,10 +97,16 @@ function initEventListeners() {
|
||||
// 批量上传CPA
|
||||
elements.batchUploadCpaBtn.addEventListener('click', handleBatchUploadCpa);
|
||||
|
||||
// 批量检测订阅
|
||||
elements.batchCheckSubBtn.addEventListener('click', handleBatchCheckSubscription);
|
||||
|
||||
// 批量上传TM
|
||||
elements.batchUploadTmBtn.addEventListener('click', handleBatchUploadTm);
|
||||
|
||||
// 批量删除
|
||||
elements.batchDeleteBtn.addEventListener('click', handleBatchDelete);
|
||||
|
||||
// 全选
|
||||
// 全选(当前页)
|
||||
elements.selectAll.addEventListener('change', (e) => {
|
||||
const checkboxes = elements.table.querySelectorAll('input[type="checkbox"][data-id]');
|
||||
checkboxes.forEach(cb => {
|
||||
@@ -103,7 +118,11 @@ function initEventListeners() {
|
||||
selectedAccounts.delete(id);
|
||||
}
|
||||
});
|
||||
if (!e.target.checked) {
|
||||
selectAllPages = false;
|
||||
}
|
||||
updateBatchButtons();
|
||||
renderSelectAllBanner();
|
||||
});
|
||||
|
||||
// 分页
|
||||
@@ -196,21 +215,26 @@ async function loadAccounts() {
|
||||
</tr>
|
||||
`;
|
||||
|
||||
// 记录当前筛选条件
|
||||
currentFilters.status = elements.filterStatus.value;
|
||||
currentFilters.email_service = elements.filterService.value;
|
||||
currentFilters.search = elements.searchInput.value.trim();
|
||||
|
||||
const params = new URLSearchParams({
|
||||
page: currentPage,
|
||||
page_size: pageSize,
|
||||
});
|
||||
|
||||
if (elements.filterStatus.value) {
|
||||
params.append('status', elements.filterStatus.value);
|
||||
if (currentFilters.status) {
|
||||
params.append('status', currentFilters.status);
|
||||
}
|
||||
|
||||
if (elements.filterService.value) {
|
||||
params.append('email_service', elements.filterService.value);
|
||||
if (currentFilters.email_service) {
|
||||
params.append('email_service', currentFilters.email_service);
|
||||
}
|
||||
|
||||
if (elements.searchInput.value.trim()) {
|
||||
params.append('search', elements.searchInput.value.trim());
|
||||
if (currentFilters.search) {
|
||||
params.append('search', currentFilters.search);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -283,6 +307,13 @@ function renderAccounts(accounts) {
|
||||
: `<span class="badge pending">-</span>`}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="cpa-status">
|
||||
${account.subscription_type
|
||||
? `<span class="badge uploaded" title="${account.subscription_type}">${account.subscription_type}</span>`
|
||||
: `<span class="badge pending">-</span>`}
|
||||
</div>
|
||||
</td>
|
||||
<td>${format.date(account.last_refresh) || '-'}</td>
|
||||
<td>
|
||||
<div class="action-buttons">
|
||||
@@ -292,6 +323,12 @@ function renderAccounts(accounts) {
|
||||
<button class="btn btn-ghost btn-sm" onclick="uploadToCpa(${account.id})" title="上传到CPA">
|
||||
☁️
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm" onclick="markSubscription(${account.id})" title="标记订阅">
|
||||
🏷️
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm" onclick="uploadToTm(${account.id})" title="上传到Team Manager">
|
||||
🚀
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm" onclick="viewAccount(${account.id})" title="查看详情">
|
||||
👁️
|
||||
</button>
|
||||
@@ -315,10 +352,24 @@ function renderAccounts(accounts) {
|
||||
selectedAccounts.add(id);
|
||||
} else {
|
||||
selectedAccounts.delete(id);
|
||||
selectAllPages = false;
|
||||
}
|
||||
// 同步全选框状态
|
||||
const allChecked = elements.table.querySelectorAll('input[type="checkbox"][data-id]');
|
||||
const checkedCount = elements.table.querySelectorAll('input[type="checkbox"][data-id]:checked').length;
|
||||
elements.selectAll.checked = allChecked.length > 0 && checkedCount === allChecked.length;
|
||||
elements.selectAll.indeterminate = checkedCount > 0 && checkedCount < allChecked.length;
|
||||
updateBatchButtons();
|
||||
renderSelectAllBanner();
|
||||
});
|
||||
});
|
||||
|
||||
// 渲染后同步全选框状态
|
||||
const allCbs = elements.table.querySelectorAll('input[type="checkbox"][data-id]');
|
||||
const checkedCbs = elements.table.querySelectorAll('input[type="checkbox"][data-id]:checked');
|
||||
elements.selectAll.checked = allCbs.length > 0 && checkedCbs.length === allCbs.length;
|
||||
elements.selectAll.indeterminate = checkedCbs.length > 0 && checkedCbs.length < allCbs.length;
|
||||
renderSelectAllBanner();
|
||||
}
|
||||
|
||||
// 切换密码显示
|
||||
@@ -344,19 +395,87 @@ function updatePagination() {
|
||||
elements.pageInfo.textContent = `第 ${currentPage} 页 / 共 ${totalPages} 页`;
|
||||
}
|
||||
|
||||
// 重置全选所有页状态
|
||||
function resetSelectAllPages() {
|
||||
selectAllPages = false;
|
||||
selectedAccounts.clear();
|
||||
updateBatchButtons();
|
||||
renderSelectAllBanner();
|
||||
}
|
||||
|
||||
// 构建批量请求体(含 select_all 和筛选参数)
|
||||
function buildBatchPayload(extraFields = {}) {
|
||||
if (selectAllPages) {
|
||||
return {
|
||||
ids: [],
|
||||
select_all: true,
|
||||
status_filter: currentFilters.status || null,
|
||||
email_service_filter: currentFilters.email_service || null,
|
||||
search_filter: currentFilters.search || null,
|
||||
...extraFields
|
||||
};
|
||||
}
|
||||
return { ids: Array.from(selectedAccounts), ...extraFields };
|
||||
}
|
||||
|
||||
// 获取有效选中数量(select_all 时用总数)
|
||||
function getEffectiveCount() {
|
||||
return selectAllPages ? totalAccounts : selectedAccounts.size;
|
||||
}
|
||||
|
||||
// 渲染全选横幅
|
||||
function renderSelectAllBanner() {
|
||||
let banner = document.getElementById('select-all-banner');
|
||||
const totalPages = Math.ceil(totalAccounts / pageSize);
|
||||
const currentPageSize = elements.table.querySelectorAll('input[type="checkbox"][data-id]').length;
|
||||
const checkedOnPage = elements.table.querySelectorAll('input[type="checkbox"][data-id]:checked').length;
|
||||
const allPageSelected = currentPageSize > 0 && checkedOnPage === currentPageSize;
|
||||
|
||||
// 只在全选了当前页且有多页时显示横幅
|
||||
if (!allPageSelected || totalPages <= 1 || totalAccounts <= pageSize) {
|
||||
if (banner) banner.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!banner) {
|
||||
banner = document.createElement('div');
|
||||
banner.id = 'select-all-banner';
|
||||
banner.style.cssText = 'background:var(--primary-light,#e8f0fe);color:var(--primary-color,#1a73e8);padding:8px 16px;text-align:center;font-size:0.875rem;border-bottom:1px solid var(--border-color);';
|
||||
const tableContainer = document.querySelector('.table-container');
|
||||
if (tableContainer) tableContainer.insertAdjacentElement('beforebegin', banner);
|
||||
}
|
||||
|
||||
if (selectAllPages) {
|
||||
banner.innerHTML = `已选中全部 <strong>${totalAccounts}</strong> 条记录。<button onclick="resetSelectAllPages()" style="margin-left:8px;color:var(--primary-color,#1a73e8);background:none;border:none;cursor:pointer;text-decoration:underline;">取消全选</button>`;
|
||||
} else {
|
||||
banner.innerHTML = `当前页已全选 <strong>${checkedOnPage}</strong> 条。<button onclick="selectAllPagesAction()" style="margin-left:8px;color:var(--primary-color,#1a73e8);background:none;border:none;cursor:pointer;text-decoration:underline;">选择全部 ${totalAccounts} 条</button>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 选中所有页
|
||||
function selectAllPagesAction() {
|
||||
selectAllPages = true;
|
||||
updateBatchButtons();
|
||||
renderSelectAllBanner();
|
||||
}
|
||||
|
||||
// 更新批量操作按钮
|
||||
function updateBatchButtons() {
|
||||
const count = selectedAccounts.size;
|
||||
const count = getEffectiveCount();
|
||||
elements.batchDeleteBtn.disabled = count === 0;
|
||||
elements.batchRefreshBtn.disabled = count === 0;
|
||||
elements.batchValidateBtn.disabled = count === 0;
|
||||
elements.batchUploadCpaBtn.disabled = count === 0;
|
||||
elements.batchCheckSubBtn.disabled = count === 0;
|
||||
elements.batchUploadTmBtn.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';
|
||||
elements.batchCheckSubBtn.textContent = count > 0 ? `🔍 检测 (${count})` : '🔍 检测订阅';
|
||||
elements.batchUploadTmBtn.textContent = count > 0 ? `🚀 上传TM (${count})` : '🚀 上传TM';
|
||||
}
|
||||
|
||||
// 刷新单个账号Token
|
||||
@@ -378,19 +497,17 @@ async function refreshToken(id) {
|
||||
|
||||
// 批量刷新Token
|
||||
async function handleBatchRefresh() {
|
||||
if (selectedAccounts.size === 0) return;
|
||||
const count = getEffectiveCount();
|
||||
if (count === 0) return;
|
||||
|
||||
const confirmed = await confirm(`确定要刷新选中的 ${selectedAccounts.size} 个账号的Token吗?`);
|
||||
const confirmed = await confirm(`确定要刷新选中的 ${count} 个账号的Token吗?`);
|
||||
if (!confirmed) return;
|
||||
|
||||
elements.batchRefreshBtn.disabled = true;
|
||||
elements.batchRefreshBtn.textContent = '刷新中...';
|
||||
|
||||
try {
|
||||
const result = await api.post('/accounts/batch-refresh', {
|
||||
ids: Array.from(selectedAccounts)
|
||||
});
|
||||
|
||||
const result = await api.post('/accounts/batch-refresh', buildBatchPayload());
|
||||
toast.success(`成功刷新 ${result.success_count} 个,失败 ${result.failed_count} 个`);
|
||||
loadAccounts();
|
||||
} catch (error) {
|
||||
@@ -402,16 +519,13 @@ async function handleBatchRefresh() {
|
||||
|
||||
// 批量验证Token
|
||||
async function handleBatchValidate() {
|
||||
if (selectedAccounts.size === 0) return;
|
||||
if (getEffectiveCount() === 0) return;
|
||||
|
||||
elements.batchValidateBtn.disabled = true;
|
||||
elements.batchValidateBtn.textContent = '验证中...';
|
||||
|
||||
try {
|
||||
const result = await api.post('/accounts/batch-validate', {
|
||||
ids: Array.from(selectedAccounts)
|
||||
});
|
||||
|
||||
const result = await api.post('/accounts/batch-validate', buildBatchPayload());
|
||||
toast.info(`有效: ${result.valid_count},无效: ${result.invalid_count}`);
|
||||
loadAccounts();
|
||||
} catch (error) {
|
||||
@@ -499,6 +613,17 @@ async function viewAccount(id) {
|
||||
${tokens.refresh_token ? `<button class="btn btn-ghost btn-sm" onclick="copyToClipboard('${escapeHtml(tokens.refresh_token)}')" style="margin-left: 8px;">📋</button>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item" style="grid-column: span 2;">
|
||||
<span class="label">Cookies(支付用)</span>
|
||||
<div class="value">
|
||||
<textarea id="cookies-input-${id}" rows="3"
|
||||
style="width:100%;font-size:0.7rem;font-family:var(--font-mono);background:var(--surface-hover);border:1px solid var(--border);border-radius:4px;padding:6px;color:var(--text-primary);resize:vertical;"
|
||||
placeholder="粘贴完整 cookie 字符串,留空则清除">${escapeHtml(account.cookies || '')}</textarea>
|
||||
<button class="btn btn-secondary btn-sm" style="margin-top:4px" onclick="saveCookies(${id})">
|
||||
保存 Cookies
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: var(--spacing-lg); display: flex; gap: var(--spacing-sm);">
|
||||
<button class="btn btn-primary" onclick="refreshToken(${id}); elements.detailModal.classList.remove('active');">
|
||||
@@ -536,18 +661,17 @@ async function deleteAccount(id, email) {
|
||||
|
||||
// 批量删除
|
||||
async function handleBatchDelete() {
|
||||
if (selectedAccounts.size === 0) return;
|
||||
const count = getEffectiveCount();
|
||||
if (count === 0) return;
|
||||
|
||||
const confirmed = await confirm(`确定要删除选中的 ${selectedAccounts.size} 个账号吗?此操作不可恢复。`);
|
||||
const confirmed = await confirm(`确定要删除选中的 ${count} 个账号吗?此操作不可恢复。`);
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
const result = await api.post('/accounts/batch-delete', {
|
||||
ids: Array.from(selectedAccounts)
|
||||
});
|
||||
|
||||
const result = await api.post('/accounts/batch-delete', buildBatchPayload());
|
||||
toast.success(`成功删除 ${result.deleted_count} 个账号`);
|
||||
selectedAccounts.clear();
|
||||
selectAllPages = false;
|
||||
loadStats();
|
||||
loadAccounts();
|
||||
} catch (error) {
|
||||
@@ -557,12 +681,13 @@ async function handleBatchDelete() {
|
||||
|
||||
// 导出账号
|
||||
async function exportAccounts(format) {
|
||||
if (selectedAccounts.size === 0) {
|
||||
const count = getEffectiveCount();
|
||||
if (count === 0) {
|
||||
toast.warning('请先选择要导出的账号');
|
||||
return;
|
||||
}
|
||||
|
||||
toast.info(`正在导出 ${selectedAccounts.size} 个账号...`);
|
||||
toast.info(`正在导出 ${count} 个账号...`);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/accounts/export/' + format, {
|
||||
@@ -570,9 +695,7 @@ async function exportAccounts(format) {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
ids: Array.from(selectedAccounts)
|
||||
})
|
||||
body: JSON.stringify(buildBatchPayload())
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -584,7 +707,7 @@ async function exportAccounts(format) {
|
||||
|
||||
// 从 Content-Disposition 获取文件名
|
||||
const disposition = response.headers.get('Content-Disposition');
|
||||
let filename = `accounts_${Date.now()}.${format === 'cpa' ? 'json' : format}`;
|
||||
let filename = `accounts_${Date.now()}.${(format === 'cpa' || format === 'sub2api') ? 'json' : format}`;
|
||||
if (disposition) {
|
||||
const match = disposition.match(/filename=(.+)/);
|
||||
if (match) {
|
||||
@@ -636,18 +759,17 @@ async function uploadToCpa(id) {
|
||||
|
||||
// 批量上传到CPA
|
||||
async function handleBatchUploadCpa() {
|
||||
if (selectedAccounts.size === 0) return;
|
||||
const count = getEffectiveCount();
|
||||
if (count === 0) return;
|
||||
|
||||
const confirmed = await confirm(`确定要将选中的 ${selectedAccounts.size} 个账号上传到CPA吗?`);
|
||||
const confirmed = await confirm(`确定要将选中的 ${count} 个账号上传到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)
|
||||
});
|
||||
const result = await api.post('/accounts/batch-upload-cpa', buildBatchPayload());
|
||||
|
||||
let message = `成功: ${result.success_count}`;
|
||||
if (result.failed_count > 0) {
|
||||
@@ -665,3 +787,101 @@ async function handleBatchUploadCpa() {
|
||||
updateBatchButtons();
|
||||
}
|
||||
}
|
||||
|
||||
// ============== 订阅状态 ==============
|
||||
|
||||
// 手动标记订阅类型
|
||||
async function markSubscription(id) {
|
||||
const type = prompt('请输入订阅类型 (plus / team / free):', 'plus');
|
||||
if (!type) return;
|
||||
if (!['plus', 'team', 'free'].includes(type.trim().toLowerCase())) {
|
||||
toast.error('无效的订阅类型,请输入 plus、team 或 free');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await api.post(`/payment/accounts/${id}/mark-subscription`, {
|
||||
subscription_type: type.trim().toLowerCase()
|
||||
});
|
||||
toast.success('订阅状态已更新');
|
||||
loadAccounts();
|
||||
} catch (e) {
|
||||
toast.error('标记失败: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 批量检测订阅状态
|
||||
async function handleBatchCheckSubscription() {
|
||||
const count = getEffectiveCount();
|
||||
if (count === 0) return;
|
||||
const confirmed = await confirm(`确定要检测选中的 ${count} 个账号的订阅状态吗?`);
|
||||
if (!confirmed) return;
|
||||
|
||||
elements.batchCheckSubBtn.disabled = true;
|
||||
elements.batchCheckSubBtn.textContent = '检测中...';
|
||||
|
||||
try {
|
||||
const result = await api.post('/payment/accounts/batch-check-subscription', buildBatchPayload());
|
||||
let message = `成功: ${result.success_count}`;
|
||||
if (result.failed_count > 0) message += `, 失败: ${result.failed_count}`;
|
||||
toast.success(message);
|
||||
loadAccounts();
|
||||
} catch (e) {
|
||||
toast.error('批量检测失败: ' + e.message);
|
||||
} finally {
|
||||
updateBatchButtons();
|
||||
}
|
||||
}
|
||||
|
||||
// ============== Team Manager 上传 ==============
|
||||
|
||||
// 上传单账号到 Team Manager
|
||||
async function uploadToTm(id) {
|
||||
try {
|
||||
toast.info('正在上传到 Team Manager...');
|
||||
const result = await api.post(`/payment/accounts/${id}/upload-tm`);
|
||||
if (result.success) {
|
||||
toast.success('上传成功');
|
||||
} else {
|
||||
toast.error('上传失败: ' + (result.message || '未知错误'));
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error('上传失败: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 批量上传到 Team Manager
|
||||
async function handleBatchUploadTm() {
|
||||
const count = getEffectiveCount();
|
||||
if (count === 0) return;
|
||||
const confirmed = await confirm(`确定要将选中的 ${count} 个账号上传到 Team Manager 吗?`);
|
||||
if (!confirmed) return;
|
||||
|
||||
elements.batchUploadTmBtn.disabled = true;
|
||||
elements.batchUploadTmBtn.textContent = '上传中...';
|
||||
|
||||
try {
|
||||
const result = await api.post('/payment/accounts/batch-upload-tm', buildBatchPayload());
|
||||
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 (e) {
|
||||
toast.error('批量上传失败: ' + e.message);
|
||||
} finally {
|
||||
updateBatchButtons();
|
||||
}
|
||||
}
|
||||
|
||||
// 保存账号 Cookies
|
||||
async function saveCookies(id) {
|
||||
const textarea = document.getElementById(`cookies-input-${id}`);
|
||||
if (!textarea) return;
|
||||
const cookiesValue = textarea.value.trim();
|
||||
try {
|
||||
await api.patch(`/accounts/${id}`, { cookies: cookiesValue });
|
||||
toast.success('Cookies 已保存');
|
||||
} catch (e) {
|
||||
toast.error('保存 Cookies 失败: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,8 @@ let toastShown = false; // 标记是否已显示过 toast
|
||||
let availableServices = {
|
||||
tempmail: { available: true, services: [] },
|
||||
outlook: { available: false, services: [] },
|
||||
custom_domain: { available: false, services: [] }
|
||||
custom_domain: { available: false, services: [] },
|
||||
temp_mail: { available: false, services: [] }
|
||||
};
|
||||
|
||||
// WebSocket 相关变量
|
||||
@@ -80,7 +81,9 @@ const elements = {
|
||||
concurrencyMode: document.getElementById('concurrency-mode'),
|
||||
concurrencyCount: document.getElementById('concurrency-count'),
|
||||
concurrencyHint: document.getElementById('concurrency-hint'),
|
||||
intervalGroup: document.getElementById('interval-group')
|
||||
intervalGroup: document.getElementById('interval-group'),
|
||||
// 注册后自动操作
|
||||
autoUploadCpa: document.getElementById('auto-upload-cpa')
|
||||
};
|
||||
|
||||
// 初始化
|
||||
@@ -91,8 +94,25 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
startAccountsPolling();
|
||||
initVisibilityReconnect();
|
||||
restoreActiveTask();
|
||||
checkCpaEnabled();
|
||||
});
|
||||
|
||||
// 检查 CPA 是否启用,未启用则禁用复选框
|
||||
async function checkCpaEnabled() {
|
||||
if (!elements.autoUploadCpa) return;
|
||||
try {
|
||||
const data = await api.get('/settings/cpa');
|
||||
if (!data.enabled) {
|
||||
elements.autoUploadCpa.disabled = true;
|
||||
elements.autoUploadCpa.title = '请先在设置中启用 CPA 上传';
|
||||
const label = elements.autoUploadCpa.closest('label');
|
||||
if (label) label.style.opacity = '0.5';
|
||||
}
|
||||
} catch (e) {
|
||||
elements.autoUploadCpa.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 事件监听
|
||||
function initEventListeners() {
|
||||
// 注册表单提交
|
||||
@@ -229,6 +249,23 @@ function updateEmailServiceOptions() {
|
||||
|
||||
select.appendChild(optgroup);
|
||||
}
|
||||
|
||||
// Temp-Mail(自部署)
|
||||
if (availableServices.temp_mail && availableServices.temp_mail.available) {
|
||||
const optgroup = document.createElement('optgroup');
|
||||
optgroup.label = `📮 Temp-Mail 自部署 (${availableServices.temp_mail.count} 个服务)`;
|
||||
|
||||
availableServices.temp_mail.services.forEach(service => {
|
||||
const option = document.createElement('option');
|
||||
option.value = `temp_mail:${service.id}`;
|
||||
option.textContent = service.name + (service.domain ? ` (@${service.domain})` : '');
|
||||
option.dataset.type = 'temp_mail';
|
||||
option.dataset.serviceId = service.id;
|
||||
optgroup.appendChild(option);
|
||||
});
|
||||
|
||||
select.appendChild(optgroup);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理邮箱服务切换
|
||||
@@ -317,7 +354,8 @@ async function handleStartRegistration(e) {
|
||||
|
||||
// 构建请求数据(代理从设置中自动获取)
|
||||
const requestData = {
|
||||
email_service_type: emailServiceType
|
||||
email_service_type: emailServiceType,
|
||||
auto_upload_cpa: elements.autoUploadCpa ? elements.autoUploadCpa.checked : false
|
||||
};
|
||||
|
||||
// 如果选择了数据库中的服务,传递 service_id
|
||||
@@ -1015,7 +1053,8 @@ async function handleOutlookBatchRegistration() {
|
||||
interval_min: intervalMin,
|
||||
interval_max: intervalMax,
|
||||
concurrency: Math.min(50, Math.max(1, concurrency)),
|
||||
mode: mode
|
||||
mode: mode,
|
||||
auto_upload_cpa: elements.autoUploadCpa ? elements.autoUploadCpa.checked : false
|
||||
};
|
||||
|
||||
addLog('info', `[系统] 正在启动 Outlook 批量注册 (${selectedIds.length} 个账户)...`);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
// 状态
|
||||
let outlookServices = [];
|
||||
let customServices = [];
|
||||
let customServices = []; // 合并 custom_domain + temp_mail
|
||||
let selectedOutlook = new Set();
|
||||
let selectedCustom = new Set();
|
||||
|
||||
@@ -31,7 +31,7 @@ const elements = {
|
||||
selectAllOutlook: document.getElementById('select-all-outlook'),
|
||||
batchDeleteOutlookBtn: document.getElementById('batch-delete-outlook-btn'),
|
||||
|
||||
// 自定义域名
|
||||
// 自定义域名(合并)
|
||||
customTable: document.getElementById('custom-services-table'),
|
||||
addCustomBtn: document.getElementById('add-custom-btn'),
|
||||
selectAllCustom: document.getElementById('select-all-custom'),
|
||||
@@ -47,18 +47,25 @@ const elements = {
|
||||
addCustomForm: document.getElementById('add-custom-form'),
|
||||
closeCustomModal: document.getElementById('close-custom-modal'),
|
||||
cancelAddCustom: document.getElementById('cancel-add-custom'),
|
||||
customSubType: document.getElementById('custom-sub-type'),
|
||||
addMoemailFields: document.getElementById('add-moemail-fields'),
|
||||
addTempmailFields: document.getElementById('add-tempmail-fields'),
|
||||
|
||||
// 编辑自定义域名模态框
|
||||
editCustomModal: document.getElementById('edit-custom-modal'),
|
||||
editCustomForm: document.getElementById('edit-custom-form'),
|
||||
closeEditCustomModal: document.getElementById('close-edit-custom-modal'),
|
||||
cancelEditCustom: document.getElementById('cancel-edit-custom'),
|
||||
editMoemailFields: document.getElementById('edit-moemail-fields'),
|
||||
editTempmailFields: document.getElementById('edit-tempmail-fields'),
|
||||
editCustomTypeBadge: document.getElementById('edit-custom-type-badge'),
|
||||
editCustomSubTypeHidden: document.getElementById('edit-custom-sub-type-hidden'),
|
||||
|
||||
// 编辑 Outlook 模态框
|
||||
editOutlookModal: document.getElementById('edit-outlook-modal'),
|
||||
editOutlookForm: document.getElementById('edit-outlook-form'),
|
||||
closeEditOutlookModal: document.getElementById('close-edit-outlook-modal'),
|
||||
cancelEditOutlook: document.getElementById('cancel-edit-outlook')
|
||||
cancelEditOutlook: document.getElementById('cancel-edit-outlook'),
|
||||
};
|
||||
|
||||
// 初始化
|
||||
@@ -92,11 +99,8 @@ function initEventListeners() {
|
||||
checkboxes.forEach(cb => {
|
||||
cb.checked = e.target.checked;
|
||||
const id = parseInt(cb.dataset.id);
|
||||
if (e.target.checked) {
|
||||
selectedOutlook.add(id);
|
||||
} else {
|
||||
selectedOutlook.delete(id);
|
||||
}
|
||||
if (e.target.checked) selectedOutlook.add(id);
|
||||
else selectedOutlook.delete(id);
|
||||
});
|
||||
updateBatchButtons();
|
||||
});
|
||||
@@ -104,68 +108,72 @@ function initEventListeners() {
|
||||
// Outlook 批量删除
|
||||
elements.batchDeleteOutlookBtn.addEventListener('click', handleBatchDeleteOutlook);
|
||||
|
||||
// 添加自定义域名
|
||||
elements.addCustomBtn.addEventListener('click', () => {
|
||||
elements.addCustomModal.classList.add('active');
|
||||
});
|
||||
|
||||
elements.closeCustomModal.addEventListener('click', () => {
|
||||
elements.addCustomModal.classList.remove('active');
|
||||
});
|
||||
|
||||
elements.cancelAddCustom.addEventListener('click', () => {
|
||||
elements.addCustomModal.classList.remove('active');
|
||||
});
|
||||
|
||||
elements.addCustomForm.addEventListener('submit', handleAddCustom);
|
||||
|
||||
// 编辑自定义域名模态框
|
||||
elements.closeEditCustomModal.addEventListener('click', () => {
|
||||
elements.editCustomModal.classList.remove('active');
|
||||
});
|
||||
|
||||
elements.cancelEditCustom.addEventListener('click', () => {
|
||||
elements.editCustomModal.classList.remove('active');
|
||||
});
|
||||
|
||||
elements.editCustomForm.addEventListener('submit', handleEditCustom);
|
||||
|
||||
// 编辑 Outlook 模态框
|
||||
elements.closeEditOutlookModal.addEventListener('click', () => {
|
||||
elements.editOutlookModal.classList.remove('active');
|
||||
});
|
||||
|
||||
elements.cancelEditOutlook.addEventListener('click', () => {
|
||||
elements.editOutlookModal.classList.remove('active');
|
||||
});
|
||||
|
||||
elements.editOutlookForm.addEventListener('submit', handleEditOutlook);
|
||||
|
||||
// 自定义域名全选
|
||||
elements.selectAllCustom.addEventListener('change', (e) => {
|
||||
const checkboxes = elements.customTable.querySelectorAll('input[type="checkbox"][data-id]');
|
||||
checkboxes.forEach(cb => {
|
||||
cb.checked = e.target.checked;
|
||||
const id = parseInt(cb.dataset.id);
|
||||
if (e.target.checked) {
|
||||
selectedCustom.add(id);
|
||||
} else {
|
||||
selectedCustom.delete(id);
|
||||
}
|
||||
if (e.target.checked) selectedCustom.add(id);
|
||||
else selectedCustom.delete(id);
|
||||
});
|
||||
});
|
||||
|
||||
// 添加自定义域名
|
||||
elements.addCustomBtn.addEventListener('click', () => {
|
||||
elements.addCustomForm.reset();
|
||||
switchAddSubType('moemail');
|
||||
elements.addCustomModal.classList.add('active');
|
||||
});
|
||||
elements.closeCustomModal.addEventListener('click', () => elements.addCustomModal.classList.remove('active'));
|
||||
elements.cancelAddCustom.addEventListener('click', () => elements.addCustomModal.classList.remove('active'));
|
||||
elements.addCustomForm.addEventListener('submit', handleAddCustom);
|
||||
|
||||
// 类型切换(添加表单)
|
||||
elements.customSubType.addEventListener('change', (e) => switchAddSubType(e.target.value));
|
||||
|
||||
// 编辑自定义域名
|
||||
elements.closeEditCustomModal.addEventListener('click', () => elements.editCustomModal.classList.remove('active'));
|
||||
elements.cancelEditCustom.addEventListener('click', () => elements.editCustomModal.classList.remove('active'));
|
||||
elements.editCustomForm.addEventListener('submit', handleEditCustom);
|
||||
|
||||
// 编辑 Outlook
|
||||
elements.closeEditOutlookModal.addEventListener('click', () => elements.editOutlookModal.classList.remove('active'));
|
||||
elements.cancelEditOutlook.addEventListener('click', () => elements.editOutlookModal.classList.remove('active'));
|
||||
elements.editOutlookForm.addEventListener('submit', handleEditOutlook);
|
||||
|
||||
// 临时邮箱配置
|
||||
elements.tempmailForm.addEventListener('submit', handleSaveTempmail);
|
||||
elements.testTempmailBtn.addEventListener('click', handleTestTempmail);
|
||||
}
|
||||
|
||||
// 切换添加表单子类型
|
||||
function switchAddSubType(subType) {
|
||||
elements.customSubType.value = subType;
|
||||
if (subType === 'moemail') {
|
||||
elements.addMoemailFields.style.display = '';
|
||||
elements.addTempmailFields.style.display = 'none';
|
||||
} else {
|
||||
elements.addMoemailFields.style.display = 'none';
|
||||
elements.addTempmailFields.style.display = '';
|
||||
}
|
||||
}
|
||||
|
||||
// 切换编辑表单子类型显示
|
||||
function switchEditSubType(subType) {
|
||||
elements.editCustomSubTypeHidden.value = subType;
|
||||
const isMoe = subType === 'moemail';
|
||||
elements.editMoemailFields.style.display = isMoe ? '' : 'none';
|
||||
elements.editTempmailFields.style.display = isMoe ? 'none' : '';
|
||||
elements.editCustomTypeBadge.textContent = isMoe ? '🔗 MoeMail(自定义域名 API)' : '📮 TempMail(自部署 Cloudflare Worker)';
|
||||
}
|
||||
|
||||
// 加载统计信息
|
||||
async function loadStats() {
|
||||
try {
|
||||
const data = await api.get('/email-services/stats');
|
||||
elements.outlookCount.textContent = data.outlook_count || 0;
|
||||
elements.customCount.textContent = data.custom_count || 0;
|
||||
elements.customCount.textContent = (data.custom_count || 0) + (data.temp_mail_count || 0);
|
||||
elements.tempmailStatus.textContent = data.tempmail_available ? '可用' : '不可用';
|
||||
elements.totalEnabled.textContent = data.enabled_count || 0;
|
||||
} catch (error) {
|
||||
@@ -196,10 +204,7 @@ async function loadOutlookServices() {
|
||||
|
||||
elements.outlookTable.innerHTML = outlookServices.map(service => `
|
||||
<tr data-id="${service.id}">
|
||||
<td>
|
||||
<input type="checkbox" data-id="${service.id}"
|
||||
${selectedOutlook.has(service.id) ? 'checked' : ''}>
|
||||
</td>
|
||||
<td><input type="checkbox" data-id="${service.id}" ${selectedOutlook.has(service.id) ? 'checked' : ''}></td>
|
||||
<td>${escapeHtml(service.config?.email || service.name)}</td>
|
||||
<td>
|
||||
<span class="status-badge ${service.config?.has_oauth ? 'active' : 'pending'}">
|
||||
@@ -215,65 +220,50 @@ async function loadOutlookServices() {
|
||||
<td>${format.date(service.last_used)}</td>
|
||||
<td>
|
||||
<div class="action-buttons">
|
||||
<button class="btn btn-ghost btn-sm" onclick="editOutlookService(${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="testService(${service.id})" title="测试">
|
||||
🔌
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm" onclick="deleteService(${service.id}, '${escapeHtml(service.name)}')" title="删除">
|
||||
🗑️
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm" onclick="editOutlookService(${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="testService(${service.id})" title="测试">🔌</button>
|
||||
<button class="btn btn-ghost btn-sm" onclick="deleteService(${service.id}, '${escapeHtml(service.name)}')" title="删除">🗑️</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
|
||||
// 绑定复选框事件
|
||||
elements.outlookTable.querySelectorAll('input[type="checkbox"][data-id]').forEach(cb => {
|
||||
cb.addEventListener('change', (e) => {
|
||||
const id = parseInt(e.target.dataset.id);
|
||||
if (e.target.checked) {
|
||||
selectedOutlook.add(id);
|
||||
} else {
|
||||
selectedOutlook.delete(id);
|
||||
}
|
||||
if (e.target.checked) selectedOutlook.add(id);
|
||||
else selectedOutlook.delete(id);
|
||||
updateBatchButtons();
|
||||
});
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载 Outlook 服务失败:', error);
|
||||
elements.outlookTable.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="7">
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">❌</div>
|
||||
<div class="empty-state-title">加载失败</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
elements.outlookTable.innerHTML = `<tr><td colspan="7"><div class="empty-state"><div class="empty-state-icon">❌</div><div class="empty-state-title">加载失败</div></div></td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载自定义域名服务
|
||||
// 加载自定义域名服务(custom_domain + temp_mail 合并)
|
||||
async function loadCustomServices() {
|
||||
try {
|
||||
const data = await api.get('/email-services?service_type=custom_domain');
|
||||
customServices = data.services || [];
|
||||
const [r1, r2] = await Promise.all([
|
||||
api.get('/email-services?service_type=custom_domain'),
|
||||
api.get('/email-services?service_type=temp_mail')
|
||||
]);
|
||||
customServices = [
|
||||
...(r1.services || []).map(s => ({ ...s, _subType: 'moemail' })),
|
||||
...(r2.services || []).map(s => ({ ...s, _subType: 'tempmail' }))
|
||||
];
|
||||
|
||||
if (customServices.length === 0) {
|
||||
elements.customTable.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="7">
|
||||
<td colspan="8">
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">📭</div>
|
||||
<div class="empty-state-title">暂无自定义域名服务</div>
|
||||
<div class="empty-state-description">点击"添加服务"按钮创建新服务</div>
|
||||
<div class="empty-state-description">点击「添加服务」按钮创建新服务</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -281,49 +271,35 @@ async function loadCustomServices() {
|
||||
return;
|
||||
}
|
||||
|
||||
elements.customTable.innerHTML = customServices.map(service => `
|
||||
elements.customTable.innerHTML = customServices.map(service => {
|
||||
const isMoe = service._subType === 'moemail';
|
||||
const typeLabel = isMoe ? '<span class="status-badge info">MoeMail</span>' : '<span class="status-badge warning">TempMail</span>';
|
||||
const addr = isMoe ? (service.config?.base_url || '-') : (service.config?.base_url || '-');
|
||||
return `
|
||||
<tr data-id="${service.id}">
|
||||
<td>
|
||||
<input type="checkbox" data-id="${service.id}"
|
||||
${selectedCustom.has(service.id) ? 'checked' : ''}>
|
||||
</td>
|
||||
<td><input type="checkbox" data-id="${service.id}" ${selectedCustom.has(service.id) ? 'checked' : ''}></td>
|
||||
<td>${escapeHtml(service.name)}</td>
|
||||
<td style="font-size: 0.75rem;">${escapeHtml(service.config?.base_url || '-')}</td>
|
||||
<td>
|
||||
<span class="status-badge ${service.enabled ? 'active' : 'disabled'}">
|
||||
${service.enabled ? '启用' : '禁用'}
|
||||
</span>
|
||||
</td>
|
||||
<td>${typeLabel}</td>
|
||||
<td style="font-size: 0.75rem;">${escapeHtml(addr)}</td>
|
||||
<td><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="editCustomService(${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="testService(${service.id})" title="测试">
|
||||
🔌
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm" onclick="deleteService(${service.id}, '${escapeHtml(service.name)}')" title="删除">
|
||||
🗑️
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm" onclick="editCustomService(${service.id}, '${service._subType}')" 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="testService(${service.id})" title="测试">🔌</button>
|
||||
<button class="btn btn-ghost btn-sm" onclick="deleteService(${service.id}, '${escapeHtml(service.name)}')" title="删除">🗑️</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
</tr>`;
|
||||
}).join('');
|
||||
|
||||
// 绑定复选框事件
|
||||
elements.customTable.querySelectorAll('input[type="checkbox"][data-id]').forEach(cb => {
|
||||
cb.addEventListener('change', (e) => {
|
||||
const id = parseInt(e.target.dataset.id);
|
||||
if (e.target.checked) {
|
||||
selectedCustom.add(id);
|
||||
} else {
|
||||
selectedCustom.delete(id);
|
||||
}
|
||||
if (e.target.checked) selectedCustom.add(id);
|
||||
else selectedCustom.delete(id);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -348,10 +324,7 @@ async function loadTempmailConfig() {
|
||||
// Outlook 导入
|
||||
async function handleOutlookImport() {
|
||||
const data = elements.outlookImportData.value.trim();
|
||||
if (!data) {
|
||||
toast.error('请输入导入数据');
|
||||
return;
|
||||
}
|
||||
if (!data) { toast.error('请输入导入数据'); return; }
|
||||
|
||||
elements.outlookImportBtn.disabled = true;
|
||||
elements.outlookImportBtn.textContent = '导入中...';
|
||||
@@ -366,26 +339,18 @@ async function handleOutlookImport() {
|
||||
elements.importResult.style.display = 'block';
|
||||
elements.importResult.innerHTML = `
|
||||
<div class="import-stats">
|
||||
<span>✅ 成功导入: <strong>${result.success_count || 0}</strong></span>
|
||||
<span>❌ 失败: <strong>${result.failed_count || 0}</strong></span>
|
||||
<span>✅ 成功导入: <strong>${result.success || 0}</strong></span>
|
||||
<span>❌ 失败: <strong>${result.failed || 0}</strong></span>
|
||||
</div>
|
||||
${result.errors?.length ? `
|
||||
<div class="import-errors" style="margin-top: var(--spacing-sm);">
|
||||
<strong>错误详情:</strong>
|
||||
<ul>
|
||||
${result.errors.map(e => `<li>${escapeHtml(e)}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
` : ''}
|
||||
${result.errors?.length ? `<div class="import-errors" style="margin-top: var(--spacing-sm);"><strong>错误详情:</strong><ul>${result.errors.map(e => `<li>${escapeHtml(e)}</li>`).join('')}</ul></div>` : ''}
|
||||
`;
|
||||
|
||||
if (result.success_count > 0) {
|
||||
toast.success(`成功导入 ${result.success_count} 个账户`);
|
||||
if (result.success > 0) {
|
||||
toast.success(`成功导入 ${result.success} 个账户`);
|
||||
loadOutlookServices();
|
||||
loadStats();
|
||||
elements.outlookImportData.value = '';
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
toast.error('导入失败: ' + error.message);
|
||||
} finally {
|
||||
@@ -394,19 +359,34 @@ async function handleOutlookImport() {
|
||||
}
|
||||
}
|
||||
|
||||
// 添加自定义域名服务
|
||||
// 添加自定义域名服务(根据子类型区分)
|
||||
async function handleAddCustom(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(e.target);
|
||||
const data = {
|
||||
service_type: 'custom_domain',
|
||||
name: formData.get('name'),
|
||||
config: {
|
||||
const subType = formData.get('sub_type');
|
||||
|
||||
let serviceType, config;
|
||||
if (subType === 'moemail') {
|
||||
serviceType = 'custom_domain';
|
||||
config = {
|
||||
base_url: formData.get('api_url'),
|
||||
api_key: formData.get('api_key'),
|
||||
default_domain: formData.get('domain')
|
||||
},
|
||||
};
|
||||
} else {
|
||||
serviceType = 'temp_mail';
|
||||
config = {
|
||||
base_url: formData.get('tm_base_url'),
|
||||
admin_password: formData.get('tm_admin_password'),
|
||||
domain: formData.get('tm_domain'),
|
||||
enable_prefix: true
|
||||
};
|
||||
}
|
||||
|
||||
const data = {
|
||||
service_type: serviceType,
|
||||
name: formData.get('name'),
|
||||
config,
|
||||
enabled: formData.get('enabled') === 'on',
|
||||
priority: parseInt(formData.get('priority')) || 0
|
||||
};
|
||||
@@ -440,11 +420,8 @@ async function toggleService(id, enabled) {
|
||||
async function testService(id) {
|
||||
try {
|
||||
const result = await api.post(`/email-services/${id}/test`);
|
||||
if (result.success) {
|
||||
toast.success('测试成功');
|
||||
} else {
|
||||
toast.error('测试失败: ' + (result.error || '未知错误'));
|
||||
}
|
||||
if (result.success) toast.success('测试成功');
|
||||
else toast.error('测试失败: ' + (result.error || '未知错误'));
|
||||
} catch (error) {
|
||||
toast.error('测试失败: ' + error.message);
|
||||
}
|
||||
@@ -454,7 +431,6 @@ async function testService(id) {
|
||||
async function deleteService(id, name) {
|
||||
const confirmed = await confirm(`确定要删除 "${name}" 吗?`);
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
await api.delete(`/email-services/${id}`);
|
||||
toast.success('已删除');
|
||||
@@ -471,10 +447,8 @@ async function deleteService(id, name) {
|
||||
// 批量删除 Outlook
|
||||
async function handleBatchDeleteOutlook() {
|
||||
if (selectedOutlook.size === 0) return;
|
||||
|
||||
const confirmed = await confirm(`确定要删除选中的 ${selectedOutlook.size} 个账户吗?`);
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
const result = await api.request('/email-services/outlook/batch', {
|
||||
method: 'DELETE',
|
||||
@@ -492,7 +466,6 @@ async function handleBatchDeleteOutlook() {
|
||||
// 保存临时邮箱配置
|
||||
async function handleSaveTempmail(e) {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
await api.post('/settings/tempmail', {
|
||||
api_url: elements.tempmailApi.value,
|
||||
@@ -508,17 +481,12 @@ async function handleSaveTempmail(e) {
|
||||
async function handleTestTempmail() {
|
||||
elements.testTempmailBtn.disabled = true;
|
||||
elements.testTempmailBtn.textContent = '测试中...';
|
||||
|
||||
try {
|
||||
const result = await api.post('/email-services/test-tempmail', {
|
||||
api_url: elements.tempmailApi.value
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
toast.success('临时邮箱连接正常');
|
||||
} else {
|
||||
toast.error('连接失败: ' + (result.error || '未知错误'));
|
||||
}
|
||||
if (result.success) toast.success('临时邮箱连接正常');
|
||||
else toast.error('连接失败: ' + (result.error || '未知错误'));
|
||||
} catch (error) {
|
||||
toast.error('测试失败: ' + error.message);
|
||||
} finally {
|
||||
@@ -544,27 +512,32 @@ function escapeHtml(text) {
|
||||
|
||||
// ============== 编辑功能 ==============
|
||||
|
||||
// 编辑自定义域名服务
|
||||
async function editCustomService(id) {
|
||||
// 编辑自定义域名服务(支持 moemail / tempmail)
|
||||
async function editCustomService(id, subType) {
|
||||
try {
|
||||
// 获取完整的服务详情
|
||||
const service = await api.get(`/email-services/${id}/full`);
|
||||
const resolvedSubType = subType || (service.service_type === 'temp_mail' ? 'tempmail' : 'moemail');
|
||||
|
||||
// 填充表单
|
||||
document.getElementById('edit-custom-id').value = service.id;
|
||||
document.getElementById('edit-custom-name').value = service.name || '';
|
||||
document.getElementById('edit-custom-api-url').value = service.config?.base_url || '';
|
||||
document.getElementById('edit-custom-api-key').value = service.config?.api_key || '';
|
||||
document.getElementById('edit-custom-domain').value = service.config?.domain || '';
|
||||
document.getElementById('edit-custom-priority').value = service.priority || 0;
|
||||
document.getElementById('edit-custom-enabled').checked = service.enabled;
|
||||
|
||||
// 清空密码提示
|
||||
document.getElementById('edit-custom-api-key').placeholder = service.config?.has_api_key ? '已设置,留空保持不变' : 'API Key';
|
||||
switchEditSubType(resolvedSubType);
|
||||
|
||||
if (resolvedSubType === 'moemail') {
|
||||
document.getElementById('edit-custom-api-url').value = service.config?.base_url || '';
|
||||
document.getElementById('edit-custom-api-key').value = '';
|
||||
document.getElementById('edit-custom-api-key').placeholder = service.config?.has_api_key ? '已设置,留空保持不变' : 'API Key';
|
||||
document.getElementById('edit-custom-domain').value = service.config?.default_domain || service.config?.domain || '';
|
||||
} else {
|
||||
document.getElementById('edit-tm-base-url').value = service.config?.base_url || '';
|
||||
document.getElementById('edit-tm-admin-password').value = '';
|
||||
document.getElementById('edit-tm-admin-password').placeholder = service.config?.admin_password ? '已设置,留空保持不变' : '请输入 Admin 密码';
|
||||
document.getElementById('edit-tm-domain').value = service.config?.domain || '';
|
||||
}
|
||||
|
||||
// 显示模态框
|
||||
elements.editCustomModal.classList.add('active');
|
||||
|
||||
} catch (error) {
|
||||
toast.error('获取服务信息失败: ' + error.message);
|
||||
}
|
||||
@@ -573,31 +546,35 @@ async function editCustomService(id) {
|
||||
// 保存编辑自定义域名服务
|
||||
async function handleEditCustom(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const id = document.getElementById('edit-custom-id').value;
|
||||
const formData = new FormData(e.target);
|
||||
const subType = formData.get('sub_type');
|
||||
|
||||
let config;
|
||||
if (subType === 'moemail') {
|
||||
config = {
|
||||
base_url: formData.get('api_url'),
|
||||
default_domain: formData.get('domain')
|
||||
};
|
||||
const apiKey = formData.get('api_key');
|
||||
if (apiKey && apiKey.trim()) config.api_key = apiKey.trim();
|
||||
} else {
|
||||
config = {
|
||||
base_url: formData.get('tm_base_url'),
|
||||
domain: formData.get('tm_domain'),
|
||||
enable_prefix: true
|
||||
};
|
||||
const pwd = formData.get('tm_admin_password');
|
||||
if (pwd && pwd.trim()) config.admin_password = pwd.trim();
|
||||
}
|
||||
|
||||
// 构建更新数据
|
||||
const updateData = {
|
||||
name: formData.get('name'),
|
||||
priority: parseInt(formData.get('priority')) || 0,
|
||||
enabled: formData.get('enabled') === 'on'
|
||||
enabled: formData.get('enabled') === 'on',
|
||||
config
|
||||
};
|
||||
|
||||
// 构建配置
|
||||
const config = {
|
||||
base_url: formData.get('api_url'),
|
||||
default_domain: formData.get('domain')
|
||||
};
|
||||
|
||||
// 只有在填写了 API Key 时才更新
|
||||
const apiKey = formData.get('api_key');
|
||||
if (apiKey && apiKey.trim()) {
|
||||
config.api_key = apiKey.trim();
|
||||
}
|
||||
|
||||
updateData.config = config;
|
||||
|
||||
try {
|
||||
await api.patch(`/email-services/${id}`, updateData);
|
||||
toast.success('服务更新成功');
|
||||
@@ -612,10 +589,7 @@ async function handleEditCustom(e) {
|
||||
// 编辑 Outlook 服务
|
||||
async function editOutlookService(id) {
|
||||
try {
|
||||
// 获取完整的服务详情
|
||||
const service = await api.get(`/email-services/${id}/full`);
|
||||
|
||||
// 填充表单
|
||||
document.getElementById('edit-outlook-id').value = service.id;
|
||||
document.getElementById('edit-outlook-email').value = service.config?.email || service.name || '';
|
||||
document.getElementById('edit-outlook-password').value = '';
|
||||
@@ -625,10 +599,7 @@ async function editOutlookService(id) {
|
||||
document.getElementById('edit-outlook-refresh-token').placeholder = service.config?.refresh_token ? '已设置,留空保持不变' : 'OAuth Refresh Token';
|
||||
document.getElementById('edit-outlook-priority').value = service.priority || 0;
|
||||
document.getElementById('edit-outlook-enabled').checked = service.enabled;
|
||||
|
||||
// 显示模态框
|
||||
elements.editOutlookModal.classList.add('active');
|
||||
|
||||
} catch (error) {
|
||||
toast.error('获取服务信息失败: ' + error.message);
|
||||
}
|
||||
@@ -637,11 +608,9 @@ async function editOutlookService(id) {
|
||||
// 保存编辑 Outlook 服务
|
||||
async function handleEditOutlook(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const id = document.getElementById('edit-outlook-id').value;
|
||||
const formData = new FormData(e.target);
|
||||
|
||||
// 获取当前服务信息以保留未修改的敏感字段
|
||||
let currentService;
|
||||
try {
|
||||
currentService = await api.get(`/email-services/${id}/full`);
|
||||
@@ -650,23 +619,18 @@ async function handleEditOutlook(e) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建更新数据
|
||||
const updateData = {
|
||||
name: formData.get('email'), // 使用邮箱作为名称
|
||||
name: formData.get('email'),
|
||||
priority: parseInt(formData.get('priority')) || 0,
|
||||
enabled: formData.get('enabled') === 'on'
|
||||
enabled: formData.get('enabled') === 'on',
|
||||
config: {
|
||||
email: formData.get('email'),
|
||||
password: formData.get('password')?.trim() || currentService.config?.password || '',
|
||||
client_id: formData.get('client_id')?.trim() || currentService.config?.client_id || '',
|
||||
refresh_token: formData.get('refresh_token')?.trim() || currentService.config?.refresh_token || ''
|
||||
}
|
||||
};
|
||||
|
||||
// 构建配置,保留未修改的敏感字段
|
||||
const config = {
|
||||
email: formData.get('email'),
|
||||
password: formData.get('password')?.trim() || currentService.config?.password || '',
|
||||
client_id: formData.get('client_id')?.trim() || currentService.config?.client_id || '',
|
||||
refresh_token: formData.get('refresh_token')?.trim() || currentService.config?.refresh_token || ''
|
||||
};
|
||||
|
||||
updateData.config = config;
|
||||
|
||||
try {
|
||||
await api.patch(`/email-services/${id}`, updateData);
|
||||
toast.success('账户更新成功');
|
||||
|
||||
146
static/js/payment.js
Normal file
146
static/js/payment.js
Normal file
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* 支付页面 JavaScript
|
||||
*/
|
||||
|
||||
const COUNTRY_CURRENCY_MAP = {
|
||||
SG: 'SGD', US: 'USD', TR: 'TRY', JP: 'JPY',
|
||||
HK: 'HKD', GB: 'GBP', EU: 'EUR', AU: 'AUD',
|
||||
CA: 'CAD', IN: 'INR', BR: 'BRL', MX: 'MXN',
|
||||
};
|
||||
|
||||
let selectedPlan = 'plus';
|
||||
let generatedLink = '';
|
||||
|
||||
// 初始化
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadAccounts();
|
||||
});
|
||||
|
||||
// 加载账号列表
|
||||
async function loadAccounts() {
|
||||
try {
|
||||
const resp = await fetch('/api/accounts?page=1&page_size=100&status=active');
|
||||
const data = await resp.json();
|
||||
const sel = document.getElementById('account-select');
|
||||
sel.innerHTML = '<option value="">-- 请选择账号 --</option>';
|
||||
(data.accounts || []).forEach(acc => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = acc.id;
|
||||
opt.textContent = acc.email;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('加载账号失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 国家切换
|
||||
function onCountryChange() {
|
||||
const country = document.getElementById('country-select').value;
|
||||
const currency = COUNTRY_CURRENCY_MAP[country] || 'USD';
|
||||
document.getElementById('currency-display').value = currency;
|
||||
}
|
||||
|
||||
// 选择套餐
|
||||
function selectPlan(plan) {
|
||||
selectedPlan = plan;
|
||||
document.getElementById('plan-plus').classList.toggle('selected', plan === 'plus');
|
||||
document.getElementById('plan-team').classList.toggle('selected', plan === 'team');
|
||||
document.getElementById('team-options').classList.toggle('show', plan === 'team');
|
||||
// 隐藏已生成的链接
|
||||
document.getElementById('link-box').classList.remove('show');
|
||||
generatedLink = '';
|
||||
}
|
||||
|
||||
// 生成支付链接
|
||||
async function generateLink() {
|
||||
const accountId = document.getElementById('account-select').value;
|
||||
if (!accountId) {
|
||||
ui.showToast('请先选择账号', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const country = document.getElementById('country-select').value || 'SG';
|
||||
|
||||
const body = {
|
||||
account_id: parseInt(accountId),
|
||||
plan_type: selectedPlan,
|
||||
country: country,
|
||||
};
|
||||
|
||||
if (selectedPlan === 'team') {
|
||||
body.workspace_name = document.getElementById('workspace-name').value || 'MyTeam';
|
||||
body.seat_quantity = parseInt(document.getElementById('seat-quantity').value) || 5;
|
||||
body.price_interval = document.getElementById('price-interval').value;
|
||||
}
|
||||
|
||||
const btn = document.querySelector('.form-actions .btn-primary');
|
||||
if (btn) { btn.disabled = true; btn.textContent = '生成中...'; }
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/payment/generate-link', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.success && data.link) {
|
||||
generatedLink = data.link;
|
||||
document.getElementById('link-text').value = data.link;
|
||||
document.getElementById('link-box').classList.add('show');
|
||||
document.getElementById('open-status').textContent = '';
|
||||
ui.showToast('支付链接生成成功', 'success');
|
||||
} else {
|
||||
ui.showToast(data.detail || '生成链接失败', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
ui.showToast('请求失败: ' + e.message, 'error');
|
||||
} finally {
|
||||
if (btn) { btn.disabled = false; btn.textContent = '生成支付链接'; }
|
||||
}
|
||||
}
|
||||
|
||||
// 复制链接
|
||||
function copyLink() {
|
||||
if (!generatedLink) return;
|
||||
navigator.clipboard.writeText(generatedLink).then(() => {
|
||||
ui.showToast('已复制到剪贴板', 'success');
|
||||
}).catch(() => {
|
||||
const ta = document.getElementById('link-text');
|
||||
ta.select();
|
||||
document.execCommand('copy');
|
||||
ui.showToast('已复制到剪贴板', 'success');
|
||||
});
|
||||
}
|
||||
|
||||
// 无痕打开浏览器(携带账号 cookie)
|
||||
async function openIncognito() {
|
||||
if (!generatedLink) {
|
||||
ui.showToast('请先生成链接', 'warning');
|
||||
return;
|
||||
}
|
||||
const accountId = document.getElementById('account-select').value;
|
||||
const statusEl = document.getElementById('open-status');
|
||||
statusEl.textContent = '正在打开...';
|
||||
try {
|
||||
const body = { url: generatedLink };
|
||||
if (accountId) body.account_id = parseInt(accountId);
|
||||
|
||||
const resp = await fetch('/api/payment/open-incognito', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
statusEl.textContent = '已在无痕模式打开浏览器';
|
||||
ui.showToast('无痕浏览器已打开', 'success');
|
||||
} else {
|
||||
statusEl.textContent = data.message || '未找到可用浏览器,请手动复制链接';
|
||||
ui.showToast(data.message || '未找到浏览器', 'warning');
|
||||
}
|
||||
} catch (e) {
|
||||
statusEl.textContent = '请求失败: ' + e.message;
|
||||
ui.showToast('请求失败', 'error');
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,9 @@ const elements = {
|
||||
// CPA 设置
|
||||
cpaForm: document.getElementById('cpa-form'),
|
||||
testCpaBtn: document.getElementById('test-cpa-btn'),
|
||||
// Team Manager 设置
|
||||
tmForm: document.getElementById('tm-form'),
|
||||
testTmBtn: document.getElementById('test-tm-btn'),
|
||||
// 验证码设置
|
||||
emailCodeForm: document.getElementById('email-code-form'),
|
||||
// Outlook 设置
|
||||
@@ -236,6 +239,12 @@ function initEventListeners() {
|
||||
|
||||
if (elements.webuiSettingsForm) {
|
||||
elements.webuiSettingsForm.addEventListener('submit', handleSaveWebuiSettings);
|
||||
// Team Manager 设置
|
||||
if (elements.tmForm) {
|
||||
elements.tmForm.addEventListener('submit', handleSaveTm);
|
||||
}
|
||||
if (elements.testTmBtn) {
|
||||
elements.testTmBtn.addEventListener('click', handleTestTm);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,6 +283,8 @@ async function loadSettings() {
|
||||
loadCpaSettings();
|
||||
// 加载 Outlook 设置
|
||||
loadOutlookSettings();
|
||||
// 加载 Team Manager 设置
|
||||
loadTmSettings();
|
||||
|
||||
// Web UI 访问密码提示
|
||||
if (data.webui?.has_access_password) {
|
||||
@@ -1100,3 +1111,73 @@ async function handleTestDynamicProxy() {
|
||||
btn.textContent = '🔌 测试动态代理';
|
||||
}
|
||||
}
|
||||
|
||||
// ============== Team Manager 设置 ==============
|
||||
|
||||
async function loadTmSettings() {
|
||||
try {
|
||||
const data = await api.get('/settings/team-manager');
|
||||
document.getElementById('tm-enabled').checked = data.enabled || false;
|
||||
document.getElementById('tm-api-url').value = data.api_url || '';
|
||||
document.getElementById('tm-api-key').value = '';
|
||||
document.getElementById('tm-api-key').placeholder = data.has_api_key ? '已配置,留空保持不变' : '请输入 API Key';
|
||||
} catch (error) {
|
||||
console.error('加载 Team Manager 设置失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaveTm(e) {
|
||||
e.preventDefault();
|
||||
const data = {
|
||||
enabled: document.getElementById('tm-enabled').checked,
|
||||
api_url: document.getElementById('tm-api-url').value,
|
||||
api_key: document.getElementById('tm-api-key').value || ''
|
||||
};
|
||||
try {
|
||||
await api.post('/settings/team-manager', data);
|
||||
toast.success('Team Manager 设置已保存');
|
||||
loadTmSettings();
|
||||
} catch (error) {
|
||||
toast.error('保存失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTestTm() {
|
||||
const apiUrl = document.getElementById('tm-api-url').value;
|
||||
const apiKey = document.getElementById('tm-api-key').value;
|
||||
|
||||
if (!apiUrl) {
|
||||
toast.error('请先填写 API URL');
|
||||
return;
|
||||
}
|
||||
|
||||
let keyToTest = apiKey;
|
||||
if (!keyToTest) {
|
||||
const saved = await api.get('/settings/team-manager');
|
||||
if (!saved.has_api_key) {
|
||||
toast.error('请先填写 API Key');
|
||||
return;
|
||||
}
|
||||
keyToTest = 'use_saved_key';
|
||||
}
|
||||
|
||||
elements.testTmBtn.disabled = true;
|
||||
elements.testTmBtn.innerHTML = '<span class="loading-spinner"></span> 测试中...';
|
||||
|
||||
try {
|
||||
const result = await api.post('/settings/team-manager/test', {
|
||||
api_url: apiUrl,
|
||||
api_key: keyToTest
|
||||
});
|
||||
if (result.success) {
|
||||
toast.success(result.message);
|
||||
} else {
|
||||
toast.error(result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('测试失败: ' + error.message);
|
||||
} finally {
|
||||
elements.testTmBtn.disabled = false;
|
||||
elements.testTmBtn.textContent = '🔌 测试连接';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,7 +351,8 @@ const statusMap = {
|
||||
service: {
|
||||
tempmail: 'Tempmail.lol',
|
||||
outlook: 'Outlook',
|
||||
custom_domain: '自定义域名'
|
||||
custom_domain: '自定义域名',
|
||||
temp_mail: 'Temp-Mail(自部署)'
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user