关于仪表板

仪表板是由模块a-dashboard实现的。这里主要针对二次开发用户讲解如何制作部件,因此,如果对底层的实现机制感兴趣,可以直接查看模块a-dashboard的源码

关于部件

部件是构成仪表板的基本单元。CabloyJS中的部件充分利用了Vue组件的响应式机制,实现了非常灵活的数据绑定数据联动特性,可以让数据在仪表板中的部件之间流动

模块test-party内置了4个部件:水果销量水果销量(折线图)水果销量(饼图)快照

其中部件水果销量(折线图)可以从部件水果销量获取数据,并且可以向部件快照提供数据,具有承上启下的作用,因此我们以部件水果销量(折线图)为例来说明如何开发一个部件

1. 创建一个Vue组件

部件本身是一个Vue组件,但是巧妙的使用了Vue的多个语法特性。为了更清晰地说明,这里先贴出全部源码,再做详细解释

src/suite-vendor/test-party/modules/test-party/front/src/kitchen-sink/components/widgets/salesLine.vue

  1. 1<template>
  2. 2 <f7-card>
  3. 3 <f7-card-header>{{$text('Fruit Sales(Line Chart)')}}</f7-card-header>
  4. 4 <f7-card-content>
  5. 5 <canvas ref="chart"></canvas>
  6. 6 <div class="error" v-if="errorMessage">{{errorMessage}}</div>
  7. 7 </f7-card-content>
  8. 8 </f7-card>
  9. 9</template>
  10. 10<script>
  11. 11const propsSchema = {
  12. 12 type: 'object',
  13. 13 properties: {
  14. 14 dataSource: {
  15. 15 type: 'object',
  16. 16 ebType: 'text',
  17. 17 ebTitle: 'Data Source',
  18. 18 ebWidget: {
  19. 19 bindOnly: true,
  20. 20 clue: 'salesDataSource',
  21. 21 },
  22. 22 },
  23. 23 fruit: {
  24. 24 type: 'string',
  25. 25 ebType: 'select',
  26. 26 ebTitle: 'Fruit',
  27. 27 ebOptions: [
  28. 28 { title: 'All', value: 'All' },
  29. 29 { title: 'Apples', value: 'Apples' },
  30. 30 { title: 'Pears', value: 'Pears' },
  31. 31 ],
  32. 32 ebOptionsBlankAuto: true,
  33. 33 ebWidget: {
  34. 34 clue: 'salesFruit',
  35. 35 },
  36. 36 },
  37. 37 },
  38. 38};
  39. 39
  40. 40const attrsSchema = {
  41. 41 type: 'object',
  42. 42 properties: {
  43. 43 snapshot: {
  44. 44 ebTitle: 'Snapshot',
  45. 45 ebWidget: {
  46. 46 clue: 'snapshot',
  47. 47 },
  48. 48 },
  49. 49 },
  50. 50};
  51. 51
  52. 52// export
  53. 53export default {
  54. 54 installFactory,
  55. 55};
  56. 56
  57. 57// installFactory
  58. 58function installFactory(_Vue) {
  59. 59 const Vue = _Vue;
  60. 60 const ebDashboardWidgetBase = Vue.prototype.$meta.module.get('a-dashboard').options.mixins.ebDashboardWidgetBase;
  61. 61 return {
  62. 62 meta: {
  63. 63 widget: {
  64. 64 schema: {
  65. 65 props: propsSchema,
  66. 66 attrs: attrsSchema,
  67. 67 },
  68. 68 },
  69. 69 },
  70. 70 mixins: [ebDashboardWidgetBase],
  71. 71 props: {
  72. 72 dataSource: {
  73. 73 type: Object,
  74. 74 },
  75. 75 fruit: {
  76. 76 type: String,
  77. 77 },
  78. 78 },
  79. 79 data() {
  80. 80 return {
  81. 81 chartjs: null,
  82. 82 chart: null,
  83. 83 snapshot: null,
  84. 84 errorMessage: null,
  85. 85 };
  86. 86 },
  87. 87 watch: {
  88. 88 dataSource() {
  89. 89 this.__updateChart();
  90. 90 },
  91. 91 fruit() {
  92. 92 this.__updateChart();
  93. 93 },
  94. 94 },
  95. 95 mounted() {
  96. 96 this.__init();
  97. 97 },
  98. 98 beforeDestroy() {
  99. 99 if (this.chart) {
  100. 100 this.chart.destroy();
  101. 101 }
  102. 102 },
  103. 103 methods: {
  104. 104 __init() {
  105. 105 this.$meta.module.use('a-chartjs', module => {
  106. 106 this.chartjs = module.options.utils.chartjs;
  107. 107 this.__updateChart();
  108. 108 });
  109. 109 },
  110. 110 __prepareData() {
  111. 111 const fruitIndex = this.dataSource.cols.findIndex(item => item === this.fruit);
  112. 112 if (fruitIndex === -1) throw new Error();
  113. 113 const chartData = {
  114. 114 labels: this.dataSource.rows,
  115. 115 datasets: [{
  116. 116 fill: false,
  117. 117 backgroundColor: this.dataSource.colors[fruitIndex],
  118. 118 data: this.dataSource.dataset.map(item => item[fruitIndex]),
  119. 119 }, ],
  120. 120 };
  121. 121 return chartData;
  122. 122 },
  123. 123 __prepareOptions() {
  124. 124 const chartOptions = {
  125. 125 maintainAspectRatio: false,
  126. 126 responsive: true,
  127. 127 animation: {
  128. 128 onComplete: () => {
  129. 129 this.__createSnapshot();
  130. 130 }
  131. 131 },
  132. 132 title: {
  133. 133 display: true,
  134. 134 position: 'top',
  135. 135 text: this.fruit,
  136. 136 fontColor: 'rgba(128, 128, 128, 0.6)',
  137. 137 },
  138. 138 legend: {
  139. 139 display: false,
  140. 140 },
  141. 141 scales: {
  142. 142 xAxes: [{
  143. 143 gridLines: {
  144. 144 display: false,
  145. 145 },
  146. 146 ticks: {
  147. 147 fontColor: 'rgba(128, 128, 128, 0.6)',
  148. 148 },
  149. 149 }],
  150. 150 yAxes: [{
  151. 151 gridLines: {
  152. 152 display: true,
  153. 153 },
  154. 154 ticks: {
  155. 155 fontColor: 'rgba(128, 128, 128, 0.6)',
  156. 156 stepSize: 200,
  157. 157 },
  158. 158 }],
  159. 159 },
  160. 160 };
  161. 161 return chartOptions;
  162. 162 },
  163. 163 __createSnapshot() {
  164. 164 const image = this.chart.toBase64Image();
  165. 165 this.snapshot = {
  166. 166 title: this.$text('Fruit Sales(Line Chart)'),
  167. 167 image,
  168. 168 };
  169. 169 },
  170. 170 __clearChart() {
  171. 171 if (this.chart) {
  172. 172 this.chart.clear();
  173. 173 }
  174. 174 },
  175. 175 __updateChart() {
  176. 176 try {
  177. 177 if (!this.dataSource || !this.fruit) {
  178. 178 this.__clearChart();
  179. 179 this.errorMessage = this.$text('Please set data source');
  180. 180 return;
  181. 181 }
  182. 182 const chartData = this.__prepareData();
  183. 183 const chartOptions = this.__prepareOptions();
  184. 184 if (!this.chart) {
  185. 185 // canvas
  186. 186 const chartCanvas = this.$refs.chart.getContext('2d');
  187. 187 // fill
  188. 188 this.chart = new this.chartjs(chartCanvas, {
  189. 189 type: 'line',
  190. 190 data: chartData,
  191. 191 options: chartOptions,
  192. 192 });
  193. 193 } else {
  194. 194 this.chart.data = this.__prepareData();
  195. 195 this.chart.options = this.__prepareOptions();
  196. 196 this.chart.update();
  197. 197 }
  198. 198 this.errorMessage = null;
  199. 199 return;
  200. 200 } catch (err) {
  201. 201 this.__clearChart();
  202. 202 this.errorMessage = this.$text('There may be a binding error');
  203. 203 }
  204. 204 },
  205. 205 },
  206. 206 };
  207. 207
  208. 208}
  209. 209
  210. 210</script>
  211. 211<style lang="less" scoped>
  212. 212.error {
  213. 213 position: absolute;
  214. 214 bottom: 6px;
  215. 215 right: 6px;
  216. 216 font-size: smaller;
  217. 217}
  218. 218
  219. 219</style>

1.1 export default

首先,我们并不像一般的Vue组件那样直接导出Options对象,而是像Vue插件一样导出一个带installFactory方法的对象

1.2 mixins: [ebDashboardWidgetBase]

ebDashboardWidgetBase是模块a-dashboard提供的部件基类,所有部件都必须mixin这个基类

这里我们就可以解释为什么不能直接导出Options对象。因为模块a-dashboard是异步加载的模块,而所有部件又必须继承ebDashboardWidgetBase。如果直接导出Options对象,那么在编译期是获取不到ebDashboardWidgetBase的,所以mixin必然失败

1.3 meta

meta用于存储Vue组件的元信息

1.3.1 meta.widget.schema.props

schema.props是部件props对应的JSON Schema,用于渲染部件的属性表单

关于JSON Schema,请参见:表单验证

名称 默认值 说明
type 属性值的类型
ebType 用于渲染的组件类型
ebTitle 属性的标题
ebWidget.bindOnly false 只允许动态绑定
ebWidget.clue 属性值的线索。在部件之间进行数据绑定时,只有clue相同的属性才允许绑定,从而简化用户的选择,减少无关部件和数据的干扰

1.3.2 meta.widget.schema.attrs

schema.attrs是部件data属性对应的JSON Schema,在进行数据绑定时作为数据源

