ai_v/static/js/main.js
24024 a47b84e009 feat(api): 实现图片生成异步任务与任务状态查询接口
- 新增异步图片生成处理函数,支持后台任务执行及积分退还机制
- 实现任务状态查询接口,支持前端实时获取生成进度和结果
- 优化生成逻辑:根据模型类型分流,聊天模型同步调用,图片模型异步执行
- 调整积分预扣除和退还逻辑,保障用户积分安全
- 后台线程同步图片至私有存储,提升响应性能和用户体验
- 新增 /visualizer 路由对应前端控制器页面,辅助3D构图和拍摄角度设置
- 优化前端上传逻辑,新增设置器模式时单图上传限制
- 移除项目中未使用的前端脚本与配置文件,简化代码库维护
2026-01-15 21:42:03 +08:00

717 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 = []; // 存储当前待上传的参考图
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();
}
});