Commit 55f983a650ef14c16aa6108af2c622a9a1df0600

Authored by ww
2 parents 83f3cf6d 983cac4e

merge: 合并代码解决冲突

... ... @@ -102,7 +102,7 @@ function copyFileUsageOssServer(cb) {
102 102 const outPath = './build/oss'
103 103
104 104 for (const filePath of copyFileList) {
105   - src(path.resolve(__dirname, filePath), {allowEmpty:true})
  105 + src(path.resolve(__dirname, filePath), { allowEmpty: true })
106 106 .pipe((dest(outPath)))
107 107 }
108 108
... ...
... ... @@ -5,7 +5,7 @@
5 5
6 6 <head>
7 7 <!-- <title>Flowchart Maker &amp; Online Diagram Software</title>-->
8   - <title>thingskit 云组态</title>
  8 + <title>云组态</title>
9 9 <meta charset="utf-8">
10 10 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
11 11 <meta name="Description"
... ... @@ -22,26 +22,26 @@
22 22 <meta name="mobile-web-app-capable" content="yes">
23 23 <meta name="theme-color" content="#d89000">
24 24
25   - <link rel="stylesheet" href="./js/plugin/layui/css/layui.css?v=1681890411146">
  25 + <link rel="stylesheet" href="./js/plugin/layui/css/layui.css?v=1687228273654">
26 26
27 27 <!-- load configure file -->
28   - <script src="./js/config/config.js?v=1681890411146"></script>
  28 + <script src="./js/config/config.js?v=1687228273654"></script>
29 29
30 30 <!-- crypto-js -->
31   - <script src="./js/plugin/crypto-js/crypto-js.js?v=1681890411146"></script>
  31 + <script src="./js/plugin/crypto-js/crypto-js.js?v=1687228273654"></script>
32 32
33 33 <!-- storage persistent -->
34   - <script src="./js/const/persistentStorage.js?v=1681890411146"></script>
  34 + <script src="./js/const/persistentStorage.js?v=1687228273654"></script>
35 35 <!-- Global const -->
36   - <script src="./js/const/const.js?v=1681890411146"></script>
  36 + <script src="./js/const/const.js?v=1687228273654"></script>
37 37
38 38 <!-- Axios -->
39   - <script src="./js/plugin/axios/axios.min.js?v=1681890411146"></script>
40   - <script src="./js/plugin/axios/DefHttp.js?v=1681890411146"></script>
41   - <script src="./js/api/index.js?v=1681890411146"></script>
  39 + <script src="./js/plugin/axios/axios.min.js?v=1687228273654"></script>
  40 + <script src="./js/plugin/axios/DefHttp.js?v=1687228273654"></script>
  41 + <script src="./js/api/index.js?v=1687228273654"></script>
42 42
43 43 <!-- load script -->
44   - <script src="./js/config/loadScript.js?v=1681890411146"></script>
  44 + <script src="./js/config/loadScript.js?v=1687228273654"></script>
45 45
46 46 <!-- act editor -->
47 47 <!-- <script src="https://oss.yuntengcloud.com/iotdocs/thingskit-scada/ace.js"></script> -->
... ... @@ -54,7 +54,7 @@
54 54 <!-- <script src="https://vjs.zencdn.net/7.10.2/video.min.js"></script> -->
55 55 <!-- <script src="https://oss.yuntengcloud.com/iotdocs/thingskit-scada/video.min.js"></script> -->
56 56
57   - <script src="./js/plugin/layui/layui.js?v=1681890411146"></script>
  57 + <script src="./js/plugin/layui/layui.js?v=1687228273654"></script>
58 58 <!-- <link rel="stylesheet" href="https://cdnjs.loli.net/ajax/libs/layui/2.6.8/css/layui.min.css"
59 59 integrity="sha512-iQBJbsNHXUcgEIgWThd2dr8tOdKPvICwqjPEZYY81z3eMya44A5MiAqfWSCh+Ee1YzNYkdrI982Qhwgr8LEYOQ=="
60 60 crossorigin="anonymous" referrerpolicy="no-referrer" />
... ... @@ -63,7 +63,7 @@
63 63 crossorigin="anonymous" referrerpolicy="no-referrer"></script> -->
64 64
65 65 <!-- 引入修改样式 -->
66   - <link rel="stylesheet" href="./styles/formatChange.css?v=1681890411146">
  66 + <link rel="stylesheet" href="./styles/formatChange.css?v=1687228273654">
67 67
68 68 <script type="text/javascript">
69 69 /**
... ... @@ -306,7 +306,7 @@
306 306 var supportedDomain = (hostName.substring(hostName.length - 8, hostName.length) === '.draw.io') ||
307 307 (hostName.substring(hostName.length - 13, hostName.length) === '.diagrams.net');
308 308
309   - const releaseVersion = '1681890411146'
  309 + const releaseVersion = '1687228273654'
310 310 const appMinSrc = Enable_OSS ? `${OSS_Prefix}app.min.js?v=${releaseVersion}` : `js/app.min.js?v=${releaseVersion}`
311 311 function loadAppJS() {
312 312 mxscript(appMinSrc, function () {
... ... @@ -349,17 +349,17 @@
349 349 }
350 350 };
351 351 </script>
352   - <link rel="chrome-webstore-item" href="https://chrome.google.com/webstore/detail/plgmlhohecdddhbmmkncjdmlhcmaachm">
353   - <link rel="apple-touch-icon" sizes="180x180" href="images/apple-touch-icon.png">
  352 + <!-- <link rel="chrome-webstore-item" href="https://chrome.google.com/webstore/detail/plgmlhohecdddhbmmkncjdmlhcmaachm"> -->
  353 + <!-- <link rel="apple-touch-icon" sizes="180x180" href="images/apple-touch-icon.png"> -->
354 354 <!-- <link rel="icon" type="image/png" sizes="32x32" href="images/favicon-32x32.png">-->
355 355 <link rel="icon" type="image/png" sizes="32x32" href="images/logo-32x32.png">
356   - <link rel="icon" type="image/png" sizes="16x16" href="images/favicon-16x16.png">
  356 + <!-- <link rel="icon" type="image/png" sizes="16x16" href="images/favicon-16x16.png"> -->
357 357 <!-- <link rel="icon" type="image/png" sizes="16x16" href="images/favicon-16x16.png">-->
358 358 <link rel="mask-icon" href="images/safari-pinned-tab.svg" color="#d89000">
359 359 <link rel="stylesheet" type="text/css" href="styles/grapheditor.css">
360   - <link rel="preconnect" href="https://storage.googleapis.com">
  360 + <!-- <link rel="preconnect" href="https://storage.googleapis.com">
361 361 <link rel="canonical" href="https://app.diagrams.net">
362   - <link rel="manifest" href="images/manifest.json">
  362 + <link rel="manifest" href="images/manifest.json"> -->
363 363 <!-- <link rel="shortcut icon" href="favicon.ico">-->
364 364 <link rel="shortcut icon" href="images/logo-16x16.ico">
365 365 <style type="text/css">
... ... @@ -591,7 +591,6 @@
591 591 <script>
592 592
593 593 function isMobile() {
594   - // http://192.168.10.106:8083/thingskit-drawio/?configurationId=5c683cb0-48fb-4062-aabf-a5093eea0446&userId=1262f76c-ec5d-4091-85a1-52c552915d4e&lightbox=1
595 594 var urlParams = (function () {
596 595 var result = new Object();
597 596 var params = window.location.search.slice(1).split('&');
... ... @@ -649,7 +648,7 @@
649 648 left: 'center',
650 649 top: 'center',
651 650 style: {
652   - text: platfromInfo.name || 'ThingsKit Scada',
  651 + text: platfromInfo.name || 'Scada',
653 652 fontSize: isMobile() ? 20 : 70,
654 653 fontWeight: 'bold',
655 654 lineDash: [0, 200],
... ...
... ... @@ -207,7 +207,7 @@ class ConfigurationNodeApi {
207 207 * @returns
208 208 */
209 209 static getDictionaryValue(dictCode) {
210   - return defHttp.post('/yt/dict_item/find', {dictCode})
  210 + return defHttp.post('/yt/dict_item/find', { dictCode })
211 211 }
212 212
213 213 /**
... ... @@ -235,4 +235,24 @@ class ConfigurationNodeApi {
235 235 static closeFlvPlay(url, browserId) {
236 236 return defHttp.get(`/yt/rtsp/closeFlv?url=${encodeURIComponent(url)}&browserId=${browserId}`)
237 237 }
  238 +
  239 + /**
  240 + *
  241 + * @typedef {object} AlarmListRequestParamsType
  242 + * @property { number } page
  243 + * @property { number } pageSize
  244 + * @property { string } status
  245 + * @property { string } alarmType
  246 + * @property { string } severity
  247 + * @property { string[] } deviceIds
  248 + * @property { string } organizationId
  249 + * @property { string } deviceName
  250 + * @property { string } startTime
  251 + * @property { string } endTime
  252 + * @param {AlarmListRequestParamsType} params
  253 + * @returns
  254 + */
  255 + static getAlarmList(params) {
  256 + return defHttp.post('/yt/alarm/configuration/page', params)
  257 + }
238 258 }
... ...
... ... @@ -4094,6 +4094,7 @@ App.prototype.loadFile = function (id, sameWindow, file, success, force) {
4094 4094 return pageSizeControl.apply(this, arguments)
4095 4095 }
4096 4096 Editor.configurationName = response.configurationName + ".drawio";
  4097 + document.title = response.configurationName
4097 4098 if (response.configurationContentList.length > 0) {
4098 4099 response.configurationContentList.forEach((item) => {
4099 4100 Editor.configurationContentId = item.id;
... ...
... ... @@ -1703,6 +1703,8 @@ EditorUi.prototype.createPageInsertTab = function()
1703 1703 mxEvent.addListener(tab, 'click', mxUtils.bind(this, function(evt)
1704 1704 {
1705 1705 this.insertPage();
  1706 + var formats = PageSetupDialog.getFormats()
  1707 + this.editor.graph.pageFormat = formats[0].format
1706 1708 mxEvent.consume(evt);
1707 1709 }));
1708 1710
... ...
... ... @@ -272,8 +272,17 @@
272 272 /**
273 273 * @description 图片组件
274 274 */
275   - IMAGE: 'image'
276   -
  275 + IMAGE: 'image',
  276 +
  277 + /**
  278 + * @description 流量计
  279 + */
  280 + FLOWMETER: 'flowmeter',
  281 +
  282 + /**
  283 + * @description 告警列表
  284 + */
  285 + ALARM_LIST: 'alarmList'
277 286 }
278 287
279 288 Sidebar.prototype.enumComponentTypeValue = {
... ... @@ -364,7 +373,17 @@
364 373 /**
365 374 * @description 运行于停止
366 375 */
367   - RUNNING_AND_STOP: 'runningAndStop'
  376 + RUNNING_AND_STOP: 'runningAndStop',
  377 +
  378 + /**
  379 + * @description 流量计
  380 + */
  381 + FLOWMETER_PANEL: 'flowmeter',
  382 +
  383 + /**
  384 + * @description 告警列表
  385 + */
  386 + ALARM_LIST_PANEL: 'alarmListPanel'
368 387 }
369 388
370 389 /**
... ... @@ -721,8 +740,8 @@
721 740 //更多图形,显示出来的的标题跟id,同时包括图片
722 741
723 742 // TODO thingsKit 设置数据绑定展示面板
724   - const { LINE_CHART_EXPAND, BAR_CHART_EXPAND, DASHBOARD_CHART_EXPAND, DYNAMIC_EFFECT, DATA_SOURCE, VAR_IMAGE, INTERACTION, VIDEO: VIDEO_PANEL, SWITCH_STATE_SETTING, ONLY_SINGLE_EVENT, RUNNING_AND_STOP } = this.enumPermissionPanel
725   - const { LINE, LINE_CHART, REAL_TIME, TITLE, VARIABLE, DEFAULT, BAR_CHART, VIDEO, SWITCH, PARAMS_SETTING_BUTTON, DASHBOARD_CHART, IMAGE } = this.enumComponentType
  743 + const { LINE_CHART_EXPAND, BAR_CHART_EXPAND, DASHBOARD_CHART_EXPAND, DYNAMIC_EFFECT, DATA_SOURCE, VAR_IMAGE, INTERACTION, VIDEO: VIDEO_PANEL, SWITCH_STATE_SETTING, ONLY_SINGLE_EVENT, RUNNING_AND_STOP, FLOWMETER_PANEL, ALARM_LIST_PANEL } = this.enumPermissionPanel
  744 + const { LINE, LINE_CHART, REAL_TIME, TITLE, VARIABLE, DEFAULT, BAR_CHART, VIDEO, SWITCH, PARAMS_SETTING_BUTTON, DASHBOARD_CHART, IMAGE, FLOWMETER, ALARM_LIST } = this.enumComponentType
726 745 this.setComponentPermission(LINE, [RUNNING_AND_STOP, DYNAMIC_EFFECT])
727 746 this.setComponentPermission(DEFAULT, [DYNAMIC_EFFECT])
728 747 this.setComponentPermission(REAL_TIME, [DYNAMIC_EFFECT])
... ... @@ -736,6 +755,8 @@
736 755 this.setComponentPermission(SWITCH, [DATA_SOURCE, SWITCH_STATE_SETTING])
737 756 this.setComponentPermission(PARAMS_SETTING_BUTTON, [DATA_SOURCE, ONLY_SINGLE_EVENT])
738 757 this.setComponentPermission(IMAGE, [DATA_SOURCE])
  758 + this.setComponentPermission(FLOWMETER, [DATA_SOURCE, FLOWMETER_PANEL])
  759 + this.setComponentPermission(ALARM_LIST, [ALARM_LIST_PANEL])
739 760
740 761 var thingskitEntries = [
741 762 { title: mxResources.get('general'), id: 'general', image: IMAGE_PATH + '/sidebar-general.png' },
... ... @@ -1268,24 +1289,24 @@
1268 1289 this.addBasicComponentsPalette();
1269 1290 // 控制元件
1270 1291 this.addControlComponentsPalette();
  1292 + // 图表
  1293 + this.addChartsPalette();
  1294 + // 按钮
  1295 + this.addButtonPalette();
  1296 + // 灯
  1297 + this.addLightPalette();
1271 1298 // 发动机
1272 1299 this.addEnginePalette();
1273 1300 // 阀门
1274 1301 this.addValvePalette();
1275   - // 图表
1276   - this.addChartsPalette();
1277 1302 // 风机
1278 1303 this.addFanPalette();
1279 1304 // 污水处理
1280 1305 this.addSewagePalette();
1281 1306 // 管道
1282 1307 this.addConduitPalette();
1283   - // 按钮
1284   - this.addButtonPalette();
1285 1308 // 仪表
1286 1309 this.addInstrumentPalette();
1287   - // 灯
1288   - this.addLightPalette();
1289 1310 // 空气
1290 1311 this.addAirPalette(false);
1291 1312 // 过滤器
... ...
... ... @@ -65,6 +65,17 @@
65 65 this.setCellAttributes(cell, { [basicAttr.COMPONENT_TYPE]: componentType.IMAGE })
66 66 return this.createVertexTemplateFromCells([cell], cell.geometry.width, cell.geometry.height, '图片');
67 67 })),
  68 +
  69 + this.addEntry(this.getTagsForStencil(gn, '告警列表', 'basic').join(' '), mxUtils.bind(this, function () {
  70 + const id = AlarmListComponent.genId()
  71 + const template = AlarmListComponent.createAlarmList(null, 280, 200, id)
  72 + const cell = new mxCell(template, new mxGeometry(0, 0, 280, 200), 'text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;overflow=hidden;');
  73 + cell.setVertex(true)
  74 + const { UUID } = AlarmListComponent.getAttributeKeys()
  75 + this.setCellAttributes(cell, { [basicAttr.COMPONENT_TYPE]: componentType.ALARM_LIST, [UUID]: id })
  76 + console.log(cell)
  77 + return this.createVertexTemplateFromCells([cell], cell.geometry.width, cell.geometry.height, '告警列表');
  78 + })),
68 79 ];
69 80
70 81 this.addPaletteFunctions(dt, '基础元件', true, fns);
... ... @@ -77,6 +88,13 @@
77 88 return cell.getAttribute(basicAttr.COMPONENT_TYPE) === componentType.VIDEO
78 89 }
79 90
  91 +
  92 + Sidebar.prototype.isAlarmList = function (cell) {
  93 + const basicAttr = Sidebar.prototype.enumCellBasicAttribute
  94 + const componentType = Sidebar.prototype.enumComponentType
  95 + return cell.getAttribute(basicAttr.COMPONENT_TYPE) === componentType.ALARM_LIST
  96 + }
  97 +
80 98 /**
81 99 * @description charts cell发生resize时改变charts size
82 100 * @type {Function}
... ... @@ -87,6 +105,12 @@
87 105 const { width, height } = rect
88 106 cell.setAttribute('label', createVideoTemplate(width, height))
89 107 }
  108 +
  109 + if (AlarmListComponent.isAlarmList(cell)) {
  110 + const { width, height } = rect
  111 +
  112 + cell.setAttribute('label', AlarmListComponent.createAlarmList(null, width, height))
  113 + }
90 114 cellResized.apply(this, arguments)
91 115 }
92 116
... ... @@ -128,7 +152,7 @@
128 152 const allDateNode = document.querySelectorAll('.thingKit-component__real-time .real-time__date')
129 153 for (const time of allTimeNode) {
130 154 const date = new Date()
131   - time.innerHTML = `${date.getHours() < 10 ? '0' : ''}${date.getHours()}:${date.getMinutes() < 10 ? '0' : ''}${date.getMinutes()}:${date.getSeconds() < 10 ? '0' : ''}${date.getSeconds()}`
  155 + time.innerHTML = [date.getHours(), date.getMinutes(), date.getSeconds()].map(item => item.toString().padStart(2, 0)).join(':')
132 156 }
133 157
134 158 for (const date of allDateNode) {
... ... @@ -138,4 +162,123 @@
138 162 }
139 163
140 164 initRealTimeComponent()
  165 +
141 166 })();
  167 +
  168 +
  169 +function AlarmListComponent() {
  170 +
  171 +}
  172 +
  173 +/**
  174 + *
  175 + * @param {{deviceName: string, status: string, startTs: string}[]} list
  176 + * @param {number} width
  177 + * @param {number} height
  178 + * @param {string} id
  179 + * @returns
  180 + */
  181 +AlarmListComponent.createAlarmList = function (list, width = 280, height = 200, id) {
  182 +
  183 + list = list || Array.from({ length: 10 }, ((_, index) => ({
  184 + deviceName: `示例设备${index + 1}`, startTs: Date.now(), status: [
  185 + "CLEARED_UNACK",
  186 + "ACTIVE_UNACK",
  187 + "CLEARED_ACK",
  188 + "ACTIVE_ACK"
  189 + ][index % 4]
  190 + })))
  191 +
  192 + id = id || AlarmListComponent.genId()
  193 +
  194 + var template = `
  195 + <div id="${id}" class="alarm-list" style="overflow-y: auto; overflow-x: hidden; width: ${width}px; height: ${height}px;">
  196 + <div class="list-wrapper" style="font-size: 12px;" data-auto-scroll>
  197 + ${AlarmListComponent.createAlarmItem(list)}
  198 + </div>
  199 + </div>
  200 + `
  201 + return template.replace(/\n/g, '')
  202 +}
  203 +
  204 +/**
  205 + *
  206 + * @param {{deviceName: string, status: string, startTs: string}[] | {deviceName: string, status: string, startTs: string}} record
  207 + * @returns
  208 + */
  209 +AlarmListComponent.createAlarmItem = function (record) {
  210 + if (record && !Array.isArray(record)) {
  211 + record = [record]
  212 + }
  213 +
  214 + var itemHeight = 50
  215 +
  216 + function formatTime(time) {
  217 + try {
  218 + var date = new Date(time)
  219 + var year = date.getFullYear()
  220 + var month = date.getMonth() + 1
  221 + var day = date.getDate()
  222 + var hour = date.getHours()
  223 + var minute = date.getMinutes()
  224 + var second = date.getSeconds()
  225 + return `${year}-${month.toString().padStart(2, 0)}-${day.toString().padStart(2, 0)} ${hour.toString().padStart(2, 0)}:${minute.toString().padStart(2, 0)}:${second.toString().padStart(2, 0)}`
  226 + } catch (error) {
  227 + return '暂无时间 '
  228 + }
  229 + }
  230 +
  231 + return (record || []).map(item => {
  232 + var { deviceName, status, startTs } = item || {}
  233 +
  234 + var stateStyle = {
  235 + CLEARED_UNACK: 'color: #cf1322;background: #fff1f0;border-color: #ffa39e;',
  236 + ACTIVE_UNACK: 'color: #d46b08;background: #fff7e6;border-color: #ffd591;',
  237 + CLEARED_ACK: 'color: #08979c;background: #e6fffb;border-color: #87e8de;',
  238 + ACTIVE_ACK: 'color: #389e0d;background: #f6ffed;border-color: #b7eb8f;',
  239 + }
  240 +
  241 + var stateName = {
  242 + CLEARED_UNACK: '清除未确认',
  243 + ACTIVE_UNACK: '激活未确认',
  244 + CLEARED_ACK: '清除已确认',
  245 + ACTIVE_ACK: '激活已确认',
  246 + }
  247 +
  248 + var template = `
  249 + <div class="alarm-list-item" style="height: ${itemHeight}px; box-sizing: border-box; display: flex; flex-direction: column; justify-content: center; border-bottom: 1px solid black; text-align: left; min-width: 280px; width: 100%;">
  250 + <div style="padding: 0 10px;">
  251 + <span style="margin-right: 5px;">设备:</span>
  252 + <span>${deviceName}</span>
  253 + </div>
  254 + <div style="padding: 5px 10px;">
  255 + <span style="margin-right: 5px;">时间:</span>
  256 + <span>${formatTime(startTs)}</span>
  257 + <span style="padding: 5px; border-radius: 5px; margin-left: 5px; border: 1px solid; font-size: 12px; ${stateStyle[status]}">${stateName[status]}</span>
  258 + </div>
  259 + </div>`
  260 + return template
  261 + }).join('').replace(/\n/g, '')
  262 +}
  263 +
  264 +AlarmListComponent.genId = function () {
  265 + return `alarm-list-${Number(Math.random().toString().substring(2)).toString(16)}`
  266 +}
  267 +
  268 +AlarmListComponent.isAlarmList = function (cell) {
  269 + const basicAttr = Sidebar.prototype.enumCellBasicAttribute
  270 + const componentType = Sidebar.prototype.enumComponentType
  271 + return cell.getAttribute(basicAttr.COMPONENT_TYPE) === componentType.ALARM_LIST
  272 +}
  273 +
  274 +AlarmListComponent.getAttributeKeys = function () {
  275 + return {
  276 + UUID: 'uuid',
  277 + }
  278 +}
  279 +
  280 +AlarmListComponent.setAutoScroll = function (cell) {
  281 + // RAFSetInterval(() => {
  282 + // console.log(cell)
  283 + // }, 2000)
  284 +}
... ...
... ... @@ -89,6 +89,11 @@
89 89 }));
90 90 })
91 91
  92 + this.setVariableImageLib('switch', '开关', [
  93 + { name: 'switch-on', path: '/images/thingskit/switch-on.svg', staticPath: `${Proxy_Prefix}/images/thingskit/switch-on.svg` },
  94 + { name: 'switch-off', path: '/images/thingskit/switch-on.svg', staticPath: `${Proxy_Prefix}/images/thingskit/switch-off.svg` },
  95 + ])
  96 +