也就是说,并不是部件所有的data属性都可以作为数据源。要想成为数据源,必须在schema.attrs中进行定义

1.4 watch

为了实时响应数据源的变化,我们可以使用Vue的另一个语法特性watch,监测schema.props中所有属性的变更,从而做出响应

2. 注册Vue组件

为了让系统可以找到此Vue组件,需要在组件清单中注册

src/suite-vendor/test-party/modules/test-party/front/src/components.js

  1. 1import widgetSalesLine from './kitchen-sink/components/widgets/salesLine.vue';
  2. 2
  3. 3export default {
  4. 4 widgetSalesLine,
  5. 5};

3. 后端资源定义

在一个复杂的系统中,会涉及到资源授权的问题,我们需要对部件进行权限控制。模块a-dashboard提供了一个资源类型a-dashboard:widget,只需定义静态资源,并指定角色,即可同时完成资源的注册与初始授权

3.1 定义静态资源

定义一个静态资源数组

src/suite-vendor/test-party/modules/test-party/backend/src/config/static/resources.js

  1. 1module.exports = app => {
  2. 2 const moduleInfo = app.meta.mockUtil.parseInfoFromPackage(__dirname);
  3. 3 const resources = [
  4. 4 // dashboard widget
  5. 5 {
  6. 6 atomName: 'Fruit Sales(Line Chart)',
  7. 7 atomStaticKey: 'widgetSalesLine',
  8. 8 atomRevision: 0,
  9. 9 atomCategoryId: 'a-dashboard:widget.Demonstration',
  10. 10 resourceType: 'a-dashboard:widget',
  11. 11 resourceConfig: JSON.stringify({
  12. 12 module: moduleInfo.relativeName,
  13. 13 component: 'widgetSalesLine',
  14. 14 }),
  15. 15 resourceRoles: 'root',
  16. 16 },
  17. 17 ];
  18. 18 return resources;
  19. 19};
名称 说明
atomStaticKey 系统自动添加模块名称前缀,生成test-party:widgetSalesLine
atomCategoryId 资源归属目录。资源类型全称a-dashboard:widget作为一级目录
resourceType 资源类型全称
resourceConfig 资源配置信息
resourceConfig.module + component widget前端渲染组件名称
resourceRoles 资源授权对象,这里是角色root

角色root是整个角色树的根,也就意味着所有用户(包括匿名用户)均可访问此资源

更多配置说明,参见静态资源

3.2 注册静态资源

前面定义好一组静态资源,接下来就需要在模块的meta文件中进行注册

src/suite-vendor/test-party/modules/test-party/backend/src/meta.js

  1. 1const staticResources = require('./config/static/resources.js')(app);
  2. 2
  3. 3base: {
  4. 4 statics: {
  5. 5 'a-base.resource': {
  6. 6 items: staticResources,
  7. 7 },
  8. 8 },
  9. 9},
名称 说明
a-base.resource 原子类型的全称。在这里,原子类型resource是由模块a-base提供的
items 静态资源数组