自定义vue通用左侧菜单组件(未完善版本)
•
Python
使用到的技术:
vue3、pinia、view-ui-plus
拖拽功能参考技术:HTMLElement:drag 事件 – Web API 接口参考 | MDN
实现的功能:
传入一个菜单数组数据,自动生成一个左侧菜单栏。菜单栏可以添加、删除、展开、重命名,拖动插入位置等。
效果预览:




代码:
c-menu-wrap.vue
- 新建文件夹
- 新建子文件夹
- 新建文档 <!--
- 上方新建模块
- 下方新建模块 -->
- 重命名
- 删除
c-menu.vue
{{ item.val }}
import { storeToRefs } from 'pinia'
import { useMenuStore } from '@/stores/menu.ts'
import { defineProps, defineEmits, withDefaults, ref, onMounted, inject } from 'vue';
interface Props {
list: Array,
index?: number
}
const props = withDefaults(defineProps(), {
list: [],
index: 0
})
const menuStore = useMenuStore()
const {
currentMenuId,
editItem,
isEditRename,
editInput,
dragItem,
dropPosition
} = storeToRefs(menuStore)
const {
doMenuAction,
editTitle,
showPopper,
hidePopper
} = menuStore
const draggable = ref(false)
onMounted(() => {
})
function changeDraggable(_draggable: boolean) {
draggable.value = _draggable
console.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 = null
dropPosition.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.target
let 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.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')
}
.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);
}
}
}
}
}
}
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 = false
currentMenuId.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 = true
let e = param.e
let item = param.item
editItem.value = item
nextTick(() => {
let _w = modal.value?.offsetWidth || 0
let _h = modal.value?.offsetHeight || 0
modalX.value = e.clientX + _w > window.innerWidth ? window.innerWidth - _w : e.clientX+2
modalY.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 = false
isEditRename.value = false
editItem.value = null
isShowPopper.value = false
}
// 确定修改文档标题
function editTitle() {
// 新建
if(isNew.value) {
console.log('确定新建标题', editItem.value)
if(editItem.value && editItem.value.type === 'file') {
currentMenuId.value = editItem.value.id
isEdit.value = true
selectItem.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 = false
isEditRename.value = false
editItem.value = null
}
function callBack(name: string) {
console.log('callBack: ', name)
}
// 开启重命名
function openRename() {
preMenuList.value = JSON.parse(JSON.stringify(menuList.value))
isShowPopper.value = false
isEditRename.value = true
nextTick(() => {
editInput.value && editInput.value[0] && editInput.value[0].focus()
})
}
// 全部展开
function expandAll(_list: Array = []) {
if(!_list || _list.length <= 0 || !(_list instanceof Array)) {
_list = menuList.value
}
for(let i = 0; i 0) {
_list[i].showSub = true
expandAll(_list[i].children)
}
}
}
// 创建文件夹或文档
function createNew(type: number) {
isNew.value = true
isShowPopper.value = false
isEditRename.value = true
preMenuList.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, type: number) {
console.log(type)
for(let i = 0; i 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'
本文来自网络,不代表协通编程立场,如若转载,请注明出处:https://net2asp.com/3dba44bdf5.html
