Просмотр исходного кода

!8 importexport
Merge pull request !8 from Foming/dev

Foming 4 лет назад
Родитель
Сommit
6eb3a7b717
31 измененных файлов с 2419 добавлено и 53 удалено
  1. 1 0
      doc/docs/.vuepress/config.js
  2. 4 2
      doc/docs/guide/dashboard.md
  3. 17 0
      doc/docs/guide/importexport.md
  4. BIN
      doc/docs/picture/dashboard/img_18.png
  5. BIN
      doc/docs/picture/imexport/img.png
  6. BIN
      doc/docs/picture/imexport/img_1.png
  7. 31 0
      report-core/src/main/java/com/anjiplus/template/gaea/business/modules/dashboard/controller/ReportDashboardController.java
  8. 23 0
      report-core/src/main/java/com/anjiplus/template/gaea/business/modules/dashboard/service/ReportDashboardService.java
  9. 219 1
      report-core/src/main/java/com/anjiplus/template/gaea/business/modules/dashboard/service/impl/ReportDashboardServiceImpl.java
  10. 22 3
      report-core/src/main/java/com/anjiplus/template/gaea/business/modules/file/service/GaeaFileService.java
  11. 44 4
      report-core/src/main/java/com/anjiplus/template/gaea/business/modules/file/service/impl/GaeaFileServiceImpl.java
  12. 415 0
      report-core/src/main/java/com/anjiplus/template/gaea/business/util/FileUtil.java
  13. 1 0
      report-core/src/main/resources/bootstrap-dev.yml
  14. 1 1
      report-core/src/main/resources/db/migration/V1.0.10__create_report_share.sql
  15. 21 0
      report-core/src/main/resources/db/migration/V1.0.11_create_compare_table.sql
  16. 28 0
      report-ui/src/api/bigscreen.js
  17. 95 3
      report-ui/src/assets/iconfont/demo_index.html
  18. 19 3
      report-ui/src/assets/iconfont/iconfont.css
  19. 0 0
      report-ui/src/assets/iconfont/iconfont.js
  20. 28 0
      report-ui/src/assets/iconfont/iconfont.json
  21. BIN
      report-ui/src/assets/iconfont/iconfont.ttf
  22. BIN
      report-ui/src/assets/iconfont/iconfont.woff
  23. BIN
      report-ui/src/assets/iconfont/iconfont.woff2
  24. 0 3
      report-ui/src/main.js
  25. 178 31
      report-ui/src/views/report/bigscreen/designer/index.vue
  26. 642 0
      report-ui/src/views/report/bigscreen/designer/tools.js
  27. 620 0
      report-ui/src/views/report/bigscreen/designer/widget/bar/widgetBarCompareChart.vue
  28. 3 1
      report-ui/src/views/report/bigscreen/designer/widget/temp.vue
  29. 3 1
      report-ui/src/views/report/bigscreen/designer/widget/widget.vue
  30. 4 0
      report-ui/src/views/report/bigscreen/designer/widget/widgetTable.vue
  31. BIN
      report-ui/static/home.mp4

+ 1 - 0
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'},
                     ]
                 },
                 {

+ 4 - 2
doc/docs/guide/dashboard.md

@@ -88,7 +88,9 @@
 **如有问题,请提交 [Issue](https://gitee.com/anji-plus/report/issues) <br>**
 
 ### 散点图
-**开发中** <br>
+**规划中** <br>
 
 ### 对比图
-**开发中** <br>
+柱状对比图数据集需要3个字段,其中一个作为对比的字段只能为2种值,只有2种值作为对比的字段要选择“y轴字段”字典。因为底层的解析用的是堆叠图的解析,这里的y轴字段并不是指的图表上面的y轴,还请注意,有强迫症可以自行修改源码的解析,剩下的2个字段对应字典看图<br>
+![img18](../picture/dashboard/img_18.png)
+**如有问题,请提交 [Issue](https://gitee.com/anji-plus/report/issues) <br>**

+ 17 - 0
doc/docs/guide/importexport.md

@@ -0,0 +1,17 @@
+**注:导入导出目前是初始版本,报错没有细化,如果导入导出过程中页面无反应,请F12**
+**注:“导入成功/失败”的提示不一定对应当前真实导入导出情况,请根据实际导入导出的结果进行判断**
+
+## 导出
+![img](../picture/imexport/img.png) <br>
+导出会生成zip文件,包含图表、样式、图片等,不会带有该大屏的名称和code。<br>
+
+### 导出数据集
+适用于同一系统内部使用
+
+### 导出不含有数据集
+导出的图表会使用默认的静态数据集,适用于跨系统,请注意,如果你的大屏图表有部分图表是在对方系统不存在的,那么目前整个大屏是不会显示出来的,后续会进行兼容,不存在的图表留空。<br>
+
+## 导入
+![img1](../picture/imexport/img_1.png) <br>
+选择一个导出的zip文件导入即可。注意,导入会覆盖当前大屏,请新建一张空白的大屏进行导入。<br>
+**注:如果你导入的大屏中含有你当前系统不存在的图表,整个大屏是不会显示的。** <br>

BIN
doc/docs/picture/dashboard/img_18.png


BIN
doc/docs/picture/imexport/img.png


BIN
doc/docs/picture/imexport/img_1.png


+ 31 - 0
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<byte[]> 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();
+    }
+
 }

+ 23 - 0
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<ReportDashboardP
      * @return
      */
     Object getChartData(ChartDto dto);
+
+
+    /**
+     * 导出大屏,zip文件
+     * @param request
+     * @param response
+     * @param reportCode
+     * @return
+     */
+    ResponseEntity<byte[]> exportDashboard(HttpServletRequest request, HttpServletResponse response, String reportCode, Integer showDataSet);
+
+    /**
+     * 导入大屏zip
+     * @param file
+     * @param reportCode
+     * @return
+     */
+    void importDashboard(MultipartFile file, String reportCode);
 }

