/** * 导出动画弹出框 */ class ExportView { constructor() { this.overlay = null; this.modal = null; this.previewImage = null; this.previewPlaceholder = null; this.cancelBtn = null; this.confirmBtn = null; this.floatingAIBtn = null; this.imageData = null; this.spritesheetCanvas = null; this.folderName = null; this.spritesheetLayout = null; this.replacedImageData = null; this.geminiOriginalImageData = null; this.originalSpritesheetData = null; this.skipPreviewUI = false; // 下载确认对话框相关 this.downloadConfirmOverlay = null; this.downloadConfirmClose = null; this.downloadOptions = null; this.init(); } init() { this.overlay = document.getElementById('exportOverlay'); this.modal = document.getElementById('exportModal'); this.previewImage = document.getElementById('previewImage'); this.previewPlaceholder = document.getElementById('previewPlaceholder'); this.cancelBtn = document.getElementById('exportCancelBtn'); this.confirmBtn = document.getElementById('exportConfirmBtn'); this.floatingAIBtn = document.getElementById('floatingAIBtn'); // 下载确认对话框元素 this.downloadConfirmOverlay = document.getElementById('downloadConfirmOverlay'); this.downloadConfirmClose = document.getElementById('downloadConfirmClose'); this.downloadOptions = document.querySelectorAll('.download-option'); // 确保对话框初始状态是隐藏的 if (this.downloadConfirmOverlay) { this.downloadConfirmOverlay.style.display = 'none'; } // 加载VIP抠图价格 this.loadVIPMattingPrice(); this.bindEvents(); // 初始时禁用确定按钮 if (this.confirmBtn) { this.confirmBtn.disabled = true; } this.reset(); } bindEvents() { // 取消按钮(右上角) this.cancelBtn?.addEventListener('click', () => { this.close(); }); // 取消按钮(底部操作栏) this.cancelBtnBottom?.addEventListener('click', () => { this.close(); }); // 确定按钮(下载) this.confirmBtn?.addEventListener('click', () => { this.handleConfirm(); }); // 悬浮AI按钮 - 打开AI生图界面 this.floatingAIBtn?.addEventListener('click', () => { this.openAIGenerateView(); }); // 点击遮罩层关闭 this.overlay?.addEventListener('click', (e) => { if (e.target === this.overlay) { this.close(); } }); // ESC键关闭 - 直接关闭整个界面 document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { this.hideDownloadConfirm(); this.close(); } }); // 下载确认对话框事件 - 关闭时同时关闭整个界面 this.downloadConfirmClose?.addEventListener('click', () => { this.hideDownloadConfirm(); this.close(); }); // 点击遮罩层关闭下载确认对话框 - 同时关闭整个界面 this.downloadConfirmOverlay?.addEventListener('click', (e) => { if (e.target === this.downloadConfirmOverlay) { this.hideDownloadConfirm(); this.close(); } }); // 下载选项点击事件 this.downloadOptions?.forEach(option => { option.addEventListener('click', () => { const downloadType = option.dataset.option; this.handleDownloadOption(downloadType); }); }); // 监听来自父窗口的消息 window.addEventListener('message', (event) => { if (event.data && event.data.type === 'show-export-preview') { const skipPreview = !!event.data.skipPreviewUI; const directDownloadType = event.data.directDownloadType; // 直接下载类型 this.reset(); this.skipPreviewUI = skipPreview; this.directDownloadType = directDownloadType; // 存储直接下载类型 this.prepareDirectPreview(event.data.imageUrl || event.data.imageData, event.data.fileName); } else if (event.data && event.data.type === 'generate-export-preview') { // console.log('[ExportView] 收到生成预览消息:', event.data); this.reset(); this.skipPreviewUI = !!event.data.skipPreviewUI; this.folderName = event.data.folderName; this.generatePreview(event.data.folderName); } }); } /** * 显示遮罩及弹窗元素 */ showOverlayUI() { if (this.overlay) { this.overlay.style.display = 'flex'; } if (this.modal) { this.modal.style.display = 'block'; } if (this.cancelBtn) { this.cancelBtn.style.display = 'block'; } if (this.floatingAIBtn) { this.floatingAIBtn.style.display = 'block'; } if (this.confirmBtn) { this.confirmBtn.style.display = 'block'; } } /** * 加载图片 * @param {string} src - 图片地址或base64 * @returns {Promise} */ loadImage(src) { return new Promise((resolve, reject) => { const img = new Image(); img.crossOrigin = 'anonymous'; img.onload = () => resolve(img); img.onerror = () => reject(new Error('无法加载图片')); img.src = src; }); } /** * 生成预览图 * @param {string} folderName - 文件夹名称 */ async generatePreview(folderName) { if (!folderName) { // console.warn('[ExportView] 没有提供文件夹名称'); if (this.previewPlaceholder) { this.previewPlaceholder.textContent = '没有提供文件夹名称'; } return; } // 重置状态(确保每次打开都是全新状态) this.reset(); // 显示遮罩并重置预览区域 if (!this.skipPreviewUI) { this.showOverlayUI(); if (this.previewPlaceholder) { this.previewPlaceholder.style.display = 'flex'; this.previewPlaceholder.classList.remove('hide'); } if (this.previewImage) { this.previewImage.style.display = 'none'; this.previewImage.classList.remove('show'); } } else { if (this.overlay) { this.overlay.style.display = 'none'; } if (this.modal) { this.modal.style.display = 'none'; } } // 保存文件夹名称 this.folderName = folderName; // 显示加载状态 if (!this.skipPreviewUI) { if (this.previewPlaceholder) { this.previewPlaceholder.classList.remove('hide'); } if (this.previewImage) { this.previewImage.classList.remove('show'); } } try { // 获取当前登录用户名 const username = this.getCurrentUsername(); if (!username) { throw new Error('请先登录'); } // 获取帧列表 const encodedFolderName = encodeURIComponent(folderName); let apiUrl = `http://localhost:3000/api/frames/${encodedFolderName}`; if (username) { apiUrl += `?username=${encodeURIComponent(username)}`; } const response = await fetch(apiUrl); if (!response.ok) { // 服务端返回错误,解析错误信息并显示 const errorMessage = await this.handleServerError(response, '无法获取帧列表'); throw new Error(errorMessage); } const data = await response.json(); const frameNumbers = data.frames || []; const fileNames = data.fileNames || []; if (frameNumbers.length === 0) { throw new Error('该文件夹中没有图片'); } // 加载所有图片(使用正确的API路径) const images = []; for (let i = 0; i < frameNumbers.length; i++) { const frameNum = frameNumbers[i]; // 使用API路径,从用户目录加载 let imgSrc; if (fileNames[i]) { // 使用实际文件名 const imagePath = `${folderName}/${fileNames[i]}`; imgSrc = `/api/disk/preview?username=${encodeURIComponent(username)}&path=${encodeURIComponent(imagePath)}`; } else { // 回退到使用帧号构造文件名 const frameName = frameNum.toString().padStart(2, '0'); const imagePath = `${folderName}/${frameName}.png`; imgSrc = `/api/disk/preview?username=${encodeURIComponent(username)}&path=${encodeURIComponent(imagePath)}`; } const img = await new Promise((resolve, reject) => { const image = new Image(); image.crossOrigin = 'anonymous'; image.onload = () => resolve(image); image.onerror = () => reject(new Error(`Failed to load image: ${imgSrc}`)); image.src = imgSrc; }); images.push({ img: img, width: img.width, height: img.height, frameNum: frameNum }); } // 计算布局(简化版,使用简单的网格布局) const frameWidth = images[0].width; const frameHeight = images[0].height; const cols = Math.ceil(Math.sqrt(images.length)); const rows = Math.ceil(images.length / cols); // 创建 Canvas 并绘制 const canvas = document.createElement('canvas'); canvas.width = frameWidth * cols; canvas.height = frameHeight * rows; const ctx = canvas.getContext('2d'); // 保存 canvas 和布局信息用于下载 this.spritesheetCanvas = canvas; // 填充透明背景 ctx.clearRect(0, 0, canvas.width, canvas.height); // 保存布局信息(用于生成 JSON) const layout = []; // 绘制所有图片 images.forEach((item, index) => { const col = index % cols; const row = Math.floor(index / cols); const x = col * frameWidth; const y = row * frameHeight; ctx.drawImage(item.img, x, y); // 保存布局信息 layout.push({ x: x, y: y, width: item.width, height: item.height, frameNum: item.frameNum }); }); // 保存布局信息 this.spritesheetLayout = { layout: layout, sheetWidth: canvas.width, sheetHeight: canvas.height }; // 转换为 base64 const imageUrl = await new Promise((resolve) => { canvas.toBlob((blob) => { const url = URL.createObjectURL(blob); resolve(url); }, 'image/png'); }); // 保存原始 spritesheet 的 base64 数据 this.originalSpritesheetData = await new Promise((resolve) => { canvas.toBlob((blob) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.readAsDataURL(blob); }, 'image/png'); }); // 显示预览图 this.showPreview(imageUrl); // 如果已经有参考图,显示替换按钮 if (this.referenceImageData && this.replaceBtn) { this.replaceBtn.style.display = 'block'; } // 预览图生成完成后,自动显示下载选项对话框 setTimeout(() => { this.showDownloadConfirm(); // 通知父窗口内容已准备好 if (window.parent && window.parent !== window) { window.parent.postMessage({ type: 'export-view-ready' }, '*'); } }, 300); } catch (error) { // console.error('[ExportView] 生成预览图失败:', error); 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 = '生成预览图失败: ' + error.message; loadingText.style.color = '#ef4444'; } this.previewPlaceholder.classList.remove('hide'); } } } /** * 处理直接传入的图片预览(AI 历史等) * @param {string} imageUrl - 图片URL或base64 * @param {string} fileName - 基础文件名 */ async prepareDirectPreview(imageUrl, fileName) { if (!imageUrl) { this.showAlert('没有可预览的图片'); return; } try { if (!this.skipPreviewUI) { this.showOverlayUI(); if (this.previewPlaceholder) { this.previewPlaceholder.style.display = 'flex'; this.previewPlaceholder.classList.remove('hide'); } } const img = await this.loadImage(imageUrl); const canvas = document.createElement('canvas'); canvas.width = img.width; canvas.height = img.height; const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(img, 0, 0); this.spritesheetCanvas = canvas; this.spritesheetLayout = { layout: [{ x: 0, y: 0, width: img.width, height: img.height, frameNum: 1 }], sheetWidth: img.width, sheetHeight: img.height }; const safeName = (fileName || 'ai-image').toString().replace(/[^a-zA-Z0-9_-]/g, '_') || 'ai-image'; this.folderName = safeName; this.originalSpritesheetData = canvas.toDataURL('image/png'); this.imageData = imageUrl; if (!this.skipPreviewUI) { if (this.previewImage) { this.previewImage.src = imageUrl; this.previewImage.style.display = 'block'; this.previewImage.classList.add('show'); } if (this.previewPlaceholder) { this.previewPlaceholder.style.display = 'none'; this.previewPlaceholder.classList.add('hide'); } } if (this.confirmBtn) { this.confirmBtn.disabled = false; } // 如果有直接下载类型,直接开始下载 if (this.directDownloadType) { // 通知父窗口内容已准备好 if (window.parent && window.parent !== window) { window.parent.postMessage({ type: 'export-view-ready' }, '*'); } const delay = 50; setTimeout(() => this.handleDownloadOption(this.directDownloadType), delay); } else { const delay = this.skipPreviewUI ? 50 : 200; setTimeout(() => { this.showDownloadConfirm(); // 通知父窗口内容已准备好 if (window.parent && window.parent !== window) { window.parent.postMessage({ type: 'export-view-ready' }, '*'); } }, delay); } } catch (error) { console.error('[ExportView] 处理直接预览失败:', error); this.showAlert(`加载预览失败: ${error.message}`); this.close(); } } /** * 计算宽高比 * @param {number} width - 宽度 * @param {number} height - 高度 * @returns {string} 宽高比字符串(例如:16:9) */ calculateAspectRatio(width, height) { // 计算最大公约数 const gcd = (a, b) => b === 0 ? a : gcd(b, a % b); const divisor = gcd(width, height); const ratioWidth = width / divisor; const ratioHeight = height / divisor; // 如果比例太大,使用简化版本 if (ratioWidth > 100 || ratioHeight > 100) { // 使用小数形式 const ratio = width / height; return ratio.toFixed(2) + ':1'; } return `${ratioWidth}:${ratioHeight}`; } /** * 显示预览图(Spritesheet)- 简化版,不再显示预览界面 * @param {string} imageUrl - 图片URL或base64数据 */ showPreview(imageUrl) { if (!imageUrl) { return; } // 保存图片数据 this.imageData = imageUrl; } /** * 生成 JSON 数据 * @param {string} folderName - 文件夹名称 * @param {Array} layout - 布局信息数组 * @param {number} sheetWidth - Spritesheet 宽度 * @param {number} sheetHeight - Spritesheet 高度 * @returns {string} JSON 字符串 */ generateJSON(folderName, layout, sheetWidth, sheetHeight) { const frames = {}; layout.forEach((item, index) => { // 使用实际的帧号,确保与原始文件名一致 const frameNum = item.frameNum ? item.frameNum.toString().padStart(2, '0') : (index + 1).toString().padStart(2, '0'); const frameName = `${frameNum}.png`; const x = item.x; const y = item.y; const width = item.width; const height = item.height; // 标准的 TexturePacker JSON 格式,Cocos Creator 3.8 完全兼容 frames[frameName] = { frame: { x: x, y: y, w: width, h: height }, rotated: false, trimmed: false, spriteSourceSize: { x: 0, y: 0, w: width, h: height }, sourceSize: { w: width, h: height } }; }); // Cocos Creator 3.8 兼容的 TexturePacker JSON 格式 const json = { frames: frames, meta: { app: "https://www.codeandweb.com/texturepacker", version: "1.0", image: `${folderName}.png`, format: "RGBA8888", size: { w: sheetWidth, h: sheetHeight }, scale: 1 } }; return JSON.stringify(json, null, 2); } /** * 将 Blob 转换为 Base64 * @param {Blob} blob - Blob 对象 * @returns {Promise} Base64 字符串 */ blobToBase64(blob) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onloadend = () => { try { let base64 = reader.result; // 移除 data:image/png;base64, 前缀(如果存在) if (base64 && base64.includes(',')) { base64 = base64.split(',')[1]; } if (!base64) { reject(new Error('Base64 转换失败:结果为空')); return; } resolve(base64); } catch (error) { reject(new Error(`Base64 转换失败: ${error.message}`)); } }; reader.onerror = () => { reject(new Error('文件读取失败')); }; reader.readAsDataURL(blob); }); } /** * 下载文件 * @param {Blob} data - 文件数据 * @param {string} filename - 文件名 * @param {string} mimeType - MIME 类型 */ downloadFile(data, filename, mimeType) { const blob = new Blob([data], { type: mimeType }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } /** * 显示下载确认对话框 */ showDownloadConfirm() { // 确保预览遮罩被隐藏,避免只看到一条空白横条 if (this.overlay) { this.overlay.style.display = 'none'; } if (this.modal) { this.modal.style.display = 'none'; } if (this.downloadConfirmOverlay) { console.log('[ExportView] 显示下载确认对话框'); // 确保价格已加载 this.loadVIPMattingPrice(); this.downloadConfirmOverlay.style.display = 'flex'; } else { console.error('[ExportView] 下载确认对话框元素未找到'); } } /** * 隐藏下载确认对话框 */ hideDownloadConfirm() { if (this.downloadConfirmOverlay) { this.downloadConfirmOverlay.style.display = 'none'; } } /** * 打开AI生图界面 */ openAIGenerateView() { if (!this.originalSpritesheetData) { this.showAlert('请先生成预览图'); return; } // 通知父窗口打开AI生图界面 if (window.parent && window.parent !== window) { window.parent.postMessage({ type: 'open-ai-generate-view', folderName: this.folderName, spritesheetData: this.originalSpritesheetData, spritesheetLayout: this.spritesheetLayout }, '*'); } } /** * 处理下载选项选择 * @param {string} downloadType - 下载类型:'original', 'normal', 'vip' */ async handleDownloadOption(downloadType) { // 如果是VIP抠图,先检查用户Ani币是否足够 if (downloadType === 'vip') { const username = this.getCurrentUsername(); if (!username) { this.showAlert('请先登录'); return; } try { // 获取VIP抠图价格 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 vipMattingProduct = pricingResult.products.find(p => p.id === 'vip-matting'); const price = vipMattingProduct ? (vipMattingProduct.price || 0) : 0; // 如果价格为0,直接继续 if (price === 0) { // 继续执行VIP抠图流程 } else { // 检查用户点数 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 !== window) { window.parent.postMessage({ type: 'open-recharge-view', needPoints: price, currentPoints: userPoints }, '*'); } this.hideDownloadConfirm(); return; } // 点数充足,弹出确认对话框 let confirmed = false; if (window.parent && window.parent.GlobalConfirm) { confirmed = await window.parent.GlobalConfirm.show( `确定要花费 ${price} Ani币使用VIP抠图吗?` ); } else { confirmed = confirm(`确定要花费 ${price} Ani币使用VIP抠图吗?`); } if (!confirmed) { return; } // 扣除点数 const deductResponse = await fetch('/api/user/deduct-points', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: username, points: price }) }); if (!deductResponse.ok) { const deductResult = await deductResponse.json(); throw new Error(deductResult.message || '扣除点数失败'); } const deductResult = await deductResponse.json(); if (!deductResult.success) { throw new Error(deductResult.message || '扣除点数失败'); } // 通知父窗口刷新点数 if (window.parent && window.parent !== window) { window.parent.postMessage({ type: 'refresh-points' }, '*'); } } // VIP抠图:提交到队列,显示飞走动画 await this.submitVIPMattingToQueue(username); return; // 直接返回,不继续执行后续的即时处理逻辑 } catch (error) { console.error('[ExportView] VIP抠图购买检查失败:', error); this.showAlert(error.message || '操作失败,请稍后重试'); return; } } this.hideDownloadConfirm(); try { // 显示加载状态(但不隐藏预览图片) // 使用全局 Loading 提示,不干扰预览图显示 if (window.parent && window.parent.postMessage) { window.parent.postMessage({ type: 'global-loading', action: 'show', text: downloadType === 'original' ? '正在准备下载...' : '正在处理图片...' }, '*'); } let processedImageBase64; // 确定使用的图片源 if (this.geminiOriginalImageData) { // 如果有 Gemini 图片,使用 Gemini 图片 const geminiImageBase64 = this.geminiOriginalImageData.replace(/^data:image\/\w+;base64,/, ''); if (downloadType === 'original') { // 源文件下载:直接使用原始图片 processedImageBase64 = geminiImageBase64; } else if (downloadType === 'normal') { // 普通抠图:调用 rembg-matting.py const response = await fetch('http://localhost:3000/api/matting-normal', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ imageBase64: geminiImageBase64 }) }); if (!response.ok) { const errorMessage = await this.handleServerError(response, '普通抠图失败'); throw new Error(errorMessage); } const result = await response.json(); if (!result.success || !result.imageData) { throw new Error('普通抠图处理失败'); } processedImageBase64 = result.imageData; } else if (downloadType === 'vip') { // VIP抠图:调用 BiRefNet const response = await fetch('http://localhost:3000/api/matting-vip', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ imageBase64: geminiImageBase64 }) }); if (!response.ok) { const errorMessage = await this.handleServerError(response, 'VIP抠图失败'); throw new Error(errorMessage); } const result = await response.json(); if (!result.success || !result.imageData) { throw new Error('VIP抠图处理失败'); } processedImageBase64 = result.imageData; } } else { // 如果没有 Gemini 图片,使用原始 spritesheet const imageBlob = await new Promise((resolve, reject) => { this.spritesheetCanvas.toBlob((blob) => { if (blob) { resolve(blob); } else { reject(new Error('Canvas 转换失败')); } }, 'image/png'); }); // 将图片转换为 Base64 const originalImageBase64 = await this.blobToBase64(imageBlob); if (downloadType === 'original') { // 源文件下载:直接使用原始 spritesheet processedImageBase64 = originalImageBase64; } else if (downloadType === 'normal') { // 普通抠图:对原始 spritesheet 进行抠图 const response = await fetch('http://localhost:3000/api/matting-normal', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ imageBase64: originalImageBase64 }) }); if (!response.ok) { const errorMessage = await this.handleServerError(response, '普通抠图失败'); throw new Error(errorMessage); } const result = await response.json(); if (!result.success || !result.imageData) { throw new Error('普通抠图处理失败'); } processedImageBase64 = result.imageData; } else if (downloadType === 'vip') { // VIP抠图:对原始 spritesheet 进行 VIP 抠图 const response = await fetch('http://localhost:3000/api/matting-vip', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ imageBase64: originalImageBase64 }) }); if (!response.ok) { const errorMessage = await this.handleServerError(response, 'VIP抠图失败'); throw new Error(errorMessage); } const result = await response.json(); if (!result.success || !result.imageData) { throw new Error('VIP抠图处理失败'); } processedImageBase64 = result.imageData; } } // 生成 JSON 数据 const folderName = this.folderName.split('/').pop() || 'spritesheet'; const jsonData = this.generateJSON( folderName, this.spritesheetLayout.layout, this.spritesheetLayout.sheetWidth, this.spritesheetLayout.sheetHeight ); // 发送到服务器打包 const response = await fetch('http://localhost:3000/api/pack', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ folderName: folderName, imageData: processedImageBase64, jsonData: jsonData }) }); if (!response.ok) { const errorMessage = await this.handleServerError(response, '打包失败'); throw new Error(errorMessage); } // 获取 ZIP 文件的 Blob const zipBlob = await response.blob(); // 下载 ZIP 文件 this.downloadFile(zipBlob, `${folderName}.zip`, 'application/zip'); // 隐藏全局 Loading 提示 if (window.parent && window.parent.postMessage) { window.parent.postMessage({ type: 'global-loading', action: 'hide' }, '*'); } // 下载完成后,不关闭弹出框,不刷新图片 // 显示成功提示 this.showAlert('下载成功!'); } catch (error) { // console.error('[ExportView] 下载失败:', error); // 隐藏全局 Loading 提示 if (window.parent && window.parent.postMessage) { window.parent.postMessage({ type: 'global-loading', action: 'hide' }, '*'); } this.showAlert(`下载失败: ${error.message}`); } } /** * 处理下载按钮点击 */ async handleConfirm() { // console.log('[ExportView] 用户点击下载按钮'); if (!this.spritesheetCanvas || !this.spritesheetLayout) { // console.warn('[ExportView] 没有可下载的 Spritesheet'); return; } // 总是显示下载确认对话框,让用户选择下载方式 console.log('[ExportView] 显示下载选项对话框'); this.showDownloadConfirm(); } /** * 下载 Spritesheet(原始逻辑) */ async downloadSpritesheet() { try { // 生成 JSON 数据 const folderName = this.folderName.split('/').pop() || 'spritesheet'; const jsonData = this.generateJSON( folderName, this.spritesheetLayout.layout, this.spritesheetLayout.sheetWidth, this.spritesheetLayout.sheetHeight ); // 确定使用哪个图片:如果有替换后的图片,使用替换后的;否则使用原始的 let imageBase64; if (this.replacedImageData) { // 使用替换后的图片(移除 data:image/png;base64, 前缀) imageBase64 = this.replacedImageData.replace(/^data:image\/\w+;base64,/, ''); } else { // 使用原始 spritesheet const imageBlob = await new Promise((resolve, reject) => { this.spritesheetCanvas.toBlob((blob) => { if (blob) { resolve(blob); } else { reject(new Error('Canvas 转换失败')); } }, 'image/png'); }); // 将图片转换为 Base64 imageBase64 = await this.blobToBase64(imageBlob); } // 验证数据 if (!imageBase64 || typeof imageBase64 !== 'string' || imageBase64.trim().length === 0) { throw new Error('图片数据无效'); } if (!jsonData || typeof jsonData !== 'string' || jsonData.trim().length === 0) { throw new Error('JSON 数据无效'); } // 发送到服务器打包 const response = await fetch('http://localhost:3000/api/pack', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ folderName: folderName, imageData: imageBase64, jsonData: jsonData }) }); if (!response.ok) { const errorMessage = await this.handleServerError(response, '打包失败'); throw new Error(errorMessage); } // 获取 ZIP 文件的 Blob const zipBlob = await response.blob(); // 下载 ZIP 文件 this.downloadFile(zipBlob, `${folderName}.zip`, 'application/zip'); // 下载完成后,不关闭弹出框,不刷新图片 // 显示成功提示 this.showAlert('下载成功!'); } catch (error) { // console.error('[ExportView] 下载失败:', error); this.showAlert(`下载失败: ${error.message}`); } } /** * 关闭弹出框 */ close() { // console.log('[ExportView] 关闭导出弹出框'); // 清空所有数据 this.reset(); // 隐藏界面元素 if (this.overlay) { this.overlay.style.display = 'none'; } if (this.modal) { this.modal.style.display = 'none'; } this.hideDownloadConfirm(); // 通知父窗口关闭弹出框 if (window.parent && window.parent !== window) { window.parent.postMessage({ type: 'close-export-view' }, '*'); } } /** * 获取当前登录用户名 */ getCurrentUsername() { try { const loginDataStr = localStorage.getItem('loginData'); if (!loginDataStr) { return null; } const loginData = JSON.parse(loginDataStr); const now = Date.now(); // 检查是否过期 if (now >= loginData.expireTime) { localStorage.removeItem('loginData'); return null; } return loginData.user ? loginData.user.username : null; } catch (error) { console.error('[ExportView] 获取用户名失败:', error); return null; } } /** * 重置所有数据和UI状态 */ reset() { // 清空数据属性 this.imageData = null; this.spritesheetCanvas = null; this.folderName = null; this.spritesheetLayout = null; this.replacedImageData = null; this.geminiOriginalImageData = null; this.originalSpritesheetData = null; this.skipPreviewUI = false; this.directDownloadType = null; // 重置AI按钮状态 if (this.floatingAIBtn) { this.floatingAIBtn.disabled = true; } // 重置预览图区域 if (this.previewImage) { this.previewImage.src = ''; this.previewImage.classList.remove('show'); } if (this.previewPlaceholder) { const spinner = this.previewPlaceholder.querySelector('.loading-spinner'); const loadingText = this.previewPlaceholder.querySelector('.loading-text'); if (spinner) spinner.style.display = 'block'; if (loadingText) { loadingText.textContent = '正在生成预览图...'; loadingText.style.color = '#6b7280'; } this.previewPlaceholder.classList.remove('hide'); } // 重置按钮状态 if (this.confirmBtn) { this.confirmBtn.disabled = true; } } /** * 加载VIP抠图价格 */ async loadVIPMattingPrice() { try { const response = await fetch('/api/product-pricing'); if (response.ok) { const result = await response.json(); if (result.success && result.products) { const vipMattingProduct = result.products.find(p => p.id === 'vip-matting'); const priceEl = document.getElementById('vipMattingPrice'); if (vipMattingProduct && priceEl) { const price = vipMattingProduct.price || 0; if (price > 0) { priceEl.textContent = `${price} Ani币`; } else { priceEl.textContent = '免费'; } } } } } catch (error) { console.error('[ExportView] 加载VIP抠图价格失败:', error); const priceEl = document.getElementById('vipMattingPrice'); if (priceEl) { priceEl.textContent = '-'; } } } /** * 提交VIP抠图任务到队列 * @param {string} username - 用户名 */ async submitVIPMattingToQueue(username) { try { // 准备图片数据 let imageBase64; if (this.geminiOriginalImageData) { imageBase64 = this.geminiOriginalImageData.replace(/^data:image\/\w+;base64,/, ''); } else if (this.spritesheetCanvas) { const imageBlob = await new Promise((resolve, reject) => { this.spritesheetCanvas.toBlob((blob) => { if (blob) { resolve(blob); } else { reject(new Error('Canvas 转换失败')); } }, 'image/png'); }); imageBase64 = await this.blobToBase64(imageBlob); } else { throw new Error('没有可处理的图片'); } // 获取文件名 const folderName = this.folderName ? this.folderName.split('/').pop() : 'vip-matting'; // 提交到VIP抠图队列API const response = await fetch('/api/vip-matting/queue', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: username, imageBase64: imageBase64, fileName: folderName, jsonData: this.spritesheetLayout ? this.generateJSON( folderName, this.spritesheetLayout.layout, this.spritesheetLayout.sheetWidth, this.spritesheetLayout.sheetHeight ) : null }) }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(errorData.message || 'VIP抠图任务提交失败'); } const result = await response.json(); if (result.success && result.taskId) { // 隐藏对话框 this.hideDownloadConfirm(); // 显示飞走动画 this.showVIPMattingFlyAnimation(); // 通知父窗口刷新任务历史 if (window.parent && window.parent !== window) { window.parent.postMessage({ type: 'refresh-ai-history' }, '*'); } // 关闭导出视图 setTimeout(() => { this.close(); }, 500); } else { throw new Error(result.message || 'VIP抠图任务提交失败'); } } catch (error) { console.error('[ExportView] VIP抠图队列提交失败:', error); this.showAlert(error.message || 'VIP抠图任务提交失败,请稍后重试'); } } /** * 显示VIP抠图飞走动画 */ showVIPMattingFlyAnimation() { let targetDocument = document; let targetWindow = window; try { if (window.parent && window.parent !== window && window.parent.document) { targetDocument = window.parent.document; targetWindow = window.parent; } } catch (e) {} const flyElement = targetDocument.createElement('div'); flyElement.innerHTML = `
VIP抠图任务
`; let startLeft = targetWindow.innerWidth / 2; let startTop = targetWindow.innerHeight / 2; flyElement.style.cssText = ` position: fixed; left: ${startLeft}px; top: ${startTop}px; z-index: 999999; pointer-events: none; transition: all 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94); opacity: 1; transform: scale(1); background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%); color: white; padding: 12px 20px; border-radius: 12px; box-shadow: 0 8px 32px rgba(139, 92, 246, 0.4); display: flex; align-items: center; gap: 8px; `; targetDocument.body.appendChild(flyElement); requestAnimationFrame(() => { flyElement.style.left = `${targetWindow.innerWidth - 100}px`; flyElement.style.top = '50px'; flyElement.style.opacity = '0'; flyElement.style.transform = 'scale(0.3)'; }); setTimeout(() => { if (window.parent && window.parent.HintView) { window.parent.HintView.success('VIP抠图任务已添加到「我的」-「任务历史」,请稍后查看', 3000); } }, 300); setTimeout(() => { if (flyElement.parentNode) { flyElement.parentNode.removeChild(flyElement); } }, 1000); } /** * 显示提示信息(使用全局 Alert 组件,直接调用) * @param {string} message - 提示信息 * @param {number} duration - 显示时长(毫秒),默认2000 */ showAlert(message, duration = 2000) { // 直接调用父窗口的 GlobalAlert(不通过 postMessage) try { // 优先使用父窗口的 GlobalAlert if (window.parent && window.parent !== window && window.parent.GlobalAlert) { window.parent.GlobalAlert.show(message, duration); return; } // 如果不在 iframe 中,直接使用当前窗口的 GlobalAlert if (window.GlobalAlert) { window.GlobalAlert.show(message, duration); return; } // 降级处理 console.log('[Alert]', message); } catch (error) { console.error('[ExportView] 显示 alert 失败:', error); alert(message); } } // 统一的错误处理函数:解析服务端错误响应并显示 async handleServerError(response, defaultMessage = '操作失败') { let errorMessage = defaultMessage; try { // 尝试解析 JSON 错误响应 const errorData = await response.json().catch(() => null); if (errorData) { // 优先使用服务端返回的 message if (errorData.message) { errorMessage = errorData.message; } else if (errorData.error) { errorMessage = errorData.error; } else if (typeof errorData === 'string') { errorMessage = errorData; } } } catch (e) { // 如果解析失败,使用默认消息或状态码 if (response.status) { errorMessage = `${defaultMessage} (状态码: ${response.status})`; } } this.showAlert(errorMessage); return errorMessage; } } // 初始化 window.ExportView = new ExportView();