基於 HTML5 WebGL 的 3D 倉儲管理系統

倉儲管理系統(WMS)是一個實時的計算機軟件系統,它可以按照運做的業務規則和運算法則,對信息、資源、行爲、存貨和分銷運做進行更完美地管理,使其最大化知足有效產出和精確性的要求。從財務軟件、進銷存軟件CIMS,從MRP、MRPII到ERP,表明了中國企業從粗放型管理走向集約管理的要求,競爭的激烈和對成本的要求使得管理對象表現爲:整和上游、企業自己、下游一體化供應鏈的信息和資源。而倉庫,尤爲是製造業中的倉庫,做爲鏈上的節點,不一樣鏈節上的庫存觀不一樣,在物流供應鏈的管理中,再也不把庫存做爲維持生產和銷售的措施,而將其做爲一種供應鏈的平衡機制,其做用主要是協調整個供應鏈。但現代企業同時又面臨着許多不肯定因素,不管他們來自分供方仍是來自生產或客戶,對企業來講處理好庫存管理與不肯定性關係的惟一辦法是增強企業之間信息的交流和共享,增長庫存決策信息的透明性、可靠性和實時性。而這,正是WMS所要幫助企業解決的問題。html

WMS軟件和進銷存管理軟件的最大區別在於:進銷存軟件的目標是針對於特定對象(如倉庫)的商品、單據流動,是對於倉庫做業結果的記錄、覈對和管理——報警、報表、結果分析,好比記錄商品出入庫的時間、經手人等;而WMS軟件則除了管理倉庫做業的結果記錄、覈對和管理外最大的功能是對倉庫做業過程的指導和規範:即不但對結果進行處理,更是經過對做業動做的指導和規範保證做業的準確性、速度和相關記錄數據的自動登記(入計算機系統),增長倉庫的效率、管理透明度、真實度下降成本好比經過無線終端指導操做員給某定單發貨:當操做員提出發貨請求時,終端提示操做員應到哪一個具體的倉庫貨位取出指定數量的那幾種商品,掃描貨架和商品條碼覈對是否正確,而後送到接貨區,錄入運輸單位信息,完成出貨任務,重要的是包括出貨時間、操做員、貨物種類、數量、產品序列號、承運單位等信息在貨物裝車的同時已經經過無線方式傳輸到了計算機信息中心數據庫。
因爲市場需求量較大,咱們來好好解析今天這個例子。
動圖以下:
能夠在  http://download.csdn.net/download/u013161495/10136727 下載代碼。具體運行代碼請參考 readme.html。
這個例子是採用 es6 的模塊化的方式部署的。打開 index.html 進入 lib/index.js,源碼是在 src 文件夾中,咱們直接進 src/view 下的 index.js 
 在頂部加載其餘模塊中含有 export 接口的模塊:
import sidebar from './sidebar.js';
import header from './header.js';
import BorderLayout from './common/BorderLayout.js';
import shelfPane from './common/shelfPane.js';
import chartPane from './common/chartPane.js';
import graph3dView from './3d/index';

咱們將頁面上的每一個部分分開來放在不一樣的 js 文件中,就是上面加載的 js export 的部分,根層容器 BorderLayout(總體最外層的 div),整張圖上的部分都是基於 borderLayout 的。es6

  • 最外層容器 BorderLayout 是在 src/view/common 下的 BorderLayout.js 中自定義的類,其中 ht.Default.def(className, superClass, methods) 是 HT 中封裝的自定義類的函數,其中 className 爲自定義類名, superClass 爲要繼承的父類,methods 爲方法和變量聲明,要使用這個方法要先在外部定義這個函數變量,經過 functionName.superClass.constructor.call(this) 方法繼承。BorderLayout 自定義類繼承了 ht.ui.drawable.BorderLayout 佈局組件,此佈局器將自身空間劃分爲上、下、左、右、中間五個區域,每一個區域能夠放置一個子組件。爲了能正常交互,重寫 getSplitterAt 函數將 splitterRect 的寬度修改成 10,以及爲了調整左側 splitterCanvas 的尺寸,以便擋住子組件而重寫的 layoutSplitterCanvas 兩個方法:
let BorderLayout = function() {
    BorderLayout.superClass.constructor.call(this);
    this.setContinuous(true);
    this.setSplitterSize(0);
};

ht.Default.def(BorderLayout, ht.ui.BorderLayout, {//自定義類
    /**
     * splitter 寬度都爲 0,爲了能正常交互,重寫此函數將 splitterRect 的寬度修改成 10
     * @override
     */
    getSplitterAt: function (event) {//獲取事件對象下分隔條所在的區域
        var leftRect = this._leftSplitterRect, lp;
        if (leftRect) {
            leftRect = ht.Default.clone(leftRect);
            leftRect.width = 10;
            leftRect.x -= 5;
            if (event instanceof Event)
                lp = this.lp(event);
            else
                lp = event;
            if (ht.Default.containsPoint(leftRect, lp)) return 'left';
        }
        return BorderLayout.superClass.getSplitterAt.call(this, event);
    },
    /**
     * 調整左側 splitterCanvas 的尺寸,以便擋住子組件
     * @override
     */
    layoutSplitterCanvas: function(canvas, x, y, width, height, region) {
        if (region === 'left') {
            canvas.style.pointerEvents = '';
            canvas.style.display = 'block';
            ht.Default.setCanvas(canvas, 10, height);
            canvas.style.left = this.getContentLeft() + this.tx() + x - 5 + 'px';
            canvas.style.top = this.getContentTop() + this.ty() + y + 'px';
        }
        else {
            BorderLayout.superClass.layoutSplitterCanvas.call(this, canvas, x, y, width, height, region);
        }
    }
});
export default BorderLayout;
  • 左側欄 sidebar,分爲 8 個部分:頂部 logo、貨位統計表格、進度條、分割線、貨物表格、圖表、管理組、問題反饋按鈕等。

           

