作者:Rise Hao,云智慧前端开发工程师。开源我的项目数据可视化平台 FlyFish Maintainer。主攻可视化大屏方向,专一工程研发的降本增质、增效,在可视化方面具备丰盛的开发教训 。

FlyFish 是云智慧公司自主设计、研发的一款低门槛、高拓展性的低代码利用开发平台, 为数据可视化开发场景提供了高效的一站式解决方案。FlyFish提供丰盛的组件和利用模板库, 可通过利落拽的模式实现数据可视化开发,零开发背景的用户也可实现数据可视化开发工作。 同时,FlyFish也提供了灵便的拓展能力,反对组件开发、自定义函数与全局事件等配置, 面向简单需要场景可能保障高效开发与交付。

相干文档地址

  • FlyFish官网开发文档
  • YAPI数据模仿文档
  • GitHub地址
  • Gitee地址

开始前(筹备)

  1. FlyFish平台在线地址

  2. 创立我的项目(整体项目名称)

    • 查看是否有以后正要做的我的项目

    • 增加以后我的项目(如果没有以后我的项目,有则疏忽)

    • 增加利用(可视化大屏)

    • 浏览是否有满足UI设计的根底组件(UI:可视化大屏组件款式)

开始上手(高级)

  1. 抉择要开发的利用(可视化大屏)

  2. 抉择适宜的根底组件

    • 拖入可视化大屏内须要摆放的地位,去抉择适合的配置满足UI的需要

    • 如果仅通过配置项无奈满足以后组件与UI的要求,可自定义CSS,增加css名字(会增加到以后组件的最外层,并在全局款式内进行自定义)

    • 申请数据的形式

  3. 抉择以后我的项目下的组件(如果有的话...)

开始开发(中级)

  1. 有相似的我的项目组件(然而仍须要进行定制化的)

    • 复制此组件,并起一个新的名字

    • 编辑此组件信息,增加到以后我的项目

  2. 增加定制化我的项目组件(如果根底组件不具备满足你以后的需要)

  1. 我的项目组件开发,抉择刚刚创立好的我的项目组件、点击开发组件

  1. 代码构造

build/webpack.config.dev.js

组件开发阶段保留对组件进行 webpack 编译打包扩大配置文件,具体请参考更改组件编译配置

#build/webpack.config.production.js

组件导出阶段对组件进行 webpack 编译打包扩大配置文件,具体请参考更改组件编译配置

#package.json

组件信息和依赖,具体请参考增加组件依赖

#options.json

组件开发底部的组件预览大屏的预设,具体请参考减少组件开发大屏预设

#src/main.js

组件注册入口,组件开发会主动产生此文件,如务必要不须要更改。具体请参考注册组件

#src/Component.js

组件代码文件,仅反对原生 Javascript 进行开发,请参考开发组件。如应用 react 开发,请参考React 开发组件

#src/setting.js

组件设置区域注册入口,组件开发会主动产生此文件,如组件有须要开发自定义配置和数据绑定,请关上此文件内正文掉的注册内容

#src/setting/options

组件设置区域组件,需应用 react 开发,具体请参考减少组件配置

#src/setting/data

组件设置数据区域组件,需应用 react 开发,具体请参考减少组件数据配置

  1. 是否须要配置模块

1 是左边的数据申请,2 是左边的模块配置

  1. 数据申请形式(间接在代码中写)

    • 仅开发中失效--的模仿数据

    • 大屏仍然失效--的模仿数据(利用 = 可视化大屏)

    • 默认选项,没有数据,但该参数又是必须参数(传递给组件的默认数据)

  2. 组件内获取数据

    • 获取API申请数据,间接props中获取data

    • 获取默认选项

  3. 装置依赖(如果组件开发中须要援用某些插件)

FlyFish反对通过Echarts等内部平台开发组件,如有须要可通过援用相干插件的形式去实现。

  1. 更新上线

开始进阶(高级)

  1. 设置大屏(官网文档)

  2. 事件(官网文档)

    • 组件之间传递事件

      1.     第一个箭头传递给某个组件事件以及参数
      2.     第二个箭头能够间接触发某个组件的数据申请

    • 组件之间接管事件

      收到了组件传递过去的事件则会主动执行你的自定义

  • 配置组件之间的事件(不配置不会失效哟)

    •   有了方才组件的外部自定义的事件,咱们能够设置之间的关联

    •   如果抉择红框框内的事件则作用在整个组件身上,如果抉择红色箭头的事件则依照你方才创立的办法开始执行

    •   如果抉择红色圆圈内的则作用于整个大屏之上,不在于某个组件内,如果抉择红色方框则作用于所选的组件外部事件

    •   抉择刚刚定义的trigger事件

    •   接管组件定义的办法(留神不是发送事件的那个哟,当然为了防止容易犯错误,你能够将两个名字设为统一)

    •   能够抉择批改刚刚定义的事件

  1. 函数(官网文档)

    自定义函数,常见的用法是提供给大屏的事件应用。

  2. 全局数据集(官网文档)

