vue组件开发:构建响应式快捷导航

前言

快捷导航不仅能够显著提升系统的灵活性和用户交互性,还极大地增强了用户的操作体验。本文将展示如何在 vue 中实现一个既可自定义又具备响应式特性的快捷导航菜单。


一、实现思路

列表页

  • 结构设计

    定义页面结构,包含一个导航卡片和一个对话框组件;

    导航卡片中包含一个标题、添加按钮和导航项列表;

    使用 v-for 指令遍历 routeData 数组,动态生成每个导航项。

  • 数据管理

    data 中定义 routeData 数组,存储导航项的信息(如图标和标题);

    定义 dialogChild 变量,用于控制对话框的显隐状态。

  • 事件处理

    handleCellClick(item):处理点击导航项的事件,输出所点击的项的信息;
    deleteItem(index):处理删除导航项的事件,通过 splice 方法从 routeData 中移除指定项;
    navigationAdd():打开添加导航项的对话框,将 dialogChild 设置为 true

1.1 列表组件

html 复制代码
<template>
  <div class="navigation">
    <div class="cardContent">
      <el-card class="box-card">
        <div slot="header">
          <span />
          <span class="cardName">快捷导航</span>
          <el-link style="float: right" type="primary" @click="navigationAdd">添加</el-link>
        </div>
        <div v-if="routeData && routeData.length" class="gridContainer">
          <!-- 遍历路由数据,生成每个导航项 -->
          <div v-for="(item, index) in routeData" :key="index" class="gridContent">
            <div class="content" :title="item.title" @click.stop="handleCellClick(item)">
              <p><img :src="item.imgUrl" alt="" /></p>
              <p>{{ item.title }}</p>
            </div>
            <div class="overlay">
              <i title="删除" class="el-icon-circle-close" @click.stop="deleteItem(index)"></i>
            </div>
          </div>
        </div>
        <div v-else><el-empty description="暂无数据"></el-empty></div>
      </el-card>
    </div>
    <navigation-dialog :dialogChild.sync="dialogChild" />
  </div>
</template>

<script>
import NavigationDialog from "./navigationDialog.vue";
export default {
  components: { NavigationDialog },
  data() {
    return {
      dialogChild: false, // 控制对话框显隐状态
      routeData: [
        //模拟数据
        {
          imgUrl: require("../assets/demo.png"),
          title: "数据统计",
        },
        {
          imgUrl: require("../assets/demo.png"),
          title: "订单管理",
        },
        {
          imgUrl: require("../assets/demo.png"),
          title: "汇总信息",
        },
        {
          imgUrl: require("../assets/demo.png"),
          title: "客服管理",
        },
        {
          imgUrl: require("../assets/demo.png"),
          title: "财物中心",
        },
        {
          imgUrl: require("../assets/demo.png"),
          title: "系统管理",
        },
        {
          imgUrl: require("../assets/demo.png"),
          title: "系统监控",
        },
        {
          imgUrl: require("../assets/demo.png"),
          title: "系统工具",
        },
      ],
    };
  },
  methods: {
    // 点击导航项的处理函数
    handleCellClick(item) {
      console.log("item:", item);
    },
    // 删除导航项的处理函数
    deleteItem(index) {
      this.routeData.splice(index, 1);
    },
    // 打开添加导航项对话框的处理函数
    navigationAdd() {
      this.dialogChild = true;
    },
  },
};
</script>

