Vue项目实战

news2025/7/16 15:19:49

Vue项目实战

1、项目介绍

1.1、对象

有Vue2、Vue3组合api基础知识,TypeScript基础知识

1.2、涉及技术

CSS3
TypeScript
Vue3.2
Vuex4.x
Vue Router4.x
Vite2.x
Element-Plus

1.3、技能

  1. 掌握Vue3.2语法糖的使用
  2. 掌握Vue3中组合api的使用
  3. 掌握组件中业务逻辑抽离的方法
  4. 掌握TypeScript在Vue3中的使用
  5. 掌握动态菜单、动态路由、按钮权限的实现方式
  6. Vue3中全局挂载使用方式
  7. Vue3父子组件的使用
  8. Vue3中Echarts的使用
  9. token、权限验证
  10. Vuex4.x +Ts在commit、getter、dispatch中的代码提示
  11. Icons图标动态生成

2、项目创建

在这里插入图片描述

2.1、init

新建vue存放目录,cmd进入存放目录,执行命令创建项目

npm init vite@latest
or
yarn crerate vite

2.2、启动项目

√ Project name: ... vue3-ts
√ Select a framework: » vue
√ Select a variant: » vue-ts

Scaffolding project in D:\bigdata-ws\vue\vue3-ts...

Done. Now run:

  cd vue3-ts
  npm install
  npm run dev

2.3、访问项目

http://localhost:3000/

2.4、解决Network

解决:Network: use --host to expose

vite.config.ts配置文件,添加如下配置:

export default defineConfig({
  plugins: [vue()],
  server: {
    host: '0.0.0.0', //Network: use `--host` to expose
    port: 8080,
    open: true
  }
})

2.5、vite配置别名

参考官网:https://vitejs.cn/config/#resolve-alias

types/node

npm install @types/node --save-dev
npm run build

vite.config.ts配置文件

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  server: {
    host: '0.0.0.0', //Network: use `--host` to expose
    port: 8080,
    open: true
  },
  resolve:{
    alias: [
      { 
        find: '@', 
        replacement:resolve(__dirname,'src') }
    ]
  }
})

2.6、安装插件

  • 安装Vue Language Features(Volar);禁用Vuter插件,否则会冲突
  • 安装Element UI Snippets
  • 安装open in browser

2.7、安装路由(vue-router)

npm install vue-router@4
npm run build

2.7.1、src下创建router目录,然后创建index.ts文件

//vue2-router
const router = new VueRouter({
	mode: history,
	...
})

//vue-router
import { createRouter,createWebHistory} from 'vue-router'

const router = createRouter({
	history: createWebHistory(),
	...
})

2.7.2、index.tx

import { createRouter,createWebHistory,RouteRecordRaw } from "vue-router";
import Layout from '@/components/HelloWorld.vue'

const routes:Array<RouteRecordRaw> = [
	{
		path:'/',
		name:'home',
		component:Layout
	}
]

//创建
const router = createRouter({
	history:createWebHistory(),
	routes
})

//暴露 router
export default router

2.7.3、main.ts

import { createApp } from 'vue'
import App from './App.vue'
import router from './router/index'

createApp(App)
.use(router)
.mount('#app')

2.7.3、修改App.vue

<script setup lang="ts">
</script>

<template>
  <router-view />
</template>

<style lang="scss">
</style>

2.8、安装Vuex

2.8.1、官网

https://vuex.vuejs.org/zh/

2.8.2、安装vuex

npm install vuex@next --save
or
yarn add vuex@next --save

2.8.3、src下创建store目录,然后创建index.ts文件

// store.ts
import { InjectionKey } from 'vue'
import { createStore, Store } from 'vuex'

// 为 store state 声明类型
export interface State {
  count: number
}

// 定义 injection key
export const key: InjectionKey<Store<State>> = Symbol()

export const store = createStore<State>({
  state: {
    count: 0
  }
})

2.8.4、main.ts

import { createApp } from 'vue'
import App from './App.vue'
import router from './router/index'
import { store,key } from './store/index'

createApp(App)
.use(router)
.use(store,key)
.mount('#app')

2.8.5、index.tx

// store.ts
import { InjectionKey } from 'vue'
import { createStore, useStore as baseUseStore, Store } from 'vuex'

export interface State {
  count: number
}

export const key: InjectionKey<Store<State>> = Symbol()

export const store = createStore<State>({
  state: {
    count: 0
  },
	mutations: {
		setCount(state:State,count:number){
			state.count = count;
		}
	},
	getters: {
		getCount(state:State){
			return state.count;
		}
	}
})

// 定义自己的 `useStore` 组合式函数
export function useStore () {
  return baseUseStore(key)
}

2.8.6、HelloWorld.vue

<script setup lang="ts">
import { ref,computed } from 'vue'
import { storeKey } from 'vuex';
import { useStore } from '../store';

const store = useStore();
//定义响应式变量
const count = ref(0)

const showcount = computed(()=>{
  return store.getters['getCount']
})

const addBtn = ()=>{
  store.commit('setCount',++count.value);
}

</script>

<template>
  <h1>{{ showcount }}</h1>

  <button type="button" @click="addBtn">增加</button>

</template>

<style scoped>
</style>

2.9、eslint、css预处理器sass

2.9.1、ts使用@符号引入

tsconfig.json
{
  "compilerOptions": {
    "target": "esnext",
    "useDefineForClassFields": true,
    "module": "esnext",
    "moduleResolution": "node",
    "strict": true,
    "jsx": "preserve",
    "sourceMap": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "esModuleInterop": true,
    "lib": ["esnext", "dom"],
    //解决打包报`vue-tsc --noEmit && vite build`的错误,忽略所有的声明文件(*.d.ts)的类型检查
    "skipLibCheck": true,
    "baseUrl": ".",
    "paths": {
      "@/*": [
        "src/*"
      ]
    }
  },
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
  //ts排除的文件
  "exclude": ["node_modules"],
  "references": [{ "path": "./tsconfig.node.json" }]
}
修改HelloWorld.vue
import { useStore } from '../store';
修改为
import { useStore } from '@/store';
验证
npm run build
npm run dev

2.9.2、Eslint

npm install --save-dev eslint-plugin-vue

2.9.3、新建.eslintrc.js文件

注:src目录平级位置

module.exports = {
	root: true,
	parserOptions: {
		sourceType: 'module'
	},
	parser: 'vue-eslint-parser',
	extends: ['plugin:vue/vue3-essential','plugin:vue/vue3-strongly-recomended','plugin:vue/vue3-recomended'],
	env: {
		browser: true,
		node: true,
		es6: true
	},
	rules: {
		'no-console': 'off',
		//禁止使用拖尾逗号
		'comma-dangle': [2,'never']
	}
}

2.9.4、添加css预处理器sass

npm install -D sass sass-loader

2.10、element-plus

2.10.1、官网

https://element-plus.gitee.io/zh-CN/guide/installation.html

2.10.2、安装element-plus

npm install element-plus --save

2.10.3、main.ts中引入

https://element-plus.gitee.io/zh-CN/guide/quickstart.html

import { createApp } from 'vue'
import App from './App.vue'
import router from './router/index'
import { store,key } from '@/store/index'
//引入element-plus
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'

createApp(App)
.use(router)
.use(store,key)
.use(ElementPlus)
.mount('#app')

