Przeglądaj źródła

fix: 修复编辑页面数据无法回填 + 书列表增加编辑入口

- 修复getBookForEdit/updateBook API路径错误(langgraph→book-generator)
- 修复getBookForEdit/getChapter双重.data解包导致undefined
- index.vue新增编辑图标按钮和更多菜单中的编辑选项

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
MyFramework User 3 miesięcy temu
rodzic
commit
43c5b488b1

+ 69 - 27
my-uniapp-vue3/src/pages/book-generator/index.vue

@@ -31,7 +31,10 @@
           >
             <view class="book-header" @click="openBook(book)">
               <text class="book-title">{{ book.title }}</text>
-              <GenerationStatusBadge :status="book.isPublished ? 'published' : (book.genStage || book.status || 'draft')"></GenerationStatusBadge>
+              <view class="header-right">
+                <text class="edit-icon" @click.stop="goToEdit(book)">✏️</text>
+                <GenerationStatusBadge :status="book.isPublished ? 'published' : (book.genStage || book.status || 'draft')"></GenerationStatusBadge>
+              </view>
             </view>
             <text class="book-desc" @click="openBook(book)">{{ book.description }}</text>
             <view class="book-meta" @click="openBook(book)">
@@ -212,6 +215,10 @@ async function openBook(book: Book) {
   uni.navigateTo({ url: `/pages/book-generator/detail?id=${book.id}` });
 }
 
