前言
通用電氣(GE)、IBM、英特爾等公司主推的“工業網際網路”正在經歷“產品-資料分析平臺-應用-生態”的演進。這主要得益於 Predix 資料分析平臺對工業網際網路應用的整合能力。Predix 就像工業資料領域的 iOS 或者安卓系統一樣,能夠讓工程師自己建立模型和應用,打通前方數以萬計的感測器和後方每天增加超過 5000 萬條的資料庫。
在實際應用中,東方航空公司在 Predix 上使用工業網際網路應用搜集了 500 多臺 CFM56 發動機的高壓渦輪葉片保修資料,結合遠端診斷紀錄和第三方資料,建立了葉片損傷分析預測模型。從前,航空公司需要定期強制飛機“休病假”,把微型攝像頭伸入發動機內進行檢查。現在,只要根據資料分析平臺上的結果就可以預測發動機的執行情況,定製科學的重複檢查間隔,提升運營效率。除去航空領域,工廠倉庫的監管也是非常需要網際網路的介入,不僅能夠實時監控倉庫當前的資料和資訊,還能夠降低倉庫監管人員的數量,更能夠預測倉庫故障資訊並提前告知工作人員採取對應的措施,能夠有效地避免工廠運營暫停導致的損失。
http://www.hightopo.com/demo/warehouse/index.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 的。
最外層容器 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 中:
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 物件,這個物件是在 srcviewcommon 下的 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 物件,這個物件在 srcviewcommon 下的 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。
3D 場景
從 srcview3d 資料夾中的 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`;
複製程式碼
在 srcview3dcargo.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 這四個類都準備完畢,只需要在 srcview3dindex.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);
複製程式碼
貨架
從 srcviewcommon 資料夾中的 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;
}
});
複製程式碼
圖表
從 srcviewcommon 資料夾中的 chartPane.js 中獲取 graph3dView 的外部介面被 src/view 中的 index.js 呼叫:
import chartPane from `./common/chartPane.js`;
複製程式碼
chartPane 和 shelfPane 類似,都是 Pane 類的物件,屬性也類似,不同的是內容。因為今天展示的只是一個 Demo,我們並沒有做過多的關於圖表外掛的處理,所以這裡就用圖片來代替動態圖表,不過就算想做也是很容易的事,HT 官網上有更多有趣的例子!
回到正題,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 官網(http://www.hightopo.com/)上自習查閱資料,好好品味,一定會發現更大的世界。
http://www.hightopo.com/demo/large-screen/index.html
總結
一直是世界“製造工廠”的中國製造業,面臨著前所未有的挑戰,一方面貿易戰後中國會更多地進口,會加大對世界的開放,更多“特斯拉”會進入中國,給本土製造業帶來威脅;另一方面,中國製造一直面臨的產能和外貿過剩問題也需要解決,抓住國內消費升級的趨勢,走出口轉內銷的路就成為一個必然選擇,要走好這條路同樣離不開智造智造。
智慧製造的興起、貿易環境的變局,讓中國製造業轉型升級成為燃眉之急。