能夠查看 src/view 下的 sidebar.js 文件,這個 js 文件中一樣加載了  src/view/common 下的TreeHoverBackgroundDrawable.js 和 ProgressBarSelectBarDrawable.js 中的  TreeHoverBackgroundDrawable 和 ProgressBarSelectBarDrawable 變量,以及 src/controller 下的 sidebar.js 中的 controller 變量:算法

import TreeHoverBackgroundDrawable from './common/TreeHoverBackgroundDrawable.js';
import ProgressBarSelectBarDrawable from './common/ProgressBarSelectBarDrawable.js';
import controller from '../controller/sidebar.js';

HT 封裝了一個 ht.ui.VBoxLayout 函數,用來將子組件放置在同一垂直列中,咱們能夠將左側欄要顯示的部分都放到這個組件中,這樣全部的部分都是以垂直列排布:數據庫

let vBoxLayout = new ht.ui.VBoxLayout();//此佈局器將子組件放置在同一垂直列中;
vBoxLayout.setBackground('#17191a');

頂部 logo 是根據在 Label 標籤上添加 icon 的方法來實現的,並將這個 topLabel 添加進垂直列 vBoxLayout 中:json

let topLabel = new ht.ui.Label(); //標籤組件
topLabel.setText('Demo-logo');//設置文字內容
topLabel.setIcon('imgs/logo.json');//設置圖標,能夠是顏色或者圖片等
topLabel.setIconWidth(41);
topLabel.setIconHeight(37);
topLabel.setTextFont('18px arial, sans-serif');
topLabel.setTextColor('#fff');
topLabel.setPreferredSize(1, 64);//組件自身最合適的尺寸
topLabel.setBackground('rgb(49,98,232)');
vBoxLayout.addView(topLabel, {//將子組件加到容器中
    width: 'match_parent'//填滿父容器 
});

對於「貨位統計表格」,咱們採用的是 HT 封裝的 TreeTableView 組件,以樹和表格的組合方式呈現 DataModel 中數據元素屬性及父子關係,並將這個「樹表」添加進垂直列 vBoxLayout 中:canvas

let shelfTreeTable = new ht.ui.TreeTableView();//樹表組件,以樹和表格的組合方式呈現 DataModel 中數據元素屬性及父子關係
shelfTreeTable.setHoverBackgroundDrawable(new TreeHoverBackgroundDrawable('#1ceddf', 2));//設置 hover 狀態下行選中背景的 Drawable 對象
shelfTreeTable.setSelectBackgroundDrawable(new TreeHoverBackgroundDrawable('#1ceddf', 2));//設置行選中背景的 Drawable 對象 參數爲「背景
shelfTreeTable.setBackground(null);
shelfTreeTable.setIndent(20);//設置不一樣層次的縮進值
shelfTreeTable.setColumnLineVisible(false);//設置列線是否可見
shelfTreeTable.setRowLineVisible(false);
shelfTreeTable.setExpandIcon('imgs/expand.json');//設置展開圖標圖標,能夠是顏色或者圖片等
shelfTreeTable.setCollapseIcon('imgs/collapse.json');//設置合併圖標圖標,能夠是顏色或者圖片等
shelfTreeTable.setPreferredSizeRowCountLimit();//設置計算 preferredSize 時要限制的數據行數
shelfTreeTable.setId('shelfTreeTable');
vBoxLayout.addView(shelfTreeTable, {
    width: 'match_parent',
    height: 'wrap_content',//組件自身首選高度
    marginTop: 24,
    marginLeft: 4, 
    marginRight: 4
});

咱們在設置「行選中」時背景傳入了一個 TreeHoverBackgroundDrawable 對象,這個對象是在 src\view\common 下的 TreeHoverBackgroundDrawable.js 文件中定義的,其中 ht.Default.def(className, superClass, methods) 是 HT 中封裝的自定義類的函數,其中 className 爲自定義類名, superClass 爲要繼承的父類,methods 爲方法和變量聲明,要使用這個方法要先在外部定義這個函數變量,經過 functionName.superClass.constructor.call(this) 方法繼承。TreeHoverBackgroundDrawable 自定義類繼承了 ht.ui.drawable.Drawable 組件用於繪製組件背景、圖標等,只重寫了 draw 和 getSerializableProperties 兩個方法,咱們在 draw 方法中重繪了 shelfTreeTable 的行選中背景色,並重載了  getSerializableProperties 序列化組件函數,並將 TreeHoverBackgroundDrawable 傳入的參數做爲 map 中新添加的屬性:數組

let TreeHoverBackgroundDrawable = function(color, width) {
    TreeHoverBackgroundDrawable.superClass.constructor.call(this);
    this.setColor(color);
    this.setWidth(width);
};
ht.Default.def(TreeHoverBackgroundDrawable, ht.ui.drawable.Drawable, {
    ms_ac: ['color', 'width'],
    draw: function(x, y, width, height, data, view, dom) {
        var self = this,
            g = view.getRootContext(dom),
            color = self.getColor();
       
        g.beginPath();
        g.fillStyle = color;
        g.rect(x, y, self.getWidth(), height);
        g.fill();
    },
    getSerializableProperties: function() {
        var parentProperties = TreeHoverBackgroundDrawable.superClass.getSerializableProperties.call(this);
        return addMethod(parentProperties, {
            color: 1, width: 1
        });
    }
});

記住要導出 TreeHoverBackgroundDrawable :dom

