关于仪表板
仪表板是由模块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<template>
- 2 <f7-card>
- 3 <f7-card-header>{{$text('Fruit Sales(Line Chart)')}}</f7-card-header>
- 4 <f7-card-content>
- 5 <canvas ref="chart"></canvas>
- 6 <div class="error" v-if="errorMessage">{{errorMessage}}</div>
- 7 </f7-card-content>
- 8 </f7-card>
- 9</template>
- 10<script>
- 11const propsSchema = {
- 12 type: 'object',
- 13 properties: {
- 14 dataSource: {
- 15 type: 'object',
- 16 ebType: 'text',
- 17 ebTitle: 'Data Source',
- 18 ebWidget: {
- 19 bindOnly: true,
- 20 clue: 'salesDataSource',
- 21 },
- 22 },
- 23 fruit: {
- 24 type: 'string',
- 25 ebType: 'select',
- 26 ebTitle: 'Fruit',
- 27 ebOptions: [
- 28 { title: 'All', value: 'All' },
- 29 { title: 'Apples', value: 'Apples' },
- 30 { title: 'Pears', value: 'Pears' },
- 31 ],
- 32 ebOptionsBlankAuto: true,
- 33 ebWidget: {
- 34 clue: 'salesFruit',
- 35 },
- 36 },
- 37 },
- 38};
- 39
- 40const attrsSchema = {
- 41 type: 'object',
- 42 properties: {
- 43 snapshot: {
- 44 ebTitle: 'Snapshot',
- 45 ebWidget: {
- 46 clue: 'snapshot',
- 47 },
- 48 },
- 49 },
- 50};
- 51
- 52// export
- 53export default {
- 54 installFactory,
- 55};
- 56
- 57// installFactory
- 58function installFactory(_Vue) {
- 59 const Vue = _Vue;
- 60 const ebDashboardWidgetBase = Vue.prototype.$meta.module.get('a-dashboard').options.mixins.ebDashboardWidgetBase;
- 61 return {
- 62 meta: {
- 63 widget: {
- 64 schema: {
- 65 props: propsSchema,
- 66 attrs: attrsSchema,
- 67 },
- 68 },
- 69 },
- 70 mixins: [ebDashboardWidgetBase],
- 71 props: {
- 72 dataSource: {
- 73 type: Object,
- 74 },
- 75 fruit: {
- 76 type: String,
- 77 },
- 78 },
- 79 data() {
- 80 return {
- 81 chartjs: null,
- 82 chart: null,
- 83 snapshot: null,
- 84 errorMessage: null,
- 85 };
- 86 },
- 87 watch: {
- 88 dataSource() {
- 89 this.__updateChart();
- 90 },
- 91 fruit() {
- 92 this.__updateChart();
- 93 },
- 94 },
- 95 mounted() {
- 96 this.__init();
- 97 },
- 98 beforeDestroy() {
- 99 if (this.chart) {
- 100 this.chart.destroy();
- 101 }
- 102 },
- 103 methods: {
- 104 __init() {
- 105 this.$meta.module.use('a-chartjs', module => {
- 106 this.chartjs = module.options.utils.chartjs;
- 107 this.__updateChart();
- 108 });
- 109 },
- 110 __prepareData() {
- 111 const fruitIndex = this.dataSource.cols.findIndex(item => item === this.fruit);
- 112 if (fruitIndex === -1) throw new Error();
- 113 const chartData = {
- 114 labels: this.dataSource.rows,
- 115 datasets: [{
- 116 fill: false,
- 117 backgroundColor: this.dataSource.colors[fruitIndex],
- 118 data: this.dataSource.dataset.map(item => item[fruitIndex]),
- 119 }, ],
- 120 };
- 121 return chartData;
- 122 },
- 123 __prepareOptions() {
- 124 const chartOptions = {
- 125 maintainAspectRatio: false,
- 126 responsive: true,
- 127 animation: {
- 128 onComplete: () => {
- 129 this.__createSnapshot();
- 130 }
- 131 },
- 132 title: {
- 133 display: true,
- 134 position: 'top',
- 135 text: this.fruit,
- 136 fontColor: 'rgba(128, 128, 128, 0.6)',
- 137 },
- 138 legend: {
- 139 display: false,
- 140 },
- 141 scales: {
- 142 xAxes: [{
- 143 gridLines: {
- 144 display: false,
- 145 },
- 146 ticks: {
- 147 fontColor: 'rgba(128, 128, 128, 0.6)',
- 148 },
- 149 }],
- 150 yAxes: [{
- 151 gridLines: {
- 152 display: true,
- 153 },
- 154 ticks: {
- 155 fontColor: 'rgba(128, 128, 128, 0.6)',
- 156 stepSize: 200,
- 157 },
- 158 }],
- 159 },
- 160 };
- 161 return chartOptions;
- 162 },
- 163 __createSnapshot() {
- 164 const image = this.chart.toBase64Image();
- 165 this.snapshot = {
- 166 title: this.$text('Fruit Sales(Line Chart)'),
- 167 image,
- 168 };
- 169 },
- 170 __clearChart() {
- 171 if (this.chart) {
- 172 this.chart.clear();
- 173 }
- 174 },
- 175 __updateChart() {
- 176 try {
- 177 if (!this.dataSource || !this.fruit) {
- 178 this.__clearChart();
- 179 this.errorMessage = this.$text('Please set data source');
- 180 return;
- 181 }
- 182 const chartData = this.__prepareData();
- 183 const chartOptions = this.__prepareOptions();
- 184 if (!this.chart) {
- 185 // canvas
- 186 const chartCanvas = this.$refs.chart.getContext('2d');
- 187 // fill
- 188 this.chart = new this.chartjs(chartCanvas, {
- 189 type: 'line',
- 190 data: chartData,
- 191 options: chartOptions,
- 192 });
- 193 } else {
- 194 this.chart.data = this.__prepareData();
- 195 this.chart.options = this.__prepareOptions();
- 196 this.chart.update();
- 197 }
- 198 this.errorMessage = null;
- 199 return;
- 200 } catch (err) {
- 201 this.__clearChart();
- 202 this.errorMessage = this.$text('There may be a binding error');
- 203 }
- 204 },
- 205 },
- 206 };
- 207
- 208}
- 209
- 210</script>
- 211<style lang="less" scoped>
- 212.error {
- 213 position: absolute;
- 214 bottom: 6px;
- 215 right: 6px;
- 216 font-size: smaller;
- 217}
- 218
- 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
- 1import widgetSalesLine from './kitchen-sink/components/widgets/salesLine.vue';
- 2
- 3export default {
- 4 widgetSalesLine,
- 5};
3. 后端资源定义
在一个复杂的系统中,会涉及到资源授权
的问题,我们需要对部件进行权限控制
。模块a-dashboard
提供了一个资源类型a-dashboard:widget
,只需定义静态资源
,并指定角色,即可同时完成资源的注册与初始授权
3.1 定义静态资源
定义一个静态资源
数组
src/suite-vendor/test-party/modules/test-party/backend/src/config/static/resources.js
- 1module.exports = app => {
- 2 const moduleInfo = app.meta.mockUtil.parseInfoFromPackage(__dirname);
- 3 const resources = [
- 4 // dashboard widget
- 5 {
- 6 atomName: 'Fruit Sales(Line Chart)',
- 7 atomStaticKey: 'widgetSalesLine',
- 8 atomRevision: 0,
- 9 atomCategoryId: 'a-dashboard:widget.Demonstration',
- 10 resourceType: 'a-dashboard:widget',
- 11 resourceConfig: JSON.stringify({
- 12 module: moduleInfo.relativeName,
- 13 component: 'widgetSalesLine',
- 14 }),
- 15 resourceRoles: 'root',
- 16 },
- 17 ];
- 18 return resources;
- 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
- 1const staticResources = require('./config/static/resources.js')(app);
- 2
- 3base: {
- 4 statics: {
- 5 'a-base.resource': {
- 6 items: staticResources,
- 7 },
- 8 },
- 9},
名称 | 说明 |
---|---|
a-base.resource | 原子类型的全称。在这里,原子类型resource 是由模块a-base 提供的 |
items | 静态资源数组 |
评论: