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 模式
if(d.api_key) {
switchMode('key');
const keyInput = document.getElementById('apiKey');
if(keyInput && !keyInput.value) keyInput.value = d.api_key;
} else {
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 = []; // 存储当前待上传的参考图
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`);
const d = await r.json();
const list = document.getElementById('historyList');
if (d.history && d.history.length > 0) {
const html = d.history.map(item => `
${item.time}
${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 historyDrawer = document.getElementById('historyDrawer');
const showHistoryBtn = document.getElementById('showHistoryBtn');
const closeHistoryBtn = document.getElementById('closeHistoryBtn');
const historyList = document.getElementById('historyList');
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 isPremium = document.getElementById('isPremium')?.checked || false;
const selectedOption = modelSelect.options[modelSelect.selectedIndex];
if (currentMode === 'trial' && selectedOption) {
let cost = parseInt(selectedOption.getAttribute('data-cost') || 0);
if(isPremium) cost *= 2; // 优质模式 2 倍积分
costPreview.innerText = `本次生成将消耗 ${cost} 积分`;
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;
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_urls = [];
// 1. 如果有图则先上传
if (uploadedFiles.length > 0) {
const uploadData = new FormData();
for(let f of uploadedFiles) uploadData.append('images', f);
const upR = await fetch('/api/upload', { method: 'POST', body: uploadData });
const upRes = await upR.json();
if(upRes.error) throw new Error(upRes.error);
image_urls = upRes.urls;
}
// 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-[300px] flex items-center justify-center rounded-[2.5rem] border border-slate-100 shadow-sm';
slot.innerHTML = `正在创作第 ${index + 1} 张...
`;
grid.appendChild(slot);
try {
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_urls
})
});
const res = await r.json();
if(res.error) throw new Error(res.error);
if(res.message) showToast(res.message, 'success');
const imgUrl = res.data[0].url;
currentGeneratedUrls.push(imgUrl);
slot.className = 'image-frame group relative animate-in zoom-in-95 duration-700';
slot.innerHTML = `
`;
lucide.createIcons();
} 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}
`;
}
} finally {
finishedCount++;
btnText.innerText = `AI 构思中 (${finishedCount}/${num})...`;
// 每次生成任务结束后,刷新一次积分显示
if(currentMode === 'trial') checkAuth();
}
};
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');
}
};
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');
}
});