Quellcode durchsuchen

feat(epub): 增强EPUB导出功能并添加调试日志

- 添加封面HTML页面和默认样式文件
- 改进封面和图片处理逻辑,使用base64格式
- 重构文件组织结构,将章节和图片放入特定文件夹
- 添加详细的调试日志用于跟踪数据处理过程
- 改进文件名生成逻辑,支持中文文件名
YourName vor 2 Wochen
Ursprung
Commit
ecb8c40f8f
3 geänderte Dateien mit 159 neuen und 21 gelöschten Zeilen
  1. 2 0
      src/App.vue
  2. 25 2
      src/components/ChapterPanel.vue
  3. 132 19
      src/components/ExportDialog.vue

+ 2 - 0
src/App.vue

@@ -96,6 +96,8 @@ function handleContentUpdate(newContent) {
 // 处理书籍信息更新
 function handleBookInfoUpdate(newBookInfo) {
   bookInfo.value = { ...newBookInfo };
+  // 输出完整的书籍信息对象用于调试
+  console.log('更新后的完整书籍信息对象:', bookInfo.value);
 }
 
 // 打开导出对话框

+ 25 - 2
src/components/ChapterPanel.vue

@@ -336,6 +336,8 @@ function deleteNode(node) {
 // 书籍信息相关功能
 function saveBookInfo() {
   // 这里可以添加保存逻辑,比如发送到父组件
+  // 输出完整的书籍信息对象用于调试
+  console.log('保存的完整书籍信息对象:', localBookInfo.value);
   emit('update-book-info', localBookInfo.value);
   ElMessage.success('书籍信息保存成功');
 }
@@ -358,8 +360,29 @@ function resetBookInfo() {
 // 封面上传相关功能
 function handleCoverChange(file) {
   if (file && file.raw) {
-    localBookInfo.value.cover = URL.createObjectURL(file.raw);
-    ElMessage.success('封面上传成功');
+    // 将图片转换为base64格式
+    const reader = new FileReader();
+    reader.onload = (e) => {
+      localBookInfo.value.cover = e.target.result;
+      // 添加更详细的调试日志
+      console.log('封面base64数据长度:', e.target.result.length);
+      console.log('封面base64数据预览:', e.target.result.substring(0, 100) + '...');
+      console.log('封面base64完整数据预览:', e.target.result.substring(0, Math.min(200, e.target.result.length)) + '...');
+      // 输出完整base64数据
+      console.log('完整base64数据:', e.target.result);
+      // 输出完整的书籍信息对象
+      console.log('完整书籍信息对象:', localBookInfo.value);
+      // 检查数据是否以正确的前缀开始
+      if (e.target.result.startsWith('data:image/')) {
+        console.log('封面数据格式正确');
+      } else {
+        console.warn('封面数据格式可能不正确');
+      }
+      // 上传成功后自动保存书籍信息
+      saveBookInfo();
+      ElMessage.success('封面上传成功');
+    };
+    reader.readAsDataURL(file.raw);
   }
 }
 

+ 132 - 19
src/components/ExportDialog.vue

@@ -190,14 +190,42 @@ async function generateEPUB() {
   const tocNcx = generateTocNcx();
   zip.file('OEBPS/toc.ncx', tocNcx);
   
-  // 添加章节文件
+  // 添加章节文件到Text文件夹
   await addChapterFiles(zip);
   
-  // 添加封面
+  // 添加封面到Images文件夹
+// 添加封面
   if (hasCover.value) {
     await addCoverFile(zip);
+    // 添加封面HTML文件
+    const bookTitle = props.bookInfo.title || exportForm.value.title;
+    const coverHtml = `<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+  "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+  <title>封面</title>
+  <link rel="stylesheet" type="text/css" href="../Styles/style.css" />
+</head>
+<body>
+  <div class="cover">
+    <h1 class="title">${bookTitle}</h1>
+    <p class="author">作者:${exportForm.value.author}</p>
+    <img alt="cover" class="coverborder" src="../Images/cover.jpg" />
+  </div>
+</body>
+</html>`;
+    zip.file('OEBPS/Text/cover.xhtml', coverHtml);
   }
   
+  // 添加默认样式文件
+  zip.file('OEBPS/Styles/style.css', `body { margin: 5%; text-align: justify; }
+h1 { text-align: center; }
+.cover { text-align: center; margin-top: 10%; }
+.title { font-size: 2em; }
+.author { font-size: 1.2em; }
+.coverborder { max-width: 100%; height: auto; }`);
+  
   return zip;
 }
 
@@ -207,9 +235,18 @@ function generateContentOpf() {
   const spine = [];
   let chapterId = 1; // 用于章节的独立ID
   
+  // 使用书籍信息界面维护的书籍名称
+  const bookTitle = props.bookInfo.title || exportForm.value.title;
+  
+  // 添加样式文件
+  manifest.push(`    <item id="style" href="../Styles/style.css" media-type="text/css"/>`);
+  
   // 添加封面
   if (hasCover.value) {
-    manifest.push(`    <item id="cover" href="cover.jpg" media-type="image/jpeg" properties="cover-image"/>`);
+    manifest.push(`    <item id="cover" href="../Images/cover.jpg" media-type="image/jpeg" properties="cover-image"/>`);
+    manifest.push(`    <item id="cover-html" href="Text/cover.xhtml" media-type="application/xhtml+xml"/>`);
+    // 将封面HTML添加到书脊开头
+    spine.unshift(`    <itemref idref="cover-html"/>`);
   }
   
   // 添加章节
@@ -217,7 +254,7 @@ function generateContentOpf() {
     for (const node of nodes) {
       if (node.type === 'chapter') {
         const filename = `chapter-${chapterId}.xhtml`;
-        manifest.push(`    <item id="chapter-${chapterId}" href="${filename}" media-type="application/xhtml+xml"/>`);
+        manifest.push(`    <item id="chapter-${chapterId}" href="Text/${filename}" media-type="application/xhtml+xml"/>`);
         spine.push(`    <itemref idref="chapter-${chapterId}"/>`);
         chapterId++;
       }
@@ -229,17 +266,23 @@ function generateContentOpf() {
   
   addChapters(props.treeData);
   
-  return `<?xml version="1.0" encoding="UTF-8"?>
-<package version="3.0" xmlns="http://www.idpf.org/2007/opf">
-  <metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
-    <dc:title>${exportForm.value.title}</dc:title>
+  // 准备元数据内容
+  const metadataContent = `
+    <dc:title>${bookTitle}</dc:title>
     <dc:creator>${exportForm.value.author}</dc:creator>
     <dc:language>${exportForm.value.language}</dc:language>
     <dc:identifier id="BookId">${exportForm.value.identifier}</dc:identifier>
     <dc:publisher>${exportForm.value.publisher}</dc:publisher>
     <dc:rights>${exportForm.value.rights}</dc:rights>
     <dc:description>${exportForm.value.description}</dc:description>
-    <meta property="dcterms:modified">${new Date().toISOString()}</meta>
+    <meta property="dcterms:modified">${new Date().toISOString()}</meta>`;
+  
+  // 如果有封面,添加封面元数据
+  const coverMetadata = hasCover.value ? '\n    <meta name="cover" content="Images/cover.jpg"/>' : '';
+  
+  return `<?xml version="1.0" encoding="UTF-8"?>
+<package version="3.0" xmlns="http://www.idpf.org/2007/opf">
+  <metadata xmlns:dc="http://purl.org/dc/elements/1.1/">${metadataContent}${coverMetadata}
   </metadata>
   <manifest>
 ${manifest.join('\n')}
@@ -257,7 +300,22 @@ function generateTocNcx() {
   let playOrder = 1;
   let chapterIndex = 1; // 用于章节文件名的ID
   
+  // 使用书籍信息界面维护的书籍名称
+  const bookTitle = props.bookInfo.title || exportForm.value.title;
+  
   function addNavPoints(nodes) {
+    // 添加封面到目录
+    if (hasCover.value) {
+      navPoints.push(`    <navPoint id="nav-${navId}" playOrder="${playOrder}">
+      <navLabel>
+        <text>封面</text>
+      </navLabel>
+      <content src="Text/cover.xhtml"/>
+    </navPoint>`);
+      navId++;
+      playOrder++;
+    }
+    
     for (const node of nodes) {
       if (node.type === 'volume') {
         navPoints.push(`    <navPoint id="nav-${navId}" playOrder="${playOrder}">
@@ -272,7 +330,7 @@ function generateTocNcx() {
       <navLabel>
         <text>${node.label}</text>
       </navLabel>
-      <content src="chapter-${chapterIndex}.xhtml"/>
+      <content src="Text/chapter-${chapterIndex}.xhtml"/>
     </navPoint>`);
         navId++;
         playOrder++;
@@ -294,9 +352,10 @@ function generateTocNcx() {
     <meta name="dtb:depth" content="1"/>
     <meta name="dtb:totalPageCount" content="0"/>
     <meta name="dtb:maxPageNumber" content="0"/>
+    <meta name="cover" content="Images/cover.jpg"/>
   </head>
   <docTitle>
-    <text>${exportForm.value.title}</text>
+    <text>${bookTitle}</text>
   </docTitle>
   <navMap>
 ${navPoints.join('\n')}
@@ -313,12 +372,15 @@ async function addChapterFiles(zip) {
       if (node.type === 'chapter') {
         const filename = `chapter-${fileId}.xhtml`;
         const result = generateChapterContent(node, fileId);
-        zip.file(`OEBPS/${filename}`, result.content);
+        zip.file(`OEBPS/Text/${filename}`, result.content);
         
-        // 添加章节中的图片文件
+        // 添加章节中的图片文件到Images文件夹
         for (const [imageName, base64Data] of result.images) {
           const imageData = base64Data.split(',')[1];
-          zip.file(`OEBPS/${imageName}`, imageData, { base64: true });
+          // 输出章节图片的完整base64数据
+          console.log(`章节${fileId}图片${imageName}的完整base64数据:`, base64Data);
+          console.log(`章节${fileId}图片${imageName}提取后的base64数据:`, imageData);
+          zip.file(`OEBPS/Images/${imageName}`, imageData, { base64: true });
         }
         
         fileId++;
@@ -342,7 +404,9 @@ function generateChapterContent(chapter, chapterId) {
   processedContent = processedContent.replace(/<img[^>]*src="(data:image\/[^;]+;base64,[^"]+)"[^>]*>/g, (match, src) => {
     const imageName = `image-${chapterId}-${imageMap.size + 1}.jpg`;
     imageMap.set(imageName, src);
-    return match.replace(src, imageName);
+    // 输出章节中发现的base64图片数据
+    console.log(`章节${chapterId}中发现的base64图片数据:`, src);
+    return match.replace(src, `../Images/${imageName}`);
   });
   
   return {
@@ -352,6 +416,7 @@ function generateChapterContent(chapter, chapterId) {
 <head>
   <title>${chapter.label}</title>
   <meta charset="utf-8"/>
+  <link rel="stylesheet" type="text/css" href="../Styles/style.css"/>
 </head>
 <body>
   <h1>${chapter.label}</h1>
@@ -359,7 +424,8 @@ function generateChapterContent(chapter, chapterId) {
     ${processedContent}
   </div>
 </body>
-</html>`,
+</html>`
+,
     images: imageMap
   };
 }
@@ -367,13 +433,31 @@ function generateChapterContent(chapter, chapterId) {
 // 添加封面文件
 async function addCoverFile(zip) {
   try {
+    // 检查是否有封面数据
+    if (!props.bookInfo.cover || props.bookInfo.cover.trim() === '') {
+      console.log('没有封面数据');
+      return;
+    }
+    
+    console.log('封面数据存在:', props.bookInfo.cover.substring(0, 100) + '...');
+    console.log('封面数据总长度:', props.bookInfo.cover.length);
+    // 添加更详细的日志
+    console.log('封面数据完整预览:', props.bookInfo.cover.substring(0, Math.min(200, props.bookInfo.cover.length)) + '...');
+    // 输出完整base64数据用于调试
+    console.log('完整封面base64数据:', props.bookInfo.cover);
+    // 输出完整的书籍信息对象
+    console.log('完整书籍信息对象:', props.bookInfo);
+    
     // 从base64或blob URL获取图片数据
     let imageData;
     if (props.bookInfo.cover.startsWith('data:')) {
       // base64格式
       imageData = props.bookInfo.cover;
+      console.log('封面是base64格式');
+      console.log('封面base64格式正确性检查:', props.bookInfo.cover.startsWith('data:image/') ? '正确' : '可能不正确');
     } else if (props.bookInfo.cover.startsWith('blob:')) {
       // blob URL格式
+      console.log('封面是blob URL格式');
       const response = await fetch(props.bookInfo.cover);
       const blob = await response.blob();
       imageData = await new Promise((resolve) => {
@@ -381,23 +465,44 @@ async function addCoverFile(zip) {
         reader.onload = () => resolve(reader.result);
         reader.readAsDataURL(blob);
       });
+    } else {
+      console.log('封面数据格式未知');
+      console.log('封面数据前缀:', props.bookInfo.cover.substring(0, Math.min(50, props.bookInfo.cover.length)));
     }
     
     if (imageData) {
+      console.log('imageData存在:', imageData.substring(0, 100) + '...');
+      console.log('imageData总长度:', imageData.length);
+      console.log('imageData完整预览:', imageData.substring(0, Math.min(200, imageData.length)) + '...');
+      // 输出完整imageData用于调试
+      console.log('完整imageData:', imageData);
+      
       // 处理base64格式的图片数据
       let base64Data;
       if (imageData.startsWith('data:')) {
         // 如果是完整的data URL,提取base64部分
         base64Data = imageData.split(',')[1];
+        console.log('从data URL提取base64数据');
+        console.log('提取后的base64数据长度:', base64Data.length);
+        console.log('提取后的base64数据预览:', base64Data.substring(0, Math.min(100, base64Data.length)) + '...');
       } else {
         // 如果已经是base64数据,直接使用
         base64Data = imageData;
+        console.log('直接使用base64数据');
+        console.log('base64数据长度:', base64Data.length);
       }
       
       // 确保base64数据不为空
       if (base64Data && base64Data.trim() !== '') {
-        zip.file('OEBPS/cover.jpg', base64Data, { base64: true });
+        console.log('添加封面文件到EPUB');
+        console.log('最终用于EPUB的base64数据长度:', base64Data.length);
+        console.log('最终用于EPUB的base64数据预览:', base64Data.substring(0, Math.min(100, base64Data.length)) + '...');
+        zip.file('OEBPS/Images/cover.jpg', base64Data, { base64: true });
+      } else {
+        console.log('base64数据为空');
       }
+    } else {
+      console.log('imageData不存在');
     }
   } catch (error) {
     console.warn('封面添加失败:', error);
@@ -406,7 +511,10 @@ async function addCoverFile(zip) {
 
 // 导出处理
 async function handleExport() {
-  if (!exportForm.value.title.trim()) {
+  // 使用书籍信息界面维护的书籍名称
+  const bookTitle = props.bookInfo.title || exportForm.value.title;
+  
+  if (!bookTitle.trim()) {
     ElMessage.error('请输入书籍名称');
     return;
   }
@@ -420,10 +528,15 @@ async function handleExport() {
   
   try {
     const zip = await generateEPUB();
+    
+    // 输出完整的EPUB对象信息用于调试
+    console.log('EPUB对象信息:', zip);
+    
     const blob = await zip.generateAsync({ type: 'blob', mimeType: 'application/epub+zip' });
     
     // 生成文件名
-    const filename = `${exportForm.value.title.replace(/[^\w\s]/gi, '')}.epub`;
+    // 保留字母、数字、中文字符和一些安全的符号,移除其他特殊字符
+    const filename = `${bookTitle.replace(/[^\w\u4e00-\u9fa5\s\-_.]/g, '')}.epub`;
     
     // 下载文件
     saveAs(blob, filename);