pricing.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. // 定价管理模块
  2. class PricingManager {
  3. constructor(options = {}) {
  4. this.apiBaseUrl = options.apiBaseUrl || 'http://localhost:3000';
  5. this.grid = options.grid || null;
  6. this.resources = [];
  7. this.init();
  8. }
  9. init() {
  10. this.bindEvents();
  11. // 默认进入页面时加载一次
  12. this.loadResources();
  13. }
  14. bindEvents() {
  15. // 刷新按钮
  16. const refreshBtn = document.getElementById('refreshPricingBtn');
  17. if (refreshBtn) {
  18. refreshBtn.addEventListener('click', () => {
  19. this.loadResources();
  20. });
  21. }
  22. }
  23. // 加载资源
  24. async loadResources() {
  25. if (!this.grid) return;
  26. this.grid.innerHTML = '<div class="loading-text">加载中...</div>';
  27. try {
  28. const response = await fetch(`${this.apiBaseUrl}/api/store/resources`);
  29. if (!response.ok) {
  30. throw new Error(`加载资源列表失败: ${response.status} ${response.statusText}`);
  31. }
  32. const result = await response.json();
  33. if (result.success && result.resources) {
  34. this.resources = result.resources;
  35. console.log('[PricingManager] 资源数:', this.resources.length, this.resources);
  36. this.renderResources();
  37. } else {
  38. this.grid.innerHTML = '<div class="loading-text">暂无资源数据</div>';
  39. }
  40. } catch (error) {
  41. console.error('[PricingManager] 加载资源失败:', error);
  42. const errorMessage = error.message || '未知错误';
  43. this.grid.innerHTML = `<div class="loading-text" style="color: #ef4444;">加载失败: ${errorMessage}</div>`;
  44. }
  45. }
  46. // 渲染资源
  47. renderResources() {
  48. if (!this.grid) return;
  49. // 使用 grid 布局,紧凑显示
  50. this.grid.style.display = 'grid';
  51. this.grid.style.gridTemplateColumns = 'repeat(auto-fill, minmax(240px, 1fr))';
  52. this.grid.style.gap = '16px';
  53. this.grid.style.padding = '20px';
  54. this.grid.style.background = '#f9fafb';
  55. if (this.resources.length === 0) {
  56. this.grid.innerHTML = '<div class="loading-text">暂无资源数据</div>';
  57. return;
  58. }
  59. console.log('[PricingManager] 渲染资源:', this.resources.length);
  60. this.grid.innerHTML = this.resources.map(resource => `
  61. <div class="pricing-item" style="
  62. border: 1px solid #e5e7eb;
  63. border-radius: 12px;
  64. padding: 12px;
  65. display: flex;
  66. flex-direction: column;
  67. gap: 10px;
  68. box-shadow: 0 2px 4px rgba(0,0,0,0.06);
  69. background: #fff;
  70. transition: transform 0.2s, box-shadow 0.2s;
  71. cursor: default;
  72. " onmouseover="this.style.transform='translateY(-2px)';this.style.boxShadow='0 4px 12px rgba(0,0,0,0.1)';" onmouseout="this.style.transform='';this.style.boxShadow='0 2px 4px rgba(0,0,0,0.06)';">
  73. <div class="pricing-preview" style="
  74. width: 100%;
  75. aspect-ratio: 1;
  76. display: flex;
  77. align-items: center;
  78. justify-content: center;
  79. background: linear-gradient(135deg, #f7f7f7 0%, #e9e9e9 100%);
  80. border-radius: 8px;
  81. overflow: hidden;
  82. position: relative;
  83. ">
  84. ${resource.previewUrl ?
  85. `<img src="${this.apiBaseUrl}${resource.previewUrl}" alt="${resource.name}" style="max-width:100%;max-height:100%;object-fit:contain;" onerror="this.parentElement.innerHTML='<div style=\\'color:#999;font-size:12px;\\'>无预览</div>'">` :
  86. '<div style="color:#999;font-size:12px;">无预览</div>'
  87. }
  88. <div style="position:absolute;bottom:4px;right:4px;background:rgba(0,0,0,0.6);color:#fff;font-size:10px;padding:2px 6px;border-radius:4px;">${resource.categoryDir || '-'}</div>
  89. </div>
  90. <div class="pricing-info" style="display:flex;flex-direction:column;gap:8px;">
  91. <div class="pricing-name" style="font-weight:600;font-size:14px;color:#111;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;" title="${resource.name}">${resource.name}</div>
  92. <div class="pricing-price-input" style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
  93. <div style="display:flex;align-items:center;gap:6px;">
  94. <label style="font-size:12px;color:#6b7280;flex-shrink:0;">价格</label>
  95. <input type="text" class="price-input" value="${resource.points || 0}"
  96. data-path="${resource.path}"
  97. data-original="${resource.points || 0}"
  98. style="width:60px;padding:6px 8px;border:1px solid #e5e7eb;border-radius:6px;outline:none;font-size:13px;transition:border-color 0.2s;text-align:center;"
  99. onfocus="this.style.borderColor='#667eea';"
  100. onblur="this.style.borderColor='#e5e7eb';"
  101. oninput="this.closest('.pricing-price-input').querySelector('.btn-confirm').style.display = this.value !== this.dataset.original ? 'inline-flex' : 'none';">
  102. <span style="font-size:12px;color:#6b7280;">Ani币</span>
  103. <button class="btn-confirm"
  104. onclick="window.pricingManagerInstance.confirmPrice('${resource.path}', this.closest('.pricing-price-input').querySelector('.price-input'))"
  105. style="display:none;width:24px;height:24px;background:#10b981;color:#fff;border:none;border-radius:50%;font-size:14px;font-weight:bold;cursor:pointer;transition:all 0.2s;align-items:center;justify-content:center;"
  106. onmouseover="this.style.background='#059669';this.style.transform='scale(1.1)';"
  107. onmouseout="this.style.background='#10b981';this.style.transform='';"
  108. title="确认修改">✓</button>
  109. </div>
  110. </div>
  111. </div>
  112. </div>
  113. `).join('');
  114. }
  115. // 确认价格修改
  116. async confirmPrice(resourcePath, inputEl) {
  117. const price = inputEl.value.trim();
  118. const priceNum = parseFloat(price);
  119. if (isNaN(priceNum) || priceNum < 0) {
  120. this.showError('价格必须是大于等于0的数字');
  121. return;
  122. }
  123. try {
  124. const response = await fetch(`${this.apiBaseUrl}/api/admin/store/update-price`, {
  125. method: 'POST',
  126. headers: {
  127. 'Content-Type': 'application/json'
  128. },
  129. body: JSON.stringify({
  130. resourcePath: resourcePath,
  131. price: priceNum
  132. })
  133. });
  134. const result = await response.json();
  135. if (result.success) {
  136. // 更新本地数据和原始值
  137. const resource = this.resources.find(r => r.path === resourcePath);
  138. if (resource) {
  139. resource.points = priceNum;
  140. }
  141. inputEl.dataset.original = priceNum;
  142. inputEl.value = priceNum;
  143. // 隐藏确认按钮
  144. const confirmBtn = inputEl.parentElement.querySelector('.btn-confirm');
  145. if (confirmBtn) {
  146. confirmBtn.style.display = 'none';
  147. }
  148. this.showSuccess('价格更新成功');
  149. } else {
  150. this.showError('价格更新失败: ' + (result.message || '未知错误'));
  151. }
  152. } catch (error) {
  153. console.error('[PricingManager] 更新价格失败:', error);
  154. this.showError('价格更新失败,请稍后重试');
  155. }
  156. }
  157. // 更新价格(保留兼容)
  158. async updatePrice(resourcePath, price) {
  159. // 现在由 confirmPrice 处理
  160. }
  161. // 显示错误/成功消息
  162. showError(message) {
  163. if (window.showCustomAlert) {
  164. window.showCustomAlert(message);
  165. } else {
  166. console.error('[PricingManager] 错误:', message);
  167. }
  168. }
  169. showSuccess(message) {
  170. if (window.showCustomAlert) {
  171. window.showCustomAlert(message, 'success');
  172. } else {
  173. console.log('[PricingManager] 成功:', message);
  174. }
  175. }
  176. }
  177. // 导出
  178. if (typeof module !== 'undefined' && module.exports) {
  179. module.exports = PricingManager;
  180. } else {
  181. window.PricingManager = PricingManager;
  182. }