Files
usedcar/app/mzl/modules/phpoffice/service/ExcelService.php
T
2025-05-14 13:19:23 +08:00

606 lines
21 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace app\mzl\modules\phpoffice\service;
use support\Db;
// use support\Log;
use support\exception\BusinessException;
use app\api\model\PracticeItemsModel;
use app\api\model\PracticeItemsScoreModel;
use app\api\model\PracticeModel;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
use PhpOffice\PhpSpreadsheet\Style\Border;
use PhpOffice\PhpSpreadsheet\Worksheet\Drawing;
// use PhpOffice\PhpSpreadsheet\Style\Fill;
// use PhpOffice\PhpSpreadsheet\Shared\Drawing as SharedDrawing;
// use PhpOffice\PhpSpreadsheet\Writer\Pdf\Dompdf as PdfWriter;
use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup;
use PhpOffice\PhpSpreadsheet\Writer\Pdf\Mpdf as PdfWriter;
use Mpdf\Config\ConfigVariables;
use Mpdf\Config\FontVariables;
use PhpOffice\PhpSpreadsheet\Writer\Pdf\Tcpdf as TcPdfWriter;
class ExcelService
{
// 配置
protected $config = [];
public function __construct()
{
$this->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);
}
}