vue 管理系统顶部tags浏览历史实现 tab版

废话

demo预览
在这里插入图片描述
以前自己手写了一个类似的功能,用的事tags组件写的,要自己手动监听,可视宽度, tag宽度, 去显示左右操作按钮, 功能是实现了, 但是 点击的时候不能自动定位到当前页,所以用tab组件, 又写了一个, 推荐使用,原来的那个也保留了 可在个人信息下拉框里 进行切换
在这里插入图片描述
完成了哪些

  1. 最后一个不能关闭
  2. 点击路由菜单,判断有无存在,没有就添加,有就定位到上面
  3. 点击跳转,点击X可关闭
  4. 关闭当前页,自动跳到下一个tag页面
  5. 如果当前页在最后一个,默认跳到上一个tag页面
  6. 右键菜单,刷新,关闭右侧,关闭所有, 关闭左侧
  7. 自动相应有无左右两侧按钮

正文

不用任何vuex,乱七八糟的方法,全在一个文件,粘贴即用
在这里插入图片描述
tabs代码如下

<template>
  <div>
    <div class="tabs_content">
      <el-tabs
        closable
        v-model="active"
        @contextmenu.prevent.native="openContextMenu($event)"
        type="editable-card"
        @tab-remove="removeTab"
        @tab-click="tabClick"
      >
        <el-tab-pane
          class="isActive"
          v-for="item in tabList"
          :key="item.name"
          :label="item.name"
          :name="item.path"
        >
        </el-tab-pane>
      </el-tabs>
    </div>
    <!-- 右键菜单 -->
    <ul
      v-show="contextMenu.isShow"
      :style="{ left: contextMenu.menuLeft + 'px', top: '100px' }"
      class="el-dropdown-menu el-popper"
      x-placement="bottom-end"
    >
      <li class="el-dropdown-menu__item" @click="closeRightTag">关闭右侧</li>
      <li class="el-dropdown-menu__item" @click="closeLeftTag">关闭左侧</li>
      <li class="el-dropdown-menu__item" @click="closeOtherTag">关闭其它</li>
      <li
        v-if="this.active == this.contextMenu.name"
        class="el-dropdown-menu__item"
        @click="refresh"
      >
        刷新页面
      </li>
      <div x-arrow="" class="popper__arrow" style="left: 44px"></div>
    </ul>
  </div>
</template>

<script>
export default {
  data() {
    return {
      active: "/First/page1",
      tabList: [],
      // 右键的元素
      contextMenu: {
        name: "",
        menuLeft: 0,
        isShow: false,
        label: ""
      }
    };
  },
  watch: {
    $route() {
      this.getThisPage();
    }
  },
  created() {
    // 监听页面刷新
    window.addEventListener("beforeunload", e => {
      console.log(1111);
      console.log(this.active, this.tabList);
      localStorage.setItem(
        "tagInfo2",
        JSON.stringify({
          active: this.active,
          tabList: this.tabList
        })
      );
    });
    let tagInfo2 = localStorage.getItem("tagInfo2")
      ? JSON.parse(localStorage.getItem("tagInfo2"))
      : {
          active: "/First/page1",
          tabList: [
            {
              name: "首页",
              path: "/First/page1"
            }
          ]
        };
    console.log(tagInfo2);
    this.active = tagInfo2.active;
    this.tabList = tagInfo2.tabList;
  },
  mounted() {
    var that = this;
    document.addEventListener("click", function(e) {
      that.contextMenu.isShow = false;
    });
  },
  methods: {
    // 判断当前页
    getThisPage() {
      let currentPgae = this.$route,
        index = this.tabList.findIndex(tag => tag.path == currentPgae.path);
      if (index === -1) {
        this.tabList.push({
          name: this.$t("router." + currentPgae.name),
          path: currentPgae.path
        });
      }
      // 当前选择页
      this.active = currentPgae.path;
    },
    // 右键
    openContextMenu(e) {
      if (e.srcElement.id) {
        this.contextMenu.name = e.srcElement.id.substr(4);
        this.contextMenu.menuLeft = e.clientX - 45;
        this.contextMenu.isShow = true;
        this.contextMenu.label = e.srcElement.innerText;
      }
    },
    // 点击
    tabClick(tab) {
      console.log(tab);
      this.$router.push(tab.name);
      this.active = tab.name;
    },
    // 移除
    removeTab(targetPath) {
      let tabs = this.tabList;
      if (tabs.length <= 1) {
        this.$message.warning("这是最后一页, 不能在关闭了");
      } else {
        let activePath = this.active;
        if (activePath === targetPath) {
          tabs.forEach((tab, index) => {
            if (tab.path === targetPath) {
              let nextTab = tabs[index + 1] || tabs[index - 1];
              if (nextTab) {
                activePath = nextTab.path;
              }
            }
          });
        }
        this.active = activePath;
        this.$router.push(activePath);
        this.tabList = tabs.filter(tab => tab.path !== targetPath);
      }
    },
    // 刷新
    refresh() {
      this.$router.go(0);
    },
    // 关闭右侧
    closeRightTag() {
      let currentPath = this.contextMenu.name,
        index = this.tabList.findIndex(tag => tag.path == currentPath);
      this.tabList.splice(index + 1, this.tabList.length - index);
      this.active = currentPath;
      this.$router.push(currentPath);
    },
    // 关闭左侧
    closeLeftTag() {
      let currentPath = this.contextMenu.name,
        index = this.tabList.findIndex(tag => tag.path == currentPath);
      this.tabList.splice(0, index);
      this.active = currentPath;
      this.$router.push(currentPath);
    },
    // 关闭其它
    closeOtherTag() {
      let currentPath = this.contextMenu.name,
        currentName = this.contextMenu.label;
      this.tabList = [
        {
          name: currentName,
          path: currentPath
        }
      ];
      this.active = currentPath;
      this.$router.push(currentPath);
    }
  }
};
</script>

<style lang="less" scoped>
.tabList-view-container {
  // height: 40px;
  width: 100%;
  background: #f0f2f5;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0 8px;
}
.tabs_content {
  width: 100%;
  padding: 0 8px;
  box-sizing: border-box;
}
/deep/.el-tabs__header {
  margin: 0px;
}
/deep/.el-tabs--card > .el-tabs__header {
  border: 0px !important;
}
/deep/.el-tabs__nav {
  border: 0px !important;
}
/deep/.el-tabs__nav-wrap::after {
  height: 0px;
}
/deep/.el-tabs__item {
  margin-right: 4px;
  border-radius: 4px;
  height: 32px;
  line-height: 33px;
  background: #f7f7f8;
  border: 1px solid #e6e6e9;
  padding: 0 20px !important;
}
/deep/.el-tabs__item.is-active {
  background: #fff;
  border: 1px solid #fff;
  outline-color: #fff;
}
/deep/.el-tabs__item .el-icon-close {
  margin-left: 16px;
}
/deep/.el-tabs__nav-next,
/deep/.el-tabs__nav-prev {
  line-height: 46px;
  width: 30px;
  text-align: center;
}
/deep/.el-tabs__nav-wrap.is-scrollable {
  padding: 0 30px;
}
</style>

笔记

elementui tab
原来tags写的

注意自己的路由表 name 和 path , 其他ui 可根据自己需求改动

<think>嗯,用户给了一段Vue的模板代码,想要详细解释一下。首先,我需要仔细看看这段代码的结构。看起来像是一个布局组件,包含侧边栏、导航栏、标签页和主内容区域。用户可能是在学习Vue前端框架,或者是遇到了布局相关的问题需要帮助。 首先,我会分析模板的结构。最外层是一个带有classObj和app-wrapper的div。里面有一个条件渲染的drawer-bg,用于移动端点击外部关闭侧边栏。然后有sidebar-container侧边栏组件,接着是main-container主容器,里面包含fixed-header的导航栏和tags-view,然后是app-main主内容区域。 接下来,我需要考虑用户可能的疑问点。比如,classObj的作用是什么,device和sidebar.opened这些响应式数据是怎么来的,fixed-header和needTagsView这些功能是如何实现的。还有各个组件如sidebar、navbar、tags-view、app-main的功能和它们之间的协作。 用户可能还想知道这段代码是如何实现响应式布局的,特别是在移动设备上的表现,比如点击外部关闭侧边栏的功能。另外,关于CSS类名的应用,比如fixed-header如何固定头部,tags-view的作用是否需要进一步解释。 需要确认是否要详细解释每个组件的作用,比如Sidebar、Navbar、TagsView、AppMain的具体功能,以及它们在这个布局中的位置和交互。可能还需要提到Vue的响应式原理,比如如何通过Vuex或props来管理device和sidebar的状态。 另外,用户可能对代码中的条件渲染感兴趣,比如v-if="device==='mobile'&&sidebar.opened"这个条件是如何触发的,handleClickOutside方法的实现逻辑。还有needTagsView这个条件是如何判断是否需要显示标签页的,是来自Vuex还是组件的props。 还要注意代码中使用的CSS类名,比如app-wrapper、drawer-bg、sidebar-container、main-container、fixed-header等,可能需要解释它们的样式作用,特别是布局方面的,比如flex布局或定位方式。 最后,用户可能希望了解这段代码在整体项目中的应用场景,比如是否是一个通用的布局组件,如何被其他页面组件引用,以及可能存在的优化点或最佳实践,比如使用动态class绑定、条件渲染的最佳方式等。 总结一下,需要从模板结构、组件功能、响应式设计、状态管理、样式布局以及可能的优化建议等方面进行详细解释,确保用户能够全面理解这段代码的工作原理和实现细节。</think>以下是该Vue布局组件的详细解析(基于ElementUI风格的管理系统典型实现): --- ### 一、整体结构图示 ```mermaid graph TD A[app-wrapper] --> B[drawer-bg遮罩层] A --> C[sidebar-container侧边栏] A --> D[main-container主区域] D --> E[fixed-header顶部导航区] E --> F[navbar导航栏] E --> G[tags-view标签页] D --> H[app-main内容区] ``` --- ### 二、模板核心代码解析 #### 1. 根容器 ```vue <div :class="classObj" class="app-wrapper"> ``` - **功能**:布局根容器 - **动态class**: ```javascript // 典型实现(计算属性) computed: { classObj() { return { hideSidebar: !this.sidebar.opened, // 侧边栏折叠状态 openSidebar: this.sidebar.opened, // 侧边栏展开状态 withoutAnimation: this.sidebar.withoutAnimation, // 禁用动画标记 mobile: this.device === 'mobile' // 移动端标识 } } } ``` #### 2. 移动端遮罩层 ```vue <div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside" /> ``` - **触发条件**:移动端 + 侧边栏展开状态 - **交互逻辑**: ```javascript methods: { handleClickOutside() { // 典型实现:关闭侧边栏 this.$store.dispatch('app/closeSidebar', { withoutAnimation: false }) } } ``` #### 3. 侧边栏区域 ```vue <sidebar class="sidebar-container" /> ``` - **核心功能**: - 展示菜单导航 - 集成路由跳转逻辑 - 响应折叠状态变化 #### 4. 主内容区结构 ```vue <div class="main-container"> <div :class="{'fixed-header':fixedHeader}"> <navbar /> <tags-view v-if="needTagsView"></tags-view> </div> <app-main /> </div> ``` - **fixed-header作用**: ```css .fixed-header { position: fixed; top: 0; right: 0; z-index: 9; width: calc(100% - sidebar-width); /* 动态计算宽度 */ } ``` --- ### 三、核心组件说明 | 组件名称 | 功能描述 | |--------------|--------------------------------------------------------------------------| | `<sidebar>` | 侧边导航菜单,通常集成`el-menu`组件 | | `<navbar>` | 顶部导航栏,包含用户信息、系统设置、全屏切换等功能按钮 | | `<tags-view>`| 多标签页管理组件,展示已打开的路由页签(需配合`keep-alive`使用) | | `<app-main>` | 路由内容容器,通常包含以下结构:<br>`<router-view><transition>...</transition></router-view>` | --- ### 四、响应式设计机制 #### 1. 设备类型检测 ```javascript // 典型实现Vuex存储状态) const state = { device: 'desktop', // 或 'mobile' sidebar: { opened: true, // 侧边栏展开状态 withoutAnimation: false } } // 监听窗口变化 window.addEventListener('resize', () => { if (!document.hidden) { const isMobile = /* 判断逻辑 */; store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop') } }) ``` #### 2. 移动端适配逻辑 | 设备类型 | 侧边栏行为 | |-----------|-------------------------------------| | Desktop | 正常展开/折叠,支持动画 | | Mobile | 默认折叠,展开时显示遮罩层 | --- ### 五、关键CSS实现原理 #### 1. 布局方案 ```scss .app-wrapper { @include clearfix; position: relative; height: 100%; width: 100%; &.mobile.openSidebar { position: fixed; // 锁定移动端滚动 } } .drawer-bg { background: #000; opacity: 0.3; width: 100%; top: 0; height: 100%; position: absolute; z-index: 999; } ``` #### 2. 主内容区自适应 ```scss .main-container { min-height: 100%; transition: margin-left .28s; margin-left: $sideBarWidth; // 与侧边栏联动 position: relative; // 侧边栏折叠时 .hideSidebar & { margin-left: 54px; // 折叠后的宽度 } // 移动端适配 .mobile & { margin-left: 0; } } ``` --- ### 六、典型业务逻辑扩展 #### 1. 标签页控制 ```javascript // 是否显示tags-view computed: { needTagsView() { // 可通过路由meta或系统配置控制 return this.$store.state.settings.tagsView } } ``` #### 2. 固定头部优化 ```javascript // 防止内容被fixed-header遮挡 watch: { fixedHeader(val) { if (val) { document.querySelector('.app-main').style.paddingTop = '84px' } else { document.querySelector('.app-main').style.paddingTop = '0' } } } ``` --- ### 七、调试技巧 #### 1. 状态监控 ```javascript // 在Vue组件中添加 computed: { ...mapState({ device: state => state.app.device, sidebar: state => state.app.sidebar }) } ``` #### 2. 布局边界测试 | 测试场景 | 预期效果 | |-----------------------|----------------------------------| | 桌面端窗口>1200px | 侧边栏正常展开/折叠 | | 窗口宽度<768px | 自动切换到移动端模式 | | 点击移动端遮罩层 | 侧边栏自动关闭 | 该布局组件实现管理系统的基础框架结构,建议结合vuex状态树和响应式断点检测进行深度定制。
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值