前言

通用电气(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);elselp = 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 对象,这个对象是在 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 为 ../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.htmlwindow.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 场景

从 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) => {// 监听事件 addInteractorListenerconst 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.jsconst 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);

货架

从 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);// 设置标签是否可拖拽调整位置,默认为 truethis.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;}
});

图表

从 src\view\common 文件夹中的 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

总结

一直是世界“制造工厂”的中国制造业,面临着前所未有的挑战,一方面贸易战后中国会更多地进口,会加大对世界的开放,更多“特斯拉”会进入中国,给本土制造业带来威胁;另一方面,中国制造一直面临的产能和外贸过剩问题也需要解决,抓住国内消费升级的趋势,走出口转内销的路就成为一个必然选择,要走好这条路同样离不开智造智造。

智能制造的兴起、贸易环境的变局,让中国制造业转型升级成为燃眉之急。

基于 HTML5 的工业互联网 3D 展示方案相关推荐

  1. 鸿蒙工业互联网,工业互联网 3D 展示平台

    原标题:工业互联网 3D 展示平台 项目简介 产业园区占地约为 158.46 亩,现有生产车间两栋.研发楼一栋.检测楼一栋.食堂及倒班楼一栋.废品库一栋.门卫室两处.综合站房一处.主要从事电缆.电线的 ...

  2. 可视化的工业互联网 3D 展示

    前言 通用电气(GE).IBM.英特尔等公司主推的"工业互联网"正在经历"产品-数据分析平台-应用-生态"的演进.这主要得益于 Predix 数据分析平台对工业 ...

  3. 基于 HTML5 的工业互联网云平台监控机房 U 位

    前言 机柜 U 位管理是一项突破性创新技术--继承了 RFID 标签(电子标签)的优点的同时,完全解决了 RFID 技术(非接触式的自动识别技术)在机房 U 位资产监控场应用景中的四大缺陷,采用工业互 ...

  4. 基于 HTML5 结合工业互联网的智能飞机控制

    前言 从互联网+的概念一出来,就瞬间吸引了各行各业的能人志士,想要在这个领域分上一杯羹.现在传统工业生产行业运用互联网+的概念偏多,但是在大众创业万众创新的背景下,"互联网+"涌出 ...

  5. 基于 HTML5 + WebGL 的宇宙 3D 展示系统

    前言 近年来随着引力波的发现.黑洞照片的拍摄.火星上存在水的证据发现等科学上的突破,以及文学影视作品中诸如<三体>.<流浪地球>.<星际穿越>等的传播普及,宇宙空间 ...

  6. 基于 HTML5 WebGL 的加油站 3D 可视化监控

    前言 随着数字化,工业互联网,物联网的发展,我国加油站正向有人值守,无人操作,远程控制的方向发展,传统的人工巡查方式逐渐转变为以自动化控制为主的在线监控方式,即采用数据采集与监控系统 SCADA.SC ...

  7. 本弗莱数据可视化的生产流程图_工业互联网 | 3D组态|图扑软件|数据可视化|blog...

    Home » Posts tagged "工业互联网" 2020年12月,国新办发布<中国交通的可持续发展>白皮书.从"走得了"到"走得好 ...

  8. 基于 HTML5 WebGL + WebVR 的 3D 虚实现实可视化培训系统

    前言 2019 年 VR, AR, XR, 5G, 工业互联网等名词频繁出现在我们的视野中,信息的分享与虚实的结合已经成为大势所趋,5G 是新一代信息通信技术升级的重要方向,工业互联网是制造业转型升级 ...

  9. 基于 HTML5 的 WebGL 自定义 3D 摄像头监控模型

    2019独角兽企业重金招聘Python工程师标准>>> 前言 随着视频监控联网系统的不断普及和发展, 网络摄像机更多的应用于监控系统中,尤其是高清时代的来临,更加快了网络摄像机的发展 ...

最新文章

  1. java怎么添加地图_javaweb怎样添加百度地图
  2. group plot simplest approach in matlab
  3. oracle 不等于某类,Oracle如何查询不等于某数值
  4. VS2015 vc++ 项目出现new.h找不到的错误
  5. 如何打造应对超大流量的高性能负载均衡?
  6. 麦克纳姆轮运动特性分析
  7. php中的三元运算符
  8. linux网络图标在哪,如何在Linux中设置快捷方式图标
  9. [转载] Python数学实验与建模 课后习题第1章解析
  10. P1020 导弹拦截 dp 树状数组维护最长升序列
  11. Elasticserch学习之分页
  12. 解决学校断网,突破天翼校园,实现共享wifi以及linux下无天翼校园客户端
  13. OpenGL超级宝典 绘制第一个三角形
  14. 【详细】小程序模板使用教程
  15. 产品心理学:福格行为模型详解与应用
  16. 怎样限制Word文档被复制粘贴?word限制编辑的使用技巧
  17. 使用puppet自动化升级安全程序
  18. 如何查看数据库中表的前5行,3-8行,随机3行记录?
  19. scratch——这个电路仿真工具真不错
  20. 互联网时代各行业都在快速更替,金融行业为什么即将成为下一个风口?

热门文章

  1. 设置QWidget及其子类控件背景颜色
  2. linux查看端口有多少连接,Linux查看端口的连接数
  3. GUI自动化:robot framework环境搭建和RIDE工具和sublime text3
  4. abp merge rules
  5. esp32开发板学习
  6. 接口测试常用工具(转)
  7. 洛河98计算机学校王艳,小学语文课堂导入设计研究实施方案
  8. 电信宽带没有路由器也能上无线网
  9. 关于电脑开机一直press del to enter bios setup menu,oress F12 to display boot menu,无法开机,按del键也没有任何反应的解决办法
  10. python 爬虫 伪装浏览器_python爬虫之浏览器伪装设置