diff --git a/app/Support/StudyTourDeclarationExporter.php b/app/Support/StudyTourDeclarationExporter.php index 462992e..3bf427f 100644 --- a/app/Support/StudyTourDeclarationExporter.php +++ b/app/Support/StudyTourDeclarationExporter.php @@ -264,6 +264,11 @@ class StudyTourDeclarationExporter $full = storage_path('app/public/'.$rel); $real = realpath($full); $root = realpath(storage_path('app/public')); + if ($real === false || $root === false || ! is_file($real)) { + $full = public_path('storage/'.$rel); + $real = realpath($full); + $root = realpath(public_path('storage')); + } if ($real === false || $root === false || ! is_file($real)) { return null; } @@ -393,7 +398,7 @@ class StudyTourDeclarationExporter return; } - $size = self::coverImageSize($path); + $size = self::fitImageSize($path, 360, 240); try { $section->addImage($path, [ 'width' => $size['width'], @@ -409,15 +414,13 @@ class StudyTourDeclarationExporter /** * @return array{width: int, height: int} */ - private static function coverImageSize(string $path): array + private static function fitImageSize(string $path, int $maxW, int $maxH): array { - $maxW = 360; - $maxH = 240; $info = @getimagesize($path); $w = is_array($info) ? (int) ($info[0] ?? 0) : 0; $h = is_array($info) ? (int) ($info[1] ?? 0) : 0; if ($w <= 0 || $h <= 0) { - return ['width' => $maxW, 'height' => 180]; + return ['width' => $maxW, 'height' => (int) max(1, round($maxH * 0.75))]; } $scale = min($maxW / $w, $maxH / $h, 1); @@ -429,18 +432,77 @@ class StudyTourDeclarationExporter private static function addHtmlAsParagraphs($section, string $html): void { - $text = self::htmlToPlainText($html); - if ($text === '') { + $html = trim($html); + $hasImage = (bool) preg_match('/addText('(暂无)', ['color' => '86909C'], ['spaceAfter' => 160]); return; } - foreach (preg_split('/\R/u', $text) ?: [] as $line) { - $line = trim($line); - if ($line === '') { + + $parts = preg_split('/(]*>)/iu', $html, -1, PREG_SPLIT_DELIM_CAPTURE) ?: [$html]; + $wrote = false; + foreach ($parts as $part) { + $part = trim((string) $part); + if ($part === '') { + continue; + } + if (preg_match('/^addText($line, [], ['spaceAfter' => 80]); + foreach (preg_split('/\R/u', $text) ?: [] as $line) { + $line = trim($line); + if ($line === '') { + continue; + } + $section->addText($line, [], ['spaceAfter' => 80]); + $wrote = true; + } + } + + if (! $wrote) { + $section->addText('(暂无)', ['color' => '86909C'], ['spaceAfter' => 160]); + } + } + + private static function imgSrcFromTag(string $imgTag): ?string + { + if (! preg_match('/\bsrc\s*=\s*(?:(["\'])(.*?)\1|([^\s>]+))/iu', $imgTag, $matches)) { + return null; + } + $quoted = trim((string) ($matches[2] ?? '')); + $bare = trim((string) ($matches[3] ?? '')); + $src = html_entity_decode($quoted !== '' ? $quoted : $bare, ENT_QUOTES | ENT_HTML5, 'UTF-8'); + $src = trim($src); + + return $src === '' ? null : $src; + } + + private static function addInlineImage($section, ?string $src, int $maxW, int $maxH): void + { + $path = self::resolvePublicImagePath($src); + if ($path === null) { + $section->addText('(图片无法嵌入文档)', ['color' => '86909C'], ['spaceAfter' => 80]); + + return; + } + + $size = self::fitImageSize($path, $maxW, $maxH); + try { + $section->addImage($path, [ + 'width' => $size['width'], + 'height' => $size['height'], + 'wrappingStyle' => 'inline', + ]); + $section->addTextBreak(1); + } catch (\Throwable) { + $section->addText('(图片无法嵌入文档)', ['color' => '86909C'], ['spaceAfter' => 80]); } } diff --git a/tests/Unit/StudyTourDeclarationExporterTest.php b/tests/Unit/StudyTourDeclarationExporterTest.php index e686fb2..f287cfc 100644 --- a/tests/Unit/StudyTourDeclarationExporterTest.php +++ b/tests/Unit/StudyTourDeclarationExporterTest.php @@ -33,6 +33,12 @@ class StudyTourDeclarationExporterTest extends TestCase file_put_contents($png, base64_decode( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==' )); + $uploadDir = storage_path('app/public/uploads'); + if (! is_dir($uploadDir)) { + mkdir($uploadDir, 0777, true); + } + $implPng = $uploadDir.'/st-impl-test.png'; + copy($png, $implPng); $path = StudyTourDeclarationExporter::writeDocxFile([ 'name' => '太湖蚕桑研学', @@ -60,7 +66,7 @@ class StudyTourDeclarationExporterTest extends TestCase 'content' => '了解传统缫丝工艺', ]], 'fee_html' => '

680元/人

', - 'implementation_html' => '

2026年7月1日第1场

', + 'implementation_html' => '

2026年7月1日第1场

', 'season_options' => [ ['value' => 'spring', 'label' => '春季'], ['value' => 'summer', 'label' => '夏季'], @@ -105,15 +111,17 @@ class StudyTourDeclarationExporterTest extends TestCase '缫丝体验', '680元/人', '封面图', + '2026年7月1日第1场', ] as $needle) { $this->assertStringContainsString($needle, $xml, "missing {$needle}"); } - $this->assertGreaterThan(0, $mediaCount, 'cover image should be embedded'); + $this->assertGreaterThanOrEqual(2, $mediaCount, 'cover and implementation images should be embedded'); $phpWord = IOFactory::load($path); $this->assertNotEmpty($phpWord->getSections()); @unlink($path); @unlink($png); + @unlink($implPng); } }