Przeglądaj źródła

feat: 优化有声书创建流程界面交互

- 优化有声书创建流程界面交互
- 新增积分不足弹窗组件及微信登录入口
- 新增PixelleVideo模块(视频生成)
- 分享/邀请模块优化

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
MyFramework User 3 miesięcy temu
rodzic
commit
6ef8b368b7
31 zmienionych plików z 2852 dodań i 2840 usunięć
  1. 202 48
      my-uniapp-vue3/src/App.vue
  2. 52 0
      my-uniapp-vue3/src/components/GenerationStatusBadge.vue
  3. 38 22
      my-uniapp-vue3/src/components/MiniPlayer.vue
  4. 12 1
      my-uniapp-vue3/src/pages.json
  5. 103 51
      my-uniapp-vue3/src/pages/book-generator/chapter-detail.vue
  6. 437 490
      my-uniapp-vue3/src/pages/book-generator/create.vue
  7. 123 143
      my-uniapp-vue3/src/pages/book-generator/detail.vue
  8. 91 36
      my-uniapp-vue3/src/pages/book-generator/index.vue
  9. 19 11
      my-uniapp-vue3/src/pages/book-generator/interactive.vue
  10. 380 1420
      my-uniapp-vue3/src/pages/create/index.vue
  11. 18 5
      my-uniapp-vue3/src/pages/history/index.vue
  12. 258 296
      my-uniapp-vue3/src/pages/index/index.vue
  13. 24 6
      my-uniapp-vue3/src/pages/login/index.vue
  14. 171 249
      my-uniapp-vue3/src/pages/mine/index.vue
  15. 457 0
      my-uniapp-vue3/src/pages/pixelle-video/index.vue
  16. 12 4
      my-uniapp-vue3/src/pages/player/index.vue
  17. 8 1
      server/.env.example
  18. 2 0
      server/src/app.ts
  19. 4 0
      server/src/modules/auth/auth.service.ts
  20. 5 0
      server/src/modules/book-generator/album-controller.ts
  21. 76 27
      server/src/modules/book-generator/book-generator.service.ts
  22. 23 1
      server/src/modules/book-generator/book-generator.store.ts
  23. 16 2
      server/src/modules/invite/invite.controller.ts
  24. 8 7
      server/src/modules/invite/invite.service.ts
  25. 7 0
      server/src/modules/pixelle-video/index.ts
  26. 68 0
      server/src/modules/pixelle-video/pixelle-video.controller.ts
  27. 85 0
      server/src/modules/pixelle-video/pixelle-video.service.ts
  28. 34 0
      server/src/modules/pixelle-video/pixelle-video.types.ts
  29. 1 1
      server/src/modules/share/share.controller.ts
  30. 112 16
      server/src/modules/subscription/subscription.service.ts
  31. 6 3
      server/src/services/ai-call-logger.ts

+ 202 - 48
my-uniapp-vue3/src/App.vue

@@ -223,25 +223,128 @@ function applyDarkMode() {
 </template>
 
 <style>
-/* 全局样式 */
+/* ============================================================
+   全局设计系统 - Global Design System
+   ============================================================ */
+
+/* --- CSS 设计令牌 (Design Tokens) --- */
+:root {
+  /* 主色调 - Indigo */
+  --color-primary: #4f46e5;
+  --color-primary-400: #818cf8;
+  --color-primary-500: #6366f1;
+  --color-primary-600: #4f46e5;
+  --color-primary-700: #4338ca;
+  --color-primary-800: #3730a3;
+
+  /* 强调色 - Amber/Orange */
+  --color-accent: #f97316;
+  --color-accent-400: #fb923c;
+  --color-accent-500: #f97316;
+
+  /* 语义色 */
+  --color-success: #10b981;
+  --color-success-bg: #ecfdf5;
+  --color-warning: #f59e0b;
+  --color-warning-bg: #fffbeb;
+  --color-error: #ef4444;
+  --color-error-bg: #fef2f2;
+  --color-info: #3b82f6;
+  --color-info-bg: #eff6ff;
+
+  /* 中性色 */
+  --color-text-primary: #111827;
+  --color-text-secondary: #4b5563;
+  --color-text-tertiary: #9ca3af;
+  --color-text-placeholder: #d1d5db;
+  --color-text-inverse: #ffffff;
+
+  /* 背景色 */
+  --color-bg-page: #f3f4f6;
+  --color-bg-card: #ffffff;
+  --color-bg-surface: #f9fafb;
+  --color-bg-hover: #f3f4f6;
+  --color-bg-divider: #e5e7eb;
+
+  /* 阴影系统 */
+  --shadow-xs: 0 1rpx 2rpx rgba(0, 0, 0, 0.04);
+  --shadow-sm: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
+  --shadow-md: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
+  --shadow-lg: 0 8rpx 32rpx rgba(0, 0, 0, 0.12);
+  --shadow-xl: 0 16rpx 48rpx rgba(0, 0, 0, 0.16);
+  --shadow-primary: 0 4rpx 20rpx rgba(79, 70, 229, 0.25);
+
+  /* 圆角系统 */
+  --radius-xs: 6rpx;
+  --radius-sm: 10rpx;
+  --radius-md: 16rpx;
+  --radius-lg: 24rpx;
+  --radius-xl: 32rpx;
+  --radius-full: 9999rpx;
+
+  /* 间距系统 */
+  --space-1: 4rpx;
+  --space-2: 8rpx;
+  --space-3: 12rpx;
+  --space-4: 16rpx;
+  --space-5: 20rpx;
+  --space-6: 24rpx;
+  --space-8: 32rpx;
+  --space-10: 40rpx;
+  --space-12: 48rpx;
+
+  /* 字体 */
+  --font-sans: 'PingFang SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
+  --font-mono: 'SF Mono', 'Cascadia Code', 'Fira Code', monospace;
+
+  /* 字体大小 */
+  --text-xs: 20rpx;
+  --text-sm: 24rpx;
+  --text-base: 28rpx;
+  --text-lg: 32rpx;
+  --text-xl: 36rpx;
+  --text-2xl: 44rpx;
+  --text-3xl: 56rpx;
+
+  /* 过渡 */
+  --transition-fast: 0.15s cubic-bezier(0.4, 0, 0.2, 1);
+  --transition-base: 0.25s cubic-bezier(0.4, 0, 0.2, 1);
+  --transition-slow: 0.35s cubic-bezier(0.4, 0, 0.2, 1);
+
+  /* 兼容旧变量名 */
+  --primary-color: var(--color-primary);
+  --primary-light: var(--color-primary-500);
+  --primary-lighter: var(--color-primary-400);
+  --accent-color: var(--color-accent);
+  --success-color: var(--color-success);
+  --error-color: var(--color-error);
+  --text-primary: var(--color-text-primary);
+  --text-secondary: var(--color-text-secondary);
+  --bg-primary: var(--color-bg-page);
+  --bg-card: var(--color-bg-card);
+}
+
+/* --- 页面基础 --- */
 page {
-  background-color: #f9fafb;
-  font-family: 'PingFang SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+  background-color: var(--color-bg-page);
+  font-family: var(--font-sans);
   font-size: 14px;
-  color: #1f2937;
+  color: var(--color-text-primary);
   min-height: 100vh;
-  transition: background-color 0.3s, color 0.3s;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  transition: background-color var(--transition-base), color var(--transition-base);
 }
 
-/* 全局页面切换动画 - 滑入效果 */
+/* --- 页面过渡动画 --- */
 page {
-  animation: pageIn 0.3s ease-out;
+  animation: pageIn 0.28s cubic-bezier(0.22, 0.61, 0.36, 1);
 }
 
 @keyframes pageIn {
   from {
-    opacity: 0.8;
-    transform: translateX(30px);
+    opacity: 0;
+    transform: translateX(16px);
   }
   to {
     opacity: 1;
@@ -249,66 +352,117 @@ page {
   }
 }
 
-/* 列表项入场动画 */
-@keyframes listItemIn {
-  from {
-    opacity: 0;
-    transform: translateY(20px);
-  }
-  to {
-    opacity: 1;
-    transform: translateY(0);
-  }
+/* --- 通用入场动画 --- */
+@keyframes fadeInUp {
+  from { opacity: 0; transform: translateY(24px); }
+  to { opacity: 1; transform: translateY(0); }
+}
+
+@keyframes fadeIn {
+  from { opacity: 0; }
+  to { opacity: 1; }
+}
+
+@keyframes scaleIn {
+  from { opacity: 0; transform: scale(0.92); }
+  to { opacity: 1; transform: scale(1); }
+}
+
+@keyframes shimmer {
+  0% { background-position: -200% 0; }
+  100% { background-position: 200% 0; }
+}
+
+@keyframes pulse-ring {
+  0% { box-shadow: 0 0 0 0 rgba(79, 70, 229, 0.4); }
+  70% { box-shadow: 0 0 0 12rpx rgba(79, 70, 229, 0); }
+  100% { box-shadow: 0 0 0 0 rgba(79, 70, 229, 0); }
 }
 
 .list-item-enter {
-  animation: listItemIn 0.4s ease-out forwards;
+  animation: fadeInUp 0.4s cubic-bezier(0.22, 0.61, 0.36, 1) forwards;
 }
 
-/* 按钮点击反馈 */
+/* --- 交互反馈 --- */
 button:active,
-.btn:active,
-.action-btn:active,
-.generate-btn:active {
-  transform: scale(0.96);
-  transition: transform 0.1s ease;
+.btn:active {
+  transform: scale(0.97);
+  transition: transform 0.12s cubic-bezier(0.4, 0, 0.2, 1);
 }
 
-/* 触摸反馈元素 */
 .touch-feedback:active {
-  transform: scale(0.96);
-  transition: transform 0.1s ease;
-  opacity: 0.9;
+  transform: scale(0.97);
+  transition: transform 0.12s cubic-bezier(0.4, 0, 0.2, 1);
+  opacity: 0.85;
 }
 
-/* 主色调 */
-:root {
-  --primary-color: #4f46e5;
-  --primary-light: #6366f1;
-  --primary-lighter: #818cf8;
-  --accent-color: #f97316;
-  --success-color: #10b981;
-  --error-color: #ef4444;
-  --text-primary: #1f2937;
-  --text-secondary: #6b7280;
-  --bg-primary: #f9fafb;
-  --bg-card: #ffffff;
+/* 通用点击态 */
+.tappable {
+  transition: transform var(--transition-fast), opacity var(--transition-fast);
+}
+.tappable:active {
+  transform: scale(0.97);
+  opacity: 0.8;
+}
+
+/* --- 渐变工具类 --- */
+.gradient-primary {
+  background: linear-gradient(135deg, var(--color-primary-600), var(--color-primary-700));
+}
+.gradient-accent {
+  background: linear-gradient(135deg, var(--color-accent), var(--color-accent-400));
+}
+.gradient-success {
+  background: linear-gradient(135deg, #059669, var(--color-success));
 }
 
-/* 夜间模式 */
+/* --- 毛玻璃效果 --- */
+.glass {
+  background: rgba(255, 255, 255, 0.85);
+  backdrop-filter: blur(20px);
+  -webkit-backdrop-filter: blur(20px);
+}
+
+.glass-dark {
+  background: rgba(17, 24, 39, 0.85);
+  backdrop-filter: blur(20px);
+  -webkit-backdrop-filter: blur(20px);
+}
+
+/* --- 夜间模式 --- */
 page.dark-mode {
-  background-color: #111827;
-  color: #ffffff;
+  --color-bg-page: #0f172a;
+  --color-bg-card: #1e293b;
+  --color-bg-surface: #1a2332;
+  --color-bg-hover: #334155;
+  --color-bg-divider: #334155;
+  --color-text-primary: #f1f5f9;
+  --color-text-secondary: #94a3b8;
+  --color-text-tertiary: #64748b;
+  background-color: #0f172a;
+  color: #f1f5f9;
+}
+
+/* --- 工具类 --- */
+.text-ellipsis {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.text-clamp-2 {
+  display: -webkit-box;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+  overflow: hidden;
 }
 
-/* 禁止选择文本 */
 text {
   user-select: none;
 }
 
-/* 安全区域 */
 .safe-area-bottom {
   padding-bottom: constant(safe-area-inset-bottom);
   padding-bottom: env(safe-area-inset-bottom);
 }
-</style>
+</style>

+ 52 - 0
my-uniapp-vue3/src/components/GenerationStatusBadge.vue

@@ -80,6 +80,7 @@ const label = computed(() => display.value.label);
   font-weight: 500;
   white-space: nowrap;
   transition: all 0.3s ease;
+  user-select: none;
 }
 
 .gen-icon {
@@ -176,4 +177,55 @@ const label = computed(() => display.value.label);
   from { transform: rotate(0deg); }
   to { transform: rotate(360deg); }
 }
+
+/* ===== 暗色模式 ===== */
+.dark-mode .gen-badge.idle,
+.dark-mode .gen-badge.pending,
+.dark-mode .gen-badge.paused,
+.dark-mode .gen-badge.queued {
+  background: rgba(255, 255, 255, 0.08);
+  color: #9ca3af;
+}
+
+.dark-mode .gen-badge.generating,
+.dark-mode .gen-badge.processing,
+.dark-mode .gen-badge.running,
+.dark-mode .gen-badge.uploading {
+  background: rgba(251, 191, 36, 0.15);
+  color: #fbbf24;
+}
+
+.dark-mode .gen-badge.retrying {
+  background: rgba(251, 146, 60, 0.15);
+  color: #fb923c;
+}
+
+.dark-mode .gen-badge.completed,
+.dark-mode .gen-badge.success,
+.dark-mode .gen-badge.paid {
+  background: rgba(52, 211, 153, 0.15);
+  color: #34d399;
+}
+
+.dark-mode .gen-badge.failed {
+  background: rgba(248, 113, 113, 0.15);
+  color: #f87171;
+}
+
+.dark-mode .gen-badge.cancelled,
+.dark-mode .gen-badge.interrupted,
+.dark-mode .gen-badge.refunded {
+  background: rgba(255, 255, 255, 0.06);
+  color: #6b7280;
+}
+
+.dark-mode .gen-badge.draft {
+  background: rgba(255, 255, 255, 0.06);
+  color: #9ca3af;
+}
+
+.dark-mode .gen-badge.planning {
+  background: rgba(129, 140, 248, 0.15);
+  color: #a5b4fc;
+}
 </style>

+ 38 - 22
my-uniapp-vue3/src/components/MiniPlayer.vue

@@ -82,41 +82,55 @@ function goToPlayer() {
   bottom: 120rpx;
   left: 16rpx;
   right: 16rpx;
-  height: 100rpx;
-  background: #ffffff;
-  border-radius: 50rpx;
-  box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.15);
+  height: 96rpx;
+  background: rgba(255, 255, 255, 0.9);
+  backdrop-filter: blur(28px);
+  -webkit-backdrop-filter: blur(28px);
+  border-radius: 48rpx;
+  box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.12), 0 0 0 0.5rpx rgba(0, 0, 0, 0.06);
   display: flex;
   align-items: center;
-  padding: 0 20rpx;
+  padding: 0 16rpx;
   z-index: 999;
+  transition: transform 0.3s cubic-bezier(0.22, 0.61, 0.36, 1);
+  animation: miniPlayerIn 0.4s cubic-bezier(0.22, 0.61, 0.36, 1);
+}
+
+@keyframes miniPlayerIn {
+  from { opacity: 0; transform: translateY(20px); }
+  to { opacity: 1; transform: translateY(0); }
+}
+
+.mini-player:active {
+  transform: scale(0.98);
 }
 
 .mini-cover {
-  width: 72rpx;
-  height: 72rpx;
-  border-radius: 16rpx;
+  width: 68rpx;
+  height: 68rpx;
+  border-radius: 14rpx;
   display: flex;
   align-items: center;
   justify-content: center;
   flex-shrink: 0;
+  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
 }
 
 .cover-icon {
-  font-size: 32rpx;
-  opacity: 0.6;
+  font-size: 28rpx;
+  opacity: 0.7;
 }
 
 .mini-info {
   flex: 1;
-  margin-left: 20rpx;
+  margin-left: 16rpx;
   overflow: hidden;
 }
 
 .mini-title {
   font-size: 26rpx;
-  color: #1f2937;
-  font-weight: 500;
+  color: #111827;
+  font-weight: 600;
   display: block;
   overflow: hidden;
   text-overflow: ellipsis;
@@ -124,30 +138,32 @@ function goToPlayer() {
 }
 
 .mini-subtitle {
-  font-size: 22rpx;
+  font-size: 20rpx;
   color: #9ca3af;
-  margin-top: 4rpx;
+  margin-top: 2rpx;
   display: block;
 }
 
 .mini-controls {
   display: flex;
   align-items: center;
-  gap: 16rpx;
+  gap: 8rpx;
 }
 
 .control-btn {
-  width: 64rpx;
-  height: 64rpx;
+  width: 60rpx;
+  height: 60rpx;
   border-radius: 50%;
-  background: #f3f4f6;
+  background: rgba(79, 70, 229, 0.08);
   display: flex;
   align-items: center;
   justify-content: center;
-  font-size: 28rpx;
+  font-size: 26rpx;
+  color: #4f46e5;
+  transition: all 0.2s;
 }
-
 .control-btn:active {
-  background: #e5e7eb;
+  transform: scale(0.9);
+  background: rgba(79, 70, 229, 0.18);
 }
 </style>

+ 12 - 1
my-uniapp-vue3/src/pages.json

@@ -211,6 +211,13 @@
       "style": {
         "navigationStyle": "custom"
       }
+    },
+    {
+      "path": "pages/pixelle-video/index",
+      "style": {
+        "navigationBarTitleText": "AI视频生成",
+        "navigationStyle": "custom"
+      }
     }
   ],
   "globalStyle": {
@@ -234,9 +241,13 @@
         "text": "生成"
       },
       {
-        "pagePath": "pages/book-generator/index",
+        "pagePath": "pages/book-generator/create",
         "text": "生成书籍"
       },
+      {
+        "pagePath": "pages/pixelle-video/index",
+        "text": "视频生成"
+      },
       {
         "pagePath": "pages/mine/index",
         "text": "我的"

+ 103 - 51
my-uniapp-vue3/src/pages/book-generator/chapter-detail.vue

@@ -1125,7 +1125,7 @@ onUnmounted(() => {
 <style scoped>
 .page {
   min-height: 100vh;
-  background: #f9fafb;
+  background: var(--color-bg-page, #f3f4f6);
 }
 
 .nav-bar {
@@ -1134,8 +1134,10 @@ onUnmounted(() => {
   left: 0;
   right: 0;
   z-index: 100;
-  background: #ffffff;
-  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+  background: rgba(255, 255, 255, 0.88);
+  backdrop-filter: blur(24px);
+  -webkit-backdrop-filter: blur(24px);
+  border-bottom: 0.5rpx solid rgba(0, 0, 0, 0.06);
 }
 
 .nav-content {
@@ -1147,12 +1149,12 @@ onUnmounted(() => {
   padding-top: env(safe-area-inset-top);
 }
 
-.nav-left, .nav-right { width: 80rpx; }
-.back-icon { font-size: 40rpx; color: #1f2937; }
+.nav-left, .nav-right { width: 80rpx; cursor: pointer; }
+.back-icon { font-size: 36rpx; color: var(--color-text-primary, #111827); }
 .page-title {
-  font-size: 32rpx;
-  font-weight: 600;
-  color: #1f2937;
+  font-size: var(--text-lg, 32rpx);
+  font-weight: 700;
+  color: var(--color-text-primary, #111827);
   max-width: 400rpx;
   overflow: hidden;
   text-overflow: ellipsis;
@@ -1166,8 +1168,9 @@ onUnmounted(() => {
 
 .chapter-header {
   padding: 32rpx;
-  background: #ffffff;
+  background: var(--color-bg-card, #ffffff);
   margin-bottom: 16rpx;
+  box-shadow: var(--shadow-xs, 0 1rpx 2rpx rgba(0,0,0,0.04));
 }
 
 .chapter-num-badge {
@@ -1175,9 +1178,9 @@ onUnmounted(() => {
   font-size: 24rpx;
   font-weight: 600;
   color: #ffffff;
-  background: #4f46e5;
+  background: var(--color-primary-600, #4f46e5);
   padding: 6rpx 16rpx;
-  border-radius: 8rpx;
+  border-radius: var(--radius-sm, 10rpx);
   margin-bottom: 16rpx;
 }
 
@@ -1185,9 +1188,10 @@ onUnmounted(() => {
   display: block;
   font-size: 40rpx;
   font-weight: 700;
-  color: #1f2937;
+  color: var(--color-text-primary, #111827);
   margin-bottom: 16rpx;
   line-height: 1.3;
+  letter-spacing: -0.3rpx;
 }
 
 .chapter-meta {
@@ -1219,7 +1223,10 @@ onUnmounted(() => {
   color: white;
   border-radius: 8rpx;
   font-size: 24rpx;
+  cursor: pointer;
+  transition: transform 0.15s, opacity 0.15s;
 }
+.edit-toggle-btn:active { transform: scale(0.96); opacity: 0.85; }
 
 .regenerate-btn {
   padding: 6rpx 16rpx;
@@ -1227,7 +1234,10 @@ onUnmounted(() => {
   color: white;
   border-radius: 8rpx;
   font-size: 24rpx;
+  cursor: pointer;
+  transition: transform 0.15s, opacity 0.15s;
 }
+.regenerate-btn:active { transform: scale(0.96); opacity: 0.85; }
 
 .edit-mode {
   padding: 24rpx;
@@ -1237,12 +1247,14 @@ onUnmounted(() => {
   width: 100%;
   min-height: 600rpx;
   padding: 24rpx;
-  background: #f9fafb;
-  border: 2rpx solid #e5e7eb;
-  border-radius: 12rpx;
+  background: var(--color-bg-surface, #f9fafb);
+  border: 2rpx solid var(--color-bg-divider, #e5e7eb);
+  border-radius: var(--radius-md, 16rpx);
   font-size: 28rpx;
   line-height: 1.8;
+  transition: border-color 0.2s;
 }
+.content-editor:focus { border-color: var(--color-primary-600, #4f46e5); }
 
 .edit-actions {
   display: flex;
@@ -1253,13 +1265,17 @@ onUnmounted(() => {
 .edit-btn {
   flex: 1;
   padding: 24rpx;
-  border-radius: 12rpx;
+  border-radius: var(--radius-md, 16rpx);
   font-size: 28rpx;
+  border: none;
+  transition: transform 0.15s, opacity 0.15s;
 }
 
+.edit-btn:active { transform: scale(0.97); }
+
 .edit-btn.cancel {
-  background: #f3f4f6;
-  color: #6b7280;
+  background: var(--color-bg-surface, #f9fafb);
+  color: var(--color-text-secondary, #6b7280);
 }
 
 .edit-btn.save {
@@ -1267,21 +1283,21 @@ onUnmounted(() => {
   color: white;
 }
 
-.edit-btn.save[disabled] {
+.edit-btn.save[disabled], .edit-btn.cancel[disabled] {
   opacity: 0.5;
 }
 
 .chapter-body {
-  background: #ffffff;
+  background: var(--color-bg-card, #ffffff);
   padding: 32rpx;
   margin-bottom: 16rpx;
-  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
+  box-shadow: var(--shadow-xs, 0 1rpx 2rpx rgba(0,0,0,0.04));
 }
 
 .chapter-text {
   font-size: 30rpx;
   line-height: 1.8;
-  color: #374151;
+  color: var(--color-text-secondary, #374151);
   word-break: break-word;
 }
 
@@ -1383,13 +1399,14 @@ onUnmounted(() => {
 .chapter-text :deep(img) {
   max-width: 100%;
   height: auto;
-  border-radius: 8px;
+  border-radius: 8rpx;
 }
 
 .chapter-actions {
   padding: 32rpx;
-  background: #ffffff;
+  background: var(--color-bg-card, #ffffff);
   margin-bottom: 16rpx;
+  box-shadow: var(--shadow-xs, 0 1rpx 2rpx rgba(0,0,0,0.04));
 }
 
 .level-hint {
@@ -1398,8 +1415,8 @@ onUnmounted(() => {
   gap: 12rpx;
   padding: 24rpx;
   background: rgba(79, 70, 229, 0.05);
-  border: 1px solid rgba(79, 70, 229, 0.2);
-  border-radius: 12rpx;
+  border: 2rpx solid rgba(79, 70, 229, 0.15);
+  border-radius: var(--radius-md, 16rpx);
   margin-bottom: 20rpx;
 }
 
@@ -1419,7 +1436,7 @@ onUnmounted(() => {
 
 .hint-text-large {
   font-size: 32rpx;
-  color: #333;
+  color: var(--color-text-primary, #111827);
   font-weight: 500;
   margin-bottom: 16rpx;
 }
@@ -1427,15 +1444,18 @@ onUnmounted(() => {
 .retry-btn {
   margin-top: 32rpx;
   padding: 16rpx 48rpx;
-  background: #4f46e5;
+  background: var(--color-primary-600, #4f46e5);
   color: #fff;
-  border-radius: 12rpx;
+  border-radius: var(--radius-md, 16rpx);
   font-size: 28rpx;
+  border: none;
+  transition: transform 0.15s, opacity 0.15s;
 }
+.retry-btn:active { transform: scale(0.97); opacity: 0.85; }
 
 .hint-subtext {
   font-size: 26rpx;
-  color: #999;
+  color: var(--color-text-tertiary, #9ca3af);
 }
 
 .hint-icon {
@@ -1451,20 +1471,22 @@ onUnmounted(() => {
 .action-btn {
   width: 100%;
   height: 88rpx;
-  border-radius: 16rpx;
+  border-radius: var(--radius-lg, 16rpx);
   font-size: 28rpx;
   display: flex;
   align-items: center;
   justify-content: center;
   border: none;
   margin-bottom: 20rpx;
+  transition: transform 0.15s, opacity 0.15s;
 }
+.action-btn:active { transform: scale(0.98); opacity: 0.9; }
 
 .action-btn.audio-btn { background: linear-gradient(135deg, #10b981 0%, #059669 100%); color: #ffffff; }
 .action-btn.upgrade-btn { background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); color: #ffffff; }
 .audio-gen-area { display: flex; flex-direction: column; gap: 8rpx; }
 .quota-info-inline { display: flex; flex-wrap: wrap; gap: 12rpx; padding: 8rpx 0; }
-.quota-text { font-size: 22rpx; color: #6b7280; }
+.quota-text { font-size: 22rpx; color: var(--color-text-secondary, #6b7280); }
 .quota-warn { font-size: 22rpx; color: #ef4444; font-weight: 600; }
 .action-btn.video-btn { background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); color: #ffffff; }
 .action-btn[disabled] { opacity: 0.6; }
@@ -1472,17 +1494,17 @@ onUnmounted(() => {
 .audio-player, .video-player {
   margin-top: 24rpx;
   padding: 24rpx;
-  border-radius: 16rpx;
+  border-radius: var(--radius-lg, 16rpx);
 }
 
 .audio-player {
   background: rgba(16, 185, 129, 0.05);
-  border: 1px solid rgba(16, 185, 129, 0.2);
+  border: 2rpx solid rgba(16, 185, 129, 0.15);
 }
 
 .video-player {
   background: rgba(245, 158, 11, 0.05);
-  border: 1px solid rgba(245, 158, 11, 0.2);
+  border: 2rpx solid rgba(245, 158, 11, 0.15);
 }
 
 .audio-player-header, .video-player-header {
@@ -1504,10 +1526,12 @@ onUnmounted(() => {
   padding: 16rpx 32rpx;
   background: linear-gradient(135deg, #10b981 0%, #059669 100%);
   color: #ffffff;
-  border-radius: 12rpx;
+  border-radius: var(--radius-md, 16rpx);
   font-size: 28rpx;
   border: none;
+  transition: transform 0.15s, opacity 0.15s;
 }
+.play-btn:active { transform: scale(0.98); opacity: 0.9; }
 
 .video-play-btn { background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); }
 .audio-time { font-size: 26rpx; color: #6b7280; }
@@ -1517,8 +1541,8 @@ onUnmounted(() => {
   margin-top: 24rpx;
   padding: 24rpx;
   background: rgba(79, 70, 229, 0.05);
-  border: 1px solid rgba(79, 70, 229, 0.2);
-  border-radius: 16rpx;
+  border: 2rpx solid rgba(79, 70, 229, 0.15);
+  border-radius: var(--radius-lg, 16rpx);
 }
 
 .public-toggle-header {
@@ -1588,8 +1612,8 @@ onUnmounted(() => {
   margin-top: 24rpx;
   padding: 24rpx;
   background: linear-gradient(135deg, rgba(16, 185, 129, 0.05) 0%, rgba(5, 150, 105, 0.05) 100%);
-  border: 1px solid rgba(16, 185, 129, 0.2);
-  border-radius: 16rpx;
+  border: 2rpx solid rgba(16, 185, 129, 0.15);
+  border-radius: var(--radius-lg, 16rpx);
 }
 
 .download-header {
@@ -1599,7 +1623,7 @@ onUnmounted(() => {
 .download-title {
   font-size: 28rpx;
   font-weight: 600;
-  color: #333;
+  color: var(--color-text-primary, #111827);
 }
 
 .download-buttons {
@@ -1613,9 +1637,9 @@ onUnmounted(() => {
   line-height: 72rpx;
   font-size: 26rpx;
   font-weight: 500;
-  border-radius: 12rpx;
+  border-radius: var(--radius-md, 16rpx);
   border: none;
-  transition: all 0.3s;
+  transition: transform 0.15s, opacity 0.15s;
 }
 
 .audio-download {
@@ -1638,8 +1662,8 @@ onUnmounted(() => {
   margin-top: 24rpx;
   padding: 24rpx;
   background: linear-gradient(135deg, rgba(102, 126, 234, 0.05) 0%, rgba(118, 75, 162, 0.05) 100%);
-  border: 1px solid rgba(102, 126, 234, 0.2);
-  border-radius: 16rpx;
+  border: 2rpx solid rgba(102, 126, 234, 0.15);
+  border-radius: var(--radius-lg, 16rpx);
 }
 
 .publish-header {
@@ -1677,8 +1701,9 @@ onUnmounted(() => {
   display: flex;
   justify-content: space-between;
   padding: 32rpx;
-  background: #ffffff;
+  background: var(--color-bg-card, #ffffff);
   margin-bottom: 40rpx;
+  box-shadow: var(--shadow-xs, 0 1rpx 2rpx rgba(0,0,0,0.04));
 }
 
 .nav-btn {
@@ -1687,20 +1712,23 @@ onUnmounted(() => {
   align-items: center;
   gap: 12rpx;
   padding: 20rpx;
-  background: #f9fafb;
-  border-radius: 12rpx;
+  background: var(--color-bg-surface, #f9fafb);
+  border-radius: var(--radius-md, 16rpx);
+  cursor: pointer;
+  transition: background 0.15s;
 }
+.nav-btn:active { background: var(--color-bg-hover, #f3f4f6); }
 
 .nav-btn.disabled { opacity: 0.5; justify-content: center; }
 .nav-btn.prev-btn { margin-right: 16rpx; }
 .nav-btn.next-btn { justify-content: flex-end; margin-left: 16rpx; }
 
 .nav-info { display: flex; flex-direction: column; flex: 1; overflow: hidden; }
-.nav-arrow { font-size: 28rpx; color: #4f46e5; flex-shrink: 0; }
-.nav-label { font-size: 24rpx; color: #6b7280; }
+.nav-arrow { font-size: 28rpx; color: var(--color-primary-600, #4f46e5); flex-shrink: 0; }
+.nav-label { font-size: 24rpx; color: var(--color-text-secondary, #6b7280); }
 .nav-title {
   font-size: 26rpx;
-  color: #1f2937;
+  color: var(--color-text-primary, #111827);
   font-weight: 500;
   overflow: hidden;
   text-overflow: ellipsis;
@@ -1765,10 +1793,34 @@ onUnmounted(() => {
   font-size: 40rpx;
   color: #999;
   margin-left: 16rpx;
+  cursor: pointer;
+  border-radius: 50%;
+  transition: color 0.15s, background 0.15s;
 }
+.video-modal-close:active { color: #fff; background: rgba(255,255,255,0.15); }
 
 .video-player-modal {
   width: 100%;
   display: block;
 }
+
+/* ===== 暗色模式 ===== */
+.dark-mode .page { background: #111827; }
+.dark-mode .nav-bar { background: rgba(17, 24, 39, 0.9); border-bottom-color: rgba(255,255,255,0.06); }
+.dark-mode .page-title, .dark-mode .back-icon { color: #f9fafb; }
+.dark-mode .chapter-header { background: #1f2937; box-shadow: none; }
+.dark-mode .chapter-title-large { color: #f9fafb; }
+.dark-mode .chapter-body { background: #1f2937; box-shadow: none; }
+.dark-mode .chapter-text { color: #d1d5db; }
+.dark-mode .chapter-actions { background: #1f2937; box-shadow: none; }
+.dark-mode .chapter-nav { background: #1f2937; box-shadow: none; }
+.dark-mode .nav-btn { background: rgba(255,255,255,0.05); }
+.dark-mode .nav-btn:active { background: rgba(255,255,255,0.1); }
+.dark-mode .nav-title { color: #e5e7eb; }
+.dark-mode .hint-text-large { color: #e5e7eb; }
+.dark-mode .content-editor { background: #1f2937; color: #e5e7eb; border-color: #374151; }
+.dark-mode .chapter-text :deep(h1), .dark-mode .chapter-text :deep(h2) { color: #f9fafb; }
+.dark-mode .chapter-text :deep(h3) { color: #e5e7eb; }
+.dark-mode .chapter-text :deep(pre), .dark-mode .chapter-text :deep(code) { background: #374151; }
+.dark-mode .chapter-text :deep(blockquote) { border-left-color: #4b5563; color: #9ca3af; }
 </style>

Plik diff jest za duży
+ 437 - 490
my-uniapp-vue3/src/pages/book-generator/create.vue


+ 123 - 143
my-uniapp-vue3/src/pages/book-generator/detail.vue

@@ -1021,83 +1021,75 @@ onUnmounted(() => {
 <style scoped>
 .page {
   min-height: 100vh;
-  background: #f9fafb;
+  background: var(--color-bg-page, #f3f4f6);
 }
 
+/* --- 导航栏 --- */
 .nav-bar {
-  position: fixed;
-  top: 0;
-  left: 0;
-  right: 0;
-  z-index: 100;
-  background: #ffffff;
-  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+  position: fixed; top: 0; left: 0; right: 0; z-index: 100;
+  background: rgba(255, 255, 255, 0.88);
+  backdrop-filter: blur(24px);
+  -webkit-backdrop-filter: blur(24px);
+  border-bottom: 0.5rpx solid rgba(0, 0, 0, 0.06);
 }
 
 .nav-content {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  height: 88rpx;
-  padding: 0 32rpx;
-  padding-top: env(safe-area-inset-top);
+  display: flex; align-items: center; justify-content: space-between;
+  height: 88rpx; padding: 0 32rpx; padding-top: env(safe-area-inset-top);
 }
 
 .nav-left, .nav-right { width: 80rpx; }
-.nav-edit-btn { font-size: 28rpx; color: #4f46e5; font-weight: 600; }
-.back-icon { font-size: 40rpx; color: #1f2937; }
+.nav-edit-btn { font-size: 28rpx; color: var(--color-primary-600, #4f46e5); font-weight: 600; }
+.back-icon { font-size: 36rpx; color: var(--color-text-primary, #111827); }
 .page-title {
-  font-size: 32rpx;
-  font-weight: 600;
-  color: #1f2937;
-  max-width: 400rpx;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
+  font-size: var(--text-lg, 32rpx); font-weight: 700; color: var(--color-text-primary, #111827);
+  max-width: 400rpx; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
 }
 
 .main-content {
   padding-top: calc(88rpx + env(safe-area-inset-top));
-  padding: 120rpx 32rpx 32rpx;
+  padding: calc(88rpx + env(safe-area-inset-top) + 16rpx) 24rpx 32rpx;
 }
 
+/* --- 卡片 --- */
 .card {
-  background: #ffffff;
-  border-radius: 24rpx;
-  padding: 32rpx;
-  margin-bottom: 24rpx;
-  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
+  background: var(--color-bg-card, #ffffff);
+  border-radius: var(--radius-lg, 24rpx);
+  padding: 28rpx;
+  margin-bottom: 16rpx;
+  box-shadow: var(--shadow-xs, 0 1rpx 2rpx rgba(0,0,0,0.04));
 }
 
-.card-header-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 24rpx; }
-.header-actions { display: flex; align-items: center; gap: 16rpx; }
-.edit-btn { padding: 12rpx 24rpx; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 8rpx; font-size: 28rpx; }
+.card-header-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20rpx; }
+.header-actions { display: flex; align-items: center; gap: 12rpx; }
+.edit-btn { padding: 10rpx 20rpx; background: linear-gradient(135deg, var(--color-primary-600, #4f46e5), #7c3aed); color: white; border-radius: 8rpx; font-size: 24rpx; font-weight: 600; }
 
-.batch-actions-bar { display: flex; gap: 16rpx; padding: 24rpx; background: #f9fafb; border-radius: 16rpx; margin-bottom: 24rpx; flex-wrap: wrap; }
-.batch-btn { padding: 16rpx 32rpx; background: white; border: 2rpx solid #e5e7eb; border-radius: 12rpx; font-size: 28rpx; color: #374151; }
-.batch-btn.regenerate { background: linear-gradient(135deg, #10b981 0%, #059669 100%); color: white; border: none; }
+.batch-actions-bar { display: flex; gap: 12rpx; padding: 20rpx; background: var(--color-bg-surface, #f9fafb); border-radius: var(--radius-md, 16rpx); margin-bottom: 20rpx; flex-wrap: wrap; }
+.batch-btn { padding: 14rpx 28rpx; background: white; border: 1.5rpx solid var(--color-bg-divider, #e5e7eb); border-radius: var(--radius-sm, 10rpx); font-size: 26rpx; color: #374151; font-weight: 500; }
+.batch-btn.regenerate { background: linear-gradient(135deg, #10b981, #059669); color: white; border: none; }
 .batch-btn.regenerate.disabled { opacity: 0.5; }
-.batch-btn.delete { background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); color: white; border: none; }
+.batch-btn.delete { background: linear-gradient(135deg, #ef4444, #dc2626); color: white; border: none; }
 .batch-btn.delete.disabled { opacity: 0.5; }
-.batch-btn.cancel { background: #f3f4f6; color: #6b7280; border: none; }
+.batch-btn.cancel { background: var(--color-bg-surface, #f9fafb); color: var(--color-text-secondary, #4b5563); border: none; }
 
-.card-title { font-size: 32rpx; font-weight: 600; color: #1f2937; }
+.card-title { font-size: var(--text-lg, 32rpx); font-weight: 700; color: var(--color-text-primary, #111827); }
 
-.book-info { margin-bottom: 24rpx; }
+.book-info { margin-bottom: 16rpx; }
 .book-title-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8rpx; }
-.book-title-large { font-size: 36rpx; font-weight: 700; color: #1f2937; flex: 1; }
-.book-subtitle { font-size: 28rpx; color: #6b7280; margin-bottom: 16rpx; display: block; }
-.book-meta-row { display: flex; gap: 24rpx; font-size: 26rpx; color: #9ca3af; margin-top: 16rpx; }
+.book-title-large { font-size: var(--text-xl, 36rpx); font-weight: 700; color: var(--color-text-primary, #111827); flex: 1; letter-spacing: -0.3rpx; }
+.book-subtitle { font-size: var(--text-base, 28rpx); color: var(--color-text-secondary, #4b5563); margin-bottom: 16rpx; display: block; }
+.book-desc { font-size: 24rpx; color: var(--color-text-secondary, #4b5563); line-height: 1.6; display: block; }
+.book-meta-row { display: flex; gap: 24rpx; font-size: 24rpx; color: var(--color-text-tertiary, #9ca3af); margin-top: 16rpx; }
 
 /* 状态标签由 GenerationStatusBadge 组件统一管理 */
 
-.progress-card { margin-bottom: 24rpx; }
-.progress-display { display: flex; align-items: baseline; gap: 16rpx; margin: 16rpx 0; }
-.progress-text { font-size: 48rpx; font-weight: 700; color: #4f46e5; }
+.progress-card { margin-bottom: 16rpx; }
+.progress-display { display: flex; align-items: baseline; gap: 16rpx; margin: 20rpx 0 12rpx; }
+.progress-text { font-size: 52rpx; font-weight: 800; color: var(--color-primary-600, #4f46e5); letter-spacing: -1rpx; }
 .progress-text.progress-failed { color: #ef4444; }
 .progress-fill-failed { background: linear-gradient(90deg, #ef4444, #dc2626) !important; }
-.progress-detail { font-size: 28rpx; color: #6b7280; }
-.progress-bar-large { height: 12rpx; background: #e5e7eb; border-radius: 6rpx; }
+.progress-detail { font-size: var(--text-base, 28rpx); color: var(--color-text-secondary, #4b5563); }
+.progress-bar-large { height: 10rpx; background: var(--color-bg-divider, #e5e7eb); border-radius: 5rpx; overflow: hidden; }
 
 .generation-status { margin-top: 24rpx; padding: 24rpx; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 16rpx; color: white; }
 .status-header { display: flex; align-items: center; margin-bottom: 16rpx; }
@@ -1131,132 +1123,119 @@ onUnmounted(() => {
 
 /* 章节搜索工具栏 */
 .chapter-toolbar {
-  display: flex;
-  align-items: center;
-  gap: 16rpx;
-  margin-top: 20rpx;
-  margin-bottom: 8rpx;
+  display: flex; align-items: center; gap: 12rpx;
+  margin-top: 20rpx; margin-bottom: 12rpx;
 }
 
 .search-box {
-  flex: 1;
-  display: flex;
-  align-items: center;
-  background: #f3f4f6;
-  border-radius: 16rpx;
-  padding: 12rpx 20rpx;
-  border: 2rpx solid transparent;
-  transition: border-color 0.2s;
+  flex: 1; display: flex; align-items: center;
+  background: var(--color-bg-surface, #f9fafb);
+  border-radius: var(--radius-md, 16rpx);
+  padding: 14rpx 20rpx;
+  border: 1.5rpx solid transparent;
+  transition: all 0.2s;
 }
-
 .search-box:focus-within {
-  border-color: #4f46e5;
-  background: #ffffff;
-}
-
-.search-icon-small {
-  font-size: 28rpx;
-  margin-right: 12rpx;
-  flex-shrink: 0;
-}
-
-.search-input {
-  flex: 1;
-  font-size: 26rpx;
-  color: #1f2937;
-  background: transparent;
-}
-
-.search-placeholder-text {
-  color: #9ca3af;
-  font-size: 26rpx;
+  border-color: var(--color-primary-600, #4f46e5);
+  background: var(--color-bg-card, #ffffff);
+  box-shadow: 0 0 0 4rpx rgba(79, 70, 229, 0.08);
 }
 
-.search-clear {
-  font-size: 28rpx;
-  color: #9ca3af;
-  padding: 4rpx 8rpx;
-  flex-shrink: 0;
-}
+.search-icon-small { font-size: 26rpx; margin-right: 10rpx; flex-shrink: 0; opacity: 0.4; }
+.search-input { flex: 1; font-size: 26rpx; color: var(--color-text-primary, #111827); background: transparent; }
+.search-placeholder-text { color: var(--color-text-tertiary, #9ca3af); font-size: 26rpx; }
+.search-clear { font-size: 26rpx; color: var(--color-text-tertiary, #9ca3af); padding: 4rpx 8rpx; flex-shrink: 0; }
 
 .toggle-btn {
-  flex-shrink: 0;
-  padding: 12rpx 20rpx;
-  background: #f3f4f6;
-  border-radius: 16rpx;
-  font-size: 24rpx;
-  color: #4f46e5;
-  white-space: nowrap;
+  flex-shrink: 0; padding: 14rpx 20rpx;
+  background: var(--color-bg-surface, #f9fafb);
+  border-radius: var(--radius-md, 16rpx);
+  font-size: 24rpx; color: var(--color-primary-600, #4f46e5);
+  white-space: nowrap; transition: all 0.15s;
 }
+.toggle-btn:active { background: var(--color-bg-hover, #f3f4f6); }
 
-.toggle-btn:active {
-  background: #e5e7eb;
-}
+.search-empty { text-align: center; padding: 40rpx; color: var(--color-text-tertiary, #9ca3af); font-size: 26rpx; }
 
-.search-empty {
-  text-align: center;
-  padding: 40rpx;
-  color: #9ca3af;
-  font-size: 26rpx;
+.content-item {
+  display: flex; align-items: center; justify-content: space-between;
+  padding: 20rpx; background: var(--color-bg-surface, #f9fafb);
+  border-radius: var(--radius-md, 16rpx); margin-top: 16rpx;
+  transition: transform 0.15s, background 0.15s;
+}
+.content-item:active { background: var(--color-bg-hover, #f3f4f6); }
+.content-item.foreword, .content-item.afterword {
+  background: linear-gradient(135deg, #fef3c7, #fde68a);
+  cursor: pointer;
 }
-
-.content-item { display: flex; align-items: center; justify-content: space-between; padding: 24rpx; background: #f9fafb; border-radius: 16rpx; margin-top: 16rpx; }
-.content-item.foreword, .content-item.afterword { background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%); cursor: pointer; }
 .content-item-left { display: flex; align-items: center; gap: 16rpx; }
-.content-label { font-size: 28rpx; font-weight: 600; color: #1f2937; }
-.content-word-count { font-size: 24rpx; color: #9ca3af; }
+.content-label { font-size: var(--text-base, 28rpx); font-weight: 600; color: var(--color-text-primary, #111827); }
+.content-word-count { font-size: 22rpx; color: var(--color-text-tertiary, #9ca3af); }
 .content-item-right { display: flex; align-items: center; }
-.expand-icon { font-size: 28rpx; color: #9ca3af; }
+.expand-icon { font-size: 26rpx; color: var(--color-text-tertiary, #9ca3af); }
 
 .content-card { margin-top: 0; }
 
-.content-tree-list { margin-top: 20rpx; }
-.content-tree-item { margin-bottom: 12rpx; }
-.content-tree-row { display: flex; align-items: center; gap: 16rpx; padding: 16rpx; background: #f9fafb; border-radius: 12rpx; }
-
-.chapter-num-badge { width: 48rpx; height: 48rpx; background: #4f46e5; color: #ffffff; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 24rpx; font-weight: 600; flex-shrink: 0; }
+.content-tree-list { margin-top: 16rpx; }
+.content-tree-item { margin-bottom: 10rpx; }
+.content-tree-row {
+  display: flex; align-items: center; gap: 14rpx;
+  padding: 16rpx 14rpx;
+  background: var(--color-bg-surface, #f9fafb);
+  border-radius: var(--radius-sm, 10rpx);
+  transition: background 0.15s;
+}
+.content-tree-row:active { background: var(--color-bg-hover, #f3f4f6); }
+
+.chapter-num-badge {
+  width: 44rpx; height: 44rpx;
+  background: var(--color-primary-600, #4f46e5);
+  color: #ffffff; border-radius: 50%;
+  display: flex; align-items: center; justify-content: center;
+  font-size: 22rpx; font-weight: 700; flex-shrink: 0;
+}
 .chapter-info { flex: 1; }
-.chapter-title { font-size: 28rpx; font-weight: 600; color: #1f2937; display: block; margin-bottom: 6rpx; }
+.chapter-title { font-size: 26rpx; font-weight: 600; color: var(--color-text-primary, #111827); display: block; margin-bottom: 6rpx; }
 .chapter-content-hint { margin-bottom: 6rpx; }
-.content-hint-tag { font-size: 22rpx; padding: 4rpx 12rpx; background: #e0f2fe; color: #0369a1; border-radius: 6rpx; }
-.chapter-outline-summary { font-size: 24rpx; color: #6b7280; display: block; margin-bottom: 8rpx; }
-.chapter-key-points { display: flex; flex-wrap: wrap; gap: 8rpx; margin-bottom: 8rpx; }
-.point-tag { font-size: 20rpx; padding: 4rpx 12rpx; background: #f3f4f6; color: #6b7280; border-radius: 6rpx; }
-.chapter-media-badges { display: flex; gap: 8rpx; }
-.media-badge { font-size: 20rpx; padding: 4rpx 12rpx; background: rgba(16, 185, 129, 0.1); color: #059669; border-radius: 6rpx; }
-
-.chapter-status { display: flex; flex-direction: column; align-items: center; justify-content: center; min-width: 80rpx; padding: 8rpx 16rpx; border-radius: 8rpx; flex-shrink: 0; }
+.content-hint-tag { font-size: 20rpx; padding: 3rpx 10rpx; background: var(--color-info-bg, #eff6ff); color: #0369a1; border-radius: 4rpx; font-weight: 500; }
+.chapter-outline-summary { font-size: 22rpx; color: var(--color-text-secondary, #4b5563); display: block; margin-bottom: 6rpx; }
+.chapter-key-points { display: flex; flex-wrap: wrap; gap: 6rpx; margin-bottom: 6rpx; }
+.point-tag { font-size: 18rpx; padding: 3rpx 10rpx; background: var(--color-bg-hover, #f3f4f6); color: var(--color-text-secondary, #4b5563); border-radius: 4rpx; }
+.chapter-media-badges { display: flex; gap: 6rpx; }
+.media-badge { font-size: 18rpx; padding: 3rpx 10rpx; background: rgba(16, 185, 129, 0.1); color: #059669; border-radius: 4rpx; font-weight: 500; }
+
+.chapter-status { display: flex; flex-direction: column; align-items: center; justify-content: center; min-width: 76rpx; padding: 8rpx 14rpx; border-radius: 8rpx; flex-shrink: 0; }
 .chapter-status.status-completed { background: #d1fae5; color: #059669; }
-.chapter-status.status-pending { background: #e5e7eb; color: #6b7280; }
-.chapter-status.status-generating { background: #fef3c7; color: #d97706; animation: pulse-bg 2s ease-in-out infinite; }
-.chapter-status.status-failed { background: #fee2e2; color: #dc2626; }
+.chapter-status.status-pending { background: var(--color-bg-divider, #e5e7eb); color: var(--color-text-tertiary, #9ca3af); }
+.chapter-status.status-generating { background: var(--color-warning-bg, #fffbeb); color: #d97706; animation: pulse-bg 2s ease-in-out infinite; }
+.chapter-status.status-failed { background: var(--color-error-bg, #fef2f2); color: #dc2626; }
 @keyframes pulse-bg { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } }
-.status-icon { font-size: 28rpx; font-weight: bold; line-height: 1; }
-.status-text { font-size: 20rpx; margin-top: 4rpx; white-space: nowrap; }
+.status-icon { font-size: 24rpx; font-weight: bold; line-height: 1; }
+.status-text { font-size: 18rpx; margin-top: 2rpx; white-space: nowrap; }
 
-.section-num-badge { width: 40rpx; height: 40rpx; background: #8b5cf6; color: #ffffff; border-radius: 8rpx; display: flex; align-items: center; justify-content: center; font-size: 22rpx; font-weight: 600; flex-shrink: 0; }
+.section-num-badge { width: 38rpx; height: 38rpx; background: #8b5cf6; color: #ffffff; border-radius: 8rpx; display: flex; align-items: center; justify-content: center; font-size: 20rpx; font-weight: 600; flex-shrink: 0; }
 .section-info { flex: 1; }
-.section-title { font-size: 26rpx; font-weight: 500; color: #374151; display: block; margin-bottom: 4rpx; }
-.section-summary { font-size: 22rpx; color: #9ca3af; display: block; }
+.section-title { font-size: 24rpx; font-weight: 500; color: #374151; display: block; margin-bottom: 2rpx; }
+.section-summary { font-size: 20rpx; color: var(--color-text-tertiary, #9ca3af); display: block; }
 
-.section-status { display: flex; flex-direction: column; align-items: center; justify-content: center; min-width: 70rpx; padding: 6rpx 12rpx; border-radius: 6rpx; flex-shrink: 0; }
+.section-status { display: flex; flex-direction: column; align-items: center; justify-content: center; min-width: 66rpx; padding: 6rpx 10rpx; border-radius: 6rpx; flex-shrink: 0; }
 .section-status.status-completed { background: #d1fae5; color: #059669; }
-.section-status.status-pending { background: #f3f4f6; color: #9ca3af; }
-.section-status.status-generating { background: #fef3c7; color: #d97706; }
-.section-status.status-failed { background: #fee2e2; color: #dc2626; }
+.section-status.status-pending { background: var(--color-bg-hover, #f3f4f6); color: var(--color-text-tertiary, #9ca3af); }
+.section-status.status-generating { background: var(--color-warning-bg, #fffbeb); color: #d97706; }
+.section-status.status-failed { background: var(--color-error-bg, #fef2f2); color: #dc2626; }
 
-.subsection-num-badge { width: 36rpx; height: 36rpx; background: #06b6d4; color: #ffffff; border-radius: 6rpx; display: flex; align-items: center; justify-content: center; font-size: 20rpx; font-weight: 500; flex-shrink: 0; }
+.subsection-num-badge { width: 34rpx; height: 34rpx; background: #06b6d4; color: #ffffff; border-radius: 6rpx; display: flex; align-items: center; justify-content: center; font-size: 18rpx; font-weight: 500; flex-shrink: 0; }
 .subsection-info { flex: 1; }
-.subsection-title { font-size: 24rpx; color: #6b7280; display: block; }
-.subsection-media-badges { display: flex; gap: 8rpx; margin-top: 4rpx; }
-.media-badge-small { font-size: 18rpx; padding: 2rpx 8rpx; background: rgba(16, 185, 129, 0.1); color: #059669; border-radius: 4rpx; }
-.subsection-content-preview { font-size: 22rpx; color: #6b7280; margin-top: 4rpx; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
+.subsection-title { font-size: 22rpx; color: var(--color-text-secondary, #4b5563); display: block; }
+.subsection-media-badges { display: flex; gap: 6rpx; margin-top: 4rpx; }
+.media-badge-small { font-size: 16rpx; padding: 2rpx 8rpx; background: rgba(16, 185, 129, 0.1); color: #059669; border-radius: 4rpx; }
+.subsection-content-preview { font-size: 20rpx; color: var(--color-text-secondary, #4b5563); margin-top: 4rpx; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
 
-.subsection-status { display: flex; flex-direction: column; align-items: center; justify-content: center; min-width: 70rpx; padding: 6rpx 12rpx; border-radius: 6rpx; flex-shrink: 0; }
+.subsection-status { display: flex; flex-direction: column; align-items: center; justify-content: center; min-width: 66rpx; padding: 6rpx 10rpx; border-radius: 6rpx; flex-shrink: 0; }
 .subsection-status.status-completed { background: #d1fae5; color: #059669; }
-.subsection-status.status-pending { background: #f3f4f6; color: #9ca3af; }
-.subsection-status.status-generating { background: #fef3c7; color: #d97706; }
-.subsection-status.status-failed { background: #fee2e2; color: #dc2626; }
+.subsection-status.status-pending { background: var(--color-bg-hover, #f3f4f6); color: var(--color-text-tertiary, #9ca3af); }
+.subsection-status.status-generating { background: var(--color-warning-bg, #fffbeb); color: #d97706; }
+.subsection-status.status-failed { background: var(--color-error-bg, #fef2f2); color: #dc2626; }
 
 .action-card { margin-bottom: 24rpx; }
 .btn-col { margin-bottom: 20rpx; }
@@ -1269,6 +1248,7 @@ onUnmounted(() => {
 .action-btn.audio-btn { background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%); color: #ffffff; }
 .action-btn.upgrade-btn { background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); color: #ffffff; }
 .btn-with-quota { display: flex; flex-direction: column; gap: 8rpx; width: 100%; }
+.btn-with-quota .action-btn { width: 100%; flex: none; }
 .quota-info-inline { display: flex; flex-wrap: wrap; gap: 12rpx; padding: 8rpx 0; }
 .quota-text { font-size: 22rpx; color: #6b7280; }
 .quota-warn { font-size: 22rpx; color: #ef4444; font-weight: 600; }

+ 91 - 36
my-uniapp-vue3/src/pages/book-generator/index.vue

@@ -455,44 +455,99 @@ onUnmounted(() => {
 </script>
 
 <style scoped>
-.page { min-height: 100vh; background: #f9fafb; }
-.nav-bar { position: fixed; top: 0; left: 0; right: 0; z-index: 100; background: #ffffff; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); }
-.nav-content { display: flex; align-items: center; justify-content: space-between; height: 88rpx; padding: 0 32rpx; padding-top: env(safe-area-inset-top); }
+.page { min-height: 100vh; background: var(--color-bg-page, #f3f4f6); }
+
+/* --- 导航栏 --- */
+.nav-bar {
+  position: fixed; top: 0; left: 0; right: 0; z-index: 100;
+  background: rgba(255, 255, 255, 0.88);
+  backdrop-filter: blur(24px);
+  -webkit-backdrop-filter: blur(24px);
+  border-bottom: 0.5rpx solid rgba(0, 0, 0, 0.06);
+}
+.nav-content {
+  display: flex; align-items: center; justify-content: space-between;
+  height: 88rpx; padding: 0 32rpx; padding-top: env(safe-area-inset-top);
+}
 .nav-left, .nav-right { width: 80rpx; height: 88rpx; display: flex; align-items: center; justify-content: center; }
-.back-icon, .nav-btn { font-size: 40rpx; color: #1f2937; }
-.page-title { font-size: 32rpx; font-weight: 600; color: #1f2937; }
-.main-content { padding: 120rpx 32rpx 32rpx; }
-.card { background: #ffffff; border-radius: 24rpx; padding: 32rpx; margin-bottom: 24rpx; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); }
-.create-card { display: flex; align-items: center; justify-content: center; gap: 16rpx; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 24rpx; padding: 48rpx; margin-bottom: 24rpx; }
-.create-icon { font-size: 48rpx; color: #ffffff; }
-.create-text { font-size: 32rpx; font-weight: 600; color: #ffffff; }
-.book-list { display: flex; flex-direction: column; gap: 20rpx; }
-.book-card { background: #ffffff; border-radius: 20rpx; padding: 28rpx; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); }
-.book-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12rpx; }
-.book-title { font-size: 30rpx; font-weight: 600; color: #1f2937; flex: 1; }
+.back-icon, .nav-btn { font-size: 36rpx; color: var(--color-text-primary, #111827); }
+.page-title { font-size: var(--text-lg, 32rpx); font-weight: 700; color: var(--color-text-primary, #111827); }
+
+.main-content { padding: calc(88rpx + env(safe-area-inset-top) + 20rpx) 24rpx 32rpx; }
+
+/* --- 创建按钮 --- */
+.create-card {
+  display: flex; align-items: center; justify-content: center; gap: 12rpx;
+  background: linear-gradient(135deg, var(--color-primary-600, #4f46e5), #7c3aed);
+  border-radius: var(--radius-lg, 24rpx);
+  padding: 36rpx; margin-bottom: 20rpx;
+  box-shadow: var(--shadow-primary, 0 4rpx 20rpx rgba(79,70,229,0.25));
+  transition: transform 0.15s, box-shadow 0.15s;
+}
+.create-card:active { transform: scale(0.98); box-shadow: 0 2rpx 8rpx rgba(79,70,229,0.25); }
+.create-icon { font-size: 44rpx; color: #ffffff; font-weight: 300; }
+.create-text { font-size: var(--text-lg, 32rpx); font-weight: 600; color: #ffffff; }
+
+/* --- 书籍卡片 --- */
+.book-list { display: flex; flex-direction: column; gap: 16rpx; }
+.book-card {
+  background: var(--color-bg-card, #ffffff);
+  border-radius: var(--radius-md, 16rpx);
+  padding: 24rpx;
+  box-shadow: var(--shadow-xs, 0 1rpx 2rpx rgba(0,0,0,0.04));
+  transition: box-shadow 0.2s, transform 0.2s;
+}
+.book-card:active { transform: scale(0.985); }
+
+.book-header {
+  display: flex; align-items: center; justify-content: space-between;
+  margin-bottom: 10rpx;
+}
+.book-title { font-size: var(--text-base, 28rpx); font-weight: 700; color: var(--color-text-primary, #111827); flex: 1; }
 .header-right { display: flex; align-items: center; gap: 12rpx; flex-shrink: 0; }
-.edit-icon { font-size: 28rpx; padding: 4rpx; opacity: 0.5; }
+.edit-icon { font-size: 26rpx; padding: 6rpx; opacity: 0.4; transition: opacity 0.15s; }
 .edit-icon:active { opacity: 1; }
-/* 状态标签由 GenerationStatusBadge 组件统一管理 */
-.book-desc { font-size: 26rpx; color: #6b7280; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; margin-bottom: 16rpx; }
-.book-meta { display: flex; gap: 24rpx; font-size: 24rpx; color: #9ca3af; margin-bottom: 12rpx; }
-.progress-bar { height: 6rpx; background: #e5e7eb; border-radius: 3rpx; }
-.progress-fill { height: 100%; background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); border-radius: 3rpx; transition: width 0.3s; }
-.book-actions { display: flex; gap: 12rpx; }
-.primary-btn, .publish-btn, .more-btn { height: 72rpx; border-radius: 12rpx; font-size: 26rpx; display: flex; align-items: center; justify-content: center; border: none; white-space: nowrap; }
-.primary-btn { flex: 1; background: linear-gradient(135deg, #10b981 0%, #059669 100%); color: #ffffff; padding: 0 16rpx; }
-.publish-btn { width: auto; min-width: 160rpx; padding: 0 24rpx; background: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%); color: #ffffff; }
-.more-btn { width: 80rpx; background: #e5e7eb; color: #6b7280; font-size: 32rpx; font-weight: bold; }
-.more-btn { width: 80rpx; background: #e5e7eb; color: #6b7280; font-size: 32rpx; font-weight: bold; }
-.primary-btn[disabled], .publish-btn[disabled], .more-btn[disabled] { opacity: 0.6; }
-/* 夜间模式适配 */
-@media (prefers-color-scheme: dark) {
-  .more-btn { background: #374151; color: #d1d5db; }
-  .primary-btn { background: linear-gradient(135deg, #059669 0%, #047857 100%); }
-  .publish-btn { background: linear-gradient(135deg, #4338ca 0%, #4f46e5 100%); }
+
+.book-desc {
+  font-size: 24rpx; color: var(--color-text-secondary, #4b5563);
+  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
+  overflow: hidden; margin-bottom: 14rpx; line-height: 1.5;
+}
+.book-meta { display: flex; gap: 24rpx; font-size: 22rpx; color: var(--color-text-tertiary, #9ca3af); margin-bottom: 12rpx; }
+
+.progress-bar { height: 5rpx; background: var(--color-bg-divider, #e5e7eb); border-radius: 3rpx; overflow: hidden; }
+.progress-fill {
+  height: 100%;
+  background: linear-gradient(90deg, var(--color-primary-600, #4f46e5), #7c3aed);
+  border-radius: 3rpx; transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+/* --- 操作按钮 --- */
+.book-actions { display: flex; gap: 10rpx; margin-top: 16rpx; }
+.primary-btn, .publish-btn, .more-btn {
+  height: 68rpx; border-radius: var(--radius-sm, 10rpx); font-size: 24rpx;
+  display: flex; align-items: center; justify-content: center; border: none;
+  white-space: nowrap; font-weight: 600; transition: opacity 0.15s, transform 0.15s;
 }
-.empty-state { display: flex; flex-direction: column; align-items: center; padding: 80rpx 0; }
-.empty-icon { font-size: 120rpx; margin-bottom: 24rpx; }
-.empty-text { font-size: 32rpx; color: #6b7280; margin-bottom: 12rpx; }
-.empty-hint { font-size: 26rpx; color: #9ca3af; }
+.primary-btn:active, .publish-btn:active, .more-btn:active { transform: scale(0.97); }
+.primary-btn { flex: 1; background: linear-gradient(135deg, #10b981, #059669); color: #ffffff; padding: 0 16rpx; }
+.publish-btn { width: auto; min-width: 160rpx; padding: 0 20rpx; background: linear-gradient(135deg, var(--color-primary-600, #4f46e5), var(--color-primary-500, #6366f1)); color: #ffffff; }
+.more-btn { width: 68rpx; background: var(--color-bg-surface, #f9fafb); color: var(--color-text-secondary, #4b5563); font-size: 28rpx; font-weight: 700; }
+.primary-btn[disabled], .publish-btn[disabled], .more-btn[disabled] { opacity: 0.5; }
+
+/* --- 空状态 --- */
+.empty-state { display: flex; flex-direction: column; align-items: center; padding: 100rpx 0; }
+.empty-icon { font-size: 100rpx; margin-bottom: 24rpx; opacity: 0.7; }
+.empty-text { font-size: var(--text-lg, 32rpx); font-weight: 600; color: var(--color-text-secondary, #4b5563); margin-bottom: 8rpx; }
+.empty-hint { font-size: var(--text-sm, 24rpx); color: var(--color-text-tertiary, #9ca3af); }
+
+/* 夜间模式 */
+.dark-mode .nav-bar { background: rgba(15, 23, 42, 0.9); border-bottom-color: rgba(255,255,255,0.06); }
+.dark-mode .back-icon, .dark-mode .nav-btn, .dark-mode .page-title { color: #f1f5f9; }
+.dark-mode .book-card { background: var(--color-bg-card, #1e293b); }
+.dark-mode .book-title { color: #f1f5f9; }
+.dark-mode .book-desc { color: #94a3b8; }
+.dark-mode .book-meta { color: #64748b; }
+.dark-mode .more-btn { background: var(--color-bg-hover, #334155); color: #d1d5db; }
+.dark-mode .empty-text { color: #94a3b8; }
 </style>

+ 19 - 11
my-uniapp-vue3/src/pages/book-generator/interactive.vue

@@ -899,11 +899,11 @@ onMounted(() => {
 <style scoped>
 .page { min-height: 100vh; background: #f9fafb; }
 
-.nav-bar { position: fixed; top: 0; left: 0; right: 0; z-index: 100; background: #ffffff; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
+.nav-bar { position: fixed; top: 0; left: 0; right: 0; z-index: 100; background: rgba(255,255,255,0.88); backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px); border-bottom: 0.5rpx solid rgba(0,0,0,0.06); }
 .nav-content { display: flex; align-items: center; height: 88rpx; padding: 0 32rpx; padding-top: env(safe-area-inset-top); }
-.nav-left { width: 60rpx; flex-shrink: 0; }
-.page-title { font-size: 32rpx; font-weight: 600; color: #1f2937; text-align: center; flex: 1; }
-.back-icon { font-size: 40rpx; color: #1f2937; }
+.nav-left { width: 60rpx; flex-shrink: 0; cursor: pointer; }
+.page-title { font-size: 32rpx; font-weight: 700; color: var(--color-text-primary, #111827); text-align: center; flex: 1; }
+.back-icon { font-size: 36rpx; color: var(--color-text-primary, #111827); }
 
 /* 模式切换独立行 */
 .mode-bar { padding: 0 32rpx 16rpx 32rpx; }
@@ -934,20 +934,25 @@ onMounted(() => {
 .chip-desc { font-size: 20rpx; color: inherit; }
 .form-label-sm { font-size: 24rpx; font-weight: 600; color: #374151; margin-bottom: 8rpx; display: block; }
 
-.form-input { width: 100%; height: 88rpx; padding: 0 24rpx; background: #f9fafb; border: 2rpx solid #e5e7eb; border-radius: 12rpx; font-size: 28rpx; }
-.form-input-sm { width: 100%; height: 72rpx; padding: 0 16rpx; background: #f9fafb; border: 2rpx solid #e5e7eb; border-radius: 8rpx; font-size: 26rpx; }
+.form-input { width: 100%; height: 88rpx; padding: 0 24rpx; background: var(--color-bg-surface, #f9fafb); border: 2rpx solid var(--color-bg-divider, #e5e7eb); border-radius: 12rpx; font-size: 28rpx; transition: border-color 0.2s, box-shadow 0.2s; }
+.form-input:focus { border-color: #4f46e5; box-shadow: 0 0 0 4rpx rgba(79,70,229,0.08); }
+.form-input-sm { width: 100%; height: 72rpx; padding: 0 16rpx; background: var(--color-bg-surface, #f9fafb); border: 2rpx solid var(--color-bg-divider, #e5e7eb); border-radius: 8rpx; font-size: 26rpx; transition: border-color 0.2s; }
+.form-input-sm:focus { border-color: #4f46e5; }
 .form-textarea { width: 100%; min-height: 160rpx; padding: 24rpx; background: #f9fafb; border: 2rpx solid #e5e7eb; border-radius: 12rpx; font-size: 28rpx; line-height: 1.6; }
 .form-textarea-sm { width: 100%; min-height: 100rpx; padding: 16rpx; background: #f9fafb; border: 2rpx solid #e5e7eb; border-radius: 8rpx; font-size: 24rpx; line-height: 1.5; }
 
 .chip-group { display: flex; flex-wrap: wrap; gap: 12rpx; }
-.chip { display: flex; align-items: center; gap: 8rpx; padding: 12rpx 20rpx; background: #f3f4f6; border-radius: 24rpx; font-size: 24rpx; color: #4b5563; transition: all .2s; }
+.chip { display: flex; align-items: center; gap: 8rpx; padding: 12rpx 20rpx; background: var(--color-bg-surface, #f3f4f6); border-radius: 24rpx; font-size: 24rpx; color: #4b5563; transition: all .2s; }
+.chip:active { transform: scale(0.96); }
 .chip.active { background: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%); color: white; }
 .chip-icon { font-size: 24rpx; }
 .chip-text { font-size: 24rpx; }
 
 .scale-picker { display: flex; flex-wrap: wrap; gap: 12rpx; }
-.scale-chip { display: flex; flex-direction: column; align-items: center; gap: 4rpx; padding: 12rpx 16rpx; background: #f3f4f6; border-radius: 12rpx; min-width: 100rpx; transition: all .2s; }
+.scale-chip { display: flex; flex-direction: column; align-items: center; gap: 4rpx; padding: 12rpx 16rpx; background: var(--color-bg-surface, #f3f4f6); border-radius: 12rpx; min-width: 100rpx; transition: all .2s; }
+.scale-chip:active { transform: scale(0.96); }
 .scale-chip.active { background: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%); color: white; }
+.scale-chip.active .scale-desc { color: rgba(255,255,255,0.7); }
 .scale-label { font-size: 24rpx; font-weight: 600; }
 .scale-desc { font-size: 20rpx; color: #9ca3af; }
 
@@ -957,9 +962,12 @@ onMounted(() => {
 .estimate-value { font-size: 24rpx; color: #4f46e5; font-weight: 600; }
 
 .btn-group { display: flex; flex-direction: column; gap: 12rpx; margin-top: 32rpx; }
-.btn-primary { width: 100%; height: 88rpx; background: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%); color: white; border-radius: 12rpx; font-size: 28rpx; font-weight: 600; border: none; display: flex; align-items: center; justify-content: center; white-space: nowrap; }
-.btn-secondary { width: 100%; height: 88rpx; background: #f3f4f6; color: #4b5563; border-radius: 12rpx; font-size: 28rpx; font-weight: 600; border: none; display: flex; align-items: center; justify-content: center; white-space: nowrap; }
-.btn-cancel { width: 100%; height: 88rpx; background: #f3f4f6; color: #6b7280; border-radius: 12rpx; font-size: 28rpx; border: none; display: flex; align-items: center; justify-content: center; white-space: nowrap; }
+.btn-primary { width: 100%; height: 88rpx; background: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%); color: white; border-radius: 12rpx; font-size: 28rpx; font-weight: 600; border: none; display: flex; align-items: center; justify-content: center; white-space: nowrap; transition: transform 0.15s, opacity 0.15s; }
+.btn-primary:active { transform: scale(0.98); opacity: 0.9; }
+.btn-secondary { width: 100%; height: 88rpx; background: var(--color-bg-surface, #f3f4f6); color: #4b5563; border-radius: 12rpx; font-size: 28rpx; font-weight: 600; border: none; display: flex; align-items: center; justify-content: center; white-space: nowrap; transition: transform 0.15s, background 0.15s; }
+.btn-secondary:active { background: var(--color-bg-hover, #e5e7eb); transform: scale(0.98); }
+.btn-cancel { width: 100%; height: 88rpx; background: var(--color-bg-surface, #f3f4f6); color: #6b7280; border-radius: 12rpx; font-size: 28rpx; border: none; display: flex; align-items: center; justify-content: center; white-space: nowrap; transition: transform 0.15s, background 0.15s; }
+.btn-cancel:active { background: var(--color-bg-hover, #e5e7eb); transform: scale(0.98); }
 .btn-row-sm { display: flex; gap: 8rpx; margin-top: 12rpx; }
 .btn-delete-sm { padding: 8rpx 16rpx; background: #fef2f2; color: #ef4444; border-radius: 8rpx; font-size: 22rpx; border: none; }
 .btn-move-sm { padding: 8rpx 16rpx; background: #f3f4f6; color: #6b7280; border-radius: 8rpx; font-size: 22rpx; border: none; }

+ 380 - 1420
my-uniapp-vue3/src/pages/create/index.vue

@@ -1,52 +1,9 @@
 <template>
   <view class="page">
-    <!-- 顶部导航栏 -->
-    <view class="nav-bar">
-      <view class="nav-content">
-        <text class="page-title">生成音频</text>
-      </view>
-    </view>
-
-    <!-- 主内容区 -->
+    <!-- 主内容区:只有文本输入 -->
     <view class="main-content">
-      <!-- 文档上传区 -->
-      <view class="card doc-upload-card">
-        <view class="card-header">
-          <text class="card-title">📄 上传文档</text>
-          <text v-if="uploading" class="upload-status uploading">解析中...</text>
-        </view>
-        <!-- 未上传时:选择文件 -->
-        <view v-if="!uploadedDoc.fileName" class="doc-upload-area" @click="handleUploadDoc">
-          <text class="doc-upload-icon">📁</text>
-          <text class="doc-upload-text">点击上传文档</text>
-          <text class="doc-upload-hint">支持 .txt .docx .pdf 格式,最大 20MB</text>
-        </view>
-        <!-- 已上传:显示文档信息 -->
-        <view v-else class="doc-info-bar">
-          <view class="doc-info-left">
-            <text class="doc-format-badge">{{ uploadedDoc.format?.toUpperCase() || 'DOC' }}</text>
-            <view class="doc-info-text">
-              <text class="doc-filename">{{ uploadedDoc.fileName }}</text>
-              <text class="doc-meta">{{ uploadedDoc.wordCount }} 字 · {{ formatFileSize(uploadedDoc.fileSize) }}</text>
-              <text v-if="uploadedDoc.truncated" class="doc-truncated-hint">⚠ 内容过长,已截取前2000字</text>
-            </view>
-          </view>
-          <text class="doc-remove-btn" @click="clearDoc">✕</text>
-        </view>
-        <!-- 上传中 -->
-        <view v-if="uploading" class="doc-uploading-bar">
-          <text>正在解析文档,请稍候...</text>
-        </view>
-      </view>
-
-      <!-- 文本输入区 -->
-      <view class="card input-card">
-        <view class="card-header">
-          <text class="card-title">输入文本</text>
-          <text class="word-count" :class="{ warning: text.length > 1800, error: text.length >= 2000 }">
-            {{ text.length }}/2000 字
-          </text>
-        </view>
+      <!-- 文本输入 -->
+      <view class="input-area">
         <textarea
           v-model="text"
           class="text-input"
@@ -54,170 +11,171 @@
           :maxlength="2000"
           auto-height
         />
-        <view class="input-actions">
-          <button class="action-btn ai-generate-btn" @click="goToAIGenerate">
-            <text>🤖 AI 生成内容</text>
-          </button>
-          <button class="action-btn" @click="clearText">清空</button>
-          <button class="action-btn" @click="pasteText">粘贴</button>
-        </view>
-        <view class="input-hint">
-          <text>支持长按粘贴文本内容</text>
+        <!-- 字数统计 -->
+        <text class="word-count" :class="{ warning: text.length > 1800, error: text.length >= 2000 }">
+          {{ text.length }}/2000
+        </text>
+
+        <!-- 已上传文档信息 -->
+        <view v-if="uploadedDoc.fileName && !uploading" class="doc-info-bar">
+          <text class="doc-format-badge">{{ uploadedDoc.format?.toUpperCase() || 'DOC' }}</text>
+          <text class="doc-filename">{{ uploadedDoc.fileName }}</text>
+          <text class="doc-meta">{{ uploadedDoc.wordCount }} 字</text>
+          <text v-if="uploadedDoc.truncated" class="doc-truncated-hint">⚠ 已截取</text>
+          <text class="doc-remove-btn" @click="clearDoc">✕</text>
         </view>
-      </view>
-
-      <!-- 音色选择 - 网格布局 -->
-      <view class="card voice-card">
-        <text class="card-title">选择音色</text>
-        <view class="voice-grid">
-          <view
-            v-for="voice in audioStore.voices"
-            :key="voice.id"
-            class="voice-item"
-            :class="{ active: selectedVoice === voice.id }"
-            @click="selectedVoice = voice.id"
-          >
-            <view class="voice-icon">
-              <text>{{ voice.gender === 'female' ? '👩' : '👨' }}</text>
-            </view>
-            <text class="voice-name">{{ voice.name }}</text>
-            <view class="voice-preview-btn" @click.stop="previewVoice(voice.id)">
-              <text v-if="previewingVoice !== voice.id">▶</text>
-              <text v-else>⏹</text>
-            </view>
-          </view>
+        <!-- 上传中 -->
+        <view v-if="uploading" class="doc-uploading-bar">
+          <text>正在解析文档,请稍候...</text>
         </view>
-      </view>
 
-      <!-- 参数调节 -->
-      <view class="card params-card">
-        <text class="card-title">参数调节</text>
-        <view class="param-item">
-          <view class="param-header">
-            <text class="param-label">语速</text>
-            <view class="param-value">{{ voiceParams.speed.toFixed(1) }}x</view>
+        <!-- 工具按钮行 -->
+        <view class="tool-row">
+          <view class="tool-btn" @click="handleUploadDoc">
+            <text>{{ uploadedDoc.fileName ? '📄 重新上传' : '📄 上传文档' }}</text>
           </view>
-          <slider
-            :value="(voiceParams.speed - 0.5) * 100 / 1.5"
-            :min="0"
-            :max="100"
-            @changing="(e: any) => voiceParams.speed = Number((0.5 + (e.detail.value / 100) * 1.5).toFixed(1))"
-            activeColor="#4F46E5"
-            backgroundColor="#e5e7eb"
-            block-size="20"
-          />
-          <view class="param-range">
-            <text>0.5x</text>
-            <text>2.0x</text>
+          <view class="tool-btn" @click="goToAIGenerate">
+            <text>🤖 AI 生成</text>
           </view>
-        </view>
-        <view class="param-item">
-          <view class="param-header">
-            <text class="param-label">音调</text>
-            <view class="param-value">{{ voiceParams.pitch > 0 ? '+' : '' }}{{ voiceParams.pitch }}</view>
+          <view class="tool-btn" @click="pasteText">
+            <text>📋 粘贴</text>
           </view>
-          <slider
-            :value="(voiceParams.pitch + 500) / 10"
-            :min="0"
-            :max="100"
-            @changing="(e: any) => voiceParams.pitch = Math.round(e.detail.value * 10 - 500)"
-            activeColor="#4F46E5"
-            backgroundColor="#e5e7eb"
-            block-size="20"
-          />
-          <view class="param-range">
-            <text>-500</text>
-            <text>+500</text>
+          <view class="tool-btn" @click="clearText">
+            <text>🗑 清空</text>
           </view>
         </view>
-        <view class="param-item">
-          <view class="param-header">
-            <text class="param-label">音量</text>
-            <view class="param-value">{{ voiceParams.volume }}%</view>
-          </view>
-          <slider
-            :value="voiceParams.volume"
-            :min="0"
-            :max="100"
-            @changing="(e: any) => voiceParams.volume = e.detail.value"
-            activeColor="#4F46E5"
-            backgroundColor="#e5e7eb"
-            block-size="20"
-          />
-          <view class="param-range">
-            <text>0%</text>
-            <text>100%</text>
-          </view>
+
+        <!-- 专辑选择(必填) -->
+        <view class="album-selector" @click="showAlbumListPanel = true">
+          <text class="album-selector-label">专辑</text>
+          <text class="album-selector-value" :class="{ placeholder: !selectedAlbum }">
+            {{ selectedAlbum ? selectedAlbum.title : '请选择专辑(必填)' }}
+          </text>
+          <text class="album-selector-arrow">▼</text>
         </view>
       </view>
+    </view>
 
-      <!-- 专辑选择 -->
-      <view class="card album-card">
-        <view class="card-header-row">
-          <text class="card-title">📚 所属专辑</text>
-          <text class="album-add-btn" @click="showAlbumPanel = true">+ 新建专辑</text>
+    <!-- 底部固定区:Tab 工具栏 -->
+    <view class="bottom-bar">
+      <!-- 主操作行:生成按钮 + 快捷信息 -->
+      <view class="bottom-action-row">
+        <view class="quick-info">
+          <text class="quick-info-text">🎤 {{ currentVoiceName || '默认音色' }}</text>
         </view>
-        <view class="album-selector" @click="showAlbumListPanel = true">
-          <text v-if="selectedAlbum" class="album-name">{{ selectedAlbum.title }}</text>
-          <text v-else class="album-placeholder">点击选择专辑(可选)</text>
-          <text class="album-arrow">›</text>
+        <view class="generate-btn-tab" :class="{ disabled: !canGenerate }" @click="handleGenerate">
+          <text class="generate-btn-tab-text">{{ generating ? '提交中...' : '开始生成' }}</text>
         </view>
       </view>
 
-      <!-- 音频生成入口 -->
-      <view class="audio-entry-card" @click="handleGenerate">
-        <view class="audio-entry-content">
-          <text class="audio-entry-icon">🎵</text>
-          <view class="audio-entry-text">
-            <text class="audio-entry-title">音频生成</text>
-            <text class="audio-entry-desc">将文字转换为语音</text>
-          </view>
+      <!-- 图标 Tab 栏(等宽分布,图标+文字居中) -->
+      <view class="tab-toolbar">
+        <view class="tab-item" :class="{ active: showVoicePanel }" @click="showVoicePanel = true">
+          <text class="tab-icon">🎤</text>
+          <text class="tab-text">音色</text>
+        </view>
+        <view class="tab-item" @click="showParamsPanel = true">
+          <text class="tab-icon">⚙️</text>
+          <text class="tab-text">参数</text>
         </view>
-        <text class="audio-entry-arrow">→</text>
       </view>
+    </view>
+
+    <!-- 音色面板(暗色主题) -->
+    <view v-if="showVoicePanel" class="bottom-panel-overlay" @click="showVoicePanel = false">
+      <view class="bottom-panel voice-panel-dark" @click.stop>
+        <!-- 分类标签栏 -->
+        <view class="voice-tabs">
+          <scroll-view scroll-x :show-scrollbar="false" class="voice-tabs-scroll">
+            <view class="voice-tabs-inner">
+              <text
+                v-for="(cat, idx) in voiceCategories"
+                :key="cat.key"
+                class="voice-tab"
+                :class="{ active: voiceCategory === cat.key }"
+                @click="voiceCategory = cat.key"
+              >{{ cat.label }}</text>
+            </view>
+          </scroll-view>
+        </view>
 
-      <!-- 视频生成入口 -->
-      <view v-if="false" class="video-entry-card" @click="goToVideoGenerator">
-        <view class="video-entry-content">
-          <text class="video-entry-icon">🎬</text>
-          <view class="video-entry-text">
-            <text class="video-entry-title">视频生成</text>
-            <text class="video-entry-desc">图片 + 音频 = 精美视频</text>
+        <!-- 音色网格 -->
+        <scroll-view scroll-y class="voice-grid-scroll">
+          <view class="voice-card-grid">
+            <view
+              v-for="voice in filteredVoices"
+              :key="voice.id"
+              class="voice-card"
+              :class="{ active: selectedVoice === voice.id }"
+              @click="selectVoiceAndClose(voice.id)"
+            >
+              <view class="voice-card-icon">
+                <text>{{ voice.gender === 'female' ? '👩' : '👨' }}</text>
+              </view>
+              <text class="voice-card-name">{{ voice.name }}</text>
+              <view v-if="selectedVoice === voice.id" class="voice-card-check">✓</view>
+              <view class="voice-card-play" @click.stop="previewVoice(voice.id)">
+                <text>{{ previewingVoice === voice.id ? '⏹' : '▶' }}</text>
+              </view>
+            </view>
           </view>
+        </scroll-view>
+
+        <!-- 关闭按钮 -->
+        <view class="panel-close-bar" @click="showVoicePanel = false">
+          <view class="panel-close-bar-line"></view>
         </view>
-        <text class="video-entry-arrow">→</text>
       </view>
     </view>
 
-    <!-- 新手引导 -->
-    <view v-if="showGuide" class="guide-overlay">
-      <view class="guide-content">
-        <view class="guide-header">
-          <text class="guide-step">步骤 {{ guideStep }} / {{ guideSteps.length }}</text>
-        </view>
-        <view class="guide-body">
-          <text class="guide-title">{{ guideSteps[guideStep - 1]?.title }}</text>
-          <text class="guide-desc">{{ guideSteps[guideStep - 1]?.desc }}</text>
-        </view>
-        <view class="guide-dots">
-          <view
-            v-for="i in guideSteps.length"
-            :key="i"
-            class="guide-dot"
-            :class="{ active: i === guideStep }"
-          />
-        </view>
-        <view class="guide-actions">
-          <button v-if="guideStep > 1" class="guide-btn skip" @click="closeGuide">
-            <text>跳过</text>
-          </button>
-          <button v-if="guideStep < guideSteps.length" class="guide-btn next" @click="nextGuideStep">
-            <text>下一步</text>
-          </button>
-          <button v-else class="guide-btn finish" @click="closeGuide">
-            <text>开始使用</text>
-          </button>
+    <!-- 参数面板 -->
+    <view v-if="showParamsPanel" class="bottom-panel-overlay" @click="showParamsPanel = false">
+      <view class="bottom-panel" @click.stop>
+        <view class="panel-header">
+          <text class="panel-title">参数调节</text>
+          <text class="panel-close" @click="showParamsPanel = false">✕</text>
         </view>
+        <scroll-view scroll-y class="panel-body">
+          <view class="param-item">
+            <view class="param-header">
+              <text class="param-label">语速</text>
+              <text class="param-value">{{ voiceParams.speed.toFixed(1) }}x</text>
+            </view>
+            <slider
+              :value="(voiceParams.speed - 0.5) * 100 / 1.5"
+              :min="0" :max="100"
+              @changing="(e: any) => voiceParams.speed = Number((0.5 + (e.detail.value / 100) * 1.5).toFixed(1))"
+              activeColor="#4F46E5" backgroundColor="#e5e7eb" block-size="20"
+            />
+            <view class="param-range"><text>0.5x</text><text>2.0x</text></view>
+          </view>
+          <view class="param-item">
+            <view class="param-header">
+              <text class="param-label">音调</text>
+              <text class="param-value">{{ voiceParams.pitch > 0 ? '+' : '' }}{{ voiceParams.pitch }}</text>
+            </view>
+            <slider
+              :value="(voiceParams.pitch + 500) / 10"
+              :min="0" :max="100"
+              @changing="(e: any) => voiceParams.pitch = Math.round(e.detail.value * 10 - 500)"
+              activeColor="#4F46E5" backgroundColor="#e5e7eb" block-size="20"
+            />
+            <view class="param-range"><text>-500</text><text>+500</text></view>
+          </view>
+          <view class="param-item">
+            <view class="param-header">
+              <text class="param-label">音量</text>
+              <text class="param-value">{{ voiceParams.volume }}%</text>
+            </view>
+            <slider
+              :value="voiceParams.volume"
+              :min="0" :max="100"
+              @changing="(e: any) => voiceParams.volume = e.detail.value"
+              activeColor="#4F46E5" backgroundColor="#e5e7eb" block-size="20"
+            />
+            <view class="param-range"><text>0%</text><text>100%</text></view>
+          </view>
+        </scroll-view>
       </view>
     </view>
 
@@ -228,29 +186,23 @@
         <text class="quota-title">今日免费额度已用完</text>
         <text class="quota-desc">开通会员解锁更多生成次数</text>
         <view class="quota-actions">
-          <button class="quota-btn primary" @click="goToMember">
-            <text>开通会员</text>
-          </button>
-          <button class="quota-btn secondary" @click="showQuotaModal = false">
-            <text>稍后再说</text>
-          </button>
+          <button class="quota-btn primary" @click="goToMember"><text>开通会员</text></button>
+          <button class="quota-btn secondary" @click="showQuotaModal = false"><text>稍后再说</text></button>
         </view>
       </view>
     </view>
 
     <!-- 专辑列表面板 -->
-    <view v-if="showAlbumListPanel" class="album-panel" @click="showAlbumListPanel = false">
-      <view class="album-content" @click.stop>
-        <view class="album-header">
-          <text class="album-title">选择专辑</text>
-          <text class="album-close" @click="showAlbumListPanel = false">✕</text>
+    <view v-if="showAlbumListPanel" class="bottom-panel-overlay" @click="showAlbumListPanel = false">
+      <view class="bottom-panel" @click.stop>
+        <view class="panel-header">
+          <text class="panel-title">选择专辑</text>
+          <text class="panel-close" @click="showAlbumListPanel = false">✕</text>
         </view>
-        <scroll-view scroll-y class="album-list" :scroll-top="scrollTop">
+        <scroll-view scroll-y class="panel-body">
           <view
-            v-for="album in albums"
-            :key="album.id"
-            class="album-item"
-            :class="{ active: selectedAlbum?.id === album.id }"
+            v-for="album in albums" :key="album.id"
+            class="album-item" :class="{ active: selectedAlbum?.id === album.id }"
             @click="selectAlbum(album)"
           >
             <view class="album-item-content">
@@ -259,46 +211,34 @@
             </view>
             <text v-if="selectedAlbum?.id === album.id" class="album-check">✓</text>
           </view>
-          <view v-if="albums.length === 0" class="album-empty">
-            <text>暂无专辑</text>
-          </view>
+          <view v-if="albums.length === 0" class="album-empty"><text>暂无专辑</text></view>
         </scroll-view>
-        <button class="album-create-btn" @click="showAlbumListPanel = false; showAlbumPanel = true">
+        <button class="panel-action-btn" @click="showAlbumListPanel = false; showAlbumPanel = true">
           <text>+ 新建专辑</text>
         </button>
       </view>
     </view>
 
     <!-- 新建专辑面板 -->
-    <view v-if="showAlbumPanel" class="album-panel" @click="showAlbumPanel = false">
-      <view class="album-content" @click.stop>
-        <view class="album-header">
-          <text class="album-title">新建专辑</text>
-          <text class="album-close" @click="showAlbumPanel = false">✕</text>
+    <view v-if="showAlbumPanel" class="bottom-panel-overlay" @click="showAlbumPanel = false">
+      <view class="bottom-panel" @click.stop>
+        <view class="panel-header">
+          <text class="panel-title">新建专辑</text>
+          <text class="panel-close" @click="showAlbumPanel = false">✕</text>
         </view>
-        <view class="album-form">
+        <view class="panel-body">
           <view class="form-item">
             <text class="form-label">专辑名称 *</text>
-            <input
-              v-model="newAlbumTitle"
-              class="form-input"
-              placeholder="例如:我的有声书"
-              maxlength="50"
-            />
+            <input v-model="newAlbumTitle" class="form-input" placeholder="例如:我的有声书" maxlength="50" />
           </view>
           <view class="form-item">
             <text class="form-label">描述(可选)</text>
-            <textarea
-              v-model="newAlbumDescription"
-              class="form-textarea"
-              placeholder="简单描述这个专辑..."
-              maxlength="200"
-            />
+            <textarea v-model="newAlbumDescription" class="form-textarea" placeholder="简单描述这个专辑..." maxlength="200" />
           </view>
+          <button class="panel-action-btn" :disabled="!newAlbumTitle.trim()" @click="createAlbum">
+            <text>创建专辑</text>
+          </button>
         </view>
-        <button class="album-submit-btn" :disabled="!newAlbumTitle.trim()" @click="createAlbum">
-          <text>创建专辑</text>
-        </button>
       </view>
     </view>
   </view>
@@ -315,40 +255,30 @@ import type { VoiceParams } from '../../types';
 const userStore = useUserStore();
 const audioStore = useAudioStore();
 
-// 状态
 const text = ref('');
 const selectedVoice = ref('voice_01');
-const voiceParams = ref<VoiceParams>({
-  speed: 1.0,
-  pitch: 0,
-  volume: 50,
-});
+const voiceParams = ref<VoiceParams>({ speed: 1.0, pitch: 0, volume: 50 });
 const generating = ref(false);
-const btnPressed = ref(false);
 const previewingVoice = ref<string | null>(null);
 let previewAudio: HTMLAudioElement | null = null;
 
-// 文档上传状态
-interface UploadedDoc {
-  fileName: string;
-  wordCount: number;
-  fileSize: number;
-  format: string;
-  truncated: boolean;
-}
-const uploadedDoc = ref<UploadedDoc>({
-  fileName: '',
-  wordCount: 0,
-  fileSize: 0,
-  format: '',
-  truncated: false,
-});
+// 文档上传
+interface UploadedDoc { fileName: string; wordCount: number; fileSize: number; format: string; truncated: boolean; }
+const uploadedDoc = ref<UploadedDoc>({ fileName: '', wordCount: 0, fileSize: 0, format: '', truncated: false });
 const uploading = ref(false);
 
-// scroll-top 用于避免 scrollTop 错误
-const scrollTop = ref(0);
+// 面板状态
+const showVoicePanel = ref(false);
+const showParamsPanel = ref(false);
+const voiceCategory = ref('all');
+const voiceCategories = [
+  { key: 'all', label: '全部' },
+  { key: 'hot', label: '热门' },
+  { key: 'female', label: '女声' },
+  { key: 'male', label: '男声' },
+];
 
-// 专辑相关状态
+// 专辑
 const selectedAlbum = ref<{ id: string; title: string } | null>(null);
 const albums = ref<any[]>([]);
 const showAlbumListPanel = ref(false);
@@ -356,1288 +286,318 @@ const showAlbumPanel = ref(false);
 const newAlbumTitle = ref('');
 const newAlbumDescription = ref('');
 
-// 新手引导状态
-const showGuide = ref(false); // 已禁用引导功能
-const guideStep = ref(1);
-const guideSteps = [
-  { step: 1, title: '输入文本', desc: '在文本框中输入要转换的文本内容', target: '.input-card' },
-  { step: 2, title: '选择音色', desc: '选择你喜欢的声音音色', target: '.voice-card' },
-  { step: 3, title: '调整参数', desc: '可以调整语速、音调、音量', target: '.params-card' },
-  { step: 4, title: '生成音频', desc: '点击按钮开始生成', target: '.generate-btn' },
-];
-
-// 额度用尽弹窗
 const showQuotaModal = ref(false);
 
-// 计算属性
-const canGenerate = computed(() => {
-  return text.value.trim().length > 0 && !generating.value;
+const canGenerate = computed(() => text.value.trim().length > 0 && !!selectedAlbum.value && !generating.value);
+const currentVoiceName = computed(() => {
+  const v = audioStore.voices.find(item => item.id === selectedVoice.value);
+  return v ? v.name : '';
+});
+const filteredVoices = computed(() => {
+  if (voiceCategory.value === 'all') return audioStore.voices;
+  if (voiceCategory.value === 'female') return audioStore.voices.filter(v => v.gender === 'female');
+  if (voiceCategory.value === 'male') return audioStore.voices.filter(v => v.gender === 'male');
+  // hot: return all for now (could be flagged by backend)
+  return audioStore.voices;
 });
+function selectVoiceAndClose(id: string) { selectedVoice.value = id; showVoicePanel.value = false; }
 
-// 专辑相关函数
 async function fetchAlbums() {
   try {
     const result = await get<{ albums: any[] }>('/book-generator/books');
     albums.value = result.albums || [];
-  } catch (error) {
-    console.error('获取专辑列表失败:', error);
-  }
+  } catch (error) { console.error('获取专辑列表失败:', error); }
 }
 
-function selectAlbum(album: any) {
-  selectedAlbum.value = { id: album.id, title: album.title };
-  showAlbumListPanel.value = false;
-}
+function selectAlbum(album: any) { selectedAlbum.value = { id: album.id, title: album.title }; showAlbumListPanel.value = false; }
 
 async function createAlbum() {
   if (!newAlbumTitle.value.trim()) return;
-
   try {
-    const result = await post<{ id: number | string; title: string; description?: string }>('/book-generator/books', {
-      title: newAlbumTitle.value.trim(),
-      description: newAlbumDescription.value.trim(),
-    });
-
+    const result = await post<{ id: number | string; title: string }>('/book-generator/books', { title: newAlbumTitle.value.trim(), description: newAlbumDescription.value.trim() });
     if (result && result.id) {
       selectedAlbum.value = { id: String(result.id), title: result.title };
-      await fetchAlbums(); // 刷新列表
+      await fetchAlbums();
       showAlbumPanel.value = false;
       newAlbumTitle.value = '';
       newAlbumDescription.value = '';
       uni.showToast({ title: '专辑创建成功', icon: 'success' });
     }
-  } catch (error) {
-    console.error('创建专辑失败:', error);
-    uni.showToast({ title: '创建失败', icon: 'none' });
-  }
+  } catch (error) { uni.showToast({ title: '创建失败', icon: 'none' }); }
 }
 
-// 加载用户偏好设置
 async function loadUserPreferences() {
   try {
-    // 先尝试从本地加载
     const localPrefs = uni.getStorageSync('userPreferences');
     if (localPrefs) {
-      if (localPrefs.defaultVoiceId) {
-        selectedVoice.value = localPrefs.defaultVoiceId;
-      }
-      if (localPrefs.defaultVolume !== undefined) {
-        voiceParams.value.volume = localPrefs.defaultVolume;
-      }
+      if (localPrefs.defaultVoiceId) selectedVoice.value = localPrefs.defaultVoiceId;
+      if (localPrefs.defaultVolume !== undefined) voiceParams.value.volume = localPrefs.defaultVolume;
     }
-
-    // 从服务器加载
     const result = await get<any>('/user/preferences');
     if (result) {
-      if (result.defaultVoiceId) {
-        selectedVoice.value = result.defaultVoiceId;
-      }
-      if (result.defaultVolume !== undefined) {
-        voiceParams.value.volume = result.defaultVolume;
-      }
-      // 保存到本地
+      if (result.defaultVoiceId) selectedVoice.value = result.defaultVoiceId;
+      if (result.defaultVolume !== undefined) voiceParams.value.volume = result.defaultVolume;
       uni.setStorageSync('userPreferences', result);
     }
-  } catch (error) {
-    console.error('加载用户偏好设置失败:', error);
-  }
+  } catch (error) { /* ignore */ }
 }
 
-// 初始化
 onMounted(async () => {
   await audioStore.fetchVoices();
-  await fetchAlbums(); // 获取专辑列表
-  await loadUserPreferences(); // 加载用户偏好设置
-
-  // 检查是否显示新手引导(已禁用)
-  // const hasSeenGuide = uni.getStorageSync('hasSeenCreateGuide');
-  // if (!hasSeenGuide) {
-  //   setTimeout(() => {
-  //     showGuide.value = true;
-  //   }, 500);
-  // }
+  await fetchAlbums();
+  await loadUserPreferences();
 });
 
-// 跳转到视频生成页面
-function goToVideoGenerator() {
-  uni.navigateTo({ url: '/pages/video-generator/index' });
-}
-
-// 跳转到 AI 生成内容页面
-function goToAIGenerate() {
-  uni.navigateTo({ url: '/pages/ai-generate/index' });
-}
+function goToAIGenerate() { uni.navigateTo({ url: '/pages/ai-generate/index' }); }
 
-// 页面显示时检查是否有 AI 生成的文本
 onShow(() => {
   const aiText = uni.getStorageSync('ai_generated_text');
-  if (aiText) {
-    text.value = aiText;
-    uni.removeStorageSync('ai_generated_text');
-    uni.showToast({ title: '已填充 AI 生成的文本', icon: 'success' });
-  }
+  if (aiText) { text.value = aiText; uni.removeStorageSync('ai_generated_text'); uni.showToast({ title: '已填充 AI 生成的文本', icon: 'success' }); }
 });
 
-// 清空文本
-function clearText() {
-  text.value = '';
-}
-
-// 粘贴文本
-async function pasteText() {
-  const content = await uni.getClipboardData();
-  if (content.data) {
-    text.value = content.data;
-  }
-}
+function clearText() { text.value = ''; }
+async function pasteText() { const content = await uni.getClipboardData(); if (content.data) text.value = content.data; }
 
-// 上传文档
+// 上传文档(保持原逻辑)
 async function handleUploadDoc() {
   if (uploading.value) return;
-
   try {
-    // 使用 uni.chooseFile 选择文件(H5和App端支持)
     // #ifdef H5
-    const input = document.createElement('input');
-    input.type = 'file';
-    input.accept = '.txt,.docx,.pdf';
-    input.onchange = async (e: any) => {
-      const file = e.target.files?.[0];
-      if (!file) return;
-      await processUploadedFile(file);
-    };
-    input.click();
-    return;
+    const input = document.createElement('input'); input.type = 'file'; input.accept = '.txt,.docx,.pdf';
+    input.onchange = async (e: any) => { const file = e.target.files?.[0]; if (!file) return; await processUploadedFile(file); };
+    input.click(); return;
     // #endif
-
     // #ifdef APP-PLUS
-    // App 端使用系统文件选择器
-    const result = await (uni as any).chooseFile({
-      extension: ['.txt', '.docx', '.pdf'],
-      type: 'file',
-    });
-    if (result?.tempFiles?.[0]) {
-      await processUploadedFile(result.tempFiles[0]);
-    }
+    const result = await (uni as any).chooseFile({ extension: ['.txt', '.docx', '.pdf'], type: 'file' });
+    if (result?.tempFiles?.[0]) await processUploadedFile(result.tempFiles[0]);
     // #endif
-
-    // 小程序端暂不支持
     // #ifdef MP-WEIXIN
     uni.showToast({ title: '小程序暂不支持文档上传', icon: 'none' });
     // #endif
-  } catch (error: any) {
-    console.error('选择文件失败:', error);
-    if (error.message && !error.message.includes('cancel')) {
-      uni.showToast({ title: '选择文件失败', icon: 'none' });
-    }
-  }
+  } catch (error: any) { if (error.message && !error.message.includes('cancel')) uni.showToast({ title: '选择文件失败', icon: 'none' }); }
 }
 
-// 处理上传的文件
 async function processUploadedFile(file: any) {
   uploading.value = true;
-
-  // 检查文件大小(20MB)
-  if (file.size && file.size > 20 * 1024 * 1024) {
-    uni.showToast({ title: '文件过大,最大支持20MB', icon: 'none' });
-    uploading.value = false;
-    return;
-  }
-
-  // 检查格式
+  if (file.size && file.size > 20 * 1024 * 1024) { uni.showToast({ title: '文件过大,最大支持20MB', icon: 'none' }); uploading.value = false; return; }
   const name = (file.name || '').toLowerCase();
-  if (!name.endsWith('.txt') && !name.endsWith('.docx') && !name.endsWith('.pdf')) {
-    uni.showToast({ title: '不支持的格式,请上传 .txt .docx .pdf', icon: 'none' });
-    uploading.value = false;
-    return;
-  }
-
+  if (!name.endsWith('.txt') && !name.endsWith('.docx') && !name.endsWith('.pdf')) { uni.showToast({ title: '不支持的格式', icon: 'none' }); uploading.value = false; return; }
   uni.showLoading({ title: '解析文档中...', mask: true });
-
   try {
-    const filePath = file.path || file.tempFilePath || file;
     let result: any;
-
     // #ifdef H5
-    // H5 端直接使用 FormData 上传
-    const formData = new FormData();
-    formData.append('file', file);
+    const formData = new FormData(); formData.append('file', file);
     const baseUrl = (await import('../../utils/config')).getApiBaseUrl();
     const token = uni.getStorageSync('token');
-    const headers: Record<string, string> = {};
-    if (token) headers['Authorization'] = `Bearer ${token}`;
-
-    const response = await fetch(baseUrl + '/tts/upload-document', {
-      method: 'POST',
-      headers,
-      body: formData,
-    });
+    const headers: Record<string, string> = {}; if (token) headers['Authorization'] = `Bearer ${token}`;
+    const response = await fetch(baseUrl + '/tts/upload-document', { method: 'POST', headers, body: formData });
     const json = await response.json();
-    if (json.code === 0) {
-      result = json.data;
-    } else {
-      throw new Error(json.message || '解析失败');
-    }
+    if (json.code === 0) result = json.data; else throw new Error(json.message || '解析失败');
     // #endif
-
     // #ifdef APP-PLUS
-    result = await uploadFile('/tts/upload-document', filePath);
+    result = await uploadFile('/tts/upload-document', file.path || file.tempFilePath || file);
     // #endif
-
     if (result && result.text) {
-      // 更新上传状态
-      uploadedDoc.value = {
-        fileName: result.fileName || file.name,
-        wordCount: result.wordCount || result.text.length,
-        fileSize: result.fileSize || file.size || 0,
-        format: result.format || '',
-        truncated: result.truncated || false,
-      };
-
-      // 截取前2000字填充到文本框
-      const maxLen = 2000;
+      uploadedDoc.value = { fileName: result.fileName || file.name, wordCount: result.wordCount || result.text.length, fileSize: result.fileSize || file.size || 0, format: result.format || '', truncated: result.truncated || false };
       let displayText = result.text;
-      if (displayText.length > maxLen) {
-        displayText = displayText.substring(0, maxLen);
-        uploadedDoc.value.truncated = true;
-      }
+      if (displayText.length > 2000) { displayText = displayText.substring(0, 2000); uploadedDoc.value.truncated = true; }
       text.value = displayText;
-
       uni.hideLoading();
       uni.showToast({ title: `解析成功,${uploadedDoc.value.wordCount}字`, icon: 'success' });
-    } else {
-      throw new Error('文档内容为空');
-    }
-  } catch (error: any) {
-    uni.hideLoading();
-    console.error('文档上传失败:', error);
-    uni.showToast({
-      title: error.message || '文档解析失败',
-      icon: 'none',
-      duration: 3000,
-    });
-  } finally {
-    uploading.value = false;
-  }
-}
-
-// 清除已上传的文档
-function clearDoc() {
-  uploadedDoc.value = {
-    fileName: '',
-    wordCount: 0,
-    fileSize: 0,
-    format: '',
-    truncated: false,
-  };
+    } else { throw new Error('文档内容为空'); }
+  } catch (error: any) { uni.hideLoading(); uni.showToast({ title: error.message || '文档解析失败', icon: 'none', duration: 3000 }); }
+  finally { uploading.value = false; }
 }
 
-// 格式化文件大小
-function formatFileSize(bytes: number): string {
-  if (!bytes || bytes <= 0) return '0 B';
-  if (bytes < 1024) return bytes + ' B';
-  if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
-  return (bytes / 1024 / 1024).toFixed(2) + ' MB';
-}
+function clearDoc() { uploadedDoc.value = { fileName: '', wordCount: 0, fileSize: 0, format: '', truncated: false }; }
 
-// 试听音色
 async function previewVoice(voiceId: string) {
-  // 如果正在试听当前音色,停止试听
-  if (previewingVoice.value === voiceId) {
-    stopPreview();
-    return;
-  }
-
-  // 停止之前的试听
+  if (previewingVoice.value === voiceId) { stopPreview(); return; }
   stopPreview();
-
   try {
-    previewingVoice.value = voiceId;
-    uni.showLoading({ title: '生成试听音频...' });
-
-    // 调用API生成预览音频
-    const res = await post<{ previewText: string; voiceId: string; audioUrl: string }>('/tts/preview', {
-      voiceId,
-      voiceParams: voiceParams.value,
-    });
-
+    previewingVoice.value = voiceId; uni.showLoading({ title: '生成试听音频...' });
+    const res = await post<{ audioUrl: string }>('/tts/preview', { voiceId, voiceParams: voiceParams.value });
     uni.hideLoading();
-
     if (res.audioUrl) {
       // #ifdef H5
-      previewAudio = new Audio(res.audioUrl);
-      previewAudio.play();
-      previewAudio.onended = () => {
-        previewingVoice.value = null;
-        previewAudio = null;
-      };
+      previewAudio = new Audio(res.audioUrl); previewAudio.play();
+      previewAudio.onended = () => { previewingVoice.value = null; previewAudio = null; };
       // #endif
-
       // #ifndef H5
-      // 非H5环境直接停止
-      uni.showToast({ title: '试听已播放', icon: 'success' });
-      setTimeout(() => {
-        previewingVoice.value = null;
-      }, 2000);
+      uni.showToast({ title: '试听已播放', icon: 'success' }); setTimeout(() => { previewingVoice.value = null; }, 2000);
       // #endif
     }
-  } catch (error: any) {
-    uni.hideLoading();
-    uni.showToast({ title: '试听生成失败', icon: 'none' });
-    previewingVoice.value = null;
-  }
+  } catch (error: any) { uni.hideLoading(); uni.showToast({ title: '试听生成失败', icon: 'none' }); previewingVoice.value = null; }
 }
 
-// 停止试听
-function stopPreview() {
-  if (previewAudio) {
-    previewAudio.pause();
-    previewAudio = null;
-  }
-  previewingVoice.value = null;
-}
+function stopPreview() { if (previewAudio) { previewAudio.pause(); previewAudio = null; } previewingVoice.value = null; }
 
-// 生成音频
 async function handleGenerate() {
   if (!canGenerate.value) return;
-
-  // 检查额度(无限额度 -1 跳过检查)
   const quota = userStore.memberStatus?.quota;
-  const quotaExhausted = quota && quota.dailyRemaining !== -1 && quota.dailyRemaining <= 0;
-  if (quotaExhausted) {
-    showQuotaModal.value = true;
-    return;
-  }
-
-  // 如果没有选择专辑,使用后端自动创建的默认书籍
+  if (quota && quota.dailyRemaining !== -1 && quota.dailyRemaining <= 0) { showQuotaModal.value = true; return; }
   const bookId = selectedAlbum.value?.id;
-
   generating.value = true;
   try {
-    const result = await audioStore.generateAudio(
-      text.value,
-      selectedVoice.value,
-      voiceParams.value,
-      { bookId }
-    );
-
-    // 清空文本,方便下次输入
+    const result = await audioStore.generateAudio(text.value, selectedVoice.value, voiceParams.value, { bookId });
     text.value = '';
-
-    // 和书籍创建一样,跳转到详情页等待音频生成
     uni.showToast({ title: '任务已提交', icon: 'success' });
-    setTimeout(() => {
-      uni.navigateTo({
-        url: `/pages/book-generator/detail?id=${result.bookId}`
-      });
-    }, 800);
+    setTimeout(() => { uni.navigateTo({ url: `/pages/book-generator/detail?id=${result.bookId}` }); }, 800);
   } catch (error: any) {
-    console.error('生成失败:', error);
-
-    if (error.message?.includes('额度') || error.message?.includes('quota')) {
-      showQuotaModal.value = true;
-    } else {
-      uni.showToast({
-        title: error.message || '提交失败,请重试',
-        icon: 'none'
-      });
-    }
-  } finally {
-    generating.value = false;
-  }
-}
-
-// 新手引导 - 下一步
-function nextGuideStep() {
-  if (guideStep.value < guideSteps.length) {
-    guideStep.value++;
-  }
+    if (error.message?.includes('额度') || error.message?.includes('quota')) showQuotaModal.value = true;
+    else uni.showToast({ title: error.message || '提交失败,请重试', icon: 'none' });
+  } finally { generating.value = false; }
 }
 
-// 新手引导 - 关闭
-function closeGuide() {
-  showGuide.value = false;
-  uni.setStorageSync('hasSeenCreateGuide', true);
-}
-
-// 跳转到会员页
-function goToMember() {
-  showQuotaModal.value = false;
-  uni.navigateTo({ url: '/pages/member/index' });
-}
+function goToMember() { showQuotaModal.value = false; uni.navigateTo({ url: '/pages/member/index' }); }
 </script>
 
 <style scoped>
-.page {
-  min-height: 100vh;
-  background: #f5f5f5;
-}
-
-.nav-bar {
-  position: fixed;
-  top: 0;
-  left: 0;
-  right: 0;
-  height: 88rpx;
-  background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
-  z-index: 100;
-  padding-top: 44rpx;
-}
-
-.nav-content {
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  padding: 0 32rpx;
-  height: 88rpx;
-}
-
-.page-title {
-  font-size: 34rpx;
-  font-weight: 600;
-  color: #ffffff;
-}
-
-.main-content {
-  padding: 132rpx 32rpx 220rpx;
-}
-
-.card {
-  background: #ffffff;
-  border-radius: 24rpx;
-  padding: 32rpx;
-  margin-bottom: 24rpx;
-  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
-}
-
-.card-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 24rpx;
-}
-
-.card-title {
-  font-size: 32rpx;
-  font-weight: 600;
-  color: #1f2937;
-}
-
-/* 文档上传卡片 */
-.doc-upload-card {
-  border-left: 6rpx solid #4F46E5;
-}
-
-.doc-upload-area {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  padding: 40rpx 20rpx;
-  border: 2rpx dashed #d1d5db;
-  border-radius: 16rpx;
-  background: #f9fafb;
-  transition: all 0.2s;
-}
-
-.doc-upload-area:active {
-  background: #eef2ff;
-  border-color: #4F46E5;
-}
-
-.doc-upload-icon {
-  font-size: 64rpx;
-  margin-bottom: 12rpx;
-}
-
-.doc-upload-text {
-  font-size: 28rpx;
-  font-weight: 500;
-  color: #4F46E5;
-  margin-bottom: 8rpx;
-}
-
-.doc-upload-hint {
-  font-size: 22rpx;
-  color: #9ca3af;
-}
+.page { min-height: 100vh; background: #f5f5f5; display: flex; flex-direction: column; }
 
-.upload-status {
-  font-size: 24rpx;
-  color: #6b7280;
-}
-
-.upload-status.uploading {
-  color: #f59e0b;
-  animation: pulse 1.5s ease-in-out infinite;
-}
+/* 主内容区 */
+.main-content { flex: 1; padding: 16rpx 24rpx 0; overflow-y: auto; }
+.input-area { background: #ffffff; border-radius: 16rpx; padding: 24rpx; }
 
-@keyframes pulse {
-  0%, 100% { opacity: 1; }
-  50% { opacity: 0.5; }
-}
-
-/* 文档信息栏 */
-.doc-info-bar {
+/* 专辑选择器(页面内下拉) */
+.album-selector {
   display: flex;
   align-items: center;
-  justify-content: space-between;
-  padding: 20rpx;
-  background: #f0fdf4;
-  border-radius: 16rpx;
-  border: 2rpx solid #86efac;
+  gap: 12rpx;
+  padding: 20rpx 24rpx;
+  background: #f3f4f6;
+  border-radius: 12rpx;
+  border: 2rpx solid #e5e7eb;
+  margin-top: 20rpx;
 }
-
-.doc-info-left {
-  display: flex;
-  align-items: center;
-  gap: 16rpx;
+.album-selector:active { opacity: 0.7; }
+.album-selector-label { font-size: 26rpx; color: #6b7280; flex-shrink: 0; }
+.album-selector-value {
   flex: 1;
-  min-width: 0;
-}
-
-.doc-format-badge {
-  flex-shrink: 0;
-  font-size: 22rpx;
-  font-weight: 700;
-  color: #ffffff;
-  background: linear-gradient(135deg, #4F46E5, #7C3AED);
-  padding: 6rpx 14rpx;
-  border-radius: 8rpx;
-}
-
-.doc-info-text {
-  display: flex;
-  flex-direction: column;
-  gap: 4rpx;
-  min-width: 0;
-}
-
-.doc-filename {
   font-size: 26rpx;
-  font-weight: 600;
   color: #1f2937;
+  font-weight: 500;
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
 }
-
-.doc-meta {
-  font-size: 22rpx;
-  color: #6b7280;
-}
-
-.doc-truncated-hint {
-  font-size: 20rpx;
-  color: #f59e0b;
-}
-
-.doc-remove-btn {
-  flex-shrink: 0;
-  width: 48rpx;
-  height: 48rpx;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  font-size: 32rpx;
-  color: #ef4444;
-  background: #fef2f2;
-  border-radius: 50%;
-}
-
-.doc-remove-btn:active {
-  background: #fca5a5;
-  color: #ffffff;
-}
-
-/* 上传中提示 */
-.doc-uploading-bar {
-  padding: 24rpx;
-  text-align: center;
-  background: #fefce8;
-  border-radius: 12rpx;
-  font-size: 26rpx;
-  color: #a16207;
-  margin-top: 16rpx;
-}
-
-.word-count {
-  font-size: 24rpx;
-  color: #6b7280;
-}
-
-.word-count.warning {
-  color: #f59e0b;
-}
-
-.word-count.error {
-  color: #ef4444;
-  font-weight: 600;
-}
-
-.text-input {
-  width: 100%;
-  height: 300rpx;
-  font-size: 28rpx;
-  color: #1f2937;
-  line-height: 1.6;
-  overflow-y: auto;
-}
-
-.input-actions {
-  display: flex;
-  justify-content: flex-end;
-  gap: 16rpx;
-  margin-top: 16rpx;
-}
-
-.input-hint {
-  margin-top: 12rpx;
-  text-align: right;
-}
-
-.input-hint text {
-  font-size: 22rpx;
-  color: #9ca3af;
-}
-
-.action-btn {
-  font-size: 24rpx;
-  color: #6b7280;
-  background: #f3f4f6;
-  padding: 12rpx 24rpx;
-  border-radius: 12rpx;
-  border: none;
-  transition: all 0.2s;
-}
-
-.action-btn:active {
-  opacity: 0.7;
-  transform: scale(0.98);
-}
-
-.action-btn::after {
-  border: none;
-}
-
-.voice-list {
-  white-space: nowrap;
-  margin-top: 24rpx;
-}
-
-/* 音色网格布局 - 2列 */
-.voice-grid {
-  display: grid;
-  grid-template-columns: repeat(2, 1fr);
-  gap: 16rpx;
-  margin-top: 24rpx;
-}
-
-.voice-item {
-  position: relative;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  padding: 24rpx 16rpx;
-  border-radius: 16rpx;
-  background: #f9fafb;
-  border: 2rpx solid transparent;
-  transition: all 0.3s;
-}
-
-.voice-item.active {
-  background: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%);
-  border-color: #4f46e5;
-}
-
-.voice-item.active .voice-name {
-  color: #ffffff;
-}
-
-.voice-item:active {
-  transform: scale(0.95);
-}
-
-.voice-icon {
-  font-size: 48rpx;
-  margin-bottom: 12rpx;
-}
-
-.voice-name {
-  font-size: 26rpx;
-  font-weight: 500;
-  color: #1f2937;
-  margin-bottom: 4rpx;
-}
-
-.voice-preview-btn {
-  position: absolute;
-  top: 8rpx;
-  right: 8rpx;
-  width: 40rpx;
-  height: 40rpx;
-  background: rgba(79, 70, 229, 0.9);
-  border-radius: 50%;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  font-size: 20rpx;
-  color: #ffffff;
-}
-
-.voice-item.active .voice-preview-btn {
-  background: rgba(255, 255, 255, 0.9);
-  color: #4f46e5;
-}
-
-.voice-desc {
-  font-size: 20rpx;
-  color: #6b7280;
-}
-
-.param-item {
-  margin-top: 24rpx;
-}
-
-.param-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 12rpx;
-}
-
-.param-label {
-  font-size: 26rpx;
-  color: #6b7280;
-}
-
-.param-value {
-  font-size: 28rpx;
-  font-weight: 600;
-  color: #4F46E5;
-  background: #EEF2FF;
-  padding: 4rpx 16rpx;
-  border-radius: 8rpx;
-}
-
-.param-range {
-  display: flex;
-  justify-content: space-between;
-  margin-top: 8rpx;
-}
-
-.param-range text {
-  font-size: 22rpx;
-  color: #9ca3af;
-}
-
-/* 专辑选择 */
-.album-card {
-  background: #ffffff;
-  border-radius: 24rpx;
-  padding: 32rpx;
-  margin-bottom: 24rpx;
-  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
-}
-
-.card-header-row {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 20rpx;
-}
-
-.album-add-btn {
-  font-size: 28rpx;
-  color: #4f46e5;
-  font-weight: 500;
-}
-
-.album-selector {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  background: #f9fafb;
-  border-radius: 16rpx;
-  padding: 24rpx;
-}
-
-.album-name {
-  font-size: 28rpx;
-  color: #1f2937;
-  font-weight: 500;
-}
-
-.album-placeholder {
-  font-size: 28rpx;
-  color: #9ca3af;
-}
-
-.album-arrow {
-  font-size: 40rpx;
-  color: #9ca3af;
-}
-
-/* 专辑面板 */
-.album-panel {
-  position: fixed;
-  top: 0;
-  left: 0;
-  right: 0;
-  bottom: 0;
-  background: rgba(0, 0, 0, 0.5);
-  z-index: 1000;
-  display: flex;
-  align-items: flex-end;
-}
-
-.album-content {
-  width: 100%;
-  max-height: 80vh;
-  background: #ffffff;
-  border-radius: 32rpx 32rpx 0 0;
-  padding: 32rpx;
-  display: flex;
-  flex-direction: column;
-}
-
-.album-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 24rpx;
-}
-
-.album-title {
-  font-size: 34rpx;
-  font-weight: 600;
-  color: #1f2937;
-}
-
-.album-close {
-  font-size: 40rpx;
-  color: #9ca3af;
-  padding: 8rpx;
-}
-
-.album-list {
-  flex: 1;
-  max-height: 50vh;
-}
-
-.album-item {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  padding: 24rpx;
-  border-radius: 16rpx;
-  background: #f9fafb;
-  margin-bottom: 16rpx;
-}
-
-.album-item.active {
-  background: #eef2ff;
-  border: 2px solid #4f46e5;
-}
-
-.album-item-content {
-  flex: 1;
-}
-
-.album-item-title {
-  display: block;
-  font-size: 28rpx;
-  font-weight: 600;
-  color: #1f2937;
-  margin-bottom: 8rpx;
-}
-
-.album-item-desc {
-  display: block;
-  font-size: 24rpx;
-  color: #6b7280;
-}
-
-.album-check {
-  font-size: 32rpx;
-  color: #4f46e5;
-  margin-left: 16rpx;
-}
-
-.album-empty {
-  text-align: center;
-  padding: 60rpx 0;
-  color: #9ca3af;
-  font-size: 28rpx;
-}
-
-.album-create-btn,
-.album-submit-btn {
-  width: 100%;
-  height: 100rpx;
-  background: linear-gradient(135deg, #4f46e5 0%, #818cf8 100%);
-  border-radius: 24rpx;
-  border: none;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  font-size: 32rpx;
-  font-weight: 600;
-  color: #ffffff;
-  margin-top: 24rpx;
-}
-
-.album-create-btn::after,
-.album-submit-btn::after {
-  border: none;
-}
-
-/* 专辑表单 */
-.album-form {
-  margin: 24rpx 0;
-}
-
-.form-item {
-  margin-bottom: 24rpx;
-}
-
-.form-label {
-  display: block;
-  font-size: 28rpx;
-  color: #1f2937;
-  font-weight: 500;
-  margin-bottom: 12rpx;
-}
-
-.form-input {
-  width: 100%;
-  height: 88rpx;
-  background: #f9fafb;
-  border-radius: 16rpx;
-  padding: 0 24rpx;
-  font-size: 28rpx;
-  color: #1f2937;
-}
-
-.form-textarea {
-  width: 100%;
-  min-height: 160rpx;
-  background: #f9fafb;
-  border-radius: 16rpx;
-  padding: 24rpx;
-  font-size: 28rpx;
-  color: #1f2937;
-  line-height: 1.6;
-}
-
-.generate-btn {
-  position: fixed;
-  bottom: 0;
-  left: 0;
-  right: 0;
-  height: 120rpx;
-  padding: 16rpx 32rpx;
-  padding-bottom: calc(16rpx + env(safe-area-inset-bottom));
-  background: linear-gradient(135deg, #4f46e5 0%, #818cf8 100%);
-  border-radius: 0;
-  border: none;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  box-shadow: 0 -4rpx 24rpx rgba(79, 70, 229, 0.3);
-  transition: opacity 0.3s ease, transform 0.3s ease;
-  opacity: 1;
-  z-index: 1001;
-}
-
-.generate-btn::after {
-  border: none;
-}
-
-.generate-btn.disabled {
-  opacity: 0.5;
-  background: #9ca3af;
-  box-shadow: none;
-}
-
-.generate-btn.pressed {
-  transform: scale(0.98);
-}
-
-/* 视频生成入口卡片 */
-.video-entry-card {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-  border-radius: 16rpx;
-  padding: 30rpx;
-  margin-top: 20rpx;
-}
-
-.video-entry-content {
-  display: flex;
-  align-items: center;
-  gap: 20rpx;
-}
-
-.video-entry-icon {
-  font-size: 60rpx;
-}
-
-.video-entry-text {
-  display: flex;
-  flex-direction: column;
-  gap: 6rpx;
-}
-
-.video-entry-title {
-  font-size: 32rpx;
-  font-weight: 600;
-  color: #ffffff;
-}
-
-.video-entry-desc {
-  font-size: 24rpx;
-  color: rgba(255, 255, 255, 0.8);
-}
-
-.video-entry-arrow {
-  font-size: 40rpx;
-  color: #ffffff;
-}
-
-.btn-text {
-  font-size: 34rpx;
-  font-weight: 600;
-  color: #ffffff;
-  letter-spacing: 2rpx;
-}
-
-/* 音频生成入口卡片 */
-.audio-entry-card {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
-  border-radius: 16rpx;
-  padding: 30rpx;
-  margin-top: 20rpx;
-}
-
-.audio-entry-content {
-  display: flex;
-  align-items: center;
-  gap: 20rpx;
-}
-
-.audio-entry-icon {
-  font-size: 60rpx;
-}
-
-.audio-entry-text {
-  display: flex;
-  flex-direction: column;
-  gap: 6rpx;
-}
-
-.audio-entry-title {
-  font-size: 32rpx;
-  font-weight: 600;
-  color: #ffffff;
-}
-
-.audio-entry-desc {
-  font-size: 24rpx;
-  color: rgba(255, 255, 255, 0.8);
-}
-
-.audio-entry-arrow {
-  font-size: 40rpx;
-  color: #ffffff;
-}
-
-.ai-btn {
-  display: flex;
-  align-items: center;
-  gap: 8rpx;
-}
-
-/* 新手引导 */
-.guide-overlay {
-  position: fixed;
-  top: 0;
-  left: 0;
-  right: 0;
-  bottom: 0;
-  background: rgba(0, 0, 0, 0.8);
-  z-index: 3000;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-}
-
-.guide-content {
-  width: 600rpx;
-  background: #ffffff;
-  border-radius: 32rpx;
-  padding: 48rpx 40rpx;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-}
-
-.guide-header {
-  margin-bottom: 24rpx;
-}
-
-.guide-step {
-  font-size: 24rpx;
-  color: #9ca3af;
-}
-
-.guide-body {
-  text-align: center;
-  margin-bottom: 32rpx;
-}
-
-.guide-title {
-  font-size: 36rpx;
-  font-weight: 600;
-  color: #1f2937;
-  display: block;
-  margin-bottom: 16rpx;
-}
-
-.guide-desc {
-  font-size: 28rpx;
-  color: #6b7280;
-  display: block;
-  line-height: 1.6;
-}
-
-.guide-dots {
-  display: flex;
-  gap: 12rpx;
-  margin-bottom: 32rpx;
-}
-
-.guide-dot {
-  width: 12rpx;
-  height: 12rpx;
-  border-radius: 50%;
-  background: #e5e7eb;
-}
-
-.guide-dot.active {
-  background: #4f46e5;
-  width: 32rpx;
-  border-radius: 6rpx;
-}
-
-.guide-actions {
-  display: flex;
-  gap: 20rpx;
-  width: 100%;
-}
-
-.guide-btn {
-  flex: 1;
-  height: 88rpx;
-  border-radius: 44rpx;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  font-size: 30rpx;
-  border: none;
-}
-
-.guide-btn::after {
-  border: none;
-}
-
-.guide-btn.skip {
-  background: #f3f4f6;
-  color: #6b7280;
-}
-
-.guide-btn.next,
-.guide-btn.finish {
-  background: linear-gradient(135deg, #4f46e5 0%, #818cf8 100%);
-  color: #ffffff;
-}
-
-/* 额度用尽弹窗 */
-.quota-modal {
-  position: fixed;
-  top: 0;
-  left: 0;
-  right: 0;
-  bottom: 0;
-  background: rgba(0, 0, 0, 0.6);
-  z-index: 3000;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-}
-
-.quota-content {
-  width: 600rpx;
-  background: #ffffff;
-  border-radius: 32rpx;
-  padding: 60rpx 40rpx;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-}
-
-.quota-icon {
-  font-size: 100rpx;
-  margin-bottom: 24rpx;
-}
-
-.quota-title {
-  font-size: 36rpx;
-  font-weight: 600;
-  color: #1f2937;
-  margin-bottom: 12rpx;
-}
-
-.quota-desc {
-  font-size: 28rpx;
-  color: #6b7280;
-  margin-bottom: 40rpx;
-}
-
-.quota-actions {
-  width: 100%;
-  display: flex;
-  flex-direction: column;
-  gap: 20rpx;
-}
-
-.quota-btn {
-  width: 100%;
-  height: 88rpx;
-  border-radius: 44rpx;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  font-size: 30rpx;
-  border: none;
-}
-
-.quota-btn::after {
-  border: none;
-}
-
-.quota-btn.primary {
-  background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
-  color: #ffffff;
-}
-
-.quota-btn.secondary {
-  background: #f3f4f6;
-  color: #6b7280;
-}
+.album-selector-value.placeholder { color: #9ca3af; font-weight: 400; }
+.album-selector-arrow { font-size: 20rpx; color: #9ca3af; flex-shrink: 0; }
+
+.text-input { width: 100%; min-height: 600rpx; font-size: 30rpx; color: #1f2937; line-height: 1.7; }
+.word-count { display: block; text-align: right; font-size: 22rpx; color: #9ca3af; margin-top: 8rpx; }
+.word-count.warning { color: #f59e0b; }
+.word-count.error { color: #ef4444; font-weight: 600; }
+
+/* 文档信息 */
+.doc-info-bar { display: flex; align-items: center; gap: 12rpx; padding: 12rpx 16rpx; background: #f0fdf4; border-radius: 10rpx; border: 1rpx solid #86efac; margin-top: 12rpx; }
+.doc-format-badge { flex-shrink: 0; font-size: 20rpx; font-weight: 700; color: #fff; background: #4f46e5; padding: 4rpx 10rpx; border-radius: 6rpx; }
+.doc-filename { flex: 1; font-size: 24rpx; font-weight: 600; color: #1f2937; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.doc-meta { font-size: 20rpx; color: #6b7280; flex-shrink: 0; }
+.doc-truncated-hint { font-size: 18rpx; color: #f59e0b; flex-shrink: 0; }
+.doc-remove-btn { flex-shrink: 0; width: 36rpx; height: 36rpx; border-radius: 50%; background: #fef2f2; color: #ef4444; font-size: 24rpx; display: flex; align-items: center; justify-content: center; }
+.doc-uploading-bar { padding: 16rpx; text-align: center; background: #fefce8; border-radius: 10rpx; font-size: 24rpx; color: #a16207; margin-top: 12rpx; }
+
+/* 工具按钮行 */
+.tool-row { display: flex; gap: 12rpx; margin-top: 16rpx; }
+.tool-btn { flex: 1; font-size: 24rpx; color: #6b7280; background: #f3f4f6; padding: 14rpx 0; border-radius: 10rpx; text-align: center; }
+.tool-btn:active { opacity: 0.7; }
+
+/* 底部固定区:Tab 工具栏风格 */
+.bottom-bar { background: #ffffff; box-shadow: 0 -2rpx 16rpx rgba(0,0,0,0.08); padding-bottom: calc(env(safe-area-inset-bottom) + 120rpx); }
+
+/* 主操作行:左侧快捷信息 + 右侧生成按钮 */
+.bottom-action-row { display: flex; align-items: center; gap: 16rpx; padding: 16rpx 24rpx; }
+.quick-info { flex: 1; min-width: 0; }
+.quick-info-text { font-size: 24rpx; color: #6b7280; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: block; }
+
+/* 生成按钮(紧凑 Tab 风格) */
+.generate-btn-tab { flex-shrink: 0; padding: 18rpx 36rpx; background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%); border-radius: 40rpx; }
+.generate-btn-tab:active { opacity: 0.8; transform: scale(0.96); }
+.generate-btn-tab.disabled { opacity: 0.35; pointer-events: none; }
+.generate-btn-tab-text { font-size: 28rpx; font-weight: 700; color: #ffffff; white-space: nowrap; }
+
+/* 图标 Tab 栏(等宽分布,居中) */
+.tab-toolbar { display: flex; border-top: 1rpx solid #f3f4f6; padding: 10rpx 0 16rpx; }
+.tab-item { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 4rpx; padding: 6rpx 0; border-radius: 12rpx; transition: all 0.15s; }
+.tab-item:active { background: #f9fafb; }
+.tab-item.active .tab-icon { transform: scale(1.15); }
+.tab-item.active .tab-text { color: #4f46e5; font-weight: 600; }
+.tab-icon { font-size: 40rpx; line-height: 1; transition: transform 0.15s; }
+.tab-text { font-size: 22rpx; color: #9ca3af; transition: color 0.15s; }
+
+/* 底部面板 */
+.bottom-panel-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 10000; display: flex; align-items: flex-end; }
+.bottom-panel { width: 100%; max-height: 70vh; background: #ffffff; border-radius: 24rpx 24rpx 0 0; display: flex; flex-direction: column; }
+.panel-header { display: flex; justify-content: space-between; align-items: center; padding: 32rpx 32rpx 16rpx; }
+.panel-title { font-size: 34rpx; font-weight: 600; color: #1f2937; }
+.panel-close { font-size: 36rpx; color: #9ca3af; padding: 8rpx; }
+.panel-body { padding: 0 32rpx 32rpx; max-height: 55vh; }
+
+/* 音色面板(亮色,与整体风格统一) */
+.voice-panel-dark { background: #ffffff; border-radius: 24rpx 24rpx 0 0; max-height: 75vh; }
+
+/* 分类标签栏 */
+.voice-tabs { padding: 24rpx 28rpx 8rpx; border-bottom: 1rpx solid #f3f4f6; }
+.voice-tabs-scroll { white-space: nowrap; }
+.voice-tabs-inner { display: inline-flex; gap: 16rpx; }
+.voice-tab { font-size: 26rpx; color: #6b7280; padding: 10rpx 22rpx; border-radius: 20rpx; white-space: nowrap; flex-shrink: 0; background: #f9fafb; }
+.voice-tab.active { color: #ffffff; background: #4f46e5; font-weight: 600; }
+
+/* 音色网格(5列紧凑卡片) */
+.voice-grid-scroll { max-height: 56vh; }
+.voice-card-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 12rpx; padding: 20rpx 24rpx 32rpx; }
+.voice-card { position: relative; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 18rpx 4rpx 14rpx; border-radius: 14rpx; background: #f9fafb; border: 2rpx solid transparent; min-height: 120rpx; }
+.voice-card.active { background: #eef2ff; border-color: #4f46e5; }
+.voice-card:active { opacity: 0.7; transform: scale(0.96); }
+.voice-card-icon { width: 64rpx; height: 64rpx; border-radius: 50%; background: #f3f4f6; display: flex; align-items: center; justify-content: center; margin-bottom: 6rpx; font-size: 30rpx; }
+.voice-card.active .voice-card-icon { background: rgba(79,70,229,0.12); }
+.voice-card-name { font-size: 20rpx; color: #374151; text-align: center; line-height: 1.3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
+.voice-card.active .voice-card-name { color: #4f46e5; font-weight: 600; }
+.voice-card-check { position: absolute; top: 6rpx; right: 6rpx; font-size: 20rpx; color: #4f46e5; font-weight: 700; }
+.voice-card-play { position: absolute; top: 4rpx; right: 4rpx; width: 36rpx; height: 36rpx; border-radius: 50%; background: rgba(79,70,229,0.1); display: flex; align-items: center; justify-content: center; font-size: 16rpx; color: #4f46e5; }
+.voice-card.active .voice-card-play { display: none; }
+
+/* 面板底部关闭条 */
+.panel-close-bar { padding: 16rpx 0 28rpx; display: flex; justify-content: center; }
+.panel-close-bar-line { width: 80rpx; height: 6rpx; border-radius: 3rpx; background: #e5e7eb; }
+
+/* 参数面板 */
+.param-item { margin-bottom: 32rpx; }
+.param-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12rpx; }
+.param-label { font-size: 28rpx; color: #374151; }
+.param-value { font-size: 28rpx; font-weight: 600; color: #4f46e5; background: #eef2ff; padding: 4rpx 16rpx; border-radius: 8rpx; }
+.param-range { display: flex; justify-content: space-between; margin-top: 8rpx; }
+.param-range text { font-size: 22rpx; color: #9ca3af; }
+
+/* 专辑 */
+.album-item { display: flex; justify-content: space-between; align-items: center; padding: 20rpx 24rpx; border-radius: 14rpx; background: #f9fafb; margin-bottom: 12rpx; }
+.album-item.active { background: #eef2ff; border: 2rpx solid #4f46e5; }
+.album-item-content { flex: 1; }
+.album-item-title { display: block; font-size: 28rpx; font-weight: 600; color: #1f2937; margin-bottom: 6rpx; }
+.album-item-desc { display: block; font-size: 24rpx; color: #6b7280; }
+.album-check { font-size: 32rpx; color: #4f46e5; }
+.album-empty { text-align: center; padding: 60rpx 0; color: #9ca3af; font-size: 28rpx; }
+
+.panel-action-btn { width: 100%; height: 96rpx; background: linear-gradient(135deg, #4f46e5 0%, #818cf8 100%); border-radius: 16rpx; border: none; display: flex; align-items: center; justify-content: center; font-size: 32rpx; font-weight: 600; color: #ffffff; margin-top: 16rpx; }
+.panel-action-btn::after { border: none; }
+.panel-action-btn[disabled] { opacity: 0.4; }
+
+/* 表单 */
+.form-item { margin-bottom: 24rpx; }
+.form-label { display: block; font-size: 28rpx; color: #1f2937; font-weight: 500; margin-bottom: 12rpx; }
+.form-input { width: 100%; height: 88rpx; background: #f9fafb; border-radius: 14rpx; padding: 0 24rpx; font-size: 28rpx; color: #1f2937; }
+.form-textarea { width: 100%; min-height: 140rpx; background: #f9fafb; border-radius: 14rpx; padding: 20rpx 24rpx; font-size: 28rpx; color: #1f2937; line-height: 1.6; }
+
+/* 额度弹窗 */
+.quota-modal { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.6); z-index: 3000; display: flex; align-items: center; justify-content: center; }
+.quota-content { width: 600rpx; background: #ffffff; border-radius: 32rpx; padding: 60rpx 40rpx; display: flex; flex-direction: column; align-items: center; }
+.quota-icon { font-size: 100rpx; margin-bottom: 24rpx; }
+.quota-title { font-size: 36rpx; font-weight: 600; color: #1f2937; margin-bottom: 12rpx; }
+.quota-desc { font-size: 28rpx; color: #6b7280; margin-bottom: 40rpx; }
+.quota-actions { width: 100%; display: flex; flex-direction: column; gap: 16rpx; }
+.quota-btn { width: 100%; height: 88rpx; border-radius: 44rpx; display: flex; align-items: center; justify-content: center; font-size: 30rpx; border: none; }
+.quota-btn::after { border: none; }
+.quota-btn.primary { background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); color: #fff; }
+.quota-btn.secondary { background: #f3f4f6; color: #6b7280; }
 </style>

+ 18 - 5
my-uniapp-vue3/src/pages/history/index.vue

@@ -470,7 +470,7 @@ onMounted(async () => {
 <style scoped>
 .page {
   min-height: 100vh;
-  background: #f5f5f5;
+  background: var(--color-bg-page, #f3f4f6);
 }
 
 .search-bar {
@@ -517,8 +517,9 @@ onMounted(async () => {
 
 .search-placeholder {
   font-size: 26rpx;
-  color: rgba(255, 255, 255, 0.8);
+  color: rgba(255, 255, 255, 0.7);
 }
+.search-input::placeholder { color: rgba(255, 255, 255, 0.7); }
 
 .edit-bar {
   position: fixed;
@@ -550,7 +551,11 @@ onMounted(async () => {
   padding: 12rpx 24rpx;
   border-radius: 8rpx;
   font-size: 28rpx;
+  border: none;
+  transition: transform 0.15s, opacity 0.15s;
 }
+.action-btn:active { transform: scale(0.96); opacity: 0.8; }
+.action-btn[disabled] { opacity: 0.5; pointer-events: none; }
 
 .action-btn.delete {
   background: #ef4444;
@@ -602,6 +607,7 @@ onMounted(async () => {
   color: #666;
   flex-shrink: 0;
   position: relative;
+  transition: color 0.2s;
 }
 
 .tab-item.active {
@@ -659,12 +665,14 @@ onMounted(async () => {
 
 .audio-card {
   width: calc(50% - 10rpx);
-  background: #ffffff;
+  background: var(--color-bg-card, #ffffff);
   border-radius: 16rpx;
   overflow: hidden;
-  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
+  box-shadow: var(--shadow-sm, 0 2rpx 8rpx rgba(0,0,0,0.06));
   position: relative;
+  transition: transform 0.15s;
 }
+.audio-card:active { transform: scale(0.98); }
 
 .audio-card.selected {
   background: #eff6ff;
@@ -801,7 +809,9 @@ onMounted(async () => {
   justify-content: center;
   box-shadow: 0 4rpx 12rpx rgba(102, 126, 234, 0.4);
   z-index: 10;
+  transition: transform 0.15s;
 }
+.publish-btn:active { transform: scale(0.9); }
 
 .publish-icon {
   font-size: 28rpx;
@@ -830,8 +840,11 @@ onMounted(async () => {
   transform: translateY(-50%);
   background: rgba(79, 70, 229, 0.08);
   border-radius: 32rpx;
-  border: 1rpx solid rgba(79, 70, 229, 0.2);
+  border: 2rpx solid rgba(79, 70, 229, 0.15);
+  cursor: pointer;
+  transition: background 0.15s;
 }
+.group-toggle:active { background: rgba(79, 70, 229, 0.15); }
 
 .group-toggle-text {
   font-size: 24rpx;

+ 258 - 296
my-uniapp-vue3/src/pages/index/index.vue

@@ -123,18 +123,6 @@
       </view>
     </scroll-view>
 
-    <!-- 底部操作按钮 -->
-    <view class="bottom-bar">
-      <button class="action-btn" @click="goToAlbums">
-        <text class="btn-icon">📚</text>
-        <text class="btn-text">管理书籍</text>
-      </button>
-      <button class="generate-btn" @click="goToBookGenerator">
-        <text class="btn-icon">+</text>
-        <text class="btn-text">生成书籍</text>
-      </button>
-    </view>
-
     <!-- 迷你播放器 -->
     <MiniPlayer />
   </view>
@@ -283,15 +271,7 @@ function goToAlbumDetail(item: Book) {
   uni.navigateTo({ url: `/pages/album/index?id=${item.id}` });
 }
 
-// 跳转书籍管理页
-function goToAlbums() {
-  uni.switchTab({ url: '/pages/book-generator/index' });
-}
 
-// 跳转书籍生成页
-function goToBookGenerator() {
-  uni.switchTab({ url: '/pages/book-generator/index' });
-}
 
 // 跳转搜索页
 function goToSearch() {
@@ -344,81 +324,104 @@ onShow(() => {
 <style scoped>
 .page {
   min-height: 100vh;
-  background: #f9fafb;
+  background: var(--color-bg-page, #f3f4f6);
   padding-bottom: calc(180rpx + env(safe-area-inset-bottom) + 120rpx);
 }
 
+/* --- 搜索栏 - 毛玻璃效果 --- */
 .search-bar {
   position: fixed;
   top: 0;
   left: 0;
   right: 0;
-  height: 100rpx;
+  height: 96rpx;
   padding-top: env(safe-area-inset-top);
-  background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
+  background: rgba(255, 255, 255, 0.88);
+  backdrop-filter: blur(24px);
+  -webkit-backdrop-filter: blur(24px);
   display: flex;
   align-items: center;
-  padding-left: 24rpx;
-  padding-right: 24rpx;
+  padding-left: 28rpx;
+  padding-right: 20rpx;
   z-index: 100;
+  border-bottom: 0.5rpx solid rgba(0, 0, 0, 0.06);
+}
+
+.search-bar::before {
+  content: '';
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  height: 1rpx;
+  background: linear-gradient(90deg, transparent, rgba(79, 70, 229, 0.15), transparent);
 }
 
 .search-icon {
-  font-size: 28rpx;
+  font-size: 30rpx;
   margin-right: 12rpx;
+  opacity: 0.5;
 }
 
 .search-placeholder {
   flex: 1;
   font-size: 26rpx;
-  color: rgba(255, 255, 255, 0.8);
+  color: var(--color-text-tertiary, #9ca3af);
 }
 
 /* 通知铃铛 */
 .notif-bell {
   position: relative;
-  width: 64rpx;
-  height: 64rpx;
+  width: 68rpx;
+  height: 68rpx;
   display: flex;
   align-items: center;
   justify-content: center;
   flex-shrink: 0;
-  margin-left: 12rpx;
+  margin-left: 8rpx;
+  border-radius: 50%;
+  background: var(--color-bg-surface, #f9fafb);
+  transition: background 0.2s;
+}
+.notif-bell:active {
+  background: var(--color-bg-hover, #f3f4f6);
 }
 
 .bell-icon {
-  font-size: 36rpx;
+  font-size: 34rpx;
 }
 
 .bell-badge {
   position: absolute;
   top: 4rpx;
-  right: 0;
+  right: 4rpx;
   min-width: 32rpx;
   height: 32rpx;
-  background: #ef4444;
+  background: var(--color-error, #ef4444);
   color: #ffffff;
-  font-size: 20rpx;
-  font-weight: 600;
+  font-size: 18rpx;
+  font-weight: 700;
   border-radius: 16rpx;
   display: flex;
   align-items: center;
   justify-content: center;
   padding: 0 6rpx;
   border: 2rpx solid #ffffff;
+  box-shadow: 0 2rpx 6rpx rgba(239, 68, 68, 0.4);
 }
 
-/* 最近收听区域 */
+/* --- 最近收听 --- */
 .recent-section {
-  padding: 24rpx;
+  padding: 28rpx 28rpx 8rpx;
 }
 
 .section-title {
-  font-size: 32rpx;
-  font-weight: 600;
-  color: #1f2937;
+  font-size: var(--text-lg, 32rpx);
+  font-weight: 700;
+  color: var(--color-text-primary, #111827);
   margin-bottom: 20rpx;
   display: block;
+  letter-spacing: -0.3rpx;
 }
 
 .recent-scroll {
@@ -428,20 +431,25 @@ onShow(() => {
 
 .recent-card {
   display: inline-block;
-  width: 200rpx;
-  margin-right: 20rpx;
+  width: 180rpx;
+  margin-right: 16rpx;
   vertical-align: top;
+  transition: transform var(--transition-fast, 0.15s);
+}
+.recent-card:active {
+  transform: scale(0.95);
 }
 
 .recent-cover {
   position: relative;
-  width: 200rpx;
-  height: 200rpx;
-  border-radius: 16rpx;
+  width: 180rpx;
+  height: 180rpx;
+  border-radius: var(--radius-md, 16rpx);
   overflow: hidden;
   display: flex;
   align-items: center;
   justify-content: center;
+  box-shadow: var(--shadow-sm, 0 2rpx 8rpx rgba(0,0,0,0.06));
 }
 
 .cover-img {
@@ -454,83 +462,104 @@ onShow(() => {
   bottom: 0;
   left: 0;
   right: 0;
-  height: 8rpx;
-  background: rgba(0, 0, 0, 0.3);
+  height: 6rpx;
+  background: rgba(0, 0, 0, 0.25);
+  backdrop-filter: blur(4px);
 }
 
 .progress-fill {
   height: 100%;
-  background: linear-gradient(90deg, #4f46e5 0%, #818cf8 100%);
-  transition: width 0.3s ease;
+  background: linear-gradient(90deg, var(--color-primary-600, #4f46e5), var(--color-primary-400, #818cf8));
+  border-radius: 0 3rpx 0 0;
+  transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
 }
 
 .recent-title {
-  font-size: 26rpx;
-  color: #1f2937;
+  font-size: 24rpx;
+  font-weight: 500;
+  color: var(--color-text-primary, #111827);
   display: block;
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
-  margin-top: 12rpx;
+  margin-top: 10rpx;
 }
 
 .recent-progress {
-  font-size: 22rpx;
-  color: #9ca3af;
+  font-size: 20rpx;
+  color: var(--color-text-tertiary, #9ca3af);
   display: block;
   margin-top: 4rpx;
 }
 
 .album-list {
-  padding-top: 120rpx;
-  height: calc(100vh - 120rpx);
+  padding-top: 110rpx;
+  height: calc(100vh - 110rpx);
 }
 
+/* --- 空状态 --- */
 .empty {
   display: flex;
   flex-direction: column;
   align-items: center;
   justify-content: center;
-  padding: 200rpx 0;
+  padding: 160rpx 0;
 }
 
 .empty-icon {
-  font-size: 120rpx;
+  font-size: 100rpx;
   margin-bottom: 24rpx;
+  opacity: 0.8;
 }
 
 .empty-text {
-  font-size: 32rpx;
-  color: #6b7280;
-  margin-bottom: 12rpx;
+  font-size: var(--text-lg, 32rpx);
+  font-weight: 600;
+  color: var(--color-text-secondary, #4b5563);
+  margin-bottom: 8rpx;
 }
 
 .empty-hint {
-  font-size: 26rpx;
-  color: #9ca3af;
+  font-size: var(--text-sm, 24rpx);
+  color: var(--color-text-tertiary, #9ca3af);
 }
 
-/* 冷启动引导 */
+/* --- 冷启动引导 --- */
 .onboarding {
-  padding: 32rpx 24rpx;
+  padding: 36rpx 28rpx;
 }
 
 .onboarding-card {
-  background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
-  border-radius: 24rpx;
-  padding: 48rpx 32rpx;
+  background: linear-gradient(135deg, var(--color-primary-600, #4f46e5), #7c3aed);
+  border-radius: var(--radius-lg, 24rpx);
+  padding: 48rpx 36rpx;
   text-align: center;
-  margin-bottom: 32rpx;
+  margin-bottom: 36rpx;
+  box-shadow: var(--shadow-primary, 0 4rpx 20rpx rgba(79, 70, 229, 0.25));
+  position: relative;
+  overflow: hidden;
+}
+
+.onboarding-card::after {
+  content: '';
+  position: absolute;
+  top: -50%;
+  right: -30%;
+  width: 300rpx;
+  height: 300rpx;
+  background: rgba(255, 255, 255, 0.05);
+  border-radius: 50%;
+  pointer-events: none;
 }
 
 .onboarding-icon {
-  font-size: 80rpx;
+  font-size: 72rpx;
   display: block;
   margin-bottom: 20rpx;
 }
 
 .onboarding-title {
-  font-size: 36rpx;
+  font-size: var(--text-xl, 36rpx);
   font-weight: 700;
   color: #ffffff;
   display: block;
@@ -538,11 +567,11 @@ onShow(() => {
 }
 
 .onboarding-desc {
-  font-size: 26rpx;
-  color: rgba(255, 255, 255, 0.85);
+  font-size: var(--text-sm, 24rpx);
+  color: rgba(255, 255, 255, 0.82);
   display: block;
   margin-bottom: 28rpx;
-  line-height: 1.5;
+  line-height: 1.6;
 }
 
 .onboarding-btn {
@@ -550,13 +579,19 @@ onShow(() => {
   align-items: center;
   justify-content: center;
   height: 80rpx;
-  padding: 0 40rpx;
+  padding: 0 44rpx;
   background: #ffffff;
-  color: #4f46e5;
-  font-size: 28rpx;
+  color: var(--color-primary-600, #4f46e5);
+  font-size: var(--text-base, 28rpx);
   font-weight: 600;
   border-radius: 40rpx;
   border: none;
+  box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.12);
+  transition: transform 0.15s, box-shadow 0.15s;
+}
+.onboarding-btn:active {
+  transform: scale(0.97);
+  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.12);
 }
 
 .onboarding-btn::after { border: none; }
@@ -566,9 +601,9 @@ onShow(() => {
 }
 
 .template-title {
-  font-size: 28rpx;
+  font-size: var(--text-base, 28rpx);
   font-weight: 600;
-  color: #1f2937;
+  color: var(--color-text-primary, #111827);
   display: block;
   margin-bottom: 16rpx;
 }
@@ -576,22 +611,22 @@ onShow(() => {
 .template-list {
   display: flex;
   flex-direction: column;
-  gap: 16rpx;
+  gap: 12rpx;
 }
 
 .template-card {
   display: flex;
   align-items: center;
   gap: 16rpx;
-  padding: 24rpx;
-  background: #ffffff;
-  border-radius: 16rpx;
-  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
-  transition: transform 0.1s;
+  padding: 24rpx 20rpx;
+  background: var(--color-bg-card, #ffffff);
+  border-radius: var(--radius-md, 16rpx);
+  box-shadow: var(--shadow-xs, 0 1rpx 2rpx rgba(0,0,0,0.04));
+  transition: transform 0.15s, box-shadow 0.15s;
 }
-
 .template-card:active {
   transform: scale(0.98);
+  box-shadow: var(--shadow-sm, 0 2rpx 8rpx rgba(0,0,0,0.06));
 }
 
 .template-icon {
@@ -600,45 +635,40 @@ onShow(() => {
 }
 
 .template-name {
-  font-size: 28rpx;
+  font-size: var(--text-base, 28rpx);
   font-weight: 600;
-  color: #1f2937;
+  color: var(--color-text-primary, #111827);
   flex-shrink: 0;
   min-width: 120rpx;
 }
 
 .template-desc {
-  font-size: 24rpx;
-  color: #9ca3af;
+  font-size: var(--text-sm, 24rpx);
+  color: var(--color-text-tertiary, #9ca3af);
   flex: 1;
 }
 
+/* --- 书籍网格 --- */
 .album-grid {
   display: flex;
   flex-wrap: wrap;
-  padding: 24rpx;
-  gap: 20rpx;
+  padding: 24rpx 20rpx;
+  gap: 16rpx;
 }
 
 .album-card {
-  width: calc(50% - 10rpx);
-  background: #ffffff;
-  border-radius: 16rpx;
+  width: calc(50% - 8rpx);
+  background: var(--color-bg-card, #ffffff);
+  border-radius: var(--radius-md, 16rpx);
   overflow: hidden;
-  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
-  animation: listItemIn 0.4s ease-out forwards;
+  box-shadow: var(--shadow-xs, 0 1rpx 2rpx rgba(0,0,0,0.04));
+  animation: fadeInUp 0.45s cubic-bezier(0.22, 0.61, 0.36, 1) forwards;
   opacity: 0;
+  transition: transform 0.2s, box-shadow 0.2s;
 }
-
-@keyframes listItemIn {
-  from {
-    opacity: 0;
-    transform: translateY(20px);
-  }
-  to {
-    opacity: 1;
-    transform: translateY(0);
-  }
+.album-card:active {
+  transform: scale(0.98);
+  box-shadow: var(--shadow-sm, 0 2rpx 8rpx rgba(0,0,0,0.06));
 }
 
 .album-cover {
@@ -653,7 +683,7 @@ onShow(() => {
 
 .cover-letter {
   position: absolute;
-  font-size: 160rpx;
+  font-size: 140rpx;
   font-weight: 800;
   color: #ffffff;
   left: 50%;
@@ -664,58 +694,61 @@ onShow(() => {
 
 .cover-title-small {
   position: absolute;
-  bottom: 48rpx;
+  bottom: 44rpx;
   left: 16rpx;
   right: 16rpx;
-  font-size: 24rpx;
-  font-weight: 500;
+  font-size: 22rpx;
+  font-weight: 600;
   color: rgba(255, 255, 255, 0.95);
   text-align: center;
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
-  text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
+  text-shadow: 0 1rpx 4rpx rgba(0, 0, 0, 0.3);
 }
 
 .album-count {
   position: absolute;
-  bottom: 12rpx;
-  right: 12rpx;
-  background: rgba(0, 0, 0, 0.6);
+  bottom: 10rpx;
+  right: 10rpx;
+  background: rgba(0, 0, 0, 0.55);
+  backdrop-filter: blur(8px);
   color: #ffffff;
-  font-size: 22rpx;
-  padding: 4rpx 12rpx;
-  border-radius: 8rpx;
+  font-size: 20rpx;
+  font-weight: 500;
+  padding: 4rpx 10rpx;
+  border-radius: 6rpx;
 }
 
 .album-info {
-  padding: 20rpx;
+  padding: 16rpx 18rpx 18rpx;
 }
 
 .album-title {
-  font-size: 28rpx;
-  font-weight: 500;
-  color: #1f2937;
+  font-size: 26rpx;
+  font-weight: 600;
+  color: var(--color-text-primary, #111827);
   display: block;
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
-  margin-bottom: 8rpx;
+  margin-bottom: 6rpx;
 }
 
 .album-desc {
   font-size: 22rpx;
-  color: #9ca3af;
+  color: var(--color-text-tertiary, #9ca3af);
   display: block;
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
-  margin-bottom: 8rpx;
+  margin-bottom: 6rpx;
 }
 
 .album-meta {
-  font-size: 22rpx;
-  color: #9ca3af;
+  font-size: 20rpx;
+  color: var(--color-primary-600, #4f46e5);
+  font-weight: 500;
 }
 
 .loading,
@@ -726,231 +759,160 @@ onShow(() => {
 
 .loading text,
 .no-more text {
-  font-size: 24rpx;
-  color: #9ca3af;
+  font-size: var(--text-sm, 24rpx);
+  color: var(--color-text-tertiary, #9ca3af);
+}
+
+
+
+/* --- 夜间模式 --- */
+.dark-mode .page {
+  background: var(--color-bg-page, #0f172a);
+}
+.dark-mode .search-bar {
+  background: rgba(15, 23, 42, 0.9);
+  border-bottom-color: rgba(255, 255, 255, 0.06);
+}
+.dark-mode .search-bar::before {
+  background: linear-gradient(90deg, transparent, rgba(129, 140, 248, 0.2), transparent);
+}
+.dark-mode .notif-bell {
+  background: var(--color-bg-hover, #334155);
+}
+.dark-mode .section-title {
+  color: #f1f5f9;
+}
+.dark-mode .recent-title {
+  color: #e2e8f0;
+}
+.dark-mode .recent-progress {
+  color: #64748b;
+}
+.dark-mode .album-card {
+  background: var(--color-bg-card, #1e293b);
+}
+.dark-mode .album-title {
+  color: #f1f5f9;
+}
+.dark-mode .album-desc {
+  color: #94a3b8;
+}
+.dark-mode .album-meta {
+  color: #818cf8;
+}
+.dark-mode .template-card {
+  background: var(--color-bg-card, #1e293b);
+}
+.dark-mode .template-name {
+  color: #f1f5f9;
+}
+
+.dark-mode .action-btn {
+  background: var(--color-bg-hover, #334155);
+}
+.dark-mode .action-btn .btn-text,
+.dark-mode .action-btn .btn-icon {
+  color: #e2e8f0;
+}
+.dark-mode .cover-title-small {
+  color: rgba(255, 255, 255, 0.9);
+}
+.dark-mode .empty-text {
+  color: #94a3b8;
+}
+
+/* 骨架屏 */
+.skeleton-section-title,
+.skeleton-recent-cover,
+.skeleton-recent-title,
+.skeleton-recent-progress {
+  background: linear-gradient(90deg, #e5e7eb 25%, #f3f4f6 50%, #e5e7eb 75%);
+  background-size: 200% 100%;
+  animation: shimmer 1.5s infinite;
+}
+.dark-mode .skeleton-section-title,
+.dark-mode .skeleton-recent-cover,
+.dark-mode .skeleton-recent-title,
+.dark-mode .skeleton-recent-progress {
+  background: linear-gradient(90deg, #334155 25%, #475569 50%, #334155 75%);
+  background-size: 200% 100%;
 }
 
-/* 骨架屏样式 */
 .skeleton-section-title {
   height: 32rpx;
   width: 120rpx;
-  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
-  background-size: 200% 100%;
-  animation: shimmer 1.5s infinite;
   border-radius: 8rpx;
   margin-bottom: 20rpx;
 }
-
 .skeleton-recent-card {
   display: inline-block;
-  width: 200rpx;
-  margin-right: 20rpx;
+  width: 180rpx;
+  margin-right: 16rpx;
   vertical-align: top;
 }
-
 .skeleton-recent-cover {
-  width: 200rpx;
-  height: 200rpx;
-  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
-  background-size: 200% 100%;
-  animation: shimmer 1.5s infinite;
+  width: 180rpx;
+  height: 180rpx;
   border-radius: 16rpx;
 }
-
 .skeleton-recent-title {
   height: 26rpx;
   width: 80%;
-  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
-  background-size: 200% 100%;
-  animation: shimmer 1.5s infinite;
   border-radius: 8rpx;
   margin-top: 12rpx;
 }
-
 .skeleton-recent-progress {
   height: 22rpx;
   width: 50%;
-  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
-  background-size: 200% 100%;
-  animation: shimmer 1.5s infinite;
   border-radius: 8rpx;
   margin-top: 8rpx;
 }
-
 .skeleton-grid {
   display: flex;
   flex-wrap: wrap;
-  padding: 24rpx;
-  gap: 20rpx;
+  padding: 24rpx 20rpx;
+  gap: 16rpx;
 }
-
 .skeleton-card {
-  width: calc(50% - 10rpx);
-  background: #ffffff;
+  width: calc(50% - 8rpx);
+  background: var(--color-bg-card, #ffffff);
   border-radius: 16rpx;
   overflow: hidden;
 }
-
 .skeleton-cover {
   width: 100%;
   height: 200rpx;
-  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
+  background: linear-gradient(90deg, #e5e7eb 25%, #f3f4f6 50%, #e5e7eb 75%);
   background-size: 200% 100%;
   animation: shimmer 1.5s infinite;
 }
-
 .skeleton-info {
-  padding: 20rpx;
+  padding: 16rpx 18rpx;
 }
-
 .skeleton-title {
-  height: 28rpx;
+  height: 26rpx;
   width: 80%;
-  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
+  background: linear-gradient(90deg, #e5e7eb 25%, #f3f4f6 50%, #e5e7eb 75%);
   background-size: 200% 100%;
   animation: shimmer 1.5s infinite;
   border-radius: 8rpx;
-  margin-bottom: 12rpx;
+  margin-bottom: 10rpx;
 }
-
 .skeleton-meta {
   height: 22rpx;
   width: 50%;
-  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
+  background: linear-gradient(90deg, #e5e7eb 25%, #f3f4f6 50%, #e5e7eb 75%);
   background-size: 200% 100%;
   animation: shimmer 1.5s infinite;
   border-radius: 8rpx;
 }
-
-@keyframes shimmer {
-  0% { background-position: 200% 0; }
-  100% { background-position: -200% 0; }
-}
-
-/* 底部操作栏 */
-.bottom-bar {
-  position: fixed;
-  bottom: calc(100rpx + env(safe-area-inset-bottom));
-  left: 0;
-  right: 0;
-  display: flex;
-  gap: 20rpx;
-  padding: 20rpx 32rpx;
-  background: #ffffff;
-  box-shadow: 0 -2rpx 12rpx rgba(0, 0, 0, 0.04);
-}
-
-.action-btn {
-  flex: 1;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  height: 88rpx;
-  background: #f5f5f5;
-  border-radius: 44rpx;
-  border: none;
-  transition: transform 0.1s ease;
-}
-
-.action-btn:active {
-  transform: scale(0.96);
-}
-
-.action-btn::after {
-  border: none;
-}
-
-.generate-btn {
-  flex: 2;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  height: 88rpx;
-  background: linear-gradient(135deg, #4f46e5 0%, #818cf8 100%);
-  border-radius: 44rpx;
-  border: none;
-  transition: transform 0.1s ease;
-}
-
-.generate-btn:active {
-  transform: scale(0.96);
-}
-
-.generate-btn::after {
-  border: none;
-}
-
-.btn-icon {
-  font-size: 32rpx;
-  color: #ffffff;
-  margin-right: 8rpx;
-}
-
-.btn-text {
-  font-size: 28rpx;
-  font-weight: 600;
-  color: #ffffff;
-}
-
-.action-btn .btn-text {
-  color: #333;
-}
-
-/* 夜间模式适配 */
-.dark-mode .section-title {
-  color: #f3f4f6;
-}
-
-.dark-mode .recent-title {
-  color: #e5e7eb;
-}
-
-.dark-mode .recent-progress {
-  color: #6b7280;
-}
-
-.dark-mode .album-card {
-  background: #1f2937;
-}
-
-.dark-mode .album-title {
-  color: #f3f4f6;
-}
-
-.dark-mode .album-desc {
-  color: #9ca3af;
-}
-
-.dark-mode .album-meta {
-  color: #6b7280;
-}
-
-.dark-mode .bottom-bar {
-  background: #1f2937;
-}
-
-.dark-mode .action-btn {
-  background: #374151;
-}
-
-.dark-mode .action-btn .btn-text {
-  color: #e5e7eb;
-}
-
-.dark-mode .page {
-  background: #111827;
-}
-
-.dark-mode .cover-title-small {
-  color: rgba(255, 255, 255, 0.9);
-}
-
-/* 夜间模式骨架屏 */
-.dark-mode .skeleton-section-title,
-.dark-mode .skeleton-recent-cover,
-.dark-mode .skeleton-recent-title,
-.dark-mode .skeleton-recent-progress {
-  background: linear-gradient(90deg, #374151 25%, #4b5563 50%, #374151 75%);
+.dark-mode .skeleton-cover,
+.dark-mode .skeleton-title,
+.dark-mode .skeleton-meta {
+  background: linear-gradient(90deg, #334155 25%, #475569 50%, #334155 75%);
   background-size: 200% 100%;
 }
+.dark-mode .skeleton-card {
+  background: var(--color-bg-card, #1e293b);
+}
 </style>

+ 24 - 6
my-uniapp-vue3/src/pages/login/index.vue

@@ -198,11 +198,18 @@ onUnmounted(() => {
 .input {
   width: 100%;
   height: 88rpx;
-  background: #f9fafb;
-  border-radius: 12rpx;
+  background: var(--color-bg-surface, #f9fafb);
+  border-radius: var(--radius-md, 16rpx);
+  border: 2rpx solid transparent;
   padding: 0 24rpx;
   font-size: 28rpx;
-  color: #1f2937;
+  color: var(--color-text-primary, #1f2937);
+  transition: border-color 0.2s, background 0.2s, box-shadow 0.2s;
+}
+.input:focus {
+  border-color: var(--color-primary-600, #4f46e5);
+  background: var(--color-bg-card, #ffffff);
+  box-shadow: 0 0 0 4rpx rgba(79, 70, 229, 0.08);
 }
 
 .code-row {
@@ -218,10 +225,12 @@ onUnmounted(() => {
   width: 200rpx;
   height: 88rpx;
   background: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%);
-  border-radius: 12rpx;
+  border-radius: var(--radius-md, 16rpx);
   border: none;
   padding: 0;
+  transition: transform 0.15s, opacity 0.15s;
 }
+.code-btn:active { transform: scale(0.96); opacity: 0.85; }
 
 .code-btn::after {
   border: none;
@@ -241,10 +250,12 @@ onUnmounted(() => {
   width: 100%;
   height: 96rpx;
   background: linear-gradient(135deg, #4f46e5 0%, #818cf8 100%);
-  border-radius: 16rpx;
+  border-radius: var(--radius-lg, 16rpx);
   border: none;
   margin-top: 48rpx;
+  transition: transform 0.15s, opacity 0.15s;
 }
+.login-btn:active { transform: scale(0.98); opacity: 0.9; }
 
 .login-btn::after {
   border: none;
@@ -264,8 +275,15 @@ onUnmounted(() => {
 .tip {
   display: block;
   font-size: 22rpx;
-  color: #9ca3af;
+  color: var(--color-text-tertiary, #8b919c);
   text-align: center;
   margin-top: 32rpx;
 }
+
+/* ===== 暗色模式 ===== */
+.dark-mode .page { background: linear-gradient(180deg, #3730a3 0%, #4f46e5 50%, #111827 50%); }
+.dark-mode .form-card { background: #1f2937; }
+.dark-mode .form-title { color: #f9fafb; }
+.dark-mode .input-label { color: #9ca3af; }
+.dark-mode .input { background: #374151; color: #e5e7eb; }
 </style>

+ 171 - 249
my-uniapp-vue3/src/pages/mine/index.vue

@@ -428,36 +428,59 @@ function handleLogout() {
 <style scoped>
 .page {
   min-height: 100vh;
-  background: #f9fafb;
+  background: var(--color-bg-page, #f3f4f6);
   padding-bottom: 120rpx;
 }
 
-/* 夜间模式适配 */
-@media (prefers-color-scheme: dark) {
-  .page {
-    background: #1a1a2e;
-  }
+/* --- 用户信息卡片 --- */
+.user-card {
+  margin: 24rpx 24rpx 20rpx;
+  background: linear-gradient(135deg, var(--color-primary-600, #4f46e5) 0%, #7c3aed 100%);
+  border-radius: var(--radius-lg, 24rpx);
+  padding: 40rpx 32rpx 32rpx;
+  box-shadow: var(--shadow-primary, 0 4rpx 20rpx rgba(79, 70, 229, 0.25));
+  position: relative;
+  overflow: hidden;
 }
 
-.user-card {
-  margin: 32rpx;
-  background: linear-gradient(135deg, #4f46e5 0%, #818cf8 100%);
-  border-radius: 24rpx;
-  padding: 48rpx 32rpx;
-  box-shadow: 0 8rpx 32rpx rgba(79, 70, 229, 0.3);
+.user-card::before {
+  content: '';
+  position: absolute;
+  top: -40%;
+  right: -20%;
+  width: 280rpx;
+  height: 280rpx;
+  background: rgba(255, 255, 255, 0.04);
+  border-radius: 50%;
+  pointer-events: none;
+}
+
+.user-card::after {
+  content: '';
+  position: absolute;
+  bottom: -30%;
+  left: -10%;
+  width: 200rpx;
+  height: 200rpx;
+  background: rgba(255, 255, 255, 0.03);
+  border-radius: 50%;
+  pointer-events: none;
 }
 
 .user-info {
   display: flex;
   align-items: center;
-  margin-bottom: 32rpx;
+  margin-bottom: 28rpx;
+  position: relative;
+  z-index: 1;
 }
 
 .avatar {
   width: 96rpx;
   height: 96rpx;
   border-radius: 50%;
-  border: 4rpx solid rgba(255, 255, 255, 0.3);
+  border: 3rpx solid rgba(255, 255, 255, 0.4);
+  box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.15);
 }
 
 .avatar-placeholder {
@@ -468,7 +491,7 @@ function handleLogout() {
   display: flex;
   align-items: center;
   justify-content: center;
-  border: 4rpx solid rgba(255, 255, 255, 0.3);
+  border: 3rpx solid rgba(255, 255, 255, 0.4);
 }
 
 .avatar-text {
@@ -481,38 +504,41 @@ function handleLogout() {
 
 .nickname {
   display: block;
-  font-size: 36rpx;
-  font-weight: 600;
+  font-size: var(--text-xl, 36rpx);
+  font-weight: 700;
   color: #ffffff;
   margin-bottom: 8rpx;
+  letter-spacing: -0.3rpx;
 }
 
 .member-badge {
   display: inline-block;
   padding: 4rpx 16rpx;
-  border-radius: 16rpx;
+  border-radius: 20rpx;
   background: rgba(255, 255, 255, 0.2);
+  backdrop-filter: blur(8px);
 }
 
 .member-badge.level-1 {
-  background: linear-gradient(135deg, #f97316 0%, #fb923c 100%);
+  background: linear-gradient(135deg, rgba(249, 115, 22, 0.9), rgba(251, 146, 60, 0.9));
 }
-
 .member-badge.level-2 {
-  background: linear-gradient(135deg, #fbbf24 0%, #fcd34d 100%);
+  background: linear-gradient(135deg, rgba(251, 191, 36, 0.9), rgba(252, 211, 77, 0.9));
 }
 
 .member-text {
   font-size: 22rpx;
   color: #ffffff;
+  font-weight: 600;
 }
 
-/* 积分余额区域(两列) */
+/* 积分余额区域 */
 .credits-section {
-  background: rgba(255, 255, 255, 0.1);
-  border-radius: 16rpx;
-  padding: 24rpx;
-  margin-bottom: 16rpx;
+  background: rgba(255, 255, 255, 0.12);
+  border-radius: var(--radius-md, 16rpx);
+  padding: 20rpx 24rpx;
+  margin-bottom: 12rpx;
+  backdrop-filter: blur(8px);
 }
 
 .credit-row {
@@ -528,26 +554,27 @@ function handleLogout() {
 .credit-label {
   display: block;
   font-size: 20rpx;
-  color: rgba(255, 255, 255, 0.8);
+  color: rgba(255, 255, 255, 0.7);
   margin-bottom: 6rpx;
 }
 
 .credit-value {
   display: block;
-  font-size: 36rpx;
+  font-size: 34rpx;
   font-weight: 700;
   color: #ffffff;
+  letter-spacing: -0.5rpx;
 }
 
 .credit-sub {
   display: block;
   font-size: 18rpx;
-  color: rgba(255, 255, 255, 0.6);
+  color: rgba(255, 255, 255, 0.5);
   margin-top: 2rpx;
 }
 
 .credit-divider {
-  width: 2rpx;
+  width: 1rpx;
   height: 60rpx;
   background: rgba(255, 255, 255, 0.2);
 }
@@ -556,31 +583,29 @@ function handleLogout() {
 .credit-bar-row {
   display: flex;
   align-items: center;
-  margin-top: 16rpx;
+  margin-top: 14rpx;
   gap: 8rpx;
 }
 
 .credit-bar {
-  height: 6rpx;
+  height: 5rpx;
   background: rgba(255, 255, 255, 0.2);
   border-radius: 3rpx;
   overflow: hidden;
 }
 
 .credit-bar-gap {
-  width: 8rpx;
+  width: 6rpx;
 }
 
 .credit-bar-fill {
   height: 100%;
   border-radius: 3rpx;
-  transition: width 0.3s;
+  transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
 }
-
 .credit-bar-fill.sub {
   background: #34d399;
 }
-
 .credit-bar-fill.pack {
   background: #fbbf24;
 }
@@ -590,8 +615,9 @@ function handleLogout() {
   display: flex;
   align-items: center;
   background: rgba(255, 255, 255, 0.1);
-  border-radius: 16rpx;
-  padding: 24rpx;
+  border-radius: var(--radius-md, 16rpx);
+  padding: 20rpx 24rpx;
+  backdrop-filter: blur(8px);
 }
 
 .stat-item {
@@ -601,61 +627,51 @@ function handleLogout() {
 
 .stat-value {
   display: block;
-  font-size: 34rpx;
+  font-size: 32rpx;
   font-weight: 700;
   color: #ffffff;
+  letter-spacing: -0.5rpx;
 }
 
 .stat-label {
   display: block;
   font-size: 20rpx;
-  color: rgba(255, 255, 255, 0.8);
+  color: rgba(255, 255, 255, 0.7);
   margin-top: 4rpx;
 }
 
 .stat-divider {
-  width: 2rpx;
+  width: 1rpx;
   height: 60rpx;
   background: rgba(255, 255, 255, 0.2);
 }
 
-/* 今日创作概况卡片 */
+/* --- 今日创作概况 --- */
 .today-card {
-  margin: 0 32rpx 24rpx;
-  background: #ffffff;
-  border-radius: 20rpx;
+  margin: 0 24rpx 20rpx;
+  background: var(--color-bg-card, #ffffff);
+  border-radius: var(--radius-lg, 24rpx);
   padding: 32rpx;
-  box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
-}
-
-@media (prefers-color-scheme: dark) {
-  .today-card {
-    background: #2d2d44;
-  }
+  box-shadow: var(--shadow-xs, 0 1rpx 2rpx rgba(0,0,0,0.04));
 }
 
 .today-header {
   display: flex;
   justify-content: space-between;
   align-items: center;
-  margin-bottom: 24rpx;
+  margin-bottom: 28rpx;
 }
 
 .today-title {
-  font-size: 30rpx;
-  font-weight: 600;
-  color: #1f2937;
-}
-
-@media (prefers-color-scheme: dark) {
-  .today-title {
-    color: #f9fafb;
-  }
+  font-size: var(--text-base, 28rpx);
+  font-weight: 700;
+  color: var(--color-text-primary, #111827);
+  letter-spacing: -0.2rpx;
 }
 
 .today-date {
-  font-size: 24rpx;
-  color: #9ca3af;
+  font-size: var(--text-sm, 24rpx);
+  color: var(--color-text-tertiary, #9ca3af);
 }
 
 .today-stats {
@@ -670,85 +686,51 @@ function handleLogout() {
 
 .today-stat-value {
   display: block;
-  font-size: 44rpx;
-  font-weight: 700;
-  color: #4f46e5;
-}
-
-@media (prefers-color-scheme: dark) {
-  .today-stat-value {
-    color: #818cf8;
-  }
+  font-size: 40rpx;
+  font-weight: 800;
+  color: var(--color-primary-600, #4f46e5);
+  letter-spacing: -0.5rpx;
 }
 
 .today-stat-label {
   display: block;
   font-size: 22rpx;
-  color: #6b7280;
-  margin-top: 4rpx;
-}
-
-@media (prefers-color-scheme: dark) {
-  .today-stat-label {
-    color: #9ca3af;
-  }
+  color: var(--color-text-secondary, #4b5563);
+  margin-top: 6rpx;
 }
 
 .today-stat-divider {
-  width: 2rpx;
+  width: 1rpx;
   height: 60rpx;
-  background: #e5e7eb;
+  background: var(--color-bg-divider, #e5e7eb);
 }
 
-@media (prefers-color-scheme: dark) {
-  .today-stat-divider {
-    background: #374151;
-  }
-}
-
-/* 最近的创作卡片 */
+/* --- 最近的创作 --- */
 .recent-card {
-  margin: 0 32rpx 24rpx;
-  background: #ffffff;
-  border-radius: 20rpx;
+  margin: 0 24rpx 20rpx;
+  background: var(--color-bg-card, #ffffff);
+  border-radius: var(--radius-lg, 24rpx);
   padding: 32rpx;
-  box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
-}
-
-@media (prefers-color-scheme: dark) {
-  .recent-card {
-    background: #2d2d44;
-  }
+  box-shadow: var(--shadow-xs, 0 1rpx 2rpx rgba(0,0,0,0.04));
 }
 
 .recent-header {
   display: flex;
   justify-content: space-between;
   align-items: center;
-  margin-bottom: 24rpx;
+  margin-bottom: 20rpx;
 }
 
 .recent-title {
-  font-size: 30rpx;
-  font-weight: 600;
-  color: #1f2937;
-}
-
-@media (prefers-color-scheme: dark) {
-  .recent-title {
-    color: #f9fafb;
-  }
+  font-size: var(--text-base, 28rpx);
+  font-weight: 700;
+  color: var(--color-text-primary, #111827);
 }
 
 .recent-more {
-  font-size: 24rpx;
-  color: #4f46e5;
-}
-
-@media (prefers-color-scheme: dark) {
-  .recent-more {
-    color: #818cf8;
-  }
+  font-size: var(--text-sm, 24rpx);
+  color: var(--color-primary-600, #4f46e5);
+  font-weight: 500;
 }
 
 .recent-list {
@@ -759,50 +741,39 @@ function handleLogout() {
 .recent-item {
   display: flex;
   align-items: center;
-  padding: 16rpx 0;
-  border-bottom: 1rpx solid #f3f4f6;
+  padding: 18rpx 12rpx;
+  border-radius: var(--radius-sm, 10rpx);
+  margin-bottom: 4rpx;
+  transition: background 0.15s;
 }
-
 .recent-item:last-child {
-  border-bottom: none;
+  margin-bottom: 0;
 }
 
 .recent-item-hover {
-  background-color: #f3f4f6;
-  border-radius: 8rpx;
-}
-
-@media (prefers-color-scheme: dark) {
-  .recent-item {
-    border-bottom-color: #374151;
-  }
-  .recent-item-hover {
-    background-color: #374151;
-  }
-  .recent-item:last-child {
-    border-bottom: none;
-  }
+  background-color: var(--color-bg-surface, #f9fafb);
 }
 
 .recent-cover {
-  width: 80rpx;
-  height: 80rpx;
-  border-radius: 12rpx;
+  width: 76rpx;
+  height: 76rpx;
+  border-radius: var(--radius-sm, 10rpx);
   display: flex;
   align-items: center;
   justify-content: center;
   flex-shrink: 0;
   position: relative;
   overflow: hidden;
+  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
 }
 
 .cover-letter {
   position: absolute;
-  top: -8rpx;
+  top: -6rpx;
   left: 4rpx;
-  font-size: 60rpx;
+  font-size: 52rpx;
   font-weight: 900;
-  color: rgba(255, 255, 255, 0.15);
+  color: rgba(255, 255, 255, 0.12);
   line-height: 1;
   pointer-events: none;
 }
@@ -826,61 +797,47 @@ function handleLogout() {
 
 .recent-info {
   flex: 1;
-  margin-left: 20rpx;
+  margin-left: 18rpx;
   overflow: hidden;
 }
 
 .recent-title-text {
   display: block;
-  font-size: 28rpx;
+  font-size: var(--text-base, 28rpx);
   font-weight: 500;
-  color: #1f2937;
+  color: var(--color-text-primary, #111827);
   white-space: nowrap;
   overflow: hidden;
   text-overflow: ellipsis;
 }
 
-@media (prefers-color-scheme: dark) {
-  .recent-title-text {
-    color: #f9fafb;
-  }
-}
-
 .recent-meta {
   display: block;
   font-size: 22rpx;
-  color: #9ca3af;
+  color: var(--color-text-tertiary, #9ca3af);
   margin-top: 4rpx;
 }
 
 .recent-arrow {
-  font-size: 32rpx;
-  color: #d1d5db;
+  font-size: 28rpx;
+  color: var(--color-text-placeholder, #d1d5db);
   margin-left: 16rpx;
 }
 
-@media (prefers-color-scheme: dark) {
-  .recent-arrow {
-    color: #4b5563;
-  }
-}
-
-/* 升级会员卡片 */
+/* --- 升级会员卡片 --- */
 .upgrade-card {
-  margin: 0 32rpx 24rpx;
-  background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
-  border-radius: 20rpx;
-  padding: 28rpx 32rpx;
+  margin: 0 24rpx 20rpx;
+  background: linear-gradient(135deg, #fef3c7, #fde68a);
+  border-radius: var(--radius-lg, 24rpx);
+  padding: 24rpx 28rpx;
   display: flex;
   align-items: center;
   justify-content: space-between;
-  box-shadow: 0 4rpx 16rpx rgba(251, 191, 36, 0.2);
+  box-shadow: 0 4rpx 16rpx rgba(251, 191, 36, 0.15);
+  transition: transform 0.15s;
 }
-
-@media (prefers-color-scheme: dark) {
-  .upgrade-card {
-    background: linear-gradient(135deg, #92400e 0%, #78350f 100%);
-  }
+.upgrade-card:active {
+  transform: scale(0.98);
 }
 
 .upgrade-left {
@@ -889,7 +846,7 @@ function handleLogout() {
 }
 
 .upgrade-icon {
-  font-size: 40rpx;
+  font-size: 36rpx;
   margin-right: 16rpx;
 }
 
@@ -900,124 +857,94 @@ function handleLogout() {
 
 .upgrade-title {
   display: block;
-  font-size: 30rpx;
-  font-weight: 600;
+  font-size: var(--text-base, 28rpx);
+  font-weight: 700;
   color: #92400e;
 }
 
-@media (prefers-color-scheme: dark) {
-  .upgrade-title {
-    color: #fef3c7;
-  }
-}
-
 .upgrade-desc {
   display: block;
   font-size: 22rpx;
   color: #b45309;
-  margin-top: 4rpx;
-}
-
-@media (prefers-color-scheme: dark) {
-  .upgrade-desc {
-    color: #fde68a;
-  }
+  margin-top: 2rpx;
 }
 
 .upgrade-btn {
   font-size: 24rpx;
   color: #ffffff;
-  background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
-  padding: 12rpx 24rpx;
+  background: linear-gradient(135deg, #f59e0b, #d97706);
+  padding: 14rpx 28rpx;
   border-radius: 24rpx;
-  font-weight: 500;
+  font-weight: 600;
+  box-shadow: 0 2rpx 8rpx rgba(217, 119, 6, 0.3);
 }
 
-/* 功能菜单 */
+/* --- 功能菜单 --- */
 .menu-section {
-  margin: 32rpx;
-  background: #ffffff;
-  border-radius: 24rpx;
+  margin: 24rpx;
+  background: var(--color-bg-card, #ffffff);
+  border-radius: var(--radius-lg, 24rpx);
   overflow: hidden;
-  box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
-}
-
-@media (prefers-color-scheme: dark) {
-  .menu-section {
-    background: #2d2d44;
-  }
+  box-shadow: var(--shadow-xs, 0 1rpx 2rpx rgba(0,0,0,0.04));
 }
 
 .menu-item {
   display: flex;
   align-items: center;
-  padding: 32rpx;
-  border-bottom: 1rpx solid #f3f4f6;
-}
-
-.menu-item:last-child {
-  border-bottom: none;
+  padding: 28rpx 32rpx;
+  transition: background 0.15s;
 }
-
-@media (prefers-color-scheme: dark) {
-  .menu-item {
-    border-bottom-color: #374151;
-  }
-  .menu-item:last-child {
-    border-bottom: none;
-  }
+.menu-item:active {
+  background: var(--color-bg-surface, #f9fafb);
 }
 
 .menu-icon {
-  font-size: 40rpx;
-  margin-right: 24rpx;
+  font-size: 36rpx;
+  margin-right: 20rpx;
+  width: 48rpx;
+  text-align: center;
 }
 
 .menu-text {
   flex: 1;
-  font-size: 30rpx;
-  color: #1f2937;
-}
-
-@media (prefers-color-scheme: dark) {
-  .menu-text {
-    color: #f9fafb;
-  }
+  font-size: var(--text-base, 28rpx);
+  color: var(--color-text-primary, #111827);
+  font-weight: 500;
 }
 
 .menu-tag {
-  font-size: 20rpx;
+  font-size: 18rpx;
   padding: 4rpx 12rpx;
-  background: linear-gradient(135deg, #f97316 0%, #fb923c 100%);
+  background: linear-gradient(135deg, #f97316, #fb923c);
   color: #ffffff;
-  border-radius: 8rpx;
-  margin-right: 16rpx;
+  border-radius: 6rpx;
+  margin-right: 12rpx;
+  font-weight: 600;
 }
 
 .menu-arrow {
-  font-size: 32rpx;
-  color: #9ca3af;
-}
-
-@media (prefers-color-scheme: dark) {
-  .menu-arrow {
-    color: #6b7280;
-  }
+  font-size: 28rpx;
+  color: var(--color-text-placeholder, #d1d5db);
 }
 
 .logout-section {
-  margin: 48rpx 32rpx;
+  margin: 40rpx 24rpx;
 }
 
 .logout-btn {
   width: 100%;
   height: 88rpx;
-  background: #ffffff;
-  border-radius: 16rpx;
-  border: 2rpx solid #ef4444;
+  background: var(--color-bg-card, #ffffff);
+  border-radius: var(--radius-md, 16rpx);
+  border: 1.5rpx solid var(--color-error, #ef4444);
   display: flex;
   align-items: center;
   justify-content: center;
+  box-shadow: var(--shadow-xs, 0 1rpx 2rpx rgba(0,0,0,0.04));
+  transition: all 0.15s;
+}
+.logout-btn:active {
+  background: var(--color-error-bg, #fef2f2);
 }
 
 .logout-btn::after {
@@ -1025,13 +952,8 @@ function handleLogout() {
 }
 
 .logout-btn text {
-  font-size: 30rpx;
-  color: #ef4444;
-}
-
-@media (prefers-color-scheme: dark) {
-  .logout-btn {
-    background: #2d2d44;
-  }
+  font-size: var(--text-base, 28rpx);
+  color: var(--color-error, #ef4444);
+  font-weight: 500;
 }
 </style>

+ 457 - 0
my-uniapp-vue3/src/pages/pixelle-video/index.vue

@@ -0,0 +1,457 @@
+<template>
+  <view class="container">
+    <!-- 顶部导航 -->
+    <view class="header">
+      <view class="back-btn" @click="goBack">
+        <text class="icon">←</text>
+      </view>
+      <text class="title">AI 视频生成</text>
+      <view class="placeholder"></view>
+    </view>
+
+    <!-- 服务状态 -->
+    <view class="status-bar" :class="serviceStatus">
+      <text>{{ statusText }}</text>
+    </view>
+
+    <!-- 输入区域 -->
+    <view class="input-section">
+      <view class="input-group">
+        <text class="label">视频主题</text>
+        <textarea 
+          v-model="topic" 
+          class="textarea"
+          placeholder="例如:人工智能的未来发展趋势"
+          :disabled="isGenerating"
+        ></textarea>
+      </view>
+
+      <view class="input-group">
+        <text class="label">视频风格</text>
+        <view class="style-grid">
+          <view 
+            v-for="style in styles" 
+            :key="style"
+            class="style-item"
+            :class="{ active: selectedStyle === style }"
+            @click="selectedStyle = style"
+          >
+            {{ style }}
+          </view>
+        </view>
+      </view>
+
+      <view class="input-group">
+        <text class="label">画面比例</text>
+        <view class="ratio-group">
+          <view 
+            class="ratio-item"
+            :class="{ active: aspectRatio === '9:16' }"
+            @click="aspectRatio = '9:16'"
+          >
+            <text class="ratio-icon">📱</text>
+            <text>竖屏 9:16</text>
+          </view>
+          <view 
+            class="ratio-item"
+            :class="{ active: aspectRatio === '16:9' }"
+            @click="aspectRatio = '16:9'"
+          >
+            <text class="ratio-icon">🖥️</text>
+            <text>横屏 16:9</text>
+          </view>
+          <view 
+            class="ratio-item"
+            :class="{ active: aspectRatio === '1:1' }"
+            @click="aspectRatio = '1:1'"
+          >
+            <text class="ratio-icon">⬜</text>
+            <text>方形 1:1</text>
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <!-- 生成按钮 -->
+    <view class="action-section">
+      <button 
+        class="generate-btn"
+        :disabled="!canGenerate || isGenerating"
+        @click="generateContent"
+      >
+        <text v-if="isGenerating">生成中...</text>
+        <text v-else>开始生成</text>
+      </button>
+    </view>
+
+    <!-- 生成结果 -->
+    <view v-if="result" class="result-section">
+      <view class="result-header">
+        <text class="result-title">生成结果</text>
+      </view>
+
+      <!-- 文案 -->
+      <view v-if="result.script" class="result-card">
+        <text class="card-title">📝 生成的文案</text>
+        <text class="script-text">{{ result.script }}</text>
+      </view>
+
+      <!-- 图片列表 -->
+      <view v-if="result.images && result.images.length > 0" class="result-card">
+        <text class="card-title">🖼️ 生成的图片</text>
+        <scroll-view class="image-scroll" scroll-x>
+          <view 
+            v-for="(img, idx) in result.images" 
+            :key="idx"
+            class="image-item"
+          >
+            <image :src="img" mode="aspectFill" class="result-image"></image>
+          </view>
+        </scroll-view>
+      </view>
+
+      <!-- 语音合成 -->
+      <view v-if="result.script && !audioUrl" class="result-card">
+        <text class="card-title">🗣️ 语音合成</text>
+        <button class="tts-btn" @click="generateAudio" :disabled="isGeneratingAudio">
+          {{ isGeneratingAudio ? '生成中...' : '使用 AI 语音朗读' }}
+        </button>
+      </view>
+
+      <!-- 音频播放 -->
+      <view v-if="audioUrl" class="result-card">
+        <text class="card-title">🎧 音频预览</text>
+        <audio :src="audioUrl" controls class="audio-player"></audio>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { ref, computed } from 'vue'
+import { BASE_URL } from '@/config'
+
+const topic = ref('')
+const selectedStyle = ref('科技风')
+const aspectRatio = ref('9:16')
+const isGenerating = ref(false)
+const isGeneratingAudio = ref(false)
+const serviceStatus = ref('unknown')
+const result = ref<any>(null)
+const audioUrl = ref('')
+
+const styles = [
+  '科技风', '自然风', '商务风', '清新风', '复古风',
+  '未来风', '简约风', '活力风', '文艺风', '国潮风'
+]
+
+const statusText = computed(() => {
+  switch (serviceStatus.value) {
+    case 'online': return '✅ Pixelle-Video 服务正常'
+    case 'offline': return '⚠️ Pixelle-Video 服务未连接'
+    default: return '⏳ 检查服务状态...'
+  }
+})
+
+const canGenerate = computed(() => {
+  return topic.value.trim().length > 0 && !isGenerating.value
+})
+
+// 检查服务状态
+async function checkServiceStatus() {
+  try {
+    const res = await uni.request({
+      url: `${BASE_URL}/api/pixelle-video/health`
+    })
+    const data = res.data as any
+    serviceStatus.value = data.code === 0 ? 'online' : 'offline'
+  } catch {
+    serviceStatus.value = 'offline'
+  }
+}
+
+// 生成内容
+async function generateContent() {
+  if (!canGenerate.value) return
+  
+  isGenerating.value = true
+  result.value = null
+  audioUrl.value = ''
+  
+  try {
+    const res = await uni.request({
+      url: `${BASE_URL}/api/pixelle-video/generate`,
+      method: 'POST',
+      data: {
+        topic: topic.value,
+        style: selectedStyle.value,
+        aspectRatio: aspectRatio.value
+      }
+    })
+    
+    const data = res.data as any
+    if (data.code === 0) {
+      result.value = data.data
+    } else {
+      uni.showToast({ title: data.message || '生成失败', icon: 'none' })
+    }
+  } catch (error) {
+    uni.showToast({ title: '请求失败', icon: 'none' })
+  } finally {
+    isGenerating.value = false
+  }
+}
+
+// 生成语音
+async function generateAudio() {
+  if (!result.value?.script) return
+  
+  isGeneratingAudio.value = true
+  
+  try {
+    const res = await uni.request({
+      url: `${BASE_URL}/api/tts/synthesize`,
+      method: 'POST',
+      data: {
+        text: result.value.script,
+        voiceId: 'Cherry'
+      }
+    })
+    
+    const data = res.data as any
+    if (data.code === 0 && data.data?.audioUrl) {
+      audioUrl.value = data.data.audioUrl.startsWith('http') 
+        ? data.data.audioUrl 
+        : `${BASE_URL}${data.data.audioUrl}`
+    } else {
+      uni.showToast({ title: '语音生成失败', icon: 'none' })
+    }
+  } catch {
+    uni.showToast({ title: '语音生成失败', icon: 'none' })
+  } finally {
+    isGeneratingAudio.value = false
+  }
+}
+
+function goBack() {
+  uni.navigateBack()
+}
+
+// 初始化
+onMounted(() => {
+  checkServiceStatus()
+})
+</script>
+
+<style scoped>
+.container {
+  min-height: 100vh;
+  background: #f5f5f5;
+  padding-bottom: 40rpx;
+}
+
+.header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 60rpx 30rpx 20rpx;
+  background: #fff;
+}
+
+.back-btn {
+  width: 60rpx;
+  height: 60rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.icon {
+  font-size: 36rpx;
+  color: #333;
+}
+
+.title {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #333;
+}
+
+.placeholder {
+  width: 60rpx;
+}
+
+.status-bar {
+  padding: 20rpx 30rpx;
+  font-size: 24rpx;
+  color: #666;
+  background: #fff;
+  border-bottom: 1rpx solid #eee;
+}
+
+.status-bar.online {
+  color: #52c41a;
+}
+
+.status-bar.offline {
+  color: #faad14;
+}
+
+.input-section {
+  padding: 30rpx;
+}
+
+.input-group {
+  margin-bottom: 30rpx;
+}
+
+.label {
+  display: block;
+  font-size: 28rpx;
+  color: #333;
+  font-weight: 500;
+  margin-bottom: 16rpx;
+}
+
+.textarea {
+  width: 100%;
+  height: 200rpx;
+  padding: 20rpx;
+  background: #fff;
+  border-radius: 16rpx;
+  font-size: 28rpx;
+  box-sizing: border-box;
+}
+
+.style-grid {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 20rpx;
+}
+
+.style-item {
+  padding: 16rpx 28rpx;
+  background: #fff;
+  border-radius: 40rpx;
+  font-size: 26rpx;
+  color: #666;
+  border: 2rpx solid #eee;
+}
+
+.style-item.active {
+  background: #4F46E5;
+  color: #fff;
+  border-color: #4F46E5;
+}
+
+.ratio-group {
+  display: flex;
+  gap: 20rpx;
+}
+
+.ratio-item {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 30rpx 0;
+  background: #fff;
+  border-radius: 16rpx;
+  border: 2rpx solid #eee;
+}
+
+.ratio-item.active {
+  border-color: #4F46E5;
+  background: #f0f0ff;
+}
+
+.ratio-icon {
+  font-size: 48rpx;
+  margin-bottom: 10rpx;
+}
+
+.action-section {
+  padding: 0 30rpx;
+}
+
+.generate-btn {
+  width: 100%;
+  height: 88rpx;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: #fff;
+  font-size: 32rpx;
+  border-radius: 44rpx;
+  border: none;
+}
+
+.generate-btn[disabled] {
+  background: #ccc;
+  color: #fff;
+}
+
+.result-section {
+  padding: 30rpx;
+}
+
+.result-header {
+  margin-bottom: 20rpx;
+}
+
+.result-title {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #333;
+}
+
+.result-card {
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 30rpx;
+  margin-bottom: 20rpx;
+}
+
+.card-title {
+  display: block;
+  font-size: 28rpx;
+  font-weight: 500;
+  color: #333;
+  margin-bottom: 20rpx;
+}
+
+.script-text {
+  font-size: 28rpx;
+  color: #666;
+  line-height: 1.8;
+  white-space: pre-wrap;
+}
+
+.image-scroll {
+  white-space: nowrap;
+}
+
+.image-item {
+  display: inline-block;
+  margin-right: 20rpx;
+}
+
+.result-image {
+  width: 300rpx;
+  height: 400rpx;
+  border-radius: 12rpx;
+}
+
+.tts-btn {
+  width: 100%;
+  height: 80rpx;
+  background: #f0f0ff;
+  color: #4F46E5;
+  font-size: 28rpx;
+  border-radius: 40rpx;
+  border: none;
+}
+
+.audio-player {
+  width: 100%;
+  margin-top: 10rpx;
+}
+</style>

+ 12 - 4
my-uniapp-vue3/src/pages/player/index.vue

@@ -138,7 +138,7 @@
         <view
           v-for="(item, index) in playlist"
           :key="item.id"
-          :class="['playlist-item', { active: item.id === audio?.id }]"
+          :class="['playlist-item', { active: isCurrentTrack(item) }]"
           @click="playFromPlaylist(index)"
         >
           <view class="playlist-index">{{ index + 1 }}</view>
@@ -146,7 +146,7 @@
             <text class="playlist-item-title">{{ item.title }}</text>
             <text class="playlist-item-meta">{{ formatDuration(item.audioDuration || 0) }}</text>
           </view>
-          <view v-if="item.id === audio?.id" class="playing-icon">▶</view>
+          <view v-if="isCurrentTrack(item)" class="playing-icon">▶</view>
         </view>
       </scroll-view>
     </view>
@@ -551,6 +551,12 @@ function formatDuration(seconds: number): string {
   return `${mins}:${secs.toString().padStart(2, '0')}`;
 }
 
+// 判断播放列表项是否为当前播放
+function isCurrentTrack(item: AudioItem): boolean {
+  const currentId = String(audio.value?._id || audio.value?.id);
+  return String(item.id) === currentId || String(item._id) === currentId;
+}
+
 /**
  * 获取分享卡片数据(从后端)
  */
@@ -836,13 +842,13 @@ function copyShareLink(): string {
 
 <style scoped>
 .page {
-  min-height: 100vh;
+  height: 100vh;
   background: linear-gradient(180deg, #1f2937 0%, #111827 100%);
   padding-bottom: constant(safe-area-inset-bottom);
   padding-bottom: env(safe-area-inset-bottom);
   width: 100%;
   box-sizing: border-box;
-  overflow-x: hidden;
+  overflow: hidden;
   display: flex;
   flex-direction: column;
 }
@@ -1295,7 +1301,9 @@ function copyShareLink(): string {
 .playlist-list-mobile {
   flex: 1;
   min-height: 0;
+  max-height: 60vh;
   padding: 0 16rpx 16rpx;
+  box-sizing: border-box;
 }
 
 .playlist-item {

+ 8 - 1
server/.env.example

@@ -91,4 +91,11 @@ WECHAT_SERIAL_NO=your-cert-serial-no
 WECHAT_PRIVATE_KEY=your-private-key-content
 
 # 微信支付回调地址
-WECHAT_NOTIFY_URL=https://your-domain.com/api/payment/wechat/callback
+WECHAT_NOTIFY_URL=https://your-domain.com/api/payment/wechat/callback
+
+# ==================== Pixelle-Video 视频生成配置 ====================
+# Pixelle-Video 服务地址(本地部署或远程服务器)
+PIXELLE_VIDEO_URL=http://localhost:8501
+
+# 是否启用 Pixelle-Video 服务
+PIXELLE_VIDEO_ENABLED=false

+ 2 - 0
server/src/app.ts

@@ -62,6 +62,7 @@ import feedbackRoutes from './modules/feedback/feedback.controller';
 import queueRoutes from './modules/queue/queue.controller';
 import inviteRoutes from './modules/invite/invite.controller';
 import textToVideoRoutes from './modules/text-to-video/text-to-video.controller';
+import pixelleVideoRoutes from './modules/pixelle-video/pixelle-video.controller';
 import { initializePlans } from './modules/subscription/subscription.service';
 import { initWebSocket } from './services/websocket.service.js';
 
@@ -159,6 +160,7 @@ router.use('/api/feedback', feedbackRoutes.routes());
 router.use('/api/queue', queueRoutes.routes());
 router.use('/api/invite', inviteRoutes.routes());
 router.use('/api/text-to-video', textToVideoRoutes.routes());
+router.use('/api/pixelle-video', pixelleVideoRoutes.routes());
 router.use('/api', logRoutes.routes());
 
 app.use(router.routes()).use(router.allowedMethods());

+ 4 - 0
server/src/modules/auth/auth.service.ts

@@ -4,6 +4,7 @@ import { config } from '../../config';
 import { prisma } from '../../models';
 import { JwtPayload } from '../../types';
 import { handleInvite } from '../invite/invite.service';
+import { grantSignupBonus } from '../subscription/subscription.service';
 
 // 验证码存储(生产环境应使用 Redis)
 const smsCodes = new Map<string, { code: string; expireAt: number }>();
@@ -82,6 +83,9 @@ export async function loginWithPhone(phone: string, code?: string, inviteCode?:
     });
     isNewUser = true;
 
+    // 新用户注册赠送积分
+    await grantSignupBonus(user.id);
+
     // 处理邀请关系
     if (inviteCode) {
       const shareRecord = await prisma.shareRecord.findFirst({

+ 5 - 0
server/src/modules/book-generator/album-controller.ts

@@ -170,6 +170,9 @@ router.get('/books/:id', optionalAuth, async (ctx: Context) => {
         id: book.id,
         title: book.title,
         description: book.description,
+        targetAudience: book.targetAudience,
+        style: book.style,
+        bookScale: book.bookScale,
         totalChapters: book.totalChapters,
         genStage: book.genStage,
         progress: book.progress,
@@ -203,6 +206,7 @@ router.post('/books/:id', optionalAuth, async (ctx: Context) => {
       description?: string;
       bookScale?: string;
       style?: string;
+      targetAudience?: string;
     };
 
     const existing = await prisma.book.findUnique({ where: { id: bookId } });
@@ -217,6 +221,7 @@ router.post('/books/:id', optionalAuth, async (ctx: Context) => {
     if (body.description !== undefined) data.description = body.description;
     if (body.bookScale !== undefined) data.bookScale = body.bookScale;
     if (body.style !== undefined) data.style = body.style;
+    if (body.targetAudience !== undefined) data.targetAudience = body.targetAudience;
 
     const updated = await prisma.book.update({
       where: { id: bookId },

+ 76 - 27
server/src/modules/book-generator/book-generator.service.ts

@@ -9,6 +9,7 @@ import { createVideoProjectFromBook, generateVideoForProject } from '../video-ge
 import { mergeChapterAudios } from '../player/player.service';
 import { prisma } from '../../models';
 import { advanceChapter, regenerateChapter } from './stage-manager';
+import { estimateBookWords, estimateAudioMinutesFromWords, atomicReserveQuota, releaseQuota, getUserMonthlyCost, AUDIO_BILLING_CONFIG } from '../subscription/subscription.service';
 
 // 步骤类型
 export type GenerationStep = 'generate_content' | 'generate_audio' | 'merge_audio' | 'generate_video' | 'merge_video';
@@ -159,6 +160,27 @@ export class BatchGenerationOrchestrator {
       return;
     }
 
+    // 预估费用并预留配额
+    const userId = (book as any).userId;
+    let reservedAmount = 0;
+    if (userId) {
+      try {
+        const bookScale = book.bookScale || '1000';
+        const wordEstimate = estimateBookWords(bookScale);
+        const audioMinutes = estimateAudioMinutesFromWords(wordEstimate.avg);
+        const estimatedCost = AUDIO_BILLING_CONFIG.pricing.monthly * audioMinutes + 0.1; // +LLM预估缓冲
+        const quotaOk = await atomicReserveQuota(userId, estimatedCost);
+        if (!quotaOk) {
+          throw new Error('月度额度不足,请升级套餐');
+        }
+        reservedAmount = estimatedCost;
+        console.log(`[BatchGen][${this.taskId}] 配额预留成功: ¥${estimatedCost.toFixed(4)}, userId=${userId}`);
+      } catch (err: any) {
+        console.error(`[BatchGen][${this.taskId}] 配额预留失败:`, err.message);
+        throw err;
+      }
+    }
+
     this.pushProgress('generate_content', 20, '开始生成内容');
 
     // 调用 LangGraph 生成内容
@@ -177,41 +199,68 @@ export class BatchGenerationOrchestrator {
     let maxWaitTime = 3600 * 1000; // 最多等待60分钟
     let waited = 0;
     const checkInterval = 5000; // 每5秒检查一次
+    let generationSuccess = false;
 
-    while (waited < maxWaitTime) {
-      this.checkCancellation();
+    try {
+      while (waited < maxWaitTime) {
+        this.checkCancellation();
 
-      const currentChapters = await bookStore.getChapterTree(this.bookId);
-      const leafNodes = currentChapters.filter((c: any) => c.level === Math.max(...currentChapters.map((ch: any) => ch.level || 0)));
-      const completedCount = leafNodes.filter((c: any) => c.genStage === 'content_completed').length;
-      const totalCount = leafNodes.length;
+        const currentChapters = await bookStore.getChapterTree(this.bookId);
+        const leafNodes = currentChapters.filter((c: any) => c.level === Math.max(...currentChapters.map((ch: any) => ch.level || 0)));
+        const completedCount = leafNodes.filter((c: any) => c.genStage === 'content_completed').length;
+        const totalCount = leafNodes.length;
 
-      if (totalCount > 0) {
-        const progress = 20 + Math.round((completedCount / totalCount) * 60);
-        this.pushProgress('generate_content', Math.min(progress, 90), `内容生成中: ${completedCount}/${totalCount}`);
-      }
+        if (totalCount > 0) {
+          const progress = 20 + Math.round((completedCount / totalCount) * 60);
+          this.pushProgress('generate_content', Math.min(progress, 90), `内容生成中: ${completedCount}/${totalCount}`);
+        }
 
-      // 检查是否全部完成
-      if (totalCount > 0 && completedCount >= totalCount) {
-        this.pushProgress('generate_content', 95, '内容生成完成');
-        return;
-      }
+        // 检查是否全部完成
+        if (totalCount > 0 && completedCount >= totalCount) {
+          this.pushProgress('generate_content', 95, '内容生成完成');
+          generationSuccess = true;
+          return;
+        }
 
-      // 检查书籍状态
-      const currentBook = await bookStore.getById(this.bookId);
-      if (currentBook?.genStage === 'video_completed') {
-        this.pushProgress('generate_content', 95, '内容生成完成');
-        return;
+        // 检查书籍状态
+        const currentBook = await bookStore.getById(this.bookId);
+        if (currentBook?.genStage === 'video_completed') {
+          this.pushProgress('generate_content', 95, '内容生成完成');
+          generationSuccess = true;
+          return;
+        }
+        if (currentBook?.genStage === 'failed') {
+          throw new Error('内容生成失败: ' + (currentBook.error || '未知错误'));
+        }
+
+        await this.sleep(checkInterval);
+        waited += checkInterval;
       }
-      if (currentBook?.genStage === 'failed') {
-        throw new Error('内容生成失败: ' + (currentBook.error || '未知错误'));
+      throw new Error('内容生成超时');
+    } finally {
+      // 生成完成后,调整配额(多退少补)
+      if (userId && reservedAmount > 0) {
+        try {
+          // 等待异步日志写入完成(AI调用日志是异步写入的)
+          await this.sleep(2000);
+          const actualCost = await getUserMonthlyCost(userId);
+          const diff = actualCost - reservedAmount;
+          if (Math.abs(diff) > 0.001) {
+            if (diff > 0) {
+              // 实际消耗超过预留,需要追加预留
+              await atomicReserveQuota(userId, diff);
+              console.log(`[BatchGen][${this.taskId}] 配额追加: ¥${diff.toFixed(4)}, userId=${userId}`);
+            } else {
+              // 实际消耗少于预留,退回多余部分
+              await releaseQuota(userId, Math.abs(diff));
+              console.log(`[BatchGen][${this.taskId}] 配额退回: ¥${Math.abs(diff).toFixed(4)}, userId=${userId}`);
+            }
+          }
+        } catch (err) {
+          console.error(`[BatchGen][${this.taskId}] 配额调整失败:`, err);
+        }
       }
-
-      await this.sleep(checkInterval);
-      waited += checkInterval;
     }
-
-    throw new Error('内容生成超时');
   }
 
   /**

+ 23 - 1
server/src/modules/book-generator/book-generator.store.ts

@@ -14,7 +14,7 @@ import { countWords } from './utils';
 import { cleanThinkingText } from './utils/content-cleaner';
 import { advanceChapter, regenerateChapter, safeTransitionChapter } from './stage-manager';
 import { mergeChapterAudios } from '../player/player.service';
-import { consumeAudioMinutes } from '../subscription/subscription.service';
+import { consumeAudioMinutes, canUseTtsProvider } from '../subscription/subscription.service';
 import { runWithContext } from '../../services/llm-context';
 
 /**
@@ -1089,6 +1089,28 @@ export class BookStore {
       return { audioUrl: '' }; // 返回空,前端通过 genStage 轮询
     }
 
+    // ===== 步骤 2.5:权限检查 =====
+    const effectiveUserId = userId || chapterBefore.book?.userId;
+    if (effectiveUserId) {
+      const user = await prisma.user.findUnique({ where: { id: effectiveUserId } });
+      if (user) {
+        // 免费用户(memberLevel=0)不能使用TTS
+        if (user.memberLevel === 0) {
+          console.warn(`[Audio] 免费用户无法生成音频: userId=${effectiveUserId}`);
+          await prisma.bookChapter.update({
+            where: { id: chapterId },
+            data: { genStage: chapterBefore.genStage }
+          }).catch(() => {});
+          throw new Error('免费用户暂不支持音频生成,请升级会员');
+        }
+        // 普通会员(memberLevel=1)只能使用Edge TTS
+        // 这里只做警告,实际provider限制在TTS调用时生效
+        if (user.memberLevel === 1) {
+          console.log(`[Audio] 普通会员用户,使用Edge TTS享受2折优惠: userId=${effectiveUserId}`);
+        }
+      }
+    }
+
     // ===== 步骤 3:已有音频且 genStage 正确 → 直接返回 =====
     const alreadyDoneStages = ['audio_completed', 'video_generating', 'video_completed'];
     if (alreadyDoneStages.includes(chapterBefore.genStage) && chapterBefore.audioUrl) {

+ 16 - 2
server/src/modules/invite/invite.controller.ts

@@ -4,13 +4,27 @@ import { authMiddleware } from '../../middleware/auth';
 
 const router = new Router();
 
+/**
+ * 邀请模块根路由 - 健康检查
+ * GET /api/invite
+ */
+router.get('/', (ctx) => {
+  ctx.body = {
+    code: 0,
+    message: 'invite module ok',
+    data: {
+      endpoints: ['/stats', '/records'],
+    },
+  };
+});
+
 /**
  * 获取当前用户的邀请统计
  * GET /api/invite/stats
  */
 router.get('/stats', authMiddleware, async (ctx) => {
   try {
-    const userId = (ctx.state as any).user.id;
+    const userId = (ctx.state as any).user.userId;
     const stats = await getInviteStats(parseInt(userId));
 
     ctx.body = {
@@ -32,7 +46,7 @@ router.get('/stats', authMiddleware, async (ctx) => {
  */
 router.get('/records', authMiddleware, async (ctx) => {
   try {
-    const userId = (ctx.state as any).user.id;
+    const userId = (ctx.state as any).user.userId;
     const records = await getInviteRecords(parseInt(userId));
 
     ctx.body = {

+ 8 - 7
server/src/modules/invite/invite.service.ts

@@ -74,7 +74,7 @@ async function grantTokenReward(userId: number, tokens: number) {
  */
 export async function getInviteStats(userId: number) {
   const records = await prisma.inviteRecord.findMany({
-    where: { inviterId: userId, rewarded: true },
+    where: { inviterId: Number(userId), rewarded: true },
     select: { rewardAmount: true },
   });
 
@@ -118,8 +118,9 @@ export async function getInviteRecords(userId: number) {
  * @param shareCode 分享码
  * @returns 奖励积分数量
  */
-export async function handleShareReward(userId: number, shareCode?: string): Promise<number> {
-  if (!userId) return 0;
+export async function handleShareReward(userId: number | string, shareCode?: string): Promise<number> {
+  const uid = typeof userId === 'string' ? parseInt(userId) : userId;
+  if (!uid) return 0;
 
   // 一天内同类型分享最多奖励10次
   const today = new Date();
@@ -127,7 +128,7 @@ export async function handleShareReward(userId: number, shareCode?: string): Pro
 
   const existingCount = await prisma.shareRecord.count({
     where: {
-      userId,
+      userId: uid,
       shareCode: shareCode || '',
       createdAt: { gte: today },
     },
@@ -140,7 +141,7 @@ export async function handleShareReward(userId: number, shareCode?: string): Pro
   // 记录分享
   await prisma.shareRecord.create({
     data: {
-      userId,
+      userId: uid,
       audioId: 0,
       platform: 'invite',
       shareCode: shareCode || `share_${Date.now()}`,
@@ -149,9 +150,9 @@ export async function handleShareReward(userId: number, shareCode?: string): Pro
   });
 
   // 发放积分奖励
-  await grantTokenReward(userId, SHARE_REWARD);
+  await grantTokenReward(uid, SHARE_REWARD);
 
-  console.log(`📤 分享奖励: 用户${userId}分享获得${SHARE_REWARD}积分`);
+  console.log(`📤 分享奖励: 用户${uid}分享获得${SHARE_REWARD}积分`);
 
   return SHARE_REWARD;
 }

+ 7 - 0
server/src/modules/pixelle-video/index.ts

@@ -0,0 +1,7 @@
+/**
+ * Pixelle-Video 模块导出
+ */
+
+export { default } from './pixelle-video.controller';
+export { pixelleVideoService } from './pixelle-video.service';
+export * from './pixelle-video.types';

+ 68 - 0
server/src/modules/pixelle-video/pixelle-video.controller.ts

@@ -0,0 +1,68 @@
+/**
+ * Pixelle-Video 控制器 - API 路由
+ */
+
+import Router from '@koa/router';
+import { Context } from 'koa';
+import { pixelleVideoService } from './pixelle-video.service';
+import { GenerateVideoRequest } from './pixelle-video.types';
+import { optionalAuth } from '../../middleware/auth';
+
+const router = new Router();
+
+/**
+ * GET /api/pixelle-video/health
+ * 检查 Pixelle-Video 服务状态
+ */
+router.get('/health', async (ctx: Context) => {
+  const healthy = await pixelleVideoService.checkHealth();
+  ctx.body = {
+    code: healthy ? 0 : 1,
+    message: healthy ? 'Pixelle-Video 服务正常' : 'Pixelle-Video 服务未连接',
+    data: { status: healthy ? 'online' : 'offline' },
+  };
+});
+
+/**
+ * POST /api/pixelle-video/generate
+ * 生成视频内容(文案 + 图片)
+ */
+router.post('/generate', optionalAuth, async (ctx: Context) => {
+  try {
+    const body = ctx.request.body as GenerateVideoRequest;
+
+    if (!body.topic || body.topic.trim().length === 0) {
+      ctx.status = 400;
+      ctx.body = { code: 1, message: '请输入视频主题' };
+      return;
+    }
+
+    const result = await pixelleVideoService.generateContent({
+      topic: body.topic,
+      style: body.style,
+      aspectRatio: body.aspectRatio,
+      duration: body.duration,
+    });
+
+    ctx.body = result;
+  } catch (error) {
+    console.error('生成视频内容失败:', error);
+    ctx.status = 500;
+    ctx.body = {
+      code: 1,
+      message: '生成失败',
+      data: { script: '', images: [], videoUrl: null, duration: 0 },
+    };
+  }
+});
+
+/**
+ * GET /api/pixelle-video/styles
+ * 获取支持的视频风格列表
+ */
+router.get('/styles', async (ctx: Context) => {
+  const styles = await pixelleVideoService.getStyles();
+  ctx.body = { code: 0, message: 'success', data: { styles } };
+});
+
+export default router;

+ 85 - 0
server/src/modules/pixelle-video/pixelle-video.service.ts

@@ -0,0 +1,85 @@
+/**
+ * Pixelle-Video 服务 - 调用 Pixelle-Video API 并集成 TTS
+ */
+
+import axios from 'axios';
+import { GenerateVideoRequest, GenerateVideoResponse, PixelleVideoConfig } from './pixelle-video.types';
+
+export class PixelleVideoService {
+  private config: PixelleVideoConfig;
+
+  constructor() {
+    this.config = {
+      baseUrl: process.env.PIXELLE_VIDEO_URL || 'http://localhost:8501',
+      enabled: process.env.PIXELLE_VIDEO_ENABLED === 'true',
+    };
+  }
+
+  /**
+   * 检查 Pixelle-Video 服务是否可用
+   */
+  async checkHealth(): Promise<boolean> {
+    if (!this.config.enabled) return false;
+    try {
+      const response = await axios.get(`${this.config.baseUrl}/health`, { timeout: 3000 });
+      return response.status === 200;
+    } catch {
+      return false;
+    }
+  }
+
+  /**
+   * 生成视频内容(文案 + 图片)
+   */
+  async generateContent(request: GenerateVideoRequest): Promise<GenerateVideoResponse> {
+    if (!this.config.enabled) {
+      return {
+        code: 1,
+        message: 'Pixelle-Video 服务未启用',
+        data: { script: '', images: [], videoUrl: null, duration: 0 },
+      };
+    }
+
+    try {
+      const response = await axios.post(`${this.config.baseUrl}/generate`, {
+        topic: request.topic,
+        style: request.style || '默认风格',
+        aspect_ratio: request.aspectRatio || '9:16',
+        duration: request.duration || 60,
+        use_tts: false,  // 我们使用自己的 TTS
+      }, {
+        timeout: 120000,  // 2分钟超时
+      });
+
+      return {
+        code: 0,
+        message: '生成成功',
+        data: {
+          script: response.data.script || '',
+          images: response.data.images || [],
+          videoUrl: null,  // 无 GPU,跳过视频生成
+          duration: response.data.duration || request.duration || 60,
+        },
+      };
+    } catch (error) {
+      console.error('Pixelle-Video 调用失败:', error);
+      return {
+        code: 1,
+        message: '生成失败,请检查 Pixelle-Video 服务是否运行',
+        data: { script: '', images: [], videoUrl: null, duration: 0 },
+      };
+    }
+  }
+
+  /**
+   * 获取支持的视频风格列表
+   */
+  async getStyles(): Promise<string[]> {
+    return [
+      '科技风', '自然风', '商务风', '清新风', '复古风',
+      '未来风', '简约风', '活力风', '文艺风', '国潮风',
+    ];
+  }
+}
+
+export const pixelleVideoService = new PixelleVideoService();

+ 34 - 0
server/src/modules/pixelle-video/pixelle-video.types.ts

@@ -0,0 +1,34 @@
+/**
+ * Pixelle-Video 融合模块 - 类型定义
+ */
+
+export interface PixelleVideoConfig {
+  baseUrl: string;      // Pixelle-Video 服务地址
+  enabled: boolean;      // 是否启用
+}
+
+export interface GenerateVideoRequest {
+  topic: string;                    // 视频主题
+  style?: string;                  // 视频风格
+  aspectRatio?: '9:16' | '16:9' | '1:1';  // 画面比例
+  duration?: number;                // 时长(秒)
+  useCustomTts?: boolean;          // 是否使用自定义TTS
+  voiceId?: string;                // 音色ID
+}
+
+export interface GenerateVideoResponse {
+  code: number;
+  message: string;
+  data: {
+    script: string;                // 生成的文案
+    images: string[];              // 生成的图片URL列表
+    videoUrl: string | null;      // 视频URL(如果有)
+    duration: number;             // 预估时长
+  };
+}
+
+export interface ScriptSegment {
+  text: string;                    // 文案内容
+  imageUrl: string;                // 对应图片
+  audioUrl?: string;               // 语音文件(使用自定义TTS)
+}

+ 1 - 1
server/src/modules/share/share.controller.ts

@@ -93,7 +93,7 @@ router.post('/track', authMiddleware, async (ctx) => {
       shareCode?: string;
       isInviteShare?: boolean;
     };
-    const userId = (ctx.state as any).user.id;
+    const userId = (ctx.state as any).user.userId;
 
     const record = await shareService.trackShare(userId, parseInt(audioId) || 0, platform, shareCode);
 

+ 112 - 16
server/src/modules/subscription/subscription.service.ts

@@ -6,8 +6,16 @@ import { MemberLevel } from '../../types';
 // 原配置存在单位错误(千→应为百万/万),已全部修正
 // ============================================
 export const TTS_COST_CONFIG = {
-  // TTS服务商配置(成本:¥/万字符,基于阿里百炼官方定价
+  // TTS服务商配置(成本:¥/万字符)
   providers: {
+    // Edge TTS(普通会员可用,成本低)
+    edge: {
+      name: 'Edge TTS(微软)',
+      costPer10kChars: 0.2,  // ¥0.2/万字符(象征性成本)
+      quality: 'standard',
+      voiceOptions: 10,
+      novelRating: 3
+    },
     // 免费方案
     iflytek_free: {
       name: '科大讯飞-基础版',
@@ -78,7 +86,7 @@ export const TTS_COST_CONFIG = {
   // 各等级默认TTS方案
   planTTSProvider: {
     0: 'iflytek_free',           // 免费版:科大讯飞免费
-    1: 'aliyun_cosyvoice',       // 入门版:CosyVoice标准版
+    1: 'edge',                    // 普通会员:Edge TTS
     2: 'aliyun_cosyvoice',       // 专业版:CosyVoice标准版
     3: 'aliyun_cosyvoice_plus'   // 旗舰版:CosyVoice Plus
   },
@@ -106,12 +114,22 @@ export const BILLING = {
   // 实际超额价 = costPerMinute × monthly × overage × levelDiscount
   overageLevelDiscount: {
     0: null as number | null,
-    1: 1.0,     // 入门版:无折扣 → 0.016×2.0×1.5×1.0 = 0.048
+    1: 1.0,     // 普通会员:无折扣 → 0.016×2.0×1.5×1.0 = 0.048
     2: 0.875,   // 专业版:87.5%  → 0.016×2.0×1.5×0.875 = 0.042
     3: 0.75,    // 旗舰版:75%    → 0.016×2.0×1.5×0.75 = 0.036
     4: 0.67,    // 企业版:67%    → 0.016×2.0×1.5×0.67 ≈ 0.032
   },
 
+  // 会员TTS成本系数(用于普通会员Edge TTS折扣)
+  // 使用TTS时:实际成本 = 基础成本 × 成本系数
+  memberTtsCostCoefficient: {
+    0: null,    // 免费用户:不允许使用TTS
+    1: 0.2,    // 普通会员:2折(Edge TTS优惠)
+    2: 1.0,    // 高级会员:原价
+    3: 1.0,
+    4: 1.0,
+  },
+
   // ---- 计算属性(只读,始终从成本+系数推导)----
   get monthlyPrice() {       // ¥0.032/分钟
     return Math.round(this.costPerMinute * this.coefficients.monthly * 1000) / 1000;
@@ -129,8 +147,46 @@ export const BILLING = {
     if (disc === null || disc === undefined) return null;
     return Math.round(this.overageBasePrice * disc * 1000) / 1000;
   },
+
+  // 获取TTS成本系数(普通会员折扣)
+  getTtsCostCoefficient(level: number): number | null {
+    return this.memberTtsCostCoefficient[level] ?? 1.0;
+  },
 };
 
+// ============================================
+// TTS成本计算辅助函数
+// ============================================
+
+// 计算TTS实际成本(考虑会员折扣)
+// - 免费用户(memberLevel=0):不允许使用TTS,返回null
+// - 普通会员(memberLevel=1)使用Edge TTS:成本×0.2
+// - 高级会员(memberLevel>=2):按原价
+export function calculateTtsCost(
+  provider: string,
+  textLength: number,
+  memberLevel: number
+): number | null {
+  const providerConfig = TTS_COST_CONFIG.providers[provider];
+  if (!providerConfig) return null;
+
+  const baseCost = (textLength / 10000) * providerConfig.costPer10kChars;
+  const coefficient = BILLING.getTtsCostCoefficient(memberLevel);
+
+  if (coefficient === null) return null;  // 免费用户不允许使用TTS
+
+  return Math.round(baseCost * coefficient * 100) / 100;
+}
+
+// 检查用户是否可以使用指定TTS provider
+export function canUseTtsProvider(memberLevel: number, provider: string): boolean {
+  // 普通会员只能使用Edge TTS
+  if (memberLevel === 1 && provider !== 'edge') {
+    return false;
+  }
+  return true;
+}
+
 // ============================================
 // Token成本配置(AI生成成本 + TTS成本)
 // 2026-05-20 修正:基于阿里百炼官方定价,单位修正为百万token/万字符
@@ -143,31 +199,36 @@ export const TOKEN_COST_CONFIG = {
   baseCostPer10kChars: 1.042,
   
   // AI模型(¥/百万token,阿里百炼官方定价)
+  // abilityCoefficient: 能力系数,标准=1.0,强模型>1.0,弱模型<1.0
   models: {
     qwen_plus: {
       name: '通义千问 Qwen-Plus',
       inputCostPerMToken: 0.8,    // ¥0.8/百万token(非思考模式)
       outputCostPerMToken: 2.0,   // ¥2.0/百万token
       quality: 'standard',
-      default: true
+      default: true,
+      abilityCoefficient: 1.0     // 标准模型
     },
     qwen3_flash: {
       name: '通义千问 Qwen3.5-Flash',
       inputCostPerMToken: 0.2,    // ¥0.2/百万token
       outputCostPerMToken: 2.0,   // ¥2.0/百万token
-      quality: 'standard'
+      quality: 'standard',
+      abilityCoefficient: 0.5     // 弱模型,5折
     },
     qwen3_plus: {
       name: '通义千问 Qwen3.6-Plus',
       inputCostPerMToken: 2.0,    // ¥2/百万token
       outputCostPerMToken: 12.0,  // ¥12/百万token
-      quality: 'high'
+      quality: 'high',
+      abilityCoefficient: 1.2     // 强模型,1.2倍
     },
     deepseek_v3: {
       name: 'DeepSeek V3.2(阿里百炼)',
       inputCostPerMToken: 2.0,    // ¥2/百万token
       outputCostPerMToken: 3.0,   // ¥3/百万token
-      quality: 'standard'
+      quality: 'standard',
+      abilityCoefficient: 1.0     // 标准模型
     }
   },
   
@@ -188,6 +249,33 @@ export const TOKEN_COST_CONFIG = {
   }
 };
 
+// ============================================
+// 新用户注册赠送积分配置
+// ============================================
+export const NEW_USER_BONUS = {
+  signupBonus: 20000,  // 新用户注册赠送20000积分(约20分钟音频)
+};
+
+// 新用户注册赠送积分
+export async function grantSignupBonus(userId: number): Promise<void> {
+  const bonus = NEW_USER_BONUS.signupBonus;
+  await prisma.tokenBalance.upsert({
+    where: { userId },
+    create: {
+      userId,
+      totalTokens: bonus,
+      usedTokens: 0,
+      packTokens: bonus,
+      usedPackTokens: 0,
+    },
+    update: {
+      totalTokens: { increment: bonus },
+      packTokens: { increment: bonus },
+    },
+  });
+  console.log(`[Bonus] 用户#${userId} 新用户注册赠送 ${bonus} 积分`);
+}
+
 // 计算Token生成成本(基于阿里百炼实际定价)
 export function calculateTokenCost(
   textLength: number, 
@@ -261,15 +349,16 @@ export const DEFAULT_PLANS = [
     overagePrice: 0
   },
   {
-    name: '入门版',
+    name: '普通会员',
     level: 1,
-    priceMonthly: 0.01,
-    priceYearly: 0.1,
-    description: '适合轻度使用',
+    priceMonthly: 5,
+    priceYearly: 50,
+    description: '适合轻度使用,Edge TTS享2折优惠',
     features: JSON.stringify([
       '每月300分钟音频',
-      '全部音色',
+      'Edge TTS音色',
       '高清音质',
+      'Edge TTS享2折优惠',
       `超出¥${BILLING.getOveragePrice(1)}/分钟`
     ]),
     isRecommended: false,
@@ -997,7 +1086,13 @@ export async function consumeAudioMinutes(
   const audioMinutes = calculateAudioDuration(textLength);
   const balance = await getUserAudioBalance(userId);
   const cost = calculateAudioCost(textLength, balance.memberLevel, balance.usedMinutes);
-  
+
+  // 获取会员TTS成本系数(普通会员Edge TTS享受2折)
+  const ttsCoefficient = BILLING.getTtsCostCoefficient(balance.memberLevel);
+  const actualTotalPrice = ttsCoefficient !== null
+    ? Math.round(cost.totalPrice * ttsCoefficient * 100) / 100
+    : cost.totalPrice;
+
   // 更新用户已使用时长
   await prisma.user.update({
     where: { id: userId },
@@ -1005,7 +1100,7 @@ export async function consumeAudioMinutes(
       usedAudioMinutes: { increment: audioMinutes }
     }
   });
-  
+
   // 记录使用日志
   const usage = await prisma.tokenUsage.create({
     data: {
@@ -1016,7 +1111,7 @@ export async function consumeAudioMinutes(
       description: description || `${audioMinutes}分钟音频`
     }
   });
-  
+
   return {
     usageId: usage.id,
     audioMinutes,
@@ -1025,7 +1120,8 @@ export async function consumeAudioMinutes(
     overageMinutes: cost.overageMinutes,
     inQuotaPrice: cost.inQuotaPrice,
     overagePrice: cost.overagePrice,
-    totalPrice: cost.totalPrice
+    totalPrice: actualTotalPrice,
+    costCoefficient: ttsCoefficient
   };
 }
 

+ 6 - 3
server/src/services/ai-call-logger.ts

@@ -34,14 +34,17 @@ function estimateCost(params: LogCallParams): number {
   const outputTokens = params.outputTokens || 0;
   const textLen = params.textLen || 0;
 
-  // LLM 调用:按 token 计费
+  // LLM 调用:按 token 计费(考虑能力系数)
   if (params.callType.startsWith('llm_')) {
     const modelKey = params.model.includes('qwen') ? 'qwen_plus' :
-                     params.model.includes('deepseek') ? 'deepseek_v3' : 'qwen_plus';
+                     params.model.includes('deepseek') ? 'deepseek_v3' :
+                     params.model.includes('qwen3_flash') ? 'qwen3_flash' :
+                     params.model.includes('qwen3_plus') ? 'qwen3_plus' : 'qwen_plus';
     const modelConfig = TOKEN_COST_CONFIG.models[modelKey] || TOKEN_COST_CONFIG.models.qwen_plus;
     const inputCost = (inputTokens / 1_000_000) * modelConfig.inputCostPerMToken;
     const outputCost = (outputTokens / 1_000_000) * modelConfig.outputCostPerMToken;
-    return Math.round((inputCost + outputCost) * 10000) / 10000;
+    const abilityCoeff = modelConfig.abilityCoefficient || 1.0;
+    return Math.round((inputCost + outputCost) * abilityCoeff * 10000) / 10000;
   }
 
   // TTS 实际合成调用:按字符数计费(cosyvoice ¥1/万字符)

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