45 Commity f880b9b701 ... e52477ca01

Autor SHA1 Wiadomość Data
  Foming e52477ca01 update yml 2 miesięcy temu
  Foming e752fa0a5c Merge branch 'dev' 2 miesięcy temu
  Foming aa59a83b99 Merge remote-tracking branch 'origin/dev' into dev 2 miesięcy temu
  Foming 443fd93f73 doc更新,版本更新 2 miesięcy temu
  Foming a3ff34a6e7 update README.md. 2 miesięcy temu
  Foming 4fe52c71b9 doc更新 2 miesięcy temu
  Foming 266703f342 文本框-行高默认值 3 miesięcy temu
  Foming e310c3a946 !221 feat:日历组件支持联动组件功能 3 miesięcy temu
  李帅武 a48aa984c6 feat:日历组件支持联动组件功能 3 miesięcy temu
  Foming 7e9c603c75 !220 bug:解决预览界面缩放异常的问题 3 miesięcy temu
  李帅武 5f4a03bf14 bug:解决预览界面缩放异常的问题 3 miesięcy temu
  Foming 9f41cd0ba4 !219 update v1.7.0 3 miesięcy temu
  Foming 9ad80c5b5e update doc 3 miesięcy temu
  Foming 4ac1ddf9c9 update doc 3 miesięcy temu
  Foming 968ce14d67 update bootstrap.yml 4 miesięcy temu
  Foming 23c88cdf81 update bootstrap.yml 4 miesięcy temu
  Foming a83d8f1b20 增加监控视频属性sql 4 miesięcy temu
  Foming 373c289be7 !218 fix:监控视频组件:视频列表支持最大宽度配置 4 miesięcy temu
  shuaiwu95 7168ada412 Merge branch 'dev' of gitee.com:belief-team/report into pr-branch-feat-video-list 4 miesięcy temu
  李帅武 b53f974e3d fix:监控视频组件:视频列表支持最大宽度配置 4 miesięcy temu
  李帅武 dcb926968e feat: 新增监控视频组件 4 miesięcy temu
  Foming c46b92138d !217 fix: 优化日历组件数据解析部分 4 miesięcy temu
  Foming 2db26a7d9c 增加日历属性sql 4 miesięcy temu
  李帅武 af96edf734 fix: 优化日历组件数据解析部分 4 miesięcy temu
  Foming eaa3018140 !216 增加日历组件 4 miesięcy temu
  李帅武 183e7f2863 增加日历组件 4 miesięcy temu
  Foming 15f02c44c9 !214 丰富了文本组件的样式属性 4 miesięcy temu
  李帅武 4c4b2747c2 丰富了文本组件的样式属性 4 miesięcy temu
  Devli c131977a5a update quick start 4 miesięcy temu
  Devli c57258f95a update quick start 4 miesięcy temu
  Foming afd47e4ff8 !213 1.下拉框支持tab模式 2.下拉框支持默认值 4 miesięcy temu
  李帅武 0d1aef92dc 1.下拉框支持tab模式 2.下拉框支持默认值 4 miesięcy temu
  Foming bdf3cc50f3 按钮组件说明 4 miesięcy temu
  Foming 2371cd8c33 update version 1.7.0 4 miesięcy temu
  Foming e188ab2747 !212 表格组件新增导出按钮 4 miesięcy temu
  Foming f8a7dfbd33 菜单栏调整 4 miesięcy temu
  Foming d81ce6cf07 !211 iframe组件添加定时切换和切换动画效果 4 miesięcy temu
  Foming e80bdabc49 excel导出颜色使用设置颜色-by Brath 4 miesięcy temu
  laa f4f9492ce1 表格组件新增导出按钮 4 miesięcy temu
  lma d897972748 iframe组件添加定时切换和切换动画效果 4 miesięcy temu
  Foming 5665dce2cd form-fix 5 miesięcy temu
  Foming 066fa6d3ed !206 时间筛选器添加“日期类型”选择 5 miesięcy temu
  Foming fe856d9b6e !208 --Button按钮组件控制多条件联动图表实现 5 miesięcy temu
  jianghh1993 645245fd26 --Button按钮组件控制多条件联动图表实现 5 miesięcy temu
  lma 266391b7b3 时间筛选器添加“日期类型”选择 5 miesięcy temu
58 zmienionych plików z 3312 dodań i 105 usunięć
  1. 7 10
      README.md
  2. 3 1
      doc/docs/.vuepress/config.js
  3. 6 2
      doc/docs/guide/authManager.md
  4. 2 1
      doc/docs/guide/chartsConfig.md
  5. 27 14
      doc/docs/guide/chartsLinkage.md
  6. BIN
      doc/docs/guide/community/JiangHH/picture/img_12.png
  7. BIN
      doc/docs/guide/community/JiangHH/picture/img_13.png
  8. BIN
      doc/docs/guide/community/JiangHH/picture/img_14.png
  9. BIN
      doc/docs/guide/community/JiangHH/picture/img_15.png
  10. BIN
      doc/docs/guide/community/JiangHH/picture/img_16.png
  11. BIN
      doc/docs/guide/community/JiangHH/picture/img_17.png
  12. BIN
      doc/docs/guide/community/JiangHH/picture/img_18.png
  13. BIN
      doc/docs/guide/community/JiangHH/picture/img_19.png
  14. BIN
      doc/docs/guide/community/JiangHH/picture/img_20.png
  15. BIN
      doc/docs/guide/community/JiangHH/picture/img_21.png
  16. BIN
      doc/docs/guide/community/JiangHH/picture/img_22.png
  17. 116 0
      doc/docs/guide/community/JiangHH/按钮组件控制多条件组件联动图表实现.md
  18. 6 8
      doc/docs/guide/quicklyDistribution.md
  19. 5 6
      doc/docs/guide/quicklySeparate.md
  20. 5 7
      doc/docs/guide/quicklySource.md
  21. 55 0
      doc/docs/guide/releases/1.7.x.md
  22. BIN
      doc/docs/picture/authmanager/img_2.png
  23. BIN
      doc/docs/picture/chartsLinkage/img_12.png
  24. BIN
      doc/docs/picture/quickly/img.png
  25. BIN
      doc/docs/picture/quickly/img_2.png
  26. 1 1
      report-core/pom.xml
  27. 1 1
      report-core/src/main/java/com/anjiplus/template/gaea/business/modules/reportexcel/util/ColorUtil.java
  28. 20 5
      report-core/src/main/java/com/anjiplus/template/gaea/business/modules/reportexcel/util/XlsSheetUtil.java
  29. 2 2
      report-core/src/main/resources/bootstrap.yml
  30. 11 0
      report-core/src/main/resources/db/migration/V1.7.0__update.sql
  31. 4 2
      report-ui/package.json
  32. 358 0
      report-ui/src/components/FlvVideo.vue
  33. 36 0
      report-ui/src/mixins/queryform.js
  34. 17 0
      report-ui/src/views/bigscreenDesigner/designer/components/componentLinkage.vue
  35. 8 1
      report-ui/src/views/bigscreenDesigner/designer/components/dynamicForm.vue
  36. 189 0
      report-ui/src/views/bigscreenDesigner/designer/components/multiIframeManager.vue
  37. 14 0
      report-ui/src/views/bigscreenDesigner/designer/linkageLogic.js
  38. 216 0
      report-ui/src/views/bigscreenDesigner/designer/tools/configure/form/widget-button.js
  39. 12 0
      report-ui/src/views/bigscreenDesigner/designer/tools/configure/form/widget-form-time.js
  40. 20 0
      report-ui/src/views/bigscreenDesigner/designer/tools/configure/form/widget-select.js
  41. 250 0
      report-ui/src/views/bigscreenDesigner/designer/tools/configure/texts/widget-calendar.js
  42. 39 5
      report-ui/src/views/bigscreenDesigner/designer/tools/configure/texts/widget-iframe.js
  43. 8 0
      report-ui/src/views/bigscreenDesigner/designer/tools/configure/texts/widget-table.js
  44. 44 0
      report-ui/src/views/bigscreenDesigner/designer/tools/configure/texts/widget-text.js
  45. 215 0
      report-ui/src/views/bigscreenDesigner/designer/tools/configure/texts/widget-videoMonitor.js
  46. 6 0
      report-ui/src/views/bigscreenDesigner/designer/tools/main.js
  47. 163 0
      report-ui/src/views/bigscreenDesigner/designer/widget/form/widgetButton.vue
  48. 9 3
      report-ui/src/views/bigscreenDesigner/designer/widget/form/widgetFormTime.vue
  49. 42 0
      report-ui/src/views/bigscreenDesigner/designer/widget/form/widgetSelect.vue
  50. 6 0
      report-ui/src/views/bigscreenDesigner/designer/widget/temp.vue
  51. 401 0
      report-ui/src/views/bigscreenDesigner/designer/widget/texts/widgetCalendar.vue
  52. 198 22
      report-ui/src/views/bigscreenDesigner/designer/widget/texts/widgetIframe.vue
  53. 396 7
      report-ui/src/views/bigscreenDesigner/designer/widget/texts/widgetTable.vue
  54. 6 1
      report-ui/src/views/bigscreenDesigner/designer/widget/texts/widgetText.vue
  55. 372 0
      report-ui/src/views/bigscreenDesigner/designer/widget/texts/widgetVideoMonitor.vue
  56. 6 0
      report-ui/src/views/bigscreenDesigner/designer/widget/widget.vue
  57. 9 5
      report-ui/src/views/bigscreenDesigner/viewer/index.vue
  58. 1 1
      report-ui/src/views/layout/components/Sidebar/index.vue

+ 7 - 10
README.md

@@ -23,11 +23,11 @@
 通过百度网盘分享的文件:大屏模板
 链接:https://pan.baidu.com/s/1rkjBiiTpA4DBQdep-uAYzQ?pwd=w3rm提取码:w3rm
 
-#### &emsp; [在线提问](https://gitee.com/anji-plus/report/issues "issue"): https://gitee.com/anji-plus/report/issues <br>
+#### &emsp; [在线提问](https://gitee.com/belief-team/report/issues "issue"): https://gitee.com/belief-team/report/issues <br>
 
 ## 发行版本
 
-#### &emsp; [下载链接](https://gitee.com/anji-plus/report/releases "下载链接"): https://gitee.com/anji-plus/report/releases <br>
+#### &emsp; [下载链接](https://gitee.com/belief-team/report/releases "下载链接"): https://gitee.com/belief-team/report/releases <br>
 
 ## 功能概述
 
@@ -139,15 +139,15 @@ http://serverip:9095
 在Linux上先准备好maven、node.js、jdk
 
 - [Apache Maven] 3.5 <br>
-- [Node.js]
+- [Node.js] 请使用LTS版本
     - report1.5以下版本请使用 node <= V14
