Vue项目实战

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的基本使用
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值