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、技能
- 掌握Vue3.2语法糖的使用
- 掌握Vue3中组合api的使用
- 掌握组件中业务逻辑抽离的方法
- 掌握TypeScript在Vue3中的使用
- 掌握动态菜单、动态路由、按钮权限的实现方式
- Vue3中全局挂载使用方式
- Vue3父子组件的使用
- Vue3中Echarts的使用
- token、权限验证
- Vuex4.x +Ts在commit、getter、dispatch中的代码提示
- 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、前置知识
-
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、前置知识
-
vuex在组合API中的使用
const store = useStore();
-
vue-router在组合API中的使用
const route = useRoute(); const router = useRouter();
-
响应式数据的定义;ref、reactive
-
watch、computed的使用
-
element plus组件Tabs标签的使用
-
TypeScript中接口interface的使用,接口是一种规范
8.2、功能分析
- 点击左侧菜单,右侧内容展示区显示对应的选项卡
- 点击右侧选项卡,左侧对应菜单也要相应的选中
- 解决刷新后,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的基本使用