(function () { // 主预览卡片类 class PreviewCard { constructor(container, options = {}) { this.container = container; this.options = { fps: 8, onFpsChange: null, ...options }; this.previewImage = null; this.loadingOverlay = null; this.dropHint = null; this.imageError = null; this.fpsSlider = null; this.fpsValue = null; this.dropZone = null; this.currentFps = this.options.fps; this.frameList = []; this.currentFrameIndex = 0; this.animationTimer = null; this.frameSourceMode = "remote"; this.localFrameResources = []; this.currentFolderName = ''; // 当前登录用户名 this.currentUsername = null; this.init(); this.initUserListener(); } // 获取当前登录用户名 getCurrentUsername() { // 如果已经缓存了用户名,直接返回 if (this.currentUsername) { return this.currentUsername; } // 从导航栏 iframe 中获取用户名 try { let targetWindow = window.parent; while (targetWindow && targetWindow !== window) { try { const navigationFrame = targetWindow.document.getElementById('navigationFrame'); if (navigationFrame && navigationFrame.contentWindow) { const navWindow = navigationFrame.contentWindow; const navDoc = navigationFrame.contentDocument || navWindow.document; // 检查用户是否已登录 const userAvatarContainer = navDoc.getElementById('userAvatarContainer'); if (userAvatarContainer) { const computedStyle = navDoc.defaultView.getComputedStyle(userAvatarContainer); if (computedStyle.display !== 'none') { // 用户已登录,从 userAvatar 的 alt 属性获取用户名 const userAvatar = navDoc.getElementById('userAvatar'); if (userAvatar && userAvatar.alt && userAvatar.alt !== '用户头像') { const username = userAvatar.alt; this.currentUsername = username; return username; } } } } } catch (e) { // 跨域或访问限制,继续尝试上层窗口 } if (targetWindow.parent && targetWindow.parent !== targetWindow) { targetWindow = targetWindow.parent; } else { break; } } } catch (error) { console.warn('[PreviewCard] 无法获取用户名:', error); } return this.currentUsername; } // 监听登录成功消息 initUserListener() { window.addEventListener('message', (event) => { if (event.data && event.data.type === 'login-success' && event.data.user) { this.currentUsername = event.data.user.username; } else if (event.data && event.data.type === 'logout') { this.currentUsername = null; } }); } async init() { await this.loadTemplate(); this.bindElements(); this.bindEvents(); this.setStagePlaceholderVisible(true); // 从 localStorage 恢复登录状态 this.restoreLoginFromStorage(); } // 从 localStorage 恢复登录状态 restoreLoginFromStorage() { try { const loginDataStr = localStorage.getItem('loginData'); if (!loginDataStr) { return; } const loginData = JSON.parse(loginDataStr); const now = Date.now(); // 检查是否过期 if (now >= loginData.expireTime) { localStorage.removeItem('loginData'); return; } // 未过期,恢复登录状态 if (loginData.user && loginData.user.username) { this.currentUsername = loginData.user.username; console.log('[PreviewCard] 从 localStorage 恢复登录状态:', loginData.user.username); } } catch (error) { console.error('[PreviewCard] 恢复登录状态失败:', error); } } async loadTemplate() { const response = await fetch('./card.html'); const html = await response.text(); const wrapper = document.createElement('div'); wrapper.innerHTML = html.trim(); const template = wrapper.querySelector('#preview-card-template'); if (!template) { throw new Error('Preview card template not found'); } const content = template.content.cloneNode(true); this.container.appendChild(content); } bindElements() { this.previewImage = this.container.querySelector('.preview-image'); this.loadingOverlay = this.container.querySelector('.loading-overlay'); this.loadingText = this.container.querySelector('.loading-text'); this.dropHint = this.container.querySelector('.drop-hint'); this.imageError = this.container.querySelector('.image-error'); this.fpsSlider = this.container.querySelector('.fps-slider'); this.fpsValue = this.container.querySelector('.fps-value'); this.dropZone = this.container.querySelector('.preview-card-stage'); this.infoBar = this.container.querySelector('.preview-info-bar'); this.folderNameElement = this.container.querySelector('.folder-name'); this.btnExport = this.container.querySelector('.btn-export'); this.btnDownload = this.container.querySelector('.btn-download'); this.btnAI = this.container.querySelector('.btn-ai'); } bindEvents() { // FPS控制 if (this.fpsSlider) { this.fpsSlider.addEventListener('input', () => { const value = parseInt(this.fpsSlider.value, 10) || this.currentFps; this.setFps(value); if (this.fpsValue) { this.fpsValue.textContent = `${value} FPS`; } if (typeof this.options.onFpsChange === 'function') { this.options.onFpsChange(value); } }); } // 拖放事件 if (this.dropZone) { this.dropZone.addEventListener('dragenter', (e) => this.handleDragEnter(e)); this.dropZone.addEventListener('dragover', (e) => this.handleDragOver(e)); this.dropZone.addEventListener('dragleave', (e) => this.handleDragLeave(e)); this.dropZone.addEventListener('drop', (e) => this.handleDrop(e)); } // 导出按钮(旧版兼容) if (this.btnExport) { this.btnExport.addEventListener('click', () => this.handleExport()); } // 下载按钮 if (this.btnDownload) { this.btnDownload.addEventListener('click', () => this.handleExport()); } // AI生图按钮 if (this.btnAI) { this.btnAI.addEventListener('click', () => this.handleAIGenerate()); } } handleDragEnter(event) { event.preventDefault(); event.stopPropagation(); this.dropZone.classList.add('is-dragging'); } handleDragOver(event) { event.preventDefault(); event.stopPropagation(); } handleDragLeave(event) { event.preventDefault(); if (!this.dropZone.contains(event.relatedTarget)) { this.dropZone.classList.remove('is-dragging'); } } async handleDrop(event) { event.preventDefault(); event.stopPropagation(); this.dropZone.classList.remove('is-dragging'); // 获取拖入的文件夹名称 const transfer = event.dataTransfer; if (!transfer || !transfer.items || transfer.items.length === 0) { // console.warn('[PreviewCard] No data transfer items'); return; } // 获取文本数据(可能是JSON对象或文件夹名称) const item = transfer.items[0]; if (item.kind === 'string' && item.type === 'text/plain') { item.getAsString(async (dataString) => { // console.log('[PreviewCard] Dropped folder:', dataString); // 尝试解析为JSON对象 let folderName = dataString; let fileType = 'directory'; // 默认假设是文件夹 let pngCount = undefined; // PNG文件数量(如果有) try { const data = JSON.parse(dataString); // 如果是对象,提取路径、名称和类型 if (data && typeof data === 'object') { folderName = data.path || data.name || dataString; fileType = data.type || 'directory'; pngCount = data.pngCount; // 从拖拽数据中获取PNG数量 } } catch (e) { // 如果不是JSON,直接使用原始字符串 folderName = dataString; } // 验证:必须是文件夹 if (fileType !== 'directory') { this.showError('❌ 请拖入文件夹,不支持单个文件'); return; } // 验证:文件夹中是否包含PNG图片 const isValid = await this.validateFolderHasPNG(folderName, pngCount); if (!isValid) { this.showError('❌ 该文件夹不包含PNG图片'); return; } // console.log('[PreviewCard] Resolved folder name:', folderName); await this.loadAndCacheFolderAnimation(folderName); }); } } async validateFolderHasPNG(folderName, pngCount) { try { // 1. 优先使用传递的pngCount(来自拖拽数据) if (pngCount !== undefined) { // console.log('[PreviewCard] 使用缓存的pngCount:', pngCount); return pngCount > 0; } // 2. 尝试从DiskManager的缓存中获取 if (window.diskManager && window.diskManager.getFileFromCache) { const cachedFile = window.diskManager.getFileFromCache(folderName); if (cachedFile && cachedFile.pngCount !== undefined) { // console.log('[PreviewCard] 从DiskManager缓存获取pngCount:', cachedFile.pngCount); return cachedFile.pngCount > 0; } } // 3. 最后才请求服务器(使用新的disk API) // console.log('[PreviewCard] 请求服务器验证文件夹:', folderName); const username = this.getCurrentUsername(); if (!username) { return false; } const response = await fetch(`/api/disk/list?username=${encodeURIComponent(username)}&path=${encodeURIComponent(folderName)}`); if (!response.ok) { return false; } const data = await response.json(); // 检查是否是文件夹并且有PNG文件 if (data.success && data.files) { // 检查当前路径对应的文件夹信息 // 注意:list API返回的是文件夹内的文件列表,不是文件夹本身 // 所以我们需要统计PNG文件数量 const pngFiles = data.files.filter(f => f.type === 'file' && f.name.toLowerCase().endsWith('.png') ); return pngFiles.length > 0; } return false; } catch (error) { // console.error('[PreviewCard] 验证文件夹失败:', error); return false; } } async loadAndCacheFolderAnimation(folderName) { try { // 显示加载动画 this.showLoading(true); this.hideError(); this.setStagePlaceholderVisible(false); // console.log('[PreviewCard] 开始加载文件夹:', folderName); // 1. 从网盘系统获取该文件夹的文件列表 const username = this.getCurrentUsername(); if (!username) { throw new Error('请先登录'); } const listResponse = await fetch(`/api/disk/list?username=${encodeURIComponent(username)}&path=${encodeURIComponent(folderName)}`); if (!listResponse.ok) { throw new Error('获取文件列表失败'); } const listData = await listResponse.json(); // console.log('[PreviewCard] 文件列表响应:', listData); if (!listData.success || !listData.files) { throw new Error('获取文件列表失败'); } // 2. 筛选出PNG文件并按文件名排序 const pngFiles = listData.files .filter(f => f.type === 'file' && f.name.toLowerCase().endsWith('.png')) .sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true })); // console.log('[PreviewCard] PNG文件列表:', pngFiles.map(f => f.name)); if (!pngFiles.length) { throw new Error('文件夹中没有可用的PNG图片'); } // console.log('[PreviewCard] 找到 PNG 文件数量:', pngFiles.length); // 3. 打开或创建缓存 const cache = await caches.open('animation-frames-v1'); // 4. 构造帧URL列表(使用文件的实际名称) const frameUrls = pngFiles.map(file => { // 使用文件的完整路径(file.path)来构造URL const username = this.getCurrentUsername(); if (!username) { return null; // 未登录时返回 null } return `/api/disk/preview?username=${encodeURIComponent(username)}&path=${encodeURIComponent(file.path)}`; }); // console.log('[PreviewCard] 开始下载和缓存图片...'); // 5. 逐个下载并缓存 const cachedFrames = []; for (let i = 0; i < frameUrls.length; i++) { const url = frameUrls[i]; const fileName = pngFiles[i].name; // 更新进度 this.showLoading(true, `正在缓存图片... (${i + 1}/${frameUrls.length})`); // 检查缓存中是否已存在 const cachedResponse = await cache.match(url); if (cachedResponse) { // console.log(`[PreviewCard] [${i + 1}/${frameUrls.length}] 从缓存加载: ${fileName}`); cachedFrames.push({ url, index: i, name: fileName }); } else { // 下载并缓存 try { // console.log(`[PreviewCard] [${i + 1}/${frameUrls.length}] 下载: ${fileName}`); const response = await fetch(url); if (response.ok) { await cache.put(url, response.clone()); // console.log(`[PreviewCard] ✓ 已缓存: ${fileName}`); cachedFrames.push({ url, index: i, name: fileName }); } else { // console.warn(`[PreviewCard] ✗ 下载失败 (${response.status}): ${fileName}`); } } catch (error) { // console.error(`[PreviewCard] ✗ 下载错误: ${fileName}`, error); } } } if (cachedFrames.length === 0) { throw new Error('没有成功缓存任何图片'); } // console.log('[PreviewCard] ✅ 缓存完成,共', cachedFrames.length, '帧'); // 6. 设置文件夹名称 this.setFolderName(folderName); // 7. 加载并播放动画 this.loadFrames(cachedFrames, 'cached'); this.showLoading(false); // 添加播放状态类 if (this.dropZone) { this.dropZone.classList.add('is-playing'); } } catch (error) { // console.error('[PreviewCard] 加载失败:', error); this.showLoading(false); this.showError(error.message || '加载失败'); this.setStagePlaceholderVisible(true); this.setFolderName(''); // 清除文件夹名称 // 移除播放状态 if (this.dropZone) { this.dropZone.classList.remove('is-playing'); } } } sanitizeFrameList(frameInfo) { if (frameInfo && Array.isArray(frameInfo.frames) && frameInfo.frames.length > 0) { return frameInfo.frames; } const maxFrame = frameInfo && frameInfo.maxFrame ? frameInfo.maxFrame : 0; if (!maxFrame) { return []; } return Array.from({ length: maxFrame }, (_, idx) => idx + 1); } showLoading(isLoading, text = '正在加载图片...') { if (!this.loadingOverlay) return; if (isLoading) { this.loadingOverlay.classList.add('is-visible'); this.loadingOverlay.setAttribute('aria-hidden', 'false'); if (this.loadingText) { this.loadingText.textContent = text; } } else { this.loadingOverlay.classList.remove('is-visible'); this.loadingOverlay.setAttribute('aria-hidden', 'true'); } } showError(message) { // 使用全局alert显示错误 this.showGlobalAlert(message); } hideError() { if (!this.imageError) return; this.imageError.hidden = true; } showGlobalAlert(text, duration = 1500) { // 直接调用父窗口的 GlobalAlert(不通过 postMessage) try { // 向上查找有 GlobalAlert 的窗口 let targetWindow = window.parent; while (targetWindow && targetWindow !== window) { if (targetWindow.GlobalAlert) { targetWindow.GlobalAlert.show(text, duration); return; } if (targetWindow.parent && targetWindow.parent !== targetWindow) { targetWindow = targetWindow.parent; } else { break; } } // 降级处理 console.log('[Alert]', text); } catch (error) { console.error('[Card] 显示 alert 失败:', error); } } setStagePlaceholderVisible(isVisible) { if (this.dropHint) { this.dropHint.hidden = !isVisible; } if (this.previewImage) { this.previewImage.classList.toggle('is-hidden', isVisible); if (isVisible) { this.previewImage.removeAttribute('src'); } } } setFolderName(name) { this.currentFolderName = name; if (this.folderNameElement) { this.folderNameElement.textContent = name || '--'; } if (this.infoBar) { this.infoBar.hidden = !name; } } async handleExport() { if (!this.currentFolderName || !this.frameList.length) { this.showGlobalAlert('没有可导出的动画'); return; } // 直接打开导出弹出框,不先生成预览图 // 预览图将在弹出框中生成 this.openExportView(); } /** * 处理AI生图按钮点击 */ async handleAIGenerate() { if (!this.currentFolderName || !this.frameList.length) { this.showGlobalAlert('没有可用于AI生图的动画'); return; } // 打开AI生图界面 this.openAIGenerateView(); } /** * 打开AI生图界面 */ openAIGenerateView() { // 先生成预览图数据,然后打开AI生图界面 this.generatePreviewImage().then(result => { // 向所有父级窗口发送消息(处理多层iframe情况) let targetWindow = window.parent; while (targetWindow && targetWindow !== window) { targetWindow.postMessage({ type: 'open-ai-generate-view', folderName: this.currentFolderName, spritesheetData: result.imageUrl, spritesheetLayout: result.layout }, '*'); // 尝试向更上层发送 if (targetWindow.parent && targetWindow.parent !== targetWindow) { targetWindow = targetWindow.parent; } else { break; } } }).catch(error => { console.error('[PreviewCard] 生成预览图失败:', error); this.showGlobalAlert('生成预览图失败:' + error.message); }); } /** * 生成预览图 * @returns {Promise} 预览图的 base64 URL */ async generatePreviewImage() { const folderName = this.currentFolderName; // 获取用户名 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 errorData = await response.json().catch(() => ({})); throw new Error(errorData.error || '无法获取帧列表'); } const data = await response.json(); const frameNumbers = data.frames || []; // 服务端已经判断过是否有图片,如果返回200但frames为空,说明有问题 if (frameNumbers.length === 0) { throw new Error('该文件夹中没有图片'); } // 加载所有图片(使用正确的API路径) const images = []; for (let i = 0; i < frameNumbers.length; i++) { const frameNum = frameNumbers[i]; const frameName = frameNum.toString().padStart(2, '0'); // 使用API路径,从用户目录加载 const imagePath = `${folderName}/${frameName}.png`; const 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'); // 填充透明背景 ctx.clearRect(0, 0, canvas.width, canvas.height); // 保存布局信息 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 }); }); // 转换为 base64 return new Promise((resolve) => { canvas.toBlob((blob) => { const reader = new FileReader(); reader.onload = () => { resolve({ imageUrl: reader.result, layout: { layout: layout, sheetWidth: canvas.width, sheetHeight: canvas.height } }); }; reader.readAsDataURL(blob); }, 'image/png'); }); } /** * 打开导出弹出框 */ openExportView() { // 通过postMessage通知父页面打开导出弹出框 // 传递文件夹名称,让弹出框自己生成预览图 let targetWindow = window.parent; while (targetWindow && targetWindow !== window) { targetWindow.postMessage({ type: 'open-export-view', folderName: this.currentFolderName, // 跳过导出视图内部的预览UI,直接展示下载选项 skipPreviewUI: true }, '*'); // 尝试向更上层发送 if (targetWindow.parent && targetWindow.parent !== targetWindow) { targetWindow = targetWindow.parent; } else { break; } } } setFps(fps) { this.currentFps = fps; if (this.frameList.length > 0) { this.startAnimation(); } } startAnimation() { this.stopAnimation(); if (!this.frameList.length) return; const interval = 1000 / this.currentFps; this.animationTimer = setInterval(() => { if (!this.frameList.length) { this.stopAnimation(); return; } this.currentFrameIndex = (this.currentFrameIndex + 1) % this.frameList.length; this.updateFrame(this.frameList[this.currentFrameIndex]); }, interval); } stopAnimation() { if (this.animationTimer) { clearInterval(this.animationTimer); this.animationTimer = null; } } updateFrame(frameData) { if (!this.previewImage) return; if ((this.frameSourceMode === 'local' || this.frameSourceMode === 'cached') && frameData && frameData.url) { this.setStagePlaceholderVisible(false); if (this.previewImage.src !== frameData.url) { this.previewImage.src = frameData.url; } } } loadFrames(frames, mode = 'local') { this.frameSourceMode = mode; this.frameList = frames; this.currentFrameIndex = 0; // console.log('[PreviewCard] 加载帧列表, 模式:', mode, '数量:', frames.length); if (frames.length > 0) { this.updateFrame(frames[0]); this.startAnimation(); } } destroy() { this.stopAnimation(); if (this.container) { this.container.innerHTML = ''; } } } // 小卡片类 class SequenceCard { constructor(cardElement, folderName, index, buildFrameSrc, onSelect) { this.cardElement = cardElement; this.folderName = folderName; this.index = index; this.buildFrameSrc = buildFrameSrc; this.onSelect = onSelect; this.imageElement = cardElement.querySelector(".card-image"); this.spinnerElement = cardElement.querySelector(".loading-spinner"); this.errorElement = cardElement.querySelector(".image-error"); this.labelElement = cardElement.querySelector(".card-label"); this.downloadButton = cardElement.querySelector(".card-download-btn"); this.handleCardClick = this.handleCardClick.bind(this); this.handleDownloadClick = this.handleDownloadClick.bind(this); this.init(); } init() { this.cardElement.dataset.folder = this.folderName || ""; this.cardElement.dataset.index = this.index; this.cardElement.dataset.valid = "true"; if (this.labelElement) { this.labelElement.textContent = this.formatLabel(this.folderName); } this.cardElement.addEventListener("click", this.handleCardClick); if (this.downloadButton) { this.boundDownloadHandler = (event) => { event.stopPropagation(); event.preventDefault(); this.handleDownloadClick(); }; this.downloadButton.addEventListener("click", this.boundDownloadHandler); } this.bindImageEvents(); } bindImageEvents() { if (!this.imageElement) { return; } this.imageElement.addEventListener("load", () => { this.toggleSpinner(false); this.hideError(); }); this.imageElement.addEventListener("error", () => { this.toggleSpinner(false); this.showError("图片加载失败"); }); } formatLabel(name) { if (!name) { return "--"; } return name.replace(/_/g, " ").toUpperCase(); } loadPreview() { if (!this.imageElement || !this.buildFrameSrc || !this.folderName) { return; } this.toggleSpinner(true); const previewSrc = this.buildFrameSrc(this.folderName, 1); if (this.imageElement.src !== previewSrc) { this.imageElement.src = previewSrc; } } setActive(isActive) { if (isActive) { this.cardElement.classList.add("is-active"); } else { this.cardElement.classList.remove("is-active"); } } handleCardClick() { if (typeof this.onSelect === "function") { this.onSelect(this.folderName, this.index); } } handleDownloadClick() { if (!this.folderName) { return; } if (window.SpriteSheetMaker && typeof window.SpriteSheetMaker.handleDownloadClick === "function") { window.SpriteSheetMaker.handleDownloadClick(this.folderName, this.index); } } toggleSpinner(visible) { if (!this.spinnerElement) { return; } this.spinnerElement.style.display = visible ? "block" : "none"; } showError(message) { if (!this.errorElement) { return; } this.errorElement.textContent = message; this.errorElement.hidden = false; } hideError() { if (!this.errorElement) { return; } this.errorElement.hidden = true; } destroy() { this.cardElement.removeEventListener("click", this.handleCardClick); if (this.downloadButton && this.boundDownloadHandler) { this.downloadButton.removeEventListener("click", this.boundDownloadHandler); } } } window.PreviewCard = PreviewCard; window.SequenceCard = SequenceCard; })();