讲讲项目里的仪表盘编辑器(四)分页卡和布局容器组件
讲讲两个经典布局组件的实现
① 布局容器组件
配置面板是给用户配置布局容器背景颜色等属性。这里我们不需要关注
定义文件
规定了组件类的类型、标签、图标、默认布局属性、主文件等等。
// index.js
import Container from './container.vue';
class ContainerControl extends BaseControl {type = 'container';label = '布局容器';icon = 'tc-icon-layout';...layout = {w: 30,h: 15,minH: 8,};// 组件实现的主文件DashboardComponent = Container;
}export default new ContainerControl();
入口文件会通过一系列逻辑生成【类型枚举类】,我们最后通过control['container'].DashboardComponent找到主体文件生成组件。这些我们简单了解就好啦。
具体来看看container.vue文件。
组件主体
// container.vue
<template><drag-containerv-bind="fieldProps"@inChildComponent="$emit('inChildComponent', $event)"@add="handleAdd"@delete="handleDelete"@drop="syncDataToStore('add', $event)"><drag-container-layoutv-bind="fieldProps":layout.sync="layout":fields="fields"@resized="syncDataToStore('size', $event)"@moved="syncDataToStore('location', $event)"@edit="syncDataToStore('edit', $event)"@delete="syncDataToStore('delete', $event)"@select="handleSelect"/></drag-container>
</template>
这里的drag-container其实长这样:
// drag-container
<template><div@dragenter="dragenter"@dragover="dragover"@dragleave="dragleave"@drop="drop"><slot /></div>
</template>
是不是很熟悉?对,就是上一章讲的包裹着组件的drag事件层。用来触发inChildComponent事件的。
drag-container-layout其实就是一个 grid-layout。有运行时和设计时两种情况(设计时可以拖拽组件进去,运行时只是纯展示)
// drag-container-layout.vue
<template><grid-layout:layout.sync="layout":col-num="60":row-height="15":isDraggable="!isRuntime":isResizable="!isRuntime":useCssTransforms="!isRuntime"><template v-for="layoutItem in layout"><!-- 运行时 --><componentv-if="isRuntime":is="Item":key="layoutItem.i"v-bind="getComponentProps(layoutItem)"/><!-- 设计时 --><grid-itemv-else:key="layoutItem.i"v-bind="getLayoutProps(layoutItem)"@moved="$emit('moved', layoutItem)"@resized="$emit('moved', layoutItem)"@mousedown.native.stop="handlePointerDown"@mouseup.native.stop="handlePointerUp($event, layoutItem.i)"><component:is="getComponent(layoutItem)"v-bind="getComponentProps(layoutItem)"@deleteComponent="handleDelete({ i: $event })"/></grid-item></template></grid-layout>
</template>
添加组件
上一节我们已经将过点击添加到布局组件内,所以这节主要展开讲讲拖拽。逻辑跟上一节会有一些不一样,上一节主要还是为了方便理解。
拖拽组件进入布局组件内部时,drag-container层首先响应。触发dragenter事件
/** @name 进入-有效目标 **/dragenter() {if (this.limit) return;this.$emit('inChildComponent', true);}
当拖拽进来的组件是布局组件时,this.limit为true。这里的业务逻辑是不允许多层嵌套所以在这里做了阻断。此时不会给外界传递inChildComponent事件,仪表盘的gird-layout也不需要改变this.isInChildCom。这里跟上一节讲的不一样,是因为vue-grid-layout这个组件本身不允许组件之间重叠(组件是有碰撞体积的)。所以即使它进入到布局组件内,布局组件内不接管,也会被插件阻拦。
同时触发dragover事件,为了定位拖拽的组件在布局组件内的位置
** @name 移动-有效目标 **/
dragover(e) {if (this.limit) return;e.preventDefault();e.dataTransfer.dropEffect = 'copy';this._dragover(e);
}@throttle(100, { trailing: false })
_dragover(e) {if (this.dragContext.clientX === e.clientX &&this.dragContext.clientY === e.clientY)return;// 时刻记录鼠标的位置this.dragContext.clientX = e.clientX;this.dragContext.clientY = e.clientY;this.updateInside(e);this.updateDrag(e);
}/** @name 拖拽上下文,用于记录鼠标位置 */
dragContext = {clientX: 0,clientY: 0,
};
updateInside是为了在拖动的时候更新布局组件内的布局,让拖动元素在布局组件内部形成占位符。这一点在之前几章我都没讲过,是因为vue-grid-layout这个组件对拖拽效果已经做了很好的处理了,此时加上拖拽时占位,只不过是锦上添花的效果罢了。
/** @name 判断拖动元素是否在拖动区域内,是则添加一项(占位符),否则删除一项 **/updateInside(ev) {// 获取布局组件内部区域位置大小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 (this.dragLayout) {if (inside) {this.$emit('add', deepClone(this.dragLayout));} else {this.$emit('delete', deepClone(this.dragLayout));}}}
add和delete最终指向是操作drag-container-layout.vue里的this.layout这个属性,也就是布局容器内的布局(add操作会查找this.layout是否重复存在这个拖拽元素)。可以理解为dragover操控更新了布局容器内的布局,而一旦dragleave,则会:
①取消接管仪表盘layout层的拖拽事件。恢复到仪表盘layout层进行接管
②更新布局组件内部
/** @name 离开-有效目标 **/
dragleave(e) {if (this.limit) return;this.$emit('inChildComponent', false);this.updateInside(e);
}
那么最最最关键的一环,无非是drop事件了。它的核心思路是把布局容器当前的layout里的draglayout拿出来,将它的位置属性记录在生成的拖拽组件属性中。并抛出到vuex仓库里进行存储。如果失败,也只需要删除视图层layout里的dragLayout组件罢了。
/** @name 放置-有效目标 **/async drop() {if (this.limit) return;const dragLayout = deepClone(this.dragLayout);try {let field = createDashboardField(this.dragType);// 标记组件为子组件field.parentId = this.field.pkId;// 布局field.widget.layout = pick(dragLayout, 'x', 'y', 'w', 'h');// 添加到layoutthis.$emit('add',{...field.widget.layout,i: field.pkId,},dragLayout.i,);this.$emit('drop', field);} catch (e) {this.$emit('delete', dragLayout);throw e;}}
<drag-container...@drop="syncDataToStore('add', $event)"
>
</drag-container>
这个syncDataToStore方法会吧数据同步到vuex仓库,包括了新增/删除/变化。我们最后再讲。到这一步,我们已经把视图层关于新增的步骤完成了。
删除组件
// drag-container.vue
/** @name 删除 **/
handleDelete(layout) {this.$emit('delete', layout);
}
// container.vue
<drag-container-layout...@delete="syncDataToStore('delete', $event)"/>
放大缩小组件/ 改变位置
vue-grid-layout负责抛出
<template v-for="layoutItem in layout"><grid-item...@moved="$emit('moved', layoutItem)"@resized="$emit('sized', layoutItem)">...</grid-item>
</template >
这里很巧妙的运用了this.layout属性,vue-grid-layout的官方示例用法是这样的:
可以理解为这两个响应事件是返回了新的位置信息。而项目里的写法是利用了vue-grid-layout在moved或resized之后自身的this.layout也会随着改变,里面的layout-item也会跟随动态变化,所以直接把layout-item当做参数传出
// container.vue<drag-container-layoutv-bind="fieldProps":layout.sync="layout":fields="fields"@resized="syncDataToStore('size', $event)"@moved="syncDataToStore('location', $event)"@delete="syncDataToStore('delete', $event)"/>
和添加组件一样,视图层逻辑到此结束,等待数据层处理
数据层处理
每个项目都有自己的处理方式,到这里视图层已经完成了自己的使命,把数据教辅给数据层进行存储变更。所以参考一下就行啦
/*** @name 同步到store* @param { String } type: 添加-add、删除-delete、大小变化-size、位置变化-moved* @param { Object } value: field、layout**/async syncDataToStore(type, value) {this.updateFields(fields => {const currentField = fields.find(field => field.pkId === this.field.pkId);const currentWidget = currentField.widget;if (type === 'add') {// 布局组件里面存储普通组件的字段currentWidget.fields.push(value);} else if (type === 'moved' || type === 'size') {// 移动会改变其他元素的位置, 所以整体要重复赋值x,yconst layoutMap = generateMap(this.layout, 'i', layout => layout);currentWidget.fields.forEach(field => {field.widget.layout = pick(layoutMap[field.pkId], 'x', 'y', 'w', 'h');});} else if (type === 'delete') {const index = currentWidget.fields.findIndex(item => item.pkId === value.i,);currentWidget.fields.splice(index, 1);}return fields;});if (type === 'delete') {await this.$nextTick();// 记得更新视图,add就不用了,因为在dragover的时候已经更新了this.layout了this.syncLayout();}}
特别注意的是,移动位置或 更改大小需要更新容器内所有组件的位置,因为可能会发生挤压或换行。
区分父容器和布局容器里的点击事件
<grid-item
@mousedown.native.stop="handlePointerDown"></grid-item>handlePointerDown(ev) {// 防止和父级选中冲突setTimeout(() => {this._pointerContext = {x: ev.clientX,y: ev.clientY,};});}
settimeout(fn,0)会让方法在在下一轮“事件循环”开始时执行。从而避免与父容器冲突。
② 分页卡
跟布局容器一样,只是数据存储多了一层嵌套
相关文章:

讲讲项目里的仪表盘编辑器(四)分页卡和布局容器组件
讲讲两个经典布局组件的实现 ① 布局容器组件 配置面板是给用户配置布局容器背景颜色等属性。这里我们不需要关注 定义文件 规定了组件类的类型、标签、图标、默认布局属性、主文件等等。 // index.js import Container from ./container.vue; class ContainerControl extends…...
Qt模块、Qt开发应用程序类型、Qt未来主要市场、Qt6功能普及
Qt模块、Qt开发应用程序类型、Qt未来主要市场、Qt6功能普及 文章目录 1.Qt核心模块2.Qt的功能拓展3.Qt未来主要市场4.Qt6功能普及5.弃用的功能: Qt是一个跨平台的应用程序开发框架,提供了丰富的模块和工具来开发各种类型的应用程序。以下是Qt目前已有的…...
nodejs+vue高校校图书馆elementui
管理员输入书籍所在的书架位置,借阅提醒系统:可以查看个人借阅信息和图书到期提醒、挂失、检索、虚拟借书证不仅为群众提供了服务,而且也推广了自己,让更多的群众了解自己。 管理员页面: 第三章 系统分析 10 3.1需求分…...
CUDA C编程权威指南:1.2-CUDA基础知识点梳理
主要整理了N多年前(2013年)学习CUDA的时候开始总结的知识点,好长时间不写CUDA代码了,现在LLM推理需要重新学习CUDA编程,看来出来混迟早要还的。 1.闭扫描和开扫描 对于一个二元运算符 ⊕ \oplus ⊕和一个 n n n元…...
C语言—位运算符
目录 &(位与,AND): |(位或,OR): 位取反(~): 左移(<<): 右移(>>): &(位与,AND)&…...

怎么才能实现一个链接自动识别安卓.apk苹果.ipa手机和win电脑wac电脑
您想要实现的功能是通过检测用户代理(User Agent)来识别访问设备类型并根据设备类型展示相应的页面。您可以根据以下步骤进行实现: 选择后端语言和框架,例如:Node.js、Express。 创建一个新的Express项目。 编写一个…...

zookeeper选举机制
全新集群选举 zookeeper 全新集群选举机制网上资料很多说法很模糊,仔细思考了一下,应该是这样 得到票数最多的机器>机器总数半数 具体启动过程中的哪个节点成为 leader 与 zoo.cfg 中配置的节点数有关,下面以3个举例 选举过程如下 server…...
vcpkg切换 Visual Studio 版本
vcpkg切换 Visual Studio 版本 在使用vcpkg作为项目的包管理工具时,可能会遇到需要切换Visual Studio版本的情况。下面是一种简单的方法来实现这个目标,通过修改triplet文件来指定使用的Visual Studio版本。 步骤1: 创建或修改Triplet文件 首先&#…...

运算符重载
#include <iostream> using namespace std; class Num { private:int num1; //实部int num2; //虚部 public:Num(){}; //无参构造Num(int n1,int n2):num1(n1),num2(n2){}; //有参构造~Num(){}; //析构函数const Num operator(const Num &other)const //加号重载{Nu…...
Llama2-Chinese项目:7-外延能力LangChain集成
本文介绍了Llama2模型集成LangChain框架的具体实现,这样可更方便地基于Llama2开发文档检索、问答机器人和智能体应用等。 1.调用Llama2类 针对LangChain[1]框架封装的Llama2 LLM类见examples/llama2_for_langchain.py,调用代码如下所示:…...
ES6中数组的扩展
1. 扩展运算符 用三个点(...)表示,它如同rest参数的逆运算,将数组转为用逗号分隔的参数序列。扩展就是将一个集合分成一个个的。 console.log(...[1, 2, 3]); // 1, 2, 3可以用于函数调用 扩展运算符后还可以放置表达式 ...(x > 0 ? [a] : [])如…...

计算机考研 | 2016年 | 计算机组成原理真题
文章目录 【计算机组成原理2016年真题44题-9分】【第一步:信息提取】【第二步:具体解答】 【计算机组成原理2016年真题45题-14分】【第一步:信息提取】【第二步:具体解答】 【计算机组成原理2016年真题44题-9分】 假定CPU主频为5…...

Web版Photoshop来了,用到了哪些前端技术?
经过 Adobe 工程师多年来的努力,并与 Chrome 等浏览器供应商密切合作,通过 WebAssembly Emscripten、Web Components Lit、Service Workers Workbox 和新的 Web API 的支持,终于在近期推出了 Web 版 Photoshop(photoshop.adobe…...

FL Studio21.1.0水果中文官方网站
FL Studio 21.1.0官方中文版重磅发布纯正简体中文支持,更快捷的音频剪辑及素材管理器,多样主题随心换!Mac版新增对苹果M2/1家族芯片原生支持。DAW界萌神!极富二次元造型的水果娘FL chan通过FL插件Fruity Dance登场,为其…...

[BJDCTF2020]Mark loves cat
先用dirsearch扫一下,访问一下没有什么 需要设置线程 dirsearch -u http://8996e81f-a75c-4180-b0ad-226d97ba61b2.node4.buuoj.cn:81/ --timeout2 -t 1 -x 400,403,404,500,503,429使用githack python2 GitHack.py http://8996e81f-a75c-4180-b0ad-226d97ba61b2.…...

@SpringBootApplication注解的理解——如何排除自动装配 分布式情况下如何自动加载 nacos是怎么被发现的
前言 spring作为主流的 Java Web 开发的开源框架,是Java 世界最为成功的框架,持续不断深入认识spring框架是Java程序员不变的追求。 本篇博客介绍SpringBootApplicant注解的自动加载相关内容 其他相关的Spring博客文章列表如下: Spring基…...
HTTP的前世今生
史前时期 20 世纪 60 年代,美国国防部高等研究计划署(ARPA)建立了 ARPA 网,它有四个分布在各地的节点,被认为是如今互联网的“始祖”。 然后在 70 年代,基于对 ARPA 网的实践和思考,研究人员发…...

软件测试教程 自动化测试selenium篇(二)
掌握Selenium常用的API的使用 目录 一、webdriver API 1.1元素的定位 1.2 id定位 1.3name 定位 1.4tag name 定位和class name 定位 1.5CSS 定位 1.6XPath 定位 1.7link text定位 1.8Partial link text 定位 二、操作测试对象 2.1鼠标点击与键盘输入 2.2submit 提交…...

JavaSE入门--初始Java
文章目录 Java语言概述认识Java的main函数main函数示例运行Java程序认识注释认识标识符认识关键字 前言: 我从今天开始步入Java的学习,希望自己的博客可以带动小白学习,也能获得大佬的指点,日后能互相学习进步,都能如尝…...

leetcode做题笔记160. 相交链表
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。 图示两个链表在节点 c1 开始相交: 题目数据 保证 整个链式结构中不存在环。 注意,函数返回结果后&…...
谷歌浏览器插件
项目中有时候会用到插件 sync-cookie-extension1.0.0:开发环境同步测试 cookie 至 localhost,便于本地请求服务携带 cookie 参考地址:https://juejin.cn/post/7139354571712757767 里面有源码下载下来,加在到扩展即可使用FeHelp…...
基于大模型的 UI 自动化系统
基于大模型的 UI 自动化系统 下面是一个完整的 Python 系统,利用大模型实现智能 UI 自动化,结合计算机视觉和自然语言处理技术,实现"看屏操作"的能力。 系统架构设计 #mermaid-svg-2gn2GRvh5WCP2ktF {font-family:"trebuchet ms",verdana,arial,sans-…...
linux 错误码总结
1,错误码的概念与作用 在Linux系统中,错误码是系统调用或库函数在执行失败时返回的特定数值,用于指示具体的错误类型。这些错误码通过全局变量errno来存储和传递,errno由操作系统维护,保存最近一次发生的错误信息。值得注意的是,errno的值在每次系统调用或函数调用失败时…...

微信小程序云开发平台MySQL的连接方式
注:微信小程序云开发平台指的是腾讯云开发 先给结论:微信小程序云开发平台的MySQL,无法通过获取数据库连接信息的方式进行连接,连接只能通过云开发的SDK连接,具体要参考官方文档: 为什么? 因为…...
CRMEB 框架中 PHP 上传扩展开发:涵盖本地上传及阿里云 OSS、腾讯云 COS、七牛云
目前已有本地上传、阿里云OSS上传、腾讯云COS上传、七牛云上传扩展 扩展入口文件 文件目录 crmeb\services\upload\Upload.php namespace crmeb\services\upload;use crmeb\basic\BaseManager; use think\facade\Config;/*** Class Upload* package crmeb\services\upload* …...

SpringCloudGateway 自定义局部过滤器
场景: 将所有请求转化为同一路径请求(方便穿网配置)在请求头内标识原来路径,然后在将请求分发给不同服务 AllToOneGatewayFilterFactory import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; impor…...
根据万维钢·精英日课6的内容,使用AI(2025)可以参考以下方法:
根据万维钢精英日课6的内容,使用AI(2025)可以参考以下方法: 四个洞见 模型已经比人聪明:以ChatGPT o3为代表的AI非常强大,能运用高级理论解释道理、引用最新学术论文,生成对顶尖科学家都有用的…...

select、poll、epoll 与 Reactor 模式
在高并发网络编程领域,高效处理大量连接和 I/O 事件是系统性能的关键。select、poll、epoll 作为 I/O 多路复用技术的代表,以及基于它们实现的 Reactor 模式,为开发者提供了强大的工具。本文将深入探讨这些技术的底层原理、优缺点。 一、I…...

android13 app的触摸问题定位分析流程
一、知识点 一般来说,触摸问题都是app层面出问题,我们可以在ViewRootImpl.java添加log的方式定位;如果是touchableRegion的计算问题,就会相对比较麻烦了,需要通过adb shell dumpsys input > input.log指令,且通过打印堆栈的方式,逐步定位问题,并找到修改方案。 问题…...
OCR MLLM Evaluation
为什么需要评测体系?——背景与矛盾 能干的事: 看清楚发票、身份证上的字(准确率>90%),速度飞快(眨眼间完成)。干不了的事: 碰到复杂表格(合并单元…...