feat(图片处理): 添加图片压缩工具类并集成到上传组件

实现基于CompressorJS的图片压缩功能,添加imageCompressor工具类提供多种压缩方法
修改商品表单、店铺表单等上传组件使用新的压缩方法,统一压缩配置为质量0.7,最大尺寸980px
在package.json中添加zip打包脚本
This commit is contained in:
dzq 2025-11-08 17:16:09 +08:00
parent 8819f17aa0
commit 295dc7a8cf
6 changed files with 280 additions and 35 deletions

View File

@ -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%",

View File

@ -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;

View File

@ -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
};
};

View File

@ -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(() => {

View File

@ -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(

View File

@ -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>