feat: 2.1

* feat: 优化物模型单个拷贝、新增编辑逻辑

* fix: 修改bug

* fix: 应用管理赋权接口报错

* fix: 修改登录后跳转方式

* fix: 修改物模型的其他配置按照类型配置

* feat: 登录密码加密

* fix: 应用管理赋权接口报错

* feat: 优化物模型-标签去掉详情

* fix: 修改bug

* fix: 修改bug

* fix: 修复权限导入无法编辑问题

* fix: 修复物模型-功能无法编辑问题

* fix: 修复物模型-功能详情

* fix: 修改ui

* fix: 修改bug

* fix: 修复物模型-编辑表格

* fix: 优化物模型-编辑表格-时间、枚举

* fix: 修改bug

* fix: 优化物模型必填校验

* fix: 修改bug

* fix: 修改bug

* fix: 物模型映射搜索

* fix: 优化物模型-详情

* fix: 优化物模型-规则

* fix: 修改bug

* fix: 优化物模型-功能定义

* fix: 修改bug

* fix: 修改bug

* fix: 优化物模型-功能定义(输入、输出)

* fix: 优化物模型-功能定义(输入、输出)

* fix: 修改bug

* fix: 优化物模型-功能定义-输入

* fix: 优化物模型-事件定义

* fix: 修改bug

* fix: 优化物模型-功能定义-输出

* fix: 修改bug

* fix: 修改bug

* fix: 修改bug

* fix: 优化物模型-功能定义-输出

* fix: 优化物模型-功能定义-(输入、输出)

* fix: 修改bug

* fix: 优化物模型-编辑表格标识校验

* fix: 优化物模型-标签定义

* fix: 优化设备物模型

* fix: 修改bug

* fix: allow-scripts

* fix: 优化设备物模型

* fix: 修改bug

* fix: 修改bug

* fix: 修改message

* fix: bug#16085、16024、15967、15924、16072、16070、16067

* fix: 修改bug

* fix: 优化产品物模型编辑状态

* fix: bug#15155

* fix: bug#15531

* fix: bug#16173、16172、16138、15092

* fix: 修改bug

* fix: 去掉cancelSelect

* fix: bug#16110

* fix: 10080

* fix: bug#10080

* fix: 修改bug

* fix: bug#11210

* fix: bug#15718

* fix: 撤销bug#11210修改

* feat: 更新README

* feat: 更改顶部以及侧边菜单配置到store\system.ts

* feat: 新增README 更改配置 内容

* feat: 新增README 新增/编辑菜单

* feat: 新增README 去掉导航栏右上角jetlinks文档

* fix: 修复边缘端映射bug

* fix: 修改登录后跳转方式

* fix: 修改物模型的其他配置按照类型配置

* feat: 登录密码加密

* fix: 应用管理赋权接口报错

* fix: 修复边缘端映射bug

* fix: 修改bug

* fix: bug#16191、16070、16087、15717、15420

* fix: bug#10551

* fix: bug#16097

* fix: 修改bug

* fix: bug#16195

* fix: bug#10750

* fix: bug#11076

* fix: 边缘网关绑定子设备

* fix: bug#11093

* fix: bug#15420、16072、16195、16208、16218、16220、16222

* fix: bug#16077

* fix: bug#16212、16217、16223

* fix: 修改dueros

* fix: bug#16092

* fix: bug#16233

* fix: bug#15649

* fix: bug#15933

* fix: 优化物模型属性-指标值

* fix: bug#15649、16087、16231、16254

* fix: 优化设备物模型属性保存

* fix: 修改bug

* fix: 删除多余showSizeChanger

* fix: 修改bug

* fix: 修改bug

* fix: 修改bug

* fix: bug#16210

* fix: bug#16267

* fix: 修改bug

* fix: bug#16277、16265

* fix: 修复物模型无法新增

* fix: 修改bug

* fix: bug#16275、16087

* fix: 角色管理编辑时新增用户表格增加筛选条件,过滤超级管理员

* fix: bug#16232

* fix: bug#16280

* fix: bug#16312

* fix: 优化视频回放进度无法显示

* fix: bug#16315

* fix: 修改bug

* fix: bug#15871、#16254、#16317

* fix: 优化物模型保存多次提交

* fix: bug#16329、16310、16287、16234、16135

* fix: bug#16316、#16314

* fix: bug#16263

* fix: 根据协议列表展示数据采集菜单

* fix: bug#16333

* fix: bug#16279

* fix: 协议列表提出公共变量

* fix: bug#16337、16338

* fix: bug#16325

* fix: bug

* fix: 修改bug

* fix: bug#16301

* fix: bug#16301

* fix: 修改调试

* fix: bug#16364

* fix: bug#16329

* fix: 修改bug

* fix: 修复物模型属性-规则窗口回显

* fix: 修改bug

* fix: bug#16356

* fix: bug#16279

* fix: 数据采集

* fix: bug#16356

* fix: bug#16365

* fix: bug#16365

* fix: bug#16362

* fix: bug#16358、16366、16374、16375、16379、16385

* fix: 修改bug

* fix: bug#16382

* fix: bug#16386

* fix: bug#16373

* fix: 修复bug

* fix: bug#16322

* fix: 修改bug

* fix: 修改bug

* fix: bug#16355、16323、16349

* fix: 优化物模型重置

* fix: bug#10823

* fix: bug#16404

* fix: 修改bug

* fix: 修改bug

* fix: 修改bug

* fix: bug#16395

* fix: bug#16326

* fix: bug#16382、16381、16384

* fix: 修改bug

* fix: bug#16351

* fix: 修改bug

* fix: 修改bug

* fix: bug#16417

* fix: bug#16423

* fix: bug#16325

* fix: 修改bug

* fix: 修改bug

* fix: bug#16114

* fix: 修复bug

* fix: 修改bug

* fix: bug#16114

* fix: bug#16430

* fix: bug#15968

* fix: bug#16325

* fix: 修改bug

* fix: 修改bug

* fix: bug#16179

* fix: bug#16434

* fix: 修改bug

* feat: 新增异步新建script标签

* fix: bug#16434

* fix: bug#16436

* fix: 修改bug

* fix: 微信登录

* fix: bug#16436

* fix: bug#16441

* fix: 修复TSL不展示虚拟属性

* fix: 修改bug

* fix: bug#16436

* fix: bug#16436

* fix: 修改微信

* fix: 修复登录页动态显示可视化
This commit is contained in:
XieYongHong 2023-07-17 09:22:49 +08:00 committed by GitHub
parent f7a29fcbb4
commit 4561e03e96
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
408 changed files with 20039 additions and 5921 deletions

210
README.md
View File

