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;
const openSavePromptBtn = document.getElementById('openSavePromptBtn');
if (openSavePromptBtn) openSavePromptBtn.onclick = openSavePromptModal;
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 {
await refreshPromptsList();
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 => {
// 处理 data-id
const dataIdAttr = i.id ? ` data-id="${i.id}"` : '';
const disabledAttr = i.disabled ? ' disabled' : '';
return ``;
}).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;
// 检查分辨率并提示压缩
let largeFound = false;
Promise.all(newFiles.map(file => new Promise(resolve => {
const url = URL.createObjectURL(file);
const img = new Image();
img.onload = () => {
if (img.width > 2048 || img.height > 2048) largeFound = true;
URL.revokeObjectURL(url);
resolve();
};
img.onerror = resolve;
img.src = url;
}))).then(() => {
if (largeFound) showToast('检测到图片分辨率大于 2K,将为您自动压缩至 2K 分辨率', 'info');
});
// 如果处于设置器模式,严格限制为 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');
const deleteContainer = document.getElementById('deletePromptContainer');
const selectedOption = e.target.options[e.target.selectedIndex];
// 判断是否是用户的收藏项 (通过 data-id 属性)
const promptId = selectedOption.getAttribute('data-id');
if (promptId) {
deleteContainer.classList.remove('hidden');
document.getElementById('deletePromptBtn').onclick = () => openDeleteConfirmModal(promptId);
} else {
deleteContainer.classList.add('hidden');
}
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 processImageFile = (file) => new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
const maxDim = 2048;
let w = img.width;
let h = img.height;
if (w <= maxDim && h <= maxDim) {
resolve(e.target.result);
return;
}
if (w > h) {
if (w > maxDim) {
h = Math.round(h * (maxDim / w));
w = maxDim;
}
} else {
if (h > maxDim) {
w = Math.round(w * (maxDim / h));
h = maxDim;
}
}
const canvas = document.createElement('canvas');
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, w, h);
// 保持原格式,如果不是 png 则默认 jpeg (0.9 质量)
const outType = file.type === 'image/png' ? 'image/png' : 'image/jpeg';
resolve(canvas.toDataURL(outType, 0.9));
};
img.onerror = reject;
img.src = e.target.result;
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
image_data = await Promise.all(uploadedFiles.map(f => processImageFile(f)));
}
// 2. 并行启动多个生成任务
btnText.innerText = `AI 构思中 (0/${num})...`;
let finishedCount = 0;
const startTask = async (index) => {
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);
try {
// 1. 发起生成请求,获取任务 ID
const r = await fetch('/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
mode: currentMode,
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 (比如聊天模型),直接显示
if (res.data) {
displayResult(slot, res.data[0]);
return;
}
// 2. 轮询任务状态
const taskId = res.task_id;
let pollCount = 0;
const maxPolls = 500; // 最多轮询约 16 分钟 (2s * 500 = 1000s)
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 imgUrl = statusRes.urls[0];
currentGeneratedUrls.push(imgUrl);
displayResult(slot, { url: imgUrl });
finishedCount++;
btnText.innerText = `AI 构思中 (${finishedCount}/${num})...`;
if (currentMode === 'trial') checkAuth();
return; // 任务正常结束
} else if (statusRes.status === 'error') {
throw new Error(statusRes.message || "生成失败");
} else {
// 继续轮询状态显示
slot.innerHTML = `AI 正在努力创作中 (${pollCount * 2}s)...
`;
}
}
throw new Error("生成超时,请稍后在历史记录中查看");
} catch (e) {
slot.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('请先登录')) {
slot.innerHTML = `登录已过期,请重新登录
`;
} else {
slot.innerHTML = `生成异常: ${e.message}
`;
}
}
};
const tasks = Array.from({ length: num }, (_, i) => startTask(i));
await Promise.all(tasks);
} catch (e) {
showToast('创作引擎中断: ' + e.message, 'error');
document.getElementById('placeholder').classList.remove('hidden');
} 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.model} |
${item.change} |
${item.time} |
`).join('');
} catch (e) {
body.innerHTML = '| 加载失败 |
';
}
}
// --- 自定义提示词逻辑 ---
async function loadUserPrompts() {
try {
const r = await fetch('/api/prompts');
const d = await r.json();
return d.error ? [] : d;
} catch (e) {
return [];
}
}
async function refreshPromptsList() {
try {
const r = await fetch('/api/config');
const d = await r.json();
fillSelect('modelSelect', d.models);
fillSelect('ratioSelect', d.ratios);
fillSelect('sizeSelect', d.sizes);
const userPrompts = await loadUserPrompts();
const mergedPrompts = [
{ label: '✨ 自定义创作', value: 'manual' },
...(userPrompts.length > 0 ? [{ label: '--- 我的收藏 ---', value: 'manual', disabled: true }] : []),
...userPrompts.map(p => ({ id: p.id, label: '⭐ ' + p.label, value: p.value })),
{ label: '--- 系统预设 ---', value: 'manual', disabled: true },
...d.prompts
];
fillSelect('promptTpl', mergedPrompts);
} catch (e) { console.error(e); }
}
function openSavePromptModal() {
// Check login
if (document.getElementById('loginEntryBtn').offsetParent !== null) {
showToast('请先登录', 'warning');
return;
}
const modal = document.getElementById('savePromptModal');
const input = document.getElementById('promptTitleInput');
const prompt = document.getElementById('manualPrompt').value;
if (!prompt || !prompt.trim()) {
showToast('请先输入一些提示词内容', 'warning');
return;
}
if (modal) {
modal.classList.remove('hidden');
input.value = '';
setTimeout(() => {
modal.classList.add('opacity-100');
modal.querySelector('div').classList.remove('scale-95');
input.focus();
}, 10);
}
}
function closeSavePromptModal() {
const modal = document.getElementById('savePromptModal');
if (modal) {
modal.classList.remove('opacity-100');
modal.querySelector('div').classList.add('scale-95');
setTimeout(() => modal.classList.add('hidden'), 300);
}
}
async function confirmSavePrompt() {
const title = document.getElementById('promptTitleInput').value.trim();
const prompt = document.getElementById('manualPrompt').value.trim();
if (!title) {
showToast('请输入标题', 'warning');
return;
}
try {
const r = await fetch('/api/prompts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, prompt })
});
const d = await r.json();
if (r.ok) {
showToast('收藏成功', 'success');
closeSavePromptModal();
refreshPromptsList();
} else {
showToast(d.error || '保存失败', 'error');
}
} catch (e) {
showToast('保存异常', 'error');
}
}
async function deleteUserPrompt(id) {
// Legacy function replaced by openDeleteConfirmModal
// Kept for reference or simple fallback if needed, but logic moved to executeDeletePrompt
}
function openDeleteConfirmModal(id) {
const modal = document.getElementById('deleteConfirmModal');
const confirmBtn = document.getElementById('confirmDeleteBtn');
if (modal && confirmBtn) {
// Store ID on the button
confirmBtn.setAttribute('data-delete-id', id);
confirmBtn.onclick = () => executeDeletePrompt(id); // Bind click
modal.classList.remove('hidden');
setTimeout(() => {
modal.classList.add('opacity-100');
modal.querySelector('div').classList.remove('scale-95');
}, 10);
}
}
function closeDeleteConfirmModal() {
const modal = document.getElementById('deleteConfirmModal');
if (modal) {
modal.classList.remove('opacity-100');
modal.querySelector('div').classList.add('scale-95');
setTimeout(() => modal.classList.add('hidden'), 300);
}
}
async function executeDeletePrompt(id) {
const confirmBtn = document.getElementById('confirmDeleteBtn');
const originalText = confirmBtn.innerHTML;
confirmBtn.disabled = true;
confirmBtn.innerHTML = '';
lucide.createIcons();
try {
const r = await fetch(`/api/prompts/${id}`, {
method: 'DELETE'
});
if (r.ok) {
showToast('已删除', 'success');
// Reset selection
document.getElementById('promptTpl').value = 'manual';
document.getElementById('manualPrompt').value = '';
document.getElementById('manualPrompt').classList.remove('hidden');
document.getElementById('deletePromptContainer').classList.add('hidden');
closeDeleteConfirmModal();
refreshPromptsList();
} else {
showToast('删除失败', 'error');
}
} catch (e) {
showToast('删除异常', 'error');
} finally {
confirmBtn.disabled = false;
confirmBtn.innerHTML = originalText;
}
}