export default TreeHoverBackgroundDrawable;

HT 還封裝了很是好用的 ht.ui.ProgressBar 組件,可直接繪製進度條:ide

let progressBar = new ht.ui.ProgressBar();
progressBar.setId('progressBar');
progressBar.setBackground('#3b2a00');//設置組件的背景,能夠是顏色或者圖片等
progressBar.setBar('rgba(0,0,0,0)');//設置進度條背景,能夠是顏色或者圖片等
progressBar.setPadding(5);
progressBar.setSelectBarDrawable(new ProgressBarSelectBarDrawable('#c58348', '#ffa866')); //設置前景(即進度覆蓋區域)的 Drawable 對象,能夠是顏色或者圖片等
progressBar.setValue(40);//設置當前進度值
progressBar.setBorderRadius(0);
vBoxLayout.addView(progressBar, {
    marginTop: 24,
    width: 'match_parent',
    height: 28,
    marginBottom: 24,
    marginLeft: 14,
    marginRight: 14
});

咱們在 設置「前景」的時候傳入了一個 ProgressBarSelectBarDrawable 對象,這個對象在 src\view\common 下的 ProgressBarSelectBarDrawable.js 中定義的。具體定義方法跟上面的 TreeHoverBackgroundDrawable 函數對象相似,這裏再也不贅述。模塊化

分割線的製做最爲簡單,只要將一個矩形的高度設置爲 1 便可,咱們用 ht.ui.View() 組件來製做:

let separator = new ht.ui.View();//全部視圖組件的基類,全部可視化組件都必須今後類繼承
separator.setBackground('#666');
vBoxLayout.addView(separator, {
    width: 'match_parent',
    height: 1,
    marginLeft: 14, 
    marginRight: 14,
    marginBottom: 24
});

貨物表格的操做幾乎和貨位統計表格相同,這裏再也不贅述。

咱們將一個 json 的圖表文件當作圖片傳給圖表的組件容器做爲背景,也能很輕鬆地操做:

let chartView = new ht.ui.View();
chartView.setBackground('imgs/chart.json');
vBoxLayout.addView(chartView, {
    width: 173,
    height: 179,
    align: 'center',
    marginBottom: 10
});

管理組和頂部 logo 的定義方式相似,這裏再也不贅述。

問題反饋按鈕,咱們將這個部分用 HT 封裝的 ht.ui.Button 組件來製做,並將這個部分添加進垂直列 vBoxLayout 中:

let feedbackButton = new ht.ui.Button();//按鈕類
feedbackButton.setId('feedbackButton');
feedbackButton.setText('問題反饋:service@hightopo.com');
feedbackButton.setIcon('imgs/em.json');
feedbackButton.setTextColor('#fff');
feedbackButton.setHoverTextColor(shelfTreeTable.getHoverLabelColor());//設置 hover 狀態下文字顏色
feedbackButton.setActiveTextColor(feedbackButton.getHoverTextColor());//設置 active 狀態下文字顏色
feedbackButton.setIconWidth(16);
feedbackButton.setIconHeight(16);
feedbackButton.setIconTextGap(10);
feedbackButton.setAlign('left');
feedbackButton.setBackground(null);
feedbackButton.setHoverBackground(null);
feedbackButton.setActiveBackground(null);
vBoxLayout.addView(feedbackButton, {
    width: 'match_parent',
    marginTop: 5,
    marginBottom: 10,
    marginLeft: 20
});

視圖部分作好了,在模塊化開發中,controller 就是作交互的部分,shelfTreeTable 貨位統計表格, cargoTreeTable 貨物表格, feedbackButton 問題反饋按鈕, progressBar  進度條四個部分的交互都是在在 src/controller 下的 sidebar.js 中定義的。經過 findViewById(id, recursive) 根據id查找子組件,recursive 表示是否遞歸查找。

shelfTreeTable 貨位統計表格的數據綁定傳輸方式與 cargoTreeTable 貨物表格相似,這裏咱們只對 shelfTreeTable 貨位統計表格的數據綁定進行解析。shelfTreeTable 一共有三列,其中不一樣的部分只有「已用」和「剩餘」兩個部分,因此咱們只要將這兩個部分進行數據綁定便可,先建立兩列:

let column = new ht.ui.Column();//列數據,用於定義表格組件的列信息
column.setName('used');//設置數據元素名稱
column.setAccessType('attr');//在這裏 name 爲 used,採用 getAttr('used') 和 setAttr('used', 98) 的方式存取 set/getAttr 簡寫爲 a
column.setWidth(65);
column.setAlign('center');
columnModel.add(column);

column = new ht.ui.Column();
column.setName('remain');
column.setAccessType('attr');
column.setWidth(65);
column.setAlign('center');
columnModel.add(column);

接着遍歷 json 文件,將 json 文件中對應的 used、remain以及 labelColors 經過 set/getAttr 或 簡寫 a 的方式進行數據綁定:

for (var i = 0; i < json.length; i++) {
    var row = json[i];//獲取 json 中的屬性
    var data = new ht.Data();
    data.setIcon(row.icon);//將 json 中的 icon 傳過來
    data.setName(row.name);
    data.a('used', row.used);
    data.a('remain', row.remain);
    data.a('labelColors', row.colors);
    data.setIcon(row.icon);
    treeTable.dm().add(data);//在樹表組件的數據模型中添加這個data節點
    var children = row.children;
    if (children) {
        for (var j = 0; j < children.length; j++) {
            var child = children[j];
            var childData = new ht.Data();
            childData.setName(child.name);
            childData.setIcon(child.icon);
            childData.a('used', child.used);
            childData.a('remain', child.remain);
            childData.a('labelColors', child.colors);
            childData.setParent(data);
            treeTable.dm().add(childData);
        }
    }
}

