前言
在企業應用的快速開發中,我們需要儘快的完成一些功能。如果您使用了Ant Design Vue,在進行表單的文件上傳相關功能開發的時候,您肯定迫不及待地需要找到一篇包治百病的文章,正是如此,纔有了該文的誕生,願以此文解君憂。
方案設計
前端方案設計
- 重寫a-upload的文件上傳方式,使用axios來進行上傳
- 選擇一個文件後立即進行上傳,前端記錄上傳成功後的name和uid,並構建一個File實例,用於a-upload組件的已上傳文件列表的回顯
- 提交前讓文件列表轉化爲要後端要處理的uid列表
後端方案設計
- 提供一個統一上傳單個文件的接口,每個文件選擇後自動上傳都將上傳到該接口,並寫入到數據庫的file數據表中
- 對錶單數據新建完成後,對上傳的文件列表進行當前實體記錄的綁定
- 對錶單數據更新完成後,檢測該實體記錄的所有文件列表,對沒有在該列表中的uid的文件列表進行刪除,然後對該列表中所有的uid文件記錄進行當前實體記錄的綁定
新建與更新的一致性處理方案
- 因爲更新表單在讀取舊數據後,需要與新選擇文件進行同樣的格式化處理,這裏的處理流程一樣,進行回顯的數據是一樣的,提交表單也都是提交file表中已經存在的uid列表,所以這裏的數據結構是一致的,處理起來將會更加簡潔明瞭。
讓代碼說話
爲了讓各位看官老爺們便於理解,直接上代碼,希望能將整件事說明白。
構建表單
<a-form :form="form">
<a-form-item label="名稱" style="margin-bottom: 0;">
<a-input v-decorator="['name', {rules: [{required: true, message: '請輸入名稱!'}]}]" />
</a-form-item>
<a-form-item>
<a-upload
:multiple="true"
:fileList="downloadFiles"
:remove="handleDownloadFileRemove"
:customRequest="downloadFilesCustomRequest"
>
<a-button class="upload-btn"> <a-icon type="upload" > 相關下載 </a-button>
</a-upload>
</a-form-item>
</a-form>
編寫js代碼
- 請求後端接口的token、header以及baseUrl等我已默認您已經在axios的統一設置中已經配置好了
- 爲了簡化axios相關操作,我們將axios進行了如下封裝(您也可以按此完全使用axios來直接對數據進行提交等):
const dibootApi = {
get (url, params) {
return axios.get(url, {
params
})
},
upload(url, formData) {
return service({
url,
method: 'POST',
data: formData
})
}
}
export default dibootApi
- 我們默認爲demo實體中需要上傳一些文件列表
export default {
name: 'demoForm',
data () {
title: '新建', // 該表單的功能標題
form: this.$form.createForm(this), // 表單數據初始化,沒什麼好說的
model: {}, // 如果是更新表單,之前的數據放到這裏,來做數據初始化顯示之用
downloadFiles: [] // 已經上傳的文件列表
},
methods: {
// 初始打開的表單時的處理(如果是更新表單,這裏首先需要讀取出相關數據)
async open (id) {
if (id === undefined) {
// 沒有id數據則認爲是新建
this.model = {}
this.afterOpen()
} else {
// 否則作爲更新處理
const res = await dibootApi.get(`/${this.name}/${id}`)
if (res.code === 0) {
this.model = res.data
this.title = '編輯'
this.afterOpen(id)
} else {
this.$notification.error({
message: '獲取數據失敗',
description: res.msg
})
}
}
},
// 更新表單在讀取數據完成後的操作
afterOpen (id) {
// 獲取該記錄信息後,回顯文件列表相關操作
dibootApi.post(`/demo/getFiles/${id}`).then(res => {
if (res.code === 0){
if (res.data.downloadFile !== undefined){
res.data.downloadFile.forEach(data => {
this.downloadFiles.push(this.fileFormatter(data))
})
}
}
})
},
// 重寫a-upload的文件上傳處理方式
downloadFilesCustomRequest (data) {
this.saveFile(data)
},
// 上傳並保存文件
saveFile (data){
const formData = new FormData()
formData.append('file', data.file)
dibootApi.upload('/demo/upload', formData).then((res) => {
if (res.code === 0){
let file = this.fileFormatter(res.data)
// 上傳單個文件後,將該文件會先到a-upload組件的已上傳文件列表中的操作
this.downloadFiles.push(file)
} else {
this.$message.error(res.msg)
}
})
},
// 對上傳成功返回的數據進行格式化處理,格式化a-upload能顯示在已上傳列表中的格式(這個格式官方文檔有給出的)
fileFormatter(data) {
let file = {
uid: data.uuid, // 文件唯一標識,建議設置爲負數,防止和內部產生的 id 衝突
name: data.name, // 文件名
status: 'done', // 狀態有:uploading done error removed
response: '{"status": "success"}', // 服務端響應內容
}
return file
},
// 沒錯,刪除某個已上傳的文件的時候,就是調用的這裏
handleDownloadFileRemove (file) {
const index = this.downloadFiles.indexOf(file)
const newFileList = this.downloadFiles.slice()
newFileList.splice(index, 1)
this.downloadFiles = newFileList
},
// 表單校驗就靠他了,不過這裏面還是可以對需要提交的一些數據做些手腳的
validate () {
return new Promise((resolve, reject) => {
this.form.validateFields((err, fieldsValue) => {
if (!err) {
// 設置上傳文件列表
const downloadFiles = this.downloadFiles.map(o => {
return o.uid
})
const values = {
...fieldsValue,
'downloadFiles': downloadFiles
}
resolve(values)
} else {
reject(err)
}
})
})
},
// 表單提交的相關操作
async onSubmit () {
const values = await this.validate()
try {
let result = {}
if (this.model.id === undefined) {
// 新增該記錄
result = await this.add(values)
} else {
// 更新該記錄
values['id'] = this.model.id
result = await this.update(values)
}
// 執行提交成功的一系列後續操作
this.submitSuccess(result)
} catch (e) {
// 執行提交失敗的一系列後續操作
this.submitFailed(e)
}
},
// 新增數據的操作
async add (values) {
....
},
// 更新數據的操作
async update (values) {
...
}
}
}
編寫SpringBoot相關的接口代碼
- DemoController
/***
* 獲取文件信息列表
* @param id
* @return
* @throws Exception
*/
@PostMapping("/getFiles/{id}")
public JsonResult getFilesMap(@PathVariable("id") Serializable id) throws Exception{
List<File> files = fileService.getEntityList(
Wrappers.<File>lambdaQuery()
.eq(File::getRelObjType, Demo.class.getSimpleName())
.eq(File::getRelObjId, id)
);
return new JsonResult(Status.OK, files);
}
/***
* 上傳文件
* @param file
* @param request
* @return
* @throws Exception
*/
@PostMapping("/upload")
public JsonResult upload(@RequestParam("file") MultipartFile file) throws Exception {
File fileEntity = demoService.uploadFile(file);
return new JsonResult(Status.OK, fileEntity, "上傳文件成功");
}
/***
* 創建成功後的相關處理
* @param entity
* @return
*/
@Override
protected String afterCreated(BaseEntity entity) throws Exception {
DemoDTO demoDTO = (DemoDTO) entity;
// 更新文件關聯信息
demoService.updateFiles(new ArrayList<String>(){{
addAll(demoDTO.getDownloadFiles());
}}, demoDTO.getId(), true);
return null;
}
/***
* 更新成功後的相關處理
* @param entity
* @return
*/
@Override
protected String afterUpdated(BaseEntity entity) throws Exception {
DemoDTO demoDTO = (DemoDTO) entity;
// 更新文件關聯信息
demoService.updateFiles(new ArrayList<String>(){{
addAll(demoDTO.getDownloadFiles());
}}, demoDTO.getId(), false);
return null;
}
- DemoService
@Override
public File uploadFile(MultipartFile file) {
if(V.isEmpty(file)){
throw new BusinessException(Status.FAIL_OPERATION, "請上傳圖片");
}
String fileName = file.getOriginalFilename();
String ext = fileName.substring(fileName.lastIndexOf(".")+1);
String newFileName = S.newUuid() + "." + ext;
//TODO: 需要對合法的文件類型進行驗證
if(FileHelper.isImage(ext)){
throw new BusinessException(Status.FAIL_OPERATION, "請上傳合法的文件類型");
};
// 說明:此處爲我們的處理流程,看官們需要根據自己的需求來對文件進行保存及處理(之後我們的File組件開源之後也可以按照此處的處理)
String filePath = FileHelper.saveFile(file, newFileName);
if(V.isEmpty(filePath)){
throw new BusinessException(Status.FAIL_OPERATION, "圖片上傳失敗");
}
File fileEntity = new File();
fileEntity.setRelObjType(Demo.class.getSimpleName());
fileEntity.setFileType(ext);
fileEntity.setName(fileName);
fileEntity.setPath(filePath);
String link = "/file/download/" + D.getYearMonth() + "_" + newFileName;
fileEntity.setLink(link);
boolean success = fileService.createEntity(fileEntity);
if (!success){
throw new BusinessException(Status.FAIL_OPERATION, "上傳文件失敗");
}
return fileEntity;
}
@Override
public void updateFiles(List<String> uuids, Long currentId, boolean isCreate) {
// 如果不是創建,需要刪除不在列表中的file記錄
if (!isCreate){
fileService.deleteEntities(Wrappers.<File>lambdaQuery().notIn(File::getUuid, uuids));
}
// 進行相關更新
boolean success = fileService.updateEntity(
Wrappers.<File>lambdaUpdate()
.in(File::getUuid, uuids)
.set(File::getRelObjType, Demo.class.getSimpleName())
.set(File::getRelObjId, currentId));
if (!success){
throw new BusinessException(Status.FAIL_OPERATION, "更新文件信息失敗");
}
}
提示
- 該文章所有代碼皆爲方案示例,其中有些許刪減,不確保能直接運行,如果各位有好的想法,歡迎一起探討。