801 lines
28 KiB
JavaScript
801 lines
28 KiB
JavaScript
// 转换为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<Array>} 考生列表
|
||
*/
|
||
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<string>} 文件保存路径
|
||
*/
|
||
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<void>}
|
||
*/
|
||
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<string>} - 生成的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];
|
||
// 计算用时(分钟)- 直接使用paper_duration_seconds字段
|
||
let durationMinutes = 0;
|
||
if (paperData.paper_duration_seconds && paperData.paper_duration_seconds > 0) {
|
||
// 直接从paper_duration_seconds字段获取并转换为分钟
|
||
durationMinutes = Math.round(paperData.paper_duration_seconds / 60);
|
||
} else {
|
||
// 备选方案:如果paper_duration_seconds不存在或无效,使用原来的计算方式
|
||
const start = new Date(startTime.replace(/-/g, '/'));
|
||
const end = new Date(endTime.replace(/-/g, '/'));
|
||
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
|
||
}; |