前言

通用电气(GE)、IBM、英特尔等公司主推的“工业互联网”正在经历“产品-数据分析平台-应用-生态”的演进。这主要得益于 Predix 数据分析平台对工业互联网应用的整合能力。Predix 就像工业数据领域的 iOS 或者安卓系统一样,能够让工程师自己建立模型和应用,打通前方数以万计的传感器和后方每天增加超过 5000 万条的数据库。

在实际应用中,东方航空公司在 Predix 上使用工业互联网应用搜集了 500 多台 CFM56 发动机的高压涡轮叶片保修数据,结合远程诊断纪录和第三方数据,建立了叶片损伤分析预测模型。从前,航空公司需要定期强制飞机“休病假”,把微型摄像头伸入发动机内进行检查。现在,只要根据数据分析平台上的结果就可以预测发动机的运行情况,定制科学的重复检查间隔,提升运营效率。除去航空领域,工厂仓库的监管也是非常需要互联网的介入,不仅能够实时监控仓库当前的数据和信息,还能够降低仓库监管人员的数量,更能够预测仓库故障信息并提前告知工作人员采取对应的措施,能够有效地避免工厂运营暂停导致的损失。

代码生成

这个例子是采用 es6 的模块化的方式部署的。打开 index.html 进入 lib/index.js,源码是在 src 文件夹中,我们直接进 src/view 下的 index.js 
 在顶部加载其他模块中含有 export 接口的模块:
  1. import sidebar from './sidebar.js';
  2. import header from './header.js';
  3. import BorderLayout from './common/BorderLayout.js';
  4. import shelfPane from './common/shelfPane.js';
  5. import chartPane from './common/chartPane.js';
  6. 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 两个方法:
  1. let BorderLayout = function() {
  2. BorderLayout.superClass.constructor.call(this);
  3. this.setContinuous(true);
  4. this.setSplitterSize(0);
  5. };
  6.  
  7. ht.Default.def(BorderLayout, ht.ui.BorderLayout, {// 自定义类
  8. /**
  9. * splitter 宽度都为 0,为了能正常交互,重写此函数将 splitterRect 的宽度修改为 10
  10. * @override
  11. */
  12. getSplitterAt: function (event) {// 获取事件对象下分隔条所在的区域
  13. var leftRect = this._leftSplitterRect, lp;
  14. if (leftRect) {
  15. leftRect = ht.Default.clone(leftRect);
  16. leftRect.width = 10;
  17. leftRect.x -= 5;
  18. if (event instanceof Event)
  19. lp = this.lp(event);
  20. else
  21. lp = event;
  22. if (ht.Default.containsPoint(leftRect, lp)) return 'left';
  23. }
  24. return BorderLayout.superClass.getSplitterAt.call(this, event);
  25. },
  26. /**
  27. * 调整左侧 splitterCanvas 的尺寸,以便挡住子组件
  28. * @override
  29. */
  30. layoutSplitterCanvas: function(canvas, x, y, width, height, region) {
  31. if (region === 'left') {
  32. canvas.style.pointerEvents = '';
  33. canvas.style.display = 'block';
  34. ht.Default.setCanvas(canvas, 10, height);
  35. canvas.style.left = this.getContentLeft() + this.tx() + x - 5 + 'px';
  36. canvas.style.top = this.getContentTop() + this.ty() + y + 'px';
  37. }
  38. else {
  39. BorderLayout.superClass.layoutSplitterCanvas.call(this, canvas, x, y, width, height, region);
  40. }
  41. }
  42. });
  43. 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 变量:

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

HT 封装了一个 ht.ui.VBoxLayout 函数,用来将子组件放置在同一垂直列中,我们可以将左侧栏要显示的部分都放到这个组件中,这样所有的部分都是以垂直列排布:

  1. let vBoxLayout = new ht.ui.VBoxLayout();// 此布局器将子组件放置在同一垂直列中;
  2. vBoxLayout.setBackground('#17191a');

顶部 logo 是根据在 Label 标签上添加 icon 的方法来实现的,并将这个 topLabel 添加进垂直列 vBoxLayout 中:

  1. let topLabel = new ht.ui.Label(); // 标签组件
  2. topLabel.setText('Demo-logo');// 设置文字内容
  3. topLabel.setIcon('imgs/logo.json');// 设置图标,可以是颜色或者图片等
  4. topLabel.setIconWidth(41);
  5. topLabel.setIconHeight(37);
  6. topLabel.setTextFont('18px arial, sans-serif');
  7. topLabel.setTextColor('#fff');
  8. topLabel.setPreferredSize(1, 64);// 组件自身最合适的尺寸
  9. topLabel.setBackground('rgb(49,98,232)');
  10. vBoxLayout.addView(topLabel, {// 将子组件加到容器中
  11. width: 'match_parent'// 填满父容器
  12. });

对于“货位统计表格”,我们采用的是 HT 封装的 TreeTableView 组件,以树和表格的组合方式呈现 DataModel 中数据元素属性及父子关系,并将这个“树表”添加进垂直列 vBoxLayout 中:

  1. let shelfTreeTable = new ht.ui.TreeTableView();// 树表组件,以树和表格的组合方式呈现 DataModel 中数据元素属性及父子关系
  2. shelfTreeTable.setHoverBackgroundDrawable(new TreeHoverBackgroundDrawable('#1ceddf', 2));// 设置 hover 状态下行选中背景的 Drawable 对象
  3. shelfTreeTable.setSelectBackgroundDrawable(new TreeHoverBackgroundDrawable('#1ceddf', 2));// 设置行选中背景的 Drawable 对象 参数为“背景
  4. shelfTreeTable.setBackground(null);
  5. shelfTreeTable.setIndent(20);// 设置不同层次的缩进值
  6. shelfTreeTable.setColumnLineVisible(false);// 设置列线是否可见
  7. shelfTreeTable.setRowLineVisible(false);
  8. shelfTreeTable.setExpandIcon('imgs/expand.json');// 设置展开图标图标,可以是颜色或者图片等
  9. shelfTreeTable.setCollapseIcon('imgs/collapse.json');// 设置合并图标图标,可以是颜色或者图片等
  10. shelfTreeTable.setPreferredSizeRowCountLimit();// 设置计算 preferredSize 时要限制的数据行数
  11. shelfTreeTable.setId('shelfTreeTable');
  12. vBoxLayout.addView(shelfTreeTable, {
  13. width: 'match_parent',
  14. height: 'wrap_content',// 组件自身首选高度
  15. marginTop: 24,
  16. marginLeft: 4,
  17. marginRight: 4
  18. });

