讲讲项目里的仪表盘编辑器(二)

应用场景

        正常来说,编辑器应用场景应该包括:

  •         编辑器-预览
  •         编辑器
  •         最终运行时    

怎么去设计

        

        上一篇推文,我们已经大概了解了编辑器场景。接下来,我们来看预览时的设计

编辑器-预览

        点击预览按钮,执行以下逻辑:

  /** @name 预览 **/async handlePreview() {...// 打开抽屉组件,并往里面放置运行时模块createDrawer(h => h(DashboardRuntime, { props: { dashboard: this.form } }),{title: '预览仪表盘',width: 'calc(100vw - 200px)',},);}

        也就是说:

        所以我们直接关注运行时表现

运行时设计

<template><HabitContext :habitKey="habitKey" @init="habitContextInit"><!-- loading框 --><a-spin v-if="loading" /><divv-else:style="styleCSSVariable"><background :background="themeBackground" :class="$style.background"><grid-layout v-bind="layoutProps"><dashboard-itemv-for="field in fields":key="field.pkId"/></grid-layout></background></div></HabitContext>
</template>

         这里套了一层HabitContext框架,是用来应用和记录用户习惯的(后面讲)。a-spin是加载层。紧接着和设计器差不多,局部变量样式集里面套了个背景框架和grid-layout布局。

        我们再看看dashboard-item的实现:

<template><grid-itemv-bind="layout"static>...</grid-item>
</template>

         这里通过v-bind动态传入grid-item的属性(也就是拣选出来的x/y/w/h这些)。同时用static固定gird-item,使其无法缩放、拖动、被其他元素影响。

<template><grid-itemv-bind="layout"static><divv-if="showChart">...</div><!-- 没权限显示占位图 --><divv-elsestyle="height: 100%; width: 100%; display: flex; flex-direction: column"><div><span :style="titleCss">{{ field.name }}</span></div></div></grid-item>
</template>

        这里就是简单的做了一个占位

<template><grid-itemv-bind="layout"static><divv-if="showChart"><div :class="$style.action"><template v-for="action in actions"><a-tooltip:key="action.key"placement="bottom":mouseLeaveDelay="0":title="action.name"><x-icon:type="action.icon"@click="execAction(action)"/></a-tooltip></template></div><component:is="component":field="field"/></div><!-- 没权限显示占位图 --><divv-elsestyle="height: 100%; width: 100%; display: flex; flex-direction: column"><div><span :style="titleCss">{{ field.name }}</span></div></div></grid-item>
</template>

        浮层按钮还有具体的图表组件

数据流设计

        到这里,我们已经看完了编辑器功能的大概设计。接下来该写写这套系统最核心的部分,数据流设计了。

        创建一个仪表盘编辑器

        点下新增按钮后,我们传入一些系统参数【应用id,功能类别等等,在这里我们并不需要关注】储存新建仪表盘在系统的位置和属性。

        在接口储存完这些系统信息后,跳转到仪表盘页面进行最为关键的仪表盘初始化数据生成。

async handleAddForm(category) {// 弹窗让填写名称、图标等基础信息const result = await GroupForm.createModal({data: { parentId: this.groupId, appId: this.appId, category },},{title: this.getCategoryName(category),width: '427px',},);// 调用接口保存const formId = await add(result);this.$message.success(this.$t('common.tips.add'));// 保存完毕后跳转到页面switch (category) {case FormCategoryType.DASHBOARD:return this.$router.push(`/dashboard-design/${formId}`);...default:return this.$router.push(`/form-design/${formId}/form`);}}

        这里是通过vue-router进行跳转。这里也简单贴出路由代码

import DashboardDesign from '@/views/dashboard-design';const DashboardDesignRoutes = [{path: '/dashboard-design/:id',component: DashboardDesign,},...
];export default DashboardDesignRoutes;

        到这里结束,一个仪表盘编辑器已经创建完毕了。它只存储了系统数据,没有仪表盘的初始数据。而当我们进入仪表盘编辑器页面的时候,完成有效编辑之后,才会以正式数据存储下来

        当然这里指的是前端数据,后端还是会根据我们穿进去的系统参数生成一份默认的接口向的仪表盘数据模板(比如默认权限、默认刷新时间上面的)

     

        进入仪表盘编辑器页面 

        先通过后端接口,拿到当前仪表盘编辑器id的接口数据

@formDesignModule.Action init;
async created() {...await this.init(this.formId).then(() => {...}
}

        大概长这样,记录一些系统信息或默认属性 。这里的init是vuex的action操作。为了是把数据保存到前端本地。更多关于本项目的vuex方法请看我另外一篇文章的介绍

讲讲项目里的状态存储器vuex_AI3D_WebEngineer的博客-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/weixin_42274805/article/details/133237271?spm=1001.2014.3001.5501

         看看这个init的actions做了什么?

actions: {async init({ commit }, formId) {const form = await getFormData(formId);commit('saveForm', form);}
}
mutations: {saveForm(state, data) {state.form = data;...state.loading = false;state.changed = false;}
}

          这里是通过调用接口获取当前仪表盘的数据,并把它存到当前的formDesignModule,也就是formDesign这个命名空间的仓库里。

        

        仪表盘编辑页面的状态管理器

        我们刚刚看到了代码,在编辑页面created里我们执行了init。其实就是非显示地获取数据。吧获取数据的过程从页面隐式地放到了状态管理器里的actions里面。并通过state返回关注的数据。这样子无论我们在仪表盘功能里怎么去跳转页面,都不需要再重新调用接口了,而是直接从仓库里拿。

  @formDesignModule.State form;@formDesignModule.Action init;@formDesignModule.State loading;@formDesignModule.State selectedField;@formDesignModule.Getter fields;@formDesignModule.Mutation updateSelectedField;@formDesignModule.Mutation selectField;@formDesignModule.Mutation updateSetting;@formDesignModule.Mutation saveForm;@formDesignModule.Mutation updateDashboardConfig;@formDesignModule.Action save;

        大概有这些属性和方法来完成编辑器的功能实现。看看就行了。紧接着我们来讲其中一些实现

        点击添加组件到仪表盘

        有两种添加方法:

        ① 点击组件按钮添加

        ② 拖拽组件添加

        点击组件


handleClickAdd() {...// 初始化layoutconst layout = getDashboardLayoutByType(type);const layoutList = ensureArray(this.$refs.container.layout);layout.x = (layoutList.length * 2) % 60;layout.y = layoutList.length + 60;field.widget.layout = layout;// 初始化风格field = this.initFieldStyle(field);
}

        getDashboardLayoutByType是根据你点击的组件生成默认的组件layout数据。比如图片组件定义的默认layout是:

export function getDashboardLayoutByType(type) {const layout = getDashboardControlMeta(type, 'layout');return { x: 0, y: 0, ...(typeof layout === 'function' ? layout() : layout) };
}

         这时返回了一个初始化的layout即{w:30,h:15,minH:7,x:0,y:0}。

     const layoutList = ensureArray(this.$refs.container.layout);这里是直接获取设计器组件里面的layout属性(它的data值)。这个layout目前是个空数组(因为是新建的仪表盘,里面没有组件)。

layout.x = (layoutList.length * 2) % 60;
layout.y = layoutList.length + 60;
field.widget.layout = layout;// 更新布局
this.$refs.container.syncLayout();

        很好理解啦,我们吧初始化layout的横纵坐标调整到它应该在的位置上,并吧这个调整过的layout信息存储到新增组件的布局属性里(替换掉初始化layout)。讲讲为什么这么计算:        

        可以看到实例中这两个组件的x/y值并不像上面这个逻辑计算出来的。 如果按照上面那个逻辑计算出来,则应该是{x:0,y:60...}和{x:2,y:61...}。其实这个计算过程是为了保证第n+1个组件的x和y一定大于第n个。从而避免重叠出错,而至于精准的layout数据,是借助vue-layout-grid插件行自适应生成。具体怎么做,我们看代码:

  /** @name 同步layout **/syncLayout() {this.layout = ensureArray(this.fields).map(field => ({...field.widget.layout,i: field.pkId,}));}
<grid-layoutref="layout":class="$style.layout":layout.sync="layout"
>...
</grid-layout>

        很多人看到这里就要骂了,骗人,你这不是啥都没干?只是把layout重新赋值了一遍。让我们改下代码看看:

  /** @name 同步layout **/async syncLayout() {this.layout = ensureArray(this.fields).map(field => ({...field.widget.layout,i: field.pkId,}));console.log(this.layout);await this.$nextTick();console.log(this.layout);}

        第一个输出:

[{h: 10,w: 12,x: 0,y: 0},{h: 20,w: 60,x: 2,y: 61}   
]

        第二个输出:

[
{h: 10,w: 12,x: 0,y: 0,i: "39b19b29-c8ef-4fd3-8604-d7e168196ae6"
},
{h: 20,w: 60,x: 2,y: 10,i: "5d684834-26bd-4d35-b7ff-36d8de9d903e"
},
]   

        可以看到此时this.layout已经变了。这是因为<grid-layout>已经自适应了布局。

        由此,我们的保存仪表盘布局方法也呼之欲出了:

save() {// 拿到同步后的this.layoutconst layout = this.$refs.container.layout;// 生成组件id和layout信息的映射表const layoutMap = generateMap(layout, 'i', item =>pick(item, 'x', 'y', 'w', 'h'),);...
}

        先看到这里,这里要生成一份类似于:'amdous123623': {w:10,h:20,x:0,y:0...}这样的映射表,是整个仪表盘布局的储存并不是直接存储类似于girdLayout的这种数组,而是由一个个组件自身的layout属性(甚至无视组件排序)拣选出来生成this.layout。也就是说仪表盘的存储结构为Array<field>这样的。

save(fields) {const layout = this.$refs.container.layout;const layoutMap = generateMap(layout, 'i', item =>pick(item, 'x', 'y', 'w', 'h'),);this.privateUpdateFields((fields || this.fields).map(field => {if (!layoutMap[field.pkId]) return field;return {...field,widget: {...field.widget,layout:{...field.widget.layout,...layoutMap[field.pkId],}},};}),);}

        拖拽添加组件到仪表盘

        前面我们已经讲了拖拽添加组件的思路,和预防错位或重叠的处理。现在来讲讲具体代码实现。

        之前讲过了,在control-list.vue也就是左边的组件列表拖拽出组件,触发@dragstart方法,同时往设计器里传入dragType。设计器里根据dragType找对对应的组件初始化layout

@Watch('dragType')handleDragTypeChange(type) {this.isInChildCom = false; // 重新拖动需要重置if (type) {this.dragLayout = {i: 'drag',...getDashboardLayoutByType(type),};} else {this.dragLayout = null;}}

        假设此时拖拽元素已经拖拽到在设计器(也就是gird-layout)上面。触发@dragover.native="handleDrag"

handleDrag(ev) {if (this.isInChildCom) return; // 进入子元素范围则无需触发ev.preventDefault();this._handleDrag(ev);
}
@throttle(100)
_handleDrag(ev) {if (!this.dragType || !this.$el) return;if (this.dragContext.clientX === ev.clientX &&this.dragContext.clientY === ev.clientY)return;this.dragContext.clientX = ev.clientX;this.dragContext.clientY = ev.clientY;this.updateInside(ev);this.updateDrag(ev);
}

         _handleDrag每100秒记录一次拖拽元素的位置,当拖拽元素发生变动时,更新设计器视图。

 updateInside(ev) {if (!this.dragType || !this.$el) return;const rect = this.$el.getBoundingClientRect();const errorRate = 10;const inside =ev.clientX > rect.left + errorRate &&ev.clientX < rect.right - errorRate &&ev.clientY > rect.top + errorRate &&ev.clientY < rect.bottom - errorRate;if (inside && this.dragLayoutIndex === -1) {this.layout.push(this.dragLayout);}if (!inside && this.dragLayoutIndex !== -1) {this.layout.splice(this.dragLayoutIndex, 1);}}

        这里是获取设计器边界的位置属性(errorRate为误差范围,你可以理解为设计器有padding),判断拖拽元素是否在设计器边界内,如果是,就往layout里面加入它(重复则不加入),如果已经超出设计器,则移除。

         我们往编辑器拖拽移动,可以看到这个虚线框会一直跟随变动,可能你们就要问了,上面的代码里dragLayout一但被添加进layout,那么dragLayoutIndex就不会是-1,也就是说layout里面的dragLayout不会改变(x或y)。那这个虚框是怎么还在移动的?

        其实啊,这个虚框并不由layout里的数据决定。而是由vue-grid-layout这个插件负责渲染的。在拖动的时候,this.layout是不会变的。我们只需要每100毫秒记录一次拖拽元素的当前位置this.dragLayout,直到放置生效之后,用this.dragLayout去覆盖this.layout里面的那个被拖动元素。

         所以updateDrag是为了更新this.dragLayout。通过clientY/X换算成vue-grid-layout的x,y

 const dragRef = this.getDragRef();if (!this.dragType || !dragRef) return;const rect = this.$el.getBoundingClientRect();const dragging = {top: this.dragContext.clientY - rect.top,left: this.dragContext.clientX - rect.left,};dragRef.dragging = dragging;const newLayout = dragRef.calcXY(dragging.top, dragging.left);this.dragLayout.x = newLayout.x;this.dragLayout.y = newLayout.y;}
getDragRef() {// vue-grid-layout默认在$children内存在一个组件实例了, 其实每次拖动直接取最后一个实例应该就可以了return this.$refs.layout.$children[this.$refs.layout.$children.length - 1];
}

        当我们放手时,触发 <grid-layout>组件上的drop事件,我们来看看@drop.native="handleDrop"的handleDrop方法

 async handleDrop() {if (this.isInChildCom) return; // 进入子元素范围则无需触发if (!this.dragType) return;...
}

        重叠和空类型直接当做无效动作处理

 async handleDrop() {if (this.isInChildCom) return; // 进入子元素范围则无需触发if (!this.dragType) return;try {...}catch (e) {this.layout.splice(this.dragLayoutIndex, 1);throw e;}finally {this.$emit('update:dragType', null);}
}

         这个try catch我们之前已经讲过了。try里面的逻辑也很简单

try {let field = createDashboardField(this.dragType);...field.widget.layout = pick(this.dragLayout, 'x', 'y', 'w', 'h');...// 更新布局this.layout.splice(this.dragLayoutIndex, 1, {...field.widget.layout,i: field.pkId,});// 提交数据存储this.$emit('add', field);
}

        拖拽移动组件位置

         由插件处理,会自动更新到this.layout

        放大缩小组件

        由插件处理,会自动更新到this.layout

        删除组件

async handleDelete(pkId) {const cloneFields = deepClone(this.fields);// 摘除删除的组件数据this.updateFields(cloneFields.filter(field => {return field.pkId !== pkId;}),);await this.$nextTick();this.$refs.container.syncLayout();
}
 /** @name 同步layout **/async syncLayout() {this.layout = ensureArray(this.fields).map(field => ({...field.widget.layout,i: field.pkId,}));await this.$nextTick();}

        额外讲一下选中组件对组件进行修改

        当我们选中组件的时候,需要在vuex里登记一下当前的选中状态

<grid-itemv-for="layoutItem in layout"...@mousedown.native="handlePointerDown"@mouseup.native="handlePointerUp($event, layoutItem.i)"
>...
</grid-item>

      加了一些位置判断,以防这个组件位置出错或已经不在布局里

  /** @name 鼠标设备按下与抬起事件处理 **/_pointerContext = null;handlePointerDown(ev) {this._pointerContext = {x: ev.clientX,y: ev.clientY,};}handlePointerUp(ev, pkId) {if (!this._pointerContext || !this.fieldMap[pkId]) return;const { x, y } = this._pointerContext;if (x !== ev.clientX || y !== ev.clientY) return;this.selectField(this.fieldMap[pkId]);}@formDesignModule.Mutation selectField;

        再来看看仓库的代码

    // fromdesign.jsselectField(state, field) {state.selectedField = field;},

         如果当前组件的内容或属性发送变更,则执行

 commit('selectField', newField);

        

        

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/146211.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

<C++> 哈希表模拟实现STL_unordered_set/map

哈希表模板参数的控制 首先需要明确的是&#xff0c;unordered_set是K模型的容器&#xff0c;而unordered_map是KV模型的容器。 要想只用一份哈希表代码同时封装出K模型和KV模型的容器&#xff0c;我们必定要对哈希表的模板参数进行控制。 为了与原哈希表的模板参数进行区分…

百度资源搜索平台出现:You do not have the proper credential to access this page.怎么办?

Forbidden site not allowed You do not have the proper credential to access this page. If you think this is a server error, please contact the webmaster. 如果你的百度资源平台&#xff0c;点进去出现这个提示&#xff0c;说明您的网站已经被百度清退了。如果你的网站…

把握现在,热爱生活

博客主页&#xff1a;https://tomcat.blog.csdn.net 博主昵称&#xff1a;农民工老王 主要领域&#xff1a;Java、Linux、K8S 期待大家的关注&#x1f496;点赞&#x1f44d;收藏⭐留言&#x1f4ac; 目录 厨艺房价琐事计划随想 今年的中秋国庆假期放8天&#xff0c;比春节假期…

IIS管理器无法打开。启动后,在任务栏中有,但是窗口不见了

找到IIS管理器启动程序的所在位置 并在cmd命令行中调用 inetmgr.exe /reset 进行重启 先查看IIS管理器属性&#xff0c;找到其位置 管理员模式打开cmd命令行&#xff0c;并切换到上面的文件夹下运行Inetmgr.exe /reset 运行完成后可以重新看到IIS窗口 原因&#xff1a;由于某…

安防监控/视频汇聚平台EasyCVR云端录像不展示是什么原因?该如何解决?

视频云存储/安防监控EasyCVR视频汇聚平台基于云边端智能协同&#xff0c;支持海量视频的轻量化接入与汇聚、转码与处理、全网智能分发、视频集中存储等。音视频流媒体视频平台EasyCVR拓展性强&#xff0c;视频能力丰富&#xff0c;具体可实现视频监控直播、视频轮播、视频录像、…

在Windows11家庭中文版中启用Copilot(预览版)

1、下载ViveTool-vx.x.x.zip 2、解压下载的压缩包ViveTool-vx.x.x.zip 3、复制ViveTool文件夹的路径 4、按下wins&#xff0c;打开搜索 5、输入cmd&#xff0c;并选择“以管理员身份运行” 6、在cmd中输入以下命令&#xff0c;进入ViveTool文件夹&#xff1a; cd ViveTool…

【自监督Re-ID】ICCV_2023_Oral | ISR论文阅读

Codehttps://github.com/dcp15/ISR_%20ICCV2023_Oral 面向泛化行人再识别的身份导向自监督表征学习&#xff0c;清华大学 目录 导读 摘要 相关工作 DG ReID 用于ReID的合成数据 无监督表征学习 Identity-Seeking Representation Learning 结果 消融实验 导读 新角度…

Sentinel学习(2)——sentinel的使用,引入依赖和配置 对消费者进行流控 对生产者进行熔断降级

前言 Sentinel 是面向分布式、多语言异构化服务架构的流量治理组件&#xff0c;主要以流量为切入点&#xff0c;从流量路由、流量控制、流量整形、熔断降级、系统自适应过载保护、热点流量防护等多个维度来帮助开发者保障微服务的稳定性。 本篇博客介绍sentinel的使用&#x…

【设计模式】五、原型模式

文章目录 概述示例传统的方式的优缺点原型模式原理结构图-uml 类图 原型模式解决克隆羊问题的应用实例Sheep类实现clone()运行原型模式在 Spring 框架中源码分析 深入讨论-浅拷贝和深拷贝浅拷贝的介绍 小结 概述 示例 克隆羊问题 现在有一只羊 tom&#xff0c;姓名为: tom, 年…

嵌入式Linux应用开发-基础知识-第十九章驱动程序基石①

嵌入式Linux应用开发-基础知识-第十九章驱动程序基石① 第十九章 驱动程序基石①19.1 休眠与唤醒19.1.1 适用场景19.1.2 内核函数19.1.2.1 休眠函数19.1.2.2 唤醒函数 19.1.3 驱动框架19.1.4 编程19.1.4.1 驱动程序关键代码19.1.4.2 应用程序 19.1.5 上机实验19.1.6 使用环形缓…

十七,IBL-打印各个Mipmap级别的hdr环境贴图

预滤波环境贴图类似于辐照度图&#xff0c;是预先计算的环境卷积贴图&#xff0c;但这次考虑了粗糙度。因为随着粗糙度的增加&#xff0c;参与环境贴图卷积的采样向量会更分散&#xff0c;导致反射更模糊&#xff0c;所以对于卷积的每个粗糙度级别&#xff0c;我们将按顺序把模…

debian设置允许ssh连接

解决新debian系统安装后不能通过ssh连接的问题。 默认情况下&#xff0c;Debian系统不开启SSH远程登录&#xff0c;需要手动安装SSH软件包并设置开机启动。 > 设置允许root登录传送门&#xff1a;debian设置允许root登录 首先检查/etc/ssh/sshd_config文件是否存在。 注意…

Microsoft Office无法重装报错30015-44(3) 0-2031(17004)

1.问题描述 由于迁移文件夹导致Microsoft office软件无法使用&#xff0c;于是准备卸载重装&#xff0c;但是点击OfficeSetup.exe出现报错30015-44(3) 关闭后出现以下报错0-2031(17004) 2. 尝试的解决方式 重启后仍然无法解决问题 2.1 参考官网解决办法 手动从控制面板&…

【Python自动化测试】mock模块基本使用介绍

mock简介 py3已将mock集成到unittest库中为的就是更好的进行单元测试简单理解&#xff0c;模拟接口返回参数通俗易懂&#xff0c;直接修改接口返回参数的值官方文档&#xff1a;unittest.mock --- 模拟对象库 — Python 3.11.4 文档 mock作用 解决依赖问题&#xff0c;达到解…

Node.js 是如何处理请求的

前言&#xff1a;在服务器软件中&#xff0c;如何处理请求是非常核心的问题。不管是底层架构的设计、IO 模型的选择&#xff0c;还是上层的处理都会影响一个服务器的性能&#xff0c;本文介绍 Node.js 在这方面的内容。 TCP 协议的核心概念 要了解服务器的工作原理首先需要了…

大数据Flink(九十四):DML:TopN 子句

文章目录 DML:TopN 子句 DML:TopN 子句 TopN 定义(支持 Batch\Streaming):TopN 其实就是对应到离线数仓中的 row_number(),可以使用 row_number() 对某一个分组的数据进行排序 应用场景

Cloudflare分析第二天:解密返回数据

前言 Cloudflare分析第一天&#xff1a;简单的算法反混淆 由上篇for (j "10|8|5|9|1|4|0|2|3|6|7"["split"](|) 可以看到循环的循序 case 6:o (n {},n["msg"] f,n.cc g,hF["VNwzz"](JSON["stringify"](n))["re…

[C++ 网络协议] 异步通知I/O模型

1.什么是异步通知I/O模型 如图是同步I/O函数的调用时间流&#xff1a; 如图是异步I/O函数的调用时间流&#xff1a; 可以看出&#xff0c;同异步的差别主要是在时间流上的不一致。select属于同步I/O模型。epoll不确定是不是属于异步I/O模型&#xff0c;这个在概念上有些混乱&a…

软件设计师_数据库系统_学习笔记

文章目录 3.1 数据库模式3.1.1 三级模式 两级映射3.1.2 数据库设计过程 3.2 ER模型3.3 关系代数与元组演算3.4 规范化理论3.5 并发控制3.6 数据库完整性约束3.7 分布式数据库3.8 数据仓库与数据挖掘 3.1 数据库模式 3.1.1 三级模式 两级映射 内模式直接与物理数据库相关联的 定…

作为SiteGPT替代品,HelpLook的优势是什么?

在当今快节奏的数字化世界中&#xff0c;企业不断寻求创新方式来简化运营并增强客户体验。由于聊天机器人能够自动化任务、提供快速响应并提供个性化互动&#xff0c;它们在业务运营中的使用变得非常重要。因此&#xff0c;企业越来越意识到像SiteGPT和HelpLook这样高效的聊天机…