profile.js 27 KB

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