store.js 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001
  1. // 商店页面主逻辑
  2. // 负责资源加载、搜索、分类筛选等功能
  3. (function () {
  4. 'use strict';
  5. class StoreView {
  6. constructor() {
  7. this.resources = [];
  8. this.filteredResources = [];
  9. this.currentCategory = '';
  10. this.searchQuery = '';
  11. this.itemTemplate = null;
  12. this.currentFps = 8;
  13. this.frameUrls = [];
  14. this.currentFrame = 0;
  15. this.itemAnimations = new Map(); // 存储每个 item 的动画数据
  16. this.isUserLoggedIn = false; // 本地登录状态缓存
  17. this.init();
  18. }
  19. async init() {
  20. // 加载 item 模板
  21. await this.loadItemTemplate();
  22. // 绑定事件
  23. this.bindEvents();
  24. // 加载分类
  25. await this.loadCategories();
  26. // 加载资源
  27. await this.loadResources();
  28. }
  29. async loadItemTemplate() {
  30. try {
  31. // 使用相对于当前页面的路径
  32. // store.html 位于 page/store/store.html,item.html 位于同一目录
  33. const response = await fetch('./item.html');
  34. if (!response.ok) {
  35. throw new Error(`HTTP error! status: ${response.status}`);
  36. }
  37. const html = await response.text();
  38. this.itemTemplate = html;
  39. } catch (error) {
  40. console.error('[StoreView] 加载模板失败:', error);
  41. // 如果相对路径失败,尝试绝对路径
  42. try {
  43. const response = await fetch('/page/store/item.html');
  44. if (response.ok) {
  45. const html = await response.text();
  46. this.itemTemplate = html;
  47. } else {
  48. throw new Error('绝对路径也失败');
  49. }
  50. } catch (fallbackError) {
  51. console.error('[StoreView] 备用路径也失败:', fallbackError);
  52. this.itemTemplate = '<div class="store-item">加载失败</div>';
  53. }
  54. }
  55. }
  56. async loadCategories() {
  57. try {
  58. const response = await fetch('http://localhost:3000/api/store/categories');
  59. if (!response.ok) {
  60. throw new Error(`HTTP error! status: ${response.status}`);
  61. }
  62. const data = await response.json();
  63. if (data.success && data.categories) {
  64. const categoryBar = document.getElementById('categoryBar');
  65. if (categoryBar) {
  66. // 保留"全部"按钮,添加其他分类
  67. const allButton = categoryBar.querySelector('.category-item[data-category=""]');
  68. categoryBar.innerHTML = '';
  69. if (allButton) {
  70. categoryBar.appendChild(allButton);
  71. }
  72. // 添加动态分类按钮
  73. data.categories.forEach(category => {
  74. const button = document.createElement('button');
  75. button.className = 'category-item';
  76. button.dataset.category = category.name; // 使用文件夹名称作为分类名称
  77. button.textContent = category.name;
  78. categoryBar.appendChild(button);
  79. });
  80. }
  81. }
  82. } catch (error) {
  83. console.error('[StoreView] 加载分类失败:', error);
  84. // 如果加载失败,保持默认的"全部"按钮
  85. }
  86. }
  87. bindEvents() {
  88. // 监听登录状态变化
  89. window.addEventListener('message', (event) => {
  90. if (event.data && event.data.type === 'login-success' && event.data.user) {
  91. this.isUserLoggedIn = true;
  92. } else if (event.data && event.data.type === 'logout') {
  93. this.isUserLoggedIn = false;
  94. }
  95. });
  96. // 搜索栏
  97. const searchInput = document.getElementById('searchInput');
  98. const searchButton = document.getElementById('searchButton');
  99. if (searchInput) {
  100. searchInput.addEventListener('input', (e) => {
  101. this.searchQuery = e.target.value.trim();
  102. this.filterResources();
  103. });
  104. searchInput.addEventListener('keypress', (e) => {
  105. if (e.key === 'Enter') {
  106. this.filterResources();
  107. }
  108. });
  109. }
  110. if (searchButton) {
  111. searchButton.addEventListener('click', () => {
  112. this.filterResources();
  113. });
  114. }
  115. // 分类栏
  116. const categoryItems = document.querySelectorAll('.category-item');
  117. categoryItems.forEach(item => {
  118. item.addEventListener('click', () => {
  119. // 移除所有 active 类
  120. categoryItems.forEach(i => i.classList.remove('active'));
  121. // 添加 active 类到当前项
  122. item.classList.add('active');
  123. // 更新当前分类
  124. this.currentCategory = item.dataset.category || '';
  125. // 重新加载资源
  126. this.loadResources();
  127. });
  128. });
  129. // 预览弹窗关闭
  130. const previewClose = document.getElementById('previewClose');
  131. const previewModal = document.getElementById('previewModal');
  132. if (previewClose) {
  133. previewClose.addEventListener('click', () => {
  134. this.stopAnimation();
  135. if (previewModal) {
  136. previewModal.style.display = 'none';
  137. }
  138. });
  139. }
  140. if (previewModal) {
  141. previewModal.addEventListener('click', (e) => {
  142. if (e.target === previewModal) {
  143. this.stopAnimation();
  144. previewModal.style.display = 'none';
  145. }
  146. });
  147. }
  148. // 帧率滑块
  149. const fpsSlider = document.getElementById('fpsSlider');
  150. const fpsDisplay = document.getElementById('fpsDisplay');
  151. if (fpsSlider && fpsDisplay) {
  152. fpsSlider.addEventListener('input', (e) => {
  153. const fps = parseInt(e.target.value);
  154. this.currentFps = fps;
  155. fpsDisplay.textContent = `${fps} FPS`;
  156. // 如果动画正在播放,重新启动以应用新帧率
  157. if (this.animationInterval && this.frameUrls.length > 0) {
  158. const previewImage = document.getElementById('previewAnimationImage');
  159. if (previewImage) {
  160. this.stopAnimation();
  161. this.startAnimation(previewImage, this.frameUrls);
  162. }
  163. }
  164. });
  165. }
  166. // 使用事件委托处理动态添加的按钮
  167. const resourcesGrid = document.getElementById('resourcesGrid');
  168. if (resourcesGrid) {
  169. // 购买按钮点击
  170. resourcesGrid.addEventListener('click', (e) => {
  171. console.log('[StoreView] 点击事件触发,目标:', e.target);
  172. if (e.target.closest('.item-buy-button')) {
  173. const button = e.target.closest('.item-buy-button');
  174. const path = button.dataset.resourcePath;
  175. console.log('[StoreView] 点击购买按钮,路径:', path);
  176. // 检查是否已登录
  177. if (!this.isLoggedIn()) {
  178. console.log('[StoreView] 未登录,跳转登录页');
  179. // 显示登录界面
  180. if (window.parent !== window) {
  181. window.parent.postMessage({
  182. type: 'navigation',
  183. page: 'login'
  184. }, '*');
  185. }
  186. return;
  187. }
  188. console.log('[StoreView] 已登录,调用 handleBuy');
  189. this.handleBuy(path);
  190. }
  191. });
  192. }
  193. }
  194. async loadResources() {
  195. this.showLoading(true);
  196. this.hideEmpty();
  197. try {
  198. const params = new URLSearchParams();
  199. if (this.currentCategory) {
  200. params.append('category', this.currentCategory);
  201. }
  202. if (this.searchQuery) {
  203. params.append('search', this.searchQuery);
  204. }
  205. const response = await fetch(`http://localhost:3000/api/store/resources?${params.toString()}`);
  206. if (!response.ok) {
  207. throw new Error(`HTTP error! status: ${response.status}`);
  208. }
  209. const data = await response.json();
  210. if (data.success) {
  211. this.resources = data.resources || [];
  212. // 使用服务器返回的价格,如果没有则默认为0
  213. this.resources.forEach(resource => {
  214. if (resource.points === undefined || resource.points === null) {
  215. resource.points = 0;
  216. }
  217. });
  218. this.filteredResources = this.resources;
  219. this.renderResources();
  220. } else {
  221. throw new Error(data.error || '加载资源失败');
  222. }
  223. } catch (error) {
  224. console.error('[StoreView] 加载资源失败:', error);
  225. this.showGlobalAlert('加载资源失败: ' + error.message);
  226. this.resources = [];
  227. this.filteredResources = [];
  228. this.renderResources();
  229. } finally {
  230. this.showLoading(false);
  231. }
  232. }
  233. filterResources() {
  234. // 重新加载资源(服务器端筛选)
  235. this.loadResources();
  236. }
  237. renderResources() {
  238. const grid = document.getElementById('resourcesGrid');
  239. if (!grid) return;
  240. if (this.filteredResources.length === 0) {
  241. grid.innerHTML = '';
  242. this.showEmpty();
  243. return;
  244. }
  245. this.hideEmpty();
  246. // 计算列数
  247. const columnWidth = 200;
  248. const gap = 16;
  249. const containerWidth = grid.offsetWidth || grid.clientWidth || window.innerWidth;
  250. const columnCount = Math.max(1, Math.floor((containerWidth + gap) / (columnWidth + gap)));
  251. // 创建列容器
  252. grid.innerHTML = '';
  253. const masonryContainer = document.createElement('div');
  254. masonryContainer.className = 'resources-grid-masonry';
  255. const columns = [];
  256. const columnHeights = [];
  257. for (let i = 0; i < columnCount; i++) {
  258. const column = document.createElement('div');
  259. column.className = 'masonry-column';
  260. masonryContainer.appendChild(column);
  261. columns.push(column);
  262. columnHeights.push(0);
  263. }
  264. grid.appendChild(masonryContainer);
  265. // 创建临时容器来测量项目高度
  266. const tempContainer = document.createElement('div');
  267. tempContainer.style.position = 'absolute';
  268. tempContainer.style.visibility = 'hidden';
  269. tempContainer.style.width = `${columnWidth}px`;
  270. tempContainer.style.top = '-9999px';
  271. tempContainer.style.left = '-9999px';
  272. document.body.appendChild(tempContainer);
  273. // 渲染所有项目到临时容器并测量
  274. const itemsData = this.filteredResources.map(resource => {
  275. const itemHtml = this.renderItem(resource);
  276. tempContainer.innerHTML = itemHtml;
  277. const itemElement = tempContainer.querySelector('.store-item');
  278. const height = itemElement ? itemElement.offsetHeight : 0;
  279. return { html: itemHtml, height };
  280. });
  281. // 将项目分配到最短的列
  282. itemsData.forEach(({ html, height }) => {
  283. // 找到最短的列
  284. const shortestColumnIndex = columnHeights.indexOf(Math.min(...columnHeights));
  285. // 添加到最短的列
  286. columns[shortestColumnIndex].insertAdjacentHTML('beforeend', html);
  287. // 更新列高度
  288. columnHeights[shortestColumnIndex] += height + gap;
  289. });
  290. // 清理临时容器
  291. document.body.removeChild(tempContainer);
  292. // 为每个 item 绑定鼠标事件和 FPS 控制
  293. const allItems = masonryContainer.querySelectorAll('.store-item');
  294. allItems.forEach(item => {
  295. const category = item.dataset.category;
  296. const folder = item.dataset.folder;
  297. const previewImage = item.querySelector('.item-preview-image');
  298. const fpsSlider = item.querySelector('.item-fps-slider');
  299. const fpsDisplay = item.querySelector('.item-fps-display');
  300. if (category && folder && previewImage) {
  301. // 存储当前 FPS(每个 item 独立)
  302. let currentFps = 8;
  303. // FPS 滑块事件
  304. if (fpsSlider && fpsDisplay) {
  305. fpsSlider.addEventListener('input', (e) => {
  306. const fps = parseInt(e.target.value);
  307. currentFps = fps;
  308. fpsDisplay.textContent = `${fps} FPS`;
  309. // 如果动画正在播放,重新启动以应用新帧率
  310. if (previewImage._animationInterval) {
  311. this.stopItemAnimation(previewImage);
  312. this.startItemAnimation(previewImage, category, folder, fps);
  313. }
  314. });
  315. // 阻止事件冒泡,避免触发 item 的 mouseleave
  316. fpsSlider.addEventListener('mousedown', (e) => {
  317. e.stopPropagation();
  318. });
  319. }
  320. // 鼠标进入时播放动画
  321. item.addEventListener('mouseenter', () => {
  322. this.startItemAnimation(previewImage, category, folder, currentFps);
  323. });
  324. // 鼠标离开时停止动画
  325. item.addEventListener('mouseleave', () => {
  326. this.stopItemAnimation(previewImage);
  327. });
  328. }
  329. });
  330. // 使用 resize observer 监听窗口大小变化
  331. if (!this.resizeObserver) {
  332. this.resizeObserver = new ResizeObserver(() => {
  333. this.renderResources();
  334. });
  335. this.resizeObserver.observe(grid);
  336. }
  337. // 检查每个资源是否已存在(如果已登录),只更新按钮状态,不重新渲染
  338. if (this.isLoggedIn()) {
  339. this.checkResourcesOwnership(this.filteredResources).then(() => {
  340. // 只更新按钮状态,不重新渲染整个列表
  341. this.updateButtonStates();
  342. });
  343. }
  344. }
  345. // 更新单个资源的按钮状态
  346. updateSingleButtonState(resourcePath) {
  347. const item = document.querySelector(`[data-resource-path="${CSS.escape(resourcePath)}"]`);
  348. if (!item) {
  349. console.warn('[StoreView] 未找到资源项:', resourcePath);
  350. return;
  351. }
  352. const resource = this.resources.find(r => r.path === resourcePath);
  353. if (!resource) {
  354. console.warn('[StoreView] 未找到资源对象:', resourcePath);
  355. return;
  356. }
  357. const button = item.querySelector('.item-buy-button');
  358. const priceEl = item.querySelector('.item-price');
  359. if (button) {
  360. const isFree = resource.points === 0;
  361. const isOwned = resource.isOwned || false;
  362. if (isFree) {
  363. // 免费资源:显示"免费",按钮"添加"
  364. button.textContent = '添加';
  365. button.classList.remove('item-button-added');
  366. if (priceEl) priceEl.textContent = '免费';
  367. } else if (isOwned) {
  368. // 付费且已购买:显示价格,按钮"添加"(绿色)
  369. button.textContent = '添加';
  370. button.classList.add('item-button-added');
  371. if (priceEl) priceEl.textContent = `${resource.points} Ani币`;
  372. } else {
  373. // 付费且未购买:显示价格,按钮"购买"
  374. button.textContent = '购买';
  375. button.classList.remove('item-button-added');
  376. if (priceEl) priceEl.textContent = `${resource.points} Ani币`;
  377. }
  378. console.log('[StoreView] 单个按钮状态已更新:', resourcePath, 'isOwned:', isOwned);
  379. }
  380. }
  381. // 更新按钮状态(不重新渲染整个列表)
  382. updateButtonStates() {
  383. console.log('[StoreView] updateButtonStates 开始,资源列表:', this.resources.map(r => ({path: r.path, isOwned: r.isOwned, points: r.points})));
  384. const allItems = document.querySelectorAll('.store-item');
  385. console.log('[StoreView] 找到', allItems.length, '个 store-item 元素');
  386. allItems.forEach(item => {
  387. const path = item.dataset.resourcePath;
  388. const resource = this.resources.find(r => r.path === path);
  389. if (resource) {
  390. const button = item.querySelector('.item-buy-button');
  391. const priceEl = item.querySelector('.item-price');
  392. if (button) {
  393. const isFree = resource.points === 0;
  394. const isOwned = resource.isOwned || false;
  395. if (isFree) {
  396. // 免费资源:显示"免费",按钮"添加"
  397. button.textContent = '添加';
  398. button.classList.remove('item-button-added');
  399. if (priceEl) priceEl.textContent = '免费';
  400. } else if (isOwned) {
  401. // 付费且已购买:显示价格,按钮"添加"(绿色)
  402. button.textContent = '添加';
  403. button.classList.add('item-button-added');
  404. if (priceEl) priceEl.textContent = `${resource.points} Ani币`;
  405. } else {
  406. // 付费且未购买:显示价格,按钮"购买"
  407. button.textContent = '购买';
  408. button.classList.remove('item-button-added');
  409. if (priceEl) priceEl.textContent = `${resource.points} Ani币`;
  410. }
  411. }
  412. }
  413. });
  414. }
  415. renderItem(resource) {
  416. if (!this.itemTemplate) {
  417. return '<div class="store-item">模板未加载</div>';
  418. }
  419. // 使用已保存的点数(在加载资源时已生成)
  420. const points = resource.points !== undefined ? resource.points : 0;
  421. // 根据点数和资源状态设置按钮文字和类
  422. const isFree = points === 0;
  423. const isOwned = resource.isOwned || false;
  424. let buttonText = '添加';
  425. let buttonClass = '';
  426. let priceText = points === 0 ? '免费' : `${points} Ani币`;
  427. if (isFree) {
  428. // 免费资源:显示"免费",按钮"添加"
  429. buttonText = '添加';
  430. buttonClass = '';
  431. } else if (isOwned) {
  432. // 付费且已购买:显示价格,按钮"添加"(绿色)
  433. buttonText = '添加';
  434. buttonClass = 'item-button-added';
  435. } else {
  436. // 付费且未购买:显示价格,按钮"购买"
  437. buttonText = '购买';
  438. buttonClass = '';
  439. }
  440. // 先替换data-points中的{{points}}为原始points值
  441. let html = this.itemTemplate
  442. .replace(/data-points="\{\{points\}\}"/g, `data-points="${points}"`)
  443. .replace(/\{\{name\}\}/g, this.escapeHtml(resource.name))
  444. .replace(/\{\{category\}\}/g, this.escapeHtml(resource.category))
  445. .replace(/\{\{categoryDir\}\}/g, this.escapeHtml(resource.categoryDir))
  446. .replace(/\{\{previewUrl\}\}/g, resource.previewUrl || '')
  447. .replace(/\{\{frameCount\}\}/g, resource.frameCount || 0)
  448. .replace(/\{\{path\}\}/g, this.escapeHtml(resource.path))
  449. .replace(/\{\{buttonText\}\}/g, buttonText)
  450. .replace(/\{\{buttonClass\}\}/g, buttonClass);
  451. // 最后替换价格显示中的{{points}}为priceText
  452. html = html.replace(/\{\{points\}\}/g, priceText);
  453. return html;
  454. }
  455. escapeHtml(text) {
  456. const div = document.createElement('div');
  457. div.textContent = text;
  458. return div.innerHTML;
  459. }
  460. async playAnimation(category, folder) {
  461. const previewModal = document.getElementById('previewModal');
  462. const previewImage = document.getElementById('previewAnimationImage');
  463. const previewTitle = document.getElementById('previewTitle');
  464. if (!previewModal || !previewImage) return;
  465. // 显示弹窗
  466. previewModal.style.display = 'flex';
  467. if (previewTitle) {
  468. previewTitle.textContent = `动画预览: ${folder}`;
  469. }
  470. // 加载帧列表
  471. try {
  472. const response = await fetch(
  473. `http://localhost:3000/api/store/frames?category=${encodeURIComponent(category)}&folder=${encodeURIComponent(folder)}`
  474. );
  475. if (!response.ok) {
  476. throw new Error(`HTTP error! status: ${response.status}`);
  477. }
  478. const data = await response.json();
  479. if (data.success && data.frameUrls && data.frameUrls.length > 0) {
  480. // 保存帧URLs
  481. this.frameUrls = data.frameUrls;
  482. // 开始播放动画
  483. this.startAnimation(previewImage, data.frameUrls);
  484. } else {
  485. throw new Error('没有可用的帧');
  486. }
  487. } catch (error) {
  488. console.error('[StoreView] 播放动画失败:', error);
  489. this.showGlobalAlert('播放动画失败: ' + error.message);
  490. previewModal.style.display = 'none';
  491. }
  492. }
  493. startAnimation(imgElement, frameUrls) {
  494. // 停止之前的动画
  495. this.stopAnimation();
  496. this.currentFrame = 0;
  497. const fps = this.currentFps;
  498. const interval = 1000 / fps;
  499. // 预加载所有帧
  500. const images = [];
  501. let loadedCount = 0;
  502. frameUrls.forEach((url, index) => {
  503. const img = new Image();
  504. img.onload = () => {
  505. loadedCount++;
  506. if (loadedCount === frameUrls.length) {
  507. // 所有帧加载完成,开始播放
  508. this.animationInterval = setInterval(() => {
  509. this.currentFrame = (this.currentFrame + 1) % frameUrls.length;
  510. imgElement.src = frameUrls[this.currentFrame];
  511. }, interval);
  512. }
  513. };
  514. img.onerror = () => {
  515. loadedCount++;
  516. if (loadedCount === frameUrls.length) {
  517. this.animationInterval = setInterval(() => {
  518. this.currentFrame = (this.currentFrame + 1) % frameUrls.length;
  519. imgElement.src = frameUrls[this.currentFrame];
  520. }, interval);
  521. }
  522. };
  523. img.src = url;
  524. images.push(img);
  525. });
  526. // 立即显示第一帧
  527. if (frameUrls.length > 0) {
  528. this.currentFrame = 0;
  529. imgElement.src = frameUrls[0];
  530. }
  531. }
  532. stopAnimation() {
  533. if (this.animationInterval) {
  534. clearInterval(this.animationInterval);
  535. this.animationInterval = null;
  536. }
  537. }
  538. async startItemAnimation(imgElement, category, folder, fps = 8) {
  539. // 如果已经有动画在播放,先停止
  540. this.stopItemAnimation(imgElement);
  541. // 检查是否已缓存帧数据
  542. const cacheKey = `${category}/${folder}`;
  543. let frameUrls = this.itemAnimations.get(cacheKey);
  544. if (!frameUrls) {
  545. // 加载帧列表
  546. try {
  547. const response = await fetch(
  548. `http://localhost:3000/api/store/frames?category=${encodeURIComponent(category)}&folder=${encodeURIComponent(folder)}`
  549. );
  550. if (!response.ok) {
  551. throw new Error(`HTTP error! status: ${response.status}`);
  552. }
  553. const data = await response.json();
  554. if (data.success && data.frameUrls && data.frameUrls.length > 0) {
  555. frameUrls = data.frameUrls;
  556. this.itemAnimations.set(cacheKey, frameUrls);
  557. } else {
  558. return; // 没有可用的帧
  559. }
  560. } catch (error) {
  561. console.error('[StoreView] 加载帧失败:', error);
  562. return;
  563. }
  564. }
  565. // 开始播放动画
  566. let currentFrame = 0;
  567. const interval = 1000 / fps;
  568. // 立即显示第一帧
  569. if (frameUrls.length > 0) {
  570. imgElement.src = frameUrls[0];
  571. }
  572. // 存储动画 interval
  573. const animationInterval = setInterval(() => {
  574. currentFrame = (currentFrame + 1) % frameUrls.length;
  575. imgElement.src = frameUrls[currentFrame];
  576. }, interval);
  577. // 将 interval 和 fps 存储到 imgElement 上
  578. imgElement._animationInterval = animationInterval;
  579. imgElement._currentFps = fps;
  580. }
  581. stopItemAnimation(imgElement) {
  582. if (imgElement._animationInterval) {
  583. clearInterval(imgElement._animationInterval);
  584. imgElement._animationInterval = null;
  585. }
  586. }
  587. isLoggedIn() {
  588. // 首先检查本地缓存
  589. if (this.isUserLoggedIn) {
  590. return true;
  591. }
  592. // 如果本地缓存为 false,尝试从导航栏检查(作为备用方案)
  593. try {
  594. const navigationFrame = window.parent.document.getElementById('navigationFrame');
  595. if (navigationFrame && navigationFrame.contentWindow) {
  596. const navDoc = navigationFrame.contentDocument || navigationFrame.contentWindow.document;
  597. const userAvatarContainer = navDoc.getElementById('userAvatarContainer');
  598. // 如果用户头像容器存在且显示,说明已登录
  599. if (userAvatarContainer) {
  600. const computedStyle = navDoc.defaultView.getComputedStyle(userAvatarContainer);
  601. const isLoggedIn = computedStyle.display !== 'none';
  602. // 更新本地缓存
  603. this.isUserLoggedIn = isLoggedIn;
  604. return isLoggedIn;
  605. }
  606. }
  607. } catch (error) {
  608. // 跨域或无法访问时,使用本地缓存
  609. console.warn('[StoreView] 无法检查登录状态,使用本地缓存:', error);
  610. }
  611. return this.isUserLoggedIn;
  612. }
  613. // 检查资源是否已存在
  614. async checkResourcesOwnership(resources) {
  615. if (!this.isLoggedIn()) {
  616. return;
  617. }
  618. const username = this.getCurrentUsername();
  619. if (!username) {
  620. return;
  621. }
  622. // 批量检查资源
  623. const checkPromises = resources.map(async (resource) => {
  624. try {
  625. const response = await fetch(`/api/pay/check-resource?username=${encodeURIComponent(username)}&resourcePath=${encodeURIComponent(resource.path)}`);
  626. const result = await response.json();
  627. if (result.success) {
  628. resource.isOwned = result.exists;
  629. }
  630. } catch (error) {
  631. console.error(`[StoreView] 检查资源 ${resource.path} 失败:`, error);
  632. }
  633. });
  634. await Promise.all(checkPromises);
  635. }
  636. getCurrentUsername() {
  637. try {
  638. const loginDataStr = localStorage.getItem('loginData');
  639. if (!loginDataStr) {
  640. return null;
  641. }
  642. const loginData = JSON.parse(loginDataStr);
  643. const now = Date.now();
  644. // 检查是否过期
  645. if (now >= loginData.expireTime) {
  646. localStorage.removeItem('loginData');
  647. return null;
  648. }
  649. return loginData.user ? loginData.user.username : null;
  650. } catch (error) {
  651. console.error('[StoreView] 获取用户名失败:', error);
  652. return null;
  653. }
  654. }
  655. async handleBuy(path) {
  656. console.log('[StoreView] handleBuy 被调用,path:', path);
  657. // 获取资源信息
  658. const resource = this.resources.find(r => r.path === path);
  659. console.log('[StoreView] 找到资源:', resource);
  660. if (!resource) {
  661. this.showGlobalAlert('资源不存在');
  662. return;
  663. }
  664. // 获取点数(从渲染的 item 中获取,或使用资源的点数)
  665. const itemElement = document.querySelector(`[data-resource-path="${path}"]`);
  666. let points = resource.points;
  667. if (itemElement) {
  668. const pointsEl = itemElement.querySelector('.item-price');
  669. if (pointsEl) {
  670. const pointsText = pointsEl.textContent.replace('Ani币', '').trim();
  671. points = parseInt(pointsText) || points;
  672. }
  673. const dataPoints = itemElement.querySelector('.item-buy-button')?.dataset.points;
  674. if (dataPoints) {
  675. points = parseInt(dataPoints) || points;
  676. }
  677. }
  678. // 检查是否登录
  679. const username = this.getCurrentUsername();
  680. if (!username) {
  681. if (window.parent && window.parent !== window) {
  682. window.parent.postMessage({
  683. type: 'navigation',
  684. page: 'login'
  685. }, '*');
  686. }
  687. return;
  688. }
  689. // 如果是0点,直接添加
  690. if (points === 0) {
  691. try {
  692. const response = await fetch('/api/pay/purchase', {
  693. method: 'POST',
  694. headers: {
  695. 'Content-Type': 'application/json'
  696. },
  697. body: JSON.stringify({
  698. username: username,
  699. resourcePath: resource.path,
  700. categoryDir: resource.categoryDir,
  701. itemName: resource.name,
  702. points: 0
  703. })
  704. });
  705. const result = await response.json();
  706. if (result.success) {
  707. // 标记为已拥有
  708. resource.isOwned = true;
  709. // 立即更新单个按钮状态
  710. this.updateSingleButtonState(resource.path);
  711. // 同时更新所有按钮状态(确保一致性)
  712. this.updateButtonStates();
  713. if (window.parent && window.parent.HintView) {
  714. window.parent.HintView.success('添加成功!文件已添加到网盘', 3000);
  715. }
  716. // 通知父窗口刷新网盘
  717. if (window.parent && window.parent !== window) {
  718. window.parent.postMessage({ type: 'refresh-disk' }, '*');
  719. }
  720. } else {
  721. if (window.parent && window.parent.HintView) {
  722. window.parent.HintView.error(result.message || '添加失败', 3000);
  723. }
  724. }
  725. } catch (error) {
  726. console.error('[StoreView] 添加资源失败:', error);
  727. if (window.parent && window.parent.HintView) {
  728. window.parent.HintView.error('添加失败,请稍后重试', 3000);
  729. }
  730. }
  731. return;
  732. }
  733. // 检查用户点数
  734. console.log('[StoreView] 检查用户点数,用户名:', username, '资源价格:', points);
  735. try {
  736. const pointsResponse = await fetch(`/api/user/points?username=${encodeURIComponent(username)}`);
  737. console.log('[StoreView] 点数请求响应:', pointsResponse.status);
  738. if (!pointsResponse.ok) {
  739. throw new Error(`HTTP error! status: ${pointsResponse.status}`);
  740. }
  741. const pointsResult = await pointsResponse.json();
  742. console.log('[StoreView] 点数结果:', pointsResult);
  743. if (!pointsResult.success) {
  744. throw new Error(pointsResult.message || '获取点数失败');
  745. }
  746. const userPoints = pointsResult.points || 0;
  747. console.log('[StoreView] 用户点数:', userPoints, '需要点数:', points);
  748. if (userPoints >= points) {
  749. // 点数充足,弹出确认对话框
  750. console.log('[StoreView] 点数充足,准备弹出确认对话框');
  751. console.log('[StoreView] window.parent.GlobalConfirm 存在:', !!(window.parent && window.parent.GlobalConfirm));
  752. let confirmed = false;
  753. if (window.parent && window.parent.GlobalConfirm) {
  754. // GlobalConfirm.show 返回 Promise
  755. confirmed = await window.parent.GlobalConfirm.show(
  756. `确定要花费 ${points} Ani币购买 ${resource.name} 吗?`
  757. );
  758. console.log('[StoreView] 用户选择:', confirmed);
  759. } else {
  760. // 降级使用原生 confirm
  761. console.log('[StoreView] GlobalConfirm 不可用,使用原生 confirm');
  762. confirmed = confirm(`确定要花费 ${points} Ani币购买 ${resource.name} 吗?`);
  763. }
  764. if (confirmed) {
  765. // 确认购买
  766. try {
  767. console.log('[StoreView] 发送购买请求...');
  768. const response = await fetch('/api/pay/purchase', {
  769. method: 'POST',
  770. headers: {
  771. 'Content-Type': 'application/json'
  772. },
  773. body: JSON.stringify({
  774. username: username,
  775. resourcePath: resource.path,
  776. categoryDir: resource.categoryDir,
  777. itemName: resource.name,
  778. points: points
  779. })
  780. });
  781. const result = await response.json();
  782. console.log('[StoreView] 购买结果:', result);
  783. if (result.success) {
  784. // 标记为已拥有
  785. resource.isOwned = true;
  786. console.log('[StoreView] 购买成功,资源已标记为已拥有:', resource.path, 'isOwned:', resource.isOwned);
  787. // 立即更新单个按钮状态
  788. this.updateSingleButtonState(resource.path);
  789. // 同时更新所有按钮状态(确保一致性)
  790. this.updateButtonStates();
  791. if (window.parent && window.parent.HintView) {
  792. window.parent.HintView.success(`购买成功!已扣除 ${points} Ani币,文件已添加到网盘`, 3000);
  793. }
  794. // 通知父窗口刷新点数和网盘
  795. if (window.parent && window.parent !== window) {
  796. window.parent.postMessage({ type: 'refresh-points' }, '*');
  797. window.parent.postMessage({ type: 'refresh-disk' }, '*');
  798. }
  799. } else {
  800. if (window.parent && window.parent.HintView) {
  801. window.parent.HintView.error(result.message || '购买失败', 3000);
  802. }
  803. }
  804. } catch (error) {
  805. console.error('[StoreView] 购买失败:', error);
  806. if (window.parent && window.parent.HintView) {
  807. window.parent.HintView.error('购买失败,请稍后重试', 3000);
  808. }
  809. }
  810. }
  811. } else {
  812. // 点数不足,弹出充值窗口
  813. if (window.parent && window.parent !== window) {
  814. window.parent.postMessage({
  815. type: 'open-recharge-view',
  816. needPoints: points,
  817. currentPoints: userPoints
  818. }, '*');
  819. }
  820. }
  821. } catch (error) {
  822. console.error('[StoreView] 检查点数失败:', error);
  823. if (window.parent && window.parent.HintView) {
  824. window.parent.HintView.error('检查点数失败,请稍后重试', 3000);
  825. }
  826. }
  827. }
  828. showLoading(show) {
  829. const loadingState = document.getElementById('loadingState');
  830. if (loadingState) {
  831. loadingState.style.display = show ? 'flex' : 'none';
  832. }
  833. }
  834. showEmpty() {
  835. const emptyState = document.getElementById('emptyState');
  836. if (emptyState) {
  837. emptyState.style.display = 'flex';
  838. }
  839. }
  840. hideEmpty() {
  841. const emptyState = document.getElementById('emptyState');
  842. if (emptyState) {
  843. emptyState.style.display = 'none';
  844. }
  845. }
  846. showGlobalAlert(message) {
  847. if (window.parent && window.parent.postMessage) {
  848. window.parent.postMessage({
  849. type: 'global-alert',
  850. text: message,
  851. duration: 3000
  852. }, '*');
  853. } else {
  854. alert(message);
  855. }
  856. }
  857. }
  858. // 页面加载完成后初始化
  859. if (document.readyState === 'loading') {
  860. document.addEventListener('DOMContentLoaded', () => {
  861. new StoreView();
  862. });
  863. } else {
  864. new StoreView();
  865. }
  866. })();