diff --git a/app/Services/Survey/SurveyExport.php b/app/Services/Survey/SurveyExport.php index e6d934a..ba326f1 100644 --- a/app/Services/Survey/SurveyExport.php +++ b/app/Services/Survey/SurveyExport.php @@ -34,17 +34,27 @@ class SurveyExport $detail = $book->createSheet()->setTitle('问题详细表'); $attachments = $book->createSheet()->setTitle('附件清单'); self::row($summary, 1, ['答卷ID', '问卷名称', '答题人姓名', '学号', '公司', '职务', '首次提交时间', '最近提交时间']); - self::row($detail, 1, ['答卷ID', '答题人姓名', '章节', '题号', '题目', '子题/维度', '题型', '题目说明', '所有选项', '答案', '补充说明', '状态']); + self::row($detail, 1, ['答卷ID', '答题人姓名', '章节', '题号', '题目', '子题/维度', '答案', '补充说明', '题型', '题目说明', '所有选项']); self::row($attachments, 1, ['答卷ID', '答题人姓名', '题目', '原文件名', '压缩包内路径', '状态']); $sr = $dr = $ar = 2; + $hasSections = $hasDimensions = false; + $replyRanges = $sectionRanges = $questionRanges = $plainRows = []; $current = $survey->courseContentEvaluationAsks()->orderBy('sort')->orderBy('id')->get()->toArray(); foreach ($survey->courseContentEvaluationForms()->with('user')->lazyById(100) as $reply) { $name = $reply->user->name ?? '未填写姓名'; self::row($summary, $sr++, [$reply->id, $reply->snapshot['title'] ?? $survey->title, $name, $reply->user->no ?? '', $reply->user->company_name ?? '', $reply->user->company_position ?? '', $reply->created_at, $reply->updated_at]); + $replyStart = $sectionStart = $dr; $section = ''; $number = 0; foreach (SurveySchema::replyQuestions($current, $reply->data, $reply->snapshot) as $q) { - if ($q['edit_input'] === 'section') { $section = $q['name']; continue; } + if ($q['edit_input'] === 'section') { + if ($section !== '' && $dr > $sectionStart) $sectionRanges[] = [$sectionStart, $dr - 1]; + $sectionStart = $dr; + $section = $q['name']; + $hasSections = true; + continue; + } + $questionStart = $dr; $hidden = !empty($q['hidden']); $position = $hidden ? '' : ++$number; $extra = $hidden ? '' : ($q['other_text'] ?? ''); @@ -55,8 +65,12 @@ class SurveyExport $fields = $q['edit_input'] === 'multi_dimension' ? ($q['dimensions'] ?? []) : [$q]; foreach ($fields as $field) { $options = array_map(fn ($o) => $o['value'].(isset($o['score']) ? '('.$o['score'].'分)' : ''), $field['select_item'] ?? []); - self::row($detail, $dr++, [$reply->id, $name, $section, $position, $q['name'], $q['edit_input'] === 'multi_dimension' ? $field['name'] : '', self::TYPES[$field['edit_input']] ?? $field['edit_input'], $field['help'] ?? $q['help'] ?? '', implode("\n", $options), $hidden ? '' : ($field['display_value'] ?? ''), $extra, $hidden ? '条件未满足,未显示' : '已显示']); + self::row($detail, $dr++, [$reply->id, $name, $section, $position, $q['name'], $q['edit_input'] === 'multi_dimension' ? $field['name'] : '', $hidden ? '' : ($field['display_value'] ?? ''), $extra, self::TYPES[$field['edit_input']] ?? $field['edit_input'], $field['help'] ?? $q['help'] ?? '', implode("\n", $options)]); } + if ($q['edit_input'] === 'multi_dimension' && $dr > $questionStart) { + $hasDimensions = true; + $questionRanges[] = [$questionStart, $dr - 1]; + } elseif ($dr > $questionStart) $plainRows[] = $questionStart; if ($hidden || $q['edit_input'] !== 'file') continue; foreach ($q['value'] ?? [] as $item) { $file = Upload::find($item['id'] ?? null); @@ -67,7 +81,25 @@ class SurveyExport self::row($attachments, $ar++, [$reply->id, $name, $q['name'], $original, $valid ? $entry : '', $valid ? '已打包' : '文件缺失,未打包']); } } + if ($dr > $replyStart) $replyRanges[] = [$replyStart, $dr - 1]; + if ($section !== '' && $dr > $sectionStart) $sectionRanges[] = [$sectionStart, $dr - 1]; + } + if (!$hasDimensions) $detail->removeColumn('F'); + if (!$hasSections) $detail->removeColumn('C'); + $titleColumn = $hasSections ? 'E' : 'D'; + $numberColumn = $hasSections ? 'D' : 'C'; + $dimensionColumn = $hasSections ? 'F' : 'E'; + foreach ($replyRanges as [$start, $end]) { + self::merge($detail, 'A', $start, $end); + self::merge($detail, 'B', $start, $end); + } + if ($hasSections) foreach ($sectionRanges as [$start, $end]) self::merge($detail, 'C', $start, $end); + foreach ($questionRanges as [$start, $end]) { + self::merge($detail, $titleColumn, $start, $end); + self::merge($detail, $numberColumn, $start, $end); } + // In mixed questionnaires a plain question spans the unused dimension cell. + if ($hasDimensions) foreach ($plainRows as $row) $detail->mergeCells($titleColumn.$row.':'.$dimensionColumn.$row); foreach ($book->getAllSheets() as $sheet) { $sheet->freezePane('C2'); $sheet->setAutoFilter($sheet->calculateWorksheetDimension()); @@ -75,6 +107,7 @@ class SurveyExport $sheet->getStyle($sheet->calculateWorksheetDimension())->getAlignment()->setWrapText(true)->setVertical('top'); foreach (range(1, Coordinate::columnIndexFromString($sheet->getHighestColumn())) as $col) $sheet->getColumnDimensionByColumn($col)->setWidth($col < 3 ? 18 : 32); } + foreach ($detail->getMergeCells() as $range) $detail->getStyle($range)->getAlignment()->setVertical('center'); (new Xlsx($book))->save($base.'.xlsx'); $zip->addFile($base.'.xlsx', '问卷答卷明细.xlsx'); if (!$zip->close()) throw new \RuntimeException('导出压缩包生成失败'); @@ -90,6 +123,11 @@ class SurveyExport } } + private static function merge($sheet, string $column, int $start, int $end): void + { + if ($end > $start) $sheet->mergeCells($column.$start.':'.$column.$end); + } + private static function row($sheet, int $row, array $values): void { foreach ($values as $index => $value) { diff --git a/tests/Feature/SurveyWorkflowTest.php b/tests/Feature/SurveyWorkflowTest.php index 2bea1b2..51d395b 100644 --- a/tests/Feature/SurveyWorkflowTest.php +++ b/tests/Feature/SurveyWorkflowTest.php @@ -62,6 +62,39 @@ class SurveyWorkflowTest extends TestCase return $response->json('id'); } + public function testExportMergesChaptersAndKeepsReplyBoundaries(): void + { + $questions = [ + ['id' => 1, 'name' => '第一章', 'edit_input' => 'section'], + ['id' => 2, 'name' => '组合题', 'edit_input' => 'multi_dimension', 'dimensions' => [ + ['field' => 'a', 'name' => '数量A', 'edit_input' => 'number'], + ['field' => 'b', 'name' => '数量B', 'edit_input' => 'number'], + ]], + ['id' => 3, 'name' => '建议', 'edit_input' => 'text'], + ['id' => 4, 'name' => '第二章', 'edit_input' => 'section'], + ['id' => 5, 'name' => '评价', 'edit_input' => 'text'], + ]; + foreach ([200, 201] as $userId) { + \App\Models\CourseContentEvaluationForm::create(['course_content_evaluation_id' => 10, 'user_id' => $userId, + 'snapshot' => ['version' => 2, 'questions' => $questions], + 'data' => [['id' => 2, 'value' => ['a' => 0, 'b' => 2]], ['id' => 3, 'value' => '建议内容'], ['id' => 5, 'value' => '评价内容']]]); + } + $path = \App\Services\Survey\SurveyExport::build(\App\Models\CourseContentEvaluation::find(10)); + $zip = new \ZipArchive(); + $zip->open($path); + file_put_contents($path.'.xlsx', $zip->getFromName('问卷答卷明细.xlsx')); + $book = \PhpOffice\PhpSpreadsheet\IOFactory::load($path.'.xlsx'); + $sheet = $book->getSheetByName('问题详细表'); + $this->assertSame(['答卷ID', '答题人姓名', '章节', '题号', '题目', '子题/维度', '答案', '补充说明', '题型', '题目说明', '所有选项'], $sheet->rangeToArray('A1:K1')[0]); + foreach (['A2:A5', 'B2:B5', 'A6:A9', 'C2:C4', 'C6:C8', 'E2:E3', 'E4:F4'] as $range) $this->assertContains($range, $sheet->getMergeCells()); + $this->assertSame('第二章', $sheet->getCell('C5')->getValue()); + $this->assertSame('建议内容', $sheet->getCell('G4')->getValue()); + $book->disconnectWorksheets(); + $zip->close(); + unlink($path.'.xlsx'); + unlink($path); + } + public function testFileAnswersValidateOwnershipAndReturnNames(): void { Schema::create('uploads', function (Blueprint $table) { $table->id(); $table->string('original_name'); $table->string('folder'); $table->string('name'); $table->string('extension'); $table->integer('size'); $table->integer('creator_id'); $table->timestamp('created_at')->nullable(); }); @@ -106,7 +139,7 @@ class SurveyWorkflowTest extends TestCase $book = \PhpOffice\PhpSpreadsheet\IOFactory::load($xlsxPath); $this->assertSame('=SUM(1,2)', $book->getSheetByName('答卷列表')->getCell('C2')->getValue()); $this->assertSame('s', $book->getSheetByName('答卷列表')->getCell('C2')->getDataType()); - $this->assertSame('调研附件.pdf', $book->getSheetByName('问题详细表')->getCell('J2')->getValue()); + $this->assertSame('调研附件.pdf', $book->getSheetByName('问题详细表')->getCell('E2')->getValue()); $entry = $book->getSheetByName('附件清单')->getCell('E2')->getValue(); $this->assertSame('example attachment', $zip->getFromName($entry)); $book->disconnectWorksheets(); @@ -209,10 +242,15 @@ class SurveyWorkflowTest extends TestCase file_put_contents($path.'.xlsx', $zip->getFromName('问卷答卷明细.xlsx')); $book = \PhpOffice\PhpSpreadsheet\IOFactory::load($path.'.xlsx'); $sheet = $book->getSheetByName('问题详细表'); - $this->assertSame('融资对接', $sheet->getCell('E2')->getValue()); - $this->assertSame('意向数量', $sheet->getCell('F2')->getValue()); - $this->assertSame('0', $sheet->getCell('J2')->getValue()); - $this->assertSame('否', $sheet->getCell('J3')->getValue()); + $this->assertSame('融资对接', $sheet->getCell('D2')->getValue()); + $this->assertSame('意向数量', $sheet->getCell('E2')->getValue()); + $this->assertSame('0', $sheet->getCell('F2')->getValue()); + $this->assertSame('否', $sheet->getCell('F3')->getValue()); + $this->assertContains('A2:A3', $sheet->getMergeCells()); + $this->assertContains('B2:B3', $sheet->getMergeCells()); + $this->assertContains('D2:D3', $sheet->getMergeCells()); + $this->assertSame('题号', $sheet->getCell('C1')->getValue()); + $this->assertSame('补充说明', $sheet->getCell('G1')->getValue()); $book->disconnectWorksheets(); $zip->close(); unlink($path.'.xlsx');