2.10.4、测试

在HelloWorld.vue页面加入按钮

<template>
  <h1>{{ showcount }}</h1>

  <button type="button" @click="addBtn">增加</button>
  <el-button type="primary">Primary</el-button>
  <el-button type="success">Success</el-button>
  <el-button type="info">Info</el-button>
  <el-button type="warning">Warning</el-button>
  <el-button type="danger">Danger</el-button>
</template>

3、主界面布局

3.1、插件安装

禁用Vetur
安装Vue Language Features(Volar)
安装Element UI Snippets

3.2、前置知识

1、setup语法糖中,组件的使用方式
	setup语法糖中,引入的组件开源直接使用,无需再通过components进行注册,并且无法制定当前组件的名字,它会自动以文件名为主,不用再写name属性了。
	setup语法糖中,定义的数据和方法,直接可以在模板中使用,无需return。

2、ref使用
	定义:const xxx = ref(sss)
	作用:定义一个响应式的数据
	js中操作,需要使用xxx.value
	模板中使用不需要 .value

3.3、index.html添加样式,设置高度

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>
<style>
  html,body,#app{
    padding: 0px;
    margin: 0px;
    height: 100%;
    box-sizing: border-box;
  }
</style>

3.4、新建layout目录

3.4.1、Index.vue

https://element-plus.gitee.io/zh-CN/component/container.html

在src下创建layout目录,再创建Index.vue页面

<template>
  <div class="common-layout">
    <el-container>
      <el-aside width="200px">Aside</el-aside>
      <el-container>
        <el-header>Header</el-header>
        <el-main>Main</el-main>
      </el-container>
    </el-container>
  </div>
</template>

<script setup lang="ts">
</script>

3.4.2、修改路由

在src下router目录,修改index.ts中的路由

import { createRouter,createWebHistory,RouteRecordRaw } from "vue-router";
// import Layout from '@/components/HelloWorld.vue'
import Layout from '@/layout/index.vue'

const routes:Array<RouteRecordRaw> = [
	{
		path:'/',
		name:'home',
		component:Layout
	}
]

//创建
const router = createRouter({
	history:createWebHistory(),
	routes
})

//暴露 router
export default router

3.4.3、Index.vue样式

<template>
  <div class="common-layout">
    <el-container class="layout">
      <el-aside width="200px" class="asside">Aside</el-aside>
      <el-container>
        <el-header class="header">Header</el-header>
        <el-main class="main">Main</el-main>
      </el-container>
    </el-container>
  </div>
</template>

<script setup lang="ts">
</script>
<style lang="scss" scoped>

	.layout{
		height: 100%;
		.asside{
			background-color: aquamarine;``
		}
		.header{
			background-color: blueviolet;
		}
		.main{
			background-color: darkgray;
		}
	}
</style>

4、左侧导航菜单

4.1、前置知识

https://github.com/vuejs/rfcs/tree/master/active-rfcs
https://github.com/vuejs/rfcs/blob/master/active-rfcs/0040-script-setup.md

4.1.1、抽离头部组件

在layout目录下新建header目录,然后新建Header.vue组件

<template>
	<div>头部</div>
</template>
<script setup lang="ts">
</script>

4.1.2、引入头部

在layout目录下的Index.vue中引入Header.vue组件

<template>
  <div class="common-layout">
    <el-container class="layout">
      <el-aside width="200px" class="asside">Aside</el-aside>
      <el-container>
        <el-header class="header">
					<Header></Header>
				</el-header>
        <el-main class="main">Main</el-main>
      </el-container>
    </el-container>
  </div>
</template>

<script setup lang="ts">
//引入头部
import Header from './header/Header.vue';


</script>
<style lang="scss" scoped>

	.layout{
		height: 100%;
		.asside{
			background-color: aquamarine;
		}
		.header{
			background-color: blueviolet;
		}
		.main{
			background-color: darkgray;
		}
	}
</style>

4.2、Menu菜单

https://element-plus.gitee.io/zh-CN/component/menu.html#可折叠的菜单

4.2.1、MenuBar.vue

在layout目录先创建menu目录,新建MenuBar.vue

<template>
  <el-menu
    default-active="2"
    class="el-menu-vertical-demo"
    :collapse="isCollapse"
    @open="handleOpen"
    @close="handleClose"
  >
    <el-sub-menu index="1">
      <template #title>
        <el-icon><location /></el-icon>
        <span>Navigator One</span>
      </template>
      <el-sub-menu index="1-4">
        <template #title><span>item four</span></template>
        <el-menu-item index="1-4-1">item one</el-menu-item>
      </el-sub-menu>
    </el-sub-menu>
    <el-menu-item index="2">
      <el-icon><icon-menu /></el-icon>
      <template #title>Navigator Two</template>
    </el-menu-item>

  </el-menu>
</template>

<script setup lang="ts">
import { ref } from 'vue';

const isCollapse = ref(false)

const handleOpen = (key:string|number,keyPath:string)=>{
	console.log(key,keyPath)
}

const handleClose = (key:string|number,keyPath:string)=>{
	console.log(key,keyPath)
}

</script>

4.2.2、引入左侧导航栏

在layout目录下的Index.vue页面引入左侧导航栏

<template>
  <div class="common-layout">
    <el-container class="layout">
      <el-aside width="200px" class="asside">
				<MenuBar></MenuBar>
			</el-aside>
      <el-container>
        <el-header class="header">
					<Header></Header>
				</el-header>
        <el-main class="main">Main</el-main>
      </el-container>
    </el-container>
  </div>
</template>

<script setup lang="ts">
//引入头部
import Header from '@/layout/header/Header.vue';
//引入左侧导航栏
import MenuBar from '@/layout/menu/MenuBar.vue'


</script>
<style lang="scss" scoped>

	.layout{
		height: 100%;
		.asside{
			background-color: aquamarine;
		}
		.header{
			background-color: blueviolet;
		}
		.main{
			background-color: darkgray;
		}
	}
</style>

4.2.3、抽离MenuItem.vue组件

<template>
    <el-sub-menu index="1">
      <template #title>
        <i class="el-icon-location"></i>
        <span>Navigator One</span>
      </template>
      <el-sub-menu index="1-4">
        <template #title><span>item four</span></template>
        <el-menu-item index="1-4-1">item one</el-menu-item>
      </el-sub-menu>
    </el-sub-menu>
    <el-menu-item index="2">
      <i class="el-icon-menu"></i>
      <template #title>Navigator Two</template>
    </el-menu-item>
</template>

<script setup lang="ts">
</script>

setup语法糖父子组件传值的方法
	父组件传值给子组件,通过属性绑定方式
	子组件通过defineProps接收,无需显示的引入
	插槽的使用
reactive:响应式数据定义,适用于对象类型

4.3、导航菜单logo

4.3.1、解决::v-deep警告

[@vue/compiler-sfc] ::v-deep usage as a combinator has been deprecated. Use :deep(<inner-selector>) instead.
MenuBar.vue
<style scoped>
.el-menu-vertical-demo:not(.el-menu--collapse) {
  width: 230px;
  min-height: 400px;
}

.el-menu {
	border-right: none;
}

:deep(.el-sub-menu .el-sub-menu__title) {
	color: #f4f4f5 !important;
}

