ai-generate-view.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793
  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 直接使用参考图,image2 使用原始spritesheet
  255. const image1Base64 = this.referenceImageData.replace(/^data:image\/\w+;base64,/, '');
  256. const image2Base64 = this.originalSpritesheetData.replace(/^data:image\/\w+;base64,/, '');
  257. const image1Width = this.spritesheetLayout?.sheetWidth || 0;
  258. const image1Height = this.spritesheetLayout?.sheetHeight || 0;
  259. const additionalPrompt = this.additionalPromptInput?.value || '';
  260. // 扣除Ani币
  261. const pricingResponse = await fetch('/api/product-pricing');
  262. const pricingResult = await pricingResponse.json();
  263. const aiGenerateProduct = pricingResult.products?.find(p => p.id === 'ai-generate');
  264. const price = aiGenerateProduct ? (aiGenerateProduct.price || 0) : 0;
  265. if (price > 0) {
  266. const deductResponse = await fetch('/api/user/deduct-points', {
  267. method: 'POST',
  268. headers: { 'Content-Type': 'application/json' },
  269. body: JSON.stringify({ username, points: price })
  270. });
  271. if (!deductResponse.ok) {
  272. const deductResult = await deductResponse.json();
  273. throw new Error(deductResult.message || '扣除Ani币失败');
  274. }
  275. const deductResult = await deductResponse.json();
  276. if (!deductResult.success) {
  277. throw new Error(deductResult.message || '扣除Ani币失败');
  278. }
  279. // 通知父窗口刷新点数
  280. if (window.parent && window.parent.postMessage) {
  281. window.parent.postMessage({ type: 'refresh-points' }, '*');
  282. }
  283. }
  284. // 调用队列API
  285. const response = await fetch('/api/ai/generate', {
  286. method: 'POST',
  287. headers: { 'Content-Type': 'application/json' },
  288. body: JSON.stringify({
  289. username: username,
  290. image1: image1Base64,
  291. image2: image2Base64,
  292. image1Width: image1Width,
  293. image1Height: image1Height,
  294. additionalPrompt: additionalPrompt
  295. })
  296. });
  297. if (!response.ok) {
  298. const errorData = await response.json().catch(() => ({}));
  299. throw new Error(errorData.message || '请求生图失败');
  300. }
  301. const result = await response.json();
  302. if (result.success && result.taskId) {
  303. this.sessionTaskIds.push(result.taskId);
  304. this.showFlyAwayAnimation();
  305. this.loadAIQueue(username);
  306. } else {
  307. throw new Error(result.message || '请求失败');
  308. }
  309. } catch (error) {
  310. console.error('[AIGenerateView] 请求生图失败:', error);
  311. this.showAlert(error.message || '请求失败,请稍后重试');
  312. } finally {
  313. if (this.generateBtn) {
  314. this.generateBtn.disabled = false;
  315. }
  316. }
  317. }
  318. /**
  319. * 加载AI生图队列
  320. */
  321. async loadAIQueue(username) {
  322. if (!this.sessionTaskIds || this.sessionTaskIds.length === 0) {
  323. const queueSection = document.getElementById('aiQueueSection');
  324. if (queueSection) {
  325. queueSection.style.display = 'none';
  326. }
  327. return;
  328. }
  329. if (!username) {
  330. username = this.getCurrentUsername();
  331. }
  332. if (!username) return;
  333. try {
  334. const response = await fetch(`/api/ai/history?username=${encodeURIComponent(username)}`);
  335. if (!response.ok) return;
  336. const result = await response.json();
  337. if (!result.success || !result.history) return;
  338. const sessionTasks = result.history.filter(t => this.sessionTaskIds.includes(t.id));
  339. this.renderAIQueue(sessionTasks);
  340. // 如果有进行中的任务,继续轮询
  341. const hasPending = sessionTasks.some(t => t.status === 'queued' || t.status === 'rendering');
  342. if (hasPending) {
  343. if (this.queuePollingTimer) {
  344. clearTimeout(this.queuePollingTimer);
  345. }
  346. this.queuePollingTimer = setTimeout(() => {
  347. this.loadAIQueue(username);
  348. }, 3000);
  349. }
  350. } catch (error) {
  351. console.error('[AIGenerateView] 加载AI队列失败:', error);
  352. }
  353. }
  354. /**
  355. * 渲染AI队列
  356. */
  357. renderAIQueue(tasks) {
  358. const queueSection = document.getElementById('aiQueueSection');
  359. const queueList = document.getElementById('aiQueueList');
  360. if (!queueSection || !queueList) return;
  361. if (tasks.length === 0) {
  362. queueSection.style.display = 'none';
  363. return;
  364. }
  365. queueSection.style.display = 'block';
  366. queueList.innerHTML = tasks.map(task => this.createQueueItemHTML(task)).join('');
  367. // 绑定点击预览事件
  368. queueList.querySelectorAll('.ai-queue-item').forEach(item => {
  369. const taskId = item.dataset.taskId;
  370. const task = tasks.find(t => t.id === taskId);
  371. if (task && task.status === 'completed' && task.imageUrl) {
  372. item.style.cursor = 'pointer';
  373. item.addEventListener('click', () => {
  374. this.showImagePreviewModal(task.imageUrl, task.id);
  375. });
  376. }
  377. });
  378. }
  379. /**
  380. * 创建队列项HTML
  381. */
  382. createQueueItemHTML(task) {
  383. const previewUrl = task.previewUrl || '';
  384. if (task.status === 'queued' || task.status === 'rendering') {
  385. const statusText = task.status === 'queued' ? '等待中' : '生成中';
  386. return `
  387. <div class="ai-queue-item ${task.status}" data-task-id="${task.id}">
  388. ${previewUrl ? `<img class="ai-queue-item-preview" src="${previewUrl}" alt="预览">` : ''}
  389. <div class="ai-queue-item-overlay">
  390. <div class="ai-queue-item-spinner"></div>
  391. <div class="ai-queue-item-status">${statusText}</div>
  392. </div>
  393. </div>
  394. `;
  395. } else if (task.status === 'completed' && task.imageUrl) {
  396. return `
  397. <div class="ai-queue-item completed" data-task-id="${task.id}" title="点击放大预览">
  398. <img class="ai-queue-item-preview" src="${task.imageUrl}" alt="完成">
  399. <div class="ai-queue-item-overlay">
  400. <div class="ai-queue-item-icon">🔍</div>
  401. </div>
  402. </div>
  403. `;
  404. } else if (task.status === 'failed') {
  405. return `
  406. <div class="ai-queue-item failed" data-task-id="${task.id}">
  407. ${previewUrl ? `<img class="ai-queue-item-preview" src="${previewUrl}" alt="预览">` : ''}
  408. <div class="ai-queue-item-overlay">
  409. <div class="ai-queue-item-icon">❌</div>
  410. <div class="ai-queue-item-status">失败</div>
  411. </div>
  412. </div>
  413. `;
  414. }
  415. return '';
  416. }
  417. /**
  418. * 显示图片预览弹窗
  419. */
  420. showImagePreviewModal(imageUrl, taskId) {
  421. // 在父窗口中创建预览弹窗,确保在最上层
  422. let targetDocument = document;
  423. try {
  424. if (window.parent && window.parent !== window && window.parent.document) {
  425. targetDocument = window.parent.document;
  426. }
  427. } catch (e) {}
  428. const modal = targetDocument.createElement('div');
  429. modal.className = 'image-preview-modal';
  430. 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;';
  431. modal.innerHTML = `
  432. <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>
  433. <div class="image-preview-modal-content" style="position: relative; max-width: 90vw; max-height: 90vh; transform: scale(0.9); transition: transform 0.3s ease;">
  434. <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);">
  435. <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>
  436. </div>
  437. `;
  438. targetDocument.body.appendChild(modal);
  439. // 显示动画
  440. requestAnimationFrame(() => {
  441. modal.style.opacity = '1';
  442. modal.querySelector('.image-preview-modal-content').style.transform = 'scale(1)';
  443. });
  444. const closeModal = () => {
  445. modal.style.opacity = '0';
  446. modal.querySelector('.image-preview-modal-content').style.transform = 'scale(0.9)';
  447. setTimeout(() => {
  448. if (modal.parentNode) {
  449. modal.parentNode.removeChild(modal);
  450. }
  451. }, 300);
  452. };
  453. modal.querySelector('.image-preview-modal-backdrop').onclick = closeModal;
  454. modal.querySelector('.image-preview-modal-close').onclick = closeModal;
  455. // 鼠标悬停关闭按钮效果
  456. const closeBtn = modal.querySelector('.image-preview-modal-close');
  457. closeBtn.onmouseenter = () => {
  458. closeBtn.style.background = 'rgba(255, 255, 255, 0.3)';
  459. closeBtn.style.transform = 'scale(1.1)';
  460. };
  461. closeBtn.onmouseleave = () => {
  462. closeBtn.style.background = 'rgba(255, 255, 255, 0.2)';
  463. closeBtn.style.transform = 'scale(1)';
  464. };
  465. // ESC关闭(在父窗口监听)
  466. const targetWindow = targetDocument.defaultView || window;
  467. const escHandler = (e) => {
  468. if (e.key === 'Escape') {
  469. closeModal();
  470. targetWindow.removeEventListener('keydown', escHandler);
  471. }
  472. };
  473. targetWindow.addEventListener('keydown', escHandler);
  474. }
  475. /**
  476. * 显示飞走动画
  477. */
  478. showFlyAwayAnimation() {
  479. let targetDocument = document;
  480. let targetWindow = window;
  481. try {
  482. if (window.parent && window.parent !== window && window.parent.document) {
  483. targetDocument = window.parent.document;
  484. targetWindow = window.parent;
  485. }
  486. } catch (e) {}
  487. const flyElement = targetDocument.createElement('div');
  488. flyElement.innerHTML = `
  489. <div style="display: flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 600; white-space: nowrap;">
  490. <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  491. <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"/>
  492. </svg>
  493. <span>AI生图任务</span>
  494. </div>
  495. `;
  496. let startLeft = targetWindow.innerWidth / 2;
  497. let startTop = targetWindow.innerHeight / 2;
  498. if (this.generateBtn) {
  499. const btnRect = this.generateBtn.getBoundingClientRect();
  500. try {
  501. if (window.parent && window.parent !== window) {
  502. const iframe = window.parent.document.querySelector('iframe[src*="ai-generate"]');
  503. if (iframe) {
  504. const iframeRect = iframe.getBoundingClientRect();
  505. startLeft = iframeRect.left + btnRect.left;
  506. startTop = iframeRect.top + btnRect.top;
  507. }
  508. }
  509. } catch (e) {
  510. startLeft = btnRect.left;
  511. startTop = btnRect.top;
  512. }
  513. }
  514. flyElement.style.cssText = `
  515. position: fixed;
  516. left: ${startLeft}px;
  517. top: ${startTop}px;
  518. z-index: 999999;
  519. pointer-events: none;
  520. transition: all 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94);
  521. opacity: 1;
  522. transform: scale(1);
  523. background: linear-gradient(135deg, #10b981 0%, #059669 100%);
  524. color: white;
  525. padding: 12px 20px;
  526. border-radius: 12px;
  527. box-shadow: 0 8px 32px rgba(16, 185, 129, 0.4);
  528. display: flex;
  529. align-items: center;
  530. gap: 8px;
  531. `;
  532. targetDocument.body.appendChild(flyElement);
  533. requestAnimationFrame(() => {
  534. flyElement.style.left = `${targetWindow.innerWidth - 100}px`;
  535. flyElement.style.top = '50px';
  536. flyElement.style.opacity = '0';
  537. flyElement.style.transform = 'scale(0.3)';
  538. });
  539. setTimeout(() => {
  540. if (window.parent && window.parent.HintView) {
  541. window.parent.HintView.success('已添加到「我的」-「AI生图历史」,请稍后查看', 3000);
  542. }
  543. }, 300);
  544. setTimeout(() => {
  545. if (flyElement.parentNode) {
  546. flyElement.parentNode.removeChild(flyElement);
  547. }
  548. }, 1000);
  549. }
  550. /**
  551. * 加载AI生图价格
  552. */
  553. async loadAIGeneratePrice() {
  554. try {
  555. const response = await fetch('/api/product-pricing');
  556. if (response.ok) {
  557. const result = await response.json();
  558. if (result.success && result.products) {
  559. const aiGenerateProduct = result.products.find(p => p.id === 'ai-generate');
  560. if (aiGenerateProduct && this.aiGeneratePriceEl) {
  561. const price = aiGenerateProduct.price || 0;
  562. if (price > 0) {
  563. this.aiGeneratePriceEl.textContent = `${price} Ani币`;
  564. } else {
  565. this.aiGeneratePriceEl.textContent = '免费';
  566. }
  567. }
  568. }
  569. }
  570. } catch (error) {
  571. console.error('[AIGenerateView] 加载AI生图价格失败:', error);
  572. if (this.aiGeneratePriceEl) {
  573. this.aiGeneratePriceEl.textContent = '-';
  574. }
  575. }
  576. }
  577. /**
  578. * 获取当前登录用户名
  579. */
  580. getCurrentUsername() {
  581. try {
  582. const loginDataStr = localStorage.getItem('loginData');
  583. if (!loginDataStr) return null;
  584. const loginData = JSON.parse(loginDataStr);
  585. const now = Date.now();
  586. if (now >= loginData.expireTime) {
  587. localStorage.removeItem('loginData');
  588. return null;
  589. }
  590. return loginData.user ? loginData.user.username : null;
  591. } catch (error) {
  592. console.error('[AIGenerateView] 获取用户名失败:', error);
  593. return null;
  594. }
  595. }
  596. /**
  597. * 显示提示
  598. */
  599. showAlert(message, duration = 2000) {
  600. try {
  601. if (window.parent && window.parent !== window && window.parent.GlobalAlert) {
  602. window.parent.GlobalAlert.show(message, duration);
  603. return;
  604. }
  605. if (window.GlobalAlert) {
  606. window.GlobalAlert.show(message, duration);
  607. return;
  608. }
  609. console.log('[Alert]', message);
  610. } catch (error) {
  611. console.error('[AIGenerateView] 显示 alert 失败:', error);
  612. alert(message);
  613. }
  614. }
  615. /**
  616. * 关闭弹窗
  617. */
  618. close() {
  619. this.reset();
  620. if (window.parent && window.parent !== window) {
  621. window.parent.postMessage({
  622. type: 'close-ai-generate-view'
  623. }, '*');
  624. }
  625. }
  626. /**
  627. * 重置状态
  628. */
  629. reset() {
  630. this.imageData = null;
  631. this.referenceImageData = null;
  632. this.sessionTaskIds = [];
  633. if (this.queuePollingTimer) {
  634. clearTimeout(this.queuePollingTimer);
  635. this.queuePollingTimer = null;
  636. }
  637. const queueSection = document.getElementById('aiQueueSection');
  638. if (queueSection) {
  639. queueSection.style.display = 'none';
  640. }
  641. if (this.referenceImageWrapper) {
  642. this.referenceImageWrapper.style.display = 'none';
  643. }
  644. if (this.referenceImage) {
  645. this.referenceImage.src = '';
  646. }
  647. if (this.referenceUploadArea) {
  648. this.referenceUploadArea.classList.remove('hide');
  649. }
  650. if (this.referenceInput) {
  651. this.referenceInput.value = '';
  652. }
  653. if (this.previewImage) {
  654. this.previewImage.src = '';
  655. this.previewImage.classList.remove('show');
  656. }
  657. if (this.previewPlaceholder) {
  658. const spinner = this.previewPlaceholder.querySelector('.loading-spinner');
  659. const loadingText = this.previewPlaceholder.querySelector('.loading-text');
  660. if (spinner) spinner.style.display = 'block';
  661. if (loadingText) {
  662. loadingText.textContent = '正在生成预览图...';
  663. loadingText.style.color = '#6b7280';
  664. }
  665. this.previewPlaceholder.classList.remove('hide');
  666. }
  667. const promptConfigSection = document.getElementById('promptConfigSection');
  668. if (promptConfigSection) {
  669. promptConfigSection.style.display = 'flex';
  670. }
  671. if (this.additionalPromptInput) {
  672. this.additionalPromptInput.value = '';
  673. }
  674. this.setGenerateButtonState(false);
  675. }
  676. /**
  677. * 显示参考图并隐藏上传区域
  678. */
  679. showReferenceImage(dataUrl) {
  680. if (this.referenceImage) {
  681. this.referenceImage.src = dataUrl;
  682. }
  683. if (this.referenceImageWrapper) {
  684. this.referenceImageWrapper.style.display = 'flex';
  685. }
  686. if (this.referenceUploadArea) {
  687. this.referenceUploadArea.classList.add('hide');
  688. }
  689. const promptConfigSection = document.getElementById('promptConfigSection');
  690. if (promptConfigSection) {
  691. promptConfigSection.style.display = 'flex';
  692. }
  693. }
  694. }
  695. // 初始化
  696. window.AIGenerateView = new AIGenerateView();