Răsfoiți Sursa

fix: 修复页面导航后数据不显示的问题

1. chapter-detail.vue: 修复从详情页跳转时 navData 无 content 导致内容为空的问题,改为继续调用 API 获取
2. chapter-detail.vue: 移除 10 秒 loading Toast
3. book-generator/index.vue: 添加防重标志避免重复请求
4. playlists/detail.vue: 添加 onShow 刷新数据
5. video-generator/preview.vue: 添加 onShow 刷新数据
6. publish/index.vue: 添加 onShow 刷新数据

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
MyFramework User 4 luni în urmă
părinte
comite
e4e2c0364d

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

@@ -607,24 +607,27 @@ async function loadChapterWithParams(id: string, chapterIdParam: number) {
         chapterId.value = parseInt(chapter.id);
         chapterNumber.value = chapter.number;
         chapterTitle.value = chapter.title;
-        chapterContent.value = chapter.content || '章节内容正在生成中...';
         chapterWordCount.value = chapter.wordCount || 0;
         chapterStatus.value = chapter.status;
         chapterAudioUrl.value = chapter.audioUrl || '';
         chapterVideoUrl.value = chapter.videoUrl || '';
         chapterLevel.value = chapter.level || 1;
+        // 如果章节没有 content,继续调用 API 获取
+        if (!chapter.content) {
+          console.log('[ChapterDetail] navData chapter has no content, fetching via API');
+        } else {
+          chapterContent.value = chapter.content;
+          updatePrevNextChapters();
+          isLoading.value = false;
+          loadingChapter = false;
+          return;
+        }
       }
       updatePrevNextChapters();
-      isLoading.value = false;
-      loadingChapter = false;
-      return;
     }
 
     // 直接获取章节详情(只返回当前章节,不返回整书内容)
-    uni.showToast({ title: '正在加载...', icon: 'loading', duration: 10000 });
     const chapterData = await get<any>(`/book-generator/albums/chapters/${chapterIdParam}`);
