From 1a725424635c32353c8b7ac68313cace6e4440fb Mon Sep 17 00:00:00 2001 From: cody <648753004@qq.com> Date: Wed, 26 Nov 2025 17:49:06 +0800 Subject: [PATCH] update --- app/Exports/CommonExport.php | 466 ++++++++++++++++++++++------------- 1 file changed, 292 insertions(+), 174 deletions(-) diff --git a/app/Exports/CommonExport.php b/app/Exports/CommonExport.php index 1f89e8b..4367da7 100755 --- a/app/Exports/CommonExport.php +++ b/app/Exports/CommonExport.php @@ -20,7 +20,22 @@ class CommonExport implements FromCollection, WithStyles, WithColumnWidths, With public $fields; public $data; public $hasUsersField = false; - public $usersColumnIndex = null; + public $usersColumnStartIndex = null; + public $totalColumns = 0; + public $totalRows = 0; + public $expandedFields = []; + + // 学员信息子列定义 + const USERS_SUB_COLUMNS = [ + 'user_index' => '序号', + 'user_no' => '学号', + 'user_name' => '姓名', + 'user_schoolmate' => '校友', + 'user_position' => '职位', + 'user_mobile' => '手机', + 'user_course' => '报名课程', + 'user_sign_date' => '报名时间', + ]; public function __construct($data, $exportFields) { @@ -29,17 +44,35 @@ class CommonExport implements FromCollection, WithStyles, WithColumnWidths, With // 数据 $this->data = $data; - // 检查是否有 users 字段 - if (is_array($exportFields)) { - $index = 1; - foreach (array_keys($exportFields) as $field) { - if (str_contains($field, 'users')) { - $this->hasUsersField = true; - $this->usersColumnIndex = $this->getColumnLetter($index); + // 构建扩展后的字段列表 + $this->buildExpandedFields(); + } + + /** + * 构建扩展后的字段列表(将users字段展开成多列) + */ + private function buildExpandedFields() + { + if (!is_array($this->fields)) { + return; + } + + $index = 1; + foreach ($this->fields as $field => $label) { + if (str_contains($field, 'users')) { + $this->hasUsersField = true; + $this->usersColumnStartIndex = $index; + // 展开学员信息为多列 + foreach (self::USERS_SUB_COLUMNS as $subField => $subLabel) { + $this->expandedFields[$subField] = $subLabel; + $index++; } + } else { + $this->expandedFields[$field] = $label; $index++; } } + $this->totalColumns = count($this->expandedFields); } /** @@ -63,22 +96,21 @@ class CommonExport implements FromCollection, WithStyles, WithColumnWidths, With { $widths = []; $index = 1; - foreach (array_keys($this->fields) as $field) { + foreach (array_keys($this->expandedFields) as $field) { $letter = $this->getColumnLetter($index); - if (str_contains($field, 'users')) { - // 学员信息列设置较宽 - $widths[$letter] = 80; + if (in_array($field, ['user_course', 'user_name'])) { + $widths[$letter] = 25; + } elseif (in_array($field, ['user_mobile', 'user_sign_date'])) { + $widths[$letter] = 15; + } elseif (in_array($field, ['user_index', 'user_schoolmate'])) { + $widths[$letter] = 8; } elseif (str_contains($field, 'partners') || str_contains($field, 'project_users')) { - // 股东和项目经理列 $widths[$letter] = 50; } elseif (str_contains($field, 'all_course')) { - // 课程列 $widths[$letter] = 40; } elseif (str_contains($field, 'company_name') || str_contains($field, 'address')) { - // 公司名称和地址 $widths[$letter] = 30; } else { - // 默认列宽 $widths[$letter] = 15; } $index++; @@ -91,13 +123,14 @@ class CommonExport implements FromCollection, WithStyles, WithColumnWidths, With */ public function styles(Worksheet $sheet): array { - $rowCount = count($this->data) + 1; // 数据行数 + 表头 - $colCount = count($this->fields); - $lastCol = $this->getColumnLetter($colCount); + $lastCol = $this->getColumnLetter($this->totalColumns); + $dataStartRow = $this->hasUsersField ? 3 : 2; - return [ - // 表头样式 - 1 => [ + $styles = []; + + if ($this->hasUsersField) { + // 二级表头样式 - 第一行 + $styles[1] = [ 'font' => ['bold' => true, 'size' => 12], 'alignment' => [ 'horizontal' => Alignment::HORIZONTAL_CENTER, @@ -105,23 +138,51 @@ class CommonExport implements FromCollection, WithStyles, WithColumnWidths, With ], 'fill' => [ 'fillType' => \PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID, - 'startColor' => ['rgb' => 'E0E0E0'], + 'startColor' => ['rgb' => 'D0D0D0'], ], - ], - // 所有数据区域样式 - "A1:{$lastCol}{$rowCount}" => [ + ]; + // 二级表头样式 - 第二行 + $styles[2] = [ + 'font' => ['bold' => true, 'size' => 11], 'alignment' => [ - 'vertical' => Alignment::VERTICAL_TOP, - 'wrapText' => true, // 自动换行 + 'horizontal' => Alignment::HORIZONTAL_CENTER, + 'vertical' => Alignment::VERTICAL_CENTER, + ], + 'fill' => [ + 'fillType' => \PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID, + 'startColor' => ['rgb' => 'E8E8E8'], + ], + ]; + } else { + // 单行表头样式 + $styles[1] = [ + 'font' => ['bold' => true, 'size' => 12], + 'alignment' => [ + 'horizontal' => Alignment::HORIZONTAL_CENTER, + 'vertical' => Alignment::VERTICAL_CENTER, + ], + 'fill' => [ + 'fillType' => \PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID, + 'startColor' => ['rgb' => 'E0E0E0'], ], - 'borders' => [ - 'allBorders' => [ - 'borderStyle' => \PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN, - 'color' => ['rgb' => 'CCCCCC'], - ], + ]; + } + + // 所有数据区域样式 + $styles["A1:{$lastCol}" . ($this->totalRows + $dataStartRow - 1)] = [ + 'alignment' => [ + 'vertical' => Alignment::VERTICAL_CENTER, + 'wrapText' => true, + ], + 'borders' => [ + 'allBorders' => [ + 'borderStyle' => \PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN, + 'color' => ['rgb' => 'CCCCCC'], ], ], ]; + + return $styles; } /** @@ -132,34 +193,57 @@ class CommonExport implements FromCollection, WithStyles, WithColumnWidths, With return [ AfterSheet::class => function (AfterSheet $event) { $sheet = $event->sheet->getDelegate(); - $rowCount = count($this->data) + 1; - - // 设置表头行高 - $sheet->getRowDimension(1)->setRowHeight(25); - - // 遍历数据行,根据内容设置行高 - for ($row = 2; $row <= $rowCount; $row++) { - // 获取该行的最大内容行数 - $maxLines = 1; - foreach (array_keys($this->fields) as $index => $field) { - $col = $this->getColumnLetter($index + 1); - $cellValue = $sheet->getCell($col . $row)->getValue(); - if ($cellValue) { - $lines = substr_count($cellValue, "\n") + 1; - $maxLines = max($maxLines, $lines); - } - } - // 设置行高(每行内容约15像素,最小20,最大400) - $rowHeight = min(400, max(20, $maxLines * 15)); - $sheet->getRowDimension($row)->setRowHeight($rowHeight); + + if ($this->hasUsersField) { + // 处理二级表头的单元格合并 + $this->mergeHeaderCells($sheet); + + // 设置表头行高 + $sheet->getRowDimension(1)->setRowHeight(25); + $sheet->getRowDimension(2)->setRowHeight(22); + + // 冻结前两行 + $sheet->freezePane('A3'); + } else { + $sheet->getRowDimension(1)->setRowHeight(25); + $sheet->freezePane('A2'); } - // 冻结首行 - $sheet->freezePane('A2'); + // 设置数据行高 + $dataStartRow = $this->hasUsersField ? 3 : 2; + for ($row = $dataStartRow; $row <= $this->totalRows + $dataStartRow - 1; $row++) { + $sheet->getRowDimension($row)->setRowHeight(22); + } }, ]; } + /** + * 合并表头单元格 + */ + private function mergeHeaderCells(Worksheet $sheet) + { + $index = 1; + foreach ($this->fields as $field => $label) { + if (str_contains($field, 'users')) { + // 学员信息列:合并第一行的多个单元格 + $startCol = $this->getColumnLetter($index); + $endCol = $this->getColumnLetter($index + count(self::USERS_SUB_COLUMNS) - 1); + $sheet->mergeCells("{$startCol}1:{$endCol}1"); + $sheet->setCellValue("{$startCol}1", $label); + + // 学员信息子表头不需要合并 + $index += count(self::USERS_SUB_COLUMNS); + } else { + // 非学员信息列:合并第一行和第二行 + $col = $this->getColumnLetter($index); + $sheet->mergeCells("{$col}1:{$col}2"); + $sheet->setCellValue("{$col}1", $label); + $index++; + } + } + } + /** * 数组转集合 * @throws ErrorException @@ -173,65 +257,172 @@ class CommonExport implements FromCollection, WithStyles, WithColumnWidths, With if (!is_array($this->fields)) { throw new ErrorException('导出字段必须是数组'); } - // 获取表头 - $header = array_values($this->fields); - $moreFileds = []; - if (empty($clear)) { - // 表头追加附属数据 - if (isset($this->data[0]['data']) && is_array($this->data[0]['data'])) { - $moreHeader = array_column($this->data[0]['data'], 'name'); - // 获取头信息 - $header = array_merge($header, $moreHeader); - // 获取字段信息 - $moreFileds = array_column($this->data[0]['data'], 'field'); - } - } - // 获取字段指向 - $fields = array_keys($this->fields); + $newList = []; - foreach ($this->data as $info) { - $temp = []; - foreach ($fields as $field) { - if (str_contains($field, 'idcard')) { - // 身份证 - $temp[$field] = ' ' . $this->getDotValue($info, $field); - } elseif (str_contains($field, 'all_course')) { - // 所有课程 - $temp[$field] = $this->allCourse($info); - } elseif (str_contains($field, 'partners')) { - // 股东信息 - $temp[$field] = $this->partners($info); - } elseif (str_contains($field, 'project_users')) { - // 项目经理 - $temp[$field] = $this->projectManager($info); - } elseif (str_contains($field, 'users')) { - // 学员信息 - $temp[$field] = $this->getUsers($info); + + if ($this->hasUsersField) { + // 有学员字段:创建二级表头 + // 第一行表头(主表头)- 在 mergeHeaderCells 中处理 + $header1 = []; + foreach ($this->fields as $field => $label) { + if (str_contains($field, 'users')) { + // 学员信息占用多列,第一行只需要占位 + foreach (self::USERS_SUB_COLUMNS as $subLabel) { + $header1[] = ''; + } } else { - $temp[$field] = $this->getDotValue($info, $field); + $header1[] = ''; } } - // 如果有自定义数据,全部附件上去 - $t2 = []; + $newList[] = $header1; + + // 第二行表头(子表头) + $header2 = []; + foreach ($this->fields as $field => $label) { + if (str_contains($field, 'users')) { + foreach (self::USERS_SUB_COLUMNS as $subLabel) { + $header2[] = $subLabel; + } + } else { + $header2[] = ''; + } + } + $newList[] = $header2; + + // 数据行:每个学员的每条课程记录占一行 + foreach ($this->data as $info) { + $usersData = $this->getUsersExpanded($info); + if (empty($usersData)) { + // 没有学员数据,输出一行空数据 + $row = $this->buildRowWithoutUsers($info, []); + $newList[] = $row; + } else { + // 每个学员记录一行 + foreach ($usersData as $userData) { + $row = $this->buildRowWithoutUsers($info, $userData); + $newList[] = $row; + } + } + } + } else { + // 没有学员字段:使用原有逻辑 + $header = array_values($this->fields); + $moreFileds = []; if (empty($clear)) { - if (isset($info['data']) && $info['data'] && !empty($moreFileds)) { - $dataCollect = collect($info['data']); - foreach ($moreFileds as $moreFiled) { - $value = ($dataCollect->where('field', $moreFiled)->first()['value']) ?? ''; - if (str_contains($moreFiled, 'idcard')) { - $t2[$moreFiled] = ' ' . $value; - } else { - $t2[$moreFiled] = $value; + if (isset($this->data[0]['data']) && is_array($this->data[0]['data'])) { + $moreHeader = array_column($this->data[0]['data'], 'name'); + $header = array_merge($header, $moreHeader); + $moreFileds = array_column($this->data[0]['data'], 'field'); + } + } + $newList[] = $header; + + foreach ($this->data as $info) { + $temp = []; + foreach (array_keys($this->fields) as $field) { + if (str_contains($field, 'idcard')) { + $temp[$field] = ' ' . $this->getDotValue($info, $field); + } elseif (str_contains($field, 'all_course')) { + $temp[$field] = $this->allCourse($info); + } elseif (str_contains($field, 'partners')) { + $temp[$field] = $this->partners($info); + } elseif (str_contains($field, 'project_users')) { + $temp[$field] = $this->projectManager($info); + } else { + $temp[$field] = $this->getDotValue($info, $field); + } + } + $t2 = []; + if (empty($clear)) { + if (isset($info['data']) && $info['data'] && !empty($moreFileds)) { + $dataCollect = collect($info['data']); + foreach ($moreFileds as $moreFiled) { + $value = ($dataCollect->where('field', $moreFiled)->first()['value']) ?? ''; + if (str_contains($moreFiled, 'idcard')) { + $t2[$moreFiled] = ' ' . $value; + } else { + $t2[$moreFiled] = $value; + } } } } + $newList[] = array_values($temp + $t2); } - $newList[] = $temp + $t2; } - array_unshift($newList, $header); //插入表头 + + $this->totalRows = count($newList) - ($this->hasUsersField ? 2 : 1); return new Collection($newList); } + /** + * 获取展开的学员数据(每个学员每条课程一行) + */ + private function getUsersExpanded($info) + { + if (empty($info['users'])) { + return []; + } + + $result = []; + $userIndex = 1; + foreach ($info['users'] as $user) { + if (!empty($user['course_signs'])) { + foreach ($user['course_signs'] as $signIndex => $sign) { + $result[] = [ + 'user_index' => $signIndex === 0 ? $userIndex : '', + 'user_no' => $signIndex === 0 ? ($user['no'] ?? '-') : '', + 'user_name' => $signIndex === 0 ? ($user['username'] ?? '-') : '', + 'user_schoolmate' => $signIndex === 0 ? ($user['is_schoolmate_text'] ?? '-') : '', + 'user_position' => $signIndex === 0 ? ($user['company_position'] ?? '-') : '', + 'user_mobile' => $signIndex === 0 ? ($user['mobile'] ?? '-') : '', + 'user_course' => $sign['course']['name'] ?? '-', + 'user_sign_date' => isset($sign['created_at']) ? substr($sign['created_at'], 0, 10) : '-', + ]; + } + } else { + $result[] = [ + 'user_index' => $userIndex, + 'user_no' => $user['no'] ?? '-', + 'user_name' => $user['username'] ?? '-', + 'user_schoolmate' => $user['is_schoolmate_text'] ?? '-', + 'user_position' => $user['company_position'] ?? '-', + 'user_mobile' => $user['mobile'] ?? '-', + 'user_course' => '-', + 'user_sign_date' => '-', + ]; + } + $userIndex++; + } + return $result; + } + + /** + * 构建包含学员数据的行 + */ + private function buildRowWithoutUsers($info, $userData) + { + $row = []; + foreach ($this->fields as $field => $label) { + if (str_contains($field, 'users')) { + // 填充学员信息列 + foreach (array_keys(self::USERS_SUB_COLUMNS) as $subField) { + $row[] = $userData[$subField] ?? ''; + } + } elseif (str_contains($field, 'idcard')) { + $row[] = ' ' . $this->getDotValue($info, $field); + } elseif (str_contains($field, 'all_course')) { + $row[] = $this->allCourse($info); + } elseif (str_contains($field, 'partners')) { + $row[] = $this->partners($info); + } elseif (str_contains($field, 'project_users')) { + $row[] = $this->projectManager($info); + } else { + $row[] = $this->getDotValue($info, $field); + } + } + return $row; + } + /** * .号转数组层级并返回对应的值 * @param $key @@ -298,77 +489,4 @@ class CommonExport implements FromCollection, WithStyles, WithColumnWidths, With return implode("、\r\n", $list); } - /** - * 获取学员信息(表格格式显示) - * @param $data - */ - function getUsers($data) - { - if (empty($data['users'])) { - return ''; - } - - $result = []; - - // 表头 - $header = "序号\t学号\t姓名\t校友\t职位\t手机\t报名课程\t报名时间"; - $result[] = $header; - $result[] = str_repeat("─", 80); // 分隔线 - - // 数据行 - $index = 1; - foreach ($data['users'] as $item) { - // 如果有多个课程报名,需要展开多行 - if (!empty($item['course_signs'])) { - foreach ($item['course_signs'] as $signIndex => $sign) { - $courseName = $sign['course']['name'] ?? '-'; - $signDate = isset($sign['created_at']) ? substr($sign['created_at'], 0, 10) : '-'; - - if ($signIndex === 0) { - // 第一行显示完整信息 - $row = implode("\t", [ - $index, - $item['no'] ?? '-', - $item['username'] ?? '-', - $item['is_schoolmate_text'] ?? '-', - $item['company_position'] ?? '-', - $item['mobile'] ?? '-', - $courseName, - $signDate - ]); - } else { - // 后续行只显示课程信息,其他列留空 - $row = implode("\t", [ - '', - '', - '', - '', - '', - '', - $courseName, - $signDate - ]); - } - $result[] = $row; - } - } else { - // 没有课程报名 - $row = implode("\t", [ - $index, - $item['no'] ?? '-', - $item['username'] ?? '-', - $item['is_schoolmate_text'] ?? '-', - $item['company_position'] ?? '-', - $item['mobile'] ?? '-', - '-', - '-' - ]); - $result[] = $row; - } - $index++; - } - - return implode("\n", $result); - } - }