From d799c0601709727e97d726548823cadd78005647 Mon Sep 17 00:00:00 2001 From: kuaifan Date: Tue, 29 Oct 2024 21:06:08 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=E7=BC=A9=E7=95=A5?= =?UTF-8?q?=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Exceptions/Handler.php | 136 ++++++++++++++++++ app/Models/File.php | 2 +- app/Module/Base.php | 5 +- app/Module/Image.php | 15 +- resources/assets/js/functions/common.js | 8 +- resources/assets/js/functions/web.js | 103 ++++++++++--- .../manage/components/DialogView/file.vue | 21 ++- 7 files changed, 257 insertions(+), 33 deletions(-) diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 9ce03c8fc..8c55b826c 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -3,9 +3,11 @@ namespace App\Exceptions; use App\Module\Base; +use App\Module\Image; use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; use Illuminate\Support\Facades\Log; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Throwable; class Handler extends ExceptionHandler @@ -51,6 +53,11 @@ class Handler extends ExceptionHandler */ public function render($request, Throwable $e) { + if ($e instanceof NotFoundHttpException) { + if ($result = $this->ImagePathHandler($request)) { + return $result; + } + } if ($e instanceof ApiException) { return response()->json(Base::retError($e->getMessage(), $e->getData(), $e->getCode())); } elseif ($e instanceof ModelNotFoundException) { @@ -78,4 +85,133 @@ class Handler extends ExceptionHandler parent::report($e); } } + + /** + * 图片路径处理 + * @param $request + * @return \Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\BinaryFileResponse|null + */ + private function ImagePathHandler($request) + { + $path = $request->path(); + + // 处理图片 + $pattern = '/^(uploads\/.*\.(png|jpg|jpeg))\/crop\/([^\/]+)$/'; + if (preg_match($pattern, $path, $matches)) { + // 获取参数 + $file = $matches[1]; + $ext = $matches[2]; + $rules = preg_replace('/\s+/', '', $matches[3]); + $rules = str_replace(['=', '&'], [':', ','], $rules); + $rules = explode(',', $rules); + if (empty($rules)) { + return null; + } + + // 提取年月 + $Ym = date("Ym"); + if (preg_match('/\/(\d{6})\//', $file, $ms)) { + $Ym = $ms[1]; + } + + // 文件存在直接返回 + $dirName = str_replace(['/', '.'], '_', $file); + $fileName = str_replace([':', ','], ['-', '_'], implode(',', $rules)) . '.' . $ext; + $savePath = public_path('uploads/tmp/crop/' . $Ym . '/' . $dirName . '/' . $fileName); + if (file_exists($savePath)) { + // 设置头部声明图片缓存 + return response()->file($savePath, [ + 'Pragma' => 'public', + 'Cache-Control' => 'max-age=1814400', + 'Expires' => gmdate('D, d M Y H:i:s', time() + 1814400) . ' GMT', + 'Last-Modified' => gmdate('D, d M Y H:i:s', filemtime($savePath)) . ' GMT', + 'ETag' => md5_file($savePath) + ]); + } + + // 文件不存在处理 + $sourcePath = public_path($file); + if (!file_exists($sourcePath)) { + return null; + } + + // 判断删除多余文件 + $saveDir = dirname($savePath); + if (is_dir($saveDir)) { + $items = glob($saveDir . '/*'); + if (count($items) > 5) { + usort($items, function ($a, $b) { + return filemtime($b) - filemtime($a); + }); + $itemsToDelete = array_slice($items, 5); + foreach ($itemsToDelete as $item) { + if (is_file($item)) { + unlink($item); + } + } + } + } else { + Base::makeDir($saveDir); + } + + // 处理图片 + try { + $handle = 0; + $image = new Image($sourcePath); + foreach ($rules as $rule) { + if (!str_contains($rule, ':')) { + continue; + } + [$type, $value] = explode(':', $rule); + if (!in_array($type, ['ratio', 'size', 'percentage', 'cover', 'contain'])) { + continue; + } + switch ($type) { + // 按比例裁剪 + case 'ratio': + if (is_numeric($value)) { + $image->ratioCrop($value); + $handle++; + } + break; + + // 按尺寸缩放 + case 'size': + $size = Base::newIntval(explode('x', $value)); + if (count($size) === 2) { + $image->resize($size[0], $size[1]); + $handle++; + } + break; + + // 按尺寸缩放 + case 'percentage': + case 'cover': + case 'contain': + $size = Base::newIntval(explode('x', $value)); + if (count($size) === 2) { + $image->thumb($size[0], $size[1], $type); + $handle++; + } + break; + } + } + if ($handle > 0) { + $image->saveTo($savePath); + Image::compressImage($savePath, null, 80); + return response()->file($savePath, [ + 'Pragma' => 'public', + 'Cache-Control' => 'max-age=1814400', + 'Expires' => gmdate('D, d M Y H:i:s', time() + 1814400) . ' GMT', + 'Last-Modified' => gmdate('D, d M Y H:i:s', filemtime($savePath)) . ' GMT', + 'ETag' => md5_file($savePath) + ]); + } else { + $image->destroy(); + } + } catch (\ImagickException) { } + } + + return null; + } } diff --git a/app/Models/File.php b/app/Models/File.php index eb3a3b3d3..b35d674a6 100644 --- a/app/Models/File.php +++ b/app/Models/File.php @@ -245,7 +245,7 @@ class File extends AbstractModel } } // - $path = 'uploads/tmp/' . date("Ym") . '/'; + $path = 'uploads/tmp/file/' . date("Ym") . '/'; $data = Base::upload([ "file" => Request::file('files'), "type" => 'more', diff --git a/app/Module/Base.php b/app/Module/Base.php index 850fb5151..170ff0027 100755 --- a/app/Module/Base.php +++ b/app/Module/Base.php @@ -2507,7 +2507,7 @@ class Base */ public static function isThumb($file): bool { - return preg_match('/_thumb\.(jpg|jpeg|png)$/', $file); + return preg_match('/_thumb\.(png|jpg|jpeg)$/', $file); } /** @@ -2535,7 +2535,8 @@ class Base */ public static function thumbRestore($file): string { - return preg_replace('/_thumb\.(jpg|jpeg|png)$/', '', $file); + $file = preg_replace('/_thumb\.(png|jpg|jpeg)$/', '', $file); + return preg_replace('/\/crop\/([^\/]+)$/', '', $file); } /** diff --git a/app/Module/Image.php b/app/Module/Image.php index 97f895f73..e0d8f6384 100644 --- a/app/Module/Image.php +++ b/app/Module/Image.php @@ -58,11 +58,11 @@ class Image /** * 按比例裁剪 - * @param int $ratio + * @param float $ratio * @return $this * @throws \ImagickException */ - public function ratioCrop(int $ratio = 0): static + public function ratioCrop(float $ratio = 0): static { if ($ratio === 0) { return $this; @@ -77,7 +77,7 @@ class Image $newHeight = $height; } elseif ($height > $width * $ratio) { $newWidth = $width; - $newHeight = $width * 3; + $newHeight = $width * $ratio; } else { return $this; } @@ -201,6 +201,15 @@ class Image $this->image->destroy(); } + /** + * 销毁对象 + * @return void + */ + public function destroy() + { + $this->image->destroy(); + } + /** ******************************************************************************/ /** ******************************************************************************/ /** ******************************************************************************/ diff --git a/resources/assets/js/functions/common.js b/resources/assets/js/functions/common.js index b047bef2c..b1bd05716 100755 --- a/resources/assets/js/functions/common.js +++ b/resources/assets/js/functions/common.js @@ -1067,11 +1067,13 @@ const timezone = require("dayjs/plugin/timezone"); * 等比缩放尺寸 * @param width * @param height - * @param maxWidth - * @param maxHeight + * @param maxW + * @param maxH * @returns {{width, height}|{width: number, height: number}} */ - scaleToScale(width, height, maxWidth, maxHeight) { + scaleToScale(width, height, maxW, maxH = undefined) { + const maxWidth = maxW; + const maxHeight = typeof maxH === "undefined" ? maxW : maxH; let tempWidth; let tempHeight; if (width > 0 && height > 0) { diff --git a/resources/assets/js/functions/web.js b/resources/assets/js/functions/web.js index c380a3dc5..b30e5ef0f 100755 --- a/resources/assets/js/functions/web.js +++ b/resources/assets/js/functions/web.js @@ -252,10 +252,22 @@ import {MarkdownPreview} from "../store/markdown"; const widthMatch = res.match("width=\"(\\d+)\""), heightMatch = res.match("height=\"(\\d+)\""); if (widthMatch && heightMatch) { - const width = parseInt(widthMatch[1]), - height = parseInt(heightMatch[1]), - maxSize = 40; - const scale = $A.scaleToScale(width, height, maxSize, maxSize); + const data = { + width: parseInt(widthMatch[1]), + height: parseInt(heightMatch[1]), + maxSize: 40, + src + } + const ratioExceed = $A.imageRatioExceed(data.width, data.height, 2) + if (ratioExceed > 0 && /\.(png|jpg|jpeg)$/.test(data.src)) { + src = $A.thumbRestore(data.src) + `/crop/ratio:${ratioExceed},percentage:80x0` + if (data.width > data.height) { + data.width = data.height * ratioExceed; + } else { + data.height = data.width * ratioExceed; + } + } + const scale = $A.scaleToScale(data.width, data.height, data.maxSize); imgClassName = `${imgClassName}" style="width:${scale.width}px;height:${scale.height}px` } return `[image:${src}]` @@ -274,10 +286,11 @@ import {MarkdownPreview} from "../store/markdown"; if (imgClassName) { text = text.replace(/\[image:(.*?)\]/g, ``) text = text.replace(/\{\{RemoteURL\}\}/g, this.apiUrl('../')) - } - const tmpText = text.substring(0, 30) - if (tmpText.length < text.length) { - text = tmpText + '...' + } else { + const tmpText = text.substring(0, 30) + if (tmpText.length < text.length) { + text = tmpText + '...' + } } return text }, @@ -339,13 +352,30 @@ import {MarkdownPreview} from "../store/markdown"; const widthMatch = res.match(widthReg), heightMatch = res.match(heightReg); if (widthMatch && heightMatch) { - const width = parseInt(widthMatch[1]), - height = parseInt(heightMatch[1]), - maxSize = res.indexOf("emoticon") > -1 ? 150 : 220; // 跟css中的设置一致 - const scale = $A.scaleToScale(width, height, maxSize, maxSize); - const value = res - .replace(widthReg, `original-width="${width}"`) - .replace(heightReg, `original-height="${height}" style="width:${scale.width}px;height:${scale.height}px"`) + const data = { + res, + width: parseInt(widthMatch[1]), + height: parseInt(heightMatch[1]), + maxSize: res.indexOf("emoticon") > -1 ? 150 : 220, // 跟css中的设置一致 + } + if (data.maxSize === 220) { + const ratioExceed = $A.imageRatioExceed(data.width, data.height) + if (ratioExceed > 0) { + const srcMatch = res.match(/src=(["'])(([^'"]*)\.(png|jpg|jpeg))\1/); + if (srcMatch) { + data.res = data.res.replace(srcMatch[2], $A.thumbRestore(srcMatch[2]) + `/crop/ratio:${ratioExceed},percentage:320x0`) + if (data.width > data.height) { + data.width = data.height * ratioExceed; + } else { + data.height = data.width * ratioExceed; + } + } + } + } + const scale = $A.scaleToScale(data.width, data.height, data.maxSize); + const value = data.res + .replace(widthReg, `original-width="${data.width}"`) + .replace(heightReg, `original-height="${data.height}" style="width:${scale.width}px;height:${scale.height}px"`) text = text.replace(res, value) } else { text = text.replace(res, `
${res}
`); @@ -432,11 +462,23 @@ import {MarkdownPreview} from "../store/markdown"; if (msg.type == 'img') { if (imgClassName) { // 缩略图,主要用于回复消息预览 - const width = parseInt(msg.width), - height = parseInt(msg.height), - maxSize = 40; - const scale = $A.scaleToScale(width, height, maxSize, maxSize); - return `` + const data = { + width: parseInt(msg.width), + height: parseInt(msg.height), + maxSize: 40, + thumb: msg.thumb + } + const ratioExceed = $A.imageRatioExceed(data.width, data.height, 2) + if (ratioExceed > 0 && /\.(png|jpg|jpeg)$/.test(data.thumb)) { + data.thumb = $A.thumbRestore(data.thumb) + `/crop/ratio:${ratioExceed},percentage:80x0` + if (data.width > data.height) { + data.width = data.height * ratioExceed; + } else { + data.height = data.width * ratioExceed; + } + } + const scale = $A.scaleToScale(data.width, data.height, data.maxSize); + return `` } return `[${$A.L('图片')}]` } else if (msg.ext == 'mp4') { @@ -500,7 +542,9 @@ import {MarkdownPreview} from "../store/markdown"; * @returns {*|string} */ thumbRestore(url) { - return `${url}`.replace(/_thumb\.(jpg|jpeg|png)$/, '') + return `${url}` + .replace(/_thumb\.(png|jpg|jpeg)$/, '') + .replace(/\/crop\/([^\/]+)$/, '') }, /** @@ -520,6 +564,23 @@ import {MarkdownPreview} from "../store/markdown"; return false; }, + /** + * 图片尺寸比例超出 + * @param width + * @param height + * @param ratio + * @param float + * @returns {number} + */ + imageRatioExceed(width, height, ratio = 3, float = 0.5) { + if (width && height) { + if (width / height > (ratio + float) || height / width > (ratio + float)) { + return ratio + } + } + return 0; + }, + /** * 加载 VConsole 日志组件 * @param key diff --git a/resources/assets/js/pages/manage/components/DialogView/file.vue b/resources/assets/js/pages/manage/components/DialogView/file.vue index c6e6f83a4..bc2088468 100644 --- a/resources/assets/js/pages/manage/components/DialogView/file.vue +++ b/resources/assets/js/pages/manage/components/DialogView/file.vue @@ -1,7 +1,7 @@