web架构师编辑器内容-编辑器组件图层面板功能开发-锁定隐藏、键盘事件功能的开发

我们这一部分主要是对最右侧图层面板功能进行剖析,完成对应的功能的开发:
在这里插入图片描述
每个图层都对应编辑器上面的元素,有多少个元素就对应多少个图层,主要的功能如下:

  1. 锁定功能:点击锁定,在编辑器中没法编辑对应的组件属性,再次点击是取消锁定,恢复到可编辑的模式
  2. 可见化:点击隐藏,在编辑器中消失,再次点击,进行展示
  3. 最外层图层也是可以进行点击,单击图层就是选中的效果。在编辑器上就是自动选中的效果。
  4. 图层的文字也可以进行修改,单击图层的文字,会切换到编辑模式,展示成input输入框,可以进行文字的修改。回车确认,点击esc退出,点击外部区域确定。
  5. 比较复杂的功能:拖动排序,按住这个按钮拖动以后,可以改变图层的顺序。

图层属性需求分析

图层锁定和隐藏/显示以及选中

图层和编辑器中的元素都是一一对应的,

// editor.ts
export interface EditorProps {// 供中间编辑器渲染的数组components: ComponentData[];// 当前编辑的是哪个元素,uuidcurrentElement: string
}export interface ComponentData {// 这个元素的 属性,属性请详见下面props: Partial<AllComponentProps>;// id,uuid v4 生成id: string;// 业务组件库名称 l-text,l-image 等等name: 'l-text' | 'l-image' | 'l-shape';
}

在editor.ts中,components其实就是对应的图层,有对应的一些属性ComponentData,对于不同的状态,我们来添加对应的标识符来添加特定的标识符来表示他的状态即可。

  • 在editor.ts的store中的components添加更多的标识符

{

isLocked: boolean;
isHidden: boolean;
}

  • 点击按钮切换为不同的值,使用这个值在页面上做判断
  • 点击选中,设置 currentElement的值

图层名称编辑

  • 添加更多属性 - layerName
  • 点击图层名称的时候,在input和普通标签之间切换
  • 添加按钮响应 - 对于 esc 和 enter 键的响应
    • 可能抽象一个通用的 hooks函数 - useKeyPress,可以处理与键盘相关的事件
  • 点击到input外部区域的响应
    • 可能抽象一个通用的 hooks函数 - useClickOutside

拖动改变顺序

  • 最有难度的一个需求,涉及到一个较复杂的交互
  • 最终目的其实就是改变store中components数组的顺序

代码实现

// LayerList.vue
<ul :list="list" class="ant-list-items ant-list-border"><li class="ant-list-item" v-for="item in list" :key="item.id"><a-tooltip :title="item.isHidden ? '显示' : '隐藏'"><a-button shape="circle"><template v-slot:icon v-if="item.isHidden"><EyeInvisibleOutlined /></template><template v-slot:icon v-else><EyeOutlined /> </template></a-button></a-tooltip><a-tooltip :title="item.isLocked ? '解锁' : '锁定'"><a-button shape="circle"><template v-slot:icon v-if="item.isLocked"><LockOutlined /></template><template v-slot:icon v-else><UnlockOutlined /> </template></a-button></a-tooltip><span>{{ item.layerName }}</span></li>
</ul>// list的数据来源:在点击左侧组件模板库的时候,会在store中发射一个事件:
// Editor.vue
// 右侧图层设置组件(其中components就是store中的components)
//  const components = computed(() => store.state.editor.components);
<layer-list:list="components":selectedId="currentElement && currentElement.id"@change="handleChange"@select="setActive">
</layer-list>
// 点击左侧模板库某个组件触发的事件
const addItem = (component: any) => {store.commit('addComponent', component);
};
// editor.ts
addComponent: setDirtyWrapper((state, component: ComponentData) => {component.layerName = '图层' + (state.components.length + 1);state.components.push(component);}),// 比如点击大标题,在addItem中对应的参数如下:component: {// 通过pageUUid生成的唯一主键id: '3c78b476-7a8d-4ad1-b944-9b163993595d',// 动态需要渲染的组件name: "l-text",props: {actionType: "";backgroundColor: "";borderColor: "#000";borderRadius: "0";borderStyle: "none";borderWidth: "0";boxShadow: "0 0 0 #000000";color: "#000000";fontFamily: "";fontSize: "30px";fontStyle: "normal";fontWeight: "bold";height: "";left: "0";lineHeight: "1";opacity: "1";paddingBottom: "0px";paddingLeft: "0px";paddingRight: "0px";paddingTop: "0px";position: "absolute";right: "0";tag: "h2";text: "大标题";textAlign: "left";textDecoration: "none";top: "0";url: "";width: "100px";}