-    - report1.5及以上版本请使用 node = V16
+    - report1.5及以上版本请使用 node >= V14
 - [Jdk] 1.8
 
 简易步骤
 
 ```
-git clone https://gitee.com/anji-plus/report.git
+git clone https://gitee.com/belief-team/report.git
 cd report
 sh build.sh
 编译完成放在build文件夹 aj-report-xxxx.zip
@@ -193,7 +193,7 @@ spring.gaea.Security.jwtSecret = 修改32位jwt密钥
 
 ## SQL初始化
 
-sql文件的目录在:report-core --> src --> main --> resources -- > db.migration <br>
+sql文件的目录在:report-core --> src --> main --> resources --> db.migration <br>
 系统初始化时flyway会自动的将该目录下的sql文件执行,不需要手动执行sql文件,执行完将会创建
 aj_report(存放系统基础数据)数据库 <br>
 
@@ -261,7 +261,7 @@ AJ-Report使用[Apache2.0开源协议](http://www.apache.org/licenses/LICENSE-2.
 
 ## 技术支持
 
-**如有问题,请提交 [Issue](https://gitee.com/anji-plus/report/issues) <br>**
+**如有问题,请提交 [Issue](https://gitee.com/belief-team/report/issues) <br>**
 
 个人微信:个人微信,不闲聊,加群、咨询请备注,备注格式不限但最好是:“项目名 + 问题” ,项目名可以是**AJ-Report**、**Report**、**大屏
 **,只要能一眼分辨就行 <br>
@@ -297,6 +297,3 @@ AJ-Report使用[Apache2.0开源协议](http://www.apache.org/licenses/LICENSE-2.
 
 <br>
 
-## Stargazers over time
-
-[![Stargazers over time](https://starchart.cc/anji-plus/report.svg)](https://starchart.cc/anji-plus/report)

+ 3 - 1
doc/docs/.vuepress/config.js

@@ -42,6 +42,7 @@ module.exports = {
                         {title: 'V1.4.x', path: '/guide/releases/1.4.x'},
                         {title: 'V1.5.x', path: '/guide/releases/1.5.x'},
                         {title: 'V1.6.x', path: '/guide/releases/1.6.x'},
+                        {title: 'V1.7.x', path: '/guide/releases/1.7.x'},
                     ]
                 },
                 {
@@ -92,7 +93,8 @@ module.exports = {
                     children: [
                         {title: '说明', path: '/guide/community/report'},
                         {title: '搭建AJ-Report开发环境', path: '/guide/community/AC1688/搭建aj-report开发环境'},
-                        {title: '大屏组件对齐和拖拽实现', path: '/guide/community/JiangHH/AJ_Report大屏设计时多组件对齐和拖拽移动实现'}
+                        {title: '大屏组件对齐和拖拽实现', path: '/guide/community/JiangHH/AJ_Report大屏设计时多组件对齐和拖拽移动实现'},
+                        {title: '按钮控制多图表联动实现', path: '/guide/community/JiangHH/按钮组件控制多条件组件联动图表实现'}
                     ]
                 }
             ],

+ 6 - 2
doc/docs/guide/authManager.md

@@ -6,7 +6,12 @@
 
 新增用户的默认密码是在bootstrap.yml文件中配置的 <br>
 
-![img](../picture/authmanager/img_2.png) <br>
+```yaml
+  user:
+    ##新增用户默认密码
+    default:
+      password: 123456
+```
 
 ## 用户授权
 
@@ -31,7 +36,6 @@
 导入导出的权限是在 角色 --> 分配权限中控制。 <br>
 
 ![img](../picture/authmanager/img_7.png) <br>
-用户绑定了角色,角色则绑定了权限,是这样一层关系。 <br>
 
 
 

+ 2 - 1
doc/docs/guide/chartsConfig.md

@@ -1,7 +1,8 @@
 ## 简要说明
 
 图表配置项的中文名称基本都是直接使用Echarts图表组件对应定义的名称,非Echarts图表组件则根据功能进行命名。<br>
-每个图表组件的配置项都不尽相同,配置项的作用在于调整图表、数据的展示样式,这里着重说明一些共有的配置项和特殊的配置项,因为并不是按照一个图表一个配置项介绍的,没有介绍的部分请自行尝试了。
+每个图表组件的配置项都不尽相同,配置项的作用在于调整图表、数据的展示样式,这里着重说明一些共有的配置项和特殊的配置项,因为并不是按照一个图表一个配置项介绍的,没有介绍的部分请自行尝试了。<br>
+**请以系统中图表实际的配置项为准** <br>
 
 ## 通用配置项
 

+ 27 - 14
doc/docs/guide/chartsLinkage.md

@@ -4,23 +4,24 @@
 除下文介绍的示例外,还可去数据集、体验地址去查看示例。<br>
 
 - 表单联动 <br>
-  目前支持的有下拉框、时间筛选器。<br>
-  V1.0.0 支持被联动的组件有:<br>
-    - 文本栏 - V1.1.0
+  目前支持的有下拉框、时间筛选器、按钮。<br>
+  支持被联动的组件有(以实际系统中存在的组件为准):<br>
+    - 文本栏 - V1.1
       - 文本
       - 滚动文本
       - 表格
-    - 柱状图(包含全部)
-    - 折线图(包含全部)
-    - 柱线图(包含全部)
-    - 饼图(包含全部)
-    - 漏斗图(包含全部)
-    - 百分比(包含全部)-V1.1.0
-    - 中国地图(包含全部)
-    - 词云图(包含全部)-V1.1.0
-    - 热力图(包含全部)
-    - 雷达图(包含全部)
-    - 刻度尺(包含全部)-V1.1.0
+    - 柱状图(包含全部)-V1.0
+    - 折线图(包含全部)-V1.0
+    - 柱线图(包含全部)-V1.0
+    - 饼图(包含全部)-V1.0
+    - 漏斗图(包含全部)-V1.0
+    - 百分比(包含全部)-V1.1
+    - 中国地图(包含全部)-V1.0
+    - 词云图(包含全部)-V1.1
+    - 热力图(包含全部)-V1.0
+    - 雷达图(包含全部)-V1.0
+    - 刻度尺(包含全部)-V1.1
+    - 日历 -V1.7
 
 - 图表联动:<br>
   目前联动主要集中在柱状图、折线图、饼图等这种二维图表,二维图表是指图表只支持2个字段的数据集,当然并不是说需要3个字段的堆叠图(柱状/折线)不支持联动,而是这种三维图表去联动二维图表会带来一些问题,反之二维的去联动三维的同样存在问题,因此目前源代码中只添加了部分二维图表的联动。<br>
@@ -214,3 +215,15 @@ function doHandleMonth(month) {
   2、联动与被联动的图表必须有相同的数据格式。理论上多维向低维填充数据是没问题的,但实际操作时带来的问题会很多,因此当前版本高维图表都不支持图表组件联动。<br>
   3、被联动的图表的动态数据集必须得有查询参数。说的简单一点就是联动始终都是数据集参数的传递,图表只是数据的载体表象。<br>
   4、使用后的情况和表单组件使用后情况一致。<br>
+
+
+## 按钮联动
+
+- 简介 <br>
+  按钮联动本质上是将表单组件(下拉框、时间筛选器)组件的查询参数提交给联动图表组件,实现多条件查询。
+
+- 说明 <br>
+![img12](../picture/chartsLinkage/img_12.png)
+
+- 详细设计 <br>
+[详细设计](https://ajreport.beliefteam.cn/report-doc/guide/community/JiangHH/按钮组件控制多条件组件联动图表实现)

BIN
doc/docs/guide/community/JiangHH/picture/img_12.png


BIN
doc/docs/guide/community/JiangHH/picture/img_13.png


BIN
doc/docs/guide/community/JiangHH/picture/img_14.png


BIN
doc/docs/guide/community/JiangHH/picture/img_15.png


BIN
doc/docs/guide/community/JiangHH/picture/img_16.png


BIN
doc/docs/guide/community/JiangHH/picture/img_17.png


BIN
doc/docs/guide/community/JiangHH/picture/img_18.png


BIN
doc/docs/guide/community/JiangHH/picture/img_19.png


BIN
doc/docs/guide/community/JiangHH/picture/img_20.png


BIN
doc/docs/guide/community/JiangHH/picture/img_21.png


BIN
doc/docs/guide/community/JiangHH/picture/img_22.png


+ 116 - 0
doc/docs/guide/community/JiangHH/按钮组件控制多条件组件联动图表实现.md

@@ -0,0 +1,116 @@
+
+## 1.需求:
+    多个下拉框联动一个图表,图表只查询最后一次选择的下拉框内容
+    https://gitee.com/anji-plus/report/issues/IC2TFP
+
+## 2.分析:
+    
+    由于目前设计的组件组件联动,都是在条件组件完成选择或者输入之后就直接触发联动了,
+    针对多个条件组件共同作用同一个图表的情况下,用户可能想要在最后一个组件完成输入的情况下才会触发联动
+
+    所以,设计一个按钮组件,在条件组件都输入完成的情况下,有按钮组件触发联动。
+    
+    图表组件所需要的参数均有按钮组件提供,而按钮组件的参数由条件组件(表单组件)传递,形成一个提交表单。
+
+    按钮组件的数据集跟图表的数据集选择保持一致,即参数保持一直.
+
+    ** 原先联动逻辑无需改变 **
+    1.按钮组件 联动  图表组件;
+    2.条件组件 联动  按钮组件;
+
+## 3.设计示例:
+#### 1.测试数据库表  test_user_expenses
+
+```sql
+create table test_user_expenses
+(
+    id       bigint auto_increment comment '主键'
+        primary key,
+    user     varchar(32) not null comment '用户',
+    year     int         null comment '年份',
+    rq       date        null comment '日期',
+    category varchar(50) null comment '分类',
+    expenses decimal     null comment '支出'
+)
+    comment '测试表' collate = utf8mb4_unicode_ci;
+```
+#### 2.测试数据
+```sql
+INSERT INTO test_user_expenses (id, category, expenses, year, user, rq)VALUES (1, '住房物业', 23460, 2023, 'jhh', '2023-04-01');
+INSERT INTO test_user_expenses (id, category, expenses, year, user, rq)VALUES (2, '日用百货', 6496, 2023, 'jhh', '2023-04-15');
+INSERT INTO test_user_expenses (id, category, expenses, year, user, rq)VALUES (3, '交通出行', 5592, 2023, 'jhh', '2023-05-01');
+INSERT INTO test_user_expenses (id, category, expenses, year, user, rq)VALUES (4, '餐饮美食', 3218, 2023, 'jhh', '2023-06-01');
+INSERT INTO test_user_expenses (id, category, expenses, year, user, rq)VALUES (5, '充值缴费', 1953, 2023, 'jhh', '2023-08-08');
+INSERT INTO test_user_expenses (id, category, expenses, year, user, rq)VALUES (7, '其他', 1888, 2023, 'jhh', '2023-10-01');
+INSERT INTO test_user_expenses (id, category, expenses, year, user, rq)VALUES (8, '住房物业', 23550, 2022, 'jhh', '2022-04-01');
+INSERT INTO test_user_expenses (id, category, expenses, year, user, rq)VALUES (9, '日用百货', 2846, 2022, 'jhh', '2022-04-15');
+INSERT INTO test_user_expenses (id, category, expenses, year, user, rq)VALUES (10, '交通出行', 2108, 2022, 'jhh', '2022-05-01');
+INSERT INTO test_user_expenses (id, category, expenses, year, user, rq)VALUES (11, '餐饮美食', 2634, 2022, 'jhh', '2022-06-01');
+INSERT INTO test_user_expenses (id, category, expenses, year, user, rq)VALUES (12, '充值缴费', 5280, 2022, 'jhh', '2022-08-08');
+INSERT INTO test_user_expenses (id, category, expenses, year, user, rq)VALUES (13, '其他', 11553, 2022, 'jhh', '2022-10-01');
+INSERT INTO test_user_expenses (id, category, expenses, year, user, rq)VALUES (14, '住房物业', 40000, 2024, 'jhh', '2024-04-01');
+INSERT INTO test_user_expenses (id, category, expenses, year, user, rq)VALUES (15, '日用百货', 5000, 2024, 'jhh', '2024-04-15');
+INSERT INTO test_user_expenses (id, category, expenses, year, user, rq)VALUES (16, '交通出行', 3000, 2024, 'jhh', '2024-05-01');
+INSERT INTO test_user_expenses (id, category, expenses, year, user, rq)VALUES (17, '餐饮美食', 3000, 2024, 'jhh', '2024-06-01');
+INSERT INTO test_user_expenses (id, category, expenses, year, user, rq)VALUES (18, '充值缴费', 5000, 2024, 'jhh', '2024-08-08');
+INSERT INTO test_user_expenses (id, category, expenses, year, user, rq)VALUES (19, '其他', 10000, 2024, 'jhh', '2024-10-01');
+```
+
+#### 3.测试用的存储过程
+```sql
+create
+    definer = root@localhost procedure get_data(IN p_year varchar(255), IN p_category varchar(255))
+BEGIN
+    SET @sql = 'SELECT id, user, year,rq, category,expenses FROM test_user_expenses WHERE 1 = 1';
+
+    IF p_year IS NOT NULL AND p_year != '' AND p_year != '全部' THEN
+        SET @sql = CONCAT(@sql, ' AND year = "', p_year, '"');
+    END IF;
+
+    IF p_category IS NOT NULL AND p_category != '' AND p_category != '全部' THEN
+        SET @sql = CONCAT(@sql, ' AND category = "', p_category, '"');
+    END IF;
+
+    PREPARE stmt FROM @sql;
+    EXECUTE stmt;
+    DEALLOCATE PREPARE stmt;
+END;
+```
+
+#### 4.数据集
+数据集设计
+![](./picture/img_12.png)
+
+### 5.大屏设计
+
+![](./picture/img_13.png)
+
+按钮组件
+
+![](./picture/img_14.png)
+
+按钮配置
+![](./picture/img_15.png)
+
+按钮联动图标
+![](./picture/img_16.png)
+按钮联动图标参数信息
+![](./picture/img_17.png)
+
+条件组件联动按钮
+![](./picture/img_18.png)
+![](./picture/img_20.png)
+条件组件联动按钮参数信息
+![](./picture/img_19.png)
+![](./picture/img_21.png)
+
+### 6.测试
+![](./picture/img_22.png)
+
+### 7. 注意事项
+    
+    (1) http方式的数据集参数拼接的方式,不允许传递的参数为空,后端请求url解析后可能会带{}会报错,所以要求所有的条件输入都必须有值,不清楚最新不能把支不支持。
+    (2) 建议增加默认联动开关,看需求自行二次修改。
+    (3) 这里做了表单校验,如果存在未输入的,点击按钮会提示,提示语信息,有开发能力的自行增加配置信息。
+
+

+ 6 - 8
doc/docs/guide/quicklyDistribution.md

@@ -23,9 +23,7 @@
 解压下载好的Zip包,找到bootstrap.yml <br>
 ![img_1.png](../picture/quickly/img_17.png) <br>
 
-将图中关于mysql的连接配置信息换成你使用的IP:数据库名。如果没有修改flyway为false,那flyway会自动建立aj_report库 <br>
-
-![bootstrap.png](../picture/quickly/img_2.png) <br>
+将关于mysql的连接配置信息换成你使用的IP:数据库名。 <br>
 **注**:请确认你的Mysql是否支持远程连接,登陆用户是否有DDL权限 <br>
 
 ```yaml
@@ -37,18 +35,18 @@
 
 ## flyway
 
-flyway是用于自动执行sql,因为mysql版本及配置等问题,flyway可能会执行失败。如果你mysql没问题,注意检查flyway是否开启。<br>
+flyway是用于自动执行sql,因为mysql版本及配置等问题,flyway可能会执行失败。<br>
+如果日志提示“FlywayInitialization”错误,请修改为false,并手动执行sql(report-core --> src --> main --> resources --> db.migration)<br>
 
 ```yaml
   flyway:
-    enabled: false    #是否开启flyway(自动执行sql脚本),默认false
+    enabled: true    #是否开启flyway(自动执行sql脚本),默认true
 ```
 
 ## OSS配置
 
 OSS底层已支持minio、amazonS3、nfs,都配置的情况下优先级minio->amazonS3->nfs <br>
-![file.png](../picture/quickly/img.png) <br>
-
+需要修改downloadPath和path <br>
 ```yaml
   gaea:
     subscribes:
@@ -57,7 +55,7 @@ OSS底层已支持minio、amazonS3、nfs,都配置的情况下优先级minio->
         ##允许上传的文件后缀
         file-type-white-list: .png|.jpg|.gif|.icon|.pdf|.xlsx|.xls|.csv|.mp4|.avi|.jpeg|.aaa|.svg
         # 用于文件上传成功后,生成文件的下载公网完整URL,http://serverip:9095/file/download,注意填写IP必须填写后端服务所在的机器IP
-        downloadPath: http://10.108.26.197:9095/file/download
+        downloadPath: http://127.0.0.1:9095/file/download
         nfs:
           #上传对应本地全路径,注意目录不会自动创建,注意 Win是 \ 且有盘符,linux是 / 无盘符,注意目录权限问题
           path: /app/disk/upload/

+ 5 - 6
doc/docs/guide/quicklySeparate.md

@@ -49,9 +49,8 @@ git clone https://gitee.com/anji-plus/report.git <br>
 ### 修改mysql连接
 
 report-core --> src --> main --> resources --> bootstrap.yml <br>
-将图中关于mysql的连接配置信息换成你使用的IP <br>
 
-![bootstrap.png](../picture/quickly/img_2.png) <br>
+将关于mysql的连接配置信息换成你使用的IP:数据库名。 <br>
 **注**:请确认你的Mysql是否支持远程连接,登陆用户是否有DDL权限 <br>
 
 ```yaml
@@ -63,7 +62,8 @@ report-core --> src --> main --> resources --> bootstrap.yml <br>
 
 ## flyway
 
-flyway是用于自动执行sql,因为mysql版本及配置等问题,flyway可能会执行失败。如果你mysql没问题,注意检查flyway是否开启。<br>
+flyway是用于自动执行sql,因为mysql版本及配置等问题,flyway可能会执行失败。<br>
+如果日志提示“FlywayInitialization”错误,请修改为false,并手动执行sql(report-core --> src --> main --> resources --> db.migration)<br>
 
 ```yaml
   flyway:
@@ -73,8 +73,7 @@ flyway是用于自动执行sql,因为mysql版本及配置等问题,flyway可
 ### OSS配置
 
 OSS底层已支持minio、amazonS3、nfs,都配置的情况下优先级minio->amazonS3->nfs <br>
-![file.png](../picture/quickly/img.png) <br>
-
+需要修改downloadPath和path <br>
 ```yaml
   gaea:
     subscribes:
@@ -83,7 +82,7 @@ OSS底层已支持minio、amazonS3、nfs,都配置的情况下优先级minio->
         ##允许上传的文件后缀
         file-type-white-list: .png|.jpg|.gif|.icon|.pdf|.xlsx|.xls|.csv|.mp4|.avi|.jpeg|.aaa|.svg
         # 用于文件上传成功后,生成文件的下载公网完整URL,http://serverip:9095/file/download,注意填写IP必须填写后端服务所在的机器IP
-        downloadPath: http://10.108.26.197:9095/file/download
+        downloadPath: http://127.0.0.1:9095/file/download
         nfs:
           #上传对应本地全路径,注意目录不会自动创建,注意 Win是 \ 且有盘符,linux是 / 无盘符,注意目录权限问题
           path: /app/disk/upload/

+ 5 - 7
doc/docs/guide/quicklySource.md

@@ -55,9 +55,7 @@ git clone https://gitee.com/anji-plus/report.git <br>
 解压aj-report-xxxx.zip,找到bootstrap.yml <br>
 ![img_1.png](../picture/quickly/img_17.png) <br>
 
-将图中关于mysql的连接配置信息换成你使用的IP:数据库名。如果没有修改flyway为false,那flyway会自动建立aj_report库 <br>
-
-![bootstrap.png](../picture/quickly/img_2.png) <br>
+将关于mysql的连接配置信息换成你使用的IP:数据库名。 <br>
 **注**:请确认你的Mysql是否支持远程连接,登陆用户是否有DDL权限 <br>
 
 ```yaml
@@ -69,7 +67,8 @@ git clone https://gitee.com/anji-plus/report.git <br>
 
 ## flyway
 
-flyway是用于自动执行sql,因为mysql版本及配置等问题,flyway可能会执行失败。如果你mysql没问题,注意检查flyway是否开启。<br>
+flyway是用于自动执行sql,因为mysql版本及配置等问题,flyway可能会执行失败。<br>
+如果日志提示“FlywayInitialization”错误,请修改为false,并手动执行sql(report-core --> src --> main --> resources --> db.migration)<br>
 
 ```yaml
   flyway:
@@ -79,8 +78,7 @@ flyway是用于自动执行sql,因为mysql版本及配置等问题,flyway可
 ## OSS配置
 
 OSS底层已支持minio、amazonS3、nfs,都配置的情况下优先级minio->amazonS3->nfs <br>
