diff --git a/doc/docs/.vuepress/config.js b/doc/docs/.vuepress/config.js index 8e2fdcd0..211130b0 100644 --- a/doc/docs/.vuepress/config.js +++ b/doc/docs/.vuepress/config.js @@ -51,6 +51,7 @@ module.exports = { {title: '数据源', path: '/guide/datasource'}, {title: '数据集', path: '/guide/dataset'}, {title: '大屏设计', path: '/guide/dashboard'}, + {title: '导入导出', path: '/guide/importexport'}, ] }, { diff --git a/doc/docs/guide/dashboard.md b/doc/docs/guide/dashboard.md index 802cb0a0..fe54e683 100644 --- a/doc/docs/guide/dashboard.md +++ b/doc/docs/guide/dashboard.md @@ -88,7 +88,9 @@ **如有问题,请提交 [Issue](https://gitee.com/anji-plus/report/issues)
** ### 散点图 -**开发中**
+**规划中**
### 对比图 -**开发中**
+柱状对比图数据集需要3个字段,其中一个作为对比的字段只能为2种值,只有2种值作为对比的字段要选择“y轴字段”字典。因为底层的解析用的是堆叠图的解析,这里的y轴字段并不是指的图表上面的y轴,还请注意,有强迫症可以自行修改源码的解析,剩下的2个字段对应字典看图
+![img18](../picture/dashboard/img_18.png) +**如有问题,请提交 [Issue](https://gitee.com/anji-plus/report/issues)
** diff --git a/doc/docs/guide/importexport.md b/doc/docs/guide/importexport.md new file mode 100644 index 00000000..787dd62a --- /dev/null +++ b/doc/docs/guide/importexport.md @@ -0,0 +1,17 @@ +**注:导入导出目前是初始版本,报错没有细化,如果导入导出过程中页面无反应,请F12** +**注:“导入成功/失败”的提示不一定对应当前真实导入导出情况,请根据实际导入导出的结果进行判断** + +## 导出 +![img](../picture/imexport/img.png)
+导出会生成zip文件,包含图表、样式、图片等,不会带有该大屏的名称和code。
+ +### 导出数据集 +适用于同一系统内部使用 + +### 导出不含有数据集 +导出的图表会使用默认的静态数据集,适用于跨系统,请注意,如果你的大屏图表有部分图表是在对方系统不存在的,那么目前整个大屏是不会显示出来的,后续会进行兼容,不存在的图表留空。
+ +## 导入 +![img1](../picture/imexport/img_1.png)
+选择一个导出的zip文件导入即可。注意,导入会覆盖当前大屏,请新建一张空白的大屏进行导入。
+**注:如果你导入的大屏中含有你当前系统不存在的图表,整个大屏是不会显示的。**
diff --git a/doc/docs/picture/dashboard/img_18.png b/doc/docs/picture/dashboard/img_18.png new file mode 100644 index 00000000..1e2090a9 Binary files /dev/null and b/doc/docs/picture/dashboard/img_18.png differ diff --git a/doc/docs/picture/imexport/img.png b/doc/docs/picture/imexport/img.png new file mode 100644 index 00000000..b1f435f2 Binary files /dev/null and b/doc/docs/picture/imexport/img.png differ diff --git a/doc/docs/picture/imexport/img_1.png b/doc/docs/picture/imexport/img_1.png new file mode 100644 index 00000000..dff948d4 Binary files /dev/null and b/doc/docs/picture/imexport/img_1.png differ diff --git a/report-core/src/main/java/com/anjiplus/template/gaea/business/modules/dashboard/controller/ReportDashboardController.java b/report-core/src/main/java/com/anjiplus/template/gaea/business/modules/dashboard/controller/ReportDashboardController.java index 15e376b2..d34dbf62 100644 --- a/report-core/src/main/java/com/anjiplus/template/gaea/business/modules/dashboard/controller/ReportDashboardController.java +++ b/report-core/src/main/java/com/anjiplus/template/gaea/business/modules/dashboard/controller/ReportDashboardController.java @@ -9,7 +9,12 @@ import com.anjiplus.template.gaea.business.modules.dashboard.controller.dto.Char import com.anjiplus.template.gaea.business.modules.dashboard.controller.dto.ReportDashboardObjectDto; import io.swagger.annotations.Api; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; /** * @desc 大屏设计 controller @@ -62,4 +67,30 @@ public class ReportDashboardController { return ResponseBean.builder().data(reportDashboardService.getChartData(dto)).build(); } + + /** + * 导出大屏 + * @param reportCode + * @return + */ + @GetMapping("/export") + @Permission(code = "view", name = "导出大屏") + public ResponseEntity exportDashboard(HttpServletRequest request, HttpServletResponse response, + @RequestParam("reportCode") String reportCode, @RequestParam(value = "showDataSet",required = false, defaultValue = "1") Integer showDataSet) { + return reportDashboardService.exportDashboard(request, response, reportCode, showDataSet); + } + + /** + * 导入大屏 + * @param file 导入的zip文件 + * @param reportCode + * @return + */ + @PostMapping("/import/{reportCode}") + @Permission(code = "design", name = "导入大屏") + public ResponseBean importDashboard(@RequestParam("file") MultipartFile file, @PathVariable("reportCode") String reportCode) { + reportDashboardService.importDashboard(file, reportCode); + return ResponseBean.builder().build(); + } + } diff --git a/report-core/src/main/java/com/anjiplus/template/gaea/business/modules/dashboard/service/ReportDashboardService.java b/report-core/src/main/java/com/anjiplus/template/gaea/business/modules/dashboard/service/ReportDashboardService.java index 508bfeed..2b8c1e35 100644 --- a/report-core/src/main/java/com/anjiplus/template/gaea/business/modules/dashboard/service/ReportDashboardService.java +++ b/report-core/src/main/java/com/anjiplus/template/gaea/business/modules/dashboard/service/ReportDashboardService.java @@ -6,6 +6,11 @@ import com.anjiplus.template.gaea.business.modules.dashboard.controller.dto.Char import com.anjiplus.template.gaea.business.modules.dashboard.controller.dto.ReportDashboardObjectDto; import com.anjiplus.template.gaea.business.modules.dashboard.controller.param.ReportDashboardParam; import com.anjiplus.template.gaea.business.modules.dashboard.dao.entity.ReportDashboard; +import org.springframework.http.ResponseEntity; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; /** * @desc ReportDashboard 大屏设计服务接口 @@ -35,4 +40,22 @@ public interface ReportDashboardService extends GaeaBaseService exportDashboard(HttpServletRequest request, HttpServletResponse response, String reportCode, Integer showDataSet); + + /** + * 导入大屏zip + * @param file + * @param reportCode + * @return + */ + void importDashboard(MultipartFile file, String reportCode); } diff --git a/report-core/src/main/java/com/anjiplus/template/gaea/business/modules/dashboard/service/impl/ReportDashboardServiceImpl.java b/report-core/src/main/java/com/anjiplus/template/gaea/business/modules/dashboard/service/impl/ReportDashboardServiceImpl.java index 3a8cdab9..4f207d94 100644 --- a/report-core/src/main/java/com/anjiplus/template/gaea/business/modules/dashboard/service/impl/ReportDashboardServiceImpl.java +++ b/report-core/src/main/java/com/anjiplus/template/gaea/business/modules/dashboard/service/impl/ReportDashboardServiceImpl.java @@ -13,6 +13,9 @@ import com.anjiplus.template.gaea.business.modules.dashboard.controller.dto.Repo import com.anjiplus.template.gaea.business.modules.dashboard.dao.ReportDashboardMapper; import com.anjiplus.template.gaea.business.modules.dashboard.service.ChartStrategy; import com.anjiplus.template.gaea.business.modules.dashboard.service.ReportDashboardService; +import com.anjiplus.template.gaea.business.modules.file.entity.GaeaFile; +import com.anjiplus.template.gaea.business.modules.file.service.GaeaFileService; +import com.anjiplus.template.gaea.business.modules.file.util.FileUtils; import com.anjiplus.template.gaea.business.util.DateUtil; import com.anjiplus.template.gaea.business.modules.dashboardwidget.controller.dto.ReportDashboardWidgetDto; import com.anjiplus.template.gaea.business.modules.dashboardwidget.controller.dto.ReportDashboardWidgetValueDto; @@ -22,17 +25,30 @@ import com.anjiplus.template.gaea.business.modules.dashboardwidget.service.Repor import com.anjiplus.template.gaea.business.modules.dataset.controller.dto.DataSetDto; import com.anjiplus.template.gaea.business.modules.dataset.controller.dto.OriginalDataDto; import com.anjiplus.template.gaea.business.modules.dataset.service.DataSetService; +import com.anjiplus.template.gaea.business.util.FileUtil; +import com.anjiplus.template.gaea.business.util.UuidUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.BeanUtils; import org.springframework.beans.BeansException; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.net.URLEncoder; import java.text.SimpleDateFormat; import java.util.*; @@ -42,6 +58,7 @@ import java.util.*; * @date 2021-04-12 14:52:21.761 **/ @Service +@Slf4j //@RequiredArgsConstructor public class ReportDashboardServiceImpl implements ReportDashboardService, InitializingBean, ApplicationContextAware { @@ -54,6 +71,18 @@ public class ReportDashboardServiceImpl implements ReportDashboardService, Initi @Autowired private DataSetService dataSetService; + @Autowired + private GaeaFileService gaeaFileService; + + @Value("${customer.file.downloadPath:''}") + private String fileDownloadPath; + + @Value("${customer.file.dist-path:''}") + private String dictPath; + + private final static String ZIP_PATH = "/zip/"; + private final static String JSON_PATH = "dashboard.json"; + private Map queryServiceImplMap = new HashMap<>(); private ApplicationContext applicationContext; @@ -105,7 +134,7 @@ public class ReportDashboardServiceImpl implements ReportDashboardService, Initi * @param dto */ @Override - @Transactional + @Transactional(rollbackFor = Exception.class) public void insertDashboard(ReportDashboardObjectDto dto) { String reportCode = dto.getReportCode(); GaeaAssert.notEmpty(reportCode, ResponseCode.PARAM_IS_NULL, "reportCode"); @@ -121,6 +150,7 @@ public class ReportDashboardServiceImpl implements ReportDashboardService, Initi } else { //更新 dashboard.setId(reportDashboard.getId()); + dashboard.setVersion(null); this.update(dashboard); } @@ -165,6 +195,194 @@ public class ReportDashboardServiceImpl implements ReportDashboardService, Initi // return getTarget(chartType).transform(dto, result.getData()); } + /** + * 导出大屏,zip文件 + * + * @param request + * @param response + * @param reportCode + * @return + */ + @Override + public ResponseEntity exportDashboard(HttpServletRequest request, HttpServletResponse response, String reportCode, Integer showDataSet) { + String userAgent = request.getHeader("User-Agent"); + boolean isIeBrowser = userAgent.indexOf("MSIE") > 0; + + ReportDashboardObjectDto detail = getDetail(reportCode); + List widgets = detail.getDashboard().getWidgets(); + detail.setWidgets(widgets); + detail.getDashboard().setWidgets(null); + + + //1.组装临时目录,/app/disk/upload/zip/临时文件夹 + String path = dictPath + ZIP_PATH + UuidUtil.generateShortUuid(); + + //将涉及到的图片保存下来(1.背景图,2.组件为图片的) + String backgroundImage = detail.getDashboard().getBackgroundImage(); + zipLoadImage(backgroundImage, path); + detail.getWidgets().stream() + .filter(reportDashboardWidgetDto -> "widget-image".equals(reportDashboardWidgetDto.getType())) + .forEach(reportDashboardWidgetDto -> { + String imageAddress = reportDashboardWidgetDto.getValue().getSetup().getString("imageAdress"); + zipLoadImage(imageAddress, path); + }); + + //showDataSet == 0 代表不包含数据集 + if (0 == showDataSet) { + detail.getWidgets().forEach(reportDashboardWidgetDto -> { + ReportDashboardWidgetValueDto value = reportDashboardWidgetDto.getValue(); + JSONObject data = value.getData(); + if (null != data && data.containsKey("dataType")) { + reportDashboardWidgetDto.getValue().getData().put("dataType", "staticData"); + } + }); + } + + + //2.将大屏设计到的json文件保存 + String jsonPath = path + "/" + JSON_PATH; + FileUtil.WriteStringToFile(jsonPath, JSONObject.toJSONString(detail)); + + + //将path文件夹打包zip + String zipPath = path + ".zip"; + FileUtil.compress(path, zipPath); + + + File file = new File(zipPath); + ResponseEntity.BodyBuilder builder = ResponseEntity.ok(); + builder.contentLength(file.length()); + //application/octet-stream 二进制数据流(最常见的文件下载) + builder.contentType(MediaType.APPLICATION_OCTET_STREAM); + if (isIeBrowser) { + builder.header("Content-Disposition", "attachment; filename=" + reportCode + ".zip"); + } else { + builder.header("Content-Disposition", "attacher; filename*=UTF-8''" + reportCode + ".zip"); + } + + ResponseEntity body = builder.body(FileUtils.readFileToByteArray(file)); + + //删除zip文件 + file.delete(); + //删除path临时文件夹 + FileUtil.delete(path); + log.info("删除临时文件:{},{}", zipPath, path); + + return body; + } + + /** + * 导入大屏zip + * + * @param file + * @param reportCode + * @return + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void importDashboard(MultipartFile file, String reportCode) { + log.info("导入开始,{}", reportCode); + //1.组装临时目录,/app/disk/upload/zip/临时文件夹 + String path = dictPath + ZIP_PATH + UuidUtil.generateShortUuid(); + //2.解压 + FileUtil.decompress(file, path); + // path/uuid/ + File parentPath = new File(path); + //获取打包的第一层目录 + File firstFile = parentPath.listFiles()[0]; + + File[] files = firstFile.listFiles(); + + //定义map + Map fileMap = new HashMap<>(); + String content = ""; + + for (int i = 0; i < files.length; i++) { + File childFile = files[i]; + if (JSON_PATH.equals(childFile.getName())) { + //json文件 + content = FileUtil.readFile(childFile); + } else if ("image".equals(childFile.getName())) { + File[] imageFiles = childFile.listFiles(); + //所有需要上传的图片 + for (File imageFile : imageFiles) { + //查看是否存在此image + String fileName = imageFile.getName().split("\\.")[0]; + //根据fileId,从gaea_file中读出filePath + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(GaeaFile::getFileId, fileName); + GaeaFile gaeaFile = gaeaFileService.selectOne(queryWrapper); + if (null == gaeaFile) { + GaeaFile upload = gaeaFileService.upload(imageFile, fileName); + log.info("存入图片: {}", upload.getFilePath()); + fileMap.put(fileName, upload.getUrlPath()); + } + } + } + + } + + + //解析cotent + ReportDashboardObjectDto detail = JSONObject.parseObject(content, ReportDashboardObjectDto.class); + //将涉及到的图片路径替换(1.背景图,2.组件为图片的) + String backgroundImage = detail.getDashboard().getBackgroundImage(); + detail.getDashboard().setBackgroundImage(replaceUrl(backgroundImage, fileMap)); + detail.getWidgets().stream() + .filter(reportDashboardWidgetDto -> "widget-image".equals(reportDashboardWidgetDto.getType())) + .forEach(reportDashboardWidgetDto -> { + String imageAddress = reportDashboardWidgetDto.getValue().getSetup().getString("imageAdress"); + String address = replaceUrl(imageAddress, fileMap); + reportDashboardWidgetDto.getValue().getSetup().put("imageAdress", address); + reportDashboardWidgetDto.getOptions().getJSONArray("setup").getJSONObject(4).put("value", address); + }); + //将新的大屏编码赋值 + detail.setReportCode(reportCode); + + //解析结束,删除临时文件夹 + FileUtil.delete(path); + + log.info("解析成功,开始存入数据库..."); + insertDashboard(detail); + } + + + private String replaceUrl(String imageAddress, Map fileMap) { + String fileId = imageAddress.substring(imageAddress.trim().length() - 36); + String orDefault = fileMap.getOrDefault(fileId, null); + if (StringUtils.isBlank(orDefault)) { + return imageAddress; + } + return orDefault; + } + + /** + * 将大屏涉及到的图片存入指定文件夹 + * @param imageAddress + * @param path + */ + private void zipLoadImage(String imageAddress, String path) { + //http://10.108.26.197:9095/file/download/1d9bcd35-82a1-4f08-9465-b66b930b6a8d + if (imageAddress.trim().startsWith(fileDownloadPath)) { + //以fileDownloadPath为前缀的代表为上传的图片 + String fileName = imageAddress.substring(fileDownloadPath.length() + 1); + //根据fileId,从gaea_file中读出filePath + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(GaeaFile::getFileId, fileName); + GaeaFile gaeaFile = gaeaFileService.selectOne(queryWrapper); + if (null != gaeaFile) { + String fileType = gaeaFile.getFileType(); + path = path + "/image/" + fileName + "." + fileType; + //path = /app/disk/upload/zip/UUID/image + + //原始文件的路径 + String filePath = gaeaFile.getFilePath(); + FileUtil.copyFileUsingFileChannels(filePath, path); + } + } + + } + public ChartStrategy getTarget(String type) { for (String s : queryServiceImplMap.keySet()) { if (s.contains(type)) { diff --git a/report-core/src/main/java/com/anjiplus/template/gaea/business/modules/file/service/GaeaFileService.java b/report-core/src/main/java/com/anjiplus/template/gaea/business/modules/file/service/GaeaFileService.java index d7321404..4082ca7e 100644 --- a/report-core/src/main/java/com/anjiplus/template/gaea/business/modules/file/service/GaeaFileService.java +++ b/report-core/src/main/java/com/anjiplus/template/gaea/business/modules/file/service/GaeaFileService.java @@ -8,6 +8,7 @@ import org.springframework.web.multipart.MultipartFile; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.File; /** * (GaeaFile)Service @@ -17,15 +18,33 @@ import javax.servlet.http.HttpServletResponse; */ public interface GaeaFileService extends GaeaBaseService { + /** + * 文件上传 + * + * @param multipartFile 文件 + * @param file 文件 + * @param customFileName 自定义文件名,默认给null + * @return + */ + GaeaFile upload(MultipartFile multipartFile, File file, String customFileName); /** * 文件上传 * - * @param file - * @return 文件访问路径 + * @param multipartFile 文件 + * @return */ - GaeaFile upload(MultipartFile file); + GaeaFile upload(MultipartFile multipartFile); + + /** + * 文件上传 + * + * @param file 二选一 + * @param customFileName 自定义文件名 + * @return + */ + GaeaFile upload(File file, String customFileName); /** * 根据fileId显示图片或者下载文件 * diff --git a/report-core/src/main/java/com/anjiplus/template/gaea/business/modules/file/service/impl/GaeaFileServiceImpl.java b/report-core/src/main/java/com/anjiplus/template/gaea/business/modules/file/service/impl/GaeaFileServiceImpl.java index 9771a70b..7bfd7d1d 100644 --- a/report-core/src/main/java/com/anjiplus/template/gaea/business/modules/file/service/impl/GaeaFileServiceImpl.java +++ b/report-core/src/main/java/com/anjiplus/template/gaea/business/modules/file/service/impl/GaeaFileServiceImpl.java @@ -10,6 +10,7 @@ import com.anjiplus.template.gaea.business.modules.file.entity.GaeaFile; import com.anjiplus.template.gaea.business.modules.file.service.GaeaFileService; import com.anjiplus.template.gaea.business.modules.file.util.FileUtils; import com.anjiplus.template.gaea.business.modules.file.util.StringPatternUtil; +import com.anjiplus.template.gaea.business.util.FileUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import lombok.extern.slf4j.Slf4j; @@ -64,11 +65,18 @@ public class GaeaFileServiceImpl implements GaeaFileService { return gaeaFileMapper; } + @Override @Transactional(rollbackFor = Exception.class) - public GaeaFile upload(MultipartFile file) { + public GaeaFile upload(MultipartFile multipartFile, File file, String customFileName) { try { - String fileName = file.getOriginalFilename(); + String fileName = ""; + if (null != multipartFile) { + fileName = multipartFile.getOriginalFilename(); + }else { + fileName = file.getName(); + } + if (StringUtils.isBlank(fileName)) { throw BusinessExceptionBuilder.build(ResponseCode.FILE_EMPTY_FILENAME); } @@ -82,7 +90,12 @@ public class GaeaFileServiceImpl implements GaeaFileService { throw BusinessExceptionBuilder.build(ResponseCode.FILE_SUFFIX_UNSUPPORTED); } // 生成文件唯一性标识 - String fileId = UUID.randomUUID().toString(); + String fileId; + if (StringUtils.isBlank(customFileName)) { + fileId = UUID.randomUUID().toString(); + } else { + fileId = customFileName; + } String newFileName = fileId + suffixName; // 本地文件保存路径 String filePath = dictPath + newFileName; @@ -102,7 +115,11 @@ public class GaeaFileServiceImpl implements GaeaFileService { if (!parentFile.exists()) { parentFile.mkdirs(); } - file.transferTo(dest); + if (null != multipartFile) { + multipartFile.transferTo(dest); + }else { + FileUtil.copyFileUsingFileChannels(file, dest); + } // 将完整的http访问路径返回 return gaeaFile; } catch (Exception e) { @@ -112,6 +129,29 @@ public class GaeaFileServiceImpl implements GaeaFileService { } } + /** + * 文件上传 + * + * @param multipartFile 文件 + * @return + */ + @Override + public GaeaFile upload(MultipartFile multipartFile) { + return upload(multipartFile, null, null); + } + + /** + * 文件上传 + * + * @param file 文件 + * @param customFileName 自定义文件名 + * @return + */ + @Override + public GaeaFile upload(File file, String customFileName) { + return upload(null, file, customFileName); + } + @Override public ResponseEntity download(HttpServletRequest request, HttpServletResponse response, String fileId) { try { diff --git a/report-core/src/main/java/com/anjiplus/template/gaea/business/util/FileUtil.java b/report-core/src/main/java/com/anjiplus/template/gaea/business/util/FileUtil.java new file mode 100644 index 00000000..8a81f0c1 --- /dev/null +++ b/report-core/src/main/java/com/anjiplus/template/gaea/business/util/FileUtil.java @@ -0,0 +1,415 @@ +package com.anjiplus.template.gaea.business.util; + +import com.anji.plus.gaea.code.ResponseCode; +import com.anji.plus.gaea.exception.BusinessExceptionBuilder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.multipart.MultipartFile; + +import java.io.*; +import java.net.URL; +import java.nio.channels.FileChannel; +import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Enumeration; +import java.util.zip.*; + +/** + * Created by raodeming on 2021/8/23. + */ +@Slf4j +public class FileUtil { + + //链接url下载图片 + public static void downloadPicture(String urlPath, String path) { + URL url = null; + try { + url = new URL(urlPath); + DataInputStream dataInputStream = new DataInputStream(url.openStream()); + + FileOutputStream fileOutputStream = new FileOutputStream(path); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + + byte[] buffer = new byte[1024]; + int length; + + while ((length = dataInputStream.read(buffer)) > 0) { + output.write(buffer, 0, length); + } + fileOutputStream.write(output.toByteArray()); + dataInputStream.close(); + fileOutputStream.close(); + log.info("链接下载图片:{},临时路径:{}", urlPath, path); + } catch (IOException e) { + log.error("根据链接下载失败", e); + throw BusinessExceptionBuilder.build(ResponseCode.FAIL_CODE, e.getMessage()); + } + } + + + /** + * 复制文件 + * + * @param source + * @param dest + * @throws IOException + */ + public static void copyFileUsingFileChannels(File source, File dest) { + FileChannel inputChannel = null; + FileChannel outputChannel = null; + try { + if (!dest.getParentFile().exists()) { + dest.getParentFile().mkdirs(); + } + inputChannel = new FileInputStream(source).getChannel(); + outputChannel = new FileOutputStream(dest).getChannel(); + outputChannel.transferFrom(inputChannel, 0, inputChannel.size()); + } catch (IOException e) { + log.error("复制文件失败", e); + throw BusinessExceptionBuilder.build(ResponseCode.FAIL_CODE, e.getMessage()); + } finally { + try { + inputChannel.close(); + outputChannel.close(); + } catch (IOException e) { + log.error("", e); + throw BusinessExceptionBuilder.build(ResponseCode.FAIL_CODE, e.getMessage()); + } + } + } + + /** + * 复制文件 + * + * @param source + * @param dest + * @throws IOException + */ + public static void copyFileUsingFileChannels(String source, String dest) { + copyFileUsingFileChannels(new File(source), new File(dest)); + } + + + public static void WriteStringToFile(String filePath, String content) { + try { + //不存在创建文件 + File file = new File(filePath); + if (!file.getParentFile().exists()) { + file.getParentFile().mkdirs(); + } + FileWriter fw = new FileWriter(filePath); + BufferedWriter bw = new BufferedWriter(fw); + bw.write(content); + bw.close(); + fw.close(); + } catch (Exception e) { + log.error("写入文件失败", e); + throw BusinessExceptionBuilder.build(ResponseCode.FAIL_CODE, e.getMessage()); + } + } + + + /** + * 根据文件读取文本文件内容 + * + * @param file + * @return + */ + public static String readFile(File file) { + BufferedReader reader = null; + StringBuilder sbf = new StringBuilder(); + try { + reader = new BufferedReader(new FileReader(file)); + String tempStr; + while ((tempStr = reader.readLine()) != null) { + sbf.append(tempStr); + } + reader.close(); + return sbf.toString(); + } catch (IOException e) { + log.error("读文件失败", e); + throw BusinessExceptionBuilder.build(ResponseCode.FAIL_CODE, e.getMessage()); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException e1) { + throw BusinessExceptionBuilder.build(ResponseCode.FAIL_CODE, e1.getMessage()); + } + } + } + } + + /** + * 根据文件路径读取文本文件内容 + * + * @param filePath + * @return + */ + public static String readFile(String filePath) { + File file = new File(filePath); + return readFile(file); + } + + static final int BUFFER = 8192; + + /** + * 将文件夹压缩zip包 + * + * @param srcPath + * @param dstPath + * @throws IOException + */ + public static void compress(String srcPath, String dstPath) { + File srcFile = new File(srcPath); + File dstFile = new File(dstPath); + + FileOutputStream out = null; + ZipOutputStream zipOut = null; + try { + out = new FileOutputStream(dstFile); + CheckedOutputStream cos = new CheckedOutputStream(out, new CRC32()); + zipOut = new ZipOutputStream(cos); + String baseDir = ""; + compress(srcFile, zipOut, baseDir); + } catch (IOException e) { + log.error("压缩文件夹失败", e); + throw BusinessExceptionBuilder.build(ResponseCode.FAIL_CODE, e.getMessage()); + } finally { + if (null != zipOut) { + try { + zipOut.close(); + } catch (IOException e) { + log.error("", e); + throw BusinessExceptionBuilder.build(ResponseCode.FAIL_CODE, e.getMessage()); + } + out = null; + } + if (null != out) { + try { + out.close(); + } catch (IOException e) { + log.error("", e); + throw BusinessExceptionBuilder.build(ResponseCode.FAIL_CODE, e.getMessage()); + } + } + } + } + + private static void compress(File file, ZipOutputStream zipOut, String baseDir) { + if (file.isDirectory()) { + compressDirectory(file, zipOut, baseDir); + } else { + compressFile(file, zipOut, baseDir); + } + } + + /** + * 压缩一个目录 + */ + private static void compressDirectory(File dir, ZipOutputStream zipOut, String baseDir) { + File[] files = dir.listFiles(); + for (int i = 0; i < files.length; i++) { + compress(files[i], zipOut, baseDir + dir.getName() + "/"); + } + } + + /** + * 压缩一个文件 + */ + private static void compressFile(File file, ZipOutputStream zipOut, String baseDir) { + if (!file.exists()) { + return; + } + + BufferedInputStream bis = null; + try { + bis = new BufferedInputStream(new FileInputStream(file)); + ZipEntry entry = new ZipEntry(baseDir + file.getName()); + zipOut.putNextEntry(entry); + int count; + byte data[] = new byte[BUFFER]; + while ((count = bis.read(data, 0, BUFFER)) != -1) { + zipOut.write(data, 0, count); + } + + } catch (IOException e) { + log.error("压缩文件夹失败", e); + throw BusinessExceptionBuilder.build(ResponseCode.FAIL_CODE, e.getMessage()); + } finally { + if (null != bis) { + try { + bis.close(); + } catch (IOException e) { + log.error("", e); + throw BusinessExceptionBuilder.build(ResponseCode.FAIL_CODE, e.getMessage()); + } + } + } + } + + public static void decompress(String zipFile, String dstPath) { + try { + decompress(new ZipFile(zipFile), dstPath); + } catch (IOException e) { + log.error("", e); + throw BusinessExceptionBuilder.build(ResponseCode.FAIL_CODE, e.getMessage()); + } + } + + public static void decompress(MultipartFile zipFile, String dstPath) { + try { + File file = new File(dstPath + File.separator + zipFile.getOriginalFilename()); + if (!file.getParentFile().exists()) { + file.getParentFile().mkdirs(); + } + zipFile.transferTo(file); + decompress(new ZipFile(file), dstPath); + //解压完删除 + file.delete(); + } catch (IOException e) { + log.error("", e); + throw BusinessExceptionBuilder.build(ResponseCode.FAIL_CODE, e.getMessage()); + } + } + + /** + * 解压zip + * + * @param zip + * @param dstPath + * @throws IOException + */ + public static void decompress(ZipFile zip, String dstPath) { + log.info("解压zip:{},临时目录:{}", zip.getName(), dstPath); + File pathFile = new File(dstPath); + if (!pathFile.exists()) { + pathFile.mkdirs(); + } + try { + + for (Enumeration entries = zip.entries(); entries.hasMoreElements(); ) { + ZipEntry entry = (ZipEntry) entries.nextElement(); + String zipEntryName = entry.getName(); + InputStream in = null; + OutputStream out = null; + try { + in = zip.getInputStream(entry); + String outPath = (dstPath + "/" + zipEntryName).replaceAll("\\*", "/"); + ; + //判断路径是否存在,不存在则创建文件路径 + File file = new File(outPath.substring(0, outPath.lastIndexOf('/'))); + if (!file.exists()) { + file.mkdirs(); + } + //判断文件全路径是否为文件夹,如果是上面已经上传,不需要解压 + if (new File(outPath).isDirectory()) { + continue; + } + + out = new FileOutputStream(outPath); + byte[] buf1 = new byte[1024]; + int len; + while ((len = in.read(buf1)) > 0) { + out.write(buf1, 0, len); + } + } catch (IOException e) { + log.error("解压失败", e); + throw BusinessExceptionBuilder.build(ResponseCode.FAIL_CODE, e.getMessage()); + } finally { + if (null != in) { + try { + in.close(); + } catch (IOException e) { + log.error("", e); + throw BusinessExceptionBuilder.build(ResponseCode.FAIL_CODE, e.getMessage()); + } + } + + if (null != out) { + try { + out.close(); + } catch (IOException e) { + log.error("", e); + throw BusinessExceptionBuilder.build(ResponseCode.FAIL_CODE, e.getMessage()); + } + } + } + } + zip.close(); + } catch (IOException e) { + log.error("解压失败", e); + throw BusinessExceptionBuilder.build(ResponseCode.FAIL_CODE, e.getMessage()); + } + } + + /** + * 获取流文件 + * @param ins + * @param file + */ + private static void inputStreamToFile(InputStream ins, File file) { + try { + OutputStream os = new FileOutputStream(file); + int bytesRead = 0; + byte[] buffer = new byte[8192]; + while ((bytesRead = ins.read(buffer, 0, 8192)) != -1) { + os.write(buffer, 0, bytesRead); + } + os.close(); + ins.close(); + } catch (Exception e) { + log.error("获取流文件失败", e); + throw BusinessExceptionBuilder.build(ResponseCode.FAIL_CODE, e.getMessage()); + } + } + + /** + * 删除文件夹 + * + * @param path + */ + public static void delete(String path) { + + Path directory = Paths.get(path); + try { + Files.walkFileTree(directory, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) throws IOException { + Files.delete(file); // this will work because it's always a File + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + Files.delete(dir); //this will work because Files in the directory are already deleted + return FileVisitResult.CONTINUE; + } + }); + } catch (IOException e) { + log.error("删除文件失败", e); + throw BusinessExceptionBuilder.build(ResponseCode.FAIL_CODE, e.getMessage()); + } + } + + + public static void main(String[] args) throws Exception { +// String targetFolderPath = "D:\\aa"; +// String rawZipFilePath = "D:\\aa.zip"; +// String newZipFilePath = "D:\\aa.zip"; +// +// +// //将目标目录的文件压缩成Zip文件 +// FileUtil.compress(targetFolderPath, newZipFilePath); +// +// //将Zip文件解压缩到目标目录 +// FileUtil.decompress(rawZipFilePath, targetFolderPath); + +// FileUtil.downloadPicture("http://10.108.26.197:9095/file/download/fd20d563-00aa-45e2-b5db-aff951f814ec", "D:\\abc.png"); + + +// delete("D:\\aa"); + + } + + +} diff --git a/report-core/src/main/resources/bootstrap-dev.yml b/report-core/src/main/resources/bootstrap-dev.yml index 576f966f..ad947c14 100644 --- a/report-core/src/main/resources/bootstrap-dev.yml +++ b/report-core/src/main/resources/bootstrap-dev.yml @@ -8,3 +8,4 @@ spring: customer: file: dist-path: D:\Workspace\AJ-Report\report-core\upload + downloadPath: http://127.0.0.1:9095/file/download diff --git a/report-core/src/main/resources/db/migration/V1.0.10__create_report_share.sql b/report-core/src/main/resources/db/migration/V1.0.10__create_report_share.sql index f848dbda..240adc95 100644 --- a/report-core/src/main/resources/db/migration/V1.0.10__create_report_share.sql +++ b/report-core/src/main/resources/db/migration/V1.0.10__create_report_share.sql @@ -66,7 +66,7 @@ INSERT INTO `aj_report`.`gaea_report_data_set`(`set_code`, `set_name`, `set_desc use aj_report_init; -CREATE TABLE `aj_report_barstack` +CREATE TABLE if not exists `aj_report_barstack` ( `id` int(11) NOT NULL AUTO_INCREMENT, `time` date DEFAULT NULL, diff --git a/report-core/src/main/resources/db/migration/V1.0.11_create_compare_table.sql b/report-core/src/main/resources/db/migration/V1.0.11_create_compare_table.sql new file mode 100644 index 00000000..42393792 --- /dev/null +++ b/report-core/src/main/resources/db/migration/V1.0.11_create_compare_table.sql @@ -0,0 +1,21 @@ +use aj_report_init; +CREATE TABLE if not exists `aj_report_comparestack` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `time` date DEFAULT NULL, + `type` varchar(255) DEFAULT NULL, + `nums` bigint(11) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8; + +INSERT INTO `aj_report_init`.`aj_report_comparestack`(`id`, `time`, `type`, `nums`) VALUES (1, '2021-08-23', '成功', 12); +INSERT INTO `aj_report_init`.`aj_report_comparestack`(`id`, `time`, `type`, `nums`) VALUES (2, '2021-08-23', '失败', 1); +INSERT INTO `aj_report_init`.`aj_report_comparestack`(`id`, `time`, `type`, `nums`) VALUES (3, '2021-08-24', '成功', 24); +INSERT INTO `aj_report_init`.`aj_report_comparestack`(`id`, `time`, `type`, `nums`) VALUES (4, '2021-08-24', '失败', 5); +INSERT INTO `aj_report_init`.`aj_report_comparestack`(`id`, `time`, `type`, `nums`) VALUES (5, '2021-08-25', '成功', 13); +INSERT INTO `aj_report_init`.`aj_report_comparestack`(`id`, `time`, `type`, `nums`) VALUES (6, '2021-08-25', '失败', 8); +INSERT INTO `aj_report_init`.`aj_report_comparestack`(`id`, `time`, `type`, `nums`) VALUES (7, '2021-08-26', '成功', 19); +INSERT INTO `aj_report_init`.`aj_report_comparestack`(`id`, `time`, `type`, `nums`) VALUES (8, '2021-08-26', '失败', 3); +INSERT INTO `aj_report_init`.`aj_report_comparestack`(`id`, `time`, `type`, `nums`) VALUES (9, '2021-08-27', '成功', 9); +INSERT INTO `aj_report_init`.`aj_report_comparestack`(`id`, `time`, `type`, `nums`) VALUES (10, '2021-08-27', '失败', 15); + +INSERT INTO `aj_report`.`gaea_report_data_set`(`set_code`, `set_name`, `set_desc`, `source_code`, `dyn_sentence`, `case_result`, `enable_flag`, `delete_flag`, `create_by`, `create_time`, `update_by`, `update_time`, `version`) VALUES ('compare_ajreport', '柱状对比图示例数据', '', 'mysql_ajreport', 'SELECT time,type,nums from aj_report_comparestack', '[{\"time\":\"2021-08-23\",\"type\":\"成功\",\"nums\":12},{\"time\":\"2021-08-23\",\"type\":\"失败\",\"nums\":1},{\"time\":\"2021-08-24\",\"type\":\"成功\",\"nums\":24},{\"time\":\"2021-08-24\",\"type\":\"失败\",\"nums\":5},{\"time\":\"2021-08-25\",\"type\":\"成功\",\"nums\":13},{\"time\":\"2021-08-25\",\"type\":\"失败\",\"nums\":8},{\"time\":\"2021-08-26\",\"type\":\"成功\",\"nums\":19},{\"time\":\"2021-08-26\",\"type\":\"失败\",\"nums\":3},{\"time\":\"2021-08-27\",\"type\":\"成功\",\"nums\":9},{\"time\":\"2021-08-27\",\"type\":\"失败\",\"nums\":15}]', 1, 0, 'admin', '2021-08-27 13:48:33', 'admin', '2021-08-27 13:48:33', 1); diff --git a/report-ui/src/api/bigscreen.js b/report-ui/src/api/bigscreen.js index 79217efb..69555fc0 100644 --- a/report-ui/src/api/bigscreen.js +++ b/report-ui/src/api/bigscreen.js @@ -1,5 +1,6 @@ import request from '@/utils/request' import { getShareToken, getToken } from "@/utils/auth"; +import axios from 'axios'; // 保存大屏设计 export function insertDashboard(data) { @@ -45,3 +46,30 @@ export function getData(data) { data, }) } + +// 导出大屏 +export function exportDashboard(data) { + return new Promise((resolve) =>{ + axios({ + method:'get', + url: process.env.BASE_API + '/reportDashboard/export', + headers: { 'Authorization': getToken() }, + params:data, + responseType:'blob' + }).then(res =>{ + resolve(res.data); + }).catch(err =>{ + resolve('error'); + }) + }) + +} + +// 导入大屏 +export function importDashboard(data) { + return request({ + url: 'reportDashboard/import', + method: 'post', + data, + }) +} diff --git a/report-ui/src/assets/iconfont/demo_index.html b/report-ui/src/assets/iconfont/demo_index.html index f92c2da9..96501588 100644 --- a/report-ui/src/assets/iconfont/demo_index.html +++ b/report-ui/src/assets/iconfont/demo_index.html @@ -54,6 +54,30 @@
    +
  • + +
    导出
    +
    &#xe618;
    +
  • + +
  • + +
    导入
    +
    &#xe608;
    +
  • + +
  • + +
    对比图谱
    +
    &#xe9ca;
    +
  • + +
  • + +
    折线
    +
    &#xe635;
    +
  • +
  • 堆叠图
    @@ -762,9 +786,9 @@
    @font-face {
       font-family: 'iconfont';
    -  src: url('iconfont.woff2?t=1629425895962') format('woff2'),
    -       url('iconfont.woff?t=1629425895962') format('woff'),
    -       url('iconfont.ttf?t=1629425895962') format('truetype');
    +  src: url('iconfont.woff2?t=1629797734566') format('woff2'),
    +       url('iconfont.woff?t=1629797734566') format('woff'),
    +       url('iconfont.ttf?t=1629797734566') format('truetype');
     }
     

    第二步:定义使用 iconfont 的样式

    @@ -790,6 +814,42 @@
      +
    • + +
      + 导出 +
      +
      .icondaochu +
      +
    • + +
    • + +
      + 导入 +
      +
      .icondaoru +
      +
    • + +
    • + +
      + 对比图谱 +
      +
      .iconduibitupu +
      +
    • + +
    • + +
      + 折线 +
      +
      .iconzhexian +
      +
    • +
    • @@ -1852,6 +1912,38 @@
        +
      • + +
        导出
        +
        #icondaochu
        +
      • + +
      • + +
        导入
        +
        #icondaoru
        +
      • + +
      • + +
        对比图谱
        +
        #iconduibitupu
        +
      • + +
      • + +
        折线
        +
        #iconzhexian
        +
      • +
      • +
      - + +
      + + diff --git a/report-ui/src/views/report/bigscreen/designer/widget/temp.vue b/report-ui/src/views/report/bigscreen/designer/widget/temp.vue index 2b1e121a..9396d3f3 100644 --- a/report-ui/src/views/report/bigscreen/designer/widget/temp.vue +++ b/report-ui/src/views/report/bigscreen/designer/widget/temp.vue @@ -33,6 +33,7 @@ import widgetPiePercentageChart from "./pie/widgetPiePercentageChart"; import widgetAirBubbleMap from "./map/widgetAirBubbleMap"; import widgetBarStackChart from "./bar/widgetBarStackChart"; import widgetLineStackChart from "./line/widgetLineStackChart"; +import widgetBarCompareChart from "./bar/widgetBarCompareChart"; export default { name: "WidgetTemp", @@ -58,7 +59,8 @@ export default { widgetPiePercentageChart, widgetAirBubbleMap, widgetBarStackChart, - widgetLineStackChart + widgetLineStackChart, + widgetBarCompareChart }, model: { prop: "value", diff --git a/report-ui/src/views/report/bigscreen/designer/widget/widget.vue b/report-ui/src/views/report/bigscreen/designer/widget/widget.vue index 2096bac2..7b603787 100644 --- a/report-ui/src/views/report/bigscreen/designer/widget/widget.vue +++ b/report-ui/src/views/report/bigscreen/designer/widget/widget.vue @@ -43,6 +43,7 @@ import widgetPiePercentageChart from "./pie/widgetPiePercentageChart"; import widgetAirBubbleMap from "./map/widgetAirBubbleMap"; import widgetBarStackChart from "./bar/widgetBarStackChart"; import widgetLineStackChart from "./line/widgetLineStackChart"; +import widgetBarCompareChart from "./bar/widgetBarCompareChart"; export default { name: "Widget", @@ -68,7 +69,8 @@ export default { widgetPiePercentageChart, widgetAirBubbleMap, widgetBarStackChart, - widgetLineStackChart + widgetLineStackChart, + widgetBarCompareChart }, model: { prop: "value", diff --git a/report-ui/src/views/report/bigscreen/designer/widget/widgetTable.vue b/report-ui/src/views/report/bigscreen/designer/widget/widgetTable.vue index 7297f22d..91a48967 100644 --- a/report-ui/src/views/report/bigscreen/designer/widget/widgetTable.vue +++ b/report-ui/src/views/report/bigscreen/designer/widget/widgetTable.vue @@ -27,7 +27,11 @@