08335 / hivui-platform-template
hivui平台项目模板
Newer
Older
hivui-platform-template / project / hivuiMain / views / layout / components / conciseSidebar / concisemenu.vue
<template>
  <div
    class="concise-menu"
    ref="conciseMenu"
    :style="menuStyle"
    @mouseenter="handleMouseEnter"
    @mouseleave="handleMouseLeave"
  >
    <div class="menu-search">
      <div class="menu-title">
        <div class="title">功能搜索:</div>
      </div>
      <div class="menu-search-box">
        <input
          type="text"
          class="menu-search-input"
          placeholder="支持拼音首字母搜索"
          v-model.trim="searchWord"
        />
        <i class="iconfont icon-search"></i>
      </div>
    </div>
    <div class="menu-content">
      <div class="level1List" v-if="searchWord == ''">
        <!-- 一级菜单列表 -->
        <ul class="area">
          <li v-for="item in menus" :key="item.resId">
            <div class="menu-item">
              <div
                class="level first-level"
                :class="{
                  active: curresId == item.resId,
                  noChild: !item.children || !item.children.length,
                }"
                @click="handleFirstLevelClick(item)"
                @mouseenter="handleFirstLevelMouseEnter(item)"
              >
                <span class="title">{{ item.name }}</span>
                <i
                  v-if="item.children && item.children.length"
                  class="iconfont icon-arrow-right"
                ></i>
              </div>
            </div>
          </li>
        </ul>
        <!-- 二级及以下菜单列表 -->
        <ul class="area child-menu">
          <li v-for="item in childrenData" :key="item.resId">
            <div class="menu-item">
              <div>
                <div
                  class="level second-level"
                  :class="{
                    active:
                      curresId == item.parentId &&
                      item.children &&
                      item.children.length,
                    noChild: !item.children || !item.children.length,
                  }"
                  @click="handleSecondLevelClick(item)"
                >
                  <span class="title">{{ item.name }}</span>
                  <i
                    v-if="item.children && item.children.length"
                    class="iconfont icon-arrow-right"
                  ></i>
                </div>
              </div>

              <!-- 三级菜单 -->
              <ul
                class="child-area"
                v-if="item.children && item.children.length"
              >
                <template v-for="child in item.children">
                  <!-- 普通三级菜单项 -->
                  <li
                    :key="child.resId"
                    v-if="
                      child.isShow && !(child.children && child.children.length)
                    "
                  >
                    <div class="menu-item">
                      <div class="third-level" @click="handleOpenFunc(child)">
                        <span class="title">{{ child.name }}</span>
                        <i
                          v-if="child.children && child.children.length"
                          class="iconfont icon-arrow-right"
                        ></i>
                      </div>
                    </div>
                  </li>

                  <!-- 嵌套子菜单组件 -->
                  <nav-sub-menus
                    :level="0"
                    :menuitem="child"
                    :parent="conciseMenu"
                    v-if="
                      child.isShow && child.children && child.children.length
                    "
                    :key="child.resId"
                  >
                  </nav-sub-menus>
                </template>
              </ul>
            </div>
          </li>
        </ul>
      </div>
      <div class="searchResultList" v-else>
        <ul>
          <li
            v-for="(searchItem, index) in searchList"
            :key="'search-' + index"
            :data-index="index"
            :title="searchItem.name"
            @click="handleOpenFunc(searchItem)"
          >
            <itemIcon :item="searchItem"></itemIcon>
            <span>{{ searchItem.name }}</span>
          </li>
          <li v-show="searchList.length == 0">
            {{ $t("hivuiMain_nodata") }}
          </li>
        </ul>
      </div>
    </div>
  </div>
</template>