最开始的样子
在这里插入图片描述
进行锁定隐藏操作

// 隐藏
<a-tooltip :title="item.isHidden ? '显示' : '隐藏'"><a-buttonshape="circle"@click.stop="handleChange(item.id, 'isHidden', !item.isHidden)"><template v-slot:icon v-if="item.isHidden"><EyeInvisibleOutlined /></template><template v-slot:icon v-else><EyeOutlined /> </template></a-button>
</a-tooltip>
// 锁定
<a-tooltip :title="item.isLocked ? '解锁' : '锁定'"><a-buttonshape="circle"@click.stop="handleChange(item.id, 'isLocked', !item.isLocked)"><template v-slot:icon v-if="item.isLocked"><LockOutlined /></template><template v-slot:icon v-else><UnlockOutlined /> </template></a-button>
</a-tooltip>const handleChange = (id: string, key: string, value: boolean) => {const data = {id,key,value,isRoot: true,};context.emit("change", data);
};// 最终在子组件中emit chang事件,父组件中触发该方法,
const handleChange = (e: any) => {console.log('event', e);store.commit('updateComponent', e);
};// 对store中的updateComponent进行稍微的改造
// 原来的updateComponent
// 这个主要针对于最右侧面板设置区域中的属性设置进行更新的,改变的是props的值。
updateComponent(state, { key, value }) {const updatedComponent = state.components.find((component) => component.id === state.currentElement); if(updatedComponent) {updatedComponent.props[key as keyof TextComponentProps] = value;}
}
// 现在的
updateComponent(state, { key, value, id, isRoot }) {const updatedComponent = state.components.find((component) => component.id === (id || state.currentElement)); if(updatedComponent) {if(isRoot) {(updatedComponent as any)[key as string] = value;}updatedComponent.props[key as keyof TextComponentProps] = value;}
}
// 增加isRoot主要用来判断改变的是否是props中的某一项的值,我们进行的是展示隐藏,锁定不锁定的功能,所以直接改变key值就行:
export interface ComponentData {// 这个元素的 属性,属性请详见下面props: Partial<AllComponentProps>;// id,uuid v4 生成id: string;// 业务组件库名称 l-text,l-image 等等name: 'l-text' | 'l-image' | 'l-shape';// 图层是否隐藏isHidden?: boolean;// 图层是否锁定isLocked?: boolean;// 图层名称layerName?: string;
}// Editor.vue
// 根据isLocked来判断右侧面板设置区域属性设置是否可以进行编辑
<a-tab-pane key="component" tab="属性设置" class="no-top-radius"><div v-if="currentElement"><edit-groupv-if="!currentElement.isLocked":props="currentElement.props"@change="handleChange"></edit-group><div v-else><a-empty><template #description>该元素已被锁定,无法被编辑</template></a-empty></div></div><pre>{{ currentElement && currentElement.props }}</pre>
</a-tab-pane>// 根据hidden属性来控制中间画布区域是否可以进行显示与隐藏
// EditorWrapper.vue
:class="{ active: active, hidden: hidden }"

图层重命名组件的开发

图层重命名组件,就是在右侧面板设置中的图层设置区域,点击图层名称,变成可输入的输入框形式,可以完成图层名称的更新,并且可以添加一些键盘事件,点击回车可以显示新的值,点击esc后显示刚开始的旧的值。在点击input区域外侧恢复文本区域,并且显示新的值。基于这些,我们可以抽离出一个InlineEdit组件

InlineEdit
显示默认文本区域,点击以后显示为 Input
Input 中的值显示为文本中的值
更新值以后,键盘事件 - (useKeyPress)

  • 点击回车以后恢复文本区域,并且显示新的值
  • 点击 ESC 后恢复文本区域,并且显示刚开始的旧的值,更新值以后,点击事件 - (useClickOutside)
  • 点击 Input 区域外侧恢复文本区域,并且显示新的值

简单验证

  • 当 Input值为空的时候,不恢复,并且显示错误。

最初的InlineEdit组件

