User 6 miesięcy temu
commit
e7a81ebec4
100 zmienionych plików z 12246 dodań i 0 usunięć
  1. 1 0
      .gitignore
  2. 25 0
      AnimationManager.bat
  3. 780 0
      client/css/Index.css
  4. 46 0
      client/css/alert-view.css
  5. 86 0
      client/css/assets.css
  6. 115 0
      client/css/confirm-view.css
  7. 482 0
      client/css/disk/disk.css
  8. 42 0
      client/css/disk/right-click-menu.css
  9. 200 0
      client/css/disk/tool-bar.css
  10. 195 0
      client/css/export-view.css
  11. 752 0
      client/css/export-view/export-view.css
  12. 114 0
      client/css/navigation.css
  13. 555 0
      client/css/seq_ani_player/card.css
  14. 339 0
      client/css/seq_ani_player/seq-ani-player.css
  15. 89 0
      client/css/store.css
  16. 43 0
      client/index.html
  17. 210 0
      client/js/Index.js
  18. 0 0
      client/js/animation_character_replace/animation-character-replace.js
  19. 52 0
      client/js/assets.js
  20. 152 0
      client/js/confirm-view.js
  21. 149 0
      client/js/disk/alert.js
  22. 1712 0
      client/js/disk/disk.js
  23. 287 0
      client/js/disk/multiple-selection.js
  24. 144 0
      client/js/disk/path.js
  25. 98 0
      client/js/disk/right-click-menu.js
  26. 115 0
      client/js/disk/search-bar.js
  27. 93 0
      client/js/disk/shortcut-keys.js
  28. 133 0
      client/js/export-view-manager.js
  29. 214 0
      client/js/export-view.js
  30. 861 0
      client/js/export-view/export-view.js
  31. 54 0
      client/js/navigation.js
  32. 710 0
      client/js/seq_ani_player/card.js
  33. 878 0
      client/js/seq_ani_player/seq-ani-player.js
  34. 495 0
      client/js/sprite_sheet_maker/sprite-sheet-maker.js
  35. 8 0
      client/page/alert-overlay.html
  36. 5 0
      client/page/alert-view.html
  37. 25 0
      client/page/assets/assets.html
  38. 13 0
      client/page/confirm-view.html
  39. 94 0
      client/page/disk/disk.html
  40. 22 0
      client/page/disk/right-click-menu.html
  41. 49 0
      client/page/disk/tool-bar.html
  42. 54 0
      client/page/export-view.html
  43. 83 0
      client/page/export/export-view.html
  44. 19 0
      client/page/navigation/navigation.html
  45. 64 0
      client/page/seq_ani_player/card.html
  46. 23 0
      client/page/seq_ani_player/seq-ani-player.html
  47. 22 0
      client/page/store/store.html
  48. BIN
      client/static/favicon.png
  49. BIN
      client/static/logo.png
  50. 1544 0
      server/disk.js
  51. BIN
      server/disk_data/111/生成白色背景长矛刺击动画00.png
  52. BIN
      server/disk_data/111/生成白色背景长矛刺击动画01.png
  53. BIN
      server/disk_data/111/生成白色背景长矛刺击动画02.png
  54. BIN
      server/disk_data/111/生成白色背景长矛刺击动画03.png
  55. BIN
      server/disk_data/111/生成白色背景长矛刺击动画04.png
  56. BIN
      server/disk_data/111/生成白色背景长矛刺击动画05.png
  57. BIN
      server/disk_data/111/生成白色背景长矛刺击动画06.png
  58. BIN
      server/disk_data/111/生成白色背景长矛刺击动画07.png
  59. BIN
      server/disk_data/111/生成白色背景长矛刺击动画08.png
  60. BIN
      server/disk_data/111/生成白色背景长矛刺击动画09.png
  61. BIN
      server/disk_data/111/生成白色背景长矛刺击动画10.png
  62. BIN
      server/disk_data/111/生成白色背景长矛刺击动画11.png
  63. BIN
      server/disk_data/111/生成白色背景长矛刺击动画12.png
  64. BIN
      server/disk_data/111/生成白色背景长矛刺击动画13.png
  65. BIN
      server/disk_data/111/生成白色背景长矛刺击动画14.png
  66. BIN
      server/disk_data/111/生成白色背景长矛刺击动画15.png
  67. BIN
      server/disk_data/111/生成白色背景长矛刺击动画16.png
  68. BIN
      server/disk_data/111/生成白色背景长矛刺击动画17.png
  69. BIN
      server/disk_data/111/生成白色背景长矛刺击动画18.png
  70. BIN
      server/disk_data/111/生成白色背景长矛刺击动画19.png
  71. BIN
      server/disk_data/111/生成白色背景长矛刺击动画20.png
  72. BIN
      server/disk_data/111/生成白色背景长矛刺击动画21.png
  73. BIN
      server/disk_data/player_0001/01.png
  74. BIN
      server/disk_data/player_0001/02.png
  75. BIN
      server/disk_data/player_0001/03.png
  76. BIN
      server/disk_data/player_0001/04.png
  77. BIN
      server/disk_data/player_0001/05.png
  78. BIN
      server/disk_data/player_0001/06.png
  79. BIN
      server/disk_data/player_0001/07.png
  80. BIN
      server/disk_data/player_0001/08.png
  81. BIN
      server/disk_data/player_0001/09.png
  82. BIN
      server/disk_data/player_0001/10.png
  83. BIN
      server/disk_data/player_0001/11.png
  84. BIN
      server/disk_data/player_0001/12.png
  85. BIN
      server/disk_data/player_0001/13.png
  86. BIN
      server/disk_data/player_0001/14.png
  87. BIN
      server/disk_data/test1/生成白色背景长矛刺击动画00.png
  88. BIN
      server/disk_data/test1/生成白色背景长矛刺击动画01.png
  89. BIN
      server/disk_data/test1/生成白色背景长矛刺击动画02.png
  90. BIN
      server/disk_data/test1/生成白色背景长矛刺击动画03.png
  91. BIN
      server/disk_data/test1/生成白色背景长矛刺击动画04.png
  92. BIN
      server/disk_data/test1/生成白色背景长矛刺击动画05.png
  93. BIN
      server/disk_data/新建文件夹 (1)/player_0002/01.png
  94. BIN
      server/disk_data/新建文件夹 (1)/player_0002/02.png
  95. BIN
      server/disk_data/新建文件夹 (1)/player_0002/03.png
  96. BIN
      server/disk_data/新建文件夹 (1)/player_0002/04.png
  97. BIN
      server/disk_data/新建文件夹 (1)/player_0002/05.png
  98. BIN
      server/disk_data/新建文件夹 (1)/player_0002/06.png
  99. BIN
      server/disk_data/新建文件夹 (1)/player_0002/07.png
  100. BIN
      server/disk_data/新建文件夹 (1)/player_0002/08.png

+ 1 - 0
.gitignore

@@ -0,0 +1 @@
+/*.log

+ 25 - 0
AnimationManager.bat

@@ -0,0 +1,25 @@
+@echo off
+title Animation Manager
+chcp 65001 >nul
+
+REM ================关闭旧的 server.js 进程========================
+taskkill /F /IM node.exe >nul 2>&1
+
+REM 等待一下确保进程完全关闭
+timeout /t 1 /nobreak >nul 2>&1
+
+REM ================启动 server.js(后台运行)========================
+start /B node Server/server.js
+
+REM 等待服务器启动
+timeout /t 2 /nobreak >nul 2>&1
+
+REM ================打开浏览器访问 index.html========================
+start http://localhost:3000/index.html?auto=true
+
+echo 服务已启动!浏览器将自动打开...
+echo 按 Ctrl+C 或关闭此窗口停止服务器
+pause >nul
+
+REM 关闭 node 服务
+taskkill /F /IM node.exe >nul 2>&1

+ 780 - 0
client/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);
+}

+ 46 - 0
client/css/alert-view.css

@@ -0,0 +1,46 @@
+/* 全局文字提示 */
+.global-alert {
+    position: fixed;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%) scale(0.8);
+    z-index: 99999;
+    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);
+}
+
+.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);
+}
+
+@keyframes alert-bounce-in {
+    0% {
+        transform: translate(-50%, -50%) scale(0.8);
+        opacity: 0;
+    }
+    50% {
+        transform: translate(-50%, -50%) scale(1.05);
+    }
+    100% {
+        transform: translate(-50%, -50%) scale(1);
+        opacity: 1;
+    }
+}
+

+ 86 - 0
client/css/assets.css

@@ -0,0 +1,86 @@
+/* 我的资产页面样式 */
+
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+}
+
+body {
+  margin: 0;
+  padding: 0;
+  height: 100vh;
+  overflow: hidden;
+  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial,
+    "Noto Sans", sans-serif;
+}
+
+.assets-root {
+  display: flex;
+  flex-direction: row;
+  width: 100%;
+  height: 100vh;
+  background: linear-gradient(135deg, #fdfcfb 0%, #f7f7f8 50%, #fdfcfb 100%);
+  overflow: hidden;
+}
+
+/* 左侧:网盘管理 */
+.assets-left {
+  flex: 0 0 400px;
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  background: #ffffff;
+  border-right: 2px solid rgba(139, 92, 246, 0.1);
+  box-shadow: 2px 0 16px rgba(0, 0, 0, 0.05);
+  overflow: hidden;
+}
+
+.assets-left iframe {
+  width: 100%;
+  height: 100%;
+  border: none;
+  display: block;
+}
+
+/* 右侧:动画预览器 */
+.assets-right {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  overflow: hidden;
+  min-width: 0;
+}
+
+.assets-right iframe {
+  width: 100%;
+  height: 100%;
+  border: none;
+  display: block;
+}
+
+/* 响应式设计 */
+@media (max-width: 1200px) {
+  .assets-left {
+    flex: 0 0 350px;
+  }
+}
+
+@media (max-width: 768px) {
+  .assets-root {
+    flex-direction: column;
+  }
+
+  .assets-left {
+    flex: 0 0 50%;
+    border-right: none;
+    border-bottom: 2px solid rgba(139, 92, 246, 0.1);
+    box-shadow: 0 2px 16px rgba(0, 0, 0, 0.05);
+  }
+
+  .assets-right {
+    flex: 0 0 50%;
+  }
+}
+

+ 115 - 0
client/css/confirm-view.css

@@ -0,0 +1,115 @@
+.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);
+}
+

+ 482 - 0
client/css/disk/disk.css

@@ -0,0 +1,482 @@
+/* 全局样式 */
+* {
+    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;
+}
+
+.file-item:hover {
+    background: #f5f5f5;
+}
+
+.file-item.selected {
+    background: #e6f7ff;
+}
+
+.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.selected {
+    background: rgba(82, 196, 26, 0.1);
+}
+
+.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;
+}
+
+.file-item {
+    position: relative;
+}
+
+/* 框选区域 */
+.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;
+    }
+}
+

+ 42 - 0
client/css/disk/right-click-menu.css

@@ -0,0 +1,42 @@
+.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;
+}

+ 200 - 0
client/css/disk/tool-bar.css

@@ -0,0 +1,200 @@
+/* 网盘工具栏样式 */
+
+/* 头部 */
+.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;
+}
+

+ 195 - 0
client/css/export-view.css

@@ -0,0 +1,195 @@
+body {
+    margin: 0;
+    padding: 0;
+    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif;
+    background: transparent;
+    overflow: hidden;
+}
+
+.export-container {
+    width: 100%;
+    height: 100vh;
+    display: flex;
+    flex-direction: column;
+    background: white;
+    border-radius: 16px;
+    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+    max-width: 900px;
+    max-height: 80vh;
+    margin: 0 auto;
+}
+
+.export-header {
+    padding: 24px 32px;
+    border-bottom: 1px solid #e5e7eb;
+}
+
+.export-header h2 {
+    margin: 0;
+    font-size: 20px;
+    font-weight: 600;
+    color: #1f2937;
+}
+
+.export-content {
+    flex: 1;
+    padding: 24px 32px;
+    overflow-y: auto;
+    display: flex;
+    flex-direction: column;
+    gap: 24px;
+}
+
+.export-section h3 {
+    margin: 0 0 12px 0;
+    font-size: 15px;
+    font-weight: 600;
+    color: #374151;
+}
+
+/* Spritesheet 预览 */
+.preview-box {
+    width: 100%;
+    min-height: 300px;
+    background: repeating-conic-gradient(#f3f4f6 0% 25%, white 0% 50%) 50% / 20px 20px;
+    border: 2px solid #e5e7eb;
+    border-radius: 8px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    position: relative;
+    overflow: auto;
+}
+
+#spritesheetCanvas {
+    max-width: 100%;
+    max-height: 100%;
+    display: none;
+}
+
+#spritesheetCanvas.show {
+    display: block;
+}
+
+.preview-placeholder {
+    position: absolute;
+    color: #9ca3af;
+    font-size: 15px;
+}
+
+.preview-placeholder.hide {
+    display: none;
+}
+
+/* 参考图上传 */
+.reference-upload {
+    width: 100%;
+}
+
+.reference-preview {
+    width: 100%;
+    height: 200px;
+    border: 2px dashed #d1d5db;
+    border-radius: 8px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    cursor: pointer;
+    transition: all 0.2s;
+    background: #f9fafb;
+}
+
+.reference-preview:hover {
+    border-color: #9ca3af;
+    background: #f3f4f6;
+}
+
+.reference-preview.has-image {
+    border-style: solid;
+    border-color: #667eea;
+    background: white;
+}
+
+.upload-hint {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    gap: 8px;
+    color: #9ca3af;
+}
+
+.upload-hint svg {
+    color: #d1d5db;
+}
+
+.upload-hint p {
+    margin: 0;
+    font-size: 14px;
+}
+
+.reference-preview img {
+    max-width: 100%;
+    max-height: 100%;
+    object-fit: contain;
+}
+
+/* 操作按钮 */
+.export-actions {
+    padding: 20px 32px;
+    border-top: 1px solid #e5e7eb;
+    display: flex;
+    gap: 12px;
+    justify-content: flex-end;
+}
+
+.export-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: 100px;
+}
+
+.export-cancel {
+    background: #f3f4f6;
+    color: #6b7280;
+    border: 1px solid #e5e7eb;
+}
+
+.export-cancel:hover {
+    background: #e5e7eb;
+    color: #4b5563;
+    border-color: #d1d5db;
+}
+
+.export-cancel:active {
+    transform: scale(0.98);
+}
+
+.export-confirm {
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    color: white;
+    box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
+}
+
+.export-confirm:hover {
+    box-shadow: 0 6px 16px rgba(102, 126, 234, 0.5);
+    transform: translateY(-1px);
+}
+
+.export-confirm:active {
+    transform: translateY(0) scale(0.98);
+}
+
+.export-confirm:disabled {
+    background: #d1d5db;
+    color: #9ca3af;
+    box-shadow: none;
+    cursor: not-allowed;
+    transform: none;
+}
+

+ 752 - 0
client/css/export-view/export-view.css

@@ -0,0 +1,752 @@
+/* 确保 body 和 html 占满整个 iframe */
+html, body {
+    margin: 0;
+    padding: 0;
+    width: 100%;
+    height: 100%;
+    overflow: hidden;
+}
+
+/* 弹出框遮罩层 */
+.export-overlay {
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    width: 100%;
+    height: 100%;
+    background: rgba(0, 0, 0, 0.7);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    z-index: 999999;
+    backdrop-filter: blur(4px);
+    animation: overlayFadeIn 0.3s ease-out;
+    margin: 0;
+    padding: 0;
+}
+
+/* 关闭按钮(在弹窗右上角) */
+.modal-close-btn {
+    position: absolute;
+    width: 44px;
+    height: 44px;
+    border-radius: 50%;
+    background: white;
+    border: 2px solid #e5e7eb;
+    color: #6b7280;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    transition: background-color 0.2s, color 0.2s, border-color 0.2s, box-shadow 0.2s, transform 0.2s;
+    z-index: 10;
+    padding: 0;
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+    top: -22px;
+    right: -22px;
+}
+
+.modal-close-btn:hover {
+    background: #f9fafb;
+    color: #1f2937;
+    border-color: #d1d5db;
+    transform: scale(1.1);
+    box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
+}
+
+.modal-close-btn:active {
+    transform: scale(0.95);
+}
+
+.modal-close-btn svg {
+    width: 20px;
+    height: 20px;
+}
+
+@keyframes overlayFadeIn {
+    from {
+        background: rgba(0, 0, 0, 0);
+        backdrop-filter: blur(0);
+    }
+    to {
+        background: rgba(0, 0, 0, 0.7);
+        backdrop-filter: blur(4px);
+    }
+}
+
+/* 弹出框容器 */
+.export-modal {
+    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: 800px;
+    max-width: 1200px;
+    width: 90%;
+    max-height: 90vh;
+    display: flex;
+    flex-direction: column;
+    animation: modalSlideIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
+    overflow: visible;
+    position: relative;
+    z-index: 1000000;
+    margin: auto;
+}
+
+@keyframes modalSlideIn {
+    from {
+        opacity: 0;
+        transform: translateY(-30px) scale(0.95);
+    }
+    to {
+        opacity: 1;
+        transform: translateY(0) scale(1);
+    }
+}
+
+/* 图片预览容器 - 左右两个图框 */
+.export-preview-container {
+    width: 100%;
+    min-height: 500px;
+    max-height: 70vh;
+    display: flex;
+    gap: 32px;
+    padding: 32px;
+    box-sizing: border-box;
+    background: #fafbfc;
+}
+
+/* 预览图框 - 左右各占一半 */
+.export-preview-box {
+    flex: 1;
+    min-height: 400px;
+    background: repeating-conic-gradient(#f3f4f6 0% 25%, white 0% 50%) 50% / 20px 20px;
+    border: 2px solid #e5e7eb;
+    border-radius: 12px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    position: relative;
+    overflow: auto;
+    box-sizing: border-box;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
+    transition: box-shadow 0.2s;
+}
+
+.export-preview-box:hover {
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
+}
+
+/* 右侧 Spritesheet 区域容器 */
+.export-spritesheet-section {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+}
+
+/* 左侧:参考图上传区域 */
+.export-reference-section {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+}
+
+.export-reference-box {
+    cursor: pointer;
+    transition: border-color 0.2s;
+    flex: 1;
+}
+
+.export-reference-box:hover {
+    border-color: #667eea;
+}
+
+.reference-upload-area {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    gap: 12px;
+    color: #9ca3af;
+    pointer-events: none;
+    width: 100%;
+    height: 100%;
+    position: absolute;
+    top: 0;
+    left: 0;
+}
+
+.reference-upload-area.hide {
+    display: none;
+}
+
+.upload-icon {
+    font-size: 48px;
+    font-weight: 300;
+    color: #d1d5db;
+    line-height: 1;
+}
+
+.upload-text {
+    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;
+}
+
+.reference-remove-btn {
+    position: absolute;
+    top: 12px;
+    right: 12px;
+    width: 32px;
+    height: 32px;
+    border-radius: 50%;
+    background: rgba(0, 0, 0, 0.6);
+    backdrop-filter: blur(4px);
+    border: none;
+    color: white;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    transition: all 0.2s;
+    z-index: 10;
+    padding: 0;
+}
+
+.reference-remove-btn:hover {
+    background: rgba(239, 68, 68, 0.9);
+    transform: scale(1.1);
+    box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4);
+}
+
+.reference-remove-btn:active {
+    transform: scale(0.95);
+}
+
+.reference-remove-btn svg {
+    width: 16px;
+    height: 16px;
+}
+
+/* 替换按钮 */
+.replace-btn {
+    padding: 10px 24px;
+    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 cubic-bezier(0.4, 0, 0.2, 1);
+    box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
+    align-self: center;
+    min-width: 100px;
+    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif;
+}
+
+.replace-btn:hover {
+    box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
+    transform: translateY(-1px);
+}
+
+.replace-btn:active {
+    transform: translateY(0) scale(0.98);
+}
+
+.replace-btn:disabled {
+    background: #d1d5db;
+    color: #9ca3af;
+    box-shadow: none;
+    cursor: not-allowed;
+    transform: none;
+}
+
+/* 右侧:Spritesheet 预览 */
+.export-spritesheet-box {
+    background: repeating-conic-gradient(#f3f4f6 0% 25%, white 0% 50%) 50% / 20px 20px;
+}
+
+#previewImage {
+    max-width: 100%;
+    max-height: 100%;
+    width: auto;
+    height: auto;
+    object-fit: contain;
+    display: none;
+    margin: auto;
+}
+
+#previewImage.show {
+    display: block;
+}
+
+
+/* 悬浮下载按钮 - 右下角 */
+.floating-download-btn {
+    position: absolute;
+    bottom: 16px;
+    right: 16px;
+    width: 52px;
+    height: 52px;
+    border-radius: 50%;
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    border: none;
+    color: white;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    box-shadow: 
+        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;
+}
+
+.floating-download-btn::before {
+    content: '';
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    width: 0;
+    height: 0;
+    border-radius: 50%;
+    background: rgba(255, 255, 255, 0.2);
+    transform: translate(-50%, -50%);
+    transition: width 0.6s, height 0.6s;
+}
+
+.floating-download-btn:hover::before {
+    width: 200px;
+    height: 200px;
+}
+
+.floating-download-btn:hover {
+    background: linear-gradient(135deg, #7c8ef0 0%, #8a5fb8 100%);
+    box-shadow: 
+        0 12px 32px rgba(102, 126, 234, 0.5),
+        0 6px 16px rgba(0, 0, 0, 0.2);
+    transform: translateY(-3px) scale(1.08);
+}
+
+.floating-download-btn:active {
+    background: linear-gradient(135deg, #5a6fd8 0%, #6a3d92 100%);
+    transform: translateY(-1px) scale(1.03);
+    box-shadow: 
+        0 6px 20px rgba(102, 126, 234, 0.4),
+        0 4px 12px rgba(0, 0, 0, 0.15);
+}
+
+.floating-download-btn:disabled {
+    opacity: 0.4;
+    cursor: not-allowed;
+    transform: none;
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+    background: linear-gradient(135deg, #d1d5db 0%, #9ca3af 100%);
+}
+
+.floating-download-btn svg {
+    width: 22px;
+    height: 22px;
+    flex-shrink: 0;
+    position: relative;
+    z-index: 1;
+    filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
+    transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.floating-download-btn:hover:not(:disabled) svg {
+    transform: translateY(3px);
+}
+
+.floating-download-btn:active:not(:disabled) svg {
+    transform: translateY(5px);
+}
+
+.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;
+}
+
+.loading-text {
+    font-size: 15px;
+    font-weight: 500;
+    color: #6b7280;
+}
+
+@keyframes spin {
+    to {
+        transform: rotate(360deg);
+    }
+}
+
+/* 按钮区域 */
+.export-actions {
+    padding: 20px 32px;
+    display: flex;
+    gap: 12px;
+    justify-content: flex-end;
+    align-items: center;
+    flex-shrink: 0;
+    border-top: 1px solid #e5e7eb;
+    background: white;
+}
+
+.export-btn {
+    padding: 12px 32px;
+    border: none;
+    border-radius: 10px;
+    font-size: 15px;
+    cursor: pointer;
+    transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+    font-weight: 600;
+    letter-spacing: 0.3px;
+    min-width: 120px;
+    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+
+.export-cancel {
+    background: white;
+    color: #374151;
+    border: 2px solid #d1d5db;
+    font-weight: 600;
+}
+
+.export-cancel:hover {
+    background: #f9fafb;
+    color: #1f2937;
+    border-color: #9ca3af;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+    transform: translateY(-1px);
+}
+
+.export-cancel:active {
+    transform: translateY(0) scale(0.98);
+    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
+}
+
+.export-confirm {
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    color: white;
+    box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
+}
+
+.export-confirm:hover {
+    box-shadow: 0 6px 16px rgba(102, 126, 234, 0.5);
+    transform: translateY(-1px);
+}
+
+.export-confirm:active {
+    transform: translateY(0) scale(0.98);
+}
+
+.export-confirm:disabled {
+    background: #d1d5db;
+    color: #9ca3af;
+    box-shadow: none;
+    cursor: not-allowed;
+    transform: none;
+}
+
+.export-confirm svg {
+    flex-shrink: 0;
+    transition: transform 0.2s;
+}
+
+.export-confirm:hover:not(:disabled) svg {
+    transform: translateY(2px);
+}
+
+.export-confirm:active:not(:disabled) svg {
+    transform: translateY(4px);
+}
+
+/* 替换按钮 */
+.replace-btn {
+    padding: 10px 24px;
+    border: none;
+    border-radius: 8px;
+    font-size: 14px;
+    cursor: pointer;
+    transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+    font-weight: 500;
+    letter-spacing: 0.3px;
+    background: linear-gradient(135deg, #10b981 0%, #059669 100%);
+    color: white;
+    box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
+    align-self: center;
+    min-width: 120px;
+    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif;
+}
+
+.replace-btn:hover {
+    box-shadow: 0 6px 16px rgba(16, 185, 129, 0.5);
+    transform: translateY(-1px);
+}
+
+.replace-btn:active {
+    transform: translateY(0) scale(0.98);
+}
+
+.replace-btn:disabled {
+    background: #d1d5db;
+    color: #9ca3af;
+    box-shadow: none;
+    cursor: not-allowed;
+    transform: none;
+}
+
+/* 提示词配置区域 */
+.prompt-config-section {
+    padding: 24px 32px;
+    border-top: 1px solid #e5e7eb;
+    border-bottom: 1px solid #e5e7eb;
+    background: white;
+    display: flex;
+    flex-direction: column;
+    gap: 20px;
+}
+
+.config-row {
+    display: flex;
+    flex-direction: column;
+    gap: 8px;
+}
+
+.config-label-wrapper {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+}
+
+.config-label {
+    font-size: 14px;
+    font-weight: 500;
+    color: #374151;
+}
+
+.download-icon-hint {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 24px;
+    height: 24px;
+    cursor: pointer;
+    opacity: 0.7;
+    transition: all 0.2s;
+    border-radius: 4px;
+    padding: 2px;
+}
+
+.download-icon-hint:hover {
+    opacity: 1;
+    background: rgba(102, 126, 234, 0.1);
+    transform: scale(1.1);
+}
+
+.download-icon-hint svg {
+    width: 20px;
+    height: 20px;
+}
+
+.config-input {
+    padding: 10px 12px;
+    border: 1px solid #d1d5db;
+    border-radius: 6px;
+    font-size: 14px;
+    color: #1f2937;
+    background: white;
+    outline: none;
+    transition: all 0.2s;
+    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif;
+}
+
+.config-input:focus {
+    border-color: #667eea;
+    box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
+}
+
+.config-textarea {
+    padding: 10px 12px;
+    border: 1px solid #d1d5db;
+    border-radius: 6px;
+    font-size: 14px;
+    color: #1f2937;
+    background: white;
+    outline: none;
+    transition: all 0.2s;
+    resize: vertical;
+    min-height: 80px;
+    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif;
+    line-height: 1.5;
+}
+
+.config-textarea:focus {
+    border-color: #667eea;
+    box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
+}
+
+.prompt-actions {
+    display: flex;
+    justify-content: flex-end;
+    align-items: center;
+    gap: 12px;
+    padding-top: 10px;
+}
+
+/* 操作按钮通用样式 */
+.action-btn {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: 8px;
+    padding: 12px 24px;
+    border: none;
+    border-radius: 10px;
+    font-size: 15px;
+    font-weight: 600;
+    cursor: pointer;
+    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif;
+    letter-spacing: 0.3px;
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+    position: relative;
+    overflow: hidden;
+}
+
+.action-btn::before {
+    content: '';
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    width: 0;
+    height: 0;
+    border-radius: 50%;
+    background: rgba(255, 255, 255, 0.3);
+    transform: translate(-50%, -50%);
+    transition: width 0.6s, height 0.6s;
+}
+
+.action-btn:hover::before {
+    width: 300px;
+    height: 300px;
+}
+
+.action-btn:hover {
+    transform: translateY(-2px);
+    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
+}
+
+.action-btn:active {
+    transform: translateY(0) scale(0.98);
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+}
+
+.action-btn:disabled {
+    opacity: 0.5;
+    cursor: not-allowed;
+    transform: none;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.action-btn svg {
+    width: 20px;
+    height: 20px;
+    flex-shrink: 0;
+    transition: transform 0.3s;
+}
+
+.action-btn:hover svg {
+    transform: scale(1.1);
+}
+
+/* 下载按钮 */
+.download-action-btn {
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    color: white;
+    box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
+}
+
+.download-action-btn:hover {
+    box-shadow: 0 6px 20px rgba(102, 126, 234, 0.5);
+    background: linear-gradient(135deg, #7c8ef0 0%, #8a5fb8 100%);
+}
+
+.download-action-btn:active {
+    background: linear-gradient(135deg, #5a6fd8 0%, #6a3d92 100%);
+}
+
+/* 替换按钮 */
+.replace-action-btn {
+    background: linear-gradient(135deg, #10b981 0%, #059669 100%);
+    color: white;
+    box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
+}
+
+.replace-action-btn:hover {
+    box-shadow: 0 6px 20px rgba(16, 185, 129, 0.5);
+    background: linear-gradient(135deg, #34d399 0%, #10b981 100%);
+}
+
+.replace-action-btn:active {
+    background: linear-gradient(135deg, #059669 0%, #047857 100%);
+}
+
+/* 响应式设计 */
+@media (max-width: 900px) {
+    .export-modal {
+        min-width: auto;
+        width: 95%;
+    }
+    
+    .export-preview-container {
+        flex-direction: column;
+    }
+    
+    .export-preview-box {
+        min-height: 300px;
+    }
+}
+

+ 114 - 0
client/css/navigation.css

@@ -0,0 +1,114 @@
+/* 导航栏样式 */
+
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+}
+
+body {
+  margin: 0;
+  padding: 0;
+  overflow: hidden;
+  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%);
+}
+
+/* 顶部导航栏容器 */
+.top-navigation {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  gap: 16px;
+  padding: 24px 20px;
+  background: #ffffff;
+  
+  /* 非常明显的底部边框 - 加粗加深 */
+  border-bottom: 4px solid #8b5cf6;
+  
+  /* 增强的阴影效果 - 多层阴影营造更强的分隔感 */
+  box-shadow: 0 6px 20px rgba(139, 92, 246, 0.25), 
+              0 3px 10px rgba(0, 0, 0, 0.12),
+              0 1px 3px rgba(0, 0, 0, 0.08);
+  
+  /* 添加上边框增强边界感 */
+  border-top: 1px solid rgba(229, 231, 235, 0.8);
+  
+  /* 禁用文字选中 */
+  user-select: none;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  
+  /* 固定定位,确保边框始终可见 */
+  position: relative;
+  z-index: 100;
+}
+
+/* 导航按钮 */
+.nav-btn {
+  padding: 12px 32px;
+  font-size: 15px;
+  font-weight: 600;
+  color: #6b7280;
+  background: linear-gradient(135deg, #ffffff 0%, #f9fafb 100%);
+  border: 2px solid #e5e7eb;
+  border-radius: 12px;
+  cursor: pointer;
+  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+  outline: none;
+  user-select: none;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+  letter-spacing: 0.3px;
+}
+
+.nav-btn:hover {
+  color: #8b5cf6;
+  border-color: #8b5cf6;
+  background: linear-gradient(135deg, #faf5ff 0%, #f3e8ff 100%);
+  transform: translateY(-2px);
+  box-shadow: 0 4px 12px rgba(139, 92, 246, 0.2);
+}
+
+.nav-btn.active {
+  color: #ffffff;
+  background: linear-gradient(135deg, #8b5cf6 0%, #a78bfa 100%);
+  border-color: transparent;
+  box-shadow: 0 4px 16px rgba(139, 92, 246, 0.4), 0 2px 8px rgba(0, 0, 0, 0.1);
+  transform: translateY(-2px);
+}
+
+.nav-btn.active:hover {
+  background: linear-gradient(135deg, #7c3aed 0%, #9333ea 100%);
+  box-shadow: 0 6px 20px rgba(139, 92, 246, 0.5), 0 3px 10px rgba(0, 0, 0, 0.15);
+}
+
+/* 响应式设计 */
+@media (max-width: 768px) {
+  .top-navigation {
+    padding: 16px 12px;
+    gap: 12px;
+  }
+
+  .nav-btn {
+    padding: 10px 24px;
+    font-size: 14px;
+  }
+}
+
+@media (max-width: 480px) {
+  .top-navigation {
+    padding: 12px 8px;
+    gap: 8px;
+  }
+
+  .nav-btn {
+    padding: 8px 16px;
+    font-size: 13px;
+  }
+}
+

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

@@ -0,0 +1,555 @@
+.cards-panel {
+  margin-top: 32px;
+  padding: 24px;
+  background: #ffffff;
+  border-radius: 20px;
+  box-shadow: 0 20px 60px rgba(15, 23, 42, 0.08);
+  display: flex;
+  flex-direction: column;
+  gap: 24px;
+}
+
+.cards-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 12px;
+}
+
+.cards-title {
+  font-size: 18px;
+  font-weight: 600;
+  color: #111827;
+}
+
+.cards-count {
+  font-size: 14px;
+  color: #6b7280;
+}
+
+.cards-grid {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 20px;
+}
+
+.sequence-card {
+  flex: 1 1 calc(25% - 20px);
+  min-width: 220px;
+  max-width: 320px;
+  background: linear-gradient(135deg, #ffffff 0%, #f9fafb 100%);
+  border-radius: 16px;
+  border: 1px solid rgba(99, 102, 241, 0.15);
+  box-shadow: 0 12px 30px rgba(99, 102, 241, 0.1);
+  transition: transform 0.25s ease, box-shadow 0.25s ease, border-color 0.25s ease;
+  cursor: pointer;
+  display: flex;
+  flex-direction: column;
+}
+
+.sequence-card:hover {
+  transform: translateY(-6px);
+  box-shadow: 0 18px 45px rgba(99, 102, 241, 0.18);
+  border-color: rgba(99, 102, 241, 0.4);
+}
+
+.sequence-card.is-active {
+  border-color: #6366f1;
+  box-shadow: 0 22px 55px rgba(99, 102, 241, 0.3);
+}
+
+.card-content {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+}
+
+.image-container {
+  position: relative;
+  width: 100%;
+  padding: 12px;
+  min-height: 180px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  /* 棋盘格背景用于显示透明部分 */
+  background-color: #fefefe;
+  background-image: 
+    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: 12px 12px;
+  background-position: 0 0, 0 6px, 6px -6px, -6px 0px;
+  border-radius: 12px;
+}
+
+.card-image {
+  max-width: 100%;
+  max-height: 100%;
+  object-fit: contain;
+  image-rendering: pixelated;
+  border-radius: 10px;
+}
+
+.card-footer {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 16px 18px;
+  gap: 12px;
+}
+
+.card-label {
+  font-size: 14px;
+  font-weight: 600;
+  color: #1f2937;
+  text-transform: uppercase;
+  letter-spacing: 0.04em;
+}
+
+.card-download-btn {
+  width: 36px;
+  height: 36px;
+  border: none;
+  border-radius: 999px;
+  background: linear-gradient(135deg, #6366f1, #8b5cf6);
+  color: #ffffff;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+  transition: transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
+  box-shadow: 0 8px 18px rgba(99, 102, 241, 0.35);
+}
+
+.card-download-btn:hover {
+  transform: scale(1.1);
+  background: linear-gradient(135deg, #4f46e5, #7c3aed);
+}
+
+.card-download-btn:active {
+  transform: scale(0.94);
+}
+
+.sequence-card .loading-spinner {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  width: 38px;
+  height: 38px;
+  border-radius: 50%;
+  border: 3px solid #e0e7ff;
+  border-top-color: #6366f1;
+  animation: spin 0.8s linear infinite;
+  transform: translate(-50%, -50%);
+}
+
+.sequence-card .image-error {
+  position: absolute;
+  top: 12px;
+  right: 12px;
+  padding: 6px 10px;
+  font-size: 12px;
+  border-radius: 6px;
+  background: rgba(239, 68, 68, 0.9);
+  color: #fff;
+}
+
+@keyframes spin {
+  to {
+    transform: translate(-50%, -50%) rotate(360deg);
+  }
+}
+
+@media (max-width: 1200px) {
+  .sequence-card {
+    flex: 1 1 calc(33.333% - 20px);
+  }
+}
+
+@media (max-width: 900px) {
+  .sequence-card {
+    flex: 1 1 calc(50% - 20px);
+  }
+}
+
+@media (max-width: 600px) {
+  .sequence-card {
+    flex: 1 1 100%;
+  }
+}
+
+/* ========================================
+   预览卡片 (主预览区域)
+   ======================================== */
+
+.preview-card {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+.preview-card-stage {
+  width: 100%;
+  flex: 1;
+  border-radius: 16px;
+  /* 棋盘格背景用于显示透明部分 */
+  background-color: #f8f9fb;
+  background-image: 
+    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;
+  min-height: 75vh;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  position: relative;
+  overflow: hidden;
+  border: 2px dashed #d1d5db;
+  box-shadow: inset 0 0 0 1px #edf0f6;
+  transition: all 0.3s ease;
+}
+
+.preview-card-stage.is-dragging {
+  background: #f0f7ff;
+  border: 3px dashed #3b82f6;
+  box-shadow: inset 0 0 0 8px rgba(59, 130, 246, 0.1);
+}
+
+.preview-card-stage.is-dragging-invalid {
+  background: #fef2f2;
+  border: 3px dashed #ef4444;
+  box-shadow: inset 0 0 0 8px rgba(239, 68, 68, 0.1);
+  cursor: not-allowed;
+}
+
+.preview-card-stage.is-dragging-invalid::after {
+  content: "🚫 不支持的内容";
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  font-size: 20px;
+  font-weight: 600;
+  color: #dc2626;
+  background: rgba(254, 242, 242, 0.95);
+  padding: 16px 32px;
+  border-radius: 12px;
+  border: 2px solid #ef4444;
+  pointer-events: none;
+  z-index: 100;
+}
+
+.preview-card-stage.is-playing {
+  border: none;
+  background: #ffffff;
+  box-shadow: none;
+}
+
+/* 顶部信息栏 */
+.preview-info-bar {
+  position: absolute;
+  top: 16px;
+  left: 16px;
+  right: 16px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  z-index: 10;
+  opacity: 0;
+  transition: opacity 0.3s ease;
+}
+
+.preview-info-bar:not([hidden]) {
+  opacity: 1;
+}
+
+.folder-info {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 8px 14px;
+  background: rgba(255, 255, 255, 0.95);
+  border-radius: 8px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+  backdrop-filter: blur(4px);
+}
+
+.folder-icon {
+  color: #f59e0b;
+  flex-shrink: 0;
+}
+
+.folder-name {
+  font-size: 14px;
+  font-weight: 600;
+  color: #374151;
+  max-width: 200px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.btn-export {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 8px 16px;
+  background: rgba(139, 92, 246, 0.95);
+  border: none;
+  border-radius: 8px;
+  color: white;
+  font-size: 13px;
+  font-weight: 600;
+  cursor: pointer;
+  box-shadow: 0 2px 12px rgba(139, 92, 246, 0.3);
+  backdrop-filter: blur(4px);
+  transition: all 0.2s ease;
+  white-space: nowrap;
+}
+
+.btn-export:hover {
+  background: rgba(124, 58, 237, 0.95);
+  transform: translateY(-1px);
+  box-shadow: 0 4px 16px rgba(139, 92, 246, 0.4);
+}
+
+.btn-export:active {
+  transform: translateY(0);
+}
+
+.preview-card-stage .preview-image {
+  max-width: 100%;
+  max-height: 100%;
+  object-fit: contain;
+  image-rendering: pixelated;
+  z-index: 1;
+}
+
+.preview-card-stage .preview-image:not([src]),
+.preview-card-stage .preview-image[src=""],
+.preview-card-stage .preview-image.is-hidden {
+  opacity: 0;
+  visibility: hidden;
+  display: none;
+}
+
+.preview-card-stage .loading-overlay {
+  position: absolute;
+  inset: 0;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  gap: 20px;
+  background: rgba(248, 249, 251, 0.92);
+  backdrop-filter: blur(4px);
+  z-index: 50;
+  opacity: 0;
+  visibility: hidden;
+  transition: opacity 0.25s ease, visibility 0.25s ease;
+}
+
+.preview-card-stage .loading-overlay.is-visible {
+  opacity: 1;
+  visibility: visible;
+}
+
+.preview-card-stage .loading-spinner {
+  width: 56px;
+  height: 56px;
+  border-radius: 50%;
+  border: 4px solid rgba(59, 130, 246, 0.15);
+  border-top-color: #3b82f6;
+  animation: spin-preview 0.8s cubic-bezier(0.45, 0.05, 0.55, 0.95) infinite;
+  position: relative;
+}
+
+.preview-card-stage .loading-spinner::before {
+  content: "";
+  position: absolute;
+  inset: -12px;
+  border-radius: 50%;
+  background: radial-gradient(circle, rgba(59, 130, 246, 0.1) 0%, transparent 70%);
+  animation: pulse-glow 1.5s ease-in-out infinite;
+}
+
+.preview-card-stage .loading-text {
+  margin: 0;
+  font-size: 14px;
+  font-weight: 500;
+  color: #6b7280;
+  letter-spacing: 0.02em;
+  animation: fade-pulse 1.5s ease-in-out infinite;
+}
+
+.preview-card-stage .image-error {
+  position: absolute;
+  top: 24px;
+  left: 24px;
+  padding: 10px 16px;
+  border-radius: 12px;
+  background: rgba(120, 124, 138, 0.92);
+  color: #fff;
+  font-size: 15px;
+  z-index: 20;
+}
+
+.preview-card-stage .drop-hint {
+  position: absolute;
+  inset: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  pointer-events: none;
+  z-index: 5;
+}
+
+.preview-card-stage .drop-hint[hidden] {
+  display: none;
+}
+
+.preview-card-stage .hint-text {
+  margin: 0;
+  padding: 0;
+  font-size: 20px;
+  font-weight: 600;
+  color: #9ca3af;
+  letter-spacing: 1px;
+  text-align: center;
+}
+
+.preview-card-controls {
+  width: 100%;
+  flex-shrink: 0;
+  background: transparent;
+  padding: 16px 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 18px;
+  flex-wrap: wrap;
+}
+
+.preview-card-controls .control-label {
+  font-size: 14px;
+  font-weight: 600;
+  color: #9ca3af;
+  letter-spacing: 0.1em;
+  text-transform: uppercase;
+}
+
+.preview-card-controls .slider-container {
+  display: flex;
+  align-items: center;
+  gap: 18px;
+  flex: 1;
+  max-width: 520px;
+  padding: 16px 18px;
+  border-radius: 18px;
+  background: #f4f6fb;
+  box-shadow: inset 0 0 0 1px #e7eaf2;
+}
+
+.preview-card-controls .fps-slider {
+  flex: 1;
+  height: 10px;
+  -webkit-appearance: none;
+  appearance: none;
+  background: linear-gradient(90deg, #e5e7eb 0%, #c7ccd9 100%);
+  border-radius: 999px;
+  outline: none;
+  cursor: pointer;
+  transition: background 0.2s ease, box-shadow 0.2s ease;
+  box-shadow: inset 0 2px 6px rgba(15, 23, 42, 0.08);
+}
+
+.preview-card-controls .fps-slider::-webkit-slider-runnable-track {
+  height: 10px;
+  border-radius: 999px;
+  background: transparent;
+}
+
+.preview-card-controls .fps-slider::-webkit-slider-thumb {
+  -webkit-appearance: none;
+  appearance: none;
+  width: 22px;
+  height: 22px;
+  background: #111827;
+  border-radius: 50%;
+  cursor: pointer;
+  box-shadow: 0 6px 18px rgba(17, 24, 39, 0.25);
+  transition: transform 0.2s ease, box-shadow 0.2s ease;
+  margin-top: -6px;
+}
+
+.preview-card-controls .fps-slider::-webkit-slider-thumb:hover {
+  transform: scale(1.15);
+  box-shadow: 0 8px 22px rgba(17, 24, 39, 0.3);
+}
+
+.preview-card-controls .fps-slider::-webkit-slider-thumb:active {
+  transform: scale(1.05);
+}
+
+.preview-card-controls .fps-value {
+  min-width: 88px;
+  padding: 8px 14px;
+  background: #111827;
+  color: #ffffff;
+  border-radius: 10px;
+  font-size: 15px;
+  font-weight: 600;
+  text-align: center;
+  font-family: "SF Mono", "Monaco", "Inconsolata", "Roboto Mono", monospace;
+  box-shadow: 0 8px 20px rgba(15, 23, 42, 0.25);
+}
+
+@keyframes spin-preview {
+  to {
+    transform: rotate(360deg);
+  }
+}
+
+@keyframes pulse-glow {
+  0%, 100% {
+    transform: scale(0.9);
+    opacity: 0.5;
+  }
+  50% {
+    transform: scale(1.1);
+    opacity: 1;
+  }
+}
+
+@keyframes fade-pulse {
+  0%, 100% {
+    opacity: 0.6;
+  }
+  50% {
+    opacity: 1;
+  }
+}
+
+@media (max-width: 900px) {
+  .preview-card-stage {
+    min-height: 60vh;
+  }
+  
+  .preview-card-controls {
+    flex-direction: column;
+  }
+  
+  .preview-card-controls .slider-container {
+    width: 100%;
+  }
+}
+
+

+ 339 - 0
client/css/seq_ani_player/seq-ani-player.css

@@ -0,0 +1,339 @@
+/* 序列帧播放器布局 */
+
+.player-stage {
+  width: 100%;
+  height: 100%;
+  padding: 0;
+  background: transparent;
+  flex: 1 1 auto;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+}
+
+.preview-card-container {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+
+.player-stage-inner {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  width: 100%;
+  height: 100%;
+  flex: 1;
+}
+
+.player-display {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  height: 100%;
+}
+
+.player-image-shell {
+  width: 100%;
+  height: 100%;
+  border-radius: 16px;
+  /* 棋盘格背景用于显示透明部分 */
+  background-color: #f8f9fb;
+  background-image: 
+    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;
+  min-height: 75vh;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  position: relative;
+  overflow: hidden;
+  border: 2px dashed #d1d5db;
+  box-shadow: inset 0 0 0 1px #edf0f6;
+}
+
+
+
+.player-image-shell.is-dragging {
+  background: #f0f7ff;
+  border: 3px dashed #3b82f6;
+  box-shadow: inset 0 0 0 8px rgba(59, 130, 246, 0.1);
+}
+
+.drop-hint {
+  position: absolute;
+  inset: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  pointer-events: none;
+  z-index: 5;
+}
+
+.drop-hint[hidden] {
+  display: none;
+}
+
+.hint-text {
+  margin: 0;
+  padding: 0;
+  font-size: 20px;
+  font-weight: 600;
+  color: #ef4444;
+  letter-spacing: 1px;
+  text-align: center;
+}
+
+.player-image-shell img {
+  max-width: 100%;
+  max-height: 100%;
+  object-fit: contain;
+  image-rendering: pixelated;
+}
+
+.player-image-shell img:not([src]),
+.player-image-shell img[src=""],
+.player-image-shell img.is-hidden {
+  opacity: 0;
+  visibility: hidden;
+  display: none;
+}
+
+.loading-overlay {
+  position: absolute;
+  inset: 0;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  gap: 20px;
+  background: rgba(248, 249, 251, 0.92);
+  backdrop-filter: blur(4px);
+  z-index: 50;
+  opacity: 0;
+  visibility: hidden;
+  transition: opacity 0.25s ease, visibility 0.25s ease;
+}
+
+.loading-overlay.is-visible {
+  opacity: 1;
+  visibility: visible;
+}
+
+.loading-overlay .loading-spinner {
+  width: 56px;
+  height: 56px;
+  border-radius: 50%;
+  border: 4px solid rgba(59, 130, 246, 0.15);
+  border-top-color: #3b82f6;
+  animation: spin 0.8s cubic-bezier(0.45, 0.05, 0.55, 0.95) infinite;
+  position: relative;
+}
+
+.loading-overlay .loading-spinner::before {
+  content: "";
+  position: absolute;
+  inset: -12px;
+  border-radius: 50%;
+  background: radial-gradient(circle, rgba(59, 130, 246, 0.1) 0%, transparent 70%);
+  animation: pulse-glow 1.5s ease-in-out infinite;
+}
+
+.loading-overlay .loading-text {
+  margin: 0;
+  font-size: 14px;
+  font-weight: 500;
+  color: #6b7280;
+  letter-spacing: 0.02em;
+  animation: fade-pulse 1.5s ease-in-out infinite;
+}
+
+@keyframes pulse-glow {
+  0%, 100% {
+    transform: scale(0.9);
+    opacity: 0.5;
+  }
+  50% {
+    transform: scale(1.1);
+    opacity: 1;
+  }
+}
+
+@keyframes fade-pulse {
+  0%, 100% {
+    opacity: 0.6;
+  }
+  50% {
+    opacity: 1;
+  }
+}
+
+.player-image-shell .image-error {
+  position: absolute;
+  top: 24px;
+  left: 24px;
+  padding: 10px 16px;
+  border-radius: 12px;
+  background: rgba(120, 124, 138, 0.92);
+  color: #fff;
+  font-size: 15px;
+  z-index: 20;
+}
+
+
+.panel-card {
+  width: 100%;
+  flex-shrink: 0;
+  background: transparent;
+  border-radius: 0;
+  box-shadow: none;
+  padding: 16px 0;
+  border: none;
+}
+
+.control-panel {
+  display: flex;
+  flex-direction: column;
+  gap: 18px;
+}
+
+.control-row {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 18px;
+  width: 100%;
+  flex-wrap: wrap;
+}
+
+.folder-label {
+  font-size: 14px;
+  font-weight: 600;
+  color: #9ca3af;
+  letter-spacing: 0.1em;
+  text-transform: uppercase;
+}
+
+.slider-container {
+  display: flex;
+  align-items: center;
+  gap: 18px;
+  flex: 1;
+  max-width: 520px;
+  padding: 16px 18px;
+  border-radius: 18px;
+  background: #f4f6fb;
+  box-shadow: inset 0 0 0 1px #e7eaf2;
+}
+
+.fps-slider {
+  flex: 1;
+  height: 10px;
+  -webkit-appearance: none;
+  appearance: none;
+  background: linear-gradient(90deg, #e5e7eb 0%, #c7ccd9 100%);
+  border-radius: 999px;
+  outline: none;
+  cursor: pointer;
+  transition: background 0.2s ease, box-shadow 0.2s ease;
+  box-shadow: inset 0 2px 6px rgba(15, 23, 42, 0.08);
+}
+
+.fps-slider::-webkit-slider-runnable-track {
+  height: 10px;
+  border-radius: 999px;
+  background: transparent;
+}
+
+.fps-slider::-webkit-slider-thumb {
+  -webkit-appearance: none;
+  appearance: none;
+  width: 22px;
+  height: 22px;
+  background: #111827;
+  border-radius: 50%;
+  cursor: pointer;
+  box-shadow: 0 6px 18px rgba(17, 24, 39, 0.25);
+  transition: transform 0.2s ease, box-shadow 0.2s ease;
+  margin-top: -6px;
+}
+
+.fps-slider::-webkit-slider-thumb:hover {
+  transform: scale(1.15);
+  box-shadow: 0 8px 22px rgba(17, 24, 39, 0.3);
+}
+
+.fps-slider::-webkit-slider-thumb:active {
+  transform: scale(1.05);
+}
+
+.fps-slider::-moz-range-track {
+  height: 10px;
+  border-radius: 999px;
+  background: linear-gradient(90deg, #e5e7eb 0%, #c7ccd9 100%);
+}
+
+.fps-slider::-moz-range-thumb {
+  width: 22px;
+  height: 22px;
+  background: #111827;
+  border: none;
+  border-radius: 50%;
+  cursor: pointer;
+  box-shadow: 0 6px 18px rgba(17, 24, 39, 0.25);
+  transition: transform 0.2s ease, box-shadow 0.2s ease;
+}
+
+.fps-slider::-moz-range-thumb:hover {
+  transform: scale(1.15);
+  box-shadow: 0 8px 22px rgba(17, 24, 39, 0.3);
+}
+
+.fps-value {
+  min-width: 88px;
+  padding: 8px 14px;
+  background: #111827;
+  color: #ffffff;
+  border-radius: 10px;
+  font-size: 15px;
+  font-weight: 600;
+  text-align: center;
+  font-family: "SF Mono", "Monaco", "Inconsolata", "Roboto Mono", monospace;
+  box-shadow: 0 8px 20px rgba(15, 23, 42, 0.25);
+}
+
+@media (max-width: 900px) {
+  .player-image-shell {
+    min-height: 60vh;
+  }
+
+  .control-row {
+    flex-direction: column;
+  }
+
+  .slider-container {
+    width: 100%;
+  }
+}
+
+@media (max-width: 520px) {
+  .panel-card {
+    padding: 20px 0;
+  }
+
+  .slider-container {
+    padding: 14px;
+  }
+
+  .fps-value {
+    width: 100%;
+  }
+}

+ 89 - 0
client/css/store.css

@@ -0,0 +1,89 @@
+/* 动画商城页面样式 */
+
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+}
+
+body {
+  margin: 0;
+  padding: 0;
+  height: 100vh;
+  overflow: hidden;
+  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial,
+    "Noto Sans", sans-serif;
+}
+
+.store-root {
+  display: flex;
+  flex-direction: column;
+  min-height: 100vh;
+  height: 100vh;
+  background: linear-gradient(135deg, #fdfcfb 0%, #f7f7f8 50%, #fdfcfb 100%);
+  color: #1f2937;
+  user-select: none;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+}
+
+.store-container {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 40px;
+}
+
+.coming-soon {
+  text-align: center;
+  max-width: 600px;
+  padding: 60px 40px;
+  background: linear-gradient(135deg, #ffffff 0%, #f9fafb 100%);
+  border-radius: 24px;
+  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);
+}
+
+.coming-soon-icon {
+  font-size: 80px;
+  margin-bottom: 24px;
+  animation: float 3s ease-in-out infinite;
+}
+
+@keyframes float {
+  0%, 100% {
+    transform: translateY(0);
+  }
+  50% {
+    transform: translateY(-10px);
+  }
+}
+
+.coming-soon-title {
+  font-size: 36px;
+  font-weight: 700;
+  background: linear-gradient(135deg, #8b5cf6, #a78bfa);
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: transparent;
+  background-clip: text;
+  margin-bottom: 16px;
+  letter-spacing: 1px;
+}
+
+.coming-soon-subtitle {
+  font-size: 24px;
+  color: #6b7280;
+  font-weight: 600;
+  margin-bottom: 20px;
+  letter-spacing: 0.5px;
+}
+
+.coming-soon-description {
+  font-size: 16px;
+  color: #9ca3af;
+  line-height: 1.6;
+  margin-top: 12px;
+}
+

+ 43 - 0
client/index.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="icon" type="image/png" href="static/favicon.png" />
+  <link rel="stylesheet" href="css/index.css" />
+  <link rel="stylesheet" href="css/alert-view.css" />
+  <link rel="stylesheet" href="css/confirm-view.css" />
+</head>
+<body>
+  <div class="app-root">
+    <!-- 导航栏 iframe -->
+    <iframe id="navigationFrame" src="page/navigation/navigation.html" frameborder="0"></iframe>
+
+    <!-- 子页面容器 -->
+    <section class="page-container">
+      <iframe id="pageFrame" src="page/assets/assets.html" frameborder="0"></iframe>
+    </section>
+  </div>
+
+  <!-- 全局 Loading 遮罩 -->
+  <div class="global-loading-overlay" id="globalLoadingOverlay">
+    <div class="global-loading-content">
+      <div class="global-loading-spinner"></div>
+      <p class="global-loading-text">正在处理...</p>
+    </div>
+  </div>
+
+  <!-- 全局文字提示 -->
+  <div class="global-alert" id="globalAlert">
+    <div class="alert-message" id="alertMessage"></div>
+  </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>
+
+  <script src="js/index.js"></script>
+  <script src="js/confirm-view.js"></script>
+  <script src="js/export-view-manager.js"></script>
+</body>
+</html>

+ 210 - 0
client/js/Index.js

@@ -0,0 +1,210 @@
+// 全局 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) {
+    if (!alertContainer) init();
+    if (!alertContainer || !alertMessage) return;
+    
+    // 清除之前的自动隐藏定时器
+    if (hideTimer) {
+      clearTimeout(hideTimer);
+      hideTimer = null;
+    }
+    
+    alertMessage.textContent = text;
+    alertContainer.classList.add('show');
+    
+    // 自动隐藏
+    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 () {
+  const DEFAULT_PAGE = "assets";
+
+  function getPageFrame() {
+    return document.getElementById("pageFrame");
+  }
+
+  function getNavigationFrame() {
+    return document.getElementById("navigationFrame");
+  }
+
+  function switchPage(page) {
+    const frame = getPageFrame();
+    if (!frame) {
+      return;
+    }
+
+    switch (page) {
+      case "store":
+        frame.src = "page/store/store.html";
+        break;
+      case "assets":
+      default:
+        frame.src = "page/assets/assets.html";
+        break;
+    }
+
+    const navigationFrame = getNavigationFrame();
+    if (navigationFrame && navigationFrame.contentWindow) {
+      navigationFrame.contentWindow.postMessage(
+        { type: "navigation", page },
+        "*"
+      );
+    }
+  }
+
+  window.addEventListener("message", (event) => {
+    // console.log('[Index] ← 收到message事件:', event.data);
+    
+    if (event.origin !== window.location.origin) {
+      // console.log('[Index] ⚠ origin不匹配,忽略:', event.origin);
+      return;
+    }
+    const { data } = event;
+    if (data && data.type === "navigation" && data.page) {
+      switchPage(data.page);
+    } else if (data && data.type === "global-loading") {
+      // 处理全局loading显示/隐藏
+      // console.log('[Index] 收到global-loading消息:', data);
+      if (data.action === "show") {
+        // console.log('[Index] 显示Loading:', data.text);
+        window.GlobalLoading.show(data.text);
+      } else if (data.action === "hide") {
+        // console.log('[Index] 隐藏Loading');
+        window.GlobalLoading.hide();
+      }
+    } else if (data && data.type === "global-alert") {
+      // 处理全局alert提示
+      // console.log('[Index] 收到global-alert消息:', data);
+      if (data.text) {
+        window.GlobalAlert.show(data.text, data.duration);
+      }
+    } else if (data && data.type === "global-confirm") {
+      // 处理全局confirm对话框
+      // console.log('[Index] 收到global-confirm消息:', data);
+      if (data.message && window.GlobalConfirm) {
+        // 确保 GlobalConfirm 初始化完成后再显示
+        window.GlobalConfirm.show(data.message).then((confirmed) => {
+          // console.log('[Index] 用户选择:', confirmed ? '确认' : '取消');
+          // 将结果发送回请求的子窗口
+          event.source.postMessage({
+            type: 'global-confirm-response',
+            id: data.id,
+            confirmed: confirmed
+          }, event.origin);
+        }).catch(error => {
+          // console.error('[Index] GlobalConfirm显示失败:', error);
+          // 失败时返回取消
+          event.source.postMessage({
+            type: 'global-confirm-response',
+            id: data.id,
+            confirmed: false
+          }, event.origin);
+        });
+      }
+    } else if (data && data.type === "open-export-view") {
+      // 处理打开导出弹出框
+      // console.log('[Index] 收到open-export-view消息:', data);
+      if (data.folderName && window.ExportViewManager) {
+        // 直接打开弹出框,传递文件夹名称
+        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);
+        });
+      }
+    }
+  });
+
+  window.addEventListener("DOMContentLoaded", () => {
+    switchPage(DEFAULT_PAGE);
+  });
+})();
+

+ 0 - 0
client/js/animation_character_replace/animation-character-replace.js


+ 52 - 0
client/js/assets.js

@@ -0,0 +1,52 @@
+// 我的资产页面 - 主逻辑
+// 管理左侧网盘和右侧动画预览器的交互
+
+(function () {
+  const diskFrame = document.getElementById('diskFrame');
+  const playerFrame = document.getElementById('playerFrame');
+
+  // 页面加载完成后初始化
+  window.addEventListener('DOMContentLoaded', function () {
+    // console.log('[Assets] 我的资产页面加载完成');
+    
+    // Add interaction logic between left and right panels here
+    // E.g., preview selected files from disk on the right player
+  });
+
+  // 监听来自子页面的消息(如果需要实现跨iframe通信)
+  window.addEventListener('message', function(event) {
+    // 处理来自 Disk.html 或 SeqAniPlayer.html 的消息
+    const { data } = event;
+    
+    // 转发 global-alert、global-loading、global-confirm 消息到父窗口
+    if (data && (data.type === 'global-alert' || data.type === 'global-loading' || data.type === 'global-confirm')) {
+      console.log('[Assets] 收到子页面消息:', data.type);
+      console.log('[Assets]   内容:', data);
+      
+      if (window.parent && window.parent !== window) {
+        console.log('[Assets] → 转发消息到父窗口(index.html)');
+        window.parent.postMessage(data, '*');
+        console.log('[Assets] ✓ 消息已转发');
+      } else {
+        console.warn('[Assets] ⚠ 无法找到父窗口');
+      }
+    }
+    
+    // 转发 global-confirm-response 消息回子窗口
+    if (data && data.type === 'global-confirm-response') {
+      console.log('[Assets] 收到父窗口的确认响应:', data);
+      console.log('[Assets] → 转发响应到子窗口(disk.html)');
+      
+      // 转发给所有子iframe
+      if (diskFrame && diskFrame.contentWindow) {
+        diskFrame.contentWindow.postMessage(data, '*');
+        console.log('[Assets] ✓ 响应已转发到disk');
+      }
+      if (playerFrame && playerFrame.contentWindow) {
+        playerFrame.contentWindow.postMessage(data, '*');
+        console.log('[Assets] ✓ 响应已转发到player');
+      }
+    }
+  });
+})();
+

+ 152 - 0
client/js/confirm-view.js

@@ -0,0 +1,152 @@
+/**
+ * 全局确认对话�?
+ * 加载�?index.html 中,覆盖整个页面
+ */
+class GlobalConfirm {
+    constructor() {
+        this.overlay = null;
+        this.message = null;
+        this.okBtn = null;
+        this.cancelBtn = null;
+        this.resolveCallback = null;
+        this.ready = false;
+        
+        // 立即初始�?
+        this.initPromise = this.init();
+    }
+
+    async init() {
+// ('[GlobalConfirm] �?init 开�?);
+        
+        await this.ensureElements();
+// ('[GlobalConfirm] �?查找DOM元素...');
+        
+        this.overlay = document.getElementById('globalConfirmOverlay');
+        this.message = document.getElementById('confirmMessage');
+        this.okBtn = document.getElementById('confirmOkBtn');
+        this.cancelBtn = document.getElementById('confirmCancelBtn');
+        
+// ('[GlobalConfirm]   overlay:', !!this.overlay);
+// ('[GlobalConfirm]   message:', !!this.message);
+// ('[GlobalConfirm]   okBtn:', !!this.okBtn);
+// ('[GlobalConfirm]   cancelBtn:', !!this.cancelBtn);
+        
+        if (this.overlay && this.okBtn && this.cancelBtn) {
+            this.bindEvents();
+            this.ready = true;
+// ('[GlobalConfirm] �?初始化完成,ready=true');
+        } else {
+// ('[GlobalConfirm] �?初始化失败,部分元素未找�?);
+        }
+    }
+
+    async ensureElements() {
+// ('[GlobalConfirm] �?ensureElements 开�?);
+        
+        if (document.getElementById('globalConfirmOverlay')) {
+// ('[GlobalConfirm] �?元素已存在,跳过加载');
+            return;
+        }
+
+        try {
+// ('[GlobalConfirm] �?开始加�?/page/confirm-view.html');
+            const response = await fetch('/page/confirm-view.html', { cache: 'no-cache' });
+// ('[GlobalConfirm]   响应状�?', response.status, response.statusText);
+            
+            if (!response.ok) {
+                throw new Error('加载确认对话框组件失败');
+            }
+            const html = await response.text();
+// ('[GlobalConfirm] �?HTML已获取,长度:', html.length);
+            
+            document.body.insertAdjacentHTML('beforeend', html);
+// ('[GlobalConfirm] �?HTML已插入到body');
+        } catch (error) {
+// ('[GlobalConfirm] �?加载确认对话框组件失�?', error);
+        }
+    }
+
+    bindEvents() {
+// ('[GlobalConfirm] �?绑定事件监听�?);
+        
+        this.okBtn.addEventListener('click', () => {
+// ('[GlobalConfirm] �?用户点击确认按钮');
+            this.hide();
+            if (this.resolveCallback) {
+// ('[GlobalConfirm] �?调用 resolveCallback(true)');
+                this.resolveCallback(true);
+            }
+        });
+
+        this.cancelBtn.addEventListener('click', () => {
+// ('[GlobalConfirm] �?用户点击取消按钮');
+            this.hide();
+            if (this.resolveCallback) {
+// ('[GlobalConfirm] �?调用 resolveCallback(false)');
+                this.resolveCallback(false);
+            }
+        });
+
+        // 点击遮罩层取�?
+        this.overlay.addEventListener('click', (e) => {
+            if (e.target === this.overlay) {
+                this.hide();
+                if (this.resolveCallback) {
+                    this.resolveCallback(false);
+                }
+            }
+        });
+
+        // ESC键取�?
+        document.addEventListener('keydown', (e) => {
+            if (e.key === 'Escape' && this.overlay && this.overlay.classList.contains('show')) {
+                this.hide();
+                if (this.resolveCallback) {
+                    this.resolveCallback(false);
+                }
+            }
+        });
+    }
+
+    async show(message) {
+// ('[GlobalConfirm] �?show() 被调�?);
+// ('[GlobalConfirm]   消息:', message);
+// ('[GlobalConfirm]   ready状�?', this.ready);
+        
+        // 确保初始化完�?
+// ('[GlobalConfirm] �?等待初始化完�?..');
+        await this.initPromise;
+// ('[GlobalConfirm] �?初始化完�?);
+// ('[GlobalConfirm]   overlay:', !!this.overlay);
+// ('[GlobalConfirm]   message:', !!this.message);
+// ('[GlobalConfirm]   okBtn:', !!this.okBtn);
+// ('[GlobalConfirm]   cancelBtn:', !!this.cancelBtn);
+        
+        return new Promise((resolve) => {
+// ('[GlobalConfirm] �?创建Promise,等待用户操�?);
+            this.resolveCallback = resolve;
+            if (this.message) {
+                this.message.textContent = message;
+// ('[GlobalConfirm] �?消息已设�?);
+            }
+            if (this.overlay) {
+                this.overlay.classList.add('show');
+// ('[GlobalConfirm] �?对话框已显示');
+            }
+        });
+    }
+
+    hide() {
+// ('[GlobalConfirm] �?隐藏对话�?);
+        if (this.overlay) {
+            this.overlay.classList.remove('show');
+// ('[GlobalConfirm] �?对话框已隐藏');
+        }
+    }
+}
+
+// 全局单例
+// console.log('[GlobalConfirm] 创建全局实例...');
+window.GlobalConfirm = new GlobalConfirm();
+// console.log('[GlobalConfirm] ✓ 全局实例已创建');
+

+ 149 - 0
client/js/disk/alert.js

@@ -0,0 +1,149 @@
+(() => {
+    /**
+     * 全局提示遮罩层管理
+     * 用于服务器断开连接时阻止 UI 交互并显示重连状态
+     */
+    class AlertView {
+        constructor(options = {}) {
+            this.overlay = null;
+            this.message = null;
+            this.checkInterval = options.checkInterval || 3000; // 检测间隔
+            this.pingUrl = options.pingUrl || '/api/disk/list?path='; // 用于检测连接的接口
+            this.isConnected = true;
+            this.reconnectTimer = null;
+
+            this.init();
+        }
+
+        async init() {
+            await this.ensureElements();
+            this.overlay = document.getElementById('alertOverlay');
+            this.message = document.getElementById('alertMessage');
+            this.startConnectionCheck();
+        }
+
+        async ensureElements() {
+            if (document.getElementById('alertOverlay')) {
+                return;
+            }
+
+            try {
+                const response = await fetch('/page/alert-overlay.html', { cache: 'no-cache' });
+                if (!response.ok) {
+                    throw new Error('加载 Alert Overlay 组件失败');
+                }
+                const html = await response.text();
+                document.body.insertAdjacentHTML('beforeend', html);
+            } catch (error) {
+                console.error('加载 Alert Overlay 组件失败:', error);
+            }
+        }
+
+        show(msg) {
+            if (this.overlay) {
+                if (msg && this.message) {
+                    this.message.textContent = msg;
+                }
+                this.overlay.classList.add('show');
+            }
+        }
+
+        hide() {
+            if (this.overlay) {
+                this.overlay.classList.remove('show');
+            }
+        }
+
+        startConnectionCheck() {
+            // 定期检测服务器连接
+            setInterval(() => this.checkConnection(), this.checkInterval);
+        }
+
+        async checkConnection() {
+            try {
+                const controller = new AbortController();
+                const timeoutId = setTimeout(() => controller.abort(), 5000);
+
+                const response = await fetch(this.pingUrl, {
+                    method: 'GET',
+                    signal: controller.signal
+                });
+
+                clearTimeout(timeoutId);
+
+                if (response.ok) {
+                    if (!this.isConnected) {
+                        // 重连成功
+                        this.isConnected = true;
+                        this.hide();
+                        this.onReconnected();
+                    }
+                } else {
+                    this.handleDisconnect();
+                }
+            } catch (error) {
+                this.handleDisconnect();
+            }
+        }
+
+        handleDisconnect() {
+            if (this.isConnected) {
+                this.isConnected = false;
+                this.show('服务器断开连接,正在重连...');
+                this.startReconnect();
+            }
+        }
+
+        startReconnect() {
+            if (this.reconnectTimer) {
+                clearInterval(this.reconnectTimer);
+            }
+
+            // 每 2 秒尝试重连一次
+            this.reconnectTimer = setInterval(() => {
+                this.tryReconnect();
+            }, 2000);
+        }
+
+        async tryReconnect() {
+            try {
+                const controller = new AbortController();
+                const timeoutId = setTimeout(() => controller.abort(), 5000);
+
+                const response = await fetch(this.pingUrl, {
+                    method: 'GET',
+                    signal: controller.signal
+                });
+
+                clearTimeout(timeoutId);
+
+                if (response.ok) {
+                    // 重连成功
+                    clearInterval(this.reconnectTimer);
+                    this.reconnectTimer = null;
+                    this.isConnected = true;
+                    this.hide();
+                    this.onReconnected();
+                }
+            } catch (error) {
+                // 重连失败,继续尝试
+            }
+        }
+
+        onReconnected() {
+            // 重连成功后刷新页面数据
+            if (window.diskManager && typeof window.diskManager.loadFiles === 'function') {
+                window.diskManager.loadFiles();
+            }
+        }
+    }
+
+    // 导出到全局
+    window.AlertView = AlertView;
+
+    // 页面加载完成后自动初始化(已禁用)
+    // document.addEventListener('DOMContentLoaded', () => {
+    //     window.alertView = new AlertView();
+    // });
+})();
+

+ 1712 - 0
client/js/disk/disk.js

@@ -0,0 +1,1712 @@
+// 网盘管理系统 - 客户端逻辑
+// 版本: v1.2 - 修复404错误
+// console.log('[Disk] 加载 DiskManager v1.2');
+
+class DiskManager {
+    constructor() {
+        this.files = [];
+        this.uploadingFiles = [];
+        this.dragCounter = 0;
+        this.clipboard = null;
+        this.shortcutKeys = null;
+        this.container = document.querySelector('.disk-container');
+        
+        // 全局文件结构缓存(用于快速验证文件夹内容)
+        this.fileStructureCache = new Map(); // key: 文件路径, value: 文件信息(包括pngCount)
+        this.cacheInitialized = false;
+        
+        this.init();
+    }
+
+    async init() {
+        await this.ensureToolBar();
+        await this.ensureContextMenu();
+        this.initElements();
+        this.initUploadProgress();
+        this.initContextMenu();
+        this.initPath();
+        this.initSelection();
+        this.initShortcutKeys();
+        this.initSearchBar();
+        this.bindEvents();
+        this.loadFiles();
+        
+        // 后台初始化文件结构缓存(不阻塞页面加载)
+        this.initFileStructureCache();
+    }
+
+    async ensureToolBar() {
+        if (document.getElementById('breadcrumb')) {
+            return;
+        }
+
+        if (!this.container) {
+            return;
+        }
+
+        try {
+            const response = await fetch('tool-bar.html', { cache: 'no-cache' });
+            if (!response.ok) {
+                throw new Error('加载工具栏失败');
+            }
+            const html = await response.text();
+            this.container.insertAdjacentHTML('afterbegin', html);
+        } catch (error) {
+            console.error('加载工具栏失败:', error);
+        }
+    }
+
+    async ensureContextMenu() {
+        if (document.getElementById('contextMenu')) {
+            return;
+        }
+
+        const menuContainer = document.getElementById('contextMenuContainer');
+        if (!menuContainer) {
+            return;
+        }
+
+        try {
+            const response = await fetch('right-click-menu.html', { cache: 'no-cache' });
+            if (!response.ok) {
+                throw new Error('加载右键菜单失败');
+            }
+            const html = await response.text();
+            menuContainer.innerHTML = html;
+        } catch (error) {
+            console.error('加载右键菜单失败:', error);
+        }
+    }
+
+    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.btnCreateFolder = document.getElementById('btnCreateFolder');
+        this.fileInput = document.getElementById('fileInput');
+        this.searchInput = document.getElementById('searchInput');
+        this.searchClear = document.getElementById('searchClear');
+        this.selectionBar = document.getElementById('selectionBar');
+        this.selectionCount = document.getElementById('selectionCount');
+        this.selectionBox = document.getElementById('selectionBox');
+        this.btnDownload = document.getElementById('btnDownload');
+        this.btnDelete = document.getElementById('btnDelete');
+        this.btnUpload = document.getElementById('btnUpload');
+        this.uploadProgress = document.getElementById('uploadProgress');
+        this.uploadProgressList = document.getElementById('uploadProgressList');
+        this.uploadProgressTemplate = document.getElementById('uploadProgressTemplate');
+        this.uploadProgressClose = document.getElementById('uploadProgressClose');
+        this.contextMenu = document.getElementById('contextMenu');
+    }
+
+    // 初始化路径导航
+    initPath() {
+        this.pathNav = new PathNavigator({
+            container: this.breadcrumb,
+            rootName: '全部文件',
+            onNavigate: (path) => {
+                this.loadFiles();
+            }
+        });
+    }
+
+    // 初始化框选功能
+    initSelection() {
+        this.selection = new MultipleSelection({
+            container: this.dropZone,
+            itemsContainer: this.fileList,
+            selectionBox: this.selectionBox,
+            selectionBar: this.selectionBar,
+            selectionCount: this.selectionCount,
+            itemSelector: '.file-item',
+            onSelectionChange: (selectedItems) => {
+                // 选择变化时的回调(可用于其他逻辑)
+            }
+        });
+    }
+
+    initUploadProgress() {
+        if (this.uploadProgressClose) {
+            this.uploadProgressClose.addEventListener('click', () => this.hideUploadProgress());
+        }
+        this.hideUploadProgress();
+    }
+
+    initContextMenu() {
+        if (!this.dropZone || !this.contextMenu) {
+            return;
+        }
+
+        this.contextMenuManager = new RightClickMenu({
+            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');
+        // console.log('[Disk] 右键菜单即将显示');
+        // console.log('[Disk]   点击目标:', event.target);
+        // console.log('[Disk]   最近的文件项:', fileItem);
+        
+        if (fileItem) {
+            // 如果点击在文件项上,确保它被选中
+            const isSelected = this.selection.isSelected(fileItem.dataset.path);
+            // console.log('[Disk]   文件项路径:', fileItem.dataset.path);
+            // console.log('[Disk]   是否已选中:', isSelected);
+            
+            if (!isSelected) {
+                // console.log('[Disk] → 自动选中该文件项');
+                this.selection.selectOnly(fileItem);
+                // console.log('[Disk] ✓ 文件项已选中');
+            }
+        } else {
+            // console.log('[Disk] ⚠ 右键点击在空白区域');
+        }
+    }
+
+    showUploadProgress() {
+        if (this.uploadProgress) {
+            this.uploadProgress.classList.add('show');
+        }
+    }
+
+    hideUploadProgress() {
+        if (this.uploadProgress) {
+            this.uploadProgress.classList.remove('show');
+        }
+        if (this.uploadProgressList) {
+            this.uploadProgressList.innerHTML = '';
+        }
+    }
+    
+    showGlobalLoading(text = '正在处理...') {
+        // 通过postMessage通知父页面显示loading
+        console.log('[Disk] 显示全局Loading:', text);
+        console.log('[Disk] 当前window.parent:', window.parent !== window ? '存在' : '不存在');
+        
+        if (window.parent && window.parent !== window) {
+            console.log('[Disk] 发送global-loading消息到父窗口');
+            window.parent.postMessage({
+                type: 'global-loading',
+                action: 'show',
+                text: text
+            }, '*');
+        } else {
+            console.warn('[Disk] 无法找到父窗口');
+        }
+    }
+
+    updateGlobalLoadingProgress(current, total, operation = '处理') {
+        // 更新loading进度
+        const text = `正在${operation} ${current}/${total} 张图片...`;
+        this.showGlobalLoading(text);
+    }
+    
+    hideGlobalLoading() {
+        // 通过postMessage通知父页面隐藏loading
+        console.log('[Disk] 隐藏全局Loading');
+        if (window.parent && window.parent !== window) {
+            window.parent.postMessage({
+                type: 'global-loading',
+                action: 'hide'
+            }, '*');
+        }
+    }
+
+    showGlobalAlert(message) {
+        // 通过postMessage通知父页面显示alert
+        if (window.parent && window.parent !== window) {
+            window.parent.postMessage({
+                type: 'global-alert',
+                message: message
+            }, '*');
+        } else {
+            // 降级处理:如果没有父窗口,使用全局alert(理论上不应该发生)
+            console.warn('[Disk] ⚠ 无父窗口,使用系统alert');
+            window.alert(message);
+        }
+    }
+
+    showGlobalConfirm(message) {
+        console.log('[Disk] → showGlobalConfirm 开始');
+        console.log('[Disk]   消息内容:', message);
+        console.log('[Disk]   window.parent 存在:', window.parent && window.parent !== window);
+        
+        // 通过postMessage通知父页面显示confirm对话框
+        return new Promise((resolve) => {
+            if (window.parent && window.parent !== window) {
+                // 生成唯一ID用于识别响应
+                const confirmId = 'confirm_' + Date.now();
+                console.log('[Disk]   生成确认ID:', confirmId);
+                
+                // 监听父窗口的响应
+                const handleResponse = (event) => {
+                    console.log('[Disk] ← 收到消息响应:', event.data);
+                    if (event.data && event.data.type === 'global-confirm-response' && event.data.id === confirmId) {
+                        console.log('[Disk] ✓ 匹配到确认响应,结果:', event.data.confirmed);
+                        window.removeEventListener('message', handleResponse);
+                        resolve(event.data.confirmed);
+                    }
+                };
+                window.addEventListener('message', handleResponse);
+                console.log('[Disk] ✓ 已注册消息监听器');
+                
+                // 发送确认请求到父窗口
+                console.log('[Disk] → 发送global-confirm消息到父窗口');
+                window.parent.postMessage({
+                    type: 'global-confirm',
+                    id: confirmId,
+                    message: message
+                }, '*');
+                console.log('[Disk] ✓ 消息已发送,等待用户操作...');
+        } else {
+            // 降级到原生confirm(理论上不应该发生)
+            console.warn('[Disk] ⚠ 无父窗口,使用原生confirm');
+            resolve(window.confirm(message));
+        }
+        });
+    }
+
+    handleContextMenuAction(action) {
+        console.log(`[Disk] 右键菜单动作: ${action}`);
+        switch (action) {
+            case 'new':
+                this.createFolder();
+                break;
+            case 'cut':
+                this.cutSelected();
+                break;
+            case 'copy':
+                this.copySelected();
+                break;
+            case 'paste':
+                this.pasteClipboard();
+                break;
+            case 'remove-bg':
+                console.log('[Disk] 触发一键抠背景功能');
+                this.removeBackgroundFromSelected();
+                break;
+            case 'crop-mini':
+                console.log('[Disk] 触发剪裁最小区域功能');
+                this.cropMiniFromSelected();
+                break;
+            default:
+                console.warn('[Disk] 未知的菜单动作:', action);
+                break;
+        }
+    }
+
+    initShortcutKeys() {
+        if (this.shortcutKeys) {
+            this.shortcutKeys.destroy();
+        }
+
+        this.shortcutKeys = new ShortcutKeys({
+            selection: this.selection,
+            onDelete: () => this.deleteSelected(),
+            onRename: () => this.renameSelected(),
+            onCopy: () => this.copySelected(),
+            onCut: () => this.cutSelected(),
+            onPaste: () => this.pasteClipboard()
+        });
+    }
+
+    initSearchBar() {
+        this.searchBar = new SearchBar({
+            input: this.searchInput,
+            clearButton: this.searchClear,
+            fileList: this.fileList,
+            emptyState: this.emptyState,
+            getFiles: () => this.files,
+            renderAll: () => this.renderFiles(),
+            createFileItem: (file) => this.createFileItem(file)
+        });
+    }
+
+    bindEvents() {
+        // 拖拽上传事件
+        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));
+        document.addEventListener('dragend', () => this.resetDropState());
+
+        // 按钮事件
+        if (this.fileInput) {
+            this.fileInput.addEventListener('change', (e) => this.handleFileSelect(e));
+        }
+        if (this.btnCreateFolder) {
+            this.btnCreateFolder.addEventListener('click', () => this.createFolder());
+        }
+
+        // 选择操作按钮
+        this.btnDownload.addEventListener('click', () => this.downloadSelected());
+        this.btnDelete.addEventListener('click', () => this.deleteSelected());
+        
+        // 上传按钮
+        if (this.btnUpload) {
+            this.btnUpload.addEventListener('click', () => {
+                if (this.fileInput) {
+                    const currentPath = this.pathNav.getPath();
+                    const isRootDir = !currentPath || currentPath === '';
+                    
+                    // 根目录只允许上传文件夹
+                    if (isRootDir) {
+                        this.fileInput.setAttribute('webkitdirectory', '');
+                        this.fileInput.removeAttribute('accept');
+                    } else {
+                        this.fileInput.removeAttribute('webkitdirectory');
+                        this.fileInput.setAttribute('accept', 'image/*');
+                    }
+                    
+                    this.fileInput.click();
+                }
+            });
+        }
+    }
+
+    // 下载选中的文件
+    downloadSelected() {
+        const selectedPaths = this.selection.getSelectedItems();
+        selectedPaths.forEach(filePath => {
+            const file = this.files.find(f => f.path === filePath);
+            if (file && file.type !== 'directory') {
+                this.downloadFile(filePath);
+            }
+        });
+    }
+
+    // 删除选中的文件
+    async deleteSelected() {
+        const count = this.selection.getSelectedCount();
+        if (count === 0) return;
+
+        const confirmMsg = `确定要删除选中的 ${count} 个文件/文件夹吗?`;
+        const confirmed = await this.showGlobalConfirm(confirmMsg);
+        
+        if (!confirmed) {
+            return;
+        }
+
+        try {
+            const response = await fetch('/api/disk/delete', {
+                method: 'POST',
+                headers: {
+                    'Content-Type': 'application/json'
+                },
+                body: JSON.stringify({
+                    paths: this.selection.getSelectedItems()
+                })
+            });
+
+            const data = await response.json();
+            if (data.success) {
+                this.selection.clearSelection();
+                this.loadFiles();
+            } else {
+                this.showGlobalAlert('删除失败: ' + data.message);
+            }
+        } catch (error) {
+            console.error('删除失败:', error);
+            this.showGlobalAlert('删除失败');
+        }
+    }
+
+    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);
+        }
+    }
+
+    copySelected() {
+        if (!this.selection || !this.selection.hasSelection()) {
+            return false;
+        }
+
+        const selectedPaths = this.selection.getSelectedItems();
+        this.clearCutVisuals();
+        this.clipboard = {
+            mode: 'copy',
+            items: selectedPaths.map(path => ({ path }))
+        };
+        return true;
+    }
+
+    cutSelected() {
+        if (!this.selection || !this.selection.hasSelection()) {
+            return false;
+        }
+
+        const selectedPaths = this.selection.getSelectedItems();
+        this.clipboard = {
+            mode: 'cut',
+            items: selectedPaths.map(path => ({ path }))
+        };
+        this.applyCutVisuals(selectedPaths);
+        return true;
+    }
+
+    async pasteClipboard() {
+        if (!this.clipboard || !this.clipboard.items || this.clipboard.items.length === 0) {
+            return false;
+        }
+
+        const targetFolder = this.pathNav.getPath();
+        const isCut = this.clipboard.mode === 'cut';
+        let hasSuccess = false;
+
+        for (const item of this.clipboard.items) {
+            let result = false;
+            if (isCut) {
+                result = await this.moveFile(item.path, targetFolder, { suppressReload: true });
+            } else {
+                result = await this.copyFile(item.path, targetFolder, { suppressReload: true });
+            }
+            hasSuccess = hasSuccess || result;
+        }
+
+        if (hasSuccess) {
+            await this.loadFiles();
+        }
+
+        if (isCut) {
+            this.clipboard = null;
+            this.clearCutVisuals();
+        }
+
+        return hasSuccess;
+    }
+
+    async removeBackgroundFromSelected() {
+        console.log('\n' + '='.repeat(70));
+        console.log('[Disk] 🎨 一键抠图功能被触发');
+        console.log('='.repeat(70));
+        
+        const selectedPaths = this.selection.getSelectedItems();
+        console.log(`[Disk] 获取选中项: ${selectedPaths.length} 个`);
+        
+        if (selectedPaths.length === 0) {
+            console.warn('[Disk] ⚠ 没有选中任何项');
+            this.showGlobalAlert('请先选择要抠图的文件夹');
+            return;
+        }
+
+        // getSelectedItems() 返回的已经是路径数组
+        console.log('[Disk] 选中的路径:', selectedPaths);
+        
+        // 确认操作
+        const confirmMsg = selectedPaths.length === 1 
+            ? `一键抠背景会覆盖原文件,确定要对"${selectedPaths[0]}"进行处理吗?`
+            : `一键抠背景会覆盖原文件,确定要对选中的 ${selectedPaths.length} 个文件夹进行处理吗?`;
+        
+        console.log('[Disk] → 准备显示确认对话框...');
+        console.log('[Disk]   确认消息:', confirmMsg);
+        
+        // 通过父窗口显示全局确认对话框
+        console.log('[Disk] → 调用 showGlobalConfirm()');
+        const confirmed = await this.showGlobalConfirm(confirmMsg);
+        console.log('[Disk] ← showGlobalConfirm() 返回,结果:', confirmed);
+        
+        if (!confirmed) {
+            console.log('[Disk] ✗ 用户取消操作,函数返回');
+            return;
+        }
+        console.log('[Disk] ✓ 用户确认操作,继续执行');
+
+        // 计算总图片数
+        let totalPngCount = 0;
+        selectedPaths.forEach(path => {
+            const fileInfo = this.files.find(f => f.path === path) || this.getFileFromCache(path);
+            console.log('[Disk] 检查文件:', path, '信息:', fileInfo);
+            if (fileInfo && fileInfo.pngCount) {
+                console.log('[Disk]   pngCount:', fileInfo.pngCount);
+                totalPngCount += fileInfo.pngCount;
+            }
+        });
+        
+        console.log('[Disk] 总PNG数量:', totalPngCount);
+
+        // 显示loading(带总数信息)
+        console.log('[Disk] → 显示全局Loading...');
+        const loadingText = totalPngCount > 0 
+            ? `正在处理 0/${totalPngCount} 张图片...`
+            : '正在处理中,请稍候...';
+        this.showGlobalLoading(loadingText);
+        console.log('[Disk] ✓ Loading已显示');
+        
+        // 保存总数,用于更新进度
+        this.currentProcessTotal = totalPngCount;
+
+        try {
+            console.log('[Disk] → 准备发送HTTP请求到服务器');
+            console.log('[Disk]   URL: /api/disk/remove-background');
+            console.log('[Disk]   方法: POST');
+            console.log('[Disk]   数据:', { paths: selectedPaths });
+            
+            // 使用fetch读取SSE流
+            const response = await fetch('/api/disk/remove-background', {
+                method: 'POST',
+                headers: {
+                    'Content-Type': 'application/json'
+                },
+                body: JSON.stringify({ paths: selectedPaths })
+            });
+
+            console.log('[Disk] ✓ HTTP响应收到');
+            console.log('[Disk]   状态码:', response.status);
+
+            if (!response.ok) {
+                throw new Error('服务器请求失败');
+            }
+
+            // 读取SSE流
+            console.log('[Disk] → 开始读取SSE流...');
+            const reader = response.body.getReader();
+            const decoder = new TextDecoder();
+            let buffer = '';
+            let finalResult = null;
+
+            while (true) {
+                const { done, value } = await reader.read();
+                
+                if (done) {
+                    console.log('[Disk] ✓ SSE流读取完成');
+                    break;
+                }
+
+                const chunk = decoder.decode(value, { stream: true });
+                console.log('[Disk] ← 收到数据块:', chunk.substring(0, 100));
+                buffer += chunk;
+                
+                const lines = buffer.split('\n\n');
+                buffer = lines.pop() || '';
+
+                for (const line of lines) {
+                    if (line.startsWith('data: ')) {
+                        const jsonStr = line.substring(6);
+                        try {
+                            const data = JSON.parse(jsonStr);
+                            console.log('[Disk] ← 解析到事件:', data);
+
+                            if (data.type === 'image-progress') {
+                                // 更新进度
+                                console.log(`[Disk] → 更新进度: ${data.current}/${data.total}`);
+                                this.updateGlobalLoadingProgress(data.current, data.total, '处理');
+                            } else if (data.type === 'complete') {
+                                console.log('[Disk] ← 收到完成事件');
+                                finalResult = data;
+                            } else if (data.type === 'error') {
+                                console.error('[Disk] ← 收到错误事件');
+                                throw new Error(data.message);
+                            }
+                        } catch (e) {
+                            console.error('[Disk] 解析SSE数据失败:', e, '原始数据:', jsonStr);
+                        }
+                    }
+                }
+            }
+
+            console.log('[Disk] ✓ 最终结果:', finalResult);
+
+            if (finalResult && finalResult.success) {
+                console.log('[Disk] ✓✓✓ 服务器处理成功!');
+                console.log('[Disk]   处理文件夹数:', finalResult.folders || 0);
+                console.log('[Disk]   处理图片数:', finalResult.processed || 0);
+                
+                console.log('[Disk] → 隐藏Loading...');
+                this.hideGlobalLoading();
+                console.log('[Disk] ✓ Loading已隐藏');
+                
+                const message = `处理完成!\n共处理 ${finalResult.processed || 0} 张图片`;
+                console.log('[Disk] → 显示成功提示');
+                this.showGlobalAlert(message);
+                
+                console.log('[Disk] → 清除图片缓存...');
+                await this.clearImageCache(selectedPaths);
+                console.log('[Disk] ✓ 缓存已清除');
+                
+                console.log('[Disk] → 刷新文件列表以更新预览图...');
+                await this.loadFiles();
+                console.log('[Disk] ✓ 文件列表已刷新');
+                console.log('='.repeat(70));
+                console.log('[Disk] 🎉🎉🎉 一键抠图完成!');
+                console.log('='.repeat(70) + '\n');
+            } else {
+                throw new Error(finalResult?.message || '处理失败');
+            }
+        } catch (error) {
+            console.error('\n' + '='.repeat(70));
+            console.error('[Disk] ✗✗✗ 客户端处理失败');
+            console.error('[Disk] 错误信息:', error.message);
+            console.error('[Disk] 错误堆栈:', error.stack);
+            console.error('='.repeat(70) + '\n');
+            
+            this.hideGlobalLoading();
+            this.showGlobalAlert('处理失败: ' + error.message);
+        }
+    }
+
+    async cropMiniFromSelected() {
+        console.log('[Disk] 剪裁最小区域功能被触发');
+        
+        const selectedPaths = this.selection.getSelectedItems();
+        console.log(`[Disk] 获取选中项: ${selectedPaths.length} 个`);
+        
+        if (selectedPaths.length === 0) {
+            this.showGlobalAlert('请先选择要剪裁的文件夹');
+            return;
+        }
+
+        // 计算总图片数
+        let totalPngCount = 0;
+        selectedPaths.forEach(path => {
+            const fileInfo = this.files.find(f => f.path === path) || this.getFileFromCache(path);
+            if (fileInfo && fileInfo.pngCount) {
+                totalPngCount += fileInfo.pngCount;
+            }
+        });
+
+        // 确认操作
+        const confirmMsg = selectedPaths.length === 1 
+            ? `确定要对"${selectedPaths[0]}"进行剪裁吗?`
+            : `确定要对选中的 ${selectedPaths.length} 个文件夹进行剪裁吗?`;
+        
+        const confirmed = await this.showGlobalConfirm(confirmMsg);
+        if (!confirmed) {
+            return;
+        }
+
+        // 显示loading
+        const loadingText = totalPngCount > 0 
+            ? `正在剪裁 0/${totalPngCount} 张图片...`
+            : '正在剪裁中,请稍候...';
+        this.showGlobalLoading(loadingText);
+        
+        // 保存总数,用于更新进度
+        this.currentProcessTotal = totalPngCount;
+
+        try {
+            // 使用fetch读取SSE流
+            const response = await fetch('/api/disk/crop-mini', {
+                method: 'POST',
+                headers: {
+                    'Content-Type': 'application/json'
+                },
+                body: JSON.stringify({ paths: selectedPaths })
+            });
+
+            if (!response.ok) {
+                throw new Error('服务器请求失败');
+            }
+
+            // 读取SSE流
+            const reader = response.body.getReader();
+            const decoder = new TextDecoder();
+            let buffer = '';
+            let finalResult = null;
+
+            while (true) {
+                const { done, value } = await reader.read();
+                
+                if (done) break;
+
+                buffer += decoder.decode(value, { stream: true });
+                const lines = buffer.split('\n\n');
+                buffer = lines.pop() || '';
+
+                for (const line of lines) {
+                    if (line.startsWith('data: ')) {
+                        const jsonStr = line.substring(6);
+                        try {
+                            const data = JSON.parse(jsonStr);
+
+                            if (data.type === 'image-progress') {
+                                this.updateGlobalLoadingProgress(data.current, data.total, '剪裁');
+                            } else if (data.type === 'complete') {
+                                finalResult = data;
+                            } else if (data.type === 'error') {
+                                throw new Error(data.message);
+                            }
+                        } catch (e) {
+                            console.error('[Disk] 解析SSE数据失败:', e);
+                        }
+                    }
+                }
+            }
+
+            if (finalResult && finalResult.success) {
+                this.hideGlobalLoading();
+                
+                const message = `剪裁完成!\n共处理 ${finalResult.processed || 0} 张图片`;
+                this.showGlobalAlert(message);
+                
+                // 清除缓存并刷新
+                await this.clearImageCache(selectedPaths);
+                await this.loadFiles();
+            } else {
+                throw new Error(finalResult?.message || '剪裁失败');
+            }
+        } catch (error) {
+            console.error('[Disk] 剪裁失败:', error);
+            this.hideGlobalLoading();
+            this.showGlobalAlert('剪裁失败: ' + error.message);
+        }
+    }
+
+    applyCutVisuals(paths = []) {
+        this.clearCutVisuals();
+        paths.forEach(path => {
+            const item = this.fileList.querySelector(`[data-path="${path}"]`);
+            if (item) {
+                item.classList.add('cut');
+            }
+        });
+    }
+
+    clearCutVisuals() {
+        this.fileList.querySelectorAll('.file-item.cut').forEach(item => item.classList.remove('cut'));
+    }
+
+    clearSearch(options) {
+        if (this.searchBar) {
+            this.searchBar.clear(options);
+        }
+    }
+
+    // 检查是否是外部文件拖入(而非内部拖拽操作)
+    isExternalFileDrag(e) {
+        return e.dataTransfer.types.includes('Files') && 
+               !e.dataTransfer.types.includes('text/plain');
+    }
+
+    // 拖拽处理
+    handleDragEnter(e) {
+        e.preventDefault();
+        e.stopPropagation();
+        
+        if (this.isExternalFileDrag(e)) {
+            this.dropZone.classList.add('drag-over');
+            this.dragCounter++;
+        }
+    }
+
+    handleDragOver(e) {
+        e.preventDefault();
+        e.stopPropagation();
+    }
+
+    handleDragLeave(e) {
+        e.preventDefault();
+        e.stopPropagation();
+        
+        if (this.isExternalFileDrag(e)) {
+            this.dragCounter = Math.max(0, this.dragCounter - 1);
+            if (this.dragCounter === 0) {
+                this.dropZone.classList.remove('drag-over');
+            }
+        }
+    }
+
+    async handleDrop(e) {
+        e.preventDefault();
+        e.stopPropagation();
+        this.dropZone.classList.remove('drag-over');
+        this.dragCounter = 0;
+
+        const items = e.dataTransfer.items;
+        if (!items) return;
+
+        const entries = [];
+        for (let i = 0; i < items.length; i++) {
+            const item = items[i].webkitGetAsEntry();
+            if (item) {
+                entries.push(item);
+            }
+        }
+
+        await this.processEntries(entries);
+    }
+
+    resetDropState() {
+        this.dragCounter = 0;
+        this.dropZone.classList.remove('drag-over');
+    }
+
+    async processEntries(entries) {
+        const currentPath = this.pathNav.getPath();
+        const isRootDir = !currentPath || currentPath === '';
+        
+        // 检查是否在根目录上传文件(非文件夹)
+        if (isRootDir) {
+            const hasFiles = entries.some(entry => entry.isFile);
+            if (hasFiles) {
+                this.showGlobalAlert('根目录只允许上传文件夹');
+                return;
+            }
+        }
+        
+        const filesToUpload = [];
+        
+        for (const entry of entries) {
+            await this.traverseEntry(entry, '', filesToUpload);
+        }
+
+        if (filesToUpload.length > 0) {
+            await this.uploadFiles(filesToUpload);
+        }
+    }
+
+    async traverseEntry(entry, relativePath, filesToUpload) {
+        if (entry.isFile) {
+            const file = await new Promise((resolve) => {
+                entry.file(resolve);
+            });
+            // 只接受图片格式的文件
+            if (this.isImageFile(file.name)) {
+                filesToUpload.push({
+                    file: file,
+                    path: relativePath + file.name
+                });
+            }
+            // 非图片格式的文件直接跳过
+        } else if (entry.isDirectory) {
+            const dirReader = entry.createReader();
+            const entries = await new Promise((resolve) => {
+                dirReader.readEntries(resolve);
+            });
+
+            for (const childEntry of entries) {
+                await this.traverseEntry(
+                    childEntry,
+                    relativePath + entry.name + '/',
+                    filesToUpload
+                );
+            }
+        }
+    }
+
+    handleFileSelect(e) {
+        const currentPath = this.pathNav.getPath();
+        const isRootDir = !currentPath || currentPath === '';
+        
+        const files = Array.from(e.target.files);
+        
+        if (files.length === 0) {
+            this.fileInput.value = '';
+            return;
+        }
+        
+        // 只接受图片格式的文件,其他格式直接跳过
+        const filesToUpload = files
+            .filter(file => this.isImageFile(file.name))
+            .map(file => ({
+                file: file,
+                path: file.webkitRelativePath || file.name
+            }));
+        
+        if (filesToUpload.length > 0) {
+            this.uploadFiles(filesToUpload);
+        }
+        this.fileInput.value = '';
+    }
+
+    async uploadFiles(filesToUpload) {
+        if (!filesToUpload.length) return;
+
+        // 显示全局loading遮罩
+        this.showGlobalLoading(`正在上传 ${filesToUpload.length} 个文件...`);
+        let hasSuccess = false;
+
+        for (let i = 0; i < filesToUpload.length; i++) {
+            const fileData = filesToUpload[i];
+            
+            // 更新进度文本
+            this.showGlobalLoading(`正在上传... (${i + 1}/${filesToUpload.length})`);
+
+            try {
+                await this.uploadFile(fileData, null);
+                hasSuccess = true;
+                // 每上传成功一个文件就立即刷新显示(静默刷新,不显示加载动画)
+                await this.loadFilesQuietly();
+            } catch (error) {
+                console.error('上传失败:', error);
+            }
+        }
+
+        // 所有文件处理完成后,隐藏loading
+        this.hideGlobalLoading();
+        
+        if (hasSuccess) {
+            // 上传成功后刷新文件列表
+            await this.loadFiles();
+        }
+    }
+
+    async uploadFile(fileData, progressItem) {
+        const formData = new FormData();
+        formData.append('file', fileData.file);
+        formData.append('path', this.pathNav.getPath());
+        formData.append('relativePath', fileData.path);
+
+        const response = await fetch('/api/disk/upload', {
+            method: 'POST',
+            body: formData
+        });
+
+        if (!response.ok) {
+            throw new Error('上传失败');
+        }
+
+        return response.json();
+    }
+
+    createProgressItem(fileName) {
+        if (!this.uploadProgressTemplate || !this.uploadProgressList) {
+            return null;
+        }
+
+        const fragment = this.uploadProgressTemplate.content.cloneNode(true);
+        const item = fragment.querySelector('.upload-progress-item');
+        if (!item) {
+            return null;
+        }
+
+        const nameEl = item.querySelector('.upload-progress-name');
+        if (nameEl) {
+            nameEl.textContent = fileName;
+        }
+
+        this.uploadProgressList.appendChild(fragment);
+        return this.uploadProgressList.lastElementChild;
+    }
+
+    updateProgressItem(item, status, message) {
+        if (!item) return;
+        const statusEl = item.querySelector('.upload-progress-status');
+        const barFill = item.querySelector('.upload-progress-bar-fill');
+        
+        if (statusEl) {
+            statusEl.textContent = message;
+        }
+        
+        if (!barFill) return;
+
+        if (status === 'success') {
+            barFill.style.width = '100%';
+            barFill.style.background = '#52c41a';
+        } else if (status === 'error') {
+            barFill.style.background = '#ff4d4f';
+        }
+    }
+
+    // 初始化文件结构缓存(递归加载所有文件夹信息)
+    async initFileStructureCache() {
+        if (this.cacheInitialized) {
+            return;
+        }
+        
+        try {
+            // console.log('[Disk] 开始初始化文件结构缓存...');
+            const response = await fetch('/api/disk/list?path=&recursive=true');
+            const data = await response.json();
+
+            if (data.success && data.files) {
+                // 构建缓存 Map
+                this.fileStructureCache.clear();
+                data.files.forEach(file => {
+                    this.fileStructureCache.set(file.path, file);
+                });
+                this.cacheInitialized = true;
+                // console.log(`[Disk] 文件结构缓存初始化完成,共 ${this.fileStructureCache.size} 个项目`);
+            }
+        } catch (error) {
+            console.error('[Disk] 初始化文件结构缓存失败:', error);
+        }
+    }
+
+    // 从缓存中获取文件信息
+    getFileFromCache(filePath) {
+        return this.fileStructureCache.get(filePath);
+    }
+
+    // 更新缓存中的单个文件信息
+    updateCacheItem(filePath, fileInfo) {
+        this.fileStructureCache.set(filePath, fileInfo);
+    }
+
+    // 从缓存中移除文件信息
+    removeCacheItem(filePath) {
+        this.fileStructureCache.delete(filePath);
+    }
+
+    // 清除图片缓存(用于抠图后更新预览)
+    async clearImageCache(folderPaths) {
+        try {
+            // 清除浏览器的图片缓存
+            if ('caches' in window) {
+                const cacheNames = await caches.keys();
+                for (const cacheName of cacheNames) {
+                    const cache = await caches.open(cacheName);
+                    const requests = await cache.keys();
+                    
+                    for (const request of requests) {
+                        const url = request.url;
+                        // 检查是否是处理过的文件夹的图片
+                        for (const folderPath of folderPaths) {
+                            if (url.includes(encodeURIComponent(folderPath))) {
+                                await cache.delete(request);
+                                console.log('[Disk] 清除缓存:', url);
+                            }
+                        }
+                    }
+                }
+            }
+        } catch (error) {
+            console.error('[Disk] 清除缓存失败:', error);
+        }
+    }
+
+    // 加载文件列表
+    async loadFiles() {
+        this.showLoading(true);
+        this.selection.clearSelection();
+        this.clearSearch();
+
+        try {
+            const response = await fetch(`/api/disk/list?path=${encodeURIComponent(this.pathNav.getPath())}`);
+            const data = await response.json();
+
+            if (data.success) {
+                this.files = data.files;
+                
+                // 更新缓存
+                data.files.forEach(file => {
+                    this.updateCacheItem(file.path, file);
+                });
+                
+                this.renderFiles();
+            }
+        } catch (error) {
+            console.error('加载文件列表失败:', error);
+        } finally {
+            this.showLoading(false);
+        }
+    }
+
+    // 静默加载文件列表(用于上传过程中增量更新,不显示加载动画,不闪烁)
+    async loadFilesQuietly() {
+        try {
+            const response = await fetch(`/api/disk/list?path=${encodeURIComponent(this.pathNav.getPath())}`);
+            const data = await response.json();
+
+            if (data.success) {
+                this.files = data.files;
+                this.renderFilesSmooth();
+            }
+        } catch (error) {
+            console.error('加载文件列表失败:', error);
+        }
+    }
+
+    renderFiles() {
+        this.fileList.innerHTML = '';
+
+        if (this.files.length === 0) {
+            this.emptyState.classList.add('show');
+            return;
+        }
+
+        this.emptyState.classList.remove('show');
+
+        this.files.forEach(file => {
+            const fileItem = this.createFileItem(file);
+            this.fileList.appendChild(fileItem);
+        });
+    }
+
+    // 平滑渲染文件列表(增量更新,避免闪烁)
+    renderFilesSmooth() {
+        if (this.files.length === 0) {
+            this.emptyState.classList.add('show');
+            this.fileList.innerHTML = '';
+            return;
+        }
+
+        this.emptyState.classList.remove('show');
+
+        // 获取当前已存在的文件路径
+        const existingPaths = new Set();
+        const existingItems = this.fileList.querySelectorAll('.file-item');
+        existingItems.forEach(item => {
+            existingPaths.add(item.dataset.path);
+        });
+
+        // 创建新文件路径集合
+        const newPaths = new Set(this.files.map(f => f.path));
+
+        // 删除不存在的文件项
+        existingItems.forEach(item => {
+            if (!newPaths.has(item.dataset.path)) {
+                item.remove();
+            }
+        });
+
+        // 只添加新文件项
+        this.files.forEach(file => {
+            if (!existingPaths.has(file.path)) {
+                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;
+        div.draggable = true;
+
+        const size = file.type === 'directory' ? '' : this.formatFileSize(file.size);
+        const isImage = this.isImageFile(file.name);
+
+        // 勾选标记
+        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>
+        `;
+
+        // 检查是否是包含PNG的文件夹(通过名称模式判断)
+        const isAnimationFolder = file.type === 'directory' && this.isAnimationFolder(file.name);
+
+        if (isAnimationFolder) {
+            // 动画文件夹显示第一帧预览
+            // 使用服务器提供的预览信息(完全避免404错误)
+            if (file.hasPreview && file.previewUrl) {
+                // 服务器确认有预览图,直接使用服务器提供的URL
+                // 添加时间戳防止缓存
+                const previewUrl = file.previewUrl + '&t=' + Date.now();
+                // console.log(`[Disk] 文件夹 ${file.name} 有预览图`);
+                div.innerHTML = `
+                    ${checkMark}
+                    <div class="file-thumbnail folder-preview">
+                        <img src="${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 {
+                // 服务器确认没有预览图,直接显示文件夹图标
+                // console.log(`[Disk] 文件夹 ${file.name} 无预览图,显示图标`);
+                const icon = this.getFileIcon(file);
+                div.innerHTML = `
+                    ${checkMark}
+                    <div class="file-icon">
+                        ${icon}
+                    </div>
+                    <div class="file-name">${file.name}</div>
+                    <input type="text" class="rename-input" style="display: none;">
+                `;
+            }
+        } else if (isImage && file.type !== 'directory') {
+            // 添加时间戳防止缓存
+            const previewUrl = `/api/disk/preview?path=${encodeURIComponent(file.path)}&t=${Date.now()}`;
+            div.innerHTML = `
+                ${checkMark}
+                <div class="file-thumbnail">
+                    <img src="${previewUrl}" alt="${file.name}" loading="lazy">
+                </div>
+                <div class="file-name">${file.name}</div>
+                <input type="text" class="rename-input" style="display: none;">
+                ${size ? `<div class="file-info">${size}</div>` : ''}
+            `;
+        } else {
+            const icon = this.getFileIcon(file);
+            div.innerHTML = `
+                ${checkMark}
+                <div class="file-icon">
+                    ${icon}
+                </div>
+                <div class="file-name">${file.name}</div>
+                <input type="text" class="rename-input" style="display: none;">
+                ${size ? `<div class="file-info">${size}</div>` : ''}
+            `;
+        }
+
+        // 用于区分单击和双击的定时器
+        let clickTimer = null;
+        
+        // 单击选中(Windows 11 行为)
+        div.addEventListener('click', (e) => {
+            // 如果正在重命名,不处理
+            if (e.target.classList.contains('rename-input')) {
+                return;
+            }
+            e.stopPropagation();
+            
+            const isAlreadySelected = 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) {
+                    // Ctrl+点击:切换选中状态(多选)
+                    this.selection.toggleSelection(div);
+                } else if (clickedOnName && isAlreadySelected) {
+                    // 点击已选中项的名称:触发重命名
+                    this.startRename(div);
+                } else {
+                    // 普通点击:选中当前项,取消其他选择
+                    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') {
+                this.navigateToPath(file.path);
+            } else {
+                this.downloadFile(file.path);
+            }
+        });
+
+        // 拖拽开始
+        div.addEventListener('dragstart', (e) => {
+            e.stopPropagation();
+            div.classList.add('dragging');
+            e.dataTransfer.effectAllowed = 'move';
+            e.dataTransfer.setData('text/plain', JSON.stringify({
+                type: file.type, // 'directory' 或 'file'
+                path: file.path,
+                name: file.name,
+                isDirectory: file.type === 'directory',
+                pngCount: file.pngCount || 0, // PNG文件数量
+                hasPreview: file.hasPreview || false, // 是否有预览图
+                needsMatting: file.needsMatting || false // 是否需要抠图
+            }));
+        });
+
+        div.addEventListener('dragend', () => {
+            div.classList.remove('dragging');
+            document.querySelectorAll('.file-item.drag-target').forEach(el => {
+                el.classList.remove('drag-target');
+            });
+        });
+
+        // 文件夹可以接收拖拽
+        if (file.type === 'directory') {
+            div.addEventListener('dragover', (e) => {
+                e.preventDefault();
+                e.stopPropagation();
+                const draggingEl = document.querySelector('.file-item.dragging');
+                if (draggingEl && draggingEl !== div) {
+                    div.classList.add('drag-target');
+                    e.dataTransfer.dropEffect = 'move';
+                }
+            });
+
+            div.addEventListener('dragleave', (e) => {
+                e.stopPropagation();
+                div.classList.remove('drag-target');
+            });
+
+            div.addEventListener('drop', async (e) => {
+                e.preventDefault();
+                e.stopPropagation();
+                div.classList.remove('drag-target');
+
+                try {
+                    const data = JSON.parse(e.dataTransfer.getData('text/plain'));
+                    if (data.type === 'move-file' && data.path !== file.path) {
+                        await this.moveFile(data.path, file.path);
+                    }
+                } catch (error) {
+                    // 可能是外部文件拖拽,忽略
+                }
+            });
+        }
+
+        return div;
+    }
+
+    // 移动文件/文件夹
+    async moveFile(sourcePath, targetFolder, options = {}) {
+        const { suppressReload = false, silent = false } = options;
+        try {
+            const response = await fetch('/api/disk/move', {
+                method: 'POST',
+                headers: {
+                    'Content-Type': 'application/json'
+                },
+                body: JSON.stringify({
+                    sourcePath,
+                    targetFolder
+                })
+            });
+
+            const data = await response.json();
+            if (data.success) {
+                if (!suppressReload) {
+                    await this.loadFiles();
+                }
+                return true;
+            } else {
+                if (!silent) {
+                    this.showGlobalAlert('移动失败: ' + data.message);
+                }
+                return false;
+            }
+        } catch (error) {
+            console.error('移动失败:', error);
+            if (!silent) {
+                this.showGlobalAlert('移动失败');
+            }
+            return false;
+        }
+    }
+
+    getFileIcon(file) {
+        if (file.type === 'directory') {
+            return `
+                <svg class="folder-icon" 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>
+            `;
+        }
+
+        const ext = file.name.split('.').pop().toLowerCase();
+        
+        if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext)) {
+            return `
+                <svg class="file-icon-image" viewBox="0 0 64 64" fill="currentColor">
+                    <path d="M12 8h40a4 4 0 014 4v40a4 4 0 01-4 4H12a4 4 0 01-4-4V12a4 4 0 014-4z"/>
+                    <circle cx="20" cy="20" r="4" fill="white"/>
+                    <path d="M8 48l16-16 12 12 16-20v24H8z" fill="white" opacity="0.8"/>
+                </svg>
+            `;
+        }
+
+        if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv'].includes(ext)) {
+            return `
+                <svg class="file-icon-video" viewBox="0 0 64 64" fill="currentColor">
+                    <path d="M12 12h40a4 4 0 014 4v32a4 4 0 01-4 4H12a4 4 0 01-4-4V16a4 4 0 014-4z"/>
+                    <path d="M24 20l20 12-20 12z" fill="white"/>
+                </svg>
+            `;
+        }
+
+        if (['mp3', 'wav', 'ogg', 'flac', 'aac'].includes(ext)) {
+            return `
+                <svg class="file-icon-audio" viewBox="0 0 64 64" fill="currentColor">
+                    <path d="M12 12h40a4 4 0 014 4v32a4 4 0 01-4 4H12a4 4 0 01-4-4V16a4 4 0 014-4z"/>
+                    <path d="M24 20h4v20a6 6 0 11-4-5.66V20zm20-4v20a6 6 0 11-4-5.66V16h4z" fill="white"/>
+                </svg>
+            `;
+        }
+
+        if (['zip', 'rar', '7z', 'tar', 'gz'].includes(ext)) {
+            return `
+                <svg class="file-icon-zip" viewBox="0 0 64 64" fill="currentColor">
+                    <path d="M16 8h32a4 4 0 014 4v40a4 4 0 01-4 4H16a4 4 0 01-4-4V12a4 4 0 014-4z"/>
+                    <rect x="28" y="12" width="8" height="4" fill="white"/>
+                    <rect x="28" y="20" width="8" height="4" fill="white"/>
+                    <rect x="28" y="28" width="8" height="4" fill="white"/>
+                    <rect x="28" y="36" width="8" height="8" rx="2" fill="white"/>
+                </svg>
+            `;
+        }
+
+        if (['doc', 'docx', 'txt', 'pdf', 'xls', 'xlsx', 'ppt', 'pptx'].includes(ext)) {
+            return `
+                <svg class="file-icon-document" viewBox="0 0 64 64" fill="currentColor">
+                    <path d="M16 8h24l12 12v32a4 4 0 01-4 4H16a4 4 0 01-4-4V12a4 4 0 014-4z"/>
+                    <path d="M40 8v12h12" fill="white" opacity="0.5"/>
+                    <rect x="20" y="28" width="24" height="2" fill="white"/>
+                    <rect x="20" y="34" width="24" height="2" fill="white"/>
+                    <rect x="20" y="40" width="16" height="2" fill="white"/>
+                </svg>
+            `;
+        }
+
+        return `
+            <svg class="file-icon-default" viewBox="0 0 64 64" fill="currentColor">
+                <path d="M16 8h24l12 12v32a4 4 0 01-4 4H16a4 4 0 01-4-4V12a4 4 0 014-4z"/>
+                <path d="M40 8v12h12" fill="white" opacity="0.5"/>
+            </svg>
+        `;
+    }
+
+    formatFileSize(bytes) {
+        if (bytes === 0) return '0 B';
+        const k = 1024;
+        const sizes = ['B', 'KB', 'MB', 'GB'];
+        const i = Math.floor(Math.log(bytes) / Math.log(k));
+        return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
+    }
+
+    isImageFile(fileName) {
+        const ext = fileName.split('.').pop().toLowerCase();
+        return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext);
+    }
+    
+    isAnimationFolder(folderName) {
+        // 判断是否是动画文件夹(通过名称模式)
+        // 放宽匹配规则,让更多文件夹能显示预览图
+        return /^player_\d+$/i.test(folderName) || 
+               /^(idle|walk|run|attack|死亡|站立|行走|攻击)/i.test(folderName) ||
+               /动画|animation|ani|sprite|序列|sequence/i.test(folderName) ||
+               // 新增:所有文件夹都尝试显示预览图(如果有图片就显示)
+               true; // 默认返回true,让所有文件夹都尝试加载预览图
+    }
+
+    // 注意:guessFirstFrameName、tryAlternativePreview 和 showFolderIcon 方法已废弃
+    // 现在使用服务器端提供的预览信息,完全避免客户端的404错误
+
+    // 导航到指定路径
+    navigateToPath(path) {
+        this.pathNav.navigateTo(path);
+    }
+
+    // 创建文件夹
+    async createFolder() {
+        let folderName = '新建文件夹';
+        let counter = 1;
+        
+        while (this.files.some(f => f.name === folderName && f.type === 'directory')) {
+            folderName = `新建文件夹 (${counter})`;
+            counter++;
+        }
+
+        try {
+            const response = await fetch('/api/disk/create-folder', {
+                method: 'POST',
+                headers: {
+                    'Content-Type': 'application/json'
+                },
+                body: JSON.stringify({
+                    path: this.pathNav.getPath(),
+                    name: folderName
+                })
+            });
+
+            const data = await response.json();
+            if (data.success) {
+                await this.loadFiles();
+                const newFolderItem = this.fileList.querySelector(`[data-name="${folderName}"]`);
+                if (newFolderItem) {
+                    this.startRename(newFolderItem);
+                }
+            } else {
+                this.showGlobalAlert('创建文件夹失败: ' + data.message);
+            }
+        } catch (error) {
+            console.error('创建文件夹失败:', error);
+            this.showGlobalAlert('创建文件夹失败');
+        }
+    }
+
+    // 开始重命名(参考 Windows 行为)
+    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;
+
+        // 标记当前正在重命名
+        this._isRenaming = true;
+
+        // 显示输入框,隐藏文件名
+        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 = () => {
+            this._isRenaming = false;
+            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();
+        };
+
+        // 使用 addEventListener 确保事件被捕获
+        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('/api/disk/rename', {
+                method: 'POST',
+                headers: {
+                    'Content-Type': 'application/json'
+                },
+                body: JSON.stringify({
+                    oldPath: oldPath,
+                    newName: newName
+                })
+            });
+
+            const data = await response.json();
+            if (data.success) {
+                this.loadFiles();
+            } else {
+                this.showGlobalAlert('重命名失败: ' + data.message);
+                this.loadFiles();
+            }
+        } catch (error) {
+            console.error('重命名失败:', error);
+            this.showGlobalAlert('重命名失败');
+            this.loadFiles();
+        }
+    }
+
+    // 下载文件
+    downloadFile(filePath) {
+        window.open(`/api/disk/download?path=${encodeURIComponent(filePath)}`, '_blank');
+    }
+
+    async copyFile(sourcePath, targetFolder, options = {}) {
+        const { suppressReload = false, silent = false } = options;
+        try {
+            const response = await fetch('/api/disk/copy', {
+                method: 'POST',
+                headers: {
+                    'Content-Type': 'application/json'
+                },
+                body: JSON.stringify({
+                    sourcePath,
+                    targetFolder
+                })
+            });
+
+            const data = await response.json();
+            if (data.success) {
+                if (!suppressReload) {
+                    await this.loadFiles();
+                }
+                return true;
+            } else {
+                if (!silent) {
+                    this.showGlobalAlert('复制失败: ' + data.message);
+                }
+                return false;
+            }
+        } catch (error) {
+            console.error('复制失败:', error);
+            if (!silent) {
+                this.showGlobalAlert('复制失败');
+            }
+            return false;
+        }
+    }
+
+    showLoading(show) {
+        if (show) {
+            this.loading.classList.add('show');
+        } else {
+            this.loading.classList.remove('show');
+        }
+    }
+}
+
+// 初始化
+document.addEventListener('DOMContentLoaded', () => {
+    window.diskManager = new DiskManager();
+});

+ 287 - 0
client/js/disk/multiple-selection.js

@@ -0,0 +1,287 @@
+// 多选框选功能模块
+
+class MultipleSelection {
+    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 (count > 0) {
+            this.selectionBar.classList.add('show');
+            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();
+    }
+}
+

+ 144 - 0
client/js/disk/path.js

@@ -0,0 +1,144 @@
+// 路径导航模块(面包屑导航)
+
+class PathNavigator {
+    constructor(options) {
+        // 面包屑容器元素
+        this.container = options.container;
+        
+        // 根目录名称
+        this.rootName = options.rootName || '全部文件';
+        
+        // 当前路径
+        this.currentPath = '';
+        
+        // 路径变化回调
+        this.onNavigate = options.onNavigate || null;
+        
+        this.init();
+    }
+
+    init() {
+        this.bindEvents();
+        this.render();
+    }
+
+    bindEvents() {
+        // 面包屑点击事件
+        this.container.addEventListener('click', (e) => {
+            if (e.target.classList.contains('breadcrumb-item')) {
+                const path = e.target.dataset.path;
+                this.navigateTo(path);
+            }
+        });
+    }
+
+    // 导航到指定路径
+    navigateTo(path) {
+        if (this.currentPath === path) return;
+        
+        this.currentPath = path;
+        this.render();
+        
+        // 触发回调
+        if (this.onNavigate) {
+            this.onNavigate(path);
+        }
+    }
+
+    // 获取当前路径
+    getPath() {
+        return this.currentPath;
+    }
+
+    // 设置路径(不触发回调,仅更新显示)
+    setPath(path) {
+        this.currentPath = path;
+        this.render();
+    }
+
+    // 渲染面包屑
+    render() {
+        this.container.innerHTML = '';
+
+        // 添加根目录
+        const rootItem = document.createElement('span');
+        rootItem.className = 'breadcrumb-item';
+        rootItem.textContent = this.rootName;
+        rootItem.dataset.path = '';
+        if (this.currentPath === '') {
+            rootItem.classList.add('active');
+        }
+        this.container.appendChild(rootItem);
+
+        // 添加路径中的各个部分
+        if (this.currentPath) {
+            const parts = this.currentPath.split('/').filter(p => p);
+            let accumulatedPath = '';
+
+            parts.forEach((part, index) => {
+                accumulatedPath += part;
+                
+                const item = document.createElement('span');
+                item.className = 'breadcrumb-item';
+                item.textContent = part;
+                item.dataset.path = accumulatedPath;
+                
+                if (index === parts.length - 1) {
+                    item.classList.add('active');
+                }
+                
+                this.container.appendChild(item);
+                
+                if (index < parts.length - 1) {
+                    accumulatedPath += '/';
+                }
+            });
+        }
+    }
+
+    // 返回上一级
+    goUp() {
+        if (!this.currentPath) return;
+        
+        const parts = this.currentPath.split('/').filter(p => p);
+        parts.pop();
+        const parentPath = parts.join('/');
+        
+        this.navigateTo(parentPath);
+    }
+
+    // 进入子目录
+    goInto(folderName) {
+        const newPath = this.currentPath 
+            ? `${this.currentPath}/${folderName}`
+            : folderName;
+        
+        this.navigateTo(newPath);
+    }
+
+    // 回到根目录
+    goRoot() {
+        this.navigateTo('');
+    }
+
+    // 获取当前目录名
+    getCurrentFolderName() {
+        if (!this.currentPath) return this.rootName;
+        const parts = this.currentPath.split('/').filter(p => p);
+        return parts[parts.length - 1] || this.rootName;
+    }
+
+    // 获取父目录路径
+    getParentPath() {
+        if (!this.currentPath) return '';
+        const parts = this.currentPath.split('/').filter(p => p);
+        parts.pop();
+        return parts.join('/');
+    }
+
+    // 判断是否在根目录
+    isAtRoot() {
+        return this.currentPath === '';
+    }
+}
+

+ 98 - 0
client/js/disk/right-click-menu.js

@@ -0,0 +1,98 @@
+(() => {
+    class RightClickMenu {
+        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) 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;
+            
+            // 如果有回调,在显示菜单前调用
+            if (this.onBeforeShow) {
+                this.onBeforeShow(e);
+            }
+            
+            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.RightClickMenu = RightClickMenu;
+})();

+ 115 - 0
client/js/disk/search-bar.js

@@ -0,0 +1,115 @@
+(() => {
+    /**
+     * 统一管理搜索输入、模糊匹配与结果渲染
+     */
+    class SearchBar {
+        constructor(options = {}) {
+            this.input = options.input || null;
+            this.clearButton = options.clearButton || null;
+            this.fileList = options.fileList || null;
+            this.emptyState = options.emptyState || null;
+            this.getFiles = options.getFiles || (() => []);
+            this.renderAll = options.renderAll || (() => {});
+            this.createFileItem = options.createFileItem || (() => null);
+            this.noResultMessage = options.noResultMessage || '没有找到匹配的文件';
+
+            this.emptyStatePrimary = this.emptyState ? this.emptyState.querySelector('p') : null;
+            this.defaultEmptyMessage = this.emptyStatePrimary ? this.emptyStatePrimary.textContent : '';
+
+            this.bindEvents();
+            this.toggleClearButton(this.getKeyword());
+        }
+
+        bindEvents() {
+            if (this.input) {
+                this.input.addEventListener('input', () => this.handleInput());
+            }
+
+            if (this.clearButton) {
+                this.clearButton.addEventListener('click', () => this.clear());
+            }
+        }
+
+        handleInput() {
+            const keyword = this.getKeyword();
+            this.toggleClearButton(keyword);
+
+            if (!keyword) {
+                this.restoreEmptyStateMessage();
+                this.renderAll();
+                return;
+            }
+
+            const filteredFiles = this.filterFiles(keyword);
+            this.renderFilteredFiles(filteredFiles);
+        }
+
+        getKeyword() {
+            if (!this.input) return '';
+            return this.input.value.trim().toLowerCase();
+        }
+
+        filterFiles(keyword) {
+            const files = this.getFiles() || [];
+            return files.filter(file => {
+                if (!file || !file.name) return false;
+                return file.name.toLowerCase().includes(keyword);
+            });
+        }
+
+        renderFilteredFiles(files) {
+            if (!this.fileList) return;
+
+            this.fileList.innerHTML = '';
+
+            if (!files.length) {
+                if (this.emptyState) {
+                    this.emptyState.classList.add('show');
+                }
+                if (this.emptyStatePrimary) {
+                    this.emptyStatePrimary.textContent = this.noResultMessage;
+                }
+                return;
+            }
+
+            if (this.emptyState) {
+                this.emptyState.classList.remove('show');
+            }
+
+            files.forEach(file => {
+                const item = this.createFileItem(file);
+                if (item) {
+                    this.fileList.appendChild(item);
+                }
+            });
+        }
+
+        clear(options = {}) {
+            const { shouldRender = true } = options;
+
+            if (this.input) {
+                this.input.value = '';
+            }
+
+            this.toggleClearButton('');
+            this.restoreEmptyStateMessage();
+
+            if (shouldRender) {
+                this.renderAll();
+            }
+        }
+
+        toggleClearButton(keyword) {
+            if (!this.clearButton) return;
+            this.clearButton.style.display = keyword ? 'flex' : 'none';
+        }
+
+        restoreEmptyStateMessage() {
+            if (this.emptyStatePrimary) {
+                this.emptyStatePrimary.textContent = this.defaultEmptyMessage;
+            }
+        }
+    }
+
+    window.SearchBar = SearchBar;
+})();

+ 93 - 0
client/js/disk/shortcut-keys.js

@@ -0,0 +1,93 @@
+(() => {
+    /**
+     * 统一管理网盘的键盘快捷键
+     */
+    class ShortcutKeys {
+        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);
+        }
+
+        destroy() {
+            document.removeEventListener('keydown', this.handleKeyDown);
+        }
+
+        handleKeyDown(e) {
+            if (this.shouldIgnoreTarget(e.target)) {
+                return;
+            }
+
+            // Delete
+            if (e.key === 'Delete') {
+                if (this.selection && this.selection.hasSelection()) {
+                    e.preventDefault();
+                    this.onDelete && this.onDelete();
+                }
+                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.ShortcutKeys = ShortcutKeys;
+})();

+ 133 - 0
client/js/export-view-manager.js

@@ -0,0 +1,133 @@
+/**
+ * 导出动画弹出框管理器
+ * 负责管理 export-view iframe 的显示和隐藏
+ */
+class ExportViewManager {
+    constructor() {
+        this.frame = null;
+        this.isShowing = false;
+        this.resolveCallback = null;
+        this.init();
+    }
+
+    init() {
+        this.frame = document.getElementById('exportViewFrame');
+        if (!this.frame) {
+            // console.error('[ExportViewManager] exportViewFrame 未找到');
+            return;
+        }
+
+        // 监听来自 export-view 的消息
+        window.addEventListener('message', (event) => {
+            if (event.origin !== window.location.origin) {
+                return;
+            }
+
+            const { data } = event;
+            if (data && data.type === 'close-export-view') {
+                // console.log('[ExportViewManager] 收到关闭消息');
+                this.hide();
+            } else if (data && data.type === 'export-confirmed') {
+                // console.log('[ExportViewManager] 用户确认导出');
+                this.handleExportConfirmed(data);
+            }
+        });
+    }
+
+    /**
+     * 显示导出弹出框
+     * @param {string} folderName - 文件夹名称
+     * @returns {Promise<boolean>} - 返回用户是否确认导出
+     */
+    async show(folderName) {
+        // console.log('[ExportViewManager] → show() 被调用');
+        // console.log('[ExportViewManager]   文件夹名称:', folderName);
+
+        if (!this.frame) {
+            // console.error('[ExportViewManager] frame 未初始化');
+            return false;
+        }
+
+        return new Promise((resolve) => {
+            this.resolveCallback = resolve;
+            this.isShowing = true;
+
+            // 显示 iframe
+            this.frame.style.display = 'block';
+
+            // 等待 iframe 加载完成后发送文件夹名称
+            const sendFolderName = () => {
+                if (this.frame.contentWindow) {
+                    this.frame.contentWindow.postMessage({
+                        type: 'generate-export-preview',
+                        folderName: folderName
+                    }, '*');
+                    // console.log('[ExportViewManager] ✓ 已发送文件夹名称到 export-view');
+                } else {
+                    // 如果 iframe 还没加载完,等待一下再试
+                    setTimeout(sendFolderName, 100);
+                }
+            };
+
+            // 监听 iframe 加载完成
+            const handleLoad = () => {
+                // console.log('[ExportViewManager] ✓ iframe 加载完成');
+                // 等待一小段时间确保 iframe 内容完全初始化
+                setTimeout(() => {
+                    sendFolderName();
+                }, 100);
+            };
+            
+            this.frame.onload = handleLoad;
+
+            // 如果 iframe 已经加载,立即发送
+            if (this.frame.contentDocument && this.frame.contentDocument.readyState === 'complete') {
+                handleLoad();
+            } else {
+                // 重新加载 iframe 以确保它处于干净状态
+                this.frame.src = this.frame.src;
+            }
+        });
+    }
+
+    /**
+     * 隐藏导出弹出框
+     */
+    hide() {
+        // console.log('[ExportViewManager] → hide() 被调用');
+        
+        if (this.frame) {
+            this.frame.style.display = 'none';
+        }
+        
+        this.isShowing = false;
+        
+        // 如果还有未完成的 Promise,返回 false(取消)
+        if (this.resolveCallback) {
+            this.resolveCallback(false);
+            this.resolveCallback = null;
+        }
+    }
+
+    /**
+     * 处理用户确认导出
+     * @param {Object} data - 导出数据
+     */
+    handleExportConfirmed(data) {
+        // console.log('[ExportViewManager] 处理导出确认');
+        
+        if (this.resolveCallback) {
+            this.resolveCallback(true);
+            this.resolveCallback = null;
+        }
+        
+        // 关闭弹出框
+        this.hide();
+    }
+}
+
+// 全局单例
+// console.log('[ExportViewManager] 创建全局实例...');
+window.ExportViewManager = new ExportViewManager();
+// console.log('[ExportViewManager] ✓ 全局实例已创建');
+

+ 214 - 0
client/js/export-view.js

@@ -0,0 +1,214 @@
+/**
+ * 导出动画弹出框
+ */
+class ExportView {
+    constructor() {
+        this.canvas = null;
+        this.ctx = null;
+        this.previewPlaceholder = null;
+        this.referenceInput = null;
+        this.referencePreview = null;
+        this.referenceUpload = null;
+        this.cancelBtn = null;
+        this.confirmBtn = null;
+        this.referenceImage = null;
+        this.spritesheetData = null;
+        
+        this.init();
+    }
+
+    init() {
+        this.canvas = document.getElementById('spritesheetCanvas');
+        this.ctx = this.canvas?.getContext('2d');
+        this.previewPlaceholder = document.getElementById('previewPlaceholder');
+        this.referenceInput = document.getElementById('referenceInput');
+        this.referencePreview = document.getElementById('referencePreview');
+        this.referenceUpload = document.getElementById('referenceUpload');
+        this.cancelBtn = document.getElementById('exportCancelBtn');
+        this.confirmBtn = document.getElementById('exportConfirmBtn');
+        
+        this.bindEvents();
+        
+        // 初始时禁用确定按钮
+        if (this.confirmBtn) {
+            this.confirmBtn.disabled = true;
+        }
+    }
+
+    bindEvents() {
+        // 取消按钮
+        this.cancelBtn?.addEventListener('click', () => {
+            this.close();
+        });
+
+        // 确定按钮
+        this.confirmBtn?.addEventListener('click', () => {
+            this.downloadSpritesheet();
+        });
+
+        // 参考图上传区点击
+        this.referenceUpload?.addEventListener('click', () => {
+            this.referenceInput?.click();
+        });
+
+        // 参考图选择
+        this.referenceInput?.addEventListener('change', (e) => {
+            const file = e.target.files[0];
+            if (file) {
+                this.loadReferenceImage(file);
+            }
+        });
+
+        // 拖拽上传参考图
+        this.referenceUpload?.addEventListener('dragover', (e) => {
+            e.preventDefault();
+            e.stopPropagation();
+            this.referenceUpload.style.borderColor = '#667eea';
+        });
+
+        this.referenceUpload?.addEventListener('dragleave', (e) => {
+            e.preventDefault();
+            e.stopPropagation();
+            this.referenceUpload.style.borderColor = '';
+        });
+
+        this.referenceUpload?.addEventListener('drop', (e) => {
+            e.preventDefault();
+            e.stopPropagation();
+            this.referenceUpload.style.borderColor = '';
+            
+            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 === 'generate-spritesheet') {
+                // console.log('[ExportView] 收到生成Spritesheet消息:', event.data);
+                this.generateSpritesheet(event.data.folderName, event.data.frames);
+            }
+        });
+    }
+
+    loadReferenceImage(file) {
+        const reader = new FileReader();
+        reader.onload = (e) => {
+            this.referenceImage = new Image();
+            this.referenceImage.onload = () => {
+                this.referencePreview.innerHTML = '';
+                this.referencePreview.classList.add('has-image');
+                this.referencePreview.appendChild(this.referenceImage);
+                // console.log('[ExportView] 参考图已加载');
+            };
+            this.referenceImage.src = e.target.result;
+        };
+        reader.readAsDataURL(file);
+    }
+
+    async generateSpritesheet(folderName, frames) {
+        // console.log('[ExportView] 开始生成Spritesheet');
+        // console.log('[ExportView]   文件夹:', folderName);
+        // console.log('[ExportView]   帧数:', frames?.length);
+        
+        if (!frames || frames.length === 0) {
+            // console.error('[ExportView] 没有帧数据');
+            return;
+        }
+
+        try {
+            // 加载所有帧
+            const images = await Promise.all(
+                frames.map(frameUrl => {
+                    return new Promise((resolve, reject) => {
+                        const img = new Image();
+                        img.crossOrigin = 'anonymous';
+                        img.onload = () => resolve(img);
+                        img.onerror = reject;
+                        img.src = frameUrl;
+                    });
+                })
+            );
+
+            // console.log('[ExportView] 所有帧已加载');
+
+            // 计算spritesheet尺寸
+            const frameWidth = images[0].width;
+            const frameHeight = images[0].height;
+            const cols = Math.ceil(Math.sqrt(images.length));
+            const rows = Math.ceil(images.length / cols);
+
+            this.canvas.width = frameWidth * cols;
+            this.canvas.height = frameHeight * rows;
+
+            // console.log('[ExportView] 画布尺寸:', this.canvas.width, 'x', this.canvas.height);
+
+            // 绘制所有帧
+            images.forEach((img, index) => {
+                const col = index % cols;
+                const row = Math.floor(index / cols);
+                const x = col * frameWidth;
+                const y = row * frameHeight;
+                this.ctx.drawImage(img, x, y);
+            });
+
+            // 保存数据用于下载
+            this.spritesheetData = {
+                folderName: folderName,
+                canvas: this.canvas
+            };
+
+            // 显示预览
+            this.canvas.classList.add('show');
+            this.previewPlaceholder.classList.add('hide');
+            
+            // 启用确定按钮
+            if (this.confirmBtn) {
+                this.confirmBtn.disabled = false;
+            }
+
+            // console.log('[ExportView] ✓ Spritesheet生成完成');
+        } catch (error) {
+            // console.error('[ExportView] 生成Spritesheet失败:', error);
+            this.previewPlaceholder.textContent = '生成失败';
+        }
+    }
+
+    downloadSpritesheet() {
+        if (!this.spritesheetData) {
+            // console.warn('[ExportView] 没有可下载的数据');
+            return;
+        }
+
+        // console.log('[ExportView] 开始下载Spritesheet');
+
+        this.canvas.toBlob((blob) => {
+            const url = URL.createObjectURL(blob);
+            const a = document.createElement('a');
+            a.href = url;
+            a.download = `${this.spritesheetData.folderName}_spritesheet.png`;
+            a.click();
+            URL.revokeObjectURL(url);
+
+            // console.log('[ExportView] ✓ 下载已触发');
+            
+            // 下载后关闭弹出框
+            this.close();
+        }, 'image/png');
+    }
+
+    close() {
+        // console.log('[ExportView] 关闭导出弹出框');
+        // 通知父窗口关闭
+        if (window.parent && window.parent !== window) {
+            window.parent.postMessage({
+                type: 'close-export-view'
+            }, '*');
+        }
+    }
+}
+
+// 初始化
+window.ExportView = new ExportView();
+

+ 861 - 0
client/js/export-view/export-view.js

@@ -0,0 +1,861 @@
+/**
+ * 导出动画弹出框
+ */
+class ExportView {
+    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.replaceBtn = null;
+        this.additionalPromptInput = null;
+        this.cancelBtn = null;
+        this.confirmBtn = null;
+        this.imageData = null;
+        this.referenceImageData = null;
+        this.spritesheetCanvas = null;
+        this.folderName = null;
+        this.spritesheetLayout = null; // 保存布局信息用于生成 JSON
+        this.replacedImageData = null; // 保存 Gemini 返回的替换后的图片(base64)
+        this.originalSpritesheetData = null; // 保存原始 spritesheet 的 base64 数据
+        
+        this.init();
+    }
+
+    init() {
+        this.overlay = document.getElementById('exportOverlay');
+        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.bindEvents();
+        
+        // 初始时禁用确定按钮
+        if (this.confirmBtn) {
+            this.confirmBtn.disabled = true;
+        }
+        
+        this.reset();
+    }
+
+    bindEvents() {
+        // 取消按钮(右上角)
+        this.cancelBtn?.addEventListener('click', () => {
+            this.close();
+        });
+
+        // 取消按钮(底部操作栏)
+        this.cancelBtnBottom?.addEventListener('click', () => {
+            this.close();
+        });
+
+        // 确定按钮
+        this.confirmBtn?.addEventListener('click', () => {
+            this.handleConfirm();
+        });
+
+        // 替换按钮
+        this.replaceBtn?.addEventListener('click', () => {
+            this.replaceCharacter();
+        });
+
+        // 删除参考图按钮
+        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-export-preview') {
+                // console.log('[ExportView] 收到显示预览消息:', event.data);
+                this.showPreview(event.data.imageUrl || event.data.imageData);
+            } else if (event.data && event.data.type === 'generate-export-preview') {
+                // console.log('[ExportView] 收到生成预览消息:', event.data);
+                this.reset();
+                this.folderName = event.data.folderName;
+                this.generatePreview(event.data.folderName);
+            }
+        });
+    }
+
+    /**
+     * 生成预览图
+     * @param {string} folderName - 文件夹名称
+     */
+    async generatePreview(folderName) {
+        if (!folderName) {
+            // console.warn('[ExportView] 没有提供文件夹名称');
+            if (this.previewPlaceholder) {
+                this.previewPlaceholder.textContent = '没有提供文件夹名称';
+            }
+            return;
+        }
+
+        // 重置状态(确保每次打开都是全新状态)
+        this.reset();
+
+        // 保存文件夹名称
+        this.folderName = folderName;
+
+        // 显示加载状态
+        if (this.previewPlaceholder) {
+            this.previewPlaceholder.classList.remove('hide');
+        }
+        if (this.previewImage) {
+            this.previewImage.classList.remove('show');
+        }
+
+        try {
+            const TEXTURE_ROOT = "http://localhost:3000/disk_data";
+            
+            // 获取帧列表
+            const encodedFolderName = encodeURIComponent(folderName);
+            const response = await fetch(`http://localhost:3000/api/frames/${encodedFolderName}`);
+            
+            if (!response.ok) {
+                // 服务端返回错误,解析错误信息
+                const errorData = await response.json().catch(() => ({}));
+                throw new Error(errorData.error || '无法获取帧列表');
+            }
+            
+            const data = await response.json();
+            const frameNumbers = data.frames || [];
+            const fileNames = data.fileNames || [];
+            
+            if (frameNumbers.length === 0) {
+                throw new Error('该文件夹中没有图片');
+            }
+            
+            // 加载所有图片
+            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('/');
+                
+                // 如果有文件名列表,使用实际文件名;否则使用帧号构造文件名
+                let imgSrc;
+                if (fileNames[i]) {
+                    // 使用实际文件名
+                    imgSrc = `${TEXTURE_ROOT}/${encodedPath}/${encodeURIComponent(fileNames[i])}`;
+                } else {
+                    // 回退到使用帧号构造文件名
+                    const frameName = frameNum.toString().padStart(2, '0');
+                    imgSrc = `${TEXTURE_ROOT}/${encodedPath}/${frameName}.png`;
+                }
+                
+                const img = await new Promise((resolve, reject) => {
+                    const image = new Image();
+                    image.crossOrigin = 'anonymous';
+                    image.onload = () => resolve(image);
+                    image.onerror = () => reject(new Error(`Failed to load image: ${imgSrc}`));
+                    image.src = imgSrc;
+                });
+                
+                images.push({
+                    img: img,
+                    width: img.width,
+                    height: img.height,
+                    frameNum: frameNum
+                });
+            }
+            
+            // 计算布局(简化版,使用简单的网格布局)
+            const frameWidth = images[0].width;
+            const frameHeight = images[0].height;
+            const cols = Math.ceil(Math.sqrt(images.length));
+            const rows = Math.ceil(images.length / cols);
+            
+            // 创建 Canvas 并绘制
+            const canvas = document.createElement('canvas');
+            canvas.width = frameWidth * cols;
+            canvas.height = frameHeight * rows;
+            const ctx = canvas.getContext('2d');
+            
+            // 保存 canvas 和布局信息用于下载
+            this.spritesheetCanvas = canvas;
+            
+            // 填充透明背景
+            ctx.clearRect(0, 0, canvas.width, canvas.height);
+            
+            // 保存布局信息(用于生成 JSON)
+            const layout = [];
+            
+            // 绘制所有图片
+            images.forEach((item, index) => {
+                const col = index % cols;
+                const row = Math.floor(index / cols);
+                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
+                });
+            });
+            
+            // 保存布局信息
+            this.spritesheetLayout = {
+                layout: layout,
+                sheetWidth: canvas.width,
+                sheetHeight: canvas.height
+            };
+            
+            // 转换为 base64
+            const imageUrl = await new Promise((resolve) => {
+                canvas.toBlob((blob) => {
+                    const url = URL.createObjectURL(blob);
+                    resolve(url);
+                }, 'image/png');
+            });
+            
+            // 保存原始 spritesheet 的 base64 数据
+            this.originalSpritesheetData = await new Promise((resolve) => {
+                canvas.toBlob((blob) => {
+                    const reader = new FileReader();
+                    reader.onload = () => resolve(reader.result);
+                    reader.readAsDataURL(blob);
+                }, 'image/png');
+            });
+            
+            // 显示预览图
+            this.showPreview(imageUrl);
+            
+            // 如果已经有参考图,显示替换按钮
+            if (this.referenceImageData && this.replaceBtn) {
+                this.replaceBtn.style.display = 'block';
+            }
+            
+            // 移除自动调用替换 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');
+            }
+        }
+    }
+
+    /**
+     * 计算宽高比
+     * @param {number} width - 宽度
+     * @param {number} height - 高度
+     * @returns {string} 宽高比字符串(例如:16:9)
+     */
+    calculateAspectRatio(width, height) {
+        // 计算最大公约数
+        const gcd = (a, b) => b === 0 ? a : gcd(b, a % b);
+        const divisor = gcd(width, height);
+        const ratioWidth = width / divisor;
+        const ratioHeight = height / divisor;
+        
+        // 如果比例太大,使用简化版本
+        if (ratioWidth > 100 || ratioHeight > 100) {
+            // 使用小数形式
+            const ratio = width / height;
+            return ratio.toFixed(2) + ':1';
+        }
+        
+        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) {
+            alert('请先上传参考图和生成 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 errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
+                throw new Error(errorData.error || `服务器错误: ${response.status}`);
+            }
+
+            const result = await response.json();
+            
+            if (result.success && result.imageData) {
+                // Gemini 返回的图片 base64
+                const geminiImageBase64 = result.imageData;
+                
+                // 调用抠图 API 处理图片
+                if (this.previewPlaceholder) {
+                    const loadingText = this.previewPlaceholder.querySelector('.loading-text');
+                    if (loadingText) {
+                        loadingText.textContent = '正在抠图处理...';
+                    }
+                }
+                
+                const mattingResponse = await fetch('http://localhost:3000/api/remove-background-base64', {
+                    method: 'POST',
+                    headers: {
+                        'Content-Type': 'application/json'
+                    },
+                    body: JSON.stringify({
+                        imageBase64: geminiImageBase64
+                    })
+                });
+
+                if (!mattingResponse.ok) {
+                    const errorData = await mattingResponse.json().catch(() => ({ error: 'Unknown error' }));
+                    throw new Error(errorData.error || `抠图失败: ${mattingResponse.status}`);
+                }
+
+                const mattingResult = await mattingResponse.json();
+                
+                if (mattingResult.success && mattingResult.imageData) {
+                    // 保存抠图后的图片
+                    this.replacedImageData = `data:image/png;base64,${mattingResult.imageData}`;
+                    
+                    // 显示抠图后的图片
+                    this.showPreview(this.replacedImageData);
+                } else {
+                    throw new Error('抠图处理失败');
+                }
+            } 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)
+     * @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;
+    }
+
+    /**
+     * 生成 JSON 数据
+     * @param {string} folderName - 文件夹名称
+     * @param {Array} layout - 布局信息数组
+     * @param {number} sheetWidth - Spritesheet 宽度
+     * @param {number} sheetHeight - Spritesheet 高度
+     * @returns {string} JSON 字符串
+     */
+    generateJSON(folderName, layout, sheetWidth, sheetHeight) {
+        const frames = {};
+        
+        layout.forEach((item, index) => {
+            // 使用实际的帧号,确保与原始文件名一致
+            const frameNum = item.frameNum ? item.frameNum.toString().padStart(2, '0') : (index + 1).toString().padStart(2, '0');
+            const frameName = `${frameNum}.png`;
+            const x = item.x;
+            const y = item.y;
+            const width = item.width;
+            const height = item.height;
+            
+            // 标准的 TexturePacker JSON 格式,Cocos Creator 3.8 完全兼容
+            frames[frameName] = {
+                frame: { 
+                    x: x, 
+                    y: y,
+                    w: width, 
+                    h: height 
+                },
+                rotated: false,
+                trimmed: false,
+                spriteSourceSize: { x: 0, y: 0, w: width, h: height },
+                sourceSize: { w: width, h: height }
+            };
+        });
+
+        // Cocos Creator 3.8 兼容的 TexturePacker JSON 格式
+        const json = {
+            frames: frames,
+            meta: {
+                app: "https://www.codeandweb.com/texturepacker",
+                version: "1.0",
+                image: `${folderName}.png`,
+                format: "RGBA8888",
+                size: { w: sheetWidth, h: sheetHeight },
+                scale: 1
+            }
+        };
+
+        return JSON.stringify(json, null, 2);
+    }
+
+    /**
+     * 将 Blob 转换为 Base64
+     * @param {Blob} blob - Blob 对象
+     * @returns {Promise<string>} Base64 字符串
+     */
+    blobToBase64(blob) {
+        return new Promise((resolve, reject) => {
+            const reader = new FileReader();
+            reader.onloadend = () => {
+                // 移除 data:image/png;base64, 前缀
+                const base64 = reader.result.split(',')[1];
+                resolve(base64);
+            };
+            reader.onerror = reject;
+            reader.readAsDataURL(blob);
+        });
+    }
+
+    /**
+     * 下载文件
+     * @param {Blob} data - 文件数据
+     * @param {string} filename - 文件名
+     * @param {string} mimeType - MIME 类型
+     */
+    downloadFile(data, filename, mimeType) {
+        const blob = new Blob([data], { type: mimeType });
+        const url = URL.createObjectURL(blob);
+        const a = document.createElement('a');
+        a.href = url;
+        a.download = filename;
+        document.body.appendChild(a);
+        a.click();
+        document.body.removeChild(a);
+        URL.revokeObjectURL(url);
+    }
+
+    /**
+     * 处理下载按钮点击
+     */
+    async handleConfirm() {
+        // console.log('[ExportView] 用户点击下载按钮');
+        
+        if (!this.spritesheetCanvas || !this.spritesheetLayout) {
+            // console.warn('[ExportView] 没有可下载的 Spritesheet');
+            return;
+        }
+        
+        try {
+            // 生成 JSON 数据
+            const folderName = this.folderName.split('/').pop() || 'spritesheet';
+            const jsonData = this.generateJSON(
+                folderName,
+                this.spritesheetLayout.layout,
+                this.spritesheetLayout.sheetWidth,
+                this.spritesheetLayout.sheetHeight
+            );
+            
+            // 确定使用哪个图片:如果有替换后的图片,使用替换后的;否则使用原始的
+            let imageBase64;
+            if (this.replacedImageData) {
+                // 使用替换后的图片(移除 data:image/png;base64, 前缀)
+                imageBase64 = this.replacedImageData.replace(/^data:image\/\w+;base64,/, '');
+            } else {
+                // 使用原始 spritesheet
+                const imageBlob = await new Promise((resolve, reject) => {
+                    this.spritesheetCanvas.toBlob((blob) => {
+                        if (blob) {
+                            resolve(blob);
+                        } else {
+                            reject(new Error('Canvas 转换失败'));
+                        }
+                    }, 'image/png');
+                });
+                
+                // 将图片转换为 Base64
+                imageBase64 = await this.blobToBase64(imageBlob);
+            }
+            
+            // 发送到服务器打包
+            const response = await fetch('http://localhost:3000/api/pack', {
+                method: 'POST',
+                headers: {
+                    'Content-Type': 'application/json'
+                },
+                body: JSON.stringify({
+                    folderName: folderName,
+                    imageData: imageBase64,
+                    jsonData: jsonData
+                })
+            });
+
+            if (!response.ok) {
+                const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
+                throw new Error(errorData.error || `服务器错误: ${response.status}`);
+            }
+
+            // 获取 ZIP 文件的 Blob
+            const zipBlob = await response.blob();
+            
+            // 下载 ZIP 文件
+            this.downloadFile(zipBlob, `${folderName}.zip`, 'application/zip');
+            
+            // 关闭弹出框
+            this.close();
+        } catch (error) {
+            // console.error('[ExportView] 下载失败:', error);
+            alert(`下载失败: ${error.message}`);
+        }
+    }
+
+    /**
+     * 关闭弹出框
+     */
+    close() {
+        // console.log('[ExportView] 关闭导出弹出框');
+        
+        // 清空所有数据
+        this.reset();
+        
+        // 通知父窗口关闭弹出框
+        if (window.parent && window.parent !== window) {
+            window.parent.postMessage({
+                type: 'close-export-view'
+            }, '*');
+        }
+    }
+
+    /**
+     * 重置所有数据和UI状态
+     */
+    reset() {
+        // 清空数据属性
+        this.imageData = null;
+        this.referenceImageData = null;
+        this.spritesheetCanvas = null;
+        this.folderName = null;
+        this.spritesheetLayout = null;
+        this.replacedImageData = 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;
+        }
+
+        // 重置预览图区域
+        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 = '';
+        }
+
+        // 重置按钮状态
+        if (this.confirmBtn) {
+            this.confirmBtn.disabled = true;
+        }
+    }
+}
+
+// 初始化
+window.ExportView = new ExportView();
+

