【Vue3+Ts項目】硅谷甄選 — 路由配置+登錄模塊+layout組件+路由鑒權

一、路由配置

項目一共需要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的表單驗證功能 ,步驟如下:

  1. 給el-form添加 :model="loginFrom"和:rules="rules"
  2. 給需要驗證的每個el-form-item添加prop屬性,如?prop="username"、prop="password"
  3. 定義表單校驗需要配置對象rules
  4. 請求前使用 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等文件)

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/209306.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/209306.shtml
英文地址,請注明出處:http://en.pswp.cn/news/209306.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Graphpad Prism10.1.0 安裝教程 (含Win/Mac版)

GraphPad Prism GraphPad Prism是一款非常專業強大的科研醫學生物數據處理繪圖軟件&#xff0c;它可以將科學圖形、綜合曲線擬合&#xff08;非線性回歸&#xff09;、可理解的統計數據、數據組織結合在一起&#xff0c;除了最基本的數據統計分析外&#xff0c;還能自動生成統…

Python:核心知識點整理大全8-筆記

目錄 ?編輯 4.5 元組 4.5.1 定義元組 dimensions.py 4.5.2 遍歷元組中的所有值 4.5.3 修改元組變量 4.6 設置代碼格式 4.6.1 格式設置指南 4.6.2 縮進 4.6.3 行長 4.6.4 空行 4.6.5 其他格式設置指南 4.7 小結 第5章 if語句 5.1 一個簡單示例 cars.py 5.2 條…

現代皮質沙發模型材質編輯

在線工具推薦&#xff1a; 3D數字孿生場景編輯器 - GLTF/GLB材質紋理編輯器 - 3D模型在線轉換 - Three.js AI自動紋理開發包 - YOLO 虛幻合成數據生成器 - 三維模型預覽圖生成器 - 3D模型語義搜索引擎 當談到游戲角色的3D模型風格時&#xff0c;有幾種不同的風格&#xf…

線性容器(QByteArray、QString、QList模板類)、堆棧窗體

QT 線性容器 點擊查看&#xff1a;字符和字節的區別&#xff0c;ASCII、Unicode 和 UTF-8 編碼的區別。&#xff08;&#x1f448; 安全鏈接&#xff0c;放心跳轉&#xff09; QByteArray 思考&#xff1a;char buf[6] “hello”; 如果 C 語言中要利用 buf 內容重新生成 “…

學生備考使用臺燈到底好不好?公認好用的護眼臺燈推薦

在現代生活中&#xff0c;許多學生的學習壓力越來越大&#xff0c;面臨的近視幾率也越來越大&#xff0c;特別是初中生&#xff0c;眼睛發育還未完全&#xff0c;使用不恰當的燈光也會對眼睛造成損害&#xff0c;特別是護眼臺燈。雖然護眼臺燈在功能上能夠提供充足、柔和的光線…

harbor倉庫鏡像遷移腳本

import subprocess import json import logging# 配置日志 logging.basicConfig(levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s)# 替換這里的Harbor倉庫地址和憑據 harbor_url "https://harbor.test.com" harbor_name "harbor.test.co…

《文存閱刊》期刊發表簡介

《文存閱刊》以“深研文化創新&#xff0c;崇尚科學真理&#xff0c;堅持雙百方針&#xff0c;打造學術精品”為辦刊宗旨&#xff0c;涵蓋藝術、文學、社科等多項內容&#xff0c;適應了文化市場需求&#xff0c;很好的回應了廣大文化理論工作者的關切&#xff0c;為下一步打造…

ChatGPT新媒體運營神器:輕松駕馭內容創作與傳播

文章目錄 1. 內容創作2. 社交媒體管理3. 用戶互動與客戶服務 《巧用ChatGPT輕松玩轉新媒體運營》內容簡介作者簡介目錄前言/序言本書內容本書特色本書讀者對象獲取方式 隨著互聯網的高速發展&#xff0c;新媒體已經成為了人們獲取信息、交流思想的重要渠道。在這個信息爆炸的時…

【SpringCache】快速入門 通俗易懂

1. 介紹 Spring Cache 是一個框架&#xff0c;實現了基于注解的緩存功能&#xff0c;只需要簡單地加一個注解&#xff0c;就能實現緩存功能。 Spring Cache 提供了一層抽象&#xff0c;底層可以切換不同的緩存實現&#xff0c;例如&#xff1a; EHCache Caffeine Redis(常用…

Centos7、Mysql8.0 load_file函數返回為空的終極解決方法--暨selinux的深入理解

零、問題背景 最近想換房&#xff0c;為了方便自己對比感興趣的房子&#xff0c;因此決定將目標房源的基本信息放在表里&#xff0c;特別是要一目了然的看到眾多房子的各種圖紙和照片&#xff0c;因此決定要在Mysql8.0.34數據庫中以二進制形式保存圖片&#xff08;拋開合理性和…

喝酒誰先倒

劃拳是古老中國酒文化的一個有趣的組成部分。酒桌上兩人劃拳的方法為&#xff1a;每人口中喊出一個數字&#xff0c;同時用手比劃出一個數字。如果誰比劃出的數字正好等于兩人喊出的數字之和&#xff0c;誰就輸了&#xff0c;輸家罰一杯酒。兩人同贏或兩人同輸則繼續下一輪&…

Vue 2.0源碼分析-update

Vue 的 _update 是實例的一個私有方法&#xff0c;它被調用的時機有 2 個&#xff0c;一個是首次渲染&#xff0c;一個是數據更新的時候&#xff1b;由于我們這一章節只分析首次渲染部分&#xff0c;數據更新部分會在之后分析響應式原理的時候涉及。_update 方法的作用是把 VNo…

思維鏈(CoT)提出者 Jason Wei:關于大語言模型的六個直覺

文章目錄 一、前言二、主要內容三、總結 &#x1f349; CSDN 葉庭云&#xff1a;https://yetingyun.blog.csdn.net/ 一、前言 Jason Wei 的主頁&#xff1a;https://www.jasonwei.net/ Jason Wei&#xff0c;一位于 2020 年從達特茅斯學院畢業的杰出青年&#xff0c;隨后加盟了…

大數據安全保障的四種關鍵技術

隨著大數據時代的到來&#xff0c;數據安全保障的重要性日益凸顯。大數據安全保障涉及多種關鍵技術&#xff0c;以下是四種關鍵技術的詳細介紹。 數據加密技術 數據加密技術是大數據安全保障的核心技術之一。它通過將明文數據轉化為密文數據&#xff0c;以保護數據的機密性和完…

CSS中 設置文字下劃線 的幾種方法

在網頁設計和開發中&#xff0c;我們經常需要對文字進行樣式設置&#xff0c;包括字體,顏色&#xff0c;大小等&#xff0c;其中&#xff0c;設置文字下劃線是一種常見需求 一 、CSS種使用 text-decoration 屬性來設置文字的裝飾效果&#xff0c;包括下劃線。 常用的取值&…

Visual Studio 2015 中 FFmpeg 開發環境的搭建

Visual Studio 2015 中 FFmpeg 開發環境的搭建 Visual Studio 2015 中 FFmpeg 開發環境的搭建新建控制臺工程拷貝并配置 FFmpeg 開發文件測試FFmpeg 開發文件的下載鏈接 Visual Studio 2015 中 FFmpeg 開發環境的搭建 新建控制臺工程 新建 Win32 控制臺應用程序。 具體流程&…

炫酷不止一面:探索JavaScript動畫的奇妙世界(下)

&#x1f90d; 前端開發工程師&#xff08;主業&#xff09;、技術博主&#xff08;副業&#xff09;、已過CET6 &#x1f368; 阿珊和她的貓_CSDN個人主頁 &#x1f560; 牛客高級專題作者、在牛客打造高質量專欄《前端面試必備》 &#x1f35a; 藍橋云課簽約作者、已在藍橋云…

proftpd安全加固:限制用戶FTP登錄

其實無所謂安全加固&#xff0c;因為proftp默認就是限制用戶FTP登錄的&#xff0c;這里有點凌亂得研究和實驗了proftpd如何進行限制的&#xff0c;以及可能的放開限制。懂了這些才能更好的進行防護配置。 RootLogin指令其實主要作用就是啟用ROOT訪問。通常&#xff0c;proftpd在…

【Fastadmin】一個完整的輪播圖功能示例

目錄 1.效果展示&#xff1a; 列表 添加及編輯頁面同 2.建表&#xff1a; 3.使用crud一鍵生成并創建控制器 4.html頁面 add.html edit.html index.php 5.js頁面 6.小知識點 1.效果展示&#xff1a; 列表 添加及編輯頁面同 2.建表&#xff1a; 表名&#xff1a;fa_x…

【LabVIEW學習】5.數據通信之TCP協議,控制電腦的一種方式

一。tcp連接以及寫數據&#xff08;登錄&#xff09; 數據通信--》協議--》TCP 1.tcp連接 創建while循環&#xff0c;中間加入事件結構&#xff0c;創建tcp連接&#xff0c;寫入IP地址與端口號 2.寫入tcp數據 登錄服務器除了要知道IP地址以及端口以外&#xff0c;需要用戶名與密…