// InlineEdit.vue
<template><div class="inline-edit" @click.stop="handleClick" ref="wrapper"><inputv-model="innerValue"v-if="isEditing"placeholder="文本不能为空"ref="inputRef"/><slot v-else :text="innerValue"><span>{{innerValue}}</span></slot></div>
</template><script lang="ts">
import { defineComponent, nextTick, ref, watch } from 'vue'
export default defineComponent({name: 'inline-edit',props: {value: {type: String,required: true}},emits: ['change'],setup (props, context) {const innerValue = ref(props.value)const isEditing = ref(false)const handleClick = () => {isEditing.value = true}return {handleClick,innerValue,isEditing}}
})
</script><style>
.inline-edit {cursor: pointer;
}
.ant-input.input-error {border: 1px solid #f5222d;
}
.ant-input.input-error:focus {border-color:  #f5222d;
}
.ant-input.input-error::placeholder {color: #f5222d;
}
</style>
键盘事件整合成hooks函数:
// hooks/useKeyPress.ts
import { onMounted, onUnmounted } from 'vue'
const useKeyPress = (key: string, cb: () => any) => {const trigger = (event: KeyboardEvent) => {if (event.key === key) {cb()}}onMounted(() => {document.addEventListener('keydown', trigger)})onUnmounted(() => {document.removeEventListener('keydown', trigger)})
}
// 组件中使用 InlineEdit.vue
// 缓存之前编辑的值
watch(isEditing, (isEditing) => {if (isEditing) {cachedOldValue = innerValue.value}
})
useKeyPress("Enter", () => {if (isEditing.value) {isEditing.value = false;context.emit("change", innerValue.value);}
});
useKeyPress("Escape", () => {if (isEditing.value) {isEditing.value = false;innerValue.value = cachedOldValue;}
});// 父组件接受change事件
<inline-editclass="edit-area":value="item.layerName"@change="(value) => {handleChange(item.id, 'layerName', value)}"
></inline-edit>

键盘响应的功能常规做法其实就是向document.addEventListener上添加各种一系列的回调,在项目后期还会遇到各种复杂的键盘响应,比如组合键,ctrl+c,ctrl+v,我们可能会进化到第三方库来完成对应的需求,先使用实际代码演示一个比较简单的功能,然后再使用第三方库的解决方案,这样能让我们了解第三方库的基本原理。上面就是按键响应的基本原理。
后来增加一个需求:在点击编辑,变成输入框的时候,增加自动聚焦的功能:

//这样写有问题
watch(isEditing, isEditing => {if (isEditing) {cachedOldValue = innerValue.valueif (inputRef.value) {inputRef.value.focus()}}
})

这样写的话,发现不起任何作用,input没有自动聚焦。
watchEffect
在vue3的官网api中,我们可以看到:
在这里插入图片描述
watchEffect的flush默认是pre,默认是在dom生成之前执行的,所以拿不到dom。但是vue没有提供可以改变flush的选项,没有办法在post中执行。所以我们这里可以vue提供的nextTick,等待dom生成完毕后,再运行,改写后的:

watch(isEditing, async (isEditing) => {if (isEditing) {cachedOldValue = innerValue.valueawait nextTick()if (inputRef.value) {inputRef.value.focus()}}
})
外侧点击整合hooks函数
// hooks/useClickOutside.ts
import { ref, onMounted, onUnmounted, Ref } from 'vue';
const useClickOutside = (elementRef: Ref<null | HTMLElement>) => {const isClickOutside = ref(false)const handler = (e: MouseEvent) => {if (elementRef.value && e.target) {// 检查当前元素是否在目标元素范围内if (elementRef.value.contains(e.target as HTMLElement)) {isClickOutside.value = false} else {isClickOutside.value = true}}}onMounted(() => {document.addEventListener('click', handler)})onUnmounted(() => {document.removeEventListener('click', handler)})return isClickOutside
}// 组件中使用 InlineEdit.vue
const inputRef = ref<null | HTMLInputElement>(null)
const isOutside = useClickOutside(wrapper)
watch(isOutside, (newValue) => {if (newValue && isEditing.value) {isEditing.value = falsecontext.emit('change', innerValue.value)}// 注意这里要将isOutside重新复原到false,因为如果不恢复成false的话,在isOutside为true的时候,点击外部区域,不会走这里的回调,因为值(true => true)没有改变。isOutside.value = false;
})

在进行图层设置对于图层名称点击编辑的时候,遇到一个这样的问题:从属性设置到图层设置切换后,点击图层名称进行图层编辑,编辑完成后,点击外层空白区域输入框没有变成原来的文本域。
在这里插入图片描述
产生上面问题的原因:
 在页面上打印的值是isOutside,在进行属性设置和图层设置的时候,其实触发了useClickOutside事件,返回了true,在鼠标进行文本点击的时候,由于在InlineEdit.vue 组件中加了@click.stop="handleClick",导致没有冒泡到document,所以这个时候useClickOutside事件没有被触发,isOutside的值并没有被改变,点击外面空白区域的时候,触发useClickOutside事件使isOutsidetrue,从truetruewatch第二个回调函数不会触发。所以需要再watch里面手动将isOutside置为false.
 另外我们知道是事件冒泡导致的,我们把事件冒泡去掉,直接写成@click="handleClick"不可以嘛?答案是不可以的,我们来看一下效果:
在这里插入图片描述
点击除了文字外的区域进行编辑的时候没有问题,但是点击的文字的时候就有问题了,主要原因就是没有使用到冒泡,导致useClickOutside事件触发了
判断是否点击到了对应的dom节点的功能是比较常见的,比如说下拉菜单的关闭,点击下拉菜单的外面,会关闭下拉菜单使用的是同一个思想。

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

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

相关文章

gin+gorm增删改查目录框架

从网上找资料,发现,很多都是直接的结构 路由&#xff0c;后端的controller层&#xff0c;还有model层&#xff0c;都是放在了同一个main.go文件中&#xff0c;如果写项目的话&#xff0c;还得自己去拆文件&#xff0c;拆代码&#xff0c;经过查询和自己总结&#xff0c;下面放…

如何在 Ubuntu / Raspbian 上安装 MariaDB

Raspberry Pi OS&#xff08;原为Raspbian&#xff09;是为树莓派基于Debian开发的操作系统。 从2015年起&#xff0c;树莓派基金会正式将其作为树莓派的官方操作系统。 Raspbian是由Mike Thompson和Peter Green创建的一个独立项目。第一个版本于2012年6月发布&#xff0c;至…

SQL慢语句执行的很慢,如何分析优化呢,(如何优化的呢?)

慢查询出现的情况&#xff1a; SQL执行慢如何解决&#xff1f; 可以采用MySQL自带的分析工具Explain。 通过key和key_len检查是否命中了索引&#xff08;如果你已经添加了索引&#xff0c;还可以判断索引是否失效&#xff09;通过type字段查看SQL是否有进一步优化的空间&#…

macOS修改默认时区显示中国时间

默认时区不是中国,显示时间不是中国时间 打开终端 ,删除旧区,并复制新时区到etcreb sudo -rm -rf /etc/localtime sudo ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime 重启系统后时间显示为中国时间

k8s节点RouteCreated为false

出现该情况后&#xff0c;一般是初始化节点失败。因此&#xff0c;需要把节点从集群中移除&#xff0c;再加入到集群中&#xff0c;即可解决。 通常出现这个状况后&#xff0c;该节点上是没有被分配pod ip的&#xff0c;可以通过命令查看&#xff1a; # 发现没有PodCIDR、PodC…

蓝桥杯java基础

数组的倒序与隔位输出 时间限制&#xff1a;1.000S 空间限制&#xff1a;128MB 题目描述 给定一个整数数组&#xff0c;编写一个程序实现以下功能&#xff1a; 1. 将输入的整数数组倒序输出&#xff0c;每个数之间用空格分隔。 2. 从正序数组中&#xff0c;每隔一个单位&a…

【C语言基础考研向】05 scanf读取标准输入超详解

文章目录 一.scanf函数的原理 样例问题原因解决方法 二.多种数据类型混合输入 错误样例正确样例 一.scanf函数的原理 C语言未提供输入/输出关键字&#xff0c;其输入和输出是通过标准函数库来实现的。C语言通过scanf函数读取键盘输入&#xff0c;键盘输入又被称为标准输入。…

【Docker】未来已来 | Docker技术在云计算、边缘计算领域的应用前景

欢迎来到英杰社区&#xff1a; https://bbs.csdn.net/topics/617804998 欢迎来到阿Q社区&#xff1a; https://bbs.csdn.net/topics/617897397 &#x1f4d5;作者简介&#xff1a;热爱跑步的恒川&#xff0c;致力于C/C、Java、Python等多编程语言&#xff0c;热爱跑步&#xff…

Spring Boot多环境配置

Spring Boot的针对不同的环境创建不同的配置文件&#xff0c; 语法结构&#xff1a;application-{profile}.properties profile:代表的就是一套环境 1.需求 application-dev.yml 开发环境 端口8090 application-test.yml 测试环境 端口8091 application-pro…

【Python】--- 基础语法(1)

目录 1.变量和表达式2.变量和类型2.1变量是什么2.2变量的语法2.3变量的类型2.3.1整数2.3.2浮点数&#xff08;小数&#xff09;2.3.3字符串2.3.4布尔2.3.5其他 2.4为什么要有这么多类型2.5动态类型特征 3.注释3.1注释的语法3.2注释的规范 结语 1.变量和表达式 对python的学习就…

SpringCloud之Nacos的学习、快速上手

1、什么是Nacos Nacos是阿里的一个开源产品&#xff0c;是针对微服务架构中的服务发现、配置管理、服务治理的综合型解决方案&#xff0c;用来实现配置中心和服务注册中心。 Nacos 快速开始 2、安装运行nacos nacos下载地址 下载地址: https://github.com/alibaba/nacos/rel…

Linux第33步_TF-A移植的第1步_创建新的设备树

TF-A移植第1步就是创建新的设备树&#xff0c;并命名为“stm32mp157d-atk”。 和“TF-A移植”有关的知识点&#xff1a; 1)设备树英文名字叫做Device tree&#xff0c;用来描述板子硬件信息的&#xff0c;比如开发板上的 CPU有几个核 、每个CPU核主频是多少&#xff0c;IIC、…

使用 MinIO 和 PostgreSQL 简化数据事件

本教程将教您如何使用 Docker 和 Docker Compose 在 MinIO 和 PostgreSQL 之间设置和管理数据事件&#xff0c;也称为存储桶或对象事件。 您可能已经在利用 MinIO 事件与外部服务进行通信&#xff0c;现在您将通过使用 PostgreSQL 自动化和简化数据事件管理来增强数据处理能力…

【C++】STL 算法 - 累加填充算法 ( 元素累加算法 - accumulate 函数 | 元素填充算法 - fill 函数 )

文章目录 一、元素累加算法 - accumulate 函数1、函数原型分析2、代码示例 二、元素填充算法 - fill 函数1、函数原型分析2、代码示例 一、元素累加算法 - accumulate 函数 1、函数原型分析 在 C 语言 的 标准模板库 ( STL , STL Standard Template Library ) 中 , 提供了 accu…

2024 年最佳 PDF 编辑器:我的首选清单

在我们日益多设备、跨平台的世界中&#xff0c;PDF 可能比以往任何时候都更有用。有大量免费的 PDF 阅读器可让您审阅和评论这些文件&#xff0c;但不可避免地&#xff0c;您需要具有编辑 PDF 的能力。这通常需要升级到高级 PDF 编辑器。 Adobe Acrobat 是最著名的&#xff0c…

20240119-子数组最小值之和

题目要求 给定一个整数数组 arr&#xff0c;求 min(b) 的总和&#xff0c;其中 b 的范围涵盖 arr 的每个&#xff08;连续&#xff09;子数组。由于答案可能很大&#xff0c;因此返回答案模数 Example 1: Input: arr [3,1,2,4] Output: 17 Explanation: Subarrays are [3]…

Unity3D学习之Unity基础——3D数学

文章目录 1. 前言2 Mathf和Math基础2.1 一般用于只计算一次的函数2.1.1 PI Π PI2.1.2 取绝对值 Abs2.1.3 向上取整 CeilToInt2.1.4 向下取整 FloorToInt2.1.5 钳制函数 Clamp2.1.6 获取最大值 Max2.1.7 获取最小值 Min2.1.8 一个数的n次幂 Pow2.1.9 四舍五入 RoundToInt2.1.10…

苹果Find My可查找添加32件物品,伦茨科技ST17H6x芯片加速产品赋能

苹果最近更新的支持文档证实&#xff0c;从 iOS 16 开始&#xff0c;"Find My"可查找添加物品从16件增加到32件&#xff0c;AirTag 和“查找”网络中的物品利用“查找”网络的强大功能来发挥作用&#xff0c;这个网络由数亿台加密的匿名 Apple 设备构成。“查找”网络…

【Unity】AB包下载

【Unity】AB包下载 1.使用插件打AB包 a.AB包分类 一般地&#xff0c;将预制体作为AB包资源&#xff0c;不仅需要对预制体本身进行归类&#xff0c;还要对其涉及的动画&#xff08;AnimationClip&#xff09;、动画状态机&#xff08;AnimatorController&#xff09;、以及所…

golang学习笔记——http.Handle和http.HandleFunc的区别与type func巧妙运用

文章目录 http.Handle和http.HandleFunc的区别http.Handle分析type func巧妙运用 http.HandleFunc分析总结参考资料 http.Handle和http.HandleFunc的区别 http.Handle和http.HandleFunc的区别体现了Go语言接口的巧妙运用 下面代码启动了一个 http 服务器&#xff0c;监听 808…