全局数据集能够给多个组件应用

开始进阶(骨灰级)

  1. 默认选项追随数据进行实时渲染?

    • 重写load办法,因为他能够更新默认的选项 defaultOptions。

       /**  * 加载数据  * @param {Object=} options 长期加载选项  * @param {function(Array.<Object>)=} onSuccess 加载实现回调  * @param {function(string)} onError 加载失败回调  * @returns {Component}  */ load(options = {}, onSuccess = null, onError = null) {   if (this.hasDataSource()) {     if (isFunction(options)) {       /* eslint-disable no-param-reassign */       onError = onSuccess;       onSuccess = options;       options = {};       /* eslint-enable no-param-reassign */     }     // 加载数据事件     this.trigger('load');     this.dataSource.load(       options,       (data) => {         call(onSuccess, this, data);         let opt = this.getOptions()         const { lineBackgroundDefault, lineBackground } = opt;         const newLineBackground = data.dataList.map((_,i) => lineBackground[i] || lineBackgroundDefault);         // 数据加载实现事件         console.log(newLineBackground, '<--data')         this.trigger('loaded', data);         this.setOptions({lineBackground: JSON.parse(JSON.stringify(newLineBackground))})         this.draw(data);       },       onError     );   }   return this; }
  2. 配置面板如何依据数据实现联动变动?

    • 在options.js文件写上上面的句子就能够拿到更新之后的数据了。todo(data)

      /* * @Author: Rise.Hao * @Date: 2022-05-11 22:53:50 * @LastEditors: Rise.Hao * @LastEditTime: 2022-06-01 21:33:08 * @Description: file content */'use strict';import React from 'react';import Base from './panel/index.js'import { cloneDeep } from "data-vi/helpers";import { recursionOptions } from '@cloudwise-fe/chart-panel'import { ComponentOptionsSetting } from 'datavi-editor/templates';export default class OptionsSetting extends ComponentOptionsSetting {  constructor(props) { super(props)  }  // 可自定义款式: 若您在设置面板中书写款式会抽离出setting.css.  // 显式的将以下属性设置为true可告知FlyFish来加载您的款式文件  enableLoadCssFile = true;  componentDidMount() { const { component } = this.props; component.bind('draw', () => {   this.forceUpdate() })  }  componentWillUnmount() { const { component } = this.props; this.computedSettingStyleAppend(true); component.unbind('draw');  }  getTabs() { const options = recursionOptions(this.props.options, true) const {component, updateOptions} = this.props; return {   config: {     label: '配置',     content: () => <Base initialValues={options}  props={this.props} options={cloneDeep(component.getOptions())} onChange={updateOptions} />,   }, }  }}
      • 配置面板应该怎么写?
      /* * @Author: Rise.Hao * @Date: 2022-05-29 13:33:05 * @LastEditors: Rise.Hao * @LastEditTime: 2022-06-01 22:00:47 * @Description: file content */import React from 'react'import { Input, Select, ConfigProvider, InputNumber } from 'antd';import { ChartProvider, FormItem, FormItemGroup, CollapsePanel, Collapse, ColorPickerInput } from '@cloudwise-fe/chart-panel'export default function Index(props) {  const { options, initialValues, onChange } = props;  const { lineBackground = [] } = options || {};  const lineFunc = (e, index, key) => { const newLineBackground = lineBackground.map((item, i) => {   return i === index ? {     ...item,     [key]: e   } : item }) onChange({ lineBackground: newLineBackground })  }console.log(lineBackground,'<--lineBackground')  return <ChartProvider> <ConfigProvider prefixCls="ant4">   <Collapse>     <CollapsePanel title="面积色彩" key="1">       <FormItemGroup layout="vertical">         {           lineBackground.map((item, index) => {             return <FormItem key={`background${index}`} label={`第${index + 1}条线面积色彩`}>               <ColorPickerInput onChange={(e) => lineFunc(e, index, 'background')} value={item.background} gradientMode="gradient" />             </FormItem>           })         }       </FormItemGroup>     </CollapsePanel>     <CollapsePanel title="字体色彩" key="1">       <FormItemGroup layout="vertical"  initialValues={initialValues} onValuesChange={onChange}>         <FormItem label="X轴字体" name="xAxisFontColor">           <ColorPickerInput gradientMode="gradient" />         </FormItem>         <FormItem label="Y轴字体" name="yAxisFontColor">           <ColorPickerInput gradientMode="gradient" />         </FormItem>       </FormItemGroup>     </CollapsePanel>   </Collapse> </ConfigProvider>  </ChartProvider>}
  3. 有些时候更改了某个配置项而他有没有失效?

    比方:参数自身是一个数组又或者是一个对象,这个数组自身就存在,而你此次操作只是给数组外面删除了一个对象,最终没有失效。起因是FlyFish默认执行的setOptions是合并数据而不是更新数据

    • 把数组进行字符串解决,让他变成一个值,这样就不是合并了。
    • 重写setOptions办法,数组外面有的参数都执行更新操作,没有的执行合并操作。
import { defaultsDeep } from "data-vi/helpers";/**     * 设置选项     *     * @param {Object} options 选项     * @param {boolean} merge 是否合并原来的选项     * @returns {Component}     */  setOptions(options = {}, merge = true) {    const { replaceAll, ...mergeOptions } = options;    const replaceKeys = ['lineBackground'];    // 魔改一下局部后果解决    if (replaceAll) {      this.options = mergeOptions;    } else if (merge) {      let cloneOption = defaultsDeep({}, mergeOptions, this.options);      if (replaceKeys.find((v) => typeof mergeOptions[v] !== 'undefined')) {        cloneOption = {          ...cloneOption,          ...mergeOptions,        };      }      this.options = cloneOption;    } else {      this.options = defaultsDeep({}, mergeOptions, this.getDefaultOptions());    }
  1. 确保在所有组件加载实现后主动执行一个trigger办法?

useEffect(() => {    if (!nowdata) return;//nowdata是申请后端返回来的数据    if (parent && parent.screen) {      const allComponent = parent.screen.getComponents();      const lastComponent = allComponent[allComponent.length - 1];      if (lastComponent.mounted) {        parent.trigger('add', { id: currentItem, value: nowdata })      } else {        lastComponent.bind("mounted", () => {          parent.trigger('add', { id: currentItem, value: nowdata })          lastComponent.unbind("mounted");        })      }    }  }, [nowdata])
  1. 我这个组件怎么去更改别的组件的默认选项?(审慎操作)

const compontentList = this.props.component.screen.getComponents()compontentList.forEach((item)=>{//这里能够做判断对那个组件进行操作    item.setConfig({      visible: true    })}
  1. 倡议不带 get 的 static?

 // 默认配置 static defaultConfig = {};getDefaultConfig() {    return defaultsDeep({}, this.constructor.defaultConfig, {        width: 100,        height: 100,        index: 0,        left: 0,        top: 0,        name: '',        visible: true,        class: ''    });}
  1. 输入框和FlyFish的事件抵触?

// 禁止冒泡掉const bubblingFunc= (event)=>{    event.stopPropagation();}<input onKeyUp={bubblingFunc} onKeyDown={bubblingFunc} />
  1. 事件能够在组件外面间接写好了!

// 注册事件registerComponentEvents("id", "DEFAULT_VERSION", {    onChange: "变更",    onValueChange: "值变更",});// 注册actionregisterComponentAction("id", "DEFAULT_VERSION", "changeValue", ReactCompont);call(component, "changeValue", ...args);// ReactCompont;export default (props) => (    <Form>        <FormItem label="横坐标(X)" cols={[8, 8]}>            <Input                value={toString(props.args[0])}                placeholder="请输出横坐标(X)"                onChange={(event) =>                    props.onChange(0, toNumber(event.target.value))                }            />        </FormItem>        <FormItem label="纵坐标(Y)" cols={[8, 8]}>            <Input                value={toString(props.args[1])}                placeholder="请输出纵坐标(Y)"                onChange={(event) =>                    props.onChange(1, toNumber(event.target.value))                }            />        </FormItem>    </Form>);
  1. 动态文件从根目录取绝对路径的该如何设置?

import { DEFAULT_VERSION } from "data-vi/components";import config from "data-vi/config";const componentStaticDir = props.parent.getVersion() == null || props.parent.getVersion() === DEFAULT_VERSION ? "components" : "release";  const link = `${config.componentsDir}/${props.parent.getType()}/${props.parent.getVersion() || DEFAULT_VERSION}/${componentStaticDir}/public`;    //webpack.config.production.js文件  const CopyPlugin = require("copy-webpack-plugin");  plugins: [        new CopyPlugin( [            { from: path.resolve(__dirname, '../') + '/src/ModelRotates/public', to: path.resolve(__dirname, '../') + '/components/public/', },        ]),    ]      //装置依赖  "copy-webpack-plugin": "5.1.1"
  1. 组件内须要本人写申请?

import { getHttpData } from 'data-vi/api';import { componentApiDomain } from 'data-vi/config';const getMapdata = (name) => {    getHttpData(componentApiDomain + `/atlas/info?location=${encodeURIComponent(name)}`, 'GET', {})      .done((request) => {        console.log('申请胜利', request)        setNowdata(request.data)      })      .fail((request, xhr, msg) => {        console.log('失败了')      });  }
  1. 比方跳转大屏如何依据url实现数据变更?

function preDisposeParams(params) {     let sumParams = window.location.search ? window.location.search.split('?')[1] : '';    let eachParams = sumParams.split('&')[1] || '';    let systemCode = eachParams.split('=')[1] || '';    let jsonParams ={      "systemCode":systemCode    }    console.log(sumParams,"-",eachParams,"-",systemCode)  return jsonParams; }
  1. 整张大屏内如何应用字体?【前期可能会更改】

    •   示例组件
/**     * 钩子办法 组件mount挂载时调用     */    _mount() {      const container = this.getContainer();      console.log(this.getType(),this.getVersion(),'123')      const componentStaticDir =      this.getVersion() == null || this.getVersion() === DEFAULT_VERSION ? "components" : "release";      const link = `${config.componentsDir}/${this.getType()}/${          this.getVersion() || DEFAULT_VERSION      }/${componentStaticDir}/assets`;      container.html(`        <style>          @font-face {            font-family: FZZYJW;            src: url('${link}/FZZYJW.TTF');          }          @font-face {            font-family: FZZZHONGJW;            src: url('${link}/FZZZHONGJW.TTF');          }          @font-face {            font-family: HYa9gj;            src: url('${link}/hya9gjm.ttf');          }          @font-face {            font-family: HYk2gj;            src: url('${link}/HYLingXin.ttf');          }          @font-face {            font-family: SourceHanSerifSCHeavy;            src: url('${link}/SourceHanSerifSCHeavy.ttf');          }        </style>      `);    }

开发实现

  1. 点击预览,查看成果是否满足,并简略自测是否有BUG

  1. 导出已实现的可视化大屏

部署上线

  1. componentApiDomain = 申请后端数据的ip地址

  2. 如果nginx代理没有从根目录配置则须要更改

    • iplpadImgDir的门路 = ./ (/data/app须要依据nginx代理的具体门路来配置)
    • components的门路= /data/app/components (/data/app须要依据nginx代理的具体门路来配置)

  3. 规范流程 tengine部署

    • 批改前端部署包配置文件
    • 新建前端部署文件夹 web(/data/web/ tengine部署中都以该文件夹为例)
    • 将前端包文件screen.zip拷贝到该目录下
    • 解压命令:unzip screen.zip
    • 批改/data/app/sxdl_web/config/env.production.js
    • 批改/data/tengine/conf/vhost/门路 (tengine部署目录为/data/tengine)
    • 批改/data/app/sxdl_web/index.html (浏览器刷新文本)
    • 批改/data/app/sxdl_web/config/env.conf.json (浏览器页签文本)
    • 重启tengine服务 /data/app/tengine/sbin/nginx -s reload
    • 前端拜访地址
  4. nginx部署

    • 批改前端部署目录xxxx/config/env.production.js配置文件
    • 配置文件: env.production.js:{ componentApiDomain:后端接口地址 }
    • 部署环境nginx 留神:大屏前端配置的端口不能够和其余服务的前端的端口抵触
    • 首先备份/nginx/conf下的nginx.conf
    • 编辑nginx.conf

      重启ngnix /sbinx下 ./ngnix  -t                 //查看配置文件nginx.conf的正确性./ngnix  -s  reload                                     //从新载入配置文件 
  5. 上传screen.zip file.dir: /var/www/html 将解压后screen.zip文件放入该目录(先清空html文件夹)

    备注:如果没有雷同的门路,则轻易找个门路放文件就行
  6. 解压screen.zip文件
  7. 批改/var/www/html/env.production.js文件 \批改配置文档: env.production.js
  8. 批改配置后,重启nginx
  9. 我的项目运行地址: 服务器地址+’/index.html’

    (注:前端本次用FlyFish开发页面,间接打出包,git上无仓库)

下载源码

  1. 前缀:http://10.0.16.230:7001/appli...

    演示样例,实在链接会依据FlyFish本地的地址变动
  2. 大屏的ID

  1. 最终下载地址(演示样例,非实在地址):http://10.0.16.230:7001/appli...

    注:前缀 + 大屏ID = 下载地址(请审慎频繁调用)

    Echarts配置及导出(如有下载失败,请更换版本号):https://www.npmjs.com/package...

  2. 装置依赖: yarn 或 npm install
  3. 启动我的项目:yarn dev 或 npm run dev
  4. 再次编译:yarn build 或 npm run build