const API_BASE = '/api';
const REFRESH_INTERVAL = 3000;
let autoRefreshTimer = null;
// Admin Password Management
let adminPassword = null;
const ADMIN_STORAGE_KEY = 'zencoder_admin_pass';
// State
let currentState = {
page: 1,
size: 10,
category: 'normal',
total: 0,
selectedIds: new Set(),
items: []
};
// --- Admin Password Management ---
function savePassword(password) {
try {
localStorage.setItem(ADMIN_STORAGE_KEY, password);
console.log('Password saved to localStorage');
return true;
} catch (e) {
console.error('Failed to save password to localStorage:', e);
return false;
}
}
function getSavedPassword() {
try {
const saved = localStorage.getItem(ADMIN_STORAGE_KEY);
if (saved) {
console.log('Found saved password in localStorage');
}
return saved;
} catch (e) {
console.error('Failed to get password from localStorage:', e);
return null;
}
}
function clearSavedPassword() {
try {
localStorage.removeItem(ADMIN_STORAGE_KEY);
} catch (e) {
console.error('Failed to clear password from localStorage:', e);
}
}
async function verifyAdminPassword(password) {
try {
// 尝试调用一个需要管理密码的API来验证
const response = await fetch(`${API_BASE}/accounts?page=1&size=1`, {
headers: {
'X-Admin-Password': password
}
});
return response.ok;
} catch (e) {
return false;
}
}
function showAdminLogin() {
document.getElementById('adminPasswordModal').classList.remove('hidden');
document.getElementById('mainApp').classList.add('hidden');
document.getElementById('adminPassword').focus();
}
function hideAdminLogin() {
document.getElementById('adminPasswordModal').classList.add('hidden');
document.getElementById('mainApp').classList.remove('hidden');
document.getElementById('mainApp').classList.add('flex');
}
async function handleAdminLogin(password, remember = false) {
console.log('Attempting login, remember:', remember);
const isValid = await verifyAdminPassword(password);
if (isValid) {
adminPassword = password;
if (remember) {
const saved = savePassword(password);
if (!saved) {
console.warn('Failed to save password to localStorage');
}
} else {
// 如果没有勾选记住,清除之前保存的密码
clearSavedPassword();
}
hideAdminLogin();
document.getElementById('passwordError').classList.add('hidden');
// 开始加载数据
initializeApp();
return true;
} else {
document.getElementById('passwordError').classList.remove('hidden');
return false;
}
}
function logout() {
adminPassword = null;
clearSavedPassword();
// 停止自动刷新
if (autoRefreshTimer) {
clearInterval(autoRefreshTimer);
autoRefreshTimer = null;
}
// 显示登录界面
showAdminLogin();
}
async function initAdminAuth() {
// 检查是否有保存的密码
const savedPassword = getSavedPassword();
if (savedPassword) {
console.log('Found saved password, attempting auto-login...');
// 直接设置密码,不验证(因为验证可能由于网络问题失败)
adminPassword = savedPassword;
// 尝试验证密码
try {
const isValid = await verifyAdminPassword(savedPassword);
if (isValid) {
console.log('Saved password validated successfully');
hideAdminLogin();
initializeApp();
return;
} else {
console.log('Saved password validation failed');
// 密码无效,清除并显示登录界面
adminPassword = null;
clearSavedPassword();
}
} catch (e) {
console.log('Password validation error, keeping saved password:', e);
// 网络错误时仍然保留密码并尝试使用
hideAdminLogin();
initializeApp();
return;
}
}
// 显示登录界面
showAdminLogin();
}
function toggleAdminPasswordVisibility() {
const input = document.getElementById('adminPassword');
const eyeIcon = document.getElementById('adminEyeIcon');
if (input.type === 'password') {
input.type = 'text';
eyeIcon.innerHTML = '';
} else {
input.type = 'password';
eyeIcon.innerHTML = '';
}
}
// Admin Password Form Handler
document.addEventListener('DOMContentLoaded', function() {
const adminForm = document.getElementById('adminPasswordForm');
if (adminForm) {
// 检查并恢复记住密码的勾选状态
const hasSavedPassword = getSavedPassword() !== null;
if (hasSavedPassword) {
const rememberCheckbox = document.getElementById('rememberPassword');
if (rememberCheckbox) {
rememberCheckbox.checked = true;
}
}
adminForm.addEventListener('submit', async (e) => {
e.preventDefault();
const password = document.getElementById('adminPassword').value.trim();
const remember = document.getElementById('rememberPassword').checked;
const btn = document.getElementById('adminLoginBtn');
const btnText = document.getElementById('adminBtnText');
const btnLoading = document.getElementById('adminBtnLoading');
if (!password) {
document.getElementById('passwordError').textContent = '请输入管理密码';
document.getElementById('passwordError').classList.remove('hidden');
return;
}
btn.disabled = true;
btnText.textContent = '验证中...';
btnLoading.classList.remove('hidden');
const success = await handleAdminLogin(password, remember);
btn.disabled = false;
btnText.textContent = '验证';
btnLoading.classList.add('hidden');
if (success) {
document.getElementById('adminPassword').value = '';
}
});
}
});
function getAuthHeaders() {
const headers = {};
if (adminPassword) {
headers['X-Admin-Password'] = adminPassword;
}
return headers;
}
// --- Theme Management ---
function initTheme() {
const isDark = localStorage.theme === 'dark' ||
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches);
if (isDark) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
updateThemeIcons(isDark);
}
function toggleTheme() {
const isDark = document.documentElement.classList.toggle('dark');
localStorage.theme = isDark ? 'dark' : 'light';
updateThemeIcons(isDark);
}
function updateThemeIcons(isDark) {
const sun = document.getElementById('sunIcon');
const moon = document.getElementById('moonIcon');
if (isDark) {
sun.classList.remove('hidden');
moon.classList.add('hidden');
} else {
sun.classList.add('hidden');
moon.classList.remove('hidden');
}
}
document.getElementById('themeToggle').addEventListener('click', toggleTheme);
// --- Password Visibility ---
function togglePasswordVisibility() {
const input = document.getElementById('client_secret');
const eyeIcon = document.getElementById('eyeIcon');
if (input.type === 'password') {
input.type = 'text';
eyeIcon.innerHTML = '';
} else {
input.type = 'password';
eyeIcon.innerHTML = '';
}
}
// --- Data Logic ---
const PLAN_LIMITS = { Free: 30, Starter: 280, Core: 750, Advanced: 1900, Max: 4200 };
function getStatusConfig(acc) {
switch (acc.status) {
case 'banned':
return { text: '已封禁', class: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400', dot: 'bg-red-500' };
case 'error':
return { text: '异常', class: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400', dot: 'bg-yellow-500' };
case 'cooling':
return { text: '冷却中', class: 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400', dot: 'bg-orange-500' };
case 'disabled':
return { text: '已禁用', class: 'bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400', dot: 'bg-gray-500' };
default:
return { text: '正常', class: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400', dot: 'bg-green-500' };
}
}
function getTokenStatusConfig(record) {
switch (record.status) {
case 'banned':
return { text: '已封禁', class: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400', dot: 'bg-red-500' };
case 'expired':
return { text: '已过期', class: 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400', dot: 'bg-orange-500' };
case 'disabled':
return { text: '已禁用', class: 'bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400', dot: 'bg-gray-500' };
default:
return { text: '正常', class: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400', dot: 'bg-green-500' };
}
}
function formatDate(dateStr) {
if (!dateStr || dateStr.startsWith('0001')) return '-';
const d = new Date(dateStr);
return d.toLocaleDateString('zh-CN');
}
function formatLastUsed(dateStr) {
if (!dateStr || dateStr.startsWith('0001')) return '从未';
const date = new Date(dateStr);
const now = new Date();
const diff = now - date;
// 转换为秒、分钟、小时、天
const seconds = Math.floor(diff / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days > 0) {
return `${days}天前`;
} else if (hours > 0) {
return `${hours}小时前`;
} else if (minutes > 0) {
return `${minutes}分钟前`;
} else if (seconds > 0) {
return `${seconds}秒前`;
} else {
return '刚刚';
}
}
// 格式化积分刷新时间显示
function formatCreditRefresh(creditRefreshTimeStr) {
if (!creditRefreshTimeStr || creditRefreshTimeStr.startsWith('0001')) {
return { text: '未知', class: 'text-gray-400', detail: '' };
}
const refreshTime = new Date(creditRefreshTimeStr);
const now = new Date();
const diffMs = refreshTime - now;
if (diffMs < 0) {
// 已过期,需要刷新
const daysPast = Math.floor(-diffMs / (1000 * 60 * 60 * 24));
const hoursPast = Math.floor(-diffMs / (1000 * 60 * 60));
if (daysPast > 0) {
return {
text: '需要刷新',
class: 'text-red-600 dark:text-red-400',
detail: `${daysPast}天前过期`
};
} else if (hoursPast > 0) {
return {
text: '需要刷新',
class: 'text-red-600 dark:text-red-400',
detail: `${hoursPast}小时前过期`
};
} else {
return {
text: '需要刷新',
class: 'text-red-600 dark:text-red-400',
detail: '刚过期'
};
}
} else if (diffMs < 1000 * 60 * 60) {
// 1小时内刷新
const minutes = Math.floor(diffMs / (1000 * 60));
return {
text: `${minutes}分钟后`,
class: 'text-orange-600 dark:text-orange-400',
detail: '即将刷新'
};
} else if (diffMs < 1000 * 60 * 60 * 24) {
// 24小时内刷新
const hours = Math.floor(diffMs / (1000 * 60 * 60));
return {
text: `${hours}小时后`,
class: 'text-yellow-600 dark:text-yellow-400',
detail: '今日刷新'
};
} else {
// 超过1天
const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
return {
text: `${days}天后`,
class: 'text-green-600 dark:text-green-400',
detail: refreshTime.toLocaleDateString('zh-CN') + ' ' + refreshTime.toLocaleTimeString('zh-CN', {hour: '2-digit', minute: '2-digit'})
};
}
}
async function loadAccounts(isAutoRefresh = false) {
try {
const params = new URLSearchParams({
page: currentState.page,
size: currentState.size,
status: currentState.category // map category to status param
});
const resp = await fetch(`${API_BASE}/accounts?${params}`, {
headers: getAuthHeaders()
});
if (!resp.ok) throw new Error('Failed to fetch');
const data = await resp.json();
// Handle both old and new API response formats temporarily if needed, but we know it's new
const items = data.items || [];
const total = data.total || 0;
currentState.items = items;
currentState.total = total;
// Retain selection if items still exist
const newSet = new Set();
items.forEach(item => {
if (currentState.selectedIds.has(item.id)) {
newSet.add(item.id);
}
});
currentState.selectedIds = newSet;
renderAccounts(items);
updatePaginationUI();
updateTabsUI();
updateBatchUI();
if (data.stats) {
updateStatsUI(data.stats);
}
} catch (e) {
if (!isAutoRefresh) console.error("Failed to load accounts", e);
}
}
function updateStatsUI(stats) {
if (!stats) return;
document.getElementById('stat-total-accounts').textContent = stats.total_accounts;
document.getElementById('stat-active-accounts').textContent = stats.active_accounts;
document.getElementById('stat-cooling-accounts').textContent = stats.cooling_accounts || 0;
document.getElementById('stat-banned-accounts').textContent = stats.banned_accounts;
document.getElementById('stat-error-accounts').textContent = stats.error_accounts;
document.getElementById('stat-disabled-accounts').textContent = stats.disabled_accounts || 0;
document.getElementById('stat-today-usage').textContent = stats.today_usage.toFixed(2);
document.getElementById('stat-total-usage').textContent = stats.total_usage.toFixed(2);
}
function renderAccounts(accounts) {
const tbody = document.getElementById('accountList');
const emptyState = document.getElementById('emptyState');
const tableContainer = document.getElementById('tableContainer');
const paginationContainer = document.getElementById('paginationContainer');
const selectAll = document.getElementById('selectAll');
if (accounts.length === 0) {
tbody.innerHTML = '';
tableContainer.classList.add('hidden');
paginationContainer.classList.add('hidden');
emptyState.classList.remove('hidden');
emptyState.classList.add('flex');
selectAll.checked = false;
selectAll.disabled = true;
return;
}
tableContainer.classList.remove('hidden');
paginationContainer.classList.remove('hidden');
emptyState.classList.add('hidden');
emptyState.classList.remove('flex');
selectAll.disabled = false;
selectAll.checked = accounts.length > 0 && accounts.every(a => currentState.selectedIds.has(a.id));
const html = accounts.map(acc => {
const status = getStatusConfig(acc);
const limit = PLAN_LIMITS[acc.plan_type] || 30;
const subDate = formatDate(acc.subscription_start_date);
const emailOrId = acc.email ? acc.email : acc.client_id;
const shortId = acc.client_id.length > 12 ? acc.client_id.substring(0, 12) + '...' : acc.client_id;
const usagePercent = Math.min((acc.daily_used / limit) * 100, 100);
const isSelected = currentState.selectedIds.has(acc.id);
const lastUsedText = formatLastUsed(acc.last_used);
// 格式化Token过期时间
const formatTokenExpiry = (tokenExpiryStr) => {
if (!tokenExpiryStr || tokenExpiryStr.startsWith('0001')) {
return { text: '未知', class: 'text-gray-400', detail: '' };
}
const expiryDate = new Date(tokenExpiryStr);
const now = new Date();
const diffMs = expiryDate - now;
if (diffMs < 0) {
// 已过期
const daysPast = Math.floor(-diffMs / (1000 * 60 * 60 * 24));
return {
text: '已过期',
class: 'text-red-600 dark:text-red-400',
detail: `${daysPast}天前过期`
};
} else if (diffMs < 1000 * 60 * 60) {
// 1小时内过期
const minutes = Math.floor(diffMs / (1000 * 60));
return {
text: `${minutes}分钟后`,
class: 'text-red-500 dark:text-red-400',
detail: '即将过期'
};
} else if (diffMs < 1000 * 60 * 60 * 24) {
// 24小时内过期
const hours = Math.floor(diffMs / (1000 * 60 * 60));
return {
text: `${hours}小时后`,
class: 'text-orange-600 dark:text-orange-400',
detail: '今日过期'
};
} else {
// 超过1天
const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
return {
text: `${days}天后`,
class: 'text-green-600 dark:text-green-400',
detail: expiryDate.toLocaleDateString('zh-CN')
};
}
};
const tokenExpiry = formatTokenExpiry(acc.token_expiry);
return `
|
|
${acc.id}
|
${emailOrId}
ID: ${shortId}
|
${acc.plan_type}
自: ${subDate}
|
${acc.daily_used.toFixed(2)}
/ ${limit}
总计: ${acc.total_used.toFixed(2)}
|
${lastUsedText}
${acc.last_used && !acc.last_used.startsWith('0001') ?
` ${new Date(acc.last_used).toLocaleTimeString('zh-CN', {hour: '2-digit', minute: '2-digit'})} `
: ''}
|
${tokenExpiry.text}
${tokenExpiry.detail ? ` ${tokenExpiry.detail} ` : ''}
|
${formatCreditRefresh(acc.credit_refresh_time).text}
${formatCreditRefresh(acc.credit_refresh_time).detail ? ` ${formatCreditRefresh(acc.credit_refresh_time).detail} ` : ''}
|
${status.text}
${acc.status === 'cooling' && acc.cooling_until && !acc.cooling_until.startsWith('0001') ?
(() => {
const coolingDate = new Date(acc.cooling_until);
const now = new Date();
const diffMs = coolingDate - now;
if (diffMs > 0) {
const hours = Math.floor(diffMs / (1000 * 60 * 60));
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
let timeText = '';
if (hours > 0) {
timeText = `${hours}小时${minutes}分钟后解除`;
} else {
timeText = `${minutes}分钟后解除`;
}
return ``;
} else {
return ` 即将解除 `;
}
})()
: acc.ban_reason ? `${acc.ban_reason} ` : ''}
|
|
`;
}).join('');
// Optimization: Only update if content changed
if (tbody.innerHTML !== html) {
tbody.innerHTML = html;
}
}
// --- Interactions ---
function switchCategory(cat) {
currentState.category = cat;
currentState.page = 1;
currentState.selectedIds.clear();
loadAccounts();
}
function updateTabsUI() {
['normal', 'banned', 'cooling', 'disabled', 'error'].forEach(cat => {
const btn = document.getElementById(`tab-${cat}`);
if (currentState.category === cat) {
btn.className = "px-3 py-1.5 text-xs font-medium rounded-md transition-all bg-white dark:bg-gray-600 text-gray-900 dark:text-white shadow-sm";
} else {
btn.className = "px-3 py-1.5 text-xs font-medium rounded-md transition-all text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white";
}
});
}
function changePage(delta) {
const newPage = currentState.page + delta;
if (newPage > 0 && newPage <= Math.ceil(currentState.total / currentState.size)) {
currentState.page = newPage;
loadAccounts();
}
}
function updatePaginationUI() {
const totalPages = Math.ceil(currentState.total / currentState.size);
document.getElementById('pageStart').textContent = currentState.total === 0 ? 0 : (currentState.page - 1) * currentState.size + 1;
document.getElementById('pageEnd').textContent = Math.min(currentState.page * currentState.size, currentState.total);
document.getElementById('totalItems').textContent = currentState.total;
document.getElementById('prevPage').disabled = currentState.page <= 1;
document.getElementById('nextPage').disabled = currentState.page >= totalPages;
}
function toggleSelectAll() {
const selectAll = document.getElementById('selectAll');
if (selectAll.checked) {
currentState.items.forEach(item => currentState.selectedIds.add(item.id));
} else {
currentState.selectedIds.clear();
}
renderAccounts(currentState.items); // re-render to show selection state
updateBatchUI();
}
function toggleSelect(id) {
if (currentState.selectedIds.has(id)) {
currentState.selectedIds.delete(id);
} else {
currentState.selectedIds.add(id);
}
renderAccounts(currentState.items);
updateBatchUI();
}
function updateBatchUI() {
const batchActions = document.getElementById('batchActions');
const countSpan = document.getElementById('selectedCount');
const count = currentState.selectedIds.size;
// 始终显示批量操作区域
batchActions.classList.remove('hidden');
batchActions.classList.add('flex');
// 根据当前tab和选中状态显示不同的按钮
const buttonsHtml = getBatchButtonsHtml(currentState.category, count);
// 更新按钮区域内容
if (count > 0) {
countSpan.textContent = `${count} 选中`;
countSpan.classList.remove('hidden');
} else {
countSpan.classList.add('hidden');
}
// 更新按钮容器
const buttonsContainer = document.getElementById('batchButtonsContainer');
if (buttonsContainer) {
buttonsContainer.innerHTML = buttonsHtml;
}
}
function getBatchButtonsHtml(category, selectedCount) {
// 根据是否有选中数据,决定使用哪个函数
const moveHandler = selectedCount > 0 ? 'batchMove' : 'oneClickMove';
const deleteHandler = selectedCount > 0 ? 'batchDelete(false)' : 'batchDelete(true)';
// 统一的按钮布局,只隐藏当前tab对应的按钮
return `
`;
}
async function oneClickMove(targetCategory) {
// 一键移动当前分类的所有账号到目标分类
if (currentState.category === targetCategory) {
alert('当前已在目标分类中');
return;
}
const confirmMsg = `确定要将当前分类"${getCategoryName(currentState.category)}"的所有账号移至"${getCategoryName(targetCategory)}"吗?`;
if (!confirm(confirmMsg)) return;
try {
const resp = await fetch(`${API_BASE}/accounts/batch/move-all`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...getAuthHeaders()
},
body: JSON.stringify({
from_status: currentState.category,
to_status: targetCategory
})
});
if (!resp.ok) throw new Error('One-click move failed');
const result = await resp.json();
alert(`成功移动 ${result.moved_count || 0} 个账号`);
// 刷新当前页面
loadAccounts();
} catch (e) {
alert('一键操作失败: ' + e.message);
}
}
function getCategoryName(category) {
const names = {
'normal': '正常',
'cooling': '冷却',
'banned': '封禁',
'disabled': '禁用',
'error': '异常'
};
return names[category] || category;
}
async function batchMove(category) {
if (currentState.selectedIds.size === 0) return;
const ids = Array.from(currentState.selectedIds);
try {
const resp = await fetch(`${API_BASE}/accounts/batch/category`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...getAuthHeaders()
},
body: JSON.stringify({ ids, status: category }) // send as status
});
if (!resp.ok) throw new Error('Batch update failed');
currentState.selectedIds.clear();
loadAccounts();
} catch (e) {
alert('批量操作失败');
}
}
// 批量删除功能
async function batchDelete(deleteAll = false) {
let confirmMsg;
let requestData;
if (deleteAll) {
// 删除当前分类的所有账号
confirmMsg = `确定要删除当前分类"${getCategoryName(currentState.category)}"的所有账号吗?此操作不可逆转!`;
requestData = {
delete_all: true,
status: currentState.category
};
} else {
// 删除选中的账号
if (currentState.selectedIds.size === 0) {
alert('请先选择要删除的账号');
return;
}
confirmMsg = `确定要删除选中的 ${currentState.selectedIds.size} 个账号吗?此操作不可逆转!`;
requestData = {
ids: Array.from(currentState.selectedIds)
};
}
if (!confirm(confirmMsg)) return;
try {
const resp = await fetch(`${API_BASE}/accounts/batch/delete`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...getAuthHeaders()
},
body: JSON.stringify(requestData)
});
if (!resp.ok) {
const err = await resp.json();
throw new Error(err.error || 'Delete failed');
}
const result = await resp.json();
alert(`成功删除 ${result.deleted_count || 0} 个账号`);
// 清空选择并刷新页面
currentState.selectedIds.clear();
loadAccounts();
} catch (e) {
alert('批量删除失败: ' + e.message);
}
}
// 批量刷新Token功能
async function batchRefreshToken(refreshAll = false) {
let confirmMsg;
let requestData;
if (refreshAll) {
confirmMsg = "确定要刷新所有正常账号的Token吗?此操作可能需要较长时间。";
requestData = { all: true };
} else {
if (currentState.selectedIds.size === 0) {
alert('请先选择要刷新Token的账号');
return;
}
confirmMsg = `确定要刷新选中的 ${currentState.selectedIds.size} 个账号的Token吗?`;
requestData = { ids: Array.from(currentState.selectedIds) };
}
if (!confirm(confirmMsg)) return;
try {
// 显示进度弹窗
showRefreshProgressModal();
addRefreshProgressLog('开始批量刷新Token...', 'info');
const resp = await fetch(`${API_BASE}/accounts/batch/refresh-token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...getAuthHeaders()
},
body: JSON.stringify(requestData)
});
if (!resp.ok) {
const err = await resp.json();
throw new Error(err.error || 'Unknown error');
}
// 处理流式响应
const reader = resp.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let successCount = 0;
let failCount = 0;
let total = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop(); // 保留不完整的行
for (const line of lines) {
if (!line.trim() || !line.startsWith('data: ')) continue;
try {
const jsonStr = line.substring(6); // 移除 "data: " 前缀
const event = JSON.parse(jsonStr);
switch (event.type) {
case 'start':
total = event.total;
updateRefreshProgress(0, total, '开始刷新');
addRefreshProgressLog(`准备刷新 ${total} 个账号的Token`, 'info');
break;
case 'success':
successCount++;
updateRefreshProgress(successCount + failCount, total, '刷新中');
addRefreshProgressLog(`✓ [${event.index}/${total}] 刷新成功: ${event.account_id}${event.email ? ` (${event.email})` : ''}`, 'success');
break;
case 'error':
failCount++;
updateRefreshProgress(successCount + failCount, total, '刷新中');
addRefreshProgressLog(`✗ [${event.index}/${total}] ${event.message} (${event.account_id})`, 'error');
break;
case 'complete':
updateRefreshProgress(total, total, '完成');
addRefreshProgressLog(`批量刷新完成!成功 ${event.success} 个,失败 ${event.fail} 个`, 'info');
showRefreshProgressSummary(event.success, event.fail);
break;
}
} catch (e) {
console.error('解析事件失败:', e, line);
}
}
}
return true;
} catch (e) {
addRefreshProgressLog(`错误: ${e.message}`, 'error');
document.getElementById('refreshProgressCloseBtn').classList.remove('hidden');
return false;
}
}
// 刷新进度弹窗管理
function showRefreshProgressModal() {
document.getElementById('refreshProgressModal').classList.remove('hidden');
document.getElementById('refreshProgressLog').innerHTML = '';
document.getElementById('refreshProgressBar').style.width = '0%';
document.getElementById('refreshProgressText').textContent = '准备中...';
document.getElementById('refreshProgressCount').textContent = '0/0';
document.getElementById('refreshProgressSummary').classList.add('hidden');
document.getElementById('refreshProgressCloseBtn').classList.add('hidden');
}
function closeRefreshProgressModal() {
document.getElementById('refreshProgressModal').classList.add('hidden');
loadAccounts(); // 刷新账号列表
currentState.selectedIds.clear(); // 清空选择
updateBatchUI(); // 更新批量操作UI
}
function addRefreshProgressLog(message, type = 'info') {
const log = document.getElementById('refreshProgressLog');
const colors = {
info: 'text-gray-600 dark:text-gray-400',
success: 'text-green-600 dark:text-green-400',
error: 'text-red-600 dark:text-red-400',
warning: 'text-yellow-600 dark:text-yellow-400'
};
const entry = document.createElement('div');
entry.className = colors[type] || colors.info;
entry.textContent = message;
log.appendChild(entry);
// 自动滚动到底部
log.scrollTop = log.scrollHeight;
}
function updateRefreshProgress(current, total, text) {
const percentage = total > 0 ? (current / total) * 100 : 0;
document.getElementById('refreshProgressBar').style.width = `${percentage}%`;
document.getElementById('refreshProgressText').textContent = text;
document.getElementById('refreshProgressCount').textContent = `${current}/${total}`;
}
function showRefreshProgressSummary(success, fail) {
document.getElementById('refreshSummarySuccess').textContent = success;
document.getElementById('refreshSummaryFail').textContent = fail;
document.getElementById('refreshProgressSummary').classList.remove('hidden');
document.getElementById('refreshProgressCloseBtn').classList.remove('hidden');
}
async function addAccount(data) {
const btn = document.getElementById('submitBtn');
const btnText = document.getElementById('btnText');
const btnLoading = document.getElementById('btnLoading');
btn.disabled = true;
btnLoading.classList.remove('hidden');
try {
// 生成模式使用流式传输
if (data.generate_mode) {
btnText.textContent = '生成中...';
const resp = await fetch(`${API_BASE}/accounts`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...getAuthHeaders()
},
body: JSON.stringify(data)
});
if (!resp.ok) {
const err = await resp.json();
throw new Error(err.error || 'Unknown error');
}
// 处理流式响应
const reader = resp.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let successCount = 0;
let failCount = 0;
let total = 0;
let firstSuccessAccount = null; // 记录第一个成功的账号信息
let isProgressShown = false;
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop(); // 保留不完整的行
for (const line of lines) {
if (!line.trim() || !line.startsWith('data: ')) continue;
try {
const jsonStr = line.substring(6); // 移除 "data: " 前缀
const event = JSON.parse(jsonStr);
switch (event.type) {
case 'start':
total = event.total;
// 如果是单个账号生成,不显示进度窗口
if (total > 1) {
showProgressModal();
updateProgress(0, total, '开始生成');
addProgressLog('开始批量生成凭证...', 'info');
addProgressLog(`准备生成 ${total} 个凭证`, 'info');
isProgressShown = true;
}
break;
case 'success':
successCount++;
// 记录第一个成功的账号信息(单个生成时使用)
if (!firstSuccessAccount && event.email) {
firstSuccessAccount = {
email: event.email,
plan: event.plan,
token_expiry: event.token_expiry,
subscription_start_date: event.subscription_start_date
};
}
if (isProgressShown) {
updateProgress(successCount + failCount, total, '生成中');
const action = event.action === 'created' ? '创建' : '更新';
addProgressLog(`✓ [${event.index}/${total}] ${action}成功: ${event.email} (${event.plan})`, 'success');
}
break;
case 'error':
failCount++;
if (isProgressShown) {
updateProgress(successCount + failCount, total, '生成中');
const clientInfo = event.client_id ? ` (${event.client_id})` : '';
addProgressLog(`✗ [${event.index}/${total}] ${event.message}${clientInfo}`, 'error');
}
break;
case 'complete':
if (isProgressShown) {
// 批量生成完成
updateProgress(total, total, '完成');
addProgressLog(`批量生成完成!成功 ${event.success} 个,失败 ${event.fail} 个`, 'info');
showProgressSummary(event.success, event.fail);
} else {
// 单个账号生成完成
if (firstSuccessAccount) {
// 显示账号信息弹窗
showAccountInfoModal(firstSuccessAccount);
} else if (failCount > 0) {
// 生成失败
showToast('账号生成失败', 'error');
}
}
break;
}
} catch (e) {
console.error('解析事件失败:', e, line);
}
}
}
return true;
} else {
// 凭证模式使用普通请求
btnText.textContent = '添加中...';
const resp = await fetch(`${API_BASE}/accounts`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...getAuthHeaders()
},
body: JSON.stringify(data)
});
if (!resp.ok) {
const err = await resp.json();
throw new Error(err.error || 'Unknown error');
}
loadAccounts();
return true;
}
} catch (e) {
if (data.generate_mode) {
showToast(`生成失败: ${e.message}`, 'error');
} else {
alert('操作失败: ' + e.message);
}
return false;
} finally {
btn.disabled = false;
btnText.textContent = currentAddMode === 'credential' ? '添加账号' : '批量生成';
btnLoading.classList.add('hidden');
}
}
async function deleteAccount(id) {
if (!confirm('确定要删除此账号吗?')) return;
try {
await fetch(`${API_BASE}/accounts/${id}`, {
method: 'DELETE',
headers: getAuthHeaders()
});
loadAccounts();
} catch (e) {
alert('删除失败');
}
}
async function toggleAccount(id) {
try {
await fetch(`${API_BASE}/accounts/${id}/toggle`, {
method: 'POST',
headers: getAuthHeaders()
});
loadAccounts();
} catch (e) {
alert('操作失败');
}
}
// --- Progress Modal Management ---
function showProgressModal() {
document.getElementById('progressModal').classList.remove('hidden');
document.getElementById('progressLog').innerHTML = '';
document.getElementById('progressBar').style.width = '0%';
document.getElementById('progressText').textContent = '准备中...';
document.getElementById('progressCount').textContent = '0/0';
document.getElementById('progressSummary').classList.add('hidden');
document.getElementById('progressCloseBtn').classList.add('hidden');
}
function closeProgressModal() {
document.getElementById('progressModal').classList.add('hidden');
loadAccounts(); // 刷新账号列表
}
function addProgressLog(message, type = 'info') {
const log = document.getElementById('progressLog');
const colors = {
info: 'text-gray-600 dark:text-gray-400',
success: 'text-green-600 dark:text-green-400',
error: 'text-red-600 dark:text-red-400',
warning: 'text-yellow-600 dark:text-yellow-400'
};
const entry = document.createElement('div');
entry.className = colors[type] || colors.info;
entry.textContent = message;
log.appendChild(entry);
// 自动滚动到底部
log.scrollTop = log.scrollHeight;
}
function updateProgress(current, total, text) {
const percentage = total > 0 ? (current / total) * 100 : 0;
document.getElementById('progressBar').style.width = `${percentage}%`;
document.getElementById('progressText').textContent = text;
document.getElementById('progressCount').textContent = `${current}/${total}`;
}
function showProgressSummary(success, fail) {
document.getElementById('summarySuccess').textContent = success;
document.getElementById('summaryFail').textContent = fail;
document.getElementById('progressSummary').classList.remove('hidden');
document.getElementById('progressCloseBtn').classList.remove('hidden');
}
// --- Add Mode Management ---
let currentAddMode = 'credential'; // 'credential' or 'generate'
function switchAddMode(mode) {
currentAddMode = mode;
const credentialBtn = document.getElementById('mode-credential');
const generateBtn = document.getElementById('mode-generate');
const credentialFields = document.getElementById('credentialFields');
const generateFields = document.getElementById('generateFields');
const submitBtn = document.getElementById('btnText');
if (mode === 'credential') {
credentialBtn.classList.add('bg-white', 'dark:bg-gray-600', 'text-gray-900', 'dark:text-white', 'shadow-sm');
credentialBtn.classList.remove('text-gray-500', 'hover:text-gray-900', 'dark:text-gray-400', 'dark:hover:text-white');
generateBtn.classList.remove('bg-white', 'dark:bg-gray-600', 'text-gray-900', 'dark:text-white', 'shadow-sm');
generateBtn.classList.add('text-gray-500', 'hover:text-gray-900', 'dark:text-gray-400', 'dark:hover:text-white');
credentialFields.classList.remove('hidden');
generateFields.classList.add('hidden');
submitBtn.textContent = '添加账号';
} else {
generateBtn.classList.add('bg-white', 'dark:bg-gray-600', 'text-gray-900', 'dark:text-white', 'shadow-sm');
generateBtn.classList.remove('text-gray-500', 'hover:text-gray-900', 'dark:text-gray-400', 'dark:hover:text-white');
credentialBtn.classList.remove('bg-white', 'dark:bg-gray-600', 'text-gray-900', 'dark:text-white', 'shadow-sm');
credentialBtn.classList.add('text-gray-500', 'hover:text-gray-900', 'dark:text-gray-400', 'dark:hover:text-white');
credentialFields.classList.add('hidden');
generateFields.classList.remove('hidden');
submitBtn.textContent = '批量生成';
}
}
// Form Handler
document.getElementById('addForm').addEventListener('submit', async (e) => {
e.preventDefault();
let data;
if (currentAddMode === 'credential') {
const accessToken = document.getElementById('credential_access_token').value.trim();
const refreshToken = document.getElementById('credential_refresh_token').value.trim();
const proxy = document.getElementById('proxy').value.trim();
if (!accessToken && !refreshToken) {
alert('请填写 Access Token 或 Refresh Token(至少一个)');
return;
}
data = {
proxy: proxy,
generate_mode: false
};
// 提交用户填写的所有token字段,后端会根据优先级处理
if (accessToken) {
data.access_token = accessToken;
}
if (refreshToken) {
data.refresh_token = refreshToken;
}
} else {
const masterAccessToken = document.getElementById('generate_access_token').value.trim();
const masterRefreshToken = document.getElementById('generate_refresh_token').value.trim();
const proxy = document.getElementById('proxy').value.trim();
if (!masterAccessToken && !masterRefreshToken) {
alert('请填写 Master Access Token 或 Master Refresh Token(至少一个)');
return;
}
data = {
proxy: proxy,
generate_mode: true
};
// 提交用户填写的所有token字段,后端会根据优先级处理
if (masterAccessToken) {
data.access_token = masterAccessToken;
}
if (masterRefreshToken) {
data.refresh_token = masterRefreshToken;
}
}
if (await addAccount(data)) {
e.target.reset();
if (currentAddMode === 'credential') {
document.getElementById('credential_refresh_token').focus();
} else {
document.getElementById('generate_refresh_token').focus();
}
}
});
// Initialization function for after admin login
function initializeApp() {
loadAccounts();
// Auto Refresh
if (autoRefreshTimer) {
clearInterval(autoRefreshTimer);
}
autoRefreshTimer = setInterval(() => {
loadAccounts(true);
}, REFRESH_INTERVAL);
}
// --- Token Management ---
let currentMainView = 'pool'; // 'pool' or 'token'
let tokenRecords = [];
let generationTasks = [];
function switchMainView(view) {
currentMainView = view;
const poolView = document.getElementById('poolView');
const tokenView = document.getElementById('tokenView');
const poolBtn = document.getElementById('view-pool');
const tokenBtn = document.getElementById('view-token');
if (view === 'pool') {
poolView.classList.remove('hidden');
tokenView.classList.add('hidden');
poolBtn.classList.add('bg-white', 'dark:bg-gray-600', 'text-gray-900', 'dark:text-white', 'shadow-sm');
poolBtn.classList.remove('text-gray-500', 'hover:text-gray-900', 'dark:text-gray-400', 'dark:hover:text-white');
tokenBtn.classList.remove('bg-white', 'dark:bg-gray-600', 'text-gray-900', 'dark:text-white', 'shadow-sm');
tokenBtn.classList.add('text-gray-500', 'hover:text-gray-900', 'dark:text-gray-400', 'dark:hover:text-white');
loadAccounts();
} else {
poolView.classList.add('hidden');
tokenView.classList.remove('hidden');
tokenBtn.classList.add('bg-white', 'dark:bg-gray-600', 'text-gray-900', 'dark:text-white', 'shadow-sm');
tokenBtn.classList.remove('text-gray-500', 'hover:text-gray-900', 'dark:text-gray-400', 'dark:hover:text-white');
poolBtn.classList.remove('bg-white', 'dark:bg-gray-600', 'text-gray-900', 'dark:text-white', 'shadow-sm');
poolBtn.classList.add('text-gray-500', 'hover:text-gray-900', 'dark:text-gray-400', 'dark:hover:text-white');
loadTokenData();
}
}
async function loadTokenData() {
await Promise.all([
loadTokenRecords(),
loadGenerationTasks(),
loadPoolStatus()
]);
}
async function loadTokenRecords() {
try {
const resp = await fetch(`${API_BASE}/tokens`, {
headers: getAuthHeaders()
});
if (!resp.ok) throw new Error('Failed to fetch token records');
const data = await resp.json();
tokenRecords = data.items || [];
renderTokenRecords(tokenRecords);
} catch (e) {
console.error("Failed to load token records", e);
}
}
async function loadGenerationTasks() {
try {
const resp = await fetch(`${API_BASE}/tokens/tasks`, {
headers: getAuthHeaders()
});
if (!resp.ok) throw new Error('Failed to fetch generation tasks');
const data = await resp.json();
generationTasks = data.items || [];
renderGenerationTasks(generationTasks);
} catch (e) {
console.error("Failed to load generation tasks", e);
}
}
async function loadPoolStatus() {
try {
const resp = await fetch(`${API_BASE}/tokens/pool-status`, {
headers: getAuthHeaders()
});
if (!resp.ok) throw new Error('Failed to fetch pool status');
const data = await resp.json();
updateTokenStatsUI(data);
} catch (e) {
console.error("Failed to load pool status", e);
}
}
function updateTokenStatsUI(stats) {
document.getElementById('token-stat-active').textContent = stats.active_tokens || 0;
document.getElementById('token-stat-normal').textContent = stats.normal_accounts || 0;
document.getElementById('token-stat-running').textContent = stats.running_tasks || 0;
}
function renderTokenRecords(records) {
const tbody = document.getElementById('tokenList');
const emptyState = document.getElementById('tokenEmptyState');
if (records.length === 0) {
tbody.innerHTML = '';
emptyState.classList.remove('hidden');
emptyState.classList.add('flex');
return;
}
emptyState.classList.add('hidden');
emptyState.classList.remove('flex');
const html = records.map(record => {
// 格式化订阅日期
const subDate = record.subscription_start_date && !record.subscription_start_date.startsWith('0001')
? new Date(record.subscription_start_date).toLocaleDateString('zh-CN')
: '-';
// 格式化Token过期时间
const tokenExpiryDate = record.token_expiry && !record.token_expiry.startsWith('0001')
? new Date(record.token_expiry)
: null;
let tokenStatusClass, tokenStatusText;
if (!tokenExpiryDate) {
tokenStatusClass = 'bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400';
tokenStatusText = '未知';
} else {
const now = new Date();
const hoursUntilExpiry = (tokenExpiryDate - now) / (1000 * 60 * 60);
if (hoursUntilExpiry < 0) {
tokenStatusClass = 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400';
tokenStatusText = '已过期';
} else if (hoursUntilExpiry < 1) {
tokenStatusClass = 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400';
tokenStatusText = `${Math.floor(hoursUntilExpiry * 60)}分钟后过期`;
} else if (hoursUntilExpiry < 24) {
tokenStatusClass = 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400';
tokenStatusText = `${Math.floor(hoursUntilExpiry)}小时后过期`;
} else {
tokenStatusClass = 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400';
tokenStatusText = `${Math.floor(hoursUntilExpiry / 24)}天后过期`;
}
}
return `
${record.email || '未知邮箱'}
${record.description || `令牌 #${record.id}`}
|
${record.plan_type || 'Free'}
|
${subDate}
|
${getTokenStatusConfig(record).text}
${tokenStatusText}
|
${record.generated_count || 0}
/
${record.threshold}
批次: ${record.generate_batch}
|
${record.total_success || 0}
/
${record.total_fail || 0}
|
|
${record.is_active ? `
` : ''}
${record.has_refresh_token ? `
` : ''}
|
`;
}).join('');
tbody.innerHTML = html;
}
// 添加刷新令牌函数
async function refreshToken(tokenId) {
try {
const resp = await fetch(`${API_BASE}/tokens/${tokenId}/refresh`, {
method: 'POST',
headers: getAuthHeaders()
});
if (!resp.ok) {
const error = await resp.json();
throw new Error(error.error || 'Failed to refresh token');
}
const result = await resp.json();
showToast(result.message || 'Token刷新成功', 'success');
loadTokenRecords();
} catch (e) {
showToast('Token刷新失败: ' + e.message, 'error');
}
}
function renderGenerationTasks(tasks) {
const tbody = document.getElementById('taskList');
if (tasks.length === 0) {
tbody.innerHTML = '| 暂无生成任务记录 |
';
return;
}
const html = tasks.map(task => {
const startTime = task.started_at && !task.started_at.startsWith('0001')
? new Date(task.started_at).toLocaleString('zh-CN')
: '-';
const completeTime = task.completed_at && !task.completed_at.startsWith('0001')
? new Date(task.completed_at).toLocaleString('zh-CN')
: '-';
let statusClass, statusText;
switch (task.status) {
case 'running':
statusClass = 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400';
statusText = '运行中';
break;
case 'completed':
statusClass = 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400';
statusText = '已完成';
break;
case 'failed':
statusClass = 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400';
statusText = '失败';
break;
default:
statusClass = 'bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400';
statusText = '待处理';
}
return `
| #${task.id} |
${task.batch_size} |
${task.success_count}
/
${task.fail_count}
|
${statusText}
|
${startTime} |
${completeTime} |
`;
}).join('');
tbody.innerHTML = html;
}
async function quickToggleAutoGenerate(tokenId, enable) {
try {
const resp = await fetch(`${API_BASE}/tokens/${tokenId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
...getAuthHeaders()
},
body: JSON.stringify({ auto_generate: enable })
});
if (!resp.ok) throw new Error('Failed to update token');
loadTokenRecords();
} catch (e) {
alert('更新失败: ' + e.message);
}
}
async function quickToggleActive(tokenId, enable) {
try {
const resp = await fetch(`${API_BASE}/tokens/${tokenId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
...getAuthHeaders()
},
body: JSON.stringify({ is_active: enable })
});
if (!resp.ok) throw new Error('Failed to update token');
loadTokenRecords();
} catch (e) {
alert('更新失败: ' + e.message);
}
}
async function deleteTokenRecord(tokenId) {
if (!confirm('确定要删除此令牌记录吗?')) return;
try {
const resp = await fetch(`${API_BASE}/tokens/${tokenId}`, {
method: 'DELETE',
headers: getAuthHeaders()
});
if (!resp.ok) throw new Error('Failed to delete token');
const result = await resp.json();
alert(result.message || '删除成功');
loadTokenRecords();
} catch (e) {
alert('删除失败: ' + e.message);
}
}
async function triggerGeneration(tokenId) {
if (!confirm('确定要手动触发生成任务吗?')) return;
try {
const resp = await fetch(`${API_BASE}/tokens/${tokenId}/trigger`, {
method: 'POST',
headers: getAuthHeaders()
});
if (!resp.ok) throw new Error('Failed to trigger generation');
alert('生成任务已触发,请稍后查看结果');
setTimeout(() => {
loadTokenData();
}, 2000);
} catch (e) {
alert('触发失败: ' + e.message);
}
}
let currentEditingToken = null;
function showTokenConfigModal(record) {
currentEditingToken = record;
document.getElementById('tokenConfigModal').classList.remove('hidden');
// 填充表单数据
document.getElementById('configTokenId').value = record.id;
document.getElementById('configDescription').value = record.description || '';
document.getElementById('configThreshold').value = record.threshold || 10;
document.getElementById('configBatch').value = record.generate_batch || 30;
// 设置开关状态
setConfigSwitch('configAutoGenerate', record.auto_generate);
setConfigSwitch('configIsActive', record.is_active);
}
function closeTokenConfigModal() {
document.getElementById('tokenConfigModal').classList.add('hidden');
currentEditingToken = null;
}
function setConfigSwitch(switchId, isOn) {
const switchBtn = document.getElementById(switchId);
const indicator = switchBtn.querySelector('span');
if (isOn) {
switchBtn.classList.remove('bg-gray-200', 'dark:bg-gray-600');
switchBtn.classList.add('bg-primary');
indicator.classList.remove('translate-x-1');
indicator.classList.add('translate-x-6');
switchBtn.dataset.checked = 'true';
} else {
switchBtn.classList.add('bg-gray-200', 'dark:bg-gray-600');
switchBtn.classList.remove('bg-primary');
indicator.classList.add('translate-x-1');
indicator.classList.remove('translate-x-6');
switchBtn.dataset.checked = 'false';
}
}
function toggleConfigSwitch(switchId) {
const switchBtn = document.getElementById(switchId);
const isChecked = switchBtn.dataset.checked === 'true';
setConfigSwitch(switchId, !isChecked);
}
// Token config form handler
document.getElementById('tokenConfigForm').addEventListener('submit', async (e) => {
e.preventDefault();
const tokenId = document.getElementById('configTokenId').value;
const config = {
description: document.getElementById('configDescription').value,
threshold: parseInt(document.getElementById('configThreshold').value),
generate_batch: parseInt(document.getElementById('configBatch').value),
auto_generate: document.getElementById('configAutoGenerate').dataset.checked === 'true',
is_active: document.getElementById('configIsActive').dataset.checked === 'true'
};
try {
const resp = await fetch(`${API_BASE}/tokens/${tokenId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
...getAuthHeaders()
},
body: JSON.stringify(config)
});
if (!resp.ok) throw new Error('Failed to update token config');
closeTokenConfigModal();
loadTokenRecords();
} catch (e) {
alert('更新配置失败: ' + e.message);
}
});
async function refreshTokenData() {
await loadTokenData();
}
// OAuth RT获取功能
function startOAuthForRT() {
// 打开新窗口进行OAuth认证
const width = 600;
const height = 700;
const left = (window.screen.width - width) / 2;
const top = (window.screen.height - height) / 2;
const authWindow = window.open(
'/api/oauth/start-rt',
'ZenCoderOAuth',
`width=${width},height=${height},left=${left},top=${top},toolbar=no,menubar=no,scrollbars=yes,resizable=yes`
);
// 监听OAuth完成消息
window.addEventListener('message', function handleOAuthMessage(event) {
// 验证消息来源
if (event.origin !== window.location.origin) return;
if (event.data.type === 'oauth-rt-complete') {
// 关闭认证窗口
if (authWindow && !authWindow.closed) {
authWindow.close();
}
// 移除事件监听器
window.removeEventListener('message', handleOAuthMessage);
if (event.data.success && (event.data.accessToken || event.data.refreshToken)) {
// 自动填充到对应的输入框
const currentMode = currentAddMode;
if (currentMode === 'credential') {
// 优先填充access_token
if (event.data.accessToken) {
document.getElementById('credential_access_token').value = event.data.accessToken;
}
// 同时填充refresh_token
if (event.data.refreshToken) {
document.getElementById('credential_refresh_token').value = event.data.refreshToken;
}
// 显示成功提示
showToast('Token 获取成功!', 'success');
} else {
// 生成模式
if (event.data.accessToken) {
document.getElementById('generate_access_token').value = event.data.accessToken;
}
if (event.data.refreshToken) {
document.getElementById('generate_refresh_token').value = event.data.refreshToken;
}
showToast('Master Token 获取成功!', 'success');
}
} else {
showToast(event.data.error || 'OAuth认证失败', 'error');
}
}
});
}
// Toast提示功能
function showToast(message, type = 'info') {
// 创建toast元素
const toast = document.createElement('div');
toast.className = `fixed top-4 right-4 px-6 py-3 rounded-lg shadow-lg transition-all duration-300 transform translate-x-96 z-50`;
// 根据类型设置样式
const styles = {
success: 'bg-green-600 text-white',
error: 'bg-red-600 text-white',
warning: 'bg-yellow-500 text-white',
info: 'bg-blue-600 text-white'
};
toast.className += ` ${styles[type] || styles.info}`;
toast.innerHTML = `
${message}
`;
document.body.appendChild(toast);
// 动画显示
setTimeout(() => {
toast.classList.remove('translate-x-96');
toast.classList.add('translate-x-0');
}, 10);
// 3秒后自动消失
setTimeout(() => {
toast.classList.remove('translate-x-0');
toast.classList.add('translate-x-96');
setTimeout(() => {
document.body.removeChild(toast);
}, 300);
}, 3000);
}
// --- Account Info Modal Management ---
function showAccountInfoModal(accountInfo) {
document.getElementById('accountInfoModal').classList.remove('hidden');
// 填充账号信息
document.getElementById('accountInfoEmail').textContent = accountInfo.email || '未知';
document.getElementById('accountInfoPlan').textContent = accountInfo.plan || 'Free';
// 格式化Token过期时间
let tokenExpiryText = '未知';
if (accountInfo.token_expiry && !accountInfo.token_expiry.startsWith('0001')) {
const expiryDate = new Date(accountInfo.token_expiry);
const now = new Date();
const diffMs = expiryDate - now;
if (diffMs < 0) {
tokenExpiryText = '已过期';
} else if (diffMs < 1000 * 60 * 60 * 24) {
// 24小时内过期
const hours = Math.floor(diffMs / (1000 * 60 * 60));
tokenExpiryText = `${hours}小时后过期`;
} else {
// 显示具体日期
tokenExpiryText = expiryDate.toLocaleDateString('zh-CN') + ' ' + expiryDate.toLocaleTimeString('zh-CN', {hour: '2-digit', minute: '2-digit'});
}
}
document.getElementById('accountInfoTokenExpiry').textContent = tokenExpiryText;
// 格式化订阅开始时间
let subscriptionStartText = '未知';
if (accountInfo.subscription_start_date && !accountInfo.subscription_start_date.startsWith('0001')) {
const startDate = new Date(accountInfo.subscription_start_date);
subscriptionStartText = startDate.toLocaleDateString('zh-CN') + ' ' + startDate.toLocaleTimeString('zh-CN', {hour: '2-digit', minute: '2-digit'});
}
document.getElementById('accountInfoSubscriptionStart').textContent = subscriptionStartText;
}
function closeAccountInfoModal() {
document.getElementById('accountInfoModal').classList.add('hidden');
// 刷新账号列表
loadAccounts();
}
// Page Initialization
window.addEventListener('load', function() {
console.log('Page loaded, initializing...');
initTheme();
initAdminAuth();
});