From 65df91fa68b6c6b70c12caa5605c8132751c8774 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Tue, 16 Dec 2025 15:42:51 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=BC=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/controller/SkuController.php | 275 ++++++------------------- 1 file changed, 63 insertions(+), 212 deletions(-) diff --git a/app/admin/controller/SkuController.php b/app/admin/controller/SkuController.php index 9673a67..3730da4 100644 --- a/app/admin/controller/SkuController.php +++ b/app/admin/controller/SkuController.php @@ -256,10 +256,7 @@ class SkuController extends BaseController } /** - * docx 转 HTML(自动上传图片到 OSS) - * - * @param string $docxPath docx 文件绝对路径 - * @return string HTML + * docx 转 HTML(富文本可用,图片上传 OSS) */ function docxToHtmlWithImages(string $docxPath): string { @@ -267,238 +264,92 @@ class SkuController extends BaseController return ''; } - try { - // 1. 创建临时目录 - $tmpDir = runtime_path('tmp/' . uniqid('docx_', true)); - if (!is_dir($tmpDir)) { - mkdir($tmpDir, 0755, true); - } + // 1. 加载 docx + $phpWord = WordIOFactory::load($docxPath, 'Word2007'); - // 2. 解压 docx 文件(docx 本质上是 zip 文件) - $zip = new ZipArchive(); - if ($zip->open($docxPath) === true) { - $zip->extractTo($tmpDir); - $zip->close(); - } else { - throw new Exception('无法解压 docx 文件'); - } - - // 3. 读取文档内容 - $documentXml = $tmpDir . '/word/document.xml'; - if (!is_file($documentXml)) { - throw new Exception('文档内容文件不存在'); - } - - // 4. 创建 DOM 对象 - $dom = new DOMDocument(); - $dom->load($documentXml); - - // 5. 处理图片 - $uploadService = new UploadService(); - $images = $dom->getElementsByTagName('drawing'); - - foreach ($images as $image) { - // 查找图片引用 - $blip = $image->getElementsByTagName('blip'); - if ($blip->length > 0) { - $embedId = $blip->item(0)->getAttribute('r:embed'); - if ($embedId) { - // 查找关系文件中的图片路径 - $relationshipsXml = $tmpDir . '/word/_rels/document.xml.rels'; - if (is_file($relationshipsXml)) { - $relsDom = new DOMDocument(); - $relsDom->load($relationshipsXml); - - $xpath = new DOMXPath($relsDom); - $xpath->registerNamespace('r', 'http://schemas.openxmlformats.org/package/2006/relationships'); - - $query = "//r:Relationship[@Id='$embedId']"; - $nodes = $xpath->query($query); - - if ($nodes->length > 0) { - $target = $nodes->item(0)->getAttribute('Target'); - $imagePath = $tmpDir . '/word/' . $target; - - if (is_file($imagePath)) { - // 上传到 OSS - $ossUrl = $uploadService->uploadLocalFile([ - 'path' => $imagePath, - 'scene' => 'desc' - ]); - - // 创建 img 标签 - $imgNode = $dom->createElement('img'); - $imgNode->setAttribute('src', $ossUrl); - $imgNode->setAttribute('alt', '图片'); - - // 替换 drawing 标签 - $image->parentNode->replaceChild($imgNode, $image); - } - } - } - } - } - } - - // 6. 将文档内容转换为 HTML - $htmlContent = $this->convertDocumentXmlToHtml($dom, $tmpDir); - - // 7. 清理 HTML - $htmlContent = $this->cleanHtmlContent($htmlContent); - - // 8. 清理临时目录 - $this->deleteDirectory($tmpDir); - - return $htmlContent; - - } catch (\Exception $e) { - // 清理临时目录 - if (isset($tmpDir) && is_dir($tmpDir)) { - $this->deleteDirectory($tmpDir); - } - - error_log("docx转HTML失败: " . $e->getMessage()); - return ''; - } - } - - /** - * 将 document.xml 转换为 HTML - */ - private function convertDocumentXmlToHtml(DOMDocument $dom, string $tmpDir): string - { - $xpath = new DOMXPath($dom); - $xpath->registerNamespace('w', 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'); - - $html = ''; - $bodyNodes = $xpath->query('//w:body/w:*'); - - foreach ($bodyNodes as $node) { - $html .= $this->convertNodeToHtml($node, $xpath); + // 2. 临时目录(非常重要) + $tmpDir = runtime_path('tmp/' . uniqid()); + if (!is_dir($tmpDir)) { + mkdir($tmpDir, 0755, true); } - return $html; - } + $tmpHtml = $tmpDir . '/index.html'; - /** - * 将 XML 节点转换为 HTML - */ - private function convertNodeToHtml(DOMNode $node, DOMXPath $xpath): string - { - $html = ''; - $nodeName = $node->nodeName; + // 3. 导出 HTML + $writer = WordIOFactory::createWriter($phpWord, 'HTML'); + $writer->save($tmpHtml); - switch ($nodeName) { - case 'w:p': // 段落 - $html .= '

'; - $textNodes = $xpath->query('.//w:t', $node); - foreach ($textNodes as $textNode) { - $html .= htmlspecialchars($textNode->nodeValue); - } - $html .= '

'; - break; + $html = file_get_contents($tmpHtml); - case 'w:tbl': // 表格 - $html .= ''; - $rows = $xpath->query('.//w:tr', $node); - foreach ($rows as $row) { - $html .= ''; - $cells = $xpath->query('.//w:tc', $row); - foreach ($cells as $cell) { - $html .= ''; - } - $html .= ''; - } - $html .= '
'; - $cellTextNodes = $xpath->query('.//w:t', $cell); - foreach ($cellTextNodes as $cellText) { - $html .= htmlspecialchars($cellText->nodeValue); - } - $html .= '
'; - break; + // 4. 清洗 Word 垃圾 HTML(关键) + $html = $this->cleanWordHtml($html); - case 'w:r': // 文本运行(可能是加粗等格式) - $bold = $xpath->query('.//w:b', $node)->length > 0; - $italic = $xpath->query('.//w:i', $node)->length > 0; - $underline = $xpath->query('.//w:u', $node)->length > 0; + $uploadService = new UploadService(); - $text = ''; - $textNodes = $xpath->query('.//w:t', $node); - foreach ($textNodes as $textNode) { - $text .= htmlspecialchars($textNode->nodeValue); - } - - if ($bold) { - $text = '' . $text . ''; - } - if ($italic) { - $text = '' . $text . ''; - } - if ($underline) { - $text = '' . $text . ''; - } - - $html .= $text; - break; - - default: - // 处理其他节点 - break; - } - - return $html; - } - - /** - * 清理 HTML 内容 - */ - private function cleanHtmlContent(string $html): string - { - // 1. 清理多余的空格和换行 - $html = preg_replace('/\s+/', ' ', $html); - $html = preg_replace('/>\s+<', $html); - - // 2. 清理空的段落 - $html = preg_replace('/

\s*<\/p>/', '', $html); - - // 3. 确保图片有正确的样式 + // 5. 处理图片 $html = preg_replace_callback( - '/]+)>/', - function($matches) { - $attrs = $matches[1]; + '/]+src="([^"]+)"[^>]*>/i', + function ($matches) use ($tmpDir, $uploadService) { + $src = $matches[1]; - // 检查是否有样式 - if (!str_contains($attrs, 'style=')) { - return ''; + // 只处理本地图片 + if (preg_match('#^(http|https|data:)#i', $src)) { + return $matches[0]; } - return ''; + $localImg = realpath($tmpDir . '/' . $src); + if (!$localImg || !is_file($localImg)) { + return $matches[0]; + } + + // 上传 OSS + $ossUrl = $uploadService->uploadLocalFile([ + 'path' => $localImg, + 'scene' => 'desc' + ]); + + return str_replace($src, $ossUrl, $matches[0]); }, $html ); + // 6. 清理临时目录 + $this->deleteDir($tmpDir); + return trim($html); } - /** - * 删除目录 - */ - private function deleteDirectory(string $dir): bool + function cleanWordHtml(string $html): string { - if (!is_dir($dir)) { - return false; + // 删除 Word 注释 + $html = preg_replace('//', '', $html); + + // 删除 style / meta / link + $html = preg_replace('/<(style|meta|link)[^>]*>.*?<\/\1>/is', '', $html); + + // 删除 xml 标签 + $html = preg_replace('/.*?<\/xml>/is', '', $html); + + // 删除 mso-xxx 样式 + $html = preg_replace('/\s*mso-[^:]+:[^;"]+;?/i', '', $html); + + // 清理 class="Mso..." + $html = preg_replace('/class="Mso[^"]*"/i', '', $html); + + // 删除空 span + $html = preg_replace('/]*>\s*<\/span>/i', '', $html); + + // 删除 Office 条件注释 + $html = preg_replace('/.*?/is', '', $html); + + // 只保留 body 内容 + if (preg_match('/]*>(.*?)<\/body>/is', $html, $m)) { + $html = $m[1]; } - $files = array_diff(scandir($dir), ['.', '..']); - foreach ($files as $file) { - $path = $dir . '/' . $file; - is_dir($path) ? $this->deleteDirectory($path) : @unlink($path); - } - - return @rmdir($dir); + return $html; } - /** * docx 转 HTML(富文本可用,图片上传 OSS) */