自定义vue通用左侧菜单组件(未完善版本)

使用到的技术:

vue3、pinia、view-ui-plus

实现的功能:

传入一个菜单数组数据,自动生成一个左侧菜单栏。菜单栏可以添加、删除、展开、重命名,拖动插入位置等。

效果预览:

 

代码:

c-menu-wrap.vue

<template><div class="main-container"><div class="nav-top"><Tooltip content="展开全部" placement="top"><Icon type="ios-code" @click="expandAll"/></Tooltip><Tooltip content="创建文档" placement="top"><Icon type="ios-add" @click.stop="showPopper({e: $event, item: null})"/></Tooltip></div><div class="nav-list"><cMenu :list="menuList"/></div><div @click.stop="hidePopper"class="menu-modal"ref="modal"v-show="isShowPopper":style="{left: modalX+'px',top: modalY+'px'}"><ul><li v-if="!editItem" @click.stop="createNew(1)"><Icon type="ios-add" /><span>新建文件夹</span></li><li v-if="editItem && editItem.type === 'folder'" @click.stop="createNew(1)"><Icon type="ios-add" /><span>新建子文件夹</span></li><li v-if="!editItem || (editItem && editItem.type === 'folder')" @click.stop="createNew(2)"><Icon type="ios-add" /><span>新建文档</span></li><!-- <li v-if="editItem"><Icon type="ios-add" /><span>上方新建模块</span></li><li v-if="editItem"><Icon type="ios-add" /><span>下方新建模块</span></li> --><li @click.stop="openRename" v-if="editItem"><Icon type="ios-create-outline" /><span>重命名</span></li><li @click.stop="delDoc" v-if="editItem"><Icon type="ios-trash-outline" /><span>删除</span></li></ul></div></div>
</template><script setup lang="ts">import { storeToRefs } from 'pinia'import { useMenuStore } from '@/stores/menu'import { defineProps, defineEmits, withDefaults, ref, nextTick, onMounted, onBeforeUnmount, provide } from 'vue'import { Modal, Message } from 'view-ui-plus'import cMenu from './c-menu.vue'const menuStore = useMenuStore()const { modal,modalX,modalY,menuList, isShowPopper, editItem } = storeToRefs(menuStore)const { expandAll, showPopper, hidePopper, createNew, openRename} = menuStoreconst emit = defineEmits(['doAction'])onMounted(() => {// document.addEventListener('click', hidePopper)})onBeforeUnmount(() => {// document.removeEventListener('click', hidePopper)})// 删除文档function delDoc() {Modal.confirm({title: '提示',content: '确定要删除该文档吗?',onOk: () => {},onCancel: () => {Message.info('取消操作~');}});}</script><style lang="scss" scoped>.main-container {display: flex;flex-direction: column;height: 100%;.nav-top {flex-shrink: 0;box-sizing: border-box;padding: 0 24px;height: 48px;display: flex;justify-content: flex-end;align-items: center;font-size: 25px;// position: absolute;// top: 0;// left: 0;// width: 100%;border-bottom: 1px solid rgba(37, 55, 92, 0.10);i {		cursor: pointer;margin-left: 15px;&:hover {color: #05F;}}:deep(.ivu-icon-ios-code) {font-size: 20px;transform: rotate(90deg);transform-origin: center;margin: 0;	}}.nav-list {flex: auto;margin-top: 10px;overflow: auto;}.menu-modal {z-index: 10;position: fixed;top: 0;left: 0;&:after {z-index: 20;content: '';position: fixed;top: 0;left: 0;right: 0;bottom: 0;}ul {z-index: 21;position: relative;background: white;border: 1px solid rgba(37, 55, 92, 0.10);li {cursor: pointer;padding: 10px 20px;display: flex;align-items: center;&:hover {i, span {color: #05F;}}i {font-size: 15px;}span {margin-left: 10px;font-size: 14px;}}}}}</style>

c-menu.vue

<template><div class="menu-box"><div class="li"v-for="(item, key) in list" :key="key":class="{active: item.id == currentMenuId}"><divclass="edit-title" v-if="isEditRename && editItem && editItem.id == item.id" ><Input ref="editInput"@click.stop=""class="edit-input"v-model="editItem.val"maxlength="50":placeholder="editItem.type === 'folder' ? '请输入文件夹名称' : '请输入文档名称'" @on-enter="editTitle"@on-blur="hidePopper"/><Icon class="icon" type="md-checkmark-circle" @click.stop="editTitle" /></div><div v-elseclass="title-box":class="dragItem?.id == item.id ? 'drag' : ''":node-id="item.id":draggable="draggable"@dragstart="dragStart($event, item)" @drop="drop($event, item)"@dragend="dragEnd($event)"@dragover="dragOver($event)"@dragenter="dragEnter($event)"@dragleave="dragLeave($event)"@click.stop="doMenuAction($event, item)"><div :style="{pointerEvents: 'none',marginLeft: (index*10)+'px'}"><span class="txt">{{ item.val }}</span><i v-if="item.children && item.type === 'folder'" class="icon" :class="{active: item.showSub}"></i></div><Icon class="md-reorder" type="md-reorder" @mousedown="changeDraggable(true)"/><Icon class="md-more" type="md-more" @click.stop="showPopper({e: $event, item})"/></div><div class="sub-box" :style="{height: item.showSub ? 'auto': 0}" v-if="item.children"><c-menu :index="index+1" :list="item.children" ></c-menu></div></div></div></template><script setup lang="ts">import { storeToRefs } from 'pinia'import { useMenuStore } from '@/stores/menu.ts'import { defineProps, defineEmits, withDefaults, ref, onMounted, inject } from 'vue';interface Props {list: Array<any>,index?: number}const props = withDefaults(defineProps<Props>(), {list: [],index: 0})const menuStore = useMenuStore()const { currentMenuId, editItem,isEditRename,editInput,dragItem,dropPosition} = storeToRefs(menuStore)const { doMenuAction, editTitle, showPopper,hidePopper} = menuStoreconst draggable = ref(false)onMounted(() => {})function changeDraggable(_draggable: boolean) {draggable.value = _draggableconsole.log(_draggable)}function drop($event: any, item: string) {$event.preventDefault()$event.stopPropagation()let data = $event.dataTransfer.getData("item");if(data) {let mitem = JSON.parse(data)console.log('拖动放置:', mitem)if(mitem.id == item.id) {console.log('同一个元素')} else {console.log('放置位置', dropPosition.value)}}$event.target.classList.remove('over')$event.target.classList.remove('over-top')$event.target.classList.remove('over-bottom')dragItem.value = nulldropPosition.value = 0}// 拖动时触发function dragStart($event: any, item: any) {console.log("开始拖动:", item);$event.stopPropagation()$event.dataTransfer.setData("item",JSON.stringify(item))dragItem.value = JSON.parse(JSON.stringify(item))}function dragEnd($event: any) {$event.preventDefault()$event.stopPropagation()draggable.value = false}function dragOver($event: any) {$event.preventDefault()$event.stopPropagation()let t = $event.targetlet e = '.title-box'for (var i = t.matches || t.webkitMatchesSelector || t.mozMatchesSelector || t.msMatchesSelector; t && !i.call(t, e); ) {t = t.parentElement;}// 判断是否是同一个元素if(t.className.indexOf('title-box')!== -1 && t.getAttribute('node-id') != dragItem.value?.id) {t.classList.add('over')let dom = t.getBoundingClientRect()if($event.clientY < dom.top + 5) {// console.log('上')t.classList.add('over-top')t.classList.remove('over-bottom')dropPosition.value = 1} else if($event.clientY > dom.bottom - 5) {// console.log('下')t.classList.add('over-bottom')t.classList.remove('over-top')dropPosition.value = 2} else {// console.log('中')t.classList.remove('over-bottom')t.classList.remove('over-top')dropPosition.value = 0}}}function dragEnter($event: any) {console.log('dragEnter')$event.preventDefault()$event.stopPropagation()}function dragLeave($event: any) {$event.stopPropagation();$event.target.classList.remove('over')$event.target.classList.remove('over-top')$event.target.classList.remove('over-bottom')}</script><style lang="scss" scoped>.menu-box {.li {cursor: pointer;.edit-title {position: relative;.edit-input {padding: 0 24px;:deep(.ivu-input) {padding-right: 25px;}}.icon {position: absolute;top: 50%;right: 30px;transform: translate(0, -50%);font-size: 18px;cursor: pointer;&:hover {color: #0055FF;}}}.title-box {position: relative;padding: 0 24px;border: 1px solid transparent;&.drag {background: rgb(203, 218, 245, 0.5);}&::before {background: transparent;content: "";height: 2px;top: 0;left: 0;position: absolute;width: 100%;}&::after {background: transparent;content: "";height: 2px;bottom: 0;left: 0;position: absolute;width: 100%;}&.over {border: 1px dashed #0055FF;}&.over-top {&::before {background: #0055FF;}}&.over-bottom {&::after {background: #0055FF;}}.md-reorder {display: none;position: absolute;left: 5px;top: 50%;transform: translate(0, -50%);font-size: 15px;cursor: move;&:hover {color: #0055FF;}}.md-more {display: none;position: absolute;right: 5px;top: 50%;transform: translate(0, -50%);	font-size: 15px;&:hover {color: #0055FF;}			}&>div {padding: 8px 12px;display: flex;align-items: center;justify-content: space-between;.txt {display: block;color:  #81838C;font-size: 14px;font-style: normal;font-weight: 500;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;}.icon {width: 8px;height: 8px;background: url(../../assets/ic8_draw-down_normal.png) no-repeat;background-size: 100%;&.active {transform: rotate(180deg)}}}&:hover {&>div{.txt {color: #0055FF;}}.md-reorder, .md-more {display: block;}}}.sub-box {overflow: hidden;}&.active {.title-box {&>div {border-radius: 8px;background: #EBF2FF;.txt {color: #0055FF;}.icon {background-image: url(../../assets/ic8_draw-down_normal_hover.png);transition: all 0.5s;transform: rotate(180deg);}}}}}}</style>

menu.ts

import { defineStore } from 'pinia'
import { ref, nextTick } from 'vue'
import { randomString } from '@/utils/index.js'export const useMenuStore = defineStore('menu', () => {const menuList = ref([{id: 1,val: '标题1',type: 'folder'},{id: 2,val: '标题1',type: 'folder',children: [{id: 21,val: '标题2',type: 'folder',children: [{id: 211,val: '标题3',type: 'file',content: '123'}, {id: 212,val: '标题3',type: 'file',content: '345'}]}]}])const preMenuList = ref([])const currentMenuId = ref('')const isShowPopper = ref(false)const modal = ref(null)const modalX = ref(0)const modalY = ref(0)const editItem = ref(null) // 当前标题编辑对象(文件夹或文件)const editInput = ref(null)const selectItem = ref(null) // 当前选中的文件对象const preSelectItem = ref(null)const isEdit = ref(false) // 文档是否开启编辑状态const isNew = ref(false) // 是否新建const isEditRename = ref(false) // 是否重命名const dragItem = ref(null) // 当前拖动元素const dropPosition = ref(0) // 拖动元素插入的位置:0 中,1 上,2 下// 点击菜单function doMenuAction(e:any, item: any) {hidePopper()if(item.type === 'folder') {item.showSub = !item.showSub} // 选中的文档else if(item.type === 'file') {if(item.id !== currentMenuId.value) {isEdit.value = falsecurrentMenuId.value = item.id.toString()selectItem.value = JSON.parse(JSON.stringify(item))preSelectItem.value = JSON.parse(JSON.stringify(selectItem.value))}}}// 显示更多菜单function showPopper(param: any) {hidePopper()isShowPopper.value = truelet e = param.elet item = param.itemeditItem.value = itemnextTick(() => {let _w = modal.value?.offsetWidth || 0let _h = modal.value?.offsetHeight || 0modalX.value = e.clientX + _w > window.innerWidth ? window.innerWidth - _w : e.clientX+2modalY.value = e.clientY + _h > window.innerHeight ? window.innerHeight - _h : e.clientY+2})}// 隐藏更多菜单function hidePopper() {console.log('隐藏更多菜单')if(isNew.value || (!isNew.value && isEditRename.value)) {	menuList.value = JSON.parse(JSON.stringify(preMenuList.value))}isNew.value = falseisEditRename.value = falseeditItem.value = nullisShowPopper.value = false}// 确定修改文档标题function editTitle() {// 新建if(isNew.value) {console.log('确定新建标题', editItem.value)if(editItem.value && editItem.value.type === 'file') {currentMenuId.value = editItem.value.idisEdit.value = trueselectItem.value = JSON.parse(JSON.stringify(editItem.value))preSelectItem.value = JSON.parse(JSON.stringify(selectItem.value ))}} // 修改else {console.log('确定修改标题', editItem.value)if(editItem.value && editItem.value.type === 'file' && currentMenuId.value === editItem.value.id) {selectItem.value = JSON.parse(JSON.stringify(editItem.value))preSelectItem.value = JSON.parse(JSON.stringify(selectItem.value ))}}isNew.value = falseisEditRename.value = falseeditItem.value = null}function callBack(name: string) {console.log('callBack: ', name)}// 开启重命名function openRename() {preMenuList.value = JSON.parse(JSON.stringify(menuList.value))isShowPopper.value = falseisEditRename.value = truenextTick(() => {editInput.value && editInput.value[0] && editInput.value[0].focus()})}// 全部展开function expandAll(_list: Array<any> = []) {if(!_list || _list.length <= 0 || !(_list instanceof Array)) {_list = menuList.value}for(let i = 0; i < _list.length; i++) {if(_list[i].children && _list[i].children.length > 0) {_list[i].showSub = trueexpandAll(_list[i].children)}}}// 创建文件夹或文档function createNew(type: number) {isNew.value = trueisShowPopper.value = falseisEditRename.value = truepreMenuList.value = JSON.parse(JSON.stringify(menuList.value))// 文件夹if(type === 1) {// 新建文件夹if(!editItem.value) {console.log('新建文件夹')editItem.value = JSON.parse(JSON.stringify({id: randomString(32),val: '',type: 'folder',}))menuList.value.push(editItem.value)} // 新建子文件夹else {console.log('新建子文件夹')findParentMenu(menuList.value, type)}} // 文档else if(type === 2) {// 新建文档if(!editItem.value) {console.log('新建文档')editItem.value = JSON.parse(JSON.stringify({id: randomString(32),val: '',type: 'file',}))menuList.value.push(editItem.value)} // 新建子文档else {console.log('新建子文档')findParentMenu(menuList.value, type)}}nextTick(() => {editInput.value && editInput.value[0] && editInput.value[0].focus()})}function findParentMenu(list: Array<any>, type: number) {console.log(type)for(let i = 0; i < list.length; i++) {if(list[i].id === editItem.value.id) {list[i].showSub = trueeditItem.value = JSON.parse(JSON.stringify({id: randomString(32),val: '',type: type === 1 ? 'folder' : 'file',}))if(!list[i].children) {list[i].children = []}list[i].children.push(editItem.value)break} else {if(list[i].children && list[i].children.length > 0) {findParentMenu(list[i].children, type)}}}}return {editInput,isEdit,isNew,isEditRename,isShowPopper,modal,modalX,modalY,currentMenuId,editItem,menuList,preMenuList,selectItem,preSelectItem,dragItem,dropPosition,doMenuAction,expandAll,createNew,openRename,editTitle,showPopper,hidePopper,callBack}})

使用

import cMenuWrap from '@/components/menu/c-menu-wrap.vue'<c-menu-wrap></c-menu-wrap>

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

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

相关文章

【Linux】压缩脚本、报警脚本

一、压缩搅拌 要求&#xff1a; 写一个脚本&#xff0c;完成如下功能 传递一个参数给脚本&#xff0c;此参数为gzip、bzip2或者xz三者之一&#xff1b; (1) 如果参数1的值为gzip&#xff0c;则使用tar和gzip归档压缩/etc目录至/backups目录中&#xff0c;并命名为/backups/etc…

鸿蒙OS之Rust开发

背景 Rust是一门静态强类型语言&#xff0c;具有更安全的内存管理、更好的运行性能、原生支持多线程开发等优势。Rust官方也使用Cargo工具来专门为Rust代码创建工程和构建编译。 OpenHarmony为了集成C/C 代码和提升编译速度&#xff0c;使用了GN Ninja的编译构建系统。GN的构…

Open CASCADE学习|遍历曲面的边

目录 1、球面的Brep数据 2、C遍历球面的边 ​这里以球面为例来说明如何遍历曲面的边。 1、球面的Brep数据 使用Tcl命令在Draw Test Harness中生成的球面并到出Brep数据如下&#xff1a; pload ALL psphere asphere 1 dump asphere 结果如下&#xff1a; *********** D…

构建高效外卖系统:利用Spring Boot框架实现

在当今快节奏的生活中&#xff0c;外卖系统已经成为人们生活中不可或缺的一部分。为了构建一个高效、可靠的外卖系统&#xff0c;我们可以利用Spring Boot框架来实现。本文将介绍如何利用Spring Boot框架构建一个简单但功能完善的外卖系统&#xff0c;并提供相关的技术代码示例…

Qt SQLite3数据库加密 QtCipherSqlitePlugin

在客户端软件开发过程中&#xff0c;基本都会涉及到数据库的开发。QT支持的数据库也有好几种&#xff08;QSQLITE, QODBC, QODBC3, QPSQL, QPSQL7&#xff09;&#xff0c;SQLite就是其中之一&#xff0c;但这个 SQLite 是官方提供的开源版本&#xff0c;没有加密功能的。如果对…

安全小记-sqli-labs闯关

1.安装靶场 介绍&#xff1a; SQLI&#xff0c;sql injection&#xff0c;我们称之为sql注入。何为sql&#xff0c;英文&#xff1a;Structured Query Language&#xff0c;叫做结构化查询语言。常见的结构化数据库有MySQL&#xff0c;MS SQL ,Oracle以及Postgresql。Sql语言…

【Spark系列4】Task的执行

一、Task的执行流程 1.1、Task执行流程 DAGScheduler将Stage生成TaskSet之后&#xff0c;会将Task交给TaskScheduler进行处理&#xff0c;TaskScheduler负责将Task提交到集群中运行&#xff0c;并负责失败重试&#xff0c;为DAGScheduler返回事件信息等&#xff0c;整体如流程…

OpenGL ES 渲染 NV21、NV12 格式图像有哪些“姿势”?

使用2个纹理实现 NV21 格式图像渲染 前文提到渲染 NV21 格式图像需要使用 2 个纹理,分别用于保存 Y plane 和 UV plane 的数据,然后在片段着色器中分别对 2 个纹理进行采样,转换成 RGB 数据。 OpenGLES 渲染 NV21或 NV12 格式图像需要用到 GL_LUMINANCE 和 GL_LUMINANCE_A…

http和https的区别是什么?https有什么优缺点?

HTTP&#xff08;Hypertext Transfer Protocol&#xff0c;超文本传输协议&#xff09;是一个简单的请求-响应协议&#xff0c;它通常运行在TCP之上。它指定了客户端可能发送给服务器什么样的消息以及得到什么样的响应。这个简单模型是早期Web成功的有功之臣&#xff0c;因为它…

The following untracked working tree files would be overwritten by merge问题的解决

作者&#xff1a;朱金灿 来源&#xff1a;clever101的专栏 为什么大多数人学不会人工智能编程&#xff1f;>>> 在更新git仓库时出现了一个The following untracked working tree files would be overwritten by merge的错误&#xff0c;具体如下图&#xff1a; 分析…

ES 分词器

概述 分词器的主要作用将用户输入的一段文本&#xff0c;按照一定逻辑&#xff0c;分析成多个词语的一种工具 什么是分词器 顾名思义&#xff0c;文本分析就是把全文本转换成一系列单词&#xff08;term/token&#xff09;的过程&#xff0c;也叫分词。在 ES 中&#xff0c;Ana…

2024年新提出的算法:(凤头豪猪优化器)冠豪猪优化算法Crested Porcupine Optimizer(附Matlab代码)

本次介绍一种新的自然启发式元启发式算法——凤头豪猪优化器(Crested Porcupine Optimizer&#xff0c;CPO)。该成果于2024年1月发表在中科院1区SCI top期刊Knowledge-Based Systems&#xff08;IF 8.8&#xff09;上。 1、简介 受到凤头豪猪&#xff08;CP&#xff09;各种…

iOS 自动打包如何配置配置打包证书和profile provision文件【脚本方式配置】

iOS 最新Jenkins自动化打包总结 本文主要内容&#xff1a; 1.Xcode和Jenkins的相关设置&#xff0c;以及环境切换 2.通过shell脚本将证书和描述文件拷贝到自动化打包的机器&#xff0c;并archive导出ipa包 3.上传到蒲公英 4.解决Swift不支持use_frameworks!的问题 开搞&…

【开源】SpringBoot框架开发天然气工程业务管理系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块三、使用角色3.1 施工人员3.2 管理员 四、数据库设计4.1 用户表4.2 分公司表4.3 角色表4.4 数据字典表4.5 工程项目表4.6 使用材料表4.7 使用材料领用表4.8 整体E-R图 五、系统展示六、核心代码6.1 查询工程项目6.2 工程物资…

[Grafana]ES数据源Alert告警发送

简单的记录一下使用es作为数据源&#xff0c;如何在发送告警是带上相关字段 目录 前言 一、邮件配置 二、配置 1.Query 2.Alerts 总结 前言 ES作为数据源&#xff0c;算是Grafana中比较常见的&#xff0c;Alerts告警是我近期刚接触&#xff0c;有一个需求是当表空间大于…

flutter实现:使用三方组件syncfusion_flutter_datagrid

Syncfusion Flutter DataGrid 是一个用于 Flutter 的数据网格组件&#xff0c;它提供了丰富的功能来显示和编辑数据。这个组件提供了灵活的配置选项&#xff0c;使得开发者能够根据需要定制数据的显示和编辑方式。 项目中有两个需求&#xff0c;一是在列表中要使用可变高度&am…

OpenCV 5 - 图像混合处理addWeighted()

图像混合 1 理论-线性混合操作 其中α的取值范围为0~1之间,表示图像的所占的权重 2 混合处理函数addWeighted() 3 代码示例 Mat src1, src2, dst;src1 imread("./1.png");src2 imread("./2.png");if (!src1.data && src2.empty()) //判断图片是…

云安全中的常见云漏洞和威胁,有哪些防范措施

随着企业在数字化时代的脚步中愈发倚重云托管服务&#xff0c;云安全问题成为不容忽视的焦点。云服务的便捷性为企业提供了强大的存储和计算能力&#xff0c;然而&#xff0c;与之伴随而来的攻击风险也日益显著。最新的研究数据揭示&#xff0c;云安全漏洞可能导致的数据泄露&a…

ETCD监控方法以及核心指标

文章目录 1. 监控指标采集1.1 监控指标采集1.2 配置promethues采集和大盘 2. 核心告警指标3. 参考文章 探讨etcd的监控数据采集方式以及需要关注的核心指标&#xff0c;便于日常生产进行监控和巡检。 1. 监控指标采集 etcd默认通过/metrics指标暴露相关指标&#xff0c;因此不…

opencv#37 形态学操作——腐蚀

图像腐蚀目的 去除图像中微小物体 分离较近的两个物体 我们对图像中所有米粒进行二值化处理&#xff0c;之后进行连通域分割以求去整个图像中共用多少米粒&#xff0c;处理结果在可以发现&#xff0c;在上图中有一小块区域上有个小点&#xff08;非米粒&#xff09;&#xff…