+ 54 - 0
client/js/navigation.js

@@ -0,0 +1,54 @@
+// 导航栏交互逻辑
+
+(function () {
+  // 导航切换功能
+  function initNavigation() {
+    const navButtons = document.querySelectorAll('.nav-btn');
+    
+    navButtons.forEach(button => {
+      button.addEventListener('click', function() {
+        const page = this.getAttribute('data-page');
+        
+        // 更新按钮状态
+        navButtons.forEach(btn => btn.classList.remove('active'));
+        this.classList.add('active');
+        
+        // 通知父窗口切换页面
+        if (window.parent !== window) {
+          window.parent.postMessage({
+            type: 'navigation',
+            page: page
+          }, '*');
+        }
+      });
+    });
+  }
+
+  // 接收来自父窗口的消息,同步导航状态
+  window.addEventListener('message', function(event) {
+    // 安全检查:确保消息来自同源
+    if (event.origin !== window.location.origin) {
+      return;
+    }
+    
+    const data = event.data;
+    
+    // 处理页面切换消息
+    if (data.type === 'navigation' && data.page) {
+      const navButtons = document.querySelectorAll('.nav-btn');
+      navButtons.forEach(btn => {
+        if (btn.getAttribute('data-page') === data.page) {
+          btn.classList.add('active');
+        } else {
+          btn.classList.remove('active');
+        }
+      });
+    }
+  });
+
+  // 页面加载完成后初始化
+  window.addEventListener('DOMContentLoaded', function() {
+    initNavigation();
+  });
+})();
+

