release: 2.2.0

* fix: 优化docker

* fix: 修复用户管理查询条件在..中,不在..中无效问题

* fix: 删除FRuleEditor多余文件

* fix: 修复产品进行规则属性调试时,部分内容无法调试成功

* fix: bug#25001

* fix: 优化物联卡详情仪表盘显示异常;修改菜单拖拽排序

* fix:bug#25001

* fix: 优化物联卡详情仪表盘显示异常

* fix: 修改菜单拖拽排序

* fix: 24641、24804、25111、22958、24806、25377、25597、24628、25704、24545、24479

* fix: 优化物联卡批量导入数量统计;优化物联卡状态查询列表

* fix: 24641、24804、25111、22958、24806、25377、25597

* fix: 24628

* feat: 修改区域管理删除逻辑

* fix: 24628、25704

* fix: 24545

* fix: bug#25414

* fix: bug#24479

* fix: bug#23410

* fix: bug#24404

* fix: bug#25374

* fix: bug#25062

* fix: bug#25062

* fix: bug#22409

* fix: bug#25525、26083

* fix: bug#25525、26083

Co-authored-by: leiqiaochu <leiqiaochu@aossci.com>

* fix: bug#26186

* fix: bug#26233

* fix: bug#26233

* feat: 物模型-功能定义、事件定义添加【其它配置】

* feat: 新增metadataTable组件;新增虚拟滚动table功能;优化物模型右键菜单;优化物模型搜索;优化物模型拓展配置

* feat: 新增metadataTable组件

* feat: 新增虚拟滚动table功能

* fix: 优化虚拟滚动

* fix: 优化物模型属性、功能、事件、标签组件

* fix: 优化物模型右键菜单

* feat: 新增右键功能

* fix: 优化虚拟滚动

* fix: 优化物模型其它配置参数

* fix: 优化物模型搜索;优化物模型拓展配置

* fix: 优化存储方式导致物模型无法新增、修改提示语

* fix: 优化存储方式导致物模型无法新增、修改提示语

* feat: 新增日历维护管理

* feat: 日历维护日历组件

* feat: 日历维护新增标签

* feat: 日历维护左侧标签树新增功能

* feat: 日历维护左侧标签树

* feat: 日历维护左侧标签功能

* feat: 日历维护

* feat: 日历维护日历功能

* feat: 日历维护添加标签功能

* feat: 日历维护模块

* feat: 修改日历组件交互逻辑

* feat: 修改日历维护组件

* feat: 日历维护tips

* feat: 封装日历预览组件

* feat: 日历维护数据请求逻辑

* feat: 修改请求逻辑及快速跳转功能

* feat: 远程升级交互修改

* style: 远程升级交互修改

* style: 远程升级模块交互修改

* style: 远程升级关闭单个升级记录

* feat: 新增批量操作后更新状态逻辑

* fix: 修改任务详情交互逻辑

* fix: 修改远程升级权限按钮

* style: 远程升级抽屉无数据样式

* style: 修改升级任务详情滚动样式

* fix: 新建远程升级任务接口传参

* update: 场景联动-时间添加Calendar组件;优化场景联动多条件;优化场景联动样式

* feat: 场景联动-时间添加Calendar组件

* fix: 优化场景联动多条件

* fix: 优化场景联动绑定告警配置

* update: 对接场景联动-执行动作绑定告警新接口

* update: 场景联动-定时触发新增自定义日历

* fix: 优化场景联动-自定义日历标签下拉筛选

* fix: 优化场景联动-自定义日历标签数据回显异常

* fix: 优化场景联动-说明排版及样式

* fix: 优化场景联动样式

* fix: 优化物模型编辑状态校验逻辑;优化场景联动-执行动作绑定告警查询条件

* fix: 优化物模型编辑状态校验逻辑

* fix: 优化场景联动-执行动作绑定告警查询条件

* fix: 优化物模型其它配置tag显示

* fix: 优化设备物模型-继承属性删除限制

* update: 告警记录优化

* feat: 告警记录

* feat: 抽屉蒙层关闭

* feat: 告警记录

* feat: 告警记录功能开发

* feat: 告警记录基础功能

* feat: 告警日志跳转设备逻辑

* feat: 提取计算时间差组件

* feat: 删除冗余代码

* fix: 修复执行动作-设备-无法选择内置属性

* fix: 修复执行动作-通知-无法选择固定邮箱

* fix: 修复执行动作-通知-固定邮箱校验提示

* feat: 新增设备详情告警列表

* feat: 抽屉蒙层关闭

* feat: 告警记录功能开发

* feat: 告警记录基础功能

* feat: 告警日志跳转设备逻辑

* feat: 设备详情告警列表

* feat: 提取计算时间差组件

* feat: 产品告警列表和设备告警列表

* feat: 产品请求数据逻辑

* feat: 基础配置增加部分配置项

* update: 取消地图不填写key兼容

* feat: 告警等级请求优化

* feat: 新增视频插件接入和视频插件网关

* feat: 视频接入

* feat: 视频插件接入和视频插件网关

* update: 优化告警配置新增场景联动逻辑

* update: 优化告警配置新增场景联动逻辑

* update: 优化告警配置新增场景联动逻辑

* update: 优化告警配置关联场景联动

* update: 优化告警配置关联场景联动; 修复部分场景联动bug

* update: 优化告警配置-关联场景联动取消关联操作

* feat: 新增设备物联卡

* feat: 物联网卡

* feat: 设备物联卡

* fix: bug#26868、26876、26860、26869

* fix: bug#26868、26876、26876、26860

* fix: bug#26869

* update: 优化地区管理

* update: 优化地区管理

* bug#27400、27423、27035、26867、27395

* fix: bug#27395

* fix: bug#27400、27423

* fix: bug#27035

* fix: bug#26867

* fix: 优化代码逻辑

* fix: 优化登录页面挂载久了之后无法登录;修复执行动作-触发告警-关联告警,关联告警弹窗中搜索项未调用接口

* fix: 优化登录页面挂载久了之后无法登录

* fix: 修复执行动作-触发告警-关联告警,关联告警弹窗中搜索项未调用接口

* fix: bug#27018

* fix: bug#27019

* fix: 优化告警卡片样式

* fix: bug#27029

* fix: 优化主题色

* fix: 修复设备物模型无法保存

* update: 优化告警配置删除交互逻辑

* fix: bug#26877

* feat: 修改告警配置删除交互逻辑

* fix: 修改请求判断条件及提示语句

* fix: bug#27220物模型-标签 删除读写类型;bug#27217修复设备物模型保存之后继承的物模型变为可编辑状态问题

* fix: bug#27220物模型-标签 删除读写类型

* fix: bug#27217修复设备物模型保存之后继承的物模型变为可编辑状态问题

* fix: bug#27230修复物模型-全屏状态下,无法显示属性详情弹窗

* fix: bug#27230修复物模型-全屏状态下,无法显示保存操作提示

* fix: bug#27368修复告警配置查询条件

* fix: bug#27415场景联动-关联告警:弹窗隐藏footer

* fix: 优化登录密码

* fix: 修复物模型新增行属性来源不显示编辑按钮

* fix: 修复物模型复制行-粘贴行,不能编辑

* fix: 修复物模型删除行,错误提示依旧存在

* fix: bug#27152修复物模型保存页面无提示

* fix: bug#27155修复物模型存在相同标识提示异常

* fix: bug#27155修复物模型数据类型为object时联动配置中进行多次配置数据类型界面异常

* fix: bug#27173优化物模型读写类型样式

* fix: bug#26862日历新增后删除失败问题;Bacnet新增广播端口限制1-65535;优化日历维护可拖拽区域;空白日期快速作用覆盖

* fix: bug#27347新增onvif和插件视频接入图标

* fix: bug#26862日历新增后删除失败问题

* fix: bug#27035日历组件渲染宽高不固定问题

* fix: bug#27234 Bacnet新增广播端口限制1-65535

* fix: bug#27373修复告警记录告警持续时间错误问题

* fix: bug#27374 优化高景源设备ID

* fix: bug#26860优化日历维护可拖拽区域、bug#27528空白日期快速作用覆盖

* fix: bug#27222修复物模型查看属性详情时,详情中存储方式显示错误;修复物模型-编辑规则-规则属性详情显示标识

* fix: bug#27425修复物模型,配置单位可搜索

* fix: bug#27182优化物模型编辑规则样式

* fix: bug#27184修改物模型编辑规则不能选择规则属性

* fix: 修复切换产品,物模型显示异常

* fix: bug#27218修复物模型-编辑规则-规则属性详情显示标识

* fix: bug#27222修复物模型查看属性详情时,详情中存储方式显示错误

* fix: 修复告警配置查询结果异常

* fix: bug修改数采表单校验、个人中心站内信json无法展开、修改告警记录处理方式取值

* fix: bug修改数采表单校验、bug#27384个人中心站内信json无法展开

* feat: 修改告警记录处理方式取值

* feat: 手动触发展示优化

* fix: 优化函数引用

* fix: 修改引用和查询条件 告警手动触发

* fix: 修复物模型详情精度显示;bug#27225修复物模型复制粘贴操作逻辑;修复物模型编辑规则-查看属性,是否只读展示错误

* fix: 修复物模型详情精度显示

* fix: bug#27224修复物模型详情指标显示

* fix: bug#27225修复物模型复制粘贴操作逻辑

* fix: bug#27226修复物模型复制跨标签粘贴逻辑

* fix: bug#27425修复物模型,单位无法进行搜索

* fix: bug#27565修复物模型,单位无法回显

* fix: 优化物模型详情数据类型展示

* revert: 撤销物模型-编辑规则过滤条件

* fix: bug#27569修复物模型编辑规则-查看属性,是否只读展示错误

* fix: bug#27216修复编辑规则属性赋值为空时进行运行需进行提示

* fix: bug#27182优化编辑规则样式

* fix: bug#27223修复物模型详情,属性来源后方无查看按钮

* fix: bug#27506优化场景联动触发规则样式

* fix: bug#27434修复场景联动新增执行动作后,在告警配置新增添加时,变更为了无效的数据

* fix: bug#27263修复场景联动添加设备下发指令读操作,保存后查看,提示数据发生更改

* fix: bug#27365修复场景联动添加设备下发指选择内置参数为设备名称,upperKey异常

* fix: bug#27365修复场景联动条件下拉没有字段名称

* fix: bug#27090修复场景联动触发条件参数类型为array时显示异常

* fix: 优化物联网卡ui,远程升级ui

* fix: 修改告警记录ui

* feat: 告警记录ui修改

* feat: 修改日历维护ui

* feat: 远程升级ui

* feat: 物联网卡ui

* fix: bug#27530IEC104采集器,在点位中设置点位死区“百分比”时,过滤异常

* fix: 日历事件新增tooltip

* fix: bug#27521新增BACNet设备实例号只输入正整数

* fix: bug#27534IEC104采集批量导入,文案没有实时更新

* fix: bug#27530IEC104采集器,在点位中设置点位死区“百分比”时,过滤异常

* fix: bug#27688设备导出不选择产品,未导出全部设备

* feat: 视频设备新增配置是否支持录像和云台控制功能

* fix: bug#27696当阿里云关联付费实例时,无法绑定成功,界面报错

* fix: 屏蔽bug27696修改代码

* fix: bug#27696当阿里云关联付费实例时,无法绑定成功,界面报错,服务器异常

* fix: bug#27701MODBUS_TCP点位,配置中寄存器过大时,卡片中显示优化

* fix: bug#27543视频设备>>>插件视频,无法进行录制(本地和云端都不行)

* fix: 控制录像显隐

* fix: bug#27618 OPC_UA点位进行批量操作-编辑时,只推送变化的数据在不进行选择时,是否对当前点位的数据进行修改

* fix: 日历事件新增tooltip

* fix: bug#27521新增BACNet设备实例号只输入正整数

* fix: bug#27534IEC104采集批量导入,文案没有实时更新

* fix: bug#27530IEC104采集器,在点位中设置点位死区“百分比”时,过滤异常

* fix: bug#27688设备导出不选择产品,未导出全部设备

* feat: 视频设备新增配置是否支持录像和云台控制功能

* fix: bug#27696当阿里云关联付费实例时,无法绑定成功,界面报错

* fix: 屏蔽bug27696修改代码

* fix: bug#27696当阿里云关联付费实例时,无法绑定成功,界面报错,服务器异常

* fix: bug#27701MODBUS_TCP点位,配置中寄存器过大时,卡片中显示优化

* fix: bug#27543视频设备>>>插件视频,无法进行录制(本地和云端都不行)

* fix: 控制录像显隐

* fix: bug#27618 OPC_UA点位进行批量操作-编辑时,只推送变化的数据在不进行选择时,是否对当前点位的数据进行修改

* fix: bug#27741产品(设备)快速导入时,复制的其它产品物模型TSL在另外一个产品导入时,无法导入成功

* fix: 远程升级ui

* fix: bug#27698修复设备标签-地图无法正确回显点位;修复视频设备通道管理无法修改厂商;优化场景联动-触发条件提示语

* fix: bug#27350修复告警配置查看关联的场景联动显示异常

* fix: bug#27053修复场景联动名称没有长度限制

* fix: bug#27693修复告警配置-定时触发界面展示错误

* fix: bug#27432修复告警配置-过滤执行动作中没有关联告警的场景联动

* fix: 优化告警配置-场景联动交互

* fix: bug#27071修复场景联动-触发告警动作删除,同步删除关联告警

* fix: 优化场景联动-触发规则弹窗宽度

* fix: bug#27051修复场景联动条件删除提示

* fix: 修改ICCID查询条件

* fix: 优化物模型条件数组输入框

* fix: bug#27542优化物模型条件数组输入框

* fix: bug#27580修复地区管理下级区域没有地图边界

* fix: bug#27595修复地区管理无法拖拽操作

* fix: bug#27189优化场景联动-手动触发绑定告警查询参数

* fix: bug#27090优化场景联动-触发条件提示语

* fix: bug#27755修复视频设备通道管理无法修改厂商

* fix: bug#27758修复视频播放界面样式

* fix: bug#27698修复设备标签-地图无法正确回显点位

* fix: bug#27742告警记录>>>设备tab页进行搜索,搜索结果展示错误;删除采集器后,右侧该采集器下的点位仍然显示在页面

* fix: bug#26613当认证配置填入为数值类型时,填写完成后保存,再次查看未回显

* fix: bug#26399规则编排列表模式下查看,界面展示需优化

* fix: bug#26527视频中心仪表盘,进行跨年搜索时,界面展示需优化

* fix: bug#26423物联卡详情界面查看,平台类型和运营商显示错误

* fix: bug#26542边缘网关>>>网关设备>>>快速,接入网关选择鼠标放上去后,没有浮窗展示省略的内容

* fix: bug#27822删除采集器后,右侧该采集器下的点位仍然显示在页面

* fix: bug#27742告警记录>>>设备tab页进行搜索,搜索结果展示错误

* fix: bug#27837修复场景联动属性为array,输入值校验无法通过;【地区管理】修复无法把下级区域拖动到上一级

* fix: bug#26716修复场景联动-执行动作-通知方式样式

* fix: bug#27042修复场景联动-多条件保存校验无错误提示

* fix: bug#27744修复物模型-规则编排标签多出的按钮

* fix: bug#27756修复视频设备-无法清除用户名和密码

* fix: 修复地区管理下级区域无法显示范围

* fix: 修复地区管理无法拖拽

* fix: bug#27740修复物模型-array类型元素类型为enum无校验提示

* fix: bug#27832屏蔽视频分享

* fix: bug#27837修复场景联动属性为array,输入值校验无法通过

* fix: bug#27848修复设备功能样式错位

* fix: bug#27850【地区管理】修复新增勾选下一级区域无法新增

* fix: bug#27852【地区管理】修复无法把下级区域拖动到上一级

* feat: 2.2版本运行状态优化

* feat: 运行状态2.2

* feat: 设备运行状态列表高级搜索

* feat: 新增列表原始值查询

* feat: 2.2版本运行状态优化

* feat: 删除冗余代码

* fix: bug#18167应用管理>>>第三方应用>>>API服务,seucreKey,提高密码强度

Co-authored-by: leiqiaochu <leiqiaochu@aossci.com>

* fix: bug#2785720【告警配置】优化绑定场景联动详情展示防抖配置

* fix: bug#27100【场景联动】执行动作,功能执行内置参数添加columns值

* fix: bug#27579【地区管理】修复编辑弹窗关闭后,地图区域消失

* fix: bug#27714【告警配置】关联场景联动样式优化

* fix: bug#27859【地区管理】修复拖拽区域后排序错乱

* fix: bug#27859【地区管理】修复切换路由页面异常

* fix: bug#27859【地区管理】优化自定义区域自适应地图可视范围

* fix: bug#27851【场景联动】修改设备功能时间类型为时间插件

* fix: bug#2785720【告警配置】优化绑定场景联动详情展示防抖配置

* feat: 新增国际化配置

* fix: bug#27891 物联卡进行运营商搜索时,无法搜索出正确结果

* fix: bug#27992【物联卡管理】ctwingCmp平台类型在停机后,运营商状态后面感叹号无提示;【设备】物联网卡绑定设备后,设备详情中上方多了导航栏

* fix: bug#27911编辑标签弹框文案和标签颜色优化

* fix: bug#27920新增onvif页,部分输入框红色提示语错误 bug#27919新增onvif页,部分输入框内无说明文案,需优化

* fix: bug#27928onvif通道列表页,可点击新增通道

* fix: bug#27924新增onvif页,所属产品下拉框没有默认选中产品

* fix: bug#27931插件视频设备通道列表-搜索下拉框多了“厂商” bug#27932插件视频设备通道列表-新增通道弹框缺少“视频地址”输入框

* fix: bug#27934插件视频设备通道列表-编辑通道展示内容需要同新增一致

* fix: bug#27918onvif查看通道页页面布局与需求不一致

* fix: bug#27935插件视频设备通道列表回放-本地下载到云端后,下载按钮已经变成了查看按钮,文案优化

* fix: bug#27945详情页,图表下方错误显示暂无数据图标

* fix: bug#27879【采集器】colector_gateway协议-批量导入的下载模板报错

* fix: bug#27938产品(设备)导入物模型TSL导出的json文件时,无法导入成功(复制json进行导入时,可以导入,但是界面有错误提示)

* fix: bug#27980升级任务-任务详情 列表中状态的展示文案与需求不符

* fix: 删除远程升级老版代码

* fix: bug#数采gateway协议兼容导入数据

* fix: bug#27995【设备】物联网卡绑定设备后,设备详情中上方多了导航栏

* fix: bug#27992【物联卡管理】ctwingCmp平台类型在停机后,运营商状态后面感叹号无提示

* fix: bug#27926修复物模型规则属性弹窗不展示读写类型;bug#27940优化GeoJson自适应显示范围;bug#27978优化告警配置-关联场景联动卡片样式

* fix: bug#27926修复物模型规则属性弹窗不展示读写类型

* fix: 物模型-编辑规则代码优化

* fix: bug#27944优化上传GeoJson提示语

* fix: bug#27940优化GeoJson自适应显示范围

* fix: 【地区管理】优化GeoJSON销毁函数

* fix: bug#27976 告警配置卡片优化

* fix: bug#27978优化告警配置-关联场景联动卡片样式

* fix: bug#28027【日历维护】新增标签,红色必填文案优化;【采集器】当opc_ua和BacNet新增点位时,可以进行批量添加

* fix: 通道管理查询筛选值

* fix: bug#28027【日历维护】新增标签,红色必填文案优化

* fix: bug#27911【日历维护】编辑标签弹框文案和标签颜色优化

* fix: bug#国标级联解绑通道选择部分通道导致当页全部解绑bug

* fix: bug#27889【采集器】当opc_ua和BacNet新增点位时,可以进行批量添加

* fix: bug#27931【视频设备】插件视频设备通道列表-搜索下拉框多了“厂商”

* fix: bug运营商状态查询优化

* fix: bug#27887修复告警配置条件回显异常;修复数据字典内置数据操作限制;修复场景联动-无法删除执行动作;修复物模型-编辑规则,无法选择属性

* fix: 优化按需引入配置

* fix: bug#27887修复告警配置条件回显异常

* fix: bug#27913修复物模型滚动事件异常导致枚举数量显示不对

* fix: bug#27974修复数据字典内置数据操作限制

* feat: bug#28012场景联动-编辑执行动作新增校验

* fix: bug#28015修复场景联动-设备触发无法选择日历

* fix: bug#28030修复场景联动-无法删除执行动作

* fix: bug#28057修复物模型-编辑规则,无法选择属性

* bug#28051修复初始化数据没有个人中心菜单;优化个人中心菜单展示;修复场景联动-手动触发,执行动作没有按”标签“

* fix: bug#28051修复初始化数据没有个人中心菜单;优化个人中心菜单展示

* fix: bug#28034修复场景联动-手动触发,执行动作没有按”标签“

* fix: bug#28058优化视频通道左侧树没有数据时隐藏

* feat: 优化二次确认请求逻辑

* feat: 二次确认交互修改

* feat: 修改二次确认组件,替换popConfirm

* feat: 修改二次确认请求

* feat: 请求中loading逻辑修改

* feat: 当被删除设备的网关类型为Ctwing设备接入时,提醒弹窗内容与其他设备有区别

* feat: 优化二次弹窗

* feat: 优化二次确认弹窗逻辑

* feat: 优化二次弹窗逻辑

* feat: 优化二次确认请求逻辑

* fix: 优化日历维护权限问题

* fix: 优化日历维护

* fix: 优化日历维护权限问题

* fix: 修改错误请求

* feat: 优化远程升级ui;优化二次确认弹窗

* feat: 产品和设备详情页新增远程升级页面tab

* feat: 修改设备远程升级过滤逻辑

* feat: 升级任务详情新增设备版本列

* feat: 优化二次确认弹窗

* feat: 优化远程升级ui

* fix: 优化物模型复制显示;优化物模型hooks代码;优化场景联动执行动作icon;优化物模型分组新增逻辑;优化全局样式

* fix: 物模型优化复制显示;优化物模型hooks代码

* fix: 优化场景联动执行动作icon

* remove: 删除多余文件

* fix: 优化物模型分组新增逻辑

* fix: 优化全局样式

* fix: 修复Ctwing和OneNet保存时transport传值为HTTP;物模型列表新增筛选

* fix: 修复Ctwing和OneNet保存时transport传值为HTTP

* feat: 物模型列表新增筛选

* fix: bug#28077视频设备查看通道,通道新增按钮展示需优化;【Dueros】动作映射-指令类型为修改属性,参数类型为date时,重新点击指令类型下拉框显示异常;【组织管理】资产解绑二次确认弹框和组织删除二次确认弹框文案优化

* fix: bug#28077视频设备查看通道,通道新增按钮展示需优化

* fix: bug#28088【产品】产品查看详情,选择物模型映射页面后切换到产品列表页面,再次点击没有物模型映射的产品查看详情,没有物模型映射tab的也展示了物模型映射

* fix: bug#28102 选择API后点击第二次保存时提示错误

* fix: bug#28119【Dueros】动作映射-指令类型为修改属性,参数类型为date时,重新点击指令类型下拉框显示异常

* fix: bug#28118【Dueros】动作映射-指令类型为调用功能,参数列表值输入框与类型不一致

* fix: bug#28121 【远程升级】远程升级>>>升级任务,任务详情列表展示当前设备版本

* fix: bug#28169【产品】产品、设备、远程升级,查看升级任务,界面展示需优化

* fix: bug#28174【产品】产品查看远程升级界面展示需优化(设备界面也需要优化)

* fix: bug#28173【组织管理】资产解绑二次确认弹框和组织删除二次确认弹框文案优化

* fix: bug#28172启用/禁用二次确认弹框提示语优化

* fix: 列表模式下操作栏间距优化

* fix: bug#28165【通道管理】 正常状态通道删除按钮提示语优化

* fix: bug#28164【远程升级】设备拉取任务的任务详情中,[全部暂停]按钮的逻辑错误

* fix: bug#28156【网络组件】列表形式,删除按钮超出操作列

* fix: bug#28155【设备】设备诊断tab-【忽略】二次弹框优化

* fix: bug#28124【采集器】modbus_tcp新增点位失败

* fix: bug#28161【告警配置】删除二次弹框,缺少图标

* feat: 优化北向输出样式

* feat: 北向输出交互修改

* feat: 优化北向输出样式

* feat: 修改视频设备概要抽屉样式

* feat: 视频设备点击概要说明抽屉

* feat: “用户名”列中的数据添加背景色,原判断规则不变(仍支持中文)

* feat: 优化用户系统ui

* feat: 修改视频设备概要抽屉样式

* feat: 采集器Bacnet写入新增写入优先级表单项

* feat: 修改场景联动执行动作选择方式显示逻辑

* feat: 规则引擎-场景联动-设备触发

* feat: 采集器Bacnet写入新增写入优先级表单项

* feat: 新增物模型-属性导入

* feat: 新增物模型-属性导入

* feat: 设备接入网关详情

* feat: 设备接入网关详情

* fix: bug#28230修复场景联动-执行动作-设备无法选择标签;优化设备映射查询接口;修改物模型导入接口以及相关处理逻辑

* fix: bug#28119修复zIndex导致布局错乱

* fix: bug#28230修复场景联动-执行动作-设备无法选择标签

* fix: 优化设备映射查询接口

* fix: 修改物模型导入接口以及相关处理逻辑

* fix: bug#24828修复插件设备ID映射不回显已映射的设备;修复场景联动触发规则选择自定义日历未触发;修复通知管理-通知记录回显之前输入的筛选值

* fix: bug#24828修复插件设备ID映射不回显已映射的设备

* fix: bug#24062修复场景联动触发规则选择自定义日历未触发

* fix: bug#24085优化菜单配置,保存后刷新页面

* fix: bug#28113 修复通知管理-通知记录回显之前输入的筛选值

* fix: bug#28243修复场景联动设备触发默认条件名称显示错误

* fix: bug#28233修复场景联动执行动作-设备触发-首个执行动作不可选择

* fix: bug#28248修复物模型属性-array数据类型回显异常

* fix: bug#28250 优化物模型全屏退出导致popover无法关闭

* fix: bug#28234 优化物模型复制提示显示数值错误

* fix: bug#28239 修复物模型保存时,校验成功后,异常提示还存在

* fix: bug#28196【阿里云】新增阿里云时,名称输入框回显了上一次选中阿里云的名称;【数据源管理】管理页面中左侧新增后,进行切换到存在字段的表后,在切换到新增的表,右侧页面未同步刷新;【设备接入网关】设备接入网关,当说明内容过长,查看详情时,界面展示需优化;【产品】产品导入物模型时,导入失败后,界面提示语需优化

* fix: bug#28196【阿里云】新增阿里云时,名称输入框回显了上一次选中阿里云的名称

* fix: bug#28197新增阿里云时,触发了必填校验,切换到其他阿里云数据时,必填红色文案仍然显示

* fix: bug#28195【阿里云】所选择的平台产品被禁用时,页面上方没有展示横幅

* fix: bug#28194【DuerOS】左侧数据进行启/禁用时按钮文案未进行更新

* fix: bug#28193【阿里云】搜索的数据不存在时,页面右侧展示优化

* fix: bug#28192【DuerOS】数据名称过长时,左侧展示异常

* fix: bug#28191【DuerOS】启用状态的数据,应不能进行删除

* fix: bug#28186【规则编排】启用二次确认弹框文案错误

* fix: bug#28203 【设备接入网关】创建onvif设备接入网关时,右侧说明内容错误

* fix: bug#28204【仪表盘】昨日流量统计时间统计错误,统计成了今日

* fix: bug#28187 【采集器】数据类型为16进制时,进行输入数据解密展示异常

* fix: bug#28208【视频设备】视频设备展示接入方式同原型不一致

* fix: bug#28209【视频设备】视频设备查看详情,接入密码展示同需求不符

* fix: bug#28206【插件管理】进行新增时,上传插件后,下发还提示的“请上传文件”

* fix: bug#28164 全部暂停时状态文案显示错误多了0%

* fix: bug#28158【设备】多次点击删除按钮,没有弹出弹框

* fix: bug#28146设备运行状态>>>详情查看,图标搜索展示结果错误(ClickHouse-行式存储)

* fix: 优化运行状态图表模式时间轴取值问题

* fix: 产品设备接入网关物模型兼容问题

* fix: bug#28205【视频设备】视频设备,选择接入方式为插件时,快速新增产品失败

* fix: bug#28033 【远程升级】设备拉取中已升级设备进行二次添加到升级中时,列表的状态显示优化

* fix: bug#11092 【仪表盘】功能缺失同需求不符

* fix: bug#28092 【数据源管理】管理页面中左侧新增后,进行切换到存在字段的表后,在切换到新增的表,右侧页面未同步刷新

* fix: bug#28252【设备接入网关】设备接入网关,当说明内容过长,查看详情时,界面展示需优化

* fix: bug#28255【采集器】新增S7协议的点位时,字符串长度校验异常

* fix: bug#28089【产品】产品导入物模型时,导入失败后,界面提示语需优化

* feat: 新增物模型属性阈值

* feat: 物模型属性阈值

* feat: 物模型属性阈值限制功能

* feat: 阈值功能传值优化

* fix: bug#28187【采集器】数据类型为16进制时,进行输入数据解密展示异常;【插件管理】进行新增时,上传插件后,下发还提示的“请上传文件”;【设备】物模型-属性定义-其他配置,不填写任何配置项,点击确认,页面报错

* fix: bug#28187【采集器】数据类型为16进制时,进行输入数据解密展示异常

* fix: bug#28317、28308、28307、28306、28305、28303

* fix: bug#28206 【插件管理】进行新增时,上传插件后,下发还提示的“请上传文件”

* feat: 新增新Onenet

* fix: bug#28331【设备】物模型-属性定义-其他配置,不填写任何配置项,点击确认,页面报错

* fix: 产品设备告警详情删除原始值项

* feat: 【场景联动】执行动作(告警)-过滤条件新增告警下拉列表

* fix: 修复物模型属性导入请求取消异常

* fix: 修复场景联动执行动作-设备触发无法选择设置属性

* feat: 【场景联动】执行动作(告警)-过滤条件新增告警下拉列表

