最近,不知道是不是最后一搏,事业群上层领导要求7天内完成ABS项目中保理系统的一期开发。需求评审会后,我发现各个功能模块都有大量的文件材料展示与上传,为了提高开发效率,我与组长商量,我先开发文件模块,为其他同事提供公共方法,减少重复工作。
使用的前端技术栈:Bootstrap、Bootstrap-table、layer.js、layui.js、jquery.media.js
我的想法是:文件的展示依托于表格形式,表格内展示文件的类型、名称、操作等信息,然后通过表格中或表头上的按钮完成上传、下载、预览等操作。其次,我又考虑的文件模块复用性与拓展性,我将文件类型信息存入数据字典中。这样做的好处有两点:一是、方便以后文件类型的拓展,即新增加一种文件,不需要改页面,只需要配置数据字典;二是、方便同事复用文件模块方法,即调用者,只需要传入类型后,就可以渲染相关数据。文件模块和数据字段模块图示如下:
图1 - 文件管理模块图片
图2 - 数据字典模块图片
列表基于Bootstrap-table完成的文件展示,我在这块还做了一个优化:可以根据开发者传入的文件类型展示相应的文件,如果没有,也会展示该类,但是没有文件名称和操作。具体演示和代码如下:
演示:
图3 - 文件列表演示图片
页面代码:
JS代码:考虑到文件的复用性,初始化表格的JS入参有好几个
$(function () {
initFileTable('1','A','','fileTable','1');
});
/**
* 初始化文件表格
*
* @param belongId 所属ID (必传)
* @param type 文件总类型 (必传)
* @param fileTag 文件分类型
* @param tableId 表格id (必传)
* @param delFlag 删除表示 (必传)
*/
function initFileTable(belongId,type,fileTag,tableId,delFlag) {
$("#"+tableId).bootstrapTable({
striped: true, // 设置为true会有隔行变色效果
cache: false,
sortable: false,
url: filePrefix+"/listByBelongIdAndTypeAndFileTag",
method: "get",
dataType: "json", // 服务器返回的数据类型
queryParams: {"belongId": belongId,"type":type,"fileTag":fileTag},
onLoadSuccess: function(data) {
},
columns: [
{
checkbox: true
},
{
class: 'uid',
title: '序号',
align: 'center',
formatter: function (value, row, index) {
return index + 1;
}
},
{
field: "id",
visible: false
},
{
field: "uuid",
visible: false
},
{
title: '上传文件',
align: 'center',
formatter: function (value, row, index) {
return uploadFileBtn(row,tableId);
}
},
{
field: "typeName",
align: 'center',
title: "文件总类型",
visible: false
},
{
field: "fileTagName",
align: 'center',
title: "文件类型"
},
{
field: "fileName",
align: 'center',
title: "文件名"
},
{
title: '操作',
align: 'center',
formatter: function (value, row, index) {
var id = row.id;
if(id==null){
return '-';
}
var p = '预览 ';
var e = '下载 ';
var d = '删除 ';
if(delFlag=='1'){
return p + e + d;
}else {
return p + e;
}
}
}
]
})
}
Java代码:
-----------------Controller------------------
/**
* 文件Demo
*
* @return
*/
@GetMapping()
private String File(){
return "common/file/file";
}
/**
* 查询文件列表
*
* @param belongId
* @param type
* @param fileTag
* @return
*/
@GetMapping("/listByBelongIdAndTypeAndFileTag")
@ResponseBody
public List listByTypeAndFileTag(@RequestParam("belongId") Long belongId,
@RequestParam("type") String type,
@RequestParam("fileTag") String fileTag){
List busFileDOList=new ArrayList<>();
try {
busFileDOList =fileService.listByBelongIdAndTypeAndFileTag(belongId,type,fileTag);
} catch (Exception e) {
log.info("查询文件信息,异常信息:[{}]",e.getMessage());
}
return busFileDOList;
}
-----------------Service------------------
@Override
public List listByBelongIdAndTypeAndFileTag(Long belongId, String type, String fileTag) {
return fileDao.listByBelongIdAndTypeAndFileTag(belongId,type,fileTag);
}
文件上传,我这里提供了三种方式,第一种方式:我们原来项目中常用方法,它是使用了layui.js插件,这插件的作者帮助我们封装了好多前端方法和前端样式,方便我们调用;第二种方式:是我新想出来的一种方式,点击上传按钮,进入一个上传页面,在页面中有一个下拉框可以选择上传文件类型,还有一个上传按钮用来上传文件,然后点击提交;第三种方式:是原型上要求的方式,这次开发时间比较紧张,我在开发的中就没有研究和使用,它是把按钮放在了列表中,不同列代表上传不同的文件类型,用户体验感比较好。而且,我这里还做一个优化,如果上传的文件在列表中不存在文件,就在原位置插入,反之,则在这个类型文件的最后一行后边插入(因为要实现这个,我引入一个唯一标识UUID)。具体演示和代码如下:
方式一:
点击上传按钮,上传文件就可以,缺点就是要在按钮的方法中固定上传的类型,单类型可以使用。
图4 - 文件上传方式一演示图片
$(function () {
fileUpload('A','A01','fileTable');
});
/**
* 上传
*
* @param type
* @param fileTag
* @param tableId
*/
function fileUpload(type,fileTag,tableId) {
layui.use('upload', function () {
var upload = layui.upload;
//执行实例
var uploadInst = upload.render({
elem: '#uploadFile', //绑定元素
url: filePrefix+'/uploadFile1/'+type+'/'+fileTag, //上传接口
size: 102400,
accept: 'file',
done: function (data) {
if (data.code == 0) {
var busFile = data.busFile;
//插入一条数据
insertFile(busFile,tableId);
}
layer.msg(data.msg);
},
error: function (data) {
layer.msg(data.msg);
}
});
});
}
/**
* 插入文件
*
* @param row
* @param tableId
*/
function insertFile(row,tableId) {
var table = $("#"+tableId);
var rows = table.bootstrapTable('getData');
var type = row.type;
var fileTag = row.fileTag;
var uuid=null;
var insertIndex = rows.length-1;
//寻找插入文件文件位置和可能需要删除的行
$.each(rows, function (i, oldRow) {
if(type==oldRow.type && fileTag==oldRow.fileTag){
if(null == oldRow.id){
uuid=oldRow.uuid;
insertIndex=i;
}else {
insertIndex=i+1;
}
}
});
// 删除表格空的数据
if(uuid!=null){
var values = [uuid];
table.bootstrapTable('remove', {
field: 'uuid',
values: values
});
}
// 表格插入一条
table.bootstrapTable('insertRow', {
index: insertIndex,
row: row
});
}
-----------------Controller------------------
/**
* 上传文件1
*
* @param file
* @param type
* @param fileTag
* @return
*/
@PostMapping("/uploadFile1/{type}/{fileTag}")
@ResponseBody
public R uploadFile1(@RequestParam("file") MultipartFile file, @PathVariable("type") String type, @PathVariable("fileTag") String fileTag) {
log.info("上传文件开始,type:[{}],fileTag:[{}]",type,fileTag);
if (file == null || StringUtil.isBlank(file.getOriginalFilename())) {
log.info("未选择文件");
return R.error("请选择需要上传的文件");
}
Map map = new HashMap<>(2);
try {
FileDTO busFile = fileService.uploadFile(file, type, fileTag);
map.put("busFile", busFile);
log.info("上传文件结束,type:[{}],fileTag:[{}],id:[{}],fileName:[{}]",type,fileTag,busFile.getId(),busFile.getFileFull());
} catch (IOException e) {
log.info("上传文件IO流异常:异常信息:[{}]",e.getMessage());
return R.error("上传文件IO流异常:异常信息:"+e.getMessage());
}catch (Exception e){
log.info("上传文件异常:异常信息:[{}]",e.getMessage());
return R.error("上传文件异常:异常信息:"+e.getMessage());
}
return R.ok(map);
}
-----------------Service------------------
@Override
public FileDTO uploadFile(MultipartFile file, String type, String fileTag) throws IOException {
FileDTO busFile = new FileDTO();
Date now = new Date();
String fileName = file.getOriginalFilename();
String preName = fileName.substring(0, fileName.lastIndexOf("."));
String extName = fileName.substring(fileName.lastIndexOf(".") + 1);
// 上传文件到Fast
// StorePath storePath = storageClient.uploadFile(file.getInputStream(), file.getSize(), FilenameUtils.getExtension(file.getOriginalFilename()), null);
// String groupPath = storePath.getFullPath();
// String serverPath = fdfsConfig.getUrl();
String serverPath = "https://img-blog.csdnimg.cn";
String groupPath = "/20200404131040657.png";
String fileUrl=null;
// 处理fileUrl
if (serverPath.endsWith("/")) {
fileUrl = serverPath + groupPath;
} else {
fileUrl = serverPath + "/" + groupPath;
}
//封装数据
busFile.setFileName(preName)
.setFileExt(extName)
.setFileFull(fileName)
.setUrl(fileUrl)
.setType(type)
.setServerPath(serverPath)
.setGroupPath(groupPath)
.setFileTag(fileTag)
.setCreateTime(now)
.setUpdateTime(now)
.setDelFlag(0);
fileDao.save(busFile);
//处理数据
DictDO dict = dictDao.getByTypeAndValue(type, fileTag);
busFile.setTypeName(dict.getDescription());
busFile.setFileTagName(dict.getName());
busFile.setUuid(UUID.randomUUID().toString());
return busFile;
}
方式二:
点击上传按钮,进入上传页面:选择类型、上传文件、点击提交。
图5-文件上传方式二演示图片
图6 - 文件上传方式二演示图片
---------------------------按钮----------------------
---------------------------上传页面----------------------
---------------------------按钮JS----------------------
/**
* 上传文件页面
*
* @param type
* @param tableId
*/
function uploadFilePage(type,tableId) {
var index = layer.open({
type: 2,
title: '上传文件',
maxmin: true,
shadeClose: false, // 点击遮罩关闭层
area: ['800px', '520px'],
content: filePrefix+'/uploadFilePage/' + type+'/'+tableId
});
layer.full(index);
}
---------------------------页面JS----------------------
var filePrefix = "/common/file";
$(function () {
});
$("#excelImport").click(function() {
$('#fileupload').click();
});
$('#fileupload').change(function(){
$('#textfield').val( document.getElementById("fileupload").files[0].name);
})
function uploadFile() {
var fileBtnId='signupForm';
var type=$('#type').val();
var fileTag= $('#fileTag option:selected').val();
var tableId=$('#tableId').val();
$.ajax({
type: "POST",
dataType: "json",
cache: false,
processData: false,
contentType: false,
url: filePrefix+"/uploadFile1/"+type+"/"+fileTag,
data: new FormData($('#'+fileBtnId)[0]),
success: function (data) {
if (data.code == 0) {
parent.layer.msg("操作成功");
parent.insertFile(data.busFile,tableId);
var index = parent.layer.getFrameIndex(window.name); // 获取窗口索引
parent.layer.close(index);
} else {
parent.layer.alert(data.msg)
}
}
});
}
-----------------Controller------------------
/**
* 上传文件页面
*
* @param type
* @param model
* @return
*/
@GetMapping("/uploadFilePage/{type}/{tableId}")
public String uploadFilePage(@PathVariable("type") String type,@PathVariable("tableId") String tableId, Model model) {
List busDictList = dictDao.listByType(type);
model.addAttribute("type",type);
model.addAttribute("tableId",tableId);
model.addAttribute("busDictList",busDictList);
return "common/file/uploadFile";
}
/**
* 上传文件2
*
* @param file
* @param type
* @param fileTag
* @return
*/
@PostMapping("/uploadFile2")
@ResponseBody
public R uploadFile2(@RequestParam("file") MultipartFile file,@RequestParam("type") String type, @RequestParam("fileTag") String fileTag) {
log.info("上传文件开始,type:[{}],fileTag:[{}]",type,fileTag);
if (file == null || StringUtil.isBlank(file.getOriginalFilename())) {
log.info("未选择文件");
return R.error("请选择需要上传的文件");
}
Map map = new HashMap<>(2);
try {
FileDTO busFile = fileService.uploadFile(file, type, fileTag);
map.put("busFile", busFile);
log.info("上传文件结束,type:[{}],fileTag:[{}],id:[{}],fileName:[{}]",type,fileTag,busFile.getId(),busFile.getFileFull());
} catch (IOException e) {
log.info("上传文件IO流异常:异常信息:[{}]",e.getMessage());
return R.error("上传文件IO流异常:异常信息:"+e.getMessage());
}catch (Exception e){
log.info("上传文件异常:异常信息:[{}]",e.getMessage());
return R.error("上传文件异常:异常信息:"+e.getMessage());
}
return R.ok(map);
}
-----------------Service------------------
与方式一相同
方式三:
点击上传按钮,就可以上传文件。
图7 - 文件上传方式三演示图片
没有页面代码,页面是通过js渲染的。
/**
* 上传文件按钮
*
* @param row
* @param tableId
* @returns {string}
*/
function uploadFileBtn(row,tableId) {
var formId=row.uuid;
return '';
}
/**
* 上传文件按钮-上传方法
*
* @param formId
* @param tableId
*/
function uploadFile(formId,tableId) {
$.ajax({
type: "POST",
dataType: "json",
cache: false,
processData: false,
contentType: false,
url: filePrefix+"/uploadFile2",
data: new FormData($('#'+formId)[0]),
success: function (data) {
if (data.code == 0) {
layer.msg("上传成功");
insertFile(data.busFile,tableId);
} else {
layer.alert(data.msg)
}
}
});
}
-----------------Controller------------------
/**
* 上传文件2
*
* @param file
* @param type
* @param fileTag
* @return
*/
@PostMapping("/uploadFile2")
@ResponseBody
public R uploadFile2(@RequestParam("file") MultipartFile file,@RequestParam("type") String type, @RequestParam("fileTag") String fileTag) {
log.info("上传文件开始,type:[{}],fileTag:[{}]",type,fileTag);
if (file == null || StringUtil.isBlank(file.getOriginalFilename())) {
log.info("未选择文件");
return R.error("请选择需要上传的文件");
}
Map map = new HashMap<>(2);
try {
FileDTO busFile = fileService.uploadFile(file, type, fileTag);
map.put("busFile", busFile);
log.info("上传文件结束,type:[{}],fileTag:[{}],id:[{}],fileName:[{}]",type,fileTag,busFile.getId(),busFile.getFileFull());
} catch (IOException e) {
log.info("上传文件IO流异常:异常信息:[{}]",e.getMessage());
return R.error("上传文件IO流异常:异常信息:"+e.getMessage());
}catch (Exception e){
log.info("上传文件异常:异常信息:[{}]",e.getMessage());
return R.error("上传文件异常:异常信息:"+e.getMessage());
}
return R.ok(map);
}
-----------------Service------------------
与方式一相同
通过预览按钮进入预览页面,现在支持预览文件格式只有图片和PDF。具体演示和代码如下:
演示:
图8 - 文件预览演示图片
页面代码:
JS代码:
/**
* 预览文件
*
* @param url
*/
function previewFile(url) {
//获取最后一个.的位置
var index= url.lastIndexOf(".");
//获取后缀
var ext = url.substr(index+1);
if(ext=='docx'||ext=='doc'||ext=='txt'||
ext=='zip'||ext=='xlsx'||ext=='xls'||
ext=='ppt'||ext=='pptx'){
layer.msg("不支持此"+ext+"格式文件预览");
return;
}
var index = layer.open({
type: 2,
title: '预览文件',
maxmin: true,
shadeClose: false, // 点击遮罩关闭层
area: ['800px', '520px'],
content: filePrefix+'/previewFile?url=' + url
});
layer.full(index);
}
Java代码:
/**
* 预览文件
*
* @param url
* @param model
* @return
*/
@GetMapping("/previewFile")
public String previewFile(@RequestParam("url") String url, Model model) {
model.addAttribute("url",url);
return "common/file/previewFile";
}
点击下载按钮,就可下载。具体代码如下:
页面代码:
没有页面代码,JS渲染了一个按钮。
JS代码:
/**
* 下载文件
*
* @param id
*/
function downloadFile(id) {
window.location.href = filePrefix+"/downloadFile/" + id;
}
Java代码:
/**
* 下载
*
* @param id
* @return
* @throws Exception
*/
@GetMapping("/downloadFile/{id}")
public ResponseEntity downloadFile(@PathVariable("id") Long id){
FileDO file = fileDao.get(id);
log.info("下载文件开始,文件id:[{}],文件名称:[{}]",file.getId(),file.getFileFull());
byte[] content = ZipDownloadUtil.downloadUrlConvertByte(file.getUrl());
HttpHeaders headers = new HttpHeaders();
try {
headers.set("Content-Disposition", "attachment;Filename=" + URLEncoder.encode(file.getFileFull(), "UTF-8"));
} catch (UnsupportedEncodingException e) {
log.info("文件名编码集转换异常,异常信息[{}]",e.getMessage());
}
HttpStatus statusCode = HttpStatus.OK;
ResponseEntity entity = new ResponseEntity<>(content, headers, statusCode);
return entity;
}
点击删除按钮,弹出删除却框,点击确定后删除,我这里是逻辑删除,只更新数据删除标识,不删除数据库记录和文件服务器中的文件。具体演示和代码如下:
演示:
图9 - 文件删除演示图片
页面代码:
没有页面代码,JS渲染了一个按钮。
JS代码:
/**
* 删除文件
*
* @param id
* @param tableId
*/
function deleteFile(id,tableId) {
layer.confirm("确认要删除此文件吗?",
{btn: ['确定', '取消']}, function () {
$.ajax({
cache: true,
type: "get",
url: filePrefix+"/deleteFile/" + id,
async: false,
error: function (request) {
parent.layer.alert("Connection error");
},
success: function (data) {
if (data.code == 0) {
var table = $("#"+tableId);
var values = [parseInt(id)];
table.bootstrapTable('remove', {
field: 'id',
values: values
});
layer.closeAll('dialog');
parent.layer.msg("文件删除成功");
} else {
parent.layer.alert(data.msg)
}
}
});
})
}
Java代码:
/**
* 删除文件
*
* @param id
* @return
*/
@ResponseBody
@GetMapping("/deleteFile/{id}")
public R deleteFile(@PathVariable("id") Long id) {
try {
fileDao.updateDelFlagById(id);
} catch (Exception e) {
log.info("删除文件信息,异常信息:[{}]",e.getMessage());
return R.error("异常信息:"+e.getMessage());
}
return R.ok();
}
我这里提供了两种打包下载方法方式,第一种方式:根据文件类型和所属ID打成一个压缩包,第二种方式:根据所属id的集合批量下载,并且压缩包中会按照所属ID和文件类型创建不同目录下载(这块搞了好久,我对Java IO流不是很熟悉;JDK8 Stream 分组的功能帮了我很大忙,JDK8 Stream的使用,见我写的这篇文章《JDK8都发行5年多了,你还不会使用Stream流和Lambda表达式吗?》)。
方式一:
点击一键打包下载按钮,就可以下文件,这种方式需要传入文件类型和所属ID,具体代码如下。
/**
* 打包下载1
*
* @param belongId
* @param type
* @param fileTag
*/
function downloadZip1(belongId,type,fileTag) {
window.location.href = filePrefix+"/downloadZip1?belongId=" + belongId+"&type="+type+"&fileTag"+fileTag;
}
-----------------Controller------------------
/**
* 打包下载1
*
* @param belongId
* @param type
* @param fileTag
*/
@GetMapping("/downloadZip1")
@ResponseBody
public void downloadZip1(@RequestParam("belongId") Long belongId,
@RequestParam("type") String type,
@RequestParam("fileTag") String fileTag,
HttpServletResponse response) {
log.info("打包下载文件开始,文件所属id:[{}],文件总类型:[{}],文件类型:[{}]",belongId,type,fileTag);
try {
fileService.downloadZip(belongId,type,fileTag,"压缩包",response);
log.info("打包下载文件结束,文件所属id:[{}],文件总类型:[{}],文件类型:[{}]",belongId,type,fileTag);
} catch (Exception e) {
log.error("打包下载文件异常,异常信息:[{}]", e.getMessage());
}
}
-----------------Service------------------
@Override
public void downloadZip(Long belongId, String type, String fileTag, String zipFilename, HttpServletResponse response) throws IOException {
Map map = new HashMap<>(4);
map.put("belongId", belongId);
map.put("type", type);
map.put("fileTag", fileTag);
List list = fileDao.list(map);
List contentList = new ArrayList<>();
List filenameList = new ArrayList<>();
String typeName = "";
if (!list.isEmpty()) {
typeName = list.get(0).getTypeName();
list.forEach(x -> {
log.info("文件名称和url,name:[{}],url:[{}] ", x.getFileName(), x.getUrl());
if (StringUtil.isNotBlank(x.getUrl())) {
contentList.add(ZipDownloadUtil.downloadUrlConvertByte(x.getUrl()));
filenameList.add(getFilename(x));
}
});
}
ZipDownloadUtil.downloadZip(response, zipFilename, contentList, filenameList);
}
方式二:
点击一键打包下载按钮,就可以下文件,这种方式需要所属ID类型和压缩包名称,具体代码如下。
/**
* 打包下载2
*/
function downloadZip2() {
var params={};
params.belongIds=[1,2];
params.zipFilename='测试';
var param = jQuery.param(params);
window.location.href = filePrefix+"/downloadZip2?" + param;
}
-----------------Controller------------------
/**
*
* 打包下载2
*
* @param belongIds
* @param zipFilename
*/
@GetMapping("/downloadZip2")
@ResponseBody
public void downloadZip2(@RequestParam("belongIds[]") List belongIds,
@RequestParam("zipFilename") String zipFilename,
HttpServletResponse response) {
log.info("打包下载文件开始,文件所属id集合:[{}]", JsonUtil.beanToJson(belongIds));
try {
fileService.batchDownloadZip(belongIds,zipFilename,response);
log.info("打包下载文件结束,文件所属id集合:[{}],压缩文件名称:[{}]", JsonUtil.beanToJson(belongIds),zipFilename);
} catch (Exception e) {
log.error("打包下载文件异常,异常信息:[{}]", e.getMessage());
}
}
-----------------Service------------------
@Override
public void batchDownloadZip(List belongIdList, String zipFilename, HttpServletResponse response) throws IOException {
//数据为空
if (belongIdList.isEmpty()) {
return;
}
List contentList = new ArrayList<>();
List filenameList = new ArrayList<>();
//遍历每一个资产包
belongIdList.forEach(belongId->{
Map param = new HashMap<>(2);
param.put("belongId", belongIdList.get(0));
List list = fileDao.list(param);
//按文件类型分组
Map> map = list.stream().collect(Collectors.groupingBy(FileDTO::getTypeName));
String partlPackageNo = "";
//遍历分组
map.forEach((key, value)->{
String typeName=key;
value.forEach(x->{
log.info("文件名称和url,name:[{}],url:[{}] ", x.getUrl(), x.getFileName());
contentList.add(ZipDownloadUtil.downloadUrlConvertByte(x.getUrl()));
filenameList.add(partlPackageNo+"/"+typeName+"/"+getFilename(x));
});
});
});
//封装压缩包名字
zipFilename=zipFilename+"_"+ LocalDateTime.now().toString()+".zip";
ZipDownloadUtil.downloadZip(response, zipFilename, contentList, filenameList);
}
https://gitee.com/hanxinghua2017/springboot_demo.git 《 springboot-some-function模块》