92 97 this.setVariableImageLib(dt, label, lib)
93 98
94 99 this.addPaletteFunctions(dt, label, false, fns);
... ...
... ... @@ -42,10 +42,29 @@
42 42 CHART_IMG_PLACEHOLDER_SIZE: 30,
43 43 }
44 44
  45 + Sidebar.prototype.enumFlowmeterAttr = {
  46 + WIDTH: 'width',
  47 + HEIGHT: 'height',
  48 + VALUE: 'value',
  49 + TYPE: 'type',
  50 + UUID: 'uuid',
  51 + BG_COLOR: 'bgColor',
  52 + WAVE_FIRST_COLOR: 'waveFirstColor',
  53 + WAVE_SECOND_COLOR: 'waveSecondColor',
  54 + WAVE_THIRD_COLOR: 'waveThirdColor',
  55 + }
  56 +
  57 + Sidebar.prototype.enumFlowmeterType = {
  58 + RECT: 'rect',
  59 + CIRCLE: 'circle',
  60 + THERMOMETER: 'thermometer'
  61 + }
  62 +
45 63 // Adds Atlassian shapes
46 64 // 图表
47 65 Sidebar.prototype.addChartsPalette = function () {
48 66 this.chartsComponentInit()
  67 +
49 68 const self = this
50 69 const componentType = this.enumComponentType
51 70 const { DATA_SOURCE, DYNAMIC_EFFECT, DISPLAY_TYPE } = this.enumPermissionPanel
... ... @@ -74,7 +93,19 @@
74 93 const id = self.generatorChartsId()
75 94 const cell = self.generatorCell(id, enumConst.CHART_IMG_PLACEHOLDER_SIZE, enumConst.CHART_IMG_PLACEHOLDER_SIZE, componentType.DASHBOARD_CHART, `${Proxy_Prefix}/images/thingskit/dashboard-chart.png`)
76 95 return this.createVertexTemplateFromCells([cell], cell.geometry.width, cell.geometry.height, '仪表盘');
77   - }))
  96 + })),
  97 + this.addEntry('flowmeter-circle', mxUtils.bind(this, function () {
  98 + const cell = self.generateFlowmeterCell(100, 100, Sidebar.prototype.enumFlowmeterType.CIRCLE)
  99 + return this.createVertexTemplateFromCells([cell], cell.geometry.width, cell.geometry.height, '流量计');
  100 + })),
  101 + this.addEntry('flowmeter-rect', mxUtils.bind(this, function () {
  102 + const cell = self.generateFlowmeterCell(100, 100, Sidebar.prototype.enumFlowmeterType.RECT)
  103 + return this.createVertexTemplateFromCells([cell], cell.geometry.width, cell.geometry.height, '流量计');
  104 + })),
  105 + this.addEntry('flowmeter-thermometer', mxUtils.bind(this, function () {
  106 + const cell = self.generateFlowmeterCell(100, 100, Sidebar.prototype.enumFlowmeterType.THERMOMETER)
  107 + return this.createVertexTemplateFromCells([cell], cell.geometry.width, cell.geometry.height, '流量计');
  108 + })),