<style lang="less" scoped>
.navigation {
  width: 36%;
  padding: 16px;
  .cardContent {
    span:first-child {
      display: inline-block;
      width: 10px;
      height: 10px;
      background: #409eff;
      margin-right: 10px;
    }
    .cardName,
    .el-link {
      font-weight: bold;
    }
    .gridContainer {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
      gap: 16px;
      padding: 16px;
      .gridContent {
        width: 100px;
        height: 100px;
        border-radius: 4px;
        position: relative;
        box-sizing: border-box;
        overflow: hidden;
        background: linear-gradient(to bottom, rgba(106, 183, 255, 0.7), rgba(106, 183, 255, 0.2));
        display: flex;
        align-items: center;
        justify-content: center;
        text-align: center;
        .content {
          cursor: pointer;
          color: rgb(60, 60, 60);
          img {
            width: 50px;
            height: 50px;
          }
          p:nth-child(2) {
            margin-top: 2px;
            width: 92px;
            padding: 0px 4px;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
          }
        }
      }
      .overlay {
        position: absolute;
        top: 3px;
        right: 3px;
      }
      .el-icon-circle-close {
        cursor: pointer;
        font-size: 18px;
        color: rgb(255, 255, 255);
      }
      .el-icon-circle-close:hover {
        color: rgba(255, 255, 255, 0.5);
      }
    }
  }
}
::v-deep {
  .el-checkbox-button__inner {
    margin: 0 8px 10px 8px;
    border: 1px solid #d3d3d3;
    border-radius: 4px !important;
  }
}
</style>

弹框页

  • 结构设计

    使用 <el-dialog> 组件创建一个对话框,包含标题和内容区域;

    在内容区域中,使用 v-for 遍历 cities 数组,生成每个菜单的选项和复选框。

  • 数据管理

    data 中定义 cities 数组,模拟系统管理和监控等菜单项;

    定义 checkStates 对象,用于存储每个菜单的选中状态(全选、部分选中和选中的城市)。

  • 事件处理

    initializeCheckStates():初始化每个菜单的选中状态,设置全选状态和选中的菜单;
    handleCheckAllChange(item):处理全选复选框的变化,更新选中状态;
    handleCheckedCitiesChange(item):处理菜单复选框的变化,更新全选和部分选中状态;
    submit():提交选中的信息,过滤出选中的路由项,并输出到控制台;
    cancel():取消操作,重置选中状态并关闭对话框。

2.1 弹框组件

html 复制代码
<template>
  <el-dialog :close-on-click-modal="false" title="添加快捷导航" width="60%" :visible.sync="dialogVisible" @close="cancel">
    <el-card class="box-card">
      <!-- 遍历菜单数组,生成每个菜单的选项 -->
      <div v-for="(item, index) in cities" :key="index" class="checkboxContent">
        <span class="navTit">{{ item.label }}</span>
        <!-- 全选复选框 -->
        <el-checkbox v-model="checkStates[item.label].checkAll" :indeterminate="checkStates[item.label].isIndeterminate" @change="handleCheckAllChange(item)">全选</el-checkbox>
        <!-- 菜单的复选框组 -->
        <el-checkbox-group v-model="checkStates[item.label].checkedCities" size="medium" @change="handleCheckedCitiesChange(item)">
          <el-checkbox-button v-for="(e, isx) in item.router" :key="isx" :label="e.title" border>{{ e.title }}</el-checkbox-button>
        </el-checkbox-group>
      </div>
      <div class="btns">
        <el-button size="medium" type="primary" @click="submit">保 存</el-button>
        <el-button size="medium" @click="cancel">取 消</el-button>
      </div>
    </el-card>
  </el-dialog>