@ -37,3 +37,213 @@ yarn dev:force
* 项目在开发模式下,首页加载慢属于正常现象; * 项目在开发模式下,首页加载慢属于正常现象;
* 打开F12后页面卡顿是`vuetools`引起,[https://github.com/vuejs/devtools/issues/1987](https://github.com/vuejs/devtools/issues/1987) * 打开F12后页面卡顿是`vuetools`引起,[https://github.com/vuejs/devtools/issues/1987](https://github.com/vuejs/devtools/issues/1987)
## 更改配置
### 默认图标以及系统名称
#### 1.基础配置
首先启动项目,找到顶部菜单的 系统管理 -> 基础配置
此处可以更改系统名称、主题色、系统logo、浏览器页签等
#### 2.默认配置
在代码根目录找到`config\config.ts`文件
> 默认图标以及系统名称优先使用基础配置的数据!
```javascript
export default {
...
logo: '/favicon.ico', // 浏览器标签页logo(不要修改如需修改默认图标请在根目录public\favicon.ico替换此文件)
title: 'Jetlinks', // 浏览器标签页title(刷新状态时浏览器标签页名称)
layout: {
title: '物联网平台', // 平台title(默认配置不生效,优先使用基础配置的数据)
logo: '/logo.png', // 平台logo(不要修改如需修改默认logo请在根目录public\logo.png替换此文件)
...
}
}
```
### 去掉或修改备案信息
#### 修改备案信息
在`src\views\user\Login\index.vue`文件
在第16行左右修改以下代码`备案: xxx(自己的备案信息)`
```javascript
<a
href="https://beian.miit.gov.cn/#/Integrated/index"
target="_blank"
rel="noopener noreferrer"
class="records"
>
备案: xxx(自己的备案信息)
</a>
```
### 去掉导航栏右上角jetlinks文档
在`src\components\Layout\BasicLayoutPage.vue`文件
在第23行左右注释以下代码
```javascript
<!-- <AIcon type="QuestionCircleOutlined" @click="toDoc" /> -->
```
### 新增菜单
新增或者修改菜单有两种方式,第一个是代码内的初始化菜单,第二个系统管理的菜单管理
* 初始化菜单
初始化菜单是默认的菜单,在进行系统初始化会使用到。
> 在进行菜单初始化时,如果只在菜单管理新增或修改,但是没有在初始化菜单里新增或者修改,则只会保留初始化菜单!
* 菜单管理
菜单管理是 系统管理 -> 菜单管理 的菜单,可以动态修改,新增或者更改
> 如果需要系统初始化时不丢失,请在`src\views\init-home\data\baseMenu.ts`文件下新增或者修改初始化菜单
**新增或者修改菜单之前,确保`src\views`文件夹下有对应的文件夹以及index.vue入口文件**
#### 1.菜单管理
新建文件夹以及文件`src\views\test\Home\index.vue`
##### 新增顶部菜单 test菜单
1. 启动项目,找到顶部菜单的 系统管理 -> 菜单管理,点击菜单配置旁边的新增按钮。
2. 完成菜单图标、名称、编码、页面地址、权限配置等
> 编码是唯一的,必须和文件路径一致此处是顶级菜单编码填入: test
> 页面地址建议和文件路径保持一致: /test
3. 点击保存,刷新页面后生效
4. 按钮权限 顶级菜单没有页面可以不用添加按钮权限
##### 新增子菜单
1. 在菜单管理 test菜单 新增子菜单
2. 完成菜单图标、名称、编码、页面地址、权限配置等
> 编码是唯一的必须和文件路径一致此处是test菜单下的二级菜单编码填入: test/Home
> 页面地址建议和文件路径保持一致: /test/Home
4. 点击保存,刷新页面后生效
5. 按钮权限 如果有权限控制可以添加对应权限
#### 2.初始化菜单
建议在菜单管理新增或修改菜单之后,再来新增或修改初始化菜单
##### 新增顶部菜单test以及子菜单
**确定有对应的文件夹以及文件`src\views\test\Home\index.vue`**
1. 启动项目,找到顶部菜单的 系统管理 -> 菜单管理
打开控制台(F12),选中网络(Network),点击菜单管理的搜索或者重置,直到有 tree 的接口请求。
点击接口请求 tree , 并选中响应或者预览选项找到刚刚新增code为test的数据复制test菜单的数据
2. 在`src\views\init-home\data\baseMenu.ts`文件中添加配置
把第一步test菜单的数据复制到对应位置即可
```javascript
export default [
// 物联网
...
// 视频中心
...
// 系统管理
...
// 物联卡
...
// test菜单
{
"id": "eb2858ec8dc6d12645a19ee0ed6aba36",
"parentId": "",
"path": "FwY9",
"sortIndex": 5,
"level": 1,
"owner": "iot",
"name": "test菜单",
"code": "test",
"describe": "",
"url": "/test",
"icon": "StepForwardOutlined",
"status": 1,
"permissions": [],
"accessSupport": {
"text": "不支持",
"value": "unsupported"
},
"indirectMenus": [],
"children": [
{
"id": "1995dcd016aaad7c5515f8ead14ca617",
"parentId": "eb2858ec8dc6d12645a19ee0ed6aba36",
"path": "FwY9-T6lF",
"sortIndex": 1,
"level": 2,
"owner": "iot",
"name": "首页",
"code": "test/Home",
"describe": "",
"url": "/test/Home",
"icon": "HeatMapOutlined",
"status": 1,
"permissions": [],
"accessSupport": {
"text": "不支持",
"value": "unsupported"
},
"indirectMenus": [],
"buttons": [
{
"id": "add",
"name": "新增",
"permissions": [
{
"permission": "role",
"actions": [
"query",
"save",
"delete"
]
}
]
}
],
"creatorId": "1199596756811550720",
"createTime": 1688032521555,
"supportDataAccess": false
}
],
"creatorId": "1199596756811550720",
"createTime": 1688032467222,
"supportDataAccess": false
}
]
```
> 新增初始化菜单之后需要进行系统初始化才能生效
##### 修改初始化菜单
同上,在菜单管理修改对应的数据,复制对应的菜单数据,然后替换掉`baseMenu.ts`对应数据即可。
> 修改初始化菜单之后需要进行菜单初始化才能生效
##### 系统初始化
在浏览器顶部修改页面路径,`/#/`后边输入`init-home`,回车进入系统初始化页面
例如: `http://localhost:5174/#/init-home`
填写好基本信息,角色初始化等,点击确定

View File

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

View File

@ -7,9 +7,6 @@ export default {
layout: { layout: {
title: '物联网平台', // 平台title title: '物联网平台', // 平台title
logo: '/logo.png', // 平台logo logo: '/logo.png', // 平台logo
siderWidth: 208, // 左侧菜单栏宽度
headerHeight: 48, // 头部高度
collapsedWidth: 48,
mode: 'inline', mode: 'inline',
theme: 'light', // 'dark' 'light' theme: 'light', // 'dark' 'light'
} }

View File

@ -25,8 +25,9 @@
"event-source-polyfill": "^1.0.31", "event-source-polyfill": "^1.0.31",
"global": "^4.4.0", "global": "^4.4.0",
"jetlinks-store": "^0.0.3", "jetlinks-store": "^0.0.3",
"jetlinks-ui-components": "^1.0.21", "jetlinks-ui-components": "^1.0.24",
"js-cookie": "^3.0.1", "js-cookie": "^3.0.1",
"jsencrypt": "^3.3.2",
"less": "^4.1.3", "less": "^4.1.3",
"less-loader": "^11.1.0", "less-loader": "^11.1.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
@ -41,6 +42,7 @@
"v-clipboard3": "^0.1.4", "v-clipboard3": "^0.1.4",
"vite-plugin-monaco-editor": "^1.1.0", "vite-plugin-monaco-editor": "^1.1.0",
"vue": "^3.2.45", "vue": "^3.2.45",
"vue-cropper": "^1.0.9",
"vue-json-viewer": "^3.0.4", "vue-json-viewer": "^3.0.4",
"vue-router": "^4.1.6", "vue-router": "^4.1.6",
"vue3-json-viewer": "^2.2.2", "vue3-json-viewer": "^2.2.2",

View File

@ -136,7 +136,7 @@ const matchComponents: IMatcher[] = [
}, },
{ {
pattern: /^Select/, pattern: /^Select|^SelectBoolean/,
styleDir: 'Select' styleDir: 'Select'
}, },
{ {
@ -204,6 +204,10 @@ const matchComponents: IMatcher[] = [
pattern: /^Empty/, pattern: /^Empty/,
styleDir: 'Empty' styleDir: 'Empty'
}, },
{
pattern: /^PopconfirmModal/,
styleDir: 'PopconfirmModal'
},
{ {
pattern: /^Popconfirm/, pattern: /^Popconfirm/,
styleDir: 'Popconfirm' styleDir: 'Popconfirm'
@ -215,7 +219,15 @@ const matchComponents: IMatcher[] = [
{ {
pattern: /^Notification/, pattern: /^Notification/,
styleDir: 'Notification' styleDir: 'Notification'
} },
{
pattern: /^DataTable/,
styleDir: 'DataTable'
},
{
pattern: /^CheckButton/,
styleDir: 'CheckButton'
},
] ]
export interface JetlinksVueResolverOptions { export interface JetlinksVueResolverOptions {
@ -294,7 +306,19 @@ function getSideEffects(compName: string, options: JetlinksVueResolverOptions, _
} }
const filterName = ['message', 'Notification'] const filterName = ['message', 'Notification']
const primitiveNames = ['AIcon','Affix', 'Anchor', 'AnchorLink', 'message', 'Notification', 'AutoComplete', 'AutoCompleteOptGroup', 'AutoCompleteOption', 'Alert', 'Avatar', 'AvatarGroup', 'BackTop', 'Badge', 'BadgeRibbon', 'Breadcrumb', 'BreadcrumbItem', 'BreadcrumbSeparator', 'Button', 'ButtonGroup', 'Calendar', 'Card', 'CardGrid', 'CardMeta', 'Collapse', 'CollapsePanel', 'Carousel', 'Cascader', 'Checkbox', 'CheckboxGroup', 'Col', 'Comment', 'ConfigProvider', 'DatePicker', 'MonthPicker', 'WeekPicker', 'RangePicker', 'QuarterPicker', 'Descriptions', 'DescriptionsItem', 'Divider', 'Dropdown', 'DropdownButton', 'Drawer', 'Empty', 'Form', 'FormItem', 'FormItemRest', 'Grid', 'Input', 'InputGroup', 'InputPassword', 'InputSearch', 'Textarea', 'Image', 'ImagePreviewGroup', 'InputNumber', 'Layout', 'LayoutHeader', 'LayoutSider', 'LayoutFooter', 'LayoutContent', 'List', 'ListItem', 'ListItemMeta', 'Menu', 'MenuDivider', 'MenuItem', 'MenuItemGroup', 'SubMenu', 'Mentions', 'MentionsOption', 'Modal', 'Statistic', 'StatisticCountdown', 'PageHeader', 'Pagination', 'Popconfirm', 'Popover', 'Progress', 'Radio', 'RadioButton', 'RadioGroup', 'Rate', 'Result', 'Row', 'Select', 'SelectOptGroup', 'SelectOption', 'Skeleton', 'SkeletonButton', 'SkeletonAvatar', 'SkeletonInput', 'SkeletonImage', 'Slider', 'Space', 'Spin', 'Steps', 'Step', 'Switch', 'Table', 'TableColumn', 'TableColumnGroup', 'TableSummary', 'TableSummaryRow', 'TableSummaryCell', 'Transfer', 'Tree', 'TreeNode', 'DirectoryTree', 'TreeSelect', 'TreeSelectNode', 'Tabs', 'TabPane', 'Tag', 'CheckableTag', 'TimePicker', 'TimeRangePicker', 'Timeline', 'TimelineItem', 'Tooltip', 'Typography', 'TypographyLink', 'TypographyParagraph', 'TypographyText', 'TypographyTitle', 'Upload', 'UploadDragger', 'LocaleProvider', 'ProTable', 'Search', 'AdvancedSearch', 'Ellipsis', 'MonacoEditor', 'ProLayout', 'ScrollTable', 'TableCard', 'Scrollbar', 'CardSelect', 'ColorPicker'] const primitiveNames = ['AIcon','Affix', 'Anchor', 'AnchorLink', 'message', 'Notification', 'AutoComplete', 'AutoCompleteOptGroup', 'AutoCompleteOption', 'Alert', 'Avatar', 'AvatarGroup', 'BackTop', 'Badge', 'BadgeRibbon', 'Breadcrumb', 'BreadcrumbItem', 'BreadcrumbSeparator', 'Button', 'ButtonGroup', 'Calendar', 'Card', 'CardGrid', 'CardMeta', 'Collapse', 'CollapsePanel', 'Carousel', 'Cascader', 'Checkbox', 'CheckboxGroup', 'Col', 'Comment', 'ConfigProvider', 'DatePicker', 'MonthPicker', 'WeekPicker', 'RangePicker', 'QuarterPicker', 'Descriptions', 'DescriptionsItem', 'Divider', 'Dropdown', 'DropdownButton', 'Drawer', 'Empty', 'Form', 'FormItem', 'FormItemRest', 'Grid', 'Input', 'InputGroup', 'InputPassword', 'InputSearch', 'Textarea', 'Image', 'ImagePreviewGroup', 'InputNumber', 'Layout', 'LayoutHeader', 'LayoutSider', 'LayoutFooter', 'LayoutContent', 'List', 'ListItem', 'ListItemMeta', 'Menu', 'MenuDivider', 'MenuItem', 'MenuItemGroup', 'SubMenu', 'Mentions', 'MentionsOption', 'Modal', 'Statistic', 'StatisticCountdown', 'PageHeader', 'Pagination', 'Popconfirm', 'Popover', 'Progress', 'Radio', 'RadioButton', 'RadioGroup', 'Rate', 'Result', 'Row', 'Select', 'SelectOptGroup', 'SelectOption', 'SelectBoolean', 'Skeleton', 'SkeletonButton', 'SkeletonAvatar', 'SkeletonInput', 'SkeletonImage', 'Slider', 'Space', 'Spin', 'Steps', 'Step', 'Switch', 'Table', 'TableColumn', 'TableColumnGroup', 'TableSummary', 'TableSummaryRow', 'TableSummaryCell', 'Transfer', 'Tree', 'TreeNode', 'DirectoryTree', 'TreeSelect', 'TreeSelectNode', 'Tabs', 'TabPane', 'Tag', 'CheckableTag', 'TimePicker', 'TimeRangePicker', 'Timeline', 'TimelineItem', 'Tooltip', 'Typography', 'TypographyLink', 'TypographyParagraph', 'TypographyText', 'TypographyTitle', 'Upload', 'UploadDragger', 'LocaleProvider', 'ProTable', 'Search', 'AdvancedSearch', 'Ellipsis', 'MonacoEditor', 'ProLayout', 'ScrollTable', 'TableCard', 'Scrollbar', 'CardSelect', 'ColorPicker', 'PopconfirmModal', 'DataTable',
'DataTableArray',
'DataTableString',
'DataTableInteger',
'DataTableDouble',
'DataTableBoolean',
'DataTableEnum',
'DataTableFile',
'DataTableDate',
'DataTableTypeSelect',
'DataTableObject',
'CheckButton',
]
const prefix = 'J' const prefix = 'J'
let jetlinksNames: Set<string> let jetlinksNames: Set<string>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 37 KiB

BIN
public/images/apply/6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 356 B

After

Width:  |  Height:  |  Size: 898 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1015 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -30,3 +30,8 @@ export const checkOldPassword_api = (password:string) => server.post(`/user/me/p
'Content-Type': 'text/plain' 'Content-Type': 'text/plain'
} }
}); });
// 我的订阅
// 查询当前用户可访问的通道配置
export const getAllNotice = () => server.get(`/notify/channel/all`);

View File

@ -1,24 +1,29 @@
import server from '@/utils/request' import server from '@/utils/request'
// 获取记录列表 // 获取记录列表
export const getList_api = (data: object): any => server.post(`/notifications/_query`, data) export const getList_api = (data: any): any => server.post(`/notifications/_query`, data)
// 获取未读记录列表 // 获取未读记录列表
export const getListByUnRead_api = (data: object): any => server.post(`/notifications/_query`, data) // export const getListByUnRead_api = (data: any): any => server.post(`/notifications/_query`, data)
// 修改记录状态 // 修改记录状态
export const changeStatus_api = (type: '_read' | '_unread', data: string[]): any => server.post(`/notifications/${type}`, data) export const changeStatus_api = (type: '_read' | '_unread', data: string[]): any => server.post(`/notifications/${type}`, data)
export const changeAllStatus = (type: '_read' | '_unread', data: string[]): any => server.post(`/notifications/${type}/provider`, data)
const encodeParams = (params: Record<string, any>) => {
let result = {}
for (const key in params) {
if (Object.prototype.hasOwnProperty.call(params, key)) {
const value = params[key];
if (key === 'terms') {
result['terms[0].column:'] = 0
result['terms[0].value'] = JSON.stringify(value[0])
} else result[key] = value
}
}
return result // 查询告警记录详情
}; export const getDetail = (id: string): any => server.get(`/alarm/record/${id}`)
// const encodeParams = (params: Record<string, any>) => {
// let result = {}
// for (const key in params) {
// if (Object.prototype.hasOwnProperty.call(params, key)) {
// const value = params[key];
// if (key === 'terms') {
// result['terms[0].column:'] = 0
// result['terms[0].value'] = JSON.stringify(value[0])
// } else result[key] = value
// }
// }
// return result
// };

View File

@ -20,3 +20,22 @@ export const getAlarmList_api = () => server.post(`/alarm/config/_query/no-pagin
sorts: [{ name: 'createTime', order: 'desc' }], sorts: [{ name: 'createTime', order: 'desc' }],
paging: false, paging: false,
}); });
// 判断获取当前用户绑定信
export const getIsBindThird = () => server.get(`/user/third-party/me`);
// 生成OAuth2授权URL
export const getWechatOAuth2 = (configId: string, templateId: string, url: string) => server.get(`/notifier/wechat/corp/${configId}/${templateId}/oauth2/binding-user-url?redirectUri=${url}`);
export const getDingTalkOAuth2 = (configId: string, url: string) => server.get(`/notifier/dingtalk/corp/${configId}/oauth2/binding-user-url?authCode=${url}`);
// 获取oauth2授权的用户绑定码
export const getUserBind = (type: 'wechat' | 'dingtalk', params: any) => server.get(`/notifier/${type}/corp/oauth2/user-bind-code`, params);
// 根据绑定码绑定当前用户
export const bindThirdParty = (type: string, provider: string, bindCode: string) => server.post(`/user/third-party/me/${type}/${provider}/${bindCode}/_bind`);

View File

@ -35,3 +35,5 @@ export const systemVersion = () => server.get<{edition?: string}>('/system/versi
* @returns * @returns
*/ */
export const queryDashboard = (data: Record<string, any>) => server.post(`/dashboard/_multi`, data) export const queryDashboard = (data: Record<string, any>) => server.post(`/dashboard/_multi`, data)
export const fileUpload = (data: any) => server.post('/file/static', data)

View File

@ -99,7 +99,7 @@ export const templateDownload = (productId: string, type: string) => server.get(
* @param type * @param type
* @returns * @returns
*/ */
export const deviceImport = (productId: string, fileUrl: string, autoDeploy: boolean) => `${BASE_API_PATH}/device-instance/${productId}/import?fileUrl=${fileUrl}&autoDeploy=${autoDeploy}&:X_Access_Token=${LocalStore.get(TOKEN_KEY)}` 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)}`
/** /**
* *
@ -252,6 +252,17 @@ export const unbindBatchDevice = (deviceId: string, data: Record<string, any>) =
*/ */
export const bindDevice = (deviceId: string, data: Record<string, any>) => server.post(`/device/gateway/${deviceId}/bind`, data) export const bindDevice = (deviceId: string, data: Record<string, any>) => server.post(`/device/gateway/${deviceId}/bind`, data)
/**
*
*/
export const queryDeviceMapping = (deviceId: string, data?: any) => server.post(`/edge/operations/${deviceId}/device-mapping-list/invoke`, data)
/**
*
*/
export const saveDeviceMapping = (deviceId: string, data: any) => server.post(`/edge/operations/${deviceId}/device-mapping-save-batch/invoke`, data)
/** /**
* *
* @param data * @param data
@ -576,14 +587,23 @@ export const getDeviceNumber = (data?:any) => server.post<number>('/device-insta
/** /**
* *
* @param productId * @param productId
* @param data * @param data/
*/ */
export const importDeviceByPlugin = (productId: string, data: any[]) => server.post(`/device/instance/plugin/${productId}/import`, data) export const importDeviceByPlugin = (productId: string, data: any[]) => server.post(`/device/instance/plugin/${productId}/import`, data)
export const metadateMapById = (productId: string, data: ant[]) => server.patch(`/device/metadata/mapping/product/${productId}`, data) export const metadataMapById = (type: 'device' | 'product', productId: string, data: any[]) => server.patch(`/device/metadata/mapping/${type}/${productId}`, data)
export const getMetadateMapById = (productId: string) => server.get(`/device/metadata/mapping/product/${productId}`) export const getMetadataMapById = (type: 'device' | 'product', productId: string) => server.get(`/device/metadata/mapping/${type}/${productId}`)
export const getInkingDevices = (data: string[]) => server.post('/plugin/mapping/device/_all', data) export const getInkingDevices = (data: string[]) => server.post('/plugin/mapping/device/_all', data)
export const getProtocolMetadata = (id: string, transport: string) => server.get(`/protocol/${id}/${transport}/metadata`)
/**
*
*/
export const saveDeviceVirtualProperty = (productId: string, deviceId: string, data: any[]) => server.patch(`/virtual/property/product/${productId}/${deviceId}/_batch`, data)
export const queryDeviceVirtualProperty = (productId: string, deviceId: string, propertyId: string) => server.get(`/virtual/property/device/${productId}/${deviceId}/${propertyId}`)
export const queryByParent = (deviceId: string) => server.get(`/device/gateway/${deviceId}/parent`)

View File

@ -212,3 +212,12 @@ export const getMetadataDeviceConfig = (params: {
}; };
}) => server.get<Record<any, any>[]>(`/device/instance/${params.deviceId}/config-metadata/${params.metadata.type}/${params.metadata.id}/${params.metadata.dataType}`) }) => server.get<Record<any, any>[]>(`/device/instance/${params.deviceId}/config-metadata/${params.metadata.type}/${params.metadata.id}/${params.metadata.dataType}`)
/**
*
*/
export const saveProductVirtualProperty = (productId: string, data: any[]) => server.patch(`/virtual/property/product/${productId}/_batch`, data)
export const queryProductVirtualProperty = (productId: string, propertyId: string) => server.get(`/virtual/property/product/${productId}/${propertyId}`)

View File

@ -61,3 +61,10 @@ export const loginout_api = () => server.get<any>('/user-token/reset')
export const getOAuth2 = (params: any) => server.get<any>('/oauth2/authorize', params) export const getOAuth2 = (params: any) => server.get<any>('/oauth2/authorize', params)
export const initApplication = (clientId: string | number) => server.get<{name: string}>(`/application/${clientId}/info`) export const initApplication = (clientId: string | number) => server.get<{name: string}>(`/application/${clientId}/info`)
/**
*
* @returns
*/
export const authLoginConfig = () => server.get(`/authorize/login/configs`)

View File

@ -43,7 +43,7 @@ export default {
server.post<recordsItemType[]>(`/media/device/${deviceId}/${channelId}/records/in-server/files`, data), server.post<recordsItemType[]>(`/media/device/${deviceId}/${channelId}/records/in-server/files`, data),
// 播放云端回放 // 播放云端回放
playbackStart: (recordId: string) => `${BASE_API_PATH}/record/${recordId}.mp4?:X_Access_Token=${LocalStore.get(TOKEN_KEY)}`, playbackStart: (recordId: string) => `${BASE_API_PATH}/media/record/${recordId}.mp4?:X_Access_Token=${LocalStore.get(TOKEN_KEY)}`,
downLoadFile: (recordId: string) => `${BASE_API_PATH}/record/${recordId}.mp4?download=true&:X_Access_Token=${LocalStore.get(TOKEN_KEY)}` downLoadFile: (recordId: string) => `${BASE_API_PATH}/media/record/${recordId}.mp4?download=true&:X_Access_Token=${LocalStore.get(TOKEN_KEY)}`
} }

View File

@ -25,4 +25,5 @@ export default {
// 短信获取签名 // 短信获取签名
getSigns: (id: any) => get(`/notifier/sms/aliyun/${id}/signs`), getSigns: (id: any) => get(`/notifier/sms/aliyun/${id}/signs`),
getListByConfigId: (id: string, data: any): any => post(`/notifier/template/${id}/_query`, data), getListByConfigId: (id: string, data: any): any => post(`/notifier/template/${id}/_query`, data),
getListVariableByConfigId: (id: string, data?: any): any => post(`/notifier/template/${id}/detail/_query`, data),
} }

View File

@ -8,7 +8,7 @@ export const changeApplyStatus_api = (id: string, data: any) => server.put(`/app
// 删除应用 // 删除应用
export const delApply_api = (id: string) => server.remove(`/application/${id}`) export const delApply_api = (id: string) => server.remove(`/application/${id}`)
export const queryType = () => server.get(`/application/providers`)
// 获取组织列表 // 获取组织列表
export const getDepartmentList_api = (params: any) => server.get(`/organization/_all/tree`, params); export const getDepartmentList_api = (params: any) => server.get(`/organization/_all/tree`, params);

View File

@ -5,7 +5,7 @@ export const getTreeData_api = (data: object) => server.post(`/organization/_all
// 新增部门 // 新增部门
export const addDepartment_api = (data: object) => server.post(`/organization`, data); export const addDepartment_api = (data: object) => server.post(`/organization`, data);
// 更新部门 // 更新部门
export const updateDepartment_api = (data: object) => server.patch(`/organization`, data); export const updateDepartment_api = (data: any) => server.put(`/organization/${data.id}`, data);
// 删除部门 // 删除部门
export const delDepartment_api = (id: string) => server.remove(`/organization/${id}`); export const delDepartment_api = (id: string) => server.remove(`/organization/${id}`);

View File

@ -0,0 +1,26 @@
import server from '@/utils/request';
// 获取角色列表
export const queryRoleList = (data: any): Promise<any> => server.post(`/role/_query/`, data);
// 查询所有通道配置
export const queryChannelConfig = (): Promise<any> => server.get(`/notify/channel/all-for-save`);
// 查询通知通道类型
export const queryChannelProviders = (): Promise<any> => server.get(`/notify/channel/providers`);
// 保存通道配置
export const saveChannelConfig = (data: any[]): Promise<any> => server.patch(`/notify/channel`, data);
export const updateChannelConfig = (providerId: string, data: any[]): Promise<any> => server.patch(`/notify/channel/${providerId}`, data);
export const editChannelConfig = (providerId: string, data: any): Promise<any> => server.put(`/notify/channel/${providerId}`, data);
export const actionChannelConfig = (providerId: string, type: 'enable' | 'disable'): Promise<any> => server.post(`/notify/channel/${providerId}/${type}`);
export const deleteChannelConfig = (providerId: string): Promise<any> => server.remove(`/notify/channel/${providerId}`);
export const queryConfigVariables = (providerId: string): Promise<any> => server.get(`/notify/channel/${providerId}/variables`);

View File

@ -79,6 +79,12 @@ export default defineComponent({
this.PathNavigatorRef?.moveToPoint(0, 0); this.PathNavigatorRef?.moveToPoint(0, 0);
this.PathNavigatorRef?.stop(); this.PathNavigatorRef?.stop();
}, },
pause() {
this.PathNavigatorRef?.pause()
},
resume() {
this.PathNavigatorRef?.resume()
}
}, },
watch: { watch: {
pathData: { pathData: {
@ -101,6 +107,6 @@ export default defineComponent({
deep: true, deep: true,
}, },
}, },
expose: ['start', 'stop'] expose: ['start', 'stop', 'pause', 'resume']
}); });
</script> </script>

View File

@ -2,7 +2,7 @@
<div class="card j-table-card-box"> <div class="card j-table-card-box">
<div <div
class="card-warp" class="card-warp"
:class="{ active: active ? 'active' : '' }" :class="{ active: active ? 'active' : '', 'disabled': disabled }"
@click="handleClick" @click="handleClick"
> >
<div class="card-type" v-if="slots.type"> <div class="card-type" v-if="slots.type">
@ -140,6 +140,10 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
disabled: {
type: Boolean,
default: false,
}
}); });
const getBackgroundColor = (code: string | number) => { const getBackgroundColor = (code: string | number) => {
@ -160,6 +164,7 @@ const handleClick = () => {
.card { .card {
width: 100%; width: 100%;
background-color: #fff; background-color: #fff;
.checked-icon { .checked-icon {
position: absolute; position: absolute;
right: -22px; right: -22px;
@ -190,16 +195,20 @@ const handleClick = () => {
position: relative; position: relative;
border: 1px solid #e6e6e6; border: 1px solid #e6e6e6;
overflow: hidden; overflow: hidden;
cursor: pointer;
&:hover { &:hover {
cursor: pointer;
box-shadow: 0 0 24px rgba(#000, 0.1); box-shadow: 0 0 24px rgba(#000, 0.1);
.card-mask { .card-mask {
visibility: visible; visibility: visible;
} }
} }
&.disabled {
filter: grayscale(100%);
cursor: not-allowed;
}
&.active { &.active {
position: relative; position: relative;
border: 1px solid #2f54eb; border: 1px solid #2f54eb;

View File

@ -0,0 +1,303 @@
<template>
<div class="debug-container">
<div class="left">
<div class="header">
<div>
<div class="title">
属性赋值
<div class="description">请对上方规则使用的属性进行赋值</div>
</div>
<div v-if="!isBeginning && virtualRule?.type === 'window'" class="action" @click="runScriptAgain">
<a style="margin-left: 75px;">发送数据</a>
</div>
</div>
</div>
<j-table :columns="columns" :data-source="property" :pagination="false" bordered size="small">
<template #bodyCell="{ column, record, index }">
<template v-if="column.key === 'id'">
<j-auto-complete :options="options" v-model:value="record.id" size="small" style="width: 130px" />
</template>
<template v-if="column.key === 'current'">
<j-input v-model:value="record.current" size="small"></j-input>
</template>
<template v-if="column.key === 'last'">
<j-input v-model:value="record.last" size="small"></j-input>
</template>
<template v-if="column.key === 'action'">
<AIcon type="DeleteOutlined" @click="deleteItem(index)" />
</template>
</template>
</j-table>
<j-button type="dashed" block style="margin-top: 5px" @click="addItem">
<template #icon>
<AIcon type="PlusOutlined" />
</template>
添加条目
</j-button>
</div>
<div class="right">
<div class="header">
<div class="title">
<div>运行结果</div>
</div>
<div class="action">
<div>
<a v-if="isBeginning" @click="beginAction">
开始运行
</a>
<a v-else @click="stopAction">
停止运行
</a>
</div>
<div>
<a @click="clearAction">
清空
</a>
</div>
</div>
</div>
<div class="log">
<j-descriptions>
<j-descriptions-item v-for="item in ruleEditorStore.state.log" :label="moment(item.time).format('HH:mm:ss')"
:key="item.time" :span="3">
<j-tooltip placement="top" :title="item.content">
{{ item.content }}
</j-tooltip>
</j-descriptions-item>
</j-descriptions>
</div>
</div>
</div>
</template>
<script setup lang="ts" name="Debug">
import { PropType } from 'vue';
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons-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 { onlyMessage } from '@/utils/comm';
const props = defineProps({
virtualRule: Object as PropType<Record<any, any>>,
id: String,
})
const isBeginning = ref(true)
type propertyType = {
id?: string,
current?: string,
last?: string
}
const property = ref<propertyType[]>([])
const columns = [{
title: '属性ID',
dataIndex: 'id',
key: 'id'
}, {
title: '当前值',
dataIndex: 'current',
key: 'current'
}, {
title: '上一值',
dataIndex: 'last',
key: 'last'
}, {
title: '',
key: 'action'
}]
const addItem = () => {
property.value.push({})
}
const deleteItem = (index: number) => {
property.value.splice(index, 1)
}
const ws = ref()
const virtualIdRef = ref(new Date().getTime());
const productStore = useProductStore()
const ruleEditorStore = useRuleEditorStore()
const runScript = () => {
const metadata = productStore.current.metadata || '{}';
const propertiesList = JSON.parse(metadata).properties || [];
const _properties = property.value.map((item: any) => {
const _item = propertiesList.find((i: any) => i.id === item.id);
return { ...item, type: _item?.valueType?.type };
});
if (ws.value) {
ws.value.unsubscribe?.();
}
if (!props.virtualRule?.script) {
isBeginning.value = true;
onlyMessage('请编辑规则', 'warning');
return;
}
ws.value = getWebSocket(`virtual-property-debug-${props.id}-${new Date().getTime()}`,
'/virtual-property-debug',
{
virtualId: `${virtualIdRef.value}-virtual-id`,
property: props.id,
virtualRule: {
...props.virtualRule,
},
properties: _properties || [],
}).subscribe((data: any) => {
ruleEditorStore.state.log.push({ time: new Date().getTime(), content: JSON.stringify(data.payload) });
if (props.virtualRule?.type !== 'window') {
stopAction()
}
})
}
const wsAgain = ref<any>();
const runScriptAgain = async () => {
if (wsAgain.value) {
wsAgain.value.unsubscribe?.();
}
const metadata = productStore.current.metadata || '{}';
const propertiesList = JSON.parse(metadata).properties || [];
const _properties = property.value.map((item: any) => {
const _item = propertiesList.find((i: any) => i.id === item.id);
return { ...item, type: _item?.valueType?.type };
});
wsAgain.value = getWebSocket(`virtual-property-debug-${props.id}-${new Date().getTime()}`,
'/virtual-property-debug',
{
virtualId: `${virtualIdRef.value}-virtual-id`,
property: props.id,
virtualRule: {
...props.virtualRule,
},
properties: _properties || [],
}).subscribe((data: any) => { })
}
const beginAction = () => {
isBeginning.value = false;
runScript();
}
const stopAction = () => {
isBeginning.value = true;
if (ws.value) {
ws.value.unsubscribe?.();
}
}
const clearAction = () => {
ruleEditorStore.set('log', []);
}
onUnmounted(() => {
if (ws.value) {
ws.value.unsubscribe?.();
}
clearAction()
})
const options = ref<{ label: string, value: string }[]>()
const getProperty = () => {
const metadata = productStore.current.metadata || '{}';
const _p: PropertyMetadata[] = JSON.parse(metadata).properties || [];
options.value = _p.filter((p) => p.id !== props.id).map((item) => ({
label: item.name,
value: item.id,
}));
}
getProperty()
</script>
<style lang="less" scoped>
.debug-container {
display: flex;
width: 100%;
height: 340px;
margin-top: 20px;
.left {
flex: 1;
min-width: 0;
max-width: 550px;
overflow-y: auto;
border: 1px solid lightgray;
.header {
display: flex;
align-items: center;
width: 100%;
height: 40px;
border-bottom: 1px solid lightgray;
//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;
}
}
.action {
width: 150px;
font-size: 14px;
}
}
}
.right {
flex: 1;
min-width: 0;
border: 1px solid lightgray;
border-left: none;
.header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
height: 40px;
border-bottom: 1px solid lightgray;
.title {
display: flex;
div {
margin: 0 10px;
}
}
.action {
display: flex;
div {
margin: 0 10px;
}
}
}
.log {
height: 290px;
padding: 5px;
overflow: auto;
}
}
}
</style>

View File

@ -0,0 +1,213 @@
<template>
<div class="editor-box">
<div class="top">
<div class="left">
<span v-for="item in symbolList.filter((t: SymbolType, i: number) => i <= 3)" :key="item.key"
@click="addOperatorValue(item.value)">
{{ item.value }}
</span>
<span>
<j-dropdown>
<AIcon type="MoreOutlined" />
<template #overlay>
<j-menu>
<j-menu-item v-for="item in symbolList.filter((t: SymbolType, i: number) => i > 6)" :key="item.key"
@click="addOperatorValue(item.value)">
{{ item.value }}
</j-menu-item>
</j-menu>
</template>
</j-dropdown>
</span>
</div>
<div class="right">
<span v-if="mode !== 'advance'">
<j-tooltip :title="!id ? '请先输入标识' : '设置属性规则'">
<AIcon type="FullscreenOutlined" :class="!id ? 'disabled' : ''" @click="fullscreenClick" />
</j-tooltip>
</span>
</div>
</div>
<div class="editor">
<j-monaco-editor v-if="loading" v-model:model-value="_value" theme="vs" ref="editor" language="javascript"/>
</div>
</div>
</template>
<script setup lang="ts" name="Editor">
interface Props {
mode?: 'advance' | 'simple';
id?: string;
value?: string;
}
const props = defineProps<Props>()
interface Emits {
(e: 'change', data: string): void;
(e: 'update:value', data: string): void;
}
const emit = defineEmits<Emits>()
type editorType = {
insert(val: string): void
}
const editor = ref<editorType>()
type SymbolType = {
key: string,
value: string
}
const symbolList = [
{
key: 'add',
value: '+',
},
{
key: 'subtract',
value: '-',
},
{
key: 'multiply',
value: '*',
},
{
key: 'divide',
value: '/',
},
{
key: 'parentheses',
value: '()',
},
{
key: 'cubic',
value: '^',
},
{
key: 'dayu',
value: '>',
},
{
key: 'dayudengyu',
value: '>=',
},
{
key: 'dengyudengyu',
value: '==',
},
{
key: 'xiaoyudengyu',
value: '<=',
},
{
key: 'xiaoyu',
value: '<',
},
{
key: 'jiankuohao',
value: '<>',
},
{
key: 'andand',
value: '&&',
},
{
key: 'huohuo',
value: '||',
},
{
key: 'fei',
value: '!',
},
{
key: 'and',
value: '&',
},
{
key: 'huo',
value: '|',
},
{
key: 'bolang',
value: '~',
},
] as SymbolType[];
const _value = computed({
get: () => props.value || '',
set: (data: string) => {
emit('update:value', data);
}
})
const loading = ref(false)
onMounted(() => {
setTimeout(() => {
loading.value = true;
}, 100);
})
const addOperatorValue = (val: string) => {
editor.value?.insert(val)
}
const fullscreenClick = () => {
if (props.id) {
emit('change', 'advance');
}
}
defineExpose({
addOperatorValue
})
</script>
<style lang="less" scoped>
.editor-box {
margin-bottom: 10px;
border: 1px solid lightgray;
.top {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
width: 100%;
border-bottom: 1px solid lightgray;
.left {
display: flex;
align-items: center;
width: 60%;
margin: 0 5px;
span {
display: inline-block;
height: 40px;
margin: 0 10px;
line-height: 40px;
cursor: pointer;
}
}
.right {
display: flex;
align-items: center;
width: 10%;
margin: 0 5px;
span {
margin: 0 5px;
}
}
.disabled {
color: rgba(#000, 0.5);
cursor: not-allowed;
}
}
.editor {
height: 300px;
}
}
</style>

View File

@ -0,0 +1,161 @@
<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">
<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 'vue3-markdown-it'
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

@ -0,0 +1,10 @@
import type { TreeNode } from '@/utils/tree';
interface OperatorItem extends TreeNode {
id: string;
name: string;
key: string;
description: string;
code: string;
children: OperatorItem[];
}

View File

@ -0,0 +1,44 @@
<template>
<Editor key="simple" @change="change" v-model:value="_value" :id="id" />
<Advance v-if="ruleEditorStore.state.model === 'advance'" v-model:value="_value" :model="ruleEditorStore.state.model"
:virtualRule="virtualRule" :id="id" @change="change" />
</template>
<script setup lang="ts">
import { useRuleEditorStore } from '@/store/ruleEditor'
import Editor from './Editor/index.vue'
import Advance from './Advance/index.vue'
interface Props {
value: string;
property?: string;
virtualRule?: any;
id?: string;
}
const props = defineProps<Props>()
interface Emits {
(e: 'update:value', data: string): void;
}
const emit = defineEmits<Emits>()
const _value = computed({
get: () => props.value,
set: (val: string) => {
emit('update:value', val)
}
})
const ruleEditorStore = useRuleEditorStore()
const change = (v: string) => {
ruleEditorStore.set('model', v);
}
onMounted(() => {
ruleEditorStore.set('property', props.property)
ruleEditorStore.set('code', props.value);
})
</script>
<style lang="less" scoped></style>

View File

@ -1,305 +1,399 @@
<template> <template>
<div class="debug-container"> <div class="debug-container">
<div class="left"> <div class="top">
<div class="header"> <div class="header">
<div> <div>
<div class="title"> <div class="title">
属性赋值 属性赋值
<div class="description">请对上方规则使用的属性进行赋值</div> <div class="description">
</div> 请对上方规则使用的属性进行赋值
<div v-if="!isBeginning && virtualRule?.type === 'window'" class="action" @click="runScriptAgain"> </div>
<a style="margin-left: 75px;">发送数据</a> </div>
</div> </div>
</div>
<div class="top-bottom">
<j-table
:columns="columns"
:data-source="property"
:pagination="false"
bordered
size="small"
:scroll="{ y: 200 }"
>
<template #bodyCell="{ column, record, index }">
<template v-if="column.key === 'id'">
<j-select
showSearch
:options="options"
v-model:value="record.id"
size="small"
style="width: 100%; z-index: 1400 !important"
/>
</template>
<template v-if="column.key === 'current'">
<j-input
v-model:value="record.current"
size="small"
></j-input>
</template>
<template v-if="column.key === 'last'">
<j-input
v-model:value="record.last"
size="small"
></j-input>
</template>
<template v-if="column.key === 'action'">
<AIcon
type="DeleteOutlined"
@click="deleteItem(index)"
/>
</template>
</template>
</j-table>
<j-button
type="dashed"
block
style="margin-top: 5px"
@click="addItem"
>
<template #icon>
<AIcon type="PlusOutlined" />
</template>
添加条目
</j-button>
</div>
</div>
<div class="bottom">
<div class="header">
<div class="title">
<div>运行结果</div>
<div v-if="virtualRule?.script && !isBeginning">
正在运行......
</div>
</div>
<div class="action">
<div
v-if="!isBeginning && virtualRule?.type === 'window'"
class="action"
@click="runScriptAgain"
>
<a style="margin-left: 75px">发送数据</a>
</div>
<div v-if="virtualRule?.script">
<a v-if="isBeginning" @click="beginAction">
开始运行
</a>
<a v-else @click="stopAction"> 停止运行 </a>
</div>
<div>
<a @click="clearAction"> 清空 </a>
</div>
</div>
</div>
<div class="log">
<j-descriptions>
<j-descriptions-item
v-for="(item, index) in ruleEditorStore.state.log"
:key="item.time"
:span="3"
>
<template #label>
<template v-if="!!runningState(index + 1, item._time)">
{{ runningState(index + 1, item._time) }}
</template>
<template v-else>{{
moment(item.time).format('HH:mm:ss')
}}</template>
</template>
<div v-if="!!runningState(index + 1, item._time)">
{{ moment(item.time).format('HH:mm:ss') }}
</div>
<j-tooltip placement="top" :title="item.content">
{{ item.content }}
</j-tooltip>
</j-descriptions-item>
</j-descriptions>
</div>
</div> </div>
</div>
<j-table :columns="columns" :data-source="property" :pagination="false" bordered size="small">
<template #bodyCell="{ column, record, index }">
<template v-if="column.key === 'id'">
<j-auto-complete :options="options" v-model:value="record.id" size="small" style="width: 130px" />
</template>
<template v-if="column.key === 'current'">
<j-input v-model:value="record.current" size="small"></j-input>
</template>
<template v-if="column.key === 'last'">
<j-input v-model:value="record.last" size="small"></j-input>
</template>
<template v-if="column.key === 'action'">
<AIcon type="DeleteOutlined" @click="deleteItem(index)" />
</template>
</template>
</j-table>
<j-button type="dashed" block style="margin-top: 5px" @click="addItem">
<template #icon>
<AIcon type="PlusOutlined" />
</template>
添加条目
</j-button>
</div> </div>
<div class="right">
<div class="header">
<div class="title">
<div>运行结果</div>
</div>
<div class="action">
<div>
<a v-if="isBeginning" @click="beginAction">
开始运行
</a>
<a v-else @click="stopAction">
停止运行
</a>
</div>
<div>
<a @click="clearAction">
清空
</a>
</div>
</div>
</div>
<div class="log">
<j-descriptions>
<j-descriptions-item v-for="item in ruleEditorStore.state.log" :label="moment(item.time).format('HH:mm:ss')"
:key="item.time" :span="3">
<j-tooltip placement="top" :title="item.content">
{{ item.content }}
</j-tooltip>
</j-descriptions-item>
</j-descriptions>
</div>
</div>
</div>
</template> </template>
<script setup lang="ts" name="Debug"> <script setup lang="ts" name="Debug">
import { PropType } from 'vue'; import { PropType, Ref } from 'vue';
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons-vue';
import { useProductStore } from '@/store/product'; import { useProductStore } from '@/store/product';
import { message } from 'jetlinks-ui-components';
import { useRuleEditorStore } from '@/store/ruleEditor'; import { useRuleEditorStore } from '@/store/ruleEditor';
import moment from 'moment'; import moment from 'moment';
import { getWebSocket } from '@/utils/websocket'; import { getWebSocket } from '@/utils/websocket';
import { PropertyMetadata } from '@/views/device/Product/typings'; import { PropertyMetadata } from '@/views/device/Product/typings';
import { onlyMessage } from '@/utils/comm';
const props = defineProps({ const props = defineProps({
virtualRule: Object as PropType<Record<any, any>>, virtualRule: Object as PropType<Record<any, any>>,
id: String, id: String,
}) });
const emits = defineEmits(['success']);
const isBeginning = ref(true) const isBeginning = ref(true);
type propertyType = { type propertyType = {
id?: string, id?: string;
current?: string, current?: string;
last?: string last?: string;
} };
const property = ref<propertyType[]>([]) const property = ref<propertyType[]>([]);
const columns = [{ const columns = [
title: '属性ID', {
dataIndex: 'id', title: '属性名称',
key: 'id' dataIndex: 'id',
}, { key: 'id',
title: '当前值', },
dataIndex: 'current', {
key: 'current' title: '当前值',
}, { dataIndex: 'current',
title: '上一值', key: 'current',
dataIndex: 'last', },
key: 'last' {
}, { title: '上一值',
title: '', dataIndex: 'last',
key: 'action' key: 'last',
}] },
{
title: '操作',
key: 'action',
width: 50,
},
];
const addItem = () => { const addItem = () => {
property.value.push({}) property.value.push({});
} };
const deleteItem = (index: number) => { const deleteItem = (index: number) => {
property.value.splice(index, 1) property.value.splice(index, 1);
} };
const ws = ref() const ws = ref();
const virtualIdRef = ref(new Date().getTime()); const virtualIdRef = ref(new Date().getTime());
const medataSource = inject<Ref<any[]>>('_dataSource');
const productStore = useProductStore();
const ruleEditorStore = useRuleEditorStore();
const time = ref<number>(0);
const timer = ref<any>(null);
const productStore = useProductStore()
const ruleEditorStore = useRuleEditorStore()
const runScript = () => { const runScript = () => {
const metadata = productStore.current.metadata || '{}'; const metadata = productStore.current.metadata || '{}';
const propertiesList = JSON.parse(metadata).properties || []; const propertiesList = JSON.parse(metadata).properties || [];
const _properties = property.value.map((item: any) => { const _properties = property.value.map((item: any) => {
const _item = propertiesList.find((i: any) => i.id === item.id); const _item = propertiesList.find((i: any) => i.id === item.id);
return { ...item, type: _item?.valueType?.type }; return { ...item, type: _item?.valueType?.type };
}); });
if (ws.value) { if (ws.value) {
ws.value.unsubscribe?.(); ws.value.unsubscribe?.();
}
if (!props.virtualRule?.script) {
isBeginning.value = true;
message.warning('请编辑规则');
return;
}
ws.value = getWebSocket(`virtual-property-debug-${props.id}-${new Date().getTime()}`,
'/virtual-property-debug',
{
virtualId: `${virtualIdRef.value}-virtual-id`,
property: props.id,
virtualRule: {
...props.virtualRule,
},
properties: _properties || [],
})
ws.value.subscribe((data: any) => {
ruleEditorStore.state.log.push({ time: new Date().getTime(), content: JSON.stringify(data.payload) });
if (props.virtualRule?.type !== 'window') {
stopAction()
} }
}) if (!props.virtualRule?.script) {
} isBeginning.value = true;
onlyMessage('请编辑规则', 'warning');
return;
}
ws.value = getWebSocket(
`virtual-property-debug-${props.id}-${new Date().getTime()}`,
'/virtual-property-debug',
{
virtualId: `${virtualIdRef.value}-virtual-id`,
property: props.id,
virtualRule: {
...props.virtualRule,
},
properties: _properties || [],
},
).subscribe((data: any) => {
ruleEditorStore.state.log.push({
time: new Date().getTime(),
content: JSON.stringify(data.payload),
_time: unref(time.value)
});
emits('success', false);
if (props.virtualRule?.type !== 'window') {
stopAction();
}
});
};
const runningState = (_index: number, _time: number) => {
if (props.virtualRule?.windowType === 'time') {
return `已运行${_time}`;
}
if (props.virtualRule?.windowType === 'num') {
return `${_index}次运行`;
}
return false;
};
const wsAgain = ref<any>(); const wsAgain = ref<any>();
const runScriptAgain = async () => { const runScriptAgain = async () => {
if (wsAgain.value) { if (wsAgain.value) {
wsAgain.value.unsubscribe?.(); wsAgain.value.unsubscribe?.();
} }
const metadata = productStore.current.metadata || '{}'; const metadata = productStore.current.metadata || '{}';
const propertiesList = JSON.parse(metadata).properties || []; const propertiesList = JSON.parse(metadata).properties || [];
const _properties = property.value.map((item: any) => { const _properties = property.value.map((item: any) => {
const _item = propertiesList.find((i: any) => i.id === item.id); const _item = propertiesList.find((i: any) => i.id === item.id);
return { ...item, type: _item?.valueType?.type }; return { ...item, type: _item?.valueType?.type };
}); });
wsAgain.value = getWebSocket(`virtual-property-debug-${props.id}-${new Date().getTime()}`, wsAgain.value = getWebSocket(
'/virtual-property-debug', `virtual-property-debug-${props.id}-${new Date().getTime()}`,
{ '/virtual-property-debug',
virtualId: `${virtualIdRef.value}-virtual-id`, {
property: props.id, virtualId: `${virtualIdRef.value}-virtual-id`,
virtualRule: { property: props.id,
...props.virtualRule, virtualRule: {
}, ...props.virtualRule,
properties: _properties || [], },
}) properties: _properties || [],
wsAgain.value.subscribe((data: any) => { }) },
} ).subscribe((data: any) => {});
};
const getTime = () => {
time.value = 0;
timer.value = setInterval(() => {
time.value += 1;
}, 1000);
};
const beginAction = () => { const beginAction = () => {
isBeginning.value = false; isBeginning.value = false;
runScript(); runScript();
} getTime();
};
const stopAction = () => { const stopAction = () => {
isBeginning.value = true; isBeginning.value = true;
if (ws.value) { if (ws.value) {
ws.value.unsubscribe?.(); ws.value.unsubscribe?.();
} }
} window.clearInterval(timer.value)
timer.value = null
};
const clearAction = () => { const clearAction = () => {
ruleEditorStore.set('log', []); ruleEditorStore.set('log', []);
} };
onUnmounted(() => { onUnmounted(() => {
if (ws.value) { if (ws.value) {
ws.value.unsubscribe?.(); ws.value.unsubscribe?.();
} }
clearAction() clearAction();
}) window.clearInterval(timer.value)
timer.value = null
});
const options = ref<{ label: string, value: string }[]>() const options = computed(() => {
const getProperty = () => { return (medataSource.value || [])
const metadata = productStore.current.metadata || '{}'; .filter((p) => p.id !== props.id)
const _p: PropertyMetadata[] = JSON.parse(metadata).properties || []; .map((item) => ({
options.value = _p.filter((p) => p.id !== props.id).map((item) => ({ label: item.name,
label: item.name, value: item.id,
value: item.id, }));
})); });
}
getProperty() // const getProperty = () => {
// // const metadata = productStore.current.metadata || '{}';
// // const _p: PropertyMetadata[] = JSON.parse(metadata).properties || [];
// console.log(medataSource.value)
// options.value =
// };
// getProperty();
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.debug-container { .debug-container {
display: flex; // display: flex;
width: 100%; // width: 100%;
height: 340px; // height: 340px;
margin-top: 20px; // margin-top: 20px;
.left { .top {
flex: 1; // min-width: 0;
min-width: 0; // max-width: 550px;
max-width: 550px; // overflow-y: auto;
overflow-y: auto; height: 350px;
border: 1px solid lightgray; border: 1px solid lightgray;
margin-bottom: 10px;
.header { .header {
display: flex; display: flex;
align-items: center; align-items: center;
width: 100%; width: 100%;
height: 40px; height: 40px;
border-bottom: 1px solid lightgray; border-bottom: 1px solid lightgray;
//justify-content: space-around; //justify-content: space-around;
div { div {
display: flex; display: flex;
//width: 100%; //width: 100%;
align-items: center; align-items: center;
justify-content: flex-start; justify-content: flex-start;
height: 100%; height: 100%;
.title { .title {
margin: 0 10px; margin: 0 10px;
font-weight: 600; font-weight: 600;
font-size: 16px; font-size: 16px;
}
.description {
margin-left: 10px;
color: lightgray;
font-size: 12px;
}
}
.action {
width: 150px;
font-size: 14px;
}
} }
.description { .top-bottom {
margin-left: 10px; padding: 10px;
color: lightgray;
font-size: 12px;
} }
}
.action {
width: 150px;
font-size: 14px;
}
}
}
.right {
flex: 1;
min-width: 0;
border: 1px solid lightgray;
border-left: none;
.header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
height: 40px;
border-bottom: 1px solid lightgray;
.title {
display: flex;
div {
margin: 0 10px;
}
}
.action {
display: flex;
div {
margin: 0 10px;
}
}
} }
.log { .bottom {
height: 290px; border: 1px solid lightgray;
padding: 5px; .header {
overflow: auto; display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
height: 40px;
border-bottom: 1px solid lightgray;
.title {
display: flex;
div {
margin: 0 10px;
}
}
.action {
display: flex;
div {
margin: 0 10px;
}
}
}
.log {
height: 300px;
padding: 5px;
overflow: auto;
}
} }
}
} }
</style> </style>

View File

@ -1,161 +1,213 @@
<template> <template>
<div class="operator-box"> <div class="operator-box">
<j-input-search @search="search" allow-clear placeholder="搜索关键字" /> <div class="left">
<div class="tree"> <j-input-search
<j-tree @select="selectTree" :field-names="{ title: 'name', key: 'id', }" auto-expand-parent @search="search"
:tree-data="data"> allow-clear
<template #title="node"> placeholder="搜索关键字"
<div class="node"> />
<div style="max-width: 180px"><Ellipsis>{{ node.name }}</Ellipsis></div> <div class="tree">
<div :class="node.children?.length > 0 ? 'parent' : 'add'"> <j-scrollbar>
<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)">
添加 <j-tree
</a> @select="selectTree"
:field-names="{ title: 'name', key: 'id' }"
auto-expand-parent
:tree-data="data"
>
<template #title="node">
<div class="node">
<div style="max-width: 160px">
<Ellipsis>{{ node.name }}</Ellipsis>
</div>
<div
:class="
node.children?.length > 0 ? 'parent' : 'add'
"
>
<j-popover
v-if="node.type === 'property'"
:overlayStyle="{
zIndex: 1200
}"
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 class="has-property">添加</a>
</j-popover>
<a class="no-property" v-else @click.stop="addClick(node)"> 添加 </a>
</div>
</div>
</template>
</j-tree>
</j-scrollbar>
</div> </div>
</div> </div>
</template> <div class="right">
</j-tree> <Markdown :source="item?.description || ''"></Markdown>
</div>
</div> </div>
<div class="explain">
<Markdown :source="item?.description || ''"></Markdown>
</div>
</div>
</template> </template>
<script setup lang="ts" name="Operator"> <script setup lang="ts" name="Operator">
import { useProductStore } from '@/store/product'; import { useProductStore } from '@/store/product';
import type { OperatorItem } from './typings'; import type { OperatorItem } from './typings';
import { treeFilter } from '@/utils/tree' import { treeFilter } from '@/utils/tree';
import { PropertyMetadata } from '@/views/device/Product/typings'; import { PropertyMetadata } from '@/views/device/Product/typings';
import { getOperator } from '@/api/device/product' import { getOperator } from '@/api/device/product';
import Markdown from 'vue3-markdown-it' import Markdown from 'vue3-markdown-it';
const props = defineProps({ const props = defineProps({
id: String id: String,
}) });
interface Emits { interface Emits {
(e: 'addOperatorValue', data: string): void; (e: 'addOperatorValue', data: string): void;
} }
const emit = defineEmits<Emits>(); const emit = defineEmits<Emits>();
const item = ref<Partial<OperatorItem>>() const item = ref<Partial<OperatorItem>>();
const data = ref<OperatorItem[]>([]) const data = ref<OperatorItem[]>([]);
const dataRef = ref<OperatorItem[]>([]) const dataRef = ref<OperatorItem[]>([]);
const search = (value: string) => { const search = (value: string) => {
if (value) { if (value) {
const nodes = treeFilter(dataRef.value, value, 'name') as OperatorItem[]; const nodes = treeFilter(
data.value = nodes; dataRef.value,
} else { value,
data.value = dataRef.value; 'name',
} ) as OperatorItem[];
data.value = nodes;
} else {
data.value = dataRef.value;
}
}; };
const selectTree = (k: any, info: any) => { const selectTree = (k: any, info: any) => {
item.value = info.node as unknown as OperatorItem; item.value = info.node as unknown as OperatorItem;
} };
const recentClick = (node: OperatorItem) => { const recentClick = (node: OperatorItem) => {
emit('addOperatorValue', `$recent("${node.id}")`) emit('addOperatorValue', `$recent("${node.id}")`);
} };
const lastClick = (node: OperatorItem) => { const lastClick = (node: OperatorItem) => {
emit('addOperatorValue', `$lastState("${node.id}")`) emit('addOperatorValue', `$lastState("${node.id}")`);
} };
const addClick = (node: OperatorItem) => { const addClick = (node: OperatorItem) => {
emit('addOperatorValue', node.code) console.log(node)
} emit('addOperatorValue', node.code);
};
const productStore = useProductStore() const productStore = useProductStore();
const getData = async (id?: string) => { const getData = async (id?: string) => {
const metadata = productStore.current.metadata || '{}'; const metadata = productStore.current.metadata || '{}';
const _properties = JSON.parse(metadata).properties || [] as PropertyMetadata[] const _properties =
const properties = { JSON.parse(metadata).properties || ([] as PropertyMetadata[]);
id: 'property', const properties = {
name: '属性', id: 'property',
description: '', name: '属性',
code: '', description: '',
children: _properties code: '',
.filter((p: PropertyMetadata) => p.id !== id) children: _properties
.map((p: PropertyMetadata) => ({ .filter((p: PropertyMetadata) => p.id !== id)
id: p.id, .map((p: PropertyMetadata) => ({
name: p.name, id: p.id,
description: `### ${p.name} name: p.name,
description: `### ${p.name}
\n 数据类型: ${p.valueType?.type} \n 数据类型: ${p.valueType?.type}
\n 是否只读: ${p.expands?.readOnly || 'false'} \n 是否只读: ${p.expands?.readOnly || 'false'}
\n 可写数值范围: `, \n 可写数值范围: `,
type: 'property', type: 'property',
})), })),
}; };
const response = await getOperator(); const response = await getOperator();
if (response.status === 200) { if (response.status === 200) {
data.value = [properties as OperatorItem, ...response.result]; data.value = [properties as OperatorItem, ...response.result];
dataRef.value = [properties as OperatorItem, ...response.result]; dataRef.value = [properties as OperatorItem, ...response.result];
} }
}; };
watch(() => props.id, watch(
(val) => { () => props.id,
getData(val) (val) => {
}, getData(val);
{ immediate: true } },
) { immediate: true },
);
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.border { .border {
margin-top: 10px; margin-top: 10px;
padding: 10px; padding: 10px;
border-top: 1px solid lightgray; border-top: 1px solid lightgray;
} }
.operator-box { .operator-box {
width: 100%; width: 100%;
display: flex;
.explain { .left,
.border; .right {
} width: 50%;
height: 350px;
.tree { border: 1px solid lightgray;
.border; }
height: 350px; .left {
overflow-y: auto; padding: 10px;
margin-right: 10px;
.node { .tree {
display: flex;
justify-content: space-between; height: 300px;
width: 220px; //overflow-y: auto;
//.add { .node {
// display: none; display: flex;
//} justify-content: space-between;
// width: 190px;
//&:hover .add {
// display: block; .parent {
//} display: none;
}
.parent { }
display: none; }
} }
.right {
padding: 20px;
} }
} }
</style>
<style>
.rule-popover {
z-index: 1200;
} }
</style> </style>

View File

@ -1,44 +1,133 @@
<template> <template>
<Editor key="simple" @change="change" v-model:value="_value" :id="id" /> <j-modal
<Advance v-if="ruleEditorStore.state.model === 'advance'" v-model:value="_value" :model="ruleEditorStore.state.model" :zIndex="1030"
:virtualRule="virtualRule" :id="id" @change="change" /> :mask-closable="false"
visible
width="70vw"
title="编辑规则"
@cancel="handleCancel"
:destroyOnClose="true"
>
<div class="header" v-if="virtualRule?.windowType && virtualRule?.windowType !== 'undefined'">
<div class="header-item">
{{
virtualRule?.windowType === 'time' ? '时间窗口' : '频次窗口'
}}
</div>
<div class="header-item">
<div>聚合函数: <span>{{ aggType || '--' }}</span></div>
<div>窗口长度()<span>{{ virtualRule?.window?.span || '--' }}</span></div>
<div>步长(): <span>{{ virtualRule?.window?.every || '--' }}</span></div>
</div>
</div>
<div class="box">
<div class="left">
<div>
<Operator :id="id" @add-operator-value="addOperatorValue" />
</div>
<div style="margin-top: 10px;">
<Editor
ref="editor"
mode="advance"
key="advance"
v-model:value="_value"
/>
</div>
</div>
<div class="right">
<Debug
:virtualRule="{
...virtualRule,
script: _value,
}"
:id="id"
@success="onSuccess"
/>
</div>
</div>
<template #footer>
<j-space>
<j-button @click="handleCancel">取消</j-button>
<j-button :disabled="_disabled" @click="handleOk" type="primary">确定</j-button>
</j-space>
</template>
</j-modal>
</template> </template>
<script setup lang="ts" name="FRuleEditor"> <script setup lang="ts" name="FRuleEditor">
import { useRuleEditorStore } from '@/store/ruleEditor' import Editor from './Editor/index.vue';
import Editor from './Editor/index.vue' import Debug from './Debug/index.vue';
import Advance from './Advance/index.vue' import Operator from './Operator/index.vue';
interface Props {
value: string;
property?: string;
virtualRule?: any;
id?: string;
}
const props = defineProps<Props>()
interface Emits { interface Emits {
(e: 'update:value', data: string): void; (e: 'save', data: string | undefined): void;
(e: 'close'): void;
} }
const emit = defineEmits<Emits>();
const emit = defineEmits<Emits>() const props = defineProps({
value: String,
id: String,
virtualRule: Object,
aggList: Array
});
const _value = computed({ const _value = ref<string | undefined>(props.value);
get: () => props.value, const _disabled = ref<boolean>(true);
set: (val: string) => {
emit('update:value', val) const handleCancel = () => {
} emit('close');
};
const handleOk = () => {
emit('save', _value.value);
};
const aggType = computed(() => {
const _item: any = (props?.aggList || []).find((item: any) => {
return item?.value === props.virtualRule?.aggType
})
return _item?.label
}) })
const ruleEditorStore = useRuleEditorStore() const editor = ref();
const addOperatorValue = (val: string) => {
editor.value.addOperatorValue(val);
};
const change = (v: string) => { watch(() => _value.value, () => {
ruleEditorStore.set('model', v); _disabled.value = true
}
onMounted(() => {
ruleEditorStore.set('property', props.property)
ruleEditorStore.set('code', props.value);
}) })
const onSuccess = (bool: boolean) => {
_disabled.value = bool;
}
</script> </script>
<style lang="less" scoped></style> <style lang="less" scoped>
.header {
margin-bottom: 20px;
.header-item {
display: flex;
gap: 24px;
div span {
color: rgba(0, 0, 0, 0.8);
}
}
}
.box {
display: flex;
justify-content: flex-start;
width: 100%;
.left {
width: 60%;
}
.right {
width: 40%;
margin-left: 10px;
padding-left: 10px;
border-left: 1px solid lightgray;
}
}
</style>

View File

@ -4,7 +4,7 @@
v-model:openKeys="state.openKeys" v-model:openKeys="state.openKeys"
v-model:collapsed="state.collapsed" v-model:collapsed="state.collapsed"
v-model:selectedKeys="state.selectedKeys" v-model:selectedKeys="state.selectedKeys"
:headerHeight='60' :headerHeight='layout.headerHeight'
:pure="state.pure" :pure="state.pure"
:breadcrumb="{ routes: breadcrumb }" :breadcrumb="{ routes: breadcrumb }"
@backClick='routerBack' @backClick='routerBack'
@ -54,11 +54,11 @@ const route = useRoute();
const menu = useMenuStore(); const menu = useMenuStore();
const system = useSystem(); const system = useSystem();
const {configInfo} = storeToRefs(system); const {configInfo,layout} = storeToRefs(system);
const layoutConf = reactive({ const layoutConf = reactive({
theme: DefaultSetting.layout.theme, theme: DefaultSetting.layout.theme,
siderWidth: DefaultSetting.layout.siderWidth, siderWidth: layout.value.siderWidth,
logo: DefaultSetting.layout.logo, logo: DefaultSetting.layout.logo,
title: DefaultSetting.layout.title, title: DefaultSetting.layout.title,
menuData: [...clearMenuItem(menu.siderMenus), AccountMenu], menuData: [...clearMenuItem(menu.siderMenus), AccountMenu],

View File

@ -1,5 +1,5 @@
<template> <template>
<div class='full-page-warp' ref='fullPage' :style='{ minHeight: `calc(100vh - ${y + 24}px)`}'> <div ref='fullPage' :style="{ minHeight: MinHeight}" class='full-page-warp' >
<div class="full-page-warp-content"> <div class="full-page-warp-content">
<slot></slot> <slot></slot>
</div> </div>
@ -9,9 +9,21 @@
<script setup lang='ts' name='FullPage'> <script setup lang='ts' name='FullPage'>
import { useElementBounding } from '@vueuse/core' import { useElementBounding } from '@vueuse/core'
const props = defineProps({
extraHeight: {
type: Number,
default: 0
}
})
const fullPage = ref(null) const fullPage = ref(null)
const { y } = useElementBounding(fullPage) const { y } = useElementBounding(fullPage)
const MinHeight = computed(() => {
const _y = (y.value < 0 ? 0 : y.value) + props.extraHeight
return `calc(100vh - ${_y + 24}px)`
})
</script> </script>
<style scoped lang="less"> <style scoped lang="less">

View File

@ -3,18 +3,15 @@
<j-dropdown <j-dropdown
v-model:visible="visible" v-model:visible="visible"
:trigger="['click']" :trigger="['click']"
:destroyPopupOnHide="true"
@visible-change="visibleChange" @visible-change="visibleChange"
> >
<!-- <div class="icon-content">
<AIcon type="BellOutlined" style="font-size: 16px" />
<span class="unread" v-show="total > 0">{{ total }}</span>
</div> -->
<j-badge :count="total" :offset="[3, -3]"> <j-badge :count="total" :offset="[3, -3]">
<AIcon type="BellOutlined" style="font-size: 16px" /> <AIcon type="BellOutlined" style="font-size: 16px" />
</j-badge> </j-badge>
<template #overlay> <template #overlay>
<div> <div>
<NoticeInfo :data="list" @on-action="handleRead" /> <NoticeInfo :tabs="tabs" @action="handleRead" />
</div> </div>
</template> </template>
</j-dropdown> </j-dropdown>
@ -22,7 +19,7 @@
</template> </template>
<script setup lang="tsx"> <script setup lang="tsx">
import { getListByUnRead_api } from '@/api/account/notificationRecord'; import { getList_api } from '@/api/account/notificationRecord';
import NoticeInfo from './NoticeInfo.vue'; import NoticeInfo from './NoticeInfo.vue';
import { getWebSocket } from '@/utils/websocket'; import { getWebSocket } from '@/utils/websocket';
import { notification, Button } from 'jetlinks-ui-components'; import { notification, Button } from 'jetlinks-ui-components';
@ -30,13 +27,16 @@ import { changeStatus_api } from '@/api/account/notificationRecord';
import { useUserInfo } from '@/store/userInfo'; import { useUserInfo } from '@/store/userInfo';
import { useMenuStore } from '@/store/menu'; import { useMenuStore } from '@/store/menu';
import { getAllNotice } from '@/api/account/center';
import { flatten } from 'lodash-es';
const { jumpPage } = useMenuStore(); const updateCount = computed(() => useUserInfo().alarmUpdateCount);
const updateCount = computed(() => useUserInfo().$state.alarmUpdateCount); const menuStory = useMenuStore();
const total = ref(0); const total = ref(0);
const list = ref<any[]>([]); // const list = ref<any[]>([]);
const loading = ref(false); const loading = ref(false);
const visible = ref(false);
const subscribeNotice = () => { const subscribeNotice = () => {
getWebSocket('notification', '/notifications', {}) getWebSocket('notification', '/notifications', {})
@ -63,7 +63,6 @@ const subscribeNotice = () => {
) )
, ,
onClick: () => { onClick: () => {
// changeStatus_api('_read', [resp.id])
read('', resp); read('', resp);
}, },
key: resp.payload.id, key: resp.payload.id,
@ -93,14 +92,41 @@ const read = (type: string, data: any) => {
notification.close(data.payload.id); notification.close(data.payload.id);
getList(); getList();
if (type !== '_read') { if (type !== '_read') {
jumpPage('account/NotificationRecord', { menuStory.routerPush('account/center', {
tabKey: 'StationMessage',
row: data.payload.detail, row: data.payload.detail,
}); });
} }
}); });
}; };
const tab = [
{
key: 'alarm',
tab: '告警',
type: [
'alarm-product',
'alarm-device',
'alarm-other',
'alarm-org',
'alarm',
],
},
{
key: 'system-monitor',
tab: '系统监控',
type: ['system-event'],
},
{
key: 'system-business',
tab: '业务监控',
type: ['device-transparent-codec'],
},
];
//
const getList = () => { const getList = () => {
if(tabs.value.length <= 0) return;
loading.value = true; loading.value = true;
const params = { const params = {
sorts: [{ sorts: [{
@ -111,34 +137,63 @@ const getList = () => {
{ {
terms: [ terms: [
{ {
type: 'or', type: 'and',
value: 'unread', value: 'unread',
termType: 'eq', termType: 'eq',
column: 'state', column: 'state',
}, },
], ],
}, },
{
terms: [
{
type: 'and',
value: flatten(tabs.value.map((i: any) => i?.type)),
termType: 'in',
column: 'topicProvider',
},
],
},
], ],
}; };
getListByUnRead_api(params) getList_api(params)
.then((resp: any) => { .then((resp: any) => {
list.value = resp.result.data;
total.value = resp.result.total; total.value = resp.result.total;
}) })
.finally(() => (loading.value = false)); .finally(() => (loading.value = false));
}; };
subscribeNotice();
getList();
watch(updateCount, () => getList());
const visibleChange = (bool: boolean) => { const visibleChange = (bool: boolean) => {
bool && getList(); bool && getList();
}; };
const visible = ref(false);
const handleRead = () => { const handleRead = () => {
visible.value = false; visible.value = false;
getList(); getList();
}; };
watch(updateCount, () => getList());
const tabs = ref<any>([]);
const queryTypeList = async () => {
const resp: any = await getAllNotice();
if (resp.status === 200) {
const provider = resp.result.map((i: any) => i.provider) || [];
const arr = tab.filter((item: any) => {
return item.type.some((i: any) => provider.includes(i))
});
tabs.value = arr;
if(arr.length > 0) {
subscribeNotice();
getList();
}
}
};
onMounted(() => {
queryTypeList()
})
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@ -1,57 +1,156 @@
<template> <template>
<div class="notice-info-container"> <div class="notice-info-container">
<j-tabs :activeKey="'default'"> <j-tabs
<j-tab-pane key="default" tab="未读消息"> v-model:activeKey="activeKey"
<div class="no-data" v-if="props.data.length === 0"> :destroyInactiveTabPane="true"
<img @change="onChange"
src="https://gw.alipayobjects.com/zos/rmsportal/sAuJeJzSKbUmHfBQRzmZ.svg" v-if="tabs.length"
alt="" >
<j-tab-pane v-for="item in tabs" :key="item.key">
<template #tab>
<NoticeTab
:refresh="refreshObj[item.key]"
:tab="item?.tab"
:type="item.type"
/> />
</div> </template>
<j-spin :spinning="loading">
<div v-else class="content"> <div class="content">
<j-scrollbar class="list" max-height="400"> <j-scrollbar class="list" :max-height="450" v-if="list.length">
<div <template v-for="i in list" :key="i.id">
class="list-item" <NoticeItem
v-for="item in props.data" :data="i"
@click.stop="read(item.id)" :type="item.key"
> @action="emits('action')"
<h5>{{ item.topicName }}</h5> @refresh="onRefresh(item.key)"
<p>{{ item.message }}</p> />
</template>
<div
v-if="list.length < 12"
style="
color: #666666;
text-align: center;
padding: 8px;
"
>
这是最后一条数据了
</div>
</j-scrollbar>
<div class="no-data" v-else>
<j-empty />
</div>
<div class="btns">
<j-button type="link" @click="onMore(item.key)"
>查看更多</j-button
>
</div> </div>
</j-scrollbar>
<div class="btns">
<span @click="read()">当前标记为已读</span>
<span @click="jumpPage('account/NotificationRecord')"
>查看更多</span
>
</div> </div>
</div> </j-spin>
</j-tab-pane> </j-tab-pane>
</j-tabs> </j-tabs>
<div class="no-data" v-else>
<j-empty />
</div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { changeStatus_api } from '@/api/account/notificationRecord'; import { getList_api } from '@/api/account/notificationRecord';
import { useMenuStore } from '@/store/menu'; import { useMenuStore } from '@/store/menu';
import { useUserInfo } from '@/store/userInfo';
import { cloneDeep } from 'lodash-es';
import NoticeItem from './NoticeItem.vue';
import NoticeTab from './NoticeTab.vue';
const emits = defineEmits(['onAction']); const emits = defineEmits(['action']);
const props = defineProps<{
data: any[];
}>();
const { jumpPage } = useMenuStore();
const read = (id?: string) => { type DataType = 'alarm' | 'system-monitor' | 'system-business';
const ids = id ? [id] : props.data.map((item) => item.id);
changeStatus_api('_read', ids).then((resp: any) => { const refreshObj = ref({
if (resp.status === 200) { 'alarm': true,
jumpPage('account/NotificationRecord', { 'system-monitor': true,
row: props.data.find((f: any) => f.id === id), 'system-business': true,
}); });
emits('onAction');
} const props = defineProps({
}); tabs: {
type: Array,
default: () => []
}
})
const loading = ref(false);
const total = ref(0);
const list = ref<any[]>([]);
const activeKey = ref<DataType>(props.tabs?.[0]?.key || 'alarm');
const menuStory = useMenuStore();
const route = useRoute();
const userInfo = useUserInfo();
const getData = (type: string[]) => {
loading.value = true;
const params = {
sorts: [
{
name: 'notifyTime',
order: 'desc',
},
],
pageSize: 12,
terms: [
{
terms: [
{
type: 'or',
value: type,
termType: 'in',
column: 'topicProvider',
},
],
},
],
};
getList_api(params)
.then((resp: any) => {
total.value = resp.result.total;
list.value = resp.result?.data || [];
})
.finally(() => (loading.value = false));
};
const onChange = (_key: string) => {
const type = props.tabs.find((item: any) => item.key === _key)?.type || [];
getData(type);
};
onMounted(async () => {
onChange(props.tabs?.[0]?.key || "alarm");
});
const onRefresh = (id: string) => {
const flag = cloneDeep(refreshObj.value[id]);
refreshObj.value = {
...refreshObj.value,
[id]: !flag,
};
};
const onMore = (key: string) => {
// /account/center
if (route.path === '/account/center') {
userInfo.tabKey = 'StationMessage';
userInfo.other.tabKey = key;
} else {
menuStory.routerPush('account/center', {
tabKey: 'StationMessage',
other: {
tabKey: key,
},
});
}
emits('action');
}; };
</script> </script>
@ -81,7 +180,7 @@ const read = (id?: string) => {
.content { .content {
.list { .list {
max-height: 400px; max-height: 450px;
overflow: auto; overflow: auto;
padding: 0; padding: 0;
margin: 0; margin: 0;
@ -89,41 +188,12 @@ const read = (id?: string) => {
// //
display: none; display: none;
} }
.list-item {
padding: 12px 24px;
list-style: none;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
h5 {
color: rgba(0, 0, 0, 0.85);
font-size: 14px;
font-weight: normal;
}
p {
font-size: 12px;
color: rgba(0, 0, 0, 0.45);
}
&:hover {
background: #f0f5ff;
}
}
} }
.btns { .btns {
display: flex; display: flex;
height: 46px; height: 46px;
line-height: 46px; justify-content: center;
span { align-items: center;
display: block;
width: 50%;
text-align: center;
cursor: pointer;
&:first-child {
border-right: 1px solid #f0f0f0;
}
}
} }
} }
} }

View File

@ -0,0 +1,182 @@
<template>
<div class="list-items">
<div
class="list-item"
@click="onMove"
:style="{
transform: `translate(${num}px, 0)`,
}"
>
<div class="list-item-left">
<div class="header">
<div class="title">
<div>
{{ props.data?.topicName }}
</div>
<span :style="{color: state === 'unread' ? 'red' : '#AAAAAA'}">{{ state === 'unread' ? '未读' : '已读' }}</span>
</div>
<div class="time">
{{
dayjs(props.data?.notifyTime).format(
'YYYY-MM-DD HH:mm:ss',
)
}}
</div>
</div>
<j-ellipsis :lineClamp="2">
{{ props.data?.message }}
</j-ellipsis>
</div>
<div class="list-item-right">
<j-button style="margin-bottom: 5px;" class="btn" @click.stop="detail">查看详情</j-button>
<j-button class="btn" v-if="state === 'unread'" @click.stop="read('_read')">标为已读</j-button>
<j-button class="btn" v-else @click.stop="read('_unread')">标为未读</j-button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import dayjs from 'dayjs';
import { changeStatus_api } from '@/api/account/notificationRecord';
import { useMenuStore } from '@/store/menu';
import { useUserInfo } from '@/store/userInfo';
import { onlyMessage } from '@/utils/comm';
const menuStory = useMenuStore();
const route = useRoute();
const userInfo = useUserInfo();
const emits = defineEmits(['action', 'refresh']);
const props = defineProps({
data: {
type: Object,
default: () => {},
},
type: {
type: String,
default: "alarm"
}
});
const num = ref<-100 | 0>(0);
const state = ref(props.data.state?.value)
watchEffect(() => {
state.value = props.data.state?.value
})
const onMove = () => {
num.value = num.value === 0 ? -100 : 0;
};
const detail = () => {
// /account/center
if (route.path === '/account/center') {
userInfo.tabKey = 'StationMessage';
userInfo.messageInfo = props.data;
userInfo.other.tabKey = props.type;
} else {
menuStory.routerPush('account/center', {
row: props.data,
tabKey: 'StationMessage',
other: {
tabKey: props.type
}
});
}
emits('action');
};
const read = (type: '_read' | '_unread') => {
changeStatus_api(type, [props.data.id]).then((resp: any) => {
if (resp.status === 200) {
if(type === '_read') {
userInfo.alarmUpdateCount -= 1;
} else {
userInfo.alarmUpdateCount += 1;
}
num.value = 0;
state.value = type === '_read' ? 'read' : 'unread'
onlyMessage('操作成功!');
emits('refresh')
}
});
};
</script>
<style lang="less" scoped>
.list-items {
width: 312px;
overflow: hidden;
border-bottom: 1px solid #f0f0f0;
margin: 0 24px;
box-sizing: content-box;
// &:hover {
// background-color: #F9FAFF;
// }
}
.list-item {
list-style: none;
cursor: pointer;
display: flex;
width: 412px;
transition: all 0.3s;
gap: 24px;
.list-item-left {
padding: 12px 0;
width: 312px;
// height: 100px;
.header {
display: flex;
align-items: center;
justify-content: space-between;
.title {
display: flex;
align-items: center;
width: calc(100% - 120px);
div {
color: rgba(0, 0, 0, 0.85);
font-size: 14px;
font-weight: bold;
margin-right: 10px;
max-width: calc(100% - 40px);
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
span {
color: red;
font-size: 13px;
width: 30px;
}
}
.time {
font-size: 12px;
color: rgba(0, 0, 0, 0.45);
width: 120px;
}
}
}
.list-item-right {
width: 100px;
padding: 5px 12px 5px 0;
display: flex;
flex-direction: column;
justify-content: center;
.btn {
border: none;
background-color: #F1F4FF;
color: @primary-color;
}
}
}
</style>

View File

@ -0,0 +1,69 @@
<template>
<j-badge :count="total" :offset="[3, -3]">
{{ tab }}
</j-badge>
</template>
<script setup lang="ts">
import { getList_api } from '@/api/account/notificationRecord';
import { PropType } from 'vue';
const props = defineProps({
tab: {
type: String,
default: '',
},
type: {
type: Array as PropType<string[]>,
default: () => [],
},
refresh: {
type: Boolean
}
});
const total = ref<number>(0);
const getData = (type: string[]) => {
const params = {
sorts: [
{
name: 'notifyTime',
order: 'desc',
},
],
terms: [
{
terms: [
{
type: 'and',
value: type,
termType: 'in',
column: 'topicProvider',
},
{
type: 'and',
value: 'unread',
termType: 'eq',
column: 'state',
},
],
},
],
};
getList_api(params).then((resp: any) => {
total.value = resp.result.total;
});
};
watch(
() => props.refresh,
() => {
getData(props.type);
},
{
immediate: true,
deep: true
}
);
</script>

View File

@ -1,13 +1,14 @@
<template> <template>
<div> <div>
<j-dropdown placement="bottomRight"> <j-dropdown placement="bottomRight">
<div style="cursor: pointer;height: 100%;"> <div style="cursor: pointer;height: 100%;white-space: nowrap;overflow: hidden;text-overflow:ellipsis; max-width: 170px;" >
<img <j-avatar
:src="userInfo.avatar" :src="userInfo.userInfos?.avatar"
alt="" alt=""
style="width: 24px; margin-right: 12px" :size="24"
style="margin-right: 12px"
/> />
<span>{{ userInfo.name }}</span> <span>{{ userInfo.userInfos?.name }}</span>
</div> </div>
<template #overlay> <template #overlay>
<j-menu> <j-menu>
@ -32,8 +33,7 @@ import { LoginPath } from '@/router/menu'
const {push} = useRouter(); const {push} = useRouter();
const userInfo = useUserInfo().$state.userInfos as any; const userInfo = useUserInfo() as any;
const logOut = () => { const logOut = () => {
loginout_api().then(() => { loginout_api().then(() => {
@ -43,4 +43,5 @@ const logOut = () => {
}; };
</script> </script>
<style scoped></style> <style scoped>
</style>

View File

@ -0,0 +1,89 @@
<template>
<div class="box">
<div class="box-btn" v-if="pageIndex > 0">
<div class="box-item-action" @click="onLeft">
<AIcon type="LeftOutlined" />
</div>
</div>
<div class="box-item" v-for="item in getData" :key="item.id">
<slot name="card" v-bind="item"></slot>
</div>
<div class="box-item">
<slot name="add"></slot>
</div>
<div class="box-btn" v-if="(pageIndex + 1) * showLength < data.length">
<div class="box-item-action" @click="onRight">
<AIcon type="RightOutlined" />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { PropType } from 'vue';
const props = defineProps({
data: {
type: Array as PropType<any[]>,
default: () => [],
},
showLength: {
type: Number,
default: 8,
},
});
const pageIndex = ref<number>(0);
const getData = computed(() => {
const start = pageIndex.value >= 0 ? pageIndex.value * props.showLength : 0;
const end =
(pageIndex.value + 1) * props.showLength < props.data.length
? props.showLength * (pageIndex.value + 1)
: props.data.length;
return props.data.slice(start, end);
});
const onRight = () => {
const flag = pageIndex.value + 1;
if (flag < props.data.length) {
pageIndex.value = flag;
}
};
const onLeft = () => {
const flag = pageIndex.value - 1;
if (flag >= 0) {
pageIndex.value -= 1;
}
};
</script>
<style scoped lang="less">
.box {
display: flex;
align-items: center;
margin: 5px 0;
.box-item {
margin: 0 6px;
max-width: 48px;
}
.box-btn {
margin-right: 12px;
.box-item-action {
width: 12px;
background-color: #F7F8FA;
padding: 15px 0;
text-align: center;
font-size: 12px;
color: #666666;
cursor: pointer;
&:hover {
background-color: #EFF2FE;
color: @primary-color;
}
}
}
}
</style>

View File

@ -8,18 +8,32 @@
</template> </template>
<template #content> <template #content>
<div style="max-width: 400px;" class="ant-form-vertical"> <div style="max-width: 400px;" class="ant-form-vertical">
<j-form-item v-for="item in config.properties" :name="name.concat([item.property])" :label="item.name"> <j-form-item v-for="item in config.properties" :key="item.property" :name="name.concat([item.property])" :label="item.name">
<template v-if='item.type?.type === "string"'> <!-- <template v-if='item.type?.type === "string"'>
<j-input v-model:value='value[item.property]' size="small" :placeholder="`请输入${item.name}`"/> <j-input v-model:value='value[item.property]' size="small" :placeholder="`请输入${item.name}`"/>
</template> </template>
<j-select v-else v-model:value="value[item.property]" :options="item.type?.elements?.map((e: { 'text': string, 'value': string }) => ({ <template v-else-if='item.type?.type === "int"'>
<j-input-number style="width: 100%;" v-model:value='value[item.property]' size="small" :placeholder="`请输入${item.name}`"/>
</template>
<j-select v-else :mode="item.type?.multi ? 'multiple' : ''" v-model:value="value[item.property]" :options="item.type?.elements?.map((e: { 'text': string, 'value': string }) => ({
label: e.text, label: e.text,
value: e.value, value: e.value,
}))" size="small" :placeholder="`请输入${item.name}`"></j-select> }))" size="small" :placeholder="`请输入${item.name}`"></j-select> -->
<ValueItem
v-model:modelValue="value[item.property]"
:itemType="item.type?.type"
:mode="item?.type?.multi ? 'multiple' : ''"
:options="
item.type?.elements?.map(e => ({
label: e.text,
value: e.value,
}))
"
/>
</j-form-item> </j-form-item>
</div> </div>
</template> </template>
{{ config.name || 存储配置 }} {{ config.name || '存储配置' }}
<AIcon type="EditOutlined" class="item-icon" /> <AIcon type="EditOutlined" class="item-icon" />
</j-popover> </j-popover>
</j-button> </j-button>

View File

@ -0,0 +1,26 @@
<template>
<j-modal
:mask-closable="false"
visible width="70vw"
title="设置属性规则"
@cancel="handleCancel"
@ok="handleOk"
>
</j-modal>
</template>
<script setup lang="ts" name="RuleModal">
const handleCancel = () => {
}
const handleOk = () => {
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,161 @@
<template>
<j-popconfirm-modal @confirm="confirm" bodyStyle="width: 450px;height: 300px">
<template #content>
<j-scrollbar>
<j-form ref="formRef" layout="vertical" :model="formData">
<ReadType v-model:value="formData.type" :disabled="true" />
<j-form-item name="promi">
<template #label>
触发属性
<j-popover>
<template #title>
<div>选择当前产品物模型下的属性作为触发属性</div>
<div>任意属性值更新时将触发下方计算规则</div>
</template>
<AIcon style="padding-left: 4px" type="icon-bangzhu" />
</j-popover>
</template>
<j-select />
</j-form-item>
<j-form-item label="计算规则">
<div class="rule-add" @click="showRuleWindow">
编辑规则
</div>
</j-form-item>
<j-form-item label="窗口" :name="['virtualRule', 'windowType']" required>
<j-select
v-model:value="formData.virtualRule.windowType"
:options="[
{ label: '无', value: 'undefined' },
{ label: '时间窗口', value: 'time' },
{ label: '频次窗口', value: 'num' },
]"
/>
</j-form-item>
<template v-if="showWindow">
<j-form-item label="聚合函数" :name="['virtualRule', 'aggType']">
<j-select
v-model:value="formData.virtualRule.aggType"
:options="[
{ label: '时间窗口', value: 'time' },
{ label: '频次窗口', value: 'num' },
]"
placeholder="请选择聚合函数"
/>
</j-form-item>
<j-form-item :name="['virtualRule', 'window', 'span']">
<template #label>
窗口长度({{ formData.virtualRule.aggType === 'num' ? '次' : 's' }})
</template>
<j-input-number v-model:value="formData.virtualRule.window.span" style="width: 100%" />
</j-form-item>
<j-form-item :name="['virtualRule', 'window', 'every']">
<template #label>
步长({{ formData.virtualRule.aggType === 'num' ? '次' : 's' }})
</template>
<j-input-number v-model:value="formData.virtualRule.window.every" style="width: 100%" />
</j-form-item>
</template>
</j-form>
</j-scrollbar>
</template>
<j-button style="padding: 4px 8px;">
<AIcon type="EditOutlined" />
</j-button>
</j-popconfirm-modal>
<Modal
v-if="visible"
@ok="ruleOk"
@cancel="ruleCancel"
/>
</template>
<script setup lang="ts" name="Rule">
import { ReadType } from '../components'
import Modal from './Modal.vue'
type Emit = {
(e: 'update:value', data: Record<string, any>): void
}
const props = defineProps({
value: {
type: Object,
default: () => ({})
}
})
const emit = defineEmits<Emit>()
const formRef = ref<any>(null)
const visible = ref(false)
const formData = reactive<{
type?: string[]
virtualRule: Record<string, any>
}>({
type: ['report'],
virtualRule: {
windowType: 'undefined',
aggType: undefined,
isVirtualRule: false,
type: undefined,
window: {
every: undefined,
span: undefined
},
}
})
const showWindow = computed(() => {
const hasWindowType = formData.virtualRule.windowType !== 'undefined'
if (!hasWindowType) {
formData.virtualRule.window = {
every: undefined,
span: undefined
}
formData.virtualRule.aggType = undefined
}
formData.virtualRule.isVirtualRule = hasWindowType
formData.virtualRule.type = hasWindowType ? 'window' : 'script'
return hasWindowType
})
const confirm = () => {
return new Promise(async (resolve, reject) => {
const data = await formRef.value!.validate().catch(() => {
reject()
})
if (data) {
emit('update:value', formData)
resolve(true)
}
})
}
const showRuleWindow = () => {
visible.value = true
}
const ruleCancel = () => {
visible.value = false
}
const ruleOk = () => {
}
watch(() => props.value, () => {
Object.assign(formData, props.value)
}, { immediate: true })
</script>
<style scoped>
.rule-add {
padding: 8px;
width: 100%;
text-align: center;
border:1px solid rgba(0,0,0,.3);
}
</style>

View File

@ -0,0 +1,3 @@
import Rule from './Rule.vue'
export default Rule

View File

@ -24,12 +24,12 @@
<j-form-item :label="spanLabel" :name="name.concat(['window', 'span'])" :rules="[ <j-form-item :label="spanLabel" :name="name.concat(['window', 'span'])" :rules="[
{ required: true, message: '请输入窗口长度' }, { required: true, message: '请输入窗口长度' },
]"> ]">
<j-input-number v-model:value="value.window.span" size="small" style="width: 100%;"></j-input-number> <j-input-number stringMode v-model:value="value.window.span" size="small" style="width: 100%;"></j-input-number>
</j-form-item> </j-form-item>
<j-form-item :label="everyLabel" :name="name.concat(['window', 'every'])" :rules="[ <j-form-item :label="everyLabel" :name="name.concat(['window', 'every'])" :rules="[
{ required: true, message: '请输入步长' }, { required: true, message: '请输入步长' },
]"> ]">
<j-input-number v-model:value="value.window.every" size="small" style="width: 100%;"></j-input-number> <j-input-number stringMode :maxlength="10" v-model:value="value.window.every" size="small" style="width: 100%;"></j-input-number>
</j-form-item> </j-form-item>
</template> </template>
</template> </template>

View File

@ -0,0 +1,56 @@
<template>
<j-form-item name="type" label="读写类型" required>
<j-select
v-model:value="myValue"
mode="multiple"
:options="options"
:disabled="disabled"
placeholder="请选择读写类型"
@change="onChange"
/>
</j-form-item>
</template>
<script setup lang="ts" name="ReadType">
import type {PropType} from "vue";
type Emit = {
(e: 'update:value', data: Array<string>): void
(e: 'change', data: Array<string>): void
}
const props = defineProps({
disabled: {
type: Boolean,
default: false
},
value: {
type: Array as PropType<Array<string>>,
default: () => []
},
options: {
type: Array as PropType<Array<{label: string, value: string}>>,
default: () => []
}
})
const emit = defineEmits<Emit>()
const myValue = ref<Array<string>>([])
const onChange = (keys: Array<string>) =>{
myValue.value = keys
emit('update:value', keys)
emit('change', keys)
}
watch(() => props.value, () => {
myValue.value = props.value
}, { immediate: true})
</script>
<style scoped>
</style>

View File

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

View File

@ -30,9 +30,21 @@
</j-space> </j-space>
<div style="margin-top: 20px" v-if="importLoading"> <div style="margin-top: 20px" v-if="importLoading">
<j-badge v-if="flag" status="processing" text="进行中" /> <j-badge v-if="flag" status="processing" text="进行中" />
<j-badge v-else status="success" text="已完成" /> <div v-else>
<span>总数量{{ count }}</span> <div>
<p style="color: red">{{ errMessage }}</p> <j-space size="large">
<j-badge status="success" text="已完成" />
<span>总数量{{ count }}</span>
</j-space>
</div>
<div>
<j-space size="large">
<j-badge status="error" text="失败&emsp;" />
<span>总数量{{ failCount }}</span>
<a :href="detailFile" v-if="failCount">下载</a>
</j-space>
</div>
</div>
</div> </div>
</template> </template>
@ -46,7 +58,6 @@ import {
templateDownload, templateDownload,
} from '@/api/device/instance'; } from '@/api/device/instance';
import { EventSourcePolyfill } from 'event-source-polyfill'; import { EventSourcePolyfill } from 'event-source-polyfill';
import { message } from 'jetlinks-ui-components';
type Emits = { type Emits = {
(e: 'update:modelValue', data: string[]): void; (e: 'update:modelValue', data: string[]): void;
@ -86,7 +97,9 @@ const props = defineProps({
const importLoading = ref<boolean>(false); const importLoading = ref<boolean>(false);
const flag = ref<boolean>(false); const flag = ref<boolean>(false);
const count = ref<number>(0); const count = ref<number>(0);
const failCount = ref(0);
const errMessage = ref<string>(''); const errMessage = ref<string>('');
const detailFile = ref('');
const downFile = async (type: string) => { const downFile = async (type: string) => {
const res: any = await templateDownload(props.product, type); const res: any = await templateDownload(props.product, type);
@ -113,6 +126,7 @@ const beforeUpload = (_file: any) => {
const submitData = async (fileUrl: string) => { const submitData = async (fileUrl: string) => {
if (!!fileUrl) { if (!!fileUrl) {
count.value = 0; count.value = 0;
failCount.value = 0;
errMessage.value = ''; errMessage.value = '';
flag.value = true; flag.value = true;
const autoDeploy = !!props?.file?.autoDeploy || false; const autoDeploy = !!props?.file?.autoDeploy || false;
@ -127,8 +141,11 @@ const submitData = async (fileUrl: string) => {
const temp = res.result.total; const temp = res.result.total;
dt += temp; dt += temp;
count.value = dt; count.value = dt;
} else { } else if(!res.success && !res.detailFile) {
failCount.value++;
errMessage.value = res.message || '失败'; errMessage.value = res.message || '失败';
} else if(res.detailFile) {
detailFile.value = res.detailFile;
} }
}; };
source.onerror = (e: { status: number }) => { source.onerror = (e: { status: number }) => {
@ -138,7 +155,7 @@ const submitData = async (fileUrl: string) => {
}; };
source.onopen = () => {}; source.onopen = () => {};
} else { } else {
message.error('请先上传文件'); onlyMessage('请先上传文件', 'error');
} }
}; };

View File

@ -84,7 +84,7 @@ const props = defineProps({
type: Object as PropType<PopconfirmProps>, type: Object as PropType<PopconfirmProps>,
}, },
hasPermission: { hasPermission: {
type: String || Array || Boolean, type: [String , Array, Boolean],
}, },
style: { style: {
type: Object as PropType<CSSProperties> type: Object as PropType<CSSProperties>

View File

@ -123,7 +123,7 @@
@click="playerActive = index" @click="playerActive = index"
> >
<div <div
class="media-btn-refresh" class="media-btn-refresh refreshBtn"
:style="{ :style="{
display: item.url ? 'block' : 'none', display: item.url ? 'block' : 'none',
}" }"
@ -149,9 +149,9 @@ import {
getSearchHistory, getSearchHistory,
saveSearchHistory, saveSearchHistory,
} from '@/api/comm'; } from '@/api/comm';
import { message } from 'jetlinks-ui-components';
import LivePlayer from '@/components/Player/index.vue'; import LivePlayer from '@/components/Player/index.vue';
import MediaTool from '@/components/Player/mediaTool.vue'; import MediaTool from '@/components/Player/mediaTool.vue';
import { onlyMessage } from '@/utils/comm';
type Player = { type Player = {
id?: string; id?: string;
@ -347,10 +347,10 @@ const saveHistory = async () => {
if (res.success) { if (res.success) {
visible.value = false; visible.value = false;
getHistory(); getHistory();
message.success('保存成功'); onlyMessage('保存成功');
formRef.value.resetFields(); formRef.value.resetFields();
} else { } else {
message.error('保存失败'); onlyMessage('保存失败', 'error');
} }
}) })
.catch((err: any) => { .catch((err: any) => {
@ -458,4 +458,16 @@ defineExpose({
<style lang="less" scoped> <style lang="less" scoped>
@import './index.less'; @import './index.less';
:deep(.live-player-stretch-btn){
display: none;
}
:deep(.vjs-icon-spinner){
display: none;
}
.refreshBtn{
opacity: 0;
}
.refreshBtn:hover{
opacity: 1;
}
</style> </style>

View File

@ -86,3 +86,11 @@ defineExpose({
paused, paused,
}); });
</script> </script>
<style lang="less" scoped>
:deep(.live-player-stretch-btn){
display: none;
}
:deep(.vjs-icon-spinner){
display: none;
}
</style>

View File

@ -11,11 +11,8 @@
layout === 'horizontal' layout === 'horizontal'
? 'm-radio-checked-item' ? 'm-radio-checked-item'
: 'm-radio-item', : 'm-radio-item',
{ active: myValue === item.value },
checkStyle && myValue === item.value ? 'checked' : '', checkStyle && myValue === item.value ? 'checked' : '',
disabled && myValue === item.value { active: myValue === item.value },
? 'active-checked-disabled'
: '',
item.disabled ? 'disabled' : '', item.disabled ? 'disabled' : '',
]" ]"
v-for="(item, index) in options" v-for="(item, index) in options"
@ -100,9 +97,12 @@ const handleRadio = (item: any) => {
flex-wrap: wrap; flex-wrap: wrap;
justify-content: space-between; justify-content: space-between;
.disabled { .disabled {
color: rgba(0, 0, 0, 0.25); >div {
border-color: #f5f5f5; color: rgba(0, 0, 0, 0.25);
cursor: not-allowed; border-color: #f5f5f5;
cursor: not-allowed;
}
} }
&-item { &-item {
width: 49%; width: 49%;
@ -152,14 +152,14 @@ const handleRadio = (item: any) => {
} }
} }
.disabled { //.disabled {
color: rgba(0, 0, 0, 0.25) !important; // color: rgba(0, 0, 0, 0.25) !important;
cursor: not-allowed; // cursor: not-allowed;
} //}
.active-checked-disabled { //.active-checked-disabled {
color: rgba(0, 0, 0, 0.25) !important; // color: rgba(0, 0, 0, 0.25) !important;
border: 1px #d9d9d9 solid !important; // border: 1px #d9d9d9 solid !important;
} //}
.checked-icon-disabled { .checked-icon-disabled {
color: rgba(0, 0, 0, 0.25) !important; color: rgba(0, 0, 0, 0.25) !important;
border-color: #e6e6e6 !important; border-color: #e6e6e6 !important;
@ -169,17 +169,25 @@ const handleRadio = (item: any) => {
.m-radio { .m-radio {
display: flex; display: flex;
gap: 16px;
&.disabled {
>div {
opacity: 0.7;
cursor: not-allowed;
}
}
&-item { &-item {
width: 140px; width: 140px;
height: 140px; height: 140px;
padding: 10px 15px; padding: 10px 16px;
margin-right: 15px;
border: 1px solid #d9d9d9; border: 1px solid #d9d9d9;
border-radius: 2px; border-radius: 2px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 5px; gap: 6px;
cursor: pointer; cursor: pointer;
.img { .img {
width: 100px; width: 100px;
@ -189,6 +197,8 @@ const handleRadio = (item: any) => {
color: #1d39c4; color: #1d39c4;
border-color: #1d39c4; border-color: #1d39c4;
} }
} }
} }
</style> </style>

View File

@ -2,9 +2,9 @@
<j-advanced-search <j-advanced-search
:target='target' :target='target'
:type='type' :type='type'
:request='saveSearchHistory' :request='(data) => saveSearchHistory(data, target)'
:historyRequest='getSearchHistory' :historyRequest='() => getSearchHistory(target)'
:deleteRequest='deleteSearchHistory' :deleteRequest='(_target: string, id: string) => deleteSearchHistory(target, id)'
:columns='columns' :columns='columns'
:class='props.class' :class='props.class'
style='padding-top: 18px; padding-bottom: 18px;' style='padding-top: 18px; padding-bottom: 18px;'

View File

@ -0,0 +1,71 @@
<template>
<j-modal
:title="title"
visible
:width="400"
@cancel="cancel"
@ok="ok"
:confirmLoading="loading"
>
<div style="height: 300px; width: 100%;">
<vue-cropper
ref="cropper"
:img="img"
:fixed-box="true"
:autoCrop="true"
:auto-crop-width="200"
:auto-crop-height="200"
outputType="jpg"
></vue-cropper>
</div>
</j-modal>
</template>
<script setup lang="ts" name="UploadCropper">
import 'vue-cropper/dist/index.css'
import { VueCropper } from 'vue-cropper';
import { fileUpload } from '@/api/comm';
const props = defineProps({
img: {
type: String
},
title: {
type: String,
default: '图片编辑'
}
})
const emit = defineEmits(['cancel', 'ok'])
const imgUrl = ref()
const cropper = ref()
const loading = ref(false)
const ok = () => {
cropper.value.getCropBlob(async (data: Blob) => {
console.log(data)
let formData = new FormData()
formData.append('file', data, new Date().getTime() + '.jpg')
imgUrl.value = data
loading.value = true
fileUpload(formData).then(res => {
if (res.success) {
emit('ok', res.result)
}
}).finally(() => {
loading.value = false
})
})
}
const cancel = () => {
emit('cancel')
}
</script>
<style scoped>
</style>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="upload-image-warp"> <div class="upload-image-warp">
<div class="upload-image-border"> <div class="upload-image-border" :style="borderStyle">
<j-upload <j-upload
name="file" name="file"
list-type="picture-card" list-type="picture-card"
@ -16,13 +16,7 @@
> >
<div class="upload-image-content" :style="props.style"> <div class="upload-image-content" :style="props.style">
<template v-if="imageUrl"> <template v-if="imageUrl">
<!-- <div class="upload-image" <img :src="imageUrl" width="100%" class="upload-image" />
:style="{
backgroundSize: props.backgroundSize,
backgroundImage: `url(${imageUrl})`
}"
></div> -->
<img :src="imageUrl" class="upload-image" />
<div class="upload-image-mask">点击修改</div> <div class="upload-image-mask">点击修改</div>
</template> </template>
<template v-else> <template v-else>
@ -52,15 +46,21 @@
</div> </div>
</div> </div>
</div> </div>
<ImageCropper
v-if="cropperVisible"
:img="cropperImg"
@cancel="cropperVisible = false"
@ok="saveImage"
/>
</template> </template>
<script lang="ts" setup name='JProUpload'> <script lang="ts" setup name='JProUpload'>
import { UploadChangeParam, UploadProps } from 'ant-design-vue'; import { UploadChangeParam, UploadProps } from 'ant-design-vue';
import { message } from 'jetlinks-ui-components';
import { FILE_UPLOAD } from '@/api/comm'; import { FILE_UPLOAD } from '@/api/comm';
import { TOKEN_KEY } from '@/utils/variable'; import { TOKEN_KEY } from '@/utils/variable';
import { LocalStore } from '@/utils/comm'; import {getBase64, LocalStore, onlyMessage} from '@/utils/comm';
import { CSSProperties } from 'vue'; import { CSSProperties } from 'vue';
import ImageCropper from './Cropper.vue';
type Emits = { type Emits = {
(e: 'update:modelValue', data: string): void; (e: 'update:modelValue', data: string): void;
@ -73,6 +73,7 @@ interface JUploadProps extends UploadProps {
size?: number; size?: number;
style?: CSSProperties; style?: CSSProperties;
bgImage?: string; bgImage?: string;
borderStyle?:CSSProperties;
} }
const emit = defineEmits<Emits>(); const emit = defineEmits<Emits>();
@ -93,6 +94,14 @@ const props: JUploadProps = defineProps({
accept:{ accept:{
type: String, type: String,
default: undefined default: undefined
},
borderStyle: {
type: Object,
default: undefined
},
size: {
type: Number,
default: undefined,
} }
}); });
@ -100,6 +109,9 @@ const loading = ref<boolean>(false);
const imageUrl = ref<string>(props?.modelValue || ''); const imageUrl = ref<string>(props?.modelValue || '');
const imageTypes = props.types ? props.types : ['image/jpeg', 'image/png']; const imageTypes = props.types ? props.types : ['image/jpeg', 'image/png'];
const cropperImg = ref()
const cropperVisible = ref(false)
watch( watch(
() => props.modelValue, () => props.modelValue,
(newValue) => { (newValue) => {
@ -123,26 +135,41 @@ const handleChange = (info: UploadChangeParam) => {
} }
if (info.file.status === 'error') { if (info.file.status === 'error') {
loading.value = false; loading.value = false;
message.error('上传失败'); onlyMessage('上传失败', 'error');
} }
}; };
const beforeUpload = (file: UploadProps['fileList'][number]) => { const beforeUpload = (file: UploadProps['fileList'][number]) => {
const isType = imageTypes.includes(file.type); const isType = imageTypes.includes(file.type);
const maxSize = props.size || 2 //
if (!isType) { if (!isType) {
if (props.errorMessage) { if (props.errorMessage) {
message.error(props.errorMessage); onlyMessage(props.errorMessage, 'error');
} else { } else {
message.error(`请上传正确格式的图片`); onlyMessage(`请上传正确格式的图片`, 'error');
} }
return false; return false;
} }
const isSize = file.size / 1024 / 1024 < (props.size || 4); const isSize = file.size / 1024 / 1024 < maxSize;
if (!isSize) { if (!isSize) {
message.error(`图片大小必须小于${props.size || 4}M`); onlyMessage(`图片大小必须小于${maxSize}M`, 'error');
return false
} }
return isType && isSize;
getBase64(file, (base64Url) => {
cropperImg.value = base64Url
cropperVisible.value = true
})
return false;
}; };
const saveImage = (url: string) => {
cropperVisible.value = false
imageUrl.value = url
emit('update:modelValue', url);
}
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@ -3,6 +3,7 @@
<div class="value-item-warp"> <div class="value-item-warp">
<j-select <j-select
v-if="typeMap.get(itemType) === 'select'" v-if="typeMap.get(itemType) === 'select'"
:mode="mode"
v-model:value="myValue" v-model:value="myValue"
:options="options" :options="options"
allowClear allowClear
@ -77,6 +78,7 @@
/> />
<j-input <j-input
v-else v-else
:placeholder="placeholder"
allowClear allowClear
type="text" type="text"
v-model:value="myValue" v-model:value="myValue"
@ -139,6 +141,15 @@ const props = defineProps({
type: Array as PropType<DefaultOptionType[]>, type: Array as PropType<DefaultOptionType[]>,
default: () => [], default: () => [],
}, },
//
mode: {
type: String as PropType<'multiple' | 'tags' | 'combobox' | ''>,
default: ''
},
placeholder: {
type: String,
default: () => '',
}
}); });
// type Props = { // type Props = {
// itemData?: Object; // itemData?: Object;

View File

@ -9,6 +9,7 @@ import NormalUpload from './NormalUpload/index.vue'
import FileFormat from './FileFormat/index.vue' import FileFormat from './FileFormat/index.vue'
import JProUpload from './Upload/index.vue' import JProUpload from './Upload/index.vue'
import { BasicLayoutPage, BlankLayoutPage, FullPage } from './Layout' import { BasicLayoutPage, BlankLayoutPage, FullPage } from './Layout'
import RadioCard from './RadioCard/index.vue'
import { PageContainer, AIcon, Ellipsis } from 'jetlinks-ui-components' import { PageContainer, AIcon, Ellipsis } from 'jetlinks-ui-components'
// import Ellipsis from './Ellipsis/index.vue' // import Ellipsis from './Ellipsis/index.vue'
import JEmpty from './Empty/index.vue' import JEmpty from './Empty/index.vue'
@ -39,5 +40,6 @@ export default {
.component('ValueItem', ValueItem) .component('ValueItem', ValueItem)
.component('RowPagination', RowPagination) .component('RowPagination', RowPagination)
.component('FullPage', FullPage) .component('FullPage', FullPage)
.component('RadioCard', RadioCard)
} }
} }

View File

@ -23,34 +23,34 @@ export const AccountMenu = {
name: 'account/center', name: 'account/center',
code: 'account/center', code: 'account/center',
meta: { meta: {
title: '基本设置', title: '个人中心',
icon: '', icon: '',
hideInMenu: false hideInMenu: true
}, },
component: () => import('@/views/account/Center/index.vue') component: () => import('@/views/account/Center/index.vue')
}, },
{ // {
path: '/account/NotificationSubscription', // path: '/account/NotificationSubscription',
name: 'account/NotificationSubscription', // name: 'account/NotificationSubscription',
code: 'account/NotificationSubscription', // code: 'account/NotificationSubscription',
meta: { // meta: {
title: '通知订阅', // title: '通知订阅',
icon: '', // icon: '',
hideInMenu: false // hideInMenu: false
}, // },
component: () => import('@/views/account/NotificationSubscription/index.vue') // component: () => import('@/views/account/NotificationSubscription/index.vue')
}, // },
{ // {
path: '/account/NotificationRecord', // path: '/account/NotificationRecord',
name: 'account/NotificationRecord', // name: 'account/NotificationRecord',
code: 'account/NotificationRecord', // code: 'account/NotificationRecord',
meta: { // meta: {
title: '通知记录', // title: '通知记录',
icon: '', // icon: '',
hideInMenu: false // hideInMenu: false
}, // },
component: () => import('@/views/account/NotificationRecord/index.vue') // component: () => import('@/views/account/NotificationRecord/index.vue')
}, // },
] ]
} }
@ -78,6 +78,13 @@ export default [
title: '授权页' title: '授权页'
}, },
component: () => import('@/views/oauth/index.vue') component: () => import('@/views/oauth/index.vue')
} },
{
path: '/oauth/wechat',
meta: {
title: '微信授权页'
},
component: () => import('@/views/oauth/WeChat.vue')
},
AccountMenu
] ]

View File

@ -5,8 +5,8 @@ import { cloneDeep, isArray } from 'lodash-es'
import { usePermissionStore } from './permission' import { usePermissionStore } from './permission'
import router from '@/router' import router from '@/router'
import { onlyMessage } from '@/utils/comm' import { onlyMessage } from '@/utils/comm'
import { AccountMenu, NotificationRecordCode, NotificationSubscriptionCode } from '@/router/menu' // import { AccountMenu, NotificationRecordCode, NotificationSubscriptionCode } from '@/router/menu'
import { MESSAGE_SUBSCRIBE_MENU_CODE, USER_CENTER_MENU_CODE } from '@/utils/consts' import { USER_CENTER_MENU_CODE } from '@/utils/consts'
import {isNoCommunity} from "@/utils/utils"; import {isNoCommunity} from "@/utils/utils";
const defaultOwnParams = [ const defaultOwnParams = [
@ -90,6 +90,12 @@ export const useMenuStore = defineStore({
console.warn(`没有找到对应的页面: ${name}`) console.warn(`没有找到对应的页面: ${name}`)
} }
}, },
routerPush(name: string, params?: Record<string, any>, query?: Record<string, any>) {
this.params = { [name]: params || {}}
router.push({
name, params, query, state: { params }
})
},
queryMenuTree(isCommunity = false): Promise<any[]> { queryMenuTree(isCommunity = false): Promise<any[]> {
return new Promise(async (res) => { return new Promise(async (res) => {
//过滤非集成的菜单 //过滤非集成的菜单
@ -103,12 +109,7 @@ export const useMenuStore = defineStore({
permission.permissions = {} permission.permissions = {}
const { menusData, silderMenus } = filterAsyncRouter(resultData) const { menusData, silderMenus } = filterAsyncRouter(resultData)
// 是否存在通知订阅 this.menus = findCodeRoute([...resultData]) // AccountMenu
const hasMessageSub = resultData.some((item: { code: string }) => item.code === MESSAGE_SUBSCRIBE_MENU_CODE)
if (!hasMessageSub) {
AccountMenu.children = AccountMenu.children.filter((item: { code: string }) => ![NotificationSubscriptionCode, NotificationRecordCode].includes(item.code) )
}
this.menus = findCodeRoute([...resultData, AccountMenu])
Object.keys(this.menus).forEach((item) => { Object.keys(this.menus).forEach((item) => {
const _item = this.menus[item] const _item = this.menus[item]
if (_item.buttons?.length) { if (_item.buttons?.length) {
@ -123,8 +124,8 @@ export const useMenuStore = defineStore({
hideInMenu: true hideInMenu: true
} }
}) })
menusData.push(AccountMenu) // menusData.push(AccountMenu)
this.siderMenus = silderMenus.filter((item: { name: string }) => ![USER_CENTER_MENU_CODE, MESSAGE_SUBSCRIBE_MENU_CODE].includes(item.name)) this.siderMenus = silderMenus.filter((item: { name: string }) => ![USER_CENTER_MENU_CODE].includes(item.name))
res(menusData) res(menusData)
} }
}) })

View File

@ -21,7 +21,8 @@ export const useMetadataStore = defineStore({
action: 'add', action: 'add',
import: false, import: false,
importMetadata: false, importMetadata: false,
} as MetadataModelType } as MetadataModelType,
tabActiveKey: 'properties',
}), }),
actions: { actions: {
set(key: string, value: any) { set(key: string, value: any) {

View File

@ -7,6 +7,7 @@ type RuleEditorType = {
log: { log: {
content: string; content: string;
time: number; time: number;
_time: number;
}[]; }[];
}; };

View File

@ -9,13 +9,23 @@ import { SystemConst } from '@/utils/consts'
type SystemStateType = { type SystemStateType = {
isCommunity: boolean; isCommunity: boolean;
configInfo: Partial<ConfigInfoType>; configInfo: Partial<ConfigInfoType>;
layout:{
siderWidth: string | number | undefined; // 左侧菜单栏宽度
headerHeight: string | number | undefined; // 头部高度
collapsedWidth: string | number | undefined;
}
} }
export const useSystem = defineStore('system', { export const useSystem = defineStore('system', {
state: (): SystemStateType => ({ state: (): SystemStateType => ({
isCommunity: false, isCommunity: false,
// configInfo: [] as any[] // configInfo: [] as any[]
configInfo: {} configInfo: {},
layout:{
siderWidth: 208, // 左侧菜单栏宽度
headerHeight: 60, // 头部高度
collapsedWidth: 48,
}
}), }),
actions: { actions: {
getSystemVersion(): Promise<any[]> { getSystemVersion(): Promise<any[]> {

View File

@ -19,8 +19,19 @@ export const useUserInfo = defineStore('userInfo', {
roles: [], roles: [],
token: '', token: '',
user: {}, user: {},
name: '',
orgList: [],
roleList: [],
telephone: '',
email: '',
avatar: ''
}, },
alarmUpdateCount: 0 alarmUpdateCount: 0,
tabKey: 'HomeView', // 个人中心的tabKey,
messageInfo: {}, // 站内信的row
other: {
tabKey: '' // 站内信的tabkey
}
}), }),
actions: { actions: {

View File

@ -175,3 +175,13 @@ export const openKeysByTree = (data: any[], search: any, searchKey: string = 'id
findKey(filterTree) findKey(filterTree)
return openKeys return openKeys
} }
export const getBase64 = (img: File, callback: (base64Url: string) => void) => {
const reader = new FileReader();
reader.readAsDataURL(img);
reader.onload = (result: any) => {
console.log(result)
callback(result.target.result)
}
}

View File

@ -1,5 +1,3 @@
import { MESSAGE_SUBSCRIBE_MENU_DATA } from '@/views/init-home/data/baseMenu'
/** /**
* *
*/ */
@ -52,5 +50,9 @@ export const SystemConst = {
export const USER_CENTER_MENU_CODE = 'account-center' export const USER_CENTER_MENU_CODE = 'account-center'
export const USER_CENTER_MENU_BUTTON_CODE = 'user-center-passwd-update' export const USER_CENTER_MENU_BUTTON_CODE = 'user-center-passwd-update'
export const MESSAGE_SUBSCRIBE_MENU_CODE = 'message-subscribe'
export const MESSAGE_SUBSCRIBE_MENU_BUTTON_CODE = 'message-subscribe-view' /**协议列表 */
export const protocolList = [
{ label: 'OPC-UA', value: 'OPC_UA', alias: 'opc-ua' },
{ label: 'Modbus/TCP', value: 'MODBUS_TCP', alias: 'modbus-tcp' },
]

11
src/utils/document.ts Normal file
View File

@ -0,0 +1,11 @@
export const createScript = (src: string) => {
return new Promise((resolve) => {
const script = document.createElement('script')
script.onload = () => {
resolve(true)
}
script.setAttribute('type', 'text/javascript')
script.setAttribute('src', src)
document.body.appendChild(script)
})
}

7
src/utils/encrypt.ts Normal file
View File

@ -0,0 +1,7 @@
import JSEncrypt from "jsencrypt";
export const encrypt =(txt:string,publicKey:string)=>{
const encryptor = new JSEncrypt()
encryptor.setPublicKey(publicKey)
return encryptor.encrypt(txt)
}

View File

@ -134,6 +134,9 @@ export const postStream = function(url: string, data = {}, params = {}) {
const showNotification = (message: string, description: string, key?: string, show: boolean = true) => { const showNotification = (message: string, description: string, key?: string, show: boolean = true) => {
if (show) { if (show) {
Notification.error({ Notification.error({
style: {
zIndex: 1040
},
key, key,
message: '', message: '',
description description

View File

@ -145,3 +145,41 @@ export const ArrayToTree = (list: any[]): any[] => {
// 返回出去 // 返回出去
return treeList; return treeList;
}; };
export const EventEmitter = {
list: {},
subscribe: function(events: string[], fn: Function) {
const list = this.list
events.forEach(event => {
(list[event] || (list[event] = [])).push(fn)
})
return this
},
emit: function(events:string, data?: any) {
const list = this.list
const fns: Function[] = list[events] ? [...list[events]] : []
if (!fns.length) return false;
fns.forEach(fn => {
fn(data)
})
return this
},
unSubscribe: function(events:string[], fn: Function) {
const list = this.list
events.forEach(key => {
if (key in list) {
const fns = list[key]
for (let i = 0; i < fns.length; i++) {
if (fns[i] === fn) {
fns.splice(i, 1)
break;
}
}
}
})
return this
}
}

View File

@ -43,7 +43,9 @@ export const initWebSocket = () => {
const data = JSON.parse(msg.data) const data = JSON.parse(msg.data)
if (data.type === 'error') { if (data.type === 'error') {
notification.error({ key: 'wserr', message: data.message }) notification.error({ key: 'wserr', message: data.message, style: {
zIndex: 1040
} })
} }
if (subs[data.requestId]) { if (subs[data.requestId]) {

View File

@ -197,6 +197,7 @@ import { FormValidate, FormState } from '../data';
import type { FormInstance } from 'ant-design-vue'; import type { FormInstance } from 'ant-design-vue';
import type { FormDataType } from '../type.d'; import type { FormDataType } from '../type.d';
import { cloneDeep, isArray } from 'lodash-es'; import { cloneDeep, isArray } from 'lodash-es';
import { protocolList } from '@/utils/consts';
const props = defineProps({ const props = defineProps({
data: { data: {
@ -281,20 +282,16 @@ const getCertificateList = async () => {
const getProvidersList = async () => { const getProvidersList = async () => {
const resp: any = await getProviders(); const resp: any = await getProviders();
if (resp.status === 200) { if (resp.status === 200) {
const list = [
{ label: 'OPC UA', value: 'OPC_UA' },
{ label: 'Modbus TCP', value: 'MODBUS_TCP' },
];
const arr = resp.result const arr = resp.result
.filter( .filter(
(item: any) => item.id === 'modbus-tcp' || item.id === 'opc-ua', (item: any) => item.id === 'modbus-tcp' || item.id === 'opc-ua',
) )
.map((it: any) => (it?.id === 'opc-ua' ? 'OPC_UA' : 'MODBUS_TCP')); .map((it: any) => it.id);
const providers: any = list.filter((item: any) => const providers: any = protocolList.filter((item: any) =>
arr.includes(item.value), arr.includes(item.alias),
); );
providersList.value = providers; providersList.value = providers;
if (arr.includes('OPC_UA')) { if (arr.includes('opc-ua')) {
getOptionsList(); getOptionsList();
} }
} }

View File

@ -50,9 +50,19 @@ export const regDomain = new RegExp(
); );
export const checkEndpoint = (_rule: Rule, value: string): Promise<any> => export const checkEndpoint = (_rule: Rule, value: string): Promise<any> =>
new Promise(async (resolve, reject) => { new Promise(async (resolve, reject) => {
if(!value) return resolve('');
const res: any = await validateField(value); const res: any = await validateField(value);
return res.result.passed ? resolve('') : reject(res.result.reason); return res.result.passed ? resolve('') : reject(res.result.reason);
}); });
export const checkHost = (_rule: Rule, value: string): Promise<any> =>
new Promise(async (resolve, reject) => {
if(!value) return resolve('');
if(!(regIP.test(value) || regIPv6.test(value) || regDomain.test(value))) {
return reject('请输入正确格式的Modbus主机IP地址')
}
return resolve('')
});
export const FormValidate = { export const FormValidate = {
name: [ name: [
{ required: true, message: '请输入名称', trigger: 'blur' }, { required: true, message: '请输入名称', trigger: 'blur' },
@ -65,8 +75,9 @@ export const FormValidate = {
message: '请输入Modbus主机IP', message: '请输入Modbus主机IP',
}, },
{ {
pattern: regIP || regIPv6 || regDomain, validator: checkHost,
message: '请输入正确格式的Modbus主机IP地址', trigger: 'blur',
// message: '请输入正确格式的Modbus主机IP地址',
}, },
], ],
port: [ port: [

View File

@ -3,7 +3,7 @@
<div> <div>
<pro-search <pro-search
:columns="columns" :columns="columns"
target="search" target="search-datacollect-channel"
@search="handleSearch" @search="handleSearch"
/> />
<FullPage> <FullPage>
@ -73,7 +73,7 @@
</j-tooltip> </j-tooltip>
</div> </div>
</j-col> </j-col>
<j-col :span="12"> <!-- <j-col :span="12">
<div class="card-item-content-text"> <div class="card-item-content-text">
地址 地址
</div> </div>
@ -98,6 +98,14 @@
> >
</j-tooltip> </j-tooltip>
</div> </div>
</j-col> -->
<j-col :span="12">
<div class="card-item-content-text">
说明
</div>
<div class="card-item-content-text">
<j-ellipsis>{{slotProps.description}}</j-ellipsis>
</div>
</j-col> </j-col>
</j-row> </j-row>
</div> </div>

View File

@ -44,6 +44,7 @@
<j-form-item <j-form-item
label="地址" label="地址"
:name="['pointKey']" :name="['pointKey']"
validateFirst
:rules="[ :rules="[
...ModBusRules.pointKey, ...ModBusRules.pointKey,
{ {
@ -57,7 +58,7 @@
placeholder="请输入地址" placeholder="请输入地址"
v-model:value="formData.pointKey" v-model:value="formData.pointKey"
:min="0" :min="0"
:max="999999999" :max="999999"
:precision="0" :precision="0"
/> />
</j-form-item> </j-form-item>
@ -131,7 +132,7 @@
style="width: 100%" style="width: 100%"
placeholder="请输入小数保留位数" placeholder="请输入小数保留位数"
:min="0" :min="0"
:max="255" :max="65535"
:precision="0" :precision="0"
v-model:value=" v-model:value="
formData.configuration.codec.configuration.scale formData.configuration.codec.configuration.scale
@ -220,7 +221,8 @@
placeholder="请输入采集频率" placeholder="请输入采集频率"
v-model:value="formData.configuration.interval" v-model:value="formData.configuration.interval"
addon-after="ms" addon-after="ms"
:max="9999999999999998" :max="2147483648"
:min="0"
/> />
</j-form-item> </j-form-item>
@ -378,8 +380,7 @@ const changeWriteByteCount = (value: Array<string>) => {
formData.value.configuration.parameter.writeByteCount = value[0]; formData.value.configuration.parameter.writeByteCount = value[0];
}; };
const changeFunction = (value: string) => { const changeFunction = (value: string) => {
formData.value.accessModes = formData.value.accessModes = ['InputRegisters', 'DiscreteInputs'].includes(value) ? ['read'] : ['read', 'write'];
value === 'InputRegisters' ? ['read'] : ['read', 'write'];
}; };
const checkProvider = (_rule: Rule, value: string): Promise<any> => const checkProvider = (_rule: Rule, value: string): Promise<any> =>

View File

@ -58,7 +58,8 @@
placeholder="请输入采集频率" placeholder="请输入采集频率"
v-model:value="formData.configuration.interval" v-model:value="formData.configuration.interval"
addon-after="ms" addon-after="ms"
:max="9999999999999998" :max="2147483648"
:min="0"
/> />
</j-form-item> </j-form-item>
<j-form-item label="" :name="['features']"> <j-form-item label="" :name="['features']">

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