2026-01-12 00:53:31 +08:00
|
|
|
|
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'
|
2026-01-12 23:29:29 +08:00
|
|
|
|
let uploadedFiles = []; // 存储当前待上传的参考图
|
2026-01-12 00:53:31 +08:00
|
|
|
|
|
|
|
|
|
|
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">
|
2026-01-12 23:29:29 +08:00
|
|
|
|
${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">
|
2026-01-12 00:53:31 +08:00
|
|
|
|
</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');
|
|
|
|
|
|
|
|
|
|
|
|
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); }
|
2026-01-12 23:29:29 +08:00
|
|
|
|
|
|
|
|
|
|
// 初始化拖拽排序
|
|
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-01-12 00:53:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-12 23:29:29 +08:00
|
|
|
|
// 渲染参考图预览
|
|
|
|
|
|
function renderImagePreviews() {
|
2026-01-12 00:53:31 +08:00
|
|
|
|
const prev = document.getElementById('imagePreview');
|
2026-01-12 23:29:29 +08:00
|
|
|
|
if(!prev) return;
|
2026-01-12 00:53:31 +08:00
|
|
|
|
prev.innerHTML = '';
|
2026-01-12 23:29:29 +08:00
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
// 异步加载图片内容
|
2026-01-12 00:53:31 +08:00
|
|
|
|
const reader = new FileReader();
|
|
|
|
|
|
reader.onload = (ev) => {
|
2026-01-12 23:29:29 +08:00
|
|
|
|
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();
|
2026-01-12 00:53:31 +08:00
|
|
|
|
};
|
|
|
|
|
|
reader.readAsDataURL(file);
|
|
|
|
|
|
});
|
2026-01-12 23:29:29 +08:00
|
|
|
|
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 = ''; // 重置以允许重复选择同一文件
|
2026-01-12 00:53:31 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-12 23:29:29 +08:00
|
|
|
|
// 拖拽上传逻辑
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-12 00:53:31 +08:00
|
|
|
|
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;
|
2026-01-12 23:29:29 +08:00
|
|
|
|
area.classList.add('hidden');
|
2026-01-12 00:53:31 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
area.value = '';
|
2026-01-12 23:29:29 +08:00
|
|
|
|
area.classList.remove('hidden');
|
2026-01-12 00:53:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
document.getElementById('submitBtn').onclick = async () => {
|
|
|
|
|
|
const btn = document.getElementById('submitBtn');
|
2026-01-12 23:29:29 +08:00
|
|
|
|
const prompt = document.getElementById('manualPrompt').value;
|
2026-01-12 00:53:31 +08:00
|
|
|
|
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');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-12 23:29:29 +08:00
|
|
|
|
// 允许文生图(不强制要求图片),但至少得有提示词或图片
|
|
|
|
|
|
if(!prompt && uploadedFiles.length === 0) {
|
|
|
|
|
|
return showToast('请至少输入提示词或上传参考图', 'warning');
|
|
|
|
|
|
}
|
2026-01-12 00:53:31 +08:00
|
|
|
|
|
|
|
|
|
|
// UI 锁定
|
|
|
|
|
|
btn.disabled = true;
|
|
|
|
|
|
const btnText = btn.querySelector('span');
|
2026-01-12 23:29:29 +08:00
|
|
|
|
btnText.innerText = uploadedFiles.length > 0 ? "正在同步参考图..." : "正在开启 AI 引擎...";
|
2026-01-12 00:53:31 +08:00
|
|
|
|
|
|
|
|
|
|
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 {
|
2026-01-12 23:29:29 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2026-01-12 00:53:31 +08:00
|
|
|
|
|
|
|
|
|
|
// 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 = `<div class="text-slate-400 text-xs font-bold italic">正在创作第 ${index + 1} 张...</div>`;
|
|
|
|
|
|
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 = `
|
|
|
|
|
|
<img src="${imgUrl}" class="w-full h-auto rounded-[2.5rem] object-contain shadow-xl">
|
|
|
|
|
|
<div class="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity bg-black/20 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();
|
|
|
|
|
|
} 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>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
} 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');
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|