</template>
<script>
export default {
  props: {
    dialogChild: {
      type: Boolean,
      default: false,
    },
  },
  data() {
    return {
      dialogVisible: false, // 控制对话框显隐状态
      cities: [
        //模拟数据
        {
          label: "系统管理",
          router: [
            { title: "用户管理", id: 0 },
            { title: "角色管理", id: 1 },
            { title: "菜单管理", id: 2 },
            { title: "部门管理", id: 3 },
            { title: "日志管理", id: 4 },
            { title: "字典管理", id: 5 },
          ],
        },
        {
          label: "系统监控",
          router: [
            { title: "在线用户", id: 6 },
            { title: "定时任务", id: 7 },
          ],
        },
        {
          label: "系统工具",
          router: [{ title: "表单构建", id: 8 }],
        },
      ],
      checkStates: {}, // 存储每个菜单的选中状态
    };
  },
  watch: {
    dialogChild: {
      handler(newName) {
        this.dialogVisible = newName;
      },
      deep: true,
    },
  },
  mounted() {
    this.initializeCheckStates();
  },
  methods: {
    // 初始化每个菜单的选中状态
    initializeCheckStates() {
      this.cities.forEach((item) => {
        this.$set(this.checkStates, item.label, {
          checkAll: false, // 全选状态
          checkedCities: [], // 选中的菜单
          isIndeterminate: false, // 是否为部分选中状态
        });
      });
    },
    // 处理全选复选框的变化
    handleCheckAllChange(item) {
      this.checkStates[item.label].checkedCities = this.checkStates[item.label].checkAll ? item.router.map((e) => e.title) : [];
      this.checkStates[item.label].isIndeterminate = false; // 重置部分选中状态
    },
    // 处理菜单复选框的变化
    handleCheckedCitiesChange(item) {
      const checkedCount = this.checkStates[item.label].checkedCities.length; // 选中的菜单数量
      this.checkStates[item.label].checkAll = checkedCount === item.router.length; // 更新全选状态
      this.checkStates[item.label].isIndeterminate = checkedCount > 0 && checkedCount < item.router.length; // 更新部分选中状态
    },
    // 提交选中的信息
    submit() {
      const selectedInfo = this.cities.map((item) => {
        const { checkedCities } = this.checkStates[item.label]; // 获取当前导航项的选中状态
        const selectedRoutes = item.router.filter((route) => checkedCities.includes(route.title)); // 过滤出选中的路由项
        return {
          label: item.label, // 导航项标签
          selectedRoutes: selectedRoutes.map((route) => ({
            title: route.title,
            id: route.id,
          })), // 选中的路由项
        };
      });
      console.log("选中的信息:", selectedInfo); // 输出选中的信息
    },
    // 取消操作
    cancel() {
      this.initializeCheckStates(); // 重置选中状态
      this.$emit("update:dialogChild", false);
    },
  },
};
</script>

<style lang="less" scoped>
.checkboxContent {
  margin-bottom: 16px;
  .navTit {
    margin-right: 10px;
    color: rgb(41, 107, 255);
    font-weight: bold;
    font-size: 16px;
  }
  .el-checkbox {
    margin: 16px 0;
  }
}
.btns {
  margin-top: 16px;
  display: flex;
  justify-content: right;
}
::v-deep {
  .el-checkbox-button__inner:nth-child(n + 2) {
    margin: 0 !important;
    margin: 0 10px 10px 0 !important;
  }
}
</style>

1.3 组件通信

使用 props$emit 实现父子组件之间的通信。A 页面通过 dialogChild 属性控制 B 页面的对话框显隐状态,并通过 update:dialogChild 事件通知 B 页面关闭对话框。


二、实现效果

相关推荐
下雪了 ~1 小时前
HTTP和HTTPS的区别有哪些?
服务器·前端·笔记·网络协议·计算机网络
Krorainas1 小时前
将PDF流使用 canvas 绘制然后转为图片展示在页面上(二)
前端·javascript·pdf·react
吉吉安2 小时前
使用echarts实现3d柱状图+折线图
前端·javascript·echarts
yanlele2 小时前
企业级 AI Coding 已经来临, 目前其发展可能已经超越想象,对此我的一些思考
前端·后端·openai
m0_748244832 小时前
WebSpoon9.0(KETTLE的WEB版本)编译 + tomcatdocker部署 + 远程调试教程
前端
大饼酥2 小时前
使用Nexus3搭建npm私有仓库
前端·npm·node.js
苦逼的猿宝2 小时前
React+Antd修改Table组件滚动条样式
前端·javascript·react.js
yqcoder2 小时前
event 用 ts 类型声明
前端·javascript·react.js
芝芝葡萄2 小时前
react-dnd 拖拽事件与输入框的文本选中冲突
前端·react.js·前端框架
搜狐技术产品小编20232 小时前
Flutter Navigator2.0的原理和Web端实践
前端·flutter