Vue+Ant: 自定义上传文件时机以实现导入(简易通用组件封装)
MarkTime: 2024-12-16 08:47:00
LogTime: 2024-12-16 07:00:00
LastModifiedTime: 2025-01-31 00:01:06
导入相关的组件, 觉得有一些用到的东西挺具有通用性, 蛮值得记录一下的
知识点:
- 利用axios结合表单传输文件;
- 在ant-desgisn-vue的原生a-upload基础上, 使用自定义上传文件方式 达到 延时触发文件上传的目的;
- 是一个导入的封装简易组件(提供①下载导入模板; ②上传装载着导入数据的文件;)
- ①和②涉及到的后端代码部分不进行展示, 利用EasyExcel完成, 下载模板的时候接收
效果
逻辑
- 点击 蓝字 触发导入模板下载 并自定义保存位置;
- 在此导入模板基础上进行数据增加/修改;
- 点击"选取文件", 选择刚刚操作过的文件;
- 点击"导入", 等待后端校验完毕返回导入结果.
附图







源码
自身组成
ls-import.vue
<template>
<div class="ls-import">
<!-- 遮罩 -->
<a-spin
:spinning="isPageLoading"
:indicator="h(LoadingOutlined, { style: { fontSize: '24px' }, spin: false})"
>
<div class="ls-box redure50">
<a-form style="padding: 0 20px 10px;" name="formRef" ref="formRef" v-model:model="form">
<a-row>
<a-col :span="24">
<div class="remark">
<exclamation-circle-outlined style="padding-right: 5px;"/>
<span>备注:请使用模板进行导入!</span>
</div>
</a-col>
</a-row>
<a-row>
<a-col :span="24">
<a-form-item label="模板下载:" style="font-size: 16px; display: flex; flex-direction: row; align-items: baseline;">
<a @click="toDownloadTmpl" href="javascript: void(0)" style="text-decoration: underline;">{{ props.tmplName }}</a>
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col :span="1" :xl="{span: 24}" :xs="24" style="width: 110px; display: flex; flex-direction: column; align-items: center;">
<div :class="['ls-upload',fileList.length>0?'ls-upload-fill':'']" >
<!-- <a-upload
v-model:file-list="fileList"
:accept="'.xls,.xlsx'"
:action="actionUrl"
:before-upload="beforeUpload"
:custom-request="customRequest"
@change="handleChange"
@download="handleDownload"
@reject="handleReject"
@drop="handleDrop"
@preview="handlePreview"
> -->
<a-upload
v-model:file-list="fileList"
:max-count="1"
:accept="'.xls,.xlsx'"
:before-upload="onBeforeUpload"
@change="onChangeUpload"
>
<a-button :style="{ border:'none' }"><upload-outlined></upload-outlined>选取文件</a-button>
<template #previewIcon>
<link-outlined :style="{width: '14px',color:'#BFBFBF'}"/>
</template>
<template #removeIcon>
<close-circle-filled :style="{width: '14px',color:'#BFBFBF'}"/>
</template>
</a-upload>
</div>
</a-col>
</a-row>
</a-form>
</div>
<div class="ls-buttom-btn center">
<a-button @click="toImport" type="primary" style="margin-right: 10px;">导入</a-button>
<a-button @click="toCancel">取消</a-button>
</div>
</a-spin>
</div>
</template>
<script>
export default {
name: 'ls-import'
}
</script>
<script setup>
import { ref, reactive , computed, defineProps, defineEmits, h } from 'vue';
import { Upload, message } from 'ant-design-vue';
import axios from "axios";
const props = defineProps({
// 模板名称
tmplName: {
type: String,
default: ''
},
// 模板下载地址
tmplUrl: {
type: String,
default: '',
},
// 文件上传地址
uploadUrl: {
type: String,
default: '',
}
})
const form = reactive({})
const iFile = ref({});
const fileList = ref([]);
const isPageLoading = ref(false); // 遮罩
const tmplUrlCmpt = computed(() => {
let url = window.server.baseURL + props.tmplUrl
// console.log("tmplUrlCmpt"+url)
return url;
})
const uploadUrlCmpt = computed(() => {
let url = window.server.baseURL + props.uploadUrl;
// console.log("uploadUrlCmpt"+url)
return url;
})
/**
* 模板下载
*/
const toDownloadTmpl = () => {
window.open(tmplUrlCmpt.value, '_blank') // 触发新的标签页闪出并弹出模板下载保存窗口
}
/**
* 文件上传 前置校验
*/
const onBeforeUpload = (file) => {
let acceptTypeList = ['xls', 'xlsx'];
let fileSuffix = file.name.substring(file.name.lastIndexOf('.') + 1);
let fileMaxSize = 10*1024*1024; // M
let isAcceptType = acceptTypeList.indexOf(fileSuffix) !== -1
if (!isAcceptType) {
// message.destroy(); // 全局清除
message.warning('上传文件格式不符合规范, 支持格式为: .xls,.xlsx');
return Upload.LIST_IGNORE;
}
if (file.size > fileMaxSize) {
message.warning('选择的文件大小超过限制, 大小不得超过: 10M');
return Upload.LIST_IGNORE;
}
iFile.value = file;
return false; // 阻止默认上传事件
}
/**
* a-upload 组件没有针对上传的成功失败有专门的回调函数
*/
const onChangeUpload = (info) => {
if (info.fileList[0].uid===iFile.value.uid) {
iFile.value = info.fileList[0].originFileObj;
}
// console.log('onChangeUpload: ' + e)
}
/**
* 自定义上传
*/
const toCustomRequest = (e) => {
if (!e?.file) {
message.warning('请先选择导入文件! ', 'warning', );
return;
}
isPageLoading.value = true;
let formData = new FormData();
// console.log(e.file); // 确保它是 File 对象
// console.log(Object.prototype.toString.call(e.file)); // [object File]
formData.append("file", e.file);
axios({
url: uploadUrlCmpt.value,
method: 'POST',
data: formData,
transformRequest: [function(data, headers) {
// 去除post请求默认的Content-Type
delete headers.post['Content-Type']
return data
}],
headers: {
// 'Content-type': 'multipart/form-data',
'tk': localStorage.getItem("tk") // 这里只是自定义携带一下需要的当前登录用户信息
},
}).then(res => { // 成功
// 成功之后就可以把表单关调了
if (res.data.success) {
message.success(res.data.resultObject, 1);
emit('onSuccess')
} else {
message.error(res.data.resultObject, 1);
}
}).catch(err => { // 失败
fileList.value = [];
message.error(`文件上传失败! `, 1);
}).finally(() => { isPageLoading.value = false; })
}
/**
* 行为 导入
*/
const toImport = () => {
toCustomRequest({ file: iFile.value });
}
/**
* 取消
*/
const toCancel = () => {
emit('onClose')
}
// 回调
const emit = defineEmits(['onClose', 'onSuccess'])
</script>
<style lang="less" scoped>
.ls-import {
position: relative;
height: 100%;
width: 100%;
.remark {
color: #ff3118;
font-size: 14px;
height: 30px;
}
}
</style>
<style lang="less">
.ant-spin-nested-loading {
height: 100% !important;
}
.ant-spin-container {
height: 100% !important;
}
.ls-upload {
border-radius: 4px;
.ant-upload-select {
width: 100%;
border-radius: 4px;
border: 1px solid #D9D9D9;
}
.toRight {
border-left: 1px solid #BFBFBF !important;
float: right;
background: #F7F5F7;
}
.ant-upload-list-picture-container {
padding: 0 20px;
}
.ant-upload-list-text-container {
padding: 0 20px;
}
}
.ls-upload-fill {
padding-bottom: 10px;
border: 1px solid #D9D9D9;
.ant-upload-select {
border: none;
border-bottom: 1px solid #D9D9D9;
}
}
</style>
ls-global.css
.ls-box {
position: relative;
height: 100%;
overflow-y: auto;
}
.redure50 {
height: calc(100% - 50px);
}
.ls-buttom-btn {
background-color: #fff;
display: flex;
justify-content: right;
align-items: center;
position: absolute;
width: 100%;
height: 50px;
bottom: 0;
&.center {
justify-content: center;
}
}
.ls-select-disabled-allow-copy {
&.ant-select-disabled {
cursor: default; /* 去掉悬浮时出现的小手 */
.ant-select-selector {
pointer-events: none; /* 你可以看到元素,但你操作不到,操作会穿透触发到下层的元素 */
.ant-select-selection-item {
user-select: auto; /* 允许选择文本 */
}
}
}
}
.ls-input-disabled-allow-copy {
&.ant-input-disabled {
cursor: default;
user-select: auto;
}
}
main.js
暴露样式文件ls-global.css到全局
import '../public/ls/ls-global.css';
ls-import.vue的使用
<template>
<div class="lsl-test">
</div>
</template>
<script>
export default {
name: 'lsl-test',
}
</script>
<script setup>
// 导入 ------------------------------------------------------
const LsImport = defineAsyncComponent(() => import('@views/business/base/import-export/ls-import.vue')); // 导入
const lsImportRef = ref();
const isShowImport = ref(false);
const impAssisted = reactive({
tmplName: 'lsl测试导入模板.xlsx',
tmplUrl: '/html/forest/substanceDeviceAction.do?action=downloadSubstDevExcelImpTmpl', // 模板下载地址
uploadUrl: '/html/forest/substanceDeviceAction/importSubstDevFromExcel', // 文件上传地址
})
// 在页面内设置一个按钮或者其他什么, 触发的时候调用这个 toImport() 就行
const toImport = () => {
isShowImport.value = true;
}
// -----------------------------------------------------------
</script>
<style lang="less" scoped>
.lsl-test {
position: relative;
height: 100%;
width: 100%;
}
</style>
补充
导入模板下载(后端实现简述)
导入模板下载的后端实现: 逻辑:
① 将以项目resource文件夹下的lsl测试导入模板.xlsx为基底的文件, 进行一些模板占位值替换行为后, 得到可供用户下载的excel模板文件;
② Controller层接口接收HttpServletResponse作为入参, 调用封装好的工具类下载方法, 将准备好的excel模板文件 复制到 HttpServletResponse提供的字节流以输出;
③ 后端主要还是使用了EasyExcel达成目的.
以下为主实现代码