* fix: 【场景联动】优化执行动作(告警)- 过滤条件告警下拉列表无法隐藏;修复可编辑表格虚拟滚动逻辑

* fix: 【场景联动】优化执行动作(告警)- 过滤条件告警下拉列表无法隐藏

* fix: 【告警配置】-关联场景联动-优化卡片样式

* fix: 修复可编辑表格虚拟滚动逻辑

* fix: bug#27692 【采集器】采集器的点位数据数组类型时,写入数据不能进行写入数组数据;物模型-属性定义-其他配置-拓展配置,处理方式选择“记录”或“告警”点击确认无响应

* fix: bug#28350【告警记录】告警记录列表中,处理类型缺失,导致列表展示内容错位(系统处理的缺少类型导致)

* fix: bacnet批量添加

* fix: bug#28365 物模型-属性定义-其他配置-拓展配置,处理方式选择“记录”或“告警”点击确认无响应

* fix: bug#27692 【采集器】采集器的点位数据数组类型时,写入数据不能进行写入数组数据

* fix: bug#27974、28378、28380【数据字典】内置的数据字典应不能进行编辑,删除,禁用

* fix: 修改详情告警无效数据接口

* fix: bug#28368、28369、28317仪表盘时间轴格式

* fix: bug#27974、28378、28380【数据字典】内置的数据字典应不能进行编辑,删除,禁用

* fix: bug#28399物模型-属性定义-其他配置-指标配置,删除操作无响应,点击确认按钮也无响应;【采集器】采集器可以扫描后批量选择点位

* fix: bug#28387、28392、28393

* fix: bug#28399物模型-属性定义-其他配置-指标配置,删除操作无响应,点击确认按钮也无响应

* fix: bug#28026 【采集器】采集器可以扫描后批量选择点位

* fix: bug#28403产品(设备)页面中的告警记录展示需要有告警记录(告警中心)查询权限,告警记录处理按钮需要有告警记录(告警中心)中的操作权限

* fix: bug#28413、28415【应用管理】编辑内部集成应用,清空接入地址也能校验成功、应用类型字段为空

* fix: bug#28409 【应用管理】启用或禁用时,整个页面会进行刷新操作,使用感不太好

* fix: bug#28440 【证书管理】证书管理上传对应的证书文件后,依然后错误提示;【设备接入网关】新的OneNet网关查看详情中无数据

* fix: bug#28403、28423、28427

* fix: bug#物模型属性定义其他配置,阈值限制不展开渲染导致校验逻辑不通过,确定按钮没有效果的bug

* fix: bug#28422 【设备接入网关】新的OneNet网关查看详情中无数据

* fix: bug#28440 【证书管理】证书管理上传对应的证书文件后,依然后错误提示

* update: 替换播放器插件为xgplayer

* fix: 【场景联动】-过滤条件优化告警下拉选择

* fix: 【场景联动】修复执行动作-标签没有下拉数据

* fix: 【告警配置】优化卡片数据展示

* update: 替换播放器插件为xgplayer

* update: 更新相关播放器逻辑

* fix: bug#28443、284444

* fix: bug 修改预处理数据相关bug

* fix: bug#28443、284444

* fix: 修复物模型-规则编辑开始运行参数添加类型

* fix: 更换全局AlarmLevelIcon组件

* fix: bug#28434 修复编辑未绘制范围也能进行保存

* fix: 优化物模型-规则编辑ts类型

* fix: 修复物模型-规则编辑开始运行参数添加类型

* fix: 更新monaco-editor版本,修复高亮报错异常

* fix: 修改2.2版本ui

* fix: 优化场景联动、告警配置ui

* fix: 更新可编辑表格右键菜单

* fix: 优化场景联动、告警配置ui

* fix: 【场景联动】兼容上传物模型没有expands属性

* fix: 告警配置搜索条件优化;【设备】预处理数据-告警数据,列表排序错乱

* fix: 修改2.2版本bug和ui

* fix: bug#28462【产品】预处理数据-告警数据-告警日志 下方的文案需进行优化

* fix: bug#28464【设备】预处理数据-告警数据,列表排序错乱

* fix: 告警配置搜索条件优化

* fix: 隐藏告警配置场景联动列表项

Co-authored-by: leiqiaochu <leiqiaochu@aossci.com>

* fix: 展示设备和产品详情告警日志的告警等级

* fix: 优化全局modal样式

* fix: 【设备管理】优化仪表盘x轴时间格式

* fix: 优化全局滚动条样式

* fix: 【场景联动】优化执行动作-选择方式样式

* fix: 优化全局modal样式

* fix: 修改设备和产品详情告警查询接口

* fix: 【场景联动】优化条件按钮颜色

* fix: bug#28467、28466

* fix: 修改设备和产品详情告警查询接口

* fix: 修改预处理数据ui样式

* fix: bug#28467、28466

* fix: bug#27001【远程升级】升级进度展示数据与需求不符

* fix: bug#28471【产品】拓展配置处理方式说明文案优化

* release: 2.2.0

* update: 更新docker版本号
This commit is contained in:
XieYongHong 2024-08-01 14:50:54 +08:00 committed by GitHub
parent a28ac3bc85
commit 0beea4d461
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
444 changed files with 32349 additions and 11505 deletions

View File

@ -15,6 +15,8 @@ module.exports = {
'style', // 格式(不影响代码变动)
'revert', // 撤销commit 回滚上一版本
'perf', // 性能优化
'remove', //删除
'release', //
]
],
'scope-case': [0],
@ -24,11 +26,11 @@ module.exports = {
rules: {
"commit-rule": ({ raw }) => {
return [
/^\[(build|feat|fix|update|refactor|docs|chore|style|revert|perf)].+/g.test(raw),
/^\[(build|feat|fix|update|refactor|docs|chore|style|revert|perf|remove|release)].+/g.test(raw),
`commit备注信息格式错误格式为 <[type] 修改内容>type支持${types.join(",")}`
]
}
}
}
]
}
}

3
.gitignore vendored
View File

@ -24,4 +24,5 @@ components.d.ts
*.njsproj
*.sln
*.sw?
.history
.history
du-i18n.config.json

View File

@ -1,3 +1,3 @@
#!/usr/bin/env bash
docker build -t registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-ui-vue:2.2.0-SNAPSHOT .
docker push registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-ui-vue:2.2.0-SNAPSHOT
docker build -t registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-ui-vue:2.3.0-SNAPSHOT .
docker push registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-ui-vue:2.3.0-SNAPSHOT

View File

@ -1,6 +1,6 @@
export default {
theme: {
'primary-color': '#1d39c4',
'primary-color': '#1677FF',
},
logo: '/favicon.ico', // 浏览器标签页logo
title: 'Jetlinks', // 浏览器标签页title
@ -10,4 +10,4 @@ export default {
mode: 'inline',
theme: 'light', // 'dark' 'light'
}
}
}

25
du-i18n.config.json Normal file
View File

@ -0,0 +1,25 @@
{
"quoteKeys": [
"$t",
"$t",
"i18n.t"
],
"defaultLang": "zh",
"tempLangs": [
"zh",
"en"
],
"langPaths": "**/src/i18n/locale/**",
"transSourcePaths": "**/src/i18n/source/**",
"tempPaths": "**/src/i18n/temp/**",
"tempFileName": "",
"multiFolders": [
"src",
"views"
],
"uncheckMissKeys": [],
"isSingleQuote": true,
"isOnlineTrans": true,
"baiduAppid": "20240704002091621",
"baiduSecrectKey": "CQdjuZ1v8AaZtx1NQQsW"
}

View File

@ -1,7 +1,7 @@
{
"name": "jetlinks-vue",
"private": true,
"version": "0.0.0",
"version": "2.2.0",
"scripts": {
"dev": "vite --mode develop",
"dev:force": "vite --force --mode develop",
@ -14,19 +14,27 @@
},
"dependencies": {
"@ant-design/icons-vue": "^7.0.1",
"@fullcalendar/core": "^6.1.13",
"@fullcalendar/daygrid": "^6.1.13",
"@fullcalendar/interaction": "^6.1.13",
"@fullcalendar/vue3": "^6.1.13",
"@liveqing/liveplayer-v3": "^3.7.10",
"@types/axios": "^0.14.0",
"@types/marked": "^4.0.8",
"@vitejs/plugin-vue-jsx": "^3.0.0",
"@vuemap/vue-amap": "^1.1.20",
"@vuemap/vue-amap": "^2.1.2",
"@vueuse/core": "^9.10.0",
"ant-design-vue": "^3.2.15",
"async-validator": "^4.2.5",
"axios": "^1.2.1",
"colorpicker-v3": "^2.10.2",
"cronstrue": "^2.50.0",
"driver.js": "^0.9.8",
"echarts": "^5.4.1",
"event-source-polyfill": "^1.0.31",
"global": "^4.4.0",
"jetlinks-store": "^0.0.3",
"jetlinks-ui-components": "^1.0.38",
"jetlinks-ui-components": "^1.0.47",
"jsencrypt": "^3.3.2",
"less": "^4.1.3",
"less-loader": "^11.1.0",
@ -46,9 +54,10 @@
"markdown-it-toc-done-right": "^4.2.0",
"marked": "^4.2.12",
"moment": "^2.29.4",
"monaco-editor": "^0.36.0",
"monaco-editor": "^0.50.0",
"nrm": "^1.2.5",
"pinia": "^2.0.28",
"resize-observer-polyfill": "^1.5.1",
"rollup-plugin-copy": "^3.4.0",
"rxjs": "^7.8.1",
"unplugin-auto-import": "^0.12.1",
@ -57,10 +66,15 @@
"vite-plugin-monaco-editor": "^1.1.0",
"vue": "3.3.4",
"vue-cropper": "^1.0.9",
"vue-i18n": "^9.13.1",
"vue-json-viewer": "^3.0.4",
"vue-router": "^4.1.6",
"vue3-json-viewer": "^2.2.2",
"vue3-ts-jsoneditor": "^2.7.1"
"vue3-ts-jsoneditor": "^2.7.1",
"xgplayer": "^3.0.19",
"xgplayer-flv": "^3.0.20-beta.0",
"xgplayer-hls": "^3.0.19",
"xgplayer-hls.js": "2.2.2"
},
"devDependencies": {
"@commitlint/cli": "^17.4.1",

View File

@ -295,7 +295,6 @@ function getSideEffects(compName: string, options: JetlinksVueResolverOptions, _
return
const lib = options.cjs ? 'lib' : 'es'
const packageName = options?.packageName || 'jetlinks-ui-components'
if (importStyle === 'less' || importLess) {
const styleDir = getStyleDir(compName, _isAntd)
return `${packageName}/${lib}/${styleDir}/style`
@ -319,8 +318,25 @@ const primitiveNames = ['AIcon','Affix', 'Anchor', 'AnchorLink', 'message', 'Not
'DataTableObject',
'CheckButton',
]
const prefix = 'J'
const proComponents = [
'ProTable', 'Search', 'AdvancedSearch', 'Ellipsis', 'MonacoEditor', 'ProLayout', 'ScrollTable', 'TableCard', 'Scrollbar', 'CardSelect', 'PopconfirmModal', 'DataTable',
'DataTableArray',
'DataTableString',
'DataTableInteger',
'DataTableDouble',
'DataTableBoolean',
'DataTableEnum',
'DataTableFile',
'DataTableDate',
'DataTableTypeSelect',
'DataTableObject',
'CheckButton',
'ValueItem'
]
let jetlinksNames: Set<string>
function genJetlinksNames(primitiveNames: string[]): void {
@ -356,11 +372,20 @@ export function JetlinksVueResolver(options: JetlinksVueResolverOptions = {}): a
}
const _isJetlinks = isJetlinks(name)
const _isAntd = isAntdv(name)
if ((_isJetlinks || _isAntd) && !options?.exclude?.includes(name)) {
// const importName = filterName.includes(name) ? name : name.slice(1)
//
// options.packageName = proComponents.includes(importName) ? 'jetlinks-ui-components' : 'ant-design-vue'
//
// const path = `${options.packageName}/${options.cjs ? 'lib' : 'es'}`
// const stylePath = getSideEffects(importName, options, !proComponents.includes(importName))
const importName = filterName.includes(name) ? name : name.slice(1)
options.packageName = _isJetlinks ? 'jetlinks-ui-components' : 'ant-design-vue'
const path = `${options.packageName}/${options.cjs ? 'lib' : 'es'}`
const stylePath = getSideEffects(importName, options, _isAntd)
return {
name: importName,
from: path,
@ -369,4 +394,4 @@ export function JetlinksVueResolver(options: JetlinksVueResolverOptions = {}): a
}
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,5 +1,10 @@
<template>
<ConfigProvider :locale='zhCN'>
<ConfigProvider
:locale='zhCN'
:IconConfig="{
scriptUrl: '//at.alicdn.com/t/c/font_4035907_i1jazcune3.js'
}"
>
<router-view />
</ConfigProvider>
</template>
@ -9,7 +14,6 @@ import { ConfigProvider } from 'jetlinks-ui-components'
import zhCN from 'jetlinks-ui-components/es/locale/zh_CN';
import { storeToRefs } from 'pinia';
import { useSystem } from './store/system';
import DefaultSetting from '../config/config'
import {LocalStore} from "@/utils/comm";
import {TOKEN_KEY} from "@/utils/variable";

View File

@ -4,6 +4,8 @@ import { SearchHistoryList } from 'components/Search/types'
export const FILE_UPLOAD = `${BASE_API_PATH}/file/static`;
export const FileUpload = `${BASE_API_PATH}/file/upload`;
/**
*
* @param data
@ -31,11 +33,11 @@ export const systemVersion = () => server.get<{edition?: string}>('/system/versi
/**
*
* @param data
* @returns
* @param data
* @returns
*/
export const queryDashboard = (data: Record<string, any>) => server.post(`/dashboard/_multi`, data)
export const fileUpload = (data: any) => server.post('/file/static', data)
export const lowCodeUrl = () => server.get('/system/config/low-code')
export const lowCodeUrl = () => server.get('/system/config/low-code')

View File

@ -83,5 +83,15 @@ export const getBacnetObjectList = (channelId: string, instanceNumber: string) =
*/
export const getBacnetPropertyIdNotUse = (data: any) => server.post(`/collect/bacnet/${data.collectorId}/unused/ids`, data)
// /**
// * 查询所有属性id
// */
// export const getBacnetAllPropertyId = () => server.get('/collect/bacnet/property/ids')
/**查询bacnet值类型*/
export const getBacnetValueType = () => server.get(`/collect/bacnet/value/types`)
export const getBacnetValueType = () => server.get(`/collect/bacnet/value/types`)
/**
*
*/
export const exportPoint = (collectorId:string,provider:string) => server.get(`/data-collect/point/${collectorId}/${provider}/export.xlsx`, {}, {responseType: 'blob'})

View File

@ -11,12 +11,16 @@ export const remove = (id: string) => server.remove(`/firmware/${id}`);
export const query = (data: object) => server.post(`/firmware/_query/`, data);
export const queryPaginateNot = (data: object) => server.post('/firmware/_query/no-paging',data)
export const querySystemApi = (data?: object) =>
server.post(`/system/config/scopes`, data);
export const task = (data: Record<string, unknown>) =>
server.post(`/firmware/upgrade/task/detail/_query`, data);
export const queryTaskPaginateNot = (data:any)=> server.post('/firmware/upgrade/task/detail/_query/no-paging',data)
export const taskById = (id: string) =>
server.get(`/firmware/upgrade/task/${id}`);
@ -29,6 +33,10 @@ export const deleteTask = (id: string) =>
export const history = (data: Record<string, unknown>) =>
server.post(`/firmware/upgrade/history/_query`, data);
export const historyPaginateNot =(data:Record<string,unknown>) =>
server.post('/firmware/upgrade/history/_query/no-paging',data)
export const historyCount = (data: Record<string, unknown>) =>
server.post(`/firmware/upgrade/history/_count`, data);
@ -41,6 +49,8 @@ export const stopTask = (id: string) =>
export const startOneTask = (data: string[]) =>
server.post(`/firmware/upgrade/task/_start`, data);
export const stopOneTask = (data: string[]) =>
server.post('/firmware/upgrade/task/_stop',data)
// export const queryProduct = (data?: any) =>
// server.post(`/device-product/_query/no-paging`, data);
export const queryProduct = (data?: any) =>

View File

@ -14,7 +14,7 @@ export const resetRule = (productId:string,deviceId:string,data:any) => server.r
/**
*
* @param deviceId ID
* @returns
* @returns
*/
export const deleteMetadata = (deviceId: string) => server.remove(`/device-instance/${deviceId}/metadata`)
@ -22,7 +22,7 @@ export const deleteMetadata = (deviceId: string) => server.remove(`/device-insta
*
* @param id ID
* @param data
* @returns
* @returns
*/
export const saveMetadata = (id: string, data: DeviceMetadata) => server.put(`/device/instance/${id}/metadata`, data)
@ -36,58 +36,58 @@ export const detail = (id: string) => server.get<DeviceInstance>(`/device-instan
/**
*
* @param data
* @returns
* @returns
*/
export const query = (data?: Record<string, any>) => server.post('/device-instance/_query', data)
/**
*
* @param data
* @returns
* @param data
* @returns
*/
export const queryNoPagingPost = (data?: Record<string, any>) => server.post('/device-instance/_query/no-paging?paging=false', data)
/**
*
* @param id ID
* @returns
* @returns
*/
export const _delete = (id: string) => server.remove(`/device-instance/${id}`)
/**
*
* @param id ID
* @param data
* @returns
* @param data
* @returns
*/
export const _deploy = (id: string) => server.post(`/device-instance/${id}/deploy`)
/**
*
* @param id ID
* @param data
* @returns
* @param data
* @returns
*/
export const _undeploy = (id: string) => server.post(`/device-instance/${id}/undeploy`)
/**
*
* @param data id数组
* @returns
* @returns
*/
export const batchDeployDevice = (data: string[]) => server.put(`/device-instance/batch/_deploy`, data)
/**
*
* @param data id数组
* @returns
* @returns
*/
export const batchUndeployDevice = (data: string[]) => server.put(`/device-instance/batch/_unDeploy`, data)
/**
*
* @param data id数组
* @returns
* @returns
*/
export const batchDeleteDevice = (data: string[]) => server.put(`/device-instance/batch/_delete`, data)
@ -95,7 +95,7 @@ export const batchDeleteDevice = (data: string[]) => server.put(`/device-instanc
*
* @param productId id
* @param type
* @returns
* @returns
*/
export const deviceTemplateDownload = (productId: string, type: string) => `${BASE_API_PATH}/device-instance/${productId}/template.${type}`
@ -104,7 +104,7 @@ export const templateDownload = (productId: string, type: string) => server.get(
*
* @param productId id
* @param type
* @returns
* @returns
*/
export const deviceImport = (productId: string, fileUrl: string, autoDeploy: boolean) => `${BASE_API_PATH}/device-instance/${productId}/import/_withlog?fileUrl=${fileUrl}&autoDeploy=${autoDeploy}&:X_Access_Token=${LocalStore.get(TOKEN_KEY)}`
@ -112,21 +112,21 @@ export const deviceImport = (productId: string, fileUrl: string, autoDeploy: boo
*
* @param productId id
* @param type
* @returns
* @returns
*/
export const deviceExport = (productId: string, type: string, params?: any) => server.get(`/device-instance${!!productId ? `/${productId}` : ''}/export.${type}`, params, {responseType: 'blob'})
export const deviceExportPath = (productId: string, type: string) => (`${BASE_API_PATH}/device-instance${!!productId ? `/${productId}` : ''}/export.${type}`)
/**
* ID是否重复
* @param id id
* @returns
* @returns
*/
export const isExists = (id: string) => server.get(`/device-instance/${id}/exists`)
/**
*
* @param data
* @returns
* @returns
*/
export const update = (data: Partial<DeviceInstance>) => data.id ? server.patch(`/device-instance`, data) : server.post(`/device-instance`, data)
@ -134,27 +134,27 @@ export const update = (data: Partial<DeviceInstance>) => data.id ? server.patch(
*
* @param id id
* @param data
* @returns
* @returns
*/
export const modify = (id: string, data: Partial<DeviceInstance>) => server.put(`/device-instance/${id}`, data)
/**
*
* @param id id
* @returns
* @returns
*/
export const getConfigMetadata = (id: string) => server.get(`/device-instance/${id}/config-metadata`)
/**
*
* @param id id
* @returns
* @returns
*/
export const _disconnect = (id: string) => server.post(`/device-instance/${id}/disconnect`)
/**
*
* @returns
* @returns
*/
export const queryUserListNoPaging = () => server.post(`/user/_query/no-paging`, {
paging: false,
@ -164,16 +164,16 @@ export const queryUserListNoPaging = () => server.post(`/user/_query/no-paging`,
/**
*
* @param id id
* @param data
* @returns
* @param data
* @returns
*/
export const saveRelations = (id: string, data: Record<string, any>) => server.patch(`/device/instance/${id}/relations`, data)
/**
*
* @param id id
* @param data
* @returns
* @param data
* @returns
*/
export const saveTags = (id: string, data: Record<string, any>) => server.patch(`/device/instance/${id}/tag`, data)
@ -181,14 +181,14 @@ export const saveTags = (id: string, data: Record<string, any>) => server.patch(
*
* @param deviceId id
* @param id id
* @returns
* @returns
*/
export const delTags = (deviceId: string, id: string) => server.remove(`/device/instance/${deviceId}/tag/${id}`)
/**
*
* @param deviceId id
* @returns
* @returns
*/
export const configurationReset = (deviceId: string) => server.put(`/device-instance/${deviceId}/configuration/_reset`)
@ -196,16 +196,16 @@ export const configurationReset = (deviceId: string) => server.put(`/device-inst
*
* @param deviceId id
* @param eventId id
* @param data
* @returns
* @param data
* @returns
*/
export const getEventList = (deviceId: string, eventId: string, data: Record<string, any>) => server.post(`/device-instance/${deviceId}/event/${eventId}?format=true`, data)
/**
*
* @param deviceId id
* @param data
* @returns
* @param data
* @returns
*/
export const setProperty = (deviceId: string, data: Record<string, any>) => server.put(`/device-instance/${deviceId}/property`, data)
@ -213,7 +213,7 @@ export const setProperty = (deviceId: string, data: Record<string, any>) => serv
*
* @param deviceId id
* @param type id
* @returns
* @returns
*/
export const getProperty = (deviceId: string, type: string) => server.get(`/device/standard/${deviceId}/property/${type}`)
@ -221,7 +221,7 @@ export const getProperty = (deviceId: string, type: string) => server.get(`/devi
*
* @param deviceId id
* @param propertyId id
* @returns
* @returns
*/
export const queryMetric = (deviceId: string, propertyId: string) => server.get(`/device-instance/${deviceId}/metric/property/${propertyId}`)
@ -229,8 +229,8 @@ export const queryMetric = (deviceId: string, propertyId: string) => server.get(
*
* @param deviceId id
* @param propertyId id
* @param data
* @returns
* @param data
* @returns
*/
export const saveMetric = (deviceId: string, propertyId: string, data: Record<string, any>) => server.patch(`/device-instance/${deviceId}/metric/property/${propertyId}`, data)
@ -238,24 +238,24 @@ export const saveMetric = (deviceId: string, propertyId: string, data: Record<st
*
* @param deviceId id
* @param childrenId id
* @param data
* @returns
* @param data
* @returns
*/
export const unbindDevice = (deviceId: string, childrenId: string, data: Record<string, any>) => server.post(`/device/gateway/${deviceId}/unbind/${childrenId}`, data)
/**
*
* @param deviceId id
* @param data
* @returns
* @param data
* @returns
*/
export const unbindBatchDevice = (deviceId: string, data: Record<string, any>) => server.post(`/device/gateway/${deviceId}/unbind`, data)
/**
*
* @param deviceId id
* @param data
* @returns
* @param data
* @returns
*/
export const bindDevice = (deviceId: string, data: Record<string, any>) => server.post(`/device/gateway/${deviceId}/bind`, data)
@ -271,7 +271,7 @@ export const queryDeviceMapping = (deviceId: string, data?: any) => server.post(
export const saveDeviceMapping = (deviceId: string, data: any) => server.post(`/edge/operations/${deviceId}/device-mapping-save-batch/invoke`, data)
/**
*
*
*/
export const deleteDeviceMapping = (deviceId: string, data:any) => server.post(`/edge/operations/${deviceId}/device-mapping-delete-by-deviceid/invoke`, data)
@ -294,64 +294,64 @@ export const addDevice = (params: any) => server.post("/device-instance", params
/**
*
* @param id id
* @returns
* @returns
*/
export const queryGatewayState = (id: string) => server.get(`/gateway/device/${id}/detail`)
/**
*
* @param id id
* @returns
* @returns
*/
export const queryNetworkState = (id: string) => server.get(`/network/config/${id}`)
/**
*
* @param id id
* @returns
* @returns
*/
export const queryProductState = (id: string) => server.get(`/device/product/${id}`)
/**
*
* @param id id
* @returns
* @returns
*/
export const queryProductConfig = (id: string) => server.get(`/device/product/${id}/config-metadata`)
/**
*
* @param id id
* @returns
* @returns
*/
export const queryDeviceConfig = (id: string) => server.get(`/device-instance/${id}/config-metadata`)
/**
*
* @param type
* @param transport
* @returns
* @param type
* @param transport
* @returns
*/
export const queryProtocolDetail = (type: string, transport: string) => server.get(`/protocol/${type}/transport/${transport}`)
/**
*
* @param id ID
* @returns
* @returns
*/
export const startNetwork = (id: string) => server.post(`/network/config/${id}/_start`)
/**
*
* @param id id
* @returns
* @returns
*/
export const startGateway = (id: string) => server.post(`/gateway/device/${id}/_startup`)
/**
*
* @param id id
* @returns
* @returns
*/
export const getGatewayDetail = (id: string) => server.get(`/gateway/device/${id}`)
@ -366,231 +366,231 @@ export const getUnit = () => server.get<UnitType[]>(`/protocol/units`)
*
* @param deviceId id
* @param functionId id
* @param data
* @returns
* @param data
* @returns
*/
export const executeFunctions = (deviceId: string, functionId: string, data: any) => server.post(`/device/invoked/${deviceId}/function/${functionId}`, data)
/**
*
* @param deviceId id
* @param data
* @returns
* @param data
* @returns
*/
export const readProperties = (deviceId: string, data: any) => server.post(`/device/instance/${deviceId}/properties/_read`, data)
/**
*
* @param deviceId id
* @param data
* @returns
* @param data
* @returns
*/
export const settingProperties = (deviceId: string, data: any) => server.put(`/device/instance/${deviceId}/property`, data)
/**
* -
* @param id id
* @param action
* @param data
* @returns
* @param action
* @param data
* @returns
*/
export const execute = (id: string, action: string, data: any) => server.post(`/device/invoked/${id}/function/${action}`, data)
/**
*
* @param data
* @returns
* @param data
* @returns
*/
export const queryChannelNoPaging = (data: any) => server.post(`data-collect/channel/_query/no-paging`, data)
/**
*
* @param data
* @returns
* @param data
* @returns
*/
export const queryCollectorNoPaging = (data: any) => server.post(`/data-collect/collector/_query/no-paging`, data)
/**
*
* @param data
* @returns
* @param data
* @returns
*/
export const queryPointNoPaging = (data: any) => server.post(`/data-collect/point/_query/no-paging`, data)
/**
*
* @param thingType
* @param thingId
* @param params
* @returns
* @param thingType
* @param thingId
* @param params
* @returns
*/
export const queryMapping = (thingType: string, thingId: any, params?: any) => server.get(`/things/collector/${thingType}/${thingId}/_query`, params)
/**
*
* @param thingType
* @param thingId
* @param data
* @returns
* @param thingType
* @param thingId
* @param data
* @returns
*/
export const removeMapping = (thingType: string, thingId: any, data?: any) => server.post(`/things/collector/${thingType}/${thingId}/_delete`, data)
/**
*
* @param data
* @returns
* @param data
* @returns
*/
export const treeMapping = (data?: any) => server.post(`/data-collect/channel/_all/tree`, data)
/**
*
* @param thingId
* @param provider
* @param data
* @returns
* @param thingId
* @param provider
* @param data
* @returns
*/
export const saveMapping = (thingId: any, provider: string, data?: any) => server.patch(`/things/collector/device/${thingId}/${provider}`, data)
/**
*
* @param deviceId
* @param data
* @returns
* @param deviceId
* @param data
* @returns
*/
export const edgeChannel = (deviceId: string, data?: any) => server.post(`/edge/operations/${deviceId}/data-collector-channel-list/invoke`, data)
/**
*
* @param deviceId
* @param data
* @returns
* @param deviceId
* @param data
* @returns
*/
export const edgeCollector = (deviceId: string, data?: any) => server.post(`/edge/operations/${deviceId}/data-collector-list/invoke`, data)
/**
*
* @param deviceId
* @param data
* @returns
* @param deviceId
* @param data
* @returns
*/
export const edgePoint = (deviceId: string, data?: any) => server.post(`/edge/operations/${deviceId}/data-collector-point-list/invoke`, data)
/**
*
* @param deviceId
* @param data
* @returns
*
* @param deviceId
* @param data
* @returns
*/
export const getEdgeMap = (deviceId: string, data?: any) => server.post(`/edge/operations/${deviceId}/device-collector-list/invoke`, data)
/**
*
* @param deviceId
* @param data
* @returns
*
* @param deviceId
* @param data
* @returns
*/
export const removeEdgeMap = (deviceId: string, data?: any) => server.post(`/edge/operations/${deviceId}/device-collector-delete/invoke`, data)
/**
*
* @param deviceId
* @param data
* @returns
*
* @param deviceId
* @param data
* @returns
*/
export const treeEdgeMap = (deviceId: string, data?: any) => server.post(`/edge/operations/${deviceId}/data-collector-channel-tree/invoke`, data)
/**
*
* @param deviceId
* @param data
* @returns
*
* @param deviceId
* @param data
* @returns
*/
export const saveEdgeMap = (deviceId: string, data?: any) => server.post(`/edge/operations/${deviceId}/device-collector-save/invoke`, data)
/**
*
* @param deviceId
* @param params
* @returns
* @param deviceId
* @param params
* @returns
*/
export const getPropertyData = (deviceId: string, params: Record<string, unknown>) => server.get(`/device-instance/${deviceId}/properties/_query`, params)
/**
*
* @param deviceId
* @param data
* @returns
* @param deviceId
* @param data
* @returns
*/
export const getPropertiesInfo = (deviceId: string, data: Record<string, unknown>) => server.post(`/device-instance/${deviceId}/agg/_query`, data)
/**
*
* @param deviceId
* @param data
* @returns
* @param deviceId
* @param data
* @returns
*/
export const getPropertiesList = (deviceId: string, property: string, data: Record<string, unknown>) => server.post(`/device-instance/${deviceId}/property/${property}/_query/no-paging`, data)
/**
*
* @param id
* @param transport
* @returns
* @param id
* @param transport
* @returns
*/
export const getProtocal = (id: string, transport: string) => server.get(`/protocol/${id}/transport/${transport}`)
/**
*
* @param productId
* @returns
* @param productId
* @returns
*/
export const productCode = (productId: string) => server.get(`/device/transparent-codec/${productId}`)
/**
*
* @param productId
* @returns
* @param productId
* @returns
*/
export const saveProductCode = (productId: string, data: Record<string, unknown>) => server.post(`/device/transparent-codec/${productId}`, data)
/**
*
* @param productId
* @param deviceId
* @returns
* @param productId
* @param deviceId
* @returns
*/
export const deviceCode = (productId: string, deviceId: string) => server.get(`device/transparent-codec/${productId}/${deviceId}`)
/**
*
* @param productId
* @param productId
*
* @param deviceId
* @param data
* @returns
* @param deviceId
* @param data
* @returns
*/
export const saveDeviceCode = (productId: string, deviceId: string, data: Record<string, unknown>) => server.post(`/device/transparent-codec/${productId}/${deviceId}`, data)
/**
*
* @param data
* @returns
* @param data
* @returns
*/
export const testCode = (data: Record<string, unknown>) => server.post(`/device/transparent-codec/decode-test`, data)
/**
*
* @param productId
* @param deviceId
* @returns
* @param productId
* @param deviceId
* @returns
*/
export const delDeviceCode = (productId: string, deviceId: string) => server.remove(`/device/transparent-codec/${productId}/${deviceId}`)
/**
*
* @param productId
* @returns
* @param productId
* @returns
*/
export const delProductCode = (productId: string) => server.remove(`/device/transparent-codec/${productId}`)
export const queryLog = (deviceId: string, data: Record<string, unknown>) => server.post(`/device-instance/${deviceId}/logs`, data)
/**
*
* @returns
* @returns
*/
export const queryLogsType = () => server.get(`/dictionary/device-log-type/items`)
@ -626,13 +626,60 @@ export const queryProductCodeTips = (productId: string) => server.get(`/device/t
/**
* TS
* @param deviceId ID
* @returns
* @returns
*/
export const queryTypescript = (deviceId:string) => server.get(`/device/${deviceId}/virtual-property.d.ts`)
export const queryTypescript = (deviceId:string) => server.get(`/device/${deviceId}/virtual-property.d.ts`)
/**
* TS
* @param productId ID
* @returns
*/
export const queryProductTs = (productId:string) => server.get(`/product/${productId}/virtual-property.d.ts`)
/**
* -/-
* @param data
*/
export const updateProductThreshold = (productId:string,propertyId:string,data: any) => server.put(`/message/preprocessor/product/${productId}/property/${propertyId}`, data)
/**
* -/-
* @param data
*/
export const updateDeviceThreshold = (productId:string,deviceId:string,propertyId:string,data: any) => server.put(`/message/preprocessor/device/${productId}/${deviceId}/property/${propertyId}`, data)
/**
* -
* @param productId
* @param deviceId
* @param propertyId
*/
export const queryDeviceThreshold = (productId: string, deviceId: string, propertyId: string) => server.get(`/message/preprocessor/device/${productId}/${deviceId}/property/${propertyId}`)
/**
* -
* @param productId
* @param propertyId
*/
export const queryProductThreshold = (productId: string, propertyId: string) => server.get(`/message/preprocessor/product/${productId}/property/${propertyId}`)
/**
* -
* @param productId
* @param propertyId
* @returns
*/
export const queryProductTs = (productId:string) => server.get(`/product/${productId}/virtual-property.d.ts`)
export const deleteProductThreshold = (productId:string,propertyId:string,data:any) => server.remove(`/message/preprocessor/product/${productId}/property/${propertyId}`,data)
/**
* -
* @param productId
* @param propertyId
* @returns
*/
export const deleteDeviceThreshold = (productId:string,deviceId:string,propertyId:string,data:any) => server.remove(`/message/preprocessor/device/${productId}/${deviceId}/property/${propertyId}`,data)
export const getTemplate = (id: string, format: string) => `${BASE_API_PATH}/device/instance/${id}/property-metadata/template.${format}`
export const uploadAnalyzeMetadata = (data: any) => server.post('/device/instance/property-metadata/file/analyze', data)

View File

@ -1,11 +1,12 @@
import { OperatorItem } from '@/components/FRuleEditor/Operator/typings'
import server from '@/utils/request'
import { DeviceMetadata, ProductItem, DepartmentItem, MetadataType } from '@/views/device/Product/typings'
import {BASE_API_PATH} from "@/utils/variable";
/**
*
* @param data
* @returns
* @returns
*/
export const queryNoPagingPost = (data: any) => server.post(`/device-product/_query/no-paging?paging=false`, data)
@ -14,7 +15,7 @@ export const queryNoPagingPost = (data: any) => server.post(`/device-product/_qu
* @param direction from|to
* @param type
* @param data
* @returns
* @returns
*/
export const convertMetadata = (direction: 'from' | 'to', type: string, data: any) => server.post<DeviceMetadata>(`/device/product/metadata/convert-${direction}/${type}`, data)
@ -22,20 +23,20 @@ export const convertMetadata = (direction: 'from' | 'to', type: string, data: an
*
* @param id ID
* @param data
* @returns
* @returns
*/
export const modify = (id: string, data: any) => server.put(`/device-product/${id}`, data)
/**
*
* @returns
*
* @returns
*/
export const getCodecs = () => server.get<{id: string, name: string}>('/device/product/metadata/codecs')
/**
* ID获取产品详情
* @param id ID
* @returns
* @returns
*/
export const detail = (id: string) => server.get<ProductItem>(`/device-product/${id}`)
@ -80,31 +81,31 @@ export const category = (data: any) => server.get('/device/category/_tree?paging
/**
*
* @param productId ID
* @param data
* @returns
* @param data
* @returns
*/
export const _deploy = (productId: string) => server.post(`/device-product/${productId}/deploy`)
/**
*
* @param productId ID
* @param data
* @returns
* @param data
* @returns
*/
export const _undeploy = (productId: string) => server.post(`/device-product/${productId}/undeploy`)
/**
*
* @param data
* @returns
* @param data
* @returns
*/
export const addProduct = (data:any) => server.post('/device-product',data)
/**
*
* @param id ID
* @param data
* @returns
* @param data
* @returns
*/
export const editProduct = (data: any) => server.put(`/device-product/${data.id}`, data)
@ -122,7 +123,7 @@ export const deleteProduct = (id: string) => server.remove(`/device-product/${id
/**
*
* @param data
* @returns
* @returns
*/
export const saveProductMetadata = (data: Record<string, unknown>) => server.patch('/device-product', data)
@ -219,5 +220,7 @@ export const saveProductVirtualProperty = (productId: string, data: any[]) => se
export const queryProductVirtualProperty = (productId: string, propertyId: string) => server.get(`/virtual/property/product/${productId}/${propertyId}`)
export const getTemplate = (id: string, format: string) => `${BASE_API_PATH}/device/product/${id}/property-metadata/template.${format}`

View File

@ -7,6 +7,11 @@ import server from '@/utils/request'
*/
export const query = (data: Record<string, any>) => server.post('/device/aliyun/bridge/_query', data)
/**
*
*/
export const queryPaginateNot = (data: any) => server.post('/device/aliyun/bridge/_query/no-paging', data)
/**
*
* @param data

View File

@ -7,6 +7,11 @@ import server from '@/utils/request'
*/
export const query = (data: Record<string, any>) => server.post('/dueros/product/_query', data)
/**
*
*/
export const queryPaginateNot = (data:any) => server.post('/dueros/product/_query/no-paging',data)
/**
*
* @param id

View File

@ -46,7 +46,15 @@ export const detail = (id:string) => server.get(`/alarm/config/${id}`);
export const unbindScene = (id:string,data:any) => server.post(`/alarm/rule/bind/${id}/_delete`,data);
export const unBindAlarm = (id: string, alarmId: string, data: any) => server.post(`/alarm/rule/bind/${alarmId}/${id}/_delete`, data)
export const unBindAlarmMultiple = (data: any) => server.post(`/alarm/rule/bind/_delete`, data)
/**
*
*/
export const bindScene = (data:any) => server.patch("/alarm/rule/bind",data)
/**
* id
* @param data
*/
export const queryBindScene = (data:any) => server.post("/alarm/rule/bind/_query",data)

View File

@ -25,6 +25,13 @@ export const getOrgList = (parmas?:any) => server.get('/organization/_query/no-p
*/
export const query = (data:any) => server.post('/alarm/record/_query/',data);
/**
*
* @param data
* @returns
*/
export const queryByDevice = (data:any) => server.post(`/alarm/record/device/_query`,data)
/**
*
*/
@ -49,4 +56,14 @@ export const queryHistoryList = (data:any) => server.post('/alarm/history/_query
/**
*
*/
export const queryHandleHistory = (data:any) => server.post('/alarm/record/handle-history/_query',data);
export const queryHandleHistory = (data:any) => server.post('/alarm/record/handle-history/_query',data);
/**
*
*/
export const queryLogList = (alarmConfigId:any,data:any) => server.post(`/alarm/history/${alarmConfigId}/_query`,data)
/**
*
*/
export const queryInvalidData = (data:any) => server.post('/message/preprocessor/invalid/_query',data)

View File

@ -27,4 +27,7 @@ export const queryBuiltInParams = (data: any, params?: any) => server.post(`/sce
export const getParseTerm = (data: Record<string, any>) => server.post(`/scene/parse-term-column`, data)
export const queryAlarmList = (data: Record<string, any>) => server.post(`/alarm/config/_query/`, data)
export const queryAlarmPage = (data: Record<string, any>) => server.post(`/alarm/config/_query`, data)
export const queryAlarmList = (data: Record<string, any>) => server.post(`/alarm/config/_query/no-paging`, data)
export const queryAlarmCount = (data: Record<string, any>) => server.post(`/alarm/config/_count`, data)

View File

@ -0,0 +1,17 @@
import server from '@/utils/request';
//编辑标签
export const saveTag = (data:any) => server.patch('/calendar/tags',data)
//查询标签列表
export const queryTags = () => server.get('/calendar/tags')
//删除标签
export const deleteTags = (ids:any) => server.remove('/calendar/tags',{},{data:ids})
//保存标签颜色
export const saveTagsColor = (data:any) => server.post('/system/config/calendar-tag-color',data)
//查询标签颜色
export const getTagsColor = () => server.get('/system/config/calendar-tag-color');
//查询指定日期内的日历
export const queryEvents = (dateFrom:any,dateTo:any) => server.get(`/calendar/${dateFrom}/${dateTo}`)
//批量保存指定日期的日历
export const saveEvents = (data:any) => server.patch('/calendar',data)
//清空日历
export const clearAll = () => server.remove('/calendar/mine/_all')

View File

@ -0,0 +1,116 @@
<template>
</template>
<script setup>
import { useMap } from './useMap'
import {pick} from "lodash-es";
defineOptions({
name: 'DistrictSearch'
})
const instance = useMap()
const props = defineProps({
subdistrict: {
type: Number,
default: 0
},
extensions: {
type: String,
default: 'all'
},
level: {
type: String,
default: 'district'
},
view: {
type: Boolean,
default: true
},
styles: {
type: Object,
default: () => ({})
},
adcode: {
type: String,
default: undefined
}
})
let district
let polygon
const remove = () => {
if (polygon && instance.$amapComponent) {
if (instance.$amapComponent.getLayers().length) {
instance.$amapComponent.remove(polygon)
}
polygon = null
}
}
const drawBounds = (paths) => {
if (polygon && instance.$amapComponent?.remove) {
instance.$amapComponent.remove(polygon)
polygon = null
}
for (var i = 0; i < paths.length; i += 1) {//MultiPolygonpath
paths[i] = [paths[i]]
}
const _styles = Object.assign({
strokeWeight: 1,
path: paths,
fillOpacity: 0.25,
fillColor: '#80d8ff',
strokeColor: '#0091ea'
},props.styles)
polygon = new AMap.Polygon(_styles);
instance.$amapComponent.add(polygon)
if (props.view) {
instance.$amapComponent.setFitView(polygon)
}
}
const queryDistrict = (code) => {
const opts = {
subdistrict: 0, //
extensions: 'all', //
level: 'district' //
}
const options = Object.assign(opts, pick(props, ['subdistrict', 'extensions', 'level']))
if (!district) {
district = new AMap.DistrictSearch(options)
}
if (!code) return
district.search(code, (status, result) => {
if (!result || !result.districtList || !result.districtList[0]) {
console.warn('请正确填写名称或更新其他名称');
return
}
const bounds = result.districtList[0].boundaries;
drawBounds(bounds)
})
}
onBeforeUnmount(() => {
remove()
})
watch(() => props.adcode, () =>{
queryDistrict(props.adcode)
}, { immediate: true })
</script>
<style scoped>
</style>

View File

@ -0,0 +1,91 @@
<template>
</template>
<script setup>
import { useMap } from './useMap'
import { max, min } from 'lodash-es'
defineOptions({
name: 'GeoJson'
})
const props = defineProps({
geo: {
type: Object,
default: undefined
},
view: {
type: Boolean,
default: true
},
})
const instance = useMap()
let geoJsonLayer
const remove = () => {
if (geoJsonLayer && instance.$amapComponent) {
if (instance.$amapComponent.getLayers().length) {
instance.$amapComponent.remove(geoJsonLayer)
}
geoJsonLayer = null
}
}
const drawBounds = () => {
remove()
if (!props.geo) return
geoJsonLayer = new AMap.GeoJSON({
geoJSON: props.geo,
getPolygon: (geojson, lnglats) => {
return new AMap.Polygon({
path: lnglats,
fillOpacity: 0.25,//
strokeColor: '#0091ea',
fillColor: '#80d8ff'
});
}
})
instance.$amapComponent.add(geoJsonLayer)
if (props.view) {
const points = props.geo.features.reduce((prev, next) => {
const coordinates = next.geometry.coordinates
prev.push(...coordinates[0])
return prev
}, [])
if (points.length) {
const lngArr = points.map(lnglat => lnglat[0])
const latArr = points.map(lnglat => lnglat[1])
const maxLng = max(lngArr)
const maxLat = max(latArr)
const minLng = min(lngArr)
const minLat = min(latArr)
const southWest = new AMap.LngLat(maxLng, maxLat)
const northEast = new AMap.LngLat(minLng, minLat)
const bounds = new AMap.Bounds(southWest, northEast)
instance.$amapComponent.setBounds(bounds)
}
}
}
onBeforeUnmount(() => {
remove()
})
watch(() => JSON.stringify(props.geo), () =>{
drawBounds()
}, { immediate: true })
</script>
<style scoped>
</style>

View File

@ -0,0 +1,7 @@
import AMap from './AMap.vue'
export * from './useMap'
export { default as DistrictSearch } from './DistrictSearch.vue'
export { default as GeoJson } from './GeoJson.vue'
export default AMap

View File

@ -0,0 +1,4 @@
export const useMap = () => {
return inject('parentInstance')
}

View File

@ -0,0 +1,27 @@
<template>
<img :src="src" />
</template>
<script setup>
defineOptions({
name: 'LevelIcon'
})
const props = defineProps({
level: {
type: Number,
default: undefined
}
})
const src = computed(() => {
return `/images/alarm/alarm${props.level}.png`
})
</script>
<style scoped>
</style>

View File

@ -5,12 +5,6 @@ const color = {
'success': '36, 178, 118',
'warning': '255, 144, 0',
'default': '102, 102, 102',
//告警颜色
'level1': '229, 0, 18',
'level2': '255, 148, 87',
'level3': '250, 189, 71',
'level4': '153, 153, 153',
'level5': '196, 196, 196'
}
export const getHexColor = (code: string, pe: number = 0.1) => {
const _color = color[code] || color.default

View File

@ -53,6 +53,7 @@
<script lang="ts" setup>
import { PropType } from 'vue';
import { BatchActionsType } from './types';
import { defineExpose } from 'vue';
const props = defineProps({
actions: {
@ -90,14 +91,17 @@ const reload = () => {
const onPopConfirm = (e: any, fun: any) => {
if (fun) {
fun(e);
onPopCancel();
return fun(e);
}
};
const onPopCancel = () => {
visible.value = false;
};
defineExpose({
reload
})
</script>
<style lang="less" scoped>

View File

@ -25,7 +25,7 @@
background: getBackgroundColor(statusNames[status]),
}"
></div>
<div style="display: flex">
<div class="card-content-body">
<!-- 图片 -->
<div class="card-item-avatar">
<slot name="img"> </slot>
@ -57,8 +57,8 @@
:text="statusText"
:statusNames="statusNames"
></BadgeStatus>
<CustomBadgeStatus
v-else
<CustomBadgeStatus
v-else
:status="status"
:text="statusText"
:statusNames="statusNames">
@ -176,7 +176,7 @@ const handleClick = () => {
.card {
width: 100%;
background-color: #fff;
.checked-icon {
position: absolute;
right: -22px;
@ -249,6 +249,12 @@ const handleClick = () => {
padding: 30px 12px 30px 30px;
overflow: hidden;
.card-content-body {
display: flex;
position: relative;
z-index: 99;
}
.card-item-avatar {
margin-right: 16px;
display: flex;
@ -317,6 +323,7 @@ const handleClick = () => {
}
}
.card-content-top-line {
&::before {
position: absolute;

View File

@ -0,0 +1,282 @@
<template>
<div :class="{ 'j-card-panel': true, 'no-column': noColumn }">
<j-row v-if="!noColumn" :gutter="[16, 16]">
<j-col
v-for="item in itemOptions"
:key="item.value"
:span="24 / column"
>
<div
:class="{
'j-card-item': true,
active: activeKeys.includes(item.value),
disabled: disabled || item.disabled,
horizontal: type === 'horizontal',
vertical: type === 'vertical',
right: float === 'right',
left: float === 'left',
}"
@click="() => handleSelect(item.value, item)"
>
<div class="j-card-title-warp">
<div class="title">
<slot
name="title"
:title="item.label"
:option="item"
>
<j-ellipsis>
{{ item.label }}
</j-ellipsis>
</slot>
</div>
<div
v-if="item.subLabel && showSubLabel"
class="sub-title"
>
<slot
name="subLabel"
:sub-label="item.subLabel"
:option="item"
>
{{ item.subLabel }}
</slot>
</div>
</div>
<div v-if="showImage" class="j-card-image">
<slot name="image" :image="item.iconUrl" :option="item">
<j-avatar
class="icon box-shadow"
:src="item.iconUrl"
/>
</slot>
</div>
</div>
</j-col>
</j-row>
<template v-else>
<div
v-for="item in itemOptions"
:key="item.value"
:class="{
'j-card-item': true,
active: activeKeys.includes(item.value),
disabled: disabled || item.disabled,
horizontal: type === 'horizontal',
vertical: type === 'vertical',
right: float === 'right',
left: float === 'left',
}"
@click="() => handleSelect(item.value, item)"
>
<div class="j-card-title-warp">
<div class="title">
<slot name="title" :title="item.label" :option="item">
<j-ellipsis>
{{ item.label }}
</j-ellipsis>
</slot>
</div>
<div v-if="item.subLabel && showSubLabel" class="sub-title">
<slot
name="subLabel"
:sub-label="item.subLabel"
:option="item"
>
{{ item.subLabel }}
</slot>
</div>
</div>
<div v-if="showImage" class="j-card-image">
<slot name="image" :image="item.iconUrl" :option="item">
<j-avatar class="icon box-shadow" :src="item.iconUrl" />
</slot>
</div>
</div>
</template>
</div>
</template>
<script lang="ts" setup>
import { computed, PropType, ref, toRefs, watch } from 'vue';
interface CardOption {
value: string | number;
label: string;
subLabel?: string;
iconUrl: string;
disabled?: boolean;
}
const props = defineProps({
type: {
type: String as PropType<'vertical' | 'horizontal'>,
default: 'horizontal',
},
float: {
type: String as PropType<'left' | 'right'>,
default: 'left',
},
options: {
type: Array as PropType<Array<CardOption>>,
default: () => [],
},
disabled: {
type: Boolean,
default: false,
},
multiple: {
type: Boolean,
default: false,
},
column: {
type: Number,
default: 3,
},
noColumn: {
type: Boolean,
default: false,
},
showImage: {
type: Boolean,
default: true,
},
showSubLabel: {
type: Boolean,
default: true,
},
value: {
type: [String, Array],
default: undefined,
},
allowClear: {
type: Boolean,
default: false,
},
});
const { multiple, type, disabled, float } = toRefs(props);
const emits = defineEmits(['update:value', 'change', 'select']);
const activeKeys = ref<Array<string | number>>([]);
const itemOptions = computed(() => props.options);
const isAllowClear = computed(() => {
return props.allowClear !== false;
});
const getOptions = (keys: Array<string | number>): CardOption[] => {
return itemOptions.value.filter((item) => {
return keys.includes(item.value);
});
};
const handleSelect = (key: string | number, item: CardOption) => {
if (disabled.value || item.disabled) return;
let cloneActiveKeys = new Set(activeKeys.value);
const isActive = cloneActiveKeys.has(key);
// allowClearfalsereturn
if (isActive && !multiple.value && isAllowClear.value === false) return;
if (isActive) {
//
cloneActiveKeys.delete(key);
} else {
//
multiple.value
? cloneActiveKeys.add(key)
: (cloneActiveKeys = new Set([key]));
}
activeKeys.value = [...cloneActiveKeys.keys()];
const options = multiple.value ? getOptions(activeKeys.value) : item;
const values = props.multiple ? activeKeys.value : activeKeys.value[0]
emits('update:value', values);
emits('change', values, options);
emits('select', values, key, !isActive)
};
watch(
() => props.value,
() => {
activeKeys.value = Array.isArray(props.value)
? props.value
: [props.value];
},
{ immediate: true },
);
</script>
<style lang="less" scoped>
@card-border: #e6e6e6;
.j-card-panel {
.j-card-item {
border: 1px solid @card-border;
border-radius: 4px;
cursor: pointer;
color: @black;
display: flex;
width: 100%;
gap: 12px;
.j-card-title-warp {
flex: 1 1 auto;
max-width: 100%;
.title {
word-break: keep-all;
width: 100%;
}
}
&.vertical {
flex-direction: column-reverse;
padding: 22px 4px;
align-items: center;
.j-card-image {
margin-bottom: 4px;
}
}
&.horizontal {
padding: 20px;
}
.sub-title {
color: rgba(0, 0, 0, 0.24);
}
&.right {
flex-direction: row-reverse;
}
}
&.no-column {
display: flex;
flex-wrap: wrap;
gap: 16px;
.j-card-item {
min-width: 36px;
width: unset;
&.vertical {
padding: 14px 16px;
}
}
}
.active {
border: 1px solid var(--ant-primary-color) !important;
}
.disabled {
cursor: not-allowed;
opacity: 0.75;
}
}
</style>

View File

@ -0,0 +1,72 @@
<template>
<div class="radio-button" :style="styles">
<div v-for="item in options" @click="onClick(item)" class="radio-button-item" :class="{'active': myValue === item.value }">
{{ item.label }}
</div>
</div>
</template>
<script setup>
defineOptions({
name: 'RadioButton',
})
const props = defineProps({
value: {
type: [String, Number],
default: undefined
},
options: {
type: Array,
default: () => []
},
columns: {
type: Number,
default: 3
}
})
const emit = defineEmits(['update:value'])
const myValue = ref(props.value)
const styles = computed(() => {
return {
'grid-template-columns': `repeat(${props.columns}, 1fr)`
}
})
const onClick = (record) => {
if (myValue.value !== record.value) {
myValue.value = record.value
emit('update:value', record.value)
emit('select', record.value)
}
}
watch(() => props.value, () => {
myValue.value = props.value
})
</script>
<style scoped lang="less">
.radio-button {
display: grid;
gap: 16px;
.radio-button-item {
padding: 6px 12px;
text-align: center;
height: 100%;
border-radius: 2px;
background-color: #f5f5f5;
cursor: pointer;
&.active {
color: #fff;
background-color: @primary-color;
}
}
}
</style>

View File

@ -0,0 +1,11 @@
import CardSelect from './CardSelect.vue';
import type { App } from 'vue';
CardSelect.name = 'JCardSelect';
CardSelect.install = function (app: App) {
app.component('JCardSelect', CardSelect);
return app;
};
export default CardSelect;

View File

@ -0,0 +1,163 @@
<template>
<div :class="['j-check-button', props.class]" :style="styles">
<div
v-for="item in _options"
:key="item.value"
:class="{
'j-check-button-item': true,
'selected': myValue.includes(item.value),
'disabled': item.disabled
}"
@click="
() => {
selected(item.value, item.disabled);
}
"
>
{{ item.label }}
</div>
</div>
</template>
<script setup lang="ts">
import { computed, CSSProperties, PropType, ref, watch } from 'vue';
import { isArray } from 'lodash-es';
import { Form } from 'ant-design-vue'
defineOptions({
name: 'CheckButton'
})
const props = defineProps({
value: {
type: [String, Array],
default: undefined,
},
options: {
type: Array,
default: () => [],
},
multiple: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
class: {
type: String,
default: undefined,
},
style: {
type: Object as PropType<CSSProperties>,
default: () => ({}),
},
columns: {
type: Number,
default: 3
}
});
const emit = defineEmits(['update:value', 'change', 'select']);
const formItemContext = Form.useInjectFormItemContext();
const myValue = ref();
const optionsMap = ref(new Map());
const styles = computed(() => {
return {
'grid-template-columns': `repeat(${props.columns}, 1fr)`,
...props.style
}
})
const _options = computed(() => {
props.options.forEach((item: any) => {
if (props.disabled) {
item.disabled = props.disabled
}
optionsMap.value.set(item.value, item);
});
return props.options;
});
const selected = (key: string | number, disabeld: boolean) => {
if (disabeld || props.disabled) return;
const values = new Set(myValue.value);
if (values.has(key)) {
values.delete(key);
} else {
if (!props.multiple) {
values.clear();
}
values.add(key);
}
myValue.value = [...values.values()];
const optionsItems = myValue.value.map((_key) => {
return optionsMap.value.get(_key);
});
const _value = props.multiple ? myValue.value : myValue.value[0];
emit('update:value', _value);
emit('change', _value, props.multiple ? optionsItems : optionsItems[0]);
emit('select', _value, props.multiple ? optionsItems : optionsItems[0]);
formItemContext.onFieldChange()
};
watch(
() => props.value,
() => {
if (props.value) {
myValue.value = isArray(props.value) ? props.value : [props.value];
} else {
myValue.value = [];
}
},
{ immediate: true, deep: true },
);
</script>
<style scoped lang="less">
.j-check-button {
display: grid;
gap: 16px;
width: 100%;
.j-check-button-item {
flex: 1;
min-width: 0;
padding: 8px;
border-radius: @border-radius-base;
background-color: #f2f3f5;
transition: all 0.3s;
color: #333;
text-align: center;
cursor: pointer;
&:hover {
background-color: @primary-color;
opacity: 0.85;
color: #fff;
}
&.selected {
background-color: @primary-color;
color: #fff;
}
&.disabled {
cursor: not-allowed;
color: #00000040;
background-color: #e6e6e6;
opacity: 1
}
}
}
</style>

View File

@ -0,0 +1,3 @@
import CheckButton from "./CheckButton.vue";
export default CheckButton

View File

@ -0,0 +1,76 @@
<template>
<!-- <a-tooltip v-if="toolTip" v-bind="toolTip">
<span @click="showConfirm" :class="props.className" v-show="show">
{{ props.class }}
<slot></slot>
</span>
</a-tooltip> -->
<span @click="showConfirm" :class="props.className" v-show="show">
{{ props.class }}
<slot></slot>
</span>
</template>
<script setup>
import { Modal } from 'ant-design-vue';
const props = defineProps({
title: {
type: String,
default: '',
},
onConfirm: {
type: Object,
default: {},
},
className: {
type: String,
},
show: {
type: Boolean,
default: true,
},
disabled: {
type: Boolean,
default: false,
},
toolTip: {
type: Object
},
});
// const confirmLoading = ref(false);
// const modalVisible = ref(false);
// const modalConfirm = async() => {
// if (typeof props.onConfirm === 'function') {
// confirmLoading.value = true;
// const res = await props.onConfirm()?.finally(()=>{
// confirmLoading.value = false;
// modalVisible.value = false;
// return
// });
// if(!res?.finally){
// confirmLoading.value = false;
// modalVisible.value = false;
// }
// } else {
// modalVisible.value = false;
// }
// };
const showConfirm = () => {
if (props.disabled) {
return;
}
Modal.confirm({
title: props.title,
content: props?.content,
onOk() {
return props?.onConfirm();
},
onCancel() {},
});
};
</script>
<style lang="less" scoped>
.modalContent {
text-align: center;
}
</style>

View File

@ -1,163 +0,0 @@
<template>
<div class="operator-box">
<j-input-search @search="search" allow-clear placeholder="搜索关键字" />
<div class="tree">
<j-tree @select="selectTree" :field-names="{ title: 'name', key: 'id', }" auto-expand-parent
:tree-data="data"
:showLine="{ showLeafIcon: false }"
:show-icon="true">
<template #title="node">
<div class="node">
<div style="max-width: 180px"><Ellipsis>{{ node.name }}</Ellipsis></div>
<div :class="node.children?.length > 0 ? 'parent' : 'add'">
<j-popover v-if="node.type === 'property'" placement="right" title="请选择使用值">
<template #content>
<j-space direction="vertical">
<j-tooltip placement="right" title="实时值为空时获取上一有效值补齐,实时值不为空则使用实时值">
<j-button type="text" @click="recentClick(node)">
$recent实时值
</j-button>
</j-tooltip>
<j-tooltip placement="right" title="实时值的上一有效值">
<j-button @click="lastClick(node)" type="text">
上一值
</j-button>
</j-tooltip>
</j-space>
</template>
<a>添加</a>
</j-popover>
<a v-else @click="addClick(node)">
添加
</a>
</div>
</div>
</template>
</j-tree>
</div>
<div class="explain">
<Markdown :source="item?.description || ''"></Markdown>
</div>
</div>
</template>
<script setup lang="ts" name="Operator">
import { useProductStore } from '@/store/product';
import type { OperatorItem } from './typings';
import { treeFilter } from '@/utils/tree'
import { PropertyMetadata } from '@/views/device/Product/typings';
import { getOperator } from '@/api/device/product'
import Markdown from '@/components/Markdown'
const props = defineProps({
id: String
})
interface Emits {
(e: 'addOperatorValue', data: string): void;
}
const emit = defineEmits<Emits>();
const item = ref<Partial<OperatorItem>>()
const data = ref<OperatorItem[]>([])
const dataRef = ref<OperatorItem[]>([])
const search = (value: string) => {
if (value) {
const nodes = treeFilter(dataRef.value, value, 'name') as OperatorItem[];
data.value = nodes;
} else {
data.value = dataRef.value;
}
};
const selectTree = (k: any, info: any) => {
item.value = info.node as unknown as OperatorItem;
}
const recentClick = (node: OperatorItem) => {
emit('addOperatorValue', `$recent("${node.id}")`)
}
const lastClick = (node: OperatorItem) => {
emit('addOperatorValue', `$lastState("${node.id}")`)
}
const addClick = (node: OperatorItem) => {
emit('addOperatorValue', node.code)
}
const productStore = useProductStore()
const getData = async (id?: string) => {
const metadata = productStore.current.metadata || '{}';
const _properties = JSON.parse(metadata).properties || [] as PropertyMetadata[]
const properties = {
id: 'property',
name: '属性',
description: '',
code: '',
children: _properties
.filter((p: PropertyMetadata) => p.id !== id)
.map((p: PropertyMetadata) => ({
id: p.id,
name: p.name,
description: `### ${p.name}
\n 数据类型: ${p.valueType?.type}
\n 是否只读: ${p.expands?.readOnly || 'false'}
\n 可写数值范围: `,
type: 'property',
})),
};
const response = await getOperator();
if (response.status === 200) {
data.value = [properties as OperatorItem, ...response.result];
dataRef.value = [properties as OperatorItem, ...response.result];
}
};
watch(() => props.id,
(val) => {
getData(val)
},
{ immediate: true }
)
</script>
<style lang="less" scoped>
.border {
margin-top: 10px;
padding: 10px;
border-top: 1px solid lightgray;
}
.operator-box {
width: 100%;
.explain {
.border;
}
.tree {
.border;
height: 350px;
overflow-y: auto;
.node {
display: flex;
justify-content: space-between;
width: 220px;
//.add {
// display: none;
//}
//
//&:hover .add {
// display: block;
//}
.parent {
display: none;
}
}
}
}
</style>

View File

@ -3,6 +3,11 @@
<div class="top">
<div class="header">
<j-tabs v-model:activeKey="headerType">
<template #rightExtra>
<a v-if="virtualRule?.script && isBeginning" @click="beginAction">
开始运行
</a>
</template>
<j-tab-pane key="property">
<template #tab>
<span class="title">
@ -18,44 +23,6 @@
</template>
</j-tab-pane>
</j-tabs>
<!-- <div>
<j-dropdown>
<div class="title" @click.prevent>
{{
headerType === 'property'
? '属性赋值'
: '标签赋值'
}}
<div class="description">
{{
`请对上方规则使用的${
headerType === 'property'
? '属性'
: '标签'
}进行赋值`
}}
</div>
</div>
<template #overlay>
<j-menu>
<j-menu-item>
<a
href="javascript:;"
@click="headerType = 'property'"
>属性赋值</a
>
</j-menu-item>
<j-menu-item>
<a
href="javascript:;"
@click="headerType = 'tag'"
>标签赋值</a
>
</j-menu-item>
</j-menu>
</template>
</j-dropdown>
</div> -->
</div>
<div class="description">
{{
@ -71,7 +38,7 @@
:pagination="false"
bordered
size="small"
:scroll="{ y: 200 }"
:scroll="{ y: 180 }"
>
<template #bodyCell="{ column, record, index }">
<template v-if="column.key === 'id'">
@ -79,8 +46,13 @@
showSearch
:options="options"
v-model:value="record.id"
:getPopupContainer="(node) => tableWrapperRef || node"
size="small"
style="width: 100%; z-index: 1400 !important"
style="width: 100%;"
:virtual="true"
:dropdownStyle="{
zIndex: 1072
}"
/>
</template>
<template v-if="column.key === 'current'">
@ -131,7 +103,12 @@
:options="tagOptions"
v-model:value="record.id"
size="small"
style="width: 100%; z-index: 1400 !important"
style="width: 100%;"
:virtual="true"
:getPopupContainer="(node) => tableWrapperRef || node"
:dropdownStyle="{
zIndex: 1072
}"
/>
</template>
<template v-if="column.key === 'current'">
@ -175,13 +152,13 @@
class="action"
@click="runScriptAgain"
>
<a style="margin-left: 75px">发送数据</a>
<a>发送数据</a>
</div>
<div v-if="virtualRule?.script">
<a v-if="isBeginning" @click="beginAction">
开始运行
</a>
<a v-else @click="stopAction"> 停止运行 </a>
<div v-if="virtualRule?.script && !isBeginning">
<!-- <a v-if="isBeginning" @click="beginAction">-->
<!-- 开始运行-->
<!-- </a>-->
<a v-if="!isBeginning" @click="stopAction"> 停止运行 </a>
</div>
<div>
<a @click="clearAction"> 清空 </a>
@ -219,16 +196,17 @@
</template>
<script setup lang="ts" name="Debug">
import { PropType, Ref } from 'vue';
import { useProductStore } from '@/store/product';
import { useRuleEditorStore } from '@/store/ruleEditor';
import moment from 'moment';
import { getWebSocket } from '@/utils/websocket';
import { PropertyMetadata } from '@/views/device/Product/typings';
import {useTableWrapper} from "@/components/Metadata/Table/context";
import { onlyMessage } from '@/utils/comm';
import {message} from "ant-design-vue";
const props = defineProps({
virtualRule: Object as PropType<Record<any, any>>,
id: String,
propertiesOptions: Array,
});
const emits = defineEmits(['success']);
@ -238,21 +216,13 @@ type propertyType = {
id?: string;
current?: string;
last?: string;
type?: string
};
const property = ref<propertyType[]>([]);
const tag = ref<Array<any>>([]);
const headerOptions = [
{
key: 'property',
label: '属性赋值',
title: '属性赋值',
},
{
key: 'tag',
label: '标签赋值',
title: '标签赋值',
},
];
const tableWrapperRef = useTableWrapper()
const columns = [
{
title: '属性名称',
@ -310,21 +280,20 @@ const deleteTagItem = (index: number) => {
const ws = ref();
const virtualIdRef = ref(new Date().getTime());
const medataSource = inject<Ref<any[]>>('_dataSource');
const medataSource = inject<Ref<any[]>>('metadataSource');
const tagsSource = inject<Ref<any[]>>('_tagsDataSource');
const productStore = useProductStore();
const ruleEditorStore = useRuleEditorStore();
const time = ref<number>(0);
const timer = ref<any>(null);
const runScript = () => {
const metadata = productStore.current.metadata || '{}';
const propertiesList = JSON.parse(metadata).properties || [];
const propertiesList = medataSource?.value || []
const _properties = property.value.map((item: any) => {
const _item = propertiesList.find((i: any) => i.id === item.id);
return { ...item, type: _item?.valueType?.type };
});
console.log('runScript', _properties, propertiesList)
let _tags = {};
tag.value.forEach((item) => {
_tags[item.id] = item.current;
@ -334,6 +303,11 @@ const runScript = () => {
}
if (!props.virtualRule?.script) {
isBeginning.value = true;
message.config({
getContainer() {
return tableWrapperRef.value || document.body
}
})
onlyMessage('请编辑规则', 'warning');
return;
}
@ -385,12 +359,12 @@ const runScriptAgain = async () => {
if (wsAgain.value) {
wsAgain.value.unsubscribe?.();
}
const metadata = productStore.current.metadata || '{}';
const propertiesList = JSON.parse(metadata).properties || [];
const propertiesList = medataSource?.value || []
const _properties = property.value.map((item: any) => {
const _item = propertiesList.find((i: any) => i.id === item.id);
return { ...item, type: _item?.valueType?.type };
});
console.log('runScriptAgain', _properties, propertiesList)
wsAgain.value = getWebSocket(
`virtual-property-debug-${props.id}-${new Date().getTime()}`,
@ -414,6 +388,14 @@ const getTime = () => {
};
const beginAction = () => {
if (!property.value.length || property.value.some(item => !item.id || !(item.current || item.last) )) {
message.config({
getContainer() {
return tableWrapperRef.value || document.body
},
})
return onlyMessage('请填写属性值', 'warning')
}
isBeginning.value = false;
runScript();
getTime();
@ -435,13 +417,18 @@ onUnmounted(() => {
ws.value.unsubscribe?.();
}
clearAction();
message.config({
getContainer() {
return document.body
},
})
window.clearInterval(timer.value);
timer.value = null;
});
const options = computed(() => {
return (medataSource.value || [])
.filter((p) => p.id !== props.id)
return (props.propertiesOptions || [])
.map((item) => ({
label: item.name,
value: item.id,
@ -455,6 +442,9 @@ const tagOptions = computed(() => {
}));
});
defineExpose({
beginAction
})
// const getProperty = () => {
// // const metadata = productStore.current.metadata || '{}';
// // const _p: PropertyMetadata[] = JSON.parse(metadata).properties || [];
@ -465,10 +455,11 @@ const tagOptions = computed(() => {
</script>
<style lang="less" scoped>
.debug-container {
// display: flex;
display: flex;
// width: 100%;
// height: 340px;
// margin-top: 20px;
gap: 12px;
.top {
// min-width: 0;
@ -476,35 +467,36 @@ const tagOptions = computed(() => {
// overflow-y: auto;
height: 350px;
border: 1px solid lightgray;
margin-bottom: 10px;
//margin-bottom: 10px;
.header {
display: flex;
align-items: center;
//display: flex;
//align-items: center;
width: 100%;
height: 40px;
height: 46px;
border-bottom: 1px solid lightgray;
padding: 0 12px;
//justify-content: space-around;
div {
display: flex;
//width: 100%;
align-items: center;
justify-content: flex-start;
height: 100%;
.title {
margin: 0 10px;
font-weight: 600;
font-size: 16px;
}
.description {
margin-left: 10px;
color: lightgray;
font-size: 12px;
}
}
//div {
// display: flex;
// //width: 100%;
// align-items: center;
// justify-content: flex-start;
// height: 100%;
//
// .title {
// margin: 0 10px;
// font-weight: 600;
// font-size: 16px;
// }
//
// .description {
// margin-left: 10px;
// color: lightgray;
// font-size: 12px;
// }
//}
.action {
width: 150px;
@ -512,6 +504,11 @@ const tagOptions = computed(() => {
}
}
.description {
margin-left: 10px;
font-size: 12px;
}
.top-bottom {
padding: 10px;
}

View File

@ -23,7 +23,7 @@
</div>
<div
:class="
node.children?.length > 0
!node.isLeaf
? 'parent'
: 'add'
"
@ -35,6 +35,7 @@
}"
placement="right"
title="请选择使用值"
:getPopupContainer="getPopupContainer"
>
<template #content>
<j-space direction="vertical">
@ -73,6 +74,7 @@
}"
placement="right"
title="请选择使用值"
:getPopupContainer="getPopupContainer"
>
<template #content>
<j-space direction="vertical">
@ -119,7 +121,7 @@ import { treeFilter } from '@/utils/tree';
import { PropertyMetadata } from '@/views/device/Product/typings';
import { getOperator } from '@/api/device/product';
import { inject } from 'vue';
import { Descriptions } from 'ant-design-vue';
import {useTableWrapper, useTableFullScreen} from "@/components/Metadata/Table/context";
import Markdown from '@/components/Markdown'
const props = defineProps({
@ -136,6 +138,8 @@ const item = ref<Partial<OperatorItem>>();
const data = ref<OperatorItem[]>([]);
const dataRef = ref<OperatorItem[]>([]);
const tagsMetadata: any = inject('_tagsDataSource');
const tableWrapperRef = useTableWrapper()
const fullScreen = useTableFullScreen()
const search = (value: string) => {
if (value) {
const nodes = treeFilter(
@ -176,30 +180,39 @@ const getData = async (id?: string) => {
name: '属性',
description: '',
code: '',
isLeaf: false,
children: _properties
.filter((p: PropertyMetadata) => p.id !== id)
.map((p: PropertyMetadata) => ({
.map((p: PropertyMetadata) => {
const readOnly = p.expands.type.length === 1 && p.expands.type[0] === 'read' ? '是' : '否'
return {
id: p.id,
name: p.name,
isLeaf: true,
description: `### ${p.name}
\n 数据类型: ${p.valueType?.type}
\n 是否只读: ${p.expands?.readOnly || 'false'}
\n 可写数值范围: `,
\n 标识: ${p.id}
\n 数据类型: ${p.valueType?.type}
\n 是否只读: ${readOnly}
\n 可写数值范围: `,
type: 'property',
})),
}
}),
};
const tags = {
id: 'tags',
name: '标签',
Description: '',
code: '',
isLeaf: false,
children: tagsMetadata.value.map((i: any) => ({
id: i.id,
name: i.name,
isLeaf: true,
description: `### ${i.name}
\n 数据类型: ${i.valueType?.type}
\n 是否只读: ${i.expands?.readOnly || 'false'}
\n 可写数值范围: `,
\n 标识: ${i.id}
\n 数据类型: ${i.valueType?.type}
\n 可写数值范围: `,
type: 'tags',
})),
};
@ -218,6 +231,14 @@ const getData = async (id?: string) => {
}
};
const getPopupContainer = (node: any) => {
if (fullScreen.value) {
return tableWrapperRef.value || node
}
return document.body
}
watch(
() => props.id,
(val) => {
@ -236,17 +257,21 @@ watch(
.operator-box {
width: 100%;
display: flex;
flex-direction: column;
gap: 12px;
height: 100%;
.left,
.right {
width: 50%;
height: 350px;
//width: 50%;
//height: 350px;
height: calc(50% - 7px);
border: 1px solid lightgray;
}
.left {
padding: 10px;
margin-right: 10px;
//margin-right: 10px;
.tree {
height: 300px;
//overflow-y: auto;

View File

@ -1,13 +1,16 @@
<template>
<j-modal
:zIndex="1030"
:zIndex="1072"
:mask-closable="false"
visible
width="70vw"
width="1300px"
title="编辑规则"
:getContainer="(node) => fullRef || node"
@cancel="handleCancel"
:destroyOnClose="true"
:dialogStyle="{
zIndex: 1072
}"
:getContainer="(node) => tableWrapperRef || node"
@cancel="handleCancel"
>
<div class="header" v-if="virtualRule?.windowType && virtualRule?.windowType !== 'undefined'">
<div class="header-item">
@ -24,27 +27,27 @@
<div class="box">
<div class="left">
<div>
<Operator :id="id" :propertiesOptions="propertiesOptions" @add-operator-value="addOperatorValue" />
<Editor
ref="editor"
mode="advance"
key="advance"
v-model:value="_value"
:tips="tips"
/>
</div>
<div style="margin-top: 10px;">
<Editor
ref="editor"
mode="advance"
key="advance"
v-model:value="_value"
:tips="tips"
/>
</div>
</div>
<div class="right">
<Debug
<Debug
:virtualRule="{
...virtualRule,
script: _value,
}"
:propertiesOptions="propertiesOptions"
:id="id"
@success="onSuccess"
/>
/>
</div>
</div>
<div class="right">
<Operator :id="id" :propertiesOptions="propertiesOptions" @add-operator-value="addOperatorValue" />
</div>
</div>
<template #footer>
@ -59,9 +62,8 @@
import Editor from './Editor/index.vue';
import Debug from './Debug/index.vue';
import Operator from './Operator/index.vue';
import { FULL_CODE } from 'jetlinks-ui-components/es/DataTable'
import { cloneDeep } from 'lodash-es';
import { PropertyMetadata } from '@/views/device/Product/typings';
import {useTableWrapper} from "@/components/Metadata/Table/context";
interface Emits {
(e: 'save', data: string | undefined): void;
(e: 'close'): void;
@ -77,8 +79,7 @@ const props = defineProps({
});
const _value = ref<string | undefined>(props.value);
const _disabled = ref<boolean>(true);
const fullRef = inject(FULL_CODE);
const tableWrapperRef = useTableWrapper()
const tips = ref<any[]>([])
const handleCancel = () => {
emit('close');
@ -110,7 +111,7 @@ const getAllCrud = () => {
console.log(item)
const config = item
tips.value.push({
label: `${config.name}$recent实时值`,
label: `${config.name}$recent实时值`,
insertText:`$recent ("${config.id}")`,
kind: 18,
})
@ -151,14 +152,14 @@ getAllCrud()
width: 100%;
.left {
width: 60%;
width: 75%;
}
.right {
width: 40%;
flex: 1 1 0;
margin-left: 10px;
padding-left: 10px;
border-left: 1px solid lightgray;
}
}
</style>
</style>

View File

@ -3,7 +3,7 @@
<div class="page-container">
<j-input allowClear v-model:value="inputPoint">
<template #addonAfter>
<environment-outlined @click="modalVis = true" />
<environment-outlined @click="showMap" />
</template>
</j-input>
<j-modal
@ -101,6 +101,11 @@ const clickMap = (e: any) => {
position.value = [e.lnglat.lng, e.lnglat.lat];
};
const showMap = () => {
mapPoint.value = props.point
modalVis.value = true;
}
/**
* 选择搜索结果
* @param e
@ -110,6 +115,8 @@ const selectPoi = (e: any) => {
mapPoint.value = selectPoint.join(',');
map.setCenter(selectPoint);
};
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,468 @@
<template>
<div :class="{
'metadata-edit-table-wrapper': true,
'table-full-screen': isFullscreen
}" ref="tableWrapper">
<div class="metadata-edit-table-extra">
<slot name="extra" :isFullscreen="isFullscreen" :fullScreenToggle="toggle"/>
</div>
<div class="metadata-edit-table">
<div class="metadata-edit-table-header" style="height: 50px" :style="{paddingRight: scrollWidth + 'px'}">
<Header
:columns="myColumns"
:style="{width: tableStyle.width}"
/>
</div>
<div class="metadata-edit-table-body" :style="{width: tableStyle.width, height: `${height}px`}">
<Body
ref="tableBody"
:dataSource="bodyDataSource"
:columns="myColumns"
:cellHeight="cellHeight"
:height="height"
:disableMenu="disableMenu"
:rowKey="rowKey"
:groupKey="groupActive.value"
:openGroup="openGroup"
:rowSelection="rowSelection"
@scrollDown="onScrollDown"
>
<template v-for="(_, name) in slots" #[name]="slotData">
<slot :name="name" v-bind="slotData || {}"/>
</template>
</Body>
<slot name="bodyExtra"></slot>
</div>
<Group
v-if="dataSource.length && openGroup"
v-model:activeKey="groupActive.value"
:options="groupOptions"
@add="addGroup"
@delete="groupDelete"
@edit="groupEdit"
@change="updateGroupActive"
/>
</div>
</div>
</template>
<script setup name="MetadataBaseTable">
import {
FULL_SCREEN,
RIGHT_MENU,
TABLE_DATA_SOURCE,
TABLE_ERROR,
TABLE_GROUP_ACTIVE,
TABLE_GROUP_ERROR,
TABLE_GROUP_OPTIONS,
TABLE_OPEN_GROUP,
TABLE_TOOL,
TABLE_WRAPPER
} from './consts'
import {handleColumnsWidth} from './utils'
import {useGroup, useResizeObserver, useValidate} from './hooks'
import {tableProps} from 'ant-design-vue/lib/table'
import {useFormContext} from './context'
import Header from './header.vue'
import Body from './body.vue'
import {useFullscreen} from '@vueuse/core';
import {provide, useAttrs, useSlots} from 'vue'
import Group from './group.vue'
import {bodyProps} from "./props";
import {findIndex, get, sortBy} from 'lodash-es'
const emit = defineEmits(['scrollDown', 'rightMenuClick', 'editChange', 'groupDelete', 'groupEdit'])
const props = defineProps({
...tableProps(),
...bodyProps(),
serial: {
type: [Object, Boolean],
default: () => ({
width: 66,
title: '序号'
})
},
validateRowKey: {
type: Boolean,
default: false
},
})
const slots = useSlots()
const attrs = useAttrs()
const myColumns = ref([])
const tableWrapper = ref()
const tableBody = ref()
const tableStyle = reactive({
width: '100%',
height: props.height
})
const fields = {}
const defaultGroupId = 'group_1'
const fieldsErrMap = ref({})
const fieldsGroupError = ref({})
const sortData = reactive({
key: undefined,
order: undefined,
orderKeys: [],
dataIndex: undefined
})
const {
groupActive,
groupOptions,
addGroup,
removeGroup,
updateGroupActive,
updateGroupOptions
} = useGroup(props.openGroup)
const _dataSource = computed(() => {
const _options = new Map()
const sortDataSource = sortData.key ?
sortBy(props.dataSource, (val) => {
if (!val.id) return 99999999
const index = findIndex(sortData.orderKeys, val2 => get(val, sortData.key) === val2)
return sortData.order === 'desc' ? index : ~index + 1
}) : props.dataSource
sortDataSource.forEach((item, index) => {
item.__dataIndex = index
if (props.openGroup) {
const _groupId = item.expands?.groupId
if (!_groupId) {
item.expands.groupId = groupActive.value || defaultGroupId
item.expands.groupName = groupActive.label || '分组_1'
}
const _optionsItem = _options.get(item.expands.groupId)
if (!_optionsItem) {
_options.set(item.expands.groupId, {
value: item.expands?.groupId,
label: item.expands?.groupName,
effective: item.id ? 1 : 0, //
len: 1 //
})
} else {
if (item.id) {
_optionsItem.effective += 1
}
_optionsItem.len += 1
_options.set(item.expands.groupId, _optionsItem)
}
item.__serial = _optionsItem?.len || 1
} else {
item.__serial = index + 1
}
})
if (props.openGroup) {
updateGroupOptions([..._options.values()])
}
return sortDataSource
})
const bodyDataSource = computed(() => {
if (props.openGroup) {
return _dataSource.value.filter(item => {
return item.expands.groupId === groupActive.value
})
}
return _dataSource.value
})
useResizeObserver(tableWrapper, onResize)
const {isFullscreen, toggle} = useFullscreen(tableWrapper);
const {rules, validateItem, validate, errorMap} = useValidate(
_dataSource,
props.columns,
props.rowKey,
{
onError: (err) => {
fieldsErrMap.value = {}
fieldsGroupError.value = {}
const errMap = {}
// err
err.forEach((item, errIndex) => {
item.forEach((e, eIndex) => {
const field = findField(e.__dataIndex, e.field)
const _eventKey = field ? field.eventKey : `${e.__dataIndex}-${e.field}`
if (field) {
field.showErrorTip(e.message)
}
errMap[_eventKey] = e.message
if (errIndex === 0 && eIndex === 0) {
if (props.openGroup) {
const expands = _dataSource.value[e.__dataIndex].expands
updateGroupActive(expands.groupId, expands.groupName)
}
setTimeout(() => {
tableBody.value.scrollTo(e.__serial - 1)
}, 10)
}
})
})
fieldsErrMap.value = errMap
},
onSuccess: () => {
fieldsErrMap.value = {}
},
onEdit: () => {
emit('editChange', true)
},
validateRowKey: props.validateRowKey
}
)
provide(TABLE_WRAPPER, tableWrapper)
provide(FULL_SCREEN, isFullscreen)
provide(RIGHT_MENU, {click: rightMenu, getPopupContainer: () => tableWrapper.value})
provide(TABLE_ERROR, fieldsErrMap)
provide(TABLE_GROUP_ERROR, fieldsGroupError)
provide(TABLE_DATA_SOURCE, _dataSource)
provide(TABLE_OPEN_GROUP, props.openGroup)
provide(TABLE_TOOL, {
scrollTo: (record) => {
if (props.openGroup) {
const expands = record.expands
updateGroupActive(expands.groupId, expands.groupName)
}
setTimeout(() => {
tableBody.value.scrollTo(record.__serial)
}, 10)
},
selected: (keys) => {
tableBody.value.updateSelectedKeys(keys)
},
order: (type, key, orderKeys, dataIndex) => {
sortData.key = key
sortData.order = type
sortData.orderKeys = orderKeys
sortData.dataIndex = dataIndex
},
cleanOrder: () => {
sortData.key = undefined
sortData.order = undefined
sortData.orderKeys = []
sortData.dataIndex = undefined
},
sortData
})
provide(TABLE_GROUP_OPTIONS, groupOptions)
provide(TABLE_GROUP_ACTIVE, groupActive)
const addField = (key, field) => {
fields[key] = field
}
const removeField = (key) => {
delete fields[key]
}
function findField(index, name) {
const fieldId = Object.keys(fields).find(key => {
const {names} = fields[key]
return names[0] === index && names[1] === name
})
return fields[fieldId]
}
function removeFieldError(key) {
delete fieldsErrMap.value[key]
}
function addFieldError(key, message) {
fieldsErrMap.value[key] = message
}
const scrollWidth = computed(() => {
return (props.dataSource.length * props.cellHeight) > props.height ? 17 : 0
})
function onResize({width = 0, height}) {
const _width = width - scrollWidth.value
tableStyle.width = width || '100%'
// tableStyle.height = height - 146
let newColumns = [...props.columns]
if (props.serial) {
const serial = {
dataIndex: '__serial',
title: props.serial.title || '序号',
customRender: (customData) => {
if (props.serial?.customRender) {
return props.serial?.customRender(customData)
}
return customData.index + 1
},
width: props.serial?.width
}
newColumns = [serial, ...props.columns]
}
myColumns.value = handleColumnsWidth(newColumns, _width)
}
const onScrollDown = (len) => {
emit('scrollDown', len)
}
function rightMenu(menuType, record, copyValue) {
emit('rightMenuClick', menuType, record, copyValue)
}
const scrollToById = (key) => {
const _index = _dataSource.value.findIndex(item => item[props.rowKey] === key)
tableBody.value.scrollTo(_index)
}
const scrollToByIndex = (index) => {
tableBody.value.scrollTo(index)
}
const getTableWrapperRef = () => {
return tableWrapper.value
}
const groupDelete = (id, index) => {
removeGroup(index)
Object.keys(fieldsErrMap.value).forEach(errorKey => {
const [index] = errorKey.split('-')
const dataSourceItem = _dataSource.value[index]
const groupId = dataSourceItem.expands?.groupId
if (groupId === id) {
removeFieldError(errorKey)
removeField(errorKey)
}
})
emit('groupDelete', id)
}
const groupEdit = (record) => {
emit('groupEdit', record)
}
const getGroupActive = () => {
return groupActive.value
}
watch(() => fieldsErrMap.value, (errorMap) => {
fieldsGroupError.value = {}
if (props.openGroup) {
const _errorObj = errorMap
const groupErrorMap = {}
Object.keys(_errorObj).forEach(errorKey => {
const [index] = errorKey.split('-')
const dataSourceItem = _dataSource.value[index]
const groupId = dataSourceItem.expands?.groupId
const groupError = groupErrorMap[groupId]
const groupErrorItem = {
[errorKey]: {
message: _errorObj[errorKey],
index,
serial: dataSourceItem.__serial
}
}
if (groupError) {
groupError.push(groupErrorItem)
} else {
groupErrorMap[groupId] = [groupErrorItem]
}
})
console.log(groupErrorMap)
fieldsGroupError.value = groupErrorMap
}
}, {deep: true})
watch(() => scrollWidth.value, () => {
onResize({width: tableStyle.width})
})
useFormContext({
dataSource: computed(() => {
return props.dataSource
}),
errorMap,
rules,
addField,
removeField,
removeFieldError,
addFieldError,
validateItem
})
defineExpose({
validate,
tableWrapper,
scrollToById,
scrollToByIndex,
getTableWrapperRef,
getGroupActive
})
</script>
<style scoped lang="less">
.metadata-edit-table-wrapper {
background: #fff;
height: 100%;
position: relative;
&.table-full-screen {
padding: 24px;
}
.metadata-edit-table {
display: flex;
flex-direction: column;
flex-grow: 0;
flex-shrink: 0;
background: #fafafa;
transition: background-color .3s ease;
.metadata-edit-table-header {
overflow: hidden;
width: 100%;
}
.metadata-edit-table-body {
background-color: #fff;
overflow-y: hidden;
position: relative;
height: 100%;
width: 100%;
flex: 1 1 auto;
flex-direction: row;
}
}
}
</style>

View File

@ -0,0 +1,201 @@
<template>
<a-tooltip
color="#ffffff"
:get-popup-container="popContainer"
:arrowPointAtCenter="true"
>
<template #title>
<span style="color: #1d2129">{{errorMap.message}}</span>
</template>
<div v-if="errorMap.visible" class="table-form-error-target" ></div>
</a-tooltip>
<div :id="eventKey" style="position: relative" :class="{'edit-table-form-has-error': errorMap.message }">
<slot />
</div>
</template>
<script setup name="TableFormItem">
import {useInjectError, useInjectForm, useTableWrapper} from "./context";
import {get, isArray } from 'lodash-es'
import {onBeforeUnmount, computed} from "vue";
import { useProvideFormItemContext } from 'ant-design-vue/es/form/FormItemContext'
import {TABLE_FORM_ITEM_ERROR} from "@/components/Metadata/Table/consts";
const props = defineProps({
name: {
type: [String, Array],
default: undefined
},
required: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['change'])
const tableWrapperRef = useTableWrapper()
const context = useInjectForm()
const globalErrorMessage = useInjectError()
let hideTimer
const eventKey = computed(() => {
const names = isArray(props.name) ? props.name : [props.name]
return names.join('-')
})
const errorMap = reactive({
message: '',
visible: false
})
const filedId = computed(() => {
const names = isArray(props.name) ? props.name : [props.name]
return names.join('_')
})
const filedName = computed(() => {
const names = isArray(props.name) ? props.name : [props.name]
const _rules = context.rules.value
let tempKey = undefined
for (const key of names) {
if (key in _rules) {
tempKey = key
}
}
return tempKey
})
const filedValue = computed(() => {
return get(context.dataSource.value, props.name)
})
provide(TABLE_FORM_ITEM_ERROR, errorMap)
const popContainer = (e) => {
return e
}
const removeTimer = () => {
if (hideTimer) {
window.clearTimeout(hideTimer)
hideTimer = null
}
}
const showErrorTip = (msg) => {
removeTimer()
errorMap.message = msg
errorMap.visible = true
}
const hideErrorTip = () => {
errorMap.visible = false
removeTimer()
hideTimer = setTimeout(() => {
errorMap.message = ''
}, 300)
}
const validateRules = () => {
let index = 0
if (isArray(props.name)) {
index = props.name[0]
}
const promise = context.validateItem({ [filedName.value]: get(context.dataSource.value, props.name) }, index)
promise.catch(res => {
const error = res?.filter(item => item.field === filedName.value) || []
if (error.length === 0) {
hideErrorTip()
context.removeFieldError(eventKey.value)
} else {
removeTimer()
errorMap.message = error[0]?.message || errorMap.message
errorMap.visible = !!error.length
context.addFieldError(eventKey.value, errorMap.message)
}
return errorMap.message
})
return promise
}
const onFieldBlur = () => {
// validateRules()
}
const onFieldChange = () => {
validateRules()
emit('change')
}
watch(() => globalErrorMessage.value, (val) => {
if (val[eventKey.value]) {
showErrorTip(val[eventKey.value])
} else {
hideErrorTip()
}
}, { immediate: true, deep: true})
useProvideFormItemContext({
id: filedId,
onFieldChange,
onFieldBlur,
}, computed(() => get(context.dataSource.value, props.name)))
onBeforeUnmount(() => {
hideErrorTip()
// context.removeField(eventKey.value)
})
watch(() => [filedName.value, props.name], () => {
context.addField(eventKey.value, {
filedName: filedName.value,
eventKey: eventKey.value,
names: props.name,
validateRules,
showErrorTip
})
}, { immediate: true })
</script>
<style scoped lang="less">
.table-form-error-target {
position: absolute;
right: 2px;
top: -9px;
border: 16px solid transparent;
border-top-color: @error-color;
border-right-width: 0;
border-bottom-width: 0;
}
</style>
<style lang="less">
.edit-table-form-has-error {
.select-no-value {
.ant-select-selector {
border-color: @error-color !important;
&:focus {
box-shadow: 0 0 0 2px var(--ant-error-color-outline) !important;
}
}
}
> input {
border-color: @error-color !important;
&:focus {
box-shadow: 0 0 0 2px var(--ant-error-color-outline) !important;
}
}
.table-form-required-aicon {
color: @error-color;
}
}
</style>

View File

@ -0,0 +1,282 @@
<template>
<div
v-if="dataSource.length"
class="metadata-edit-table-body-viewport" :style="{ ...style, height: height + 'px'}" ref="viewScrollRef" @scroll="onScroll">
<div :style="{position: 'relative'}">
<div class="metadata-edit-scrollbar" :style="containerStyle"> </div>
<div class="metadata-edit-table-center" ref="tableCenterRef" >
<div
v-if="virtualData.length"
v-for="(item, index) in virtualData"
:class="{
'metadata-edit-table-row': true,
'metadata-edit-table-row-selected': selectedRowKeys?.includes(item[rowKey] || virtualRang.start + index + 1)
}"
:key="`record_${item.__key}`"
:style="{height: `${cellHeight}px`,}"
:data-row-key="item[rowKey] || virtualRang.start + index + 1"
@click.right.native="(e) => showContextMenu(e,item, virtualRang.start + index)"
@click.stop="() => rowClick(item)"
>
<div
v-for="column in columns"
class="metadata-edit-table-cell"
:style="{
width: `${column.width}px`,
left: `${column.left}px`,
}"
>
<div v-if="column.dataIndex === '__serial'" class="body-cell-box">
<slot name="serial" :record="item" :index="item.__dataIndex" :column="column" >
{{ virtualRang.start + index + 1 }}
</slot>
</div>
<div v-else class="body-cell-box">
<slot :name="column.dataIndex" :record="item" :index="item.__dataIndex" :column="column" >
{{ item[column.dataIndex] }}
</slot>
</div>
</div>
</div>
</div>
</div>
</div>
<template v-else>
<slot name="empty">
<div class="metadata-edit-table-body-empty">
<j-empty />
</div>
</slot>
</template>
</template>
<script setup name="MetadataBaseTableBody">
import ContextMenu from './components/ContextMenu'
import {useRightMenuContext} from "@/components/Metadata/Table/context";
import {randomString} from "@/utils/utils";
import {bodyProps} from "./props";
const props = defineProps({
...bodyProps(),
groupKey: {
type: [String, Number],
default: undefined
}
})
const emit = defineEmits(['update:dataSource', 'scrollDown'])
const viewScrollRef = ref()
const tableCenterRef = ref()
// const virtualData = ref([])
const virtualRang = reactive({
start: 0,
end: 15
})
const containerStyle = ref(0)
const context = useRightMenuContext()
let scrollLock = ref(false)
let menuInstance
const maxLen = computed(() => {
return Math.trunc(props.height / props.cellHeight)
})
const selectedRowKeys = ref([])
const virtualData = computed(()=> {
const array = props.dataSource.slice(virtualRang.start, virtualRang.end)
if (tableCenterRef.value) {
tableCenterRef.value.style.webkitTransform = `translate3d(0, ${virtualRang.start * props.cellHeight}px, 0)`
}
return array
})
// const updateVirtualData = (start, end) => {
// debugger
// virtualData.value = props.dataSource.slice(start, end)
// if (tableCenterRef.value) {
// tableCenterRef.value.style.webkitTransform = `translate3d(0, ${start * props.cellHeight}px, 0)`
// }
// }
const onScroll = () => {
const height = viewScrollRef.value.scrollTop
const clientHeight = viewScrollRef.value.clientHeight
const scrollHeight = viewScrollRef.value.scrollHeight
const _index = Math.round(height / props.cellHeight) - 1
const start = _index < 0 ? 0 : _index
const end = start + maxLen.value + 4
if (height + clientHeight >= props.dataSource.length * props.cellHeight && !scrollLock.value) { //
emit('scrollDown')
scrollLock.value = true
}
virtualRang.start = start
virtualRang.end = end
// updateVirtualData(start, end)
}
const scrollTo = (index) => {
if (viewScrollRef.value) {
let top = index * props.cellHeight
viewScrollRef.value.scrollTop = top
}
}
const showContextMenu = (e, record, _index) => {
e.preventDefault()
if (props.disableMenu) {
record = {
...record,
__index: _index
}
menuInstance = ContextMenu(e, record, context)
}
}
// const updateView = () => {
// updateVirtualData(virtualRang.start, virtualRang.start + maxLen.value)
// }
const rowClick = (record) => {
if (props.rowSelection?.selectedRowKeys) {
const rowSet = new Set(selectedRowKeys.value)
const key = record[props.rowKey]
const selected = !rowSet.has(key)
if (selected) {
rowSet.delete(key)
} else {
rowSet.add(key)
}
props.rowSelection.onSelect?.(record, selected )
selectedRowKeys.value = [...rowSet.values()]
}
}
const updateSelectedKeys = (keys) => {
selectedRowKeys.value = keys
}
onMounted(() => {
nextTick(() => {
onScroll()
})
})
onBeforeUnmount(() => {
menuInstance?.destroy()
menuInstance?.cleanCopy()
})
watch(() => JSON.stringify(props.rowSelection?.selectedRowKeys), (val) => {
selectedRowKeys.value = JSON.parse(val || '[]')
}, { immediate: true })
watch(() => props.dataSource, (val, oldVal) => {
props.dataSource.forEach((item, index) => {
if (!item.__key) {
item.__key = randomString()
}
})
// updateView()
}, {
immediate: true,
deep: true
})
watch(() => props.dataSource.length, () => {
scrollLock.value = false
containerStyle.value = {
height: props.dataSource.length * props.cellHeight + 'px'
}
if (props.dataSource.length <= maxLen.value || props.dataSource.length === 0) {
emit('scrollDown', maxLen.value - props.dataSource.length + 3)
}
}, { immediate: true})
// watch(() => props.height, () => {
// updateView()
// })
watch(() => props.groupKey, () => {
if (props.openGroup) {
scrollTo(0)
}
})
defineExpose({
scrollTo,
updateSelectedKeys
})
</script>
<style scoped lang="less">
.metadata-edit-table-body-viewport {
max-height: 100%;
width: 100%;
overflow: hidden auto;
position: relative;
.metadata-edit-scrollbar {
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
}
.metadata-edit-table-body-container {
overflow: hidden;
height: 100%;
}
.metadata-edit-table-center {
position: relative;
flex: 1 1 auto;
min-width: 0;
height: 100%;
.metadata-edit-table-row {
width: 100%;
display: flex;
align-items: center;
//position: absolute;
transition: top .2s, height .2s, background-color .1s;
border-bottom: 1px solid #ebebeb;
&:hover {
background-color: rgb(248, 248, 248);
}
&.metadata-edit-table-row-selected {
background-color: var(--ant-primary-1);
}
.body-cell-box {
padding: 0 12px;
position: relative;
}
}
}
}
.metadata-edit-table-body-empty {
display: flex;
width: 100%;
justify-content: center;
padding-top: 24px;
}
</style>

View File

@ -0,0 +1,216 @@
<template>
<PopoverModal
v-model:visible="visible"
:placement="placement"
@ok="onOk"
@cancel="onCancel"
>
<template #content>
<div style="width: 450px">
<a-form ref="formRef" layout="vertical" :model="formData">
<a-form-item label="元素类型" required name="type" :rules="rules" :validate-first="true">
<TypeSelect v-model:value="formData.type"/>
</a-form-item>
<ScaleItem v-if="showDouble" v-model:value="formData.scale" />
<StringItem v-else-if="showString" v-model:value="formData.expands.maxLength" />
<BooleanItem v-else-if="showBoolean" v-model:value="formData.boolean" name="boolean"/>
<DateItem v-else-if="showDate" v-model:value="formData.format"/>
<EnumItem ref="enumTableRef" v-else-if="showEnum" v-model:value="formData.enum.elements"/>
<a-form-item v-else-if="showArray" label="子元素类型" required :name="['elementType','type']" :rules="[{ required: true, message: '请选择子元素类型'}]">
<TypeSelect v-model:value="formData.elementType.type" :filter="['array', 'object']" />
</a-form-item>
</a-form>
</div>
</template>
<slot>
<a-button type="link" :disabled="disabled" style="padding: 0">
<template #icon>
<AIcon type="EditOutlined" :class="{'table-form-required-aicon': !value.type}"/>
</template>
</a-button>
</slot>
</PopoverModal>
</template>
<script setup name="MetadataArray">
import { PopoverModal, TypeSelect } from '../index'
import ScaleItem from '../Double/ScaleItem.vue'
import StringItem from '../String/Item.vue'
import BooleanItem from '../Boolean/Item.vue'
import DateItem from '../Date/Item.vue'
import EnumItem from '../Enum/Item.vue'
import {cloneDeep, pick} from 'lodash-es'
import {Form} from "ant-design-vue";
const emit = defineEmits(['update:value', 'cancel', 'confirm']);
const props = defineProps({
value: {
type: Object,
default: () => ({}),
},
unitOptions: {
type: [Array, Function],
default: () => []
},
placement: {
type: String,
default: 'top',
},
disabled: {
type: Boolean,
default:false
}
});
const formItemContext = Form.useInjectFormItemContext();
const formRef = ref()
const enumTableRef = ref()
const visible = ref(false)
const formData = reactive({
type: props.value?.type,
scale: props.value?.scale,
expands: {
maxLength: props.value?.maxLength || props.value?.expands?.maxLength,
},
boolean: {
trueText: props.value?.trueText || '是',
trueValue: props.value?.trueValue || 'true',
falseText: props.value?.falseText || '否',
falseValue: props.value?.falseValue || 'false',
},
format: props.value?.format,
enum: {
multiple: props.value?.multiple,
elements: cloneDeep(props.value?.elements) || [],
},
elementType: props.value?.type === 'array' ? props.value.elementType : {
type: undefined
}
});
const showDouble = computed(() => {
return ['float', 'double'].includes(formData.type)
})
const showString = computed(() => {
return ['string', 'password'].includes(formData.type)
})
const showBoolean = computed(() => {
return formData.type === 'boolean'
})
const showDate = computed(() => {
return formData.type === 'date'
})
const showEnum = computed(() => {
return formData.type === 'enum'
})
const showArray = computed(() => {
return formData.type === 'array'
})
const rules = [
{
validator(_, value) {
if (!value) {
return Promise.reject('请选择元素类型');
}
return Promise.resolve();
},
trigger: 'change',
},
];
const typeChange = (e) => {
if (['float', 'double'].includes(e)) {
formData.scale = 2;
}
};
const initValue = () => {
formData.type = props.value?.type;
formData.scale = props.value?.scale;
formData.expands.maxLength = props.value?.maxLength || props.value?.expands?.maxLength;
formData.boolean = {
trueText: props.value?.trueText || '是',
trueValue: props.value?.trueValue || 'true',
falseText: props.value?.falseText || '否',
falseValue: props.value?.falseValue || 'false',
};
formData.format = props.value?.format;
formData.enum = {
multiple: props.value?.multiple,
elements: cloneDeep(props.value?.elements || []),
};
formData.elementType = props.value?.type === 'array' ? props.value.elementType : {
type: undefined
}
};
const handleValue = (type, data) => {
let newObject = {};
switch (type) {
case 'float':
case 'double':
newObject = pick(data, 'scale');
break;
case 'boolean':
newObject = { ...data.boolean };
break;
case 'enum':
newObject.elements = data.enum.elements;
break;
case 'string':
case 'password':
newObject = pick(data, 'expands');
break;
case 'date':
newObject = pick(data, 'format');
break;
case 'array':
newObject = pick(data, 'elementType')
}
return {
type: type,
...newObject,
};
};
const onOk = async () => {
const data = await formRef.value.validate()
let enumTable = true
if (enumTableRef.value) {
enumTable = !!(await enumTableRef.value.validate())
}
if (data && enumTable) {
visible.value = false
const _value = handleValue(formData.type, formData)
emit('update:value', _value);
emit('confirm', _value);
formItemContext.onFieldChange()
}
}
const onCancel = () => {
formRef.value?.resetFields();
initValue();
emit('cancel');
}
watch(() => JSON.stringify(props.value), () => {
initValue()
})
</script>
<style scoped>
</style>

View File

@ -0,0 +1,67 @@
<template>
<a-form-item
label="布尔值"
required
:name="name"
:rules="rules"
:validate-first="true"
>
<Content
v-model:value="myValue"
@change="change"
/>
</a-form-item>
</template>
<script setup name="MetadataBooleanItem">
import Content from './ItemContext.vue'
const emits = defineEmits([
'update:value',
'change'
])
const props = defineProps({
value: {
type: Object,
default: () => ({}),
},
name: {
type: String,
default: 'value',
},
});
const myValue = ref(props.value)
const rules = [{
validator(_, v) {
const isMax = Object.values(v).some(
(item) => item.length > 64,
);
const isNull = Object.values(v).some((item) => !item);
if (isMax) {
return Promise.reject('最多可输入64个字符');
}
if (isNull) {
return Promise.reject('请输入布尔值');
}
return Promise.resolve();
}
}]
const change = () => {
emits(`update:value`, myValue.value)
emits(`change`, myValue.value)
}
watch(() => JSON.stringify(props.value), () => {
myValue.value = props.value
})
</script>
<style scoped>
</style>

View File

@ -0,0 +1,76 @@
<template>
<div class="boolean-items">
<div class="boolean-item boolean-true">
<div class="boolean-true-text">
<a-input v-model:value="formData.trueText" @change="valueChange"/>
</div>
<span>-</span>
<div class="boolean-true-value">
<a-input v-model:value="formData.trueValue" @change="valueChange"/>
</div>
</div>
<div class="boolean-item boolean-false">
<div class="boolean-false-text">
<a-input v-model:value="formData.falseText" @change="valueChange"/>
</div>
<span>-</span>
<div class="boolean-false-value">
<a-input v-model:value="formData.falseValue" @change="valueChange"/>
</div>
</div>
</div>
</template>
<script setup name="BooleanItemContext">
const emits = defineEmits([
'update:value',
'change'
])
const props = defineProps({
value: {
type: Object,
default: {}
}
})
const formData = reactive({
trueText: props.value?.trueText || '是',
trueValue: props.value?.trueValue || 'true',
falseText: props.value?.falseText || '否',
falseValue: props.value?.falseValue || 'false',
});
const valueChange = () => {
emits(`update:value`, formData)
emits(`change`, formData)
}
watch(() => JSON.stringify(props.value), () => {
formData.trueText = props.value?.trueText;
formData.trueValue = props.value?.trueValue;
formData.falseText = props.value?.falseText;
formData.falseValue = props.value?.falseValue;
})
</script>
<style scoped lang="less">
.boolean-items {
.boolean-item {
display: flex;
gap: 4px;
align-items: center;
> div {
flex: 1;
min-width: 0;
}
}
.boolean-true {
margin-bottom: 8px;
}
}
</style>

View File

@ -0,0 +1,117 @@
<template>
<PopoverModal
v-model:visible="visible"
:placement="placement"
@ok="onOk"
@cancel="onCancel"
>
<template #content>
<div style="width: 250px">
<a-form ref="formRef" layout="vertical" :model="formData">
<Item v-model:value="formData.value" />
</a-form>
</div>
</template>
<slot>
<a-button type="link" :disabled="disabled" style="padding: 0">
<template #icon>
<AIcon type="EditOutlined" />
</template>
</a-button>
</slot>
</PopoverModal>
</template>
<script setup name="MetadataBoolean">
import { PopoverModal } from '../index'
import Item from './Item.vue'
import {Form} from "ant-design-vue";
const emit = defineEmits([
'update:trueText',
'update:trueValue',
'update:falseText',
'update:falseValue',
'confirm',
'cancel'
]);
const props = defineProps({
trueText: {
type: String,
default: undefined
},
trueValue: {
type: String,
default: undefined
},
falseText: {
type: String,
default: undefined
},
falseValue: {
type: String,
default: undefined
},
placement: {
type: String,
default: 'top',
},
disabled: {
type: Boolean,
default:false
}
});
const formItemContext = Form.useInjectFormItemContext();
const formRef = ref()
const visible = ref(false)
const formData = reactive({
value: {
trueText: props.trueText || '是',
trueValue: props.trueValue || 'true',
falseText: props.falseText || '否',
falseValue: props.falseValue || 'false',
}
})
const onOk = async () => {
const data = await formRef.value.validate()
if (data) {
visible.value = false
emit('update:trueText', formData.value.trueText);
emit('update:trueValue', formData.value.trueValue);
emit('update:falseText', formData.value.falseText);
emit('update:falseValue', formData.value.falseValue);
emit('confirm', formData.value);
formItemContext.onFieldChange()
}
}
const onCancel = () => {
formRef.value?.resetFields();
formData.value.trueText = props.trueText || '是'
formData.value.trueValue = props.trueValue || 'true'
formData.value.falseText = props.falseText || '否'
formData.value.falseValue = props.falseValue || 'false'
emit('cancel');
}
watch(() => [
props.trueText,
props.trueValue,
props.falseText,
props.falseValue,
], () => {
formData.value.trueText = props.trueText || '是'
formData.value.trueValue = props.trueValue || 'true'
formData.value.falseText = props.falseText || '否'
formData.value.falseValue = props.falseValue || 'false'
})
</script>
<style scoped>
</style>

View File

@ -0,0 +1,80 @@
<template>
<j-select
v-model:value="myValue"
style="width: 100%;"
:options="options"
:getPopupContainer="(node) => tableWrapperRef || node"
@change="change"
>
</j-select>
</template>
<script setup name="BooleanSelect">
import {useTableWrapper} from "@/components/Metadata/Table/context";
import {isBoolean} from "lodash-es";
import { selectProps } from 'ant-design-vue/lib/select'
const props = defineProps({
...selectProps(),
value: {
type: [Boolean, Number, String],
default: true
},
trueLabel: {
type: String,
default: '必填',
},
falseLabel: {
type: String,
default: '不必填',
},
trueValue: {
type: [Boolean, Number, String],
default: true,
},
falseValue: {
type: [Boolean, Number, String],
default: false
}
})
const emit = defineEmits(['update:value', 'change'])
const tableWrapperRef = useTableWrapper()
const myValue = ref()
const options = computed(() => {
const _trueValue = isBoolean(props.trueValue) ? String(props.trueValue) : props.trueValue
const _falseValue = isBoolean(props.falseValue) ? String(props.falseValue) : props.falseValue
return [
{
label: props.trueLabel,
value: _trueValue,
baseValue: props.trueValue
},
{
label: props.falseLabel,
value: _falseValue,
baseValue: props.falseValue
}
]
})
const change = (e) => {
const item = options.value.find(item => item.value === myValue.value)
emit('update:value', item.baseValue)
emit('change', item.baseValue)
}
watch(() => [props.value, options.value], () => {
const item = options.value.find(item => item.baseValue === props.value)
myValue.value = item ? item.value : options.value[0].value
}, { immediate: true })
</script>
<style scoped>
</style>

View File

@ -0,0 +1,90 @@
import MenuContext from './menu.vue'
import { h, render } from 'vue'
import {handlePureRecord} from "@/components/Metadata/Table/utils";
import {omit} from "lodash-es";
let curInstance: Record<string, any> | null = null
let seed = 1
let copyValue: any
const contextMenu = (e: Event, data: any, context: any) => {
if (curInstance) {
curInstance.destroy()
}
curInstance = null
let id = seed++
// 创建一个临时的div用于挂载我们的菜单
const container = document.createElement('div') as HTMLElement
// 获取body标签用于挂载整个菜单
const appendTo = context.getPopupContainer() || document.body
// 传给menu组件的props
const props = {
data: data,
onClose: () => {
if(curInstance){
curInstance.destroy()
}
},
onClick: (type: string) => {
const copyRecord = handlePureRecord(copyValue)
if (copyRecord.expands) {
copyRecord.expands = omit(copyRecord.expands, ['isProduct'])
}
context.click(type, data, handlePureRecord(copyRecord))
},
onCopy: (data: any) => {
copyValue = data
context.click('copy', data)
},
paste: !!copyValue
}
// 渲染虚拟节点
const vnode = h(
MenuContext,
props
)
// vnode为需要渲染的虚拟节点container为渲染的容器
render(vnode, container)
// 首先需要先把菜单真正渲染到页面,才能拿到它的宽度和高度
appendTo.appendChild(container.firstElementChild as Node)
// 当前真正的菜单节点上面输出的vnode中可以看到el就是我们的菜单节点
const curMenu = vnode.el!
// 获取curMenu的高度和宽度用于临界的计算
const { offsetWidth, offsetHeight } = curMenu!
// 获取body的可视区域的宽度
const { clientWidth } = appendTo
// 取出右键点击时的坐标clientX是距离左侧的位置clientY是距离顶部的位置
const { clientX, clientY } = e
// 当前可视区域的宽度 - 当前鼠标距离浏览器左边的距离
// 如果 大于菜单的宽度,说明正常设置菜单距离左边界的距离,即设置style.left
// 否则菜单需要在鼠标左侧展示即需要设置style.right组件距离可视区域右侧的距离
const leftOrRight = clientWidth - clientX > offsetWidth ? "left" : "right"
// 当前浏览器的高度(不包含滚动条) - 当前鼠标距离浏览器上边的距离
// 如果 大于菜单的高度,说明可以正常设置菜单距离上边界的距离,即设置style.top
// 否则需要设置菜单距离底部边界的位置即style.bottom
const topOrBottom = window.innerHeight - clientY > offsetHeight ? "top" : "bottom"
const offsetLeft = Math.abs(clientWidth - clientX)
// 设置left或者right的style
curMenu.style[leftOrRight] = leftOrRight === "left" ? `${clientX + 20}px` : `${offsetLeft}px`
// 设置top或者bottom的style
curMenu.style[topOrBottom] = topOrBottom === 'bottom' ? '2px' : `${clientY}px`
const instance = {
id,
destroy: () => {
curInstance = null
render(null, container)
},
cleanCopy: () => {
copyValue = null
}
}
curInstance = instance
return instance
}
export default contextMenu

View File

@ -0,0 +1,117 @@
<template>
<div
class="metadata-context-menu"
ref="contextMenu"
tabindex="-1"
@blur="close"
>
<a-menu @click="clickFunc">
<a-menu-item key="add">
<template #icon>
<AIcon type="PlusSquareOutlined" />
</template>
新增行
</a-menu-item>
<a-menu-item key="copy">
<template #icon>
<AIcon type="icon-copy" />
</template>
复制行
</a-menu-item>
<a-menu-item key="paste" :disabled="showPaste">
<template #icon>
<AIcon type="icon-paste" />
</template>
粘贴行
</a-menu-item>
<a-menu-item key="detail" :disabled="showDetail">
<template #icon>
<AIcon type="icon-chakan" />
</template>
查看详情
</a-menu-item>
<a-menu-item key="delete" class="danger" :disabled="showDelete">
<template #icon>
<AIcon type="DeleteOutlined" />
</template>
删除
</a-menu-item>
</a-menu>
</div>
</template>
<script setup name="MetadataContextMenu">
import { onMounted, ref, nextTick } from "vue";
import { AIcon } from 'jetlinks-ui-components'
const props = defineProps({
data: {type: Object, default: () => ({})},
onClose: { type: Function, default: () => {} },
onClick: { type: Function, default: () => {} },
onCopy: { type: Function, default: () => {} },
paste: { type: Object, default: () => ({}) }
});
const contextMenu = ref(null);
const showDetail = computed(() => {
return !props.data.id
})
const showPaste = computed(() => {
return !props.paste
})
const showDelete = computed(() => {
return props.data.expands?.isProduct
})
const clickFunc = ({ key }) => {
if (key === 'copy') {
props.onCopy(props.data)
}
props.onClick(key)
};
const close = (e) => {
setTimeout(() => {
props.onClose()
}, 300)
}
onMounted(async () => {
//
await nextTick();
// focus
contextMenu.value.focus();
});
</script>
<style scoped lang="less">
.metadata-context-menu{
position: fixed;
box-shadow: 0 0 12px rgba(0, 0, 0 ,.2);
border-radius: 4px;
overflow: hidden;
width: 192px;
padding: 4px;
background-color: #fff;
:deep(.ant-menu) {
border-right: none;
.ant-menu-item {
margin: 0;
height: 32px;
&.danger {
color: @error-color;
}
}
}
:deep(.ant-menu-item-active) {
background-color: var(--ant-primary-1);;
}
}
</style>

View File

@ -0,0 +1,66 @@
<template>
<a-form-item
label="时间格式"
required
:name="name"
:rules="[
{
required: true,
message: '请选择时间格式',
},
]"
>
<AutoComplete
v-model:value="date"
:options="options"
mode="tags"
placeholder="请选择时间格式"
:dropdownStyle="{ zIndex: 1072}"
:getPopupContainer="(node) => tableWrapperRef || node"
@change="change"
/>
</a-form-item>
</template>
<script setup name="MetadataDateItem">
import { AutoComplete } from 'jetlinks-ui-components'
import {useTableWrapper} from "components/Metadata/Table/context";
const props = defineProps({
value: {
type: String,
default: undefined,
},
name: {
type: String,
default: 'format',
},
});
const emit = defineEmits(['update:value']);
const options = [
{ label: 'yyyy-MM-dd HH:mm:ss', value: 'yyyy-MM-dd HH:mm:ss' },
{ label: 'yyyy-MM-dd', value: 'yyyy-MM-dd' },
{ label: 'hh:mm:ss', value: 'hh:mm:ss' },
];
const date = ref(props.value);
const tableWrapperRef = useTableWrapper()
const change = () => {
emit('update:value', date.value);
};
watch(
() => props.value,
() => {
date.value = props.value;
}
);
</script>
<style scoped>
</style>

View File

@ -0,0 +1,78 @@
<template>
<PopoverModal
v-model:visible="visible"
:placement="placement"
@ok="onOk"
@cancel="onCancel"
>
<template #content>
<div style="width: 200px">
<a-form ref="formRef" layout="vertical" :model="formData">
<Item v-model:value="formData.format" />
</a-form>
</div>
</template>
<slot>
<a-button type="link" :disabled="disabled" style="padding: 0">
<template #icon>
<AIcon type="EditOutlined" :class="{'table-form-required-aicon': !value}"/>
</template>
</a-button>
</slot>
</PopoverModal>
</template>
<script setup name="MetadataDate">
import { PopoverModal } from '../index'
import Item from './Item.vue'
import { Form } from 'ant-design-vue'
const emit = defineEmits(['update:value', 'confirm', 'cancel']);
const props = defineProps({
value: {
type: String,
default: undefined,
},
placement: {
type: String,
default: 'top',
},
disabled: {
type: Boolean,
default:false
}
});
const formItemContext = Form.useInjectFormItemContext();
const formRef = ref()
const visible = ref(false)
const formData = reactive({
format: props.value,
})
const onOk = async () => {
const data = await formRef.value.validate()
if (data) {
visible.value = false
emit('update:value', formData.format);
emit('confirm', formData.format);
formItemContext.onFieldChange()
}
}
const onCancel = () => {
formRef.value?.resetFields();
formData.format = props.value;
emit('cancel');
}
watch(() => props.value, (newValue) => {
formData.format = newValue
})
</script>
<style scoped>
</style>

View File

@ -0,0 +1,48 @@
<template>
<a-form-item
label="精度"
:name="name"
>
<a-input-number
v-model:value="scale"
style="width: 100%"
:precision="0"
:min="0"
:max="99"
placeholder="请输入0-99以内的整数"
@change="change"
/>
</a-form-item>
</template>
<script setup name="MetadataScaleItem">
const emit = defineEmits(['update:value']);
const props = defineProps({
value: {
type: Number,
default: 0,
},
name: {
type: String,
default: 'scale',
},
});
const scale = ref(props.value);
const change = () => {
emit('update:value', scale.value);
};
watch(
() => props.value,
() => {
scale.value = props.value;
});
</script>
<style scoped>
</style>

View File

@ -0,0 +1,90 @@
<template>
<PopoverModal
v-model:visible="visible"
:placement="placement"
@ok="onOk"
@cancel="onCancel"
>
<template #content>
<div style="width: 200px">
<a-form ref="formRef" layout="vertical" :model="formData">
<a-form-item label="单位" name="unit" :rules="[{ max: 64, message: '最多可输入64个字符' }]">
<UnitSelect v-model:value="formData.unit"/>
</a-form-item>
<ScaleItem v-model:value="formData.scale" />
</a-form>
</div>
</template>
<slot>
<a-button type="link" :disabled="disabled" style="padding: 0">
<template #icon>
<AIcon type="EditOutlined"/>
</template>
</a-button>
</slot>
</PopoverModal>
</template>
<script setup name="MetadataDouble">
import { UnitSelect, PopoverModal } from '../index'
import ScaleItem from './ScaleItem.vue'
import {Form} from "ant-design-vue";
const emit = defineEmits(['update:value', 'cancel', 'confirm']);
const props = defineProps({
value: {
type: Object,
default: () => ({}),
},
placement: {
type: String,
default: 'top',
},
disabled: {
type: Boolean,
default:false
}
});
const formItemContext = Form.useInjectFormItemContext();
const formRef = ref()
const visible = ref(false)
const formData = reactive({
unit: props.value?.unit,
scale: props.value?.scale || 0, //
});
const onOk = async () => {
const data = await formRef.value.validate()
if (data) {
visible.value = false
emit('update:value', {
...props.value,
...formData
});
emit('confirm', {
...props.value,
...formData
});
formItemContext.onFieldChange()
}
}
const onCancel = () => {
formRef.value?.resetFields();
formData.unit = props.value?.unit;
formData.scale = props.value?.scale || 0;
emit('cancel');
}
watch(() => props.value, (newValue) => {
formData.unit = props.value?.unit;
formData.scale = props.value?.scale || 0;
})
</script>
<style scoped>
</style>

View File

@ -0,0 +1,74 @@
<template>
<a-form-item :name="name" :rules="rules" :validate-first="true">
<template #label>
<span style="color: #ff4d4f; padding-right: 4px; padding-top: 2px">*</span>
枚举项
</template>
<Content ref="tableRef" v-model:value="dataSource" @change="change" />
</a-form-item>
</template>
<script setup name="MetadataEnumItem">
import Content from './ItemContent.vue'
const emit = defineEmits(['update:value'])
const props = defineProps({
value: {
type: Array,
default: () => [],
},
type: {
type: Boolean,
default: false,
},
name: {
type: String,
default: 'elements',
},
});
const dataSource = ref(props.value || [])
const tableRef = ref()
const rules = [
{
validator: async (_, value) =>{
console.log(value, dataSource.value)
if (!dataSource.value?.length) {
return Promise.reject('请添加枚举项');
}
// console.log(tableRef, tableRef.value)
// const data = await tableRef.value?.validate?.()
// if (!data) {
// return Promise.reject('');
// }
return Promise.resolve();
},
},
];
const change = () => {
emit('update:value', dataSource.value)
}
const validate = async () => {
const res = await tableRef.value?.validate()
return res
}
defineExpose({
validate
})
watch(
() => JSON.stringify(props.value),
(val) => {
dataSource.value = val ? JSON.parse(val) : [];
},
);
</script>
<style scoped>
</style>

View File

@ -0,0 +1,150 @@
<template>
<div>
<EditTable
ref="tableRef"
rowKey="value"
:columns="columns"
:dataSource="dataSource"
:pagination="false"
:height="200"
:disableMenu="false"
:validateRowKey="true"
>
<template #value="{ record, index }">
<EditTableFormItem
:name="[index, 'value']"
>
<a-input v-model:value="record.value" @change="valueChange"/>
</EditTableFormItem>
</template>
<template #text="{ record, index }">
<EditTableFormItem
:name="[index, 'text']"
>
<a-input v-model:value="record.text" @change="valueChange"/>
</EditTableFormItem>
</template>
<template #action="{ index }">
<a-button danger type="link" @click="() => deleteItem(index)">
<template #icon>
<AIcon type="DeleteOutlined" />
</template>
</a-button>
</template>
</EditTable>
<a-button class="enum-table-add" @click="addItem">
<template #icon><AIcon type="PlusOutlined" /></template>
新增枚举项
</a-button>
</div>
</template>
<script setup name="EnumItemContent">
import EditTable from '../../Table.vue'
import EditTableFormItem from '../../TableFormItem.vue'
import { Form } from "ant-design-vue";
const emit = defineEmits(['update:value', 'change'])
const props = defineProps({
value: {
type: Array,
default: () => [],
},
});
const formItemContext = Form.useInjectFormItemContext();
const dataSource = ref(props.value || [])
const tableRef = ref()
const columns = [{
title: 'Value',
dataIndex: 'value',
form: {
rules: [
{ max: 64, message: '最多可输入64个字符' },
{
asyncValidator: (rule, value, ...setting) => {
const option = setting[2]
if (!value) {
return Promise.reject('请输入Value值')
}
const isSome = dataSource.value.some((item) => {
return item.__dataIndex !== option.index && item.value === value
})
if (isSome) {
return Promise.reject('该Value值已存在')
}
return Promise.resolve();
}
}
]
},
},
{
title: 'Text',
dataIndex: 'text',
width: 150,
form: {
rules: [
{ required: true, message: '请输入Text' },
{ max: 64, message: '最多可输入64个字符' },
]
}
},
{
title: '操作',
dataIndex: 'action',
width: 60,
},
]
const deleteItem = (index) => {
dataSource.value.splice(index, 1)
emit('update:value', dataSource.value)
emit('change', dataSource.value)
formItemContext.onFieldChange()
}
const addItem = () => {
dataSource.value.push({
value: undefined,
text: undefined
})
emit('update:value', dataSource.value)
emit('change', dataSource.value)
formItemContext.onFieldChange()
}
const valueChange = () => {
emit('update:value', dataSource.value)
emit('change', dataSource.value)
formItemContext.onFieldChange()
}
const validate = async () => {
const res = await tableRef.value?.validate()
return res
}
defineExpose({
validate
})
watch(
() => JSON.stringify(props.value),
() => {
dataSource.value = props.value || [];
},
);
</script>
<style scoped>
.enum-table-add {
width: 100%;
margin-top: 4px;
}
</style>

View File

@ -0,0 +1,87 @@
<template>
<PopoverModal
v-model:visible="visible"
:placement="placement"
@ok="onOk"
@cancel="onCancel"
>
<template #content>
<div style="width: 450px" v-if="visible">
<a-form ref="formRef" layout="vertical" :model="formData">
<Item ref="tableRef" :value="formData.elements"/>
</a-form>
</div>
</template>
<slot>
<a-button type="link" :disabled="disabled" style="padding: 0">
<template #icon>
<AIcon type="EditOutlined" :class="{'table-form-required-aicon': !value.length}"/>
</template>
</a-button>
</slot>
</PopoverModal>
</template>
<script setup name="MetadataEnum">
import { PopoverModal } from '../index'
import { cloneDeep } from 'lodash-es'
import Item from './Item.vue'
import {Form} from "ant-design-vue";
const emit = defineEmits(['update:value', 'confirm', 'cancel']);
const props = defineProps({
value: {
type: Array,
default: () => ([]),
},
placement: {
type: String,
default: 'top',
},
disabled: {
type: Boolean,
default:false
}
});
const formItemContext = Form.useInjectFormItemContext();
const formRef = ref();
const tableRef = ref();
const visible = ref(false)
const formData = reactive({
elements: cloneDeep(props.value) || [],
});
const onCancel = () => {
formRef.value?.resetFields();
formData.elements = cloneDeep(props.value) || [];
emit('cancel');
};
const onOk = async () => {
const data = await formRef.value.validate()
const tableData = await tableRef.value.validate()
console.log(data, tableData)
if (data && tableData) {
visible.value = false
formData.elements = tableData
emit('update:value', formData.elements)
emit('confirm', formData.elements);
formItemContext.onFieldChange()
}
}
watch(
() => JSON.stringify(props.value),
() => {
formData.elements = cloneDeep(props.value) || [];
}
);
</script>
<style scoped>
</style>

View File

@ -0,0 +1,54 @@
<template>
<a-form-item
label="文件类型"
required
:name="name"
:rules="[{ required: true, message: '请选择文件类型' }]"
>
<j-check-button
v-model:value="myValue"
:options="[
{ label: 'URL', value: 'url' },
{ label: 'Base64', value: 'base64' },
{ label: 'binary', value: 'binary' },
]"
@change="change"
/>
</a-form-item>
</template>
<script setup name="MetadataFileType">
import { ref, watch } from 'vue';
const props = defineProps({
name: {
type: [String, Array],
default: 'bodyType',
},
value: {
type: String,
default: undefined,
},
});
const emit = defineEmits(['update:value']);
const myValue = ref(props.value);
const change = () => {
emit('update:value', myValue.value);
};
watch(
() => props.value,
() => {
myValue.value = props.value;
}
);
</script>
<style scoped>
</style>

View File

@ -0,0 +1,81 @@
<template>
<PopoverModal
v-model:visible="visible"
:placement="placement"
@ok="onOk"
@cancel="onCancel"
>
<template #content>
<div style="width: 250px">
<a-form ref="formRef" :model="formData" layout="vertical">
<Type v-model:value="formData.bodyType" name="bodyType" />
</a-form>
</div>
</template>
<slot>
<a-button type="link" :disabled="disabled" style="padding: 0">
<template #icon>
<AIcon type="EditOutlined" :class="{'table-form-required-aicon': !value}"/>
</template>
</a-button>
</slot>
</PopoverModal>
</template>
<script setup name="MetadataFile">
import Type from './Type.vue'
import { PopoverModal } from '../index'
import {Form} from "ant-design-vue";
const emit = defineEmits(['update:value', 'confirm', 'cancel']);
const props = defineProps({
value: {
type: String,
default: undefined,
},
placement: {
type: String,
default: 'top',
},
disabled: {
type: Boolean,
default:false
}
});
const formItemContext = Form.useInjectFormItemContext();
const formRef = ref();
const formData = reactive({
bodyType: props.value,
});
const visible = ref(false)
const onOk = async () => {
const data = await formRef.value.validate()
if (data) {
visible.value = false
emit('update:value', formData.bodyType)
emit('onOk', formData.bodyType)
formItemContext.onFieldChange()
}
}
const onCancel = () => {
formRef.value?.resetFields();
formData.bodyType = props.value;
emit('cancel');
}
watch(
() => props.value,
() => {
formData.bodyType = props.value;
},
);
</script>
<style scoped>
</style>

View File

@ -0,0 +1,68 @@
<template>
<a-auto-complete
v-model:value="myValue"
:options="options"
placeholder="请选择分组"
style="width: 100%;"
:getPopupContainer="(node) => tableWrapperRef || node"
@search="onSearch"
@select="onSelect"
/>
</template>
<script setup name="MetadataGroup">
import {METADATA_GROUP_OPTIONS} from "../../consts";
import {useTableWrapper} from "@/components/Metadata/Table/context";
const props = defineProps({
value: {
type: String,
default: undefined
}
})
const emit = defineEmits(['update:value', 'change'])
const myValue = ref(props.value)
const searchValue = ref()
const groupSetting = inject(METADATA_GROUP_OPTIONS, {})
const tableWrapperRef = useTableWrapper()
const options = computed(() => {
if (searchValue.value) {
let _options = (groupSetting.options.value || []).filter(item => {
return item.value.includes(searchValue.value)
})
if (!_options.length) {
_options.unshift({
label: searchValue.value,
value: searchValue.value
})
}
return _options
}
return groupSetting.options.value || []
})
const onSearch = (value) => {
searchValue.value = value
}
const onSelect = (value) => {
groupSetting.addOptions({ label: value, value })
emit('update:value', value)
emit('change', value)
}
watch(() => props.value, () => {
myValue.value = props.value
})
</script>
<style scoped>
</style>

View File

@ -0,0 +1,75 @@
<template>
<PopoverModal
v-model:visible="visible"
:placement="placement"
@ok="onOk"
@cancel="onCancel"
>
<template #content>
<div style="width: 200px">
<a-form ref="formRef" layout="vertical" :model="formData">
<a-form-item label="单位" name="unit" :rules="[{ max: 64, message: '最多可输入64个字符' }]">
<UnitSelect v-model:value="formData.unit" />
</a-form-item>
</a-form>
</div>
</template>
<slot>
<a-button type="link" :disabled="disabled" style="padding: 0">
<template #icon>
<AIcon type="EditOutlined"/>
</template>
</a-button>
</slot>
</PopoverModal>
</template>
<script setup name="MetadataInteger">
import { UnitSelect, PopoverModal } from '../index'
const emit = defineEmits(['update:value', 'cancel', 'confirm']);
const props = defineProps({
value: {
type: String,
default: undefined,
},
placement: {
type: String,
default: 'top',
},
disabled: {
type: Boolean,
default:false
}
});
const formRef = ref()
const visible = ref(false)
const formData = reactive({
unit: props.value,
});
const onOk = async () => {
const data = await formRef.value.validate()
if (data) {
visible.value = false
emit('update:value', formData.unit);
emit('confirm', formData.unit);
}
}
const onCancel = () => {
formRef.value?.resetFields();
formData.unit = props.value;
emit('cancel');
}
watch(() => props.value, (newValue) => {
formData.unit = newValue
})
</script>
<style scoped>
</style>

View File

@ -0,0 +1,232 @@
<template>
<PopoverModal
v-model:visible="visible"
:placement="placement"
@ok="onOk"
@cancel="onCancel"
>
<template #content>
<div style="width: 750px">
<EditTable
ref="tableRef"
:columns="myColumns"
:dataSource="dataSource"
:pagination="false"
:height="200"
>
<!-- <template v-for="(_, key) in slots" :key="key" #[key]="slotData">-->
<!-- <slot :name="key" v-bind="slotData"/>-->
<!-- </template>-->
<template #id="{ record, index }">
<EditTableFormItem :name="[index, 'id']">
<a-input v-model:value="record.id" placeholder="请输入标识"/>
</EditTableFormItem>
</template>
<template #name="{ record, index }">
<EditTableFormItem :name="[index, 'name']">
<a-input v-model:value="record.name" placeholder="请输入名称"/>
</EditTableFormItem>
</template>
<template #expands="{ record }">
<BooleanSelect v-model:value="record.expands.required"/>
</template>
<template #valueType="{ record, index }">
<EditTableFormItem :name="[index, 'valueType']">
<div style="display: flex; gap: 12px; align-items: center">
<TypeSelect v-model:value="record.valueType.type" style="flex: 1 1 0;min-width: 0" />
<DoubleParams v-if="['float', 'double'].includes(record.valueType.type)" v-model:value="record.valueType" placement="topRight"/>
<StringParams v-else-if="record.valueType.type === 'string'" v-model:value="record.valueType" placement="topRight"/>
<DateParams v-else-if="record.valueType.type === 'date'" v-model:value="record.valueType.format" placement="topRight"/>
<FileParams v-else-if="record.valueType.type === 'file'" v-model:value="record.valueType.bodyType" placement="topRight"/>
<EnumParams v-else-if="record.valueType.type === 'enum'" v-model:value="record.valueType.elements" placement="topRight"/>
<BooleanParams
v-else-if="record.valueType.type === 'boolean'"
v-model:falseText="record.valueType.falseText"
v-model:falseValue="record.valueType.falseValue"
v-model:trueText="record.valueType.trueText"
v-model:trueValue="record.valueType.trueValue"
placement="topRight"
/>
<ArrayParams v-else-if="record.valueType.type === 'array'" v-model:value="record.valueType.elementType" placement="topRight"/>
</div>
</EditTableFormItem>
</template>
<template #action="{ index }">
<a-button danger type="link" style="padding: 0 5px" @click="() => deleteItem(index)">
<template #icon>
<AIcon type="DeleteOutlined" />
</template>
</a-button>
</template>
</EditTable>
<a-button style="width: 100%;margin-top: 4px" @click="addItem">
<template #icon><AIcon type="PlusOutlined" /></template>
新增
</a-button>
</div>
</template>
<slot>
<a-button type="link" :disabled="disabled" style="padding: 0">
<template #icon>
<AIcon type="EditOutlined" :class="{'table-form-required-aicon': !value.length}"/>
</template>
</a-button>
</slot>
</PopoverModal>
</template>
<script setup name="MetadataObject">
import { PopoverModal } from '../index'
import BooleanSelect from "../BooleanSelect/index.vue";
import { EditTable, TypeSelect, EditTableFormItem, StringParams, DateParams, FileParams, EnumParams, BooleanParams, ObjectParams, ArrayParams, DoubleParams } from '@/components/Metadata/Table'
import {Form} from "ant-design-vue";
const props = defineProps({
value: {
type: Array,
default: () => [],
},
placement: {
type: String,
default: 'top',
},
type: {
type: String,
default: 'properties'
},
disabled: {
type: Boolean,
default:false
}
});
const emit = defineEmits(['update:value', 'confirm', 'cancel']);
const formItemContext = Form.useInjectFormItemContext();
const slots = useSlots()
const tableRef = ref()
const dataSource = ref([])
const visible = ref(false)
const defaultColumns = [
{
title: '参数标识',
dataIndex: 'id',
form: {
rules: [
{
asyncValidator(_, value, ...setting) {
if (value) {
const option = setting[2]
const isSome = dataSource.value.some((item) => {
return item.__dataIndex !== option.index && item.id === value
})
if (isSome) {
return Promise.reject('该标识已存在')
}
return Promise.resolve()
}
return Promise.reject('请输入标识')
}
},
{ max: 64, message: '最多可输入64个字符' },
{
pattern: /^[a-zA-Z0-9_]+$/,
message: '标识只能由数字、字母、下划线组成',
},
]
}
},
{
title: '参数名称',
dataIndex: 'name',
form: {
rules: [
{
required: true,
message: '请输入名称'
},
{ max: 64, message: '最多可输入64个字符' },
]
}
},
props.type === 'functions' ? {
title: '填写约束',
dataIndex: 'expands',
width: 120,
} : null,
{
title: '数据类型',
dataIndex: 'valueType',
width: 240,
form: {
required: true,
rules: [{
validator(_, value) {
if (!value?.type) {
return Promise.reject('请选择数据类型')
}
return Promise.resolve()
}
}]
},
}
]
const myColumns = computed(() => {
return [
...defaultColumns.filter(item => !!item),
{
dataIndex: 'action',
title: '操作',
width: 60,
}
]
})
const onOk = async () => {
const data = await tableRef.value.validate()
if (data) {
visible.value = false
emit('update:value', data)
emit('confirm', data)
formItemContext.onFieldChange()
}
}
const onCancel = () => {
emit('cancel');
};
const deleteItem = (index) => {
dataSource.value.splice(index, 1)
}
const addItem = () => {
dataSource.value.push({
id: undefined,
name: undefined,
expands: {
required: false
},
valueType: {
expands: {}
}
})
}
watch(() => [JSON.stringify(props.value), visible.value], (val) => {
if (visible.value) {
dataSource.value = JSON.parse(val[0] || '[]')
}
}, { immediate: true })
</script>
<style scoped>
</style>

View File

@ -0,0 +1,111 @@
<template>
<a-popover
trigger="click"
:visible="visible"
:overlay-class-name="{
[warpClassNames]: true,
'metadata-table-popover-warp': true
}"
:overlayStyle="{
'zIndex': 1070
}"
:getPopupContainer="(node) => tableWrapperRef || node"
@visibleChange="null"
>
<template #content>
<div v-if="visible">
<div :style="bodyStyle">
<slot name="content"/>
</div>
<div class="footer">
<a-space>
<a-button v-if="showCancel" size="small" @click="cancel">取消</a-button>
<a-button v-if="showOk" type="primary" size="small" @click="submit" :loading="confirmLoading">确认</a-button>
</a-space>
</div>
</div>
</template>
<span @click="() => visibleChange(true)">
<slot />
</span>
</a-popover>
</template>
<script setup name="MetadataPopover">
import { useMask } from '../utils'
import {useTableWrapper, useTableFullScreen} from "@/components/Metadata/Table/context";
const props = defineProps({
placement: {
type: String,
default: 'top'
},
confirmLoading: {
type: Boolean,
default: false
},
showCancel: {
type: Boolean,
default: true
},
showOk: {
type: Boolean,
default: true
},
bodyStyle: {
type: [String, Object],
default: undefined
},
visible: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['ok', 'cancel', 'update:visible'])
const tableWrapperRef = useTableWrapper()
const fullScreen = useTableFullScreen()
const { warpClassNames, visibleChange, visible } = useMask(props.visible, {
visibleChange(v) {
emit('update:visible', v)
}
})
const cancel = () => {
emit('cancel')
visibleChange(false)
}
const submit = async () => {
emit('ok')
}
watch(() => props.visible, (newValue) => {
visibleChange(newValue)
})
watch(() => fullScreen.value, (val) => {
if (!val) {
cancel()
}
})
</script>
<style scoped lang="less">
.footer {
display: flex;
flex-direction: row-reverse;
margin-top: 8px;
}
</style>
<style lang="less">
.metadata-table-popover-warp {
.ant-popover-arrow {
display: none;
}
}
</style>

View File

@ -0,0 +1,153 @@
<template>
<a-popover
trigger="click"
placement="bottom"
:overlayStyle="{
'zIndex': 1050
}"
:getPopupContainer="(node) => tableWrapperRef || node"
@visibleChange="visibleChange"
>
<template #content>
<div class="table-sort-content">
<div class="table-sort-title">
排序
</div>
<div class="table-sort-body">
<a-table
v-if="visible"
size="small"
:columns="columns"
:dataSource="dataSource"
:row-selection="{ selectedRowKeys: mySelectedRowKeys, onChange: onSelectChange }"
:pagination="false"
:scroll="dataSource.length > 5 ? { y: 200 } : undefined"
>
</a-table>
</div>
<div class="table-sort-footer">
<j-space :size="16">
<a-button type="primary" @click="cleanParams" :disabled="!mySelectedRowKeys.length">清空条件</a-button>
<a-button type="primary" ghost @click="onAsc">升序</a-button>
<a-button type="primary" ghost @click="onDesc">降序</a-button>
</j-space>
</div>
</div>
</template>
<AIcon type="icon-paixu" :class="{ 'table-sort-icon': true, 'active': props.active }" />
</a-popover>
</template>
<script setup name="MetadataTableSort">
import {useTableTool, useTableWrapper} from "@/components/Metadata/Table/context";
const props = defineProps({
dataSource: {
type: [Function, Array],
default: () => []
},
sortKey: {
type: [String, Array],
default: undefined
},
dataIndex: {
type: [Number, String],
default: undefined
},
selectedRowKeys: {
type: Array,
default: () => []
},
active: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['click'])
const tableWrapperRef = useTableWrapper()
const tableTool = useTableTool()
const visible = ref(false)
const dataSource = ref([])
const mySelectedRowKeys = ref([])
const columns = [
{
dataIndex: 'name',
title: '名称'
},
{
dataIndex: 'total',
title: '计数',
width: 70
}
]
const visibleChange = (e) => {
if (e) {
mySelectedRowKeys.value = props.selectedRowKeys
dataSource.value = (props.dataSource?.() || []).map((item, index) => Object.assign(item, { __index: index + 1}))
}
if (!e) {
setTimeout(() => {
visible.value = e
}, 300)
} else {
visible.value = e
}
}
const onSelectChange = (keys) => {
mySelectedRowKeys.value = keys
}
const cleanParams = () => {
mySelectedRowKeys.value = []
tableTool.cleanOrder()
}
const handleSortRowKeys = () => {
return dataSource.value
.filter(item => mySelectedRowKeys.value.includes(item.key))
.sort((a, b) => b.__index - a.__index)
.map(item => item.key)
}
const onAsc = () => {
tableTool.order('asc', props.sortKey, handleSortRowKeys(), props.dataIndex)
emit('click')
}
const onDesc = () => {
tableTool.order('desc', props.sortKey, handleSortRowKeys(), props.dataIndex)
emit('click')
}
</script>
<style scoped lang="less">
.table-sort-content {
width: 300px;
.table-sort-title {
}
.table-sort-body {
margin: 12px 0;
}
}
.table-sort-icon {
color: rgba(0,0,0, 0.25);
font-size: 16px;
&.active {
color: @primary-color;
}
}
</style>

View File

@ -0,0 +1,2 @@
export { default as SearchModal } from './search.vue'
export { default as Sort } from './Sort.vue'

View File

@ -0,0 +1,205 @@
<template>
<DragModal
:width="800"
:height="modalHeight"
:title="false"
:dragRang="[600, 200]"
:bodyStyle="{
overflow: 'hidden'
}"
@heightChange="heightChange"
>
<div class="table-search">
<div class="table-search-header">
<div>
<a-space>
<span>查找</span>
<a-input v-model:value="searchValue" :maxlength="64" placeholder="请输入查找内容" />
<a-button type="primary" ghost @click="() => search('all')">查找全部</a-button>
<a-button type="primary" ghost @click="() => search('prev')">上一个</a-button>
<a-button type="primary" ghost @click="() => search('next')">下一个</a-button>
</a-space>
</div>
<div>
<a-button type="primary" @click.stop="onClose">关闭</a-button>
</div>
</div>
<div v-if="visible" style="margin: 12px 0">
<Table
ref="tableRef"
:data-source="filterArray"
:columns="columns"
:height="tableHeight"
:disableMenu="false"
:cellHeight="36"
:rowSelection="{
onSelect: onSelect,
selectedRowKeys: selectedRowKeys
}"
:serial="{
width: openGroup ? 150 : 66,
title: '行数'
}"
>
<template #serial="{ record }">
<span v-if="openGroup">
<Ellipsis>
{{ record.expands.groupName }} {{ record.__oldSerial }}
</Ellipsis>
</span>
<span v-else>
{{ record.__serial }}
</span>
</template>
<template #id="{ record }">
<Ellipsis>
{{ record.id }}
</Ellipsis>
</template>
<template #name="{ record }">
<Ellipsis>
{{ record.name }}
</Ellipsis>
</template>
</Table>
</div>
<div v-if="visible">
查找到 <span class="table-search-result-total">{{filterArray.length}}</span> 条相关属性
</div>
</div>
</DragModal>
</template>
<script setup name="MetadataTableSearch">
import { DragModal } from '@/components/Modal'
import Table from '../../Table.vue'
import {useTableDataSource, useTableOpenGroup, useTableTool, useGroupOptions} from "@/components/Metadata/Table/context";
const props = defineProps({
searchKey: {
type: String,
default: 'id'
}
})
const emit = defineEmits(['close'])
const dataSource = useTableDataSource()
const openGroup = useTableOpenGroup()
const tableTool = useTableTool()
const groupOptions = useGroupOptions()
const searchValue = ref()
const filterArray = ref([])
const visible = ref(false)
const searchIndex = ref(-1)
const modalHeight = ref(100)
const tableHeight = ref(230)
const selectedRowKeys = ref([])
const tableRef = ref()
const columns = [
{
title: '标识',
dataIndex: 'id',
},
{
title: '名称',
dataIndex: 'name',
}
]
const selectedTableRow = (record) => {
tableTool.scrollTo({
...record,
__serial: record.__serial - 1
})
tableTool.selected([record.id])
}
const handleFilterArray = () => {
const cloneDataSource = JSON.parse(JSON.stringify(dataSource.value || '[]')).map(item => Object.assign(item, { __oldSerial: item.__serial}))
const _filterArray = cloneDataSource.filter(item => {
if (item[props.searchKey]) {
return item[props.searchKey].includes(searchValue.value)
}
return false
})
if (openGroup) {
const handleGroup = []
groupOptions.value.forEach(group => {
handleGroup.push(..._filterArray.filter(item => item.expands.groupId === group.value))
})
return handleGroup
}
return _filterArray
}
const search = (key) => {
filterArray.value = handleFilterArray()
if (key === 'all') {
visible.value = true
modalHeight.value = 400
searchIndex.value = 0
} else if (key === 'next') {
searchIndex.value += 1
} else {
searchIndex.value -= 1
}
if (searchIndex.value < 0) {
searchIndex.value = filterArray.value.length - 1
} else if (searchIndex.value > filterArray.value.length - 1){
searchIndex.value = 0
}
const searchItem = filterArray.value[searchIndex.value]
if (key !== 'all' && visible.value) {
tableRef.value?.scrollToByIndex(searchIndex.value - 1)
}
if (filterArray.value.length > 1) {
selectedRowKeys.value = [searchItem.id]
selectedTableRow(searchItem)
} else {
selectedRowKeys.value = []
tableTool.selected([])
}
}
const heightChange = (h) => {
if (h > 340) {
tableHeight.value = h - 160
}
}
const onClose = () => {
console.log('close')
emit('close')
}
const onSelect = (record) => {
searchIndex.value = filterArray.value.findIndex(item => item.id === record.id)
selectedRowKeys.value = [record.id]
selectedTableRow(record)
}
</script>
<style scoped lang="less">
.table-search-header {
display: flex;
justify-content: space-between;
}
.table-search-result-total {
color: @primary-color;
}
</style>

View File

@ -0,0 +1,48 @@
<template>
<a-form-item label="最大长度" :name="name">
<a-input-number
v-model:value="myValue"
style="width: 100%"
placeholder="请输入0-9999以内的整数"
:precision="0"
:min="0"
:max="9999"
@change="change"
/>
</a-form-item>
</template>
<script setup name="MetadataStringItem">
const props = defineProps({
value: {
type: Number,
default: 0
},
name: {
type: String,
default: 'maxLength'
},
})
const myValue = ref(props.value)
const emit = defineEmits(['update:value']);
const change = () => {
emit('update:value', myValue.value);
};
watch(
() => props.value,
(newValue) => {
myValue.value = newValue
},
{ immediate: true },
);
</script>
<style scoped>
</style>

View File

@ -0,0 +1,79 @@
<template>
<PopoverModal
v-model:visible="visible"
:placement="placement"
@ok="onOk"
@cancel="onCancel"
>
<template #content>
<div style="width: 200px">
<a-form ref="formRef" layout="vertical" :model="formData">
<Item v-model:value="formData.maxLength"/>
</a-form>
</div>
</template>
<slot>
<a-button type="link" :disabled="disabled" style="padding: 0">
<template #icon>
<AIcon type="EditOutlined"/>
</template>
</a-button>
</slot>
</PopoverModal>
</template>
<script setup name="MetadataString">
import { PopoverModal } from '../index'
import Item from './Item.vue'
const emit = defineEmits(['update:value', 'confirm', 'cancel']);
const props = defineProps({
value: {
type: Object,
default: () => ({}),
},
placement: {
type: String,
default: 'top',
},
disabled: {
type: Boolean,
default:false
}
});
const formRef = ref()
const visible = ref(false)
const formData = reactive({
maxLength: props.value.maxLength || props.value.expands?.maxLength,
})
const onOk = () => {
visible.value = false
const obj = {
...props.value,
expands: {
maxLength: formData.maxLength
}
}
emit('update:value', obj);
emit('confirm', obj);
}
const onCancel = () => {
formRef.value?.resetFields();
formData.maxLength = props.value.maxLength || props.value.expands?.maxLength,
emit('cancel');
}
watch(() => props.value, (newValue) => {
formData.maxLength = newValue.maxLength || newValue.expands?.maxLength
})
</script>
<style scoped>
</style>

View File

@ -0,0 +1,67 @@
const type = [
{
value: 'int',
label: 'int(整数型)',
},
{
value: 'long',
label: 'long(长整数型)',
},
{
value: 'float',
label: 'float(单精度浮点型)',
},
{
value: 'double',
label: 'double(双精度浮点数)',
},
{
value: 'string',
label: 'text(字符串)',
},
{
value: 'boolean',
label: 'boolean(布尔型)',
},
{
value: 'date',
label: 'date(时间型)',
},
{
value: 'enum',
label: 'enum(枚举)',
},
{
value: 'array',
label: 'array(数组)',
},
{
value: 'object',
label: 'object(结构体)',
},
{
value: 'file',
label: 'file(文件)',
},
{
value: 'password',
label: 'password(密码)',
},
{
value: 'geoPoint',
label: 'geoPoint(地理位置)',
},
];
export const findTypeItem = (value: string) => {
return type.find(item => item.value === value)
}
export const getTypeMap = () => {
return type.reduce((prev, next) => {
prev[next.value] = next.label
return prev
}, {})
}
export default type

View File

@ -0,0 +1,57 @@
<template>
<div :class="{'select-no-value': !value}">
<a-select
v-bind="props"
allow-clear
:value="myValue"
style="width: 100%"
placeholder="请选择数据类型"
:dropdownStyle="{
zIndex: 1071
}"
:options="options"
:getPopupContainer="(node) => tableWrapperRef || node"
@change="change"
/>
</div>
</template>
<script setup name="MetadataType">
import { selectProps } from 'ant-design-vue/lib/select';
import defaultOptions from './data';
import {useTableWrapper} from "@/components/Metadata/Table/context";
const props = defineProps({
...selectProps(),
filter: {
type: Array ,
default: () => [],
}
});
const emit = defineEmits(['update:value']);
const myValue = ref(props.value)
const options = computed(() => {
return defaultOptions.filter(item => !props.filter.includes(item.value) )
})
const tableWrapperRef = useTableWrapper()
const change = (key) => {
myValue.value = key
emit('update:value', key)
}
watch(
() => props.value,
(newValue) => {
myValue.value = newValue;
},
);
</script>
<style scoped>
</style>

View File

@ -0,0 +1,74 @@
<template>
<Select
placeholder="请选择单位"
style="width: 100%"
mode="tags"
v-model:value="myValue"
:dropdownStyle="{
zIndex: 1071
}"
:getPopupContainer="(node) => tableWrapperRef || node"
:options="options"
optionFilterProp="label"
@change="change"
/>
</template>
<script setup name="MetadataUnitSelect">
import { Form, Select } from 'ant-design-vue'
import {useTableWrapper} from "@/components/Metadata/Table/context";
import {useGetUnit} from "@/views/device/components/Metadata/Base/columns";
const props = defineProps({
value: {
type: [String, Number],
default: undefined
},
size: {
type: String,
default: 'middle'
},
name: {
type: String,
default: 'maxLength'
},
rules: {
type: Array,
default: () => []
}
})
const emit = defineEmits(['update:value', 'change'])
const myValue = ref(props.value)
const options = useGetUnit()
const formItemContext = Form.useInjectFormItemContext();
const tableWrapperRef = useTableWrapper()
const change = (v) => {
const newValue = v.length > 1 ? v.pop() : v?.[0];
myValue.value = [newValue];
emit('update:value', newValue);
emit('change', newValue);
formItemContext.onFieldChange();
};
const filterOption = (v, option) => {
return option.label.includes(v)
}
watch(
() => props.value,
(newV) => {
myValue.value = newV ? [newV] : undefined;
},
{ immediate: true },
);
</script>
<style scoped>
</style>

View File

@ -0,0 +1,15 @@
export { default as ArrayParams } from './Array/index.vue'
export { default as ObjectParams } from './Object/index.vue'
export { default as DateParams } from './Date/index.vue'
export { default as BooleanParams } from './Boolean/index.vue'
export { default as EnumParams } from './Enum/index.vue'
export { default as FileParams } from './File/index.vue'
export { default as IntegerParams } from './Integer/index.vue'
export { default as DoubleParams } from './Double/index.vue'
export { default as StringParams } from './String/index.vue'
export { default as PopoverModal } from './Popover/index.vue'
export { default as UnitSelect } from './UnitSelect/index.vue'
export { default as TypeSelect } from './type/index.vue'
export { default as GroupSelect } from './group/index.vue'
export { default as BooleanSelect } from './BooleanSelect/index.vue'

View File

@ -0,0 +1,156 @@
import {randomString} from "@/utils/utils";
import {Ref} from "vue";
let maskIds: string[] = []
export const maskNodeClassName = 'popover-mask'
const bodyHasScrollbar = () => {
return document.body.scrollHeight > document.body.clientHeight;
};
const updateStyle = (dom: HTMLElement | undefined, style: Record<string, any>) => {
if (!dom) return
Object.keys(style).forEach(key => {
dom.style[key] = style[key]
})
}
const bodyHidden = () => {
const hasScrollbar = bodyHasScrollbar()
if (hasScrollbar) {
updateStyle(document.body as HTMLElement, {
overflow: 'hidden',
width: 'calc(100% - 17px)',
})
}
}
const getMaskNode = (id: string, warpClassNames: string) => {
let maskNode = document.querySelector(`#${id}`) as HTMLElement
if (maskNode) {
return maskNode
}
maskNode = document.createElement('div')
maskNode.id = id
updateStyle(maskNode, {
position: 'fixed',
top: 0,
bottom: 0,
left: 0,
right: 0,
'z-index': 1070,
'background-color': '#0003'
})
const warpNode = document.querySelector(`.${warpClassNames}`) as HTMLDivElement
if (!warpNode) return undefined
warpNode.insertAdjacentElement('beforebegin', maskNode)
return maskNode
}
export const useMask = (propVisible: boolean, options: { visibleChange: (visible: boolean) => void }): {
warpClassNames: string,
visible: Ref<boolean>,
hideMask: Function,
showMask: Function,
visibleChange: (visible: boolean) => void
} => {
const visible = ref(propVisible)
const maskDomId = `${maskNodeClassName}-${randomString(6)}`
const warpClassNames = `${maskNodeClassName}-warp-${randomString(4)}`
const createMask = () => {
if (!maskIds.includes(maskDomId)) {
maskIds.push(maskDomId)
}
return getMaskNode(maskDomId, warpClassNames)
}
const getLastMask = (): HTMLElement | undefined => {
const index = maskIds.findIndex(key => key === maskDomId) // 当前遮罩层下标
let dom = undefined
let lastIndex = 0
if (maskIds.length > 0) {
lastIndex = index < 0 ? 0 : index - 1
const lastMaskId = maskIds[lastIndex]
dom = document.querySelector(`#${lastMaskId}`) as HTMLElement
}
return dom
}
const hideLastMask = () => {
const lastMaskNode = getLastMask()
updateStyle(lastMaskNode, {
display: 'none'
})
}
const showLastMask = () => {
const lastMaskNode = getLastMask()
updateStyle(lastMaskNode, {
display: 'block'
})
}
const showMask = () => {
bodyHidden()
setTimeout(() => {
hideLastMask()
const maskNode = createMask()
updateStyle(maskNode, {
display: 'block'
})
}, 10)
}
const hideMask = () => {
const maskNode = createMask()
showLastMask()
const index = maskIds.findIndex(key => key === maskDomId) // 当前遮罩层下标
maskIds.splice(index, 1)
if (index === 0) {
document.body.style = ''
maskIds = []
}
updateStyle(maskNode, {
display: 'none'
})
}
const visibleChange = (v: boolean) => {
visible.value = v
options?.visibleChange(v)
if (v) {
showMask()
} else {
hideMask()
}
}
return {
warpClassNames,
visible,
hideMask,
showMask,
visibleChange
}
}

View File

@ -0,0 +1,22 @@
export const METADATA_GROUP_OPTIONS = Symbol('metadata-group-options')
export const TABLE_WRAPPER = Symbol('table-wrapper')
export const FULL_SCREEN = Symbol('full')
export const RIGHT_MENU = Symbol('right-menu')
export const TABLE_ERROR = Symbol('table-error')
export const TABLE_GROUP_ERROR = Symbol('table-group-error')
export const TABLE_GROUP_OPTIONS = Symbol('table-group-options')
export const TABLE_DATA_SOURCE = Symbol('table-data-source')
export const TABLE_OPEN_GROUP = Symbol('table-open-group')
export const TABLE_TOOL = Symbol('table-tool')
export const TABLE_FORM_ITEM_ERROR = Symbol('table-form-item-error')
export const TABLE_GROUP_ACTIVE = Symbol('table-group-active')

View File

@ -0,0 +1,51 @@
import { provide } from 'vue'
import {
RIGHT_MENU,
TABLE_DATA_SOURCE,
TABLE_ERROR,
TABLE_GROUP_ERROR,
TABLE_WRAPPER,
TABLE_OPEN_GROUP,
TABLE_TOOL,
TABLE_GROUP_OPTIONS,
TABLE_FORM_ITEM_ERROR,
TABLE_GROUP_ACTIVE, FULL_SCREEN
} from "./consts";
type FiledExpose = {
}
const FormContextKey = 'form-context'
export const useFormContext = (options: Record<string, any>) => {
provide(FormContextKey, options)
}
export const useInjectForm = () => {
return inject(FormContextKey, {
addField: (key: string, field: FiledExpose) => {},
dataSource: computed(() => []),
rules: computed(() => undefined),
})
}
export const useInjectError = () => inject(TABLE_ERROR)
export const useTableWrapper = () => inject(TABLE_WRAPPER)
export const useRightMenuContext = () => inject(RIGHT_MENU)
export const useTableGroupError = () => inject(TABLE_GROUP_ERROR)
export const useTableDataSource = () => inject(TABLE_DATA_SOURCE, [])
export const useTableOpenGroup = () => inject(TABLE_OPEN_GROUP, false)
export const useTableTool = () => inject(TABLE_TOOL, false)
export const useGroupOptions = () => inject(TABLE_GROUP_OPTIONS, [])
export const useFormItemError = () => inject(TABLE_FORM_ITEM_ERROR)
export const useGroupActive = () => inject(TABLE_GROUP_ACTIVE)
export const useTableFullScreen = () => inject(FULL_SCREEN)

View File

@ -0,0 +1,245 @@
<template>
<Table
ref="tableRef"
:columns="columns"
:dataSource="dataSource"
:pagination="false"
@scroll-down="onScrollDown"
>
<template #string="{record}">
<div>
<a-input />
<StringParams v-model:value="record.string"/>
</div>
</template>
<template #double="{record}">
<DoubleParams v-model:value="record.double"/>
</template>
<template #integer="{record}">
<IntegerParams v-model:value="record.integer"/>
</template>
<template #date="{record}">
<DateParams v-model:value="record.date"/>
</template>
<template #boolean="{record}">
<BooleanParams v-model:value="record.boolean"/>
</template>
<template #file="{record}">
<FileParams v-model:value="record.file"/>
</template>
<template #enum="{record}">
<EnumParams v-model:value="record.enum"/>
</template>
<template #object="{record}">
<ObjectParams v-model:value="record.object"/>
</template>
<template #array="{record}">
<ArrayParams v-model:value="record.array"/>
</template>
</Table>
<a-button @click="addItem">新增</a-button>
<a-button @click="validate">校验</a-button>
</template>
<script setup name="demo">
import Table from './Table.vue'
import TableFormItem from './TableFormItem.vue'
import {
ArrayParams,
ObjectParams,
DateParams,
BooleanParams,
EnumParams,
FileParams,
IntegerParams,
DoubleParams,
StringParams
} from './components'
const tableRef = ref()
const dataSource = ref(new Array(10).fill(0).map((_, index) => {
return {
string: undefined,
double: undefined,
integer: undefined,
object: undefined,
file: undefined,
date: undefined,
boolean: undefined,
enum: undefined,
array: undefined,
}
}))
const columns = [
{
dataIndex: 'string',
title: 'string',
form: {
rules: {
asyncValidator: (rule, value, cb) => {
if (!value) {
return Promise.reject('请输入列名')
}
return Promise.resolve()
}
}
}
},
{
dataIndex: 'double',
title: 'double',
form: {
rules: {
asyncValidator: (rule, value, cb) => {
if (!value) {
return Promise.reject('请输入age')
}
return Promise.resolve()
}
}
}
},
{
dataIndex: 'integer',
title: 'integer',
form: {
rules: {
asyncValidator: (rule, value, cb) => {
if (!value) {
return Promise.reject('请输入类型')
}
return Promise.resolve()
}
}
}
},
{
dataIndex: 'date',
title: 'date',
form: {
rules: {
asyncValidator: (rule, value, cb) => {
if (!value) {
return Promise.reject('请输入类型')
}
return Promise.resolve()
}
}
}
},
{
dataIndex: 'boolean',
title: 'boolean',
form: {
rules: {
asyncValidator: (rule, value, cb) => {
if (!value) {
return Promise.reject('请输入类型')
}
return Promise.resolve()
}
}
}
},
{
dataIndex: 'file',
title: 'file',
form: {
rules: {
asyncValidator: (rule, value, cb) => {
if (!value) {
return Promise.reject('请输入类型')
}
return Promise.resolve()
}
}
}
},
{
dataIndex: 'enum',
title: 'enum',
form: {
rules: {
asyncValidator: (rule, value, cb) => {
if (!value) {
return Promise.reject('请输入类型')
}
return Promise.resolve()
}
}
}
},
{
dataIndex: 'object',
title: 'object',
form: {
rules: {
asyncValidator: (rule, value, cb) => {
if (!value) {
return Promise.reject('请输入属性')
}
return Promise.resolve()
}
}
}
},
{
dataIndex: 'array',
title: 'array',
form: {
rules: {
asyncValidator: (rule, value, cb) => {
if (!value) {
return Promise.reject('请输入其它')
}
return Promise.resolve()
}
}
}
}
]
const addItem = async () => {
if (dataSource.length) {
await validate()
}
dataSource.value.push({
string: undefined,
double: undefined,
integer: undefined,
object: undefined,
file: undefined,
date: undefined,
boolean: undefined,
enum: undefined,
array: undefined,
})
}
const onScrollDown = () => {
dataSource.value.push(...(new Array(10).fill(1).map(() =>({
string: undefined,
double: undefined,
integer: undefined,
object: undefined,
file: undefined,
date: undefined,
boolean: undefined,
enum: undefined,
array: undefined,
}))))
}
const validate = async () => {
const data = await tableRef.value.validate().catch((error) => {
console.log(error)
})
console.log(data, dataSource.value)
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,252 @@
<template>
<div class="table-group-warp">
<a-tabs type="editable-card" v-model:activeKey="myActiveKey" @edit="onAdd" @change="change">
<a-tab-pane v-for="item in options" :key="item.value" :closable="false">
<template #tab>
<a-dropdown
v-if="myActiveKey === item.value"
:trigger="['click']"
:getPopupContainer="(node) => tableWrapperRef || node"
>
<template #overlay>
<a-menu @click="(e) => { menuClick(e, item)}">
<a-menu-item key="edit">
编辑
</a-menu-item>
<a-menu-item key="delete">
删除
</a-menu-item>
</a-menu>
</template>
<div class="table-group-error-warp">
{{ item.label }} ({{ item.effective}})
<a-tooltip
v-if="errorMap[item.value]"
color="#ffffff"
:arrowPointAtCenter="true"
:get-popup-container="popContainer"
>
<template #title>
<span style="color: #1d2129">校验不合规</span>
</template>
<div class="table-group-error-target"></div>
</a-tooltip>
</div>
</a-dropdown>
<div v-else class="table-group-error-warp">
{{ item.label }} ({{ item.effective}})
<a-tooltip
v-if="errorMap[item.value]"
color="#ffffff"
:arrowPointAtCenter="true"
:getContainer="popContainer"
>
<template #title>
<span style="color: #1d2129">校验不合规</span>
</template>
<div class="table-group-error-target"></div>
</a-tooltip>
</div>
</template>
</a-tab-pane>
</a-tabs>
<a-modal
:visible="visible"
title="编辑分组"
:maskClosable="false"
:getContainer="modalContainer"
@cancel="onCancel"
@ok="onOk"
>
<a-form ref="formRef" :model="formData" @finish="onOk">
<a-form-item name="label" :rules="[{ required: true, message: '请输入分组名称'}, { max: 16, message: '最多可输入16个字符'}]">
<a-input v-model:value="formData.label" placeholder="请输入分组名称"/>
</a-form-item>
<a-form-item v-show="false">
<a-button html-type="submit"></a-button>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup name="MetadataBaseTableGroup">
import {Modal} from "ant-design-vue";
import {randomNumber} from "@/utils/utils";
import {isFullScreen} from "@/utils/comm";
import {useTableGroupError, useTableWrapper} from './context'
const props = defineProps({
options: {
type: Array,
default: () => []
},
activeKey: {
type: String,
default: undefined
}
})
const emit = defineEmits(['delete', 'edit', 'add', 'change', 'update:activeKey'])
const myActiveKey = ref(props.activeKey)
const visible = ref(false)
const type = ref('新增分组')
const errorMap = useTableGroupError()
const tableWrapperRef = useTableWrapper()
const addIndex = ref(0)
const formRef = ref()
const formData = reactive({
label: undefined
})
const onAdd = (targetKey, action) => {
if (action === 'add') {
type.value = 'add'
// _
const groupName = props.options.filter(item => item.label.includes('分组_'))
let index = addIndex.value + 1
let findStatus = false
while (!findStatus) {
const status = groupName.some(item => {
const [ _, _index] = item.label.split('_')
if (index === Number(_index)) {
index = Number(_index) + 1
return true
}
return false
})
findStatus = !status
}
addIndex.value = index
formData.label = '分组_' + index
onOk()
}
}
const onEdit = (record) => {
visible.value = true
type.value = 'edit'
formData.label = record.label
}
const onCancel = () => {
formRef.value.resetFields()
visible.value = false
}
const onOk = () => {
const data = { ...formData }
if (type.value === 'add') {
data.value = 'group_'+randomNumber()
myActiveKey.value = data.value
emit(type.value, data)
emit('change', data.value, data.label)
emit('update:activeKey', data.value)
visible.value = false
} else {
formRef.value.validate().then(() => {
data.value = myActiveKey.value
emit(type.value, data)
emit('change', data.value, data.label)
visible.value = false
})
}
}
const change = () => {
const item = props.options.find(item => item.value === myActiveKey.value)
emit('change', myActiveKey.value, item.label)
emit('update:activeKey', myActiveKey.value)
}
const menuClick = (e, record) => {
if (e.key === 'edit') {
onEdit(record)
} else {
Modal.confirm({
title: '将会同步删除分组内的数据,确认删除?',
onOk: () => {
// activeKey
const index = props.options.findIndex(item => item.value === record.value)
let label = ''
if (index !== 0 && index === props.options.length - 1) {
myActiveKey.value = props.options[index - 1].value
label = props.options[index - 1].label
} else if (index === 0 && props.options.length === 1) {
myActiveKey.value = props.options[0].value
label = props.options[0].label
} else {
myActiveKey.value = props.options[index + 1].value
label = props.options[index + 1].label
}
emit('delete', record.value, index)
emit('change', myActiveKey.value, label)
},
})
}
}
const popContainer = (e) => {
return tableWrapperRef.value || e
}
const modalContainer = (e) =>{
if (isFullScreen()) {
return tableWrapperRef.value || document.body
}
return document.body
}
onMounted(() => {
myActiveKey.value = props.options[0]?.value
emit('change', myActiveKey.value, props.options[0]?.label)
emit('update:activeKey', myActiveKey.value)
})
watch(() => props.activeKey, (val) => {
myActiveKey.value = val
})
</script>
<style scoped lang="less">
.table-group-warp {
:deep(.ant-tabs-nav) {
margin-bottom: 0;
.ant-tabs-tab {
overflow: hidden;
position: relative;
}
.ant-tabs-tab-active {
background-color: #BAE0FF !important;
border-color: #91CAFF !important;
}
.ant-tabs-nav-add {
border: none;
}
}
}
.table-group-error-warp {
color: #1a1a1a !important;
.table-group-error-target {
position: absolute;
right: 0;
top: 0;
border: 16px solid transparent;
border-top-color: @error-color;
border-right-width: 0;
border-bottom-width: 0;
}
}
</style>

View File

@ -0,0 +1,132 @@
<template>
<div class="metadata-edit-table-header-container" :style="style">
<div class="metadata-edit-table-header-cell" v-for="(item, index) in columns" :id="item.dataIndex" :style="{width: `${item.width}px`, left: `${item.left}px`}">
<div :class="{ 'metadata-edit-table-header-cell-box': true, 'header-cell-box-tool': !!(item.sort || item.filter) }">
<div class="table-header-cell-title">
<span>{{ item.title }}</span>
<span v-if="item.form?.required" class="header-cell-required">*</span>
</div>
<div v-if="!!(item.sort || item.filter)" class="table-header-cell-trigger">
<AIcon
v-if="item.filter"
type="SearchOutlined"
style="color: rgba(0,0,0, 0.25)"
@click="() => {showFilter(item.filter.key || item.dataIndex)}"
/>
<Sort
v-if="item.sort"
v-bind="item.sort"
:key="item.dataIndex"
:active="tableTool.sortData.dataIndex === item.dataIndex"
:selectedRowKeys="tableTool.sortData.dataIndex === item.dataIndex ? tableTool.sortData.orderKeys : []"
:dataIndex="item.dataIndex"
@click="sortClick"
/>
</div>
</div>
</div>
<SearchModal
v-if="searchData.visible"
:searchKey="searchData.key"
@close="searchData.visible = false"
/>
</div>
</template>
<script setup name="MetadataBaseTableHeader">
import { SearchModal, Sort } from './components/Search'
import {useTableTool} from "@/components/Metadata/Table/context";
const props = defineProps({
columns: {
type: Array,
default: () => []
},
serial: {
type: Boolean,
default: false
},
style: {
type: Object,
default: undefined
}
})
const tableTool = useTableTool()
const searchData = reactive({
visible: false,
key: undefined
})
const showFilter = (key) => {
searchData.visible = true
searchData.key = key
}
const sortClick = () => {
searchData.visible = false
}
</script>
<style scoped lang="less">
.metadata-edit-table-header-container {
height: 100%;
position: relative;
.metadata-edit-table-header-cell {
height: 100%;
display: inline-flex;
align-items: center;
float: left;
overflow: visible;
position: absolute;
top: 0;
.metadata-edit-table-header-cell-box {
padding: 0 12px;
width: 100%;
&.header-cell-box-tool {
display: flex;
justify-content: space-between;
align-items: center;
}
.table-header-cell-title {
flex: 1;
display: flex;
align-items: center;
font-weight: 600;
}
.header-cell-required {
color: @error-color;
padding-left: 8px;
transform: translateY(3px);
font-weight: 500;
}
&::before {
position: absolute;
top: 50%;
right: 1px;
width: 1px;
height: 1.6em;
pointer-events: none;
background-color: rgba(0,0,0,.06);
transform: translateY(-50%);
transition: background-color .3s;
content: "";
}
&:not(:last-child) {
&::before {
background-color: transparent;
}
}
}
}
}
</style>

View File

@ -0,0 +1,3 @@
export * from './useResizeObserver'
export * from './useValidate'
export * from './useGroup'

View File

@ -0,0 +1,57 @@
import { ref, reactive } from 'vue'
type GroupActiveType = {
value?: number
label?: string
len?: number
}
export const useGroup = (openGroup: boolean = false) => {
const groupOptions = ref<Array<GroupActiveType>>([])
const groupActive = reactive<GroupActiveType>({
value: undefined,
label: undefined
})
/**
* id
* @param key
* @param name
*/
const updateGroupActive = (key: number, name: string) => {
if (openGroup) {
groupActive.value = key
groupActive.label = name
}
}
const addGroup = (val: GroupActiveType) => {
if (openGroup) {
groupOptions.value.push(val)
}
}
const removeGroup = (index: number) => {
if (openGroup) {
groupOptions.value.splice(index, 1)
}
}
const updateGroupOptions = (options: Array<GroupActiveType> = []) => {
if (openGroup) {
groupOptions.value = options
}
}
return {
groupOptions,
groupActive,
updateGroupActive,
addGroup,
removeGroup,
updateGroupOptions
}
}

View File

@ -0,0 +1,26 @@
import {onBeforeUnmount, Ref} from "vue";
import ResizeObserver from "resize-observer-polyfill";
import {debounce} from "lodash-es";
export const useResizeObserver = (tableWrapper: Ref<HTMLElement>, cb: Function) => {
let observer: ResizeObserver
const onResize = (e: any[]) => {
let rect = {}
for (const entry of e) {
rect = entry.contentRect;
}
cb(rect, e)
}
onMounted(() => {
observer = new ResizeObserver(debounce(onResize, 100))
observer.observe(tableWrapper.value)
})
onBeforeUnmount(() => {
observer?.unobserve(tableWrapper.value)
})
}

View File

@ -0,0 +1,112 @@
import {Ref, watch} from "vue";
import Schema from "async-validator";
import {handlePureRecord, collectValidateRules} from "../utils";
import type {ColumnsType } from "../utils";
type DataSourceType = Array<Record<string, any> & { __validate_id?: string, __validate_index?: number}>
export type ErrorItem = {
message: string
__serial: number
__dataIndex: number
field: string
filedValue: any
groupId: string
}
export type OptionsType = {
onError?: (err: Array<Array<ErrorItem>>) => void
onSuccess?: () => void
onEdit?: (item: any) => void
validateRowKey?: Boolean
}
export const useValidate = (dataSource: Ref<DataSourceType>, columns: ColumnsType, rowKey: string, options: OptionsType = {}): {
validate: () => Promise<any>
validateItem: (data: Record<string, any> ) => Promise<any>
errorMap: Ref<Record<string, any>>
rules: Ref<Record<string, any>>
} => {
const errorMap = ref({})
let schemaInstance: any
let rules = ref({})
const _options = Object.assign({ validateRowKey: false }, options)
const validateItem = (data: Record<string, any>, index: number = 0): Promise<any> => {
return new Promise((resolve, reject) => {
schemaInstance.validate(data, { firstFields: true, index}, (err: any[]) => {
if (err?.length) {
reject(err.map(item => ({ ...item, __serial: data.__serial, __dataIndex: index })))
} else {
resolve(data)
}
})
})
}
const validate = () => {
return new Promise((resolve, reject) => {
const filterDataSource = dataSource.value
const len = filterDataSource.length
const error: any[] = []
const success: any[] = []
let validateLen = 0
const end = () => {
validateLen += 1
if (validateLen === len) {
const isSuccess = !Object.keys(error).length
if (isSuccess) {
resolve(success)
_options.onSuccess?.()
} else {
_options.onError?.(error)
reject(error)
}
}
}
const validateRowKey = _options.validateRowKey
if (filterDataSource.length) {
filterDataSource.forEach((record, index) => {
if (validateRowKey || record[rowKey]) {
validateItem(record, index).then(res => {
success.push(handlePureRecord(res))
end()
}).catch(err => {
error.push(err)
end()
})
} else {
end()
}
})
} else {
resolve(filterDataSource)
}
})
}
const createValidate = () => {
rules.value = collectValidateRules(columns)
schemaInstance = new Schema(rules.value)
}
createValidate()
return {
validate,
validateItem,
errorMap,
rules,
}
}

View File

@ -0,0 +1,5 @@
export { default as EditTable } from './Table.vue'
export { default as EditTableFormItem } from './TableFormItem.vue'
export { default as EditTableGroup } from './group.vue'
export * from './components'

View File

@ -0,0 +1,38 @@
export const bodyProps = () => ({
dataSource: {
type: Array,
default: () => ([])
},
columns: {
type: Array,
default: () => ([])
},
cellHeight: {
type: Number,
default: 50
},
height: {
type: Number,
default: 300
},
style: {
type: Object,
default: () => ({})
},
disableMenu: {
type: Boolean,
default: true
},
rowKey: {
type: String,
default: 'id'
},
openGroup: {
type: Boolean,
default: false
},
rowSelection: {
type: Object,
default: undefined
}
})

View File

@ -0,0 +1,96 @@
import type { ColumnType } from 'ant-design-vue/lib/table'
import { omit} from "lodash-es";
type ColumnsFormType = {
rules: Array<Record<string, any>>,
watch: Array<string>
}
export type ColumnsType = Array<ColumnType & { form?: ColumnsFormType }>
/**
* columns中的rules和watch
* @param columns
*/
export const collectValidateRules = (columns: ColumnsType): Record<string, any> => {
const rules = {}
columns.forEach(item => {
if (item.form) {
if (item.form.rules) {
rules[item.dataIndex as string] = item.form.rules
}
}
})
return rules
}
export const handlePureRecord = (record: Record<string, any>) => {
if (!record) return {}
// if (record.expands) {
// record.expands = omit(record.expands, ['isProduct'])
// }
return omit(record, ['__serial', '__index', '__top', '__selected', '__key', '__dataIndex'])
}
export const handleColumnsWidth = (columns: any[], warpWidth: number): any[] => {
let newColumns = [...columns]
let noWidthLen = 0 // 没有width属性的长度
let hasWidthCount = 0 // 有width属性的合计
let average = 0 // 每个column宽度
let parseAverage = 0 // 取整宽度
let decimalCount = 0 // 收集每个取整后的小数
let lastNoWidthIndex : number | undefined = undefined // 最后一个没有width属性的位置
newColumns.forEach(item => {
if (item.width) {
hasWidthCount += item.width
} else {
noWidthLen += 1
}
})
if (noWidthLen) {
average = (warpWidth - hasWidthCount) / noWidthLen // 剩余平分分配宽度
parseAverage = Math.trunc(average)
decimalCount = (average - parseAverage) * noWidthLen
}
newColumns.forEach((item, index) => {
if (!item.width) {
lastNoWidthIndex = index
}
})
return newColumns.map((item, index) => {
let _width = item.width
let left = 0
if (!item.width) {
_width = parseAverage
}
if (index === lastNoWidthIndex) {
_width = Math.trunc(decimalCount) + parseAverage
}
if (index !== 0) {
left = newColumns[index - 1].width + newColumns[index - 1].left
}
item.width = _width
item.left = left
// prev.push({
// ...next,
// width: _width,
// left
// })
return item
}, [])
}

View File

@ -1,17 +1,33 @@
<template>
<j-form-item name="type" label="读写类型" :rules="[
{
required: true,
message: '请选择读写类型'
}
]">
<j-select
v-model:value="myValue"
mode="multiple"
:options="options"
:disabled="disabled"
placeholder="请选择读写类型"
@change="onChange"
<j-form-item
name="type"
label="读写类型"
:rules="[
{
required: true,
message: '请选择读写类型'
}
]"
style="margin-bottom: 0"
>
<!-- <j-select-->
<!-- v-model:value="myValue"-->
<!-- mode="multiple"-->
<!-- :options="options"-->
<!-- :disabled="disabled"-->
<!-- :dropdownStyle="{-->
<!-- zIndex: 1071-->
<!-- }"-->
<!-- :getPopupContainer="(node) => tableWrapperRef || node"-->
<!-- placeholder="请选择读写类型"-->
<!-- @change="onChange"-->
<!-- />-->
<CheckButton
v-model:value="myValue"
:multiple="true"
:options="options"
:disabled="disabled"
@change="onChange"
/>
</j-form-item>
</template>
@ -19,6 +35,7 @@
<script setup lang="ts" name="ReadType">
import type {PropType} from "vue";
import {useTableWrapper} from "@/components/Metadata/Table/context";
type Emit = {
(e: 'update:value', data: Array<string>): void
@ -43,6 +60,7 @@ const props = defineProps({
const emit = defineEmits<Emit>()
const myValue = ref<Array<string>>([])
const tableWrapperRef = useTableWrapper()
const onChange = (keys: Array<string>) =>{
myValue.value = keys
@ -58,4 +76,4 @@ watch(() => props.value, () => {
<style scoped>
</style>
</style>

View File

View File

@ -0,0 +1,326 @@
<template>
<div ref='dialog' :style='styleName' class='dialog'>
<Transition name='dialog'>
<div class='dialog-sprite' ref='header'>
<div class='header' v-if="title !== false">
<span>{{ title }}</span>
<a-button size='small' type='text' @click.stop='onClose'>
<AIcon type='CloseOutlined' />
</a-button>
</div>
<div class='dialog-body' :style="bodyStyle">
<slot></slot>
</div>
<!-- <div class='dialog-footer' v-if='slots?.footer'>-->
<!-- <slot name='footer'></slot>-->
<!-- </div>-->
</div>
</Transition>
<!-- <template v-for='item in rangeList'>-->
<!-- <div :class="{'range': true, [item.classname]: true}" @mousedown.stop='rangeMove($event,item.classname)'></div>-->
<!-- </template>-->
<div :class="{'range': true, 'bottom-right': true}" @mousedown.stop='rangeMove($event,"bottom-right")'></div>
</div>
</template>
<script setup lang='ts'>
defineOptions({
name: 'DragModal'
})
const props = defineProps({
title: {
type: [String, Boolean],
default: ''
},
width: {
type: Number,
default: 400
},
height: {
type: Number,
default: 100
},
dragRang: {
type: [Array , Number],
default: [400, 200]
},
bodyStyle: {
type: Object,
default: () => ({})
}
})
const emits = defineEmits(['close', 'heightChange'])
const slots = useSlots()
const ele = document.body
const dialog = ref()
const header = ref()
const baseWidth = ref(props.width || 400)
const baseHeight = ref(props.height || 100)
const baseLeft = ref(100)
const baseTop = ref(100)
const rangeList = [
// {
// classname: 'top-left'
// },
// {
// classname: 'top-right'
// },
// {
// classname: 'bottom-left'
// },
{
classname: 'bottom-right'
}
]
const styleName = computed(() => {
return {
top: getFixed(baseTop.value) + 'px',
left: getFixed(baseLeft.value) + 'px',
width: getFixed(baseWidth.value) + 'px',
height: getFixed(baseHeight.value) + 'px'
}
})
const getFixed = (val: number) => {
return Number(val.toFixed(2))
}
const onDrag = () => {
let active = false
let initialX: number
let initialY: number
let initialWindowX: number
let initialWindowY: number
header.value.addEventListener('mousedown', (e: MouseEvent) => {
active = true
initialX = e.clientX
initialY = e.clientY
initialWindowX = dialog.value.offsetLeft
initialWindowY = dialog.value.offsetTop
})
document.addEventListener('mouseup', () => {
active = false
})
document.addEventListener('mousemove', (e) => {
if (active) {
const dx = e.clientX - initialX
const dy = e.clientY - initialY
baseLeft.value = initialWindowX + dx
baseTop.value = initialWindowY + dy
}
})
}
const handleClear = () => {
document.onmouseup = () => {
document.onmousemove = null
document.onmouseup = null
}
}
const rangeMove = (e: MouseEvent, position: string) => {
//
let x: boolean = false
let y: boolean = false
//
let xp: boolean = false
let yp: boolean = false
//
let xc: boolean = false
let yc: boolean = false
let disX = e.clientX
let disY = e.clientY
document.onmousemove = e => {
if (position === 'bottom-right') {
x = true
y = true
} else if (position === 'bottom-left') {
x = true
y = true
xp = true
xc = true
} else if (position === 'top-right') {
x = true
y = true
yp = true
yc = true
} else if (position === 'top-left') {
x = true
y = true
xp = true
xc = true
yp = true
yc = true
}
let left = e.clientX - disX
let top = e.clientY - disY
disX = e.clientX
disY = e.clientY
if (x) {
let calc = left
if (xc) {
calc = -calc
}
if (xp) {
baseLeft.value = baseLeft.value - calc
}
const width = baseWidth.value + calc
baseWidth.value = width <= props.dragRang[0] ? props.dragRang[0] : width
}
if (y) {
let calc = top
if (yc) {
calc = -calc
}
if (yp) {
baseTop.value = baseTop.value - calc
}
const height = baseHeight.value + calc
baseHeight.value = height <= props.dragRang[1] ? props.dragRang[1] : height
emits('heightChange', baseHeight.value)
}
}
handleClear()
}
const onClose = () => {
console.log('close---1')
emits("close")
}
onMounted(() => {
if (dialog.value && header.value) {
onDrag()
}
if (ele) {
const data = ele?.getBoundingClientRect()
baseLeft.value = (data?.right - baseWidth.value) / 2 || 0
baseTop.value = data?.top + 200 || 0
}
})
watch(() => props.height, () => {
if (props.height > baseHeight.value) {
baseHeight.value = props.height
}
})
</script>
<style lang='less' scoped>
@boxColor: rgb(@primary-color);
//
.dialog-enter-active,
.dialog-leave-active {
transition: opacity .5s;
}
.dialog-enter,
.dialog-leave-to {
opacity: 0;
}
.dialog {
position: fixed;
z-index: 1000;
.dialog-sprite {
position: absolute;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
z-index: 23456765435;
background-color: #ffffff;
border-radius: 4px;
border: 1px solid #91CAFF;
box-shadow: 0 3px 8px 0 rgba(#1677FF, 0.24);
.header {
padding: 5px 15px;
font-size: 18px;
font-weight: 700;
color: #333;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #f0f0f0;
cursor: move;
}
.dialog-body {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 24px 20px;
}
.dialog-footer {
border-top: 1px solid #f0f0f0;
padding: 5px 15px;
}
}
.range {
position: absolute;
width: 16px;
height: 16px;
border-radius: 100%;
z-index: 23456765436;
}
.bottom-right, .top-left {
&:hover {
cursor: nwse-resize
}
}
.bottom-left, .top-right {
&:hover {
cursor: nesw-resize
}
}
.top-right {
top: -6px;
right: -6px;
}
.top-left {
top: -6px;
left: -6px;
}
.bottom-right {
bottom: -6px;
right: -6px;
}
.bottom-left {
bottom: -6px;
left: -6px;
}
}
</style>

View File

@ -0,0 +1 @@
export { default as DragModal } from './DragModal.vue'

View File

@ -1,123 +1,204 @@
<template>
<template v-if="isPermission">
<template v-if="popConfirm">
<j-popconfirm :disabled="!isPermission || props.disabled" :overlayStyle='{width: "220px", zIndex: 1075 }' v-bind="popConfirm">
<j-tooltip v-if="tooltip" v-bind="tooltip">
<slot v-if="noButton"></slot>
<j-button v-else v-bind="props" :disabled="_isPermission" :style="props.style">
<template v-if="isPermission">
<template v-if="popConfirm">
<!-- <j-popconfirm
:disabled="!isPermission || props.disabled"
:overlayStyle="{ width: '220px', zIndex: 1075 }"
v-bind="popConfirm"
>
<j-tooltip v-if="tooltip" v-bind="tooltip">
<slot v-if="noButton"></slot>
<j-button
v-else
v-bind="props"
:disabled="_isPermission"
:style="props.style"
>
<slot></slot>
<template #icon>
<slot name="icon"></slot>
</template>
</j-button>
</j-tooltip>
<j-button v-else v-bind="props" :disabled="_isPermission">
<slot></slot>
<template #icon>
<slot name="icon"></slot>
</template>
</j-button>
</j-popconfirm> -->
<!-- <a-modal
v-if="modalVisible"
visible
:closable="false"
@cancel="modalVisible = false"
@ok="modalConfirm"
:confirmLoading="confirmLoading"
:width="300"
centered
:maskClosable="false"
z-index="9999"
>
<div class="modalContent">
{{ popConfirm.title }}
</div>
</a-modal> -->
<j-tooltip v-if="tooltip" v-bind="tooltip">
<slot v-if="noButton"></slot>
<j-button
v-else
v-bind="props"
:disabled="_isPermission"
:style="props.style"
@click="showConfirm"
>
<slot></slot>
<template #icon>
<slot name="icon"></slot>
</template>
</j-button>
</j-tooltip>
<j-button
v-else
v-bind="props"
:disabled="_isPermission"
@click="showConfirm"
>
<slot></slot>
<template #icon>
<slot name="icon"></slot>
</template>
</j-button>
</template>
<template v-else-if="tooltip">
<j-tooltip v-bind="tooltip">
<slot v-if="noButton"></slot>
<j-button
v-else
v-bind="props"
:disabled="_isPermission"
:style="props.style"
>
<slot></slot>
<template #icon>
<slot name="icon"></slot>
</template>
</j-button>
</j-tooltip>
</template>
<template v-else>
<slot v-if="noButton"></slot>
<j-button
v-else
v-bind="props"
:disabled="_isPermission"
:style="props.style"
>
<slot></slot>
<template #icon>
<slot name="icon"></slot>
</template>
</j-button>
</template>
</template>
<j-tooltip v-else title="暂无权限,请联系管理员" :placement="placement">
<slot v-if="noButton"></slot>
<j-button
v-else
v-bind="props"
:disabled="_isPermission"
:style="props.style"
>
<slot></slot>
<template #icon>
<slot name="icon"></slot>
<slot name="icon"></slot>
</template>
</j-button>
</j-tooltip>
<j-button v-else v-bind="props" :disabled="_isPermission" >
<slot></slot>
<template #icon>
<slot name="icon"></slot>
</template>
</j-button>
</j-popconfirm>
</template>
<template v-else-if="tooltip">
<j-tooltip v-bind="tooltip">
<slot v-if="noButton"></slot>
<j-button v-else v-bind="props" :disabled="_isPermission" :style="props.style">
<slot></slot>
<template #icon>
<slot name="icon"></slot>
</template>
</j-button>
</j-tooltip>
</template>
<template v-else>
<slot v-if="noButton"></slot>
<j-button v-else v-bind="props" :disabled="_isPermission" :style="props.style">
<slot></slot>
<template #icon>
<slot name="icon"></slot>
</template>
</j-button>
</template>
</template>
<j-tooltip v-else title="暂无权限,请联系管理员" :placement="placement">
<slot v-if="noButton"></slot>
<j-button v-else v-bind="props" :disabled="_isPermission" :style="props.style">
<slot></slot>
<template #icon>
<slot name="icon"></slot>
</template>
</j-button>
</j-tooltip>
</j-tooltip>
</template>
<script setup lang="ts" name="PermissionButton">
import { CSSProperties, PropType } from 'vue'
import { TooltipProps, PopconfirmProps } from 'ant-design-vue/es'
import { buttonProps } from 'ant-design-vue/es/button/button'
import { CSSProperties, PropType } from 'vue';
import { TooltipProps, PopconfirmProps } from 'ant-design-vue/es';
import { buttonProps } from 'ant-design-vue/es/button/button';
import { usePermissionStore } from '@/store/permission';
import { omit } from 'lodash-es';
import { Modal } from 'ant-design-vue';
// interface PermissionButtonEmits {
// (e: 'click', data: MouseEvent): void;
// }
// const emits = defineEmits<PermissionButtonEmits>()
// interface PermissionButtonProps extends ButtonProps {
// tooltip?: TooltipProps;
// popConfirm?: PopconfirmProps;
// hasPermission?: string | Array<string>;
// noButton?: boolean;
// }
// const props = withDefaults(defineProps<PermissionButtonProps>(), {
// noButton: false,
// })
const props = defineProps({
noButton: {
type: Boolean,
default: () => false
},
tooltip: {
type: Object as PropType<TooltipProps>,
},
popConfirm: {
type: Object as PropType<PopconfirmProps>,
},
hasPermission: {
type: [String , Array, Boolean],
},
style: {
type: Object as PropType<CSSProperties>
},
placement:{
type: String,
default: 'top'
},
...omit(buttonProps(), 'icon')
})
// const { tooltip, popConfirm, hasPermission, noButton, ..._buttonProps } = props;
const permissionStore = usePermissionStore()
noButton: {
type: Boolean,
default: () => false,
},
tooltip: {
type: Object as PropType<TooltipProps>,
},
popConfirm: {
type: Object as PropType<PopconfirmProps>,
},
hasPermission: {
type: [String, Array, Boolean],
},
style: {
type: Object as PropType<CSSProperties>,
},
placement: {
type: String,
default: 'top',
},
...omit(buttonProps(), 'icon'),
});
// const modalVisible = ref(false);
// const confirmLoading = ref(false);
const permissionStore = usePermissionStore();
const isPermission = computed(() => {
if (!props.hasPermission || props.hasPermission === true) {
return true
}
return permissionStore.hasPermission(props.hasPermission)
})
if (!props.hasPermission || props.hasPermission === true) {
return true;
}
return permissionStore.hasPermission(props.hasPermission);
});
const _isPermission = computed(() =>
'hasPermission' in props && isPermission.value
? 'disabled' in props
? props.disabled as boolean
: false
: true
)
'hasPermission' in props && isPermission.value
? 'disabled' in props
? (props.disabled as boolean)
: false
: true,
);
// const conform = (e: MouseEvent) => {
// props.popConfirm?.onConfirm?.(e)
// }
// const modalConfirm = async (e: MouseEvent) => {
// if (typeof props.popConfirm?.onConfirm === 'function') {
// confirmLoading.value = true;
// const res: any = await props.popConfirm?.onConfirm(e)?.finally(()=>{
// confirmLoading.value = false;
// modalVisible.value = false;
// return
// });
// if(!res?.finally){
// confirmLoading.value = false;
// modalVisible.value = false;
// }
// } else {
// modalVisible.value = false;
// }
// };
const showConfirm = () => {
Modal.confirm({
title: props.popConfirm?.title,
content: props.popConfirm?.content,
onOk() {
return props.popConfirm?.onConfirm();
},
onCancel() {},
});
};
</script>
<style scoped lang="less">
// .modalContent {
// text-align: center;
// }
// .control {
// margin-top: 20px;
// display: flex;
// justify-content: space-between;
// }
</style>

View File

@ -75,24 +75,20 @@
>
<j-space>
<span>{{ item.name }}</span>
<j-popconfirm
title="确认删除?"
ok-text="确认"
cancel-text="取消"
@confirm="(e: any) => {
<PermissionButton
type="text"
:popConfirm="{
title: '确认删除?',
onConfirm: (e: any) => {
e?.stopPropagation();
deleteHistory(item.key);
}
"
}"
>
<AIcon
type="DeleteOutlined"
@click="
(e:any) =>
e?.stopPropagation()
"
/>
</j-popconfirm>
</PermissionButton>
</j-space>
</j-menu-item>
</j-menu>
@ -316,12 +312,15 @@ const getHistory = async () => {
* 删除历史分屏
* @param id
*/
const deleteHistory = async (id: string) => {
const res = await deleteSearchHistory(DEFAULT_SAVE_CODE, id);
if (res.success) {
getHistory();
visible.value = false;
}
const deleteHistory = (id: string) => {
const response = deleteSearchHistory(DEFAULT_SAVE_CODE, id);
response.then((res)=>{
if(res.success){
getHistory();
visible.value = false;
}
})
return response
};
/**
@ -458,16 +457,16 @@ defineExpose({
<style lang="less" scoped>
@import './index.less';
:deep(.live-player-stretch-btn){
display: none;
:deep(.live-player-stretch-btn) {
display: none;
}
:deep(.vjs-icon-spinner){
display: none;
:deep(.vjs-icon-spinner) {
display: none;
}
.refreshBtn{
opacity: 0;
.refreshBtn {
opacity: 0;
}
.refreshBtn:hover{
.refreshBtn:hover {
opacity: 1;
}
</style>

View File

@ -1,5 +1,6 @@
.live-player-warp {
display: flex;
height: 100%;
.live-player-content {
display: flex;
@ -25,6 +26,7 @@
position: relative;
display: grid;
box-sizing: border-box;
height: 100%;
&.screen-1 {
grid-template-columns: 1fr;
@ -46,7 +48,18 @@
}
.active {
border: 2px solid red;
position: relative;
&::after {
content: '';
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 99;
border: 2px solid red;
pointer-events: none;
}
}
.full-screen {

View File

@ -1,28 +1,37 @@
<!-- 视频播放 -->
<template>
<LivePlayer
ref="player"
fluent
:protocol="props.protocol || 'mp4'"
:class="props.className"
:loading="props.loading"
:live="'live' in props ? props.live !== false : true"
:autoplay="'autoplay' in props ? props.autoplay !== false : true"
:muted="'muted' in props ? props.muted !== false : true"
:hide-big-play-button="true"
:poster="props.poster || ''"
:timeout="props.timeout || 30"
:video-url="url || ''"
@play="props.onPlay"
@pause="props.onPause"
@ended="props.onEnded"
@error="props.onError"
@timeupdate="props.onTimeUpdate"
/>
<!-- <LivePlayer-->
<!-- ref="player"-->
<!-- fluent-->
<!-- :protocol="props.protocol || 'mp4'"-->
<!-- :class="props.className"-->
<!-- :loading="props.loading"-->
<!-- :live="'live' in props ? props.live !== false : true"-->
<!-- :autoplay="'autoplay' in props ? props.autoplay !== false : true"-->
<!-- :muted="'muted' in props ? props.muted !== false : true"-->
<!-- :hide-big-play-button="true"-->
<!-- :poster="props.poster || ''"-->
<!-- :timeout="props.timeout || 30"-->
<!-- :video-url="url || ''"-->
<!-- @play="props.onPlay"-->
<!-- @pause="props.onPause"-->
<!-- @ended="props.onEnded"-->
<!-- @error="props.onError"-->
<!-- @timeupdate="props.onTimeUpdate"-->
<!-- />-->
<div class="media-player-container" >
<div ref="playerElement">
<span v-if="!props.url">
No Video
</span>
</div>
</div>
</template>
<script setup lang="ts">
import LivePlayer from '@liveqing/liveplayer-v3';
<script setup lang="ts" name="LivePlayer">
import Player, { Events, Sniffer } from 'xgplayer'
import { settingEnum } from './utils'
type PlayerProps = {
url?: string;
@ -35,7 +44,7 @@ type PlayerProps = {
updateTime?: number;
key?: string | number;
loading?: boolean;
protocol?: 'mp4' | 'flv' | 'hls' | 'rtc';
protocol?: 'mp4' | 'flv' | 'm3u8' | 'rtc';
onDestroy?: (e?: any) => void;
onMessage?: (msg: any) => void;
onError?: (err: any) => void;
@ -52,34 +61,115 @@ type PlayerProps = {
const props = defineProps<PlayerProps>();
const player = ref<HTMLVideoElement>();
const url = ref(props.url);
watchEffect(() => {
url.value = props.url;
});
const playerElement = ref<HTMLVideoElement>();
let player: any = null
/**
* 播放
*/
const play = () => {
player.value?.play();
player?.play();
};
/**
* 暂停
*/
const pause = () => {
player.value?.pause();
player?.pause();
};
/**
* 暂停状态
*/
const paused = () => {
return player.value?.paused;
return player?.paused;
};
const destroy = () => {
if (player) {
player.destroy()
player = null
}
}
const init = () => {
destroy()
setTimeout(() => {
console.log(props.protocol)
player = new Player({
el: playerElement.value,
// autoplay: props.autoplay ?? true,
url: props.url,
isLive: props.live,
width: '100%',
height: '100%',
hasStart: false,
playbackRate: false,
ignores: ['progress', 'volume', 'time', 'replay'],
closeVideoClick: true,
closeVideoDblclick: true,
closeVideoTouch: true,
closePlayerBlur: true,
closeControlsBlur: true,
closeFocusVideoFocus: true,
closePlayVideoFocus: true,
...settingEnum[props.protocol || 'mp4']
})
player.on(Events.PLAY, (ev) => {
console.log('-播放开始-', ev);
props.onPlay?.()
})
player.on(Events.PAUSE, (ev) => {
console.log('-播放暂停-', ev);
props.onPause?.()
})
player.on(Events.ENDED, (ev) => {
console.log('-播放结束-', ev);
props.onEnded?.()
})
player.on(Events.TIME_UPDATE, (ev) => {
props.onTimeUpdate?.(ev)
})
player.on(Events.CANPLAY, (ev) => {
console.log('-媒体数据加载好了-', ev);
if (props.autoplay !== false) {
play()
}
})
player.on(Events.SEEKED, (ev) => {
console.log('-跳着播放-', ev);
if (props.live) {
init()
}
})
player.on(Events.ERROR, (ev) => {
console.log('-播放错误-', ev);
if (props.live) {
setTimeout(() => {
init()
}, 5000)
}
props.onError?.(ev)
})
}, 30)
}
watch(() => props.url, () => {
if (props.url) {
nextTick(() => {
init()
})
}
}, { immediate: true })
onBeforeUnmount(() => {
destroy()
})
defineExpose({
play,
pause,
@ -93,4 +183,13 @@ defineExpose({
:deep(.vjs-icon-spinner){
display: none;
}
.media-player-container {
width: 100%;
height: 100%;
background-color: #000;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
}
</style>

View File

@ -0,0 +1,13 @@
import FlvPlugin from 'xgplayer-flv'
import HlsPlugin from "xgplayer-hls"
export const settingEnum = {
mp4: {
isLive: false,
},
flv: {
plugins: [FlvPlugin],
},
m3u8: {
plugins: [HlsPlugin],
}
}

View File

@ -114,15 +114,15 @@ const handleRadio = (item: any) => {
border-radius: 2px;
display: flex;
align-items: center;
cursor: pointer;
.img {
width: 32px;
height: 32px;
}
&.active {
color: #1d39c4;
border-color: #1d39c4;
color: @primary-color;
border-color: @primary-color;
}
}
}
@ -195,8 +195,8 @@ const handleRadio = (item: any) => {
height: 100px;
}
&.active {
color: #1d39c4;
border-color: #1d39c4;
color: @primary-color;
border-color: @primary-color;
}
}

View File

@ -3,11 +3,12 @@
<div class="value-item-warp">
<j-select
v-if="typeMap.get(itemType) === 'select'"
:mode="mode"
v-model:value="myValue"
:options="options"
allowClear
style="width: 100%"
allowClear
:mode="mode"
v-bind="extra"
:options="options"
:getPopupContainer="getPopupContainer"
@change='selectChange'
/>
@ -17,6 +18,7 @@
allowClear
valueFormat="HH:mm:ss"
style="width: 100%"
v-bind="extra"
:getPopupContainer="getPopupContainer"
@change='timeChange'
/>
@ -26,7 +28,8 @@
allowClear
showTime
valueFormat="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
style="width: 100%;"
v-bind="extra"
:getPopupContainer="getPopupContainer"
@change='dateChange'
/>
@ -35,12 +38,14 @@
v-model:value="myValue"
allowClear
style="width: 100%"
v-bind="extra"
@change='inputChange'
/>
<j-input
allowClear
v-else-if="typeMap.get(itemType) === 'object'"
v-model:value="myValue"
v-bind="extra"
@change='inputChange'
>
<template #addonAfter>
@ -50,11 +55,13 @@
<GeoComponent
v-else-if="typeMap.get(itemType) === 'geoPoint'"
v-model:point="myValue"
v-bind="extra"
@change='inputChange'
/>
<j-input
v-else-if="typeMap.get(itemType) === 'file'"
v-model:value="myValue"
v-bind="extra"
placeholder="请输入链接"
allowClear
@change='inputChange'
@ -75,6 +82,7 @@
v-else-if="typeMap.get(itemType) === 'password'"
allowClear
type="password"
v-bind="extra"
v-model:value="myValue"
style="width: 100%"
@change='inputChange'
@ -85,6 +93,7 @@
allowClear
type="text"
v-model:value="myValue"
v-bind="extra"
style="width: 100%"
@change='inputChange'
/>
@ -113,11 +122,10 @@ import { PropType } from 'vue';
import { UploadChangeParam, UploadFile } from 'ant-design-vue';
import { DefaultOptionType } from 'ant-design-vue/lib/select';
import GeoComponent from '@/components/GeoComponent/index.vue';
import { BASE_API_PATH, TOKEN_KEY } from '@/utils/variable';
import { TOKEN_KEY } from '@/utils/variable';
import { LocalStore } from '@/utils/comm';
import { ItemData, ITypes } from './types';
import { FILE_UPLOAD } from '@/api/comm';
import { Upload } from 'jetlinks-ui-components'
type Emits = {
(e: 'update:modelValue', data: string | number | boolean): void;
@ -157,6 +165,10 @@ const props = defineProps({
getPopupContainer: {
type: Function,
default: undefined
},
extra: {
type: Object,
default: () => ({})
}
});
// type Props = {
@ -182,6 +194,7 @@ const componentsType = ref<ITypes>({
object: 'object',
geoPoint: 'geoPoint',
file: 'file',
time: 'time',
});
const typeMap = new Map(Object.entries(componentsType.value));

View File

@ -16,4 +16,6 @@ export type ITypes = {
object: string
geoPoint: string
file: string
}
time: string
}

View File

@ -12,12 +12,16 @@ import { BasicLayoutPage, BlankLayoutPage, FullPage } from './Layout'
import RadioCard from './RadioCard/index.vue'
import { PageContainer, AIcon, Ellipsis } from 'jetlinks-ui-components'
import MarkDown from './Markdown'
import CardSelect from './CardSelect'
// import Ellipsis from './Ellipsis/index.vue'
import JEmpty from './Empty/index.vue'
import AMapComponent from './AMapComponent/index.vue'
import AMapComponent from './AMapComponent/AMap.vue'
import PathSimplifier from './AMapComponent/PathSimplifier.vue'
import ValueItem from './ValueItem/index.vue'
import RowPagination from './RowPagination/index.vue'
import LevelIcon from './AlarmLeveIcon/index.vue'
import CheckButton from "./CheckButton";
import ConfirmModal from './ConfirmModal/index.vue'
export default {
install(app: App) {
@ -43,5 +47,9 @@ export default {
.component('FullPage', FullPage)
.component('RadioCard', RadioCard)
.component('MarkDown', MarkDown)
.component('CardSelect', CardSelect)
.component('CheckButton', CheckButton)
.component('ConfirmModal',ConfirmModal)
.component('LevelIcon',LevelIcon)
}
}

Some files were not shown because too many files have changed in this diff Show More