:deep(.el-menu .el-menu-item) {
	color: #bfcbd9;
}

/* 菜单点中文字的颜色 */
:deep(el-menu-item.is-active) {
	color: #409eff !important;
}

/* 当前打开菜单的所有子菜单颜色 */
:deep(.is-opened .el-menu-item) {
	background-color: #1f2d3d !important;
}

/* 鼠标移动菜单的颜色 */
:deep(.el-menu-item:hover) {
	background-color: #001528 !important;
}
</style>

4.3.2、MenuLogo.vue

新建src/layout/menu/MenuLogo.vue

<template>
	<div class="logo">
		<img src="https://wpimg.wallstcn.com/69a1c46c-eb1c-4b46-8bd4-e9e686ef5251.png" alt="logo">
		<span class="title">Vue实战</span>
	</div>
</template>
<script setup lang="ts">

</script>
<style lang="scss" scoped>
.logo {
	background-color: #2b2f3a;
	height: 50px;
	border: none;
	line-height: 50px;
	display: flex;
	align-items: center;
	padding-left: 15px;
	color: #fff;
	img {
		width: 32px;
		height: 32px;
		margin-right: 12px;
	}
	span {
		font-weight: 600;
		line-height: 50px;
		font-size: 16px;
		font-family: Arial, Helvetica Neue, Arial, Helvetica, sans-serif;
		vertical-align: middle;
	}
}
</style>

4.3.3、MenuBar.vue

在src/layout/menu/MenuBar.vue中引入MenuLogo.vue

<template>
	<MenuLogo></MenuLogo>
  <el-menu
    default-active="2"
    class="el-menu-vertical-demo"
    :collapse="isCollapse"
		background-color="#304156"
		unique-opened

  >
	<!-- 引入导航栏Item -->
	<MenuItem :menuList='menuList'></MenuItem>
  </el-menu>
</template>

<script setup lang="ts">
// setup语法糖父子组件传值的方法
// 	父组件传值给子组件,通过属性绑定方式
// 	子组件通过defineProps接收,无需显示的引入
// 	插槽的使用
// reactive:响应式数据定义,适用于对象类型

import { reactive, ref } from 'vue';
import MenuItem from './MenuItem.vue'
import MenuLogo from '@/layout/menu/MenuLogo.vue'

//setup 语法糖中,定义的数据和方法,直接可以在模板中使用,无需return
//菜单
let menuList = reactive([
	{
		path: '/dashboard',
		component: "Layout",
		meta: {
			title: "首页",
			icon: "el-icon-s-home",
			roles: ["sys:manage"]
		},
		children: []
	},
	{
		path: "/system",
		component: "Layout",
		alwaysShow: true,
		name: "system",
		meta: {
			title: "系统管理",
			icon: "el-icon-menu",
			roles: ["sys:manage"],
			parentId: 0
		},
		children: [
			{
				path: "/department",
				component: "/system/department/department",
				alwaysShow: false,
				name: "department",
				meta: {
					title: "机构管理",
					icon: "el-icon-document",
					roles: ["sys:dept"],
					parentId: 17,
				},
			},
			{
				path: "userList",
				coomponent: "/system/User/UserList",
				alwaysShow: false,
				name: "userList",
				meta: {
					title: "用户管理",
					icon: "el-icon-s-custom",
					roles: ["sys:user"],
					parentId: 17,
				},
			},
			{
				path: "roleList",
				component: "/system/Role/RoelList",
				alwaysShow: false,
				name: "roleList",
				meta: {
					title: "角色管理",
					icon: "el-icon-s-tools",
					roles: ["sys:role"],
					parentId: 17,
				},
			},
			{
				path: "/menuList",
				component: "/system/Menu/MenuList",
				alwaysShow: false,
				name: "menuList",
				meta: {
					title: "权限管理",
					icon: "el-icon-document",
					roles: ["sys:menu"],
					parentId: 17,
				},
			},
		],	
	},
	{
		path: "/goods",
		component: "Layout",
		alwaysShow: true,
		name: "goods",
		meta: {
			title: "商品管理",
			icon: "el-icon-document",
			roles: ["sys:goods"],
			parentId: 0,
		},
		children: [
			{
				path: "/goodsCategory",
				component: "/goods/goodsCategory/goodsCategoryList",
				alwaysShow: false,
				name: "goodCategory",
				meta: {
					title: "商品分类",
					icon: "el-icon-document",
					roles: ["sys:goodsCategory"],
					parentId: 34,
				},
			},
		],
	},
	{
		path: "systemConfig",
		component: "Layout",
		alwaysShow: true,
		name: "systemConfig",
		meta: {
			title: "系统工具",
			icon: "el-icon-document",
			roles: ["sys:systemConfig"],
			parentId: 0,
		},
		children: [
			{
				path: "/document",
				component: "/system/config/systemDocument",
				alwaysShow: false,
				name: "http://42.193.158.170:8089/swagger-ui/index.html",
				meta: {
					title: "接口文档",
					icon: "el-icon-document",
					roles: ["sys:document"],
					parentId: 42,
				},
			},
		],
	},

]);

//控制菜单展开和关闭
const isCollapse = ref(false)

</script>

<style scoped>
.el-menu-vertical-demo:not(.el-menu--collapse) {
  width: 230px;
  min-height: 400px;
}

.el-menu {
	border-right: none;
}

:deep(.el-sub-menu .el-sub-menu__title) {
	color: #f4f4f5 !important;
}

:deep(.el-menu .el-menu-item) {
	color: #bfcbd9;
}

/* 菜单点中文字的颜色 */
:deep(el-menu-item.is-active) {
	color: #409eff !important;
}

/* 当前打开菜单的所有子菜单颜色 */
:deep(.is-opened .el-menu-item) {
	background-color: #1f2d3d !important;
}

/* 鼠标移动菜单的颜色 */
:deep(.el-menu-item:hover) {
	background-color: #001528 !important;
}
</style>

4.4、Element Plus图标

4.4.1、vue3 setup语法糖

https://v3.cn.vuejs.org/api/sfc-script-setup.html
https://github.com/vuejs/rfcs/tree/master/active-rfcs

4.4.2、前置知识

  • element plus图标使用

    https://element-plus.gitee.io/zh-CN/component/icon.html

4.4.3、Element Plus图标基本使用

安装
npm install @element-plus/icons
引入图标
import { Fold } from '@element-plus/icons'
使用方式
<el-icon><Fold /></el-icon>
Header.vue
//局部引用图标
<template>
  <div>
    <el-icon>
      <Edit />
    </el-icon>
  </div>
</template>

<script setup lang="ts">
import {Edit} from '@element-plus/icons'
</script>

4.4.4、Element Plus动态生成菜单图标

图标注册为全局组件

https://v3.cn.vuejs.org/api/sfc-script-setup.html#使用组件

在main.ts把图标注册为全局组件

方式一
main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router/index'
import { store,key } from '@/store/index'
//引入element-plus
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
//统一导入element-icon图标
import * as  Icons from '@element-plus/icons'

const app = createApp(App);

app.use(router)
.use(store,key)
.use(ElementPlus)
.mount('#app')

