仿写微信公众号后台添加菜单页面

154 阅读1分钟

我正在参加「掘金·启航计划」

1、背景

由于公司业务需求,需要实现微信公众号营销相关的功能,其中有一块需要实现公众号后台管理中的自定义菜单的页面,作为一个前端菜鸡提前写了个demo,在此记录一下,希望对你有所帮助。 实现的效果图如下:

微信公众号自定义菜单.gif

2、实现

可以看到页面分为两部分,左侧新增菜单视图 + 右侧菜单名称编辑视图。

主要实现的功能有:

  1. 新增/删除 1、2级菜单;
  2. 修改1、2级菜单名称;
  3. 拖拽移动2级菜单。

下面看下具体实现。

2.1 左侧新增菜单视图

首先对左侧菜单视图进行拆分,可以分为 背景 + 底部一级菜单 + 底部二级菜单

底部一级菜单、底部二级菜单都采用绝对定位。

  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级索引去删除。

移动菜单 根据拖动的位置,去判断当前拖动是否在原来的组或者是新的组去做处理。

具体可以看代码。

  1. 样式代码
@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、删除菜单

具体的代码实现如下所示:

  1. 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去做删除操作。

到这里基本的菜单新增、编辑、删除、拖拽移动的功能基本实现了,希望对你有所帮助。