我们在设置“行选中”时背景传入了一个 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 中新添加的属性:

  1. let TreeHoverBackgroundDrawable = function(color, width) {
  2. TreeHoverBackgroundDrawable.superClass.constructor.call(this);
  3. this.setColor(color);
  4. this.setWidth(width);
  5. };
  6. ht.Default.def(TreeHoverBackgroundDrawable, ht.ui.drawable.Drawable, {
  7. ms_ac: ['color', 'width'],
  8. draw: function(x, y, width, height, data, view, dom) {
  9. var self = this,
  10. g = view.getRootContext(dom),
  11. color = self.getColor();
  12.  
  13. g.beginPath();
  14. g.fillStyle = color;
  15. g.rect(x, y, self.getWidth(), height);
  16. g.fill();
  17. },
  18. getSerializableProperties: function() {
  19. var parentProperties = TreeHoverBackgroundDrawable.superClass.getSerializableProperties.call(this);
  20. return addMethod(parentProperties, {
  21. color: 1, width: 1
  22. });
  23. }
  24. });

记住要导出 TreeHoverBackgroundDrawable :

  1. export default TreeHoverBackgroundDrawable;

HT 还封装了非常好用的 ht.ui.ProgressBar 组件,可直接绘制进度条:

  1. let progressBar = new ht.ui.ProgressBar();
  2. progressBar.setId('progressBar');
  3. progressBar.setBackground('#3b2a00');// 设置组件的背景,可以是颜色或者图片等
  4. progressBar.setBar('rgba(0,0,0,0)');// 设置进度条背景,可以是颜色或者图片等
  5. progressBar.setPadding(5);
  6. progressBar.setSelectBarDrawable(new ProgressBarSelectBarDrawable('#c58348', '#ffa866')); // 设置前景(即进度覆盖区域)的 Drawable 对象,可以是颜色或者图片等
  7. progressBar.setValue(40);// 设置当前进度值
  8. progressBar.setBorderRadius(0);
  9. vBoxLayout.addView(progressBar, {
  10. marginTop: 24,
  11. width: 'match_parent',
  12. height: 28,
  13. marginBottom: 24,
  14. marginLeft: 14,
  15. marginRight: 14
  16. });

我们在 设置“前景”的时候传入了一个 ProgressBarSelectBarDrawable 对象,这个对象在 src\view\common 下的 ProgressBarSelectBarDrawable.js 中定义的。具体定义方法跟上面的 TreeHoverBackgroundDrawable 函数对象类似,这里不再赘述。

分割线的制作最为简单,只要将一个矩形的高度设置为 1 即可,我们用 ht.ui.View() 组件来制作:

  1. let separator = new ht.ui.View();// 所有视图组件的基类,所有可视化组件都必须从此类继承
  2. separator.setBackground('#666');
  3. vBoxLayout.addView(separator, {
  4. width: 'match_parent',
  5. height: 1,
  6. marginLeft: 14,
  7. marginRight: 14,
  8. marginBottom: 24
  9. });

货物表格的操作几乎和货位统计表格相同,这里不再赘述。

我们将一个 json 的图表文件当做图片传给图表的组件容器作为背景,也能很轻松地操作:

  1. let chartView = new ht.ui.View();
  2. chartView.setBackground('imgs/chart.json');
  3. vBoxLayout.addView(chartView, {
  4. width: 173,
  5. height: 179,
  6. align: 'center',
  7. marginBottom: 10
  8. });

管理组和顶部 logo 的定义方式类似,这里不再赘述。

问题反馈按钮,我们将这个部分用 HT 封装的 ht.ui.Button 组件来制作,并将这个部分添加进垂直列 vBoxLayout 中:

  1. let feedbackButton = new ht.ui.Button();// 按钮类
  2. feedbackButton.setId('feedbackButton');
  3. feedbackButton.setText('问题反馈:service@hightopo.com');
  4. feedbackButton.setIcon('imgs/em.json');
  5. feedbackButton.setTextColor('#fff');
  6. feedbackButton.setHoverTextColor(shelfTreeTable.getHoverLabelColor());// 设置 hover 状态下文字颜色
  7. feedbackButton.setActiveTextColor(feedbackButton.getHoverTextColor());// 设置 active 状态下文字颜色
  8. feedbackButton.setIconWidth(16);
  9. feedbackButton.setIconHeight(16);
  10. feedbackButton.setIconTextGap(10);
  11. feedbackButton.setAlign('left');
  12. feedbackButton.setBackground(null);
  13. feedbackButton.setHoverBackground(null);
  14. feedbackButton.setActiveBackground(null);
  15. vBoxLayout.addView(feedbackButton, {
  16. width: 'match_parent',
  17. marginTop: 5,
  18. marginBottom: 10,
  19. marginLeft: 20
  20. });

交互

视图部分做好了,在模块化开发中,controller 就是做交互的部分,shelfTreeTable 货位统计表格, cargoTreeTable 货物表格, feedbackButton 问题反馈按钮, progressBar  进度条四个部分的交互都是在在 src/controller 下的 sidebar.js 中定义的。通过 findViewById(id, recursive) 根据id查找子组件,recursive 表示是否递归查找。

shelfTreeTable 货位统计表格的数据绑定传输方式与 cargoTreeTable 货物表格类似,这里我们只对 shelfTreeTable 货位统计表格的数据绑定进行解析。shelfTreeTable 一共有三列,其中不同的部分只有“已用”和“剩余”两个部分,所以我们只要将这两个部分进行数据绑定即可,先创建两列:

  1. let column = new ht.ui.Column();// 列数据,用于定义表格组件的列信息
  2. column.setName('used');// 设置数据元素名称
  3. column.setAccessType('attr');// 在这里 name 为 used,采用 getAttr('used') 和 setAttr('used', 98) 的方式存取 set/getAttr 简写为 a
  4. column.setWidth(65);
  5. column.setAlign('center');
  6. columnModel.add(column);
  7.  
  8. column = new ht.ui.Column();
  9. column.setName('remain');
  10. column.setAccessType('attr');
  11. column.setWidth(65);
  12. column.setAlign('center');
  13. columnModel.add(column);

接着遍历 json 文件,将 json 文件中对应的 used、remain以及 labelColors 通过 set/getAttr 或 简写 a 的方式进行数据绑定:

  1. for (var i = 0; i < json.length; i++) {
  2. var row = json[i];// 获取 json 中的属性
  3. var data = new ht.Data();
  4. data.setIcon(row.icon);// 将 json 中的 icon 传过来
  5. data.setName(row.name);
  6. data.a('used', row.used);
  7. data.a('remain', row.remain);
  8. data.a('labelColors', row.colors);
  9. data.setIcon(row.icon);
  10. treeTable.dm().add(data);// 在树表组件的数据模型中添加这个 data 节点
  11. var children = row.children;
  12. if (children) {
  13. for (var j = 0; j < children.length; j++) {
  14. var child = children[j];
  15. var childData = new ht.Data();
  16. childData.setName(child.name);
  17. childData.setIcon(child.icon);
  18. childData.a('used', child.used);
  19. childData.a('remain', child.remain);
  20. childData.a('labelColors', child.colors);
  21. childData.setParent(data);
  22. treeTable.dm().add(childData);
  23. }
  24. }
  25. }

最后在 controller 函数对象中调用 这个函数:

  1. initTreeTableDatas(shelfTreeTable, json);// json 为 ../model/shelf.json 传入

progressBar 进度条的变化是通过设置定时器改变 progressBar 的 value 值来动态改变的:

  1. setInterval(() => {
  2. if (progressBar.getValue() >= 100) {
  3. progressBar.setValue(0);
  4. }
  5. progressBar.setValue(progressBar.getValue() + 1);
  6. }, 50);

feedbackButton 问题反馈按钮,通过增加 View 事件监听器来监听按钮的点击事件:

  1. feedbackButton.addViewListener(e => {
  2. if (e.kind === 'click') {// HT 自定义的事件属性,具体查看 http://hightopo.com/guide/guide/core/beginners/ht-beginners-guide.html
  3. window.location.href = "mailto:service@www.hightopo.com";// 当前页面打开URL页面
  4. }
  5. });

右侧容器splitLayout

直接用的分割组件 ht.ui.SplitLayout 进行分割布局:

  1. let splitLayout = new ht.ui.SplitLayout();// 此布局器将自身空间划分为上、下两个区域或左、右两个区域,每个区域可以放置一个子组件
  2. splitLayout.setSplitterVisible(false);
  3. splitLayout.setPositionType('absoluteFirst');
  4. splitLayout.setOrientation('v');
  • 右侧头部 header

这个 header 是从 src/view 下的 header.js 中获取的对象,为 ht.ui.RelativeLayout 相对定位布局器,分为 5 个部分:searchField 搜索框、titleLabel 主标题、temperatureLabel1 温度、humidityLabel1 湿度以及 airpressureLabel1 气压。

这里我们没有对“搜索框” searchField 进行数据绑定,以及搜索的功能,这只是一个样例,不涉及业务部分:

  1. let searchField = new ht.ui.TextField();// 文本框组件
  2. searchField.setBorder(new ht.ui.border.LineBorder(1, '#d8d8d8'));// 在组件的画布上绘制直线边框
  3. searchField.setBorderRadius(12);
  4. searchField.setBackground(null);
  5. searchField.setIcon('imgs/search.json');
  6. searchField.setIconPosition('left');
  7. searchField.setPadding([2, 16, 2, 16]);
  8. searchField.setColor('rgb(138, 138, 138)');
  9. searchField.setPlaceholder('Find everything...');
  10. searchField.getView().className = 'search';
  11. header.addView(searchField, {
  12. width: 180,
  13. marginLeft: 20,
  14. vAlign: 'middle'
  15. });

对于 titleLabel 主标题比较简单,和温度、湿度以及气压类似,我就只说明一下主标题 titleLabel 的定义:

  1. let titleLabel = new ht.ui.Label();// 标签组件
  2. titleLabel.setId('title');
  3. titleLabel.setIcon('imgs/expand.json');
  4. titleLabel.setTextColor('rgb(138, 138, 138)');
  5. titleLabel.setText('杭州仓库');
  6. titleLabel.setHTextPosition('left');// 设置文字在水平方向相对于图标的位置,默认值为 'right'
  7. titleLabel.setIconTextGap(10);// 设置图标和文字之间的间距
  8. titleLabel.setBorder(new ht.ui.border.IndividualLineBorder(0, 0, 3, 0, '#3162e8'))// 在组件的画布上绘制直线边框;与 LineBorder 不同的是,此边框可以单独绘制某一个或几个方向的边框
  9. titleLabel.setTextFont('16px arial');
  10.  
  11. header.addView(titleLabel, {
  12. height: 'match_parent',
  13. width: 'wrap_content',
  14. align: 'center'
  15. });