//全局注册组件
//方式一
//typeof获取一个对象的类型
//keyof获取某种类型的所有键
Object.keys(Icons).forEach(
	(key)=>{
		console.log(key)
		// app.component(key,Icons[key])
		app.component(key,Icons[key as keyof typeof Icons])
	}
);
MenuBar.vue
	{
		path: '/dashboard',
		component: "Layout",
		meta: {
			title: "首页",
			icon: "HomeFilled",
			roles: ["sys:manage"]
		},
		children: []
	},
MenuItem.vue
<template>
	<template v-for="menu in menuList" :key='menu.path'>
		  <el-sub-menu v-if="menu.children && menu.children.length >0" :index="menu.path">
      <template #title>
        <i v-if="menu.meta.icon && menu.meta.icon.includes('el-icon')" :class="menu.meta.icon"></i>
        <!-- 动态组件的使用方式 -->
        <component class="icons" v-else :is="menu.meta.icon" />
        <span>{{menu.meta.title}}</span>
      </template>
			<menu-item :menuList='menu.children'></menu-item>
    </el-sub-menu>

    <el-menu-item 
		style="color:#f4f4f5"
		v-else :index="menu.path">
        <i v-if="menu.meta.icon && menu.meta.icon.includes('el-icon')" :class="menu.meta.icon"></i>
        <!-- 动态组件的使用方式 -->
        <component class="icons" v-else :is="menu.meta.icon" />
      <template #title>{{menu.meta.title}}</template>
    </el-menu-item>
	</template>
</template>

<script setup lang="ts">

defineProps(['menuList'])
</script>
<style>
  .icons {
    width: 24px;
    height: 18px;
    margin-right: 5px;
  }
</style>
方式二
main.ts
import { createApp,createVNode } from 'vue'
import App from './App.vue'
import router from './router/index'
import { store,key } from '@/store/index'
//引入element-plus
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
//统一导入element-icon图标
import * as  Icons from '@element-plus/icons'

const app = createApp(App);

app.use(router)
.use(store,key)
.use(ElementPlus)
.mount('#app')

//全局注册组件
//方式二
const Icon = (props: {icon: string})=>{
	const { icon } = props
	return createVNode(Icons[icon as keyof typeof Icons]);
};
app.component('Icon',Icon);
MenuItem.vue
<template>
	<template v-for="menu in menuList" :key='menu.path'>
		  <el-sub-menu v-if="menu.children && menu.children.length >0" :index="menu.path">
      <template #title>
        <i v-if="menu.meta.icon && menu.meta.icon.includes('el-icon')" :class="menu.meta.icon"></i>
        <!-- 动态组件的使用方式 -->
        <!-- <component class="icons" v-else :is="menu.meta.icon" /> -->
        <Icon class="icons" v-else :icon="menu.meta.icon"></Icon>
        <span>{{menu.meta.title}}</span>
      </template>
			<menu-item :menuList='menu.children'></menu-item>
    </el-sub-menu>

    <el-menu-item 
		style="color:#f4f4f5"
		v-else :index="menu.path">
        <i v-if="menu.meta.icon && menu.meta.icon.includes('el-icon')" :class="menu.meta.icon"></i>
        <!-- 动态组件的使用方式 -->
        <!-- <component class="icons" v-else :is="menu.meta.icon" /> -->
         <Icon class="icons" v-else :icon="menu.meta.icon"></Icon>
      <template #title>{{menu.meta.title}}</template>
    </el-menu-item>
	</template>
</template>

<script setup lang="ts">

defineProps(['menuList'])
</script>
<style>
  .icons {
    width: 24px;
    height: 18px;
    margin-right: 5px;
  }
</style>

解决type ‘string’ can’t be used to index type ‘typeof’ 字符串不能做下标的错,在tsconfig.json的compilerOptions中添加如下配置

方式一

"suppressExcessPropertyErrors": true,	//解决用字符串做下标报错

方式二

key as keyof typeof Icons

5、路由配置与页面创建

5.1、前置

5.1.1、vue3 setup语法糖文档

https://v3.cn.vuejs.org/api/sfc-script-setup.html
https://github.com/vuejs/rfcs/tree/master/active-rfcs
https://router.vuejs.org/zh/installation.html

5.1.2、代码模板配置

  • 首先在vscode编辑器中打开,[文件]->[首选项]->[用户片段]->[新代码片段]->取名vue.json->回车

  • 把下面代码粘进去,其中prefix里面的内容就是快捷键

    {
      "Print to console": {
        "prefix": "vue",
        "body": [
          "<template>",
          "  <div></div>",
          "</template>",
          "",
          "<script setup lang='ts'>",
          "import { ref, reactive } from 'vue';",
          "</script>",
          "<style scoped lang='scss'>",
          "</style>"
        ],
        "description": "Log output to console"
      }
    }
    
  • 新建.vue结尾文件,代码区域输入 vue 回车,即可生成定义的模板代码

5.1.3、功能分析

点击左侧菜单,能够在内容展示区展示对应页面

5.1.4、前置知识

在setup里面没有访问this,所以不能再直接访问this.$router或this.$route;使用useRouter和useRoute替代;
const router = useRouter() ---> this.$router
const route = useRoute() ---> this.$route

5.2、添加路由

5.2.1、index.ts

在router/index.ts添加路由

import { createRouter,createWebHistory,RouteRecordRaw } from "vue-router";
// import Layout from '@/components/HelloWorld.vue'
import Layout from '@/layout/index.vue'

const routes:Array<RouteRecordRaw> = [
	{
		path:'/',
		name:'home',
		component:Layout,
		redirect: '/dashboard',
		children: [
			{
				path: '/dashboard',
				component: ()=>import('@/layout/dashboard/Index.vue'),
				name: 'dashboard',
				meta: {
					title: '首页',
					icon: 'HomeFilled'
				},
			},
		],
	},
	{
		path:'/system',
		name:'system',
		component:Layout,
		meta: {
			title: "系统管理",
			icon: "Menu",
			roles: ["sys:manage"],
			parentId: 0,
		},
		children: [
			{
				path: "/department",
				component: ()=>import('@/views/system/department/department.vue'),
				name: 'department',
				meta: {
					title: "机构管理",
					icon: "Document",
					roles: ["sys:dept"]
				}
			},
			{
				path: "/userList",
				component: ()=>import('@/views/system/user/UserList.vue'),
				name: "userList",
				meta: {
					title: "用户管理",
					icon: "Avatar",
					roles: ["sys:user"]
				},
			},
			{
				path: "/roleList",
				component:()=>import('@/views/system/role/RoleList.vue'),
				name: "roleList",
				meta: {
					title: "角色管理",
					icon: "Tools",
					roles: ["sys:role"]
				},
			},
			{
				path: "/menuList",
				component: ()=>import('@/views/system/menu/MenuList.vue'),
				name: "menuList",
				meta: {
					title: "权限管理",
					icon: "Document",
					roles: ["sys:menu"]
				},
			},
		]
	},
	{
		path: "/goods",
		component: Layout,
		name: "goods",
		meta: {
			title: "商品管理",
			icon: "Shop",
			roles: ["sys:goods"]
		},
		children: [
			{
				path: "/goodsCategory",
				component: ()=>import('@/views/goods/goodscategory/goodsCategoryList.vue'),
				name: "goodsCategory",
				meta: {
					title: "商品分类",
					icon: "Sell",
					roles: ["sys:goodsCategory"]
				}
			}
		]
	},
	{
		path: "/systemConfig",
		component: Layout,
		name: "systemConfig",
		meta: {
			title: "系统工具",
			icon: "Setting",
			roles: ["sys:systemConfig"]
		},
		children: [
			{
				path: "/document",
				component: ()=>import('@/views/system/config/systemDocument.vue'),
				name: "https://42.193.158.170:8089/swagger-ui/index.html",
				meta: {
					title: "接口文档",
					icon: "Document",
					roles: ["sys:document"]
				},
			},
		],
	},

]