+function goToEdit(book: Book) {
+  uni.navigateTo({ url: `/pages/book-generator/create?editId=${book.id}` });
+}
+
 // 辅助函数:清理指定书籍的轮询定时器
 function clearAudioPollTimer(bookId: string) {
   if (audioPollTimers.value[bookId]) {
@@ -303,34 +310,41 @@ async function handleMergeChapterVideo(book: Book) {
 
 async function handleGenerateAllVideo(book: Book) {
   if (generatingVideo.value[book.id]) return;
-  generatingVideo.value[book.id] = true;
-  // 清理可能存在的旧定时器
-  clearVideoPollTimer(book.id);
-  try {
-    const result = await api.generateAllChaptersVideo(book.id);
-    uni.showToast({ title: `已启动 ${result.totalChapters} 个章节的视频生成`, icon: 'none', duration: 2500 });
-    // 开始轮询视频生成状态
-    videoPollTimers.value[book.id] = setInterval(async () => {
+  uni.showModal({
+    title: '确认重新生成视频',
+    content: `确定要重新生成《${book.title}》的所有视频吗?这将清除现有视频并重新生成。`,
+    success: async (res) => {
+      if (!res.confirm) return;
+      generatingVideo.value[book.id] = true;
+      // 清理可能存在的旧定时器
+      clearVideoPollTimer(book.id);
       try {
-        const status = await api.getVideoStatus(book.id);
-        if (status.allCompleted) {
-          clearVideoPollTimer(book.id);
-          generatingVideo.value[book.id] = false;
-          await loadBooks();
-          uni.showToast({ title: '视频生成完成', icon: 'success' });
-          // 写入通知
-          const notifStore = useNotificationStore();
-          notifStore.add({ type: 'video_complete', title: '视频生成完成', message: `《${book.title}》视频已全部生成完毕`, bookId: String(book.id) });
-        }
-      } catch (e) {
-        console.error('轮询视频状态失败:', e);
+        const result = await api.generateAllChaptersVideo(book.id);
+        uni.showToast({ title: `已启动 ${result.totalChapters} 个章节的视频生成`, icon: 'none', duration: 2500 });
+        // 开始轮询视频生成状态
+        videoPollTimers.value[book.id] = setInterval(async () => {
+          try {
+            const status = await api.getVideoStatus(book.id);
+            if (status.allCompleted) {
+              clearVideoPollTimer(book.id);
+              generatingVideo.value[book.id] = false;
+              await loadBooks();
+              uni.showToast({ title: '视频生成完成', icon: 'success' });
+              // 写入通知
+              const notifStore = useNotificationStore();
+              notifStore.add({ type: 'video_complete', title: '视频生成完成', message: `《${book.title}》视频已全部生成完毕`, bookId: String(book.id) });
+            }
+          } catch (e) {
+            console.error('轮询视频状态失败:', e);
+          }
+        }, 5000);
+      } catch (e: any) {
+        clearVideoPollTimer(book.id);
+        generatingVideo.value[book.id] = false;
+        uni.showToast({ title: e.message || '生成失败', icon: 'none' });
       }
-    }, 5000);
-  } catch (e: any) {
-    clearVideoPollTimer(book.id);
-    generatingVideo.value[book.id] = false;
-    uni.showToast({ title: e.message || '生成失败', icon: 'none' });
-  }
+    },
+  });
 }
 
 async function handleTogglePublish(book: any) {
@@ -370,6 +384,10 @@ function showMoreActions(book: Book) {
   if (videoStatus.status === 'completed') {
     actions.push('🔄 重新生成视频');
   }
+  // 编辑书籍
+  actions.push('✏️ 编辑书籍');
+  // 删除书籍
+  actions.push('🗑 删除书籍');
 
   // 如果没有可用操作,提示用户
   if (actions.length === 0) {
@@ -393,6 +411,27 @@ function showMoreActions(book: Book) {
         handleGenerateAllVideo(book);
       } else if (action === '🔄 重新生成视频') {
         handleGenerateAllVideo(book);
+      } else if (action === '✏️ 编辑书籍') {
+        goToEdit(book);
+      } else if (action === '🗑 删除书籍') {
+        handleDeleteBook(book);
+      }
+    }
+  });
+}
+
+async function handleDeleteBook(book: Book) {
+  uni.showModal({
+    title: '确认删除',
+    content: `确定要删除《${book.title}》吗?此操作不可恢复。`,
+    success: async (res) => {
+      if (!res.confirm) return;
+      try {
+        await api.deleteBook(book.id);
+        uni.showToast({ title: '删除成功', icon: 'success' });
+        await loadBooks();
+      } catch (e: any) {
+        uni.showToast({ title: e.message || '删除失败', icon: 'none' });
       }
     }
   });
@@ -431,6 +470,9 @@ onUnmounted(() => {
 .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; }
+.header-right { display: flex; align-items: center; gap: 12rpx; flex-shrink: 0; }
+.edit-icon { font-size: 28rpx; padding: 4rpx; opacity: 0.5; }
+.edit-icon:active { opacity: 1; }
 /* 状态标签由 GenerationStatusBadge 组件统一管理 */
 .book-desc { font-size: 26rpx; color: #6b7280; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; margin-bottom: 16rpx; }
 .book-meta { display: flex; gap: 24rpx; font-size: 24rpx; color: #9ca3af; margin-bottom: 12rpx; }

+ 22 - 2
my-uniapp-vue3/src/utils/book-generator-api.ts

@@ -173,8 +173,7 @@ export async function getBook(id: string): Promise<Book> {
  * 获取单个章节详情(从专辑控制器获取,只返回单章节内容)
  */
 export async function getChapter(chapterId: string): Promise<any> {
-  const result = await request<any>(`/book-generator/albums/chapters/${chapterId}`);
-  return result.data;
+  return await request<any>(`/book-generator/albums/chapters/${chapterId}`);
 }
 
 /**
@@ -652,3 +651,24 @@ export function needsVideoGeneration(genStage?: string): boolean {
 export function isBookFullyCompleted(genStage?: string): boolean {
   return genStage === 'video_completed';
 }
+
+/**
+ * 获取书籍完整信息(用于编辑)
+ */
+export async function getBookForEdit(id: string): Promise<any> {
+  return await request<any>(`/book-generator/albums/${id}`);
+}
+
+/**
+ * 更新书籍基础信息
+ */
+export async function updateBook(id: string, data: {
+  title?: string;
+  description?: string;
+  coverUrl?: string;
+  targetAudience?: string;
+  style?: string;
+  bookScale?: string;
+}): Promise<{ id: number; title: string }> {
+  return post(`/book-generator/albums/${id}`, data);
+}