ai-generate-view.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794
  1. /**
  2. * AI生图弹出框
  3. */
  4. class AIGenerateView {
  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.generateBtn = null;
  17. this.isGenerateEnabled = false;
  18. this.additionalPromptInput = null;
  19. this.cancelBtn = null;
  20. this.imageData = null;
  21. this.referenceImageData = null;
  22. this.originalSpritesheetData = null;
  23. this.spritesheetLayout = null;
  24. this.folderName = null;
  25. // 当前会话的任务ID列表
  26. this.sessionTaskIds = [];
  27. this.queuePollingTimer = null;
  28. this.init();
  29. }
  30. init() {
  31. this.overlay = document.getElementById('aiGenerateOverlay');
  32. this.modal = document.getElementById('aiGenerateModal');
  33. this.previewImage = document.getElementById('previewImage');
  34. this.previewPlaceholder = document.getElementById('previewPlaceholder');
  35. this.referenceBox = document.getElementById('referenceBox');
  36. this.referenceInput = document.getElementById('referenceInput');
  37. this.referenceUploadArea = document.getElementById('referenceUploadArea');
  38. this.referenceImage = document.getElementById('referenceImage');
  39. this.referenceImageWrapper = document.getElementById('referenceImageWrapper');
  40. this.referenceRemoveBtn = document.getElementById('referenceRemoveBtn');
  41. this.generateBtn = document.getElementById('generateBtn');
  42. this.aiGeneratePriceEl = document.getElementById('aiGeneratePrice');
  43. this.additionalPromptInput = document.getElementById('additionalPromptInput');
  44. this.cancelBtn = document.getElementById('aiGenerateCancelBtn');
  45. // 加载AI生图价格
  46. this.loadAIGeneratePrice();
  47. this.bindEvents();
  48. this.reset();
  49. }
  50. bindEvents() {
  51. // 关闭按钮
  52. this.cancelBtn?.addEventListener('click', () => {
  53. this.close();
  54. });
  55. // AI生图按钮
  56. this.generateBtn?.addEventListener('click', () => {
  57. this.generateAI();
  58. });
  59. // 删除参考图按钮
  60. this.referenceRemoveBtn?.addEventListener('click', (e) => {
  61. e.stopPropagation();
  62. this.removeReferenceImage();
  63. });
  64. // 点击遮罩层关闭
  65. this.overlay?.addEventListener('click', (e) => {
  66. if (e.target === this.overlay) {
  67. this.close();
  68. }
  69. });
  70. // ESC键关闭
  71. document.addEventListener('keydown', (e) => {
  72. if (e.key === 'Escape' && this.overlay) {
  73. this.close();
  74. }
  75. });
  76. // 参考图上传区域点击
  77. this.referenceBox?.addEventListener('click', () => {
  78. this.referenceInput?.click();
  79. });
  80. // 参考图选择
  81. this.referenceInput?.addEventListener('change', (e) => {
  82. const file = e.target.files[0];
  83. if (file) {
  84. this.loadReferenceImage(file);
  85. }
  86. });
  87. // 拖拽上传参考图
  88. this.referenceBox?.addEventListener('dragover', (e) => {
  89. e.preventDefault();
  90. e.stopPropagation();
  91. if (this.referenceBox) {
  92. this.referenceBox.style.borderColor = '#667eea';
  93. }
  94. });
  95. this.referenceBox?.addEventListener('dragleave', (e) => {
  96. e.preventDefault();
  97. e.stopPropagation();
  98. if (this.referenceBox) {
  99. this.referenceBox.style.borderColor = '#e5e7eb';
  100. }
  101. });
  102. this.referenceBox?.addEventListener('drop', (e) => {
  103. e.preventDefault();
  104. e.stopPropagation();
  105. if (this.referenceBox) {
  106. this.referenceBox.style.borderColor = '#e5e7eb';
  107. }
  108. const file = e.dataTransfer.files[0];
  109. if (file && file.type.startsWith('image/')) {
  110. this.loadReferenceImage(file);
  111. }
  112. });
  113. // 监听来自父窗口的消息
  114. window.addEventListener('message', (event) => {
  115. if (event.data && event.data.type === 'show-ai-generate') {
  116. this.reset();
  117. this.folderName = event.data.folderName;
  118. this.originalSpritesheetData = event.data.spritesheetData;
  119. this.spritesheetLayout = event.data.spritesheetLayout;
  120. this.showPreview(event.data.spritesheetData);
  121. }
  122. });
  123. }
  124. /**
  125. * 显示预览图
  126. */
  127. showPreview(imageUrl) {
  128. if (!imageUrl) return;
  129. this.imageData = imageUrl;
  130. const img = new Image();
  131. img.onload = () => {
  132. if (this.previewImage) {
  133. this.previewImage.src = imageUrl;
  134. this.previewImage.classList.add('show');
  135. }
  136. if (this.previewPlaceholder) {
  137. this.previewPlaceholder.classList.add('hide');
  138. }
  139. };
  140. img.onerror = () => {
  141. if (this.previewPlaceholder) {
  142. const spinner = this.previewPlaceholder.querySelector('.loading-spinner');
  143. const loadingText = this.previewPlaceholder.querySelector('.loading-text');
  144. if (spinner) spinner.style.display = 'none';
  145. if (loadingText) {
  146. loadingText.textContent = '图片加载失败';
  147. loadingText.style.color = '#ef4444';
  148. }
  149. this.previewPlaceholder.classList.remove('hide');
  150. }
  151. };
  152. img.src = imageUrl;
  153. }
  154. /**
  155. * 加载参考图
  156. */
  157. loadReferenceImage(file) {
  158. const reader = new FileReader();
  159. reader.onload = (e) => {
  160. const data = e.target.result;
  161. this.referenceImageData = data;
  162. this.showReferenceImage(data);
  163. this.setGenerateButtonState(true);
  164. };
  165. reader.readAsDataURL(file);
  166. }
  167. /**
  168. * 删除参考图
  169. */
  170. removeReferenceImage() {
  171. this.referenceImageData = null;
  172. if (this.referenceImageWrapper) {
  173. this.referenceImageWrapper.style.display = 'none';
  174. }
  175. if (this.referenceImage) {
  176. this.referenceImage.src = '';
  177. }
  178. if (this.referenceUploadArea) {
  179. this.referenceUploadArea.classList.remove('hide');
  180. }
  181. if (this.referenceInput) {
  182. this.referenceInput.value = '';
  183. }
  184. this.setGenerateButtonState(false);
  185. }
  186. /**
  187. * 设置生图按钮状态
  188. * @param {boolean} enabled
  189. */
  190. setGenerateButtonState(enabled) {
  191. this.isGenerateEnabled = !!enabled;
  192. if (this.generateBtn) {
  193. this.generateBtn.disabled = !enabled;
  194. }
  195. }
  196. /**
  197. * AI生图
  198. */
  199. async generateAI() {
  200. if (!this.originalSpritesheetData || !this.referenceImageData) {
  201. this.showAlert('请先上传参考图');
  202. return;
  203. }
  204. const username = this.getCurrentUsername();
  205. if (!username) {
  206. this.showAlert('请先登录');
  207. return;
  208. }
  209. // 检查 Ani币余额
  210. try {
  211. const pricingResponse = await fetch('/api/product-pricing');
  212. if (!pricingResponse.ok) {
  213. throw new Error('获取价格失败');
  214. }
  215. const pricingResult = await pricingResponse.json();
  216. if (!pricingResult.success || !pricingResult.products) {
  217. throw new Error('获取价格失败');
  218. }
  219. const aiGenerateProduct = pricingResult.products.find(p => p.id === 'ai-generate');
  220. const price = aiGenerateProduct ? (aiGenerateProduct.price || 0) : 0;
  221. if (price > 0) {
  222. const pointsResponse = await fetch(`/api/user/points?username=${encodeURIComponent(username)}`);
  223. if (!pointsResponse.ok) {
  224. throw new Error('获取点数失败');
  225. }
  226. const pointsResult = await pointsResponse.json();
  227. if (!pointsResult.success) {
  228. throw new Error('获取点数失败');
  229. }
  230. const userPoints = pointsResult.points || 0;
  231. if (userPoints < price) {
  232. if (window.parent && window.parent.postMessage) {
  233. window.parent.postMessage({
  234. type: 'open-recharge-view',
  235. needPoints: price,
  236. currentPoints: userPoints
  237. }, '*');
  238. }
  239. this.showAlert(`Ani币不足,需要 ${price} Ani币,您当前有 ${userPoints.toFixed(2)} Ani币。请先充值!`);
  240. return;
  241. }
  242. }
  243. } catch (error) {
  244. console.error('[AIGenerateView] 余额检查失败:', error);
  245. this.showAlert('检查余额失败:' + error.message);
  246. return;
  247. }
  248. // 禁用生图按钮
  249. if (this.generateBtn) {
  250. this.generateBtn.disabled = true;
  251. }
  252. try {
  253. // 准备图片数据
  254. // image1 使用原始spritesheet(模板),image2 使用参考图(角色设计)
  255. // 这样与 prompt 中的描述一致:image1 是模板布局,image2 是要应用的角色外观
  256. const image1Base64 = this.originalSpritesheetData.replace(/^data:image\/\w+;base64,/, '');
  257. const image2Base64 = this.referenceImageData.replace(/^data:image\/\w+;base64,/, '');
  258. const image1Width = this.spritesheetLayout?.sheetWidth || 0;
  259. const image1Height = this.spritesheetLayout?.sheetHeight || 0;
  260. const additionalPrompt = this.additionalPromptInput?.value || '';
  261. // 扣除Ani币
  262. const pricingResponse = await fetch('/api/product-pricing');
  263. const pricingResult = await pricingResponse.json();
  264. const aiGenerateProduct = pricingResult.products?.find(p => p.id === 'ai-generate');
  265. const price = aiGenerateProduct ? (aiGenerateProduct.price || 0) : 0;
  266. if (price > 0) {
  267. const deductResponse = await fetch('/api/user/deduct-points', {
  268. method: 'POST',
  269. headers: { 'Content-Type': 'application/json' },
  270. body: JSON.stringify({ username, points: price })
  271. });
  272. if (!deductResponse.ok) {
  273. const deductResult = await deductResponse.json();
  274. throw new Error(deductResult.message || '扣除Ani币失败');
  275. }
  276. const deductResult = await deductResponse.json();
  277. if (!deductResult.success) {
  278. throw new Error(deductResult.message || '扣除Ani币失败');
  279. }
  280. // 通知父窗口刷新点数
  281. if (window.parent && window.parent.postMessage) {
  282. window.parent.postMessage({ type: 'refresh-points' }, '*');
  283. }
  284. }
  285. // 调用队列API
  286. const response = await fetch('/api/ai/generate', {
  287. method: 'POST',
  288. headers: { 'Content-Type': 'application/json' },
  289. body: JSON.stringify({
  290. username: username,
  291. image1: image1Base64,
  292. image2: image2Base64,
  293. image1Width: image1Width,
  294. image1Height: image1Height,
  295. additionalPrompt: additionalPrompt
  296. })
  297. });
  298. if (!response.ok) {
  299. const errorData = await response.json().catch(() => ({}));
  300. throw new Error(errorData.message || '请求生图失败');
  301. }
  302. const result = await response.json();
  303. if (result.success && result.taskId) {
  304. this.sessionTaskIds.push(result.taskId);
  305. this.showFlyAwayAnimation();
  306. this.loadAIQueue(username);
  307. } else {
  308. throw new Error(result.message || '请求失败');
  309. }
  310. } catch (error) {
  311. console.error('[AIGenerateView] 请求生图失败:', error);
  312. this.showAlert(error.message || '请求失败,请稍后重试');
  313. } finally {
  314. if (this.generateBtn) {
  315. this.generateBtn.disabled = false;
  316. }
  317. }
  318. }
  319. /**
  320. * 加载AI生图队列
  321. */
  322. async loadAIQueue(username) {
  323. if (!this.sessionTaskIds || this.sessionTaskIds.length === 0) {
  324. const queueSection = document.getElementById('aiQueueSection');
  325. if (queueSection) {
  326. queueSection.style.display = 'none';
  327. }
  328. return;
  329. }
  330. if (!username) {
  331. username = this.getCurrentUsername();
  332. }
  333. if (!username) return;
  334. try {
  335. const response = await fetch(`/api/ai/history?username=${encodeURIComponent(username)}`);
  336. if (!response.ok) return;
  337. const result = await response.json();
  338. if (!result.success || !result.history) return;
  339. const sessionTasks = result.history.filter(t => this.sessionTaskIds.includes(t.id));
  340. this.renderAIQueue(sessionTasks);
  341. // 如果有进行中的任务,继续轮询
  342. const hasPending = sessionTasks.some(t => t.status === 'queued' || t.status === 'rendering');
  343. if (hasPending) {
  344. if (this.queuePollingTimer) {
  345. clearTimeout(this.queuePollingTimer);
  346. }
  347. this.queuePollingTimer = setTimeout(() => {
  348. this.loadAIQueue(username);
  349. }, 3000);
  350. }
  351. } catch (error) {
  352. console.error('[AIGenerateView] 加载AI队列失败:', error);
  353. }
  354. }
  355. /**
  356. * 渲染AI队列
  357. */
  358. renderAIQueue(tasks) {
  359. const queueSection = document.getElementById('aiQueueSection');
  360. const queueList = document.getElementById('aiQueueList');
  361. if (!queueSection || !queueList) return;
  362. if (tasks.length === 0) {
  363. queueSection.style.display = 'none';
  364. return;
  365. }
  366. queueSection.style.display = 'block';
  367. queueList.innerHTML = tasks.map(task => this.createQueueItemHTML(task)).join('');
  368. // 绑定点击预览事件
  369. queueList.querySelectorAll('.ai-queue-item').forEach(item => {
  370. const taskId = item.dataset.taskId;
  371. const task = tasks.find(t => t.id === taskId);
  372. if (task && task.status === 'completed' && task.imageUrl) {
  373. item.style.cursor = 'pointer';
  374. item.addEventListener('click', () => {
  375. this.showImagePreviewModal(task.imageUrl, task.id);
  376. });
  377. }
  378. });
  379. }
  380. /**
  381. * 创建队列项HTML
  382. */
  383. createQueueItemHTML(task) {
  384. const previewUrl = task.previewUrl || '';
  385. if (task.status === 'queued' || task.status === 'rendering') {
  386. const statusText = task.status === 'queued' ? '等待中' : '生成中';
  387. return `
  388. <div class="ai-queue-item ${task.status}" data-task-id="${task.id}">
  389. ${previewUrl ? `<img class="ai-queue-item-preview" src="${previewUrl}" alt="预览">` : ''}
  390. <div class="ai-queue-item-overlay">
  391. <div class="ai-queue-item-spinner"></div>
  392. <div class="ai-queue-item-status">${statusText}</div>
  393. </div>
  394. </div>
  395. `;
  396. } else if (task.status === 'completed' && task.imageUrl) {
  397. return `
  398. <div class="ai-queue-item completed" data-task-id="${task.id}" title="点击放大预览">
  399. <img class="ai-queue-item-preview" src="${task.imageUrl}" alt="完成">
  400. <div class="ai-queue-item-overlay">
  401. <div class="ai-queue-item-icon">🔍</div>
  402. </div>
  403. </div>
  404. `;
  405. } else if (task.status === 'failed') {
  406. return `
  407. <div class="ai-queue-item failed" data-task-id="${task.id}">
  408. ${previewUrl ? `<img class="ai-queue-item-preview" src="${previewUrl}" alt="预览">` : ''}
  409. <div class="ai-queue-item-overlay">
  410. <div class="ai-queue-item-icon">❌</div>
  411. <div class="ai-queue-item-status">失败</div>
  412. </div>
  413. </div>
  414. `;
  415. }
  416. return '';
  417. }
  418. /**
  419. * 显示图片预览弹窗
  420. */
  421. showImagePreviewModal(imageUrl, taskId) {
  422. // 在父窗口中创建预览弹窗,确保在最上层
  423. let targetDocument = document;
  424. try {
  425. if (window.parent && window.parent !== window && window.parent.document) {
  426. targetDocument = window.parent.document;
  427. }
  428. } catch (e) {}
  429. const modal = targetDocument.createElement('div');
  430. modal.className = 'image-preview-modal';
  431. modal.style.cssText = 'position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 9999999; display: flex; align-items: center; justify-content: center; opacity: 0; transition: opacity 0.3s ease;';
  432. modal.innerHTML = `
  433. <div class="image-preview-modal-backdrop" style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.85); backdrop-filter: blur(8px);"></div>
  434. <div class="image-preview-modal-content" style="position: relative; max-width: 90vw; max-height: 90vh; transform: scale(0.9); transition: transform 0.3s ease;">
  435. <img src="${imageUrl}" alt="AI生成图预览" style="max-width: 100%; max-height: 85vh; border-radius: 12px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);">
  436. <button class="image-preview-modal-close" style="position: absolute; top: -40px; right: 0; width: 36px; height: 36px; border: none; background: rgba(255, 255, 255, 0.2); color: white; font-size: 24px; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; line-height: 1; transition: all 0.2s;">×</button>
  437. </div>
  438. `;
  439. targetDocument.body.appendChild(modal);
  440. // 显示动画
  441. requestAnimationFrame(() => {
  442. modal.style.opacity = '1';
  443. modal.querySelector('.image-preview-modal-content').style.transform = 'scale(1)';
  444. });
  445. const closeModal = () => {
  446. modal.style.opacity = '0';
  447. modal.querySelector('.image-preview-modal-content').style.transform = 'scale(0.9)';
  448. setTimeout(() => {
  449. if (modal.parentNode) {
  450. modal.parentNode.removeChild(modal);
  451. }
  452. }, 300);
  453. };
  454. modal.querySelector('.image-preview-modal-backdrop').onclick = closeModal;
  455. modal.querySelector('.image-preview-modal-close').onclick = closeModal;
  456. // 鼠标悬停关闭按钮效果
  457. const closeBtn = modal.querySelector('.image-preview-modal-close');
  458. closeBtn.onmouseenter = () => {
  459. closeBtn.style.background = 'rgba(255, 255, 255, 0.3)';
  460. closeBtn.style.transform = 'scale(1.1)';
  461. };
  462. closeBtn.onmouseleave = () => {
  463. closeBtn.style.background = 'rgba(255, 255, 255, 0.2)';
  464. closeBtn.style.transform = 'scale(1)';
  465. };
  466. // ESC关闭(在父窗口监听)
  467. const targetWindow = targetDocument.defaultView || window;
  468. const escHandler = (e) => {
  469. if (e.key === 'Escape') {
  470. closeModal();
  471. targetWindow.removeEventListener('keydown', escHandler);
  472. }
  473. };
  474. targetWindow.addEventListener('keydown', escHandler);
  475. }
  476. /**
  477. * 显示飞走动画
  478. */
  479. showFlyAwayAnimation() {
  480. let targetDocument = document;
  481. let targetWindow = window;
  482. try {
  483. if (window.parent && window.parent !== window && window.parent.document) {
  484. targetDocument = window.parent.document;
  485. targetWindow = window.parent;
  486. }
  487. } catch (e) {}
  488. const flyElement = targetDocument.createElement('div');
  489. flyElement.innerHTML = `
  490. <div style="display: flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 600; white-space: nowrap;">
  491. <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  492. <path d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z"/>
  493. </svg>
  494. <span>AI生图任务</span>
  495. </div>
  496. `;
  497. let startLeft = targetWindow.innerWidth / 2;
  498. let startTop = targetWindow.innerHeight / 2;
  499. if (this.generateBtn) {
  500. const btnRect = this.generateBtn.getBoundingClientRect();
  501. try {
  502. if (window.parent && window.parent !== window) {
  503. const iframe = window.parent.document.querySelector('iframe[src*="ai-generate"]');
  504. if (iframe) {
  505. const iframeRect = iframe.getBoundingClientRect();
  506. startLeft = iframeRect.left + btnRect.left;
  507. startTop = iframeRect.top + btnRect.top;
  508. }
  509. }
  510. } catch (e) {
  511. startLeft = btnRect.left;
  512. startTop = btnRect.top;
  513. }
  514. }
  515. flyElement.style.cssText = `
  516. position: fixed;
  517. left: ${startLeft}px;
  518. top: ${startTop}px;
  519. z-index: 999999;
  520. pointer-events: none;
  521. transition: all 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94);
  522. opacity: 1;
  523. transform: scale(1);
  524. background: linear-gradient(135deg, #10b981 0%, #059669 100%);
  525. color: white;
  526. padding: 12px 20px;
  527. border-radius: 12px;
  528. box-shadow: 0 8px 32px rgba(16, 185, 129, 0.4);
  529. display: flex;
  530. align-items: center;
  531. gap: 8px;
  532. `;
  533. targetDocument.body.appendChild(flyElement);
  534. requestAnimationFrame(() => {
  535. flyElement.style.left = `${targetWindow.innerWidth - 100}px`;
  536. flyElement.style.top = '50px';
  537. flyElement.style.opacity = '0';
  538. flyElement.style.transform = 'scale(0.3)';
  539. });
  540. setTimeout(() => {
  541. if (window.parent && window.parent.HintView) {
  542. window.parent.HintView.success('AI生图任务已添加到「我的」-「任务历史」,请稍后查看', 3000);
  543. }
  544. }, 300);
  545. setTimeout(() => {
  546. if (flyElement.parentNode) {
  547. flyElement.parentNode.removeChild(flyElement);
  548. }
  549. }, 1000);
  550. }
  551. /**
  552. * 加载AI生图价格
  553. */
  554. async loadAIGeneratePrice() {
  555. try {
  556. const response = await fetch('/api/product-pricing');
  557. if (response.ok) {
  558. const result = await response.json();
  559. if (result.success && result.products) {
  560. const aiGenerateProduct = result.products.find(p => p.id === 'ai-generate');
  561. if (aiGenerateProduct && this.aiGeneratePriceEl) {
  562. const price = aiGenerateProduct.price || 0;
  563. if (price > 0) {
  564. this.aiGeneratePriceEl.textContent = `${price} Ani币`;
  565. } else {
  566. this.aiGeneratePriceEl.textContent = '免费';
  567. }
  568. }
  569. }
  570. }
  571. } catch (error) {
  572. console.error('[AIGenerateView] 加载AI生图价格失败:', error);
  573. if (this.aiGeneratePriceEl) {
  574. this.aiGeneratePriceEl.textContent = '-';
  575. }
  576. }
  577. }
  578. /**
  579. * 获取当前登录用户名
  580. */
  581. getCurrentUsername() {
  582. try {
  583. const loginDataStr = localStorage.getItem('loginData');
  584. if (!loginDataStr) return null;
  585. const loginData = JSON.parse(loginDataStr);
  586. const now = Date.now();
  587. if (now >= loginData.expireTime) {
  588. localStorage.removeItem('loginData');
  589. return null;
  590. }
  591. return loginData.user ? loginData.user.username : null;
  592. } catch (error) {
  593. console.error('[AIGenerateView] 获取用户名失败:', error);
  594. return null;
  595. }
  596. }
  597. /**
  598. * 显示提示
  599. */
  600. showAlert(message, duration = 2000) {
  601. try {
  602. if (window.parent && window.parent !== window && window.parent.GlobalAlert) {
  603. window.parent.GlobalAlert.show(message, duration);
  604. return;
  605. }
  606. if (window.GlobalAlert) {
  607. window.GlobalAlert.show(message, duration);
  608. return;
  609. }
  610. console.log('[Alert]', message);
  611. } catch (error) {
  612. console.error('[AIGenerateView] 显示 alert 失败:', error);
  613. alert(message);
  614. }
  615. }
  616. /**
  617. * 关闭弹窗
  618. */
  619. close() {
  620. this.reset();
  621. if (window.parent && window.parent !== window) {
  622. window.parent.postMessage({
  623. type: 'close-ai-generate-view'
  624. }, '*');
  625. }
  626. }
  627. /**
  628. * 重置状态
  629. */
  630. reset() {
  631. this.imageData = null;
  632. this.referenceImageData = null;
  633. this.sessionTaskIds = [];
  634. if (this.queuePollingTimer) {
  635. clearTimeout(this.queuePollingTimer);
  636. this.queuePollingTimer = null;
  637. }
  638. const queueSection = document.getElementById('aiQueueSection');
  639. if (queueSection) {
  640. queueSection.style.display = 'none';
  641. }
  642. if (this.referenceImageWrapper) {
  643. this.referenceImageWrapper.style.display = 'none';
  644. }
  645. if (this.referenceImage) {
  646. this.referenceImage.src = '';
  647. }
  648. if (this.referenceUploadArea) {
  649. this.referenceUploadArea.classList.remove('hide');
  650. }
  651. if (this.referenceInput) {
  652. this.referenceInput.value = '';
  653. }
  654. if (this.previewImage) {
  655. this.previewImage.src = '';
  656. this.previewImage.classList.remove('show');
  657. }
  658. if (this.previewPlaceholder) {
  659. const spinner = this.previewPlaceholder.querySelector('.loading-spinner');
  660. const loadingText = this.previewPlaceholder.querySelector('.loading-text');
  661. if (spinner) spinner.style.display = 'block';
  662. if (loadingText) {
  663. loadingText.textContent = '正在生成预览图...';
  664. loadingText.style.color = '#6b7280';
  665. }
  666. this.previewPlaceholder.classList.remove('hide');
  667. }
  668. const promptConfigSection = document.getElementById('promptConfigSection');
  669. if (promptConfigSection) {
  670. promptConfigSection.style.display = 'flex';
  671. }
  672. if (this.additionalPromptInput) {
  673. this.additionalPromptInput.value = '';
  674. }
  675. this.setGenerateButtonState(false);
  676. }
  677. /**
  678. * 显示参考图并隐藏上传区域
  679. */
  680. showReferenceImage(dataUrl) {
  681. if (this.referenceImage) {
  682. this.referenceImage.src = dataUrl;
  683. }
  684. if (this.referenceImageWrapper) {
  685. this.referenceImageWrapper.style.display = 'flex';
  686. }
  687. if (this.referenceUploadArea) {
  688. this.referenceUploadArea.classList.add('hide');
  689. }
  690. const promptConfigSection = document.getElementById('promptConfigSection');
  691. if (promptConfigSection) {
  692. promptConfigSection.style.display = 'flex';
  693. }
  694. }
  695. }
  696. // 初始化
  697. window.AIGenerateView = new AIGenerateView();