+ 710 - 0
client/js/seq_ani_player/card.js

@@ -0,0 +1,710 @@
+(function () {
+  // 主预览卡片类
+  class PreviewCard {
+    constructor(container, options = {}) {
+      this.container = container;
+      this.options = {
+        fps: 8,
+        onFpsChange: null,
+        ...options
+      };
+      
+      this.previewImage = null;
+      this.loadingOverlay = null;
+      this.dropHint = null;
+      this.imageError = null;
+      this.fpsSlider = null;
+      this.fpsValue = null;
+      this.dropZone = null;
+      
+      this.currentFps = this.options.fps;
+      this.frameList = [];
+      this.currentFrameIndex = 0;
+      this.animationTimer = null;
+      this.frameSourceMode = "remote";
+      this.localFrameResources = [];
+      this.currentFolderName = '';
+      
+      this.init();
+    }
+    
+    async init() {
+      await this.loadTemplate();
+      this.bindElements();
+      this.bindEvents();
+      this.setStagePlaceholderVisible(true);
+    }
+    
+    async loadTemplate() {
+      const response = await fetch('./card.html');
+      const html = await response.text();
+      const wrapper = document.createElement('div');
+      wrapper.innerHTML = html.trim();
+      const template = wrapper.querySelector('#preview-card-template');
+      if (!template) {
+        throw new Error('Preview card template not found');
+      }
+      const content = template.content.cloneNode(true);
+      this.container.appendChild(content);
+    }
+    
+    bindElements() {
+      this.previewImage = this.container.querySelector('.preview-image');
+      this.loadingOverlay = this.container.querySelector('.loading-overlay');
+      this.loadingText = this.container.querySelector('.loading-text');
+      this.dropHint = this.container.querySelector('.drop-hint');
+      this.imageError = this.container.querySelector('.image-error');
+      this.fpsSlider = this.container.querySelector('.fps-slider');
+      this.fpsValue = this.container.querySelector('.fps-value');
+      this.dropZone = this.container.querySelector('.preview-card-stage');
+      this.infoBar = this.container.querySelector('.preview-info-bar');
+      this.folderNameElement = this.container.querySelector('.folder-name');
+      this.btnExport = this.container.querySelector('.btn-export');
+    }
+    
+    bindEvents() {
+      // FPS控制
+      if (this.fpsSlider) {
+        this.fpsSlider.addEventListener('input', () => {
+          const value = parseInt(this.fpsSlider.value, 10) || this.currentFps;
+          this.setFps(value);
+          if (this.fpsValue) {
+            this.fpsValue.textContent = `${value} FPS`;
+          }
+          if (typeof this.options.onFpsChange === 'function') {
+            this.options.onFpsChange(value);
+          }
+        });
+      }
+      
+      // 拖放事件
+      if (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));
+      }
+      
+      // 导出按钮
+      if (this.btnExport) {
+        this.btnExport.addEventListener('click', () => this.handleExport());
+      }
+    }
+    
+    handleDragEnter(event) {
+      event.preventDefault();
+      event.stopPropagation();
+      this.dropZone.classList.add('is-dragging');
+    }
+    
+    handleDragOver(event) {
+      event.preventDefault();
+      event.stopPropagation();
+    }
+    
+    handleDragLeave(event) {
+      event.preventDefault();
+      if (!this.dropZone.contains(event.relatedTarget)) {
+        this.dropZone.classList.remove('is-dragging');
+      }
+    }
+    
+    async handleDrop(event) {
+      event.preventDefault();
+      event.stopPropagation();
+      this.dropZone.classList.remove('is-dragging');
+      
+      // 获取拖入的文件夹名称
+      const transfer = event.dataTransfer;
+      if (!transfer || !transfer.items || transfer.items.length === 0) {
+        // console.warn('[PreviewCard] No data transfer items');
+        return;
+      }
+      
+      // 获取文本数据(可能是JSON对象或文件夹名称)
+      const item = transfer.items[0];
+      if (item.kind === 'string' && item.type === 'text/plain') {
+        item.getAsString(async (dataString) => {
+          // console.log('[PreviewCard] Dropped folder:', dataString);
+          
+          // 尝试解析为JSON对象
+          let folderName = dataString;
+          let fileType = 'directory'; // 默认假设是文件夹
+          let pngCount = undefined; // PNG文件数量(如果有)
+          
+          try {
+            const data = JSON.parse(dataString);
+            // 如果是对象,提取路径、名称和类型
+            if (data && typeof data === 'object') {
+              folderName = data.path || data.name || dataString;
+              fileType = data.type || 'directory';
+              pngCount = data.pngCount; // 从拖拽数据中获取PNG数量
+            }
+          } catch (e) {
+            // 如果不是JSON,直接使用原始字符串
+            folderName = dataString;
+          }
+          
+          // 验证:必须是文件夹
+          if (fileType !== 'directory') {
+            this.showError('❌ 请拖入文件夹,不支持单个文件');
+            return;
+          }
+          
+          // 验证:文件夹中是否包含PNG图片
+          const isValid = await this.validateFolderHasPNG(folderName, pngCount);
+          if (!isValid) {
+            this.showError('❌ 该文件夹不包含PNG图片');
+            return;
+          }
+          
+          // console.log('[PreviewCard] Resolved folder name:', folderName);
+          await this.loadAndCacheFolderAnimation(folderName);
+        });
+      }
+    }
+    
+    async validateFolderHasPNG(folderName, pngCount) {
+      try {
+        // 1. 优先使用传递的pngCount(来自拖拽数据)
+        if (pngCount !== undefined) {
+          // console.log('[PreviewCard] 使用缓存的pngCount:', pngCount);
+          return pngCount > 0;
+        }
+        
+        // 2. 尝试从DiskManager的缓存中获取
+        if (window.diskManager && window.diskManager.getFileFromCache) {
+          const cachedFile = window.diskManager.getFileFromCache(folderName);
+          if (cachedFile && cachedFile.pngCount !== undefined) {
+            // console.log('[PreviewCard] 从DiskManager缓存获取pngCount:', cachedFile.pngCount);
+            return cachedFile.pngCount > 0;
+          }
+        }
+        
+        // 3. 最后才请求服务器(使用新的disk API)
+        // console.log('[PreviewCard] 请求服务器验证文件夹:', folderName);
+        const response = await fetch(`/api/disk/list?path=${encodeURIComponent(folderName)}`);
+        if (!response.ok) {
+          return false;
+        }
+        
+        const data = await response.json();
+        
+        // 检查是否是文件夹并且有PNG文件
+        if (data.success && data.files) {
+          // 检查当前路径对应的文件夹信息
+          // 注意:list API返回的是文件夹内的文件列表,不是文件夹本身
+          // 所以我们需要统计PNG文件数量
+          const pngFiles = data.files.filter(f => 
+            f.type === 'file' && f.name.toLowerCase().endsWith('.png')
+          );
+          return pngFiles.length > 0;
+        }
+        
+        return false;
+        } catch (error) {
+          // console.error('[PreviewCard] 验证文件夹失败:', error);
+          return false;
+        }
+    }
+    
+    async loadAndCacheFolderAnimation(folderName) {
+      try {
+        // 显示加载动画
+        this.showLoading(true);
+        this.hideError();
+        this.setStagePlaceholderVisible(false);
+        
+        // console.log('[PreviewCard] 开始加载文件夹:', folderName);
+        
+        // 1. 从网盘系统获取该文件夹的文件列表
+        const listResponse = await fetch(`/api/disk/list?path=${encodeURIComponent(folderName)}`);
+        if (!listResponse.ok) {
+          throw new Error('获取文件列表失败');
+        }
+        
+        const listData = await listResponse.json();
+        // console.log('[PreviewCard] 文件列表响应:', listData);
+        
+        if (!listData.success || !listData.files) {
+          throw new Error('获取文件列表失败');
+        }
+        
+        // 2. 筛选出PNG文件并按文件名排序
+        const pngFiles = listData.files
+          .filter(f => f.type === 'file' && f.name.toLowerCase().endsWith('.png'))
+          .sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true }));
+        
+        // console.log('[PreviewCard] PNG文件列表:', pngFiles.map(f => f.name));
+        
+        if (!pngFiles.length) {
+          throw new Error('文件夹中没有可用的PNG图片');
+        }
+        
+        // console.log('[PreviewCard] 找到 PNG 文件数量:', pngFiles.length);
+        
+        // 3. 打开或创建缓存
+        const cache = await caches.open('animation-frames-v1');
+        
+        // 4. 构造帧URL列表(使用文件的实际名称)
+        const frameUrls = pngFiles.map(file => {
+          // 使用文件的完整路径(file.path)来构造URL
+          return `/api/disk/preview?path=${encodeURIComponent(file.path)}`;
+        });
+        
+        // console.log('[PreviewCard] 开始下载和缓存图片...');
+        
+        // 5. 逐个下载并缓存
+        const cachedFrames = [];
+        for (let i = 0; i < frameUrls.length; i++) {
+          const url = frameUrls[i];
+          const fileName = pngFiles[i].name;
+          
+          // 更新进度
+          this.showLoading(true, `正在缓存图片... (${i + 1}/${frameUrls.length})`);
+          
+          // 检查缓存中是否已存在
+          const cachedResponse = await cache.match(url);
+          if (cachedResponse) {
+            // console.log(`[PreviewCard] [${i + 1}/${frameUrls.length}] 从缓存加载: ${fileName}`);
+            cachedFrames.push({ url, index: i, name: fileName });
+          } else {
+            // 下载并缓存
+            try {
+              // console.log(`[PreviewCard] [${i + 1}/${frameUrls.length}] 下载: ${fileName}`);
+              const response = await fetch(url);
+              if (response.ok) {
+                await cache.put(url, response.clone());
+                // console.log(`[PreviewCard] ✓ 已缓存: ${fileName}`);
+                cachedFrames.push({ url, index: i, name: fileName });
+              } else {
+                // console.warn(`[PreviewCard] ✗ 下载失败 (${response.status}): ${fileName}`);
+              }
+            } catch (error) {
+              // console.error(`[PreviewCard] ✗ 下载错误: ${fileName}`, error);
+            }
+          }
+        }
+        
+        if (cachedFrames.length === 0) {
+          throw new Error('没有成功缓存任何图片');
+        }
+        
+        // console.log('[PreviewCard] ✅ 缓存完成,共', cachedFrames.length, '帧');
+        
+        // 6. 设置文件夹名称
+        this.setFolderName(folderName);
+        
+        // 7. 加载并播放动画
+        this.loadFrames(cachedFrames, 'cached');
+        this.showLoading(false);
+        
+        // 添加播放状态类
+        if (this.dropZone) {
+          this.dropZone.classList.add('is-playing');
+        }
+        
+      } catch (error) {
+        // console.error('[PreviewCard] 加载失败:', error);
+        this.showLoading(false);
+        this.showError(error.message || '加载失败');
+        this.setStagePlaceholderVisible(true);
+        this.setFolderName(''); // 清除文件夹名称
+        // 移除播放状态
+        if (this.dropZone) {
+          this.dropZone.classList.remove('is-playing');
+        }
+      }
+    }
+    
+    sanitizeFrameList(frameInfo) {
+      if (frameInfo && Array.isArray(frameInfo.frames) && frameInfo.frames.length > 0) {
+        return frameInfo.frames;
+      }
+      const maxFrame = frameInfo && frameInfo.maxFrame ? frameInfo.maxFrame : 0;
+      if (!maxFrame) {
+        return [];
+      }
+      return Array.from({ length: maxFrame }, (_, idx) => idx + 1);
+    }
+    
+    showLoading(isLoading, text = '正在加载图片...') {
+      if (!this.loadingOverlay) return;
+      if (isLoading) {
+        this.loadingOverlay.classList.add('is-visible');
+        this.loadingOverlay.setAttribute('aria-hidden', 'false');
+        if (this.loadingText) {
+          this.loadingText.textContent = text;
+        }
+      } else {
+        this.loadingOverlay.classList.remove('is-visible');
+        this.loadingOverlay.setAttribute('aria-hidden', 'true');
+      }
+    }
+    
+    showError(message) {
+      // 使用全局alert显示错误
+      this.showGlobalAlert(message);
+    }
+    
+    hideError() {
+      if (!this.imageError) return;
+      this.imageError.hidden = true;
+    }
+    
+    showGlobalAlert(text, duration = 1500) {
+      // 通过postMessage通知父页面显示alert
+      // 需要发送两层(iframe嵌套:disk -> assets -> index)
+      let targetWindow = window.parent;
+      while (targetWindow && targetWindow !== window) {
+        targetWindow.postMessage({
+          type: 'global-alert',
+          text: text,
+          duration: duration
+        }, '*');
+        
+        // 尝试向更上层发送
+        if (targetWindow.parent && targetWindow.parent !== targetWindow) {
+          targetWindow = targetWindow.parent;
+        } else {
+          break;
+        }
+      }
+    }
+    
+    setStagePlaceholderVisible(isVisible) {
+      if (this.dropHint) {
+        this.dropHint.hidden = !isVisible;
+      }
+      if (this.previewImage) {
+        this.previewImage.classList.toggle('is-hidden', isVisible);
+        if (isVisible) {
+          this.previewImage.removeAttribute('src');
+        }
+      }
+    }
+    
+    setFolderName(name) {
+      this.currentFolderName = name;
+      if (this.folderNameElement) {
+        this.folderNameElement.textContent = name || '--';
+      }
+      if (this.infoBar) {
+        this.infoBar.hidden = !name;
+      }
+    }
+    
+    async handleExport() {
+      if (!this.currentFolderName || !this.frameList.length) {
+        this.showGlobalAlert('没有可导出的动画');
+        return;
+      }
+      
+      // 直接打开导出弹出框,不先生成预览图
+      // 预览图将在弹出框中生成
+      this.openExportView();
+    }
+    
+    /**
+     * 生成预览图
+     * @returns {Promise<string>} 预览图的 base64 URL
+     */
+    async generatePreviewImage() {
+      const TEXTURE_ROOT = "http://localhost:3000/disk_data";
+      const folderName = this.currentFolderName;
+      
+      // 获取帧列表(从服务端获取,服务端会判断是否有图片)
+      const encodedFolderName = encodeURIComponent(folderName);
+      const response = await fetch(`http://localhost:3000/api/frames/${encodedFolderName}`);
+      
+      if (!response.ok) {
+        // 服务端返回错误,解析错误信息
+        const errorData = await response.json().catch(() => ({}));
+        throw new Error(errorData.error || '无法获取帧列表');
+      }
+      
+      const data = await response.json();
+      const frameNumbers = data.frames || [];
+      
+      // 服务端已经判断过是否有图片,如果返回200但frames为空,说明有问题
+      if (frameNumbers.length === 0) {
+        throw new Error('该文件夹中没有图片');
+      }
+      
+      // 加载所有图片
+      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`;
+        
+        const img = await new Promise((resolve, reject) => {
+          const image = new Image();
+          image.crossOrigin = 'anonymous';
+          image.onload = () => resolve(image);
+          image.onerror = () => reject(new Error(`Failed to load image: ${imgSrc}`));
+          image.src = imgSrc;
+        });
+        
+        images.push({
+          img: img,
+          width: img.width,
+          height: img.height,
+          frameNum: frameNum
+        });
+      }
+      
+      // 计算布局(简化版,使用简单的网格布局)
+      const frameWidth = images[0].width;
+      const frameHeight = images[0].height;
+      const cols = Math.ceil(Math.sqrt(images.length));
+      const rows = Math.ceil(images.length / cols);
+      
+      // 创建 Canvas 并绘制
+      const canvas = document.createElement('canvas');
+      canvas.width = frameWidth * cols;
+      canvas.height = frameHeight * rows;
+      const ctx = canvas.getContext('2d');
+      
+      // 填充透明背景
+      ctx.clearRect(0, 0, canvas.width, canvas.height);
+      
+      // 绘制所有图片
+      images.forEach((item, index) => {
+        const col = index % cols;
+        const row = Math.floor(index / cols);
+        const x = col * frameWidth;
+        const y = row * frameHeight;
+        ctx.drawImage(item.img, x, y);
+      });
+      
+      // 转换为 base64
+      return new Promise((resolve) => {
+        canvas.toBlob((blob) => {
+          const url = URL.createObjectURL(blob);
+          resolve(url);
+        }, 'image/png');
+      });
+    }
+    
+    /**
+     * 打开导出弹出框
+     */
+    openExportView() {
+      // 通过postMessage通知父页面打开导出弹出框
+      // 传递文件夹名称,让弹出框自己生成预览图
+      let targetWindow = window.parent;
+      while (targetWindow && targetWindow !== window) {
+        targetWindow.postMessage({
+          type: 'open-export-view',
+          folderName: this.currentFolderName
+        }, '*');
+        
+        // 尝试向更上层发送
+        if (targetWindow.parent && targetWindow.parent !== targetWindow) {
+          targetWindow = targetWindow.parent;
+        } else {
+          break;
+        }
+      }
+    }
+    
+    setFps(fps) {
+      this.currentFps = fps;
+      if (this.frameList.length > 0) {
+        this.startAnimation();
+      }
+    }
+    
+    startAnimation() {
+      this.stopAnimation();
+      if (!this.frameList.length) return;
+      
+      const interval = 1000 / this.currentFps;
+      this.animationTimer = setInterval(() => {
+        if (!this.frameList.length) {
+          this.stopAnimation();
+          return;
+        }
+        this.currentFrameIndex = (this.currentFrameIndex + 1) % this.frameList.length;
+        this.updateFrame(this.frameList[this.currentFrameIndex]);
+      }, interval);
+    }
+    
+    stopAnimation() {
+      if (this.animationTimer) {
+        clearInterval(this.animationTimer);
+        this.animationTimer = null;
+      }
+    }
+    
+    updateFrame(frameData) {
+      if (!this.previewImage) return;
+      
+      if ((this.frameSourceMode === 'local' || this.frameSourceMode === 'cached') && frameData && frameData.url) {
+        this.setStagePlaceholderVisible(false);
+        if (this.previewImage.src !== frameData.url) {
+          this.previewImage.src = frameData.url;
+        }
+      }
+    }
+    
+    loadFrames(frames, mode = 'local') {
+      this.frameSourceMode = mode;
+      this.frameList = frames;
+      this.currentFrameIndex = 0;
+      
+      // console.log('[PreviewCard] 加载帧列表, 模式:', mode, '数量:', frames.length);
+      
+      if (frames.length > 0) {
+        this.updateFrame(frames[0]);
+        this.startAnimation();
+      }
+    }
+    
+    destroy() {
+      this.stopAnimation();
+      if (this.container) {
+        this.container.innerHTML = '';
+      }
+    }
+  }
+  
+  // 小卡片类
+  class SequenceCard {
+    constructor(cardElement, folderName, index, buildFrameSrc, onSelect) {
+      this.cardElement = cardElement;
+      this.folderName = folderName;
+      this.index = index;
+      this.buildFrameSrc = buildFrameSrc;
+      this.onSelect = onSelect;
+
+      this.imageElement = cardElement.querySelector(".card-image");
+      this.spinnerElement = cardElement.querySelector(".loading-spinner");
+      this.errorElement = cardElement.querySelector(".image-error");
+      this.labelElement = cardElement.querySelector(".card-label");
+      this.downloadButton = cardElement.querySelector(".card-download-btn");
+
+      this.handleCardClick = this.handleCardClick.bind(this);
+      this.handleDownloadClick = this.handleDownloadClick.bind(this);
+
+      this.init();
+    }
+
+    init() {
+      this.cardElement.dataset.folder = this.folderName || "";
+      this.cardElement.dataset.index = this.index;
+      this.cardElement.dataset.valid = "true";
+
+      if (this.labelElement) {
+        this.labelElement.textContent = this.formatLabel(this.folderName);
+      }
+
+      this.cardElement.addEventListener("click", this.handleCardClick);
+
+      if (this.downloadButton) {
+        this.boundDownloadHandler = (event) => {
+          event.stopPropagation();
+          event.preventDefault();
+          this.handleDownloadClick();
+        };
+        this.downloadButton.addEventListener("click", this.boundDownloadHandler);
+      }
+
+      this.bindImageEvents();
+    }
+
+    bindImageEvents() {
+      if (!this.imageElement) {
+        return;
+      }
+
+      this.imageElement.addEventListener("load", () => {
+        this.toggleSpinner(false);
+        this.hideError();
+      });
+
+      this.imageElement.addEventListener("error", () => {
+        this.toggleSpinner(false);
+        this.showError("图片加载失败");
+      });
+    }
+
+    formatLabel(name) {
+      if (!name) {
+        return "--";
+      }
+      return name.replace(/_/g, " ").toUpperCase();
+    }
+
+    loadPreview() {
+      if (!this.imageElement || !this.buildFrameSrc || !this.folderName) {
+        return;
+      }
+      this.toggleSpinner(true);
+      const previewSrc = this.buildFrameSrc(this.folderName, 1);
+      if (this.imageElement.src !== previewSrc) {
+        this.imageElement.src = previewSrc;
+      }
+    }
+
+    setActive(isActive) {
+      if (isActive) {
+        this.cardElement.classList.add("is-active");
+      } else {
+        this.cardElement.classList.remove("is-active");
+      }
+    }
+
+    handleCardClick() {
+      if (typeof this.onSelect === "function") {
+        this.onSelect(this.folderName, this.index);
+      }
+    }
+
+    handleDownloadClick() {
+      if (!this.folderName) {
+        return;
+      }
+      if (window.SpriteSheetMaker && typeof window.SpriteSheetMaker.handleDownloadClick === "function") {
+        window.SpriteSheetMaker.handleDownloadClick(this.folderName, this.index);
+      }
+    }
+
+    toggleSpinner(visible) {
+      if (!this.spinnerElement) {
+        return;
+      }
+      this.spinnerElement.style.display = visible ? "block" : "none";
+    }
+
+    showError(message) {
+      if (!this.errorElement) {
+        return;
+      }
+      this.errorElement.textContent = message;
+      this.errorElement.hidden = false;
+    }
+
+    hideError() {
+      if (!this.errorElement) {
+        return;
+      }
+      this.errorElement.hidden = true;
+    }
+
+    destroy() {
+      this.cardElement.removeEventListener("click", this.handleCardClick);
+      if (this.downloadButton && this.boundDownloadHandler) {
+        this.downloadButton.removeEventListener("click", this.boundDownloadHandler);
+      }
+    }
+  }
+
+  window.PreviewCard = PreviewCard;
+  window.SequenceCard = SequenceCard;
+})();
+
+

+ 878 - 0
client/js/seq_ani_player/seq-ani-player.js

@@ -0,0 +1,878 @@
+// PNG 序列播放工具 - 主逻辑
+// 负责动画播放、帧管理等功能
+
+(function () {
+  // 调试日志开关
+  const DEBUG = true;
+  function log(...args) {
+    if (DEBUG) console.log("[SeqAniPlayer]", ...args);
+  }
+  function logError(...args) {
+    console.error("[SeqAniPlayer ERROR]", ...args);
+  }
+  function logWarn(...args) {
+    console.warn("[SeqAniPlayer WARN]", ...args);
+  }
+
+  const TEXTURE_ROOT = "http://localhost:3000/disk_data";
+  const foldersApi = "http://localhost:3000/api/folders";
+
+  const fpsSlider = document.getElementById("fpsSlider");
+  const fpsValue = document.getElementById("fpsValue");
+  const cardsGrid = document.getElementById("cardsGrid");
+  const cardCountLabel = document.getElementById("cardCount");
+  const folderNameLabel = document.getElementById("folderName");
+  const folderCounterLabel = document.getElementById("folderCounter");
+  const frameCounterLabel = document.getElementById("frameCounter");
+  const downloadBtn = document.getElementById("downloadBtn");
+  const prevBtn = document.getElementById("prevFolderBtn");
+  const nextBtn = document.getElementById("nextFolderBtn");
+  const playerImage = document.getElementById("playerImage");
+  const playerLoadingOverlay = document.getElementById("playerLoadingOverlay");
+  const playerError = document.getElementById("playerError");
+  const playerShell = document.querySelector(".player-image-shell");
+  const dropHint = document.getElementById("dropHint");
+
+  let availableFolders = [];
+  let cards = [];
+  let cardTemplate = null;
+  let cardTemplatePromise = null;
+
+  let currentIndex = -1;
+  let currentFps = 8;
+  let currentFolder = "";
+  let frameList = [];
+  let currentFrameCursor = 0;
+  let stageTimer = null;
+  let frameSourceMode = "remote";
+  let localFrameResources = [];
+
+  function padFrame(index) {
+    return index.toString().padStart(2, "0");
+  }
+
+  function buildFrameSrc(folder, index) {
+    const frameName = padFrame(index);
+    return `${TEXTURE_ROOT}/${folder}/${frameName}.png`;
+  }
+
+  function buildFolderName(prefix, index, padding) {
+    const num = index.toString().padStart(padding, "0");
+    return `${prefix}${num}`;
+  }
+
+  async function ensureCardTemplate() {
+    if (cardTemplate) {
+      return cardTemplate;
+    }
+    if (cardTemplatePromise) {
+      return cardTemplatePromise;
+    }
+    cardTemplatePromise = fetch("./card.html")
+      .then((response) => {
+        if (!response.ok) {
+          throw new Error("Failed to load card template");
+        }
+        return response.text();
+      })
+      .then((html) => {
+        const wrapper = document.createElement("div");
+        wrapper.innerHTML = html.trim();
+        const template = wrapper.querySelector("#card-template");
+        if (!template) {
+          throw new Error("Card template missing");
+        }
+        cardTemplate = template;
+        return cardTemplate;
+      })
+      .catch((error) => {
+        console.error(error);
+        cardTemplatePromise = null;
+        throw error;
+      });
+    return cardTemplatePromise;
+  }
+
+  function destroyCards() {
+    cards.forEach((card) => card.destroy && card.destroy());
+    cards = [];
+    if (cardsGrid) {
+      cardsGrid.innerHTML = "";
+    }
+  }
+
+  function updateCardCount() {
+    if (!cardCountLabel) return;
+    cardCountLabel.textContent = `${availableFolders.length} 个动画`;
+  }
+
+  function updateNavDisabledState() {
+    const disabled = availableFolders.length <= 1;
+    if (prevBtn) prevBtn.disabled = disabled;
+    if (nextBtn) nextBtn.disabled = disabled;
+  }
+
+  function updateMeta() {
+    const total = availableFolders.length;
+    const safeIndex = currentIndex >= 0 ? currentIndex : 0;
+    const folderName = availableFolders[safeIndex] || "--";
+    if (folderNameLabel) {
+      folderNameLabel.textContent = folderName;
+    }
+    if (folderCounterLabel) {
+      folderCounterLabel.textContent = total ? `${safeIndex + 1} / ${total}` : "0 / 0";
+    }
+  }
+
+  function showLoading(isLoading) {
+    if (!playerLoadingOverlay) return;
+    playerLoadingOverlay.classList.toggle("is-visible", isLoading);
+  }
+
+  function showPlayerError(message) {
+    if (!playerError) return;
+    playerError.textContent = message;
+    playerError.hidden = false;
+  }
+
+  function hidePlayerError() {
+    if (!playerError) return;
+    playerError.hidden = true;
+  }
+
+  function stopStageAnimation() {
+    if (stageTimer) {
+      clearInterval(stageTimer);
+      stageTimer = null;
+    }
+  }
+
+  function setStagePlaceholderVisible(isVisible) {
+    if (!playerImage) {
+      return;
+    }
+    playerImage.classList.toggle("is-hidden", Boolean(isVisible));
+    if (isVisible) {
+      playerImage.removeAttribute("src");
+    }
+  }
+
+  function cleanupLocalFrames() {
+    if (!localFrameResources.length) {
+      return;
+    }
+    localFrameResources.forEach((frame) => {
+      if (frame && frame.url) {
+        URL.revokeObjectURL(frame.url);
+      }
+    });
+    localFrameResources = [];
+  }
+
+  function switchToRemoteSource() {
+    if (frameSourceMode !== "remote") {
+      cleanupLocalFrames();
+      frameSourceMode = "remote";
+    }
+  }
+
+  function startStageLoop() {
+    log("🎬 启动动画循环");
+    stopStageAnimation();
+    if (!frameList.length) {
+      logWarn("帧列表为空,无法启动循环");
+      return;
+    }
+    const interval = 1000 / currentFps;
+    log(`⏱️ 动画循环启动,帧数: ${frameList.length}, FPS: ${currentFps}, 间隔: ${interval}ms`);
+    stageTimer = setInterval(() => {
+      if (!frameList.length) {
+        stopStageAnimation();
+        return;
+      }
+      currentFrameCursor = (currentFrameCursor + 1) % frameList.length;
+      updateStageImage(frameList[currentFrameCursor]);
+    }, interval);
+  }
+
+  function updateStageImage(frameData) {
+    if (!playerImage) {
+      logWarn("playerImage 元素不存在");
+      return;
+    }
+
+    if (frameSourceMode === "local") {
+      const frame = typeof frameData === "object" ? frameData : frameList[frameData];
+      if (!frame || !frame.url) {
+        logWarn("本地模式:帧数据无效");
+        return;
+      }
+      setStagePlaceholderVisible(false);
+      if (playerImage.src !== frame.url) {
+        log(`🖼️ 更新图片: ${frame.name}`);
+        playerImage.src = frame.url;
+      }
+      return;
+    }
+
+    if (!currentFolder) {
+      logWarn("远程模式:文件夹未设置");
+      return;
+    }
+    const frameNumber = typeof frameData === "number" ? frameData : parseInt(frameData, 10);
+    if (Number.isNaN(frameNumber)) {
+      logWarn("远程模式:帧编号无效");
+      return;
+    }
+    const nextSrc = buildFrameSrc(currentFolder, frameNumber);
+    setStagePlaceholderVisible(false);
+    if (playerImage.src !== nextSrc) {
+      playerImage.src = nextSrc;
+    }
+  }
+
+  function setFps(fps) {
+    currentFps = fps;
+    if (frameList.length > 0) {
+      startStageLoop();
+    }
+  }
+
+  async function buildCards() {
+    if (!cardsGrid || !window.SequenceCard) {
+      return;
+    }
+    const template = await ensureCardTemplate();
+    destroyCards();
+
+    availableFolders.forEach((folderName, index) => {
+      const templateContent = template.content.firstElementChild;
+      if (!templateContent) {
+        return;
+      }
+      const cardElement = templateContent.cloneNode(true);
+      cardsGrid.appendChild(cardElement);
+      const cardInstance = new window.SequenceCard(
+        cardElement,
+        folderName,
+        index,
+        buildFrameSrc,
+        handleCardSelect
+      );
+      cardInstance.loadPreview();
+      cards.push(cardInstance);
+    });
+
+    updateCardCount();
+  }
+
+  function handleCardSelect(folderName, cardIndex) {
+    if (typeof cardIndex !== "number") {
+      return;
+    }
+    selectCardByIndex(cardIndex);
+  }
+
+  function highlightActiveCard() {
+    cards.forEach((card) => {
+      if (typeof card.setActive === "function") {
+        card.setActive(card.index === currentIndex);
+      }
+    });
+  }
+
+  function resetStage() {
+    stopStageAnimation();
+    switchToRemoteSource();
+    frameList = [];
+    currentFolder = "";
+    currentFrameCursor = 0;
+    showLoading(false);
+    hidePlayerError();
+    setStagePlaceholderVisible(true);
+    if (frameCounterLabel) {
+      frameCounterLabel.textContent = "0 帧";
+    }
+    if (folderNameLabel) {
+      folderNameLabel.textContent = "--";
+    }
+    if (folderCounterLabel) {
+      folderCounterLabel.textContent = "0 / 0";
+    }
+  }
+
+  async function selectCardByIndex(index) {
+    if (!availableFolders.length) {
+      resetStage();
+      return;
+    }
+
+    const total = availableFolders.length;
+    currentIndex = ((index % total) + total) % total;
+    currentFolder = availableFolders[currentIndex];
+    updateMeta();
+    highlightActiveCard();
+    await startStageForFolder(currentFolder);
+  }
+
+  function sanitizeFrameList(frameInfo) {
+    if (frameInfo && Array.isArray(frameInfo.frames) && frameInfo.frames.length > 0) {
+      return frameInfo.frames;
+    }
+    const maxFrame = frameInfo && frameInfo.maxFrame ? frameInfo.maxFrame : 0;
+    if (!maxFrame) {
+      return [];
+    }
+    return Array.from({ length: maxFrame }, (_, idx) => idx + 1);
+  }
+
+  async function startStageForFolder(folderName) {
+    stopStageAnimation();
+    switchToRemoteSource();
+    frameList = [];
+    currentFrameCursor = 0;
+    showLoading(true);
+    hidePlayerError();
+    if (frameCounterLabel) {
+      frameCounterLabel.textContent = "0 帧";
+    }
+
+    if (!folderName) {
+      showLoading(false);
+      showPlayerError("未选择动画");
+      return;
+    }
+
+    try {
+      const response = await fetch(`http://localhost:3000/api/frames/${folderName}`);
+      if (!response.ok) {
+        throw new Error("Failed to fetch frames");
+      }
+      const data = await response.json();
+      frameList = sanitizeFrameList(data);
+
+      if (!frameList.length) {
+        showPlayerError("暂无可用帧");
+        setStagePlaceholderVisible(true);
+        showLoading(false);
+        return;
+      }
+
+      if (frameCounterLabel) {
+        frameCounterLabel.textContent = `${frameList.length} 帧`;
+      }
+
+      updateStageImage(frameList[0]);
+      showLoading(false);
+      startStageLoop();
+    } catch (error) {
+      console.error(error);
+      showLoading(false);
+      showPlayerError("加载失败");
+    }
+  }
+
+  async function loadAvailableFolders() {
+    availableFolders = [];
+    try {
+      const response = await fetch(foldersApi);
+      if (!response.ok) {
+        throw new Error("Failed to fetch folders");
+      }
+      const folders = await response.json();
+      availableFolders = Array.isArray(folders) ? folders : [];
+    } catch (error) {
+      console.warn("使用回退文件夹列表:", error.message);
+      const maxCount = 3002;
+      for (let i = 1; i <= maxCount; i++) {
+        availableFolders.push(buildFolderName("player_", i, 4));
+      }
+    }
+
+    updateNavDisabledState();
+    await buildCards();
+
+    if (!availableFolders.length) {
+      resetStage();
+    }
+  }
+
+  async function startLocalPreview(files, folderLabel) {
+    log("🎬 开始本地预览,文件数:", files?.length, "文件夹:", folderLabel);
+    
+    if (!Array.isArray(files) || !files.length) {
+      logWarn("文件数组为空或无效");
+      showPlayerError("未检测到 PNG 图片");
+      return;
+    }
+
+    log("文件列表:");
+    files.forEach((file, idx) => {
+      log(`  [${idx}] ${file.name} - ${file.size} bytes`);
+    });
+
+    stopStageAnimation();
+    showLoading(true);
+    hidePlayerError();
+    setStagePlaceholderVisible(false);
+    cleanupLocalFrames();
+
+    const orderedFiles = sortFilesForPlayback(files);
+    log("排序后的文件数:", orderedFiles.length);
+    
+    localFrameResources = orderedFiles.map((file, index) => ({
+      url: URL.createObjectURL(file),
+      name: file.name || `frame_${index + 1}`,
+      loaded: false,
+    }));
+
+    frameSourceMode = "local";
+    frameList = localFrameResources;
+    currentFrameCursor = 0;
+    currentFolder = folderLabel || "本地导入";
+    currentIndex = -1;
+
+    if (folderNameLabel) {
+      folderNameLabel.textContent = currentFolder;
+    }
+    if (folderCounterLabel) {
+      folderCounterLabel.textContent = "-";
+    }
+    if (frameCounterLabel) {
+      frameCounterLabel.textContent = `${localFrameResources.length} 帧`;
+    }
+
+    // 预加载所有图片后再开始播放
+    try {
+      log("开始预加载图片...");
+      await preloadLocalFrames(localFrameResources);
+      if (localFrameResources.length > 0) {
+        log("✅ 预加载完成,开始播放");
+        updateStageImage(localFrameResources[0]);
+        showLoading(false);
+        startStageLoop();
+      } else {
+        logWarn("预加载后资源为空");
+        showLoading(false);
+        showPlayerError("图片加载失败");
+      }
+    } catch (error) {
+      logError("预加载图片失败:", error);
+      showLoading(false);
+      showPlayerError("图片加载失败");
+    }
+  }
+
+  function preloadLocalFrames(frames) {
+    return new Promise((resolve) => {
+      if (!frames || !frames.length) {
+        log("⚠️ 预加载:帧列表为空");
+        resolve();
+        return;
+      }
+
+      let loadedCount = 0;
+      let hasError = false;
+      const totalFrames = frames.length;
+      log(`🖼️ 预加载 ${totalFrames} 个帧...`);
+
+      frames.forEach((frame, index) => {
+        const img = new Image();
+        
+        img.onload = () => {
+          frame.loaded = true;
+          loadedCount++;
+          log(`  ✅ [${loadedCount}/${totalFrames}] ${frame.name} 加载成功`);
+          if (loadedCount === totalFrames) {
+            log(`🎉 所有帧加载完成!`);
+            resolve();
+          }
+        };
+
+        img.onerror = () => {
+          hasError = true;
+          loadedCount++;
+          logWarn(`  ❌ [${loadedCount}/${totalFrames}] 帧 ${index + 1} 加载失败: ${frame.name}`);
+          if (loadedCount === totalFrames) {
+            logWarn(`预加载完成,但有 ${hasError ? '错误' : '部分失败'}`);
+            resolve();
+          }
+        };
+
+        log(`  ⏳ 开始加载 [${index + 1}/${totalFrames}]: ${frame.name}`);
+        img.src = frame.url;
+      });
+
+      // 超时保护:最多等待 10 秒
+      setTimeout(() => {
+        if (loadedCount < totalFrames) {
+          logWarn(`⏰ 预加载超时,已加载 ${loadedCount}/${totalFrames} 帧`);
+          resolve();
+        }
+      }, 10000);
+    });
+  }
+
+  function sortFilesForPlayback(files) {
+    return files
+      .slice()
+      .sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: "base" }));
+  }
+
+  function bindDropZone() {
+    if (!playerShell) {
+      return;
+    }
+    playerShell.addEventListener("dragenter", handleDropZoneDragEnter);
+    playerShell.addEventListener("dragover", handleDropZoneDragOver);
+    playerShell.addEventListener("dragleave", handleDropZoneDragLeave);
+    playerShell.addEventListener("drop", handleDropZoneDrop);
+  }
+
+  function preventDragDefaults(event) {
+    if (!event) {
+      return;
+    }
+    event.preventDefault();
+    event.stopPropagation();
+  }
+
+  function setDropZoneState(isActive) {
+    if (!playerShell) {
+      return;
+    }
+    playerShell.classList.toggle("is-dragging", Boolean(isActive));
+  }
+
+  function handleDropZoneDragEnter(event) {
+    preventDragDefaults(event);
+    setDropZoneState(true);
+  }
+
+  function handleDropZoneDragOver(event) {
+    preventDragDefaults(event);
+    setDropZoneState(true);
+  }
+
+  function handleDropZoneDragLeave(event) {
+    preventDragDefaults(event);
+    if (!playerShell) {
+      return;
+    }
+    const related = event.relatedTarget;
+    if (related && playerShell.contains(related)) {
+      return;
+    }
+    setDropZoneState(false);
+  }
+
+  async function handleDropZoneDrop(event) {
+    log("📥 Drop事件触发");
+    preventDragDefaults(event);
+    setDropZoneState(false);
+    const transfer = event.dataTransfer;
+    if (!transfer) {
+      logWarn("dataTransfer 为空");
+      return;
+    }
+
+    log("dataTransfer.items.length:", transfer.items?.length);
+    log("dataTransfer.files.length:", transfer.files?.length);
+
+    try {
+      const { files, folderLabel } = await collectDroppedPngFiles(transfer);
+      log("✅ 收集到的文件数:", files.length, "文件夹名:", folderLabel);
+      if (!files.length) {
+        logWarn("未检测到 PNG 图片");
+        showPlayerError("未检测到 PNG 图片");
+        return;
+      }
+      await startLocalPreview(files, folderLabel);
+    } catch (error) {
+      logError("拖放处理错误:", error);
+      showPlayerError("读取文件夹失败");
+    }
+  }
+
+  async function collectDroppedPngFiles(dataTransfer) {
+    const items = Array.from((dataTransfer && dataTransfer.items) || []);
+    log("🔍 开始收集文件,items数量:", items.length);
+    let collected = [];
+
+    if (items.length) {
+      log("处理 dataTransfer.items...");
+      const results = await Promise.all(items.map((item, idx) => {
+        log(`  - Item ${idx}: kind=${item.kind}, type=${item.type}`);
+        return readDataTransferItem(item);
+      }));
+      collected = results.flat();
+      log("从 items 收集到:", collected.length, "个条目");
+    }
+
+    if (!collected.length && dataTransfer && dataTransfer.files) {
+      log("Items为空,尝试使用 dataTransfer.files...");
+      collected = Array.from(dataTransfer.files).map((file) => {
+        log(`  - File: ${file.name}, type=${file.type}, size=${file.size}`);
+        return { file };
+      });
+      log("从 files 收集到:", collected.length, "个文件");
+    }
+
+    log("过滤前总数:", collected.length);
+    collected.forEach((entry, idx) => {
+      if (entry.file) {
+        log(`  [${idx}] ${entry.file.name} - type: ${entry.file.type}, path: ${entry.file.webkitRelativePath || '(无路径)'}`);
+      }
+    });
+
+    const pngEntries = collected.filter(({ file }) => {
+      const isPng = isTopLevelPngFile(file);
+      if (!isPng && file) {
+        log(`  ❌ 过滤掉: ${file.name} (type=${file.type})`);
+      }
+      return isPng;
+    });
+    
+    log("✅ 过滤后 PNG 文件数:", pngEntries.length);
+    
+    return {
+      files: pngEntries.map((entry) => entry.file),
+      folderLabel: deriveFolderLabel(pngEntries),
+    };
+  }
+
+  function isTopLevelPngFile(file) {
+    if (!file) {
+      log("    🔍 isTopLevelPngFile: file 为空");
+      return false;
+    }
+    const isPng = file.type === "image/png" || /\.png$/i.test(file.name || "");
+    log(`    🔍 isTopLevelPngFile: ${file.name}, type="${file.type}", isPng=${isPng}`);
+    if (!isPng) {
+      return false;
+    }
+    if (!file.webkitRelativePath) {
+      log(`      ✅ 无相对路径,视为顶级文件`);
+      return true;
+    }
+    const segments = file.webkitRelativePath.split("/").filter(Boolean);
+    const isTopLevel = segments.length <= 2;
+    log(`      📂 相对路径: ${file.webkitRelativePath}, 层级=${segments.length}, isTopLevel=${isTopLevel}`);
+    return isTopLevel;
+  }
+
+  function deriveFolderLabel(entries) {
+    if (!entries || !entries.length) {
+      return "本地导入";
+    }
+    for (const entry of entries) {
+      if (entry.rootName) {
+        return entry.rootName;
+      }
+      const inferred = inferFolderFromFile(entry.file);
+      if (inferred) {
+        return inferred;
+      }
+    }
+    const fallback = entries[0] && entries[0].file && entries[0].file.name;
+    return fallback ? fallback.replace(/\.png$/i, "") : "本地导入";
+  }
+
+  function inferFolderFromFile(file) {
+    if (!file || !file.webkitRelativePath) {
+      return "";
+    }
+    const segments = file.webkitRelativePath.split("/").filter(Boolean);
+    return segments.length ? segments[0] : "";
+  }
+
+  function inferFolderFromEntry(entry) {
+    if (!entry || !entry.fullPath) {
+      return "";
+    }
+    const segments = entry.fullPath.split("/").filter(Boolean);
+    if (segments.length >= 2) {
+      return segments[segments.length - 2];
+    }
+    return segments[0] || "";
+  }
+
+  function readDataTransferItem(item) {
+    return new Promise((resolve) => {
+      if (!item || item.kind !== "file") {
+        log("  ⚠️ Item 不是文件类型");
+        resolve([]);
+        return;
+      }
+      const entry = item.webkitGetAsEntry ? item.webkitGetAsEntry() : null;
+      if (!entry) {
+        log("  ⚠️ 无法获取 entry,使用 getAsFile");
+        const file = item.getAsFile();
+        resolve(file ? [{ file }] : []);
+        return;
+      }
+      log(`  📁 Entry: ${entry.name}, isFile=${entry.isFile}, isDirectory=${entry.isDirectory}`);
+      if (entry.isFile) {
+        entry.file(
+          (file) => {
+            log(`    ✅ 读取文件成功: ${file.name}`);
+            resolve(file ? [{ file, rootName: inferFolderFromEntry(entry) }] : []);
+          },
+          (err) => {
+            logError(`    ❌ 读取文件失败:`, err);
+            resolve([]);
+          }
+        );
+        return;
+      }
+      if (entry.isDirectory) {
+        log(`    📂 开始读取目录: ${entry.name}`);
+        readDirectoryImmediateFiles(entry).then((files) => {
+          log(`    ✅ 目录读取完成,文件数: ${files.length}`);
+          resolve(
+            files.map((file) => ({
+              file,
+              rootName: entry.name || inferFolderFromFile(file),
+            }))
+          );
+        });
+        return;
+      }
+      resolve([]);
+    });
+  }
+
+  function readDirectoryImmediateFiles(directoryEntry) {
+    return new Promise((resolve) => {
+      if (!directoryEntry || !directoryEntry.isDirectory) {
+        logWarn("directoryEntry 无效或不是目录");
+        resolve([]);
+        return;
+      }
+      const reader = directoryEntry.createReader();
+      const files = [];
+      let batchCount = 0;
+
+      const readBatch = () => {
+        reader.readEntries(
+          (entries) => {
+            batchCount++;
+            log(`      📦 读取批次 ${batchCount}: ${entries.length} 个条目`);
+            if (!entries.length) {
+              log(`      ✅ 目录读取完成,总文件数: ${files.length}`);
+              resolve(files);
+              return;
+            }
+            let pending = entries.length;
+            entries.forEach((entry) => {
+              if (entry.isFile) {
+                log(`        📄 文件: ${entry.name}`);
+                entry.file(
+                  (file) => {
+                    if (file) {
+                      log(`          ✅ 文件读取成功: ${file.name}, size=${file.size}, type=${file.type}`);
+                      files.push(file);
+                    }
+                    pending -= 1;
+                    if (pending === 0) {
+                      readBatch();
+                    }
+                  },
+                  (err) => {
+                    logError(`          ❌ 文件读取失败: ${entry.name}`, err);
+                    pending -= 1;
+                    if (pending === 0) {
+                      readBatch();
+                    }
+                  }
+                );
+              } else {
+                log(`        📁 跳过子目录: ${entry.name}`);
+                // skip subdirectories completely
+                pending -= 1;
+                if (pending === 0) {
+                  readBatch();
+                }
+              }
+            });
+          },
+          (err) => {
+            logError("      ❌ readEntries 失败:", err);
+            resolve(files);
+          }
+        );
+      };
+
+      readBatch();
+    });
+  }
+
+  function bindControls() {
+    if (fpsSlider && fpsValue) {
+      fpsSlider.value = currentFps;
+      fpsValue.textContent = `${currentFps} FPS`;
+      fpsSlider.addEventListener("input", () => {
+        const value = parseInt(fpsSlider.value, 10) || currentFps;
+        setFps(value);
+        fpsValue.textContent = `${value} FPS`;
+      });
+    }
+
+    if (prevBtn) {
+      prevBtn.addEventListener("click", () => selectCardByIndex(currentIndex - 1));
+    }
+
+    if (nextBtn) {
+      nextBtn.addEventListener("click", () => selectCardByIndex(currentIndex + 1));
+    }
+
+    if (downloadBtn) {
+      downloadBtn.addEventListener("click", () => {
+        if (!currentFolder) {
+          return;
+        }
+        if (window.SpriteSheetMaker && typeof window.SpriteSheetMaker.handleDownloadClick === "function") {
+          window.SpriteSheetMaker.handleDownloadClick(currentFolder, currentIndex);
+        }
+      });
+    }
+
+    document.addEventListener("keydown", (event) => {
+      if (event.key === "ArrowLeft") {
+        selectCardByIndex(currentIndex - 1);
+      } else if (event.key === "ArrowRight") {
+        selectCardByIndex(currentIndex + 1);
+      }
+    });
+
+    bindDropZone();
+  }
+
+  let previewCard = null;
+
+  window.addEventListener("DOMContentLoaded", async () => {
+    // 初始化预览卡片
+    const container = document.getElementById('previewCardContainer');
+    if (container && window.PreviewCard) {
+      try {
+        previewCard = new window.PreviewCard(container, {
+          fps: 8,
+          onFpsChange: (fps) => {
+            // log('FPS changed to:', fps);
+            setFps(fps);
+          }
+        });
+        // log('✅ PreviewCard initialized');
+      } catch (error) {
+        logError('Failed to initialize PreviewCard:', error);
+      }
+    }
+    
+    bindControls();
+    loadAvailableFolders();
+  });
+
+  window.addEventListener("beforeunload", cleanupLocalFrames);
+})();
+
+

+ 495 - 0
client/js/sprite_sheet_maker/sprite-sheet-maker.js

@@ -0,0 +1,495 @@
+// 九宫格图生成器 - 下载处理模块
+// 实现类似 Texture Packer 的功能:将多张图片拼接成一张图,并生成 JSON 文件
+
+(function () {
+  const TEXTURE_ROOT = "http://localhost:3000/disk_data";
+
+  // Cocos Creator 配置选项
+  const COCOS_CONFIG = {
+    // 是否使用 2 的幂次方尺寸(Cocos Creator 3.8 不需要,但可以启用以获得更好的性能)
+    usePowerOfTwo: false,
+    // 图片拼接时是否对齐到像素边界(推荐开启)
+    pixelPerfect: true
+  };
+
+  // 计算最小2的幂次方(可选,用于优化纹理内存使用)
+  function nextPowerOfTwo(n) {
+    if (n <= 0) return 1;
+    if ((n & (n - 1)) === 0) return n; // 已经是2的幂
+    let power = 1;
+    while (power < n) {
+      power <<= 1;
+    }
+    return power;
+  }
+
+  // 计算最终尺寸(根据配置决定是否使用 2 的幂次方)
+  function calculateFinalSize(width, height) {
+    if (COCOS_CONFIG.usePowerOfTwo) {
+      return {
+        width: nextPowerOfTwo(width),
+        height: nextPowerOfTwo(height)
+      };
+    } else {
+      // 直接使用实际尺寸,Cocos Creator 3.8 完全支持
+      return { width, height };
+    }
+  }
+
+  // 加载图片
+  function loadImage(src) {
+    return new Promise((resolve, reject) => {
+      const img = new Image();
+      img.crossOrigin = 'anonymous';
+      img.onload = () => resolve(img);
+      img.onerror = () => reject(new Error(`Failed to load image: ${src}`));
+      img.src = src;
+    });
+  }
+
+  // 显示加载动画
+  function showLoadingModal(folderName) {
+    // 创建模态框
+    const modal = document.createElement('div');
+    modal.id = 'spriteSheetLoadingModal';
+    modal.style.cssText = `
+      position: fixed;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: 100%;
+      background: rgba(0, 0, 0, 0.7);
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      justify-content: center;
+      z-index: 99999;
+      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+    `;
+
+    const content = document.createElement('div');
+    content.style.cssText = `
+      background: #ffffff;
+      border-radius: 12px;
+      padding: 30px 40px;
+      text-align: center;
+      box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
+      min-width: 300px;
+    `;
+
+    const title = document.createElement('div');
+    title.textContent = '正在生成 Sprite Sheet';
+    title.style.cssText = `
+      font-size: 18px;
+      font-weight: 600;
+      color: #1f2937;
+      margin-bottom: 10px;
+    `;
+
+    const folder = document.createElement('div');
+    folder.textContent = folderName;
+    folder.style.cssText = `
+      font-size: 14px;
+      color: #6b7280;
+      margin-bottom: 20px;
+    `;
+
+    // 加载动画
+    const spinner = document.createElement('div');
+    spinner.style.cssText = `
+      width: 40px;
+      height: 40px;
+      border: 4px solid #e5e7eb;
+      border-top-color: #3b82f6;
+      border-radius: 50%;
+      animation: spin 0.8s linear infinite;
+      margin: 0 auto 20px;
+    `;
+
+    // 添加动画样式
+    if (!document.getElementById('spriteSheetLoadingStyle')) {
+      const style = document.createElement('style');
+      style.id = 'spriteSheetLoadingStyle';
+      style.textContent = `
+        @keyframes spin {
+          to { transform: rotate(360deg); }
+        }
+      `;
+      document.head.appendChild(style);
+    }
+
+    const status = document.createElement('div');
+    status.id = 'spriteSheetStatus';
+    status.textContent = '加载图片中...';
+    status.style.cssText = `
+      font-size: 14px;
+      color: #374151;
+    `;
+
+    content.appendChild(title);
+    content.appendChild(folder);
+    content.appendChild(spinner);
+    content.appendChild(status);
+    modal.appendChild(content);
+    document.body.appendChild(modal);
+
+    return {
+      modal,
+      updateStatus: (text) => {
+        status.textContent = text;
+      }
+    };
+  }
+
+  // 隐藏加载动画
+  function hideLoadingModal() {
+    const modal = document.getElementById('spriteSheetLoadingModal');
+    if (modal) {
+      modal.remove();
+    }
+  }
+
+  // 下载文件
+  function downloadFile(data, filename, mimeType) {
+    const blob = new Blob([data], { type: mimeType });
+    const url = URL.createObjectURL(blob);
+    const a = document.createElement('a');
+    a.href = url;
+    a.download = filename;
+    document.body.appendChild(a);
+    a.click();
+    document.body.removeChild(a);
+    URL.revokeObjectURL(url);
+  }
+
+  // 生成 Cocos Creator 3.8 兼容的 JSON 格式的 sprite sheet 数据
+  // Cocos Creator 使用标准的 TexturePacker JSON 格式,坐标系统从上到下(左上角为原点)
+  function generateJSON(folderName, images, sheetWidth, sheetHeight) {
+    const frames = {};
+    
+    images.forEach((img, index) => {
+      const frameNum = (index + 1).toString().padStart(2, '0');
+      const frameName = `${frameNum}.png`;
+      const x = img.x;
+      const y = img.y;  // Cocos Creator 和 Canvas 都使用从上到下的坐标系统
+      const width = img.width;
+      const height = img.height;
+      
+      // 标准的 TexturePacker JSON 格式,Cocos Creator 3.8 完全兼容
+      frames[frameName] = {
+        frame: { 
+          x: x, 
+          y: y,
+          w: width, 
+          h: height 
+        },
+        rotated: false,
+        trimmed: false,
+        spriteSourceSize: { x: 0, y: 0, w: width, h: height },
+        sourceSize: { w: width, h: height }
+      };
+    });
+
+    const json = {
+      frames: frames,
+      meta: {
+        app: "SpriteSheetMaker for Cocos Creator 3.8",
+        version: "1.0",
+        image: `${folderName}.png`,
+        format: "RGBA8888",
+        size: { w: sheetWidth, h: sheetHeight },
+        scale: "1"
+      }
+    };
+
+    return JSON.stringify(json, null, 2);
+  }
+
+  // 将 Blob 转换为 Base64
+  function blobToBase64(blob) {
+    return new Promise((resolve, reject) => {
+      const reader = new FileReader();
+      reader.onloadend = () => {
+        // 移除 data:image/png;base64, 前缀
+        const base64 = reader.result.split(',')[1];
+        resolve(base64);
+      };
+      reader.onerror = reject;
+      reader.readAsDataURL(blob);
+    });
+  }
+
+  // 发送数据到服务器打包并下载
+  async function packAndDownload(folderName, imageBlob, jsonData, loading) {
+    try {
+      loading.updateStatus('转换图片数据...');
+      // 将图片转换为 Base64
+      const imageBase64 = await blobToBase64(imageBlob);
+      
+      loading.updateStatus('发送到服务器打包...');
+      // 发送到服务器打包
+      const response = await fetch('http://localhost:3000/api/pack', {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json'
+        },
+        body: JSON.stringify({
+          folderName: folderName,
+          imageData: imageBase64,
+          jsonData: jsonData
+        })
+      });
+
+      if (!response.ok) {
+        const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
+        throw new Error(errorData.error || `服务器错误: ${response.status}`);
+      }
+
+      loading.updateStatus('下载 ZIP 文件...');
+      // 获取 ZIP 文件的 Blob
+      const zipBlob = await response.blob();
+      
+      // 下载 ZIP 文件
+      downloadFile(zipBlob, `${folderName}.zip`, 'application/zip');
+      
+      hideLoadingModal();
+    } catch (error) {
+      hideLoadingModal();
+      console.error('打包失败:', error);
+      alert(`打包失败: ${error.message}`);
+    }
+  }
+
+  // 拼接图片
+  async function packImages(folderName, frameNumbers) {
+    const loading = showLoadingModal(folderName);
+
+    try {
+      // 1. 加载所有图片
+      loading.updateStatus(`加载图片中... (0/${frameNumbers.length})`);
+      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`;
+        
+        loading.updateStatus(`加载图片中... (${i + 1}/${frameNumbers.length})`);
+        const img = await loadImage(imgSrc);
+        images.push({
+          img: img,
+          width: img.width,
+          height: img.height,
+          frameNum: frameNum
+        });
+      }
+
+      // 2. 计算最优布局(尽可能接近正方形)
+      loading.updateStatus('计算布局中...');
+      
+      // 计算所有图片的平均尺寸和总面积
+      let totalArea = 0;
+      let maxImageWidth = 0;
+      let maxImageHeight = 0;
+      images.forEach((item) => {
+        totalArea += item.width * item.height;
+        maxImageWidth = Math.max(maxImageWidth, item.width);
+        maxImageHeight = Math.max(maxImageHeight, item.height);
+      });
+      
+      // 估算目标尺寸(接近正方形)
+      const estimatedSide = Math.ceil(Math.sqrt(totalArea));
+      const estimatedCols = Math.ceil(estimatedSide / maxImageWidth);
+      
+      // 尝试不同的列数,找到长宽差最小的方案
+      let bestLayout = null;
+      let bestDiff = Infinity;
+      
+      // 缩小搜索范围以提高效率,但确保覆盖合理的范围
+      let maxCols = Math.min(images.length, Math.ceil(estimatedCols * 1.5));
+      let minCols = Math.max(1, Math.floor(estimatedCols * 0.7));
+      
+      // 确保minCols不超过maxCols,并且至少尝试几种列数
+      if (minCols > maxCols) {
+        minCols = Math.max(1, Math.floor(maxCols * 0.5));
+      }
+      
+      // 对于图片数量很少的情况,确保尝试所有可能的列数
+      if (images.length <= 10) {
+        minCols = 1;
+        maxCols = images.length;
+      }
+      
+      for (let cols = minCols; cols <= maxCols; cols++) {
+        const rows = Math.ceil(images.length / cols);
+        const layout = [];
+        
+        // 计算每行的宽度和高度
+        const rowWidths = new Array(rows).fill(0);
+        const rowHeights = new Array(rows).fill(0);
+        
+        images.forEach((item, index) => {
+          const row = Math.floor(index / cols);
+          const col = index % cols;
+          
+          rowWidths[row] += item.width;
+          rowHeights[row] = Math.max(rowHeights[row], item.height);
+        });
+        
+        // 计算总尺寸(实际尺寸)
+        const totalWidth = Math.max(...rowWidths);
+        const totalHeight = rowHeights.reduce((sum, h) => sum + h, 0);
+        
+        // 计算最终尺寸(根据配置决定是否使用 2 的幂次方)
+        const finalSize = calculateFinalSize(totalWidth, totalHeight);
+        const sheetWidth = finalSize.width;
+        const sheetHeight = finalSize.height;
+        
+        // 计算长宽差的绝对值(使用实际尺寸比较,以便找到最接近正方形的布局)
+        const diff = Math.abs(totalWidth - totalHeight);
+        
+        // 如果这个方案更好,保存它
+        if (diff < bestDiff) {
+          bestDiff = diff;
+          
+          // 生成完整的布局位置信息
+          const currentLayout = [];
+          let currentY = 0;
+          
+          for (let row = 0; row < rows; row++) {
+            let currentX = 0;
+            const rowHeight = rowHeights[row];
+            
+            for (let col = 0; col < cols; col++) {
+              const index = row * cols + col;
+              if (index >= images.length) break;
+              
+              const item = images[index];
+              currentLayout.push({
+                x: currentX,
+                y: currentY,
+                width: item.width,
+                height: item.height,
+                img: item.img,
+                frameNum: item.frameNum
+              });
+              
+              currentX += item.width;
+            }
+            
+            currentY += rowHeight;
+          }
+          
+          bestLayout = {
+            layout: currentLayout,
+            width: sheetWidth,
+            height: sheetHeight
+          };
+        }
+      }
+      
+      // 如果没找到最佳布局(理论上不应该发生),使用默认的水平布局作为后备
+      if (!bestLayout) {
+        let currentX = 0;
+        let maxHeight = 0;
+        const defaultLayout = [];
+        
+        images.forEach((item) => {
+          defaultLayout.push({
+            x: currentX,
+            y: 0,
+            width: item.width,
+            height: item.height,
+            img: item.img,
+            frameNum: item.frameNum
+          });
+          currentX += item.width;
+          maxHeight = Math.max(maxHeight, item.height);
+        });
+        
+        const totalWidth = currentX;
+        const totalHeight = maxHeight;
+          const finalSize = calculateFinalSize(totalWidth, totalHeight);
+          bestLayout = {
+            layout: defaultLayout,
+            width: finalSize.width,
+            height: finalSize.height
+          };
+      }
+      
+      // 使用最佳布局
+      const layout = bestLayout.layout;
+      const sheetWidth = bestLayout.width;
+      const sheetHeight = bestLayout.height;
+
+      // 4. 创建 Canvas 并绘制
+      loading.updateStatus('拼接图片中...');
+      const canvas = document.createElement('canvas');
+      canvas.width = sheetWidth;
+      canvas.height = sheetHeight;
+      const ctx = canvas.getContext('2d');
+
+      // 填充透明背景
+      ctx.clearRect(0, 0, sheetWidth, sheetHeight);
+
+      // 绘制所有图片
+      layout.forEach((item) => {
+        ctx.drawImage(item.img, item.x, item.y);
+      });
+
+      // 5. 生成 JSON 文件
+      loading.updateStatus('生成 JSON 文件...');
+      const jsonData = generateJSON(folderName, layout, sheetWidth, sheetHeight);
+
+      // 6. 打包并下载(使用服务器端打包)
+      loading.updateStatus('准备打包...');
+      
+      // 将图片转换为 blob,然后发送到服务器打包
+      canvas.toBlob(async (imageBlob) => {
+        await packAndDownload(folderName, imageBlob, jsonData, loading);
+      }, 'image/png');
+
+    } catch (error) {
+      hideLoadingModal();
+      alert(`生成失败: ${error.message}`);
+    }
+  }
+
+  // 处理卡片下载按钮点击事件
+  async function handleDownloadClick(folderName, index) {
+    if (!folderName) {
+      return;
+    }
+
+    try {
+      // 获取帧列表
+      const encodedFolderName = encodeURIComponent(folderName);
+      const response = await fetch(`http://localhost:3000/api/frames/${encodedFolderName}`);
+      if (!response.ok) {
+        throw new Error('无法获取帧列表');
+      }
+
+      const data = await response.json();
+      const frameNumbers = data.frames || [];
+
+      if (frameNumbers.length === 0) {
+        alert('该文件夹中没有图片');
+        return;
+      }
+
+      // 开始拼接
+      await packImages(folderName, frameNumbers);
+
+    } catch (error) {
+      alert(`下载失败: ${error.message}`);
+    }
+  }
+
+  // 导出到全局
+  window.SpriteSheetMaker = {
+    handleDownloadClick: handleDownloadClick
+  };
+})();

+ 8 - 0
client/page/alert-overlay.html

@@ -0,0 +1,8 @@
+<!-- 全局提示遮罩层(用于服务器断连重连) -->
+<div class="alert-overlay" id="alertOverlay">
+    <div class="alert-content">
+        <div class="alert-spinner"></div>
+        <div class="alert-message" id="alertMessage">服务器断开连接,正在重连...</div>
+    </div>
+</div>
+

+ 5 - 0
client/page/alert-view.html

@@ -0,0 +1,5 @@
+<!-- 全局文字提示 -->
+<div class="global-alert" id="globalAlert">
+    <div class="alert-message" id="alertMessage"></div>
+</div>
+

+ 25 - 0
client/page/assets/assets.html

@@ -0,0 +1,25 @@
+<!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/assets.css" />
+</head>
+<body>
+  <div class="assets-root">
+    <!-- 左侧:网盘管理 -->
+    <div class="assets-left">
+      <iframe id="diskFrame" src="../disk/disk.html" frameborder="0"></iframe>
+    </div>
+
+    <!-- 右侧:动画预览器 -->
+    <div class="assets-right">
+      <iframe id="playerFrame" src="../seq_ani_player/seq-ani-player.html" frameborder="0"></iframe>
+    </div>
+  </div>
+
+  <script src="../../js/Assets.js"></script>
+</body>
+</html>
+

+ 13 - 0
client/page/confirm-view.html

@@ -0,0 +1,13 @@
+<!-- 全局确认对话框 -->
+<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>
+

+ 94 - 0
client/page/disk/disk.html

@@ -0,0 +1,94 @@
+<!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/disk/tool-bar.css">
+    <link rel="stylesheet" href="../../css/disk/disk.css">
+    <link rel="stylesheet" href="../../css/disk/right-click-menu.css">
+</head>
+<body>
+    <div class="disk-container" id="toolbarContainer">
+        <!-- 工具栏将动态加载到这里 -->
+
+        <!-- 文件列表区域 -->
+        <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 id="contextMenuContainer"></div>
+    </div>
+
+    <!-- 隐藏的文件输入 -->
+    <input type="file" id="fileInput" multiple accept="image/*" style="display: none;">
+
+    <!-- 框选区域 -->
+    <div class="selection-box" id="selectionBox"></div>
+
+    <script src="../../js/disk/alert.js"></script>
+    <script src="../../js/disk/path.js"></script>
+    <script src="../../js/disk/multiple-selection.js"></script>
+    <script src="../../js/disk/shortcut-keys.js"></script>
+    <script src="../../js/disk/search-bar.js"></script>
+    <script src="../../js/disk/right-click-menu.js"></script>
+    <script src="../../js/disk/disk.js"></script>
+</body>
+</html>
+

+ 22 - 0
client/page/disk/right-click-menu.html

@@ -0,0 +1,22 @@
+<!-- 自定义右键菜单 -->
+<div class="context-menu" id="contextMenu">
+    <button class="context-menu-item" data-action="new">
+        <span>新建</span>
+    </button>
+    <button class="context-menu-item" data-action="cut">
+        <span>剪切</span>
+    </button>
+    <button class="context-menu-item" data-action="copy">
+        <span>复制</span>
+    </button>
+    <button class="context-menu-item" data-action="paste">
+        <span>粘贴</span>
+    </button>
+    <button class="context-menu-item" data-action="remove-bg">
+        <span>一键抠背景</span>
+    </button>
+    <button class="context-menu-item" data-action="crop-mini">
+        <span>剪裁最小区域</span>
+    </button>
+</div>
+

+ 49 - 0
client/page/disk/tool-bar.html

@@ -0,0 +1,49 @@
+<!-- 网盘工具栏组件 -->
+
+<!-- 头部区域 -->
+<div class="header">
+    <div class="breadcrumb" id="breadcrumb">
+        <span class="breadcrumb-item active" data-path="">全部文件</span>
+    </div>
+    <div class="header-right">
+        <button class="btn-upload" id="btnUpload" title="上传文件">
+            <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
+                <path d="M8 4L3 9h3v5h4V9h3L8 4z"/>
+                <path d="M2 2h12v2H2z"/>
+            </svg>
+            <span>上传</span>
+        </button>
+        <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;">
+                <svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor">
+                    <path d="M7 0a7 7 0 1 0 0 14A7 7 0 0 0 7 0zm3 9.5L8.5 11 7 9.5 5.5 11 4 9.5 5.5 8 4 6.5 5.5 5 7 6.5 8.5 5 10 6.5 8.5 8 10 9.5z"/>
+                </svg>
+            </button>
+        </div>
+    </div>
+</div>
+
+<!-- 选中操作栏 -->
+<div class="selection-bar" id="selectionBar">
+    <div class="selection-actions">
+        <button class="btn-action" id="btnDownload">
+            <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
+                <path d="M8 12L3 7h3V2h4v5h3L8 12z"/>
+                <path d="M2 13h12v2H2z"/>
+            </svg>
+            下载
+        </button>
+        <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>
+

+ 54 - 0
client/page/export-view.html

@@ -0,0 +1,54 @@
+<!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/export-view.css">
+</head>
+<body>
+    <div class="export-container">
+        <div class="export-header">
+            <h2>导出动画</h2>
+        </div>
+
+        <div class="export-content">
+            <!-- Spritesheet 预览区 -->
+            <div class="export-section">
+                <h3>Spritesheet 预览</h3>
+                <div class="preview-box" id="previewBox">
+                    <canvas id="spritesheetCanvas"></canvas>
+                    <div class="preview-placeholder" id="previewPlaceholder">
+                        正在生成 Spritesheet...
+                    </div>
+                </div>
+            </div>
+
+            <!-- 参考图上传区 -->
+            <div class="export-section">
+                <h3>参考图(可选)</h3>
+                <div class="reference-upload" id="referenceUpload">
+                    <input type="file" id="referenceInput" accept="image/*" style="display: none;">
+                    <div class="reference-preview" id="referencePreview">
+                        <div class="upload-hint">
+                            <svg width="48" height="48" viewBox="0 0 48 48" fill="currentColor">
+                                <path d="M24 8L16 16h6v12h4V16h6L24 8z"/>
+                                <path d="M8 36h32v4H8z"/>
+                            </svg>
+                            <p>点击或拖拽上传参考图</p>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <div class="export-actions">
+            <button class="export-btn export-cancel" id="exportCancelBtn">取消</button>
+            <button class="export-btn export-confirm" id="exportConfirmBtn">确定并下载</button>
+        </div>
+    </div>
+
+    <script src="../js/export-view.js"></script>
+</body>
+</html>
+

+ 83 - 0
client/page/export/export-view.html

@@ -0,0 +1,83 @@
+<!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/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>
+        </div>
+    </div>
+
+    <script src="../../js/export-view/export-view.js"></script>
+</body>
+</html>

+ 19 - 0
client/page/navigation/navigation.html

@@ -0,0 +1,19 @@
+<!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/navigation.css" />
+</head>
+<body>
+  <!-- 导航栏 -->
+  <nav class="top-navigation">
+    <button class="nav-btn" data-page="store">动画商城</button>
+    <button class="nav-btn active" data-page="assets">我的资产</button>
+  </nav>
+
+  <script src="../../js/navigation.js"></script>
+</body>
+</html>
+

+ 64 - 0
client/page/seq_ani_player/card.html

@@ -0,0 +1,64 @@
+<!-- 大卡片模板:主预览区域 -->
+<template id="preview-card-template">
+  <div class="preview-card">
+    <div class="preview-card-stage">
+      <!-- 顶部信息栏 -->
+      <div class="preview-info-bar" hidden>
+        <div class="folder-info">
+          <svg class="folder-icon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
+            <path d="M1 3.5A1.5 1.5 0 0 1 2.5 2h3.879a1.5 1.5 0 0 1 1.06.44l1.122 1.12A.5.5 0 0 0 9.207 4H14.5A1.5 1.5 0 0 1 16 5.5v7a1.5 1.5 0 0 1-1.5 1.5h-13A1.5 1.5 0 0 1 0 12.5v-9A1.5 1.5 0 0 1 1 3.5z"/>
+          </svg>
+          <span class="folder-name">--</span>
+        </div>
+        <button class="btn-export" title="导出为Sprite Sheet">
+          导出动画
+        </button>
+      </div>
+      
+      <img class="preview-image" alt="" />
+      <div class="loading-overlay" aria-hidden="true">
+        <div class="loading-spinner"></div>
+        <p class="loading-text">正在加载图片...</p>
+      </div>
+      <div class="image-error" hidden>暂无可用帧</div>
+      <div class="drop-hint">
+        <p class="hint-text">从左边拖入文件夹预览动画</p>
+      </div>
+    </div>
+    <div class="preview-card-controls">
+      <label for="fps-slider-{{id}}" class="control-label">播放速度</label>
+      <div class="slider-container">
+        <input
+          id="fps-slider-{{id}}"
+          class="fps-slider"
+          type="range"
+          min="1"
+          max="60"
+          value="8"
+        />
+        <span class="fps-value">8 FPS</span>
+      </div>
+    </div>
+  </div>
+</template>
+
+<!-- 小卡片模板:展示单个序列帧动画 -->
+<template id="card-template">
+  <article class="sequence-card" data-folder="" data-index="" data-valid="true">
+    <div class="card-content">
+      <div class="image-container">
+        <img class="card-image" alt="序列帧预览" />
+        <div class="loading-spinner" style="display: none;"></div>
+        <div class="image-error" hidden>加载失败</div>
+      </div>
+      <footer class="card-footer">
+        <span class="card-label">--</span>
+        <button class="card-download-btn" type="button" title="下载序列">
+          <span class="icon">⬇</span>
+        </button>
+      </footer>
+    </div>
+  </article>
+</template>
+
+

+ 23 - 0
client/page/seq_ani_player/seq-ani-player.html

@@ -0,0 +1,23 @@
+<!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/index.css" />
+  <link rel="stylesheet" href="../../css/seq_ani_player/seq-ani-player.css" />
+  <link rel="stylesheet" href="../../css/seq_ani_player/card.css" />
+</head>
+<body>
+  <div class="app-root">
+    <!-- 预览卡片容器 -->
+    <section class="player-stage">
+      <div id="previewCardContainer" class="preview-card-container"></div>
+    </section>
+  </div>
+
+  <script src="../../js/seq_ani_player/card.js"></script>
+  <script src="../../js/seq_ani_player/seq-ani-player.js"></script>
+  <script src="../../js/sprite_sheet_maker/sprite-sheet-maker.js"></script>
+</body>
+</html>

+ 22 - 0
client/page/store/store.html

@@ -0,0 +1,22 @@
+<!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/store.css" />
+</head>
+<body>
+  <div class="store-root">
+    <div class="store-container">
+      <div class="coming-soon">
+        <div class="coming-soon-icon">🎬</div>
+        <h2 class="coming-soon-title">动画商城</h2>
+        <p class="coming-soon-subtitle">即将上线</p>
+        <p class="coming-soon-description">这里将提供海量高质量动画资源供您选择</p>
+      </div>
+    </div>
+  </div>
+</body>
+</html>
+

BIN
client/static/favicon.png


BIN
client/static/logo.png


+ 1544 - 0
server/disk.js

