百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分类 > 正文

在vue中实现tab标签切换页面功能(vue做tab切换的效果)

ztj100 2024-11-13 14:02 30 浏览 0 评论


页面效果

需求分析

在导航条上,默认打开一个页面(列表页面),当点击列表项,打开一个详情页面,需添加到标签导航中,同时显示当前标签的页面内容,继续点击列表项,添加标签到导航中,依次类推,效果如上图。

具体功能:

  • 标签可关闭;
  • 标签可切换,同时内容切换;
  • 标签大于可视范围,需添加到选项卡列表中,可点击切换;
  • 当点击选项卡列表中的标签,需要在可视区中与最后一个标签进行切换;
  • 标签的显示个数自适应;

思考

在一些后台系统中,见过类似的功能,是利用 iframe标签添加链接的方式,实现这个功能。那么在vue中怎么去实现?

首先,这是一个单页面网站应用,使用了vue-router进行前端路由,那么我们可以使用 vue-router 的命名视图,这种方式去代替iframe。

vue-router命名视图官方代码:

<router-view class="view one"></router-view><router-view class="view two" name="a"></router-view><router-view class="view three" name="b"></router-view>
复制代码
const router = new VueRouter({
 routes: [
 {
 path: '/',
 components: {
 default: Foo,
 a: Bar,
 b: Baz
 }
 }
 ]
})
复制代码

项目目录大同小异,就不过多解释了~~

路由文件-命名视图

router代码:

import Vue from "vue";import Router from "vue-router";import store from '../store';Vue.use(Router);// layout页面const Layout = () => import(/* webpackChunkName: "layout" */ "@/views/layout/Layout.vue");// 产品相关页面const Product = () => import(/* webpackChunkName: "product" */ "@/views/product/Index.vue");const ProductDetail = () => import(/* webpackChunkName: "productDetail" */ "@/views/product/Detail.vue");// 新闻相关页面const News = () => import(/* webpackChunkName: "news" */ "@/views/news/Index.vue");const NewsDetail = () => import(/* webpackChunkName: "newsDetail" */ "@/views/news/Detail.vue");const rotuer = new Router({ mode: "history", base: process.env.BASE_URL, routes: [ { path: "/", name: "layout", component: Layout, redirect: "/product", children: [ { path: "/product", name: "product", meta: { title: "产品", tabName: "产品列表" }, components: { default: Product, detail: ProductDetail } }, { path: "/news", name: "news", meta: { title: "新闻", tabName: "新闻列表" }, components: { default: News, detail: NewsDetail } } ] } ]});rotuer.afterEach(to => { // 初始化navTag标签 if (to.meta && to.meta.tabName) { store.dispatch('navTab/addTab', { tabName: to.meta.tabName, initStatus: true }) }});export default rotuer;
复制代码

根据上面的功能分析,路由使用了命名视图的方式,每个模块的列表页面有个name属性,同时components定义了一个默认的页面,和一个详情页面,如果还有其他页面可以继续添加到commontents中,如:

components: {
 default: Product,
 detail: ProductDetail,
 create: ProductCreate
}
复制代码

布局文件

在layou.vue的文件中,对 navTabs(vuex中定义的标签数组) 进行循环生成 <router-view :name=""></router-view> 标签,这个 name 对应的是路由里 components 属性定义的key值,如:"detail"。

layou.vue的部分代码:

<template> <el-row class="container"> <el-col :span="24"> <Header @collapse="collapseHandler" :isCollapse="isCollapse"></Header> </el-col> <el-col :span="24" class="main"> <Menu :isCollapse="isCollapse"></Menu> <el-main class="content-container"> <nav-tab :navTabs="navTabs"></nav-tab> <div class="main-content"> <router-view v-if="!navTabs.length"></router-view> <div class="navTab-content" v-for="item in navTabs" :key="item.tabId" v-show="item.active"> <keep-alive> <router-view :name="item.vName"></router-view> </keep-alive> </div> </div> </el-main> </el-col> </el-row> </template>
复制代码

主要使用<router-view :name="item.vName"></router-view>,显示命名的组件。

使用vuex

我们来看看navTabs是什么?将tab标签数据放在了vuex的state里,可以全局对标签集合继续操作控制,后面会贴出完整代码。

以下是对navTab.js中的部分代码说明:

1、state定义

const tabIdStr = "tab_"; // 标签的id前缀
const state = {
 tabs: [],
 tabMaxNum: 0
};
复制代码

tabIdStr变量是在新增标签的时,防止与其他命名冲突

state对象:

  • tabs属性:保存标签的数据
  • tabMaxNum:最大可显示的标签数量(导航可视区)

2、新增标签

/**
 * 添加标签
 * @param {Any} tabId tab的唯一id标识
 * @param {String} tabName tab的标题
 * @param {String} vName 对应router命名视图的名称
 * @param {Object} pParams 参数传递
 * @param {Boolean} initStatus 初始化状态
 */
 addTab({ commit, state }, { tabId, tabName, vName, pParams, initStatus = false }) {
 // 设置标签id
 let newTabId = tabIdStr + (tabId || 0); // 默认 0
 let opts = {
 tabName: "",
 vName: "default",
 pParams: {},
 active: true,
 ...{
 tabId: newTabId,
 tabName,
 vName,
 pParams
 }
 };
 // 初始化时,重置标签
 if (initStatus) {
 commit("resetTabs");
 }
 // 判断函数
 let hasTabId = item => {
 return item.tabId === newTabId;
 };
 // 判断新增标签是否已存在,如果存在直接激活,否则新增
 if (state.tabs.some(hasTabId)) {
 // 激活标签
 commit("activeTab", newTabId);
 return false;
 }
 // 添加标签
 commit("addTab", opts);
 },
复制代码

这里代码主要注意的是:

// 初始化时,重置标签
if (initStatus) {
 commit("resetTabs");
}
复制代码

例如在产品模块中,切换到新闻模块时,需重置tabs的数据,可以在router的afterEach方法中使用,路由部分代码:

rotuer.afterEach(to => {
 // 初始化navTag标签
 if (to.meta && to.meta.tabName) {
 store.dispatch('navTab/addTab', {
 tabName: to.meta.tabName,
 initStatus: true
 })
 }
});
复制代码

navTab.js完整代码:

const tabIdStr = "tab_"; // 标签的id前缀
const state = {
 tabs: [],
 tabMaxNum: 0
};
const getters = {
 getNavTabs: state => state.tabs
};
const actions = {
 /**
 * 添加标签
 * @param {Any} tabId tab的唯一id标识
 * @param {String} tabName tab的标题
 * @param {String} vName 对应router命名视图的名称
 * @param {Object} pParams 参数传递
 * @param {Boolean} initStatus 初始化状态
 */
 addTab({ commit, state }, { tabId, tabName, vName, pParams, initStatus = false }) {
 // 设置标签id
 let newTabId = tabIdStr + (tabId || 0); // 默认 0
 let opts = {
 tabName: "",
 vName: "default",
 pParams: {},
 active: true,
 ...{
 tabId: newTabId,
 tabName,
 vName,
 pParams
 }
 };
 // 初始化时,重置标签
 if (initStatus) {
 commit("resetTabs");
 }
 // 判断函数
 let hasTabId = item => {
 return item.tabId === newTabId;
 };
 // 判断新增标签是否已存在,如果存在直接激活,否则新增
 if (state.tabs.some(hasTabId)) {
 // 激活标签
 commit("activeTab", newTabId);
 return false;
 }
 // 添加标签
 commit("addTab", opts);
 },
 /**
 * 切换标签
 * @param {String} tabId tab的唯一id标识
 */
 changeTab({ commit }, { tabId }) {
 // 激活标签
 commit('activeTab', tabId);
 },
 /**
 * 更多标签处理
 * @param {Number} index tabs数组下标
 */
 handleMoreTab({ commit }, { index }) {
 commit('handleMoreTab', index);
 },
 /**
 * 删除标签
 * @param {Number} index tabs数组下标
 */
 deleteTab({ commit }, { index }) {
 commit('deleteTab', index);
 },
 /**
 * 删除其他标签
 */
 deleteOtherTab({ commit, state }) {
 // 保存第一个标签
 let firstTab = state.tabs[0];
 // 如果第一个当前标签是第一个,则直接删除全部
 if(firstTab.active) {
 commit('deleteAllTab');
 } else {
 commit('deleteOtherTab');
 }
 },
 /**
 * 删除全部标签
 */
 deleteAllTab({ commit }) {
 commit('deleteAllTab');
 }
};
const mutations = {
 /**
 * 添加标签
 */
 addTab(state, opts) {
 // 隐藏其他标签状态
 state.tabs.forEach(item => {
 item.active = false;
 });
 // 当tabs数量大于或等于标签的最大显示数,新添加的标签放在可显示的最后一位
 if(state.tabs.length >= state.tabMaxNum) {
 state.tabs.splice(state.tabMaxNum - 1, 0, opts);
 } else {
 state.tabs.push(opts);
 }
 },
 /**
 * 激活标签
 */
 activeTab(state, tabId) {
 state.tabs.forEach(item => {
 item.active = false;
 if (item.tabId === tabId) {
 item.active = true;
 }
 });
 },
 /**
 * 更多标签处理
 */
 handleMoreTab(state, index) {
 let tabs = state.tabs;
 let _index = state.tabMaxNum + index;
 // 激活点击标签
 tabs[_index].active = true;
 // 拷贝点击标签
 let copyTab = [tabs[_index]];
 // 删除点击标签
 tabs.splice(_index, 1);
 // 隐藏其他标签
 tabs.forEach(item => {
 item.active = false;
 });
 // 插入到可显示的标签最后一个位置
 tabs.splice([state.tabMaxNum - 1], 0, ...copyTab);
 },
 /**
 * 删除标签
 */
 deleteTab(state, index) {
 let tabs = state.tabs;
 // 判断删除的是当前标签,需激活上一个标签
 if(tabs[index].active && tabs.length > 0) {
 tabs[index -1].active = true;
 }
 tabs.splice(index, 1);
 },
 /**
 * 删除其他标签
 */
 deleteOtherTab(state) {
 // 解构第一个标签,其他标签
 let [firstTab, ...otherTabs] = state.tabs;
 // 获取当前标签
 let curTab = otherTabs.filter(item => item.active);
 state.tabs = [firstTab, ...curTab];
 },
 /**
 * 删除全部标签
 */
 deleteAllTab(state) {
 let tabs = state.tabs;
 // 除了第一个标签其他的都删除
 let firstTab = tabs[0];
 firstTab.active = true;
 state.tabs = [firstTab];
 },
 /**
 * 重置标签
 */
 resetTabs(state) {
 state.tabs = [];
 },
 /**
 * 设置显示标签最大值
 */
 setMaxTabVal(state, val) {
 state.tabMaxNum = parseInt(val);
 },
};
export default {
 namespaced: true,
 state,
 getters,
 actions,
 mutations
};
复制代码

tps:navTab.js实现了标签的新增、点击切换、更多点击切换、删除等功能

NavTab组件

标签的导航组件,对于标签进行操作,使用navTab.js里面的方法,对tabs数据进行增、删、改、查~~

部分代码:

<template> <div class="nav-wrap"> <div class="nav-title"> <strong>{{$route.meta.title}}</strong> </div> <div class="nav-tabs" ref="tabsNav"> <div class="tabs-item" :class="{ 'acitve': item.active }" @click="handleClickTab(item)" v-for="(item, index) in navTabs.slice(0, this.tabMaxNum)" :key="item.tabId"> {{item.tabName}} <i class="el-icon-close icon-close" @click.stop="handleCloseTab(index)" v-if="index"></i> </div> <div class="more"> <div class="more-btn" @click="handleClickMore"> <i class="icon el-icon-arrow-down"></i> </div> <ul class="more-dropdown-menu" v-show="moreStatus"> <li @click.stop="handleClickMoreTab(index)" v-for="(item, index) in navTabs.slice(this.tabMaxNum)" :key="item.tabId"> <span>{{item.tabName}}</span> <i class="el-icon-close icon-close" @click.stop="handleCloseTab(item, index)"></i> </li> <li @click.stop="handleClickDelAll"> <span>关闭全部</span> </li> <li @click.stop="handleClickDelOther"> <span>关闭其他</span> </li> </ul> </div> </div> </div></template><script>import { mapGetters } from 'vuex';export default { data() { return { tabMaxNum: 1, moreStatus: false } }, computed: { ...mapGetters({ navTabs: 'navTab/getNavTabs' }) }, mounted() { // 初始化 this.init(); window.addEventListener('resize', this.init, false); }, deactivated() { window.removeEventListener('resize', this.init, false); }, methods: { /** * 初始化 */ init() { // 计算标签最大显示个数 this.calcTabMaxNum(); }, /** * 计算标签最大显示个数 */ calcTabMaxNum() { if (!this.$refs.tabsNav) { return false; } let tabsNav = this.$refs.tabsNav; let tabsItem = tabsNav.querySelectorAll('.tabs-item'); let moreW = tabsNav.querySelector('.more').getBoundingClientRect().width; let navW = tabsNav.getBoundingClientRect().width - moreW; let itemW = tabsItem[0].getBoundingClientRect().width; // 设置最大值 this.tabMaxNum = Math.floor(navW / itemW); this.$store.commit('navTab/setMaxTabVal', this.tabMaxNum); }, /** * 点击标签 */ handleClickTab(item) { let { tabId, acitve } = item; if(acitve) return; this.hideMore(); this.$store.dispatch('navTab/changeTab', { tabId }); }, /** * 点击更多 */ handleClickMore() { this.moreStatus = !this.moreStatus; }, /** * 更多标签点击 */ handleClickMoreTab(index) { this.hideMore(); this.$store.dispatch('navTab/handleMoreTab', { index }); }, /** * 关闭标签 */ handleCloseTab(index) { this.$store.dispatch('navTab/deleteTab', { index }); }, /** * 关闭全部 */ handleClickDelAll() { if(this.navTabs.length === 1) return; this.hideMore(); this.$store.dispatch('navTab/deleteAllTab'); }, /** * 关闭其他 */ handleClickDelOther() { if(this.navTabs.length === 1) return; this.hideMore(); this.$store.dispatch('navTab/deleteOtherTab'); }, /** * 隐藏更多列表 */ hideMore() { this.moreStatus = false; } }}</script>
复制代码

这里初始化的时候进行了,最大可视标签数量的计算。

calcTabMaxNum方法,根据标签导航的宽度和标签的宽度进行计算,得到的值赋值给 state 里的 tabMaxNum。

创建一个标签页面

handleRowClick(row) {
 let { id, title, intro } = row;
 this.$store.dispatch('navTab/addTab', {
 tabId: 'detail_' + id,
 tabName: title,
 vName: 'detail',
 pParams: {
 title,
 intro
 }
 })
}
复制代码

调用标签的 addTab 方法。

  • tabId: 标签的唯一Id标识
  • tabName: 标签导航的title值
  • vName: 'detail' 就是指向路由里面 components 定义的key值
  • pParams: 需要传递到其他页面的参数

怎么获取到传递过来的参数(pParams值)

在utils/index.js中定义的方法:

/**
 * 获取当前标签的传递参数
 * @param {Array} tabs 标签数据
 */
export const getCurTabParams = (tabs) => {
 if(!tabs || !Array.isArray(tabs)) return {};
 // 查找当前标签
 let curTab = tabs.filter(item => {
 return item.active;
 });
 return curTab.length > 0 ? curTab[0].pParams : {};
}
复制代码

Detail.vue中的部分代码,getCurTabParams方法的使用:

computed: {
 ...mapGetters({
 navTabs: 'navTab/getNavTabs'
 }),
 tabParams() {
 return getCurTabParams(this.navTabs) ? getCurTabParams(this.navTabs) : {};
 }
},
复制代码

基本功能实现完成,到这里就结束了,第一次写分析,思路有点乱,如果有错误的地方欢迎指正~~

附上项目地址:github.com/GuJiBao/vue…

相关推荐

sharding-jdbc实现`分库分表`与`读写分离`

一、前言本文将基于以下环境整合...

三分钟了解mysql中主键、外键、非空、唯一、默认约束是什么

在数据库中,数据表是数据库中最重要、最基本的操作对象,是数据存储的基本单位。数据表被定义为列的集合,数据在表中是按照行和列的格式来存储的。每一行代表一条唯一的记录,每一列代表记录中的一个域。...

MySQL8行级锁_mysql如何加行级锁

MySQL8行级锁版本:8.0.34基本概念...

mysql使用小技巧_mysql使用入门

1、MySQL中有许多很实用的函数,好好利用它们可以省去很多时间:group_concat()将取到的值用逗号连接,可以这么用:selectgroup_concat(distinctid)fr...

MySQL/MariaDB中如何支持全部的Unicode?

永远不要在MySQL中使用utf8,并且始终使用utf8mb4。utf8mb4介绍MySQL/MariaDB中,utf8字符集并不是对Unicode的真正实现,即不是真正的UTF-8编码,因...

聊聊 MySQL Server 可执行注释,你懂了吗?

前言MySQLServer当前支持如下3种注释风格:...

MySQL系列-源码编译安装(v5.7.34)

一、系统环境要求...

MySQL的锁就锁住我啦!与腾讯大佬的技术交谈,是我小看它了

对酒当歌,人生几何!朝朝暮暮,唯有己脱。苦苦寻觅找工作之间,殊不知今日之事乃我心之痛,难道是我不配拥有工作嘛。自面试后他所谓的等待都过去一段时日,可惜在下京东上的小金库都要见低啦。每每想到不由心中一...

MySQL字符问题_mysql中字符串的位置

中文写入乱码问题:我输入的中文编码是urf8的,建的库是urf8的,但是插入mysql总是乱码,一堆"???????????????????????"我用的是ibatis,终于找到原因了,我是这么解决...

深圳尚学堂:mysql基本sql语句大全(三)

数据开发-经典1.按姓氏笔画排序:Select*FromTableNameOrderByCustomerNameCollateChinese_PRC_Stroke_ci_as//从少...

MySQL进行行级锁的?一会next-key锁,一会间隙锁,一会记录锁?

大家好,是不是很多人都对MySQL加行级锁的规则搞的迷迷糊糊,一会是next-key锁,一会是间隙锁,一会又是记录锁。坦白说,确实还挺复杂的,但是好在我找点了点规律,也知道如何如何用命令分析加...

一文讲清怎么利用Python Django实现Excel数据表的导入导出功能

摘要:Python作为一门简单易学且功能强大的编程语言,广受程序员、数据分析师和AI工程师的青睐。本文系统讲解了如何使用Python的Django框架结合openpyxl库实现Excel...

用DataX实现两个MySQL实例间的数据同步

DataXDataX使用Java实现。如果可以实现数据库实例之间准实时的...

MySQL数据库知识_mysql数据库基础知识

MySQL是一种关系型数据库管理系统;那废话不多说,直接上自己以前学习整理文档:查看数据库命令:(1).查看存储过程状态:showprocedurestatus;(2).显示系统变量:show...

如何为MySQL中的JSON字段设置索引

背景MySQL在2015年中发布的5.7.8版本中首次引入了JSON数据类型。自此,它成了一种逃离严格列定义的方式,可以存储各种形状和大小的JSON文档,例如审计日志、配置信息、第三方数据包、用户自定...

取消回复欢迎 发表评论: