Files

606 lines
21 KiB
PHP
Raw Permalink Normal View History

2025-05-14 13:19:23 +08:00
<?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);
}
}