最後在 controller 函數對象中調用 這個函數:

initTreeTableDatas(shelfTreeTable, json);//json 爲 ../model/shelf.json傳入

progressBar 進度條的變化是經過設置定時器改變 progressBar 的 value 值來動態改變的:

setInterval(() => {
    if (progressBar.getValue() >= 100) {
        progressBar.setValue(0);
    }
    progressBar.setValue(progressBar.getValue() + 1);
}, 50);

feedbackButton 問題反饋按鈕,經過增長 View 事件監聽器來監聽按鈕的點擊事件:

feedbackButton.addViewListener(e => {
    if (e.kind === 'click') {//HT 自定義的事件屬性,具體查看 http://hightopo.com/guide/guide/core/beginners/ht-beginners-guide.html
        window.location.href = "mailto:service@www.hightopo.com";//當前頁面打開URL頁面
    }
});
  • 右側根容器 splitLayout

          

直接用的分割組件 ht.ui.SplitLayout 進行分割佈局:

let splitLayout = new ht.ui.SplitLayout();//此佈局器將自身空間劃分爲上、下兩個區域或左、右兩個區域,每一個區域能夠放置一個子組件
splitLayout.setSplitterVisible(false);
splitLayout.setPositionType('absoluteFirst');
splitLayout.setOrientation('v');
  • 右側頭部 header

        

這個 header 是從 src/view 下的 header.js 中獲取的對象,爲 ht.ui.RelativeLayout 相對定位佈局器,分爲 5 個部分:searchField 搜索框、titleLabel 主標題、temperatureLabel1 溫度、humidityLabel1 溼度以及 airpressureLabel1 氣壓。

這裏咱們沒有對「搜索框」 searchField 進行數據綁定,以及搜索的功能,這只是一個樣例,不涉及業務部分:

let searchField = new ht.ui.TextField();//文本框組件
searchField.setBorder(new ht.ui.border.LineBorder(1, '#d8d8d8'));//在組件的畫布上繪製直線邊框
searchField.setBorderRadius(12);
searchField.setBackground(null);
searchField.setIcon('imgs/search.json');
searchField.setIconPosition('left');
searchField.setPadding([2, 16, 2, 16]);
searchField.setColor('rgb(138, 138, 138)');
searchField.setPlaceholder('Find everything...');
searchField.getView().className = 'search';
header.addView(searchField, {
    width: 180,
    marginLeft: 20,
    vAlign: 'middle'
});

對於 titleLabel 主標題比較簡單,和溫度、溼度以及氣壓相似,我就只說明一下主標題 titleLabel 的定義:

let titleLabel = new ht.ui.Label();//標籤組件
titleLabel.setId('title');
titleLabel.setIcon('imgs/expand.json');
titleLabel.setTextColor('rgb(138, 138, 138)');
titleLabel.setText('杭州倉庫');
titleLabel.setHTextPosition('left');//設置文字在水平方向相對於圖標的位置,默認值爲 'right'
titleLabel.setIconTextGap(10);//設置圖標和文字之間的間距
titleLabel.setBorder(new ht.ui.border.IndividualLineBorder(0, 0, 3, 0, '#3162e8'))//在組件的畫布上繪製直線邊框;與 LineBorder 不一樣的是,此邊框能夠單獨繪製某一個或幾個方向的邊框
titleLabel.setTextFont('16px arial');

header.addView(titleLabel, {
    height: 'match_parent',
    width: 'wrap_content',
    align: 'center'
});

而後交互部分在 src/controller 下的 header.js 中作了右鍵點擊出現菜單欄以及單擊 titleLabel 的位置出現下拉菜單兩種交互,經過控制鼠標的點擊事件來控制事件的交互:

let title, contextMenu;
export default function controller (view) {
    title = view.findViewById('title');

    contextMenu = new ht.ui.ContextMenu();//右鍵菜單組件
    contextMenu.setLabelColor('rgb(138, 138, 138)');
    contextMenu.setHoverRowBackground('#3664e4');
    contextMenu.setItems([
        {
            label: '北京倉庫'
        },
        {
            label: '上海倉庫'
        },
        {
            label: '廈門倉庫'
        }
    ]);

    contextMenu.addViewListener((e) => {
        if (e.kind === 'action') {//HT 自定義的事件類型
            title.setText(e.item.label);
        }
    });

    title.getView().addEventListener('mousedown', e => {
        if (contextMenu.isInDOM()) {//判斷組件是否在 DOM 樹中
            contextMenu.hide();//隱藏菜單
            document.removeEventListener('mousedown', handleWindowClick);//移除mousedown監聽事件
        }
        else {//沒有右鍵點擊過
            var items = contextMenu.getItems();
            for (var i = 0; i < items.length; i++) {
                items[i].width = title.getWidth();
            }

            let windowInfo = ht.Default.getWindowInfo(),//獲取當前窗口left|top|width|height的參數信息
                titleRect = title.getView().getBoundingClientRect();
            contextMenu.show(windowInfo.left + titleRect.left, windowInfo.top + titleRect.top + titleRect.height);

            document.addEventListener('mousedown', handleWindowClick);
        }
    });
}

function handleWindowClick(e) {
    if (!contextMenu.getView().contains(e.target) && !title.getView().contains(e.target)) {//判斷元素是否在數組中
        contextMenu.hide();
        document.removeEventListener('mousedown', handleWindowClick);
    }
}
  • 右側下部分 RelativeLayout 相對佈局器(相對於右側下部分最根層 div),包含中間顯示 3d 部分 graph3dView、雙擊貨櫃或貨物纔會出現的 shelfPane、以及出如今右下角的圖表 chartPane,將這三部分添加進 RelativeLayout 相對佈局容器:

           