<script>
import itemIcon from "@main/views/layout/components/allFuncMenu/itemIcon";
import NavSubMenus from "@main/views/layout/components/kyMenus/menus.vue";
import pinyin from "js-pinyin";
import cloneDeep from "lodash/cloneDeep";
export default {
  name: "ConciseMenu",
  inject: ["addTab"],
  provide() {
    let me = this;
    return {
      hideMenus() {
        me.handleMouseLeave();
      },
    };
  },
  components: { NavSubMenus, itemIcon },
  props: {
    targetElRect: {
      type: Object,
      required: true,
    },
  },
  data() {
    return {
      searchWord: "",
      searchList: [],

      isVisible: false,
      isMouseOverMenu: false,
      curresId: "",
      childrenData: [],
    };
  },
  watch: {
    targetElRect: {
      handler() {
        this.updatePosition();
      },
      deep: true,
    },
    searchWord(newVal, oldVal) {
      this.seldIndex = 0;
      this.doSearch(newVal);
    },
  },
  computed: {
    menuStyle() {
      return {
        position: "absolute",
        top: this.isVisible ? this.position.top + "px" : "-9999px",
        left: this.position.left + "px",
        opacity: this.isVisible ? 1 : 0,
        visibility: this.isVisible ? "visible" : "hidden",
        width: this.isVisible ? "600px" : "0",
        transition: " opacity 0.3s ease 0.1s, visibility 0.3s ease 0.1s",
        backgroundColor: "#fff",
        border: "1px solid #ddd",
        boxShadow: "0 2px 10px 0 #ddd",
        boxSizing: "border-box",
        overflow: "hidden",
        zIndex: 1000,
      };
    },
    position() {
      return {
        top: 0,
        left: 0,
      };
    },
    menus() {
      function sort(list) {
        let dirList = list.filter((item) => {
          return item.type == "dir";
        });
        let funcList = list.filter((item) => {
          return item.type != "dir";
        });
        list = funcList.concat(dirList);
        for (let item of list) {
          if (item.type == "dir") {
            if (item.children && item.children.length > 0) {
              item.children = sort(item.children);
            }
          }
        }
        return list;
      }
      return cloneDeep(this.$store.getters.menus);
    },
  },
  methods: {
    /**
     * 显示菜单
     */
    show() {
      this.isVisible = true;
      this.$nextTick(() => {
        this.updatePosition();
      });
    },

    /**
     * 隐藏菜单
     */
    hide() {
      this.isVisible = false;
    },

    /**
     * 更新菜单位置
     */
    updatePosition() {
      if (!this.isVisible) return;

      this.$nextTick(() => {
        const menuEl = this.$el;
        if (!menuEl) return;

        const rect = this.targetElRect;
        if (!rect) return;

        let top = rect.top;
        let left = rect.right;

        // 应用位置
        menuEl.style.top = top + "px";
        menuEl.style.left = left + "px";
      });
    },

    /**
     * 鼠标进入菜单区域处理
     */
    handleMouseEnter(e) {
      if (e) {
        const target = e.target || e.srcElement;
        if (target.closest && target.closest(".pl-menus-item")) {
          this.isMouseOverMenu = true;
          return;
        }
      }
      this.isMouseOverMenu = true;
    },

    /**
     * 鼠标离开菜单区域处理
     */
    handleMouseLeave(e) {
      if (e) {
        // 检查鼠标是否移向.pl-menus-item类元素
        const relatedTarget = e.relatedTarget || e.toElement;
        if (
          relatedTarget &&
          relatedTarget.closest &&
          relatedTarget.closest(".pl-menus-item")
        ) {
          this.isMouseOverMenu = true;
          return;
        }
      }
      this.isMouseOverMenu = false;
      this.hide();
    },

    /**
     * 处理一级菜单点击事件
     */
    handleFirstLevelClick(item) {
      if (!item.children || !item.children.length) {
        this.handleOpenFunc(item);
      }
    },

    /**
     * 处理一级菜单鼠标悬停事件
     */
    handleFirstLevelMouseEnter(item) {
      if (item.children && item.children.length) {
        this.handleOpenFunc(item, true);
      }
    },

    /**
     * 处理二级菜单点击事件
     */
    handleSecondLevelClick(item) {
      if (!item.children || !item.children.length) {
        this.handleOpenFunc(item);
      }
    },

    /**
     * 打开功能页面
     * @param {Object} item - 菜单项
     * @param {Boolean} isTabClick - 是否是标签页点击(仅设置子菜单数据)
     */
    async handleOpenFunc(item, isTabClick) {
      let me = this;
      if (isTabClick) {
        this.setChildrenData(item.resId);
        return;
      }

      if (item.type == "link") {
        window.open(item.resUrl, item.name);
        return;
      } else if (item.type == "sso") {
        item.ssoUrl = await me.$store.dispatch("user/openSSOFuncPage", {
          serviceUrl: item.resUrl,
        });
      }

      // 添加标签页
      me.addTab(item);

      setTimeout(() => {
        me.handleMouseLeave && me.handleMouseLeave();
      }, 100);
    },

    /**
     * 设置子菜单数据
     * @param {String} resId - 菜单资源ID
     */
    setChildrenData(resId) {
      this.curresId = resId;
      const menu = this.menus.find((m) => m.resId === resId);
      if (menu && menu.children) {
        this.childrenData = menu.children;
      } else {
        this.childrenData = [];
      }
    },
    //获取搜索列表数据
    doSearch(cnKey) {
      let me = this;
      let list = [],
        records = me.$store.getters.menusList,
        hideMenuItemList = me.$store.getters.hideMenuItemList;
      let py = pinyin.getCamelChars(cnKey).toLocaleLowerCase();
      let re = new RegExp("^[a-zA-Z]+$");
      for (let i = 0, l = records.length; i < l; i++) {
        let item = records[i];
        if (!(!item.isShow || hideMenuItemList.indexOf(item.parentId) != -1)) {
          if (item.parentId == -1 && item.type == "root") {
            continue;
          }
          if (re.test(cnKey)) {
            let y = pinyin.getCamelChars(item.name).toLocaleLowerCase();
            if (y.indexOf(py) > -1 && item.type != "dir") {
              list.push(item);
            }
          } else {
            if (item.name.indexOf(cnKey) > -1 && item.type != "dir") {
              list.push(item);
            }
          }
        }
      }
      me.$set(me, "searchList", list);
      // me.$nextTick(() => {
      //   me.countHeight();
      //   me.$refs.listBox.scrollTo({
      //     top: 0,
      //   });
      // });
    },
  },
  mounted() {
    if (this.menus.length > 0) {
      const menu = this.menus[0];
      this.setChildrenData(menu.resId);
    }
  },
};
</script>

<style lang="less" scoped>
.concise-menu {
  border-radius: 5px;

  ul,
  li {
    padding: 0;
    margin: 0;
    list-style: none;
  }

  * {
    box-sizing: border-box;
  }
  .menu-search {
    display: flex;
    background-color: #ebf1f7;
    border-bottom: 1px solid #ced7e0;
    padding: 5px 10px;
    align-items: center;

    .menu-title {
      // width: 110px;
      text-align: right;
      padding-right: 10px;
    }
    .menu-search-box {
      flex: 1;
      display: flex;
      align-items: center;
      background-color: #fff;
      padding-right: 10px;
      border-radius: 5px;
      input {
        flex: 1;
        border: none;
        outline: none;
        line-height: 30px;
        padding: 0 15px;
        border-radius: 5px;
      }
      i {
        font-size: 20px;
        cursor: pointer;
      }
    }
  }

  .menu-content {
    position: relative;
    .level1List{
      min-height: 100px;
      max-height: calc(100vh - 200px);
      overflow: auto;
    }
    .area {
      display: flex;
      flex-direction: column;

      li{
        &:first-child{
          .menu-item{
            .first-level{
              border-top: none;
            }
          }
        }
      }

      &.child-menu {
        width: calc(100% - 120px);
        height: 100%;
        position: absolute;
        top: 0;
        right: 0;
        background-color: #fff;
        overflow: auto;
      }

      .menu-item {
        display: flex;

        .level {
          position: relative;
          width: 120px;
          text-align: right;
          padding-right: 30px;
          i {
            position: absolute;
            right: 5px;
            top: 50%;
            transform: translateY(-50%);
          }

          &.noChild {
            cursor: pointer;
          }

          &.first-level {
            cursor: pointer;
            background-color: #ebf1f7;
            border: 1px solid #ced7e0;
            border-bottom: none;
            line-height: 32px;
          
            &.active {
              color: #0066cc;
              background-color: #fff;
              border-right-color: transparent;
              cursor: default;
            }

            &:hover {
              color: #0066cc;
            }
          }

          &.second-level {
            flex: 0 0 120px;
            padding-top: 5px;
            padding-left: 5px;
            padding-bottom: 5px;

            &.active {
              color: #0066cc;
              background-color: #fff;
              border-color: transparent;
            }

            &:hover {
              color: #0066cc;
            }
          }
        }
      }

      .child-area {
        width: 100%;
        display: flex;
        flex-wrap: wrap;
        gap: 8px;
        padding: 8px 0;
        border-bottom: 1px solid #ced7e0;

        .third-level {
          padding: 0 8px;
          border-left: 1px solid #ced7e0;
          cursor: pointer;

          &:hover {
            color: #0066cc;
          }
        }

        ::v-deep .pl-menus-item {
          padding: 0 8px;
          border-left: 1px solid #ced7e0;

          &.pl-menus-itemSeld {
            color: #0066cc;
          }

          .title {
            position: relative;

            .txt {
              display: inline-block;
              padding-right: 10px;

              i {
                position: absolute;
                right: -5px;
                top: 50%;
                transform: translateY(-50%);
              }
            }
          }
        }
      }
    }

    .searchResultList {
      width: 100%;
      min-height: 100px;
      max-height: calc(100vh - 200px);
      overflow: auto;
      padding: 5px 10px;
      ul {
        display: flex;
        flex-wrap: wrap;
        padding-bottom: 10px;
        li {
          flex: 0 0 33.3%;
          white-space: nowrap;
          overflow: hidden;
          text-overflow: ellipsis;
          line-height: 32px;
          font-size: 14px;
          display: flex;
          align-items: center;
          cursor: pointer;
          &:hover {
            color: #06c;
          }
        }
        .itemSeld {
          color: #f00;
        }
      }
    }
  }
}
</style>