mirror of
https://github.com/kuaifan/dootask.git
synced 2025-12-15 13:22:49 +00:00
perf: 优化缩略图
This commit is contained in:
parent
50a7950ccd
commit
d799c06017
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
/** ******************************************************************************/
|
||||
/** ******************************************************************************/
|
||||
/** ******************************************************************************/
|
||||
|
||||
8
resources/assets/js/functions/common.js
vendored
8
resources/assets/js/functions/common.js
vendored
@ -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) {
|
||||
|
||||
103
resources/assets/js/functions/web.js
vendored
103
resources/assets/js/functions/web.js
vendored
@ -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, `<img class="${imgClassName}" src="$1">`)
|
||||
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, `<div class="no-size-image-box">${res}</div>`);
|
||||
@ -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 `<img class="${imgClassName}" style="width:${scale.width}px;height:${scale.height}px" src="${msg.thumb}">`
|
||||
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 `<img class="${imgClassName}" style="width:${scale.width}px;height:${scale.height}px" src="${data.thumb}">`
|
||||
}
|
||||
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
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div :class="`content-file ${msg.type}`">
|
||||
<div class="dialog-file">
|
||||
<img v-if="msg.type === 'img'" class="file-img" :style="imageStyle(msg)" :src="msg.thumb" @click="viewFile"/>
|
||||
<img v-if="msg.type === 'img'" class="file-img" :style="imageStyle(msg)" :src="imageSrc(msg)" @click="viewFile"/>
|
||||
<div v-else-if="isVideoFile(msg)" class="file-video" :style="imageStyle(msg)" @click="viewFile">
|
||||
<img v-if="msg.thumb" :src="msg.thumb">
|
||||
<video v-else :width="imageStyle(msg, 'width')" :height="imageStyle(msg, 'height')">
|
||||
@ -49,9 +49,16 @@ export default {
|
||||
return {};
|
||||
},
|
||||
|
||||
imageStyle(info, type = 'style') {
|
||||
const {width, height} = info;
|
||||
imageStyle({width, height, ext}, type = 'style') {
|
||||
if (width && height) {
|
||||
const ratioExceed = $A.imageRatioExceed(width, height)
|
||||
if (['png', 'jpg', 'jpeg'].includes(ext) && ratioExceed > 0) {
|
||||
if (width > height) {
|
||||
width = height * ratioExceed;
|
||||
} else {
|
||||
height = width * ratioExceed;
|
||||
}
|
||||
}
|
||||
let maxW = 220,
|
||||
maxH = 220,
|
||||
tempW = width,
|
||||
@ -82,6 +89,14 @@ export default {
|
||||
return {};
|
||||
},
|
||||
|
||||
imageSrc({width, height, ext, thumb}) {
|
||||
const ratioExceed = $A.imageRatioExceed(width, height)
|
||||
if (['png', 'jpg', 'jpeg'].includes(ext) && ratioExceed > 0) {
|
||||
thumb = $A.thumbRestore(thumb) + `/crop/ratio:${ratioExceed},percentage:320x0`;
|
||||
}
|
||||
return thumb;
|
||||
},
|
||||
|
||||
isVideoFile(msg) {
|
||||
return msg.type === 'file'
|
||||
&& msg.ext === 'mp4'
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user