//创建
const router = createRouter({
	history:createWebHistory(),
	routes
})

//暴露 router
export default router

5.2.2、新建相关模块

dashboard
Index.vue

src/layout/dashboard/Index.vue

<template>
	<div>首页</div>
</template>

<script setup lang='ts'>
import { ref, reactive } from 'vue';
</script>
<style scoped lang='scss'>
</style>
views
goods

src/views/goods/goodscategory/goodsCategoryList.vue

<template>
	<div>商品分类</div>
</template>

<script setup lang='ts'>
import { ref, reactive } from 'vue';
</script>
<style scoped lang='scss'>
</style>
system

src/views/system/config/systemDoument.vue

<template>
	<div>接口文档</div>
</template>

<script setup lang='ts'>
import { ref, reactive } from 'vue';
</script>
<style scoped lang='scss'>
</style>

src/views/system/department/department.vue

<template>
	<div>机构管理</div>
</template>

<script setup lang='ts'>
import { ref, reactive } from 'vue';
</script>
<style scoped lang='scss'>
</style>

src/views/system/menu/MenuList.vue

<template>
	<div>权限管理</div>
</template>

<script setup lang='ts'>
import { ref, reactive } from 'vue';
</script>
<style scoped lang='scss'>
</style>

src/views/system/role/RoleList.vue

<template>
	<div>角色管理</div>
</template>

<script setup lang='ts'>
import { ref, reactive } from 'vue';
</script>
<style scoped lang='scss'>
</style>

src/views/system/user/UserList.vue

<template>
	<div>用户管理</div>
</template>

<script setup lang='ts'>
import { ref, reactive } from 'vue';
</script>
<style scoped lang='scss'>
</style>
router-view

src/layout/Index.vue

<template>
  <div class="common-layout">
    <el-container class="layout">
      <el-aside width="auto" class="asside">
				<MenuBar></MenuBar>
			</el-aside>
      <el-container>
        <el-header class="header">
					<Header></Header>
				</el-header>
        <el-main class="main">
					<!-- 添加路由 -->
					<router-view></router-view>
				</el-main>
      </el-container>
    </el-container>
  </div>
</template>

<script setup lang="ts">
//引入头部
import Header from '@/layout/header/Header.vue';
//引入左侧导航栏
import MenuBar from '@/layout/menu/MenuBar.vue'


</script>
<style lang="scss" scoped>

	.layout{
		height: 100%;
		.asside{
			background-color: rgb(48, 65, 86);
		}
		.header{
			height: 50px;
			background-color: 1px solid #e5e5e5;
		}
		.main{
			background-color: darkgray;
		}
	}
</style>
Menu属性

https://element-plus.gitee.io/zh-CN/component/menu.html#menu-属性
src/layout/menu/MenBar.vue
router 是否启用 vue-router 模式。 启用该模式会在激活导航时以 index 作为 path 进行路由跳转

<template>
	<MenuLogo></MenuLogo>
  <el-menu
    default-active="2"
    class="el-menu-vertical-demo"
    :collapse="isCollapse"
		background-color="#304156"
		unique-opened
		router

  >
	<!-- 引入导航栏Item -->
	<MenuItem :menuList='menuList'></MenuItem>
  </el-menu>
</template>
......

src/layout/menu/MenBar.vue
default-active 默认激活菜单的 index

<template>
	<MenuLogo></MenuLogo>
	<!-- 使用default-active -->
  <el-menu
    :default-active="activeIdx"
    class="el-menu-vertical-demo"
    :collapse="isCollapse"
		background-color="#304156"
		unique-opened
		router

  >
	<!-- 引入导航栏Item -->
	<MenuItem :menuList='menuList'></MenuItem>
  </el-menu>
</template>

<script setup lang="ts">
// setup语法糖父子组件传值的方法
// 	父组件传值给子组件,通过属性绑定方式
// 	子组件通过defineProps接收,无需显示的引入
// 	插槽的使用
// reactive:响应式数据定义,适用于对象类型

import { reactive, ref } from 'vue';
//引入路由
import { useRoute } from 'vue-router'
import MenuItem from './MenuItem.vue'
import MenuLogo from '@/layout/menu/MenuLogo.vue'
import { computed } from '@vue/reactivity';

//setup 语法糖中,定义的数据和方法,直接可以在模板中使用,无需return

//获取当前路由
const route = useRoute();
const activeIdx = computed(()=>{
	const {path} = route;
	return path;
})

......

6、菜单收缩与展开

6.1、前置知识

  1. element plus图标使用

    https://element-plus.gitee.io/zh-CN/component/icon.html

6.2、切换按钮的实现

6.2.1、安装element plus图标

安装
npm install @element-plus/icons
引入图标
import { Fold }from '@element-plus/icons'
使用方式
import { Fold }from '@element-plus/icons'

6.2.2、Collapse.vue组件

在src/layout/header下新建Collapse.vue组件

<template>
	<el-icon @click="changeIcon" class="fa-icon">
			<!-- 在 <script setup> 中要使用动态组件的时候,就应该使用动态的 :is 来绑定-->
			<component :is="status ? Expand : Fold"/>
	</el-icon>
</template>

<script setup lang='ts'>
import { ref, computed } from 'vue';
//引入图标(局部)
import { Fold,Expand } from '@element-plus/icons'
//引入自己的useStore
import { useStore } from '@/store/index'

const store = useStore();

//定义状态
const status = computed(()=>{
	return store.getters['getCollapse']
})
//切换图标
const changeIcon = ()=>{
	// states.value = !states.value;
	store.commit('setCollapse',!status.value);
}
</script>
<style scoped lang='scss'>
	.fa-icons {
		display: flex;
		align-items: center;
		font-size: 24px;
		color: #303133;
		cursor: pointer;
	}
</style>

6.2.3、index.ts

https://element-plus.gitee.io/zh-CN/component/menu.html#menu-属性

在src/store/index.ts

// store.ts
import { InjectionKey } from 'vue'
import { createStore, useStore as baseUseStore, Store } from 'vuex'

export interface State {
  count: number,
	//collapse	是否水平折叠收起菜单(仅在 mode 为 vertical 时可用)	boolean
	collapse:boolean
}

export const key: InjectionKey<Store<State>> = Symbol()

export const store = createStore<State>({
  state: {
    count: 0,
		collapse: false
  },
	mutations: {
		setCount(state:State,count:number){
			state.count = count;
		},
		//设置collapse
		setCollapse:(state: State,collapse: boolean)=>{
			state.collapse = collapse;
		}
	},
	getters: {
		getCount(state:State){
			return state.count;
		},
		//获取collapse
		getCollapse:(state:State)=>{
			return state.collapse;
		}
	}
})

// 定义自己的 `useStore` 组合式函数
export function useStore () {
  return baseUseStore(key)
}

