mirror of
https://github.com/cnlimiter/codex-register.git
synced 2026-05-07 05:02:53 +08:00
feat(pay): 支付跳转功能
- 账号管理:补充订阅状态管理、TeamManager上传说明 - 新增「支付升级」功能模块描述 - 系统设置:补充 CPA配置和 TeamManager配置项
This commit is contained in:
@@ -24,6 +24,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'),
|
||||
@@ -88,6 +90,12 @@ function initEventListeners() {
|
||||
// 批量上传CPA
|
||||
elements.batchUploadCpaBtn.addEventListener('click', handleBatchUploadCpa);
|
||||
|
||||
// 批量检测订阅
|
||||
elements.batchCheckSubBtn.addEventListener('click', handleBatchCheckSubscription);
|
||||
|
||||
// 批量上传TM
|
||||
elements.batchUploadTmBtn.addEventListener('click', handleBatchUploadTm);
|
||||
|
||||
// 批量删除
|
||||
elements.batchDeleteBtn.addEventListener('click', handleBatchDelete);
|
||||
|
||||
@@ -283,6 +291,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 +307,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>
|
||||
@@ -351,12 +372,16 @@ function updateBatchButtons() {
|
||||
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
|
||||
@@ -665,3 +690,90 @@ 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() {
|
||||
if (selectedAccounts.size === 0) return;
|
||||
const confirmed = await confirm(`确定要检测选中的 ${selectedAccounts.size} 个账号的订阅状态吗?`);
|
||||
if (!confirmed) return;
|
||||
|
||||
elements.batchCheckSubBtn.disabled = true;
|
||||
elements.batchCheckSubBtn.textContent = '检测中...';
|
||||
|
||||
try {
|
||||
const result = await api.post('/payment/accounts/batch-check-subscription', {
|
||||
ids: Array.from(selectedAccounts)
|
||||
});
|
||||
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() {
|
||||
if (selectedAccounts.size === 0) return;
|
||||
const confirmed = await confirm(`确定要将选中的 ${selectedAccounts.size} 个账号上传到 Team Manager 吗?`);
|
||||
if (!confirmed) return;
|
||||
|
||||
elements.batchUploadTmBtn.disabled = true;
|
||||
elements.batchUploadTmBtn.textContent = '上传中...';
|
||||
|
||||
try {
|
||||
const result = await api.post('/payment/accounts/batch-upload-tm', {
|
||||
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 (e) {
|
||||
toast.error('批量上传失败: ' + e.message);
|
||||
} finally {
|
||||
updateBatchButtons();
|
||||
}
|
||||
}
|
||||
|
||||
122
static/js/payment.js
Normal file
122
static/js/payment.js
Normal file
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* 支付页面 JavaScript
|
||||
*/
|
||||
|
||||
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 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 body = {
|
||||
account_id: parseInt(accountId),
|
||||
plan_type: selectedPlan,
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
// 复制链接
|
||||
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');
|
||||
});
|
||||
}
|
||||
|
||||
// 无痕打开浏览器
|
||||
async function openIncognito() {
|
||||
if (!generatedLink) {
|
||||
ui.showToast('请先生成链接', 'warning');
|
||||
return;
|
||||
}
|
||||
const statusEl = document.getElementById('open-status');
|
||||
statusEl.textContent = '正在打开...';
|
||||
try {
|
||||
const resp = await fetch('/api/payment/open-incognito', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ url: generatedLink }),
|
||||
});
|
||||
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 设置
|
||||
@@ -231,6 +234,14 @@ function initEventListeners() {
|
||||
if (elements.outlookSettingsForm) {
|
||||
elements.outlookSettingsForm.addEventListener('submit', handleSaveOutlookSettings);
|
||||
}
|
||||
|
||||
// Team Manager 设置
|
||||
if (elements.tmForm) {
|
||||
elements.tmForm.addEventListener('submit', handleSaveTm);
|
||||
}
|
||||
if (elements.testTmBtn) {
|
||||
elements.testTmBtn.addEventListener('click', handleTestTm);
|
||||
}
|
||||
}
|
||||
|
||||
// 加载设置
|
||||
@@ -268,6 +279,8 @@ async function loadSettings() {
|
||||
loadCpaSettings();
|
||||
// 加载 Outlook 设置
|
||||
loadOutlookSettings();
|
||||
// 加载 Team Manager 设置
|
||||
loadTmSettings();
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载设置失败:', error);
|
||||
@@ -1066,3 +1079,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 = '🔌 测试连接';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user