项目介绍
我的职责
主要工作
悟空功能模块
组件接口配置功能
背景:物料组件接入动态数据是从API平台接入,需要在项目中将不同来源类型下的接口统一管理,方便用户使用。接口目录是一个树状结构,根节点和子节点(目录)是一个接口,叶子节点是另一个接口(点击某个目录时调用)。然而这里存在两个问题:
- 由于叶子节点中只有其父级目录的ID,那么在选中叶子节点时如何高亮它的所有父级?
- 对于已选中接口,数据中只有接口id和目录id。再次打开接口树时如何展开所有父级并选中该接口?
任务:
- 要求点击叶子节点,高亮它的所有父级
- 对于已选中节点,再次打开接口选择弹窗时,需要自动展开所有父级并选中
行动:
点击叶子节点,如何高亮它的所有父级?
数据格式化。获得接口数据后以递归的形式将数据格式化。格式化时设置scopedSlots属性,区分节点和叶子节点的title。
export const treeDataFormat = (arr: InterfaceTreeType[], isLeaf = false) => {
return arr.map((tree: InterfaceTreeType) => {
const base: TreeData = {
key: tree.id + new Date().getTime().toString(),
// key: tree.id,
title: tree.name || "--",
isLeaf: isLeaf,
scopedSlots: { title: isLeaf ? "leafTitle" : "title" },
_original: tree
};
if (!isLeaf) {
base.loaded = false;
}
if (!isEmpty(tree.children)) {
base.children = treeDataFormat(tree.children, isLeaf);
}
return base;
});
};
基于AntDesign中的树组件封装接口树组件,组件监听传值改变后,采用BFS将数据中的树打平生成平铺列表。 ```typescript const arrDeepFlatten = (data: TreeData[], deepFlatten: TreeData[] = []) => { data.forEach((root: TreeData) => { deepFlatten = deepFlatten.concat(breadthFirstTraversal(root)); }); return deepFlatten; };
const breadthFirstTraversal = (root: TreeData, arr: TreeData[] = []) => {
if (!root) return arr;
let queue = [root],
current;
while ((current = queue.shift())) {
arr.push(current);
if (!isEmpty(current.children)) {
queue.push(…(
3. 点击叶子节点时,有一个parentId能拿到其父级ID。从平铺列表中找到其父级节点,由于每个节点都存在一个parentId,因此可以定义一个递归方法,根据节点的parentId字段,不断从平铺列表中找到所有父级节点,并记录节点ID保存到激活节点数组。
```typescript
const findRelationParentArr = (data: TreeData[], categoryId: string, arr: TreeData[] = []) => {
const findDataById = (id: string) => data.find((it: TreeData) => it._original.id === id);
const item = findDataById(categoryId);
if (!item) return arr;
arr.push(item);
const parentId = item._original.parentId;
if (parentId) {
const parent = findDataById(parentId);
findRelationParentArr(data, parent?._original.id, arr);
}
return arr;
};
- 在HTML中绑定class,如果激活节点数组中包含当前key,则启用active样式
<template slot="title" slot-scope="{key, title}">
<span :class="['title',{'active': activeNode.key.includes(key)}]">{{title}}</span>
</template>
对于已选中接口,再次打开接口树时如何展开所有父级并选中改接口?
根据上面的方法找到选中接口的所有关联的父级并生成relationParentArr数组,取relationParentArr中的key作为展开数组expandedKeys传递给树组件的expandedKeys字段。目的是将树节点的展开改成受控模式。
const getActiveNode = (data: TreeData[], categoryId: string) => {
const relationParentArr = findRelationParentArr(data, categoryId, []).reverse();
const mapArr = (arr: TreeData[]) => (key: keyof TreeData) => arr.map(g => g[key]);
const getKeyOfValue = mapArr(relationParentArr);
return {
relationParentArr,
key: getKeyOfValue("key"),
title: getKeyOfValue("title")
};
};
由于每个节点下都有可能有叶子节点,因此只是展开还不够,还要查询每个节点下的叶子节点数据并concant到children中。遍历relationParentArr数组,查询数组中所有节点的叶子节点。
当数组中最后一个元素(上次选中接口的父级目录)的叶子节点查询完成后,在叶子节点数据中找到上次已选中的接口。设置selectedKeys给父组件使其高亮,调用scrollIntoView方法将该叶子节点滚动到可视范围内,执行emit事件查询接口详情。
onRoll(key: string) {
let scrollPara: any = { behavior: "smooth", block: "nearest", inline: "start" };
(<any>this.$refs[`interface${key}`])?.scrollIntoView(scrollPara);
}
结果: :::info 表层事实:(用了哪些新技能或工具)
项目中的树数据往往是异步的,叶子节点的查询只发生在节点展开时。为了解决选中叶子节点后,高亮所有父级问题。拿到节点树后,采用BFS算法将树打平转换成平铺列表数组,采用递归的方式根据parentId字段匹配出所有父级节点生成relationParentArr,并取节点key生成激活节点数组。HTML中根据作用域插槽传出的key字段进行匹配,满足条件就设置active样式。这样,用户在点击某个接口时能很清晰的看到接口所在目录。
当用户打开一个已经选择的接口,也需要展开所有父级并选中该接口。于是,采用同样的方式生成选中接口的relationParentArr数据。数组中的key作为expendKeys传给树组件,用以将树节点的展开变成受控模式。与此同时,由于每个节点下都有可能有叶子节点,在展开节点时还需要查该节点关联的叶子节点数据。异步遍历relationParentArr,查询每个节点下的叶子节点数据并concat到children中。当最后一个节点的数据查询完毕,从数据中找到已选中节点。设置selectKeys给树组件选中,调用scrollIntoView将选中的节点滚动到可视区,查询接口详情。
这样,用户能快速定位到接口所在目录,方便使用与排查问题。
关键词:BFS算法、递归、作用域插槽、异步遍历、scrollIntoView
深度细节:(如何完成任务的)
感受和观点:
成就感
接口选择功能使用很频繁,几乎每个组件都需要调用接口。
产品角度上:无论是选中高亮所有父级还是展开已选中接口的所有父级,都能提高用户的配置与问题排查效率。同时,考虑到了滚动条的情况,增加了将接口滚动到可视区方面的细节处理。
技术角度上:学习到的算法和Element方法在业务中有所实践,对JS的异步遍历方法有了进一步的认识。
完成任务的关键
- 抽象思维,将问题转换成对应的数据结构
- 思路清晰,事先理清需求逻辑,对公共的组件和方法抽离、复用,每一步都做到有的放矢
- 日常积累,日常组件开发封装组件或封装方法前都会思考变与不变;会关注别的产品一些细节; ::: 深挖的点:
JS中的树,树的遍历方式:BFS DFS 前序 中序 后序
- JS中异步循环的方法
- 递归与尾递归
地图图层菜单功能开发及重构
重点:组件封装、二级菜单的自定义指令
背景:地图图层控制菜单(以下简称图层菜单)是大屏中不可或缺的部分,承载着与地图图层交互的功能。图层菜单有两种类型:一种是基础显隐,只有显示和隐藏两种状态,控制它关联图层的开启和关闭;一种是多选菜单,这种情况下,图层菜单无法点击,但是鼠标移入时会出现一个二级菜单,这个二级菜单具有复选功能,可以执行单选、多选 、全选等逻辑。此外,为了适配项目的设计风格,我们推出了5种样式风格的图层菜单组件,这些组件除了菜单样式和二级菜单样式不同外,核心逻辑并无差别,开发人员为了图“快”直接CV的第一个菜单组件代码,然后改改样式。
后来,图层菜单的配置逻辑做了升级改造,下游组件应用时也要随之升级。我作为升级人员看到一模一样的代码要改5次,就加班把菜单组件进行拆分、提取、复用,让自己早点下班。
此外,二级菜单的显示逻辑是鼠标移入某个一级菜单上时显示,采用的是css的伪类hover控制。这种控制方式需要用户在使用时很小心,稍有不慎就会出现用户还没点击二级菜单就隐藏的情况。
为了解决以上问题,我对菜单组件做了升级改造。
任务:
- 对5种类型的菜单组件进行升级,只需要定义样式,核心代码直接复用
- 对二级菜单触发与关闭的方式进行改造,让用户使用起来更加方便
行动:
- 新增common、components、config目录
- common中存放的是菜单组件的公用方法和interface等
- components中存放的是公共组件
- config中存放的是交互配置文件
- 抽离公用的图层菜单组件和多选菜单组件
- 图层菜单组件主要负责接受上游数据并渲染视图,以及处理菜单的交互、多选菜单的显隐控制等功能
- 多选菜单组件主要逻辑是图层的全选、多选和单选。组件的背景图以插槽的形式扩展。
- 5种业务组件内部引入公用组件,实现自己的样式
- 主菜单样式可以直接使用css
- 二级菜单样式比较复杂,因为二级菜单通常具有一个不规则的背景图。由于二级菜单关联的图层数量不定,因此二级菜单的高度是未知的。如果使用CSS的background或者HTML的img标签引入背景图,往往会导致图片或拉伸或缩小变形,影响展示效果。我的解决方案是封装一个背景组件,该组件基于svg绘制背景。当获取到二级菜单的宽高后,将其宽高和边框、背景颜色等数据传入组件中,根据设计稿中的背景图效果绘制svg的path。
- 更改二级菜单的触发逻辑,由hover改成onmouseover触发。菜单的关闭逻辑并没有使用onmouseout。因为二级菜单是基于主菜单的绝对定位且二者直接有一定间距,如果使用onmouseout事件,鼠标在从主菜单移入二级菜单的过程中遇到中间的间距部分,二级菜单就会隐藏,除非你快速移入二级菜单。为了解决这个问题,在鼠标移入时触发二级菜单,移出不做任何更改。可以在二级菜单中点击选项做图层控制,也可以鼠标移入其他菜单展示出它的二级菜单。只有在点击了 图层菜单组件以外的元素时才会关闭二级菜单。那么如何实现这种效果呢?如何复用这种效果?
- 开发自定义指令v-clickoutside。指令表达式接受一个函数。在bind的逻辑中定义documentHandler方法,使用contains方法判断点击元素是否在目标元素内部。如果不是则调用外部函数并执行。将documentHandler方法的引用保存在元素的DOM对象中,使用addEventListener监听click事件触发documentHandler方法。
- 在unbind中使用removeEventListener移除定义的documentHandler方法,避免造成内存泄漏。删掉元素中保存的documentHandler方法的引用
结果:
:::info
表层事实:(用了哪些新技能或工具)
为了解决图层控制菜单中因代码CV导致的难以维护问题,梳理了组件之间的差异性,抽离公用的菜单组件以及多选菜单组件。业务中使用的菜单组件由二者拼装而成。菜单的样式和多选菜单的样式由业务组件内部定义。结果表明,如果遇到菜单逻辑更改,只需要更改公共组件即可;新增业务菜单时只需要开发样式;减少代码体积的同时提高了开发效率。
为了解决以伪类hover触发多选菜单组件造成的组件误关闭问题,采用onmouseover事件根据当前移入的菜单id触发二级菜单。考虑到二级菜单特殊的定位方式以及业务需求,开发了自定义指令v-clickoutside,判断鼠标点击事件的target是否在菜单组件内部,如果在外部就关闭多选菜单。结果表明,用户在操作二级菜单时鼠标可以随意移动,只有点击了非菜单组件二级菜单才会消失,增强了用户体验。
关键词:组件封装、鼠标事件、自定义指令
感受和观点:
- 成就感
菜单的核心逻辑更改只需要维护一份代码,业务组件只需要开发样式。减少了代码体积,提高了工作效率。
- 完成任务的关键
- 坚持底线,不做无意义的CV。前期埋的坑总会有人填,不是你就是其他接手的人。
:::
地图信息窗功能
重点:外框的绘制;样式配置、数据处理及交互配置地图工具栏功能
重点:根据工具个数动态生成,1/4圆和半圆。参数方程确定图标的位置
背景:工具栏组件用于存放地图的辅助工具。鼠标移入工具栏图标时,会显示出一个扇形圆环(扇环)状的背景块,背景块上放着用以表示工具的图标元素。 出于配置化的需求,组件的宽高和配色不定,扇环背景块不能使用图片实现。其次,针对不同数量的图标,扇环背景范围也有所不同,图标数小于等于4个时应该展示1/4扇环,大于4小于等于8时,应该显示1/2扇环。而且,工具栏中的图标元素要位均匀分布在扇环上。因此,这里需要完成如下任务:
任务:
- 根据传入的工具个数动态生成工具栏扇环样式
- 在圆环形的工具栏扇环中确定图标元素位置
行动:
- 使用canvas绘制扇环背景。
- 根据组件宽度w计算扇环的宽度s,s = 2/7 * w;
- 确定圆心坐标(x,y),及外圆半径r1,内圆半径r2,两侧圆半径r3;
- 根据工具栏个数确定扇环结尾处的角弧度,如果是4个以下则角弧度为π,4个以上8个以下为π/2;
- 使用canvas的arc API绘制4个部分的圆弧;
- 计算每个图标在扇环上的角度,根据圆的参数方程计算图标元素的相对坐标
- 图标的位置应该始终位于扇环中间,首先计算扇环的平分圆半径r4
- 对于均匀分布问题,假如有n个图标,那么就是将扇环分成了n-1块,根据扇环的起始角度,计算每一块的角弧度,然后根据圆的参数方程计算元素在圆上的坐标。
- 图标的位置采用绝对定位,上一步计算的坐标其实是图标左上角的位置;还需要减去图标宽/高的一半才是图标的实际位置。
- 给图标元素和扇环增加显隐动画
- 图标元素和扇环的显隐是通过鼠标移入移出工具栏图片控制的
- 获取图片的中心点位置坐标,将图标元素的起点位置设置成图片中心点的坐标。给图片增加hover效果和mouseover事件,鼠标移入时mouse事件控制图标元素的坐标数据,hover事件控制图标元素的动画效果。transform rotate控制图标旋转, transition控制动画效果。
- 扇环背景的动画是放大效果,默认transform scale为0,鼠标移入到图片将scale设置为1。transition 给scale增加动效。注意,要给扇环设置transform-origin的值为图片中心点,保证扇环放大缩小都是基于图片进行。
结果:
:::info
表层事实:
为了解决在配置化的过程中,地图工具栏的扇环背景块的绘制问题。采用canvas方案根据工具栏组件的宽高和配色绘制扇环背景。为了解决图标元素在扇环背景上的分布问题,对扇环的圆心角按照图标的个数n分成n-1块,计算每一块的角度,利用圆的参数方程求解图标元素位置,考虑到图标元素采用绝对定位定位,在计算left和top的距离时,减去了图标元素宽/高的一半,保证图标能在扇环中间显示。使用transform和transition给图标和扇环增加动画效果。
关键词:canvas 圆参数方程 动画
感受和观点:
- 成就感
- 使用canvas生成扇环,适用于配置化业务场景。不同项目中使用只需要设置宽高和主题色即可。不用重复使用图片
- canvas知识能够得到实践,尤其是参数方程的使用
- 完成任务的关键
- 细心。无论是扇环的绘制,还是图标元素坐标的计算,需要沉下心来去思考,需要动笔去演算。
:::
页面通用组件封装
重点:组件封装的注意事项,分业务组件和独立组件页面返回逻辑
重点:Vue路由钩子函数的应用,不用详细讲hook开发
重点:作用、实现的流程。extend 和$mounte配置面板组件
Select的实现逻辑
input相关组件实现逻辑
单选与多选的逻辑
Tab组件的逻辑
slider的逻辑
折叠面板
图片上传
图片混合
可视化物料中心之自定义形状堆叠图
背景:物料中心是平台最基础的模块,包含可视化大屏所需的图表组件和自主开发的组件。通过定义标准的DSL协议,将物料中心的组件接入画布编辑器中使用。一般的柱形(条形)图组件使用Echarts实现,随着用户的审美水平不断提高,普通形状的图表越来越无法吸引用户眼球。于是,开发一种支持自定义形状,并且具有堆叠效果柱形(条形)图组件迫在眉睫。考虑到象形图不具备堆叠效果,因此需要考虑其他实现方案。通过查阅文档,ZRender中定义了extendSharp和registerSharp API,支持开发者自定义图形的渲染逻辑,再结合Echarts中的自定义系列(custom series)展示。
任务:
- 细心。无论是扇环的绘制,还是图标元素坐标的计算,需要沉下心来去思考,需要动笔去演算。
:::
- 基于ZRender中的API实现自定义堆叠图形渲染逻辑,使之支持自定义形状渲染、宽度与间距可配置
行动:
- 定义Echarts自定义类型的renderItem函数
这个函数接收一个使用registerShape方法注册的自定义形状名称数组,返回一个匿名函数赋值给自定义类型的renderItem属性
匿名函数中包含了图形渲染逻辑,函数第一个参数params包含了系列index和数据项的index等数据;第二个参数api是一个对象,包含了一系列方法。该函数的返回值是一个type为自定义形状类型的对象。
根据api参数中的coord方法,获取数据在画布上的映射坐标(x,y)、画布的起点坐标等信息。将这些信息存放在一名函数返回对象的shape属性中,以供自定义图形绘制逻辑中的buildPath函数使用。
- 定义自定义形状绘制函数
该函数接收表示图形高度与间距的参数,调用extendShape方法,返回一个新的shape class。
extendShape方法接受一个对象,对象中包含buildPath属性,该属性的值是一个函数,包含ctx参数和shape参数。ctx参数是CanvasRenderingContext2D的实例,提供了canvas相关API。shape参数是上一步renderItem函数中返回对象中的shape属性,包含了数据的映射坐标等数据。
根据renderItem函数返回对象中的shape中数据的映射坐标与画布的起点坐标,结合传入的图形高度,计算出图形的四个角坐标。使用ctx参数中的canvasAPI绘制自定义形状。
- 堆叠效果的实现
上一步中我们发现,所有数据的起点都是相同的,都是数据0在画布中的映射坐标。这样的效果不是堆叠 而是重叠。要想实现堆叠效果,必须要对绘制的图形进行平移。因此在计算图形四个角坐标之前,需要计算出这个图形的平移距离。
定义一个m行n列的矩阵,m表示系列,n表示数据,用以存放所有系列中每个数据在图表中的映射坐标。由于renderItem函数调用是优先于形状绘制函数的,因此我们再renderItem函数中对矩阵进行赋值。刚刚提到renderItem函数中的params参数中包含该数据项的系列index和数据项index。因此我们可根据这两个index生成一个二维数组,数组中存放的是当前数据在画布中的映射坐标。
在计算图形四个角的坐标之前,根据该数据所处的系列index和数据项index,从二维数组中不断找出他之前所有系别中同数据项index的数据的映射坐标。这些映射坐标之和就是该图形的偏移距离(如果传入了间距还需要加上间距)。
结果:
:::info
表层事实:
为了解决Ecahrts中堆叠条形图无法使用自定义形状的问题,考虑到象形图不支持堆叠效果,使用ZRender提供的自定义形状绘制方法,结合Echarts的自定义类型custom实现。
首先,定义自定义类型的returnItem函数,利用函数中api参数的coord方法确定每个数据在画布中的映射位置以供自定义形状绘制逻辑使用。同时为了处理堆叠问题,事先定义一个以系列index为行,以数据项index为列的矩阵,用以存放每个数据的映射坐标。
然后,定义自定义形状生成函数,在buildPath方法中利用数据的系列index和数据项index结合刚刚的矩阵,计算其在画布上的偏移距离。结合数据的映射坐标和画布起点坐标以及条形图高度计算图形四个角的坐标。最后利用buildPath方法提供的ctx实例绘制自定义形状。
结果表明,相比于以往传统的堆叠效果,采用自定义形状的组件更能体现专题特色并且不需要重复开发组件,用户只需要提供图片或者自定义形状的绘制方法即可。
关键词:ZRender Echarts 堆叠图 自定义形状
感受和观点:
成就感:
- 技术上,使用Echarts的自定义类型开发,对堆叠图的绘制方式有了进一步理解
- 业务上:
- 自定义形状图表特征鲜明,可以加入与专题相关的元素,在表现形式上更具有竞争力
- 能根据坐标绘制更多丰富的图形,不需要重新开发组件
完成任务的关键:
- 技术积累:之前使用自定义类型开发过伪3D图,就是绘制三视图实现。
- 做事比较有耐心,实现的过程中涉及到很多计算
:::
输出的公共方法、指令
自定义指令clickoutside
自定义指定showtootip
颜色转换方法 rgba与hex互转
核心功能梳理
大屏组件打包与导入逻辑
搞定大屏组件拖拽与页面生成逻辑