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 .= '| '; + $cellTextNodes = $xpath->query('.//w:t', $cell); + foreach ($cellTextNodes as $cellText) { + $html .= htmlspecialchars($cellText->nodeValue); + } + $html .= ' | '; + } + $html .= '
\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 专属标签