然后交互部分在 src/controller 下的 header.js 中做了右键点击出现菜单栏以及单击 titleLabel 的位置出现下拉菜单两种交互,通过控制鼠标的点击事件来控制事件的交互:

  1. let title, contextMenu;
  2. export default function controller (view) {
  3. title = view.findViewById('title');
  4.  
  5. contextMenu = new ht.ui.ContextMenu();// 右键菜单组件
  6. contextMenu.setLabelColor('rgb(138, 138, 138)');
  7. contextMenu.setHoverRowBackground('#3664e4');
  8. contextMenu.setItems([
  9. {
  10. label: '北京仓库'
  11. },
  12. {
  13. label: '上海仓库'
  14. },
  15. {
  16. label: '厦门仓库'
  17. }
  18. ]);
  19.  
  20. contextMenu.addViewListener((e) => {
  21. if (e.kind === 'action') {// HT 自定义的事件类型
  22. title.setText(e.item.label);
  23. }
  24. });
  25.  
  26. title.getView().addEventListener('mousedown', e => {
  27. if (contextMenu.isInDOM()) {// 判断组件是否在 DOM 树中
  28. contextMenu.hide();// 隐藏菜单
  29. document.removeEventListener('mousedown', handleWindowClick);// 移除mousedown监听事件
  30. }
  31. else {// 没有右键点击过
  32. var items = contextMenu.getItems();
  33. for (var i = 0; i < items.length; i++) {
  34. items[i].width = title.getWidth();
  35. }
  36.  
  37. let windowInfo = ht.Default.getWindowInfo(),// 获取当前窗口left|top|width|height的参数信息
  38. titleRect = title.getView().getBoundingClientRect();
  39. contextMenu.show(windowInfo.left + titleRect.left, windowInfo.top + titleRect.top + titleRect.height);
  40.  
  41. document.addEventListener('mousedown', handleWindowClick);
  42. }
  43. });
  44. }
  45.  
  46. function handleWindowClick(e) {
  47. if (!contextMenu.getView().contains(e.target) && !title.getView().contains(e.target)) {// 判断元素是否在数组中
  48. contextMenu.hide();
  49. document.removeEventListener('mousedown', handleWindowClick);
  50. }
  51. }
  • 右侧下部分 RelativeLayout 相对布局器(相对于右侧下部分最根层 div),包含中间显示 3d 部分 graph3dView、双击货柜或货物才会出现的 shelfPane、以及出现在右下角的图表 chartPane,将这三部分添加进 RelativeLayout 相对布局容器:

  1. let relativeLayout = new ht.ui.RelativeLayout();// 创建相对布局器
  2. relativeLayout.setId('contentRelative');
  3. relativeLayout.setBackground('#060811');
  4.  
  5. var htView = new ht.ui.HTView(graph3dView);
  6. htView.setId('contentHTView');
  7. relativeLayout.addView(htView, {// 将 3d 组件添加进relativeLayout 相对布局器
  8. width: 'match_parent',
  9. height: 'match_parent'
  10. });
  11.  
  12. relativeLayout.addView(shelfPane, {// 将双击出现的详细信息 shelfPane 组件添加进relativeLayout 相对布局器
  13. width: 220,
  14. height: 'wrap_content',
  15. align: 'right',
  16. marginRight: 30,
  17. marginTop: 30
  18. });
  19.  
  20. relativeLayout.addView(chartPane, {// 将图表 chartPane 组件添加进relativeLayout 相对布局器
  21. width: 220,
  22. height: 200,
  23. align: 'right',
  24. vAlign: 'bottom',
  25. marginRight: 30,
  26. marginBottom: 30
  27. })

然后将右侧相对布局器 relativeLayout 和右侧头部 header 添加进右侧底部容器 splitLayout:

  1. let splitLayout = new ht.ui.SplitLayout();
  2. splitLayout.setSplitterVisible(false);
  3. splitLayout.setPositionType('absoluteFirst');
  4. splitLayout.setOrientation('v');
  5. splitLayout.addView(header, {
  6. region: 'first'// 指定组件所在的区域,可选值为:'first'|'second'
  7. });
  8. splitLayout.addView(relativeLayout, {
  9. region: 'second'
  10. });

再将左侧部分的 sidebar 和右侧部分的所有也就是 splitLayout 添加进整个底部容器 borderLayout,再将底部容器添加进 html body 体中:

  1. let borderLayout = new BorderLayout();
  2. borderLayout.setLeftWidth(250);
  3. borderLayout.addView(sidebar, {
  4. region: 'left',// 指定组件所在的区域,可选值为:'top'|'right'|'bottom'|'left'|'center'
  5. width: 'match_parent'// 组件自身首选宽度
  6. });
  7. borderLayout.addView(splitLayout, {
  8. region: 'center'
  9. });
  10.  
  11. borderLayout.addToDOM();// 将 borderLayout 添加进 body 体中

我们具体说说这个相对布局器内部包含的 3d 部分 graph3dView、双击货柜或货物才会出现的 shelfPane、以及出现在右下角的图表 chartPane。

3D 场景

从 src\view\3d 文件夹中的 index.js 中获取 graph3dView 的外部接口被 src/view 中的 index.js 调用:

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

从这个 3d 场景中可以看到,我们需要“地板”、“墙面”、“货架”、“叉车”、“货物”以及 3d 场景。

在 3d 文件夹下的 index.js 中,我们从文件夹中导入所有需要的接口:

  1. import {// 这里导入的都是一些基础数据
  2. sceneWidth, sceneHeight, sceneTall,
  3. toShelfList, randomCargoType
  4. } from './G.js';
  5.  
  6. // 模拟数据接口
  7. import {
  8. stockinout,// 出入库
  9. initiate,// 初始化
  10. inoutShelf// 上下架
  11. } from './interfaces';
  12.  
  13. import { Shelf } from './shelf';// 货架
  14. import { Floor } from './floor';// 地板
  15. import { Wall } from './wall';// 墙面
  16. import { Car } from './car';// 叉车
  17. import { g3d } from './g3d';// 3d场景
  18. import { getCargoById } from './cargo';// 货物

g3d.js 文件中只设置了场景以及对部分事件的监听:

  1. g3d.mi((e) => {// 监听事件 addInteractorListener
  2. const kind = e.kind;
  3. if (kind === 'doubleClickData') {// 双击图元事件
  4. let data = e.data;// 事件相关的数据元素
  5. if (data instanceof Shelf) {// 如果是货架
  6. data.setTransparent(false);
  7.  
  8. eventbus.fire({ type: 'cargoBlur' });// 派发事件,依次调用所有的监听器函数
  9. }
  10. else {
  11. data = data.a('cargo');
  12. if (!data) return;
  13. data.transparent = false;
  14.  
  15. eventbus.fire({ type: 'cargoFocus', data: data });
  16. }
  17.  
  18. for (let i = shelfList.length - 1; i >= 0; i--) {// 除了双击的图元,其他的图元都设置透明
  19. const shelf = shelfList[i];
  20. shelf.setTransparent(true, data);
  21. }
  22. return;
  23. }
  24. if (kind === 'doubleClickBackground') {// 双击背景事件
  25. for (let i = shelfList.length - 1; i >= 0; i--) {// 双击背景,所有的图元都不透明
  26. const shelf = shelfList[i];
  27. shelf.setTransparent(false);
  28. }
  29.  
  30. eventbus.fire({ type: 'cargoBlur' });
  31. return;
  32. }
  33. });

