Sfoglia il codice sorgente

第一个完整项目有bug

User 5 mesi fa
parent
commit
b67f4ed966
100 ha cambiato i file con 1639 aggiunte e 537 eliminazioni
  1. 0 61
      AnimationManager.bat
  2. 0 196
      client/MESSAGE_PASSING_ARCHITECTURE.md
  3. 113 0
      client/css/export-view/export-view.css
  4. 17 1
      client/index.html
  5. 244 7
      client/js/Index.js
  6. 20 17
      client/js/alert-view.js
  7. 16 0
      client/js/assets.js
  8. 255 13
      client/js/disk/disk.js
  9. 317 39
      client/js/export-view/export-view.js
  10. 89 12
      client/js/login/login.js
  11. 89 3
      client/js/navigation.js
  12. 109 3
      client/js/seq_ani_player/card.js
  13. 0 8
      client/page/alert-overlay.html
  14. 2 2
      client/page/disk/tool-bar.html
  15. 37 0
      client/page/export/export-view.html
  16. 2 0
      client/page/login/login.html
  17. 2 2
      client/page/navigation/navigation.html
  18. 69 7
      client/page/store/store.html
  19. BIN
      server/data.db
  20. 258 33
      server/disk.js
  21. BIN
      server/disk_data/111/生成白色背景长矛刺击动画00.png
  22. BIN
      server/disk_data/111/生成白色背景长矛刺击动画01.png
  23. BIN
      server/disk_data/111/生成白色背景长矛刺击动画02.png
  24. BIN
      server/disk_data/111/生成白色背景长矛刺击动画03.png
  25. BIN
      server/disk_data/111/生成白色背景长矛刺击动画04.png
  26. BIN
      server/disk_data/111/生成白色背景长矛刺击动画05.png
  27. BIN
      server/disk_data/111/生成白色背景长矛刺击动画06.png
  28. BIN
      server/disk_data/111/生成白色背景长矛刺击动画07.png
  29. BIN
      server/disk_data/111/生成白色背景长矛刺击动画08.png
  30. BIN
      server/disk_data/111/生成白色背景长矛刺击动画09.png
  31. BIN
      server/disk_data/111/生成白色背景长矛刺击动画10.png
  32. BIN
      server/disk_data/111/生成白色背景长矛刺击动画11.png
  33. BIN
      server/disk_data/111/生成白色背景长矛刺击动画12.png
  34. BIN
      server/disk_data/111/生成白色背景长矛刺击动画13.png
  35. BIN
      server/disk_data/111/生成白色背景长矛刺击动画14.png
  36. BIN
      server/disk_data/111/生成白色背景长矛刺击动画15.png
  37. BIN
      server/disk_data/111/生成白色背景长矛刺击动画16.png
  38. BIN
      server/disk_data/111/生成白色背景长矛刺击动画17.png
  39. BIN
      server/disk_data/111/生成白色背景长矛刺击动画18.png
  40. BIN
      server/disk_data/111/生成白色背景长矛刺击动画19.png
  41. BIN
      server/disk_data/111/生成白色背景长矛刺击动画20.png
  42. BIN
      server/disk_data/111/生成白色背景长矛刺击动画21.png
  43. BIN
      server/disk_data/player_0001/01.png
  44. BIN
      server/disk_data/player_0001/02.png
  45. BIN
      server/disk_data/player_0001/03.png
  46. BIN
      server/disk_data/player_0001/04.png
  47. BIN
      server/disk_data/player_0001/05.png
  48. BIN
      server/disk_data/player_0001/06.png
  49. BIN
      server/disk_data/player_0001/07.png
  50. BIN
      server/disk_data/player_0001/08.png
  51. BIN
      server/disk_data/player_0001/09.png
  52. BIN
      server/disk_data/player_0001/10.png
  53. BIN
      server/disk_data/player_0001/11.png
  54. BIN
      server/disk_data/player_0001/12.png
  55. BIN
      server/disk_data/player_0001/13.png
  56. BIN
      server/disk_data/player_0001/14.png
  57. BIN
      server/disk_data/test1/生成白色背景长矛刺击动画00.png
  58. BIN
      server/disk_data/test1/生成白色背景长矛刺击动画01.png
  59. BIN
      server/disk_data/test1/生成白色背景长矛刺击动画02.png
  60. BIN
      server/disk_data/test1/生成白色背景长矛刺击动画03.png
  61. BIN
      server/disk_data/test1/生成白色背景长矛刺击动画04.png
  62. BIN
      server/disk_data/test1/生成白色背景长矛刺击动画05.png
  63. BIN
      server/disk_data/新建文件夹 (1)/player_0002/01.png
  64. BIN
      server/disk_data/新建文件夹 (1)/player_0002/02.png
  65. BIN
      server/disk_data/新建文件夹 (1)/player_0002/03.png
  66. BIN
      server/disk_data/新建文件夹 (1)/player_0002/04.png
  67. BIN
      server/disk_data/新建文件夹 (1)/player_0002/05.png
  68. BIN
      server/disk_data/新建文件夹 (1)/player_0002/06.png
  69. BIN
      server/disk_data/新建文件夹 (1)/player_0002/07.png
  70. BIN
      server/disk_data/新建文件夹 (1)/player_0002/08.png
  71. BIN
      server/disk_data/新建文件夹 (1)/player_0002/09.png
  72. BIN
      server/disk_data/新建文件夹 (1)/player_0002/10.png
  73. BIN
      server/disk_data/新建文件夹 (1)/player_0002/11.png
  74. BIN
      server/disk_data/新建文件夹 (1)/player_0002/12.png
  75. BIN
      server/disk_data/新建文件夹 (1)/player_0002/13.png
  76. BIN
      server/disk_data/新建文件夹 (1)/player_0002/14.png
  77. BIN
      server/disk_data/新建文件夹/生成白色背景长矛刺击动画00.png
  78. BIN
      server/disk_data/新建文件夹/生成白色背景长矛刺击动画01.png
  79. BIN
      server/disk_data/新建文件夹/生成白色背景长矛刺击动画02.png
  80. BIN
      server/disk_data/新建文件夹/生成白色背景长矛刺击动画03.png
  81. BIN
      server/disk_data/新建文件夹/生成白色背景长矛刺击动画04.png
  82. BIN
      server/disk_data/新建文件夹/生成白色背景长矛刺击动画05.png
  83. BIN
      server/disk_data/新建文件夹/生成白色背景长矛刺击动画06.png
  84. BIN
      server/disk_data/新建文件夹/生成白色背景长矛刺击动画07.png
  85. BIN
      server/disk_data/新建文件夹/生成白色背景长矛刺击动画08.png
  86. BIN
      server/disk_data/新建文件夹/生成白色背景长矛刺击动画09.png
  87. BIN
      server/disk_data/新建文件夹/生成白色背景长矛刺击动画10.png
  88. BIN
      server/disk_data/新建文件夹/生成白色背景长矛刺击动画11.png
  89. BIN
      server/disk_data/新建文件夹/生成白色背景长矛刺击动画12.png
  90. BIN
      server/disk_data/新建文件夹/生成白色背景长矛刺击动画13.png
  91. BIN
      server/disk_data/新建文件夹/生成白色背景长矛刺击动画14.png
  92. BIN
      server/disk_data/新建文件夹/生成白色背景长矛刺击动画15.png
  93. BIN
      server/disk_data/新建文件夹/生成白色背景长矛刺击动画16.png
  94. BIN
      server/disk_data/新建文件夹/生成白色背景长矛刺击动画17.png
  95. BIN
      server/disk_data/新建文件夹/生成白色背景长矛刺击动画18.png
  96. BIN
      server/disk_data/新建文件夹/生成白色背景长矛刺击动画19.png
  97. BIN
      server/disk_data/新建文件夹/生成白色背景长矛刺击动画20.png
  98. BIN
      server/disk_data/新建文件夹/生成白色背景长矛刺击动画21.png
  99. 0 1
      server/market_data/.gitignore
  100. 0 132
      server/python/image-matting.py

+ 0 - 61
AnimationManager.bat

@@ -1,61 +0,0 @@
-@echo off
-setlocal enabledelayedexpansion
-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 ================检测服务器是否启动成功========================
-echo.
-echo [批处理脚本] 正在检测服务器状态...
-set MAX_ATTEMPTS=30
-set ATTEMPT=0
-set SERVER_READY=0
-
-:check_server
-set /a ATTEMPT+=1
-if %ATTEMPT% GTR %MAX_ATTEMPTS% (
-    echo 错误:服务器启动超时,请检查服务器日志
-    pause
-    exit /b 1
-)
-
-REM 使用 PowerShell 检测服务器是否响应(兼容性更好)
-powershell -NoProfile -Command "try { $response = Invoke-WebRequest -Uri 'http://localhost:3000/' -TimeoutSec 2 -UseBasicParsing -ErrorAction Stop; exit 0 } catch { exit 1 }" >nul 2>&1
-if %ERRORLEVEL% EQU 0 (
-    set SERVER_READY=1
-) else (
-    REM 如果 PowerShell 方法失败,尝试简单的端口检测
-    powershell -NoProfile -Command "try { $tcpClient = New-Object System.Net.Sockets.TcpClient; $tcpClient.Connect('localhost', 3000); $tcpClient.Close(); exit 0 } catch { exit 1 }" >nul 2>&1
-    if %ERRORLEVEL% EQU 0 (
-        set SERVER_READY=1
-    )
-)
-
-if %SERVER_READY% EQU 0 (
-    timeout /t 1 /nobreak >nul 2>&1
-    goto check_server
-)
-
-REM ================服务器已启动,打开浏览器========================
-echo [批处理脚本] 服务器已启动!
-echo.
-start http://localhost:3000/index.html?auto=true
-
-echo [批处理脚本] 浏览器将自动打开...
-echo.
-echo [批处理脚本] 按 Ctrl+C 或关闭此窗口停止服务器
-pause >nul
-
-REM 关闭 node 服务
-taskkill /F /IM node.exe >nul 2>&1

+ 0 - 196
client/MESSAGE_PASSING_ARCHITECTURE.md

@@ -1,196 +0,0 @@
-# 消息传递架构说明
-
-## 架构概览
-
-所有子 view 都通过 **Index.js** 作为消息中心来传递消息。消息传递采用 `postMessage` API,确保跨 iframe 通信的安全性。
-
-## 页面层级结构
-
-```
-index.html (主窗口)
-├── navigationFrame (navigation.html)
-├── pageFrame (assets.html 或其他页面)
-│   ├── diskFrame (disk.html) - 在 assets.html 中
-│   └── playerFrame (seq-ani-player.html) - 在 assets.html 中
-├── loginViewFrame (login.html)
-└── exportViewFrame (export-view.html)
-```
-
-## 消息传递路径
-
-### 1. 直接子页面 → Index.js
-以下页面直接发送消息到 `window.parent`(即 Index.js):
-- **navigation.html** → Index.js
-- **login.html** → Index.js
-- **export-view.html** → Index.js
-
-### 2. 嵌套子页面 → Assets.js → Index.js
-以下页面在 assets.html 中,消息先发送到 assets.html,再由 assets.js 转发到 Index.js:
-- **disk.html** → assets.html → Index.js
-- **seq-ani-player.html** → assets.html → Index.js
-
-## 支持的消息类型
-
-### 1. `global-alert` - 全局提示
-**消息格式:**
-```javascript
-{
-  type: 'global-alert',
-  text: '提示消息内容',
-  duration: 3000  // 可选,默认 3000ms
-}
-```
-
-**使用示例:**
-```javascript
-window.parent.postMessage({
-  type: 'global-alert',
-  text: '账号不存在',
-  duration: 3000
-}, '*');
-```
-
-**处理位置:** Index.js 第 204-212 行
-
-### 2. `global-loading` - 全局加载提示
-**消息格式:**
-```javascript
-{
-  type: 'global-loading',
-  action: 'show' | 'hide',
-  text: '正在处理...'  // 仅 action='show' 时需要
-}
-```
-
-**使用示例:**
-```javascript
-// 显示
-window.parent.postMessage({
-  type: 'global-loading',
-  action: 'show',
-  text: '正在处理...'
-}, '*');
-
-// 隐藏
-window.parent.postMessage({
-  type: 'global-loading',
-  action: 'hide'
-}, '*');
-```
-
-**处理位置:** Index.js 第 194-203 行
-
-### 3. `global-confirm` - 全局确认对话框
-**消息格式:**
-```javascript
-{
-  type: 'global-confirm',
-  id: 'confirm_1234567890',  // 唯一ID,用于匹配响应
-  message: '确认要删除吗?'
-}
-```
-
-**响应消息格式:**
-```javascript
-{
-  type: 'global-confirm-response',
-  id: 'confirm_1234567890',  // 与请求的 id 匹配
-  confirmed: true | false
-}
-```
-
-**使用示例:**
-```javascript
-const confirmId = 'confirm_' + Date.now();
-
-// 监听响应
-const handleResponse = (event) => {
-  if (event.data && event.data.type === 'global-confirm-response' && event.data.id === confirmId) {
-    window.removeEventListener('message', handleResponse);
-    if (event.data.confirmed) {
-      // 用户确认
-    } else {
-      // 用户取消
-    }
-  }
-};
-window.addEventListener('message', handleResponse);
-
-// 发送请求
-window.parent.postMessage({
-  type: 'global-confirm',
-  id: confirmId,
-  message: '确认要删除吗?'
-}, '*');
-```
-
-**处理位置:** Index.js 第 213-235 行
-
-### 4. `navigation` - 页面导航
-**消息格式:**
-```javascript
-{
-  type: 'navigation',
-  page: 'assets' | 'store' | 'login' | 'register'
-}
-```
-
-**处理位置:** Index.js 第 158-193 行
-
-### 5. `open-export-view` - 打开导出弹出框
-**消息格式:**
-```javascript
-{
-  type: 'open-export-view',
-  folderName: '文件夹名称'
-}
-```
-
-**处理位置:** Index.js 第 236-251 行
-
-### 6. `close-login-view` - 关闭登录窗口
-**消息格式:**
-```javascript
-{
-  type: 'close-login-view'
-}
-```
-
-**处理位置:** Index.js 第 252-257 行
-
-## Assets.js 的消息转发
-
-Assets.js 作为中间层,负责转发以下消息类型:
-- `global-alert`
-- `global-loading`
-- `global-confirm`
-- `global-confirm-response` (反向转发)
-
-**转发逻辑:** Assets.js 第 17-50 行
-
-## 已统一的消息传递方式
-
-所有子 view 现在都使用 `postMessage` 发送消息,而不是直接访问 `window.parent.GlobalAlert`:
-
-✅ **已统一:**
-- login.js - 使用 postMessage
-- disk.js - 使用 postMessage(已修复消息格式)
-- card.js (seq-ani-player) - 使用 postMessage
-- sprite-sheet-maker.js - 已改为使用 postMessage
-- export-view.js - 已改为使用 postMessage
-- navigation.js - 使用 postMessage
-
-## 注意事项
-
-1. **消息格式必须一致:** 所有 `global-alert` 消息必须使用 `text` 字段,而不是 `message`
-2. **消息转发:** 嵌套在 assets.html 中的页面,消息会自动通过 assets.js 转发
-3. **安全性:** 所有消息都检查 `event.origin` 确保同源
-4. **降级处理:** 如果 postMessage 失败,会降级到 console.log 或 alert
-
-## 调试
-
-在浏览器控制台中可以看到消息传递的日志:
-- `[Index] 收到global-alert消息:` - Index.js 收到消息
-- `[Assets] 收到子页面消息:` - Assets.js 收到消息并转发
-- `[Login] 通过 postMessage 发送 alert 消息给父窗口` - 登录页面发送消息
-

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

