738 lines
30 KiB
JavaScript
738 lines
30 KiB
JavaScript
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`);
|
||
const d = await r.json();
|
||
|
||
const list = document.getElementById('historyList');
|
||
|
||
if (d.history && d.history.length > 0) {
|
||
const html = d.history.map(item => `
|
||
<div class="bg-white border border-slate-100 rounded-2xl p-4 space-y-3 hover:border-indigo-100 transition-all shadow-sm group">
|
||
<div class="flex items-center justify-between">
|
||
<span class="text-[10px] font-black text-slate-400 bg-slate-50 px-2 py-0.5 rounded-md uppercase tracking-widest">${item.time}</span>
|
||
<span class="text-[10px] font-bold text-indigo-500">${item.model}</span>
|
||
</div>
|
||
<div class="grid grid-cols-3 gap-2">
|
||
${item.urls.map(u => `
|
||
<div class="aspect-square rounded-lg overflow-hidden border border-slate-100 cursor-pointer transition-transform hover:scale-105" onclick="window.open('${u.url}')">
|
||
<img src="${u.thumb}" class="w-full h-full object-cover" loading="lazy">
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
|
||
if (isLoadMore) {
|
||
list.insertAdjacentHTML('beforeend', html);
|
||
} else {
|
||
list.innerHTML = html;
|
||
}
|
||
|
||
hasMoreHistory = d.has_next;
|
||
currentHistoryPage++;
|
||
} else if (!isLoadMore) {
|
||
list.innerHTML = `<div class="flex flex-col items-center justify-center h-64 text-slate-300">
|
||
<i data-lucide="inbox" class="w-12 h-12 mb-4"></i>
|
||
<span class="text-xs font-bold">暂无生成记录</span>
|
||
</div>`;
|
||
}
|
||
lucide.createIcons();
|
||
} catch (e) {
|
||
console.error('加载历史失败:', e);
|
||
if (!isLoadMore) {
|
||
document.getElementById('historyList').innerHTML = `<div class="text-center text-rose-400 text-xs font-bold py-10">加载失败: ${e.message}</div>`;
|
||
}
|
||
} 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');
|
||
|
||
// 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 => `<option value="${i.value}" data-cost="${i.cost || 0}">${i.label} (${i.cost || 0}积分)</option>`).join('');
|
||
} else {
|
||
el.innerHTML = list.map(i => `<option value="${i.value}">${i.label}</option>`).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 = `
|
||
<div class="w-full h-full bg-slate-100 animate-pulse flex items-center justify-center">
|
||
<i data-lucide="image" class="w-5 h-5 text-slate-300"></i>
|
||
</div>
|
||
<div class="absolute bottom-1 left-1 bg-black/60 text-white text-[8px] px-1.5 py-0.5 rounded-md font-black backdrop-blur-sm z-10">图${index + 1}</div>
|
||
<button onclick="removeUploadedFile(${index})" class="absolute top-1 right-1 bg-rose-500 text-white p-1 rounded-full opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center z-20">
|
||
<i data-lucide="x" class="w-3 h-3"></i>
|
||
</button>
|
||
`;
|
||
prev.appendChild(d);
|
||
|
||
// 异步加载图片内容
|
||
const reader = new FileReader();
|
||
reader.onload = (ev) => {
|
||
d.innerHTML = `
|
||
<img src="${ev.target.result}" class="w-full h-full object-cover">
|
||
<div class="absolute bottom-1 left-1 bg-black/60 text-white text-[8px] px-1.5 py-0.5 rounded-md font-black backdrop-blur-sm z-10">图${index + 1}</div>
|
||
<button onclick="removeUploadedFile(${index})" class="absolute top-1 right-1 bg-rose-500 text-white p-1 rounded-full opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center z-20">
|
||
<i data-lucide="x" class="w-3 h-3"></i>
|
||
</button>
|
||
`;
|
||
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. 并行启动多个生成任务
|
||
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 = `<div class="text-slate-400 text-[10px] font-bold italic">正在排队中...</div>`;
|
||
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 = `<div class="text-slate-400 text-[10px] font-bold italic">AI 正在努力创作中 (${pollCount * 2}s)...</div>`;
|
||
}
|
||
}
|
||
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 = `<div class="text-red-400 text-[10px] font-bold text-center">登录已过期,请重新登录</div>`;
|
||
} else {
|
||
slot.innerHTML = `<div class="text-red-400 text-[10px] font-bold text-center">生成异常: ${e.message}</div>`;
|
||
}
|
||
}
|
||
};
|
||
|
||
// 提取结果展示逻辑
|
||
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 = `<div class="prose prose-slate prose-sm max-w-none text-slate-600 font-medium leading-relaxed">${data.content.replace(/\n/g, '<br>')}</div>`;
|
||
} 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 = `
|
||
<div class="w-full h-full flex items-center justify-center bg-slate-50/20 p-2">
|
||
<img src="${imgUrl}" class="max-w-full max-h-[60vh] md:max-h-[70vh] object-contain rounded-2xl shadow-sm transition-transform duration-500 group-hover:scale-[1.01]">
|
||
</div>
|
||
<div class="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity bg-slate-900/40 backdrop-blur-[2px] rounded-[2.5rem]">
|
||
<button onclick="downloadImage('${imgUrl}')" class="bg-white/90 p-4 rounded-full text-indigo-600 shadow-xl hover:scale-110 transition-transform">
|
||
<i data-lucide="download-cloud" class="w-6 h-6"></i>
|
||
</button>
|
||
</div>
|
||
`;
|
||
}
|
||
lucide.createIcons();
|
||
};
|
||
|
||
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');
|
||
}
|
||
});
|
||
|
||
// 拍摄角度设置器弹窗控制
|
||
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();
|
||
}
|
||
});
|