6.2.4、Header.vue

在src/layout/header/Header.vue中引入Collapse组件

<template>
  <Collapse></Collapse>
</template>

<script setup lang="ts">
  import Collapse from '@/layout/header/Collapse.vue'
</script>

6.2.5、MenuBar.vue

在src/layout/menu/MenuBar.vue中,控制菜单展开和关闭

<template>
	<!-- 绑定isCollapse -->
	<MenuLogo v-if="!isCollapse"></MenuLogo>
	<!-- 使用default-active -->
  <el-menu
    :default-active="activeIdx"
    class="el-menu-vertical-demo"
    :collapse="isCollapse"
		background-color="#304156"
		unique-opened
		router

  >
	<!-- 引入导航栏Item -->
	<MenuItem :menuList='menuList'></MenuItem>
  </el-menu>
</template>

<script setup lang="ts">
// setup语法糖父子组件传值的方法
// 	父组件传值给子组件,通过属性绑定方式
// 	子组件通过defineProps接收,无需显示的引入
// 	插槽的使用
// reactive:响应式数据定义,适用于对象类型

import { reactive, ref } from 'vue';
//引入路由
import { useRoute } from 'vue-router'
//引入自己的useStore
import { useStore } from '@/store/index'
import MenuItem from './MenuItem.vue'
import MenuLogo from '@/layout/menu/MenuLogo.vue'
import { computed } from '@vue/reactivity';

//setup 语法糖中,定义的数据和方法,直接可以在模板中使用,无需return

const store = useStore()
//获取当前路由
const route = useRoute();
const activeIdx = computed(()=>{
	const {path} = route;
	return path;
})

//菜单
let menuList = reactive([
	{
		path: '/dashboard',
		component: "Layout",
		meta: {
			title: "首页",
			icon: "HomeFilled",
			roles: ["sys:manage"]
		},
		children: []
	},
	{
		path: "/system",
		component: "Layout",
		alwaysShow: true,
		name: "system",
		meta: {
			title: "系统管理",
			icon: "Menu",
			roles: ["sys:manage"],
			parentId: 0
		},
		children: [
			{
				path: "/department",
				component: "/system/department/department",
				alwaysShow: false,
				name: "department",
				meta: {
					title: "机构管理",
					icon: "Document",
					roles: ["sys:dept"],
					parentId: 17,
				},
			},
			{
				path: "userList",
				coomponent: "/system/User/UserList",
				alwaysShow: false,
				name: "userList",
				meta: {
					title: "用户管理",
					icon: "Avatar",
					roles: ["sys:user"],
					parentId: 17,
				},
			},
			{
				path: "roleList",
				component: "/system/Role/RoelList",
				alwaysShow: false,
				name: "roleList",
				meta: {
					title: "角色管理",
					icon: "Tools",
					roles: ["sys:role"],
					parentId: 17,
				},
			},
			{
				path: "/menuList",
				component: "/system/Menu/MenuList",
				alwaysShow: false,
				name: "menuList",
				meta: {
					title: "权限管理",
					icon: "Document",
					roles: ["sys:menu"],
					parentId: 17,
				},
			},
		],	
	},
	{
		path: "/goods",
		component: "Layout",
		alwaysShow: true,
		name: "goods",
		meta: {
			title: "商品管理",
			icon: "Shop",
			roles: ["sys:goods"],
			parentId: 0,
		},
		children: [
			{
				path: "/goodsCategory",
				component: "/goods/goodsCategory/goodsCategoryList",
				alwaysShow: false,
				name: "goodCategory",
				meta: {
					title: "商品分类",
					icon: "Sell",
					roles: ["sys:goodsCategory"],
					parentId: 34,
				},
			},
		],
	},
	{
		path: "systemConfig",
		component: "Layout",
		alwaysShow: true,
		name: "systemConfig",
		meta: {
			title: "系统工具",
			icon: "Setting",
			roles: ["sys:systemConfig"],
			parentId: 0,
		},
		children: [
			{
				path: "/document",
				component: "/system/config/systemDocument",
				alwaysShow: false,
				name: "http://42.193.158.170:8089/swagger-ui/index.html",
				meta: {
					title: "接口文档",
					icon: "Document",
					roles: ["sys:document"],
					parentId: 42,
				},
			},
		],
	},

]);

//控制菜单展开和关闭
const isCollapse = computed(()=>{
	return store.getters['getCollapse']
})
</script>

<style scoped>
.el-menu-vertical-demo:not(.el-menu--collapse) {
  width: 230px;
  min-height: 400px;
}

.el-menu {
	border-right: none;
}

:deep(.el-sub-menu .el-sub-menu__title) {
	color: #f4f4f5 !important;
}

:deep(.el-menu .el-menu-item) {
	color: #bfcbd9;
}

/* 菜单点中文字的颜色 */
:deep(el-menu-item.is-active) {
	color: #409eff !important;
}

/* 当前打开菜单的所有子菜单颜色 */
:deep(.is-opened .el-menu-item) {
	background-color: #1f2d3d !important;
}

/* 鼠标移动菜单的颜色 */
:deep(.el-menu-item:hover) {
	background-color: #001528 !important;
}
</style>

7、面包屑导航

7.1、前置知识

7.2、MenuLogo.vue添加动画

7.2.1、animation

在src/layout/menu/MenuBar.vue中添加动画样式

<style lang="scss" scoped>
/* 添加动画 */
@keyframes logoAnimation {
	0% {
		transform: scale(0);
	}
	50% {
		transform: scale(1);
	}
	100% {
		transform: scale(1);
	}
}
.layout-logo {
	animation: logoAnimation 1s ease-out;
}
</style>

7.2.2、layout-logo

在src/layout/menu/MenuBar.vue中的MenuLogo添加class

<template>
	<!-- 绑定isCollapse -->
	<MenuLogo class="layout-logo" v-if="!isCollapse"></MenuLogo>
	<!-- 使用default-active -->
  <el-menu
    :default-active="activeIdx"
    class="el-menu-vertical-demo"
    :collapse="isCollapse"
		background-color="#304156"
		unique-opened
		router

  >
	<!-- 引入导航栏Item -->
	<MenuItem :menuList='menuList'></MenuItem>
  </el-menu>
</template>

7.3、BredCum.vue

在src/layout/header/下新建BredCum.vue组件
https://element-plus.gitee.io/zh-CN/component/breadcrumb.html

<template>
	<el-breadcrumb separator="/">
    <el-breadcrumb-item :to="{ path: '/' }">homepage</el-breadcrumb-item>
    <el-breadcrumb-item
      ><a href="/">promotion management</a></el-breadcrumb-item
    >
    <el-breadcrumb-item>promotion list</el-breadcrumb-item>
    <el-breadcrumb-item>promotion detail</el-breadcrumb-item>
  </el-breadcrumb>
</template>

<script setup lang='ts'>
import { ref, reactive } from 'vue';
</script>
<style scoped lang='scss'>
</style>

7.3.1、Header.vue使用面包屑

在src/layout/header/Header.vue中使用面包屑

<template>
  <Collapse></Collapse>
  <!-- 使用面包屑 -->
  <BredCum></BredCum>
</template>

<script setup lang="ts">
  import Collapse from '@/layout/header/Collapse.vue'
  //引入面包屑
  import BredCum from '@/layout/header/BredCum.vue'
