User преди 2 месеца
родител
ревизия
8f6f071158
променени са 27 файла, в които са добавени 1243 реда и са изтрити 773 реда
  1. 48 0
      .github/copilot-instructions.md
  2. 1 25
      admin/js/index.js
  3. 142 57
      client/css/ai-generate/ai-generate-view.css
  4. 307 219
      client/css/profile/profile.css
  5. 0 1
      client/index.html
  6. 112 0
      client/js/Index.js
  7. 43 24
      client/js/ai-generate/ai-generate-view.js
  8. 0 138
      client/js/export-view-manager.js
  9. 150 8
      client/js/export-view/export-view.js
  10. 145 69
      client/js/profile/profile.js
  11. 2 4
      client/page/ai-generate/ai-generate-view.html
  12. 69 7
      client/page/profile/profile.html
  13. 0 52
      server/ai-history.json
  14. 58 166
      server/ai-queue.js
  15. 6 1
      server/ai-queue.json
  16. BIN
      server/data.db
  17. 135 1
      server/sql.js
  18. 1 1
      server/user.js
  19. BIN
      server/users/yichael/ai-images/ai_1765560074612_twqt5peep_preview.png
  20. BIN
      server/users/yichael/ai-images/ai_1765560102494_e2omjm6p9_preview.png
  21. BIN
      server/users/yichael/ai-images/ai_1765560852619_oe2dgxfph.png
  22. BIN
      server/users/yichael/ai-images/ai_1765560852619_oe2dgxfph_preview.png
  23. BIN
      server/users/yichael/ai-images/ai_1765562471902_n8wnmu31o.png
  24. BIN
      server/users/yichael/ai-images/ai_1765562471902_n8wnmu31o_preview.png
  25. BIN
      server/users/yichael/ai-images/ai_1765564852659_ponpnyjr7.png
  26. BIN
      server/users/yichael/ai-images/ai_1765564852659_ponpnyjr7_preview.png
  27. 24 0
      server/users/yichael/generate-history/history.json

+ 48 - 0
.github/copilot-instructions.md

@@ -0,0 +1,48 @@
+# Copilot instructions — AnimationManager
+
+This file gives actionable, repository-specific guidance for AI coding agents working on AnimationManager.
+
+Overview
+- Single-process Node.js server: `server/server.js` is the HTTP server and router. It serves static files from `client/` and `admin/` and exposes REST endpoints under `/api/*`.
+- UI: `client/` (end-user) and `admin/` (management) are static HTML/JS pages (no build step) loaded by the server.
+- Data and assets live in `server/` (modules, `users/`, `avatar/`, `temp/`) and `server/python/` contains matting/AI scripts invoked by server modules.
+
+Quick start (dev on Windows)
+- Install server deps: open `server/` and run `npm install` (dependencies in `server/package.json`).
+- Start server: from `server/` run `npm start` or run the workspace root `start-server.bat`.
+- Server default port: `3000`.
+
+Key files to read before coding
+- `server/server.js` — central router, API registration, static file handling, CORS and caching logic.
+- `server/package.json` — start script and runtime deps (`sharp`, `sqlite3`, `formidable`, etc.).
+- `server/ai-queue.js`, `server/disk.js`, `server/store/store.js`, `server/user.js` — implementation points for AI, storage, store/catalog, and user logic.
+- `client/index.html` and `admin/index.html` — demonstrate iframe-based UI structure and pages under `page/`.
+
+Patterns & conventions (explicit)
+- Routing: Add new endpoints by editing `server/server.js`. Follow existing style: check `pathname`, branch, call handler modules, then `return`.
+- Static file resolution: server decodes URL path segments (`decodeURIComponent`) and maps `/admin/` → `admin/`, other → `client/`, and `/texture|/avatar|/users` → `server/`.
+- Cache behavior: image responses set long `Cache-Control` and `ETag` based on `stat` (follow that pattern for image endpoints).
+- File paths: endpoints often expect URL-encoded path segments (e.g. `/api/frames/<encoded-folder>`). Use `encodeURIComponent` per-segment when calling.
+- Store mutations: when changing store resources, call `storeManager.clearCategoriesCache()` or `storeManager.clearPricesCache()` as appropriate (seen in `server.js`).
+- Error handling style: many handlers write simple JSON/text responses and log to console. Keep changes consistent with existing try/catch and `res.writeHead` usage.
+
+Integration & external dependencies
+- Native Node modules + npm: `sharp`, `sqlite3`, `ws`, `formidable`, `archiver` (see `server/package.json`).
+- Python matting/AI: scripts live in `server/python/` and are invoked by server modules (ensure a Python venv with `pip install -r server/python/requirements.txt` when testing matting locally).
+- AI images: generated assets are stored under `server/users/<username>/ai-images` and served via `/api/ai/image?username=...&id=...`.
+
+Developer tips for common tasks
+- Add an API route: update `server/server.js` near other `/api/...` handlers, require or delegate to `server/*.js` handler modules.
+- Debugging: server logs API calls when `pathname.startsWith('/api/')`. For interactive debugging, run `node --inspect server.js` from `server/` and attach VS Code.
+- Static UI changes: edit files under `client/` or `admin/` and reload browser; no build step.
+
+Examples (call sites)
+- Pack sprites: `POST /api/pack` handled by `server/zip.js`.
+- AI generate: `POST /api/ai/generate` handled by `server/ai-queue.js`; preview/image endpoints are `/api/ai/preview` and `/api/ai/image`.
+- Disk upload/list: `server/disk.js` handlers exposed under `/api/disk/*`.
+
+When to ask maintainers / open an issue
+- If you need changes to global routing structure rather than a single endpoint (server.js becomes a long file), ask before refactoring.
+- If new native dependencies are required (native `npm` modules or Python libs), request permission because CI/deployment may require environment changes.
+
+If anything above is unclear or you want more examples (handler templates, common response shapes, or a suggested VS Code launch config), tell me which part to expand.

+ 1 - 25
admin/js/index.js

@@ -252,31 +252,7 @@ window.GlobalAlert = (function() {
     } else if (data && data.type === "navigation" && data.page) {
       switchPage(data.page);
     }
-    // 注意:global-alert、global-loading、global-confirm 消息不再通过 index.js 处理
-    // 各个 view 现在直接调用父窗口的 GlobalAlert/GlobalLoading/GlobalConfirm
-    else if (data && data.type === "open-export-view") {
-      // 处理打开导出弹出框
-      console.log('[Index] 收到open-export-view消息:', data);
-      if (!data.folderName) {
-        console.error('[Index] 缺少文件夹名称');
-        return;
-      }
-      if (!window.ExportViewManager) {
-        console.error('[Index] ExportViewManager 未初始化');
-        return;
-      }
-      // 直接打开弹出框,传递文件夹名称
-      window.ExportViewManager.show(data.folderName).then((confirmed) => {
-        console.log('[Index] 用户选择:', confirmed ? '确认导出' : '取消');
-        // 如果用户确认,可以在这里处理实际的导出下载逻辑
-        if (confirmed) {
-          // TODO: 处理实际的导出下载逻辑
-          console.log('[Index] 用户确认导出,文件夹:', data.folderName);
-        }
-      }).catch(error => {
-        console.error('[Index] ExportViewManager显示失败:', error);
-      });
-    } else if (data && data.type === "close-login-view") {
+    else if (data && data.type === "close-login-view") {
       const loginFrame = document.getElementById('loginViewFrame');
       if (loginFrame) {
         loginFrame.style.display = 'none';

+ 142 - 57
client/css/ai-generate/ai-generate-view.css

@@ -69,45 +69,61 @@ body {
 .ai-generate-preview-container {
     display: grid;
     grid-template-columns: 1fr 1fr;
-    gap: 20px;
-    padding: 24px;
-    padding-top: 50px;
+    gap: 24px;
+    padding: 28px;
+    padding-top: 56px;
     flex: 1;
     min-height: 0;
     overflow: auto;
 }
 
+/* 预览区块 */
+.ai-generate-reference-section,
+.ai-generate-spritesheet-section {
+    display: flex;
+    flex-direction: column;
+}
+
 /* 预览框通用样式 */
 .ai-generate-preview-box {
     border-radius: 16px;
-    overflow: hidden; /* 保持 hidden 以维持圆角效果 */
+    overflow: hidden;
     position: relative;
     display: flex;
     align-items: center;
     justify-content: center;
-    min-height: 300px;
-    max-height: 450px;
-    padding: 4px; /* 添加小量 padding,确保内部按钮不被裁剪 */
-    box-sizing: border-box; /* 确保 padding 不影响尺寸计算 */
+    min-height: 320px;
+    max-height: 420px;
+    flex: 1;
+    box-sizing: border-box;
+    transition: all 0.3s ease;
 }
 
 /* 左侧参考图区域 */
 .ai-generate-reference-box {
-    background: linear-gradient(135deg, #f0f4ff 0%, #e8f0ff 100%);
-    border: 2px dashed #667eea;
+    background: linear-gradient(135deg, #f8faff 0%, #f0f4ff 100%);
+    border: 2px dashed #a5b4fc;
+    overflow: visible;
+    box-shadow: 0 4px 20px rgba(102, 126, 234, 0.08);
+}
+
+.ai-generate-reference-box:hover {
+    border-color: #667eea;
+    box-shadow: 0 6px 24px rgba(102, 126, 234, 0.15);
 }
 
 /* 右侧预览区域 */
 .ai-generate-spritesheet-box {
     background: 
-        linear-gradient(45deg, #e5e7eb 25%, transparent 25%),
-        linear-gradient(-45deg, #e5e7eb 25%, transparent 25%),
-        linear-gradient(45deg, transparent 75%, #e5e7eb 75%),
-        linear-gradient(-45deg, transparent 75%, #e5e7eb 75%);
-    background-size: 20px 20px;
-    background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
-    background-color: #f9fafb;
+        linear-gradient(45deg, #f3f4f6 25%, transparent 25%),
+        linear-gradient(-45deg, #f3f4f6 25%, transparent 25%),
+        linear-gradient(45deg, transparent 75%, #f3f4f6 75%),
+        linear-gradient(-45deg, transparent 75%, #f3f4f6 75%);
+    background-size: 16px 16px;
+    background-position: 0 0, 0 8px, 8px -8px, -8px 0px;
+    background-color: #ffffff;
     border: 1px solid #e5e7eb;
+    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
 }
 
 /* 预览图 */
@@ -132,11 +148,16 @@ body {
     padding: 40px;
     width: 100%;
     height: 100%;
-    transition: all 0.3s;
+    transition: all 0.3s ease;
+    border-radius: 14px;
 }
 
 .reference-upload-area:hover {
-    background: rgba(102, 126, 234, 0.1);
+    background: rgba(102, 126, 234, 0.08);
+}
+
+.reference-upload-area:hover .upload-icon {
+    transform: scale(1.1);
 }
 
 .reference-upload-area.hide {
@@ -144,57 +165,69 @@ body {
 }
 
 .upload-icon {
+    color: #9ca3af;
     font-size: 48px;
-    color: #667eea;
-    margin-bottom: 12px;
-    font-weight: 300;
+    font-weight: 200;
+    line-height: 1;
+    transition: color 0.2s ease;
 }
 
-.upload-text {
-    color: #667eea;
-    font-size: 14px;
-    font-weight: 500;
+.reference-upload-area:hover .upload-icon {
+    color: #6b7280;
 }
 
 /* 参考图容器 */
 .reference-image-wrapper {
-    position: relative;
-    width: 100%;
-    height: 100%;
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
     display: flex;
     align-items: center;
     justify-content: center;
+    z-index: 1;
+    padding: 16px;
+    box-sizing: border-box;
 }
 
 .reference-image {
     max-width: 100%;
     max-height: 100%;
+    width: auto;
+    height: auto;
     object-fit: contain;
-    display: block !important; /* 覆盖 .ai-generate-preview-box img 的 display: none */
+    display: block !important;
+    border-radius: 12px;
+    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
 }
 
 /* 删除参考图按钮 */
 .reference-remove-btn {
     position: absolute;
-    top: 8px; /* 距离顶部足够远,不会被裁剪 */
-    right: 8px; /* 距离右边足够远,不会被裁剪 */
-    width: 32px;
-    height: 32px;
+    top: 12px;
+    right: 12px;
+    width: 36px;
+    height: 36px;
     border-radius: 50%;
-    background: rgba(0, 0, 0, 0.6);
-    border: none;
+    background: rgba(0, 0, 0, 0.65);
+    backdrop-filter: blur(8px);
+    border: 2px solid rgba(255, 255, 255, 0.2);
     cursor: pointer;
     display: flex;
     align-items: center;
     justify-content: center;
     color: white;
-    transition: all 0.2s;
-    z-index: 10; /* 确保按钮在最上层 */
+    transition: all 0.25s ease;
+    z-index: 100;
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
 }
 
 .reference-remove-btn:hover {
-    background: #ef4444;
-    transform: scale(1.1);
+    background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
+    border-color: rgba(255, 255, 255, 0.3);
+    transform: scale(1.1) rotate(90deg);
+    box-shadow: 0 6px 20px rgba(239, 68, 68, 0.4);
 }
 
 /* 加载状态 */
@@ -391,32 +424,39 @@ body {
 
 /* 提示词配置区域 */
 .prompt-config-section {
-    padding: 0 20px 20px;
+    padding: 0 28px 24px;
     display: flex;
     flex-direction: column;
-    gap: 12px;
+    gap: 16px;
+    background: linear-gradient(180deg, transparent 0%, rgba(248, 250, 252, 0.5) 100%);
 }
 
 .config-row {
     display: flex;
-    gap: 12px;
+    gap: 16px;
+    align-items: flex-start;
 }
 
 .config-textarea {
     flex: 1;
-    min-height: 80px;
-    padding: 14px;
+    min-height: 72px;
+    max-height: 120px;
+    padding: 14px 16px;
     border: 2px solid #e5e7eb;
-    border-radius: 12px;
+    border-radius: 14px;
     font-size: 14px;
-    resize: vertical;
-    transition: border-color 0.2s;
+    line-height: 1.5;
+    resize: none;
+    transition: all 0.25s ease;
     font-family: inherit;
+    background: white;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
 }
 
 .config-textarea:focus {
     outline: none;
     border-color: #667eea;
+    box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1), 0 4px 12px rgba(0, 0, 0, 0.06);
 }
 
 .config-textarea::placeholder {
@@ -434,30 +474,59 @@ body {
     display: flex;
     align-items: center;
     gap: 8px;
-    padding: 12px 24px;
-    border-radius: 12px;
-    font-size: 14px;
+    padding: 14px 28px;
+    border-radius: 14px;
+    font-size: 15px;
     font-weight: 600;
     cursor: pointer;
-    transition: all 0.2s;
+    transition: all 0.25s ease;
     border: none;
 }
 
 .generate-action-btn {
     background: linear-gradient(135deg, #10b981 0%, #059669 100%);
     color: white;
-    box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
+    box-shadow: 0 6px 20px rgba(16, 185, 129, 0.35);
+    position: relative;
+    overflow: hidden;
+}
+
+.generate-action-btn::before {
+    content: '';
+    position: absolute;
+    top: 0;
+    left: -100%;
+    width: 100%;
+    height: 100%;
+    background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
+    transition: left 0.5s ease;
+}
+
+.generate-action-btn:hover:not(:disabled)::before {
+    left: 100%;
 }
 
 .generate-action-btn:hover:not(:disabled) {
-    transform: translateY(-2px);
-    box-shadow: 0 6px 20px rgba(16, 185, 129, 0.4);
+    transform: translateY(-3px);
+    box-shadow: 0 10px 28px rgba(16, 185, 129, 0.45);
+}
+
+.generate-action-btn:active:not(:disabled) {
+    transform: translateY(-1px);
 }
 
 .generate-action-btn:disabled {
     opacity: 0.5;
     cursor: not-allowed;
     transform: none;
+    box-shadow: none;
+}
+
+.btn-price {
+    background: rgba(255, 255, 255, 0.2);
+    padding: 4px 10px;
+    border-radius: 8px;
+    font-size: 13px;
 }
 
 /* 飞走动画 */
@@ -572,10 +641,26 @@ body {
 @media (max-width: 768px) {
     .ai-generate-preview-container {
         grid-template-columns: 1fr;
+        padding: 20px;
+        padding-top: 48px;
+        gap: 20px;
     }
     
     .ai-generate-preview-box {
-        min-height: 200px;
+        min-height: 220px;
+        max-height: 300px;
+    }
+    
+    .prompt-config-section {
+        padding: 0 20px 20px;
+    }
+    
+    .config-textarea {
+        min-height: 60px;
+    }
+    
+    .action-btn {
+        padding: 12px 20px;
+        font-size: 14px;
     }
 }
-

+ 307 - 219
client/css/profile/profile.css

@@ -118,7 +118,8 @@ html, body {
 
 .history-section .ai-history-container {
   flex: 1;
-  overflow: hidden;
+  overflow-y: auto;
+  overflow-x: hidden;
   min-height: 0;
 }
 
@@ -266,47 +267,96 @@ html, body {
   display: flex;
   align-items: center;
   justify-content: space-between;
-  flex-wrap: wrap;
-  gap: 14px;
+  padding: 20px 24px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  border-radius: 16px;
+  position: relative;
+  overflow: hidden;
+}
+
+.points-section::before {
+  content: '';
+  position: absolute;
+  top: -50%;
+  right: -20%;
+  width: 200px;
+  height: 200px;
+  background: rgba(255, 255, 255, 0.1);
+  border-radius: 50%;
+}
+
+.points-section::after {
+  content: '💰';
+  position: absolute;
+  right: 100px;
+  top: 50%;
+  transform: translateY(-50%);
+  font-size: 48px;
+  opacity: 0.15;
+}
+
+.points-card {
+  padding: 0 !important;
+  overflow: hidden;
 }
 
 .points-display {
   display: flex;
-  align-items: baseline;
-  gap: 8px;
+  flex-direction: column;
+  gap: 6px;
+}
+
+.points-value-row {
+  display: flex;
+  flex-direction: column;
+  gap: 2px;
+}
+
+.points-section .recharge-btn {
+  flex-shrink: 0;
+  z-index: 1;
 }
 
 .points-label {
-  font-size: 14px;
-  color: #6b7280;
+  font-size: 13px;
+  color: rgba(255, 255, 255, 0.8);
+  font-weight: 500;
 }
 
 .points-value {
-  font-size: 28px;
-  font-weight: 700;
-  color: #667eea;
+  font-size: 36px;
+  font-weight: 800;
+  color: white;
+  text-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);
+  line-height: 1;
 }
 
 .points-unit {
   font-size: 14px;
-  color: #6b7280;
+  color: rgba(255, 255, 255, 0.75);
+  font-weight: 500;
 }
 
 .recharge-btn {
-  padding: 8px 20px;
-  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-  color: white;
+  padding: 12px 28px;
+  background: white;
+  color: #667eea;
   border: none;
-  border-radius: 8px;
-  font-size: 13px;
-  font-weight: 600;
+  border-radius: 25px;
+  font-size: 14px;
+  font-weight: 700;
   cursor: pointer;
-  transition: all 0.2s;
+  transition: all 0.3s ease;
+  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15);
 }
 
 .recharge-btn:hover {
-  transform: translateY(-1px);
-  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
+  transform: translateY(-2px) scale(1.02);
+  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
+}
+
+.recharge-btn:active {
+  transform: translateY(0) scale(0.98);
 }
 
 .ai-history-container {
@@ -326,33 +376,37 @@ html, body {
 
 .history-grid {
   display: grid;
-  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
-  gap: 12px;
-  padding: 4px;
+  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
+  gap: 16px;
+  padding: 8px;
   overflow: visible;
 }
 
 .history-item {
   position: relative;
-  border-radius: 12px;
+  border-radius: 16px;
   overflow: hidden;
-  background: #f9fafb;
+  background: white;
   aspect-ratio: 1;
-  border: 2px solid #e5e5e5;
-  transition: all 0.3s ease;
+  border: 1px solid #e5e7eb;
+  transition: all 0.25s ease;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
 }
 
 .history-item:hover {
   border-color: #667eea;
-  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
-  transform: translateY(-2px);
+  box-shadow: 0 12px 24px rgba(102, 126, 234, 0.15);
+  transform: translateY(-4px);
+}
+
+.history-item.completed {
+  cursor: zoom-in;
 }
 
 .history-item.rendering {
   display: flex;
   align-items: center;
   justify-content: center;
-  background: linear-gradient(135deg, #f0f0f0 0%, #e5e5e5 100%);
 }
 
 .history-item.rendering .rendering-content {
@@ -424,12 +478,12 @@ html, body {
 .history-item.rendering,
 .history-item.queued {
   overflow: hidden;
-  background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
 }
 
 .history-item.failed {
   overflow: hidden;
-  background: linear-gradient(135deg, #2d1b1b 0%, #1a1a1a 100%);
+  background: linear-gradient(135deg, #374151 0%, #1f2937 100%);
 }
 
 .history-item.failed .failed-content {
@@ -458,14 +512,14 @@ html, body {
   text-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
 }
 
-/* 重新生图按钮 */
-.history-item.failed .retry-btn {
+/* 删除按钮 */
+.history-item.failed .delete-btn {
   display: flex;
   align-items: center;
   gap: 6px;
   margin-top: 8px;
   padding: 8px 16px;
-  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  background: rgba(239, 68, 68, 0.8);
   color: white;
   border: none;
   border-radius: 20px;
@@ -473,47 +527,33 @@ html, body {
   font-weight: 600;
   cursor: pointer;
   transition: all 0.3s ease;
-  box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
 }
 
-.history-item.failed .retry-btn:hover {
+.history-item.failed .delete-btn:hover {
+  background: rgba(239, 68, 68, 1);
   transform: translateY(-2px);
-  box-shadow: 0 6px 20px rgba(102, 126, 234, 0.5);
 }
 
-.history-item.failed .retry-btn:active {
+.history-item.failed .delete-btn:active {
   transform: translateY(0);
 }
 
-.history-item.failed .retry-btn svg {
+.history-item.failed .delete-btn svg {
   flex-shrink: 0;
 }
 
-/* 已重试标记 */
-.history-item.failed .retried-badge {
-  margin-top: 8px;
-  padding: 4px 12px;
-  background: rgba(100, 100, 100, 0.6);
-  color: #999;
-  border-radius: 12px;
-  font-size: 11px;
-  font-weight: 500;
-}
-
 .history-item-time {
   position: absolute;
-  bottom: 4px;
-  left: 4px;
-  right: 4px;
-  font-size: 10px;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  font-size: 11px;
   color: #fff;
-  background: rgba(0, 0, 0, 0.6);
-  padding: 2px 6px;
-  border-radius: 4px;
+  background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
+  padding: 20px 10px 8px;
   text-align: center;
   font-weight: 500;
   z-index: 3;
-  backdrop-filter: blur(4px);
 }
 
 @keyframes spin {
@@ -523,73 +563,15 @@ html, body {
 .history-item-image {
   width: 100%;
   height: 100%;
-  object-fit: cover;
-}
-
-.history-item-overlay {
-  position: absolute;
-  top: 0;
-  left: 0;
-  right: 0;
-  bottom: 0;
-  background: rgba(0, 0, 0, 0.5);
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  opacity: 0;
-  transition: opacity 0.3s;
-}
-
-.history-item:hover .history-item-overlay {
-  opacity: 1;
-}
-
-/* 操作按钮组 */
-.history-item-actions {
-  display: flex;
-  gap: 12px;
-}
-
-.history-action-btn {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  gap: 6px;
-  padding: 12px 20px;
-  background: rgba(255, 255, 255, 0.95);
-  color: #667eea;
-  border: none;
-  border-radius: 12px;
-  cursor: pointer;
-  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
-  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
-  transform: translateY(10px);
-  opacity: 0;
-}
-
-.history-item:hover .history-action-btn {
-  transform: translateY(0);
-  opacity: 1;
-}
-
-.history-item:hover .history-action-btn:nth-child(2) {
-  transition-delay: 0.05s;
-}
-
-.history-action-btn span {
-  font-size: 12px;
-  font-weight: 600;
-}
-
-.history-action-btn:hover {
-  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-  color: white;
-  transform: translateY(-2px) scale(1.05) !important;
-  box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
+  object-fit: contain;
+  background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%);
+  padding: 12px;
+  cursor: zoom-in;
+  transition: transform 0.25s ease;
 }
 
-.history-action-btn:active {
-  transform: translateY(0) scale(0.98) !important;
+.history-item:hover .history-item-image {
+  transform: scale(1.03);
 }
 
 /* 图片预览弹窗 */
@@ -735,76 +717,29 @@ html, body {
 /* 购买记录样式 */
 .purchase-grid {
   display: grid;
-  grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
-  gap: 20px;
-  padding: 12px;
+  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
+  gap: 16px;
+  padding: 8px;
   overflow-y: auto;
   max-height: 100%;
 }
 
 .purchase-item {
   position: relative;
-  border-radius: 20px;
+  border-radius: 16px;
   overflow: hidden;
   background: white;
-  aspect-ratio: 0.82;
-  border: 1px solid rgba(102, 126, 234, 0.08);
-  transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+  border: 1px solid #e5e7eb;
+  transition: all 0.25s ease;
   display: flex;
   flex-direction: column;
-  box-shadow: 
-    0 4px 20px rgba(102, 126, 234, 0.08),
-    0 2px 8px rgba(0, 0, 0, 0.04);
-}
-
-.purchase-item::before {
-  content: '';
-  position: absolute;
-  top: 0;
-  left: 0;
-  right: 0;
-  height: 4px;
-  background: linear-gradient(90deg, #667eea 0%, #764ba2 40%, #f093fb 70%, #667eea 100%);
-  background-size: 200% 100%;
-  opacity: 0;
-  transition: opacity 0.3s ease;
-}
-
-.purchase-item::after {
-  content: '';
-  position: absolute;
-  top: 8px;
-  right: 8px;
-  width: 28px;
-  height: 28px;
-  background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
-  border-radius: 50%;
-  opacity: 0;
-  transition: all 0.3s ease;
-  z-index: 1;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
 }
 
 .purchase-item:hover {
-  transform: translateY(-8px);
-  border-color: rgba(102, 126, 234, 0.2);
-  box-shadow: 
-    0 24px 48px rgba(102, 126, 234, 0.18),
-    0 12px 24px rgba(0, 0, 0, 0.06);
-}
-
-.purchase-item:hover::before {
-  opacity: 1;
-  animation: shimmer 2s ease-in-out infinite;
-}
-
-.purchase-item:hover::after {
-  opacity: 1;
-  transform: scale(1.2);
-}
-
-@keyframes shimmer {
-  0% { background-position: 200% 0; }
-  100% { background-position: -200% 0; }
+  transform: translateY(-4px);
+  border-color: #667eea;
+  box-shadow: 0 12px 24px rgba(102, 126, 234, 0.15);
 }
 
 .purchase-item.deleted {
@@ -814,39 +749,34 @@ html, body {
 
 .purchase-item-image {
   width: 100%;
-  height: 62%;
+  height: 140px;
   object-fit: contain;
-  background: linear-gradient(145deg, #f8fafc 0%, #eef2f7 50%, #e8ecf2 100%);
-  padding: 16px;
-  transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
-  position: relative;
+  background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%);
+  padding: 20px;
+  transition: transform 0.3s ease;
 }
 
 .purchase-item:hover .purchase-item-image {
-  transform: scale(1.08);
-  filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.1));
+  transform: scale(1.05);
 }
 
 .purchase-item-placeholder {
   width: 100%;
-  height: 62%;
+  height: 140px;
   display: flex;
   align-items: center;
   justify-content: center;
-  background: linear-gradient(145deg, #f8fafc 0%, #eef2f7 100%);
+  background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%);
   color: #9ca3af;
   font-size: 32px;
 }
 
 .purchase-item-info {
-  padding: 14px 16px;
-  flex: 1;
+  padding: 14px 16px 16px;
   display: flex;
   flex-direction: column;
-  justify-content: center;
-  background: linear-gradient(180deg, #ffffff 0%, #fafbfc 100%);
-  border-top: 1px solid rgba(102, 126, 234, 0.06);
-  position: relative;
+  gap: 8px;
+  border-top: 1px solid #f1f5f9;
 }
 
 .purchase-item-overlay {
@@ -911,55 +841,48 @@ html, body {
 
 .purchase-item-name {
   font-size: 14px;
-  font-weight: 700;
+  font-weight: 600;
   color: #1f2937;
-  margin-bottom: 6px;
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
-  letter-spacing: -0.2px;
-  line-height: 1.3;
+  line-height: 1.4;
 }
 
 .purchase-item-category {
   font-size: 11px;
-  color: #6b7280;
-  margin-bottom: 8px;
+  color: #667eea;
   display: inline-flex;
   align-items: center;
-  gap: 6px;
-  background: linear-gradient(135deg, rgba(102, 126, 234, 0.08) 0%, rgba(118, 75, 162, 0.08) 100%);
-  padding: 3px 10px;
-  border-radius: 12px;
+  gap: 4px;
+  background: rgba(102, 126, 234, 0.1);
+  padding: 3px 8px;
+  border-radius: 4px;
   width: fit-content;
 }
 
 .purchase-item-category::before {
   content: '';
   display: inline-block;
-  width: 5px;
-  height: 5px;
-  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  width: 4px;
+  height: 4px;
+  background: #667eea;
   border-radius: 50%;
   flex-shrink: 0;
 }
 
 .purchase-item-price {
-  font-size: 13px;
-  font-weight: 800;
-  color: transparent;
-  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-  -webkit-background-clip: text;
-  background-clip: text;
+  font-size: 14px;
+  font-weight: 700;
+  color: #10b981;
   display: inline-flex;
   align-items: center;
-  gap: 3px;
-  letter-spacing: -0.3px;
+  gap: 2px;
 }
 
 .purchase-item-price::before {
-  content: '💎';
-  font-size: 11px;
+  content: '💰';
+  font-size: 12px;
 }
 
 .purchase-item-deleted-badge {
@@ -1012,3 +935,168 @@ 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;
+}
+
+@keyframes overlayFadeIn {
+  from {
+    background: rgba(0, 0, 0, 0);
+    backdrop-filter: blur(0);
+  }
+  to {
+    background: rgba(0, 0, 0, 0.7);
+    backdrop-filter: blur(4px);
+  }
+}
+
+.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;
+}
+
+@keyframes modalSlideIn {
+  from {
+    opacity: 0;
+    transform: translateY(-30px) scale(0.95);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0) scale(1);
+  }
+}
+
+.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 {
+  width: 32px;
+  height: 32px;
+  flex-shrink: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #667eea;
+}
+
+.download-option-icon svg {
+  width: 100%;
+  height: 100%;
+}
+
+.download-option-info {
+  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;
+}
+
+.download-option-price {
+  margin-top: 8px;
+  font-size: 16px;
+  font-weight: 700;
+  color: #667eea;
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
+  padding: 6px 12px;
+  border-radius: 8px;
+  display: inline-block;
+  border: 2px solid rgba(102, 126, 234, 0.3);
+  box-shadow: 0 2px 8px rgba(102, 126, 234, 0.15);
+  transition: all 0.3s ease;
+}
+
+.download-option:hover .download-option-price {
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.15) 0%, rgba(118, 75, 162, 0.15) 100%);
+  border-color: rgba(102, 126, 234, 0.5);
+  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.25);
+  transform: scale(1.05);
+}

+ 0 - 1
client/index.html

@@ -58,7 +58,6 @@
   <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>

+ 112 - 0
client/js/Index.js

@@ -1,3 +1,115 @@
+// 导出动画弹出框管理器
+window.ExportViewManager = (function() {
+  let frame = null;
+  let isShowing = false;
+  let resolveCallback = null;
+
+  function init() {
+    frame = document.getElementById('exportViewFrame');
+    if (!frame) {
+      return;
+    }
+
+    // 监听来自 export-view 的消息
+    window.addEventListener('message', (event) => {
+      if (event.origin !== window.location.origin) {
+        return;
+      }
+
+      const { data } = event;
+      if (data && data.type === 'close-export-view') {
+        hide();
+      } else if (data && data.type === 'export-confirmed') {
+        handleExportConfirmed(data);
+      }
+    });
+  }
+
+  function show(folderName) {
+    if (!frame) {
+      init();
+    }
+    
+    if (!frame) {
+      return Promise.resolve(false);
+    }
+
+    return new Promise((resolve) => {
+      resolveCallback = resolve;
+      isShowing = true;
+
+      // 显示 iframe
+      frame.style.display = 'block';
+      frame.style.pointerEvents = 'auto';
+      frame.style.visibility = 'visible';
+
+      // 等待 iframe 加载完成后发送文件夹名称
+      const sendFolderName = () => {
+        if (frame.contentWindow) {
+          frame.contentWindow.postMessage({
+            type: 'generate-export-preview',
+            folderName: folderName
+          }, '*');
+        } else {
+          setTimeout(sendFolderName, 100);
+        }
+      };
+
+      // 监听 iframe 加载完成
+      const handleLoad = () => {
+        setTimeout(() => {
+          sendFolderName();
+        }, 100);
+      };
+      
+      frame.onload = handleLoad;
+
+      // 如果 iframe 已经加载,立即发送
+      if (frame.contentDocument && frame.contentDocument.readyState === 'complete') {
+        handleLoad();
+      } else {
+        const baseSrc = frame.src.split('?')[0];
+        frame.src = baseSrc + '?t=' + Date.now();
+      }
+    });
+  }
+
+  function hide() {
+    if (frame) {
+      frame.style.display = 'none';
+      frame.style.pointerEvents = 'none';
+      frame.style.visibility = 'hidden';
+    }
+    
+    isShowing = false;
+    
+    if (resolveCallback) {
+      resolveCallback(false);
+      resolveCallback = null;
+    }
+  }
+
+  function handleExportConfirmed(data) {
+    if (resolveCallback) {
+      resolveCallback(true);
+      resolveCallback = null;
+    }
+    hide();
+  }
+
+  // 初始化
+  if (document.readyState === 'loading') {
+    document.addEventListener('DOMContentLoaded', init);
+  } else {
+    init();
+  }
+
+  return {
+    show,
+    hide
+  };
+})();
+
 // 全局 Loading 控制器
 window.GlobalLoading = (function() {
   let overlay = null;

+ 43 - 24
client/js/ai-generate/ai-generate-view.js

@@ -14,6 +14,7 @@ class AIGenerateView {
         this.referenceImageWrapper = null;
         this.referenceRemoveBtn = null;
         this.generateBtn = null;
+        this.isGenerateEnabled = false;
         this.additionalPromptInput = null;
         this.cancelBtn = null;
         this.imageData = null;
@@ -179,22 +180,11 @@ class AIGenerateView {
     loadReferenceImage(file) {
         const reader = new FileReader();
         reader.onload = (e) => {
-            this.referenceImageData = e.target.result;
-            if (this.referenceImage) {
-                this.referenceImage.src = e.target.result;
-            }
-            if (this.referenceImageWrapper) {
-                this.referenceImageWrapper.style.display = 'flex';
-            }
-            if (this.referenceUploadArea) {
-                this.referenceUploadArea.classList.add('hide');
-            }
-            
-            // 显示提示词配置区域
-            const promptConfigSection = document.getElementById('promptConfigSection');
-            if (promptConfigSection) {
-                promptConfigSection.style.display = 'flex';
-            }
+            const data = e.target.result;
+            this.referenceImageData = data;
+            this.showReferenceImage(data);
+
+            this.setGenerateButtonState(true);
         };
         reader.readAsDataURL(file);
     }
@@ -218,10 +208,17 @@ class AIGenerateView {
             this.referenceInput.value = '';
         }
 
-        // 隐藏提示词配置区域
-        const promptConfigSection = document.getElementById('promptConfigSection');
-        if (promptConfigSection) {
-            promptConfigSection.style.display = 'none';
+        this.setGenerateButtonState(false);
+    }
+
+    /**
+     * 设置生图按钮状态
+     * @param {boolean} enabled
+     */
+    setGenerateButtonState(enabled) {
+        this.isGenerateEnabled = !!enabled;
+        if (this.generateBtn) {
+            this.generateBtn.disabled = !enabled;
         }
     }
 
@@ -291,8 +288,9 @@ class AIGenerateView {
 
         try {
             // 准备图片数据
-            const image1Base64 = this.originalSpritesheetData.replace(/^data:image\/\w+;base64,/, '');
-            const image2Base64 = this.referenceImageData.replace(/^data:image\/\w+;base64,/, '');
+            // image1 直接使用参考图,image2 使用原始spritesheet
+            const image1Base64 = this.referenceImageData.replace(/^data:image\/\w+;base64,/, '');
+            const image2Base64 = this.originalSpritesheetData.replace(/^data:image\/\w+;base64,/, '');
             const image1Width = this.spritesheetLayout?.sheetWidth || 0;
             const image1Height = this.spritesheetLayout?.sheetHeight || 0;
             const additionalPrompt = this.additionalPromptInput?.value || '';
@@ -761,14 +759,35 @@ class AIGenerateView {
 
         const promptConfigSection = document.getElementById('promptConfigSection');
         if (promptConfigSection) {
-            promptConfigSection.style.display = 'none';
+            promptConfigSection.style.display = 'flex';
         }
         if (this.additionalPromptInput) {
             this.additionalPromptInput.value = '';
         }
+
+        this.setGenerateButtonState(false);
     }
+
+    /**
+     * 显示参考图并隐藏上传区域
+     */
+    showReferenceImage(dataUrl) {
+        if (this.referenceImage) {
+            this.referenceImage.src = dataUrl;
+        }
+        if (this.referenceImageWrapper) {
+            this.referenceImageWrapper.style.display = 'flex';
+        }
+        if (this.referenceUploadArea) {
+            this.referenceUploadArea.classList.add('hide');
+        }
+        const promptConfigSection = document.getElementById('promptConfigSection');
+        if (promptConfigSection) {
+            promptConfigSection.style.display = 'flex';
+        }
+    }
+
 }
 
 // 初始化
 window.AIGenerateView = new AIGenerateView();
-

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

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

+ 150 - 8
client/js/export-view/export-view.js

@@ -17,6 +17,7 @@ class ExportView {
         this.replacedImageData = null;
         this.geminiOriginalImageData = null;
         this.originalSpritesheetData = null;
+        this.skipPreviewUI = false;
         
         // 下载确认对话框相关
         this.downloadConfirmOverlay = null;
@@ -119,17 +120,56 @@ class ExportView {
         // 监听来自父窗口的消息
         window.addEventListener('message', (event) => {
             if (event.data && event.data.type === 'show-export-preview') {
-                // console.log('[ExportView] 收到显示预览消息:', event.data);
-                this.showPreview(event.data.imageUrl || event.data.imageData);
+                const skipPreview = !!event.data.skipPreviewUI;
+                this.reset();
+                this.skipPreviewUI = skipPreview;
+                this.prepareDirectPreview(event.data.imageUrl || event.data.imageData, event.data.fileName);
             } else if (event.data && event.data.type === 'generate-export-preview') {
                 // console.log('[ExportView] 收到生成预览消息:', event.data);
                 this.reset();
+                this.skipPreviewUI = !!event.data.skipPreviewUI;
                 this.folderName = event.data.folderName;
                 this.generatePreview(event.data.folderName);
             }
         });
     }
 
+    /**
+     * 显示遮罩及弹窗元素
+     */
+    showOverlayUI() {
+        if (this.overlay) {
+            this.overlay.style.display = 'flex';
+        }
+        if (this.modal) {
+            this.modal.style.display = 'block';
+        }
+        if (this.cancelBtn) {
+            this.cancelBtn.style.display = 'block';
+        }
+        if (this.floatingAIBtn) {
+            this.floatingAIBtn.style.display = 'block';
+        }
+        if (this.confirmBtn) {
+            this.confirmBtn.style.display = 'block';
+        }
+    }
+
+    /**
+     * 加载图片
+     * @param {string} src - 图片地址或base64
+     * @returns {Promise<HTMLImageElement>}
+     */
+    loadImage(src) {
+        return new Promise((resolve, reject) => {
+            const img = new Image();
+            img.crossOrigin = 'anonymous';
+            img.onload = () => resolve(img);
+            img.onerror = () => reject(new Error('无法加载图片'));
+            img.src = src;
+        });
+    }
+
     /**
      * 生成预览图
      * @param {string} folderName - 文件夹名称
@@ -146,15 +186,37 @@ class ExportView {
         // 重置状态(确保每次打开都是全新状态)
         this.reset();
 
+        // 显示遮罩并重置预览区域
+        if (!this.skipPreviewUI) {
+            this.showOverlayUI();
+            if (this.previewPlaceholder) {
+                this.previewPlaceholder.style.display = 'flex';
+                this.previewPlaceholder.classList.remove('hide');
+            }
+            if (this.previewImage) {
+                this.previewImage.style.display = 'none';
+                this.previewImage.classList.remove('show');
+            }
+        } else {
+            if (this.overlay) {
+                this.overlay.style.display = 'none';
+            }
+            if (this.modal) {
+                this.modal.style.display = 'none';
+            }
+        }
+
         // 保存文件夹名称
         this.folderName = folderName;
 
         // 显示加载状态
-        if (this.previewPlaceholder) {
-            this.previewPlaceholder.classList.remove('hide');
-        }
-        if (this.previewImage) {
-            this.previewImage.classList.remove('show');
+        if (!this.skipPreviewUI) {
+            if (this.previewPlaceholder) {
+                this.previewPlaceholder.classList.remove('hide');
+            }
+            if (this.previewImage) {
+                this.previewImage.classList.remove('show');
+            }
         }
 
         try {
@@ -311,6 +373,77 @@ class ExportView {
         }
     }
 
+    /**
+     * 处理直接传入的图片预览(AI 历史等)
+     * @param {string} imageUrl - 图片URL或base64
+     * @param {string} fileName - 基础文件名
+     */
+    async prepareDirectPreview(imageUrl, fileName) {
+        if (!imageUrl) {
+            this.showAlert('没有可预览的图片');
+            return;
+        }
+
+        try {
+            if (!this.skipPreviewUI) {
+                this.showOverlayUI();
+                if (this.previewPlaceholder) {
+                    this.previewPlaceholder.style.display = 'flex';
+                    this.previewPlaceholder.classList.remove('hide');
+                }
+            }
+
+            const img = await this.loadImage(imageUrl);
+            const canvas = document.createElement('canvas');
+            canvas.width = img.width;
+            canvas.height = img.height;
+            const ctx = canvas.getContext('2d');
+            ctx.clearRect(0, 0, canvas.width, canvas.height);
+            ctx.drawImage(img, 0, 0);
+
+            this.spritesheetCanvas = canvas;
+            this.spritesheetLayout = {
+                layout: [{
+                    x: 0,
+                    y: 0,
+                    width: img.width,
+                    height: img.height,
+                    frameNum: 1
+                }],
+                sheetWidth: img.width,
+                sheetHeight: img.height
+            };
+
+            const safeName = (fileName || 'ai-image').toString().replace(/[^a-zA-Z0-9_-]/g, '_') || 'ai-image';
+            this.folderName = safeName;
+
+            this.originalSpritesheetData = canvas.toDataURL('image/png');
+            this.imageData = imageUrl;
+
+            if (!this.skipPreviewUI) {
+                if (this.previewImage) {
+                    this.previewImage.src = imageUrl;
+                    this.previewImage.style.display = 'block';
+                    this.previewImage.classList.add('show');
+                }
+                if (this.previewPlaceholder) {
+                    this.previewPlaceholder.style.display = 'none';
+                    this.previewPlaceholder.classList.add('hide');
+                }
+            }
+            if (this.confirmBtn) {
+                this.confirmBtn.disabled = false;
+            }
+
+            const delay = this.skipPreviewUI ? 50 : 200;
+            setTimeout(() => this.showDownloadConfirm(), delay);
+        } catch (error) {
+            console.error('[ExportView] 处理直接预览失败:', error);
+            this.showAlert(`加载预览失败: ${error.message}`);
+            this.close();
+        }
+    }
+
     /**
      * 计算宽高比
      * @param {number} width - 宽度
@@ -892,6 +1025,15 @@ class ExportView {
         
         // 清空所有数据
         this.reset();
+
+        // 隐藏界面元素
+        if (this.overlay) {
+            this.overlay.style.display = 'none';
+        }
+        if (this.modal) {
+            this.modal.style.display = 'none';
+        }
+        this.hideDownloadConfirm();
         
         // 通知父窗口关闭弹出框
         if (window.parent && window.parent !== window) {
@@ -939,6 +1081,7 @@ class ExportView {
         this.replacedImageData = null;
         this.geminiOriginalImageData = null;
         this.originalSpritesheetData = null;
+        this.skipPreviewUI = false;
         
         // 重置AI按钮状态
         if (this.floatingAIBtn) {
@@ -1056,4 +1199,3 @@ class ExportView {
 
 // 初始化
 window.ExportView = new ExportView();
-

+ 145 - 69
client/js/profile/profile.js

@@ -4,7 +4,24 @@
   let currentUsername = null;
   let currentUserInfo = null;
 
+  // 下载弹窗相关
+  let downloadConfirmOverlay = null;
+  let downloadConfirmClose = null;
+  let downloadOptions = null;
+  let pendingDownloadUrl = null;
+  let pendingDownloadFilename = null;
+  let exportViewFrame = null;
+
   function init() {
+    // 初始化下载弹窗元素
+    downloadConfirmOverlay = document.getElementById('downloadConfirmOverlay');
+    downloadConfirmClose = document.getElementById('downloadConfirmClose');
+    downloadOptions = document.querySelectorAll('.download-option');
+    exportViewFrame = document.getElementById('exportViewFrame');
+    
+    // 加载VIP抠图价格
+    loadVIPMattingPrice();
+    
     bindEvents();
     // 延迟加载,确保DOM完全渲染
     setTimeout(() => {
@@ -15,6 +32,36 @@
     }, 200);
   }
 
+  /**
+     * 加载VIP抠图价格
+     */
+  async function loadVIPMattingPrice() {
+    try {
+        const response = await fetch('/api/product-pricing');
+        if (response.ok) {
+            const result = await response.json();
+            if (result.success && result.products) {
+                const vipMattingProduct = result.products.find(p => p.id === 'vip-matting');
+                const priceEl = document.getElementById('vipMattingPrice');
+                if (vipMattingProduct && priceEl) {
+                    const price = vipMattingProduct.price || 0;
+                    if (price > 0) {
+                        priceEl.textContent = `${price} Ani币`;
+                    } else {
+                        priceEl.textContent = '免费';
+                    }
+                }
+            }
+        }
+    } catch (error) {
+        console.error('[ExportView] 加载VIP抠图价格失败:', error);
+        const priceEl = document.getElementById('vipMattingPrice');
+        if (priceEl) {
+            priceEl.textContent = '-';
+        }
+    }
+}
+
   function bindEvents() {
     // 返回按钮
     const backBtn = document.getElementById('backBtn');
@@ -74,6 +121,8 @@
       } else if (event.data && event.data.type === 'refresh-ai-history') {
         // 刷新AI历史(生图请求成功后触发)
         loadAIHistory();
+      } else if (event.data && (event.data.type === 'close-export-view' || event.data.type === 'export-confirmed')) {
+        hideExportViewFrame();
       }
     });
   }
@@ -123,6 +172,67 @@
     }
   }
 
+  // 显示导出视图 iframe
+  function showExportViewFrame() {
+    if (!exportViewFrame) return;
+    exportViewFrame.style.display = 'block';
+    exportViewFrame.style.pointerEvents = 'auto';
+    exportViewFrame.style.visibility = 'visible';
+  }
+
+  // 隐藏导出视图 iframe
+  function hideExportViewFrame() {
+    if (!exportViewFrame) return;
+    exportViewFrame.style.display = 'none';
+    exportViewFrame.style.pointerEvents = 'none';
+    exportViewFrame.style.visibility = 'hidden';
+  }
+
+  // 打开导出视图并传递图片数据
+  function openExportView(imageUrl, fileName, options = {}) {
+    if (!imageUrl) {
+      return;
+    }
+
+    if (!exportViewFrame) {
+      downloadImage(imageUrl, fileName || 'ai-image');
+      return;
+    }
+
+    showExportViewFrame();
+
+    const sendPreviewData = () => {
+      try {
+        exportViewFrame.contentWindow.postMessage({
+          type: 'show-export-preview',
+          imageUrl: imageUrl,
+          fileName: fileName || 'ai-image',
+          skipPreviewUI: !!options.skipPreviewUI
+        }, '*');
+      } catch (error) {
+        console.error('[Profile] 发送导出预览数据失败:', error);
+        hideExportViewFrame();
+        downloadImage(imageUrl, fileName || 'ai-image');
+      }
+    };
+
+    const handleLoad = () => {
+      setTimeout(sendPreviewData, 50);
+    };
+
+    if (exportViewFrame.contentDocument && exportViewFrame.contentDocument.readyState === 'complete') {
+      handleLoad();
+    } else {
+      exportViewFrame.addEventListener('load', handleLoad, { once: true });
+      let baseSrc = exportViewFrame.getAttribute('data-base-src');
+      if (!baseSrc) {
+        baseSrc = exportViewFrame.src.split('?')[0];
+        exportViewFrame.setAttribute('data-base-src', baseSrc);
+      }
+      exportViewFrame.src = `${baseSrc}?t=${Date.now()}`;
+    }
+  }
+
   // 加载用户信息
   async function loadUserInfo() {
     const username = getCurrentUsername();
@@ -335,22 +445,17 @@
     div.className = 'history-item';
     div.dataset.taskId = item.id;
     
-    // 格式化时间
+    // 格式化时间 - 显示具体年月日时分秒
     const formatTime = (dateString) => {
       if (!dateString) return '';
       const date = new Date(dateString);
-      const now = new Date();
-      const diff = now - date;
-      const minutes = Math.floor(diff / 60000);
-      const hours = Math.floor(diff / 3600000);
-      const days = Math.floor(diff / 86400000);
-      
-      if (minutes < 1) return '刚刚';
-      if (minutes < 60) return `${minutes}分钟前`;
-      if (hours < 24) return `${hours}小时前`;
-      if (days < 7) return `${days}天前`;
-      
-      return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
+      const year = date.getFullYear();
+      const month = String(date.getMonth() + 1).padStart(2, '0');
+      const day = String(date.getDate()).padStart(2, '0');
+      const hours = String(date.getHours()).padStart(2, '0');
+      const minutes = String(date.getMinutes()).padStart(2, '0');
+      const seconds = String(date.getSeconds()).padStart(2, '0');
+      return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
     };
     
     const timeText = formatTime(item.createdAt);
@@ -372,74 +477,45 @@
         <div class="history-item-time">${timeText}</div>
       `;
     } else if (item.status === 'completed' && item.imageUrl) {
+      div.classList.add('completed');
       div.innerHTML = `
         <img class="history-item-image" src="${item.imageUrl}" alt="AI生成图" onerror="this.src='../../static/default-avatar.png'">
-        <div class="history-item-overlay">
-          <div class="history-item-actions">
-            <button class="history-action-btn preview-btn" title="预览">
-              <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-                <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
-                <circle cx="12" cy="12" r="3"/>
-              </svg>
-              <span>预览</span>
-            </button>
-            <button class="history-action-btn download-btn" title="下载">
-              <svg width="22" height="22" viewBox="0 0 16 16" fill="none">
-                <path d="M8 11V3M8 11L5 8M8 11L11 8M3 13H13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-              </svg>
-              <span>下载</span>
-            </button>
-          </div>
-        </div>
         <div class="history-item-time">${timeText}</div>
       `;
       
-      // 点击预览按钮
-      const previewBtn = div.querySelector('.preview-btn');
-      if (previewBtn) {
-        previewBtn.addEventListener('click', (e) => {
-          e.stopPropagation();
-          showImagePreviewModal(item.imageUrl, item.id || 'ai-image');
-        });
-      }
+      const openPreview = (e) => {
+        e.stopPropagation();
+        showImagePreviewModal(item.imageUrl, item.id || 'ai-image');
+      };
       
-      // 点击下载按钮 - 直接下载PNG
-      const downloadBtn = div.querySelector('.download-btn');
-      if (downloadBtn) {
-        downloadBtn.addEventListener('click', (e) => {
-          e.stopPropagation();
-          downloadImage(item.imageUrl, item.id || 'ai-image');
-        });
+      const imageEl = div.querySelector('.history-item-image');
+      if (imageEl) {
+        imageEl.addEventListener('click', openPreview);
       }
+      div.addEventListener('click', openPreview);
     } else if (item.status === 'failed') {
       div.classList.add('failed');
       
-      // 如果已经重试过,显示"已重试"标记
-      const retryButtonHtml = item.retried 
-        ? `<div class="retried-badge">已重试</div>`
-        : `<button class="retry-btn" data-task-id="${item.id}">
-            <svg width="14" height="14" viewBox="0 0 16 16" fill="none">
-              <path d="M2 8C2 4.686 4.686 2 8 2C10.5 2 12.5 3.5 13.5 5.5M14 8C14 11.314 11.314 14 8 14C5.5 14 3.5 12.5 2.5 10.5M14 2V6H10M2 14V10H6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
-            </svg>
-            重新生图
-          </button>`;
-      
       div.innerHTML = `
-        ${previewUrl ? `<img class="history-item-preview failed-preview" src="${previewUrl}" alt="预览图" onerror="this.style.display='none'">` : ''}
         <div class="failed-content">
           <div class="failed-icon">❌</div>
           <div class="failed-text">生成失败</div>
-          ${retryButtonHtml}
+          <button class="delete-btn" data-task-id="${item.id}">
+            <svg width="14" height="14" viewBox="0 0 16 16" fill="none">
+              <path d="M2 4H14M5 4V2H11V4M6 7V12M10 7V12M3 4L4 14H12L13 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+            </svg>
+            删除
+          </button>
         </div>
         <div class="history-item-time">${timeText}</div>
       `;
       
-      // 绑定重试按钮事件
-      const retryBtn = div.querySelector('.retry-btn');
-      if (retryBtn) {
-        retryBtn.addEventListener('click', async (e) => {
+      // 绑定删除按钮事件
+      const deleteBtn = div.querySelector('.delete-btn');
+      if (deleteBtn) {
+        deleteBtn.addEventListener('click', async (e) => {
           e.stopPropagation();
-          await retryAIGeneration(item.id);
+          await deleteFailedTask(item.id);
         });
       }
     }
@@ -447,8 +523,8 @@
     return div;
   }
 
-  // 重新生图(免费)
-  async function retryAIGeneration(taskId) {
+  // 删除失败的任务
+  async function deleteFailedTask(taskId) {
     const username = getCurrentUsername();
     if (!username) {
       alert('请先登录');
@@ -473,11 +549,11 @@
         // 刷新历史列表
         loadAIHistory();
       } else {
-        alert('重试失败: ' + (result.error || '未知错误'));
+        alert('删除失败: ' + (result.error || '未知错误'));
       }
     } catch (error) {
-      console.error('[Profile] 重试失败:', error);
-      alert('重试失败: ' + error.message);
+      console.error('[Profile] 删除失败:', error);
+      alert('删除失败: ' + error.message);
     }
   }
 
@@ -525,7 +601,8 @@
     modal.querySelector('.image-preview-close').onclick = closeModal;
     modal.querySelector('.image-preview-download-btn').onclick = (e) => {
       e.stopPropagation();
-      downloadImage(imageUrl, filename);
+      closeModal();
+      openExportView(imageUrl, filename, { skipPreviewUI: true });
     };
     
     document.addEventListener('keydown', function escHandler(e) {
@@ -758,4 +835,3 @@
     init();
   }
 })();
-

+ 2 - 4
client/page/ai-generate/ai-generate-view.html

@@ -26,11 +26,10 @@
                         <input type="file" id="referenceInput" accept="image/*" style="display: none;">
                         <div class="reference-upload-area" id="referenceUploadArea">
                             <div class="upload-icon">+</div>
-                            <div class="upload-text">点击上传参考图</div>
                         </div>
                         <div class="reference-image-wrapper" id="referenceImageWrapper" style="display: none;">
                             <img id="referenceImage" alt="参考图" class="reference-image" />
-                            <button class="reference-remove-btn" id="referenceRemoveBtn" title="删除参考图">
+                            <button class="reference-remove-btn" id="referenceRemoveBtn" title="更换参考图">
                                 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
                                     <path d="M12 4L4 12M4 4l8 8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
                                 </svg>
@@ -73,7 +72,7 @@
                             <circle cx="10" cy="10" r="2" fill="currentColor"/>
                         </svg>
                         <span>AI生图</span>
-                        <span class="btn-price" id="aiGeneratePrice" style="margin-left: 8px; font-size: 12px; opacity: 0.9;">-</span>
+                        <span class="btn-price" id="aiGeneratePrice">-</span>
                     </button>
                 </div>
             </div>
@@ -83,4 +82,3 @@
     <script src="../../js/ai-generate/ai-generate-view.js"></script>
 </body>
 </html>
-

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

@@ -11,6 +11,66 @@
 <body>
   <!-- 充值界面 iframe -->
   <iframe id="rechargeViewFrame" src="../recharge-view/recharge-view.html" frameborder="0" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 1000004; border: none; background: transparent;"></iframe>
+  <!-- 导出下载 iframe -->
+  <iframe id="exportViewFrame" src="../export/export-view.html" frameborder="0" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 1000005; border: none; pointer-events: none; visibility: hidden; background: transparent;"></iframe>
+  <!-- 下载选择弹窗 -->
+  <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">
+            <svg width="32" height="32" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+              <path d="M14 2H6C5.46957 2 4.96086 2.21071 4.58579 2.58579C4.21071 2.96086 4 3.46957 4 4V20C4 20.5304 4.21071 21.0391 4.58579 21.4142C4.96086 21.7893 5.46957 22 6 22H18C18.5304 22 19.0391 21.7893 19.4142 21.4142C19.7893 21.0391 20 20.5304 20 20V8L14 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+              <path d="M14 2V8H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+              <path d="M16 13H8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+              <path d="M16 17H8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+              <path d="M10 9H9H8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+            </svg>
+          </div>
+          <div class="download-option-info">
+            <div class="download-option-title">源文件下载</div>
+            <div class="download-option-desc">直接下载原始图片,不进行抠图处理</div>
+          </div>
+        </div>
+        <div class="download-option" data-option="normal">
+          <div class="download-option-icon">
+            <svg width="32" height="32" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+              <path d="M6 7C6 7 8 9 10 11C12 13 12 15 12 15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+              <path d="M18 7C18 7 16 9 14 11C12 13 12 15 12 15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+              <circle cx="6" cy="7" r="2" stroke="currentColor" stroke-width="2" fill="none"/>
+              <circle cx="18" cy="7" r="2" stroke="currentColor" stroke-width="2" fill="none"/>
+              <path d="M12 15L12 21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+            </svg>
+          </div>
+          <div class="download-option-info">
+            <div class="download-option-title">普通抠图下载</div>
+            <div class="download-option-desc">使用 rembg 进行抠图处理</div>
+          </div>
+        </div>
+        <div class="download-option" data-option="vip">
+          <div class="download-option-icon">
+            <svg width="32" height="32" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+              <path d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="currentColor" fill-opacity="0.1"/>
+            </svg>
+          </div>
+          <div class="download-option-info">
+            <div class="download-option-title">VIP抠图下载</div>
+            <div class="download-option-desc">使用 BiRefNet 进行高质量抠图处理</div>
+            <div class="download-option-price" id="vipMattingPrice">-</div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+
   <div class="profile-container">
     <!-- 返回按钮 -->
     <div class="profile-header">
@@ -51,15 +111,18 @@
           </div>
 
           <!-- Ani币区域 -->
-          <div class="profile-section">
-            <h2 class="section-title">我的Ani币</h2>
+          <div class="profile-section points-card">
             <div class="points-section">
               <div class="points-display">
-                <span class="points-label">当前余额:</span>
-                <span class="points-value" id="pointsValue">0</span>
-                <span class="points-unit">Ani币</span>
+                <span class="points-label">账户余额</span>
+                <div class="points-value-row">
+                  <span class="points-value" id="pointsValue">0</span>
+                  <span class="points-unit">Ani币</span>
+                </div>
               </div>
-              <button class="recharge-btn" id="rechargeBtn">充值</button>
+              <button class="recharge-btn" id="rechargeBtn">
+                <span>充值</span>
+              </button>
             </div>
           </div>
 
@@ -95,4 +158,3 @@
   <script src="../../js/profile/profile.js"></script>
 </body>
 </html>
-

+ 0 - 52
server/ai-history.json

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

+ 58 - 166
server/ai-queue.js

@@ -2,13 +2,22 @@
 const fs = require('fs');
 const path = require('path');
 const ReplaceCharacterHandler = require('./replace-character');
+const { getDatabase } = require('./sql');
 
-const AI_HISTORY_FILE = path.join(__dirname, 'ai-history.json');
 const QUEUE_FILE = path.join(__dirname, 'ai-queue.json');
 
 // 队列状态
 let queue = [];
 let isProcessing = false;
+let dbInstance = null;
+
+// 获取数据库实例
+async function getDB() {
+  if (!dbInstance) {
+    dbInstance = await getDatabase();
+  }
+  return dbInstance;
+}
 
 // 初始化:加载队列
 function initQueue() {
@@ -32,30 +41,8 @@ function saveQueue() {
   }
 }
 
-// 加载AI历史
-function loadAIHistory() {
-  try {
-    if (fs.existsSync(AI_HISTORY_FILE)) {
-      const data = fs.readFileSync(AI_HISTORY_FILE, 'utf-8');
-      return JSON.parse(data);
-    }
-  } catch (error) {
-    console.error('[AIQueue] 加载历史失败:', error);
-  }
-  return {};
-}
-
-// 保存AI历史
-function saveAIHistory(history) {
-  try {
-    fs.writeFileSync(AI_HISTORY_FILE, JSON.stringify(history, null, 2), 'utf-8');
-  } catch (error) {
-    console.error('[AIQueue] 保存历史失败:', error);
-  }
-}
-
 // 添加任务到队列
-function addToQueue(username, taskData) {
+async function addToQueue(username, taskData) {
   const taskId = `ai_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
   const task = {
     id: taskId,
@@ -68,25 +55,13 @@ function addToQueue(username, taskData) {
   queue.push(task);
   saveQueue();
   
-  // 保存原始图片预览(用于模糊显示)
-  let previewUrl = null;
-  if (taskData.image1) {
-    previewUrl = savePreviewImage(username, taskId, taskData.image1);
-  }
-  
-  // 添加到历史记录
-  const history = loadAIHistory();
-  if (!history[task.username]) {
-    history[task.username] = [];
+  // 添加到历史记录(不保存参考图)
+  try {
+    const db = await getDB();
+    db.addAIHistory(taskId, username, task.status, null);
+  } catch (error) {
+    console.error('[AIQueue] 添加历史记录失败:', error);
   }
-  history[task.username].unshift({
-    id: taskId,
-    status: task.status,
-    createdAt: task.createdAt,
-    imageUrl: null,
-    previewUrl: previewUrl // 原始图片预览
-  });
-  saveAIHistory(history);
   
   // 如果队列为空且没有正在处理的任务,立即开始处理
   if (queue.length === 1 && !isProcessing) {
@@ -96,23 +71,6 @@ function addToQueue(username, taskData) {
   return taskId;
 }
 
-// 保存预览图片(原始texture的缩略图)
-function savePreviewImage(username, taskId, imageBase64) {
-  const usersDir = path.join(__dirname, 'users');
-  const userDir = path.join(usersDir, username.toLowerCase());
-  const aiDir = path.join(userDir, 'ai-images');
-  
-  // 确保目录存在
-  if (!fs.existsSync(aiDir)) {
-    fs.mkdirSync(aiDir, { recursive: true });
-  }
-  
-  const imagePath = path.join(aiDir, `${taskId}_preview.png`);
-  const imageBuffer = Buffer.from(imageBase64.replace(/^data:image\/\w+;base64,/, ''), 'base64');
-  fs.writeFileSync(imagePath, imageBuffer);
-  
-  return `/api/ai/preview?username=${encodeURIComponent(username)}&id=${encodeURIComponent(taskId)}`;
-}
 
 // 处理队列
 async function processQueue() {
@@ -329,38 +287,27 @@ function saveAIImage(username, taskId, imageBase64) {
 }
 
 // 更新任务状态
-function updateTaskStatus(taskId, status, imageUrl = null, error = null, retryData = null) {
-  const history = loadAIHistory();
-  
-  for (const username in history) {
-    const userHistory = history[username];
-    const task = userHistory.find(t => t.id === taskId);
-    if (task) {
-      task.status = status;
-      if (imageUrl) {
-        task.imageUrl = imageUrl;
-      }
-      if (error) {
-        task.error = error;
-      }
-      if (retryData) {
-        task.retryData = retryData; // 保存重试所需数据
-      }
-      if (status === 'completed' || status === 'failed') {
-        task.completedAt = new Date().toISOString();
-      }
-      break;
+async function updateTaskStatus(taskId, status, imageUrl = null, error = null, retryData = null) {
+  try {
+    const db = await getDB();
+    db.updateAITaskStatus(taskId, status, imageUrl, error);
+    if (retryData) {
+      db.updateAITaskRetryData(taskId, retryData);
     }
+  } catch (err) {
+    console.error('[AIQueue] 更新任务状态失败:', err);
   }
-  
-  saveAIHistory(history);
 }
 
 // 获取用户AI历史
-function getUserAIHistory(username) {
-  const history = loadAIHistory();
-  const normalizedUsername = username.toLowerCase();
-  return history[normalizedUsername] || [];
+async function getUserAIHistory(username) {
+  try {
+    const db = await getDB();
+    return db.getAIHistory(username);
+  } catch (error) {
+    console.error('[AIQueue] 获取用户AI历史失败:', error);
+    return [];
+  }
 }
 
 // 处理AI生图请求(队列版本)
@@ -377,7 +324,7 @@ function handleAIRequest(req, res) {
     body += chunk.toString();
   });
 
-  req.on('end', () => {
+  req.on('end', async () => {
     try {
       const data = JSON.parse(body);
       const { username, image1, image2, image1Width, image1Height, additionalPrompt } = data;
@@ -401,7 +348,7 @@ function handleAIRequest(req, res) {
       }
 
       // 添加到队列
-      const taskId = addToQueue(username, {
+      const taskId = await addToQueue(username, {
         image1,
         image2,
         image1Width,
@@ -433,83 +380,28 @@ function handleAIRequest(req, res) {
   });
 }
 
-// 重试失败的任务(免费)
-function retryTask(taskId, username) {
-  const history = loadAIHistory();
-  const userHistory = history[username.toLowerCase()];
-  
-  if (!userHistory) {
-    return { success: false, error: '用户历史不存在' };
-  }
-  
-  const task = userHistory.find(t => t.id === taskId);
-  if (!task) {
-    return { success: false, error: '任务不存在' };
-  }
-  
-  if (task.status !== 'failed') {
-    return { success: false, error: '只能重试失败的任务' };
-  }
-  
-  // 读取预览图作为 image1
-  const previewPath = path.join(__dirname, 'users', username.toLowerCase(), 'ai-images', `${taskId}_preview.png`);
-  if (!fs.existsSync(previewPath)) {
-    return { success: false, error: '预览图不存在,无法重试' };
-  }
-  
-  const previewData = fs.readFileSync(previewPath);
-  const image1Base64 = previewData.toString('base64');
-  
-  // 生成新任务ID
-  const newTaskId = `ai_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
-  
-  // 获取重试数据
-  const retryData = task.retryData || {};
-  
-  // 创建新任务
-  const newTask = {
-    id: newTaskId,
-    username: username.toLowerCase(),
-    status: queue.length === 0 && !isProcessing ? 'rendering' : 'queued',
-    createdAt: new Date().toISOString(),
-    image1: image1Base64,
-    image2: image1Base64, // 重试时使用同一张图
-    image1Width: retryData.image1Width || 512,
-    image1Height: retryData.image1Height || 512,
-    additionalPrompt: retryData.additionalPrompt || '',
-    isRetry: true,
-    originalTaskId: taskId
-  };
-  
-  queue.push(newTask);
-  saveQueue();
-  
-  // 保存预览图到新任务
-  const newPreviewPath = path.join(__dirname, 'users', username.toLowerCase(), 'ai-images', `${newTaskId}_preview.png`);
-  fs.copyFileSync(previewPath, newPreviewPath);
-  
-  // 添加到历史记录
-  userHistory.unshift({
-    id: newTaskId,
-    status: newTask.status,
-    createdAt: newTask.createdAt,
-    imageUrl: null,
-    previewUrl: `/api/ai/preview?username=${encodeURIComponent(username)}&id=${encodeURIComponent(newTaskId)}`,
-    isRetry: true
-  });
-  
-  // 将原任务标记为已重试
-  task.retried = true;
-  task.retriedTaskId = newTaskId;
-  
-  saveAIHistory(history);
-  
-  // 开始处理队列
-  if (queue.length === 1 && !isProcessing) {
-    processQueue();
+// 重试失败的任务 - 直接删除失败记录(不再支持重试,因为不保存参考图)
+async function retryTask(taskId, username) {
+  try {
+    const db = await getDB();
+    const task = db.getAITask(taskId);
+    
+    if (!task) {
+      return { success: false, error: '任务不存在' };
+    }
+    
+    if (task.status !== 'failed') {
+      return { success: false, error: '只能删除失败的任务' };
+    }
+    
+    // 直接删除失败的任务记录
+    db.deleteAITask(taskId);
+    
+    return { success: true, message: '已删除失败记录' };
+  } catch (error) {
+    console.error('[AIQueue] 删除任务失败:', error);
+    return { success: false, error: error.message };
   }
-  
-  return { success: true, taskId: newTaskId };
 }
 
 // 处理重试请求
@@ -520,7 +412,7 @@ function handleRetryRequest(req, res) {
     body += chunk.toString();
   });
   
-  req.on('end', () => {
+  req.on('end', async () => {
     try {
       const { taskId, username } = JSON.parse(body);
       
@@ -530,7 +422,7 @@ function handleRetryRequest(req, res) {
         return;
       }
       
-      const result = retryTask(taskId, username);
+      const result = await retryTask(taskId, username);
       
       res.writeHead(result.success ? 200 : 400, { 'Content-Type': 'application/json' });
       res.end(JSON.stringify(result));

Файловите разлики са ограничени, защото са твърде много
+ 6 - 1
server/ai-queue.json


BIN
server/data.db


+ 135 - 1
server/sql.js

@@ -53,7 +53,7 @@ class DatabaseManager {
       this.db.exec(createUsersTable);
       console.log('[SQL] users表已创建或已存在');
     } catch (err) {
-      console.error('[SQL] 创建users表失败:', err);
+      console.error('[SQL] 创建表失败:', err);
       throw err;
     }
   }
@@ -153,6 +153,140 @@ class DatabaseManager {
     return { changes: result.changes };
   }
 
+  // ============ AI 历史记录操作(保存到用户目录) ============
+  
+  // 获取用户历史文件路径
+  _getHistoryFilePath(username) {
+    const userDir = path.join(__dirname, 'users', username.toLowerCase(), 'generate-history');
+    if (!fs.existsSync(userDir)) {
+      fs.mkdirSync(userDir, { recursive: true });
+    }
+    return path.join(userDir, 'history.json');
+  }
+  
+  // 加载用户历史
+  _loadUserHistory(username) {
+    const filePath = this._getHistoryFilePath(username);
+    try {
+      if (fs.existsSync(filePath)) {
+        return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
+      }
+    } catch (err) {
+      console.error('[SQL] 加载用户历史失败:', err);
+    }
+    return [];
+  }
+  
+  // 保存用户历史
+  _saveUserHistory(username, history) {
+    const filePath = this._getHistoryFilePath(username);
+    try {
+      fs.writeFileSync(filePath, JSON.stringify(history, null, 2), 'utf-8');
+    } catch (err) {
+      console.error('[SQL] 保存用户历史失败:', err);
+    }
+  }
+  
+  // 添加 AI 历史记录
+  addAIHistory(taskId, username, status, previewUrl, retryData = null, createdAt = null) {
+    const history = this._loadUserHistory(username);
+    const timestamp = createdAt || new Date().toISOString();
+    
+    history.unshift({
+      id: taskId,
+      status: status,
+      imageUrl: null,
+      previewUrl: previewUrl,
+      error: null,
+      isRetry: false,
+      retryData: retryData,
+      createdAt: timestamp,
+      completedAt: null
+    });
+    
+    this._saveUserHistory(username, history);
+    return history.length;
+  }
+  
+  // 获取用户 AI 历史记录
+  getAIHistory(username) {
+    return this._loadUserHistory(username);
+  }
+  
+  // 更新 AI 任务状态
+  updateAITaskStatus(taskId, status, imageUrl = null, error = null) {
+    // 需要找到任务所属用户
+    const usersDir = path.join(__dirname, 'users');
+    if (!fs.existsSync(usersDir)) return false;
+    
+    const users = fs.readdirSync(usersDir);
+    for (const username of users) {
+      const history = this._loadUserHistory(username);
+      const task = history.find(t => t.id === taskId);
+      if (task) {
+        task.status = status;
+        if (imageUrl) task.imageUrl = imageUrl;
+        if (error) task.error = error;
+        task.completedAt = new Date().toISOString();
+        this._saveUserHistory(username, history);
+        return true;
+      }
+    }
+    return false;
+  }
+  
+  // 获取 AI 任务
+  getAITask(taskId) {
+    const usersDir = path.join(__dirname, 'users');
+    if (!fs.existsSync(usersDir)) return null;
+    
+    const users = fs.readdirSync(usersDir);
+    for (const username of users) {
+      const history = this._loadUserHistory(username);
+      const task = history.find(t => t.id === taskId);
+      if (task) {
+        return { ...task, username };
+      }
+    }
+    return null;
+  }
+  
+  // 删除 AI 任务
+  deleteAITask(taskId) {
+    const usersDir = path.join(__dirname, 'users');
+    if (!fs.existsSync(usersDir)) return false;
+    
+    const users = fs.readdirSync(usersDir);
+    for (const username of users) {
+      const history = this._loadUserHistory(username);
+      const index = history.findIndex(t => t.id === taskId);
+      if (index !== -1) {
+        history.splice(index, 1);
+        this._saveUserHistory(username, history);
+        return true;
+      }
+    }
+    return false;
+  }
+  
+  // 更新 AI 任务重试数据
+  updateAITaskRetryData(taskId, retryData) {
+    const usersDir = path.join(__dirname, 'users');
+    if (!fs.existsSync(usersDir)) return false;
+    
+    const users = fs.readdirSync(usersDir);
+    for (const username of users) {
+      const history = this._loadUserHistory(username);
+      const task = history.find(t => t.id === taskId);
+      if (task) {
+        task.retryData = retryData;
+        this._saveUserHistory(username, history);
+        return true;
+      }
+    }
+    return false;
+  }
+
   // 关闭数据库连接
   close() {
     if (this.db) {

+ 1 - 1
server/user.js

@@ -242,7 +242,7 @@ async function handleGetAIHistory(req, res) {
   
   try {
     const { getUserAIHistory } = require('./ai-queue');
-    const history = getUserAIHistory(username);
+    const history = await getUserAIHistory(username);
     
     res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
     res.end(JSON.stringify({

BIN
server/users/yichael/ai-images/ai_1765560074612_twqt5peep_preview.png


BIN
server/users/yichael/ai-images/ai_1765560102494_e2omjm6p9_preview.png


BIN
server/users/yichael/ai-images/ai_1765560852619_oe2dgxfph.png


BIN
server/users/yichael/ai-images/ai_1765560852619_oe2dgxfph_preview.png


BIN
server/users/yichael/ai-images/ai_1765562471902_n8wnmu31o.png


BIN
server/users/yichael/ai-images/ai_1765562471902_n8wnmu31o_preview.png


BIN
server/users/yichael/ai-images/ai_1765564852659_ponpnyjr7.png


BIN
server/users/yichael/ai-images/ai_1765564852659_ponpnyjr7_preview.png


+ 24 - 0
server/users/yichael/generate-history/history.json

@@ -0,0 +1,24 @@
+[
+  {
+    "id": "ai_1765567312624_q2xdyx8su",
+    "status": "rendering",
+    "imageUrl": null,
+    "previewUrl": null,
+    "error": null,
+    "isRetry": false,
+    "retryData": null,
+    "createdAt": "2025-12-12T19:21:52.646Z",
+    "completedAt": "2025-12-12T19:22:45.913Z"
+  },
+  {
+    "id": "ai_1765567010788_d6spkgx4o",
+    "status": "queued",
+    "imageUrl": null,
+    "previewUrl": null,
+    "error": null,
+    "isRetry": false,
+    "retryData": null,
+    "createdAt": "2025-12-12T19:16:50.799Z",
+    "completedAt": "2025-12-12T19:22:45.896Z"
+  }
+]

Някои файлове не бяха показани, защото твърде много файлове са промени