整合element-plus上传组件和tui-image-editor

改写了element-plus,主要拓展了编辑图片功能,新增了超出大小限制提示,超出数量限制提示,超出数量限制后,隐藏上传按钮。
在这里插入图片描述
编辑图片
编辑图片没有设置背景,可根据实际需要自行设置背景。

支持查看(el-image-viewer)、图片编辑(tui-image-editor)、删除、下载操作,可通过props配置开启哪些功能,配置项:

可选配置项默认值说明
imgListtype: Array, default: () => []图片列表(默认为空,父组件通过.getPhotoFileList()方法获取列表)
numbertype: Number, default: Infinity上传数量(默认为不限制上传数量,超出数量隐藏上传按钮)
limitSizetype: Number, default: Infinity限制大小,单位:MB(默认为不限制大小,超出大小提示用户)
limitTypetype: Array, default: [ ‘image/jpeg’, ‘image/png’,‘image/gif’,‘image/webp’,‘image/svg+xml’ ]限制类型(默认只支持标准格式图片)
openBtntype: 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>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值