78 109 ];
79 110
80 111 this.addPaletteFunctions('charts', '图表', false, fns);
... ... @@ -210,7 +241,8 @@
210 241 Sidebar.prototype.addClickHandler = function (elt, ds, cells) {
211 242 const cell = cells[0]
212 243 const cellValue = cell.value
213   - const validate = cellValue && cellValue.nodeName === 'UserObject' && this.isChartCell(cell)
  244 + const validateChart = cellValue && cellValue.nodeName === 'UserObject' && this.isChartCell(cell)
  245 + const validateFlowmeter = cellValue && cellValue.nodeName === 'UserObject' && this.isFlowmeter(cell)
214 246
215 247 /**
216 248 * @description 拓展Sidebar鼠标按下
... ... @@ -218,7 +250,7 @@
218 250 */
219 251 const mouseDown = ds.mouseDown
220 252 ds.mouseDown = function (evt) {
221   - if (validate) {
  253 + if (validateChart) {
222 254 const id = self.generatorChartsId()
223 255 const geo = Object.assign(graph.model.getGeometry(cell), { width: 400, height: 400 })
224 256 cell.setGeometry(geo)
... ... @@ -227,6 +259,14 @@
227 259 self.graph.setAttributeForCell(cell, enumConst.CHART_CELL_HEIGHT, enumConst.CHART_CELL_DEFAULT_HEIGHT);
228 260 self.graph.setAttributeForCell(cell, 'label', self.createChartsNode(id))
229 261 }
  262 +
  263 + if (validateFlowmeter) {
  264 + const { UUID, TYPE } = getFlowmeterAttrKey()
  265 + const id = uuid()
  266 + const type = self.graph.getAttributeForCell(cell, TYPE)
  267 + self.graph.setAttributeForCell(cell, UUID, id);
  268 + self.graph.setAttributeForCell(cell, 'label', Sidebar.prototype.generateFlowmeterTemplate(id, type));
  269 + }
230 270 mouseDown.apply(this, arguments)
231 271 };
232 272
... ... @@ -239,7 +279,7 @@
239 279 try {
240 280 mouseUp.apply(this, arguments)
241 281 } finally {
242   - if (validate) {
  282 + if (validateChart) {
243 283 const id = self.getCellId(cell)
244 284 const chartType = cell.getAttribute(basicAttr.COMPONENT_TYPE)
245 285 self.generatorEChartInstance(id, enumConst.CHART_CELL_DEFAULT_WIDTH, enumConst.CHART_CELL_DEFAULT_HEIGHT, chartType)
... ... @@ -332,6 +372,7 @@
332 372 const { width, height, chartType } = domIdMapping.get(id)
333 373 Sidebar.prototype.generatorEChartInstance(id, width, height, chartType)
334 374 }
  375 + // Sidebar.prototype.initFlowmeter(graph)
335 376 }
336 377
337 378 /**
... ... @@ -357,6 +398,7 @@
357 398 refresh.apply(this, arguments)
358 399 if (!arguments.length) {
359 400 Sidebar.prototype.initChartInstance(this)
  401 + Sidebar.prototype.resetFlowmeter(this)
360 402 }
361 403 }
362 404
... ... @@ -367,6 +409,7 @@
367 409 EditorUi.prototype.setFileData = function () {
368 410 setFileData.apply(this, arguments)
369 411 Sidebar.prototype.initChartInstance(this.editor.graph)
  412 + Sidebar.prototype.resetFlowmeter(this.editor.graph)
370 413 }
371 414
372 415 // const selectPage = EditorUi.prototype.selectPage
... ... @@ -496,5 +539,418 @@
496 539 }
497 540 }
498 541
  542 + function uuid() {
  543 + return Number(Math.random().toString().substring(2)).toString(32)
  544 + }
  545 +
  546 + /**
  547 + * @description 生成流量计模版
  548 + */
  549 + Sidebar.prototype.generateFlowmeterTemplate = function (id = uuid(), type) {
  550 + const flowmeterType = Sidebar.prototype.enumFlowmeterType
  551 + const typeGenFn = {
  552 + [flowmeterType.CIRCLE]: Sidebar.prototype.generateFlowmeterCircle,
  553 + [flowmeterType.RECT]: Sidebar.prototype.generateFlowmeterRect,
  554 + [flowmeterType.THERMOMETER]: Sidebar.prototype.generateFlowmeterThermometer,
  555 + }
  556 + const template = `<div class="flowmeter" style="transform: scale(0.9);font-size: 0;" id="${id}">${typeGenFn[type] && typeGenFn[type]()}</div>`
  557 + return template
  558 + }
  559 +
  560 + Sidebar.prototype.generateFlowmeterCell = function (width = 100, height = 100, type) {
  561 + const id = uuid()
  562 + const template = Sidebar.prototype.generateFlowmeterTemplate(id, type)
  563 + const cell = new mxCell(template, new mxGeometry(0, 0, width, height), 'text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;overflow=hidden;');
  564 + cell.setVertex(true)
  565 + const componentType = Sidebar.prototype.enumComponentType
  566 + const { WIDTH, HEIGHT, VALUE, TYPE, COMPONENT_TYPE, UUID } = getFlowmeterAttrKey()
  567 + this.graph.setAttributeForCell(cell, WIDTH, width);
  568 + this.graph.setAttributeForCell(cell, HEIGHT, height);
  569 + this.graph.setAttributeForCell(cell, VALUE, 20);
  570 + this.graph.setAttributeForCell(cell, TYPE, type);
  571 + this.graph.setAttributeForCell(cell, UUID, id);
  572 + this.graph.setAttributeForCell(cell, COMPONENT_TYPE, componentType.FLOWMETER);
  573 + return cell
  574 + }
  575 +
  576 + Sidebar.prototype.generateFlowmeterRect = function () {
  577 + return `
  578 + <svg class="waves-rect" viewBox="0 0 100 100" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg"
  579 + xmlns:xlink="http://www.w3.org/1999/xlink"
  580 + style="--width: 100; --height: 100; --value: 20; --play-state: running; --full-flag: clamp(0, calc(100 - var(--value)), 1); --over-min-flag: clamp(0, calc(calc(var(--value) - 1) * -1), 1);">
  581 + <defs>
  582 + <style>
  583 + .waves-rect {
  584 + width: calc(var(--width) * 1px);
  585 + height: calc(var(--height) * 1px);
  586 + }
  587 +
  588 + @keyframes move {
  589 + from {
  590 + transform: translate(-90px, 0%);
  591 + }
  592 +
  593 + to {
  594 + transform: translate(85px, 0%);
  595 + }
  596 + }
  597 +
  598 + .wave {
  599 + animation: move 3s linear infinite;
  600 + animation-play-state: running;
  601 + }
  602 +
  603 + .wave:nth-child(1) {
  604 + animation-delay: -2s;
  605 + animation-duration: 9s;
  606 + }
  607 +
  608 + .wave:nth-child(2) {
  609 + animation-delay: -4s;
  610 + animation-duration: 6s;
  611 + }
  612 +
  613 + .wave:nth-child(3) {
  614 + animation-delay: -6s;
  615 + animation-duration: 3s;
  616 + }
  617 +
  618 + .waves-rect>g+rect {
  619 + transform: translateY(calc(calc(100 - var(--value)) * var(--full-flag) * 1% + var(--full-flag) * 15%));
  620 + transition: transform linear 1s;
  621 + }
  622 +
  623 + .height {
  624 + transform: translateY(calc(var(--value) * -1% - 10% + var(--over-min-flag) * 10%));
  625 + transition: transform linear 1s;
  626 + }
  627 +
  628 + .waves-rect .text {
  629 + display: flex;
  630 + justify-content: center;
  631 + align-items: center;
  632 + width: 100%;
  633 + height: 100%;
  634 + color: #fff;
  635 + font-size: 18px;
  636 + font-weight: 700;
  637 + }
  638 +
  639 + .waves-rect .text::after {
  640 + counter-reset: value var(--value);
  641 + content: counter(value) ' %';
  642 + }
  643 + </style>
  644 + <path id="wave" d="M-160 118c30 0 58-18 88-18s 58 18 88 18 58-18 88-18 58 18 88 18 v100h-352z" />
  645 + </defs>
  646 + <rect class="bgColor" x="0" y="0" width="100" height="100" fill="#8badcb"></rect>
  647 + <g class="height">
  648 + <use class="wave waveFirst" xlink:href="#wave" fill="#4579e2" x="0" y="0"></use>
  649 + <use class="wave waveSecond" xlink:href="#wave" fill="#3461c1" x="0" y="2"></use>
  650 + <use class="wave waveThird" xlink:href="#wave" fill="#2d55aa" x="0" y="4"></use>
  651 + </g>
  652 + <rect class="waveThird" x="0" y="0" width="100" height="100" fill="#2d55aa"></rect>
  653 + <foreignObject x="0" y="0" width="100" height="100" text-anchor="middle" dominant-baseline="middle">
  654 + <div xmlns="http://www.w3.org/1999/xhtml" class="text"></div>
  655 + </foreignObject>
  656 +</svg>
  657 + `.replace(/\n/g, '')
  658 + }
  659 +
  660 + Sidebar.prototype.generateFlowmeterCircle = function () {
  661 + return `
  662 + <svg class="waves-circle" viewBox="0 0 100 100" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg"
  663 + xmlns:xlink="http://www.w3.org/1999/xlink"
  664 + style="--width: 100; --height: 100; --value: 20; --play-state: running; --full-flag: clamp(0, calc(100 - var(--value)), 1); --over-min-flag: clamp(0, calc(calc(var(--value) - 1) * -1), 1)">
  665 + <style>
  666 + .waves-circle {
  667 + width: calc(min(var(--width), var(--height)) * 1px);
  668 + height: calc(min(var(--width), var(--height)) * 1px);
  669 + clip-path: circle(calc(min(var(--width), var(--height)) / 2 * 1px));
  670 + }
  671 +
  672 + @keyframes move {
  673 + from {
  674 + transform: translate(-90px, 0%);
  675 + }
  676 +
  677 + to {
  678 + transform: translate(85px, 0%);
  679 + }
  680 + }
  681 +
  682 + .wave {
  683 + animation: move 3s linear infinite;
  684 + animation-play-state: var(--play-state);
  685 + }
  686 +
  687 + .wave:nth-child(1) {
  688 + animation-delay: -2s;
  689 + animation-duration: 9s;
  690 + }
  691 +
  692 + .wave:nth-child(2) {
  693 + animation-delay: -4s;
  694 + animation-duration: 6s;
  695 + }
  696 +
  697 + .wave:nth-child(3) {
  698 + animation-delay: -6s;
  699 + animation-duration: 3s;
  700 + }
  701 +
  702 + .height {
  703 + transform: translateY(calc(var(--value) * -1% - 10% + var(--over-min-flag) * 10%));
  704 + transition: transform linear 1s;
  705 + }
  706 +
  707 + .waves-circle>g+rect {
  708 + transform: translateY(calc(calc(100 - var(--value)) * var(--full-flag) * 1% + var(--full-flag) * 15%));
  709 + transition: transform linear 1s;
  710 + }
  711 +
  712 + .waves-circle .text {
  713 + display: flex;
  714 + justify-content: center;
  715 + align-items: center;
  716 + width: 100%;
  717 + height: 100%;
  718 + color: #fff;
  719 + font-size: 18px;
  720 + font-weight: 700;
  721 + }
  722 +
  723 + .waves-circle .text::after {
  724 + counter-reset: value var(--value);
  725 + content: counter(value) ' %';
  726 + }
  727 + </style>
  728 + <defs>
  729 + <path id="wave" d="M-160 118c30 0 58-18 88-18s 58 18 88 18 58-18 88-18 58 18 88 18 v100h-352z" />
  730 + </defs>
  731 + <circle class="bgColor" cx="50" cy="50" r="50" fill="#8badcb" />
  732 + <g class="height">
  733 + <use class="wave waveFirst" xlink:href="#wave" fill="#4579e2" x="0" y="0"></use>
  734 + <use class="wave waveSecond" xlink:href="#wave" fill="#3461c1" x="0" y="2"></use>
  735 + <use class="wave waveThird" xlink:href="#wave" fill="#2d55aa" x="0" y="4"></use>
  736 + </g>
  737 + <rect class="waveThird" x="0" y="0" width="100" height="100" fill="#2d55aa"></rect>
  738 + <foreignObject x="0" y="0" width="100" height="100" text-anchor="middle" dominant-baseline="middle">
  739 + <div xmlns="http://www.w3.org/1999/xhtml" class="text"></div>
  740 + </foreignObject>
  741 + </svg>
  742 + `.replace(/\n/g, '')
  743 + }
  744 +
  745 + Sidebar.prototype.generateFlowmeterThermometer = function () {
  746 + return `
  747 + <svg class="flowmeter-thermometer" viewBox="0 0 200 250" xmlns="http://www.w3.org/2000/svg"
  748 + style="--range: 4; --min: 50; --max: 70; --width: 100; --height: 100; --value: 50;">
  749 + <style>
  750 + .flowmeter-thermometer {
  751 + width: calc(min(var(--width), var(--height)) * 1px);
  752 + height: calc(min(var(--width), var(--height)) * 1px);
  753 + }
  754 +
  755 + .thermometer-mercury-column {
  756 + y: var(--value);
  757 + }
  758 +
  759 + .tick-label {
  760 + font-size: 12px;
  761 + text-align: right;
  762 + overflow: hidden;
  763 + text-overflow: ellipsis;
  764 + color: #5b6b73;
  765 + }
  766 +
  767 + .thermometer-mercury-column {
  768 + transition: y .5s cubic-bezier(0.52, 0.05, 0.47, 0.99);
  769 + }
  770 + </style>
  771 + <defs>
  772 + <radialGradient id="thermometerdiv_meter_2" cx="50%" cy="50%" r="50%" fx="50%" fy="50%">
  773 + <stop offset="0%" style="stop-color: rgb(230, 200, 200);"></stop>
  774 + <stop offset="90%" style="stop-color: rgb(230, 0, 0);"></stop>
  775 + </radialGradient>
  776 + <clipPath id="over">
  777 + <rect width="100" height="190" x="100" y="10" />
  778 + </clipPath>
  779 + </defs>
  780 + <circle r="9.25" cx="109" cy="14.25" style="fill: rgb(255, 255, 255); stroke: rgb(136, 136, 136); stroke-width: 1px;">
  781 + </circle>
  782 + <rect x="99.75" y="14.25" height="192.75" width="18.5"
  783 + style="shape-rendering: crispedges; fill: rgb(255, 255, 255); stroke: rgb(136, 136, 136); stroke-width: 1px;">
  784 + </rect>
  785 + <circle r="8.75" cx="109" cy="14.25" style="fill: rgb(255, 255, 255); stroke: none;"></circle>
  786 + <circle r="18" cx="109" cy="207" style="fill: rgb(255, 255, 255); stroke: rgb(136, 136, 136);"></circle>
  787 + <rect x="100.25" y="14.25" height="192.75" width="17.5"
  788 + style="shape-rendering: crispedges; fill: rgb(255, 255, 255); stroke: none;"></rect>
  789 + <line class="thermometer-min-line" x1="99.75" x2="140.25" y1="165" y2="165"
  790 + style="stroke: rgb(136, 136, 136); stroke-width: 1px; shape-rendering: crispedges;"></line>
  791 + <text class="thermometer-min-label" x="120.25" y="168.46428571428572" dy="0.72em"
  792 + style="fill: rgb(0, 0, 230); font-size: 10px;">min</text>
  793 + <line class="thermometer-max-line" x1="99.75" x2="140.25" y1="40" y2="40"
  794 + style="stroke: rgb(136, 136, 136); stroke-width: 1px; shape-rendering: crispedges;"></line>
  795 + <text class="thermometer-max-label" x="120.25" y="35.285714285714306"
  796 + style="fill: rgb(230, 0, 0); font-size: 10px;">max</text>
  797 + <rect class="thermometer-mercury-column" x="104" y="15" width="10.5" height="190"
  798 + style="shape-rendering: crispedges; fill: rgb(230, 0, 0);" clip-path="url(#over)"></rect>
  799 + <circle r="13" cx="109" cy="207"
  800 + style="fill: url(&quot;#thermometerdiv_meter_2&quot;); stroke: rgb(230, 0, 0); stroke-width: 2px;"></circle>
  801 + <g class="thermometer-temperature-axis" transform="translate(99.75,0)" fill="none" font-size="10"
  802 + font-family="sans-serif" text-anchor="end">
  803 + <g class="tick" opacity="1" transform="translate(0,190)">
  804 + <line stroke="currentColor" x2="-7"
  805 + style="stroke: rgb(136, 136, 136); shape-rendering: crispedges; stroke-width: 1px;"></line>
  806 + <foreignObject xmlns="http://www.w3.org/2000/svg" x="-55" y="-10" width="45" height="20">
  807 + <div class="tick-label" xmlns="http://www.w3.org/1999/xhtml">-20</div>
  808 + </foreignObject>
  809 + </g>
  810 + <g class="tick" opacity="1" transform="translate(0,165)">
  811 + <line stroke="currentColor" x2="-7"
  812 + style="stroke: rgb(136, 136, 136); shape-rendering: crispedges; stroke-width: 1px;"></line>
  813 + <foreignObject xmlns="http://www.w3.org/2000/svg" x="-55" y="-10" width="45" height="20">
  814 + <div class="tick-label" xmlns="http://www.w3.org/1999/xhtml">0</div>
  815 + </foreignObject>
  816 + </g>
  817 + <g class="tick" opacity="1" transform="translate(0,140)">
  818 + <line stroke="currentColor" x2="-7"
  819 + style="stroke: rgb(136, 136, 136); shape-rendering: crispedges; stroke-width: 1px;"></line>
  820 + <foreignObject xmlns="http://www.w3.org/2000/svg" x="-55" y="-10" width="45" height="20">
  821 + <div class="tick-label" xmlns="http://www.w3.org/1999/xhtml">20</div>
  822 + </foreignObject>
  823 + </g>
  824 + <g class="tick" opacity="1" transform="translate(0,115)">
  825 + <line stroke="currentColor" x2="-7"
  826 + style="stroke: rgb(136, 136, 136); shape-rendering: crispedges; stroke-width: 1px;"></line>
  827 + <foreignObject xmlns="http://www.w3.org/2000/svg" x="-55" y="-10" width="45" height="20">
  828 + <div class="tick-label" xmlns="http://www.w3.org/1999/xhtml">40</div>
  829 + </foreignObject>
  830 + </g>
  831 + <g class="tick" opacity="1" transform="translate(0,90)">
  832 + <line stroke="currentColor" x2="-7"
  833 + style="stroke: rgb(136, 136, 136); shape-rendering: crispedges; stroke-width: 1px;"></line>
  834 + <foreignObject xmlns="http://www.w3.org/2000/svg" x="-55" y="-10" width="45" height="20">
  835 + <div class="tick-label" xmlns="http://www.w3.org/1999/xhtml">60</div>
  836 + </foreignObject>
  837 + </g>
  838 + <g class="tick" opacity="1" transform="translate(0,65)">
  839 + <line stroke="currentColor" x2="-7"
  840 + style="stroke: rgb(136, 136, 136); shape-rendering: crispedges; stroke-width: 1px;"></line>
  841 + <foreignObject xmlns="http://www.w3.org/2000/svg" x="-55" y="-10" width="45" height="20">
  842 + <div class="tick-label" xmlns="http://www.w3.org/1999/xhtml">80</div>
  843 + </foreignObject>
  844 + </g>
  845 + <g class="tick" opacity="1" transform="translate(0,40)">
  846 + <line stroke="currentColor" x2="-7"
  847 + style="stroke: rgb(136, 136, 136); shape-rendering: crispedges; stroke-width: 1px;"></line>
  848 + <foreignObject xmlns="http://www.w3.org/2000/svg" x="-55" y="-10" width="45" height="20">
  849 + <div class="tick-label" xmlns="http://www.w3.org/1999/xhtml">100</div>
  850 + </foreignObject>
  851 + </g>
  852 + <g class="tick" opacity="1" transform="translate(0,15)">
  853 + <line stroke="currentColor" x2="-7"
  854 + style="stroke: rgb(136, 136, 136); shape-rendering: crispedges; stroke-width: 1px;"></line>
  855 + <foreignObject xmlns="http://www.w3.org/2000/svg" x="-55" y="-10" width="45" height="20">
  856 + <div class="tick-label" xmlns="http://www.w3.org/1999/xhtml">120</div>
  857 + </foreignObject>
  858 + </g>
  859 + </g>
  860 + </svg>
  861 + `.replace(/\n/g, '')
  862 + }
  863 +
  864 + Sidebar.prototype.resetFlowmeter = function (graph) {
  865 + const { COMPONENT_TYPE, UUID, WIDTH, HEIGHT } = getFlowmeterAttrKey()
  866 + const componentType = Sidebar.prototype.enumComponentType
  867 + const cells = Object.entries(graph.getModel().cells || {}).map(([_, item]) => item) || []
  868 + const needReset = cells.filter(item => item.value && item.getAttribute(COMPONENT_TYPE) === componentType.FLOWMETER)
  869 +
  870 + needReset.forEach(item => {
  871 + const id = item.getAttribute(UUID)
  872 + const element = document.getElementById(id).querySelector('svg')
  873 + if (element) {
  874 + const width = item.getAttribute(WIDTH)
  875 + const height = item.getAttribute(HEIGHT)
  876 +
  877 + element.style.setProperty('--width', width)
  878 + element.style.setProperty('--height', height)
  879 + }
  880 + })
  881 + }
  882 +
  883 + Sidebar.prototype.isFlowmeter = function (cell) {
  884 + const basicAttr = Sidebar.prototype.enumCellBasicAttribute
  885 + const componentType = Sidebar.prototype.enumComponentType
  886 + return cell.getAttribute(basicAttr.COMPONENT_TYPE) === componentType.FLOWMETER
  887 + }
  888 +
  889 + Sidebar.prototype.updateFlowmeterCell = function (cell, { width, height, value, type }) {
  890 + const { WIDTH, HEIGHT, VALUE, TYPE, UUID } = getFlowmeterAttrKey()
  891 + const id = cell.getAttribute(UUID)
  892 + const element = document.getElementById(id).querySelector('svg')
  893 + if (element) {
  894 + width && element.style.setProperty('--width', width)
  895 + height && element.style.setProperty('--height', height)
  896 + value && element.style.setProperty('--value', value)
  897 + width && cell.setAttribute(WIDTH, width)
  898 + height && cell.setAttribute(HEIGHT, height)
  899 + value && cell.setAttribute(VALUE, value)
  900 + type && cell.setAttribute(TYPE, type)
  901 + }
  902 + }
  903 +
  904 + /**
  905 + * @description charts cell发生resize时改变charts size
  906 + * @type {Function}
  907 + */
  908 + const cellResized = mxGraph.prototype.cellResized
  909 + mxGraph.prototype.cellResized = function (cell, rect) {
  910 +
  911 + if (Sidebar.prototype.isFlowmeter(cell)) {
  912 + const { width, height } = rect
  913 + Sidebar.prototype.updateFlowmeterCell(cell, { width, height })
  914 + }
  915 + cellResized.apply(this, arguments)
  916 + }
499 917 })();
500 918
  919 +/**
  920 + * @description 获取流量计属性key
  921 + * @returns {{WIDTH: 'width', HEIGHT: 'height', VALUE: 'value', TYPE: 'type', UUID: 'uuid', COMPONENT_TYPE: 'componentType', BG_COLOR: 'bgColor', WAVE_FIRST_COLOR: 'waveFirstColor', WAVE_SECOND_COLOR: 'waveSecondColor', WAVE_THIRD_COLOR: 'waveThirdColor'}}
  922 + */
  923 +function getFlowmeterAttrKey() {
  924 + const basicAttr = Sidebar.prototype.enumCellBasicAttribute
  925 + const flowmeterAttr = Sidebar.prototype.enumFlowmeterAttr
  926 + return {
  927 + WIDTH: flowmeterAttr.WIDTH,
  928 + HEIGHT: flowmeterAttr.HEIGHT,
  929 + VALUE: flowmeterAttr.VALUE,
  930 + TYPE: flowmeterAttr.TYPE,
  931 + UUID: flowmeterAttr.UUID,
  932 + BG_COLOR: flowmeterAttr.BG_COLOR,
  933 + WAVE_FIRST_COLOR: flowmeterAttr.WAVE_FIRST_COLOR,
  934 + WAVE_SECOND_COLOR: flowmeterAttr.WAVE_SECOND_COLOR,
  935 + WAVE_THIRD_COLOR: flowmeterAttr.WAVE_THIRD_COLOR,
  936 + COMPONENT_TYPE: basicAttr.COMPONENT_TYPE,
  937 + }
  938 +}
  939 +
  940 +/**
  941 + * @description 获取流量计类型
  942 + * @returns {{RECT: 'rect', CIRCLE: 'circle'}}
  943 + */
  944 +function getFlowmeterType() {
  945 + const type = Sidebar.prototype.enumFlowmeterType
  946 + return {
  947 + RECT: type.RECT,
  948 + CIRCLE: type.CIRCLE
  949 + }
  950 +}
  951 +
  952 +
  953 +function getComponentType() {
  954 + const componentType = Sidebar.prototype.enumComponentType
  955 + return
  956 +}
... ...
... ... @@ -30,7 +30,7 @@
30 30 return this.createVertexTemplateFromCells([cell], cell.geometry.width, cell.geometry.height, '开关');
31 31 })),
32 32 this.addEntry(this.getTagsForStencil(gn, 'Params Setting', dt).join(' '), mxUtils.bind(this, function () {
33   - const cell = new mxCell('<button class="param-setting-button">参数设置</button>', new mxGeometry(0, 0, 100, 60), 'text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;overflow=hidden;');
  33 + const cell = new mxCell('参数设置', new mxGeometry(0, 0, 60, 32), 'text;html=1;strokeColor=#1890ff;fillColor=#1890ff;align=center;verticalAlign=middle;whiteSpace=wrap;overflow=hidden;fontColor=#FFFFFF;rounded=1;shadow=0;');
34 34 // 自定义属性
35 35 const cellAttribute = {
36 36 [COMPONENT_TYPE]: PARAMS_SETTING_BUTTON
... ...
... ... @@ -55,11 +55,6 @@
55 55 }));
56 56 })
57 57
58   - this.setVariableImageLib('switch', '开关', [
59   - { name: 'switch-on', path: '/images/thingskit/switch-on.svg', staticPath: `${Proxy_Prefix}/images/thingskit/switch-on.svg` },
60   - { name: 'switch-off', path: '/images/thingskit/switch-on.svg', staticPath: `${Proxy_Prefix}/images/thingskit/switch-off.svg` },
61   - ])
62   -
63 58 this.setVariableImageLib(dt, label, lib)
64 59
65 60 this.addPaletteFunctions(dt, label, false, fns);
... ...
... ... @@ -2294,10 +2294,15 @@ EditorUi.prototype.initCanvas = function()
2294 2294 var layout = this.graph.getPageLayout();
2295 2295 var page = this.graph.getPageSize();
2296 2296
2297   - return new mxRectangle(this.scale * (this.translate.x + layout.x * page.width),
2298   - this.scale * (this.translate.y + layout.y * page.height),
2299   - this.scale * layout.width * page.width,
2300   - this.scale * layout.height * page.height);
  2297 + // TODO thingsKit 固定页面尺寸 超出边界后隐藏
  2298 + return new mxRectangle(this.scale * (this.translate.x),
  2299 + this.scale * (this.translate.y),
  2300 + this.scale * page.width,
  2301 + this.scale * page.height)
  2302 + // return new mxRectangle(this.scale * (this.translate.x + layout.x * page.width),
  2303 + // this.scale * (this.translate.y + layout.y * page.height),
  2304 + // this.scale * layout.width * page.width,
  2305 + // this.scale * layout.height * page.height);
2301 2306 };
2302 2307
2303 2308 graph.getPreferredPageSize = function(bounds, width, height)
... ... @@ -3381,11 +3386,28 @@ EditorUi.prototype.lightboxFit = function(maxHeight)
3381 3386 }
3382 3387
3383 3388 // LATER: Use initial graph bounds to avoid rounding errors
3384   - this.editor.graph.maxFitScale = this.lightboxMaxFitScale;
3385   - this.editor.graph.fit(border, null, null, null, null, null, maxHeight);
3386   - this.editor.graph.maxFitScale = null;
3387   - // TODO thingsKit lightbox 默认缩放为1
3388   - this.editor.graph.view.setScale(1);
  3389 + // this.editor.graph.maxFitScale = this.lightboxMaxFitScale;
  3390 + // this.editor.graph.fit(border, null, null, null, null, null, maxHeight);
  3391 + // this.editor.graph.maxFitScale = null;
  3392 + // // TODO thingsKit lightbox 默认缩放为1
  3393 + // console.log(this.editor.graph.view)
  3394 + // this.editor.graph.view.setScale(1);
  3395 + /// TODO thingsKit 设置内容最大化
  3396 + var margin = 2;
  3397 + var max = 3;
  3398 + var graph = this.editor.graph
  3399 + var pageFormat = graph.pageFormat
  3400 + var bounds = graph.getGraphBounds();
  3401 + var cw = graph.container.clientWidth - margin;
  3402 + var ch = graph.container.clientHeight - margin;
  3403 + var w = pageFormat.width
  3404 + // var w = pageFormat.width / graph.view.scale;
  3405 + var h = pageFormat.height;
  3406 + // var h = pageFormat.height / graph.view.scale;
  3407 + var s = Math.min(max, Math.min(cw / w, ch / h));
  3408 + graph.view.scaleAndTranslate(s,
  3409 + (margin + cw - w * s) / (2 * s) - bounds.x / graph.view.scale,
  3410 + (margin + ch - h * s) / (2 * s) - bounds.y / graph.view.scale);
3389 3411 }
3390 3412 };
3391 3413
... ...
... ... @@ -4903,8 +4903,8 @@ DataFormatPanel.prototype.addDataFont = function (container) {
4903 4903 const ss = ui.getSelectionState();
4904 4904 const vertices = ss.vertices || []
4905 4905 const sidebarInstance = ui.sidebar
4906   - console.log(vertices)
4907   - console.log(ui)
  4906 + // console.log(vertices)
  4907 + // console.log(ui)
4908 4908
4909 4909 const hasModifyNotSave = editor.status
4910 4910
... ... @@ -4922,7 +4922,7 @@ DataFormatPanel.prototype.addDataFont = function (container) {
4922 4922 const graphId = vertices[0].id;
4923 4923
4924 4924 // 解构全局属性layui要用到的模块
4925   - const { layer, form, jquery: $, colorpicker, upload, element } = layui;
  4925 + const { layer, form, jquery: $, colorpicker, upload, element, laydate } = layui;
4926 4926
4927 4927 const CONTAINER_FILTER = 'containerFilter'
4928 4928 $(container).addClass('layui-form').attr('lay-filter', CONTAINER_FILTER)
... ... @@ -4959,8 +4959,8 @@ DataFormatPanel.prototype.addDataFont = function (container) {
4959 4959
4960 4960 /**
4961 4961 * @description 覆盖当前节点数据
4962   - * @param {'act' | 'dataSources' | 'event'} key
4963   - * @param {string}
  4962 + * @param {'act' | 'dataSources' | 'event'} key
  4963 + * @param {string}
4964 4964 */
4965 4965 function overrideCurrentData(key, uuid = 'id', value = {}) {
4966 4966 if (!currentNodeData[key]) currentNodeData[key] = []
... ... @@ -5063,7 +5063,8 @@ DataFormatPanel.prototype.addDataFont = function (container) {
5063 5063 GATEWAY: 'GATEWAY',
5064 5064 ADDITIONAL: 'additional',
5065 5065 DEVICE_PROFILE_ID: 'deviceProfileId',
5066   - DEVICE_TYPE: 'deviceType'
  5066 + DEVICE_TYPE: 'deviceType',
  5067 + SOURCE_OPTION: 'sourceOption'
5067 5068 }
5068 5069
5069 5070 /**
... ... @@ -5132,7 +5133,9 @@ DataFormatPanel.prototype.addDataFont = function (container) {
5132 5133 [permissionKey.VIDEO]: createVideoBindPanel,
5133 5134 [permissionKey.SWITCH_STATE_SETTING]: createSwitchStateSettingPanel,
5134 5135 [permissionKey.ONLY_SINGLE_EVENT]: createParamsSettingButtonPanel,
5135   - [permissionKey.RUNNING_AND_STOP]: createRunningAndStopPanel
  5136 + [permissionKey.RUNNING_AND_STOP]: createRunningAndStopPanel,
  5137 + [permissionKey.FLOWMETER_PANEL]: createFlowmeterPanel,
  5138 + [permissionKey.ALARM_LIST_PANEL]: createAlarmListPanel
5136 5139 }
5137 5140
5138 5141
... ... @@ -5142,7 +5145,7 @@ DataFormatPanel.prototype.addDataFont = function (container) {
5142 5145 const permission = graph.getAttributeForCell(cell, basicAttr.COMPONENT_TYPE)
5143 5146 const needDisplayPanel = sidebarInstance.getComponentPermission(permission)
5144 5147 for (const key of needDisplayPanel) {
5145   - renderMapping[key]()
  5148 + renderMapping[key]?.()
5146 5149 }
5147 5150 if (needDisplayPanel.length) createSubmitPanel()
5148 5151 UseLayUi.nextTick(() => form.render())
... ... @@ -5177,7 +5180,7 @@ DataFormatPanel.prototype.addDataFont = function (container) {
5177 5180 const event = currentNodeData.event ?? []
5178 5181 const actionType = {}
5179 5182
5180   - const hasExistEl = $(`.layui-form[lay-filter="${CONTAINER_FILTER}"]`).find('input[type="checkbox"]')
  5183 + const hasExistEl = $(`.interaction__container`).find('input[type="checkbox"]')
5181 5184 $(hasExistEl).each((i) => {
5182 5185 $(hasExistEl[i]).attr('disabled', true)
5183 5186 })
... ... @@ -5241,6 +5244,7 @@ DataFormatPanel.prototype.addDataFont = function (container) {
5241 5244 * @type {Function}
5242 5245 */
5243 5246 const refreshFn = echoRefreshFn
  5247 +
5244 5248 echoRefreshFn = function () {
5245 5249 refreshFn.apply(this)
5246 5250 const { dataSources: [dataSource] = [] } = currentNodeData || {}
... ... @@ -5428,8 +5432,554 @@ DataFormatPanel.prototype.addDataFont = function (container) {
5428 5432 }
5429 5433
5430 5434 /**
  5435 + * @description 创建流量计面板
  5436 + */
  5437 + function createFlowmeterPanel() {
  5438 +
  5439 + const enumConst = {
  5440 + MAX_VALUE: 'maxValue',
  5441 + MIN_VALUE: 'minValue',
  5442 + BG_COLOR: 'bgColor',
  5443 + WAVE_FIRST: 'waveFirst',
  5444 + WAVE_SECOND: 'waveSecond',
  5445 + WAVE_THIRD: 'waveThird',
  5446 + COMPONENT_TYPE: 'componentType'
  5447 + }
  5448 +
  5449 + const enumEl = {
  5450 + BG_COLOR_PICKER_EL: 'bgColorPickerEl',
  5451 + WAVE_FIRST: 'waveFirstEl',
  5452 + WAVE_SECOND: 'waveSecondEl',
  5453 + WAVE_THIRD: 'waveThirdEl',
  5454 + }
  5455 +
  5456 + const componentType = Sidebar.prototype.enumComponentType
  5457 + const flowmeterType = Sidebar.prototype.enumFlowmeterType
  5458 + const { TYPE } = getFlowmeterAttrKey()
  5459 + const cell = vertices[0]
  5460 + const isThemometerComponent = cell.getAttribute(TYPE) === flowmeterType.THERMOMETER
  5461 +
  5462 + if (isThemometerComponent) return
  5463 +
  5464 + const fragment = document.createDocumentFragment()
  5465 + const title = createTitle('流量计配置')
  5466 + $(title).addClass('override__title--default')
  5467 +
  5468 + const defaultPanel = createPanel()
  5469 + $(defaultPanel).addClass('override__panel--default')
  5470 + $(defaultPanel).append(`<div style="display: none;" class="layui-form-item data-source__component-select"><label class="layui-form-label">最小值</label><div class="layui-input-block"><input class="layui-input" name="${enumConst.COMPONENT_TYPE}" value="${componentType.FLOWMETER}" placeholder="组件类型"></div></div>`)
  5471 + // $(defaultPanel).append(`<div class="layui-form-item data-source__component-select"><label class="layui-form-label">最小值</label><div class="layui-input-block"><input class="layui-input" name="${enumConst.MIN_VALUE}" value="0" placeholder="请输入最小值"></div></div>`)
  5472 + // $(defaultPanel).append(`<div class="layui-form-item data-source__component-select"><label class="layui-form-label">最大值</label><div class="layui-input-block"><input class="layui-input" name="${enumConst.MAX_VALUE}" value="100" placeholder="请输入最大值"></div></div>`)
  5473 + $(defaultPanel).append(`<div style="display: ${isThemometerComponent ? 'none' : 'block'}" class="layui-form-item data-source__component-select"><label class="layui-form-label">背景色</label><div class="layui-input-block" style="display: flex; align-items: center;"><input style="border: none;" id="${enumEl.BG_COLOR_PICKER_EL}" type="color" name="${enumConst.BG_COLOR}" value="#8badcb" /></div></div>`)
  5474 + $(defaultPanel).append(`<div style="display: ${isThemometerComponent ? 'none' : 'block'}" class="layui-form-item data-source__component-select"><label class="layui-form-label">颜色一</label><div class="layui-input-block" style="display: flex; align-items: center;"><input style="border: none;" id="${enumEl.WAVE_FIRST}" type="color" name="${enumConst.WAVE_FIRST}" value="#4579e2" /></div></div>`)
  5475 + $(defaultPanel).append(`<div style="display: ${isThemometerComponent ? 'none' : 'block'}" class="layui-form-item data-source__component-select"><label class="layui-form-label">颜色二</label><div class="layui-input-block" style="display: flex; align-items: center;"><input style="border: none;" id="${enumEl.WAVE_SECOND}" type="color" name="${enumConst.WAVE_SECOND}" value="#3461c1" /></div></div>`)
  5476 + $(defaultPanel).append(`<div style="display: ${isThemometerComponent ? 'none' : 'block'}" class="layui-form-item data-source__component-select"><label class="layui-form-label">颜色三</label><div class="layui-input-block" style="display: flex; align-items: center;"><input style="border: none;" id="${enumEl.WAVE_THIRD}" type="color" name="${enumConst.WAVE_THIRD}" value="#2d55aa" /></div></div>`)
  5477 +
  5478 +
  5479 + fragment.append(title)
  5480 + fragment.append(defaultPanel)
  5481 + $(container).append(fragment)
  5482 +
  5483 + function init() {
  5484 +
  5485 + const refreshFn = echoRefreshFn
  5486 + echoRefreshFn = function () {
  5487 + refreshFn.apply(this)
  5488 + const { dataSources: [dataSource] = [] } = currentNodeData || {}
  5489 + form.val(CONTAINER_FILTER, dataSource?.additional || {})
  5490 + }
  5491 +
  5492 + form.render()
  5493 + }
  5494 +
  5495 + init()
  5496 + }
  5497 +
  5498 + /**
  5499 + * @description 创建告警列表面板
  5500 + */
  5501 + function createAlarmListPanel() {
  5502 +
  5503 + const enumActionEl = {
  5504 + BIND_DEVICE_ICON: 'bindDevice',
  5505 + DATE_RANGE: 'dateRange',
  5506 + DEVICES_INPUT: 'devicesGroup',
  5507 + AUTO_PLAY: 'autoPlay',
  5508 + START_TIME: 'startTime',
  5509 + END_TIME: 'endTime',
  5510 + }
  5511 +
  5512 + const enumFields = {
  5513 + DEVICE_INFO: 'devicesInfo',
  5514 + AUTO_PLAY: 'autoPlay',
  5515 + INTERVAL: 'interval',
  5516 + START_TIME: 'startTime',
  5517 + END_TIME: 'endTime',
  5518 + }
  5519 +
  5520 + const fragment = document.createDocumentFragment()
  5521 + const title = createTitle('数据绑定')
  5522 + $(title).addClass('override__title--default')
  5523 +
  5524 +
  5525 + const deviceGroupPanel = createPanel()
  5526 + $(deviceGroupPanel).addClass('override__panel--default')
  5527 + $(deviceGroupPanel).append(`<div style="display: flex; justify-content: space-between;"><input style="display: none;" id="${enumActionEl.DEVICES_INPUT}" name="${enumFields.DEVICE_INFO}" /><div>设备绑定</div><i id="${enumActionEl.BIND_DEVICE_ICON}" class="layui-icon-edit layui-icon" style="cursor: pointer;"><i/></div>`)
  5528 +
  5529 + const queryTimeRangePanel = createPanel()
  5530 + $(queryTimeRangePanel).addClass('override__panel--default')
  5531 + $(queryTimeRangePanel).append(`<input style="display: none;" type="number" id="${enumActionEl.START_TIME}" name="${enumFields.START_TIME}"><input style="display: none;" type="number" id="${enumActionEl.END_TIME}" name="${enumFields.END_TIME}">`)
  5532 + $(queryTimeRangePanel).append(`<div class="layui-form-item" style="margin-bottom: 0;"><label class="layui-form-label" style="width: 80px; padding: 9px 0; text-align: left;">查询时间</label><div class="layui-input-block" style="margin-left: 80px;"><input style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;" class="layui-input" id="${enumActionEl.DATE_RANGE}" autocomplete="off" placeholder="请选择查询日期"></div></div>`)
  5533 +
  5534 + const autoPlayPanel = createPanel()
  5535 + $(autoPlayPanel).addClass('override__panel--default')
  5536 + $(autoPlayPanel).append(`<div class="layui-form-item" style="margin-bottom: 0;" id="${enumActionEl.AUTO_PLAY}"><label class="layui-form-label" style="width: 80px; padding: 9px 0; text-align: left;">自动滚动</label><div class="layui-input-block" style="margin-left: 80px;"><input type="checkbox" name="${enumFields.AUTO_PLAY}" title="开启|关闭" lay-skin="switch"> </div></div>`)
  5537 + // $(autoPlayPanel).append(`<div class="layui-form-item" style="margin-bottom: 0;" id="${enumActionEl.AUTO_PLAY}"><label class="layui-form-label" style="width: 80px; padding: 9px 0; text-align: left;">自动滚动</label><div class="layui-input-block" style="margin-left: 80px;"><input type="radio" name="${enumActionEl.AUTO_PLAY}" value="true" title="开" checked><input type="radio" name="${enumActionEl.AUTO_PLAY}" value="false" title="关"></div></div>`)
  5538 +
  5539 + const playIntervalPanel = createPanel()
  5540 + $(playIntervalPanel).addClass('override__panel--default')
  5541 + $(playIntervalPanel).append(`<div class="layui-form-item" style="margin-bottom: 0;"><label class="layui-form-label" style="width: 80px; padding: 9px 0; text-align: left;">停留时间()</label><div class="layui-input-block" style="margin-left: 80px;"><input class="layui-input" name="${enumFields.INTERVAL}" placeholder="请输入报警停留时间" /></div></div>`)
  5542 +
  5543 + function openBindDeviceLayer() {
  5544 +
  5545 + const enumTableActionEl = {
  5546 + TABLE_ID: 'deviceGroupEl',
  5547 + TREE_CLASS: 'org-tree',
  5548 + ADD_ROW_BUTTON: 'addRow',
  5549 + DELETE_BUTTON: 'delete-btn',
  5550 + SUBMIT_BUTTON: 'deviceGroupsSubmitBtn'
  5551 + }
  5552 +
  5553 + const enumLayFilter = {
  5554 + ROW_FILTER_NAME: 'deviceBindRowFilter',
  5555 + SUBMIT: 'deviceGroupsSubmit'
  5556 + }
  5557 +
  5558 + const enumDeviceKeys = {
  5559 + DEVICE_TYPE: 'deviceType',
  5560 + ORGANIZATION_ID: 'organizationId',
  5561 + DEVICE_PROFILE_ID: 'deviceProfileId',
  5562 + DEVICE_ID: 'deviceId'
  5563 + }
  5564 +
  5565 + let rowCount = 0
  5566 +
  5567 + let deviceType = []
  5568 +
  5569 + let organizationList = []
  5570 +
  5571 + const getRowFilter = () => {
  5572 + rowCount++
  5573 + return `${enumLayFilter.ROW_FILTER_NAME}-${rowCount}`
  5574 + }
  5575 +
  5576 + async function getOrganization(organizationId, rowFilter) {
  5577 + if (!organizationList.length) {
  5578 + const [err, res] = await to(ConfigurationNodeApi.getOrgTree())
  5579 + if (!err) organizationList = res
  5580 + }
  5581 +
  5582 + $(`#${enumTableActionEl.TABLE_ID}`).find(`tbody tr .${enumTableActionEl.TREE_CLASS}`).each((_index, item) => {
  5583 +
  5584 + const rowFilter = $(item).parents(`tr`).attr('lay-filter')
  5585 +
  5586 + if ($(item).data('initialize')) return
  5587 + $(item).data('initialize', true)
  5588 +
  5589 + UseLayUi.createTreeSelect({
  5590 + elem: item,
  5591 + layFilter: enumDeviceKeys.ORGANIZATION_ID,
  5592 + singleUsage: false,
  5593 + hiddenLabel: true,
  5594 + layVerify: 'required',
  5595 + layVerType: 'tips',
  5596 + popupMountToBody: true,
  5597 + treeProps: {
  5598 + data: organizationList,
  5599 + onlyIconControl: true,
  5600 + click(node) {
  5601 + getDevice(rowFilter)
  5602 + },
  5603 + },
  5604 + })
  5605 + })
  5606 +
  5607 + form.render()
  5608 +
  5609 + if (organizationId) {
  5610 + UseLayUi.nextTick(() => {
  5611 + const node = UseLayUi.findTreeObjectByField(organizationList, organizationId)
  5612 + $(`#${enumTableActionEl.TABLE_ID} tr[lay-filter="${rowFilter}"] input[name="${enumDeviceKeys.ORGANIZATION_ID}"]`).val(organizationId).parent().find('span').html(node?.name)
  5613 + })
  5614 + }
  5615 + }
  5616 +
  5617 + async function getDeviceType(value, rowFilter) {
  5618 + if (!deviceType.length) {
  5619 + const [err, res] = await to(ConfigurationNodeApi.getDictionaryValue('device_type'))
  5620 + if (!err) deviceType = res
  5621 + }
  5622 + const template = UseLayUi.generateOptionTemplate({ dataSource: deviceType, labelField: 'itemText', valueField: 'itemValue' })
  5623 + $(`#${enumTableActionEl.TABLE_ID}`).find(`select[name="${enumDeviceKeys.DEVICE_TYPE}"]`).each((_index, target) => {
  5624 + // if ($(target).children()) return
  5625 + if ($(target).data('initialize')) return
  5626 + $(target).data('initialize', true)
  5627 + $(target).html(template)
  5628 + })
  5629 + form.render()
  5630 +
  5631 + if (value) {
  5632 + UseLayUi.nextTick(() => {
  5633 + form.val(rowFilter, { [enumDeviceKeys.DEVICE_TYPE]: value })
  5634 + })
  5635 + }
  5636 + }
  5637 +
  5638 + async function getDeviceProfile(currentRowFilter, record, value) {
  5639 + const { deviceType } = record || form.val(currentRowFilter) || {}
  5640 + if (!deviceType) return
  5641 + const [err, res] = await to(ConfigurationNodeApi.getProduct(deviceType))
  5642 + const deviceProfile = res
  5643 + const template = UseLayUi.generateOptionTemplate({ dataSource: deviceProfile, labelField: 'name', valueField: 'id' })
  5644 + $(`#${enumTableActionEl.TABLE_ID}`).find(`tr[lay-filter="${currentRowFilter}"] select[name="${enumDeviceKeys.DEVICE_PROFILE_ID}"]`).html(template)
  5645 + form.render()
  5646 +
  5647 + if (value) {
  5648 + UseLayUi.nextTick(() => {
  5649 + form.val(currentRowFilter, { [enumDeviceKeys.DEVICE_PROFILE_ID]: value })
  5650 + })
  5651 + }
  5652 + }
  5653 +
  5654 + async function getDevice(currentRowFilter, record, value) {
  5655 + const { deviceProfileId, organizationId, deviceType } = record || form.val(currentRowFilter) || {}
  5656 + if (![deviceProfileId, organizationId, deviceType].every(Boolean)) return
  5657 + const [err, res] = await to(ConfigurationNodeApi.getMeetConditionsDevice({ deviceProfileId, deviceType, organizationId }))
  5658 + if (err) return
  5659 + const template = UseLayUi.generateOptionTemplate({ dataSource: res, labelField: 'name', valueField: 'id', alias: 'alias' })
  5660 + $(`#${enumTableActionEl.TABLE_ID}`).find(`tr[lay-filter="${currentRowFilter}"] select[name="${enumDeviceKeys.DEVICE_ID}"]`).html(template)
  5661 + form.render()
  5662 + if (value) {
  5663 + UseLayUi.nextTick(() => {
  5664 + form.val(currentRowFilter, { [enumDeviceKeys.DEVICE_ID]: value })
  5665 + })
  5666 + }
  5667 + }
  5668 +
  5669 + function echoDeviceBindInfo() {
  5670 + const dataSource = currentNodeData?.dataSources?.find(item => item.nodeId === nodeInfo.id)
  5671 + if (!dataSource) return
  5672 + const record = dataSource.sourceOption
  5673 + const devicesInfo = record.devicesInfo
  5674 + const value = UseLayUi.parseStringToJSON(devicesInfo, [])
  5675 + if (value.length) {
  5676 + $(`#${enumTableActionEl.TABLE_ID}`).find('tbody').empty()
  5677 + }
  5678 + value.forEach((item) => {
  5679 + const template = createRow()
  5680 + const rowFilter = `${enumLayFilter.ROW_FILTER_NAME}-${rowCount}`
  5681 + $(`#${enumTableActionEl.TABLE_ID}`).find('tbody').append(template)
  5682 +
  5683 + // form.render(null, rowFilter)
  5684 + getDeviceType(item[enumDeviceKeys.DEVICE_TYPE], rowFilter)
  5685 + getOrganization(item[enumDeviceKeys.ORGANIZATION_ID], rowFilter)
  5686 + getDeviceProfile(rowFilter, item, item[enumDeviceKeys.DEVICE_PROFILE_ID])
  5687 + getDevice(rowFilter, item, item[enumDeviceKeys.DEVICE_ID])
  5688 + })
  5689 + }
  5690 +
  5691 + function createEventListener(index) {
  5692 +
  5693 + /**
  5694 + * @description 下拉选项定位
  5695 + */
  5696 + $(`#${enumTableActionEl.TABLE_ID}`).on('click', '.layui-form-select', event => {
  5697 + const width = event.currentTarget.offsetWidth || 200
  5698 + const height = event.currentTarget.offsetHeight || 38
  5699 + const offset = $(event.currentTarget).offset()
  5700 + $(event.currentTarget).find('dl').css({
  5701 + position: 'fixed',
  5702 + 'min-width': width + 'px',
  5703 + top: offset.top + height + 'px',
  5704 + left: offset.left + 'px'
  5705 + })
  5706 + })
  5707 +
  5708 + $(`#${enumTableActionEl.ADD_ROW_BUTTON}`).on('click', (event) => {
  5709 + const template = createRow()
  5710 + $(`#${enumTableActionEl.TABLE_ID}`).find('tbody').append(template)
  5711 + getDeviceType()
  5712 + getOrganization()
  5713 + form.render()
  5714 + })
  5715 +
  5716 + $(`#${enumTableActionEl.TABLE_ID}`).on('click', `tr .${enumTableActionEl.DELETE_BUTTON}`, (event) => {
  5717 + $(event.currentTarget).parents('tr').remove()
  5718 + })
  5719 +
  5720 + form.on('select', (data) => {
  5721 + const { elem, value } = data || {}
  5722 + if (!elem) return
  5723 +
  5724 + const key = $(elem).attr('name')
  5725 + const currentRow = $(elem).parents('tr')
  5726 + const currentRowFilter = $(currentRow).attr('lay-filter')
  5727 +
  5728 + if (key === enumDeviceKeys.DEVICE_TYPE) {
  5729 + getDeviceProfile(currentRowFilter)
  5730 + form.val(currentRowFilter, {
  5731 + deviceId: null,
  5732 + deviceProfileId: null
  5733 + })
  5734 + }
  5735 +
  5736 + if (key === enumDeviceKeys.DEVICE_PROFILE_ID) {
  5737 + getDevice(currentRowFilter)
  5738 + form.val(currentRowFilter, {
  5739 + deviceId: null
  5740 + })
  5741 + }
  5742 + })
  5743 +
  5744 + form.on(`submit`, () => {
  5745 + const deviceGroupValues = []
  5746 + $(`#${enumTableActionEl.TABLE_ID}`).find(`tbody tr`).each((_index, item) => {
  5747 + const rowFilter = $(item).attr('lay-filter')
  5748 + const value = form.val(rowFilter)
  5749 + deviceGroupValues.push(value)
  5750 + $(`#${enumActionEl.DEVICES_INPUT}`).val(JSON.stringify(deviceGroupValues))
  5751 + layer.close(index)
  5752 + })
  5753 + })
  5754 + }
  5755 +
  5756 + function createRow() {
  5757 + const template = `
  5758 + <tr lay-filter="${getRowFilter()}" class="layui-form">
  5759 + <td class="layui-form-item" style="margin: 0;">
  5760 + <div>
  5761 + <select lay-verify="required" lay-verType="tips" name="${enumDeviceKeys.DEVICE_TYPE}"></select>
  5762 + </div>
  5763 + </td>
  5764 + <td class="layui-form-item" style="margin: 0;">
  5765 + <div>
  5766 + <select lay-verify="required" lay-verType="tips" name="${enumDeviceKeys.DEVICE_PROFILE_ID}"></select>
  5767 + </div>
  5768 + </td>
  5769 + <td class="layui-form-item" style="margin: 0;">
  5770 + <div class="${enumTableActionEl.TREE_CLASS}" style="width: 100%;">
  5771 + <select lay-verify="required" lay-verType="tips" name="${enumDeviceKeys.ORGANIZATION_ID}"></select>
  5772 + </div>
  5773 + </td>
  5774 + <td class="layui-form-item" style="margin: 0;">
  5775 + <div>
  5776 + <select lay-verify="required" lay-verType="tips" name="${enumDeviceKeys.DEVICE_ID}"></select>
  5777 + </div>
  5778 + </td>
  5779 + <td>
  5780 + <div class="${enumTableActionEl.DELETE_BUTTON}" style="cursor: pointer; color: #1E9FFF;">删除</div>
  5781 + </td>
  5782 + </tr>
  5783 + `
  5784 + return template
  5785 + }
  5786 +
  5787 + layer.open({
  5788 + title: '设备绑定',
  5789 + area: ['800px', '540px'],
  5790 + content: `
  5791 + <div class="override__table layui-form">
  5792 + <table id="${enumTableActionEl.TABLE_ID}" class="layui-table">
  5793 + <colgroup>
  5794 + <col width="150">
  5795 + <col width="150">
  5796 + <col width="150">
  5797 + <col width="150">
  5798 + <col width="100">
  5799 + </colgroup>
  5800 + <thead>
  5801 + <tr>
  5802 + <th>设备类型</th>
  5803 + <th>产品</th>
  5804 + <th>组织</th>
  5805 + <th>设备</th>
  5806 + <th>操作</th>
  5807 + </tr>
  5808 + </thead>
  5809 + <tbody>
  5810 + ${createRow()}
  5811 + </tbody>
  5812 + </table>
  5813 + <div style="display: flex; justify-content: center;">
  5814 + <button id="${enumTableActionEl.ADD_ROW_BUTTON}" type="button" class="layui-btn layui-btn-fluid layui-btn-normal">增加</button>
  5815 + </div>
  5816 + </div>
  5817 + `,
  5818 + async success(el, index) {
  5819 + form.render()
  5820 + await getDeviceType()
  5821 + await getOrganization()
  5822 + createEventListener(index)
  5823 + $(el).addClass('layui-form').find('.layui-layer-btn0').attr('lay-submit', true).attr('lay-filter', enumTableActionEl.SUBMIT_BUTTON)
  5824 + echoDeviceBindInfo()
  5825 + },
  5826 + yes() {
  5827 + return false
  5828 + }
  5829 + })
  5830 + }
  5831 +
  5832 + function createDateRange() {
  5833 + laydate.render({
  5834 + elem: `#${enumActionEl.DATE_RANGE}`,
  5835 + type: 'datetime',
  5836 + range: true,
  5837 + shortcuts: [
  5838 + {
  5839 + text: '过去一小时',
  5840 + value: (() => {
  5841 + const now = Date.now()
  5842 + const date1 = new Date(now - 1000 * 60 * 60)
  5843 + return [date1, new Date(now)]
  5844 + })()
  5845 + },
  5846 + {
  5847 + text: '今天',
  5848 + value: (() => {
  5849 + const date1 = new Date()
  5850 + const date2 = new Date()
  5851 + date1.setHours(0)
  5852 + date1.setMinutes(0)
  5853 + date1.setSeconds(0)
  5854 + date2.setHours(23)
  5855 + date2.setMinutes(59)
  5856 + date2.setSeconds(59)
  5857 + return [date1, date2]
  5858 + })()
  5859 + },
  5860 + {
  5861 + text: '过去七天',
  5862 + value: (() => {
  5863 + const now = Date.now()
  5864 + const date1 = new Date(now - 1000 * 60 * 60 * 24 * 7)
  5865 + return [date1, new Date(now)]
  5866 + })()
  5867 + },
  5868 + {
  5869 + text: '过去三十天',
  5870 + value: (() => {
  5871 + const now = Date.now()
  5872 + const date1 = new Date(now - 1000 * 60 * 60 * 24 * 30)
  5873 + return [date1, new Date(now)]
  5874 + })()
  5875 + },
  5876 + {
  5877 + text: '本月',
  5878 + value: (() => {
  5879 + const date1 = new Date()
  5880 + let date2 = new Date()
  5881 + date1.setDate(1)
  5882 + date1.setHours(0)
  5883 + date1.setMinutes(0)
  5884 + date1.setSeconds(0)
  5885 +
  5886 + date2.setMonth(date2.getMonth() + 1)
  5887 + date2 = new Date(date2.valueOf() - 1000 * 60 * 60 * ((date2.getDate()) * 24))
  5888 + date2.setHours(23)
  5889 + date2.setMinutes(59)
  5890 + date2.setSeconds(59)
  5891 + return [date1, date2]
  5892 + })()
  5893 + },
  5894 + {
  5895 + text: '近半年',
  5896 + value: (() => {
  5897 + const date1 = new Date()
  5898 + const date2 = new Date()
  5899 +
  5900 + date1.getMonth() < 6
  5901 + ? date1.setFullYear(date1.getFullYear() - 1) && date1.setMonth(12 - (6 % (date1.getMonth())))
  5902 + : date1.setMonth(date1.getMonth() - 6)
  5903 +
  5904 + return [date1, date2]
  5905 + })()
  5906 + },
  5907 + {
  5908 + text: '近一年',
  5909 + value: (() => {
  5910 + const date1 = new Date()
  5911 + const date2 = new Date()
  5912 + date1.setFullYear(date1.getFullYear() - 1)
  5913 + return [date1, date2]
  5914 + })()
  5915 + }
  5916 + ],
  5917 + done(value, date, endDate) {
  5918 + const [startTime, endTime] = value.split(' - ')
  5919 + const startTs = new Date(startTime).valueOf()
  5920 + const endTs = new Date(endTime).valueOf()
  5921 +
  5922 + $(`#${enumActionEl.START_TIME}`).val(startTs)
  5923 + $(`#${enumActionEl.END_TIME}`).val(endTs)
  5924 + }
  5925 + })
  5926 + }
  5927 +
  5928 + function formatTimespanToDateString(timespan) {
  5929 + if (isNaN(timespan)) return
  5930 + const date = new Date(Number(timespan))
  5931 + const year = date.getFullYear()
  5932 + const month = (date.getMonth() + 1).toString().padStart(2, 0)
  5933 + const day = date.getDate().toString().padStart(2, 0)
  5934 + const hour = date.getHours().toString().padStart(2, 0)
  5935 + const minute = date.getMinutes().toString().padStart(2, 0)
  5936 + const second = date.getSeconds().toString().padStart(2, 0)
  5937 +
  5938 + return `${year}-${month}-${day} ${hour}:${minute}:${second}`
  5939 + }
  5940 +
  5941 + async function echoData() {
  5942 + const getBindValue = currentNodeData?.dataSources?.find(item => item.nodeId === nodeInfo.id)
  5943 + if (!getBindValue) return
  5944 + const record = getBindValue.sourceOption
  5945 + form.val(CONTAINER_FILTER, record)
  5946 +
  5947 + const startTime = formatTimespanToDateString(record.startTime)
  5948 + const endTime = formatTimespanToDateString(record.endTime)
  5949 + laydate.render({
  5950 + elem: `#${enumActionEl.DATE_RANGE}`,
  5951 + value: `${startTime} - ${endTime}`
  5952 + })
  5953 + }
  5954 +
  5955 + function init() {
  5956 + $(`#${enumActionEl.BIND_DEVICE_ICON}`).on('click', openBindDeviceLayer)
  5957 + form.render()
  5958 +
  5959 + createDateRange()
  5960 +
  5961 + const refreshFN = echoRefreshFn
  5962 + echoRefreshFn = async function () {
  5963 + refreshFN.apply(this, arguments)
  5964 + await echoData()
  5965 + }
  5966 +
  5967 + }
  5968 +
  5969 + fragment.append(title)
  5970 + fragment.append(deviceGroupPanel)
  5971 + fragment.append(queryTimeRangePanel)
  5972 + fragment.append(autoPlayPanel)
  5973 + fragment.append(playIntervalPanel)
  5974 + $(container).append(fragment)
  5975 +
  5976 + UseLayUi.nextTick(init)
  5977 +
  5978 + }
  5979 +
  5980 + /**
5431 5981 * @description 是否是折线图
5432   - * @param {boolean} isLineChart
  5982 + * @param {boolean} isLineChart
5433 5983 */
5434 5984 function createChartBindPanel(chartType) {
5435 5985 const fragment = document.createDocumentFragment()
... ... @@ -5900,6 +6450,7 @@ DataFormatPanel.prototype.addDataFont = function (container) {
5900 6450 }
5901 6451 const { field } = data
5902 6452 const value = getValueOnSubmit(field)
  6453 + if (!value) return
5903 6454 await to(autoSaveGraphInfo())
5904 6455 const [err, res] = await to(ConfigurationNodeApi.updateNodeInfo(value))
5905 6456 if (err) return
... ... @@ -5927,12 +6478,14 @@ DataFormatPanel.prototype.addDataFont = function (container) {
5927 6478 [componentType.VIDEO]: getVideoSubmitValue,
5928 6479 [componentType.SWITCH]: getSwitchSubmitValue,
5929 6480 [componentType.PARAMS_SETTING_BUTTON]: getSwitchSubmitValue,
5930   - [componentType.IMAGE]: getSubmitValue
  6481 + [componentType.IMAGE]: getSubmitValue,
  6482 + [componentType.FLOWMETER]: getFlowmeterSubmitValue,
  6483 + [componentType.ALARM_LIST]: getAlarmListSubmitValue,
5931 6484 }
5932 6485
5933 6486 const cell = vertices[0]
5934 6487 const type = graph.getAttributeForCell(cell, basicAttr.COMPONENT_TYPE)
5935   - return renderMapping[type]?.(field) || {}
  6488 + return renderMapping[type]?.(field) || false
5936 6489
5937 6490 function getSubmitValue(field) {
5938 6491 const ENABLED_FLAG = 'on'
... ... @@ -6027,8 +6580,8 @@ DataFormatPanel.prototype.addDataFont = function (container) {
6027 6580 }
6028 6581
6029 6582 /**
6030   - * @description 处理开关组件的保存值
6031   - * @returns
  6583 + * @description 处理开关组件的保存值
  6584 + * @returns
6032 6585 */
6033 6586 function getSwitchSubmitValue(field) {
6034 6587 const dataSources = getDataSourceBindValue()
... ... @@ -6062,6 +6615,56 @@ DataFormatPanel.prototype.addDataFont = function (container) {
6062 6615 }
6063 6616 return value
6064 6617 }
  6618 +
  6619 + function getFlowmeterSubmitValue(field = {}) {
  6620 + const additionalKey = HandleDataSource.enumConst
  6621 +
  6622 + const value = {
  6623 + configurationId,
  6624 + contentId: currentPageId.id,
  6625 + nodeId: graphId,
  6626 + [enumCategory.ACT]: [],
  6627 + [enumCategory.EVENT]: [],
  6628 + [enumCategory.DATA_SOURCE]: {
  6629 + [enumDataSourceConst.ORG_ID]: field[enumDataSourceConst.ORG_ID],
  6630 + [enumDataSourceConst.DEVICE_ID]: field[enumDataSourceConst.DEVICE_ID],
  6631 + [enumDataSourceConst.DEVICE_TYPE]: field[enumDataSourceConst.DEVICE_TYPE],
  6632 + [enumDataSourceConst.DEVICE_PROFILE_ID]: field[enumDataSourceConst.DEVICE_PROFILE_ID],
  6633 + [enumDataSourceConst.ATTR]: field[enumDataSourceConst.ATTR],
  6634 + [enumDataSourceConst.ADDITIONAL]: {
  6635 + [additionalKey.COMPONENT_TYPE]: field[additionalKey.COMPONENT_TYPE],
  6636 + // [additionalKey.MIN_VALUE]: field[additionalKey.MIN_VALUE],
  6637 + // [additionalKey.MAX_VALUE]: field[additionalKey.MAX_VALUE],
  6638 + [additionalKey.BG_COLOR]: field[additionalKey.BG_COLOR],
  6639 + [additionalKey.WAVE_FIRST_COLOR]: field[additionalKey.WAVE_FIRST_COLOR],
  6640 + [additionalKey.WAVE_SECOND_COLOR]: field[additionalKey.WAVE_SECOND_COLOR],
  6641 + [additionalKey.WAVE_THIRD_COLOR]: field[additionalKey.WAVE_THIRD_COLOR],
  6642 + }
  6643 + },
  6644 + }
  6645 + return value
  6646 + }
  6647 +
  6648 + function getAlarmListSubmitValue(filed = {}) {
  6649 + console.log(field?.devicesInfo)
  6650 + if (!field?.devicesInfo) {
  6651 + UseLayUi.errorMsg('请先进行设备绑定!')
  6652 + return
  6653 + }
  6654 + const value = {
  6655 + configurationId,
  6656 + contentId: currentPageId.id,
  6657 + nodeId: graphId,
  6658 + [enumCategory.ACT]: [],
  6659 + [enumCategory.EVENT]: [],
  6660 + [enumCategory.DATA_SOURCE]: {
  6661 + [enumDataSourceConst.ADDITIONAL]: { componentType: 'alarmList' },
  6662 + [enumDataSourceConst.SOURCE_OPTION]: field
  6663 + }
  6664 + }
  6665 +
  6666 + return value
  6667 + }
6065 6668 }
6066 6669
6067 6670 /**
... ... @@ -7032,7 +7635,7 @@ DataFormatPanel.prototype.addDataFont = function (container) {
7032 7635 }
7033 7636
7034 7637 /**
7035   - * @description
  7638 + * @description
7036 7639 */
7037 7640 const recordData = {
7038 7641 enabled: false
... ... @@ -7049,7 +7652,7 @@ DataFormatPanel.prototype.addDataFont = function (container) {
7049 7652
7050 7653 // 参数设置
7051 7654 [enumConst.JSON_COMMAND]: content[enumConst.JSON_COMMAND],
7052   - [enumConst.WAY]: content[enumConst.WAY],
  7655 + [enumConst.WAY]: content[enumConst.WAY] || enumWayType.ONE_WAY,
7053 7656 [enumConst.COMMAND_TYPE]: content[enumConst.COMMAND_TYPE],
7054 7657 [enumConst.TCP_COMMAND]: content[enumConst.TCP_COMMAND],
7055 7658 [enumConst.TRANSPORTTYPE]: content[enumConst.TRANSPORTTYPE],
... ... @@ -7073,7 +7676,7 @@ DataFormatPanel.prototype.addDataFont = function (container) {
7073 7676
7074 7677 /**
7075 7678 * @description 控制form
7076   - * @param {enumActionType} value
  7679 + * @param {enumActionType} value
7077 7680 */
7078 7681 async function controlFormDisplay(value, isTCP, isCustom) {
7079 7682 if (value === enumActionType.PAGE) {
... ... @@ -7141,7 +7744,6 @@ DataFormatPanel.prototype.addDataFont = function (container) {
7141 7744 return false
7142 7745 }
7143 7746 } else {
7144   - console.log(formVal)
7145 7747 if (!isJson(formVal[enumConst.JSON_COMMAND])) {
7146 7748 UseLayUi.topErrorMsg('命令配置存在错误')
7147 7749 return false
... ... @@ -7214,7 +7816,7 @@ DataFormatPanel.prototype.addDataFont = function (container) {
7214 7816
7215 7817 /**
7216 7818 * @description 生成命令类型选项
7217   - * @returns
  7819 + * @returns
7218 7820 */
7219 7821 function generateCommandTypeOptions() {
7220 7822 const options = [
... ... @@ -7404,7 +8006,7 @@ DataFormatPanel.prototype.addDataFont = function (container) {
7404 8006 <label class="layui-form-label" style="width: 120px;">单向/双向 ${createHelpMessage(`单向:服务器向网关设备、直连设备发送指令。发送指令后,设备不会返回任何信息。\n
7405 8007 双向:服务器向网关设备、直连设备发送指令。发送指令后,设备返回响应信息。`, 'way')}</label>
7406 8008 <div class="layui-input-block" style="margin-left: 150px;">
7407   - <input type="radio" name="${enumConst.WAY}" value="${enumWayType.ONE_WAY}" title="单向" checked="">
  8009 + <input type="radio" name="${enumConst.WAY}" value="${enumWayType.ONE_WAY}" title="单向" checked>
7408 8010 <input type="radio" name="${enumConst.WAY}" value="${enumWayType.TWO_WAY}" title="双向">
7409 8011 </div>
7410 8012 </div>
... ... @@ -7527,6 +8129,7 @@ DataFormatPanel.prototype.addDataFont = function (container) {
7527 8129 SLAVE_DEVICE_ID: 'slaveDeviceId',
7528 8130 ATTR: 'attr',
7529 8131 GATEWAY: 'GATEWAY',
  8132 + TITLE: 'title'
7530 8133 }
7531 8134
7532 8135 const enumDisplayType = HandleDynamicEffect.enumDisplayType
... ... @@ -7563,6 +8166,16 @@ DataFormatPanel.prototype.addDataFont = function (container) {
7563 8166 `
7564 8167 }
7565 8168
  8169 + function generateTitle() {
  8170 + return `
  8171 + <div class="layui-form-item" style="margin-bottom: 0px">
  8172 + <div class="layui-input-block" style="margin-left: 0px;">
  8173 + <input name="${enumConst.TITLE}" class="layui-input" lay-verType="tips" />
  8174 + </div>
  8175 + </div>
  8176 + `
  8177 + }
  8178 +
7566 8179 /**
7567 8180 * @description 添加一条记录
7568 8181 */
... ... @@ -7577,6 +8190,7 @@ DataFormatPanel.prototype.addDataFont = function (container) {
7577 8190 <td>
7578 8191 <input autocomplete="off" lay-verType="tips" lay-verify="required" type="number" name="${enumConst.MAX}" class="layui-input ${enumActionEl.MAX_FILTER}">
7579 8192 </td>
  8193 + ${IS_DISPLAY && `<td>${generateTitle()}</td>`}
7580 8194 <td style="text-align: center;">
7581 8195 <button type="button" class="layui-btn layui-btn-primary layui-border-red ${enumActionEl.DEL_BTN_EL}">删除</button>
7582 8196 </td>
... ... @@ -7745,6 +8359,7 @@ DataFormatPanel.prototype.addDataFont = function (container) {
7745 8359 ${IS_RUNNING ? '<th style="text-align:center">类型</th>' : ''}
7746 8360 <th style="text-align:center">最小值(>=)</th>
7747 8361 <th style="text-align:center">最大值(<=)</th>
  8362 + ${IS_DISPLAY ? '<th style="text-align:center">标签</th>' : ''}
7748 8363 <th style="text-align:center">操作</th>
7749 8364 </tr>
7750 8365 </thead>
... ... @@ -7810,7 +8425,7 @@ DataFormatPanel.prototype.addDataFont = function (container) {
7810 8425
7811 8426 /**
7812 8427 * @description
7813   - * @param {} event
  8428 + * @param {} event
7814 8429 */
7815 8430 function handleStateSetting(event) {
7816 8431
... ... @@ -8296,7 +8911,7 @@ DataFormatPanel.prototype.addDataFont = function (container) {
8296 8911 }
8297 8912
8298 8913 /**
8299   - *
  8914 + *
8300 8915 * @returns {{orgId: string, attr: string, deviceId: string, devi}}
8301 8916 */
8302 8917 function getValue() {
... ... @@ -8628,7 +9243,7 @@ DataFormatPanel.prototype.addDataFont = function (container) {
8628 9243 CONTAINER_FILTER: 'imgContainerFilter',
8629 9244
8630 9245 /**
8631   - * @description
  9246 + * @description
8632 9247 */
8633 9248 SET_IMG_EL: 'variableImageTableSetImgEl',
8634 9249
... ... @@ -8662,7 +9277,7 @@ DataFormatPanel.prototype.addDataFont = function (container) {
8662 9277
8663 9278 /**
8664 9279 * @description 设置回显
8665   - * @param {} value
  9280 + * @param {} value
8666 9281 */
8667 9282 function setValue(value = {}) {
8668 9283 form.val(getFormFilter, value)
... ... @@ -8670,7 +9285,7 @@ DataFormatPanel.prototype.addDataFont = function (container) {
8670 9285
8671 9286 /**
8672 9287 * @description 获取值
8673   - * @returns
  9288 + * @returns
8674 9289 */
8675 9290 function getValue() {
8676 9291 return form.val(getFormFilter) || {}
... ... @@ -12541,10 +13156,22 @@ class UseLayUi {
12541 13156 `
12542 13157 }
12543 13158
  13159 + static parseStringToJSON(string, defaultValue = {}) {
  13160 + try {
  13161 + if (typeof string === 'string') {
  13162 + const value = JSON.parse(string)
  13163 + if (typeof value === 'object') return value
  13164 + }
  13165 + return defaultValue
  13166 + } catch (error) {
  13167 + return defaultValue
  13168 + }
  13169 + }
  13170 +
12544 13171 /**
12545 13172 * @description 生成输入框控件
12546   - * @param {{label: string, value: string, labelWidth: number, numberInput: boolean}} params
12547   - * @returns
  13173 + * @param {{label: string, value: string, labelWidth: number, numberInput: boolean}} params
  13174 + * @returns
12548 13175 */
12549 13176 static createInputTemplate({ label, value, labelWidth = 80, required = false, type = 'TEXT' }) {
12550 13177 return `
... ... @@ -12562,19 +13189,19 @@ class UseLayUi {
12562 13189 }
12563 13190
12564 13191 /**
12565   - *
  13192 + *
12566 13193 * @param {{
12567   - * accessMode: 'r' | 'w',
  13194 + * accessMode: 'r' | 'w',
12568 13195 * functionName: string,
12569   - * id: string,
12570   - * identifier: string,
  13196 + * id: string,
  13197 + * identifier: string,
12571 13198 * dataType: {
12572   - * type: 'TEXT' | 'INT' | 'DOUBLE' | 'STRUCT',
12573   - * specs: { unit: {value: string, label: string},
12574   - * unitName: string,
12575   - * valueRange: {min: number, max: number},
  13199 + * type: 'TEXT' | 'INT' | 'DOUBLE' | 'STRUCT',
  13200 + * specs: { unit: {value: string, label: string},
  13201 + * unitName: string,
  13202 + * valueRange: {min: number, max: number},
12576 13203 * length: number
12577   - * }}}[]} inputData
  13204 + * }}}[]} inputData
12578 13205 * @param {number} labelWidth = 80
12579 13206 * @param {}
12580 13207 */
... ... @@ -12592,20 +13219,20 @@ class UseLayUi {
12592 13219 return template
12593 13220 }
12594 13221 /**
12595   - *
  13222 + *
12596 13223 * @param {{
12597   - * accessMode: 'r' | 'w',
  13224 + * accessMode: 'r' | 'w',
12598 13225 * functionName: string,
12599   - * id: string,
12600   - * identifier: string,
  13226 + * id: string,
  13227 + * identifier: string,
12601 13228 * dataType: {
12602   - * type: 'TEXT' | 'INT' | 'DOUBLE' | 'STRUCT',
12603   - * specs: { unit: {value: string, label: string},
12604   - * unitName: string,
12605   - * valueRange: {min: number, max: number},
  13229 + * type: 'TEXT' | 'INT' | 'DOUBLE' | 'STRUCT',
  13230 + * specs: { unit: {value: string, label: string},
  13231 + * unitName: string,
  13232 + * valueRange: {min: number, max: number},
12606 13233 * length: number
12607   - * }}}[]} inputData
12608   - * @param {Record<string, any>} value = 80
  13234 + * }}}[]} inputData
  13235 + * @param {Record<string, any>} value = 80
12609 13236 */
12610 13237 static validateThingsModelInputDataForm(inputData, value, needFormat = false) {
12611 13238 let flag = true
... ... @@ -12708,12 +13335,13 @@ class UseLayUi {
12708 13335
12709 13336 /**
12710 13337 * @description generator options template 生产下拉选项模板
12711   - * @param options
12712   - * @param {object[]} [dataSource] options.dataSource
12713   - * @param {boolean} [addPlaceholderOption = true] options.addPlaceholderOption
12714   - * @param {string} [labelField = 'name'] options.labelField
12715   - * @param {string} [valueField = 'name'] options.valueField
12716   - * @param {string} [alias] options.alias
  13338 + * @typedef {Object} GenerateOptionTemplateParamsType
  13339 + * @property {any[]} [dataSource] options.dataSource
  13340 + * @property {boolean} [addPlaceholderOption = true] options.addPlaceholderOption
  13341 + * @property {string} [labelField = 'name'] options.labelField
  13342 + * @property {string} [valueField = 'name'] options.valueField
  13343 + * @property {string} [alias] options.alias
  13344 + * @param {GenerateOptionTemplateParamsType} options
12717 13345 * @returns {*}
12718 13346 */
12719 13347 static generateOptionTemplate(options) {
... ... @@ -12858,17 +13486,21 @@ class UseLayUi {
12858 13486 // TODO Tree Select
12859 13487 /**
12860 13488 * @description create a tree select controls
12861   - * @param {string} [options.layFilter] options.layFilter
12862   - * @param {string} [options.label] options.label
12863   - * @param {object} [options.treeProps] options.treeProps
12864   - * @param {HTMLDivElement} [options.elem] options.elem
12865   - * @param {boolean} [options.singleUsage = true] options.singleUsage
12866   - * @param {Function} [options.customSetTree = ((record) => ({ id: record.id, title: record.name }))] options.customSetTree
12867   - * @param {boolean} [options.autoFormatDataSource = true] options.autoFormatDataSource
12868   - * @param {string} [options.layVerify] options.layVerify
12869   - * @param {string} [options.layVerType] options.layVerType
12870   - * @param {boolean} [options.addPlaceholderOption] options.addPlaceholderOption
12871   - * @param {Function} [options.treeProps.onReady] options.treeProps.onReady
  13489 + * @typedef CreateTreeSelectParamsType
  13490 + * @property {string} [layFilter] options.layFilter
  13491 + * @property {string} [label] options.label
  13492 + * @property {object} [treeProps] options.treeProps
  13493 + * @property {HTMLDivElement} [elem] options.elem
  13494 + * @property {boolean} [singleUsage = true] options.singleUsage
  13495 + * @property {Function} [customSetTree = ((record) => ({ id: record.id, title: record.name }))] options.customSetTree
  13496 + * @property {boolean} [autoFormatDataSource = true] options.autoFormatDataSource
  13497 + * @property {string} [layVerify] options.layVerify
  13498 + * @property {string} [layVerType] options.layVerType
  13499 + * @property {boolean} [addPlaceholderOption] options.addPlaceholderOption
  13500 + * @property {boolean} [hiddenLabel] - hiddenLabel
  13501 + * @property {Function} [treeProps.onReady] options.treeProps.onReady
  13502 + * @property {boolean} [popupMountToBody] popupMountToBody
  13503 + * @param {CreateTreeSelectParamsType} options
12872 13504 */
12873 13505 static createTreeSelect(options) {
12874 13506 const CLASS_NAME = 'things-kit-tree-select'
... ... @@ -12889,27 +13521,29 @@ class UseLayUi {
12889 13521 childrenField = 'children',
12890 13522 layVerify,
12891 13523 layVerType,
12892   - addPlaceholderOption
  13524 + addPlaceholderOption,
  13525 + hiddenLabel,
  13526 + popupMountToBody
12893 13527 } = options
12894 13528
12895 13529 let { data = [], click, onReady } = treeProps
12896 13530
12897 13531 let template = `
12898   - <div class="layui-form-item ${CLASS_NAME} ${className}">
12899   - <label class="layui-form-label">${label}</label>
  13532 + <div class="layui-form-item ${CLASS_NAME} ${className}" style="margin-bottom: ${hiddenLabel ? '0' : '15px'};">
  13533 + <label class="layui-form-label" style="display: ${hiddenLabel ? 'none' : 'block'};">${label}</label>
12900 13534 <div class="layui-input-block">
12901   - <div class="layui-unselect layui-form-select ${SELECT_CLS}">
12902   - <div class="layui-select-title">
12903   - <span class="layui-input layui-unselect tree-select__label">请选择</span>
12904   - <input ${this.dynamicAttr('lay-verify', layVerify)} ${this.dynamicAttr('lay-verType', layVerType)} type="text" style="visibility: hidden; position: absolute; top: 0" name="${layFilter}">
12905   - <i class="layui-edge"></i>
  13535 + <div class="layui-unselect layui-form-select ${SELECT_CLS}">
  13536 + <div class="layui-select-title">
  13537 + <span class="layui-input layui-unselect tree-select__label">请选择</span>
  13538 + <input ${this.dynamicAttr('lay-verify', layVerify)} ${this.dynamicAttr('lay-verType', layVerType)} type="text" style="visibility: hidden; position: absolute; top: 0" name="${layFilter}">
  13539 + <i class="layui-edge"></i>
  13540 + </div>
  13541 + <dl class="layui-anim layui-anim-upbit">
  13542 + <dd>
  13543 + <ul class="tree-select__tree-mount"></ul>
  13544 + </dd>
  13545 + </dl>
12906 13546 </div>
12907   - <dl class="layui-anim layui-anim-upbit">
12908   - <dd>
12909   - <ul class="tree-select__tree-mount"></ul>
12910   - </dd>
12911   - </dl>
12912   - </div>
12913 13547 </div>
12914 13548 </div>`
12915 13549
... ... @@ -12933,7 +13567,7 @@ class UseLayUi {
12933 13567 $(elem).html(template)
12934 13568 const treeData = UseLayUi.formatTreeDataSource(data, customSetTree, valueField, labelField, childrenField)
12935 13569 if (addPlaceholderOption) treeData.unshift({ title: '请选择', id: undefined })
12936   - // mount tree
  13570 + // mount tree
12937 13571 tree.render({
12938 13572 ...treeProps,
12939 13573 ...(autoFormatDataSource ? { data: treeData } : {}),
... ... @@ -12950,6 +13584,21 @@ class UseLayUi {
12950 13584 $(document).find('.layui-form-select').removeClass('layui-form-selected')
12951 13585 $(this).parents(`.${SELECT_CLS}`).toggleClass("layui-form-selected");
12952 13586 layui.stope(e);
  13587 + if (popupMountToBody) {
  13588 + const popup = $(this).parents(`.${SELECT_CLS}`).find('.layui-anim')
  13589 + const titleEl = $(this).parents(`.${SELECT_CLS}`).find('.layui-select-title')
  13590 + const offset = $(titleEl).offset()
  13591 + const height = $(titleEl).height()
  13592 + const width = $(titleEl).width()
  13593 +
  13594 + $(popup).css({
  13595 + position: 'fixed',
  13596 + top: `${offset.top + height}px`,
  13597 + 'min-width': width,
  13598 + left: `${offset.left}px`,
  13599 + width
  13600 + })
  13601 + }
12953 13602 })
12954 13603 .on('click', '.layui-anim', (e) => {
12955 13604 layui.stope(e)
... ... @@ -13104,7 +13753,7 @@ class UseLayUi {
13104 13753 class Utils {
13105 13754 /**
13106 13755 * @description 字符串是否能转换为对象
13107   - * @param {string} value
  13756 + * @param {string} value
13108 13757 */
13109 13758 static stringIsJSON(value) {
13110 13759 try {
... ... @@ -13118,8 +13767,8 @@ class Utils {
13118 13767
13119 13768 /**
13120 13769 * @description 字符串转对象
13121   - * @param {string} value
13122   - * @returns
  13770 + * @param {string} value
  13771 + * @returns
13123 13772 */
13124 13773 static stringToJSON(value, defaultValue = {}) {
13125 13774 try {
... ... @@ -13660,7 +14309,7 @@ class DispatchCenter {
13660 14309 subList.forEach(item => {
13661 14310 const { dataOrigin, additional } = item
13662 14311 if (dataOrigin === 'dataSources') {
13663   - if (additional) {
  14312 + if (additional && (additional || {})?.dataType) {
13664 14313 const { dataType } = additional || {}
13665 14314 if (dataType === HandleDataSource.enumDataBindType.REAL) {
13666 14315 this.dataSourceHandlerInstance.updateRealTimeDataSource(message, item)
... ... @@ -13678,10 +14327,6 @@ class DispatchCenter {
13678 14327 handleFunction(message, item)
13679 14328 }
13680 14329 })
13681   - // this.subscribeEvent(cmdId, this.updateCommonDataSource.bind(this))
13682   - return
13683   - // const { subscriptionId, data } = message
13684   - // DispatchCenter.instance.publishEvent(subscriptionId, data, message, event, ws)
13685 14330 }
13686 14331
13687 14332 /**
... ... @@ -13712,6 +14357,92 @@ class DispatchCenter {
13712 14357 if (!id) return
13713 14358 const [err, res] = await to(ConfigurationNodeApi.getConfigurationInfo('CONTENT', id))
13714 14359 this.contentData = res
  14360 + this.afterGetContentDataNode()
  14361 + }
  14362 +
  14363 + afterGetContentDataNode() {
  14364 + this.handleFlowmeterComponent()
  14365 + this.handleAlarmList()
  14366 + }
  14367 +
  14368 + handleFlowmeterComponent() {
  14369 + setTimeout(() => {
  14370 + const componentType = Sidebar.prototype.enumComponentType
  14371 + if (!this.contentData?.dataSources) return
  14372 + const flowmeterComponent = this.contentData?.dataSources?.filter(item => item.additional && (item.additional || {}).componentType === componentType.FLOWMETER)
  14373 + const { MIN_VALUE, MAX_VALUE, BG_COLOR, WAVE_FIRST_COLOR, WAVE_SECOND_COLOR, WAVE_THIRD_COLOR } = HandleDataSource.enumConst
  14374 + flowmeterComponent.forEach(item => {
  14375 + const { nodeId, additional } = item
  14376 + const { bgColor, maxValue, minValue, waveFirst, waveSecond, waveThird } = additional || {}
  14377 + const cell = this.graph?.getCellsById([nodeId])?.[0]
  14378 + if (cell) {
  14379 + cell.setAttribute(BG_COLOR, bgColor)
  14380 + cell.setAttribute(MIN_VALUE, minValue)
  14381 + cell.setAttribute(MAX_VALUE, maxValue)
  14382 + cell.setAttribute(WAVE_FIRST_COLOR, waveFirst)
  14383 + cell.setAttribute(WAVE_SECOND_COLOR, waveSecond)
  14384 + cell.setAttribute(WAVE_THIRD_COLOR, waveThird)
  14385 +
  14386 + const { UUID } = getFlowmeterAttrKey()
  14387 + const id = cell.getAttribute(UUID)
  14388 +
  14389 + const element = document.getElementById(id)
  14390 +
  14391 + if (element) {
  14392 + element.querySelectorAll(`.${BG_COLOR}`).forEach(item => item.style.fill = bgColor)
  14393 + element.querySelectorAll(`.${WAVE_FIRST_COLOR}`).forEach(item => item.style.fill = waveFirst)
  14394 + element.querySelectorAll(`.${WAVE_SECOND_COLOR}`).forEach(item => item.style.fill = waveSecond)
  14395 + element.querySelectorAll(`.${WAVE_THIRD_COLOR}`).forEach(item => item.style.fill = waveThird)
  14396 + }
  14397 + }
  14398 + })
  14399 + }, 10);
  14400 + }
  14401 +
  14402 +
  14403 + // 处理告警列表
  14404 + async handleAlarmList() {
  14405 + const componentType = Sidebar.prototype.enumComponentType
  14406 + if (!this.contentData?.dataSources) return
  14407 + const alarmList = this.contentData?.dataSources?.filter(item => item.additional && (item.additional || {}).componentType === componentType.ALARM_LIST)
  14408 + const allCell = Object.entries(this.graph?.getModel()?.cells || {}).map(([_, item]) => item) || []
  14409 + const { UUID } = AlarmListComponent.getAttributeKeys()
  14410 + const { jquery: $ } = layui
  14411 + for (const item of alarmList || []) {
  14412 + const { nodeId, sourceOption } = item || {}
  14413 + const { devicesInfo, autoPlay, interval, startTime, endTime } = sourceOption || {}
  14414 + const node = allCell.find(item => item.id === nodeId)
  14415 + const deviceIds = UseLayUi.parseStringToJSON(devicesInfo, []).map(item => item.deviceId)
  14416 + const id = node.getAttribute(UUID)
  14417 + const element = document.getElementById(id)
  14418 + if (element) {
  14419 + const [err, data] = await to(ConfigurationNodeApi.getAlarmList({ page: 1, pageSize: 30, startTime: Number(startTime), endTime: Number(endTime), deviceIds }))
  14420 + if (err) return
  14421 + if (data?.items && data?.items?.length) {
  14422 + const template = AlarmListComponent.createAlarmItem(data.items)
  14423 + $(`#${id}`).find('.list-wrapper').html(template)
  14424 + } else {
  14425 + $(`#${id}`).find('.list-wrapper').html(`<div>暂无数据</div>`)
  14426 + }
  14427 + if (autoPlay) {
  14428 + const wrapperHeight = $(`#${id}`).height()
  14429 + const listWrapper = $(`#${id}`).find('.list-wrapper')
  14430 + const allHeight = listWrapper.height()
  14431 + const itemHeight = allHeight / listWrapper.children().length
  14432 + let scrollDistance = 0
  14433 + const cancel = RAFSetInterval(() => {
  14434 + try {
  14435 + scrollDistance = itemHeight + scrollDistance
  14436 + if (scrollDistance + wrapperHeight > allHeight) scrollDistance = 0
  14437 + document.getElementById(id).scrollTo({ top: scrollDistance, behavior: 'smooth' })
  14438 + } catch (error) {
  14439 + cancel?.()
  14440 + }
  14441 +
  14442 + }, Number(interval) * 1000)
  14443 + }
  14444 + }
  14445 + }
13715 14446 }
13716 14447
13717 14448 sendSubscribeMessage() {
... ... @@ -13751,7 +14482,7 @@ class DispatchCenter {
13751 14482 * orgName?: string,
13752 14483 * slaveDeviceName?: string
13753 14484 * }
13754   - * }} record
  14485 + * }} record
13755 14486 */
13756 14487 const setDeviceMapping = (record) => {
13757 14488 const { deviceId, slaveDeviceId } = record
... ... @@ -13767,12 +14498,12 @@ class DispatchCenter {
13767 14498 /**
13768 14499 * @type {{
13769 14500 * id: string,
13770   - * nodeId: string,
  14501 + * nodeId: string,
13771 14502 * deviceId: string,
13772   - * slaveDeviceId: string,
13773   - * attr: string,
13774   - * enabled: boolean,
13775   - * additional: object,
  14503 + * slaveDeviceId: string,
  14504 + * attr: string,
  14505 + * enabled: boolean,
  14506 + * additional: object,
13776 14507 * condition: object
13777 14508 * }[]}
13778 14509 */
... ... @@ -13792,10 +14523,10 @@ class DispatchCenter {
13792 14523 })
13793 14524
13794 14525 /**
13795   - * @param {{
13796   - * entityId: string,
13797   - * cmdId: number,
13798   - * keys: string,
  14526 + * @param {{
  14527 + * entityId: string,
  14528 + * cmdId: number,
  14529 + * keys: string,
13799 14530 * agg?: string,
13800 14531 * interval?: number,
13801 14532 * startTs?: number,
... ... @@ -13866,7 +14597,7 @@ class DispatchCenter {
13866 14597 entityId: key,
13867 14598 cmdId,
13868 14599 agg,
13869   - keys: getKeys(moreFilterRecord.map(item => item.attr)),
  14600 + keys: item.attr,
13870 14601 interval: toNumber(interval, 1000),
13871 14602 startTs: Date.now() - toNumber(effectScope),
13872 14603 ...(dataType === HandleDataSource.enumDataBindType.REAL ? {} : { endTs: Date.now() })
... ... @@ -14043,7 +14774,42 @@ class HandleDataSource {
14043 14774 /**
14044 14775 * @description 属性名称
14045 14776 */
14046   - ATTR_NAME: 'attrName'
  14777 + ATTR_NAME: 'attrName',
  14778 +
  14779 + /**
  14780 + * @description 组件类型
  14781 + */
  14782 + COMPONENT_TYPE: 'componentType',
  14783 +
  14784 + /**
  14785 + * @description 流量计最大值
  14786 + */
  14787 + MAX_VALUE: 'maxValue',
  14788 +
  14789 + /**
  14790 + * @description 流量计最小值
  14791 + */
  14792 + MIN_VALUE: 'minValue',
  14793 +
  14794 + /**
  14795 + * @description 流量计背景颜色
  14796 + */
  14797 + BG_COLOR: 'bgColor',
  14798 +
  14799 + /**
  14800 + * @description 流量计颜色一
  14801 + */
  14802 + WAVE_FIRST_COLOR: 'waveFirst',
  14803 +
  14804 + /**
  14805 + * @description 流量计颜色二
  14806 + */
  14807 + WAVE_SECOND_COLOR: 'waveSecond',
  14808 +
  14809 + /**
  14810 + * @description 流量计颜色三
  14811 + */
  14812 + WAVE_THIRD_COLOR: 'waveThird',
14047 14813 }
14048 14814
14049 14815
... ... @@ -14074,8 +14840,6 @@ class HandleDataSource {
14074 14840
14075 14841 constructor(DispatchInstance) {
14076 14842 this.DispatchInstance = DispatchInstance
14077   - // this.generatorCommonDataSourceMapping()
14078   - // this.generatorChartDataSourceMapping()
14079 14843 }
14080 14844
14081 14845 get graph() {
... ... @@ -14114,76 +14878,6 @@ class HandleDataSource {
14114 14878 }
14115 14879
14116 14880 /**
14117   - * @description 生成普通数据源绑定映射关系
14118   - * @param dataSources
14119   - * @return {{cmdId: number, entityType: string, keys: *, scope: string, entityId: *}[]}
14120   - */
14121   - generatorCommonDataSourceMapping() {
14122   - const msg = this.commonDataSourceBindList.map((datum) => {
14123   - const { deviceId, attr, nodeId, slaveDeviceId } = datum
14124   - const cmdId = this.getCmdId(nodeId)
14125   - const sendMsgTemplate = {
14126   - entityType: "DEVICE",
14127   - entityId: slaveDeviceId ? slaveDeviceId : deviceId,
14128   - scope: "LATEST_TELEMETRY",
14129   - cmdId,
14130   - keys: attr,
14131   - }
14132   - this.dataSourceNodeMapping.set(nodeId, datum)
14133   - this.subscribeEvent(cmdId, this.updateCommonDataSource.bind(this))
14134   - return sendMsgTemplate
14135   - })
14136   - const { REAL } = HandleDataSource.enumDataBindType
14137   - if (msg.length) this.sendMsg({ [REAL]: msg })
14138   - }
14139   -
14140   -
14141   - /**
14142   - * @description 图表数据源绑定关系
14143   - * @param {any[]} dataSource
14144   - */
14145   - generatorChartDataSourceMapping() {
14146   - const realList = []
14147   - const historyList = []
14148   - const { HISTORY, REAL } = HandleDataSource.enumDataBindType
14149   - const { STARTTs, ENDTs } = HandleDataSource.enumConst
14150   - for (const item of this.chartDataSourceBindList) {
14151   - const { additional = {}, deviceId, attr, nodeId, slaveDeviceId } = item
14152   - if (!attr) continue
14153   - const { agg, interval = 1000, dataType, effectScope = 0 } = additional
14154   - const cmdId = this.getCmdId(nodeId)
14155   - const template = {
14156   - entityType: "DEVICE",
14157   - entityId: slaveDeviceId ? slaveDeviceId : deviceId,
14158   - cmdId,
14159   - interval: Number(interval),
14160   - agg,
14161   - keys: attr,
14162   - }
14163   - let scope = isNaN(effectScope) ? 0 : Number(effectScope)
14164   - if (dataType === HISTORY) {
14165   - template[STARTTs] = Date.now() - scope
14166   - template[ENDTs] = Date.now()
14167   - historyList.push(template)
14168   - this.subscribeEvent(cmdId, (message) => {
14169   - this.updateHistoryDataSource(message, agg)
14170   - })
14171   - }
14172   - else if (dataType === REAL) {
14173   - template[STARTTs] = Date.now() - scope
14174   - // template['timeWindow'] = interval
14175   - realList.push(template)
14176   - this.subscribeEvent(cmdId, (message) => {
14177   - this.updateRealTimeDataSource(message, agg)
14178   - })
14179   - }
14180   - this.dataSourceNodeMapping.set(nodeId, item)
14181   - }
14182   -
14183   - if (historyList.length || realList.length) this.sendMsg({ [HISTORY]: historyList, [REAL]: realList })
14184   - }
14185   -
14186   - /**
14187 14881 * @description 订阅事件 绑定回调
14188 14882 * @param eventName
14189 14883 * @param callback
... ... @@ -14194,15 +14888,22 @@ class HandleDataSource {
14194 14888
14195 14889 /**
14196 14890 * @description 更新变量值
14197   - * @param {} message
  14891 + * @param {} message
14198 14892 */
14199 14893 updateCommonDataSource(message, record) {
14200 14894 const { nodeId, attr } = record
14201   - const node = this.getNodeByCmdId(nodeId)
  14895 + const node = this.getNodeByNodeId(nodeId)
  14896 + const { data } = message
  14897 + const type = this.getComponentType(node)
14202 14898
  14899 + if (node && type === this.componentType.FLOWMETER) {
  14900 + this.handleFlowmeterComponent(message, record)
  14901 + return
  14902 + }
  14903 +
  14904 + // 需要刷新页面
14203 14905 node && this.updatePage(() => {
14204   - const { data } = message
14205   - const type = this.getComponentType(node)
  14906 +
14206 14907 if (type === this.componentType.SWITCH) {
14207 14908 this.handleSwitchComponent(message, record)
14208 14909 return
... ... @@ -14227,13 +14928,14 @@ class HandleDataSource {
14227 14928
14228 14929 /**
14229 14930 * @description 处理switch 组件
14230   - * @param {} message
  14931 + * @param {} message
14231 14932 */
14232 14933 handleSwitchComponent(message, record) {
14233 14934 const { data = {} } = message
14234 14935 const { nodeId, attr } = record
14235   - const node = this.getNodeByCmdId(nodeId)
  14936 + const node = this.getNodeByNodeId(nodeId)
14236 14937 const [[_timespan, receiveValue] = []] = data[attr] || []
  14938 + if (receiveValue === null || receiveValue === undefined) return
14237 14939 const switchConfig = this.DispatchInstance.contentData.act.find(item => item.id === nodeId && item.type === 'SWITCH')
14238 14940 const { condition = [] } = switchConfig || {}
14239 14941 let reg = /image=[^;]+/g
... ... @@ -14253,7 +14955,7 @@ class HandleDataSource {
14253 14955 if ((style || '').includes(imagePath)) return
14254 14956 const sendValue = getSendValue(type)
14255 14957 node.setStyle(style.replace(reg, `image=${imagePath}`))
14256   - node.setAttribute('label', '')
  14958 + // node.setAttribute('label', '')
14257 14959 node.setAttribute(SWITCH_VALUE, receiveValue)
14258 14960 node.setAttribute(SWITCH_SEND_VALUE, sendValue)
14259 14961 node.setAttribute(SWITCH_STATE, type)
... ... @@ -14273,21 +14975,56 @@ class HandleDataSource {
14273 14975
14274 14976 }
14275 14977
  14978 + handleFlowmeterComponent(message, record) {
  14979 + const { data = {} } = message
  14980 + if (!data) return
  14981 + const { nodeId, attr } = record
  14982 + const node = this.getNodeByNodeId(nodeId)
  14983 +
  14984 + let [[_timespan, receiveValue] = []] = data[attr] || []
  14985 + const { UUID, TYPE } = getFlowmeterAttrKey()
  14986 + const type = node.getAttribute(TYPE)
  14987 + const flowmeterType = Sidebar.prototype.enumFlowmeterType
  14988 + const id = node.getAttribute(UUID)
  14989 + const element = document.getElementById(id)
  14990 + const bindData = this.getBindData(nodeId)
  14991 + const { additional } = bindData || {}
  14992 + const { maxValue, minValue } = additional || {}
  14993 +
  14994 + receiveValue = isNaN(receiveValue) ? 0 : Number(receiveValue)
  14995 + if (element) {
  14996 + if (type === flowmeterType.CIRCLE || type === flowmeterType.RECT) {
  14997 + const element = document.getElementById(id).querySelector('svg')
  14998 + element.style.setProperty('--value', receiveValue)
  14999 + }
  15000 + if (type === flowmeterType.THERMOMETER) {
  15001 + const element = document.getElementById(id).querySelector('svg')
  15002 + const range = 140
  15003 + const ratio = (190 - 15) / range
  15004 + receiveValue = receiveValue >= 0 ? receiveValue + 20 : 20 - Math.abs(receiveValue)
  15005 + receiveValue = 190 - receiveValue * ratio
  15006 + receiveValue = receiveValue < 15 ? 15 : receiveValue
  15007 + element.style.setProperty('--value', receiveValue)
  15008 + }
  15009 + }
  15010 + }
  15011 +
14276 15012 handleParamSettingButton(message, record) {
14277 15013 const { data = {} } = message
14278 15014 if (!data) return
14279 15015 const { nodeId, attr } = record
14280   - const node = this.getNodeByCmdId(nodeId)
  15016 + const node = this.getNodeByNodeId(nodeId)
14281 15017 const [[_timespan, receiveValue] = []] = data[attr] || []
  15018 + if (receiveValue === null || receiveValue === undefined) return
14282 15019 this.updatePage(() => {
14283   - node.setAttribute('label', `<button class="param-setting-button">${receiveValue}</button>`)
  15020 + node.setAttribute('label', receiveValue)
14284 15021 }, node)
14285 15022 }
14286 15023
14287 15024 handleImageComponent(message, record) {
14288 15025 const { data = {} } = message
14289 15026 const { nodeId, attr } = record
14290   - const node = this.getNodeByCmdId(nodeId)
  15027 + const node = this.getNodeByNodeId(nodeId)
14291 15028 const [[_timespan, receiveValue] = []] = data[attr] || []
14292 15029 this.updatePage(() => {
14293 15030 node.setAttribute('label', `<img class="basic-component__image" alt="图片" src="${receiveValue}" />`)
... ... @@ -14296,14 +15033,14 @@ class HandleDataSource {
14296 15033
14297 15034 /**
14298 15035 * @description 更新实时数据
14299   - * @param {} message
  15036 + * @param {} message
14300 15037 * @param {} record 聚合方式
14301 15038 */
14302 15039 updateRealTimeDataSource(message, record) {
14303 15040 const { data = {} } = message
14304 15041 const { nodeId, attr, additional = {} } = record
14305 15042 const { agg } = additional
14306   - const node = this.getNodeByCmdId(nodeId)
  15043 + const node = this.getNodeByNodeId(nodeId)
14307 15044 if (!node) return
14308 15045 const enumConst = Sidebar.prototype.enumCellBasicAttribute
14309 15046 const chartInstanceMap = Sidebar.prototype.chartsInstanceMapping
... ... @@ -14334,7 +15071,7 @@ class HandleDataSource {
14334 15071 const { data = {} } = message
14335 15072 const { nodeId, attr, additional = {} } = record
14336 15073 const { agg } = additional
14337   - const node = this.getNodeByCmdId(nodeId)
  15074 + const node = this.getNodeByNodeId(nodeId)
14338 15075 if (!node) return
14339 15076 const enumConst = Sidebar.prototype.enumCellBasicAttribute
14340 15077 const chartInstanceMap = Sidebar.prototype.chartsInstanceMapping
... ... @@ -14411,9 +15148,9 @@ class HandleDataSource {
14411 15148 }
14412 15149
14413 15150 /**
14414   - *
14415   - * @param {@} params
14416   - * @returns
  15151 + *
  15152 + * @param {@} params
  15153 + * @returns
14417 15154 */
14418 15155 getBasicChartOption(params = { dataList: [], attr: '', chartType: 'bar', action, additional }) {
14419 15156 const { dataList = [], attr = '', chartType = 'bar', action, additional = {} } = params
... ... @@ -14500,6 +15237,8 @@ class HandleDataSource {
14500 15237 const [timespan, value] = dataList[i]
14501 15238 xAxisData.push(new Date(Number(timespan)).toLocaleTimeString())
14502 15239 seriesValue.push(Number(value))
  15240 + xAxisData = xAxisData.slice(0, 30)
  15241 + seriesValue = seriesValue.slice(0, 30)
14503 15242 }
14504 15243
14505 15244 if (Number(oldOptions.dataZoom[0].endValue) === seriesValue.length - 1) {
... ... @@ -14681,8 +15420,8 @@ class HandleDataSource {
14681 15420
14682 15421 /**
14683 15422 * @description 获取cmdId
14684   - * @param {string} nodeId
14685   - * @returns
  15423 + * @param {string} nodeId
  15424 + * @returns
14686 15425 */
14687 15426 getCmdId(nodeId) {
14688 15427 return this.DispatchInstance.getCmdId(nodeId)
... ... @@ -14703,7 +15442,7 @@ class HandleDataSource {
14703 15442 * @param subscriptionId
14704 15443 * @return {*}
14705 15444 */
14706   - getNodeByCmdId(nodeId) {
  15445 + getNodeByNodeId(nodeId) {
14707 15446 // const nodeId = this.getNodeIdByCmdId(subscriptionId)
14708 15447 return this.contentAllCell.find(item => item.id === nodeId)
14709 15448 }
... ... @@ -14714,8 +15453,8 @@ class HandleDataSource {
14714 15453
14715 15454 /**
14716 15455 * @description 发送socket 消息
14717   - * @param {any} msg
14718   - * @returns
  15456 + * @param {any} msg
  15457 + * @returns
14719 15458 */
14720 15459 sendMsg(msg) {
14721 15460 return this.DispatchInstance.sendMessageToGetRealTimeData(msg)
... ... @@ -14985,7 +15724,6 @@ class HandleDataInteraction {
14985 15724 const { COMPONENT_TYPE } = Sidebar.prototype.enumCellBasicAttribute
14986 15725 const contentData = this.contentData
14987 15726 const currentNode = this.contentAllCell.find(item => item.id === nodeId)
14988   -
14989 15727 const enumConst = {
14990 15728 VALUE: 'value',
14991 15729 ISSUED_WAY: 'way',
... ... @@ -15166,7 +15904,7 @@ class HandleDataInteraction {
15166 15904 } else {
15167 15905 const replaceValue = currentNode.getAttribute(SWITCH_SEND_VALUE)
15168 15906 value = jsonParse(content.jsonCommand)
15169   - value = replaceAttrPlaceholder(value, attr, replaceValue)
  15907 + value = replaceAttrPlaceholder(value, attr, isNaN(replaceValue) ? 0 : Number(replaceValue))
15170 15908 if (value) flag = true
15171 15909 }
15172 15910 } else {
... ... @@ -15340,7 +16078,7 @@ class HandleDataInteraction {
15340 16078 }
15341 16079
15342 16080 try {
15343   - handle[componentType]()
  16081 + handle[componentType]?.()
15344 16082 } catch (error) {
15345 16083 throw error
15346 16084 }
... ... @@ -15501,7 +16239,7 @@ class HandleDynamicEffect {
15501 16239 }
15502 16240
15503 16241 get contentAllCell() {
15504   - // return this.graph.getDefaultParent().children || []
  16242 + // return this.graph.getDefaultParent().children || []
15505 16243 return Object.entries(this.graph?.getModel()?.cells || {}).map(([_, item]) => item) || []
15506 16244 }
15507 16245
... ... @@ -15658,6 +16396,7 @@ class HandleDynamicEffect {
15658 16396 } else if (condition.type === HandleDynamicEffect.enumDisplayType.HIDDEN) {
15659 16397 isShow = false
15660 16398 }
  16399 + console.log(condition)
15661 16400 const updateFn = () => {
15662 16401 if (!isShow) {
15663 16402 Object.keys(HandleDynamicEffect.enumActType).forEach(key => {
... ... @@ -15670,7 +16409,10 @@ class HandleDynamicEffect {
15670 16409 const temp = this.actNodeMapping.get(node.id)
15671 16410 temp.display = true
15672 16411 }
15673   -
  16412 + if (condition.title) {
  16413 + node.setAttribute('label', condition.title)
  16414 + console.log(node)
  16415 + }
15674 16416 node.setVisible(isShow)
15675 16417 }
15676 16418 this.insertOnceUpdateFn(node, updateFn)
... ... @@ -15976,8 +16718,8 @@ class HandleDynamicEffect {
15976 16718
15977 16719 /**
15978 16720 * @description 验证数据动效优先级 显示隐藏优先级最高
15979   - * @param {string} nodeId
15980   - * @returns
  16721 + * @param {string} nodeId
  16722 + * @returns
15981 16723 */
15982 16724 validatePriority(nodeId) {
15983 16725 return this.actNodeMapping.get(nodeId).display
... ... @@ -16011,7 +16753,7 @@ class UpdateQueue {
16011 16753
16012 16754 /**
16013 16755 * @description 创建更新队列
16014   - * @param {number} time
  16756 + * @param {number} time
16015 16757 */
16016 16758 createUpdateQueue(time) {
16017 16759 const callback = () => {
... ... @@ -16165,13 +16907,13 @@ function RAFSetInterval(callback, time) {
16165 16907 class Validate {
16166 16908 /**
16167 16909 * @description
16168   - * @type {{value: any, message: string, required?: boolean, validator?: any}[]} list
  16910 + * @type {{value: any, message: string, required?: boolean, validator?: any}[]} list
16169 16911 */
16170 16912 list = []
16171 16913
16172 16914 /**
16173 16915 * @description
16174   - * @param {{value: any, message: string, required?: boolean, validator?: any}[]} ruleList
  16916 + * @param {{value: any, message: string, required?: boolean, validator?: any}[]} ruleList
16175 16917 */
16176 16918 constructor(ruleList = []) {
16177 16919 this.list = ruleList
... ... @@ -16183,7 +16925,7 @@ class Validate {
16183 16925
16184 16926 /**
16185 16927 * @description 设置规则
16186   - * @param {{value: any, message: string, required?: boolean, validator?: any}} rule
  16928 + * @param {{value: any, message: string, required?: boolean, validator?: any}} rule
16187 16929 */
16188 16930 set(rule) {
16189 16931 this.list.push(rule)
... ...
... ... @@ -1682,8 +1682,10 @@ Graph.sanitizeHtml = function(value, editing)
1682 1682 return null;
1683 1683 };
1684 1684 function idX(id) { return id };
1685   -
1686   - return html_sanitize(value, urlX, idX);
  1685 + // console.log(html_sanitize(value, urlX, idX))
  1686 + // TODO THINGS_KIT 取消html标签限制
  1687 + return value
  1688 + // return html_sanitize(value, urlX, idX);
1687 1689 };
1688 1690
1689 1691 /**
... ...
... ... @@ -167,6 +167,10 @@
167 167 table-layout: fixed;
168 168 }
169 169
  170 +.override__table .org-tree .layui-input-block {
  171 + margin-left: 0;
  172 +}
  173 +
170 174 /**/
171 175 .override__radio-default {
172 176 margin-right: 16px;
... ... @@ -503,36 +507,6 @@
503 507 margin-left: 0;
504 508 }
505 509
506   -/* 参数设置按钮 */
507   -.param-setting-button {
508   - color: #fff;
509   - border-color: #1890ff;
510   - background: #1890ff;
511   - text-shadow: 0 -1px 0 rgb(0 0 0 / 12%);
512   - box-shadow: 0 2px #0000000b;
513   -
514   - line-height: 1.5715;
515   - position: relative;
516   - display: inline-block;
517   - font-weight: 400;
518   - white-space: nowrap;
519   - text-align: center;
520   - background-image: none;
521   - border: 1px solid transparent;
522   - box-shadow: 0 2px #00000004;
523   - cursor: pointer;
524   - transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
525   - -webkit-user-select: none;
526   - -moz-user-select: none;
527   - -ms-user-select: none;
528   - user-select: none;
529   - touch-action: manipulation;
530   - height: 32px;
531   - padding: 4px 15px;
532   - font-size: 14px;
533   - border-radius: 2px;
534   -}
535   -
536 510 /* ========== help message ========== */
537 511 .thingskit-help-message {
538 512 cursor: pointer;
... ...
  1 +<svg class="flowmeter-thermometer" viewBox="0 0 200 250" xmlns="http://www.w3.org/2000/svg"
  2 + style="--range: 4; --min: 50; --max: 70; --width: 500; --height: 500; --value: 50;">
  3 + <style>
  4 + .flowmeter-thermometer {
  5 + width: calc(min(var(--width), var(--height)) * 1px);
  6 + height: calc(min(var(--width), var(--height)) * 1px);
  7 + }
  8 +
  9 + .thermometer-mercury-column {
  10 + y: var(--value);
  11 + }
  12 +
  13 + .tick-label {
  14 + font-size: 12px;
  15 + text-align: right;
  16 + overflow: hidden;
  17 + text-overflow: ellipsis;
  18 + color: #5b6b73;
  19 + }
  20 +
  21 + .thermometer-mercury-column {
  22 + transition: y .5s cubic-bezier(0.19, 1, 0.22, 1);
  23 + }
  24 + </style>
  25 + <defs>
  26 + <radialGradient id="thermometerdiv_meter_2" cx="50%" cy="50%" r="50%" fx="50%" fy="50%">
  27 + <stop offset="0%" style="stop-color: rgb(230, 200, 200);"></stop>
  28 + <stop offset="90%" style="stop-color: rgb(230, 0, 0);"></stop>
  29 + </radialGradient>
  30 + <clipPath id="over">
  31 + <rect width="100" height="190" x="100" y="10" />
  32 + </clipPath>
  33 + </defs>
  34 + <circle r="9.25" cx="109" cy="14.25" style="fill: rgb(255, 255, 255); stroke: rgb(136, 136, 136); stroke-width: 1px;">
  35 + </circle>
  36 + <rect x="99.75" y="14.25" height="192.75" width="18.5"
  37 + style="shape-rendering: crispedges; fill: rgb(255, 255, 255); stroke: rgb(136, 136, 136); stroke-width: 1px;">
  38 + </rect>
  39 + <circle r="8.75" cx="109" cy="14.25" style="fill: rgb(255, 255, 255); stroke: none;"></circle>
  40 + <circle r="18" cx="109" cy="207" style="fill: rgb(255, 255, 255); stroke: rgb(136, 136, 136);"></circle>
  41 + <rect x="100.25" y="14.25" height="192.75" width="17.5"
  42 + style="shape-rendering: crispedges; fill: rgb(255, 255, 255); stroke: none;"></rect>
  43 + <line class="thermometer-min-line" x1="99.75" x2="140.25" y1="165" y2="165"
  44 + style="stroke: rgb(136, 136, 136); stroke-width: 1px; shape-rendering: crispedges;"></line>
  45 + <text class="thermometer-min-label" x="120.25" y="168.46428571428572" dy="0.72em"
  46 + style="fill: rgb(0, 0, 230); font-size: 10px;">min</text>
  47 + <line class="thermometer-max-line" x1="99.75" x2="140.25" y1="40" y2="40"
  48 + style="stroke: rgb(136, 136, 136); stroke-width: 1px; shape-rendering: crispedges;"></line>
  49 + <text class="thermometer-max-label" x="120.25" y="35.285714285714306"
  50 + style="fill: rgb(230, 0, 0); font-size: 10px;">max</text>
  51 + <rect class="thermometer-mercury-column" x="104" y="15" width="10.5" height="190"
  52 + style="shape-rendering: crispedges; fill: rgb(230, 0, 0);" clip-path="url(#over)"></rect>
  53 + <circle r="13" cx="109" cy="207"
  54 + style="fill: url(&quot;#thermometerdiv_meter_2&quot;); stroke: rgb(230, 0, 0); stroke-width: 2px;"></circle>
  55 + <foreignObject>
  56 + <div></div>
  57 + </foreignObject>
  58 + <g class="thermometer-temperature-axis" transform="translate(99.75,0)" fill="none" font-size="10"
  59 + font-family="sans-serif" text-anchor="end">
  60 + <g class="tick" opacity="1" transform="translate(0,190)">
  61 + <line stroke="currentColor" x2="-7"
  62 + style="stroke: rgb(136, 136, 136); shape-rendering: crispedges; stroke-width: 1px;"></line>
  63 + <foreignObject xmlns="http://www.w3.org/2000/svg" x="-55" y="-10" width="45" height="20">
  64 + <div class="tick-label" xmlns="http://www.w3.org/1999/xhtml">-20</div>
  65 + </foreignObject>
  66 + </g>
  67 + <g class="tick" opacity="1" transform="translate(0,165)">
  68 + <line stroke="currentColor" x2="-7"
  69 + style="stroke: rgb(136, 136, 136); shape-rendering: crispedges; stroke-width: 1px;"></line>
  70 + <foreignObject xmlns="http://www.w3.org/2000/svg" x="-55" y="-10" width="45" height="20">
  71 + <div class="tick-label" xmlns="http://www.w3.org/1999/xhtml">0</div>
  72 + </foreignObject>
  73 + </g>
  74 + <g class="tick" opacity="1" transform="translate(0,140)">
  75 + <line stroke="currentColor" x2="-7"
  76 + style="stroke: rgb(136, 136, 136); shape-rendering: crispedges; stroke-width: 1px;"></line>
  77 + <foreignObject xmlns="http://www.w3.org/2000/svg" x="-55" y="-10" width="45" height="20">
  78 + <div class="tick-label" xmlns="http://www.w3.org/1999/xhtml">20</div>
  79 + </foreignObject>
  80 + </g>
  81 + <g class="tick" opacity="1" transform="translate(0,115)">
  82 + <line stroke="currentColor" x2="-7"
  83 + style="stroke: rgb(136, 136, 136); shape-rendering: crispedges; stroke-width: 1px;"></line>
  84 + <foreignObject xmlns="http://www.w3.org/2000/svg" x="-55" y="-10" width="45" height="20">
  85 + <div class="tick-label" xmlns="http://www.w3.org/1999/xhtml">40</div>
  86 + </foreignObject>
  87 + </g>
  88 + <g class="tick" opacity="1" transform="translate(0,90)">
  89 + <line stroke="currentColor" x2="-7"
  90 + style="stroke: rgb(136, 136, 136); shape-rendering: crispedges; stroke-width: 1px;"></line>
  91 + <foreignObject xmlns="http://www.w3.org/2000/svg" x="-55" y="-10" width="45" height="20">
  92 + <div class="tick-label" xmlns="http://www.w3.org/1999/xhtml">60</div>
  93 + </foreignObject>
  94 + </g>
  95 + <g class="tick" opacity="1" transform="translate(0,65)">
  96 + <line stroke="currentColor" x2="-7"
  97 + style="stroke: rgb(136, 136, 136); shape-rendering: crispedges; stroke-width: 1px;"></line>
  98 + <foreignObject xmlns="http://www.w3.org/2000/svg" x="-55" y="-10" width="45" height="20">
  99 + <div class="tick-label" xmlns="http://www.w3.org/1999/xhtml">80</div>
  100 + </foreignObject>
  101 + </g>
  102 + <g class="tick" opacity="1" transform="translate(0,40)">
  103 + <line stroke="currentColor" x2="-7"
  104 + style="stroke: rgb(136, 136, 136); shape-rendering: crispedges; stroke-width: 1px;"></line>
  105 + <foreignObject xmlns="http://www.w3.org/2000/svg" x="-55" y="-10" width="45" height="20">
  106 + <div class="tick-label" xmlns="http://www.w3.org/1999/xhtml">100</div>
  107 + </foreignObject>
  108 + </g>
  109 + <g class="tick" opacity="1" transform="translate(0,15)">
  110 + <line stroke="currentColor" x2="-7"
  111 + style="stroke: rgb(136, 136, 136); shape-rendering: crispedges; stroke-width: 1px;"></line>
  112 + <foreignObject xmlns="http://www.w3.org/2000/svg" x="-55" y="-10" width="45" height="20">
  113 + <div class="tick-label" xmlns="http://www.w3.org/1999/xhtml">120</div>
  114 + </foreignObject>
  115 + </g>
  116 + </g>
  117 +</svg>
... ...