改写了element-plus,主要拓展了编辑图片功能,新增了超出大小限制提示,超出数量限制提示,超出数量限制后,隐藏上传按钮。
编辑图片没有设置背景,可根据实际需要自行设置背景。
支持查看(el-image-viewer)、图片编辑(tui-image-editor)、删除、下载操作,可通过props配置开启哪些功能,配置项:
可选配置项 | 默认值 | 说明 |
---|---|---|
imgList | type: Array, default: () => [] | 图片列表(默认为空,父组件通过.getPhotoFileList()方法获取列表) |
number | type: Number, default: Infinity | 上传数量(默认为不限制上传数量,超出数量隐藏上传按钮) |
limitSize | type: Number, default: Infinity | 限制大小,单位:MB(默认为不限制大小,超出大小提示用户) |
limitType | type: Array, default: [ ‘image/jpeg’, ‘image/png’,‘image/gif’,‘image/webp’,‘image/svg+xml’ ] | 限制类型(默认只支持标准格式图片) |
openBtn | type: Array, default: () => [‘view’,‘edit’,‘delete’,‘download’] | 开启哪些功能(默认全部开启) |
获取图片列表:.getPhotoFileList()
WuTongImage代码(实际使用的Image组件):
<template>
<div>
<el-upload :accept="[...new Set(props.limitType.flatMap(t => mimeToExtension[t] || []))].join(',')" ref="photoRef" :limit="number"
:auto-upload="false" list-type="picture-card" :on-exceed="handlePhotoExceed" :on-change="handleChange"
v-model:file-list="photoFileList" multiple :class="{ disabled: photoFileList.length >= number }">
<template #trigger>
<el-icon>
<Plus />
</el-icon>
</template>
<template #file="{ file }">
<div>
<img class="el-upload-list__item-thumbnail" :src="file.url" alt="" />
<span class="el-upload-list__item-actions">
<span class="el-upload-list__item-preview" @click="openImgViewer(file)" v-if="openBtn.includes('view')">
<el-icon :size="20"><zoom-in /></el-icon>
</span>
<span class="el-upload-list__item-edit" @click="handlePictureCardEdit(file)" v-if="openBtn.includes('edit')">
<el-icon :size="20">
<Edit />
</el-icon>
</span>
<span class="el-upload-list__item-delete" @click="handleDeletePhoto(file)" v-if="openBtn.includes('delete')">
<el-icon :size="20">
<Delete />
</el-icon>
</span>
<span class="el-upload-list__item-download" @click="handleDownloadPhoto(file)" v-if="openBtn.includes('download')">
<el-icon :size="20">
<Download />
</el-icon>
</span>
</span>
</div>
</template>
</el-upload>
<el-image-viewer v-if="showImgViewer" @close="closeImgViewer" :url-list="imgPreviewList" :zoomable="true"
:hide-on-click-modal="true" :close-on-press-escape="true" />
<my-image-edit ref="myImageEditRef" @closeEditor="handleCloseEditor" />
</div>
</template>
<script setup>
import { nextTick, ref } from "vue";
import { ElMessage, genFileId } from "element-plus";
import MyImageEdit from "@/components/WuTongImageEdit.vue";
// 添加映射关系
const mimeToExtension = {
'image/jpeg': ['.jpg', '.jpeg'],
'image/png': ['.png'],
'image/gif': ['.gif'],
'image/webp': ['.webp'],
'image/svg+xml': ['.svg'],
'image/tiff': ['.tif', '.tiff'],
'image/x-portable-pixmap': ['.ppm'],
'image/x-portable-graymap': ['.pgm'],
'image/x-portable-bitmap': ['.pbm'],
'image/x-photoshop': ['.psd'],
'image/vnd.microsoft.icon': ['.ico'],
'image/heif': ['.heif', '.heic'],
'image/avif': ['.avif'],
'image/apng': ['.apng'],
'image/dicom-rle': ['.dcm'],
'image/x-ms-bmp': ['.bmp'],
'image/pjpeg': ['.jpg'], // 非标准扩展名
'image/x-pcx': ['.pcx'],
'image/x-cmu-raster': ['.ras']
};
const props = defineProps({
imgList: { type: Array, default: () => [] }, // 图片列表(默认为空,父组件通过.getPhotoFileList()方法获取列表)
number: { type: Number, default: Infinity }, // 上传数量(默认为不限制上传数量)
limitSize: { type: Number, default: Infinity }, // 限制大小,单位:MB(默认为不限制大小)
limitType: {
type: Array,
default: [
// 标准格式
'image/jpeg', // JPEG(.jpg/.jpeg) - 照片标准格式
'image/png', // PNG(.png) - 透明背景首选
'image/gif', // GIF(.gif) - 简单动图
'image/webp', // WebP(.webp) - 现代高压缩格式
'image/svg+xml', // SVG(.svg) - 矢量图形
// // 专业格式
// 'image/tiff', // TIFF(.tif/.tiff) - 印刷行业标准
// 'image/x-portable-pixmap', // PPM(.ppm)
// 'image/x-portable-graymap', // PGM(.pgm)
// 'image/x-portable-bitmap', // PBM(.pbm)
// 'image/x-photoshop', // PSD(.psd) - Photoshop专用
// // 特殊格式
// 'image/vnd.microsoft.icon', // ICO(.ico) - 浏览器图标
// 'image/heif', // HEIF(.heif/.heic) - iOS高清格式
// 'image/avif', // AVIF(.avif) - 新一代压缩格式
// 'image/apng', // APNG(.apng) - 动态PNG
// // 医学/科研
// 'image/dicom-rle', // DICOM(.dcm) - 医学影像
// 'image/x-ms-bmp', // BMP(.bmp) - 位图格式
// // 过时格式
// 'image/pjpeg', // 已废弃的JPEG类型
// 'image/x-pcx', // PCX(.pcx) - 早期绘图软件格式
// 'image/x-cmu-raster' // RAS(.ras) - SunOS系统格式
],
}, // 限制类型(默认只支持标准格式图片)
openBtn: { type: Array, default: () => ['view','edit','delete','download'] }, // 开启哪些功能(默认全部开启)
});
const showImgViewer = ref(false);
const imgPreviewList = ref([]);
// 打开图片查看器
const openImgViewer = (img) => {
showImgViewer.value = true;
if (typeof img === "string") {
imgPreviewList.value = [img];
} else {
imgPreviewList.value = [img.url];
}
};
// 关闭图片查看器
const closeImgViewer = () => {
showImgViewer.value = false;
imgPreviewList.value = [];
};
// 修改图片函数
const handlePictureCardEdit = (file) => {
myImageEditRef.value.handleOpen(file.url || file);
};
// 图片上传组件实例对象
const photoRef = ref(null);
// 文件列表
const photoFileList = ref([]);
// 编辑图片组件实例对象
const myImageEditRef = ref(null);
// 接收图片函数
const handleCloseEditor = (file) => {
photoFileList.value = [convertAndAddBlob(file)];
funEditAssignment(file);
};
// 编辑图片后的赋值函数
const funEditAssignment = (file) => {
photoFileList.value = [];
photoFileList.value = [convertAndAddBlob(file)];
};
// 将Blob转换为el-upload可用的格式并添加到fileList
const convertAndAddBlob = (blob) => {
let now = Date.now();
if (blob instanceof Blob) {
let file = {
name: now + ".png",
url: URL.createObjectURL(blob),
status: "success",
uid: now,
size: blob.size,
type: now,
};
return file;
} else {
return blob;
}
};
// 文件改变函数
const handleChange = (uploadFile, uploadFiles) => {
if (checkImage(uploadFile) === true) {
photoFileList.value = [uploadFile.raw || uploadFile.url || uploadFile];
} else {
nextTick(() => {
handleDeletePhoto(uploadFile);
});
}
showValidationErrors(uploadFiles);
};
// 检查图片是否符合要求(返回错误类型)
const checkImage = (file) => {
// 安全获取文件类型
const fileType = file.raw?.type || file.type || '';
// 获取真实扩展名
const extension = (file.name?.split('.').pop() || '').toLowerCase();
// 增强类型检查:同时验证MIME类型和扩展名
const isValidType = props.limitType.some(t => {
const expectedExtensions = mimeToExtension[t] || [];
return fileType === t || expectedExtensions.includes(`.${extension}`);
});
if (!isValidType) {
return "type_error";
}
// 增强大小检查:添加最小尺寸限制(可选)
const maxSize = 1024 * 1024 * props.limitSize;
if (file.size > maxSize || file.size === 0) {
return "size_error";
}
return true;
};
// 新增错误处理函数
const showValidationErrors = (files) => {
const errors = new Set();
files.forEach((file) => {
const result = checkImage(file);
if (result === "type_error") errors.add("type");
if (result === "size_error") errors.add("size");
});
if (errors.has("type")) {
ElMessage.error(`图片类型只支持 ${props.limitType.join("、")} 格式`);
}
if (errors.has("size")) {
ElMessage.error(`图片大小不能超过 ${props.limitSize}MB`);
}
};
// 图片超出清空列表
const handlePhotoExceed = (files, fileList) => {
// 计算还能上传的数量
const remaining = props.number - fileList.length;
// 截取有效数量的文件
const validFiles = files.slice(0, remaining);
// 生成带唯一ID的文件对象
validFiles.forEach((file) => {
file.uid = genFileId();
file.status = "success";
});
photoFileList.value = [
...fileList,
...validFiles.map((file) => ({
...file,
name: file.name,
url: URL.createObjectURL(file),
})),
].slice(0, props.number); // 确保总长度不超过限制
ElMessage.error(
`最多只能上传${props.number}个文件,已自动截取前${remaining}个有效文件`
);
};
// 删除图片函数
const handleDeletePhoto = (file) => {
photoFileList.value = photoFileList.value.filter(
(item) => item.uid !== file.uid
);
};
const handleDownloadPhoto = (file) => {
const a = document.createElement("a");
a.href = file?.url;
a.download = file.name;
a.style.display = 'none';
document.body.appendChild(a);
a.click();
// 清理DOM和内存
document.body.removeChild(a);
}
// 父元素获取图片列表
const getPhotoFileList = () => {
return photoFileList.value;
}
defineExpose({
getPhotoFileList
});
</script>
<style lang="scss" scoped>
:deep(.disabled .el-upload--picture-card) {
display: none !important;
}
:deep(.el-upload--picture-card) {
background-color: transparent !important;
border: 1px dashed #fff;
}
:deep(.el-upload-list--picture-card .el-upload-list__item) {
background-color: transparent !important;
}
:deep(.el-upload-list__item) {
display: flex;
justify-content: center;
}
:deep(.el-image-viewer__actions){
background-color: transparent;
}
:deep(.el-image-viewer__close){
background-color: transparent;
}
</style>
WuTongImageEdit编辑图片代码,实际使用过程中,可以抽离 zh_CN 编辑图片汉化配置与 customTheme 编辑图片主题颜色配置到单独文件进行配置
<!-- 图片编辑组件,传入方法:handleOpen(参数1:要编辑的文件) -->
<template>
<el-dialog v-model="isTuiEditorVisible" title="编辑图片" draggable
custom-class="custom-dialog" width="1000px" style="margin-top: 20px;background-color: transparent;">
<div class="tui-editor-container"></div>
<div class="dialog-buttons">
<el-button type="primary" @click="saveEditedImage">保存图片</el-button>
<el-button @click="closeTuiEditor">取消</el-button>
</div>
</el-dialog>
</template>
<script setup>
import ImageEditor from "tui-image-editor";
import "tui-image-editor/dist/tui-image-editor.css";
const { proxy } = getCurrentInstance();
// 是否显示编辑图片组件
const isTuiEditorVisible = ref(false);
// 当前需要编辑的图片
const currentEditingImage = ref(null);
// 编辑图片实例
const imageEditorInstances = ref(null);
// 编辑图片汉化配置
const zh_CN = {
// 通用
Load: "加载",
Download: "下载",
Undo: "撤销",
Redo: "重做",
Delete: "删除",
DeleteAll: "清空", // 新增
Reset: "重置",
ZoomIn: "放大",
ZoomOut: "缩小",
Hand: "移动",
Range: "范围",
History: "历史", // 新增
// 应用和取消按钮
Apply: "应用", // 新增
Cancel: "取消", // 新增
// 裁剪菜单
Crop: "裁剪",
Custom: "自定义",
Square: "正方形",
"3:2": "3:2",
"4:3": "4:3",
"5:4": "5:4",
"7:5": "7:5",
"16:9": "16:9",
// 翻转菜单
Flip: "翻转",
FlipX: "水平翻转",
FlipY: "垂直翻转",
// 旋转菜单
Rotate: "旋转",
RotateClockwise: "顺时针旋转",
RotateCounterClockwise: "逆时针旋转",
// 绘画菜单
Draw: "绘画",
Free: "自由绘画",
Straight: "直线",
Color: "颜色",
// 形状菜单
Shape: "形状",
Rectangle: "矩形",
Circle: "圆形",
Triangle: "三角形",
Fill: "填充",
Stroke: "边框",
// 图标菜单
Icon: "图标",
Arrow: "箭头",
"Arrow-2": "箭头-2",
"Arrow-3": "箭头-3",
"Star-1": "星形-1",
"Star-2": "星形-2",
Polygon: "多边形",
Location: "位置",
Heart: "心形",
Bubble: "气泡",
"Custom icon": "自定义图标",
// 文字菜单
Text: "文字",
Bold: "加粗",
Italic: "斜体",
Underline: "下划线",
Left: "左对齐",
Center: "居中对齐",
Right: "右对齐",
"Text size": "文字大小",
// 滤镜菜单
Filter: "滤镜",
Grayscale: "灰度",
Invert: "反相",
Sepia: "怀旧",
Sepia2: "怀旧2",
Blur: "模糊",
Sharpen: "锐化",
Emboss: "浮雕",
"Remove White": "移除白色",
Distance: "距离",
Brightness: "亮度",
Noise: "噪点",
Pixelate: "像素化",
"Color Filter": "颜色滤镜",
Threshold: "阈值",
Tint: "色调",
Multiply: "正片叠底",
Blend: "混合",
};
// 编辑图片主题颜色配置
const customTheme = {
// 左上角图片
"common.bi.image": "", // logo图片
"common.bisize.width": "0px",
"common.bisize.height": "0px",
"common.backgroundImage": "none",
"common.border": "1px solid #444",
"common.backgroundColor": "rgba(0, 0, 0, 0)", // 整体背景颜色
// header(头部)
"header.backgroundImage": "none",
"header.border": "0px",
"header.backgroundColor": "rgba(0, 0, 0, 0)", // 头部的背景颜色
// load button(上传按钮)
"loadButton.backgroundColor": "#fff",
"loadButton.border": "1px solid #ddd",
"loadButton.color": "#222",
"loadButton.fontFamily": "NotoSans, sans-serif",
"loadButton.fontSize": "12px",
"loadButton.display": "none", // 可以直接隐藏掉
// download button(下载按钮)
"downloadButton.backgroundColor": "#fdba3b",
"downloadButton.border": "1px solid #fdba3b",
"downloadButton.color": "#fff",
"downloadButton.fontFamily": "NotoSans, sans-serif",
"downloadButton.fontSize": "12px",
"downloadButton.display": "none", // 可以直接隐藏掉
// 菜单-普通状态 - 绿色
"menu.normalIcon.color": "#2d8cf0",
// 菜单-选中状态 - 蓝色
"menu.activeIcon.color": "blue",
// 菜单-禁用状态 - 灰色
"menu.disabledIcon.color": "grey",
// 菜单-鼠标悬浮状态 - 黄色
"menu.hoverIcon.color": "yellow",
"menu.iconSize.width": "24px",
"menu.iconSize.height": "24px",
"submenu.iconSize.width": "32px",
"submenu.iconSize.height": "32px",
// submenu primary color
"submenu.backgroundColor": "#1e1e1e",
"submenu.partition.color": "#858585",
// submenu labels
"submenu.normalLabel.color": "#858585",
"submenu.normalLabel.fontWeight": "lighter",
"submenu.activeLabel.color": "#fff",
"submenu.activeLabel.fontWeight": "lighter",
// checkbox style
"checkbox.border": "1px solid #ccc",
"checkbox.backgroundColor": "#fff",
// rango style
"range.pointer.color": "#fff",
"range.bar.color": "#666",
"range.subbar.color": "#d1d1d1",
"range.disabledPointer.color": "#414141",
"range.disabledBar.color": "#282828",
"range.disabledSubbar.color": "#414141",
"range.value.color": "#fff",
"range.value.fontWeight": "lighter",
"range.value.fontSize": "11px",
"range.value.border": "1px solid #353535",
"range.value.backgroundColor": "#151515",
"range.title.color": "#fff",
"range.title.fontWeight": "lighter",
// colorpicker style
"colorpicker.button.border": "1px solid #1e1e1e",
"colorpicker.title.color": "#fff",
};
// 打开图片编辑器函数
const handleOpen = (imageUrl) => {
// 记录当前正在编辑的字段名和图片
currentEditingImage.value = imageUrl; // 当前图片的 URL
isTuiEditorVisible.value = true; // 显示图片编辑器的弹出框
nextTick(() => {
const container = document.querySelector(".tui-editor-container"); // 获取编辑器的容器元素
if (!container) {
console.error("未找到编辑器容器 .tui-editor-container"); // 如果未找到容器,打印错误信息
return;
}
container.innerHTML = ""; // 清空容器内的旧内容
// 如果存在旧的编辑器实例,则销毁
if (imageEditorInstances.value) {
imageEditorInstances.value.destroy(); // 销毁旧的图片编辑器实例
}
// 初始化新的图片编辑器实例
const instance = new ImageEditor(container, {
includeUI: {
loadImage: { path: imageUrl, name: "编辑图片" }, // 加载图片时的路径和图片名称
menu: ["crop", "flip", "rotate", "draw", "shape", "icon", "text", "filter"], // 启用的工具菜单项
// menu: ["crop", "flip", "rotate", "text"], // 启用的工具菜单项
initMenu: "", // 默认打开的工具菜单项
uiSize: { width: "970px", height: "750px" }, // 设置用户界面的宽度和高度
menuBarPosition: "bottom", // 设置菜单栏位置为底部
locale: zh_CN, // 传入自定义的中文配置
theme: customTheme,
},
cssMaxWidth: 900, // 设置图片的最大宽度(CSS限制)
cssMaxHeight: 600, // 设置图片的最大高度(CSS限制)
usageStatistics: false, // 禁用使用统计信息
});
imageEditorInstances.value = instance; // 保存当前字段对应的图片编辑器实例
// 手动设置背景透明
document.querySelector('.tui-image-editor-container').style.backgroundColor = 'transparent';
document.querySelector('.tui-image-editor-controls').style.backgroundColor = 'transparent';
document.querySelector('.tui-image-editor-container .tui-image-editor-help-menu').style.backgroundColor = 'transparent';
// 设置某些功能关闭
document.querySelector('[tooltip-content="删除"]').style.display = 'none' // 删除选中编辑内容
document.querySelector('[tooltip-content="清空"]').style.display = 'none' // 清空
// 隐藏分割线
document.querySelectorAll('.tui-image-editor-icpartition').forEach(item => {
item.style.display = 'none'
})
});
}
// 保存图片函数
const saveEditedImage = () => {
let editedImageUrl = imageEditorInstances.value.toDataURL("image/jpeg",0.8);
const byteString = atob(editedImageUrl.split(",")[1]);
const mimeString = editedImageUrl.split(",")[0].split(":")[1].split(";")[0];
const ab = new ArrayBuffer(byteString.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
const blob = new Blob([ab], { type: mimeString });
emits("closeEditor",blob);
proxy.$message.success("图片保存成功");
// 关闭弹框
isTuiEditorVisible.value = false;
}
// 关闭函数,不保存图片
const closeTuiEditor = () => {
// 关闭弹框
isTuiEditorVisible.value = false;
}
// 通知父组件 关闭编辑器(不保存)closeEditor:第一个参数为图片信息
const emits = defineEmits(["closeEditor"]);
// 父组件调用关闭
const handleClose = () => {
// 如果存在编辑器实例,则销毁
if (imageEditorInstances.value) {
imageEditorInstances.value.destroy(); // 销毁旧的图片编辑器实例
}
// 关闭弹框
isTuiEditorVisible.value = false;
}
// 方法暴露
defineExpose({
handleOpen,
handleClose,
});
</script>
<style lang="scss" scoped >
.dialog-buttons {
display: flex;
justify-content: flex-end;
margin-top: 20px;
gap: 10px;
}
</style>