@@ -750,3 +750,116 @@ html, body {
     }
 }
 
+/* 下载确认对话框 */
+.download-confirm-overlay {
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    width: 100%;
+    height: 100%;
+    background: rgba(0, 0, 0, 0.7);
+    display: none; /* 默认隐藏 */
+    align-items: center;
+    justify-content: center;
+    z-index: 10000000;
+    backdrop-filter: blur(4px);
+    animation: overlayFadeIn 0.3s ease-out;
+}
+
+.download-confirm-modal {
+    background: white;
+    border-radius: 16px;
+    padding: 0;
+    max-width: 600px;
+    width: 90%;
+    max-height: 90vh;
+    overflow: hidden;
+    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+    animation: modalSlideIn 0.3s ease-out;
+}
+
+.download-confirm-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 24px;
+    border-bottom: 1px solid #e5e7eb;
+}
+
+.download-confirm-header h3 {
+    margin: 0;
+    font-size: 20px;
+    font-weight: 600;
+    color: #111827;
+}
+
+.download-confirm-close {
+    width: 36px;
+    height: 36px;
+    border-radius: 50%;
+    background: #f3f4f6;
+    border: none;
+    color: #6b7280;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    transition: all 0.2s;
+    padding: 0;
+}
+
+.download-confirm-close:hover {
+    background: #e5e7eb;
+    color: #374151;
+}
+
+.download-confirm-content {
+    padding: 24px;
+    display: flex;
+    flex-direction: column;
+    gap: 16px;
+}
+
+.download-option {
+    display: flex;
+    align-items: center;
+    gap: 16px;
+    padding: 20px;
+    border: 2px solid #e5e7eb;
+    border-radius: 12px;
+    cursor: pointer;
+    transition: all 0.2s;
+    background: #f9fafb;
+}
+
+.download-option:hover {
+    border-color: #667eea;
+    background: #f0f4ff;
+    transform: translateY(-2px);
+    box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
+}
+
+.download-option-icon {
+    font-size: 32px;
+    flex-shrink: 0;
+}
+
+.download-option-info {
+    flex: 1;
+}
+
+.download-option-title {
+    font-size: 16px;
+    font-weight: 600;
+    color: #111827;
+    margin-bottom: 4px;
+}
+
+.download-option-desc {
+    font-size: 14px;
+    color: #6b7280;
+    line-height: 1.5;
+}
+

+ 17 - 1
client/index.html

@@ -8,6 +8,7 @@
   <link rel="stylesheet" href="css/index.css" />
   <link rel="stylesheet" href="css/alert-view.css" />
   <link rel="stylesheet" href="css/confirm-view.css" />
+  <link rel="stylesheet" href="css/hint-view.css" />
 </head>
 <body>
   <div class="app-root">
@@ -16,7 +17,7 @@
 
     <!-- 子页面容器 -->
     <section class="page-container">
-      <iframe id="pageFrame" src="page/assets/assets.html" frameborder="0"></iframe>
+      <iframe id="pageFrame" src="page/store/store.html" frameborder="0"></iframe>
     </section>
   </div>
 
@@ -33,14 +34,29 @@
     <div class="alert-message" id="alertMessage"></div>
   </div>
 
+  <!-- 全局提示遮罩层(用于服务器断连重连) -->
+  <div class="alert-overlay" id="alertOverlay">
+    <div class="alert-content">
+      <div class="alert-spinner"></div>
+      <div class="alert-message" id="alertOverlayMessage">服务器断开连接,正在重连...</div>
+    </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>
 
   <!-- 登录/注册弹出框 iframe -->
   <iframe id="loginViewFrame" src="page/login/login.html" frameborder="0" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 1000001; border: none; background: transparent;"></iframe>
 
+  <!-- 支付界面 iframe -->
+  <iframe id="payViewFrame" src="page/pay-view/pay-view.html" frameborder="0" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 1000003; border: none; background: transparent;"></iframe>
+
+  <!-- 充值界面 iframe -->
+  <iframe id="rechargeViewFrame" src="page/recharge-view/recharge-view.html" frameborder="0" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 1000004; border: none; background: transparent;"></iframe>
+
   <script src="js/alert-view.js"></script>
   <script src="js/export-view-manager.js"></script>
+  <script src="js/hint-view.js"></script>
   <script src="js/index.js"></script>
   <script src="js/confirm-view.js"></script>
 </body>

+ 244 - 7
client/js/Index.js

@@ -119,7 +119,7 @@ window.GlobalAlert = (function() {
 // 页面管理:负责切换 iframe 中的子页面,并保持与导航栏状态同步
 (function () {
   console.log('[Index] index.js 已加载');
-  const DEFAULT_PAGE = "assets";
+  const DEFAULT_PAGE = "store";
 
   function getPageFrame() {
     return document.getElementById("pageFrame");
@@ -130,13 +130,19 @@ window.GlobalAlert = (function() {
   }
 
   function switchPage(page) {
-    const frame = getPageFrame();
-    if (!frame) {
+    // 只处理实际的页面切换,不处理login/register
+    if (page === "login" || page === "register") {
       return;
     }
 
-    // 只处理实际的页面切换,不处理login/register
-    if (page === "login" || page === "register") {
+    // profile页面是独立页面,直接跳转
+    if (page === "profile") {
+      window.location.href = "page/profile/profile.html";
+      return;
+    }
+
+    const frame = getPageFrame();
+    if (!frame) {
       return;
     }
 
@@ -145,9 +151,11 @@ window.GlobalAlert = (function() {
         frame.src = "page/store/store.html";
         break;
       case "assets":
-      default:
         frame.src = "page/assets/assets.html";
         break;
+      default:
+        frame.src = "page/store/store.html";
+        break;
     }
 
     // 同步导航栏状态
@@ -281,8 +289,132 @@ window.GlobalAlert = (function() {
       if (loginFrame) {
         loginFrame.style.display = 'none';
       }
+    } else if (data && data.type === "open-pay-view") {
+      // 处理打开支付界面
+      const payFrame = document.getElementById('payViewFrame');
+      if (payFrame) {
+        payFrame.style.display = 'block';
+        payFrame.style.pointerEvents = 'auto';
+        payFrame.style.visibility = 'visible';
+        
+        const sendPayData = () => {
+          if (payFrame.contentWindow) {
+            payFrame.contentWindow.postMessage({
+              type: 'open-pay-view',
+              itemName: data.itemName,
+              price: data.price,
+              resourcePath: data.resourcePath,
+              categoryDir: data.categoryDir
+            }, '*');
+          } else {
+            setTimeout(sendPayData, 100);
+          }
+        };
+        
+        sendPayData();
+        
+        const handleLoad = () => {
+          setTimeout(() => {
+            sendPayData();
+          }, 50);
+        };
+        
+        if (payFrame.contentDocument && payFrame.contentDocument.readyState === 'complete') {
+          handleLoad();
+        } else {
+          payFrame.addEventListener('load', handleLoad, { once: true });
+        }
+      }
+    } else if (data && data.type === "close-pay-view") {
+      const payFrame = document.getElementById('payViewFrame');
+      if (payFrame) {
+        payFrame.style.display = 'none';
+        payFrame.style.pointerEvents = 'none';
+        // 确保 iframe 不会遮挡其他元素
+        payFrame.style.visibility = 'hidden';
+      }
+    } else if (data && data.type === "payment-success") {
+      // 支付成功,刷新商店页面(如果需要)
+      if (window.HintView) {
+        window.HintView.success(`购买成功!${data.itemName} 已添加到网盘`, 3000);
+      }
+    } else if (data && data.type === "open-recharge-view") {
+      // 处理打开充值界面
+      const rechargeFrame = document.getElementById('rechargeViewFrame');
+      if (rechargeFrame) {
+        rechargeFrame.style.display = 'block';
+        rechargeFrame.style.pointerEvents = 'auto';
+        rechargeFrame.style.visibility = 'visible';
+        
+        const sendRechargeData = () => {
+          if (rechargeFrame.contentWindow) {
+            rechargeFrame.contentWindow.postMessage({
+              type: 'open-recharge-view',
+              needPoints: data.needPoints,
+              currentPoints: data.currentPoints
+            }, '*');
+          } else {
+            setTimeout(sendRechargeData, 100);
+          }
+        };
+        
+        sendRechargeData();
+        
+        const handleLoad = () => {
+          setTimeout(() => {
+            sendRechargeData();
+          }, 50);
+        };
+        
+        if (rechargeFrame.contentDocument && rechargeFrame.contentDocument.readyState === 'complete') {
+          handleLoad();
+        } else {
+          rechargeFrame.addEventListener('load', handleLoad, { once: true });
+        }
+      }
+    } else if (data && data.type === "close-recharge-view") {
+      const rechargeFrame = document.getElementById('rechargeViewFrame');
+      if (rechargeFrame) {
+        rechargeFrame.style.display = 'none';
+        rechargeFrame.style.pointerEvents = 'none';
+        rechargeFrame.style.visibility = 'hidden';
+      }
+    } else if (data && data.type === "recharge-success") {
+      // 充值成功,刷新商店页面(如果需要)
+      if (window.HintView) {
+        window.HintView.success(`充值成功!获得 ${data.points} Ani币`, 3000);
+      }
+    } else if (data && data.type === "avatar-updated") {
+      // 头像更新,通知导航栏更新
+      const navigationFrame = document.getElementById('navigationFrame');
+      if (navigationFrame && navigationFrame.contentWindow) {
+        // 从localStorage获取用户信息并更新头像
+        try {
+          const loginDataStr = localStorage.getItem('loginData');
+          if (loginDataStr) {
+            const loginData = JSON.parse(loginDataStr);
+            if (loginData.user) {
+              loginData.user.avatar = data.avatar;
+              localStorage.setItem('loginData', JSON.stringify(loginData));
+              
+              // 通知导航栏更新
+              navigationFrame.contentWindow.postMessage({
+                type: 'login-success',
+                user: loginData.user
+              }, '*');
+            }
+          }
+        } catch (error) {
+          console.error('[Index] 更新头像信息失败:', error);
+        }
+      }
     } else if (data && data.type === "login-success" && data.user) {
-      // 处理登录成功消息,转发给 navigation iframe
+      // 显示登录成功提示(在主窗口)
+      if (window.HintView) {
+        window.HintView.success('登录成功', 2000);
+      }
+      
+      // 处理登录成功消息,转发给 navigation iframe 和 pageFrame
       const navigationFrame = getNavigationFrame();
       if (navigationFrame && navigationFrame.contentWindow) {
         navigationFrame.contentWindow.postMessage({
@@ -290,10 +422,115 @@ window.GlobalAlert = (function() {
           user: data.user
         }, '*');
       }
+      
+      // 也转发给 pageFrame(store 页面)
+      const pageFrame = getPageFrame();
+      if (pageFrame && pageFrame.contentWindow) {
+        pageFrame.contentWindow.postMessage({
+          type: 'login-success',
+          user: data.user
+        }, '*');
+      }
+      
+      // 转发给 assets iframe(如果存在),它会再转发给 disk iframe
+      const assetsFrame = document.getElementById('assetsFrame');
+      if (assetsFrame && assetsFrame.contentWindow) {
+        assetsFrame.contentWindow.postMessage({
+          type: 'login-success',
+          user: data.user
+        }, '*');
+      }
+    } else if (data && data.type === "logout") {
+      // 清除 localStorage 中的登录信息
+      try {
+        localStorage.removeItem('loginData');
+        console.log('[Index] 登出,已清除登录信息');
+      } catch (error) {
+        console.error('[Index] 清除登录信息失败:', error);
+      }
+      
+      // 处理登出消息,转发给所有 iframe
+      const navigationFrame = getNavigationFrame();
+      if (navigationFrame && navigationFrame.contentWindow) {
+        navigationFrame.contentWindow.postMessage({
+          type: 'logout'
+        }, '*');
+      }
+      
+      const pageFrame = getPageFrame();
+      if (pageFrame && pageFrame.contentWindow) {
+        pageFrame.contentWindow.postMessage({
+          type: 'logout'
+        }, '*');
+      }
+      
+      const assetsFrame = document.getElementById('assetsFrame');
+      if (assetsFrame && assetsFrame.contentWindow) {
+        assetsFrame.contentWindow.postMessage({
+          type: 'logout'
+        }, '*');
+      }
     }
   });
 
+  // 检查并恢复登录状态
+  function checkAndRestoreLogin() {
+    try {
+      const loginDataStr = localStorage.getItem('loginData');
+      if (!loginDataStr) {
+        return;
+      }
+      
+      const loginData = JSON.parse(loginDataStr);
+      const now = Date.now();
+      
+      // 检查是否过期(2小时)
+      if (now >= loginData.expireTime) {
+        // 已过期,清除登录信息
+        localStorage.removeItem('loginData');
+        console.log('[Index] 登录信息已过期(2小时),已清除');
+        return;
+      }
+      
+      // 未过期,恢复登录状态
+      if (loginData.user) {
+        console.log('[Index] 恢复登录状态,用户:', loginData.user.username);
+        
+        // 通知所有 iframe 登录成功
+        const navigationFrame = getNavigationFrame();
+        if (navigationFrame && navigationFrame.contentWindow) {
+          navigationFrame.contentWindow.postMessage({
+            type: 'login-success',
+            user: loginData.user
+          }, '*');
+        }
+        
+        const pageFrame = getPageFrame();
+        if (pageFrame && pageFrame.contentWindow) {
+          pageFrame.contentWindow.postMessage({
+            type: 'login-success',
+            user: loginData.user
+          }, '*');
+        }
+        
+        const assetsFrame = document.getElementById('assetsFrame');
+        if (assetsFrame && assetsFrame.contentWindow) {
+          assetsFrame.contentWindow.postMessage({
+            type: 'login-success',
+            user: loginData.user
+          }, '*');
+        }
+      }
+    } catch (error) {
+      console.error('[Index] 恢复登录状态失败:', error);
+      localStorage.removeItem('loginData');
+    }
+  }
+
   window.addEventListener("DOMContentLoaded", () => {
+    // 先检查并恢复登录状态
+    checkAndRestoreLogin();
+    
     switchPage(DEFAULT_PAGE);
     syncNavigationState(DEFAULT_PAGE);
   });

+ 20 - 17
client/js/alert-view.js

@@ -16,36 +16,39 @@
         }
 
         async init() {
-            await this.ensureElements();
+            // 直接使用 DOM 中的元素,不再需要 fetch 加载
             this.overlay = document.getElementById('alertOverlay');
-            this.message = document.getElementById('alertMessage');
+            this.message = document.getElementById('alertOverlayMessage');
+            
+            // 如果元素不存在,等待 DOM 加载完成
+            if (!this.overlay || !this.message) {
+                await this.waitForElements();
+            }
+            
             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 组件失败');
+        async waitForElements() {
+            // 等待元素出现(最多等待 1 秒)
+            const maxWait = 1000;
+            const startTime = Date.now();
+            
+            while (!this.overlay || !this.message) {
+                if (Date.now() - startTime > maxWait) {
+                    console.error('Alert Overlay 元素未找到');
+                    return;
                 }
-                const html = await response.text();
-                document.body.insertAdjacentHTML('beforeend', html);
-            } catch (error) {
-                console.error('加载 Alert Overlay 组件失败:', error);
+                await new Promise(resolve => setTimeout(resolve, 50));
+                this.overlay = document.getElementById('alertOverlay');
+                this.message = document.getElementById('alertOverlayMessage');
             }
         }
 
         show(msg) {
             if (this.overlay) {
-                console.log('5555555555')
                 if (msg && this.message) {
                     this.message.textContent = msg;
                 }
-                console.log('666666')
                 this.overlay.classList.add('show');
             }
         }

+ 16 - 0
client/js/assets.js

@@ -13,6 +13,22 @@
     // E.g., preview selected files from disk on the right player
   });
 
+  // 监听来自父窗口的登录成功/登出消息,转发给 disk iframe
+  window.addEventListener('message', function(event) {
+    // 只处理来自父窗口的消息
+    if (event.source !== window.parent) {
+      return;
+    }
+    
+    const data = event.data;
+    if (data && (data.type === 'login-success' || data.type === 'logout')) {
+      // 转发给 disk iframe
+      if (diskFrame && diskFrame.contentWindow) {
+        diskFrame.contentWindow.postMessage(data, '*');
+      }
+    }
+  });
+
   // 注意:不再需要转发 global-alert、global-loading、global-confirm 消息
   // 各个 view 现在直接调用父窗口的 GlobalAlert/GlobalLoading/GlobalConfirm
   // 如果需要处理其他类型的消息,可以在这里添加

+ 255 - 13
client/js/disk/disk.js

@@ -15,8 +15,117 @@ class DiskManager {
         this.fileStructureCache = new Map(); // key: 文件路径, value: 文件信息(包括pngCount)
         this.cacheInitialized = false;
         
+        // 当前登录用户名
+        this.currentUsername = null;
+        
         this.init();
     }
+    
+    // 获取当前登录用户名
+    getCurrentUsername() {
+        // 如果已经缓存了用户名,直接返回
+        if (this.currentUsername) {
+            return this.currentUsername;
+        }
+        
+        // 从导航栏 iframe 中获取用户名
+        try {
+            // 尝试从父窗口的 navigationFrame 获取
+            let targetWindow = window.parent;
+            while (targetWindow && targetWindow !== window) {
+                try {
+                    const navigationFrame = targetWindow.document.getElementById('navigationFrame');
+                    if (navigationFrame && navigationFrame.contentWindow) {
+                        const navWindow = navigationFrame.contentWindow;
+                        const navDoc = navigationFrame.contentDocument || navWindow.document;
+                        
+                        // 检查用户是否已登录
+                        const userAvatarContainer = navDoc.getElementById('userAvatarContainer');
+                        if (userAvatarContainer) {
+                            const computedStyle = navDoc.defaultView.getComputedStyle(userAvatarContainer);
+                            if (computedStyle.display !== 'none') {
+                                // 用户已登录,从 userAvatar 的 alt 属性获取用户名
+                                const userAvatar = navDoc.getElementById('userAvatar');
+                                if (userAvatar && userAvatar.alt && userAvatar.alt !== '用户头像') {
+                                    const username = userAvatar.alt;
+                                    // 缓存用户名
+                                    this.currentUsername = username;
+                                    console.log('[DiskManager] 从导航栏获取用户名:', username);
+                                    return username;
+                                }
+                            }
+                        }
+                    }
+                } catch (e) {
+                    // 跨域或访问限制,继续尝试上层窗口
+                    console.warn('[DiskManager] 访问导航栏失败:', e);
+                }
+                
+                // 尝试更上层的窗口
+                if (targetWindow.parent && targetWindow.parent !== targetWindow) {
+                    targetWindow = targetWindow.parent;
+                } else {
+                    break;
+                }
+            }
+        } catch (error) {
+            console.warn('[DiskManager] 无法获取用户名:', error);
+        }
+        
+        // 如果仍然没有获取到,返回 null
+        return this.currentUsername;
+    }
+    
+    // 设置当前用户名(从外部调用,例如登录成功后)
+    setCurrentUsername(username) {
+        this.currentUsername = username;
+    }
+    
+    // 从 localStorage 恢复登录状态
+    restoreLoginFromStorage() {
+        try {
+            const loginDataStr = localStorage.getItem('loginData');
+            if (!loginDataStr) {
+                return;
+            }
+            
+            const loginData = JSON.parse(loginDataStr);
+            const now = Date.now();
+            
+            // 检查是否过期
+            if (now >= loginData.expireTime) {
+                localStorage.removeItem('loginData');
+                return;
+            }
+            
+            // 未过期,恢复登录状态
+            if (loginData.user && loginData.user.username) {
+                this.setCurrentUsername(loginData.user.username);
+                console.log('[DiskManager] 从 localStorage 恢复登录状态:', loginData.user.username);
+            }
+        } catch (error) {
+            console.error('[DiskManager] 恢复登录状态失败:', error);
+        }
+    }
+    
+    // 监听登录成功消息
+    initUserListener() {
+        window.addEventListener('message', (event) => {
+            if (event.data && event.data.type === 'login-success' && event.data.user) {
+                const username = event.data.user.username;
+                this.setCurrentUsername(username);
+                console.log('[DiskManager] 收到登录成功消息,设置用户名:', username);
+                // 登录成功后重新加载文件列表
+                this.loadFiles();
+            } else if (event.data && event.data.type === 'logout') {
+                this.setCurrentUsername(null);
+                console.log('[DiskManager] 收到登出消息,清除用户名');
+                // 登出后清空文件列表
+                this.files = [];
+                this.renderFiles();
+            }
+        });
+    }
 
     async init() {
         await this.ensureToolBar();
@@ -28,8 +137,20 @@ class DiskManager {
         this.initSelection();
         this.initShortcutKeys();
         this.initSearchBar();
+        this.initUserListener();
         this.bindEvents();
+        
+        // 延迟一下,确保导航栏已加载,然后尝试获取用户名
+        setTimeout(() => {
+            // 先尝试从 localStorage 恢复登录状态
+            this.restoreLoginFromStorage();
+            
+            const username = this.getCurrentUsername();
+            if (username) {
+                console.log('[DiskManager] 初始化时检测到已登录用户:', username);
+            }
         this.loadFiles();
+        }, 500);
         
         // 后台初始化文件结构缓存(不阻塞页面加载)
         this.initFileStructureCache();
@@ -336,11 +457,13 @@ class DiskManager {
                     }
                 }
                 
-                // 降级到原生confirm
-                resolve(window.confirm(message));
+                // 如果找不到 GlobalConfirm,直接返回 false(不降级到原生 confirm)
+                console.error('[Disk] 无法找到 GlobalConfirm,操作已取消');
+                resolve(false);
             } catch (error) {
                 console.error('[Disk] 显示 confirm 失败:', error);
-                resolve(window.confirm(message));
+                // 不降级到原生 confirm,直接返回 false
+                resolve(false);
             }
         });
     }
@@ -467,12 +590,19 @@ class DiskManager {
         }
 
         try {
+            const username = this.getCurrentUsername();
+            if (!username) {
+                this.showGlobalAlert('请先登录');
+                return;
+            }
+            
             const response = await fetch('/api/disk/delete', {
                 method: 'POST',
                 headers: {
                     'Content-Type': 'application/json'
                 },
                 body: JSON.stringify({
+                    username: username,
                     paths: this.selection.getSelectedItems()
                 })
             });
@@ -631,13 +761,19 @@ class DiskManager {
             console.log('[Disk]   方法: POST');
             console.log('[Disk]   数据:', { paths: selectedPaths });
             
+            const username = this.getCurrentUsername();
+            if (!username) {
+                this.showGlobalAlert('请先登录');
+                return;
+            }
+            
             // 使用fetch读取SSE流
             const response = await fetch('/api/disk/remove-background', {
                 method: 'POST',
                 headers: {
                     'Content-Type': 'application/json'
                 },
-                body: JSON.stringify({ paths: selectedPaths })
+                body: JSON.stringify({ username: username, paths: selectedPaths })
             });
 
             console.log('[Disk] ✓ HTTP响应收到');
@@ -776,13 +912,19 @@ class DiskManager {
         this.currentProcessTotal = totalPngCount;
 
         try {
+            const username = this.getCurrentUsername();
+            if (!username) {
+                this.showGlobalAlert('请先登录');
+                return;
+            }
+            
             // 使用fetch读取SSE流
             const response = await fetch('/api/disk/crop-mini', {
                 method: 'POST',
                 headers: {
                     'Content-Type': 'application/json'
                 },
-                body: JSON.stringify({ paths: selectedPaths })
+                body: JSON.stringify({ username: username, paths: selectedPaths })
             });
 
             if (!response.ok) {
@@ -944,6 +1086,17 @@ class DiskManager {
         }
 
         if (filesToUpload.length > 0) {
+            // 显示确认对话框
+            const folderName = entries.length === 1 && entries[0].isDirectory 
+                ? entries[0].name 
+                : '选中的文件';
+            const confirmMsg = `将 ${filesToUpload.length} 个文件上传到此网站?\n此操作会上传"${folderName}"下的所有文件。请仅在您信任该网站的情况下执行此操作。`;
+            
+            const confirmed = await this.showGlobalConfirm(confirmMsg);
+            if (!confirmed) {
+                return;
+            }
+            
             await this.uploadFiles(filesToUpload);
         }
     }
@@ -977,7 +1130,7 @@ class DiskManager {
         }
     }
 
-    handleFileSelect(e) {
+    async handleFileSelect(e) {
         const currentPath = this.pathNav.getPath();
         const isRootDir = !currentPath || currentPath === '';
         
@@ -988,6 +1141,14 @@ class DiskManager {
             return;
         }
         
+        // 检查登录状态
+        const username = this.getCurrentUsername();
+        if (!username) {
+            this.showGlobalAlert('请先登录');
+            this.fileInput.value = '';
+            return;
+        }
+        
         // 只接受图片格式的文件,其他格式直接跳过
         const filesToUpload = files
             .filter(file => this.isImageFile(file.name))
@@ -997,7 +1158,19 @@ class DiskManager {
             }));
         
         if (filesToUpload.length > 0) {
-            this.uploadFiles(filesToUpload);
+            // 显示确认对话框
+            const folderName = files[0].webkitRelativePath 
+                ? files[0].webkitRelativePath.split('/')[0] 
+                : '选中的文件';
+            const confirmMsg = `将 ${filesToUpload.length} 个文件上传到此网站?\n此操作会上传"${folderName}"下的所有文件。请仅在您信任该网站的情况下执行此操作。`;
+            
+            const confirmed = await this.showGlobalConfirm(confirmMsg);
+            if (!confirmed) {
+                this.fileInput.value = '';
+                return;
+            }
+            
+            await this.uploadFiles(filesToUpload);
         }
         this.fileInput.value = '';
     }
@@ -1035,10 +1208,16 @@ class DiskManager {
     }
 
     async uploadFile(fileData, progressItem) {
+        const username = this.getCurrentUsername();
+        if (!username) {
+            throw new Error('请先登录');
+        }
+        
         const formData = new FormData();
         formData.append('file', fileData.file);
         formData.append('path', this.pathNav.getPath());
         formData.append('relativePath', fileData.path);
+        formData.append('username', username);
 
         const response = await fetch('/api/disk/upload', {
             method: 'POST',
@@ -1099,7 +1278,12 @@ class DiskManager {
         
         try {
             // console.log('[Disk] 开始初始化文件结构缓存...');
-            const response = await fetch('/api/disk/list?path=&recursive=true');
+            const username = this.getCurrentUsername();
+            if (!username) {
+                console.warn('[Disk] 未登录,无法初始化缓存');
+                return;
+            }
+            const response = await fetch(`/api/disk/list?username=${encodeURIComponent(username)}&path=&recursive=true`);
             
             if (!response.ok) {
                 await this.handleServerError(response, '加载文件列表失败');
@@ -1173,7 +1357,13 @@ class DiskManager {
         this.clearSearch();
 
         try {
-            const response = await fetch(`/api/disk/list?path=${encodeURIComponent(this.pathNav.getPath())}`);
+            const username = this.getCurrentUsername();
+            if (!username) {
+                this.showGlobalAlert('请先登录');
+                this.showLoading(false);
+                return;
+            }
+            const response = await fetch(`/api/disk/list?username=${encodeURIComponent(username)}&path=${encodeURIComponent(this.pathNav.getPath())}`);
             
             if (!response.ok) {
                 await this.handleServerError(response, '加载文件列表失败');
@@ -1204,7 +1394,11 @@ class DiskManager {
     // 静默加载文件列表(用于上传过程中增量更新,不显示加载动画,不闪烁)
     async loadFilesQuietly() {
         try {
-            const response = await fetch(`/api/disk/list?path=${encodeURIComponent(this.pathNav.getPath())}`);
+            const username = this.getCurrentUsername();
+            if (!username) {
+                return;
+            }
+            const response = await fetch(`/api/disk/list?username=${encodeURIComponent(username)}&path=${encodeURIComponent(this.pathNav.getPath())}`);
             
             if (!response.ok) {
                 // 静默加载失败时不显示错误,避免干扰用户
@@ -1302,8 +1496,16 @@ class DiskManager {
             // 使用服务器提供的预览信息(完全避免404错误)
             if (file.hasPreview && file.previewUrl) {
                 // 服务器确认有预览图,直接使用服务器提供的URL
+                // 确保 URL 包含用户名参数(如果服务器没有提供)
+                const username = this.getCurrentUsername();
+                let previewUrl = file.previewUrl;
+                if (username && !previewUrl.includes('username=')) {
+                    // 如果 URL 中没有用户名参数,添加它
+                    const separator = previewUrl.includes('?') ? '&' : '?';
+                    previewUrl = previewUrl + separator + 'username=' + encodeURIComponent(username);
+                }
                 // 添加时间戳防止缓存
-                const previewUrl = file.previewUrl + '&t=' + Date.now();
+                previewUrl = previewUrl + (previewUrl.includes('?') ? '&' : '?') + 't=' + Date.now();
                 // console.log(`[Disk] 文件夹 ${file.name} 有预览图`);
                 div.innerHTML = `
                     ${checkMark}
@@ -1329,7 +1531,10 @@ class DiskManager {
             }
         } else if (isImage && file.type !== 'directory') {
             // 添加时间戳防止缓存
-            const previewUrl = `/api/disk/preview?path=${encodeURIComponent(file.path)}&t=${Date.now()}`;
+            const username = this.getCurrentUsername();
+            const previewUrl = username 
+                ? `/api/disk/preview?username=${encodeURIComponent(username)}&path=${encodeURIComponent(file.path)}&t=${Date.now()}`
+                : `/api/disk/preview?path=${encodeURIComponent(file.path)}&t=${Date.now()}`;
             div.innerHTML = `
                 ${checkMark}
                 <div class="file-thumbnail">
@@ -1473,6 +1678,14 @@ class DiskManager {
     // 移动文件/文件夹
     async moveFile(sourcePath, targetFolder, options = {}) {
         const { suppressReload = false, silent = false } = options;
+        const username = this.getCurrentUsername();
+        if (!username) {
+            if (!silent) {
+                this.showGlobalAlert('请先登录');
+            }
+            return;
+        }
+        
         try {
             const response = await fetch('/api/disk/move', {
                 method: 'POST',
@@ -1480,6 +1693,7 @@ class DiskManager {
                     'Content-Type': 'application/json'
                 },
                 body: JSON.stringify({
+                    username: username,
                     sourcePath,
                     targetFolder
                 })
@@ -1624,6 +1838,12 @@ class DiskManager {
             counter++;
         }
 
+        const username = this.getCurrentUsername();
+        if (!username) {
+            this.showGlobalAlert('请先登录');
+            return;
+        }
+
         try {
             const response = await fetch('/api/disk/create-folder', {
                 method: 'POST',
@@ -1631,6 +1851,7 @@ class DiskManager {
                     'Content-Type': 'application/json'
                 },
                 body: JSON.stringify({
+                    username: username,
                     path: this.pathNav.getPath(),
                     name: folderName
                 })
@@ -1743,6 +1964,12 @@ class DiskManager {
 
     // 执行重命名
     async renameFile(oldPath, newName) {
+        const username = this.getCurrentUsername();
+        if (!username) {
+            this.showGlobalAlert('请先登录');
+            return;
+        }
+        
         try {
             const response = await fetch('/api/disk/rename', {
                 method: 'POST',
@@ -1750,6 +1977,7 @@ class DiskManager {
                     'Content-Type': 'application/json'
                 },
                 body: JSON.stringify({
+                    username: username,
                     oldPath: oldPath,
                     newName: newName
                 })
@@ -1771,11 +1999,24 @@ class DiskManager {
 
     // 下载文件
     downloadFile(filePath) {
-        window.open(`/api/disk/download?path=${encodeURIComponent(filePath)}`, '_blank');
+        const username = this.getCurrentUsername();
+        if (!username) {
+            this.showGlobalAlert('请先登录');
+            return;
+        }
+        window.open(`/api/disk/download?username=${encodeURIComponent(username)}&path=${encodeURIComponent(filePath)}`, '_blank');
     }
 
     async copyFile(sourcePath, targetFolder, options = {}) {
         const { suppressReload = false, silent = false } = options;
+        const username = this.getCurrentUsername();
+        if (!username) {
+            if (!silent) {
+                this.showGlobalAlert('请先登录');
+            }
+            return;
+        }
+        
         try {
             const response = await fetch('/api/disk/copy', {
                 method: 'POST',
@@ -1783,6 +2024,7 @@ class DiskManager {
                     'Content-Type': 'application/json'
                 },
                 body: JSON.stringify({
+                    username: username,
                     sourcePath,
                     targetFolder
                 })

+ 317 - 39
client/js/export-view/export-view.js

@@ -23,8 +23,14 @@ class ExportView {
         this.folderName = null;
         this.spritesheetLayout = null; // 保存布局信息用于生成 JSON
         this.replacedImageData = null; // 保存 Gemini 返回的替换后的图片(base64)
+        this.geminiOriginalImageData = null; // 保存 Gemini 返回的原始图片(未抠图)
         this.originalSpritesheetData = null; // 保存原始 spritesheet 的 base64 数据
         
+        // 下载确认对话框相关
+        this.downloadConfirmOverlay = null;
+        this.downloadConfirmClose = null;
+        this.downloadOptions = null;
+        
         this.init();
     }
 
@@ -44,6 +50,16 @@ class ExportView {
         this.cancelBtn = document.getElementById('exportCancelBtn');
         this.confirmBtn = document.getElementById('exportConfirmBtn');
         
+        // 下载确认对话框元素
+        this.downloadConfirmOverlay = document.getElementById('downloadConfirmOverlay');
+        this.downloadConfirmClose = document.getElementById('downloadConfirmClose');
+        this.downloadOptions = document.querySelectorAll('.download-option');
+        
+        // 确保对话框初始状态是隐藏的
+        if (this.downloadConfirmOverlay) {
+            this.downloadConfirmOverlay.style.display = 'none';
+        }
+        
         this.bindEvents();
         
         // 初始时禁用确定按钮
@@ -90,10 +106,34 @@ class ExportView {
 
         // ESC键关闭
         document.addEventListener('keydown', (e) => {
-            if (e.key === 'Escape' && this.overlay) {
-                this.close();
+            if (e.key === 'Escape') {
+                if (this.downloadConfirmOverlay && this.downloadConfirmOverlay.style.display !== 'none') {
+                    this.hideDownloadConfirm();
+                } else if (this.overlay) {
+                    this.close();
+                }
+            }
+        });
+        
+        // 下载确认对话框事件
+        this.downloadConfirmClose?.addEventListener('click', () => {
+            this.hideDownloadConfirm();
+        });
+        
+        // 点击遮罩层关闭下载确认对话框
+        this.downloadConfirmOverlay?.addEventListener('click', (e) => {
+            if (e.target === this.downloadConfirmOverlay) {
+                this.hideDownloadConfirm();
             }
         });
+        
+        // 下载选项点击事件
+        this.downloadOptions?.forEach(option => {
+            option.addEventListener('click', () => {
+                const downloadType = option.dataset.option;
+                this.handleDownloadOption(downloadType);
+            });
+        });
 
         // 参考图上传区域点击
         this.referenceBox?.addEventListener('click', () => {
@@ -182,9 +222,16 @@ class ExportView {
         try {
             const TEXTURE_ROOT = "http://localhost:3000/disk_data";
             
+            // 获取当前登录用户名
+            const username = this.getCurrentUsername();
+            
             // 获取帧列表
             const encodedFolderName = encodeURIComponent(folderName);
-            const response = await fetch(`http://localhost:3000/api/frames/${encodedFolderName}`);
+            let apiUrl = `http://localhost:3000/api/frames/${encodedFolderName}`;
+            if (username) {
+                apiUrl += `?username=${encodeURIComponent(username)}`;
+            }
+            const response = await fetch(apiUrl);
             
             if (!response.ok) {
                 // 服务端返回错误,解析错误信息并显示
@@ -493,43 +540,15 @@ class ExportView {
             const result = await response.json();
             
             if (result.success && result.imageData) {
-                // Gemini 返回的图片 base64
+                // Gemini 返回的图片 base64(不进行抠图,直接保存原始图片)
                 const geminiImageBase64 = result.imageData;
                 
-                // 调用抠图 API 处理图片
-                if (this.previewPlaceholder) {
-                    const loadingText = this.previewPlaceholder.querySelector('.loading-text');
-                    if (loadingText) {
-                        loadingText.textContent = '正在抠图处理...';
-                    }
-                }
+                // 保存 Gemini 返回的原始图片(不抠图)
+                this.geminiOriginalImageData = `data:image/png;base64,${geminiImageBase64}`;
+                this.replacedImageData = this.geminiOriginalImageData; // 用于显示预览
                 
-                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 errorMessage = await this.handleServerError(mattingResponse, '抠图失败');
-                    throw new Error(errorMessage);
-                }
-
-                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('抠图处理失败');
-                }
+                // 显示原始图片(不抠图)
+                this.showPreview(this.geminiOriginalImageData);
             } else {
                 throw new Error('API 返回失败或未找到图片数据');
             }
@@ -715,6 +734,228 @@ class ExportView {
         URL.revokeObjectURL(url);
     }
 
+    /**
+     * 显示下载确认对话框
+     */
+    showDownloadConfirm() {
+        if (this.downloadConfirmOverlay) {
+            console.log('[ExportView] 显示下载确认对话框');
+            this.downloadConfirmOverlay.style.display = 'flex';
+        } else {
+            console.error('[ExportView] 下载确认对话框元素未找到');
+        }
+    }
+
+    /**
+     * 隐藏下载确认对话框
+     */
+    hideDownloadConfirm() {
+        if (this.downloadConfirmOverlay) {
+            this.downloadConfirmOverlay.style.display = 'none';
+        }
+    }
+
+    /**
+     * 处理下载选项选择
+     * @param {string} downloadType - 下载类型:'original', 'normal', 'vip'
+     */
+    async handleDownloadOption(downloadType) {
+        this.hideDownloadConfirm();
+        
+        try {
+            // 显示加载状态(但不隐藏预览图片)
+            // 使用全局 Loading 提示,不干扰预览图显示
+            if (window.parent && window.parent.postMessage) {
+                window.parent.postMessage({
+                    type: 'global-loading',
+                    action: 'show',
+                    text: downloadType === 'original' ? '正在准备下载...' : '正在处理图片...'
+                }, '*');
+            }
+            
+            let processedImageBase64;
+            
+            // 确定使用的图片源
+            if (this.geminiOriginalImageData) {
+                // 如果有 Gemini 图片,使用 Gemini 图片
+                const geminiImageBase64 = this.geminiOriginalImageData.replace(/^data:image\/\w+;base64,/, '');
+                
+                if (downloadType === 'original') {
+                    // 源文件下载:直接使用原始图片
+                    processedImageBase64 = geminiImageBase64;
+                } else if (downloadType === 'normal') {
+                    // 普通抠图:调用 rembg-matting.py
+                    const response = await fetch('http://localhost:3000/api/matting-normal', {
+                        method: 'POST',
+                        headers: {
+                            'Content-Type': 'application/json'
+                        },
+                        body: JSON.stringify({
+                            imageBase64: geminiImageBase64
+                        })
+                    });
+                    
+                    if (!response.ok) {
+                        const errorMessage = await this.handleServerError(response, '普通抠图失败');
+                        throw new Error(errorMessage);
+                    }
+                    
+                    const result = await response.json();
+                    if (!result.success || !result.imageData) {
+                        throw new Error('普通抠图处理失败');
+                    }
+                    
+                    processedImageBase64 = result.imageData;
+                } else if (downloadType === 'vip') {
+                    // VIP抠图:调用 BiRefNet
+                    const response = await fetch('http://localhost:3000/api/matting-vip', {
+                        method: 'POST',
+                        headers: {
+                            'Content-Type': 'application/json'
+                        },
+                        body: JSON.stringify({
+                            imageBase64: geminiImageBase64
+                        })
+                    });
+                    
+                    if (!response.ok) {
+                        const errorMessage = await this.handleServerError(response, 'VIP抠图失败');
+                        throw new Error(errorMessage);
+                    }
+                    
+                    const result = await response.json();
+                    if (!result.success || !result.imageData) {
+                        throw new Error('VIP抠图处理失败');
+                    }
+                    
+                    processedImageBase64 = result.imageData;
+                }
+            } else {
+                // 如果没有 Gemini 图片,使用原始 spritesheet
+                const imageBlob = await new Promise((resolve, reject) => {
+                    this.spritesheetCanvas.toBlob((blob) => {
+                        if (blob) {
+                            resolve(blob);
+                        } else {
+                            reject(new Error('Canvas 转换失败'));
+                        }
+                    }, 'image/png');
+                });
+                
+                // 将图片转换为 Base64
+                const originalImageBase64 = await this.blobToBase64(imageBlob);
+                
+                if (downloadType === 'original') {
+                    // 源文件下载:直接使用原始 spritesheet
+                    processedImageBase64 = originalImageBase64;
+                } else if (downloadType === 'normal') {
+                    // 普通抠图:对原始 spritesheet 进行抠图
+                    const response = await fetch('http://localhost:3000/api/matting-normal', {
+                        method: 'POST',
+                        headers: {
+                            'Content-Type': 'application/json'
+                        },
+                        body: JSON.stringify({
+                            imageBase64: originalImageBase64
+                        })
+                    });
+                    
+                    if (!response.ok) {
+                        const errorMessage = await this.handleServerError(response, '普通抠图失败');
+                        throw new Error(errorMessage);
+                    }
+                    
+                    const result = await response.json();
+                    if (!result.success || !result.imageData) {
+                        throw new Error('普通抠图处理失败');
+                    }
+                    
+                    processedImageBase64 = result.imageData;
+                } else if (downloadType === 'vip') {
+                    // VIP抠图:对原始 spritesheet 进行 VIP 抠图
+                    const response = await fetch('http://localhost:3000/api/matting-vip', {
+                        method: 'POST',
+                        headers: {
+                            'Content-Type': 'application/json'
+                        },
+                        body: JSON.stringify({
+                            imageBase64: originalImageBase64
+                        })
+                    });
+                    
+                    if (!response.ok) {
+                        const errorMessage = await this.handleServerError(response, 'VIP抠图失败');
+                        throw new Error(errorMessage);
+                    }
+                    
+                    const result = await response.json();
+                    if (!result.success || !result.imageData) {
+                        throw new Error('VIP抠图处理失败');
+                    }
+                    
+                    processedImageBase64 = result.imageData;
+                }
+            }
+            
+            // 生成 JSON 数据
+            const folderName = this.folderName.split('/').pop() || 'spritesheet';
+            const jsonData = this.generateJSON(
+                folderName,
+                this.spritesheetLayout.layout,
+                this.spritesheetLayout.sheetWidth,
+                this.spritesheetLayout.sheetHeight
+            );
+            
+            // 发送到服务器打包
+            const response = await fetch('http://localhost:3000/api/pack', {
+                method: 'POST',
+                headers: {
+                    'Content-Type': 'application/json'
+                },
+                body: JSON.stringify({
+                    folderName: folderName,
+                    imageData: processedImageBase64,
+                    jsonData: jsonData
+                })
+            });
+
+            if (!response.ok) {
+                const errorMessage = await this.handleServerError(response, '打包失败');
+                throw new Error(errorMessage);
+            }
+
+            // 获取 ZIP 文件的 Blob
+            const zipBlob = await response.blob();
+            
+            // 下载 ZIP 文件
+            this.downloadFile(zipBlob, `${folderName}.zip`, 'application/zip');
+            
+            // 隐藏全局 Loading 提示
+            if (window.parent && window.parent.postMessage) {
+                window.parent.postMessage({
+                    type: 'global-loading',
+                    action: 'hide'
+                }, '*');
+            }
+            
+            // 下载完成后,不关闭弹出框,不刷新图片
+            // 显示成功提示
+            this.showAlert('下载成功!');
+        } catch (error) {
+            // console.error('[ExportView] 下载失败:', error);
+            
+            // 隐藏全局 Loading 提示
+            if (window.parent && window.parent.postMessage) {
+                window.parent.postMessage({
+                    type: 'global-loading',
+                    action: 'hide'
+                }, '*');
+            }
+            
+            this.showAlert(`下载失败: ${error.message}`);
+        }
+    }
+
     /**
      * 处理下载按钮点击
      */
@@ -726,6 +967,15 @@ class ExportView {
             return;
         }
         
+        // 总是显示下载确认对话框,让用户选择下载方式
+        console.log('[ExportView] 显示下载选项对话框');
+        this.showDownloadConfirm();
+    }
+    
+    /**
+     * 下载 Spritesheet(原始逻辑)
+     */
+    async downloadSpritesheet() {
         try {
             // 生成 JSON 数据
             const folderName = this.folderName.split('/').pop() || 'spritesheet';
@@ -790,8 +1040,9 @@ class ExportView {
             // 下载 ZIP 文件
             this.downloadFile(zipBlob, `${folderName}.zip`, 'application/zip');
             
-            // 关闭弹出框
-            this.close();
+            // 下载完成后,不关闭弹出框,不刷新图片
+            // 显示成功提示
+            this.showAlert('下载成功!');
         } catch (error) {
             // console.error('[ExportView] 下载失败:', error);
             this.showAlert(`下载失败: ${error.message}`);
@@ -815,6 +1066,32 @@ class ExportView {
         }
     }
 
+    /**
+     * 获取当前登录用户名
+     */
+    getCurrentUsername() {
+        try {
+            const loginDataStr = localStorage.getItem('loginData');
+            if (!loginDataStr) {
+                return null;
+            }
+            
+            const loginData = JSON.parse(loginDataStr);
+            const now = Date.now();
+            
+            // 检查是否过期
+            if (now >= loginData.expireTime) {
+                localStorage.removeItem('loginData');
+                return null;
+            }
+            
+            return loginData.user ? loginData.user.username : null;
+        } catch (error) {
+            console.error('[ExportView] 获取用户名失败:', error);
+            return null;
+        }
+    }
+
     /**
      * 重置所有数据和UI状态
      */
@@ -826,6 +1103,7 @@ class ExportView {
         this.folderName = null;
         this.spritesheetLayout = null;
         this.replacedImageData = null;
+        this.geminiOriginalImageData = null;
         this.originalSpritesheetData = null;
 
         // 重置参考图区域

+ 89 - 12
client/js/login/login.js

@@ -35,6 +35,9 @@
   // 验证码倒计时
   let loginCodeCountdown = 0;
   let registerCodeCountdown = 0;
+  
+  // 默认头像URL(注册时使用)
+  let defaultAvatarUrl = null;
 
   // 初始化
   function init() {
@@ -113,6 +116,35 @@
     } else {
       loginForm.style.display = 'none';
       registerForm.style.display = 'block';
+      // 切换到注册标签时,加载默认头像
+      loadDefaultAvatar();
+    }
+  }
+  
+  // 加载默认头像
+  async function loadDefaultAvatar() {
+    try {
+      const response = await fetch('http://localhost:3000/api/avatars/default');
+      if (!response.ok) {
+        throw new Error('获取默认头像失败');
+      }
+      
+      const data = await response.json();
+      if (data.success && data.avatars && data.avatars.length > 0) {
+        // 随机选择一个默认头像
+        const randomIndex = Math.floor(Math.random() * data.avatars.length);
+        const avatarFilename = data.avatars[randomIndex];
+        defaultAvatarUrl = `http://localhost:3000/avatar/${encodeURIComponent(avatarFilename)}`;
+        
+        // 显示默认头像
+        if (avatarImage && !registerAvatar.files[0]) {
+          avatarImage.src = defaultAvatarUrl;
+          avatarImage.style.display = 'block';
+          avatarPreview.querySelector('.avatar-placeholder').style.display = 'none';
+        }
+      }
+    } catch (error) {
+      console.error('[Login] 加载默认头像失败:', error);
     }
   }
 
@@ -378,7 +410,26 @@
         }
 
         if (response.ok && result.success) {
-          showSuccess('登录成功');
+          // 保存登录信息到 localStorage(有效期2小时)
+          if (result.user) {
+            const loginData = {
+              user: result.user,
+              expireTime: Date.now() + 2 * 60 * 60 * 1000 // 2小时后过期
+            };
+            try {
+              localStorage.setItem('loginData', JSON.stringify(loginData));
+              console.log('[Login] 登录信息已保存到 localStorage,有效期2小时');
+            } catch (error) {
+              console.warn('[Login] 保存登录信息失败:', error);
+            }
+          }
+          
+          // 显示成功提示(在登录 iframe 中)
+          if (window.HintView) {
+            window.HintView.success('登录成功', 2000);
+          } else {
+            showSuccess('登录成功');
+          }
           // 通知父窗口登录成功,更新导航栏
           if (window.parent !== window && result.user) {
             window.parent.postMessage({
@@ -389,7 +440,7 @@
           // 延迟关闭
           setTimeout(() => {
             closeLogin();
-          }, 1000);
+          }, 2000);
         } else {
           // 处理各种错误状态,使用 alert-view 显示错误
           // 优先使用服务器返回的错误消息(区分账号不存在和密码错误)
@@ -475,7 +526,26 @@
         }
 
         if (response.ok && result.success) {
-          showSuccess('登录成功');
+          // 保存登录信息到 localStorage(有效期2小时)
+          if (result.user) {
+            const loginData = {
+              user: result.user,
+              expireTime: Date.now() + 2 * 60 * 60 * 1000 // 2小时后过期
+            };
+            try {
+              localStorage.setItem('loginData', JSON.stringify(loginData));
+              console.log('[Login] 登录信息已保存到 localStorage,有效期2小时');
+            } catch (error) {
+              console.warn('[Login] 保存登录信息失败:', error);
+            }
+          }
+          
+          // 显示成功提示(在登录 iframe 中)
+          if (window.HintView) {
+            window.HintView.success('登录成功', 2000);
+          } else {
+            showSuccess('登录成功');
+          }
           // 通知父窗口登录成功,更新导航栏
           if (window.parent !== window && result.user) {
             window.parent.postMessage({
@@ -486,7 +556,7 @@
           // 延迟关闭
           setTimeout(() => {
             closeLogin();
-          }, 1000);
+          }, 2000);
         } else {
           // 处理各种错误状态
           if (response.status === 401) {
@@ -512,6 +582,9 @@
     const code = registerCode.value.trim();
     const password = registerPassword.value;
     const passwordConfirm = registerPasswordConfirm.value;
+    const avatarFile = registerAvatar.files[0];
+    
+    // 注意:头像不再是必填,如果没有上传则使用默认头像
 
     // 验证用户名(现代网站标准)
     if (!username) {
@@ -587,12 +660,9 @@
     formData.append('code', code);
     formData.append('password', password);
     formData.append('passwordConfirm', passwordConfirm);
-
-    // 添加头像文件(如果有)
-    const avatarFile = registerAvatar.files[0];
-    if (avatarFile) {
-      formData.append('avatar', avatarFile);
-    }
+    
+    // 添加头像文件(必填,已在前面验证)
+    formData.append('avatar', avatarFile);
 
     httpRequest('http://localhost:3000/api/register', {
       method: 'POST',
@@ -616,12 +686,17 @@
       }
 
       if (result.success) {
-        showSuccess('注册成功');
+        // 显示成功提示
+        if (window.HintView) {
+          window.HintView.success('注册成功', 2000);
+        } else {
+          showSuccess('注册成功');
+        }
         // 延迟关闭并切换到登录
         setTimeout(() => {
           switchTab('login');
           clearRegisterForm();
-        }, 1000);
+        }, 2000);
       } else {
         showError(result.message || '注册失败');
       }
@@ -643,6 +718,8 @@
     const hint = registerPassword.parentElement.querySelector('.hint-text');
     hint.style.color = '#6b7280';
     hint.textContent = '密码要求:8-20位,包含大小写字母和数字';
+    // 重新加载默认头像
+    loadDefaultAvatar();
   }
 
   // 验证手机号格式

+ 89 - 3
client/js/navigation.js

@@ -1,6 +1,17 @@
 // 导航栏交互逻辑
 
 (function () {
+  // 检查是否已登录
+  function isLoggedIn() {
+    const userAvatarContainer = document.getElementById('userAvatarContainer');
+    // 如果用户头像容器显示,说明已登录
+    if (userAvatarContainer) {
+      const computedStyle = window.getComputedStyle(userAvatarContainer);
+      return computedStyle.display !== 'none';
+    }
+    return false;
+  }
+
   // 导航切换功能
   function initNavigation() {
     const navButtons = document.querySelectorAll('.nav-btn');
@@ -9,6 +20,25 @@
       button.addEventListener('click', function() {
         const page = this.getAttribute('data-page');
         
+        // 如果点击"我的资产"且未登录,显示登录界面
+        if (page === 'assets' && !isLoggedIn()) {
+          // 恢复按钮状态
+          navButtons.forEach(btn => btn.classList.remove('active'));
+          const storeBtn = document.querySelector('.nav-btn[data-page="store"]');
+          if (storeBtn) {
+            storeBtn.classList.add('active');
+          }
+          
+          // 显示登录界面
+          if (window.parent !== window) {
+            window.parent.postMessage({
+              type: 'navigation',
+              page: 'login'
+            }, '*');
+          }
+          return;
+        }
+        
         // 更新按钮状态
         navButtons.forEach(btn => btn.classList.remove('active'));
         this.classList.add('active');
@@ -50,6 +80,29 @@
         }
       });
     }
+
+    // 用户头像点击功能
+    const userAvatarContainer = document.getElementById('userAvatarContainer');
+    const userAvatar = document.getElementById('userAvatar');
+    
+    function handleAvatarClick() {
+      if (window.parent !== window) {
+        window.parent.postMessage({
+          type: 'navigation',
+          page: 'profile'
+        }, '*');
+      }
+    }
+    
+    if (userAvatarContainer) {
+      userAvatarContainer.addEventListener('click', handleAvatarClick);
+      userAvatarContainer.style.cursor = 'pointer';
+    }
+    
+    if (userAvatar) {
+      userAvatar.addEventListener('click', handleAvatarClick);
+      userAvatar.style.cursor = 'pointer';
+    }
   }
 
   // 更新用户头像显示
@@ -58,8 +111,8 @@
     const userAvatarContainer = document.getElementById('userAvatarContainer');
     const userAvatar = document.getElementById('userAvatar');
     
-    if (userInfo && userInfo.avatar) {
-      // 显示用户头像,隐藏登录/注册按钮
+    if (userInfo) {
+      // 有用户信息,说明已登录 - 显示用户头像,隐藏登录/注册按钮
       if (authButtons) {
         authButtons.style.display = 'none';
       }
@@ -77,7 +130,7 @@
         userAvatar.alt = userInfo.username || '用户头像';
       }
     } else {
-      // 隐藏用户头像,显示登录/注册按钮
+      // 没有用户信息,说明未登录 - 隐藏用户头像,显示登录/注册按钮
       if (authButtons) {
         authButtons.style.display = 'flex';
       }
@@ -119,8 +172,41 @@
     }
   });
 
+  // 检查并恢复登录状态
+  function checkAndRestoreLogin() {
+    try {
+      const loginDataStr = localStorage.getItem('loginData');
+      if (!loginDataStr) {
+        return;
+      }
+      
+      const loginData = JSON.parse(loginDataStr);
+      const now = Date.now();
+      
+      // 检查是否过期
+      if (now >= loginData.expireTime) {
+        // 已过期,清除登录信息
+        localStorage.removeItem('loginData');
+        console.log('[Navigation] 登录信息已过期,已清除');
+        return;
+      }
+      
+      // 未过期,恢复登录状态
+      if (loginData.user) {
+        console.log('[Navigation] 恢复登录状态,用户:', loginData.user.username);
+        updateUserAvatar(loginData.user);
+      }
+    } catch (error) {
+      console.error('[Navigation] 恢复登录状态失败:', error);
+      localStorage.removeItem('loginData');
+    }
+  }
+
   // 页面加载完成后初始化
   window.addEventListener('DOMContentLoaded', function() {
+    // 先检查并恢复登录状态
+    checkAndRestoreLogin();
+    
     initNavigation();
   });
 })();

+ 109 - 3
client/js/seq_ani_player/card.js

@@ -25,7 +25,71 @@
       this.localFrameResources = [];
       this.currentFolderName = '';
       
+      // 当前登录用户名
+      this.currentUsername = null;
+      
       this.init();
+      this.initUserListener();
+    }
+    
+    // 获取当前登录用户名
+    getCurrentUsername() {
+      // 如果已经缓存了用户名,直接返回
+      if (this.currentUsername) {
+        return this.currentUsername;
+      }
+      
+      // 从导航栏 iframe 中获取用户名
+      try {
+        let targetWindow = window.parent;
+        while (targetWindow && targetWindow !== window) {
+          try {
+            const navigationFrame = targetWindow.document.getElementById('navigationFrame');
+            if (navigationFrame && navigationFrame.contentWindow) {
+              const navWindow = navigationFrame.contentWindow;
+              const navDoc = navigationFrame.contentDocument || navWindow.document;
+              
+              // 检查用户是否已登录
+              const userAvatarContainer = navDoc.getElementById('userAvatarContainer');
+              if (userAvatarContainer) {
+                const computedStyle = navDoc.defaultView.getComputedStyle(userAvatarContainer);
+                if (computedStyle.display !== 'none') {
+                  // 用户已登录,从 userAvatar 的 alt 属性获取用户名
+                  const userAvatar = navDoc.getElementById('userAvatar');
+                  if (userAvatar && userAvatar.alt && userAvatar.alt !== '用户头像') {
+                    const username = userAvatar.alt;
+                    this.currentUsername = username;
+                    return username;
+                  }
+                }
+              }
+            }
+          } catch (e) {
+            // 跨域或访问限制,继续尝试上层窗口
+          }
+          
+          if (targetWindow.parent && targetWindow.parent !== targetWindow) {
+            targetWindow = targetWindow.parent;
+          } else {
+            break;
+          }
+        }
+      } catch (error) {
+        console.warn('[PreviewCard] 无法获取用户名:', error);
+      }
+      
+      return this.currentUsername;
+    }
+    
+    // 监听登录成功消息
+    initUserListener() {
+      window.addEventListener('message', (event) => {
+        if (event.data && event.data.type === 'login-success' && event.data.user) {
+          this.currentUsername = event.data.user.username;
+        } else if (event.data && event.data.type === 'logout') {
+          this.currentUsername = null;
+        }
+      });
     }
     
     async init() {
@@ -33,6 +97,36 @@
       this.bindElements();
       this.bindEvents();
       this.setStagePlaceholderVisible(true);
+      
+      // 从 localStorage 恢复登录状态
+      this.restoreLoginFromStorage();
+    }
+    
+    // 从 localStorage 恢复登录状态
+    restoreLoginFromStorage() {
+      try {
+        const loginDataStr = localStorage.getItem('loginData');
+        if (!loginDataStr) {
+          return;
+        }
+        
+        const loginData = JSON.parse(loginDataStr);
+        const now = Date.now();
+        
+        // 检查是否过期
+        if (now >= loginData.expireTime) {
+          localStorage.removeItem('loginData');
+          return;
+        }
+        
+        // 未过期,恢复登录状态
+        if (loginData.user && loginData.user.username) {
+          this.currentUsername = loginData.user.username;
+          console.log('[PreviewCard] 从 localStorage 恢复登录状态:', loginData.user.username);
+        }
+      } catch (error) {
+        console.error('[PreviewCard] 恢复登录状态失败:', error);
+      }
     }
     
     async loadTemplate() {
@@ -183,7 +277,11 @@
         
         // 3. 最后才请求服务器(使用新的disk API)
         // console.log('[PreviewCard] 请求服务器验证文件夹:', folderName);
-        const response = await fetch(`/api/disk/list?path=${encodeURIComponent(folderName)}`);
+        const username = this.getCurrentUsername();
+        if (!username) {
+          return false;
+        }
+        const response = await fetch(`/api/disk/list?username=${encodeURIComponent(username)}&path=${encodeURIComponent(folderName)}`);
         if (!response.ok) {
           return false;
         }
@@ -218,7 +316,11 @@
         // console.log('[PreviewCard] 开始加载文件夹:', folderName);
         
         // 1. 从网盘系统获取该文件夹的文件列表
-        const listResponse = await fetch(`/api/disk/list?path=${encodeURIComponent(folderName)}`);
+        const username = this.getCurrentUsername();
+        if (!username) {
+          throw new Error('请先登录');
+        }
+        const listResponse = await fetch(`/api/disk/list?username=${encodeURIComponent(username)}&path=${encodeURIComponent(folderName)}`);
         if (!listResponse.ok) {
           throw new Error('获取文件列表失败');
         }
@@ -249,7 +351,11 @@
         // 4. 构造帧URL列表(使用文件的实际名称)
         const frameUrls = pngFiles.map(file => {
           // 使用文件的完整路径(file.path)来构造URL
-          return `/api/disk/preview?path=${encodeURIComponent(file.path)}`;
+          const username = this.getCurrentUsername();
+          if (!username) {
+            return null; // 未登录时返回 null
+          }
+          return `/api/disk/preview?username=${encodeURIComponent(username)}&path=${encodeURIComponent(file.path)}`;
         });
         
         // console.log('[PreviewCard] 开始下载和缓存图片...');

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

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

+ 2 - 2
client/page/disk/tool-bar.html

@@ -18,9 +18,9 @@
                 <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;">
+            <button class="search-clear" id="searchClear" style="display: none;" title="清空搜索">
                 <svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor">
-                    <path d="M7 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"/>
+                    <path d="M7 0C3.13 0 0 3.13 0 7s3.13 7 7 7 7-3.13 7-7S10.87 0 7 0zm3.5 9.21L9.21 10.5 7 8.29 4.79 10.5 3.5 9.21 5.71 7 3.5 4.79 4.79 3.5 7 5.71 9.21 3.5 10.5 4.79 8.29 7 10.5 9.21z"/>
                 </svg>
             </button>
         </div>

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

@@ -78,6 +78,43 @@
         </div>
     </div>
 
+    <!-- 下载确认对话框 -->
+    <div class="download-confirm-overlay" id="downloadConfirmOverlay" style="display: none;">
+        <div class="download-confirm-modal">
+            <div class="download-confirm-header">
+                <h3>选择下载方式</h3>
+                <button class="download-confirm-close" id="downloadConfirmClose">
+                    <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>
+            <div class="download-confirm-content">
+                <div class="download-option" data-option="original">
+                    <div class="download-option-icon">📄</div>
+                    <div class="download-option-info">
+                        <div class="download-option-title">源文件下载</div>
+                        <div class="download-option-desc">直接下载原始图片,不进行抠图处理</div>
+                    </div>
+                </div>
+                <div class="download-option" data-option="normal">
+                    <div class="download-option-icon">✂️</div>
+                    <div class="download-option-info">
+                        <div class="download-option-title">普通抠图下载</div>
+                        <div class="download-option-desc">使用 rembg 进行抠图处理</div>
+                    </div>
+                </div>
+                <div class="download-option" data-option="vip">
+                    <div class="download-option-icon">✨</div>
+                    <div class="download-option-info">
+                        <div class="download-option-title">VIP抠图下载</div>
+                        <div class="download-option-desc">使用 BiRefNet 进行高质量抠图处理</div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+
     <script src="../../js/export-view/export-view.js"></script>
 </body>
 </html>

+ 2 - 0
client/page/login/login.html

@@ -5,6 +5,7 @@
   <meta name="viewport" content="width=device-width, initial-scale=1.0" />
   <title>登录/注册</title>
   <link rel="stylesheet" href="../../css/login/login.css" />
+  <link rel="stylesheet" href="../../css/hint-view.css" />
 </head>
 <body>
   <!-- 半透明背景遮罩 -->
@@ -102,6 +103,7 @@
     </div>
   </div>
 
+  <script src="../../js/hint-view.js"></script>
   <script src="../../js/login/login.js"></script>
 </body>
 </html>

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

@@ -14,8 +14,8 @@
       <span class="nav-title">游戏动画生成器</span>
     </div>
     <div class="nav-center">
-      <button class="nav-btn" data-page="store">动画商城</button>
-      <button class="nav-btn active" data-page="assets">我的资产</button>
+      <button class="nav-btn active" data-page="store">动画商城</button>
+      <button class="nav-btn" data-page="assets">我的资产</button>
     </div>
     <div class="nav-right">
       <div class="auth-buttons" id="authButtons">

+ 69 - 7
client/page/store/store.html

@@ -4,19 +4,81 @@
   <meta charset="UTF-8" />
   <meta name="viewport" content="width=device-width, initial-scale=1.0" />
   <title>动画商城</title>
-  <link rel="stylesheet" href="../../css/store.css" />
+  <link rel="stylesheet" href="../../css/store/store.css" />
+  <link rel="stylesheet" href="../../css/store/item.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 class="store-header">
+        <div class="search-bar">
+          <input 
+            type="text" 
+            id="searchInput" 
+            class="search-input" 
+            placeholder="搜索资源..."
+          />
+          <button class="search-button" id="searchButton">
+            <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+              <path d="M9 17A8 8 0 1 0 9 1a8 8 0 0 0 0 16zM18 18l-4-4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+            </svg>
+          </button>
+        </div>
+      </div>
+
+      <!-- 分类栏 -->
+      <div class="category-bar" id="categoryBar">
+        <button class="category-item active" data-category="">全部</button>
+        <!-- 分类按钮将通过 JavaScript 动态加载 -->
+      </div>
+
+      <!-- 资源网格 -->
+      <div class="resources-grid" id="resourcesGrid">
+        <!-- 资源项将通过 JavaScript 动态加载 -->
+      </div>
+
+      <!-- 空状态 -->
+      <div class="empty-state" id="emptyState" style="display: none;">
+        <div class="empty-icon">📦</div>
+        <div class="empty-text">暂无资源</div>
+      </div>
+
+      <!-- 加载状态 -->
+      <div class="loading-state" id="loadingState" style="display: none;">
+        <div class="loading-spinner"></div>
+        <div class="loading-text">加载中...</div>
       </div>
     </div>
   </div>
+
+  <!-- 动画预览弹窗 -->
+  <div class="preview-modal" id="previewModal" style="display: none;">
+    <div class="preview-modal-content">
+      <div class="preview-modal-header">
+        <h3 class="preview-modal-title" id="previewTitle">动画预览</h3>
+        <button class="preview-modal-close" id="previewClose">
+          <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+            <path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
+          </svg>
+        </button>
+      </div>
+      <div class="preview-modal-body">
+        <div class="preview-animation-container">
+          <img id="previewAnimationImage" class="preview-animation-image" alt="动画预览" />
+        </div>
+      </div>
+      <div class="preview-modal-footer">
+        <div class="fps-control">
+          <label class="fps-label">播放速度</label>
+          <input type="range" id="fpsSlider" class="fps-slider" min="1" max="30" value="8" step="1">
+          <span class="fps-display" id="fpsDisplay">8 FPS</span>
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <script src="../../js/store/item.js"></script>
+  <script src="../../js/store/store.js"></script>
 </body>
 </html>
-

BIN
server/data.db


+ 258 - 33
server/disk.js

@@ -16,21 +16,45 @@ const unlink = promisify(fs.unlink);
 
 class DiskManager {
     constructor() {
-        // 数据存储根目录(在 Server 目录下)
-        this.rootDir = path.join(__dirname, 'disk_data');
+        // 用户数据根目录
+        this.usersDir = path.join(__dirname, 'users');
         this.tempDir = path.join(__dirname, 'temp');
         this.pythonDir = path.join(__dirname, 'python');
-        this.ensureRootDir();
     }
 
-    // 确保根目录存在
-    async ensureRootDir() {
+    // 根据用户名获取用户的数据目录
+    getUserRootDir(username) {
+        if (!username) {
+            throw new Error('用户名不能为空');
+        }
+        // 用户名转换为小写,确保一致性
+        const normalizedUsername = username.toLowerCase();
+        return path.join(this.usersDir, normalizedUsername, 'disk_data');
+    }
+
+    // 确保用户目录存在
+    async ensureUserDir(username) {
+        const userRootDir = this.getUserRootDir(username);
         try {
-            await access(this.rootDir);
+            await access(userRootDir);
         } catch (error) {
-            await mkdir(this.rootDir, { recursive: true });
-            console.log('创建disk_data目录:', this.rootDir);
+            await mkdir(userRootDir, { recursive: true });
+            console.log('创建用户disk_data目录:', userRootDir);
+        }
+    }
+
+    // 从请求中获取用户名(支持查询参数和请求体)
+    getUsernameFromRequest(req, fields = null) {
+        // 先尝试从查询参数获取
+        const url = new URL(req.url, `http://${req.headers.host}`);
+        let username = url.searchParams.get('username');
+        
+        // 如果查询参数没有,尝试从请求体(fields)获取
+        if (!username && fields) {
+            username = Array.isArray(fields.username) ? fields.username[0] : (fields.username || null);
         }
+        
+        return username;
     }
 
     // 获取Python命令(尝试python或python3)
@@ -65,7 +89,7 @@ class DiskManager {
         }
 
         const pythonExe = path.join(this.pythonDir, 'venv', 'Scripts', 'python.exe');
-        const scriptPath = path.join(this.pythonDir, 'image-matting.py');
+        const scriptPath = path.join(this.pythonDir, 'rembg-matting.py');
         
         // 检查是否使用虚拟环境
         const usePython = await new Promise((resolve) => {
@@ -271,14 +295,18 @@ class DiskManager {
         });
     }
 
-    // 获取安全的文件路径
-    getSafePath(relativePath) {
+    // 获取安全的文件路径(基于用户名)
+    getSafePath(relativePath, username) {
+        if (!username) {
+            throw new Error('用户名不能为空');
+        }
+        const userRootDir = this.getUserRootDir(username);
         // 移除开头的斜杠
         relativePath = relativePath.replace(/^\/+/, '');
         // 解析路径,防止目录遍历攻击
-        const fullPath = path.join(this.rootDir, relativePath);
-        // 确保路径在根目录内
-        if (!fullPath.startsWith(this.rootDir)) {
+        const fullPath = path.join(userRootDir, relativePath);
+        // 确保路径在用户根目录内
+        if (!fullPath.startsWith(userRootDir)) {
             throw new Error('非法路径');
         }
         return fullPath;
@@ -427,10 +455,21 @@ class DiskManager {
     async handleListRequest(req, res) {
         try {
             const url = new URL(req.url, `http://${req.headers.host}`);
+            const username = url.searchParams.get('username');
+            if (!username) {
+                res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
+                res.end(JSON.stringify({ success: false, error: '缺少用户名参数' }));
+                return;
+            }
+            
             const relativePath = url.searchParams.get('path') || '';
             const recursive = url.searchParams.get('recursive') === 'true'; // 是否递归获取所有子文件夹
             
-            const fullPath = this.getSafePath(relativePath);
+            // 确保用户目录存在
+            await this.ensureUserDir(username);
+            
+            const fullPath = this.getSafePath(relativePath, username);
+            
 
             // 检查目录是否存在
             try {
@@ -472,8 +511,8 @@ class DiskManager {
                         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)}`;
+                            // 返回完整的预览URL路径(包含用户名)
+                            fileInfo.previewUrl = `/api/disk/preview?username=${encodeURIComponent(username)}&path=${encodeURIComponent(itemRelativePath + '/' + previewInfo.previewFile)}`;
                         }
                         
                         // 检查是否需要抠图(是否有非透明背景的PNG)
@@ -588,13 +627,24 @@ class DiskManager {
                 }
 
                 try {
+                    // 获取用户名
+                    const username = this.getUsernameFromRequest(req, fields);
+                    if (!username) {
+                        res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
+                        res.end(JSON.stringify({ success: false, message: '缺少用户名参数' }));
+                        return;
+                    }
+                    
+                    // 确保用户目录存在
+                    await this.ensureUserDir(username);
+                    
                     // 新版 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 targetPath = this.getSafePath(uploadPath, username);
                     
                     // 确保目标目录存在
                     const targetDir = path.dirname(targetPath);
@@ -661,6 +711,16 @@ class DiskManager {
             req.on('end', async () => {
                 try {
                     const data = JSON.parse(body);
+                    const username = data.username;
+                    if (!username) {
+                        res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
+                        res.end(JSON.stringify({ success: false, message: '缺少用户名参数' }));
+                        return;
+                    }
+                    
+                    // 确保用户目录存在
+                    await this.ensureUserDir(username);
+                    
                     const relativePath = data.path || '';
                     const folderName = data.name;
 
@@ -674,7 +734,7 @@ class DiskManager {
                     }
 
                     const folderPath = path.join(relativePath, folderName);
-                    const fullPath = this.getSafePath(folderPath);
+                    const fullPath = this.getSafePath(folderPath, username);
 
                     // 检查文件夹是否已存在
                     try {
@@ -723,6 +783,13 @@ class DiskManager {
             req.on('end', async () => {
                 try {
                     const data = JSON.parse(body);
+                    const username = data.username;
+                    if (!username) {
+                        res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
+                        res.end(JSON.stringify({ success: false, message: '缺少用户名参数' }));
+                        return;
+                    }
+                    
                     const oldPath = data.oldPath;
                     const newName = data.newName;
 
@@ -735,12 +802,13 @@ class DiskManager {
                         throw new Error('名称包含非法字符');
                     }
 
-                    const oldFullPath = this.getSafePath(oldPath);
+                    const userRootDir = this.getUserRootDir(username);
+                    const oldFullPath = this.getSafePath(oldPath, username);
                     const parentDir = path.dirname(oldFullPath);
                     const newFullPath = path.join(parentDir, newName);
 
-                    // 确保新路径也在根目录内
-                    if (!newFullPath.startsWith(this.rootDir)) {
+                    // 确保新路径也在用户根目录内
+                    if (!newFullPath.startsWith(userRootDir)) {
                         throw new Error('非法路径');
                     }
 
@@ -789,9 +857,16 @@ class DiskManager {
     async handleDownloadRequest(req, res) {
         try {
             const url = new URL(req.url, `http://${req.headers.host}`);
+            const username = url.searchParams.get('username');
+            if (!username) {
+                res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
+                res.end(JSON.stringify({ success: false, message: '缺少用户名参数' }));
+                return;
+            }
+            
             const relativePath = url.searchParams.get('path') || '';
             
-            const fullPath = this.getSafePath(relativePath);
+            const fullPath = this.getSafePath(relativePath, username);
 
             // 检查文件是否存在
             await access(fullPath);
@@ -822,13 +897,109 @@ class DiskManager {
         }
     }
 
+    // 处理获取用户网盘中的帧文件列表
+    async handleGetUserFrames(username, folderName, res) {
+        try {
+            console.log(`[DiskManager] 获取用户帧列表: username=${username}, folderName=${folderName}`);
+            
+            const userRootDir = this.getUserRootDir(username);
+            // 处理URL编码和空格
+            const decodedFolderName = decodeURIComponent(folderName).replace(/%20/g, ' ');
+            const folderPath = path.join(userRootDir, decodedFolderName);
+            
+            console.log(`[DiskManager] 用户网盘目录: ${userRootDir}`);
+            console.log(`[DiskManager] 解码后的文件夹名: ${decodedFolderName}`);
+            console.log(`[DiskManager] 完整路径: ${folderPath}`);
+            
+            // 检查目录是否存在
+            try {
+                await access(folderPath);
+            } catch (accessErr) {
+                console.error(`[DiskManager] 文件夹不存在: ${folderPath}`);
+                res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
+                res.end(JSON.stringify({ 
+                    error: 'Folder not found', 
+                    details: `文件夹不存在: ${folderPath}`,
+                    folderPath: folderPath
+                }));
+                return;
+            }
+            
+            // 读取文件夹内容
+            const files = await readdir(folderPath);
+            console.log(`[DiskManager] 找到 ${files.length} 个文件`);
+            
+            // 过滤出 PNG 文件,并按数字排序
+            const pngFiles = files
+                .filter(file => file.toLowerCase().endsWith('.png'))
+                .map(file => {
+                    let frameNum = null;
+                    // 先尝试匹配纯数字格式:00.png, 01.png
+                    let match = /^(\d+)\.png$/i.exec(file);
+                    if (match) {
+                        frameNum = parseInt(match[1], 10);
+                    } else {
+                        // 再尝试匹配文件名末尾的数字格式
+                        match = /(\d+)\.png$/i.exec(file);
+                        if (match) {
+                            frameNum = parseInt(match[1], 10);
+                        }
+                    }
+                    
+                    if (frameNum !== null) {
+                        return {
+                            frameNum: frameNum,
+                            fileName: file
+                        };
+                    }
+                    return null;
+                })
+                .filter(item => item !== null)
+                .sort((a, b) => a.frameNum - b.frameNum);
+            
+            if (pngFiles.length === 0) {
+                console.log(`[DiskManager] 文件夹中没有PNG文件`);
+                res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
+                res.end(JSON.stringify({ 
+                    error: '该文件夹中没有图片',
+                    frames: []
+                }));
+                return;
+            }
+            
+            const result = {
+                frames: pngFiles.map(item => item.frameNum),
+                fileNames: pngFiles.map(item => item.fileName),
+                maxFrame: pngFiles.length > 0 ? Math.max(...pngFiles.map(item => item.frameNum)) : 0
+            };
+            
+            console.log(`[DiskManager] ✓ 成功获取 ${result.frames.length} 帧`);
+            res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
+            res.end(JSON.stringify(result));
+        } catch (error) {
+            console.error('[DiskManager] 获取用户帧列表失败:', error);
+            res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
+            res.end(JSON.stringify({ 
+                error: 'Failed to get frames', 
+                details: error.message 
+            }));
+        }
+    }
+
     // 处理图片预览请求
     async handlePreviewRequest(req, res) {
         try {
             const url = new URL(req.url, `http://${req.headers.host}`);
+            const username = url.searchParams.get('username');
+            if (!username) {
+                res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
+                res.end(JSON.stringify({ success: false, message: '缺少用户名参数' }));
+                return;
+            }
+            
             const relativePath = url.searchParams.get('path') || '';
             
-            const fullPath = this.getSafePath(relativePath);
+            const fullPath = this.getSafePath(relativePath, username);
 
             // 检查文件是否存在
             await access(fullPath);
@@ -886,6 +1057,13 @@ class DiskManager {
             req.on('end', async () => {
                 try {
                     const data = JSON.parse(body);
+                    const username = data.username;
+                    if (!username) {
+                        res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
+                        res.end(JSON.stringify({ success: false, message: '缺少用户名参数' }));
+                        return;
+                    }
+                    
                     const sourcePath = data.sourcePath;
                     const targetFolder = data.targetFolder;
 
@@ -893,12 +1071,13 @@ class DiskManager {
                         throw new Error('源路径不能为空');
                     }
 
-                    const sourceFullPath = this.getSafePath(sourcePath);
+                    const userRootDir = this.getUserRootDir(username);
+                    const sourceFullPath = this.getSafePath(sourcePath, username);
                     const fileName = path.basename(sourceFullPath);
                     
                     // 目标路径
                     const targetPath = targetFolder ? path.join(targetFolder, fileName) : fileName;
-                    const targetFullPath = this.getSafePath(targetPath);
+                    const targetFullPath = this.getSafePath(targetPath, username);
 
                     // 确保源文件存在
                     await access(sourceFullPath);
@@ -907,6 +1086,11 @@ class DiskManager {
                     if (targetFullPath.startsWith(sourceFullPath + path.sep) || targetFullPath === sourceFullPath) {
                         throw new Error('不能移动到自身或子目录');
                     }
+                    
+                    // 确保目标路径也在用户根目录内
+                    if (!targetFullPath.startsWith(userRootDir)) {
+                        throw new Error('非法路径');
+                    }
 
                     // 检查目标是否已存在
                     try {
@@ -961,6 +1145,13 @@ class DiskManager {
             req.on('end', async () => {
                 try {
                     const data = JSON.parse(body);
+                    const username = data.username;
+                    if (!username) {
+                        res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
+                        res.end(JSON.stringify({ success: false, message: '缺少用户名参数' }));
+                        return;
+                    }
+                    
                     const sourcePath = data.sourcePath;
                     const targetFolder = data.targetFolder;
 
@@ -968,16 +1159,22 @@ class DiskManager {
                         throw new Error('源路径不能为空');
                     }
 
-                    const sourceFullPath = this.getSafePath(sourcePath);
+                    const userRootDir = this.getUserRootDir(username);
+                    const sourceFullPath = this.getSafePath(sourcePath, username);
                     const fileName = path.basename(sourceFullPath);
                     const targetPath = targetFolder ? path.join(targetFolder, fileName) : fileName;
-                    const targetFullPath = this.getSafePath(targetPath);
+                    const targetFullPath = this.getSafePath(targetPath, username);
 
                     await access(sourceFullPath);
 
                     if (targetFullPath === sourceFullPath || targetFullPath.startsWith(sourceFullPath + path.sep)) {
                         throw new Error('不能复制到自身或子目录');
                     }
+                    
+                    // 确保目标路径也在用户根目录内
+                    if (!targetFullPath.startsWith(userRootDir)) {
+                        throw new Error('非法路径');
+                    }
 
                     try {
                         await access(targetFullPath);
@@ -1042,6 +1239,13 @@ class DiskManager {
             req.on('end', async () => {
                 try {
                     const data = JSON.parse(body);
+                    const username = data.username;
+                    if (!username) {
+                        res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
+                        res.end(JSON.stringify({ success: false, message: '缺少用户名参数' }));
+                        return;
+                    }
+                    
                     const paths = data.paths;
 
                     if (!paths || !Array.isArray(paths) || paths.length === 0) {
@@ -1052,7 +1256,7 @@ class DiskManager {
 
                     for (const filePath of paths) {
                         try {
-                            const fullPath = this.getSafePath(filePath);
+                            const fullPath = this.getSafePath(filePath, username);
                             await access(fullPath);
                             const stats = await stat(fullPath);
 
@@ -1151,9 +1355,20 @@ class DiskManager {
             try {
                 console.log('[API] ✓ 请求数据接收完成');
                 console.log('[API] → 解析JSON数据...');
-                const { paths } = JSON.parse(body);
+                const data = JSON.parse(body);
+                const { paths, username } = data;
                 console.log('[API] ✓ JSON解析成功');
                 console.log('[API]   请求路径:', paths);
+                console.log('[API]   用户名:', username);
+                
+                if (!username) {
+                    res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
+                    res.end(JSON.stringify({
+                        success: false,
+                        message: '缺少用户名参数'
+                    }));
+                    return;
+                }
                 
                 if (!paths || !Array.isArray(paths) || paths.length === 0) {
                     console.warn('[API] ⚠ 路径为空或无效');
@@ -1183,7 +1398,7 @@ class DiskManager {
                     });
                     
                     console.log('[API] → 获取安全路径...');
-                    const fullPath = this.getSafePath(relativePath);
+                    const fullPath = this.getSafePath(relativePath, username);
                     console.log('[API] ✓ 完整路径:', fullPath);
                     
                     console.log('[API] → 读取文件状态...');
@@ -1477,7 +1692,17 @@ class DiskManager {
 
         req.on('end', async () => {
             try {
-                const { paths } = JSON.parse(body);
+                const data = JSON.parse(body);
+                const { paths, username } = data;
+                
+                if (!username) {
+                    res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
+                    res.end(JSON.stringify({
+                        success: false,
+                        message: '缺少用户名参数'
+                    }));
+                    return;
+                }
                 
                 if (!paths || !Array.isArray(paths) || paths.length === 0) {
                     res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
@@ -1497,7 +1722,7 @@ class DiskManager {
                     const relativePath = paths[i];
                     console.log(`\n[API] 裁剪项目 ${i + 1}/${paths.length}: ${relativePath}`);
                     
-                    const fullPath = this.getSafePath(relativePath);
+                    const fullPath = this.getSafePath(relativePath, username);
                     const stats = await stat(fullPath);
 
                     if (stats.isDirectory()) {

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


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


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


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


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


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


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


BIN
server/disk_data/新建文件夹/生成白色背景长矛刺击动画00.png


BIN
server/disk_data/新建文件夹/生成白色背景长矛刺击动画01.png


BIN
server/disk_data/新建文件夹/生成白色背景长矛刺击动画02.png


BIN
server/disk_data/新建文件夹/生成白色背景长矛刺击动画03.png


BIN
server/disk_data/新建文件夹/生成白色背景长矛刺击动画04.png


BIN
server/disk_data/新建文件夹/生成白色背景长矛刺击动画05.png


BIN
server/disk_data/新建文件夹/生成白色背景长矛刺击动画06.png


BIN
server/disk_data/新建文件夹/生成白色背景长矛刺击动画07.png


BIN
server/disk_data/新建文件夹/生成白色背景长矛刺击动画08.png


BIN
server/disk_data/新建文件夹/生成白色背景长矛刺击动画09.png


BIN
server/disk_data/新建文件夹/生成白色背景长矛刺击动画10.png


BIN
server/disk_data/新建文件夹/生成白色背景长矛刺击动画11.png


BIN
server/disk_data/新建文件夹/生成白色背景长矛刺击动画12.png


BIN
server/disk_data/新建文件夹/生成白色背景长矛刺击动画13.png


BIN
server/disk_data/新建文件夹/生成白色背景长矛刺击动画14.png


BIN
server/disk_data/新建文件夹/生成白色背景长矛刺击动画15.png


BIN
server/disk_data/新建文件夹/生成白色背景长矛刺击动画16.png


BIN
server/disk_data/新建文件夹/生成白色背景长矛刺击动画17.png


BIN
server/disk_data/新建文件夹/生成白色背景长矛刺击动画18.png


BIN
server/disk_data/新建文件夹/生成白色背景长矛刺击动画19.png


BIN
server/disk_data/新建文件夹/生成白色背景长矛刺击动画20.png


BIN
server/disk_data/新建文件夹/生成白色背景长矛刺击动画21.png


+ 0 - 1
server/market_data/.gitignore

@@ -1 +0,0 @@
-sequences

+ 0 - 132
server/python/image-matting.py

@@ -1,132 +0,0 @@
-"""
-使用 rembg 库进行图像背景移除
-"""
-import sys
-import os
-from rembg import remove
-from PIL import Image
-from glob import glob
-
-SUPPORTED_FORMATS = ['.jpg', '.jpeg', '.png', '.bmp', '.webp', '.tiff', '.tif']
-
-def extract_character_rembg(input_path, output_path):
-    """
-    使用 rembg 库移除图像背景
-    """
-    try:
-        print(f'  -> 读取图片: {os.path.basename(input_path)}')
-        
-        if not os.path.exists(input_path):
-            print(f'    [X] 错误: 文件不存在')
-            return False
-        
-        # 创建输出目录
-        output_dir = os.path.dirname(output_path)
-        if output_dir and not os.path.exists(output_dir):
-            print(f'  -> 创建输出目录: {output_dir}')
-            os.makedirs(output_dir, exist_ok=True)
-        
-        # 读取输入图像
-        print(f'  -> 加载图像...')
-        input_image = Image.open(input_path)
-        original_size = input_image.size
-        print(f'    图像尺寸: {original_size[0]}x{original_size[1]}')
-        
-        # 确保输入图像是RGB模式
-        if input_image.mode != 'RGB':
-            print(f'  -> 转换颜色模式: {input_image.mode} -> RGB')
-            input_image = input_image.convert('RGB')
-        
-        # 使用 rembg 移除背景
-        print(f'  -> 执行AI抠图...')
-        output_image = remove(input_image, post_process_mask=False)
-        print(f'    [OK] 抠图完成')
-        
-        # 确保输出图像尺寸与输入图像一致
-        if output_image.size != original_size:
-            print(f'  -> 调整图像尺寸')
-            final_image = Image.new('RGBA', original_size, (0, 0, 0, 0))
-            final_image.paste(output_image, (0, 0), output_image if output_image.mode == 'RGBA' else None)
-            output_image = final_image
-        else:
-            if output_image.mode != 'RGBA':
-                output_image = output_image.convert('RGBA')
-        
-        # 保存结果
-        print(f'  -> 保存结果: {os.path.basename(output_path)}')
-        output_image.save(output_path, 'PNG')
-        print(f'    [OK] 保存成功')
-        return True
-        
-    except Exception as error:
-        print(f'    [X] 处理失败: {error}')
-        return False
-
-def process_folder(input_folder, output_folder):
-    """
-    批量处理文件夹中的所有图片
-    """
-    print('=' * 60)
-    print('[Step 1/2] AI Image Matting')
-    print('=' * 60)
-    
-    if not os.path.exists(input_folder):
-        print(f'[X] Error: Input folder not found: {input_folder}')
-        return 0
-    
-    print(f'-> Input folder: {input_folder}')
-    
-    # 创建输出文件夹
-    if not os.path.exists(output_folder):
-        print(f'-> Creating output folder: {output_folder}')
-        os.makedirs(output_folder, exist_ok=True)
-    else:
-        print(f'-> Output folder: {output_folder}')
-    
-    # 获取所有支持的图片文件
-    print(f'-> Scanning image files...')
-    image_files = []
-    for ext in SUPPORTED_FORMATS:
-        image_files.extend(glob(os.path.join(input_folder, f'*{ext}')))
-        image_files.extend(glob(os.path.join(input_folder, f'*{ext.upper()}')))
-    
-    # 去重(Windows文件系统不区分大小写,可能重复)
-    image_files = list(set(image_files))
-    image_files.sort()
-    
-    if not image_files:
-        print(f'[X] No supported image files found')
-        return 0
-    
-    print(f'[OK] Found {len(image_files)} images')
-    print('-' * 60)
-    
-    success_count = 0
-    for idx, input_path in enumerate(image_files, 1):
-        filename = os.path.basename(input_path)
-        name_without_ext = os.path.splitext(filename)[0]
-        output_path = os.path.join(output_folder, f'{name_without_ext}.png')
-        
-        print(f'\n[{idx}/{len(image_files)}] {filename}')
-        print(f'PROGRESS: {idx}/{len(image_files)}')
-        
-        if extract_character_rembg(input_path, output_path):
-            success_count += 1
-            print(f'    [OK] Done')
-    
-    print('\n' + '=' * 60)
-    print(f'[Step 1 Complete] Success: {success_count}/{len(image_files)}')
-    print('=' * 60)
-    return success_count
-
-if __name__ == '__main__':
-    if len(sys.argv) != 3:
-        print('用法: python image-matting.py <输入文件夹> <输出文件夹>')
-        sys.exit(1)
-    
-    input_folder = sys.argv[1]
-    output_folder = sys.argv[2]
-    
-    processed = process_folder(input_folder, output_folder)
-    sys.exit(0 if processed > 0 else 1)
-

Some files were not shown because too many files changed in this diff