@@ -0,0 +1,1544 @@
+// 网盘管理系统 - 服务端逻辑
+
+const fs = require('fs');
+const path = require('path');
+const { promisify } = require('util');
+const { spawn } = require('child_process');
+const formidable = require('formidable');
+
+const mkdir = promisify(fs.mkdir);
+const readdir = promisify(fs.readdir);
+const stat = promisify(fs.stat);
+const access = promisify(fs.access);
+const copyFile = promisify(fs.copyFile);
+const rmdir = promisify(fs.rmdir);
+const unlink = promisify(fs.unlink);
+
+class DiskManager {
+    constructor() {
+        // 数据存储根目录(在 Server 目录下)
+        this.rootDir = path.join(__dirname, 'disk_data');
+        this.tempDir = path.join(__dirname, 'temp');
+        this.pythonDir = path.join(__dirname, 'python');
+        this.ensureRootDir();
+    }
+
+    // 确保根目录存在
+    async ensureRootDir() {
+        try {
+            await access(this.rootDir);
+        } catch (error) {
+            await mkdir(this.rootDir, { recursive: true });
+            console.log('创建disk_data目录:', this.rootDir);
+        }
+    }
+
+    // 获取Python命令(尝试python或python3)
+    async getPythonCommand() {
+        return new Promise((resolve) => {
+            const python = spawn('python', ['--version']);
+            python.on('close', (code) => {
+                if (code === 0) {
+                    resolve('python');
+                } else {
+                    const python3 = spawn('python3', ['--version']);
+                    python3.on('close', (code) => {
+                        resolve(code === 0 ? 'python3' : null);
+                    });
+                }
+            });
+            python.on('error', () => {
+                const python3 = spawn('python3', ['--version']);
+                python3.on('close', (code) => {
+                    resolve(code === 0 ? 'python3' : null);
+                });
+                python3.on('error', () => resolve(null));
+            });
+        });
+    }
+
+    // 调用Python抠图脚本
+    async runImageMatting(inputFolder, outputFolder, onProgress) {
+        const pythonCmd = await this.getPythonCommand();
+        if (!pythonCmd) {
+            throw new Error('未找到Python环境,请安装Python 3.7+');
+        }
+
+        const pythonExe = path.join(this.pythonDir, 'venv', 'Scripts', 'python.exe');
+        const scriptPath = path.join(this.pythonDir, 'image-matting.py');
+        
+        // 检查是否使用虚拟环境
+        const usePython = await new Promise((resolve) => {
+            fs.access(pythonExe, fs.constants.F_OK, (err) => {
+                resolve(err ? pythonCmd : pythonExe);
+            });
+        });
+
+        return new Promise((resolve, reject) => {
+            console.log('\n' + '='.repeat(70));
+            console.log('【Python】🚀 启动AI抠图进程');
+            console.log('='.repeat(70));
+            console.log('【Python】命令:', usePython);
+            console.log('【Python】脚本:', scriptPath);
+            console.log('【Python】输入文件夹:', inputFolder);
+            console.log('【Python】输出文件夹:', outputFolder);
+            console.log('='.repeat(70) + '\n');
+            
+            // 使用 -u 参数让Python输出无缓冲
+            const python = spawn(usePython, ['-u', scriptPath, inputFolder, outputFolder]);
+            
+            console.log('【Python】进程已启动,PID:', python.pid);
+
+            let stdout = '';
+            let stderr = '';
+            let processed = 0;
+
+            python.stdout.on('data', (data) => {
+                const output = data.toString();
+                stdout += output;
+                // 分行打印,保持格式
+                const lines = output.split('\n');
+                lines.forEach(line => {
+                    if (line.trim()) {
+                        console.log(`【Python】抠图: ${line}`);
+                        
+                        // 捕获进度信息: PROGRESS: 1/22
+                        const progressMatch = line.match(/PROGRESS:\s*(\d+)\/(\d+)/);
+                        if (progressMatch) {
+                            const current = parseInt(progressMatch[1], 10);
+                            const total = parseInt(progressMatch[2], 10);
+                            console.log(`【Python】进度: ${current}/${total} (${Math.round(current/total*100)}%)`);
+                            
+                            // 调用进度回调
+                            if (onProgress) {
+                                onProgress(current, total);
+                            }
+                        }
+                    }
+                });
+
+                const successMatch = output.match(/成功:?\s*(\d+)|成功\s*(\d+)|Success:\s*(\d+)/);
+                if (successMatch) {
+                    processed = parseInt(successMatch[1] || successMatch[2] || successMatch[3], 10);
+                }
+            });
+
+            python.stderr.on('data', (data) => {
+                const error = data.toString();
+                stderr += error;
+                // 实时打印stderr
+                const lines = error.split('\n');
+                lines.forEach(line => {
+                    if (line.trim()) {
+                        console.error(`【Python】抠图错误: ${line}`);
+                    }
+                });
+            });
+
+            python.on('close', (code) => {
+                console.log('\n' + '='.repeat(70));
+                console.log(`【Python】抠图进程结束,退出码: ${code}`);
+                if (code === 0) {
+                    console.log(`【Python】✅ 抠图成功!处理了 ${processed} 张图片`);
+                    console.log('='.repeat(70) + '\n');
+                    resolve({ success: true, processed, message: '抠图完成' });
+                } else {
+                    console.error(`【Python】❌ 抠图失败,退出码: ${code}`);
+                    if (stderr) console.error(`【Python】错误详情:\n${stderr}`);
+                    console.log('='.repeat(70) + '\n');
+                    reject(new Error(`抠图失败,退出码: ${code}\n${stderr}`));
+                }
+            });
+
+            python.on('error', (error) => {
+                console.error(`【Python】❌ 启动进程失败:`, error);
+                reject(new Error(`启动Python进程失败: ${error.message}`));
+            });
+        });
+    }
+
+    // 调用Python裁剪脚本
+    async runCutMiniSize(inputFolder, outputFolder, onProgress) {
+        const pythonCmd = await this.getPythonCommand();
+        if (!pythonCmd) {
+            throw new Error('未找到Python环境,请安装Python 3.7+');
+        }
+
+        const pythonExe = path.join(this.pythonDir, 'venv', 'Scripts', 'python.exe');
+        const scriptPath = path.join(this.pythonDir, 'cut-mini-size.py');
+        
+        const usePython = await new Promise((resolve) => {
+            fs.access(pythonExe, fs.constants.F_OK, (err) => {
+                resolve(err ? pythonCmd : pythonExe);
+            });
+        });
+
+        return new Promise((resolve, reject) => {
+            console.log('\n' + '='.repeat(70));
+            console.log('【Python】✂️ 启动智能裁剪进程');
+            console.log('='.repeat(70));
+            console.log('【Python】命令:', usePython);
+            console.log('【Python】脚本:', scriptPath);
+            console.log('【Python】输入文件夹:', inputFolder);
+            console.log('【Python】输出文件夹:', outputFolder);
+            console.log('='.repeat(70) + '\n');
+            
+            // 使用 -u 参数让Python输出无缓冲
+            const python = spawn(usePython, ['-u', scriptPath, inputFolder, outputFolder]);
+            
+            console.log('【Python】进程已启动,PID:', python.pid);
+
+            let stdout = '';
+            let stderr = '';
+            let processed = 0;
+            let width = 0;
+            let height = 0;
+
+            python.stdout.on('data', (data) => {
+                const output = data.toString();
+                stdout += output;
+                // 分行打印,保持格式
+                const lines = output.split('\n');
+                lines.forEach(line => {
+                    if (line.trim()) {
+                        console.log(`【Python】裁剪: ${line}`);
+                        
+                        // 捕获进度信息: PROGRESS: 1/22
+                        const progressMatch = line.match(/PROGRESS:\s*(\d+)\/(\d+)/);
+                        if (progressMatch) {
+                            const current = parseInt(progressMatch[1], 10);
+                            const total = parseInt(progressMatch[2], 10);
+                            console.log(`【Python】进度: ${current}/${total} (${Math.round(current/total*100)}%)`);
+                            
+                            // 调用进度回调
+                            if (onProgress) {
+                                onProgress(current, total);
+                            }
+                        }
+                    }
+                });
+
+                const successMatch = output.match(/成功:?\s*(\d+)|成功\s*(\d+)|Success:\s*(\d+)/);
+                if (successMatch) {
+                    processed = parseInt(successMatch[1] || successMatch[2] || successMatch[3], 10);
+                }
+
+                const sizeMatch = output.match(/width=(\d+).*height=(\d+)/);
+                if (sizeMatch) {
+                    width = parseInt(sizeMatch[1], 10);
+                    height = parseInt(sizeMatch[2], 10);
+                }
+            });
+
+            python.stderr.on('data', (data) => {
+                const error = data.toString();
+                stderr += error;
+                // 实时打印stderr
+                const lines = error.split('\n');
+                lines.forEach(line => {
+                    if (line.trim()) {
+                        console.error(`【Python】裁剪错误: ${line}`);
+                    }
+                });
+            });
+
+            python.on('close', (code) => {
+                console.log('\n' + '='.repeat(70));
+                console.log(`【Python】裁剪进程结束,退出码: ${code}`);
+                if (code === 0) {
+                    console.log(`【Python】✅ 裁剪成功!处理了 ${processed} 张图片`);
+                    console.log(`【Python】📐 最终尺寸: ${width}x${height}`);
+                    console.log('='.repeat(70) + '\n');
+                    resolve({ 
+                        success: true, 
+                        processed, 
+                        width, 
+                        height,
+                        message: `裁剪完成,尺寸: ${width}x${height}` 
+                    });
+                } else {
+                    console.error(`【Python】❌ 裁剪失败,退出码: ${code}`);
+                    if (stderr) console.error(`【Python】错误详情:\n${stderr}`);
+                    console.log('='.repeat(70) + '\n');
+                    reject(new Error(`裁剪失败,退出码: ${code}\n${stderr}`));
+                }
+            });
+
+            python.on('error', (error) => {
+                console.error(`【Python】❌ 启动进程失败:`, error);
+                reject(new Error(`启动Python进程失败: ${error.message}`));
+            });
+        });
+    }
+
+    // 获取安全的文件路径
+    getSafePath(relativePath) {
+        // 移除开头的斜杠
+        relativePath = relativePath.replace(/^\/+/, '');
+        // 解析路径,防止目录遍历攻击
+        const fullPath = path.join(this.rootDir, relativePath);
+        // 确保路径在根目录内
+        if (!fullPath.startsWith(this.rootDir)) {
+            throw new Error('非法路径');
+        }
+        return fullPath;
+    }
+
+    // 检查文件夹是否包含预览图
+    async checkFolderPreview(folderPath) {
+        const commonNames = ['01.png', '00.png', '001.png', '0001.png', '1.png', '0.png'];
+        
+        try {
+            const items = await readdir(folderPath);
+            
+            // 首先检查常见的帧文件名
+            for (const name of commonNames) {
+                if (items.includes(name)) {
+                    return {
+                        hasPreview: true,
+                        previewFile: name
+                    };
+                }
+            }
+            
+            // 检查是否有任何PNG文件
+            const pngFiles = items.filter(item => item.toLowerCase().endsWith('.png'));
+            if (pngFiles.length > 0) {
+                // 按文件名排序,取第一个
+                pngFiles.sort();
+                return {
+                    hasPreview: true,
+                    previewFile: pngFiles[0]
+                };
+            }
+            
+            return {
+                hasPreview: false,
+                previewFile: null
+            };
+        } catch (error) {
+            return {
+                hasPreview: false,
+                previewFile: null
+            };
+        }
+    }
+
+    // 检查文件夹是否需要抠图(检测PNG图片是否有非透明背景)
+    async checkNeedMatting(folderPath) {
+        try {
+            const sharp = require('sharp');
+            const items = await readdir(folderPath);
+            
+            // 获取所有PNG文件(只计算一次,不区分大小写)
+            const pngFiles = items.filter(item => item.toLowerCase().endsWith('.png'));
+            
+            console.log(`[DiskManager] checkNeedMatting: ${folderPath}, 找到 ${pngFiles.length} 个PNG文件`);
+            
+            if (pngFiles.length === 0) {
+                return {
+                    needsMatting: false,
+                    pngCount: 0
+                };
+            }
+            
+            // 检查前3张PNG图片是否有非透明背景(采样检测,提高性能)
+            const samplesToCheck = Math.min(3, pngFiles.length);
+            let hasOpaqueBackground = false;
+            
+            for (let i = 0; i < samplesToCheck; i++) {
+                const filePath = path.join(folderPath, pngFiles[i]);
+                const hasOpaque = await this.checkImageHasOpaqueBackground(filePath);
+                if (hasOpaque) {
+                    hasOpaqueBackground = true;
+                    break; // 只要有一张有非透明背景,就需要抠图
+                }
+            }
+            
+            return {
+                needsMatting: hasOpaqueBackground,
+                pngCount: pngFiles.length
+            };
+        } catch (error) {
+            console.error('[DiskManager] 检查抠图需求失败:', error);
+            return {
+                needsMatting: true, // 出错时默认显示抠图选项(保守策略)
+                pngCount: 0
+            };
+        }
+    }
+
+    // 检查单个图片是否有非透明背景
+    async checkImageHasOpaqueBackground(imagePath) {
+        try {
+            const sharp = require('sharp');
+            const image = sharp(imagePath);
+            const metadata = await image.metadata();
+            
+            // 如果图片没有alpha通道,肯定有不透明背景
+            if (!metadata.hasAlpha) {
+                return true;
+            }
+            
+            // 获取图片数据,快速检查边缘像素
+            const { data, info } = await image
+                .ensureAlpha()
+                .raw()
+                .toBuffer({ resolveWithObject: true });
+            
+            const { width, height, channels } = info;
+            
+            // 采样检测:检查四个角和边缘的像素
+            const samplePoints = [
+                { x: 0, y: 0 }, // 左上
+                { x: width - 1, y: 0 }, // 右上
+                { x: 0, y: height - 1 }, // 左下
+                { x: width - 1, y: height - 1 }, // 右下
+                { x: Math.floor(width / 2), y: 0 }, // 顶部中间
+                { x: Math.floor(width / 2), y: height - 1 }, // 底部中间
+            ];
+            
+            let opaqueCount = 0;
+            for (const point of samplePoints) {
+                const idx = (point.y * width + point.x) * channels;
+                const alpha = data[idx + 3];
+                
+                // 如果alpha接近255(不透明),说明可能有背景
+                if (alpha > 250) {
+                    const r = data[idx];
+                    const g = data[idx + 1];
+                    const b = data[idx + 2];
+                    
+                    // 检查是否是白色或浅色背景(可能需要抠图)
+                    if (r > 230 && g > 230 && b > 230) {
+                        opaqueCount++;
+                    }
+                }
+            }
+            
+            // 如果超过一半的采样点都是不透明的浅色,判断为需要抠图
+            return opaqueCount >= 3;
+            
+        } catch (error) {
+            console.error('[DiskManager] 检查图片背景失败:', error);
+            return true; // 出错时默认需要抠图
+        }
+    }
+
+    // 处理文件列表请求
+    async handleListRequest(req, res) {
+        try {
+            const url = new URL(req.url, `http://${req.headers.host}`);
+            const relativePath = url.searchParams.get('path') || '';
+            const recursive = url.searchParams.get('recursive') === 'true'; // 是否递归获取所有子文件夹
+            
+            const fullPath = this.getSafePath(relativePath);
+
+            // 检查目录是否存在
+            try {
+                await access(fullPath);
+            } catch (error) {
+                // 目录不存在,创建它
+                await mkdir(fullPath, { recursive: true });
+            }
+
+            if (recursive) {
+                // 递归获取所有文件夹结构
+                const allFiles = await this.getFilesRecursive(fullPath, relativePath);
+                res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
+                res.end(JSON.stringify({
+                    success: true,
+                    files: allFiles,
+                    recursive: true
+                }));
+            } else {
+                // 只获取当前目录
+                const items = await readdir(fullPath);
+                const files = [];
+
+                for (const item of items) {
+                    const itemPath = path.join(fullPath, item);
+                    const stats = await stat(itemPath);
+                    const itemRelativePath = path.join(relativePath, item).replace(/\\/g, '/');
+
+                    const fileInfo = {
+                        name: item,
+                        path: itemRelativePath,
+                        type: stats.isDirectory() ? 'directory' : 'file',
+                        size: stats.size,
+                        modifiedTime: stats.mtime
+                    };
+
+                    // 如果是文件夹,检查是否包含PNG预览图,并提供完整的预览URL
+                    if (stats.isDirectory()) {
+                        const previewInfo = await this.checkFolderPreview(itemPath);
+                        fileInfo.hasPreview = previewInfo.hasPreview;
+                        if (previewInfo.hasPreview && previewInfo.previewFile) {
+                            // 返回完整的预览URL路径
+                            fileInfo.previewUrl = `/api/disk/preview?path=${encodeURIComponent(itemRelativePath + '/' + previewInfo.previewFile)}`;
+                        }
+                        
+                        // 检查是否需要抠图(是否有非透明背景的PNG)
+                        const mattingInfo = await this.checkNeedMatting(itemPath);
+                        fileInfo.needsMatting = mattingInfo.needsMatting;
+                        fileInfo.pngCount = mattingInfo.pngCount;
+                    }
+
+                    files.push(fileInfo);
+                }
+
+                // 排序:文件夹在前,然后按名称排序
+                files.sort((a, b) => {
+                    if (a.type === 'directory' && b.type !== 'directory') return -1;
+                    if (a.type !== 'directory' && b.type === 'directory') return 1;
+                    return a.name.localeCompare(b.name, 'zh-CN');
+                });
+
+                res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
+                res.end(JSON.stringify({
+                    success: true,
+                    files: files,
+                    recursive: false
+                }));
+            }
+        } catch (error) {
+            console.error('获取文件列表失败:', error);
+            res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
+            res.end(JSON.stringify({
+                success: false,
+                message: error.message
+            }));
+        }
+    }
+
+    // 递归获取所有文件夹结构(用于前端缓存)
+    async getFilesRecursive(fullPath, relativePath) {
+        const allFiles = [];
+        
+        const processDirectory = async (dirFullPath, dirRelativePath) => {
+            try {
+                const items = await readdir(dirFullPath);
+                
+                for (const item of items) {
+                    const itemPath = path.join(dirFullPath, item);
+                    const stats = await stat(itemPath);
+                    const itemRelativePath = dirRelativePath ? 
+                        path.join(dirRelativePath, item).replace(/\\/g, '/') : 
+                        item;
+
+                    const fileInfo = {
+                        name: item,
+                        path: itemRelativePath,
+                        type: stats.isDirectory() ? 'directory' : 'file',
+                        size: stats.size,
+                        modifiedTime: stats.mtime
+                    };
+
+                    // 如果是文件夹,检查是否包含PNG
+                    if (stats.isDirectory()) {
+                        const previewInfo = await this.checkFolderPreview(itemPath);
+                        fileInfo.hasPreview = previewInfo.hasPreview;
+                        if (previewInfo.hasPreview && previewInfo.previewFile) {
+                            fileInfo.previewUrl = `/api/disk/preview?path=${encodeURIComponent(itemRelativePath + '/' + previewInfo.previewFile)}`;
+                        }
+                        
+                        const mattingInfo = await this.checkNeedMatting(itemPath);
+                        fileInfo.needsMatting = mattingInfo.needsMatting;
+                        fileInfo.pngCount = mattingInfo.pngCount;
+                        
+                        // 递归处理子文件夹
+                        await processDirectory(itemPath, itemRelativePath);
+                    }
+
+                    allFiles.push(fileInfo);
+                }
+            } catch (error) {
+                console.error(`递归读取目录失败: ${dirRelativePath}`, error);
+            }
+        };
+        
+        await processDirectory(fullPath, relativePath);
+        
+        // 排序:文件夹在前,然后按路径排序
+        allFiles.sort((a, b) => {
+            if (a.type === 'directory' && b.type !== 'directory') return -1;
+            if (a.type !== 'directory' && b.type === 'directory') return 1;
+            return a.path.localeCompare(b.path, 'zh-CN');
+        });
+        
+        console.log(`[DiskManager] 递归获取文件结构完成,共 ${allFiles.length} 个项目`);
+        return allFiles;
+    }
+
+    // 处理文件上传请求
+    async handleUploadRequest(req, res) {
+        try {
+            const form = formidable.formidable({
+                multiples: true,
+                maxFileSize: 500 * 1024 * 1024, // 500MB
+                keepExtensions: true
+            });
+
+            form.parse(req, async (err, fields, files) => {
+                if (err) {
+                    console.error('解析上传文件失败:', err);
+                    res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
+                    res.end(JSON.stringify({
+                        success: false,
+                        message: '上传失败'
+                    }));
+                    return;
+                }
+
+                try {
+                    // 新版 formidable 将字段解析为数组,需要取第一个元素
+                    const relativePath = Array.isArray(fields.path) ? fields.path[0] : (fields.path || '');
+                    const fileRelativePath = Array.isArray(fields.relativePath) ? fields.relativePath[0] : (fields.relativePath || '');
+                    
+                    // 获取上传文件的完整路径
+                    const uploadPath = path.join(relativePath, fileRelativePath);
+                    const targetPath = this.getSafePath(uploadPath);
+                    
+                    // 确保目标目录存在
+                    const targetDir = path.dirname(targetPath);
+                    await mkdir(targetDir, { recursive: true });
+
+                    // 获取上传的文件
+                    const uploadedFile = Array.isArray(files.file) ? files.file[0] : files.file;
+                    
+                    // 移动文件
+                    await this.moveFile(uploadedFile.filepath, targetPath);
+
+                    res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
+                    res.end(JSON.stringify({
+                        success: true,
+                        message: '上传成功'
+                    }));
+                } catch (error) {
+                    console.error('保存文件失败:', error);
+                    res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
+                    res.end(JSON.stringify({
+                        success: false,
+                        message: error.message
+                    }));
+                }
+            });
+        } catch (error) {
+            console.error('处理上传请求失败:', error);
+            res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
+            res.end(JSON.stringify({
+                success: false,
+                message: error.message
+            }));
+        }
+    }
+
+    // 移动文件
+    async moveFile(source, target) {
+        return new Promise((resolve, reject) => {
+            const readStream = fs.createReadStream(source);
+            const writeStream = fs.createWriteStream(target);
+
+            readStream.on('error', reject);
+            writeStream.on('error', reject);
+            writeStream.on('finish', () => {
+                // 删除临时文件
+                fs.unlink(source, (err) => {
+                    if (err) console.error('删除临时文件失败:', err);
+                    resolve();
+                });
+            });
+
+            readStream.pipe(writeStream);
+        });
+    }
+
+    // 处理创建文件夹请求
+    async handleCreateFolderRequest(req, res) {
+        try {
+            let body = '';
+            req.on('data', chunk => {
+                body += chunk.toString();
+            });
+
+            req.on('end', async () => {
+                try {
+                    const data = JSON.parse(body);
+                    const relativePath = data.path || '';
+                    const folderName = data.name;
+
+                    if (!folderName) {
+                        throw new Error('文件夹名称不能为空');
+                    }
+
+                    // 验证文件夹名称
+                    if (/[\\/:*?"<>|]/.test(folderName)) {
+                        throw new Error('文件夹名称包含非法字符');
+                    }
+
+                    const folderPath = path.join(relativePath, folderName);
+                    const fullPath = this.getSafePath(folderPath);
+
+                    // 检查文件夹是否已存在
+                    try {
+                        await access(fullPath);
+                        throw new Error('文件夹已存在');
+                    } catch (error) {
+                        if (error.message === '文件夹已存在') {
+                            throw error;
+                        }
+                        // 文件夹不存在,创建它
+                        await mkdir(fullPath, { recursive: true });
+                    }
+
+                    res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
+                    res.end(JSON.stringify({
+                        success: true,
+                        message: '创建成功'
+                    }));
+                } catch (error) {
+                    console.error('创建文件夹失败:', error);
+                    res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
+                    res.end(JSON.stringify({
+                        success: false,
+                        message: error.message
+                    }));
+                }
+            });
+        } catch (error) {
+            console.error('处理创建文件夹请求失败:', error);
+            res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
+            res.end(JSON.stringify({
+                success: false,
+                message: error.message
+            }));
+        }
+    }
+
+    // 处理重命名请求
+    async handleRenameRequest(req, res) {
+        try {
+            let body = '';
+            req.on('data', chunk => {
+                body += chunk.toString();
+            });
+
+            req.on('end', async () => {
+                try {
+                    const data = JSON.parse(body);
+                    const oldPath = data.oldPath;
+                    const newName = data.newName;
+
+                    if (!oldPath || !newName) {
+                        throw new Error('路径和新名称不能为空');
+                    }
+
+                    // 验证新名称
+                    if (/[\\/:*?"<>|]/.test(newName)) {
+                        throw new Error('名称包含非法字符');
+                    }
+
+                    const oldFullPath = this.getSafePath(oldPath);
+                    const parentDir = path.dirname(oldFullPath);
+                    const newFullPath = path.join(parentDir, newName);
+
+                    // 确保新路径也在根目录内
+                    if (!newFullPath.startsWith(this.rootDir)) {
+                        throw new Error('非法路径');
+                    }
+
+                    // 检查旧文件是否存在
+                    await access(oldFullPath);
+
+                    // 检查新名称是否已存在
+                    try {
+                        await access(newFullPath);
+                        throw new Error('该名称已存在');
+                    } catch (error) {
+                        if (error.message === '该名称已存在') {
+                            throw error;
+                        }
+                        // 文件不存在,可以重命名
+                    }
+
+                    // 执行重命名
+                    await promisify(fs.rename)(oldFullPath, newFullPath);
+
+                    res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
+                    res.end(JSON.stringify({
+                        success: true,
+                        message: '重命名成功'
+                    }));
+                } catch (error) {
+                    console.error('重命名失败:', error);
+                    res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
+                    res.end(JSON.stringify({
+                        success: false,
+                        message: error.message
+                    }));
+                }
+            });
+        } catch (error) {
+            console.error('处理重命名请求失败:', error);
+            res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
+            res.end(JSON.stringify({
+                success: false,
+                message: error.message
+            }));
+        }
+    }
+
+    // 处理文件下载请求
+    async handleDownloadRequest(req, res) {
+        try {
+            const url = new URL(req.url, `http://${req.headers.host}`);
+            const relativePath = url.searchParams.get('path') || '';
+            
+            const fullPath = this.getSafePath(relativePath);
+
+            // 检查文件是否存在
+            await access(fullPath);
+            const stats = await stat(fullPath);
+
+            if (stats.isDirectory()) {
+                throw new Error('无法下载文件夹');
+            }
+
+            // 设置响应头
+            const fileName = path.basename(fullPath);
+            res.writeHead(200, {
+                'Content-Type': 'application/octet-stream',
+                'Content-Disposition': `attachment; filename="${encodeURIComponent(fileName)}"`,
+                'Content-Length': stats.size
+            });
+
+            // 创建读取流并发送文件
+            const readStream = fs.createReadStream(fullPath);
+            readStream.pipe(res);
+        } catch (error) {
+            console.error('下载文件失败:', error);
+            res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
+            res.end(JSON.stringify({
+                success: false,
+                message: '文件不存在'
+            }));
+        }
+    }
+
+    // 处理图片预览请求
+    async handlePreviewRequest(req, res) {
+        try {
+            const url = new URL(req.url, `http://${req.headers.host}`);
+            const relativePath = url.searchParams.get('path') || '';
+            
+            const fullPath = this.getSafePath(relativePath);
+
+            // 检查文件是否存在
+            await access(fullPath);
+            const stats = await stat(fullPath);
+
+            if (stats.isDirectory()) {
+                throw new Error('无法预览文件夹');
+            }
+
+            // 获取文件扩展名
+            const ext = path.extname(fullPath).toLowerCase();
+            const mimeTypes = {
+                '.jpg': 'image/jpeg',
+                '.jpeg': 'image/jpeg',
+                '.png': 'image/png',
+                '.gif': 'image/gif',
+                '.bmp': 'image/bmp',
+                '.webp': 'image/webp',
+                '.svg': 'image/svg+xml'
+            };
+
+            const contentType = mimeTypes[ext];
+            if (!contentType) {
+                throw new Error('不支持的图片格式');
+            }
+
+            // 设置响应头,添加缓存
+            res.writeHead(200, {
+                'Content-Type': contentType,
+                'Content-Length': stats.size,
+                'Cache-Control': 'public, max-age=86400'
+            });
+
+            // 创建读取流并发送文件
+            const readStream = fs.createReadStream(fullPath);
+            readStream.pipe(res);
+        } catch (error) {
+            console.error('预览图片失败:', error);
+            res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
+            res.end(JSON.stringify({
+                success: false,
+                message: '图片不存在'
+            }));
+        }
+    }
+
+    // 处理移动文件/文件夹请求
+    async handleMoveRequest(req, res) {
+        try {
+            let body = '';
+            req.on('data', chunk => {
+                body += chunk.toString();
+            });
+
+            req.on('end', async () => {
+                try {
+                    const data = JSON.parse(body);
+                    const sourcePath = data.sourcePath;
+                    const targetFolder = data.targetFolder;
+
+                    if (!sourcePath) {
+                        throw new Error('源路径不能为空');
+                    }
+
+                    const sourceFullPath = this.getSafePath(sourcePath);
+                    const fileName = path.basename(sourceFullPath);
+                    
+                    // 目标路径
+                    const targetPath = targetFolder ? path.join(targetFolder, fileName) : fileName;
+                    const targetFullPath = this.getSafePath(targetPath);
+
+                    // 确保源文件存在
+                    await access(sourceFullPath);
+
+                    // 检查是否移动到自身或子目录
+                    if (targetFullPath.startsWith(sourceFullPath + path.sep) || targetFullPath === sourceFullPath) {
+                        throw new Error('不能移动到自身或子目录');
+                    }
+
+                    // 检查目标是否已存在
+                    try {
+                        await access(targetFullPath);
+                        throw new Error('目标位置已存在同名文件或文件夹');
+                    } catch (error) {
+                        if (error.message === '目标位置已存在同名文件或文件夹') {
+                            throw error;
+                        }
+                        // 文件不存在,可以移动
+                    }
+
+                    // 确保目标目录存在
+                    const targetDir = path.dirname(targetFullPath);
+                    await mkdir(targetDir, { recursive: true });
+
+                    // 执行移动
+                    await promisify(fs.rename)(sourceFullPath, targetFullPath);
+
+                    res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
+                    res.end(JSON.stringify({
+                        success: true,
+                        message: '移动成功'
+                    }));
+                } catch (error) {
+                    console.error('移动失败:', error);
+                    res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
+                    res.end(JSON.stringify({
+                        success: false,
+                        message: error.message
+                    }));
+                }
+            });
+        } catch (error) {
+            console.error('处理移动请求失败:', error);
+            res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
+            res.end(JSON.stringify({
+                success: false,
+                message: error.message
+            }));
+        }
+    }
+
+    // 处理复制文件/文件夹请求
+    async handleCopyRequest(req, res) {
+        try {
+            let body = '';
+            req.on('data', chunk => {
+                body += chunk.toString();
+            });
+
+            req.on('end', async () => {
+                try {
+                    const data = JSON.parse(body);
+                    const sourcePath = data.sourcePath;
+                    const targetFolder = data.targetFolder;
+
+                    if (!sourcePath) {
+                        throw new Error('源路径不能为空');
+                    }
+
+                    const sourceFullPath = this.getSafePath(sourcePath);
+                    const fileName = path.basename(sourceFullPath);
+                    const targetPath = targetFolder ? path.join(targetFolder, fileName) : fileName;
+                    const targetFullPath = this.getSafePath(targetPath);
+
+                    await access(sourceFullPath);
+
+                    if (targetFullPath === sourceFullPath || targetFullPath.startsWith(sourceFullPath + path.sep)) {
+                        throw new Error('不能复制到自身或子目录');
+                    }
+
+                    try {
+                        await access(targetFullPath);
+                        throw new Error('目标位置已存在同名文件或文件夹');
+                    } catch (error) {
+                        if (error.message === '目标位置已存在同名文件或文件夹') {
+                            throw error;
+                        }
+                    }
+
+                    await this.copyItemRecursive(sourceFullPath, targetFullPath);
+
+                    res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
+                    res.end(JSON.stringify({
+                        success: true,
+                        message: '复制成功'
+                    }));
+                } catch (error) {
+                    console.error('复制失败:', error);
+                    res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
+                    res.end(JSON.stringify({
+                        success: false,
+                        message: error.message
+                    }));
+                }
+            });
+        } catch (error) {
+            console.error('处理复制请求失败:', error);
+            res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
+            res.end(JSON.stringify({
+                success: false,
+                message: error.message
+            }));
+        }
+    }
+
+    async copyItemRecursive(source, target) {
+        const stats = await stat(source);
+        if (stats.isDirectory()) {
+            await mkdir(target, { recursive: true });
+            const entries = await readdir(source);
+            for (const entry of entries) {
+                const childSource = path.join(source, entry);
+                const childTarget = path.join(target, entry);
+                await this.copyItemRecursive(childSource, childTarget);
+            }
+        } else {
+            const targetDir = path.dirname(target);
+            await mkdir(targetDir, { recursive: true });
+            await copyFile(source, target);
+        }
+    }
+
+    // 处理删除文件/文件夹请求
+    async handleDeleteRequest(req, res) {
+        try {
+            let body = '';
+            req.on('data', chunk => {
+                body += chunk.toString();
+            });
+
+            req.on('end', async () => {
+                try {
+                    const data = JSON.parse(body);
+                    const paths = data.paths;
+
+                    if (!paths || !Array.isArray(paths) || paths.length === 0) {
+                        throw new Error('请选择要删除的文件');
+                    }
+
+                    const errors = [];
+
+                    for (const filePath of paths) {
+                        try {
+                            const fullPath = this.getSafePath(filePath);
+                            await access(fullPath);
+                            const stats = await stat(fullPath);
+
+                            if (stats.isDirectory()) {
+                                // 递归删除文件夹
+                                await this.deleteDirectory(fullPath);
+                            } else {
+                                // 删除文件
+                                await promisify(fs.unlink)(fullPath);
+                            }
+                        } catch (error) {
+                            errors.push(`${filePath}: ${error.message}`);
+                        }
+                    }
+
+                    if (errors.length > 0) {
+                        res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
+                        res.end(JSON.stringify({
+                            success: true,
+                            message: `部分删除成功,${errors.length} 个失败`,
+                            errors: errors
+                        }));
+                    } else {
+                        res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
+                        res.end(JSON.stringify({
+                            success: true,
+                            message: '删除成功'
+                        }));
+                    }
+                } catch (error) {
+                    console.error('删除失败:', error);
+                    res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
+                    res.end(JSON.stringify({
+                        success: false,
+                        message: error.message
+                    }));
+                }
+            });
+        } catch (error) {
+            console.error('处理删除请求失败:', error);
+            res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
+            res.end(JSON.stringify({
+                success: false,
+                message: error.message
+            }));
+        }
+    }
+
+    // 递归删除目录
+    async deleteDirectory(dirPath) {
+        const items = await readdir(dirPath);
+        
+        for (const item of items) {
+            const itemPath = path.join(dirPath, item);
+            const stats = await stat(itemPath);
+            
+            if (stats.isDirectory()) {
+                await this.deleteDirectory(itemPath);
+            } else {
+                await promisify(fs.unlink)(itemPath);
+            }
+        }
+        
+        await promisify(fs.rmdir)(dirPath);
+    }
+
+    // 处理一键抠图请求(使用SSE实时推送进度)
+    async handleRemoveBackgroundRequest(req, res) {
+        console.log('\n' + '▓'.repeat(70));
+        console.log('[API] 收到一键抠图请求');
+        console.log('▓'.repeat(70));
+        
+        // 设置SSE响应头
+        res.writeHead(200, {
+            'Content-Type': 'text/event-stream',
+            'Cache-Control': 'no-cache',
+            'Connection': 'keep-alive',
+            'Access-Control-Allow-Origin': '*'
+        });
+        
+        // 发送进度的辅助函数
+        const sendProgress = (data) => {
+            console.log('[API] → 发送SSE事件:', data);
+            const message = `data: ${JSON.stringify(data)}\n\n`;
+            res.write(message);
+            console.log('[API] ✓ SSE事件已发送');
+        };
+        
+        let body = '';
+        req.on('data', chunk => {
+            body += chunk.toString();
+            console.log('[API] → 接收请求数据...');
+        });
+
+        req.on('end', async () => {
+            try {
+                console.log('[API] ✓ 请求数据接收完成');
+                console.log('[API] → 解析JSON数据...');
+                const { paths } = JSON.parse(body);
+                console.log('[API] ✓ JSON解析成功');
+                console.log('[API]   请求路径:', paths);
+                
+                if (!paths || !Array.isArray(paths) || paths.length === 0) {
+                    console.warn('[API] ⚠ 路径为空或无效');
+                    res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
+                    res.end(JSON.stringify({
+                        success: false,
+                        message: '请选择要处理的文件或文件夹'
+                    }));
+                    return;
+                }
+
+                console.log(`[API] → 开始批量处理,共 ${paths.length} 个项目`);
+
+                let totalProcessed = 0;
+                let totalFolders = 0;
+
+                for (let i = 0; i < paths.length; i++) {
+                    const relativePath = paths[i];
+                    console.log(`\n[API] 处理项目 ${i + 1}/${paths.length}: ${relativePath}`);
+                    
+                    // 发送开始处理的进度
+                    sendProgress({
+                        type: 'folder-start',
+                        current: i + 1,
+                        total: paths.length,
+                        folderName: relativePath
+                    });
+                    
+                    console.log('[API] → 获取安全路径...');
+                    const fullPath = this.getSafePath(relativePath);
+                    console.log('[API] ✓ 完整路径:', fullPath);
+                    
+                    console.log('[API] → 读取文件状态...');
+                    const stats = await stat(fullPath);
+                    console.log('[API] ✓ 文件状态:', stats.isDirectory() ? '文件夹' : '文件');
+
+                    if (stats.isDirectory()) {
+                        console.log('[API] → 开始处理文件夹...');
+                        const result = await this.processFolder(fullPath, sendProgress, relativePath);
+                        console.log('[API] ✓ 文件夹处理完成');
+                        console.log('[API]   结果:', result);
+                        
+                        if (result.success) {
+                            totalProcessed += result.processed;
+                            totalFolders++;
+                            
+                            // 发送文件夹完成的进度
+                            sendProgress({
+                                type: 'folder-complete',
+                                current: i + 1,
+                                total: paths.length,
+                                folderName: relativePath,
+                                processed: result.processed
+                            });
+                        } else {
+                            console.error('[API] ✗ 文件夹处理失败:', result.message);
+                            sendProgress({
+                                type: 'folder-error',
+                                current: i + 1,
+                                total: paths.length,
+                                folderName: relativePath,
+                                error: result.message
+                            });
+                        }
+                    } else {
+                        console.log('[API] ⚠ 跳过单个文件(暂不支持)');
+                    }
+                }
+
+                console.log('\n' + '▓'.repeat(70));
+                console.log('[API] ✓✓✓ 所有项目处理完成!');
+                console.log(`[API]   处理文件夹: ${totalFolders} 个`);
+                console.log(`[API]   处理图片: ${totalProcessed} 张`);
+                console.log('▓'.repeat(70));
+                
+                console.log('[API] → 发送完成消息...');
+                sendProgress({
+                    type: 'complete',
+                    success: true,
+                    processed: totalProcessed,
+                    folders: totalFolders,
+                    message: `处理完成!处理了 ${totalFolders} 个文件夹,共 ${totalProcessed} 张图片`
+                });
+                res.end();
+                console.log('[API] ✓ SSE连接已关闭\n');
+            } catch (error) {
+                console.error('\n' + '▓'.repeat(70));
+                console.error('[API] ✗✗✗ 请求处理失败');
+                console.error('[API] 错误类型:', error.name);
+                console.error('[API] 错误信息:', error.message);
+                console.error('[API] 错误堆栈:', error.stack);
+                console.error('▓'.repeat(70) + '\n');
+                
+                sendProgress({
+                    type: 'error',
+                    success: false,
+                    message: error.message
+                });
+                res.end();
+            }
+        });
+    }
+
+    // 处理单个文件夹:只抠图(不裁剪)
+    async processFolder(folderPath, sendProgress, relativePath) {
+        const timestamp = Date.now();
+        const folderName = path.basename(folderPath);
+        
+        // 创建临时目录
+        const tempBase = path.join(this.tempDir, `matting_${timestamp}_${folderName}`);
+        const tempMatting = path.join(tempBase, 'output');
+
+        try {
+            console.log(`\n${'='.repeat(70)}`);
+            console.log(`[DiskManager] 开始抠背景: ${folderName}`);
+            console.log(`${'='.repeat(70)}`);
+            console.log(`[DiskManager] 原始路径: ${folderPath}`);
+            
+            // 创建临时目录
+            console.log(`[DiskManager] 创建临时目录...`);
+            await mkdir(tempBase, { recursive: true });
+            await mkdir(tempMatting, { recursive: true });
+            console.log(`[DiskManager] ✓ 临时目录创建完成`);
+            console.log(`[DiskManager]   - 抠图输出: ${tempMatting}`);
+
+            // 使用Python脚本进行抠图
+            console.log(`\n[DiskManager] >>> 开始AI抠背景 <<<`);
+            const mattingResult = await this.runImageMatting(folderPath, tempMatting, (current, total) => {
+                if (sendProgress) {
+                    sendProgress({
+                        type: 'image-progress',
+                        current: current,
+                        total: total,
+                        folderName: relativePath || folderName
+                    });
+                }
+            });
+            
+            if (!mattingResult.success) {
+                throw new Error(`抠背景失败: ${mattingResult.message}`);
+            }
+
+            console.log(`[DiskManager] ✓ 抠背景完成,处理了 ${mattingResult.processed} 张图片`);
+
+            // 将处理后的文件复制回原文件夹
+            console.log(`\n[DiskManager] >>> 保存结果到原文件夹 <<<`);
+            const saveResult = await this.saveProcessedFiles(tempMatting, folderPath);
+            
+            console.log(`[DiskManager] ✓ 保存完成,已替换 ${saveResult.saved} 张图片`);
+            console.log(`\n${'='.repeat(70)}`);
+            console.log(`[DiskManager] ✓✓✓ 处理完成!文件夹: ${folderName} ✓✓✓`);
+            console.log(`${'='.repeat(70)}\n`);
+
+            return {
+                success: true,
+                processed: saveResult.saved
+            };
+
+        } catch (error) {
+            console.error(`\n${'='.repeat(70)}`);
+            console.error(`[DiskManager] ✗✗✗ 处理失败: ${folderName} ✗✗✗`);
+            console.error(`[DiskManager] 错误信息: ${error.message}`);
+            console.error(`${'='.repeat(70)}\n`);
+            return {
+                success: false,
+                processed: 0,
+                message: error.message
+            };
+        } finally {
+            // 清理临时文件
+            try {
+                console.log('[DiskManager] 清理临时文件...');
+                await this.deleteDirectory(tempBase);
+                console.log('[DiskManager] ✓ 临时文件已清理');
+            } catch (error) {
+                console.warn('[DiskManager] ⚠ 清理临时文件失败:', error.message);
+            }
+        }
+    }
+
+    // 处理单个文件夹:只裁剪
+    async processFolderCropOnly(folderPath, sendProgress, relativePath) {
+        const timestamp = Date.now();
+        const folderName = path.basename(folderPath);
+        
+        // 创建临时目录
+        const tempBase = path.join(this.tempDir, `crop_${timestamp}_${folderName}`);
+        const tempCrop = path.join(tempBase, 'output');
+
+        try {
+            console.log(`\n${'='.repeat(70)}`);
+            console.log(`[DiskManager] 开始剪裁: ${folderName}`);
+            console.log(`${'='.repeat(70)}`);
+            console.log(`[DiskManager] 原始路径: ${folderPath}`);
+            
+            // 创建临时目录
+            console.log(`[DiskManager] 创建临时目录...`);
+            await mkdir(tempBase, { recursive: true });
+            await mkdir(tempCrop, { recursive: true });
+            console.log(`[DiskManager] ✓ 临时目录创建完成`);
+            console.log(`[DiskManager]   - 裁剪输出: ${tempCrop}`);
+
+            // 使用Python脚本裁剪多余透明区域
+            console.log(`\n[DiskManager] >>> 开始智能裁剪 <<<`);
+            const folderBaseName = path.basename(folderPath);
+            const cropResult = await this.runCutMiniSize(folderPath, tempCrop, (current, total) => {
+                if (sendProgress) {
+                    sendProgress({
+                        type: 'image-progress',
+                        current: current,
+                        total: total,
+                        folderName: relativePath || folderBaseName
+                    });
+                }
+            });
+            
+            if (!cropResult.success) {
+                throw new Error(`裁剪失败: ${cropResult.message}`);
+            }
+
+            console.log(`[DiskManager] ✓ 裁剪完成,最终尺寸: ${cropResult.width}x${cropResult.height}`);
+
+            // 将处理后的文件复制回原文件夹
+            console.log(`\n[DiskManager] >>> 保存结果到原文件夹 <<<`);
+            const saveResult = await this.saveProcessedFiles(tempCrop, folderPath);
+            
+            console.log(`[DiskManager] ✓ 保存完成,已替换 ${saveResult.saved} 张图片`);
+            console.log(`\n${'='.repeat(70)}`);
+            console.log(`[DiskManager] ✓✓✓ 裁剪完成!文件夹: ${folderName} ✓✓✓`);
+            console.log(`${'='.repeat(70)}\n`);
+
+            return {
+                success: true,
+                processed: saveResult.saved,
+                width: cropResult.width,
+                height: cropResult.height
+            };
+
+        } catch (error) {
+            console.error(`\n${'='.repeat(70)}`);
+            console.error(`[DiskManager] ✗✗✗ 裁剪失败: ${folderName} ✗✗✗`);
+            console.error(`[DiskManager] 错误信息: ${error.message}`);
+            console.error(`${'='.repeat(70)}\n`);
+            return {
+                success: false,
+                processed: 0,
+                message: error.message
+            };
+        } finally {
+            // 清理临时文件
+            try {
+                console.log('[DiskManager] 清理临时文件...');
+                await this.deleteDirectory(tempBase);
+                console.log('[DiskManager] ✓ 临时文件已清理');
+            } catch (error) {
+                console.warn('[DiskManager] ⚠ 清理临时文件失败:', error.message);
+            }
+        }
+    }
+
+    // 保存处理后的文件到原文件夹(直接替换)
+    async saveProcessedFiles(sourceFolder, targetFolder) {
+        console.log('[DiskManager] → 开始保存文件...');
+        console.log('[DiskManager]   源文件夹:', sourceFolder);
+        console.log('[DiskManager]   目标文件夹:', targetFolder);
+        
+        let saved = 0;
+        
+        try {
+            console.log('[DiskManager] → 读取源文件夹...');
+            const files = await readdir(sourceFolder);
+            console.log(`[DiskManager] ✓ 找到 ${files.length} 个文件`);
+            
+            for (let i = 0; i < files.length; i++) {
+                const file = files[i];
+                const sourcePath = path.join(sourceFolder, file);
+                const targetPath = path.join(targetFolder, file);
+                
+                console.log(`[DiskManager]   [${i + 1}/${files.length}] 替换: ${file}`);
+                
+                // 不备份,直接替换原文件
+                await copyFile(sourcePath, targetPath);
+                saved++;
+                console.log(`[DiskManager]     ✓ 已替换`);
+            }
+            
+            console.log(`[DiskManager] ✓ 所有文件保存完成,共 ${saved} 个`);
+            return { saved };
+        } catch (error) {
+            console.error('[DiskManager] ✗ 保存文件失败:', error);
+            throw error;
+        }
+    }
+
+    // 处理剪裁最小区域请求(使用SSE实时推送进度)
+    async handleCropMiniRequest(req, res) {
+        console.log('\n' + '▓'.repeat(70));
+        console.log('[API] 收到剪裁最小区域请求');
+        console.log('▓'.repeat(70));
+        
+        // 设置SSE响应头
+        res.writeHead(200, {
+            'Content-Type': 'text/event-stream',
+            'Cache-Control': 'no-cache',
+            'Connection': 'keep-alive',
+            'Access-Control-Allow-Origin': '*'
+        });
+        
+        // 发送进度的辅助函数
+        const sendProgress = (data) => {
+            console.log('[API] → 发送SSE事件:', data);
+            const message = `data: ${JSON.stringify(data)}\n\n`;
+            res.write(message);
+            console.log('[API] ✓ SSE事件已发送');
+        };
+        
+        let body = '';
+        req.on('data', chunk => {
+            body += chunk.toString();
+        });
+
+        req.on('end', async () => {
+            try {
+                const { paths } = JSON.parse(body);
+                
+                if (!paths || !Array.isArray(paths) || paths.length === 0) {
+                    res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
+                    res.end(JSON.stringify({
+                        success: false,
+                        message: '请选择要处理的文件或文件夹'
+                    }));
+                    return;
+                }
+
+                console.log(`[API] → 开始批量裁剪,共 ${paths.length} 个项目`);
+
+                let totalProcessed = 0;
+                let totalFolders = 0;
+
+                for (let i = 0; i < paths.length; i++) {
+                    const relativePath = paths[i];
+                    console.log(`\n[API] 裁剪项目 ${i + 1}/${paths.length}: ${relativePath}`);
+                    
+                    const fullPath = this.getSafePath(relativePath);
+                    const stats = await stat(fullPath);
+
+                    if (stats.isDirectory()) {
+                        const result = await this.processFolderCropOnly(fullPath, sendProgress, relativePath);
+                        
+                        if (result.success) {
+                            totalProcessed += result.processed;
+                            totalFolders++;
+                        }
+                    }
+                }
+
+                console.log('\n' + '▓'.repeat(70));
+                console.log('[API] ✓✓✓ 所有项目裁剪完成!');
+                console.log(`[API]   处理文件夹: ${totalFolders} 个`);
+                console.log(`[API]   处理图片: ${totalProcessed} 张`);
+                console.log('▓'.repeat(70));
+                
+                sendProgress({
+                    type: 'complete',
+                    success: true,
+                    processed: totalProcessed,
+                    folders: totalFolders,
+                    message: `裁剪完成!处理了 ${totalFolders} 个文件夹,共 ${totalProcessed} 张图片`
+                });
+                res.end();
+            } catch (error) {
+                console.error('[API] 裁剪请求处理失败:', error);
+                sendProgress({
+                    type: 'error',
+                    success: false,
+                    message: error.message
+                });
+                res.end();
+            }
+        });
+    }
+}
+
+module.exports = DiskManager;
+

BIN
server/disk_data/111/生成白色背景长矛刺击动画00.png


BIN
server/disk_data/111/生成白色背景长矛刺击动画01.png


BIN
server/disk_data/111/生成白色背景长矛刺击动画02.png


BIN
server/disk_data/111/生成白色背景长矛刺击动画03.png


BIN
server/disk_data/111/生成白色背景长矛刺击动画04.png


BIN
server/disk_data/111/生成白色背景长矛刺击动画05.png


BIN
server/disk_data/111/生成白色背景长矛刺击动画06.png


BIN
server/disk_data/111/生成白色背景长矛刺击动画07.png


BIN
server/disk_data/111/生成白色背景长矛刺击动画08.png


BIN
server/disk_data/111/生成白色背景长矛刺击动画09.png


BIN
server/disk_data/111/生成白色背景长矛刺击动画10.png


BIN
server/disk_data/111/生成白色背景长矛刺击动画11.png


BIN
server/disk_data/111/生成白色背景长矛刺击动画12.png


BIN
server/disk_data/111/生成白色背景长矛刺击动画13.png


BIN
server/disk_data/111/生成白色背景长矛刺击动画14.png


BIN
server/disk_data/111/生成白色背景长矛刺击动画15.png


BIN
server/disk_data/111/生成白色背景长矛刺击动画16.png


BIN
server/disk_data/111/生成白色背景长矛刺击动画17.png


BIN
server/disk_data/111/生成白色背景长矛刺击动画18.png


BIN
server/disk_data/111/生成白色背景长矛刺击动画19.png


BIN
server/disk_data/111/生成白色背景长矛刺击动画20.png


BIN
server/disk_data/111/生成白色背景长矛刺击动画21.png


BIN
server/disk_data/player_0001/01.png


BIN
server/disk_data/player_0001/02.png


BIN
server/disk_data/player_0001/03.png


BIN
server/disk_data/player_0001/04.png


BIN
server/disk_data/player_0001/05.png


BIN
server/disk_data/player_0001/06.png


BIN
server/disk_data/player_0001/07.png


BIN
server/disk_data/player_0001/08.png


BIN
server/disk_data/player_0001/09.png


BIN
server/disk_data/player_0001/10.png


BIN
server/disk_data/player_0001/11.png


BIN
server/disk_data/player_0001/12.png


BIN
server/disk_data/player_0001/13.png


BIN
server/disk_data/player_0001/14.png


BIN
server/disk_data/test1/生成白色背景长矛刺击动画00.png


BIN
server/disk_data/test1/生成白色背景长矛刺击动画01.png


BIN
server/disk_data/test1/生成白色背景长矛刺击动画02.png


BIN
server/disk_data/test1/生成白色背景长矛刺击动画03.png


BIN
server/disk_data/test1/生成白色背景长矛刺击动画04.png


BIN
server/disk_data/test1/生成白色背景长矛刺击动画05.png


BIN
server/disk_data/新建文件夹 (1)/player_0002/01.png


BIN
server/disk_data/新建文件夹 (1)/player_0002/02.png


BIN
server/disk_data/新建文件夹 (1)/player_0002/03.png


BIN
server/disk_data/新建文件夹 (1)/player_0002/04.png


BIN
server/disk_data/新建文件夹 (1)/player_0002/05.png


BIN
server/disk_data/新建文件夹 (1)/player_0002/06.png


BIN
server/disk_data/新建文件夹 (1)/player_0002/07.png


BIN
server/disk_data/新建文件夹 (1)/player_0002/08.png


Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików