config = getConfig('/mzl/modules/phpoffice/config/excel_import.php'); } /** * 导入Excel数据 */ public function importToData(string $template_key, string $filePath): array { $config = $this->config[$template_key] ?? null; if (!$config) { throw new BusinessException("未找到模板配置:$template_key"); } $spreadsheet = IOFactory::load($filePath); $sheet = $spreadsheet->getActiveSheet(); $rows = $sheet->toArray(null, true, true, true); $callback = $this->callback($template_key) ?? null; return $this->toData($rows, $config['column_map'], $callback); } /** * 条件回调 */ public function callback(string $template_key) { switch ($template_key) { case 'student': $callback = function ($row, $i) { return $i < 3 || empty($row['C']); }; break; case 'user': $callback = function ($row, $i) { return $i < 2 || empty($row['D']); }; break; } return $callback; } /** * 获取数据 */ public function toData(array $rows = [], array $columnMap = [], $callback = null) { $data = []; foreach ($rows as $i => $row) { if ($callback && $callback($row, $i)) { continue; } $item = []; foreach ($columnMap as $excelKey => $dbField) { $item[$dbField] = trim((string)($row[$excelKey] ?? '')); } $data[] = $item; } return $data; } /** * 导入practice */ public function importPractice(string $filePath) { Db::beginTransaction(); try { $spreadsheet = IOFactory::load($filePath); $sheet = $spreadsheet->getSheet(0); $rows = $sheet->toArray(); // 解析标题行 $titleRow = trim($rows[0][0]); preg_match('/(.*?)\s+([A-Za-z0-9-]+\/[A-Za-z0-9]+)$/', $titleRow, $matches); $practice = PracticeModel::create([ 'sn' => generateRandomSN(), 'title' => $matches[1] ?? '未知标题', 'code' => $matches[2] ?? '未编号' ]); $currentItem = null; foreach ($rows as $index => $row) { // 跳过表头前两行(标题行+列头行) if ($index < 2 || empty(array_filter($row))) continue; // 处理主检查项 if (!empty($row[0]) && is_numeric($row[0])) { $currentItem = PracticeItemsModel::create([ 'practice_id' => $practice->id, 'index' => $row[0], 'exam_content' => $row[1], 'scoring_element' => $row[2], 'score_allocation' => $row[3] ]); } // 处理评分标准 if ($currentItem && !empty($row[4])) { PracticeItemsScoreModel::create([ 'practice_item_id' => $currentItem->id, 'scoring_criteria' => $row[4], 'max_deduction' => $row[5], 'single_deduction' => $row[6] ]); } } DB::commit(); return true; } catch (\Throwable $e) { DB::rollBack(); return false; } } // ====== ====== ====== 公共方法 ====== ====== ====== private function setColumnWidths($sheet, array $widths) { foreach ($widths as $column => $width) { $sheet->getColumnDimension($column)->setWidth($width); } } private function setRowHeights($sheet, array $heights) { foreach ($heights as $row => $height) { $sheet->getRowDimension($row)->setRowHeight($height); } } private function centerAlignAllCells($sheet) { $maxRow = $sheet->getHighestRow(); $maxCol = $sheet->getHighestColumn(); $sheet->getStyle("A1:{$maxCol}{$maxRow}")->applyFromArray([ 'alignment' => [ 'horizontal' => \PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER, 'vertical' => \PhpOffice\PhpSpreadsheet\Style\Alignment::VERTICAL_CENTER, ] ]); } private function setGlobalFont($sheet, $start = null, $end = null, $name = '宋体', $size = 10) { if ($start && $end) { $fw = "{$start}:{$end}"; } else { $maxRow = $sheet->getHighestRow(); $maxCol = $sheet->getHighestColumn(); $fw = "A1:{$maxCol}{$maxRow}"; } $sheet->getStyle($fw)->applyFromArray([ 'font' => [ 'name' => $name, 'size' => $size, ], ]); } // ====== ====== ====== 公共方法 ====== ====== ====== /** * 导出 班级总结 */ public function exportClassSummary2(array $data, string $outputPath = '班级总结', ?string $signatureImage = null) { $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); // ====== 1. 标题 & 基础信息 ====== $sheet->setCellValue('A1', $data['title'] ?? 'XX(班级名称)班级信息'); // 标题行立即合并 $sheet->mergeCells('A1:C1'); $basicInfo = [ ['开班地点', 'location'], ['开班类型', 'type'], ['培训项目', 'program'], ['授课老师', 'teacher'], ['培训时间', 'time'], ['参培学员', 'students'], ['合格人员', 'passed'], ['通过率', 'pass_rate'], ]; $row = 2; foreach ($basicInfo as $info) { [$label, $key] = $info; $sheet->setCellValue("A{$row}", $label); $sheet->setCellValue("B{$row}", $data[$key] ?? ''); $sheet->mergeCells("B{$row}:C{$row}"); $row++; } // ====== 2. 理论考试 ====== if (! empty($data['theory_exams'])) { foreach ($data['theory_exams'] as $idx => $exam) { $subject = $exam['subject'] ?? "科目" . ($idx + 1); $fields = [ '考试时间' => 'time', '考试地点' => 'location', '主监考' => 'main_invigilator', '监考人员' => 'invigilators', '不及格人数' => 'fail_count', '合格率' => 'pass_rate', ]; $blockSize = count($fields); $startRow = $row; $endRow = $row + $blockSize - 1; $sheet->mergeCells("A{$startRow}:A{$endRow}"); $sheet->setCellValue("A{$startRow}", "理论考试信息({$subject})"); $r = $startRow; foreach ($fields as $label => $key) { $sheet->setCellValue("B{$r}", $label); $sheet->setCellValue("C{$r}", $exam[$key] ?? ''); $r++; } $row = $endRow + 1; } } // ====== 3. 实操考试 ====== if (! empty($data['practice_exams'])) { foreach ($data['practice_exams'] as $idx => $exam) { $subject = $exam['subject'] ?? "实操" . ($idx + 1); $fields = [ '考试科目' => 'subject', '考试时间' => 'time', '考试地点' => 'location', '不及格人数' => 'fail_count', '合格率' => 'pass_rate', ]; $blockSize = count($fields); $startRow = $row; $endRow = $row + $blockSize - 1; $sheet->mergeCells("A{$startRow}:A{$endRow}"); $sheet->setCellValue("A{$startRow}", "实操考试信息({$subject})"); $r = $startRow; foreach ($fields as $label => $key) { $sheet->setCellValue("B{$r}", $label); $sheet->setCellValue("C{$r}", $exam[$key] ?? ''); $r++; } $row = $endRow + 1; } } // ====== 5. 样式:合并、边框、对齐、行高、字体 ====== $lastRow = $row; // 全区域合并边框 $sheet->getStyle("A1:C{$lastRow}")->applyFromArray([ 'borders' => ['allBorders'=>['borderStyle'=>Border::BORDER_THIN]], ]); // 对齐 $maxCol = $sheet->getHighestColumn(); $sheet->getStyle("A1:{$maxCol}{$lastRow}")->applyFromArray([ 'alignment'=>[ 'horizontal'=>\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER, 'vertical' =>\PhpOffice\PhpSpreadsheet\Style\Alignment::VERTICAL_CENTER, ] ]); // 字体 // 标题行 $sheet->getStyle('A1')->applyFromArray([ 'font'=>['name'=>'黑体','size'=>22], ]); // 正文字体 $sheet->getStyle("A2:C{$lastRow}")->applyFromArray([ 'font'=>['name'=>'宋体','size'=>10], ]); // 班主任行单独字号 12 $sheet->getStyle("A{$row}:C{$row}")->applyFromArray([ 'font'=>['name'=>'宋体','size'=>12], ]); // 行高 $sheet->getRowDimension(1)->setRowHeight(36); for ($r = 2; $r < $lastRow; $r++) { $sheet->getRowDimension($r)->setRowHeight(21.75); } $sheet->getRowDimension($lastRow)->setRowHeight(54); // 列宽 $sheet->getColumnDimension('A')->setWidth(30); $sheet->getColumnDimension('B')->setWidth(30); $sheet->getColumnDimension('C')->setWidth(24); // ====== 4. 班主任签名(水平+垂直居中) ====== $sheet->setCellValue("A{$row}", '班主任'); // 合并 B、C 列 $sheet->mergeCells("B{$row}:C{$row}"); if ($signatureImage && file_exists($signatureImage)) { $drawing = new Drawing(); $drawing->setPath($signatureImage); // 先指定高度,等比例缩放宽度 $drawing->setHeight(40); $drawing->setResizeProportional(true); // 锚定到 B{$row} 单元格 $drawing->setCoordinates("B{$row}"); // 必须先 add 到工作表,才能正确计算后续的 getWidth()/getHeight() $drawing->setWorksheet($sheet); // —— 计算区域像素尺寸 —— // 列宽单位转像素近似:1 单位 ≈ 7px // 行高点(pt)转像素近似:1pt ≈ 96/72 px $pxPerWidth = 7; $pxPerPoint = 96 / 72; $widthB = $sheet->getColumnDimension('B')->getWidth(); $widthC = $sheet->getColumnDimension('C')->getWidth(); $totalWidthPx = ($widthB + $widthC) * $pxPerWidth; $rowHeightPt = $sheet->getRowDimension($row)->getRowHeight(); $totalHeightPx = $rowHeightPt * $pxPerPoint; // 这时 getWidth()/getHeight() 才会返回渲染后的像素值 $imageWidthPx = $drawing->getWidth(); $imageHeightPx = $drawing->getHeight(); // 计算居中偏移 $offsetX = max(0, ($totalWidthPx - $imageWidthPx) / 2); $offsetY = max(0, ($totalHeightPx - $imageHeightPx) / 2); $drawing->setOffsetX($offsetX); $drawing->setOffsetY($offsetY); } else { $sheet->setCellValue("B{$row}", $data['class_teacher'] ?? ''); } // 最后保存 $writer = new Xlsx($spreadsheet); $writer->save($outputPath); } public function exportClassSummary(array $data, string $outputPath = '班级总结', ?string $signatureImage = null) { $spreadsheet = $this->buildClassSummarySpreadsheet($data, $signatureImage); $writer = new Xlsx($spreadsheet); $writer->save($outputPath); } /** * 构建班级总结的 Spreadsheet 对象(复用给 XLSX 和 PDF) * * @param array $data * @param string|null $signatureImage * @return Spreadsheet */ protected function buildClassSummarySpreadsheet(array $data, ?string $signatureImage = null): Spreadsheet { $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); // // 1. 标题 & 基础信息 // $sheet->setCellValue('A1', $data['title'] ?? 'XX(班级名称)班级信息'); $sheet->mergeCells('A1:C1'); $basicInfo = [ ['开班地点', 'location'], ['开班类型', 'type'], ['培训项目', 'program'], ['授课老师', 'teacher'], ['培训时间', 'time'], ['参培学员', 'students'], ['合格人员', 'passed'], ['通过率', 'pass_rate'], ]; $row = 2; foreach ($basicInfo as $info) { [$label, $key] = $info; $sheet->setCellValue("A{$row}", $label); $sheet->setCellValue("B{$row}", $data[$key] ?? ''); $sheet->mergeCells("B{$row}:C{$row}"); $row++; } // // 2. 理论考试(theory_exams) // if (! empty($data['theory_exams']) && is_array($data['theory_exams'])) { foreach ($data['theory_exams'] as $idx => $exam) { $subject = $exam['subject'] ?? '科目' . ($idx + 1); $fields = [ '考试时间' => 'time', '考试地点' => 'location', '主监考' => 'main_invigilator', '监考人员' => 'invigilators', '不及格人数' => 'fail_count', '合格率' => 'pass_rate', ]; $blockSize = count($fields); $startRow = $row; $endRow = $row + $blockSize - 1; $sheet->mergeCells("A{$startRow}:A{$endRow}"); $sheet->setCellValue("A{$startRow}", "理论考试信息({$subject})"); $r = $startRow; foreach ($fields as $label => $key) { $sheet->setCellValue("B{$r}", $label); $sheet->setCellValue("C{$r}", $exam[$key] ?? ''); $r++; } $row = $endRow + 1; } } // // 3. 实操考试(practice_exams) // if (! empty($data['practice_exams']) && is_array($data['practice_exams'])) { foreach ($data['practice_exams'] as $idx => $exam) { $subject = $exam['subject'] ?? '实操' . ($idx + 1); $fields = [ '考试科目' => 'subject', '考试时间' => 'time', '考试地点' => 'location', '不及格人数' => 'fail_count', '合格率' => 'pass_rate', ]; $blockSize = count($fields); $startRow = $row; $endRow = $row + $blockSize - 1; $sheet->mergeCells("A{$startRow}:A{$endRow}"); $sheet->setCellValue("A{$startRow}", "实操考试信息({$subject})"); $r = $startRow; foreach ($fields as $label => $key) { $sheet->setCellValue("B{$r}", $label); $sheet->setCellValue("C{$r}", $exam[$key] ?? ''); $r++; } $row = $endRow + 1; } } // // 4. 样式预设:列宽、行高、边框、对齐、字体 // // 列宽 $sheet->getColumnDimension('A')->setWidth(30); $sheet->getColumnDimension('B')->setWidth(30); $sheet->getColumnDimension('C')->setWidth(24); // 当前签名行号 $lastRow = $row; // 行高 $sheet->getRowDimension(1)->setRowHeight(36); for ($r = 2; $r < $lastRow; $r++) { $sheet->getRowDimension($r)->setRowHeight(21.75); } $sheet->getRowDimension($lastRow)->setRowHeight(54); // 边框 + 对齐 + 正文字体 $sheet->getStyle("A1:C{$lastRow}")->applyFromArray([ 'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN]], 'alignment' => [ 'horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER, ], 'font' => ['name' => '宋体', 'size' => 10], ]); // 标题行字体 $sheet->getStyle('A1')->applyFromArray([ 'font' => ['name' => '黑体', 'size' => 22], ]); // // 5. 班主任签名 & 图片居中 // $sheet->setCellValue("A{$lastRow}", '班主任'); $sheet->mergeCells("B{$lastRow}:C{$lastRow}"); if ($signatureImage && file_exists($signatureImage)) { $drawing = new Drawing(); $drawing->setPath($signatureImage); $drawing->setResizeProportional(true); $drawing->setHeight(40); $drawing->setCoordinates("B{$lastRow}"); $drawing->setWorksheet($sheet); // 近似像素转换系数 $pxPerColWidth = 7; $pxPerPoint = 96 / 72; // 区域宽度 & 高度 $wB = $sheet->getColumnDimension('B')->getWidth(); $wC = $sheet->getColumnDimension('C')->getWidth(); $areaW = ($wB + $wC) * $pxPerColWidth; $hPt = $sheet->getRowDimension($lastRow)->getRowHeight(); $areaH = $hPt * $pxPerPoint; // 图片渲染后尺寸 $imgW = $drawing->getWidth(); $imgH = $drawing->getHeight(); // 计算偏移并应用 $drawing->setOffsetX(max(0, ($areaW - $imgW) / 2)); $drawing->setOffsetY(max(0, ($areaH - $imgH) / 2)); } else { // 文字班主任行单独字号 $sheet->getStyle("B{$lastRow}:C{$lastRow}")->applyFromArray([ 'font' => ['name' => '宋体', 'size' => 12], ]); $sheet->setCellValue("B{$lastRow}", $data['class_teacher'] ?? ''); } return $spreadsheet; } /** * 导出 PDF 文件 * * @param array $data * @param string $pdfPath * @param string|null $signatureImage */ public function exportClassSummaryPdf(array $data, string $pdfPath, ?string $signatureImage = null) { // 1. 复用生成表格的逻辑 $spreadsheet = $this->buildClassSummarySpreadsheet($data, $signatureImage); // // 2) mPDF 默认目录和字体数据 // $configVars = new ConfigVariables(); // $fontDirs = $configVars->getDefaults()['fontDir']; // $fontVars = new FontVariables(); // $fontData = $fontVars->getDefaults()['fontdata']; // $myFontDir = base_path('resources/fonts'); // $mpdfConfig = [ // 'mode' => 'utf-8', // 'format' => 'A4', // 'fontDir' => array_merge($fontDirs, [$myFontDir]), // 'fontdata' => array_merge($fontData, [ // 'sourcehansc' => [ // 'R' => 'SourceHanSans-Regular.ttf', // 'B' => 'SourceHanSans-Bold.ttf', // ] // ]), // 'default_font'=> 'sourcehansc', // 'autoScriptToLang'=> true, // 'autoLangToFont' => true, // ]; // // 2. 用 mPDF Writer 输出 PDF // $pdfWriter = new PdfWriter($spreadsheet, $mpdfConfig); // $pdfWriter->setFont('宋体'); // $pdfWriter->setPaperSize(PageSetup::PAPERSIZE_A4); // $pdfWriter->setOrientation(PageSetup::ORIENTATION_PORTRAIT); // $pdfWriter->save($pdfPath); // 2. 用 TCPDF Writer 输出 PDF $tcpdfWriter = new TcPdfWriter($spreadsheet); $tcpdfWriter->setFont('cid0cs'); $tcpdfWriter->setPaperSize(PageSetup::PAPERSIZE_A4); $tcpdfWriter->setOrientation(PageSetup::ORIENTATION_PORTRAIT); $tcpdfWriter->save($pdfPath); } }