-![file.png](../picture/quickly/img.png) <br>
-
+需要修改downloadPath和path <br>
 ```yaml
   gaea:
     subscribes:
@@ -89,7 +87,7 @@ OSS底层已支持minio、amazonS3、nfs,都配置的情况下优先级minio->
         ##允许上传的文件后缀
         file-type-white-list: .png|.jpg|.gif|.icon|.pdf|.xlsx|.xls|.csv|.mp4|.avi|.jpeg|.aaa|.svg
         # 用于文件上传成功后,生成文件的下载公网完整URL,http://serverip:9095/file/download,注意填写IP必须填写后端服务所在的机器IP
-        downloadPath: http://10.108.26.197:9095/file/download
+        downloadPath: http://127.0.0.1:9095/file/download
         nfs:
           #上传对应本地全路径,注意目录不会自动创建,注意 Win是 \ 且有盘符,linux是 / 无盘符,注意目录权限问题
           path: /app/disk/upload/

+ 55 - 0
doc/docs/guide/releases/1.7.x.md

@@ -0,0 +1,55 @@
+## 注意事项
+
+1、前端目录:report-ui/node_modules_echarts_need/ <br>
+执行完npm install后需要将node_modules_echarts_need目录下map文件夹整体拷贝到node_modules/echarts/文件夹下,
+因为echarts v5.0之后的版本不在包含map文件夹 <br>
+![img](../../picture/releases/img.png)
+
+2、执行完1之后需要修改 report-ui/node_modules/echarts目录下的package.json
+在“sideEffects”追加 "map/js/\*.js" 和 "map/js/province/\*.js" <br>
+不明白可参考:https://blog.csdn.net/m0_45159572/article/details/130077091
+
+**注意:** 上面执行了一次就可以了,但如果都不执行,地图组件将不显示
+
+## V1.7.0
+
+### 新增
+
+1、大屏设计器-新增视频监控组件 --贡献人:shuaiwu95@shuaiwu123
+2、大屏设计器-新增日历组件 --贡献人:shuaiwu95@shuaiwu123
+3、大屏设计器-下拉框组件增加tab模式 --贡献人:shuaiwu95@shuaiwu123
+4、大屏设计器-下拉框支持默认值显示 --贡献人:shuaiwu95@shuaiwu123
+5、大屏设计器-增加按钮组件(按钮组件使用说明请看在线文档) --贡献人:JiangHH@jianghh1993
+6、大屏设计器-表格组件新增导出按钮 --贡献人:lma@lma_lll
+7、大屏设计器-内联框架组件增加定时切换等功能 --贡献人:lma@lma_lll
+
+### 优化
+
+1、大屏设计器-文本组件丰富样式 --贡献人:shuaiwu95@shuaiwu123
+2、大屏设计器-时间筛选器增加了日期格式选择 --贡献人:lma@lma_lll
+
+### 修复
+
+1、修复excel导出时颜色和设置颜色不一致问题 --贡献人:Brath
+
+### 其他
+
+1、doc更新
+
+## V1.7.1
+
+### 新增
+
+1、大屏设计器-日历组件支持联动 --贡献人:shuaiwu95@shuaiwu123
+
+### 优化
+
+1、大屏设计器-文本框增加行高默认值
+
+### 修复
+
+1、修复预览页面缩放异常问题 --贡献人:shuaiwu95@shuaiwu123
+
+### 其他
+
+1、doc更新

BIN
doc/docs/picture/authmanager/img_2.png


BIN
doc/docs/picture/chartsLinkage/img_12.png


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


BIN
doc/docs/picture/quickly/img_2.png


+ 1 - 1
report-core/pom.xml

@@ -13,7 +13,7 @@
 
     <groupId>com.anji-plus</groupId>
     <artifactId>aj-report</artifactId>
-    <version>1.6.0.RELEASE</version>
+    <version>1.7.1.RELEASE</version>
 
     <properties>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

+ 1 - 1
report-core/src/main/java/com/anjiplus/template/gaea/business/modules/reportexcel/util/ColorUtil.java

@@ -48,7 +48,7 @@ public class ColorUtil {
         if (colorStr.equals("#000")) {
             colorStr = "#000000";
         }
-        if (colorStr != null && colorStr.length() >= 6) {
+        if (colorStr.length() >= 6) {
             try {
                 if (colorStr.length() == 8) {
                     colorStr = colorStr.substring(2);

+ 20 - 5
report-core/src/main/java/com/anjiplus/template/gaea/business/modules/reportexcel/util/XlsSheetUtil.java

@@ -5,6 +5,8 @@ import com.alibaba.fastjson.JSONObject;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.poi.ss.usermodel.*;
 import org.apache.poi.ss.util.CellRangeAddress;
+import org.apache.poi.xssf.usermodel.XSSFCellStyle;
+import org.apache.poi.xssf.usermodel.XSSFColor;
 
 import java.util.*;
 
@@ -137,15 +139,22 @@ public class XlsSheetUtil {
                     setCellStyleFont(wb, style, v_json);
 
                     //bg 背景颜色
-                    if (v_json.containsKey("bg")) {
+/*                    if (v_json.containsKey("bg")) {
                         String _v = getByDBObject(v_json, "bg");
                         Short _color = ColorUtil.getColorByStr(_v);
                         if (_color != null) {
                             style.setFillForegroundColor(_color);
                             style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
                         }
+                    }*/
+                    if (v_json.containsKey("bg")) {
+                        String _v = getByDBObject(v_json, "bg");
+                        if (_v != null && !_v.isEmpty()) {
+                            XSSFColor color = new XSSFColor(hex2Rgb(_v), null);
+                            ((XSSFCellStyle) style).setFillForegroundColor(color);
+                            style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+                        }
                     }
-
                     //vt 垂直对齐    垂直对齐方式(0=居中,1=上,2=下)
                     if (v_json.containsKey("vt")) {
                         Integer _v = getIntByDBObject(v_json, "vt");
@@ -204,14 +213,20 @@ public class XlsSheetUtil {
                             }
                         }
                     }
-
-
                 }
-
             }
         }
     }
 
+    public static byte[] hex2Rgb(String colorStr) {
+        colorStr = colorStr.replace("#", "");
+        return new byte[] {
+                (byte) Integer.parseInt(colorStr.substring(0, 2), 16),
+                (byte) Integer.parseInt(colorStr.substring(2, 4), 16),
+                (byte) Integer.parseInt(colorStr.substring(4, 6), 16)
+        };
+    }
+
     /**
      * 设置边框
      *

+ 2 - 2
report-core/src/main/resources/application.yml → report-core/src/main/resources/bootstrap.yml

@@ -76,7 +76,7 @@ spring:
 #    password: root
 #    database: 1
   flyway:
-    enabled: false   #是否开启flyway(自动执行sql脚本),true,注意部分mysql版本存在自动执行失败的情况,需要改成false并手动执行所有sql文件:report-core --> src --> main --> resources -- > db.migration
+    enabled: true   #是否开启flyway(自动执行sql脚本),true,注意部分mysql版本存在自动执行失败的情况,需要改成false并手动执行所有sql文件:report-core --> src --> main --> resources -- > db.migration
     baseline-on-migrate: true
     #数据库连接配置
     url: ${spring.datasource.url}
@@ -92,7 +92,7 @@ spring:
         ##允许上传的文件后缀
         file-type-white-list: .png|.jpg|.gif|.icon|.pdf|.xlsx|.xls|.csv|.mp4|.avi|.jpeg|.aaa|.svg
         # 用于文件上传成功后,生成文件的下载公网完整URL,http://serverip:9095/file/download,注意填写IP必须填写后端服务所在的机器IP
-        downloadPath: http://10.108.26.197:9095/file/download
+        downloadPath: http://127.0.0.1:9095/file/download
         nfs:
           #上传对应本地全路径,注意目录不会自动创建,注意 Win是 \ 且有盘符,linux是 / 无盘符,注意目录权限问题
           path: /app/disk/upload/

+ 11 - 0
report-core/src/main/resources/db/migration/V1.7.0__update.sql

@@ -0,0 +1,11 @@
+-- 增加日历解析属性
+INSERT INTO `gaea_dict`(`dict_name`, `dict_code`, `remark`, `create_by`, `create_time`, `update_by`, `update_time`, `version`) VALUES ('日历属性', 'CALENDAR_PROPERTIES', '日历属性', 'admin', '2021-04-29 10:28:15', 'admin', '2021-06-23 10:47:20', 1);
+
+INSERT INTO `gaea_dict_item`(`dict_code`, `item_name`, `item_value`, `item_extend`, `enabled`, `locale`, `remark`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`, `version`) VALUES ('CALENDAR_PROPERTIES', '日期', 'date', NULL, 1, 'zh', NULL, 1, 'admin', '2021-08-20 10:31:51', 'admin', '2021-08-20 10:31:51', 1);
+INSERT INTO `gaea_dict_item`(`dict_code`, `item_name`, `item_value`, `item_extend`, `enabled`, `locale`, `remark`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`, `version`) VALUES ('CALENDAR_PROPERTIES', '数据', 'data', NULL, 1, 'zh', NULL, 6, 'admin', '2021-08-20 10:32:52', 'admin', '2021-08-20 10:32:52', 1);
+
+-- 增加监控视频解析属性
+INSERT INTO `gaea_dict`(`dict_name`, `dict_code`, `remark`, `create_by`, `create_time`, `update_by`, `update_time`, `version`) VALUES ('监控视频属性', 'MONITOR_PROPERTIES', '监控视频属性', 'admin', '2021-04-29 10:28:15', 'admin', '2021-06-23 10:47:20', 1);
+
+INSERT INTO `gaea_dict_item`(`dict_code`, `item_name`, `item_value`, `item_extend`, `enabled`, `locale`, `remark`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`, `version`) VALUES ('MONITOR_PROPERTIES', '视频地址', 'src', NULL, 1, 'zh', NULL, 1, 'admin', '2021-08-20 10:31:51', 'admin', '2021-08-20 10:31:51', 1);
+INSERT INTO `gaea_dict_item`(`dict_code`, `item_name`, `item_value`, `item_extend`, `enabled`, `locale`, `remark`, `sort`, `create_by`, `create_time`, `update_by`, `update_time`, `version`) VALUES ('MONITOR_PROPERTIES', '视频名称', 'name', NULL, 1, 'zh', NULL, 6, 'admin', '2021-08-20 10:32:52', 'admin', '2021-08-20 10:32:52', 1);