-    uni.hideToast();
-    uni.hideToast();
     console.log('[ChapterDetail] chapter loaded via API:', chapterData);
 
     if (!chapterData) {
@@ -840,45 +843,42 @@ async function handleRegenerateAudio() {
 
 onMounted(() => {
   console.log('[ChapterDetail] onMounted, bookId:', bookId.value, 'chapterId:', chapterId.value);
-  // 如果 onLoad 已经处理了初始化(App环境或H5环境),这里不再重复调用
-  // 只有在 bookId 有值但还没加载的情况下才调用
-  if (!loadingChapter && !initialLoadDone) {
+  // 如果还没有参数,从 URL 获取
+  if (!bookId.value || !chapterId.value) {
+    const params = getParamsFromUrl();
+    if (params) {
+      bookId.value = params.bookId;
+      chapterId.value = params.chapterNumber;
+    }
+  }
+  // 如果有参数就加载
+  if (bookId.value && chapterId.value) {
     loadChapter();
   }
 });
 
-let initialLoadDone = false;
-
 onShow(() => {
-  // onShow 只需要刷新状态,不需要重新加载数据
+  // onShow 时从 URL 获取最新参数并加载
   console.log('[ChapterDetail] onShow, bookId:', bookId.value, 'chapterId:', chapterId.value);
+  const params = getParamsFromUrl();
+  if (params) {
+    bookId.value = params.bookId;
+    chapterId.value = params.chapterNumber;
+  }
+  if (bookId.value && chapterId.value) {
+    loadChapter();
+  }
 });
 
 onLoad((query: any) => {
   console.log('[ChapterDetail] onLoad, query:', query);
-  // H5 环境下从 URL hash 获取参数
-  // #ifdef H5
-  if (typeof window !== 'undefined') {
-    const hash = window.location.hash;
-    const match = hash.match(/bookId=([^&]+)&chapter=(\d+)/);
-    if (match) {
-      bookId.value = decodeURIComponent(match[1]);
-      chapterId.value = parseInt(match[2], 10);
-      console.log('[ChapterDetail] onLoad set params from hash, bookId:', bookId.value, 'chapterId:', chapterId.value);
-      initialLoadDone = true;
-      loadChapter();
-      return;
-    }
-  }
-  // #endif
-  // App 使用 onLoad 获取参数
+  // APP 环境使用 onLoad 的 query 参数
   if (query?.bookId && query?.chapter) {
     bookId.value = query.bookId;
     chapterId.value = parseInt(query.chapter);
     loadChapter();
-  } else {
-    console.log('[ChapterDetail] onLoad: missing params', query);
   }
+  // H5 环境由 onMounted 从 hash 获取参数
 });
 </script>
 

+ 197 - 3574
my-uniapp-vue3/src/pages/book-generator/index.vue

@@ -15,28 +15,26 @@
 
     <!-- 主内容区 -->
     <view class="main-content">
-      <!-- 视图1:书籍列表 -->
-      <view v-if="currentView === 'list'" class="list-view">
-        <!-- 创建新书籍按钮 -->
-        <view class="create-card" @click="currentView = 'create'">
-          <text class="create-icon">+</text>
-          <text class="create-text">创建新书籍</text>
-        </view>
+      <!-- 书籍列表 -->
+      <view class="create-card" @click="goToCreate">
+        <text class="create-icon">+</text>
+        <text class="create-text">创建新书籍</text>
+      </view>
 
-        <!-- 视频生成入口 -->
-        <view class="video-nav-card" @click="goToVideoGenerator">
-          <view class="video-nav-content">
-            <text class="video-nav-icon">🎬</text>
-            <view class="video-nav-text">
-              <text class="video-nav-title">视频生成</text>
-              <text class="video-nav-desc">图片+音频生成精美视频</text>
-            </view>
+      <!-- 视频生成入口 -->
+      <view class="video-nav-card" @click="goToVideoGenerator">
+        <view class="video-nav-content">
+          <text class="video-nav-icon">🎬</text>
+          <view class="video-nav-text">
+            <text class="video-nav-title">视频生成</text>
+            <text class="video-nav-desc">图片+音频生成精美视频</text>
           </view>
-          <text class="video-nav-arrow">→</text>
         </view>
+        <text class="video-nav-arrow">→</text>
+      </view>
 
-        <!-- 书籍列表 -->
-        <view v-if="books.length > 0" class="book-list">
+      <!-- 书籍列表 -->
+      <view v-if="books.length > 0" class="book-list">
           <view
             v-for="book in books"
             :key="book.id"
@@ -44,8 +42,8 @@
           >
             <view class="book-header" @click="openBook(book)">
               <text class="book-title">{{ book.title }}</text>
-              <view :class="['status-badge', book.status]">
-                {{ getStatusText(book.status) }}
+              <view :class="['status-badge', book.isPublished ? 'published' : book.status]">
+                {{ book.isPublished ? '已公开' : getStatusText(book.status) }}
               </view>
             </view>
             <text class="book-desc" @click="openBook(book)">{{ book.description }}</text>
@@ -56,23 +54,48 @@
             <view v-if="book.progress > 0 && book.progress < 100" class="progress-bar" @click="openBook(book)">
               <view class="progress-fill" :style="{ width: book.progress + '%' }"></view>
             </view>
-            <!-- 批量生成音频按钮 - 只要有章节就显示 -->
+            <!-- 批量生成音频按钮 - 根据状态显示不同文案 -->
             <view v-if="book.chapters && book.chapters.length > 0" class="book-actions">
               <button
                 class="audio-btn"
-                :disabled="generatingAudio"
+                :disabled="generatingAudio[book.id]"
                 @click.stop="handleGenerateAllAudio(book)"
               >
-                {{ generatingAudio ? '生成中...' : '🎵 生成全部音频' }}
+                <text v-if="generatingAudio[book.id]">🎵 生成中...</text>
+                <text v-else-if="getBookAudioStatus(book).status === 'completed'">🔄 重新生成音频</text>
+                <text v-else-if="getBookAudioStatus(book).status === 'partial'">🎵 继续生成音频({{ getBookAudioStatus(book).completed }}/{{ getBookAudioStatus(book).total }})</text>
+                <text v-else>🎵 生成全部音频</text>
+              </button>
+              <button
+                class="merge-audio-btn"
+                :disabled="generatingAudio[book.id] || mergingAudio[book.id] || !canMergeAudio(book)"
+                @click.stop="handleMergeChapterAudio(book)"
+              >
+                <text v-if="mergingAudio[book.id]">🔄 合并中...</text>
+                <text v-else-if="canMergeAudio(book)">🔊 合并音频</text>
+                <text v-else>🔒 需先完成音频生成</text>
+              </button>
+              <button
+                class="merge-video-btn"
+                :disabled="generatingVideo[book.id] || mergingVideo[book.id] || !canMergeVideo(book)"
+                @click.stop="handleMergeChapterVideo(book)"
+              >
+                <text v-if="mergingVideo[book.id]">🔄 合并中...</text>
+                <text v-else-if="canMergeVideo(book)">🎬 合并视频</text>
+                <text v-else>🔒 需先完成视频生成</text>
               </button>
               <button
                 class="video-btn"
-                :disabled="generatingVideo"
+                :disabled="generatingVideo[book.id]"
                 @click.stop="handleGenerateAllVideo(book)"
               >
-                {{ generatingVideo ? '生成中...' : '🎬 生成全部视频' }}
+                <text v-if="generatingVideo[book.id]">🎬 生成中...</text>
+                <text v-else-if="getBookVideoStatus(book).status === 'completed'">🔄 重新生成视频</text>
+                <text v-else-if="getBookVideoStatus(book).status === 'partial'">🎬 继续生成视频({{ getBookVideoStatus(book).completed }}/{{ getBookVideoStatus(book).total }})</text>
+                <text v-else>🎬 生成全部视频</text>
               </button>
               <button
+                v-if="false"
                 class="publish-all-btn"
                 @click.stop="goToPublishBook(book)"
               >
@@ -95,860 +118,42 @@
           <text class="empty-text">暂无书籍</text>
           <text class="empty-hint">点击上方按钮创建第一本书</text>
         </view>
-      </view>
-
-      <!-- 视图2:创建书籍 -->
-      <view v-if="currentView === 'create'" class="create-view">
-        <view class="card">
-          <view class="card-header">
-            <text class="card-title">📖 创建新书籍</text>
-          </view>
-
-          <!-- 书名 -->
-          <view class="form-item">
-            <text class="form-label">书名 *</text>
-            <input
-              v-model="newBook.title"
-              class="form-input"
-              placeholder="例如:《时间是什么》"
-            />
-          </view>
-
-          <!-- 副标题 -->
-          <view class="form-item">
-            <text class="form-label">副标题</text>
-            <input
-              v-model="newBook.subtitle"
-              class="form-input"
-              placeholder="例如:一本写给青少年的科普书"
-            />
-          </view>
-
-          <!-- 描述/主题 -->
-          <view class="form-item">
-            <text class="form-label">内容描述 *</text>
-            <textarea
-              v-model="newBook.description"
-              class="form-textarea"
-              placeholder="描述这本书的内容、主题、写作目的..."
-              :maxlength="500"
-            />
-          </view>
-
-          <!-- 快速模板 -->
-          <view class="form-item">
-            <text class="form-label">📋 快速模板</text>
-            <text class="form-hint-small">点击模板自动填充所有配置,也可手动修改</text>
-            <view class="template-grid">
-              <view
-                v-for="template in quickTemplates"
-                :key="template.name"
-                class="template-card"
-                @click="applyTemplate(template)"
-              >
-                <text class="template-icon">{{ template.icon }}</text>
-                <text class="template-name">{{ template.name }}</text>
-                <text class="template-desc">{{ template.desc }}</text>
-              </view>
-            </view>
-          </view>
-
-          <!-- 重要提示 -->
-          <view class="important-tip">
-            <view class="tip-header">
-              <text class="tip-icon">💡</text>
-              <text class="tip-title">重要提示</text>
-            </view>
-            <text class="tip-content">
-              知识难度和面向人群会显著影响生成内容的风格和深度。同样的知识,面向不同人群,表达方式完全不同:
-            </text>
-            <view class="tip-examples">
-              <text class="tip-example">• 面向小学生:形象、简单、通俗易懂</text>
-              <text class="tip-example">• 面向大学生:系统、专业、有理论深度</text>
-              <text class="tip-example">• 面向研究生:前沿、深入、有研究价值</text>
-            </view>
-          </view>
-
-          <!-- 知识难度 -->
-          <view class="form-item highlight-item">
-            <view class="label-with-badge">
-              <text class="form-label">知识难度</text>
-              <view class="required-badge">重要</view>
-            </view>
-            <text class="form-hint">选择适合的知识深度,AI会根据难度调整内容深度和专业程度</text>
-            <view class="chip-group">
-              <view
-                v-for="level in knowledgeLevels"
-                :key="level.value"
-                :class="['chip', 'level-chip', newBook.knowledgeLevel === level.value ? 'active' : '']"
-                @click="newBook.knowledgeLevel = level.value"
-              >
-                <text class="chip-icon">{{ level.icon }}</text>
-                <text class="chip-text">{{ level.label }}</text>
-              </view>
-            </view>
-          </view>
-
-          <!-- 面向人群 -->
-          <view class="form-item highlight-item">
-            <view class="label-with-badge">
-              <text class="form-label">面向人群</text>
-              <view class="required-badge">重要</view>
-            </view>
-            <text class="form-hint">选择目标读者,AI会根据人群调整语言风格和表达方式</text>
-            <view class="chip-group">
-              <view
-                v-for="audience in audiences"
-                :key="audience.value"
-                :class="['chip', 'audience-chip', newBook.targetAudience === audience.value ? 'active' : '']"
-                @click="newBook.targetAudience = audience.value"
-              >
-                <text class="chip-icon">{{ audience.icon }}</text>
-                <text class="chip-text">{{ audience.label }}</text>
-              </view>
-            </view>
-          </view>
-
-          <!-- 写作风格 -->
-          <view class="form-item">
-            <text class="form-label">写作风格</text>
-            <view class="chip-group">
-              <view
-                v-for="style in styles"
-                :key="style"
-                :class="['chip', newBook.style === style ? 'active' : '']"
-                @click="newBook.style = style"
-              >
-                {{ style }}
-              </view>
-            </view>
-          </view>
-
-          <!-- 行业领域 -->
-          <view class="form-item">
-            <text class="form-label">行业领域</text>
-            <text class="form-hint-small">选择行业领域,AI会使用相关的案例和术语</text>
-            <view class="chip-group">
-              <view
-                v-for="industry in industries"
-                :key="industry.value"
-                :class="['chip', newBook.industry === industry.value ? 'active' : '']"
-                @click="newBook.industry = industry.value"
-              >
-                <text class="chip-icon">{{ industry.icon }}</text>
-                <text class="chip-text">{{ industry.label }}</text>
-              </view>
-            </view>
-          </view>
-
-          <!-- 特殊要求 (多选) -->
-          <view class="form-item">
-            <text class="form-label">特殊要求</text>
-            <text class="form-hint-small">可多选,AI会在生成时包含这些元素</text>
-            <view class="chip-group">
-              <view
-                v-for="feature in specialFeatures"
-                :key="feature.value"
-                :class="['chip', 'feature-chip', newBook.specialFeatures.includes(feature.value) ? 'active' : '']"
-                @click="toggleSpecialFeature(feature.value)"
-              >
-                <text class="chip-icon">{{ feature.icon }}</text>
-                <text class="chip-text">{{ feature.label }}</text>
-              </view>
-            </view>
-          </view>
-
-          <!-- 书籍规模 -->
-          <view class="form-item">
-            <text class="form-label">书籍规模</text>
-            <view class="scale-picker">
-              <view
-                v-for="scale in bookScales"
-                :key="scale.value"
-                :class="['scale-option', { active: newBook.bookScale === scale.value }]"
-                @click="selectBookScale(scale.value)"
-              >
-                <text class="scale-words">{{ scale.words }}</text>
-                <text class="scale-label">{{ scale.label }}</text>
-                <text class="scale-pages">{{ scale.pages }}</text>
-              </view>
-            </view>
-          </view>
-
-          <!-- 预估信息(选择规模后显示) -->
-          <view v-if="bookEstimate && bookEstimate.words && bookEstimate.audioMinutes" class="estimate-card">
-            <view class="estimate-header">
-              <text class="estimate-title">📊 生成预估</text>
-            </view>
-            <view class="estimate-row">
-              <text class="estimate-label">预估字数:</text>
-              <text class="estimate-value">{{ bookEstimate.words.min }}~{{ bookEstimate.words.max }}字</text>
-            </view>
-            <view class="estimate-row">
-              <text class="estimate-label">预估时长:</text>
-              <text class="estimate-value">{{ bookEstimate.audioMinutes.min }}~{{ bookEstimate.audioMinutes.max }}分钟</text>
-            </view>
-            <view class="estimate-row">
-              <text class="estimate-label">预估章节:</text>
-              <text class="estimate-value">约{{ bookEstimate.estimatedChapters }}章</text>
-            </view>
-            
-            <!-- 配额检查结果 -->
-            <view v-if="quotaCheck" class="quota-check">
-              <view v-if="quotaCheck.allowed" class="quota-ok">
-                <text class="quota-icon">✅</text>
-                <text class="quota-text">额度充足,可生成</text>
-              </view>
-              <view v-else class="quota-warning">
-                <text class="quota-icon">⚠️</text>
-                <text class="quota-text">{{ quotaCheck.reason }}</text>
-              </view>
-              
-              <!-- 详细配额信息 -->
-              <view class="quota-detail">
-                <text class="quota-info">您当前额度:{{ quotaCheck.quota.remainingMinutes }}分钟 / {{ quotaCheck.quota.totalMinutes }}分钟</text>
-                <view v-if="quotaCheck.costEstimate.overageMinutes > 0" class="quota-overage">
-                  <text>预计超出:{{ quotaCheck.costEstimate.overageMinutes }}分钟</text>
-                  <text class="quota-price">额外费用:¥{{ quotaCheck.costEstimate.estimatedPrice }}</text>
-                </view>
-              </view>
-            </view>
-          </view>
-
-          <view class="btn-group">
-            <button class="btn-cancel" @click="currentView = 'list'">取消</button>
-            <button
-              class="btn-primary"
-              :disabled="!canCreateBook || creating"
-              @click="createNewBook"
-            >
-              {{ creating ? '创建中...' : '创建书籍' }}
-            </button>
-          </view>
-        </view>
-      </view>
-
-      <!-- 视图3:书籍详情/生成 -->
-      <view v-if="currentView === 'detail'" class="detail-view">
-        <!-- 书籍信息 -->
-        <view class="card book-info">
-          <view class="book-title-row">
-            <text class="book-title-large">{{ currentBook?.title }}</text>
-            <view :class="['status-badge', currentBook?.status]">
-              {{ getStatusText(currentBook?.status || 'draft') }}
-            </view>
-          </view>
-          <text v-if="currentBook?.subtitle" class="book-subtitle">{{ currentBook.subtitle }}</text>
-          <text class="book-desc">{{ currentBook?.description }}</text>
-          <view class="book-meta-row">
-            <text>章节:{{ currentBook?.totalChapters }} 章</text>
-            <text>预估:{{ currentBook?.estimatedWords || 0 }} 字</text>
-          </view>
-        </view>
-
-        <!-- 进度显示 -->
-        <view v-if="currentBook && currentBook.progress > 0" class="card progress-card">
-          <text class="card-title">📊 生成进度</text>
-          <view class="progress-display">
-            <text class="progress-text">{{ currentBook.progress }}%</text>
-            <text class="progress-detail">
-              {{ completedChapters }}/{{ currentBook.totalChapters }} 章
-            </text>
-          </view>
-          <view class="progress-bar-large">
-            <view class="progress-fill" :style="{ width: currentBook.progress + '%' }"></view>
-          </view>
-          
-          <!-- 实时生成状态面板 -->
-          <view v-if="currentBook.status === 'generating'" class="generation-status">
-            <view class="status-header">
-              <text class="status-icon">⚡</text>
-              <text class="status-title">正在生成中...</text>
-            </view>
-            
-            <!-- 当前阶段 -->
-            <view class="status-stage">
-              <text class="stage-label">当前阶段:</text>
-              <text class="stage-value">{{ getCurrentStage() }}</text>
-            </view>
-            
-            <!-- 错误信息(如果有) -->
-            <view v-if="currentBook.error" class="status-error">
-              <text class="error-icon">⚠️</text>
-              <text class="error-text">{{ currentBook.error }}</text>
-            </view>
-            
-            <!-- 自动恢复提示 -->
-            <view v-if="currentBook.error && currentBook.error.includes('自动恢复')" class="status-recovery">
-              <text class="recovery-icon">🔄</text>
-              <text class="recovery-text">系统正在尝试自动恢复...</text>
-            </view>
-            
-            <!-- 提示 -->
-            <view class="status-tip">
-              <text class="tip-text">💡 生成过程可能需要几分钟,请耐心等待</text>
-            </view>
-          </view>
-          
-          <!-- 实时配额显示 -->
-          <view v-if="currentBook.status === 'generating'" class="quota-monitor">
-            <text class="quota-monitor-title">🎧 额度监控</text>
-            <view class="quota-monitor-row">
-              <text>已用:{{ usedAudioMinutes }}分钟</text>
-              <text>剩余:{{ remainingAudioMinutes }}分钟</text>
-            </view>
-            <view v-if="overageMinutes > 0" class="quota-overage-warning">
-              <text>⚠️ 已超出配额 {{ overageMinutes }} 分钟</text>
-            </view>
-          </view>
-          
-          <!-- 中断提示 -->
-          <view v-if="currentBook.status === 'failed' && currentBook?.error?.includes('额度')" class="interrupted-tip">
-            <text class="interrupted-icon">⚠️</text>
-            <text class="interrupted-text">生成已中断:额度不足,已保存当前进度</text>
-            <text class="interrupted-hint">可升级套餐后继续生成</text>
-          </view>
-          
-          <!-- 失败提示(非额度原因) -->
-          <view v-if="currentBook.status === 'failed' && !currentBook?.error?.includes('额度')" class="failed-tip">
-            <text class="failed-icon">❌</text>
-            <text class="failed-text">生成失败</text>
-            <text v-if="currentBook.error" class="failed-error">{{ currentBook.error }}</text>
-            <text class="failed-hint">请检查网络或稍后重试</text>
-            <button class="retry-btn" @click="handleRetryGenerate">🔄 重新生成</button>
-          </view>
-        </view>
-
-        <!-- 完整内容区(整合大纲+实际内容) -->
-        <view v-if="currentBook?.chapters && currentBook.chapters.length > 0" class="card content-card">
-          <view class="card-header-row">
-            <text class="card-title">📖 完整内容</text>
-            <text v-if="currentBook.outline?.mainTheme" class="outline-theme">{{ currentBook.outline.mainTheme }}</text>
-          </view>
-
-          <!-- 前言 -->
-          <view v-if="currentBook?.metadata?.foreword" class="content-item foreword" @click="viewForeword">
-            <view class="content-item-left">
-              <text class="content-label">前言</text>
-              <text class="content-word-count">{{ currentBook?.metadata?.foreword?.length || 0 }}字</text>
-            </view>
-            <view class="content-item-right">
-              <text class="expand-icon">→</text>
-            </view>
-          </view>
-
-          <!-- 章节列表(树形结构,整合大纲信息) -->
-          <view class="content-tree-list">
-            <!-- 章 (只遍历level=1) -->
-            <view
-              v-for="chapter in chaptersLevel1"
-              :key="chapter.id"
-              class="content-tree-item content-chapter"
-            >
-              <view class="content-tree-row chapter-row">
-                <view class="chapter-num-badge">{{ chapter.number }}</view>
-                <view class="chapter-info">
-                  <text class="chapter-title">{{ chapter.title }}</text>
-                  <!-- 显示大纲概要 -->
-                  <text v-if="getChapterOutline(chapter.number)?.summary" class="chapter-outline-summary">
-                    {{ getChapterOutline(chapter.number)?.summary }}
-                  </text>
-                  <!-- 显示核心知识点 -->
-                  <view v-if="getChapterOutline(chapter.number)?.keyPoints" class="chapter-key-points">
-                    <text 
-                      v-for="(point, idx) in getChapterOutline(chapter.number)?.keyPoints?.slice(0, 3) || []" 
-                      :key="idx" 
-                      class="point-tag"
-                    >
-                      {{ point }}
-                    </text>
-                  </view>
-                  <!-- 媒体标识 -->
-                  <view class="chapter-media-badges">
-                    <text v-if="chapter.audioUrl" class="media-badge">🎵 音频</text>
-                    <text v-if="chapter.videoUrl" class="media-badge">🎬 视频</text>
-                  </view>
-                </view>
-                <!-- 章状态 -->
-                <view :class="['chapter-status', getNodeStatusClass(chapter)]">
-                  <text class="status-icon">{{ getNodeStatusIcon(chapter) }}</text>
-                  <text class="status-text">{{ getNodeStatusText(chapter) }}</text>
-                </view>
-              </view>
-          
-              <!-- 节(从数据库获取) -->
-              <view v-if="getChapterSectionsFromDB(chapter.id) && getChapterSectionsFromDB(chapter.id).length > 0" class="content-sections-list">
-                <view
-                  v-for="section in getChapterSectionsFromDB(chapter.id)"
-                  :key="'sec-' + section.id"
-                  class="content-tree-item content-section"
-                >
-                  <view class="content-tree-row section-row">
-                    <view class="section-num-badge">{{ section.number }}</view>
-                    <view class="section-info">
-                      <text class="section-title">{{ section.title }}</text>
-                      <text v-if="section.summary" class="section-summary">{{ section.summary }}</text>
-                    </view>
-                    <!-- 节状态 -->
-                    <view :class="['section-status', getNodeStatusClass(section)]">
-                      <text class="status-icon">{{ getNodeStatusIcon(section) }}</text>
-                      <text class="status-text">{{ getNodeStatusText(section) }}</text>
-                    </view>
-                  </view>
-          
-                  <!-- 小节(从数据库获取) -->
-                  <view v-if="getSectionSubsectionsFromDB(section.id) && getSectionSubsectionsFromDB(section.id).length > 0" class="content-subsections-list">
-                    <view
-                      v-for="subsection in getSectionSubsectionsFromDB(section.id)"
-                      :key="'sub-' + subsection.id"
-                      class="content-tree-item content-subsection"
-                      @click="goToChapterDetail(subsection)"
-                    >
-                      <view class="content-tree-row subsection-row">
-                        <view class="subsection-num-badge">{{ subsection.number }}</view>
-                        <view class="subsection-info">
-                          <text class="subsection-title">{{ subsection.title }}</text>
-                          <!-- 媒体标识 -->
-                          <view class="subsection-media-badges">
-                            <text v-if="subsection.audioUrl" class="media-badge-small">🎵 音频</text>
-                            <text v-if="subsection.videoUrl" class="media-badge-small">🎬 视频</text>
-                          </view>
-                          <!-- 内容预览 -->
-                          <text v-if="subsection.content" class="subsection-content-preview">
-                            {{ subsection.content.substring(0, 50) }}...
-                          </text>
-                        </view>
-                        <!-- 小节状态 -->
-                        <view :class="['subsection-status', getNodeStatusClass(subsection)]">
-                          <text class="status-icon">{{ getNodeStatusIcon(subsection) }}</text>
-                          <text class="status-text">{{ getNodeStatusText(subsection) }}</text>
-                        </view>
-                      </view>
-                    </view>
-                  </view>
-                </view>
-              </view>
-          
-              <!-- 如果章没有节,章本身就是叶子节点,添加点击链接 -->
-              <view v-else class="content-tree-item content-subsection" @click="goToChapterDetail(chapter)">
-                <view class="content-tree-row subsection-row">
-                  <view class="subsection-info" style="margin-left: 56rpx;">
-                    <text class="subsection-title">{{ chapter.title }}</text>
-                  </view>
-                  <view class="content-item-right">
-                    <text class="expand-icon">→</text>
-                  </view>
-                </view>
-              </view>
-            </view>
-          </view>
-
-          <!-- 后记 -->
-          <view v-if="currentBook?.metadata?.afterword" class="content-item afterword" @click="viewAfterword">
-            <view class="content-item-left">
-              <text class="content-label">后记</text>
-              <text class="content-word-count">{{ currentBook?.metadata?.afterword?.length || 0 }}字</text>
-            </view>
-            <view class="content-item-right">
-              <text class="expand-icon">→</text>
-            </view>
-          </view>
-        </view>
-
-        <!-- 操作按钮 -->
-        <view class="card action-card">
-          <!-- LangGraph 生成按钮 -->
-          <view class="btn-col">
-            <view class="btn-row">
-              <button
-                class="action-btn langgraph-btn"
-                :disabled="generating"
-                @click="handleLangGraphGenerate"
-              >
-                {{ generating ? '生成中...' : '🤖 开始生成' }}
-              </button>
-            </view>
-          </view>
-
-          <!-- 生成中 -->
-          <view v-if="generating" class="generating-tip">
-            <text>正在生成中,请稍候...</text>
-          </view>
-
-          <!-- 无章节警告 -->
-          <view v-if="currentBook?.status === 'completed' && (!currentBook.chapters || currentBook.chapters.length === 0)" class="warning-tip">
-            <text class="warning-icon">⚠️</text>
-            <text class="warning-text">该书籍状态标记为已完成,但没有任何章节内容。</text>
-            <text class="warning-hint">请尝试点击上方按钮重新生成。</text>
-          </view>
-
-          <!-- 返回列表 -->
-          <button class="action-btn full-width" @click="backToList">
-            返回书籍列表
-          </button>
-        </view>
-      </view>
     </view>
   </view>
 </template>
 
 <script setup lang="ts">
-import { ref, computed, onMounted, watch, nextTick } from 'vue';
+import { ref, onMounted } from 'vue';
 import { onShow } from '@dcloudio/uni-app';
 import * as api from '../../utils/book-generator-api';
-import type { Book, BookOutline, Chapter } from '../../utils/book-generator-api';
-import { wsService } from '../../utils/websocket';
-
-// 使用相对路径(Nginx 反向代理)
-const BASE_URL = '/api';
-
-// 视图状态
-const currentView = ref<'list' | 'create' | 'detail' | 'toc' | 'chapter-detail'>('list');
-
-// 监听视图变化
-watch(currentView, (newView) => {
-  if (newView === 'create') {
-    // 打开创建视图时,加载默认规模的预估
-    nextTick(() => {
-      selectBookScale(newBook.value.bookScale);
-    });
-  }
-});
+import type { Book } from '../../utils/book-generator-api';
 
 // 书籍列表
 const books = ref<Book[]>([]);
+const isLoadingBooks = ref(false);
 
-// 当前书籍
-const currentBook = ref<Book | null>(null);
-
-// 创建表单
-const showCreateModal = ref(false);
-const creating = ref(false);
-const newBook = ref({
-  title: '',
-  subtitle: '',
-  description: '',
-  targetAudience: '',
-  knowledgeLevel: '',  // 知识难度
-  industry: '',  // 行业领域
-  specialFeatures: [] as string[],  // 特殊要求(多选)
-  style: '',
-  bookScale: 'medium',
-});
-
-// 生成状态
-const generating = ref(false);
-
-// 书籍预估信息
-const bookEstimate = ref<{
-  scale: string;
-  words: { min: number; max: number; avg: number };
-  audioMinutes: { min: number; max: number; avg: number };
-  estimatedChapters: number;
-} | null>(null);
-
-// 配额检查结果
-const quotaCheck = ref<{
-  allowed: boolean;
-  reason: string | null;
-  estimatedWords: { min: number; max: number; avg: number };
-  estimatedAudioMinutes: { min: number; max: number; avg: number };
-  quota: {
-    totalMinutes: number;
-    usedMinutes: number;
-    remainingMinutes: number;
-    overageEnabled: boolean;
-    overagePrice: number;
-  };
-  costEstimate: {
-    inQuotaMinutes: number;
-    overageMinutes: number;
-    estimatedPrice: number;
-    displayText: string;
-  };
-} | null>(null);
-
-// 音频生成状态
-const generatingAudio = ref(false);
-
-// 视频生成状态
-const generatingVideo = ref(false);
-
-// 书籍公开状态
-const togglingPublish = ref<Record<string, boolean>>({});
-
-// 选项配置
-// 知识难度选项
-const knowledgeLevels = [
-  { value: '入门', label: '入门级', icon: '🌱', desc: '零基础,简单易懂' },
-  { value: '基础', label: '基础级', icon: '📚', desc: '基本概念,循序渐进' },
-  { value: '进阶', label: '进阶级', icon: '📈', desc: '深入讲解,系统全面' },
-  { value: '高级', label: '高级级', icon: '🎯', desc: '专业深入,前沿技术' },
-  { value: '专家', label: '专家级', icon: '🔬', desc: '研究级别,底层原理' },
-];
-
-// 面向人群选项
-const audiences = [
-  { value: '儿童', label: '儿童', icon: '👶', desc: '6-12岁,形象生动' },
-  { value: '青少年', label: '青少年', icon: '🧑', desc: '13-18岁,通俗易懂' },
-  { value: '大学生', label: '大学生', icon: '🎓', desc: '系统专业,有理论' },
-  { value: '专业人士', label: '专业人士', icon: '💼', desc: '从业者,实用深入' },
-  { value: '研究生', label: '研究生', icon: '🔍', desc: '研究级别,前沿深入' },
-  { value: '大众读者', label: '大众读者', icon: '👥', desc: '通俗普及,有趣味' },
-];
-
-// 行业领域选项
-const industries = [
-  { value: 'IT/计算机', label: 'IT/计算机', icon: '💻', desc: '编程、软件、互联网' },
-  { value: '金融', label: '金融', icon: '💰', desc: '投资、银行、证券' },
-  { value: '医学', label: '医学', icon: '🏥', desc: '医疗、健康、临床' },
-  { value: '教育', label: '教育', icon: '📚', desc: '教学、学习、心理学' },
-  { value: '文学', label: '文学', icon: '✍️', desc: '小说、散文、传记' },
-  { value: '科普', label: '科普', icon: '🔬', desc: '科学、自然、探索' },
-  { value: '商业', label: '商业', icon: '📊', desc: '管理、营销、创业' },
-  { value: '艺术', label: '艺术', icon: '🎨', desc: '设计、音乐、影视' },
-];
-
-// 特殊要求选项
-const specialFeatures = [
-  { value: '案例分析', label: '案例分析', icon: '📖' },
-  { value: '代码示例', label: '代码示例', icon: '💻' },
-  { value: '习题练习', label: '习题练习', icon: '✍️' },
-  { value: '图表插图', label: '图表插图', icon: '📊' },
-  { value: '项目实战', label: '项目实战', icon: '🎯' },
-  { value: '思维导图', label: '思维导图', icon: '🧠' },
-  { value: '总结回顾', label: '总结回顾', icon: '📝' },
-];
-
-// 快速模板
-const quickTemplates = [
-  {
-    name: '大学教材',
-    icon: '🎓',
-    desc: '面向大学生,系统专业,有案例习题',
-    config: {
-      knowledgeLevel: '进阶',
-      targetAudience: '大学生',
-      industry: '教育',
-      specialFeatures: ['案例分析', '习题练习', '总结回顾'],
-      style: '专业严谨',
-    },
-  },
-  {
-    name: '技术教程',
-    icon: '💻',
-    desc: '面向初学者,有代码示例和实战项目',
-    config: {
-      knowledgeLevel: '入门',
-      targetAudience: '初学者',
-      industry: 'IT/计算机',
-      specialFeatures: ['代码示例', '项目实战', '案例分析'],
-      style: '通俗易懂',
-    },
-  },
-  {
-    name: '科普读物',
-    icon: '🔬',
-    desc: '面向大众,通俗易懂,有趣味性',
-    config: {
-      knowledgeLevel: '入门',
-      targetAudience: '大众读者',
-      industry: '科普',
-      specialFeatures: ['案例分析', '图表插图'],
-      style: '轻松幽默',
-    },
-  },
-  {
-    name: '商业书籍',
-    icon: '📊',
-    desc: '面向从业者,有真实案例,实用性强',
-    config: {
-      knowledgeLevel: '进阶',
-      targetAudience: '专业人士',
-      industry: '商业',
-      specialFeatures: ['案例分析', '总结回顾'],
-      style: '专业严谨',
-    },
-  },
-  {
-    name: '儿童读物',
-    icon: '👶',
-    desc: '面向儿童,形象生动,有趣味性',
-    config: {
-      knowledgeLevel: '入门',
-      targetAudience: '儿童',
-      industry: '教育',
-      specialFeatures: ['图表插图'],
-      style: '轻松幽默',
-    },
-  },
-  {
-    name: '学术研究',
-    icon: '🔍',
-    desc: '面向研究生,前沿深入,有研究价值',
-    config: {
-      knowledgeLevel: '专家',
-      targetAudience: '研究生',
-      industry: 'IT/计算机',
-      specialFeatures: ['案例分析', '总结回顾'],
-      style: '专业严谨',
-    },
-  },
-];
-
-const styles = ['不填', '通俗易懂', '专业严谨', '轻松幽默', '诗意优美', '故事化'];
-const bookScales = [
-  { value: '800', label: '短文', words: '800字', pages: '约3页' },
-  { value: '2000', label: '短文', words: '2000字', pages: '约8页' },
-  { value: '5000', label: '短文', words: '5000字', pages: '约20页' },
-  { value: '小册子', label: '小册子', words: '1~5万字', pages: '约50~120页' },
-  { value: '标准教程', label: '标准教程', words: '5~15万字', pages: '约150~300页' },
-  { value: '系统教材', label: '系统教材', words: '15~30万字', pages: '约300~550页' },
-  // { value: '专业厚本', label: '专业厚本', words: '30~60万字', pages: '约550~900页' },
-  // { value: '大部头', label: '大部头', words: '60万字以上', pages: '900页以上' },
-];
-
-// 计算属性
-const canCreateBook = computed(() => {
-  // 免费版需要检查配额
-  if (quotaCheck.value && !quotaCheck.value.allowed) {
-    return false;
-  }
-  return newBook.value.title.trim() && newBook.value.description.trim();
-});
-
-const completedChapters = computed(() => {
-  if (!currentBook.value) return 0;
-  return currentBook.value.chapters.filter((c) => c.status === 'completed').length;
-});
-
-// 过滤出level=1的章(避免在模板中使用filter导致重复渲染)
-const chaptersLevel1 = computed(() => {
-  if (!currentBook.value?.chapters) return [];
-  return currentBook.value.chapters.filter((c) => c.level === 1);
-});
-
-// 计算已使用的音频时长(基于已完成章节的字数)
-const usedAudioMinutes = computed(() => {
-  if (!currentBook.value) return 0;
-  const totalWords = currentBook.value.chapters
-    .filter((c) => c.status === 'completed')
-    .reduce((sum, c) => sum + (c.wordCount || 0), 0);
-  return Math.ceil(totalWords / 150); // 150字/分钟
-});
-
-// 计算剩余配额
-const remainingAudioMinutes = computed(() => {
-  if (!quotaInfo.value) return 0;
-  return Math.max(0, quotaInfo.value.totalMinutes - quotaInfo.value.usedMinutes);
-});
-
-// 计算超出配额分钟数
-const overageMinutes = computed(() => {
-  if (!currentBook.value) return 0;
-  const used = usedAudioMinutes.value;
-  if (!quotaInfo.value) return 0;
-  return Math.max(0, used - quotaInfo.value.totalMinutes);
-});
-
-// 配额信息(从 API 获取)
-const quotaInfo = ref<{
-  totalMinutes: number;
-  usedMinutes: number;
-  remainingMinutes: number;
-  overageEnabled: boolean;
-} | null>(null);
-
-// 配额加载状态
-let quotaLoading = false;
-let quotaLastLoadTime = 0;
-const QUOTA_CACHE_DURATION = 30000; // 30秒缓存;
+// 音频/视频生成状态
+const generatingAudio = ref<Record<string, boolean> >({});
+const generatingVideo = ref<Record<string, boolean> >({});
+const mergingAudio = ref<Record<string, boolean> >({});
+const mergingVideo = ref<Record<string, boolean> >({});
+const togglingPublish = ref<Record<string, boolean> >({});
 
-// 切换特殊要求
-function toggleSpecialFeature(feature: string) {
-  const features = newBook.value.specialFeatures;
-  const index = features.indexOf(feature);
-  if (index > -1) {
-    features.splice(index, 1);
-  } else {
-    features.push(feature);
-  }
-}
-
-// 应用模板
-function applyTemplate(template: typeof quickTemplates[0]) {
-  const config = template.config;
-  newBook.value.knowledgeLevel = config.knowledgeLevel;
-  newBook.value.targetAudience = config.targetAudience;
-  newBook.value.industry = config.industry;
-  newBook.value.specialFeatures = [...config.specialFeatures];
-  newBook.value.style = config.style;
-  
-  uni.showToast({
-    title: `已应用${template.name}模板`,
-    icon: 'success',
-  });
-}
-
-// 选择书籍规模时加载预估
-async function selectBookScale(scale: string) {
-  newBook.value.bookScale = scale;
-  
-  try {
-    const response = await uni.request({
-      url: `/api/book-generator/langgraph/estimate?scale=${encodeURIComponent(scale)}&userId=1`,
-      method: 'GET'
-    });
-    
-    const res = response.data as any;
-    if (res.code === 0 && res.data && res.data.words && res.data.audioMinutes) {
-      bookEstimate.value = {
-        scale: res.data.scale,
-        words: res.data.words,
-        audioMinutes: res.data.audioMinutes,
-        estimatedChapters: res.data.estimatedChapters
-      };
-      
-      // 如果有配额检查结果
-      if (res.data.quotaCheck) {
-        quotaCheck.value = res.data.quotaCheck;
-      }
-    } else {
-      console.warn('预估数据不完整:', res);
-      bookEstimate.value = null;
-    }
-  } catch (e) {
-    console.error('加载预估失败:', e);
-    bookEstimate.value = null;
-    quotaCheck.value = null;
-  }
-}
-
-// 从大纲中获取章节的 sections
-function getChapterSections(chapterNumber: number) {
-  if (!currentBook.value?.outline?.chapters) return [];
-  const chapter = currentBook.value.outline.chapters.find(c => c.number === chapterNumber);
-  return chapter?.sections || [];
-}
-
-// 从大纲中获取章节信息(包含summary和keyPoints)
-function getChapterOutline(chapterNumber: number) {
-  if (!currentBook.value?.outline?.chapters) return null;
-  return currentBook.value.outline.chapters.find(c => c.number === chapterNumber) || null;
-}
-
-// 方法
 function goBack() {
-  // 如果在详情页,返回列表;否则返回上一页
-  if (currentView.value === 'detail') {
-    backToList();
-  } else {
+  const pages = getCurrentPages();
+  if (pages.length > 1) {
     uni.navigateBack();
+  } else {
+    uni.switchTab({ url: '/pages/index/index' });
   }
 }
 
 function switchTab() {
-  uni.switchTab({ url: '/pages/index/index' });
+  uni.switchTab({ url: '/pages/book-generator/index' });
+}
+
+function goToCreate() {
+  uni.navigateTo({ url: '/pages/book-generator/create' });
 }
 
 function goToVideoGenerator() {
@@ -966,2765 +171,183 @@ function getStatusText(status: string): string {
   return map[status] || status;
 }
 
-/**
- * 获取当前生成阶段
- */
-function getCurrentStage(): string {
-  if (!currentBook.value) return '未知';
-  
-  const progress = currentBook.value.progress || 0;
-  const error = currentBook.value.error || '';
-  
-  // 如果有错误,优先显示
-  if (error) {
-    if (error.includes('AI调用')) return 'AI调用失败,正在重试...';
-    if (error.includes('超时')) return '节点执行超时,正在恢复...';
-    if (error.includes('自动恢复')) return '系统正在自动恢复...';
-  }
-  
-  // 根据进度判断阶段
-  if (progress === 0) return '准备中...';
-  if (progress <= 10) return '正在生成章大纲...';
-  if (progress <= 20) return '正在生成节大纲...';
-  if (progress <= 30) return '正在生成小节大纲...';
-  if (progress <= 90) return '正在生成章节内容...';
-  if (progress <= 95) return '正在生成前言...';
-  if (progress < 100) return '正在生成后记...';
-  if (progress === 100) return '生成完成!';
-  
-  return '生成中...';
-}
-
-/**
- * 获取节点状态样式类
- */
-function getNodeStatusClass(node: any): string {
-  if (node.contentStatus === 'completed') return 'status-completed';
-  if (node.contentStatus === 'generating') return 'status-generating';
-  if (node.contentStatus === 'failed') return 'status-failed';
-  if (node.status === 'completed') return 'status-completed';
-  if (node.status === 'pending') return 'status-pending';
-  return 'status-pending';
-}
-
-/**
- * 获取节点状态图标
- */
-function getNodeStatusIcon(node: any): string {
-  if (node.contentStatus === 'completed') return '✓';
-  if (node.contentStatus === 'generating') return '⚡';
-  if (node.contentStatus === 'failed') return '✗';
-  if (node.contentError) return '⚠';
-  if (node.status === 'completed') return '✓';
-  if (node.status === 'pending') return '○';
-  return '○';
+function getBookLeafLevel(book: Book): number {
+  const chapters = book.chapters || [];
+  if (chapters.length === 0) return 0;
+  const maxLevel = Math.max(...chapters.map(c => c.level || 0));
+  return maxLevel > 0 ? maxLevel : 1;
 }
 
-/**
- * 获取节点状态文本
- */
-function getNodeStatusText(node: any): string {
-  if (node.contentStatus === 'completed') return '已完成';
-  if (node.contentStatus === 'generating') return '生成中';
-  if (node.contentStatus === 'failed') return '失败';
-  if (node.contentError) return '错误';
-  if (node.status === 'completed') return '大纲完成';
-  if (node.status === 'pending') return '待生成';
-  return '待生成';
+function getBookLeafNodes(book: Book, leafLevel: number): any[] {
+  return (book.chapters || []).filter(c => c.level === leafLevel);
 }
 
-/**
- * 从数据库获取章的节列表(level=2)
- */
-function getChapterSectionsFromDB(chapterId: string | number) {
-  if (!currentBook.value?.chapters) return [];
-  const chapterIdNum = typeof chapterId === 'string' ? parseInt(chapterId) : chapterId;
-  const result = currentBook.value.chapters.filter((c: any) => {
-    const cParentId = typeof c.parentId === 'string' ? parseInt(c.parentId) : c.parentId;
-    return cParentId === chapterIdNum && c.level === 2;
-  });
-  console.log(`[getChapterSectionsFromDB] chapterId=${chapterId}, 找到${result.length}个节`);
-  return result;
+function getBookAudioStatus(book: Book) {
+  const leafLevel = getBookLeafLevel(book);
+  const leafNodes = getBookLeafNodes(book, leafLevel);
+  const total = leafNodes.length;
+  const completed = leafNodes.filter(n => n.audioUrl).length;
+  let status: 'none' | 'partial' | 'completed' = 'none';
+  if (completed === total && total > 0) status = 'completed';
+  else if (completed > 0) status = 'partial';
+  if (generatingAudio.value[book.id]) status = 'generating';
+  return { total, completed, status };
 }
 
-/**
- * 从数据库获取节的小节列表(level=3)
- */
-function getSectionSubsectionsFromDB(sectionId: string | number) {
-  if (!currentBook.value?.chapters) return [];
-  const sectionIdNum = typeof sectionId === 'string' ? parseInt(sectionId) : sectionId;
-  const result = currentBook.value.chapters.filter((c: any) => {
-    const cParentId = typeof c.parentId === 'string' ? parseInt(c.parentId) : c.parentId;
-    return cParentId === sectionIdNum && c.level === 3;
-  });
-  console.log(`[getSectionSubsectionsFromDB] sectionId=${sectionId}, 找到${result.length}个小节`);
-  return result;
+function getBookVideoStatus(book: Book) {
+  const leafLevel = getBookLeafLevel(book);
+  const leafNodes = getBookLeafNodes(book, leafLevel);
+  const total = leafNodes.length;
+  const completed = leafNodes.filter(n => n.videoUrl).length;
+  let status: 'none' | 'partial' | 'completed' = 'none';
+  if (completed === total && total > 0) status = 'completed';
+  else if (completed > 0) status = 'partial';
+  if (generatingVideo.value[book.id]) status = 'generating';
+  return { total, completed, status };
 }
 
-/**
- * 重新生成
- */
-async function handleRetryGenerate() {
-  if (!currentBook.value) return;
-  
-  uni.showModal({
-    title: '确认重新生成',
-    content: '将重新开始生成,是否继续?',
-    success: async (res) => {
-      if (res.confirm) {
-        // 调用LangGraph生成
-        await handleLangGraphGenerate();
-      }
-    }
-  });
+function canMergeAudio(book: Book): boolean {
+  const leafLevel = getBookLeafLevel(book);
+  if (leafLevel <= 1) return false;
+  const leafNodes = getBookLeafNodes(book, leafLevel);
+  return leafNodes.every(n => n.audioUrl) && leafNodes.length > 0;
 }
 
-function getChapterStatus(chapterNum: number): string {
-  const chapter = currentBook.value?.chapters.find((c) => c.number === chapterNum);
-  if (!chapter) return 'pending';
-  return chapter.status === 'completed' ? 'done' : 'pending';
+function canMergeVideo(book: Book): boolean {
+  const leafLevel = getBookLeafLevel(book);
+  if (leafLevel <= 1) return false;
+  const leafNodes = getBookLeafNodes(book, leafLevel);
+  return leafNodes.every(n => n.videoUrl) && leafNodes.length > 0;
 }
 
-function getChapterStatusText(chapterNum: number): string {
-  const chapter = currentBook.value?.chapters.find((c) => c.number === chapterNum);
-  if (!chapter) return '待生成';
-  return chapter.status === 'completed' ? '✓' : '○';
-}
-
-// 加载书籍列表
 async function loadBooks() {
+  if (isLoadingBooks.value) return;
+  isLoadingBooks.value = true;
   try {
-    const data = await api.getBooks();
-    books.value = data || [];
+    books.value = await api.getBooks() || [];
   } catch (e) {
     console.error('加载书籍失败:', e);
     books.value = [];
-  }
-}
-
-// 创建新书籍
-async function createNewBook() {
-  if (!canCreateBook.value) return;
-
-  // 显示确认弹窗
-  const configSummary: string[] = [];
-  if (newBook.value.knowledgeLevel) configSummary.push(`知识难度:${newBook.value.knowledgeLevel}`);
-  if (newBook.value.targetAudience) configSummary.push(`面向人群:${newBook.value.targetAudience}`);
-  if (newBook.value.industry) configSummary.push(`行业领域:${newBook.value.industry}`);
-  if (newBook.value.specialFeatures.length > 0) configSummary.push(`特殊要求:${newBook.value.specialFeatures.join('、')}`);
-  if (newBook.value.style) configSummary.push(`写作风格:${newBook.value.style}`);
-
-  const confirmResult = await new Promise<boolean>((resolve) => {
-    uni.showModal({
-      title: '📋 生成配置确认',
-      content: `书名:《${newBook.value.title}》\n\n${configSummary.join('\n')}\n\n确认开始生成?`,
-      confirmText: '确认生成',
-      cancelText: '返回修改',
-      success: (res) => resolve(res.confirm),
-    });
-  });
-
-  if (!confirmResult) return;
-
-  creating.value = true;
-  try {
-    // 将目标受众、知识难度、行业领域、特殊要求和写作风格合并到描述中,AI 生成时直接读取
-    let mergedDesc = newBook.value.description.trim();
-    if (newBook.value.knowledgeLevel && newBook.value.knowledgeLevel !== '不填') {
-      mergedDesc += `\n知识难度:${newBook.value.knowledgeLevel}`;
-    }
-    if (newBook.value.targetAudience && newBook.value.targetAudience !== '不填') {
-      mergedDesc += `\n面向人群:${newBook.value.targetAudience}`;
-    }
-    if (newBook.value.industry && newBook.value.industry !== '不填') {
-      mergedDesc += `\n行业领域:${newBook.value.industry}`;
-    }
-    if (newBook.value.specialFeatures && newBook.value.specialFeatures.length > 0) {
-      mergedDesc += `\n特殊要求:${newBook.value.specialFeatures.join('、')}`;
-    }
-    if (newBook.value.style && newBook.value.style !== '不填') {
-      mergedDesc += `\n写作风格:${newBook.value.style}`;
-    }
-
-    await api.createBook({
-      title: newBook.value.title,
-      subtitle: newBook.value.subtitle,
-      description: mergedDesc,
-      bookScale: newBook.value.bookScale,
-    });
-
-    // 重置表单
-    newBook.value = {
-      title: '',
-      subtitle: '',
-      description: '',
-      targetAudience: '',
-      knowledgeLevel: '',
-      industry: '',
-      specialFeatures: [],
-      style: '',
-      bookScale: 'medium',
-    };
-    showCreateModal.value = false;
-    currentView.value = 'list';
-    await loadBooks();
-    uni.showToast({ title: '创建成功', icon: 'success' });
-  } catch (e: any) {
-    uni.showToast({ title: e.message || '创建失败', icon: 'none' });
   } finally {
-    creating.value = false;
+    isLoadingBooks.value = false;
   }
 }
 
-// 打开书籍
 async function openBook(book: Book) {
-  try {
-    currentBook.value = await api.getBook(book.id);
-    currentView.value = 'detail';
-    // 更新 URL hash,带上书籍 ID
-    updateUrlWithBookId(book.id);
-    // 加载配额信息
-    loadQuotaInfo();
-  } catch (e) {
-    uni.showToast({ title: '加载失败', icon: 'none' });
-  }
+  uni.navigateTo({ url: `/pages/book-generator/detail?id=${book.id}` });
 }
 
-// 更新 URL,带上书籍 ID
-function updateUrlWithBookId(bookId: string) {
-  if (typeof window !== 'undefined') {
-    const url = new URL(window.location.href);
-    url.hash = `/pages/book-generator/index?id=${bookId}`;
-    window.history.pushState({}, '', url.toString());
-  }
-}
-
-// 从 URL 获取书籍 ID
-function getBookIdFromUrl(): string | null {
-  if (typeof window !== 'undefined') {
-    const hash = window.location.hash;
-    const match = hash.match(/id=([^&]+)/);
-    return match ? match[1] : null;
-  }
-  return null;
-}
-
-// 加载指定 ID 的书籍
-async function loadBookById(bookId: string) {
+async function handleGenerateAllAudio(book: Book) {
+  generatingAudio.value[book.id] = true;
   try {
-    currentBook.value = await api.getBook(bookId);
-    currentView.value = 'detail';
-    loadQuotaInfo();
-  } catch (e) {
-    console.error('加载书籍失败:', e);
-    uni.showToast({ title: '加载失败', icon: 'none' });
+    const result = await api.generateAllChaptersAudio(book.id, 'cherry');
+    uni.showToast({ title: `已启动 ${result.totalChapters} 个章节的音频生成`, icon: 'none', duration: 2500 });
+    setTimeout(async () => { await loadBooks(); generatingAudio.value[book.id] = false; }, 5000);
+  } catch (e: any) {
+    generatingAudio.value[book.id] = false;
+    uni.showToast({ title: e.message || '生成失败', icon: 'none' });
   }
 }
 
-// 返回列表
-function backToList() {
-  currentView.value = 'list';
-  loadBooks();
-  // 清除 URL 参数
-  if (typeof window !== 'undefined') {
-    const url = new URL(window.location.href);
-    url.hash = '/pages/book-generator/index';
-    window.history.pushState({}, '', url.toString());
+async function handleMergeChapterAudio(book: Book) {
+  if (!canMergeAudio(book)) { uni.showToast({ title: '并非所有叶节点音频都已生成完成', icon: 'none' }); return; }
+  mergingAudio.value[book.id] = true;
+  try {
+    const result = await api.mergeChapterAudios(book.id);
+    uni.showToast({ title: `已启动音频合并,处理${result.processedParents}个上级章节`, icon: 'none', duration: 2500 });
+    setTimeout(async () => { await loadBooks(); mergingAudio.value[book.id] = false; }, 5000);
+  } catch (e: any) {
+    mergingAudio.value[book.id] = false;
+    uni.showToast({ title: e.message || '音频合并失败', icon: 'none' });
   }
 }
 
-// 页面显示时检查 URL 参数
-onShow(() => {
-  const bookId = getBookIdFromUrl();
-  if (bookId && currentView.value === 'list') {
-    loadBookById(bookId);
-  }
-});
-
-// 加载用户配额信息(带缓存和防抖)
-async function loadQuotaInfo() {
-  // 防止并发请求
-  if (quotaLoading) {
-    return;
-  }
-
-  // 检查缓存是否有效(30秒内不重复请求)
-  const now = Date.now();
-  if (quotaInfo.value && (now - quotaLastLoadTime) < QUOTA_CACHE_DURATION) {
-    return;
-  }
-
-  quotaLoading = true;
+async function handleMergeChapterVideo(book: Book) {
+  if (!canMergeVideo(book)) { uni.showToast({ title: '并非所有叶节点视频都已生成完成', icon: 'none' }); return; }
+  mergingVideo.value[book.id] = true;
   try {
-    const response = await uni.request({
-      url: '/api/subscription/audio-balance',
-      method: 'GET'
-    });
-    
-    const res = response.data as any;
-    if (res.code === 0 && res.data) {
-      quotaInfo.value = {
-        totalMinutes: res.data.totalMinutes,
-        usedMinutes: res.data.usedMinutes,
-        remainingMinutes: res.data.remainingMinutes,
-        overageEnabled: res.data.overageEnabled
-      };
-      quotaLastLoadTime = now;
-    } else if (res.code === 429) {
-      // 如果是 429 错误,等待更长时间再重试
-      console.warn('配额接口请求过于频繁,稍后重试');
-      quotaLastLoadTime = now; // 更新时间戳,避免立即重试
-    }
-  } catch (e) {
-    console.error('加载配额信息失败:', e);
-  } finally {
-    quotaLoading = false;
+    const result = await api.mergeChapterVideos(book.id);
+    uni.showToast({ title: `已启动视频合并,处理${result.processedParents}个上级章节`, icon: 'none', duration: 2500 });
+    setTimeout(async () => { await loadBooks(); mergingVideo.value[book.id] = false; }, 5000);
+  } catch (e: any) {
+    mergingVideo.value[book.id] = false;
+    uni.showToast({ title: e.message || '视频合并失败', icon: 'none' });
   }
 }
 
-// 生成大纲
-async function handleGenerateOutline() {
-  if (!currentBook.value) return;
-
-  generating.value = true;
+async function handleGenerateAllVideo(book: Book) {
+  generatingVideo.value[book.id] = true;
   try {
-    const outline = await api.generateOutline(currentBook.value.id);
-    currentBook.value.outline = outline;
-    currentBook.value.status = 'planning';
-    // 初始化章节状态
-    currentBook.value.chapters = outline.chapters.map((c) => ({
-      id: '',
-      bookId: currentBook.value!.id,
-      number: c.number,
-      title: c.title,
-      content: '',
-      wordCount: 0,
-      status: 'pending' as const,
-    }));
-    uni.showToast({ title: '大纲生成成功', icon: 'success' });
+    const result = await api.generateAllChaptersVideo(book.id);
+    uni.showToast({ title: `已启动 ${result.totalChapters} 个章节的视频生成`, icon: 'none', duration: 2500 });
+    setTimeout(async () => { await loadBooks(); generatingVideo.value[book.id] = false; }, 5000);
   } catch (e: any) {
+    generatingVideo.value[book.id] = false;
     uni.showToast({ title: e.message || '生成失败', icon: 'none' });
-  } finally {
-    generating.value = false;
   }
 }
 
-// 一键生成整本书(大纲+章节+前言+后记)- 异步模式
-let pollTimer: ReturnType<typeof setInterval> | null = null;
-
-async function handleGenerateAll() {
-  if (!currentBook.value) return;
-
-  generating.value = true;
-  currentBook.value.status = 'generating';
-  currentBook.value.progress = 0;
-
+async function handleTogglePublish(book: any) {
+  togglingPublish.value[book.id] = true;
   try {
-    // 启动异步生成任务(立即返回)
-    await api.generateBook(currentBook.value.id, {
-      generateForeword: true,
-      generateAfterword: true,
-    });
-
-    // 显示提示
-    uni.showToast({ title: '生成任务已启动,请稍候...', icon: 'none', duration: 2000 });
-
-    // 开始轮询进度
-    startPollingProgress(currentBook.value.id);
+    const res = await api.toggleBookPublish(book.id);
+    book.isPublished = res.isPublished;
+    uni.showToast({ title: book.isPublished ? '已公开到首页' : '已取消公开', icon: 'none' });
   } catch (e: any) {
-    generating.value = false;
-    currentBook.value.status = 'failed';
-    uni.showToast({ title: e.message || '启动失败', icon: 'none' });
+    uni.showToast({ title: e.message || '操作失败', icon: 'none' });
+  } finally {
+    togglingPublish.value[book.id] = false;
   }
 }
 
-// 轮询进度
-function startPollingProgress(bookId: string) {
-  // 停止之前的轮询
-  stopPollingProgress();
-
-  pollTimer = setInterval(async () => {
-    try {
-      // 直接轮询书籍进度,不依赖工作流
-      const progress = await api.getProgress(bookId);
-
-      if (!progress) {
-        return;
-      }
-
-      // 更新进度
-      if (currentBook.value) {
-        currentBook.value.progress = progress.progress;
-        currentBook.value.status = progress.status as any;
-      }
-
-      // 检查是否完成
-      if (progress.status === 'completed' || progress.status === 'failed') {
-        stopPollingProgress();
-        generating.value = false;
-
-        // 刷新书籍详情
-        currentBook.value = await api.getBook(bookId);
+onMounted(() => {
+  loadBooks();
+});
 
-        if (progress.status === 'completed') {
-          uni.showToast({ title: '整本书生成完成!', icon: 'success' });
-        } else if (progress.status === 'failed') {
-          uni.showToast({ title: '生成过程中有错误', icon: 'none' });
-        }
-      }
-    } catch (e) {
-      console.error('轮询进度失败:', e);
-    }
-  }, 3000); // 每3秒轮询一次
-}
+onShow(() => {
+  loadBooks();
+});
+</script>
 
-// 停止轮询
-function stopPollingProgress() {
-  if (pollTimer) {
-    clearInterval(pollTimer);
-    pollTimer = null;
-  }
-}
-
-// LangGraph 生成
-async function handleLangGraphGenerate() {
-  if (!currentBook.value) return;
-
-  generating.value = true;
-  currentBook.value.status = 'generating';
-  currentBook.value.progress = 0;
-
-  try {
-    const result = await api.generateWithLangGraph(currentBook.value.id);
-    uni.showToast({ title: 'LangGraph 生成已启动', icon: 'none', duration: 2000 });
-    startPollingProgress(currentBook.value.id);
-  } catch (e: any) {
-    generating.value = false;
-    currentBook.value.status = 'failed';
-    uni.showToast({ title: e.message || '启动失败', icon: 'none' });
-  }
-}
-
-// 生成全部章节
-async function handleGenerateAllChapters() {
-  if (!currentBook.value || !currentBook.value.outline) {
-    uni.showToast({ title: '请先生成大纲', icon: 'none' });
-    return;
-  }
-
-  generating.value = true;
-  currentBook.value.status = 'generating';
-
-  try {
-    for (const outlineChapter of currentBook.value.outline.chapters) {
-      try {
-        const chapter = await api.generateChapter(currentBook.value.id, outlineChapter.number);
-        // 更新章节
-        const idx = currentBook.value.chapters.findIndex((c) => c.number === chapter.number);
-        if (idx >= 0) {
-          currentBook.value.chapters[idx] = chapter;
-        }
-        // 更新进度
-        const completed = currentBook.value.chapters.filter((c) => c.status === 'completed').length;
-        currentBook.value.progress = Math.round((completed / currentBook.value.totalChapters) * 100);
-      } catch (e) {
-        console.error(`生成第${outlineChapter.number}章失败:`, e);
-      }
-    }
-
-    currentBook.value.status = 'completed';
-    uni.showToast({ title: '全部章节生成完成', icon: 'success' });
-  } catch (e: any) {
-    currentBook.value.status = 'failed';
-    uni.showToast({ title: e.message || '生成失败', icon: 'none' });
-  } finally {
-    generating.value = false;
-  }
-}
-
-// 生成前言
-async function handleGenerateForeword() {
-  if (!currentBook.value) return;
-
-  generating.value = true;
-  try {
-    const foreword = await api.generateForeword(currentBook.value.id);
-    currentBook.value.metadata = currentBook.value.metadata || {};
-    currentBook.value.metadata.foreword = foreword;
-    uni.showToast({ title: '前言生成成功', icon: 'success' });
-  } catch (e: any) {
-    uni.showToast({ title: e.message || '生成失败', icon: 'none' });
-  } finally {
-    generating.value = false;
-  }
-}
-
-// 生成后记
-async function handleGenerateAfterword() {
-  if (!currentBook.value) return;
-
-  generating.value = true;
-  try {
-    const afterword = await api.generateAfterword(currentBook.value.id);
-    currentBook.value.metadata = currentBook.value.metadata || {};
-    currentBook.value.metadata.afterword = afterword;
-    uni.showToast({ title: '后记生成成功', icon: 'success' });
-  } catch (e: any) {
-    uni.showToast({ title: e.message || '生成失败', icon: 'none' });
-  } finally {
-    generating.value = false;
-  }
-}
-
-// 跳转到章节详情页
-function goToChapterDetail(chapter: Chapter, sectionIndex?: number, subsectionIndex?: number) {
-  if (!currentBook.value) return;
-  // 跳转到独立的章节详情页面,使用数据库中的真实ID
-  uni.navigateTo({
-    url: `/pages/book-generator/chapter-detail?bookId=${encodeURIComponent(currentBook.value.id)}&chapter=${chapter.id}`
-  });
-}
-
-// 查看前言
-function viewForeword() {
-  uni.showModal({
-    title: '前言',
-    content: currentBook.value?.metadata?.foreword || '',
-    showCancel: true,
-    cancelText: '关闭',
-    confirmText: '复制',
-    success: (res) => {
-      if (res.confirm) {
-        uni.setClipboardData({
-          data: currentBook.value?.metadata?.foreword || '',
-          success: () => {
-            uni.showToast({ title: '已复制', icon: 'success' });
-          }
-        });
-      }
-    }
-  });
-}
-
-// 查看后记
-function viewAfterword() {
-  uni.showModal({
-    title: '后记',
-    content: currentBook.value?.metadata?.afterword || '',
-    showCancel: true,
-    cancelText: '关闭',
-    confirmText: '复制',
-    success: (res) => {
-      if (res.confirm) {
-        uni.setClipboardData({
-          data: currentBook.value?.metadata?.afterword || '',
-          success: () => {
-            uni.showToast({ title: '已复制', icon: 'success' });
-          }
-        });
-      }
-    }
-  });
-}
-
-// ============ 音频生成 ============
-
-/**
- * 生成单个章节音频
- */
-async function handleGenerateChapterAudio(chapter: Chapter) {
-  if (!currentBook.value) return;
-
-  generatingAudio.value = true;
-  try {
-    await api.generateChapterAudio(currentBook.value.id, chapter.number, 'cherry');
-    uni.showToast({ title: '音频生成任务已启动', icon: 'none', duration: 2000 });
-
-    // 3秒后刷新书籍信息,检查音频是否生成完成
-    setTimeout(async () => {
-      if (currentBook.value) {
-        currentBook.value = await api.getBook(currentBook.value.id);
-      }
-    }, 3000);
-  } catch (e: any) {
-    uni.showToast({ title: e.message || '生成失败', icon: 'none' });
-  } finally {
-    generatingAudio.value = false;
-  }
-}
-
-/**
- * 批量生成书籍所有章节音频
- */
-async function handleGenerateAllAudio(book: Book) {
-  generatingAudio.value = true;
-  try {
-    const result = await api.generateAllChaptersAudio(book.id, 'cherry');
-    uni.showToast({
-      title: `已启动 ${result.totalChapters} 个章节的音频生成`,
-      icon: 'none',
-      duration: 2500,
-    });
-
-    // 5秒后刷新书籍信息
-    setTimeout(async () => {
-      await loadBooks();
-      if (currentBook.value?.id === book.id) {
-        currentBook.value = await api.getBook(book.id);
-      }
-    }, 5000);
-  } catch (e: any) {
-    uni.showToast({ title: e.message || '生成失败', icon: 'none' });
-  } finally {
-    generatingAudio.value = false;
-  }
-}
-
-// ============ 视频生成 ============
-
-/**
- * 生成单个章节视频
- */
-async function handleGenerateChapterVideo(chapter: Chapter) {
-  if (!currentBook.value) return;
-
-  generatingVideo.value = true;
-  try {
-    await api.generateChapterVideo(currentBook.value.id, chapter.number);
-    uni.showToast({ title: '视频生成任务已启动', icon: 'none', duration: 2000 });
-
-    // 3秒后刷新书籍信息,检查视频是否生成完成
-    setTimeout(async () => {
-      if (currentBook.value) {
-        currentBook.value = await api.getBook(currentBook.value.id);
-      }
-    }, 3000);
-  } catch (e: any) {
-    uni.showToast({ title: e.message || '生成失败', icon: 'none' });
-  } finally {
-    generatingVideo.value = false;
-  }
-}
-
-/**
- * 批量生成书籍所有章节视频
- */
-async function handleGenerateAllVideo(book: Book) {
-  generatingVideo.value = true;
-  try {
-    const result = await api.generateAllChaptersVideo(book.id);
-    uni.showToast({
-      title: `已启动 ${result.totalChapters} 个章节的视频生成`,
-      icon: 'none',
-      duration: 2500,
-    });
-
-    // 5秒后刷新书籍信息
-    setTimeout(async () => {
-      await loadBooks();
-      if (currentBook.value?.id === book.id) {
-        currentBook.value = await api.getBook(book.id);
-      }
-    }, 5000);
-  } catch (e: any) {
-    uni.showToast({ title: e.message || '生成失败', icon: 'none' });
-  } finally {
-    generatingVideo.value = false;
-  }
-}
-
-// 切换书籍公开状态
-async function handleTogglePublish(book: any) {
-  togglingPublish.value[book.id] = true;
-  try {
-    const res = await api.toggleBookPublish(book.id);
-    book.isPublished = res.isPublished;
-    uni.showToast({
-      title: book.isPublished ? '已公开到首页' : '已取消公开',
-      icon: 'none'
-    });
-  } catch (e: any) {
-    uni.showToast({ title: e.message || '操作失败', icon: 'none' });
-  } finally {
-    togglingPublish.value[book.id] = false;
-  }
-}
-
-// 跳转到发布页面
-function goToPublishBook(book: Book) {
-  // 检查是否有音频或视频
-  const hasAudio = book.chapters?.some(c => c.audioUrl);
-  const hasVideo = book.chapters?.some(c => c.videoUrl);
-  
-  if (!hasAudio && !hasVideo) {
-    uni.showToast({ 
-      title: '请先生成音频或视频', 
-      icon: 'none',
-      duration: 2000
-    });
-    return;
-  }
-  
-  // 打开书籍详情页,让用户选择要发布的章节
-  openBook(book);
-  uni.showToast({ 
-    title: '请在章节详情中点击发布', 
-    icon: 'none',
-    duration: 2000
-  });
-}
-
-// 页面加载
-onMounted(() => {
-  loadBooks();
-  
-  // 连接WebSocket接收实时通知
-  try {
-    const token = uni.getStorageSync('token');
-    if (token) {
-      wsService.connect(token);
-      
-      // 监听书籍生成通知
-      wsService.on('book_generation_notification', (data: any) => {
-        console.log('[书籍生成通知]', data);
-        
-        // 如果是当前正在查看的书籍,更新显示
-        if (currentBook.value && data.bookId === currentBook.value.id.toString()) {
-          // 显示通知消息
-          if (data.message) {
-            uni.showToast({
-              title: data.message,
-              icon: 'none',
-              duration: 3000
-            });
-          }
-          
-          // 刷新书籍信息
-          openBook(currentBook.value);
-        }
-      });
-    }
-  } catch (error) {
-    console.error('[WebSocket] 初始化失败:', error);
-  }
-});
-</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);
-}
-
-.nav-left,
-.nav-right {
-  width: 80rpx;
-}
-
-.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);
-}
-
-.card-header {
-  margin-bottom: 24rpx;
-}
-
-.card-header-row {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  margin-bottom: 24rpx;
-}
-
-.card-title {
-  font-size: 32rpx;
-  font-weight: 600;
-  color: #1f2937;
-}
-
-/* 创建卡片 */
-.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;
-}
-
-/* 视频导航卡片 */
-.video-nav-card {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
-  border-radius: 24rpx;
-  padding: 36rpx;
-  margin-bottom: 24rpx;
-}
-
-.video-nav-content {
-  display: flex;
-  align-items: center;
-  gap: 24rpx;
-}
-
-.video-nav-icon {
-  font-size: 56rpx;
-}
-
-.video-nav-text {
-  display: flex;
-  flex-direction: column;
-  gap: 6rpx;
-}
-
-.video-nav-title {
-  font-size: 30rpx;
-  font-weight: 600;
-  color: #ffffff;
-}
-
-.video-nav-desc {
-  font-size: 22rpx;
-  color: rgba(255, 255, 255, 0.8);
-}
-
-.video-nav-arrow {
-  font-size: 40rpx;
-  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;
-}
-
-.status-badge {
-  font-size: 22rpx;
-  padding: 6rpx 16rpx;
-  border-radius: 20rpx;
-  background: #e5e7eb;
-  color: #6b7280;
-}
-
-.status-badge.planning,
-.status-badge.generating {
-  background: #fef3c7;
-  color: #d97706;
-}
-
-.status-badge.completed {
-  background: #d1fae5;
-  color: #059669;
-}
-
-.status-badge.failed {
-  background: #fee2e2;
-  color: #dc2626;
-}
-
-.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;
-}
-
-/* 空状态 */
-.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;
-}
-
-/* 表单 */
-.form-item {
-  margin-bottom: 28rpx;
-}
-
-/* 重要表单项 */
-.highlight-item {
-  background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
-  padding: 24rpx;
-  border-radius: 16rpx;
-  margin-bottom: 28rpx;
-  border: 2rpx solid #bae6fd;
-}
-
-.label-with-badge {
-  display: flex;
-  align-items: center;
-  gap: 12rpx;
-  margin-bottom: 12rpx;
-}
-
-.required-badge {
-  background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
-  color: white;
-  font-size: 20rpx;
-  padding: 4rpx 12rpx;
-  border-radius: 8rpx;
-  font-weight: bold;
-}
-
-.form-hint {
-  display: block;
-  font-size: 24rpx;
-  color: #64748b;
-  margin-bottom: 16rpx;
-  line-height: 1.5;
-}
-
-.form-hint-small {
-  display: block;
-  font-size: 22rpx;
-  color: #94a3b8;
-  margin-bottom: 12rpx;
-  line-height: 1.4;
-}
-
-/* 重要提示卡片 */
-.important-tip {
-  background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
-  padding: 24rpx;
-  border-radius: 16rpx;
-  margin-bottom: 28rpx;
-  border: 2rpx solid #f59e0b;
-}
-
-.tip-header {
-  display: flex;
-  align-items: center;
-  gap: 12rpx;
-  margin-bottom: 12rpx;
-}
-
-.tip-icon {
-  font-size: 32rpx;
-}
-
-.tip-title {
-  font-size: 28rpx;
-  font-weight: bold;
-  color: #92400e;
-}
-
-.tip-content {
-  display: block;
-  font-size: 24rpx;
-  color: #78350f;
-  line-height: 1.6;
-  margin-bottom: 12rpx;
-}
-
-.tip-examples {
-  display: flex;
-  flex-direction: column;
-  gap: 8rpx;
-}
-
-.tip-example {
-  display: block;
-  font-size: 22rpx;
-  color: #92400e;
-  line-height: 1.5;
-  padding-left: 16rpx;
-}
-
-/* 知识难度和面向人群的chip样式 */
-.level-chip,
-.audience-chip {
-  display: flex;
-  align-items: center;
-  gap: 8rpx;
-  padding: 12rpx 20rpx;
-}
-
-/* 特殊要求chip样式(多选) */
-.feature-chip.active {
-  background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
-  border-color: #2563eb;
-  color: white;
-}
-
-.chip-icon {
-  font-size: 28rpx;
-}
-
-.chip-text {
-  font-size: 26rpx;
-}
-
-/* 快速模板网格 */
-.template-grid {
-  display: grid;
-  grid-template-columns: repeat(2, 1fr);
-  gap: 16rpx;
-  margin-top: 16rpx;
-}
-
-.template-card {
-  background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
-  border: 2rpx solid #e2e8f0;
-  border-radius: 16rpx;
-  padding: 20rpx;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  gap: 8rpx;
-  transition: all 0.3s;
-}
-
-.template-card:active {
-  transform: scale(0.95);
-  background: linear-gradient(135deg, #e0f2fe 0%, #bae6fd 100%);
-  border-color: #3b82f6;
-}
-
-.template-icon {
-  font-size: 48rpx;
-  margin-bottom: 4rpx;
-}
-
-.template-name {
-  font-size: 28rpx;
-  font-weight: bold;
-  color: #1e293b;
-}
-
-.template-desc {
-  font-size: 22rpx;
-  color: #64748b;
-  text-align: center;
-  line-height: 1.4;
-}
-
-.form-label {
-  display: block;
-  font-size: 28rpx;
-  color: #374151;
-  margin-bottom: 12rpx;
-}
-
-.form-input {
-  width: 100%;
-  height: 88rpx;
-  padding: 0 24rpx;
-  background: #f9fafb;
-  border-radius: 16rpx;
-  font-size: 28rpx;
-  box-sizing: border-box;
-}
-
-.form-textarea {
-  width: 100%;
-  height: 200rpx;
-  padding: 24rpx;
-  background: #f9fafb;
-  border-radius: 16rpx;
-  font-size: 28rpx;
-  box-sizing: border-box;
-}
-
-.slider-range {
-  display: flex;
-  justify-content: space-between;
-  font-size: 24rpx;
-  color: #9ca3af;
-  margin-top: 8rpx;
-}
-
-.scale-picker {
-  display: flex;
-  flex-direction: column;
-  gap: 12rpx;
-  margin-top: 12rpx;
-}
-
-.scale-option {
-  display: flex;
-  align-items: center;
-  padding: 20rpx 16rpx;
-  border: 2rpx solid #e5e7eb;
-  border-radius: 12rpx;
-  background: #f9fafb;
-  transition: all 0.2s;
-}
-
-.scale-option.active {
-  border-color: #4F46E5;
-  background: #eef2ff;
-}
-
-.scale-label {
-  font-size: 28rpx;
-  font-weight: 600;
-  color: #374151;
-  min-width: 160rpx;
-  margin-left: 24rpx;
-}
-
-.scale-option.active .scale-label {
-  color: #4F46E5;
-}
-
-.scale-words {
-  font-size: 28rpx;
-  color: #374151;
-  min-width: 160rpx;
-  font-weight: 500;
-}
-
-.scale-pages {
-  font-size: 22rpx;
-  color: #6b7280;
-  flex: 1;
-  text-align: right;
-}
-
-/* 预估信息卡片 */
-.estimate-card {
-  background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
-  border-radius: 16rpx;
-  padding: 24rpx;
-  margin-top: 20rpx;
-  border: 1px solid #fcd34d;
-}
-
-.estimate-header {
-  margin-bottom: 16rpx;
-}
-
-.estimate-title {
-  font-size: 28rpx;
-  font-weight: 600;
-  color: #92400e;
-}
-
-.estimate-row {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  padding: 8rpx 0;
-}
-
-.estimate-label {
-  font-size: 26rpx;
-  color: #78350f;
-}
-
-.estimate-value {
-  font-size: 26rpx;
-  font-weight: 600;
-  color: #92400e;
-}
-
-/* 配额检查结果 */
-.quota-check {
-  margin-top: 16rpx;
-  padding-top: 16rpx;
-  border-top: 1px dashed #fcd34d;
-}
-
-.quota-ok, .quota-warning {
-  display: flex;
-  align-items: center;
-  gap: 12rpx;
-  padding: 12rpx 16rpx;
-  border-radius: 12rpx;
-  margin-bottom: 12rpx;
-}
-
-.quota-ok {
-  background: rgba(16, 185, 129, 0.1);
-}
-
-.quota-warning {
-  background: rgba(245, 158, 11, 0.1);
-}
-
-.quota-icon {
-  font-size: 32rpx;
-}
-
-.quota-text {
-  font-size: 26rpx;
-  color: #374151;
-}
-
-.quota-detail {
-  padding: 8rpx 0;
-}
-
-.quota-info {
-  font-size: 24rpx;
-  color: #6b7280;
-}
-
-.quota-overage {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-top: 8rpx;
-  padding: 8rpx 12rpx;
-  background: rgba(239, 68, 68, 0.1);
-  border-radius: 8rpx;
-}
-
-.quota-overage text {
-  font-size: 24rpx;
-  color: #dc2626;
-}
-
-.quota-price {
-  font-weight: 600;
-}
-
-/* 标签组 */
-.chip-group {
-  display: flex;
-  flex-wrap: wrap;
-  gap: 16rpx;
-}
-
-.chip {
-  padding: 12rpx 24rpx;
-  background: #f3f4f6;
-  border-radius: 30rpx;
-  font-size: 26rpx;
-  color: #6b7280;
-}
-
-.chip.active {
-  background: #4f46e5;
-  color: #ffffff;
-}
-
-/* 按钮组 */
-.btn-group {
-  display: flex;
-  gap: 20rpx;
-  margin-top: 32rpx;
-}
-
-.btn-cancel,
-.btn-primary {
-  flex: 1;
-  height: 88rpx;
-  border-radius: 16rpx;
-  font-size: 28rpx;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  border: none;
-}
-
-.btn-cancel {
-  background: #f3f4f6;
-  color: #6b7280;
-}
-
-.btn-primary {
-  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-  color: #ffffff;
-}
-
-.btn-primary[disabled] {
-  opacity: 0.6;
-}
-
-/* 详情页 */
-.book-info {
-  margin-bottom: 24rpx;
-}
-
-.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;
-}
-
-.book-meta-row {
-  display: flex;
-  gap: 24rpx;
-  font-size: 26rpx;
-  color: #9ca3af;
-  margin-top: 16rpx;
-}
-
-/* 进度卡片 */
-.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-detail {
-  font-size: 28rpx;
-  color: #6b7280;
-}
-
-.progress-bar-large {
-  height: 12rpx;
-  background: #e5e7eb;
-  border-radius: 6rpx;
-}
-
-/* 实时生成状态面板 */
-.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;
-}
-
-.status-icon {
-  font-size: 36rpx;
-  margin-right: 12rpx;
-  animation: pulse 2s ease-in-out infinite;
-}
-
-@keyframes pulse {
-  0%, 100% { opacity: 1; }
-  50% { opacity: 0.5; }
-}
-
-.status-title {
-  font-size: 32rpx;
-  font-weight: 600;
-}
-
-.status-stage {
-  display: flex;
-  align-items: center;
-  padding: 16rpx;
-  background: rgba(255, 255, 255, 0.2);
-  border-radius: 12rpx;
-  margin-bottom: 16rpx;
-}
-
-.stage-label {
-  font-size: 26rpx;
-  opacity: 0.9;
-  margin-right: 8rpx;
-}
-
-.stage-value {
-  font-size: 28rpx;
-  font-weight: 600;
-}
-
-.status-error {
-  display: flex;
-  align-items: flex-start;
-  padding: 16rpx;
-  background: rgba(239, 68, 68, 0.3);
-  border-radius: 12rpx;
-  margin-bottom: 16rpx;
-}
-
-.error-icon {
-  font-size: 28rpx;
-  margin-right: 12rpx;
-  flex-shrink: 0;
-}
-
-.error-text {
-  font-size: 24rpx;
-  line-height: 1.6;
-  flex: 1;
-}
-
-.status-recovery {
-  display: flex;
-  align-items: center;
-  padding: 16rpx;
-  background: rgba(251, 191, 36, 0.3);
-  border-radius: 12rpx;
-  margin-bottom: 16rpx;
-}
-
-.recovery-icon {
-  font-size: 32rpx;
-  margin-right: 12rpx;
-  animation: spin 1s linear infinite;
-}
-
-@keyframes spin {
-  from { transform: rotate(0deg); }
-  to { transform: rotate(360deg); }
-}
-
-.recovery-text {
-  font-size: 26rpx;
-  font-weight: 500;
-}
-
-.status-tip {
-  padding: 12rpx;
-  background: rgba(255, 255, 255, 0.1);
-  border-radius: 8rpx;
-}
-
-.tip-text {
-  font-size: 24rpx;
-  opacity: 0.9;
-}
-
-/* 失败提示 */
-.failed-tip {
-  margin-top: 24rpx;
-  padding: 24rpx;
-  background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%);
-  border-radius: 16rpx;
-  border: 2rpx solid #fca5a5;
-}
-
-.failed-icon {
-  font-size: 40rpx;
-  display: block;
-  text-align: center;
-  margin-bottom: 12rpx;
-}
-
-.failed-text {
-  font-size: 32rpx;
-  font-weight: 600;
-  color: #dc2626;
-  display: block;
-  text-align: center;
-  margin-bottom: 12rpx;
-}
-
-.failed-error {
-  font-size: 26rpx;
-  color: #991b1b;
-  display: block;
-  padding: 16rpx;
-  background: rgba(255, 255, 255, 0.5);
-  border-radius: 8rpx;
-  margin-bottom: 12rpx;
-  line-height: 1.6;
-}
-
-.failed-hint {
-  font-size: 24rpx;
-  color: #6b7280;
-  display: block;
-  text-align: center;
-  margin-bottom: 16rpx;
-}
-
-.retry-btn {
-  width: 100%;
-  height: 72rpx;
-  background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
-  color: white;
-  border: none;
-  border-radius: 12rpx;
-  font-size: 28rpx;
-  font-weight: 600;
-}
-
-.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;
-}
-
-.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;
-}
-
-@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;
-}
-
-/* 节状态 */
-.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.status-completed {
-  background: #d1fae5;
-  color: #059669;
-}
-
-.section-status.status-pending {
-  background: #f3f4f6;
-  color: #9ca3af;
-}
-
-.section-status.status-generating {
-  background: #fef3c7;
-  color: #d97706;
-  animation: pulse-bg 2s ease-in-out infinite;
-}
-
-.section-status.status-failed {
-  background: #fee2e2;
-  color: #dc2626;
-}
-
-/* 小节状态 */
-.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.status-completed {
-  background: #d1fae5;
-  color: #059669;
-}
-
-.subsection-status.status-pending {
-  background: #f3f4f6;
-  color: #9ca3af;
-}
-
-.subsection-status.status-generating {
-  background: #fef3c7;
-  color: #d97706;
-  animation: pulse-bg 2s ease-in-out infinite;
-}
-
-.subsection-status.status-failed {
-  background: #fee2e2;
-  color: #dc2626;
-}
-
-/* 内容预览 */
-.subsection-content-preview {
-  font-size: 22rpx;
-  color: #6b7280;
-  margin-top: 4rpx;
-  display: -webkit-box;
-  -webkit-line-clamp: 2;
-  -webkit-box-orient: vertical;
-  overflow: hidden;
-}
-
-/* 大纲卡片 */
-.outline-theme {
-  font-size: 24rpx;
-  color: #6b7280;
-}
-
-.outline-list {
-  margin-top: 20rpx;
-}
-
-.outline-item {
-  display: flex;
-  align-items: flex-start;
-  gap: 20rpx;
-  padding: 20rpx 0;
-  border-bottom: 1px solid #f3f4f6;
-}
-
-.outline-item:last-child {
-  border-bottom: none;
-}
-
-.chapter-num {
-  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;
-}
-
-.chapter-info {
-  flex: 1;
-}
-
-.chapter-title {
-  font-size: 28rpx;
-  font-weight: 600;
-  color: #1f2937;
-  margin-bottom: 6rpx;
-  display: block;
-}
-
-.chapter-summary {
-  font-size: 24rpx;
-  color: #6b7280;
-  display: block;
-  margin-bottom: 10rpx;
-}
-
-.chapter-points {
-  display: flex;
-  flex-wrap: wrap;
-  gap: 8rpx;
-}
-
-.point-tag {
-  font-size: 20rpx;
-  padding: 4rpx 12rpx;
-  background: #f3f4f6;
-  color: #6b7280;
-  border-radius: 6rpx;
-}
-
-.chapter-status {
-  font-size: 28rpx;
-  color: #9ca3af;
-  flex-shrink: 0;
-}
-
-.chapter-status.done {
-  color: #059669;
-}
-
-/* 三级大纲样式 */
-.outline-chapter {
-  flex-direction: column;
-  align-items: stretch;
-  padding: 16rpx 0;
-  border-bottom: 2rpx solid #e5e7eb;
-}
-
-.outline-row {
-  display: flex;
-  align-items: flex-start;
-  gap: 16rpx;
-}
-
-.chapter-row {
-  width: 100%;
-}
-
-.sections-list {
-  margin-left: 64rpx;
-  margin-top: 8rpx;
-  padding-left: 20rpx;
-  border-left: 2rpx solid #e5e7eb;
-}
-
-.outline-section {
-  flex-direction: column;
-  align-items: stretch;
-  padding: 12rpx 0;
-  border-bottom: 1px dashed #f3f4f6;
-}
-
-.section-num {
-  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-info {
-  flex: 1;
-}
-
-.section-title {
-  font-size: 26rpx;
-  font-weight: 500;
-  color: #374151;
-  margin-bottom: 4rpx;
-  display: block;
-}
-
-.section-summary {
-  font-size: 22rpx;
-  color: #9ca3af;
-  display: block;
-}
-
-.subsections-list {
-  margin-left: 56rpx;
-  margin-top: 6rpx;
-}
-
-.outline-subsection {
-  padding: 8rpx 0;
-  border-bottom: none;
-}
-
-.subsection-num {
-  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-info {
-  flex: 1;
-}
-
-.subsection-title {
-  font-size: 24rpx;
-  color: #6b7280;
-  display: block;
-}
-
-/* 操作卡片 */
-.action-card {
-  margin-bottom: 24rpx;
-}
-
-.btn-row {
-  display: flex;
-  gap: 16rpx;
-  margin-bottom: 20rpx;
-}
-
-.action-btn {
-  flex: 1;
-  height: 88rpx;
-  background: #f3f4f6;
-  border-radius: 16rpx;
-  font-size: 28rpx;
-  color: #374151;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  border: none;
-}
-
-.action-btn.primary {
-  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-  color: #ffffff;
-}
-
-.action-btn.full-width {
-  width: 100%;
-  flex: none;
-  margin-bottom: 16rpx;
-}
-
-.action-btn[disabled] {
-  opacity: 0.6;
-}
-
-.generating-tip {
-  text-align: center;
-  padding: 20rpx;
-  color: #6b7280;
-  font-size: 26rpx;
-}
-
-/* LangGraph 按钮 */
-.action-btn.langgraph-btn {
-  background: linear-gradient(135deg, #10b981 0%, #059669 100%);
-  color: #ffffff;
-}
-
-
-/* 全屏弹窗 */
-.modal-overlay {
-  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;
-}
-
-.content-modal {
-  width: 100%;
-  height: 90vh;
-  background: #ffffff;
-  border-radius: 32rpx 32rpx 0 0;
-  display: flex;
-  flex-direction: column;
-}
-
-.modal-header {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  padding: 32rpx;
-  border-bottom: 1px solid #f3f4f6;
-  flex-shrink: 0;
-}
-
-.modal-title {
-  font-size: 32rpx;
-  font-weight: 600;
-  color: #1f2937;
-}
-
-.modal-close {
-  font-size: 40rpx;
-  color: #9ca3af;
-  padding: 8rpx;
-}
-
-.content-scroll {
-  flex: 1;
-  padding: 32rpx;
-}
-
-.content-body {
-  font-size: 28rpx;
-  line-height: 1.8;
-  color: #374151;
-}
-
-/* ==================== 目录视图 ==================== */
-.toc-view {
-  position: fixed;
-  top: 0;
-  left: 0;
-  right: 0;
-  bottom: 0;
-  background: #f9fafb;
-  z-index: 200;
-  display: flex;
-  flex-direction: column;
-}
-
-.toc-view .nav-bar {
-  position: relative;
-  flex-shrink: 0;
-}
-
-.toc-content {
-  flex: 1;
-  padding: 24rpx 32rpx;
-  padding-top: calc(120rpx + env(safe-area-inset-top));
-  overflow-y: auto;
-}
-
-.toc-item {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  background: #ffffff;
-  border-radius: 16rpx;
-  padding: 28rpx 32rpx;
-  margin-bottom: 16rpx;
-  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
-}
-
-.toc-item.foreword,
-.toc-item.afterword {
-  background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
-}
-
-.toc-item.foreword .toc-label,
-.toc-item.afterword .toc-label {
-  font-size: 30rpx;
-  font-weight: 600;
-  color: #92400e;
-}
-
-.toc-item.chapter {
-  cursor: pointer;
-}
-
-.toc-item.chapter.pending {
-  opacity: 0.6;
-}
-
-.toc-item.chapter.done {
-  background: #ffffff;
-}
-
-.toc-left {
-  display: flex;
-  align-items: center;
-  gap: 20rpx;
-  flex: 1;
-}
-
-.toc-left .chapter-num {
-  width: 56rpx;
-  height: 56rpx;
-  background: #4f46e5;
-  color: #ffffff;
-  border-radius: 50%;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  font-size: 24rpx;
-  font-weight: 600;
-  flex-shrink: 0;
-}
-
-.toc-item.chapter.pending .chapter-num {
-  background: #9ca3af;
-}
-
-.toc-left .chapter-title {
-  font-size: 28rpx;
-  font-weight: 500;
-  color: #1f2937;
-}
-
-.toc-arrow {
-  font-size: 32rpx;
-  color: #9ca3af;
-}
-
-/* ==================== 章节详情视图 ==================== */
-.chapter-detail-view {
-  position: fixed;
-  top: 0;
-  left: 0;
-  right: 0;
-  bottom: 0;
-  background: #f9fafb;
-  z-index: 200;
-  display: flex;
-  flex-direction: column;
-}
-
-.chapter-detail-view .nav-bar {
-  position: relative;
-  flex-shrink: 0;
-}
-
-.chapter-content {
-  flex: 1;
-  padding-top: calc(88rpx + env(safe-area-inset-top));
-  overflow: hidden;
-}
-
-.chapter-scroll {
-  height: 100%;
-  padding: 32rpx;
-}
-
-.chapter-header {
-  margin-bottom: 32rpx;
-  padding-bottom: 24rpx;
-  border-bottom: 1px solid #e5e7eb;
-}
-
-.chapter-title-large {
-  display: block;
-  font-size: 36rpx;
-  font-weight: 700;
-  color: #1f2937;
-  margin-bottom: 12rpx;
-}
-
-.chapter-word-count {
-  font-size: 24rpx;
-  color: #9ca3af;
-}
-
-.chapter-body {
-  background: #ffffff;
-  border-radius: 16rpx;
-  padding: 32rpx;
-  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
-}
-
-.chapter-text {
-  font-size: 30rpx;
-  line-height: 1.9;
-  color: #374151;
-  text-align: justify;
-}
-
-/* ==================== 完整内容区样式 ==================== */
-.content-card {
-  margin-top: 0;
-}
-
-.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-header {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  flex: 1;
-  cursor: pointer;
-}
-
-.content-item-left {
-  display: flex;
-  align-items: center;
-  gap: 16rpx;
-  flex: 1;
-}
-
-.content-item-right {
-  flex-shrink: 0;
-  margin-left: 16rpx;
-}
-
-.content-label {
-  font-size: 30rpx;
-  font-weight: 600;
-  color: #92400e;
-}
-
-.content-item.foreword .content-label,
-.content-item.afterword .content-label {
-  color: #92400e;
-}
-
-.chapter-num-badge {
-  font-size: 24rpx;
-  font-weight: 600;
-  color: #ffffff;
-  background: #4f46e5;
-  padding: 6rpx 12rpx;
-  border-radius: 8rpx;
-}
-
-.chapter-title-short {
-  font-size: 28rpx;
-  font-weight: 500;
-  color: #1f2937;
-  flex: 1;
-}
-
-.audio-badge,
-.video-badge {
-  font-size: 24rpx;
-}
-
-.content-word-count {
-  font-size: 22rpx;
-  color: #9ca3af;
-}
-
-.expand-icon {
-  font-size: 24rpx;
-  color: #9ca3af;
-}
-
-/* 树形内容样式 */
-.content-tree-list {
-  margin-top: 20rpx;
-}
-
-.content-tree-item {
-  display: flex;
-  flex-direction: column;
-  padding: 16rpx 0;
-  border-bottom: 1px solid #f3f4f6;
-}
-
-.content-tree-item:last-child {
-  border-bottom: none;
-}
-
-.content-tree-row {
-  display: flex;
-  align-items: flex-start;
-  gap: 16rpx;
-}
-
-/* 章样式 */
-.content-chapter {
-  padding: 20rpx 0;
-  border-bottom: 2rpx solid #e5e7eb;
-}
-
-.chapter-row {
-  width: 100%;
-  cursor: pointer;
-  background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
-  padding: 16rpx;
-  border-radius: 12rpx;
-  border-left: 4rpx solid #3b82f6;
-}
-
-.chapter-info {
-  flex: 1;
-}
-
-.chapter-title {
-  font-size: 28rpx;
-  font-weight: 600;
-  color: #1f2937;
-  margin-bottom: 8rpx;
-  display: block;
-}
-
-.chapter-outline-summary {
-  font-size: 24rpx;
-  color: #6b7280;
-  margin-bottom: 10rpx;
-  display: block;
-  line-height: 1.5;
-}
-
-.chapter-key-points {
-  display: flex;
-  flex-wrap: wrap;
-  gap: 8rpx;
-  margin-bottom: 10rpx;
-}
-
-.point-tag {
-  font-size: 20rpx;
-  padding: 4rpx 12rpx;
-  background: #f3f4f6;
-  color: #6b7280;
-  border-radius: 6rpx;
-}
-
-.chapter-media-badges {
-  display: flex;
-  gap: 12rpx;
-  margin-top: 8rpx;
-}
-
-.media-badge {
-  font-size: 20rpx;
-  padding: 4rpx 12rpx;
-  border-radius: 6rpx;
-  background: #f3f4f6;
-  color: #6b7280;
-}
-
-.chapter-status {
-  font-size: 28rpx;
-  color: #9ca3af;
-  flex-shrink: 0;
-}
-
-.chapter-status.done {
-  color: #059669;
-}
-
-.chapter-status.pending {
-  color: #d1d5db;
-}
-
-/* 节列表 */
-
-/* 小节媒体标识 */
-.subsection-media-badges {
-  display: flex;
-  gap: 8rpx;
-  margin-top: 6rpx;
-}
-
-.media-badge-small {
-  font-size: 18rpx;
-  padding: 2rpx 8rpx;
-  border-radius: 4rpx;
-  background: rgba(79, 70, 229, 0.1);
-  color: #4f46e5;
-}
-.content-sections-list {
-  margin-left: 40rpx;
-  margin-top: 12rpx;
-  padding-left: 24rpx;
-  border-left: 3rpx solid #e5e7eb;
-  background: linear-gradient(to right, rgba(243, 244, 246, 0.3), transparent);
-}
-
-.content-section {
-  padding: 12rpx 0;
-  border-bottom: 1px dashed #f3f4f6;
-}
-
-.section-row {
-  width: 100%;
-  background: #fafafa;
-  padding: 12rpx;
-  border-radius: 8rpx;
-  border-left: 3rpx solid #10b981;
-}
-
-.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-info {
-  flex: 1;
-}
-
-.section-title {
-  font-size: 26rpx;
-  font-weight: 500;
-  color: #374151;
-  margin-bottom: 4rpx;
-  display: block;
-}
-
-.section-summary {
-  font-size: 22rpx;
-  color: #9ca3af;
-  display: block;
-}
-
-/* 小节列表 */
-.content-subsections-list {
-  margin-left: 32rpx;
-  margin-top: 8rpx;
-  padding-left: 20rpx;
-  border-left: 2rpx dashed #d1d5db;
-  background: linear-gradient(to right, rgba(209, 213, 219, 0.2), transparent);
-}
-
-.content-subsection {
-  padding: 8rpx 0;
-  border-bottom: none;
-}
-
-.subsection-row {
-  width: 100%;
-  padding: 10rpx;
-  border-radius: 6rpx;
-  border-left: 2rpx solid #8b5cf6;
-  transition: all 0.2s;
-}
-
-.subsection-row:active {
-  background: #f3f4f6;
-  transform: translateX(4rpx);
-}
-
-.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-info {
-  flex: 1;
-}
-
-.subsection-title {
-  font-size: 24rpx;
-  color: #6b7280;
-  display: block;
-}
-
-.content-body {
-  padding: 24rpx;
-  background: #ffffff;
-  border-radius: 12rpx;
-  margin-top: 12rpx;
-  border: 1px solid #e5e7eb;
-}
-
-.content-meta {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  margin-bottom: 16rpx;
-  padding-bottom: 16rpx;
-  border-bottom: 1px solid #f3f4f6;
-}
-
-.content-actions {
-  display: flex;
-  gap: 12rpx;
-}
-
-.mini-btn {
-  padding: 8rpx 16rpx;
-  border-radius: 8rpx;
-  font-size: 22rpx;
-  border: none;
-}
-
-.mini-btn.audio-btn {
-  background: linear-gradient(135deg, #10b981 0%, #059669 100%);
-  color: #ffffff;
-}
-
-.mini-btn.video-btn {
-  background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
-  color: #ffffff;
-}
-
-.content-text {
-  font-size: 28rpx;
-  line-height: 1.9;
-  color: #374151;
-  text-align: justify;
-  white-space: pre-wrap;
-}
-
-/* ==================== 音频生成相关样式 ==================== */
-
-/* 书籍卡片操作区 */
-.book-actions {
-  margin-top: 20rpx;
-  padding-top: 20rpx;
-  border-top: 1px solid #f3f4f6;
-}
-
-.audio-btn {
-  width: 100%;
-  height: 72rpx;
-  background: linear-gradient(135deg, #10b981 0%, #059669 100%);
-  border-radius: 12rpx;
-  font-size: 26rpx;
-  color: #ffffff;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  border: none;
-}
-
-.audio-btn[disabled] {
-  opacity: 0.6;
-}
-
-/* 视频按钮样式 */
-.video-btn {
-  width: 100%;
-  height: 72rpx;
-  background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
-  border-radius: 12rpx;
-  font-size: 26rpx;
-  color: #ffffff;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  border: none;
-  margin-top: 16rpx;
-}
-
-.video-btn[disabled] {
-  opacity: 0.6;
-}
-
-.publish-btn {
-  flex: 1;
-  height: 72rpx;
-  background: linear-gradient(135deg, #059669 0%, #047857 100%);
-  border-radius: 16rpx;
-  font-size: 24rpx;
-  color: #fff;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  border: none;
-  padding: 0;
-  margin-top: 8rpx;
-}
-
-.publish-btn[disabled] {
-  opacity: 0.6;
-}
-
-.publish-all-btn {
-  width: 100%;
-  height: 72rpx;
-  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-  border-radius: 12rpx;
-  font-size: 26rpx;
-  color: #ffffff;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  border: none;
-  margin-top: 16rpx;
-}
-
-/* 目录项操作区 */
-.toc-actions {
-  display: flex;
-  align-items: center;
-  gap: 16rpx;
-}
-
-.chapter-info-col {
-  display: flex;
-  flex-direction: column;
-  gap: 6rpx;
-}
-
-.audio-status {
-  font-size: 20rpx;
-  color: #059669;
-}
-
-.mini-audio-btn {
-  width: 64rpx;
-  height: 64rpx;
-  background: linear-gradient(135deg, #10b981 0%, #059669 100%);
-  border-radius: 50%;
-  font-size: 28rpx;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  border: none;
-  padding: 0;
-}
-
-.mini-audio-btn[disabled] {
-  opacity: 0.6;
-}
-
-.mini-video-btn {
-  width: 64rpx;
-  height: 64rpx;
-  background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
-  border-radius: 50%;
-  font-size: 28rpx;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  border: none;
-  padding: 0;
-}
-
-.mini-video-btn[disabled] {
-  opacity: 0.6;
-}
-
-.video-status {
-  font-size: 20rpx;
-  color: #f5576c;
-}
-
-/* 额度监控 */
-.quota-monitor {
-  margin-top: 20rpx;
-  padding: 16rpx;
-  background: rgba(16, 185, 129, 0.1);
-  border-radius: 12rpx;
-}
-
-.quota-monitor-title {
-  font-size: 24rpx;
-  font-weight: 600;
-  color: #059669;
-  margin-bottom: 8rpx;
-  display: block;
-}
-
-.quota-monitor-row {
-  display: flex;
-  justify-content: space-between;
-  font-size: 24rpx;
-  color: #374151;
-}
-
-.quota-overage-warning {
-  margin-top: 8rpx;
-  padding: 8rpx 12rpx;
-  background: rgba(239, 68, 68, 0.1);
-  border-radius: 8rpx;
-}
-
-.quota-overage-warning text {
-  font-size: 22rpx;
-  color: #dc2626;
-}
-
-/* 中断提示 */
-.interrupted-tip {
-  margin-top: 20rpx;
-  padding: 20rpx;
-  background: rgba(245, 158, 11, 0.1);
-  border: 1px solid #fbbf24;
-  border-radius: 12rpx;
-  display: flex;
-  flex-direction: column;
-  gap: 8rpx;
-}
-
-.interrupted-icon {
-  font-size: 36rpx;
-}
-
-.interrupted-text {
-  font-size: 26rpx;
-  color: #92400e;
-  font-weight: 600;
-}
-
-.interrupted-hint {
-  font-size: 24rpx;
-  color: #b45309;
-}
-
-/* 无章节警告 */
-.warning-tip {
-  margin-top: 20rpx;
-  padding: 24rpx;
-  background: rgba(239, 68, 68, 0.1);
-  border: 1px solid #fca5a5;
-  border-radius: 12rpx;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  gap: 12rpx;
-}
-
-.warning-icon {
-  font-size: 48rpx;
-}
-
-.warning-text {
-  font-size: 26rpx;
-  color: #dc2626;
-  text-align: center;
-  font-weight: 500;
-}
-
-.warning-hint {
-  font-size: 24rpx;
-  color: #991b1b;
-  margin-bottom: 8rpx;
-}
-
-.warning-tip .action-btn {
-  margin-top: 8rpx;
-  width: 100%;
-}
+<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); }
+.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; }
+.video-nav-card { display: flex; align-items: center; justify-content: space-between; background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); border-radius: 24rpx; padding: 36rpx; margin-bottom: 24rpx; }
+.video-nav-content { display: flex; align-items: center; gap: 24rpx; }
+.video-nav-icon { font-size: 56rpx; }
+.video-nav-text { display: flex; flex-direction: column; gap: 6rpx; }
+.video-nav-title { font-size: 30rpx; font-weight: 600; color: #ffffff; }
+.video-nav-desc { font-size: 22rpx; color: rgba(255, 255, 255, 0.8); }
+.video-nav-arrow { font-size: 40rpx; 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; }
+.status-badge { font-size: 22rpx; padding: 6rpx 16rpx; border-radius: 20rpx; background: #e5e7eb; color: #6b7280; }
+.status-badge.published { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #ffffff; font-weight: 600; }
+.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; flex-direction: column; gap: 12rpx; }
+.audio-btn, .merge-audio-btn, .merge-video-btn, .video-btn, .publish-btn { width: 100%; height: 72rpx; border-radius: 12rpx; font-size: 26rpx; display: flex; align-items: center; justify-content: center; border: none; }
+.audio-btn { background: linear-gradient(135deg, #10b981 0%, #059669 100%); color: #ffffff; }
+.merge-audio-btn { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #ffffff; }
+.merge-video-btn { background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); color: #ffffff; }
+.video-btn { background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); color: #ffffff; }
+.publish-btn { background: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%); color: #ffffff; }
+.audio-btn[disabled], .merge-audio-btn[disabled], .merge-video-btn[disabled], .video-btn[disabled], .publish-btn[disabled] { opacity: 0.6; }
+.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; }
 </style>
-
-

+ 12 - 2
my-uniapp-vue3/src/pages/playlists/detail.vue

@@ -8,7 +8,7 @@
       <view class="nav-btn" />
     </view>
 
-    <scroll-view scroll-y class="content">
+    <scroll-view scroll-y class="content" :scroll-top="scrollTop">
       <view v-if="!loading && playlist" class="playlist-header">
         <view class="playlist-cover">
           <text class="cover-icon">🎵</text>
@@ -55,10 +55,13 @@
 
 <script setup lang="ts">
 import { ref, onMounted } from 'vue';
-import { onLoad } from '@dcloudio/uni-app';
+import { onLoad, onShow } from '@dcloudio/uni-app';
 import { useAudioStore } from '../../store/audio';
 import { get, post, del } from '../../utils/request';
 
+// scroll-top 用于避免 scrollTop 错误
+const scrollTop = ref(0);
+
 const audioStore = useAudioStore();
 const playlistId = ref('');
 const playlist = ref<any>(null);
@@ -147,6 +150,13 @@ onLoad((options: any) => {
     fetchPlaylist();
   }
 });
+
+onShow(() => {
+  // H5 环境下 onLoad 可能不会再次触发,需要在 onShow 中检查并刷新
+  if (playlistId.value) {
+    fetchPlaylist();
+  }
+});
 </script>
 
 <style scoped>

+ 37 - 4
my-uniapp-vue3/src/pages/publish/index.vue

@@ -211,7 +211,7 @@
 
 <script setup lang="ts">
 import { ref, computed, onMounted } from 'vue';
-import { onLoad } from '@dcloudio/uni-app';
+import { onLoad, onShow } from '@dcloudio/uni-app';
 import {
   getPlatformAccounts,
   bindPlatformAccount,
@@ -486,7 +486,12 @@ const fullVideoUrl = computed(() => {
 });
 
 function goBack() {
-  uni.navigateBack();
+  const pages = getCurrentPages();
+  if (pages.length > 1) {
+    uni.navigateBack();
+  } else {
+    uni.switchTab({ url: '/pages/index/index' });
+  }
 }
 
 function selectPlatform(p: PlatformItem) {
@@ -798,14 +803,42 @@ onLoad(async (query: any) => {
     console.error('加载平台账号失败', e);
     // 如果是401错误,提示用户登录
     if (e.message && (e.message.includes('登录') || e.message.includes('401'))) {
-      uni.showToast({ 
-        title: '请先登录后再使用发布功能', 
+      uni.showToast({
+        title: '请先登录后再使用发布功能',
         icon: 'none',
         duration: 3000
       });
     }
   }
 });
+
+let initialLoadDone = false;
+onShow(() => {
+  // H5 环境下 onLoad 可能不会再次触发,但页面需要刷新数据
+  // 只有当有数据时才能刷新
+  if (initialLoadDone && (audioId.value || projectId.value)) {
+    // 重新加载页面数据
+    if (audioId.value) {
+      get<any>(`/history/${audioId.value}`).then(audioData => {
+        if (audioData) {
+          audioInfo.value = audioData;
+          publishTitle.value = audioData.title || '音频';
+          publishDesc.value = audioData.text?.substring(0, 200) || '';
+        }
+      }).catch(console.error);
+    }
+    if (projectId.value) {
+      getVideoPublishPreview(projectId.value).then(videoInfo => {
+        if (videoInfo) {
+          videoInfo.value = videoInfo;
+          publishTitle.value = videoInfo.title || '';
+          publishDesc.value = videoInfo.description || '';
+        }
+      }).catch(console.error);
+    }
+  }
+  initialLoadDone = true;
+});
 </script>
 
 <style scoped>

+ 21 - 4
my-uniapp-vue3/src/pages/video-generator/preview.vue

@@ -50,13 +50,20 @@
 
 <script setup lang="ts">
 import { ref, onMounted } from 'vue';
-import { onLoad } from '@dcloudio/uni-app';
+import { onLoad, onShow } from '@dcloudio/uni-app';
 import { getVideoProject, generateVideo, getGenerateProgress, type VideoProjectResponse } from '@/utils/video-generator-api';
 
 const baseUrl = ''; // 使用相对路径,依赖 Nginx 代理
 const project = ref<VideoProjectResponse | null>(null);
 
-function goBack() { uni.navigateBack(); }
+function goBack() {
+  const pages = getCurrentPages();
+  if (pages.length > 1) {
+    uni.navigateBack();
+  } else {
+    uni.switchTab({ url: '/pages/index/index' });
+  }
+}
 
 function formatDuration(seconds: number | null): string {
   if (!seconds) return '--:--';
@@ -159,11 +166,21 @@ async function regenerateVideo() {
   }
 }
 
+let projectIdLoaded = 0;
+
 onLoad((query: any) => {
-  if (query?.id) loadProject(Number(query.id));
+  if (query?.id) {
+    projectIdLoaded = Number(query.id);
+    loadProject(projectIdLoaded);
+  }
 });
 
-onMounted(() => {});
+onShow(() => {
+  // H5 环境下 onLoad 可能不会再次触发,需要在 onShow 中检查并刷新
+  if (projectIdLoaded && project.value?.id !== projectIdLoaded) {
+    loadProject(projectIdLoaded);
+  }
+});
 </script>
 
 <style scoped>