'序号', 'user_no' => '学号', 'user_name' => '姓名', 'user_schoolmate' => '校友', 'user_position' => '职位', 'user_mobile' => '手机', 'user_course' => '报名课程', 'user_sign_date' => '报名时间', ]; // 项目经理信息子列定义 const PROJECT_USERS_SUB_COLUMNS = [ 'pm_platform' => '管理平台', 'pm_name' => '项目经理', 'pm_invest_date' => '首次出资时间', 'pm_amount' => '投资金额', ]; // 历史课程信息子列定义 const HISTORY_COURSES_SUB_COLUMNS = [ 'hc_course_type' => '课程体系', 'hc_course_name' => '课程名称', 'hc_signs_pass' => '培养人数未去重', 'hc_signs_pass_unique' => '培养人数去重', 'hc_course_signs_pass' => '课程培养人数', ]; public function __construct($data, $exportFields) { // 需要导出的字段。格式:['name'=>'名字','user.sex'=>'性别'] $this->fields = $exportFields; // 数据 $this->data = $data; // 构建扩展后的字段列表 $this->buildExpandedFields(); } /** * 构建扩展后的字段列表(将users和project_users字段展开成多列) */ private function buildExpandedFields() { if (!is_array($this->fields)) { return; } $index = 1; // 第一遍:处理除了历史课程之外的所有字段 foreach ($this->fields as $field => $label) { if (str_contains($field, 'history_courses')) { // 跳过历史课程,稍后处理 continue; } if (str_contains($field, 'users') && !str_contains($field, 'project_users')) { $this->hasUsersField = true; // 展开学员信息为多列 foreach (self::USERS_SUB_COLUMNS as $subField => $subLabel) { $this->expandedFields[$subField] = $subLabel; $index++; } } elseif (str_contains($field, 'project_users')) { $this->hasProjectUsersField = true; // 展开项目经理信息为多列 foreach (self::PROJECT_USERS_SUB_COLUMNS as $subField => $subLabel) { $this->expandedFields[$subField] = $subLabel; $index++; } } else { $this->expandedFields[$field] = $label; $index++; } } // 第二遍:处理历史课程字段,放在最后 foreach ($this->fields as $field => $label) { if (str_contains($field, 'history_courses')) { $this->hasHistoryCoursesField = true; // 展开历史课程信息为多列 foreach (self::HISTORY_COURSES_SUB_COLUMNS as $subField => $subLabel) { $this->expandedFields[$subField] = $subLabel; $index++; } } } $this->totalColumns = count($this->expandedFields); } /** * 是否需要二级表头 */ private function needsDoubleHeader() { return $this->hasUsersField || $this->hasProjectUsersField || $this->hasHistoryCoursesField; } /** * 获取列字母 */ private function getColumnLetter($columnNumber) { $letter = ''; while ($columnNumber > 0) { $columnNumber--; $letter = chr(65 + ($columnNumber % 26)) . $letter; $columnNumber = intval($columnNumber / 26); } return $letter; } /** * 设置列宽 */ public function columnWidths(): array { $widths = []; $index = 1; foreach (array_keys($this->expandedFields) as $field) { $letter = $this->getColumnLetter($index); if (in_array($field, ['user_course', 'user_name'])) { $widths[$letter] = 25; } elseif (in_array($field, ['user_mobile', 'user_sign_date', 'pm_invest_date', 'pm_amount'])) { $widths[$letter] = 15; } elseif (in_array($field, ['user_index', 'user_schoolmate'])) { $widths[$letter] = 8; } elseif (in_array($field, ['pm_platform', 'pm_name'])) { $widths[$letter] = 18; } elseif (str_contains($field, 'partners')) { $widths[$letter] = 50; } elseif (str_contains($field, 'hc_course_type') || str_contains($field, 'hc_course_name')) { $widths[$letter] = 25; } elseif (str_contains($field, 'hc_')) { $widths[$letter] = 18; } elseif (str_contains($field, 'history_courses')) { // 历史课程信息通常较长,适当放宽列宽(如果没有展开的情况) $widths[$letter] = 40; } 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++; } return $widths; } /** * 设置样式 */ public function styles(Worksheet $sheet): array { $lastCol = $this->getColumnLetter($this->totalColumns); $dataStartRow = $this->needsDoubleHeader() ? 3 : 2; $styles = []; if ($this->needsDoubleHeader()) { // 二级表头样式 - 第一行 $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' => 'D0D0D0'], ], ]; // 二级表头样式 - 第二行 $styles[2] = [ 'font' => ['bold' => true, 'size' => 11], 'alignment' => [ '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'], ], ]; } // 所有数据区域样式 $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; } /** * 注册事件 */ public function registerEvents(): array { return [ AfterSheet::class => function (AfterSheet $event) { $sheet = $event->sheet->getDelegate(); if ($this->needsDoubleHeader()) { // 处理二级表头的单元格合并 $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'); } // 设置数据行高 $dataStartRow = $this->needsDoubleHeader() ? 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') && !str_contains($field, 'project_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); } elseif (str_contains($field, 'project_users')) { // 项目经理信息列:合并第一行的多个单元格 $startCol = $this->getColumnLetter($index); $endCol = $this->getColumnLetter($index + count(self::PROJECT_USERS_SUB_COLUMNS) - 1); $sheet->mergeCells("{$startCol}1:{$endCol}1"); $sheet->setCellValue("{$startCol}1", $label); $index += count(self::PROJECT_USERS_SUB_COLUMNS); } elseif (str_contains($field, 'history_courses')) { // 历史课程信息列:合并第一行的多个单元格 $startCol = $this->getColumnLetter($index); $endCol = $this->getColumnLetter($index + count(self::HISTORY_COURSES_SUB_COLUMNS) - 1); $sheet->mergeCells("{$startCol}1:{$endCol}1"); $sheet->setCellValue("{$startCol}1", $label); $index += count(self::HISTORY_COURSES_SUB_COLUMNS); } else { // 其他列:合并第一行和第二行 $col = $this->getColumnLetter($index); $sheet->mergeCells("{$col}1:{$col}2"); $sheet->setCellValue("{$col}1", $label); $index++; } } } /** * 数组转集合 * @throws ErrorException */ public function collection() { $clear = request('clear', 0); if (empty($this->fields)) { throw new ErrorException('导出字段不能为空'); } if (!is_array($this->fields)) { throw new ErrorException('导出字段必须是数组'); } $newList = []; if ($this->needsDoubleHeader()) { // 有需要展开的字段:创建二级表头 // 第一行表头(主表头)- 在 mergeHeaderCells 中处理 $header1 = []; foreach ($this->fields as $field => $label) { if (str_contains($field, 'users') && !str_contains($field, 'project_users')) { foreach (self::USERS_SUB_COLUMNS as $subLabel) { $header1[] = ''; } } elseif (str_contains($field, 'project_users')) { foreach (self::PROJECT_USERS_SUB_COLUMNS as $subLabel) { $header1[] = ''; } } elseif (str_contains($field, 'history_courses')) { foreach (self::HISTORY_COURSES_SUB_COLUMNS as $subLabel) { $header1[] = ''; } } else { $header1[] = ''; } } $newList[] = $header1; // 第二行表头(子表头) $header2 = []; foreach ($this->fields as $field => $label) { if (str_contains($field, 'users') && !str_contains($field, 'project_users')) { foreach (self::USERS_SUB_COLUMNS as $subLabel) { $header2[] = $subLabel; } } elseif (str_contains($field, 'project_users')) { foreach (self::PROJECT_USERS_SUB_COLUMNS as $subLabel) { $header2[] = $subLabel; } } elseif (str_contains($field, 'history_courses')) { foreach (self::HISTORY_COURSES_SUB_COLUMNS as $subLabel) { $header2[] = $subLabel; } } else { $header2[] = ''; } } $newList[] = $header2; // 数据行:每个展开记录占一行 foreach ($this->data as $info) { $expandedRows = $this->getExpandedRows($info); if (empty($expandedRows)) { // 没有展开数据,输出一行空数据 $row = $this->buildExpandedRow($info, [], [], []); $newList[] = $row; } else { foreach ($expandedRows as $expandedRow) { $row = $this->buildExpandedRow($info, $expandedRow['users'] ?? [], $expandedRow['project_users'] ?? [], $expandedRow['history_courses'] ?? []); $newList[] = $row; } } } } else { // 没有需要展开的字段:使用原有逻辑 $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'); } } $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, 'history_courses')) { // 历史课程信息字段,格式化为多行文本 $temp[$field] = $this->historyCourses($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); } } $this->totalRows = count($newList) - ($this->needsDoubleHeader() ? 2 : 1); return new Collection($newList); } /** * 获取展开后的所有行数据 */ private function getExpandedRows($info) { $usersData = $this->hasUsersField ? $this->getUsersExpanded($info) : []; $projectUsersData = $this->hasProjectUsersField ? $this->getProjectUsersExpanded($info) : []; $historyCoursesData = $this->hasHistoryCoursesField ? $this->getHistoryCoursesExpanded($info) : []; // 计算最大行数 $maxRows = max(count($usersData), count($projectUsersData), count($historyCoursesData), 1); $result = []; for ($i = 0; $i < $maxRows; $i++) { $result[] = [ 'users' => $usersData[$i] ?? [], 'project_users' => $projectUsersData[$i] ?? [], 'history_courses' => $historyCoursesData[$i] ?? [], ]; } return $result; } /** * 获取展开的项目经理数据(每条记录一行) */ private function getProjectUsersExpanded($info) { if (empty($info['project_users'])) { return []; } $result = []; foreach ($info['project_users'] as $item) { $result[] = [ 'pm_platform' => $item['groupName'] ?? '-', 'pm_name' => $item['userName'] ?? '-', 'pm_invest_date' => $item['investDate'] ?? '-', 'pm_amount' => $item['amount'] ?? '-', ]; } return $result; } /** * 获取展开的历史课程数据(每条历史课程一行) */ private function getHistoryCoursesExpanded($info) { if (empty($info['history_courses']) || !is_array($info['history_courses'])) { return []; } $result = []; foreach ($info['history_courses'] as $item) { // 获取课程体系名称 $courseTypeName = ''; if (isset($item['type_detail']) && isset($item['type_detail']['name'])) { // 关联数据可能是 type_detail(下划线形式) $courseTypeName = $item['type_detail']['name']; } elseif (isset($item['typeDetail']) && isset($item['typeDetail']['name'])) { // 关联数据可能是 typeDetail(驼峰形式) $courseTypeName = $item['typeDetail']['name']; } elseif (isset($item['type'])) { // 如果只有类型ID,尝试查询类型名称 $typeId = $item['type']; try { $courseType = CourseType::find($typeId); if ($courseType) { $courseTypeName = $courseType->name; } else { $courseTypeName = $typeId; // 如果查询不到,显示ID } } catch (\Exception $e) { $courseTypeName = $typeId; // 查询失败,显示ID } } $result[] = [ 'hc_course_type' => $courseTypeName, 'hc_course_name' => $item['course_name'] ?? '', 'hc_signs_pass' => $item['course_type_signs_pass'] ?? 0, 'hc_signs_pass_unique' => $item['course_type_signs_pass_unique'] ?? 0, 'hc_course_signs_pass' => $item['course_signs_pass'] ?? 0, ]; } return $result; } /** * 获取展开的学员数据(每个学员每条课程一行) */ 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 buildExpandedRow($info, $userData, $projectUserData, $historyCourseData = []) { $row = []; foreach ($this->fields as $field => $label) { if (str_contains($field, 'users') && !str_contains($field, 'project_users')) { // 填充学员信息列 foreach (array_keys(self::USERS_SUB_COLUMNS) as $subField) { $row[] = $userData[$subField] ?? ''; } } elseif (str_contains($field, 'project_users')) { // 填充项目经理信息列 foreach (array_keys(self::PROJECT_USERS_SUB_COLUMNS) as $subField) { $row[] = $projectUserData[$subField] ?? ''; } } elseif (str_contains($field, 'history_courses')) { // 填充历史课程信息列 foreach (array_keys(self::HISTORY_COURSES_SUB_COLUMNS) as $subField) { $row[] = $historyCourseData[$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); } else { $row[] = $this->getDotValue($info, $field); } } return $row; } /** * .号转数组层级并返回对应的值 * @param $key * @param null $default * @return mixed|null */ function getDotValue($config, $key, $default = null) { // 如果在第一层,就直接返回 if (isset($config[$key])) { return $config[$key]; } // 如果找不到,直接返回默认值 if (false === strpos($key, '.')) { return $default; } // 临时数组 $tempArr = explode('.', $key); foreach ($tempArr as $segment) { if (!is_array($config) || !array_key_exists($segment, $config)) { return $default; } $config = $config[$segment]; } return $config; } /** * 获取所有课程名称 * @param $data */ function allCourse($data) { $list = []; foreach ($data['course_signs'] as $item) { $list[] = $item['course']['name'] ?? ''; } return implode("、\r\n", $list); } /** * 获取所有历史课程信息(用于日历导出) * @param $data * @return string */ function historyCourses($data) { if (empty($data['history_courses']) || !is_array($data['history_courses'])) { return ''; } $list = []; foreach ($data['history_courses'] as $item) { $courseName = $item['course_name'] ?? ''; $type = $item['type'] ?? ''; $pass = $item['course_type_signs_pass'] ?? 0; $passUnique = $item['course_type_signs_pass_unique'] ?? 0; $signPass = $item['course_signs_pass'] ?? 0; $start = $item['start_time'] ?? ''; $end = $item['end_time'] ?? ''; // 构造单行描述:课程名称[体系ID](开始~结束) 培养人数/去重/课程人数 $parts = []; if ($courseName !== '') { $parts[] = $courseName; } if ($type !== '') { $parts[] = "[类型:{$type}]"; } if ($start !== '' || $end !== '') { $parts[] = "({$start}~{$end})"; } $parts[] = "培养:{$pass}/去重:{$passUnique}/课程:{$signPass}"; $list[] = implode(' ', $parts); } // 每门历史课程占一行 return implode("\r\n", $list); } /** * 获取所有股东信息 * @param $data */ function partners($data) { $list = []; foreach ($data['partners'] as $item) { $list[] = $item['stockName'] . '-' . $item['stockPercent'] ?? ''; } return implode("、\r\n", $list); } /** * 获取所有项目经理 * @param $data */ function projectManager($data) { $list = []; foreach ($data['project_users'] as $item) { $list[] = $item['groupName'] . '-' . ($item['userName'] ?? '') . '-' . ($item['investDate'] ?? '') . '-' . ($item['amount'] ?? ''); } return implode("、\r\n", $list); } }