我们在 G.js 中定义了一些基础数据,其他引用的 js 中都会反复调用这些变量,所以我们先来解析这个文件:

  1. const sceneWidth = 1200;// 场景宽度
  2. const sceneHeight = 800;// 场景高度
  3. const sceneTall = 410;// 场景的深度
  4.  
  5. const globalOpacity = 0.3;// 透明度
  6.  
  7. const cargoTypes = {// 货物类型,分为四种
  8. 'cask': {// 木桶
  9. 'name': 'bucket'
  10. },
  11. 'carton': {// 纸箱
  12. 'name': 'carton'
  13. },
  14. 'woodenBox1': {// 木箱1
  15. 'name': 'woodenBox1'
  16. },
  17. 'woodenBox2': {// 木箱2
  18. 'name': 'woodenBox2'
  19. }
  20. };

里面有三个函数,分别是“货架的 obj 分解”、“加载模型”、“随机分配货物的类型”:

  1. function toShelfList(list) {// 将货架的 obj 分解,
  2. const obj = {};
  3. list.forEach((o) => {// 这边的参数o具体内容可以查看 view/3d/interface.js
  4. const strs = o.cubeGeoId.split('-');
  5.  
  6. let rs = obj[o.rackId];
  7. if (!rs) {
  8. rs = obj[o.rackId] = [];
  9. }
  10.  
  11. const ri = parseInt(strs[2].substr(1)) - 1;
  12. let ps = rs[ri];
  13. if (!ps) {
  14. ps = rs[ri] = [];
  15. }
  16.  
  17. let type = 'cask';
  18. if (o.inventoryType === 'Import') {
  19. while((type = randomCargoType()) === 'cask') {}
  20. }
  21.  
  22. const pi = parseInt(strs[3].substr(1)) - 1;
  23. ps[pi] = {
  24. id: o.cubeGeoId,
  25. type: type
  26. };
  27. });
  28.  
  29. return obj;
  30. }
  31.  
  32. function loadObj(shape3d, fileName, cbFunc) {// 加载模型
  33. const path = './objs/' + fileName;
  34. ht.Default.loadObj(path + '.obj', path + '.mtl', {
  35. shape3d: shape3d,
  36. center: true,
  37. cube: true,
  38. finishFunc: cbFunc
  39. });
  40. }
  41.  
  42. function randomCargoType() {// 随机分配“货物”的类型
  43. const keys = Object.keys(cargoTypes);
  44. const i = Math.floor(Math.random() * keys.length);
  45. return keys[i];
  46. }

这个 3d 场景中还有不可缺少的“货物”和“货架”以及“叉车”,三者的定义方式类似,这里只对“货架”进行解释。我们直接在“货物”的 js 中引入底下的“托盘”的 js 文件,将它们看做一个整体:

  1. import { Pallet } from './pallet';
  2. import {
  3. cargoTypes,
  4. loadObj,
  5. globalOpacity
  6. } from './G';

在 src\view\3d\cargo.js 文件中,定义了一个“货物”类,这个类中声明了很多方法,比较基础,有需要的自己可以查看这个文件,这里我不过多解释。主要讲一下如何加载这个“货物”的 obj,我们在 G.js 文件中有定义一个 loadObj 函数,我们在代码顶部也有引入,导入 obj 文件之后就在“货物”的库存增加这个“货物”:

  1. for (let type in cargoTypes) {// 遍历 cargoTypes 数组, G.js 中定义的
  2. const cargo = cargoTypes[type];
  3. loadObj(type, cargo.name, (map, array, s3) => {// loadObj(shape3d, fileName, cbFunc) cbFunc 中的参数可以参考 obj 手册
  4. cargo.s3 = s3;// 将 cargo 的 s3 设置原始大小
  5.  
  6. updateCargoSize();
  7. });
  8. }
  9.  
  10. function updateCargoSize() {
  11. let c, obj;
  12. for (let i = cargoList.length - 1; i >= 0; i--) {
  13. c = cargoList[i];
  14.  
  15. obj = cargoTypes[c.type];
  16. if (!obj.s3) continue;
  17. c.boxS3 = obj.s3;
  18. }
  19. }

还有就是界面上“货物”的进出库的动画,主要用的方法是 HT 封装的 ht.Default.startAnim 函数(HT for Web 动画手册),出的动画与进的动画类似,这里不赘述:

  1. // 货物进
  2. in() {
  3. if (anim) {// 如果有值,就停止动画
  4. anim.stop(true);
  5. }
  6.  
  7. this.x = this.basicX + moveDistance;
  8. this.opacity = 1;
  9.  
  10. anim = ht.Default.startAnim({
  11. duration: 1500,
  12. finishFunc: () => {// 动画结束之后调用这个函数,将anim设置为空停止动画
  13. anim = null;
  14. },
  15. action: (v, t) => {
  16. this.x = this.basicX + (1 - v) * moveDistance;// 改变x坐标,看起来像向前移动
  17. }
  18. });
  19. }

墙和地板也是比较简单的,简单地继承 ht.Node 和 ht.Shape,这里以“墙”进行解释,继承之后直接在构造函数中进行属性的设置即可:

  1. class Wall extends ht.Shape {// 继承 ht.Shape 类
  2. constructor(points, segments, tall, thickness, elevation) {
  3. super();
  4.  
  5. this.setPoints(points);// 设置“点”
  6. this.setSegments(segments);// 设置“点之间的连接方式”
  7. this.setTall(tall);// 控制Node图元在y轴的长度
  8. this.setThickness(thickness);// 设置“厚度”
  9. this.setElevation(elevation);// 控制Node图元中心位置所在3D坐标系的y轴位置
  10.  
  11. this.s({
  12. 'all.transparent': true,// 六面透明
  13. 'all.opacity': 0.3,// 透明度为 0.3
  14. 'all.reverse.flip': true,// 六面的反面显示正面的内容
  15. 'bottom.visible': false// 底面不可见
  16. });
  17. }
  18. }

floor、wall、shelf 以及 car 这四个类都准备完毕,只需要在 src\view\3d\index.js 中 new 一个新的对象并加入到数据模型 dataModel 中即可,这里只展示 car “叉车”的初始化代码:

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

