diff --git a/app/admin/controller/Post.php b/app/admin/controller/Post.php index ab113ec..4ce3d97 100644 --- a/app/admin/controller/Post.php +++ b/app/admin/controller/Post.php @@ -379,6 +379,7 @@ class Post extends Common View::assign('post', $model_post); View::assign('layoutContentHtml', $layoutContentHtml); View::assign('layoutConfig', $layoutConfig); + View::assign('lastOutputId', $postOutput ? $postOutput->id : 0); return View::fetch(); } @@ -431,6 +432,12 @@ class Post extends Common $phoneImage->savePageImage($output->id, $index + 1, $pageData); } + // 保存长图(如果有) + $longImage = $data['long_image'] ?? ''; + if (!empty($longImage)) { + $phoneImage->saveLongImage($output->id, $longImage); + } + $phoneImage->completeOutput($output->id, count($pages)); return json(['code' => 0, 'msg' => '保存成功', 'data' => ['output_id' => $output->id]]); @@ -580,6 +587,60 @@ class Post extends Common } } + /** + * 导出查看页面 + */ + public function outputView($id) + { + $output = \app\model\PostOutput::find((int) $id); + if (empty($output)) { + $this->error('输出记录不存在'); + } + + $post = ModelPost::find($output->post_id); + if (empty($post)) { + $this->error('文章不存在'); + } + + $allFiles = \app\model\PostOutputFile::where('output_id', (int) $id) + ->order('page', 'asc') + ->select(); + + // 分离长图和分页 + $longImage = null; + $pages = []; + foreach ($allFiles as $file) { + $item = [ + 'id' => $file->id, + 'page' => $file->page, + 'file_url' => $file->file_url, + 'width' => $file->width, + 'height' => $file->height, + 'image_src' => '', + ]; + // 优先使用 image_data (data URI) + if (!empty($file->image_data)) { + $item['image_src'] = 'data:image/jpeg;base64,' . $file->image_data; + } elseif (!empty($file->file_url)) { + $item['image_src'] = $file->file_url; + } + + if ($file->page == 0) { + $longImage = $item; + } else { + $pages[] = $item; + } + } + + View::assign('output', $output); + View::assign('post', $post); + View::assign('longImage', $longImage); + View::assign('pages', $pages); + View::assign('downloadZipUrl', url('post/downloadPostOutputZip', ['id' => $id])); + + return View::fetch('post/output_view'); + } + /** * 获取输出文件列表(AJAX) */ @@ -607,6 +668,7 @@ class Post extends Common 'file_size' => $file->file_size, 'width' => $file->width, 'height' => $file->height, + 'image_data' => $file->image_data ?: null, ]; } diff --git a/app/common/tools/PhoneImage.php b/app/common/tools/PhoneImage.php index 564b526..0988831 100644 --- a/app/common/tools/PhoneImage.php +++ b/app/common/tools/PhoneImage.php @@ -70,40 +70,69 @@ class PhoneImage implements PostOutputManagerInterface */ public function savePageImage(int $outputId, int $page, string $imageData) { - $imageData = str_replace('data:image/jpeg;base64,', '', $imageData); - $imageData = str_replace('data:image/png;base64,', '', $imageData); - $imageData = str_replace(' ', '+', $imageData); - $decoded = base64_decode($imageData); + // 保留原始base64用于存储 + $rawBase64 = str_replace('data:image/jpeg;base64,', '', $imageData); + $rawBase64 = str_replace('data:image/png;base64,', '', $rawBase64); + $rawBase64 = str_replace(' ', '+', $rawBase64); + $decoded = base64_decode($rawBase64); if ($decoded === false) { return false; } - $dateDir = date('Ymd'); - $relativeDir = '/upload/post_output/' . $dateDir; - $fileName = $outputId . '_' . $page . '.jpg'; - $relativePath = $relativeDir . '/' . $fileName; - $fullDir = App::getRootPath() . '/public' . $relativeDir; - $fullPath = $fullDir . '/' . $fileName; - - if (!is_dir($fullDir)) { - mkdir($fullDir, 0777, true); - } - - file_put_contents($fullPath, $decoded); - - $imageInfo = getimagesize($fullPath); - $width = $imageInfo[0] ?? 0; - $height = $imageInfo[1] ?? 0; + $imageInfo = @getimagesizefromstring($decoded); + $width = is_array($imageInfo) ? $imageInfo[0] : 0; + $height = is_array($imageInfo) ? $imageInfo[1] : 0; $fileRecord = PostOutputFile::create([ 'output_id' => $outputId, 'page' => $page, - 'file_path' => $relativePath, - 'file_url' => $relativePath, + 'file_path' => '', + 'file_url' => '', 'file_size' => strlen($decoded), 'width' => $width, 'height' => $height, + 'image_data' => $rawBase64, + ]); + + return $fileRecord; + } + + /** + * 保存长图 + * @param int $outputId 输出记录ID + * @param string $imageData base64编码的长图数据 + * @return PostOutputFile|false + */ + public function saveLongImage(int $outputId, string $imageData) + { + $rawBase64 = str_replace('data:image/jpeg;base64,', '', $imageData); + $rawBase64 = str_replace('data:image/png;base64,', '', $rawBase64); + $rawBase64 = str_replace(' ', '+', $rawBase64); + $decoded = base64_decode($rawBase64); + + if ($decoded === false) { + return false; + } + + $imageInfo = @getimagesizefromstring($decoded); + $width = is_array($imageInfo) ? $imageInfo[0] : 0; + $height = is_array($imageInfo) ? $imageInfo[1] : 0; + + // 先删除该output_id下已有的长图(page=0) + PostOutputFile::where('output_id', $outputId) + ->where('page', 0) + ->delete(); + + $fileRecord = PostOutputFile::create([ + 'output_id' => $outputId, + 'page' => 0, + 'file_path' => '', + 'file_url' => '', + 'file_size' => strlen($decoded), + 'width' => $width, + 'height' => $height, + 'image_data' => $rawBase64, ]); return $fileRecord; @@ -137,22 +166,42 @@ class PhoneImage implements PostOutputManagerInterface $zipFileName = 'post_output_' . $outputId . '.zip'; $zipPath = $tempDir . '/' . $zipFileName; + $tempFiles = []; - $zip = new \ZipArchive(); - if ($zip->open($zipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) { - throw new \Exception('无法创建ZIP文件'); - } + try { + $zip = new \ZipArchive(); + if ($zip->open($zipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) { + throw new \Exception('无法创建ZIP文件'); + } - foreach ($files as $file) { - $filePath = App::getRootPath() . '/public' . $file->file_path; - if (file_exists($filePath)) { + foreach ($files as $file) { $pageName = str_pad((string) $file->page, 2, '0', STR_PAD_LEFT) . '.jpg'; - $zip->addFile($filePath, $pageName); + + if (!empty($file->image_data)) { + $decoded = base64_decode($file->image_data); + if ($decoded !== false) { + $tempFilePath = $tempDir . '/page_' . $file->page . '_' . $outputId . '.jpg'; + file_put_contents($tempFilePath, $decoded); + $tempFiles[] = $tempFilePath; + $zip->addFile($tempFilePath, $pageName); + } + } else { + $filePath = App::getRootPath() . '/public' . $file->file_path; + if (file_exists($filePath)) { + $zip->addFile($filePath, $pageName); + } + } + } + + $zip->close(); + } finally { + foreach ($tempFiles as $tempFile) { + if (file_exists($tempFile)) { + @unlink($tempFile); + } } } - $zip->close(); - return $zipPath; } diff --git a/database/migrations/20260513000000_add_image_data_to_post_output_file.php b/database/migrations/20260513000000_add_image_data_to_post_output_file.php new file mode 100644 index 0000000..f405378 --- /dev/null +++ b/database/migrations/20260513000000_add_image_data_to_post_output_file.php @@ -0,0 +1,14 @@ +table('post_output_file'); + $table->addColumn(Column::make('image_data', 'text', ['limit' => \Phinx\Db\Adapter\MysqlAdapter::TEXT_LONG])->setComment('base64图片数据')->setNull(true)); + $table->update(); + } +} diff --git a/public/static/js/phone-image.js b/public/static/js/phone-image.js index 476337d..019a41a 100644 --- a/public/static/js/phone-image.js +++ b/public/static/js/phone-image.js @@ -912,14 +912,15 @@ var PhoneImageEngine = (function () { return; } var canvas = document.createElement('canvas'); - canvas.width = width * 2; // scale:2 高质量 - canvas.height = height * 2; + // 使用原图尺寸,保持原始清晰度 + canvas.width = img.naturalWidth; + canvas.height = img.naturalHeight; var ctx = canvas.getContext('2d'); // 白色背景 ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, canvas.width, canvas.height); - // 缩放绘制 - ctx.drawImage(img, 0, 0, canvas.width, canvas.height); + // 按原图尺寸绘制,不缩放 + ctx.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight); deferred.resolve(canvas); }; img.onerror = function () { @@ -1179,6 +1180,7 @@ var PhoneImageEngine = (function () { function saveImages(postId, saveConfig, onProgress) { var deferred = $.Deferred(); + // 第一步:生成分页图片和长图 capturePagesFromStaging().then(function (canvases) { if (onProgress) onProgress(canvases.length, canvases.length, null); var pagesData = []; @@ -1186,19 +1188,27 @@ var PhoneImageEngine = (function () { pagesData.push(canvases[i].toDataURL('image/jpeg', 0.92)); } + // 第二步:生成长图 + return generateLongImageBase64().then(function(longBase64) { + return { pages: pagesData, longImage: longBase64 }; + }); + }).then(function(result) { + var pagesData = result.pages; + var longImageData = result.longImage; + // 检查数据总大小,防止超大文章导致请求失败 var totalSize = 0; for (var i = 0; i < pagesData.length; i++) { - if (pagesData[i]) { - totalSize += pagesData[i].length; - } + if (pagesData[i]) totalSize += pagesData[i].length; } + if (longImageData) totalSize += longImageData.length; if (totalSize > 16 * 1024 * 1024) { layer.msg('数据量过大(超过16MB),请减少内容或联系管理员'); deferred.reject('数据量过大(超过16MB)'); return; } + // 第三步:发送到后端 $.ajax({ url: '/index.php/admin/post/savePostOutput', type: 'POST', @@ -1207,7 +1217,8 @@ var PhoneImageEngine = (function () { output_type: 'phone_image', config: saveConfig || config, content_html: postData.content_html, - pages: pagesData + pages: pagesData, + long_image: longImageData }), contentType: 'application/json', success: function (result) { @@ -1218,6 +1229,20 @@ var PhoneImageEngine = (function () { } }, error: function (xhr) { + // 容错:后端可能返回200 JSON + 500错误页面的混合响应 + // 尝试从响应文本开头提取有效JSON + try { + var text = xhr.responseText || ''; + var jsonEnd = text.indexOf('}'); + if (jsonEnd > 0) { + var jsonStr = text.substring(0, jsonEnd + 1); + var parsed = JSON.parse(jsonStr); + if (parsed && parsed.code === 0) { + deferred.resolve(parsed.data || {}); + return; + } + } + } catch (e) { /* 解析失败,走正常错误流程 */ } deferred.reject('网络错误: ' + xhr.statusText); } }); @@ -1262,6 +1287,19 @@ var PhoneImageEngine = (function () { } }, error: function (xhr) { + // 容错:后端可能返回200 JSON + 500错误页面的混合响应 + try { + var text = xhr.responseText || ''; + var jsonEnd = text.indexOf('}'); + if (jsonEnd > 0) { + var jsonStr = text.substring(0, jsonEnd + 1); + var parsed = JSON.parse(jsonStr); + if (parsed && parsed.code === 0) { + deferred.resolve(parsed.data || {}); + return; + } + } + } catch (e) { /* 解析失败,走正常错误流程 */ } deferred.reject('网络错误: ' + xhr.statusText); } }); @@ -1283,6 +1321,68 @@ var PhoneImageEngine = (function () { config.pageAlignments[pageNum] = align; } + // ===== 长图base64生成(不触发下载) ===== + + /** + * 生成长图base64(不触发下载) + * @returns {jQuery Deferred} resolves with base64 string or null + */ + function generateLongImageBase64() { + var deferred = $.Deferred(); + + var editorChildren = getEditorChildren(); + if (editorChildren.length === 0) { + deferred.resolve(null); + return deferred.promise(); + } + + var $container = $('