SpringBoot+VUE实现文件导入
- 一、需求
- 二、前端代码实现
- 2.1 显示实现
- 2.1.1 a标签实现
- 2.1.1.1 上传标签实现
- 2.1.1.2 查看标签实现
- 2.2 上传文件和文件查看界面实现
- 2.2.1 上传文件界面
- 2.2.1.1 上传文件界面展示部分
- 2.2.1.1 上传文件界面逻辑部分
- 2.2.2 查看文件界面
- 2.2.2.1 查看文件界面展示部分
- 2.2.2.2 查看文件界面逻辑部分
- 2.2 其他逻辑
- 2.2.1 本行数据id选择逻辑
- 2.2.2 API 逻辑
- 三、后端实现
一、需求
- 必须支持PDF、docx、xlsx、xls、doc等格式
- 文件上传后保存在本地文件夹
- 需要进行在线预览
二、前端代码实现
2.1 显示实现
首先我们需要添加一个用于操作的按钮上去,像这样的:
<a @click="fileRef.onOpen(record)">查看文件</a>
<a @click="ProfessionImpExpRef.onOpen(record)">上传文件</a>
2.1.1 a标签实现
2.1.1.1 上传标签实现
当用户点击上传文件时我们会在右侧打开一个抽屉,用以展示上传界面,页面部分像这样:
<ProfessionImpExp ref="ProfessionImpExpRef" />
具体逻辑像这样:
//professionImpExp.vue 为上传界面
import ProfessionImpExp from './professionImpExp.vue'
const ProfessionImpExpRef = ref()
2.1.1.2 查看标签实现
文件查看部分基本和上传部分实现类似,页面部分:
<File ref="fileRef" @successful1="table.refresh(true)" />
//file.vue为文件查看界面
import File from "./file.vue";
const fileRef = ref()
const file = ref(null)
2.2 上传文件和文件查看界面实现
2.2.1 上传文件界面
2.2.1.1 上传文件界面展示部分
当用户点击上传文件时,我们需要打开一个界面用以提示用户支持的类型和上传注意事项、上传结果,像这样的:
<template>
<xn-form-container title="导入导出" :width="700" :visible="visible" :destroy-on-close="true" @close="onClose">
<span
>导入数据格式严格按照要求进行数据录入,<b style="color: red">重复导入或使用重名文件将会覆盖之前数据内容</b>
</span>
<a-divider dashed />
<div>
<a-spin :spinning="impUploadLoading">
<a-upload-dragger :show-upload-list="false" :custom-request="customRequestLocal" :accept="uploadAccept">
<p class="ant-upload-drag-icon">
<inbox-outlined></inbox-outlined>
</p>
<p class="ant-upload-text">单击或拖动文件到此区域进行上传</p>
<p class="ant-upload-hint">仅支持PDF、docx、xlsx、xls、doc格式文件</p>
</a-upload-dragger>
</a-spin>
</div>
<a-alert v-if="impAlertStatus" type="info" :show-icon="false" banner closable @close="onImpClose" class="mt-3">
<template #description>
<p>导入完成</p>
</template>
</a-alert>
</xn-form-container>
</template>
展示出来就是下面的效果:
2.2.1.1 上传文件界面逻辑部分
用户点击后,我们会打开一个抽屉,并将获取到的本行数据id传入到这个界面和上传的数据文件进行绑定,之后传回后端处理。
import { message } from 'ant-design-vue'
import professionApi from '@/api/biz/professionApi'
import {cloneDeep} from "lodash-es";
const impUploadLoading = ref(false)
const impAlertStatus = ref(false)
const dataId = ref()
const impAccept = [
{
extension: '.xls',
mimeType: 'application/vnd.ms-excel'
},
{
extension: '.xlsx',
mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
},
{
extension: '.PDF',
mimeType: 'application/pdf'
},
{
extension: '.docx',
mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
},
{
extension: '.doc',
mimeType: 'application/msword'
},
]
// 指定能选择的文件类型
const uploadAccept = String(
impAccept.map((item) => {
return item.mimeType
})
)
// 导入
const customRequestLocal = (data) => {
impUploadLoading.value = true
// 校验上传文件扩展名和文件类型是否为支持类型
const extension = '.'.concat(data.file.name.split('.').slice(-1).toString().toLowerCase())
const mimeType = data.file.type
// 提取允许的扩展名
const extensionArr = impAccept.map((item) => item.extension)
// 提取允许的MIMEType
const mimeTypeArr = impAccept.map((item) => item.mimeType)
if (!extensionArr.includes(extension) || !mimeTypeArr.includes(mimeType)) {
message.warning('上传文件类型仅支持PDF、word、excel、xls、xlsx格式文件!')
impUploadLoading.value = false
return false
}
const formData = new FormData();
formData.append("file",data.file)
formData.append("id",dataId.value.id)
return professionApi
.professionImport(formData)
.then((res) => {
impAlertStatus.value = res
})
.finally(() => {
impUploadLoading.value = false
})
}
// 关闭导入提示
const onImpClose = () => {
impAlertStatus.value = false
}
// 定义emit事件
const emit = defineEmits({ successful: null })
// 默认是关闭状态
let visible = ref(false)
const submitLoading = ref(false)
// 打开抽屉
const onOpen = (record) => {
visible.value = true
if (record) {
let recordData = cloneDeep(record)
dataId.value = Object.assign({}, recordData)
}
}
// 关闭抽屉
const onClose = () => {
visible.value = false
// 关闭导入的提示
onImpClose()
}
// 调用这个函数将子组件的一些数据和方法暴露出去
defineExpose({
onOpen
})
2.2.2 查看文件界面
2.2.2.1 查看文件界面展示部分
用户点击查看文件后,进入到这个界面进行文件查看和在线展示。
<template>
<xn-form-container
:title="'文件详情'"
:width="500"
:visible="visible"
:destroy-on-close="true"
@close="onClose"
>
<a-form ref="formRef" :model="formData" :rules="formRules" layout="vertical">
<a-form-item label="文件:" name="url">
<ol>
<li v-for="data in formData">
<a :href="'http://view.officeapps.live.com/op/view.aspx?src=https://img.qcybj.com/file/'+data" target="_blank"> {{ data }}</a>
</li>
</ol>
</a-form-item>
</a-form>
<template #footer>
<a-button style="margin-right: 8px" @click="onClose">关闭</a-button>
</template>
</xn-form-container>
</template>
具体的效果类似这种:
2.2.2.2 查看文件界面逻辑部分
这部分的逻辑很简单,主要就是对后端的数据解析并绑定。
import { cloneDeep } from 'lodash-es'
import XnFormContainer from "@/components/XnFormContainer/index.vue";
// 抽屉状态
const visible = ref(false)
const emit = defineEmits({ successful: null })
const file = ref()
const submitLoading = ref(false)
const formData = ref({})
// 打开抽屉
const onOpen = (record) => {
visible.value = true
if (record) {
let recordData = cloneDeep(record)
let str = Object.assign({}, recordData).url.substr(1)
let str1 = str.substring(0,str.length-1)
formData.value = str1.split(",")
}
}
// 关闭抽屉
const onClose = () => {
visible.value = false
}
// 默认要校验的
const formRules = {
}
// 抛出函数
defineExpose({
onOpen
})
2.2 其他逻辑
2.2.1 本行数据id选择逻辑
const selectedRowKeys = ref([])
// 列表选择配置
const options = {
// columns数字类型字段加入 needTotal: true 可以勾选自动算账
alert: {
show: true,
clear: () => {
selectedRowKeys.value = ref([])
}
},
rowSelection: {
onChange: (selectedRowKey, selectedRows) => {
selectedRowKeys.value = selectedRowKey
}
}
}
2.2.2 API 逻辑
这部分内容可以参照我之前的文章,这里不再多说~
//业务导入
professionImport(data) {
return request('import', data)
},
三、后端实现
后端部分大多分逻辑SpringBoot + Ant Design Vue实现数据导出功能都有提及,这里不再多言。我们之说最重要的实现逻辑:
@Transactional(rollbackFor = Exception.class)
@Override
public String importProfession(MultipartFile file,String id ) throws IOException {
Profession profession = baseMapper.selectById(id);
String urlList = profession.getUrl();
//定义文件名
String imgName = file.getOriginalFilename();
//定义上传路径
String upPath = path + imgName;
byte[] bytes = new byte[1024];
int dataLine;
try(BufferedInputStream bufferedInputStream = new BufferedInputStream(file.getInputStream());
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(upPath))){
while ((dataLine=bufferedInputStream.read(bytes))!= -1){
bufferedOutputStream.write(bytes, 0, dataLine);
}
}
if(urlList == null){
profession.setUrl(imgName);
}else {
List<String> list = Arrays.asList(urlList.split(","));
List<String> arrList = new ArrayList<>(list);
arrList.add(imgName);
profession.setUrl(arrList.toString());
}
QueryWrapper<Profession> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(Profession::getId,id);
this.update(profession,queryWrapper);
return imgName;
}
这里有一点需要注意,Java的IO方法很多,需要注意的是liunx系统中写入要指定绝对路径,不要用相对路径。
比如 MultipartFile 的 transferTo() 方法就可能使用相对路径。一旦使用了相对路径你就会发现原本指定写入的路径前多出了它:
/tmp/tomcat.8080.450079304707479782/work/Tomcat/localhost/ROOT