let relativeLayout = new ht.ui.RelativeLayout();//建立相對佈局器
relativeLayout.setId('contentRelative');
relativeLayout.setBackground('#060811');

var htView = new ht.ui.HTView(graph3dView);
htView.setId('contentHTView');
relativeLayout.addView(htView, {//將 3d 組件添加進relativeLayout 相對佈局器
    width: 'match_parent',
    height: 'match_parent'
});

relativeLayout.addView(shelfPane, {//將雙擊出現的詳細信息 shelfPane 組件添加進relativeLayout 相對佈局器
    width: 220,
    height: 'wrap_content',
    align: 'right',
    marginRight: 30,
    marginTop: 30
});

relativeLayout.addView(chartPane, {//將圖表 chartPane 組件添加進relativeLayout 相對佈局器
    width: 220,
    height: 200,
    align: 'right',
    vAlign: 'bottom',
    marginRight: 30,
    marginBottom: 30
})

而後將右側相對佈局器 relativeLayout 和右側頭部 header 添加進右側底部容器 splitLayout:

let splitLayout = new ht.ui.SplitLayout();
splitLayout.setSplitterVisible(false);
splitLayout.setPositionType('absoluteFirst');
splitLayout.setOrientation('v');
splitLayout.addView(header, {
    region: 'first'//指定組件所在的區域,可選值爲:'first'|'second'
});
splitLayout.addView(relativeLayout, {
    region: 'second'
});

再將左側部分的 sidebar 和右側部分的全部也就是 splitLayout 添加進整個底部容器 borderLayout,再將底部容器添加進 html body 體中:

let borderLayout = new BorderLayout();
borderLayout.setLeftWidth(250);
borderLayout.addView(sidebar, {
    region: 'left',// 指定組件所在的區域,可選值爲:'top'|'right'|'bottom'|'left'|'center'
    width: 'match_parent'//組件自身首選寬度 
});
borderLayout.addView(splitLayout, {
    region: 'center'
});

borderLayout.addToDOM();//將 borderLayout 添加進 body 體中

 咱們具體說說這個相對佈局器內部包含的 3d 部分 graph3dView、雙擊貨櫃或貨物纔會出現的 shelfPane、以及出如今右下角的圖表 chartPane。

(1) graph3dView

從 src\view\3d 文件夾中的 index.js 中獲取 graph3dView 的外部接口被 src/view 中的 index.js 調用:

import graph3dView from './3d/index';

從這個 3d 場景中能夠看到,咱們須要「地板」、「牆面」、「貨架」、「叉車」、「貨物」以及 3d 場景。

在 3d 文件夾下的 index.js 中,咱們從文件夾中導入全部須要的接口:

import {//這裏導入的都是一些基礎數據
  sceneWidth, sceneHeight, sceneTall,
  toShelfList, randomCargoType
} from './G.js';

// 模擬數據接口
import {
  stockinout,// 出入庫
  initiate,// 初始化
  inoutShelf// 上下架
} from './interfaces';

import { Shelf } from './shelf';//貨架
import { Floor } from './floor';//地板
import { Wall } from './wall';//牆面
import { Car } from './car';//叉車
import { g3d } from './g3d';//3d場景
import { getCargoById } from './cargo';//貨物

g3d.js 文件中只設置了場景以及對部分事件的監聽:

g3d.mi((e) => {// 監聽事件 addInteractorListener
  const kind = e.kind;
  if (kind === 'doubleClickData') {//雙擊圖元事件
    let data = e.data;//事件相關的數據元素
    if (data instanceof Shelf) {//若是是貨架
      data.setTransparent(false);

      eventbus.fire({ type: 'cargoBlur' });//派發事件,依次調用全部的監聽器函數
    }
    else {
      data = data.a('cargo');
      if (!data) return;
      data.transparent = false;

      eventbus.fire({ type: 'cargoFocus', data: data });
    }

    for (let i = shelfList.length - 1; i >= 0; i--) {//除了雙擊的圖元,其餘的圖元都設置透明
      const shelf = shelfList[i];
      shelf.setTransparent(true, data);
    }
    return;
  }
  if (kind === 'doubleClickBackground') {//雙擊背景事件
    for (let i = shelfList.length - 1; i >= 0; i--) {//雙擊背景,全部的圖元都不透明
      const shelf = shelfList[i];
      shelf.setTransparent(false);
    }

    eventbus.fire({ type: 'cargoBlur' });
    return;
  }
});

咱們在 G.js 中定義了一些基礎數據,其餘引用的 js 中都會反覆調用這些變量,因此咱們先來解析這個文件:

const sceneWidth = 1200;//場景寬度
const sceneHeight = 800;//場景高度
const sceneTall = 410;//場景的深度

const globalOpacity = 0.3;//透明度

const cargoTypes = {//貨物類型,分爲四種
  'cask': {//木桶
    'name': 'bucket'
  },
  'carton': {//紙箱
    'name': 'carton'
  },
  'woodenBox1': {//木箱1
    'name': 'woodenBox1'
  },
  'woodenBox2': {//木箱2
    'name': 'woodenBox2'
  }
};

裏面有三個函數,分別是「貨架的 obj 分解」、「加載模型」、「隨機分配貨物的類型」:

function toShelfList(list) {//將貨架的 obj 分解,
  const obj = {};
  list.forEach((o) => {//這邊的參數o具體內容能夠查看 view/3d/interface.js
    const strs = o.cubeGeoId.split('-');

    let rs = obj[o.rackId];
    if (!rs) {
      rs = obj[o.rackId] = [];
    }

    const ri = parseInt(strs[2].substr(1)) - 1;
    let ps = rs[ri];
    if (!ps) {
      ps = rs[ri] = [];
    }

    let type = 'cask';
    if (o.inventoryType === 'Import') {
      while((type = randomCargoType()) === 'cask') {}
    }

    const pi = parseInt(strs[3].substr(1)) - 1;
    ps[pi] = {
      id: o.cubeGeoId,
      type: type
    };
  });

  return obj;
}

function loadObj(shape3d, fileName, cbFunc) {//加載模型
  const path = './objs/' + fileName;
  ht.Default.loadObj(path + '.obj', path + '.mtl', {
    shape3d: shape3d,
    center: true,
    cube: true,
    finishFunc: cbFunc
  });
}

function randomCargoType() {//隨機分配「貨物」的類型
  const keys = Object.keys(cargoTypes);
  const i = Math.floor(Math.random() * keys.length);
  return keys[i];
}

 這個 3d 場景中還有不可缺乏的「貨物」和「貨架」以及「叉車」,三者的定義方式相似,這裏只對「貨架」進行解釋。咱們直接在「貨物」的 js 中引入底下的「托盤」的 js 文件,將它們看作一個總體:

import { Pallet } from './pallet';
import {
  cargoTypes,
  loadObj,
  globalOpacity
} from './G';

在 src\view\3d\cargo.js 文件中,定義了一個「貨物」類,這個類中聲明瞭不少方法,比較基礎,有須要的本身能夠查看這個文件,這裏我不過多解釋。主要講一下如何加載這個「貨物」的 obj,咱們在 G.js 文件中有定義一個 loadObj 函數,咱們在代碼頂部也有引入,導入 obj 文件以後就在「貨物」的庫存增長這個「貨物」:

for (let type in cargoTypes) {//遍歷 cargoTypes 數組, G.js 中定義的
  const cargo = cargoTypes[type];
  loadObj(type, cargo.name, (map, array, s3) => {//loadObj(shape3d, fileName, cbFunc) cbFunc 中的參數能夠參考 obj 手冊
    cargo.s3 = s3;//將 cargo 的 s3 設置原始大小

    updateCargoSize();
  });
}

function updateCargoSize() {
  let c, obj;
  for (let i = cargoList.length - 1; i >= 0; i--) {
    c = cargoList[i];

    obj = cargoTypes[c.type];
    if (!obj.s3) continue;
    c.boxS3 = obj.s3;
  }
}

 還有就是界面上「貨物」的進出庫的動畫,主要用的方法是 HT 封裝的 ht.Default.startAnim 函數(HT for Web 動畫手冊),出的動畫與進的動畫相似,這裏不贅述:

// 貨物進
  in() {
    if (anim) {//若是有值,就中止動畫
      anim.stop(true);
    }

    this.x = this.basicX + moveDistance;
    this.opacity = 1;

    anim = ht.Default.startAnim({
      duration: 1500,
      finishFunc: () => {//動畫結束以後調用這個函數,將anim設置爲空中止動畫
        anim = null;
      },
      action: (v, t) => {
        this.x = this.basicX + (1 - v) * moveDistance;//改變x座標,看起來像向前移動
      }
    });
  }

牆和地板也是比較簡單的,簡單地繼承 ht.Node 和 ht.Shape,這裏以「牆」進行解釋,繼承以後直接在構造函數中進行屬性的設置便可:

class Wall extends ht.Shape {//繼承 ht.Shape 類
  constructor(points, segments, tall, thickness, elevation) {
    super();

    this.setPoints(points);//設置「點」
    this.setSegments(segments);//設置「點之間的鏈接方式」
    this.setTall(tall);//控制Node圖元在y軸的長度
    this.setThickness(thickness);//設置「厚度」
    this.setElevation(elevation);//控制Node圖元中心位置所在3D座標系的y軸位置

    this.s({
      'all.transparent': true,//六面透明
      'all.opacity': 0.3,//透明度爲 0.3
      'all.reverse.flip': true,//六面的反面顯示正面的內容
      'bottom.visible': false//底面不可見
    });
  }
}

floor、wall、shelf 以及 car 這四個類都準備完畢,只須要在 src\view\3d\index.js 中 new 一個新的對象並加入到數據模型 dataModel 中便可,這裏只展現 car 「叉車」的初始化代碼:

// init Car
const car = new Car();
car.addToDataModel(dm);

至於「貨物」,咱們在這個 js 上是採用定時器調用 in 和 out 方法,這裏有一個模擬的數據庫 interfaces.js 文件,有需求的能夠看一下,這裏咱們只當數據來調用(進出庫和上下架相似,這裏只展現進出庫的設置方法):

// 輪訓掉用出入庫接口
setInterval(() => {
  const obj = stockinout();//出入庫
  let type = 'cask';
  if (obj.inventoryType === 'Import') {
    while((type = randomCargoType()) === 'cask') {}//若是爲「貨物」類型爲「木桶」
  }
  car.cargoType = type;
  if (obj.inOutStatus === 'I')//若是值爲 「I」,則進庫
    car.in();
  else//不然爲「o」,出庫
    car.out();
}, 30000);

(2) shelfPane

從 src\view\common 文件夾中的 shelfPane.js 中獲取 graph3dView 的外部接口被 src/view 中的 index.js 調用:

import shelfPane from './common/shelfPane.js';

shelfPane 是基於 Pane 類的,在 shelfPane.js 文件中引入這個類和事件派發器:

import Pane from './Pane.js';
import eventbus from '../../controller/eventbus';