</script>

7.3.2、Collapse.vue添加样式

<style lang='scss' scoped>
	.fa-icons {
		display: flex;
		align-items: center;
		font-size: 24px;
		color: #303133;
		cursor: pointer;
		// 没有生效,待解决
		margin-right: 15px;
	}
</style>

7.3.3、获取面包屑导航数据

在src/layout/header/BredCum.vue中

<template>
	<el-breadcrumb separator="/">
    <el-breadcrumb-item v-for="item in tabs">{{item.meta.title}}</el-breadcrumb-item>
  </el-breadcrumb>
</template>

<script setup lang='ts'>
import { ref, watch,Ref } from 'vue';
//引入useStore
import { useRoute,RouteLocationMatched } from 'vue-router'

//定义面包屑导航数据并指定类型,Ref泛型
const tabs: Ref<RouteLocationMatched[]> = ref([]);
const route = useRoute();
//构造面包屑数据
const getBredcurm = ()=>{
	//获取所有meta和title
	let mached = route.matched.filter(item => item.meta && item.meta.title);
	//判断第一个是否首页,如果不是,构造一个
	const first = mached[0];
	if(first.path !== '/dashboard'){
		//构造一个
		mached = [{path: '/dashboard',meta:{title:'首页'}} as any].concat(mached);

	}
	//设置面包屑导航数据
	tabs.value = mached;
}
//进入时调用
getBredcurm();
//监听路由发生变化,重新获取面包屑导航数据
watch(()=>route.path,()=>getBredcurm())
//还可以制作带路由的面包屑

</script>
<style scoped lang='scss'>
</style>

8、tabs选项卡

8.1、前置知识

  1. vuex在组合API中的使用

    const store = useStore();
    
  2. vue-router在组合API中的使用

    const route = useRoute();
    const router = useRouter();
    
  3. 响应式数据的定义;ref、reactive

  4. watch、computed的使用

  5. element plus组件Tabs标签的使用

  6. TypeScript中接口interface的使用,接口是一种规范

8.2、功能分析

  1. 点击左侧菜单,右侧内容展示区显示对应的选项卡
  2. 点击右侧选项卡,左侧对应菜单也要相应的选中
  3. 解决刷新后,Tabs数据丢失的问题,window,addEventListener(“befor eunload”)

8.3、Tabs.vue

在src/layout/tabs目录下,新建Tabs.vue组件

https://element-plus.gitee.io/zh-CN/component/tag.html

<template>
		<!-- 自定义增加标签页触发器 -->
	  <el-tabs
    v-model="activeTab"
		@tab-click="handleClick"
    type="card"
    class="demo-tabs"
    closable
    @tab-remove="removeTab"
  >
    <el-tab-pane
      v-for="item in tabList"
      :key="item.path"
      :label="item.title"
      :name="item.path"
    >
    </el-tab-pane>
  </el-tabs>
</template>

<script setup lang='ts'>
import { ref, computed,watch,onMounted } from 'vue';
//引入自己的useStore
import { useStore } from '@/store/index';
//映入路由
import { useRoute,useRouter } from 'vue-router';
//引入ITab
import { ITab } from '@/store/type/index';

const route = useRoute();
const router = useRouter();
const store = useStore();
//获取tabs数据
const tabList = computed(()=>{
	return store.getters['getTabs']
});

//当前激活的选项卡,跟当前激活的路由时一样的
const activeTab = ref('');
const setActiveTab = ()=>{
	activeTab.value = route.path;
}

//删除选项卡
const removeTab = (targeName:string)=>{
	//首页不能删除
	if(targeName === '/dashboard') return;
	//选项卡数据列表
	const tabs = tabList.value;
	let activeName = activeTab.value;
	if(activeName === targeName){
		tabs.forEach((tab:ITab,index:number) => {
			if(tab.path === targeName){
				const nextTab = tabs[index+1] || tabs[index -1]
				if(nextTab){
					activeName = nextTab.path
				}
			}
		});
	}
	//重新设置当前激活的选项卡
	activeTab.value = activeName
	//重新设置选项卡数据
	store.state.tabList = tabs.filter((tab:ITab)=> tab.path !== targeName)
	//跳转路由
	router.push({path:activeName})

}

//添加选项卡
const addTab = ()=>{
	//从当前路由中获取path和title
	const {path,meta} = route;
	//通过vuex设置
	const tab:ITab = {
		path:path,
		title:meta.title as string,
	}
	store.commit('addTab',tab);
}

//监听路由的变化
watch(()=>route.path,()=>{
	//设置激活选项卡
	setActiveTab();
	//把当前路由添加到选项卡数据
	addTab();

})

//解决刷新后,Tabs数据丢失的问题
const beforeRefresh = ()=>{
	window.addEventListener('beforeunload',()=>{
		sessionStorage.setItem('tabsView',JSON.stringify(tabList.value))
	})
	let tabSession = sessionStorage.getItem("tabsView");
	if(tabSession){
		let old_tabs = JSON.parse(tabSession);
		if(old_tabs.length > 0){
			store.state.tabList = old_tabs;
		}
	}
}

onMounted(()=>{
	//解决选项卡丢失问题
	beforeRefresh();
	//设置激活选项卡
	setActiveTab();
	//把当前路由添加到选项卡数据
	addTab();
})

const handleClick = (tab:any)=>{
	console.log(tab)
	const {props} = tab;
	console.log(props)
	//跳转路由
	router.push({path:props.name})
}
</script>
<style scoped lang='scss'>
:deep(.el-tabs-header) {
	margin: 0px;
}
:deep(.el-tabs__item) {
	height: 26px !important;
	line-height: 26px !important;
	text-align: center !important;
	border: 1px solid #d8dce5 !important;
	margin: 0px 3px !important;
	color: #495060;
	font-size: 12px !important;
	padding: 0px 10px !important;
}

:deep(.el-tabs__nav) {
	border: none !important;
}

:deep(.is-active) {
	border-bottom: 1px solid transparent !important;
	border: 1px solid #42b983 !important;
	background-color: #42b983 !important;
	color: #fff !important;
}

:deep(.el-tabs__item:hover) {
	color: #495060 !important;
}

:deep(.is-active:hover) {
	color: #fff !important;
}
</style>

8.4、Tabs选项卡制作总结

8.4.1、实现原理

点击菜单,显示对应的选项卡
	watch监听路由path的变化,把当前路由的title和path放到Tabls选项卡对用的数据里面
选项卡的激活设置
	把当前激活的选项卡v-mode绑定项为当前路由的path
点击选项卡,左侧对应菜单激活
	点击选项卡,跳转到对应的路由;只需要把选项卡的v-mode绑定项设为当前路由的path,左侧菜单便可自动激活
关闭选项卡
	首页不能关闭,关闭时,删除当前选项卡,重新设置vuex里面的选项卡数据;并跳转到新的路由;
刷新浏览器时,选项卡数据丢失
	window.addEventListener('beforeunload')

8.4.2、TypeScript知识

interface和type的基本使用
typeof和keyof的使用
反型、泛型约束的使用
TppeScript模板字符串的基本使用
TypeScript中文交叉类型、联合类型的基本使用
Omit、Pick、Parameters、ReturnType的基本使用

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/8301.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

第七章 Java编程-多线程

