前言
马上放假了,时间上相对宽裕,对最近做的东西进行一些总结。今天我们来看一个非常实用的组件,上传组件
我们先从组件的定位、组件的应用场景、组件的特性几个方面进行归纳
定位:
对于上传组件,基础的组件功能属于基础组件范畴,满足上传的基本功能。但业务平台的多样化,促使我们需要在基础组件之上,进行多样化扩展,满足业务功能的需求,这个多样化扩展隶属于业务组件层。但是需要注意几个点,进行组件约定,防止腐烂。
- 业务公用性
- 保持功能独立性,尽量解耦
- 无业务需求,不开发
应用场景:
- 数据导入,以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.Dialogref="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 trueconst isSize = file.size / 1024 / 1024 < fileSizeif (!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().fileSizeif(!fileSizeRule(file, fileSize)) returnlet 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)// 拼装参数-文件流 FormDatalet 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'
最后
为大家准备了一个前端资料包。包含54本,2.57G的前端相关电子书,《前端面试宝典(附答案和解析)》,难点、重点知识视频教程(全套)。
有需要的小伙伴,可以点击下方卡片领取,无偿分享