Pane 類繼承於 HT 封裝的 ht.ui.TabLayout 類, 並作了一些特定的屬性設置:

class Pane extends ht.ui.TabLayout {
    constructor() {
        super();

        this.setBorder(new ht.ui.border.LineBorder(1, 'rgb(150,150,150)'));//設置組件的邊框
        this.setTabHeaderBackground(null);//設置標籤行背景,能夠是顏色或者圖片等
        this.setHoverTabBackground(null);//設置 Hover 狀態下的標籤背景,能夠是顏色或者圖片等
        this.setActiveTabBackground(null);//設置 Active 狀態下的標籤背景,能夠是顏色或者圖片等
        this.setTitleColor('rgb(184,184,184)');//設置正常狀態下標籤文字的顏色
        this.setActiveTitleColor('rgb(255,255,255)');//設置 Active 狀態下標籤文字的顏色
        this.setTabHeaderLineSize(0);//設置標籤行分割線寬度
        this.setMovable(false);//設置標籤是否可拖拽調整位置,默認爲 true
        this.setTabHeaderBackground('#1c258c');//設置標籤行背景,能夠是顏色或者圖片等
        this.setTabGap(0);//設置標籤之間的距離
    }
    getTabWidth(child) {//獲取指定子組件的標籤寬度
        const children = this.getChildren(),
            size = children.size();
        if (size === 0) {
            return this.getContentWidth();//獲取內容寬度,即組件寬度減去邊框寬度和左右內邊距寬度
        }
        else {
            return this.getContentWidth() / size;
        }
    }
    drawTab(g, child, x, y, w, h) {//繪製標籤
        const children = this.getChildren(),//獲取子組件列表
            size = children.size(),
            color = this.getCurrentTitleColor(child),//根據參數子組件的狀態(normal、hover、active、move),獲取標籤文字顏色
            font = this.getTitleFont(child),//獲取標籤文字字體
            title = this.getTitle(child);//獲取指定子組件的標籤文本
        if (size === 1) {
            ht.Default.drawText(g, title, font, color, x, y, w, h, 'left');//繪製文字
        }
        else {
            ht.Default.drawText(g, title, font, color, x, y, w, h, 'center');
        }

        if (children.indexOf(child) <  size - 1) {
            g.beginPath();//開始繪製
            g.rect(x + w - 1, y + 4, 1, h - 8);
            g.fillStyle = 'rgb(150,150,150)';
            g.fill();
        }
    }
    show() {
        this.setVisible(true);//設置組件是否可見
    }
    hide() {
        this.setVisible(false);
    }
}

咱們這個例子中的「信息」列表是一個表格組件,HT 經過 ht.ui.TableLayout 函數定義一個表格,而後經過 ht.ui.TableRow 向表格中添加行,這個例子中的「備註」、「編號」、「來源」、「入庫」、「發往」以及「出庫」都是文本框,這裏拿「備註」做爲舉例:

let tableLayout = new ht.ui.TableLayout();//此佈局器將自身空間按照行列數劃分爲 row * column 個單元格
tableLayout.setColumnPreferredWidth(0, 45);//設置列首選寬度
tableLayout.setColumnWeight(0, 0);//設置列寬度權重;若是佈局器的總寬度大於全部列的首選寬度之和,那麼剩餘的寬度就根據權重分配
tableLayout.setColumnPreferredWidth(1, 150);
tableLayout.setPadding(8);//設置組件內邊距,參數若是是數字,說明四邊使用相同的內邊距;若是是數組,則格式爲:[上邊距, 右邊距, 下邊距, 左邊距]

// 備註
var tableRow1 = new ht.ui.TableRow();//TableLayout 中的一行子組件;
var label = new ht.ui.Label();//標籤組件
label.setText('備註');//設置文字內容
label.setAlign('left');//設置文字和圖標在按鈕水平方向的總體對齊方式,默認爲 'center'
label.setTextColor('rgb(255,255,255)');//設置文字顏色

var textField = new ht.ui.TextField();//文本框組件
textField.setFormDataName('remark');//設置組件在表單中的名稱
textField.setBackground(null);//設置組件的背景,能夠是顏色或者圖片等;此值最終會被轉換爲 Drawable 對象
textField.setBorderRadius(0);//設置 CSS 邊框圓角
textField.setColor('rgb(138,138,138)');//設置文字顏色
textField.setPlaceholder('無');//設置輸入提示
textField.setBorder(new ht.ui.border.IndividualLineBorder(0, 0, 1, 0, 'rgb(138,138,138)'));//設置組件的邊框

tableRow1.addView(label);//添加子組件
tableRow1.addView(textField);

tableLayout.addView(tableRow1);//將子組件加到容器中

「歸類」和「模型」相似,都是下拉框,咱們用 HT 封裝的 ht.ui.ComboBox 組合框組件,跟 ht.ui.TextField 也是殊途同歸,只是具體操做不一樣而已,HT 這樣作使用上更簡便更容易上手,這裏咱們以「模型」進行解析,在設置「下拉數據」的時候咱們利用了 HT 中的數據綁定:

// 模型
var tableRow4 = new ht.ui.TableRow();
label = new ht.ui.Label();
label.setText('模型');
label.setAlign('left');
label.setTextColor('rgb(255,255,255)');

