User 2 kuukautta sitten
vanhempi
sitoutus
37a9a90bc2
100 muutettua tiedostoa jossa 15884 lisäystä ja 455 poistoa
  1. 780 0
      admin/css/Index.css
  2. 1045 0
      admin/css/admin.css
  3. 138 0
      admin/css/resource-manager/confirm-view.css
  4. 497 0
      admin/css/resource-manager/disk.css
  5. 76 0
      admin/css/resource-manager/hint-view.css
  6. 59 0
      admin/css/resource-manager/right-click-menu.css
  7. 198 0
      admin/css/resource-manager/tool-bar.css
  8. 250 0
      admin/index.html
  9. 201 0
      admin/js/admin.js
  10. 243 0
      admin/js/currency/currency.js
  11. 334 0
      admin/js/index.js
  12. 208 0
      admin/js/pricing/pricing.js
  13. 153 0
      admin/js/product-pricing/product-pricing.js
  14. 291 0
      admin/js/resource-manager/multiple-selection.js
  15. 53 0
      admin/js/resource-manager/path.js
  16. 1373 0
      admin/js/resource-manager/resource-manager.js
  17. 107 0
      admin/js/resource-manager/right-click-menu.js
  18. 109 0
      admin/js/resource-manager/search-bar.js
  19. 105 0
      admin/js/resource-manager/shortcut-keys.js
  20. 183 0
      admin/js/users/users.js
  21. 344 0
      admin/page/currency/currency.html
  22. 43 0
      admin/page/pricing/pricing.html
  23. 177 0
      admin/page/product-pricing/product-pricing.html
  24. 204 0
      admin/page/resource-manager/resource-manager.html
  25. 13 0
      admin/page/resource-manager/right-click-menu.html
  26. 90 0
      admin/page/users/users.html
  27. BIN
      admin/static/favicon.png
  28. BIN
      admin/static/logo.png
  29. 581 0
      client/css/ai-generate/ai-generate-view.css
  30. 400 4
      client/css/export-view/export-view.css
  31. 76 0
      client/css/hint-view.css
  32. 163 0
      client/css/pay-view/pay-view.css
  33. 1014 0
      client/css/profile/profile.css
  34. 238 0
      client/css/recharge-view/recharge-view.css
  35. 59 0
      client/css/seq_ani_player/card.css
  36. 253 0
      client/css/store/item.css
  37. 388 0
      client/css/store/store.css
  38. 4 1
      client/index.html
  39. 58 1
      client/js/Index.js
  40. 774 0
      client/js/ai-generate/ai-generate-view.js
  41. 20 11
      client/js/disk/disk.js
  42. 7 2
      client/js/export-view-manager.js
  43. 187 345
      client/js/export-view/export-view.js
  44. 94 0
      client/js/hint-view.js
  45. 239 0
      client/js/pay-view/pay-view.js
  46. 761 0
      client/js/profile/profile.js
  47. 249 0
      client/js/recharge-view/recharge-view.js
  48. 94 9
      client/js/seq_ani_player/card.js
  49. 37 2
      client/js/seq_ani_player/seq-ani-player.js
  50. 32 5
      client/js/sprite_sheet_maker/sprite-sheet-maker.js
  51. 12 0
      client/js/store/item.js
  52. 1001 0
      client/js/store/store.js
  53. 86 0
      client/page/ai-generate/ai-generate-view.html
  54. 32 71
      client/page/export/export-view.html
  55. 10 0
      client/page/hint-view.html
  56. 1 1
      client/page/navigation/navigation.html
  57. 44 0
      client/page/pay-view/pay-view.html
  58. 98 0
      client/page/profile/profile.html
  59. 79 0
      client/page/recharge-view/recharge-view.html
  60. 10 3
      client/page/seq_ani_player/card.html
  61. 23 0
      client/page/store/item.html
  62. 854 0
      server/admin.js
  63. 52 0
      server/ai-history.json
  64. 554 0
      server/ai-queue.js
  65. 1 0
      server/ai-queue.json
  66. 19 0
      server/currency-settings.json
  67. BIN
      server/data.db
  68. 6 0
      server/market_data/prices.json
  69. BIN
      server/market_data/角色/player_0001/01.png
  70. BIN
      server/market_data/角色/player_0001/02.png
  71. BIN
      server/market_data/角色/player_0001/03.png
  72. BIN
      server/market_data/角色/player_0001/04.png
  73. BIN
      server/market_data/角色/player_0001/05.png
  74. BIN
      server/market_data/角色/player_0001/06.png
  75. BIN
      server/market_data/角色/player_0001/07.png
  76. BIN
      server/market_data/角色/player_0001/08.png
  77. BIN
      server/market_data/角色/player_0001/09.png
  78. BIN
      server/market_data/角色/player_0001/10.png
  79. BIN
      server/market_data/角色/player_0001/11.png
  80. BIN
      server/market_data/角色/player_0001/12.png
  81. BIN
      server/market_data/角色/player_0001/13.png
  82. BIN
      server/market_data/角色/player_0001/14.png
  83. BIN
      server/market_data/角色/player_0002/01.png
  84. BIN
      server/market_data/角色/player_0002/02.png
  85. BIN
      server/market_data/角色/player_0002/03.png
  86. BIN
      server/market_data/角色/player_0002/04.png
  87. BIN
      server/market_data/角色/player_0002/05.png
  88. BIN
      server/market_data/角色/player_0002/06.png
  89. BIN
      server/market_data/角色/player_0002/07.png
  90. BIN
      server/market_data/角色/player_0002/08.png
  91. BIN
      server/market_data/角色/player_0002/09.png
  92. BIN
      server/market_data/角色/player_0002/10.png
  93. BIN
      server/market_data/角色/player_0002/11.png
  94. BIN
      server/market_data/角色/player_0002/12.png
  95. BIN
      server/market_data/角色/player_0002/13.png
  96. BIN
      server/market_data/角色/player_0002/14.png
  97. BIN
      server/market_data/角色/player_0003/01.png
  98. BIN
      server/market_data/角色/player_0003/02.png
  99. BIN
      server/market_data/角色/player_0003/03.png
  100. BIN
      server/market_data/角色/player_0003/04.png

+ 780 - 0
admin/css/Index.css

@@ -0,0 +1,780 @@
+.app-root {
+  /* 整体占满屏幕,使用 flex 布局 */
+  display: flex;
+  flex-direction: column;
+  min-height: 100vh;
+  height: 100vh; /* 确保在 iframe 中占满高度 */
+  padding: 32px;
+  box-sizing: border-box;
+  background: linear-gradient(135deg, #fdfcfb 0%, #f7f7f8 50%, #fdfcfb 100%);
+  color: #1f2937;
+  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial,
+    "Noto Sans", sans-serif;
+  /* 禁用文字选中 */
+  user-select: none;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  margin: 0;
+  overflow: hidden; /* 防止滚动条出现在子页面 */
+}
+
+/* 导航栏 iframe 容器 */
+#navigationFrame {
+  width: 100%;
+  height: 80px; /* 固定导航栏高度 */
+  border: none;
+  flex-shrink: 0;
+  display: block;
+  background: #ffffff;
+  margin-bottom: 24px; /* 与下方内容保持间距 */
+  border-radius: 12px;
+  overflow: hidden; /* 确保圆角生效 */
+}
+
+/* 标题区域 */
+.app-header {
+  flex-shrink: 0;
+  text-align: center;
+  margin-bottom: 20px;
+  padding: 20px 0 0 0;
+}
+
+.app-title {
+  margin: 0;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 8px;
+  user-select: none;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+}
+
+.title-main {
+  font-size: 36px;
+  font-weight: 700;
+  background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 50%, #ec4899 100%);
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: transparent;
+  background-clip: text;
+  letter-spacing: 2px;
+  text-shadow: 0 2px 4px rgba(59, 130, 246, 0.1);
+  position: relative;
+  display: inline-block;
+  user-select: none;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+}
+
+.title-main::after {
+  content: '';
+  position: absolute;
+  bottom: -4px;
+  left: 50%;
+  transform: translateX(-50%);
+  width: 60px;
+  height: 3px;
+  background: linear-gradient(90deg, transparent, #3b82f6, transparent);
+  border-radius: 2px;
+}
+
+.title-subtitle {
+  font-size: 14px;
+  font-weight: 400;
+  color: #9ca3af;
+  letter-spacing: 1px;
+  text-transform: uppercase;
+  font-family: "SF Mono", "Monaco", "Inconsolata", "Roboto Mono", monospace;
+  user-select: none;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+}
+
+/* Cover Flow 容器 */
+.coverflow-container {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  overflow: hidden;
+  margin-bottom: 32px;
+  /* iTunes Cover Flow 风格的透视效果 */
+  perspective: 2000px;
+  perspective-origin: 50% 50%;
+  pointer-events: auto; /* 确保容器可以接收点击事件 */
+  background: radial-gradient(ellipse at center, rgba(139, 92, 246, 0.03) 0%, transparent 70%);
+  border-radius: 20px;
+}
+
+.coverflow-wrapper {
+  position: relative;
+  width: 100%;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  transform-style: preserve-3d;
+  pointer-events: auto; /* 确保包装器可以接收点击事件 */
+  /* 禁用文字选中 */
+  user-select: none;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+}
+
+/* 卡片信息显示(右下角) */
+.card-info {
+  position: absolute;
+  bottom: 20px;
+  right: 20px;
+  padding: 8px 16px;
+  background: rgba(0, 0, 0, 0.7);
+  color: #ffffff;
+  border-radius: 8px;
+  font-size: 14px;
+  font-weight: 500;
+  z-index: 10000;
+  pointer-events: none; /* 不拦截鼠标事件 */
+  user-select: none;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  font-family: "SF Mono", "Monaco", "Inconsolata", "Roboto Mono", monospace;
+}
+
+.coverflow {
+  position: relative;
+  width: 100%;
+  height: 500px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  transform-style: preserve-3d;
+  margin: 0 auto;
+  /* 禁用文字选中 */
+  user-select: none;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+}
+
+.coverflow-card {
+  position: absolute;
+  width: 300px;
+  height: 400px;
+  /* iTunes Cover Flow 风格的平滑动画 - 使用更自然的缓动函数 */
+  transition: transform 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94);
+  cursor: pointer !important; /* 强制显示手型指针 */
+  transform-style: preserve-3d;
+  transform-origin: center center;
+  pointer-events: auto; /* 确保所有卡片都可以点击 */
+  /* 为遮罩层提供定位基准 */
+  /* 注意:遮罩层不应该继承 3D 变换 */
+  will-change: transform; /* 优化动画性能 */
+  /* 禁用文字选中 */
+  user-select: none;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+}
+
+.coverflow-card.prev-2 {
+  /* iTunes Cover Flow 风格:更自然的旋转角度和深度 */
+  transform: translateX(-600px) translateZ(-400px) rotateY(60deg);
+  opacity: 1; /* 完全可见,清晰显示 */
+  z-index: 0;
+  pointer-events: auto; /* 确保侧边卡片可以点击 */
+  cursor: pointer !important; /* 强制显示手型指针 */
+}
+
+.coverflow-card.prev-1 {
+  /* iTunes Cover Flow 风格:更自然的旋转角度和深度 */
+  transform: translateX(-300px) translateZ(-200px) rotateY(45deg);
+  opacity: 1; /* 完全可见,清晰显示 */
+  z-index: 1;
+  pointer-events: auto; /* 确保侧边卡片可以点击 */
+  cursor: pointer !important; /* 强制显示手型指针 */
+}
+
+.coverflow-card.current {
+  /* 中心卡片:完全可见,无旋转,在最前面 */
+  transform: translateX(0) translateZ(0) rotateY(0deg) scale(1);
+  opacity: 1;
+  z-index: 3;
+  pointer-events: auto; /* 确保中间卡片可以点击 */
+  cursor: pointer !important; /* 强制显示手型指针 */
+}
+
+.coverflow-card.next-1 {
+  /* iTunes Cover Flow 风格:更自然的旋转角度和深度 */
+  transform: translateX(300px) translateZ(-200px) rotateY(-45deg);
+  opacity: 1; /* 完全可见,清晰显示 */
+  z-index: 1;
+  pointer-events: auto; /* 确保侧边卡片可以点击 */
+  cursor: pointer !important; /* 强制显示手型指针 */
+}
+
+.coverflow-card.next-2 {
+  /* iTunes Cover Flow 风格:更自然的旋转角度和深度 */
+  transform: translateX(600px) translateZ(-400px) rotateY(-60deg);
+  opacity: 1; /* 完全可见,清晰显示 */
+  z-index: 0;
+  pointer-events: auto; /* 确保侧边卡片可以点击 */
+  cursor: pointer !important; /* 强制显示手型指针 */
+}
+
+.coverflow-card.hidden {
+  opacity: 0;
+  pointer-events: none;
+  /* 注意:不设置 display: none,因为由 JavaScript 的 style.display 控制 */
+  /* 确保 hidden 类不会覆盖内联样式 */
+  display: none !important;
+}
+
+.card-content {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  background: linear-gradient(135deg, #ffffff 0%, #f9fafb 100%);
+  border-radius: 16px;
+  box-shadow: 0 10px 40px rgba(139, 92, 246, 0.15), 0 4px 16px rgba(0, 0, 0, 0.1);
+  border: 2px solid rgba(139, 92, 246, 0.1);
+  overflow: hidden;
+  transform-style: preserve-3d;
+  backface-visibility: visible;
+  pointer-events: auto;
+  position: relative; /* 为遮罩层定位 */
+  transition: all 0.3s ease;
+  /* 禁用文字选中 */
+  user-select: none;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+}
+
+.coverflow-card.current .card-content {
+  border: 2px solid rgba(139, 92, 246, 0.3);
+  box-shadow: 0 12px 48px rgba(139, 92, 246, 0.25), 0 6px 20px rgba(0, 0, 0, 0.15);
+}
+
+/* 透明交互遮罩层 - 避免3D形变干扰鼠标事件 */
+.card-interaction-overlay {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  cursor: pointer !important; /* 强制显示手型指针 */
+  pointer-events: auto !important; /* 强制启用事件 */
+  z-index: 9999; /* 确保在最上层 */
+  background: transparent; /* 完全透明 */
+  border-radius: 12px; /* 与卡片圆角一致 */
+  /* 关键:不应用任何 3D 变换,保持平面 */
+  transform: none !important;
+  transform-style: flat !important;
+  backface-visibility: visible;
+}
+
+.image-container {
+  width: 100%;
+  height: 85%;
+  position: relative;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: #ffffff;
+  pointer-events: auto;
+  cursor: pointer; /* 确保图片容器也显示手型指针 */
+}
+
+.card-image {
+  max-width: 100%;
+  max-height: 100%;
+  width: auto;
+  height: auto;
+  object-fit: contain;
+  image-rendering: pixelated;
+  background: transparent;
+  pointer-events: auto;
+  user-select: none;
+  cursor: pointer; /* 确保图片也显示手型指针 */
+}
+
+.loading-spinner {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  width: 40px;
+  height: 40px;
+  border: 3px solid #e5e7eb;
+  border-top-color: #3b82f6;
+  border-radius: 50%;
+  animation: spin 0.8s linear infinite;
+  z-index: 10;
+}
+
+@keyframes spin {
+  to {
+    transform: translate(-50%, -50%) rotate(360deg);
+  }
+}
+
+.image-error {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  padding: 8px 16px;
+  background: rgba(239, 68, 68, 0.9);
+  color: white;
+  border-radius: 6px;
+  font-size: 12px;
+  z-index: 10;
+  white-space: nowrap;
+  /* 禁用文字选中 */
+  user-select: none;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+}
+
+.card-label {
+  height: 15%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 14px;
+  font-weight: 500;
+  color: #374151;
+  text-transform: uppercase;
+  letter-spacing: 0.05em;
+  /* 禁用文字选中 */
+  user-select: none;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+}
+
+/* 卡片下载按钮 */
+.card-download-btn {
+  position: absolute;
+  bottom: 8px;
+  right: 8px;
+  width: 32px;
+  height: 32px;
+  border: none;
+  border-radius: 50%;
+  background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
+  color: #ffffff;
+  font-size: 18px;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  box-shadow: 0 2px 8px rgba(59, 130, 246, 0.4);
+  transition: all 0.2s ease;
+  z-index: 10000; /* 确保在遮罩层之上 */
+  pointer-events: auto;
+  /* 禁用文字选中 */
+  user-select: none;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+}
+
+.card-download-btn:hover {
+  transform: scale(1.1);
+  box-shadow: 0 4px 12px rgba(59, 130, 246, 0.6);
+  background: linear-gradient(135deg, #2563eb 0%, #7c3aed 100%);
+}
+
+.card-download-btn:active {
+  transform: scale(0.95);
+}
+
+/* 底部控制面板 */
+.panel-card {
+  flex-shrink: 0;
+  width: 100%;
+  max-width: 800px;
+  margin: 0 auto;
+  padding: 24px 32px;
+  border-radius: 16px;
+  background: linear-gradient(135deg, #ffffff 0%, #f9fafb 100%);
+  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08), 0 2px 8px rgba(0, 0, 0, 0.04);
+  border: 1px solid rgba(229, 231, 235, 0.8);
+  backdrop-filter: blur(10px);
+}
+
+.control-panel {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 20px;
+  width: 100%;
+}
+
+.folder-label {
+  font-size: 15px;
+  color: #4b5563;
+  font-weight: 600;
+  white-space: nowrap;
+  flex-shrink: 0;
+  letter-spacing: 0.3px;
+  /* 禁用文字选中 */
+  user-select: none;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+}
+
+.folder-input {
+  width: 200px;
+  padding: 12px 16px;
+  border-radius: 10px;
+  border: 1px solid #d1d5db;
+  background: #ffffff;
+  color: #1f2937;
+  font-size: 14px;
+  outline: none;
+  box-sizing: border-box;
+  transition: all 0.2s;
+}
+
+.folder-input:focus {
+  border-color: #3b82f6;
+  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
+}
+
+/* 子页面容器 */
+.page-container {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  min-height: 0; /* 允许 flex 子元素收缩 */
+}
+
+#pageFrame {
+  width: 100%;
+  height: 100%;
+  border: none;
+  background: #ffffff;
+}
+
+/* SeqAniPlayer 页面专用样式 */
+
+/* 控制行布局 */
+.control-row {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 24px;
+  width: 100%;
+  max-width: 600px;
+}
+
+.control-row:not(:last-child) {
+  margin-bottom: 0;
+}
+
+/* 滑动条容器 */
+.slider-container {
+  display: flex;
+  align-items: center;
+  gap: 20px;
+  flex: 1;
+  min-width: 0;
+}
+
+/* FPS 滑动条样式 */
+.fps-slider {
+  flex: 1;
+  height: 10px;
+  -webkit-appearance: none;
+  appearance: none;
+  background: linear-gradient(to right, #ddd6fe 0%, #a78bfa 50%, #8b5cf6 100%);
+  border-radius: 6px;
+  outline: none;
+  cursor: pointer;
+  transition: all 0.3s ease;
+  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
+}
+
+.fps-slider:hover {
+  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.15), 0 0 8px rgba(139, 92, 246, 0.3);
+}
+
+/* 滑块轨道 - WebKit (Chrome, Safari, Edge) */
+.fps-slider::-webkit-slider-runnable-track {
+  height: 8px;
+  border-radius: 4px;
+  background: transparent;
+}
+
+/* 滑块按钮 - WebKit */
+.fps-slider::-webkit-slider-thumb {
+  -webkit-appearance: none;
+  appearance: none;
+  width: 24px;
+  height: 24px;
+  background: linear-gradient(135deg, #8b5cf6, #a78bfa);
+  border-radius: 50%;
+  cursor: pointer;
+  box-shadow: 0 3px 12px rgba(139, 92, 246, 0.5), 0 1px 4px rgba(0, 0, 0, 0.2);
+  transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.2s ease;
+  margin-top: -7px;
+  border: 2px solid #ffffff;
+}
+
+.fps-slider::-webkit-slider-thumb:hover {
+  transform: scale(1.2);
+  box-shadow: 0 4px 16px rgba(139, 92, 246, 0.7), 0 2px 8px rgba(0, 0, 0, 0.3);
+}
+
+.fps-slider::-webkit-slider-thumb:active {
+  transform: scale(1.1);
+  box-shadow: 0 2px 8px rgba(139, 92, 246, 0.8);
+}
+
+/* 滑块轨道 - Firefox */
+.fps-slider::-moz-range-track {
+  height: 10px;
+  border-radius: 6px;
+  background: linear-gradient(to right, #ddd6fe 0%, #a78bfa 50%, #8b5cf6 100%);
+  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
+}
+
+/* 滑块按钮 - Firefox */
+.fps-slider::-moz-range-thumb {
+  width: 24px;
+  height: 24px;
+  background: linear-gradient(135deg, #8b5cf6, #a78bfa);
+  border: 2px solid #ffffff;
+  border-radius: 50%;
+  cursor: pointer;
+  box-shadow: 0 3px 12px rgba(139, 92, 246, 0.5), 0 1px 4px rgba(0, 0, 0, 0.2);
+  transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.2s ease;
+}
+
+.fps-slider::-moz-range-thumb:hover {
+  transform: scale(1.2);
+  box-shadow: 0 4px 16px rgba(139, 92, 246, 0.7), 0 2px 8px rgba(0, 0, 0, 0.3);
+}
+
+.fps-slider::-moz-range-thumb:active {
+  transform: scale(1.1);
+  box-shadow: 0 2px 8px rgba(139, 92, 246, 0.8);
+}
+
+/* FPS 数值显示 */
+.fps-value {
+  min-width: 80px;
+  padding: 10px 18px;
+  background: linear-gradient(135deg, #8b5cf6, #a78bfa);
+  color: #ffffff;
+  border-radius: 10px;
+  font-size: 16px;
+  font-weight: 700;
+  text-align: center;
+  font-family: "SF Mono", "Monaco", "Inconsolata", "Roboto Mono", monospace;
+  box-shadow: 0 4px 12px rgba(139, 92, 246, 0.4), 0 2px 4px rgba(0, 0, 0, 0.1);
+  user-select: none;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  letter-spacing: 0.5px;
+  transition: all 0.3s ease;
+}
+
+.fps-value:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 6px 16px rgba(139, 92, 246, 0.5), 0 3px 6px rgba(0, 0, 0, 0.15);
+}
+
+/* 内容区域(用于子页面) */
+.content-area {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  overflow: auto;
+  padding: 20px;
+}
+
+/* 基础样式 - 适用于主页面和子页面 */
+body {
+  margin: 0;
+  padding: 0;
+  height: 100vh;
+  overflow: hidden;
+}
+
+html {
+  margin: 0;
+  padding: 0;
+  height: 100%;
+}
+
+/* 响应式设计 */
+@media (max-width: 768px) {
+  .app-header {
+    margin-bottom: 20px;
+    padding: 15px 0;
+  }
+
+  .title-main {
+    font-size: 28px;
+    letter-spacing: 1px;
+  }
+
+  .title-subtitle {
+    font-size: 12px;
+  }
+
+  .coverflow {
+    height: 400px;
+  }
+
+  .coverflow-card {
+    width: 250px;
+    height: 350px;
+  }
+}
+
+@media (max-width: 480px) {
+  .app-root {
+    padding: 15px;
+  }
+
+  .title-main {
+    font-size: 24px;
+  }
+
+  .title-subtitle {
+    font-size: 11px;
+  }
+}
+
+/* ========================================
+   全局 Loading 遮罩
+   ======================================== */
+
+.global-loading-overlay {
+  position: fixed;
+  inset: 0;
+  background: rgba(0, 0, 0, 0.5);
+  backdrop-filter: blur(4px);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 99999;
+  opacity: 0;
+  visibility: hidden;
+  transition: opacity 0.3s ease, visibility 0.3s ease;
+  pointer-events: none;
+}
+
+.global-loading-overlay.is-visible {
+  opacity: 1;
+  visibility: visible;
+  pointer-events: all;
+}
+
+.global-loading-content {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 24px;
+  padding: 40px 48px;
+  background: rgba(255, 255, 255, 0.98);
+  border-radius: 20px;
+  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+}
+
+.global-loading-spinner {
+  width: 64px;
+  height: 64px;
+  border-radius: 50%;
+  border: 5px solid rgba(139, 92, 246, 0.15);
+  border-top-color: #8b5cf6;
+  border-bottom-color: #8b5cf6;
+  animation: spin-global 0.9s cubic-bezier(0.45, 0.05, 0.55, 0.95) infinite;
+  position: relative;
+}
+
+.global-loading-spinner::before {
+  content: "";
+  position: absolute;
+  inset: -16px;
+  border-radius: 50%;
+  background: radial-gradient(circle, rgba(139, 92, 246, 0.1) 0%, transparent 70%);
+  animation: pulse-global 1.5s ease-in-out infinite;
+}
+
+.global-loading-text {
+  margin: 0;
+  font-size: 16px;
+  font-weight: 600;
+  color: #374151;
+  letter-spacing: 0.5px;
+}
+
+@keyframes spin-global {
+  to {
+    transform: rotate(360deg);
+  }
+}
+
+@keyframes pulse-global {
+  0%, 100% {
+    transform: scale(0.9);
+    opacity: 0.5;
+  }
+  50% {
+    transform: scale(1.15);
+    opacity: 1;
+  }
+}
+
+/* ========================================
+   全局文字提示 (Alert)
+   ======================================== */
+
+.global-alert {
+  position: fixed;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%) scale(0.8);
+  z-index: 100000;
+  opacity: 0;
+  visibility: hidden;
+  transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
+  pointer-events: none;
+}
+
+.global-alert.show {
+  opacity: 1;
+  visibility: visible;
+  transform: translate(-50%, -50%) scale(1);
+}
+
+.global-alert .alert-message {
+  padding: 16px 32px;
+  background: rgba(31, 41, 55, 0.95);
+  color: white;
+  border-radius: 12px;
+  font-size: 15px;
+  font-weight: 500;
+  text-align: center;
+  white-space: nowrap;
+  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
+  backdrop-filter: blur(8px);
+}

+ 1045 - 0
admin/css/admin.css

@@ -0,0 +1,1045 @@
+/* 管理后台样式 */
+
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+}
+
+body {
+  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+  background: #f5f7fa;
+  color: #1f2937;
+}
+
+/* 登录页面 */
+.login-page {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  min-height: 100vh;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+}
+
+.login-container {
+  background: white;
+  border-radius: 16px;
+  padding: 40px;
+  width: 100%;
+  max-width: 400px;
+  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+}
+
+.login-header {
+  text-align: center;
+  margin-bottom: 32px;
+}
+
+.login-header h1 {
+  font-size: 28px;
+  font-weight: 700;
+  color: #1f2937;
+  margin-bottom: 8px;
+}
+
+.login-header p {
+  color: #6b7280;
+  font-size: 14px;
+}
+
+.login-form {
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+}
+
+.form-group {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.form-group label {
+  font-size: 14px;
+  font-weight: 500;
+  color: #374151;
+}
+
+.form-group input,
+.form-group select {
+  padding: 12px 16px;
+  border: 1px solid #e5e7eb;
+  border-radius: 8px;
+  font-size: 14px;
+  transition: all 0.2s;
+}
+
+.form-group input:focus,
+.form-group select:focus {
+  outline: none;
+  border-color: #667eea;
+  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
+}
+
+.form-hint {
+  display: block;
+  margin-top: 4px;
+  font-size: 12px;
+  color: #6b7280;
+}
+
+.login-btn {
+  padding: 12px 24px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+  border: none;
+  border-radius: 8px;
+  font-size: 16px;
+  font-weight: 600;
+  cursor: pointer;
+  transition: all 0.2s;
+  margin-top: 8px;
+}
+
+.login-btn:hover {
+  transform: translateY(-1px);
+  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
+}
+
+.error-message {
+  color: #ef4444;
+  font-size: 14px;
+  text-align: center;
+  padding: 8px;
+  background: #fef2f2;
+  border-radius: 6px;
+}
+
+/* 主界面 */
+.main-page {
+  display: flex;
+  min-height: 100vh;
+}
+
+/* 侧边栏 */
+.sidebar {
+  width: 240px;
+  background: white;
+  border-right: 1px solid #e5e7eb;
+  display: flex;
+  flex-direction: column;
+  position: fixed;
+  height: 100vh;
+  left: 0;
+  top: 0;
+}
+
+.sidebar-header {
+  padding: 20px;
+  border-bottom: 1px solid #e5e7eb;
+}
+
+.sidebar-header h2 {
+  font-size: 18px;
+  font-weight: 700;
+  color: #1f2937;
+}
+
+.sidebar-nav {
+  flex: 1;
+  padding: 12px 0;
+  overflow-y: auto;
+}
+
+.nav-item {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  padding: 12px 20px;
+  color: #6b7280;
+  text-decoration: none;
+  transition: all 0.2s;
+  border-left: 3px solid transparent;
+}
+
+.nav-item:hover {
+  background: #f9fafb;
+  color: #667eea;
+}
+
+.nav-item.active {
+  background: #f3f4f6;
+  color: #667eea;
+  border-left-color: #667eea;
+  font-weight: 600;
+}
+
+.nav-item svg {
+  width: 20px;
+  height: 20px;
+}
+
+.sidebar-footer {
+  padding: 16px 20px;
+  border-top: 1px solid #e5e7eb;
+}
+
+.logout-btn {
+  width: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 10px;
+  padding: 12px 16px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+  border: none;
+  border-radius: 10px;
+  font-size: 14px;
+  font-weight: 600;
+  cursor: pointer;
+  transition: all 0.3s ease;
+  box-shadow: 0 2px 8px rgba(102, 126, 234, 0.2);
+  position: relative;
+  overflow: hidden;
+}
+
+.logout-btn::before {
+  content: '';
+  position: absolute;
+  top: 0;
+  left: -100%;
+  width: 100%;
+  height: 100%;
+  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
+  transition: left 0.5s;
+}
+
+.logout-btn:hover::before {
+  left: 100%;
+}
+
+.logout-btn:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 4px 16px rgba(102, 126, 234, 0.4);
+  background: linear-gradient(135deg, #5568d3 0%, #6a3d91 100%);
+}
+
+.logout-btn:active {
+  transform: translateY(0);
+  box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
+}
+
+.logout-btn svg {
+  flex-shrink: 0;
+  transition: transform 0.3s ease;
+}
+
+.logout-btn:hover svg {
+  transform: translateX(2px);
+}
+
+/* 主内容区 */
+.main-content {
+  flex: 1;
+  margin-left: 240px;
+  display: flex;
+  flex-direction: column;
+}
+
+.main-header {
+  background: white;
+  padding: 20px 32px;
+  border-bottom: 1px solid #e5e7eb;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.main-header h1 {
+  font-size: 22px;
+  font-weight: 700;
+  color: #1f2937;
+}
+
+.header-actions {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+}
+
+.admin-name {
+  color: #6b7280;
+  font-size: 14px;
+}
+
+.content-area {
+  flex: 1;
+  padding: 0;
+  overflow: hidden;
+  position: relative;
+}
+
+.content-area iframe {
+  width: 100%;
+  height: 100%;
+  border: none;
+  display: block;
+}
+
+.page-content {
+  display: none;
+}
+
+.page-content.active {
+  display: block;
+}
+
+/* 页面头部 */
+.page-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 20px;
+}
+
+.page-header h2 {
+  font-size: 18px;
+  font-weight: 600;
+  color: #1f2937;
+}
+
+.header-buttons {
+  display: flex;
+  gap: 12px;
+}
+
+/* 按钮 */
+.btn-primary {
+  padding: 10px 20px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+  border: none;
+  border-radius: 8px;
+  font-size: 14px;
+  font-weight: 600;
+  cursor: pointer;
+  transition: all 0.2s;
+}
+
+.btn-primary:hover {
+  transform: translateY(-1px);
+  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
+}
+
+.btn-secondary {
+  padding: 10px 20px;
+  background: #f3f4f6;
+  color: #374151;
+  border: none;
+  border-radius: 8px;
+  font-size: 14px;
+  font-weight: 600;
+  cursor: pointer;
+  transition: all 0.2s;
+}
+
+.btn-secondary:hover {
+  background: #e5e7eb;
+}
+
+.btn-danger {
+  padding: 6px 12px;
+  background: #ef4444;
+  color: white;
+  border: none;
+  border-radius: 6px;
+  font-size: 12px;
+  cursor: pointer;
+  transition: all 0.2s;
+}
+
+.btn-danger:hover {
+  background: #dc2626;
+}
+
+.btn-edit {
+  padding: 6px 12px;
+  background: #667eea;
+  color: white;
+  border: none;
+  border-radius: 6px;
+  font-size: 12px;
+  cursor: pointer;
+  transition: all 0.2s;
+}
+
+.btn-edit:hover {
+  background: #5568d3;
+}
+
+/* 表格 */
+.table-container {
+  background: white;
+  border-radius: 12px;
+  overflow: hidden;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+}
+
+.data-table {
+  width: 100%;
+  border-collapse: collapse;
+}
+
+.data-table thead {
+  background: #f9fafb;
+}
+
+.data-table th {
+  padding: 14px 16px;
+  text-align: left;
+  font-size: 13px;
+  font-weight: 600;
+  color: #374151;
+  border-bottom: 1px solid #e5e7eb;
+}
+
+.data-table td {
+  padding: 14px 16px;
+  font-size: 13px;
+  color: #1f2937;
+  border-bottom: 1px solid #f3f4f6;
+}
+
+.data-table tbody tr:hover {
+  background: #f9fafb;
+}
+
+.data-table tbody tr:last-child td {
+  border-bottom: none;
+}
+
+.loading-text {
+  text-align: center;
+  color: #6b7280;
+  padding: 40px !important;
+}
+
+.action-buttons {
+  display: flex;
+  gap: 8px;
+}
+
+/* 弹窗 */
+.modal {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000;
+}
+
+.modal-content {
+  background: white;
+  border-radius: 12px;
+  width: 90%;
+  max-width: 500px;
+  max-height: 90vh;
+  overflow-y: auto;
+  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+}
+
+.modal-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 20px 24px;
+  border-bottom: 1px solid #e5e7eb;
+}
+
+.modal-header h3 {
+  font-size: 18px;
+  font-weight: 600;
+  color: #1f2937;
+}
+
+.modal-close {
+  background: none;
+  border: none;
+  font-size: 24px;
+  color: #6b7280;
+  cursor: pointer;
+  padding: 0;
+  width: 32px;
+  height: 32px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border-radius: 6px;
+  transition: all 0.2s;
+}
+
+.modal-close:hover {
+  background: #f3f4f6;
+  color: #1f2937;
+}
+
+.modal-form {
+  padding: 24px;
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+}
+
+.modal-actions {
+  display: flex;
+  gap: 12px;
+  justify-content: flex-end;
+  margin-top: 8px;
+}
+
+/* 素材管理页面(网盘形式) */
+.store-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 16px 24px;
+  background: white;
+  border-bottom: 1px solid #e5e5e5;
+  flex-shrink: 0;
+}
+
+.store-breadcrumb {
+  display: flex;
+  align-items: center;
+  flex: 1;
+  overflow-x: auto;
+  gap: 8px;
+}
+
+.store-breadcrumb .breadcrumb-item {
+  display: inline-flex;
+  align-items: center;
+  padding: 6px 12px;
+  color: #666;
+  font-size: 14px;
+  white-space: nowrap;
+  cursor: pointer;
+  border-radius: 4px;
+  transition: all 0.2s;
+  background: none;
+  border: none;
+}
+
+.store-breadcrumb .breadcrumb-item:hover {
+  background: #f0f0f0;
+  color: #333;
+}
+
+.store-breadcrumb .breadcrumb-item.active {
+  color: #333;
+  font-weight: 500;
+  background: #f0f0f0;
+}
+
+.store-breadcrumb .breadcrumb-separator {
+  color: #999;
+  margin: 0 4px;
+}
+
+.store-header-right {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  flex-shrink: 0;
+}
+
+.btn-new-folder {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  padding: 8px 16px;
+  background: #10b981;
+  color: white;
+  border: none;
+  border-radius: 8px;
+  font-size: 14px;
+  font-weight: 500;
+  cursor: pointer;
+  transition: all 0.2s ease;
+  box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3);
+}
+
+.btn-new-folder:hover {
+  background: #059669;
+  transform: translateY(-1px);
+  box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
+}
+
+.btn-new-folder:active {
+  transform: translateY(0);
+}
+
+.btn-upload {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  padding: 8px 16px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+  border: none;
+  border-radius: 8px;
+  font-size: 14px;
+  font-weight: 500;
+  cursor: pointer;
+  transition: all 0.2s ease;
+  box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
+}
+
+.btn-upload:hover {
+  transform: translateY(-1px);
+  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
+}
+
+.btn-upload:active {
+  transform: translateY(0);
+}
+
+.btn-upload svg {
+  flex-shrink: 0;
+}
+
+.search-box {
+  display: flex;
+  align-items: center;
+  position: relative;
+  background: #f5f5f5;
+  border-radius: 20px;
+  padding: 0 12px;
+  transition: all 0.2s;
+}
+
+.search-box:focus-within {
+  background: white;
+  box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
+}
+
+.search-icon {
+  color: #999;
+  flex-shrink: 0;
+}
+
+.search-box input {
+  width: 180px;
+  padding: 8px 8px;
+  border: none;
+  background: transparent;
+  font-size: 14px;
+  outline: none;
+  color: #333;
+}
+
+.search-box input::placeholder {
+  color: #999;
+}
+
+.search-clear {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: none;
+  border: none;
+  color: #999;
+  cursor: pointer;
+  padding: 4px;
+  flex-shrink: 0;
+}
+
+.search-clear:hover {
+  color: #666;
+}
+
+.store-file-list-container {
+  position: relative;
+  flex: 1;
+  overflow-y: auto;
+  padding: 24px;
+  background: white;
+}
+
+.file-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 16px;
+  padding: 8px;
+}
+
+.file-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  width: calc((100% - 32px) / 3);
+  min-width: 120px;
+  max-width: 180px;
+  padding: 16px 8px;
+  border-radius: 8px;
+  cursor: pointer;
+  transition: background 0.2s;
+  user-select: none;
+  position: relative;
+}
+
+.file-item:hover {
+  background: #f5f5f5;
+}
+
+.file-icon {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  width: 100%;
+  max-width: 140px;
+  aspect-ratio: 1;
+  margin: 0 auto 8px;
+}
+
+.file-icon svg {
+  width: 60%;
+  height: 60%;
+}
+
+.file-icon.folder-icon {
+  color: #ffc107;
+}
+
+.file-thumbnail {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  width: 100%;
+  max-width: 140px;
+  aspect-ratio: 1;
+  margin: 0 auto 8px;
+  border-radius: 6px;
+  overflow: hidden;
+  background-color: #ffffff;
+  background-image: 
+    linear-gradient(45deg, #e0e0e0 25%, transparent 25%),
+    linear-gradient(-45deg, #e0e0e0 25%, transparent 25%),
+    linear-gradient(45deg, transparent 75%, #e0e0e0 75%),
+    linear-gradient(-45deg, transparent 75%, #e0e0e0 75%);
+  background-size: 16px 16px;
+  background-position: 0 0, 0 8px, 8px -8px, -8px 0px;
+  border: 1px solid #e5e5e5;
+  position: relative;
+}
+
+.file-thumbnail img {
+  width: 100%;
+  height: 100%;
+  object-fit: contain;
+}
+
+.file-name {
+  width: 100%;
+  font-size: 13px;
+  text-align: center;
+  word-break: break-all;
+  color: #333;
+  line-height: 1.4;
+  max-height: 2.8em;
+  overflow: hidden;
+  display: -webkit-box;
+  -webkit-line-clamp: 2;
+  line-clamp: 2;
+  -webkit-box-orient: vertical;
+  padding: 2px 4px;
+  border-radius: 4px;
+  cursor: text;
+}
+
+.file-name:hover {
+  background: rgba(24, 144, 255, 0.1);
+}
+
+/* 重命名输入框 */
+.rename-input {
+  width: 100%;
+  font-size: 13px;
+  text-align: center;
+  padding: 2px 4px;
+  border: 1px solid #1890ff;
+  border-radius: 4px;
+  outline: none;
+  background: white;
+}
+
+.empty-state {
+  display: none;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  height: 100%;
+  color: #999;
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+}
+
+.empty-state.show {
+  display: flex;
+}
+
+.empty-state svg {
+  margin-bottom: 24px;
+}
+
+.empty-state p {
+  font-size: 16px;
+  margin-bottom: 8px;
+}
+
+.loading {
+  display: none;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(255, 255, 255, 0.9);
+  z-index: 20;
+}
+
+.loading.show {
+  display: flex;
+}
+
+.spinner {
+  width: 40px;
+  height: 40px;
+  border: 3px solid #f3f3f3;
+  border-top: 3px solid #1890ff;
+  border-radius: 50%;
+  animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+  0% { transform: rotate(0deg); }
+  100% { transform: rotate(360deg); }
+}
+
+.loading p {
+  margin-top: 16px;
+  color: #666;
+  font-size: 14px;
+}
+
+.loading-text {
+  text-align: center;
+  color: #6b7280;
+  padding: 40px;
+  width: 100%;
+}
+
+/* 定价管理页面 */
+.pricing-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+  gap: 20px;
+  padding: 8px;
+}
+
+.pricing-item {
+  background: white;
+  border-radius: 12px;
+  padding: 16px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+  transition: all 0.2s;
+}
+
+.pricing-item:hover {
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+  transform: translateY(-2px);
+}
+
+.pricing-preview {
+  width: 100%;
+  aspect-ratio: 1;
+  border-radius: 8px;
+  overflow: hidden;
+  background-color: #ffffff;
+  background-image: 
+    linear-gradient(45deg, #e0e0e0 25%, transparent 25%),
+    linear-gradient(-45deg, #e0e0e0 25%, transparent 25%),
+    linear-gradient(45deg, transparent 75%, #e0e0e0 75%),
+    linear-gradient(-45deg, transparent 75%, #e0e0e0 75%);
+  background-size: 16px 16px;
+  background-position: 0 0, 0 8px, 8px -8px, -8px 0px;
+  border: 1px solid #e5e5e5;
+  margin-bottom: 12px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.pricing-preview img {
+  width: 100%;
+  height: 100%;
+  object-fit: contain;
+}
+
+.no-preview {
+  color: #999;
+  font-size: 14px;
+  padding: 20px;
+}
+
+.pricing-info {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.pricing-name {
+  font-size: 14px;
+  font-weight: 600;
+  color: #1f2937;
+  text-align: center;
+}
+
+.pricing-category {
+  font-size: 12px;
+  color: #6b7280;
+  text-align: center;
+}
+
+.pricing-price-input {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+  margin-top: 8px;
+}
+
+.pricing-price-input label {
+  font-size: 12px;
+  color: #6b7280;
+  font-weight: 500;
+}
+
+.price-input {
+  width: 100%;
+  padding: 8px 12px;
+  border: 1px solid #e5e5e5;
+  border-radius: 6px;
+  font-size: 14px;
+  transition: border-color 0.2s;
+}
+
+.price-input:focus {
+  outline: none;
+  border-color: #667eea;
+  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
+}
+
+/* 上传进度和预览 */
+.upload-progress-container {
+  margin-top: 16px;
+  padding: 16px;
+  background: #f9fafb;
+  border-radius: 8px;
+  border: 1px solid #e5e5e5;
+}
+
+.upload-progress-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 12px;
+}
+
+.upload-progress-header span:first-child {
+  font-size: 14px;
+  font-weight: 600;
+  color: #1f2937;
+}
+
+.upload-progress-header span:last-child {
+  font-size: 14px;
+  font-weight: 600;
+  color: #667eea;
+}
+
+.upload-progress-bar {
+  width: 100%;
+  height: 8px;
+  background: #e5e5e5;
+  border-radius: 4px;
+  overflow: hidden;
+  margin-bottom: 8px;
+}
+
+.upload-progress-fill {
+  height: 100%;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  transition: width 0.3s;
+  border-radius: 4px;
+}
+
+.upload-progress-text {
+  font-size: 12px;
+  color: #6b7280;
+  text-align: center;
+}
+
+.upload-success-preview {
+  margin-top: 16px;
+  padding: 16px;
+  background: #f0fdf4;
+  border-radius: 8px;
+  border: 1px solid #86efac;
+}
+
+.preview-header {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  margin-bottom: 12px;
+  font-size: 14px;
+  font-weight: 600;
+  color: #16a34a;
+}
+
+.preview-image-container {
+  width: 100%;
+  aspect-ratio: 1;
+  border-radius: 8px;
+  overflow: hidden;
+  background: white;
+  border: 1px solid #e5e5e5;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  position: relative;
+}
+
+.preview-image {
+  width: 100%;
+  height: 100%;
+  object-fit: contain;
+  display: none;
+}
+
+.preview-loading {
+  color: #6b7280;
+  font-size: 14px;
+}

+ 138 - 0
admin/css/resource-manager/confirm-view.css

@@ -0,0 +1,138 @@
+/* 确认对话框样式(复制自 client/css/confirm-view.css) */
+
+.global-confirm-overlay {
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    background: rgba(0, 0, 0, 0.6);
+    display: none;
+    align-items: center;
+    justify-content: center;
+    z-index: 100001;
+    backdrop-filter: blur(4px);
+}
+
+.global-confirm-overlay.show {
+    display: flex;
+    animation: overlayFadeIn 0.3s ease-out;
+}
+
+@keyframes overlayFadeIn {
+    from {
+        background: rgba(0, 0, 0, 0);
+        backdrop-filter: blur(0);
+    }
+    to {
+        background: rgba(0, 0, 0, 0.6);
+        backdrop-filter: blur(4px);
+    }
+}
+
+.global-confirm-dialog {
+    background: white;
+    border-radius: 16px;
+    box-shadow: 
+        0 20px 60px rgba(0, 0, 0, 0.3),
+        0 0 0 1px rgba(0, 0, 0, 0.05);
+    min-width: 420px;
+    max-width: 520px;
+    animation: confirmSlideIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
+}
+
+@keyframes confirmSlideIn {
+    from {
+        opacity: 0;
+        transform: translateY(-30px) scale(0.95);
+    }
+    to {
+        opacity: 1;
+        transform: translateY(0) scale(1);
+    }
+}
+
+.confirm-content {
+    padding: 36px 32px 24px;
+}
+
+.confirm-message {
+    font-size: 17px;
+    line-height: 1.7;
+    color: #1f2937;
+    white-space: pre-wrap;
+    font-weight: 400;
+    letter-spacing: 0.2px;
+}
+
+.confirm-actions {
+    padding: 0 32px 32px;
+    display: flex;
+    gap: 12px;
+    justify-content: flex-end;
+}
+
+.confirm-btn {
+    padding: 12px 28px;
+    border: none;
+    border-radius: 8px;
+    font-size: 15px;
+    cursor: pointer;
+    transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+    font-weight: 500;
+    letter-spacing: 0.3px;
+    min-width: 90px;
+}
+
+.confirm-cancel {
+    background: #f3f4f6;
+    color: #6b7280;
+    border: 1px solid #e5e7eb;
+}
+
+.confirm-cancel:hover {
+    background: #e5e7eb;
+    color: #4b5563;
+    border-color: #d1d5db;
+}
+
+.confirm-cancel:active {
+    transform: scale(0.98);
+}
+
+.confirm-ok {
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    color: white;
+    box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
+}
+
+.confirm-ok:hover {
+    box-shadow: 0 6px 16px rgba(102, 126, 234, 0.5);
+    transform: translateY(-1px);
+}
+
+.confirm-ok:active {
+    transform: translateY(0) scale(0.98);
+}
+
+/* 输入框样式 */
+.prompt-input {
+    width: 100%;
+    margin-top: 16px;
+    padding: 12px 16px;
+    font-size: 15px;
+    border: 2px solid #e5e7eb;
+    border-radius: 8px;
+    outline: none;
+    transition: border-color 0.2s, box-shadow 0.2s;
+}
+
+.prompt-input:focus {
+    border-color: #667eea;
+    box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
+}
+
+.prompt-input::placeholder {
+    color: #9ca3af;
+}
+

+ 497 - 0
admin/css/resource-manager/disk.css

@@ -0,0 +1,497 @@
+/* 素材管理 - 磁盘样式(复制自 client/css/disk/disk.css) */
+
+/* 全局样式 */
+* {
+    margin: 0;
+    padding: 0;
+    box-sizing: border-box;
+}
+
+body {
+    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif;
+    background: #f5f5f5;
+    color: #333;
+}
+
+/* 主容器 */
+.disk-container {
+    display: flex;
+    flex-direction: column;
+    height: 100vh;
+    background: white;
+}
+
+/* 文件列表容器 */
+.file-list-container {
+    position: relative;
+    flex: 1;
+    overflow-y: auto;
+    padding: 24px;
+}
+
+/* 拖拽提示 */
+.drop-hint {
+    display: none;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    position: absolute;
+    top: 24px;
+    left: 24px;
+    right: 24px;
+    bottom: 24px;
+    background: rgba(24, 144, 255, 0.05);
+    border: 2px dashed #1890ff;
+    border-radius: 8px;
+    color: #1890ff;
+    z-index: 10;
+    pointer-events: none;
+}
+
+.drop-hint svg {
+    margin-bottom: 16px;
+    opacity: 0.6;
+}
+
+.drop-hint p {
+    font-size: 16px;
+    font-weight: 500;
+}
+
+.file-list-container.drag-over .drop-hint {
+    display: flex;
+}
+
+/* 文件列表 */
+.file-list {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 16px;
+    padding: 8px;
+}
+
+/* 文件项 */
+.file-item {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    width: calc((100% - 32px) / 3);
+    min-width: 120px;
+    max-width: 180px;
+    padding: 16px 8px;
+    border-radius: 8px;
+    cursor: pointer;
+    transition: background 0.2s;
+    user-select: none;
+    position: relative;
+}
+
+.file-item:hover {
+    background: #f5f5f5;
+}
+
+.file-item.selected {
+    background: rgba(82, 196, 26, 0.1);
+}
+
+.file-item.dragging {
+    opacity: 0.5;
+    background: #e6f7ff;
+}
+
+.file-item.cut {
+    opacity: 0.6;
+    filter: grayscale(0.3);
+}
+
+.file-item.drag-target {
+    background: rgba(24, 144, 255, 0.15);
+    outline: 2px dashed #1890ff;
+    outline-offset: -2px;
+}
+
+/* 拖拽排序指示器 */
+.file-item.drag-over-left::before,
+.file-item.drag-over-right::after {
+    content: '';
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    width: 3px;
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    border-radius: 2px;
+    box-shadow: 0 0 8px rgba(102, 126, 234, 0.6);
+}
+
+.file-item.drag-over-left::before {
+    left: -8px;
+}
+
+.file-item.drag-over-right::after {
+    right: -8px;
+}
+
+/* 选中状态勾选标记 */
+.file-item .check-mark {
+    position: absolute;
+    top: 4px;
+    left: 4px;
+    width: 20px;
+    height: 20px;
+    background: #52c41a;
+    border-radius: 50%;
+    display: none;
+    align-items: center;
+    justify-content: center;
+    color: white;
+    z-index: 5;
+}
+
+.file-item.selected .check-mark {
+    display: flex;
+}
+
+/* 框选区域 */
+.selection-box {
+    position: fixed;
+    border: 1px solid #1890ff;
+    background: rgba(24, 144, 255, 0.1);
+    pointer-events: none;
+    z-index: 1000;
+    display: none;
+}
+
+.file-icon {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    width: 100%;
+    max-width: 140px;
+    aspect-ratio: 1;
+    margin: 0 auto 8px;
+}
+
+.file-icon svg {
+    width: 60%;
+    height: 60%;
+}
+
+/* 图片缩略图 */
+.file-thumbnail {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    width: 100%;
+    max-width: 140px;
+    aspect-ratio: 1;
+    margin: 0 auto 8px;
+    border-radius: 6px;
+    overflow: hidden;
+    /* 棋盘格背景用于显示透明部分 */
+    background-color: #ffffff;
+    background-image: 
+        linear-gradient(45deg, #e0e0e0 25%, transparent 25%),
+        linear-gradient(-45deg, #e0e0e0 25%, transparent 25%),
+        linear-gradient(45deg, transparent 75%, #e0e0e0 75%),
+        linear-gradient(-45deg, transparent 75%, #e0e0e0 75%);
+    background-size: 16px 16px;
+    background-position: 0 0, 0 8px, 8px -8px, -8px 0px;
+    border: 1px solid #e5e5e5;
+    position: relative;
+}
+
+.file-thumbnail img {
+    width: 100%;
+    height: 100%;
+    object-fit: contain;
+}
+
+/* 文件夹预览徽章 */
+.folder-preview {
+    border: 2px solid #fbbf24;
+    background: #ffffff !important;
+    background-image: none !important;
+}
+
+.folder-badge {
+    position: absolute;
+    bottom: 4px;
+    right: 4px;
+    width: 24px;
+    height: 24px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    background: rgba(255, 255, 255, 0.95);
+    border-radius: 6px;
+    font-size: 14px;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+}
+
+/* 文件夹图标 */
+.folder-icon {
+    color: #ffc107;
+}
+
+/* 文件图标 */
+.file-icon-default {
+    color: #90caf9;
+}
+
+.file-icon-image {
+    color: #81c784;
+}
+
+.file-icon-video {
+    color: #e57373;
+}
+
+.file-icon-audio {
+    color: #ba68c8;
+}
+
+.file-icon-zip {
+    color: #ff8a65;
+}
+
+.file-icon-document {
+    color: #64b5f6;
+}
+
+/* 文件名 */
+.file-name {
+    width: 100%;
+    font-size: 13px;
+    text-align: center;
+    word-break: break-all;
+    color: #333;
+    line-height: 1.4;
+    max-height: 2.8em;
+    overflow: hidden;
+    display: -webkit-box;
+    -webkit-line-clamp: 2;
+    line-clamp: 2;
+    -webkit-box-orient: vertical;
+    padding: 2px 4px;
+    border-radius: 4px;
+    cursor: text;
+}
+
+.file-name:hover {
+    background: rgba(24, 144, 255, 0.1);
+}
+
+/* 重命名输入框 */
+.rename-input {
+    width: 100%;
+    font-size: 13px;
+    text-align: center;
+    padding: 2px 4px;
+    border: 1px solid #1890ff;
+    border-radius: 4px;
+    outline: none;
+    background: white;
+}
+
+/* 文件信息 */
+.file-info {
+    font-size: 11px;
+    color: #999;
+    margin-top: 4px;
+}
+
+/* 空状态 */
+.empty-state {
+    display: none;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    height: 100%;
+    color: #999;
+}
+
+.empty-state svg {
+    margin-bottom: 24px;
+}
+
+.empty-state p {
+    font-size: 16px;
+    margin-bottom: 8px;
+}
+
+.empty-state .hint {
+    font-size: 13px;
+    color: #bbb;
+}
+
+.empty-state.show {
+    display: flex;
+}
+
+/* 加载状态 */
+.loading {
+    display: none;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    background: rgba(255, 255, 255, 0.9);
+    z-index: 20;
+}
+
+.loading.show {
+    display: flex;
+}
+
+.spinner {
+    width: 40px;
+    height: 40px;
+    border: 3px solid #f3f3f3;
+    border-top: 3px solid #1890ff;
+    border-radius: 50%;
+    animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+    0% { transform: rotate(0deg); }
+    100% { transform: rotate(360deg); }
+}
+
+.loading p {
+    margin-top: 16px;
+    color: #666;
+    font-size: 14px;
+}
+
+/* 上传进度 */
+.upload-progress {
+    display: none;
+    position: fixed;
+    bottom: 24px;
+    right: 24px;
+    background: white;
+    border-radius: 8px;
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+    padding: 16px;
+    min-width: 300px;
+    max-width: 400px;
+    z-index: 1000;
+}
+
+.upload-progress.show {
+    display: block;
+}
+
+.upload-progress-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 12px;
+}
+
+.upload-progress-title {
+    font-size: 14px;
+    font-weight: 500;
+    color: #333;
+}
+
+.upload-progress-close {
+    background: none;
+    border: none;
+    color: #999;
+    cursor: pointer;
+    padding: 4px;
+    display: flex;
+    align-items: center;
+}
+
+.upload-progress-item {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    padding: 8px 0;
+    border-top: 1px solid #f0f0f0;
+}
+
+.upload-progress-icon {
+    flex-shrink: 0;
+    color: #1890ff;
+}
+
+.upload-progress-info {
+    flex: 1;
+    min-width: 0;
+}
+
+.upload-progress-name {
+    font-size: 13px;
+    color: #333;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+}
+
+.upload-progress-status {
+    font-size: 12px;
+    color: #999;
+    margin-top: 2px;
+}
+
+.upload-progress-bar {
+    width: 100%;
+    height: 4px;
+    background: #f0f0f0;
+    border-radius: 2px;
+    margin-top: 4px;
+    overflow: hidden;
+}
+
+.upload-progress-bar-fill {
+    height: 100%;
+    background: #1890ff;
+    transition: width 0.3s;
+}
+
+.upload-progress-list {
+    max-height: 320px;
+    overflow-y: auto;
+}
+
+/* 响应式 */
+@media (max-width: 768px) {
+    .header {
+        flex-direction: column;
+        gap: 12px;
+        align-items: stretch;
+    }
+    
+    .breadcrumb {
+        justify-content: flex-start;
+    }
+    
+    .actions {
+        justify-content: flex-end;
+    }
+    
+    .file-list {
+        gap: 12px;
+    }
+    
+    .file-item {
+        width: calc((100% - 24px) / 3);
+        min-width: 100px;
+        max-width: none;
+    }
+}
+
+@media (max-width: 650px) {
+    .file-item {
+        width: calc((100% - 12px) / 2);
+        min-width: 100px;
+    }
+}
+

+ 76 - 0
admin/css/resource-manager/hint-view.css

@@ -0,0 +1,76 @@
+/* 提示消息组件样式(复制自 client/css/hint-view.css) */
+
+.hint-view {
+    position: fixed;
+    top: 20px;
+    left: 50%;
+    transform: translateX(-50%);
+    z-index: 1000002;
+    pointer-events: none;
+    opacity: 0;
+    transition: opacity 0.3s ease, transform 0.3s ease;
+}
+
+.hint-view.show {
+    opacity: 1;
+    transform: translateX(-50%) translateY(0);
+    pointer-events: auto;
+}
+
+.hint-view.hide {
+    opacity: 0;
+    transform: translateX(-50%) translateY(-20px);
+    pointer-events: none;
+}
+
+.hint-content {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    padding: 12px 20px;
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    color: white;
+    border-radius: 8px;
+    box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
+    min-width: 200px;
+    max-width: 400px;
+    font-size: 14px;
+    font-weight: 500;
+}
+
+.hint-icon {
+    flex-shrink: 0;
+    width: 20px;
+    height: 20px;
+    color: white;
+}
+
+.hint-message {
+    flex: 1;
+    text-align: center;
+}
+
+/* 成功提示样式 */
+.hint-view.success .hint-content {
+    background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%);
+    box-shadow: 0 4px 12px rgba(82, 196, 26, 0.4);
+}
+
+/* 错误提示样式 */
+.hint-view.error .hint-content {
+    background: linear-gradient(135deg, #ff4d4f 0%, #cf1322 100%);
+    box-shadow: 0 4px 12px rgba(255, 77, 79, 0.4);
+}
+
+/* 警告提示样式 */
+.hint-view.warning .hint-content {
+    background: linear-gradient(135deg, #faad14 0%, #d48806 100%);
+    box-shadow: 0 4px 12px rgba(250, 173, 20, 0.4);
+}
+
+/* 信息提示样式 */
+.hint-view.info .hint-content {
+    background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
+    box-shadow: 0 4px 12px rgba(24, 144, 255, 0.4);
+}
+

+ 59 - 0
admin/css/resource-manager/right-click-menu.css

@@ -0,0 +1,59 @@
+/* 右键菜单样式(复制自 client/css/disk/right-click-menu.css) */
+
+.context-menu {
+    position: fixed;
+    display: none;
+    flex-direction: column;
+    min-width: 160px;
+    background: white;
+    border: 1px solid rgba(0, 0, 0, 0.1);
+    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
+    border-radius: 8px;
+    padding: 4px;
+    z-index: 1200;
+}
+
+.context-menu.show {
+    display: flex;
+}
+
+.context-menu-item {
+    display: flex;
+    align-items: center;
+    justify-content: flex-start;
+    gap: 8px;
+    width: 100%;
+    padding: 8px 12px;
+    background: transparent;
+    border: none;
+    border-radius: 6px;
+    font-size: 14px;
+    color: #333;
+    cursor: pointer;
+    transition: background 0.2s, color 0.2s;
+}
+
+.context-menu-item:hover {
+    background: rgba(24, 144, 255, 0.1);
+    color: #1890ff;
+}
+
+.context-menu-item:disabled {
+    color: #bbb;
+    cursor: not-allowed;
+}
+
+.context-menu-item.danger {
+    color: #ff4d4f;
+}
+
+.context-menu-item.danger:hover {
+    background: rgba(255, 77, 79, 0.1);
+    color: #ff4d4f;
+}
+
+.context-menu-divider {
+    height: 1px;
+    background: rgba(0, 0, 0, 0.08);
+    margin: 4px 8px;
+}

+ 198 - 0
admin/css/resource-manager/tool-bar.css

@@ -0,0 +1,198 @@
+/* 素材管理工具栏样式(复制自 client/css/disk/tool-bar.css) */
+
+/* 头部 */
+.header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 16px 24px;
+    background: white;
+    border-bottom: 1px solid #e5e5e5;
+    flex-shrink: 0;
+}
+
+/* 面包屑导航 */
+.breadcrumb {
+    display: flex;
+    align-items: center;
+    flex: 1;
+    overflow-x: auto;
+    gap: 8px;
+}
+
+.breadcrumb-item {
+    display: inline-flex;
+    align-items: center;
+    padding: 6px 12px;
+    color: #666;
+    font-size: 14px;
+    white-space: nowrap;
+    cursor: pointer;
+    border-radius: 4px;
+    transition: all 0.2s;
+}
+
+.breadcrumb-item:hover {
+    background: #f0f0f0;
+    color: #333;
+}
+
+.breadcrumb-item.active {
+    color: #333;
+    font-weight: 500;
+}
+
+.breadcrumb-item:not(:last-child)::after {
+    content: '/';
+    margin-left: 8px;
+    color: #999;
+}
+
+/* 头部右侧区域 */
+.header-right {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    flex-shrink: 0;
+}
+
+/* 上传按钮 */
+.btn-upload {
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    padding: 8px 16px;
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    color: white;
+    border: none;
+    border-radius: 8px;
+    font-size: 14px;
+    font-weight: 500;
+    cursor: pointer;
+    transition: all 0.2s ease;
+    box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
+}
+
+.btn-upload:hover {
+    transform: translateY(-1px);
+    box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
+}
+
+.btn-upload:active {
+    transform: translateY(0);
+}
+
+.btn-upload svg {
+    flex-shrink: 0;
+}
+
+/* 搜索框 */
+.search-box {
+    display: flex;
+    align-items: center;
+    position: relative;
+    background: #f5f5f5;
+    border-radius: 20px;
+    padding: 0 12px;
+    transition: all 0.2s;
+}
+
+.search-box:focus-within {
+    background: white;
+    box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
+}
+
+.search-icon {
+    color: #999;
+    flex-shrink: 0;
+}
+
+.search-box input {
+    width: 180px;
+    padding: 8px 8px;
+    border: none;
+    background: transparent;
+    font-size: 14px;
+    outline: none;
+    color: #333;
+}
+
+.search-box input::placeholder {
+    color: #999;
+}
+
+.search-clear {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    background: none;
+    border: none;
+    color: #999;
+    cursor: pointer;
+    padding: 4px;
+    flex-shrink: 0;
+}
+
+.search-clear:hover {
+    color: #666;
+}
+
+/* 选中操作栏 */
+.selection-bar {
+    display: none;
+    justify-content: space-between;
+    align-items: center;
+    padding: 10px 24px;
+    background: #f7f9fc;
+    border-bottom: 1px solid #e5e5e5;
+    flex-shrink: 0;
+}
+
+.selection-bar.show {
+    display: flex;
+}
+
+.selection-count {
+    font-size: 14px;
+    color: #666;
+}
+
+.selection-actions {
+    display: flex;
+    align-items: center;
+    gap: 4px;
+}
+
+.btn-action {
+    display: flex;
+    align-items: center;
+    gap: 4px;
+    padding: 6px 12px;
+    background: transparent;
+    color: #333;
+    border: none;
+    border-radius: 4px;
+    font-size: 14px;
+    cursor: pointer;
+    transition: all 0.2s;
+}
+
+.btn-action:hover {
+    background: rgba(24, 144, 255, 0.1);
+    color: #1890ff;
+}
+
+.btn-action svg {
+    color: inherit;
+}
+
+.btn-action.btn-danger {
+    color: #333;
+    background: transparent;
+}
+
+.btn-action.btn-danger:hover {
+    background: rgba(255, 77, 79, 0.1);
+    color: #ff4d4f;
+}
+

+ 250 - 0
admin/index.html

@@ -0,0 +1,250 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>管理后台</title>
+  <link rel="icon" type="image/png" href="static/favicon.png">
+  <link rel="stylesheet" href="css/admin.css">
+</head>
+<body>
+  <!-- 登录页面 -->
+  <div id="loginPage" class="login-page">
+    <div class="login-container">
+      <div class="login-header">
+        <h1>管理后台</h1>
+        <p>管理员登录</p>
+      </div>
+      <form id="loginForm" class="login-form">
+        <div class="form-group">
+          <label>用户名</label>
+          <input type="text" id="adminUsername" placeholder="请输入管理员用户名" required>
+        </div>
+        <div class="form-group">
+          <label>密码</label>
+          <input type="password" id="adminPassword" placeholder="请输入密码" required>
+        </div>
+        <button type="submit" class="login-btn">登录</button>
+        <div id="loginError" class="error-message" style="display: none;"></div>
+      </form>
+    </div>
+  </div>
+
+  <!-- 主界面 -->
+  <div id="mainPage" class="main-page" style="display: none;">
+    <!-- 侧边栏 -->
+    <aside class="sidebar">
+      <div class="sidebar-header">
+        <h2>管理后台</h2>
+      </div>
+      <nav class="sidebar-nav">
+        <a href="#" class="nav-item active" data-page="users">
+          <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
+            <path d="M10 10C12.7614 10 15 7.76142 15 5C15 2.23858 12.7614 0 10 0C7.23858 0 5 2.23858 5 5C5 7.76142 7.23858 10 10 10Z" fill="currentColor"/>
+            <path d="M10 12C5.58172 12 2 13.7909 2 16V20H18V16C18 13.7909 14.4183 12 10 12Z" fill="currentColor"/>
+          </svg>
+          <span>用户管理</span>
+        </a>
+        <a href="#" class="nav-item" data-page="resource-manager">
+          <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
+            <path d="M18 4H2L0 8L2 12H18L20 8L18 4Z" fill="currentColor"/>
+            <path d="M2 12L4 20H16L18 12" fill="currentColor"/>
+          </svg>
+          <span>素材管理</span>
+        </a>
+        <a href="#" class="nav-item" data-page="pricing">
+          <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
+            <path d="M10 2L3 7V18H17V7L10 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+            <path d="M10 10V18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
+            <path d="M6 10H14" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
+          </svg>
+          <span>素材定价</span>
+        </a>
+        <a href="#" class="nav-item" data-page="product-pricing">
+          <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
+            <path d="M10 2L3 7V18H17V7L10 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+            <path d="M10 10V18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
+            <path d="M6 10H14" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
+          </svg>
+          <span>商品定价</span>
+        </a>
+        <a href="#" class="nav-item" data-page="currency">
+          <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
+            <circle cx="10" cy="10" r="8" stroke="currentColor" stroke-width="2"/>
+            <path d="M10 5V15M7 8H13M7 12H13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
+          </svg>
+          <span>充值与货币</span>
+        </a>
+      </nav>
+      <div class="sidebar-footer">
+        <button id="logoutBtn" class="logout-btn">
+          <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
+            <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
+            <polyline points="16 17 21 12 16 7"/>
+            <line x1="21" y1="12" x2="9" y2="12"/>
+          </svg>
+          <span>退出登录</span>
+        </button>
+      </div>
+    </aside>
+
+    <!-- 主内容区 -->
+    <main class="main-content">
+      <header class="main-header">
+        <h1 id="pageTitle">用户管理</h1>
+        <div class="header-actions">
+          <span class="admin-name" id="adminName">管理员</span>
+        </div>
+      </header>
+
+      <div class="content-area" id="contentArea">
+        <!-- 页面容器 - 使用 iframe 加载各个页面 -->
+        <iframe id="pageFrame" src="page/users/users.html" frameborder="0" style="width: 100%; height: 100%; border: none;"></iframe>
+      </div>
+    </main>
+  </div>
+
+  <!-- 编辑用户弹窗 -->
+  <div id="editUserModal" class="modal" style="display: none;">
+    <div class="modal-content">
+      <div class="modal-header">
+        <h3>编辑用户</h3>
+        <button class="modal-close" id="closeEditUserModal">&times;</button>
+      </div>
+      <form id="editUserForm" class="modal-form">
+        <input type="hidden" id="editUserId">
+        <div class="form-group">
+          <label>用户名</label>
+          <input type="text" id="editUsername" required>
+        </div>
+        <div class="form-group">
+          <label>手机号</label>
+          <input type="text" id="editPhone" required>
+        </div>
+        <div class="form-group">
+          <label>Ani币</label>
+          <input type="number" id="editPoints" min="0" required>
+        </div>
+        <div class="modal-actions">
+          <button type="button" class="btn-secondary" id="cancelEditUser">取消</button>
+          <button type="submit" class="btn-primary">保存</button>
+        </div>
+      </form>
+    </div>
+  </div>
+
+  <!-- 上传素材弹窗 -->
+  <div id="uploadStoreModal" class="modal" style="display: none;">
+    <div class="modal-content">
+      <div class="modal-header">
+        <h3>上传素材</h3>
+        <button class="modal-close" id="closeUploadModal">&times;</button>
+      </div>
+      <form id="uploadStoreForm" class="modal-form">
+        <div class="form-group">
+          <label>上传到分类目录</label>
+          <select id="uploadCategory" required>
+            <option value="">请选择分类目录</option>
+            <!-- 分类选项将通过 JavaScript 动态加载 -->
+          </select>
+          <small class="form-hint">选择素材要上传到的分类目录,文件夹名称将作为资源名称</small>
+        </div>
+        <div class="form-group">
+          <label>资源名称(文件夹名称)</label>
+          <input type="text" id="uploadName" placeholder="例如: player_0001" required>
+          <small class="form-hint">上传的文件夹名称将作为资源名称显示</small>
+        </div>
+        <div class="form-group">
+          <label>上传文件夹</label>
+          <div class="file-upload-area" id="fileUploadArea">
+            <input type="file" id="uploadFiles" webkitdirectory directory multiple required style="display: none;">
+            <div class="file-upload-content">
+              <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
+                <polyline points="17 8 12 3 7 8"></polyline>
+                <line x1="12" y1="3" x2="12" y2="15"></line>
+              </svg>
+              <p class="file-upload-text" id="fileUploadText">点击选择文件夹或拖拽文件夹到此处</p>
+              <p class="file-upload-hint" id="fileUploadHint">未选择任何文件</p>
+            </div>
+          </div>
+          <small class="form-hint">请选择包含PNG序列帧的文件夹</small>
+        </div>
+        
+        <!-- 上传进度 -->
+        <div id="uploadProgressContainer" class="upload-progress-container" style="display: none;">
+          <div class="upload-progress-header">
+            <span>上传进度</span>
+            <span id="uploadProgressPercent">0%</span>
+          </div>
+          <div class="upload-progress-bar">
+            <div class="upload-progress-fill" id="uploadProgressFill"></div>
+          </div>
+          <div class="upload-progress-text" id="uploadProgressText">准备上传...</div>
+        </div>
+
+        <!-- 上传成功预览 -->
+        <div id="uploadSuccessPreview" class="upload-success-preview" style="display: none;">
+          <div class="preview-header">
+            <svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2">
+              <path d="M16 4h2a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path>
+              <rect x="8" y="2" width="4" height="4" rx="1" ry="1"></rect>
+            </svg>
+            <span>上传成功!预览图:</span>
+          </div>
+          <div class="preview-image-container">
+            <img id="previewImage" src="" alt="预览图" class="preview-image">
+            <div class="preview-loading" id="previewLoading">加载预览图...</div>
+          </div>
+        </div>
+
+        <div class="modal-actions">
+          <button type="button" class="btn-secondary" id="cancelUpload">取消</button>
+          <button type="submit" class="btn-primary" id="uploadSubmitBtn">上传</button>
+        </div>
+      </form>
+    </div>
+  </div>
+
+  <!-- 自定义确认对话框 -->
+  <div id="customConfirmModal" class="modal" style="display: none;">
+    <div class="modal-content confirm-modal-content">
+      <div class="modal-header">
+        <h3 id="confirmTitle">确认操作</h3>
+      </div>
+      <div class="modal-body">
+        <p id="confirmMessage"></p>
+        <div id="confirmFileList" class="file-list" style="display: none;">
+          <div class="file-list-header">
+            <span>文件列表 (<span id="fileCount">0</span> 个文件)</span>
+          </div>
+          <div class="file-list-content" id="fileListContent"></div>
+        </div>
+      </div>
+      <div class="modal-actions">
+        <button type="button" class="btn-secondary" id="confirmCancel">取消</button>
+        <button type="button" class="btn-primary" id="confirmOk">确认</button>
+      </div>
+    </div>
+  </div>
+
+  <!-- 自定义提示对话框 -->
+  <div id="customAlertModal" class="modal" style="display: none;">
+    <div class="modal-content alert-modal-content">
+      <div class="modal-header">
+        <h3 id="alertTitle">提示</h3>
+        <button class="modal-close" id="closeAlertModal">&times;</button>
+      </div>
+      <div class="modal-body">
+        <p id="alertMessage"></p>
+      </div>
+      <div class="modal-actions">
+        <button type="button" class="btn-primary" id="alertOk">确定</button>
+      </div>
+    </div>
+  </div>
+
+  <!-- 主脚本 -->
+  <script src="js/admin.js"></script>
+</body>
+</html>

+ 201 - 0
admin/js/admin.js

@@ -0,0 +1,201 @@
+// 管理后台主入口逻辑
+
+(function() {
+  const ADMIN_USERNAME = 'admin';
+  const ADMIN_PASSWORD = '123456'; // 简单密码,生产环境应使用更安全的方式
+
+  let currentPage = 'users';
+  let usersManager = null;
+  let resourceManager = null;
+  let pricingManager = null;
+
+  // 初始化
+  function init() {
+    checkLogin();
+    bindEvents();
+  }
+
+  // 检查登录状态(5小时免登录)
+  function checkLogin() {
+    const loginTime = localStorage.getItem('adminLoginTime');
+    const isLoggedIn = localStorage.getItem('adminLoggedIn') === 'true';
+    
+    if (isLoggedIn && loginTime) {
+      const now = Date.now();
+      const loginTimestamp = parseInt(loginTime, 10);
+      const fiveHours = 5 * 60 * 60 * 1000; // 5小时的毫秒数
+      
+      // 检查是否在5小时内
+      if (now - loginTimestamp < fiveHours) {
+        showMainPage();
+        return;
+      } else {
+        // 超过5小时,清除登录状态
+        localStorage.removeItem('adminLoggedIn');
+        localStorage.removeItem('adminLoginTime');
+      }
+    }
+    
+    showLoginPage();
+  }
+
+  // 显示登录页
+  function showLoginPage() {
+    document.getElementById('loginPage').style.display = 'flex';
+    document.getElementById('mainPage').style.display = 'none';
+  }
+
+  // 显示主页面
+  function showMainPage() {
+    document.getElementById('loginPage').style.display = 'none';
+    document.getElementById('mainPage').style.display = 'flex';
+    initManagers();
+    switchPage('users');
+  }
+
+  // 初始化各个管理器(现在由各个页面自己初始化)
+  function initManagers() {
+    // 管理器现在由各个独立页面初始化
+    // 这里保留函数以保持兼容性
+  }
+
+  // 获取 API 基础 URL(供子页面使用)
+  window.getApiBaseUrl = function() {
+    return getApiBaseUrl();
+  }
+
+  // 绑定事件
+  function bindEvents() {
+    // 登录表单
+    const loginForm = document.getElementById('loginForm');
+    if (loginForm) {
+      loginForm.addEventListener('submit', handleLogin);
+    }
+
+    // 退出登录
+    const logoutBtn = document.getElementById('logoutBtn');
+    if (logoutBtn) {
+      logoutBtn.addEventListener('click', handleLogout);
+    }
+
+    // 导航
+    const navItems = document.querySelectorAll('.nav-item');
+    navItems.forEach(item => {
+      item.addEventListener('click', (e) => {
+        e.preventDefault();
+        const page = item.getAttribute('data-page');
+        switchPage(page);
+      });
+    });
+
+    // 自定义提示对话框
+    const alertOk = document.getElementById('alertOk');
+    const closeAlertModal = document.getElementById('closeAlertModal');
+    if (alertOk) {
+      alertOk.addEventListener('click', () => {
+        document.getElementById('customAlertModal').style.display = 'none';
+      });
+    }
+    if (closeAlertModal) {
+      closeAlertModal.addEventListener('click', () => {
+        document.getElementById('customAlertModal').style.display = 'none';
+      });
+    }
+  }
+
+  // 处理登录
+  function handleLogin(e) {
+    e.preventDefault();
+    
+    const username = document.getElementById('adminUsername').value;
+    const password = document.getElementById('adminPassword').value;
+    const errorDiv = document.getElementById('loginError');
+    
+    if (username === ADMIN_USERNAME && password === ADMIN_PASSWORD) {
+      // 保存登录状态和登录时间戳
+      localStorage.setItem('adminLoggedIn', 'true');
+      localStorage.setItem('adminLoginTime', Date.now().toString());
+      errorDiv.style.display = 'none';
+      showMainPage();
+    } else {
+      errorDiv.textContent = '用户名或密码错误';
+      errorDiv.style.display = 'block';
+    }
+  }
+
+  // 处理退出登录
+  function handleLogout() {
+    if (confirm('确定要退出登录吗?')) {
+      localStorage.removeItem('adminLoggedIn');
+      localStorage.removeItem('adminLoginTime');
+      showLoginPage();
+    }
+  }
+
+  // 切换页面
+  function switchPage(page) {
+    currentPage = page;
+    
+    // 更新导航状态
+    document.querySelectorAll('.nav-item').forEach(item => {
+      item.classList.remove('active');
+      if (item.getAttribute('data-page') === page) {
+        item.classList.add('active');
+      }
+    });
+
+    // 更新标题
+    const titles = {
+      'users': '用户管理',
+      'resource-manager': '素材管理',
+      'pricing': '素材定价',
+      'currency': '充值与货币',
+      'product-pricing': '商品定价'
+    };
+    document.getElementById('pageTitle').textContent = titles[page] || '管理后台';
+
+    // 切换 iframe 的 src
+    const pageFrame = document.getElementById('pageFrame');
+    if (pageFrame) {
+      const pageMap = {
+        'users': 'page/users/users.html',
+        'resource-manager': 'page/resource-manager/resource-manager.html',
+        'pricing': 'page/pricing/pricing.html',
+        'currency': 'page/currency/currency.html',
+        'product-pricing': 'page/product-pricing/product-pricing.html'
+      };
+      const pagePath = pageMap[page] || pageMap['users'];
+      pageFrame.src = pagePath;
+    }
+  }
+
+  // 获取API基础URL
+  function getApiBaseUrl() {
+    // 明确使用 http://localhost:3000 作为API服务器地址
+    return 'http://localhost:3000';
+  }
+
+  // 显示自定义提示
+  window.showCustomAlert = function(message, type = 'info') {
+    const alertModal = document.getElementById('customAlertModal');
+    const alertMessage = document.getElementById('alertMessage');
+    if (alertModal && alertMessage) {
+      alertMessage.textContent = message;
+      alertModal.style.display = 'flex';
+      
+      // 自动关闭
+      setTimeout(() => {
+        alertModal.style.display = 'none';
+      }, 2000);
+    } else {
+      alert(message);
+    }
+  };
+
+  // 页面加载完成后初始化
+  if (document.readyState === 'loading') {
+    document.addEventListener('DOMContentLoaded', init);
+  } else {
+    init();
+  }
+})();

+ 243 - 0
admin/js/currency/currency.js

@@ -0,0 +1,243 @@
+// 充值与货币管理模块
+
+class CurrencyManager {
+  constructor(options = {}) {
+    this.apiBaseUrl = options.apiBaseUrl || 'http://localhost:3000';
+    
+    this.packages = [
+      { points: 100, bonus: 20, price: 5 },
+      { points: 1000, bonus: 200, price: 50 },
+      { points: 10000, bonus: 800, price: 500 }
+    ];
+    
+    // 保存原始值用于比较
+    this.originalPackages = [];
+    
+    this.init();
+  }
+
+  init() {
+    this.bindEvents();
+    this.loadSettings();
+  }
+
+  bindEvents() {
+    const addBtn = document.getElementById('addBtn');
+    if (addBtn) {
+      addBtn.addEventListener('click', () => this.addPackage());
+    }
+  }
+
+  async loadSettings() {
+    try {
+      const response = await fetch(`${this.apiBaseUrl}/api/admin/currency/settings`);
+      if (response.ok) {
+        const result = await response.json();
+        if (result.success && result.packages) {
+          this.packages = result.packages;
+        }
+      }
+    } catch (error) {
+      console.log('[CurrencyManager] 使用默认设置');
+    }
+    
+    // 深拷贝保存原始值
+    this.originalPackages = JSON.parse(JSON.stringify(this.packages));
+    this.render();
+  }
+
+  render() {
+    this.renderPackages();
+    this.renderPreview();
+  }
+
+  renderPackages() {
+    const list = document.getElementById('packageList');
+    if (!list) return;
+
+    list.innerHTML = this.packages.map((pkg, i) => `
+      <div class="package-item" data-index="${i}">
+        <div class="package-index">${i + 1}</div>
+        <div class="package-fields">
+          <div class="package-field">
+            <label>价格</label>
+            <span class="unit">¥</span>
+            <input type="number" value="${pkg.price}" data-field="price" min="0.01" step="0.01">
+          </div>
+          <div class="package-field">
+            <label>基础</label>
+            <input type="number" value="${pkg.points}" data-field="points" min="1">
+            <span class="unit">Ani币</span>
+          </div>
+          <div class="package-field">
+            <label>赠送</label>
+            <input type="number" value="${pkg.bonus}" data-field="bonus" min="0">
+            <span class="unit">Ani币</span>
+          </div>
+          <div class="package-total">= ${pkg.points + pkg.bonus} Ani币</div>
+        </div>
+        <div class="package-actions">
+          <button class="btn-confirm" data-index="${i}" title="确认修改">✓</button>
+          <button class="btn-remove" data-index="${i}" title="删除">×</button>
+        </div>
+      </div>
+    `).join('');
+
+    // 绑定输入事件
+    list.querySelectorAll('input').forEach(input => {
+      input.addEventListener('input', (e) => this.onInputChange(e));
+    });
+
+    // 绑定确认按钮
+    list.querySelectorAll('.btn-confirm').forEach(btn => {
+      btn.addEventListener('click', (e) => {
+        const index = parseInt(e.target.dataset.index);
+        this.savePackage(index);
+      });
+    });
+
+    // 绑定删除按钮
+    list.querySelectorAll('.btn-remove').forEach(btn => {
+      btn.addEventListener('click', (e) => {
+        const index = parseInt(e.target.dataset.index);
+        this.removePackage(index);
+      });
+    });
+  }
+
+  onInputChange(e) {
+    const item = e.target.closest('.package-item');
+    const index = parseInt(item.dataset.index);
+    const field = e.target.dataset.field;
+    const value = parseFloat(e.target.value) || 0;
+    
+    this.packages[index][field] = value;
+    
+    // 更新总计显示
+    const pkg = this.packages[index];
+    const totalEl = item.querySelector('.package-total');
+    if (totalEl) {
+      totalEl.textContent = `= ${pkg.points + pkg.bonus} Ani币`;
+    }
+    
+    // 检查是否有变化,显示确认按钮
+    const confirmBtn = item.querySelector('.btn-confirm');
+    const original = this.originalPackages[index];
+    const hasChange = !original || 
+      pkg.price !== original.price || 
+      pkg.points !== original.points || 
+      pkg.bonus !== original.bonus;
+    
+    if (confirmBtn) {
+      confirmBtn.classList.toggle('show', hasChange);
+    }
+    
+    this.renderPreview();
+  }
+
+  renderPreview() {
+    const preview = document.getElementById('preview');
+    if (!preview) return;
+
+    preview.innerHTML = this.packages.map(pkg => `
+      <div class="preview-card">
+        <div class="points">${pkg.points} Ani币</div>
+        <div class="card-bottom">
+          ${pkg.bonus > 0 ? `<div class="bonus">送 ${pkg.bonus} Ani币</div>` : ''}
+          <div class="price">¥${pkg.price}</div>
+        </div>
+      </div>
+    `).join('');
+  }
+
+  addPackage() {
+    const last = this.packages[this.packages.length - 1] || { points: 100, bonus: 0, price: 10 };
+    const newPkg = { 
+      points: last.points * 2, 
+      bonus: Math.floor(last.bonus * 1.5), 
+      price: last.price * 2 
+    };
+    this.packages.push(newPkg);
+    this.originalPackages.push(null); // 新增的没有原始值
+    this.render();
+    
+    // 显示新增项的确认按钮
+    setTimeout(() => {
+      const items = document.querySelectorAll('.package-item');
+      const lastItem = items[items.length - 1];
+      if (lastItem) {
+        const confirmBtn = lastItem.querySelector('.btn-confirm');
+        if (confirmBtn) confirmBtn.classList.add('show');
+      }
+    }, 0);
+  }
+
+  async removePackage(index) {
+    if (this.packages.length <= 1) {
+      this.showMsg('至少保留一个套餐', 'error');
+      return;
+    }
+    
+    this.packages.splice(index, 1);
+    this.originalPackages.splice(index, 1);
+    
+    // 保存到服务器
+    await this.saveAll();
+    this.render();
+  }
+
+  async savePackage(index) {
+    await this.saveAll();
+    
+    // 更新原始值
+    this.originalPackages[index] = JSON.parse(JSON.stringify(this.packages[index]));
+    
+    // 隐藏确认按钮
+    const items = document.querySelectorAll('.package-item');
+    const item = items[index];
+    if (item) {
+      const confirmBtn = item.querySelector('.btn-confirm');
+      if (confirmBtn) confirmBtn.classList.remove('show');
+    }
+  }
+
+  async saveAll() {
+    try {
+      const response = await fetch(`${this.apiBaseUrl}/api/admin/currency/settings`, {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify({ packages: this.packages })
+      });
+
+      const result = await response.json();
+      
+      if (result.success) {
+        this.showMsg('保存成功', 'success');
+        // 更新所有原始值
+        this.originalPackages = JSON.parse(JSON.stringify(this.packages));
+      } else {
+        this.showMsg('保存失败: ' + (result.message || ''), 'error');
+      }
+    } catch (error) {
+      this.showMsg('保存失败', 'error');
+    }
+  }
+
+  showMsg(text, type) {
+    const msgBox = document.getElementById('msgBox');
+    if (msgBox) {
+      msgBox.textContent = text;
+      msgBox.className = 'msg ' + type;
+      msgBox.style.display = 'block';
+      setTimeout(() => {
+        msgBox.style.display = 'none';
+      }, 2000);
+    }
+  }
+}
+
+if (typeof module !== 'undefined' && module.exports) {
+  module.exports = CurrencyManager;
+} else {
+  window.CurrencyManager = CurrencyManager;
+}

+ 334 - 0
admin/js/index.js

@@ -0,0 +1,334 @@
+// 全局 Loading 控制器
+window.GlobalLoading = (function() {
+  let overlay = null;
+  let loadingText = null;
+  
+  function init() {
+    overlay = document.getElementById('globalLoadingOverlay');
+    loadingText = overlay ? overlay.querySelector('.global-loading-text') : null;
+  }
+  
+  function show(text = '正在处理...') {
+    // console.log('[GlobalLoading] show() 被调用');
+    // console.log('[GlobalLoading]   文本:', text);
+    
+    if (!overlay) {
+      // console.log('[GlobalLoading] → 初始化overlay元素...');
+      init();
+    }
+    
+    if (!overlay) {
+      // console.error('[GlobalLoading] ✗ overlay元素未找到!');
+      return;
+    }
+    
+    // console.log('[GlobalLoading] ✓ overlay元素存在');
+    
+    if (loadingText) {
+      loadingText.textContent = text;
+      // console.log('[GlobalLoading] ✓ 设置Loading文本:', text);
+    }
+    
+    overlay.classList.add('is-visible');
+    // console.log('[GlobalLoading] ✓ 添加is-visible类,Loading应该可见了');
+  }
+  
+  function hide() {
+    // console.log('[GlobalLoading] hide() 被调用');
+    
+    if (!overlay) {
+      // console.error('[GlobalLoading] ✗ overlay元素不存在');
+      return;
+    }
+    
+    overlay.classList.remove('is-visible');
+    // console.log('[GlobalLoading] ✓ 移除is-visible类,Loading已隐藏');
+  }
+  
+  return {
+    show,
+    hide
+  };
+})();
+
+// 全局 Alert 控制器
+window.GlobalAlert = (function() {
+  let alertContainer = null;
+  let alertMessage = null;
+  let hideTimer = null;
+  
+  function init() {
+    alertContainer = document.getElementById('globalAlert');
+    alertMessage = alertContainer ? alertContainer.querySelector('#alertMessage') : null;
+  }
+  
+  function show(text, duration = 1500) {
+    console.log('[GlobalAlert] show() 被调用:', { text, duration });
+    
+    if (!alertContainer) {
+      console.log('[GlobalAlert] 初始化 alertContainer');
+      init();
+    }
+    
+    if (!alertContainer) {
+      console.error('[GlobalAlert] alertContainer 未找到!');
+      return;
+    }
+    
+    if (!alertMessage) {
+      console.error('[GlobalAlert] alertMessage 未找到!');
+      return;
+    }
+    
+    console.log('[GlobalAlert] 设置消息:', text);
+    
+    // 清除之前的自动隐藏定时器
+    if (hideTimer) {
+      clearTimeout(hideTimer);
+      hideTimer = null;
+    }
+    
+    alertMessage.textContent = text;
+    alertContainer.classList.add('show');
+    
+    console.log('[GlobalAlert] 已添加 show 类,alert 应该可见了');
+    
+    // 自动隐藏
+    if (duration > 0) {
+      hideTimer = setTimeout(() => {
+        hide();
+      }, duration);
+    }
+  }
+  
+  function hide() {
+    if (!alertContainer) return;
+    alertContainer.classList.remove('show');
+    if (hideTimer) {
+      clearTimeout(hideTimer);
+      hideTimer = null;
+    }
+  }
+  
+  return {
+    show,
+    hide
+  };
+})();
+
+// 页面管理:负责切换 iframe 中的子页面,并保持与导航栏状态同步
+(function () {
+  console.log('[Index] index.js 已加载');
+  const DEFAULT_PAGE = "store";
+
+  function getPageFrame() {
+    return document.getElementById("pageFrame");
+  }
+
+  function getNavigationFrame() {
+    return document.getElementById("navigationFrame");
+  }
+
+  function switchPage(page) {
+    const frame = getPageFrame();
+    if (!frame) {
+      return;
+    }
+
+    // 只处理实际的页面切换,不处理login/register
+    if (page === "login" || page === "register") {
+      return;
+    }
+
+    switch (page) {
+      case "store":
+        frame.src = "page/store/store.html";
+        break;
+      case "assets":
+      default:
+        frame.src = "page/assets/assets.html";
+        break;
+    }
+
+    // 同步导航栏状态
+    syncNavigationState(page);
+  }
+
+  // 同步导航栏状态
+  function syncNavigationState(page) {
+    const navigationFrame = getNavigationFrame();
+    if (navigationFrame && navigationFrame.contentWindow) {
+      // 使用setTimeout确保iframe已加载
+      setTimeout(() => {
+        navigationFrame.contentWindow.postMessage(
+          { type: "navigation", page },
+          "*"
+        );
+      }, 100);
+    }
+  }
+
+  window.addEventListener("message", (event) => {
+    // 调试:记录所有收到的消息
+    console.log('[Index] 收到message事件:', {
+      origin: event.origin,
+      expectedOrigin: window.location.origin,
+      data: event.data,
+      source: event.source,
+      type: event.data?.type
+    });
+    
+    // 检查 origin(允许同源或 localhost,或者来自 iframe)
+    const isSameOrigin = event.origin === window.location.origin;
+    const isLocalhost = event.origin === 'http://localhost:3000' || 
+                       event.origin === 'http://127.0.0.1:3000' ||
+                       event.origin.startsWith('http://localhost:') ||
+                       event.origin.startsWith('http://127.0.0.1:');
+    // 允许来自同源的 iframe(即使 origin 不完全匹配)
+    const isFromIframe = event.source && event.source !== window;
+    
+    console.log('[Index] Origin 检查:', {
+      eventOrigin: event.origin,
+      windowOrigin: window.location.origin,
+      isSameOrigin,
+      isLocalhost,
+      isFromIframe,
+      willPass: isSameOrigin || isLocalhost || isFromIframe
+    });
+    
+    // 对于 global-alert 消息,放宽 origin 检查(允许来自任何同源 iframe)
+    if (event.data && event.data.type === 'global-alert') {
+      console.log('[Index] 这是 global-alert 消息,放宽 origin 检查');
+      // 允许来自任何 iframe 的消息(只要 source 存在且不是 window 本身)
+      if (isFromIframe || isSameOrigin || isLocalhost) {
+        console.log('[Index] global-alert 消息通过检查');
+      } else {
+        console.warn('[Index] global-alert 消息被 origin 检查过滤:', event.origin);
+        // 即使 origin 不匹配,也允许 global-alert 消息通过(因为来自同源 iframe)
+        console.log('[Index] 但允许通过(来自 iframe)');
+      }
+    } else if (!isSameOrigin && !isLocalhost && !isFromIframe) {
+      console.log('[Index] 消息被 origin 检查过滤:', event.origin);
+      return;
+    }
+    
+    console.log('[Index] 消息通过 origin 检查,继续处理');
+    
+    const { data } = event;
+    if (data && data.type === "navigation" && (data.page === "login" || data.page === "register")) {
+      // console.log('[2-Index] 收到login/register消息');
+      const loginFrame = document.getElementById('loginViewFrame');
+      if (loginFrame) {
+        const mode = data.page === "register" ? "register" : "login";
+        loginFrame.style.display = 'block';
+        // console.log('[3-Index] iframe已显示');
+        
+        const sendMode = () => {
+          if (loginFrame.contentWindow) {
+            loginFrame.contentWindow.postMessage({
+              type: 'open-login-view',
+              mode: mode
+            }, '*');
+            // console.log('[4-Index] 消息已发送到login iframe');
+          } else {
+            setTimeout(sendMode, 100);
+          }
+        };
+        
+        sendMode();
+        
+        const handleLoad = () => {
+          setTimeout(() => {
+            sendMode();
+          }, 50);
+        };
+        
+        if (loginFrame.contentDocument && loginFrame.contentDocument.readyState === 'complete') {
+          handleLoad();
+        } else {
+          loginFrame.addEventListener('load', handleLoad, { once: true });
+        }
+      }
+    } else if (data && data.type === "navigation" && data.page) {
+      switchPage(data.page);
+    }
+    // 注意:global-alert、global-loading、global-confirm 消息不再通过 index.js 处理
+    // 各个 view 现在直接调用父窗口的 GlobalAlert/GlobalLoading/GlobalConfirm
+    else if (data && data.type === "open-export-view") {
+      // 处理打开导出弹出框
+      console.log('[Index] 收到open-export-view消息:', data);
+      if (!data.folderName) {
+        console.error('[Index] 缺少文件夹名称');
+        return;
+      }
+      if (!window.ExportViewManager) {
+        console.error('[Index] ExportViewManager 未初始化');
+        return;
+      }
+      // 直接打开弹出框,传递文件夹名称
+      window.ExportViewManager.show(data.folderName).then((confirmed) => {
+        console.log('[Index] 用户选择:', confirmed ? '确认导出' : '取消');
+        // 如果用户确认,可以在这里处理实际的导出下载逻辑
+        if (confirmed) {
+          // TODO: 处理实际的导出下载逻辑
+          console.log('[Index] 用户确认导出,文件夹:', data.folderName);
+        }
+      }).catch(error => {
+        console.error('[Index] ExportViewManager显示失败:', error);
+      });
+    } else if (data && data.type === "close-login-view") {
+      const loginFrame = document.getElementById('loginViewFrame');
+      if (loginFrame) {
+        loginFrame.style.display = 'none';
+      }
+    } else if (data && data.type === "login-success" && data.user) {
+      // 处理登录成功消息,转发给 navigation iframe 和 pageFrame
+      const navigationFrame = getNavigationFrame();
+      if (navigationFrame && navigationFrame.contentWindow) {
+        navigationFrame.contentWindow.postMessage({
+          type: 'login-success',
+          user: data.user
+        }, '*');
+      }
+      
+      // 也转发给 pageFrame(store 页面)
+      const pageFrame = getPageFrame();
+      if (pageFrame && pageFrame.contentWindow) {
+        pageFrame.contentWindow.postMessage({
+          type: 'login-success',
+          user: data.user
+        }, '*');
+      }
+      
+      // 转发给 assets iframe(如果存在),它会再转发给 disk iframe
+      const assetsFrame = document.getElementById('assetsFrame');
+      if (assetsFrame && assetsFrame.contentWindow) {
+        assetsFrame.contentWindow.postMessage({
+          type: 'login-success',
+          user: data.user
+        }, '*');
+      }
+    } else if (data && data.type === "logout") {
+      // 处理登出消息,转发给所有 iframe
+      const navigationFrame = getNavigationFrame();
+      if (navigationFrame && navigationFrame.contentWindow) {
+        navigationFrame.contentWindow.postMessage({
+          type: 'logout'
+        }, '*');
+      }
+      
+      const pageFrame = getPageFrame();
+      if (pageFrame && pageFrame.contentWindow) {
+        pageFrame.contentWindow.postMessage({
+          type: 'logout'
+        }, '*');
+      }
+    }
+  });
+
+  window.addEventListener("DOMContentLoaded", () => {
+    switchPage(DEFAULT_PAGE);
+    syncNavigationState(DEFAULT_PAGE);
+  });
+})();
+

+ 208 - 0
admin/js/pricing/pricing.js

@@ -0,0 +1,208 @@
+// 定价管理模块
+
+class PricingManager {
+  constructor(options = {}) {
+    this.apiBaseUrl = options.apiBaseUrl || 'http://localhost:3000';
+    this.grid = options.grid || null;
+    
+    this.resources = [];
+    
+    this.init();
+  }
+
+  init() {
+    this.bindEvents();
+    // 默认进入页面时加载一次
+    this.loadResources();
+  }
+
+  bindEvents() {
+    // 刷新按钮
+    const refreshBtn = document.getElementById('refreshPricingBtn');
+    if (refreshBtn) {
+      refreshBtn.addEventListener('click', () => {
+        this.loadResources();
+      });
+    }
+  }
+
+  // 加载资源
+  async loadResources() {
+    if (!this.grid) return;
+    
+    this.grid.innerHTML = '<div class="loading-text">加载中...</div>';
+
+    try {
+      const response = await fetch(`${this.apiBaseUrl}/api/store/resources`);
+      if (!response.ok) {
+        throw new Error(`加载资源列表失败: ${response.status} ${response.statusText}`);
+      }
+      const result = await response.json();
+      
+      if (result.success && result.resources) {
+        this.resources = result.resources;
+        console.log('[PricingManager] 资源数:', this.resources.length, this.resources);
+        this.renderResources();
+      } else {
+        this.grid.innerHTML = '<div class="loading-text">暂无资源数据</div>';
+      }
+    } catch (error) {
+      console.error('[PricingManager] 加载资源失败:', error);
+      const errorMessage = error.message || '未知错误';
+      this.grid.innerHTML = `<div class="loading-text" style="color: #ef4444;">加载失败: ${errorMessage}</div>`;
+    }
+  }
+
+  // 渲染资源
+  renderResources() {
+    if (!this.grid) return;
+    
+    // 使用 grid 布局,紧凑显示
+    this.grid.style.display = 'grid';
+    this.grid.style.gridTemplateColumns = 'repeat(auto-fill, minmax(240px, 1fr))';
+    this.grid.style.gap = '16px';
+    this.grid.style.padding = '20px';
+    this.grid.style.background = '#f9fafb';
+    
+    if (this.resources.length === 0) {
+      this.grid.innerHTML = '<div class="loading-text">暂无资源数据</div>';
+      return;
+    }
+    
+    console.log('[PricingManager] 渲染资源:', this.resources.length);
+    
+    this.grid.innerHTML = this.resources.map(resource => `
+      <div class="pricing-item" style="
+        border: 1px solid #e5e7eb;
+        border-radius: 12px;
+        padding: 12px;
+        display: flex;
+        flex-direction: column;
+        gap: 10px;
+        box-shadow: 0 2px 4px rgba(0,0,0,0.06);
+        background: #fff;
+        transition: transform 0.2s, box-shadow 0.2s;
+        cursor: default;
+      " 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)';">
+        <div class="pricing-preview" style="
+          width: 100%;
+          aspect-ratio: 1;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          background: linear-gradient(135deg, #f7f7f7 0%, #e9e9e9 100%);
+          border-radius: 8px;
+          overflow: hidden;
+          position: relative;
+        ">
+          ${resource.previewUrl ? 
+            `<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>'">` : 
+            '<div style="color:#999;font-size:12px;">无预览</div>'
+          }
+          <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>
+        </div>
+        <div class="pricing-info" style="display:flex;flex-direction:column;gap:8px;">
+          <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>
+          <div class="pricing-price-input" style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
+            <div style="display:flex;align-items:center;gap:6px;">
+              <label style="font-size:12px;color:#6b7280;flex-shrink:0;">价格</label>
+              <input type="text" class="price-input" value="${resource.points || 0}" 
+                     data-path="${resource.path}" 
+                     data-original="${resource.points || 0}"
+                     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;"
+                     onfocus="this.style.borderColor='#667eea';"
+                     onblur="this.style.borderColor='#e5e7eb';"
+                     oninput="this.closest('.pricing-price-input').querySelector('.btn-confirm').style.display = this.value !== this.dataset.original ? 'inline-flex' : 'none';">
+              <span style="font-size:12px;color:#6b7280;">Ani币</span>
+              <button class="btn-confirm" 
+                      onclick="window.pricingManagerInstance.confirmPrice('${resource.path}', this.closest('.pricing-price-input').querySelector('.price-input'))"
+                      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;"
+                      onmouseover="this.style.background='#059669';this.style.transform='scale(1.1)';"
+                      onmouseout="this.style.background='#10b981';this.style.transform='';"
+                      title="确认修改">✓</button>
+            </div>
+          </div>
+        </div>
+      </div>
+    `).join('');
+  }
+
+  // 确认价格修改
+  async confirmPrice(resourcePath, inputEl) {
+    const price = inputEl.value.trim();
+    const priceNum = parseFloat(price);
+    
+    if (isNaN(priceNum) || priceNum < 0) {
+      this.showError('价格必须是大于等于0的数字');
+      return;
+    }
+
+    try {
+      const response = await fetch(`${this.apiBaseUrl}/api/admin/store/update-price`, {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json'
+        },
+        body: JSON.stringify({
+          resourcePath: resourcePath,
+          price: priceNum
+        })
+      });
+
+      const result = await response.json();
+      
+      if (result.success) {
+        // 更新本地数据和原始值
+        const resource = this.resources.find(r => r.path === resourcePath);
+        if (resource) {
+          resource.points = priceNum;
+        }
+        inputEl.dataset.original = priceNum;
+        inputEl.value = priceNum;
+        
+        // 隐藏确认按钮
+        const confirmBtn = inputEl.parentElement.querySelector('.btn-confirm');
+        if (confirmBtn) {
+          confirmBtn.style.display = 'none';
+        }
+        
+        this.showSuccess('价格更新成功');
+      } else {
+        this.showError('价格更新失败: ' + (result.message || '未知错误'));
+      }
+    } catch (error) {
+      console.error('[PricingManager] 更新价格失败:', error);
+      this.showError('价格更新失败,请稍后重试');
+    }
+  }
+
+  // 更新价格(保留兼容)
+  async updatePrice(resourcePath, price) {
+    // 现在由 confirmPrice 处理
+  }
+
+  // 显示错误/成功消息
+  showError(message) {
+    if (window.showCustomAlert) {
+      window.showCustomAlert(message);
+    } else {
+      console.error('[PricingManager] 错误:', message);
+    }
+  }
+
+  showSuccess(message) {
+    if (window.showCustomAlert) {
+      window.showCustomAlert(message, 'success');
+    } else {
+      console.log('[PricingManager] 成功:', message);
+    }
+  }
+}
+
+// 导出
+if (typeof module !== 'undefined' && module.exports) {
+  module.exports = PricingManager;
+} else {
+  window.PricingManager = PricingManager;
+}
+

+ 153 - 0
admin/js/product-pricing/product-pricing.js

@@ -0,0 +1,153 @@
+// 商品定价管理模块
+
+class ProductPricingManager {
+  constructor(options = {}) {
+    this.apiBaseUrl = options.apiBaseUrl || 'http://localhost:3000';
+    
+    // 默认商品配置
+    this.products = [
+      { id: 'vip-matting', name: 'VIP抠图', desc: '使用VIP服务进行图片抠图', price: 0 },
+      { id: 'ai-generate', name: 'AI生图', desc: '使用AI生成图片', price: 0 }
+    ];
+    
+    this.init();
+  }
+
+  init() {
+    this.bindEvents();
+    this.loadSettings();
+  }
+
+  bindEvents() {
+    const productList = document.getElementById('productList');
+    if (productList) {
+      productList.addEventListener('input', (e) => {
+        if (e.target.classList.contains('price-input')) {
+          const productId = e.target.dataset.productId;
+          const confirmBtn = e.target.closest('.product-item').querySelector('.btn-confirm');
+          if (confirmBtn) {
+            confirmBtn.classList.add('show');
+          }
+        }
+      });
+
+      productList.addEventListener('click', (e) => {
+        if (e.target.closest('.btn-confirm')) {
+          const button = e.target.closest('.btn-confirm');
+          const productId = button.dataset.productId;
+          this.saveProduct(productId);
+        }
+      });
+    }
+  }
+
+  async loadSettings() {
+    try {
+      const response = await fetch(`${this.apiBaseUrl}/api/admin/product-pricing/settings`);
+      if (response.ok) {
+        const result = await response.json();
+        if (result.success && result.products) {
+          // 更新产品价格,保持默认产品结构
+          result.products.forEach(serverProduct => {
+            const localProduct = this.products.find(p => p.id === serverProduct.id);
+            if (localProduct) {
+              localProduct.price = serverProduct.price || 0;
+            }
+          });
+        }
+      }
+    } catch (error) {
+      console.log('[ProductPricingManager] 使用默认设置');
+    }
+    
+    this.render();
+  }
+
+  render() {
+    const list = document.getElementById('productList');
+    if (!list) return;
+
+    list.innerHTML = this.products.map(product => `
+      <div class="product-item" data-product-id="${product.id}">
+        <div class="product-info">
+          <div class="product-name">${product.name}</div>
+          <div class="product-desc">${product.desc}</div>
+        </div>
+        <div class="product-price">
+          <span class="price-label">价格</span>
+          <div class="price-input-wrapper">
+            <input type="text" class="price-input" value="${product.price}" 
+                   data-product-id="${product.id}" 
+                   data-original="${product.price}">
+            <span class="price-unit">Ani币</span>
+          </div>
+          <button class="btn-confirm" data-product-id="${product.id}" title="确认修改">✓</button>
+        </div>
+      </div>
+    `).join('');
+  }
+
+  async saveProduct(productId) {
+    const product = this.products.find(p => p.id === productId);
+    if (!product) return;
+
+    const input = document.querySelector(`.price-input[data-product-id="${productId}"]`);
+    if (!input) return;
+
+    const newPrice = parseFloat(input.value);
+    if (isNaN(newPrice) || newPrice < 0) {
+      this.showMessage('价格必须是大于等于0的数字', 'error');
+      return;
+    }
+
+    const originalPrice = parseFloat(input.dataset.original);
+    if (newPrice === originalPrice) {
+      const confirmBtn = input.closest('.product-item').querySelector('.btn-confirm');
+      if (confirmBtn) {
+        confirmBtn.classList.remove('show');
+      }
+      return;
+    }
+
+    try {
+      const response = await fetch(`${this.apiBaseUrl}/api/admin/product-pricing/settings`, {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json'
+        },
+        body: JSON.stringify({
+          productId: productId,
+          price: newPrice
+        })
+      });
+
+      const result = await response.json();
+      if (result.success) {
+        product.price = newPrice;
+        input.dataset.original = newPrice.toString();
+        const confirmBtn = input.closest('.product-item').querySelector('.btn-confirm');
+        if (confirmBtn) {
+          confirmBtn.classList.remove('show');
+        }
+        this.showMessage('价格更新成功', 'success');
+      } else {
+        this.showMessage(result.message || '保存失败', 'error');
+      }
+    } catch (error) {
+      console.error('[ProductPricingManager] 保存失败:', error);
+      this.showMessage('保存失败,请稍后重试', 'error');
+    }
+  }
+
+  showMessage(message, type) {
+    const msgBox = document.getElementById('msgBox');
+    if (msgBox) {
+      msgBox.textContent = message;
+      msgBox.className = `msg ${type}`;
+      setTimeout(() => {
+        msgBox.className = 'msg';
+      }, 3000);
+    }
+  }
+}
+

+ 291 - 0
admin/js/resource-manager/multiple-selection.js

@@ -0,0 +1,291 @@
+// 多选框选功能模块(复制自 client/js/disk/multiple-selection.js)
+
+class ResourceManagerMultipleSelection {
+    constructor(options) {
+        // 必需的元素
+        this.container = options.container;         // 框选容器(dropZone)
+        this.itemsContainer = options.itemsContainer; // 文件项容器(fileList)
+        this.selectionBox = options.selectionBox;   // 选框元素
+        this.selectionBar = options.selectionBar;   // 选择操作栏
+        this.selectionCount = options.selectionCount; // 选择计数元素
+        
+        // 选择项的选择器
+        this.itemSelector = options.itemSelector || '.file-item';
+        
+        // 回调函数
+        this.onSelectionChange = options.onSelectionChange || null;
+        
+        // 状态
+        this.selectedItems = new Set();
+        this.isSelecting = false;
+        this.selectionStart = null;
+        this.selectionHasMoved = false;
+        this.selectionDragJustFinished = false;
+        
+        this.init();
+    }
+
+    init() {
+        this.bindEvents();
+    }
+
+    bindEvents() {
+        // 框选事件
+        this.container.addEventListener('mousedown', (e) => this.startSelection(e));
+        document.addEventListener('mousemove', (e) => this.updateSelection(e));
+        document.addEventListener('mouseup', () => this.endSelection());
+        
+        // 窗口失去焦点时结束框选
+        window.addEventListener('blur', () => this.endSelection());
+        
+        // 拖拽结束时也结束框选
+        document.addEventListener('dragend', () => this.endSelection());
+        
+        // 点击空白处取消选择
+        this.container.addEventListener('click', (e) => this.handleContainerClick(e));
+    }
+
+    // 框选开始
+    startSelection(e) {
+        // 只响应左键
+        if (e.button !== 0) return;
+        
+        // 如果点击的是文件项或其子元素,不启动框选
+        if (e.target.closest(this.itemSelector)) {
+            return;
+        }
+
+        // 如果点击的是重命名输入框,不启动框选
+        if (e.target.classList.contains('rename-input')) {
+            return;
+        }
+
+        // 如果有输入框正在获得焦点(重命名中),不阻止默认行为,让 blur 触发
+        const activeInput = document.querySelector('.rename-input:focus');
+        if (activeInput) {
+            return;
+        }
+        
+        // 阻止默认行为,防止浏览器的文本选择和拖拽干扰框选
+        e.preventDefault();
+        
+        // 如果没有按 Ctrl,先清除之前的选择(Windows 行为)
+        if (!e.ctrlKey) {
+            this.clearSelection();
+        }
+        
+        this.isSelecting = true;
+        this.selectionHasMoved = false;
+        this.selectionStart = { x: e.clientX, y: e.clientY };
+        this.selectionBox.style.display = 'block';
+        this.selectionBox.style.left = e.clientX + 'px';
+        this.selectionBox.style.top = e.clientY + 'px';
+        this.selectionBox.style.width = '0';
+        this.selectionBox.style.height = '0';
+        
+        // 防止拖拽时选中文本
+        document.body.style.userSelect = 'none';
+    }
+
+    // 框选更新
+    updateSelection(e) {
+        if (!this.isSelecting || !this.selectionStart) return;
+        
+        // 阻止默认行为
+        e.preventDefault();
+
+        const x = Math.min(e.clientX, this.selectionStart.x);
+        const y = Math.min(e.clientY, this.selectionStart.y);
+        const width = Math.abs(e.clientX - this.selectionStart.x);
+        const height = Math.abs(e.clientY - this.selectionStart.y);
+        
+        // 检测是否有实际移动(超过5像素算作拖拽)
+        if (width > 5 || height > 5) {
+            this.selectionHasMoved = true;
+        }
+
+        this.selectionBox.style.left = x + 'px';
+        this.selectionBox.style.top = y + 'px';
+        this.selectionBox.style.width = width + 'px';
+        this.selectionBox.style.height = height + 'px';
+
+        // 检测框选区域内的文件
+        const boxRect = {
+            left: x,
+            top: y,
+            right: x + width,
+            bottom: y + height
+        };
+
+        const items = this.itemsContainer.querySelectorAll(this.itemSelector);
+        items.forEach(item => {
+            const itemRect = item.getBoundingClientRect();
+            const isIntersecting = !(
+                itemRect.right < boxRect.left ||
+                itemRect.left > boxRect.right ||
+                itemRect.bottom < boxRect.top ||
+                itemRect.top > boxRect.bottom
+            );
+
+            if (isIntersecting) {
+                this.selectItem(item, false);
+            } else if (!e.ctrlKey) {
+                this.deselectItem(item, false);
+            }
+        });
+
+        this.updateSelectionBar();
+        this.triggerSelectionChange();
+    }
+
+    // 框选结束
+    endSelection() {
+        // 只有实际拖拽移动过才标记,防止 click 事件清除选择
+        if (this.isSelecting && this.selectionHasMoved) {
+            this.selectionDragJustFinished = true;
+        }
+        
+        this.isSelecting = false;
+        this.selectionStart = null;
+        this.selectionHasMoved = false;
+        this.selectionBox.style.display = 'none';
+        
+        // 恢复文本选择
+        document.body.style.userSelect = '';
+    }
+
+    // 处理容器点击
+    handleContainerClick(e) {
+        // 如果刚完成拖拽框选,不清除选择
+        if (this.selectionDragJustFinished) {
+            this.selectionDragJustFinished = false;
+            return;
+        }
+        // 如果点击的是重命名输入框,不处理
+        if (e.target.classList.contains('rename-input')) {
+            return;
+        }
+        // 如果点击的不是文件项,清除选择
+        if (!e.target.closest(this.itemSelector)) {
+            this.clearSelection();
+        }
+    }
+
+    // 选中项目
+    selectItem(item, updateBar = true) {
+        const path = item.dataset.path;
+        if (!this.selectedItems.has(path)) {
+            this.selectedItems.add(path);
+            item.classList.add('selected');
+            if (updateBar) {
+                this.updateSelectionBar();
+                this.triggerSelectionChange();
+            }
+        }
+    }
+
+    // 取消选中项目
+    deselectItem(item, updateBar = true) {
+        const path = item.dataset.path;
+        if (this.selectedItems.has(path)) {
+            this.selectedItems.delete(path);
+            item.classList.remove('selected');
+            if (updateBar) {
+                this.updateSelectionBar();
+                this.triggerSelectionChange();
+            }
+        }
+    }
+
+    // 切换选中状态(Ctrl+点击)
+    toggleSelection(item) {
+        const path = item.dataset.path;
+        if (this.selectedItems.has(path)) {
+            this.deselectItem(item);
+        } else {
+            this.selectItem(item);
+        }
+    }
+
+    // 只选中一个项目,取消其他选择(Windows 单击行为)
+    selectOnly(item) {
+        // 先清除所有选择(但不触发回调)
+        this.itemsContainer.querySelectorAll(`${this.itemSelector}.selected`).forEach(el => {
+            el.classList.remove('selected');
+        });
+        this.selectedItems.clear();
+        
+        // 选中当前项
+        const path = item.dataset.path;
+        this.selectedItems.add(path);
+        item.classList.add('selected');
+        
+        this.updateSelectionBar();
+        this.triggerSelectionChange();
+    }
+
+    // 清除所有选择
+    clearSelection() {
+        this.selectedItems.clear();
+        this.itemsContainer.querySelectorAll(`${this.itemSelector}.selected`).forEach(item => {
+            item.classList.remove('selected');
+        });
+        this.updateSelectionBar();
+        this.triggerSelectionChange();
+    }
+
+    // 更新选择操作栏
+    updateSelectionBar() {
+        const count = this.selectedItems.size;
+        if (this.selectionBar) {
+            if (count > 0) {
+                this.selectionBar.classList.add('show');
+                if (this.selectionCount) {
+                    this.selectionCount.textContent = `已选择 ${count} 项`;
+                }
+            } else {
+                this.selectionBar.classList.remove('show');
+            }
+        }
+    }
+
+    // 触发选择变化回调
+    triggerSelectionChange() {
+        if (this.onSelectionChange) {
+            this.onSelectionChange(this.getSelectedItems());
+        }
+    }
+
+    // 获取已选择的项目路径
+    getSelectedItems() {
+        return Array.from(this.selectedItems);
+    }
+
+    // 获取已选择的项目数量
+    getSelectedCount() {
+        return this.selectedItems.size;
+    }
+
+    // 检查是否有选中项
+    hasSelection() {
+        return this.selectedItems.size > 0;
+    }
+
+    // 检查指定项是否被选中
+    isSelected(path) {
+        return this.selectedItems.has(path);
+    }
+
+    // 全选所有项目
+    selectAll() {
+        const items = this.itemsContainer.querySelectorAll(this.itemSelector);
+        items.forEach(item => {
+            const path = item.dataset.path;
+            this.selectedItems.add(path);
+            item.classList.add('selected');
+        });
+        this.updateSelectionBar();
+        this.triggerSelectionChange();
+    }
+}
+

+ 53 - 0
admin/js/resource-manager/path.js

@@ -0,0 +1,53 @@
+// 路径导航模块(复制自 client/js/disk/path.js)
+
+class ResourceManagerPathNavigator {
+    constructor(options = {}) {
+        this.container = options.container;
+        this.rootName = options.rootName || '全部文件';
+        this.onNavigate = options.onNavigate || (() => {});
+        
+        this.currentPath = '';
+        this.pathParts = [];
+        
+        this.init();
+    }
+
+    init() {
+        this.render();
+        this.bindEvents();
+    }
+
+    bindEvents() {
+        this.container.addEventListener('click', (e) => {
+            const item = e.target.closest('.breadcrumb-item');
+            if (!item) return;
+            
+            const path = item.dataset.path || '';
+            this.navigateTo(path);
+        });
+    }
+
+    navigateTo(path) {
+        this.currentPath = path;
+        this.pathParts = path ? path.split('/').filter(p => p) : [];
+        this.render();
+        this.onNavigate(path);
+    }
+
+    getPath() {
+        return this.currentPath;
+    }
+
+    render() {
+        let html = `<span class="breadcrumb-item ${this.pathParts.length === 0 ? 'active' : ''}" data-path="">${this.rootName}</span>`;
+        
+        let currentPath = '';
+        this.pathParts.forEach((part, index) => {
+            currentPath += (index === 0 ? '' : '/') + part;
+            const isLast = index === this.pathParts.length - 1;
+            html += `<span class="breadcrumb-item ${isLast ? 'active' : ''}" data-path="${currentPath}">${part}</span>`;
+        });
+        
+        this.container.innerHTML = html;
+    }
+}

+ 1373 - 0
admin/js/resource-manager/resource-manager.js

@@ -0,0 +1,1373 @@
+// 素材管理主逻辑模块(参考 client/js/disk/disk.js)
+
+class ResourceManager {
+    constructor(options = {}) {
+        this.apiBaseUrl = options.apiBaseUrl || 'http://localhost:3000';
+        this.files = [];
+        this.currentPath = '';
+        
+        // DOM 元素
+        this.container = document.querySelector('.disk-container');
+        this.dropZone = null;
+        this.fileList = null;
+        this.breadcrumb = null;
+        this.emptyState = null;
+        this.loading = null;
+        this.selectionBar = null;
+        this.selectionCount = null;
+        this.selectionBox = null;
+        this.contextMenu = null;
+        this.fileInput = null;
+        this.searchInput = null;
+        this.searchClear = null;
+        this.btnUpload = null;
+        this.btnDelete = null;
+        
+        // 功能模块
+        this.pathNav = null;
+        this.selection = null;
+        this.searchBar = null;
+        this.contextMenuManager = null;
+        this.shortcutKeys = null;
+        
+        // 等待 DOM 加载完成
+        if (document.readyState === 'loading') {
+            document.addEventListener('DOMContentLoaded', () => this.init());
+        } else {
+            this.init();
+        }
+    }
+
+    async init() {
+        this.initElements();
+        this.initPath();
+        this.initSelection();
+        this.initShortcutKeys();
+        this.initSearchBar();
+        this.initContextMenu();
+        this.bindEvents();
+        
+        // 加载文件列表
+        await this.loadFiles();
+    }
+
+    initElements() {
+        this.dropZone = document.getElementById('dropZone');
+        this.fileList = document.getElementById('fileList');
+        this.breadcrumb = document.getElementById('breadcrumb');
+        this.emptyState = document.getElementById('emptyState');
+        this.loading = document.getElementById('loading');
+        this.selectionBar = document.getElementById('selectionBar');
+        this.selectionCount = document.getElementById('selectionCount');
+        this.selectionBox = document.getElementById('selectionBox');
+        this.contextMenu = document.getElementById('contextMenu');
+        this.fileInput = null; // 动态创建
+        this.searchInput = document.getElementById('searchInput');
+        this.searchClear = document.getElementById('searchClear');
+        this.btnUpload = document.getElementById('btnUpload');
+        this.btnDelete = document.getElementById('btnDelete');
+    }
+
+    // 初始化路径导航
+    initPath() {
+        if (this.breadcrumb) {
+            this.pathNav = new ResourceManagerPathNavigator({
+                container: this.breadcrumb,
+                rootName: '全部文件',
+                onNavigate: (path) => {
+                    this.loadFiles(path);
+                }
+            });
+        }
+    }
+
+    // 初始化框选功能
+    initSelection() {
+        if (this.dropZone && this.fileList && this.selectionBox) {
+            this.selection = new ResourceManagerMultipleSelection({
+                container: this.dropZone,
+                itemsContainer: this.fileList,
+                selectionBox: this.selectionBox,
+                selectionBar: this.selectionBar,
+                selectionCount: this.selectionCount,
+                itemSelector: '.file-item',
+                onSelectionChange: (selectedItems) => {
+                    // 选择变化时的回调
+                }
+            });
+        }
+    }
+
+    // 初始化快捷键
+    initShortcutKeys() {
+        if (this.shortcutKeys) {
+            this.shortcutKeys.destroy();
+        }
+
+        this.shortcutKeys = new ResourceManagerShortcutKeys({
+            selection: this.selection,
+            onDelete: () => this.deleteSelected(),
+            onRename: () => this.renameSelected()
+        });
+    }
+
+    // 初始化搜索栏
+    initSearchBar() {
+        if (this.searchInput && this.fileList) {
+            this.searchBar = new ResourceManagerSearchBar({
+                input: this.searchInput,
+                clearButton: this.searchClear,
+                fileList: this.fileList,
+                emptyState: this.emptyState,
+                getResources: () => this.files,
+                renderAll: () => this.renderFiles(),
+                createFileItem: (file) => this.createFileItem(file),
+                noResultMessage: '没有找到匹配的资源'
+            });
+        }
+    }
+
+    // 初始化右键菜单
+    initContextMenu() {
+        if (!this.dropZone || !this.contextMenu) return;
+
+        this.contextMenuManager = new ResourceManagerRightClickMenu({
+            target: this.dropZone,
+            menu: this.contextMenu,
+            onAction: (action, event) => this.handleContextMenuAction(action, event),
+            onBeforeShow: (event) => this.handleBeforeShowContextMenu(event)
+        });
+    }
+
+    // 显示右键菜单前的处理
+    handleBeforeShowContextMenu(event) {
+        const fileItem = event.target.closest('.file-item');
+        
+        if (!this.contextMenu) return false;
+        
+        const newBtn = this.contextMenu.querySelector('[data-action="new"]');
+        const uploadBtn = this.contextMenu.querySelector('[data-action="upload"]');
+        const backBtn = this.contextMenu.querySelector('[data-action="back"]');
+        const deleteBtn = this.contextMenu.querySelector('[data-action="delete"]');
+        const renameBtn = this.contextMenu.querySelector('[data-action="rename"]');
+        const refreshBtn = this.contextMenu.querySelector('[data-action="refresh"]');
+        
+        const isRoot = this.currentPath === '';
+        
+        // 新建分类:只在根目录时显示
+        if (newBtn) {
+            newBtn.style.display = isRoot ? 'flex' : 'none';
+        }
+        
+        // 上传素材:只在子目录时显示
+        if (uploadBtn) {
+            uploadBtn.style.display = isRoot ? 'none' : 'flex';
+        }
+        
+        // 返回上级:只在子目录时显示
+        if (backBtn) {
+            backBtn.style.display = isRoot ? 'none' : 'flex';
+        }
+        
+        // 刷新按钮始终显示
+        if (refreshBtn) {
+            refreshBtn.style.display = 'flex';
+        }
+        
+        // 如果点击在文件项上
+        if (fileItem) {
+            // 确保被点击的项被选中
+            const isSelected = this.selection && this.selection.isSelected(fileItem.dataset.path);
+            if (!isSelected && this.selection) {
+                this.selection.selectOnly(fileItem);
+            }
+            
+            if (deleteBtn) deleteBtn.style.display = 'flex';
+            if (renameBtn) renameBtn.style.display = 'flex';
+        } else {
+            if (deleteBtn) deleteBtn.style.display = 'none';
+            if (renameBtn) renameBtn.style.display = 'none';
+        }
+        
+        // 更新分隔线显示
+        this.updateContextMenuDividers();
+        
+        return true;
+    }
+    
+    // 更新右键菜单分隔线显示
+    updateContextMenuDividers() {
+        if (!this.contextMenu) return;
+        
+        const items = Array.from(this.contextMenu.children);
+        let lastVisibleWasDivider = true;
+        
+        items.forEach((item, index) => {
+            if (item.classList.contains('context-menu-divider')) {
+                // 如果上一个可见项也是分隔线,或者是第一个,隐藏
+                if (lastVisibleWasDivider) {
+                    item.style.display = 'none';
+                } else {
+                    item.style.display = 'block';
+                    lastVisibleWasDivider = true;
+                }
+            } else if (item.style.display !== 'none') {
+                lastVisibleWasDivider = false;
+            }
+        });
+        
+        // 隐藏末尾的分隔线
+        for (let i = items.length - 1; i >= 0; i--) {
+            const item = items[i];
+            if (item.classList.contains('context-menu-divider')) {
+                if (item.style.display !== 'none') {
+                    item.style.display = 'none';
+                }
+            } else if (item.style.display !== 'none') {
+                break;
+            }
+        }
+    }
+
+    // 处理右键菜单操作
+    handleContextMenuAction(action) {
+        switch (action) {
+            case 'new':
+                this.createFolder();
+                break;
+            case 'upload':
+                // 触发上传
+                this.triggerUpload();
+                break;
+            case 'back':
+                // 返回上级目录
+                this.loadFiles('');
+                break;
+            case 'delete':
+                this.deleteSelected();
+                break;
+            case 'rename':
+                this.renameSelected();
+                break;
+            case 'refresh':
+                this.loadFiles(this.currentPath);
+                break;
+        }
+    }
+
+    bindEvents() {
+        // 上传按钮已移除,使用右键菜单或拖拽上传
+
+        // 删除按钮
+        if (this.btnDelete) {
+            this.btnDelete.addEventListener('click', () => this.deleteSelected());
+        }
+
+        // 拖拽上传
+        if (this.dropZone) {
+            console.log('[ResourceManager] 绑定拖拽事件到 dropZone:', this.dropZone);
+            this.dropZone.addEventListener('dragenter', (e) => this.handleDragEnter(e));
+            this.dropZone.addEventListener('dragover', (e) => this.handleDragOver(e));
+            this.dropZone.addEventListener('dragleave', (e) => this.handleDragLeave(e));
+            this.dropZone.addEventListener('drop', (e) => this.handleDrop(e));
+        } else {
+            console.error('[ResourceManager] dropZone 未找到!');
+        }
+    }
+    
+    // 触发上传(动态创建文件输入)
+    triggerUpload() {
+        // 创建新的文件输入(每次都重新创建以确保属性正确)
+        const newInput = document.createElement('input');
+        newInput.type = 'file';
+        newInput.style.display = 'none';
+        
+        // 使用文件夹选择模式
+        newInput.webkitdirectory = true;
+        newInput.directory = true;
+        newInput.multiple = true;
+        
+        // 移除旧的文件输入
+        if (this.fileInput) {
+            this.fileInput.remove();
+        }
+        
+        // 添加到DOM
+        document.body.appendChild(newInput);
+        this.fileInput = newInput;
+        
+        // 绑定change事件 - 跳过浏览器确认后直接处理
+        this.fileInput.addEventListener('change', (e) => this.handleFileSelectDirect(e));
+        
+        // 触发点击
+        this.fileInput.click();
+    }
+    
+    // 直接处理文件选择(跳过额外确认)
+    async handleFileSelectDirect(e) {
+        const files = Array.from(e.target.files);
+        if (files.length === 0) {
+            this.fileInput.value = '';
+            return;
+        }
+
+        // 验证文件夹结构
+        const validation = this.validateFolderFiles(files);
+        
+        if (!validation.valid) {
+            this.showError(validation.errors.join('\n'));
+            this.fileInput.value = '';
+            return;
+        }
+        
+        // 浏览器已经确认过了,直接上传,不再弹出我们的确认框
+        const filesToUpload = validation.files.map(file => ({
+            file: file,
+            path: file.webkitRelativePath || file.name
+        }));
+        
+        try {
+            await this.uploadFolderFiles(validation.folderName, filesToUpload);
+            this.showSuccess(`上传成功: ${validation.folderName}`);
+            await this.loadFiles(this.currentPath);
+        } catch (error) {
+            this.showError('上传失败: ' + error.message);
+        }
+        
+        this.fileInput.value = '';
+    }
+
+    // 加载文件列表
+    async loadFiles(path = '') {
+        this.showLoading(true);
+        if (this.selection) {
+            this.selection.clearSelection();
+        }
+        if (this.searchBar) {
+            this.searchBar.clear({ noRender: true });
+        }
+        
+        this.currentPath = path;
+
+        try {
+            if (path === '') {
+                // 根目录:加载分类列表
+                const response = await fetch(`${this.apiBaseUrl}/api/store/categories`);
+                if (!response.ok) {
+                    throw new Error(`加载分类失败: ${response.status}`);
+                }
+                const result = await response.json();
+                
+                if (result.success && result.categories) {
+                    this.files = result.categories.map(cat => ({
+                        name: cat.name,
+                        path: cat.dir,
+                        type: 'directory',
+                        isCategory: true
+                    }));
+                } else {
+                    this.files = [];
+                }
+            } else {
+                // 子目录:加载资源列表
+                const response = await fetch(`${this.apiBaseUrl}/api/store/resources?category=${encodeURIComponent(path)}`);
+                if (!response.ok) {
+                    throw new Error(`加载资源失败: ${response.status}`);
+                }
+                const result = await response.json();
+                
+                if (result.success && result.resources) {
+                    this.files = result.resources.map(res => ({
+                        ...res,
+                        type: 'directory'
+                    }));
+                } else {
+                    this.files = [];
+                }
+            }
+            
+            this.renderFiles();
+        } catch (error) {
+            console.error('[ResourceManager] 加载失败:', error);
+            this.files = [];
+            this.renderFiles();
+            this.showError('加载失败: ' + error.message);
+        } finally {
+            this.showLoading(false);
+        }
+    }
+
+    // 渲染文件列表
+    renderFiles() {
+        if (!this.fileList) return;
+        
+        this.fileList.innerHTML = '';
+
+        if (this.files.length === 0) {
+            if (this.emptyState) {
+                this.emptyState.classList.add('show');
+            }
+            return;
+        }
+
+        if (this.emptyState) {
+            this.emptyState.classList.remove('show');
+        }
+
+        this.files.forEach(file => {
+            const fileItem = this.createFileItem(file);
+            this.fileList.appendChild(fileItem);
+        });
+    }
+
+    // 创建文件项
+    createFileItem(file) {
+        const div = document.createElement('div');
+        div.className = 'file-item';
+        div.dataset.name = file.name;
+        div.dataset.type = file.type;
+        div.dataset.path = file.path;
+        
+        // 在根目录时,分类文件夹可以拖拽排序
+        if (this.currentPath === '' && file.isCategory) {
+            div.draggable = true;
+            this.setupDragReorder(div, file);
+        }
+
+        // 勾选标记
+        const checkMark = `
+            <div class="check-mark">
+                <svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor">
+                    <path d="M10 3L4.5 8.5 2 6" stroke="white" stroke-width="2" fill="none"/>
+                </svg>
+            </div>
+        `;
+
+        // 检查是否有预览图
+        if (file.previewUrl) {
+            div.innerHTML = `
+                ${checkMark}
+                <div class="file-thumbnail folder-preview">
+                    <img src="${this.apiBaseUrl}${file.previewUrl}" alt="${file.name}" loading="lazy">
+                    <div class="folder-badge">📁</div>
+                </div>
+                <div class="file-name">${file.name}</div>
+                <input type="text" class="rename-input" style="display: none;">
+            `;
+        } else {
+            div.innerHTML = `
+                ${checkMark}
+                <div class="file-icon folder-icon">
+                    <svg viewBox="0 0 64 64" fill="currentColor">
+                        <path d="M8 12h20l4 6h24a4 4 0 014 4v28a4 4 0 01-4 4H8a4 4 0 01-4-4V16a4 4 0 014-4z"/>
+                    </svg>
+                </div>
+                <div class="file-name">${file.name}</div>
+                <input type="text" class="rename-input" style="display: none;">
+            `;
+        }
+
+        // 点击事件
+        let clickTimer = null;
+        
+        div.addEventListener('click', (e) => {
+            if (e.target.classList.contains('rename-input')) return;
+            e.stopPropagation();
+            
+            const isAlreadySelected = this.selection && this.selection.isSelected(file.path);
+            const clickedOnName = e.target.classList.contains('file-name');
+            const ctrlKey = e.ctrlKey;
+            
+            if (clickTimer) {
+                clearTimeout(clickTimer);
+                clickTimer = null;
+            }
+            
+            clickTimer = setTimeout(() => {
+                clickTimer = null;
+                
+                if (ctrlKey && this.selection) {
+                    this.selection.toggleSelection(div);
+                } else if (clickedOnName && isAlreadySelected) {
+                    this.startRename(div);
+                } else if (this.selection) {
+                    this.selection.selectOnly(div);
+                }
+            }, 200);
+        });
+
+        // 双击进入文件夹
+        div.addEventListener('dblclick', (e) => {
+            if (e.target.classList.contains('rename-input')) return;
+            e.stopPropagation();
+            
+            if (clickTimer) {
+                clearTimeout(clickTimer);
+                clickTimer = null;
+            }
+            
+            if (file.type === 'directory') {
+                if (file.isCategory) {
+                    this.pathNav.navigateTo(file.path);
+                } else {
+                    // 资源文件夹可以进一步打开查看
+                    // 或者在这里可以打开预览
+                }
+            }
+        });
+
+        return div;
+    }
+
+    // 开始重命名
+    startRename(fileItem) {
+        const nameEl = fileItem.querySelector('.file-name');
+        const input = fileItem.querySelector('.rename-input');
+        if (!nameEl || !input) return;
+
+        const oldName = fileItem.dataset.name;
+        const filePath = fileItem.dataset.path;
+
+        input.value = oldName;
+        nameEl.style.display = 'none';
+        input.style.display = '';
+
+        setTimeout(() => {
+            const dotIndex = oldName.lastIndexOf('.');
+            if (dotIndex > 0 && fileItem.dataset.type !== 'directory') {
+                input.setSelectionRange(0, dotIndex);
+            } else {
+                input.select();
+            }
+            input.focus();
+        }, 10);
+
+        let finished = false;
+
+        const exitRename = () => {
+            input.style.display = 'none';
+            nameEl.style.display = '';
+        };
+
+        const commitRename = async () => {
+            if (finished) return;
+            finished = true;
+
+            const newName = input.value.trim();
+            exitRename();
+
+            if (newName && newName !== oldName) {
+                await this.renameFile(filePath, newName);
+            }
+        };
+
+        const cancelRename = () => {
+            if (finished) return;
+            finished = true;
+            exitRename();
+        };
+
+        const handleBlur = () => {
+            input.removeEventListener('blur', handleBlur);
+            input.removeEventListener('keydown', handleKeydown);
+            if (!finished) {
+                commitRename();
+            }
+        };
+
+        const handleKeydown = (e) => {
+            if (e.key === 'Enter') {
+                e.preventDefault();
+                e.stopPropagation();
+                input.removeEventListener('blur', handleBlur);
+                input.removeEventListener('keydown', handleKeydown);
+                commitRename();
+            } else if (e.key === 'Escape') {
+                e.preventDefault();
+                e.stopPropagation();
+                input.removeEventListener('blur', handleBlur);
+                input.removeEventListener('keydown', handleKeydown);
+                cancelRename();
+            }
+        };
+
+        input.addEventListener('blur', handleBlur);
+        input.addEventListener('keydown', handleKeydown);
+    }
+
+    // 重命名文件
+    async renameFile(oldPath, newName) {
+        try {
+            const response = await fetch(`${this.apiBaseUrl}/api/admin/store/rename`, {
+                method: 'POST',
+                headers: { 'Content-Type': 'application/json' },
+                body: JSON.stringify({ resourcePath: oldPath, newName })
+            });
+
+            const result = await response.json();
+            if (result.success) {
+                this.showSuccess('重命名成功');
+                await this.loadFiles(this.currentPath);
+            } else {
+                this.showError('重命名失败: ' + (result.message || '未知错误'));
+                await this.loadFiles(this.currentPath);
+            }
+        } catch (error) {
+            this.showError('重命名失败: ' + error.message);
+            await this.loadFiles(this.currentPath);
+        }
+    }
+
+    // 重命名选中项
+    renameSelected() {
+        if (!this.selection || this.selection.getSelectedCount() !== 1) return;
+
+        const selectedPath = this.selection.getSelectedItems()[0];
+        const selectedItem = this.fileList.querySelector(`[data-path="${selectedPath}"]`);
+        if (selectedItem) {
+            this.startRename(selectedItem);
+        }
+    }
+
+    // 删除选中项
+    async deleteSelected() {
+        console.log('[ResourceManager] deleteSelected 被调用');
+        console.log('[ResourceManager] this.selection:', this.selection);
+        
+        if (!this.selection) {
+            console.log('[ResourceManager] selection 为空,退出');
+            return;
+        }
+        
+        const count = this.selection.getSelectedCount();
+        console.log('[ResourceManager] 选中数量:', count);
+        
+        if (count === 0) {
+            console.log('[ResourceManager] 没有选中项,退出');
+            return;
+        }
+
+        const confirmed = await this.showConfirm(`确定要删除选中的 ${count} 个文件/文件夹吗?`);
+        if (!confirmed) return;
+
+        try {
+            const selectedPaths = this.selection.getSelectedItems();
+            
+            for (const path of selectedPaths) {
+                const response = await fetch(`${this.apiBaseUrl}/api/admin/store/delete`, {
+                    method: 'POST',
+                    headers: { 'Content-Type': 'application/json' },
+                    body: JSON.stringify({ resourcePath: path })
+                });
+
+                const result = await response.json();
+                if (!result.success) {
+                    this.showError('删除失败: ' + (result.message || '未知错误'));
+                }
+            }
+
+            this.selection.clearSelection();
+            await this.loadFiles(this.currentPath);
+            this.showSuccess('删除成功');
+        } catch (error) {
+            this.showError('删除失败: ' + error.message);
+        }
+    }
+
+    // 设置拖拽排序
+    setupDragReorder(div, file) {
+        div.addEventListener('dragstart', (e) => {
+            e.stopPropagation();
+            div.classList.add('dragging');
+            e.dataTransfer.effectAllowed = 'move';
+            e.dataTransfer.setData('text/plain', JSON.stringify({
+                type: 'reorder',
+                name: file.name
+            }));
+        });
+
+        div.addEventListener('dragend', () => {
+            div.classList.remove('dragging');
+            // 移除所有拖拽目标样式
+            this.fileList.querySelectorAll('.file-item.drag-over-left, .file-item.drag-over-right').forEach(el => {
+                el.classList.remove('drag-over-left', 'drag-over-right');
+            });
+            // 确保移除上传提示
+            if (this.dropZone) {
+                this.dropZone.classList.remove('drag-over');
+            }
+        });
+
+        div.addEventListener('dragover', (e) => {
+            e.preventDefault();
+            
+            // 只有内部拖拽排序时才处理
+            const draggingEl = this.fileList.querySelector('.file-item.dragging');
+            if (!draggingEl || draggingEl === div) {
+                // 外部文件拖入时,不阻止冒泡,让 dropZone 处理
+                return;
+            }
+            
+            e.stopPropagation();
+            e.dataTransfer.dropEffect = 'move';
+            
+            // 判断鼠标在元素的左半边还是右半边
+            const rect = div.getBoundingClientRect();
+            const midX = rect.left + rect.width / 2;
+            
+            div.classList.remove('drag-over-left', 'drag-over-right');
+            if (e.clientX < midX) {
+                div.classList.add('drag-over-left');
+            } else {
+                div.classList.add('drag-over-right');
+            }
+        });
+
+        div.addEventListener('dragleave', (e) => {
+            // 只有内部拖拽排序时才阻止冒泡
+            const draggingEl = this.fileList.querySelector('.file-item.dragging');
+            if (draggingEl) {
+                e.stopPropagation();
+            }
+            div.classList.remove('drag-over-left', 'drag-over-right');
+        });
+
+        div.addEventListener('drop', async (e) => {
+            e.preventDefault();
+            
+            // 如果是外部文件拖入,让事件继续冒泡到 dropZone
+            if (e.dataTransfer.types.includes('Files') && !e.dataTransfer.getData('text/plain')) {
+                console.log('[ResourceManager] 外部文件拖入到文件项,转发到 dropZone');
+                this.handleDrop(e);
+                return;
+            }
+            
+            e.stopPropagation();
+            
+            const isLeft = div.classList.contains('drag-over-left');
+            div.classList.remove('drag-over-left', 'drag-over-right');
+
+            try {
+                const data = JSON.parse(e.dataTransfer.getData('text/plain'));
+                if (data.type !== 'reorder') return;
+                
+                const draggedName = data.name;
+                const targetName = file.name;
+                
+                if (draggedName === targetName) return;
+                
+                // 重新计算顺序
+                const currentOrder = this.files.map(f => f.name);
+                const draggedIndex = currentOrder.indexOf(draggedName);
+                let targetIndex = currentOrder.indexOf(targetName);
+                
+                if (draggedIndex === -1 || targetIndex === -1) return;
+                
+                // 移除拖拽的元素
+                currentOrder.splice(draggedIndex, 1);
+                
+                // 重新计算目标位置(因为移除了一个元素)
+                targetIndex = currentOrder.indexOf(targetName);
+                
+                // 插入到目标位置
+                if (isLeft) {
+                    currentOrder.splice(targetIndex, 0, draggedName);
+                } else {
+                    currentOrder.splice(targetIndex + 1, 0, draggedName);
+                }
+                
+                // 保存新顺序
+                await this.saveCategoryOrder(currentOrder);
+            } catch (error) {
+                console.error('[ResourceManager] 拖拽排序失败:', error);
+            }
+        });
+    }
+
+    // 保存分类排序
+    async saveCategoryOrder(order) {
+        try {
+            const response = await fetch(`${this.apiBaseUrl}/api/admin/store/update-order`, {
+                method: 'POST',
+                headers: { 'Content-Type': 'application/json' },
+                body: JSON.stringify({ order })
+            });
+
+            const result = await response.json();
+            if (result.success) {
+                this.showSuccess('排序已保存');
+                await this.loadFiles('');
+            } else {
+                this.showError('保存排序失败: ' + (result.message || '未知错误'));
+            }
+        } catch (error) {
+            this.showError('保存排序失败: ' + error.message);
+        }
+    }
+
+    // 创建文件夹
+    async createFolder() {
+        if (this.currentPath !== '') {
+            this.showError('只能在根目录创建分类文件夹');
+            return;
+        }
+
+        const folderName = await this.showPrompt('请输入分类文件夹名称:');
+        if (!folderName || !folderName.trim()) return;
+
+        const name = folderName.trim();
+        if (/[\\/:*?"<>|]/.test(name)) {
+            this.showError('文件夹名称包含非法字符');
+            return;
+        }
+
+        try {
+            const response = await fetch(`${this.apiBaseUrl}/api/admin/store/create-folder`, {
+                method: 'POST',
+                headers: { 'Content-Type': 'application/json' },
+                body: JSON.stringify({ name })
+            });
+
+            const result = await response.json();
+            if (result.success) {
+                this.showSuccess('创建文件夹成功');
+                await this.loadFiles('');
+            } else {
+                this.showError('创建文件夹失败: ' + (result.message || '未知错误'));
+            }
+        } catch (error) {
+            this.showError('创建文件夹失败: ' + error.message);
+        }
+    }
+
+    // 处理文件选择(始终为文件夹模式)
+    async handleFileSelect(e) {
+        const files = Array.from(e.target.files);
+        if (files.length === 0) {
+            this.fileInput.value = '';
+            return;
+        }
+
+        // 验证文件夹结构
+        const validation = this.validateFolderFiles(files);
+        
+        if (!validation.valid) {
+            this.showError(validation.errors.join('\n'));
+            this.fileInput.value = '';
+            return;
+        }
+        
+        const targetPath = this.currentPath || validation.folderName;
+        const confirmMsg = this.currentPath 
+            ? `将上传文件夹 "${validation.folderName}" 到分类 "${this.currentPath}",包含 ${validation.files.length} 个PNG图片,确认上传?`
+            : `将上传文件夹 "${validation.folderName}",包含 ${validation.files.length} 个PNG图片,确认上传?`;
+        
+        const confirmed = await this.showConfirm(confirmMsg);
+        if (!confirmed) {
+            this.fileInput.value = '';
+            return;
+        }
+
+        // 根据当前位置决定上传方式
+        // 转换 validation.files (File[]) 为 {file, path}[] 格式
+        const filesToUpload = validation.files.map(file => ({
+            file: file,
+            path: file.webkitRelativePath || file.name
+        }));
+        
+        if (this.currentPath === '') {
+            // 在根目录上传,文件夹名就是分类名
+            await this.uploadFolderFiles(validation.folderName, filesToUpload);
+        } else {
+            // 在子目录上传
+            await this.uploadFolderFiles(validation.folderName, filesToUpload);
+        }
+        
+        this.fileInput.value = '';
+    }
+    
+    
+    // 验证通过文件输入选择的文件夹
+    validateFolderFiles(files) {
+        const errors = [];
+        const validFiles = [];
+        let folderName = '';
+        
+        // 获取文件夹名(第一层目录)
+        const paths = new Set();
+        for (const file of files) {
+            const relativePath = file.webkitRelativePath || file.name;
+            const parts = relativePath.split('/');
+            if (parts.length > 0) {
+                paths.add(parts[0]);
+                if (!folderName) folderName = parts[0];
+            }
+        }
+        
+        // 检查是否只有一个顶级文件夹
+        if (paths.size > 1) {
+            errors.push('请只选择一个文件夹');
+            return { valid: false, errors, files: [], folderName: '' };
+        }
+        
+        for (const file of files) {
+            const relativePath = file.webkitRelativePath || file.name;
+            const parts = relativePath.split('/');
+            
+            // 检查是否有子文件夹(路径深度超过2层)
+            if (parts.length > 2) {
+                errors.push(`不允许有子文件夹: ${parts.slice(0, -1).join('/')}`);
+                continue;
+            }
+            
+            // 检查文件类型
+            const fileName = file.name.toLowerCase();
+            if (!fileName.endsWith('.png')) {
+                errors.push(`文件 "${file.name}" 不是PNG格式,只允许PNG图片`);
+                continue;
+            }
+            
+            validFiles.push(file);
+        }
+        
+        if (validFiles.length === 0 && errors.length === 0) {
+            errors.push('文件夹为空或没有PNG图片');
+        }
+        
+        // 只显示前5个错误
+        const displayErrors = errors.slice(0, 5);
+        if (errors.length > 5) {
+            displayErrors.push(`... 还有 ${errors.length - 5} 个错误`);
+        }
+        
+        return {
+            valid: errors.length === 0,
+            errors: displayErrors,
+            files: validFiles,
+            folderName
+        };
+    }
+    
+    // 上传文件
+    async uploadFiles(files) {
+        if (files.length === 0) return;
+
+        // 获取目标路径
+        const category = this.currentPath || files[0].webkitRelativePath?.split('/')[0] || 'default';
+        const folderName = files[0].webkitRelativePath?.split('/')[0] || 'upload_' + Date.now();
+
+        const formData = new FormData();
+        formData.append('category', category);
+        formData.append('name', folderName);
+        formData.append('price', 0);
+        
+        for (const file of files) {
+            formData.append('files', file);
+        }
+
+        try {
+            const response = await fetch(`${this.apiBaseUrl}/api/admin/store/upload`, {
+                method: 'POST',
+                body: formData
+            });
+
+            const result = await response.json();
+            if (result.success) {
+                this.showSuccess('上传成功');
+                await this.loadFiles(this.currentPath);
+            } else {
+                this.showError('上传失败: ' + (result.message || '未知错误'));
+            }
+        } catch (error) {
+            this.showError('上传失败: ' + error.message);
+        }
+    }
+
+    // 检查是否是外部文件拖入(而非内部排序拖拽)
+    isExternalFileDrag(e) {
+        // 如果有正在拖拽的内部元素,说明是内部排序
+        if (this.fileList && this.fileList.querySelector('.file-item.dragging')) {
+            return false;
+        }
+        // 检查是否有文件类型
+        return e.dataTransfer.types.includes('Files');
+    }
+
+    // 拖拽处理
+    handleDragEnter(e) {
+        e.preventDefault();
+        e.stopPropagation();
+        console.log('[ResourceManager] dragenter 触发');
+        
+        // 只有外部文件拖入时才显示上传提示
+        if (this.isExternalFileDrag(e)) {
+            this.dropZone.classList.add('drag-over');
+        }
+    }
+
+    handleDragOver(e) {
+        e.preventDefault();
+        e.stopPropagation();
+    }
+
+    handleDragLeave(e) {
+        e.preventDefault();
+        e.stopPropagation();
+        
+        // 检查是否真正离开了 dropZone(而不是进入子元素)
+        if (!this.dropZone.contains(e.relatedTarget)) {
+            this.dropZone.classList.remove('drag-over');
+        }
+    }
+
+    async handleDrop(e) {
+        console.log('[ResourceManager] ===== handleDrop 触发 =====');
+        e.preventDefault();
+        e.stopPropagation();
+        this.dropZone.classList.remove('drag-over');
+
+        console.log('[ResourceManager] dataTransfer.types:', Array.from(e.dataTransfer.types));
+        console.log('[ResourceManager] isExternalFileDrag:', this.isExternalFileDrag(e));
+
+        // 如果是内部排序拖拽,不处理文件上传
+        if (!this.isExternalFileDrag(e)) {
+            console.log('[ResourceManager] 是内部拖拽,跳过');
+            return;
+        }
+
+        const items = e.dataTransfer.items;
+        console.log('[ResourceManager] items:', items, 'length:', items ? items.length : 0);
+        if (!items) return;
+
+        const entries = [];
+        for (let i = 0; i < items.length; i++) {
+            const item = items[i].webkitGetAsEntry();
+            console.log(`[ResourceManager] entry ${i}:`, item ? item.name : 'null');
+            if (item) {
+                entries.push(item);
+            }
+        }
+
+        console.log('[ResourceManager] 共收集到', entries.length, '个 entries');
+        await this.processDropEntries(entries);
+    }
+    
+    // 处理拖入的 entries(完全按照 client/js/disk/disk.js 的方式)
+    async processDropEntries(entries) {
+        console.log('[ResourceManager] processDropEntries 开始处理', entries.length, '个 entries');
+        
+        // 检查是否有非文件夹
+        const hasFiles = entries.some(entry => entry.isFile);
+        if (hasFiles) {
+            this.showError('只能拖拽文件夹,不能拖拽单个文件');
+            return;
+        }
+        
+        const filesToUpload = [];
+        
+        for (const entry of entries) {
+            console.log('[ResourceManager] 开始遍历:', entry.name);
+            try {
+                await this.traverseEntry(entry, '', filesToUpload);
+                console.log('[ResourceManager] 遍历完成:', entry.name, '当前文件数:', filesToUpload.length);
+            } catch (err) {
+                console.error('[ResourceManager] 遍历失败:', entry.name, err);
+            }
+        }
+        
+        console.log('[ResourceManager] 总共收集到', filesToUpload.length, '个 PNG 文件');
+
+        if (filesToUpload.length > 0) {
+            // 按文件夹分组
+            const folderMap = new Map();
+            for (const item of filesToUpload) {
+                const folderName = item.path.split('/')[0];
+                if (!folderMap.has(folderName)) {
+                    folderMap.set(folderName, []);
+                }
+                folderMap.get(folderName).push(item);
+            }
+            
+            const folderNames = Array.from(folderMap.keys());
+            const confirmMsg = `将上传 ${folderMap.size} 个文件夹(${folderNames.slice(0, 5).join('、')}${folderNames.length > 5 ? '...' : ''}),共 ${filesToUpload.length} 个PNG图片\n\n确认上传?`;
+            
+            const confirmed = await this.showConfirm(confirmMsg);
+            if (!confirmed) {
+                return;
+            }
+            
+            // 按文件夹逐个上传,每上传完一个就刷新显示
+            let successCount = 0;
+            let failCount = 0;
+            const totalCount = folderMap.size;
+            
+            for (const [folderName, files] of folderMap) {
+                try {
+                    await this.uploadFolderFiles(folderName, files);
+                    successCount++;
+                    // 每上传完一个文件夹就刷新显示
+                    await this.loadFiles(this.currentPath);
+                    this.showInfo(`上传进度: ${successCount}/${totalCount} - ${folderName}`);
+                } catch (error) {
+                    console.error(`[ResourceManager] 上传 ${folderName} 失败:`, error);
+                    failCount++;
+                }
+            }
+            
+            if (failCount === 0) {
+                this.showSuccess(`成功上传 ${successCount} 个文件夹`);
+            } else {
+                this.showWarning(`上传完成: ${successCount} 成功, ${failCount} 失败`);
+            }
+        } else {
+            console.log('[ResourceManager] 没有找到可上传的PNG图片');
+            this.showError('没有找到可上传的PNG图片');
+        }
+    }
+    
+    // 递归遍历文件夹
+    async traverseEntry(entry, relativePath, filesToUpload) {
+        console.log('[ResourceManager] traverseEntry:', entry.name, 'isFile:', entry.isFile, 'isDirectory:', entry.isDirectory);
+        
+        if (entry.isFile) {
+            console.log('[ResourceManager] 读取文件:', entry.name);
+            const file = await new Promise((resolve, reject) => {
+                entry.file(resolve, reject);
+            });
+            console.log('[ResourceManager] 文件读取完成:', file.name, file.size);
+            // 只接受 PNG 文件
+            if (file.name.toLowerCase().endsWith('.png')) {
+                filesToUpload.push({
+                    file: file,
+                    path: relativePath + file.name
+                });
+                console.log('[ResourceManager] 添加PNG:', relativePath + file.name);
+            }
+        } else if (entry.isDirectory) {
+            console.log('[ResourceManager] 开始读取目录:', entry.name);
+            const dirReader = entry.createReader();
+            
+            // 读取所有条目 - readEntries 每次最多返回100条,需要循环读取
+            let allEntries = [];
+            let batch;
+            let readCount = 0;
+            do {
+                console.log('[ResourceManager] 调用 readEntries 第', readCount + 1, '次');
+                batch = await new Promise((resolve) => {
+                    dirReader.readEntries(
+                        (entries) => {
+                            console.log('[ResourceManager] readEntries 成功, 返回', entries.length, '条');
+                            resolve(entries);
+                        }, 
+                        (err) => {
+                            console.error('[ResourceManager] readEntries 错误:', err);
+                            resolve([]); // 出错返回空数组继续
+                        }
+                    );
+                });
+                allEntries = allEntries.concat(batch);
+                readCount++;
+            } while (batch.length > 0);
+            
+            console.log('[ResourceManager] 目录', entry.name, '共有', allEntries.length, '个条目');
+
+            for (const childEntry of allEntries) {
+                await this.traverseEntry(
+                    childEntry,
+                    relativePath + entry.name + '/',
+                    filesToUpload
+                );
+            }
+            console.log('[ResourceManager] 目录', entry.name, '遍历完成');
+        }
+    }
+    
+    // 上传单个文件夹的文件
+    async uploadFolderFiles(folderName, files) {
+        if (files.length === 0) return;
+        
+        const category = this.currentPath || folderName;
+        
+        const formData = new FormData();
+        formData.append('category', category);
+        formData.append('name', folderName);
+        formData.append('price', '0');
+        
+        for (const item of files) {
+            formData.append('files', item.file, item.path);
+        }
+        
+        const response = await fetch(`${this.apiBaseUrl}/api/admin/store/upload`, {
+            method: 'POST',
+            body: formData
+        });
+        
+        const result = await response.json();
+        
+        if (!result.success) {
+            throw new Error(result.message || '上传失败');
+        }
+        
+        return result;
+    }
+    
+    isImageFile(fileName) {
+        const ext = fileName.split('.').pop().toLowerCase();
+        return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext);
+    }
+
+    showLoading(show) {
+        if (this.loading) {
+            if (show) {
+                this.loading.classList.add('show');
+            } else {
+                this.loading.classList.remove('show');
+            }
+        }
+    }
+
+    // 显示提示消息
+    showHint(message, type = 'info') {
+        const hintView = document.getElementById('hintView');
+        const hintMessage = document.getElementById('hintMessage');
+        
+        if (!hintView || !hintMessage) {
+            console.log(`[${type}]`, message);
+            return;
+        }
+
+        // 移除之前的类型类
+        hintView.classList.remove('success', 'error', 'warning', 'info');
+        hintView.classList.add(type);
+        
+        hintMessage.textContent = message;
+        hintView.classList.remove('hide');
+        hintView.classList.add('show');
+
+        // 自动隐藏
+        setTimeout(() => {
+            hintView.classList.remove('show');
+            hintView.classList.add('hide');
+        }, 3000);
+    }
+
+    showError(message) {
+        this.showHint(message, 'error');
+    }
+
+    showSuccess(message) {
+        this.showHint(message, 'success');
+    }
+
+    showWarning(message) {
+        this.showHint(message, 'warning');
+    }
+
+    showInfo(message) {
+        this.showHint(message, 'info');
+    }
+
+    // 显示确认对话框
+    showConfirm(message) {
+        return new Promise((resolve) => {
+            const overlay = document.getElementById('globalConfirmOverlay');
+            const messageEl = document.getElementById('confirmMessage');
+            const okBtn = document.getElementById('confirmOkBtn');
+            const cancelBtn = document.getElementById('confirmCancelBtn');
+
+            if (!overlay || !messageEl || !okBtn || !cancelBtn) {
+                // 降级到原生 confirm
+                resolve(confirm(message));
+                return;
+            }
+
+            messageEl.textContent = message;
+            overlay.classList.add('show');
+
+            const cleanup = () => {
+                overlay.classList.remove('show');
+                okBtn.removeEventListener('click', handleOk);
+                cancelBtn.removeEventListener('click', handleCancel);
+            };
+
+            const handleOk = () => {
+                cleanup();
+                resolve(true);
+            };
+
+            const handleCancel = () => {
+                cleanup();
+                resolve(false);
+            };
+
+            okBtn.addEventListener('click', handleOk);
+            cancelBtn.addEventListener('click', handleCancel);
+        });
+    }
+
+    // 显示输入对话框
+    showPrompt(message, defaultValue = '') {
+        return new Promise((resolve) => {
+            const overlay = document.getElementById('globalPromptOverlay');
+            const messageEl = document.getElementById('promptMessage');
+            const input = document.getElementById('promptInput');
+            const okBtn = document.getElementById('promptOkBtn');
+            const cancelBtn = document.getElementById('promptCancelBtn');
+
+            if (!overlay || !messageEl || !input || !okBtn || !cancelBtn) {
+                // 降级到原生 prompt
+                resolve(prompt(message, defaultValue));
+                return;
+            }
+
+            messageEl.textContent = message;
+            input.value = defaultValue;
+            overlay.classList.add('show');
+            
+            // 自动聚焦输入框
+            setTimeout(() => {
+                input.focus();
+                input.select();
+            }, 100);
+
+            const cleanup = () => {
+                overlay.classList.remove('show');
+                okBtn.removeEventListener('click', handleOk);
+                cancelBtn.removeEventListener('click', handleCancel);
+                input.removeEventListener('keydown', handleKeydown);
+            };
+
+            const handleOk = () => {
+                const value = input.value;
+                cleanup();
+                resolve(value);
+            };
+
+            const handleCancel = () => {
+                cleanup();
+                resolve(null);
+            };
+
+            const handleKeydown = (e) => {
+                if (e.key === 'Enter') {
+                    e.preventDefault();
+                    handleOk();
+                } else if (e.key === 'Escape') {
+                    e.preventDefault();
+                    handleCancel();
+                }
+            };
+
+            okBtn.addEventListener('click', handleOk);
+            cancelBtn.addEventListener('click', handleCancel);
+            input.addEventListener('keydown', handleKeydown);
+        });
+    }
+}
+
+// 导出
+if (typeof module !== 'undefined' && module.exports) {
+    module.exports = ResourceManager;
+} else {
+    window.ResourceManager = ResourceManager;
+}

+ 107 - 0
admin/js/resource-manager/right-click-menu.js

@@ -0,0 +1,107 @@
+// 右键菜单模块(复制自 client/js/disk/right-click-menu.js)
+
+(() => {
+    class ResourceManagerRightClickMenu {
+        constructor(options = {}) {
+            this.target = options.target || document.body;
+            this.menu = options.menu || null;
+            this.onAction = options.onAction || (() => {});
+            this.onBeforeShow = options.onBeforeShow || null;
+            this.lastRightClickTarget = null;
+
+            this.handleContextMenu = this.handleContextMenu.bind(this);
+            this.handleDocumentClick = this.handleDocumentClick.bind(this);
+            this.handleKeydown = this.handleKeydown.bind(this);
+
+            this.bindEvents();
+        }
+
+        bindEvents() {
+            if (!this.target || !this.menu) {
+                console.error('[RightClickMenu] 缺少 target 或 menu 元素', { target: this.target, menu: this.menu });
+                return;
+            }
+
+            this.target.addEventListener('contextmenu', this.handleContextMenu);
+            document.addEventListener('click', this.handleDocumentClick);
+            document.addEventListener('keydown', this.handleKeydown);
+
+            this.menu.addEventListener('click', (e) => {
+                const btn = e.target.closest('[data-action]');
+                if (!btn) return;
+
+                const action = btn.dataset.action;
+                this.hide();
+                this.onAction(action, e);
+            });
+        }
+
+        handleContextMenu(e) {
+            const editable = e.target.closest('input, textarea');
+            if (editable) {
+                return;
+            }
+            e.preventDefault();
+            
+            // 保存右键点击的目标元素
+            this.lastRightClickTarget = e.target;
+            
+            // 如果有回调,在显示菜单前调用
+            // 如果回调返回 false,不显示菜单
+            if (this.onBeforeShow) {
+                const shouldShow = this.onBeforeShow(e);
+                if (shouldShow === false) {
+                    return;
+                }
+            }
+            
+            this.show(e.clientX, e.clientY);
+        }
+
+        handleDocumentClick(e) {
+            // 点击重命名输入框时不隐藏菜单
+            if (e.target.classList.contains('rename-input')) {
+                return;
+            }
+            if (this.menu && !this.menu.contains(e.target)) {
+                this.hide();
+            }
+        }
+
+        handleKeydown(e) {
+            if (e.key === 'Escape') {
+                this.hide();
+            }
+        }
+
+        show(x, y) {
+            if (!this.menu) return;
+
+            this.menu.classList.add('show');
+
+            const { innerWidth, innerHeight } = window;
+            const rect = this.menu.getBoundingClientRect();
+            let left = x;
+            let top = y;
+
+            if (left + rect.width > innerWidth) {
+                left = innerWidth - rect.width - 8;
+            }
+
+            if (top + rect.height > innerHeight) {
+                top = innerHeight - rect.height - 8;
+            }
+
+            this.menu.style.left = `${left}px`;
+            this.menu.style.top = `${top}px`;
+        }
+
+        hide() {
+            if (this.menu) {
+                this.menu.classList.remove('show');
+            }
+        }
+    }
+
+    window.ResourceManagerRightClickMenu = ResourceManagerRightClickMenu;
+})();

+ 109 - 0
admin/js/resource-manager/search-bar.js

@@ -0,0 +1,109 @@
+// 搜索栏模块(复制自 client/js/disk/search-bar.js)
+
+class ResourceManagerSearchBar {
+    constructor(options = {}) {
+        this.input = options.input;
+        this.clearButton = options.clearButton;
+        this.fileList = options.fileList;
+        this.emptyState = options.emptyState;
+        this.getResources = options.getResources || (() => []);
+        this.renderAll = options.renderAll || (() => {});
+        this.createFileItem = options.createFileItem || (() => null);
+        this.noResultMessage = options.noResultMessage || '没有找到匹配的文件';
+        
+        this.isSearching = false;
+        
+        this.init();
+    }
+
+    init() {
+        if (!this.input) return;
+        
+        this.input.addEventListener('input', () => this.handleSearch());
+        this.input.addEventListener('keydown', (e) => {
+            if (e.key === 'Escape') {
+                this.clear();
+            }
+        });
+        
+        if (this.clearButton) {
+            this.clearButton.addEventListener('click', () => this.clear());
+        }
+    }
+
+    async handleSearch() {
+        const keyword = this.input.value.trim().toLowerCase();
+        
+        if (!keyword) {
+            this.clear({ noRender: false });
+            return;
+        }
+        
+        this.isSearching = true;
+        this.showClearButton();
+        
+        // 获取资源列表(可能是异步的)
+        let resources = this.getResources();
+        if (resources instanceof Promise) {
+            resources = await resources;
+        }
+        
+        // 过滤匹配的资源
+        const filtered = resources.filter(resource => {
+            const name = (resource.name || '').toLowerCase();
+            return name.includes(keyword);
+        });
+        
+        this.renderSearchResults(filtered);
+    }
+
+    renderSearchResults(resources) {
+        if (!this.fileList) return;
+        
+        this.fileList.innerHTML = '';
+        
+        if (resources.length === 0) {
+            if (this.emptyState) {
+                this.emptyState.classList.add('show');
+                const p = this.emptyState.querySelector('p');
+                if (p) p.textContent = this.noResultMessage;
+            }
+            return;
+        }
+        
+        if (this.emptyState) {
+            this.emptyState.classList.remove('show');
+        }
+        
+        resources.forEach(resource => {
+            const item = this.createFileItem(resource);
+            if (item) {
+                this.fileList.appendChild(item);
+            }
+        });
+    }
+
+    clear(options = {}) {
+        const { noRender = false } = options;
+        
+        this.input.value = '';
+        this.isSearching = false;
+        this.hideClearButton();
+        
+        if (!noRender) {
+            this.renderAll();
+        }
+    }
+
+    showClearButton() {
+        if (this.clearButton) {
+            this.clearButton.style.display = 'flex';
+        }
+    }
+
+    hideClearButton() {
+        if (this.clearButton) {
+            this.clearButton.style.display = 'none';
+        }
+    }
+}

+ 105 - 0
admin/js/resource-manager/shortcut-keys.js

@@ -0,0 +1,105 @@
+(() => {
+    /**
+     * 统一管理素材管理的键盘快捷键(复制自 client/js/disk/shortcut-keys.js)
+     */
+    class ResourceManagerShortcutKeys {
+        constructor(options = {}) {
+            this.selection = options.selection || null;
+            this.onDelete = options.onDelete || null;
+            this.onRename = options.onRename || null;
+            this.onCopy = options.onCopy || null;
+            this.onCut = options.onCut || null;
+            this.onPaste = options.onPaste || null;
+
+            this.handleKeyDown = this.handleKeyDown.bind(this);
+            document.addEventListener('keydown', this.handleKeyDown);
+            
+            // 确保 iframe 可以接收键盘事件
+            window.focus();
+            console.log('[ShortcutKeys] 快捷键模块已初始化');
+        }
+
+        destroy() {
+            document.removeEventListener('keydown', this.handleKeyDown);
+        }
+
+        handleKeyDown(e) {
+            if (this.shouldIgnoreTarget(e.target)) {
+                return;
+            }
+
+            // Delete
+            if (e.key === 'Delete') {
+                console.log('[ShortcutKeys] Delete 键被按下');
+                console.log('[ShortcutKeys] this.selection:', this.selection);
+                console.log('[ShortcutKeys] hasSelection:', this.selection ? this.selection.hasSelection() : 'N/A');
+                
+                if (this.selection && this.selection.hasSelection()) {
+                    console.log('[ShortcutKeys] 有选中项,调用 onDelete');
+                    e.preventDefault();
+                    this.onDelete && this.onDelete();
+                } else {
+                    console.log('[ShortcutKeys] 没有选中项或 selection 为空');
+                }
+                return;
+            }
+
+            // 重命名
+            if (e.key === 'F2') {
+                if (this.selection && this.selection.getSelectedCount() === 1) {
+                    e.preventDefault();
+                    this.onRename && this.onRename();
+                }
+                return;
+            }
+
+            if (!e.ctrlKey) {
+                return;
+            }
+
+            const key = e.key.toLowerCase();
+
+            switch (key) {
+                case 'a':
+                    if (this.selection) {
+                        e.preventDefault();
+                        this.selection.selectAll();
+                    }
+                    break;
+                case 'c':
+                    if (this.selection && this.selection.hasSelection() && this.onCopy) {
+                        e.preventDefault();
+                        this.onCopy();
+                    }
+                    break;
+                case 'x':
+                    if (this.selection && this.selection.hasSelection() && this.onCut) {
+                        e.preventDefault();
+                        this.onCut();
+                    }
+                    break;
+                case 'v':
+                    if (this.onPaste) {
+                        e.preventDefault();
+                        Promise.resolve(this.onPaste()).catch((error) => {
+                            console.error('粘贴失败:', error);
+                        });
+                    }
+                    break;
+                default:
+                    break;
+            }
+        }
+
+        shouldIgnoreTarget(target) {
+            if (!target) return false;
+            const tag = target.tagName;
+            if (!tag) return false;
+            if (target.isContentEditable) return true;
+            return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';
+        }
+    }
+
+    window.ResourceManagerShortcutKeys = ResourceManagerShortcutKeys;
+})();
+

+ 183 - 0
admin/js/users/users.js

@@ -0,0 +1,183 @@
+// 用户管理模块
+
+class UsersManager {
+  constructor(options = {}) {
+    this.apiBaseUrl = options.apiBaseUrl || 'http://localhost:3000';
+    this.tbody = options.tbody || null;
+    
+    this.users = [];
+    
+    this.init();
+  }
+
+  init() {
+    this.bindEvents();
+    this.loadUsers();
+  }
+
+  bindEvents() {
+    // 刷新按钮
+    const refreshBtn = document.getElementById('refreshUsersBtn');
+    if (refreshBtn) {
+      refreshBtn.addEventListener('click', () => {
+        this.loadUsers();
+      });
+    }
+
+    // 编辑用户表单
+    const editForm = document.getElementById('editUserForm');
+    if (editForm) {
+      editForm.addEventListener('submit', (e) => this.handleEditUser(e));
+    }
+
+    // 关闭编辑弹窗
+    const closeEditModal = document.getElementById('closeEditUserModal');
+    if (closeEditModal) {
+      closeEditModal.addEventListener('click', () => {
+        document.getElementById('editUserModal').style.display = 'none';
+      });
+    }
+
+    const cancelEdit = document.getElementById('cancelEditUser');
+    if (cancelEdit) {
+      cancelEdit.addEventListener('click', () => {
+        document.getElementById('editUserModal').style.display = 'none';
+      });
+    }
+  }
+
+  // 加载用户列表
+  async loadUsers() {
+    if (!this.tbody) return;
+    
+    this.tbody.innerHTML = '<tr><td colspan="6" class="loading-text">加载中...</td></tr>';
+
+    try {
+      const apiUrl = `${this.apiBaseUrl}/api/admin/users`;
+      console.log('[UsersManager] 请求用户列表,URL:', apiUrl);
+      
+      const response = await fetch(apiUrl);
+      console.log('[UsersManager] 响应状态:', response.status, response.statusText);
+      
+      if (!response.ok) {
+        throw new Error(`加载用户列表失败: ${response.status} ${response.statusText}`);
+      }
+      
+      const result = await response.json();
+      console.log('[UsersManager] 获取到的数据:', result);
+      
+      if (result.success && result.users) {
+        this.users = result.users;
+        console.log('[UsersManager] 用户数量:', this.users.length);
+        this.renderUsers();
+      } else {
+        console.warn('[UsersManager] 返回数据格式不正确或没有用户:', result);
+        this.tbody.innerHTML = '<tr><td colspan="6" class="loading-text">暂无用户数据</td></tr>';
+      }
+    } catch (error) {
+      console.error('[UsersManager] 加载用户失败:', error);
+      const errorMessage = error.message || '未知错误';
+      this.tbody.innerHTML = `<tr><td colspan="6" class="loading-text" style="color: #ef4444;">加载失败: ${errorMessage}<br>请确保服务器正在运行 (http://localhost:3000)</td></tr>`;
+    }
+  }
+
+  // 渲染用户列表
+  renderUsers() {
+    if (!this.tbody) return;
+    
+    if (this.users.length === 0) {
+      this.tbody.innerHTML = '<tr><td colspan="6" class="loading-text">暂无用户数据</td></tr>';
+      return;
+    }
+
+    this.tbody.innerHTML = this.users.map(user => `
+      <tr>
+        <td>${user.id}</td>
+        <td>${user.username}</td>
+        <td>${user.phone || '-'}</td>
+        <td>${user.points || 0}</td>
+        <td>${user.created_at ? new Date(user.created_at).toLocaleString('zh-CN') : '-'}</td>
+        <td>
+          <div class="action-buttons">
+            <button class="btn-edit" onclick="window.usersManagerInstance.editUser(${user.id})">编辑</button>
+          </div>
+        </td>
+      </tr>
+    `).join('');
+  }
+
+  // 编辑用户
+  editUser(userId) {
+    const user = this.users.find(u => u.id === userId);
+    if (!user) return;
+
+    document.getElementById('editUserId').value = user.id;
+    document.getElementById('editUsername').value = user.username;
+    document.getElementById('editPhone').value = user.phone || '';
+    document.getElementById('editPoints').value = user.points || 0;
+    document.getElementById('editUserModal').style.display = 'flex';
+  }
+
+  // 处理编辑用户
+  async handleEditUser(e) {
+    e.preventDefault();
+    
+    const userId = document.getElementById('editUserId').value;
+    const username = document.getElementById('editUsername').value;
+    const phone = document.getElementById('editPhone').value;
+    const points = parseInt(document.getElementById('editPoints').value);
+
+    try {
+      const response = await fetch(`${this.apiBaseUrl}/api/admin/users/update`, {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json'
+        },
+        body: JSON.stringify({
+          id: userId,
+          username: username,
+          phone: phone,
+          points: points
+        })
+      });
+
+      const result = await response.json();
+      
+      if (result.success) {
+        this.showSuccess('修改成功');
+        document.getElementById('editUserModal').style.display = 'none';
+        this.loadUsers();
+      } else {
+        this.showError('修改失败: ' + (result.message || '未知错误'));
+      }
+    } catch (error) {
+      console.error('[UsersManager] 修改用户失败:', error);
+      this.showError('修改失败,请稍后重试');
+    }
+  }
+
+  // 显示错误/成功消息
+  showError(message) {
+    if (window.showCustomAlert) {
+      window.showCustomAlert(message);
+    } else {
+      console.error('[UsersManager] 错误:', message);
+    }
+  }
+
+  showSuccess(message) {
+    if (window.showCustomAlert) {
+      window.showCustomAlert(message, 'success');
+    } else {
+      console.log('[UsersManager] 成功:', message);
+    }
+  }
+}
+
+// 导出
+if (typeof module !== 'undefined' && module.exports) {
+  module.exports = UsersManager;
+} else {
+  window.UsersManager = UsersManager;
+}
+

+ 344 - 0
admin/page/currency/currency.html

@@ -0,0 +1,344 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>充值与货币</title>
+  <style>
+    * {
+      margin: 0;
+      padding: 0;
+      box-sizing: border-box;
+    }
+
+    body {
+      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+      background: #f9fafb;
+      min-height: 100vh;
+    }
+
+    .page-content {
+      padding: 24px;
+    }
+
+    .section {
+      background: #fff;
+      border-radius: 12px;
+      padding: 20px;
+      margin-bottom: 20px;
+      box-shadow: 0 1px 3px rgba(0,0,0,0.08);
+    }
+
+    .section-title {
+      font-size: 15px;
+      font-weight: 600;
+      color: #111;
+      margin-bottom: 16px;
+    }
+
+    .package-list {
+      display: flex;
+      flex-direction: column;
+      gap: 12px;
+    }
+
+    .package-item {
+      display: flex;
+      align-items: center;
+      gap: 16px;
+      padding: 14px 16px;
+      background: #fafafa;
+      border-radius: 8px;
+      border: 1px solid #eee;
+    }
+
+    .package-index {
+      width: 26px;
+      height: 26px;
+      background: linear-gradient(135deg, #667eea, #764ba2);
+      color: #fff;
+      border-radius: 50%;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      font-size: 12px;
+      font-weight: 600;
+      flex-shrink: 0;
+    }
+
+    .package-fields {
+      display: flex;
+      align-items: center;
+      gap: 20px;
+      flex: 1;
+    }
+
+    .package-field {
+      display: flex;
+      align-items: center;
+      gap: 6px;
+    }
+
+    .package-field label {
+      font-size: 12px;
+      color: #666;
+      white-space: nowrap;
+    }
+
+    .package-field input {
+      width: 80px;
+      padding: 6px 10px;
+      border: 1px solid #ddd;
+      border-radius: 5px;
+      font-size: 13px;
+      outline: none;
+      transition: border-color 0.2s;
+    }
+
+    .package-field input:focus {
+      border-color: #667eea;
+    }
+
+    .package-field .unit {
+      font-size: 12px;
+      color: #999;
+    }
+
+    .package-total {
+      padding: 6px 12px;
+      background: #f0fdf4;
+      border-radius: 5px;
+      font-size: 13px;
+      color: #16a34a;
+      font-weight: 500;
+      white-space: nowrap;
+    }
+
+    .package-actions {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+      flex-shrink: 0;
+    }
+
+    .btn-confirm {
+      display: none;
+      width: 28px;
+      height: 28px;
+      background: #10b981;
+      color: #fff;
+      border: none;
+      border-radius: 50%;
+      cursor: pointer;
+      font-size: 14px;
+      align-items: center;
+      justify-content: center;
+      transition: all 0.2s;
+    }
+
+    .btn-confirm:hover {
+      background: #059669;
+      transform: scale(1.1);
+    }
+
+    .btn-confirm.show {
+      display: flex;
+    }
+
+    .btn-remove {
+      width: 28px;
+      height: 28px;
+      background: #fee2e2;
+      color: #ef4444;
+      border: none;
+      border-radius: 50%;
+      cursor: pointer;
+      font-size: 16px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      transition: all 0.2s;
+    }
+
+    .btn-remove:hover {
+      background: #fecaca;
+    }
+
+    .btn-add {
+      display: inline-flex;
+      align-items: center;
+      gap: 6px;
+      padding: 8px 14px;
+      background: #f0fdf4;
+      color: #16a34a;
+      border: 1px solid #86efac;
+      border-radius: 6px;
+      cursor: pointer;
+      font-size: 13px;
+      transition: all 0.2s;
+      margin-top: 12px;
+    }
+
+    .btn-add:hover {
+      background: #dcfce7;
+    }
+
+    .preview-section {
+      margin-top: 24px;
+      padding: 24px;
+      background: linear-gradient(145deg, #f8fafc 0%, #f1f5f9 100%);
+      border-radius: 16px;
+      border: 1px solid rgba(102, 126, 234, 0.1);
+    }
+
+    .preview-title {
+      font-size: 14px;
+      font-weight: 600;
+      color: #374151;
+      margin-bottom: 20px;
+      display: flex;
+      align-items: center;
+      gap: 8px;
+    }
+
+    .preview-title::before {
+      content: '';
+      width: 4px;
+      height: 16px;
+      background: linear-gradient(135deg, #667eea, #764ba2);
+      border-radius: 2px;
+    }
+
+    .preview-packages {
+      display: grid;
+      grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+      gap: 20px;
+    }
+
+    .preview-card {
+      padding: 28px 24px;
+      background: linear-gradient(145deg, #667eea 0%, #764ba2 50%, #9333ea 100%);
+      color: #fff;
+      border-radius: 20px;
+      text-align: center;
+      box-shadow: 
+        0 10px 30px rgba(102, 126, 234, 0.3),
+        0 4px 12px rgba(0, 0, 0, 0.1);
+      transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+      position: relative;
+      overflow: hidden;
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      justify-content: center;
+      min-height: 140px;
+    }
+
+    .preview-card::before {
+      content: '';
+      position: absolute;
+      top: -50%;
+      left: -50%;
+      width: 200%;
+      height: 200%;
+      background: radial-gradient(circle, rgba(255,255,255,0.15) 0%, transparent 60%);
+      opacity: 0;
+      transition: opacity 0.3s;
+    }
+
+    .preview-card:hover {
+      transform: translateY(-6px) scale(1.02);
+      box-shadow: 
+        0 20px 40px rgba(102, 126, 234, 0.4),
+        0 8px 20px rgba(0, 0, 0, 0.15);
+    }
+
+    .preview-card:hover::before {
+      opacity: 1;
+    }
+
+    .preview-card .points {
+      font-size: 28px;
+      font-weight: 700;
+      letter-spacing: -0.5px;
+      text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+      margin-bottom: 16px;
+    }
+
+    .preview-card .card-bottom {
+      display: flex;
+      align-items: center;
+      gap: 10px;
+      margin-top: auto;
+    }
+
+    .preview-card .bonus {
+      font-size: 12px;
+      opacity: 0.95;
+      padding: 6px 14px;
+      background: rgba(255, 255, 255, 0.15);
+      border-radius: 20px;
+      white-space: nowrap;
+    }
+
+    .preview-card .price {
+      font-size: 15px;
+      font-weight: 600;
+      background: rgba(255, 255, 255, 0.25);
+      padding: 6px 16px;
+      border-radius: 20px;
+      backdrop-filter: blur(4px);
+      box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2);
+      white-space: nowrap;
+    }
+
+    .msg {
+      padding: 10px 14px;
+      border-radius: 6px;
+      margin-bottom: 12px;
+      font-size: 13px;
+      display: none;
+    }
+
+    .msg.success {
+      background: #f0fdf4;
+      color: #16a34a;
+    }
+
+    .msg.error {
+      background: #fef2f2;
+      color: #ef4444;
+    }
+  </style>
+</head>
+<body>
+  <div class="page-content">
+    <div id="msgBox" class="msg"></div>
+
+    <div class="section">
+      <h3 class="section-title">充值套餐配置</h3>
+      <div class="package-list" id="packageList"></div>
+      <button class="btn-add" id="addBtn">+ 添加套餐</button>
+
+      <div class="preview-section">
+        <div class="preview-title">客户端预览</div>
+        <div class="preview-packages" id="preview"></div>
+      </div>
+    </div>
+  </div>
+
+  <script src="../../js/currency/currency.js"></script>
+  <script>
+    (function() {
+      let apiBaseUrl = 'http://localhost:3000';
+      try {
+        if (window.parent && window.parent.getApiBaseUrl) {
+          apiBaseUrl = window.parent.getApiBaseUrl();
+        }
+      } catch (e) {}
+      
+      window.currencyManager = new CurrencyManager({ apiBaseUrl });
+    })();
+  </script>
+</body>
+</html>

+ 43 - 0
admin/page/pricing/pricing.html

@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>定价管理</title>
+  <link rel="stylesheet" href="../../css/admin.css">
+</head>
+<body>
+  <div class="page-content active">
+    <!-- 定价资源网格 -->
+    <div class="pricing-grid" id="pricingGrid">
+      <div class="loading-text">加载中...</div>
+    </div>
+  </div>
+
+  <script src="../../js/pricing/pricing.js"></script>
+  <script>
+    // 初始化定价管理器
+    (function() {
+      let apiBaseUrl = 'http://localhost:3000';
+      
+      // 尝试从父窗口获取 API 地址(file:// 协议下会失败,使用默认值)
+      try {
+        if (window.parent && window.parent.getApiBaseUrl) {
+          apiBaseUrl = window.parent.getApiBaseUrl();
+        }
+      } catch (e) {
+        console.log('[PricingManager] 使用默认 API 地址:', apiBaseUrl);
+      }
+      
+      if (!window.pricingManager) {
+        window.pricingManager = new PricingManager({
+          apiBaseUrl: apiBaseUrl,
+          grid: document.getElementById('pricingGrid')
+        });
+        window.pricingManagerInstance = window.pricingManager;
+      }
+    })();
+  </script>
+</body>
+</html>
+

+ 177 - 0
admin/page/product-pricing/product-pricing.html

@@ -0,0 +1,177 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>商品定价</title>
+  <style>
+    * {
+      margin: 0;
+      padding: 0;
+      box-sizing: border-box;
+    }
+
+    body {
+      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+      background: #f9fafb;
+      min-height: 100vh;
+    }
+
+    .page-content {
+      padding: 24px;
+    }
+
+    .section {
+      background: #fff;
+      border-radius: 12px;
+      padding: 20px;
+      margin-bottom: 20px;
+      box-shadow: 0 1px 3px rgba(0,0,0,0.08);
+    }
+
+    .section-title {
+      font-size: 15px;
+      font-weight: 600;
+      color: #111;
+      margin-bottom: 16px;
+    }
+
+    .product-list {
+      display: flex;
+      flex-direction: column;
+      gap: 16px;
+    }
+
+    .product-item {
+      display: flex;
+      align-items: center;
+      gap: 20px;
+      padding: 16px;
+      background: #fafafa;
+      border-radius: 8px;
+      border: 1px solid #eee;
+    }
+
+    .product-info {
+      flex: 1;
+    }
+
+    .product-name {
+      font-size: 16px;
+      font-weight: 600;
+      color: #111;
+      margin-bottom: 8px;
+    }
+
+    .product-desc {
+      font-size: 13px;
+      color: #666;
+    }
+
+    .product-price {
+      display: flex;
+      align-items: center;
+      gap: 12px;
+    }
+
+    .price-label {
+      font-size: 13px;
+      color: #666;
+    }
+
+    .price-input-wrapper {
+      display: flex;
+      align-items: center;
+      gap: 6px;
+    }
+
+    .price-input {
+      width: 100px;
+      padding: 8px 12px;
+      border: 1px solid #ddd;
+      border-radius: 6px;
+      font-size: 14px;
+      outline: none;
+      transition: border-color 0.2s;
+    }
+
+    .price-input:focus {
+      border-color: #667eea;
+    }
+
+    .price-unit {
+      font-size: 13px;
+      color: #999;
+    }
+
+    .btn-confirm {
+      display: none;
+      width: 32px;
+      height: 32px;
+      background: #10b981;
+      color: #fff;
+      border: none;
+      border-radius: 50%;
+      cursor: pointer;
+      font-size: 16px;
+      align-items: center;
+      justify-content: center;
+      transition: all 0.2s;
+    }
+
+    .btn-confirm:hover {
+      background: #059669;
+      transform: scale(1.1);
+    }
+
+    .btn-confirm.show {
+      display: flex;
+    }
+
+    .msg {
+      padding: 12px 16px;
+      border-radius: 6px;
+      margin-bottom: 16px;
+      font-size: 13px;
+      display: none;
+    }
+
+    .msg.success {
+      background: #f0fdf4;
+      color: #16a34a;
+      display: block;
+    }
+
+    .msg.error {
+      background: #fef2f2;
+      color: #ef4444;
+      display: block;
+    }
+  </style>
+</head>
+<body>
+  <div class="page-content">
+    <div id="msgBox" class="msg"></div>
+
+    <div class="section">
+      <h3 class="section-title">商品定价配置</h3>
+      <div class="product-list" id="productList"></div>
+    </div>
+  </div>
+
+  <script src="../../js/product-pricing/product-pricing.js"></script>
+  <script>
+    (function() {
+      let apiBaseUrl = 'http://localhost:3000';
+      try {
+        if (window.parent && window.parent.getApiBaseUrl) {
+          apiBaseUrl = window.parent.getApiBaseUrl();
+        }
+      } catch (e) {}
+      
+      window.productPricingManager = new ProductPricingManager({ apiBaseUrl });
+    })();
+  </script>
+</body>
+</html>
+

+ 204 - 0
admin/page/resource-manager/resource-manager.html

@@ -0,0 +1,204 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>素材管理</title>
+    <link rel="stylesheet" href="../../css/resource-manager/tool-bar.css">
+    <link rel="stylesheet" href="../../css/resource-manager/disk.css">
+    <link rel="stylesheet" href="../../css/resource-manager/right-click-menu.css">
+    <link rel="stylesheet" href="../../css/resource-manager/hint-view.css">
+    <link rel="stylesheet" href="../../css/resource-manager/confirm-view.css">
+</head>
+<body>
+    <div class="disk-container" id="diskContainer">
+        <!-- 头部区域 -->
+        <div class="header">
+            <div class="breadcrumb" id="breadcrumb">
+                <span class="breadcrumb-item active" data-path="">全部文件</span>
+            </div>
+            <div class="header-right">
+                <div class="search-box">
+                    <svg class="search-icon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
+                        <path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"/>
+                    </svg>
+                    <input type="text" id="searchInput" placeholder="搜索文件或文件夹...">
+                    <button class="search-clear" id="searchClear" style="display: none;" title="清空搜索">
+                        <svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor">
+                            <path d="M7 0C3.13 0 0 3.13 0 7s3.13 7 7 7 7-3.13 7-7S10.87 0 7 0zm3.5 9.21L9.21 10.5 7 8.29 4.79 10.5 3.5 9.21 5.71 7 3.5 4.79 4.79 3.5 7 5.71 9.21 3.5 10.5 4.79 8.29 7 10.5 9.21z"/>
+                        </svg>
+                    </button>
+                </div>
+            </div>
+        </div>
+
+        <!-- 选中操作栏 -->
+        <div class="selection-bar" id="selectionBar">
+            <div class="selection-actions">
+                <button class="btn-action btn-danger" id="btnDelete">
+                    <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
+                        <path d="M5 2V1h6v1h4v2H1V2h4zm1 3h4v9H6V5zm-3 0h2v9H3V5zm8 0h2v9h-2V5z"/>
+                    </svg>
+                    删除
+                </button>
+            </div>
+            <span class="selection-count" id="selectionCount">已选中 0 项</span>
+        </div>
+
+        <!-- 文件列表区域 -->
+        <div class="file-list-container" id="dropZone">
+            <div class="drop-hint" id="dropHint">
+                <svg width="64" height="64" viewBox="0 0 64 64" fill="currentColor">
+                    <path d="M32 8L16 24h10v16h12V24h10L32 8z"/>
+                    <path d="M8 48h48v8H8z"/>
+                </svg>
+                <p>拖拽文件或文件夹到此处上传</p>
+            </div>
+            
+            <div class="file-list" id="fileList">
+                <!-- 文件项将动态生成 -->
+            </div>
+
+            <div class="empty-state" id="emptyState">
+                <svg width="120" height="120" viewBox="0 0 120 120" fill="#ddd">
+                    <path d="M30 20h40l20 20v60H30V20z"/>
+                    <path d="M70 20v20h20"/>
+                </svg>
+                <p>此文件夹为空</p>
+                <p class="hint">拖拽文件或文件夹到此处上传</p>
+            </div>
+
+            <div class="loading" id="loading">
+                <div class="spinner"></div>
+                <p>加载中...</p>
+            </div>
+        </div>
+
+        <!-- 上传进度 -->
+        <div class="upload-progress" id="uploadProgress">
+            <div class="upload-progress-header">
+                <span class="upload-progress-title">上传文件</span>
+                <button class="upload-progress-close" id="uploadProgressClose">
+                    <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
+                        <path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" stroke-width="2"/>
+                    </svg>
+                </button>
+            </div>
+            <div class="upload-progress-list" id="uploadProgressList"></div>
+        </div>
+
+        <template id="uploadProgressTemplate">
+            <div class="upload-progress-item">
+                <div class="upload-progress-icon">
+                    <svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
+                        <path d="M10 2L5 7h3v6h4V7h3L10 2z"/>
+                        <path d="M3 16h14v2H3z"/>
+                    </svg>
+                </div>
+                <div class="upload-progress-info">
+                    <div class="upload-progress-name"></div>
+                    <div class="upload-progress-status">上传中...</div>
+                    <div class="upload-progress-bar">
+                        <div class="upload-progress-bar-fill" style="width: 0%"></div>
+                    </div>
+                </div>
+            </div>
+        </template>
+
+    </div>
+
+    <!-- 右键菜单(放在 body 直接下面) -->
+    <div class="context-menu" id="contextMenu">
+        <button class="context-menu-item" data-action="new">
+            <span>新建分类</span>
+        </button>
+        <button class="context-menu-item" data-action="upload">
+            <span>上传素材</span>
+        </button>
+        <button class="context-menu-item" data-action="back">
+            <span>返回上级</span>
+        </button>
+        <div class="context-menu-divider"></div>
+        <button class="context-menu-item" data-action="rename">
+            <span>重命名</span>
+        </button>
+        <button class="context-menu-item danger" data-action="delete">
+            <span>删除</span>
+        </button>
+        <div class="context-menu-divider"></div>
+        <button class="context-menu-item" data-action="refresh">
+            <span>刷新</span>
+        </button>
+    </div>
+
+    <!-- 文件输入通过 JS 动态创建 -->
+
+    <!-- 框选区域 -->
+    <div class="selection-box" id="selectionBox"></div>
+
+    <!-- 提示消息组件 -->
+    <div class="hint-view" id="hintView">
+        <div class="hint-content">
+            <svg class="hint-icon" width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
+                <path d="M10 0C4.48 0 0 4.48 0 10s4.48 10 10 10 10-4.48 10-10S15.52 0 10 0zm-1 15l-5-5 1.41-1.41L9 12.17l7.59-7.59L18 6l-9 9z"/>
+            </svg>
+            <span class="hint-message" id="hintMessage"></span>
+        </div>
+    </div>
+
+    <!-- 全局确认对话框 -->
+    <div class="global-confirm-overlay" id="globalConfirmOverlay">
+        <div class="global-confirm-dialog">
+            <div class="confirm-content">
+                <div class="confirm-message" id="confirmMessage"></div>
+            </div>
+            <div class="confirm-actions">
+                <button class="confirm-btn confirm-cancel" id="confirmCancelBtn">取消</button>
+                <button class="confirm-btn confirm-ok" id="confirmOkBtn">确认</button>
+            </div>
+        </div>
+    </div>
+
+    <!-- 全局输入对话框 -->
+    <div class="global-confirm-overlay" id="globalPromptOverlay">
+        <div class="global-confirm-dialog">
+            <div class="confirm-content">
+                <div class="confirm-message" id="promptMessage"></div>
+                <input type="text" class="prompt-input" id="promptInput" placeholder="">
+            </div>
+            <div class="confirm-actions">
+                <button class="confirm-btn confirm-cancel" id="promptCancelBtn">取消</button>
+                <button class="confirm-btn confirm-ok" id="promptOkBtn">确认</button>
+            </div>
+        </div>
+    </div>
+
+    <script src="../../js/resource-manager/path.js"></script>
+    <script src="../../js/resource-manager/multiple-selection.js"></script>
+    <script src="../../js/resource-manager/shortcut-keys.js"></script>
+    <script src="../../js/resource-manager/search-bar.js"></script>
+    <script src="../../js/resource-manager/right-click-menu.js"></script>
+    <script src="../../js/resource-manager/resource-manager.js"></script>
+    <script>
+        // 初始化素材管理器
+        (function() {
+            let apiBaseUrl = 'http://localhost:3000';
+            
+            // 尝试从父窗口获取 API 地址(file:// 协议下会失败,使用默认值)
+            try {
+                if (window.parent && window.parent.getApiBaseUrl) {
+                    apiBaseUrl = window.parent.getApiBaseUrl();
+                }
+            } catch (e) {
+                console.log('[ResourceManager] 使用默认 API 地址:', apiBaseUrl);
+            }
+            
+            if (!window.resourceManager) {
+                window.resourceManager = new ResourceManager({
+                    apiBaseUrl: apiBaseUrl
+                });
+            }
+        })();
+    </script>
+</body>
+</html>

+ 13 - 0
admin/page/resource-manager/right-click-menu.html

@@ -0,0 +1,13 @@
+<!-- 自定义右键菜单 -->
+<div class="context-menu" id="contextMenu">
+    <button class="context-menu-item" data-action="new">
+        <span>新建文件夹</span>
+    </button>
+    <button class="context-menu-item" data-action="delete">
+        <span>删除</span>
+    </button>
+    <button class="context-menu-item" data-action="rename">
+        <span>重命名</span>
+    </button>
+</div>
+

+ 90 - 0
admin/page/users/users.html

@@ -0,0 +1,90 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>用户管理</title>
+  <link rel="stylesheet" href="../../css/admin.css">
+</head>
+<body>
+  <div class="page-content active">
+    <div class="page-header">
+      <h2>用户列表</h2>
+      <button class="btn-primary" id="refreshUsersBtn">刷新</button>
+    </div>
+    <div class="table-container">
+      <table class="data-table">
+        <thead>
+          <tr>
+            <th>ID</th>
+            <th>用户名</th>
+            <th>手机号</th>
+            <th>Ani币</th>
+            <th>注册时间</th>
+            <th>操作</th>
+          </tr>
+        </thead>
+        <tbody id="usersTableBody">
+          <tr>
+            <td colspan="6" class="loading-text">加载中...</td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+  </div>
+
+  <!-- 编辑用户弹窗 -->
+  <div id="editUserModal" class="modal" style="display: none;">
+    <div class="modal-content">
+      <div class="modal-header">
+        <h3>编辑用户</h3>
+        <button class="modal-close" id="closeEditUserModal">&times;</button>
+      </div>
+      <form id="editUserForm" class="modal-form">
+        <input type="hidden" id="editUserId">
+        <div class="form-group">
+          <label>用户名</label>
+          <input type="text" id="editUsername" required>
+        </div>
+        <div class="form-group">
+          <label>手机号</label>
+          <input type="text" id="editPhone" required>
+        </div>
+        <div class="form-group">
+          <label>Ani币</label>
+          <input type="number" id="editPoints" min="0" required>
+        </div>
+        <div class="modal-actions">
+          <button type="button" class="btn-secondary" id="cancelEditUser">取消</button>
+          <button type="submit" class="btn-primary">保存</button>
+        </div>
+      </form>
+    </div>
+  </div>
+
+  <script src="../../js/users/users.js"></script>
+  <script>
+    // 初始化用户管理器
+    (function() {
+      let apiBaseUrl = 'http://localhost:3000';
+      
+      // 尝试从父窗口获取 API 地址(file:// 协议下会失败,使用默认值)
+      try {
+        if (window.parent && window.parent.getApiBaseUrl) {
+          apiBaseUrl = window.parent.getApiBaseUrl();
+        }
+      } catch (e) {
+        console.log('[UsersManager] 使用默认 API 地址:', apiBaseUrl);
+      }
+      
+      if (!window.usersManager) {
+        window.usersManager = new UsersManager({
+          apiBaseUrl: apiBaseUrl,
+          tbody: document.getElementById('usersTableBody')
+        });
+      }
+    })();
+  </script>
+</body>
+</html>
+

BIN
admin/static/favicon.png


BIN
admin/static/logo.png


+ 581 - 0
client/css/ai-generate/ai-generate-view.css

@@ -0,0 +1,581 @@
+/* AI生图弹窗样式 */
+* {
+    margin: 0;
+    padding: 0;
+    box-sizing: border-box;
+}
+
+body {
+    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+    background: transparent;
+}
+
+/* 遮罩层 */
+.ai-generate-overlay {
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    background: rgba(0, 0, 0, 0.6);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    z-index: 10000;
+    backdrop-filter: blur(4px);
+}
+
+/* 弹窗主体 */
+.ai-generate-modal {
+    background: white;
+    border-radius: 20px;
+    width: 90%;
+    max-width: 1100px;
+    max-height: 85vh;
+    overflow: visible;
+    box-shadow: 0 25px 80px rgba(0, 0, 0, 0.3);
+    position: relative;
+    display: flex;
+    flex-direction: column;
+    margin: 20px;
+}
+
+/* 关闭按钮 */
+.modal-close-btn {
+    position: absolute;
+    top: 12px;
+    right: 12px;
+    width: 36px;
+    height: 36px;
+    border-radius: 50%;
+    background: rgba(0, 0, 0, 0.05);
+    border: none;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    color: #6b7280;
+    transition: all 0.2s;
+    z-index: 100;
+}
+
+.modal-close-btn:hover {
+    background: rgba(0, 0, 0, 0.1);
+    color: #374151;
+    transform: scale(1.1);
+}
+
+/* 预览区域容器 */
+.ai-generate-preview-container {
+    display: grid;
+    grid-template-columns: 1fr 1fr;
+    gap: 20px;
+    padding: 24px;
+    padding-top: 50px;
+    flex: 1;
+    min-height: 0;
+    overflow: auto;
+}
+
+/* 预览框通用样式 */
+.ai-generate-preview-box {
+    border-radius: 16px;
+    overflow: hidden; /* 保持 hidden 以维持圆角效果 */
+    position: relative;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    min-height: 300px;
+    max-height: 450px;
+    padding: 4px; /* 添加小量 padding,确保内部按钮不被裁剪 */
+    box-sizing: border-box; /* 确保 padding 不影响尺寸计算 */
+}
+
+/* 左侧参考图区域 */
+.ai-generate-reference-box {
+    background: linear-gradient(135deg, #f0f4ff 0%, #e8f0ff 100%);
+    border: 2px dashed #667eea;
+}
+
+/* 右侧预览区域 */
+.ai-generate-spritesheet-box {
+    background: 
+        linear-gradient(45deg, #e5e7eb 25%, transparent 25%),
+        linear-gradient(-45deg, #e5e7eb 25%, transparent 25%),
+        linear-gradient(45deg, transparent 75%, #e5e7eb 75%),
+        linear-gradient(-45deg, transparent 75%, #e5e7eb 75%);
+    background-size: 20px 20px;
+    background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
+    background-color: #f9fafb;
+    border: 1px solid #e5e7eb;
+}
+
+/* 预览图 */
+.ai-generate-preview-box img {
+    max-width: 100%;
+    max-height: 100%;
+    object-fit: contain;
+    display: none;
+}
+
+.ai-generate-preview-box img.show {
+    display: block;
+}
+
+/* 上传区域 */
+.reference-upload-area {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    cursor: pointer;
+    padding: 40px;
+    width: 100%;
+    height: 100%;
+    transition: all 0.3s;
+}
+
+.reference-upload-area:hover {
+    background: rgba(102, 126, 234, 0.1);
+}
+
+.reference-upload-area.hide {
+    display: none;
+}
+
+.upload-icon {
+    font-size: 48px;
+    color: #667eea;
+    margin-bottom: 12px;
+    font-weight: 300;
+}
+
+.upload-text {
+    color: #667eea;
+    font-size: 14px;
+    font-weight: 500;
+}
+
+/* 参考图容器 */
+.reference-image-wrapper {
+    position: relative;
+    width: 100%;
+    height: 100%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+
+.reference-image {
+    max-width: 100%;
+    max-height: 100%;
+    object-fit: contain;
+    display: block !important; /* 覆盖 .ai-generate-preview-box img 的 display: none */
+}
+
+/* 删除参考图按钮 */
+.reference-remove-btn {
+    position: absolute;
+    top: 8px; /* 距离顶部足够远,不会被裁剪 */
+    right: 8px; /* 距离右边足够远,不会被裁剪 */
+    width: 32px;
+    height: 32px;
+    border-radius: 50%;
+    background: rgba(0, 0, 0, 0.6);
+    border: none;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    color: white;
+    transition: all 0.2s;
+    z-index: 10; /* 确保按钮在最上层 */
+}
+
+.reference-remove-btn:hover {
+    background: #ef4444;
+    transform: scale(1.1);
+}
+
+/* 加载状态 */
+.preview-placeholder {
+    position: absolute;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    gap: 16px;
+    color: #9ca3af;
+}
+
+.preview-placeholder.hide {
+    display: none;
+}
+
+.loading-spinner {
+    width: 40px;
+    height: 40px;
+    border: 4px solid #e5e7eb;
+    border-top-color: #667eea;
+    border-radius: 50%;
+    animation: spin 0.8s linear infinite;
+}
+
+@keyframes spin {
+    to { transform: rotate(360deg); }
+}
+
+.loading-text {
+    font-size: 14px;
+    color: #6b7280;
+}
+
+/* AI生图队列区域 */
+.ai-queue-section {
+    padding: 0 20px 16px;
+}
+
+.ai-queue-title {
+    font-size: 13px;
+    font-weight: 600;
+    color: #667eea;
+    margin-bottom: 12px;
+    display: flex;
+    align-items: center;
+    gap: 6px;
+}
+
+.ai-queue-title::before {
+    content: '';
+    width: 3px;
+    height: 14px;
+    background: linear-gradient(135deg, #667eea, #764ba2);
+    border-radius: 2px;
+}
+
+.ai-queue-list {
+    display: flex;
+    gap: 12px;
+    overflow-x: auto;
+    overflow-y: hidden;
+    padding-bottom: 10px;
+    scroll-behavior: smooth;
+    -webkit-overflow-scrolling: touch;
+}
+
+.ai-queue-list::-webkit-scrollbar {
+    height: 6px;
+}
+
+.ai-queue-list::-webkit-scrollbar-track {
+    background: #f1f1f1;
+    border-radius: 3px;
+}
+
+.ai-queue-list::-webkit-scrollbar-thumb {
+    background: linear-gradient(135deg, #667eea, #764ba2);
+    border-radius: 3px;
+}
+
+.ai-queue-item {
+    flex-shrink: 0;
+    width: 100px;
+    height: 100px;
+    border-radius: 12px;
+    overflow: hidden;
+    position: relative;
+    background: #f8fafc;
+    border: 2px solid #e5e7eb;
+    cursor: pointer;
+    transition: all 0.3s;
+}
+
+.ai-queue-item:hover {
+    border-color: #667eea;
+    transform: translateY(-2px);
+    box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
+}
+
+.ai-queue-item.rendering {
+    border-color: #667eea;
+}
+
+.ai-queue-item.completed {
+    border-color: #10b981;
+}
+
+.ai-queue-item.failed {
+    border-color: #ef4444;
+}
+
+.ai-queue-item-preview {
+    width: 100%;
+    height: 100%;
+    object-fit: cover;
+    filter: blur(4px) brightness(0.8);
+}
+
+.ai-queue-item.completed .ai-queue-item-preview {
+    filter: none;
+}
+
+.ai-queue-item-overlay {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    background: rgba(0, 0, 0, 0.3);
+}
+
+.ai-queue-item.completed .ai-queue-item-overlay {
+    background: transparent;
+    opacity: 0;
+    transition: opacity 0.2s;
+}
+
+.ai-queue-item.completed:hover .ai-queue-item-overlay {
+    opacity: 1;
+    background: rgba(0, 0, 0, 0.5);
+}
+
+.ai-queue-item-spinner {
+    width: 24px;
+    height: 24px;
+    border: 3px solid rgba(255, 255, 255, 0.3);
+    border-top-color: white;
+    border-radius: 50%;
+    animation: spin 1s linear infinite;
+}
+
+.ai-queue-item-status {
+    font-size: 10px;
+    color: white;
+    margin-top: 6px;
+    font-weight: 500;
+    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
+}
+
+.ai-queue-item-icon {
+    font-size: 20px;
+}
+
+.ai-queue-item-actions {
+    display: flex;
+    gap: 4px;
+}
+
+.ai-queue-item-action {
+    width: 28px;
+    height: 28px;
+    border-radius: 50%;
+    border: none;
+    background: white;
+    color: #667eea;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    transition: all 0.2s;
+}
+
+.ai-queue-item-action:hover {
+    background: #667eea;
+    color: white;
+    transform: scale(1.1);
+}
+
+/* 提示词配置区域 */
+.prompt-config-section {
+    padding: 0 20px 20px;
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+}
+
+.config-row {
+    display: flex;
+    gap: 12px;
+}
+
+.config-textarea {
+    flex: 1;
+    min-height: 80px;
+    padding: 14px;
+    border: 2px solid #e5e7eb;
+    border-radius: 12px;
+    font-size: 14px;
+    resize: vertical;
+    transition: border-color 0.2s;
+    font-family: inherit;
+}
+
+.config-textarea:focus {
+    outline: none;
+    border-color: #667eea;
+}
+
+.config-textarea::placeholder {
+    color: #9ca3af;
+}
+
+/* 操作按钮区域 */
+.prompt-actions {
+    display: flex;
+    justify-content: flex-end;
+    gap: 12px;
+}
+
+.action-btn {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    padding: 12px 24px;
+    border-radius: 12px;
+    font-size: 14px;
+    font-weight: 600;
+    cursor: pointer;
+    transition: all 0.2s;
+    border: none;
+}
+
+.generate-action-btn {
+    background: linear-gradient(135deg, #10b981 0%, #059669 100%);
+    color: white;
+    box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
+}
+
+.generate-action-btn:hover:not(:disabled) {
+    transform: translateY(-2px);
+    box-shadow: 0 6px 20px rgba(16, 185, 129, 0.4);
+}
+
+.generate-action-btn:disabled {
+    opacity: 0.5;
+    cursor: not-allowed;
+    transform: none;
+}
+
+/* 飞走动画 */
+.ai-task-fly-animation {
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    color: white;
+    padding: 12px 20px;
+    border-radius: 12px;
+    box-shadow: 0 8px 32px rgba(102, 126, 234, 0.4);
+    display: flex;
+    align-items: center;
+    gap: 8px;
+}
+
+/* 图片预览弹窗 */
+.image-preview-modal {
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    z-index: 200000;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    opacity: 0;
+    transition: opacity 0.3s ease;
+}
+
+.image-preview-modal.show {
+    opacity: 1;
+}
+
+.image-preview-modal-backdrop {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    background: rgba(0, 0, 0, 0.85);
+    backdrop-filter: blur(8px);
+}
+
+.image-preview-modal-content {
+    position: relative;
+    max-width: 90vw;
+    max-height: 90vh;
+    transform: scale(0.9);
+    transition: transform 0.3s ease;
+}
+
+.image-preview-modal.show .image-preview-modal-content {
+    transform: scale(1);
+}
+
+.image-preview-modal-content img {
+    max-width: 100%;
+    max-height: 85vh;
+    border-radius: 12px;
+    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
+}
+
+.image-preview-modal-close {
+    position: absolute;
+    top: -40px;
+    right: 0;
+    width: 36px;
+    height: 36px;
+    border: none;
+    background: rgba(255, 255, 255, 0.2);
+    color: white;
+    font-size: 24px;
+    border-radius: 50%;
+    cursor: pointer;
+    transition: all 0.2s;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    line-height: 1;
+}
+
+.image-preview-modal-close:hover {
+    background: rgba(255, 255, 255, 0.3);
+    transform: scale(1.1);
+}
+
+.image-preview-modal-download {
+    position: absolute;
+    bottom: -50px;
+    left: 50%;
+    transform: translateX(-50%);
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    padding: 10px 24px;
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    color: white;
+    text-decoration: none;
+    border-radius: 25px;
+    font-size: 14px;
+    font-weight: 600;
+    transition: all 0.2s;
+    box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
+}
+
+.image-preview-modal-download:hover {
+    transform: translateX(-50%) translateY(-2px);
+    box-shadow: 0 6px 20px rgba(102, 126, 234, 0.5);
+}
+
+/* 响应式设计 */
+@media (max-width: 768px) {
+    .ai-generate-preview-container {
+        grid-template-columns: 1fr;
+    }
+    
+    .ai-generate-preview-box {
+        min-height: 200px;
+    }
+}
+

+ 400 - 4
client/css/export-view/export-view.css

@@ -119,6 +119,16 @@ html, body {
     background: #fafbfc;
 }
 
+/* 单列预览容器(仅显示Spritesheet) */
+.export-preview-container.export-preview-single {
+    justify-content: center;
+}
+
+.export-preview-container.export-preview-single .export-spritesheet-box {
+    max-width: 800px;
+    width: 100%;
+}
+
 /* 预览图框 - 左右各占一半 */
 .export-preview-box {
     flex: 1;
@@ -300,11 +310,73 @@ html, body {
 }
 
 
-/* 悬浮下载按钮 - 右下角 */
-.floating-download-btn {
+/* 悬浮按钮 - 右下角 */
+.floating-btn-group {
     position: absolute;
     bottom: 16px;
     right: 16px;
+    display: flex;
+    flex-direction: column;
+    gap: 10px;
+    z-index: 100;
+}
+
+/* 悬浮AI按钮 */
+.floating-ai-btn {
+    width: 46px;
+    height: 46px;
+    border-radius: 50%;
+    background: linear-gradient(135deg, #10b981 0%, #059669 100%);
+    border: none;
+    color: white;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    box-shadow: 
+        0 6px 20px rgba(16, 185, 129, 0.4),
+        0 3px 10px rgba(0, 0, 0, 0.15);
+    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+    padding: 0;
+    overflow: hidden;
+}
+
+.floating-ai-btn:hover:not(:disabled) {
+    background: linear-gradient(135deg, #34d399 0%, #10b981 100%);
+    box-shadow: 
+        0 10px 28px rgba(16, 185, 129, 0.5),
+        0 5px 14px rgba(0, 0, 0, 0.2);
+    transform: translateY(-3px) scale(1.08);
+}
+
+.floating-ai-btn:active:not(:disabled) {
+    transform: translateY(-1px) scale(1.03);
+}
+
+.floating-ai-btn:disabled {
+    opacity: 0.4;
+    cursor: not-allowed;
+    transform: none;
+    box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
+    background: linear-gradient(135deg, #d1d5db 0%, #9ca3af 100%);
+}
+
+.floating-ai-btn svg {
+    width: 20px;
+    height: 20px;
+    flex-shrink: 0;
+    filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.2));
+}
+
+.floating-ai-btn.active {
+    background: linear-gradient(135deg, #34d399 0%, #10b981 100%);
+    box-shadow: 
+        0 0 0 3px rgba(16, 185, 129, 0.3),
+        0 6px 20px rgba(16, 185, 129, 0.4);
+}
+
+/* 悬浮下载按钮 */
+.floating-download-btn {
     width: 52px;
     height: 52px;
     border-radius: 50%;
@@ -319,7 +391,6 @@ html, body {
         0 8px 24px rgba(102, 126, 234, 0.4),
         0 4px 12px rgba(0, 0, 0, 0.15);
     transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
-    z-index: 100;
     padding: 0;
     overflow: hidden;
 }
@@ -842,8 +913,18 @@ html, body {
 }
 
 .download-option-icon {
-    font-size: 32px;
+    width: 32px;
+    height: 32px;
     flex-shrink: 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    color: #667eea;
+}
+
+.download-option-icon svg {
+    width: 100%;
+    height: 100%;
 }
 
 .download-option-info {
@@ -863,3 +944,318 @@ html, body {
     line-height: 1.5;
 }
 
+.download-option-price {
+    margin-top: 8px;
+    font-size: 16px;
+    font-weight: 700;
+    color: #667eea;
+    background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
+    padding: 6px 12px;
+    border-radius: 8px;
+    display: inline-block;
+    border: 2px solid rgba(102, 126, 234, 0.3);
+    box-shadow: 0 2px 8px rgba(102, 126, 234, 0.15);
+    transition: all 0.3s ease;
+}
+
+.download-option:hover .download-option-price {
+    background: linear-gradient(135deg, rgba(102, 126, 234, 0.15) 0%, rgba(118, 75, 162, 0.15) 100%);
+    border-color: rgba(102, 126, 234, 0.5);
+    box-shadow: 0 4px 12px rgba(102, 126, 234, 0.25);
+    transform: scale(1.05);
+}
+
+/* AI生图队列区域 */
+.ai-queue-section {
+    padding: 0 20px 16px;
+}
+
+.ai-queue-title {
+    font-size: 13px;
+    font-weight: 600;
+    color: #667eea;
+    margin-bottom: 12px;
+    display: flex;
+    align-items: center;
+    gap: 6px;
+}
+
+.ai-queue-title::before {
+    content: '';
+    width: 3px;
+    height: 14px;
+    background: linear-gradient(135deg, #667eea, #764ba2);
+    border-radius: 2px;
+}
+
+.ai-queue-list {
+    display: flex;
+    gap: 12px;
+    overflow-x: auto;
+    overflow-y: hidden;
+    padding-bottom: 10px;
+    scroll-behavior: smooth;
+    -webkit-overflow-scrolling: touch;
+}
+
+.ai-queue-list::-webkit-scrollbar {
+    height: 6px;
+}
+
+.ai-queue-list::-webkit-scrollbar-track {
+    background: #f1f1f1;
+    border-radius: 3px;
+}
+
+.ai-queue-list::-webkit-scrollbar-thumb {
+    background: linear-gradient(135deg, #667eea, #764ba2);
+    border-radius: 3px;
+}
+
+.ai-queue-list::-webkit-scrollbar-thumb:hover {
+    background: linear-gradient(135deg, #5a6fd6, #6a4190);
+}
+
+.ai-queue-item {
+    flex-shrink: 0;
+    width: 100px;
+    height: 100px;
+    border-radius: 12px;
+    overflow: hidden;
+    position: relative;
+    background: #f8fafc;
+    border: 2px solid #e5e7eb;
+    cursor: pointer;
+    transition: all 0.3s;
+}
+
+.ai-queue-item:hover {
+    border-color: #667eea;
+    transform: translateY(-2px);
+    box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
+}
+
+.ai-queue-item.rendering {
+    border-color: #667eea;
+}
+
+.ai-queue-item.completed {
+    border-color: #10b981;
+}
+
+.ai-queue-item.failed {
+    border-color: #ef4444;
+}
+
+.ai-queue-item-preview {
+    width: 100%;
+    height: 100%;
+    object-fit: cover;
+    filter: blur(4px) brightness(0.8);
+}
+
+.ai-queue-item.completed .ai-queue-item-preview {
+    filter: none;
+}
+
+.ai-queue-item-overlay {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    background: rgba(0, 0, 0, 0.3);
+}
+
+.ai-queue-item.completed .ai-queue-item-overlay {
+    background: transparent;
+    opacity: 0;
+    transition: opacity 0.2s;
+}
+
+.ai-queue-item.completed:hover .ai-queue-item-overlay {
+    opacity: 1;
+    background: rgba(0, 0, 0, 0.5);
+}
+
+.ai-queue-item-spinner {
+    width: 24px;
+    height: 24px;
+    border: 3px solid rgba(255, 255, 255, 0.3);
+    border-top-color: white;
+    border-radius: 50%;
+    animation: spin 1s linear infinite;
+}
+
+.ai-queue-item-status {
+    font-size: 10px;
+    color: white;
+    margin-top: 6px;
+    font-weight: 500;
+    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
+}
+
+.ai-queue-item-icon {
+    font-size: 20px;
+}
+
+.ai-queue-item-actions {
+    display: flex;
+    gap: 4px;
+}
+
+.ai-queue-item-action {
+    width: 28px;
+    height: 28px;
+    border-radius: 50%;
+    border: none;
+    background: white;
+    color: #667eea;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    transition: all 0.2s;
+}
+
+.ai-queue-item-action:hover {
+    background: #667eea;
+    color: white;
+    transform: scale(1.1);
+}
+
+/* AI生图任务飞走动画 */
+.ai-task-fly-animation {
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    color: white;
+    padding: 12px 20px;
+    border-radius: 12px;
+    box-shadow: 0 8px 32px rgba(102, 126, 234, 0.4);
+    display: flex;
+    align-items: center;
+    gap: 8px;
+}
+
+.ai-task-fly-animation .fly-content {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    font-size: 14px;
+    font-weight: 600;
+    white-space: nowrap;
+}
+
+.ai-task-fly-animation svg {
+    fill: rgba(255, 255, 255, 0.3);
+    animation: sparkle 0.3s ease-in-out infinite alternate;
+}
+
+@keyframes sparkle {
+    from {
+        filter: drop-shadow(0 0 2px rgba(255, 255, 255, 0.5));
+    }
+    to {
+        filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.8));
+    }
+}
+
+/* 图片放大预览弹窗 */
+.image-preview-modal {
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    z-index: 200000;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    opacity: 0;
+    transition: opacity 0.3s ease;
+}
+
+.image-preview-modal.show {
+    opacity: 1;
+}
+
+.image-preview-modal-backdrop {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    background: rgba(0, 0, 0, 0.85);
+    backdrop-filter: blur(8px);
+}
+
+.image-preview-modal-content {
+    position: relative;
+    max-width: 90vw;
+    max-height: 90vh;
+    transform: scale(0.9);
+    transition: transform 0.3s ease;
+}
+
+.image-preview-modal.show .image-preview-modal-content {
+    transform: scale(1);
+}
+
+.image-preview-modal-content img {
+    max-width: 100%;
+    max-height: 85vh;
+    border-radius: 12px;
+    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
+}
+
+.image-preview-modal-close {
+    position: absolute;
+    top: -40px;
+    right: 0;
+    width: 36px;
+    height: 36px;
+    border: none;
+    background: rgba(255, 255, 255, 0.2);
+    color: white;
+    font-size: 24px;
+    border-radius: 50%;
+    cursor: pointer;
+    transition: all 0.2s;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    line-height: 1;
+}
+
+.image-preview-modal-close:hover {
+    background: rgba(255, 255, 255, 0.3);
+    transform: scale(1.1);
+}
+
+.image-preview-modal-download {
+    position: absolute;
+    bottom: -50px;
+    left: 50%;
+    transform: translateX(-50%);
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    padding: 10px 24px;
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    color: white;
+    text-decoration: none;
+    border-radius: 25px;
+    font-size: 14px;
+    font-weight: 600;
+    transition: all 0.2s;
+    box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
+}
+
+.image-preview-modal-download:hover {
+    transform: translateX(-50%) translateY(-2px);
+    box-shadow: 0 6px 20px rgba(102, 126, 234, 0.5);
+}
+

+ 76 - 0
client/css/hint-view.css

@@ -0,0 +1,76 @@
+/* 提示消息组件样式 */
+
+.hint-view {
+    position: fixed;
+    top: 20px;
+    left: 50%;
+    transform: translateX(-50%);
+    z-index: 1000002;
+    pointer-events: none;
+    opacity: 0;
+    transition: opacity 0.3s ease, transform 0.3s ease;
+}
+
+.hint-view.show {
+    opacity: 1;
+    transform: translateX(-50%) translateY(0);
+    pointer-events: auto;
+}
+
+.hint-view.hide {
+    opacity: 0;
+    transform: translateX(-50%) translateY(-20px);
+    pointer-events: none;
+}
+
+.hint-content {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    padding: 12px 20px;
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    color: white;
+    border-radius: 8px;
+    box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
+    min-width: 200px;
+    max-width: 400px;
+    font-size: 14px;
+    font-weight: 500;
+}
+
+.hint-icon {
+    flex-shrink: 0;
+    width: 20px;
+    height: 20px;
+    color: white;
+}
+
+.hint-message {
+    flex: 1;
+    text-align: center;
+}
+
+/* 成功提示样式 */
+.hint-view.success .hint-content {
+    background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%);
+    box-shadow: 0 4px 12px rgba(82, 196, 26, 0.4);
+}
+
+/* 错误提示样式 */
+.hint-view.error .hint-content {
+    background: linear-gradient(135deg, #ff4d4f 0%, #cf1322 100%);
+    box-shadow: 0 4px 12px rgba(255, 77, 79, 0.4);
+}
+
+/* 警告提示样式 */
+.hint-view.warning .hint-content {
+    background: linear-gradient(135deg, #faad14 0%, #d48806 100%);
+    box-shadow: 0 4px 12px rgba(250, 173, 20, 0.4);
+}
+
+/* 信息提示样式 */
+.hint-view.info .hint-content {
+    background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
+    box-shadow: 0 4px 12px rgba(24, 144, 255, 0.4);
+}
+

+ 163 - 0
client/css/pay-view/pay-view.css

@@ -0,0 +1,163 @@
+/* 支付界面样式 */
+
+.pay-view-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background: rgba(0, 0, 0, 0.6);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000003;
+  opacity: 0;
+  transition: opacity 0.3s ease;
+  pointer-events: none;
+}
+
+.pay-view-overlay.show {
+  opacity: 1;
+  pointer-events: auto;
+}
+
+.pay-view-container {
+  background: white;
+  border-radius: 12px;
+  width: 90%;
+  max-width: 480px;
+  max-height: 90vh;
+  overflow: hidden;
+  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
+  transform: scale(0.9);
+  transition: transform 0.3s ease;
+}
+
+.pay-view-overlay.show .pay-view-container {
+  transform: scale(1);
+}
+
+.pay-view-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 20px 24px;
+  border-bottom: 1px solid #e5e5e5;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+}
+
+.pay-view-title {
+  margin: 0;
+  font-size: 18px;
+  font-weight: 600;
+}
+
+.pay-view-close {
+  background: none;
+  border: none;
+  color: white;
+  font-size: 28px;
+  line-height: 1;
+  cursor: pointer;
+  padding: 0;
+  width: 32px;
+  height: 32px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border-radius: 4px;
+  transition: background 0.2s;
+}
+
+.pay-view-close:hover {
+  background: rgba(255, 255, 255, 0.2);
+}
+
+.pay-view-content {
+  padding: 32px 24px;
+}
+
+.pay-info {
+  text-align: center;
+  margin-bottom: 32px;
+}
+
+.pay-item-name {
+  font-size: 18px;
+  font-weight: 600;
+  color: #333;
+  margin-bottom: 8px;
+}
+
+.pay-item-price {
+  font-size: 32px;
+  font-weight: 700;
+  color: #667eea;
+  margin-bottom: 4px;
+}
+
+.qr-code-container {
+  text-align: center;
+  margin-bottom: 32px;
+}
+
+.qr-code-placeholder {
+  display: inline-block;
+  padding: 20px;
+  background: white;
+  border: 2px solid #e5e5e5;
+  border-radius: 8px;
+  margin-bottom: 16px;
+}
+
+.qr-code-hint {
+  font-size: 16px;
+  color: #666;
+  margin: 8px 0;
+}
+
+.qr-code-tip {
+  font-size: 14px;
+  color: #999;
+  margin: 4px 0;
+}
+
+.pay-view-actions {
+  display: flex;
+  justify-content: center;
+  gap: 12px;
+}
+
+.pay-confirm-btn,
+.pay-cancel-btn {
+  flex: 1;
+  padding: 12px 24px;
+  border: none;
+  border-radius: 8px;
+  font-size: 16px;
+  font-weight: 500;
+  cursor: pointer;
+  transition: all 0.2s;
+}
+
+.pay-confirm-btn {
+  background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%);
+  color: white;
+  box-shadow: 0 2px 8px rgba(82, 196, 26, 0.3);
+}
+
+.pay-confirm-btn:hover {
+  transform: translateY(-1px);
+  box-shadow: 0 4px 12px rgba(82, 196, 26, 0.4);
+}
+
+.pay-cancel-btn {
+  background: #f5f5f5;
+  color: #666;
+}
+
+.pay-cancel-btn:hover {
+  background: #e5e5e5;
+}
+

+ 1014 - 0
client/css/profile/profile.css

@@ -0,0 +1,1014 @@
+/* 我的界面样式 */
+
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+}
+
+html, body {
+  width: 100%;
+  height: 100%;
+  margin: 0;
+  padding: 0;
+  overflow: hidden;
+}
+
+.profile-container {
+  width: 100%;
+  height: 100vh;
+  background: #f5f5f5;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+}
+
+.profile-header {
+  display: flex;
+  align-items: center;
+  padding: 16px 20px;
+  background: white;
+  border-bottom: 1px solid #e5e5e5;
+  position: sticky;
+  top: 0;
+  z-index: 100;
+}
+
+.back-btn {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  background: none;
+  border: none;
+  color: #667eea;
+  font-size: 14px;
+  font-weight: 500;
+  cursor: pointer;
+  padding: 8px 12px;
+  border-radius: 8px;
+  transition: background 0.2s;
+}
+
+.back-btn:hover {
+  background: rgba(102, 126, 234, 0.1);
+}
+
+.profile-content {
+  padding: 16px 20px;
+  width: 100%;
+  max-width: 1600px;
+  margin: 0 auto;
+  overflow: hidden;
+  height: calc(100vh - 80px);
+  display: flex;
+  flex-direction: column;
+}
+
+.page-title {
+  margin: 0 0 20px 0;
+  font-size: 28px;
+  font-weight: 700;
+  color: #1f2937;
+  padding-bottom: 12px;
+  border-bottom: 3px solid #667eea;
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+
+.page-title::before {
+  content: '';
+  width: 4px;
+  height: 28px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  border-radius: 2px;
+}
+
+.profile-layout {
+  display: grid;
+  grid-template-columns: 320px 1fr;
+  gap: 16px;
+  align-items: start;
+  height: calc(100% - 60px);
+  overflow: hidden;
+}
+
+.profile-left {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  height: 100%;
+}
+
+.profile-right {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+  height: 100%;
+  overflow-y: auto;
+}
+
+.history-section {
+  display: flex;
+  flex-direction: column;
+  flex: 1;
+  min-height: 0;
+  overflow: hidden;
+}
+
+.history-section .ai-history-container {
+  flex: 1;
+  overflow: hidden;
+  min-height: 0;
+}
+
+.purchase-section {
+  display: flex;
+  flex-direction: column;
+  flex: 1;
+  min-height: 0;
+  overflow: hidden;
+}
+
+.purchase-container {
+  flex: 1;
+  max-height: 100%;
+  overflow-y: auto;
+  overflow-x: hidden;
+  min-height: 0;
+}
+
+.profile-section {
+  background: white;
+  border-radius: 12px;
+  padding: 16px;
+  margin-bottom: 0;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+  flex-shrink: 0;
+}
+
+.section-title {
+  margin: 0 0 16px 0;
+  font-size: 16px;
+  font-weight: 600;
+  color: #1f2937;
+  padding-bottom: 10px;
+  border-bottom: 2px solid #f0f0f0;
+}
+
+.profile-info {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+  align-items: center;
+}
+
+.avatar-section {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 10px;
+}
+
+.avatar-preview {
+  width: 100px;
+  height: 100px;
+  border-radius: 50%;
+  overflow: hidden;
+  border: 3px solid #e5e5e5;
+  background: #f9fafb;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+}
+
+.avatar-preview img {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
+
+.avatar-upload-btn {
+  padding: 8px 16px;
+  background: #667eea;
+  color: white;
+  border: none;
+  border-radius: 8px;
+  font-size: 14px;
+  font-weight: 500;
+  cursor: pointer;
+  transition: all 0.2s;
+}
+
+.avatar-upload-btn:hover {
+  background: #5568d3;
+  transform: translateY(-1px);
+}
+
+.info-form {
+  width: 100%;
+}
+
+.form-item {
+  margin-bottom: 14px;
+}
+
+.form-label {
+  display: block;
+  font-size: 13px;
+  font-weight: 500;
+  color: #374151;
+  margin-bottom: 6px;
+}
+
+.form-input {
+  width: 100%;
+  padding: 8px 12px;
+  border: 1px solid #e5e5e5;
+  border-radius: 8px;
+  font-size: 14px;
+  color: #1f2937;
+  transition: border-color 0.2s;
+  box-sizing: border-box;
+}
+
+.form-input:focus {
+  outline: none;
+  border-color: #667eea;
+}
+
+.form-input[readonly] {
+  background: #f9fafb;
+  color: #6b7280;
+  cursor: not-allowed;
+}
+
+.save-btn {
+  padding: 10px 24px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+  border: none;
+  border-radius: 8px;
+  font-size: 14px;
+  font-weight: 600;
+  cursor: pointer;
+  transition: all 0.2s;
+  margin-top: 8px;
+}
+
+.save-btn:hover {
+  transform: translateY(-1px);
+  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
+}
+
+.points-section {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  flex-wrap: wrap;
+  gap: 14px;
+}
+
+.points-display {
+  display: flex;
+  align-items: baseline;
+  gap: 8px;
+}
+
+.points-label {
+  font-size: 14px;
+  color: #6b7280;
+}
+
+.points-value {
+  font-size: 28px;
+  font-weight: 700;
+  color: #667eea;
+}
+
+.points-unit {
+  font-size: 14px;
+  color: #6b7280;
+}
+
+.recharge-btn {
+  padding: 8px 20px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+  border: none;
+  border-radius: 8px;
+  font-size: 13px;
+  font-weight: 600;
+  cursor: pointer;
+  transition: all 0.2s;
+}
+
+.recharge-btn:hover {
+  transform: translateY(-1px);
+  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
+}
+
+.ai-history-container {
+  min-height: 200px;
+  max-height: 100%;
+  overflow-y: auto;
+  overflow-x: hidden;
+}
+
+.loading-state,
+.empty-state {
+  text-align: center;
+  padding: 40px;
+  color: #9ca3af;
+  font-size: 14px;
+}
+
+.history-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
+  gap: 12px;
+  padding: 4px;
+  overflow: visible;
+}
+
+.history-item {
+  position: relative;
+  border-radius: 12px;
+  overflow: hidden;
+  background: #f9fafb;
+  aspect-ratio: 1;
+  border: 2px solid #e5e5e5;
+  transition: all 0.3s ease;
+}
+
+.history-item:hover {
+  border-color: #667eea;
+  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
+  transform: translateY(-2px);
+}
+
+.history-item.rendering {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: linear-gradient(135deg, #f0f0f0 0%, #e5e5e5 100%);
+}
+
+.history-item.rendering .rendering-content {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 8px;
+}
+
+/* 模糊预览图 */
+.history-item-preview {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+  filter: blur(8px) brightness(0.7);
+  transform: scale(1.1); /* 防止模糊边缘漏出 */
+}
+
+.history-item-preview.failed-preview {
+  filter: blur(8px) brightness(0.5) grayscale(0.5);
+}
+
+/* 加载中蒙层 */
+.history-item-loading-overlay {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  gap: 12px;
+  z-index: 2;
+}
+
+.loading-spinner-container {
+  width: 48px;
+  height: 48px;
+  position: relative;
+}
+
+.loading-spinner {
+  width: 100%;
+  height: 100%;
+  border: 3px solid rgba(255, 255, 255, 0.3);
+  border-top-color: #fff;
+  border-radius: 50%;
+  animation: spin 1s linear infinite;
+  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
+}
+
+.loading-status-text {
+  font-size: 13px;
+  font-weight: 600;
+  color: #fff;
+  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
+  padding: 4px 12px;
+  background: rgba(0, 0, 0, 0.3);
+  border-radius: 12px;
+  backdrop-filter: blur(4px);
+}
+
+/* rendering 和 queued 状态共用基础样式 */
+.history-item.rendering,
+.history-item.queued {
+  overflow: hidden;
+  background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
+}
+
+.history-item.failed {
+  overflow: hidden;
+  background: linear-gradient(135deg, #2d1b1b 0%, #1a1a1a 100%);
+}
+
+.history-item.failed .failed-content {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+  z-index: 2;
+  background: rgba(0, 0, 0, 0.4);
+}
+
+.history-item.failed .failed-icon {
+  font-size: 32px;
+}
+
+.history-item.failed .failed-text {
+  font-size: 14px;
+  font-weight: 600;
+  color: #fca5a5;
+  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
+}
+
+/* 重新生图按钮 */
+.history-item.failed .retry-btn {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  margin-top: 8px;
+  padding: 8px 16px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+  border: none;
+  border-radius: 20px;
+  font-size: 12px;
+  font-weight: 600;
+  cursor: pointer;
+  transition: all 0.3s ease;
+  box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
+}
+
+.history-item.failed .retry-btn:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 6px 20px rgba(102, 126, 234, 0.5);
+}
+
+.history-item.failed .retry-btn:active {
+  transform: translateY(0);
+}
+
+.history-item.failed .retry-btn svg {
+  flex-shrink: 0;
+}
+
+/* 已重试标记 */
+.history-item.failed .retried-badge {
+  margin-top: 8px;
+  padding: 4px 12px;
+  background: rgba(100, 100, 100, 0.6);
+  color: #999;
+  border-radius: 12px;
+  font-size: 11px;
+  font-weight: 500;
+}
+
+.history-item-time {
+  position: absolute;
+  bottom: 4px;
+  left: 4px;
+  right: 4px;
+  font-size: 10px;
+  color: #fff;
+  background: rgba(0, 0, 0, 0.6);
+  padding: 2px 6px;
+  border-radius: 4px;
+  text-align: center;
+  font-weight: 500;
+  z-index: 3;
+  backdrop-filter: blur(4px);
+}
+
+@keyframes spin {
+  to { transform: rotate(360deg); }
+}
+
+.history-item-image {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
+
+.history-item-overlay {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  opacity: 0;
+  transition: opacity 0.3s;
+}
+
+.history-item:hover .history-item-overlay {
+  opacity: 1;
+}
+
+/* 操作按钮组 */
+.history-item-actions {
+  display: flex;
+  gap: 12px;
+}
+
+.history-action-btn {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 6px;
+  padding: 12px 20px;
+  background: rgba(255, 255, 255, 0.95);
+  color: #667eea;
+  border: none;
+  border-radius: 12px;
+  cursor: pointer;
+  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
+  transform: translateY(10px);
+  opacity: 0;
+}
+
+.history-item:hover .history-action-btn {
+  transform: translateY(0);
+  opacity: 1;
+}
+
+.history-item:hover .history-action-btn:nth-child(2) {
+  transition-delay: 0.05s;
+}
+
+.history-action-btn span {
+  font-size: 12px;
+  font-weight: 600;
+}
+
+.history-action-btn:hover {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+  transform: translateY(-2px) scale(1.05) !important;
+  box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
+}
+
+.history-action-btn:active {
+  transform: translateY(0) scale(0.98) !important;
+}
+
+/* 图片预览弹窗 */
+.image-preview-modal {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  z-index: 100000;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  opacity: 0;
+  transition: opacity 0.3s ease;
+}
+
+.image-preview-modal.show {
+  opacity: 1;
+}
+
+.image-preview-backdrop {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.9);
+  backdrop-filter: blur(8px);
+}
+
+.image-preview-content {
+  position: relative;
+  max-width: 90vw;
+  max-height: 90vh;
+  transform: scale(0.9);
+  transition: transform 0.3s ease;
+}
+
+.image-preview-modal.show .image-preview-content {
+  transform: scale(1);
+}
+
+.image-preview-content img {
+  max-width: 100%;
+  max-height: 85vh;
+  border-radius: 12px;
+  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
+}
+
+.image-preview-close {
+  position: absolute;
+  top: -50px;
+  right: 0;
+  width: 40px;
+  height: 40px;
+  border: none;
+  background: rgba(255, 255, 255, 0.15);
+  color: white;
+  font-size: 28px;
+  border-radius: 50%;
+  cursor: pointer;
+  transition: all 0.2s;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  line-height: 1;
+}
+
+.image-preview-close:hover {
+  background: rgba(255, 255, 255, 0.25);
+  transform: scale(1.1);
+}
+
+.image-preview-download-btn {
+  position: absolute;
+  bottom: -60px;
+  right: 50%;
+  transform: translateX(50%);
+  width: 50px;
+  height: 50px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+  border: none;
+  border-radius: 50%;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  transition: all 0.2s;
+  box-shadow: 0 4px 20px rgba(102, 126, 234, 0.5);
+}
+
+.image-preview-download-btn:hover {
+  transform: translateX(50%) scale(1.1);
+  box-shadow: 0 6px 25px rgba(102, 126, 234, 0.6);
+}
+
+.rendering-text {
+  position: absolute;
+  bottom: 12px;
+  left: 50%;
+  transform: translateX(-50%);
+  background: rgba(0, 0, 0, 0.7);
+  color: white;
+  padding: 6px 12px;
+  border-radius: 6px;
+  font-size: 12px;
+  font-weight: 500;
+}
+
+/* 退出登录按钮 */
+.logout-btn {
+  width: 100%;
+  padding: 10px 20px;
+  border: none;
+  border-radius: 8px;
+  font-size: 13px;
+  font-weight: 600;
+  cursor: pointer;
+  transition: all 0.2s;
+  background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
+  color: white;
+}
+
+.logout-btn:hover {
+  transform: translateY(-1px);
+  box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
+}
+
+/* 响应式设计 */
+@media (max-width: 1024px) {
+  .profile-layout {
+    grid-template-columns: 1fr;
+  }
+  
+  .history-section {
+    height: auto;
+    min-height: 400px;
+  }
+}
+
+/* 购买记录样式 */
+.purchase-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
+  gap: 20px;
+  padding: 12px;
+  overflow-y: auto;
+  max-height: 100%;
+}
+
+.purchase-item {
+  position: relative;
+  border-radius: 20px;
+  overflow: hidden;
+  background: white;
+  aspect-ratio: 0.82;
+  border: 1px solid rgba(102, 126, 234, 0.08);
+  transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+  display: flex;
+  flex-direction: column;
+  box-shadow: 
+    0 4px 20px rgba(102, 126, 234, 0.08),
+    0 2px 8px rgba(0, 0, 0, 0.04);
+}
+
+.purchase-item::before {
+  content: '';
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  height: 4px;
+  background: linear-gradient(90deg, #667eea 0%, #764ba2 40%, #f093fb 70%, #667eea 100%);
+  background-size: 200% 100%;
+  opacity: 0;
+  transition: opacity 0.3s ease;
+}
+
+.purchase-item::after {
+  content: '';
+  position: absolute;
+  top: 8px;
+  right: 8px;
+  width: 28px;
+  height: 28px;
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
+  border-radius: 50%;
+  opacity: 0;
+  transition: all 0.3s ease;
+  z-index: 1;
+}
+
+.purchase-item:hover {
+  transform: translateY(-8px);
+  border-color: rgba(102, 126, 234, 0.2);
+  box-shadow: 
+    0 24px 48px rgba(102, 126, 234, 0.18),
+    0 12px 24px rgba(0, 0, 0, 0.06);
+}
+
+.purchase-item:hover::before {
+  opacity: 1;
+  animation: shimmer 2s ease-in-out infinite;
+}
+
+.purchase-item:hover::after {
+  opacity: 1;
+  transform: scale(1.2);
+}
+
+@keyframes shimmer {
+  0% { background-position: 200% 0; }
+  100% { background-position: -200% 0; }
+}
+
+.purchase-item.deleted {
+  opacity: 0.5;
+  filter: grayscale(0.5);
+}
+
+.purchase-item-image {
+  width: 100%;
+  height: 62%;
+  object-fit: contain;
+  background: linear-gradient(145deg, #f8fafc 0%, #eef2f7 50%, #e8ecf2 100%);
+  padding: 16px;
+  transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+  position: relative;
+}
+
+.purchase-item:hover .purchase-item-image {
+  transform: scale(1.08);
+  filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.1));
+}
+
+.purchase-item-placeholder {
+  width: 100%;
+  height: 62%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: linear-gradient(145deg, #f8fafc 0%, #eef2f7 100%);
+  color: #9ca3af;
+  font-size: 32px;
+}
+
+.purchase-item-info {
+  padding: 14px 16px;
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  background: linear-gradient(180deg, #ffffff 0%, #fafbfc 100%);
+  border-top: 1px solid rgba(102, 126, 234, 0.06);
+  position: relative;
+}
+
+.purchase-item-overlay {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: linear-gradient(180deg, rgba(102, 126, 234, 0.9) 0%, rgba(118, 75, 162, 0.95) 100%);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  opacity: 0;
+  transition: all 0.35s ease;
+  z-index: 10;
+  backdrop-filter: blur(6px);
+}
+
+.purchase-item:hover .purchase-item-overlay {
+  opacity: 1;
+}
+
+.purchase-item-add-btn {
+  padding: 12px 24px;
+  background: white;
+  color: #667eea;
+  border: none;
+  border-radius: 30px;
+  font-size: 13px;
+  font-weight: 700;
+  cursor: pointer;
+  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);
+  transform: translateY(10px);
+  opacity: 0;
+}
+
+.purchase-item:hover .purchase-item-add-btn {
+  transform: translateY(0);
+  opacity: 1;
+}
+
+.purchase-item-add-btn:hover {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+  transform: scale(1.08) !important;
+  box-shadow: 0 8px 25px rgba(102, 126, 234, 0.5);
+}
+
+.purchase-item-add-btn:active {
+  transform: scale(1) !important;
+}
+
+.purchase-item-add-btn:disabled {
+  opacity: 0.6;
+  cursor: not-allowed;
+  transform: none;
+}
+
+.purchase-item-name {
+  font-size: 14px;
+  font-weight: 700;
+  color: #1f2937;
+  margin-bottom: 6px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  letter-spacing: -0.2px;
+  line-height: 1.3;
+}
+
+.purchase-item-category {
+  font-size: 11px;
+  color: #6b7280;
+  margin-bottom: 8px;
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.08) 0%, rgba(118, 75, 162, 0.08) 100%);
+  padding: 3px 10px;
+  border-radius: 12px;
+  width: fit-content;
+}
+
+.purchase-item-category::before {
+  content: '';
+  display: inline-block;
+  width: 5px;
+  height: 5px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  border-radius: 50%;
+  flex-shrink: 0;
+}
+
+.purchase-item-price {
+  font-size: 13px;
+  font-weight: 800;
+  color: transparent;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  -webkit-background-clip: text;
+  background-clip: text;
+  display: inline-flex;
+  align-items: center;
+  gap: 3px;
+  letter-spacing: -0.3px;
+}
+
+.purchase-item-price::before {
+  content: '💎';
+  font-size: 11px;
+}
+
+.purchase-item-deleted-badge {
+  position: absolute;
+  top: 14px;
+  right: 14px;
+  background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
+  color: white;
+  padding: 6px 12px;
+  border-radius: 20px;
+  font-size: 10px;
+  font-weight: 700;
+  letter-spacing: 0.5px;
+  box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4);
+  z-index: 5;
+  animation: pulse-badge 2s ease-in-out infinite;
+}
+
+@keyframes pulse-badge {
+  0%, 100% { transform: scale(1); }
+  50% { transform: scale(1.05); }
+}
+
+.rendering-text {
+  font-size: 14px;
+  font-weight: 600;
+  color: #667eea;
+}
+
+@media (max-width: 768px) {
+  .profile-content {
+    padding: 16px;
+  }
+  
+  .profile-layout {
+    gap: 16px;
+  }
+  
+  .profile-left {
+    gap: 16px;
+  }
+  
+  .profile-section {
+    padding: 16px;
+  }
+  
+  .avatar-preview {
+    width: 100px;
+    height: 100px;
+  }
+}
+

+ 238 - 0
client/css/recharge-view/recharge-view.css

@@ -0,0 +1,238 @@
+/* 充值界面样式 */
+
+.recharge-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background: rgba(0, 0, 0, 0.6);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000004;
+  opacity: 0;
+  transition: opacity 0.3s ease;
+  pointer-events: none;
+}
+
+.recharge-overlay.show {
+  opacity: 1;
+  pointer-events: auto;
+}
+
+.recharge-container {
+  background: white;
+  border-radius: 12px;
+  width: 90%;
+  max-width: 600px;
+  max-height: 90vh;
+  overflow-y: auto;
+  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
+  transform: scale(0.9);
+  transition: transform 0.3s ease;
+}
+
+.recharge-overlay.show .recharge-container {
+  transform: scale(1);
+}
+
+.recharge-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 20px 24px;
+  border-bottom: 1px solid #e5e5e5;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+}
+
+.recharge-title {
+  margin: 0;
+  font-size: 18px;
+  font-weight: 600;
+}
+
+.recharge-close {
+  background: none;
+  border: none;
+  color: white;
+  font-size: 28px;
+  line-height: 1;
+  cursor: pointer;
+  padding: 0;
+  width: 32px;
+  height: 32px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border-radius: 4px;
+  transition: background 0.2s;
+}
+
+.recharge-close:hover {
+  background: rgba(255, 255, 255, 0.2);
+}
+
+.recharge-content {
+  padding: 32px 24px;
+}
+
+.recharge-packages {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  gap: 16px;
+  margin-bottom: 32px;
+}
+
+.recharge-package {
+  border: 2px solid #e5e5e5;
+  border-radius: 12px;
+  padding: 20px 16px;
+  cursor: pointer;
+  transition: all 0.3s ease;
+  text-align: center;
+  background: white;
+}
+
+.recharge-package:hover {
+  border-color: #8b5cf6;
+  box-shadow: 0 4px 12px rgba(139, 92, 246, 0.2);
+}
+
+.recharge-package.selected {
+  border-color: #8b5cf6;
+  background: rgba(139, 92, 246, 0.05);
+  box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
+}
+
+.package-header {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+  align-items: center;
+}
+
+.package-points {
+  font-size: 20px;
+  font-weight: 700;
+  color: #1f2937;
+  line-height: 1.2;
+}
+
+.package-bonus {
+  font-size: 12px;
+  color: #10b981;
+  font-weight: 600;
+  background: rgba(16, 185, 129, 0.1);
+  padding: 3px 10px;
+  border-radius: 8px;
+  display: inline-block;
+  margin: 2px 0;
+}
+
+.package-price {
+  font-size: 18px;
+  font-weight: 600;
+  color: #667eea;
+  margin-top: 4px;
+}
+
+.package-total {
+  font-size: 12px;
+  color: #6b7280;
+  margin-top: 8px;
+  padding-top: 8px;
+  border-top: 1px solid #f5f5f5;
+}
+
+.recharge-info {
+  margin-top: 32px;
+  padding-top: 32px;
+  border-top: 1px solid #e5e5e5;
+}
+
+.selected-package-info {
+  text-align: center;
+  margin-bottom: 32px;
+}
+
+.info-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 8px 0;
+  margin-bottom: 8px;
+}
+
+.info-item:last-child {
+  margin-bottom: 0;
+}
+
+.info-label {
+  font-size: 14px;
+  color: #6b7280;
+}
+
+.info-value {
+  font-size: 16px;
+  font-weight: 600;
+  color: #333;
+}
+
+.qr-code-container {
+  text-align: center;
+  margin-bottom: 32px;
+}
+
+.qr-code-placeholder {
+  display: inline-block;
+  padding: 20px;
+  background: white;
+  border: 2px solid #e5e5e5;
+  border-radius: 8px;
+  margin-bottom: 16px;
+}
+
+.qr-code-hint {
+  font-size: 16px;
+  color: #666;
+  margin: 8px 0;
+}
+
+.qr-code-tip {
+  font-size: 14px;
+  color: #999;
+  margin: 4px 0;
+}
+
+.test-buy-btn {
+  margin-top: 20px;
+  padding: 12px 32px;
+  background: linear-gradient(135deg, #10b981 0%, #059669 100%);
+  color: white;
+  border: none;
+  border-radius: 8px;
+  font-size: 16px;
+  font-weight: 600;
+  cursor: pointer;
+  transition: all 0.2s;
+  box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3);
+}
+
+.test-buy-btn:hover {
+  transform: translateY(-1px);
+  box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
+}
+
+.test-buy-btn:active {
+  transform: translateY(0);
+}
+
+.test-buy-tip {
+  font-size: 12px;
+  color: #999;
+  margin-top: 8px;
+  font-style: italic;
+}
+

+ 59 - 0
client/css/seq_ani_player/card.css

@@ -297,6 +297,65 @@
   white-space: nowrap;
 }
 
+/* 操作按钮组 */
+.preview-action-btns {
+  display: flex;
+  gap: 8px;
+}
+
+.btn-action {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 40px;
+  height: 40px;
+  border: none;
+  border-radius: 10px;
+  color: white;
+  cursor: pointer;
+  backdrop-filter: blur(4px);
+  transition: all 0.2s ease;
+}
+
+.btn-action.btn-ai {
+  background: linear-gradient(135deg, #10b981 0%, #059669 100%);
+  box-shadow: 0 2px 12px rgba(16, 185, 129, 0.3);
+}
+
+.btn-action.btn-ai:hover {
+  background: linear-gradient(135deg, #34d399 0%, #10b981 100%);
+  transform: translateY(-2px);
+  box-shadow: 0 4px 16px rgba(16, 185, 129, 0.4);
+}
+
+.btn-action.btn-download {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  box-shadow: 0 2px 12px rgba(102, 126, 234, 0.3);
+}
+
+.btn-action.btn-download:hover {
+  background: linear-gradient(135deg, #7c8ef0 0%, #8a5fb8 100%);
+  transform: translateY(-2px);
+  box-shadow: 0 4px 16px rgba(102, 126, 234, 0.4);
+}
+
+.btn-action:active {
+  transform: translateY(0);
+}
+
+.btn-action svg {
+  width: 20px;
+  height: 20px;
+}
+
+.btn-action .ai-icon {
+  font-size: 14px;
+  font-weight: 800;
+  letter-spacing: -0.5px;
+  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+}
+
+/* 旧的导出按钮样式(保留兼容性) */
 .btn-export {
   display: flex;
   align-items: center;

+ 253 - 0
client/css/store/item.css

@@ -0,0 +1,253 @@
+/* 商店资源项样式 */
+
+.store-item {
+  background: #ffffff;
+  border-radius: 12px;
+  overflow: hidden;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+  transition: all 0.3s ease;
+  display: flex;
+  flex-direction: column;
+  align-items: stretch;
+  justify-content: flex-start;
+  cursor: pointer;
+  width: 100%;
+  max-width: 100%;
+  min-height: 0;
+  box-sizing: border-box;
+}
+
+.store-item:hover {
+  transform: translateY(-4px);
+  box-shadow: 0 8px 24px rgba(139, 92, 246, 0.2);
+}
+
+/* 预览图区域 */
+.item-preview {
+  position: relative;
+  width: 100%;
+  background: #f9fafb;
+  overflow: hidden;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  aspect-ratio: 1 / 1;
+  flex-shrink: 0;
+  min-height: 200px;
+}
+
+.item-preview .item-category {
+  position: absolute;
+  top: 8px;
+  left: 8px;
+  z-index: 5;
+  display: inline-block;
+  padding: 5px 12px;
+  background: linear-gradient(135deg, rgba(139, 92, 246, 0.15) 0%, rgba(124, 58, 237, 0.15) 100%);
+  color: #8b5cf6;
+  border-radius: 12px;
+  font-size: 11px;
+  font-weight: 600;
+  letter-spacing: 0.3px;
+  border: 1px solid rgba(139, 92, 246, 0.2);
+  box-shadow: 0 1px 3px rgba(139, 92, 246, 0.1);
+  transition: all 0.2s ease;
+  backdrop-filter: blur(4px);
+}
+
+.item-preview-image {
+  width: 100%;
+  height: 100%;
+  object-fit: contain;
+  object-position: center;
+  image-rendering: pixelated;
+  background-color: #f9fafb;
+  box-sizing: border-box;
+  min-width: 0;
+  min-height: 0;
+  display: block;
+}
+
+/* FPS 控制条 */
+.item-fps-control {
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  background: rgba(0, 0, 0, 0.8);
+  backdrop-filter: blur(4px);
+  padding: 8px 12px;
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  opacity: 0;
+  transform: translateY(100%);
+  transition: all 0.3s ease;
+  z-index: 10;
+}
+
+.store-item:hover .item-fps-control {
+  opacity: 1;
+  transform: translateY(0);
+}
+
+.item-fps-label {
+  font-size: 12px;
+  font-weight: 500;
+  color: #ffffff;
+  white-space: nowrap;
+  flex-shrink: 0;
+}
+
+.item-fps-slider {
+  flex: 1;
+  height: 4px;
+  border-radius: 2px;
+  background: rgba(255, 255, 255, 0.3);
+  outline: none;
+  -webkit-appearance: none;
+  appearance: none;
+}
+
+.item-fps-slider::-webkit-slider-thumb {
+  -webkit-appearance: none;
+  appearance: none;
+  width: 14px;
+  height: 14px;
+  border-radius: 50%;
+  background: #8b5cf6;
+  cursor: pointer;
+  transition: all 0.2s ease;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
+}
+
+.item-fps-slider::-webkit-slider-thumb:hover {
+  background: #7c3aed;
+  transform: scale(1.1);
+}
+
+.item-fps-slider::-moz-range-thumb {
+  width: 14px;
+  height: 14px;
+  border-radius: 50%;
+  background: #8b5cf6;
+  cursor: pointer;
+  border: none;
+  transition: all 0.2s ease;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
+}
+
+.item-fps-slider::-moz-range-thumb:hover {
+  background: #7c3aed;
+  transform: scale(1.1);
+}
+
+.item-fps-display {
+  font-size: 12px;
+  font-weight: 600;
+  color: #ffffff;
+  background: rgba(139, 92, 246, 0.3);
+  padding: 4px 8px;
+  border-radius: 4px;
+  min-width: 50px;
+  text-align: center;
+  font-family: 'Courier New', monospace;
+  flex-shrink: 0;
+}
+
+/* 信息区域 */
+.item-info {
+  padding: 10px 12px;
+  width: 100%;
+  flex: 0 0 auto;
+  display: flex;
+  flex-direction: column;
+  box-sizing: border-box;
+}
+
+.item-meta {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  flex-wrap: wrap;
+}
+
+.item-name {
+  font-size: 16px;
+  font-weight: 600;
+  color: #1f2937;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  flex: 1;
+  min-width: 0;
+}
+
+.item-frames {
+  display: flex;
+  align-items: center;
+}
+
+/* 操作区域 */
+.item-actions {
+  padding: 10px 12px;
+  width: 100%;
+  border-top: 1px solid rgba(0, 0, 0, 0.05);
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+  flex: 0 0 auto;
+  box-sizing: border-box;
+}
+
+.item-price {
+  font-size: 18px;
+  font-weight: 700;
+  color: #8b5cf6;
+  text-align: center;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.item-buy-button {
+  width: 100%;
+  padding: 8px;
+  background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
+  color: white;
+  border: none;
+  border-radius: 8px;
+  font-size: 14px;
+  font-weight: 600;
+  cursor: pointer;
+  transition: all 0.3s ease;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.item-buy-button:hover {
+  background: linear-gradient(135deg, #7c3aed 0%, #6d28d9 100%);
+  transform: translateY(-1px);
+  box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
+}
+
+.item-buy-button:active {
+  transform: translateY(0);
+}
+
+.item-buy-button.item-button-added {
+  background: linear-gradient(135deg, #10b981 0%, #059669 100%);
+  cursor: pointer;
+}
+
+.item-buy-button.item-button-added:hover {
+  background: linear-gradient(135deg, #059669 0%, #047857 100%);
+  transform: translateY(-1px);
+  box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
+}
+
+.item-buy-button.item-button-added:active {
+  transform: translateY(0);
+}
+

+ 388 - 0
client/css/store/store.css

@@ -0,0 +1,388 @@
+/* 商店页面样式 */
+
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+}
+
+body {
+  margin: 0;
+  padding: 0;
+  height: 100vh;
+  overflow-x: hidden;
+  overflow-y: auto;
+  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial,
+    "Noto Sans", sans-serif;
+  background: linear-gradient(135deg, #fdfcfb 0%, #f7f7f8 50%, #fdfcfb 100%);
+}
+
+.store-root {
+  width: 100%;
+  min-height: 100vh;
+  display: flex;
+  flex-direction: column;
+  overflow-x: hidden;
+  overflow-y: auto;
+}
+
+.store-container {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  padding: 16px;
+  overflow-x: hidden;
+  min-width: 0;
+  width: 100%;
+  box-sizing: border-box;
+}
+
+/* 搜索栏 */
+.store-header {
+  margin-bottom: 16px;
+}
+
+.search-bar {
+  display: flex;
+  gap: 10px;
+  max-width: 600px;
+}
+
+.search-input {
+  flex: 1;
+  padding: 12px 16px;
+  border: 2px solid rgba(139, 92, 246, 0.2);
+  border-radius: 8px;
+  font-size: 14px;
+  outline: none;
+  transition: all 0.3s ease;
+  background: #ffffff;
+}
+
+.search-input:focus {
+  border-color: #8b5cf6;
+  box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.1);
+}
+
+.search-button {
+  padding: 12px 20px;
+  background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
+  color: white;
+  border: none;
+  border-radius: 8px;
+  cursor: pointer;
+  transition: all 0.3s ease;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.search-button:hover {
+  background: linear-gradient(135deg, #7c3aed 0%, #6d28d9 100%);
+  transform: translateY(-1px);
+  box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
+}
+
+.search-button:active {
+  transform: translateY(0);
+}
+
+/* 分类栏 */
+.category-bar {
+  display: flex;
+  gap: 10px;
+  margin-bottom: 16px;
+  flex-wrap: wrap;
+}
+
+.category-item {
+  padding: 10px 20px;
+  background: #ffffff;
+  border: 2px solid rgba(139, 92, 246, 0.2);
+  border-radius: 8px;
+  font-size: 14px;
+  font-weight: 500;
+  color: #6b7280;
+  cursor: pointer;
+  transition: all 0.3s ease;
+}
+
+.category-item:hover {
+  border-color: #8b5cf6;
+  color: #8b5cf6;
+  transform: translateY(-1px);
+}
+
+.category-item.active {
+  background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
+  border-color: #8b5cf6;
+  color: white;
+  box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
+}
+
+/* 资源网格 - 瀑布流布局 */
+.resources-grid {
+  flex: 1;
+  position: relative;
+  overflow-x: hidden;
+  overflow-y: auto;
+  padding: 0;
+  min-width: 0;
+  width: 100%;
+  box-sizing: border-box;
+}
+
+.resources-grid-masonry {
+  display: flex;
+  flex-direction: row;
+  align-items: flex-start;
+  gap: 16px;
+}
+
+.masonry-column {
+  flex: 1;
+  min-width: 200px;
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+.resources-grid::-webkit-scrollbar {
+  width: 8px;
+}
+
+.resources-grid::-webkit-scrollbar-track {
+  background: rgba(0, 0, 0, 0.05);
+  border-radius: 4px;
+}
+
+.resources-grid::-webkit-scrollbar-thumb {
+  background: rgba(139, 92, 246, 0.3);
+  border-radius: 4px;
+}
+
+.resources-grid::-webkit-scrollbar-thumb:hover {
+  background: rgba(139, 92, 246, 0.5);
+}
+
+/* 空状态 */
+.empty-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 60px 20px;
+  color: #9ca3af;
+}
+
+.empty-icon {
+  font-size: 64px;
+  margin-bottom: 16px;
+  opacity: 0.5;
+}
+
+.empty-text {
+  font-size: 18px;
+  font-weight: 500;
+}
+
+/* 加载状态 */
+.loading-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 60px 20px;
+  color: #9ca3af;
+}
+
+.loading-spinner {
+  width: 40px;
+  height: 40px;
+  border: 4px solid rgba(139, 92, 246, 0.1);
+  border-top-color: #8b5cf6;
+  border-radius: 50%;
+  animation: spin 1s linear infinite;
+  margin-bottom: 16px;
+}
+
+@keyframes spin {
+  to {
+    transform: rotate(360deg);
+  }
+}
+
+.loading-text {
+  font-size: 16px;
+  font-weight: 500;
+}
+
+/* 预览弹窗 */
+.preview-modal {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background: rgba(0, 0, 0, 0.7);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 10000;
+  backdrop-filter: blur(4px);
+}
+
+.preview-modal-content {
+  background: #ffffff;
+  border-radius: 16px;
+  width: 90%;
+  max-width: 800px;
+  max-height: 90vh;
+  display: flex;
+  flex-direction: column;
+  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+  overflow: hidden;
+}
+
+.preview-modal-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 20px 24px;
+  border-bottom: 1px solid rgba(0, 0, 0, 0.1);
+}
+
+.preview-modal-title {
+  font-size: 20px;
+  font-weight: 600;
+  color: #1f2937;
+}
+
+.preview-modal-close {
+  background: none;
+  border: none;
+  cursor: pointer;
+  padding: 8px;
+  color: #6b7280;
+  transition: all 0.2s ease;
+  border-radius: 4px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.preview-modal-close:hover {
+  background: rgba(0, 0, 0, 0.05);
+  color: #1f2937;
+}
+
+.preview-modal-body {
+  flex: 1;
+  padding: 24px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  overflow: auto;
+}
+
+.preview-animation-container {
+  width: 100%;
+  max-width: 600px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: #f9fafb;
+  border-radius: 12px;
+  padding: 40px;
+  min-height: 400px;
+}
+
+.preview-animation-image {
+  max-width: 100%;
+  max-height: 500px;
+  object-fit: contain;
+  image-rendering: pixelated;
+}
+
+/* 预览弹窗底部控制栏 */
+.preview-modal-footer {
+  padding: 16px 24px;
+  border-top: 1px solid rgba(0, 0, 0, 0.1);
+  background: #f9fafb;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.fps-control {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  width: 100%;
+  max-width: 400px;
+}
+
+.fps-label {
+  font-size: 14px;
+  font-weight: 500;
+  color: #6b7280;
+  white-space: nowrap;
+}
+
+.fps-slider {
+  flex: 1;
+  height: 6px;
+  border-radius: 3px;
+  background: #e5e7eb;
+  outline: none;
+  -webkit-appearance: none;
+  appearance: none;
+}
+
+.fps-slider::-webkit-slider-thumb {
+  -webkit-appearance: none;
+  appearance: none;
+  width: 18px;
+  height: 18px;
+  border-radius: 50%;
+  background: #8b5cf6;
+  cursor: pointer;
+  transition: all 0.2s ease;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+}
+
+.fps-slider::-webkit-slider-thumb:hover {
+  background: #7c3aed;
+  transform: scale(1.1);
+}
+
+.fps-slider::-moz-range-thumb {
+  width: 18px;
+  height: 18px;
+  border-radius: 50%;
+  background: #8b5cf6;
+  cursor: pointer;
+  border: none;
+  transition: all 0.2s ease;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+}
+
+.fps-slider::-moz-range-thumb:hover {
+  background: #7c3aed;
+  transform: scale(1.1);
+}
+
+.fps-display {
+  font-size: 14px;
+  font-weight: 600;
+  color: #1f2937;
+  background: #ffffff;
+  padding: 6px 12px;
+  border-radius: 6px;
+  min-width: 60px;
+  text-align: center;
+  border: 1px solid #e5e7eb;
+  font-family: 'Courier New', monospace;
+  image-rendering: pixelated;
+}
+

+ 4 - 1
client/index.html

@@ -43,7 +43,10 @@
   </div>
 
   <!-- 导出动画弹出框 iframe -->
-  <iframe id="exportViewFrame" src="page/export/export-view.html" frameborder="0" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 999999; border: none;"></iframe>
+  <iframe id="exportViewFrame" src="page/export/export-view.html?v=4" frameborder="0" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 999999; border: none; pointer-events: none; visibility: hidden;"></iframe>
+
+  <!-- AI生图弹出框 iframe -->
+  <iframe id="aiGenerateViewFrame" src="page/ai-generate/ai-generate-view.html" frameborder="0" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 999999; border: none;"></iframe>
 
   <!-- 登录/注册弹出框 iframe -->
   <iframe id="loginViewFrame" src="page/login/login.html" frameborder="0" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 1000001; border: none; background: transparent;"></iframe>

+ 58 - 1
client/js/Index.js

@@ -289,6 +289,37 @@ window.GlobalAlert = (function() {
       if (loginFrame) {
         loginFrame.style.display = 'none';
       }
+    } else if (data && data.type === "open-ai-generate-view") {
+      // 处理打开AI生图界面
+      console.log('[Index] 收到open-ai-generate-view消息:', data);
+      const aiGenerateFrame = document.getElementById('aiGenerateViewFrame');
+      if (aiGenerateFrame) {
+        aiGenerateFrame.style.display = 'block';
+        aiGenerateFrame.style.pointerEvents = 'auto';
+        aiGenerateFrame.style.visibility = 'visible';
+        
+        const sendAIData = () => {
+          if (aiGenerateFrame.contentWindow) {
+            aiGenerateFrame.contentWindow.postMessage({
+              type: 'show-ai-generate',
+              folderName: data.folderName,
+              spritesheetData: data.spritesheetData,
+              spritesheetLayout: data.spritesheetLayout
+            }, '*');
+          } else {
+            setTimeout(sendAIData, 100);
+          }
+        };
+        
+        sendAIData();
+      }
+    } else if (data && data.type === "close-ai-generate-view") {
+      const aiGenerateFrame = document.getElementById('aiGenerateViewFrame');
+      if (aiGenerateFrame) {
+        aiGenerateFrame.style.display = 'none';
+        aiGenerateFrame.style.pointerEvents = 'none';
+        aiGenerateFrame.style.visibility = 'hidden';
+      }
     } else if (data && data.type === "open-pay-view") {
       // 处理打开支付界面
       const payFrame = document.getElementById('payViewFrame');
@@ -380,7 +411,20 @@ window.GlobalAlert = (function() {
         rechargeFrame.style.visibility = 'hidden';
       }
     } else if (data && data.type === "recharge-success") {
-      // 充值成功,刷新商店页面(如果需要)
+      // 充值成功,关闭充值界面
+      const rechargeFrame = document.getElementById('rechargeViewFrame');
+      if (rechargeFrame) {
+        rechargeFrame.style.display = 'none';
+        rechargeFrame.style.pointerEvents = 'none';
+        rechargeFrame.style.visibility = 'hidden';
+      }
+      
+      // 刷新点数显示
+      if (window.postMessage) {
+        window.postMessage({ type: 'refresh-points' }, '*');
+      }
+      
+      // 显示成功提示
       if (window.HintView) {
         window.HintView.success(`充值成功!获得 ${data.points} Ani币`, 3000);
       }
@@ -440,6 +484,19 @@ window.GlobalAlert = (function() {
           user: data.user
         }, '*');
       }
+    } else if (data && data.type === "refresh-points") {
+      // 刷新用户点数显示
+      console.log('[Index] 收到刷新点数请求');
+      // 转发给导航栏更新点数
+      const navigationFrame = getNavigationFrame();
+      if (navigationFrame && navigationFrame.contentWindow) {
+        navigationFrame.contentWindow.postMessage({ type: 'refresh-points' }, '*');
+      }
+      // 也转发给个人中心页面(如果打开的话)
+      const pageFrame = getPageFrame();
+      if (pageFrame && pageFrame.contentWindow) {
+        pageFrame.contentWindow.postMessage({ type: 'refresh-points' }, '*');
+      }
     } else if (data && data.type === "logout") {
       // 清除 localStorage 中的登录信息
       try {

+ 774 - 0
client/js/ai-generate/ai-generate-view.js

@@ -0,0 +1,774 @@
+/**
+ * AI生图弹出框
+ */
+class AIGenerateView {
+    constructor() {
+        this.overlay = null;
+        this.modal = null;
+        this.previewImage = null;
+        this.previewPlaceholder = null;
+        this.referenceBox = null;
+        this.referenceInput = null;
+        this.referenceUploadArea = null;
+        this.referenceImage = null;
+        this.referenceImageWrapper = null;
+        this.referenceRemoveBtn = null;
+        this.generateBtn = null;
+        this.additionalPromptInput = null;
+        this.cancelBtn = null;
+        this.imageData = null;
+        this.referenceImageData = null;
+        this.originalSpritesheetData = null;
+        this.spritesheetLayout = null;
+        this.folderName = null;
+        
+        // 当前会话的任务ID列表
+        this.sessionTaskIds = [];
+        this.queuePollingTimer = null;
+        
+        this.init();
+    }
+
+    init() {
+        this.overlay = document.getElementById('aiGenerateOverlay');
+        this.modal = document.getElementById('aiGenerateModal');
+        this.previewImage = document.getElementById('previewImage');
+        this.previewPlaceholder = document.getElementById('previewPlaceholder');
+        this.referenceBox = document.getElementById('referenceBox');
+        this.referenceInput = document.getElementById('referenceInput');
+        this.referenceUploadArea = document.getElementById('referenceUploadArea');
+        this.referenceImage = document.getElementById('referenceImage');
+        this.referenceImageWrapper = document.getElementById('referenceImageWrapper');
+        this.referenceRemoveBtn = document.getElementById('referenceRemoveBtn');
+        this.generateBtn = document.getElementById('generateBtn');
+        this.aiGeneratePriceEl = document.getElementById('aiGeneratePrice');
+        this.additionalPromptInput = document.getElementById('additionalPromptInput');
+        this.cancelBtn = document.getElementById('aiGenerateCancelBtn');
+        
+        // 加载AI生图价格
+        this.loadAIGeneratePrice();
+        
+        this.bindEvents();
+        this.reset();
+    }
+
+    bindEvents() {
+        // 关闭按钮
+        this.cancelBtn?.addEventListener('click', () => {
+            this.close();
+        });
+
+        // AI生图按钮
+        this.generateBtn?.addEventListener('click', () => {
+            this.generateAI();
+        });
+
+        // 删除参考图按钮
+        this.referenceRemoveBtn?.addEventListener('click', (e) => {
+            e.stopPropagation();
+            this.removeReferenceImage();
+        });
+
+        // 点击遮罩层关闭
+        this.overlay?.addEventListener('click', (e) => {
+            if (e.target === this.overlay) {
+                this.close();
+            }
+        });
+
+        // ESC键关闭
+        document.addEventListener('keydown', (e) => {
+            if (e.key === 'Escape' && this.overlay) {
+                this.close();
+            }
+        });
+
+        // 参考图上传区域点击
+        this.referenceBox?.addEventListener('click', () => {
+            this.referenceInput?.click();
+        });
+
+        // 参考图选择
+        this.referenceInput?.addEventListener('change', (e) => {
+            const file = e.target.files[0];
+            if (file) {
+                this.loadReferenceImage(file);
+            }
+        });
+
+        // 拖拽上传参考图
+        this.referenceBox?.addEventListener('dragover', (e) => {
+            e.preventDefault();
+            e.stopPropagation();
+            if (this.referenceBox) {
+                this.referenceBox.style.borderColor = '#667eea';
+            }
+        });
+
+        this.referenceBox?.addEventListener('dragleave', (e) => {
+            e.preventDefault();
+            e.stopPropagation();
+            if (this.referenceBox) {
+                this.referenceBox.style.borderColor = '#e5e7eb';
+            }
+        });
+
+        this.referenceBox?.addEventListener('drop', (e) => {
+            e.preventDefault();
+            e.stopPropagation();
+            if (this.referenceBox) {
+                this.referenceBox.style.borderColor = '#e5e7eb';
+            }
+            
+            const file = e.dataTransfer.files[0];
+            if (file && file.type.startsWith('image/')) {
+                this.loadReferenceImage(file);
+            }
+        });
+
+        // 监听来自父窗口的消息
+        window.addEventListener('message', (event) => {
+            if (event.data && event.data.type === 'show-ai-generate') {
+                this.reset();
+                this.folderName = event.data.folderName;
+                this.originalSpritesheetData = event.data.spritesheetData;
+                this.spritesheetLayout = event.data.spritesheetLayout;
+                this.showPreview(event.data.spritesheetData);
+            }
+        });
+    }
+
+    /**
+     * 显示预览图
+     */
+    showPreview(imageUrl) {
+        if (!imageUrl) return;
+
+        this.imageData = imageUrl;
+        
+        const img = new Image();
+        img.onload = () => {
+            if (this.previewImage) {
+                this.previewImage.src = imageUrl;
+                this.previewImage.classList.add('show');
+            }
+            if (this.previewPlaceholder) {
+                this.previewPlaceholder.classList.add('hide');
+            }
+        };
+        
+        img.onerror = () => {
+            if (this.previewPlaceholder) {
+                const spinner = this.previewPlaceholder.querySelector('.loading-spinner');
+                const loadingText = this.previewPlaceholder.querySelector('.loading-text');
+                if (spinner) spinner.style.display = 'none';
+                if (loadingText) {
+                    loadingText.textContent = '图片加载失败';
+                    loadingText.style.color = '#ef4444';
+                }
+                this.previewPlaceholder.classList.remove('hide');
+            }
+        };
+        
+        img.src = imageUrl;
+    }
+
+    /**
+     * 加载参考图
+     */
+    loadReferenceImage(file) {
+        const reader = new FileReader();
+        reader.onload = (e) => {
+            this.referenceImageData = e.target.result;
+            if (this.referenceImage) {
+                this.referenceImage.src = e.target.result;
+            }
+            if (this.referenceImageWrapper) {
+                this.referenceImageWrapper.style.display = 'flex';
+            }
+            if (this.referenceUploadArea) {
+                this.referenceUploadArea.classList.add('hide');
+            }
+            
+            // 显示提示词配置区域
+            const promptConfigSection = document.getElementById('promptConfigSection');
+            if (promptConfigSection) {
+                promptConfigSection.style.display = 'flex';
+            }
+        };
+        reader.readAsDataURL(file);
+    }
+
+    /**
+     * 删除参考图
+     */
+    removeReferenceImage() {
+        this.referenceImageData = null;
+
+        if (this.referenceImageWrapper) {
+            this.referenceImageWrapper.style.display = 'none';
+        }
+        if (this.referenceImage) {
+            this.referenceImage.src = '';
+        }
+        if (this.referenceUploadArea) {
+            this.referenceUploadArea.classList.remove('hide');
+        }
+        if (this.referenceInput) {
+            this.referenceInput.value = '';
+        }
+
+        // 隐藏提示词配置区域
+        const promptConfigSection = document.getElementById('promptConfigSection');
+        if (promptConfigSection) {
+            promptConfigSection.style.display = 'none';
+        }
+    }
+
+    /**
+     * AI生图
+     */
+    async generateAI() {
+        if (!this.originalSpritesheetData || !this.referenceImageData) {
+            this.showAlert('请先上传参考图');
+            return;
+        }
+
+        const username = this.getCurrentUsername();
+        if (!username) {
+            this.showAlert('请先登录');
+            return;
+        }
+
+        // 检查 Ani币余额
+        try {
+            const pricingResponse = await fetch('/api/product-pricing');
+            if (!pricingResponse.ok) {
+                throw new Error('获取价格失败');
+            }
+            const pricingResult = await pricingResponse.json();
+            if (!pricingResult.success || !pricingResult.products) {
+                throw new Error('获取价格失败');
+            }
+            
+            const aiGenerateProduct = pricingResult.products.find(p => p.id === 'ai-generate');
+            const price = aiGenerateProduct ? (aiGenerateProduct.price || 0) : 0;
+            
+            if (price > 0) {
+                const pointsResponse = await fetch(`/api/user/points?username=${encodeURIComponent(username)}`);
+                if (!pointsResponse.ok) {
+                    throw new Error('获取点数失败');
+                }
+                const pointsResult = await pointsResponse.json();
+                if (!pointsResult.success) {
+                    throw new Error('获取点数失败');
+                }
+                
+                const userPoints = pointsResult.points || 0;
+                
+                if (userPoints < price) {
+                    if (window.parent && window.parent.postMessage) {
+                        window.parent.postMessage({
+                            type: 'open-recharge-view',
+                            needPoints: price,
+                            currentPoints: userPoints
+                        }, '*');
+                    }
+                    this.showAlert(`Ani币不足,需要 ${price} Ani币,您当前有 ${userPoints.toFixed(2)} Ani币。请先充值!`);
+                    return;
+                }
+            }
+        } catch (error) {
+            console.error('[AIGenerateView] 余额检查失败:', error);
+            this.showAlert('检查余额失败:' + error.message);
+            return;
+        }
+
+        // 禁用生图按钮
+        if (this.generateBtn) {
+            this.generateBtn.disabled = true;
+        }
+
+        try {
+            // 准备图片数据
+            const image1Base64 = this.originalSpritesheetData.replace(/^data:image\/\w+;base64,/, '');
+            const image2Base64 = this.referenceImageData.replace(/^data:image\/\w+;base64,/, '');
+            const image1Width = this.spritesheetLayout?.sheetWidth || 0;
+            const image1Height = this.spritesheetLayout?.sheetHeight || 0;
+            const additionalPrompt = this.additionalPromptInput?.value || '';
+
+            // 扣除Ani币
+            const pricingResponse = await fetch('/api/product-pricing');
+            const pricingResult = await pricingResponse.json();
+            const aiGenerateProduct = pricingResult.products?.find(p => p.id === 'ai-generate');
+            const price = aiGenerateProduct ? (aiGenerateProduct.price || 0) : 0;
+            
+            if (price > 0) {
+                const deductResponse = await fetch('/api/user/deduct-points', {
+                    method: 'POST',
+                    headers: { 'Content-Type': 'application/json' },
+                    body: JSON.stringify({ username, points: price })
+                });
+                
+                if (!deductResponse.ok) {
+                    const deductResult = await deductResponse.json();
+                    throw new Error(deductResult.message || '扣除Ani币失败');
+                }
+                const deductResult = await deductResponse.json();
+                if (!deductResult.success) {
+                    throw new Error(deductResult.message || '扣除Ani币失败');
+                }
+                
+                // 通知父窗口刷新点数
+                if (window.parent && window.parent.postMessage) {
+                    window.parent.postMessage({ type: 'refresh-points' }, '*');
+                }
+            }
+
+            // 调用队列API
+            const response = await fetch('/api/ai/generate', {
+                method: 'POST',
+                headers: { 'Content-Type': 'application/json' },
+                body: JSON.stringify({
+                    username: username,
+                    image1: image1Base64,
+                    image2: image2Base64,
+                    image1Width: image1Width,
+                    image1Height: image1Height,
+                    additionalPrompt: additionalPrompt
+                })
+            });
+
+            if (!response.ok) {
+                const errorData = await response.json().catch(() => ({}));
+                throw new Error(errorData.message || '请求生图失败');
+            }
+
+            const result = await response.json();
+            
+            if (result.success && result.taskId) {
+                this.sessionTaskIds.push(result.taskId);
+                this.showFlyAwayAnimation();
+                this.loadAIQueue(username);
+            } else {
+                throw new Error(result.message || '请求失败');
+            }
+        } catch (error) {
+            console.error('[AIGenerateView] 请求生图失败:', error);
+            this.showAlert(error.message || '请求失败,请稍后重试');
+        } finally {
+            if (this.generateBtn) {
+                this.generateBtn.disabled = false;
+            }
+        }
+    }
+
+    /**
+     * 加载AI生图队列
+     */
+    async loadAIQueue(username) {
+        if (!this.sessionTaskIds || this.sessionTaskIds.length === 0) {
+            const queueSection = document.getElementById('aiQueueSection');
+            if (queueSection) {
+                queueSection.style.display = 'none';
+            }
+            return;
+        }
+        
+        if (!username) {
+            username = this.getCurrentUsername();
+        }
+        if (!username) return;
+
+        try {
+            const response = await fetch(`/api/ai/history?username=${encodeURIComponent(username)}`);
+            if (!response.ok) return;
+            
+            const result = await response.json();
+            if (!result.success || !result.history) return;
+            
+            const sessionTasks = result.history.filter(t => this.sessionTaskIds.includes(t.id));
+            this.renderAIQueue(sessionTasks);
+            
+            // 如果有进行中的任务,继续轮询
+            const hasPending = sessionTasks.some(t => t.status === 'queued' || t.status === 'rendering');
+            if (hasPending) {
+                if (this.queuePollingTimer) {
+                    clearTimeout(this.queuePollingTimer);
+                }
+                this.queuePollingTimer = setTimeout(() => {
+                    this.loadAIQueue(username);
+                }, 3000);
+            }
+        } catch (error) {
+            console.error('[AIGenerateView] 加载AI队列失败:', error);
+        }
+    }
+
+    /**
+     * 渲染AI队列
+     */
+    renderAIQueue(tasks) {
+        const queueSection = document.getElementById('aiQueueSection');
+        const queueList = document.getElementById('aiQueueList');
+        
+        if (!queueSection || !queueList) return;
+        
+        if (tasks.length === 0) {
+            queueSection.style.display = 'none';
+            return;
+        }
+        
+        queueSection.style.display = 'block';
+        queueList.innerHTML = tasks.map(task => this.createQueueItemHTML(task)).join('');
+        
+        // 绑定点击预览事件
+        queueList.querySelectorAll('.ai-queue-item').forEach(item => {
+            const taskId = item.dataset.taskId;
+            const task = tasks.find(t => t.id === taskId);
+            
+            if (task && task.status === 'completed' && task.imageUrl) {
+                item.style.cursor = 'pointer';
+                item.addEventListener('click', () => {
+                    this.showImagePreviewModal(task.imageUrl, task.id);
+                });
+            }
+        });
+    }
+
+    /**
+     * 创建队列项HTML
+     */
+    createQueueItemHTML(task) {
+        const previewUrl = task.previewUrl || '';
+        
+        if (task.status === 'queued' || task.status === 'rendering') {
+            const statusText = task.status === 'queued' ? '等待中' : '生成中';
+            return `
+                <div class="ai-queue-item ${task.status}" data-task-id="${task.id}">
+                    ${previewUrl ? `<img class="ai-queue-item-preview" src="${previewUrl}" alt="预览">` : ''}
+                    <div class="ai-queue-item-overlay">
+                        <div class="ai-queue-item-spinner"></div>
+                        <div class="ai-queue-item-status">${statusText}</div>
+                    </div>
+                </div>
+            `;
+        } else if (task.status === 'completed' && task.imageUrl) {
+            return `
+                <div class="ai-queue-item completed" data-task-id="${task.id}" title="点击放大预览">
+                    <img class="ai-queue-item-preview" src="${task.imageUrl}" alt="完成">
+                    <div class="ai-queue-item-overlay">
+                        <div class="ai-queue-item-icon">🔍</div>
+                    </div>
+                </div>
+            `;
+        } else if (task.status === 'failed') {
+            return `
+                <div class="ai-queue-item failed" data-task-id="${task.id}">
+                    ${previewUrl ? `<img class="ai-queue-item-preview" src="${previewUrl}" alt="预览">` : ''}
+                    <div class="ai-queue-item-overlay">
+                        <div class="ai-queue-item-icon">❌</div>
+                        <div class="ai-queue-item-status">失败</div>
+                    </div>
+                </div>
+            `;
+        }
+        return '';
+    }
+
+    /**
+     * 显示图片预览弹窗
+     */
+    showImagePreviewModal(imageUrl, taskId) {
+        // 在父窗口中创建预览弹窗,确保在最上层
+        let targetDocument = document;
+        try {
+            if (window.parent && window.parent !== window && window.parent.document) {
+                targetDocument = window.parent.document;
+            }
+        } catch (e) {}
+        
+        const modal = targetDocument.createElement('div');
+        modal.className = 'image-preview-modal';
+        modal.style.cssText = 'position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 9999999; display: flex; align-items: center; justify-content: center; opacity: 0; transition: opacity 0.3s ease;';
+        modal.innerHTML = `
+            <div class="image-preview-modal-backdrop" style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.85); backdrop-filter: blur(8px);"></div>
+            <div class="image-preview-modal-content" style="position: relative; max-width: 90vw; max-height: 90vh; transform: scale(0.9); transition: transform 0.3s ease;">
+                <img src="${imageUrl}" alt="AI生成图预览" style="max-width: 100%; max-height: 85vh; border-radius: 12px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);">
+                <button class="image-preview-modal-close" style="position: absolute; top: -40px; right: 0; width: 36px; height: 36px; border: none; background: rgba(255, 255, 255, 0.2); color: white; font-size: 24px; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; line-height: 1; transition: all 0.2s;">×</button>
+            </div>
+        `;
+        
+        targetDocument.body.appendChild(modal);
+        
+        // 显示动画
+        requestAnimationFrame(() => {
+            modal.style.opacity = '1';
+            modal.querySelector('.image-preview-modal-content').style.transform = 'scale(1)';
+        });
+        
+        const closeModal = () => {
+            modal.style.opacity = '0';
+            modal.querySelector('.image-preview-modal-content').style.transform = 'scale(0.9)';
+            setTimeout(() => {
+                if (modal.parentNode) {
+                    modal.parentNode.removeChild(modal);
+                }
+            }, 300);
+        };
+        
+        modal.querySelector('.image-preview-modal-backdrop').onclick = closeModal;
+        modal.querySelector('.image-preview-modal-close').onclick = closeModal;
+        
+        // 鼠标悬停关闭按钮效果
+        const closeBtn = modal.querySelector('.image-preview-modal-close');
+        closeBtn.onmouseenter = () => {
+            closeBtn.style.background = 'rgba(255, 255, 255, 0.3)';
+            closeBtn.style.transform = 'scale(1.1)';
+        };
+        closeBtn.onmouseleave = () => {
+            closeBtn.style.background = 'rgba(255, 255, 255, 0.2)';
+            closeBtn.style.transform = 'scale(1)';
+        };
+        
+        // ESC关闭(在父窗口监听)
+        const targetWindow = targetDocument.defaultView || window;
+        const escHandler = (e) => {
+            if (e.key === 'Escape') {
+                closeModal();
+                targetWindow.removeEventListener('keydown', escHandler);
+            }
+        };
+        targetWindow.addEventListener('keydown', escHandler);
+    }
+
+    /**
+     * 显示飞走动画
+     */
+    showFlyAwayAnimation() {
+        let targetDocument = document;
+        let targetWindow = window;
+        
+        try {
+            if (window.parent && window.parent !== window && window.parent.document) {
+                targetDocument = window.parent.document;
+                targetWindow = window.parent;
+            }
+        } catch (e) {}
+        
+        const flyElement = targetDocument.createElement('div');
+        flyElement.innerHTML = `
+            <div style="display: flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 600; white-space: nowrap;">
+                <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                    <path d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z"/>
+                </svg>
+                <span>AI生图任务</span>
+            </div>
+        `;
+        
+        let startLeft = targetWindow.innerWidth / 2;
+        let startTop = targetWindow.innerHeight / 2;
+        
+        if (this.generateBtn) {
+            const btnRect = this.generateBtn.getBoundingClientRect();
+            try {
+                if (window.parent && window.parent !== window) {
+                    const iframe = window.parent.document.querySelector('iframe[src*="ai-generate"]');
+                    if (iframe) {
+                        const iframeRect = iframe.getBoundingClientRect();
+                        startLeft = iframeRect.left + btnRect.left;
+                        startTop = iframeRect.top + btnRect.top;
+                    }
+                }
+            } catch (e) {
+                startLeft = btnRect.left;
+                startTop = btnRect.top;
+            }
+        }
+        
+        flyElement.style.cssText = `
+            position: fixed;
+            left: ${startLeft}px;
+            top: ${startTop}px;
+            z-index: 999999;
+            pointer-events: none;
+            transition: all 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94);
+            opacity: 1;
+            transform: scale(1);
+            background: linear-gradient(135deg, #10b981 0%, #059669 100%);
+            color: white;
+            padding: 12px 20px;
+            border-radius: 12px;
+            box-shadow: 0 8px 32px rgba(16, 185, 129, 0.4);
+            display: flex;
+            align-items: center;
+            gap: 8px;
+        `;
+        
+        targetDocument.body.appendChild(flyElement);
+        
+        requestAnimationFrame(() => {
+            flyElement.style.left = `${targetWindow.innerWidth - 100}px`;
+            flyElement.style.top = '50px';
+            flyElement.style.opacity = '0';
+            flyElement.style.transform = 'scale(0.3)';
+        });
+        
+        setTimeout(() => {
+            if (window.parent && window.parent.HintView) {
+                window.parent.HintView.success('已添加到「我的」-「AI生图历史」,请稍后查看', 3000);
+            }
+        }, 300);
+        
+        setTimeout(() => {
+            if (flyElement.parentNode) {
+                flyElement.parentNode.removeChild(flyElement);
+            }
+        }, 1000);
+    }
+
+    /**
+     * 加载AI生图价格
+     */
+    async loadAIGeneratePrice() {
+        try {
+            const response = await fetch('/api/product-pricing');
+            if (response.ok) {
+                const result = await response.json();
+                if (result.success && result.products) {
+                    const aiGenerateProduct = result.products.find(p => p.id === 'ai-generate');
+                    if (aiGenerateProduct && this.aiGeneratePriceEl) {
+                        const price = aiGenerateProduct.price || 0;
+                        if (price > 0) {
+                            this.aiGeneratePriceEl.textContent = `${price} Ani币`;
+                        } else {
+                            this.aiGeneratePriceEl.textContent = '免费';
+                        }
+                    }
+                }
+            }
+        } catch (error) {
+            console.error('[AIGenerateView] 加载AI生图价格失败:', error);
+            if (this.aiGeneratePriceEl) {
+                this.aiGeneratePriceEl.textContent = '-';
+            }
+        }
+    }
+
+    /**
+     * 获取当前登录用户名
+     */
+    getCurrentUsername() {
+        try {
+            const loginDataStr = localStorage.getItem('loginData');
+            if (!loginDataStr) return null;
+            
+            const loginData = JSON.parse(loginDataStr);
+            const now = Date.now();
+            
+            if (now >= loginData.expireTime) {
+                localStorage.removeItem('loginData');
+                return null;
+            }
+            
+            return loginData.user ? loginData.user.username : null;
+        } catch (error) {
+            console.error('[AIGenerateView] 获取用户名失败:', error);
+            return null;
+        }
+    }
+
+    /**
+     * 显示提示
+     */
+    showAlert(message, duration = 2000) {
+        try {
+            if (window.parent && window.parent !== window && window.parent.GlobalAlert) {
+                window.parent.GlobalAlert.show(message, duration);
+                return;
+            }
+            
+            if (window.GlobalAlert) {
+                window.GlobalAlert.show(message, duration);
+                return;
+            }
+            
+            console.log('[Alert]', message);
+        } catch (error) {
+            console.error('[AIGenerateView] 显示 alert 失败:', error);
+            alert(message);
+        }
+    }
+
+    /**
+     * 关闭弹窗
+     */
+    close() {
+        this.reset();
+        
+        if (window.parent && window.parent !== window) {
+            window.parent.postMessage({
+                type: 'close-ai-generate-view'
+            }, '*');
+        }
+    }
+
+    /**
+     * 重置状态
+     */
+    reset() {
+        this.imageData = null;
+        this.referenceImageData = null;
+        this.sessionTaskIds = [];
+        
+        if (this.queuePollingTimer) {
+            clearTimeout(this.queuePollingTimer);
+            this.queuePollingTimer = null;
+        }
+        
+        const queueSection = document.getElementById('aiQueueSection');
+        if (queueSection) {
+            queueSection.style.display = 'none';
+        }
+
+        if (this.referenceImageWrapper) {
+            this.referenceImageWrapper.style.display = 'none';
+        }
+        if (this.referenceImage) {
+            this.referenceImage.src = '';
+        }
+        if (this.referenceUploadArea) {
+            this.referenceUploadArea.classList.remove('hide');
+        }
+        if (this.referenceInput) {
+            this.referenceInput.value = '';
+        }
+
+        if (this.previewImage) {
+            this.previewImage.src = '';
+            this.previewImage.classList.remove('show');
+        }
+        if (this.previewPlaceholder) {
+            const spinner = this.previewPlaceholder.querySelector('.loading-spinner');
+            const loadingText = this.previewPlaceholder.querySelector('.loading-text');
+            if (spinner) spinner.style.display = 'block';
+            if (loadingText) {
+                loadingText.textContent = '正在生成预览图...';
+                loadingText.style.color = '#6b7280';
+            }
+            this.previewPlaceholder.classList.remove('hide');
+        }
+
+        const promptConfigSection = document.getElementById('promptConfigSection');
+        if (promptConfigSection) {
+            promptConfigSection.style.display = 'none';
+        }
+        if (this.additionalPromptInput) {
+            this.additionalPromptInput.value = '';
+        }
+    }
+}
+
+// 初始化
+window.AIGenerateView = new AIGenerateView();
+

+ 20 - 11
client/js/disk/disk.js

@@ -128,29 +128,38 @@ class DiskManager {
     }
 
     async init() {
-        await this.ensureToolBar();
-        await this.ensureContextMenu();
+        // 先尝试从 localStorage 恢复登录状态(同步操作,立即执行)
+        this.restoreLoginFromStorage();
+        
+        // 并行加载工具栏和右键菜单(不阻塞页面)
+        const toolbarPromise = this.ensureToolBar();
+        const contextMenuPromise = this.ensureContextMenu();
+        
+        // 初始化基本元素(这些不依赖工具栏和菜单的HTML)
         this.initElements();
         this.initUploadProgress();
-        this.initContextMenu();
+        this.initUserListener();
+        
+        // 等待工具栏加载完成后,初始化依赖它的组件
+        await toolbarPromise;
+        this.initElements(); // 重新获取工具栏中的元素
         this.initPath();
         this.initSelection();
         this.initShortcutKeys();
         this.initSearchBar();
-        this.initUserListener();
         this.bindEvents();
         
-        // 延迟一下,确保导航栏已加载,然后尝试获取用户名
-        setTimeout(() => {
-            // 先尝试从 localStorage 恢复登录状态
-            this.restoreLoginFromStorage();
-            
+        // 工具栏加载完成后立即开始加载文件列表
             const username = this.getCurrentUsername();
             if (username) {
                 console.log('[DiskManager] 初始化时检测到已登录用户:', username);
-            }
+            // 开始加载文件(不阻塞后续初始化)
         this.loadFiles();
-        }, 500);
+        }
+        
+        // 等待右键菜单加载完成后初始化(并行进行,不影响文件加载)
+        await contextMenuPromise;
+        this.initContextMenu();
         
         // 后台初始化文件结构缓存(不阻塞页面加载)
         this.initFileStructureCache();

+ 7 - 2
client/js/export-view-manager.js

@@ -54,6 +54,8 @@ class ExportViewManager {
 
             // 显示 iframe
             this.frame.style.display = 'block';
+            this.frame.style.pointerEvents = 'auto';
+            this.frame.style.visibility = 'visible';
 
             // 等待 iframe 加载完成后发送文件夹名称
             const sendFolderName = () => {
@@ -84,8 +86,9 @@ class ExportViewManager {
             if (this.frame.contentDocument && this.frame.contentDocument.readyState === 'complete') {
                 handleLoad();
             } else {
-                // 重新加载 iframe 以确保它处于干净状态
-                this.frame.src = this.frame.src;
+                // 重新加载 iframe 以确保它处于干净状态(添加时间戳防止缓存)
+                const baseSrc = this.frame.src.split('?')[0];
+                this.frame.src = baseSrc + '?t=' + Date.now();
             }
         });
     }
@@ -98,6 +101,8 @@ class ExportViewManager {
         
         if (this.frame) {
             this.frame.style.display = 'none';
+            this.frame.style.pointerEvents = 'none';
+            this.frame.style.visibility = 'hidden';
         }
         
         this.isShowing = false;

+ 187 - 345
client/js/export-view/export-view.js

@@ -7,24 +7,16 @@ class ExportView {
         this.modal = null;
         this.previewImage = null;
         this.previewPlaceholder = null;
-        this.referenceBox = null;
-        this.referenceInput = null;
-        this.referenceUploadArea = null;
-        this.referenceImage = null;
-        this.referenceImageWrapper = null;
-        this.referenceRemoveBtn = null;
-        this.replaceBtn = null;
-        this.additionalPromptInput = null;
         this.cancelBtn = null;
         this.confirmBtn = null;
+        this.floatingAIBtn = null;
         this.imageData = null;
-        this.referenceImageData = null;
         this.spritesheetCanvas = null;
         this.folderName = null;
-        this.spritesheetLayout = null; // 保存布局信息用于生成 JSON
-        this.replacedImageData = null; // 保存 Gemini 返回的替换后的图片(base64)
-        this.geminiOriginalImageData = null; // 保存 Gemini 返回的原始图片(未抠图)
-        this.originalSpritesheetData = null; // 保存原始 spritesheet 的 base64 数据
+        this.spritesheetLayout = null;
+        this.replacedImageData = null;
+        this.geminiOriginalImageData = null;
+        this.originalSpritesheetData = null;
         
         // 下载确认对话框相关
         this.downloadConfirmOverlay = null;
@@ -39,16 +31,9 @@ class ExportView {
         this.modal = document.getElementById('exportModal');
         this.previewImage = document.getElementById('previewImage');
         this.previewPlaceholder = document.getElementById('previewPlaceholder');
-        this.referenceBox = document.getElementById('referenceBox');
-        this.referenceInput = document.getElementById('referenceInput');
-        this.referenceUploadArea = document.getElementById('referenceUploadArea');
-        this.referenceImage = document.getElementById('referenceImage');
-        this.referenceImageWrapper = document.getElementById('referenceImageWrapper');
-        this.referenceRemoveBtn = document.getElementById('referenceRemoveBtn');
-        this.replaceBtn = document.getElementById('replaceBtn');
-        this.additionalPromptInput = document.getElementById('additionalPromptInput');
         this.cancelBtn = document.getElementById('exportCancelBtn');
         this.confirmBtn = document.getElementById('exportConfirmBtn');
+        this.floatingAIBtn = document.getElementById('floatingAIBtn');
         
         // 下载确认对话框元素
         this.downloadConfirmOverlay = document.getElementById('downloadConfirmOverlay');
@@ -60,6 +45,9 @@ class ExportView {
             this.downloadConfirmOverlay.style.display = 'none';
         }
         
+        // 加载VIP抠图价格
+        this.loadVIPMattingPrice();
+        
         this.bindEvents();
         
         // 初始时禁用确定按钮
@@ -81,20 +69,14 @@ class ExportView {
             this.close();
         });
 
-        // 确定按钮
+        // 确定按钮(下载)
         this.confirmBtn?.addEventListener('click', () => {
             this.handleConfirm();
         });
 
-        // 替换按钮
-        this.replaceBtn?.addEventListener('click', () => {
-            this.replaceCharacter();
-        });
-
-        // 删除参考图按钮
-        this.referenceRemoveBtn?.addEventListener('click', (e) => {
-            e.stopPropagation(); // 阻止触发父元素的点击事件
-            this.removeReferenceImage();
+        // 悬浮AI按钮 - 打开AI生图界面
+        this.floatingAIBtn?.addEventListener('click', () => {
+            this.openAIGenerateView();
         });
 
         // 点击遮罩层关闭
@@ -104,26 +86,25 @@ class ExportView {
             }
         });
 
-        // ESC键关闭
+        // ESC键关闭 - 直接关闭整个界面
         document.addEventListener('keydown', (e) => {
             if (e.key === 'Escape') {
-                if (this.downloadConfirmOverlay && this.downloadConfirmOverlay.style.display !== 'none') {
                     this.hideDownloadConfirm();
-                } else if (this.overlay) {
                     this.close();
-                }
             }
         });
         
-        // 下载确认对话框事件
+        // 下载确认对话框事件 - 关闭时同时关闭整个界面
         this.downloadConfirmClose?.addEventListener('click', () => {
             this.hideDownloadConfirm();
+            this.close();
         });
         
-        // 点击遮罩层关闭下载确认对话框
+        // 点击遮罩层关闭下载确认对话框 - 同时关闭整个界面
         this.downloadConfirmOverlay?.addEventListener('click', (e) => {
             if (e.target === this.downloadConfirmOverlay) {
                 this.hideDownloadConfirm();
+                this.close();
             }
         });
         
@@ -135,49 +116,6 @@ class ExportView {
             });
         });
 
-        // 参考图上传区域点击
-        this.referenceBox?.addEventListener('click', () => {
-            this.referenceInput?.click();
-        });
-
-        // 参考图选择
-        this.referenceInput?.addEventListener('change', (e) => {
-            const file = e.target.files[0];
-            if (file) {
-                this.loadReferenceImage(file);
-            }
-        });
-
-        // 拖拽上传参考图
-        this.referenceBox?.addEventListener('dragover', (e) => {
-            e.preventDefault();
-            e.stopPropagation();
-            if (this.referenceBox) {
-                this.referenceBox.style.borderColor = '#667eea';
-            }
-        });
-
-        this.referenceBox?.addEventListener('dragleave', (e) => {
-            e.preventDefault();
-            e.stopPropagation();
-            if (this.referenceBox) {
-                this.referenceBox.style.borderColor = '#e5e7eb';
-            }
-        });
-
-        this.referenceBox?.addEventListener('drop', (e) => {
-            e.preventDefault();
-            e.stopPropagation();
-            if (this.referenceBox) {
-                this.referenceBox.style.borderColor = '#e5e7eb';
-            }
-            
-            const file = e.dataTransfer.files[0];
-            if (file && file.type.startsWith('image/')) {
-                this.loadReferenceImage(file);
-            }
-        });
-
         // 监听来自父窗口的消息
         window.addEventListener('message', (event) => {
             if (event.data && event.data.type === 'show-export-preview') {
@@ -220,10 +158,11 @@ class ExportView {
         }
 
         try {
-            const TEXTURE_ROOT = "http://localhost:3000/disk_data";
-            
             // 获取当前登录用户名
             const username = this.getCurrentUsername();
+            if (!username) {
+                throw new Error('请先登录');
+            }
             
             // 获取帧列表
             const encodedFolderName = encodeURIComponent(folderName);
@@ -247,22 +186,22 @@ class ExportView {
                 throw new Error('该文件夹中没有图片');
             }
             
-            // 加载所有图片
+            // 加载所有图片(使用正确的API路径)
             const images = [];
             for (let i = 0; i < frameNumbers.length; i++) {
                 const frameNum = frameNumbers[i];
-                const pathSegments = folderName.split('/').map(seg => encodeURIComponent(seg));
-                const encodedPath = pathSegments.join('/');
                 
-                // 如果有文件名列表,使用实际文件名;否则使用帧号构造文件名
+                // 使用API路径,从用户目录加载
                 let imgSrc;
                 if (fileNames[i]) {
                     // 使用实际文件名
-                    imgSrc = `${TEXTURE_ROOT}/${encodedPath}/${encodeURIComponent(fileNames[i])}`;
+                    const imagePath = `${folderName}/${fileNames[i]}`;
+                    imgSrc = `/api/disk/preview?username=${encodeURIComponent(username)}&path=${encodeURIComponent(imagePath)}`;
                 } else {
                     // 回退到使用帧号构造文件名
                     const frameName = frameNum.toString().padStart(2, '0');
-                    imgSrc = `${TEXTURE_ROOT}/${encodedPath}/${frameName}.png`;
+                    const imagePath = `${folderName}/${frameName}.png`;
+                    imgSrc = `/api/disk/preview?username=${encodeURIComponent(username)}&path=${encodeURIComponent(imagePath)}`;
                 }
                 
                 const img = await new Promise((resolve, reject) => {
@@ -352,7 +291,10 @@ class ExportView {
                 this.replaceBtn.style.display = 'block';
             }
             
-            // 移除自动调用替换 API 的逻辑
+            // 预览图生成完成后,自动显示下载选项对话框
+            setTimeout(() => {
+                this.showDownloadConfirm();
+            }, 300);
         } catch (error) {
             // console.error('[ExportView] 生成预览图失败:', error);
             if (this.previewPlaceholder) {
@@ -392,246 +334,18 @@ class ExportView {
         return `${ratioWidth}:${ratioHeight}`;
     }
 
-    /**
-     * 加载参考图
-     * @param {File} file - 图片文件
-     */
-    loadReferenceImage(file) {
-        const reader = new FileReader();
-        reader.onload = (e) => {
-            this.referenceImageData = e.target.result;
-            if (this.referenceImage) {
-                this.referenceImage.src = e.target.result;
-            }
-            if (this.referenceImageWrapper) {
-                this.referenceImageWrapper.style.display = 'flex';
-            }
-            if (this.referenceUploadArea) {
-                this.referenceUploadArea.classList.add('hide');
-            }
-            
-            // 显示替换按钮和提示词配置区域(如果已经有 spritesheet)
-            if (this.originalSpritesheetData) {
-                if (this.replaceBtn) {
-                    this.replaceBtn.style.display = 'block';
-                }
-                // 显示提示词配置区域
-                const promptConfigSection = document.getElementById('promptConfigSection');
-                if (promptConfigSection) {
-                    promptConfigSection.style.display = 'flex';
-                }
-            }
-            
-            // 移除自动调用替换 API 的逻辑
-        };
-        reader.readAsDataURL(file);
-    }
-
-    /**
-     * 删除参考图
-     */
-    removeReferenceImage() {
-        // 清空参考图数据
-        this.referenceImageData = null;
-        this.replacedImageData = null; // 同时清空替换后的图片
-
-        // 隐藏参考图,显示上传区域
-        if (this.referenceImageWrapper) {
-            this.referenceImageWrapper.style.display = 'none';
-        }
-        if (this.referenceImage) {
-            this.referenceImage.src = '';
-        }
-        if (this.referenceUploadArea) {
-            this.referenceUploadArea.classList.remove('hide');
-        }
-        if (this.referenceInput) {
-            this.referenceInput.value = '';
-        }
-
-        // 隐藏替换按钮和提示词配置区域
-        if (this.replaceBtn) {
-            this.replaceBtn.style.display = 'none';
-            this.replaceBtn.disabled = false;
-        }
-        const promptConfigSection = document.getElementById('promptConfigSection');
-        if (promptConfigSection) {
-            promptConfigSection.style.display = 'none';
-        }
-
-        // 如果预览图是替换后的图片,恢复显示原始 spritesheet
-        if (this.replacedImageData && this.originalSpritesheetData) {
-            // 清空替换后的图片,恢复显示原始预览
-            this.replacedImageData = null;
-            // 重新显示原始 spritesheet
-            if (this.spritesheetCanvas) {
-                this.spritesheetCanvas.toBlob((blob) => {
-                    const url = URL.createObjectURL(blob);
-                    this.showPreview(url);
-                }, 'image/png');
-            }
-        }
-    }
-
-    /**
-     * 调用角色替换 API
-     */
-    async replaceCharacter() {
-        if (!this.originalSpritesheetData || !this.referenceImageData) {
-            this.showAlert('请先上传参考图和生成 Spritesheet');
-            return;
-        }
-
-        // 禁用替换按钮和下载按钮
-        if (this.replaceBtn) {
-            this.replaceBtn.disabled = true;
-        }
-        if (this.confirmBtn) {
-            this.confirmBtn.disabled = true;
-        }
-
-        try {
-            // 显示加载状态
-            if (this.previewPlaceholder) {
-                const spinner = this.previewPlaceholder.querySelector('.loading-spinner');
-                const loadingText = this.previewPlaceholder.querySelector('.loading-text');
-                if (spinner) spinner.style.display = 'block';
-                if (loadingText) {
-                    loadingText.textContent = '正在生成替换后的图片...';
-                    loadingText.style.color = '#6b7280';
-                }
-                this.previewPlaceholder.classList.remove('hide');
-            }
-            if (this.previewImage) {
-                this.previewImage.classList.remove('show');
-            }
-
-            // 准备图片数据(移除 data:image/png;base64, 前缀)
-            const image1Base64 = this.originalSpritesheetData.replace(/^data:image\/\w+;base64,/, '');
-            const image2Base64 = this.referenceImageData.replace(/^data:image\/\w+;base64,/, '');
-
-            // 获取 image1 的尺寸
-            const image1Width = this.spritesheetLayout?.sheetWidth || 0;
-            const image1Height = this.spritesheetLayout?.sheetHeight || 0;
-            
-            // 获取额外提示词
-            const additionalPrompt = this.additionalPromptInput?.value || '';
-
-            // 调用 API
-            const response = await fetch('http://localhost:3000/api/replace-character', {
-                method: 'POST',
-                headers: {
-                    'Content-Type': 'application/json'
-                },
-                body: JSON.stringify({
-                    image1: image1Base64,
-                    image2: image2Base64,
-                    image1Width: image1Width,
-                    image1Height: image1Height,
-                    additionalPrompt: additionalPrompt
-                })
-            });
-
-            if (!response.ok) {
-                const errorMessage = await this.handleServerError(response, '角色替换失败');
-                throw new Error(errorMessage);
-            }
-
-            const result = await response.json();
-            
-            if (result.success && result.imageData) {
-                // Gemini 返回的图片 base64(不进行抠图,直接保存原始图片)
-                const geminiImageBase64 = result.imageData;
-                
-                // 保存 Gemini 返回的原始图片(不抠图)
-                this.geminiOriginalImageData = `data:image/png;base64,${geminiImageBase64}`;
-                this.replacedImageData = this.geminiOriginalImageData; // 用于显示预览
-                
-                // 显示原始图片(不抠图)
-                this.showPreview(this.geminiOriginalImageData);
-            } else {
-                throw new Error('API 返回失败或未找到图片数据');
-            }
-        } catch (error) {
-            // console.error('[ExportView] 替换角色失败:', error);
-            if (this.previewPlaceholder) {
-                const spinner = this.previewPlaceholder.querySelector('.loading-spinner');
-                const loadingText = this.previewPlaceholder.querySelector('.loading-text');
-                if (spinner) spinner.style.display = 'none';
-                if (loadingText) {
-                    loadingText.textContent = '替换失败: ' + error.message;
-                    loadingText.style.color = '#ef4444';
-                }
-                this.previewPlaceholder.classList.remove('hide');
-            }
-        } finally {
-            // 重新启用替换按钮和下载按钮
-            if (this.replaceBtn) {
-                this.replaceBtn.disabled = false;
-            }
-            if (this.confirmBtn) {
-                this.confirmBtn.disabled = false;
-            }
-        }
-    }
 
     /**
-     * 显示预览图(Spritesheet)
+     * 显示预览图(Spritesheet)- 简化版,不再显示预览界面
      * @param {string} imageUrl - 图片URL或base64数据
      */
     showPreview(imageUrl) {
         if (!imageUrl) {
-            // console.warn('[ExportView] 没有提供图片数据');
             return;
         }
-
-        // console.log('[ExportView] 显示预览图');
         
         // 保存图片数据
         this.imageData = imageUrl;
-        
-        // 图片加载前禁用下载按钮
-        if (this.confirmBtn) {
-            this.confirmBtn.disabled = true;
-        }
-        
-        // 加载图片
-        const img = new Image();
-        img.onload = () => {
-            if (this.previewImage) {
-                this.previewImage.src = imageUrl;
-                this.previewImage.classList.add('show');
-            }
-            if (this.previewPlaceholder) {
-                this.previewPlaceholder.classList.add('hide');
-            }
-            // 图片加载完成后启用下载按钮
-            if (this.confirmBtn) {
-                this.confirmBtn.disabled = false;
-            }
-            // console.log('[ExportView] ✓ 预览图已加载');
-        };
-        
-        img.onerror = () => {
-            // 图片加载失败时禁用下载按钮
-            if (this.confirmBtn) {
-                this.confirmBtn.disabled = true;
-            }
-            // console.error('[ExportView] 图片加载失败');
-            if (this.previewPlaceholder) {
-                // 隐藏加载动画,显示错误信息
-                const spinner = this.previewPlaceholder.querySelector('.loading-spinner');
-                const loadingText = this.previewPlaceholder.querySelector('.loading-text');
-                if (spinner) spinner.style.display = 'none';
-                if (loadingText) {
-                    loadingText.textContent = '图片加载失败';
-                    loadingText.style.color = '#ef4444';
-                }
-                this.previewPlaceholder.classList.remove('hide');
-            }
-        };
-        
-        img.src = imageUrl;
     }
 
     /**
@@ -740,6 +454,8 @@ class ExportView {
     showDownloadConfirm() {
         if (this.downloadConfirmOverlay) {
             console.log('[ExportView] 显示下载确认对话框');
+            // 确保价格已加载
+            this.loadVIPMattingPrice();
             this.downloadConfirmOverlay.style.display = 'flex';
         } else {
             console.error('[ExportView] 下载确认对话框元素未找到');
@@ -755,11 +471,130 @@ class ExportView {
         }
     }
 
+    /**
+     * 打开AI生图界面
+     */
+    openAIGenerateView() {
+        if (!this.originalSpritesheetData) {
+            this.showAlert('请先生成预览图');
+            return;
+        }
+        
+        // 通知父窗口打开AI生图界面
+        if (window.parent && window.parent !== window) {
+            window.parent.postMessage({
+                type: 'open-ai-generate-view',
+                folderName: this.folderName,
+                spritesheetData: this.originalSpritesheetData,
+                spritesheetLayout: this.spritesheetLayout
+            }, '*');
+        }
+    }
+
     /**
      * 处理下载选项选择
      * @param {string} downloadType - 下载类型:'original', 'normal', 'vip'
      */
     async handleDownloadOption(downloadType) {
+        // 如果是VIP抠图,先检查用户Ani币是否足够
+        if (downloadType === 'vip') {
+            const username = this.getCurrentUsername();
+            if (!username) {
+                this.showAlert('请先登录');
+                return;
+            }
+
+            try {
+                // 获取VIP抠图价格
+                const pricingResponse = await fetch('/api/product-pricing');
+                if (!pricingResponse.ok) {
+                    throw new Error('获取价格失败');
+                }
+                const pricingResult = await pricingResponse.json();
+                if (!pricingResult.success || !pricingResult.products) {
+                    throw new Error('获取价格失败');
+                }
+                
+                const vipMattingProduct = pricingResult.products.find(p => p.id === 'vip-matting');
+                const price = vipMattingProduct ? (vipMattingProduct.price || 0) : 0;
+                
+                // 如果价格为0,直接继续
+                if (price === 0) {
+                    // 继续执行VIP抠图流程
+                } else {
+                    // 检查用户点数
+                    const pointsResponse = await fetch(`/api/user/points?username=${encodeURIComponent(username)}`);
+                    if (!pointsResponse.ok) {
+                        throw new Error('获取点数失败');
+                    }
+                    const pointsResult = await pointsResponse.json();
+                    if (!pointsResult.success) {
+                        throw new Error('获取点数失败');
+                    }
+                    
+                    const userPoints = pointsResult.points || 0;
+                    
+                    if (userPoints < price) {
+                        // 点数不足,弹出充值界面
+                        if (window.parent && window.parent !== window) {
+                            window.parent.postMessage({
+                                type: 'open-recharge-view',
+                                needPoints: price,
+                                currentPoints: userPoints
+                            }, '*');
+                        }
+                        this.hideDownloadConfirm();
+                        return;
+                    }
+                    
+                    // 点数充足,弹出确认对话框
+                    let confirmed = false;
+                    if (window.parent && window.parent.GlobalConfirm) {
+                        confirmed = await window.parent.GlobalConfirm.show(
+                            `确定要花费 ${price} Ani币使用VIP抠图吗?`
+                        );
+                    } else {
+                        confirmed = confirm(`确定要花费 ${price} Ani币使用VIP抠图吗?`);
+                    }
+                    
+                    if (!confirmed) {
+                        return;
+                    }
+                    
+                    // 扣除点数
+                    const deductResponse = await fetch('/api/user/deduct-points', {
+                        method: 'POST',
+                        headers: {
+                            'Content-Type': 'application/json'
+                        },
+                        body: JSON.stringify({
+                            username: username,
+                            points: price
+                        })
+                    });
+                    
+                    if (!deductResponse.ok) {
+                        const deductResult = await deductResponse.json();
+                        throw new Error(deductResult.message || '扣除点数失败');
+                    }
+                    
+                    const deductResult = await deductResponse.json();
+                    if (!deductResult.success) {
+                        throw new Error(deductResult.message || '扣除点数失败');
+                    }
+                    
+                    // 通知父窗口刷新点数
+                    if (window.parent && window.parent !== window) {
+                        window.parent.postMessage({ type: 'refresh-points' }, '*');
+                    }
+                }
+            } catch (error) {
+                console.error('[ExportView] VIP抠图购买检查失败:', error);
+                this.showAlert(error.message || '操作失败,请稍后重试');
+                return;
+            }
+        }
+        
         this.hideDownloadConfirm();
         
         try {
@@ -1098,30 +933,16 @@ class ExportView {
     reset() {
         // 清空数据属性
         this.imageData = null;
-        this.referenceImageData = null;
         this.spritesheetCanvas = null;
         this.folderName = null;
         this.spritesheetLayout = null;
         this.replacedImageData = null;
         this.geminiOriginalImageData = null;
         this.originalSpritesheetData = null;
-
-        // 重置参考图区域
-        if (this.referenceImageWrapper) {
-            this.referenceImageWrapper.style.display = 'none';
-        }
-        if (this.referenceImage) {
-            this.referenceImage.src = '';
-        }
-        if (this.referenceUploadArea) {
-            this.referenceUploadArea.classList.remove('hide');
-        }
-        if (this.referenceInput) {
-            this.referenceInput.value = '';
-        }
-        if (this.replaceBtn) {
-            this.replaceBtn.style.display = 'none';
-            this.replaceBtn.disabled = false;
+        
+        // 重置AI按钮状态
+        if (this.floatingAIBtn) {
+            this.floatingAIBtn.disabled = true;
         }
 
         // 重置预览图区域
@@ -1140,21 +961,42 @@ class ExportView {
             this.previewPlaceholder.classList.remove('hide');
         }
 
-        // 重置提示词配置区域
-        const promptConfigSection = document.getElementById('promptConfigSection');
-        if (promptConfigSection) {
-            promptConfigSection.style.display = 'none';
-        }
-        if (this.additionalPromptInput) {
-            this.additionalPromptInput.value = '';
-        }
-
         // 重置按钮状态
         if (this.confirmBtn) {
             this.confirmBtn.disabled = true;
         }
     }
 
+    /**
+     * 加载VIP抠图价格
+     */
+    async loadVIPMattingPrice() {
+        try {
+            const response = await fetch('/api/product-pricing');
+            if (response.ok) {
+                const result = await response.json();
+                if (result.success && result.products) {
+                    const vipMattingProduct = result.products.find(p => p.id === 'vip-matting');
+                    const priceEl = document.getElementById('vipMattingPrice');
+                    if (vipMattingProduct && priceEl) {
+                        const price = vipMattingProduct.price || 0;
+                        if (price > 0) {
+                            priceEl.textContent = `${price} Ani币`;
+                        } else {
+                            priceEl.textContent = '免费';
+                        }
+                    }
+                }
+            }
+        } catch (error) {
+            console.error('[ExportView] 加载VIP抠图价格失败:', error);
+            const priceEl = document.getElementById('vipMattingPrice');
+            if (priceEl) {
+                priceEl.textContent = '-';
+            }
+        }
+    }
+
     /**
      * 显示提示信息(使用全局 Alert 组件,直接调用)
      * @param {string} message - 提示信息

+ 94 - 0
client/js/hint-view.js

@@ -0,0 +1,94 @@
+// 提示消息组件
+(function() {
+    let hintView = null;
+    let hintMessage = null;
+    let hideTimeout = null;
+
+    function init() {
+        // 如果 hint-view.html 已经加载,直接获取元素
+        hintView = document.getElementById('hintView');
+        hintMessage = document.getElementById('hintMessage');
+        
+        // 如果元素不存在,创建它们
+        if (!hintView) {
+            hintView = document.createElement('div');
+            hintView.className = 'hint-view';
+            hintView.id = 'hintView';
+            
+            hintMessage = document.createElement('span');
+            hintMessage.className = 'hint-message';
+            hintMessage.id = 'hintMessage';
+            
+            const hintContent = document.createElement('div');
+            hintContent.className = 'hint-content';
+            
+            const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+            icon.setAttribute('class', 'hint-icon');
+            icon.setAttribute('width', '20');
+            icon.setAttribute('height', '20');
+            icon.setAttribute('viewBox', '0 0 20 20');
+            icon.setAttribute('fill', 'currentColor');
+            const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+            path.setAttribute('d', 'M10 0C4.48 0 0 4.48 0 10s4.48 10 10 10 10-4.48 10-10S15.52 0 10 0zm-1 15l-5-5 1.41-1.41L9 12.17l7.59-7.59L18 6l-9 9z');
+            icon.appendChild(path);
+            
+            hintContent.appendChild(icon);
+            hintContent.appendChild(hintMessage);
+            hintView.appendChild(hintContent);
+            
+            document.body.appendChild(hintView);
+        }
+    }
+
+    function show(message, type = 'success', duration = 3000) {
+        init();
+        
+        // 清除之前的定时器
+        if (hideTimeout) {
+            clearTimeout(hideTimeout);
+            hideTimeout = null;
+        }
+        
+        // 移除之前的类型类
+        hintView.classList.remove('success', 'error', 'warning', 'info', 'show', 'hide');
+        
+        // 设置消息和类型
+        hintMessage.textContent = message;
+        hintView.classList.add(type);
+        
+        // 显示提示
+        requestAnimationFrame(() => {
+            hintView.classList.add('show');
+        });
+        
+        // 自动隐藏
+        hideTimeout = setTimeout(() => {
+            hide();
+        }, duration);
+    }
+
+    function hide() {
+        if (!hintView) return;
+        
+        // 添加隐藏动画类
+        hintView.classList.remove('show');
+        hintView.classList.add('hide');
+        
+        // 动画结束后移除类
+        setTimeout(() => {
+            hintView.classList.remove('hide', 'success', 'error', 'warning', 'info');
+            hintMessage.textContent = '';
+        }, 300); // 与 CSS 动画时间一致
+    }
+
+    // 导出全局函数
+    window.HintView = {
+        show: show,
+        hide: hide,
+        success: (message, duration) => show(message, 'success', duration),
+        error: (message, duration) => show(message, 'error', duration),
+        warning: (message, duration) => show(message, 'warning', duration),
+        info: (message, duration) => show(message, 'info', duration)
+    };
+})();
+

+ 239 - 0
client/js/pay-view/pay-view.js

@@ -0,0 +1,239 @@
+// 支付界面逻辑
+
+(function() {
+  let overlay = null;
+  let container = null;
+  let closeBtn = null;
+  let cancelBtn = null;
+  let itemNameEl = null;
+  let itemPriceEl = null;
+  
+  let currentPaymentData = null;
+  let resolveCallback = null;
+
+  function init() {
+    overlay = document.getElementById('payViewOverlay');
+    container = document.getElementById('payViewContainer');
+    closeBtn = document.getElementById('payViewClose');
+    cancelBtn = document.getElementById('payCancelBtn');
+    itemNameEl = document.getElementById('payItemName');
+    itemPriceEl = document.getElementById('payItemPrice');
+
+    if (!overlay || !container) {
+      console.error('[PayView] 支付界面元素未找到');
+      return;
+    }
+
+    bindEvents();
+  }
+
+  function bindEvents() {
+    // 关闭按钮 - 点击关闭视为支付成功
+    if (closeBtn) {
+      closeBtn.addEventListener('click', handlePaymentSuccess);
+    }
+
+    // 取消按钮 - 点击取消视为支付成功(演示模式)
+    if (cancelBtn) {
+      cancelBtn.addEventListener('click', handlePaymentSuccess);
+    }
+
+    // 点击遮罩层关闭 - 视为支付成功
+    if (overlay) {
+      overlay.addEventListener('click', (e) => {
+        if (e.target === overlay) {
+          handlePaymentSuccess();
+        }
+      });
+    }
+
+    // ESC 键关闭 - 视为支付成功
+    document.addEventListener('keydown', (e) => {
+      if (e.key === 'Escape' && overlay && overlay.classList.contains('show')) {
+        handlePaymentSuccess();
+      }
+    });
+
+    // 监听来自父窗口的消息
+    window.addEventListener('message', (event) => {
+      if (event.data && event.data.type === 'open-pay-view') {
+        show(event.data.itemName, event.data.price, event.data.resourcePath, event.data.categoryDir);
+      }
+    });
+  }
+
+  function show(itemName, price, resourcePath, categoryDir) {
+    if (!overlay) {
+      init();
+    }
+
+    // 保存支付数据
+    currentPaymentData = {
+      itemName,
+      price,
+      resourcePath,
+      categoryDir
+    };
+
+    // 更新显示内容
+    if (itemNameEl) {
+      itemNameEl.textContent = itemName;
+    }
+    if (itemPriceEl) {
+      itemPriceEl.textContent = `¥${price}`;
+    }
+
+    // 显示支付界面
+    overlay.classList.add('show');
+    overlay.style.pointerEvents = 'auto';
+
+    // 返回 Promise,等待支付结果
+    return new Promise((resolve) => {
+      resolveCallback = resolve;
+    });
+  }
+
+  function hide() {
+    if (overlay) {
+      overlay.classList.remove('show');
+      // 确保遮罩层不会阻止点击
+      overlay.style.pointerEvents = 'none';
+    }
+    currentPaymentData = null;
+    resolveCallback = null;
+  }
+
+  async function handlePaymentSuccess() {
+    if (!currentPaymentData) {
+      hide();
+      return;
+    }
+
+    try {
+      // 调用支付成功 API
+      const username = getCurrentUsername();
+      if (!username) {
+        alert('请先登录');
+        hide();
+        if (resolveCallback) {
+          resolveCallback({ success: false, message: '未登录' });
+        }
+        return;
+      }
+
+      const response = await fetch('/api/pay/purchase', {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json'
+        },
+        body: JSON.stringify({
+          username: username,
+          resourcePath: currentPaymentData.resourcePath,
+          categoryDir: currentPaymentData.categoryDir,
+          itemName: currentPaymentData.itemName,
+          price: currentPaymentData.price
+        })
+      });
+
+      const result = await response.json();
+
+      if (result.success) {
+        // 支付成功,通知父窗口
+        if (window.parent && window.parent !== window) {
+          window.parent.postMessage({
+            type: 'payment-success',
+            itemName: currentPaymentData.itemName
+          }, '*');
+          
+          // 通知父窗口关闭支付界面 iframe
+          window.parent.postMessage({
+            type: 'close-pay-view'
+          }, '*');
+        }
+
+        // 显示成功提示
+        if (window.parent && window.parent.HintView) {
+          window.parent.HintView.success('购买成功!文件已添加到网盘', 3000);
+        }
+
+        hide();
+        if (resolveCallback) {
+          resolveCallback({ success: true, result });
+        }
+      } else {
+        // 支付失败
+        if (window.parent && window.parent.HintView) {
+          window.parent.HintView.error(result.message || '购买失败', 3000);
+        }
+        
+        // 通知父窗口关闭支付界面 iframe
+        if (window.parent && window.parent !== window) {
+          window.parent.postMessage({
+            type: 'close-pay-view'
+          }, '*');
+        }
+        
+        hide();
+        if (resolveCallback) {
+          resolveCallback({ success: false, message: result.message });
+        }
+      }
+    } catch (error) {
+      console.error('[PayView] 支付处理失败:', error);
+      if (window.parent && window.parent.HintView) {
+        window.parent.HintView.error('购买失败,请稍后重试', 3000);
+      }
+      
+      // 通知父窗口关闭支付界面 iframe
+      if (window.parent && window.parent !== window) {
+        window.parent.postMessage({
+          type: 'close-pay-view'
+        }, '*');
+      }
+      
+      hide();
+      if (resolveCallback) {
+        resolveCallback({ success: false, message: error.message });
+      }
+    }
+  }
+
+
+  // 获取当前登录用户名
+  function getCurrentUsername() {
+    try {
+      const loginDataStr = localStorage.getItem('loginData');
+      if (!loginDataStr) {
+        return null;
+      }
+      
+      const loginData = JSON.parse(loginDataStr);
+      const now = Date.now();
+      
+      // 检查是否过期
+      if (now >= loginData.expireTime) {
+        localStorage.removeItem('loginData');
+        return null;
+      }
+      
+      return loginData.user ? loginData.user.username : null;
+    } catch (error) {
+      console.error('[PayView] 获取用户名失败:', error);
+      return null;
+    }
+  }
+
+  // 导出全局函数
+  window.PayView = {
+    show: show,
+    hide: hide
+  };
+
+  // 初始化
+  if (document.readyState === 'loading') {
+    document.addEventListener('DOMContentLoaded', init);
+  } else {
+    init();
+  }
+})();
+

+ 761 - 0
client/js/profile/profile.js

@@ -0,0 +1,761 @@
+// 我的界面逻辑
+
+(function() {
+  let currentUsername = null;
+  let currentUserInfo = null;
+
+  function init() {
+    bindEvents();
+    // 延迟加载,确保DOM完全渲染
+    setTimeout(() => {
+      loadUserInfo();
+      loadPoints();
+      loadAIHistory();
+      loadPurchaseHistory();
+    }, 200);
+  }
+
+  function bindEvents() {
+    // 返回按钮
+    const backBtn = document.getElementById('backBtn');
+    if (backBtn) {
+      backBtn.addEventListener('click', () => {
+        // 如果在 iframe 中,通过 postMessage 通知父窗口切换页面
+        if (window.parent && window.parent !== window) {
+          window.parent.postMessage({
+            type: 'navigation',
+            page: 'store'
+          }, '*');
+        } else {
+          // 独立页面,直接跳转到主页面
+          window.location.href = '../../index.html';
+        }
+      });
+    }
+
+    // 头像上传功能已移除
+
+    // 充值按钮
+    const rechargeBtn = document.getElementById('rechargeBtn');
+    if (rechargeBtn) {
+      rechargeBtn.addEventListener('click', () => {
+        // 打开充值界面(通过postMessage通知父窗口,如果是独立页面则直接打开)
+        if (window.parent && window.parent !== window) {
+          window.parent.postMessage({
+            type: 'open-recharge-view'
+          }, '*');
+        } else {
+          // 独立页面,需要创建充值iframe
+          openRechargeView();
+        }
+      });
+    }
+
+    // 退出登录按钮
+    const logoutBtn = document.getElementById('logoutBtn');
+    if (logoutBtn) {
+      logoutBtn.addEventListener('click', handleLogout);
+    }
+
+    // 监听消息
+    window.addEventListener('message', (event) => {
+      if (event.data && event.data.type === 'recharge-success') {
+        loadPoints(); // 刷新点数
+        closeRechargeView(); // 关闭充值界面
+      } else if (event.data && event.data.type === 'close-recharge-view') {
+        closeRechargeView();
+      } else if (event.data && event.data.type === 'open-recharge-view') {
+        // 如果充值iframe发送消息,也打开充值界面
+        openRechargeView();
+      } else if (event.data && event.data.type === 'refresh-points') {
+        // 刷新点数(购买成功后触发)
+        loadPoints();
+        loadPurchaseHistory(); // 同时刷新购买记录
+      } else if (event.data && event.data.type === 'refresh-ai-history') {
+        // 刷新AI历史(生图请求成功后触发)
+        loadAIHistory();
+      }
+    });
+  }
+
+  // 打开充值界面
+  function openRechargeView() {
+    const rechargeFrame = document.getElementById('rechargeViewFrame');
+    if (rechargeFrame) {
+      rechargeFrame.style.display = 'block';
+      rechargeFrame.style.pointerEvents = 'auto';
+      rechargeFrame.style.visibility = 'visible';
+      
+      // 发送消息给充值iframe
+      const sendRechargeData = () => {
+        if (rechargeFrame.contentWindow) {
+          rechargeFrame.contentWindow.postMessage({
+            type: 'open-recharge-view'
+          }, '*');
+        } else {
+          setTimeout(sendRechargeData, 100);
+        }
+      };
+      
+      sendRechargeData();
+      
+      const handleLoad = () => {
+        setTimeout(() => {
+          sendRechargeData();
+        }, 50);
+      };
+      
+      if (rechargeFrame.contentDocument && rechargeFrame.contentDocument.readyState === 'complete') {
+        handleLoad();
+      } else {
+        rechargeFrame.addEventListener('load', handleLoad, { once: true });
+      }
+    }
+  }
+
+  // 关闭充值界面
+  function closeRechargeView() {
+    const rechargeFrame = document.getElementById('rechargeViewFrame');
+    if (rechargeFrame) {
+      rechargeFrame.style.display = 'none';
+      rechargeFrame.style.pointerEvents = 'none';
+      rechargeFrame.style.visibility = 'hidden';
+    }
+  }
+
+  // 加载用户信息
+  async function loadUserInfo() {
+    const username = getCurrentUsername();
+    if (!username) {
+      // 未登录,跳转到登录页
+      window.location.href = '../../index.html';
+      return;
+    }
+
+    currentUsername = username;
+
+    // 先从localStorage读取用户信息作为备用
+    try {
+      const loginDataStr = localStorage.getItem('loginData');
+      if (loginDataStr) {
+        const loginData = JSON.parse(loginDataStr);
+        if (loginData.user) {
+          // 先显示localStorage中的用户信息
+          displayUserInfo(loginData.user);
+        }
+      }
+    } catch (error) {
+      console.error('[Profile] 从localStorage读取用户信息失败:', error);
+    }
+
+    // 然后从API获取最新信息
+    try {
+      const response = await fetch(`/api/user/info?username=${encodeURIComponent(username)}`);
+      if (!response.ok) {
+        throw new Error(`HTTP error! status: ${response.status}`);
+      }
+      const result = await response.json();
+      
+      if (result.success && result.user) {
+        currentUserInfo = result.user;
+        displayUserInfo(result.user);
+      } else {
+        console.error('[Profile] API返回数据格式错误:', result);
+      }
+    } catch (error) {
+      console.error('[Profile] 加载用户信息失败:', error);
+      // 如果API调用失败,至少显示localStorage中的信息
+      if (window.parent && window.parent.HintView) {
+        window.parent.HintView.error('加载用户信息失败,显示缓存信息', 2000);
+      }
+    }
+  }
+
+  // 手机号脱敏处理
+  function maskPhone(phone) {
+    if (!phone || phone.length < 7) {
+      return phone;
+    }
+    // 保留前3位和后4位,中间用*代替
+    const start = phone.substring(0, 3);
+    const end = phone.substring(phone.length - 4);
+    const middle = '*'.repeat(phone.length - 7);
+    return start + middle + end;
+  }
+
+  // 显示用户信息
+  function displayUserInfo(user) {
+    if (!user) {
+      console.error('[Profile] 用户信息为空');
+      return;
+    }
+
+    // 使用函数来确保元素存在
+    const setUserInfo = () => {
+      const avatarImage = document.getElementById('avatarImage');
+      const usernameInput = document.getElementById('usernameInput');
+      const phoneInput = document.getElementById('phoneInput');
+
+      if (avatarImage) {
+        let avatarUrl = user.avatar;
+        if (avatarUrl && !avatarUrl.startsWith('http') && !avatarUrl.startsWith('data:')) {
+          avatarUrl = 'http://localhost:3000' + (avatarUrl.startsWith('/') ? avatarUrl : '/' + avatarUrl);
+        }
+        avatarImage.src = avatarUrl || '../../static/default-avatar.png';
+        avatarImage.onerror = function() {
+          this.src = '../../static/default-avatar.png';
+        };
+      }
+
+      if (usernameInput) {
+        usernameInput.value = user.username || '';
+      }
+
+      if (phoneInput) {
+        const phone = user.phone || '';
+        phoneInput.value = maskPhone(phone);
+      }
+    };
+
+    // 如果元素还没准备好,等待一下再试
+    if (!document.getElementById('usernameInput') || !document.getElementById('phoneInput')) {
+      setTimeout(setUserInfo, 100);
+    } else {
+      setUserInfo();
+    }
+  }
+
+  // 加载点数
+  async function loadPoints() {
+    const username = getCurrentUsername();
+    if (!username) return;
+
+    try {
+      const response = await fetch(`/api/user/points?username=${encodeURIComponent(username)}`);
+      if (!response.ok) {
+        throw new Error(`HTTP error! status: ${response.status}`);
+      }
+      const result = await response.json();
+      
+      if (result.success) {
+        const pointsValue = document.getElementById('pointsValue');
+        if (pointsValue) {
+          const points = parseFloat(result.points || 0);
+          pointsValue.textContent = points.toFixed(2);
+        }
+      }
+    } catch (error) {
+      console.error('[Profile] 加载点数失败:', error);
+    }
+  }
+
+  // 处理退出登录
+  function handleLogout() {
+    if (confirm('确定要退出登录吗?')) {
+      // 清除localStorage
+      localStorage.removeItem('loginData');
+      
+      // 通知父窗口(如果是在iframe中)
+      if (window.parent && window.parent !== window) {
+        window.parent.postMessage({
+          type: 'logout'
+        }, '*');
+      }
+      
+      // 跳转到登录页
+      window.location.href = '../../index.html';
+    }
+  }
+
+
+  // 加载AI生图历史
+  async function loadAIHistory() {
+    const username = getCurrentUsername();
+    if (!username) return;
+
+    const historyLoading = document.getElementById('historyLoading');
+    const historyEmpty = document.getElementById('historyEmpty');
+    const historyGrid = document.getElementById('historyGrid');
+
+    if (historyLoading) {
+      historyLoading.style.display = 'block';
+    }
+    if (historyEmpty) {
+      historyEmpty.style.display = 'none';
+    }
+    if (historyGrid) {
+      historyGrid.innerHTML = '';
+    }
+
+    try {
+      const response = await fetch(`/api/ai/history?username=${encodeURIComponent(username)}`);
+      if (!response.ok) {
+        throw new Error(`HTTP error! status: ${response.status}`);
+      }
+      const result = await response.json();
+      
+      if (historyLoading) {
+        historyLoading.style.display = 'none';
+      }
+
+      if (result.success && result.history && result.history.length > 0) {
+        if (historyGrid) {
+          result.history.forEach(item => {
+            const historyItem = createHistoryItem(item);
+            historyGrid.appendChild(historyItem);
+          });
+        }
+      } else {
+        if (historyEmpty) {
+          historyEmpty.style.display = 'block';
+        }
+      }
+      
+      // 如果有正在处理的任务,定期刷新
+      if (result.success && result.history) {
+        const hasProcessing = result.history.some(item => item.status === 'rendering' || item.status === 'queued');
+        if (hasProcessing) {
+          setTimeout(loadAIHistory, 3000); // 3秒后刷新
+        }
+      }
+    } catch (error) {
+      console.error('[Profile] 加载AI历史失败:', error);
+      if (historyLoading) {
+        historyLoading.style.display = 'none';
+      }
+      if (historyEmpty) {
+        historyEmpty.style.display = 'block';
+      }
+    }
+  }
+
+  // 创建历史记录项
+  function createHistoryItem(item) {
+    const div = document.createElement('div');
+    div.className = 'history-item';
+    div.dataset.taskId = item.id;
+    
+    // 格式化时间
+    const formatTime = (dateString) => {
+      if (!dateString) return '';
+      const date = new Date(dateString);
+      const now = new Date();
+      const diff = now - date;
+      const minutes = Math.floor(diff / 60000);
+      const hours = Math.floor(diff / 3600000);
+      const days = Math.floor(diff / 86400000);
+      
+      if (minutes < 1) return '刚刚';
+      if (minutes < 60) return `${minutes}分钟前`;
+      if (hours < 24) return `${hours}小时前`;
+      if (days < 7) return `${days}天前`;
+      
+      return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
+    };
+    
+    const timeText = formatTime(item.createdAt);
+    
+    // 预览图URL(模糊显示)
+    const previewUrl = item.previewUrl || '';
+    
+    if (item.status === 'rendering' || item.status === 'queued') {
+      div.classList.add(item.status);
+      const statusText = item.status === 'rendering' ? '正在生成' : '等待中';
+      div.innerHTML = `
+        ${previewUrl ? `<img class="history-item-preview" src="${previewUrl}" alt="预览图" onerror="this.style.display='none'">` : ''}
+        <div class="history-item-loading-overlay">
+          <div class="loading-spinner-container">
+            <div class="loading-spinner"></div>
+          </div>
+          <div class="loading-status-text">${statusText}</div>
+        </div>
+        <div class="history-item-time">${timeText}</div>
+      `;
+    } else if (item.status === 'completed' && item.imageUrl) {
+      div.innerHTML = `
+        <img class="history-item-image" src="${item.imageUrl}" alt="AI生成图" onerror="this.src='../../static/default-avatar.png'">
+        <div class="history-item-overlay">
+          <div class="history-item-actions">
+            <button class="history-action-btn preview-btn" title="预览">
+              <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
+                <circle cx="12" cy="12" r="3"/>
+              </svg>
+              <span>预览</span>
+            </button>
+            <button class="history-action-btn download-btn" title="下载">
+              <svg width="22" height="22" viewBox="0 0 16 16" fill="none">
+                <path d="M8 11V3M8 11L5 8M8 11L11 8M3 13H13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+              </svg>
+              <span>下载</span>
+            </button>
+          </div>
+        </div>
+        <div class="history-item-time">${timeText}</div>
+      `;
+      
+      // 点击预览按钮
+      const previewBtn = div.querySelector('.preview-btn');
+      if (previewBtn) {
+        previewBtn.addEventListener('click', (e) => {
+          e.stopPropagation();
+          showImagePreviewModal(item.imageUrl, item.id || 'ai-image');
+        });
+      }
+      
+      // 点击下载按钮 - 直接下载PNG
+      const downloadBtn = div.querySelector('.download-btn');
+      if (downloadBtn) {
+        downloadBtn.addEventListener('click', (e) => {
+          e.stopPropagation();
+          downloadImage(item.imageUrl, item.id || 'ai-image');
+        });
+      }
+    } else if (item.status === 'failed') {
+      div.classList.add('failed');
+      
+      // 如果已经重试过,显示"已重试"标记
+      const retryButtonHtml = item.retried 
+        ? `<div class="retried-badge">已重试</div>`
+        : `<button class="retry-btn" data-task-id="${item.id}">
+            <svg width="14" height="14" viewBox="0 0 16 16" fill="none">
+              <path d="M2 8C2 4.686 4.686 2 8 2C10.5 2 12.5 3.5 13.5 5.5M14 8C14 11.314 11.314 14 8 14C5.5 14 3.5 12.5 2.5 10.5M14 2V6H10M2 14V10H6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+            </svg>
+            重新生图
+          </button>`;
+      
+      div.innerHTML = `
+        ${previewUrl ? `<img class="history-item-preview failed-preview" src="${previewUrl}" alt="预览图" onerror="this.style.display='none'">` : ''}
+        <div class="failed-content">
+          <div class="failed-icon">❌</div>
+          <div class="failed-text">生成失败</div>
+          ${retryButtonHtml}
+        </div>
+        <div class="history-item-time">${timeText}</div>
+      `;
+      
+      // 绑定重试按钮事件
+      const retryBtn = div.querySelector('.retry-btn');
+      if (retryBtn) {
+        retryBtn.addEventListener('click', async (e) => {
+          e.stopPropagation();
+          await retryAIGeneration(item.id);
+        });
+      }
+    }
+    
+    return div;
+  }
+
+  // 重新生图(免费)
+  async function retryAIGeneration(taskId) {
+    const username = getCurrentUsername();
+    if (!username) {
+      alert('请先登录');
+      return;
+    }
+    
+    try {
+      const response = await fetch('/api/ai/retry', {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json'
+        },
+        body: JSON.stringify({
+          taskId: taskId,
+          username: username
+        })
+      });
+      
+      const result = await response.json();
+      
+      if (result.success) {
+        // 刷新历史列表
+        loadAIHistory();
+      } else {
+        alert('重试失败: ' + (result.error || '未知错误'));
+      }
+    } catch (error) {
+      console.error('[Profile] 重试失败:', error);
+      alert('重试失败: ' + error.message);
+    }
+  }
+
+  // 下载图片(直接下载PNG)
+  function downloadImage(url, filename) {
+    const a = document.createElement('a');
+    a.href = url;
+    a.download = `${filename}.png`;
+    document.body.appendChild(a);
+    a.click();
+    document.body.removeChild(a);
+  }
+
+  // 显示图片预览弹窗
+  function showImagePreviewModal(imageUrl, filename) {
+    const modal = document.createElement('div');
+    modal.className = 'image-preview-modal';
+    modal.innerHTML = `
+      <div class="image-preview-backdrop"></div>
+      <div class="image-preview-content">
+        <img src="${imageUrl}" alt="预览图">
+        <button class="image-preview-close">×</button>
+        <button class="image-preview-download-btn" title="下载">
+          <svg width="24" height="24" viewBox="0 0 16 16" fill="none">
+            <path d="M8 11V3M8 11L5 8M8 11L11 8M3 13H13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+          </svg>
+        </button>
+      </div>
+    `;
+    
+    document.body.appendChild(modal);
+    
+    requestAnimationFrame(() => {
+      modal.classList.add('show');
+    });
+    
+    const closeModal = () => {
+      modal.classList.remove('show');
+      setTimeout(() => {
+        if (modal.parentNode) modal.parentNode.removeChild(modal);
+      }, 300);
+    };
+    
+    modal.querySelector('.image-preview-backdrop').onclick = closeModal;
+    modal.querySelector('.image-preview-close').onclick = closeModal;
+    modal.querySelector('.image-preview-download-btn').onclick = (e) => {
+      e.stopPropagation();
+      downloadImage(imageUrl, filename);
+    };
+    
+    document.addEventListener('keydown', function escHandler(e) {
+      if (e.key === 'Escape') {
+        closeModal();
+        document.removeEventListener('keydown', escHandler);
+      }
+    });
+  }
+
+  // 加载购买记录
+  async function loadPurchaseHistory() {
+    const username = getCurrentUsername();
+    if (!username) return;
+
+    const purchaseLoading = document.getElementById('purchaseLoading');
+    const purchaseEmpty = document.getElementById('purchaseEmpty');
+    const purchaseGrid = document.getElementById('purchaseGrid');
+
+    if (purchaseLoading) {
+      purchaseLoading.style.display = 'block';
+    }
+    if (purchaseEmpty) {
+      purchaseEmpty.style.display = 'none';
+    }
+    if (purchaseGrid) {
+      purchaseGrid.innerHTML = '';
+    }
+
+    try {
+      const response = await fetch(`/api/pay/purchase-history?username=${encodeURIComponent(username)}`);
+      if (!response.ok) {
+        throw new Error(`HTTP error! status: ${response.status}`);
+      }
+      const result = await response.json();
+      
+      if (purchaseLoading) {
+        purchaseLoading.style.display = 'none';
+      }
+
+      if (result.success && result.history && result.history.length > 0) {
+        if (purchaseGrid) {
+          result.history.forEach(item => {
+            const purchaseItem = createPurchaseItem(item);
+            purchaseGrid.appendChild(purchaseItem);
+          });
+        }
+      } else {
+        if (purchaseEmpty) {
+          purchaseEmpty.style.display = 'block';
+        }
+      }
+    } catch (error) {
+      console.error('[Profile] 加载购买记录失败:', error);
+      if (purchaseLoading) {
+        purchaseLoading.style.display = 'none';
+      }
+      if (purchaseEmpty) {
+        purchaseEmpty.style.display = 'block';
+      }
+    }
+  }
+
+  // 创建购买记录项
+  function createPurchaseItem(item) {
+    const div = document.createElement('div');
+    div.className = 'purchase-item';
+    
+    if (item.deleted) {
+      div.classList.add('deleted');
+    }
+    
+    const previewHtml = item.previewUrl 
+      ? `<img class="purchase-item-image" src="${item.previewUrl}" alt="${item.name}" onerror="this.src='../../static/default-avatar.png'">`
+      : `<div class="purchase-item-placeholder">
+           <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+             <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"/>
+           </svg>
+         </div>`;
+    
+    const deletedBadge = item.deleted ? '<div class="purchase-item-deleted-badge">已删除</div>' : '';
+    const priceText = item.points > 0 ? `<div class="purchase-item-price">${item.points} Ani币</div>` : '';
+    
+    // 格式化时间(购买记录暂时没有时间,可以添加)
+    const formatTime = (dateString) => {
+      if (!dateString) return '';
+      const date = new Date(dateString);
+      const now = new Date();
+      const diff = now - date;
+      const minutes = Math.floor(diff / 60000);
+      const hours = Math.floor(diff / 3600000);
+      const days = Math.floor(diff / 86400000);
+      
+      if (minutes < 1) return '刚刚';
+      if (minutes < 60) return `${minutes}分钟前`;
+      if (hours < 24) return `${hours}小时前`;
+      if (days < 7) return `${days}天前`;
+      
+      return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
+    };
+    
+    const timeText = item.purchasedAt ? formatTime(item.purchasedAt) : '';
+    
+    div.innerHTML = `
+      ${previewHtml}
+      <div class="purchase-item-info">
+        <div class="purchase-item-name">${item.name}</div>
+        <div class="purchase-item-category">${item.category}</div>
+        ${priceText}
+      </div>
+      <div class="purchase-item-overlay">
+        <button class="purchase-item-add-btn" data-path="${item.path}" data-category="${item.category}" data-name="${item.name}">
+          <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2">
+            <path d="M8 3V13M3 8H13" stroke-linecap="round"/>
+          </svg>
+          添加
+        </button>
+      </div>
+      ${timeText ? `<div class="purchase-item-time">${timeText}</div>` : ''}
+      ${deletedBadge}
+    `;
+    
+    // 绑定添加按钮事件
+    const addBtn = div.querySelector('.purchase-item-add-btn');
+    if (addBtn) {
+      addBtn.addEventListener('click', async (e) => {
+        e.stopPropagation();
+        const resourcePath = addBtn.dataset.path;
+        const categoryDir = addBtn.dataset.category;
+        const itemName = addBtn.dataset.name;
+        
+        await handleAddToDisk(resourcePath, categoryDir, itemName, addBtn);
+      });
+    }
+    
+    return div;
+  }
+
+  // 处理添加到网盘
+  async function handleAddToDisk(resourcePath, categoryDir, itemName, buttonEl) {
+    const username = getCurrentUsername();
+    if (!username) {
+      if (window.parent && window.parent.HintView) {
+        window.parent.HintView.error('请先登录', 2000);
+      }
+      return;
+    }
+
+    // 禁用按钮
+    if (buttonEl) {
+      buttonEl.disabled = true;
+      buttonEl.textContent = '添加中...';
+    }
+
+    try {
+      const response = await fetch('/api/pay/purchase', {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json'
+        },
+        body: JSON.stringify({
+          username: username,
+          resourcePath: resourcePath,
+          categoryDir: categoryDir,
+          itemName: itemName,
+          points: 0 // 已购买,不需要扣除点数
+        })
+      });
+
+      const result = await response.json();
+      
+      if (result.success) {
+        if (window.parent && window.parent.HintView) {
+          window.parent.HintView.success('添加成功!文件已添加到网盘', 2000);
+        }
+        
+        // 通知父窗口刷新网盘
+        if (window.parent && window.parent.postMessage) {
+          window.parent.postMessage({ type: 'refresh-disk' }, '*');
+        }
+      } else {
+        throw new Error(result.message || '添加失败');
+      }
+    } catch (error) {
+      console.error('[Profile] 添加到网盘失败:', error);
+      if (window.parent && window.parent.HintView) {
+        window.parent.HintView.error(error.message || '添加失败,请稍后重试', 2000);
+      }
+    } finally {
+      // 恢复按钮状态
+      if (buttonEl) {
+        buttonEl.disabled = false;
+        buttonEl.innerHTML = `
+          <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2">
+            <path d="M8 3V13M3 8H13" stroke-linecap="round"/>
+          </svg>
+          添加
+        `;
+      }
+    }
+  }
+
+  // 获取当前登录用户名
+  function getCurrentUsername() {
+    try {
+      const loginDataStr = localStorage.getItem('loginData');
+      if (!loginDataStr) {
+        return null;
+      }
+      
+      const loginData = JSON.parse(loginDataStr);
+      const now = Date.now();
+      
+      if (now >= loginData.expireTime) {
+        localStorage.removeItem('loginData');
+        return null;
+      }
+      
+      return loginData.user ? loginData.user.username : null;
+    } catch (error) {
+      console.error('[Profile] 获取用户名失败:', error);
+      return null;
+    }
+  }
+
+  // 初始化
+  if (document.readyState === 'loading') {
+    document.addEventListener('DOMContentLoaded', init);
+  } else {
+    init();
+  }
+})();
+

+ 249 - 0
client/js/recharge-view/recharge-view.js

@@ -0,0 +1,249 @@
+// 充值界面逻辑
+
+(function() {
+  let overlay = null;
+  let container = null;
+  let closeBtn = null;
+  let packages = null;
+  let rechargeInfo = null;
+  let selectedPointsEl = null;
+  let selectedPriceEl = null;
+  
+  let currentPackage = null;
+  let currentUsername = null;
+
+  function init() {
+    overlay = document.getElementById('rechargeOverlay');
+    container = document.getElementById('rechargeContainer');
+    closeBtn = document.getElementById('rechargeClose');
+    packages = document.querySelectorAll('.recharge-package');
+    rechargeInfo = document.getElementById('rechargeInfo');
+    selectedPointsEl = document.getElementById('selectedPoints');
+    selectedPriceEl = document.getElementById('selectedPrice');
+
+    if (!overlay || !container) {
+      console.error('[RechargeView] 充值界面元素未找到');
+      return;
+    }
+
+    bindEvents();
+  }
+
+  function bindEvents() {
+    // 关闭按钮 - 只关闭界面,不触发充值
+    if (closeBtn) {
+      closeBtn.addEventListener('click', () => {
+        hide();
+        // 通知父窗口关闭充值界面
+        if (window.parent && window.parent !== window) {
+          window.parent.postMessage({
+            type: 'close-recharge-view'
+          }, '*');
+        }
+      });
+    }
+
+    // 点击遮罩层关闭 - 只关闭界面,不触发充值
+    if (overlay) {
+      overlay.addEventListener('click', (e) => {
+        if (e.target === overlay) {
+          hide();
+          // 通知父窗口关闭充值界面
+          if (window.parent && window.parent !== window) {
+            window.parent.postMessage({
+              type: 'close-recharge-view'
+            }, '*');
+          }
+        }
+      });
+    }
+
+    // ESC 键关闭 - 只关闭界面,不触发充值
+    document.addEventListener('keydown', (e) => {
+      if (e.key === 'Escape' && overlay && overlay.classList.contains('show')) {
+        hide();
+        // 通知父窗口关闭充值界面
+        if (window.parent && window.parent !== window) {
+          window.parent.postMessage({
+            type: 'close-recharge-view'
+          }, '*');
+        }
+      }
+    });
+
+    // 测试购买按钮
+    const testBuyBtn = document.getElementById('testBuyBtn');
+    if (testBuyBtn) {
+      testBuyBtn.addEventListener('click', handleRechargeSuccess);
+    }
+
+    // 套餐选择
+    if (packages) {
+      packages.forEach(pkg => {
+        pkg.addEventListener('click', () => {
+          selectPackage(pkg);
+        });
+      });
+    }
+
+    // 监听来自父窗口的消息
+    window.addEventListener('message', (event) => {
+      if (event.data && event.data.type === 'open-recharge-view') {
+        show(event.data.needPoints, event.data.currentPoints);
+      }
+    });
+  }
+
+  function selectPackage(pkg) {
+    // 移除所有选中状态
+    packages.forEach(p => p.classList.remove('selected'));
+    
+    // 添加选中状态
+    pkg.classList.add('selected');
+    currentPackage = {
+      points: parseInt(pkg.dataset.points),
+      price: parseFloat(pkg.dataset.price),
+      package: pkg.dataset.package
+    };
+
+    // 显示充值信息
+    if (rechargeInfo) {
+      rechargeInfo.style.display = 'block';
+    }
+    
+    if (selectedPointsEl) {
+      selectedPointsEl.textContent = currentPackage.points + ' Ani币';
+    }
+    
+    if (selectedPriceEl) {
+      selectedPriceEl.textContent = '¥' + currentPackage.price;
+    }
+  }
+
+  function show(needPoints, currentPoints) {
+    if (!overlay) {
+      init();
+    }
+
+    currentUsername = getCurrentUsername();
+    if (!currentUsername) {
+      if (window.parent && window.parent.HintView) {
+        window.parent.HintView.error('请先登录', 2000);
+      }
+      return;
+    }
+
+    // 重置状态
+    if (packages) {
+      packages.forEach(p => p.classList.remove('selected'));
+    }
+    if (rechargeInfo) {
+      rechargeInfo.style.display = 'none';
+    }
+    currentPackage = null;
+
+    // 显示充值界面
+    overlay.classList.add('show');
+    overlay.style.pointerEvents = 'auto';
+    
+    // 默认选中第一个套餐
+    if (packages && packages.length > 0) {
+      selectPackage(packages[0]);
+    }
+  }
+
+  function hide() {
+    if (overlay) {
+      overlay.classList.remove('show');
+      overlay.style.pointerEvents = 'none';
+    }
+    currentPackage = null;
+    currentUsername = null;
+  }
+
+  async function handleRechargeSuccess() {
+    if (!currentPackage || !currentUsername) {
+      hide();
+      return;
+    }
+
+    try {
+      // 调用充值API
+      const response = await fetch('/api/recharge', {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json'
+        },
+        body: JSON.stringify({
+          username: currentUsername,
+          points: currentPackage.points,
+          price: currentPackage.price,
+          package: currentPackage.package
+        })
+      });
+
+      const result = await response.json();
+
+      if (result.success) {
+        // 充值成功,通知父窗口
+        if (window.parent && window.parent !== window) {
+          window.parent.postMessage({
+            type: 'recharge-success',
+            points: currentPackage.points
+          }, '*');
+        }
+
+        // 显示成功提示
+        if (window.parent && window.parent.HintView) {
+          window.parent.HintView.success(`充值成功!获得 ${currentPackage.points} Ani币`, 3000);
+        }
+
+        hide();
+      } else {
+        // 充值失败
+        if (window.parent && window.parent.HintView) {
+          window.parent.HintView.error(result.message || '充值失败', 3000);
+        }
+        hide();
+      }
+    } catch (error) {
+      console.error('[RechargeView] 充值处理失败:', error);
+      if (window.parent && window.parent.HintView) {
+        window.parent.HintView.error('充值失败,请稍后重试', 3000);
+      }
+      hide();
+    }
+  }
+
+  // 获取当前登录用户名
+  function getCurrentUsername() {
+    try {
+      const loginDataStr = localStorage.getItem('loginData');
+      if (!loginDataStr) {
+        return null;
+      }
+      
+      const loginData = JSON.parse(loginDataStr);
+      const now = Date.now();
+      
+      // 检查是否过期
+      if (now >= loginData.expireTime) {
+        localStorage.removeItem('loginData');
+        return null;
+      }
+      
+      return loginData.user ? loginData.user.username : null;
+    } catch (error) {
+      console.error('[RechargeView] 获取用户名失败:', error);
+      return null;
+    }
+  }
+
+  // 初始化
+  if (document.readyState === 'loading') {
+    document.addEventListener('DOMContentLoaded', init);
+  } else {
+    init();
+  }
+})();
+

+ 94 - 9
client/js/seq_ani_player/card.js

@@ -154,6 +154,8 @@
       this.infoBar = this.container.querySelector('.preview-info-bar');
       this.folderNameElement = this.container.querySelector('.folder-name');
       this.btnExport = this.container.querySelector('.btn-export');
+      this.btnDownload = this.container.querySelector('.btn-download');
+      this.btnAI = this.container.querySelector('.btn-ai');
     }
     
     bindEvents() {
@@ -179,10 +181,20 @@
         this.dropZone.addEventListener('drop', (e) => this.handleDrop(e));
       }
       
-      // 导出按钮
+      // 导出按钮(旧版兼容)
       if (this.btnExport) {
         this.btnExport.addEventListener('click', () => this.handleExport());
       }
+      
+      // 下载按钮
+      if (this.btnDownload) {
+        this.btnDownload.addEventListener('click', () => this.handleExport());
+      }
+      
+      // AI生图按钮
+      if (this.btnAI) {
+        this.btnAI.addEventListener('click', () => this.handleAIGenerate());
+      }
     }
     
     handleDragEnter(event) {
@@ -515,17 +527,68 @@
       this.openExportView();
     }
     
+    /**
+     * 处理AI生图按钮点击
+     */
+    async handleAIGenerate() {
+      if (!this.currentFolderName || !this.frameList.length) {
+        this.showGlobalAlert('没有可用于AI生图的动画');
+        return;
+      }
+      
+      // 打开AI生图界面
+      this.openAIGenerateView();
+    }
+    
+    /**
+     * 打开AI生图界面
+     */
+    openAIGenerateView() {
+      // 先生成预览图数据,然后打开AI生图界面
+      this.generatePreviewImage().then(result => {
+        // 向所有父级窗口发送消息(处理多层iframe情况)
+        let targetWindow = window.parent;
+        while (targetWindow && targetWindow !== window) {
+          targetWindow.postMessage({
+            type: 'open-ai-generate-view',
+            folderName: this.currentFolderName,
+            spritesheetData: result.imageUrl,
+            spritesheetLayout: result.layout
+          }, '*');
+          
+          // 尝试向更上层发送
+          if (targetWindow.parent && targetWindow.parent !== targetWindow) {
+            targetWindow = targetWindow.parent;
+          } else {
+            break;
+          }
+        }
+      }).catch(error => {
+        console.error('[PreviewCard] 生成预览图失败:', error);
+        this.showGlobalAlert('生成预览图失败:' + error.message);
+      });
+    }
+    
     /**
      * 生成预览图
      * @returns {Promise<string>} 预览图的 base64 URL
      */
     async generatePreviewImage() {
-      const TEXTURE_ROOT = "http://localhost:3000/disk_data";
       const folderName = this.currentFolderName;
       
+      // 获取用户名
+      const username = this.getCurrentUsername();
+      if (!username) {
+        throw new Error('请先登录');
+      }
+      
       // 获取帧列表(从服务端获取,服务端会判断是否有图片)
       const encodedFolderName = encodeURIComponent(folderName);
-      const response = await fetch(`http://localhost:3000/api/frames/${encodedFolderName}`);
+      let apiUrl = `http://localhost:3000/api/frames/${encodedFolderName}`;
+      if (username) {
+        apiUrl += `?username=${encodeURIComponent(username)}`;
+      }
+      const response = await fetch(apiUrl);
       
       if (!response.ok) {
         // 服务端返回错误,解析错误信息
@@ -541,14 +604,14 @@
         throw new Error('该文件夹中没有图片');
       }
       
-      // 加载所有图片
+      // 加载所有图片(使用正确的API路径)
       const images = [];
       for (let i = 0; i < frameNumbers.length; i++) {
         const frameNum = frameNumbers[i];
         const frameName = frameNum.toString().padStart(2, '0');
-        const pathSegments = folderName.split('/').map(seg => encodeURIComponent(seg));
-        const encodedPath = pathSegments.join('/');
-        const imgSrc = `${TEXTURE_ROOT}/${encodedPath}/${frameName}.png`;
+        // 使用API路径,从用户目录加载
+        const imagePath = `${folderName}/${frameName}.png`;
+        const imgSrc = `/api/disk/preview?username=${encodeURIComponent(username)}&path=${encodeURIComponent(imagePath)}`;
         
         const img = await new Promise((resolve, reject) => {
           const image = new Image();
@@ -581,6 +644,9 @@
       // 填充透明背景
       ctx.clearRect(0, 0, canvas.width, canvas.height);
       
+      // 保存布局信息
+      const layout = [];
+      
       // 绘制所有图片
       images.forEach((item, index) => {
         const col = index % cols;
@@ -588,13 +654,32 @@
         const x = col * frameWidth;
         const y = row * frameHeight;
         ctx.drawImage(item.img, x, y);
+        
+        // 保存布局信息
+        layout.push({
+          x: x,
+          y: y,
+          width: item.width,
+          height: item.height,
+          frameNum: item.frameNum
+        });
       });
       
       // 转换为 base64
       return new Promise((resolve) => {
         canvas.toBlob((blob) => {
-          const url = URL.createObjectURL(blob);
-          resolve(url);
+          const reader = new FileReader();
+          reader.onload = () => {
+            resolve({
+              imageUrl: reader.result,
+              layout: {
+                layout: layout,
+                sheetWidth: canvas.width,
+                sheetHeight: canvas.height
+              }
+            });
+          };
+          reader.readAsDataURL(blob);
         }, 'image/png');
       });
     }

+ 37 - 2
client/js/seq_ani_player/seq-ani-player.js

@@ -14,9 +14,37 @@
     console.warn("[SeqAniPlayer WARN]", ...args);
   }
 
-  const TEXTURE_ROOT = "http://localhost:3000/disk_data";
   const foldersApi = "http://localhost:3000/api/folders";
 
+  // 获取当前登录用户名
+  let currentUsername = null;
+  function getCurrentUsername() {
+    if (currentUsername) {
+      return currentUsername;
+    }
+    try {
+      const loginDataStr = localStorage.getItem('loginData');
+      if (!loginDataStr) {
+        return null;
+      }
+      
+      const loginData = JSON.parse(loginDataStr);
+      const now = Date.now();
+      
+      // 检查是否过期
+      if (now >= loginData.expireTime) {
+        localStorage.removeItem('loginData');
+        return null;
+      }
+      
+      currentUsername = loginData.user ? loginData.user.username : null;
+      return currentUsername;
+    } catch (error) {
+      console.error('[SeqAniPlayer] 获取用户名失败:', error);
+      return null;
+    }
+  }
+
   const fpsSlider = document.getElementById("fpsSlider");
   const fpsValue = document.getElementById("fpsValue");
   const cardsGrid = document.getElementById("cardsGrid");
@@ -53,7 +81,14 @@
 
   function buildFrameSrc(folder, index) {
     const frameName = padFrame(index);
-    return `${TEXTURE_ROOT}/${folder}/${frameName}.png`;
+    const username = getCurrentUsername();
+    if (!username) {
+      logWarn('未登录,无法加载图片');
+      return '';
+    }
+    // 使用API路径,从用户目录加载
+    const imagePath = `${folder}/${frameName}.png`;
+    return `/api/disk/preview?username=${encodeURIComponent(username)}&path=${encodeURIComponent(imagePath)}`;
   }
 
   function buildFolderName(prefix, index, padding) {

+ 32 - 5
client/js/sprite_sheet_maker/sprite-sheet-maker.js

@@ -2,7 +2,29 @@
 // 实现类似 Texture Packer 的功能:将多张图片拼接成一张图,并生成 JSON 文件
 
 (function () {
-  const TEXTURE_ROOT = "http://localhost:3000/disk_data";
+  // 获取当前登录用户名
+  function getCurrentUsername() {
+    try {
+      const loginDataStr = localStorage.getItem('loginData');
+      if (!loginDataStr) {
+        return null;
+      }
+      
+      const loginData = JSON.parse(loginDataStr);
+      const now = Date.now();
+      
+      // 检查是否过期
+      if (now >= loginData.expireTime) {
+        localStorage.removeItem('loginData');
+        return null;
+      }
+      
+      return loginData.user ? loginData.user.username : null;
+    } catch (error) {
+      console.error('[SpriteSheet] 获取用户名失败:', error);
+      return null;
+    }
+  }
 
   // 显示提示信息(使用全局 Alert 组件,直接调用)
   function showAlert(message, duration = 2000) {
@@ -321,13 +343,18 @@
       loading.updateStatus(`加载图片中... (0/${frameNumbers.length})`);
       const images = [];
       
+      // 获取用户名
+      const username = getCurrentUsername();
+      if (!username) {
+        throw new Error('请先登录');
+      }
+      
       for (let i = 0; i < frameNumbers.length; i++) {
         const frameNum = frameNumbers[i];
         const frameName = frameNum.toString().padStart(2, '0');
-        // 对路径的每一段进行编码
-        const pathSegments = folderName.split('/').map(seg => encodeURIComponent(seg));
-        const encodedPath = pathSegments.join('/');
-        const imgSrc = `${TEXTURE_ROOT}/${encodedPath}/${frameName}.png`;
+        // 使用API路径,从用户目录加载
+        const imagePath = `${folderName}/${frameName}.png`;
+        const imgSrc = `/api/disk/preview?username=${encodeURIComponent(username)}&path=${encodeURIComponent(imagePath)}`;
         
         loading.updateStatus(`加载图片中... (${i + 1}/${frameNumbers.length})`);
         const img = await loadImage(imgSrc);

+ 12 - 0
client/js/store/item.js

@@ -0,0 +1,12 @@
+// 商店资源项逻辑
+// 这个文件目前主要用于占位,主要逻辑在 store.js 中通过事件委托处理
+
+(function () {
+  'use strict';
+
+  // 如果需要,可以在这里添加资源项特定的逻辑
+  // 目前所有逻辑都在 store.js 中通过事件委托处理
+
+  console.log('[StoreItem] 资源项模块已加载');
+})();
+

+ 1001 - 0
client/js/store/store.js

@@ -0,0 +1,1001 @@
+// 商店页面主逻辑
+// 负责资源加载、搜索、分类筛选等功能
+
+(function () {
+  'use strict';
+
+  class StoreView {
+    constructor() {
+      this.resources = [];
+      this.filteredResources = [];
+      this.currentCategory = '';
+      this.searchQuery = '';
+      this.itemTemplate = null;
+      this.currentFps = 8;
+      this.frameUrls = [];
+      this.currentFrame = 0;
+      this.itemAnimations = new Map(); // 存储每个 item 的动画数据
+      this.isUserLoggedIn = false; // 本地登录状态缓存
+      
+      this.init();
+    }
+
+    async init() {
+      // 加载 item 模板
+      await this.loadItemTemplate();
+      
+      // 绑定事件
+      this.bindEvents();
+      
+      // 加载分类
+      await this.loadCategories();
+      
+      // 加载资源
+      await this.loadResources();
+    }
+
+    async loadItemTemplate() {
+      try {
+        // 使用相对于当前页面的路径
+        // store.html 位于 page/store/store.html,item.html 位于同一目录
+        const response = await fetch('./item.html');
+        if (!response.ok) {
+          throw new Error(`HTTP error! status: ${response.status}`);
+        }
+        const html = await response.text();
+        this.itemTemplate = html;
+      } catch (error) {
+        console.error('[StoreView] 加载模板失败:', error);
+        // 如果相对路径失败,尝试绝对路径
+        try {
+          const response = await fetch('/page/store/item.html');
+          if (response.ok) {
+            const html = await response.text();
+            this.itemTemplate = html;
+          } else {
+            throw new Error('绝对路径也失败');
+          }
+        } catch (fallbackError) {
+          console.error('[StoreView] 备用路径也失败:', fallbackError);
+          this.itemTemplate = '<div class="store-item">加载失败</div>';
+        }
+      }
+    }
+
+    async loadCategories() {
+      try {
+        const response = await fetch('http://localhost:3000/api/store/categories');
+        if (!response.ok) {
+          throw new Error(`HTTP error! status: ${response.status}`);
+        }
+        
+        const data = await response.json();
+        
+        if (data.success && data.categories) {
+          const categoryBar = document.getElementById('categoryBar');
+          if (categoryBar) {
+            // 保留"全部"按钮,添加其他分类
+            const allButton = categoryBar.querySelector('.category-item[data-category=""]');
+            categoryBar.innerHTML = '';
+            if (allButton) {
+              categoryBar.appendChild(allButton);
+            }
+            
+            // 添加动态分类按钮
+            data.categories.forEach(category => {
+              const button = document.createElement('button');
+              button.className = 'category-item';
+              button.dataset.category = category.name; // 使用文件夹名称作为分类名称
+              button.textContent = category.name;
+              categoryBar.appendChild(button);
+            });
+          }
+        }
+      } catch (error) {
+        console.error('[StoreView] 加载分类失败:', error);
+        // 如果加载失败,保持默认的"全部"按钮
+      }
+    }
+
+    bindEvents() {
+      // 监听登录状态变化
+      window.addEventListener('message', (event) => {
+        if (event.data && event.data.type === 'login-success' && event.data.user) {
+          this.isUserLoggedIn = true;
+        } else if (event.data && event.data.type === 'logout') {
+          this.isUserLoggedIn = false;
+        }
+      });
+      
+      // 搜索栏
+      const searchInput = document.getElementById('searchInput');
+      const searchButton = document.getElementById('searchButton');
+      
+      if (searchInput) {
+        searchInput.addEventListener('input', (e) => {
+          this.searchQuery = e.target.value.trim();
+          this.filterResources();
+        });
+        
+        searchInput.addEventListener('keypress', (e) => {
+          if (e.key === 'Enter') {
+            this.filterResources();
+          }
+        });
+      }
+      
+      if (searchButton) {
+        searchButton.addEventListener('click', () => {
+          this.filterResources();
+        });
+      }
+      
+      // 分类栏
+      const categoryItems = document.querySelectorAll('.category-item');
+      categoryItems.forEach(item => {
+        item.addEventListener('click', () => {
+          // 移除所有 active 类
+          categoryItems.forEach(i => i.classList.remove('active'));
+          // 添加 active 类到当前项
+          item.classList.add('active');
+          // 更新当前分类
+          this.currentCategory = item.dataset.category || '';
+          // 重新加载资源
+          this.loadResources();
+        });
+      });
+      
+      // 预览弹窗关闭
+      const previewClose = document.getElementById('previewClose');
+      const previewModal = document.getElementById('previewModal');
+      
+      if (previewClose) {
+        previewClose.addEventListener('click', () => {
+          this.stopAnimation();
+          if (previewModal) {
+            previewModal.style.display = 'none';
+          }
+        });
+      }
+      
+      if (previewModal) {
+        previewModal.addEventListener('click', (e) => {
+          if (e.target === previewModal) {
+            this.stopAnimation();
+            previewModal.style.display = 'none';
+          }
+        });
+      }
+      
+      // 帧率滑块
+      const fpsSlider = document.getElementById('fpsSlider');
+      const fpsDisplay = document.getElementById('fpsDisplay');
+      
+      if (fpsSlider && fpsDisplay) {
+        fpsSlider.addEventListener('input', (e) => {
+          const fps = parseInt(e.target.value);
+          this.currentFps = fps;
+          fpsDisplay.textContent = `${fps} FPS`;
+          // 如果动画正在播放,重新启动以应用新帧率
+          if (this.animationInterval && this.frameUrls.length > 0) {
+            const previewImage = document.getElementById('previewAnimationImage');
+            if (previewImage) {
+              this.stopAnimation();
+              this.startAnimation(previewImage, this.frameUrls);
+            }
+          }
+        });
+      }
+      
+      // 使用事件委托处理动态添加的按钮
+      const resourcesGrid = document.getElementById('resourcesGrid');
+      if (resourcesGrid) {
+        // 购买按钮点击
+        resourcesGrid.addEventListener('click', (e) => {
+          console.log('[StoreView] 点击事件触发,目标:', e.target);
+          if (e.target.closest('.item-buy-button')) {
+            const button = e.target.closest('.item-buy-button');
+            const path = button.dataset.resourcePath;
+            console.log('[StoreView] 点击购买按钮,路径:', path);
+            
+            // 检查是否已登录
+            if (!this.isLoggedIn()) {
+              console.log('[StoreView] 未登录,跳转登录页');
+              // 显示登录界面
+              if (window.parent !== window) {
+                window.parent.postMessage({
+                  type: 'navigation',
+                  page: 'login'
+                }, '*');
+              }
+              return;
+            }
+            
+            console.log('[StoreView] 已登录,调用 handleBuy');
+            this.handleBuy(path);
+          }
+        });
+      }
+    }
+
+    async loadResources() {
+      this.showLoading(true);
+      this.hideEmpty();
+      
+      try {
+        const params = new URLSearchParams();
+        if (this.currentCategory) {
+          params.append('category', this.currentCategory);
+        }
+        if (this.searchQuery) {
+          params.append('search', this.searchQuery);
+        }
+        
+        const response = await fetch(`http://localhost:3000/api/store/resources?${params.toString()}`);
+        
+        if (!response.ok) {
+          throw new Error(`HTTP error! status: ${response.status}`);
+        }
+        
+        const data = await response.json();
+        
+        if (data.success) {
+          this.resources = data.resources || [];
+          // 使用服务器返回的价格,如果没有则默认为0
+          this.resources.forEach(resource => {
+            if (resource.points === undefined || resource.points === null) {
+              resource.points = 0;
+            }
+          });
+          this.filteredResources = this.resources;
+          this.renderResources();
+        } else {
+          throw new Error(data.error || '加载资源失败');
+        }
+      } catch (error) {
+        console.error('[StoreView] 加载资源失败:', error);
+        this.showGlobalAlert('加载资源失败: ' + error.message);
+        this.resources = [];
+        this.filteredResources = [];
+        this.renderResources();
+      } finally {
+        this.showLoading(false);
+      }
+    }
+
+    filterResources() {
+      // 重新加载资源(服务器端筛选)
+      this.loadResources();
+    }
+
+    renderResources() {
+      const grid = document.getElementById('resourcesGrid');
+      if (!grid) return;
+      
+      if (this.filteredResources.length === 0) {
+        grid.innerHTML = '';
+        this.showEmpty();
+        return;
+      }
+      
+      this.hideEmpty();
+      
+      // 计算列数
+      const columnWidth = 200;
+      const gap = 16;
+      const containerWidth = grid.offsetWidth || grid.clientWidth || window.innerWidth;
+      const columnCount = Math.max(1, Math.floor((containerWidth + gap) / (columnWidth + gap)));
+      
+      // 创建列容器
+      grid.innerHTML = '';
+      const masonryContainer = document.createElement('div');
+      masonryContainer.className = 'resources-grid-masonry';
+      
+      const columns = [];
+      const columnHeights = [];
+      
+      for (let i = 0; i < columnCount; i++) {
+        const column = document.createElement('div');
+        column.className = 'masonry-column';
+        masonryContainer.appendChild(column);
+        columns.push(column);
+        columnHeights.push(0);
+      }
+      
+      grid.appendChild(masonryContainer);
+      
+      // 创建临时容器来测量项目高度
+      const tempContainer = document.createElement('div');
+      tempContainer.style.position = 'absolute';
+      tempContainer.style.visibility = 'hidden';
+      tempContainer.style.width = `${columnWidth}px`;
+      tempContainer.style.top = '-9999px';
+      tempContainer.style.left = '-9999px';
+      document.body.appendChild(tempContainer);
+      
+      // 渲染所有项目到临时容器并测量
+      const itemsData = this.filteredResources.map(resource => {
+        const itemHtml = this.renderItem(resource);
+        tempContainer.innerHTML = itemHtml;
+        const itemElement = tempContainer.querySelector('.store-item');
+        const height = itemElement ? itemElement.offsetHeight : 0;
+        return { html: itemHtml, height };
+      });
+      
+      // 将项目分配到最短的列
+      itemsData.forEach(({ html, height }) => {
+        // 找到最短的列
+        const shortestColumnIndex = columnHeights.indexOf(Math.min(...columnHeights));
+        
+        // 添加到最短的列
+        columns[shortestColumnIndex].insertAdjacentHTML('beforeend', html);
+        
+        // 更新列高度
+        columnHeights[shortestColumnIndex] += height + gap;
+      });
+      
+      // 清理临时容器
+      document.body.removeChild(tempContainer);
+      
+      // 为每个 item 绑定鼠标事件和 FPS 控制
+      const allItems = masonryContainer.querySelectorAll('.store-item');
+      allItems.forEach(item => {
+        const category = item.dataset.category;
+        const folder = item.dataset.folder;
+        const previewImage = item.querySelector('.item-preview-image');
+        const fpsSlider = item.querySelector('.item-fps-slider');
+        const fpsDisplay = item.querySelector('.item-fps-display');
+        
+        if (category && folder && previewImage) {
+          // 存储当前 FPS(每个 item 独立)
+          let currentFps = 8;
+          
+          // FPS 滑块事件
+          if (fpsSlider && fpsDisplay) {
+            fpsSlider.addEventListener('input', (e) => {
+              const fps = parseInt(e.target.value);
+              currentFps = fps;
+              fpsDisplay.textContent = `${fps} FPS`;
+              
+              // 如果动画正在播放,重新启动以应用新帧率
+              if (previewImage._animationInterval) {
+                this.stopItemAnimation(previewImage);
+                this.startItemAnimation(previewImage, category, folder, fps);
+              }
+            });
+            
+            // 阻止事件冒泡,避免触发 item 的 mouseleave
+            fpsSlider.addEventListener('mousedown', (e) => {
+              e.stopPropagation();
+            });
+          }
+          
+          // 鼠标进入时播放动画
+          item.addEventListener('mouseenter', () => {
+            this.startItemAnimation(previewImage, category, folder, currentFps);
+          });
+          
+          // 鼠标离开时停止动画
+          item.addEventListener('mouseleave', () => {
+            this.stopItemAnimation(previewImage);
+          });
+        }
+      });
+      
+      // 使用 resize observer 监听窗口大小变化
+      if (!this.resizeObserver) {
+        this.resizeObserver = new ResizeObserver(() => {
+          this.renderResources();
+        });
+        this.resizeObserver.observe(grid);
+      }
+      
+      // 检查每个资源是否已存在(如果已登录),只更新按钮状态,不重新渲染
+      if (this.isLoggedIn()) {
+        this.checkResourcesOwnership(this.filteredResources).then(() => {
+          // 只更新按钮状态,不重新渲染整个列表
+          this.updateButtonStates();
+        });
+      }
+    }
+    
+    // 更新单个资源的按钮状态
+    updateSingleButtonState(resourcePath) {
+      const item = document.querySelector(`[data-resource-path="${CSS.escape(resourcePath)}"]`);
+      if (!item) {
+        console.warn('[StoreView] 未找到资源项:', resourcePath);
+        return;
+      }
+      
+      const resource = this.resources.find(r => r.path === resourcePath);
+      if (!resource) {
+        console.warn('[StoreView] 未找到资源对象:', resourcePath);
+        return;
+      }
+      
+      const button = item.querySelector('.item-buy-button');
+      const priceEl = item.querySelector('.item-price');
+      
+      if (button) {
+        const isFree = resource.points === 0;
+        const isOwned = resource.isOwned || false;
+        
+        if (isFree) {
+          // 免费资源:显示"免费",按钮"添加"
+          button.textContent = '添加';
+          button.classList.remove('item-button-added');
+          if (priceEl) priceEl.textContent = '免费';
+        } else if (isOwned) {
+          // 付费且已购买:显示价格,按钮"添加"(绿色)
+          button.textContent = '添加';
+          button.classList.add('item-button-added');
+          if (priceEl) priceEl.textContent = `${resource.points} Ani币`;
+        } else {
+          // 付费且未购买:显示价格,按钮"购买"
+          button.textContent = '购买';
+          button.classList.remove('item-button-added');
+          if (priceEl) priceEl.textContent = `${resource.points} Ani币`;
+        }
+        
+        console.log('[StoreView] 单个按钮状态已更新:', resourcePath, 'isOwned:', isOwned);
+      }
+    }
+
+    // 更新按钮状态(不重新渲染整个列表)
+    updateButtonStates() {
+      console.log('[StoreView] updateButtonStates 开始,资源列表:', this.resources.map(r => ({path: r.path, isOwned: r.isOwned, points: r.points})));
+      const allItems = document.querySelectorAll('.store-item');
+      console.log('[StoreView] 找到', allItems.length, '个 store-item 元素');
+      allItems.forEach(item => {
+        const path = item.dataset.resourcePath;
+        const resource = this.resources.find(r => r.path === path);
+        if (resource) {
+          const button = item.querySelector('.item-buy-button');
+          const priceEl = item.querySelector('.item-price');
+          if (button) {
+            const isFree = resource.points === 0;
+            const isOwned = resource.isOwned || false;
+            
+            if (isFree) {
+              // 免费资源:显示"免费",按钮"添加"
+              button.textContent = '添加';
+              button.classList.remove('item-button-added');
+              if (priceEl) priceEl.textContent = '免费';
+            } else if (isOwned) {
+              // 付费且已购买:显示价格,按钮"添加"(绿色)
+              button.textContent = '添加';
+              button.classList.add('item-button-added');
+              if (priceEl) priceEl.textContent = `${resource.points} Ani币`;
+            } else {
+              // 付费且未购买:显示价格,按钮"购买"
+              button.textContent = '购买';
+              button.classList.remove('item-button-added');
+              if (priceEl) priceEl.textContent = `${resource.points} Ani币`;
+            }
+          }
+        }
+      });
+    }
+
+    renderItem(resource) {
+      if (!this.itemTemplate) {
+        return '<div class="store-item">模板未加载</div>';
+      }
+      
+      // 使用已保存的点数(在加载资源时已生成)
+      const points = resource.points !== undefined ? resource.points : 0;
+      
+      // 根据点数和资源状态设置按钮文字和类
+      const isFree = points === 0;
+      const isOwned = resource.isOwned || false;
+      
+      let buttonText = '添加';
+      let buttonClass = '';
+      let priceText = points === 0 ? '免费' : `${points} Ani币`;
+      
+      if (isFree) {
+        // 免费资源:显示"免费",按钮"添加"
+        buttonText = '添加';
+        buttonClass = '';
+      } else if (isOwned) {
+        // 付费且已购买:显示价格,按钮"添加"(绿色)
+        buttonText = '添加';
+        buttonClass = 'item-button-added';
+      } else {
+        // 付费且未购买:显示价格,按钮"购买"
+        buttonText = '购买';
+        buttonClass = '';
+      }
+      
+      // 先替换data-points中的{{points}}为原始points值
+      let html = this.itemTemplate
+        .replace(/data-points="\{\{points\}\}"/g, `data-points="${points}"`)
+        .replace(/\{\{name\}\}/g, this.escapeHtml(resource.name))
+        .replace(/\{\{category\}\}/g, this.escapeHtml(resource.category))
+        .replace(/\{\{categoryDir\}\}/g, this.escapeHtml(resource.categoryDir))
+        .replace(/\{\{previewUrl\}\}/g, resource.previewUrl || '')
+        .replace(/\{\{frameCount\}\}/g, resource.frameCount || 0)
+        .replace(/\{\{path\}\}/g, this.escapeHtml(resource.path))
+        .replace(/\{\{buttonText\}\}/g, buttonText)
+        .replace(/\{\{buttonClass\}\}/g, buttonClass);
+      
+      // 最后替换价格显示中的{{points}}为priceText
+      html = html.replace(/\{\{points\}\}/g, priceText);
+      
+      return html;
+    }
+
+    escapeHtml(text) {
+      const div = document.createElement('div');
+      div.textContent = text;
+      return div.innerHTML;
+    }
+
+    async playAnimation(category, folder) {
+      const previewModal = document.getElementById('previewModal');
+      const previewImage = document.getElementById('previewAnimationImage');
+      const previewTitle = document.getElementById('previewTitle');
+      
+      if (!previewModal || !previewImage) return;
+      
+      // 显示弹窗
+      previewModal.style.display = 'flex';
+      if (previewTitle) {
+        previewTitle.textContent = `动画预览: ${folder}`;
+      }
+      
+      // 加载帧列表
+      try {
+        const response = await fetch(
+          `http://localhost:3000/api/store/frames?category=${encodeURIComponent(category)}&folder=${encodeURIComponent(folder)}`
+        );
+        
+        if (!response.ok) {
+          throw new Error(`HTTP error! status: ${response.status}`);
+        }
+        
+        const data = await response.json();
+        
+        if (data.success && data.frameUrls && data.frameUrls.length > 0) {
+          // 保存帧URLs
+          this.frameUrls = data.frameUrls;
+          // 开始播放动画
+          this.startAnimation(previewImage, data.frameUrls);
+        } else {
+          throw new Error('没有可用的帧');
+        }
+      } catch (error) {
+        console.error('[StoreView] 播放动画失败:', error);
+        this.showGlobalAlert('播放动画失败: ' + error.message);
+        previewModal.style.display = 'none';
+      }
+    }
+
+    startAnimation(imgElement, frameUrls) {
+      // 停止之前的动画
+      this.stopAnimation();
+      
+      this.currentFrame = 0;
+      const fps = this.currentFps;
+      const interval = 1000 / fps;
+      
+      // 预加载所有帧
+      const images = [];
+      let loadedCount = 0;
+      
+      frameUrls.forEach((url, index) => {
+        const img = new Image();
+        img.onload = () => {
+          loadedCount++;
+          if (loadedCount === frameUrls.length) {
+            // 所有帧加载完成,开始播放
+            this.animationInterval = setInterval(() => {
+              this.currentFrame = (this.currentFrame + 1) % frameUrls.length;
+              imgElement.src = frameUrls[this.currentFrame];
+            }, interval);
+          }
+        };
+        img.onerror = () => {
+          loadedCount++;
+          if (loadedCount === frameUrls.length) {
+            this.animationInterval = setInterval(() => {
+              this.currentFrame = (this.currentFrame + 1) % frameUrls.length;
+              imgElement.src = frameUrls[this.currentFrame];
+            }, interval);
+          }
+        };
+        img.src = url;
+        images.push(img);
+      });
+      
+      // 立即显示第一帧
+      if (frameUrls.length > 0) {
+        this.currentFrame = 0;
+        imgElement.src = frameUrls[0];
+      }
+    }
+
+    stopAnimation() {
+      if (this.animationInterval) {
+        clearInterval(this.animationInterval);
+        this.animationInterval = null;
+      }
+    }
+
+    async startItemAnimation(imgElement, category, folder, fps = 8) {
+      // 如果已经有动画在播放,先停止
+      this.stopItemAnimation(imgElement);
+      
+      // 检查是否已缓存帧数据
+      const cacheKey = `${category}/${folder}`;
+      let frameUrls = this.itemAnimations.get(cacheKey);
+      
+      if (!frameUrls) {
+        // 加载帧列表
+        try {
+          const response = await fetch(
+            `http://localhost:3000/api/store/frames?category=${encodeURIComponent(category)}&folder=${encodeURIComponent(folder)}`
+          );
+          
+          if (!response.ok) {
+            throw new Error(`HTTP error! status: ${response.status}`);
+          }
+          
+          const data = await response.json();
+          
+          if (data.success && data.frameUrls && data.frameUrls.length > 0) {
+            frameUrls = data.frameUrls;
+            this.itemAnimations.set(cacheKey, frameUrls);
+          } else {
+            return; // 没有可用的帧
+          }
+        } catch (error) {
+          console.error('[StoreView] 加载帧失败:', error);
+          return;
+        }
+      }
+      
+      // 开始播放动画
+      let currentFrame = 0;
+      const interval = 1000 / fps;
+      
+      // 立即显示第一帧
+      if (frameUrls.length > 0) {
+        imgElement.src = frameUrls[0];
+      }
+      
+      // 存储动画 interval
+      const animationInterval = setInterval(() => {
+        currentFrame = (currentFrame + 1) % frameUrls.length;
+        imgElement.src = frameUrls[currentFrame];
+      }, interval);
+      
+      // 将 interval 和 fps 存储到 imgElement 上
+      imgElement._animationInterval = animationInterval;
+      imgElement._currentFps = fps;
+    }
+
+    stopItemAnimation(imgElement) {
+      if (imgElement._animationInterval) {
+        clearInterval(imgElement._animationInterval);
+        imgElement._animationInterval = null;
+      }
+    }
+
+    isLoggedIn() {
+      // 首先检查本地缓存
+      if (this.isUserLoggedIn) {
+        return true;
+      }
+      
+      // 如果本地缓存为 false,尝试从导航栏检查(作为备用方案)
+      try {
+        const navigationFrame = window.parent.document.getElementById('navigationFrame');
+        if (navigationFrame && navigationFrame.contentWindow) {
+          const navDoc = navigationFrame.contentDocument || navigationFrame.contentWindow.document;
+          const userAvatarContainer = navDoc.getElementById('userAvatarContainer');
+          
+          // 如果用户头像容器存在且显示,说明已登录
+          if (userAvatarContainer) {
+            const computedStyle = navDoc.defaultView.getComputedStyle(userAvatarContainer);
+            const isLoggedIn = computedStyle.display !== 'none';
+            // 更新本地缓存
+            this.isUserLoggedIn = isLoggedIn;
+            return isLoggedIn;
+          }
+        }
+      } catch (error) {
+        // 跨域或无法访问时,使用本地缓存
+        console.warn('[StoreView] 无法检查登录状态,使用本地缓存:', error);
+      }
+      
+      return this.isUserLoggedIn;
+    }
+
+    // 检查资源是否已存在
+    async checkResourcesOwnership(resources) {
+      if (!this.isLoggedIn()) {
+        return;
+      }
+      
+      const username = this.getCurrentUsername();
+      if (!username) {
+        return;
+      }
+      
+      // 批量检查资源
+      const checkPromises = resources.map(async (resource) => {
+        try {
+          const response = await fetch(`/api/pay/check-resource?username=${encodeURIComponent(username)}&resourcePath=${encodeURIComponent(resource.path)}`);
+          const result = await response.json();
+          if (result.success) {
+            resource.isOwned = result.exists;
+          }
+        } catch (error) {
+          console.error(`[StoreView] 检查资源 ${resource.path} 失败:`, error);
+        }
+      });
+      
+      await Promise.all(checkPromises);
+    }
+
+    getCurrentUsername() {
+      try {
+        const loginDataStr = localStorage.getItem('loginData');
+        if (!loginDataStr) {
+          return null;
+        }
+        
+        const loginData = JSON.parse(loginDataStr);
+        const now = Date.now();
+        
+        // 检查是否过期
+        if (now >= loginData.expireTime) {
+          localStorage.removeItem('loginData');
+          return null;
+        }
+        
+        return loginData.user ? loginData.user.username : null;
+      } catch (error) {
+        console.error('[StoreView] 获取用户名失败:', error);
+        return null;
+      }
+    }
+
+    async handleBuy(path) {
+      console.log('[StoreView] handleBuy 被调用,path:', path);
+      
+      // 获取资源信息
+      const resource = this.resources.find(r => r.path === path);
+      console.log('[StoreView] 找到资源:', resource);
+      if (!resource) {
+        this.showGlobalAlert('资源不存在');
+        return;
+      }
+
+      // 获取点数(从渲染的 item 中获取,或使用资源的点数)
+      const itemElement = document.querySelector(`[data-resource-path="${path}"]`);
+      let points = resource.points;
+      if (itemElement) {
+        const pointsEl = itemElement.querySelector('.item-price');
+        if (pointsEl) {
+          const pointsText = pointsEl.textContent.replace('Ani币', '').trim();
+          points = parseInt(pointsText) || points;
+        }
+        const dataPoints = itemElement.querySelector('.item-buy-button')?.dataset.points;
+        if (dataPoints) {
+          points = parseInt(dataPoints) || points;
+        }
+      }
+
+      // 检查是否登录
+      const username = this.getCurrentUsername();
+      if (!username) {
+        if (window.parent && window.parent !== window) {
+          window.parent.postMessage({
+            type: 'navigation',
+            page: 'login'
+          }, '*');
+        }
+        return;
+      }
+
+      // 如果是0点,直接添加
+      if (points === 0) {
+        try {
+          const response = await fetch('/api/pay/purchase', {
+            method: 'POST',
+            headers: {
+              'Content-Type': 'application/json'
+            },
+            body: JSON.stringify({
+              username: username,
+              resourcePath: resource.path,
+              categoryDir: resource.categoryDir,
+              itemName: resource.name,
+              points: 0
+            })
+          });
+
+          const result = await response.json();
+          if (result.success) {
+            // 标记为已拥有
+            resource.isOwned = true;
+            
+            // 立即更新单个按钮状态
+            this.updateSingleButtonState(resource.path);
+            
+            // 同时更新所有按钮状态(确保一致性)
+            this.updateButtonStates();
+            
+            if (window.parent && window.parent.HintView) {
+              window.parent.HintView.success('添加成功!文件已添加到网盘', 3000);
+            }
+            
+            // 通知父窗口刷新网盘
+            if (window.parent && window.parent !== window) {
+              window.parent.postMessage({ type: 'refresh-disk' }, '*');
+            }
+          } else {
+            if (window.parent && window.parent.HintView) {
+              window.parent.HintView.error(result.message || '添加失败', 3000);
+            }
+          }
+        } catch (error) {
+          console.error('[StoreView] 添加资源失败:', error);
+          if (window.parent && window.parent.HintView) {
+            window.parent.HintView.error('添加失败,请稍后重试', 3000);
+          }
+        }
+        return;
+      }
+
+      // 检查用户点数
+      console.log('[StoreView] 检查用户点数,用户名:', username, '资源价格:', points);
+      try {
+        const pointsResponse = await fetch(`/api/user/points?username=${encodeURIComponent(username)}`);
+        console.log('[StoreView] 点数请求响应:', pointsResponse.status);
+        if (!pointsResponse.ok) {
+          throw new Error(`HTTP error! status: ${pointsResponse.status}`);
+        }
+        const pointsResult = await pointsResponse.json();
+        console.log('[StoreView] 点数结果:', pointsResult);
+        if (!pointsResult.success) {
+          throw new Error(pointsResult.message || '获取点数失败');
+        }
+        const userPoints = pointsResult.points || 0;
+        console.log('[StoreView] 用户点数:', userPoints, '需要点数:', points);
+
+        if (userPoints >= points) {
+          // 点数充足,弹出确认对话框
+          console.log('[StoreView] 点数充足,准备弹出确认对话框');
+          console.log('[StoreView] window.parent.GlobalConfirm 存在:', !!(window.parent && window.parent.GlobalConfirm));
+          
+          let confirmed = false;
+          if (window.parent && window.parent.GlobalConfirm) {
+            // GlobalConfirm.show 返回 Promise
+            confirmed = await window.parent.GlobalConfirm.show(
+              `确定要花费 ${points} Ani币购买 ${resource.name} 吗?`
+            );
+            console.log('[StoreView] 用户选择:', confirmed);
+          } else {
+            // 降级使用原生 confirm
+            console.log('[StoreView] GlobalConfirm 不可用,使用原生 confirm');
+            confirmed = confirm(`确定要花费 ${points} Ani币购买 ${resource.name} 吗?`);
+          }
+          
+          if (confirmed) {
+              // 确认购买
+              try {
+                console.log('[StoreView] 发送购买请求...');
+                const response = await fetch('/api/pay/purchase', {
+                  method: 'POST',
+                  headers: {
+                    'Content-Type': 'application/json'
+                  },
+                  body: JSON.stringify({
+                    username: username,
+                    resourcePath: resource.path,
+                    categoryDir: resource.categoryDir,
+                    itemName: resource.name,
+                    points: points
+                  })
+                });
+
+                const result = await response.json();
+                console.log('[StoreView] 购买结果:', result);
+                
+                if (result.success) {
+                  // 标记为已拥有
+                  resource.isOwned = true;
+                  console.log('[StoreView] 购买成功,资源已标记为已拥有:', resource.path, 'isOwned:', resource.isOwned);
+                  
+                  // 立即更新单个按钮状态
+                  this.updateSingleButtonState(resource.path);
+                  
+                  // 同时更新所有按钮状态(确保一致性)
+                  this.updateButtonStates();
+                  
+                  if (window.parent && window.parent.HintView) {
+                    window.parent.HintView.success(`购买成功!已扣除 ${points} Ani币,文件已添加到网盘`, 3000);
+                  }
+                  
+                  // 通知父窗口刷新点数和网盘
+                  if (window.parent && window.parent !== window) {
+                    window.parent.postMessage({ type: 'refresh-points' }, '*');
+                    window.parent.postMessage({ type: 'refresh-disk' }, '*');
+                  }
+                } else {
+                  if (window.parent && window.parent.HintView) {
+                    window.parent.HintView.error(result.message || '购买失败', 3000);
+                  }
+                }
+              } catch (error) {
+                console.error('[StoreView] 购买失败:', error);
+                if (window.parent && window.parent.HintView) {
+                  window.parent.HintView.error('购买失败,请稍后重试', 3000);
+                }
+              }
+          }
+        } else {
+          // 点数不足,弹出充值窗口
+          if (window.parent && window.parent !== window) {
+            window.parent.postMessage({
+              type: 'open-recharge-view',
+              needPoints: points,
+              currentPoints: userPoints
+            }, '*');
+          }
+        }
+      } catch (error) {
+        console.error('[StoreView] 检查点数失败:', error);
+        if (window.parent && window.parent.HintView) {
+          window.parent.HintView.error('检查点数失败,请稍后重试', 3000);
+        }
+      }
+    }
+
+    showLoading(show) {
+      const loadingState = document.getElementById('loadingState');
+      if (loadingState) {
+        loadingState.style.display = show ? 'flex' : 'none';
+      }
+    }
+
+    showEmpty() {
+      const emptyState = document.getElementById('emptyState');
+      if (emptyState) {
+        emptyState.style.display = 'flex';
+      }
+    }
+
+    hideEmpty() {
+      const emptyState = document.getElementById('emptyState');
+      if (emptyState) {
+        emptyState.style.display = 'none';
+      }
+    }
+
+    showGlobalAlert(message) {
+      if (window.parent && window.parent.postMessage) {
+        window.parent.postMessage({
+          type: 'global-alert',
+          text: message,
+          duration: 3000
+        }, '*');
+      } else {
+        alert(message);
+      }
+    }
+  }
+
+  // 页面加载完成后初始化
+  if (document.readyState === 'loading') {
+    document.addEventListener('DOMContentLoaded', () => {
+      new StoreView();
+    });
+  } else {
+    new StoreView();
+  }
+})();
+

+ 86 - 0
client/page/ai-generate/ai-generate-view.html

@@ -0,0 +1,86 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>AI生图</title>
+    <link rel="stylesheet" href="../../css/ai-generate/ai-generate-view.css">
+</head>
+<body>
+    <!-- 黑色背景遮罩 -->
+    <div class="ai-generate-overlay" id="aiGenerateOverlay">
+        <!-- 弹出框容器 -->
+        <div class="ai-generate-modal" id="aiGenerateModal">
+            <!-- 关闭按钮(在弹窗右上角) -->
+            <button class="modal-close-btn" id="aiGenerateCancelBtn" title="关闭">
+                <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+                    <path d="M15 5L5 15M5 5l10 10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
+                </svg>
+            </button>
+            
+            <!-- 图片预览区域 - 左右两个图框 -->
+            <div class="ai-generate-preview-container">
+                <!-- 左侧:参考图上传 -->
+                <div class="ai-generate-reference-section">
+                    <div class="ai-generate-preview-box ai-generate-reference-box" id="referenceBox">
+                        <input type="file" id="referenceInput" accept="image/*" style="display: none;">
+                        <div class="reference-upload-area" id="referenceUploadArea">
+                            <div class="upload-icon">+</div>
+                            <div class="upload-text">点击上传参考图</div>
+                        </div>
+                        <div class="reference-image-wrapper" id="referenceImageWrapper" style="display: none;">
+                            <img id="referenceImage" alt="参考图" class="reference-image" />
+                            <button class="reference-remove-btn" id="referenceRemoveBtn" title="删除参考图">
+                                <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+                                    <path d="M12 4L4 12M4 4l8 8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
+                                </svg>
+                            </button>
+                        </div>
+                    </div>
+                </div>
+                
+                <!-- 右侧:Spritesheet 预览 -->
+                <div class="ai-generate-spritesheet-section">
+                    <div class="ai-generate-preview-box ai-generate-spritesheet-box" id="spritesheetBox">
+                        <img id="previewImage" alt="Spritesheet预览" />
+                        <div class="preview-placeholder" id="previewPlaceholder">
+                            <div class="loading-spinner"></div>
+                            <div class="loading-text">正在生成预览图...</div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            
+            <!-- AI生图队列区域 -->
+            <div class="ai-queue-section" id="aiQueueSection" style="display: none;">
+                <div class="ai-queue-title">生图队列</div>
+                <div class="ai-queue-list" id="aiQueueList"></div>
+            </div>
+            
+            <!-- 提示词配置区域 -->
+            <div class="prompt-config-section" id="promptConfigSection" style="display: none;">
+                <div class="config-row">
+                    <textarea id="additionalPromptInput" class="config-textarea" placeholder="输入额外的提示词指令..."></textarea>
+                </div>
+                <!-- 操作按钮区域 -->
+                <div class="prompt-actions">
+                    <button class="action-btn generate-action-btn" id="generateBtn" title="AI生图">
+                        <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+                            <path d="M10 2L12 6H8L10 2Z" fill="currentColor"/>
+                            <path d="M2 10L6 8V12L2 10Z" fill="currentColor"/>
+                            <path d="M10 18L8 14H12L10 18Z" fill="currentColor"/>
+                            <path d="M18 10L14 8V12L18 10Z" fill="currentColor"/>
+                            <circle cx="10" cy="10" r="2" fill="currentColor"/>
+                        </svg>
+                        <span>AI生图</span>
+                        <span class="btn-price" id="aiGeneratePrice" style="margin-left: 8px; font-size: 12px; opacity: 0.9;">-</span>
+                    </button>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <script src="../../js/ai-generate/ai-generate-view.js"></script>
+</body>
+</html>
+

+ 32 - 71
client/page/export/export-view.html

@@ -7,74 +7,14 @@
     <link rel="stylesheet" href="../../css/export-view/export-view.css">
 </head>
 <body>
-    <!-- 黑色背景遮罩 -->
-    <div class="export-overlay" id="exportOverlay">
-        <!-- 弹出框容器 -->
-        <div class="export-modal" id="exportModal">
-            <!-- 关闭按钮(在弹窗右上角) -->
-            <button class="modal-close-btn" id="exportCancelBtn" title="关闭">
-                <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
-                    <path d="M15 5L5 15M5 5l10 10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
-                </svg>
-            </button>
-            <!-- 图片预览区域 - 左右两个图框 -->
-            <div class="export-preview-container">
-                <!-- 左侧:参考图上传 -->
-                <div class="export-reference-section">
-                    <div class="export-preview-box export-reference-box" id="referenceBox">
-                        <input type="file" id="referenceInput" accept="image/*" style="display: none;">
-                        <div class="reference-upload-area" id="referenceUploadArea">
-                            <div class="upload-icon">+</div>
-                            <div class="upload-text">点击上传参考图</div>
-                        </div>
-                        <div class="reference-image-wrapper" id="referenceImageWrapper" style="display: none;">
-                            <img id="referenceImage" alt="参考图" class="reference-image" />
-                            <button class="reference-remove-btn" id="referenceRemoveBtn" title="删除参考图">
-                                <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-                                    <path d="M12 4L4 12M4 4l8 8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
-                                </svg>
-                            </button>
-                        </div>
-                    </div>
-                </div>
-                
-                <!-- 右侧:Spritesheet 预览 -->
-                <div class="export-spritesheet-section">
-                    <div class="export-preview-box export-spritesheet-box" id="spritesheetBox">
-                        <img id="previewImage" alt="Spritesheet预览" />
-                        <div class="preview-placeholder" id="previewPlaceholder">
-                            <div class="loading-spinner"></div>
-                            <div class="loading-text">正在生成预览图...</div>
-                        </div>
-                        <!-- 下载按钮 - 悬浮在右下角 -->
-                        <button class="floating-download-btn" id="exportConfirmBtn" title="下载" disabled>
-                            <svg width="22" height="22" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
-                                <path d="M12 3v12m0 0l-4-4m4 4l4-4M5 21h14" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
-                            </svg>
-                        </button>
-                    </div>
-                </div>
-            </div>
-            
-            <!-- 提示词配置区域(默认隐藏,上传参考图后显示) -->
-            <div class="prompt-config-section" id="promptConfigSection" style="display: none;">
-                <div class="config-row">
-                    <textarea id="additionalPromptInput" class="config-textarea" placeholder="输入额外的提示词指令..."></textarea>
-                </div>
-                <!-- 操作按钮区域 -->
-                <div class="prompt-actions">
-                    <button class="action-btn replace-action-btn" id="replaceBtn" title="替换角色">
-                        <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
-                            <path d="M10 2L12 6H8L10 2Z" fill="currentColor"/>
-                            <path d="M2 10L6 8V12L2 10Z" fill="currentColor"/>
-                            <path d="M10 18L8 14H12L10 18Z" fill="currentColor"/>
-                            <path d="M18 10L14 8V12L18 10Z" fill="currentColor"/>
-                            <circle cx="10" cy="10" r="2" fill="currentColor"/>
-                        </svg>
-                        <span>替换</span>
-                    </button>
-                </div>
-            </div>
+    <!-- 黑色背景遮罩(隐藏,仅用于生成 spritesheet) -->
+    <div class="export-overlay" id="exportOverlay" style="display: none;">
+        <div class="export-modal" id="exportModal" style="display: none;">
+            <img id="previewImage" alt="Spritesheet预览" style="display: none;" />
+            <div class="preview-placeholder" id="previewPlaceholder" style="display: none;"></div>
+            <button id="exportCancelBtn" style="display: none;"></button>
+            <button id="floatingAIBtn" style="display: none;"></button>
+            <button id="exportConfirmBtn" style="display: none;"></button>
         </div>
     </div>
 
@@ -91,24 +31,45 @@
             </div>
             <div class="download-confirm-content">
                 <div class="download-option" data-option="original">
-                    <div class="download-option-icon">📄</div>
+                    <div class="download-option-icon">
+                        <svg width="32" height="32" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+                            <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" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+                            <path d="M14 2V8H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+                            <path d="M16 13H8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+                            <path d="M16 17H8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+                            <path d="M10 9H9H8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+                        </svg>
+                    </div>
                     <div class="download-option-info">
                         <div class="download-option-title">源文件下载</div>
                         <div class="download-option-desc">直接下载原始图片,不进行抠图处理</div>
                     </div>
                 </div>
                 <div class="download-option" data-option="normal">
-                    <div class="download-option-icon">✂️</div>
+                    <div class="download-option-icon">
+                        <svg width="32" height="32" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+                            <path d="M6 7C6 7 8 9 10 11C12 13 12 15 12 15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+                            <path d="M18 7C18 7 16 9 14 11C12 13 12 15 12 15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+                            <circle cx="6" cy="7" r="2" stroke="currentColor" stroke-width="2" fill="none"/>
+                            <circle cx="18" cy="7" r="2" stroke="currentColor" stroke-width="2" fill="none"/>
+                            <path d="M12 15L12 21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+                        </svg>
+                    </div>
                     <div class="download-option-info">
                         <div class="download-option-title">普通抠图下载</div>
                         <div class="download-option-desc">使用 rembg 进行抠图处理</div>
                     </div>
                 </div>
                 <div class="download-option" data-option="vip">
-                    <div class="download-option-icon">✨</div>
+                    <div class="download-option-icon">
+                        <svg width="32" height="32" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+                            <path d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="currentColor" fill-opacity="0.1"/>
+                        </svg>
+                    </div>
                     <div class="download-option-info">
                         <div class="download-option-title">VIP抠图下载</div>
                         <div class="download-option-desc">使用 BiRefNet 进行高质量抠图处理</div>
+                        <div class="download-option-price" id="vipMattingPrice">-</div>
                     </div>
                 </div>
             </div>

+ 10 - 0
client/page/hint-view.html

@@ -0,0 +1,10 @@
+<!-- 提示消息组件 -->
+<div class="hint-view" id="hintView">
+    <div class="hint-content">
+        <svg class="hint-icon" width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
+            <path d="M10 0C4.48 0 0 4.48 0 10s4.48 10 10 10 10-4.48 10-10S15.52 0 10 0zm-1 15l-5-5 1.41-1.41L9 12.17l7.59-7.59L18 6l-9 9z"/>
+        </svg>
+        <span class="hint-message" id="hintMessage"></span>
+    </div>
+</div>
+

+ 1 - 1
client/page/navigation/navigation.html

@@ -11,7 +11,7 @@
   <nav class="top-navigation">
     <div class="nav-left">
       <img src="../../static/logo.png" alt="公司Logo" class="nav-logo" />
-      <span class="nav-title">游戏动画生成器</span>
+      <span class="nav-title">AI动画生成器</span>
     </div>
     <div class="nav-center">
       <button class="nav-btn active" data-page="store">动画商城</button>

+ 44 - 0
client/page/pay-view/pay-view.html

@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>支付</title>
+  <link rel="stylesheet" href="../../css/pay-view/pay-view.css">
+</head>
+<body>
+  <div class="pay-view-overlay" id="payViewOverlay">
+    <div class="pay-view-container" id="payViewContainer">
+      <div class="pay-view-header">
+        <h2 class="pay-view-title">微信支付</h2>
+        <button class="pay-view-close" id="payViewClose">×</button>
+      </div>
+      
+      <div class="pay-view-content">
+        <div class="pay-info">
+          <div class="pay-item-name" id="payItemName"></div>
+          <div class="pay-item-price" id="payItemPrice"></div>
+        </div>
+        
+        <div class="qr-code-container">
+          <div class="qr-code-placeholder" id="qrCodePlaceholder">
+            <svg width="200" height="200" viewBox="0 0 200 200" fill="none">
+              <rect width="200" height="200" fill="#f5f5f5"/>
+              <text x="100" y="100" text-anchor="middle" fill="#999" font-size="14">微信二维码</text>
+            </svg>
+          </div>
+          <p class="qr-code-hint">请使用微信扫码支付</p>
+          <p class="qr-code-tip">(演示模式:关闭窗口即支付成功)</p>
+        </div>
+        
+        <div class="pay-view-actions">
+          <button class="pay-cancel-btn" id="payCancelBtn">取消</button>
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <script src="../../js/pay-view/pay-view.js"></script>
+</body>
+</html>
+

+ 98 - 0
client/page/profile/profile.html

@@ -0,0 +1,98 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>我的</title>
+  <link rel="icon" type="image/png" href="../../static/favicon.png">
+  <link rel="stylesheet" href="../../css/profile/profile.css">
+  <link rel="stylesheet" href="../../css/hint-view.css">
+</head>
+<body>
+  <!-- 充值界面 iframe -->
+  <iframe id="rechargeViewFrame" src="../recharge-view/recharge-view.html" frameborder="0" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 1000004; border: none; background: transparent;"></iframe>
+  <div class="profile-container">
+    <!-- 返回按钮 -->
+    <div class="profile-header">
+      <button class="back-btn" id="backBtn">
+        <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
+          <path d="M12.5 15L7.5 10L12.5 5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+        </svg>
+        返回
+      </button>
+    </div>
+
+    <div class="profile-content">
+      <h1 class="page-title">我的</h1>
+      <div class="profile-layout">
+        <!-- 左侧:用户信息区域 -->
+        <div class="profile-left">
+          <!-- 用户资料区域 -->
+          <div class="profile-section">
+            <h2 class="section-title">用户资料</h2>
+            <div class="profile-info">
+              <div class="avatar-section">
+                <div class="avatar-preview" id="avatarPreview">
+                  <img id="avatarImage" src="" alt="头像">
+                </div>
+              </div>
+              
+              <div class="info-form">
+                <div class="form-item">
+                  <label class="form-label">用户名</label>
+                  <input type="text" class="form-input" id="usernameInput" readonly>
+                </div>
+                <div class="form-item">
+                  <label class="form-label">手机号</label>
+                  <input type="text" class="form-input" id="phoneInput" readonly>
+                </div>
+              </div>
+            </div>
+          </div>
+
+          <!-- Ani币区域 -->
+          <div class="profile-section">
+            <h2 class="section-title">我的Ani币</h2>
+            <div class="points-section">
+              <div class="points-display">
+                <span class="points-label">当前余额:</span>
+                <span class="points-value" id="pointsValue">0</span>
+                <span class="points-unit">Ani币</span>
+              </div>
+              <button class="recharge-btn" id="rechargeBtn">充值</button>
+            </div>
+          </div>
+
+          <!-- 退出登录按钮 -->
+          <button class="logout-btn" id="logoutBtn">退出登录</button>
+        </div>
+
+        <!-- 右侧:AI生图历史和购买记录 -->
+        <div class="profile-right">
+          <div class="profile-section history-section">
+            <h2 class="section-title">AI生图历史</h2>
+            <div class="ai-history-container" id="aiHistoryContainer">
+              <div class="loading-state" id="historyLoading">加载中...</div>
+              <div class="empty-state" id="historyEmpty" style="display: none;">暂无历史记录</div>
+              <div class="history-grid" id="historyGrid"></div>
+            </div>
+          </div>
+          
+          <div class="profile-section purchase-section">
+            <h2 class="section-title">素材购买记录</h2>
+            <div class="purchase-container" id="purchaseContainer">
+              <div class="loading-state" id="purchaseLoading">加载中...</div>
+              <div class="empty-state" id="purchaseEmpty" style="display: none;">暂无购买记录</div>
+              <div class="purchase-grid" id="purchaseGrid"></div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <script src="../../js/hint-view.js"></script>
+  <script src="../../js/profile/profile.js"></script>
+</body>
+</html>
+

+ 79 - 0
client/page/recharge-view/recharge-view.html

@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>充值</title>
+  <link rel="stylesheet" href="../../css/recharge-view/recharge-view.css">
+</head>
+<body>
+  <div class="recharge-overlay" id="rechargeOverlay">
+    <div class="recharge-container" id="rechargeContainer">
+      <div class="recharge-header">
+        <h2 class="recharge-title">充值Ani币</h2>
+        <button class="recharge-close" id="rechargeClose">×</button>
+      </div>
+      
+      <div class="recharge-content">
+        <div class="recharge-packages">
+          <div class="recharge-package" data-package="100" data-price="5" data-points="120">
+            <div class="package-header">
+              <div class="package-points">100 Ani币</div>
+              <div class="package-bonus">送 20 Ani币</div>
+              <div class="package-price">¥5</div>
+            </div>
+            <div class="package-total">共 120 Ani币</div>
+          </div>
+          
+          <div class="recharge-package" data-package="1000" data-price="50" data-points="1200">
+            <div class="package-header">
+              <div class="package-points">1000 Ani币</div>
+              <div class="package-bonus">送 200 Ani币</div>
+              <div class="package-price">¥50</div>
+            </div>
+            <div class="package-total">共 1200 Ani币</div>
+          </div>
+          
+          <div class="recharge-package" data-package="10000" data-price="500" data-points="10800">
+            <div class="package-header">
+              <div class="package-points">10000 Ani币</div>
+              <div class="package-bonus">送 800 Ani币</div>
+              <div class="package-price">¥500</div>
+            </div>
+            <div class="package-total">共 10800 Ani币</div>
+          </div>
+        </div>
+        
+        <div class="recharge-info" id="rechargeInfo" style="display: none;">
+          <div class="selected-package-info">
+            <div class="info-item">
+              <span class="info-label">充值Ani币:</span>
+              <span class="info-value" id="selectedPoints">0</span>
+            </div>
+            <div class="info-item">
+              <span class="info-label">支付金额:</span>
+              <span class="info-value" id="selectedPrice">¥0</span>
+            </div>
+          </div>
+          
+          <div class="qr-code-container">
+            <div class="qr-code-placeholder" id="qrCodePlaceholder">
+              <svg width="200" height="200" viewBox="0 0 200 200" fill="none">
+                <rect width="200" height="200" fill="#f5f5f5"/>
+                <text x="100" y="100" text-anchor="middle" fill="#999" font-size="14">微信二维码</text>
+              </svg>
+            </div>
+            <p class="qr-code-hint">请使用微信扫码支付</p>
+            <p class="qr-code-tip">(正式版本:扫描二维码成功后自动完成支付)</p>
+            <button class="test-buy-btn" id="testBuyBtn">测试购买</button>
+            <p class="test-buy-tip">(测试用按钮,正式版本将移除)</p>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <script src="../../js/recharge-view/recharge-view.js"></script>
+</body>
+</html>
+

+ 10 - 3
client/page/seq_ani_player/card.html

@@ -10,9 +10,16 @@
           </svg>
           <span class="folder-name">--</span>
         </div>
-        <button class="btn-export" title="导出为Sprite Sheet">
-          导出动画
-        </button>
+        <div class="preview-action-btns">
+          <button class="btn-action btn-ai" title="AI生图">
+            <span class="ai-icon">AI</span>
+          </button>
+          <button class="btn-action btn-download" title="导出下载">
+            <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+              <path d="M12 3v12m0 0l-4-4m4 4l4-4M5 21h14" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
+            </svg>
+          </button>
+        </div>
       </div>
       
       <img class="preview-image" alt="" />

+ 23 - 0
client/page/store/item.html

@@ -0,0 +1,23 @@
+<div class="store-item" data-resource-path="{{path}}" data-category="{{categoryDir}}" data-folder="{{name}}">
+  <div class="item-preview">
+    <img class="item-preview-image" src="{{previewUrl}}" alt="{{name}}" data-preview-url="{{previewUrl}}" />
+    <span class="item-category">{{category}}</span>
+    <div class="item-fps-control">
+      <label class="item-fps-label">播放速度</label>
+      <input type="range" class="item-fps-slider" min="1" max="30" value="8" step="1">
+      <span class="item-fps-display">8 FPS</span>
+    </div>
+  </div>
+  <div class="item-info">
+    <div class="item-meta">
+      <div class="item-name">{{name}}</div>
+    </div>
+  </div>
+  <div class="item-actions">
+    <div class="item-price">{{points}} Ani币</div>
+    <button class="item-buy-button {{buttonClass}}" data-resource-path="{{path}}" data-points="{{points}}">
+      {{buttonText}}
+    </button>
+  </div>
+</div>
+

+ 854 - 0
server/admin.js

@@ -0,0 +1,854 @@
+// 管理后台API处理
+
+const { getDatabase } = require('./sql');
+const fs = require('fs');
+const path = require('path');
+const { promisify } = require('util');
+const { formidable } = require('formidable');
+const RECHARGE_FILE = path.join(__dirname, 'recharge.json');
+const PRODUCT_PRICING_FILE = path.join(__dirname, 'product-pricing.json');
+
+const mkdir = promisify(fs.mkdir);
+const access = promisify(fs.access);
+const readdir = promisify(fs.readdir);
+const stat = promisify(fs.stat);
+const unlink = promisify(fs.unlink);
+const rmdir = promisify(fs.rmdir);
+
+// 商店资源根目录(使用 market_data 目录)
+const STORE_DIR = path.join(__dirname, 'market_data');
+
+// 确保目录存在
+async function ensureDir(dirPath) {
+  try {
+    await access(dirPath);
+  } catch (error) {
+    await mkdir(dirPath, { recursive: true });
+  }
+}
+
+// 递归删除目录
+async function deleteDirectory(dirPath) {
+  try {
+    const files = await readdir(dirPath);
+    for (const file of files) {
+      const filePath = path.join(dirPath, file);
+      const stats = await stat(filePath);
+      if (stats.isDirectory()) {
+        await deleteDirectory(filePath);
+      } else {
+        await unlink(filePath);
+      }
+    }
+    await rmdir(dirPath);
+  } catch (error) {
+    console.error('[Admin] 删除目录失败:', error);
+    throw error;
+  }
+}
+
+// 获取所有用户列表
+async function handleGetAllUsers(req, res) {
+  try {
+    console.log('[Admin] 收到获取用户列表请求');
+    const db = await getDatabase();
+    const users = db.getAllUsers();
+    
+    console.log('[Admin] 从数据库获取到用户数量:', users ? users.length : 0);
+    console.log('[Admin] 用户数据:', users);
+    
+    res.writeHead(200, { 
+      'Content-Type': 'application/json; charset=utf-8',
+      'Access-Control-Allow-Origin': '*' // 允许跨域访问
+    });
+    res.end(JSON.stringify({
+      success: true,
+      users: users || []
+    }));
+  } catch (error) {
+    console.error('[Admin] 获取用户列表失败:', error);
+    res.writeHead(500, { 
+      'Content-Type': 'application/json; charset=utf-8',
+      'Access-Control-Allow-Origin': '*'
+    });
+    res.end(JSON.stringify({
+      success: false,
+      message: '获取用户列表失败: ' + error.message
+    }));
+  }
+}
+
+// 更新用户信息(管理员)
+async function handleAdminUpdateUser(req, res) {
+  let body = '';
+  
+  req.on('data', chunk => {
+    body += chunk.toString();
+  });
+  
+  req.on('end', async () => {
+    try {
+      const data = JSON.parse(body);
+      const { id, username, phone, points } = data;
+      
+      if (!id) {
+        res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
+        res.end(JSON.stringify({
+          success: false,
+          message: '缺少用户ID参数'
+        }));
+        return;
+      }
+      
+      const db = await getDatabase();
+      const user = db.findUserById(id);
+      
+      if (!user) {
+        res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
+        res.end(JSON.stringify({
+          success: false,
+          message: '用户不存在'
+        }));
+        return;
+      }
+      
+      // 构建更新对象
+      const updates = {};
+      if (username !== undefined) {
+        updates.username = username;
+      }
+      if (phone !== undefined) {
+        updates.phone = phone;
+      }
+      
+      // 更新用户信息
+      if (Object.keys(updates).length > 0) {
+        db.updateUser(id, updates);
+      }
+      
+      // 更新点数(如果提供)
+      if (points !== undefined && points !== null) {
+        const currentPoints = db.getUserPoints(user.username);
+        const diff = points - currentPoints;
+        if (diff > 0) {
+          db.addPoints(user.username, diff);
+        } else if (diff < 0) {
+          db.deductPoints(user.username, Math.abs(diff));
+        }
+      }
+      
+      res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
+      res.end(JSON.stringify({
+        success: true,
+        message: '更新成功'
+      }));
+    } catch (error) {
+      console.error('[Admin] 更新用户失败:', error);
+      res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
+      res.end(JSON.stringify({
+        success: false,
+        message: '更新失败: ' + error.message
+      }));
+    }
+  });
+}
+
+// 上传商店素材
+async function handleAdminUploadStore(req, res) {
+  console.log('[Admin] ========== 收到上传请求 ==========');
+  console.log('[Admin] 请求方法:', req.method);
+  console.log('[Admin] 请求URL:', req.url);
+  console.log('[Admin] Content-Type:', req.headers['content-type']);
+  
+  const form = formidable({
+    uploadDir: path.join(__dirname, 'temp'),
+    keepExtensions: true,
+    multiples: true
+  });
+  
+  form.parse(req, async (err, fields, files) => {
+    if (err) {
+      console.error('[Admin] ❌ 解析上传文件失败:', err);
+      res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
+      res.end(JSON.stringify({
+        success: false,
+        message: '上传失败: ' + err.message
+      }));
+      return;
+    }
+    
+    console.log('[Admin] ✅ 文件解析成功');
+    console.log('[Admin] fields:', JSON.stringify(fields, null, 2));
+    console.log('[Admin] files keys:', Object.keys(files));
+    
+    try {
+      const category = Array.isArray(fields.category) ? fields.category[0] : fields.category;
+      const name = Array.isArray(fields.name) ? fields.name[0] : fields.name;
+      const price = parseInt(Array.isArray(fields.price) ? fields.price[0] : fields.price) || 0;
+      
+      console.log('[Admin] 📦 上传参数:');
+      console.log('[Admin]   - 分类(category):', category);
+      console.log('[Admin]   - 文件夹名(name):', name);
+      console.log('[Admin]   - 价格(price):', price);
+      
+      if (!category || !name) {
+        res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
+        res.end(JSON.stringify({
+          success: false,
+          message: '缺少分类或名称参数'
+        }));
+        return;
+      }
+      
+      // 获取上传的文件
+      let fileList = [];
+      if (files.files) {
+        fileList = Array.isArray(files.files) ? files.files : [files.files];
+      }
+      
+      console.log('[Admin] 📁 收到文件数量:', fileList.length);
+      
+      if (fileList.length === 0) {
+        console.log('[Admin] ❌ 文件列表为空');
+        res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
+        res.end(JSON.stringify({
+          success: false,
+          message: '请选择要上传的文件'
+        }));
+        return;
+      }
+      
+      // 创建目标目录
+      const targetDir = path.join(STORE_DIR, category, name);
+      console.log('[Admin] 📂 目标目录:', targetDir);
+      console.log('[Admin] 📂 STORE_DIR:', STORE_DIR);
+      await ensureDir(targetDir);
+      console.log('[Admin] ✅ 目标目录已创建/确认存在');
+      
+      // 移动文件到目标目录,保持目录结构
+      console.log('[Admin] 🔄 开始处理', fileList.length, '个文件...');
+      let processedCount = 0;
+      let skippedCount = 0;
+      
+      for (let i = 0; i < fileList.length; i++) {
+        const file = fileList[i];
+        const fileObj = Array.isArray(file) ? file[0] : file;
+        
+        if (!fileObj || !fileObj.filepath) {
+          console.log(`[Admin] ⚠️  [${i+1}/${fileList.length}] 跳过无效文件对象`);
+          skippedCount++;
+          continue;
+        }
+        
+        // 获取原始文件名(包含相对路径)
+        const originalName = fileObj.originalFilename || path.basename(fileObj.filepath);
+        console.log(`[Admin] 📄 [${i+1}/${fileList.length}] 处理文件:`, originalName);
+        
+        // 处理路径分隔符(统一使用 /)
+        const normalizedName = originalName.replace(/\\/g, '/');
+        
+        // 如果 originalName 包含文件夹名(如 player_0001/image.png),需要去掉文件夹名前缀
+        // 因为 targetDir 已经是 market_data/category/name 了
+        let relativePath = normalizedName;
+        
+        // 如果路径以文件夹名开头(如 player_0001/image.png),去掉文件夹名
+        if (normalizedName.startsWith(name + '/')) {
+          relativePath = normalizedName.substring(name.length + 1);
+          console.log(`[Admin]   └─ 去掉文件夹前缀 "${name}/",相对路径:`, relativePath);
+        }
+        
+        const targetPath = path.join(targetDir, relativePath);
+        console.log(`[Admin]   └─ 目标路径:`, targetPath);
+        
+        // 确保目标目录存在
+        await ensureDir(path.dirname(targetPath));
+        
+        // 复制文件
+        await fs.promises.copyFile(fileObj.filepath, targetPath);
+        console.log(`[Admin]   └─ ✅ 文件已复制成功`);
+        processedCount++;
+        
+        // 删除临时文件
+        try {
+          await fs.promises.unlink(fileObj.filepath);
+        } catch (err) {
+          console.warn(`[Admin]   └─ ⚠️ 删除临时文件失败:`, err.message);
+        }
+      }
+      
+      console.log('[Admin] ========== 上传处理完成 ==========');
+      console.log('[Admin] ✅ 成功处理:', processedCount, '个文件');
+      if (skippedCount > 0) {
+        console.log('[Admin] ⚠️ 跳过:', skippedCount, '个无效文件');
+      }
+      console.log('[Admin] 📂 最终保存位置:', targetDir);
+      
+      // 更新商店资源配置(如果需要)
+      // 这里可以添加更新商店配置文件的逻辑
+      
+      res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
+      res.end(JSON.stringify({
+        success: true,
+        message: '上传成功',
+        path: `${category}/${name}`,
+        fileCount: processedCount
+      }));
+    } catch (error) {
+      console.error('[Admin] ❌ ========== 上传失败 ==========');
+      console.error('[Admin] 错误信息:', error.message);
+      console.error('[Admin] 错误堆栈:', error.stack);
+      res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
+      res.end(JSON.stringify({
+        success: false,
+        message: '上传失败: ' + error.message
+      }));
+    }
+  });
+}
+
+// 删除商店素材
+async function handleAdminDeleteStore(req, res) {
+  let body = '';
+  
+  req.on('data', chunk => {
+    body += chunk.toString();
+  });
+  
+  req.on('end', async () => {
+    try {
+      const data = JSON.parse(body);
+      const { resourcePath } = data;
+      
+      console.log('[Admin] 删除请求 - resourcePath:', resourcePath);
+      console.log('[Admin] STORE_DIR:', STORE_DIR);
+      
+      if (!resourcePath) {
+        res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
+        res.end(JSON.stringify({
+          success: false,
+          message: '缺少资源路径参数'
+        }));
+        return;
+      }
+      
+      // 构建完整路径
+      const fullPath = path.join(STORE_DIR, resourcePath);
+      console.log('[Admin] 完整路径:', fullPath);
+      
+      // 安全检查
+      const normalizedPath = path.normalize(fullPath);
+      const normalizedRoot = path.normalize(STORE_DIR);
+      console.log('[Admin] normalizedPath:', normalizedPath);
+      console.log('[Admin] normalizedRoot:', normalizedRoot);
+      
+      if (!normalizedPath.startsWith(normalizedRoot)) {
+        console.log('[Admin] 安全检查失败');
+        res.writeHead(403, { 'Content-Type': 'application/json; charset=utf-8' });
+        res.end(JSON.stringify({
+          success: false,
+          message: '访问被拒绝'
+        }));
+        return;
+      }
+      
+      // 检查路径是否存在
+      try {
+        const stats = await stat(fullPath);
+        console.log('[Admin] 路径存在,是目录:', stats.isDirectory());
+        
+        if (stats.isDirectory()) {
+          await deleteDirectory(fullPath);
+        } else {
+          await unlink(fullPath);
+        }
+        
+        console.log('[Admin] 删除成功');
+        res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
+        res.end(JSON.stringify({
+          success: true,
+          message: '删除成功'
+        }));
+      } catch (error) {
+        console.log('[Admin] stat 错误:', error.code, error.message);
+        if (error.code === 'ENOENT') {
+          res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
+          res.end(JSON.stringify({
+            success: false,
+            message: '资源不存在'
+          }));
+        } else {
+          throw error;
+        }
+      }
+    } catch (error) {
+      console.error('[Admin] 删除素材失败:', error);
+      res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
+      res.end(JSON.stringify({
+        success: false,
+        message: '删除失败: ' + error.message
+      }));
+    }
+  });
+}
+
+// 创建分类文件夹
+async function handleAdminCreateFolder(req, res) {
+  let body = '';
+  
+  req.on('data', chunk => {
+    body += chunk.toString();
+  });
+  
+  req.on('end', async () => {
+    try {
+      const data = JSON.parse(body);
+      const { name } = data;
+      
+      if (!name || !name.trim()) {
+        res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
+        res.end(JSON.stringify({
+          success: false,
+          message: '缺少文件夹名称参数'
+        }));
+        return;
+      }
+
+      const folderName = name.trim();
+
+      // 验证文件夹名称
+      if (/[\\/:*?"<>|]/.test(folderName)) {
+        res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
+        res.end(JSON.stringify({
+          success: false,
+          message: '文件夹名称包含非法字符'
+        }));
+        return;
+      }
+
+      // 创建分类文件夹(在 STORE_DIR 根目录下)
+      const targetDir = path.join(STORE_DIR, folderName);
+      
+      // 检查文件夹是否已存在
+      try {
+        await access(targetDir);
+        res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
+        res.end(JSON.stringify({
+          success: false,
+          message: '文件夹已存在'
+        }));
+        return;
+      } catch (error) {
+        if (error.code !== 'ENOENT') {
+          throw error;
+        }
+        // 文件夹不存在,创建它
+        await mkdir(targetDir, { recursive: true });
+      }
+
+      res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
+      res.end(JSON.stringify({
+        success: true,
+        message: '创建文件夹成功',
+        path: folderName
+      }));
+    } catch (error) {
+      console.error('[Admin] 创建文件夹失败:', error);
+      res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
+      res.end(JSON.stringify({
+        success: false,
+        message: '创建文件夹失败: ' + error.message
+      }));
+    }
+  });
+}
+
+// 重命名商店素材
+async function handleAdminRenameStore(req, res) {
+  let body = '';
+  
+  req.on('data', chunk => {
+    body += chunk.toString();
+  });
+  
+  req.on('end', async () => {
+    try {
+      const data = JSON.parse(body);
+      const { resourcePath, newName } = data;
+      
+      if (!resourcePath || !newName) {
+        res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
+        res.end(JSON.stringify({
+          success: false,
+          message: '缺少资源路径或新名称参数'
+        }));
+        return;
+      }
+
+      // 验证新名称
+      if (/[\\/:*?"<>|]/.test(newName)) {
+        res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
+        res.end(JSON.stringify({
+          success: false,
+          message: '名称包含非法字符'
+        }));
+        return;
+      }
+
+      // 构建完整路径
+      const fullPath = path.join(STORE_DIR, resourcePath);
+      
+      // 安全检查
+      const normalizedPath = path.normalize(fullPath);
+      const normalizedRoot = path.normalize(STORE_DIR);
+      if (!normalizedPath.startsWith(normalizedRoot)) {
+        res.writeHead(403, { 'Content-Type': 'application/json; charset=utf-8' });
+        res.end(JSON.stringify({
+          success: false,
+          message: '访问被拒绝'
+        }));
+        return;
+      }
+
+      // 检查路径是否存在
+      try {
+        await access(fullPath);
+      } catch (error) {
+        if (error.code === 'ENOENT') {
+          res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
+          res.end(JSON.stringify({
+            success: false,
+            message: '资源不存在'
+          }));
+          return;
+        } else {
+          throw error;
+        }
+      }
+
+      // 构建新路径
+      const parentDir = path.dirname(fullPath);
+      const newFullPath = path.join(parentDir, newName);
+
+      // 检查新名称是否已存在
+      try {
+        await access(newFullPath);
+        res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
+        res.end(JSON.stringify({
+          success: false,
+          message: '新名称已存在'
+        }));
+        return;
+      } catch (error) {
+        if (error.code !== 'ENOENT') {
+          throw error;
+        }
+      }
+
+      // 重命名文件或文件夹
+      await fs.promises.rename(fullPath, newFullPath);
+
+      const newPathKey = path.relative(STORE_DIR, newFullPath).replace(/\\/g, '/');
+      const oldPathKey = resourcePath;
+
+      // 检查是否是分类文件夹(在根目录下)
+      const stats = await stat(newFullPath);
+      const isCategoryFolder = stats.isDirectory() && !resourcePath.includes('/');
+
+      // 更新价格文件中的路径(如果存在)
+      try {
+        const pricesFilePath = path.join(STORE_DIR, 'prices.json');
+        const pricesData = await fs.promises.readFile(pricesFilePath, 'utf-8');
+        const prices = JSON.parse(pricesData);
+        
+        // 更新所有相关的价格路径
+        // 如果是分类文件夹,需要更新所有子资源的价格路径
+        if (isCategoryFolder) {
+          const updatedPrices = {};
+          for (const [key, value] of Object.entries(prices)) {
+            if (key.startsWith(oldPathKey + '/')) {
+              // 更新子资源的路径
+              const newKey = key.replace(oldPathKey + '/', newPathKey + '/');
+              updatedPrices[newKey] = value;
+            } else if (key === oldPathKey) {
+              // 更新分类文件夹本身的价格(如果有)
+              updatedPrices[newPathKey] = value;
+            } else {
+              // 保留其他路径
+              updatedPrices[key] = value;
+            }
+          }
+          await fs.promises.writeFile(pricesFilePath, JSON.stringify(updatedPrices, null, 2), 'utf-8');
+        } else {
+          // 只更新当前资源的价格路径
+          if (prices[oldPathKey] !== undefined) {
+            prices[newPathKey] = prices[oldPathKey];
+            delete prices[oldPathKey];
+            await fs.promises.writeFile(pricesFilePath, JSON.stringify(prices, null, 2), 'utf-8');
+          }
+        }
+      } catch (error) {
+        // 价格文件不存在或更新失败,不影响重命名操作
+        console.warn('[Admin] 更新价格文件失败:', error);
+      }
+
+      res.writeHead(200, { 
+        'Content-Type': 'application/json; charset=utf-8',
+        'Access-Control-Allow-Origin': '*'
+      });
+      res.end(JSON.stringify({
+        success: true,
+        message: '重命名成功',
+        newPath: newPathKey,
+        isCategoryFolder: isCategoryFolder
+      }));
+    } catch (error) {
+      console.error('[Admin] 重命名素材失败:', error);
+      res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
+      res.end(JSON.stringify({
+        success: false,
+        message: '重命名失败: ' + error.message
+      }));
+    }
+  });
+}
+
+// 更新资源价格
+async function handleAdminUpdatePrice(req, res) {
+  let body = '';
+  
+  req.on('data', chunk => {
+    body += chunk.toString();
+  });
+  
+  req.on('end', async () => {
+    try {
+      const data = JSON.parse(body);
+      const { resourcePath, price } = data;
+      
+      if (!resourcePath || price === undefined) {
+        res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
+        res.end(JSON.stringify({
+          success: false,
+          message: '缺少资源路径或价格参数'
+        }));
+        return;
+      }
+      
+      // 价格数据存储在JSON文件中
+      const pricesFilePath = path.join(STORE_DIR, 'prices.json');
+      let prices = {};
+      
+      try {
+        const pricesData = await fs.promises.readFile(pricesFilePath, 'utf-8');
+        prices = JSON.parse(pricesData);
+      } catch (error) {
+        // 文件不存在,创建新的
+        prices = {};
+      }
+      
+      // 更新价格
+      prices[resourcePath] = price;
+      
+      // 保存到文件
+      await fs.promises.writeFile(pricesFilePath, JSON.stringify(prices, null, 2), 'utf-8');
+      
+      // 通知 StoreManager 清除价格缓存(通过事件或直接调用)
+      // 注意:这里 StoreManager 的缓存会在下次请求时自动清除
+      
+      res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
+      res.end(JSON.stringify({
+        success: true,
+        message: '价格更新成功'
+      }));
+    } catch (error) {
+      console.error('[Admin] 更新价格失败:', error);
+      res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
+      res.end(JSON.stringify({
+        success: false,
+        message: '更新失败: ' + error.message
+      }));
+    }
+  });
+}
+
+// 货币设置文件路径
+const CURRENCY_SETTINGS_FILE = path.join(__dirname, 'currency-settings.json');
+
+// 获取货币/充值套餐设置
+async function handleGetCurrencySettings(req, res) {
+  try {
+    let packages = [
+      { points: 100, bonus: 20, price: 5 },
+      { points: 1000, bonus: 200, price: 50 },
+      { points: 10000, bonus: 800, price: 500 }
+    ];
+    
+    try {
+      const data = await fs.promises.readFile(CURRENCY_SETTINGS_FILE, 'utf-8');
+      const parsed = JSON.parse(data);
+      packages = parsed.packages || packages;
+    } catch (err) {
+      // 文件不存在,使用默认值
+    }
+    
+    res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
+    res.end(JSON.stringify({
+      success: true,
+      packages
+    }));
+  } catch (error) {
+    console.error('[Admin] 获取充值套餐失败:', error);
+    res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
+    res.end(JSON.stringify({
+      success: false,
+      message: '获取设置失败: ' + error.message
+    }));
+  }
+}
+
+// 保存货币/充值套餐设置
+async function handleSaveCurrencySettings(req, res) {
+  let body = '';
+  req.on('data', chunk => { body += chunk.toString(); });
+  req.on('end', async () => {
+    try {
+      const data = JSON.parse(body);
+      const { packages } = data;
+      
+      await fs.promises.writeFile(CURRENCY_SETTINGS_FILE, JSON.stringify({ packages }, null, 2), 'utf-8');
+      
+      res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
+      res.end(JSON.stringify({
+        success: true,
+        message: '充值套餐保存成功'
+      }));
+    } catch (error) {
+      console.error('[Admin] 保存充值套餐失败:', error);
+      res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
+      res.end(JSON.stringify({
+        success: false,
+        message: '保存失败: ' + error.message
+      }));
+    }
+  });
+}
+
+// 获取商品定价设置
+async function handleGetProductPricingSettings(req, res) {
+  try {
+    let products = [
+      { id: 'vip-matting', name: 'VIP抠图', desc: '使用VIP服务进行图片抠图', price: 0 },
+      { id: 'ai-generate', name: 'AI生图', desc: '使用AI生成图片', price: 0 }
+    ];
+    
+    try {
+      const data = await fs.promises.readFile(PRODUCT_PRICING_FILE, 'utf-8');
+      const parsed = JSON.parse(data);
+      if (parsed.products && Array.isArray(parsed.products)) {
+        // 合并服务器保存的价格
+        parsed.products.forEach(serverProduct => {
+          const localProduct = products.find(p => p.id === serverProduct.id);
+          if (localProduct) {
+            localProduct.price = serverProduct.price || 0;
+          }
+        });
+      }
+    } catch (err) {
+      // 文件不存在,使用默认值
+    }
+    
+    res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
+    res.end(JSON.stringify({
+      success: true,
+      products
+    }));
+  } catch (error) {
+    console.error('[Admin] 获取商品定价失败:', error);
+    res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
+    res.end(JSON.stringify({
+      success: false,
+      message: '获取设置失败: ' + error.message
+    }));
+  }
+}
+
+// 保存商品定价设置
+async function handleSaveProductPricingSettings(req, res) {
+  let body = '';
+  req.on('data', chunk => { body += chunk.toString(); });
+  req.on('end', async () => {
+    try {
+      const data = JSON.parse(body);
+      const { productId, price } = data;
+      
+      if (!productId || price === undefined) {
+        res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
+        res.end(JSON.stringify({
+          success: false,
+          message: '缺少必要参数'
+        }));
+        return;
+      }
+      
+      // 读取现有设置
+      let products = [
+        { id: 'vip-matting', name: 'VIP抠图', desc: '使用VIP服务进行图片抠图', price: 0 },
+        { id: 'ai-generate', name: 'AI生图', desc: '使用AI生成图片', price: 0 }
+      ];
+      
+      try {
+        const fileData = await fs.promises.readFile(PRODUCT_PRICING_FILE, 'utf-8');
+        const parsed = JSON.parse(fileData);
+        if (parsed.products && Array.isArray(parsed.products)) {
+          products = parsed.products;
+        }
+      } catch (err) {
+        // 文件不存在,使用默认值
+      }
+      
+      // 更新指定商品的价格
+      const product = products.find(p => p.id === productId);
+      if (product) {
+        product.price = parseFloat(price) || 0;
+      } else {
+        res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
+        res.end(JSON.stringify({
+          success: false,
+          message: '商品不存在'
+        }));
+        return;
+      }
+      
+      await fs.promises.writeFile(PRODUCT_PRICING_FILE, JSON.stringify({ products }, null, 2), 'utf-8');
+      
+      res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
+      res.end(JSON.stringify({
+        success: true,
+        message: '价格保存成功'
+      }));
+    } catch (error) {
+      console.error('[Admin] 保存商品定价失败:', error);
+      res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
+      res.end(JSON.stringify({
+        success: false,
+        message: '保存失败: ' + error.message
+      }));
+    }
+  });
+}
+
+module.exports = {
+  handleGetAllUsers,
+  handleAdminUpdateUser,
+  handleAdminUploadStore,
+  handleAdminDeleteStore,
+  handleAdminCreateFolder,
+  handleAdminRenameStore,
+  handleAdminUpdatePrice,
+  handleGetCurrencySettings,
+  handleSaveCurrencySettings,
+  handleGetProductPricingSettings,
+  handleSaveProductPricingSettings
+};
+
+

+ 52 - 0
server/ai-history.json

@@ -0,0 +1,52 @@
+{
+  "yichael": [
+    {
+      "id": "ai_1765512156809_28c4ygq1f",
+      "status": "completed",
+      "createdAt": "2025-12-12T04:02:36.809Z",
+      "imageUrl": "/api/ai/image?username=yichael&id=ai_1765512156809_28c4ygq1f",
+      "previewUrl": "/api/ai/preview?username=yichael&id=ai_1765512156809_28c4ygq1f",
+      "completedAt": "2025-12-12T04:03:52.085Z"
+    },
+    {
+      "id": "ai_1765512132695_j542svujh",
+      "status": "completed",
+      "createdAt": "2025-12-12T04:02:12.695Z",
+      "imageUrl": "/api/ai/image?username=yichael&id=ai_1765512132695_j542svujh",
+      "previewUrl": "/api/ai/preview?username=yichael&id=ai_1765512132695_j542svujh",
+      "completedAt": "2025-12-12T04:02:57.261Z"
+    },
+    {
+      "id": "ai_1765457698107_d9omozrg4",
+      "status": "completed",
+      "createdAt": "2025-12-11T12:54:58.107Z",
+      "imageUrl": "/api/ai/image?username=yichael&id=ai_1765457698107_d9omozrg4",
+      "previewUrl": "/api/ai/preview?username=yichael&id=ai_1765457698107_d9omozrg4",
+      "completedAt": "2025-12-11T12:55:38.184Z"
+    },
+    {
+      "id": "ai_1765457454382_wrgbo775q",
+      "status": "completed",
+      "createdAt": "2025-12-11T12:50:54.382Z",
+      "imageUrl": "/api/ai/image?username=yichael&id=ai_1765457454382_wrgbo775q",
+      "previewUrl": "/api/ai/preview?username=yichael&id=ai_1765457454382_wrgbo775q",
+      "completedAt": "2025-12-11T12:51:59.817Z"
+    },
+    {
+      "id": "ai_1765456075299_j9q1wps79",
+      "status": "completed",
+      "createdAt": "2025-12-11T12:27:55.299Z",
+      "imageUrl": "/api/ai/image?username=yichael&id=ai_1765456075299_j9q1wps79",
+      "previewUrl": "/api/ai/preview?username=yichael&id=ai_1765456075299_j9q1wps79",
+      "completedAt": "2025-12-11T12:28:57.642Z"
+    },
+    {
+      "id": "ai_1765455949444_acbvg2d41",
+      "status": "completed",
+      "createdAt": "2025-12-11T12:25:49.444Z",
+      "imageUrl": "/api/ai/image?username=yichael&id=ai_1765455949444_acbvg2d41",
+      "previewUrl": "/api/ai/preview?username=yichael&id=ai_1765455949444_acbvg2d41",
+      "completedAt": "2025-12-11T12:26:50.101Z"
+    }
+  ]
+}

+ 554 - 0
server/ai-queue.js

@@ -0,0 +1,554 @@
+// AI生图队列管理器
+const fs = require('fs');
+const path = require('path');
+const ReplaceCharacterHandler = require('./replace-character');
+
+const AI_HISTORY_FILE = path.join(__dirname, 'ai-history.json');
+const QUEUE_FILE = path.join(__dirname, 'ai-queue.json');
+
+// 队列状态
+let queue = [];
+let isProcessing = false;
+
+// 初始化:加载队列
+function initQueue() {
+  try {
+    if (fs.existsSync(QUEUE_FILE)) {
+      const data = fs.readFileSync(QUEUE_FILE, 'utf-8');
+      queue = JSON.parse(data);
+    }
+  } catch (error) {
+    console.error('[AIQueue] 加载队列失败:', error);
+    queue = [];
+  }
+}
+
+// 保存队列
+function saveQueue() {
+  try {
+    fs.writeFileSync(QUEUE_FILE, JSON.stringify(queue, null, 2), 'utf-8');
+  } catch (error) {
+    console.error('[AIQueue] 保存队列失败:', error);
+  }
+}
+
+// 加载AI历史
+function loadAIHistory() {
+  try {
+    if (fs.existsSync(AI_HISTORY_FILE)) {
+      const data = fs.readFileSync(AI_HISTORY_FILE, 'utf-8');
+      return JSON.parse(data);
+    }
+  } catch (error) {
+    console.error('[AIQueue] 加载历史失败:', error);
+  }
+  return {};
+}
+
+// 保存AI历史
+function saveAIHistory(history) {
+  try {
+    fs.writeFileSync(AI_HISTORY_FILE, JSON.stringify(history, null, 2), 'utf-8');
+  } catch (error) {
+    console.error('[AIQueue] 保存历史失败:', error);
+  }
+}
+
+// 添加任务到队列
+function addToQueue(username, taskData) {
+  const taskId = `ai_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+  const task = {
+    id: taskId,
+    username: username.toLowerCase(),
+    status: queue.length === 0 && !isProcessing ? 'rendering' : 'queued',
+    createdAt: new Date().toISOString(),
+    ...taskData
+  };
+  
+  queue.push(task);
+  saveQueue();
+  
+  // 保存原始图片预览(用于模糊显示)
+  let previewUrl = null;
+  if (taskData.image1) {
+    previewUrl = savePreviewImage(username, taskId, taskData.image1);
+  }
+  
+  // 添加到历史记录
+  const history = loadAIHistory();
+  if (!history[task.username]) {
+    history[task.username] = [];
+  }
+  history[task.username].unshift({
+    id: taskId,
+    status: task.status,
+    createdAt: task.createdAt,
+    imageUrl: null,
+    previewUrl: previewUrl // 原始图片预览
+  });
+  saveAIHistory(history);
+  
+  // 如果队列为空且没有正在处理的任务,立即开始处理
+  if (queue.length === 1 && !isProcessing) {
+    processQueue();
+  }
+  
+  return taskId;
+}
+
+// 保存预览图片(原始texture的缩略图)
+function savePreviewImage(username, taskId, imageBase64) {
+  const usersDir = path.join(__dirname, 'users');
+  const userDir = path.join(usersDir, username.toLowerCase());
+  const aiDir = path.join(userDir, 'ai-images');
+  
+  // 确保目录存在
+  if (!fs.existsSync(aiDir)) {
+    fs.mkdirSync(aiDir, { recursive: true });
+  }
+  
+  const imagePath = path.join(aiDir, `${taskId}_preview.png`);
+  const imageBuffer = Buffer.from(imageBase64.replace(/^data:image\/\w+;base64,/, ''), 'base64');
+  fs.writeFileSync(imagePath, imageBuffer);
+  
+  return `/api/ai/preview?username=${encodeURIComponent(username)}&id=${encodeURIComponent(taskId)}`;
+}
+
+// 处理队列
+async function processQueue() {
+  if (isProcessing || queue.length === 0) {
+    return;
+  }
+  
+  isProcessing = true;
+  
+  while (queue.length > 0) {
+    const task = queue[0];
+    
+    // 更新状态为rendering
+    if (task.status === 'queued') {
+      task.status = 'rendering';
+      updateTaskStatus(task.id, 'rendering');
+    }
+    
+    try {
+      console.log(`[AIQueue] 开始处理任务: ${task.id}`);
+      
+      // 调用Gemini API
+      const result = await callGeminiAPIWithPromise(
+        task.image1,
+        task.image2,
+        task.image1Width,
+        task.image1Height,
+        task.additionalPrompt || ''
+      );
+      
+      if (result.success && result.imageData) {
+        // 保存图片到用户目录
+        const imageUrl = await saveAIImage(task.username, task.id, result.imageData);
+        
+        // 更新任务状态
+        task.status = 'completed';
+        task.imageUrl = imageUrl;
+        task.completedAt = new Date().toISOString();
+        updateTaskStatus(task.id, 'completed', imageUrl);
+        
+        console.log(`[AIQueue] 任务完成: ${task.id}`);
+      } else {
+        // Gemini API 明确返回失败
+        const apiError = new Error(result.error || '生成失败');
+        apiError.isApiError = true;
+        throw apiError;
+      }
+    } catch (error) {
+      console.error(`[AIQueue] 任务失败: ${task.id}`, error);
+      
+      // 只有 Gemini API 明确返回失败时才标记为 failed
+      if (error.isApiError) {
+        task.status = 'failed';
+        task.error = error.message;
+        task.completedAt = new Date().toISOString();
+        // 保存原始任务数据用于重试(不包含图片数据以节省空间,重试时从预览图重新加载)
+        updateTaskStatus(task.id, 'failed', null, error.message, {
+          image1Width: task.image1Width,
+          image1Height: task.image1Height,
+          additionalPrompt: task.additionalPrompt
+        });
+      } else {
+        // 其他错误(代码错误、网络错误等)自动重试
+        console.log(`[AIQueue] 非API错误,将任务重新加入队列末尾: ${task.id}`);
+        task.status = 'queued';
+        task.retryCount = (task.retryCount || 0) + 1;
+        
+        // 最多重试3次
+        if (task.retryCount <= 3) {
+          queue.push({ ...task });
+          updateTaskStatus(task.id, 'queued');
+        } else {
+          console.error(`[AIQueue] 任务重试次数超过限制,标记为失败: ${task.id}`);
+          task.status = 'failed';
+          task.error = '多次重试失败:' + error.message;
+          task.completedAt = new Date().toISOString();
+          updateTaskStatus(task.id, 'failed', null, task.error, {
+            image1Width: task.image1Width,
+            image1Height: task.image1Height,
+            additionalPrompt: task.additionalPrompt
+          });
+        }
+      }
+    }
+    
+    // 从队列中移除
+    queue.shift();
+    saveQueue();
+  }
+  
+  isProcessing = false;
+}
+
+// 调用Gemini API(Promise版本)
+function callGeminiAPIWithPromise(image1Base64, image2Base64, image1Width, image1Height, additionalPrompt) {
+  return new Promise((resolve, reject) => {
+    const https = require('https');
+    
+    // 移除 data:image/png;base64, 前缀(如果有)
+    const cleanImage1 = image1Base64.replace(/^data:image\/\w+;base64,/, '');
+    const cleanImage2 = image2Base64.replace(/^data:image\/\w+;base64,/, '');
+    
+    // 构建请求内容
+    const content = [
+      {
+        type: "image_url",
+        image_url: {
+          url: `data:image/png;base64,${cleanImage1}`
+        }
+      },
+      {
+        type: "image_url",
+        image_url: {
+          url: `data:image/png;base64,${cleanImage2}`
+        }
+      },
+      {
+        type: "text",
+        text: ReplaceCharacterHandler.buildPromptText(image1Width, image1Height, additionalPrompt)
+      }
+    ];
+    
+    const requestData = JSON.stringify({
+      model: "gemini-3-pro-image-preview",
+      messages: [{ role: "user", content: content }]
+    });
+    
+    const options = {
+      hostname: 'api.chatanywhere.tech',
+      port: 443,
+      path: '/v1/chat/completions',
+      method: 'POST',
+      headers: {
+        'Authorization': 'Bearer sk-j32LgDixK6pfESYGfJtgc2Tzlmszx5NZhSH0sOzpLQkYuKek',
+        'Content-Type': 'application/json',
+        'Content-Length': Buffer.byteLength(requestData)
+      },
+      timeout: 300000 // 5分钟超时
+    };
+    
+    console.log('[AIQueue] 正在调用 Gemini API...');
+    
+    const geminiReq = https.request(options, (geminiRes) => {
+      let responseData = '';
+      
+      geminiRes.on('data', (chunk) => {
+        responseData += chunk;
+      });
+      
+      geminiRes.on('end', () => {
+        try {
+          if (geminiRes.statusCode !== 200) {
+            console.error('[AIQueue] Gemini API 返回错误:', geminiRes.statusCode, responseData);
+            reject(new Error(`Gemini API error: ${geminiRes.statusCode}`));
+            return;
+          }
+          
+          const response = JSON.parse(responseData);
+          
+          // 解析响应,提取图片
+          const imageData = ReplaceCharacterHandler.extractImageFromResponse(response);
+          
+          if (!imageData) {
+            console.error('[AIQueue] 无法从响应中提取图片');
+            reject(new Error('Failed to extract image from response'));
+            return;
+          }
+          
+          resolve({
+            success: true,
+            imageData: imageData
+          });
+          
+          console.log('[AIQueue] ✓ 成功处理请求');
+        } catch (error) {
+          console.error('[AIQueue] 解析响应失败:', error);
+          reject(error);
+        }
+      });
+    });
+    
+    geminiReq.on('error', (error) => {
+      console.error('[AIQueue] 请求错误:', error);
+      reject(error);
+    });
+    
+    geminiReq.on('timeout', () => {
+      console.error('[AIQueue] 请求超时');
+      geminiReq.destroy();
+      reject(new Error('Request timeout'));
+    });
+    
+    geminiReq.write(requestData);
+    geminiReq.end();
+  });
+}
+
+// 保存AI生成的图片
+function saveAIImage(username, taskId, imageBase64) {
+  const usersDir = path.join(__dirname, 'users');
+  const userDir = path.join(usersDir, username.toLowerCase());
+  const aiDir = path.join(userDir, 'ai-images');
+  
+  // 确保目录存在
+  if (!fs.existsSync(aiDir)) {
+    fs.mkdirSync(aiDir, { recursive: true });
+  }
+  
+  const imagePath = path.join(aiDir, `${taskId}.png`);
+  const imageBuffer = Buffer.from(imageBase64, 'base64');
+  fs.writeFileSync(imagePath, imageBuffer);
+  
+  return `/api/ai/image?username=${encodeURIComponent(username)}&id=${encodeURIComponent(taskId)}`;
+}
+
+// 更新任务状态
+function updateTaskStatus(taskId, status, imageUrl = null, error = null, retryData = null) {
+  const history = loadAIHistory();
+  
+  for (const username in history) {
+    const userHistory = history[username];
+    const task = userHistory.find(t => t.id === taskId);
+    if (task) {
+      task.status = status;
+      if (imageUrl) {
+        task.imageUrl = imageUrl;
+      }
+      if (error) {
+        task.error = error;
+      }
+      if (retryData) {
+        task.retryData = retryData; // 保存重试所需数据
+      }
+      if (status === 'completed' || status === 'failed') {
+        task.completedAt = new Date().toISOString();
+      }
+      break;
+    }
+  }
+  
+  saveAIHistory(history);
+}
+
+// 获取用户AI历史
+function getUserAIHistory(username) {
+  const history = loadAIHistory();
+  const normalizedUsername = username.toLowerCase();
+  return history[normalizedUsername] || [];
+}
+
+// 处理AI生图请求(队列版本)
+function handleAIRequest(req, res) {
+  if (req.method !== 'POST') {
+    res.writeHead(405, { 'Content-Type': 'application/json' });
+    res.end(JSON.stringify({ error: 'Method not allowed' }));
+    return;
+  }
+
+  let body = '';
+  
+  req.on('data', (chunk) => {
+    body += chunk.toString();
+  });
+
+  req.on('end', () => {
+    try {
+      const data = JSON.parse(body);
+      const { username, image1, image2, image1Width, image1Height, additionalPrompt } = data;
+
+      if (!username) {
+        res.writeHead(400, { 'Content-Type': 'application/json' });
+        res.end(JSON.stringify({ success: false, error: '缺少用户名参数' }));
+        return;
+      }
+
+      if (!image1 || !image2) {
+        res.writeHead(400, { 'Content-Type': 'application/json' });
+        res.end(JSON.stringify({ success: false, error: 'Missing required fields: image1, image2' }));
+        return;
+      }
+
+      if (!image1Width || !image1Height) {
+        res.writeHead(400, { 'Content-Type': 'application/json' });
+        res.end(JSON.stringify({ success: false, error: 'Missing required fields: image1Width, image1Height' }));
+        return;
+      }
+
+      // 添加到队列
+      const taskId = addToQueue(username, {
+        image1,
+        image2,
+        image1Width,
+        image1Height,
+        additionalPrompt: additionalPrompt || ''
+      });
+
+      // 立即返回任务ID
+      res.writeHead(200, { 'Content-Type': 'application/json' });
+      res.end(JSON.stringify({
+        success: true,
+        taskId: taskId,
+        message: '请求生图成功,正在处理中...'
+      }));
+
+      // 异步处理队列
+      processQueue();
+    } catch (error) {
+      console.error('[AIQueue] 处理请求失败:', error);
+      res.writeHead(500, { 'Content-Type': 'application/json' });
+      res.end(JSON.stringify({ success: false, error: '处理失败', details: error.message }));
+    }
+  });
+
+  req.on('error', (error) => {
+    console.error('[AIQueue] 请求错误:', error);
+    res.writeHead(500, { 'Content-Type': 'application/json' });
+    res.end(JSON.stringify({ success: false, error: 'Request error', details: error.message }));
+  });
+}
+
+// 重试失败的任务(免费)
+function retryTask(taskId, username) {
+  const history = loadAIHistory();
+  const userHistory = history[username.toLowerCase()];
+  
+  if (!userHistory) {
+    return { success: false, error: '用户历史不存在' };
+  }
+  
+  const task = userHistory.find(t => t.id === taskId);
+  if (!task) {
+    return { success: false, error: '任务不存在' };
+  }
+  
+  if (task.status !== 'failed') {
+    return { success: false, error: '只能重试失败的任务' };
+  }
+  
+  // 读取预览图作为 image1
+  const previewPath = path.join(__dirname, 'users', username.toLowerCase(), 'ai-images', `${taskId}_preview.png`);
+  if (!fs.existsSync(previewPath)) {
+    return { success: false, error: '预览图不存在,无法重试' };
+  }
+  
+  const previewData = fs.readFileSync(previewPath);
+  const image1Base64 = previewData.toString('base64');
+  
+  // 生成新任务ID
+  const newTaskId = `ai_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+  
+  // 获取重试数据
+  const retryData = task.retryData || {};
+  
+  // 创建新任务
+  const newTask = {
+    id: newTaskId,
+    username: username.toLowerCase(),
+    status: queue.length === 0 && !isProcessing ? 'rendering' : 'queued',
+    createdAt: new Date().toISOString(),
+    image1: image1Base64,
+    image2: image1Base64, // 重试时使用同一张图
+    image1Width: retryData.image1Width || 512,
+    image1Height: retryData.image1Height || 512,
+    additionalPrompt: retryData.additionalPrompt || '',
+    isRetry: true,
+    originalTaskId: taskId
+  };
+  
+  queue.push(newTask);
+  saveQueue();
+  
+  // 保存预览图到新任务
+  const newPreviewPath = path.join(__dirname, 'users', username.toLowerCase(), 'ai-images', `${newTaskId}_preview.png`);
+  fs.copyFileSync(previewPath, newPreviewPath);
+  
+  // 添加到历史记录
+  userHistory.unshift({
+    id: newTaskId,
+    status: newTask.status,
+    createdAt: newTask.createdAt,
+    imageUrl: null,
+    previewUrl: `/api/ai/preview?username=${encodeURIComponent(username)}&id=${encodeURIComponent(newTaskId)}`,
+    isRetry: true
+  });
+  
+  // 将原任务标记为已重试
+  task.retried = true;
+  task.retriedTaskId = newTaskId;
+  
+  saveAIHistory(history);
+  
+  // 开始处理队列
+  if (queue.length === 1 && !isProcessing) {
+    processQueue();
+  }
+  
+  return { success: true, taskId: newTaskId };
+}
+
+// 处理重试请求
+function handleRetryRequest(req, res) {
+  let body = '';
+  
+  req.on('data', chunk => {
+    body += chunk.toString();
+  });
+  
+  req.on('end', () => {
+    try {
+      const { taskId, username } = JSON.parse(body);
+      
+      if (!taskId || !username) {
+        res.writeHead(400, { 'Content-Type': 'application/json' });
+        res.end(JSON.stringify({ success: false, error: '缺少必要参数' }));
+        return;
+      }
+      
+      const result = retryTask(taskId, username);
+      
+      res.writeHead(result.success ? 200 : 400, { 'Content-Type': 'application/json' });
+      res.end(JSON.stringify(result));
+    } catch (error) {
+      console.error('[AIQueue] 重试请求失败:', error);
+      res.writeHead(500, { 'Content-Type': 'application/json' });
+      res.end(JSON.stringify({ success: false, error: '处理失败', details: error.message }));
+    }
+  });
+}
+
+// 初始化
+initQueue();
+
+module.exports = {
+  handleAIRequest,
+  handleRetryRequest,
+  getUserAIHistory,
+  processQueue
+};
+

+ 1 - 0
server/ai-queue.json

@@ -0,0 +1 @@
+[]

+ 19 - 0
server/currency-settings.json

@@ -0,0 +1,19 @@
+{
+  "packages": [
+    {
+      "points": 100,
+      "bonus": 20,
+      "price": 2
+    },
+    {
+      "points": 1000,
+      "bonus": 200,
+      "price": 50
+    },
+    {
+      "points": 10000,
+      "bonus": 800,
+      "price": 500
+    }
+  ]
+}

BIN
server/data.db


+ 6 - 0
server/market_data/prices.json

@@ -0,0 +1,6 @@
+{
+  "角色/player_0001": 0.1,
+  "角色/player_0002": 0.05,
+  "角色/player_0003": 5,
+  "角色/player_0009": 0.01
+}

BIN
server/market_data/角色/player_0001/01.png


BIN
server/market_data/角色/player_0001/02.png


BIN
server/market_data/角色/player_0001/03.png


BIN
server/market_data/角色/player_0001/04.png


BIN
server/market_data/角色/player_0001/05.png


BIN
server/market_data/角色/player_0001/06.png


BIN
server/market_data/角色/player_0001/07.png


BIN
server/market_data/角色/player_0001/08.png


BIN
server/market_data/角色/player_0001/09.png


BIN
server/market_data/角色/player_0001/10.png


BIN
server/market_data/角色/player_0001/11.png


BIN
server/market_data/角色/player_0001/12.png


BIN
server/market_data/角色/player_0001/13.png


BIN
server/market_data/角色/player_0001/14.png


BIN
server/market_data/角色/player_0002/01.png


BIN
server/market_data/角色/player_0002/02.png


BIN
server/market_data/角色/player_0002/03.png


BIN
server/market_data/角色/player_0002/04.png


BIN
server/market_data/角色/player_0002/05.png


BIN
server/market_data/角色/player_0002/06.png


BIN
server/market_data/角色/player_0002/07.png


BIN
server/market_data/角色/player_0002/08.png


BIN
server/market_data/角色/player_0002/09.png


BIN
server/market_data/角色/player_0002/10.png


BIN
server/market_data/角色/player_0002/11.png


BIN
server/market_data/角色/player_0002/12.png


BIN
server/market_data/角色/player_0002/13.png


BIN
server/market_data/角色/player_0002/14.png


BIN
server/market_data/角色/player_0003/01.png


BIN
server/market_data/角色/player_0003/02.png


BIN
server/market_data/角色/player_0003/03.png


BIN
server/market_data/角色/player_0003/04.png


Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä