card.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905
  1. (function () {
  2. // 主预览卡片类
  3. class PreviewCard {
  4. constructor(container, options = {}) {
  5. this.container = container;
  6. this.options = {
  7. fps: 8,
  8. onFpsChange: null,
  9. ...options
  10. };
  11. this.previewImage = null;
  12. this.loadingOverlay = null;
  13. this.dropHint = null;
  14. this.imageError = null;
  15. this.fpsSlider = null;
  16. this.fpsValue = null;
  17. this.dropZone = null;
  18. this.currentFps = this.options.fps;
  19. this.frameList = [];
  20. this.currentFrameIndex = 0;
  21. this.animationTimer = null;
  22. this.frameSourceMode = "remote";
  23. this.localFrameResources = [];
  24. this.currentFolderName = '';
  25. // 当前登录用户名
  26. this.currentUsername = null;
  27. this.init();
  28. this.initUserListener();
  29. }
  30. // 获取当前登录用户名
  31. getCurrentUsername() {
  32. // 如果已经缓存了用户名,直接返回
  33. if (this.currentUsername) {
  34. return this.currentUsername;
  35. }
  36. // 从导航栏 iframe 中获取用户名
  37. try {
  38. let targetWindow = window.parent;
  39. while (targetWindow && targetWindow !== window) {
  40. try {
  41. const navigationFrame = targetWindow.document.getElementById('navigationFrame');
  42. if (navigationFrame && navigationFrame.contentWindow) {
  43. const navWindow = navigationFrame.contentWindow;
  44. const navDoc = navigationFrame.contentDocument || navWindow.document;
  45. // 检查用户是否已登录
  46. const userAvatarContainer = navDoc.getElementById('userAvatarContainer');
  47. if (userAvatarContainer) {
  48. const computedStyle = navDoc.defaultView.getComputedStyle(userAvatarContainer);
  49. if (computedStyle.display !== 'none') {
  50. // 用户已登录,从 userAvatar 的 alt 属性获取用户名
  51. const userAvatar = navDoc.getElementById('userAvatar');
  52. if (userAvatar && userAvatar.alt && userAvatar.alt !== '用户头像') {
  53. const username = userAvatar.alt;
  54. this.currentUsername = username;
  55. return username;
  56. }
  57. }
  58. }
  59. }
  60. } catch (e) {
  61. // 跨域或访问限制,继续尝试上层窗口
  62. }
  63. if (targetWindow.parent && targetWindow.parent !== targetWindow) {
  64. targetWindow = targetWindow.parent;
  65. } else {
  66. break;
  67. }
  68. }
  69. } catch (error) {
  70. console.warn('[PreviewCard] 无法获取用户名:', error);
  71. }
  72. return this.currentUsername;
  73. }
  74. // 监听登录成功消息
  75. initUserListener() {
  76. window.addEventListener('message', (event) => {
  77. if (event.data && event.data.type === 'login-success' && event.data.user) {
  78. this.currentUsername = event.data.user.username;
  79. } else if (event.data && event.data.type === 'logout') {
  80. this.currentUsername = null;
  81. }
  82. });
  83. }
  84. async init() {
  85. await this.loadTemplate();
  86. this.bindElements();
  87. this.bindEvents();
  88. this.setStagePlaceholderVisible(true);
  89. // 从 localStorage 恢复登录状态
  90. this.restoreLoginFromStorage();
  91. }
  92. // 从 localStorage 恢复登录状态
  93. restoreLoginFromStorage() {
  94. try {
  95. const loginDataStr = localStorage.getItem('loginData');
  96. if (!loginDataStr) {
  97. return;
  98. }
  99. const loginData = JSON.parse(loginDataStr);
  100. const now = Date.now();
  101. // 检查是否过期
  102. if (now >= loginData.expireTime) {
  103. localStorage.removeItem('loginData');
  104. return;
  105. }
  106. // 未过期,恢复登录状态
  107. if (loginData.user && loginData.user.username) {
  108. this.currentUsername = loginData.user.username;
  109. console.log('[PreviewCard] 从 localStorage 恢复登录状态:', loginData.user.username);
  110. }
  111. } catch (error) {
  112. console.error('[PreviewCard] 恢复登录状态失败:', error);
  113. }
  114. }
  115. async loadTemplate() {
  116. const response = await fetch('./card.html');
  117. const html = await response.text();
  118. const wrapper = document.createElement('div');
  119. wrapper.innerHTML = html.trim();
  120. const template = wrapper.querySelector('#preview-card-template');
  121. if (!template) {
  122. throw new Error('Preview card template not found');
  123. }
  124. const content = template.content.cloneNode(true);
  125. this.container.appendChild(content);
  126. }
  127. bindElements() {
  128. this.previewImage = this.container.querySelector('.preview-image');
  129. this.loadingOverlay = this.container.querySelector('.loading-overlay');
  130. this.loadingText = this.container.querySelector('.loading-text');
  131. this.dropHint = this.container.querySelector('.drop-hint');
  132. this.imageError = this.container.querySelector('.image-error');
  133. this.fpsSlider = this.container.querySelector('.fps-slider');
  134. this.fpsValue = this.container.querySelector('.fps-value');
  135. this.dropZone = this.container.querySelector('.preview-card-stage');
  136. this.infoBar = this.container.querySelector('.preview-info-bar');
  137. this.folderNameElement = this.container.querySelector('.folder-name');
  138. this.btnExport = this.container.querySelector('.btn-export');
  139. this.btnDownload = this.container.querySelector('.btn-download');
  140. this.btnAI = this.container.querySelector('.btn-ai');
  141. }
  142. bindEvents() {
  143. // FPS控制
  144. if (this.fpsSlider) {
  145. this.fpsSlider.addEventListener('input', () => {
  146. const value = parseInt(this.fpsSlider.value, 10) || this.currentFps;
  147. this.setFps(value);
  148. if (this.fpsValue) {
  149. this.fpsValue.textContent = `${value} FPS`;
  150. }
  151. if (typeof this.options.onFpsChange === 'function') {
  152. this.options.onFpsChange(value);
  153. }
  154. });
  155. }
  156. // 拖放事件
  157. if (this.dropZone) {
  158. this.dropZone.addEventListener('dragenter', (e) => this.handleDragEnter(e));
  159. this.dropZone.addEventListener('dragover', (e) => this.handleDragOver(e));
  160. this.dropZone.addEventListener('dragleave', (e) => this.handleDragLeave(e));
  161. this.dropZone.addEventListener('drop', (e) => this.handleDrop(e));
  162. }
  163. // 导出按钮(旧版兼容)
  164. if (this.btnExport) {
  165. this.btnExport.addEventListener('click', () => this.handleExport());
  166. }
  167. // 下载按钮
  168. if (this.btnDownload) {
  169. this.btnDownload.addEventListener('click', () => this.handleExport());
  170. }
  171. // AI生图按钮
  172. if (this.btnAI) {
  173. this.btnAI.addEventListener('click', () => this.handleAIGenerate());
  174. }
  175. }
  176. handleDragEnter(event) {
  177. event.preventDefault();
  178. event.stopPropagation();
  179. this.dropZone.classList.add('is-dragging');
  180. }
  181. handleDragOver(event) {
  182. event.preventDefault();
  183. event.stopPropagation();
  184. }
  185. handleDragLeave(event) {
  186. event.preventDefault();
  187. if (!this.dropZone.contains(event.relatedTarget)) {
  188. this.dropZone.classList.remove('is-dragging');
  189. }
  190. }
  191. async handleDrop(event) {
  192. event.preventDefault();
  193. event.stopPropagation();
  194. this.dropZone.classList.remove('is-dragging');
  195. // 获取拖入的文件夹名称
  196. const transfer = event.dataTransfer;
  197. if (!transfer || !transfer.items || transfer.items.length === 0) {
  198. // console.warn('[PreviewCard] No data transfer items');
  199. return;
  200. }
  201. // 获取文本数据(可能是JSON对象或文件夹名称)
  202. const item = transfer.items[0];
  203. if (item.kind === 'string' && item.type === 'text/plain') {
  204. item.getAsString(async (dataString) => {
  205. // console.log('[PreviewCard] Dropped folder:', dataString);
  206. // 尝试解析为JSON对象
  207. let folderName = dataString;
  208. let fileType = 'directory'; // 默认假设是文件夹
  209. let pngCount = undefined; // PNG文件数量(如果有)
  210. try {
  211. const data = JSON.parse(dataString);
  212. // 如果是对象,提取路径、名称和类型
  213. if (data && typeof data === 'object') {
  214. folderName = data.path || data.name || dataString;
  215. fileType = data.type || 'directory';
  216. pngCount = data.pngCount; // 从拖拽数据中获取PNG数量
  217. }
  218. } catch (e) {
  219. // 如果不是JSON,直接使用原始字符串
  220. folderName = dataString;
  221. }
  222. // 验证:必须是文件夹
  223. if (fileType !== 'directory') {
  224. this.showError('❌ 请拖入文件夹,不支持单个文件');
  225. return;
  226. }
  227. // 验证:文件夹中是否包含PNG图片
  228. const isValid = await this.validateFolderHasPNG(folderName, pngCount);
  229. if (!isValid) {
  230. this.showError('❌ 该文件夹不包含PNG图片');
  231. return;
  232. }
  233. // console.log('[PreviewCard] Resolved folder name:', folderName);
  234. await this.loadAndCacheFolderAnimation(folderName);
  235. });
  236. }
  237. }
  238. async validateFolderHasPNG(folderName, pngCount) {
  239. try {
  240. // 1. 优先使用传递的pngCount(来自拖拽数据)
  241. if (pngCount !== undefined) {
  242. // console.log('[PreviewCard] 使用缓存的pngCount:', pngCount);
  243. return pngCount > 0;
  244. }
  245. // 2. 尝试从DiskManager的缓存中获取
  246. if (window.diskManager && window.diskManager.getFileFromCache) {
  247. const cachedFile = window.diskManager.getFileFromCache(folderName);
  248. if (cachedFile && cachedFile.pngCount !== undefined) {
  249. // console.log('[PreviewCard] 从DiskManager缓存获取pngCount:', cachedFile.pngCount);
  250. return cachedFile.pngCount > 0;
  251. }
  252. }
  253. // 3. 最后才请求服务器(使用新的disk API)
  254. // console.log('[PreviewCard] 请求服务器验证文件夹:', folderName);
  255. const username = this.getCurrentUsername();
  256. if (!username) {
  257. return false;
  258. }
  259. const response = await fetch(`/api/disk/list?username=${encodeURIComponent(username)}&path=${encodeURIComponent(folderName)}`);
  260. if (!response.ok) {
  261. return false;
  262. }
  263. const data = await response.json();
  264. // 检查是否是文件夹并且有PNG文件
  265. if (data.success && data.files) {
  266. // 检查当前路径对应的文件夹信息
  267. // 注意:list API返回的是文件夹内的文件列表,不是文件夹本身
  268. // 所以我们需要统计PNG文件数量
  269. const pngFiles = data.files.filter(f =>
  270. f.type === 'file' && f.name.toLowerCase().endsWith('.png')
  271. );
  272. return pngFiles.length > 0;
  273. }
  274. return false;
  275. } catch (error) {
  276. // console.error('[PreviewCard] 验证文件夹失败:', error);
  277. return false;
  278. }
  279. }
  280. async loadAndCacheFolderAnimation(folderName) {
  281. try {
  282. // 显示加载动画
  283. this.showLoading(true);
  284. this.hideError();
  285. this.setStagePlaceholderVisible(false);
  286. // console.log('[PreviewCard] 开始加载文件夹:', folderName);
  287. // 1. 从网盘系统获取该文件夹的文件列表
  288. const username = this.getCurrentUsername();
  289. if (!username) {
  290. throw new Error('请先登录');
  291. }
  292. const listResponse = await fetch(`/api/disk/list?username=${encodeURIComponent(username)}&path=${encodeURIComponent(folderName)}`);
  293. if (!listResponse.ok) {
  294. throw new Error('获取文件列表失败');
  295. }
  296. const listData = await listResponse.json();
  297. // console.log('[PreviewCard] 文件列表响应:', listData);
  298. if (!listData.success || !listData.files) {
  299. throw new Error('获取文件列表失败');
  300. }
  301. // 2. 筛选出PNG文件并按文件名排序
  302. const pngFiles = listData.files
  303. .filter(f => f.type === 'file' && f.name.toLowerCase().endsWith('.png'))
  304. .sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true }));
  305. // console.log('[PreviewCard] PNG文件列表:', pngFiles.map(f => f.name));
  306. if (!pngFiles.length) {
  307. throw new Error('文件夹中没有可用的PNG图片');
  308. }
  309. // console.log('[PreviewCard] 找到 PNG 文件数量:', pngFiles.length);
  310. // 3. 打开或创建缓存
  311. const cache = await caches.open('animation-frames-v1');
  312. // 4. 构造帧URL列表(使用文件的实际名称)
  313. const frameUrls = pngFiles.map(file => {
  314. // 使用文件的完整路径(file.path)来构造URL
  315. const username = this.getCurrentUsername();
  316. if (!username) {
  317. return null; // 未登录时返回 null
  318. }
  319. return `/api/disk/preview?username=${encodeURIComponent(username)}&path=${encodeURIComponent(file.path)}`;
  320. });
  321. // console.log('[PreviewCard] 开始下载和缓存图片...');
  322. // 5. 逐个下载并缓存
  323. const cachedFrames = [];
  324. for (let i = 0; i < frameUrls.length; i++) {
  325. const url = frameUrls[i];
  326. const fileName = pngFiles[i].name;
  327. // 更新进度
  328. this.showLoading(true, `正在缓存图片... (${i + 1}/${frameUrls.length})`);
  329. // 检查缓存中是否已存在
  330. const cachedResponse = await cache.match(url);
  331. if (cachedResponse) {
  332. // console.log(`[PreviewCard] [${i + 1}/${frameUrls.length}] 从缓存加载: ${fileName}`);
  333. cachedFrames.push({ url, index: i, name: fileName });
  334. } else {
  335. // 下载并缓存
  336. try {
  337. // console.log(`[PreviewCard] [${i + 1}/${frameUrls.length}] 下载: ${fileName}`);
  338. const response = await fetch(url);
  339. if (response.ok) {
  340. await cache.put(url, response.clone());
  341. // console.log(`[PreviewCard] ✓ 已缓存: ${fileName}`);
  342. cachedFrames.push({ url, index: i, name: fileName });
  343. } else {
  344. // console.warn(`[PreviewCard] ✗ 下载失败 (${response.status}): ${fileName}`);
  345. }
  346. } catch (error) {
  347. // console.error(`[PreviewCard] ✗ 下载错误: ${fileName}`, error);
  348. }
  349. }
  350. }
  351. if (cachedFrames.length === 0) {
  352. throw new Error('没有成功缓存任何图片');
  353. }
  354. // console.log('[PreviewCard] ✅ 缓存完成,共', cachedFrames.length, '帧');
  355. // 6. 设置文件夹名称
  356. this.setFolderName(folderName);
  357. // 7. 加载并播放动画
  358. this.loadFrames(cachedFrames, 'cached');
  359. this.showLoading(false);
  360. // 添加播放状态类
  361. if (this.dropZone) {
  362. this.dropZone.classList.add('is-playing');
  363. }
  364. } catch (error) {
  365. // console.error('[PreviewCard] 加载失败:', error);
  366. this.showLoading(false);
  367. this.showError(error.message || '加载失败');
  368. this.setStagePlaceholderVisible(true);
  369. this.setFolderName(''); // 清除文件夹名称
  370. // 移除播放状态
  371. if (this.dropZone) {
  372. this.dropZone.classList.remove('is-playing');
  373. }
  374. }
  375. }
  376. sanitizeFrameList(frameInfo) {
  377. if (frameInfo && Array.isArray(frameInfo.frames) && frameInfo.frames.length > 0) {
  378. return frameInfo.frames;
  379. }
  380. const maxFrame = frameInfo && frameInfo.maxFrame ? frameInfo.maxFrame : 0;
  381. if (!maxFrame) {
  382. return [];
  383. }
  384. return Array.from({ length: maxFrame }, (_, idx) => idx + 1);
  385. }
  386. showLoading(isLoading, text = '正在加载图片...') {
  387. if (!this.loadingOverlay) return;
  388. if (isLoading) {
  389. this.loadingOverlay.classList.add('is-visible');
  390. this.loadingOverlay.setAttribute('aria-hidden', 'false');
  391. if (this.loadingText) {
  392. this.loadingText.textContent = text;
  393. }
  394. } else {
  395. this.loadingOverlay.classList.remove('is-visible');
  396. this.loadingOverlay.setAttribute('aria-hidden', 'true');
  397. }
  398. }
  399. showError(message) {
  400. // 使用全局alert显示错误
  401. this.showGlobalAlert(message);
  402. }
  403. hideError() {
  404. if (!this.imageError) return;
  405. this.imageError.hidden = true;
  406. }
  407. showGlobalAlert(text, duration = 1500) {
  408. // 直接调用父窗口的 GlobalAlert(不通过 postMessage)
  409. try {
  410. // 向上查找有 GlobalAlert 的窗口
  411. let targetWindow = window.parent;
  412. while (targetWindow && targetWindow !== window) {
  413. if (targetWindow.GlobalAlert) {
  414. targetWindow.GlobalAlert.show(text, duration);
  415. return;
  416. }
  417. if (targetWindow.parent && targetWindow.parent !== targetWindow) {
  418. targetWindow = targetWindow.parent;
  419. } else {
  420. break;
  421. }
  422. }
  423. // 降级处理
  424. console.log('[Alert]', text);
  425. } catch (error) {
  426. console.error('[Card] 显示 alert 失败:', error);
  427. }
  428. }
  429. setStagePlaceholderVisible(isVisible) {
  430. if (this.dropHint) {
  431. this.dropHint.hidden = !isVisible;
  432. }
  433. if (this.previewImage) {
  434. this.previewImage.classList.toggle('is-hidden', isVisible);
  435. if (isVisible) {
  436. this.previewImage.removeAttribute('src');
  437. }
  438. }
  439. }
  440. setFolderName(name) {
  441. this.currentFolderName = name;
  442. if (this.folderNameElement) {
  443. this.folderNameElement.textContent = name || '--';
  444. }
  445. if (this.infoBar) {
  446. this.infoBar.hidden = !name;
  447. }
  448. }
  449. async handleExport() {
  450. if (!this.currentFolderName || !this.frameList.length) {
  451. this.showGlobalAlert('没有可导出的动画');
  452. return;
  453. }
  454. // 直接打开导出弹出框,不先生成预览图
  455. // 预览图将在弹出框中生成
  456. this.openExportView();
  457. }
  458. /**
  459. * 处理AI生图按钮点击
  460. */
  461. async handleAIGenerate() {
  462. if (!this.currentFolderName || !this.frameList.length) {
  463. this.showGlobalAlert('没有可用于AI生图的动画');
  464. return;
  465. }
  466. // 打开AI生图界面
  467. this.openAIGenerateView();
  468. }
  469. /**
  470. * 打开AI生图界面
  471. */
  472. openAIGenerateView() {
  473. // 先生成预览图数据,然后打开AI生图界面
  474. this.generatePreviewImage().then(result => {
  475. // 向所有父级窗口发送消息(处理多层iframe情况)
  476. let targetWindow = window.parent;
  477. while (targetWindow && targetWindow !== window) {
  478. targetWindow.postMessage({
  479. type: 'open-ai-generate-view',
  480. folderName: this.currentFolderName,
  481. spritesheetData: result.imageUrl,
  482. spritesheetLayout: result.layout
  483. }, '*');
  484. // 尝试向更上层发送
  485. if (targetWindow.parent && targetWindow.parent !== targetWindow) {
  486. targetWindow = targetWindow.parent;
  487. } else {
  488. break;
  489. }
  490. }
  491. }).catch(error => {
  492. console.error('[PreviewCard] 生成预览图失败:', error);
  493. this.showGlobalAlert('生成预览图失败:' + error.message);
  494. });
  495. }
  496. /**
  497. * 生成预览图
  498. * @returns {Promise<string>} 预览图的 base64 URL
  499. */
  500. async generatePreviewImage() {
  501. const folderName = this.currentFolderName;
  502. // 获取用户名
  503. const username = this.getCurrentUsername();
  504. if (!username) {
  505. throw new Error('请先登录');
  506. }
  507. // 获取帧列表(从服务端获取,服务端会判断是否有图片)
  508. const encodedFolderName = encodeURIComponent(folderName);
  509. let apiUrl = `http://localhost:3000/api/frames/${encodedFolderName}`;
  510. if (username) {
  511. apiUrl += `?username=${encodeURIComponent(username)}`;
  512. }
  513. const response = await fetch(apiUrl);
  514. if (!response.ok) {
  515. // 服务端返回错误,解析错误信息
  516. const errorData = await response.json().catch(() => ({}));
  517. throw new Error(errorData.error || '无法获取帧列表');
  518. }
  519. const data = await response.json();
  520. const frameNumbers = data.frames || [];
  521. // 服务端已经判断过是否有图片,如果返回200但frames为空,说明有问题
  522. if (frameNumbers.length === 0) {
  523. throw new Error('该文件夹中没有图片');
  524. }
  525. // 加载所有图片(使用正确的API路径)
  526. const images = [];
  527. for (let i = 0; i < frameNumbers.length; i++) {
  528. const frameNum = frameNumbers[i];
  529. const frameName = frameNum.toString().padStart(2, '0');
  530. // 使用API路径,从用户目录加载
  531. const imagePath = `${folderName}/${frameName}.png`;
  532. const imgSrc = `/api/disk/preview?username=${encodeURIComponent(username)}&path=${encodeURIComponent(imagePath)}`;
  533. const img = await new Promise((resolve, reject) => {
  534. const image = new Image();
  535. image.crossOrigin = 'anonymous';
  536. image.onload = () => resolve(image);
  537. image.onerror = () => reject(new Error(`Failed to load image: ${imgSrc}`));
  538. image.src = imgSrc;
  539. });
  540. images.push({
  541. img: img,
  542. width: img.width,
  543. height: img.height,
  544. frameNum: frameNum
  545. });
  546. }
  547. // 计算布局(简化版,使用简单的网格布局)
  548. const frameWidth = images[0].width;
  549. const frameHeight = images[0].height;
  550. const cols = Math.ceil(Math.sqrt(images.length));
  551. const rows = Math.ceil(images.length / cols);
  552. // 创建 Canvas 并绘制
  553. const canvas = document.createElement('canvas');
  554. canvas.width = frameWidth * cols;
  555. canvas.height = frameHeight * rows;
  556. const ctx = canvas.getContext('2d');
  557. // 填充透明背景
  558. ctx.clearRect(0, 0, canvas.width, canvas.height);
  559. // 保存布局信息
  560. const layout = [];
  561. // 绘制所有图片
  562. images.forEach((item, index) => {
  563. const col = index % cols;
  564. const row = Math.floor(index / cols);
  565. const x = col * frameWidth;
  566. const y = row * frameHeight;
  567. ctx.drawImage(item.img, x, y);
  568. // 保存布局信息
  569. layout.push({
  570. x: x,
  571. y: y,
  572. width: item.width,
  573. height: item.height,
  574. frameNum: item.frameNum
  575. });
  576. });
  577. // 转换为 base64
  578. return new Promise((resolve) => {
  579. canvas.toBlob((blob) => {
  580. const reader = new FileReader();
  581. reader.onload = () => {
  582. resolve({
  583. imageUrl: reader.result,
  584. layout: {
  585. layout: layout,
  586. sheetWidth: canvas.width,
  587. sheetHeight: canvas.height
  588. }
  589. });
  590. };
  591. reader.readAsDataURL(blob);
  592. }, 'image/png');
  593. });
  594. }
  595. /**
  596. * 打开导出弹出框
  597. */
  598. openExportView() {
  599. // 通过postMessage通知父页面打开导出弹出框
  600. // 传递文件夹名称,让弹出框自己生成预览图
  601. let targetWindow = window.parent;
  602. while (targetWindow && targetWindow !== window) {
  603. targetWindow.postMessage({
  604. type: 'open-export-view',
  605. folderName: this.currentFolderName
  606. }, '*');
  607. // 尝试向更上层发送
  608. if (targetWindow.parent && targetWindow.parent !== targetWindow) {
  609. targetWindow = targetWindow.parent;
  610. } else {
  611. break;
  612. }
  613. }
  614. }
  615. setFps(fps) {
  616. this.currentFps = fps;
  617. if (this.frameList.length > 0) {
  618. this.startAnimation();
  619. }
  620. }
  621. startAnimation() {
  622. this.stopAnimation();
  623. if (!this.frameList.length) return;
  624. const interval = 1000 / this.currentFps;
  625. this.animationTimer = setInterval(() => {
  626. if (!this.frameList.length) {
  627. this.stopAnimation();
  628. return;
  629. }
  630. this.currentFrameIndex = (this.currentFrameIndex + 1) % this.frameList.length;
  631. this.updateFrame(this.frameList[this.currentFrameIndex]);
  632. }, interval);
  633. }
  634. stopAnimation() {
  635. if (this.animationTimer) {
  636. clearInterval(this.animationTimer);
  637. this.animationTimer = null;
  638. }
  639. }
  640. updateFrame(frameData) {
  641. if (!this.previewImage) return;
  642. if ((this.frameSourceMode === 'local' || this.frameSourceMode === 'cached') && frameData && frameData.url) {
  643. this.setStagePlaceholderVisible(false);
  644. if (this.previewImage.src !== frameData.url) {
  645. this.previewImage.src = frameData.url;
  646. }
  647. }
  648. }
  649. loadFrames(frames, mode = 'local') {
  650. this.frameSourceMode = mode;
  651. this.frameList = frames;
  652. this.currentFrameIndex = 0;
  653. // console.log('[PreviewCard] 加载帧列表, 模式:', mode, '数量:', frames.length);
  654. if (frames.length > 0) {
  655. this.updateFrame(frames[0]);
  656. this.startAnimation();
  657. }
  658. }
  659. destroy() {
  660. this.stopAnimation();
  661. if (this.container) {
  662. this.container.innerHTML = '';
  663. }
  664. }
  665. }
  666. // 小卡片类
  667. class SequenceCard {
  668. constructor(cardElement, folderName, index, buildFrameSrc, onSelect) {
  669. this.cardElement = cardElement;
  670. this.folderName = folderName;
  671. this.index = index;
  672. this.buildFrameSrc = buildFrameSrc;
  673. this.onSelect = onSelect;
  674. this.imageElement = cardElement.querySelector(".card-image");
  675. this.spinnerElement = cardElement.querySelector(".loading-spinner");
  676. this.errorElement = cardElement.querySelector(".image-error");
  677. this.labelElement = cardElement.querySelector(".card-label");
  678. this.downloadButton = cardElement.querySelector(".card-download-btn");
  679. this.handleCardClick = this.handleCardClick.bind(this);
  680. this.handleDownloadClick = this.handleDownloadClick.bind(this);
  681. this.init();
  682. }
  683. init() {
  684. this.cardElement.dataset.folder = this.folderName || "";
  685. this.cardElement.dataset.index = this.index;
  686. this.cardElement.dataset.valid = "true";
  687. if (this.labelElement) {
  688. this.labelElement.textContent = this.formatLabel(this.folderName);
  689. }
  690. this.cardElement.addEventListener("click", this.handleCardClick);
  691. if (this.downloadButton) {
  692. this.boundDownloadHandler = (event) => {
  693. event.stopPropagation();
  694. event.preventDefault();
  695. this.handleDownloadClick();
  696. };
  697. this.downloadButton.addEventListener("click", this.boundDownloadHandler);
  698. }
  699. this.bindImageEvents();
  700. }
  701. bindImageEvents() {
  702. if (!this.imageElement) {
  703. return;
  704. }
  705. this.imageElement.addEventListener("load", () => {
  706. this.toggleSpinner(false);
  707. this.hideError();
  708. });
  709. this.imageElement.addEventListener("error", () => {
  710. this.toggleSpinner(false);
  711. this.showError("图片加载失败");
  712. });
  713. }
  714. formatLabel(name) {
  715. if (!name) {
  716. return "--";
  717. }
  718. return name.replace(/_/g, " ").toUpperCase();
  719. }
  720. loadPreview() {
  721. if (!this.imageElement || !this.buildFrameSrc || !this.folderName) {
  722. return;
  723. }
  724. this.toggleSpinner(true);
  725. const previewSrc = this.buildFrameSrc(this.folderName, 1);
  726. if (this.imageElement.src !== previewSrc) {
  727. this.imageElement.src = previewSrc;
  728. }
  729. }
  730. setActive(isActive) {
  731. if (isActive) {
  732. this.cardElement.classList.add("is-active");
  733. } else {
  734. this.cardElement.classList.remove("is-active");
  735. }
  736. }
  737. handleCardClick() {
  738. if (typeof this.onSelect === "function") {
  739. this.onSelect(this.folderName, this.index);
  740. }
  741. }
  742. handleDownloadClick() {
  743. if (!this.folderName) {
  744. return;
  745. }
  746. if (window.SpriteSheetMaker && typeof window.SpriteSheetMaker.handleDownloadClick === "function") {
  747. window.SpriteSheetMaker.handleDownloadClick(this.folderName, this.index);
  748. }
  749. }
  750. toggleSpinner(visible) {
  751. if (!this.spinnerElement) {
  752. return;
  753. }
  754. this.spinnerElement.style.display = visible ? "block" : "none";
  755. }
  756. showError(message) {
  757. if (!this.errorElement) {
  758. return;
  759. }
  760. this.errorElement.textContent = message;
  761. this.errorElement.hidden = false;
  762. }
  763. hideError() {
  764. if (!this.errorElement) {
  765. return;
  766. }
  767. this.errorElement.hidden = true;
  768. }
  769. destroy() {
  770. this.cardElement.removeEventListener("click", this.handleCardClick);
  771. if (this.downloadButton && this.boundDownloadHandler) {
  772. this.downloadButton.removeEventListener("click", this.boundDownloadHandler);
  773. }
  774. }
  775. }
  776. window.PreviewCard = PreviewCard;
  777. window.SequenceCard = SequenceCard;
  778. })();