系列文档目录
Vue3+Vite+TypeScript安装
Element Plus安装与配置
主页设计与router配置
静态菜单设计
Pinia引入
Header响应式菜单缩展
Mockjs引用与Axios封装
登录设计
登录成功跳转主页
多用户动态加载菜单
Pinia持久化
动态路由 -动态增加路由
动态路由-动态删除路由
路由守卫-无路由跳转404
路由守卫-未登录跳转登录界面
登录退出
Tags-组件构建
Tags-与菜单联动
Pinia持久化优化
按钮权限
客制按钮组件
客制Table组件
客制Form组件
国际化
配置文件
文章目录
目录
系列文档目录
文章目录
前言
Tags设定
菜单单击
演示效果
tab切换
Tag关闭
后续
前言
上一章节已完成了 Tags 组件的初步构建。本章节中,将继续完善 Tags 组件,使其与菜单实现完整联动。
Tags设定
在存储管理中进行了调整,新增了 setTabsData 方法,用于更新 Tabs 数据。
// tabssetTabsData(val: any) {console.log('val',val)if(val.name==='home'){this.currentMenu=null;}else{let index=this.tabs.findIndex((item:any)=>item.index===val.index);console.log(index)index===-1? this.tabs.push(val):"";console.log('tabs:',this.tabs)}},
优化后完整代码:
// src/stores/index.tsimport { defineStore } from 'pinia';
import router from '../router';
import type { Component } from 'vue';
import { get } from 'http';type Modules = Record<string, () => Promise<{ default: Component }>>;// 定义公共 store
export const useAllDataStore = defineStore('useAllData', {// 定义状态state: () => ({isCollapse: false, // 定义初始状态username: '',token_key: '',menuData:[],tabs:[{path:"/home" ,index:"Home",label:"home",icon:"home"}],currentMenu:null,}),// 定义 actionsactions: {// 设置用户名setUsername(username: string) {this.username = username;},// 获取用户名getUsername(): string {return this.username;},// 设置 token_keysetTokenKey(token_key: string) {// sessionStorage.setItem('useAllData-session-store', JSON.stringify({ token_key: token_key, menuData: this.menuData}));this.token_key = token_key;},// 获取 token_keygetTokenKey(): string {/*const sessionData = sessionStorage.getItem('useAllData-session-store');console.log(sessionData)if (sessionData) {const data = JSON.parse(sessionData);this.token_key = data.token_key;this.menuData = data.menuData;}else{this.token_key = ''}*/return this.token_key;},// 设置菜单数据setMenuData(menuData: any){// sessionStorage.setItem('useAllData-session-store', JSON.stringify({ token_key: this.token_key, menuData: menuData}));addRouter(menuData)this.menuData = menuData},// 获取菜单数据getMenuData(): [] {return this.menuData;},// tabssetTabsData(val: any) {console.log('val',val)if(val.name==='home'){this.currentMenu=null;}else{let index=this.tabs.findIndex((item:any)=>item.index===val.index);console.log(index)index===-1? this.tabs.push(val):"";console.log('tabs:',this.tabs)}},getTabsData(): [] {return this.tabs;},// 登出方法logout() {sessionStorage.removeItem('useAllData-store'); // 清除 sessionStorage 中的数据/*this.username = '';this.token_key = '';this.menuData = [];*/router.push({ name: 'login' }); // 重定向到登录页面},},persist: {enabled: true,key: 'useAllData-store',storage: sessionStorage, // // localStorage sessionStoragepaths: ['token_key'], // 指定持久化的字段},/*persist: {enabled: true,strategies: [{key: 'useAllData-store',storage: sessionStorage,// localStorage sessionStoragepaths: ['token_key','menuData'], // 指定需要持久化的字段},],},*/});function addRouter(menuData: any){const routerList=router.getRoutes()const modules: Modules = import.meta.glob('../views/**/*.vue') as Modules;const routerArr=[]menuData.forEach((item:any) => {// console.log(item) if(item.children){item.children.forEach((child:any) => {const componentPath = `../${child.path}.vue`;const module = modules[componentPath];if (module) {/*module().then(({ default: component }) => {child.component = component;});*/child.component = module;routerArr.push(child)}});}else{const componentPath = `../${item.path}.vue`;const module = modules[componentPath];if(module){item.component = module;routerArr.push(item)}}});// 增加删除路由routerList.forEach((item:any) => {if (item.name === 'main' || item.name === 'home' || item.name === '404' || item.name === 'login'|| item.name === 'error'|| item.name === 'undefined'|| item.path === '/'|| item.path === '/main') returnrouter.removeRoute(item.name)});routerArr.forEach((item:any) => {router.addRoute('main',{path: item.index,name: item.label,component: item.component,});})
const routerListLast=router.getRoutes()
console.log(routerListLast)}export function ReloadData() {const store = useAllDataStore();const menuData = store.getMenuData();addRouter(menuData);}
菜单单击
在 components/MainAsideCont.vue 文件中,调整了菜单的单击事件处理逻辑,通过 Pinia 状态管理新增了 Tags 数据 的处理。
const handlemenu = (item: MenuItem) => {router.push(item.index);console.log('item:', item);store.setTabsData(item)};const handlemenuchild = (item: MenuItem, subItem: MenuItem) => {router.push(subItem.index);console.log('subItem:', subItem);store.setTabsData(subItem)
};
调整后完整语句:
<template><el-menu:default-active="activeIndex"class="el-menu-vertical-demo":collapse="isCollapse"><h3 :key="TitleText">{{ TitleText }}</h3><!-- 渲染没有子菜单的项 --><el-menu-itemv-for="item in noChilden":key="item.index":index="item.index"@click="handlemenu(item)"><component v-if="item.icon" class="icon" :is="item.icon.name"></component><span>{{ item.label }}</span></el-menu-item><!-- 渲染有子菜单的项 --><el-sub-menuv-for="item in hasChilden":key="item.index":index="item.index"><template #title><component v-if="item.icon" class="icon" :is="item.icon.name"></component><span>{{ item.label }}</span></template><el-menu-itemv-for="subItem in item.children":key="subItem.index":index="subItem.index"@click="handlemenuchild(item, subItem)"><span>{{ subItem.label }}</span></el-menu-item></el-sub-menu></el-menu>
</template><script lang="ts" setup>
import { ref, computed, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { useAllDataStore } from '@/stores';const store = useAllDataStore();interface MenuItem {index: string;label: string;icon?: { name: string; __name: string };children?: MenuItem[];
}// 确保 menuAPI 是一个数组,并赋值给 menuData
const menuData = ref<MenuItem[]>([]); // 初始化为空数组// 封装数据获取和处理逻辑
const fetchMenuData = () => {try {const result = store.getMenuData(); // 调用 store 获取数据console.log('main menuAPI 返回的数据:', store.getMenuData());console.error('main menuAPI :', result);if (Array.isArray(result)) {menuData.value = result as MenuItem[];} else {console.error('menuAPI 返回的数据不是数组:', result);}} catch (error) {console.error('获取菜单数据失败:', error);}
};onMounted(() => {if (!store.getMenuData().length) {console.warn('菜单数据为空,尝试重新获取');fetchMenuData();} else {console.log('菜单数据已存在,无需重新获取');menuData.value = store.getMenuData() as MenuItem[];console.log('menuData.value:', menuData.value);}
});const hasChilden = computed(() => menuData.value.filter(item => item.children && item.children.length > 0));
const noChilden = computed(() => menuData.value.filter(item => !item.children || item.children.length === 0));const activeIndex = ref('Home');
const router = useRouter();const handlemenu = (item: MenuItem) => {router.push(item.index);console.log('item:', item);store.setTabsData(item)};const handlemenuchild = (item: MenuItem, subItem: MenuItem) => {router.push(subItem.index);console.log('subItem:', subItem);store.setTabsData(subItem)
};const TitleText = computed(() => {return store.isCollapse ? '平台' : '测试平台管理';
});const isCollapse = computed(() => store.isCollapse);</script><style>
.el-menu {height: 100%; /* 设置整个布局的高度为 100%,确保布局占满整个视口 */border-right: none; /* 去掉右边框 */
}
.el-menu-vertical-demo:not(.el-menu--collapse) {width: 180px;min-height: 400px;
}
.el-menu-vertical-demo.el-menu--collapse {width: 60px; /* 收缩时的宽度 */
}.icon {margin-right: 8px; /* 图标与文字之间的间距 */font-size: 18px; /* 图标的大小 */width: 18px;height: 18px;size: 8px;color: #606266; /* 图标的默认颜色 */vertical-align: middle; /* 垂直居中对齐 */
}/* 鼠标悬停时的样式 */
.icon:hover {color: #409eff; /* 鼠标悬停时图标的颜色 */
}
</style>
演示效果
点击菜单时,内容将以 Tabs 的形式在 Main 区域呈现。
tab切换
在 components/MainTagsCont.vue 中增加了 Tabs 的单击事件,目的是在点击 Tabs 时切换对应的内容。
@click="handleTagClick(item)"
const handleTagClick = (item) => {router.push(item.index)store.setTabsData(item);};
修改后完整代码:
<template><div class="tabs"><el-tag v-for="(item,index) in tabs":key="index":label="item.label":name="item.index":icon="item.icon":path="item.path":closable="item.index !== 'Home'":effect="route.name===item.label?'dark':'plain'"@click="handleTagClick(item)"type="primary"round>{{ item.label }}</el-tag></div>
</template>
<script lang="ts" setup>
import{ effect, ref ,computed} from "vue"
import { useRoute ,useRouter } from "vue-router";
import { useAllDataStore } from '@/stores';const store = useAllDataStore();const tabs=computed(() => store.getTabsData()); const route=useRoute()const router=useRouter()
const handleTagClick = (item) => {router.push(item.index)store.setTabsData(item);};</script>
<style scoped>.tabs{margin: 20px 0 0 20px;
}
.el-tag{margin-right: 10px;
}
</style>
测试了 Tab 点击事件,效果如下:
Tag关闭
1.在 stores/index.ts 文件中,为 Pinia 状态管理 新增了 removeTagsData 方法,用于移除指定的 Tags 数据
removeTagsData(val: any){let index=this.tabs.findIndex((item:any)=>item.index===val.index);this.tabs.splice(index,1)},
优化后完整语句:
// src/stores/index.tsimport { defineStore } from 'pinia';
import router from '../router';
import type { Component } from 'vue';
import { get } from 'http';type Modules = Record<string, () => Promise<{ default: Component }>>;// 定义公共 store
export const useAllDataStore = defineStore('useAllData', {// 定义状态state: () => ({isCollapse: false, // 定义初始状态username: '',token_key: '',menuData:[],tabs:[{path:"/home" ,index:"Home",label:"home",icon:"home"}],currentMenu:null,}),// 定义 actionsactions: {// 设置用户名setUsername(username: string) {this.username = username;},// 获取用户名getUsername(): string {return this.username;},// 设置 token_keysetTokenKey(token_key: string) {// sessionStorage.setItem('useAllData-session-store', JSON.stringify({ token_key: token_key, menuData: this.menuData}));this.token_key = token_key;},// 获取 token_keygetTokenKey(): string {/*const sessionData = sessionStorage.getItem('useAllData-session-store');console.log(sessionData)if (sessionData) {const data = JSON.parse(sessionData);this.token_key = data.token_key;this.menuData = data.menuData;}else{this.token_key = ''}*/return this.token_key;},// 设置菜单数据setMenuData(menuData: any){// sessionStorage.setItem('useAllData-session-store', JSON.stringify({ token_key: this.token_key, menuData: menuData}));addRouter(menuData)this.menuData = menuData},// 获取菜单数据getMenuData(): [] {return this.menuData;},// tabssetTabsData(val: any) {console.log('val',val)if(val.name==='home'){this.currentMenu=null;}else{let index=this.tabs.findIndex((item:any)=>item.index===val.index);console.log(index)index===-1? this.tabs.push(val):"";console.log('tabs:',this.tabs)}},getTabsData(): [] {return this.tabs;},removeTagsData(val: any){let index=this.tabs.findIndex((item:any)=>item.index===val.index);this.tabs.splice(index,1)},// 登出方法logout() {sessionStorage.removeItem('useAllData-store'); // 清除 sessionStorage 中的数据/*this.username = '';this.token_key = '';this.menuData = [];*/router.push({ name: 'login' }); // 重定向到登录页面},},persist: {enabled: true,key: 'useAllData-store',storage: sessionStorage, // // localStorage sessionStoragepaths: ['token_key'], // 指定持久化的字段},/*persist: {enabled: true,strategies: [{key: 'useAllData-store',storage: sessionStorage,// localStorage sessionStoragepaths: ['token_key','menuData'], // 指定需要持久化的字段},],},*/});function addRouter(menuData: any){const routerList=router.getRoutes()const modules: Modules = import.meta.glob('../views/**/*.vue') as Modules;const routerArr=[]menuData.forEach((item:any) => {// console.log(item) if(item.children){item.children.forEach((child:any) => {const componentPath = `../${child.path}.vue`;const module = modules[componentPath];if (module) {/*module().then(({ default: component }) => {child.component = component;});*/child.component = module;routerArr.push(child)}});}else{const componentPath = `../${item.path}.vue`;const module = modules[componentPath];if(module){item.component = module;routerArr.push(item)}}});// 增加删除路由routerList.forEach((item:any) => {if (item.name === 'main' || item.name === 'home' || item.name === '404' || item.name === 'login'|| item.name === 'error'|| item.name === 'undefined'|| item.path === '/'|| item.path === '/main') returnrouter.removeRoute(item.name)});routerArr.forEach((item:any) => {router.addRoute('main',{path: item.index,name: item.label,component: item.component,});})
const routerListLast=router.getRoutes()
console.log(routerListLast)}export function ReloadData() {const store = useAllDataStore();const menuData = store.getMenuData();addRouter(menuData);}
2.在 components/MainTagsCont.vue 文件中,新增了 关闭事件 的处理逻辑,用于实现 Tab 的关闭功能。
const handleTagClose = (item,index) => {// pinia 删除数据store.removeTagsData(item);// 如果点击关闭不是当前标签,则不执行路由跳转if(route.name !==item.label){return}if(index===store.getTabsData().length){router.push(store.getTabsData()[index-1].index)}else{router.push(store.getTabsData()[index].index)}}
修改后完整语句:
<template><div class="tabs"><el-tag v-for="(item,index) in tabs":key="index":label="item.label":name="item.index":icon="item.icon":path="item.path":closable="item.index !== 'Home'":effect="route.name===item.label?'dark':'plain'"@click="handleTagClick(item)"@close="handleTagClose(item,index)"round>{{ item.label }}</el-tag></div>
</template>
<script lang="ts" setup>
import{ effect, ref ,computed} from "vue"
import { useRoute ,useRouter } from "vue-router";
import { useAllDataStore } from '@/stores';const store = useAllDataStore();const tabs=computed(() => store.getTabsData()); const route=useRoute()const router=useRouter()
const handleTagClick = (item) => {router.push(item.index)store.setTabsData(item);};
const handleTagClose = (item,index) => {// pinia 删除数据store.removeTagsData(item);// 如果点击关闭不是当前标签,则不执行路由跳转if(route.name !==item.label){return}if(index===store.getTabsData().length){router.push(store.getTabsData()[index-1].index)}else{router.push(store.getTabsData()[index].index)}}</script>
<style scoped>.tabs{margin: 20px 0 0 20px;
}
.el-tag{margin-right: 10px;
}
</style>
3.测试Tabs关闭
运行程序登录后并打开多个菜单项。
测试1-关闭最后一个
测试2-关闭中间一个
后续
本章节完善了 Tags 组件,使其与菜单实现了完整的联动功能。然而,当前的实现仍存在一些与 Pinia 相关的问题。下一章节将重点优化 Pinia 的持久化,以提升数据管理的稳定性和用户体验。