Element Plus图片上传组件二次扩展
Element Plus 的图片上传组件主要通过 <el-upload> 实现,该组件支持多种配置和功能,如文件类型限制、文件大小限制、自动上传、手动上传、预览、删除等。以下是对 Element Plus 图片上传组件的详细介绍和使用示例:
功能概述
- 文件类型限制:可以指定允许上传的文件类型,如图片(jpg, png等)。
- 文件大小限制:可以限制上传文件的大小。
- 自动上传:选择文件后自动上传到服务器。
- 手动上传:选择文件后不立即上传,而是通过按钮手动触发上传。
- 预览:上传前或上传后可以预览图片。
- 删除:已上传的文件可以删除。
使用示例
以下是一个基本的 Element Plus 图片上传组件的使用示例,包括前端 Vue 组件代码和后端 Spring Boot 接口代码。
前端 Vue 组件基本调用
<template>
<el-upload
class="upload-demo"
action="http://localhost:8080/upload/image" <!-- 后端接收上传文件的接口 -->
:on-preview="handlePreview"
:on-remove="handleRemove"
:file-list="fileList"
:auto-upload="false" <!-- 设置为false,则不自动上传 -->
:http-request="customUpload"
list-type="picture-card"
>
<i class="el-icon-plus"></i>
</el-upload>
<el-button slot="trigger" size="small" type="primary">选取文件</el-button>
<el-button style="margin-left: 10px;" size="small" type="success" @click="submitUpload">上传到服务器</el-button>
<el-dialog
title="预览"
:visible.sync="dialogVisible"
width="30%"
>
<img :src="dialogImageUrl" class="dialog-image" />
</el-dialog>
</template>
<script>
import { ref } from 'vue';
export default {
setup() {
const fileList = ref([]);
const dialogImageUrl = ref('');
const dialogVisible = ref(false);
const handlePreview = (file) => {
dialogImageUrl.value = file.url;
dialogVisible.value = true;
};
const handleRemove = (file, fileList) => {
console.log(file, fileList);
};
const customUpload = (options) => {
// 这里可以自定义上传逻辑,比如使用axios发送请求
// 这里只是简单模拟
console.log('自定义上传', options.file);
// 假设上传成功,更新fileList
// fileList.value.push({ name: options.file.name, url: '上传后的文件URL' });
};
const submitUpload = () => {
// 这里可以调用自定义的上传函数,或者将fileList中的文件逐个上传
// 这里只是示例,没有实现具体的上传逻辑
console.log('提交上传', fileList.value);
};
return {
fileList,
dialogImageUrl,
dialogVisible,
handlePreview,
handleRemove,
customUpload,
submitUpload,
};
},
};
</script>
<style>
.dialog-image {
width: 100%;
}
</style>
DIY可视化二次扩展
输入上传式图片组件
<template>
<el-input
type="text"
ref="formImg"
:class="url && type == 'image' ? 'diygw-img-input' : ''"
v-model="url"
clearable
:placeholder="'请选择' + title"
>
<template #append>
<el-image
:src="url"
v-if="url && type == 'image'"
style="width: 30px; height: 30px"
:preview-teleported="true"
:preview-src-list="[url]"
></el-image>
<el-button @click="handleStorage"> 选择{{ title }} </el-button>
</template>
</el-input>
<diy-storage ref="storage" :type="type" :accept="accept" :limit="1" @confirm="getAttachmentFileList"></diy-storage>
</template>
<script lang="ts" setup>
import { nextTick, ref, defineEmits, watch } from 'vue';
import { useVModel } from '@vueuse/core';
import DiyStorage from './storage.vue';
const props = defineProps({
// 外部v-model值
modelValue: {
type: String,
default: '',
},
title: {
type: String,
default: '',
},
type: {
type: String,
default: 'image',
},
accept: {
type: String,
default: 'image/*',
},
});
const storage = ref(null);
const emit = defineEmits(['update:modelValue', 'change', 'blur']);
const url = useVModel(props, 'modelValue', emit);
const handleStorage = () => {
nextTick(() => {
storage.value!.handleStorageDlg('', '上传' + props.title);
});
};
watch(url, () => {
emit('change');
emit('blur');
});
const formImg: EmptyObjectType = ref(null);
// 获取商品相册资源
const getAttachmentFileList = (files = <any>[]) => {
if (!files.length) {
return;
}
url.value = files[0].url;
nextTick(() => {
formImg.value?.input.focus();
formImg.value?.input.blur();
});
};
</script>
<style lang="scss" scoped>
.diygw-img-input ::v-deep(.el-input-group__append) {
padding-left: 0px;
}
.diygw-img-input {
::v-deep(.el-image) {
margin-right: 15px;
}
}
.sortable-ghost {
opacity: 0;
}
</style>
多图式上传
<template>
<div style="line-height: 0; width: 100%" class="flex justify-start">
<VueDraggable
tag="ul"
v-bind="{ animation: 200, disabled: false, ghostClass: 'ghost' }"
v-model="imageList"
class="el-upload-list el-upload-list--picture-card"
group="subform"
item-key="url"
>
<li v-for="(item, index) in imageList" class="el-upload-list__item" :style="`width:${width}px; height:${height}px;`">
<el-image :style="`width:100%; height:100%`" :src="item" fit="contain" />
<span v-show="!state.isdrag" :class="{ 'el-upload-list__item-actions': true, 'diy-cm': isMove }">
<SvgIcon style="color: #fff" name="ele-Plus" class="margin-right" @click="handleStorage(index)" :size="20" />
<SvgIcon style="color: #fff" name="ele-Delete" @click="remove(index)" :size="20" />
</span>
</li>
</VueDraggable>
<div
v-if="limit == 0 || (imageList.length == 0 && limit == 1)"
tabindex="0"
style="margin-bottom: 8px"
class="el-upload el-upload--picture-card"
:style="`width:${width}px; height:${height}px;`"
@click="handleStorage(-1)"
>
<SvgIcon name="ele-Plus" :size="20" />
</div>
<diy-storage ref="storage" :limit="limit" @confirm="getAttachmentFileList"></diy-storage>
</div>
</template>
<script setup>
import { nextTick, ref, reactive } from 'vue';
import { useVModel } from '@vueuse/core';
import { VueDraggable } from 'vue-draggable-plus';
import { ElMessageBox, useFormItem } from 'element-plus';
import DiyStorage from './storage.vue';
const { formItem } = useFormItem();
const props = defineProps({
// 外部v-model值
modelValue: {
type: Array,
default: () => [],
},
width: {
default: 80,
},
height: {
default: 80,
},
isMove: {
default: true,
},
limit: {
default: 0,
},
customer: {
type: Object,
required: false,
default: () => {},
},
contractId: {
required: false,
default: '',
},
storageType: {
type: String,
required: false,
default: '',
},
});
const state = reactive({
isdrag: false,
index: -1,
});
const storage = ref();
const emit = defineEmits(['update:modelValue']);
const imageList = useVModel(props, 'modelValue', emit);
const handleStorage = (index = -1) => {
state.index = index;
nextTick(() => {
storage.value.handleStorageDlg('', '上传图片');
});
};
// 获取商品相册资源
const getAttachmentFileList = (files = []) => {
if (!files.length) {
return;
}
if (state.index != -1) {
imageList.value[state.index] = files[state.index].url;
} else {
files.forEach((item) => {
imageList.value.push(item.url);
});
}
nextTick(() => {
formItem?.validate?.('blur').catch((err) => {
console.log(err);
});
formItem?.validate?.('change').catch((err) => {
console.log(err);
});
});
};
const remove = (index = -1) => {
ElMessageBox({
message: '是否删除该文件?',
title: '警告',
showCancelButton: true,
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(function () {
imageList.value.splice(index, 1);
});
};
</script>
<style lang="scss" scoped>
.sortable-ghost {
opacity: 0;
}
</style>
上传组件调用
<template>
<el-dialog :title="title" top="5vh" v-model="visible" append-to-body center width="800px">
<div class="flex storages">
<div class="categorys diy-tree file-border flex flex-direction-column">
<el-button type="primary" @click="handleAdd()">新增目录</el-button>
<div class="mt-1 text-center" plain :class="[!form.parentId ? 'selected' : '']" @click="selectCategory('')">全部</div>
<div
class="mt-1 p-1 text-center"
plain
:key="item"
:class="[form.parentId == item.storageId ? 'selected' : '']"
@click="selectCategory(item.storageId)"
v-for="item in categorys"
>
{{ item.name }}
</div>
</div>
<div class="flex1 file-border">
<!-- 文件列表开始 -->
<div class="flex justify-between">
<div class="diy-mb-20">
<el-input
v-model="form.name"
label-width="0px"
placeholder="输入名称搜索"
style="width: 250px"
@keyup.enter.native="handleSearch()"
@clear="handleSearch()"
:clearable="true"
>
<template #suffix>
<SvgIcon @click="handleSearch" name="ele-Search" :size="20" />
</template>
</el-input>
</div>
<diy-upload
ref="upload"
:upload-tip="uploadConfig.uploadTip"
:multiple="uploadConfig.multiple"
:type="type"
:accept="accept"
:limit="uploadConfig.limit"
:parent-id="form.parentId"
@confirm="_getUploadFileList"
></diy-upload>
</div>
<div class="files" :max="limit !== 0 ? limit : 100">
<div class="file-list" v-loading="loading">
<div :key="index" v-for="(item, index) in currentTableData" class="item" :data-label="item.name" :class="item.selectclz">
<div
class="file-image"
:style="{
backgroundImage: 'url(' + getImageThumb(item) + ')',
}"
@click="selectFile(item)"
></div>
<div class="file-name">
{{ item.name }}
</div>
<div class="mask" @click="selectFile(item)">
<SvgIcon name="ele-Check" :size="20" />
</div>
<div class="el-dropdown" @click="handleDelete(item.storageId)">
<SvgIcon class="delete" name="ele-Delete" :size="20" />
</div>
</div>
</div>
</div>
<!-- 翻页开始 -->
<diy-pagefooter
style="margin: 0; padding: 20px 0 0 0"
:current="page.current"
:size="page.size"
:total="page.total"
:is-size="false"
@change="handlePaginationChange"
/>
</div>
</div>
<!-- 确认,取消 -->
<template #footer v-if="isSelect == '1'" class="dialog-footer">
<div style="float: left; font-size: 13px">
<span v-if="checkList.length > limit && limit !== 0" style="color: #f56c6c">
当前已选 {{ checkList.length }} 个,最多允许选择 {{ limit }} 个文件
</span>
<span v-else>当前已选 {{ checkList.length }} 个文件</span>
</div>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :loading="loadingCollection" :disabled="checkList.length > limit && limit !== 0" @click="handleConfirm"
>确定</el-button
>
</template>
</el-dialog>
<el-dialog :title="nameMap[nameStatus]" v-model="nameFormVisible" append-to-body center top="5vh" width="600px">
<el-form :model="nameForm" :rules="rules" ref="refform" label-width="50px" label-position="left" @submit.native.prevent>
<el-form-item label="名称" prop="name">
<el-input v-model="nameForm.name" placeholder="请输入目录名称" @keyup.enter.native="nameStatus === 'add' ? add() : rename()" ref="input" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="nameFormVisible = false">取消</el-button>
<el-button v-if="nameStatus === 'add'" type="primary" :loading="dialogLoading" @click="add">确定</el-button>
<el-button v-else type="primary" :loading="dialogLoading" @click="rename">修改</el-button>
</template>
</el-dialog>
</template>
<script lang="ts">
import { defineComponent, reactive, ref, nextTick, toRefs } from 'vue';
import { listData, listAllData, addData, delData } from '@/api/index';
import { ElMessageBox, ElMessage } from 'element-plus';
import DiyUpload from './upload.vue';
import DiyPagefooter from './pagefooter.vue';
export default defineComponent({
name: 'DiyStorage',
components: {
DiyUpload,
DiyPagefooter,
},
props: {
type: {
default: 'image',
},
accept: {
default: 'image/*',
},
// 最大选择数(0表示不限制)
limit: {
type: Number,
required: false,
default: 0,
},
isSelect: {
default: '1',
},
},
setup(props, { emit }) {
const data = reactive({
baseUrl: '',
title: '',
isLoad: false,
icontype: 'default',
visible: false,
loading: false,
uploadConfig: {
uploadTip: '请选择文件进行上传,',
multiple: true,
accept: 'image/*',
limit: 0,
type: 'image',
replace: false,
},
storageType: props.type,
source: '',
loadingCollection: false,
checkList: [],
categorys: [],
syscategorys: [],
currentTableData: [],
currentSysTableData: [],
searchSysTableData: [],
dialogLoading: false,
nameForm: {
type: '',
name: undefined,
parentId: undefined,
},
nameFormVisible: false,
nameStatus: 'edit',
nameMap: {
edit: '重命名',
add: '新增目录',
},
iconfonturl: '',
rules: {
name: [
{
required: true,
message: '目录名称不能为空',
trigger: 'blur',
},
{
max: 255,
message: '长度不能大于 255 个字符',
trigger: 'blur',
},
],
},
form: {
name: undefined,
parentId: undefined,
order_type: 'desc',
order_field: 'storageId',
},
sysform: {
parentId: 'icon1',
name: '',
type: 'system',
},
page: {
current: 1,
size: 15,
total: 0,
},
});
const upload = ref(null);
const getImageThumb = (item: any) => {
if (item.type == 'mp3') {
return '/static/images/mp3.png';
} else if (item.type == 'mp4') {
return '/static/images/mp3.png';
} else if (item.type == 'image' || item.type == 'scene') {
return item.url;
} else {
return '/static/images/file.png';
}
};
const handleStorageDlg = (source = '', title = '') => {
data.title = title ? title : '文件管理';
// if (type == 'mp3') {
// data.uploadConfig.accept = '.mp3';
// } else if (type == 'mp4') {
// data.uploadConfig.accept = '.mp4';
// } else {
// data.uploadConfig.accept = 'image/*';
// }
data.visible = true;
data.storageType = props.type;
data.source = source;
data.checkList = [];
data.currentTableData.forEach((item: any) => {
item.selectclz = '';
});
if (!data.isLoad) {
data.loadingCollection = false;
data.currentTableData = [];
data.uploadConfig.type = props.type;
handleSubmit();
nextTick(() => {
getStorageDirectory();
if (upload.value) {
upload.value?.handleClose();
}
});
}
};
const handlePaginationChange = (val: any) => {
data.page = val;
nextTick(() => {
handleSubmit();
});
};
const handleSearch = () => {
data.page.current = 1;
handleSubmit();
};
const handleSubmit = () => {
data.loading = true;
listData('/sys/storage', {
...data.form,
type: data.storageType,
pageNum: data.page.current,
pageSize: data.page.size,
})
.then((res) => {
data.currentTableData = res.rows || [];
data.page.total = res.total;
data.isLoad = true;
})
.finally(() => {
data.loading = false;
});
};
const handleSysSubmit = () => {
data.loading = true;
listData('/sys/storage', {
...data.sysform,
})
.then((res) => {
data.currentSysTableData = res.rows || [];
data.searchSysTableData = res.rows || [];
})
.finally(() => {
data.loading = false;
});
};
const handleSyscatesSubmit = () => {
data.loading = true;
listData('/sys/storage', {
...data.sysform,
})
.then((res) => {
data.syscategorys = res.rows || [];
data.sysform.type = 'system';
data.sysform.parentId = res.rows[0].storageId;
handleSysSubmit();
})
.finally(() => {
data.loading = false;
});
};
const handleSysSearch = () => {
let findData = data.currentSysTableData.filter((item: any) => {
return item.name.indexOf(data.sysform.name) >= 0 || item.cname.indexOf(data.sysform.name) >= 0;
});
data.searchSysTableData = findData;
};
const handleAllSearch = () => {
if (data.loading) {
ElMessage.error('点击过快,正在加载数据');
return;
}
data.sysform.parentId = '';
handleSysSubmit();
};
const handleConfirm = () => {
if (data.checkList.length <= 0) {
emit('confirm', [], data.source);
data.visible = false;
return;
}
let checkList = data.checkList;
// let finddata = data.currentTableData.filter((item) => {
// return checkList.includes(item.storageId);
// });
// let finddata2 = data.currentSysTableData.filter((item) => {
// return checkList.includes(item.storageId);
// });
// finddata = finddata.concat(finddata2)
data.loadingCollection = false;
//data.checkList = [];
data.visible = false;
emit('confirm', checkList || [], data.source);
};
// 上传文件
// const handleUpload = () => {
// upload.value.handleUploadDlg();
// };
const selectCategory = (id: any) => {
if (data.loading) {
ElMessage.error('点击过快,正在加载数据');
return;
}
data.form.parentId = id;
handleSubmit();
};
const selectSystemCategory = (item: any) => {
if (data.loading) {
ElMessage.error('点击过快,正在加载数据');
return;
}
data.sysform.name = '';
data.iconfonturl = item.url;
data.sysform.parentId = item.storageId;
handleSysSubmit();
};
const selectFile = (item: any) => {
if (props.limit === 1) {
data.currentSysTableData.forEach((item: any) => {
item.selectclz = '';
});
data.currentTableData.forEach((item: any) => {
item.selectclz = '';
});
let index = data.checkList.findIndex((checkitem: any) => {
return checkitem == item.storageId;
});
if (index >= 0) {
data.checkList.splice(index, 1);
item.selectclz = '';
} else if (data.checkList.length > 0) {
data.checkList.splice(0, 1, item);
item.selectclz = 'active';
} else {
data.checkList.push(item);
item.selectclz = 'active';
}
} else {
let index = data.checkList.findIndex((checkitem: any) => {
return checkitem.storageId == item.storageId;
});
if (index >= 0) {
data.checkList.splice(index, 1);
item.selectclz = '';
} else {
data.checkList.push(item);
item.selectclz = 'active';
}
}
};
// 批量删除
const handleDelete = (val: any) => {
const storageId = val ? [val] : data.checkList;
if (storageId.length === 0) {
ElMessage.error('请选择要操作的数据');
return;
}
ElMessageBox.confirm('确定要执行该操作吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
closeOnClickModal: false,
})
.then(() => {
delData('/sys/storage', storageId).then(() => {
for (let i = data.currentTableData.length - 1; i >= 0; i--) {
if (storageId.indexOf(data.currentTableData[i].storageId) !== -1) {
data.currentTableData.splice(i, 1);
}
}
ElMessage.success('操作成功');
});
})
.catch(() => {});
};
// 文件上传成功后处理
const _getUploadFileList = (files: any) => {
for (const value of files) {
let stroage = value.response.data;
let index = data.currentTableData.findIndex((item: any) => {
return item.url == stroage.url;
});
if (index == 0) {
continue;
}
if (index > 0) {
data.currentTableData.splice(index, 1);
}
data.currentTableData.splice(0, 0, stroage);
}
};
const refform = ref();
const refinput = ref();
// 获取可选择目录
const getStorageDirectory = () => {
//if (!this.directoryList.length) {
listAllData('/sys/storage', { type: 'category' }).then((res) => {
data.categorys = res.rows;
});
};
const add = () => {
refform.value.validate((valid: boolean) => {
if (valid) {
data.dialogLoading = true;
data.nameForm.type = 'category';
addData('/sys/storage', { ...data.nameForm })
.then((res) => {
data.categorys.unshift({
...res.data,
is_default: 0,
});
//getStorageDirectory();
data.nameFormVisible = false;
ElMessage.success('操作成功');
})
.catch(() => {
data.dialogLoading = false;
});
}
});
};
const rename = () => {
refform.value.validate((valid: boolean) => {
if (valid) {
data.dialogLoading = true;
// renameStorageItem(data.nameForm.storageId, data.nameForm.name)
// .then(res => {
// data.currentTableData[data.nameForm.index].name = data.nameForm.name
// // data.directoryList = []
// getStorageDirectory()
// data.nameFormVisible = false
// data.$message.success('操作成功')
// })
// .catch(() => {
// data.dialogLoading = false
// })
}
});
};
const handleAdd = () => {
data.nameForm['name'] = undefined;
data.nameForm['parentId'] = data.form.parentId;
data.dialogLoading = false;
data.nameStatus = 'add';
data.nameFormVisible = true;
nextTick(() => {
refform.value.clearValidate();
});
};
// for(let i=1 ;i<=9;i++){
// data.syscategorys.push({
// id:'icon'+i,
// name:'图标库'+i
// })
// }
const handleClick = () => {
if (data.syscategorys.length == 0 && data.icontype == 'system') {
data.sysform.type = 'systemcategorys';
handleSyscatesSubmit();
}
};
return {
refform,
refinput,
selectSystemCategory,
rename,
add,
handleAdd,
handleClick,
...toRefs(data),
_getUploadFileList,
selectCategory,
getImageThumb,
selectFile,
handleDelete,
handleSearch,
handleAllSearch,
handleConfirm,
handlePaginationChange,
handleStorageDlg,
handleSysSearch,
};
},
});
</script>
<style>
.storages .el-input__suffix {
top: 5px;
height: auto;
}
</style>
<style lang="scss" scoped>
.breadcrumb {
border: 1px solid #dcdfe6;
padding: 10px !important;
}
.files {
height: 50vh;
}
.file-height {
min-height: calc(40vw - 190px);
}
.file-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-wrap: wrap;
:deep(.el-loading-mask) {
background-color: transparent;
}
}
.mt-1 {
margin-top: 0.25rem;
}
.p-1 {
padding: 0.25rem;
}
.delete {
color: #fff;
}
.file-list .item {
flex: none;
position: relative;
width: calc(20% - 20px);
margin: 10px;
text-align: center;
vertical-align: middle;
.file-image {
width: 100%;
height: 100%;
background-color: #eee;
border-radius: 4px;
background-size: contain;
background-repeat: no-repeat;
background-position: 50% 50%;
position: absolute;
left: 0;
top: 0;
}
&:before {
content: '';
display: inline-block;
padding-bottom: 100%;
width: 0.1px;
vertical-align: middle;
}
.file-name {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
line-height: 34px;
height: 34px;
background: rgba(0, 0, 0, 0.5);
color: #fff;
text-align: center;
z-index: 2;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
box-sizing: border-box;
}
.mask {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 5;
background-color: rgba(0, 0, 0, 0.5);
text-align: center;
display: none;
width: 100%;
height: 100%;
color: #fff;
font-size: 40px;
background: rgba(66, 139, 202, 0.8);
}
.el-dropdown {
position: absolute;
width: 34px;
line-height: 34px;
text-align: center;
background-color: #3296fa;
cursor: pointer;
bottom: 0;
right: 0;
z-index: 6;
display: none;
}
&:hover {
&:after {
position: absolute;
bottom: -20px;
left: 0px;
position: absolute;
padding: 3px 5px;
font-size: 12px;
font-weight: 700;
color: #fff;
white-space: nowrap;
background-color: #006eff;
border-radius: 3px;
content: attr(data-label);
}
.el-dropdown {
display: block;
}
}
&.active {
.mask {
display: flex;
align-items: center;
justify-content: center;
}
}
}
.brother-showing i {
width: 16px;
}
.diy-mb-5 {
margin-bottom: 5px !important;
}
.diy-tree {
width: 200px;
}
.file-border {
border: 1px solid #dcdfe6;
padding: 10px;
:deep(.el-form-item__content) {
margin-left: 0 !important;
}
.selected {
border: 1px solid var(--el-color-primary);
border-radius: 3px;
color: var(--el-color-primary);
}
}
.tree-scroll {
overflow: auto;
padding-bottom: 1px;
}
.custom-tree-node {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding-right: 8px;
}
.diy-table {
display: table-row;
}
.diy-cell {
display: table-cell;
}
</style>