var comboBox = new ht.ui.ComboBox();
comboBox.setFormDataName('model');
comboBox.setBackground(null);
comboBox.setColor('rgb(232,143,49)');
comboBox.setDatas([////設置下拉數據數組
  { label: '紙箱', value: 'carton' },
  { label: '木箱1', value: 'woodenBox1' },
  { label: '木箱2', value: 'woodenBox2' },
  { label: '木桶', value: 'cask' }
]); 
comboBox.setIcon('imgs/combobox_icon.json');
comboBox.setHoverIcon('imgs/combobox_icon_hover.json');
comboBox.setActiveIcon('imgs/combobox_icon_hover.json');
comboBox.setBorderRadius(0);////設置 CSS 邊框圓角
comboBox.setBorder(new ht.ui.border.IndividualLineBorder(0, 0, 1, 0, 'rgb(138,138,138)'));

tableRow4.addView(label);
tableRow4.addView(comboBox);

tableLayout.addView(tableRow4);

最後一個「染色」,HT 封裝了 ht.ui.ColorPicker 顏色選擇器組件,組件從 ht.ui.ComboBox 繼承並使用 ht.ui.ColorDropDown 做爲下拉模板,跟上面的下拉列表很相似,只是下拉的模板變了而已:

// 染色
var tableRow9 = new ht.ui.TableRow();
label = new ht.ui.Label();
label.setText('染色');
label.setAlign('left');
label.setTextColor('rgb(255,255,255)');

var comboBox = new ht.ui.ColorPicker();//顏色選擇器組件
comboBox.setFormDataName('blend');//設置組件在表單中的名稱
comboBox.getView().className = 'content_colorpicker';
comboBox.setBackground(null);
comboBox.setPreviewBackground(null);//設置預覽背景;能夠是顏色或者圖片等
comboBox.getInput().style.visibility = 'visible';//獲取組件內部的 input 框的 style 樣式
comboBox.setReadOnly(true);//設置只讀
comboBox.setColor('rgba(0,0,0,0)');
comboBox.setPlaceholder('修改貨箱顏色');
comboBox.setIcon('imgs/combobox_icon.json');
comboBox.setHoverIcon('imgs/combobox_icon_hover.json');
comboBox.setActiveIcon('imgs/combobox_icon_hover.json');
comboBox.setBorderRadius(0);
comboBox.setBorder(new ht.ui.border.IndividualLineBorder(0, 0, 1, 0, 'rgb(138,138,138)'));
comboBox.setInstant(true);//設置即時模式;在這種模式下,每輸入一個字符 value 屬性變化事件就會當即被派發,不然只有失去焦點或敲回車時才被派發

tableRow9.addView(label);
tableRow9.addView(comboBox);

tableLayout.addView(tableRow9);

最後經過 ht.ui.Form 組件的 addChangeListener 事件監聽函數監聽 JSON 總體變化事件和 JSON 中單條數據變化事件,這兩種事件的解釋以下圖:

具體監聽方法以下:

form.addChangeListener((e) => {
  const cargo = form.__cargo__;
  if (e.kind === 'formDataValueChange') {//JSON 中單條數據值變化事件
    const name = e.name;
    let value = e.newValue;

    if (name === 'blend') {
      if (value && value.startsWith('rgba')) {
        const li = value.lastIndexOf(',');
        value = 'rgb' + value.substring(value.indexOf('('), li) + ')';
      }
    }
    cargo.setValue(name, value);
  }
});

而後經過 HT 封裝的事件派發器 ht.Notifier 將界面中不一樣區域的組件之間經過事件派發進行交互,根據不一樣的事件類型進行不一樣的動做:

eventbus.add((e) => {//增長監聽器 事件總線;界面中不一樣區域的組件之間經過事件派發進行交互
  if (e.type === 'cargoFocus') {
    shelfPane.show();

    const cargo = e.data;
    form.__cargo__ = cargo;
    const json = form.getJSON();//獲取由表單組件的名稱和值組裝成的 JSON 數據
    for (let k in json) {
      form.setItem(k, cargo.getValue(k));
    }
    return;
  }
  if (e.type === 'cargoBlur') {
    shelfPane.hide();
    return;
  }
});

(3) chartPane

 從 src\view\common 文件夾中的 chartPane.js 中獲取 graph3dView 的外部接口被 src/view 中的 index.js 調用:
import chartPane from './common/chartPane.js';

 chartPane 和 shelfPane 相似,都是 Pane 類的對象,屬性也相似,不一樣的是內容。由於今天展現的只是一個 Demo,咱們並無作過多的關於圖表插件的處理,因此這裏就用圖片來代替動態圖表,不過就算想作也是很容易的事,HT 運用第三方插件也是很容易上手的,能夠看這邊的例子 http://hightopo.com/demo/large-screen/index.htmlHT 官網上有更多有趣的例子!

回到正題,chartPane 圖表面板的實現很是容易,將內部的子組件設置背景圖片再添加進 chartPane 圖表面板中便可:

import Pane from './Pane.js';

var chartPane = new Pane();

var view1 = new ht.ui.View();
view1.setBackgroundDrawable(new ht.ui.drawable.ImageDrawable('imgs/chart.png', 'fill'));//設置組件的背景 Drawable 對象;組件渲染時優先使用此 Drawable 對象,若是爲空,再用 background 轉換

var view2 = new ht.ui.View();
view2.setBackgroundDrawable(new ht.ui.drawable.ImageDrawable('imgs/chart.png', 'fill'));

chartPane.getView().style.background = 'rgba(18,28,64,0.60)';//設置背景顏色

chartPane.addView(view1, {//將子組件加到容器中
    title: '其餘圖表'
});

chartPane.addView(view2, {
    title: '庫存負載'
});

chartPane.setActiveView(view2);//設置選中的子組件

整個例子解析完畢,有興趣的小夥伴能夠去 HT 官網上自習查閱資料,好好品味,必定會發現更大的世界。

個人博客即將同步至騰訊雲+社區,邀請你們一同入駐。

相關文章
相關標籤/搜索