Prechádzať zdrojové kódy

feat(ImportDialog): 添加EPUB导入功能及相关组件

- 在App.vue中引入ImportDialog组件,支持EPUB文件的导入
- 实现文件选择、解析进度和书籍信息预览功能
- 添加导入成功事件处理,更新书籍信息和章节树数据
- 更新样式以提升用户体验,支持导入按钮和对话框的交互逻辑
- 在package.json和package-lock.json中添加xmldom依赖
YourName 2 týždňov pred
rodič
commit
2b23057bdf
4 zmenil súbory, kde vykonal 801 pridanie a 12 odobranie
  1. 11 1
      package-lock.json
  2. 2 1
      package.json
  3. 69 10
      src/App.vue
  4. 719 0
      src/components/ImportDialog.vue

+ 11 - 1
package-lock.json

@@ -17,7 +17,8 @@
         "quill": "^2.0.3",
         "stream-browserify": "^3.0.0",
         "util": "^0.12.5",
-        "vue": "^3.2.13"
+        "vue": "^3.2.13",
+        "xmldom": "^0.6.0"
       },
       "devDependencies": {
         "@babel/core": "^7.12.16",
@@ -12801,6 +12802,15 @@
         }
       }
     },
+    "node_modules/xmldom": {
+      "version": "0.6.0",
+      "resolved": "https://mirrors.huaweicloud.com/repository/npm/xmldom/-/xmldom-0.6.0.tgz",
+      "integrity": "sha512-iAcin401y58LckRZ0TkI4k0VSM1Qg0KGSc3i8rU+xrxe19A/BN1zHyVSJY7uoutVlaTSzYyk/v5AmkewAP7jtg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
     "node_modules/y18n": {
       "version": "5.0.8",
       "resolved": "https://mirrors.huaweicloud.com/repository/npm/y18n/-/y18n-5.0.8.tgz",

+ 2 - 1
package.json

@@ -17,7 +17,8 @@
     "quill": "^2.0.3",
     "stream-browserify": "^3.0.0",
     "util": "^0.12.5",
-    "vue": "^3.2.13"
+    "vue": "^3.2.13",
+    "xmldom": "^0.6.0"
   },
   "devDependencies": {
     "@babel/core": "^7.12.16",

+ 69 - 10
src/App.vue

@@ -1,9 +1,10 @@
 <script setup>
 import { ref, computed } from 'vue';
-import { Document, Download } from '@element-plus/icons-vue';
+import { Document, Download, Upload } from '@element-plus/icons-vue';
 import Editor from './components/Editor.vue';
 import ChapterPanel from './components/ChapterPanel.vue';
 import ExportDialog from './components/ExportDialog.vue';
+import ImportDialog from './components/ImportDialog.vue';
 
 // 卷-章树数据,章节点有content字段
 const treeData = ref([]);
@@ -30,6 +31,9 @@ const bookInfo = ref({
 // 导出对话框显示状态
 const showExportDialog = ref(false);
 
+// 导入对话框显示状态
+const showImportDialog = ref(false);
+
 // 判断是否应该显示编辑器
 const shouldShowEditor = computed(() => {
   return selectedChapter.value !== null && 
@@ -105,6 +109,11 @@ function openExportDialog() {
   showExportDialog.value = true;
 }
 
+// 打开导入对话框
+function openImportDialog() {
+  showImportDialog.value = true;
+}
+
 // 获取安全的HTML内容
 function getSafeContent(content) {
   if (!content) {
@@ -133,6 +142,35 @@ function getSafeContent(content) {
   
   return '';
 }
+
+// 处理导入成功事件
+function handleImportSuccess(importData) {
+  console.log('收到导入数据:', importData);
+  
+  // 更新书籍信息
+  bookInfo.value = { ...importData.bookInfo };
+  
+  // 更新章节树数据
+  treeData.value = importData.treeData;
+  
+  // 如果有封面,更新封面
+  if (importData.cover) {
+    bookInfo.value.cover = importData.cover;
+  }
+  
+  // 清空当前选中的章节
+  selectedChapter.value = null;
+  
+  console.log('导入成功,更新后的数据:', {
+    bookInfo: bookInfo.value,
+    treeData: treeData.value,
+    treeDataLength: treeData.value.length
+  });
+  console.log('章节树数据结构详情:', JSON.stringify(treeData.value, null, 2));
+  
+  // 强制更新编辑器
+  editorKey.value = Date.now();
+}
 </script>
 
 <template>
@@ -141,15 +179,25 @@ function getSafeContent(content) {
       <template #header>
         <div class="panel-header">
           <span>书籍章节列表</span>
-          <el-button 
-            type="primary" 
-            size="small" 
-            @click="openExportDialog"
-            :disabled="treeData.length === 0"
-          >
-            <el-icon><Download /></el-icon>
-            导出EPUB
-          </el-button>
+          <div class="header-buttons">
+            <el-button 
+              type="success" 
+              size="small" 
+              @click="openImportDialog"
+            >
+              <el-icon><Upload /></el-icon>
+              导入EPUB
+            </el-button>
+            <el-button 
+              type="primary" 
+              size="small" 
+              @click="openExportDialog"
+              :disabled="treeData.length === 0"
+            >
+              <el-icon><Download /></el-icon>
+              导出EPUB
+            </el-button>
+          </div>
         </div>
       </template>
       <ChapterPanel 
@@ -183,6 +231,12 @@ function getSafeContent(content) {
       :book-info="bookInfo"
       :tree-data="treeData"
     />
+    
+    <!-- 导入对话框 -->
+    <ImportDialog
+      v-model:visible="showImportDialog"
+      @import-success="handleImportSuccess"
+    />
   </div>
 </template>
 
@@ -210,6 +264,11 @@ function getSafeContent(content) {
   font-size: 16px;
 }
 
+.header-buttons {
+  display: flex;
+  gap: 8px;
+}
+
 .panel-title {
   display: flex;
   align-items: center;

+ 719 - 0
src/components/ImportDialog.vue

@@ -0,0 +1,719 @@
+<template>
+  <el-dialog v-model="dialogVisible" title="导入EPUB书籍" width="600px" :before-close="handleClose">
+    <div class="import-content">
+      <!-- 文件选择区域 -->
+      <div class="file-upload-area">
+        <el-upload ref="uploadRef" :auto-upload="false" :show-file-list="false" accept=".epub"
+          :on-change="handleFileChange" drag>
+          <el-icon class="el-icon--upload"><upload-filled /></el-icon>
+          <div class="el-upload__text">
+            将 EPUB 文件拖到此处,或<em>点击上传</em>
+          </div>
+          <template #tip>
+            <div class="el-upload__tip">
+              只能上传 epub 文件
+            </div>
+          </template>
+        </el-upload>
+      </div>
+
+      <!-- 解析进度 -->
+      <div v-if="parsing" class="parsing-progress">
+        <el-progress :percentage="parseProgress" :format="progressFormat" />
+        <p>{{ parseStatus }}</p>
+      </div>
+
+      <!-- 书籍信息预览 -->
+      <div v-if="bookPreview" class="book-preview">
+        <h4>书籍信息</h4>
+        <el-form :model="bookPreview" label-width="100px" size="small">
+          <el-form-item label="书籍名称">
+            <el-input v-model="bookPreview.title" readonly />
+          </el-form-item>
+          <el-form-item label="作者">
+            <el-input v-model="bookPreview.author" readonly />
+          </el-form-item>
+          <el-form-item label="语言">
+            <el-input v-model="bookPreview.language" readonly />
+          </el-form-item>
+          <el-form-item label="标识符">
+            <el-input v-model="bookPreview.identifier" readonly />
+          </el-form-item>
+          <el-form-item label="出版社">
+            <el-input v-model="bookPreview.publisher" readonly />
+          </el-form-item>
+          <el-form-item label="版权">
+            <el-input v-model="bookPreview.rights" readonly />
+          </el-form-item>
+          <el-form-item label="简介">
+            <el-input v-model="bookPreview.description" type="textarea" :rows="3" readonly />
+          </el-form-item>
+        </el-form>
+
+        <!-- 章节预览 -->
+        <div class="chapter-preview">
+          <h4>章节结构 ({{ chapterCount }} 章)</h4>
+          <el-tree :data="chapterTree" :props="{ label: 'label' }" default-expand-all class="chapter-tree" />
+        </div>
+
+        <!-- 封面预览 -->
+        <div v-if="coverPreview" class="cover-preview">
+          <h4>封面预览</h4>
+          <img :src="coverPreview" alt="封面" class="cover-image" />
+        </div>
+      </div>
+    </div>
+
+    <template #footer>
+      <span class="dialog-footer">
+        <el-button @click="handleClose">取消</el-button>
+        <el-button type="primary" @click="handleImport" :loading="importing" :disabled="!bookPreview">
+          {{ importing ? '导入中...' : '导入书籍' }}
+        </el-button>
+      </span>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup>
+import { ref, computed, defineProps, defineEmits } from 'vue';
+import { ElMessage } from 'element-plus';
+import { UploadFilled } from '@element-plus/icons-vue';
+import JSZip from 'jszip';
+import { DOMParser } from 'xmldom';
+
+// 创建兼容的 DOMParser
+const createDOMParser = () => {
+  return new DOMParser({
+    locator: {},
+    errorHandler: {
+      warning: () => { },
+      error: () => { },
+      fatalError: () => { }
+    }
+  });
+};
+
+// Props
+const props = defineProps({
+  visible: {
+    type: Boolean,
+    default: false
+  }
+});
+
+// Emits
+const emit = defineEmits(['update:visible', 'import-success']);
+
+// 本地响应式变量
+const dialogVisible = computed({
+  get: () => props.visible,
+  set: (val) => emit('update:visible', val)
+});
+
+const uploadRef = ref();
+const parsing = ref(false);
+const importing = ref(false);
+const parseProgress = ref(0);
+const parseStatus = ref('');
+const bookPreview = ref(null);
+const chapterTree = ref([]);
+const coverPreview = ref(null);
+const chapterCount = ref(0);
+
+// 文件处理
+function handleFileChange(file) {
+  if (file.raw.type !== 'application/epub+zip' && !file.raw.name.endsWith('.epub')) {
+    ElMessage.error('请选择有效的 EPUB 文件');
+    return;
+  }
+
+  parseEPUB(file.raw);
+}
+
+// 解析 EPUB 文件
+async function parseEPUB(file) {
+  parsing.value = true;
+  parseProgress.value = 0;
+  parseStatus.value = '开始解析 EPUB 文件...';
+
+  try {
+    const zip = new JSZip();
+    const zipData = await zip.loadAsync(file);
+
+    parseProgress.value = 20;
+    parseStatus.value = '解析容器文件...';
+
+    // 检查 mimetype
+    const mimetype = await zipData.file('mimetype').async('string');
+    if (mimetype !== 'application/epub+zip') {
+      throw new Error('无效的 EPUB 文件格式');
+    }
+
+    parseProgress.value = 40;
+    parseStatus.value = '解析包文件...';
+
+    // 解析 container.xml
+    const containerXml = await zipData.file('META-INF/container.xml').async('string');
+    const containerDoc = createDOMParser().parseFromString(containerXml, 'text/xml');
+    const rootfileElement = containerDoc.getElementsByTagName('rootfile')[0];
+    const rootfilePath = rootfileElement.getAttribute('full-path');
+
+    parseProgress.value = 60;
+    parseStatus.value = '解析内容文件...';
+
+    // 解析 content.opf
+    const contentOpf = await zipData.file(rootfilePath).async('string');
+    const opfDoc = createDOMParser().parseFromString(contentOpf, 'text/xml');
+
+    // 提取元数据
+    const metadata = extractMetadata(opfDoc);
+
+    parseProgress.value = 80;
+    parseStatus.value = '解析章节结构...';
+
+    // 解析目录结构
+    const tocNcx = await zipData.file('OEBPS/toc.ncx').async('string');
+    const tocDoc = createDOMParser().parseFromString(tocNcx, 'text/xml');
+    const chapters = await parseChapters(tocDoc, zipData);
+
+    parseProgress.value = 90;
+    parseStatus.value = '处理封面...';
+
+    // 处理封面
+    const cover = await processCover(metadata, zipData);
+
+    parseProgress.value = 100;
+    parseStatus.value = '解析完成';
+
+    // 构建预览数据
+    bookPreview.value = {
+      title: metadata.title || '未知标题',
+      author: metadata.creator || '未知作者',
+      language: metadata.language || 'zh-CN',
+      identifier: metadata.identifier || '',
+      publisher: metadata.publisher || '',
+      rights: metadata.rights || '',
+      description: metadata.description || ''
+    };
+
+    chapterTree.value = chapters;
+    chapterCount.value = countChapters(chapters);
+    coverPreview.value = cover;
+
+    console.log('解析完成,章节树数据:', chapters);
+    console.log('章节数量:', chapterCount.value);
+    console.log('章节树数据结构:', JSON.stringify(chapters, null, 2));
+
+    ElMessage.success('EPUB 文件解析成功');
+
+  } catch (error) {
+    console.error('解析失败:', error);
+    ElMessage.error('EPUB 文件解析失败: ' + error.message);
+  } finally {
+    parsing.value = false;
+  }
+}
+
+// 提取元数据
+function extractMetadata(opfDoc) {
+  const metadata = {};
+
+  // 提取基本元数据
+  const title = opfDoc.getElementsByTagName('dc:title')[0] || opfDoc.getElementsByTagName('title')[0];
+  if (title) metadata.title = title.textContent;
+
+  const creator = opfDoc.getElementsByTagName('dc:creator')[0] || opfDoc.getElementsByTagName('creator')[0];
+  if (creator) metadata.creator = creator.textContent;
+
+  const language = opfDoc.getElementsByTagName('dc:language')[0] || opfDoc.getElementsByTagName('language')[0];
+  if (language) metadata.language = language.textContent;
+
+  const identifier = opfDoc.getElementsByTagName('dc:identifier')[0] || opfDoc.getElementsByTagName('identifier')[0];
+  if (identifier) metadata.identifier = identifier.textContent;
+
+  const publisher = opfDoc.getElementsByTagName('dc:publisher')[0] || opfDoc.getElementsByTagName('publisher')[0];
+  if (publisher) metadata.publisher = publisher.textContent;
+
+  const rights = opfDoc.getElementsByTagName('dc:rights')[0] || opfDoc.getElementsByTagName('rights')[0];
+  if (rights) metadata.rights = rights.textContent;
+
+  const description = opfDoc.getElementsByTagName('dc:description')[0] || opfDoc.getElementsByTagName('description')[0];
+  if (description) metadata.description = description.textContent;
+
+  return metadata;
+}
+
+// 解析章节结构
+async function parseChapters(tocDoc, zipData) {
+  const chapters = [];
+  const navPoints = tocDoc.getElementsByTagName('navPoint');
+
+  console.log('开始解析章节结构,navPoints数量:', navPoints.length);
+
+  // 用于跟踪当前卷
+  let currentVolume = null;
+  let chapterId = 1;
+  let volumeId = 1;
+
+  for (let i = 0; i < navPoints.length; i++) {
+    const navPoint = navPoints[i];
+    const navLabel = navPoint.getElementsByTagName('navLabel')[0];
+    const content = navPoint.getElementsByTagName('content')[0];
+
+    console.log(`处理第${i + 1}个navPoint:`, {
+      hasNavLabel: !!navLabel,
+      hasContent: !!content,
+      navLabelText: navLabel ? navLabel.getElementsByTagName('text')[0]?.textContent : '无',
+      contentSrc: content ? content.getAttribute('src') : '无'
+    });
+
+    if (!navLabel) {
+      console.log('跳过:没有navLabel');
+      continue;
+    }
+
+    const labelElement = navLabel.getElementsByTagName('text')[0];
+    const label = labelElement ? labelElement.textContent : '';
+
+    console.log('标签内容:', label);
+
+    // 跳过封面
+    if (label === '封面') {
+      console.log('跳过:封面');
+      continue;
+    }
+
+    // 检查是否有 content 元素
+    if (!content) {
+      // 没有 content 元素,这是一个卷(volume)
+      console.log('识别为卷:', label);
+      if (currentVolume) {
+        // 如果已经有卷了,先保存当前卷
+        console.log('保存当前卷:', currentVolume);
+        chapters.push(currentVolume);
+      }
+
+      currentVolume = {
+        id: volumeId++,
+        type: 'volume',
+        label: label,
+        children: []
+      };
+    } else {
+      // 有 content 元素,这是一个章节
+      const src = content.getAttribute('src');
+      console.log('识别为章节:', label, 'src:', src);
+      if (src) {
+        const chapterContent = await loadChapterContent(src, zipData);
+        const chapter = {
+          id: chapterId++,
+          type: 'chapter',
+          label: label,
+          src: src,
+          content: chapterContent
+        };
+
+        console.log('创建的章节对象:', chapter);
+
+        if (currentVolume) {
+          // 添加到当前卷中
+          console.log('添加到卷中:', chapter.label);
+          currentVolume.children.push(chapter);
+        } else {
+          // 没有卷,作为独立章节
+          console.log('作为独立章节:', chapter.label);
+          chapters.push(chapter);
+        }
+      }
+    }
+  }
+
+  // 保存最后一个卷
+  if (currentVolume) {
+    console.log('保存最后一个卷:', currentVolume);
+    chapters.push(currentVolume);
+  }
+
+  console.log('最终解析结果:', chapters);
+  return chapters;
+}
+
+// 加载章节内容
+async function loadChapterContent(src, zipData) {
+  try {
+    console.log('=== 开始加载章节内容 ===');
+    console.log('章节src:', src);
+
+    const allFiles = Object.keys(zipData.files);
+    console.log('EPUB中的所有文件:', allFiles);
+
+    // 尝试多种可能的路径
+    let chapterFile = null;
+
+    // 构建所有可能的路径组合
+    const possiblePaths = [
+      // 直接使用原始路径
+      src,
+      // 添加 OEBPS 前缀
+      `OEBPS/${src}`,
+      // 如果 src 包含 Text/,尝试去掉
+      src.replace('Text/', ''),
+      `OEBPS/${src.replace('Text/', '')}`,
+      // 如果 src 不包含 Text/,尝试添加
+      src.includes('Text/') ? src : `Text/${src}`,
+      `OEBPS/Text/${src}`,
+      // 尝试不同的扩展名
+      src.replace('.xhtml', '.html'),
+      `OEBPS/${src.replace('.xhtml', '.html')}`,
+      // 尝试去掉扩展名
+      src.replace('.xhtml', ''),
+      `OEBPS/${src.replace('.xhtml', '')}`,
+      // 尝试更多路径变体
+      src.replace('Text/', 'OEBPS/Text/'),
+      src.replace('Text/', 'OEBPS/'),
+      `OEBPS/Text/${src.replace('Text/', '')}`,
+      `OEBPS/${src.replace('Text/', '')}`
+    ];
+
+    console.log('尝试的路径列表:', possiblePaths);
+
+    // 首先尝试精确匹配
+    for (const path of possiblePaths) {
+      console.log('尝试精确路径:', path);
+      chapterFile = zipData.file(path);
+      if (chapterFile) {
+        console.log('✅ 找到章节文件:', path);
+        break;
+      }
+    }
+
+    // 如果精确匹配失败,尝试模糊匹配
+    if (!chapterFile) {
+      console.log('精确匹配失败,尝试模糊匹配...');
+
+      // 提取章节名称(去掉路径和扩展名)
+      const chapterName = src.split('/').pop().replace('.xhtml', '').replace('.html', '');
+      console.log('章节名称:', chapterName);
+
+      // 查找包含章节名称的所有文件
+      const matchingFiles = allFiles.filter(file => {
+        const fileName = file.split('/').pop().toLowerCase();
+        const chapterNameLower = chapterName.toLowerCase();
+        return fileName.includes(chapterNameLower) &&
+          (fileName.endsWith('.xhtml') || fileName.endsWith('.html'));
+      });
+
+      console.log('模糊匹配找到的文件:', matchingFiles);
+
+      if (matchingFiles.length > 0) {
+        // 优先选择最匹配的文件
+        const bestMatch = matchingFiles.find(file =>
+          file.toLowerCase().includes(chapterName.toLowerCase())
+        ) || matchingFiles[0];
+
+        console.log('选择最佳匹配文件:', bestMatch);
+        chapterFile = zipData.file(bestMatch);
+      }
+    }
+
+    // 如果还是找不到,尝试查找所有 HTML 文件
+    if (!chapterFile) {
+      console.log('模糊匹配也失败,查找所有HTML文件...');
+      const htmlFiles = allFiles.filter(file =>
+        file.endsWith('.xhtml') || file.endsWith('.html')
+      );
+      console.log('所有HTML文件:', htmlFiles);
+
+      if (htmlFiles.length > 0) {
+        // 选择第一个HTML文件作为备选
+        console.log('使用第一个HTML文件作为备选:', htmlFiles[0]);
+        chapterFile = zipData.file(htmlFiles[0]);
+      }
+    }
+
+    // 如果还是找不到,尝试查找任何包含章节名称的文件
+    if (!chapterFile) {
+      console.log('尝试查找任何包含章节名称的文件...');
+      const chapterName = src.split('/').pop().replace('.xhtml', '').replace('.html', '');
+      const anyMatchingFiles = allFiles.filter(file =>
+        file.toLowerCase().includes(chapterName.toLowerCase())
+      );
+      console.log('包含章节名称的文件:', anyMatchingFiles);
+
+      if (anyMatchingFiles.length > 0) {
+        console.log('使用包含章节名称的文件:', anyMatchingFiles[0]);
+        chapterFile = zipData.file(anyMatchingFiles[0]);
+      }
+    }
+
+    // 最后的备选方案:使用任何HTML文件
+    if (!chapterFile) {
+      console.log('尝试使用任何HTML文件作为最后备选...');
+      const anyHtmlFiles = allFiles.filter(file =>
+        file.endsWith('.xhtml') || file.endsWith('.html') || file.endsWith('.htm')
+      );
+      console.log('任何HTML文件:', anyHtmlFiles);
+
+      if (anyHtmlFiles.length > 0) {
+        console.log('使用任何HTML文件:', anyHtmlFiles[0]);
+        chapterFile = zipData.file(anyHtmlFiles[0]);
+      }
+    }
+
+    if (!chapterFile) {
+      console.error('❌ 无法找到章节文件');
+      console.log('所有可用文件:', allFiles);
+      return '<p>章节内容加载失败</p>';
+    }
+
+    const chapterHtml = await chapterFile.async('string');
+    console.log('章节HTML长度:', chapterHtml.length);
+    console.log('=== 章节HTML完整内容 ===');
+    console.log(chapterHtml);
+    console.log('=== 章节HTML内容结束 ===');
+
+               // 直接用字符串处理提取 div class="content" 的内容
+      console.log('=== 章节HTML完整内容 ===');
+      console.log(chapterHtml);
+      console.log('=== 章节HTML内容结束 ===');
+      
+      // 查找 <div class="content"> 的开始和结束位置
+      const startTag = '<div class="content">';
+      const endTag = '</div>';
+      
+      const startIndex = chapterHtml.indexOf(startTag);
+      if (startIndex !== -1) {
+        const contentStart = startIndex + startTag.length;
+        const endIndex = chapterHtml.indexOf(endTag, contentStart);
+        
+        if (endIndex !== -1) {
+          let content = chapterHtml.substring(contentStart, endIndex);
+          
+                     // 处理图片路径转换
+           console.log('处理前的图片路径:', content);
+           
+           // 提取并转换图片为base64
+           const imgRegex = /<img[^>]*src="([^"]*)"[^>]*>/g;
+           let match;
+           let processedContent = content;
+           
+           while ((match = imgRegex.exec(content)) !== null) {
+             const imgSrc = match[1];
+             console.log('找到图片路径:', imgSrc);
+             
+             // 处理相对路径
+             let imagePath = imgSrc;
+             if (imgSrc.startsWith('../')) {
+               imagePath = imgSrc.replace('../', 'OEBPS/');
+             } else if (!imgSrc.startsWith('OEBPS/')) {
+               imagePath = `OEBPS/${imgSrc}`;
+             }
+             
+             console.log('转换后的图片路径:', imagePath);
+             
+             // 尝试从EPUB中提取图片并转换为base64
+             try {
+               const imageFile = zipData.file(imagePath);
+               if (imageFile) {
+                 console.log('找到图片文件:', imagePath);
+                 const imageData = await imageFile.async('base64');
+                 const imageType = imagePath.endsWith('.png') ? 'image/png' : 'image/jpeg';
+                 const base64Src = `data:${imageType};base64,${imageData}`;
+                 
+                 console.log('图片转换为base64成功');
+                 
+                 // 替换图片src为base64
+                 processedContent = processedContent.replace(imgSrc, base64Src);
+               } else {
+                 console.warn('未找到图片文件:', imagePath);
+               }
+             } catch (error) {
+               console.error('图片转换失败:', error);
+             }
+           }
+           
+           content = processedContent;
+          
+          console.log('处理后的图片路径:', content);
+          console.log('=== 提取的章节内容 ===');
+          console.log(content);
+          console.log('=== 提取的章节内容结束 ===');
+          return content;
+        }
+      }
+      
+      console.warn('未找到 div class="content"');
+      return '<p>章节内容解析失败</p>';
+  } catch (error) {
+    console.error('加载章节内容失败:', error);
+    return '<p>章节内容加载失败</p>';
+  }
+}
+
+// 处理封面
+async function processCover(metadata, zipData) {
+  try {
+    // 查找封面图片
+    const coverFiles = Object.keys(zipData.files).filter(file =>
+      file.includes('cover') && (file.endsWith('.jpg') || file.endsWith('.png'))
+    );
+
+    if (coverFiles.length > 0) {
+      const coverFile = coverFiles[0];
+      const coverData = await zipData.file(coverFile).async('blob');
+      return URL.createObjectURL(coverData);
+    }
+  } catch (error) {
+    console.warn('封面处理失败:', error);
+  }
+
+  return null;
+}
+
+// 统计章节数量
+function countChapters(nodes) {
+  let count = 0;
+
+  function countRecursive(items) {
+    if (!Array.isArray(items)) {
+      return;
+    }
+
+    for (let i = 0; i < items.length; i++) {
+      const item = items[i];
+      if (item.type === 'chapter') {
+        count++;
+      }
+      if (item.children && Array.isArray(item.children)) {
+        countRecursive(item.children);
+      }
+    }
+  }
+
+  countRecursive(nodes);
+  return count;
+}
+
+// 进度格式化
+function progressFormat(percentage) {
+  return percentage === 100 ? '完成' : `${percentage}%`;
+}
+
+// 关闭对话框
+function handleClose() {
+  emit('update:visible', false);
+  // 重置状态
+  bookPreview.value = null;
+  chapterTree.value = [];
+  coverPreview.value = null;
+  chapterCount.value = 0;
+  parseProgress.value = 0;
+  parseStatus.value = '';
+}
+
+// 导入处理
+async function handleImport() {
+  if (!bookPreview.value) {
+    ElMessage.error('请先选择 EPUB 文件');
+    return;
+  }
+
+  importing.value = true;
+
+  try {
+    // 构建导入数据
+    const importData = {
+      bookInfo: bookPreview.value,
+      treeData: chapterTree.value,
+      cover: coverPreview.value
+    };
+
+    console.log('准备发送导入数据:', importData);
+    console.log('章节树数据长度:', importData.treeData.length);
+    console.log('章节树数据详情:', JSON.stringify(importData.treeData, null, 2));
+
+    // 触发导入成功事件
+    emit('import-success', importData);
+
+    ElMessage.success('书籍导入成功!');
+    handleClose();
+
+  } catch (error) {
+    console.error('导入失败:', error);
+    ElMessage.error('导入失败,请重试');
+  } finally {
+    importing.value = false;
+  }
+}
+</script>
+
+<style scoped>
+.import-content {
+  max-height: 70vh;
+  overflow-y: auto;
+}
+
+.file-upload-area {
+  margin-bottom: 20px;
+}
+
+.parsing-progress {
+  margin: 20px 0;
+  text-align: center;
+}
+
+.parsing-progress p {
+  margin-top: 10px;
+  color: #606266;
+}
+
+.book-preview {
+  margin-top: 20px;
+}
+
+.book-preview h4 {
+  margin: 0 0 15px 0;
+  color: #303133;
+  border-bottom: 1px solid #ebeef5;
+  padding-bottom: 8px;
+}
+
+.chapter-preview {
+  margin-top: 20px;
+}
+
+.chapter-tree {
+  max-height: 200px;
+  overflow-y: auto;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  padding: 10px;
+}
+
+.cover-preview {
+  margin-top: 20px;
+}
+
+.cover-image {
+  max-width: 200px;
+  max-height: 300px;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 10px;
+}
+
+:deep(.el-upload-dragger) {
+  width: 100%;
+  height: 120px;
+}
+
+:deep(.el-upload__text) {
+  margin-top: 10px;
+}
+</style>