触发导入行为(后端接口层入参)
@PostMapping("/importSubstDevFromExcel")
@ResponseBody
public String importSubstDevFromExcel(@RequestParam(value = "file") MultipartFile file{
// ...相关导入校验逻辑
}
导出的前端代码
导出的封装的前端组件, 其实和导入的差别不大, 甚至更简单, 所以只附上最主要的触发导出的方法.
以下代码的逻辑是: 携带当前页面的搜索条件, 以便于触发导出时, 也使用此搜索条件进行数据过滤, 并导成excel文件后返回给前端供用户下载.
<template>
<div class="ls-export">
<a-spin
:spinning="isPageLoading"
:indicator="h(LoadingOutlined, { style: { fontSize: '24px' }, spin: false})"
>
<div class="ls-box redure50">
<l-form style="padding: 0 20px 10px;" name="formRef" ref="formRef" v-model:model="form">
<l-row>
<a-col :span="24">
<a-form-item
style="font-size: 16px; display: flex; flex-direction: row; align-items: baseline;"
label="导出页类型"
name="expPageType"
:rules="[ {required: true, message: '请选择导出页类型', trigger: ['change', 'blur'] },]"
>
<a-select
style="padding-left: 10px;"
v-model:value.trim="form.expPageType"
placeholder="请选择导出页类型"
allow-clear
:options="expPageTypeList"
>
</a-select>
</a-form-item>
</a-col>
</l-row>
</l-form>
</div>
<div class="ls-buttom-btn center">
<a-button type="primary" style="margin-right: 10px;" @click="toExport">导出</a-button>
<a-button @click="toCancel">取消</a-button>
</div>
</a-spin>
</div>
</template>
<script>
export default {
name: 'ls-export'
}
</script>
<script setup>
import { ref, reactive , h, defineProps, defineEmits, isProxy, toRaw } from 'vue';
import axios from "axios";
const formRef = ref();
const props = defineProps({
// 导出文件名称
fileName: {
type: String,
default: '导出',
},
// 导出调用接口
url: {
type: String,
default: '',
},
// 相关参数
param: {
type: Object,
default: () => {},
}
})
// 回调
const emit = defineEmits(['onClose', 'onSuccess', 'onFailure'])
const form = reactive({ // 表单数据
expPageType: '1', // 默认 导出本页
});
const expPageTypeList = reactive([
{ label: '导出本页', value: '1'},
{ label: '导出全部', value: '2'},
]);
const isPageLoading = ref(false);
/**
* 触发导出
*/
const toExport = () => {
formRef.value.validate({
success: () => {
axios({
url: window.server.baseURL + props.url,
method: 'POST',
headers: {
tk: localStorage.getItem('tk') // 当前登录用户的信息, 可根据实际调整, 即不传
},
data: packageParam(), // 自定义的封装参数的方法, 返回一个对象(不是重点, 根据项目实际调整)
responseType: 'blob'
}).then(res => { // 成功
let url = window.URL.createObjectURL(new Blob([res.data]));
let link = document.createElement('a');
link.style.display = 'none';
link.href = url;
let fileName = props.fileName + new Date().toLocaleString().replaceAll('/', '').replaceAll(':', '').replaceAll(' ', '');
link.setAttribute('download', fileName + '.xlsx');
document.body.appendChild(link);
link.click();
emit('onSuccess');
}).catch(err => { // 失败
emit('onFailure', err.data);
}).finally(() => { isPageLoading.value = false; })
},
error: () => {
isPageLoading.value = false;
}
});
}
/**
* 封装参数(根据实际调整或删除该方法)
*/
const packageParam = () => {
let data = props.param;
data.condition = Object.assign(props.param.condition, form);
// 为一致, 使用 lh.core.js 的 fetch() 中逻辑
let paraMap = {};
for (let d in data) {
if (data[d]) {
if (d==='condition' && isProxy(data[d])) { // 判断 data[d] 是否是 Proxy 类型
data[d] = JSON.stringify(toRaw(data[d]));
}
paraMap["map['" + d + "']"] = data[d];
paraMap[d] = data[d];
}
}
paraMap.currPage = data.currPage?data.currPage:1;
paraMap.pageSize = data.pageSize?data.pageSize:10;
return paraMap;
}
/**
* 取消
*/
const toCancel = () => {
emit('onClose')
}
</script>
<style lang="less" scoped>
.ls-export {
position: relative;
height: 100%;
width: 100%;
:deep(.ant-spin-nested-loading) {
height: 100% ;
.ant-spin-container {
height: 100%;
}
}
}
</style>
参考资料
问题 <= 参考
-
[formData打印是空对象 <= 遇到一个问题,formData 对象的 append() 函数增加值,无效](https://segmentfault.com/q/1010000010087308)
-
后端获取不到file对象 <= 项目中使用antd中的upload组件file对象到底是info.file还是info.file.originFileObj_坑
-
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律