// 转换为CommonJS模块语法 - 确保所有导入语句在文件最前面 const path = require('path'); const fs = require('fs'); const PDFDocument = require('pdfkit'); const { app } = require('electron'); // 不依赖__filename和__dirname,直接使用app.getAppPath()获取应用路径 // 这是解决webpack打包导致路径问题的关键 function getAppRoot() { try { // 在Electron应用中,app.getAppPath()会返回应用的实际路径 return path.dirname(app.getAppPath()); } catch (error) { console.error('获取应用路径失败:', error); // 备选方案:使用当前工作目录 return process.cwd(); } } // 获取字体路径的函数 function getFontPath() { const appRoot = getAppRoot(); // 定义多个可能的字体目录路径 const possiblePaths = [ // 开发环境下的字体路径 path.join(appRoot, 'src', 'background', 'font'), // 打包后的字体路径 path.join(appRoot, 'background', 'font'), // 高版本工程中的字体路径 path.join(appRoot, 'electron', 'font') ]; // 检查哪个路径存在 for (const fontPath of possiblePaths) { if (fs.existsSync(fontPath)) { console.log('找到字体目录:', fontPath); return fontPath; } } // 如果都不存在,返回默认路径 const defaultPath = path.join(appRoot, 'src', 'background', 'font'); console.warn('未找到字体目录,使用默认路径:', defaultPath); return defaultPath; } // 现在使用getFontPath()函数来获取字体路径 const FONT_PATH = getFontPath(); // 统一使用微软雅黑字体 const msyhFontPath = path.join(FONT_PATH, 'msyh.ttf'); // 输出找到的字体路径供调试 console.log('FONT_PATH:', FONT_PATH); console.log('msyhFontPath:', msyhFontPath, '存在?', fs.existsSync(msyhFontPath)); /** * 服务层:获取所有考生列表 * @returns {Promise} 考生列表 */ async function createFileService() { try { /// TODO 测试用 // return await writeFileAsync('C:/Users/chenqiang/Desktop/1.txt', 'hello world'); } catch (error) { console.error('服务层: 创建文件失败', error); throw error; } } /** * 生成PDF文件并保存到合适的目录 * @param {Object} pdfData - PDF数据 * @param {string} fileName - 文件名 * @returns {Promise} 文件保存路径 */ async function generatePdfService(pdfData, fileName) { try { // 获取合适的保存目录 const appDir = path.join(getAppSaveDir(), '..') const filePath = path.join(appDir, `${fileName || 'document'}.pdf`); return new Promise((resolve, reject) => { // 创建PDF文档 const doc = new PDFDocument(); // 加载中文字体的标志 let chineseFontLoaded = false; let currentFont = null; // 专门的字体加载函数 function loadMicrosoftYaHeiFont() { try { // 只加载微软雅黑字体 if (fs.existsSync(msyhFontPath)) { doc.registerFont('MicrosoftYaHei', msyhFontPath); doc.font('MicrosoftYaHei'); currentFont = 'MicrosoftYaHei'; chineseFontLoaded = true; console.log('成功加载msyh.ttf字体'); return true; } else { console.error('微软雅黑字体文件不存在:', msyhFontPath); return false; } } catch (error) { console.error('加载微软雅黑字体失败:', error); return false; } } // 尝试加载字体 if (!loadMicrosoftYaHeiFont()) { console.warn('无法加载微软雅黑字体,将使用系统字体'); // 在macOS上尝试使用系统字体 if (process.platform === 'darwin') { try { doc.font('Arial Unicode MS'); // macOS内置支持多语言的字体 currentFont = 'Arial Unicode MS'; chineseFontLoaded = true; console.log('成功加载系统Arial Unicode MS字体'); } catch (error) { console.error('加载系统字体失败:', error); } } } // 保存到文件 const writeStream = fs.createWriteStream(filePath); doc.pipe(writeStream); // 设置文档标题 if (pdfData.title) { // 保存当前字体 const tempFont = currentFont; try { // 微软雅黑没有单独的粗体,使用普通字体但加大字号 doc.fontSize(20).text(pdfData.title, { align: 'center' }).moveDown(); } catch (error) { console.error('设置标题字体失败:', error); doc.fontSize(20).text(pdfData.title, { align: 'center' }).moveDown(); } // 恢复字体 if (currentFont) { doc.font(currentFont); } } // 添加内容 if (pdfData.content) { pdfData.content.forEach(item => { if (item.type === 'text') { doc.fontSize(item.fontSize || 12).text(item.text, item.options || {}).moveDown(); } else if (item.type === 'heading') { // 保存当前字体 const tempFont = currentFont; try { // 微软雅黑没有单独的粗体,使用普通字体但加大字号 doc.fontSize(item.fontSize || 16).text(item.text, item.options || {}).moveDown(); } catch (error) { console.error('切换到标题字体失败:', error); doc.fontSize(item.fontSize || 16).text(item.text, item.options || {}).moveDown(); } // 恢复之前的字体 if (currentFont) { doc.font(currentFont); } } else if (item.type === 'table') { // 改进表格实现 - 不使用splitTextToFit方法 const { headers, rows } = item; const cellWidth = 100; const baseCellHeight = 25; // 增加基础单元格高度,更好地适应中文 const marginLeft = 50; let currentY = doc.y; const fontSize = 12; // 辅助函数:计算文本在指定宽度内的行数 const calculateLines = (text, width) => { // 估算每行字符数(假设平均字符宽度为字体大小的一半) const charsPerLine = Math.floor(width / (fontSize / 2)); const lines = []; let currentText = text; while (currentText.length > 0) { // 找到合适的换行位置 let splitIndex = Math.min(currentText.length, charsPerLine); // 尝试在空格处换行 if (currentText.length > splitIndex && currentText[splitIndex] !== ' ') { const lastSpace = currentText.lastIndexOf(' ', splitIndex); if (lastSpace > 0) { splitIndex = lastSpace; } } lines.push(currentText.substring(0, splitIndex).trim()); currentText = currentText.substring(splitIndex).trim(); } return lines; }; // 绘制表头 headers.forEach((header, i) => { // 计算单元格实际高度(考虑换行) const lines = calculateLines(header, cellWidth - 10); const cellHeight = Math.max(baseCellHeight, lines.length * 15); doc.rect(marginLeft + i * cellWidth, currentY, cellWidth, cellHeight).stroke(); // 微软雅黑没有单独的粗体,使用普通字体 doc.fontSize(fontSize).text(header, marginLeft + i * cellWidth + 5, currentY + (cellHeight - lines.length * 15) / 2, { width: cellWidth - 10, height: cellHeight - 10 }); }); // 移动到下一行,考虑最高的表头单元格高度 const headerLines = headers.map(header => calculateLines(header, cellWidth - 10).length); const maxHeaderLines = Math.max(...headerLines); currentY += Math.max(baseCellHeight, maxHeaderLines * 15) + 5; // 添加一些间距 // 绘制行 rows.forEach(row => { // 计算这一行中最高的单元格 const rowLines = row.map(cell => calculateLines(cell.toString(), cellWidth - 10).length); const maxRowLines = Math.max(...rowLines); const rowHeight = Math.max(baseCellHeight, maxRowLines * 15); row.forEach((cell, i) => { doc.rect(marginLeft + i * cellWidth, currentY, cellWidth, rowHeight).stroke(); const cellLines = calculateLines(cell.toString(), cellWidth - 10); doc.fontSize(fontSize).text(cell.toString(), marginLeft + i * cellWidth + 5, currentY + (rowHeight - cellLines.length * 15) / 2, { width: cellWidth - 10, height: rowHeight - 10 }); }); // 移动到下一行 currentY += rowHeight + 5; // 添加一些间距 }); // 更新文档的当前Y位置 doc.y = currentY; } }); } // 结束文档 doc.end(); // 监听完成事件 writeStream.on('finish', () => { resolve(filePath); }); // 监听错误事件 writeStream.on('error', (error) => { reject(error); }); }); } catch (error) { console.error('服务层: 生成PDF失败', error); throw error; } } /** * 初始化文件相关IPC服务 * @param {ipcMain} ipcMain - Electron IPC主进程实例 */ async function initFileIpc(ipcMain) { // 测试用接口 ipcMain.handle('file-test', async () => { try { // 测试用 return '文件服务测试成功'; } catch (error) { console.error('服务层: 文件测试失败:', error); return { success: false, message: error.message }; } }); // 生成PDF文件接口 ipcMain.handle('file-generate-pdf', async (event, pdfData, fileName) => { try { const filePath = await generatePdfService(pdfData, fileName); return { success: true, filePath }; } catch (error) { console.error('服务层: 生成PDF失败:', error); return { success: false, message: error.message }; } }); // 生成试卷PDF文件接口 ipcMain.handle('file-generate-paper-pdf', async (event, jsonString) => { try { const filePath = await generatePaperPdf(jsonString); return { success: true, filePath }; } catch (error) { console.error('服务层: 生成试卷PDF失败:', error); return { success: false, message: error.message }; } }); } /** * 获取应用保存目录,适配不同操作系统和环境 * @returns {string} 保存目录路径 */ function getAppSaveDir() { // 判断是否为开发环境 const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged; if (isDev) { // 开发环境:使用项目根目录 const outputDir = path.join(process.cwd(), 'output'); if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } return outputDir; } else { // 检测是否为便携模式 const exePath = app.getPath('exe'); const appDir = path.dirname(exePath); const portableFlagPath = path.join(appDir, 'portable.txt'); const isPortable = fs.existsSync(portableFlagPath); // 便携模式:使用应用根目录 if (isPortable) { return appDir; } else { // 非便携模式:使用应用同级的output目录 const outputDir = path.join(appDir, 'output'); if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } return outputDir; } } } /** * 获取用户桌面目录路径 * @returns {string} 用户桌面绝对路径 */ function getDesktopDir() { try { // 使用Electron的app.getPath方法获取桌面路径 return app.getPath('desktop'); } catch (error) { console.error('获取桌面路径失败:', error); // 发生错误时,返回当前工作目录作为备选 return process.cwd(); } } /** * 将文件复制到用户桌面 * @param {string} filePath - 要复制的文件的绝对路径 * @returns {Promise} */ async function copyToDesk(filePath) { try { const desktopDir = getDesktopDir(); const fileName = path.basename(filePath); const destPath = path.join(desktopDir, fileName); // 使用fs.promises进行文件复制 await fs.promises.copyFile(filePath, destPath); console.log(`文件已成功复制到桌面: ${destPath}`); return destPath; } catch (error) { console.error('复制文件到桌面失败:', error); throw error; } } /** * 生成试卷PDF文件 * @param {string} jsonString - 包含试卷信息的JSON字符串 * @returns {Promise} - 生成的PDF文件绝对路径 */ async function generatePaperPdf(jsonString) { try { // 解析JSON字符串 const paperData = JSON.parse(jsonString); // 提取考生信息 const { examinee } = paperData; const examineeName = examinee.examinee_name; const idCard = examinee.examinee_id_card; const admissionTicket = examinee.examinee_admission_ticket; // 提取考试时间信息 const startTime = paperData.paper_start_time; const endTime = paperData.paper_end_time; // 截取考试日期 (假设格式为 'YYYY-MM-DD HH:mm:ss') const examDate = startTime.split(' ')[0]; // 计算用时(分钟) const start = new Date(startTime.replace(/-/g, '/')); const end = new Date(endTime.replace(/-/g, '/')); const durationMinutes = Math.round((end - start) / (1000 * 60)); // 提取试卷信息 // 计算总题量(每个question下的choice题和fill_blank题的数量之和) let totalQuestions = 0; paperData.questions.forEach(question => { if (question.choices && question.choices.length > 0) { totalQuestions += question.choices.length; } if (question.blanks && question.blanks.length > 0) { totalQuestions += question.blanks.length; } }); const totalScore = paperData.paper_score; const realScore = paperData.paper_score_real; // 对questions列表按照question_type和id进行排序 paperData.questions.sort((a, b) => { // 先按题型排序 if (a.question_type !== b.question_type) { return a.question_type.localeCompare(b.question_type); } // 再按id排序 return a.id - b.id; }); // 生成文件名 // 格式化paper_end_time,移除特殊字符 const endDate = new Date(endTime.replace(/-/g, '/')); const formattedEndTime = [ endDate.getFullYear(), String(endDate.getMonth() + 1).padStart(2, '0'), String(endDate.getDate()).padStart(2, '0'), String(endDate.getHours()).padStart(2, '0'), String(endDate.getMinutes()).padStart(2, '0'), String(endDate.getSeconds()).padStart(2, '0') ].join(''); const fileName = `${examineeName}_${idCard}_${formattedEndTime}.pdf`; // 获取保存路径 const appDir = getAppSaveDir(); // 确保目录存在 if (!fs.existsSync(appDir)) { fs.mkdirSync(appDir, { recursive: true }); } const filePath = path.join(appDir, fileName); // 创建PDF文档,保留默认边距 const doc = new PDFDocument({ size: 'A4', margin: 50 }); // 加载中文字体 - 直接使用文件开头定义的全局变量 let chineseFontLoaded = false; let currentFont = null; // 专门的字体加载函数 function loadMicrosoftYaHeiFont() { try { // 只加载微软雅黑字体 if (fs.existsSync(msyhFontPath)) { doc.registerFont('MicrosoftYaHei', msyhFontPath); doc.font('MicrosoftYaHei'); currentFont = 'MicrosoftYaHei'; chineseFontLoaded = true; console.log('成功加载msyh.ttf字体'); return true; } else { console.error('微软雅黑字体文件不存在:', msyhFontPath); return false; } } catch (error) { console.error('加载微软雅黑字体失败:', error); return false; } } // 添加字体加载状态日志 console.log('尝试加载字体:'); console.log('- 微软雅黑字体:', msyhFontPath, '存在?', fs.existsSync(msyhFontPath)); // 尝试加载字体 if (!loadMicrosoftYaHeiFont()) { console.warn('无法加载微软雅黑字体,可能导致中文显示异常'); // 在macOS上尝试使用系统字体 if (process.platform === 'darwin') { try { doc.font('Arial Unicode MS'); currentFont = 'Arial Unicode MS'; chineseFontLoaded = true; console.log('使用系统默认字体 Arial Unicode MS'); } catch (error) { console.error('加载系统字体失败:', error); } } } // 保存到文件 const writeStream = fs.createWriteStream(filePath); doc.pipe(writeStream); // 添加标题 // 微软雅黑没有单独的粗体,使用普通字体但加大字号 doc.fontSize(24).text('机考试卷', { align: 'center' }).moveDown(2); // 恢复常规字体 if (currentFont) { doc.font(currentFont); } // 辅助函数:绘制表格 function drawTable(headers, rows, cellWidth = 120, baseCellHeight = 25) { // 从左边距开始 const marginLeft = doc.page.margins.left; let currentY = doc.y; const fontSize = 12; const pageWidth = doc.page.width - doc.page.margins.left - doc.page.margins.right; // 计算文本在指定宽度内的行数 const calculateLines = (text, width) => { const charsPerLine = Math.floor(width / (fontSize / 2)); const lines = []; let currentText = text; while (currentText.length > 0) { let splitIndex = Math.min(currentText.length, charsPerLine); if (currentText.length > splitIndex && currentText[splitIndex] !== ' ') { const lastSpace = currentText.lastIndexOf(' ', splitIndex); if (lastSpace > 0) { splitIndex = lastSpace; } } lines.push(currentText.substring(0, splitIndex).trim()); currentText = currentText.substring(splitIndex).trim(); } return lines; }; // 确保表格不会超出页面宽度 const totalTableWidth = headers.length * cellWidth; const adjustedCellWidth = totalTableWidth > pageWidth ? pageWidth / headers.length : cellWidth; // 绘制表头 headers.forEach((header, i) => { const lines = calculateLines(header, adjustedCellWidth - 10); const cellHeight = Math.max(baseCellHeight, lines.length * 15); doc.rect(marginLeft + i * adjustedCellWidth, currentY, adjustedCellWidth, cellHeight).stroke(); // 微软雅黑没有单独的粗体,使用普通字体 doc.fontSize(fontSize).text(header, marginLeft + i * adjustedCellWidth + 5, currentY + (cellHeight - lines.length * 15) / 2, { width: adjustedCellWidth - 10, height: cellHeight - 10 }); }); // 移动到下一行(无间隙) const headerLines = headers.map(header => calculateLines(header, adjustedCellWidth - 10).length); const maxHeaderLines = Math.max(...headerLines); currentY += Math.max(baseCellHeight, maxHeaderLines * 15); // 绘制行 rows.forEach(row => { const rowLines = row.map(cell => calculateLines(cell.toString(), adjustedCellWidth - 10).length); const maxRowLines = Math.max(...rowLines); const rowHeight = Math.max(baseCellHeight, maxRowLines * 15); row.forEach((cell, i) => { doc.rect(marginLeft + i * adjustedCellWidth, currentY, adjustedCellWidth, rowHeight).stroke(); const cellLines = calculateLines(cell.toString(), adjustedCellWidth - 10); doc.fontSize(fontSize).text(cell.toString(), marginLeft + i * adjustedCellWidth + 5, currentY + (rowHeight - cellLines.length * 15) / 2, { width: adjustedCellWidth - 10, height: rowHeight - 10 }); }); currentY += rowHeight; // 无间隙 }); doc.y = currentY; } // 辅助函数:添加base64图片 function addBase64Image(base64String, maxWidth = 400, maxHeight = 300) { try { // 移除base64前缀 const base64Data = base64String.replace(/^data:image\/\w+;base64,/, ''); // 将base64转换为缓冲区 const imageBuffer = Buffer.from(base64Data, 'base64'); // 获取图片尺寸 const image = doc.openImage(imageBuffer); // 计算缩放比例 const scale = Math.min(maxWidth / image.width, maxHeight / image.height, 1); // 添加图片,从左边距开始 doc.image(image, doc.page.margins.left, doc.y, { width: image.width * scale, height: image.height * scale }); // 移动文档指针 doc.y += image.height * scale + 10; } catch (error) { console.error('添加图片失败:', error); doc.fontSize(10).text('图片加载失败', doc.page.margins.left, doc.y).moveDown(); } } // 绘制考生信息表格 drawTable( ['姓名', '身份证号', '准考证号'], [[examineeName, idCard, admissionTicket]] ); doc.moveDown(); // 绘制考试信息表格 drawTable( ['考试日期', '开始时间', '结束时间', '用时(分钟)'], [[examDate, startTime.split(' ')[1], endTime.split(' ')[1], durationMinutes.toString()]] ); doc.moveDown(); // 绘制试卷信息表格 drawTable( ['总题量', '试卷总分', '考试得分'], [[totalQuestions.toString(), totalScore.toString(), realScore.toString()]] ); // 留2行间距 doc.moveDown(2); // 添加题目信息 paperData.questions.forEach((question, index) => { // 题目类型和描述 // 上面留3行间距 doc.moveDown(3); // 微软雅黑没有单独的粗体,使用普通字体但加大字号 doc.fontSize(14).text(`第 ${index + 1} 题 (${question.question_type_name})`, doc.page.margins.left).moveDown(1); // 增加题干编号与上方内容的间距 if (currentFont) { doc.font(currentFont); } // 确保题干描述从左边距开始 doc.fontSize(12).text(question.question_description, { x: doc.page.margins.left, width: doc.page.width - doc.page.margins.left - doc.page.margins.right }); // 留1行间距 doc.moveDown(1); // 添加图片 if (question.images && question.images.length > 0) { // doc.fontSize(12).text('图片:', doc.page.margins.left).moveDown(); question.images.forEach((image) => { if (image.image_base64) { addBase64Image(image.image_base64); } else { doc.fontSize(10).text(`图片: ${image.image_name || '无名称'}`, doc.page.margins.left).moveDown(); } }); } // 添加数据集 if (question.datasets && question.datasets.length > 0) { doc.fontSize(12).text('数据集:', doc.page.margins.left).moveDown(); question.datasets.forEach((dataset, dataIndex) => { doc.fontSize(10).text(`数据集 ${dataIndex + 1} (${dataset.dataset_name || '无名称'})`, doc.page.margins.left).moveDown(); // 尝试解析数据集数据为表格 try { const datasetData = JSON.parse(dataset.dataset_data); if (Array.isArray(datasetData) && datasetData.length > 0) { // 假设第一行是表头 const headers = Object.keys(datasetData[0]); const rows = datasetData.map(item => headers.map(header => item[header])); // 缩小单元格宽度以适应页面 drawTable(headers, rows, 80, 20); } } catch (error) { console.error('解析数据集失败:', error); doc.fontSize(10).text(`数据集内容: ${dataset.dataset_data.substring(0, 100)}...`, doc.page.margins.left).moveDown(); } }); } // 留1行间距 // doc.moveDown(1); // 添加填空题 if (question.blanks && question.blanks.length > 0) { question.blanks.forEach((blank, blankIndex) => { // 微软雅黑没有单独的粗体,使用普通字体但加大字号 // 对于第一个问题,减少与题干的间距 // if (blankIndex === 0) { // doc.moveUp(5); // 向上调整一点,减少与题干的间距 // } doc.fontSize(12).text(`填空 ${blankIndex + 1}`, doc.page.margins.left).moveDown(); if (currentFont) { doc.font(currentFont); } // 确保问题描述从左边距开始 doc.fontSize(12).text(blank.blank_description, { x: doc.page.margins.left, width: doc.page.width - doc.page.margins.left - doc.page.margins.right }).moveDown(); drawTable( ['正确答案', '考生答案', '分值', '得分'], [ [ Array.isArray(blank.correct_answers) ? blank.correct_answers.join(', ') : blank.correct_answers, Array.isArray(blank.examinee_answers) ? blank.examinee_answers.join(', ') : blank.examinee_answers, blank.score.toString(), blank.score_real.toString() ] ], 100 ); doc.moveDown(1); }); } // 添加选择题 if (question.choices && question.choices.length > 0) { question.choices.forEach((choice, choiceIndex) => { // 微软雅黑没有单独的粗体,使用普通字体但加大字号 // 对于第一个问题,减少与题干的间距 // if (choiceIndex === 0) { // doc.moveUp(5); // 向上调整一点,减少与题干的间距 // } doc.fontSize(12).text(`选择题 ${choiceIndex + 1}`, doc.page.margins.left).moveDown(); if (currentFont) { doc.font(currentFont); } // 确保问题描述从左边距开始 doc.fontSize(12).text(choice.choice_description, { x: doc.page.margins.left, width: doc.page.width - doc.page.margins.left - doc.page.margins.right }).moveDown(); // 显示选项 if (choice.choice_options && choice.choice_options.length > 0) { const optionsText = choice.choice_options.map((option, i) => { const optionLabel = String.fromCharCode(65 + i); // A, B, C, D... return `${optionLabel}. ${option}`; }).join(' '); // 使用多个空格作为选项间的间距 doc.fontSize(10).text(`选项: ${optionsText}`, doc.page.margins.left); } // 留一行间距 doc.moveDown(1); drawTable( ['正确答案', '考生答案', '分值', '得分'], [ [ Array.isArray(choice.correct_answers) ? choice.correct_answers.join(', ') : choice.correct_answers, Array.isArray(choice.examinee_answers) ? choice.examinee_answers.join(', ') : choice.examinee_answers, choice.score.toString(), choice.score_real.toString() ] ], 100 ); doc.moveDown(1); }); } // 分页处理 if (doc.y > 700) { doc.addPage(); } }); // 结束文档 doc.end(); return new Promise((resolve, reject) => { writeStream.on('finish', async () => { try { // 调用copyToDesk方法将文件复制到桌面,获取新的路径 const newFilePath = await copyToDesk(filePath); resolve(newFilePath); } catch (error) { reject(error); } }); writeStream.on('error', reject); }); } catch (error) { console.error('生成PDF失败:', error); throw error; } } // 导出使用CommonJS格式 module.exports = { initFileIpc, createFileService, generatePaperPdf, generatePdfService };