lucide.createIcons(); async function checkAuth() { const r = await fetch('/api/auth/me'); const d = await r.json(); const profile = document.getElementById('userProfile'); const entry = document.getElementById('loginEntryBtn'); const loginHint = document.getElementById('loginHint'); const submitBtn = document.getElementById('submitBtn'); if (d.logged_in) { if (profile) profile.classList.remove('hidden'); if (entry) entry.classList.add('hidden'); if (loginHint) loginHint.classList.add('hidden'); submitBtn.disabled = false; submitBtn.classList.remove('opacity-50', 'cursor-not-allowed'); const phoneDisp = document.getElementById('userPhoneDisplay'); if (phoneDisp) phoneDisp.innerText = d.phone; // 处理积分显示 const pointsBadge = document.getElementById('pointsBadge'); const pointsDisplay = document.getElementById('pointsDisplay'); if (pointsBadge && pointsDisplay) { pointsBadge.classList.remove('hidden'); pointsDisplay.innerText = d.points; } const headerPoints = document.getElementById('headerPoints'); if (headerPoints) headerPoints.innerText = d.points; // 处理自定义 Key 显示逻辑 // 处理自定义 Key 显示逻辑 const keyBtn = document.getElementById('modeKeyBtn'); const modeButtonsContainer = keyBtn ? keyBtn.parentElement : null; if (keyBtn && modeButtonsContainer) { if (d.hide_custom_key) { // 如果后端说要隐藏(用过积分生成),则隐藏整个按钮组 modeButtonsContainer.classList.add('hidden'); // 修改标题为“账户状态” const authTitle = document.querySelector('#authSection h3'); if (authTitle) authTitle.innerText = '账户状态'; } else { // 否则显示按钮组,让用户可以选择 modeButtonsContainer.classList.remove('hidden'); // 如果有 Key,默认帮忙切过去,方便老用户 if (d.api_key) { switchMode('key'); const keyInput = document.getElementById('apiKey'); if (keyInput && !keyInput.value) keyInput.value = d.api_key; return; } } } // 如果用户已经有绑定的 Key,且当前没手动输入,则默认切到 Key 模式 // 强制使用积分模式 switchMode('trial'); } else { if (profile) profile.classList.add('hidden'); if (entry) entry.classList.remove('hidden'); if (loginHint) loginHint.classList.remove('hidden'); submitBtn.disabled = true; submitBtn.classList.add('opacity-50', 'cursor-not-allowed'); } lucide.createIcons(); } // 移除 redundant logout 监听,因为 base.html 已处理全局登出 // 历史记录分页状态 let currentHistoryPage = 1; let hasMoreHistory = true; let isHistoryLoading = false; // 存储当前生成的所有图片 URL let currentGeneratedUrls = []; let currentMode = 'trial'; // 'trial' 或 'key' let uploadedFiles = []; // 存储当前待上传的参考图 let isSetterActive = false; // 是否激活了拍摄角度设置器模式 function switchMode(mode) { currentMode = mode; const trialBtn = document.getElementById('modeTrialBtn'); const keyBtn = document.getElementById('modeKeyBtn'); const keyInputGroup = document.getElementById('keyInputGroup'); const premiumToggle = document.getElementById('premiumToggle'); if (mode === 'trial') { trialBtn.classList.add('border-indigo-500', 'bg-indigo-50', 'text-indigo-600', 'border-2'); trialBtn.classList.remove('border-slate-200', 'text-slate-400'); keyBtn.classList.remove('border-indigo-500', 'bg-indigo-50', 'text-indigo-600', 'border-2'); keyBtn.classList.add('border-slate-200', 'text-slate-400'); keyInputGroup.classList.add('hidden'); if (premiumToggle) premiumToggle.classList.remove('hidden'); } else { keyBtn.classList.add('border-indigo-500', 'bg-indigo-50', 'text-indigo-600', 'border-2'); keyBtn.classList.remove('border-slate-200', 'text-slate-400'); trialBtn.classList.remove('border-indigo-500', 'bg-indigo-50', 'text-indigo-600', 'border-2'); trialBtn.classList.add('border-slate-200', 'text-slate-400'); keyInputGroup.classList.remove('hidden'); if (premiumToggle) premiumToggle.classList.add('hidden'); } updateCostPreview(); // 切换模式时同步计费预览 } async function downloadImage(url) { try { const response = await fetch(url); const blob = await response.blob(); const blobUrl = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = blobUrl; // 从 URL 提取文件名 const filename = url.split('/').pop().split('?')[0] || 'ai-vision-image.png'; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(blobUrl); } catch (e) { console.error('下载失败:', e); showToast('下载失败,请尝试右键保存', 'error'); } } async function loadHistory(isLoadMore = false) { if (isHistoryLoading || (!hasMoreHistory && isLoadMore)) return; isHistoryLoading = true; if (!isLoadMore) { currentHistoryPage = 1; document.getElementById('historyList').innerHTML = ''; } const footer = document.getElementById('historyFooter'); footer.classList.remove('hidden'); try { const r = await fetch(`/api/history?page=${currentHistoryPage}&per_page=10&filter_type=image`); const d = await r.json(); const list = document.getElementById('historyList'); if (d.history && d.history.length > 0) { const html = d.history.map(item => `
${item.created_at} ${item.model}
${item.urls.map(u => `
`).join('')}
`).join(''); if (isLoadMore) { list.insertAdjacentHTML('beforeend', html); } else { list.innerHTML = html; } hasMoreHistory = d.has_next; currentHistoryPage++; } else if (!isLoadMore) { list.innerHTML = `
暂无生成记录
`; } lucide.createIcons(); } catch (e) { console.error('加载历史失败:', e); if (!isLoadMore) { document.getElementById('historyList').innerHTML = `
加载失败: ${e.message}
`; } } finally { isHistoryLoading = false; footer.classList.add('hidden'); } } async function init() { checkAuth(); // 模式切换监听 const modeTrialBtn = document.getElementById('modeTrialBtn'); const modeKeyBtn = document.getElementById('modeKeyBtn'); const isPremiumCheckbox = document.getElementById('isPremium'); if (modeTrialBtn) modeTrialBtn.onclick = () => switchMode('trial'); if (modeKeyBtn) modeKeyBtn.onclick = () => switchMode('key'); if (isPremiumCheckbox) isPremiumCheckbox.onchange = () => updateCostPreview(); const numSelect = document.getElementById('numSelect'); if (numSelect) numSelect.onchange = () => updateCostPreview(); // 历史记录控制 const historyDrawer = document.getElementById('historyDrawer'); const showHistoryBtn = document.getElementById('showHistoryBtn'); const closeHistoryBtn = document.getElementById('closeHistoryBtn'); const historyList = document.getElementById('historyList'); // 3D 构图辅助控制 const openVisualizerBtn = document.getElementById('openVisualizerBtn'); const closeVisualizerBtn = document.getElementById('closeVisualizerBtn'); if (openVisualizerBtn) openVisualizerBtn.onclick = openVisualizerModal; if (closeVisualizerBtn) closeVisualizerBtn.onclick = closeVisualizerModal; if (showHistoryBtn) { showHistoryBtn.onclick = () => { historyDrawer.classList.remove('translate-x-full'); loadHistory(false); }; } if (closeHistoryBtn) { closeHistoryBtn.onclick = () => { historyDrawer.classList.add('translate-x-full'); }; } // 瀑布流滚动加载 if (historyList) { historyList.onscroll = () => { const threshold = 100; if (historyList.scrollTop + historyList.clientHeight >= historyList.scrollHeight - threshold) { loadHistory(true); } }; } // 全部下载按钮逻辑 const downloadAllBtn = document.getElementById('downloadAllBtn'); if (downloadAllBtn) { downloadAllBtn.onclick = async () => { if (currentGeneratedUrls.length === 0) return; showToast(`正在准备下载 ${currentGeneratedUrls.length} 张作品...`, 'info'); for (const url of currentGeneratedUrls) { await downloadImage(url); // 稍微延迟一下,防止浏览器拦截 await new Promise(r => setTimeout(r, 300)); } }; } // 重新生成按钮逻辑 const regenBtn = document.getElementById('regenBtn'); if (regenBtn) { regenBtn.onclick = () => { const submitBtn = document.getElementById('submitBtn'); if (submitBtn) submitBtn.click(); }; } // 检查是否有来自 URL 的错误提示 const urlParams = new URLSearchParams(window.location.search); if (urlParams.has('error')) { showToast(urlParams.get('error'), 'error'); // 清理 URL 参数以防刷新时重复提示 window.history.replaceState({}, document.title, window.location.pathname); } try { const r = await fetch('/api/config'); const d = await r.json(); fillSelect('modelSelect', d.models); fillSelect('ratioSelect', d.ratios); fillSelect('sizeSelect', d.sizes); fillSelect('promptTpl', [{ label: '✨ 自定义创作', value: 'manual' }, ...d.prompts]); updateCostPreview(); // 初始化时显示默认模型的积分 } catch (e) { console.error(e); } // 初始化拖拽排序 const prev = document.getElementById('imagePreview'); if (prev) { new Sortable(prev, { animation: 150, ghostClass: 'opacity-50', onEnd: function (evt) { // 拖拽结束,重新对 uploadedFiles 排序 const oldIndex = evt.oldIndex; const newIndex = evt.newIndex; const movedFile = uploadedFiles.splice(oldIndex, 1)[0]; uploadedFiles.splice(newIndex, 0, movedFile); // 重新渲染以更新“图1/图2/图3”标签 renderImagePreviews(); } }); } } function fillSelect(id, list) { const el = document.getElementById(id); if (!el) return; // 如果是模型选择,增加积分显示 if (id === 'modelSelect') { el.innerHTML = list.map(i => ``).join(''); } else { el.innerHTML = list.map(i => ``).join(''); } } // 更新计费预览显示 function updateCostPreview() { const modelSelect = document.getElementById('modelSelect'); const costPreview = document.getElementById('costPreview'); const numSelect = document.getElementById('numSelect'); const isPremium = document.getElementById('isPremium')?.checked || false; const selectedOption = modelSelect.options[modelSelect.selectedIndex]; if (currentMode === 'trial' && selectedOption) { let baseCost = parseInt(selectedOption.getAttribute('data-cost') || 0); let num = parseInt(numSelect?.value || 1); let totalCost = baseCost * num; if (isPremium) totalCost *= 2; // 优质模式 2 倍积分 costPreview.innerText = `本次生成将消耗 ${totalCost} 积分`; costPreview.classList.remove('hidden'); } else { costPreview.classList.add('hidden'); } } // 渲染参考图预览 function renderImagePreviews() { const prev = document.getElementById('imagePreview'); if (!prev) return; prev.innerHTML = ''; uploadedFiles.forEach((file, index) => { // 同步创建容器,确保编号顺序永远正确 const d = document.createElement('div'); d.className = 'relative w-20 h-20 rounded-2xl overflow-hidden flex-shrink-0 border-2 border-white shadow-md group cursor-move'; d.setAttribute('data-index', index); d.innerHTML = `
图${index + 1}
`; prev.appendChild(d); // 异步加载图片内容 const reader = new FileReader(); reader.onload = (ev) => { d.innerHTML = `
图${index + 1}
`; lucide.createIcons(); }; reader.readAsDataURL(file); }); lucide.createIcons(); } function removeUploadedFile(index) { uploadedFiles.splice(index, 1); renderImagePreviews(); } // 统一处理新上传/粘贴/拖入的文件 function handleNewFiles(files) { const newFiles = Array.from(files).filter(f => f.type.startsWith('image/')); if (newFiles.length === 0) return; // 如果处于设置器模式,严格限制为 1 张 if (isSetterActive) { if (newFiles.length > 0) { uploadedFiles = [newFiles[0]]; showToast('设置器模式已开启,仅保留第一张参考图', 'info'); } } else { if (uploadedFiles.length + newFiles.length > 3) { showToast('最多只能上传 3 张参考图', 'warning'); const remaining = 3 - uploadedFiles.length; if (remaining > 0) { uploadedFiles = uploadedFiles.concat(newFiles.slice(0, remaining)); } } else { uploadedFiles = uploadedFiles.concat(newFiles); } } renderImagePreviews(); } document.getElementById('fileInput').onchange = (e) => { handleNewFiles(e.target.files); e.target.value = ''; // 重置以允许重复选择同一文件 }; // 拖拽上传逻辑 const dropZone = document.getElementById('dropZone'); if (dropZone) { ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(name => { dropZone.addEventListener(name, (e) => { e.preventDefault(); e.stopPropagation(); }, false); }); ['dragenter', 'dragover'].forEach(name => { dropZone.addEventListener(name, () => dropZone.classList.add('border-indigo-500', 'bg-indigo-50/50'), false); }); ['dragleave', 'drop'].forEach(name => { dropZone.addEventListener(name, () => dropZone.classList.remove('border-indigo-500', 'bg-indigo-50/50'), false); }); dropZone.addEventListener('drop', (e) => { handleNewFiles(e.dataTransfer.files); }, false); } // 粘贴上传逻辑 document.addEventListener('paste', (e) => { const items = (e.clipboardData || e.originalEvent.clipboardData).items; const files = []; for (let item of items) { if (item.type.indexOf('image') !== -1) { files.push(item.getAsFile()); } } if (files.length > 0) { handleNewFiles(files); } }); document.getElementById('modelSelect').onchange = (e) => { document.getElementById('sizeGroup').classList.toggle('hidden', e.target.value !== 'nano-banana-2'); updateCostPreview(); // 切换模型时更新计费预览 }; document.getElementById('promptTpl').onchange = (e) => { const area = document.getElementById('manualPrompt'); if (e.target.value !== 'manual') { area.value = e.target.value; area.classList.add('hidden'); } else { area.value = ''; area.classList.remove('hidden'); } }; document.getElementById('submitBtn').onclick = async () => { const btn = document.getElementById('submitBtn'); const prompt = document.getElementById('manualPrompt').value; const apiKey = document.getElementById('apiKey').value; const num = parseInt(document.getElementById('numSelect').value); // 检查登录状态并获取积分 const authCheck = await fetch('/api/auth/me'); const authData = await authCheck.json(); if (!authData.logged_in) { showToast('请先登录后再生成作品', 'warning'); return; } // 根据模式验证 if (currentMode === 'key') { if (!apiKey) return showToast('请输入您的 API 密钥', 'warning'); } else { if (authData.points <= 0) return showToast('可用积分已耗尽,请充值或切换至自定义 Key 模式', 'warning'); } // 允许文生图(不强制要求图片),但至少得有提示词或图片 if (!prompt && uploadedFiles.length === 0) { return showToast('请至少输入提示词或上传参考图', 'warning'); } // UI 锁定 btn.disabled = true; const btnText = btn.querySelector('span'); btnText.innerText = uploadedFiles.length > 0 ? "正在同步参考图..." : "正在开启 AI 引擎..."; document.getElementById('statusInfo').classList.remove('hidden'); document.getElementById('placeholder').classList.add('hidden'); document.getElementById('finalWrapper').classList.remove('hidden'); const grid = document.getElementById('imageGrid'); grid.innerHTML = ''; // 清空 currentGeneratedUrls = []; // 重置当前生成列表 try { let image_data = []; // 1. 将图片转换为 Base64 if (uploadedFiles.length > 0) { btnText.innerText = "正在准备图片数据..."; const readFileAsBase64 = (file) => new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = reject; reader.readAsDataURL(file); }); image_data = await Promise.all(uploadedFiles.map(f => readFileAsBase64(f))); } // 2. 发起单次生成请求 (包含数量 n) btnText.innerText = `AI 构思中...`; // 预先创建 Slot (对应数量) const slots = []; for (let i = 0; i < num; i++) { const slot = document.createElement('div'); slot.className = 'image-frame relative bg-white/50 animate-pulse min-h-[200px] flex items-center justify-center rounded-[2.5rem] border border-slate-100 shadow-sm'; slot.innerHTML = `
正在排队中...
`; grid.appendChild(slot); slots.push(slot); } // 3. 提交任务 const r = await fetch('/api/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mode: currentMode, n: num, // 发送数量 is_premium: document.getElementById('isPremium')?.checked || false, apiKey: currentMode === 'key' ? apiKey : '', prompt: document.getElementById('manualPrompt').value, model: document.getElementById('modelSelect').value, ratio: document.getElementById('ratioSelect').value, size: document.getElementById('sizeSelect').value, image_data // 发送 Base64 数组 }) }); const res = await r.json(); if (res.error) throw new Error(res.error); // 如果直接返回了 data (比如聊天模型),直接显示在第一个 slot if (res.data) { displayResult(slots[0], res.data[0]); // 移除其他多余的 slots for (let i = 1; i < slots.length; i++) slots[i].remove(); return; } // 4. 轮询任务状态 const taskId = res.task_id; let pollCount = 0; const maxPolls = 500; while (pollCount < maxPolls) { await new Promise(resolve => setTimeout(resolve, 2000)); pollCount++; const statusR = await fetch(`/api/task_status/${taskId}`); const statusRes = await statusR.json(); if (statusRes.status === 'complete') { const urls = statusRes.urls; currentGeneratedUrls = urls; // 填充所有 slots urls.forEach((url, idx) => { if (slots[idx]) { displayResult(slots[idx], { url }); } }); // 如果返回的数量少于 slots 数量,移除多余的 if (urls.length < slots.length) { for (let i = urls.length; i < slots.length; i++) slots[i].remove(); } btnText.innerText = `生成完成`; if (currentMode === 'trial') checkAuth(); return; } else if (statusRes.status === 'error') { throw new Error(statusRes.message || "生成失败"); } else { // 更新所有 slot 的轮询状态显示 slots.forEach(slot => { slot.innerHTML = `
AI 正在努力创作中 (${pollCount * 2}s)...
`; }); } } throw new Error("生成超时,请稍后在历史记录中查看"); } catch (e) { // 在第一个 slot 显示错误 if (grid.firstChild) { const firstSlot = grid.firstChild; firstSlot.className = 'image-frame relative bg-red-50/50 flex items-center justify-center rounded-[2.5rem] border border-red-100 p-6'; if (e.message.includes('401') || e.message.includes('请先登录')) { firstSlot.innerHTML = `
登录已过期,请重新登录
`; } else { firstSlot.innerHTML = `
生成异常: ${e.message}
`; } // 移除其他 slots while (grid.children.length > 1) { grid.lastChild.remove(); } } } finally { btn.disabled = false; btnText.innerText = "立即生成作品"; document.getElementById('statusInfo').classList.add('hidden'); } }; // 提取结果展示逻辑 const displayResult = (slot, data) => { if (data.type === 'text') { slot.className = 'image-frame relative bg-white border border-slate-100 p-8 rounded-[2.5rem] shadow-xl overflow-y-auto max-h-[600px]'; slot.innerHTML = `
${data.content.replace(/\n/g, '
')}
`; } else { const imgUrl = data.url; slot.className = 'image-frame group relative animate-in zoom-in-95 duration-700 flex flex-col items-center justify-center overflow-hidden bg-white shadow-2xl transition-all hover:shadow-indigo-100/50'; slot.innerHTML = `
`; } lucide.createIcons(); }; init(); // 修改密码弹窗控制 function openPwdModal() { const modal = document.getElementById('pwdModal'); if (!modal) return; modal.classList.remove('hidden'); setTimeout(() => { modal.classList.add('opacity-100'); modal.querySelector('div').classList.remove('scale-95'); }, 10); } function closePwdModal() { const modal = document.getElementById('pwdModal'); if (!modal) return; modal.classList.remove('opacity-100'); modal.querySelector('div').classList.add('scale-95'); setTimeout(() => { modal.classList.add('hidden'); document.getElementById('pwdForm').reset(); }, 300); } document.addEventListener('click', (e) => { if (e.target.closest('#openPwdModalBtn')) { openPwdModal(); } }); document.getElementById('pwdForm')?.addEventListener('submit', async (e) => { e.preventDefault(); const old_password = document.getElementById('oldPwd').value; const new_password = document.getElementById('newPwd').value; try { const r = await fetch('/api/auth/change_password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ old_password, new_password }) }); const d = await r.json(); if (r.ok) { showToast('密码修改成功,请记牢新密码', 'success'); closePwdModal(); } else { showToast(d.error || '修改失败', 'error'); } } catch (err) { showToast('网络连接失败', 'error'); } }); // 拍摄角度设置器弹窗控制 function openVisualizerModal() { const modal = document.getElementById('visualizerModal'); if (!modal) return; // 检查是否已上传参考图 if (uploadedFiles.length === 0) { return showToast('请先上传一张参考图作为基准', 'warning'); } modal.classList.remove('hidden'); setTimeout(() => { modal.classList.add('opacity-100'); modal.querySelector('div').classList.remove('scale-95'); // 将主页面的图片同步到设置器 iframe const reader = new FileReader(); reader.onload = (e) => { const iframe = document.getElementById('visualizerFrame'); iframe.contentWindow.postMessage({ type: 'sync_image', dataUrl: e.target.result }, '*'); }; reader.readAsDataURL(uploadedFiles[0]); }, 10); } function closeVisualizerModal() { const modal = document.getElementById('visualizerModal'); if (!modal) return; modal.classList.remove('opacity-100'); modal.querySelector('div').classList.add('scale-95'); setTimeout(() => { modal.classList.add('hidden'); }, 300); } // 监听来自设置器的消息 window.addEventListener('message', (e) => { if (e.data.type === 'apply_prompt') { isSetterActive = true; const area = document.getElementById('manualPrompt'); const promptTpl = document.getElementById('promptTpl'); const modelSelect = document.getElementById('modelSelect'); const sizeGroup = document.getElementById('sizeGroup'); // 1. 强制切换并锁定版本 2.0 (nano-banana-2) if (modelSelect) { modelSelect.value = 'nano-banana-2'; modelSelect.disabled = true; // 锁定选择 sizeGroup.classList.remove('hidden'); updateCostPreview(); } // 2. 隐藏模板选择器 if (promptTpl) { promptTpl.classList.add('hidden'); promptTpl.value = 'manual'; } // 3. 强制图片数量为 1 if (uploadedFiles.length > 1) { uploadedFiles = uploadedFiles.slice(0, 1); renderImagePreviews(); } // 4. 替换提示词 if (area) { area.value = e.data.prompt; area.classList.remove('hidden'); showToast('已同步拍摄角度并切换至 2.0 引擎', 'success'); } closeVisualizerModal(); } }); // --- 积分与钱包中心逻辑 --- let pointsChart = null; async function openPointsModal() { const modal = document.getElementById('pointsModal'); if (!modal) return; modal.classList.remove('hidden'); setTimeout(() => { modal.classList.add('opacity-100'); modal.querySelector('div').classList.remove('scale-95'); }, 10); // 加载数据 loadPointStats(); loadPointDetails(); // 更新当前余额 const r = await fetch('/api/auth/me'); const d = await r.json(); if (d.logged_in) { document.getElementById('modalPointsDisplay').innerText = d.points; } } function closePointsModal() { const modal = document.getElementById('pointsModal'); modal.classList.remove('opacity-100'); modal.querySelector('div').classList.add('scale-95'); setTimeout(() => modal.classList.add('hidden'), 300); } async function loadPointStats() { const r = await fetch('/api/stats/points?days=7'); const d = await r.json(); const canvas = document.getElementById('pointsChart'); if (!canvas) return; const ctx = canvas.getContext('2d'); if (pointsChart) pointsChart.destroy(); if (typeof Chart === 'undefined') { console.error('Chart.js not loaded'); return; } pointsChart = new Chart(ctx, { type: 'line', data: { labels: d.labels, datasets: [ { label: '消耗积分', data: d.deductions, borderColor: '#6366f1', backgroundColor: 'rgba(99, 102, 241, 0.1)', borderWidth: 3, fill: true, tension: 0.4, pointRadius: 4, pointBackgroundColor: '#6366f1' }, { label: '充值积分', data: d.incomes, borderColor: '#10b981', backgroundColor: 'rgba(16, 185, 129, 0.1)', borderWidth: 3, fill: true, tension: 0.4, pointRadius: 4, pointBackgroundColor: '#10b981' } ] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, grid: { color: 'rgba(241, 245, 249, 1)' }, ticks: { font: { weight: 'bold' } } }, x: { grid: { display: false }, ticks: { font: { weight: 'bold' } } } } } }); } async function loadPointDetails() { const body = document.getElementById('pointDetailsBody'); if (!body) return; body.innerHTML = '正在加载明细...'; try { const r = await fetch('/api/stats/details?page=1'); const d = await r.json(); if (d.items.length === 0) { body.innerHTML = '暂无积分变动记录'; return; } body.innerHTML = d.items.map(item => `
${item.desc}
${item.model} ${item.change} ${item.time} `).join(''); } catch (e) { body.innerHTML = '加载失败'; } }