持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第1天,点击查看活动详情
前言
马上放假了,时间上相对宽裕,对最近做的东西进行一些总结。
今天我们来看一个非常实用的组件,上传组件
我们先从组件的定位、组件的应用场景、组件的特性几个方面进行归纳
定位:
对于上传组件,基础的组件功能属于基础组件范畴,满足上传的基本功能。
但业务平台的多样化,促使我们需要在基础组件之上,进行多样化扩展,满足业务功能的需求,这个多样化扩展隶属于业务组件层。但是需要注意几个点,进行组件约定,防止腐烂。
- 业务公用性
- 保持功能独立性,尽量解耦
- 无业务需求,不开发
应用场景:
- 数据导入,以excel为载体,一般都是成员列表、成绩导入等业务数据,方便用户进行批量操作
- 文件管理,需要兼容格式较多,PNG/JPG/GIF/PDF/DOC/XLS/txt/md/html/mp4/avi等常用格式,多应用于平台的资源管理类功能
- 独立业务功能,图片、头像临时上传需要
以上3个应用场景,一般的平台都有应用,但是不同场景对上传组件的功能要求可能不同。
这就需要考虑到业务特性以及产品规划能力。
可根据实际情况,从定位角度对不同场景的进行应用。
属性特征
1、单文件上传、多文件上传
2、文件格式校验、文件大小校验
3、上传文件维护(显示、移除)
组件实现
目录
对上传组件划分了3层,分别是 Base、Simple、Dialog
Base属于基础组件,提供简单的文件选择,和样式展示
Simple 复合组件-提供系统级别的扩展功能
1、上传提示语
2、文件格式、文件大小、模板下载
3、已上传列表管理-移除
Dialog 业务型组件
1、展示方式-弹窗
2、文件上传功能
3、业务参数控制、组件功能控制
入口文件
/*
* @Description:
* @Author: daerduo
*/
import Upload from './Base';
import Simple from './Simple';
import Dialog from './Dialog';
// 复合组件-上传组件
Upload.Simple = Simple
// 业务组件
Upload.Dialog = Dialog
export default Upload;
使用方式
<Upload.Dialog
ref="importRefs"
:options="UploadConfig"
:request-params="UploadParams"
@refresh="refresh"
/>
/**
* 导入配置
*/
const UploadConfig = {
title: '导入成员',
download: {
type: 'AccountVo',
tempName: '导入成员模板.xlsx'
}
}
<Upload.Simple ref="SimpleUploadRefs" :multiple="multiple" :file-rule="FILE_TYPE" :options="SimpleUploadConfig">
<template #notes="scope">
<span>只允许导入</span>
<span class="primary">XLS/XLSX</span>
<span>格式,且大小不超过10M</span>
<span v-if="getDownloadConfig()">,如果需要也可以<a class="download primary" title="点击下载" @click="downloadTempHandler">下载模板</a></span>
</template>
</Upload.Simple>
/**
* 上传文件大小限制
*/
const SimpleUploadConfig = {
fileSize: 10
}
默认是Base,之间使用标签即可
<Upload @fileChange="fileChange" :multiple="multiple"></Upload>
组件之间的交互
基础上传组件 Base
基础的组件是基于现有的第三方组件库(element)el-upload基础上,做界面扩展和功能增强使用
- 界面扩展部分
主要是外框
及内部文字、图标
的调整以,在el-upload的默认插槽slot
- 行为部分
multiple
: props属性控制,受外部调用控制,默认是false,单选模式
on-change
: 添加文件、上传成功和上传失败时都会被调用
on-remove
:文件列表移除文件时触发
<template>
<div>
<el-upload drag :multiple="multiple" :http-request="(val) => { }" :on-change="fileChange" :on-remove="fileRemove">
<div class="el-upload__text">
<div class="upload-icon primary">
<svg-icon icon-class="icon-shangchuan"></svg-icon>
</div>
将文件拖到此处或 <em>点击上传</em>
</div>
</el-upload>
</div>
</template>
<script setup>
const props = defineProps({
multiple: {
type: Boolean,
default: false
},
})
const emit = defineEmits(["fileChange", "remove"]);
/**
* 选中文件后回调
*/
const fileChange = (file, fileList) => {
emit('fileChange',file,fileList)
}
/**
* 文件列表移除文件时触发
*/
const fileRemove = (file) => {
emit('remove',file)
}
</script>
<style lang="scss" scoped>
@import "../style/base.scss"
</style>
展示型组件 Simple
对于基础组件,只能满足组件的选择,实际业务应用,通用性不强。展示型组件对基础组件的进一步封装,主要目的是满足业务功能需求
,这一点很重要。
因要考虑页面交互方式,交互方式根据业务场景可划分为
1、在组件内嵌套
,如表单页面,做为表单项存在
2、独立个体,在弹窗内
内使用,如数据导入等
需要满足以上2种方式。
<template>
<div>
<div class="import-tips">
<svg-icon icon-class="icon-mianxing-jieshishuoming"></svg-icon>
填写模板时请仔细阅读文件中的说明文字,并严格按照其中所属规则填写,否则可能会导入失败
</div>
<!-- 基础上传组件 -->
<BaseUpload @fileChange="fileChange" :multiple="multiple"></BaseUpload>
<!-- 文件格式限制、tips -->
<div class="notes">
<slot name="notes"></slot>
</div>
<!-- 选择后-文件列表 -->
<div class="after" v-for="(item, index) in uploadForm.files" :key="index">
<div>
<span style="color: #283241; margin-left: 10px">{{ item.name }}</span>
</div>
<div @click="removeFile(item)" class="remove-file" title="移除文件">
<svg-icon icon-class="icon-shanchu"></svg-icon>
</div>
</div>
</div>
</template>
<script setup>
import { ElMessage } from "element-plus";
import BaseUpload from "../Base";
import useSimple from "./Simple";
const { uploadForm, clearForm, removeFile } = useSimple();
const emit = defineEmits(["fileChange", "remove"]);
const props = defineProps({
multiple: {
type: Boolean,
default: false,
},
limit: {
type: Number,
default: 1,
},
options: {
type: Object,
default: {},
},
fileRule: {
type: Array,
default: () => {},
},
});
// 获取父组件传递值-配置
const getOpions = () => props.options;
/**
* 下载模板
* excel
*/
const downloadTemplateHandler = (params) => {
const data = {};
downloadHandler(data);
};
/**
* 获取文件校验格式
*/
const getFileRule = () => {
return props.fileRule || [];
};
const fileSizeRule = (file, fileSize) => {
if (!fileSize) return true
const isSize = file.size / 1024 / 1024 < fileSize
if (!isSize) {
ElMessage({
type: 'warning',
message: `上传文件大小不能超过${fileSize}M!`
})
return false
}
return true
}
/**
* 选中文件后回调
*/
const fileChange = (file, fileList) => {
const uploadFile = uploadForm.files;
const fileType = file.raw.type;
const limit = props.limit;
if (uploadFile && uploadFile.length == limit) {
ElMessage({
type: "warning",
message: `最多上传${limit}个文件!`,
});
return false;
}
const fileRule = getFileRule();
if (fileRule.length) {
const isType = fileRule.includes(fileType);
if (!isType) {
ElMessage({
type: "warning",
message: "文件格式错误!",
});
return false;
}
}
const fileSize = getOpions().fileSize
if(!fileSizeRule(file, fileSize)) return
let flag = false;
uploadForm.files.forEach((item) => {
if (file.name == item.name) {
flag = true;
}
});
if (!flag) {
uploadForm.files.push(file.raw);
emit("fileChange", file, fileList);
}
};
/**
* 获取文件列表
*/
const getFiles = () => {
return uploadForm.files;
};
/**
* 清空上传文件
*/
const clearFiles = () => {
clearForm();
};
defineExpose({
getFiles,
clearFiles,
});
</script>
<style lang="scss" scoped>
@import "../style/base.scss";
.notes {
margin: 10px 0;
}
</style>
hooks部分
import { reactive, ref } from "vue";
export default function useSimple() {
const uploadForm = reactive({
files: [],
});
const clearForm = (state) => {
uploadForm.files = [];
}
/**
* 移除文件
*/
const removeFile = (file) => {
let index = uploadForm.files.findIndex((item) => item.name == file.name);
uploadForm.files.splice(index, 1);
}
return {
uploadForm,
clearForm,
removeFile,
};
}
移除已选择的文件列表,和单个文件的移除,以及向外暴露方法
从上面代码可以看出来,提供了样式多样化部分:
- 提示说明
- 文件选择后的列表,移除文件
- 文件模式(单文件、多文件选择)、上传个数限制、文件格式、文件大小校验
向外暴露方法
1、getFiles
获取已上传文件列表
2、clearFiles
清楚已选择的文件列表
3、事件:fileChange
,文件选择后回调、remove
移除文件后回调
业务组件 Dialog
这部分是基于展示型组件之上,
如果使用场景在表单内,可直接将展示型组件做为业务容器类组件直接使用。
如果需要弹窗使用,可以在业务功能内做为业务组件进行扩展。
因当前业务组件具备一定通用性
,固做为通用性业务组件使用。
当前业务组件,是以导入功能为引导,导入之前,需要先进去导入模板下载,对导入的文件需要进行以下判断
- 上传文件模式,单文件选择
- 文件格式限制,只能上传excel文件
- 文件上传需要按照一定的模板,弹窗内需要提供模板下载
- 文件上传请求
这是业务功能的要求。业务组件内,页面交互涉及到:
- 文件上传前的校验控制
- 上传时的进度或者Loading控制
- 上传状态后,页面控制等,如关闭窗体、回调触发刷新页面等
<template>
<div>
<Dialog.Next :title="getOpions().title || defaultValue.title" ref="uploadDGRefs" width="580px"
:show-confirm-btn="true" @close="close" @confirm="saveHandler" :limit="limit">
<div v-loading="isLoading" :element-loading-text="getOpions().loadingText || defaultValue.loadingText"
style="height:440px">
<SimpleUpload ref="SimpleUploadRefs" :multiple="multiple" :file-rule="FILE_TYPE" :options="SimpleUploadConfig">
<template #notes="scope">
<span>只允许导入</span>
<span class="primary">XLS/XLSX</span>
<span>格式,且大小不超过10M</span>
<span v-if="getDownloadConfig()">,如果需要也可以<a class="download primary" title="点击下载" @click="downloadTempHandler">下载模板</a></span>
</template>
</SimpleUpload>
</div>
</Dialog.Next>
</div>
</template>
<script setup>
import { ElMessage } from "element-plus";
import { ref } from "vue";
import { Dialog } from "@businessComponents";
import SimpleUpload from "../Simple";
import useDialog from "./dialog";
import { Controller } from "./models";
const { uploadForm, isLoading, setLoading, downloadHandler, FILE_TYPE } = useDialog()
const props = defineProps({
multiple: {
type: Boolean,
default: false
},
limit: {
type: Number,
default: 1
},
requestParams: {
type: Object,
default: {}
},
options: {
type: Object,
default: {}
},
})
/**
* 默认值
*/
const defaultValue = {
title: '导入',
loadingText: '正在导入请耐心等待...'
}
/**
* 上传文件大小限制
*/
const SimpleUploadConfig = {
fileSize: 10
}
// 获取父组件传递值-配置
const getOpions = () => props.options;
const uploadDGRefs = ref(null);
const SimpleUploadRefs = ref(null);
const emit = defineEmits(["refresh"]);
const getDownloadConfig = (params) => {
return getOpions().download
}
const downloadTempHandler = (params) => {
const config = getDownloadConfig()
downloadHandler(config)
}
/**
* 数据保存
*/
const saveAction = async (formData) => {
Controller.uploadFile(formData).then((res) => {
if (res) {
ElMessage({
type: "success",
message: "添加成功!",
});
emit("refresh");
close();
}
setLoading(false)
}).catch(() => setLoading(false));
}
/**
* 获取内部上传组件的文件列表
*/
const getFiles = () => {
return SimpleUploadRefs.value.getFiles()
}
/**
* 获取外部传入的上传参数
*/
const getRequestParams = () => {
return props.requestParams || {}
}
// 保存
const saveHandler = () => {
const files = getFiles()
if (!files || !files.length) {
return ElMessage({
type: "warning",
message: "请上传项目文件!",
});
}
if (isLoading.value) {
return ElMessage({
type: "warning",
message: "文件上传中,请稍等....",
});
return
}
setLoading(true)
// 拼装参数-文件流 FormData
let formData = new FormData();
files.forEach((item) => {
formData.append("file", item);
});
uploadForm.jsonData = getRequestParams()
formData.append("jsonData", JSON.stringify(uploadForm.jsonData));
// 上传-数据保存
saveAction(formData)
}
// 打开
const load = () => {
uploadDGRefs.value.open();
};
/**
* 关闭
*/
const close = () => {
uploadForm.jsonData = {};
setLoading(false)
SimpleUploadRefs.value.clearFiles()
uploadDGRefs.value.close();
};
defineExpose({
load,
})
</script>
<style lang="scss" scoped>
.primary {
color: #00afa5;
}
.download {
cursor: pointer;
padding-left: 10px;
}
</style>
hooks
import { reactive, ref } from "vue";
import { useExport } from "@hooks";
/**
* 上传文件格式控制
* FILE_TYPE = [
"application/pdf",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/msword",
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"text/plain",
"application/x-zip-compressed",
"application/vnd.ms-powerpoint",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"image/png",
"image/jpeg"
]
*/
export default function useDialog() {
const isLoading = ref(false);
const setLoading = (state) => {
isLoading.value = state
}
const uploadForm = reactive({
jsonData: {
name: "",
},
})
/**
* 文件格式控制
*/
const FILE_TYPE = [
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.ms-powerpoint",
]
/**
* 文件下载映射
*/
const DOWNLOAD_TYPE = {
"AccountVo":"com.iss.edu.service.teachcoursemanage.model.vo.AccountVo"
}
/**
* 下载模板
* @param {*} params
*/
const downloadHandler = (config) => {
const name = DOWNLOAD_TYPE[config?.type || 'AccountVo']
useExport({
name: 'downloadExcelTemplate',
fileName: config?.tempName || "导入模板.xlsx",
fileType: "application/vnd.ms-excel;charset=utf-8"
}, { name })
}
return {
isLoading,
FILE_TYPE,
setLoading,
uploadForm,
downloadHandler
};
}
CRUD接口请求--models
import { Service,Request } from '@basic-library';
/**
* @description: 服务器请求控制器
* @param NO
* @return {*}
*/
class MService {
// 文件导入
async uploadFile(data) {
const { success } = await Service.useMultiPart('importMember', data)
return success
}
}
const Controller = new MService()
export {
Controller
}
代码说明
loading控制部分,使用了vue的指令 v-loding 和 element-loading-text 进行上传中,和文字控制
/**
* 默认值
*/
const defaultValue = {
title: '导入',
loadingText: '正在导入请耐心等待...'
}
文件大小控制
/**
* 上传文件大小限制
*/
const SimpleUploadConfig = {
fileSize: 10
}
如果属性不存在,则不进行文件大小限制
下载模板控制,是根据外部传入配置控制,使用download对象,如果存在配置则进行功能开启
下载模板的功能,因比较复杂,系统层进行了hooks封装--useExport,可以翻阅之前的文章查看具体实现。 文件流转换为blob,然后使用a连接进行下载
下载的接口,与服务端进行讨论,不同模板的下载类型,是根据不同参数控制(后端微服务映射,比较麻烦)
文件上传,采用了需要拼装文件流-FormData
,请求方式支持 Content-Type': 'multipart/form-data'
完结~~~