倉儲治理系統(WMS)是一個實時的計算機軟件系統,它能夠按照運作的業務規則和運算法則,對信息、資源、行為、存貨和分銷運作進行更完美地治理,使其最大化滿足有效產出和精確性的要求。從財務軟件、進銷存軟件CIMS,從MRP、MRPII到ERP,代表了中國企業從粗放型治理走向集約治理的要求,競爭的激烈和對成本的要求使得治理對象表現為:整和上游、企業本身、下游一體化供給鏈的信息和資源。而倉庫,尤其是制造業中的倉庫,作為鏈上的節點,不同鏈節上的庫存觀不同,在物流供給鏈的治理中,不再把庫存作為維持生產和銷售的措施,而將其作為一種供給鏈的平衡機制,其作用主要是協調整個供給鏈。但現代企業同時又面臨著許多不確定因素,無論他們來自分供方還是來自生產或客戶,對企業來說處理好庫存治理與不確定性關系的唯一辦法是加強企業之間信息的交流和共享,增加庫存決策信息的透明性、可靠性和實時性。而這,正是WMS所要幫助企業解決的問題。
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 'http://toutiao.com/group/6620526610116248067/sidebar.js';
import header from 'http://toutiao.com/group/6620526610116248067/header.js';
import BorderLayout from 'http://toutiao.com/group/6620526610116248067/common/BorderLayout.js';
import shelfPane from 'http://toutiao.com/group/6620526610116248067/common/shelfPane.js';
import chartPane from 'http://toutiao.com/group/6620526610116248067/common/chartPane.js';
import graph3dView from 'http://toutiao.com/group/6620526610116248067/3d/index';
我們將頁面上的每個部分分開來放在不同的 js 文件中,就是上面加載的 js export 的部分,根層容器 BorderLayout(整體最外層的 div),整張圖上的部分都是基于 borderLayout 的。
最外層容器 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 'http://toutiao.com/group/6620526610116248067/common/TreeHoverBackgroundDrawable.js';
import ProgressBarSelectBarDrawable from 'http://toutiao.com/group/6620526610116248067/common/ProgressBarSelectBarDrawable.js';
import controller from 'http://toutiao.com/group/controller/sidebar.js';
HT 封裝了一個 ht.ui.VBoxLayout 函數,用來將子組件放置在同一垂直列中,我們可以將左側欄要顯示的部分都放到這個組件中,這樣所有的部分都是以垂直列排布:
let vBoxLayout = new ht.ui.VBoxLayout();//此布局器將子組件放置在同一垂直列中;
vBoxLayout.setBackground('#17191a');
頂部 logo 是根據在 Label 標簽上添加 icon 的方法來實現的,并將這個 topLabel 添加進垂直列 vBoxLayout 中:
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 中:
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 :
export default TreeHoverBackgroundDrawable;
HT 還封裝了非常好用的 ht.ui.ProgressBar 組件,可直接繪制進度條:
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 為 http://toutiao.com/group/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頁面
}
});
直接用的分割組件 ht.ui.SplitLayout 進行分割布局:
let splitLayout = new ht.ui.SplitLayout();//此布局器將自身空間劃分為上、下兩個區域或左、右兩個區域,每個區域可以放置一個子組件
splitLayout.setSplitterVisible(false);
splitLayout.setPositionType('absoluteFirst');
splitLayout.setOrientation('v');
這個 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 'http://toutiao.com/group/6620526610116248067/3d/index';
從這個 3d 場景中可以看到,我們需要“地板”、“墻面”、“貨架”、“叉車”、“貨物”以及 3d 場景。
在 3d 文件夾下的 index.js 中,我們從文件夾中導入所有需要的接口:
import{//這里導入的都是一些基礎數據
sceneWidth, sceneHeight, sceneTall,
toShelfList, randomCargoType
}from 'http://toutiao.com/group/6620526610116248067/G.js';
// 模擬數據接口
import{
stockinout,// 出入庫
initiate,// 初始化
inoutShelf// 上下架
}from 'http://toutiao.com/group/6620526610116248067/interfaces';
import{ Shelf }from 'http://toutiao.com/group/6620526610116248067/shelf';//貨架
import{ Floor }from 'http://toutiao.com/group/6620526610116248067/floor';//地板
import{ Wall }from 'http://toutiao.com/group/6620526610116248067/wall';//墻面
import{ Car }from 'http://toutiao.com/group/6620526610116248067/car';//叉車
import{ g3d }from 'http://toutiao.com/group/6620526610116248067/g3d';//3d場景
import{ getCargoById }from 'http://toutiao.com/group/6620526610116248067/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 = 'http://toutiao.com/group/6620526610116248067/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 'http://toutiao.com/group/6620526610116248067/pallet';
import{
cargoTypes,
loadObj,
globalOpacity
}from 'http://toutiao.com/group/6620526610116248067/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 'http://toutiao.com/group/6620526610116248067/common/shelfPane.js';
shelfPane 是基于 Pane 類的,在 shelfPane.js 文件中引入這個類和事件派發器:
import Pane from 'http://toutiao.com/group/6620526610116248067/Pane.js';
import eventbus from 'http://toutiao.com/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 'http://toutiao.com/group/6620526610116248067/common/chartPane.js';
chartPane 和 shelfPane 類似,都是 Pane 類的對象,屬性也類似,不同的是內容。因為今天展示的只是一個 Demo,我們并沒有做過多的關于圖表插件的處理,所以這里就用圖片來代替動態圖表,不過就算想做也是很輕易的事,HT 運用第三方插件也是很輕易上手的,可以看這邊的例子 http://hightopo.com/demo/large-screen/index.html,HT 官網上有更多有趣的例子!
回到正題,chartPane 圖表面板的實現非常輕易,將內部的子組件設置背景圖片再添加進 chartPane 圖表面板中即可:
import Pane from 'http://toutiao.com/group/6620526610116248067/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 官網上自習查閱資料,好好品味,一定會發現更大的世界。
盛世云倉專注于電商平臺包裹后期配送和代收貨款以及倉儲物流外包業務,已累計服務2000家客戶,是一家集倉儲托管、代打包、代發貨、代收貨款、倉儲外包、倉庫托管、個性化增值服務的綜合性倉儲物流企業。
歡迎來電垂詢:400-998-0273,官方網站:http://www.tjbter.com/。
注:如發現內容存在版權問題,煩請提供相關信息并聯系盛世云倉,我們將及時溝通與處理。本站內容部分采集于網絡,涉及言論、版權與本站無關,謝謝。