前言
機櫃 U 位管理是一項突破性創新技術--繼承了 RFID 標簽(電子標簽)的優點的同時,完全解決了 RFID 技術(非接觸式的自動識別技術)在機房 U 位資產監控場應用景中的四大缺陷,采用工業互聯網雲平台監控機房 U 位的方法,具有高可靠性、高准確性、精准定位、免維護的特點,滿足了 U 位級實時監控、智能運維閉環管理的需求。設備上架、下架與遷移,自動變更和實時記錄,(用戶評價):部署工業互聯網雲平台監控機房 U 位后節省了 99% 的登記變更記錄的時間,而且實現了變更后數據 100% 的准確,在這之前是難以想象的,真正實現運維管理最后的工作。
代碼生成
場景搭建
整個 Demo 由最左側的樹,中間部分的列表以及右邊的拓撲圖整體構成,為了讓整個布局干凈一點,這里結合 splitView 和 borderPane 兩種布局方式來進行。首先將場景分為左右兩個部分,左邊為樹,右邊是列表和拓撲圖的組合:
treeView = this.treeView = new ht.widget.TreeView(),// 樹組件 splitView = this.splitView = new ht.widget.SplitView(treeView, null, 'h', 280);// 分割組件,將場景分為左右兩個部分,左邊為樹組件,右邊為空,左邊的寬度為280,右邊的組件先設置為空到時候根據具體情況分配 this.splitView.addToDOM();
布局結束記得將最外層組件的最底層 div 添加到 body 中,HT 的組件一般都會嵌入 BorderPane、SplitView 和 TabView 等容器中使用,而最外層的HT組件則需要用戶手工將 getView() 返回的底層 div 元素添加到頁面的 DOM 元素中,這里需要注意的是,當父容器大小變化時,如果父容器是 BorderPane 和 SplitView 等這些HT預定義的容器組件,則HT的容器會自動遞歸調用孩子組件 invalidate 函數通知更新。但如果父容器是原生的 html 元素, 則 HT 組件無法獲知需要更新,因此最外層的 HT 組件一般需要監聽 window 的窗口大小變化事件,調用最外層組件 invalidate 函數進行更新。
為了最外層組件加載填充滿窗口的方便性,HT 的所有組件都有 addToDOM 函數,其實現邏輯如下,其中 iv 是 invalidate 的簡寫:
addToDOM = function(){ var self = this, view = self.getView(),//獲取組件的底層 div style = view.style; document.body.appendChild(view);//將組件底層div添加進body中 style.left = '0';//ht 默認將所有的組件的position都設置為absolute絕對定位 style.right = '0'; style.top = '0'; style.bottom = '0'; window.addEventListener('resize', function () { self.iv(); }, false);//窗口大小改變事件,調用刷新函數 }
右邊的拓撲圖部分是在監聽選中變化事件的時候更新的,當然,初始化設置的選中樹上的第一個節點就觸發了選中變化事件:
cms.treeView.sm().ss(cms.treeView.dm().getDatas().get(0));// 設置選中樹上的第一個節點 treeView.sm().ms(function(){// 監聽選中變化事件 var ld = treeView.sm().ld();// 獲取最后選中的節點 if (ld) self.updateForm(ld.a('type')); }); CMS.prototype.updateForm = function(type){ var self = this, ld = this.treeView.sm().ld();// 獲取樹上選中的最后一個節點 if (type === self.TYPE_RACK_SPACE) {// 如果是在樹上選中了節點,那么點擊“添加機櫃”就直接在樹上選中的節點下生成 if (!this.rackBuild) { this.rackBuild = new RackBuild(this);// 此類中定義了場景的中間列表部分,右邊拓撲圖部分以及對應的邏輯 } this.rackBuild.setData(ld);// 在樹上添加一個新的節點 this.splitView.setRightView(this.rackBuild.getHTView());// 設置分割組件右邊的內容為整個場景的中間“列表”內容+右邊的拓撲內容 } }
上面代碼中 splitView.setRightView 函數意為設置右側組件,有了這個函數,我就可以動態地改變 spliteView 組件中的右側組件了。
初始化樹
既然布局布好了,就該向具體的位置添加內容了。先來看看如何向樹上添加節點。首先我定義了一個初始化的樹上的值 treeData,通過遍歷這個數組創建樹上的節點以及節點上的父子關系:
var treeData = [{ name: 'Racks', type: 8, children: [ { name: 'rack1', type: 18, usize: 32 }, { name: 'rack2', type: 18 } ] }]; CMS.prototype.loadTreeData = function(){// 加載樹上的節點 var self = this; setTimeout(function(){ var data = treeData; data.forEach(function(d) {// 遍歷 treeData 數組的值 self.createData(d, null);// 第一個節點父親為空 }); self.treeView.expandAll();// 展開樹 }, 10); }
通過 createData 函數創建節點,並給節點設置父子關系:
CMS.prototype.createData = function(data, parent){// 在樹上創建一個節點 var self = this, htData = new ht.Data(),// 新建 Data 類型節點 dm = this.treeView.dm();// 獲取樹的數據容器 htData.a(data);// 設置節點業務屬性 data htData.setName(data.name)// 設置節點的 name 屬性 if (parent) { htData.setParent(parent);// 設置父親節點 } dm.add(htData);// 將節點添加到數據容器中 if (data.children) {// 如果節點中有 children 對象 data.children.forEach(function(d){// 遍歷 children 對象 self.createData(d, htData);// 再創建 children 對象中的節點作為孩子節點 }); } return htData; }
創建場景右邊部分
眼尖的同學在前面的代碼中可能注意到了一個未聲明的 RackBuild 類,在此類的聲明中我們將場景的右半部分主要分為左右兩個部分,左邊又分為上下兩個部分,右邊也分為上下兩個部分。
這里先將整個右邊的部分進行布局,下面代碼中的變量 listBorder 為上圖的左半部分,變量 borderPane 為上圖的右半部分,至於鷹眼組件部分,是添加到在 borderPane 的上層:
listView = this.listView = new ht.widget.ListView(),// 列表組件 listForm = this.listForm = new ht.widget.FormPane(),// 表單組件 listBorder = this.listBorder = new ht.widget.BorderPane(),// 場景中間邊框面板組件 gv = this.gv = new ht.graph.GraphView(),// 拓撲組件 borderPane = this.borderPane = new ht.widget.BorderPane(), toolbar = this.toolbar = new ht.widget.Toolbar(),// 工具條組件 splitView = this.splitView = new ht.widget.SplitView(listBorder, borderPane, 'h', 220),// 分割組件 overview = this.overview = new ht.graph.Overview(gv),// 鷹眼組件 overviewDiv = overview.getView();// 獲取鷹眼組件底層 div overviewDiv.style.height = '120px';// HT 的組件默認都是絕對定位的 overviewDiv.style.width = '120px'; overviewDiv.style.left = '0'; overviewDiv.style.bottom = '0'; overviewDiv.style.zIndex = 10; borderPane.getView().appendChild(overview.getView());// 將鷹眼組件底層 div 添加到面板組件的底層 div 中 listBorder.setTopView(listForm);// 設置頂部組件 listBorder.setCenterView(listView);// 設置中間組件 listBorder.setTopHeight(32);// 設置頂部組件高度 listForm.setVPadding(2);// 設置表單頂部和頂部與組件內容的間距 listForm.setHPadding(4);// 設置表單左邊和右邊與組件內容的間距 listForm.addRow([// 添加一行組件 { comboBox: {// 組合框類 labels: ['All', 'Pathch Panel', 'Switch', 'Server', 'Backbone Switch/Router'],// 設置下拉可選值對應文本 values: [-1, 5, 9, 10, 11],// 設置下拉可選值 value: -1,// 設置當前值,可為任意類型 onValueChanged: function(e) {// 值變化觸發函數 var val = this.getValue();// 獲取當前的值 self.listTypeFilter = val; self.listView.ivm();// 最徹底的刷新方式 } } } ], [0.1], 28);// 參數二為行內元素的寬度,參數三為該行高度 borderPane.setCenterView(gv);// 設置中間組件 borderPane.setTopView(toolbar);// 設置頂部組件 borderPane.setTopHeight(32);// 設置中間組件高度
從上面的代碼可以看出,splitView 為最外層組件,通過 getHTView 函數返回這個組件,在前面動態設置整個場景的右半部分的組件的時候我們就是通過設置 this.splitView.setRightView(this.rackBuild.getHTView()) 設置場景的右半部分為 rackBuild 的底層 div:
getHTView: function(){// 獲取最外層組件 return this.splitView; }
添加工具條內容
toolbar 工具條中總共的元素就三個:添加機櫃,編輯機櫃和刪除機櫃。這三個元素只需要通過 setItems 的方式添加到 toolbar 工具條組件上即可,元素的具體定義如下:
var toolbarItems = [// 工具條上三個的元素 { icon: self.getToolbarIcon('toolbar.add.rack'),// 用的是我們前面聲明過的圖片 toolTip: 'Add a rack',// 文字提示顯示內容 action: function(){// 點擊按鈕后觸發的函數 self._editingRack = null; self.addRackForm.reset(); self.addRackDialog.show();// 彈出對話框,添加一個新的機架,並填寫該機架的信息 } },{ icon: self.getToolbarIcon('toolbar.edit.rack', function(){// 判斷右側拓撲圖上最后選中的節點 來決定這個圖標的顯示顏色(如果沒有選中機櫃,那么此圖標顯示顏色為灰色) return self.gv.sm().ld() instanceof Rack; }), toolTip: 'Edit rack info', action: function(){ var ld = self.gv.sm().ld();// 獲取 gv 中最后選中的節點 if (!ld) return; self._editingRack = ld; self.addRackForm.v('name', ld.a('name'));// 彈出框中的 name 賦值為 ld 的業務屬性 name 的值 self.addRackForm.v('usize', ld.a('usize'));// 彈出框中的 usize 賦值為 ld 的業務屬性 usize 的值 self.addRackDialog.show();// 點擊此按鈕會出現彈出框 } },{ icon: self.getToolbarIcon('toolbar.delete', function(){ return self.gv.sm().ld() instanceof Rack;// 判斷右側拓撲圖上最后選中的節點的類型 }), toolTip: 'Delete a rack', action: function(){ self.handleRemoveRack();// 在拓撲圖上刪除機櫃,並刪除樹上此機櫃對應的節點 } }, ]
接下來只要把這個 item 添加到 toolbar 中並設置一下排布的方式即可:
toolbar.setItems(toolbarItems);// 設置工具條元素數組 toolbar.setStickToRight(true);// 設置工具條是否向右對齊排布 toolbar.enableToolTip(true);// 工具條允許文字提示
上面出現的點擊 toolbar 工具條按鈕觸發的事件中有一個“彈出對話框”的操作,通過 this.addRackDialog.show() 來實現,addRackDialog 對象定義在 initDialog 函數中,作用為創建一個 dialog 對話框,我們設置此對話框中的內容為一個 form 表單進行顯示,同時還設計了兩個按鈕,“OK”按鈕作為執行創建/更改機櫃的屬性,“Cancel”按鈕不執行其他操作,只是將對話框隱藏:
initDialog: function(){// 初始化點擊“增改”出現的對話框 var self = this, addRackDialog = this.addRackDialog = new ht.widget.Dialog(), addRackForm = this.addRackForm = new FormPane(),// 此類繼承於 ht.widget.FormPane labelWidth = 72; addRackForm.addRow([// 添加行 'Name',{ id: 'name', textField: {} } ], [labelWidth, 0.1]); addRackForm.addRow([ 'Height(U)',{ id: 'usize', textField: { type: 'number' } } ], [labelWidth, 0.1]); addRackDialog.setConfig({// 配置對話框的標題,尺寸,內容等 title: "New Rack",// 對話框的標題 content: addRackForm,// 指定對話框的內容 width: 320,// 指定對話框的寬度 height: 220,// 指定對話框的高度 draggable: true,// 指定對話框是否可拖拽調整位置 closable: true,// 可選值為true/false,表示是否顯示關閉按鈕 resizeMode: "none",// 鼠標移動到對話框右下角可改變對話框的大小 none 表示不可調整寬高 buttons: [// 指定對話框按鈕組內容 { label: "Ok",// 按鈕顯示文本 action: function(button, e) {// action為回調函數,當此按鈕被當點擊時,回調函數會執行 var formData = addRackForm.getValueObject(), rack; if (!formData.usize) {// 如果沒有填寫 Height 的值,則默認高度為18 formData.usize = 18; } if (self._editingRack) {// 如果是“編輯rack信息”的彈框 rack = self._editingRack; rack.a(formData); rack.a('treeNode').a(rack.getAttrObject());// } else {// “增加”新的機櫃 rack = self.createRack(formData);// 創建一個新的 rack 模型 self.gv.dm().add(rack);// 在拓撲圖上添加這個rack // update tree formData.type = self.cms.TYPE_RACK; var treeNode = self.cms.createData(formData, cms.treeView.sm().ld()); rack.a('treeNode', treeNode); } self.gv.fitContent(1);// 添加元素之后,讓所有的圖元顯示在界面上 addRackDialog.hide();// 隱藏對話框 } }, { label: 'Cancel', action: function(){ addRackDialog.hide();// 隱藏對話框 } } ], buttonsAlign: "right" }); }
上面代碼出現的 FormPane 類,繼承於 ht.widget.FormPane 類,在 htwidget.FormPane 的基礎上修改也增加了一些函數,主要的內容還是 ht.widget.FormPane 的實現,文章篇幅有限,這里就不貼代碼了,有興趣的可以參考 FormPane.js 文件。
實現了添加和編輯機櫃的兩個功能,刪除機櫃的功能實現上非常容易,只要將節點從拓撲圖和樹上移除即可:
handleRemoveRack: function(){// 在拓撲圖上刪除機櫃,並刪除樹上此機櫃對應的節點 var ld = this.gv.sm().ld();// 獲取 gv 上選中的最后一個節點 if (ld && ld instanceof Rack) {// 機櫃是 Rack 類型 this.cms.treeView.dm().remove(ld.a('treeNode'));// 移出樹上的有 treeNode 屬性的節點 this.gv.dm().remove(ld);// 刪除 gv 中的節點 } }
列表中元素拖拽
所有的內容都創建完畢,接下來要考慮的就是交互的內容了。列表組件中有 handleDragAndDrop 函數實現拖拽的功能:
listView.handleDragAndDrop = this.handleListDND.bind(this);// 列表上拖拽事件監聽 handleListDND: function(e, state){// 拖拽listView列表組件中的事件監聽 var self = this, listView = self.listView, gv = self.gv, dm = gv.dm(), dnd = self.dnd; // handleDragAndDrop 函數有 prepare-begin-between-end 四種狀態 if (state ==='prepare') { var data = listView.getDataAt(e);// 傳入邏輯坐標點或者交互event事件參數,返回當前點下的數據元素 listView.sm().ss(data);// 在拖拽的過程中設置列表組件中的被拖拽的元素被選中 if (dnd && dnd.parentNode) { document.body.removeChild(dnd); } dnd = self.dnd = ht.Default.createDiv();// 創建一個 div dnd.style.zIndex = 10; dnd.innerText = data.getName(); } else if (state === 'begin') { if (dnd) { var pagePoint = ht.Default.getPagePoint(e);// 返回頁面坐標 dnd.style.left = pagePoint.x - dnd.offsetWidth * 0.5 + 'px'; dnd.style.top = pagePoint.y - dnd.offsetHeight * 0.5 + 'px'; document.body.appendChild(dnd) } } else if (state === 'between') { if (dnd) { var pagePoint = ht.Default.getPagePoint(e); dnd.style.left = pagePoint.x - dnd.offsetWidth * 0.5 + 'px'; dnd.style.top = pagePoint.y - dnd.offsetHeight * 0.5 + 'px'; self.showDragHelper(e); } } else {// 拖拽“放開”鼠標后的操作 if (ht.Default.containedInView(e, self.gv)) {// 判斷交互事件所處位置是否在View組件之上 if (dm.contains(self.dragHelper)) {// 判斷容器是否包含該data對象 var rect = self.dragHelper.getRect(),// 獲取圖元的矩形區域(包括旋轉) target = self.showDragHelper(e),// node, ld = self.listView.sm().ld(), uindex = target.getCellIndex(rect.y); node = self.createPane(rect, ld.getAttrObject(), target, uindex);// 創建設備 dm.add(node); // update tree data var treeNode = self.cms.createData(ld.getAttrObject(), target.a('treeNode'));// 在樹上創建節點,並設置父親節點 treeNode.a('uindex', uindex); node.a('treeNode', treeNode); dm.remove(self.dragHelper); } } document.body.removeChild(dnd); self.dnd = null; } }
設備拖動
既然有了從列表組件上拖拽下來的交互動作,接下來應該是做設備在機櫃上的拖拽改變位置的功能了,我們通過監聽拓撲組件 gv 的交互事件來對節點移動進行事件處理:
gv.mi(this.handleInteractor.bind(this));// 監聽交互 handleInteractor: function(e){// 移動機櫃中的設備 的事件監聽 if (e.kind.indexOf('Move') < 0) return;// 如果非move事件則直接返回不做處理 var self = this, listView = self.listView, gv = self.gv, dm = gv.dm(),// 獲取數據容器 target = gv.sm().ld(),// 獲取最后選中的節點 uHeight = target.a('uHeight') || 1;// target.a('uHeight')獲取最后選中的節點的高度 if (e.kind === 'prepareMove') {// 准備移動 self._oldPosition = target.p();// 獲取節點當前的位置 } else if (e.kind === 'betweenMove') {// 正在移動 self.showDragHelper(e.event, uHeight); dm.sendToTop(target);// 將data在拓撲上置頂,顯示在最頂層 不會被別的節點遮蓋 } else if (e.kind === 'endMove') {// 結束移動 var rack = self.showDragHelper(e.event, uHeight); if (dm.contains(self.dragHelper)) {// 判斷容器是否包含該data對象 target.p(self.dragHelper.p());// 設置節點的坐標 target.a('uindex', rack.getCellIndex(target.p().y));// 設置節點的業務屬性 uindex dm.remove(self.dragHelper);// 移除 self._savable = true; self.toolbar.iv(); target.setHost(rack);// 設置宿主節點 target.setParent(rack);// 設置父親節點 // update tree var treeNode = target.a('treeNode');// 獲取拓撲圖上對應的樹上的節點 treeNode.setParent(rack.a('treeNode')); } else { target.p(self._oldPosition); } } }
代碼中的 showDragHelper 就是在設備拖動的過程中,顯示在機櫃上,設備下的作為占位的綠色的矩形,為了方面看到當前移動的位置在機櫃上顯示的位置。有興趣的可以自己了解一下,篇幅有限,這里就不提了。
列表組件過濾
會不會有同學對列表欄頂部的 form 表單做過濾有些好奇?這塊代碼非常簡單,只需要對選中的類型進行過濾即可:
listView.setVisibleFunc(function(data){// 設置可見過濾器 if (!self.listTypeFilter || self.listTypeFilter === -1) return true; return data.a('type') === self.listTypeFilter;// 根據節點的自定義屬性 type 來判斷節點屬於哪個類型 返回與當前 form 表單中選中的名稱相同的所有節點進行顯示 });
主要的代碼就解釋到這里,其他部分的內容有興趣的同學可以自己去摳代碼了解。
工業互聯網可視化監控除了 2D 機房的監控,當然還有 3D 的,涉及到工業生產以及建設的方方面面,目前 3D 的占比比較重,比較直觀,視覺沖擊也相對來說比較強,比如動車的整體構造體現:
利用工業互聯網監控的 3D 工業園:
工業互聯網雲平台可涉及到工業生產的方方面面,應用在工廠以及內部設備工作運行以及所回傳數據的可視化,可以將任一時間段的數據以軌跡圖、儀表盤等各種方式加以展現,工業互聯網雲平台可以廣泛應用於智慧城市的各個領域,除了本例中展示的機房 U 位監控,拓撲圖展示,數據的傳輸意外,其他的工業領域,三維建模的展示以及能源和通訊領域中,甚至軌道交通,醫療領域此工業互聯網雲平台都可應用。