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');
|
2026-01-16 22:24:14 +08:00
|
|
|
|
|
|
|
|
|
|
if (d.logged_in) {
|
|
|
|
|
|
if (profile) profile.classList.remove('hidden');
|
|
|
|
|
|
if (entry) entry.classList.add('hidden');
|
|
|
|
|
|
if (loginHint) loginHint.classList.add('hidden');
|
2026-01-12 00:53:31 +08:00
|
|
|
|
submitBtn.disabled = false;
|
|
|
|
|
|
submitBtn.classList.remove('opacity-50', 'cursor-not-allowed');
|
|
|
|
|
|
const phoneDisp = document.getElementById('userPhoneDisplay');
|
2026-01-16 22:24:14 +08:00
|
|
|
|
if (phoneDisp) phoneDisp.innerText = d.phone;
|
2026-01-12 00:53:31 +08:00
|
|
|
|
|
|
|
|
|
|
// 处理积分显示
|
|
|
|
|
|
const pointsBadge = document.getElementById('pointsBadge');
|
|
|
|
|
|
const pointsDisplay = document.getElementById('pointsDisplay');
|
2026-01-16 22:24:14 +08:00
|
|
|
|
if (pointsBadge && pointsDisplay) {
|
2026-01-12 00:53:31 +08:00
|
|
|
|
pointsBadge.classList.remove('hidden');
|
|
|
|
|
|
pointsDisplay.innerText = d.points;
|
|
|
|
|
|
}
|
|
|
|
|
|
const headerPoints = document.getElementById('headerPoints');
|
2026-01-16 22:24:14 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-12 00:53:31 +08:00
|
|
|
|
|
|
|
|
|
|
// 如果用户已经有绑定的 Key,且当前没手动输入,则默认切到 Key 模式
|
2026-01-16 22:24:14 +08:00
|
|
|
|
// 强制使用积分模式
|
|
|
|
|
|
switchMode('trial');
|
2026-01-12 00:53:31 +08:00
|
|
|
|
} else {
|
2026-01-16 22:24:14 +08:00
|
|
|
|
if (profile) profile.classList.add('hidden');
|
|
|
|
|
|
if (entry) entry.classList.remove('hidden');
|
|
|
|
|
|
if (loginHint) loginHint.classList.remove('hidden');
|
2026-01-12 00:53:31 +08:00
|
|
|
|
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-15 21:42:03 +08:00
|
|
|
|
let isSetterActive = false; // 是否激活了拍摄角度设置器模式
|
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');
|
|
|
|
|
|
|
2026-01-16 22:24:14 +08:00
|
|
|
|
if (mode === 'trial') {
|
2026-01-12 00:53:31 +08:00
|
|
|
|
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');
|
2026-01-16 22:24:14 +08:00
|
|
|
|
if (premiumToggle) premiumToggle.classList.remove('hidden');
|
2026-01-12 00:53:31 +08:00
|
|
|
|
} 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');
|
2026-01-16 22:24:14 +08:00
|
|
|
|
if (premiumToggle) premiumToggle.classList.add('hidden');
|
2026-01-12 00:53:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
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;
|
2026-01-16 22:24:14 +08:00
|
|
|
|
|
2026-01-12 00:53:31 +08:00
|
|
|
|
isHistoryLoading = true;
|
|
|
|
|
|
if (!isLoadMore) {
|
|
|
|
|
|
currentHistoryPage = 1;
|
|
|
|
|
|
document.getElementById('historyList').innerHTML = '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const footer = document.getElementById('historyFooter');
|
|
|
|
|
|
footer.classList.remove('hidden');
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2026-01-17 23:15:58 +08:00
|
|
|
|
const r = await fetch(`/api/history?page=${currentHistoryPage}&per_page=10&filter_type=image`);
|
2026-01-12 00:53:31 +08:00
|
|
|
|
const d = await r.json();
|
2026-01-16 22:24:14 +08:00
|
|
|
|
|
2026-01-12 00:53:31 +08:00
|
|
|
|
const list = document.getElementById('historyList');
|
2026-01-16 22:24:14 +08:00
|
|
|
|
|
2026-01-12 00:53:31 +08:00
|
|
|
|
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">
|
2026-01-17 23:15:58 +08:00
|
|
|
|
<span class="text-[10px] font-black text-slate-400 bg-slate-50 px-2 py-0.5 rounded-md uppercase tracking-widest">${item.created_at}</span>
|
2026-01-12 00:53:31 +08:00
|
|
|
|
<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;
|
|
|
|
|
|
}
|
2026-01-16 22:24:14 +08:00
|
|
|
|
|
2026-01-12 00:53:31 +08:00
|
|
|
|
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();
|
2026-01-16 22:24:14 +08:00
|
|
|
|
|
2026-01-12 00:53:31 +08:00
|
|
|
|
// 模式切换监听
|
|
|
|
|
|
const modeTrialBtn = document.getElementById('modeTrialBtn');
|
|
|
|
|
|
const modeKeyBtn = document.getElementById('modeKeyBtn');
|
|
|
|
|
|
const isPremiumCheckbox = document.getElementById('isPremium');
|
2026-01-16 22:24:14 +08:00
|
|
|
|
if (modeTrialBtn) modeTrialBtn.onclick = () => switchMode('trial');
|
|
|
|
|
|
if (modeKeyBtn) modeKeyBtn.onclick = () => switchMode('key');
|
|
|
|
|
|
if (isPremiumCheckbox) isPremiumCheckbox.onchange = () => updateCostPreview();
|
2026-01-20 09:29:01 +08:00
|
|
|
|
const numSelect = document.getElementById('numSelect');
|
|
|
|
|
|
if (numSelect) numSelect.onchange = () => updateCostPreview();
|
2026-01-12 00:53:31 +08:00
|
|
|
|
|
|
|
|
|
|
// 历史记录控制
|
|
|
|
|
|
const historyDrawer = document.getElementById('historyDrawer');
|
|
|
|
|
|
const showHistoryBtn = document.getElementById('showHistoryBtn');
|
|
|
|
|
|
const closeHistoryBtn = document.getElementById('closeHistoryBtn');
|
|
|
|
|
|
const historyList = document.getElementById('historyList');
|
|
|
|
|
|
|
2026-01-15 21:42:03 +08:00
|
|
|
|
// 3D 构图辅助控制
|
|
|
|
|
|
const openVisualizerBtn = document.getElementById('openVisualizerBtn');
|
|
|
|
|
|
const closeVisualizerBtn = document.getElementById('closeVisualizerBtn');
|
2026-01-16 22:24:14 +08:00
|
|
|
|
if (openVisualizerBtn) openVisualizerBtn.onclick = openVisualizerModal;
|
|
|
|
|
|
if (closeVisualizerBtn) closeVisualizerBtn.onclick = closeVisualizerModal;
|
2026-01-21 20:43:46 +08:00
|
|
|
|
const openSavePromptBtn = document.getElementById('openSavePromptBtn');
|
|
|
|
|
|
if (openSavePromptBtn) openSavePromptBtn.onclick = openSavePromptModal;
|
2026-01-15 21:42:03 +08:00
|
|
|
|
|
2026-01-16 22:24:14 +08:00
|
|
|
|
if (showHistoryBtn) {
|
2026-01-12 00:53:31 +08:00
|
|
|
|
showHistoryBtn.onclick = () => {
|
|
|
|
|
|
historyDrawer.classList.remove('translate-x-full');
|
|
|
|
|
|
loadHistory(false);
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
2026-01-16 22:24:14 +08:00
|
|
|
|
if (closeHistoryBtn) {
|
2026-01-12 00:53:31 +08:00
|
|
|
|
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');
|
2026-01-16 22:24:14 +08:00
|
|
|
|
if (downloadAllBtn) {
|
2026-01-12 00:53:31 +08:00
|
|
|
|
downloadAllBtn.onclick = async () => {
|
2026-01-16 22:24:14 +08:00
|
|
|
|
if (currentGeneratedUrls.length === 0) return;
|
2026-01-12 00:53:31 +08:00
|
|
|
|
showToast(`正在准备下载 ${currentGeneratedUrls.length} 张作品...`, 'info');
|
2026-01-16 22:24:14 +08:00
|
|
|
|
for (const url of currentGeneratedUrls) {
|
2026-01-12 00:53:31 +08:00
|
|
|
|
await downloadImage(url);
|
|
|
|
|
|
// 稍微延迟一下,防止浏览器拦截
|
|
|
|
|
|
await new Promise(r => setTimeout(r, 300));
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 重新生成按钮逻辑
|
|
|
|
|
|
const regenBtn = document.getElementById('regenBtn');
|
2026-01-16 22:24:14 +08:00
|
|
|
|
if (regenBtn) {
|
2026-01-12 00:53:31 +08:00
|
|
|
|
regenBtn.onclick = () => {
|
|
|
|
|
|
const submitBtn = document.getElementById('submitBtn');
|
2026-01-16 22:24:14 +08:00
|
|
|
|
if (submitBtn) submitBtn.click();
|
2026-01-12 00:53:31 +08:00
|
|
|
|
};
|
|
|
|
|
|
}
|
2026-01-16 22:24:14 +08:00
|
|
|
|
|
2026-01-12 00:53:31 +08:00
|
|
|
|
// 检查是否有来自 URL 的错误提示
|
|
|
|
|
|
const urlParams = new URLSearchParams(window.location.search);
|
2026-01-16 22:24:14 +08:00
|
|
|
|
if (urlParams.has('error')) {
|
2026-01-12 00:53:31 +08:00
|
|
|
|
showToast(urlParams.get('error'), 'error');
|
|
|
|
|
|
// 清理 URL 参数以防刷新时重复提示
|
|
|
|
|
|
window.history.replaceState({}, document.title, window.location.pathname);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2026-01-21 20:43:46 +08:00
|
|
|
|
await refreshPromptsList();
|
2026-01-12 00:53:31 +08:00
|
|
|
|
updateCostPreview(); // 初始化时显示默认模型的积分
|
2026-01-16 22:24:14 +08:00
|
|
|
|
} catch (e) { console.error(e); }
|
2026-01-12 23:29:29 +08:00
|
|
|
|
|
|
|
|
|
|
// 初始化拖拽排序
|
|
|
|
|
|
const prev = document.getElementById('imagePreview');
|
2026-01-16 22:24:14 +08:00
|
|
|
|
if (prev) {
|
2026-01-12 23:29:29 +08:00
|
|
|
|
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);
|
2026-01-16 22:24:14 +08:00
|
|
|
|
if (!el) return;
|
|
|
|
|
|
if (id === 'modelSelect') {
|
2026-01-12 00:53:31 +08:00
|
|
|
|
el.innerHTML = list.map(i => `<option value="${i.value}" data-cost="${i.cost || 0}">${i.label} (${i.cost || 0}积分)</option>`).join('');
|
|
|
|
|
|
} else {
|
2026-01-21 20:43:46 +08:00
|
|
|
|
el.innerHTML = list.map(i => {
|
|
|
|
|
|
// 处理 data-id
|
|
|
|
|
|
const dataIdAttr = i.id ? ` data-id="${i.id}"` : '';
|
|
|
|
|
|
const disabledAttr = i.disabled ? ' disabled' : '';
|
|
|
|
|
|
return `<option value="${i.value}"${dataIdAttr}${disabledAttr}>${i.label}</option>`;
|
|
|
|
|
|
}).join('');
|
2026-01-12 00:53:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新计费预览显示
|
|
|
|
|
|
function updateCostPreview() {
|
|
|
|
|
|
const modelSelect = document.getElementById('modelSelect');
|
|
|
|
|
|
const costPreview = document.getElementById('costPreview');
|
2026-01-20 09:29:01 +08:00
|
|
|
|
const numSelect = document.getElementById('numSelect');
|
2026-01-12 00:53:31 +08:00
|
|
|
|
const isPremium = document.getElementById('isPremium')?.checked || false;
|
|
|
|
|
|
const selectedOption = modelSelect.options[modelSelect.selectedIndex];
|
2026-01-16 22:24:14 +08:00
|
|
|
|
|
2026-01-12 00:53:31 +08:00
|
|
|
|
if (currentMode === 'trial' && selectedOption) {
|
2026-01-20 09:29:01 +08:00
|
|
|
|
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} 积分`;
|
2026-01-12 00:53:31 +08:00
|
|
|
|
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-16 22:24:14 +08:00
|
|
|
|
if (!prev) return;
|
2026-01-12 00:53:31 +08:00
|
|
|
|
prev.innerHTML = '';
|
2026-01-16 22:24:14 +08:00
|
|
|
|
|
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-16 22:24:14 +08:00
|
|
|
|
|
2026-01-12 23:29:29 +08:00
|
|
|
|
// 异步加载图片内容
|
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;
|
|
|
|
|
|
|
2026-01-15 21:42:03 +08:00
|
|
|
|
// 如果处于设置器模式,严格限制为 1 张
|
|
|
|
|
|
if (isSetterActive) {
|
|
|
|
|
|
if (newFiles.length > 0) {
|
|
|
|
|
|
uploadedFiles = [newFiles[0]];
|
|
|
|
|
|
showToast('设置器模式已开启,仅保留第一张参考图', 'info');
|
2026-01-12 23:29:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
2026-01-15 21:42:03 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2026-01-12 23:29:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
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');
|
2026-01-21 20:43:46 +08:00
|
|
|
|
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');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 22:24:14 +08:00
|
|
|
|
if (e.target.value !== 'manual') {
|
2026-01-12 00:53:31 +08:00
|
|
|
|
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();
|
2026-01-16 22:24:14 +08:00
|
|
|
|
if (!authData.logged_in) {
|
2026-01-12 00:53:31 +08:00
|
|
|
|
showToast('请先登录后再生成作品', 'warning');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 根据模式验证
|
2026-01-16 22:24:14 +08:00
|
|
|
|
if (currentMode === 'key') {
|
|
|
|
|
|
if (!apiKey) return showToast('请输入您的 API 密钥', 'warning');
|
2026-01-12 00:53:31 +08:00
|
|
|
|
} else {
|
2026-01-16 22:24:14 +08:00
|
|
|
|
if (authData.points <= 0) return showToast('可用积分已耗尽,请充值或切换至自定义 Key 模式', 'warning');
|
2026-01-12 00:53:31 +08:00
|
|
|
|
}
|
2026-01-16 22:24:14 +08:00
|
|
|
|
|
2026-01-12 23:29:29 +08:00
|
|
|
|
// 允许文生图(不强制要求图片),但至少得有提示词或图片
|
2026-01-16 22:24:14 +08:00
|
|
|
|
if (!prompt && uploadedFiles.length === 0) {
|
2026-01-12 23:29:29 +08:00
|
|
|
|
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-16 22:24:14 +08:00
|
|
|
|
|
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-15 21:42:03 +08:00
|
|
|
|
let image_data = [];
|
2026-01-16 22:24:14 +08:00
|
|
|
|
|
2026-01-15 21:42:03 +08:00
|
|
|
|
// 1. 将图片转换为 Base64
|
2026-01-12 23:29:29 +08:00
|
|
|
|
if (uploadedFiles.length > 0) {
|
2026-01-15 21:42:03 +08:00
|
|
|
|
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)));
|
2026-01-12 23:29:29 +08:00
|
|
|
|
}
|
2026-01-12 00:53:31 +08:00
|
|
|
|
|
2026-01-20 09:53:06 +08:00
|
|
|
|
// 2. 并行启动多个生成任务
|
|
|
|
|
|
btnText.innerText = `AI 构思中 (0/${num})...`;
|
|
|
|
|
|
let finishedCount = 0;
|
2026-01-12 00:53:31 +08:00
|
|
|
|
|
2026-01-20 09:53:06 +08:00
|
|
|
|
const startTask = async (index) => {
|
2026-01-12 00:53:31 +08:00
|
|
|
|
const slot = document.createElement('div');
|
2026-01-14 00:00:23 +08:00
|
|
|
|
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';
|
2026-01-15 21:42:03 +08:00
|
|
|
|
slot.innerHTML = `<div class="text-slate-400 text-[10px] font-bold italic">正在排队中...</div>`;
|
2026-01-12 00:53:31 +08:00
|
|
|
|
grid.appendChild(slot);
|
2026-01-20 09:29:01 +08:00
|
|
|
|
|
2026-01-20 09:53:06 +08:00
|
|
|
|
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 数组
|
|
|
|
|
|
})
|
2026-01-20 09:29:01 +08:00
|
|
|
|
});
|
2026-01-20 09:53:06 +08:00
|
|
|
|
const res = await r.json();
|
|
|
|
|
|
if (res.error) throw new Error(res.error);
|
2026-01-20 09:29:01 +08:00
|
|
|
|
|
2026-01-20 09:53:06 +08:00
|
|
|
|
// 如果直接返回了 data (比如聊天模型),直接显示
|
|
|
|
|
|
if (res.data) {
|
|
|
|
|
|
displayResult(slot, res.data[0]);
|
|
|
|
|
|
return;
|
2026-01-15 21:42:03 +08:00
|
|
|
|
}
|
2026-01-12 00:53:31 +08:00
|
|
|
|
|
2026-01-20 09:53:06 +08:00
|
|
|
|
// 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>`;
|
|
|
|
|
|
}
|
2026-01-12 00:53:31 +08:00
|
|
|
|
}
|
2026-01-20 09:53:06 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const tasks = Array.from({ length: num }, (_, i) => startTask(i));
|
|
|
|
|
|
await Promise.all(tasks);
|
2026-01-12 00:53:31 +08:00
|
|
|
|
|
|
|
|
|
|
} catch (e) {
|
2026-01-20 09:53:06 +08:00
|
|
|
|
showToast('创作引擎中断: ' + e.message, 'error');
|
|
|
|
|
|
document.getElementById('placeholder').classList.remove('hidden');
|
2026-01-12 00:53:31 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
btn.disabled = false;
|
|
|
|
|
|
btnText.innerText = "立即生成作品";
|
|
|
|
|
|
document.getElementById('statusInfo').classList.add('hidden');
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-20 09:29:01 +08:00
|
|
|
|
// 提取结果展示逻辑
|
|
|
|
|
|
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();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-12 00:53:31 +08:00
|
|
|
|
init();
|
|
|
|
|
|
|
|
|
|
|
|
// 修改密码弹窗控制
|
|
|
|
|
|
function openPwdModal() {
|
|
|
|
|
|
const modal = document.getElementById('pwdModal');
|
2026-01-16 22:24:14 +08:00
|
|
|
|
if (!modal) return;
|
2026-01-12 00:53:31 +08:00
|
|
|
|
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');
|
2026-01-16 22:24:14 +08:00
|
|
|
|
if (!modal) return;
|
2026-01-12 00:53:31 +08:00
|
|
|
|
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) => {
|
2026-01-16 22:24:14 +08:00
|
|
|
|
if (e.target.closest('#openPwdModalBtn')) {
|
2026-01-12 00:53:31 +08:00
|
|
|
|
openPwdModal();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
document.getElementById('pwdForm')?.addEventListener('submit', async (e) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
const old_password = document.getElementById('oldPwd').value;
|
|
|
|
|
|
const new_password = document.getElementById('newPwd').value;
|
2026-01-16 22:24:14 +08:00
|
|
|
|
|
2026-01-12 00:53:31 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const r = await fetch('/api/auth/change_password', {
|
|
|
|
|
|
method: 'POST',
|
2026-01-16 22:24:14 +08:00
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
|
body: JSON.stringify({ old_password, new_password })
|
2026-01-12 00:53:31 +08:00
|
|
|
|
});
|
|
|
|
|
|
const d = await r.json();
|
2026-01-16 22:24:14 +08:00
|
|
|
|
if (r.ok) {
|
2026-01-12 00:53:31 +08:00
|
|
|
|
showToast('密码修改成功,请记牢新密码', 'success');
|
|
|
|
|
|
closePwdModal();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showToast(d.error || '修改失败', 'error');
|
|
|
|
|
|
}
|
2026-01-16 22:24:14 +08:00
|
|
|
|
} catch (err) {
|
2026-01-12 00:53:31 +08:00
|
|
|
|
showToast('网络连接失败', 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2026-01-15 21:42:03 +08:00
|
|
|
|
|
|
|
|
|
|
// 拍摄角度设置器弹窗控制
|
|
|
|
|
|
function openVisualizerModal() {
|
|
|
|
|
|
const modal = document.getElementById('visualizerModal');
|
2026-01-16 22:24:14 +08:00
|
|
|
|
if (!modal) return;
|
|
|
|
|
|
|
2026-01-15 21:42:03 +08:00
|
|
|
|
// 检查是否已上传参考图
|
|
|
|
|
|
if (uploadedFiles.length === 0) {
|
|
|
|
|
|
return showToast('请先上传一张参考图作为基准', 'warning');
|
|
|
|
|
|
}
|
2026-01-16 22:24:14 +08:00
|
|
|
|
|
2026-01-15 21:42:03 +08:00
|
|
|
|
modal.classList.remove('hidden');
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
modal.classList.add('opacity-100');
|
|
|
|
|
|
modal.querySelector('div').classList.remove('scale-95');
|
2026-01-16 22:24:14 +08:00
|
|
|
|
|
2026-01-15 21:42:03 +08:00
|
|
|
|
// 将主页面的图片同步到设置器 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');
|
2026-01-16 22:24:14 +08:00
|
|
|
|
if (!modal) return;
|
2026-01-15 21:42:03 +08:00
|
|
|
|
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;
|
2026-01-16 22:24:14 +08:00
|
|
|
|
|
2026-01-15 21:42:03 +08:00
|
|
|
|
const area = document.getElementById('manualPrompt');
|
|
|
|
|
|
const promptTpl = document.getElementById('promptTpl');
|
|
|
|
|
|
const modelSelect = document.getElementById('modelSelect');
|
|
|
|
|
|
const sizeGroup = document.getElementById('sizeGroup');
|
2026-01-16 22:24:14 +08:00
|
|
|
|
|
2026-01-15 21:42:03 +08:00
|
|
|
|
// 1. 强制切换并锁定版本 2.0 (nano-banana-2)
|
|
|
|
|
|
if (modelSelect) {
|
|
|
|
|
|
modelSelect.value = 'nano-banana-2';
|
|
|
|
|
|
modelSelect.disabled = true; // 锁定选择
|
|
|
|
|
|
sizeGroup.classList.remove('hidden');
|
|
|
|
|
|
updateCostPreview();
|
|
|
|
|
|
}
|
2026-01-16 22:24:14 +08:00
|
|
|
|
|
2026-01-15 21:42:03 +08:00
|
|
|
|
// 2. 隐藏模板选择器
|
|
|
|
|
|
if (promptTpl) {
|
|
|
|
|
|
promptTpl.classList.add('hidden');
|
|
|
|
|
|
promptTpl.value = 'manual';
|
|
|
|
|
|
}
|
2026-01-16 22:24:14 +08:00
|
|
|
|
|
2026-01-15 21:42:03 +08:00
|
|
|
|
// 3. 强制图片数量为 1
|
|
|
|
|
|
if (uploadedFiles.length > 1) {
|
|
|
|
|
|
uploadedFiles = uploadedFiles.slice(0, 1);
|
|
|
|
|
|
renderImagePreviews();
|
|
|
|
|
|
}
|
2026-01-16 22:24:14 +08:00
|
|
|
|
|
2026-01-15 21:42:03 +08:00
|
|
|
|
// 4. 替换提示词
|
|
|
|
|
|
if (area) {
|
|
|
|
|
|
area.value = e.data.prompt;
|
|
|
|
|
|
area.classList.remove('hidden');
|
|
|
|
|
|
showToast('已同步拍摄角度并切换至 2.0 引擎', 'success');
|
|
|
|
|
|
}
|
2026-01-16 22:24:14 +08:00
|
|
|
|
|
2026-01-15 21:42:03 +08:00
|
|
|
|
closeVisualizerModal();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2026-01-17 23:15:58 +08:00
|
|
|
|
|
2026-01-23 21:46:08 +08:00
|
|
|
|
// --- 积分与钱包中心逻辑 (Modern Modal Version) ---
|
2026-01-17 23:15:58 +08:00
|
|
|
|
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);
|
|
|
|
|
|
|
2026-01-23 21:46:08 +08:00
|
|
|
|
// 加载核心数据
|
2026-01-17 23:15:58 +08:00
|
|
|
|
loadPointStats();
|
2026-01-23 21:46:08 +08:00
|
|
|
|
loadPointDetails(1);
|
|
|
|
|
|
loadInviteStatsModal();
|
2026-01-17 23:15:58 +08:00
|
|
|
|
|
2026-01-23 21:46:08 +08:00
|
|
|
|
// 实时更新余额显示
|
2026-01-17 23:15:58 +08:00
|
|
|
|
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');
|
2026-01-23 21:46:08 +08:00
|
|
|
|
if (!modal) return;
|
2026-01-17 23:15:58 +08:00
|
|
|
|
modal.classList.remove('opacity-100');
|
|
|
|
|
|
modal.querySelector('div').classList.add('scale-95');
|
|
|
|
|
|
setTimeout(() => modal.classList.add('hidden'), 300);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadPointStats() {
|
2026-01-23 21:46:08 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const r = await fetch('/api/auth/point_stats');
|
|
|
|
|
|
const d = await r.json();
|
2026-01-17 23:15:58 +08:00
|
|
|
|
|
2026-01-23 21:46:08 +08:00
|
|
|
|
const canvas = document.getElementById('modalTrendChart');
|
|
|
|
|
|
if (!canvas) return;
|
|
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
|
|
|
|
|
|
|
|
if (pointsChart) pointsChart.destroy();
|
|
|
|
|
|
|
|
|
|
|
|
pointsChart = new Chart(ctx, {
|
|
|
|
|
|
type: 'line',
|
|
|
|
|
|
data: {
|
|
|
|
|
|
labels: d.labels,
|
|
|
|
|
|
datasets: [
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '消耗积分',
|
|
|
|
|
|
data: d.consumption,
|
|
|
|
|
|
borderColor: '#6366f1',
|
|
|
|
|
|
backgroundColor: 'rgba(99, 102, 241, 0.05)',
|
|
|
|
|
|
borderWidth: 3,
|
|
|
|
|
|
fill: true,
|
|
|
|
|
|
tension: 0.4,
|
|
|
|
|
|
pointRadius: 4,
|
|
|
|
|
|
pointBackgroundColor: '#6366f1'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '充值积分',
|
|
|
|
|
|
data: d.recharge,
|
|
|
|
|
|
borderColor: '#10b981',
|
|
|
|
|
|
backgroundColor: 'rgba(16, 185, 129, 0.05)',
|
|
|
|
|
|
borderWidth: 3,
|
|
|
|
|
|
fill: true,
|
|
|
|
|
|
tension: 0.4,
|
|
|
|
|
|
pointRadius: 4,
|
|
|
|
|
|
pointBackgroundColor: '#10b981'
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
2026-01-17 23:15:58 +08:00
|
|
|
|
},
|
2026-01-23 21:46:08 +08:00
|
|
|
|
options: {
|
|
|
|
|
|
responsive: true,
|
|
|
|
|
|
maintainAspectRatio: false,
|
|
|
|
|
|
plugins: {
|
|
|
|
|
|
legend: { display: false },
|
|
|
|
|
|
tooltip: {
|
|
|
|
|
|
backgroundColor: '#1e293b',
|
|
|
|
|
|
padding: 12,
|
|
|
|
|
|
cornerRadius: 12,
|
|
|
|
|
|
titleFont: { weight: 'bold' },
|
|
|
|
|
|
bodyFont: { weight: '900' }
|
|
|
|
|
|
}
|
2026-01-17 23:15:58 +08:00
|
|
|
|
},
|
2026-01-23 21:46:08 +08:00
|
|
|
|
scales: {
|
|
|
|
|
|
y: {
|
|
|
|
|
|
beginAtZero: true,
|
|
|
|
|
|
grid: { color: '#f1f5f9' },
|
|
|
|
|
|
ticks: { font: { weight: 'bold', size: 10 }, color: '#94a3b8' }
|
|
|
|
|
|
},
|
|
|
|
|
|
x: {
|
|
|
|
|
|
grid: { display: false },
|
|
|
|
|
|
ticks: { font: { weight: 'bold', size: 10 }, color: '#94a3b8' }
|
|
|
|
|
|
}
|
2026-01-17 23:15:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-23 21:46:08 +08:00
|
|
|
|
});
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('加载统计图失败:', e);
|
|
|
|
|
|
}
|
2026-01-17 23:15:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-23 21:46:08 +08:00
|
|
|
|
async function loadPointDetails(page = 1) {
|
2026-01-17 23:15:58 +08:00
|
|
|
|
const body = document.getElementById('pointDetailsBody');
|
2026-01-23 21:46:08 +08:00
|
|
|
|
const pagContainer = document.getElementById('pointDetailsPagination');
|
2026-01-17 23:15:58 +08:00
|
|
|
|
if (!body) return;
|
2026-01-23 21:46:08 +08:00
|
|
|
|
|
|
|
|
|
|
body.innerHTML = '<tr><td colspan="4" class="px-8 py-10 text-center text-slate-400">正在同步账户流水...</td></tr>';
|
2026-01-17 23:15:58 +08:00
|
|
|
|
|
|
|
|
|
|
try {
|
2026-01-23 21:46:08 +08:00
|
|
|
|
const r = await fetch(`/api/auth/point_history?page=${page}&per_page=5`);
|
2026-01-17 23:15:58 +08:00
|
|
|
|
const d = await r.json();
|
|
|
|
|
|
|
2026-01-23 21:46:08 +08:00
|
|
|
|
if (d.details.length === 0) {
|
|
|
|
|
|
body.innerHTML = '<tr><td colspan="4" class="px-8 py-10 text-center text-slate-300 italic">暂无资金变动记录</td></tr>';
|
|
|
|
|
|
if (pagContainer) pagContainer.innerHTML = '';
|
2026-01-17 23:15:58 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-23 21:46:08 +08:00
|
|
|
|
body.innerHTML = d.details.map(item => {
|
|
|
|
|
|
const isAdd = item.amount.startsWith('+');
|
|
|
|
|
|
const colorClass = isAdd ? 'text-emerald-500' : 'text-rose-500';
|
|
|
|
|
|
const iconColor = isAdd ? 'bg-emerald-500' : 'bg-rose-500';
|
|
|
|
|
|
|
|
|
|
|
|
return `
|
|
|
|
|
|
<tr class="hover:bg-slate-50 transition-colors">
|
2026-01-17 23:15:58 +08:00
|
|
|
|
<td class="px-8 py-5">
|
2026-01-23 21:46:08 +08:00
|
|
|
|
<div class="flex items-center gap-3 text-slate-700">
|
|
|
|
|
|
<div class="w-2 h-2 rounded-full ${iconColor}"></div>
|
|
|
|
|
|
<div class="flex flex-col">
|
|
|
|
|
|
<span class="font-bold text-xs">${item.type_label}</span>
|
|
|
|
|
|
<span class="text-[10px] text-slate-400 truncate max-w-[200px]">${item.desc}</span>
|
|
|
|
|
|
</div>
|
2026-01-17 23:15:58 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</td>
|
2026-01-23 21:46:08 +08:00
|
|
|
|
<td class="px-8 py-5">
|
|
|
|
|
|
<span class="px-2 py-0.5 bg-slate-100 text-slate-500 rounded text-[9px] font-black uppercase tracking-tighter border border-slate-200">
|
|
|
|
|
|
${item.type === 'consumption' ? 'MODEL' : 'SYSTEM'}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td class="px-8 py-5">
|
|
|
|
|
|
<span class="${colorClass} font-black">${item.amount}</span>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td class="px-8 py-5 text-right font-bold text-slate-300 text-[10px] tracking-tighter">${item.time}</td>
|
2026-01-17 23:15:58 +08:00
|
|
|
|
</tr>
|
2026-01-23 21:46:08 +08:00
|
|
|
|
`;
|
|
|
|
|
|
}).join('');
|
|
|
|
|
|
|
|
|
|
|
|
// 渲染分页
|
|
|
|
|
|
if (pagContainer) {
|
|
|
|
|
|
if (d.pages > 1) {
|
|
|
|
|
|
let html = '';
|
|
|
|
|
|
if (d.current_page > 1) {
|
|
|
|
|
|
html += `<button onclick="loadPointDetails(${d.current_page - 1})" class="px-3 py-1.5 bg-white border border-slate-100 rounded-lg text-[10px] font-black hover:text-indigo-600 transition-all">Prev</button>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
html += `<span class="px-4 py-1.5 text-[9px] font-black text-slate-400 uppercase">Page ${d.current_page} / ${d.pages}</span>`;
|
|
|
|
|
|
if (d.current_page < d.pages) {
|
|
|
|
|
|
html += `<button onclick="loadPointDetails(${d.current_page + 1})" class="px-3 py-1.5 bg-white border border-slate-100 rounded-lg text-[10px] font-black hover:text-indigo-600 transition-all">Next</button>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
pagContainer.innerHTML = html;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
pagContainer.innerHTML = '';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-17 23:15:58 +08:00
|
|
|
|
} catch (e) {
|
2026-01-23 21:46:08 +08:00
|
|
|
|
body.innerHTML = '<tr><td colspan="4" class="px-8 py-10 text-center text-rose-400 font-bold">流水加载异常</td></tr>';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadInviteStatsModal() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const r = await fetch('/api/auth/invite_stats');
|
|
|
|
|
|
const d = await r.json();
|
|
|
|
|
|
if (d.invite_code) {
|
|
|
|
|
|
document.getElementById('modalInviteCode').innerText = d.invite_code;
|
|
|
|
|
|
document.getElementById('modalInvitedCount').innerText = d.invited_count;
|
|
|
|
|
|
document.getElementById('modalTotalRewards').innerText = d.total_rewards;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) { console.error('加载邀请统计失败', e); }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function copyInviteCodeModal() {
|
|
|
|
|
|
const code = document.getElementById('modalInviteCode').innerText;
|
|
|
|
|
|
if (code === '---') return;
|
|
|
|
|
|
const link = `${window.location.origin}/login?invite_code=${code}`;
|
|
|
|
|
|
navigator.clipboard.writeText(link).then(() => showToast('专属邀请链接已复制', 'success'));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function scrollToModalRecharge() {
|
|
|
|
|
|
const area = document.getElementById('modalRechargeArea');
|
|
|
|
|
|
if (area) area.scrollIntoView({ behavior: 'smooth' });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function submitModalRecharge(pkgId) {
|
|
|
|
|
|
const form = document.getElementById('modalRechargeForm');
|
|
|
|
|
|
const input = document.getElementById('modalPackageId');
|
|
|
|
|
|
if (form && input) {
|
|
|
|
|
|
input.value = pkgId;
|
|
|
|
|
|
form.submit();
|
2026-01-17 23:15:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-21 20:43:46 +08:00
|
|
|
|
|
|
|
|
|
|
// --- 自定义提示词逻辑 ---
|
|
|
|
|
|
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 = '<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i>';
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|