function getCurrentPoolType() { const pool = (document.body && document.body.dataset && document.body.dataset.poolType) || "normal"; return pool === "welfare" ? "welfare" : "normal"; } /** * GPT Team 管理系统 - 通用 JavaScript */ // Toast 提示函数 function showToast(message, type = 'info') { const toast = document.getElementById('toast'); if (!toast) return; let icon = 'info'; if (type === 'success') icon = 'check-circle'; if (type === 'error') icon = 'alert-circle'; toast.innerHTML = `${message}`; toast.className = `toast ${type} show`; if (window.lucide) { lucide.createIcons(); } setTimeout(() => { toast.classList.remove('show'); }, 3000); } // 日期格式化函数 function formatDateTime(dateString) { if (!dateString) return '-'; const date = new Date(dateString); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); return `${year}-${month}-${day} ${hours}:${minutes}`; } // 登出函数 async function logout() { if (!confirm('确定要登出吗?')) { return; } try { const response = await fetch('/auth/logout', { method: 'POST', headers: { 'Content-Type': 'application/json' } }); const data = await response.json(); if (response.ok && data.success) { window.location.href = '/login'; } else { showToast('登出失败', 'error'); } } catch (error) { showToast('网络错误', 'error'); } } // API 调用封装 async function apiCall(url, options = {}) { try { const response = await fetch(url, { ...options, headers: { 'Content-Type': 'application/json', ...options.headers } }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || data.detail || '请求失败'); } return { success: true, data }; } catch (error) { return { success: false, error: error.message }; } } // 确认对话框 function confirmAction(message) { return confirm(message); } function setSingleImportMode(mode = 'quick') { const quickSection = document.getElementById('oauthQuickSection'); const manualSection = document.getElementById('manualTokenSection'); if (!quickSection || !manualSection) return; const isManual = mode === 'manual'; quickSection.style.display = isManual ? 'none' : 'block'; manualSection.style.display = isManual ? 'block' : 'none'; } // 页面加载完成后执行 document.addEventListener('DOMContentLoaded', function () { // 检查认证状态 checkAuthStatus(); // OAuth 一键导入按钮绑定(避免仅依赖内联 onclick) const btnOneClickToken = document.getElementById('btnOneClickToken'); if (btnOneClickToken) { btnOneClickToken.addEventListener('click', () => { generateOAuthAuthorizeLink(); }); } const btnParseOAuthCallback = document.getElementById('btnParseOAuthCallback'); if (btnParseOAuthCallback) { btnParseOAuthCallback.addEventListener('click', () => { parseOAuthCallbackAndFill(); }); } const btnExportOAuthJson = document.getElementById('btnExportOAuthJson'); if (btnExportOAuthJson) { btnExportOAuthJson.addEventListener('click', () => { exportOAuthJsonTemplateFile(); }); } const switchToManualFill = document.getElementById('switchToManualFill'); if (switchToManualFill) { switchToManualFill.addEventListener('click', () => setSingleImportMode('manual')); } const switchToQuickToken = document.getElementById('switchToQuickToken'); if (switchToQuickToken) { switchToQuickToken.addEventListener('click', () => setSingleImportMode('quick')); } const chooseJsonFileBtn = document.getElementById('chooseJsonFileBtn'); const jsonImportFile = document.getElementById('jsonImportFile'); if (chooseJsonFileBtn && jsonImportFile) { chooseJsonFileBtn.addEventListener('click', () => jsonImportFile.click()); jsonImportFile.addEventListener('change', async () => { const fileNameNode = document.getElementById('jsonImportFileName'); if (fileNameNode) { fileNameNode.textContent = jsonImportFile.files && jsonImportFile.files[0] ? `已选择:${jsonImportFile.files[0].name}` : '支持单对象、对象数组,或 {"teams": [...]} 格式'; } if (jsonImportFile.files && jsonImportFile.files.length > 0) { await handleJsonFileImport(); } }); } setSingleImportMode('quick'); }); // 检查认证状态 async function checkAuthStatus() { // 如果在登录页面,跳过检查 if (window.location.pathname === '/login') { return; } try { const response = await fetch('/auth/status'); const data = await response.json(); if (!data.authenticated && window.location.pathname.startsWith('/admin')) { // 未登录且在管理员页面,跳转到登录页 window.location.href = '/login'; } } catch (error) { console.error('检查认证状态失败:', error); } } // === 模态框控制逻辑 === function showModal(modalId) { const modal = document.getElementById(modalId); if (modal) { modal.classList.add('show'); document.body.style.overflow = 'hidden'; // 防止背景滚动 if (modalId === 'importTeamModal') { setSingleImportMode('quick'); } } } function hideModal(modalId) { const modal = document.getElementById(modalId); if (modal) { modal.classList.remove('show'); document.body.style.overflow = ''; } } function switchModalTab(modalId, tabId) { const modal = document.getElementById(modalId); if (!modal) return; // 切换按钮状态 const tabs = modal.querySelectorAll('.modal-tab-btn'); tabs.forEach(tab => { if (tab.getAttribute('onclick').includes(`'${tabId}'`)) { tab.classList.add('active'); } else { tab.classList.remove('active'); } }); // 切换面板显示 const panels = modal.querySelectorAll('.import-panel, .card-body'); panels.forEach(panel => { if (panel.id === tabId) { panel.style.display = 'block'; } else { panel.style.display = 'none'; } }); } /** * 切换质保时长输入框的显示 */ function toggleWarrantyDays(checkbox, targetId) { const target = document.getElementById(targetId); if (target) { target.style.display = checkbox.checked ? 'block' : 'none'; } } // === Team 导入逻辑 === async function copyTextSilently(text) { if (!text) return false; try { if (navigator.clipboard && window.isSecureContext) { await navigator.clipboard.writeText(text); return true; } } catch (err) { console.error('silent copy failed:', err); } try { const textArea = document.createElement('textarea'); textArea.value = text; textArea.style.position = 'fixed'; textArea.style.left = '-9999px'; textArea.style.top = '0'; document.body.appendChild(textArea); textArea.focus(); textArea.select(); const ok = document.execCommand('copy'); document.body.removeChild(textArea); return ok; } catch (err) { console.error('silent fallback copy failed:', err); return false; } } function unwrapApiPayload(result) { if (!result || !result.success) return null; const body = result.data || {}; if (body && typeof body === 'object' && body.data && typeof body.data === 'object') { return body.data; } return body; } let oauthDraft = { codeVerifier: '', state: '', clientId: '' }; let oauthParsedCache = null; let oauthParsedCacheKey = ''; async function generateOAuthAuthorizeLink() { const form = document.getElementById('singleImportForm'); if (!form) return; const formClientId = form.clientId ? form.clientId.value.trim() : ''; const defaultClientId = 'app_EMoamEEZ73f0CkXaXp7hrann'; const clientId = formClientId || defaultClientId; showToast('正在生成并复制授权链接...', 'info'); try { const result = await apiCall('/admin/oauth/openai/authorize', { method: 'POST', body: JSON.stringify({ client_id: clientId, redirect_uri: 'http://localhost:1455/auth/callback' }) }); if (!result.success) { showToast(result.error || '生成授权链接失败', 'error'); return; } const data = unwrapApiPayload(result) || {}; oauthDraft.codeVerifier = data.code_verifier || ''; oauthDraft.state = data.state || ''; oauthDraft.clientId = data.client_id || clientId; oauthParsedCache = null; oauthParsedCacheKey = ''; document.getElementById('oauthAuthorizeUrlOutput').value = data.authorize_url || ''; if (form.clientId) form.clientId.value = oauthDraft.clientId; const authUrl = (data.authorize_url || '').trim(); if (!authUrl) { showToast('授权链接生成失败,请重试', 'error'); return; } const copied = await copyTextSilently(authUrl); if (copied) { showToast('链接已复制,去浏览器登录后粘贴回调', 'success'); } else { showToast('授权链接已生成,请手动复制', 'warning'); } } catch (error) { showToast('生成授权链接失败', 'error'); } } async function parseOAuthCallbackData(forceRefresh = false) { const callbackText = document.getElementById('oauthCallbackInput').value.trim(); const form = document.getElementById('singleImportForm'); if (!callbackText) { throw new Error('请先粘贴回调 URL'); } if (!forceRefresh && oauthParsedCache && oauthParsedCacheKey === callbackText) { return oauthParsedCache; } const result = await apiCall('/admin/oauth/openai/parse-callback', { method: 'POST', body: JSON.stringify({ callback_text: callbackText, code_verifier: oauthDraft.codeVerifier || null, expected_state: oauthDraft.state || null, client_id: ((form.clientId ? form.clientId.value.trim() : '') || oauthDraft.clientId || 'app_EMoamEEZ73f0CkXaXp7hrann'), redirect_uri: 'http://localhost:1455/auth/callback' }) }); if (!result.success) { throw new Error(result.error || '解析回调失败'); } const parsed = unwrapApiPayload(result) || {}; oauthParsedCache = parsed; oauthParsedCacheKey = callbackText; return parsed; } function decodeJwtPayload(token) { if (!token || typeof token !== 'string') return null; const parts = token.split('.'); if (parts.length < 2) return null; try { const base64Url = parts[1]; const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); const padded = base64.padEnd(base64.length + (4 - base64.length % 4) % 4, '='); const jsonText = decodeURIComponent(escape(window.atob(padded))); return JSON.parse(jsonText); } catch (error) { return null; } } function toIsoStringWithOffset8(dateObj) { if (!(dateObj instanceof Date) || Number.isNaN(dateObj.getTime())) return ''; const shifted = new Date(dateObj.getTime() + 8 * 60 * 60 * 1000); const iso = shifted.toISOString().replace('Z', '+08:00'); return iso.slice(0, 19) + '+08:00'; } function buildOAuthJsonTemplate(parsedData) { const accessToken = parsedData.access_token || ''; const refreshToken = parsedData.refresh_token || ''; const clientId = parsedData.client_id || ''; const raw = parsedData.raw || {}; const accessPayload = decodeJwtPayload(accessToken) || {}; const accessAuth = accessPayload['https://api.openai.com/auth'] || {}; const accessProfile = accessPayload['https://api.openai.com/profile'] || {}; const accountId = raw.account_id || parsedData.account_id || accessAuth.chatgpt_account_id || ''; const email = raw.email || parsedData.email || accessProfile.email || ''; const exp = accessPayload.exp ? new Date(accessPayload.exp * 1000) : null; const expired = raw.expired || parsedData.expired || (exp ? toIsoStringWithOffset8(exp) : ''); return { access_token: accessToken, account_id: accountId, disabled: typeof raw.disabled === 'boolean' ? raw.disabled : false, email, expired, id_token: raw.id_token || parsedData.id_token || '', last_refresh: raw.last_refresh || parsedData.last_refresh || toIsoStringWithOffset8(new Date()), refresh_token: refreshToken, type: raw.type || parsedData.type || 'codex', client_id: clientId }; } function downloadJsonFile(payload, filename) { const text = JSON.stringify(payload, null, 2); const blob = new Blob([text], { type: 'application/json;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } async function exportOAuthJsonTemplateFile() { try { const data = oauthParsedCache || await parseOAuthCallbackData(); const payload = buildOAuthJsonTemplate(data); const filename = `team-oauth-${Date.now()}.json`; downloadJsonFile(payload, filename); showToast('JSON 文件已导出', 'success'); } catch (error) { showToast(error.message || '导出 JSON 失败', 'error'); } } async function parseOAuthCallbackAndFill() { const form = document.getElementById('singleImportForm'); try { const data = await parseOAuthCallbackData(true); if (form.accessToken && data.access_token) form.accessToken.value = data.access_token; if (form.refreshToken && data.refresh_token) form.refreshToken.value = data.refresh_token; if (form.clientId && data.client_id) form.clientId.value = data.client_id; showToast('已自动填充 Token 信息,请确认后导入', 'success'); } catch (error) { showToast(error.message || '解析回调失败', 'error'); } } async function handleSingleImport(event) { event.preventDefault(); const form = event.target; const accessToken = form.accessToken.value.trim(); const refreshToken = form.refreshToken ? form.refreshToken.value.trim() : null; const sessionToken = form.sessionToken ? form.sessionToken.value.trim() : null; const clientId = form.clientId ? form.clientId.value.trim() : null; const email = form.email.value.trim(); const accountId = form.accountId.value.trim(); const submitButton = form.querySelector('button[type="submit"]'); submitButton.disabled = true; submitButton.textContent = '导入中...'; try { const result = await apiCall('/admin/teams/import', { method: 'POST', body: JSON.stringify({ import_type: 'single', access_token: accessToken, refresh_token: refreshToken || null, session_token: sessionToken || null, client_id: clientId || null, email: email || null, account_id: accountId || null, pool_type: getCurrentPoolType() }) }); if (result.success) { showToast('Team 导入成功!', 'success'); form.reset(); setTimeout(() => location.reload(), 1500); } else { showToast(result.error || '导入失败', 'error'); } } catch (error) { showToast('网络错误', 'error'); } finally { submitButton.disabled = false; submitButton.textContent = '导入'; } } async function handleBatchImport(event) { event.preventDefault(); const form = event.target; const batchContent = (form.batchContent && form.batchContent.value ? form.batchContent.value.trim() : ""); const submitButton = form.querySelector('button[type="submit"]'); if (!batchContent) { showToast('请输入批量导入内容', 'error'); return; } // UI 元素 const progressContainer = document.getElementById('batchProgressContainer'); const progressBar = document.getElementById('batchProgressBar'); const progressStage = document.getElementById('batchProgressStage'); const progressPercent = document.getElementById('batchProgressPercent'); const successCountEl = document.getElementById('batchSuccessCount'); const failedCountEl = document.getElementById('batchFailedCount'); const resultsContainer = document.getElementById('batchResultsContainer'); const resultsDiv = document.getElementById('batchResults'); const finalSummaryEl = document.getElementById('batchFinalSummary'); // 重置 UI progressContainer.style.display = 'block'; resultsContainer.style.display = 'none'; progressBar.style.width = '0%'; progressStage.textContent = '准备导入...'; progressPercent.textContent = '0%'; successCountEl.textContent = '0'; failedCountEl.textContent = '0'; resultsDiv.innerHTML = '
邮箱状态消息
'; const resultsBody = document.getElementById('batchResultsBody'); submitButton.disabled = true; submitButton.textContent = '导入中...'; try { const response = await fetch('/admin/teams/import', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ import_type: 'batch', content: batchContent, pool_type: getCurrentPoolType() }) }); if (!response.ok) { let msg = '请求失败'; try { const errorData = await response.json(); msg = errorData.error || errorData.detail || msg; } catch (_) { const errorText = await response.text(); if (errorText) msg = errorText.slice(0, 200); } throw new Error(msg); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; const processStreamLine = (line) => { if (!line || !line.trim()) return; try { const trimmed = line.trim(); if (!trimmed.startsWith('{')) return; const data = JSON.parse(trimmed); if (data.type === 'start') { progressStage.textContent = `开始导入 (共 ${data.total} 条)...`; // 给用户即时反馈,避免看起来一直卡在 0% progressBar.style.width = '5%'; progressPercent.textContent = '5%'; } else if (data.type === 'progress') { const percent = Math.round((data.current / data.total) * 100); progressBar.style.width = `${percent}%`; progressPercent.textContent = `${percent}%`; progressStage.textContent = `正在导入 ${data.current}/${data.total}...`; successCountEl.textContent = data.success_count; failedCountEl.textContent = data.failed_count; if (data.last_result) { resultsContainer.style.display = 'block'; const res = data.last_result; const statusClass = res.success ? 'text-success' : 'text-danger'; const statusText = res.success ? '成功' : '失败'; const row = document.createElement('tr'); row.innerHTML = ` ${res.email} ${statusText} ${res.success ? (res.message || '导入成功') : res.error} `; resultsBody.insertBefore(row, resultsBody.firstChild); } } else if (data.type === 'finish') { progressStage.textContent = '导入完成'; progressBar.style.width = '100%'; progressPercent.textContent = '100%'; finalSummaryEl.textContent = `总数: ${data.total} | 成功: ${data.success_count} | 失败: ${data.failed_count}`; if (data.failed_count === 0) { showToast('全部导入成功!', 'success'); } else { showToast(`导入完成,成功 ${data.success_count} 条,失败 ${data.failed_count} 条`, 'warning'); } if (data.success_count > 0) { setTimeout(() => location.reload(), 3000); } } else if (data.type === 'error') { showToast(data.error, 'error'); } } catch (e) { console.error('解析流数据失败:', e, line); } }; while (true) { const { value, done } = await reader.read(); if (done) { // 处理最后一段可能没有 \n 结尾的残余数据 if (buffer && buffer.trim()) { processStreamLine(buffer); } break; } buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { processStreamLine(line); } } } catch (error) { showToast(error.message || '网络错误', 'error'); } finally { submitButton.disabled = false; submitButton.textContent = '批量导入'; } } async function handleJsonFileImport() { const fileInput = document.getElementById('jsonImportFile'); const form = document.getElementById('batchImportForm'); const submitButton = form ? form.querySelector('button[type="submit"]') : null; if (!fileInput || !fileInput.files || fileInput.files.length === 0) { showToast('请先选择 JSON 文件', 'error'); return; } const file = fileInput.files[0]; let content = ''; try { content = await file.text(); JSON.parse(content); } catch (error) { showToast('JSON 文件格式无效', 'error'); return; } if (submitButton) { submitButton.disabled = true; submitButton.textContent = 'JSON 导入中...'; } // UI 元素 const progressContainer = document.getElementById('batchProgressContainer'); const progressBar = document.getElementById('batchProgressBar'); const progressStage = document.getElementById('batchProgressStage'); const progressPercent = document.getElementById('batchProgressPercent'); const successCountEl = document.getElementById('batchSuccessCount'); const failedCountEl = document.getElementById('batchFailedCount'); const resultsContainer = document.getElementById('batchResultsContainer'); const resultsDiv = document.getElementById('batchResults'); const finalSummaryEl = document.getElementById('batchFinalSummary'); // 重置 UI progressContainer.style.display = 'block'; resultsContainer.style.display = 'none'; progressBar.style.width = '0%'; progressStage.textContent = '准备 JSON 导入...'; progressPercent.textContent = '0%'; successCountEl.textContent = '0'; failedCountEl.textContent = '0'; resultsDiv.innerHTML = '
邮箱状态消息
'; const resultsBody = document.getElementById('batchResultsBody'); try { const response = await fetch('/admin/teams/import', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ import_type: 'json', content, pool_type: getCurrentPoolType() }) }); if (!response.ok) { let msg = '请求失败'; try { const errorData = await response.json(); msg = errorData.error || errorData.detail || msg; } catch (_) { const errorText = await response.text(); if (errorText) msg = errorText.slice(0, 200); } throw new Error(msg); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; const processStreamLine = (line) => { if (!line || !line.trim()) return; try { const trimmed = line.trim(); if (!trimmed.startsWith('{')) return; const data = JSON.parse(trimmed); if (data.type === 'start') { progressStage.textContent = `开始导入 (共 ${data.total} 条)...`; // 让用户看到实时变化,避免看起来一直 0% progressBar.style.width = '5%'; progressPercent.textContent = '5%'; } else if (data.type === 'progress') { const percent = Math.round((data.current / data.total) * 100); progressBar.style.width = `${percent}%`; progressPercent.textContent = `${percent}%`; progressStage.textContent = `正在导入 ${data.current}/${data.total}...`; successCountEl.textContent = data.success_count; failedCountEl.textContent = data.failed_count; if (data.last_result) { resultsContainer.style.display = 'block'; const res = data.last_result; const statusClass = res.success ? 'text-success' : 'text-danger'; const statusText = res.success ? '成功' : '失败'; const row = document.createElement('tr'); row.innerHTML = ` ${res.email} ${statusText} ${res.success ? (res.message || '导入成功') : res.error} `; resultsBody.insertBefore(row, resultsBody.firstChild); } } else if (data.type === 'finish') { progressStage.textContent = '导入完成'; progressBar.style.width = '100%'; progressPercent.textContent = '100%'; finalSummaryEl.textContent = `总数: ${data.total} | 成功: ${data.success_count} | 失败: ${data.failed_count}`; if (data.failed_count === 0) { showToast('全部导入成功!', 'success'); } else { showToast(`导入完成,成功 ${data.success_count} 条,失败 ${data.failed_count} 条`, 'warning'); } if (data.success_count > 0) { setTimeout(() => location.reload(), 3000); } } else if (data.type === 'error') { showToast(data.error, 'error'); } } catch (e) { console.error('解析流数据失败:', e, line); } }; while (true) { const { value, done } = await reader.read(); if (done) { if (buffer && buffer.trim()) { processStreamLine(buffer); } break; } buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { processStreamLine(line); } } } catch (error) { showToast(error.message || '网络错误', 'error'); } finally { if (submitButton) { submitButton.disabled = false; submitButton.textContent = '批量导入'; } } } // === 兑换码生成逻辑 === async function generateSingle(event) { event.preventDefault(); const form = event.target; const customCode = form.customCode.value.trim(); const expiresDays = form.expiresDays.value; const hasWarranty = form.hasWarranty.checked; const warrantyDays = form.warrantyDays ? form.warrantyDays.value : 30; const data = { type: 'single', has_warranty: hasWarranty, warranty_days: parseInt(warrantyDays || 30) }; if (customCode) data.code = customCode; if (expiresDays) data.expires_days = parseInt(expiresDays); const result = await apiCall('/admin/codes/generate', { method: 'POST', body: JSON.stringify(data) }); if (result.success) { document.getElementById('generatedCode').textContent = result.data.code; document.getElementById('singleResult').style.display = 'block'; form.reset(); showToast('兑换码生成成功', 'success'); // 如果在列表中,延迟刷新 if (window.location.pathname === '/admin/codes') { setTimeout(() => location.reload(), 2000); } } else { showToast(result.error || '生成失败', 'error'); } } async function generateBatch(event) { event.preventDefault(); const form = event.target; const count = parseInt(form.count.value); const expiresDays = form.expiresDays.value; const hasWarranty = form.hasWarranty.checked; const warrantyDays = form.warrantyDays ? form.warrantyDays.value : 30; if (count < 1 || count > 1000) { showToast('生成数量必须在1-1000之间', 'error'); return; } const data = { type: 'batch', count: count, has_warranty: hasWarranty, warranty_days: parseInt(warrantyDays || 30) }; if (expiresDays) data.expires_days = parseInt(expiresDays); const result = await apiCall('/admin/codes/generate', { method: 'POST', body: JSON.stringify(data) }); if (result.success) { document.getElementById('batchTotal').textContent = result.data.total; document.getElementById('batchCodes').value = result.data.codes.join('\n'); document.getElementById('batchResult').style.display = 'block'; form.reset(); showToast(`成功生成 ${result.data.total} 个兑换码`, 'success'); if (window.location.pathname === '/admin/codes') { setTimeout(() => location.reload(), 3000); } } else { showToast(result.error || '生成失败', 'error'); } } // 统一复制到剪贴板函数 async function copyToClipboard(text) { if (!text) return; try { // 尝试使用 Modern Clipboard API if (navigator.clipboard && window.isSecureContext) { await navigator.clipboard.writeText(text); showToast('已复制到剪贴板', 'success'); return true; } } catch (err) { console.error('Modern copy failed:', err); } // Fallback: 使用 textarea 方式 try { const textArea = document.createElement("textarea"); textArea.value = text; // 确保 textarea 不可见且不影响布局 textArea.style.position = "fixed"; textArea.style.left = "-9999px"; textArea.style.top = "0"; textArea.style.opacity = "0"; document.body.appendChild(textArea); textArea.focus(); textArea.select(); const successful = document.execCommand('copy'); document.body.removeChild(textArea); if (successful) { showToast('已复制到剪贴板', 'success'); return true; } } catch (err) { console.error('Fallback copy failed:', err); } showToast('复制失败', 'error'); return false; } // === 辅助函数 === function copyCode(code) { // 如果没有传入 code,尝试从生成结果中获取 if (!code) { const generatedCodeEl = document.getElementById('generatedCode'); code = generatedCodeEl ? generatedCodeEl.textContent : ''; } if (code) { copyToClipboard(code); } else { showToast('无内容可复制', 'error'); } } function copyBatchCodes() { const codes = document.getElementById('batchCodes').value; copyToClipboard(codes); } function copyWelfareCode() { const el = document.getElementById('welfareCommonCodeValue'); const code = el ? String(el.value || '').trim() : ''; if (!code || code === '-') { showToast('暂无可复制的通用兑换码', 'warning'); return; } copyToClipboard(code); } function downloadCodes() { const codes = document.getElementById('batchCodes').value; const blob = new Blob([codes], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `redemption_codes_${new Date().getTime()}.txt`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); showToast('下载成功', 'success'); } // === 成员管理逻辑 === async function viewMembers(teamId, teamEmail = '') { window.currentTeamId = teamId; const modal = document.getElementById('manageMembersModal'); if (!modal) return; // 设置基本信息 document.getElementById('modalTeamEmail').textContent = teamEmail; // 打开模态框 showModal('manageMembersModal'); // 加载成员列表 await loadModalMemberList(teamId); } async function loadModalMemberList(teamId) { const joinedTableBody = document.getElementById('modalJoinedMembersTableBody'); const invitedTableBody = document.getElementById('modalInvitedMembersTableBody'); if (joinedTableBody) joinedTableBody.innerHTML = '加载中...'; if (invitedTableBody) invitedTableBody.innerHTML = '加载中...'; try { const result = await apiCall(`/admin/teams/${teamId}/members/list`); if (result.success) { const allMembers = result.data.members || []; const joinedMembers = allMembers.filter(m => m.status === 'joined'); const invitedMembers = allMembers.filter(m => m.status === 'invited'); // 渲染已加入成员 if (joinedTableBody) { if (joinedMembers.length === 0) { joinedTableBody.innerHTML = '暂无已加入成员'; } else { joinedTableBody.innerHTML = joinedMembers.map(m => ` ${m.email} ${m.role === 'account-owner' ? '所有者' : '成员'} ${formatDateTime(m.added_at)} ${m.role !== 'account-owner' ? ` ` : '不可删除'} `).join(''); } } // 渲染待加入成员 if (invitedTableBody) { if (invitedMembers.length === 0) { invitedTableBody.innerHTML = '暂无待加入成员'; } else { invitedTableBody.innerHTML = invitedMembers.map(m => ` ${m.email} 成员 ${formatDateTime(m.added_at)} `).join(''); } } if (window.lucide) lucide.createIcons(); } else { const errorMsg = `${result.error}`; if (joinedTableBody) joinedTableBody.innerHTML = errorMsg; if (invitedTableBody) invitedTableBody.innerHTML = errorMsg; } } catch (error) { const errorMsg = '加载失败'; if (joinedTableBody) joinedTableBody.innerHTML = errorMsg; if (invitedTableBody) invitedTableBody.innerHTML = errorMsg; } } async function revokeInvite(teamId, email, inModal = false) { if (!confirm(`确定要撤回对 "${email}" 的邀请吗?`)) { return; } try { showToast('正在撤回...', 'info'); const result = await apiCall(`/admin/teams/${teamId}/invites/revoke`, { method: 'POST', body: JSON.stringify({ email: email }) }); if (result.success) { showToast('撤回成功', 'success'); if (inModal) { await loadModalMemberList(teamId); } else { setTimeout(() => location.reload(), 1000); } } else { showToast(result.error || '撤回失败', 'error'); } } catch (error) { showToast('网络错误', 'error'); } } async function handleAddMember(event) { event.preventDefault(); const form = event.target; const email = form.email.value.trim(); const submitButton = document.getElementById('addMemberSubmitBtn'); const teamId = window.currentTeamId; if (!teamId) { showToast('无法获取 Team ID', 'error'); return; } submitButton.disabled = true; const originalText = submitButton.innerHTML; submitButton.textContent = '添加中...'; try { const result = await apiCall(`/admin/teams/${teamId}/members/add`, { method: 'POST', body: JSON.stringify({ email }) }); if (result.success) { showToast('成员添加成功!', 'success'); form.reset(); // 在模态框模式下,只负载列表 if (document.getElementById('manageMembersModal').classList.contains('show')) { await loadModalMemberList(teamId); } else { setTimeout(() => location.reload(), 1500); } } else { showToast(result.error || '添加失败', 'error'); } } catch (error) { showToast('网络错误', 'error'); } finally { submitButton.disabled = false; submitButton.innerHTML = originalText; } } async function deleteMember(teamId, userId, email, inModal = false) { if (!confirm(`确定要删除成员 "${email}" 吗?\n\n此操作不可恢复!`)) { return; } try { showToast('正在删除...', 'info'); const result = await apiCall(`/admin/teams/${teamId}/members/${userId}/delete`, { method: 'POST' }); if (result.success) { showToast('删除成功', 'success'); if (inModal) { await loadModalMemberList(teamId); } else { setTimeout(() => location.reload(), 1000); } } else { showToast(result.error || '删除失败', 'error'); } } catch (error) { showToast('网络错误', 'error'); } } // 确保内联 onclick 在任何加载模式下都可调用 if (typeof window !== 'undefined') { window.generateOAuthAuthorizeLink = generateOAuthAuthorizeLink; window.parseOAuthCallbackAndFill = parseOAuthCallbackAndFill; window.exportOAuthJsonTemplateFile = exportOAuthJsonTemplateFile; window.handleJsonFileImport = handleJsonFileImport; window.copyWelfareCode = copyWelfareCode; } async function generateWelfareCode() { try { const btn = document.getElementById('generateWelfareCodeBtn'); if (btn) { btn.disabled = true; } const result = await apiCall('/admin/welfare/code/generate', { method: 'POST' }); if (!result.success) throw new Error(result.error || '生成失败'); const codeValueEl = document.getElementById('welfareCommonCodeValue'); const newCode = result.code || ''; if (codeValueEl) codeValueEl.value = newCode; const codeTextEl = document.getElementById('welfareCommonCodeText'); if (codeTextEl) { codeTextEl.textContent = newCode || '-'; codeTextEl.title = newCode || ''; } const usageTextEl = document.getElementById('welfareCodeUsageText'); if (usageTextEl) { const used = typeof result.used === 'number' ? result.used : 0; const limit = typeof result.limit === 'number' ? result.limit : 0; const remaining = typeof result.remaining === 'number' ? result.remaining : Math.max(limit - used, 0); usageTextEl.textContent = `剩余次数 ${remaining} / ${limit}`; } const copyBtn = document.getElementById('copyWelfareCodeBtn'); if (copyBtn) copyBtn.disabled = !result.code; await copyToClipboard(result.code || ''); showToast(`通用兑换码已更新并复制,剩余次数 ${result.remaining}/${result.limit}`, 'success'); } catch (error) { showToast(error.message || '生成通用兑换码失败', 'error'); } finally { const btn = document.getElementById('generateWelfareCodeBtn'); if (btn) btn.disabled = false; } }