我正在参加「掘金·启航计划」
1、背景
由于公司业务需求,需要实现微信公众号营销相关的功能,其中有一块需要实现公众号后台管理中的自定义菜单的页面,作为一个前端菜鸡提前写了个demo,在此记录一下,希望对你有所帮助。 实现的效果图如下:
2、实现
可以看到页面分为两部分,左侧新增菜单视图 + 右侧菜单名称编辑视图。
主要实现的功能有:
- 新增/删除 1、2级菜单;
- 修改1、2级菜单名称;
- 拖拽移动2级菜单。
下面看下具体实现。
2.1 左侧新增菜单视图
首先对左侧菜单视图进行拆分,可以分为 背景 + 底部一级菜单 + 底部二级菜单
底部一级菜单、底部二级菜单都采用绝对定位。
- js代码实现如下:
<template>
<div>
<div id="app-menu">
<!-- 预览窗 -->
<div class="weixin-preview">
<div class="weixin-bd">
<div class="weixin-header">公众号菜单</div>
<ul class="weixin-menu" id="weixin-menu">
<li
v-for="(btn, i) in menu.buttons"
:key="i"
class="menu-item"
:class="{
current: selectedMenuIndex === i && selectedMenuLevel == 1,
}"
@click="selectMenu(i)"
>
<div class="menu-item-title">
<span>{{ btn.name }}</span>
</div>
<ul class="weixin-sub-menu">
<li
v-for="(sub, i2) in btn.subButtons"
:key="i2"
class="menu-sub-item"
:class="{
current:
selectedMenuIndex === i &&
selectedSubMenuIndex === i2 &&
selectedMenuLevel == 2,
'on-drag-over': onDragOverMenu == i + '_' + i2,
}"
@click.stop="selectSubMenu(i, i2)"
draggable="true"
@dragstart="selectSubMenu(i, i2)"
@dragover.prevent="onDragOverMenu = i + '_' + i2"
@drop="onDrop(i, i2)"
>
<div class="menu-item-title">
<span>{{ sub.name }}</span>
</div>
</li>
<li
v-if="btn.subButtons.length < 5"
class="menu-sub-item"
:class="{
'on-drag-over':
onDragOverMenu == i + '_' + btn.subButtons.length,
}"
@click.stop="addMenu(2, i)"
@dragover.prevent="
onDragOverMenu = i + '_' + btn.subButtons.length
"
@drop="onDrop(i, btn.subButtons.length)"
>
<div class="menu-item-title">
<i class="el-icon-plus"></i>
</div>
</li>
<i class="menu-arrow arrow_out"></i>
<i class="menu-arrow arrow_in"></i>
</ul>
</li>
<li
class="menu-item"
v-if="menu.buttons.length < 3"
@click="addMenu(1)"
>
<i class="el-icon-plus"></i>
</li>
</ul>
</div>
</div>
<!-- 菜单编辑器 -->
<div class="weixin-menu-detail" v-if="selectedMenuLevel > 0">
<wx-menu-button-editor
:buttonMenu="selectedButton"
:selectedMenuLevel="selectedMenuLevel"
@delMenu="delMenu"
></wx-menu-button-editor>
</div>
</div>
</div>
</template>
<script>
export default {
components: {
wxMenuButtonEditor: () => import("./wx-menu-button-editor"),
},
data() {
return {
menu: { buttons: [] }, //当前菜单
selectedMenuIndex: "", //当前选中菜单索引
selectedSubMenuIndex: "", //当前选中子菜单索引
selectedMenuLevel: 0, //选中菜单级别
selectedButton: "", //选中的菜单按钮
onDragOverMenu: "", //当前鼠标拖动到的位置
};
},
mounted() {
},
methods: {
//选中主菜单
selectMenu(i) {
this.selectedMenuLevel = 1;
this.selectedSubMenuIndex = "";
this.selectedMenuIndex = i;
this.selectedButton = this.menu.buttons[i];
},
//选中子菜单
selectSubMenu(i, i2) {
this.selectedMenuLevel = 2;
this.selectedMenuIndex = i;
this.selectedSubMenuIndex = i2;
this.selectedButton = this.menu.buttons[i].subButtons[i2];
},
//添加菜单
addMenu(level, i) {
if (level == 1 && this.menu.buttons.length < 3) {
this.menu.buttons.push({
type: "view",
name: "菜单名称",
subButtons: [],
url: "",
});
this.selectMenu(this.menu.buttons.length - 1);
}
if (level == 2 && this.menu.buttons[i].subButtons.length < 5) {
this.menu.buttons[i].subButtons.push({
type: "view",
name: "子菜单名称",
url: "",
});
this.selectSubMenu(i, this.menu.buttons[i].subButtons.length - 1);
}
},
//删除菜单
delMenu() {
if (
this.selectedMenuLevel == 1 &&
confirm("删除后菜单下设置的内容将被删除")
) {
this.menu.buttons.splice(this.selectedMenuIndex, 1);
this.unSelectMenu();
} else if (this.selectedMenuLevel == 2) {
this.menu.buttons[this.selectedMenuIndex].subButtons.splice(
this.selectedSubMenuIndex,
1
);
this.unSelectMenu();
}
},
unSelectMenu() {
//不选中任何菜单
this.selectedMenuLevel = 0;
this.selectedMenuIndex = "";
this.selectedSubMenuIndex = "";
this.selectedButton = "";
},
onDrop(i, i2) {
//拖拽移动位置
this.onDragOverMenu = "";
if (i == this.selectedMenuIndex && i2 == this.selectedSubMenuIndex)
//拖拽到了原位置
return;
if (
i != this.selectedMenuIndex &&
this.menu.buttons[i].subButtons.length >= 5
) {
this.$message.error("目标组已满");
return;
}
this.menu.buttons[i].subButtons.splice(i2, 0, this.selectedButton);
let delSubIndex = this.selectedSubMenuIndex;
if (i == this.selectedMenuIndex && i2 < this.selectedSubMenuIndex)
delSubIndex++;
this.menu.buttons[this.selectedMenuIndex].subButtons.splice(
delSubIndex,
1
);
this.unSelectMenu();
},
},
};
</script>
<style src="@/assets/css/wx-menu.css">
</style>
定义的菜单数据结构如下: menu: { buttons:[subButtons: []] }
buttons 是 一级菜单; subButtons是二级菜单;
限制:1级菜单最多可以添加3个,2级菜单最多可以添加5个。
添加菜单: 添加1级菜单,只需要往menu.buttons中添加菜单数。 添加2级菜单,需要知道1级菜单索引。
删除菜单: 1级菜单根据自身的索引去删除-删除后子菜单也会被清除。 2级菜单根据1级索引,2级索引去删除。
移动菜单 根据拖动的位置,去判断当前拖动是否在原来的组或者是新的组去做处理。
具体可以看代码。
- 样式代码
@charset "utf-8";
* {
box-sizing: border-box;
}
#app-menu ul {
padding: 0;
}
#app-menu li {
list-style: none;
}
#app-menu {
overflow: hidden;
width: 100%;
}
.weixin-preview {
position: relative;
width: 320px;
height: 540px;
float: left;
margin-right: 10px;
border: 1px solid #e7e7eb;
}
.weixin-preview a {
text-decoration: none;
color: #616161;
}
.weixin-preview .weixin-hd .weixin-title {
color: #fff;
font-size: 15px;
width: 100%;
text-align: center;
position: absolute;
top: 33px;
left: 0px;
}
.weixin-preview .weixin-header{
text-align: center;
padding: 10px 0;
background-color: #616161;
color: #ffffff;
}
.weixin-preview .weixin-menu {
position: absolute;
bottom: 0;
left: 0;
right: 0;
border-top: 1px solid #e7e7e7;
background-position: 0 0;
background-repeat: no-repeat;
margin-bottom: 0px;
}
/*一级*/
.weixin-preview .weixin-menu .menu-item {
position: relative;
float: left;
line-height: 50px;
height: 50px;
text-align: center;
width: 33.33%;
border-left: 1px solid #e7e7e7;
cursor: pointer;
color: #616161;
}
/*二级*/
.weixin-preview .weixin-sub-menu {
position: absolute;
bottom: 60px;
left: 0;
right: 0;
border-top: 1px solid #d0d0d0;
margin-bottom: 0px;
background: #fafafa;
display: block;
padding: 0;
}
.weixin-preview .weixin-sub-menu .menu-sub-item {
line-height: 50px;
height: 50px;
text-align: center;
width: 100%;
border: 1px solid #d0d0d0;
border-top-width: 0px;
cursor: pointer;
position: relative;
color: #616161;
}
.weixin-preview .weixin-sub-menu .menu-sub-item.on-drag-over{
border-top: 2px solid #44b549;
}
.weixin-preview .menu-arrow {
position: absolute;
left: 50%;
margin-left: -6px;
}
.weixin-preview .arrow_in {
bottom: -4px;
display: inline-block;
width: 0px;
height: 0px;
border-width: 6px 6px 0px;
border-style: solid dashed dashed;
border-color: #fafafa transparent transparent;
}
.weixin-preview .arrow_out {
bottom: -5px;
display: inline-block;
width: 0px;
height: 0px;
border-width: 6px 6px 0px;
border-style: solid dashed dashed;
border-color: #d0d0d0 transparent transparent;
}
.weixin-preview .menu-item .menu-item-title, .weixin-preview .menu-sub-item .menu-item-title {
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
box-sizing: border-box;
}
.weixin-preview .menu-item.current, .weixin-preview .menu-sub-item.current {
border: 1px solid #44b549;
background: #fff;
color: #44b549;
}
.weixin-preview .weixin-sub-menu.show {
display: block;
}
.weixin-preview .icon_menu_dot {
/* background: url(../images/index_z354723.png) 0px -36px no-repeat; */
width: 7px;
height: 7px;
vertical-align: middle;
display: inline-block;
margin-right: 2px;
margin-top: -2px;
}
.weixin-preview .icon14_menu_add {
/* background: url(../images/index_z354723.png) 0px 0px no-repeat; */
width: 14px;
height: 14px;
vertical-align: middle;
display: inline-block;
margin-top: -2px;
}
.weixin-preview li:hover .icon14_menu_add {
/* background: url(../images/index_z354723.png) 0px -18px no-repeat; */
}
.weixin-preview .menu-item:hover {
color: #000;
}
.weixin-preview .menu-sub-item:hover {
background: #eee;
}
.weixin-preview li.current:hover {
background: #fff;
color: #44b549;
}
/*菜单内容*/
.weixin-menu-detail {
width: 600px;
padding: 0px 20px 5px;
background-color: #f4f5f9;
border: 1px solid #e7e7eb;
float: left;
min-height: 540px;
}
.weixin-menu-detail .menu-name {
float: left;
height: 40px;
line-height: 40px;
font-size: 18px;
}
.weixin-menu-detail .menu-del {
float: right;
height: 40px;
line-height: 40px;
color: #459ae9;
cursor: pointer;
}
.weixin-menu-detail .menu-input-group {
width: 540px;
margin: 10px 0 30px 0;
overflow: hidden;
}
.weixin-menu-detail .menu-label {
float: left;
height: 30px;
line-height: 30px;
width: 80px;
text-align: right;
}
.weixin-menu-detail .menu-input {
float: left;
width: 380px
}
.weixin-menu-detail .menu-input-text {
border: 0px;
outline: 0px;
background: #fff;
width: 300px;
padding: 5px 0px 5px 0px;
margin-left: 10px;
text-indent: 10px;
height: 35px;
}
.weixin-menu-detail .menu-tips {
color: #8d8d8d;
padding-top: 4px;
margin: 0;
}
.weixin-menu-detail .menu-tips.cursor {
color: #459ae9;
cursor: pointer;
}
.weixin-menu-detail .menu-input .menu-tips {
margin: 0 0 0 10px;
}
.weixin-menu-detail .menu-content {
padding: 16px 20px;
border: 1px solid #e7e7eb;
background-color: #fff;
}
.weixin-menu-detail .menu-content .menu-input-group {
margin: 0px 0 10px 0;
}
.weixin-menu-detail .menu-content .menu-label {
text-align: left;
width: 100px;
}
.weixin-menu-detail .menu-content .menu-input-text {
border: 1px solid #e7e7eb;
}
.weixin-menu-detail .menu-content .menu-tips {
padding-bottom: 10px;
}
.weixin-menu-detail .menu-msg-content {
padding: 0;
border: 1px solid #e7e7eb;
background-color: #fff;
}
.weixin-menu-detail .menu-msg-content .menu-msg-head {
overflow: hidden;
border-bottom: 1px solid #e7e7eb;
line-height: 38px;
height: 38px;
padding: 0 20px;
}
.weixin-menu-detail .menu-msg-content .menu-msg-panel {
padding: 30px 50px;
}
.weixin-menu-detail .menu-msg-content .menu-msg-select {
padding: 40px 20px;
border: 2px dotted #d9dadc;
text-align: center;
}
.weixin-menu-detail .menu-msg-content .menu-msg-select:hover {
border-color: #b3b3b3;
}
.weixin-menu-detail .menu-msg-content strong {
display: block;
padding-top: 3px;
font-weight: 400;
font-style: normal;
}
.weixin-menu-detail .menu-msg-content .menu-msg-title {
float: left;
width: 310px;
height: 30px;
line-height: 30px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.icon36_common {
width: 36px;
height: 36px;
vertical-align: middle;
display: inline-block;
}
.icon_msg_sender {
margin-right: 3px;
margin-top: -2px;
width: 20px;
height: 20px;
vertical-align: middle;
display: inline-block;
/* background: url(../images/msg_tab_z25df2d.png) 0 -270px no-repeat; */
}
.weixin-btn-group {
text-align: center;
width: 100%;
margin: 30px 0px;
overflow: hidden;
}
.weixin-btn-group .btn {
width: 100px;
border-radius: 0px;
}
#material-list {
padding: 20px;
overflow-y: scroll;
height: 558px;
}
#news-list {
padding: 20px;
overflow-y: scroll;
height: 558px;
}
#material-list table {
width: 100%;
}
2.2 右侧编辑菜单视图
右侧编辑菜单视图实现的比较简单,实现的功能:
1、修改菜单名称 2、删除菜单
具体的代码实现如下所示:
- js代码
<template>
<div>
<div class="menu-input-group" style="border-bottom: 2px #e8e8e8 solid">
<div class="menu-name">{{ button.name }}</div>
<div class="menu-del" @click="$emit('delMenu')">删除菜单</div>
</div>
<div class="menu-input-group">
<div class="menu-label">菜单名称</div>
<div class="menu-input">
<input
type="text"
name="name"
placeholder="请输入菜单名称"
class="menu-input-text"
v-model="button.name"
@input="checkMenuName(button.name)"
/>
<p class="menu-tips" style="color: #e15f63" v-show="menuNameBounds">
字数超过上限
</p>
<p class="menu-tips">
字数不超过{{ selectedMenuLevel == 1 ? "5" : "8" }}个汉字
</p>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
selectedMenuLevel: {
type: Number,
default: 1,
},
button: {
type: Object,
},
},
data() {
return {
menuNameBounds: false, //菜单长度是否过长
};
},
methods: {
//检查菜单名称长度
checkMenuName: function (val) {
if (this.selectedMenuLevel == 1 && this.getMenuNameLen(val) <= 10) {
this.menuNameBounds = false;
} else if (
this.selectedMenuLevel == 2 &&
this.getMenuNameLen(val) <= 16
) {
this.menuNameBounds = false;
} else {
this.menuNameBounds = true;
}
},
//获取菜单名称长度
getMenuNameLen: function () {
var len = 0;
for (var i = 0; i < val.length; i++) {
var a = val.charAt(i);
a.match(/[^\x00-\xff]/gi) != null ? (len += 2) : (len += 1);
}
return len;
},
},
};
</script>
修改菜单名称主要是通过props传选中的菜单对象过来,然后修改菜单名称,进行回显。
删除菜单通过 this.$emit 由父组件去操作,通过选中的菜单id去做删除操作。
到这里基本的菜单新增、编辑、删除、拖拽移动的功能基本实现了,希望对你有所帮助。