feat(图片处理): 添加图片压缩工具类并集成到上传组件
实现基于CompressorJS的图片压缩功能,添加imageCompressor工具类提供多种压缩方法 修改商品表单、店铺表单等上传组件使用新的压缩方法,统一压缩配置为质量0.7,最大尺寸980px 在package.json中添加zip打包脚本
This commit is contained in:
parent
8819f17aa0
commit
295dc7a8cf
|
|
@ -5,7 +5,7 @@
|
|||
"scripts": {
|
||||
"dev": "NODE_OPTIONS=--max-old-space-size=4096 vite",
|
||||
"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",
|
||||
"report": "rimraf dist && vite build",
|
||||
"preview": "vite preview",
|
||||
|
|
@ -21,7 +21,8 @@
|
|||
"lint:pretty": "pretty-quick --staged",
|
||||
"lint": "pnpm lint:eslint && pnpm lint:prettier && pnpm lint:stylelint",
|
||||
"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": [
|
||||
"> 1%",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { useResizeObserver } from "@vueuse/core";
|
|||
import { longpress } from "@/directives/longpress";
|
||||
import { useTippy, directive as tippy } from "vue-tippy";
|
||||
import { delay, debounce, isArray, downloadByBase64 } from "@pureadmin/utils";
|
||||
import { beforeImageUpload } from "@/utils/imageCompressor";
|
||||
import {
|
||||
ref,
|
||||
unref,
|
||||
|
|
@ -218,9 +219,19 @@ export default defineComponent({
|
|||
: 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();
|
||||
reader.readAsDataURL(file);
|
||||
reader.readAsDataURL(finalFile);
|
||||
inSrc.value = "";
|
||||
reader.onload = e => {
|
||||
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 Upload from "@iconify-icons/ep/upload";
|
||||
import { useSysConfigStoreHook } from "@/store/modules/sysConfig";
|
||||
import { beforeImageUpload } from "@/utils/imageCompressor";
|
||||
import ReQrcode from "@/components/ReQrcode";
|
||||
import { copyTextToClipboard } from "@pureadmin/utils";
|
||||
const { VITE_APP_BASE_API } = import.meta.env;
|
||||
|
|
@ -105,16 +106,13 @@ const handleAvatarSuccess = (response) => {
|
|||
formData.value.coverImg = response.data.url;
|
||||
};
|
||||
|
||||
const beforeAvatarUpload = (rawFile) => {
|
||||
if (!['image/jpeg', 'image/png'].includes(rawFile.type)) {
|
||||
ElMessage.error('封面图必须是 JPG/PNG 格式!');
|
||||
return false;
|
||||
}
|
||||
if (rawFile.size > 50 * 1024 * 1024) {
|
||||
ElMessage.error('封面图大小不能超过 50MB!');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
const beforeAvatarUpload = async (rawFile: File) => {
|
||||
const result = await beforeImageUpload(rawFile, {
|
||||
quality: 0.7,
|
||||
maxWidth: 980,
|
||||
maxHeight: 980
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
const currentPaymentMethods = computed(() => {
|
||||
|
|
|
|||
|
|
@ -4,9 +4,11 @@ import { ElMessage, FormRules } from "element-plus";
|
|||
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
||||
import { GoodsDTO, updateGoodsApi, addGoodsApi, deleteGoodsApi } from "@/api/shop/goods";
|
||||
import { CategoryDTO, getCategoryAllApi } from "@/api/shop/category";
|
||||
import { beforeImageUpload } from "@/utils/imageCompressor";
|
||||
import Confirm from "@iconify-icons/ep/check";
|
||||
import Upload from "@iconify-icons/ep/upload";
|
||||
import Delete from "@iconify-icons/ep/delete";
|
||||
import { useWxStore } from "@/store/modules/wx";
|
||||
const { VITE_APP_BASE_API } = import.meta.env;
|
||||
|
||||
const props = defineProps({
|
||||
|
|
@ -22,6 +24,7 @@ const props = defineProps({
|
|||
|
||||
const emit = defineEmits(["update:visible", "refresh"]);
|
||||
|
||||
const wxStore = useWxStore();
|
||||
const formRef = ref();
|
||||
const isEdit = ref(false);
|
||||
const formData = reactive<GoodsDTO>({
|
||||
|
|
@ -34,7 +37,9 @@ const formData = reactive<GoodsDTO>({
|
|||
categoryId: 0,
|
||||
goodsDetail: "",
|
||||
coverImg: "",
|
||||
usageInstruction: ""
|
||||
usageInstruction: "",
|
||||
belongType: 0,
|
||||
corpid: wxStore.corpid
|
||||
});
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
|
|
@ -106,16 +111,13 @@ const handleAvatarSuccess = (response, uploadFile) => {
|
|||
formData.coverImg = response.data.url;
|
||||
};
|
||||
|
||||
const beforeAvatarUpload = (rawFile) => {
|
||||
if (!['image/jpeg', 'image/png'].includes(rawFile.type)) {
|
||||
ElMessage.error('封面图必须是 JPG/PNG 格式!')
|
||||
return false
|
||||
}
|
||||
if (rawFile.size > 50 * 1024 * 1024) {
|
||||
ElMessage.error('封面图大小不能超过 50MB!')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
const beforeAvatarUpload = async (rawFile: File) => {
|
||||
const result = await beforeImageUpload(rawFile, {
|
||||
quality: 0.7,
|
||||
maxWidth: 980,
|
||||
maxHeight: 980
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
watch(
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { ElMessage, FormRules } from "element-plus";
|
|||
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
||||
import { addGoodsApi, GoodsDTO } from "@/api/shop/goods";
|
||||
import { CategoryDTO, getCategoryAllApi } from "@/api/shop/category";
|
||||
import { beforeImageUpload } from "@/utils/imageCompressor";
|
||||
import Confirm from "@iconify-icons/ep/check";
|
||||
import Upload from "@iconify-icons/ep/upload";
|
||||
import { useWxStore } from "@/store/modules/wx";
|
||||
|
|
@ -87,16 +88,13 @@ const handleAvatarSuccess = (response, uploadFile) => {
|
|||
formData.coverImg = response.data.url;
|
||||
};
|
||||
|
||||
const beforeAvatarUpload = (rawFile) => {
|
||||
if (!['image/jpeg', 'image/png'].includes(rawFile.type)) {
|
||||
ElMessage.error('封面图必须是 JPG/PNG 格式!')
|
||||
return false
|
||||
}
|
||||
if (rawFile.size > 50 * 1024 * 1024) {
|
||||
ElMessage.error('封面图大小不能超过 50MB!')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
const beforeAvatarUpload = async (rawFile: File) => {
|
||||
const result = await beforeImageUpload(rawFile, {
|
||||
quality: 0.7,
|
||||
maxWidth: 980,
|
||||
maxHeight: 980
|
||||
});
|
||||
return result;
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue