一、路由配置
項目一共需要4個一級路由:登錄(login)、主頁(home)、404、任意路由(重定向到404)。
1.1 安裝路由插件
pnpm install vue-router
1.2 創建路由組件?
在src目錄下新建views文件夾,在views中創建login、home、404路由組件。
1.3 配置路由
在src目錄下新建router文件夾,書寫路由配置(包含index.ts和routes.ts)。
src/router/routes.ts
// 對外暴露配置路由(常量路由)
export const constantRoute = [{// 登錄path: '/login',component: () => import('@/views/login/index.vue'),name: 'login'},{// 登錄成功以后展示數據的路由path: '/',component: () => import('@/views/home/index.vue'),name: 'layout'},{// 404path: '/404',component: () => import('@/views/404/index.vue'),name: '404'},{// 任意路由path: '/:pathMatch(.*)*',redirect: '/404',name: 'Any'}
]
src/router/index.ts
// 通過vue-router插件實現路由配置
import { createRouter, createWebHashHistory } from 'vue-router';
// 引入routes配置項
import { constantRoute } from './routes';
// 創建路由
let router = createRouter({// 路由模式hashhistory: createWebHashHistory(),routes: constantRoute,// 滾動行為scrollBehavior() {return {left: 0,top: 0}}
})export default router;
1.4 引入路由
在入口文件(main.js)引入路由:
// 引入路由
import router from '@/router'
// 注冊模板路由
app.use(router)
最后,在模板中通過 <router-view></router-view>占位,根據當前的路由狀態動態地渲染匹配到的組件。
二、登錄模塊
?2.1 登錄路由靜態的搭建
采用element-plus中的Layout布局(柵格布局)、From表單組件、input組件、button組件。
Layout布局:一共是24 分欄,:span代表柵格占據的列數,:xs代表屏幕寬度<768px時柵格占據的列數。
input組件::prefix-icon代表前綴圖標,show-password代表是否顯示切換密碼圖標
src/views/login/index.vue
<template><div class="login_container"><el-row><el-col :span="12" :xs="0"></el-col><el-col :span="12" :xs="24"><el-form class="login_from"><h1>Hello</h1><h2>歡迎來到唧唧bong甄選</h2><el-form-item><el-input :prefix-icon="User" v-model="loginFrom.username"></el-input></el-form-item><el-form-item><el-input type="password" :prefix-icon="Lock" v-model="loginFrom.password" show-password></el-input></el-form-item><el-form-item><el-button class="login_btn" type="primary" size="default">登錄</el-button></el-form-item></el-form></el-col></el-row></div>
</template><script setup lang="ts">
import {User, Lock} from '@element-plus/icons-vue'
import { reactive } from 'vue';
let loginFrom = reactive({username: 'admin',password: '111111'
})
</script><style scoped lang="scss">
.login_container {width: 100%;height: 100vh;background: url('@/assets/images/background.jpg') no-repeat;background-size: cover;.login_from{width: 80%;position: relative;top: 30vh;background: url('@/assets/images/login_form.png') no-repeat;background-size: cover;padding: 40px;h1{color: white;font-size: 40px;}h2{color: white;font-size: 20px;margin: 20px 0;}.login_btn{width: 100%;}}
}
</style>
2.2 模板封裝登錄業務
點擊登錄時,會攜帶用戶名和密碼向服務器發請求獲取token,此時我們需要把token存儲起來,用于后續向服務端發請求獲取信息的身份驗證,這里我們用pinia和loacalStroage進行存儲。
安裝pinia
pnpm i pinia
創建大倉庫:src/store/index.ts
import { createPinia } from 'pinia'
//創建大倉庫
const pinia = createPinia()
//對外暴露:入口文件需要安裝倉庫
export default pinia
在入口文件(main.ts)中引入并安裝:src/main.ts
// 引入大倉庫
import pinia from './store'
// 安裝倉庫
app.use(pinia)
創建小倉庫:src/store/modules/user.ts
import { defineStore } from 'pinia'
// 引入接口
import { reqLogin } from '@/api/user'
// 引入類型
import type { loginForm, loginResponseData } from '@/api/user/type'
import type { UserState } from './types/type'
// 引入操作本地存儲的工具方法
import { SET_TOKEN, GET_TOKEN } from '@/utils/token'
// 創建用戶小倉庫
const useUserStore = defineStore('User', {// 小倉庫存儲數據的地方state: (): UserState => {return {token: GET_TOKEN(), //用戶唯一的標識token}},// 異步|邏輯的地方actions: {// 用戶登錄的方法async userLogin(data: loginForm) {// 登錄請求let result: loginResponseData = await reqLogin(data)// 登錄請求:成功200->token// 登錄請求:失敗201->登錄失敗錯誤的信息if (result.code === 200) {// pinia倉庫存儲一下token// 由于pinia|vuex存儲數據其實利用js對象(非持久化存儲)this.token = (result.data.token as string)// 本地化持久存儲一份SET_TOKEN((result.data.token as string))// 能保證當前async函數返回一個成功的promisereturn 'ok'}else {return Promise.reject(new Error(result.data.message))}}},getters: {}
})// 對外暴露用戶小倉庫
export default useUserStore
在登錄頁面中引入小倉庫,點擊登錄時通知user小倉庫發請求,存儲token:src/views/login/index.vue
<script setup lang="ts">
import { reactive, ref } from 'vue';
import { useRouter } from 'vue-router'
// 引入element-plus提示框
import { ElNotification } from 'element-plus'
// 引入用戶相關的小倉庫
import useUserStore from '@/store/modules/user'
let useStore = useUserStore()// 獲取路由
let $router = useRouter()
// 定義變量控制按鈕加載效果
let loading = ref(false)
// 收集賬號與密碼的數據
let loginFrom = reactive({username: 'admin',password: '111111'
})
const login = async () => {// 加載效果:開始加載loading.value = true//點擊登錄按鈕以后干什么?//通知倉庫發登錄請求//請求成功->首頁展示數據的地方//請求失敗->彈出登錄失敗信息try {// 保證登錄成功await useStore.userLogin(loginFrom)// 編程式導航跳轉到展示數據首頁$router.push('/')// 登錄成功信息提示ElNotification({type: 'success',message: '登錄成功',})// 登錄成功加載效果消失loading.value = false} catch (error) {// 登錄失敗加載效果消失loading.value = false// 登錄失敗的提示信息ElNotification({type: 'error',message: (error as Error).message})}
}
</script>
- userLogin會返回一個Promise,此處可以使用try...catch...或.then來進行下一步結果處理。
- 不管成功或失敗,都需要使登錄加載效果消失,因此也可以統一寫在finally里面:
try {// 保證登錄成功await useStore.userLogin(loginFrom)// 編程式導航跳轉到展示數據首頁$router.push('/')// 登錄成功信息提示ElNotification({type: 'success',message: '登錄成功',}) } catch (error) {// 登錄失敗的提示信息ElNotification({type: 'error',message: (error as Error).message})} finally{// 登錄成功/失敗加載效果消失loading.value = false}
2.3 用戶倉庫數據ts類型的定義
定義小倉庫數據state類型:src\store\modules\types\type.ts?
// 定義小倉庫數據state類型
export interface UserState {token: string | null
}
?登錄接口返回的數據類型:src\api\user\type.ts?
登錄請求可能返回成功/失敗的數據,因此類型需要dataType需要包括成功的數據token和失敗的數據message,且是可選的,要加上"?"。
interface dataType {token?: string,message?:string
}// 登錄接口返回的數據類型
export interface loginResponseData {code: number,data: dataType
}
封裝本地存儲數據和讀取方法:src/utils/token.js
// 存儲數據
export const SET_TOKEN = (token: string) => {localStorage.setItem('TOKEN', token)
}// 本地存儲獲取數據
export const GET_TOKEN = () => {return localStorage.getItem('TOKEN')
}
2.4 登錄時間的判斷與封裝
在utils中封裝一個函數:src/utils/time.js
// 封裝一個函數:獲取一個結果:當前早上|上午|中午|下午|晚上
export const getTime = () => {let time = ''// 通過內置的構造函數Datelet hour = new Date().getHours()if (hour < 9) {time = '早上'}else if (hour <= 12) {time = '上午'}else if (hour <= 14) {time = '中午'}else if (hour <= 18) {time = '下午'}else {time = '晚上'}return time
}
?在login組件中引入并使用
// 引入當前時間的函數
import { getTime } from '@/utils/time'
......// 登錄成功信息提示
ElNotification({type: 'success',message: '歡迎回來',title: `HI,${getTime()}好`
})
2.5 登錄模塊表單校驗
使用element-plus的表單驗證功能 ,步驟如下:
- 給el-form添加 :model="loginFrom"和:rules="rules"
- 給需要驗證的每個el-form-item添加prop屬性,如?prop="username"、prop="password"
- 定義表單校驗需要配置對象rules
- 請求前使用 loginFroms.value.validate()觸發表單中所有表單項的校驗,保證全部的表單項校驗通過再發請求
- :model:要驗證的表單數據對象
- :rules="rules":表單驗證規則
- prop:要校驗字段的屬性名
// 第一步:給el-form添加 :model="loginFrom"和:rules="rules"
<el-form class="login_form" :model="loginForm" :rules="rules" ref="loginFroms">// 第二步:給需要驗證的每個el-form-item添加prop屬性,如?prop="username"、prop="password"
<el-form-item prop="username"><el-input :prefix-icon="User" v-model="loginFrom.username"></el-input>
</el-form-item>
<el-form-item prop="password"><el-input type="password" :prefix-icon="Lock" v-model="loginFrom.password" show-password></el-input>
</el-form-item>// 第三步:定義表單校驗需要配置對象rules
// 規則對象屬性:
// required:代表這個字段必須校驗
// min:文本長度至少多少位
// max:文本長度最多多少位
// message:錯誤的提示信息
// trigger:觸發校驗表單的時機,change:文本發生變化時觸發校驗,blur:失去焦點時觸發校驗
const rules = {username: [{ required: true, min: 5, max: 10, message: '用戶名長度應為5-10位', trigger: 'change' },],password: [{ required: true, min: 6, max: 10, message: '密碼長度應為6-10位', trigger: 'change' },
?]
}第四步:請求前使用 loginFroms.value.validate()觸發表單中所有表單項的校驗,保證全部的表單項校驗通過再發請求
// 通過ref屬性獲取el-form組件
let loginFroms = ref()
const login = async () => {// 保證全部的表單項校驗通過再發請求await loginForms.value.validate()......
}
PS:在
el-form
組件中,可以使用ref
屬性來獲取表單的引用,然后調用該引用上的validate
方法。這個方法會觸發表單中所有表單項的校驗,并返回一個Promise
對象,該對象的resolve
回調函數會在校驗通過時被調用,而reject
回調函數會在校驗失敗時被調用。
?2.6 自定義校驗表單
上面的驗證比較簡單,公司的開發項目中表單驗證會更復雜,這個時候就要用到element-plus的自定義校驗規則了?。
自定義校驗表單的配置項中需要一個validator屬性,值是一個方法,用于書寫自定義規則。
// 自定義校驗規則函數
const validateUsername = (rule: any, value: any, callback: any) => {//rule:即為校驗規則對象//value:即為表單元素文本內容//函數:如果符合條件callback放行通過即為//如果不符合條件callback方法,注入錯誤提示信息if(value.length >= 5){callback()}else{callback(new Error('用戶名不少于5位'))}
}const validatePassword = (rule: any, value: any, callback: any) => {if(value.length >= 6){callback()}else{callback(new Error('用戶名不少于6位'))}
}// 定義表單校驗需要配置對象
const rules = {username: [{ validator: validateUsername, trigger: 'change' },],password: [{ validator: validatePassword, trigger: 'change' },]
}
PS:這里只是簡單的示范,正式開發中大多場景的校驗規則會更復雜,需要用到正則表達式來書寫。
三、layout組件?
3.1 layout組件的靜態搭建?
layout組件主頁分為三部分:左側菜單、頂部導航、內容展示區域。
在src目錄下創建layout組件:src/layout/index.vue
<template><div class="layout_container"><!-- 左側菜單 --><div class="layout_slider">左側菜單</div><!-- 頂部導航 --><div class="layout_tabbar">頂部導航</div><!-- 內容展示區域 --><div class="layout_main"><p style="height: 10000px;background: red;">內容</p></div></div>
</template><script setup lang="ts"></script><style scoped lang="scss">
.layout_container {width: 100%;height: 100vh;.layout_slider {width: $base-menu-width;height: 100vh;background: $base-menu-background;}.layout_tabbar {position: fixed;width: calc(100% - $base-menu-width);height: $base-tabbar-height;background: $base-tabbar-background;top: 0px;left: $base-menu-width;}.layout_main {position: absolute;width: calc(100% - $base-menu-width);height: calc(100vh - $base-tabbar-height);background: $base-main-background;top: $base-tabbar-height;left: $base-menu-width;overflow: auto;padding: 20px;}
}
</style>
配置layout相關的樣式的全局變量:src/styles/variable.scss
// 左側菜單的寬度
$base-menu-width: 260px;
// 左側菜單的背景顏色
$base-menu-background: #001529;
// 頂部導航的高度
$base-tabbar-height: 50px;
// 頂部導航的背景顏色
$base-tabbar-background: #ffffff;
// 內容展示區域的背景顏色
$base-main-background: #ccc8cc;
?設置滾動條樣式:src/styles/index.scss
// 滾動條外觀設置
::-webkit-scrollbar{width: 10px;
}::-webkit-scrollbar-track{background: $base-menu-background;
}::-webkit-scrollbar-thumb{width: 10px;background: yellowgreen;border-radius: 10px;
}
3.2 Logo組件的封裝
創建logo組件:src/layout/logo/index.vue
<template><div class="logo" v-if="setting.logoHidden"><img :src="setting.logo" alt=""><p>{{ setting.title }}</p></div>
</template><script setup lang="ts">
//引入設置標題與logo這配置文件
import setting from '@/setting'
</script><style scoped lang="scss">
.logo {width: 100%;height: $base-menu-logo-height;color: white;display: flex;align-items: center;padding: 10px;img {width: 40px;height: 40px;}p {font-size: $base-logo-title-fontSize;margin-left: 10px;}
}
</style>
配置logo相關的樣式的全局變量:src/styles/variable.scss
//左側菜單logo高度設置
$base-menu-logo-height:50px;//左側菜單logo右側文字大小
$base-logo-title-fontSize:16px;
項目logo/標題配置文件:src/setting.ts
// 用于項目logo|標題配置
export default {title:'唧唧bong甄選運營平臺', // 項目標題logo:'/logo.png', // 項目logo設置logoHidden: true // logo組件是否隱藏設置
}
3.3 左側菜單組件
3.3.1 遞歸組件生成動態菜單
創建menu組件:src/layout/menu/index.vue,并在layout中引入并使用menu組件
添加二級路由:src/router/routes.ts
{// 登錄成功以后展示數據的路由path: '/',component: () => import('@/layout/index.vue'),name: 'layout',meta: {title: 'layout',hidden: true },children: [{path: '/home',component: () => import('@/views/home/index.vue'),name: 'home',meta: {title: '首頁',hidden: false }}]},
將路由數組存儲到store中(方便組件訪問路由數據):src/store/modules/user.ts
// 引入路由(常量路由)
import { constantRoute } from '@/router/routes';
// 創建用戶小倉庫
const useUserStore = defineStore('User', {// 小倉庫存儲數據的地方state: (): UserState => {return {token: GET_TOKEN(), //用戶唯一的標識token//路由配置數據menuRoutes: constantRoute}},......
})
UserState中添加路由的類型定義:src/store/modules/types/type.ts
// 引入描述路由配置信息的類型(這個類型包含了路由的路徑、組件、子路由等信息)
import type { RouteRecordRaw } from 'vue-router'
// 定義小倉庫數據state類型
export interface UserState {token: string | null,menuRoutes: RouteRecordRaw[]
}
layout組件中引入小倉庫獲取路由數據,通過props傳遞給menu組件:src/layout/index.vue
<template><div class="layout_container"><!-- 左側菜單 --><div class="layout_slider"><Logo></Logo><!-- 展示菜單 --><el-scrollbar class="scrollbar"><el-menu background-color="#001529" text-color="white"><!-- 傳遞路由數據給menu組件 --><Menu :menuList="userStore.menuRoutes"></Menu></el-menu></el-scrollbar></div>......</div>
</template><script setup lang="ts">
// 引入菜單組件
import Menu from './menu/index.vue'// 獲取用戶相關的小倉庫
import useUserStore from '@/store/modules/user'
let userStore = useUserStore()
</script>
給每個路由添加meta元信息:src/router/routes.ts
meta: {title: '登錄', // 菜單標題hidden: true // 代表路由標題在菜單中是否隱藏 true:隱藏 false:顯示}
書寫menu組件:src/layout/menu/index.vue
1. menu組件分三種情況:
- 沒有子路由
- 有且只有一個子路由
- 有一個以上的子路由
2. 點擊菜單item跳轉路由(@click="goRoute"):
- <el-menu-item>標簽有click事件(菜單點擊時的回調函數,回調參數是el-menu-item實例。
<template><template v-for="(item, index) in menuList" :key="item.path"><!-- 沒有子路由 --><template v-if="!item.children"><el-menu-item :index="item.path" v-if="!item.meta.hidden" @click="goRoute"><template #title><el-icon><component :is="item.meta.icon"></component></el-icon><span>{{ item.meta.title }}</span></template></el-menu-item></template><!-- 有且只有一個子路由 --><template v-if="item.children && item.children.length == 1"><el-menu-item :index="item.children[0].path" v-if="!item.children[0].meta.hidden" @click="goRoute"><template #title><el-icon><component :is="item.children[0].meta.icon"></component></el-icon><span>{{ item.children[0].meta.title }}</span></template></el-menu-item></template><!-- 有子路由,且個數大于一 --><el-sub-menu v-if="item.children && item.children.length > 1" :index="item.path"><template #title><el-icon><component :is="item.meta.icon"></component></el-icon><span>{{ item.meta.title }}</span></template><Menu :menuList="item.children"></Menu></el-sub-menu></template>
</template><script setup lang="ts">
import { useRouter } from "vue-router";
// 獲取父組件傳遞過來的全部路由數組
defineProps(['menuList'])
// 獲取路由對象
let $router = useRouter()
// 點擊菜單的回調
const goRoute = (vc: any) => {// 路由跳轉$router.push(vc.index)
}
</script>
<script lang="ts">
export default {name: 'Menu'
}
</script><style scoped></style>
PS:遞歸組件必須有一個名字,因為在vue中,組件是通過其名字進行注冊和引用的。遞歸組件需要在自身的模板中引用自身,但如果組件沒有名字,Vue無法在模板中正確地引用它,從而導致遞歸出現問題。
?3.3.2 菜單圖標完成
將element-plus圖標 注冊成全局組件:src/components/index.ts
具體可參考官網:Icon 圖標 | Element Plus (gitee.io)
// 引入elemnet-plus提供全部圖標組件
import * as ElementPlusIconsVue from '@element-plus/icons-vue'// 對外暴露一個插件對象
export default {install(app: any) {// 將element-plus提供圖標注冊為全局組件for (const [key, component] of Object.entries(ElementPlusIconsVue)) {app.component(key, component)}}
}
菜單圖標由路由配置決定,meta中添加 icon 字段:src/router/routes.ts
meta: {......icon: 'Promotion', // 菜單文字左側的圖標,支持element-plus全部圖標}
在menu中使用element-plus圖標:src/layout/menu/index.vue
<el-icon><component :is="item.meta.icon"></component>
</el-icon>
3.3.3 項目全部路由配置
- 首頁重定向到home
- 權限管理和商品管理的一級路由用的還是組件 layout
src/router/routes.ts?
// 對外暴露配置路由(常量路由)
export const constantRoute = [{// 登錄path: '/login',component: () => import('@/views/login/index.vue'),name: 'login',meta: {title: '登錄', // 菜單標題hidden: true, // 代表路由標題在菜單中是否隱藏 true:隱藏 false:顯示icon: 'Promotion', // 菜單文字左側的圖標,支持element-plus全部圖標}},{// 登錄成功以后展示數據的路由path: '/',component: () => import('@/layout/index.vue'),name: 'layout',meta: {title: 'layout',hidden: true,icon: 'Avatar',},redirect: '/home',children: [{path: '/home',component: () => import('@/views/home/index.vue'),name: 'home',meta: {title: '首頁',hidden: false,icon: 'HomeFilled',}}]},{// 404path: '/404',component: () => import('@/views/404/index.vue'),name: '404',meta: {title: '404',hidden: true,icon: 'BrushFilled',}},{path: '/screen',component: () => import('@/views/screen/index.vue'),name: 'Screen',meta: {title: '數據大屏',hidden: false,icon: 'Platform',}},{path: '/acl',component: () => import('@/layout/index.vue'),name: 'Acl',meta: {title: '權限管理',icon: 'Lock',},children: [{path: '/acl/user',component: () => import('@/views/acl/user/index.vue'),name: 'User',meta: {title: '用戶管理',icon: 'User',}},{path: '/acl/role',component: () => import('@/views/acl/role/index.vue'),name: 'Role',meta: {title: '角色管理',icon: 'UserFilled',}},{path: '/acl/permission',component: () => import('@/views/acl/permission/index.vue'),name: 'Permission',meta: {title: '菜單管理',icon: 'Monitor',}},]},{path: '/product',component: () => import('@/layout/index.vue'),name: 'Product',meta: {title: '商品管理',icon: 'Goods',},children: [{path: '/product/trademark',component: () => import('@/views/product/trademark/index.vue'),name: 'Trademark',meta: {title: '品牌管理',icon: 'ShoppingCartFull',}},{path: '/product/attr',component: () => import('@/views/product/attr/index.vue'),name: 'Attr',meta: {title: '屬性管理',icon: 'ChromeFilled',}},{path: '/product/spu',component: () => import('@/views/product/spu/index.vue'),name: 'Spu',meta: {title: 'SPU管理',icon: 'Calendar',}},{path: '/product/sku',component: () => import('@/views/product/sku/index.vue'),name: 'Sku',meta: {title: 'SKU管理',icon: 'Orange',}},]},{// 任意路由path: '/:pathMatch(.*)*',redirect: '/404',name: 'Any',meta: {title: '任意路由',hidden: true,icon: 'Wallet',}}
]
?layout右側展示區域封裝成一個組件 main(為了實現一些動畫效果):src/layout/main/main.vue
關于路由過度可參考官網:過渡動效 | Vue Router (vuejs.org)
<template><!-- 路由組件出口的位置 --><router-view v-slot="{ Component }"><transition name="fade"><!-- 渲染layout一級路由組件的子路由 --><component :is="Component" /></transition></router-view>
</template><script setup lang="ts"></script><style scoped>
.fade-enter-from {opacity: 0;transform: scale(0);
}.fade-enter-active {transition: all .3s;
}.fade-enter-to {opacity: 1;transform: scale(1);
}
</style>
在layout組件中引入main:src/layout/index.vue
// 右側內容展示組件
import Main from '@/layout/main/index.vue'
?
<!-- 內容展示區域 -->
<div class="layout_main"><Main />
</div>
?3.4 頂部tabbar組件
3.4.1 頂部tabbar組件靜態搭建與拆分?
左側菜單刷新折疊問題解決:src/layout/index.vue
// el-menu中新增default-active屬性
<el-menu background-color="#001529" text-color="white" :default-active="$route.path"><!-- 根據路由動態生成菜單 --><Menu :menuList="userStore.menuRoutes"></Menu>
</el-menu>// 獲取路由對象
import { useRoute } from 'vue-router'
let $route = useRoute()
?tabbar組件封裝:拆分成左側面包屑組件(breadcrumb)和右側設置組件(setting)
面包屑組件:scr/layout/tabbar/breadcrumb/index.vue
<template><!-- 頂部左側靜態 --><el-icon style="margin-right: 10px;"><Expand /></el-icon><!-- 左側面包屑 --><el-breadcrumb separator-icon="ArrowRight"><el-breadcrumb-item>權限管理</el-breadcrumb-item><el-breadcrumb-item>用戶管理</el-breadcrumb-item></el-breadcrumb>
</template><script setup lang="ts"></script><style scoped></style>
設置組件:scr/layout/tabbar/setting/index.vue
<template><el-button size="small" icon="Refresh" circle></el-button><el-button size="small" icon="FullScreen" circle></el-button><el-button size="small" icon="Setting" circle></el-button><img src="/public/logo.png" style="width: 20px;height: 20px;margin: 0 10px;"><!-- 下拉菜單 --><el-dropdown><span class="el-dropdown-link">admin<el-icon class="el-icon--right"><arrow-down /></el-icon></span><template #dropdown><el-dropdown-menu><el-dropdown-item>退出登錄</el-dropdown-item></el-dropdown-menu></template></el-dropdown>
</template><script setup lang="ts"></script><style scoped></style>
?tabbar組件:scr/layout/tabbar/index.vue
<template><div class="tabbar"><div class="tabbar_left"><Breadcrumb /></div><div class="tabbar_right"><Setting /></div></div>
</template><script setup lang="ts">
import Breadcrumb from './breadcrumb/index.vue'
import Setting from './setting/index.vue'
</script><style scoped lang="scss">
.tabbar {width: 100%;height: 100%;display: flex;justify-content: space-between;.tabbar_left {display: flex;margin-left: 20px;align-items: center;}.tabbar_right {display: flex;align-items: center;}
}
</style>
?3.4.2 菜單折疊效果實現
定義控制折疊/展開響應式數據fold:src/store/modules/setting.ts
?因為layout組件和breadcrumb組件都需要用到fold,說定義在倉庫比較合適。
// 小倉庫:layout組件相關配置倉庫
import { defineStore } from 'pinia'const useLayoutSettingStore = defineStore('SettingStore', {state: () => {return {fold: false, // 用戶控制菜單折疊還是收起}}
})export default useLayoutSettingStore
?面包屑組件折疊圖標切換實現:src/layout/tabbar/breadcrumb/index.vue
<template><!-- 頂部左側靜態 --><el-icon style="margin-right: 10px;" @click="changeIcon"><component :is="layoutSettingStore.fold ? 'Expand' : 'Fold'"></component></el-icon>......
</template><script setup lang="ts">
import useLayoutSettingStore from "@/store/modules/setting";
// 獲取layout配置相關的倉庫
let layoutSettingStore = useLayoutSettingStore()
// 點擊圖標的方法
const changeIcon = () => {// 圖標進行切換layoutSettingStore.fold = !layoutSettingStore.fold
}
</script>
layout組件菜單折疊效果實現:src/layout/index.vue
步驟:
- 通過el-menu標簽的collapse屬性配合fold實現菜單折疊/展開效果
- 給左側菜單、頂部導航、右側內容展示區域添加動態類fold實現折疊/展開的布局改變
PS:折疊之后圖標不見的問題:將icon標簽放在title插槽外面
<template><div class="layout_container"><!-- 左側菜單 --><div class="layout_slider" :class="{ fold: layoutSettingStore.fold ? true : false }"><Logo></Logo><!-- 展示菜單 --><!-- 滾動組件 --><el-scrollbar class="scrollbar"><!-- 菜單組件 --><el-menu background-color="#001529" text-color="white" :default-active="$route.path":collapse="layoutSettingStore.fold"><!-- 根據路由動態生成菜單 --><Menu :menuList="userStore.menuRoutes"></Menu></el-menu></el-scrollbar></div><!-- 頂部導航 --><div class="layout_tabbar" :class="{ fold: layoutSettingStore.fold ? true : false }"><Tabbar></Tabbar></div><!-- 內容展示區域 --><div class="layout_main" :class="{ fold: layoutSettingStore.fold ? true : false }"><Main></Main></div></div>
</template><script setup lang="ts">
......
import useLayoutSettingStore from "@/store/modules/setting";
let userStore = useUserStore()
// 獲取layout配置倉庫
let layoutSettingStore = useLayoutSettingStore()
</script><style scoped lang="scss">
.layout_container {......&.fold {width: $base-menu-min-height;}}.layout_tabbar {......&.fold {width: calc(100vw - $base-menu-min-height);left: $base-menu-min-height;}}.layout_main {......&.fold {width: calc(100vw - $base-menu-min-height);left: $base-menu-min-height;}}
}
</style>
3.4.3 頂部面包屑動態展示?
- 通過$route.matched獲取匹配的路由信息實現動態展示。
- 點擊首頁不需要展示layout路由,所以刪除router.ts文件中layout路由中的元信息title和icon的值,并通過v-show判斷是否展示。
- 通過 :to 可使點擊面包屑跳轉匹配路由。
src/layout/tabbar/breadcrumb/index.vue
<!-- 左側面包屑 -->
<el-breadcrumb separator-icon="ArrowRight"><!-- 面包屑動態展示路由圖標與標題 --><el-breadcrumb-item v-for="(item, index) in $route.matched" :key="index" v-show="item.meta.title" :to="item.path"><!-- 圖標 --><el-icon><component :is="item.meta.icon"></component></el-icon><!-- 標題 --><span>{{ item.meta.title }}</span></el-breadcrumb-item>
</el-breadcrumb>// 獲取路由對象
import { useRoute } from 'vue-router'
let $route = useRoute()
?PS:點擊商品管理、權限管理等一級路由的面包屑時,默認跳轉到它的首個二級路由,因此需要在router.ts文件中給商品管理、權限管理的路由添加重定向。
redirect: '/acl/user',
redirect: '/product/trademark',
?3.4.4 刷新業務的實現
刷新業務就是路由組件銷毀和重建的過程。涉及頂部導航組件和內容區域組件通信,因此可以使用store存儲刷新業務相關標識。
小倉庫中添加刷新標識數據:src/store/modules/setting.ts
refresh: false,// 用于控制刷新效果
頂部導航setting組件實現控制下倉庫refresh變化 :src/layout/tabbar/setting/index.vue
// 給刷新按鈕綁定點擊事件
<el-button size="small" icon="Refresh" circle @click="updateRefresh"></el-button>// 獲取倉庫中刷新標識
import useLayoutSettingStore from '@/store/modules/setting'
let layoutSettingStore = useLayoutSettingStore()
// 刷新按鈕點擊回調
const updateRefresh = () => {// 更新刷新標識layoutSettingStore.refresh = !layoutSettingStore.refresh
}
main組件中監聽小倉庫refresh是否變化,控制路由銷毀與重建:src/layout/main/index.vue
<component :is="Component" v-if="flag" />import { watch, ref, nextTick } from 'vue'
import useLayoutSettingStore from '@/store/modules/setting'
let layoutSettingStore = useLayoutSettingStore()
// 控制當前組件是否銷毀重建
let flag = ref(true)
// 監聽倉庫內部數據是否發生變化,如果發生變化,說明用戶點擊過刷新按鈕
watch(() => layoutSettingStore.refresh, () => {// 點擊刷新按鈕:路由組件銷毀flag.value = falsenextTick(() => {flag.value = true})
})
3.4.5 全屏模式的切換
這里利用DOM實現全屏切換(不同瀏覽器可能會有兼容問題),也可以使用插件實現。?
src/layout/tabbar/setting/index.vue?
// 給全屏按鈕綁定點擊事件
<el-button size="small" icon="FullScreen" circle @click="fullScreen"></el-button>// 全屏按鈕點擊回調
const fullScreen = () => {// DOM對象的一個屬性:可以用來判斷當前是不是全屏模式(全屏:true,不是全屏:false)let full = document.fullscreenElement// 切換為全屏模式if (!full) {// 文檔根節點的方法requestFullscreen,實現全屏模式document.documentElement.requestFullscreen()} else {// 變為不是全屏模式 -> 退出全屏模式document.exitFullscreen()}
}
?3.4.6 獲取用戶信息與token理解
發生登錄請求時由后端返回的唯一標識,后續向后端發送各種請求都需要攜帶token,因此token作為每次請求都需帶的公共參數,放在請求攔截器里,通過config配置項hearders攜帶最合適。
src/utils/request.ts
// 引入用戶相關的小倉庫
import useUserStore from '@/store/modules/user';request.interceptors.request.use((config) => {// config配置對象,包括hearders屬性請求頭,經常給服務端攜帶公共參數let useStore = useUserStore()if(useStore.token){config.headers.token = useStore.token}// 返回配置對象return config;
});
home首頁掛載完畢發請求獲取用戶信息:src/views/home/index.vue
import {onMounted} from 'vue'
// 獲取倉庫
import useUserStore from '@/store/modules/user';
let useStore = useUserStore()
// 目前首頁掛載完畢發請求獲取用戶信息
onMounted(() => {useStore.userInfo()
})
?用戶小倉庫:src/store/modules/user.ts
在type.ts文件中定義username、avatar類型:
username: string, avatar: string
// 小倉庫存儲數據的地方state: (): UserState => {return {......username:'',avatar:''}},// 異步|邏輯的地方actions: {......// 獲取用戶信息async userInfo(){// 獲取用戶信息進行存儲倉庫當中(用戶頭像、名字)let result = await reqUserInfo()// 如果獲取信息成功,存儲下用戶信息if(result.code === 200){this.username = result.data.checkUser.usernamethis.avatar = result.data.checkUser.avatar}}},
在setting組件中,通過user小倉庫獲取用戶信息進行展示:src/layout/tabbar/setting/index.vue?
......
<img :src="useStore.avatar" style="width: 20px;height: 20px;margin: 0 10px;border-radius: 50%;"><!-- 下拉菜單 -->
<el-dropdown><span class="el-dropdown-link">{{ useStore.username }}</span>......
</el-dropdown>// 獲取用戶相關的小倉庫
import useUserStore from '@/store/modules/user';
let useStore = useUserStore()
3.4.7 退出登錄業務
退出登錄時,需要做的事情 :
- 需要向服務器發請求(退出登錄接口)
- 倉庫中關于用戶相關的數據清空(token|username|avatar)
- 跳轉到登錄頁面
?src/layout/tabbar/setting/index.vue
<el-dropdown-item @click="logout">退出登錄</el-dropdown-item>// 退出登錄點擊回調
const logout = () => {// 第一件事情:需要向服務器發請求(退出登錄接口)----目前還沒有// 第二件事情:倉庫中關于用戶相關的數據清空(token|username|avatar)useStore.userLogout()// 第三件事情:跳轉到登錄頁面,通過query參數傳遞退出登錄前的路徑$router.push({ path: '/login', query: { redirect: $route.path } })
}
?封裝刪除token本地存儲的方法:src/utils/token.ts?
// 本地存儲刪除數據方法
export const REMOVE_TOKEN = () => {localStorage.removeItem('TOKEN')
}
?用戶小倉庫:src/store/modules/user.ts
// 引入操作本地存儲的工具方法
import { SET_TOKEN, GET_TOKEN, REMOVE_TOKEN } from '@/utils/token'// 退出登錄
userLogout() {// 目前沒有mock接口:退出登錄接口(通知服務器本地用戶唯一標識失敗)this.token = ''this.username = ''this.avatar = ''REMOVE_TOKEN()}
login組件添加登錄前判斷跳轉路由的邏輯:src/views/login/index.vue
import { useRouter, useRoute } from 'vue-router'
// 獲取路由對象
let $route = useRoute()......// 判斷登錄的時候,路由的路徑當中是否有query參數,如果有就往query參數跳轉,沒有就跳轉到首頁
let redirect: any = $route.query.redirect
$router.push({ path: redirect || '/' })
四、路由鑒權和進度條業務?
路由鑒權:?項目中能不能被訪問的權限設置(某一個路由什么條件下可以訪問,什么條件下不可以訪問)。
安裝nprogress插件:pnpm i?nprogress
src/permission.ts?
// 路由鑒權:項目中能不能被訪問的權限設置(某一個路由什么條件下可以訪問,什么條件下不可以訪問)
import router from '@/router'
import setting from '@/setting'
import { SET_TOKEN, GET_TOKEN, REMOVE_TOKEN } from '@/utils/token'
// @ts-ignore
import nprogress from 'nprogress'
// 引入進度條樣式
import "nprogress/nprogress.css"
nprogress.configure({ showSpinner: false })
// 獲取用戶相關的小倉庫內部token數據,去判斷用戶是否登錄成功
import useUserStore from './store/modules/user'
import pinia from './store'
let useStore = useUserStore(pinia)
// 全局守衛:項目中任意路由切換都會觸發的鉤子
// 全局前置守衛
router.beforeEach(async (to: any, from: any, next: any) => {// to:你將要訪問哪個路由// from:你從哪個路由而來// next:路由的放行函數// 進度條開始nprogress.start()// 獲取token,去判斷用戶登錄,還是未登錄let token = useStore.token// 獲取用戶名字let username = useStore.username// 用戶登錄判斷if (token) {// 登錄成功,不能訪問login,指向homeif (to.path == '/login') {next({ path: '/' })} else {// 登錄成功訪問其余六個路由(登錄排除)// 有用戶信息if (username) {// 放行next()} else {// 如果沒有用戶信息,在守衛這里發請求獲取到了用戶信息再放行try {// 獲取用戶信息await useStore.userInfo()// 放行next()} catch (error) {// token過期:獲取不到用戶信息了// 用戶手動修改本地存儲token// 退出登錄->用戶相關的數據清空useStore.userLogout()next({ path: '/login' })}}}} else {// 用戶未登錄判斷if (to.path == '/login') {next()} else {next({ path: '/login', query: { redirect: to.path } })}}
})
// 全局后置守衛
router.afterEach((to: any, from: any) => {document.title = `${setting.title} - ${to.meta.title}`// 進度條結束nprogress.done()
})// 第一個問題:任意路由切換實現進度條業務 ---nprogress
// 第二個問題:路由鑒權(路由組件訪問權限的設置)
// 全部路由組件:登錄|404|任意路由|首頁|數據大屏|權限管理(三個子路由)|商品管理(四個子路由)// 用戶未登錄:可以訪問login,其余六個路由不能訪問(指向login)
// 用戶登錄成功:不可以訪問login(指向首頁)
PS:在組件的外部通過同步的語句獲取倉庫的數據是拿不到的。如果想獲取小倉庫的數據,必須先得有大倉庫(pinia)?。
在入口文件(main.ts)引入鑒權文件
// 引入路由鑒權文件
import './permission'
?五、真實接口替換mock接口和接口ts類型定義
1. 替換各個環境下的服務器地址(?.env.development、.env.production、.env.test?)
2. 配飾代理跨域:vite.config.ts(具體配置參數可參考官網:開發服務器選項 | Vite 官方中文文檔)
export default defineConfig(({ command, mode }) => {// 獲取各種環境下對應的變量let env = loadEnv(mode, process.cwd())return {......// 代理跨域server: {proxy: {[env.VITE_APP_BASE_API]: {// 獲取數據的服務器地址設置target: env.VITE_SERVE,// 是否代理跨域changeOrigin: true,// 路徑重寫rewrite: (path) => path.replace(/^\/api/, ''),}}}}
})
?3. 重新書寫API接口文件及接口類型文件
src/api/user/index.ts
// 統一管理項目用戶相關的接口
import request from "@/utils/request";
import type { loginFormData, loginResponseData, userInfoResponeData } from "./type"
// 項目用戶相關的請求地址
enum API {LOGIN_URL = '/admin/acl/index/login',USERINFO_URL = '/admin/acl/index/info',LOGOUT_URL = '/admin/acl/index/logout',
}// 暴露請求函數
// 登錄接口
export const reqLogin = (data: loginFormData) => request.post<any, loginResponseData>(API.LOGIN_URL, data)
// 獲取用戶信息
export const reqUserInfo = () => request.get<any, userInfoResponeData>(API.USERINFO_URL)
// 退出登錄
export const reqLogout = () => request.post<any, any>(API.LOGOUT_URL)
?src/api/user/index.ts
// 定義用戶相關數據的ts類型
// 用戶登錄接口攜帶參數的ts類型
export interface loginFormData {username: string,password: string
}// 定義全部接口返回數據都擁有的ts類型
export interface ResponseData {code: number,message: string,ok: boolean
}// 定義登錄接口返回數據類型
export interface loginResponseData extends ResponseData {data: string
}// 定義獲取用戶信息返回的數據類型
export interface userInfoResponeData extends ResponseData {data: {routes: string[],buttons: string[],roles: string[],name: string,avatar: string}
}
?4. 修改接口相關的代碼(src/store/modules/user.ts、permission.ts等文件)