至于“货物”,我们在这个 js 上是采用定时器调用 in 和 out 方法,这里有一个模拟的数据库 interfaces.js 文件,有需求的可以看一下,这里我们只当数据来调用(进出库和上下架类似,这里只展示进出库的设置方法):

  1. // 轮训掉用出入库接口
  2. setInterval(() => {
  3. const obj = stockinout();// 出入库
  4. let type = 'cask';
  5. if (obj.inventoryType === 'Import') {
  6. while((type = randomCargoType()) === 'cask') {}// 如果为“货物”类型为“木桶”
  7. }
  8. car.cargoType = type;
  9. if (obj.inOutStatus === 'I')// 如果值为 “I”,则进库
  10. car.in();
  11. else // 否则为“o”,出库
  12. car.out();
  13. }, 30000);

货架

从 src\view\common 文件夹中的 shelfPane.js 中获取 graph3dView 的外部接口被 src/view 中的 index.js 调用:

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

shelfPane 是基于 Pane 类的,在 shelfPane.js 文件中引入这个类和事件派发器:

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

Pane 类继承于 HT 封装的 ht.ui.TabLayout 类, 并做了一些特定的属性设置:

  1. class Pane extends ht.ui.TabLayout {
  2. constructor() {
  3. super();
  4.  
  5. this.setBorder(new ht.ui.border.LineBorder(1, 'rgb(150,150,150)'));// 设置组件的边框
  6. this.setTabHeaderBackground(null);// 设置标签行背景,可以是颜色或者图片等
  7. this.setHoverTabBackground(null);// 设置 Hover 状态下的标签背景,可以是颜色或者图片等
  8. this.setActiveTabBackground(null);// 设置 Active 状态下的标签背景,可以是颜色或者图片等
  9. this.setTitleColor('rgb(184,184,184)');// 设置正常状态下标签文字的颜色
  10. this.setActiveTitleColor('rgb(255,255,255)');// 设置 Active 状态下标签文字的颜色
  11. this.setTabHeaderLineSize(0);// 设置标签行分割线宽度
  12. this.setMovable(false);// 设置标签是否可拖拽调整位置,默认为 true
  13. this.setTabHeaderBackground('#1c258c');// 设置标签行背景,可以是颜色或者图片等
  14. this.setTabGap(0);// 设置标签之间的距离
  15. }
  16. getTabWidth(child) {// 获取指定子组件的标签宽度
  17. const children = this.getChildren(),
  18. size = children.size();
  19. if (size === 0) {
  20. return this.getContentWidth();// 获取内容宽度,即组件宽度减去边框宽度和左右内边距宽度
  21. }
  22. else {
  23. return this.getContentWidth() / size;
  24. }
  25. }
  26. drawTab(g, child, x, y, w, h) {// 绘制标签
  27. const children = this.getChildren(),// 获取子组件列表
  28. size = children.size(),
  29. color = this.getCurrentTitleColor(child),// 根据参数子组件的状态(normal、hover、active、move),获取标签文字颜色
  30. font = this.getTitleFont(child),// 获取标签文字字体
  31. title = this.getTitle(child);// 获取指定子组件的标签文本
  32. if (size === 1) {
  33. ht.Default.drawText(g, title, font, color, x, y, w, h, 'left');// 绘制文字
  34. }
  35. else {
  36. ht.Default.drawText(g, title, font, color, x, y, w, h, 'center');
  37. }
  38.  
  39. if (children.indexOf(child) < size - 1) {
  40. g.beginPath();// 开始绘制
  41. g.rect(x + w - 1, y + 4, 1, h - 8);
  42. g.fillStyle = 'rgb(150,150,150)';
  43. g.fill();
  44. }
  45. }
  46. show() {
  47. this.setVisible(true);// 设置组件是否可见
  48. }
  49. hide() {
  50. this.setVisible(false);
  51. }
  52. }

我们这个例子中的“信息”列表是一个表格组件,HT 通过 ht.ui.TableLayout 函数定义一个表格,然后通过 ht.ui.TableRow 向表格中添加行,这个例子中的“备注”、“编号”、“来源”、“入库”、“发往”以及“出库”都是文本框,这里拿“备注”作为举例:

  1. let tableLayout = new ht.ui.TableLayout();// 此布局器将自身空间按照行列数划分为 row * column 个单元格
  2. tableLayout.setColumnPreferredWidth(0, 45);// 设置列首选宽度
  3. tableLayout.setColumnWeight(0, 0);// 设置列宽度权重;如果布局器的总宽度大于所有列的首选宽度之和,那么剩余的宽度就根据权重分配
  4. tableLayout.setColumnPreferredWidth(1, 150);
  5. tableLayout.setPadding(8);// 设置组件内边距,参数如果是数字,说明四边使用相同的内边距;如果是数组,则格式为:[上边距, 右边距, 下边距, 左边距]
  6.  
  7. // 备注
  8. var tableRow1 = new ht.ui.TableRow();// TableLayout 中的一行子组件;
  9. var label = new ht.ui.Label();// 标签组件
  10. label.setText('备注');// 设置文字内容
  11. label.setAlign('left');// 设置文字和图标在按钮水平方向的整体对齐方式,默认为 'center'
  12. label.setTextColor('rgb(255,255,255)');// 设置文字颜色
  13.  
  14. var textField = new ht.ui.TextField();// 文本框组件
  15. textField.setFormDataName('remark');// 设置组件在表单中的名称
  16. textField.setBackground(null);// 设置组件的背景,可以是颜色或者图片等;此值最终会被转换为 Drawable 对象
  17. textField.setBorderRadius(0);// 设置 CSS 边框圆角
  18. textField.setColor('rgb(138,138,138)');// 设置文字颜色
  19. textField.setPlaceholder('无');// 设置输入提示
  20. textField.setBorder(new ht.ui.border.IndividualLineBorder(0, 0, 1, 0, 'rgb(138,138,138)'));// 设置组件的边框
  21.  
  22. tableRow1.addView(label);// 添加子组件
  23. tableRow1.addView(textField);
  24.  
  25. tableLayout.addView(tableRow1);// 将子组件加到容器中