线程几乎在每个编程语言中都有&#xff0c;它其实是操作系统的概念&#xff0c;编程语言是运行在操作系统上的

RK3568平台开发系列讲解(图像篇)BMP图像处理

🚀返回专栏总目录 文章目录 一、BMP文件格式解析1.1、位图文件头(bitmap-file header)1.2、位图信息头(bitmap-information header)二、LCD上显示代码沉淀、分享、成长,让自己和他人都能有所收获!😄 📢我们今天来讲解BMP文件格式的解析。 一、BMP文件格式解析 BMP是一…

发那科机床联网

一、设备信息确认 1、确认型号 数控面板拍照确认&#xff1a; 此系统为&#xff1a;0I-TD 注&#xff1a;凡是系统中带i的&#xff0c;基本上都有网络通讯和采集功能。如果系统中带有mate字样&#xff0c;并且比较老可能不含网口。 2、确认通讯接口 发那科的通讯接口有两种…

【SpringBoot项目】SpringBoot项目-瑞吉外卖【day02】员工管理业务开发

文章目录前言员工管理业务开发完善登录功能问题分析代码实现功能测试新增员工需求分析数据模型代码开发功能测试统一处理异常员工信息分页查询需求分析代码开发功能测试启用/禁用员工需求分析代码实现测试编辑员工信息需求分析代码实现功能测试总结&#x1f315;博客x主页&…

VS2022 性能提升:更快的 C++ 代码索引

基于 Visual Studio 2022 17.3 版本的性能提升&#xff0c;我们在新的 17.4 版本中添加了更多的小优化&#xff0c;且听我慢慢道来。 不论你是一个工作在大型代码库下的游戏开发者&#xff0c;或者你在解决方案中有非常多的 C 工程&#xff0c;在 Visual Studio 2022 17.4 中&…

【附源码】计算机毕业设计JAVA家装建材网

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; Springboot mybatis Maven Vue 等等组成&#xff0c;B/…

浅析DNS劫持及应对方案

DNS是网络连接中的重要一环&#xff0c;它与路由系统共同组成互联网上的寻址系统&#xff0c;如果DNS遭遇故障&#xff0c;“导航系统”失效&#xff0c;网络连接就会出现无法触达或到达错误地址的情况。由于的DNS重要作用及天生脆弱性&#xff0c;导致DNS自诞生之日起&#xf…

React源码解读之任务调度

React 设计体系如人类社会一般&#xff0c;拨动时间轮盘的那一刻&#xff0c;你便成了穿梭在轮片中的一粒细沙&#xff0c;角逐过程处处都需要亮出你的属性&#xff0c;你重要吗&#xff1f;你无可替代吗&#xff1f;你有特殊权限吗&#xff1f;没有&#xff0c;那不好意思&…

Autosar模块介绍:AutosarOS(5)

上一篇 | 返回主目录 | 下一篇 AutosarOS&#xff1a;错误处理、跟踪与调试&#xff08;5&#xff09;1 钩子例程2 错误处理&#xff08;ErrorHook&#xff09;3 系统启动&#xff08;StartupHook&#xff09;4 系统关闭&#xff08;ShutdownHook&#xff09;5 系统保护&#x…

【面试题】margin负值问题

margin-top和margin-left负值&#xff0c;元素向上、向左移动&#xff1b;margin-right负值&#xff0c;右侧元素左移&#xff0c;自身不受影响&#xff1b;margin-bottom负值&#xff0c;下方元素上移&#xff0c;自身不受影响&#xff1b; 1. margin top left为负数 <st…

0095 贪心算法,普利姆算法

import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; /* * 贪心算法 * 1.指在对问题进行求解时&#xff0c;在 每一步 选择中都采取最好或最优的选择&#xff0c;希望能够导致结果是最好或最优的算法 * 2.所得到的结果不一定是最优结果&…

【SSH远程登录长时间连接后容易出现自动断开的解决方案】

SSH远程登录长时间连接后容易出现自动断开的解决方案0 问题描述1 方法一1.1 打开ssh_config文件1.2 在文件中添加以下内容1.3 重启ssh2 方法二2.1 打开sshd_config文件2.2 在文件中添加以下内容2.3 重启ssh0 问题描述 使用SSH连接远程服务器的时候 报出 client_loop send disc…

时序分析 48 -- 时序数据转为空间数据 (七) 马尔可夫转换场 python 实践(下)

时序分析 48 – 时序数据转为空间数据 (七) 马尔可夫转换场 python 实践&#xff08;下&#xff09; … 接上 从MTF到图模型 从MTF中我们可以生成图 &#x1d43a;(&#x1d449;,&#x1d438;)&#x1d43a;(&#x1d449;,&#x1d438;)G(V,E) &#xff0c;节点V和时间…

Redis从理论到实战:使用Redis实现商铺查询缓存(逐步分析缓存更新策略)

文章目录一、什么是缓存二、缓存的作用三、添加商户缓存四、分析缓存更新策略1、删除缓存还是更新缓存&#xff1f;2、如何保证缓存与数据库的操作同时成功或失败&#xff1f;3、先操作缓存还是先操作数据库&#xff1f;加油加油&#xff0c;不要过度焦虑(#^.^#) 一、什么是缓存…

ThreadLocal为什么会出现内存泄漏,你真的知道吗?

目录 1 前言 2 ThreadLocal进行线程隔离的小示例 3 原因 1 前言 大家想要搞清楚这个问题&#xff0c;就必须知道内存泄漏和内存溢出的区别 内存泄漏&#xff1a;不就被使用的对象或者变量无法被回收 内存溢出&#xff1a;没有剩余的空间来创建新的对象 2 ThreadLocal进行…

Java中的字符串

&#x1f649; 作者简介&#xff1a; 全栈领域新星创作者 &#xff1b;天天被业务折腾得死去活来的同时依然保有对各项技术热忱的追求&#xff0c;把分享变成一种习惯&#xff0c;再小的帆也能远航。 &#x1f3e1; 个人主页&#xff1a;xiezhr的个人主页 java中的字符串一、简…

C++:重定义:符号重定义:变量重定义

概述&#xff1a;在上一篇我们知道 通过 #ifndef....#defin....#endif &#xff0c; 这个解决头文件重复包含的问题 C&#xff1a;重定义&#xff1a;class类型重定义_hongwen_yul的博客-CSDN博客 避免头文件的重复包含可以有效的避免变量的重复定义&#xff0c;其实不光变量…

[附源码]java毕业设计基于web旅游网站的设计与实现

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

使用Docker开发GO应用程序

根据Stack Overflow的2022开发者调查&#xff0c;Go&#xff08;或Golang&#xff09;是最受欢迎和最受欢迎的编程语言之一。由于与许多其他语言相比&#xff0c;Go的二进制大小更小&#xff0c;开发人员经常使用Go进行容器化应用程序开发。 Mohammad Quanit在他的社区全能课程…

小程序vant-tabbar使用示例,及报错处理

小程序vant-tabbar使用示例&#xff0c;及报错处理1. 配置信息2. 添加 tabBar 代码文件3. 编写 tabBar 代码custom-tab-bar/index.tscustom-tab-bar/index.jsoncustom-tab-bar/index.wxml使小程序使用vant-tabbar组件时&#xff0c;遇到以下报错&#xff1a;Couldn’t found th…