|
@@ -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>
|