/** * AI生图弹出框 */ class AIGenerateView { constructor() { this.overlay = null; this.modal = null; this.previewImage = null; this.previewPlaceholder = null; this.referenceBox = null; this.referenceInput = null; this.referenceUploadArea = null; this.referenceImage = null; this.referenceImageWrapper = null; this.referenceRemoveBtn = null; this.generateBtn = null; this.isGenerateEnabled = false; this.additionalPromptInput = null; this.cancelBtn = null; this.imageData = null; this.referenceImageData = null; this.originalSpritesheetData = null; this.spritesheetLayout = null; this.folderName = null; // 当前会话的任务ID列表 this.sessionTaskIds = []; this.queuePollingTimer = null; this.init(); } init() { this.overlay = document.getElementById('aiGenerateOverlay'); this.modal = document.getElementById('aiGenerateModal'); this.previewImage = document.getElementById('previewImage'); this.previewPlaceholder = document.getElementById('previewPlaceholder'); this.referenceBox = document.getElementById('referenceBox'); this.referenceInput = document.getElementById('referenceInput'); this.referenceUploadArea = document.getElementById('referenceUploadArea'); this.referenceImage = document.getElementById('referenceImage'); this.referenceImageWrapper = document.getElementById('referenceImageWrapper'); this.referenceRemoveBtn = document.getElementById('referenceRemoveBtn'); this.generateBtn = document.getElementById('generateBtn'); this.aiGeneratePriceEl = document.getElementById('aiGeneratePrice'); this.additionalPromptInput = document.getElementById('additionalPromptInput'); this.cancelBtn = document.getElementById('aiGenerateCancelBtn'); // 加载AI生图价格 this.loadAIGeneratePrice(); this.bindEvents(); this.reset(); } bindEvents() { // 关闭按钮 this.cancelBtn?.addEventListener('click', () => { this.close(); }); // AI生图按钮 this.generateBtn?.addEventListener('click', () => { this.generateAI(); }); // 删除参考图按钮 this.referenceRemoveBtn?.addEventListener('click', (e) => { e.stopPropagation(); this.removeReferenceImage(); }); // 点击遮罩层关闭 this.overlay?.addEventListener('click', (e) => { if (e.target === this.overlay) { this.close(); } }); // ESC键关闭 document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && this.overlay) { this.close(); } }); // 参考图上传区域点击 this.referenceBox?.addEventListener('click', () => { this.referenceInput?.click(); }); // 参考图选择 this.referenceInput?.addEventListener('change', (e) => { const file = e.target.files[0]; if (file) { this.loadReferenceImage(file); } }); // 拖拽上传参考图 this.referenceBox?.addEventListener('dragover', (e) => { e.preventDefault(); e.stopPropagation(); if (this.referenceBox) { this.referenceBox.style.borderColor = '#667eea'; } }); this.referenceBox?.addEventListener('dragleave', (e) => { e.preventDefault(); e.stopPropagation(); if (this.referenceBox) { this.referenceBox.style.borderColor = '#e5e7eb'; } }); this.referenceBox?.addEventListener('drop', (e) => { e.preventDefault(); e.stopPropagation(); if (this.referenceBox) { this.referenceBox.style.borderColor = '#e5e7eb'; } const file = e.dataTransfer.files[0]; if (file && file.type.startsWith('image/')) { this.loadReferenceImage(file); } }); // 监听来自父窗口的消息 window.addEventListener('message', (event) => { if (event.data && event.data.type === 'show-ai-generate') { this.reset(); this.folderName = event.data.folderName; this.originalSpritesheetData = event.data.spritesheetData; this.spritesheetLayout = event.data.spritesheetLayout; this.showPreview(event.data.spritesheetData); } }); } /** * 显示预览图 */ showPreview(imageUrl) { if (!imageUrl) return; this.imageData = imageUrl; const img = new Image(); img.onload = () => { if (this.previewImage) { this.previewImage.src = imageUrl; this.previewImage.classList.add('show'); } if (this.previewPlaceholder) { this.previewPlaceholder.classList.add('hide'); } }; img.onerror = () => { if (this.previewPlaceholder) { const spinner = this.previewPlaceholder.querySelector('.loading-spinner'); const loadingText = this.previewPlaceholder.querySelector('.loading-text'); if (spinner) spinner.style.display = 'none'; if (loadingText) { loadingText.textContent = '图片加载失败'; loadingText.style.color = '#ef4444'; } this.previewPlaceholder.classList.remove('hide'); } }; img.src = imageUrl; } /** * 加载参考图 */ loadReferenceImage(file) { const reader = new FileReader(); reader.onload = (e) => { const data = e.target.result; this.referenceImageData = data; this.showReferenceImage(data); this.setGenerateButtonState(true); }; reader.readAsDataURL(file); } /** * 删除参考图 */ removeReferenceImage() { this.referenceImageData = null; if (this.referenceImageWrapper) { this.referenceImageWrapper.style.display = 'none'; } if (this.referenceImage) { this.referenceImage.src = ''; } if (this.referenceUploadArea) { this.referenceUploadArea.classList.remove('hide'); } if (this.referenceInput) { this.referenceInput.value = ''; } this.setGenerateButtonState(false); } /** * 设置生图按钮状态 * @param {boolean} enabled */ setGenerateButtonState(enabled) { this.isGenerateEnabled = !!enabled; if (this.generateBtn) { this.generateBtn.disabled = !enabled; } } /** * AI生图 */ async generateAI() { if (!this.originalSpritesheetData || !this.referenceImageData) { this.showAlert('请先上传参考图'); return; } const username = this.getCurrentUsername(); if (!username) { this.showAlert('请先登录'); return; } // 检查 Ani币余额 try { const pricingResponse = await fetch('/api/product-pricing'); if (!pricingResponse.ok) { throw new Error('获取价格失败'); } const pricingResult = await pricingResponse.json(); if (!pricingResult.success || !pricingResult.products) { throw new Error('获取价格失败'); } const aiGenerateProduct = pricingResult.products.find(p => p.id === 'ai-generate'); const price = aiGenerateProduct ? (aiGenerateProduct.price || 0) : 0; if (price > 0) { const pointsResponse = await fetch(`/api/user/points?username=${encodeURIComponent(username)}`); if (!pointsResponse.ok) { throw new Error('获取点数失败'); } const pointsResult = await pointsResponse.json(); if (!pointsResult.success) { throw new Error('获取点数失败'); } const userPoints = pointsResult.points || 0; if (userPoints < price) { if (window.parent && window.parent.postMessage) { window.parent.postMessage({ type: 'open-recharge-view', needPoints: price, currentPoints: userPoints }, '*'); } this.showAlert(`Ani币不足,需要 ${price} Ani币,您当前有 ${userPoints.toFixed(2)} Ani币。请先充值!`); return; } } } catch (error) { console.error('[AIGenerateView] 余额检查失败:', error); this.showAlert('检查余额失败:' + error.message); return; } // 禁用生图按钮 if (this.generateBtn) { this.generateBtn.disabled = true; } try { // 准备图片数据 // image1 使用原始spritesheet(模板),image2 使用参考图(角色设计) // 这样与 prompt 中的描述一致:image1 是模板布局,image2 是要应用的角色外观 const image1Base64 = this.originalSpritesheetData.replace(/^data:image\/\w+;base64,/, ''); const image2Base64 = this.referenceImageData.replace(/^data:image\/\w+;base64,/, ''); const image1Width = this.spritesheetLayout?.sheetWidth || 0; const image1Height = this.spritesheetLayout?.sheetHeight || 0; const additionalPrompt = this.additionalPromptInput?.value || ''; // 扣除Ani币 const pricingResponse = await fetch('/api/product-pricing'); const pricingResult = await pricingResponse.json(); const aiGenerateProduct = pricingResult.products?.find(p => p.id === 'ai-generate'); const price = aiGenerateProduct ? (aiGenerateProduct.price || 0) : 0; if (price > 0) { const deductResponse = await fetch('/api/user/deduct-points', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, points: price }) }); if (!deductResponse.ok) { const deductResult = await deductResponse.json(); throw new Error(deductResult.message || '扣除Ani币失败'); } const deductResult = await deductResponse.json(); if (!deductResult.success) { throw new Error(deductResult.message || '扣除Ani币失败'); } // 通知父窗口刷新点数 if (window.parent && window.parent.postMessage) { window.parent.postMessage({ type: 'refresh-points' }, '*'); } } // 调用队列API const response = await fetch('/api/ai/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: username, image1: image1Base64, image2: image2Base64, image1Width: image1Width, image1Height: image1Height, additionalPrompt: additionalPrompt }) }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(errorData.message || '请求生图失败'); } const result = await response.json(); if (result.success && result.taskId) { this.sessionTaskIds.push(result.taskId); this.showFlyAwayAnimation(); this.loadAIQueue(username); } else { throw new Error(result.message || '请求失败'); } } catch (error) { console.error('[AIGenerateView] 请求生图失败:', error); this.showAlert(error.message || '请求失败,请稍后重试'); } finally { if (this.generateBtn) { this.generateBtn.disabled = false; } } } /** * 加载AI生图队列 */ async loadAIQueue(username) { if (!this.sessionTaskIds || this.sessionTaskIds.length === 0) { const queueSection = document.getElementById('aiQueueSection'); if (queueSection) { queueSection.style.display = 'none'; } return; } if (!username) { username = this.getCurrentUsername(); } if (!username) return; try { const response = await fetch(`/api/ai/history?username=${encodeURIComponent(username)}`); if (!response.ok) return; const result = await response.json(); if (!result.success || !result.history) return; const sessionTasks = result.history.filter(t => this.sessionTaskIds.includes(t.id)); this.renderAIQueue(sessionTasks); // 如果有进行中的任务,继续轮询 const hasPending = sessionTasks.some(t => t.status === 'queued' || t.status === 'rendering'); if (hasPending) { if (this.queuePollingTimer) { clearTimeout(this.queuePollingTimer); } this.queuePollingTimer = setTimeout(() => { this.loadAIQueue(username); }, 3000); } } catch (error) { console.error('[AIGenerateView] 加载AI队列失败:', error); } } /** * 渲染AI队列 */ renderAIQueue(tasks) { const queueSection = document.getElementById('aiQueueSection'); const queueList = document.getElementById('aiQueueList'); if (!queueSection || !queueList) return; if (tasks.length === 0) { queueSection.style.display = 'none'; return; } queueSection.style.display = 'block'; queueList.innerHTML = tasks.map(task => this.createQueueItemHTML(task)).join(''); // 绑定点击预览事件 queueList.querySelectorAll('.ai-queue-item').forEach(item => { const taskId = item.dataset.taskId; const task = tasks.find(t => t.id === taskId); if (task && task.status === 'completed' && task.imageUrl) { item.style.cursor = 'pointer'; item.addEventListener('click', () => { this.showImagePreviewModal(task.imageUrl, task.id); }); } }); } /** * 创建队列项HTML */ createQueueItemHTML(task) { const previewUrl = task.previewUrl || ''; if (task.status === 'queued' || task.status === 'rendering') { const statusText = task.status === 'queued' ? '等待中' : '生成中'; return `