export-view.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861
  1. /**
  2. * 导出动画弹出框
  3. */
  4. class ExportView {
  5. constructor() {
  6. this.overlay = null;
  7. this.modal = null;
  8. this.previewImage = null;
  9. this.previewPlaceholder = null;
  10. this.referenceBox = null;
  11. this.referenceInput = null;
  12. this.referenceUploadArea = null;
  13. this.referenceImage = null;
  14. this.referenceImageWrapper = null;
  15. this.referenceRemoveBtn = null;
  16. this.replaceBtn = null;
  17. this.additionalPromptInput = null;
  18. this.cancelBtn = null;
  19. this.confirmBtn = null;
  20. this.imageData = null;
  21. this.referenceImageData = null;
  22. this.spritesheetCanvas = null;
  23. this.folderName = null;
  24. this.spritesheetLayout = null; // 保存布局信息用于生成 JSON
  25. this.replacedImageData = null; // 保存 Gemini 返回的替换后的图片(base64)
  26. this.originalSpritesheetData = null; // 保存原始 spritesheet 的 base64 数据
  27. this.init();
  28. }
  29. init() {
  30. this.overlay = document.getElementById('exportOverlay');
  31. this.modal = document.getElementById('exportModal');
  32. this.previewImage = document.getElementById('previewImage');
  33. this.previewPlaceholder = document.getElementById('previewPlaceholder');
  34. this.referenceBox = document.getElementById('referenceBox');
  35. this.referenceInput = document.getElementById('referenceInput');
  36. this.referenceUploadArea = document.getElementById('referenceUploadArea');
  37. this.referenceImage = document.getElementById('referenceImage');
  38. this.referenceImageWrapper = document.getElementById('referenceImageWrapper');
  39. this.referenceRemoveBtn = document.getElementById('referenceRemoveBtn');
  40. this.replaceBtn = document.getElementById('replaceBtn');
  41. this.additionalPromptInput = document.getElementById('additionalPromptInput');
  42. this.cancelBtn = document.getElementById('exportCancelBtn');
  43. this.confirmBtn = document.getElementById('exportConfirmBtn');
  44. this.bindEvents();
  45. // 初始时禁用确定按钮
  46. if (this.confirmBtn) {
  47. this.confirmBtn.disabled = true;
  48. }
  49. this.reset();
  50. }
  51. bindEvents() {
  52. // 取消按钮(右上角)
  53. this.cancelBtn?.addEventListener('click', () => {
  54. this.close();
  55. });
  56. // 取消按钮(底部操作栏)
  57. this.cancelBtnBottom?.addEventListener('click', () => {
  58. this.close();
  59. });
  60. // 确定按钮
  61. this.confirmBtn?.addEventListener('click', () => {
  62. this.handleConfirm();
  63. });
  64. // 替换按钮
  65. this.replaceBtn?.addEventListener('click', () => {
  66. this.replaceCharacter();
  67. });
  68. // 删除参考图按钮
  69. this.referenceRemoveBtn?.addEventListener('click', (e) => {
  70. e.stopPropagation(); // 阻止触发父元素的点击事件
  71. this.removeReferenceImage();
  72. });
  73. // 点击遮罩层关闭
  74. this.overlay?.addEventListener('click', (e) => {
  75. if (e.target === this.overlay) {
  76. this.close();
  77. }
  78. });
  79. // ESC键关闭
  80. document.addEventListener('keydown', (e) => {
  81. if (e.key === 'Escape' && this.overlay) {
  82. this.close();
  83. }
  84. });
  85. // 参考图上传区域点击
  86. this.referenceBox?.addEventListener('click', () => {
  87. this.referenceInput?.click();
  88. });
  89. // 参考图选择
  90. this.referenceInput?.addEventListener('change', (e) => {
  91. const file = e.target.files[0];
  92. if (file) {
  93. this.loadReferenceImage(file);
  94. }
  95. });
  96. // 拖拽上传参考图
  97. this.referenceBox?.addEventListener('dragover', (e) => {
  98. e.preventDefault();
  99. e.stopPropagation();
  100. if (this.referenceBox) {
  101. this.referenceBox.style.borderColor = '#667eea';
  102. }
  103. });
  104. this.referenceBox?.addEventListener('dragleave', (e) => {
  105. e.preventDefault();
  106. e.stopPropagation();
  107. if (this.referenceBox) {
  108. this.referenceBox.style.borderColor = '#e5e7eb';
  109. }
  110. });
  111. this.referenceBox?.addEventListener('drop', (e) => {
  112. e.preventDefault();
  113. e.stopPropagation();
  114. if (this.referenceBox) {
  115. this.referenceBox.style.borderColor = '#e5e7eb';
  116. }
  117. const file = e.dataTransfer.files[0];
  118. if (file && file.type.startsWith('image/')) {
  119. this.loadReferenceImage(file);
  120. }
  121. });
  122. // 监听来自父窗口的消息
  123. window.addEventListener('message', (event) => {
  124. if (event.data && event.data.type === 'show-export-preview') {
  125. // console.log('[ExportView] 收到显示预览消息:', event.data);
  126. this.showPreview(event.data.imageUrl || event.data.imageData);
  127. } else if (event.data && event.data.type === 'generate-export-preview') {
  128. // console.log('[ExportView] 收到生成预览消息:', event.data);
  129. this.reset();
  130. this.folderName = event.data.folderName;
  131. this.generatePreview(event.data.folderName);
  132. }
  133. });
  134. }
  135. /**
  136. * 生成预览图
  137. * @param {string} folderName - 文件夹名称
  138. */
  139. async generatePreview(folderName) {
  140. if (!folderName) {
  141. // console.warn('[ExportView] 没有提供文件夹名称');
  142. if (this.previewPlaceholder) {
  143. this.previewPlaceholder.textContent = '没有提供文件夹名称';
  144. }
  145. return;
  146. }
  147. // 重置状态(确保每次打开都是全新状态)
  148. this.reset();
  149. // 保存文件夹名称
  150. this.folderName = folderName;
  151. // 显示加载状态
  152. if (this.previewPlaceholder) {
  153. this.previewPlaceholder.classList.remove('hide');
  154. }
  155. if (this.previewImage) {
  156. this.previewImage.classList.remove('show');
  157. }
  158. try {
  159. const TEXTURE_ROOT = "http://localhost:3000/disk_data";
  160. // 获取帧列表
  161. const encodedFolderName = encodeURIComponent(folderName);
  162. const response = await fetch(`http://localhost:3000/api/frames/${encodedFolderName}`);
  163. if (!response.ok) {
  164. // 服务端返回错误,解析错误信息
  165. const errorData = await response.json().catch(() => ({}));
  166. throw new Error(errorData.error || '无法获取帧列表');
  167. }
  168. const data = await response.json();
  169. const frameNumbers = data.frames || [];
  170. const fileNames = data.fileNames || [];
  171. if (frameNumbers.length === 0) {
  172. throw new Error('该文件夹中没有图片');
  173. }
  174. // 加载所有图片
  175. const images = [];
  176. for (let i = 0; i < frameNumbers.length; i++) {
  177. const frameNum = frameNumbers[i];
  178. const pathSegments = folderName.split('/').map(seg => encodeURIComponent(seg));
  179. const encodedPath = pathSegments.join('/');
  180. // 如果有文件名列表,使用实际文件名;否则使用帧号构造文件名
  181. let imgSrc;
  182. if (fileNames[i]) {
  183. // 使用实际文件名
  184. imgSrc = `${TEXTURE_ROOT}/${encodedPath}/${encodeURIComponent(fileNames[i])}`;
  185. } else {
  186. // 回退到使用帧号构造文件名
  187. const frameName = frameNum.toString().padStart(2, '0');
  188. imgSrc = `${TEXTURE_ROOT}/${encodedPath}/${frameName}.png`;
  189. }
  190. const img = await new Promise((resolve, reject) => {
  191. const image = new Image();
  192. image.crossOrigin = 'anonymous';
  193. image.onload = () => resolve(image);
  194. image.onerror = () => reject(new Error(`Failed to load image: ${imgSrc}`));
  195. image.src = imgSrc;
  196. });
  197. images.push({
  198. img: img,
  199. width: img.width,
  200. height: img.height,
  201. frameNum: frameNum
  202. });
  203. }
  204. // 计算布局(简化版,使用简单的网格布局)
  205. const frameWidth = images[0].width;
  206. const frameHeight = images[0].height;
  207. const cols = Math.ceil(Math.sqrt(images.length));
  208. const rows = Math.ceil(images.length / cols);
  209. // 创建 Canvas 并绘制
  210. const canvas = document.createElement('canvas');
  211. canvas.width = frameWidth * cols;
  212. canvas.height = frameHeight * rows;
  213. const ctx = canvas.getContext('2d');
  214. // 保存 canvas 和布局信息用于下载
  215. this.spritesheetCanvas = canvas;
  216. // 填充透明背景
  217. ctx.clearRect(0, 0, canvas.width, canvas.height);
  218. // 保存布局信息(用于生成 JSON)
  219. const layout = [];
  220. // 绘制所有图片
  221. images.forEach((item, index) => {
  222. const col = index % cols;
  223. const row = Math.floor(index / cols);
  224. const x = col * frameWidth;
  225. const y = row * frameHeight;
  226. ctx.drawImage(item.img, x, y);
  227. // 保存布局信息
  228. layout.push({
  229. x: x,
  230. y: y,
  231. width: item.width,
  232. height: item.height,
  233. frameNum: item.frameNum
  234. });
  235. });
  236. // 保存布局信息
  237. this.spritesheetLayout = {
  238. layout: layout,
  239. sheetWidth: canvas.width,
  240. sheetHeight: canvas.height
  241. };
  242. // 转换为 base64
  243. const imageUrl = await new Promise((resolve) => {
  244. canvas.toBlob((blob) => {
  245. const url = URL.createObjectURL(blob);
  246. resolve(url);
  247. }, 'image/png');
  248. });
  249. // 保存原始 spritesheet 的 base64 数据
  250. this.originalSpritesheetData = await new Promise((resolve) => {
  251. canvas.toBlob((blob) => {
  252. const reader = new FileReader();
  253. reader.onload = () => resolve(reader.result);
  254. reader.readAsDataURL(blob);
  255. }, 'image/png');
  256. });
  257. // 显示预览图
  258. this.showPreview(imageUrl);
  259. // 如果已经有参考图,显示替换按钮
  260. if (this.referenceImageData && this.replaceBtn) {
  261. this.replaceBtn.style.display = 'block';
  262. }
  263. // 移除自动调用替换 API 的逻辑
  264. } catch (error) {
  265. // console.error('[ExportView] 生成预览图失败:', error);
  266. if (this.previewPlaceholder) {
  267. // 隐藏加载动画,显示错误信息
  268. const spinner = this.previewPlaceholder.querySelector('.loading-spinner');
  269. const loadingText = this.previewPlaceholder.querySelector('.loading-text');
  270. if (spinner) spinner.style.display = 'none';
  271. if (loadingText) {
  272. loadingText.textContent = '生成预览图失败: ' + error.message;
  273. loadingText.style.color = '#ef4444';
  274. }
  275. this.previewPlaceholder.classList.remove('hide');
  276. }
  277. }
  278. }
  279. /**
  280. * 计算宽高比
  281. * @param {number} width - 宽度
  282. * @param {number} height - 高度
  283. * @returns {string} 宽高比字符串(例如:16:9)
  284. */
  285. calculateAspectRatio(width, height) {
  286. // 计算最大公约数
  287. const gcd = (a, b) => b === 0 ? a : gcd(b, a % b);
  288. const divisor = gcd(width, height);
  289. const ratioWidth = width / divisor;
  290. const ratioHeight = height / divisor;
  291. // 如果比例太大,使用简化版本
  292. if (ratioWidth > 100 || ratioHeight > 100) {
  293. // 使用小数形式
  294. const ratio = width / height;
  295. return ratio.toFixed(2) + ':1';
  296. }
  297. return `${ratioWidth}:${ratioHeight}`;
  298. }
  299. /**
  300. * 加载参考图
  301. * @param {File} file - 图片文件
  302. */
  303. loadReferenceImage(file) {
  304. const reader = new FileReader();
  305. reader.onload = (e) => {
  306. this.referenceImageData = e.target.result;
  307. if (this.referenceImage) {
  308. this.referenceImage.src = e.target.result;
  309. }
  310. if (this.referenceImageWrapper) {
  311. this.referenceImageWrapper.style.display = 'flex';
  312. }
  313. if (this.referenceUploadArea) {
  314. this.referenceUploadArea.classList.add('hide');
  315. }
  316. // 显示替换按钮和提示词配置区域(如果已经有 spritesheet)
  317. if (this.originalSpritesheetData) {
  318. if (this.replaceBtn) {
  319. this.replaceBtn.style.display = 'block';
  320. }
  321. // 显示提示词配置区域
  322. const promptConfigSection = document.getElementById('promptConfigSection');
  323. if (promptConfigSection) {
  324. promptConfigSection.style.display = 'flex';
  325. }
  326. }
  327. // 移除自动调用替换 API 的逻辑
  328. };
  329. reader.readAsDataURL(file);
  330. }
  331. /**
  332. * 删除参考图
  333. */
  334. removeReferenceImage() {
  335. // 清空参考图数据
  336. this.referenceImageData = null;
  337. this.replacedImageData = null; // 同时清空替换后的图片
  338. // 隐藏参考图,显示上传区域
  339. if (this.referenceImageWrapper) {
  340. this.referenceImageWrapper.style.display = 'none';
  341. }
  342. if (this.referenceImage) {
  343. this.referenceImage.src = '';
  344. }
  345. if (this.referenceUploadArea) {
  346. this.referenceUploadArea.classList.remove('hide');
  347. }
  348. if (this.referenceInput) {
  349. this.referenceInput.value = '';
  350. }
  351. // 隐藏替换按钮和提示词配置区域
  352. if (this.replaceBtn) {
  353. this.replaceBtn.style.display = 'none';
  354. this.replaceBtn.disabled = false;
  355. }
  356. const promptConfigSection = document.getElementById('promptConfigSection');
  357. if (promptConfigSection) {
  358. promptConfigSection.style.display = 'none';
  359. }
  360. // 如果预览图是替换后的图片,恢复显示原始 spritesheet
  361. if (this.replacedImageData && this.originalSpritesheetData) {
  362. // 清空替换后的图片,恢复显示原始预览
  363. this.replacedImageData = null;
  364. // 重新显示原始 spritesheet
  365. if (this.spritesheetCanvas) {
  366. this.spritesheetCanvas.toBlob((blob) => {
  367. const url = URL.createObjectURL(blob);
  368. this.showPreview(url);
  369. }, 'image/png');
  370. }
  371. }
  372. }
  373. /**
  374. * 调用角色替换 API
  375. */
  376. async replaceCharacter() {
  377. if (!this.originalSpritesheetData || !this.referenceImageData) {
  378. alert('请先上传参考图和生成 Spritesheet');
  379. return;
  380. }
  381. // 禁用替换按钮和下载按钮
  382. if (this.replaceBtn) {
  383. this.replaceBtn.disabled = true;
  384. }
  385. if (this.confirmBtn) {
  386. this.confirmBtn.disabled = true;
  387. }
  388. try {
  389. // 显示加载状态
  390. if (this.previewPlaceholder) {
  391. const spinner = this.previewPlaceholder.querySelector('.loading-spinner');
  392. const loadingText = this.previewPlaceholder.querySelector('.loading-text');
  393. if (spinner) spinner.style.display = 'block';
  394. if (loadingText) {
  395. loadingText.textContent = '正在生成替换后的图片...';
  396. loadingText.style.color = '#6b7280';
  397. }
  398. this.previewPlaceholder.classList.remove('hide');
  399. }
  400. if (this.previewImage) {
  401. this.previewImage.classList.remove('show');
  402. }
  403. // 准备图片数据(移除 data:image/png;base64, 前缀)
  404. const image1Base64 = this.originalSpritesheetData.replace(/^data:image\/\w+;base64,/, '');
  405. const image2Base64 = this.referenceImageData.replace(/^data:image\/\w+;base64,/, '');
  406. // 获取 image1 的尺寸
  407. const image1Width = this.spritesheetLayout?.sheetWidth || 0;
  408. const image1Height = this.spritesheetLayout?.sheetHeight || 0;
  409. // 获取额外提示词
  410. const additionalPrompt = this.additionalPromptInput?.value || '';
  411. // 调用 API
  412. const response = await fetch('http://localhost:3000/api/replace-character', {
  413. method: 'POST',
  414. headers: {
  415. 'Content-Type': 'application/json'
  416. },
  417. body: JSON.stringify({
  418. image1: image1Base64,
  419. image2: image2Base64,
  420. image1Width: image1Width,
  421. image1Height: image1Height,
  422. additionalPrompt: additionalPrompt
  423. })
  424. });
  425. if (!response.ok) {
  426. const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
  427. throw new Error(errorData.error || `服务器错误: ${response.status}`);
  428. }
  429. const result = await response.json();
  430. if (result.success && result.imageData) {
  431. // Gemini 返回的图片 base64
  432. const geminiImageBase64 = result.imageData;
  433. // 调用抠图 API 处理图片
  434. if (this.previewPlaceholder) {
  435. const loadingText = this.previewPlaceholder.querySelector('.loading-text');
  436. if (loadingText) {
  437. loadingText.textContent = '正在抠图处理...';
  438. }
  439. }
  440. const mattingResponse = await fetch('http://localhost:3000/api/remove-background-base64', {
  441. method: 'POST',
  442. headers: {
  443. 'Content-Type': 'application/json'
  444. },
  445. body: JSON.stringify({
  446. imageBase64: geminiImageBase64
  447. })
  448. });
  449. if (!mattingResponse.ok) {
  450. const errorData = await mattingResponse.json().catch(() => ({ error: 'Unknown error' }));
  451. throw new Error(errorData.error || `抠图失败: ${mattingResponse.status}`);
  452. }
  453. const mattingResult = await mattingResponse.json();
  454. if (mattingResult.success && mattingResult.imageData) {
  455. // 保存抠图后的图片
  456. this.replacedImageData = `data:image/png;base64,${mattingResult.imageData}`;
  457. // 显示抠图后的图片
  458. this.showPreview(this.replacedImageData);
  459. } else {
  460. throw new Error('抠图处理失败');
  461. }
  462. } else {
  463. throw new Error('API 返回失败或未找到图片数据');
  464. }
  465. } catch (error) {
  466. // console.error('[ExportView] 替换角色失败:', error);
  467. if (this.previewPlaceholder) {
  468. const spinner = this.previewPlaceholder.querySelector('.loading-spinner');
  469. const loadingText = this.previewPlaceholder.querySelector('.loading-text');
  470. if (spinner) spinner.style.display = 'none';
  471. if (loadingText) {
  472. loadingText.textContent = '替换失败: ' + error.message;
  473. loadingText.style.color = '#ef4444';
  474. }
  475. this.previewPlaceholder.classList.remove('hide');
  476. }
  477. } finally {
  478. // 重新启用替换按钮和下载按钮
  479. if (this.replaceBtn) {
  480. this.replaceBtn.disabled = false;
  481. }
  482. if (this.confirmBtn) {
  483. this.confirmBtn.disabled = false;
  484. }
  485. }
  486. }
  487. /**
  488. * 显示预览图(Spritesheet)
  489. * @param {string} imageUrl - 图片URL或base64数据
  490. */
  491. showPreview(imageUrl) {
  492. if (!imageUrl) {
  493. // console.warn('[ExportView] 没有提供图片数据');
  494. return;
  495. }
  496. // console.log('[ExportView] 显示预览图');
  497. // 保存图片数据
  498. this.imageData = imageUrl;
  499. // 图片加载前禁用下载按钮
  500. if (this.confirmBtn) {
  501. this.confirmBtn.disabled = true;
  502. }
  503. // 加载图片
  504. const img = new Image();
  505. img.onload = () => {
  506. if (this.previewImage) {
  507. this.previewImage.src = imageUrl;
  508. this.previewImage.classList.add('show');
  509. }
  510. if (this.previewPlaceholder) {
  511. this.previewPlaceholder.classList.add('hide');
  512. }
  513. // 图片加载完成后启用下载按钮
  514. if (this.confirmBtn) {
  515. this.confirmBtn.disabled = false;
  516. }
  517. // console.log('[ExportView] ✓ 预览图已加载');
  518. };
  519. img.onerror = () => {
  520. // 图片加载失败时禁用下载按钮
  521. if (this.confirmBtn) {
  522. this.confirmBtn.disabled = true;
  523. }
  524. // console.error('[ExportView] 图片加载失败');
  525. if (this.previewPlaceholder) {
  526. // 隐藏加载动画,显示错误信息
  527. const spinner = this.previewPlaceholder.querySelector('.loading-spinner');
  528. const loadingText = this.previewPlaceholder.querySelector('.loading-text');
  529. if (spinner) spinner.style.display = 'none';
  530. if (loadingText) {
  531. loadingText.textContent = '图片加载失败';
  532. loadingText.style.color = '#ef4444';
  533. }
  534. this.previewPlaceholder.classList.remove('hide');
  535. }
  536. };
  537. img.src = imageUrl;
  538. }
  539. /**
  540. * 生成 JSON 数据
  541. * @param {string} folderName - 文件夹名称
  542. * @param {Array} layout - 布局信息数组
  543. * @param {number} sheetWidth - Spritesheet 宽度
  544. * @param {number} sheetHeight - Spritesheet 高度
  545. * @returns {string} JSON 字符串
  546. */
  547. generateJSON(folderName, layout, sheetWidth, sheetHeight) {
  548. const frames = {};
  549. layout.forEach((item, index) => {
  550. // 使用实际的帧号,确保与原始文件名一致
  551. const frameNum = item.frameNum ? item.frameNum.toString().padStart(2, '0') : (index + 1).toString().padStart(2, '0');
  552. const frameName = `${frameNum}.png`;
  553. const x = item.x;
  554. const y = item.y;
  555. const width = item.width;
  556. const height = item.height;
  557. // 标准的 TexturePacker JSON 格式,Cocos Creator 3.8 完全兼容
  558. frames[frameName] = {
  559. frame: {
  560. x: x,
  561. y: y,
  562. w: width,
  563. h: height
  564. },
  565. rotated: false,
  566. trimmed: false,
  567. spriteSourceSize: { x: 0, y: 0, w: width, h: height },
  568. sourceSize: { w: width, h: height }
  569. };
  570. });
  571. // Cocos Creator 3.8 兼容的 TexturePacker JSON 格式
  572. const json = {
  573. frames: frames,
  574. meta: {
  575. app: "https://www.codeandweb.com/texturepacker",
  576. version: "1.0",
  577. image: `${folderName}.png`,
  578. format: "RGBA8888",
  579. size: { w: sheetWidth, h: sheetHeight },
  580. scale: 1
  581. }
  582. };
  583. return JSON.stringify(json, null, 2);
  584. }
  585. /**
  586. * 将 Blob 转换为 Base64
  587. * @param {Blob} blob - Blob 对象
  588. * @returns {Promise<string>} Base64 字符串
  589. */
  590. blobToBase64(blob) {
  591. return new Promise((resolve, reject) => {
  592. const reader = new FileReader();
  593. reader.onloadend = () => {
  594. // 移除 data:image/png;base64, 前缀
  595. const base64 = reader.result.split(',')[1];
  596. resolve(base64);
  597. };
  598. reader.onerror = reject;
  599. reader.readAsDataURL(blob);
  600. });
  601. }
  602. /**
  603. * 下载文件
  604. * @param {Blob} data - 文件数据
  605. * @param {string} filename - 文件名
  606. * @param {string} mimeType - MIME 类型
  607. */
  608. downloadFile(data, filename, mimeType) {
  609. const blob = new Blob([data], { type: mimeType });
  610. const url = URL.createObjectURL(blob);
  611. const a = document.createElement('a');
  612. a.href = url;
  613. a.download = filename;
  614. document.body.appendChild(a);
  615. a.click();
  616. document.body.removeChild(a);
  617. URL.revokeObjectURL(url);
  618. }
  619. /**
  620. * 处理下载按钮点击
  621. */
  622. async handleConfirm() {
  623. // console.log('[ExportView] 用户点击下载按钮');
  624. if (!this.spritesheetCanvas || !this.spritesheetLayout) {
  625. // console.warn('[ExportView] 没有可下载的 Spritesheet');
  626. return;
  627. }
  628. try {
  629. // 生成 JSON 数据
  630. const folderName = this.folderName.split('/').pop() || 'spritesheet';
  631. const jsonData = this.generateJSON(
  632. folderName,
  633. this.spritesheetLayout.layout,
  634. this.spritesheetLayout.sheetWidth,
  635. this.spritesheetLayout.sheetHeight
  636. );
  637. // 确定使用哪个图片:如果有替换后的图片,使用替换后的;否则使用原始的
  638. let imageBase64;
  639. if (this.replacedImageData) {
  640. // 使用替换后的图片(移除 data:image/png;base64, 前缀)
  641. imageBase64 = this.replacedImageData.replace(/^data:image\/\w+;base64,/, '');
  642. } else {
  643. // 使用原始 spritesheet
  644. const imageBlob = await new Promise((resolve, reject) => {
  645. this.spritesheetCanvas.toBlob((blob) => {
  646. if (blob) {
  647. resolve(blob);
  648. } else {
  649. reject(new Error('Canvas 转换失败'));
  650. }
  651. }, 'image/png');
  652. });
  653. // 将图片转换为 Base64
  654. imageBase64 = await this.blobToBase64(imageBlob);
  655. }
  656. // 发送到服务器打包
  657. const response = await fetch('http://localhost:3000/api/pack', {
  658. method: 'POST',
  659. headers: {
  660. 'Content-Type': 'application/json'
  661. },
  662. body: JSON.stringify({
  663. folderName: folderName,
  664. imageData: imageBase64,
  665. jsonData: jsonData
  666. })
  667. });
  668. if (!response.ok) {
  669. const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
  670. throw new Error(errorData.error || `服务器错误: ${response.status}`);
  671. }
  672. // 获取 ZIP 文件的 Blob
  673. const zipBlob = await response.blob();
  674. // 下载 ZIP 文件
  675. this.downloadFile(zipBlob, `${folderName}.zip`, 'application/zip');
  676. // 关闭弹出框
  677. this.close();
  678. } catch (error) {
  679. // console.error('[ExportView] 下载失败:', error);
  680. alert(`下载失败: ${error.message}`);
  681. }
  682. }
  683. /**
  684. * 关闭弹出框
  685. */
  686. close() {
  687. // console.log('[ExportView] 关闭导出弹出框');
  688. // 清空所有数据
  689. this.reset();
  690. // 通知父窗口关闭弹出框
  691. if (window.parent && window.parent !== window) {
  692. window.parent.postMessage({
  693. type: 'close-export-view'
  694. }, '*');
  695. }
  696. }
  697. /**
  698. * 重置所有数据和UI状态
  699. */
  700. reset() {
  701. // 清空数据属性
  702. this.imageData = null;
  703. this.referenceImageData = null;
  704. this.spritesheetCanvas = null;
  705. this.folderName = null;
  706. this.spritesheetLayout = null;
  707. this.replacedImageData = null;
  708. this.originalSpritesheetData = null;
  709. // 重置参考图区域
  710. if (this.referenceImageWrapper) {
  711. this.referenceImageWrapper.style.display = 'none';
  712. }
  713. if (this.referenceImage) {
  714. this.referenceImage.src = '';
  715. }
  716. if (this.referenceUploadArea) {
  717. this.referenceUploadArea.classList.remove('hide');
  718. }
  719. if (this.referenceInput) {
  720. this.referenceInput.value = '';
  721. }
  722. if (this.replaceBtn) {
  723. this.replaceBtn.style.display = 'none';
  724. this.replaceBtn.disabled = false;
  725. }
  726. // 重置预览图区域
  727. if (this.previewImage) {
  728. this.previewImage.src = '';
  729. this.previewImage.classList.remove('show');
  730. }
  731. if (this.previewPlaceholder) {
  732. const spinner = this.previewPlaceholder.querySelector('.loading-spinner');
  733. const loadingText = this.previewPlaceholder.querySelector('.loading-text');
  734. if (spinner) spinner.style.display = 'block';
  735. if (loadingText) {
  736. loadingText.textContent = '正在生成预览图...';
  737. loadingText.style.color = '#6b7280';
  738. }
  739. this.previewPlaceholder.classList.remove('hide');
  740. }
  741. // 重置提示词配置区域
  742. const promptConfigSection = document.getElementById('promptConfigSection');
  743. if (promptConfigSection) {
  744. promptConfigSection.style.display = 'none';
  745. }
  746. if (this.additionalPromptInput) {
  747. this.additionalPromptInput.value = '';
  748. }
  749. // 重置按钮状态
  750. if (this.confirmBtn) {
  751. this.confirmBtn.disabled = true;
  752. }
  753. }
  754. }
  755. // 初始化
  756. window.ExportView = new ExportView();