+ 219 - 1
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<String, ChartStrategy> 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<byte[]> 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<ReportDashboardWidgetDto> 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<byte[]> 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<String, String> 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<GaeaFile> 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<String, String> 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<GaeaFile> 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)) {

+ 22 - 3
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<GaeaFileParam, GaeaFile> {
 
+    /**
+     * 文件上传
+     *
+     * @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显示图片或者下载文件
      *

+ 44 - 4
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<byte[]> download(HttpServletRequest request, HttpServletResponse response, String fileId) {
         try {

+ 415 - 0
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<Path>() {
+                @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");
+
+    }
+
+
+}

+ 1 - 0
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

+ 1 - 1
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,

+ 21 - 0
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);

+ 28 - 0
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,
+  })
+}

+ 95 - 3
report-ui/src/assets/iconfont/demo_index.html

@@ -54,6 +54,30 @@
       <div class="content unicode" style="display: block;">
           <ul class="icon_lists dib-box">
           
+            <li class="dib">
+              <span class="icon iconfont">&#xe618;</span>
+                <div class="name">导出</div>
+                <div class="code-name">&amp;#xe618;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe608;</span>
+                <div class="name">导入</div>
+                <div class="code-name">&amp;#xe608;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe9ca;</span>
+                <div class="name">对比图谱</div>
+                <div class="code-name">&amp;#xe9ca;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe635;</span>
+                <div class="name">折线</div>
+                <div class="code-name">&amp;#xe635;</div>
+              </li>
+          
             <li class="dib">
               <span class="icon iconfont">&#xe621;</span>
                 <div class="name">堆叠图</div>
@@ -762,9 +786,9 @@
 <pre><code class="language-css"
 >@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');
 }
 </code></pre>
           <h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@@ -790,6 +814,42 @@
       <div class="content font-class">
         <ul class="icon_lists dib-box">
           
+          <li class="dib">
+            <span class="icon iconfont icondaochu"></span>
+            <div class="name">
+              导出
+            </div>
+            <div class="code-name">.icondaochu
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icondaoru"></span>
+            <div class="name">
+              导入
+            </div>
+            <div class="code-name">.icondaoru
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont iconduibitupu"></span>
+            <div class="name">
+              对比图谱
+            </div>
+            <div class="code-name">.iconduibitupu
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont iconzhexian"></span>
+            <div class="name">
+              折线
+            </div>
+            <div class="code-name">.iconzhexian
+            </div>
+          </li>
+          
           <li class="dib">
             <span class="icon iconfont iconbianzu23"></span>
             <div class="name">
@@ -1852,6 +1912,38 @@
       <div class="content symbol">
           <ul class="icon_lists dib-box">
           
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icondaochu"></use>
+                </svg>
+                <div class="name">导出</div>
+                <div class="code-name">#icondaochu</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icondaoru"></use>
+                </svg>
+                <div class="name">导入</div>
+                <div class="code-name">#icondaoru</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#iconduibitupu"></use>
+                </svg>
+                <div class="name">对比图谱</div>
+                <div class="code-name">#iconduibitupu</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#iconzhexian"></use>
+                </svg>
+                <div class="name">折线</div>
+                <div class="code-name">#iconzhexian</div>
+            </li>
+          
             <li class="dib">
                 <svg class="icon svg-icon" aria-hidden="true">
                   <use xlink:href="#iconbianzu23"></use>

+ 19 - 3
report-ui/src/assets/iconfont/iconfont.css

@@ -1,8 +1,8 @@
 @font-face {
   font-family: "iconfont"; /* Project id 1513211 */
-  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 {
@@ -13,6 +13,22 @@
   -moz-osx-font-smoothing: grayscale;
 }
 
+.icondaochu:before {
+  content: "\e618";
+}
+
+.icondaoru:before {
+  content: "\e608";
+}
+
+.iconduibitupu:before {
+  content: "\e9ca";
+}
+
+.iconzhexian:before {
+  content: "\e635";
+}
+
 .iconbianzu23:before {
   content: "\e621";
 }

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
report-ui/src/assets/iconfont/iconfont.js


+ 28 - 0
report-ui/src/assets/iconfont/iconfont.json

@@ -5,6 +5,34 @@
   "css_prefix_text": "icon",
   "description": "",
   "glyphs": [
+    {
+      "icon_id": "14325372",
+      "name": "导出",
+      "font_class": "daochu",
+      "unicode": "e618",
+      "unicode_decimal": 58904
+    },
+    {
+      "icon_id": "15040056",
+      "name": "导入",
+      "font_class": "daoru",
+      "unicode": "e608",
+      "unicode_decimal": 58888
+    },
+    {
+      "icon_id": "18140536",
+      "name": "对比图谱",
+      "font_class": "duibitupu",
+      "unicode": "e9ca",
+      "unicode_decimal": 59850
+    },
+    {
+      "icon_id": "23013771",
+      "name": "折线",
+      "font_class": "zhexian",
+      "unicode": "e635",
+      "unicode_decimal": 58933
+    },
     {
       "icon_id": "18888301",
       "name": "堆叠图",

BIN
report-ui/src/assets/iconfont/iconfont.ttf


BIN
report-ui/src/assets/iconfont/iconfont.woff


BIN
report-ui/src/assets/iconfont/iconfont.woff2


+ 0 - 3
report-ui/src/main.js

@@ -41,9 +41,6 @@ import Avue from '@smallwei/avue';
 import '@smallwei/avue/lib/index.css';
 Vue.use(Avue);
 
-import VueSuperSlide from 'vue-superslide'
-Vue.use(VueSuperSlide)
-
 // enable element zh-cn
 Vue.use(ElementUI, { zhLocale })
 

+ 178 - 31
report-ui/src/views/report/bigscreen/designer/index.vue

@@ -59,7 +59,7 @@
       :style="{ width: widthLeftForToolsHideButton + 'px' }"
       @click="toolIsShow = !toolIsShow"
     >
-      <i class="el-icon-arrow-right" />
+      <i class="el-icon-arrow-right"/>
     </div>
 
     <div
@@ -87,36 +87,87 @@
             <i class="iconfont iconyulan" @click="viewScreen"></i>
           </el-tooltip>
         </span>
-        <!-- <span class="btn border-left">
+        <span class="btn">
+          <el-tooltip
+            class="item"
+            effect="dark"
+            content="导入"
+            placement="bottom"
+          >
+
+            <el-upload class="el-upload"
+                       ref="upload"
+                       :action="uploadUrl"
+                       :headers="headers"
+                       accept=".zip"
+                       :on-success="handleUpload"
+                       :on-error="handleError"
+                       :show-file-list="false"
+                       :limit="1">
+              <i class="iconfont icondaoru"></i>
+          </el-upload>
+          </el-tooltip>
+        </span>
+        <span class="btn border-left">
           <ul class="nav">
             <li>
-              <i class="el-icon-brush"></i><i class="el-icon-arrow-down"></i>
+              <i class="iconfont icondaochu"></i><i class="el-icon-arrow-down"></i>
               <ul>
                 <li>
-                  <div>
-                    <i class="el-icon-full-screen mr10"></i>边框
-                    <i class="el-icon-arrow-right ml20"></i>
-                  </div>
-                  <ul class="three-level">
-                    <li><a href="#">边框1</a></li>
-                    <li><a href="#">边框2</a></li>
-                    <li><a href="#">边框3</a></li>
-                  </ul>
+                  <el-tooltip
+                    class="item"
+                    effect="dark"
+                    content="适合当前系统"
+                    placement="right"
+                  >
+                       <div @click="exportDashboard(1)">导出(包含数据集)</div>
+                  </el-tooltip>
+
                 </li>
                 <li>
-                  <div>
-                    <i class="el-icon-magic-stick mr10"></i>装饰<i
-                      class="el-icon-arrow-right ml20"
-                    ></i>
-                  </div>
-                  <ul class="three-level">
-                    <li><a href="#">装饰1</a></li>
-                  </ul>
+                  <el-tooltip
+                    class="item"
+                    effect="dark"
+                    content="适合跨系统"
+                    placement="right"
+                  >
+                       <div @click="exportDashboard(0)">导出(不包含数据集)</div>
+                  </el-tooltip>
                 </li>
               </ul>
             </li>
           </ul>
-        </span> -->
+        </span>
+        <!--         <span class="btn border-left">
+                  <ul class="nav">
+                    <li>
+                      <i class="el-icon-brush"></i><i class="el-icon-arrow-down"></i>
+                      <ul>
+                        <li>
+                          <div>
+                            <i class="el-icon-full-screen mr10"></i>边框
+                            <i class="el-icon-arrow-right ml20"></i>
+                          </div>
+                          <ul class="three-level">
+                            <li><a href="#">边框1</a></li>
+                            <li><a href="#">边框2</a></li>
+                            <li><a href="#">边框3</a></li>
+                          </ul>
+                        </li>
+                        <li>
+                          <div>
+                            <i class="el-icon-magic-stick mr10"></i>装饰<i
+                              class="el-icon-arrow-right ml20"
+                            ></i>
+                          </div>
+                          <ul class="three-level">
+                            <li><a href="#">装饰1</a></li>
+                          </ul>
+                        </li>
+                      </ul>
+                    </li>
+                  </ul>
+                </span>-->
       </div>
       <div
         class="workbench-container"
@@ -226,13 +277,15 @@
 </template>
 
 <script>
-import { insertDashboard, detailDashboard } from "@/api/bigscreen";
-import { widgetTools, getToolByCode } from "./tools";
+import {insertDashboard, detailDashboard, importDashboard, exportDashboard} from "@/api/bigscreen";
+import {widgetTools, getToolByCode} from "./tools";
 import widget from "./widget/widget.vue";
 import dynamicForm from "./form/dynamicForm.vue";
 import draggable from "vuedraggable";
 import VueRulerTool from "vue-ruler-tool"; // 大屏设计页面的标尺插件
 import contentMenu from "./form/contentMenu";
+import {getToken} from "@/utils/auth";
+
 export default {
   name: "Login",
   components: {
@@ -244,6 +297,7 @@ export default {
   },
   data() {
     return {
+      uploadUrl: process.env.BASE_API + '/reportDashboard/import/' + this.$route.query.reportCode,
       grade: false,
       layerWidget: [],
       widgetTools: widgetTools, // 左侧工具栏的组件图标,将js变量加入到当前作用域
@@ -310,6 +364,11 @@ export default {
     };
   },
   computed: {
+    headers() {
+      return {
+        Authorization: getToken(), // 直接从本地获取token就行
+      }
+    },
     // 左侧折叠切换时,动态计算中间区的宽度
     middleWidth() {
       var widthLeftAndRight = 0;
@@ -382,7 +441,7 @@ export default {
     },
     async initEchartData() {
       const reportCode = this.$route.query.reportCode;
-      const { code, data } = await detailDashboard(reportCode);
+      const {code, data} = await detailDashboard(reportCode);
       if (code != 200) return;
       const processData = this.handleInitEchartsData(data);
       const screenData = this.handleBigScreen(data.dashboard);
@@ -482,7 +541,7 @@ export default {
         },
         widgets: this.widgets
       };
-      const { code, data } = await insertDashboard(screenData);
+      const {code, data} = await insertDashboard(screenData);
       if (code == "200") {
         this.$message.success("保存成功!");
       }
@@ -491,10 +550,50 @@ export default {
     viewScreen() {
       var routeUrl = this.$router.resolve({
         path: "/bigscreen/viewer",
-        query: { reportCode: this.$route.query.reportCode }
+        query: {reportCode: this.$route.query.reportCode}
       });
       window.open(routeUrl.href, "_blank");
     },
+    //  导出
+    async exportDashboard(val) {
+      const fileName = this.$route.query.reportCode + ".zip"
+
+      const param = {
+        reportCode:this.$route.query.reportCode,
+        showDataSet:val
+      }
+      exportDashboard(param).then(res => {
+        const blob = new Blob([res], {type: "application/octet-stream"});
+        if (window.navigator.msSaveOrOpenBlob) {//msSaveOrOpenBlob方法返回bool值
+          navigator.msSaveBlob(blob, fileName);//本地保存
+        } else {
+          const link = document.createElement('a');//a标签下载
+          link.href = window.URL.createObjectURL(blob);
+          link.download = fileName;
+          link.click();
+          window.URL.revokeObjectURL(link.href);
+        }
+      })
+
+    },
+    // 上传成功的回调
+    handleUpload(response, file, fileList) {
+      //清除el-upload组件中的文件
+      this.$refs.upload.clearFiles();
+      //刷新大屏页面
+      this.initEchartData();
+      this.$message({
+        message: '导入成功!',
+        type: 'success',
+      })
+    },
+    handleError() {
+      this.$message({
+        message: '上传失败!',
+        type: 'error',
+      })
+    },
+
     // 在缩放模式下的大小
     getPXUnderScale(px) {
       return this.bigscreenScaleInWorkbench * px;
@@ -750,22 +849,28 @@ export default {
 .mr10 {
   margin-right: 10px;
 }
+
 .ml20 {
   margin-left: 20px;
 }
+
 .border-right {
   border-right: 1px solid #273b4d;
 }
+
 .border-left {
   border-left: 1px solid #273b4d;
 }
+
 .el-icon-arrow-down {
   font-size: 10px;
 }
+
 .is-active {
   background: #31455d !important;
   color: #bfcbd9 !important;
 }
+
 .layout {
   display: -webkit-box;
   display: -ms-flexbox;
@@ -775,6 +880,7 @@ export default {
   -webkit-box-sizing: border-box;
   box-sizing: border-box;
   overflow: hidden;
+
   .layout-left {
     display: inline-block;
     height: 100%;
@@ -807,6 +913,7 @@ export default {
         border: 1px solid #3a4659;
         background: #282a30;
       }
+
       .tools-item-text {
       }
     }
@@ -823,6 +930,7 @@ export default {
     background-color: #242a30;
     cursor: pointer;
     padding-top: 26%;
+
     i {
       font-size: 18px;
       width: 18px;
@@ -844,18 +952,21 @@ export default {
     align-items: center;
     vertical-align: middle;
     text-align: center;
+
     .top-button {
       display: flex;
       flex-direction: row;
       height: 40px;
       line-height: 40px;
       margin-left: 9px;
+
       .btn {
         color: #788994;
         width: 55px;
         text-align: center;
         display: block;
         cursor: pointer;
+
         .el-icon-arrow-down {
           transform: rotate(0deg);
           -ms-transform: rotate(0deg); /* IE 9 */
@@ -864,8 +975,10 @@ export default {
           -o-transform: rotate(0deg); /* Opera */
           transition: all 0.4s ease-in-out;
         }
+
         &:hover {
           background: rgb(25, 29, 34);
+
           .el-icon-arrow-down {
             transform: rotate(180deg);
             -ms-transform: rotate(180deg); /* IE 9 */
@@ -877,6 +990,7 @@ export default {
         }
       }
     }
+
     .workbench-container {
       position: relative;
       -webkit-transform-origin: 0 0;
@@ -885,10 +999,12 @@ export default {
       box-sizing: border-box;
       margin: 0;
       padding: 0;
+
       .vueRuler {
         width: 100%;
         padding: 18px 0px 0px 18px;
       }
+
       .workbench {
         background-color: #1e1e1e;
         position: relative;
@@ -901,6 +1017,7 @@ export default {
         margin: 0;
         padding: 0;
       }
+
       .bg-grid {
         position: absolute;
         top: 0;
@@ -911,11 +1028,12 @@ export default {
         background-image: linear-gradient(
             hsla(0, 0%, 100%, 0.1) 1px,
             transparent 0
-          ),
-          linear-gradient(90deg, hsla(0, 0%, 100%, 0.1) 1px, transparent 0);
+        ),
+        linear-gradient(90deg, hsla(0, 0%, 100%, 0.1) 1px, transparent 0);
         // z-index: 2;
       }
     }
+
     .bottom-text {
       width: 100%;
       color: #a0a0a0;
@@ -930,36 +1048,43 @@ export default {
     height: 100%;
   }
 
-  /deep/.el-tabs--border-card {
+  /deep/ .el-tabs--border-card {
     border: 0;
+
     .el-tabs__header {
       .el-tabs__nav {
         .el-tabs__item {
           background-color: #242f3b;
           border: 0px;
         }
+
         .el-tabs__item.is-active {
           background-color: #31455d;
         }
       }
     }
+
     .el-tabs__content {
       background-color: #242a30;
       height: calc(100vh - 39px);
       overflow-x: hidden;
       overflow-y: auto;
+
       .el-tab-pane {
         color: #bfcbd9;
       }
+
       &::-webkit-scrollbar {
         width: 5px;
         height: 14px;
       }
+
       &::-webkit-scrollbar-track,
       &::-webkit-scrollbar-thumb {
         border-radius: 1px;
         border: 0 solid transparent;
       }
+
       &::-webkit-scrollbar-track-piece {
         /*修改滚动条的背景和圆角*/
         background: #29405c;
@@ -969,14 +1094,17 @@ export default {
       &::-webkit-scrollbar-track {
         box-shadow: 1px 1px 5px rgba(116, 148, 170, 0.5) inset;
       }
+
       &::-webkit-scrollbar-thumb {
         min-height: 20px;
         background-clip: content-box;
         box-shadow: 0 0 0 5px rgba(116, 148, 170, 0.5) inset;
       }
+
       &::-webkit-scrollbar-corner {
         background: transparent;
       }
+
       /*修改垂直滚动条的样式*/
       &::-webkit-scrollbar-thumb:vertical {
         background-color: #00113a;
@@ -991,34 +1119,41 @@ export default {
     }
   }
 }
+
 ul,
 li {
   list-style: none;
   margin: 0;
   padding: 0;
 }
+
 .nav {
   width: 40px;
   padding: 0;
   list-style: none;
   /* overflow: hidden; */
 }
+
 .nav {
   zoom: 1;
 }
+
 .nav:before,
 .nav:after {
   content: "";
   display: table;
 }
+
 .nav:after {
   clear: both;
 }
+
 .nav li {
   width: 55px;
   text-align: center;
   position: relative;
 }
+
 .nav li a {
   float: left;
   padding: 12px 30px;
@@ -1026,9 +1161,11 @@ li {
   font: bold 12px;
   text-decoration: none;
 }
+
 .nav li:hover {
   color: #788994;
 }
+
 .nav li ul {
   visibility: hidden;
   position: absolute;
@@ -1043,14 +1180,17 @@ li {
   width: 120px;
   transition: all 0.2s ease-in-out;
 }
+
 .nav li:hover > ul {
   opacity: 1;
   visibility: visible;
   margin: 0;
+
   li:hover {
     background-color: rgb(25, 29, 34);
   }
 }
+
 .nav ul li {
   float: left;
   display: block;
@@ -1058,6 +1198,7 @@ li {
   width: 100%;
   font-size: 12px;
 }
+
 .nav ul a {
   padding: 10px;
   width: 100%;
@@ -1068,12 +1209,15 @@ li {
   background-color: rgb(25, 29, 34);
   transition: all 0.2s ease-in-out;
 }
+
 .nav ul a:hover {
   border: 1px solid #3c5e88;
 }
+
 .nav ul li:first-child > a:hover:before {
   border-bottom-color: #04acec;
 }
+
 .nav ul ul {
   top: 0;
   left: 120px;
@@ -1083,6 +1227,7 @@ li {
   padding: 10px;
   _margin: 0;
 }
+
 .nav ul ul li {
   width: 120px;
   height: 120px;
@@ -1090,10 +1235,12 @@ li {
   display: block;
   float: left;
 }
-/deep/.vue-ruler-h {
+
+/deep/ .vue-ruler-h {
   opacity: 0.3;
 }
-/deep/.vue-ruler-v {
+
+/deep/ .vue-ruler-v {
   opacity: 0.3;
 }
 </style>

+ 642 - 0
report-ui/src/views/report/bigscreen/designer/tools.js

@@ -7006,6 +7006,648 @@ const widgetTools = [
       ],
     },
   },
+  {
+    code: 'widgetBarCompareChart',
+    type: 'chart',
+    label: '柱状对比图',
+    icon: 'iconduibitupu',
+    options: {
+      // 配置
+      setup: [
+        {
+          type: 'el-input-text',
+          label: '图层名称',
+          name: 'layerName',
+          required: false,
+          placeholder: '',
+          value: '柱状对比图',
+        },
+        {
+          type: 'vue-color',
+          label: '背景颜色',
+          name: 'background',
+          required: false,
+          placeholder: '',
+          value: ''
+        },
+        [
+          {
+            name: '柱体设置',
+            list: [
+              {
+                type: 'el-slider',
+                label: '最大宽度',
+                name: 'maxWidth',
+                required: false,
+                placeholder: '',
+                value: 15,
+              },
+              {
+                type: 'el-slider',
+                label: '圆角',
+                name: 'radius',
+                require: false,
+                placeholder: '',
+                value: 5,
+              },
+            ],
+          },
+          {
+            name: '标题设置',
+            list: [
+              {
+                type: 'el-switch',
+                label: '标题',
+                name: 'isNoTitle',
+                required: false,
+                placeholder: '',
+                value: true,
+              },
+              {
+                type: 'el-input-text',
+                label: '标题',
+                name: 'titleText',
+                required: false,
+                placeholder: '',
+                value: '',
+              },
+              {
+                type: 'vue-color',
+                label: '字体颜色',
+                name: 'textColor',
+                required: false,
+                placeholder: '',
+                value: '#FFD700'
+              },
+              {
+                type: 'el-select',
+                label: '字体粗细',
+                name: 'textFontWeight',
+                required: false,
+                placeholder: '',
+                selectOptions: [
+                  {code: 'normal', name: '正常'},
+                  {code: 'bold', name: '粗体'},
+                  {code: 'bolder', name: '特粗体'},
+                  {code: 'lighter', name: '细体'}
+                ],
+                value: 'normal'
+              },
+              {
+                type: 'el-input-number',
+                label: '字体大小',
+                name: 'textFontSize',
+                required: false,
+                placeholder: '',
+                value: 20
+              },
+              {
+                type: 'el-select',
+                label: '字体位置',
+                name: 'textAlign',
+                required: false,
+                placeholder: '',
+                selectOptions: [
+                  {code: 'center', name: '居中'},
+                  {code: 'left', name: '左对齐'},
+                  {code: 'right', name: '右对齐'},
+                ],
+                value: 'center'
+              },
+            ],
+          },
+          {
+            name: '左X轴设置',
+            list: [
+              {
+                type: 'el-switch',
+                label: '显示',
+                name: 'hideXLeft',
+                required: false,
+                placeholder: '',
+                value: true,
+              },
+              {
+                type: 'el-input-number',
+                label: '数值间隔',
+                name: 'splitNumberLeft',
+                required: false,
+                placeholder: '',
+                value: ''
+              },
+              {
+                type: 'vue-color',
+                label: '数值颜色',
+                name: 'XcolorLeft',
+                required: false,
+                placeholder: '',
+                value: '#fff',
+              },
+              {
+                type: 'el-input-number',
+                label: '数值字号',
+                name: 'fontSizeXLeft',
+                required: false,
+                placeholder: '',
+                value: 14,
+              },
+              {
+                type: 'el-switch',
+                label: '刻度线',
+                name: 'tickLineLeft',
+                require: false,
+                placeholder: '',
+                value: false,
+              },
+              {
+                type: 'el-switch',
+                label: 'X轴线',
+                name: 'xLineLeft',
+                require: false,
+                placeholder: '',
+                value: false,
+              },
+              {
+                type: 'vue-color',
+                label: '轴颜色',
+                name: 'lineColorXLeft',
+                required: false,
+                placeholder: '',
+                value: '#fff',
+              },
+              {
+                type: 'el-switch',
+                label: '竖分割线',
+                name: 'SplitLineLeft',
+                require: false,
+                placeholder: '',
+                value: false,
+              },
+              {
+                type: 'vue-color',
+                label: '分割线颜色',
+                name: 'SplitLineColorLeft',
+                required: false,
+                placeholder: '',
+                value: '#fff',
+              },
+              {
+                type: 'el-input-number',
+                label: '分割线宽度',
+                name: 'SplitLinefontSizeLeft',
+                required: false,
+                placeholder: '',
+                value: 1,
+              },
+              {
+                type: 'el-switch',
+                label: '边框线',
+                name: 'frameLineLeft',
+                require: false,
+                placeholder: '',
+                value: false,
+              },
+            ],
+          },
+          {
+            name: '右X轴设置',
+            list: [
+              {
+                type: 'el-switch',
+                label: '显示',
+                name: 'hideXRight',
+                required: false,
+                placeholder: '',
+                value: true,
+              },
+              {
+                type: 'el-input-number',
+                label: '数值间隔',
+                name: 'splitNumberRight',
+                required: false,
+                placeholder: '',
+                value: ''
+              },
+              {
+                type: 'vue-color',
+                label: '数值颜色',
+                name: 'XcolorRight',
+                required: false,
+                placeholder: '',
+                value: '#fff',
+              },
+              {
+                type: 'el-input-number',
+                label: '数值字号',
+                name: 'fontSizeXRight',
+                required: false,
+                placeholder: '',
+                value: 14,
+              },
+              {
+                type: 'el-switch',
+                label: '刻度线',
+                name: 'tickLineRight',
+                require: false,
+                placeholder: '',
+                value: false,
+              },
+              {
+                type: 'el-switch',
+                label: 'X轴线',
+                name: 'xLineRight',
+                require: false,
+                placeholder: '',
+                value: false,
+              },
+              {
+                type: 'vue-color',
+                label: '轴颜色',
+                name: 'lineColorXRight',
+                required: false,
+                placeholder: '',
+                value: '#fff',
+              },
+              {
+                type: 'el-switch',
+                label: '竖分割线',
+                name: 'SplitLineRight',
+                require: false,
+                placeholder: '',
+                value: false,
+              },
+              {
+                type: 'vue-color',
+                label: '分割线颜色',
+                name: 'SplitLineColorRight',
+                required: false,
+                placeholder: '',
+                value: '#fff',
+              },
+              {
+                type: 'el-input-number',
+                label: '分割线宽度',
+                name: 'SplitLinefontSizeRight',
+                required: false,
+                placeholder: '',
+                value: 1,
+              },
+              {
+                type: 'el-switch',
+                label: '边框线',
+                name: 'frameLineRight',
+                require: false,
+                placeholder: '',
+                value: false,
+              },
+            ],
+          },
+          {
+            name: 'Y轴设置',
+            list: [
+              {
+                type: 'el-switch',
+                label: '显示',
+                name: 'hideY',
+                required: false,
+                placeholder: '',
+                value: true,
+              },
+              {
+                type: 'vue-color',
+                label: '数值颜色',
+                name: 'colorY',
+                required: false,
+                placeholder: '',
+                value: '#fff',
+              },
+              {
+                type: 'el-input-number',
+                label: '数值字号',
+                name: 'fontSizeY',
+                required: false,
+                placeholder: '',
+                value: 14,
+              },
+              {
+                type: 'el-select',
+                label: '数值对齐',
+                name: 'textAlign',
+                required: false,
+                placeholder: '',
+                selectOptions: [
+                  {code: 'center', name: '居中'},
+                  {code: 'left', name: '左对齐'},
+                  {code: 'right', name: '右对齐'},
+                ],
+                value: 'center'
+              },
+              {
+                type: 'el-switch',
+                label: '刻度线',
+                name: 'tickLineY',
+                require: false,
+                placeholder: '',
+                value: false,
+              },
+              {
+                type: 'el-switch',
+                label: 'Y轴线',
+                name: 'lineY',
+                require: false,
+                placeholder: '',
+                value: false,
+              },
+              {
+                type: 'vue-color',
+                label: '轴颜色',
+                name: 'lineColorY',
+                required: false,
+                placeholder: '',
+                value: '#fff',
+              },
+            ],
+          },
+          {
+            name: '数值设定',
+            list: [
+              {
+                type: 'el-switch',
+                label: '显示',
+                name: 'isShow',
+                required: false,
+                placeholder: '',
+                value: true
+              },
+              {
+                type: 'el-input-number',
+                label: '字体大小',
+                name: 'fontSize',
+                required: false,
+                placeholder: '',
+                value: 14
+              },
+              {
+                type: 'vue-color',
+                label: '字体颜色',
+                name: 'subTextColor',
+                required: false,
+                placeholder: '',
+                value: '#fff'
+              },
+              {
+                type: 'el-select',
+                label: '字体粗细',
+                name: 'fontWeight',
+                required: false,
+                placeholder: '',
+                selectOptions: [
+                  {code: 'normal', name: '正常'},
+                  {code: 'bold', name: '粗体'},
+                  {code: 'bolder', name: '特粗体'},
+                  {code: 'lighter', name: '细体'}
+                ],
+                value: 'normal'
+              },
+            ],
+          },
+          {
+            name: '提示语设置',
+            list: [
+              {
+                type: 'el-input-number',
+                label: '字体大小',
+                name: 'tipsFontSize',
+                required: false,
+                placeholder: '',
+                value: 16
+              },
+              {
+                type: 'vue-color',
+                label: '字体颜色',
+                name: 'lineColor',
+                required: false,
+                placeholder: '',
+              },
+            ],
+          },
+          {
+            name: '坐标轴边距设置',
+            list: [
+              {
+                type: 'el-slider',
+                label: '左右边距(像素)',
+                name: 'marginLeftRight',
+                required: false,
+                placeholder: '',
+                value: 10,
+              },
+              {
+                type: 'el-slider',
+                label: '顶边距(像素)',
+                name: 'marginTop',
+                required: false,
+                placeholder: '',
+                value: 40,
+              },
+              {
+                type: 'el-slider',
+                label: '底边距(像素)',
+                name: 'marginBottom',
+                required: false,
+                placeholder: '',
+                value: 10,
+              },
+            ],
+          },
+          {
+            name: '图例操作',
+            list: [
+              {
+                type: 'el-switch',
+                label: '显示',
+                name: 'isShowLegend',
+                required: false,
+                placeholder: '',
+                value: true,
+              },
+              {
+                type: 'vue-color',
+                label: '字体颜色',
+                name: 'lengedColor',
+                required: false,
+                placeholder: '',
+                value: '#fff',
+              },
+              {
+                type: 'el-input-number',
+                label: '字体大小',
+                name: 'lengedFontSize',
+                required: false,
+                placeholder: '',
+                value: 16,
+              },
+              {
+                type: 'el-input-number',
+                label: '图例宽度',
+                name: 'lengedWidth',
+                required: false,
+                placeholder: '',
+                value: 15,
+              },
+              {
+                type: 'el-select',
+                label: '横向位置',
+                name: 'lateralPosition',
+                required: false,
+                placeholder: '',
+                selectOptions: [
+                  {code: 'center', name: '居中'},
+                  {code: 'left', name: '左对齐'},
+                  {code: 'right', name: '右对齐'},
+                ],
+                value: 'center'
+              },
+              {
+                type: 'el-select',
+                label: '纵向位置',
+                name: 'longitudinalPosition',
+                required: false,
+                placeholder: '',
+                selectOptions: [
+                  {code: 'top', name: '顶部'},
+                  {code: 'bottom', name: '底部'},
+                ],
+                value: 'top'
+              },
+              {
+                type: 'el-select',
+                label: '布局前置',
+                name: 'layoutFront',
+                required: false,
+                placeholder: '',
+                selectOptions: [
+                  {code: 'vertical', name: '竖排'},
+                  {code: 'horizontal', name: '横排'},
+                ],
+                value: 'horizontal'
+              },
+            ],
+          },
+          {
+            name: '自定义配色',
+            list: [
+              {
+                type: 'customColor',
+                label: '',
+                name: 'customColor',
+                required: false,
+                value: [{color: '#36c5e7'}, {color: '#e68b55'}],
+              },
+            ],
+          },
+        ],
+      ],
+      // 数据
+      data: [
+        {
+          type: 'el-radio-group',
+          label: '数据类型',
+          name: 'dataType',
+          require: false,
+          placeholder: '',
+          selectValue: true,
+          selectOptions: [
+            {
+              code: 'staticData',
+              name: '静态数据',
+            },
+            {
+              code: 'dynamicData',
+              name: '动态数据',
+            },
+          ],
+          value: 'staticData',
+        },
+        {
+          type: 'el-input-number',
+          label: '刷新时间(毫秒)',
+          name: 'refreshTime',
+          relactiveDom: 'dataType',
+          relactiveDomValue: 'dynamicData',
+          value: 5000
+        },
+        {
+          type: 'el-button',
+          label: '静态数据',
+          name: 'staticData',
+          required: false,
+          placeholder: 'px',
+          relactiveDom: 'dataType',
+          relactiveDomValue: 'staticData',
+          value: [
+            {"axis":"07-25","name":"success","data":"2"},
+            {"axis":"07-25","name":"fail","data":"10"},
+            {"axis":"07-26","name":"success","data":"5"},
+            {"axis":"07-26","name":"fail","data":"20"},
+            {"axis":"07-27","name":"success","data":"15"},
+            {"axis":"07-27","name":"fail","data":"30"},
+            {"axis":"07-28","name":"success","data":"10"},
+            {"axis":"07-28","name":"fail","data":"12"},
+            {"axis":"07-29","name":"success","data":"9"},
+            {"axis":"07-29","name":"fail","data":"16"},
+          ],
+        },
+        {
+          type: 'dycustComponents',
+          label: '',
+          name: 'dynamicData',
+          required: false,
+          placeholder: 'px',
+          relactiveDom: 'dataType',
+          relactiveDomValue: 'dynamicData',
+          chartType: 'widget-stackchart',
+          dictKey: 'STACK_PROPERTIES',
+          value: '',
+        },
+      ],
+      // 坐标
+      position: [
+        {
+          type: 'el-input-number',
+          label: '左边距',
+          name: 'left',
+          required: false,
+          placeholder: 'px',
+          value: 0,
+        },
+        {
+          type: 'el-input-number',
+          label: '上边距',
+          name: 'top',
+          required: false,
+          placeholder: 'px',
+          value: 0,
+        },
+        {
+          type: 'el-input-number',
+          label: '宽度',
+          name: 'width',
+          required: false,
+          placeholder: '该容器在1920px大屏中的宽度',
+          value: 400,
+        },
+        {
+          type: 'el-input-number',
+          label: '高度',
+          name: 'height',
+          required: false,
+          placeholder: '该容器在1080px大屏中的高度',
+          value: 200,
+        },
+      ],
+    },
+  },
 ]
 
 const getToolByCode = function (code) {

+ 620 - 0
report-ui/src/views/report/bigscreen/designer/widget/bar/widgetBarCompareChart.vue

@@ -0,0 +1,620 @@
+<template>
+  <div :style="styleObj">
+    <v-chart :options="options" autoresize/>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "WidgetBarCompareChart",
+  //参考 https://www.makeapie.com/editor.html?c=xrJwcCF3NZ
+  components: {},
+  props: {
+    value: Object,
+    ispreview: Boolean
+  },
+  data() {
+    return {
+      options: {
+        title: {
+          //text: '柱状对比图',
+          x: 'center',
+          textStyle: {
+            color: '#ffffff',
+          },
+        },
+        //边距
+        grid: [
+          {//左
+            show: false,//边框线
+            left: '4%',
+            top: 60,
+            bottom: 10,
+            containLabel: true,
+            width: '40%'
+          },
+          {//中间字体位置
+            show: false,
+            left: '50.5%',
+            top: 60,
+            bottom: 25,
+            width: '0%'
+          },
+          {//右
+            show: false,
+            right: '4%',
+            top: 60,
+            bottom: 10,
+            containLabel: true,
+            width: '40%'
+          },
+        ],
+        //图例
+        legend: {
+          textStyle: {
+            color: '#fff',
+            textAlign: 'center'
+          },
+          //itemGap:80,
+          //itemWidth: 0
+        },
+        xAxis: [
+          {// 左
+            splitNumber: 2,
+            type: 'value',
+            inverse: true,
+            axisLine: {//底分割线
+              show: false,
+            },
+            axisTick: {
+              show: false,
+            },
+            position: 'bottom',
+            axisLabel: { // x轴
+              show: true,
+              textStyle: {
+                color: '#ffffff',
+                fontSize: 12
+              }
+            },
+            splitLine: { // 竖分割线
+              show: true,
+              lineStyle: {
+                color: '#57617f',
+                width: 1,
+                type: 'solid'
+              }
+            }
+          },
+          {
+            gridIndex: 1,
+            show: false,
+          },
+          {// 右
+            gridIndex: 2,
+            type: 'value',
+            axisLine: {
+              show: false,
+            },
+            axisTick: {
+              show: false,
+            },
+            position: 'bottom',
+            axisLabel: {
+              show: true,
+              textStyle: {
+                color: '#ffffff',
+                fontSize: 12,
+              },
+            },
+            splitLine: {
+              show: true,
+              lineStyle: {
+                color: '#57617f',
+                width: 1,
+                type: 'solid',
+              },
+            },
+          },
+        ],
+        yAxis: [
+          {
+            type: 'category',
+            inverse: true,
+            position: 'right',
+            axisLine: {
+              show: false,
+            },
+            axisTick: {
+              show: false
+            },
+            axisLabel: {
+              show: false,
+            },
+            data: [],
+          },
+          {//处理轴数据
+            gridIndex: 1,
+            type: 'category',
+            inverse: true,
+            position: 'left',
+            axisLine: {
+              show: false
+            },
+            axisTick: {
+              show: false
+            },
+            axisLabel: {
+              show: true,
+              textStyle: {
+                align: 'center',
+                color: '#ffffff',
+                fontSize: 14,
+              }
+            },
+            data: [],
+          },
+          {
+            gridIndex: 2,
+            type: 'category',
+            inverse: true,
+            position: 'left',
+            axisLine: {
+              show: false
+            },
+            axisTick: {
+              show: false
+            },
+            axisLabel: {
+              show: false,
+            },
+          },
+        ],
+        series: [
+          {
+            name: '',
+            type: 'bar',
+            barGap: 20,
+            barWidth: 15,
+            label: {
+              normal: {
+                show: true,
+                color: 'red',
+                position: 'insideLeft',
+                textStyle: {
+                  color: '#ffffff',
+                }
+
+              },
+              emphasis: {
+                show: false,
+              },
+            },
+            itemStyle: {
+              normal: {
+                color: '#36c5e7',
+                barBorderRadius: [8, 0, 0, 8],
+              },
+              emphasis: {
+                show: false,
+              },
+            },
+            data: [],
+          },
+          {
+            name: '',
+            type: 'bar',
+            barGap: 20,
+            barWidth: 15,
+            xAxisIndex: 2,
+            yAxisIndex: 2,
+            label: {
+              normal: {
+                show: true,
+                color: 'red',
+                position: 'insideRight',
+                textStyle: {
+                  color: '#ffffff',
+                }
+              },
+            },
+            itemStyle: {
+              normal: {
+                color: '#e68b55',
+                barBorderRadius: [0, 8, 8, 0],
+              },
+              emphasis: {
+                show: false,
+              },
+            },
+            data: [],
+          },
+        ]
+      },
+      optionsStyle: {},
+      optionsData: {},
+      optionsSetup: {},
+      flagInter: null
+    };
+  },
+  computed: {
+    styleObj() {
+      return {
+        position: this.ispreview ? "absolute" : "static",
+        width: this.optionsStyle.width + "px",
+        height: this.optionsStyle.height + "px",
+        left: this.optionsStyle.left + "px",
+        top: this.optionsStyle.top + "px",
+        background: this.optionsSetup.background
+      };
+    }
+  },
+  watch: {
+    value: {
+      handler(val) {
+        this.optionsStyle = val.position;
+        this.optionsData = val.data;
+        this.optionsSetup = val.setup;
+        this.optionsSetup = val.setup;
+        this.editorOptions();
+      },
+      deep: true
+    }
+  },
+  mounted() {
+    this.optionsStyle = this.value.position;
+    this.optionsData = this.value.data;
+    this.optionsCollapse = this.value.setup;
+    this.optionsSetup = this.value.setup;
+    this.editorOptions();
+  },
+  methods: {
+    // 修改图标options属性
+    editorOptions() {
+      this.setOptionsTitle();
+      this.setOptionsXLeft();
+      this.setOptionsXRight();
+      this.setOptionsY();
+      this.setOptionsTop();
+      this.setOptionsTooltip();
+      this.setOptionsGrid();
+      this.setOptionsLegend();
+      this.setOptionsColor();
+      this.setOptionsData();
+    },
+    // 标题修改
+    setOptionsTitle() {
+      const optionsCollapse = this.optionsSetup;
+      const title = {};
+      title.text = optionsCollapse.titleText;
+      title.show = optionsCollapse.isNoTitle;
+      title.left = optionsCollapse.textAlign;
+      title.textStyle = {
+        color: optionsCollapse.textColor,
+        fontSize: optionsCollapse.textFontSize,
+        fontWeight: optionsCollapse.textFontWeight
+      };
+      this.options.title = title;
+    },
+    // 左X轴设置
+    setOptionsXLeft() {
+      const optionsSetup = this.optionsSetup;
+      const xAxisLeft = {
+        splitNumber: optionsSetup.splitNumberLeft,
+        type: 'value',
+        inverse: true,
+        axisLine: {//X轴线
+          show: optionsSetup.xLineLeft,
+          lineStyle: {
+            color: optionsSetup.lineColorXLeft,
+          },
+        },
+        axisTick: {
+          show: optionsSetup.tickLineLeft,
+        },
+        position: 'bottom',
+        axisLabel: { // x轴
+          show: optionsSetup.hideXLeft,
+          textStyle: {
+            color: optionsSetup.XcolorLeft,
+            fontSize: optionsSetup.fontSizeXLeft
+          }
+        },
+        splitLine: { // 分割线
+          show: optionsSetup.SplitLineLeft,
+          lineStyle: {
+            color: optionsSetup.SplitLineColorLeft,
+            width: optionsSetup.SplitLinefontSizeLeft,
+            type: 'solid'
+          }
+        }
+      }
+      this.options.xAxis[0] = xAxisLeft;
+    },
+    // 右X轴设置
+    setOptionsXRight() {
+      const optionsSetup = this.optionsSetup;
+      const xAxisRight = {
+        gridIndex: 2,
+        splitNumber: optionsSetup.splitNumberRight,
+        type: 'value',
+        axisLine: {//X轴线
+          show: optionsSetup.xLineRight,
+          lineStyle: {
+            color: optionsSetup.lineColorXRight,
+          },
+        },
+        axisTick: {
+          show: optionsSetup.tickLineRight,
+        },
+        position: 'bottom',
+        axisLabel: { // x轴
+          show: optionsSetup.hideXRight,
+          textStyle: {
+            color: optionsSetup.XcolorRight,
+            fontSize: optionsSetup.fontSizeXRight
+          }
+        },
+        splitLine: { // 分割线
+          show: optionsSetup.SplitLineRight,
+          lineStyle: {
+            color: optionsSetup.SplitLineColorRight,
+            width: optionsSetup.SplitLinefontSizeRight,
+            type: 'solid'
+          }
+        }
+      }
+      this.options.xAxis[2] = xAxisRight;
+    },
+    // Y轴设置
+    setOptionsY() {
+      const optionsSetup = this.optionsSetup;
+      const axisLine = {
+        show: optionsSetup.lineY,
+        lineStyle: {
+          color: optionsSetup.lineColorY
+        }
+      };
+      const axisTick = {
+        show: optionsSetup.tickLineY
+      };
+      const axisLabel = {
+        show: optionsSetup.hideY,
+        textStyle: {
+          align: optionsSetup.textAlign,
+          color: optionsSetup.colorY,
+          fontSize: optionsSetup.fontSizeY,
+        }
+      };
+      this.options.yAxis[1]['axisLine'] = axisLine;
+      this.options.yAxis[1]['axisTick'] = axisTick;
+      this.options.yAxis[1]['axisLabel'] = axisLabel;
+    },
+    // 数值设定、柱体设置
+    setOptionsTop() {
+      const optionsSetup = this.optionsSetup;
+      const series = this.options.series;
+      for (const key in series) {
+        if (series[key].type == "bar") {
+          series[0].label = {
+            normal: {
+              show: optionsSetup.isShow,
+              position: 'insideLeft',
+              textStyle: {
+                fontSize: optionsSetup.fontSize,
+                color: optionsSetup.subTextColor,
+                fontWeight: optionsSetup.fontWeight
+              }
+            },
+            emphasis: {
+              show: false,
+            },
+          },
+            series[1].label = {
+              normal: {
+                show: optionsSetup.isShow,
+                color: 'red',
+                position: 'insideRight',
+                textStyle: {
+                  fontSize: optionsSetup.fontSize,
+                  color: optionsSetup.subTextColor,
+                  fontWeight: optionsSetup.fontWeight
+                }
+              },
+              emphasis: {
+                show: false,
+              },
+            },
+            series[key].barWidth = optionsSetup.maxWidth;
+        }
+      }
+      this.options.series = series;
+    },
+    // tooltip 提示语设置
+    setOptionsTooltip() {
+      const optionsSetup = this.optionsSetup;
+      const tooltip = {
+        trigger: "item",
+        show: true,
+        textStyle: {
+          color: optionsSetup.lineColor,
+          fontSize: optionsSetup.fontSize
+        }
+      };
+      this.options.tooltip = tooltip;
+    },
+    // 边距设置
+    getOptionsBottom(){
+      const optionsSetup = this.optionsSetup;
+      let bottom = optionsSetup.marginBottom;
+      if (optionsSetup.hideXLeft) {
+        bottom = optionsSetup.marginBottom + 15
+      }else if (optionsSetup.hideXRight){
+        bottom = optionsSetup.marginBottom + 15
+      }
+      return bottom
+    },
+    setOptionsGrid() {
+      const optionsSetup = this.optionsSetup;
+      const grid = [
+        {//左
+          show: optionsSetup.frameLineLeft,
+          left: optionsSetup.marginLeftRight,
+          top: optionsSetup.marginTop,
+          bottom: optionsSetup.marginBottom,
+          containLabel: true,
+          width: '40%'
+        },
+        {//中间字体位置
+          show: false,
+          left: "51%",
+          top: optionsSetup.marginTop,
+          bottom: this.getOptionsBottom(),
+          width: '0%'
+        },
+        {//右
+          show: optionsSetup.frameLineRight,
+          right: optionsSetup.marginLeftRight,
+          top: optionsSetup.marginTop,
+          bottom: optionsSetup.marginBottom,
+          containLabel: true,
+          width: '40%'
+        },
+      ]
+      this.options.grid = grid;
+    },
+    // 图例操作
+    setOptionsLegend() {
+      const optionsSetup = this.optionsSetup;
+      const legend = this.options.legend;
+      legend.show = optionsSetup.isShowLegend;
+      legend.left = optionsSetup.lateralPosition;
+      legend.top = optionsSetup.longitudinalPosition == "top" ? 0 : "auto";
+      legend.bottom =
+        optionsSetup.longitudinalPosition == "bottom" ? 0 : "auto";
+      legend.orient = optionsSetup.layoutFront;
+      legend.textStyle = {
+        color: optionsSetup.lengedColor,
+        fontSize: optionsSetup.lengedFontSize
+      };
+      legend.itemWidth = optionsSetup.lengedWidth;
+    },
+    // 颜色修改、圆角修改
+    setOptionsColor() {
+      const optionsSetup = this.optionsSetup;
+      const customColor = optionsSetup.customColor;
+      if (!customColor) return;
+      const itemStyleLeft = {
+        normal: {
+          color: customColor[0].color,
+          barBorderRadius: [optionsSetup.radius, 0, 0, optionsSetup.radius]
+        },
+        emphasis: {
+          show: false,
+        },
+      }
+      const itemStyleRight = {
+        normal: {
+          color: customColor[1].color,
+          barBorderRadius: [0, optionsSetup.radius, optionsSetup.radius, 0]
+        },
+        emphasis: {
+          show: false,
+        },
+      }
+      this.options.series[0].itemStyle = itemStyleLeft;
+      this.options.series[1].itemStyle = itemStyleRight;
+    },
+    // 数据解析
+    setOptionsData() {
+      const optionsSetup = this.optionsSetup;
+      const optionsData = this.optionsData; // 数据类型 静态 or 动态
+      optionsData.dataType == "staticData"
+        ? this.staticDataFn(optionsData.staticData, optionsSetup)
+        : this.dynamicDataFn(
+        optionsData.dynamicData,
+        optionsData.refreshTime,
+        optionsSetup
+        );
+    },
+    //去重
+    setUnique(arr) {
+      let newArr = [];
+      arr.forEach(item => {
+        return newArr.includes(item) ? '' : newArr.push(item);
+      });
+      return newArr;
+    },
+    //静态数据
+    staticDataFn(val) {
+      //数据
+      let xAxisList = [];
+      let yAxisList = [];
+      let arrayList = [];
+      for (const i in val) {
+        xAxisList[i] = val[i].axis
+        yAxisList[i] = val[i].name
+      }
+      xAxisList = this.setUnique(xAxisList)
+      yAxisList = this.setUnique(yAxisList)
+      for (const i in yAxisList) {
+        const data = new Array(yAxisList.length).fill(0)
+        for (const j in xAxisList) {
+          for (const k in val) {
+            if (val[k].name == yAxisList[i]) {
+              if (val[k].axis == xAxisList[j]) {
+                data[j] = val[k].data
+              }
+            }
+          }
+        }
+        arrayList.push({
+          name: yAxisList[i],
+          data: data
+        })
+      }
+      this.options.series[0]['name'] = arrayList[0].name
+      this.options.series[0]['data'] = arrayList[0].data
+      this.options.series[1]['name'] = arrayList[1].name
+      this.options.series[1]['data'] = arrayList[1].data
+      this.options.yAxis[1]['data'] = xAxisList
+    },
+    // 动态数据
+    dynamicDataFn(val, refreshTime, optionsSetup) {
+      if (!val) return;
+      if (this.ispreview) {
+        this.getEchartData(val, optionsSetup);
+        this.flagInter = setInterval(() => {
+          this.getEchartData(val, optionsSetup);
+        }, refreshTime);
+      } else {
+        this.getEchartData(val, optionsSetup);
+      }
+    },
+    getEchartData(val, optionsSetup) {
+      const data = this.queryEchartsData(val);
+      data.then(res => {
+        this.renderingFn(optionsSetup, res);
+      });
+    },
+    renderingFn(optionsSetup, val) {
+      this.options.yAxis[1]['data'] = val.xAxis
+      if (val.series[0].type == "bar"){
+        this.options.series[0]['name'] = val.series[0].name
+        this.options.series[0]['data'] = val.series[0].data
+        this.options.series[1]['name'] = val.series[1].name
+        this.options.series[1]['data'] = val.series[1].data
+      }
+    }
+  }
+};
+</script>
+
+<style scoped lang="scss">
+.echarts {
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+}
+
+</style>

+ 3 - 1
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",

+ 3 - 1
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",

+ 4 - 0
report-ui/src/views/report/bigscreen/designer/widget/widgetTable.vue

@@ -27,7 +27,11 @@
   </div>
 </template>
 <script>
+import VueSuperSlide from "vue-superslide";
 export default {
+  components: {
+    VueSuperSlide
+  },
   props: {
     value: Object,
     ispreview: Boolean

BIN
report-ui/static/home.mp4


Некоторые файлы не были показаны из-за большого количества измененных файлов