profile.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963
  1. // 我的界面逻辑
  2. (function() {
  3. let currentUsername = null;
  4. let currentUserInfo = null;
  5. // 超时配置(与后端保持一致)
  6. const TASK_TIMEOUT = 5 * 60 * 1000; // 5分钟超时
  7. // 下载弹窗相关
  8. let downloadConfirmOverlay = null;
  9. let downloadConfirmClose = null;
  10. let downloadOptions = null;
  11. let pendingDownloadUrl = null;
  12. let pendingDownloadFilename = null;
  13. let exportViewFrame = null;
  14. function init() {
  15. // 初始化下载弹窗元素
  16. downloadConfirmOverlay = document.getElementById('downloadConfirmOverlay');
  17. downloadConfirmClose = document.getElementById('downloadConfirmClose');
  18. downloadOptions = document.querySelectorAll('.download-option');
  19. exportViewFrame = document.getElementById('exportViewFrame');
  20. // 加载VIP抠图价格
  21. loadVIPMattingPrice();
  22. // 初始化下载确认弹窗事件
  23. initDownloadConfirmEvents();
  24. bindEvents();
  25. // 延迟加载,确保DOM完全渲染
  26. setTimeout(() => {
  27. loadUserInfo();
  28. loadPoints();
  29. loadAIHistory();
  30. loadPurchaseHistory();
  31. }, 200);
  32. }
  33. /**
  34. * 加载VIP抠图价格
  35. */
  36. async function loadVIPMattingPrice() {
  37. try {
  38. const response = await fetch('/api/product-pricing');
  39. if (response.ok) {
  40. const result = await response.json();
  41. if (result.success && result.products) {
  42. const vipMattingProduct = result.products.find(p => p.id === 'vip-matting');
  43. const priceEl = document.getElementById('vipMattingPrice');
  44. if (vipMattingProduct && priceEl) {
  45. const price = vipMattingProduct.price || 0;
  46. if (price > 0) {
  47. priceEl.textContent = `${price} Ani币`;
  48. } else {
  49. priceEl.textContent = '免费';
  50. }
  51. }
  52. }
  53. }
  54. } catch (error) {
  55. console.error('[ExportView] 加载VIP抠图价格失败:', error);
  56. const priceEl = document.getElementById('vipMattingPrice');
  57. if (priceEl) {
  58. priceEl.textContent = '-';
  59. }
  60. }
  61. }
  62. function bindEvents() {
  63. // 返回按钮
  64. const backBtn = document.getElementById('backBtn');
  65. if (backBtn) {
  66. backBtn.addEventListener('click', () => {
  67. // 如果在 iframe 中,通过 postMessage 通知父窗口切换页面
  68. if (window.parent && window.parent !== window) {
  69. window.parent.postMessage({
  70. type: 'navigation',
  71. page: 'store'
  72. }, '*');
  73. } else {
  74. // 独立页面,直接跳转到主页面
  75. window.location.href = '../../index.html';
  76. }
  77. });
  78. }
  79. // 头像上传功能已移除
  80. // 充值按钮
  81. const rechargeBtn = document.getElementById('rechargeBtn');
  82. if (rechargeBtn) {
  83. rechargeBtn.addEventListener('click', () => {
  84. // 打开充值界面(通过postMessage通知父窗口,如果是独立页面则直接打开)
  85. if (window.parent && window.parent !== window) {
  86. window.parent.postMessage({
  87. type: 'open-recharge-view'
  88. }, '*');
  89. } else {
  90. // 独立页面,需要创建充值iframe
  91. openRechargeView();
  92. }
  93. });
  94. }
  95. // 退出登录按钮
  96. const logoutBtn = document.getElementById('logoutBtn');
  97. if (logoutBtn) {
  98. logoutBtn.addEventListener('click', handleLogout);
  99. }
  100. // 监听消息
  101. window.addEventListener('message', (event) => {
  102. if (event.data && event.data.type === 'recharge-success') {
  103. loadPoints(); // 刷新点数
  104. closeRechargeView(); // 关闭充值界面
  105. } else if (event.data && event.data.type === 'close-recharge-view') {
  106. closeRechargeView();
  107. } else if (event.data && event.data.type === 'open-recharge-view') {
  108. // 如果充值iframe发送消息,也打开充值界面
  109. openRechargeView();
  110. } else if (event.data && event.data.type === 'refresh-points') {
  111. // 刷新点数(购买成功后触发)
  112. loadPoints();
  113. loadPurchaseHistory(); // 同时刷新购买记录
  114. } else if (event.data && event.data.type === 'refresh-ai-history') {
  115. // 刷新AI历史(生图请求成功后触发)
  116. loadAIHistory();
  117. } else if (event.data && (event.data.type === 'close-export-view' || event.data.type === 'export-confirmed')) {
  118. hideExportViewFrame();
  119. } else if (event.data && event.data.type === 'export-view-ready') {
  120. // 内容准备好后显示 iframe
  121. showExportViewFrame();
  122. }
  123. });
  124. }
  125. // 打开充值界面
  126. function openRechargeView() {
  127. const rechargeFrame = document.getElementById('rechargeViewFrame');
  128. if (rechargeFrame) {
  129. rechargeFrame.style.display = 'block';
  130. rechargeFrame.style.pointerEvents = 'auto';
  131. rechargeFrame.style.visibility = 'visible';
  132. // 发送消息给充值iframe
  133. const sendRechargeData = () => {
  134. if (rechargeFrame.contentWindow) {
  135. rechargeFrame.contentWindow.postMessage({
  136. type: 'open-recharge-view'
  137. }, '*');
  138. } else {
  139. setTimeout(sendRechargeData, 100);
  140. }
  141. };
  142. sendRechargeData();
  143. const handleLoad = () => {
  144. setTimeout(() => {
  145. sendRechargeData();
  146. }, 50);
  147. };
  148. if (rechargeFrame.contentDocument && rechargeFrame.contentDocument.readyState === 'complete') {
  149. handleLoad();
  150. } else {
  151. rechargeFrame.addEventListener('load', handleLoad, { once: true });
  152. }
  153. }
  154. }
  155. // 关闭充值界面
  156. function closeRechargeView() {
  157. const rechargeFrame = document.getElementById('rechargeViewFrame');
  158. if (rechargeFrame) {
  159. rechargeFrame.style.display = 'none';
  160. rechargeFrame.style.pointerEvents = 'none';
  161. rechargeFrame.style.visibility = 'hidden';
  162. }
  163. }
  164. // 显示导出视图 iframe
  165. function showExportViewFrame() {
  166. if (!exportViewFrame) return;
  167. exportViewFrame.style.display = 'block';
  168. exportViewFrame.style.pointerEvents = 'auto';
  169. exportViewFrame.style.visibility = 'visible';
  170. }
  171. // 隐藏导出视图 iframe
  172. function hideExportViewFrame() {
  173. if (!exportViewFrame) return;
  174. exportViewFrame.style.display = 'none';
  175. exportViewFrame.style.pointerEvents = 'none';
  176. exportViewFrame.style.visibility = 'hidden';
  177. }
  178. // 显示下载确认弹窗
  179. function showDownloadConfirm(imageUrl, fileName) {
  180. pendingDownloadUrl = imageUrl;
  181. pendingDownloadFilename = fileName || 'ai-image';
  182. if (downloadConfirmOverlay) {
  183. downloadConfirmOverlay.style.display = 'flex';
  184. }
  185. }
  186. // 隐藏下载确认弹窗
  187. function hideDownloadConfirm() {
  188. if (downloadConfirmOverlay) {
  189. downloadConfirmOverlay.style.display = 'none';
  190. }
  191. pendingDownloadUrl = null;
  192. pendingDownloadFilename = null;
  193. }
  194. // 初始化下载弹窗事件
  195. function initDownloadConfirmEvents() {
  196. // 关闭按钮
  197. if (downloadConfirmClose) {
  198. downloadConfirmClose.addEventListener('click', hideDownloadConfirm);
  199. }
  200. // 点击遮罩关闭
  201. if (downloadConfirmOverlay) {
  202. downloadConfirmOverlay.addEventListener('click', (e) => {
  203. if (e.target === downloadConfirmOverlay) {
  204. hideDownloadConfirm();
  205. }
  206. });
  207. }
  208. // 下载选项点击
  209. if (downloadOptions) {
  210. downloadOptions.forEach(option => {
  211. option.addEventListener('click', () => {
  212. const downloadType = option.getAttribute('data-option');
  213. handleDownloadOption(downloadType);
  214. });
  215. });
  216. }
  217. }
  218. // 处理下载选项
  219. async function handleDownloadOption(downloadType) {
  220. if (!pendingDownloadUrl) {
  221. console.error('[Profile] 没有待下载的图片');
  222. return;
  223. }
  224. hideDownloadConfirm();
  225. // 根据下载类型处理
  226. if (downloadType === 'original') {
  227. // 直接下载原始图片
  228. downloadImage(pendingDownloadUrl, pendingDownloadFilename);
  229. } else {
  230. // 普通抠图或VIP抠图,使用 iframe 处理
  231. // iframe 会在准备好后发送 'export-view-ready' 消息,此时再显示
  232. if (exportViewFrame) {
  233. const sendData = () => {
  234. try {
  235. exportViewFrame.contentWindow.postMessage({
  236. type: 'show-export-preview',
  237. imageUrl: pendingDownloadUrl,
  238. fileName: pendingDownloadFilename,
  239. skipPreviewUI: true,
  240. directDownloadType: downloadType // 传递下载类型
  241. }, '*');
  242. // iframe 会在准备好后发送 'export-view-ready' 消息
  243. } catch (error) {
  244. console.error('[Profile] 发送导出数据失败:', error);
  245. hideExportViewFrame();
  246. downloadImage(pendingDownloadUrl, pendingDownloadFilename);
  247. }
  248. };
  249. if (exportViewFrame.contentDocument && exportViewFrame.contentDocument.readyState === 'complete') {
  250. setTimeout(sendData, 50);
  251. } else {
  252. exportViewFrame.addEventListener('load', () => setTimeout(sendData, 50), { once: true });
  253. let baseSrc = exportViewFrame.getAttribute('data-base-src');
  254. if (!baseSrc) {
  255. baseSrc = exportViewFrame.src.split('?')[0];
  256. exportViewFrame.setAttribute('data-base-src', baseSrc);
  257. }
  258. exportViewFrame.src = `${baseSrc}?t=${Date.now()}`;
  259. }
  260. } else {
  261. downloadImage(pendingDownloadUrl, pendingDownloadFilename);
  262. }
  263. }
  264. }
  265. // 打开导出视图并传递图片数据
  266. function openExportView(imageUrl, fileName, options = {}) {
  267. if (!imageUrl) {
  268. return;
  269. }
  270. // 如果是跳过预览UI(直接下载),显示下载选择弹窗
  271. if (options.skipPreviewUI) {
  272. showDownloadConfirm(imageUrl, fileName);
  273. return;
  274. }
  275. if (!exportViewFrame) {
  276. downloadImage(imageUrl, fileName || 'ai-image');
  277. return;
  278. }
  279. showExportViewFrame();
  280. const sendPreviewData = () => {
  281. try {
  282. exportViewFrame.contentWindow.postMessage({
  283. type: 'show-export-preview',
  284. imageUrl: imageUrl,
  285. fileName: fileName || 'ai-image',
  286. skipPreviewUI: false
  287. }, '*');
  288. } catch (error) {
  289. console.error('[Profile] 发送导出预览数据失败:', error);
  290. hideExportViewFrame();
  291. downloadImage(imageUrl, fileName || 'ai-image');
  292. }
  293. };
  294. const handleLoad = () => {
  295. setTimeout(sendPreviewData, 50);
  296. };
  297. if (exportViewFrame.contentDocument && exportViewFrame.contentDocument.readyState === 'complete') {
  298. handleLoad();
  299. } else {
  300. exportViewFrame.addEventListener('load', handleLoad, { once: true });
  301. let baseSrc = exportViewFrame.getAttribute('data-base-src');
  302. if (!baseSrc) {
  303. baseSrc = exportViewFrame.src.split('?')[0];
  304. exportViewFrame.setAttribute('data-base-src', baseSrc);
  305. }
  306. exportViewFrame.src = `${baseSrc}?t=${Date.now()}`;
  307. }
  308. }
  309. // 加载用户信息
  310. async function loadUserInfo() {
  311. const username = getCurrentUsername();
  312. if (!username) {
  313. // 未登录,跳转到登录页
  314. window.location.href = '../../index.html';
  315. return;
  316. }
  317. currentUsername = username;
  318. // 先从localStorage读取用户信息作为备用
  319. try {
  320. const loginDataStr = localStorage.getItem('loginData');
  321. if (loginDataStr) {
  322. const loginData = JSON.parse(loginDataStr);
  323. if (loginData.user) {
  324. // 先显示localStorage中的用户信息
  325. displayUserInfo(loginData.user);
  326. }
  327. }
  328. } catch (error) {
  329. console.error('[Profile] 从localStorage读取用户信息失败:', error);
  330. }
  331. // 然后从API获取最新信息
  332. try {
  333. const response = await fetch(`/api/user/info?username=${encodeURIComponent(username)}`);
  334. if (!response.ok) {
  335. throw new Error(`HTTP error! status: ${response.status}`);
  336. }
  337. const result = await response.json();
  338. if (result.success && result.user) {
  339. currentUserInfo = result.user;
  340. displayUserInfo(result.user);
  341. } else {
  342. console.error('[Profile] API返回数据格式错误:', result);
  343. }
  344. } catch (error) {
  345. console.error('[Profile] 加载用户信息失败:', error);
  346. // 如果API调用失败,至少显示localStorage中的信息
  347. if (window.parent && window.parent.HintView) {
  348. window.parent.HintView.error('加载用户信息失败,显示缓存信息', 2000);
  349. }
  350. }
  351. }
  352. // 手机号脱敏处理
  353. function maskPhone(phone) {
  354. if (!phone || phone.length < 7) {
  355. return phone;
  356. }
  357. // 保留前3位和后4位,中间用*代替
  358. const start = phone.substring(0, 3);
  359. const end = phone.substring(phone.length - 4);
  360. const middle = '*'.repeat(phone.length - 7);
  361. return start + middle + end;
  362. }
  363. // 显示用户信息
  364. function displayUserInfo(user) {
  365. if (!user) {
  366. console.error('[Profile] 用户信息为空');
  367. return;
  368. }
  369. // 使用函数来确保元素存在
  370. const setUserInfo = () => {
  371. const avatarImage = document.getElementById('avatarImage');
  372. const usernameInput = document.getElementById('usernameInput');
  373. const phoneInput = document.getElementById('phoneInput');
  374. if (avatarImage) {
  375. let avatarUrl = user.avatar;
  376. if (avatarUrl && !avatarUrl.startsWith('http') && !avatarUrl.startsWith('data:')) {
  377. avatarUrl = 'http://localhost:3000' + (avatarUrl.startsWith('/') ? avatarUrl : '/' + avatarUrl);
  378. }
  379. avatarImage.src = avatarUrl || '../../static/default-avatar.png';
  380. avatarImage.onerror = function() {
  381. this.src = '../../static/default-avatar.png';
  382. };
  383. }
  384. if (usernameInput) {
  385. usernameInput.value = user.username || '';
  386. }
  387. if (phoneInput) {
  388. const phone = user.phone || '';
  389. phoneInput.value = maskPhone(phone);
  390. }
  391. };
  392. // 如果元素还没准备好,等待一下再试
  393. if (!document.getElementById('usernameInput') || !document.getElementById('phoneInput')) {
  394. setTimeout(setUserInfo, 100);
  395. } else {
  396. setUserInfo();
  397. }
  398. }
  399. // 加载点数
  400. async function loadPoints() {
  401. const username = getCurrentUsername();
  402. if (!username) return;
  403. try {
  404. const response = await fetch(`/api/user/points?username=${encodeURIComponent(username)}`);
  405. if (!response.ok) {
  406. throw new Error(`HTTP error! status: ${response.status}`);
  407. }
  408. const result = await response.json();
  409. if (result.success) {
  410. const pointsValue = document.getElementById('pointsValue');
  411. if (pointsValue) {
  412. const points = parseFloat(result.points || 0);
  413. pointsValue.textContent = points.toFixed(2);
  414. }
  415. }
  416. } catch (error) {
  417. console.error('[Profile] 加载点数失败:', error);
  418. }
  419. }
  420. // 处理退出登录
  421. function handleLogout() {
  422. if (confirm('确定要退出登录吗?')) {
  423. // 清除localStorage
  424. localStorage.removeItem('loginData');
  425. // 通知父窗口(如果是在iframe中)
  426. if (window.parent && window.parent !== window) {
  427. window.parent.postMessage({
  428. type: 'logout'
  429. }, '*');
  430. }
  431. // 跳转到登录页
  432. window.location.href = '../../index.html';
  433. }
  434. }
  435. // 加载AI生图历史
  436. async function loadAIHistory() {
  437. const username = getCurrentUsername();
  438. if (!username) return;
  439. const historyLoading = document.getElementById('historyLoading');
  440. const historyEmpty = document.getElementById('historyEmpty');
  441. const historyGrid = document.getElementById('historyGrid');
  442. if (historyLoading) {
  443. historyLoading.style.display = 'block';
  444. }
  445. if (historyEmpty) {
  446. historyEmpty.style.display = 'none';
  447. }
  448. if (historyGrid) {
  449. historyGrid.innerHTML = '';
  450. }
  451. try {
  452. const response = await fetch(`/api/ai/history?username=${encodeURIComponent(username)}`);
  453. if (!response.ok) {
  454. throw new Error(`HTTP error! status: ${response.status}`);
  455. }
  456. const result = await response.json();
  457. if (historyLoading) {
  458. historyLoading.style.display = 'none';
  459. }
  460. if (result.success && result.history && result.history.length > 0) {
  461. if (historyGrid) {
  462. result.history.forEach(item => {
  463. const historyItem = createHistoryItem(item);
  464. historyGrid.appendChild(historyItem);
  465. });
  466. }
  467. } else {
  468. if (historyEmpty) {
  469. historyEmpty.style.display = 'block';
  470. }
  471. }
  472. // 如果有正在处理的任务,定期刷新
  473. if (result.success && result.history) {
  474. const hasProcessing = result.history.some(item => item.status === 'rendering' || item.status === 'queued');
  475. if (hasProcessing) {
  476. setTimeout(loadAIHistory, 3000); // 3秒后刷新
  477. }
  478. }
  479. } catch (error) {
  480. console.error('[Profile] 加载AI历史失败:', error);
  481. if (historyLoading) {
  482. historyLoading.style.display = 'none';
  483. }
  484. if (historyEmpty) {
  485. historyEmpty.style.display = 'block';
  486. }
  487. }
  488. }
  489. // 创建历史记录项
  490. function createHistoryItem(item) {
  491. const div = document.createElement('div');
  492. div.className = 'history-item';
  493. div.dataset.taskId = item.id;
  494. // 格式化时间 - 显示具体年月日时分秒
  495. const formatTime = (dateString) => {
  496. if (!dateString) return '';
  497. const date = new Date(dateString);
  498. const year = date.getFullYear();
  499. const month = String(date.getMonth() + 1).padStart(2, '0');
  500. const day = String(date.getDate()).padStart(2, '0');
  501. const hours = String(date.getHours()).padStart(2, '0');
  502. const minutes = String(date.getMinutes()).padStart(2, '0');
  503. const seconds = String(date.getSeconds()).padStart(2, '0');
  504. return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
  505. };
  506. const timeText = formatTime(item.createdAt);
  507. // 预览图URL(模糊显示)
  508. const previewUrl = item.previewUrl || '';
  509. // 计算已等待时间
  510. const calculateElapsedTime = () => {
  511. const startTime = item.renderStartTime || new Date(item.createdAt).getTime();
  512. const elapsed = Date.now() - startTime;
  513. const mins = Math.floor(elapsed / 60000);
  514. const secs = Math.floor((elapsed % 60000) / 1000);
  515. return `已等待 ${mins}:${String(secs).padStart(2, '0')}`;
  516. };
  517. if (item.status === 'rendering' || item.status === 'queued') {
  518. div.classList.add(item.status);
  519. const statusText = item.status === 'rendering' ? '正在生成' : '等待中';
  520. const elapsedText = (item.status === 'rendering' || item.status === 'queued') ? calculateElapsedTime() : '';
  521. div.innerHTML = `
  522. ${previewUrl ? `<img class="history-item-preview" src="${previewUrl}" alt="预览图" onerror="this.style.display='none'">` : ''}
  523. <div class="history-item-loading-overlay">
  524. <div class="loading-spinner-container">
  525. <div class="loading-spinner"></div>
  526. </div>
  527. <div class="loading-status-text">${statusText}</div>
  528. ${elapsedText ? `<div class="loading-remaining-text">${elapsedText}</div>` : ''}
  529. </div>
  530. <div class="history-item-time">${timeText}</div>
  531. `;
  532. } else if (item.status === 'completed' && item.imageUrl) {
  533. div.classList.add('completed');
  534. div.innerHTML = `
  535. <img class="history-item-image" src="${item.imageUrl}" alt="AI生成图" onerror="this.src='../../static/default-avatar.png'">
  536. <div class="history-item-time">${timeText}</div>
  537. `;
  538. const openPreview = (e) => {
  539. e.stopPropagation();
  540. showImagePreviewModal(item.imageUrl, item.id || 'ai-image');
  541. };
  542. const imageEl = div.querySelector('.history-item-image');
  543. if (imageEl) {
  544. imageEl.addEventListener('click', openPreview);
  545. }
  546. div.addEventListener('click', openPreview);
  547. } else if (item.status === 'failed') {
  548. div.classList.add('failed');
  549. // 显示具体错误信息
  550. const errorText = item.error || '生成失败';
  551. const isTimeout = errorText.includes('超时');
  552. div.innerHTML = `
  553. <div class="failed-content">
  554. <div class="failed-icon">${isTimeout ? '⏱️' : '❌'}</div>
  555. <div class="failed-text">${errorText}</div>
  556. <button class="delete-btn" data-task-id="${item.id}">
  557. <svg width="14" height="14" viewBox="0 0 16 16" fill="none">
  558. <path d="M2 4H14M5 4V2H11V4M6 7V12M10 7V12M3 4L4 14H12L13 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
  559. </svg>
  560. 删除
  561. </button>
  562. </div>
  563. <div class="history-item-time">${timeText}</div>
  564. `;
  565. // 绑定删除按钮事件
  566. const deleteBtn = div.querySelector('.delete-btn');
  567. if (deleteBtn) {
  568. deleteBtn.addEventListener('click', async (e) => {
  569. e.stopPropagation();
  570. await deleteFailedTask(item.id);
  571. });
  572. }
  573. }
  574. return div;
  575. }
  576. // 删除失败的任务
  577. async function deleteFailedTask(taskId) {
  578. const username = getCurrentUsername();
  579. if (!username) {
  580. alert('请先登录');
  581. return;
  582. }
  583. try {
  584. const response = await fetch('/api/ai/retry', {
  585. method: 'POST',
  586. headers: {
  587. 'Content-Type': 'application/json'
  588. },
  589. body: JSON.stringify({
  590. taskId: taskId,
  591. username: username
  592. })
  593. });
  594. const result = await response.json();
  595. if (result.success) {
  596. // 刷新历史列表
  597. loadAIHistory();
  598. } else {
  599. alert('删除失败: ' + (result.error || '未知错误'));
  600. }
  601. } catch (error) {
  602. console.error('[Profile] 删除失败:', error);
  603. alert('删除失败: ' + error.message);
  604. }
  605. }
  606. // 下载图片(直接下载PNG)
  607. function downloadImage(url, filename) {
  608. const a = document.createElement('a');
  609. a.href = url;
  610. a.download = `${filename}.png`;
  611. document.body.appendChild(a);
  612. a.click();
  613. document.body.removeChild(a);
  614. }
  615. // 显示图片预览弹窗
  616. function showImagePreviewModal(imageUrl, filename) {
  617. const modal = document.createElement('div');
  618. modal.className = 'image-preview-modal';
  619. modal.innerHTML = `
  620. <div class="image-preview-backdrop"></div>
  621. <div class="image-preview-content">
  622. <img src="${imageUrl}" alt="预览图">
  623. <button class="image-preview-close">×</button>
  624. <button class="image-preview-download-btn" title="下载">
  625. <svg width="24" height="24" viewBox="0 0 16 16" fill="none">
  626. <path d="M8 11V3M8 11L5 8M8 11L11 8M3 13H13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
  627. </svg>
  628. </button>
  629. </div>
  630. `;
  631. document.body.appendChild(modal);
  632. requestAnimationFrame(() => {
  633. modal.classList.add('show');
  634. });
  635. const closeModal = () => {
  636. modal.classList.remove('show');
  637. setTimeout(() => {
  638. if (modal.parentNode) modal.parentNode.removeChild(modal);
  639. }, 300);
  640. };
  641. modal.querySelector('.image-preview-backdrop').onclick = closeModal;
  642. modal.querySelector('.image-preview-close').onclick = closeModal;
  643. modal.querySelector('.image-preview-download-btn').onclick = (e) => {
  644. e.stopPropagation();
  645. closeModal();
  646. openExportView(imageUrl, filename, { skipPreviewUI: true });
  647. };
  648. document.addEventListener('keydown', function escHandler(e) {
  649. if (e.key === 'Escape') {
  650. closeModal();
  651. document.removeEventListener('keydown', escHandler);
  652. }
  653. });
  654. }
  655. // 加载购买记录
  656. async function loadPurchaseHistory() {
  657. const username = getCurrentUsername();
  658. if (!username) return;
  659. const purchaseLoading = document.getElementById('purchaseLoading');
  660. const purchaseEmpty = document.getElementById('purchaseEmpty');
  661. const purchaseGrid = document.getElementById('purchaseGrid');
  662. if (purchaseLoading) {
  663. purchaseLoading.style.display = 'block';
  664. }
  665. if (purchaseEmpty) {
  666. purchaseEmpty.style.display = 'none';
  667. }
  668. if (purchaseGrid) {
  669. purchaseGrid.innerHTML = '';
  670. }
  671. try {
  672. const response = await fetch(`/api/pay/purchase-history?username=${encodeURIComponent(username)}`);
  673. if (!response.ok) {
  674. throw new Error(`HTTP error! status: ${response.status}`);
  675. }
  676. const result = await response.json();
  677. if (purchaseLoading) {
  678. purchaseLoading.style.display = 'none';
  679. }
  680. if (result.success && result.history && result.history.length > 0) {
  681. if (purchaseGrid) {
  682. result.history.forEach(item => {
  683. const purchaseItem = createPurchaseItem(item);
  684. purchaseGrid.appendChild(purchaseItem);
  685. });
  686. }
  687. } else {
  688. if (purchaseEmpty) {
  689. purchaseEmpty.style.display = 'block';
  690. }
  691. }
  692. } catch (error) {
  693. console.error('[Profile] 加载购买记录失败:', error);
  694. if (purchaseLoading) {
  695. purchaseLoading.style.display = 'none';
  696. }
  697. if (purchaseEmpty) {
  698. purchaseEmpty.style.display = 'block';
  699. }
  700. }
  701. }
  702. // 创建购买记录项
  703. function createPurchaseItem(item) {
  704. const div = document.createElement('div');
  705. div.className = 'purchase-item';
  706. if (item.deleted) {
  707. div.classList.add('deleted');
  708. }
  709. const previewHtml = item.previewUrl
  710. ? `<img class="purchase-item-image" src="${item.previewUrl}" alt="${item.name}" onerror="this.src='../../static/default-avatar.png'">`
  711. : `<div class="purchase-item-placeholder">
  712. <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  713. <path d="M14 2H6C5.46957 2 4.96086 2.21071 4.58579 2.58579C4.21071 2.96086 4 3.46957 4 4V20C4 20.5304 4.21071 21.0391 4.58579 21.4142C4.96086 21.7893 5.46957 22 6 22H18C18.5304 22 19.0391 21.7893 19.4142 21.4142C19.7893 21.0391 20 20.5304 20 20V8L14 2Z"/>
  714. </svg>
  715. </div>`;
  716. const deletedBadge = item.deleted ? '<div class="purchase-item-deleted-badge">已删除</div>' : '';
  717. const priceText = item.points > 0 ? `<div class="purchase-item-price">${item.points} Ani币</div>` : '';
  718. // 格式化时间(购买记录暂时没有时间,可以添加)
  719. const formatTime = (dateString) => {
  720. if (!dateString) return '';
  721. const date = new Date(dateString);
  722. const now = new Date();
  723. const diff = now - date;
  724. const minutes = Math.floor(diff / 60000);
  725. const hours = Math.floor(diff / 3600000);
  726. const days = Math.floor(diff / 86400000);
  727. if (minutes < 1) return '刚刚';
  728. if (minutes < 60) return `${minutes}分钟前`;
  729. if (hours < 24) return `${hours}小时前`;
  730. if (days < 7) return `${days}天前`;
  731. return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
  732. };
  733. const timeText = item.purchasedAt ? formatTime(item.purchasedAt) : '';
  734. div.innerHTML = `
  735. ${previewHtml}
  736. <div class="purchase-item-info">
  737. <div class="purchase-item-name">${item.name}</div>
  738. <div class="purchase-item-category">${item.category}</div>
  739. ${priceText}
  740. </div>
  741. <div class="purchase-item-overlay">
  742. <button class="purchase-item-add-btn" data-path="${item.path}" data-category="${item.category}" data-name="${item.name}">
  743. <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2">
  744. <path d="M8 3V13M3 8H13" stroke-linecap="round"/>
  745. </svg>
  746. 添加
  747. </button>
  748. </div>
  749. ${timeText ? `<div class="purchase-item-time">${timeText}</div>` : ''}
  750. ${deletedBadge}
  751. `;
  752. // 绑定添加按钮事件
  753. const addBtn = div.querySelector('.purchase-item-add-btn');
  754. if (addBtn) {
  755. addBtn.addEventListener('click', async (e) => {
  756. e.stopPropagation();
  757. const resourcePath = addBtn.dataset.path;
  758. const categoryDir = addBtn.dataset.category;
  759. const itemName = addBtn.dataset.name;
  760. await handleAddToDisk(resourcePath, categoryDir, itemName, addBtn);
  761. });
  762. }
  763. return div;
  764. }
  765. // 处理添加到网盘
  766. async function handleAddToDisk(resourcePath, categoryDir, itemName, buttonEl) {
  767. const username = getCurrentUsername();
  768. if (!username) {
  769. if (window.parent && window.parent.HintView) {
  770. window.parent.HintView.error('请先登录', 2000);
  771. }
  772. return;
  773. }
  774. // 禁用按钮
  775. if (buttonEl) {
  776. buttonEl.disabled = true;
  777. buttonEl.textContent = '添加中...';
  778. }
  779. try {
  780. const response = await fetch('/api/pay/purchase', {
  781. method: 'POST',
  782. headers: {
  783. 'Content-Type': 'application/json'
  784. },
  785. body: JSON.stringify({
  786. username: username,
  787. resourcePath: resourcePath,
  788. categoryDir: categoryDir,
  789. itemName: itemName,
  790. points: 0 // 已购买,不需要扣除点数
  791. })
  792. });
  793. const result = await response.json();
  794. if (result.success) {
  795. if (window.parent && window.parent.HintView) {
  796. window.parent.HintView.success('添加成功!文件已添加到网盘', 2000);
  797. }
  798. // 通知父窗口刷新网盘
  799. if (window.parent && window.parent.postMessage) {
  800. window.parent.postMessage({ type: 'refresh-disk' }, '*');
  801. }
  802. } else {
  803. throw new Error(result.message || '添加失败');
  804. }
  805. } catch (error) {
  806. console.error('[Profile] 添加到网盘失败:', error);
  807. if (window.parent && window.parent.HintView) {
  808. window.parent.HintView.error(error.message || '添加失败,请稍后重试', 2000);
  809. }
  810. } finally {
  811. // 恢复按钮状态
  812. if (buttonEl) {
  813. buttonEl.disabled = false;
  814. buttonEl.innerHTML = `
  815. <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2">
  816. <path d="M8 3V13M3 8H13" stroke-linecap="round"/>
  817. </svg>
  818. 添加
  819. `;
  820. }
  821. }
  822. }
  823. // 获取当前登录用户名
  824. function getCurrentUsername() {
  825. try {
  826. const loginDataStr = localStorage.getItem('loginData');
  827. if (!loginDataStr) {
  828. return null;
  829. }
  830. const loginData = JSON.parse(loginDataStr);
  831. const now = Date.now();
  832. if (now >= loginData.expireTime) {
  833. localStorage.removeItem('loginData');
  834. return null;
  835. }
  836. return loginData.user ? loginData.user.username : null;
  837. } catch (error) {
  838. console.error('[Profile] 获取用户名失败:', error);
  839. return null;
  840. }
  841. }
  842. // 初始化
  843. if (document.readyState === 'loading') {
  844. document.addEventListener('DOMContentLoaded', init);
  845. } else {
  846. init();
  847. }
  848. })();