feat(图片处理): 添加图片压缩工具类并集成到上传组件
实现基于CompressorJS的图片压缩功能,添加imageCompressor工具类提供多种压缩方法 修改商品表单、店铺表单等上传组件使用新的压缩方法,统一压缩配置为质量0.7,最大尺寸980px 在package.json中添加zip打包脚本
This commit is contained in:
parent
8819f17aa0
commit
295dc7a8cf
|
|
@ -5,7 +5,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "NODE_OPTIONS=--max-old-space-size=4096 vite",
|
"dev": "NODE_OPTIONS=--max-old-space-size=4096 vite",
|
||||||
"serve": "pnpm dev",
|
"serve": "pnpm dev",
|
||||||
"build": "rimraf dist && NODE_OPTIONS=--max-old-space-size=8192 vite build",
|
"build": "rimraf dist && NODE_OPTIONS=--max-old-space-size=8192 vite build && npm run zip",
|
||||||
"build:staging": "rimraf dist && vite build --mode staging",
|
"build:staging": "rimraf dist && vite build --mode staging",
|
||||||
"report": "rimraf dist && vite build",
|
"report": "rimraf dist && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
|
|
@ -21,7 +21,8 @@
|
||||||
"lint:pretty": "pretty-quick --staged",
|
"lint:pretty": "pretty-quick --staged",
|
||||||
"lint": "pnpm lint:eslint && pnpm lint:prettier && pnpm lint:stylelint",
|
"lint": "pnpm lint:eslint && pnpm lint:prettier && pnpm lint:stylelint",
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"preinstall": "npx only-allow pnpm"
|
"preinstall": "npx only-allow pnpm",
|
||||||
|
"zip": "7z a -tzip dist/shop-front-end-%date:~0,4%%date:~5,2%%date:~8,2%-%time:~0,2%%time:~3,2%%time:~6,2%.zip .\\dist\\* -xr!*.zip"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"> 1%",
|
"> 1%",
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { useResizeObserver } from "@vueuse/core";
|
||||||
import { longpress } from "@/directives/longpress";
|
import { longpress } from "@/directives/longpress";
|
||||||
import { useTippy, directive as tippy } from "vue-tippy";
|
import { useTippy, directive as tippy } from "vue-tippy";
|
||||||
import { delay, debounce, isArray, downloadByBase64 } from "@pureadmin/utils";
|
import { delay, debounce, isArray, downloadByBase64 } from "@pureadmin/utils";
|
||||||
|
import { beforeImageUpload } from "@/utils/imageCompressor";
|
||||||
import {
|
import {
|
||||||
ref,
|
ref,
|
||||||
unref,
|
unref,
|
||||||
|
|
@ -218,9 +219,19 @@ export default defineComponent({
|
||||||
: cropper.value?.[event]?.(arg);
|
: cropper.value?.[event]?.(arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
function beforeUpload(file) {
|
async function beforeUpload(file: File) {
|
||||||
|
// 压缩图片
|
||||||
|
const compressedFile = await beforeImageUpload(file, {
|
||||||
|
quality: 0.7,
|
||||||
|
maxWidth: 980,
|
||||||
|
maxHeight: 980
|
||||||
|
});
|
||||||
|
|
||||||
|
// 使用压缩后的文件或原文件
|
||||||
|
const finalFile = (compressedFile as File) || file;
|
||||||
|
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(finalFile);
|
||||||
inSrc.value = "";
|
inSrc.value = "";
|
||||||
reader.onload = e => {
|
reader.onload = e => {
|
||||||
inSrc.value = e.target?.result as string;
|
inSrc.value = e.target?.result as string;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,235 @@
|
||||||
|
/**
|
||||||
|
* 图片压缩工具类
|
||||||
|
* 基于 CompressorJS 实现图片上传前压缩
|
||||||
|
*/
|
||||||
|
import Compressor from 'compressorjs';
|
||||||
|
|
||||||
|
export interface CompressorOptions {
|
||||||
|
/** 压缩质量,0-1 之间,值越小文件越小但质量越差 */
|
||||||
|
quality?: number;
|
||||||
|
/** 最大宽度(像素) */
|
||||||
|
maxWidth?: number;
|
||||||
|
/** 最大高度(像素) */
|
||||||
|
maxHeight?: number;
|
||||||
|
/** 最小宽度 */
|
||||||
|
minWidth?: number;
|
||||||
|
/** 最小高度 */
|
||||||
|
minHeight?: number;
|
||||||
|
/** 固定宽度 */
|
||||||
|
width?: number;
|
||||||
|
/** 固定高度 */
|
||||||
|
height?: number;
|
||||||
|
/** 调整大小模式:'none' | 'contain' | 'cover' */
|
||||||
|
resize?: 'none' | 'contain' | 'cover';
|
||||||
|
/** 是否裁剪 */
|
||||||
|
crop?: boolean;
|
||||||
|
/** 旋转角度(度) */
|
||||||
|
rotate?: number;
|
||||||
|
/** 模糊半径 */
|
||||||
|
blur?: number;
|
||||||
|
/** 是否转为灰度图 */
|
||||||
|
grayscale?: boolean;
|
||||||
|
/** 输出类型:'blob' | 'base64' | 'datauristring' */
|
||||||
|
output?: 'blob' | 'base64' | 'datauristring';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BeforeUploadResult {
|
||||||
|
file: File;
|
||||||
|
compressed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查文件是否为图片
|
||||||
|
* @param file 上传的文件
|
||||||
|
*/
|
||||||
|
export const isImage = (file: File): boolean => {
|
||||||
|
return /^image\//.test(file.type);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认压缩配置
|
||||||
|
*/
|
||||||
|
const DEFAULT_COMPRESS_OPTIONS: Required<CompressorOptions> = {
|
||||||
|
quality: 0.8,
|
||||||
|
maxWidth: 1280,
|
||||||
|
maxHeight: 1280,
|
||||||
|
minWidth: 0,
|
||||||
|
minHeight: 0,
|
||||||
|
width: Infinity,
|
||||||
|
height: Infinity,
|
||||||
|
resize: 'cover',
|
||||||
|
crop: false,
|
||||||
|
rotate: 0,
|
||||||
|
blur: 0,
|
||||||
|
grayscale: false,
|
||||||
|
output: 'blob'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 压缩单个图片文件
|
||||||
|
* @param file 原始文件
|
||||||
|
* @param options 压缩配置
|
||||||
|
* @returns 压缩后的文件对象
|
||||||
|
*/
|
||||||
|
export const compressImage = (
|
||||||
|
file: File,
|
||||||
|
options: CompressorOptions = {}
|
||||||
|
): Promise<File> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// 合并默认配置
|
||||||
|
const mergedOptions = { ...DEFAULT_COMPRESS_OPTIONS, ...options };
|
||||||
|
|
||||||
|
new Compressor(file, {
|
||||||
|
...mergedOptions,
|
||||||
|
success(result) {
|
||||||
|
try {
|
||||||
|
// 转换为 File 对象,保持原始文件名
|
||||||
|
const targetType = file.type.includes('png') ? 'image/png' : 'image/jpeg';
|
||||||
|
const compressedFile = new File([result], file.name, {
|
||||||
|
type: targetType,
|
||||||
|
lastModified: Date.now()
|
||||||
|
});
|
||||||
|
resolve(compressedFile);
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error(err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量压缩图片文件
|
||||||
|
* @param files 文件数组
|
||||||
|
* @param options 压缩配置
|
||||||
|
* @returns 压缩后的文件数组
|
||||||
|
*/
|
||||||
|
export const compressImages = async (
|
||||||
|
files: File[],
|
||||||
|
options: CompressorOptions = {}
|
||||||
|
): Promise<File[]> => {
|
||||||
|
const promises = files.map(file => {
|
||||||
|
if (isImage(file)) {
|
||||||
|
return compressImage(file, options);
|
||||||
|
}
|
||||||
|
return Promise.resolve(file);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(promises);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 带错误处理的图片压缩
|
||||||
|
* 压缩失败时返回原文件
|
||||||
|
* @param file 原始文件
|
||||||
|
* @param options 压缩配置
|
||||||
|
* @returns { file, compressed } 压缩结果
|
||||||
|
*/
|
||||||
|
export const compressImageSafe = async (
|
||||||
|
file: File,
|
||||||
|
options: CompressorOptions = {}
|
||||||
|
): Promise<BeforeUploadResult> => {
|
||||||
|
try {
|
||||||
|
// 仅压缩图片文件
|
||||||
|
if (!isImage(file)) {
|
||||||
|
return { file, compressed: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const compressedFile = await compressImage(file, options);
|
||||||
|
return {
|
||||||
|
file: compressedFile,
|
||||||
|
compressed: true
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('图片压缩失败,使用原文件:', error);
|
||||||
|
return { file, compressed: false };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Element Plus :before-upload 钩子函数
|
||||||
|
* 自动压缩图片并返回压缩后的文件
|
||||||
|
* @param rawFile 原始文件
|
||||||
|
* @param options 压缩配置
|
||||||
|
* @returns Promise<File | boolean> 返回文件或布尔值
|
||||||
|
*/
|
||||||
|
export const beforeImageUpload = (
|
||||||
|
rawFile: File,
|
||||||
|
options: CompressorOptions = {}
|
||||||
|
): Promise<File | boolean> => {
|
||||||
|
// 首先进行基本验证
|
||||||
|
const isValidImage = isImage(rawFile);
|
||||||
|
const isValidType = ['image/jpeg', 'image/png', 'image/webp'].includes(rawFile.type);
|
||||||
|
const isValidSize = rawFile.size < 50 * 1024 * 1024; // 50MB
|
||||||
|
|
||||||
|
if (!isValidImage || !isValidType) {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValidSize) {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 压缩图片
|
||||||
|
return compressImageSafe(rawFile, options).then(result => {
|
||||||
|
return result.file;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Element Plus :before-upload 钩子函数(严格模式)
|
||||||
|
* 压缩失败时拒绝上传
|
||||||
|
* @param rawFile 原始文件
|
||||||
|
* @param options 压缩配置
|
||||||
|
* @returns Promise<File | boolean> 返回文件或布尔值
|
||||||
|
*/
|
||||||
|
export const beforeImageUploadStrict = (
|
||||||
|
rawFile: File,
|
||||||
|
options: CompressorOptions = {}
|
||||||
|
): Promise<File | boolean> => {
|
||||||
|
// 首先进行基本验证
|
||||||
|
if (!['image/jpeg', 'image/png', 'image/webp'].includes(rawFile.type)) {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rawFile.size > 50 * 1024 * 1024) {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 压缩图片,失败则拒绝上传
|
||||||
|
return compressImage(rawFile, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图片质量级别枚举
|
||||||
|
*/
|
||||||
|
export enum ImageQuality {
|
||||||
|
/** 高质量(适合产品图、头像) */
|
||||||
|
HIGH = 0.9,
|
||||||
|
/** 中等质量(适合一般场景) */
|
||||||
|
MEDIUM = 0.8,
|
||||||
|
/** 高压缩(适合缩略图、证明材料) */
|
||||||
|
LOW = 0.6
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据质量级别获取压缩配置
|
||||||
|
* @param quality 质量级别
|
||||||
|
* @param maxWidth 最大宽度
|
||||||
|
* @param maxHeight 最大高度
|
||||||
|
* @returns 压缩配置
|
||||||
|
*/
|
||||||
|
export const getQualityConfig = (
|
||||||
|
quality: ImageQuality = ImageQuality.MEDIUM,
|
||||||
|
maxWidth: number = 1280,
|
||||||
|
maxHeight: number = 1280
|
||||||
|
): CompressorOptions => {
|
||||||
|
return {
|
||||||
|
quality,
|
||||||
|
maxWidth,
|
||||||
|
maxHeight
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -6,6 +6,7 @@ import { addShop, updateShop, deleteShop, ShopDTO, UpdateShopCommand, AddShopCom
|
||||||
import { useWxStore } from "@/store/modules/wx";
|
import { useWxStore } from "@/store/modules/wx";
|
||||||
import Upload from "@iconify-icons/ep/upload";
|
import Upload from "@iconify-icons/ep/upload";
|
||||||
import { useSysConfigStoreHook } from "@/store/modules/sysConfig";
|
import { useSysConfigStoreHook } from "@/store/modules/sysConfig";
|
||||||
|
import { beforeImageUpload } from "@/utils/imageCompressor";
|
||||||
import ReQrcode from "@/components/ReQrcode";
|
import ReQrcode from "@/components/ReQrcode";
|
||||||
import { copyTextToClipboard } from "@pureadmin/utils";
|
import { copyTextToClipboard } from "@pureadmin/utils";
|
||||||
const { VITE_APP_BASE_API } = import.meta.env;
|
const { VITE_APP_BASE_API } = import.meta.env;
|
||||||
|
|
@ -105,16 +106,13 @@ const handleAvatarSuccess = (response) => {
|
||||||
formData.value.coverImg = response.data.url;
|
formData.value.coverImg = response.data.url;
|
||||||
};
|
};
|
||||||
|
|
||||||
const beforeAvatarUpload = (rawFile) => {
|
const beforeAvatarUpload = async (rawFile: File) => {
|
||||||
if (!['image/jpeg', 'image/png'].includes(rawFile.type)) {
|
const result = await beforeImageUpload(rawFile, {
|
||||||
ElMessage.error('封面图必须是 JPG/PNG 格式!');
|
quality: 0.7,
|
||||||
return false;
|
maxWidth: 980,
|
||||||
}
|
maxHeight: 980
|
||||||
if (rawFile.size > 50 * 1024 * 1024) {
|
});
|
||||||
ElMessage.error('封面图大小不能超过 50MB!');
|
return result;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentPaymentMethods = computed(() => {
|
const currentPaymentMethods = computed(() => {
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,11 @@ import { ElMessage, FormRules } from "element-plus";
|
||||||
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
||||||
import { GoodsDTO, updateGoodsApi, addGoodsApi, deleteGoodsApi } from "@/api/shop/goods";
|
import { GoodsDTO, updateGoodsApi, addGoodsApi, deleteGoodsApi } from "@/api/shop/goods";
|
||||||
import { CategoryDTO, getCategoryAllApi } from "@/api/shop/category";
|
import { CategoryDTO, getCategoryAllApi } from "@/api/shop/category";
|
||||||
|
import { beforeImageUpload } from "@/utils/imageCompressor";
|
||||||
import Confirm from "@iconify-icons/ep/check";
|
import Confirm from "@iconify-icons/ep/check";
|
||||||
import Upload from "@iconify-icons/ep/upload";
|
import Upload from "@iconify-icons/ep/upload";
|
||||||
import Delete from "@iconify-icons/ep/delete";
|
import Delete from "@iconify-icons/ep/delete";
|
||||||
|
import { useWxStore } from "@/store/modules/wx";
|
||||||
const { VITE_APP_BASE_API } = import.meta.env;
|
const { VITE_APP_BASE_API } = import.meta.env;
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|
@ -22,6 +24,7 @@ const props = defineProps({
|
||||||
|
|
||||||
const emit = defineEmits(["update:visible", "refresh"]);
|
const emit = defineEmits(["update:visible", "refresh"]);
|
||||||
|
|
||||||
|
const wxStore = useWxStore();
|
||||||
const formRef = ref();
|
const formRef = ref();
|
||||||
const isEdit = ref(false);
|
const isEdit = ref(false);
|
||||||
const formData = reactive<GoodsDTO>({
|
const formData = reactive<GoodsDTO>({
|
||||||
|
|
@ -34,7 +37,9 @@ const formData = reactive<GoodsDTO>({
|
||||||
categoryId: 0,
|
categoryId: 0,
|
||||||
goodsDetail: "",
|
goodsDetail: "",
|
||||||
coverImg: "",
|
coverImg: "",
|
||||||
usageInstruction: ""
|
usageInstruction: "",
|
||||||
|
belongType: 0,
|
||||||
|
corpid: wxStore.corpid
|
||||||
});
|
});
|
||||||
|
|
||||||
const rules = reactive<FormRules>({
|
const rules = reactive<FormRules>({
|
||||||
|
|
@ -106,16 +111,13 @@ const handleAvatarSuccess = (response, uploadFile) => {
|
||||||
formData.coverImg = response.data.url;
|
formData.coverImg = response.data.url;
|
||||||
};
|
};
|
||||||
|
|
||||||
const beforeAvatarUpload = (rawFile) => {
|
const beforeAvatarUpload = async (rawFile: File) => {
|
||||||
if (!['image/jpeg', 'image/png'].includes(rawFile.type)) {
|
const result = await beforeImageUpload(rawFile, {
|
||||||
ElMessage.error('封面图必须是 JPG/PNG 格式!')
|
quality: 0.7,
|
||||||
return false
|
maxWidth: 980,
|
||||||
}
|
maxHeight: 980
|
||||||
if (rawFile.size > 50 * 1024 * 1024) {
|
});
|
||||||
ElMessage.error('封面图大小不能超过 50MB!')
|
return result;
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { ElMessage, FormRules } from "element-plus";
|
||||||
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
||||||
import { addGoodsApi, GoodsDTO } from "@/api/shop/goods";
|
import { addGoodsApi, GoodsDTO } from "@/api/shop/goods";
|
||||||
import { CategoryDTO, getCategoryAllApi } from "@/api/shop/category";
|
import { CategoryDTO, getCategoryAllApi } from "@/api/shop/category";
|
||||||
|
import { beforeImageUpload } from "@/utils/imageCompressor";
|
||||||
import Confirm from "@iconify-icons/ep/check";
|
import Confirm from "@iconify-icons/ep/check";
|
||||||
import Upload from "@iconify-icons/ep/upload";
|
import Upload from "@iconify-icons/ep/upload";
|
||||||
import { useWxStore } from "@/store/modules/wx";
|
import { useWxStore } from "@/store/modules/wx";
|
||||||
|
|
@ -87,16 +88,13 @@ const handleAvatarSuccess = (response, uploadFile) => {
|
||||||
formData.coverImg = response.data.url;
|
formData.coverImg = response.data.url;
|
||||||
};
|
};
|
||||||
|
|
||||||
const beforeAvatarUpload = (rawFile) => {
|
const beforeAvatarUpload = async (rawFile: File) => {
|
||||||
if (!['image/jpeg', 'image/png'].includes(rawFile.type)) {
|
const result = await beforeImageUpload(rawFile, {
|
||||||
ElMessage.error('封面图必须是 JPG/PNG 格式!')
|
quality: 0.7,
|
||||||
return false
|
maxWidth: 980,
|
||||||
}
|
maxHeight: 980
|
||||||
if (rawFile.size > 50 * 1024 * 1024) {
|
});
|
||||||
ElMessage.error('封面图大小不能超过 50MB!')
|
return result;
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue