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