“归类”和“模型”类似,都是下拉框,我们用 HT 封装的 ht.ui.ComboBox 组合框组件,跟 ht.ui.TextField 也是异曲同工,只是具体操作不同而已,HT 这样做使用上更简便更容易上手,这里我们以“模型”进行解析,在设置“下拉数据”的时候我们利用了 HT 中的数据绑定:

  1. // 模型
  2. var tableRow4 = new ht.ui.TableRow();
  3. label = new ht.ui.Label();
  4. label.setText('模型');
  5. label.setAlign('left');
  6. label.setTextColor('rgb(255,255,255)');
  7.  
  8. var comboBox = new ht.ui.ComboBox();
  9. comboBox.setFormDataName('model');
  10. comboBox.setBackground(null);
  11. comboBox.setColor('rgb(232,143,49)');
  12. comboBox.setDatas([// 设置下拉数据数组
  13. { label: '纸箱', value: 'carton' },
  14. { label: '木箱1', value: 'woodenBox1' },
  15. { label: '木箱2', value: 'woodenBox2' },
  16. { label: '木桶', value: 'cask' }
  17. ]);
  18. comboBox.setIcon('imgs/combobox_icon.json');
  19. comboBox.setHoverIcon('imgs/combobox_icon_hover.json');
  20. comboBox.setActiveIcon('imgs/combobox_icon_hover.json');
  21. comboBox.setBorderRadius(0);// 设置 CSS 边框圆角
  22. comboBox.setBorder(new ht.ui.border.IndividualLineBorder(0, 0, 1, 0, 'rgb(138,138,138)'));
  23.  
  24. tableRow4.addView(label);
  25. tableRow4.addView(comboBox);
  26.  
  27. tableLayout.addView(tableRow4);

最后一个“染色”,HT 封装了 ht.ui.ColorPicker 颜色选择器组件,组件从 ht.ui.ComboBox 继承并使用 ht.ui.ColorDropDown 作为下拉模板,跟上面的下拉列表很类似,只是下拉的模板变了而已:

  1. // 染色
  2. var tableRow9 = new ht.ui.TableRow();
  3. label = new ht.ui.Label();
  4. label.setText('染色');
  5. label.setAlign('left');
  6. label.setTextColor('rgb(255,255,255)');
  7.  
  8. var comboBox = new ht.ui.ColorPicker();// 颜色选择器组件
  9. comboBox.setFormDataName('blend');// 设置组件在表单中的名称
  10. comboBox.getView().className = 'content_colorpicker';
  11. comboBox.setBackground(null);
  12. comboBox.setPreviewBackground(null);// 设置预览背景;可以是颜色或者图片等
  13. comboBox.getInput().style.visibility = 'visible';// 获取组件内部的 input 框的 style 样式
  14. comboBox.setReadOnly(true);// 设置只读
  15. comboBox.setColor('rgba(0,0,0,0)');
  16. comboBox.setPlaceholder('修改货箱颜色');
  17. comboBox.setIcon('imgs/combobox_icon.json');
  18. comboBox.setHoverIcon('imgs/combobox_icon_hover.json');
  19. comboBox.setActiveIcon('imgs/combobox_icon_hover.json');
  20. comboBox.setBorderRadius(0);
  21. comboBox.setBorder(new ht.ui.border.IndividualLineBorder(0, 0, 1, 0, 'rgb(138,138,138)'));
  22. comboBox.setInstant(true);// 设置即时模式;在这种模式下,每输入一个字符 value 属性变化事件就会立即被派发,否则只有失去焦点或敲回车时才被派发
  23.  
  24. tableRow9.addView(label);
  25. tableRow9.addView(comboBox);
  26.  
  27. tableLayout.addView(tableRow9);

最后通过 ht.ui.Form 组件的 addChangeListener 事件监听函数监听 JSON 整体变化事件和 JSON 中单条数据变化事件,这两种事件的解释如下图:

具体监听方法如下:

  1. form.addChangeListener((e) => {
  2. const cargo = form.__cargo__;
  3. if (e.kind === 'formDataValueChange') {// JSON 中单条数据值变化事件
  4. const name = e.name;
  5. let value = e.newValue;
  6.  
  7. if (name === 'blend') {
  8. if (value && value.startsWith('rgba')) {
  9. const li = value.lastIndexOf(',');
  10. value = 'rgb' + value.substring(value.indexOf('('), li) + ')';
  11. }
  12. }
  13. cargo.setValue(name, value);
  14. }
  15. });

然后通过 HT 封装的事件派发器 ht.Notifier 将界面中不同区域的组件之间通过事件派发进行交互,根据不同的事件类型进行不同的动作:

  1. eventbus.add((e) => {// 增加监听器 事件总线;界面中不同区域的组件之间通过事件派发进行交互
  2. if (e.type === 'cargoFocus') {
  3. shelfPane.show();
  4.  
  5. const cargo = e.data;
  6. form.__cargo__ = cargo;
  7. const json = form.getJSON();// 获取由表单组件的名称和值组装成的 JSON 数据
  8. for (let k in json) {
  9. form.setItem(k, cargo.getValue(k));
  10. }
  11. return;
  12. }
  13. if (e.type === 'cargoBlur') {
  14. shelfPane.hide();
  15. return;
  16. }
  17. });

图表

 从 src\view\common 文件夹中的 chartPane.js 中获取 graph3dView 的外部接口被 src/view 中的 index.js 调用:
  1. import chartPane from './common/chartPane.js';

chartPane 和 shelfPane 类似,都是 Pane 类的对象,属性也类似,不同的是内容。因为今天展示的只是一个 Demo,我们并没有做过多的关于图表插件的处理,所以这里就用图片来代替动态图表,不过就算想做也是很容易的事,HT 官网上有更多有趣的例子!

回到正题,chartPane 图表面板的实现非常容易,将内部的子组件设置背景图片再添加进 chartPane 图表面板中即可:

  1. import Pane from './Pane.js';
  2.  
  3. var chartPane = new Pane();
  4.  
  5. var view1 = new ht.ui.View();
  6. view1.setBackgroundDrawable(new ht.ui.drawable.ImageDrawable('imgs/chart.png', 'fill'));// 设置组件的背景 Drawable 对象;组件渲染时优先使用此 Drawable 对象,如果为空,再用 background 转换
  7.  
  8. var view2 = new ht.ui.View();
  9. view2.setBackgroundDrawable(new ht.ui.drawable.ImageDrawable('imgs/chart.png', 'fill'));
  10.  
  11. chartPane.getView().style.background = 'rgba(18,28,64,0.60)';// 设置背景颜色
  12.  
  13. chartPane.addView(view1, {// 将子组件加到容器中
  14. title: '其他图表'
  15. });
  16.  
  17. chartPane.addView(view2, {
  18. title: '库存负载'
  19. });
  20.  
  21. chartPane.setActiveView(view2);// 设置选中的子组件

