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

应用场景

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

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

怎么去设计

        

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

编辑器-预览

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

  /** @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.xdnf.cn/news/148938.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

Spring Cloud Loadbalancer 实现客户端负载均衡

针对 ribbon 负载均衡组件&#xff0c; 官方提出的替换解决方案是 Spring Cloud Loadbalancer。本次主要通过学习示例介绍了 Spring Cloud Loadbalancer 的基础使用。 1&#xff0c;引入pom <dependency><groupId>org.springframework.cloud</groupId><…

React18入门(第一篇)——JSX、TSX语法详解

文章目录 一、JSX 语法简介二、和 HTML 标签的几点不同三、JSX 属性四、JSX 事件4.1 简单点击事件4.2 类型限制4.3 带参数&#xff0c;箭头函数 五、插入 JS 变量六、JSX 中使用条件判断七、循环 一、JSX 语法简介 JSX - 是 JS 的扩展&#xff0c;写在 JS 代码里面&#xff0c…

ROS(0)命令及学习资源汇总

ROS安装命令 参考&#xff1a;Ubuntu20.04.4安装ROS Noetic详细教程 - 知乎 安装C和Python3 sudo apt-get install g sudo apt-get install python3 ROS运行小海龟仿真器 roscore确定ROS是否运行成功rosrun turtlesim turtlesim_node运行小海龟仿真器rosrun turtlesim turtle_…

【微信小程序开发】一文学会使用CSS控制样式布局与美化

引言 在微信小程序开发中&#xff0c;CSS样式布局和美化是非常重要的一部分&#xff0c;它能够为小程序增添美感&#xff0c;提升用户体验。本文将介绍如何学习使用CSS进行样式布局和美化&#xff0c;同时给出代码示例&#xff0c;帮助开发者更好地掌握这一技巧。 一、CSS样式布…

星宿UI2.4资源付费变现小程序源码 支持流量主

第一个小程序为星宿小程序 目前是最新版2.0 搭建星宿需要&#xff1a;备用域名 服务器 微信小程序账号 功能&#xff1a;文章展示 文章分类 资源链接下载 轮播图 直接下载附件功能 很多 很适合做资源类分享 源码下载&#xff1a;https://download.csdn.net/download/m0_6604…

mysql面试题17:MySQL引擎InnoDB与MyISAM的区别

该文章专注于面试,面试只要回答关键点即可,不需要对框架有非常深入的回答,如果你想应付面试,是足够了,抓住关键点 面试官:MySQL引擎InnoDB与MyISAM的区别 InnoDB和MyISAM是MySQL中两种常见的存储引擎,它们在功能和性能方面有一些区别。下面将详细介绍它们之间的差异。…

【算法导论】线性时间排序(计数排序、基数排序、桶排序)

引言&#xff1a;   在排序的最终结果中&#xff0c;各元素的次序依赖于它们之间的比较&#xff0c;我们把这类排序算法称为比较排序&#xff0c;对于包含n个元素的输入序列来说&#xff0c;任何比较排序在最坏情况下都要经过 Ω ( n l g n ) \Omega(nlgn) Ω(nlgn)次比较&a…

学信息系统项目管理师第4版系列17_干系人管理

1. 项目经理和团队管理干系人的能力决定着项目的成败 2. 干系人满意度应作为项目目标加以识别和管理 3. 发展趋势和新兴实践 3.1. 识别所有干系人&#xff0c;而非在限定范围内 3.2. 确保所有团队成员都涉及引导干系人参与的活 3.3. 定期审查干系人群体&#xff0c;可与单…

星际争霸之小霸王之小蜜蜂(十六)--狂奔的花猫

系列文章目录 星际争霸之小霸王之小蜜蜂&#xff08;十五&#xff09;--剧将终场 星际争霸之小霸王之小蜜蜂&#xff08;十四&#xff09;--资本家的眼泪 星际争霸之小霸王之小蜜蜂&#xff08;十三&#xff09;--接着奏乐接着舞 星际争霸之小霸王之小蜜蜂&#xff08;十二…

Lucene学习总结之Lucene的索引文件格式

当我们真正进入到Lucene源代码之中的时候&#xff0c;我们会发现: Lucene的索引过程&#xff0c;就是按照全文检索的基本过程&#xff0c;将倒排表写成此文件格式的过程。Lucene的搜索过程&#xff0c;就是按照此文件格式将索引进去的信息读出来&#xff0c;然后计算每篇文档打…

Android开源 Skeleton 骨架屏 V1.3.0

目录 一、简介 二、效果图 三、引用 Skeleton 添加jitpack 仓库 添加依赖: 四、新增 “块”骨架屏 1、bind方法更改和变化&#xff1a; 2、load方法更改和变化&#xff1a; 五、关于上一个版本 一、简介 骨架屏的作用是在网络请求较慢时&#xff0c;提供基础占位&…

Android LitePal byte[]类型字段不被创建

我创建了以下实体类&#xff0c;主要是用户分享的内容、分享的照片、分享的标题&#xff0c;然后百度了一下LitePal可以识别byte[]&#xff0c;因为需要文件的上传与读取&#xff1a; public class Context extends LitePalSupport {private Integer ContextId;private String…

LLMs 用强化学习进行微调 RLHF: Fine-tuning with reinforcement learning

让我们把一切都整合在一起&#xff0c;看看您将如何在强化学习过程中使用奖励模型来更新LLM的权重&#xff0c;并生成与人对齐的模型。请记住&#xff0c;您希望从已经在您感兴趣的任务上表现良好的模型开始。您将努力使指导发现您的LLM对齐。首先&#xff0c;您将从提示数据集…

用向量数据库Milvus Cloud 搭建AI聊天机器人

加入大语言模型(LLM) 接着,需要在聊天机器人中加入 LLM。这样,用户就可以和聊天机器人开展对话了。本示例中,我们将使用 OpenAI ChatGPT 背后的模型服务:GPT-3.5。 聊天记录 为了使 LLM 回答更准确,我们需要存储用户和机器人的聊天记录,并在查询时调用这些记录,可以用…

软件设计模式系列之二十五——访问者模式

访问者模式&#xff08;Visitor Pattern&#xff09;是一种强大的行为型设计模式&#xff0c;它允许你在不改变被访问对象的类的前提下&#xff0c;定义新的操作和行为。本文将详细介绍访问者模式&#xff0c;包括其定义、举例说明、结构、实现步骤、Java代码实现、典型应用场景…

软件工程从理论到实践客观题汇总(头歌第九章至第十七章)

九、软件体系结构设计 1、软件体系结构设计概述 2、软件体系结构模型的表示方法 3、软件体系结构设计过程 4、设计初步的软件体系结构 5、重用已有软件资源 6、精化软件体系结构 7、设计软件部署模型 8、文档化和评审软件体系结构设计 十、软件用户界面设计 1、用户界面设计概…

Ae 效果:CC Page Turn

扭曲/CC Page Turn Distort/CC Page Turn CC Page Turn &#xff08;CC 翻页&#xff09;主要用于模拟书页翻动的效果。通过使用该效果&#xff0c;用户可以创建出像书页或杂志页面翻动的视觉效果&#xff0c;增强影片的交互性和视觉吸引力。 ◆ ◆ ◆ 效果属性说明 Contro…

Is This The Intelligent Model(这是智能模型吗)

Is This The Intelligent Model 这是智能模型吗 Ruoqi Sun Academy of Military Science Defense Innovation Institute, Beijing, 100091, China E-mail: ruoqisun7163.com The exposed models are called artificial intelligent models[1-3]. These models rely on knowled…

第2篇 机器学习基础 —(1)机器学习方式及分类、回归

前言&#xff1a;Hello大家好&#xff0c;我是小哥谈。机器学习是一种人工智能的分支&#xff0c;它使用算法和数学模型来使计算机系统能够从经验数据中学习和改进&#xff0c;而无需显式地编程。机器学习的目标是通过从数据中发现模式和规律&#xff0c;从而使计算机能够自动进…

Leetcode 231.2的幂

给你一个整数 n&#xff0c;请你判断该整数是否是 2 的幂次方。如果是&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 如果存在一个整数 x 使得 n 2x &#xff0c;则认为 n 是 2 的幂次方。 示例 1&#xff1a; 输入&#xff1a;n 1 输出&#xff1a;tr…