diff --git a/app/admin/controller/SkuController.php b/app/admin/controller/SkuController.php index 2cde972..9673a67 100644 --- a/app/admin/controller/SkuController.php +++ b/app/admin/controller/SkuController.php @@ -256,7 +256,10 @@ class SkuController extends BaseController } /** - * docx 转 HTML(富文本可用,图片上传 OSS) + * docx 转 HTML(自动上传图片到 OSS) + * + * @param string $docxPath docx 文件绝对路径 + * @return string HTML */ function docxToHtmlWithImages(string $docxPath): string { @@ -264,6 +267,247 @@ class SkuController extends BaseController return ''; } + try { + // 1. 创建临时目录 + $tmpDir = runtime_path('tmp/' . uniqid('docx_', true)); + if (!is_dir($tmpDir)) { + mkdir($tmpDir, 0755, true); + } + + // 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); + } + + return $html; + } + + /** + * 将 XML 节点转换为 HTML + */ + private function convertNodeToHtml(DOMNode $node, DOMXPath $xpath): string + { + $html = ''; + $nodeName = $node->nodeName; + + switch ($nodeName) { + case 'w:p': // 段落 + $html .= '

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

'; + break; + + 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; + + 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; + + $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. 确保图片有正确的样式 + $html = preg_replace_callback( + '/]+)>/', + function($matches) { + $attrs = $matches[1]; + + // 检查是否有样式 + if (!str_contains($attrs, 'style=')) { + return ''; + } + + return ''; + }, + $html + ); + + return trim($html); + } + + /** + * 删除目录 + */ + private function deleteDirectory(string $dir): bool + { + if (!is_dir($dir)) { + return false; + } + + $files = array_diff(scandir($dir), ['.', '..']); + foreach ($files as $file) { + $path = $dir . '/' . $file; + is_dir($path) ? $this->deleteDirectory($path) : @unlink($path); + } + + return @rmdir($dir); + } + + + /** + * docx 转 HTML(富文本可用,图片上传 OSS) + */ + /*function docxToHtmlWithImages(string $docxPath): string + { + if (!is_file($docxPath)) { + return ''; + } + $phpWord = \PhpOffice\PhpWord\IOFactory::load($docxPath); $tmpDir = runtime_path('tmp/' . uniqid()); @@ -301,12 +545,10 @@ class SkuController extends BaseController ); // // 2️⃣ 关键:HTML 清洗(适配 wangEditor) -// $html = $this->cleanWordHtmlForWangEditor($html); - // 4. 清洗 HTML(关键) - $html = $this->cleanDocHtml($html); + $html = $this->cleanWordHtmlForWangEditor($html); return $html; - } + }*/ function deleteDir(string $dir): void { @@ -319,44 +561,6 @@ class SkuController extends BaseController @rmdir($dir); } - /** - * 清洗 doc/docx 转出的 HTML,适配富文本编辑器 - */ - function cleanDocHtml(string $html): string - { - libxml_use_internal_errors(true); - - $dom = new DOMDocument('1.0', 'UTF-8'); - $dom->loadHTML( - '' . $html, - LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD - ); - - $xpath = new DOMXPath($dom); - - // 1. 删除 head / meta / style / script - foreach (['head', 'meta', 'style', 'script', 'xml'] as $tag) { - foreach ($xpath->query("//{$tag}") as $node) { - $node->parentNode?->removeChild($node); - } - } - - // 2. 只取 body 内容 - $body = $dom->getElementsByTagName('body')->item(0); - if ($body) { - $innerHtml = ''; - foreach ($body->childNodes as $child) { - $innerHtml .= $dom->saveHTML($child); - } - } else { - $innerHtml = $dom->saveHTML(); - } - - libxml_clear_errors(); - - return trim($innerHtml); - } - function cleanWordHtmlForWangEditor(string $html): string { // 1. 移除 Word 专属标签