首頁 > html教程 > HTML熱點 閱讀:0更新時間:2020-12-20 09:50:24

基于 HTML5 Canvas 的工控機柜 U 位動態管理

前言

U?是一種表示服務器外部尺寸的單位,是 unit?的縮略語,詳細的尺寸由作為業界團體的美國電子工業協會(EIA)所決定。之所以要規定服務器的尺寸,是為了使服務器保持適當的尺寸以便放在鐵質或鋁質的機架上。機架上有固定服務器的螺孔,以便它能與服務器的螺孔對上號,再用螺絲加以固定好,以方便安裝每一部服務器所需要的空間。規定的尺寸是服務器的寬(48.26cm=19 英寸)與高(4.445cm 的倍數)。由于寬為19英寸,所以有時也將滿足這一規定的機架稱為“19 英寸機架”。厚度以 4.445cm 為基本單位。1U 就是 4.445cm,2U 則是 1U 的 2 倍為 8.89cm。所謂“1U 的 PC 服務器”,就是外形滿足 EIA 規格、厚度為 4.445cm 的產品。設計為能放置到 19 英寸機柜的產品一般被稱為機架服務器。

工控上運用到機柜 U 位的非常普遍,但是經常在創建 2D/3D 模型的時候,我們向內添加設備,每個設備占的 U 位不同,如果只是單純地向機柜內部添加節點,在節點還未添加的時候我們沒法直觀地看到具體的效果,所以我就想能不能在添加的過程中就讓大家直接看到機房設備的 U 位占位以及效果,這個 Demo 因此而生。

?

https://hightopo.com/demo/rack-builder/index.html

代碼生成

場景搭建

整個 Demo 由最左側的樹,中間部分的列表以及右邊的電信機柜拓撲圖整體構成,為了讓整個布局干凈一點,這里結合 splitView 和 borderPane 兩種布局方式來進行。首先將場景分為左右兩個部分,左邊為樹,右邊是列表和電信機柜拓撲圖的組合:

treeView = this.treeView = new ht.widget.TreeView(),// 樹組件 (http://www.hightopo.com/guide/guide/core/treeview/ht-treeview-guide.html)
splitView = this.splitView = new ht.widget.SplitView(treeView, null, 'h', 280);// 分割組件,將場景分為左右兩個部分,左邊為樹組件,右邊為空,左邊的寬度為280,右邊的組件先設置為空到時候根據具體情況分配 (http://www.hightopo.com/guide/guide/core/splitview/ht-splitview-guide.html)
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(),// 列表組件(http://www.hightopo.com/guide/guide/core/listview/ht-listview-guide.html)
listForm = this.listForm = new ht.widget.FormPane(),// 表單組件(http://www.hightopo.com/guide/guide/plugin/form/ht-form-guide.html)
listBorder = this.listBorder = new ht.widget.BorderPane(),// 場景中間邊框面板組件(http://www.hightopo.com/guide/guide/core/borderpane/ht-borderpane-guide.html)
gv = this.gv = new ht.graph.GraphView(),// 拓撲組件(http://www.hightopo.com/guide/guide/core/beginners/ht-beginners-guide.html#ref_graphview)
borderPane = this.borderPane = new ht.widget.BorderPane(),
toolbar = this.toolbar = new ht.widget.Toolbar(),// 工具條組件(http://www.hightopo.com/guide/guide/core/toolbar/ht-toolbar-guide.html)
splitView = this.splitView = new ht.widget.SplitView(listBorder, borderPane, 'h', 220),// 分割組件
overview = this.overview = new ht.graph.Overview(gv),// 鷹眼組件(http://www.hightopo.com/guide/guide/plugin/overview/ht-overview-guide.html)
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 對話框(http://www.hightopo.com/guide/guide/plugin/dialog/ht-dialog-guide.html),我們設置此對話框中的內容為一個 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);// 列表上拖拽事件監聽(http://www.hightopo.com/guide/guide/core/listview/ht-listview-guide.html)
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 表單中選中的名稱相同的所有節點進行顯示
});

主要的代碼就解釋到這里,其他部分的內容有興趣的同學可以自己去摳代碼了解?https://hightopo.com/demo/rack-builder/index.html。還有不懂的可以上官網了解?https://hightopo.com/

beylze編程學院,一個分享編程知識和seo優化知識的網站。跟著beylze一起學習,每天都有進步。

通俗易懂,深入淺出,一篇文章只講一個知識點。

文章不深奧,不需要鉆研,在公交、在地鐵、在廁所都可以閱讀,隨時隨地漲姿勢。

文章不涉及代碼,不燒腦細胞,人人都可以學習。

當你決定關注beylze(公眾號:beylze),你已然超越了90%的其他從業者!

相關文章

優秀教程

国产亚洲欧美日韩