整个例子解析完毕,有兴趣的小伙伴可以去 HT 官网(http://www.hightopo.com/)上自习查阅资料,好好品味,一定会发现更大的世界。

http://www.hightopo.com/demo/large-screen/index.html

总结

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

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

基于 HTML5 的 3D 工业互联网展示方案的更多相关文章

  1. 基于html5鼠标悬停图片动画展示效果

    分享一款基于html5鼠标悬停图片动画展示效果.里面包含两款不同效果的html5图片展示效果.效果图如下: 在线预览   源码下载 实现的代码. html代码: <div class=" ...

  2. 基于HTML5实现3D热图Heatmap应用

    Heatmap热图通过众多数据点信息,汇聚成直观可视化颜色效果,热图已广泛被应用于气象预报.医疗成像.机房温度监控等行业,甚至应用于竞技体育领域的数据分析. http://www.hightopo.c ...

  3. 基于html5制作3D拳击游戏源码下载

    今天给大家分享一款基于HTML5实现的3d拳王游戏源码.这款实例适用浏览器:360.FireFox.Chrome.Safari.Opera.傲游.搜狗.世界之窗. 不支持IE8及以下浏览器. 在线预览 ...

  4. 基于HTML5气3D仿真培训系统

    根据最近的上线HTML5的燃气3D培训仿真系统.曾经的老系统是採用基于C++和OpenGL的OpenSceneGraph引擎设计的,OSG引擎性能和渲染效果各方面还是不错的,但由于这次新产品需求要求能 ...

  5. 基于HTML5实现3D监控应用流动效果

    http://www.hightopo.com/guide/guide/core/lighting/examples/example_flowing.html 流动效果在3D领域有着广泛的应用场景,如 ...

  6. 基于HTML5的3D网络拓扑树呈现

    在HT for Web中2D和3D应用都支持树状结构数据的展示,展现效果各异,2D上的树状结构在展现层级关系明显,但是如果数据量大的话,看起来就没那么直观,找到指定的节点比较困难,而3D上的树状结构在 ...

  7. 基于HTML5的3D网络拓扑自动布局

    上篇将HT for Web的3D拓扑弹力布局的算法运行在Web Workers后台(http://www.hightopo.com/blog/70.html),这篇我们将进一步折腾,将算法运行到真正的 ...

  8. 基于 HTML5 的 3D 工控隧道案例

    隧道的项目我目前是第一次接触,感觉做起来的效果还蛮赞的,所以给大家分享一下.这个隧道项目的主要内容包括:照明.风机.车道指示灯.交通信号灯.情报板.消防.火灾报警.车行横洞.风向仪.COVI.微波车检 ...

  9. 基于HTML5技术的电力3D监控应用(二)

    上篇介绍了我们电力项目的基本情况,我们选用HTML5技术还是顶着很大压力,毕竟HTML5技术性能行不行,浏览器兼容性会不会有问题,这些在项目选型阶段还是充满疑惑,项目做到现在终于快收尾了我们才敢松口气 ...

随机推荐

  1. Docker最全教程——从理论到实战(二)

    上篇内容链接: https://www.cnblogs.com/codelove/p/10030439.html Docker和ASP.NET Core Docker 正在逐渐成为容器行业的事实标准, ...

  2. spring-boot-2.0.3启动源码篇四 - run方法(三)之createApplicationContext

    前言 此系列是针对springboot的启动,旨在于和大家一起来看看springboot启动的过程中到底做了一些什么事.如果大家对springboot的源码有所研究,可以挑些自己感兴趣或者对自己有帮助 ...

  3. SSM整合Netty5.0详细说明

    阅读本文约“3.2分钟” 最近又有粉丝加Q群讨论netty整合SSM项目的方式等,我在这里抽了休息日的时候整理一下,一步一步的记录,注意的是,本案例仅实现了用netty整合SSM后与单片机等类TCP应 ...

  4. 【Javaweb】poi实现通过上传excel表格批量导入数据到数据库

    1.导入poi相关jar包 对于只操作2003及以前版本的excel,只需要导入poi-XXX.jar ,如果还需要对2007及以后版本进行操作,则需要导入 poi-ooxml-XXX.jar poi ...

  5. laravel项目thinksns-plus安装出现RuntimeException Symlink from * to * failed错误

    今天xshell安装thinksns-plus的laravel项目时出现了一个错误, [RuntimeException] Symlink from "/root/www.z5w.net/t ...

  6. phpstudy等php本地环境运行缓慢的问题解决方法

    我们经常会使用些一键安装包部署本地服务器环境.比如phpstudy.但是会有不少人发现,wordpress等使用数据库的程序打开或者切换页面的速度明显低于静态站点.甚至需要好几秒.这个问题一直困扰了我 ...

  7. nginx sub模块替换文本

    nginx的ngx_http_sub_module模块,可以用于修改网站响应内容中的字符串,如过滤敏感词.第三方模块ngx_http_substitutions_filter_module,弥补了ng ...

  8. Apex 中文件夹相关的单元测试

    Salesforce 中的文件夹 在 Salesforce 中,我们可以建立各种文档.报表.仪表板.电子邮件模板等.它们都被保存在相应的文件夹中. Salesforce 的后端将这些文件夹保存为 Fo ...

  9. Android EditText常用属性

    一.EditText介绍 ①EditText是一个输入框,在Android开发中是常用的控件.也是获取用户数据的一种方式. ②EditText是TextView的子类,它继承了TextView的所有属 ...

  10. 用 Heapster 监控集群 - 每天5分钟玩转 Docker 容器技术(176)

    Heapster 是 Kubernetes 原生的集群监控方案.Heapster 以 Pod 的形式运行,它会自动发现集群节点.从节点上的 Kubelet 获取监控数据.Kubelet 则是从节点上的 ...