diff --git a/output/试考者2_2_20250907.pdf b/output/试考者2_2_20250907.pdf new file mode 100644 index 0000000..b7da3c2 Binary files /dev/null and b/output/试考者2_2_20250907.pdf differ diff --git a/output/试考者5_5_20250908062131.pdf b/output/试考者5_5_20250908062131.pdf new file mode 100644 index 0000000..91feff1 Binary files /dev/null and b/output/试考者5_5_20250908062131.pdf differ diff --git a/output/试考者6_6_20250908061538.pdf b/output/试考者6_6_20250908061538.pdf new file mode 100644 index 0000000..29951c5 Binary files /dev/null and b/output/试考者6_6_20250908061538.pdf differ diff --git a/output/试考者7_7_20250908060404.pdf b/output/试考者7_7_20250908060404.pdf new file mode 100644 index 0000000..196436d Binary files /dev/null and b/output/试考者7_7_20250908060404.pdf differ diff --git a/src/background/service/fileService.js b/src/background/service/fileService.js index 6cfe931..cc0b661 100644 --- a/src/background/service/fileService.js +++ b/src/background/service/fileService.js @@ -1,23 +1,445 @@ -// 在文件顶部添加fs模块导入 +// 转换为CommonJS模块语法 - 确保所有导入语句在文件最前面 const path = require('path'); const fs = require('fs'); const PDFDocument = require('pdfkit'); const { app } = require('electron'); -// 字体路径 - 简化为只关注宋体字体 -const FONT_PATH = path.join(__dirname, '..', 'font'); -const simsunTtfPath = path.join(FONT_PATH, 'simsun.ttf'); -const simsunTtcPath = path.join(FONT_PATH, 'simsun.ttc'); +// 不依赖__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(); +// 优先使用SourceHanSansSC字体 +const primaryFontPath = path.join(FONT_PATH, 'SourceHanSansSC-Regular.otf'); +const boldFontPath = path.join(FONT_PATH, 'SourceHanSansSC-Bold.otf'); +// 保留宋体作为备选 +const simsunPath = path.join(FONT_PATH, 'simsun.ttf'); +const fallbackFontPath = path.join(FONT_PATH, 'simsun.ttc'); // 备选字体路径 + +// 输出找到的字体路径供调试 +console.log('FONT_PATH:', FONT_PATH); +console.log('primaryFontPath:', primaryFontPath); +console.log('boldFontPath:', boldFontPath); +console.log('simsunPath:', simsunPath); +console.log('fallbackFontPath:', fallbackFontPath); /** - * 获取用户桌面路径 - * @returns {string} 桌面路径 + * 服务层:获取所有考生列表 + * @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 boldFontLoaded = false; + + // 保存当前字体 + let currentFont = null; + + // 修改字体加载逻辑 + try { + // 1. 尝试加载SourceHanSansSC常规字体 + if (fs.existsSync(primaryFontPath)) { + try { + doc.registerFont('SourceHanSans', primaryFontPath); + doc.font('SourceHanSans'); + currentFont = 'SourceHanSans'; + chineseFontLoaded = true; + console.log('成功加载SourceHanSansSC-Regular.otf字体'); + } catch (error) { + console.error('加载SourceHanSansSC-Regular.otf字体失败:', error); + } + } + + // 2. 尝试加载SourceHanSansSC粗体字体(用于标题) + if (fs.existsSync(boldFontPath)) { + try { + doc.registerFont('SourceHanSansBold', boldFontPath); + boldFontLoaded = true; + console.log('成功加载SourceHanSansSC-Bold.otf字体'); + } catch (error) { + console.error('加载SourceHanSansSC-Bold.otf字体失败:', error); + } + } + + // 3. 如果SourceHanSansSC字体加载失败,尝试加载宋体 + if (!chineseFontLoaded) { + if (fs.existsSync(simsunPath)) { + try { + doc.registerFont('SimSun', simsunPath); + doc.font('SimSun'); + currentFont = 'SimSun'; + chineseFontLoaded = true; + console.log('成功加载simsun.ttf字体'); + } catch (ttfError) { + console.error('加载simsun.ttf字体失败:', ttfError); + // 尝试加载备选TTC字体 + if (fs.existsSync(fallbackFontPath)) { + try { + doc.registerFont('SimSun', fallbackFontPath); + doc.font('SimSun'); + currentFont = 'SimSun'; + chineseFontLoaded = true; + console.log('成功加载simsun.ttc字体'); + } catch (ttcError) { + console.error('加载simsun.ttc字体失败:', ttcError); + } + } + } + } else { + console.warn(`未找到simsun.ttf字体文件: ${simsunPath}`); + // 检查是否有备选TTC字体 + if (fs.existsSync(fallbackFontPath)) { + try { + doc.registerFont('SimSun', fallbackFontPath); + doc.font('SimSun'); + currentFont = 'SimSun'; + chineseFontLoaded = true; + console.log('成功加载simsun.ttc字体'); + } catch (error) { + console.error('加载simsun.ttc字体失败:', error); + } + } + } + } + + if (!chineseFontLoaded) { + 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); + } + } + } + } catch (error) { + console.error('加载字体失败:', error); + console.warn('将使用默认字体,可能导致中文显示异常'); + } + + // 保存到文件 + const writeStream = fs.createWriteStream(filePath); + doc.pipe(writeStream); + + // 设置文档标题 + if (pdfData.title) { + // 保存当前字体 + const tempFont = currentFont; + try { + // 尝试使用粗体字体 + if (boldFontLoaded) { + doc.fontSize(20).font('SourceHanSansBold').text(pdfData.title, { align: 'center' }).moveDown(); + } else if (process.platform === 'darwin') { + doc.fontSize(20).font('Arial Unicode MS Bold').text(pdfData.title, { align: 'center' }).moveDown(); + } else { + 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 { + // 尝试使用SourceHanSansBold粗体字体 + if (boldFontLoaded) { + doc.fontSize(item.fontSize || 16).font('SourceHanSansBold').text(item.text, item.options || {}).moveDown(); + } else if (process.platform === 'darwin') { + doc.fontSize(item.fontSize || 16).font('Arial Unicode MS Bold').text(item.text, item.options || {}).moveDown(); + } else { + // 如果没有粗体字体,使用当前字体加大字号 + 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(); + + // 保存当前字体 + const tempFont = currentFont; + try { + if (boldFontLoaded) { + doc.font('SourceHanSansBold'); + } else if (process.platform === 'darwin') { + doc.font('Arial Unicode MS Bold'); + } + // 垂直居中显示文本 + doc.fontSize(fontSize).text(header, marginLeft + i * cellWidth + 5, currentY + (cellHeight - lines.length * 15) / 2, { + width: cellWidth - 10, + height: cellHeight - 10 + }); + } catch (error) { + console.error('设置表头字体失败:', error); + doc.fontSize(fontSize).text(header, marginLeft + i * cellWidth + 5, currentY + (cellHeight - lines.length * 15) / 2, { + width: cellWidth - 10, + height: cellHeight - 10 + }); + } + // 恢复字体 + if (currentFont) { + doc.font(currentFont); + } + }); + // 移动到下一行,考虑最高的表头单元格高度 + 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(); } } @@ -25,7 +447,7 @@ function getDesktopDir() { /** * 将文件复制到用户桌面 * @param {string} filePath - 要复制的文件的绝对路径 - * @returns {Promise} 复制后的文件路径 + * @returns {Promise} */ async function copyToDesk(filePath) { try { @@ -33,6 +455,7 @@ async function copyToDesk(filePath) { const fileName = path.basename(filePath); const destPath = path.join(desktopDir, fileName); + // 使用fs.promises进行文件复制 await fs.promises.copyFile(filePath, destPath); console.log(`文件已成功复制到桌面: ${destPath}`); return destPath; @@ -42,498 +465,438 @@ async function copyToDesk(filePath) { } } -/** - * 获取应用保存目录 - * @returns {string} 保存目录路径 - */ -function getAppSaveDir() { - let saveDir; - - try { - let userDataPath; - try { - userDataPath = app.getPath('userData'); - console.log('用户数据路径:', userDataPath); - } catch (error) { - console.error('获取用户数据路径失败,使用当前目录作为备选:', error); - userDataPath = process.cwd(); - } - - saveDir = path.join(userDataPath, 'output'); - console.log('PDF保存目录:', saveDir); - - // 确保目录存在 - try { - if (!fs.existsSync(saveDir)) { - console.log('创建保存目录:', saveDir); - fs.mkdirSync(saveDir, { recursive: true }); - } - } catch (error) { - console.error('创建保存目录失败:', error); - saveDir = process.cwd(); - console.log('使用备选保存目录:', saveDir); - } - } catch (error) { - console.error('获取保存目录时发生严重错误:', error); - saveDir = process.cwd(); - } - - return saveDir; -} - /** * 生成试卷PDF文件 * @param {string} jsonString - 包含试卷信息的JSON字符串 - * @returns {Promise} - 生成的PDF文件信息 + * @returns {Promise} - 生成的PDF文件绝对路径 */ -async function generatePaperPdfService(jsonString) { +// 在generatePaperPdf函数中,删除重复定义的字体路径变量 +async function generatePaperPdf(jsonString) { try { - console.log('开始生成PDF,收到数据长度:', jsonString.length); + // 解析JSON字符串 + const paperData = JSON.parse(jsonString); - // 解析JSON字符串为对象 - let paperData; - try { - paperData = JSON.parse(jsonString); - } catch (parseError) { - console.error('JSON解析失败:', parseError); - return { - success: false, - message: '试卷数据格式错误', - errorStack: parseError.stack - }; - } + // 提取考生信息 + const { examinee } = paperData; + const examineeName = examinee.examinee_name; + const idCard = examinee.examinee_id_card; + const admissionTicket = examinee.examinee_admission_ticket; - // 验证数据结构 - if (!paperData || typeof paperData !== 'object') { - return { - success: false, - message: '无效的试卷数据' - }; - } + // 提取考试时间信息 + 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)); - // 获取考生和试卷信息,并提供默认值 - const examinee = paperData.examinee || {}; - const paper = paperData.paper || paperData; // 兼容不同的数据结构 + // 提取试卷信息 + // 计算总题量(每个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; - // 安全获取数据,提供默认值 - const examineeName = examinee.examinee_name || '未知考生'; - const idCard = examinee.examinee_id_card || ''; - - // 提取身份证号后几位作为文件名的备选 - const idCardSuffix = idCard ? idCard.slice(-4) : '0000'; - - // 格式化日期为yyyymmdd格式 - const examDate = paper.paper_submit_time ? - new Date(paper.paper_submit_time).toISOString().slice(0, 10).replace(/-/g, '') : - new Date().toISOString().slice(0, 10).replace(/-/g, ''); - - // 生成符合要求的文件名:姓名_身份证号_yyyymmdd.pdf - const safeExamineeName = examineeName || `考生${idCardSuffix}`; - const safeIdCard = idCard || `ID${idCardSuffix}`; - const fileName = `${safeExamineeName}_${safeIdCard}_${examDate}.pdf` - .replace(/[\/:*?"<>|]/g, '_'); - - console.log('生成的PDF文件名:', fileName); - - // 获取保存目录 - const appDir = getAppSaveDir(); - const tempFilePath = path.join(appDir, fileName); - - console.log('PDF临时保存路径:', tempFilePath); - - // 创建PDF文档对象 - const doc = new PDFDocument({ - size: 'A4', - margin: 50, - autoFirstPage: true, - bufferPages: true + // 对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 + }); + + // 加载中文字体 - 直接使用文件开头定义的全局变量,不要重新定义 + // 注意:这里不再重新定义FONT_PATH等变量,直接使用全局变量 let chineseFontLoaded = false; + let boldFontLoaded = false; let currentFont = null; - // 打印字体路径,用于调试 - console.log('字体路径检查:'); - console.log('simsun.ttf:', fs.existsSync(simsunTtfPath)); - console.log('simsun.ttc:', fs.existsSync(simsunTtcPath)); + // 添加字体加载状态日志 + console.log('尝试加载字体:'); + console.log('- 主要字体:', primaryFontPath, '存在?', fs.existsSync(primaryFontPath)); + console.log('- 粗体字体:', boldFontPath, '存在?', fs.existsSync(boldFontPath)); + console.log('- 宋体:', simsunPath, '存在?', fs.existsSync(simsunPath)); + console.log('- 备选字体:', fallbackFontPath, '存在?', fs.existsSync(fallbackFontPath)); + // 尝试加载字体 try { - // 1. 优先尝试加载simsun.ttf字体 - if (fs.existsSync(simsunTtfPath)) { - try { - doc.registerFont('SimSun', simsunTtfPath); - doc.font('SimSun'); - currentFont = 'SimSun'; - chineseFontLoaded = true; - console.log('成功加载simsun.ttf字体'); - } catch (ttfError) { - console.error('加载simsun.ttf字体失败:', ttfError); - } + if (fs.existsSync(primaryFontPath)) { + doc.registerFont('SourceHanSans', primaryFontPath); + doc.font('SourceHanSans'); + currentFont = 'SourceHanSans'; + chineseFontLoaded = true; + console.log('成功加载主要字体'); + } else if (fs.existsSync(simsunPath)) { + doc.registerFont('SimSun', simsunPath); + doc.font('SimSun'); + currentFont = 'SimSun'; + chineseFontLoaded = true; + console.log('成功加载宋体'); + } else if (fs.existsSync(fallbackFontPath)) { + doc.registerFont('SimSun', fallbackFontPath); + doc.font('SimSun'); + currentFont = 'SimSun'; + chineseFontLoaded = true; + console.log('成功加载备选字体'); + } else if (process.platform === 'darwin') { + doc.font('Arial Unicode MS'); + currentFont = 'Arial Unicode MS'; + chineseFontLoaded = true; + console.log('使用系统默认字体 Arial Unicode MS'); } - // 2. 如果simsun.ttf加载失败,尝试加载simsun.ttc字体 - if (!chineseFontLoaded && fs.existsSync(simsunTtcPath)) { - try { - // 对于TTC字体,需要指定字体索引,通常0是常规字体,1是粗体 - doc.registerFont('SimSun', simsunTtcPath, 0); - doc.font('SimSun'); - currentFont = 'SimSun'; - chineseFontLoaded = true; - console.log('成功加载simsun.ttc字体,使用索引0'); - } catch (ttcError) { - console.error('加载simsun.ttc字体失败:', ttcError); - try { - // 尝试使用不同的索引 - doc.registerFont('SimSun', simsunTtcPath, 1); - doc.font('SimSun'); - currentFont = 'SimSun'; - chineseFontLoaded = true; - console.log('成功加载simsun.ttc字体,使用索引1'); - } catch (ttcError2) { - console.error('使用索引1加载simsun.ttc字体也失败:', ttcError2); - } - } + if (fs.existsSync(boldFontPath)) { + doc.registerFont('SourceHanSansBold', boldFontPath); + boldFontLoaded = true; + console.log('成功加载粗体字体'); } if (!chineseFontLoaded) { - 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); - } - } else { - try { - // Windows系统上尝试直接使用'SimSun' - doc.font('SimSun'); - currentFont = 'SimSun'; - chineseFontLoaded = true; - console.log('尝试使用系统SimSun字体'); - } catch (error) { - console.error('无法设置默认字体:', error); - } - } + console.warn('无法加载中文字体,可能导致中文显示异常'); } } catch (error) { - console.error('字体加载过程中发生错误:', error); + console.error('加载字体失败:', error); } - // 返回Promise处理异步操作 - return new Promise((resolve, reject) => { + // 保存到文件 + const writeStream = fs.createWriteStream(filePath); + doc.pipe(writeStream); + + // 添加标题 + if (boldFontLoaded) { + doc.font('SourceHanSansBold'); + } else if (process.platform === 'darwin' && currentFont === 'Arial Unicode MS') { + doc.font('Arial Unicode MS Bold'); + } + 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(); + + if (boldFontLoaded) { + doc.font('SourceHanSansBold'); + } else if (process.platform === 'darwin' && currentFont === 'Arial Unicode MS') { + doc.font('Arial Unicode MS Bold'); + } + + doc.fontSize(fontSize).text(header, marginLeft + i * adjustedCellWidth + 5, currentY + (cellHeight - lines.length * 15) / 2, { + width: adjustedCellWidth - 10, + height: cellHeight - 10 + }); + + if (currentFont) { + doc.font(currentFont); + } + }); + + // 移动到下一行(无间隙) + 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 { - // 保存到文件 - const writeStream = fs.createWriteStream(tempFilePath); - doc.pipe(writeStream); - - // 添加标题 - doc.fontSize(24).text('机考试卷', { align: 'center' }).moveDown(2); - - // 辅助函数:绘制表格 - 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 + 1).text(header, marginLeft + i * adjustedCellWidth + 5, currentY + (cellHeight - lines.length * 15) / 2, { - width: adjustedCellWidth - 10, - height: cellHeight - 10 - }); - // 恢复默认字体大小 - doc.fontSize(fontSize); - }); - - // 移动到下一行 - 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 { - const base64Data = base64String.replace(/^data:image\/\w+;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(); - } - } - - // 获取考试信息 - const startTime = paper.paper_start_time || ''; - const endTime = paper.paper_end_time || ''; - const durationMinutes = paper.paper_minutes || 0; - const totalQuestions = paper.questions ? paper.questions.length : 0; - const totalScore = paper.paper_score || 0; - const realScore = paper.paper_score_real || 0; - const admissionTicket = examinee.examinee_admission_ticket || ''; - - // 绘制考生信息表格 - drawTable( - ['姓名', '身份证号', '准考证号'], - [[examineeName, idCard, admissionTicket]] - ); - doc.moveDown(); - - // 绘制考试信息表格 - drawTable( - ['考试日期', '开始时间', '结束时间', '用时(分钟)'], - [[examDate, startTime ? startTime.split(' ')[1] : '', endTime ? endTime.split(' ')[1] : '', durationMinutes.toString()]] - ); - doc.moveDown(); - - // 绘制试卷信息表格 - drawTable( - ['总题量', '试卷总分', '考试得分'], - [[totalQuestions.toString(), totalScore.toString(), realScore.toString()]] - ); - doc.moveDown(2); - - // 添加题目信息(如果有) - if (paper.questions && paper.questions.length > 0) { - // 对questions列表按照question_type和id进行排序 - paper.questions.sort((a, b) => { - if (a.question_type !== b.question_type) { - return a.question_type.localeCompare(b.question_type); - } - return a.id - b.id; - }); - - paper.questions.forEach((question, index) => { - // 题目类型和描述 - 使用稍大的字体表示重点 - doc.fontSize(14).text(`第 ${index + 1} 题 (${question.question_type_name || question.question_type})`, doc.page.margins.left).moveDown(); - // 确保题干描述从左边距开始 - doc.fontSize(12).text(question.question_description || '', { - x: doc.page.margins.left, - width: doc.page.width - doc.page.margins.left - doc.page.margins.right - }).moveDown(); - - // 添加图片 - 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(); - } - }); - } - - // 添加填空题 - if (question.blanks && question.blanks.length > 0) { - question.blanks.forEach((blank, blankIndex) => { - // 使用稍大字体突出显示填空标题 - doc.fontSize(13).text(`填空 ${blankIndex + 1}`, doc.page.margins.left).moveDown(); - // 确保问题描述从左边距开始 - 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 || 0).toString(), - (blank.score_real || 0).toString() - ] - ], - 100 - ); - }); - } - - // 添加选择题 - if (question.choices && question.choices.length > 0) { - question.choices.forEach((choice, choiceIndex) => { - // 使用稍大字体突出显示选择题标题 - doc.fontSize(13).text(`选择题 ${choiceIndex + 1}`, doc.page.margins.left).moveDown(); - // 确保问题描述从左边距开始 - 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).moveDown(); - } - 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 || 0).toString(), - (choice.score_real || 0).toString() - ] - ], - 100 - ); - }); - } - - // 分页处理 - if (doc.y > 700) { - doc.addPage(); - // 在新页面上重新设置字体,确保字体设置正确应用 - if (currentFont) { - doc.font(currentFont); - } - } - }); - } - - // 结束文档 - doc.end(); - - // 处理流事件 - writeStream.on('finish', async () => { - try { - // 复制文件到桌面 - const desktopFilePath = await copyToDesk(tempFilePath); - console.log('PDF生成成功,文件已保存到桌面:', desktopFilePath); - resolve({ filePath: desktopFilePath }); - } catch (copyError) { - console.error('复制文件到桌面失败,返回临时文件路径:', copyError); - // 如果复制到桌面失败,返回临时文件路径 - resolve({ filePath: tempFilePath }); - } - }); - - writeStream.on('error', (error) => { - console.error('PDF写入失败:', error); - reject(error); + // 移除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('PDF生成过程中发生错误:', error); - reject(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()]] + ); + doc.moveDown(2); + + // 添加题目信息 + paperData.questions.forEach((question, index) => { + // 题目类型和描述 + if (boldFontLoaded) { + doc.font('SourceHanSansBold'); + } else if (process.platform === 'darwin' && currentFont === 'Arial Unicode MS') { + doc.font('Arial Unicode MS Bold'); + } + doc.fontSize(14).text(`第 ${index + 1} 题 (${question.question_type_name})`, doc.page.margins.left).moveDown(); + + 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 + }).moveDown(); + + // 添加图片 + 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(); + } + }); + } + + // 添加填空题 + if (question.blanks && question.blanks.length > 0) { + question.blanks.forEach((blank, blankIndex) => { + if (boldFontLoaded) { + doc.font('SourceHanSansBold'); + } else if (process.platform === 'darwin' && currentFont === 'Arial Unicode MS') { + doc.font('Arial Unicode MS Bold'); + } + 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 + ); + }); + } + + // 添加选择题 + if (question.choices && question.choices.length > 0) { + question.choices.forEach((choice, choiceIndex) => { + if (boldFontLoaded) { + doc.font('SourceHanSansBold'); + } else if (process.platform === 'darwin' && currentFont === 'Arial Unicode MS') { + doc.font('Arial Unicode MS Bold'); + } + 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).moveDown(); + } + 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 + ); + }); + } + + // 分页处理 + 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); - return { - success: false, - message: `生成PDF失败: ${error.message}`, - errorStack: error.stack - }; + console.error('生成PDF失败:', error); + throw error; } } -/** - * 初始化文件服务IPC处理程序 - * @param {*} ipcMain - Electron的ipcMain实例 - */ -function initFileIpc(ipcMain) { - // 生成试卷PDF - ipcMain.handle('file-generate-paper-pdf', async (event, jsonString) => { - try { - return await generatePaperPdfService(jsonString); - } catch (error) { - console.error('生成试卷PDF失败:', error); - return { - success: false, - message: `生成PDF失败: ${error.message}` - }; - } - }); -} - -// 导出模块 -exports = module.exports = { - generatePaperPdfService, - initFileIpc +// 导出使用CommonJS格式 +module.exports = { + initFileIpc, + createFileService, + generatePaperPdf, + generatePdfService }; \ No newline at end of file diff --git a/src/views/user/EndView.vue b/src/views/user/EndView.vue index 08a9556..b226d69 100644 --- a/src/views/user/EndView.vue +++ b/src/views/user/EndView.vue @@ -276,9 +276,9 @@ export default { if (result && result.success) { // 更新store中的试卷信息 - if (result.data) { - this.$store.commit('setPaper', result.data) - } + // if (result.data) { + // this.$store.commit('setPaper', result.data) + // } this.$message.success('判卷完成!'); } else { console.error('判卷失败:', result?.message || '未知错误'); diff --git a/src/views/user/ExamingView.vue b/src/views/user/ExamingView.vue index 695284b..3af8222 100644 --- a/src/views/user/ExamingView.vue +++ b/src/views/user/ExamingView.vue @@ -324,9 +324,9 @@ export default { // 如果无法获取试卷信息,创建一个默认的试卷对象以避免页面白屏 this.$store.commit('setPaper', { id: paperId, - paper_score: 100, - paper_minutes: 120, - paper_minutes_min: 60 + paper_score: 0, + paper_minutes: 0, + paper_minutes_min: 0 }) this.canSubmit = false } @@ -335,9 +335,9 @@ export default { // 错误情况下也创建一个默认的试卷对象 this.$store.commit('setPaper', { id: paperId, - paper_score: 100, - paper_minutes: 120, - paper_minutes_min: 60 + paper_score: 0, + paper_minutes: 0, + paper_minutes_min: 0 }) this.canSubmit = false }