导入
This commit is contained in:
@@ -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 .= '<p>';
|
||||
$textNodes = $xpath->query('.//w:t', $node);
|
||||
foreach ($textNodes as $textNode) {
|
||||
$html .= htmlspecialchars($textNode->nodeValue);
|
||||
}
|
||||
$html .= '</p>';
|
||||
break;
|
||||
|
||||
case 'w:tbl': // 表格
|
||||
$html .= '<table border="1" style="border-collapse: collapse; width: 100%;">';
|
||||
$rows = $xpath->query('.//w:tr', $node);
|
||||
foreach ($rows as $row) {
|
||||
$html .= '<tr>';
|
||||
$cells = $xpath->query('.//w:tc', $row);
|
||||
foreach ($cells as $cell) {
|
||||
$html .= '<td>';
|
||||
$cellTextNodes = $xpath->query('.//w:t', $cell);
|
||||
foreach ($cellTextNodes as $cellText) {
|
||||
$html .= htmlspecialchars($cellText->nodeValue);
|
||||
}
|
||||
$html .= '</td>';
|
||||
}
|
||||
$html .= '</tr>';
|
||||
}
|
||||
$html .= '</table>';
|
||||
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 = '<strong>' . $text . '</strong>';
|
||||
}
|
||||
if ($italic) {
|
||||
$text = '<em>' . $text . '</em>';
|
||||
}
|
||||
if ($underline) {
|
||||
$text = '<u>' . $text . '</u>';
|
||||
}
|
||||
|
||||
$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('/<p>\s*<\/p>/', '', $html);
|
||||
|
||||
// 3. 确保图片有正确的样式
|
||||
$html = preg_replace_callback(
|
||||
'/<img([^>]+)>/',
|
||||
function($matches) {
|
||||
$attrs = $matches[1];
|
||||
|
||||
// 检查是否有样式
|
||||
if (!str_contains($attrs, 'style=')) {
|
||||
return '<img' . $attrs . ' style="max-width: 100%; height: auto;">';
|
||||
}
|
||||
|
||||
return '<img' . $attrs . '>';
|
||||
},
|
||||
$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(
|
||||
'<?xml encoding="UTF-8">' . $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 专属标签
|
||||
|
||||
Reference in New Issue
Block a user