+ 4 - 2
report-ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "report-ui",
-  "version": "1.6.0",
+  "version": "1.7.1",
   "description": "report-ui",
   "author": "beliefteam",
   "scripts": {
@@ -24,6 +24,7 @@
     "echarts-liquidfill": "^3.1.0",
     "echarts-wordcloud": "^2.1.0",
     "element-ui": "^2.15.14",
+    "flv.js": "^1.6.2",
     "js-cookie": "2.2.0",
     "jsbarcode": "^3.11.4",
     "miment": "^0.0.9",
@@ -45,7 +46,8 @@
     "vue-ruler-tool": "^1.2.4",
     "vue-superslide": "^0.1.1",
     "vuedraggable": "^2.24.1",
-    "vuex": "^3.6.2"
+    "vuex": "^3.6.2",
+    "xlsx": "^0.18.5"
   },
   "devDependencies": {
     "autoprefixer": "8.5.0",

+ 358 - 0
report-ui/src/components/FlvVideo.vue

@@ -0,0 +1,358 @@
+<template>
+  <div class="video-container" :class="`layout-${currentLayout}`" :style="styleObj">
+    <div 
+      v-for="(player, index) in sortedPlayers" 
+      :key="player.videoId" 
+      class="video-item"
+      :style="getItemStyle(index)"
+    >
+      <video 
+        :ref="`video-${player.videoId}`" 
+        class="video-element" 
+        :class="`video-${player.videoId}`"
+        controls
+        autoplay
+        muted
+      ></video>
+      <!-- <span class="video-title">通道_{{ (player.videoId + 1) }}</span> -->
+      <div v-if="player.src === ''" class="video-placeholder">
+        <svg t="1745889737159" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4930" width="48" height="48"><path d="M504.832 644.608c-47.616 0-86.016 38.4-86.016 86.016s38.4 86.016 86.016 86.016 86.016-38.4 86.016-86.016-38.4-86.016-86.016-86.016z m422.4 250.88h-153.088c118.784-84.48 196.096-222.72 196.096-379.392 0-257.024-208.384-465.408-465.408-465.408S39.424 258.56 39.424 515.584s208.384 465.408 465.408 465.408h422.4c23.552 0 43.008-19.456 43.008-43.008s-19.456-42.496-43.008-42.496z m-422.4 0c-209.408 0-379.392-169.984-379.392-379.392s169.984-379.392 379.392-379.392 379.392 169.984 379.392 379.392c0 209.408-169.984 379.392-379.392 379.392zM289.792 430.08c-47.616 0-86.016 38.4-86.016 86.016s38.4 86.016 86.016 86.016 86.016-38.4 86.016-86.016S337.408 430.08 289.792 430.08z m250.88 92.672c0-19.968-15.872-35.84-35.84-35.84s-35.84 15.872-35.84 35.84 15.872 35.84 35.84 35.84 35.84-15.872 35.84-35.84zM712.704 430.08c-47.616 0-86.016 38.4-86.016 86.016s38.4 86.016 86.016 86.016 86.016-38.4 86.016-86.016S759.808 430.08 712.704 430.08zM504.832 215.04c-47.616 0-86.016 38.4-86.016 86.016s38.4 86.016 86.016 86.016 86.016-38.4 86.016-86.016S552.448 215.04 504.832 215.04z" fill="#ffffff" p-id="4931"></path></svg>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import flvjs from 'flv.js'
+import { set } from 'vue'
+
+export default {
+  name: 'FlvVideo',
+  props: {
+    maxPlayers: {
+      type: Number,
+      default: 9 // 3x3最大数量
+    },
+    indexs: {
+      type: Array,
+      default: () => [],
+    },
+    styleObj: {
+      type: Object,
+      default: () => ({}),
+    },
+  },
+  data() {
+    return {
+      timer: null,
+      currentLayout: '1x1',
+      players: {}, // 改为对象存储,key为videoId
+      flvjs: null
+    }
+  },
+  computed: {
+    // 根据indexs排序后的播放器数组
+    sortedPlayers() {
+      const maxCount = this.getMaxPlayerCount()
+      const result = []
+      
+      // 1. 先添加indexs中存在的播放器
+      this.indexs.slice(0, maxCount).forEach(videoId => {
+        if (this.players[videoId]) {
+          result.push(this.players[videoId])
+        } else {
+          // 如果不存在则创建新播放器占位
+          result.push({
+            videoId,
+            active: false,
+            flvPlayer: null,
+            src: ''
+          })
+        }
+      })
+      
+      // 2. 如果还有空位,添加已存在但不在indexs中的播放器(保留状态)
+      // const remainingSlots = maxCount - result.length
+      // if (remainingSlots > 0) {
+      //   const existingPlayers = Object.values(this.players)
+      //     .filter(player => !this.indexs.includes(player.videoId))
+      //     .slice(0, remainingSlots)
+      //   result.push(...existingPlayers)
+      // }
+      // 2. 清理不在indexs中的播放器
+      Object.keys(this.players).forEach(videoId => {
+        if (!this.indexs.includes(Number(videoId))) {
+          this.stopPlayer(videoId)
+        }
+      })
+      
+      // 3. 如果还有空位,添加空白占位
+      while (result.length < maxCount) {
+        result.push({
+          videoId: `placeholder-${result.length}`,
+          active: false,
+          flvPlayer: null,
+          src: ''
+        })
+      }
+
+      console.log('this.players:', result)
+      
+      return result
+    }
+  },
+  watch: {
+    indexs: {
+      immediate: true,
+      deep: true,
+      handler(newIds, oldIds = []) {
+        this.syncPlayers(newIds, oldIds)
+      }
+    }
+  },
+  mounted() {
+    this.flvjs = flvjs
+    if (!this.flvjs.isSupported()) {
+      console.error('FLV is not supported in this browser')
+    }
+  },
+  beforeDestroy() {
+    this.stopAll()
+  },
+  methods: {
+    // 同步播放器状态
+    syncPlayers(newIds, oldIds) {
+      if (this.timer) { clearTimeout(this.timer) }
+      this.timer = setTimeout(() => {
+        const maxCount = this.getMaxPlayerCount()
+        const idsToKeep = newIds.slice(0, maxCount)
+        
+        // 停止不再需要的播放器
+        oldIds.forEach(videoId => {
+          if (!idsToKeep.includes(videoId)) {
+            this.stopPlayer(videoId)
+          }
+        })
+        
+        // 更新布局
+        this.updateLayout()
+
+        clearTimeout(this.timer)
+      }, 0)
+    },
+    
+    // 更新布局
+    updateLayout() {
+      const count = Math.min(this.indexs.length, this.getMaxPlayerCount())
+      let layout = '1x1'
+      if (count > 4) {
+        layout = '3x3'
+      } else if (count > 1) {
+        layout = '2x2'
+      }
+      this.currentLayout = layout
+    },
+    
+    // 设置分屏布局
+    setLayout(layout) {
+      return new Promise((resolve) => {
+        const validLayouts = ['1x1', '2x2', '3x3']
+        if (!validLayouts.includes(layout)) {
+          console.error(`Invalid layout: ${layout}. Supported layouts are ${validLayouts.join(', ')}`)
+          return
+        }
+        this.currentLayout = layout
+        this.$nextTick(resolve)
+      })
+    },
+    
+    // 开始预览指定视频
+    startPreview(src, videoId) {
+      if (!src) {
+        console.error('No video source provided')
+        return
+      }
+      
+      // 如果已经存在相同的播放器且src相同,则不处理
+      if (this.players[videoId] && this.players[videoId].src === src && this.players[videoId].active) {
+        return
+      }
+      
+      // 停止当前播放器(如果存在)
+      if (this.players[videoId]) {
+        this.stopPlayer(videoId)
+      }
+      
+      this.createPlayer(src, videoId)
+    },
+
+    getFileExtension(url) {
+      // 匹配最后一个点后的字母数字组合(忽略查询参数和哈希)
+      const match = /\.([a-z0-9]+)(?:[?#]|$)/i.exec(url.split('/').pop());
+      return match ? match[1].toLowerCase() : '';
+    },
+    
+    // 创建FLV播放器
+    createPlayer(src, videoId) {
+      this.$nextTick(() => {
+        const videoElement = this.$refs[`video-${videoId}`][0] || null
+        if (!videoElement) {
+          // console.error(`Video element not found for videoId: ${videoId}`)
+          return
+        }
+        
+        if (this.flvjs.isSupported()) {
+          const flvPlayer = this.flvjs.createPlayer({
+            type: this.getFileExtension(src),
+            url: src,
+            isLive: true
+          })
+          
+          flvPlayer.attachMediaElement(videoElement)
+          flvPlayer.load()
+          
+          // 初始化播放器状态
+          this.$set(this.players, videoId, {
+            active: false,
+            flvPlayer,
+            src,
+            videoId
+          })
+          
+          // 添加事件监听
+          flvPlayer.on(flvjs.Events.METADATA_ARRIVED, () => {
+            this.$set(this.players[videoId], 'active', true)
+          })
+          
+          flvPlayer.on(flvjs.Events.ERROR, (err) => {
+            console.error('FLV player error:', err)
+            this.stopPlayer(videoId)
+            this.$message.error('Playback error:' + err)
+          })
+          
+          try {
+            flvPlayer.play()
+          } catch (err) {
+            console.error('Playback error:', err)
+            this.stopPlayer(videoId)
+            this.$message.error('Playback error:' + err)
+          }
+        }
+      })
+      // console.log('this.players:', this.players)
+    },
+    
+    // 停止所有视频播放
+    stop() {
+      this.stopAll()
+    },
+    
+    // 停止指定播放器
+    stopPlayer(videoId) {
+      const player = this.players[videoId]
+      if (player && player.flvPlayer) {
+        player.flvPlayer.pause()
+        player.flvPlayer.unload()
+        player.flvPlayer.detachMediaElement()
+        player.flvPlayer.destroy()
+        this.$set(this.players[videoId], 'active', false)
+        this.$set(this.players[videoId], 'flvPlayer', null)
+        this.$set(this.players[videoId], 'src', '')
+        // this.players = this.players.filter(item => item.videoId !== videoId)
+
+        // console.log('this.players:', this.players)
+      }
+    },
+    
+    // 停止所有播放器
+    stopAll() {
+      Object.keys(this.players).forEach(videoId => {
+        this.stopPlayer(videoId)
+      })
+    },
+    
+    // 获取当前布局下的最大播放器数量
+    getMaxPlayerCount() {
+      const [rows, cols] = this.currentLayout.split('x').map(Number)
+      return rows * cols
+    },
+    
+    // 获取视频项的样式
+    getItemStyle(index) {
+      const [rows, cols] = this.currentLayout.split('x').map(Number)
+      const maxIndex = rows * cols - 1
+      
+      if (index > maxIndex) {
+        return { display: 'none' }
+      }
+      
+      return {
+        width: `${100 / cols}%`,
+        height: `${100 / rows}%`
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.video-container {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-wrap: wrap;
+  background-color: #000;
+  position: relative;
+}
+
+.video-item {
+  position: relative;
+  box-sizing: border-box;
+  border: 1px solid #333;
+}
+
+.video-element {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+  /* background-color: #000; */
+}
+.video-title {
+  position: absolute;
+  top: 10px;
+  right: 10px;
+  font-size: 14px;
+  color: red;
+  font-weight: bold;
+}
+
+.video-placeholder {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #fff;
+  background-color: #222;
+  font-size: 14px;
+}
+
+/* 不同布局下的样式 */
+.layout-1x1 .video-item {
+  width: 100% !important;
+  height: 100% !important;
+}
+
+.layout-2x2 .video-item {
+  width: 50% !important;
+  height: 50% !important;
+}
+
+.layout-3x3 .video-item {
+  width: 33.33% !important;
+  height: 33.33% !important;
+}
+</style>

+ 36 - 0
report-ui/src/mixins/queryform.js

@@ -173,6 +173,8 @@ export default {
         return this.piechartFn(params.chartProperties, data);
       } else if (chartType == "widget-text") {
         return this.widgettext(params.chartProperties, data)
+      } else if (chartType == "widget-calendar") {
+        return this.widgetcalendar(params.chartProperties, data)
       } else if (chartType == "widget-stackchart") {
         return this.stackChartFn(params.chartProperties, data)
       } else if (chartType == "widget-coord") {
@@ -187,6 +189,8 @@ export default {
         return this.mapLLChartFn(params.chartProperties, data)
       } else if (chartType == "widget-stackMoreShowChart") {
         return this.stackMoreShowFn(params.chartProperties, data);
+      } else if (chartType == "widget-videoMonitor") {
+        return this.widgetVideoMonitor(params.chartProperties, data);
       } else {
         return data
       }
@@ -285,6 +289,38 @@ export default {
       }
       return analysisData;
     },
+    widgetcalendar (chartProperties, data) {
+      const analysisData = [];
+      for (let i = 0; i < data.length; i++) {
+        const obj = {};
+        for (const key in chartProperties) {
+          const value = chartProperties[key];
+          if (value === "date") {
+            obj["date"] = data[i][key];
+          } else {
+            obj["data"] = data[i][key];
+          }
+        }
+        analysisData.push(obj);
+      }
+      return analysisData;
+    },
+    widgetVideoMonitor (chartProperties, data) {
+      const analysisData = [];
+      for (let i = 0; i < data.length; i++) {
+        const obj = {};
+        for (const key in chartProperties) {
+          const value = chartProperties[key];
+          if (value === "name") {
+            obj["name"] = data[i][key];
+          } else {
+            obj["src"] = data[i][key];
+          }
+        }
+        analysisData.push(obj);
+      }
+      return analysisData;
+    },
     // 坐标系数据解析
     coordChartFn(chartProperties, data) {
       const analysisData = {};

+ 17 - 0
report-ui/src/views/bigscreenDesigner/designer/components/componentLinkage.vue

@@ -47,6 +47,7 @@
             size="mini"
             clearable
             placeholder="请选择"
+            @change="handleChange"
           >
             <el-option
               v-for="(item, index) in layerWidget"
@@ -185,6 +186,22 @@ export default {
         })
       }
     },
+    handleChange(val){
+      //判断当前源联动组件是否是按钮
+      if('widget-button'=== this.layerWidget[this.widgetIndex].code){
+        //根据目标组件的参数个数来确定按钮组件的参数个数
+        this.linkageForm.paramsConfig = []; //每次切换都置空
+        let paramKeys = Object.keys(this.widgetParamsConfig[this.targetIndex].dynamicData.contextData);
+        paramKeys.forEach(param=>{
+          this.linkageForm.paramsConfig.push({
+            originKey: param,
+            targetKey: ''
+          });
+        });
+        //把参数传给button组件的表单
+        //this.layerWidget[this.widgetIndex].setFormData(paramKeys);
+      }
+    },
     // 弹出框关闭
     handleClose() {
       this.dialogVisible = false

+ 8 - 1
report-ui/src/views/bigscreenDesigner/designer/components/dynamicForm.vue

@@ -192,6 +192,11 @@
             :chart-type="item.chartType"
             @change="changed($event, item.name)"
           />
+          <multiIframeManager
+            v-if="item.type == 'multiIframeManager'"
+            v-model="formData[item.name]"
+            @change="(val) => changed(val, item.name)"
+          />
         </div>
         <div v-else-if="isShowForm(item, '[object Array]')" :key="'a-' + index">
           <el-collapse accordion>
@@ -347,6 +352,7 @@ import dynamicAddRadar from "./dynamicAddRadar";
 import MonacoEditor from "@/components/MonacoEditor/index";
 import componentLinkage from './componentLinkage';
 import imageSelect from './imageSelect';
+import multiIframeManager from './multiIframeManager.vue';
 export default {
   name: "DynamicForm",
   components: {
@@ -360,7 +366,8 @@ export default {
     customUpload,
     dynamicAddRadar,
     MonacoEditor,
-    componentLinkage
+    componentLinkage,
+    multiIframeManager
   },
   model: {
     prop: "value",

+ 189 - 0
report-ui/src/views/bigscreenDesigner/designer/components/multiIframeManager.vue

@@ -0,0 +1,189 @@
+<template>
+  <div class="multi-iframe-manager">
+    <el-button
+      type="primary"
+      size="mini"
+      icon="el-icon-plus"
+      plain
+      @click="handleAddClick"
+    >
+      新增地址
+    </el-button>
+    <el-table :data="formData" style="width: 100%">
+      <el-table-column prop="name" label="名称" width="80">
+        <template slot-scope="scope">
+          <span>{{ scope.row.name || `地址${scope.$index + 1}` }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column prop="url" label="地址" show-overflow-tooltip>
+        <template slot-scope="scope">
+          <el-tooltip :content="scope.row.url" placement="top">
+            <span class="url-text">{{ scope.row.url }}</span>
+          </el-tooltip>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" width="150" align="center">
+        <template slot-scope="scope">
+          <span
+            class="editor"
+            @click="handleEditorClick(scope.$index, scope.row)"
+          >
+            <i class="el-icon-edit" /> 编辑
+          </span>
+          <span
+            class="delete"
+            @click="handleDeleteClick(scope.$index, scope.row)"
+          >
+            <i class="el-icon-delete" /> 删除
+          </span>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <el-dialog
+      :title="isAddFlag ? '新增iframe地址' : '修改iframe地址'"
+      :visible.sync="dialogVisible"
+      width="30%"
+      :before-close="handleClose"
+    >
+      <el-form ref="iframeForm" :model="iframeForm" label-width="80px">
+        <el-form-item label="名称">
+          <el-input v-model="iframeForm.name" size="mini" placeholder="给iframe地址一个名称"></el-input>
+        </el-form-item>
+        <el-form-item label="地址" required>
+          <el-input v-model="iframeForm.url" size="mini" placeholder="请输入iframe地址"></el-input>
+        </el-form-item>
+      </el-form>
+      <span slot="footer" class="dialog-footer">
+        <el-button size="mini" @click="handleClose">取 消</el-button>
+        <el-button size="mini" type="primary" @click="handleSaveClick">确 定</el-button>
+      </span>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'MultiIframeManager',
+  model: {
+    prop: 'formData',
+    event: 'input'
+  },
+  props: {
+    formData: {
+      type: Array,
+      default: () => []
+    }
+  },
+  data() {
+    return {
+      isAddFlag: true, // true 新增, false 编辑
+      indexEditor: -1, // 编辑第几个数据
+      iframeForm: {
+        name: '',
+        url: ''
+      },
+      dialogVisible: false // 显示弹窗
+    }
+  },
+  methods: {
+    // 弹出框关闭
+    handleClose() {
+      this.dialogVisible = false
+      this.resetForm()
+    },
+    // 重置表单
+    resetForm() {
+      this.iframeForm = {
+        name: '',
+        url: ''
+      }
+    },
+    // 新增按钮
+    handleAddClick() {
+      this.resetForm()
+      this.isAddFlag = true
+      this.dialogVisible = true
+    },
+    // 修改按钮
+    handleEditorClick(index, row) {
+      this.isAddFlag = false
+      this.iframeForm = JSON.parse(JSON.stringify(row))
+      this.dialogVisible = true
+      this.indexEditor = index
+    },
+    // 删除
+    handleDeleteClick(index) {
+      this.$confirm('确定要删除该地址吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        this.formData.splice(index, 1)
+        this.$emit('input', this.formData)
+        this.$emit('change', this.formData)
+        this.$message({
+          type: 'success',
+          message: '删除成功!'
+        })
+      }).catch(() => {})
+    },
+    // 确定
+    handleSaveClick() {
+      if (!this.iframeForm.url) {
+        this.$message.error('请输入iframe地址')
+        return
+      }
+      
+      const obj = JSON.parse(JSON.stringify(this.iframeForm))
+      
+      // 如果没有设置名称,则使用默认名称
+      if (!obj.name) {
+        obj.name = this.isAddFlag 
+          ? `地址${this.formData.length + 1}` 
+          : `地址${this.indexEditor + 1}`
+      }
+      
+      if (this.isAddFlag) {
+        // 新增
+        this.formData.push(obj)
+        this.dialogVisible = false
+      } else {
+        // 编辑
+        this.formData[this.indexEditor] = obj
+        this.dialogVisible = false
+      }
+      
+      this.$emit('input', this.formData)
+      this.$emit('change', this.formData)
+      this.resetForm()
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.multi-iframe-manager {
+  margin-bottom: 20px;
+  
+  .editor,
+  .delete {
+    margin: 0 5px;
+    font-size: 12px;
+    color: #409EFF;
+    cursor: pointer;
+  }
+  
+  .delete {
+    color: #F56C6C;
+  }
+  
+  .url-text {
+    display: inline-block;
+    max-width: 150px;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+}
+</style> 

+ 14 - 0
report-ui/src/views/bigscreenDesigner/designer/linkageLogic.js

@@ -22,6 +22,11 @@ export const lickageParamsConfig = [
     code: 'widget-form-time',
     paramsKey: ['startTime','endTime']
   },
+  {
+    name: '日历',
+    code: 'widget-calendar',
+    paramsKey: ['date']
+  },
   {
     name: '柱图',
     code: 'widget-barchart',
@@ -53,6 +58,11 @@ export const lickageParamsConfig = [
     code: 'WidgetPieNightingaleRoseArea',
     paramsKey: ['name', 'value']
   },
+  {
+    name: '按钮',
+    code: 'widget-button',
+    paramsKey: [] //按钮需要的key来源于要联动的组件所需要的参数
+  },
 ]
 
 export const getOneConfigByCode = function (code) {
@@ -135,6 +145,10 @@ export const targetWidgetLinkageLogic = function (self) {
     if (item.index !== -1 && item.linkageArr.length) {
       item.linkageArr.some(obj => {
         if (obj.targetId === self.value.setup.widgetId) {
+          //如果当前对象是button按钮,需要把传递参数设置给他,方便点击的时候校验表单输入情况
+          if (self.value.setup.widgetCode === 'widget-button') {
+            self.setFormData(obj.paramsConfig); //把要校验的参数传给button表单
+          }
           self.hasLinkage = true
           busEvents.push({
             eventName: `bus_${obj.originId}_${obj.targetId}`,

+ 216 - 0
report-ui/src/views/bigscreenDesigner/designer/tools/configure/form/widget-button.js

@@ -0,0 +1,216 @@
+/*
+ * @Descripttion: 超链接文本
+ * @version:
+ * @Author: qianlishi
+ * @Date: 2021-08-29 07:03:58
+ * @LastEditors: qianlishi qianlishi@anji-plus.com
+ * @LastEditTime: 2023-01-11 13:16:21
+ */
+export const widgetButton = {
+  code: 'widget-button',
+  type: 'form',
+  tabName: '表单',
+  label: '按钮',
+  icon: 'iconanniu',
+  options: {
+    // 配置
+    setup: [
+      {
+        type: 'el-input-text',
+        label: '图层名称',
+        name: 'layerName',
+        required: false,
+        placeholder: '',
+        value: '按钮',
+      },
+      {
+        type: 'el-input-text',
+        label: '按钮文本',
+        name: 'text',
+        required: false,
+        placeholder: '',
+        value: '按钮',
+      },
+      {
+        type: 'el-input-number',
+        label: '字体字号',
+        name: 'fontSize',
+        required: false,
+        placeholder: '',
+        value: '16',
+      },
+      {
+        type: 'vue-color',
+        label: '字体颜色',
+        name: 'color',
+        required: false,
+        placeholder: '',
+        value: '#FAD400',
+      },
+      {
+        type: 'vue-color',
+        label: '字体背景',
+        name: 'backgroundColor',
+        required: false,
+        placeholder: '',
+        value: 'rgba(115,170,229,.5)',
+      },
+      {
+        type: 'el-input-number',
+        label: '字体间距',
+        name: 'letterSpacing',
+        required: false,
+        placeholder: '',
+        value: '0',
+      },
+      {
+        type: 'el-select',
+        label: '文字粗细',
+        name: 'fontWeight',
+        required: false,
+        placeholder: '',
+        selectOptions: [
+          { code: 'normal', name: '正常' },
+          { code: 'bold', name: '粗体' },
+          { code: 'bolder', name: '特粗体' },
+          { code: 'lighter', name: '细体' }
+        ],
+        value: 'normal'
+      },
+      {
+        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: 'isBorderRadius',
+        required: false,
+        placeholder: '',
+        value: 'false',
+      },
+      {
+        type: 'el-input-number',
+        label: '圆角边框设置',
+        name: 'borderRadius',
+        required: false,
+        placeholder: '单位px',
+        value: '10',
+      },
+     [
+       {
+         name: '组件联动',
+         list: [
+           {
+             type: 'componentLinkage',
+             label: '',
+             name: 'componentLinkage',
+             required: false,
+             value: []
+           }
+         ]
+       }
+     ]
+    ],
+    // 数据
+    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: 30000
+      },
+      {
+        type: 'el-button',
+        label: '静态数据',
+        name: 'staticData',
+        required: false,
+        placeholder: '',
+        relactiveDom: 'dataType',
+        relactiveDomValue: 'staticData',
+        value: [
+          { paramKey: 'key1', paramValue: 'value1'},
+          { paramKey: 'key2', paramValue: 'value2'},
+          { paramKey: 'key3', paramValue: 'value3'}
+        ],
+      },
+      {
+        type: 'dycustComponents',
+        label: '',
+        name: 'dynamicData',
+        required: false,
+        placeholder: '',
+        relactiveDom: 'dataType',
+        relactiveDomValue: 'dynamicData',
+        chartType: 'widget-button',
+        dictKey: 'TEXT_PROPERTIES',
+        value: '',
+      }
+    ],
+    // 坐标
+    position: [
+      {
+        type: 'el-input-number',
+        label: '左边距',
+        name: 'left',
+        required: false,
+        placeholder: '',
+        value: 0,
+      },
+      {
+        type: 'el-input-number',
+        label: '上边距',
+        name: 'top',
+        required: false,
+        placeholder: '',
+        value: 0,
+      },
+      {
+        type: 'el-input-number',
+        label: '宽度',
+        name: 'width',
+        required: false,
+        placeholder: '该容器在1920px大屏中的宽度',
+        value: 100,
+      },
+      {
+        type: 'el-input-number',
+        label: '高度',
+        name: 'height',
+        required: false,
+        placeholder: '该容器在1080px大屏中的高度',
+        value: 40,
+      },
+    ],
+  }
+}

+ 12 - 0
report-ui/src/views/bigscreenDesigner/designer/tools/configure/form/widget-form-time.js

@@ -18,6 +18,18 @@ export const widgetFormTime = {
         placeholder: '',
         value: '时间筛选器',
       },
+      {
+        type: 'el-select',
+        label: '日期类型',
+        name: 'dateType',
+        required: false,
+        placeholder: '',
+        selectOptions: [
+          { code: 'datetimerange', name: 'yyyy-MM-dd HH:mm:ss' },
+          { code: 'daterange', name: 'yyyy-MM-dd' },
+        ],
+        value: 'datetimerange',
+      },
       {
         type: 'vue-color',
         label: '背景',

+ 20 - 0
report-ui/src/views/bigscreenDesigner/designer/tools/configure/form/widget-select.js

@@ -46,6 +46,26 @@ export const widgetSelect = {
         placeholder: '',
         value: 'rgba(115,170,229,.5)',
       },
+      {
+        type: 'el-input-text',
+        label: '默认值',
+        name: 'defaultValue',
+        required: false,
+        placeholder: '',
+        value: '',
+      },
+      {
+        type: 'el-select',
+        label: '模式',
+        name: 'model',
+        required: false,
+        placeholder: '',
+        selectOptions: [
+          { code: '1', name: '下拉框' },
+          { code: '2', name: 'TAB按钮' }
+        ],
+        value: '1'
+      },
 /*      {
         type: 'el-select',
         label: '触发事件',

+ 250 - 0
report-ui/src/views/bigscreenDesigner/designer/tools/configure/texts/widget-calendar.js

@@ -0,0 +1,250 @@
+/*
+ * @Descripttion: 日历组件json
+ * @version:
+ * @Author: lishuaiwu
+ * @Date: 2025-06-05 13:44:32
+ */
+export const widgetCalendar = {
+  code: 'widget-calendar',
+  type: 'text',
+  tabName: '文本栏',
+  label: '日历',
+  icon: 'iconbiaoge',
+  options: {
+    // 配置
+    setup: [
+      {
+        type: 'el-input-text',
+        label: '图层名称',
+        name: 'layerName',
+        required: false,
+        placeholder: '',
+        value: '日历',
+      },
+      [
+        {
+          name: '日历样式',
+          list: [
+            {
+              type: 'vue-color',
+              label: '边框线颜色',
+              name: 'borderColor',
+              required: false,
+              placeholder: '',
+              value: 'rgb(221, 221, 221)'
+            },
+            {
+              type: 'vue-color',
+              label: '头部背景颜色',
+              name: 'headerBackground',
+              required: false,
+              placeholder: '',
+              value: 'rgb(10, 115, 255)'
+            },
+            {
+              type: 'vue-color',
+              label: '头部文字颜色',
+              name: 'headerTextColor',
+              required: false,
+              placeholder: '',
+              value: 'rgb(255, 255, 255)'
+            },
+            {
+              type: 'vue-color',
+              label: '头部按钮颜色',
+              name: 'headerBtnBackground',
+              required: false,
+              placeholder: '',
+              value: 'rgb(10, 115, 255)'
+            },
+            {
+              type: 'vue-color',
+              label: '头部按钮文字',
+              name: 'headerBtnTextColor',
+              required: false,
+              placeholder: '',
+              value: 'rgb(255, 255, 255)'
+            },
+            {
+              type: 'vue-color',
+              label: '星期背景色',
+              name: 'weekBackground',
+              required: false,
+              placeholder: '',
+              value: 'rgb(10, 39, 50)'
+            },
+            {
+              type: 'vue-color',
+              label: '星期文字颜色',
+              name: 'weekTextColor',
+              required: false,
+              placeholder: '',
+              value: 'rgb(255, 255, 255)'
+            },
+            {
+              type: 'vue-color',
+              label: '日期背景色',
+              name: 'dayBackground',
+              required: false,
+              placeholder: '',
+              value: 'rgb(0, 59, 81)'
+            },
+            {
+              type: 'vue-color',
+              label: '日期文字颜色',
+              name: 'dayTextColor',
+              required: false,
+              placeholder: '',
+              value: 'rgb(255, 255, 255)'
+            },
+            {
+              type: 'vue-color',
+              label: '当日背景色',
+              name: 'todayBackground',
+              required: false,
+              placeholder: '',
+              value: 'rgba(0, 59, 81, .5)'
+            },
+            {
+              type: 'vue-color',
+              label: '当日文字颜色',
+              name: 'todayTextColor',
+              required: false,
+              placeholder: '',
+              value: 'rgb(0, 0, 0)'
+            },{
+              type: 'vue-color',
+              label: '选中背景色',
+              name: 'selectedBackground',
+              required: false,
+              placeholder: '',
+              value: 'rgb(11, 180, 241)'
+            },
+            {
+              type: 'vue-color',
+              label: '选中文字颜色',
+              name: 'selectedTextColor',
+              required: false,
+              placeholder: '',
+              value: 'rgb(0, 0, 0)'
+            },
+            {
+              type: 'el-switch',
+              label: '按钮显隐',
+              name: 'isButton',
+              required: false,
+              placeholder: '',
+              value: true,
+            },
+          ]
+        }
+      ],
+      [{
+        name: '组件联动',
+        list: [
+          {
+            type: 'componentLinkage',
+            label: '',
+            name: 'componentLinkage',
+            required: false,
+            value: []
+          }
+        ]
+      }]
+    ],
+    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: 600000
+      },
+      {
+        type: 'el-button',
+        label: '静态数据',
+        name: 'staticData',
+        required: false,
+        placeholder: '',
+        relactiveDom: 'dataType',
+        relactiveDomValue: 'staticData',
+        value: [
+          {
+            "date": "2025-06-01",
+            "data": '2单'
+          },
+          {
+            "date": "2025-06-02",
+            "data": '10单'
+          }
+        ],
+      },
+      {
+        type: 'dycustComponents',
+        label: '',
+        name: 'dynamicData',
+        required: false,
+        placeholder: '',
+        relactiveDom: 'dataType',
+        relactiveDomValue: 'dynamicData',
+        chartType: 'widget-calendar',
+        dictKey: 'CALENDAR_PROPERTIES',
+        value: '',
+      }
+    ],
+    position: [
+      {
+        type: 'el-input-number',
+        label: '左边距',
+        name: 'left',
+        required: false,
+        placeholder: '',
+        value: 0,
+      },
+      {
+        type: 'el-input-number',
+        label: '上边距',
+        name: 'top',
+        required: false,
+        placeholder: '',
+        value: 0,
+      },
+      {
+        type: 'el-input-number',
+        label: '宽度',
+        name: 'width',
+        required: false,
+        placeholder: '该容器在1920px大屏中的宽度',
+        value: 600,
+      },
+      {
+        type: 'el-input-number',
+        label: '高度',
+        name: 'height',
+        required: false,
+        placeholder: '该容器在1080px大屏中的高度',
+        value: 400,
+      },
+    ]
+  }
+}

+ 39 - 5
report-ui/src/views/bigscreenDesigner/designer/tools/configure/texts/widget-iframe.js

@@ -24,12 +24,46 @@ export const widgetIframe = {
         value: 'iframe',
       },
       {
-        type: 'el-input-text',
-        label: '地址',
-        name: 'iframeAdress',
+        type: 'el-select',
+        label: '切换动画',
+        name: 'transitionEffect',
         required: false,
-        placeholder: '',
-        value: 'https://ajreport.beliefteam.cn/index.html',
+        placeholder: '请选择切换动画效果',
+        selectOptions: [
+          {name: '无动画', code: 'none'},
+          {name: '淡入淡出', code: 'fade'},
+          {name: '滑动', code: 'slide'},
+          {name: '缩放', code: 'zoom'}
+        ],
+        value: 'none',
+      },
+      {
+        type: 'el-input-number',
+        label: '切换间隔(秒)',
+        name: 'autoSwitchInterval',
+        required: false,
+        placeholder: '0表示不自动切换',
+        value: 0,
+      },
+      {
+        type: 'multiIframeManager',
+        label: 'iframe地址管理',
+        name: 'iframeUrls',
+        required: false,
+        value: [
+          {
+            name: '百度地图',
+            url: 'https://map.baidu.com/'
+          },
+          {
+            name: 'gitee',
+            url: 'https://gitee.com/'
+          },
+          {
+            name: 'deepseek',
+            url: 'https://chat.deepseek.com/'
+          }
+        ],
       },
     ],
     // 数据

+ 8 - 0
report-ui/src/views/bigscreenDesigner/designer/tools/configure/texts/widget-table.js

@@ -26,6 +26,14 @@ export const widgetTable = {
         {
           name: '表格设置',
           list: [
+              {
+                type: 'el-switch',
+                label: '显示导出按钮',
+                name: 'showExportButton',
+                required: false,
+                placeholder: '',
+                value: false
+              },
             {
               type: 'el-input-number',
               label: '显示行数',

+ 44 - 0
report-ui/src/views/bigscreenDesigner/designer/tools/configure/texts/widget-text.js

@@ -98,6 +98,50 @@ export const widgetText = {
         ],
         value: 'center'
       },
+      {
+        type: 'el-input-number',
+        label: '行高',
+        name: 'lineHeight',
+        required: false,
+        placeholder: '',
+        value: '40',
+      },
+      {
+        type: 'el-input-number',
+        label: '边框宽度',
+        name: 'borderWidth',
+        required: false,
+        placeholder: '',
+        value: '0',
+      },
+      {
+        type: 'vue-color',
+        label: '边框颜色',
+        name: 'borderColor',
+        required: false,
+        placeholder: '',
+        value: 'rgba(255,255,255,0)',
+      },
+      {
+        type: 'el-select',
+        label: '边框类型',
+        name: 'borderStyle',
+        required: false,
+        placeholder: '',
+        selectOptions: [
+          { code: 'solid', name: '实线' },
+          { code: 'dashed', name: '虚线' },
+        ],
+        value: 'solid',
+      },
+      {
+        type: 'el-input-number',
+        label: '边框圆角',
+        name: 'borderRadius',
+        required: false,
+        placeholder: '',
+        value: '0',
+      },
       {
         type: 'el-switch',
         label: '识别换行符',

+ 215 - 0
report-ui/src/views/bigscreenDesigner/designer/tools/configure/texts/widget-videoMonitor.js

@@ -0,0 +1,215 @@
+/*
+ * @Descripttion: 监控视频组件json
+ * @version:
+ * @Author: lishuaiwu
+ * @Date: 2025-06-06 11:35:23
+ */
+export const widgetVideoMonitor = {
+  code: 'widget-videoMonitor',
+  type: 'text',
+  tabName: '文本栏',
+  label: '监控视频',
+  icon: 'iconshipin',
+  options: {
+    // 配置
+    setup: [
+      {
+        type: 'el-input-text',
+        label: '图层名称',
+        name: 'layerName',
+        required: false,
+        placeholder: '',
+        value: '监控视频',
+      },
+      {
+        type: 'vue-color',
+        label: '背景色',
+        name: 'videoBgColor',
+        require: false,
+        placeholder: '',
+        value: 'rgba(10, 115, 255, .8)',
+      },
+      {
+        type: 'el-input-number',
+        label: '圆角',
+        name: 'borderRadius',
+        require: false,
+        placeholder: '',
+        value: 10
+      },
+      [
+        {
+          name: '列表样式',
+          list: [
+            {
+              type: 'vue-color',
+              label: '背景色',
+              name: 'videoListBgColor',
+              require: false,
+              placeholder: '',
+              value: 'rgb(0, 59, 81)',
+            },
+            {
+              type: 'vue-color',
+              label: '文字颜色',
+              name: 'videoListFontColor',
+              require: false,
+              placeholder: '',
+              value: 'rgb(255, 255, 255)',
+            },
+            {
+              type: 'el-input-number',
+              label: '字号',
+              name: 'fontSize',
+              require: false,
+              placeholder: '',
+              value: 14
+            },
+            {
+              type: 'el-input-number',
+              label: '最小宽度',
+              name: 'minWidth',
+              require: false,
+              placeholder: '',
+              value: 200
+            },
+            {
+              type: 'el-input-number',
+              label: '最大宽度',
+              name: 'maxWidth',
+              require: false,
+              placeholder: '',
+              value: 500
+            },
+            {
+              type: 'vue-color',
+              label: '选中背景色',
+              name: 'activeBgColor',
+              require: false,
+              placeholder: '',
+              value: 'rgba(10, 39, 50, 1)',
+            },
+            {
+              type: 'vue-color',
+              label: '选中文字颜色色',
+              name: 'activeFontColor',
+              require: false,
+              placeholder: '',
+              value: 'rgba(255, 255, 255, 1)',
+            },
+          ]
+        }
+      ],
+      {
+        type: 'el-switch',
+        label: '自动播放首视频',
+        name: 'isAutoPlayFirst',
+        required: false,
+        placeholder: '',
+        value: false,
+      },
+      {
+        type: 'el-switch',
+        label: '显示播放列表',
+        name: 'isShowList',
+        required: false,
+        placeholder: '',
+        value: true,
+      },
+    ],
+    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: 600000
+      },
+      {
+        type: 'el-button',
+        label: '静态数据',
+        name: 'staticData',
+        required: false,
+        placeholder: '',
+        relactiveDom: 'dataType',
+        relactiveDomValue: 'staticData',
+        value: [
+          {
+            "name": "测试视频-mp4",
+            "src": 'https://www.w3cschool.cn/statics/demosource/movie.mp4'
+          },
+          {
+            "name": "测试视频-flv",
+            "src": 'https://sf1-cdn-tos.huoshanstatic.com/obj/media-fe/xgplayer_doc_video/flv/xgplayer-demo-360p.flv'
+          }
+        ],
+      },
+      {
+        type: 'dycustComponents',
+        label: '',
+        name: 'dynamicData',
+        required: false,
+        placeholder: '',
+        relactiveDom: 'dataType',
+        relactiveDomValue: 'dynamicData',
+        chartType: 'widget-videoMonitor',
+        dictKey: 'MONITOR_PROPERTIES',
+        value: '',
+      }
+    ],
+    position: [
+      {
+        type: 'el-input-number',
+        label: '左边距',
+        name: 'left',
+        required: false,
+        placeholder: '',
+        value: 0,
+      },
+      {
+        type: 'el-input-number',
+        label: '上边距',
+        name: 'top',
+        required: false,
+        placeholder: '',
+        value: 0,
+      },
+      {
+        type: 'el-input-number',
+        label: '宽度',
+        name: 'width',
+        required: false,
+        placeholder: '该容器在1920px大屏中的宽度',
+        value: 600,
+      },
+      {
+        type: 'el-input-number',
+        label: '高度',
+        name: 'height',
+        required: false,
+        placeholder: '该容器在1080px大屏中的高度',
+        value: 400,
+      },
+    ]
+  }
+}

+ 6 - 0
report-ui/src/views/bigscreenDesigner/designer/tools/main.js

@@ -12,10 +12,13 @@ import {widgetMarquee} from "./configure/texts/widget-marquee"
 import {widgetHref} from "./configure/texts/widget-href"
 import {widgetTime} from "./configure/texts/widget-time"
 import {widgetImage} from "./configure/texts/widget-image"
+import {widgetButton} from "./configure/form/widget-button"
 import {widgetSliders} from "./configure/texts/widget-slider"
 import {widgetVideo} from "./configure/texts/widget-video"
+import {widgetVideoMonitor} from "./configure/texts/widget-videoMonitor"
 import {widgetTable} from "./configure/texts/widget-table"
 import {widgetIframe} from "./configure/texts/widget-iframe"
+import {widgetCalendar} from "./configure/texts/widget-calendar"
 import {widgetUniversal} from "./configure/widget-universal"
 import {widgetBarchart} from "./configure/barCharts/widget-barchart"
 import {widgetGradientBarchart} from "./configure/barCharts/widget-gradient-barchart"
@@ -64,10 +67,13 @@ export const widgetTool = [
   widgetHref,
   widgetTime,
   widgetImage,
+  widgetButton,
   //  widgetSliders,
   widgetVideo,
+  widgetVideoMonitor,
   widgetTable,
   widgetIframe,
+  widgetCalendar,
   //  widgetUniversal,
   widgetBarchart,
   widgetGradientBarchart,

+ 163 - 0
report-ui/src/views/bigscreenDesigner/designer/widget/form/widgetButton.vue

@@ -0,0 +1,163 @@
+<!--
+ * @Author: lide1202@hotmail.com
+ * @Date: 2021-3-13 11:04:24
+ * @Last Modified by:   lide1202@hotmail.com
+ * @Last Modified time: 2021-3-13 11:04:24
+ !-->
+<template >
+  <div class="button">
+      <el-button
+        :style="{
+                      position:styleColor.position,
+                      width: styleColor.width,
+                      height: styleColor.height,
+                      left: styleColor.left,
+                      right: styleColor.right,
+                      top: styleColor.top,
+                      color: styleColor.color,
+                      borderRadius: styleColor.borderRadius,
+                      'line-height': styleColor['line-height'],
+                      'text-align': styleColor['text-align'],
+                      'font-weight': styleColor['font-weight'],
+                      'font-size': styleColor['font-size'],
+                      'letter-spacing': styleColor['letter-spacing'],
+                      'background-color': styleColor['background-color'],
+                      display: styleColor.display
+               }"
+        @click="click"
+      >
+        {{styleColor.text}}
+      </el-button>
+  </div>
+
+</template>
+
+<script>
+import {
+  originWidgetLinkageLogic,
+  targetWidgetLinkageLogic,
+} from "@/views/bigscreenDesigner/designer/linkageLogic";
+export default {
+  name: "WidgetButton",
+  components: {},
+  props: {
+    value: Object,
+    ispreview: Boolean,
+    widgetIndex: {
+      type: Number,
+      default: 0,
+    }, // 当前组件,在工作区变量widgetInWorkbench中的索引
+  },
+  data() {
+    return {
+      options: {},
+      formData: {} , //要提交的参数表单
+    };
+  },
+  computed: {
+    transStyle() {
+      return this.objToOne(this.options);
+    },
+    styleColor() {
+      return {
+        position: this.ispreview ? "relative" : "static",
+        color: this.transStyle.color,
+        text: this.transStyle.text || "按钮",
+        width: this.transStyle.width + "px",
+        height: this.transStyle.height + "px",
+        left: this.transStyle.left + "px",
+        top: this.transStyle.top + "px",
+        right: this.transStyle.right + "px",
+        borderRadius: this.transStyle.isBorderRadius ? this.transStyle.borderRadius +"px" :"0px",
+        "text-align": this.transStyle.textAlign,
+        "font-weight": this.transStyle.fontWeight || "600",
+        "font-size": this.transStyle.fontSize + "px" || "12px",
+        "letter-spacing": this.transStyle.letterSpacing + "em",
+        "background-color": this.transStyle.backgroundColor || "rgba(115,170,229,.5)",
+        display:
+          this.transStyle.hideLayer === undefined
+            ? "block"
+            : this.transStyle.hideLayer ? "none" : "block",
+      };
+    },
+    allComponentLinkage() {
+      return this.$store.state.designer.allComponentLinkage;
+    },
+  },
+  watch: {
+    value: {
+      handler(val) {
+        this.options = val;
+      },
+      deep: true,
+    },
+  },
+  mounted() {
+    this.options = this.value;
+    this.optionsSetup = this.value.setup;
+    this.optionsData = this.value.data;
+    this.optionsStyle = this.value.position;
+    targetWidgetLinkageLogic(this); // 联动-目标组件逻辑
+  },
+  methods: {
+    //设置表单Key
+    setFormData(paramConfig){
+      console.log("paramConfig:" + paramConfig);
+      paramConfig.forEach(item =>{
+        this.formData[item.targetKey] = "";
+      });
+    },
+    click() {
+      console.log(this.formData);
+      let formDataKey = Object.keys(this.formData);
+      let needInputAll = false;
+      for(let index in formDataKey){
+        if(this.formData[formDataKey[index]] === null || this.formData[formDataKey[index]] === "" ){
+          needInputAll = true;
+          break;
+        }
+      }
+      if(needInputAll){
+        this.$message.error("请填写所有必填字段");
+        return;
+      }
+      originWidgetLinkageLogic(this, true, {
+        currentData: this.formData,
+      }); // 联动-源组件逻辑
+    },
+
+    setOptionsData(e, paramsConfig) {
+      let _this = this;
+      console.log("ces", e);
+      console.log("ces", paramsConfig);
+      const optionsData = this.optionsData; // 数据类型 静态 or 动态
+      // 联动接收者逻辑开始
+      optionsData.dynamicData = optionsData.dynamicData || {}; // 兼容 dynamicData undefined
+      const myDynamicData = optionsData.dynamicData;
+      clearInterval(this.flagInter); // 不管咋,先干掉上一次的定时任务,避免多跑
+      if (
+        e &&
+        optionsData.dataType !== "staticData" &&
+        Object.keys(myDynamicData.contextData).length
+      ) {
+        const keyArr = Object.keys(myDynamicData.contextData);
+        paramsConfig.forEach((conf) => {
+          if (keyArr.includes(conf.targetKey)) {
+            myDynamicData.contextData[conf.targetKey] = e[conf.originKey];
+            _this.formData[conf.targetKey] = e[conf.originKey];    //把参数设置到FormData
+          }
+        });
+      }
+      //这里,button按钮是要通过【按钮】联动触发其他组件,本身没有要展示的动态数据,所以无需执行 dynamicDataFn
+      // // 联动接收者逻辑结束
+      // optionsData.dataType == "staticData"
+      //   ? this.staticDataFn(optionsData.staticData)
+      //   : this.dynamicDataFn(optionsData.dynamicData, optionsData.refreshTime);
+    }
+  },
+};
+</script>
+
+<style scoped lang="scss">
+
+</style>

+ 9 - 3
report-ui/src/views/bigscreenDesigner/designer/widget/form/widgetFormTime.vue

@@ -8,9 +8,9 @@
   <el-date-picker
     :style="styleObj"
     v-model="timeValue"
-    value-format="yyyy-MM-dd HH:mm:ss"
+    :value-format="valueFormat"
     :picker-options="datetimeRangePickerOptions"
-    type="datetimerange"
+    :type="dateType"
     @[eventChange]="change"
   />
 </template>
@@ -119,6 +119,12 @@ export default {
     eventChange() {
       return "change";
     },
+    dateType() {
+      return this.optionsSetup.dateType || 'datetimerange';
+    },
+    valueFormat() {
+      return this.dateType === 'daterange' ? 'yyyy-MM-dd' : 'yyyy-MM-dd HH:mm:ss';
+    },
     allComponentLinkage() {
       return this.$store.state.designer.allComponentLinkage;
     },
@@ -153,7 +159,7 @@ export default {
 };
 </script>
 <style scoped lang="scss">
- .el-select {
+  .el-select {
   height: 100%;
 
   .el-input {

+ 42 - 0
report-ui/src/views/bigscreenDesigner/designer/widget/form/widgetSelect.vue

@@ -1,5 +1,16 @@
 <template>
+  <el-radio-group
+    class="el-radio-button-custom"
+    ref="select"
+    v-model="selectValue"
+    size="medium"
+    :style="styleObj"
+     @[eventChange]="(val) => change(val, options.find(item => item.value === val))"
+    v-if="model === '2'">
+    <el-radio-button v-for="item in options" :key="item.value" :label="item.value">{{ item.label }}</el-radio-button>
+  </el-radio-group>
   <anji-select
+    v-else
     class="select"
     ref="select"
     :style="styleObj"
@@ -10,6 +21,7 @@
     option="value"
     @[eventChange]="(val, item) => change(val, item)"
   />
+  
 </template>
 <script>
 import {
@@ -33,6 +45,7 @@ export default {
       optionsData: {},
       optionsSetup: {},
       options: {},
+      model: '1'
     };
   },
   computed: {
@@ -84,6 +97,9 @@ export default {
     },
     setOptions() {
       const optionsData = this.optionsData;
+      const optionsSetup = this.optionsSetup;
+      this.selectValue = optionsSetup.defaultValue;
+      this.model = optionsSetup.model;
       return optionsData.dataType === "staticData"
         ? this.staticData(optionsData.staticData)
         : this.dynamicDataFn(optionsData.dynamicData, optionsData.refreshTime);
@@ -137,4 +153,30 @@ export default {
   }
 }
 
+::v-deep.el-radio-button-custom {
+  display: flex;
+  flex-direction: row;
+  .el-radio-button {
+    height: 100%!important;
+    flex: 1;
+  }
+  .el-radio-button__inner {
+    padding: 0!important;
+    height: 100%!important;
+    width: 100%!important;
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    justify-content: center;
+    font-size: 16px;
+    background: inherit;
+    color: inherit;
+  }
+  .el-radio-button__orig-radio:checked+.el-radio-button__inner {
+    background-color: #40a0ffb3!important;
+    color: #ffffff!important;
+    border: 1px solid #ffffff!important;
+  }
+}
+
 </style>

+ 6 - 0
report-ui/src/views/bigscreenDesigner/designer/widget/temp.vue

@@ -12,12 +12,15 @@
 <script>
 import widgetHref from "./texts/widgetHref.vue";
 import widgetText from "./texts/widgetText.vue";
+import widgetButton from "./form/widgetButton.vue";
 import WidgetMarquee from "./texts/widgetMarquee.vue";
 import widgetTime from "./texts/widgetTime.vue";
 import widgetImage from "./texts/widgetImage.vue";
 import widgetSlider from "./texts/widgetSlider.vue";
 import widgetVideo from "./texts/widgetVideo.vue";
+import widgetVideoMonitor from "./texts/widgetVideoMonitor.vue";
 import WidgetIframe from "./texts/widgetIframe.vue";
+import widgetCalendar from "./texts/widgetCalendar.vue";
 import widgetBarchart from "./bar/widgetBarchart.vue";
 import widgetLinechart from "./line/widgetLinechart.vue";
 import widgetBarlinechart from "./barline/widgetBarlinechart";
@@ -61,6 +64,7 @@ export default {
   components: {
     widgetHref,
     widgetText,
+    widgetButton,
     widgetBorder,
     widgetDecorateFlowLine,
     widgetDecoration,
@@ -69,7 +73,9 @@ export default {
     widgetImage,
     widgetSlider,
     widgetVideo,
+    widgetVideoMonitor,
     WidgetIframe,
+    widgetCalendar,
     widgetBarchart,
     widgetGradientColorBarchart,
     widgetLinechart,

+ 401 - 0
report-ui/src/views/bigscreenDesigner/designer/widget/texts/widgetCalendar.vue

@@ -0,0 +1,401 @@
+<template>
+  <div class="calendar-container full-box" :style="styleObj">
+    <div class="calendar-header" :style="headerStyle">
+      <button class="nav-button" :style="headerBtnStyle" @click="prevMonth" v-if="showButton">&lt;</button>
+      <div class="current-month-title">{{ currentMonthTitle }}</div>
+      <button class="nav-button" :style="headerBtnStyle" @click="nextMonth" v-if="showButton">&gt;</button>
+    </div>
+    
+    <div class="calendar-month">
+      <div class="weekdays" :style="weekStyle">
+        <div class="weekday" v-for="day in weekdays" :key="day">{{ day }}</div>
+      </div>
+      <div class="days-grid" :style="dayStyle">
+        <div 
+          class="day-cell"
+          v-for="day in currentMonthDays"
+          :key="day.date"
+          :class="{
+            'out-of-range': day.isOutOfRange,
+            'current-month': day.isCurrentMonth,
+            'today': day.isToday,
+            'selected': day.date === selectDayStr
+          }"
+          :style="cellStyle(day.isToday, day.date === selectDayStr)"
+          @click="selectDay(day)"
+        >
+          <slot name="day" :day="day">
+            <div class="day-number">{{ day.day }}</div>
+            <div class="day-content" v-html="text[day.date] || ''"></div>
+          </slot>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import moment from 'moment'
+import { targetWidgetLinkageLogic, originWidgetLinkageLogic } from "@/views/bigscreenDesigner/designer/linkageLogic";
+
+export default {
+  name: 'widgetCalendar',
+  props: {
+    value: Object,
+    ispreview: Boolean,
+    widgetIndex: {
+      type: Number,
+      default: 0,
+    },
+  },
+  data() {
+    return {
+      weekdays: ['日', '一', '二', '三', '四', '五', '六'],
+      currentMonth: moment().startOf('month'),
+      startDate: moment().format('YYYY-MM-DD'),
+      endDate: moment().format('YYYY-MM-DD'),
+      options: {},
+      optionsSetup: {},
+      optionsPosition: {},
+      optionsData: {},
+
+      text: '',
+      selectDayStr: moment().format('YYYY-MM-DD')
+    }
+  },
+  computed: {
+    momentStartDate() {
+      return moment(this.startDate, 'YYYY-MM-DD').startOf('day')
+    },
+    momentEndDate() {
+      return moment(this.endDate, 'YYYY-MM-DD').endOf('day')
+    },
+    showButton () {
+      return this.optionsSetup.isButton;
+    },
+    styleObj() {
+      const allStyle = this.optionsPosition;
+      return {
+        position: this.ispreview ? "absolute" : "static",
+        width: allStyle.width + "px",
+        height: allStyle.height + "px",
+        left: allStyle.left + "px",
+        top: allStyle.top + "px",
+        background: this.optionsSetup.tableBgColor,
+        borderColor: this.optionsSetup.borderColor
+      };
+    },
+    headerStyle () {
+      return {
+        background: this.optionsSetup.headerBackground,
+        color: this.optionsSetup.headerTextColor,
+        justifyContent: this.optionsSetup.isButton ? 'space-between' : 'center'
+
+      };
+    },
+    headerBtnStyle () {
+      return {
+        background: this.optionsSetup.headerBtnBackground,
+        color: this.optionsSetup.headerBtnTextColor
+      };
+    },
+    weekStyle () {
+      return {
+        background: this.optionsSetup.weekBackground,
+        color: this.optionsSetup.weekTextColor
+      };
+    },
+    dayStyle () {
+      return {
+        backgroundColor: this.optionsSetup.borderColor
+      }
+    },
+    currentMonthTitle() {
+      return `${this.currentMonth.year()}年${this.currentMonth.month() + 1}月`
+    },
+    currentMonthDays() {
+      const daysInMonth = this.currentMonth.daysInMonth()
+      const firstDayOfMonth = moment(this.currentMonth).startOf('month')
+      const startingDayOfWeek = firstDayOfMonth.day()
+      
+      const days = []
+      
+      // 添加上个月的日期
+      const daysFromPrevMonth = startingDayOfWeek
+      const prevMonth = moment(this.currentMonth).subtract(1, 'month')
+      for (let i = 0; i < daysFromPrevMonth; i++) {
+        const day = prevMonth.daysInMonth() - daysFromPrevMonth + i + 1
+        const date = moment(prevMonth).date(day)
+        days.push(this.createDayObject(date, false))
+      }
+      
+      // 添加当前月的日期
+      for (let day = 1; day <= daysInMonth; day++) {
+        const date = moment(this.currentMonth).date(day)
+        days.push(this.createDayObject(date, true))
+      }
+      
+      // 添加下个月的日期
+      const totalCells = daysFromPrevMonth + daysInMonth
+      const remainingCells = totalCells % 7 === 0 ? 0 : 7 - (totalCells % 7)
+      const nextMonth = moment(this.currentMonth).add(1, 'month')
+      for (let i = 1; i <= remainingCells; i++) {
+        const date = moment(nextMonth).date(i)
+        days.push(this.createDayObject(date, false))
+      }
+      
+      return days
+    },
+    allComponentLinkage() {
+      return this.$store.state.designer.allComponentLinkage;
+    },
+  },
+  watch: {
+    value: {
+      handler(val) {
+        this.options = val;
+        this.optionsSetup = val.setup;
+        this.optionsPosition = val.position;
+        this.optionsData = val.data;
+        this.setOptionsData();
+      },
+      deep: true
+    }
+  },
+  mounted() {
+    this.options = this.value;
+    this.optionsSetup = this.value.setup;
+    this.optionsPosition = this.value.position;
+    this.optionsData = this.value.data;
+    targetWidgetLinkageLogic(this);
+    this.setOptionsData();
+  },
+  methods: {
+    selectDay (data) {
+      this.selectDayStr = data.date
+      const formTimeData = {}
+      formTimeData['date'] = data.date
+      originWidgetLinkageLogic(this, true, {
+        currentData: formTimeData,
+      }); // 联动-源组件逻辑
+    },
+    // 数据解析
+    setOptionsData(e, paramsConfig) {
+      const optionsData = this.optionsData; // 数据类型 静态 or 动态
+      // 联动接收者逻辑开始
+      optionsData.dynamicData = optionsData.dynamicData || {}; // 兼容 dynamicData undefined
+      const myDynamicData = optionsData.dynamicData;
+      clearInterval(this.flagInter); // 不管咋,先干掉上一次的定时任务,避免多跑
+      if (
+        e &&
+        optionsData.dataType !== "staticData" &&
+        Object.keys(myDynamicData.contextData).length
+      ) {
+        const keyArr = Object.keys(myDynamicData.contextData);
+        paramsConfig.forEach((conf) => {
+          if (keyArr.includes(conf.targetKey)) {
+            myDynamicData.contextData[conf.targetKey] = e[conf.originKey];
+          }
+        });
+      }
+      // 联动接收者逻辑结束
+      if (optionsData.dataType === "dynamicData") {
+        this.dynamicDataFn(optionsData.dynamicData, optionsData.refreshTime);
+      } else {
+        const data = this.objToOne(this.options).staticData
+        let obj = {}
+        data.forEach(e => {
+          obj[e.date] = e.data
+        });
+        this.text = obj
+      };
+    },
+    dynamicDataFn(val, refreshTime) {
+      if (!val) return;
+      if (this.ispreview) {
+        this.getEchartData(val);
+        this.flagInter = setInterval(() => {
+          this.getEchartData(val);
+        }, refreshTime);
+      } else {
+        this.getEchartData(val);
+      }
+    },
+    getEchartData(val) {
+      const data = this.queryEchartsData(val);
+      data.then(res => {
+        let obj = {}
+        res.forEach(e => {
+          obj[e.date] = e.data
+        });
+        this.text = obj
+        this.$forceUpdate();
+      });
+    },
+    cellStyle (isToday, isSelected) {
+      if (isSelected) {
+        return {
+          backgroundColor: this.optionsSetup.selectedBackground,
+          color: this.optionsSetup.selectedTextColor,
+        }
+      } else {
+        return {
+          backgroundColor: isToday ? this.optionsSetup.todayBackground : this.optionsSetup.dayBackground,
+          color: isToday ? this.optionsSetup.todayTextColor : this.optionsSetup.dayTextColor,
+        }
+      }
+    },
+    prevMonth() {
+      this.currentMonth = moment(this.currentMonth).subtract(1, 'month')
+    },
+    nextMonth() {
+      this.currentMonth = moment(this.currentMonth).add(1, 'month')
+    },
+    createDayObject(date, isCurrentMonth) {
+      const isToday = date.isSame(moment(), 'day')
+      const isBeforeRange = date.isBefore(this.momentStartDate, 'day')
+      const isAfterRange = date.isAfter(this.momentEndDate, 'day')
+      const isDisabled = isBeforeRange || isAfterRange
+      
+      return {
+        date: date.format('YYYY-MM-DD'),
+        year: date.year(),
+        month: date.month(), // 0-11
+        day: date.date(),   // 1-31
+        weekday: date.day(), // 0-6 (0是周日)
+        isCurrentMonth,
+        isOutOfRange: !isCurrentMonth,
+        isToday,
+        isBeforeRange,
+        isAfterRange,
+        isDisabled,
+        fullDate: date.toDate()
+      }
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.calendar-container {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  border: 1px solid #ddd;
+}
+
+.full-box {
+  height: 100%;
+  .calendar-month {
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    .days-grid {
+      flex: 1;
+    }
+  }
+}
+
+.calendar-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 10px;
+  background-color: rgb(10, 115, 255);
+  color: #ffffff;
+}
+
+.current-month-title {
+  font-size: 18px;
+  font-weight: bold;
+}
+
+.nav-button {
+  background: none;
+  border: 1px solid #ddd;
+  border-radius: 4px;
+  padding: 5px 10px;
+  cursor: pointer;
+  font-size: 16px;
+  color: #ffffff;
+  
+  &:hover {
+    opacity: .5;
+  }
+}
+
+.weekdays {
+  display: grid;
+  grid-template-columns: repeat(7, 1fr);
+  background-color: rgb(10, 39, 50);
+  color: #ffffff;
+}
+
+.weekday {
+  text-align: center;
+  padding: 8px 0;
+  font-weight: bold;
+}
+
+.days-grid {
+  display: grid;
+  grid-template-columns: repeat(7, 1fr);
+  grid-gap: 1px;
+  background-color: rgb(221, 221, 221);
+  flex: 1;
+}
+
+.day-cell {
+  min-height: 60px;
+  background-color: rgb(0, 59, 81);
+  padding: 5px;
+  position: relative;
+  width: calc(100% - 0px);
+  display: flex;
+  flex-direction: column;
+  cursor: pointer;
+}
+
+/* 当前月份的日期 */
+.day-cell.current-month {
+  background-color: rgb(0, 59, 81);
+  color: #ffffff;
+}
+
+.day-cell.selected {
+  background-color: rgb(11, 180, 241);
+}
+
+/* 非当前月份的日期 */
+.day-cell:not(.current-month) {
+ background-color: rgba(0, 59, 81, .5);
+  color: #ffffff;
+}
+
+/* 今天日期 */
+.day-cell.today {
+  background-color: #e6f7ff;
+  color: #000;
+}
+
+/* 日期范围外的单元格 */
+.day-cell.disabled-day {
+  background-color: #e0e0e0;
+  color: #999;
+  cursor: not-allowed;
+}
+
+.day-number {
+  font-weight: bold;
+  width: 30px;
+}
+.day-content {
+  text-align: left;
+  padding: 0 10px;
+  flex: 1;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+</style>

+ 198 - 22
report-ui/src/views/bigscreenDesigner/designer/widget/texts/widgetIframe.vue

@@ -1,10 +1,29 @@
 <template>
-  <iframe
-    :style="styleColor"
-    :src="this.toGetUrl(styleColor.iframeAdress)"
-    width="100%"
-    height="100%"
-  />
+  <div class="iframe-container" :style="containerStyle">
+    <transition :name="transitionName">
+      <iframe
+        v-if="hasIframes"
+        :ref="`iframe-${currentIframeIndex}`"
+        :key="`iframe-${currentIframeIndex}`"
+        :style="iframeStyle"
+        :src="processedUrl"
+        width="100%"
+        height="100%"
+        class="iframe"
+      />
+    </transition>
+    
+    <!-- 切换按钮,只在有多个iframe时显示 -->
+    <div v-if="hasMultipleUrls" class="iframe-switcher">
+      <div 
+        v-for="(iframe, index) in iframeUrls" 
+        :key="index"
+        :class="['switcher-dot', currentIframeIndex === index ? 'active' : '']"
+        @click="switchToIframe(index)"
+        :title="iframe.name || `地址${index+1}`">
+      </div>
+    </div>
+  </div>
 </template>
 
 <script>
@@ -17,61 +36,218 @@ export default {
   },
   data() {
     return {
-      options: {}
+      options: {},
+      currentIframeIndex: 0,
+      switchTimer: null,
+      processedUrl: ''
     };
   },
   computed: {
     transStyle() {
       return this.objToOne(this.options);
     },
-    styleColor() {
+    // 容器样式 - 分离样式属性
+    containerStyle() {
       return {
         position: this.ispreview ? "absolute" : "static",
         width: this.transStyle.width + "px",
         height: this.transStyle.height + "px",
         left: this.transStyle.left + "px",
         top: this.transStyle.top + "px",
-        right: this.transStyle.right + "px",
-        iframeAdress: this.transStyle.iframeAdress
+        right: this.transStyle.right + "px"
       };
+    },
+    // iframe样式
+    iframeStyle() {
+      return {
+        width: "100%",
+        height: "100%",
+        border: "none"
+      };
+    },
+    // 获取iframe地址数组
+    iframeUrls() {
+      return this.transStyle.iframeUrls || [];
+    },
+    // 判断是否有iframe地址配置
+    hasIframes() {
+      return this.iframeUrls.length > 0;
+    },
+    // 判断是否有多个iframe地址
+    hasMultipleUrls() {
+      return this.iframeUrls.length > 1;
+    },
+    // 当前显示的iframe地址
+    currentIframeUrl() {
+      if (!this.hasIframes || this.currentIframeIndex >= this.iframeUrls.length) {
+        return '';
+      }
+      return this.iframeUrls[this.currentIframeIndex].url;
+    },
+    // 根据选择的动画效果返回transition名称
+    transitionName() {
+      const effect = this.transStyle.transitionEffect;
+      if (!effect || effect === 'none') return '';
+      return effect;
     }
   },
   watch: {
     value: {
       handler(val) {
         this.options = val;
+        // 如果配置变化,重新设置自动切换
+        this.setupAutoSwitch();
+        // 更新处理后的URL
+        this.updateUrl();
+        
+        // 当iframe列表变更时,确保当前索引在有效范围内
+        if (this.hasIframes && this.currentIframeIndex >= this.iframeUrls.length) {
+          this.currentIframeIndex = 0;
+        }
       },
       deep: true
+    },
+    currentIframeUrl: {
+      handler() {
+        this.updateUrl();
+      }
     }
   },
   mounted() {
     this.options = this.value;
+    this.setupAutoSwitch();
+    this.updateUrl();
+  },
+  beforeDestroy() {
+    // 组件销毁前清除定时器
+    this.clearSwitchTimer();
   },
   methods: {
+    // 更新处理后的URL
+    updateUrl() {
+      if (this.currentIframeUrl) {
+        this.processedUrl = this.toGetUrl(this.currentIframeUrl);
+      } else {
+        this.processedUrl = '';
+      }
+    },
+    
     toGetUrl(url) {
-      if (url.indexOf('{') < 0 && url.indexOf('}' < 0)) {
-        return url
+      if (!url || (url.indexOf('{') < 0 && url.indexOf('}') < 0)) {
+        return url;
       }
-      const reg = /{[a-zA-Z0-9]*\}/g
-      const list = url.match(reg)
-      console.log(list)
-      let result = url
-      const query = this.$route.query
+      const reg = /{[a-zA-Z0-9]*\}/g;
+      const list = url.match(reg);
+      if (!list) return url;
+      
+      let result = url;
+      const query = this.$route.query;
       for (let i = 0; i < list.length; i++) {
-        const sub = list[i]
-        const key = sub.replace('{', '').replace('}', '')
-        result = result.replace(sub, query[key])
+        const sub = list[i];
+        const key = sub.replace('{', '').replace('}', '');
+        result = result.replace(sub, query[key] || '');
+      }
+      return result;
+    },
+    // 切换到指定的iframe
+    switchToIframe(index) {
+      if (index === this.currentIframeIndex || index >= this.iframeUrls.length) return;
+      
+      this.currentIframeIndex = index;
+    },
+    // 设置自动切换
+    setupAutoSwitch() {
+      this.clearSwitchTimer();
+      
+      const interval = this.transStyle.autoSwitchInterval;
+      if (interval && interval > 0 && this.iframeUrls.length > 1) {
+        this.switchTimer = setInterval(() => {
+          this.currentIframeIndex = (this.currentIframeIndex + 1) % this.iframeUrls.length;
+        }, interval * 1000);
+      }
+    },
+    // 清除自动切换定时器
+    clearSwitchTimer() {
+      if (this.switchTimer) {
+        clearInterval(this.switchTimer);
+        this.switchTimer = null;
       }
-      return result
     }
   }
 };
 </script>
 
 <style scoped lang="scss">
-iframe {
+.iframe-container {
+  position: relative;
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+}
+
+.iframe {
   width: 100%;
   height: 100%;
   border: none;
+  position: absolute;
+  top: 0;
+  left: 0;
+}
+
+/* 切换指示器样式 */
+.iframe-switcher {
+  position: absolute;
+  bottom: 10px;
+  left: 50%;
+  transform: translateX(-50%);
+  display: flex;
+  gap: 8px;
+  z-index: 10;
+}
+
+.switcher-dot {
+  width: 10px;
+  height: 10px;
+  border-radius: 50%;
+  background-color: rgba(255, 255, 255, 0.5);
+  cursor: pointer;
+  transition: all 0.3s;
+}
+
+.switcher-dot.active {
+  background-color: #fff;
+  box-shadow: 0 0 5px rgba(255, 255, 255, 0.8);
+}
+
+/* 淡入淡出动画 */
+.fade-enter-active, .fade-leave-active {
+  transition: opacity 0.5s;
+}
+.fade-enter, .fade-leave-to {
+  opacity: 0;
+}
+
+/* 滑动动画 */
+.slide-enter-active, .slide-leave-active {
+  transition: transform 0.5s;
+}
+.slide-enter {
+  transform: translateX(100%);
+}
+.slide-leave-to {
+  transform: translateX(-100%);
+}
+
+/* 缩放动画 */
+.zoom-enter-active, .zoom-leave-active {
+  transition: transform 0.5s, opacity 0.5s;
+}
+.zoom-enter {
+  transform: scale(0.8);
+  opacity: 0;
+}
+.zoom-leave-to {
+  transform: scale(1.2);
+  opacity: 0;
 }
 </style>

+ 396 - 7
report-ui/src/views/bigscreenDesigner/designer/widget/texts/widgetTable.vue

@@ -1,6 +1,6 @@
 <template>
   <div :style="styleObj">
-    <superslide v-if="hackReset" :options="options" class="txtScroll-top">
+    <superslide v-if="hackReset" :options="options" class="txtScroll-top" ref="superslide">
       <!--表头-->
       <div class="title">
         <div v-for="(item, index) in header" :key="index"
@@ -27,6 +27,48 @@
         </ul>
       </div>
     </superslide>
+
+    <!-- 添加导出按钮,仅在预览模式显示 -->
+    <div v-if="ispreview && optionsSetUp.showExportButton" class="export-button-container" :style="exportButtonContainerStyle">
+      <div class="export-button" :style="exportButtonStyle" @click="openExportDialog">
+        <span class="button-text">导出</span>
+      </div>
+    </div>
+    
+    <!-- 导出对话框 -->
+    <el-dialog
+      title="导出表格数据"
+      :visible.sync="exportDialogVisible"
+      :width="dialogWidth"
+      :fullscreen="isMobile"
+      :close-on-click-modal="false"
+      center
+      :modal-append-to-body="true"
+      append-to-body
+      :z-index="9999"
+      class="export-dialog">
+      <el-form :model="exportForm" label-width="100px" size="small">
+        <el-form-item label="文件名称">
+          <el-input v-model="exportForm.fileName" placeholder="请输入文件名称" clearable></el-input>
+        </el-form-item>
+        <el-form-item label="导出格式">
+          <el-radio-group v-model="exportForm.format">
+            <el-radio label="excel">Excel格式(.xlsx)</el-radio>
+            <el-radio label="csv">CSV格式(.csv)</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item>
+          <div class="export-info">
+            <p><i class="el-icon-info"></i> 表格名称:数据表</p>
+            <p><i class="el-icon-tickets"></i> 数据总量:{{list.length}}行 {{header.length}}列</p>
+          </div>
+        </el-form-item>
+      </el-form>
+      <span slot="footer" class="dialog-footer">
+        <el-button size="small" @click="exportDialogVisible = false">取 消</el-button>
+        <el-button size="small" type="primary" @click="confirmExport" :loading="exporting">确定导出</el-button>
+      </span>
+    </el-dialog>
   </div>
 </template>
 <script>
@@ -60,6 +102,16 @@ export default {
       optionsPosition: {},
       optionsData: {},
       flagInter: null,
+      // 新增导出对话框相关数据
+      exportDialogVisible: false,
+      exporting: false,
+      exportForm: {
+        fileName: '',
+        format: 'excel'
+      },
+      // 添加设备检测变量
+      isMobile: false,
+      screenWidth: window.innerWidth
     };
   },
   computed: {
@@ -107,6 +159,57 @@ export default {
         "border-color": bodyStyle.borderColor,
         "color": bodyStyle.bodyColor,
       };
+    },
+    // 新增样式计算属性
+    exportButtonStyle() {
+      // 根据表头字体大小动态计算按钮大小
+      const headerFontSize = parseInt(this.optionsSetUp.fontSizeHeader || 16);
+      
+      // 计算图标尺寸,基于表头字体大小
+      const iconSize = Math.max(14, Math.min(24, headerFontSize * 1.1));
+      
+      // 计算按钮高度,略大于图标尺寸
+      const buttonHeight = Math.round(iconSize * 1.8);
+      
+      return {
+        height: `${buttonHeight}px`,
+        padding: '0 12px',
+        fontSize: `${iconSize}px`,
+        backgroundColor: 'rgba(39, 174, 96, 0.9)',
+        borderRadius: '4px',
+        display: 'flex',
+        alignItems: 'center',
+        justifyContent: 'center',
+        boxShadow: '0 2px 12px 0 rgba(0, 0, 0, 0.1)',
+        color: '#ffffff'
+      };
+    },
+    exportButtonContainerStyle() {
+      // 计算按钮容器的位置,使其恰好超出表头几个像素
+      const headerHeight = this.optionsSetUp.rowHeight || 50;
+      // 按钮高度
+      const buttonHeight = parseInt(this.exportButtonStyle.height);
+      // 设置按钮位置
+      return {
+        position: 'absolute',
+        top: `-${buttonHeight + 5}px`, // 按钮高度 + 5px的间距
+        right: '10px',
+        zIndex: '100'
+      };
+    },
+    // 添加对话框宽度计算属性
+    dialogWidth() {
+      if (this.isMobile) {
+        return '100%';
+      } else if (this.screenWidth < 768) {
+        return '90%';
+      } else if (this.screenWidth < 992) {
+        return '80%';
+      } else if (this.screenWidth < 1200) {
+        return '60%';
+      } else {
+        return '420px';
+      }
     }
   },
   watch: {
@@ -126,6 +229,13 @@ export default {
     this.optionsData = this.value.data;
     this.initData();
     targetWidgetLinkageLogic(this); // 联动-目标组件逻辑
+    // 添加窗口大小变化监听
+    this.checkDeviceType();
+    window.addEventListener('resize', this.handleResize);
+  },
+  beforeDestroy() {
+    // 移除窗口大小变化监听
+    window.removeEventListener('resize', this.handleResize);
   },
   methods: {
     initData() {
@@ -230,7 +340,163 @@ export default {
         styleJson["width"] = this.optionsSetUp.dynamicAddTable[index].width;
       }
       return styleJson;
-    }
+    },
+    // 添加的方法 - 设备类型检测
+    checkDeviceType() {
+      this.screenWidth = window.innerWidth;
+      this.isMobile = this.screenWidth <= 576;
+    },
+    
+    // 处理窗口大小变化
+    handleResize() {
+      this.checkDeviceType();
+    },
+    
+    // 打开导出对话框前检查设备类型
+    openExportDialog() {
+      if (!this.list || this.list.length === 0) {
+        this.$message.warning('没有数据可导出');
+        return;
+      }
+      
+      // 重新检查设备类型
+      this.checkDeviceType();
+
+      // 设置默认文件名(表格名称+日期时间)
+      const now = new Date();
+      const dateStr = `${now.getFullYear()}${(now.getMonth()+1).toString().padStart(2,'0')}${now.getDate().toString().padStart(2,'0')}`;
+      const timeStr = `${now.getHours().toString().padStart(2,'0')}${now.getMinutes().toString().padStart(2,'0')}`;
+      
+      // 准备对话框数据
+      this.exportForm.fileName = `表格数据_${dateStr}_${timeStr}`;
+      this.exportForm.format = 'excel'; // 默认选择Excel格式
+      
+      // 显示导出对话框
+      this.exportDialogVisible = true;
+    },
+    
+    // 确认导出方法
+    confirmExport() {
+      // 检查文件名是否为空
+      if (!this.exportForm.fileName || this.exportForm.fileName.trim() === '') {
+        this.$message.warning('请输入文件名');
+        return;
+      }
+      
+      // 设置导出中状态
+      this.exporting = true;
+      
+      // 根据选择的格式执行不同的导出方法
+      try {
+        if (this.exportForm.format === 'excel') {
+          this.doExportExcel();
+        } else {
+          this.doExportCSV();
+          // 只有CSV导出直接在这里显示成功消息和关闭对话框
+          this.$message.success('导出成功');
+          this.exportDialogVisible = false;
+          this.exporting = false;
+        }
+      } catch (error) {
+        console.error('导出失败:', error);
+        this.$message.error('导出失败,请重试');
+        this.exporting = false;
+      }
+    },
+    
+    // 执行Excel导出
+    doExportExcel() {
+      // 引入xlsx库,使用动态导入以减少初始加载时间
+      import('xlsx').then(XLSX => {
+        // 准备数据:表头和内容
+        const tableHeaders = this.header.map(header => header.name);
+        const tableKeys = this.header.map(header => header.key);
+        
+        // 创建表格数据数组,从列表数据映射
+        const tableData = this.list.map(row => {
+          const rowData = {};
+          tableKeys.forEach((key, index) => {
+            rowData[tableHeaders[index]] = row[key];
+          });
+          return rowData;
+        });
+        
+        // 创建工作簿和工作表
+        const wb = XLSX.utils.book_new();
+        const ws = XLSX.utils.json_to_sheet(tableData, { header: tableHeaders });
+        
+        // 计算列宽 - 动态计算合适的列宽
+        ws['!cols'] = tableHeaders.map((header, index) => {
+          // 尝试计算合适的列宽,考虑标题和内容
+          const headerLength = header ? header.toString().length : 0;
+          
+          // 计算该列数据的最大长度
+          let maxLength = headerLength;
+          tableData.forEach(row => {
+            const cellValue = row[header];
+            const cellLength = cellValue ? cellValue.toString().length : 0;
+            maxLength = Math.max(maxLength, cellLength);
+          });
+          
+          // 为确保列宽适合内容,添加一些额外空间
+          return { wch: Math.min(50, maxLength + 2) }; // 限制最大宽度为50个字符
+        });
+        
+        // 添加工作表到工作簿
+        XLSX.utils.book_append_sheet(wb, ws, '数据表');
+        
+        // 导出文件,使用用户指定的文件名
+        XLSX.writeFile(wb, `${this.exportForm.fileName}.xlsx`);
+        
+        // 导出成功
+        this.$message.success('导出成功');
+        this.exportDialogVisible = false;
+        this.exporting = false;
+      }).catch(err => {
+        console.error('导出Excel失败:', err);
+        this.$message.error('导出Excel失败,请重试');
+        this.exporting = false;
+      });
+    },
+    
+    // 执行CSV导出
+    doExportCSV() {
+      // 准备表头和数据
+      const tableHeaders = this.header.map(header => header.name);
+      const tableKeys = this.header.map(header => header.key);
+      
+      // 创建CSV内容
+      let csvContent = '\uFEFF'; // 添加BOM标记以支持中文
+      
+      // 添加表头
+      csvContent += tableHeaders.join(',') + '\r\n';
+      
+      // 添加数据行
+      this.list.forEach(row => {
+        const rowValues = tableKeys.map(key => {
+          const value = row[key] !== undefined && row[key] !== null ? row[key] : '';
+          // 如果值包含逗号、引号或换行符,需要用引号包裹并转义内部引号
+          if (typeof value === 'string' && (value.includes(',') || value.includes('"') || value.includes('\n'))) {
+            return `"${value.replace(/"/g, '""')}"`;
+          }
+          return value;
+        });
+        csvContent += rowValues.join(',') + '\r\n';
+      });
+      
+      // 创建Blob对象
+      const blob = new Blob([csvContent], {
+        type: 'text/csv;charset=utf-8'
+      });
+      
+      // 创建下载链接,使用用户指定的文件名
+      const link = document.createElement('a');
+      link.href = URL.createObjectURL(blob);
+      link.download = `${this.exportForm.fileName}.csv`;
+      document.body.appendChild(link);
+      link.click();
+      document.body.removeChild(link);
+    },
   }
 };
 </script>
@@ -272,11 +538,134 @@ export default {
   flex-wrap: wrap;
 }
 
-/*.txtScroll-top .infoList li:nth-child(n) {
-  background: rgb(0, 59, 81);
+/* 添加导出按钮样式 */
+.export-button-container {
+  position: absolute;
+  right: 0px;
+  z-index: 100;
+
+  .export-button {
+    transition: all 0.3s ease;
+    cursor: pointer;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+    
+    &:hover {
+      transform: scale(1.05);
+      background-color: rgba(39, 174, 96, 1) !important;
+      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
+    }
+    
+    i {
+      margin-right: 5px;
+    }
+    
+    .button-text {
+      font-size: 14px;
+      font-weight: 500;
+    }
+  }
+}
+
+/* 添加导出信息样式 */
+.export-info {
+  background-color: #f5f7fa;
+  border-radius: 4px;
+  padding: 10px 15px;
+  margin-top: 5px;
+  
+  p {
+    margin: 5px 0;
+    font-size: 13px;
+    color: #606266;
+    display: flex;
+    align-items: center;
+    
+    i {
+      margin-right: 5px;
+      color: #409EFF;
+    }
+  }
+}
+
+/* 确保对话框在大屏中显示正常 */
+::v-deep .el-dialog {
+  background-color: #fff;
+  border-radius: 6px;
+  box-shadow: 0 3px 10px rgba(0, 0, 0, 0.3);
+  position: relative;
+  z-index: 9999 !important;
+  
+  .el-dialog__header {
+    padding: 15px 20px;
+    border-bottom: 1px solid #e4e7ed;
+  }
+  
+  .el-dialog__body {
+    padding: 20px;
+  }
+  
+  .el-dialog__footer {
+    padding: 10px 20px 15px;
+    border-top: 1px solid #e4e7ed;
+  }
+  
+  .el-form-item {
+    margin-bottom: 15px;
+  }
+}
+
+/* 为移动设备添加特定样式 */
+::v-deep .export-dialog {
+  @media screen and (max-width: 576px) {
+    display: flex;
+    flex-direction: column;
+    margin: 0 !important;
+    
+    .el-dialog {
+      width: 100% !important;
+      margin: 0 !important;
+      max-width: 100%;
+      border-radius: 0;
+      display: flex;
+      flex-direction: column;
+      height: 100%;
+      
+      .el-dialog__body {
+        flex: 1;
+        overflow-y: auto;
+        padding: 15px;
+      }
+      
+      .el-form-item__label {
+        width: 80px !important;
+      }
+      
+      .el-radio-group {
+        display: flex;
+        flex-direction: column;
+        
+        .el-radio {
+          margin-left: 0;
+          margin-bottom: 10px;
+        }
+      }
+    }
+  }
+  
+  @media screen and (max-width: 768px) {
+    .el-form-item__label {
+      width: 80px !important;
+    }
+    
+    .el-form {
+      .el-radio-group {
+        flex-wrap: wrap;
+      }
+    }
+  }
 }
 
-.txtScroll-top .infoList li:nth-child(2n) {
-  background: rgb(10, 39, 50);
-}*/
+::v-deep .v-modal {
+  z-index: 9998 !important;
+}
 </style>

+ 6 - 1
report-ui/src/views/bigscreenDesigner/designer/widget/texts/widgetText.vue

@@ -46,7 +46,12 @@ export default {
         left: this.transStyle.left + "px",
         top: this.transStyle.top + "px",
         right: this.transStyle.right + "px",
-        whiteSpace: this.transStyle.whiteSpace ? "pre-line": "normal"
+        whiteSpace: this.transStyle.whiteSpace ? "pre-line": "normal",
+        lineHeight:  this.transStyle.lineHeight + "px",
+        borderColor: this.transStyle.borderColor,
+        borderWidth: this.transStyle.borderWidth + "px",
+        borderStyle: this.transStyle.borderStyle,
+        borderRadius: this.transStyle.borderRadius + "px",
       };
     },
     computedStyleColor() {

+ 372 - 0
report-ui/src/views/bigscreenDesigner/designer/widget/texts/widgetVideoMonitor.vue

@@ -0,0 +1,372 @@
+<template>
+  <div class="video-container" :style="styleObj">
+    <div class="video-list" :style="videoListStyle2" v-loading="loading" v-if="isShowList">
+      <div class="video-check-list" :style="videoListStyle">
+        <ul>
+          <li :style="videoListLiStyle" @click="handleCheck(item)" v-for="item in monitorPointList" :key="item.devid" :class="checkVideos.indexOf(item.devid) !== -1 ? 'list-active' : ''">
+            <span :style="videoListCheckStyle" class="check-box" :class="checkVideos.indexOf(item.devid) !== -1 ? 'check-active' : ''"></span>
+            <span class="check-label">{{ item.monitorName }}</span>
+          </li>
+        </ul>
+        <div class="no-data" v-if="monitorPointList.length === 0">
+          <span>暂无数据</span>
+        </div>
+      </div>
+    </div>
+    <!-- 监控视频区域 -->
+    <div class="video-wrapper">
+      <FlvVideo :indexs="JSON.parse(JSON.stringify(checkVideos))" :styleObj="videoWrapperStyle" ref="video"/>
+    </div>
+  </div>
+</template>
+
+<script>
+import { targetWidgetLinkageLogic } from "@/views/bigscreenDesigner/designer/linkageLogic";
+import FlvVideo from '@/components/FlvVideo';
+
+export default {
+  name: 'widgetVideoMonitor',
+  components: {
+    FlvVideo
+  },
+  props: {
+    value: Object,
+    ispreview: Boolean,
+  },
+  data() {
+    return {
+      options: {},
+      optionsSetUp: {},
+      optionsPosition: {},
+      optionsData: {},
+      timer: null,
+      videoList: [],
+      checkVideos: [],
+      monitorPointList: [
+        // { monitorName: '测试', devid: 0, videoUrl: 'http://example.com/video.flv' },
+        // { monitorName: '测试2', devid: 1, videoUrl: 'http://example.com/video.flv' }
+      ],
+      loading: false,
+      autoPlayFirst: false,
+      autoPlayTimer: null,
+      isShowList: true
+    }
+  },
+  computed: {
+    styleObj() {
+      const allStyle = this.optionsPosition;
+      return {
+        position: this.ispreview ? "absolute" : "static",
+        width: allStyle.width + "px",
+        height: allStyle.height + "px",
+        left: allStyle.left + "px",
+        top: allStyle.top + "px",
+        backgroundColor: this.optionsSetUp.videoBgColor,
+        borderRadius: this.optionsSetUp.borderRadius + 'px'
+      };
+    },
+    videoListStyle () {
+      return {
+        borderRadius: this.optionsSetUp.borderRadius + 'px',
+        backgroundColor: this.optionsSetUp.videoListBgColor,
+      };
+    },
+    videoListStyle2 () {
+      return {
+        minWidth: this.optionsSetUp.minWidth + 'px',
+        maxWidth: this.optionsSetUp.maxWidth + 'px',
+      };
+    },
+    videoListLiStyle () {
+      return {
+        color: this.optionsSetUp.videoListFontColor,
+        fontSize: this.optionsSetUp.fontSize + 'px',
+      };
+    },
+    videoListCheckStyle() {
+      return {
+        borderColor: this.optionsSetUp.videoListFontColor,
+      };
+    },
+    videoWrapperStyle () {
+      return {
+        borderRadius: this.optionsSetUp.borderRadius + 'px'
+      };
+    },
+  },
+  watch: {
+    value: {
+      handler(val) {
+        this.options = val;
+        this.optionsSetUp = val.setup;
+        this.optionsPosition = val.position;
+        this.optionsData = val.data;
+        this.setOptionsData();
+      },
+      deep: true
+    }
+  },
+  mounted() {
+    this.options = this.value;
+    this.optionsSetUp = this.value.setup;
+    this.optionsPosition = this.value.position;
+    this.optionsData = this.value.data;
+    targetWidgetLinkageLogic(this);
+    this.setOptionsData();
+  },
+  methods: {
+    setProStyle () {
+      this.$nextTick(() => {
+        const checks = document.querySelectorAll('.check-box');
+        checks.forEach(element => {
+          element.style.setProperty('--after-active-color', this.optionsSetUp.videoListFontColor);
+        });
+
+        // --hover-list-color
+        const lists = document.querySelectorAll('.video-check-list li');
+        lists.forEach(element => {
+          element.style.setProperty('--hover-list-color', this.optionsSetUp.activeBgColor);
+        });
+      })
+    },
+    handleCheck (item) {
+      const id = item.devid
+      if (this.checkVideos.indexOf(id) === -1) {
+        if (this.checkVideos.length === 9) {
+          this.$message.warning('最多同时播放9个视频')
+          return false
+        }
+        this.checkVideos.push(id)
+      } else {
+        this.checkVideos = this.checkVideos.filter(videoId => id !== videoId)
+      }
+
+      this.$nextTick(() => {
+        const listActives = document.querySelectorAll('.list-active');
+        listActives.forEach(element => {
+          element.style.setProperty('--hover-list-color', this.optionsSetUp.activeBgColor);
+          element.style.setProperty('--hover-list-font-color', this.optionsSetUp.activeFontColor);
+        });
+      })
+      this.initVideo()
+    },
+    initVideo () {
+      const getSplitParams = num => {
+        if (num === 0) {
+          return '1x1'
+        }
+        let splitCount = 1
+        while (splitCount * splitCount < num) {
+          splitCount++
+        }
+        return `${splitCount}x${splitCount}`
+      }
+      // 获取视频编号
+      const videoDeviceList = this.checkVideos
+      let split = getSplitParams(videoDeviceList.length)
+      // 5ee093b281a74185aeafec3a463053cd
+      this.$refs.video.setLayout(split, 0).then(() => {
+        if (videoDeviceList.length === 0) {
+          this.$refs.video.stop()
+          return false
+        }
+        // 获取视频编号
+        // this.$refs.video.stop()
+        if (this.timer) { clearTimeout(this.timer) }
+        this.timer = setTimeout(() => {
+          // videoDeviceList按devid的顺序排序
+          const list_ = videoDeviceList.sort((a, b) => a - b)
+          list_.forEach((devid, index) => {
+            // item.devid
+            const pointList = this.monitorPointList
+            const find = pointList.find(item => item.devid === devid)
+            if (find) {
+              console.info('url:', find.videoUrl)
+              this.$refs.video.startPreview(find.videoUrl, devid, 0)
+            }
+          })
+          clearTimeout(this.timer)
+        }, 0)
+      }) // 分屏 播放模式
+    },
+    // 数据解析
+    setOptionsData(e, paramsConfig) {
+      this.autoPlayFirst = this.optionsSetUp.isAutoPlayFirst
+      this.isShowList = this.optionsSetUp.isShowList
+      const optionsData = this.optionsData; // 数据类型 静态 or 动态
+      // 联动接收者逻辑开始
+      optionsData.dynamicData = optionsData.dynamicData || {}; // 兼容 dynamicData undefined
+      const myDynamicData = optionsData.dynamicData;
+      clearInterval(this.flagInter); // 不管咋,先干掉上一次的定时任务,避免多跑
+      if (
+        e &&
+        optionsData.dataType !== "staticData" &&
+        Object.keys(myDynamicData.contextData).length
+      ) {
+        const keyArr = Object.keys(myDynamicData.contextData);
+        paramsConfig.forEach((conf) => {
+          if (keyArr.includes(conf.targetKey)) {
+            myDynamicData.contextData[conf.targetKey] = e[conf.originKey];
+          }
+        });
+      }
+      // 联动接收者逻辑结束
+      if (optionsData.dataType === "dynamicData") {
+        this.dynamicDataFn(optionsData.dynamicData, optionsData.refreshTime);
+      } else {
+        const data = this.objToOne(this.options).staticData
+        this.monitorPointList = []
+        data.forEach((e, i) => {
+          let obj = {
+            monitorName: e.name,
+            devid: i,
+            videoUrl: e.src
+          }
+          this.monitorPointList.push(obj)
+          if (!this.autoPlayFirst) return false
+          if (this.autoPlayTimer) { clearTimeout(this.autoPlayTimer) }
+          this.autoPlayTimer = setTimeout(() => {
+            if (this.monitorPointList.length > 0) {
+              this.handleCheck(this.monitorPointList[0])
+              this.loading = false
+            }
+            clearTimeout(this.autoPlayTimer)
+          }, 500)
+        });
+      };
+
+      this.setProStyle()
+    },
+    dynamicDataFn(val, refreshTime) {
+      if (!val) return;
+      if (this.ispreview) {
+        this.getEchartData(val);
+        this.flagInter = setInterval(() => {
+          this.getEchartData(val);
+        }, refreshTime);
+      } else {
+        this.getEchartData(val);
+      }
+    },
+    getEchartData(val) {
+      const data = this.queryEchartsData(val);
+      data.then(res => {
+        this.monitorPointList = []
+        res.forEach((e, i) => {
+          let obj = {
+            monitorName: e.name,
+            devid: i,
+            videoUrl: e.src
+          }
+          this.monitorPointList.push(obj)
+        });
+        this.$forceUpdate();
+        if (!this.autoPlayFirst) return false
+        if (this.autoPlayTimer) { clearTimeout(this.autoPlayTimer) }
+        this.autoPlayTimer = setTimeout(() => {
+          if (this.monitorPointList.length > 0) {
+            this.handleCheck(this.monitorPointList[0])
+            this.loading = false
+          }
+          clearTimeout(this.autoPlayTimer)
+        }, 500)
+      });
+    },
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.video-container {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: row;
+  padding: 10px;
+  border-radius: 10px;
+  .video-list {
+    flex: .3;
+    min-width: 200px;
+    height: 100%;
+    padding-right: 10px;
+    display: flex;
+    flex-direction: column;
+    .video-check-list {
+      flex: 1;
+      overflow-y: auto;
+      background-color: rgb(0, 59, 81);
+      padding: 6px 10px;
+      border-radius: 10px;
+      ul {
+        li {
+          display: flex;
+          flex-direction: row;
+          align-items: center;
+          padding: 4px 10px;
+          margin-bottom: 4px;
+          list-style: none;
+          border-radius: 10px;
+          cursor: pointer;
+          color: #ffffff;
+          &:hover {
+            // background-color: rgba(10, 39, 50, 1)
+            background-color: var(--hover-list-color, rgba(10, 39, 50, 1));
+          }
+          .check-label {
+            // font-size: 16px;
+            font-weight: bold;
+            margin-left: 10px;
+          }
+          .check-box {
+            display: inline-block;
+            width: 14px;
+            height: 14px;
+            border: 2px solid #ffffff;
+            border-radius: 4px;
+          }
+          .check-active {
+            background-color: rgba(10, 39, 50, 1);
+            position: relative;
+            &::after {
+              content: "✓";
+              color: var(--after-active-color, #ffffff);
+              font-size: 16px;
+              position: absolute;
+              left: 50%;
+              top: 50%;
+              transform: translate(-50%, -50%);
+            }
+          }
+        }
+        .list-active {
+          background-color: var(--hover-list-color, rgba(10, 39, 50, 1));
+          color: var(--hover-list-font-color, #ffffff)!important;
+        }
+      }
+    }
+  }
+  .video-wrapper {
+    // box-sizing: border-box;
+    // border: 1px solid #ffffff;
+    flex: 1;
+    font-size: 14px;
+    display: flex;
+    flex-direction: row;
+    justify-content: center;
+    align-items: center;
+  }
+  .no-data {
+    width: 100%;
+    height: 100%;
+    font-size: 14px;
+    font-weight: bold;
+    display: flex;
+    flex-direction: row;
+    justify-content: center;
+    align-items: center;
+    color: #ffffff;
+  }
+}
+
+
+</style>

+ 6 - 0
report-ui/src/views/bigscreenDesigner/designer/widget/widget.vue

@@ -19,12 +19,15 @@
 <script>
 import widgetHref from "./texts/widgetHref.vue";
 import widgetText from "./texts/widgetText.vue";
+import widgetButton from './form/widgetButton.vue';
 import WidgetMarquee from "./texts/widgetMarquee.vue";
 import widgetTime from "./texts/widgetTime.vue";
 import widgetImage from "./texts/widgetImage.vue";
 import widgetSlider from "./texts/widgetSlider.vue";
 import widgetVideo from "./texts/widgetVideo.vue";
+import widgetVideoMonitor from "./texts/widgetVideoMonitor.vue";
 import WidgetIframe from "./texts/widgetIframe.vue";
+import widgetCalendar from "./texts/widgetCalendar.vue";
 import widgetBarchart from "./bar/widgetBarchart.vue";
 import widgetScatter from "./scatter/widgetScatter.vue";
 import widgetGradientColorBarchart from "./bar/widgetGradientColorBarchart.vue";
@@ -68,6 +71,7 @@ export default {
   components: {
     widgetHref,
     widgetText,
+    widgetButton,
     widgetBorder,
     widgetDecorateFlowLine,
     widgetDecoration,
@@ -76,7 +80,9 @@ export default {
     widgetImage,
     widgetSlider,
     widgetVideo,
+    widgetVideoMonitor,
     WidgetIframe,
+    widgetCalendar,
     widgetBarchart,
     widgetGradientColorBarchart,
     widgetLinechart,

+ 9 - 5
report-ui/src/views/bigscreenDesigner/viewer/index.vue

@@ -96,11 +96,15 @@ export default {
       this.bigScreenStyle.transform='scale('+scale.scalex+','+scale.scaley+')'
     },
     getScale(){
-      let width=this.dashboard.width;
-      let height=this.dashboard.height;
-      return{
-        scalex:(window.innerWidth)/width,
-        scaley:(window.innerHeight)/height,
+      let width = this.dashboard.width
+      let height = this.dashboard.height
+      // 固定宽度比例
+      const scalex = window.innerWidth / width
+      // 高度按相同比例缩放,但允许超出屏幕高度
+      const scaley = scalex
+      return {
+        scalex,
+        scaley
       }
     },
   }

+ 1 - 1
report-ui/src/views/layout/components/Sidebar/index.vue

@@ -3,7 +3,7 @@
     <div class="admin-title" @click="goBigScreen">
       <div class="con">
         <img src="/static/logo-dp.png" width="50" />
-        <span class="version">V1.6.0</span>
+        <span class="version">V1.7.1</span>
       </div>
     </div>
     <el-menu