1.路由配置
1.1路由元件的雛形
src\views\home\index.vue
(以home元件為例)
1.2路由配置
1.2.1路由index檔案
src\router\index.ts
//透過vue-router外掛實現模板路由配置
import { createRouter, createWebHashHistory } from 'vue-router'
import { constantRoute } from './router'
//建立路由器
const router = createRouter({
//路由模式hash
history: createWebHashHistory(),
routes: constantRoute,
//滾動行為
scrollBehavior() {
return {
left: 0,
top: 0,
}
},
})
export default router
1.2.2路由配置
src\router\router.ts
//對外暴露配置路由(常量路由)
export const constantRoute = [
{
//登入路由
path: '/login',
component: () => import('@/views/login/index.vue'),
name: 'login', //命名路由
},
{
//登入成功以後展示資料的路由
path: '/',
component: () => import('@/views/home/index.vue'),
name: 'layout',
},
{
path: '/404',
component: () => import('@/views/404/index.vue'),
name: '404',
},
{
//重定向
path: '/:pathMatch(.*)*',
redirect: '/404',
name: 'Any',
},
]
1.3路由出口
src\App.vue
2.登入模組
2.1 登入路由靜態元件
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_form">
<h1>Hello</h1>
<h2>歡迎來到矽谷甄選</h2>
<el-form-item>
<el-input
:prefix-icon="User"
v-model="loginForm.username"
></el-input>
</el-form-item>
<el-form-item>
<el-input
type="password"
:prefix-icon="Lock"
v-model="loginForm.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 loginForm = reactive({ username: 'admin', password: '111111' })
</script>
<style lang="scss" scoped>
.login_container {
width: 100%;
height: 100vh;
background: url('@/assets/images/background.jpg') no-repeat;
background-size: cover;
.login_form {
position: relative;
width: 80%;
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 0px;
}
.login_btn {
width: 100%;
}
}
}
</style>
注意:el-col是24份的,在此左右分為了12份。我們在右邊放置我們的結構。:xs="0"
是為了響應式。el-form
下的element-plus元素都用el-form-item
包裹起來。
2.2 登陸業務實現
2.2.1 登入按鈕繫結回撥
回撥應該做的事情
const login = () => {
//點選登入按鈕以後幹什麼
//通知倉庫發起請求
//請求成功->路由跳轉
//請求失敗->彈出登陸失敗資訊
}
2.2.2 倉庫store初始化
- 大倉庫(筆記只寫一次)
安裝pinia:pnpm i pinia@2.0.34
src\store\index.ts
//倉庫大倉庫
import { createPinia } from 'pinia'
//建立大倉庫
const pinia = createPinia()
//對外暴露:入口檔案需要安裝倉庫
export default pinia
- 使用者相關的小倉庫
src\store\modules\user.ts
//建立使用者相關的小倉庫
import { defineStore } from 'pinia'
//建立使用者小倉庫
const useUserStore = defineStore('User', {
//小倉庫儲存資料地方
state: () => {},
//處理非同步|邏輯地方
actions: {},
getters: {},
})
//對外暴露小倉庫
export default useUserStore
2.2.3 按鈕回撥
//登入按鈕的回撥
const login = async () => {
//按鈕載入效果
loading.value = true
//點選登入按鈕以後幹什麼
//通知倉庫發起請求
//請求成功->路由跳轉
//請求失敗->彈出登陸失敗資訊
try {
//也可以書寫.then語法
await useStore.userLogin(loginForm)
//程式設計式導航跳轉到展示資料的首頁
$router.push('/')
//登入成功的提示資訊
ElNotification({
type: 'success',
message: '登入成功!',
})
//登入成功,載入效果也消失
loading.value = false
} catch (error) {
//登陸失敗載入效果消失
loading.value = false
//登入失敗的提示資訊
ElNotification({
type: 'error',
message: (error as Error).message,
})
}
}
2.2.4 使用者倉庫
//建立使用者相關的小倉庫
import { defineStore } from 'pinia'
//引入介面
import { reqLogin } from '@/api/user'
//引入資料型別
import type { loginForm } from '@/api/user/type'
//建立使用者小倉庫
const useUserStore = defineStore('User', {
//小倉庫儲存資料地方
state: () => {
return {
token: localStorage.getItem('TOKEN'), //使用者唯一標識token
}
},
//處理非同步|邏輯地方
actions: {
//使用者登入的方法
async userLogin(data: loginForm) {
//登入請求
const result: any = await reqLogin(data)
if (result.code == 200) {
//pinia倉庫儲存token
//由於pinia|vuex儲存資料其實利用js物件
this.token = result.data.token
//本地儲存持久化儲存一份
localStorage.setItem('TOKEN', result.data.token)
//保證當前async函式返回一個成功的promise函式
return 'ok'
} else {
return Promise.reject(new Error(result.data.message))
}
},
},
getters: {},
})
//對外暴露小倉庫
export default useUserStore
2.2.5 小結
- Element-plus中ElNotification用法(彈窗):
引入:import { ElNotification } from 'element-plus'
使用:
//登入失敗的提示資訊
ElNotification({
type: 'error',
message: (error as Error).message,
})
- Element-plus中el-button的loading屬性。
- pinia使用actions、state的方式和vuex不同:需要引入函式和建立例項
- $router的使用:也需要引入函式和建立例項
- 在actions中使用state的token資料:this.token
- 型別定義需要注意。
- promise的使用和vue2現在看來是一樣的。
2.3模板封裝登陸業務
2.3.1 result返回型別封裝
interface dataType {
token?: string
message?: string
}
//登入介面返回的資料型別
export interface loginResponseData {
code: number
data: dataType
}
2.3.2 State倉庫型別封裝
//定義小倉庫資料state型別
export interface UserState {
token: string | null
}
2.3.3 本地儲存封裝
將本地儲存的方法封裝到一起
//封裝本地儲存儲存資料與讀取資料方法
export const SET_TOKEN = (token: string) => {
localStorage.setItem('TOKEN', token)
}
export const GET_TOKEN = () => {
return localStorage.getItem('TOKEN')
}
2.4 登入時間的判斷
- 封裝函式
//封裝函式:獲取當前時間段
export const getTime = () => {
let message = ''
//透過內建建構函式Date
const hour = new Date().getHours()
if (hour <= 9) {
message = '早上'
} else if (hour <= 14) {
message = '上午'
} else if (hour <= 18) {
message = '下午'
} else {
message = '晚上'
}
return message
}
- 使用(引入後)
- 效果
2.5 表單校驗規則
2.5.1 表單校驗
- 表單繫結項
:model:繫結的資料
//收集賬號與密碼資料
let loginForm = reactive({ username: 'admin', password: '111111' })
:rules:對應要使用的規則
//定義表單校驗需要的配置物件
const rules = {}
ref="loginForms":獲取表單元素
//獲取表單元素
let loginForms = ref()
- 表單元素繫結項
Form 元件提供了表單驗證的功能,只需為 rules 屬性傳入約定的驗證規則,並將 form-Item 的 prop 屬性設定為需要驗證的特殊鍵值即可
- 使用規則rules
//定義表單校驗需要的配置物件
const rules = {
username: [
//規則物件屬性:
{
required: true, // required,代表這個欄位務必要校驗的
min: 5, //min:文字長度至少多少位
max: 10, // max:文字長度最多多少位
message: '長度應為6-10位', // message:錯誤的提示資訊
trigger: 'change', //trigger:觸發校驗表單的時機 change->文字發生變化觸發校驗, blur:失去焦點的時候觸發校驗規則
},
],
password: [
{
required: true,
min: 6,
max: 10,
message: '長度應為6-15位',
trigger: 'change',
},
],
}
- 校驗規則透過後執行
const login = async () => {
//保證全部表單項校驗透過
await loginForms.value.validate()
。。。。。。
}
2.5.2自定義表單校驗
- 修改使用規則rules
使用自己編寫的函式作為規則校驗。
//定義表單校驗需要的配置物件
const rules = {
username: [
//規則物件屬性:
/* {
required: true, // required,代表這個欄位務必要校驗的
min: 5, //min:文字長度至少多少位
max: 10, // max:文字長度最多多少位
message: '長度應為6-10位', // message:錯誤的提示資訊
trigger: 'change', //trigger:觸發校驗表單的時機 change->文字發生變化觸發校驗, blur:失去焦點的時候觸發校驗規則
}, */
{ trigger: 'change', validator: validatorUserName },
],
password: [
{ trigger: 'change', validator: validatorPassword },
],
}
- 自定義校驗規則函式
//自定義校驗規則函式
const validatorUserName = (rule: any, value: any, callback: any) => {
//rule:校驗規則物件
//value:表單元素文字內容
//callback:符合條件,callback放行透過,不符合:注入錯誤提示資訊
if (value.length >= 5) {
callback()
} else {
callback(new Error('賬號長度至少5位'))
}
}
const validatorPassword = (rule: any, value: any, callback: any) => {
if (value.length >= 6) {
callback()
} else {
callback(new Error('密碼長度至少6位'))
}
}
3. Layout模組(主介面)
3.1 元件的靜態頁面
3.1.1 元件的靜態頁面
注意:我們將主介面單獨放一個資料夾(頂替原來的home路由元件)。注意修改一下路由配置
<template>
<div class="layout_container">
<!-- 左側選單 -->
<div class="layout_slider"></div>
<!-- 頂部導航 -->
<div class="layout_tabbar"></div>
<!-- 內容展示區域 -->
<div class="layout_main">
<p style="height: 1000000px"></p>
</div>
</div>
</template>
<script setup lang="ts"></script>
<style lang="scss" scoped>
.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: cyan;
top: 0;
left: $base-menu-width;
}
.layout_main {
position: absolute;
width: calc(100% - $base-menu-width);
height: calc(100vh - $base-tabbar-height);
background-color: yellowgreen;
left: $base-menu-width;
top: $base-tabbar-height;
padding: 20px;
overflow: auto;
}
}
</style>
3.1.2定義部分全域性變數&捲軸
scss全域性變數
//左側選單寬度
$base-menu-width :260px;
//左側選單背景顏色
$base-menu-background: #001529;
//頂部導航的高度
$base-tabbar-height:50px;
捲軸
//捲軸外觀設定
::-webkit-scrollbar{
width: 10px;
}
::-webkit-scrollbar-track{
background: $base-menu-background;
}
::-webkit-scrollbar-thumb{
width: 10px;
background-color: yellowgreen;
border-radius: 10px;
}
3.2 Logo子元件的搭建
頁面左上角的這部分,我們將它做成子元件,並且封裝方便維護以及修改。
3.2.1 Logo子元件
在這裡我們引用了封裝好的setting
<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 lang="scss" scoped>
.logo {
width: 100%;
height: $base-menu-logo-height;
color: white;
display: flex;
align-items: center;
padding: 20px;
img {
width: 40px;
height: 40px;
}
p {
font-size: $base-logo-title-fontSize;
margin-left: 10px;
}
}
</style>
3.2.2 封裝setting
為了方便我們以後對logo以及標題的修改。
//用於專案logo|標題配置
export default {
title: '矽谷甄選運營平臺', //專案的標題
logo: '/public/logo.png', //專案logo設定
logoHidden: true, //logo元件是否隱藏
}
3.2.3 使用
在layout元件中引入並使用
3.3 左側選單元件
3.3.1靜態頁面(未封裝)
主要使用到了element-plus的menu元件。附帶使用了滾動元件
<!-- 左側選單 -->
<div class="layout_slider">
<Logo></Logo>
<!-- 展示選單 -->
<!-- 滾動元件 -->
<el-scrollbar class="scrollbar">
<!-- 選單元件 -->
<el-menu background-color="#001529" text-color="white">
<el-menu-item index="1">首頁</el-menu-item>
<el-menu-item index="2">資料大屏</el-menu-item>
<!-- 摺疊選單 -->
<el-sub-menu index="3">
<template #title>
<span>許可權管理</span>
</template>
<el-menu-item index="3-1">使用者管理</el-menu-item>
<el-menu-item index="3-2">角色管理</el-menu-item>
<el-menu-item index="3-3">選單管理</el-menu-item>
</el-sub-menu>
</el-menu>
</el-scrollbar>
</div>
3.3.2 遞迴元件生成動態選單
在這一部分,我們要根據路由生成左側的選單欄
- 將父元件中寫好的子元件結構提取出去
<!-- 展示選單 -->
<!-- 滾動元件 -->
<el-scrollbar class="scrollbar">
<!-- 選單元件 -->
<el-menu background-color="#001529" text-color="white">
<!-- 更具路由動態生成選單 -->
<Menu></Menu>
</el-menu>
</el-scrollbar>
- 動態選單子元件:src\layout\menu\index.vue
- 處理路由
因為我們要根據路由以及其子路由作為我們選單的一級|二級標題。因此我們要獲取路由資訊。
給路由中加入了路由元資訊meta:它包含了2個屬性:title以及hidden
{
//登入路由
path: '/login',
component: () => import('@/views/login/index.vue'),
name: 'login', //命名路由
meta: {
title: '登入', //選單標題
hidden: true, //路由的標題在選單中是否隱藏
},
},
- 倉庫引入路由並對路由資訊型別宣告(vue-router有對應函式)
//引入路由(常量路由)
import { constantRoute } from '@/router/routes'
。。。。。
//小倉庫儲存資料地方
state: (): UserState => {
return {
token: GET_TOKEN(), //使用者唯一標識token
menuRoutes: constantRoute, //倉庫儲存生成選單需要陣列(路由)
}
- 父元件拿到倉庫路由資訊並傳遞給子元件
<script setup lang="ts">
。。。。。。
//引入選單元件
import Menu from './menu/index.vue'
//獲取使用者相關的小倉庫
import useUserStore from '@/store/modules/user'
let userStore = useUserStore()
</script>
- 子元件prps接收並且處理結構
<template>
<template v-for="(item, index) in menuList" :key="item.path">
<!-- 沒有子路由 -->
<template v-if="!item.children">
<el-menu-item v-if="!item.meta.hidden" :index="item.path">
<template #title>
<span>標</span>
<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"
>
<template #title>
<span>標</span>
<span>{{ item.children[0].meta.title }}</span>
</template>
</el-menu-item>
</template>
<!-- 有子路由且個數大於一個 -->
<el-sub-menu
:index="item.path"
v-if="item.children && item.children.length >= 2"
>
<template #title>
<span>{{ item.meta.title }}</span>
</template>
<Menu :menuList="item.children"></Menu>
</el-sub-menu>
</template>
</template>
<script setup lang="ts">
//獲取父元件傳遞過來的全部路由陣列
defineProps(['menuList'])
</script>
<script lang="ts">
export default {
name: 'Menu',
}
</script>
<style lang="scss" scoped></style>
注意:
1:因為每一個項我們要判斷倆次(是否要隱藏,以及子元件個數),所以在el-menu-item外面又套了一層模板
2:當子路由個數大於等於一個時,並且或許子路由還有後代路由時。這裡我們使用了遞迴元件。遞迴元件需要命名(另外使用一個script標籤,vue2格式)。
3.3.3 選單圖示
- 註冊圖示元件
因為我們要根據路由配置對應的圖示,也要為了後續方便更改。因此我們將所有的圖示註冊為全域性元件。(使用之前將分頁器以及向量圖註冊全域性元件的自定義外掛)(所有圖示全域性註冊的方法element-plus文件中已給出)
。。。。。。
//引入element-plus提供全部圖示元件
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
。。。。。。
//對外暴露外掛物件
export default {
//必須叫做install方法
//會接收我們的app
。。。。。。
//將element-plus提供全部圖示註冊為全域性元件
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
},
}
- 給路由元資訊新增屬性:icon
以laytou和其子元件為例:首先在element-puls找到你要使用的圖示的名字。將它新增到路由元資訊的icon屬性上
{
//登入成功以後展示資料的路由
path: '/',
component: () => import('@/layout/index.vue'),
name: 'layout',
meta: {
title: 'layout',
hidden: false,
icon: 'Avatar',
},
children: [
{
path: '/home',
component: () => import('@/views/home/index.vue'),
meta: {
title: '首頁',
hidden: false,
icon: 'HomeFilled',
},
},
],
},
- 選單元件使用
以只有一個子路由的元件為例:
<!-- 有且只有一個子路由 -->
<template v-if="item.children && item.children.length == 1">
<el-menu-item
index="item.children[0].path"
v-if="!item.children[0].meta.hidden"
>
<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>
3.3.4 專案全部路由配置
- 全部路由配置(以許可權管理為例)
{
path: '/acl',
component: () => import('@/layout/index.vue'),
name: 'Acl',
meta: {
hidden: false,
title: '許可權管理',
icon: 'Lock',
},
children: [
{
path: '/acl/user',
component: () => import('@/views/acl/user/index.vue'),
name: 'User',
meta: {
hidden: false,
title: '使用者管理',
icon: 'User',
},
},
{
path: '/acl/role',
component: () => import('@/views/acl/role/index.vue'),
name: 'Role',
meta: {
hidden: false,
title: '角色管理',
icon: 'UserFilled',
},
},
{
path: '/acl/permission',
component: () => import('@/views/acl/permission/index.vue'),
name: 'Permission',
meta: {
hidden: false,
title: '選單管理',
icon: 'Monitor',
},
},
],
},
- 新增路由跳轉函式
第三種情況我們使用元件遞迴,所以只需要給前面的2個新增函式
<script setup lang="ts">
。。。。。。
//獲取路由器物件
let $router = useRouter()
const goRoute = (vc: any) => {
//路由跳轉
$router.push(vc.index)
}
</script>
- layout元件
3.3.5 Bug&&總結
在這部分對router-link遇到一些bug,理解也更深了,特意寫一個小結總結一下
bug:router-link不生效。
描述:當我點選跳轉函式的時候,直接跳轉到一個新頁面,而不是layout元件展示的部分更新。
思路:首先輸出了一下路徑,發現路徑沒有錯。其次,因為跳轉到新頁面,代表layout元件中的router-link不生效,刪除router-link,發現沒有影響。所以確定了是router-link沒有生效。
解決:仔細檢查了src\router\routes.ts
檔案,最後發現一級路由的component關鍵字寫錯。導致下面的二級路由沒有和以及路由構成父子關係。所以會跳轉到APP元件下的router-link
總結:router-link會根據下面的子路由來進行展示。如果發生了路由跳轉不對的情況,去仔細檢查一下路由關係有沒有寫對。APP是所有一級路由元件的父元件
3.3.6 動畫 && 自動展示
- 將router-link封裝成單獨的檔案並且新增一些動畫
<template>
<!-- 路由元件出口的位置 -->
<router-view v-slot="{ Component }">
<transition name="fade">
<!-- 渲染layout一級路由的子路由 -->
<component :is="Component" />
</transition>
</router-view>
</template>
<script setup lang="ts"></script>
<style lang="scss" scoped>
.fade-enter-from {
opacity: 0;
}
.fade-enter-active {
transition: all 0.3s;
}
.fade-enter-to {
opacity: 1;
}
</style>
- 自動展示
當頁面重新整理時,選單會自動收起。我們使用element-plus的**default-active **處理。$router.path為當前路由。
src\layout\index.vue
3.4 頂部tabbar元件
3.4.1靜態頁面
element-plus:breadcrumb el-button el-dropdown
<template>
<div class="tabbar">
<div class="tabbar_left">
<!-- 頂部左側的圖示 -->
<el-icon style="margin-right: 10px">
<Expand></Expand>
</el-icon>
<!-- 左側的麵包屑 -->
<el-breadcrumb separator-icon="ArrowRight">
<el-breadcrumb-item>許可權掛曆</el-breadcrumb-item>
<el-breadcrumb-item>使用者管理</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="tabbar_right">
<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: 24px; height: 24px; margin: 0px 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>
</div>
</div>
</template>
<script setup lang="ts"></script>
<style lang="scss" scoped>
.tabbar {
width: 100%;
height: 100%;
display: flex;
justify-content: space-between;
background-image: linear-gradient(
to right,
rgb(236, 229, 229),
rgb(151, 136, 136),
rgb(240, 234, 234)
);
.tabbar_left {
display: flex;
align-items: center;
margin-left: 20px;
}
.tabbar_right {
display: flex;
align-items: center;
}
}
</style>
元件拆分:
<template>
<!-- 頂部左側的圖示 -->
<el-icon style="margin-right: 10px">
<Expand></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 lang="scss" scoped></style>
<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: 24px; height: 24px; margin: 0px 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 lang="scss" scoped></style>
3.4.2 選單摺疊
- 摺疊變數
定義一個摺疊變數來判斷現在的狀態是否摺疊。因為這個變數同時給breadcrumb元件以及父元件layout使用,因此將這個變數定義在pinia中
//小倉庫:layout元件相關配置倉庫
import { defineStore } from 'pinia'
let useLayOutSettingStore = defineStore('SettingStore', {
state: () => {
return {
fold: false, //使用者控制選單摺疊還是收起的控制
}
},
})
export default useLayOutSettingStore
- 麵包屑元件點選圖示切換狀態
<template>
<!-- 頂部左側的圖示 -->
<el-icon style="margin-right: 10px" @click="changeIcon">
<component :is="LayOutSettingStore.fold ? 'Fold' : 'Expand'"></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元件根據fold狀態來修改個子元件的樣式(以左側選單為例)
繫結動態樣式修改scss
- 左側選單使用element-plus摺疊collapse屬性
效果圖:
注意:摺疊文字的時候會把圖示也摺疊起來。在menu元件中吧圖示放到template外面就可以。
3.4.3 頂部麵包屑動態展示
- 引入$route
注意$router和$route是不一樣的
<script setup lang="ts">
import { useRoute } from 'vue-router'
//獲取路由物件
let $route = useRoute()
//點選圖示的切換
</script>
- 結構展示
注意:使用了$route.matched函式,此函式能得到當前路由的資訊
- 首頁修改
訪問首頁時,因為它是二級路由,會遍歷出layout麵包屑,處理:刪除layout路由的title。再加上一個判斷
- 麵包屑點選跳轉
注意:將路由中的一級路由許可權管理以及商品管理重定向到第一個孩子,這樣點選跳轉的時候會定向到第一個孩子。
3.4.4 重新整理業務的實現
- 使用pinia定義一個變數作為標記
- 點選重新整理按鈕,修改標記
<script setup lang="ts">
//使用layout的小倉庫
import useLayOutSettingStore from '@/store/modules/setting'
let layoutSettingStore = useLayOutSettingStore()
//重新整理按鈕點選的回撥
const updateRefresh = () => {
layoutSettingStore.refresh = !layoutSettingStore.refresh
}
</script>
- main元件檢測標記銷燬&重載入元件(nextTick)
<script setup lang="ts">
import { watch, ref, nextTick } from 'vue'
//使用layout的小倉庫
import useLayOutSettingStore from '@/store/modules/setting'
let layOutSettingStore = useLayOutSettingStore()
//控制當前元件是否銷燬重建
let flag = ref(true)
//監聽倉庫內部的資料是否發生變化,如果發生變化,說明使用者點選過重新整理按鈕
watch(
() => layOutSettingStore.refresh,
() => {
//點選重新整理按鈕:路由元件銷燬
flag.value = false
nextTick(() => {
flag.value = true
})
},
)
</script>
3.4.5 全屏模式的實現
- 給全屏按鈕繫結函式
- 實現全屏效果(利用docment根節點的方法)
//全屏按鈕點選的回撥
const fullScreen = () => {
//DOM物件的一個屬性:可以用來判斷當前是不是全屏的模式【全屏:true,不是全屏:false】
let full = document.fullscreenElement
//切換成全屏
if (!full) {
//文件根節點的方法requestFullscreen實現全屏
document.documentElement.requestFullscreen()
} else {
//退出全屏
document.exitFullscreen()
}
4.部分功能處理完善
登入這一塊大概邏輯,前端傳送使用者名稱密碼到後端,後端返回token,前端儲存,並且請求攔截器,請求頭有token就要攜帶token
4.1 登入獲取使用者資訊(TOKEN)
登入之後頁面(home)上來就要獲取使用者資訊。並且將它使用到頁面中
- home元件掛載獲取使用者資訊
<script setup lang="ts">
//引入組合是API生命週期函式
import { onMounted } from 'vue'
import useUserStore from '@/store/modules/user'
let userStore = useUserStore()
onMounted(() => {
userStore.userInfo()
})
</script>
- 小倉庫中定義使用者資訊以及type宣告
import type { RouteRecordRaw } from 'vue-router'
//定義小倉庫資料state型別
export interface UserState {
token: string | null
menuRoutes: RouteRecordRaw[]
username: string
avatar: string
}
- 請求頭新增TOKEN
//引入使用者相關的倉庫
import useUserStore from '@/store/modules/user'
。。。。。。
//請求攔截器
request.interceptors.request.use((config) => {
//獲取使用者相關的小倉庫,獲取token,登入成功以後攜帶個i伺服器
const userStore = useUserStore()
if (userStore.token) {
config.headers.token = userStore.token
}
//config配置物件,headers請求頭,經常給伺服器端攜帶公共引數
//返回配置物件
return config
})
- 小倉庫發請求並且拿到使用者資訊
//獲取使用者資訊方法
async userInfo() {
//獲取使用者資訊進行儲存
let result = await reqUserInfo()
if (result.code == 200) {
this.username = result.data.checkUser.username
this.avatar = result.data.checkUser.avatar
}
},
- 更新tabbar的資訊(記得先引入並建立例項)
src\layout\tabbar\setting\index.vue
4.2 退出功能
- 退出登入繫結函式,呼叫倉庫函式
//退出登陸點選的回撥
const logout = () => {
//第一件事:需要項伺服器發請求【退出登入介面】(我們這裡沒有)
//第二件事:倉庫當中和關於使用者的相關的資料清空
userStore.userLogout()
//第三件事:跳轉到登陸頁面
}
- pinia倉庫
//退出登入
userLogout() {
//當前沒有mock介面(不做):伺服器資料token失效
//本地資料清空
this.token = ''
this.username = ''
this.avatar = ''
REMOVE_TOKEN()
},
- 退出登入,路由跳轉
注意:攜帶的query引數方便下次登陸時直接跳轉到當時推出的介面
個人覺得這個功能沒什麼作用。但是可以學習方法
//退出登陸點選的回撥
const logout = () => {
//第一件事:需要項伺服器發請求【退出登入介面】(我們這裡沒有)
//第二件事:倉庫當中和關於使用者的相關的資料清空
userStore.userLogout()
//第三件事:跳轉到登陸頁面
$router.push({ path: '/login', query: { redirect: $route.path } })
}
- 登入按鈕進行判斷
4.3 路由守衛
src\permisstion.ts
(新建檔案)
main.ts引入
4.3.1 進度條
- 安裝
pnpm i nprogress
- 引入並使用
//路由鑑權:鑑權:專案當中路由能不能被訪問的許可權
import router from '@/router'
import nprogress from 'nprogress'
//引入進度條樣式
import 'nprogress/nprogress.css'
//全域性前置守衛
router.beforeEach((to: any, from: any, next: any) => {
//訪問某一個路由之前的守衛
nprogress.start()
next()
})
//全域性後置守衛
router.afterEach((to: any, from: any) => {
// to and from are both route objects.
nprogress.done()
})
//第一個問題:任意路由切換實現進度條業務 ----nprogress
4.3.2 路由鑑權
//路由鑑權:鑑權:專案當中路由能不能被訪問的許可權
import router from '@/router'
import setting from './setting'
import nprogress from 'nprogress'
//引入進度條樣式
import 'nprogress/nprogress.css'
//進度條的載入圓圈不要
nprogress.configure({ showSpinner: false })
//獲取使用者相關的小倉庫內部token資料,去判斷使用者是否登陸成功
import useUserStore from './store/modules/user'
//為什麼要引pinia
import pinia from './store'
const userStore = useUserStore(pinia)
//全域性前置守衛
router.beforeEach(async (to: any, from: any, next: any) => {
//網頁的名字
document.title = `${setting.title}-${to.meta.title}`
//訪問某一個路由之前的守衛
nprogress.start()
//獲取token,去判斷使用者登入、還是未登入
const token = userStore.token
//獲取使用者名稱字
let username = userStore.username
//使用者登入判斷
if (token) {
//登陸成功,訪問login。指向首頁
if (to.path == '/login') {
next('/home')
} else {
//登陸成功訪問其餘的,放行
//有使用者資訊
if (username) {
//放行
next()
} else {
//如果沒有使用者資訊,在收尾這裡發請求獲取到了使用者資訊再放行
try {
//獲取使用者資訊
await userStore.userInfo()
next()
} catch (error) {
//token過期|使用者手動處理token
//退出登陸->使用者相關的資料清空
userStore.userLogout()
next({ path: '/login', query: { redirect: to.path } })
}
}
}
} else {
//使用者未登入
if (to.path == '/login') {
next()
} else {
next({ path: '/login', query: { redirect: to.path } })
}
}
next()
})
//全域性後置守衛
router.afterEach((to: any, from: any) => {
// to and from are both route objects.
nprogress.done()
})
//第一個問題:任意路由切換實現進度條業務 ----nprogress
//第二個問題:路由鑑權
//全部路由元件 :登入|404|任意路由|首頁|資料大屏|許可權管理(三個子路由)|商品管理(4個子路由)
//使用者未登入 :可以訪問login 其餘都不行
//登陸成功:不可以訪問login 其餘都可以
路由鑑權幾個注意點:
- 獲取使用者小倉庫為什麼要匯入pinia?
個人理解:之前在app中是不需要匯入pinia的,是因為我們這次的檔案時寫在和main.ts同級的下面,所以我們使用的時候是沒有pinia的。而之前使用時app已經使用了pinia了,所以我們不需要匯入pina。
- 全域性路由守衛將獲取使用者資訊的請求放在了跳轉之前。實現了重新整理後使用者資訊丟失的功能。
4.4 真實介面替代mock介面
介面文件:
http://139.198.104.58:8209/swagger-ui.html
http://139.198.104.58:8212/swagger-ui.html#/
- 修改伺服器域名
將.env.development,.env.production .env.test,三個環境檔案下的伺服器域名寫為:
- 代理跨域
import { loadEnv } from '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/, ''),
},
},
},
}
})
- 修改api
在這裡退出登入有了自己的api
//統一管理專案使用者相關的介面
import request from '@/utils/request'
//專案使用者相關的請求地址
enum API {
LOGIN_URL = '/admin/acl/index/login',
USERINFO_URL = '/admin/acl/index/info',
LOGOUT_URL = '/admin/acl/index/logout',
}
//對外暴露請求函式
//登入介面方法
export const reqLogin = (data: any) => {
return request.post<any, any>(API.LOGIN_URL, data)
}
//獲取使用者資訊介面方法
export const reqUserInfo = () => {
return request.get<any, any>(API.USERINFO_URL)
}
//退出登入
export const reqLogout = () => {
return request.post<any, any>(API.LOGOUT_URL)
}
- 小倉庫(user)
替換原有的請求介面函式,以及修改退出登入函式。以及之前引入的型別顯示我們展示都設定為any
//建立使用者相關的小倉庫
import { defineStore } from 'pinia'
//引入介面
import { reqLogin, reqUserInfo, reqLogout } from '@/api/user'
import type { UserState } from './types/type'
//引入操作本地儲存的工具方法
import { SET_TOKEN, GET_TOKEN, REMOVE_TOKEN } from '@/utils/token'
//引入路由(常量路由)
import { constantRoute } from '@/router/routes'
//建立使用者小倉庫
const useUserStore = defineStore('User', {
//小倉庫儲存資料地方
state: (): UserState => {
return {
token: GET_TOKEN(), //使用者唯一標識token
menuRoutes: constantRoute, //倉庫儲存生成選單需要陣列(路由)
username: '',
avatar: '',
}
},
//處理非同步|邏輯地方
actions: {
//使用者登入的方法
async userLogin(data: any) {
//登入請求
const result: any = await reqLogin(data)
if (result.code == 200) {
//pinia倉庫儲存token
//由於pinia|vuex儲存資料其實利用js物件
this.token = result.data as string
//本地儲存持久化儲存一份
SET_TOKEN(result.data as string)
//保證當前async函式返回一個成功的promise函式
return 'ok'
} else {
return Promise.reject(new Error(result.data))
}
},
//獲取使用者資訊方法
async userInfo() {
//獲取使用者資訊進行儲存
const result = await reqUserInfo()
console.log(result)
if (result.code == 200) {
this.username = result.data.name
this.avatar = result.data.avatar
return 'ok'
} else {
return Promise.reject(new Error(result.message))
}
},
//退出登入
async userLogout() {
const result = await reqLogout()
if (result.code == 200) {
//本地資料清空
this.token = ''
this.username = ''
this.avatar = ''
REMOVE_TOKEN()
return 'ok'
} else {
return Promise.reject(new Error(result.message))
}
},
},
getters: {},
})
//對外暴露小倉庫
export default useUserStore
- 退出登入按鈕的點選函式修改
退出成功後再跳轉
- 路由跳轉判斷條件修改
src\permisstion.ts
也是退出成功後再跳轉
4.5 介面型別定義
//登入介面需要攜帶引數型別
export interface loginFormData {
username: string
password: string
}
//定義全部介面返回資料都有的資料型別
export interface ResponseData {
code: number
message: string
ok: boolean
}
//定義登入介面返回資料型別
export interface loginResponseData extends ResponseData {
data: string
}
//定義獲取使用者資訊返回的資料型別
export interface userInfoResponseData extends ResponseData {
data: {
routes: string[]
button: string[]
roles: string[]
name: string
avatar: string
}
}
注意:在src\store\modules\user.ts以及src\api\user\index.ts檔案中對發請求時的引數以及返回的資料新增型別定義
5.品牌管理模組
5.1 靜態元件
使用element-plus。
<template>
<el-card class="box-card">
<!-- 卡片頂部新增品牌按鈕 -->
<el-button type="primary" size="default" icon="Plus">新增品牌</el-button>
<!-- 表格元件,用於展示已有的資料 -->
<!--
table
---border:是否有縱向的邊框
table-column
---lable:某一個列表
---width:設定這一列的寬度
---align:設定這一列對齊方式
-->
<el-table style="margin: 10px 0px" border>
<el-table-column
label="序號"
width="80px"
align="center"
></el-table-column>
<el-table-column label="品牌名稱"></el-table-column>
<el-table-column label="品牌LOGO"></el-table-column>
<el-table-column label="品牌操作"></el-table-column>
</el-table>
<!-- 分頁器元件 -->
<!--
pagination
---v-model:current-page:設定當前分頁器頁碼
---v-model:page-size:設定每一也展示資料條數
---page-sizes:每頁顯示個數選擇器的選項設定
---background:背景顏色
---layout:分頁器6個子元件佈局的調整 "->"把後面的子元件頂到右側
-->
<el-pagination
v-model:current-page="pageNo"
v-model:page-size="limit"
:page-sizes="[3, 5, 7, 9]"
:background="true"
layout=" prev, pager, next, jumper,->,total, sizes,"
:total="400"
/>
</el-card>
</template>
<script setup lang="ts">
//引入組合式API函式
import { ref } from 'vue'
//當前頁碼
let pageNo = ref<number>(1)
//每一頁展示的資料
let limit = ref<number>(3)
</script>
<style lang="scss" scoped></style>
5.2 資料模組
5.2.1 API
- api函式
//書寫品牌管理模組介面
import request from '@/utils/request'
//品牌管理模組介面地址
enum API {
//獲取已有品牌介面
TRADEMARK_URL = '/admin/product/baseTrademark/',
}
//獲取一樣偶品牌的介面方法
//page:獲取第幾頁 ---預設第一頁
//limit:獲取幾個已有品牌的資料
export const reqHasTrademark = (page: number, limit: number) =>
request.get<any, any>(API.TRADEMARK_URL + `${page}/${limit}`)
- 獲取資料
我們獲取資料沒有放在pinia中,二是放在元件中掛載時獲取資料
<script setup lang="ts">
import { reqHasTrademark } from '@/api/product/trademark'
//引入組合式API函式
import { ref, onMounted } from 'vue'
//當前頁碼
let pageNo = ref<number>(1)
//每一頁展示的資料
let limit = ref<number>(3)
//儲存已有品牌資料總數
let total = ref<number>(0)
//儲存已有品牌的資料
let trademarkArr = ref<any>([])
//獲取已有品牌的介面封裝為一個函式:在任何情況下向獲取資料,呼叫次函式即可
const getHasTrademark = async (pager = 1) => {
//當前頁碼
pageNo.value = pager
let result = await reqHasTrademark(pageNo.value, limit.value)
console.log(result)
if (result.code == 200) {
//儲存已有品牌總個數
total.value = result.data.total
trademarkArr.value = result.data.records
console.log(trademarkArr)
}
}
//元件掛載完畢鉤子---發一次請求,獲取第一頁、一頁三個已有品牌資料
onMounted(() => {
getHasTrademark()
})
</script>
5.2.2 資料展示
在資料展示模組,我們使用了element-plus的el-table,下面組要講解屬性和注意點。
- data屬性:顯示的資料
比如我們這裡繫結的trademarkArr是個三個物件的陣列,就會多出來3行。
- el-table-column的type屬性:對應列的型別。 如果設定了selection則顯示多選框; 如果設定了 index 則顯示該行的索引(從 1 開始計算); 如果設定了 expand 則顯示為一個可展開的按鈕
- el-table-column的prop屬性:欄位名稱 對應列內容的欄位名, 也可以使用 property屬性
注意:因為我們之前已經繫結了資料,所以在這裡直接使用資料的屬性tmName
- el-table-column的插槽
為什麼要使用插槽呢?因為prop屬性雖然能夠展示資料,但是他預設是div,如果我們的圖片使用prop展示的話,會展示圖片的路徑。因此如果想展示圖片或者按鈕,我們就要使用插槽
注意:row就是我們的trademarkArr的每一個資料(物件)
5.3 品牌型別定義
API中的以及元件中。
export interface ResponseData {
code: number
message: string
ok: boolean
}
//已有的品牌的ts資料型別
export interface TradeMark {
id?: number
tmName: string
logoUrl: string
}
//包含全部品牌資料的ts型別
export type Records = TradeMark[]
//獲取的已有全部品牌的資料ts型別
export interface TradeMarkResponseData extends ResponseData {
data: {
records: Records
total: number
size: number
current: number
searchCount: boolean
pages: number
}
}
5.4 分頁展示資料
此部分主要是倆個功能,第一個是當點選分頁器頁數時能跳轉到對應的頁數。第二個是每頁展示的資料條數能正確顯示
5.4.1 跳轉頁數函式
這裡我們繫結的點選回撥直接用的是之前寫好的傳送請求的回撥。可以看出,傳送請求的回撥函式是有預設的引數:1.
注意:因為current-change方法時element-plus封裝好的,它會給父元件傳遞並注入一個引數(點選的頁碼),所以相當於把這個引數傳遞給了getHasTrademark函式,因此能夠跳轉到正確的頁碼數
//獲取已有品牌的介面封裝為一個函式:在任何情況下向獲取資料,呼叫次函式即可
const getHasTrademark = async (pager = 1) => {
//當前頁碼
pageNo.value = pager
let result: TradeMarkResponseData = await reqHasTrademark(
pageNo.value,
limit.value,
)
if (result.code == 200) {
//儲存已有品牌總個數
total.value = result.data.total
trademarkArr.value = result.data.records
}
}
5.4.2 每頁展示資料條數
//當下拉選單發生變化的時候觸發此方法
//這個自定義事件,分頁器元件會將下拉選單選中資料返回
const sizeChange = () => {
//當前每一頁的資料量發生變化的時候,當前頁碼歸1
getHasTrademark()
console.log(123)
}
同樣的這個函式也會返回一個引數。但是我們不需要使用這個引數,因此才另外寫一個回撥函式。
5.5 dialog對話方塊靜態搭建
- 對話方塊的標題&&顯示隱藏
v-model:屬性使用者控制對話方塊的顯示與隱藏的 true顯示 false隱藏
title:設定對話方塊左上角標題
- 表單項
<el-form style="width: 80%">
<el-form-item label="品牌名稱" label-width="100px" prop="tmName">
<el-input
placeholder="請您輸入品牌名稱"
v-model="trademarkParams.tmName"
></el-input>
</el-form-item>
<el-form-item label="品牌LOGO" label-width="100px" prop="logoUrl">
<!-- upload元件屬性:action圖片上傳路徑書寫/api,代理伺服器不傳送這次post請求 -->
<el-upload
class="avatar-uploader"
action="/api/admin/product/fileUpload"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload"
>
<img
v-if="trademarkParams.logoUrl"
:src="trademarkParams.logoUrl"
class="avatar"
/>
<el-icon v-else class="avatar-uploader-icon">
<Plus />
</el-icon>
</el-upload>
</el-form-item>
</el-form>
- 確定與取消按鈕
<template #footer>
<el-button type="primary" size="default" @click="cancel">取消</el-button>
<el-button type="primary" size="default" @click="confirm">確定</el-button>
</template>
5.5 新增品牌資料
5.4.1 API(新增與修改品牌)
因為這2個介面的攜帶的資料差不多,我們將其寫為一個方法
//書寫品牌管理模組介面
import request from '@/utils/request'
import type { TradeMarkResponseData, TradeMark } from './type'
//品牌管理模組介面地址
enum API {
。。。。。。
//新增品牌
ADDTRADEMARK_URL = '/admin/product/baseTrademark/save',
//修改已有品牌
UPDATETRADEMARK_URL = '/admin/product/baseTrademark/update',
}
。。。。。。
//新增與修改已有品牌介面方法
export const reqAddOrUpdateTrademark = (data: TradeMark) => {
//修改已有品牌的資料
if (data.id) {
return request.put<any, any>(API.UPDATETRADEMARK_URL, data)
} else {
//新增品牌
return request.post<any, any>(API.ADDTRADEMARK_URL, data)
}
}
5.4.2 收集新增品牌資料
- 定義資料
import type {
。。。。。。。
TradeMark,
} from '@/api/product/trademark/type'
//定義收集新增品牌資料
let trademarkParams = reactive<TradeMark>({
tmName: '',
logoUrl: '',
})
- 收集品牌名稱
- upload元件的屬性介紹
<el-upload
class="avatar-uploader"
action="/api/admin/product/fileUpload"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload"
>
<img
v-if="trademarkParams.logoUrl"
:src="trademarkParams.logoUrl"
class="avatar"
/>
<el-icon v-else class="avatar-uploader-icon">
<Plus />
</el-icon>
</el-upload>
class:帶的一些樣式,需複製到style中
action:圖片上傳路徑需要書寫/api,否則代理伺服器不傳送這次post請求
:show-file-list:是否展示已經上傳的檔案
:before-upload:上傳圖片之前的鉤子函式
//上傳圖片元件->上傳圖片之前觸發的鉤子函式
const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
//鉤子是在圖片上傳成功之前觸發,上傳檔案之前可以約束檔案型別與大小
//要求:上傳檔案格式png|jpg|gif 4M
if (
rawFile.type == 'image/png' ||
rawFile.type == 'image/jpeg' ||
rawFile.type == 'image/gif'
) {
if (rawFile.size / 1024 / 1024 < 4) {
return true
} else {
ElMessage({
type: 'error',
message: '上傳檔案大小小於4M',
})
return false
}
} else {
ElMessage({
type: 'error',
message: '上傳檔案格式務必PNG|JPG|GIF',
})
return false
}
}
:on-success:圖片上傳成功鉤子(收集了上傳圖片的地址)
在這裡,你將本地的圖片上傳到之前el-upload元件的action="/api/admin/product/fileUpload"
這個地址上,然後on-success鉤子會將上傳後圖片的地址返回
//圖片上傳成功鉤子
const handleAvatarSuccess: UploadProps['onSuccess'] = (
response,
uploadFile,
) => {
//response:即為當前這次上傳圖片post請求伺服器返回的資料
//收集上傳圖片的地址,新增一個新的品牌的時候帶給伺服器
trademarkParams.logoUrl = response.data
//圖片上傳成功,清除掉對應圖片校驗結果
formRef.value.clearValidate('logoUrl')
}
- 上傳圖片後,用圖片代替加號
5.4.3 新增品牌
- 點選確定按鈕回撥
const confirm = async () => {
//在你發請求之前,要對於整個表單進行校驗
//呼叫這個方法進行全部表單相校驗,如果校驗全部透過,在執行後面的語法
// await formRef.value.validate()
let result: any = await reqAddOrUpdateTrademark(trademarkParams)
//新增|修改已有品牌
if (result.code == 200) {
//關閉對話方塊
dialogFormVisible.value = false
//彈出提示資訊
ElMessage({
type: 'success',
message: trademarkParams.id ? '修改品牌成功' : '新增品牌成功',
})
//再次發請求獲取已有全部的品牌資料
getHasTrademark(trademarkParams.id ? pageNo.value : 1)
} else {
//新增品牌失敗
ElMessage({
type: 'error',
message: trademarkParams.id ? '修改品牌失敗' : '新增品牌失敗',
})
//關閉對話方塊
dialogFormVisible.value = false
}
}
- 每次點選新增品牌的時候先情況之前的資料
//新增品牌按鈕的回撥
const addTrademark = () => {
//對話方塊顯示
dialogFormVisible.value = true
//清空收集資料
trademarkParams.tmName = ''
trademarkParams.logoUrl = ''
}
5.6 修改品牌資料
- 繫結點選函式
其中的row就是當前的資料
- 回撥函式
//修改已有品牌的按鈕的回撥
//row:row即為當前已有的品牌
const updateTrademark = (row: TradeMark) => {
//對話方塊顯示
dialogFormVisible.value = true
//ES6語法合併物件
Object.assign(trademarkParams, row)
}
- 對確認按鈕回撥修改
const confirm = async () => {
。。。。。。。
if (result.code == 200) {
。。。
//彈出提示資訊
ElMessage({
。。。。
message: trademarkParams.id ? '修改品牌成功' : '新增品牌成功',
})
//再次發請求獲取已有全部的品牌資料
getHasTrademark(trademarkParams.id ? pageNo.value : 1)
} else {
//新增品牌失敗
ElMessage({
。。。。
message: trademarkParams.id ? '修改品牌失敗' : '新增品牌失敗',
})
。。。。
}
}
- 設定對話方塊標題
- 小問題
當我們修改操作之後再點選新增品牌,對話方塊的title依舊是修改品牌。怎麼是因為對話方塊的title是根據trademarkParams.id來的,我們之前新增品牌按鈕操作沒有對id進行清除。修改為如下就可
//新增品牌按鈕的回撥
const addTrademark = () => {
//對話方塊顯示
dialogFormVisible.value = true
//清空收集資料
trademarkParams.id = 0
trademarkParams.tmName = ''
trademarkParams.logoUrl = ''
}
5.7 品牌管理模組表單校驗
5.7.1 表單校驗(自定義規則校驗,可以簡略堪稱三步走)
- 繫結引數
:model:校驗的資料
:rules:校驗規則
ref="formRef":表單例項
prop:表單元素校驗的資料,可以直接使用表單繫結的資料。
- Rules
//表單校驗規則物件
const rules = {
tmName: [
//required:這個欄位務必校驗,表單項前面出來五角星
//trigger:代表觸發校驗規則時機[blur、change]
{ required: true, trigger: 'blur', validator: validatorTmName },
],
logoUrl: [{ required: true, validator: validatorLogoUrl }],
}
- Rules中寫的方法
//品牌自定義校驗規則方法
const validatorTmName = (rule: any, value: any, callBack: any) => {
//是當表單元素觸發blur時候,會觸發此方法
//自定義校驗規則
if (value.trim().length >= 2) {
callBack()
} else {
//校驗未透過返回的錯誤的提示資訊
callBack(new Error('品牌名稱位數大於等於兩位'))
}
}
//品牌LOGO圖片的自定義校驗規則方法
const validatorLogoUrl = (rule: any, value: any, callBack: any) => {
//如果圖片上傳
if (value) {
callBack()
} else {
callBack(new Error('LOGO圖片務必上傳'))
}
}
5.7.2 存在的一些問題
- 圖片校驗時機
因為img是圖片,不好判斷。因此使用表單的validate屬性,全部校驗,放在確認按鈕的回撥函式中
const confirm = async () => {
//在你發請求之前,要對於整個表單進行校驗
//呼叫這個方法進行全部表單相校驗,如果校驗全部透過,在執行後面的語法
await formRef.value.validate()
。。。。。。
}
- 清除校驗資訊
當圖片沒有上傳點選確認後會出來校驗的提示資訊,我們上傳圖片後校驗資訊應該消失。使用表單的clearValidate屬性
//圖片上傳成功鉤子
const handleAvatarSuccess: UploadProps['onSuccess'] = (
。。。。。。
) => {
。。。。。。。
//圖片上傳成功,清除掉對應圖片校驗結果
formRef.value.clearValidate('logoUrl')
}
- 清除校驗資訊2
當我們未填寫資訊去點選確認按鈕時,會彈出2個校驗資訊。當我們關閉後再開啟,校驗資訊還在。因為,我們需要在新增品牌按鈕時清除校驗資訊。但是因為點選新增品牌,表單還沒有載入,所以我們需要換個寫法。
//新增品牌按鈕的回撥
const addTrademark = () => {
//對話方塊顯示
dialogFormVisible.value = true
//清空收集資料
trademarkParams.id = 0
trademarkParams.tmName = ''
trademarkParams.logoUrl = ''
//第一種寫法:ts的問號語法
formRef.value?.clearValidate('tmName')
formRef.value?.clearValidate('logoUrl')
/* nextTick(() => {
formRef.value.clearValidate('tmName')
formRef.value.clearValidate('logoUrl')
}) */
}
同理修改按鈕
//修改已有品牌的按鈕的回撥
//row:row即為當前已有的品牌
const updateTrademark = (row: TradeMark) => {
//清空校驗規則錯誤提示資訊
nextTick(() => {
formRef.value.clearValidate('tmName')
formRef.value.clearValidate('logoUrl')
})
。。。。。。
}
5.8刪除業務
刪除業務要做的事情不多,包括API以及發請求。不過有些點要注意
- API
//書寫品牌管理模組介面
import request from '@/utils/request'
import type { TradeMarkResponseData, TradeMark } from './type'
//品牌管理模組介面地址
enum API {
。。。。。。。
//刪除已有品牌
DELETE_URL = '/admin/product/baseTrademark/remove/',
}
。。。。。。
//刪除某一個已有品牌的資料
export const reqDeleteTrademark = (id: number) =>
request.delete<any, any>(API.DELETE_URL + id)
- 繫結函式
這裡使用了一個氣泡元件,@confirm繫結的就是回撥函式
- 回撥函式
//氣泡確認框確定按鈕的回撥
const removeTradeMark = async (id: number) => {
//點選確定按鈕刪除已有品牌請求
let result = await reqDeleteTrademark(id)
if (result.code == 200) {
//刪除成功提示資訊
ElMessage({
type: 'success',
message: '刪除品牌成功',
})
//再次獲取已有的品牌資料
getHasTrademark(
trademarkArr.value.length > 1 ? pageNo.value : pageNo.value - 1,
)
} else {
ElMessage({
type: 'error',
message: '刪除品牌失敗',
})
}
}
6 屬性管理模組
6.1 屬性管理模組的靜態元件
屬性管理分為上面部分的三級分類模組以及下面的新增屬性部分。我們將三級分類模組單獨提取出來做成全域性元件
6.1.1 三級分類全域性元件(靜態)
注意:要在src\components\index.ts
下引入。
<template>
<el-card>
<el-form inline>
<el-form-item label="一級分類">
<el-select>
<el-option label="北京"></el-option>
<el-option label="深圳"></el-option>
<el-option label="廣州"></el-option>
</el-select>
</el-form-item>
<el-form-item label="二級分類">
<el-select>
<el-option label="北京"></el-option>
<el-option label="深圳"></el-option>
<el-option label="廣州"></el-option>
</el-select>
</el-form-item>
<el-form-item label="三級分類">
<el-select>
<el-option label="北京"></el-option>
<el-option label="深圳"></el-option>
<el-option label="廣州"></el-option>
</el-select>
</el-form-item>
</el-form>
</el-card>
</template>
<script setup lang="ts"></script>
<style lang="" scoped></style>
6.1.2 新增屬性模組(靜態)
<template>
<!-- 三級分類全域性元件-->
<Category></Category>
<el-card style="margin: 10px 0px">
<el-button type="primary" size="default" icon="Plus">新增屬性</el-button>
<el-table border style="margin: 10px 0px">
<el-table-column
label="序號"
type="index"
align="center"
width="80px"
></el-table-column>
<el-table-column label="屬性名稱" width="120px"></el-table-column>
<el-table-column label="屬性值名稱"></el-table-column>
<el-table-column label="操作" width="120px"></el-table-column>
</el-table>
</el-card>
</template>
<script setup lang="ts"></script>
<style lang="scss" scoped></style>
6.2 一級分類資料
一級分類的流程時:API->pinia->元件
為什麼要使用pinia呢?因為在下面的新增屬性那部分,父元件要用到三級分類元件的資訊(id),所以將資料放在pinia中是最方便的。
6.2.1 API
//這裡書寫屬性相關的API檔案
import request from '@/utils/request'
//屬性管理模組介面地址
enum API {
//獲取一級分類介面地址
C1_URL = '/admin/product/getCategory1',
//獲取二級分類介面地址
C2_URL = '/admin/product/getCategory2/',
//獲取三級分類介面地址
C3_URL = '/admin/product/getCategory3/',
}
//獲取一級分類的介面方法
export const reqC1 = () => request.get<any, any>(API.C1_URL)
//獲取二級分類的介面方法
export const reqC2 = (category1Id: number | string) => {
return request.get<any, any>(API.C2_URL + category1Id)
}
//獲取三級分類的介面方法
export const reqC3 = (category2Id: number | string) => {
return request.get<any, any>(API.C3_URL + category2Id)
}
6.2.2 pinia
//商品分類全域性元件的小倉庫
import { defineStore } from 'pinia'
import { reqC1, } from '@/api/product/attr'
const useCategoryStore = defineStore('Category', {
state: () => {
return {
//儲存一級分類的資料
c1Arr: [],
//儲存一級分類的ID
c1Id: '',
}
},
actions: {
//獲取一級分類的方法
async getC1() {
//發請求獲取一級分類的資料
const result = await reqC1()
if (result.code == 200) {
this.c1Arr = result.data
}
},
},
getters: {},
})
export default useCategoryStore
6.2.3 Category元件
注意:el-option中的:value屬性,它將繫結的值傳遞給el-select中的v-model繫結的值
<template>
<el-card>
<el-form inline>
<el-form-item label="一級分類">
<el-select v-model="categoryStore.c1Id">
<!-- label:即為展示資料 value:即為select下拉選單收集的資料 -->
<el-option
v-for="(c1, index) in categoryStore.c1Arr"
:key="c1.id"
:label="c1.name"
:value="c1.id"
></el-option>
</el-select>
</el-form-item>
。。。。。。
</template>
<script setup lang="ts">
//引入元件掛載完畢方法
import { onMounted } from 'vue'
//引入分類相關的倉庫
import useCategoryStore from '@/store/modules/category'
let categoryStore = useCategoryStore()
//分類全域性元件掛載完畢,通知倉庫發請求獲取一級分類的資料
onMounted(() => {
getC1()
})
//通知倉庫獲取一級分類的方法
const getC1 = () => {
//通知分類倉庫發請求獲取一級分類的資料
categoryStore.getC1()
}
</script>
<style lang="" scoped></style>
6.3 分類資料ts型別
6.3.1 API下的type
//分類相關的資料ts型別
export interface ResponseData {
code: number
message: string
ok: boolean
}
//分類ts型別
export interface CategoryObj {
id: number | string
name: string
category1Id?: number
category2Id?: number
}
//相應的分類介面返回資料的型別
export interface CategoryResponseData extends ResponseData {
data: CategoryObj[]
}
使用:倉庫中的result,API中的介面返回的資料
6.3.2 元件下的type
import type { CategoryObj } from '@/api/product/attr/type'
。。。。。
//定義分類倉庫state物件的ts型別
export interface CategoryState {
c1Id: string | number
c1Arr: CategoryObj[]
c2Arr: CategoryObj[]
c2Id: string | number
c3Arr: CategoryObj[]
c3Id: string | number
}
使用:倉庫中的state資料型別
6.4 完成分類元件業務
分類元件就是以及元件上來就拿到資料,透過使用者選擇後我們會拿到id,透過id傳送請求之後二級分類就會拿到資料。以此類推三級元件。我們以二級分類為例。
6.4.1 二級分類流程
- 繫結函式
二級分類不是一上來就發生變化,而是要等一級分類確定好之後再傳送請求獲得資料。於是我們將這個傳送請求的回撥函式繫結在了一級分類的change屬性上
- 回撥函式
//此方法即為一級分類下拉選單的change事件(選中值的時候會觸發,保證一級分類ID有了)
const handler = () => {
//通知倉庫獲取二級分類的資料
categoryStore.getC2()
}
- pinia
//獲取二級分類的資料
async getC2() {
//獲取對應一級分類的下二級分類的資料
const result: CategoryResponseData = await reqC2(this.c1Id)
if (result.code == 200) {
this.c2Arr = result.data
}
},
- 元件資料展示
- 三級元件同理
6.4.2 小問題
當我們選擇好三級選單後,此時修改一級選單。二、三級選單應該清空
清空id之後就不會顯示了。
//此方法即為一級分類下拉選單的change事件(選中值的時候會觸發,保證一級分類ID有了)
const handler = () => {
//需要將二級、三級分類的資料清空
categoryStore.c2Id = ''
categoryStore.c3Arr = []
categoryStore.c3Id = ''
//通知倉庫獲取二級分類的資料
categoryStore.getC2()
}
//此方法即為二級分類下拉選單的change事件(選中值的時候會觸發,保證二級分類ID有了)
const handler1 = () => {
//清理三級分類的資料
categoryStore.c3Id = ''
categoryStore.getC3()
}
6.4.3 新增屬性按鈕禁用
在我們沒選擇好三級選單之前,新增屬性按鈕應該處於禁用狀態
src\views\product\attr\index.vue
(父元件)
6.5 已有屬性與屬性值展示
6.5.1 返回type型別
//屬性值物件的ts型別
export interface AttrValue {
id?: number
valueName: string
attrId?: number
flag?: boolean
}
//儲存每一個屬性值的陣列型別
export type AttrValueList = AttrValue[]
//屬性物件
export interface Attr {
id?: number
attrName: string
categoryId: number | string
categoryLevel: number
attrValueList: AttrValueList
}
//儲存每一個屬性物件的陣列ts型別
export type AttrList = Attr[]
//屬性介面返回的資料ts型別
export interface AttrResponseData extends ResponseData {
data: Attr[]
}
6.5.2 API傳送請求
//這裡書寫屬性相關的API檔案
import request from '@/utils/request'
import type { CategoryResponseData, AttrResponseData, Attr } from './type'
//屬性管理模組介面地址
enum API {
。。。。。。。
//獲取分類下已有的屬性與屬性值
ATTR_URL = '/admin/product/attrInfoList/',
}
。。。。。。
//獲取對應分類下已有的屬性與屬性值介面
export const reqAttr = (
category1Id: string | number,
category2Id: string | number,
category3Id: string | number,
) => {
return request.get<any, AttrResponseData>(
API.ATTR_URL + `${category1Id}/${category2Id}/${category3Id}`,
)
}
6.5.3 元件獲取返回資料並儲存資料
注意:透過watch監聽c3Id,來適時的獲取資料。
<script setup lang="ts">
//組合式API函式
import { watch, ref } from 'vue'
//引入獲取已有屬性與屬性值介面
import { reqAttr } from '@/api/product/attr'
import type { AttrResponseData, Attr } from '@/api/product/attr/type'
//引入分類相關的倉庫
import useCategoryStore from '@/store/modules/category'
let categoryStore = useCategoryStore()
//儲存已有的屬性與屬性值
let attrArr = ref<Attr[]>([])
//監聽倉庫三級分類ID變化
watch(
() => categoryStore.c3Id,
() => {
//獲取分類的ID
getAttr()
},
)
//獲取已有的屬性與屬性值方法
const getAttr = async () => {
const { c1Id, c2Id, c3Id } = categoryStore
//獲取分類下的已有的屬性與屬性值
let result: AttrResponseData = await reqAttr(c1Id, c2Id, c3Id)
console.log(result)
if (result.code == 200) {
attrArr.value = result.data
}
}
</script>
6.5.4 將資料放入模板中
<el-card style="margin: 10px 0px">
<el-button
type="primary"
size="default"
icon="Plus"
:disabled="categoryStore.c3Id ? false : true"
>
新增屬性
</el-button>
<el-table border style="margin: 10px 0px" :data="attrArr">
<el-table-column
label="序號"
type="index"
align="center"
width="80px"
></el-table-column>
<el-table-column
label="屬性名稱"
width="120px"
prop="attrName"
></el-table-column>
<el-table-column label="屬性值名稱">
<!-- row:已有的屬性物件 -->
<template #="{ row, $index }">
<el-tag
style="margin: 5px"
v-for="(item, index) in row.attrValueList"
:key="item.id"
>
{{ item.valueName }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120px">
<!-- row:已有的屬性物件 -->
<template #="{ row, $index }">
<!-- 修改已有屬性的按鈕 -->
<el-button type="primary" size="small" icon="Edit"></el-button>
<el-button type="primary" size="small" icon="Delete"></el-button>
</template>
</el-table-column>
</el-table>
</el-card>
6.5.5 小問題
當我們獲取資料並展示以後,此時修改一級分類或者二級分類,由於watch的存在,同樣會傳送請求。但是此時沒有c3Id,請求會失敗。因此將watch改為如下
//監聽倉庫三級分類ID變化
watch(
() => categoryStore.c3Id,
() => {
//清空上一次查詢的屬性與屬性值
attrArr.value = []
//保證三級分類得有才能發請求
if (!categoryStore.c3Id) return
//獲取分類的ID
getAttr()
},
)
6.6 新增屬性頁面的靜態展示
當點選新增屬性後:
6.6.1 定義變數控制頁面展示與隱藏
//定義card元件內容切換變數
let scene = ref<number>(0) //scene=0,顯示table,scene=1,展示新增與修改屬性結構
6.6.2 表單
6.6.3 按鈕
6.6.4 表格
6.6.5按鈕
6.6.6 三級分類禁用
當點選新增屬性之後,三級分類應該被禁用。因此使用props給子元件傳參
子元件:
二三級分類同理。
6.7 新增屬性&&修改屬性的介面型別
6.7.1修改屬性
6.7.2 新增屬性
6.7.3 type
//屬性值物件的ts型別
export interface AttrValue {
id?: number
valueName: string
attrId?: number
flag?: boolean
}
//儲存每一個屬性值的陣列型別
export type AttrValueList = AttrValue[]
//屬性物件
export interface Attr {
id?: number
attrName: string
categoryId: number | string
categoryLevel: number
attrValueList: AttrValueList
}
6.7.4 元件收集新增的屬性的資料
//收集新增的屬性的資料
let attrParams = reactive<Attr>({
attrName: '', //新增的屬性的名字
attrValueList: [
//新增的屬性值陣列
],
categoryId: '', //三級分類的ID
categoryLevel: 3, //代表的是三級分類
})
6.8 新增屬性值
一個操作最重要的是理清楚思路。新增屬性值的總體思路是:收集表單的資料(繫結對應的表單項等)->傳送請求(按鈕回撥函式,攜帶的引數)->更新頁面
6.8.1 收集表單的資料(attrParams)
- 屬性名稱(attrName)
- 屬性值陣列(attrValueList)
我們給新增屬性值按鈕繫結一個回撥,點選的時候會往attrParams.attrValueList中新增一個空陣列。我們根據空陣列的數量生成input框,再將input的值與陣列中的值繫結。
//新增屬性值按鈕的回撥
const addAttrValue = () => {
//點選新增屬性值按鈕的時候,向陣列新增一個屬性值物件
attrParams.attrValueList.push({
valueName: '',
flag: true, //控制每一個屬性值編輯模式與切換模式的切換
})
}
- 三級分類的id(categoryId)
三級分類的id(c3Id)在頁面1的新增屬性按鈕之前就有了,因此我們把它放到新增屬性按鈕的回撥身上
注意:每一次點選的時候,先清空一下資料再收集資料。防止下次點選時會顯示上次的資料
//新增屬性按鈕的回撥
const addAttr = () => {
//每一次點選的時候,先清空一下資料再收集資料
Object.assign(attrParams, {
attrName: '', //新增的屬性的名字
attrValueList: [
//新增的屬性值陣列
],
categoryId: categoryStore.c3Id, //三級分類的ID
categoryLevel: 3, //代表的是三級分類
})
//切換為新增與修改屬性的結構
scene.value = 1
}
- categoryLevel(固定的,無需收集)
6.8.2 傳送請求&&更新頁面
//儲存按鈕的回撥
const save = async () => {
//發請求
let result: any = await reqAddOrUpdateAttr(attrParams)
//新增屬性|修改已有的屬性已經成功
if (result.code == 200) {
//切換場景
scene.value = 0
//提示資訊
ElMessage({
type: 'success',
message: attrParams.id ? '修改成功' : '新增成功',
})
//獲取全部已有的屬性與屬性值(更新頁面)
getAttr()
} else {
ElMessage({
type: 'error',
message: attrParams.id ? '修改失敗' : '新增失敗',
})
}
}
6.9 屬性值的編輯與檢視模式
6.9.1 模板的切換
在input下面新增了一個div,使用flag來決定哪個展示。
注意:flag放在哪?由於每一個屬性值物件都需要一個flag屬性,因此將flag的新增放在新增屬性值的按鈕的回撥上。(注意修改屬性值的type)
//新增屬性值按鈕的回撥
const addAttrValue = () => {
//點選新增屬性值按鈕的時候,向陣列新增一個屬性值物件
attrParams.attrValueList.push({
valueName: '',
flag: true, //控制每一個屬性值編輯模式與切換模式的切換
})
}
src\api\product\attr\type.ts
6.9.2 切換的回撥
//屬性值表單元素失卻焦點事件回撥
const toLook = (row: AttrValue, $index: number) => {
。。。。。。
//相應的屬性值物件flag:變為false,展示div
row.flag = false
}
//屬性值div點選事件
const toEdit = (row: AttrValue, $index: number) => {
//相應的屬性值物件flag:變為true,展示input
row.flag = true
。。。。。。
}
6.9.3 處理非法屬性值
//屬性值表單元素失卻焦點事件回撥
const toLook = (row: AttrValue, $index: number) => {
//非法情況判斷1
if (row.valueName.trim() == '') {
//刪除呼叫對應屬性值為空的元素
attrParams.attrValueList.splice($index, 1)
//提示資訊
ElMessage({
type: 'error',
message: '屬性值不能為空',
})
return
}
//非法情況2
let repeat = attrParams.attrValueList.find((item) => {
//切記把當前失卻焦點屬性值物件從當前陣列扣除判斷
if (item != row) {
return item.valueName === row.valueName
}
})
if (repeat) {
//將重複的屬性值從陣列當中幹掉
attrParams.attrValueList.splice($index, 1)
//提示資訊
ElMessage({
type: 'error',
message: '屬性值不能重複',
})
return
}
//相應的屬性值物件flag:變為false,展示div
row.flag = false
}
6.10 表單聚焦&&刪除按鈕
表單聚焦可以直接呼叫input提供foces方法:當選擇器的輸入框獲得焦點時觸發
6.10.1 儲存元件例項
使用ref的函式形式,每有一個input就將其存入inputArr中
//準備一個陣列:將來儲存對應的元件例項el-input
let inputArr = ref<any>([])
6.10.2 點選div轉換成input框後的自動聚焦
注意:使用nextTick是因為點選後,元件需要載入,沒辦法第一時間拿到元件例項。所以使用nextTick會等到元件載入完畢後才呼叫,達到聚焦效果。
//屬性值div點選事件
const toEdit = (row: AttrValue, $index: number) => {
//相應的屬性值物件flag:變為true,展示input
row.flag = true
//nextTick:響應式資料發生變化,獲取更新的DOM(元件例項)
nextTick(() => {
inputArr.value[$index].focus()
})
}
6.10.3 新增屬性值自動聚焦
//新增屬性值按鈕的回撥
const addAttrValue = () => {
//點選新增屬性值按鈕的時候,向陣列新增一個屬性值物件
attrParams.attrValueList.push({
valueName: '',
flag: true, //控制每一個屬性值編輯模式與切換模式的切換
})
//獲取最後el-input元件聚焦
nextTick(() => {
inputArr.value[attrParams.attrValueList.length - 1].focus()
})
}
6.10.4 刪除按鈕
6.11屬性修改業務
6.11.1屬性修改業務
修改業務很簡單:當我們點選修改按鈕的時候,將修改的例項(row)傳遞給回撥函式。回撥函式:首先跳轉到第二頁面,第二頁面是根據attrParams值生成的,我們跳轉的時候將例項的值傳遞給attrParams
//table表格修改已有屬性按鈕的回撥
const updateAttr = (row: Attr) => {
//切換為新增與修改屬性的結構
scene.value = 1
//將已有的屬性物件賦值給attrParams物件即為
//ES6->Object.assign進行物件的合併
Object.assign(attrParams, JSON.parse(JSON.stringify(row)))
}
6.11.2 深複製與淺複製
深複製和淺複製的區別
1.淺複製: 將原物件或原陣列的引用直接賦給新物件,新陣列,新物件/陣列只是原物件的一個引用
2.深複製: 建立一個新的物件和陣列,將原物件的各項屬性的“值”(陣列的所有元素)複製過來,是“值”而不是“引用”
這裡存在一個問題,也就是當我們修改屬性值後,並沒有儲存(發請求),但是介面還是改了。這是因為我們的賦值語句:Object.assign(attrParams, row)
是淺複製。相當於我們在修改伺服器發回來的資料並展示在頁面上。伺服器內部並沒有修改。
解決:將淺複製改為深複製:Object.assign(attrParams, JSON.parse(JSON.stringify(row)))
6.12 刪除按鈕&&清空資料
6.12.1刪除按鈕
- API
//這裡書寫屬性相關的API檔案
import request from '@/utils/request'
import type { CategoryResponseData, AttrResponseData, Attr } from './type'
//屬性管理模組介面地址
enum API {
。。。。。。
//刪除某一個已有的屬性
DELETEATTR_URL = '/admin/product/deleteAttr/',
}
。。。。。。
//刪除某一個已有的屬性業務
export const reqRemoveAttr = (attrId: number) =>
request.delete<any, any>(API.DELETEATTR_URL + attrId)
- 繫結點選函式&&氣泡彈出框
- 回撥函式(功能實現&&重新整理頁面)
//刪除某一個已有的屬性方法回撥
const deleteAttr = async (attrId: number) => {
//發相應的刪除已有的屬性的請求
let result: any = await reqRemoveAttr(attrId)
//刪除成功
if (result.code == 200) {
ElMessage({
type: 'success',
message: '刪除成功',
})
//獲取一次已有的屬性與屬性值
getAttr()
} else {
ElMessage({
type: 'error',
message: '刪除失敗',
})
}
}
6.12.2路由跳轉前清空資料
//路由元件銷燬的時候,把倉庫分類相關的資料清空
onBeforeUnmount(() => {
//清空倉庫的資料
categoryStore.$reset()
})
7. Spu模組
SPU(Standard Product Unit):標準化產品單元。是商品資訊聚合的最小單位,是一組可複用、易檢索的標準化資訊的集合,該集合描述了一個產品的特性。通俗點講,屬性值、特性相同的商品就可以稱為一個SPU。
7.1 Spu模組的靜態頁面
<template>
<div>
<!-- 三級分類 -->
<Category :scene="scene"></Category>
<el-card style="margin: 10px 10px">
<el-button type="primary" size="default" icon="Plus">新增SPU</el-button>
<el-table border style="margin: 10px 10px">
<el-table-column
label="序號"
type="index"
align="center"
width="80px"
></el-table-column>
<el-table-column label="SPU名稱"></el-table-column>
<el-table-column label="SPU描述"></el-table-column>
<el-table-column label="SPU操作"></el-table-column>
</el-table>
</el-card>
<!-- 分頁器 -->
<el-pagination
v-model:current-page="pageNo"
v-model:page-size="pageSize"
:page-sizes="[3, 5, 7, 9]"
:background="true"
layout=" prev, pager, next, jumper,->, sizes,total"
:total="400"
/>
</div>
</template>
<script setup lang="ts">
import { ref, watch, onBeforeUnmount } from 'vue'
//場景的資料
let scene = ref<number>(0)
//分頁器預設頁碼
let pageNo = ref<number>(1)
//每一頁展示幾條資料
let pageSize = ref<number>(3)
</script>
<style lang="scss" scoped></style>
7.2 Spu模組展示已有資料
7.2.1 API
//SPU管理模組的介面
import request from '@/utils/request'
import type { HasSpuResponseData } from './type'
enum API {
//獲取已有的SPU的資料
HASSPU_URL = '/admin/product/',
}
//獲取某一個三級分類下已有的SPU資料
export const reqHasSpu = (
page: number,
limit: number,
category3Id: string | number,
) => {
return request.get<any, HasSpuResponseData>(
API.HASSPU_URL + `${page}/${limit}?category3Id=${category3Id}`,
)
}
7.2.2 type
//伺服器全部介面返回的資料型別
export interface ResponseData {
code: number
message: string
ok: boolean
}
//SPU資料的ts型別:需要修改
export interface SpuData {
category3Id: string | number
id?: number
spuName: string
tmId: number | string
description: string
spuImageList: null
spuSaleAttrList: null
}
//陣列:元素都是已有SPU資料型別
export type Records = SpuData[]
//定義獲取已有的SPU介面返回的資料ts型別
export interface HasSpuResponseData extends ResponseData {
data: {
records: Records
total: number
size: number
current: number
searchCount: boolean
pages: number
}
}
7.2.3 新增SPU按鈕
7.2.4 表單資料
<el-table border style="margin: 10px 10px" :data="records">
<el-table-column
label="序號"
type="index"
align="center"
width="80px"
></el-table-column>
<el-table-column label="SPU名稱" prop="spuName"></el-table-column>
<el-table-column
label="SPU描述"
prop="description"
show-overflow-tooltip
></el-table-column>
<el-table-column label="SPU操作">
<!-- row:即為已有的SPU物件 -->
<template #="{ row, $index }">
<el-button
type="primary"
size="small"
icon="Plus"
title="新增SKU"
></el-button>
<el-button
type="primary"
size="small"
icon="Edit"
title="修改SPU"
></el-button>
<el-button
type="primary"
size="small"
icon="View"
title="檢視SKU列表"
></el-button>
<el-popconfirm :title="`你確定刪除${row.spuName}?`" width="200px">
<template #reference>
<el-button
type="primary"
size="small"
icon="Delete"
title="刪除SPU"
></el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
7.2.5 分頁器
注意getHasSpu函式攜帶的引數。預設為1
<!-- 分頁器 -->
<el-pagination
v-model:current-page="pageNo"
v-model:page-size="pageSize"
:page-sizes="[3, 5, 7, 9]"
:background="true"
layout=" prev, pager, next, jumper,->, sizes,total"
:total="total"
@current-change="getHasSpu"
@size-change="changeSize"
/>
//此方法執行:可以獲取某一個三級分類下全部的已有的SPU
const getHasSpu = async (pager = 1) => {
//修改當前頁碼
pageNo.value = pager
let result: HasSpuResponseData = await reqHasSpu(
pageNo.value,
pageSize.value,
categoryStore.c3Id,
)
if (result.code == 200) {
records.value = result.data.records
total.value = result.data.total
}
}
//分頁器下拉選單發生變化的時候觸發
const changeSize = () => {
getHasSpu()
}
7.2.6 watch監聽
//監聽三級分類ID變化
watch(
() => categoryStore.c3Id,
() => {
//當三級分類發生變化的時候清空對應的資料
records.value = []
//務必保證有三級分類ID
if (!categoryStore.c3Id) return
getHasSpu()
},
)
7.3 SPU場景一的靜態&&場景切換
7.3.1 子元件搭建
由於SPU模組需要在三個場景進行切換,全都放在一個元件裡面的話會顯得很臃腫。因此我們將它放到三個元件當中。
使用v-show來展示頁面:v-if是銷燬元件,v-show是隱藏元件。在初載入的時候v-if比較快,但是在頻繁切換的時候v-if任務重。
7.3.2 SPU場景一子元件靜態
<template>
<el-form label-width="100px">
<el-form-item label="SPU名稱">
<el-input placeholder="請你輸入SPU名稱"></el-input>
</el-form-item>
<el-form-item label="SPU品牌">
<el-select>
<el-option label="華為"></el-option>
<el-option label="oppo"></el-option>
<el-option label="vivo"></el-option>
</el-select>
</el-form-item>
<el-form-item label="SPU描述">
<el-input type="textarea" placeholder="請你輸入SPU描述"></el-input>
</el-form-item>
<el-form-item label="SPU圖片">
<el-upload
v-model:file-list="fileList"
action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15"
list-type="picture-card"
:on-preview="handlePictureCardPreview"
:on-remove="handleRemove"
>
<el-icon><Plus /></el-icon>
</el-upload>
<el-dialog v-model="dialogVisible">
<img w-full :src="dialogImageUrl" alt="Preview Image" />
</el-dialog>
</el-form-item>
<el-form-item label="SPU銷售屬性" size="normal">
<!-- 展示銷售屬性的下拉選單 -->
<el-select>
<el-option label="華為"></el-option>
<el-option label="oppo"></el-option>
<el-option label="vivo"></el-option>
</el-select>
<el-button
style="margin-left: 10px"
type="primary"
size="default"
icon="Plus"
>
新增屬性
</el-button>
<!-- table展示銷售屬性與屬性值的地方 -->
<el-table border style="margin: 10px 0px">
<el-table-column
label="序號"
type="index"
align="center"
width="80px"
></el-table-column>
<el-table-column
label="銷售屬性名字"
width="120px"
prop="saleAttrName"
></el-table-column>
<el-table-column label="銷售屬性值">
<!-- row:即為當前SPU已有的銷售屬性物件 -->
</el-table-column>
<el-table-column label="操作" width="120px"></el-table-column>
</el-table>
</el-form-item>
<el-form-item>
<el-button type="primary" size="default">儲存</el-button>
<el-button type="primary" size="default" @click="cancel">取消</el-button>
</el-form-item>
</el-form>
</template>
7.3.3 父元件中新增SPU按鈕&&修改按鈕
這兩個按鈕都是跳轉到場景一.下面是對應的回撥
//新增新的SPU按鈕的回撥
const addSpu = () => {
//切換為場景1:新增與修改已有SPU結構->SpuForm
scene.value = 1
}
//修改已有的SPU的按鈕的回撥
const updateSpu = () => {
//切換為場景1:新增與修改已有SPU結構->SpuForm
scene.value = 1
}
7.3.4 子元件中取消按鈕的回撥
需要改變的是父元件中的scene,因此涉及到父子元件通訊。這裡使用自定義事件。
父元件:
子元件:
//取消按鈕的回撥
const cancel = () => {
$emit('changeScene', 0)
}
7.4 SPU模組API&&TS型別(修改&&新增)
修改和新增的頁面是差不多的。頁面1的四個地方都需要發請求拿資料,我們在這一部分分別編寫4個部分的API以及ts型別
7.4.1 SPU品牌
- API:
//獲取全部品牌的資料
ALLTRADEMARK_URL = '/admin/product/baseTrademark/getTrademarkList',
//獲取全部的SPU的品牌的資料
export const reqAllTradeMark = () => {
return request.get<any, AllTradeMark>(API.ALLTRADEMARK_URL)
}
- ts
//品牌資料的TS型別
export interface Trademark {
id: number
tmName: string
logoUrl: string
}
//品牌介面返回的資料ts型別
export interface AllTradeMark extends ResponseData {
data: Trademark[]
}
7.4.2 SPU圖片
- API
//獲取某個SPU下的全部的售賣商品的圖片資料
IMAGE_URL = '/admin/product/spuImageList/',
//獲取某一個已有的SPU下全部商品的圖片地址
export const reqSpuImageList = (spuId: number) => {
return request.get<any, SpuHasImg>(API.IMAGE_URL + spuId)
}
- ts
//商品圖片的ts型別
export interface SpuImg {
id?: number
imgName?: string
imgUrl?: string
createTime?: string
updateTime?: string
spuId?: number
name?: string
url?: string
}
//已有的SPU的照片牆資料的型別
export interface SpuHasImg extends ResponseData {
data: SpuImg[]
}
7.4.3 全部銷售屬性
- API
//獲取整個專案全部的銷售屬性[顏色、版本、尺碼]
ALLSALEATTR_URL = '/admin/product/baseSaleAttrList',
//獲取全部的銷售屬性
export const reqAllSaleAttr = () => {
return request.get<any, HasSaleAttrResponseData>(API.ALLSALEATTR_URL)
}
- ts
//已有的全部SPU的返回資料ts型別
export interface HasSaleAttr {
id: number
name: string
}
export interface HasSaleAttrResponseData extends ResponseData {
data: HasSaleAttr[]
}
7.4.4 已有的銷售屬性
- API
//獲取某一個SPU下全部的已有的銷售屬性介面地址
SPUHASSALEATTR_URL = '/admin/product/spuSaleAttrList/',
//獲取某一個已有的SPU擁有多少個銷售屬性
export const reqSpuHasSaleAttr = (spuId: number) => {
return request.get<any, SaleAttrResponseData>(API.SPUHASSALEATTR_URL + spuId)
}
- ts
//銷售屬性物件ts型別
export interface SaleAttr {
id?: number
createTime?: null
updateTime?: null
spuId?: number
baseSaleAttrId: number | string
saleAttrName: string
spuSaleAttrValueList: SpuSaleAttrValueList
flag?: boolean
saleAttrValue?: string
}
//SPU已有的銷售屬性介面返回資料ts型別
export interface SaleAttrResponseData extends ResponseData {
data: SaleAttr[]
}
7.5 獲取SPU的資料
首先:SPU的資料應該分為5部分:第一部分:是父元件裡的展示的資料,也是我們點選修改按鈕時的那個資料。其餘4個部分的資料需要我們發請求得到。
問題1:子元件需要用到父元件中的資料,應該怎麼辦?答:要傳遞的資料是指定的,也就是我們點選修改時的資料。透過ref的方式,拿到子元件時的例項,再呼叫子元件暴露的方法將資料做為引數傳遞過去。(有點類似於反向的自定義事件)
問題2:其餘4個部分的資料什麼時候獲取。答:同樣的在點選修改按鈕時獲取,問題一中透過呼叫子元件的函式傳遞資料,我們同時也在這個函式中發請求得到資料
7.5.1 第一部分資料的傳遞
- 父元件拿到子元件例項
- 子元件暴露對外函式
- 修改按鈕點選函式中呼叫子元件函式,並傳遞第一部分資料
//修改已有的SPU的按鈕的回撥
const updateSpu = (row: SpuData) => {
//切換為場景1:新增與修改已有SPU結構->SpuForm
scene.value = 1
//呼叫子元件例項方法獲取完整已有的SPU的資料
spu.value.initHasSpuData(row)
}
7.5.2 其餘資料
子元件中直接發起請求,並且將伺服器返回的四個資料儲存,加上引數傳遞的第一部分資料,這樣子元件拿到了全部的資料。
//子元件書寫一個方法
const initHasSpuData = async (spu: SpuData) => {
//spu:即為父元件傳遞過來的已有的SPU物件[不完整]
//獲取全部品牌的資料
let result: AllTradeMark = await reqAllTradeMark()
//獲取某一個品牌旗下全部售賣商品的圖片
let result1: SpuHasImg = await reqSpuImageList(spu.id as number)
//獲取已有的SPU銷售屬性的資料
let result2: SaleAttrResponseData = await reqSpuHasSaleAttr(spu.id as number)
//獲取整個專案全部SPU的銷售屬性
let result3: HasSaleAttrResponseData = await reqAllSaleAttr()
//儲存全部品牌的資料
MYAllTradeMark.value = result.data
//SPU對應商品圖片
imgList.value = result1.data.map((item) => {
return {
name: item.imgName,
url: item.imgUrl,
}
})
//儲存已有的SPU的銷售屬性
saleAttr.value = result2.data
//儲存全部的銷售屬性
allSaleAttr.value = result3.data
}
7.6 修改與新增的介面&&TS
7.6.1 介面(API)
/追加一個新的SPU
ADDSPU_URL = '/admin/product/saveSpuInfo',
//更新已有的SPU
UPDATESPU_URL = '/admin/product/updateSpuInfo',
//新增一個新的SPU的
//更新已有的SPU介面
//data:即為新增的SPU|或者已有的SPU物件
export const reqAddOrUpdateSpu = (data: any) => {
//如果SPU物件擁有ID,更新已有的SPU
if (data.id) {
return request.post<any, any>(API.UPDATESPU_URL, data)
} else {
return request.post<any, any>(API.ADDSPU_URL, data)
}
}
7.6.2 ts
//SPU資料的ts型別:需要修改
export interface SpuData {
category3Id: string | number
id?: number
spuName: string
tmId: number | string
description: string
spuImageList: null | SpuImg[]
spuSaleAttrList: null | SaleAttr[]
}
7.7 展示與收集已有的資料
7.7.1 儲存父元件傳遞過來的資料
//儲存已有的SPU物件
let SpuParams = ref<SpuData>({
category3Id: '', //收集三級分類的ID
spuName: '', //SPU的名字
description: '', //SPU的描述
tmId: '', //品牌的ID
spuImageList: [],
spuSaleAttrList: [],
})
//子元件書寫一個方法
const initHasSpuData = async (spu: SpuData) => {
//儲存已有的SPU物件,將來在模板中展示
SpuParams.value = spu
。。。。。。
}
7.7.2 展示SPU名稱
7.7.3 展示SPU品牌
注意:下方的紅框展示的是所有品牌,上方的繫結的是一個數字也就是下方的第幾個
7.7.4 SPU描述
7.7.5 照片牆PART
照片牆部分我們使用了element-plus的el-upload元件。下面詳細介紹元件的功能及作用
- 整體結構
上面el-upload是上傳照片的照片牆,下面是檢視照片的對話方塊
- v-model:file-list
//商品圖片
let imgList = ref<SpuImg[]>([])
//子元件書寫一個方法
const initHasSpuData = async (spu: SpuData) => {
。。。。。。
//獲取某一個品牌旗下全部售賣商品的圖片
let result1: SpuHasImg = await reqSpuImageList(spu.id as number)
......
//SPU對應商品圖片
imgList.value = result1.data.map((item) => {
return {
name: item.imgName,
url: item.imgUrl,
}
})
......
}
這部分是一個雙向繫結的資料,我們從伺服器得到資料會展示到照片牆上。得到資料的過程我們使用了陣列的map方法,這是因為元件對於資料的格式有要求。
- action
action是指圖片上傳的地址。元件還會將返回的資料放到對應的img的資料中
- list-type:照片牆的形式
- :on-preview
預覽的鉤子,預覽照片時會觸發。會注入對應圖片的資料。
//控制對話方塊的顯示與隱藏
let dialogVisible = ref<boolean>(false)
//儲存預覽圖片地址
let dialogImageUrl = ref<string>('')
//照片牆點選預覽按鈕的時候觸發的鉤子
const handlePictureCardPreview = (file: any) => {
dialogImageUrl.value = file.url
//對話方塊彈出來
dialogVisible.value = true
}
- :on-remove
移除圖片前的鉤子
- :before-upload
上傳前的鉤子,我們用來對資料做預處理
//照片錢上傳成功之前的鉤子約束檔案的大小與型別
const handlerUpload = (file: any) => {
if (
file.type == 'image/png' ||
file.type == 'image/jpeg' ||
file.type == 'image/gif'
) {
if (file.size / 1024 / 1024 < 3) {
return true
} else {
ElMessage({
type: 'error',
message: '上傳檔案務必小於3M',
})
return false
}
} else {
ElMessage({
type: 'error',
message: '上傳檔案務必PNG|JPG|GIF',
})
return false
}
}
7.8 展示已有的銷售屬性與屬性值
資料結構如下:
7.8.1 展示銷售屬性與屬性值
其實就是4列,對應好每一列以及對應的資料就好
<!-- table展示銷售屬性與屬性值的地方 -->
<el-table border style="margin: 10px 0px" :data="saleAttr">
<el-table-column
label="序號"
type="index"
align="center"
width="80px"
></el-table-column>
<el-table-column
label="銷售屬性名字"
width="120px"
prop="saleAttrName"
></el-table-column>
<el-table-column label="銷售屬性值">
<!-- row:即為當前SPU已有的銷售屬性物件 -->
<template #="{ row, $index }">
<el-tag
class="mx-1"
closable
style="margin: 0px 5px"
@close="row.spuSaleAttrValueList.splice(index, 1)"
v-for="(item, index) in row.spuSaleAttrValueList"
:key="row.id"
>
{{ item.saleAttrValueName }}
</el-tag>
<el-button type="primary" size="small" icon="Plus"></el-button>
</template>
</el-table-column>
<el-table-column label="操作" width="120px">
<template #="{ row, $index }">
<el-button
type="primary"
size="small"
icon="Delete"
@click="saleAttr.splice($index, 1)"
></el-button>
</template>
</el-table-column>
</el-table>
7.8.2 刪除操作
<el-table-column label="操作" width="120px">
<template #="{ row, $index }">
<el-button
type="primary"
size="small"
icon="Delete"
@click="saleAttr.splice($index, 1)"
></el-button>
</template>
</el-table-column>
7.9 完成收集新增銷售屬性業務
7.9.1 計算出還未擁有的銷售屬性
//計算出當前SPU還未擁有的銷售屬性
let unSelectSaleAttr = computed(() => {
//全部銷售屬性:顏色、版本、尺碼
//已有的銷售屬性:顏色、版本
let unSelectArr = allSaleAttr.value.filter((item) => {
return saleAttr.value.every((item1) => {
return item.name != item1.saleAttrName
})
})
return unSelectArr
})
7.9.2 收集你選擇的屬性的id以及name
7.9.3 新增屬性按鈕的回撥
//新增銷售屬性的方法
const addSaleAttr = () => {
/*
"baseSaleAttrId": number,
"saleAttrName": string,
"spuSaleAttrValueList": SpuSaleAttrValueList
*/
const [baseSaleAttrId, saleAttrName] = saleAttrIdAndValueName.value.split(':')
//準備一個新的銷售屬性物件:將來帶給伺服器即可
let newSaleAttr: SaleAttr = {
baseSaleAttrId,
saleAttrName,
spuSaleAttrValueList: [],
}
//追加到陣列當中
saleAttr.value.push(newSaleAttr)
//清空收集的資料
saleAttrIdAndValueName.value = ''
}
7.10 銷售屬性值的新增刪除業務
其實銷售屬性值和之前的新增屬性業務差不多。最重要的是熟悉資料的結構。步驟分為:元件收集資料->回撥中將資料整理後push到對應的陣列中。
7.10.1 新增按鈕與input框的切換
透過flag屬性。一上來是沒有的,點選按鈕新增。輸入框輸入完畢blur時再將flag變為false
//屬性值按鈕的點選事件
const toEdit = (row: SaleAttr) => {
//點選按鈕的時候,input元件不就不出來->編輯模式
row.flag = true
row.saleAttrValue = ''
}
7.10.2 收集&&新增屬性值
收集的資料有倆個
saleAttrValue:點選新增按鈕時初始化為空,收集輸入的資訊
baseSaleAttrId:所在的資料的id。由row給出
其餘做的事就是:非法資料的過濾
//表單元素失卻焦點的事件回撥
const toLook = (row: SaleAttr) => {
//整理收集的屬性的ID與屬性值的名字
const { baseSaleAttrId, saleAttrValue } = row
//整理成伺服器需要的屬性值形式
let newSaleAttrValue: SaleAttrValue = {
baseSaleAttrId,
saleAttrValueName: saleAttrValue as string,
}
//非法情況判斷
if ((saleAttrValue as string).trim() == '') {
ElMessage({
type: 'error',
message: '屬性值不能為空的',
})
return
}
//判斷屬性值是否在陣列當中存在
let repeat = row.spuSaleAttrValueList.find((item) => {
return item.saleAttrValueName == saleAttrValue
})
if (repeat) {
ElMessage({
type: 'error',
message: '屬性值重複',
})
return
}
//追加新的屬性值物件
row.spuSaleAttrValueList.push(newSaleAttrValue)
//切換為檢視模式
row.flag = false
}
7.10.3 刪除屬性值
7.12 儲存
整理資料+傳送請求+通知父元件更新頁面
//儲存按鈕的回撥
const save = async () => {
//整理引數
//發請求:新增SPU|更新已有的SPU
//成功
//失敗
//1:照片牆的資料
SpuParams.value.spuImageList = imgList.value.map((item: any) => {
return {
imgName: item.name, //圖片的名字
imgUrl: (item.response && item.response.data) || item.url,
}
})
//2:整理銷售屬性的資料
SpuParams.value.spuSaleAttrList = saleAttr.value
let result = await reqAddOrUpdateSpu(SpuParams.value)
if (result.code == 200) {
ElMessage({
type: 'success',
message: SpuParams.value.id ? '更新成功' : '新增成功',
})
//通知父元件切換場景為0
$emit('changeScene', {
flag: 0,
params: SpuParams.value.id ? 'update' : 'add',
})
} else {
ElMessage({
type: 'success',
message: SpuParams.value.id ? '更新成功' : '新增成功',
})
}
}
7.13 新增spu業務&&收尾工作
7.13.1 新增spu業務
新增spu業務我們要做什麼?收集資料(發請求得到的、自己新增的)放到對應的資料(儲存資料用的容器)中,發起請求(儲存按鈕已經做完了),更新頁面
- 父元件新增按鈕回撥
新增和修改按鈕不同的地方在於對於資料的來源不同,修改按鈕是一部分(spuParams)來源於父元件傳遞的資料,將他們與元件繫結,在資料上展示。新增按鈕父元件只需要傳遞category3Id就行,其他的自己收集。
//新增新的SPU按鈕的回撥
const addSpu = () => {
//切換為場景1:新增與修改已有SPU結構->SpuForm
scene.value = 1
//點選新增SPU按鈕,呼叫子元件的方法初始化資料
spu.value.initAddSpu(categoryStore.c3Id)
}
- 子元件收集資料
注意要對外暴露,讓父元件可以使用
//新增一個新的SPU初始化請求方法
const initAddSpu = async (c3Id: number | string) => {
//儲存三級分類的ID
SpuParams.value.category3Id = c3Id
//獲取全部品牌的資料
let result: AllTradeMark = await reqAllTradeMark()
let result1: HasSaleAttrResponseData = await reqAllSaleAttr()
//儲存資料
MYAllTradeMark.value = result.data
allSaleAttr.value = result1.data
}
//對外暴露
defineExpose({ initHasSpuData, initAddSpu })
- 整理資料與傳送請求
這部分透過儲存按鈕的回撥已經做完了。
7.13.2 清空資料
我們應該在每次新增spu前清空上次的資料。
//新增一個新的SPU初始化請求方法
const initAddSpu = async (c3Id: number | string) => {
//清空資料
Object.assign(SpuParams.value, {
category3Id: '', //收集三級分類的ID
spuName: '', //SPU的名字
description: '', //SPU的描述
tmId: '', //品牌的ID
spuImageList: [],
spuSaleAttrList: [],
})
//清空照片
imgList.value = []
//清空銷售屬性
saleAttr.value = []
saleAttrIdAndValueName.value = ''
、、、、、、
}
7.13.3 跳轉頁面
在新增和修改spu屬性後,跳轉的頁面不一樣。修改應該跳轉到當前頁面,新增應該跳轉到第一頁。如何區分?SpuParams.value.id屬性修改按鈕的SpuParams是自帶這個屬性的,而新增按鈕沒有這個屬性。因此在儲存的時候透過這個屬性告知父元件。
子元件:
//儲存按鈕的回撥
const save = async () => {
。。。。。。。
//通知父元件切換場景為0
$emit('changeScene', {
flag: 0,
params: SpuParams.value.id ? 'update' : 'add',
})
。。。。。。
}
父元件:
//子元件SpuForm繫結自定義事件:目前是讓子元件通知父元件切換場景為0
const changeScene = (obj: any) => {
//子元件Spuform點選取消變為場景0:展示已有的SPU
scene.value = obj.flag
if (obj.params == 'update') {
//更新留在當前頁
getHasSpu(pageNo.value)
} else {
//新增留在第一頁
getHasSpu()
}
}
7.14新增SKU的靜態
7.14.1 繫結回撥
//新增SKU按鈕的回撥
const addSku = (row: SpuData) => {
//點選新增SKU按鈕切換場景為2
scene.value = 2
}
7.14.2 靜態頁面
<template>
<el-form label-width="100px">
<el-form-item label="SKU名稱">
<el-input placeholder="SKU名稱"></el-input>
</el-form-item>
<el-form-item label="價格(元)">
<el-input placeholder="價格(元)" type="number"></el-input>
</el-form-item>
<el-form-item label="重量(g)">
<el-input placeholder="重量(g)" type="number"></el-input>
</el-form-item>
<el-form-item label="SKU描述">
<el-input placeholder="SKU描述" type="textarea"></el-input>
</el-form-item>
<el-form-item label="平臺屬性">
<el-form :inline="true">
<el-form-item label="記憶體" size="normal">
<el-select>
<el-option label="213"></el-option>
<el-option label="213"></el-option>
<el-option label="213"></el-option>
<el-option label="213"></el-option>
</el-select>
</el-form-item>
<el-form-item label="記憶體" size="normal">
<el-select>
<el-option label="213"></el-option>
<el-option label="213"></el-option>
<el-option label="213"></el-option>
<el-option label="213"></el-option>
</el-select>
</el-form-item>
<el-form-item label="記憶體" size="normal">
<el-select>
<el-option label="213"></el-option>
<el-option label="213"></el-option>
<el-option label="213"></el-option>
<el-option label="213"></el-option>
</el-select>
</el-form-item>
<el-form-item label="記憶體" size="normal">
<el-select>
<el-option label="213"></el-option>
<el-option label="213"></el-option>
<el-option label="213"></el-option>
<el-option label="213"></el-option>
</el-select>
</el-form-item>
</el-form>
</el-form-item>
<el-form-item label="銷售屬性">
<el-form :inline="true">
<el-form-item label="顏色" size="normal">
<el-select>
<el-option label="213"></el-option>
<el-option label="213"></el-option>
<el-option label="213"></el-option>
<el-option label="213"></el-option>
</el-select>
</el-form-item>
</el-form>
</el-form-item>
<el-form-item label="圖片名稱" size="normal">
<el-table border>
<el-table-column
type="selection"
width="80px"
align="center"
></el-table-column>
<el-table-column label="圖片"></el-table-column>
<el-table-column label="名稱"></el-table-column>
<el-table-column label="操作"></el-table-column>
</el-table>
</el-form-item>
<el-form-item>
<el-button type="primary" size="default">儲存</el-button>
<el-button type="primary" size="default" @click="cancel">取消</el-button>
</el-form-item>
</el-form>
</template>
7.14.3 取消按鈕
//自定義事件的方法
let $emit = defineEmits(['changeScene'])
//取消按鈕的回撥
const cancel = () => {
$emit('changeScene', { flag: 0, params: '' })
}
7.15 獲取新增SKU資料並展示
7.15.2 父元件新增按鈕回撥->呼叫子元件函式收集資料
父元件
//新增SKU按鈕的回撥
const addSku = (row: SpuData) => {
//點選新增SKU按鈕切換場景為2
scene.value = 2
//呼叫子元件的方法初始化新增SKU的資料
sku.value.initSkuData(categoryStore.c1Id, categoryStore.c2Id,row)
}
子元件暴露:
//對外暴露方法
defineExpose({ initSkuData })
7.15.2 子元件函式收集資料(平臺屬性、銷售屬性、圖片名稱)
//當前子元件的方法對外暴露
const initSkuData = async (
c1Id: number | string,
c2Id: number | string,
spu: any,
) => {
//獲取平臺屬性
let result: any = await reqAttr(c1Id, c2Id, spu.category3Id)
//獲取對應的銷售屬性
let result1: any = await reqSpuHasSaleAttr(spu.id)
//獲取照片牆的資料
let result2: any = await reqSpuImageList(spu.id)
//平臺屬性
attrArr.value = result.data
//銷售屬性
saleArr.value = result1.data
//圖片
imgArr.value = result2.data
}
7.15.3 模板展示(以圖片為例)
<el-form-item label="圖片名稱" size="normal">
<el-table border :data="imgArr" ref="table">
<el-table-column
type="selection"
width="80px"
align="center"
></el-table-column>
<el-table-column label="圖片">
<template #="{ row, $index }">
<img :src="row.imgUrl" alt="" style="width: 100px; height: 100px" />
</template>
</el-table-column>
<el-table-column label="名稱" prop="imgName"></el-table-column>
<el-table-column label="操作">
<template #="{ row, $index }">
<el-button type="primary" size="small">設定預設</el-button>
</template>
</el-table-column>
</el-table>
</el-form-item>
7.16 sku收集總資料
使用skuParams將sku模組的所有資料全都儲存下來
7.16.1 API&&Ts
API:
//追加一個新增的SKU地址
ADDSKU_URL = '/admin/product/saveSkuInfo',
}
//新增SKU的請求方法
export const reqAddSku = (data: SkuData) => {
request.post<any, any>(API.ADDSKU_URL, data)
}
ts:
export interface Attr {
attrId: number | string //平臺屬性的ID
valueId: number | string //屬性值的ID
}
export interface saleArr {
saleAttrId: number | string //屬性ID
saleAttrValueId: number | string //屬性值的ID
}
export interface SkuData {
category3Id: string | number //三級分類的ID
spuId: string | number //已有的SPU的ID
tmId: string | number //SPU品牌的ID
skuName: string //sku名字
price: string | number //sku價格
weight: string | number //sku重量
skuDesc: string //sku的描述
skuAttrValueList?: Attr[]
skuSaleAttrValueList?: saleArr[]
skuDefaultImg: string //sku圖片地址
}
7.16.2 收集父元件傳遞過來的資料
這部分資料包括三級id,spuid還有品牌id。由於是父元件傳遞過來的,我們可以直接在新增按鈕呼叫的那個函式中收集
//當前子元件的方法對外暴露
const initSkuData = async (
c1Id: number | string,
c2Id: number | string,
spu: any,
) => {
//收集資料
skuParams.category3Id = spu.category3Id
skuParams.spuId = spu.id
skuParams.tmId = spu.tmId
。。。。。。
}
7.16.3 input框收集資料
sku名稱、價格、重量、sku描述都是收集的使用者輸入的資料。我們直接使用v-model
7.16.4 收集平臺屬性以及銷售屬性
我們在資料繫結的時候將這倆個屬性所選擇的資料繫結到自身。之後整合資料的時候透過遍歷得到
7.16.5 img 資料&&設定預設圖片
//設定預設圖片的方法回撥
const handler = (row: any) => {
//點選的時候,全部圖片的的核取方塊不勾選
imgArr.value.forEach((item: any) => {
table.value.toggleRowSelection(item, false)
})
//選中的圖片才勾選
table.value.toggleRowSelection(row, true)
//收集圖片地址
skuParams.skuDefaultImg = row.imgUrl
}
7.17 完成新增sku
7.17.1 整合資料&&發請求
//收集SKU的引數
let skuParams = reactive<SkuData>({
//父元件傳遞過來的資料
category3Id: '', //三級分類的ID
spuId: '', //已有的SPU的ID
tmId: '', //SPU品牌的ID
//v-model收集
skuName: '', //sku名字
price: '', //sku價格
weight: '', //sku重量
skuDesc: '', //sku的描述
skuAttrValueList: [
//平臺屬性的收集
],
skuSaleAttrValueList: [
//銷售屬性
],
skuDefaultImg: '', //sku圖片地址
})
//儲存按鈕的方法
const save = async () => {
//整理引數
//平臺屬性
skuParams.skuAttrValueList = attrArr.value.reduce((prev: any, next: any) => {
if (next.attrIdAndValueId) {
let [attrId, valueId] = next.attrIdAndValueId.split(':')
prev.push({
attrId,
valueId,
})
}
return prev
}, [])
//銷售屬性
skuParams.skuSaleAttrValueList = saleArr.value.reduce(
(prev: any, next: any) => {
if (next.saleIdAndValueId) {
let [saleAttrId, saleAttrValueId] = next.saleIdAndValueId.split(':')
prev.push({
saleAttrId,
saleAttrValueId,
})
}
return prev
},
[],
)
//新增SKU的請求
let result: any = await reqAddSku(skuParams)
if (result.code == 200) {
ElMessage({
type: 'success',
message: '新增SKU成功',
})
//通知父元件切換場景為零
$emit('changeScene', { flag: 0, params: '' })
} else {
ElMessage({
type: 'error',
message: '新增SKU失敗',
})
}
}
7.17.2 bug
bug1:在傳送請求的時候返回時undefined:注意;這種情況一般是由於API的請求函式沒有寫返回值(格式化之後)
bug2:平臺屬性和銷售屬性收集不到。可能時element-plus自帶的table校驗。前面資料填的格式不對(比如重量和價格input確定是數字但是可以輸入字母e,這時候會導致錯誤)或者沒有填寫會導致後面的資料出問題。
7.18 sku展示
7.18.1 API&&type
API:
//檢視某一個已有的SPU下全部售賣的商品
SKUINFO_URL = '/admin/product/findBySpuId/',
//獲取SKU資料
export const reqSkuList = (spuId: number | string) => {
return request.get<any, SkuInfoData>(API.SKUINFO_URL + spuId)
}
TYPE
//獲取SKU資料介面的ts型別
export interface SkuInfoData extends ResponseData {
data: SkuData[]
}
7.18.2 繫結點選函式&&回撥
//儲存全部的SKU資料
let skuArr = ref<SkuData[]>([])
let show = ref<boolean>(false)
//檢視SKU列表的資料
const findSku = async (row: SpuData) => {
let result: SkuInfoData = await reqSkuList(row.id as number)
if (result.code == 200) {
skuArr.value = result.data
//對話方塊顯示出來
show.value = true
}
}
7.18.3 模板展示
其實就是彈出一個對話方塊dialog,然後裡面是一個form
<!-- dialog對話方塊:展示已有的SKU資料 -->
<el-dialog v-model="show" title="SKU列表">
<el-table border :data="skuArr">
<el-table-column label="SKU名字" prop="skuName"></el-table-column>
<el-table-column label="SKU價格" prop="price"></el-table-column>
<el-table-column label="SKU重量" prop="weight"></el-table-column>
<el-table-column label="SKU圖片">
<template #="{ row, $index }">
<img
:src="row.skuDefaultImg"
style="width: 100px; height: 100px"
/>
</template>
</el-table-column>
</el-table>
</el-dialog>
7.19 刪除spu業務
7.19.1 API
type為any,因此沒有寫專門的type
//刪除已有的SPU
REMOVESPU_URL = '/admin/product/deleteSpu/',
//刪除已有的SPU
export const reqRemoveSpu = (spuId: number | string) => {
return request.delete<any, any>(API.REMOVESPU_URL + spuId)
}
7.19.2 繫結點選函式
7.19.3 回撥函式
//刪除已有的SPU按鈕的回撥
const deleteSpu = async (row: SpuData) => {
let result: any = await reqRemoveSpu(row.id as number)
if (result.code == 200) {
ElMessage({
type: 'success',
message: '刪除成功',
})
//獲取剩餘SPU資料
getHasSpu(records.value.length > 1 ? pageNo.value : pageNo.value - 1)
} else {
ElMessage({
type: 'error',
message: '刪除失敗',
})
}
}
7.20 spu業務完成
//路由元件銷燬前,清空倉庫關於分類的資料
onBeforeUnmount(() => {
categoryStore.$reset()
})
8 SKU模組
8.1 SKU靜態
<template>
<el-card>
<el-table border style="margin: 10px 0px">
<el-table-column type="index" label="序號" width="80px"></el-table-column>
<el-table-column
label="名稱"
width="80px"
show-overflow-tooltip
></el-table-column>
<el-table-column
label="描述"
width="300px"
show-overflow-tooltip
></el-table-column>
<el-table-column label="圖片" width="300px"></el-table-column>
<el-table-column label="重量" width="300px"></el-table-column>
<el-table-column label="價格" width="300px"></el-table-column>
<el-table-column
label="操作"
width="300px"
fixed="right"
></el-table-column>
</el-table>
<el-pagination
v-model:current-page="pageNo"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 30, 40]"
:background="true"
layout="prev, pager, next, jumper, ->,sizes,total "
:total="400"
/>
</el-card>
</template>
8.2 獲取展示資料
8.2.1 API&&TYPE
API:
//SKU模組介面管理
import request from '@/utils/request'
import type { SkuResponseData} from './type'
//列舉地址
enum API {
//獲取已有的商品的資料-SKU
SKU_URL = '/admin/product/list/',
}
//獲取商品SKU的介面
export const reqSkuList = (page: number, limit: number) => {
return request.get<any, SkuResponseData>(API.SKU_URL + `${page}/${limit}`)
}
type:
export interface ResponseData {
code: number
message: string
ok: boolean
}
//定義SKU物件的ts型別
export interface Attr {
id?: number
attrId: number | string //平臺屬性的ID
valueId: number | string //屬性值的ID
}
export interface saleArr {
id?: number
saleAttrId: number | string //屬性ID
saleAttrValueId: number | string //屬性值的ID
}
export interface SkuData {
category3Id?: string | number //三級分類的ID
spuId?: string | number //已有的SPU的ID
tmId?: string | number //SPU品牌的ID
skuName?: string //sku名字
price?: string | number //sku價格
weight?: string | number //sku重量
skuDesc?: string //sku的描述
skuAttrValueList?: Attr[]
skuSaleAttrValueList?: saleArr[]
skuDefaultImg?: string //sku圖片地址
isSale?: number //控制商品的上架與下架
id?: number
}
//獲取SKU介面返回的資料ts型別
export interface SkuResponseData extends ResponseData {
data: {
records: SkuData[]
total: number
size: number
current: number
orders: []
optimizeCountSql: boolean
hitCount: boolean
countId: null
maxLimit: null
searchCount: boolean
pages: number
}
}
8.2.2 元件獲取資料
import { ref, onMounted } from 'vue'
//引入請求
import { reqSkuList } from '@/api/product/sku'
//引入ts型別
import type {
SkuResponseData,
SkuData,
SkuInfoData,
} from '@/api/product/sku/type'
//分頁器當前頁碼
let pageNo = ref<number>(1)
//每一頁展示幾條資料
let pageSize = ref<number>(10)
let total = ref<number>(0)
let skuArr = ref<SkuData[]>([])
//元件掛載完畢
onMounted(() => {
getHasSku()
})
const getHasSku = async (pager = 1) => {
//當前分頁器的頁碼
pageNo.value = pager
let result: SkuResponseData = await reqSkuList(pageNo.value, pageSize.value)
if (result.code == 200) {
total.value = result.data.total
skuArr.value = result.data.records
}
}
8.2.3 展示資料(el-table)
<el-table border style="margin: 10px 0px" :data="skuArr">
<el-table-column type="index" label="序號" width="80px"></el-table-column>
<el-table-column
prop="skuName"
label="名稱"
width="80px"
show-overflow-tooltip
></el-table-column>
<el-table-column
prop="skuDesc"
label="描述"
width="300px"
show-overflow-tooltip
></el-table-column>
<el-table-column label="圖片" width="300px">
<template #="{ row, $index }">
<img
:src="row.skuDefaultImg"
alt=""
style="width: 100px; height: 100px"
/>
</template>
</el-table-column>
<el-table-column
label="重量"
width="300px"
prop="weight"
></el-table-column>
<el-table-column
label="價格"
width="300px"
prop="price"
></el-table-column>
<el-table-column label="操作" width="300px" fixed="right">
<el-button type="primary" size="small" icon="Top"></el-button>
<el-button type="primary" size="small" icon="Edit"></el-button>
<el-button type="primary" size="small" icon="InfoFilled"></el-button>
<el-button type="primary" size="small" icon="Delete"></el-button>
</el-table-column>
</el-table>
8.2.4 分頁器
//分頁器下拉選單發生變化觸發
const handler = () => {
getHasSku()
}
注意:在這裡切換頁碼和切換每頁資料條數的回撥不同是因為:它們都能對函式注入資料,切換頁碼注入的是點選的頁碼數,因此我們可以直接使用getHasSku作為他的回撥。切換每頁資料條數注入的是切換的頁碼條數,我們希望切換後跳轉到第一頁,因此使用handler,間接呼叫getHasSku。
8.3 上架下架按鈕
8.3.1 API&&TYPE
//上架
SALE_URL = '/admin/product/onSale/',
//下架的介面
CANCELSALE_URL = '/admin/product/cancelSale/',
//已有商品上架的請求
export const reqSaleSku = (skuId: number) => {
return request.get<any, any>(API.SALE_URL + skuId)
}
//下架的請求
export const reqCancelSale = (skuId: number) => {
return request.get<any, any>(API.CANCELSALE_URL + skuId)
}
type都是any
8.3.2 按鈕切換
根據資料切換
8.3.2 上架下架回撥
流程:發請求->更新頁面
//商品的上架與下架的操作
const updateSale = async (row: SkuData) => {
//如果當前商品的isSale==1,說明當前商品是上架的額狀態->更新為下架
//否則else情況與上面情況相反
if (row.isSale == 1) {
//下架操作
await reqCancelSale(row.id as number)
//提示資訊
ElMessage({ type: 'success', message: '下架成功' })
//發請求獲取當前更新完畢的全部已有的SKU
getHasSku(pageNo.value)
} else {
//下架操作
await reqSaleSku(row.id as number)
//提示資訊
ElMessage({ type: 'success', message: '上架成功' })
//發請求獲取當前更新完畢的全部已有的SKU
getHasSku(pageNo.value)
}
}
8.4 更新按鈕
更新按鈕這裡沒有業務。個人覺得是因為SKU的編寫在SPU已經做完了。防止業務邏輯混亂
//更新已有的SKU
const updateSku = () => {
ElMessage({ type: 'success', message: '程式設計師在努力的更新中....' })
}
8.5 商品詳情靜態搭建
8.5.1 Drawer 抽屜
描述:撥出一個臨時的側邊欄, 可以從多個方向撥出
//控制抽屜顯示與隱藏的欄位
let drawer = ref<boolean>(false)
//檢視商品詳情按鈕的回撥
const findSku = async (row: SkuData) => {
//抽屜展示出來
drawer.value = true
}
8.5.2 Layout 佈局
透過基礎的 24 分欄,迅速簡便地建立佈局。
、
效果圖:
8.5.3 輪播圖 carousel
注意:把對應的style也複製過來
8.6 商品詳情展示業務
8.6.1 API&&TYPE
API
//獲取商品詳情的介面
SKUINFO_URL = '/admin/product/getSkuInfo/',
//獲取商品詳情的介面
export const reqSkuInfo = (skuId: number) => {
return request.get<any, SkuInfoData>(API.SKUINFO_URL + skuId)
}
type
//獲取SKU商品詳情介面的ts型別
export interface SkuInfoData extends ResponseData {
data: SkuData
}
8.6.2 發請求&&儲存資料
let skuInfo = ref<any>({})
//檢視商品詳情按鈕的回撥
const findSku = async (row: SkuData) => {
//抽屜展示出來
drawer.value = true
//獲取已有商品詳情資料
let result: SkuInfoData = await reqSkuInfo(row.id as number)
//儲存已有的SKU
skuInfo.value = result.data
}
8.6.3 展示資料(銷售屬性為例)
8.7 刪除模組
注:忘記寫了,後面才想起來。簡短寫一下思路
API->繫結點選事件->發請求
比較簡單。
8.8 小結
這模組的思路其實都比較簡單。無外乎API(type),元件內發請求拿資料、將資料放到模板中。再加上一個對倉庫的處理。
這部分真正的難點也是最值得學習的點在於
1:type的書寫
2:對資料結構的理解(可以將請求回來的資料放到正確的位置上)
3:element-plus元件的使用。
其實現在看來這部分模組做的事情就是我們前端人的一些縮影。思路不難,難在瑣碎的工作中要處理的各種各樣的東西。
9 使用者管理模組
9.1 靜態搭建
主要是el-form、el-pagination
<template>
<el-card style="height: 80px">
<el-form :inline="true" class="form">
<el-form-item label="使用者名稱:">
<el-input placeholder="請你輸入搜尋使用者名稱"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" size="default">搜尋</el-button>
<el-button type="primary" size="default" @click="reset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card style="margin: 10px 0px">
<el-button type="primary" size="default">新增使用者</el-button>
<el-button type="primary" size="default">批次刪除</el-button>
<!-- table展示使用者資訊 -->
<el-table style="margin: 10px 0px" border>
<el-table-column type="selection" align="center"></el-table-column>
<el-table-column label="#" align="center" type="index"></el-table-column>
<el-table-column label="ID" align="center"></el-table-column>
<el-table-column
label="使用者名稱字"
align="center"
show-overflow-tooltip
></el-table-column>
<el-table-column
label="使用者名稱稱"
align="center"
show-overflow-tooltip
></el-table-column>
<el-table-column
label="使用者角色"
align="center"
show-overflow-tooltip
></el-table-column>
<el-table-column
label="建立時間"
align="center"
show-overflow-tooltip
></el-table-column>
<el-table-column
label="更新時間"
align="center"
show-overflow-tooltip
></el-table-column>
<el-table-column
label="操作"
width="300px"
align="center"
></el-table-column>
</el-table>
<!-- 分頁器 -->
<el-pagination
v-model:current-page="pageNo"
v-model:page-size="pageSize"
:page-sizes="[5, 7, 9, 11]"
:background="true"
layout="prev, pager, next, jumper,->,sizes,total"
:total="400"
/>
</el-card>
</template>
9.2 使用者管理基本資訊展示
9.2.1 API&&type
//使用者管理模組的介面
import request from '@/utils/request'
import type { UserResponseData } from './type'
//列舉地址
enum API {
//獲取全部已有使用者賬號資訊
ALLUSER_URL = '/admin/acl/user/',
}
//獲取使用者賬號資訊的介面
export const reqUserInfo = (page: number, limit: number) => {
return request.get<any, UserResponseData>(
API.ALLUSER_URL + `${page}/${limit}`,
)
}
//賬號資訊的ts型別
export interface ResponseData {
code: number
message: string
ok: boolean
}
//代表一個賬號資訊的ts型別
export interface User {
id?: number
createTime?: string
updateTime?: string
username?: string
password?: string
name?: string
phone?: null
roleName?: string
}
//陣列包含全部的使用者資訊
export type Records = User[]
//獲取全部使用者資訊介面返回的資料ts型別
export interface UserResponseData extends ResponseData {
data: {
records: Records
total: number
size: number
current: number
pages: number
}
}
9.2.2 傳送請求(onMounted)
//使用者總個數
let total = ref<number>(0)
//儲存全部使用者的陣列
let userArr = ref<Records>([])
onMounted(() => {
getHasUser()
})
//獲取全部已有的使用者資訊
const getHasUser = async (pager = 1) => {
//收集當前頁碼
pageNo.value = pager
let result: UserResponseData = await reqUserInfo(
pageNo.value,
pageSize.value,
/* keyword.value, */
)
if (result.code == 200) {
total.value = result.data.total
userArr.value = result.data.records
}
}
9.2.3 模板展示資料
9.2.4 分頁器倆個函式回撥
//分頁器下拉選單的自定義事件的回撥
const handler = () => {
getHasUser()
}
9.3 新增與修改使用者靜態
<!-- 抽屜結構:完成新增新的使用者賬號|更新已有的賬號資訊 -->
<el-drawer v-model="drawer">
<!-- 頭部標題:將來文字內容應該動態的 -->
<template #header>
<h4>新增使用者</h4>
</template>
<!-- 身體部分 -->
<template #default>
<el-form>
<el-form-item label="使用者姓名">
<el-input placeholder="請您輸入使用者姓名"></el-input>
</el-form-item>
<el-form-item label="使用者暱稱">
<el-input placeholder="請您輸入使用者暱稱"></el-input>
</el-form-item>
<el-form-item label="使用者密碼">
<el-input placeholder="請您輸入使用者密碼"></el-input>
</el-form-item>
</el-form>
</template>
<template #footer>
<div style="flex: auto">
<el-button>取消</el-button>
<el-button type="primary">確定</el-button>
</div>
</template>
</el-drawer>
注意繫結的是新增使用者以及修改使用者的回撥
9.4 新賬號新增業務
9.4.1 API&&TYPE
API:
新增和修改的請求封裝成一個。
//新增一個新的使用者賬號
ADDUSER_URL = '/admin/acl/user/save',
//更新已有的使用者賬號
UPDATEUSER_URL = '/admin/acl/user/update',
//新增使用者與更新已有使用者的介面
export const reqAddOrUpdateUser = (data: User) => {
//攜帶引數有ID更新
if (data.id) {
return request.put<any, any>(API.UPDATEUSER_URL, data)
} else {
return request.post<any, any>(API.ADDUSER_URL, data)
}
}
type
//代表一個賬號資訊的ts型別
export interface User {
id?: number
createTime?: string
updateTime?: string
username?: string
password?: string
name?: string
phone?: null
roleName?: string
}
9.4.2 元件收集資料
//收集使用者資訊的響應式資料
let userParams = reactive<User>({
username: '',
name: '',
password: '',
})
9.4.3 發起請求
//儲存按鈕的回撥
const save = async () => {
//儲存按鈕:新增新的使用者|更新已有的使用者賬號資訊
let result: any = await reqAddOrUpdateUser(userParams)
//新增或者更新成功
if (result.code == 200) {
//關閉抽屜
drawer.value = false
//提示訊息
ElMessage({
type: 'success',
message: userParams.id ? '更新成功' : '新增成功',
})
//獲取最新的全部賬號的資訊
getHasUser(userParams.id ? pageNo.value : 1)
} else {
//關閉抽屜
drawer.value = false
//提示訊息
ElMessage({
type: 'error',
message: userParams.id ? '更新失敗' : '新增失敗',
})
}
}
9.4.4 新增使用者按鈕&&取消按鈕
新增使用者按鈕:我們在點選新增使用者按鈕的時候,先把之前的使用者資料清空
//新增使用者按鈕的回撥
const addUser = () => {
//抽屜顯示出來
drawer.value = true
//清空資料
Object.assign(userParams, {
id: 0,
username: '',
name: '',
password: '',
})
}
取消按鈕:
點選取消按鈕之後:關閉抽屜
//取消按鈕的回撥
const cancel = () => {
//關閉抽屜
drawer.value = false
}
9.5 表單校驗功能
9.5.1 表單繫結校驗資訊
注意點:注意表單FORM與表格Table的區別。
主要還是收集與展示資料的區別。
表單繫結的:model="userParams"是資料,prop="username"是屬性,繫結是為了對錶單進行驗證。
表格繫結的data是要顯示的資料,item項的prop也是要展示的資料。
9.5.2 校驗規則
//校驗使用者名稱字回撥函式
const validatorUsername = (rule: any, value: any, callBack: any) => {
//使用者名稱字|暱稱,長度至少五位
if (value.trim().length >= 5) {
callBack()
} else {
callBack(new Error('使用者名稱字至少五位'))
}
}
//校驗使用者名稱字回撥函式
const validatorName = (rule: any, value: any, callBack: any) => {
//使用者名稱字|暱稱,長度至少五位
if (value.trim().length >= 5) {
callBack()
} else {
callBack(new Error('使用者暱稱至少五位'))
}
}
const validatorPassword = (rule: any, value: any, callBack: any) => {
//使用者名稱字|暱稱,長度至少五位
if (value.trim().length >= 6) {
callBack()
} else {
callBack(new Error('使用者密碼至少六位'))
}
}
//表單校驗的規則物件
const rules = {
//使用者名稱字
username: [{ required: true, trigger: 'blur', validator: validatorUsername }],
//使用者暱稱
name: [{ required: true, trigger: 'blur', validator: validatorName }],
//使用者的密碼
password: [{ required: true, trigger: 'blur', validator: validatorPassword }],
}
9.5.3 確保校驗透過再發請求
先獲取form元件的例項,在呼叫form元件的方法validate()
//獲取form元件例項
let formRef = ref<any>()
//儲存按鈕的回撥
const save = async () => {
//點選儲存按鈕的時候,務必需要保證表單全部複合條件在去發請求
await formRef.value.validate()
。。。。。。
}
9.5.4 再次校驗前先清空上次的校驗展示
使用nextTick是因為第一次的時候還沒有formRef例項。
//新增使用者按鈕的回撥
const addUser = () => {
。。。。。。
//清除上一次的錯誤的提示資訊
nextTick(() => {
formRef.value.clearValidate('username')
formRef.value.clearValidate('name')
formRef.value.clearValidate('password')
})
}
9.6 更新賬號業務
9.6.1 抽屜結構變化分析
標題應該該為更新使用者,沒有輸入密碼。因為修改業務時我們需要用到使用者id,因此再修改按鈕儲存賬號資訊賦值了使用者的id。
我們根據這個id來決定我們的介面。
初始化使用者id:
我們再修改的時候將row的值複製給userParams,因此在展示抽屜的時候就會變換
//更新已有的使用者按鈕的回撥
//row:即為已有使用者的賬號資訊
const updateUser = (row: User) => {
//抽屜顯示出來
drawer.value = true
//儲存收集已有的賬號資訊
Object.assign(userParams, row)
//清除上一次的錯誤的提示資訊
nextTick(() => {
formRef.value.clearValidate('username')
formRef.value.clearValidate('name')
})
}
9.6.1 其餘工作
- 新增按鈕回撥
- 清除上一次的錯誤的提示資訊
//更新已有的使用者按鈕的回撥
//row:即為已有使用者的賬號資訊
const updateUser = (row: User) => {
//抽屜顯示出來
drawer.value = true
//儲存收集已有的賬號資訊
Object.assign(userParams, row)
//清除上一次的錯誤的提示資訊
nextTick(() => {
formRef.value.clearValidate('username')
formRef.value.clearValidate('name')
})
}
3.更改當前帳號之後,應該重新登陸
window身上的方法,重新整理一次。
//儲存按鈕的回撥
const save = async () => {
。。。。。。。
//新增或者更新成功
。。。。。。。
//獲取最新的全部賬號的資訊
getHasUser(userParams.id ? pageNo.value : 1)
//瀏覽器自動重新整理一次
window.location.reload()
} 。。。。。。。
}
9.6.3 更改當前賬號再重新整理這一步到底發生了什麼?
首先,當你更改當前賬號再重新整理的時候,瀏覽器還是會往當前頁面跳轉
這時候路由前置守衛就會發生作用:
你會發現,此時你的token儲存在本地儲存裡面,所以是有的,username儲存在倉庫裡面,所以重新整理就沒了。這也是之前說的倉庫儲存的問題。此時你的路由守衛就會走到下面這部分
它會向倉庫發起獲取使用者資訊的請求,獲取成功後就放行了。
問題來了!!!為什麼修改當前賬戶之後就會跳轉到登陸頁面呢?
首先我們建立一個使用者
登陸後再修改:
跳轉到了login介面
此時來看一下倉庫:token和username都沒了。這是為什麼呢?
因此我們回過頭來看一下路由守衛,可以看出走到了下面的位置,清除了使用者相關的資料清空。也就是說:
結論:當我們修改了賬戶在重新整理之後,我們再路由守衛裡呼叫** await userStore.userInfo()**
語句會失敗(伺服器端會阻止),因此我們走到了**next({ path: '/login', query: { redirect: to.path } })**
這裡,跳轉到了login頁面。
補充:證明一下我們修改了賬戶之後伺服器會阻止我們登入。
此時修改一下路由守衛(做個標記)
重新整理一下,證明路由確實是從這走的
此時在修改路由守衛以及使用者資訊方法
修改完之後再發請求:
此時可以得出結論,在修改使用者資訊之後,向伺服器發起userInfo()請求確實會失敗,導致我們跳轉到login介面
9.7 分配角色靜態搭建
<el-form-item label="使用者姓名">
<el-input v-model="userParams.username" :disabled="true"></el-input>
</el-form-item>
<el-form-item label="職位列表">
<el-checkbox>
全選
</el-checkbox>
<!-- 顯示職位的的核取方塊 -->
<el-checkbox-group>
<el-checkbox
v-for="(role, index) in 10"
:key="index"
:label="index"
>
{{ index }}
</el-checkbox>
</el-checkbox-group>
</el-form-item>
9.8 分配角色業務
9.8.1 API&&TYPE
//獲取全部職位以及包含當前使用者的已有的職位
export const reqAllRole = (userId: number) => {
return request.get<any, AllRoleResponseData>(API.ALLROLEURL + userId)
}
//代表一個職位的ts型別
export interface RoleData {
id?: number
createTime?: string
updateTime?: string
roleName: string
remark: null
}
//全部職位的列表
export type AllRole = RoleData[]
//獲取全部職位的介面返回的資料ts型別
export interface AllRoleResponseData extends ResponseData {
data: {
assignRoles: AllRole
allRolesList: AllRole
}
}
9.8.2獲取&&儲存資料
//收集頂部核取方塊全選資料
let checkAll = ref<boolean>(false)
//控制頂部全選核取方塊不確定的樣式
let isIndeterminate = ref<boolean>(true)
//儲存全部職位的資料
let allRole = ref<AllRole>([])
//當前使用者已有的職位
let userRole = ref<AllRole>([])
//分配角色按鈕的回撥
const setRole = async (row: User) => {
//儲存已有的使用者資訊
Object.assign(userParams, row)
//獲取全部的職位的資料與當前使用者已有的職位的資料
let result: AllRoleResponseData = await reqAllRole(userParams.id as number)
if (result.code == 200) {
//儲存全部的職位
allRole.value = result.data.allRolesList
//儲存當前使用者已有的職位
userRole.value = result.data.assignRoles
//抽屜顯示出來
drawer1.value = true
}
}
9.8.3 展示資料
<!-- 抽屜結構:使用者某一個已有的賬號進行職位分配 -->
<el-drawer v-model="drawer1">
<template #header>
<h4>分配角色(職位)</h4>
</template>
<template #default>
<el-form>
<el-form-item label="使用者姓名">
<el-input v-model="userParams.username" :disabled="true"></el-input>
</el-form-item>
<el-form-item label="職位列表">
<el-checkbox
@change="handleCheckAllChange"
v-model="checkAll"
:indeterminate="isIndeterminate"
>
全選
</el-checkbox>
<!-- 顯示職位的的核取方塊 -->
<el-checkbox-group
v-model="userRole"
@change="handleCheckedCitiesChange"
>
<el-checkbox
v-for="(role, index) in allRole"
:key="index"
:label="role"
>
{{ role.roleName }}
</el-checkbox>
</el-checkbox-group>
</el-form-item>
</el-form>
</template>
</el-drawer>
詳細解釋:
全選部分:
@change:全選框點選時的回撥
v-model:繫結的資料,根據這個值決定是否全選
:indeterminate:不確定狀態,既沒有全選也沒有全不選
核取方塊部分:
v-for="(role, index) in allRole"
:遍歷allRole。
:label="role"
:收集的資料(勾上的資料)
v-model="userRole"
:繫結收集的資料,也就是收集的資料儲存到userRole中。
@change:勾選變化時的回撥
全選框勾選的回撥:
實現原理:函式會將勾選與否注入到val中,如果是,就將全部資料(allRole)賦值給選中的資料(userRole),選中的資料透過v-model實現頁面的同步變化。
//頂部的全部核取方塊的change事件
const handleCheckAllChange = (val: boolean) => {
//val:true(全選)|false(沒有全選)
userRole.value = val ? allRole.value : []
//不確定的樣式(確定樣式)
isIndeterminate.value = false
}
核取方塊
//頂部全部的核取方塊的change事件
const handleCheckedCitiesChange = (value: string[]) => {
//頂部核取方塊的勾選資料
//代表:勾選上的專案個數與全部的職位個數相等,頂部的核取方塊勾選上
checkAll.value = value.length === allRole.value.length
//不確定的樣式
isIndeterminate.value = value.length !== allRole.value.length
}
9.8.4 分配角色業務(給伺服器發請求)
- api&&type
//分配職位
export const reqSetUserRole = (data: SetRoleData) => {
return request.post<any, any>(API.SETROLE_URL, data)
}
//給使用者分配職位介面攜帶引數的ts型別
export interface SetRoleData {
roleIdList: number[]
userId: number
}
- 元件傳送請求
回撥綁在確認按鈕身上就可以了
//確定按鈕的回撥(分配職位)
const confirmClick = async () => {
//收集引數
let data: SetRoleData = {
userId: userParams.id as number,
roleIdList: userRole.value.map((item) => {
return item.id as number
}),
}
//分配使用者的職位
let result: any = await reqSetUserRole(data)
if (result.code == 200) {
//提示資訊
ElMessage({ type: 'success', message: '分配職務成功' })
//關閉抽屜
drawer1.value = false
//獲取更新完畢使用者的資訊,更新完畢留在當前頁
getHasUser(pageNo.value)
}
}
9.8 刪除&&批次刪除業務
9.8.1 API&TYPE
//刪除某一個賬號
DELETEUSER_URL = '/admin/acl/user/remove/',
//批次刪除的介面
DELETEALLUSER_URL = '/admin/acl/user/batchRemove',
//刪除某一個賬號的資訊
export const reqRemoveUser = (userId: number) => {
return request.delete<any, any>(API.DELETEUSER_URL + userId)
}
//批次刪除的介面
export const reqSelectUser = (idList: number[]) => {
return request.delete(API.DELETEALLUSER_URL, { data: idList })
}
9.8.2 刪除業務
- 繫結點選函式
- 回撥函式
//刪除某一個使用者
const deleteUser = async (userId: number) => {
let result: any = await reqRemoveUser(userId)
if (result.code == 200) {
ElMessage({ type: 'success', message: '刪除成功' })
getHasUser(userArr.value.length > 1 ? pageNo.value : pageNo.value - 1)
}
}
9.8.3 批次刪除業務
- 繫結點選函式
- table收集選中的資料
//table核取方塊勾選的時候會觸發的事件
const selectChange = (value: any) => {
selectIdArr.value = value
}
- 批次刪除回撥
//批次刪除按鈕的回撥
const deleteSelectUser = async () => {
//整理批次刪除的引數
let idsList: any = selectIdArr.value.map((item) => {
return item.id
})
//批次刪除的請求
let result: any = await reqSelectUser(idsList)
if (result.code == 200) {
ElMessage({ type: 'success', message: '刪除成功' })
getHasUser(userArr.value.length > 1 ? pageNo.value : pageNo.value - 1)
}
}
9.8.4 小bug
個人覺得這裡的批次刪除有個小bug,假設所有資料都可以刪除的話,那麼把最後一頁的資料都刪除掉,會使得頁面跳轉到當前頁而不是前一頁。在這裡因為admin不可刪除,如果以後遇到這樣的問題的時候要注意!。
9.9 搜尋與重置業務
9.9.1 搜尋業務
搜尋業務與獲取初始資料的請求是同一個,因此我們修改一下獲取初始業務的請求。更具是否寫道username來判斷。
//獲取使用者賬號資訊的介面
export const reqUserInfo = (page: number, limit: number, username: string) => {
return request.get<any, UserResponseData>(
API.ALLUSER_URL + `${page}/${limit}/?username=${username}`,
)
}
收集資料:
傳送請求
//搜尋按鈕的回撥
const search = () => {
//根據關鍵字獲取相應的使用者資料
getHasUser()
//清空關鍵字
keyword.value = ''
}
9.9.2重置業務
重置業務是透過呼叫setting倉庫實現的
import useLayOutSettingStore from '@/store/modules/setting'
//獲取模板setting倉庫
let settingStore = useLayOutSettingStore()
//重置按鈕
const reset = () => {
settingStore.refresh = !settingStore.refresh
}
具體的功能實現是在之前寫好的main元件裡實現的,透過監聽銷燬重建元件。
<template>
<!-- 路由元件出口的位置 -->
<router-view v-slot="{ Component }">
<transition name="fade">
<!-- 渲染layout一級路由的子路由 -->
<component :is="Component" v-if="flag" />
</transition>
</router-view>
</template>
//監聽倉庫內部的資料是否發生變化,如果發生變化,說明使用者點選過重新整理按鈕
watch(
() => layOutSettingStore.refresh,
() => {
//點選重新整理按鈕:路由元件銷燬
flag.value = false
nextTick(() => {
flag.value = true
})
},
)
10 角色管理模組
10.1 角色管理模組靜態搭建
還是熟悉的元件:el-card、el-table 、el-pagination、el-form
<template>
<el-card>
<el-form :inline="true" class="form">
<el-form-item label="職位搜尋">
<el-input placeholder="請你輸入搜尋職位關鍵字"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" size="default">搜尋</el-button>
<el-button type="primary" size="default">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card>
<el-button type="primary" size="default" icon="Plus">新增職位</el-button>
<el-table border style="margin: 10px 0px">
<el-table-column type="index" align="center" label="#"></el-table-column>
<el-table-column label="ID" align="center" prop="id"></el-table-column>
<el-table-column
label="職位名稱"
align="center"
prop="roleName"
show-overflow-tooltip
></el-table-column>
<el-table-column
label="建立時間"
align="center"
show-overflow-tooltip
prop="createTime"
></el-table-column>
<el-table-column
label="更新時間"
align="center"
show-overflow-tooltip
prop="updateTime"
></el-table-column>
<el-table-column label="操作" width="280px" align="center">
<!-- row:已有的職位物件 -->
<template #="{ row, $index }">
<el-button type="primary" size="small" icon="User">
分配許可權
</el-button>
<el-button type="primary" size="small" icon="Edit">編輯</el-button>
<el-popconfirm :title="`你確定要刪除${row.roleName}?`" width="260px">
<template #reference>
<el-button type="primary" size="small" icon="Delete">
刪除
</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
</el-card>
<el-pagination
v-model:current-page="pageNo"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 30, 40]"
:background="true"
layout="prev, pager, next, jumper,->,sizes,total"
:total="400"
@current-change="getHasRole"
@size-change="sizeChange"
/>
</template>
10.2 角色管理資料展示
10.2.1 API&&type
api:
//角色管理模組的的介面
import request from '@/utils/request'
import type { RoleResponseData, RoleData, MenuResponseData } from './type'
//列舉地址
enum API {
//獲取全部的職位介面
ALLROLE_URL = '/admin/acl/role/',
}
//獲取全部的角色
export const reqAllRoleList = (
page: number,
limit: number,
roleName: string,
) => {
return request.get<any, RoleResponseData>(
API.ALLROLE_URL + `${page}/${limit}/?roleName=${roleName}`,
)
}
type:
export interface ResponseData {
code: number
message: string
ok: boolean
}
//職位資料型別
export interface RoleData {
id?: number
createTime?: string
updateTime?: string
roleName: string
remark?: null
}
//全部職位的陣列的ts型別
export type Records = RoleData[]
//全部職位資料的相應的ts型別
export interface RoleResponseData extends ResponseData {
data: {
records: Records
total: number
size: number
current: number
orders: []
optimizeCountSql: boolean
hitCount: boolean
countId: null
maxLimit: null
searchCount: boolean
pages: number
}
}
10.2.2 元件獲取資料
//當前頁碼
let pageNo = ref<number>(1)
//一頁展示幾條資料
let pageSize = ref<number>(10)
//搜尋職位關鍵字
let keyword = ref<string>('')
//元件掛載完畢
onMounted(() => {
//獲取職位請求
getHasRole()
})
//獲取全部使用者資訊的方法|分頁器當前頁碼發生變化的回撥
const getHasRole = async (pager = 1) => {
//修改當前頁碼
pageNo.value = pager
let result: RoleResponseData = await reqAllRoleList(
pageNo.value,
pageSize.value,
keyword.value,
)
if (result.code == 200) {
total.value = result.data.total
allRole.value = result.data.records
}
}
10.2.3 表格資料
<el-table border style="margin: 10px 0px" :data="allRole">
<el-table-column
type="index"
align="center"
label="#"
></el-table-column>
<el-table-column label="ID" align="center" prop="id"></el-table-column>
<el-table-column
label="職位名稱"
align="center"
prop="roleName"
show-overflow-tooltip
></el-table-column>
<el-table-column
label="建立時間"
align="center"
show-overflow-tooltip
prop="createTime"
></el-table-column>
<el-table-column
label="更新時間"
align="center"
show-overflow-tooltip
prop="updateTime"
></el-table-column>
<el-table-column label="操作" width="280px" align="center">
<!-- row:已有的職位物件 -->
<template #="{ row, $index }">
<el-button type="primary" size="small" icon="User">
分配許可權
</el-button>
<el-button type="primary" size="small" icon="Edit">編輯</el-button>
<el-popconfirm
:title="`你確定要刪除${row.roleName}?`"
width="260px"
>
<template #reference>
<el-button type="primary" size="small" icon="Delete">
刪除
</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
10.2.4 分頁器資料
同樣的@current-change與@size-change函式回撥。
<el-pagination
v-model:current-page="pageNo"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 30, 40]"
:background="true"
layout="prev, pager, next, jumper,->,sizes,total"
:total="total"
@current-change="getHasRole"
@size-change="sizeChange"
/>
//下拉選單的回撥
const sizeChange = () => {
getHasRole()
}
10.2.5 搜尋按鈕
//搜尋按鈕的回撥
const search = () => {
//再次發請求根據關鍵字
getHasRole()
keyword.value = ''
}
10.2.6 重置按鈕
重置模組我在使用者管理模組仔細解釋過。
import useLayOutSettingStore from '@/store/modules/setting'
let settingStore = useLayOutSettingStore()
//重置按鈕的回撥
const reset = () => {
settingStore.refresh = !settingStore.refresh
}
10.3 新增&&修改職位
10.3.1 靜態
<!-- 新增職位與更新已有職位的結構:對話方塊 -->
<el-dialog v-model="dialogVisite" title="新增職位">
<el-form>
<el-form-item label="職位名稱">
<el-input placeholder="請你輸入職位名稱"></el-input>
</el-form-item>
</el-form>
<template #footer>
<el-button type="primary" size="default" @click="dialogVisite = false">
取消
</el-button>
<el-button type="primary" size="default">確定</el-button>
</template>
</el-dialog>
10.3.2 API&&TYPE
//新增崗位的介面地址
ADDROLE_URL = '/admin/acl/role/save',
//更新已有的職位
UPDATEROLE_URL = '/admin/acl/role/update',
//新增職位與更新已有職位介面
export const reqAddOrUpdateRole = (data: RoleData) => {
if (data.id) {
return request.put<any, any>(API.UPDATEROLE_URL, data)
} else {
return request.post<any, any>(API.ADDROLE_URL, data)
}
}
10.3.3 新增&&修改按鈕繫結點選函式
10.3.4 新增&&修改按鈕回撥
//新增職位按鈕的回撥
const addRole = () => {
//對話方塊顯示出來
dialogVisite.value = true
//清空資料
Object.assign(RoleParams, {
roleName: '',
id: 0,
})
//清空上一次表單校驗錯誤結果
nextTick(() => {
form.value.clearValidate('roleName')
})
}
//更新已有的職位按鈕的回撥
const updateRole = (row: RoleData) => {
//顯示出對話方塊
dialogVisite.value = true
//儲存已有的職位----帶有ID的
Object.assign(RoleParams, row)
//清空上一次表單校驗錯誤結果
nextTick(() => {
form.value.clearValidate('roleName')
})
}
10.3.5 表單校驗
:model:要校驗的資料
:rules:校驗的規則
ref:獲取表單例項,方便後面呼叫validate函式來確保校驗透過才放行
prop:繫結資料的屬性
//自定義校驗規則的回撥
const validatorRoleName = (rule: any, value: any, callBack: any) => {
if (value.trim().length >= 2) {
callBack()
} else {
callBack(new Error('職位名稱至少兩位'))
}
}
//職位校驗規則
const rules = {
roleName: [{ required: true, trigger: 'blur', validator: validatorRoleName }],
}
10.3.6 儲存按鈕的回撥
//確定按鈕的回撥
const save = async () => {
//表單校驗結果,結果透過在發請求、結果沒有透過不應該在發生請求
await form.value.validate()
//新增職位|更新職位的請求
let result: any = await reqAddOrUpdateRole(RoleParams)
if (result.code == 200) {
//提示文字
ElMessage({
type: 'success',
message: RoleParams.id ? '更新成功' : '新增成功',
})
//對話方塊顯示
dialogVisite.value = false
//再次獲取全部的已有的職位
getHasRole(RoleParams.id ? pageNo.value : 1)
}
}
10.4 分配角色許可權業務
10.4.1 API&&type(獲取全部選單)
//獲取全部的選單與按鈕的資料
ALLPERMISSTION = '/admin/acl/permission/toAssign/',
//獲取全部選單與按鈕許可權資料
export const reqAllMenuList = (roleId: number) => {
return request.get<any, MenuResponseData>(API.ALLPERMISSTION + roleId)
}
注意:type這裡MenuData與MenuList互相呼叫,適合這種樹狀的資料結構
//選單與按鈕資料的ts型別
export interface MenuData {
id: number
createTime: string
updateTime: string
pid: number
name: string
code: string
toCode: string
type: number
status: null
level: number
children?: MenuList
select: boolean
}
export type MenuList = MenuData[]
10.4.2 獲取資料
分配許可權按鈕:
獲取&&儲存資料
//準備一個陣列:陣列用於儲存勾選的節點的ID(四級的)
let selectArr = ref<number[]>([])
//已有的職位的資料
const setPermisstion = async (row: RoleData) => {
//抽屜顯示出來
drawer.value = true
//收集當前要分類許可權的職位的資料
Object.assign(RoleParams, row)
//根據職位獲取許可權的資料
let result: MenuResponseData = await reqAllMenuList(RoleParams.id as number)
if (result.code == 200) {
menuArr.value = result.data
// selectArr.value = filterSelectArr(menuArr.value, [])
}
}
10.4.3 展示資料
我們重點關注el-tree元件
data:展示的資料
show-checkbox:節點是否可被選擇
node-key:每個樹節點用來作為唯一標識的屬性,整棵樹應該是唯一的
default-expand-all:預設展開所有節點
default-checked-keys:預設勾選的節點的 key 的陣列
props:屬性: label:指定節點標籤為節點物件的某個屬性值 children:指定子樹為節點物件的某個屬性值
const defaultProps = {
//子樹為節點物件的children
children: 'children',
//節點標籤為節點物件的name屬性
label: 'name',
}
10.4.4 展示資料(已分配的許可權)
獲取已分配許可權的id,這裡我們只需要收集最後一層的id即可,因為元件會自動更具最後一層的選擇情況決定上層的選擇狀況。
注意:獲取最後一層id的函式filterSelectArr使用了遞迴。
//分配許可權按鈕的回撥
//已有的職位的資料
const setPermisstion = async (row: RoleData) => {
//抽屜顯示出來
drawer.value = true
//收集當前要分類許可權的職位的資料
Object.assign(RoleParams, row)
//根據職位獲取許可權的資料
let result: MenuResponseData = await reqAllMenuList(RoleParams.id as number)
if (result.code == 200) {
menuArr.value = result.data
selectArr.value = filterSelectArr(menuArr.value, [])
}
}
const filterSelectArr = (allData: any, initArr: any) => {
allData.forEach((item: any) => {
if (item.select && item.level == 4) {
initArr.push(item.id)
}
if (item.children && item.children.length > 0) {
filterSelectArr(item.children, initArr)
}
})
return initArr
}
10.4.5 API&&type(分配許可權)
//給相應的職位分配許可權
SETPERMISTION_URL = '/admin/acl/permission/doAssign/?',
//給相應的職位下發許可權
export const reqSetPermisstion = (roleId: number, permissionId: number[]) => {
return request.post(
API.SETPERMISTION_URL + `roleId=${roleId}&permissionId=${permissionId}`,
)
}
10.4.6 收集使用者分配的許可權(每個許可權的id)&&發請求
我們這裡收集主要用到了2個方法:getCheckedKeys、getHalfCheckedKeys。它們會返回已選擇以及半選擇使用者的id陣列
//抽屜確定按鈕的回撥
const handler = async () => {
//職位的ID
const roleId = RoleParams.id as number
//選中節點的ID
let arr = tree.value.getCheckedKeys()
//半選的ID
let arr1 = tree.value.getHalfCheckedKeys()
let permissionId = arr.concat(arr1)
//下發許可權
let result: any = await reqSetPermisstion(roleId, permissionId)
if (result.code == 200) {
//抽屜關閉
drawer.value = false
//提示資訊
ElMessage({ type: 'success', message: '分配許可權成功' })
//頁面重新整理
window.location.reload()
}
}
10.4.7刪除業務
API&&TYPE
//刪除已有的職位
export const reqRemoveRole = (roleId: number) => {
return request.delete<any, any>(API.REMOVEROLE_URL + roleId)
}
刪除的回撥
//刪除已有的職位
const removeRole = async (id: number) => {
let result: any = await reqRemoveRole(id)
if (result.code == 200) {
//提示資訊
ElMessage({ type: 'success', message: '刪除成功' })
getHasRole(allRole.value.length > 1 ? pageNo.value : pageNo.value - 1)
}
}
11 選單管理模組
11.1 模組初始介面
11.1.1 API&&type
API:
import request from '@/utils/request'
import type { PermisstionResponseData, MenuParams } from './type'
//列舉地址
enum API {
//獲取全部選單與按鈕的標識資料
ALLPERMISSTION_URL = '/admin/acl/permission',
}
//獲取選單資料
export const reqAllPermisstion = () => {
return request.get<any, PermisstionResponseData>(API.ALLPERMISSTION_URL)
}
TYPE:
注意:type這裡使用了巢狀
//資料型別定義
export interface ResponseData {
code: number
message: string
ok: boolean
}
//選單資料與按鈕資料的ts型別
export interface Permisstion {
id?: number
createTime: string
updateTime: string
pid: number
name: string
code: null
toCode: null
type: number
status: null
level: number
children?: PermisstionList
select: boolean
}
export type PermisstionList = Permisstion[]
//選單介面返回的資料型別
export interface PermisstionResponseData extends ResponseData {
data: PermisstionList
}
11.1.2 元件獲取初始資料
//儲存選單的資料
let PermisstionArr = ref<PermisstionList>([])
//元件掛載完畢
onMounted(() => {
getHasPermisstion()
})
//獲取選單資料的方法
const getHasPermisstion = async () => {
let result: PermisstionResponseData = await reqAllPermisstion()
if (result.code == 200) {
PermisstionArr.value = result.data
}
}
11.1.3 模板展示資料
<div>
<el-table
:data="PermisstionArr"
style="width: 100%; margin-bottom: 20px"
row-key="id"
border
>
<el-table-column label="名稱" prop="name"></el-table-column>
<el-table-column label="許可權值" prop="code"></el-table-column>
<el-table-column label="修改時間" prop="updateTime"></el-table-column>
<el-table-column label="操作">
<!-- row:即為已有的選單物件|按鈕的物件的資料 -->
<template #="{ row, $index }">
<el-button
type="primary"
size="small"
:disabled="row.level == 4 ? true : false"
>
{{ row.level == 3 ? '新增功能' : '新增選單' }}
</el-button>
<el-button
type="primary"
size="small"
:disabled="row.level == 1 ? true : false"
>
編輯
</el-button>
<el-button
type="primary"
size="small"
:disabled="row.level == 1 ? true : false"
>
刪除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
11.2 更新與新增選單功能
11.2.1 API&&TYPE
API:
//給某一級選單新增一個子選單
ADDMENU_URL = '/admin/acl/permission/save',
//更新某一個已有的選單
UPDATE_URL = '/admin/acl/permission/update',
//新增與更新選單的方法
export const reqAddOrUpdateMenu = (data: MenuParams) => {
if (data.id) {
return request.put<any, any>(API.UPDATE_URL, data)
} else {
return request.post<any, any>(API.ADDMENU_URL, data)
}
}
11.2.2 對話方塊靜態
<!-- 對話方塊元件:新增或者更新已有的選單的資料結構 -->
<el-dialog
v-model="dialogVisible"
>
<!-- 表單元件:收集新增與已有的選單的資料 -->
<el-form>
<el-form-item label="名稱">
<el-input
placeholder="請你輸入選單名稱"
></el-input>
</el-form-item>
<el-form-item label="許可權">
<el-input
placeholder="請你輸入許可權數值"
></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="save">確定</el-button>
</span>
</template>
</el-dialog>
11.2.3 收集資料
需要的引數一共是4個,其中code、name由v-model繫結的對話方塊收集。其餘倆個透過點選按鈕傳遞的引數收集。
//攜帶的引數
let menuData = reactive<MenuParams>({
code: '',
level: 0,
name: '',
pid: 0,
})
//新增選單按鈕的回撥
const addPermisstion = (row: Permisstion) => {
//清空資料
Object.assign(menuData, {
id: 0,
code: '',
level: 0,
name: '',
pid: 0,
})
//對話方塊顯示出來
dialogVisible.value = true
//收集新增的選單的level數值
menuData.level = row.level + 1
//給誰新增子選單
menuData.pid = row.id as number
}
//編輯已有的選單
const updatePermisstion = (row: Permisstion) => {
dialogVisible.value = true
//點選修改按鈕:收集已有的選單的資料進行更新
Object.assign(menuData, row)
}
11.2.4 傳送請求
//確定按鈕的回撥
const save = async () => {
//發請求:新增子選單|更新某一個已有的選單的資料
let result: any = await reqAddOrUpdateMenu(menuData)
if (result.code == 200) {
//對話方塊隱藏
dialogVisible.value = false
//提示資訊
ElMessage({
type: 'success',
message: menuData.id ? '更新成功' : '新增成功',
})
//再次獲取全部最新的選單的資料
getHasPermisstion()
}
}
11.3 刪除模組
11.3.1 API
//刪除已有的選單
DELETEMENU_URL = '/admin/acl/permission/remove/',
//刪除某一個已有的選單
export const reqRemoveMenu = (id: number) => {
return request.delete<any, any>(API.DELETEMENU_URL + id)
}
11.3.2 刪除點選函式
<el-popconfirm
:title="`你確定要刪除${row.name}?`"
width="260px"
@confirm="removeMenu(row.id)"
>
<template #reference>
<el-button
type="primary"
size="small"
:disabled="row.level == 1 ? true : false"
>
刪除
</el-button>
</template>
</el-popconfirm>
11.3.3 刪除的回撥
//刪除按鈕回撥
const removeMenu = async (id: number) => {
let result = await reqRemoveMenu(id)
if (result.code == 200) {
ElMessage({ type: 'success', message: '刪除成功' })
getHasPermisstion()
}
}
12 首頁模組
首頁模組比較簡單,程式碼量也少。這裡直接放上原始碼
<template>
<div>
<el-card>
<div class="box">
<img :src="userStore.avatar" alt="" class="avatar" />
<div class="bottom">
<h3 class="title">{{ getTime() }}好呀{{ userStore.username }}</h3>
<p class="subtitle">矽谷甄選運營平臺</p>
</div>
</div>
</el-card>
<div class="bottoms">
<svg-icon name="welcome" width="800px" height="400px"></svg-icon>
</div>
</div>
</template>
<script setup lang="ts">
import { getTime } from '@/utils/time'
//引入使用者相關的倉庫,獲取當前使用者的頭像、暱稱
import useUserStore from '@/store/modules/user'
//獲取儲存使用者資訊的倉庫物件
let userStore = useUserStore()
</script>
<style lang="scss" scoped>
.box {
display: flex;
.avatar {
width: 100px;
height: 100px;
border-radius: 50%;
}
.bottom {
margin-left: 20px;
.title {
font-size: 30px;
font-weight: 900;
margin-bottom: 30px;
}
.subtitle {
font-style: italic;
color: skyblue;
}
}
}
.bottoms {
margin-top: 10px;
display: flex;
justify-content: center;
}
</style>
13 setting按鈕模組
13.1 暗黑模式設定
13.1.1 暗黑模式靜態
這裡使用了el-switch元件,下面介紹一下屬性
@change:點選切換時的回撥
v-model:雙向繫結的資料,用來控制開關的切換
class:預設的類
style:樣式
active-ico、inactive-icon:開和關的圖示
inline-prompt:可以把圖示放在開關裡面
13.1.2 暗黑模式
//暗黑模式需要的樣式
import 'element-plus/theme-chalk/dark/css-vars.css'
13.1.3 切換的回撥
//收集開關的資料
let dark = ref<boolean>(false)
//switch開關的chang事件進行暗黑模式的切換
const changeDark = () => {
//獲取HTML根節點
let html = document.documentElement
//判斷HTML標籤是否有類名dark
dark.value ? (html.className = 'dark') : (html.className = '')
}
13.2 主題顏色切換
Element Plus 預設提供一套主題,也提供了相應的修改主題顏色的方法。我們要使用的時透過js來修改主題顏色
13.2.1 靜態搭建
使用了el-color-picker元件
@change:切換的回撥
v-model:繫結的資料
show-alpha:是否支援透明度選擇
predefine:預定義顏色(會在下面顯示)
13.2.2 點選切換回撥
//主題顏色的設定
const setColor = () => {
//通知js修改根節點的樣式物件的屬性與屬性值
const html = document.documentElement
html.style.setProperty('--el-color-primary', color.value)
}
13.2.3 預定義顏色展示
predefine:預定義顏色
const predefineColors = ref([
'#ff4500',
'#ff8c00',
'#ffd700',
'#90ee90',
'#00ced1',
'#1e90ff',
'#c71585',
'rgba(255, 69, 0, 0.68)',
'rgb(255, 120, 0)',
'hsv(51, 100, 98)',
'hsva(120, 40, 94, 0.5)',
'hsl(181, 100%, 37%)',
'hsla(209, 100%, 56%, 0.73)',
'#c7158577',
])
14 資料大屏
14.1 資料大屏初始靜態
14.1.1初始靜態
<div class="container">
<!-- 資料大屏展示內容區域 -->
<div class="screen" ref="screen">
<div class="top"><Top /></div>
<div class="bottom">
<div class="left">左側</div>
<div class="center">中間</div>
<div class="right">右側</div>
</div>
</div>
</div>
<style lang="scss" scoped>
.container {
width: 100vw;
height: 100vh;
background: url(./images/bg.png) no-repeat;
background-size: cover;
.screen {
position: fixed;
width: 1920px;
height: 1080px;
left: 50%;
top: 50%;
transform-origin: left top;
.top {
width: 100%;
height: 40px;
}
.bottom {
display: flex;
.right {
flex: 1;
display: flex;
flex-direction: column;
margin-left: 40px;
}
.left {
flex: 1;
height: 1040px;
display: flex;
flex-direction: column;
}
.center {
flex: 1.5;
display: flex;
flex-direction: column;
}
}
}
}
</style>
14.1.2 大屏適配的解決方案
<script setup lang="ts">
import { ref, onMounted } from 'vue'
//獲取資料大屏展示內容盒子的DOM元素
//引入頂部的子元件
import Top from './components/top/index.vue'
let screen = ref()
onMounted(() => {
screen.value.style.transform = `scale(${getScale()}) translate(-50%,-50%)`
})
//定義大屏縮放比例
function getScale(w = 1920, h = 1080) {
const ww = window.innerWidth / w
const wh = window.innerHeight / h
return ww < wh ? ww : wh
}
//監聽視口變化
window.onresize = () => {
screen.value.style.transform = `scale(${getScale()}) translate(-50%,-50%)`
}
</script>
14.2 頂部靜態
14.2.1 頂部靜態
<template>
<div class="top">
<div class="left">
<span class="lbtn" @click="goHome">首頁</span>
</div>
<div class="center">
<div class="title">智慧旅遊視覺化大資料平臺</div>
</div>
<div class="right">
<span class="rbtn">統計報告</span>
<span class="time">當前時間:{{ time }}</span>
</div>
</div>
</template>
<style lang="scss" scoped>
.top {
width: 100%;
height: 40px;
display: flex;
.left {
flex: 1.5;
background: url(../../images/dataScreen-header-left-bg.png) no-repeat;
background-size: cover;
.lbtn {
width: 150px;
height: 40px;
float: right;
background: url(../../images/dataScreen-header-btn-bg-l.png) no-repeat;
background-size: 100% 100%;
text-align: center;
line-height: 40px;
color: #29fcff;
font-size: 20px;
}
}
.center {
flex: 2;
.title {
width: 100%;
height: 74px;
background: url(../../images/dataScreen-header-center-bg.png) no-repeat;
background-size: 100% 100%;
text-align: center;
line-height: 74px;
color: #29fcff;
font-size: 30px;
}
}
.right {
flex: 1.5;
background: url(../../images/dataScreen-header-left-bg.png) no-repeat;
background-size: cover;
display: flex;
justify-content: space-between;
align-items: center;
.rbtn {
width: 150px;
height: 40px;
background: url(../../images/dataScreen-header-btn-bg-r.png) no-repeat;
background-size: 100% 100%;
text-align: center;
line-height: 40px;
color: #29fcff;
}
.time {
color: #29fcff;
font-size: 20px;
}
}
}
</style>
14.2.2 當前時間
- 安裝moment外掛
pnpm i moment
- 使用
import moment from 'moment'
let timer = ref(0)
//儲存當前時間
let time = ref(moment().format('YYYY年MM月DD日 hh:mm:ss'))
//元件掛載完畢更新當前的事件
onMounted(() => {
timer.value = setInterval(() => {
time.value = moment().format('YYYY年MM月DD日 hh:mm:ss')
}, 1000)
})
onBeforeUnmount(() => {
clearInterval(timer.value)
})
- 模板使用
14.2.3 頂部按鈕
//按鈕的點選回撥
const goHome = () => {
$router.push('/home')
}
14.3 左側的上面部分
14.3.1 左側部分劃分
父元件中對左側使用了垂直方向的彈性盒
.bottom {
display: flex;
.left {
flex: 1;
height: 1040px;
display: flex;
// 彈性方向:列方向
flex-direction: column;
.tourist {
flex: 1.2;
}
.sex {
flex: 1;
}
.age {
flex: 1;
}
}
}
14.3.2 左側上面部分的靜態
注意:在“可預約總量99999人”那裡使用了float: right;
,float對上面的塊級元素不會產生影響,因此不會飄上去。
<template>
<div class="box">
<div class="top">
<p class="title">實時遊客統計</p>
<p class="bg"></p>
<p class="right">
可預約總量
<span>99999</span>
人
</p>
</div>
<div class="number">
<span v-for="(item, index) in people" :key="index">{{ item }}</span>
</div>
<!-- 盒子將來echarts展示圖形圖示的節點 -->
<div class="charts" ref="charts">123</div>
</div>
</template>
<style lang="scss" scoped>
.box {
background: url(../../images/dataScreen-main-lb.png) no-repeat;
background-size: 100% 100%;
margin-top: 10px;
.top {
margin-left: 20px;
.title {
color: white;
font-size: 20px;
}
.bg {
width: 68px;
height: 7px;
background: url(../../images/dataScreen-title.png) no-repeat;
background-size: 100% 100%;
margin-top: 10px;
}
.right {
float: right;
color: white;
font-size: 20px;
span {
color: yellowgreen;
}
}
}
.number {
padding: 10px;
margin-top: 30px;
display: flex;
span {
flex: 1;
height: 40px;
text-align: center;
line-height: 40px;
background: url(../../images/total.png) no-repeat;
background-size: 100% 100%;
color: #29fcff;
}
}
.charts {
width: 100%;
height: 270px;
}
}
</style>
14.3.3 水球圖
- 安裝
pnpm i echarts
pnpm i echarts-liquidfill
- 使用
onMounted(() => {
//獲取echarts類的例項
let mycharts = echarts.init(charts.value)
//設定例項的配置項
mycharts.setOption({
//標題元件
title: {
text: '水球圖',
},
//x|y軸元件
xAxis: {},
yAxis: {},
//系列:決定你展示什麼樣的圖形圖示
series: {
type: 'liquidFill', //系列
data: [0.6, 0.4, 0.2], //展示的資料
waveAnimation: true, //動畫
animationDuration: 3,
animationDurationUpdate: 0,
radius: '90%', //半徑
outline: {
//外層邊框設定
show: true,
borderDistance: 8,
itemStyle: {
color: 'skyblue',
borderColor: '#294D99',
borderWidth: 8,
shadowBlur: 20,
shadowColor: 'rgba(0, 0, 0, 0.25)',
},
},
},
//佈局元件
grid: {
left: 0,
right: 0,
top: 0,
bottom: 0,
},
})
})
14.4 左側的中間部分
14.4.1 上面的樣式部分
<template>
<div class="box1">
<div class="title">
<p>男女比例</p>
<img src="../../images/dataScreen-title.png" alt="" />
</div>
<div class="sex">
<div class="man">
<img src="../../images/man.png" alt="" />
</div>
<div class="women">
<img src="../../images/woman.png" alt="" />
</div>
</div>
<div class="rate">
<p>男士58%</p>
<p>女士42%</p>
</div>
<div class="charts" ref="charts"></div>
</div>
</template>
<style scoped lang="scss">
.box1 {
width: 100%;
height: 100%;
background: url(../../images/dataScreen-main-cb.png) no-repeat;
background-size: 100% 100%;
margin: 20px 0px;
.title {
margin-left: 20px;
p {
color: white;
font-size: 20px;
}
}
.sex {
display: flex;
justify-content: center;
.man {
margin: 20px;
width: 111px;
height: 115px;
background: url(../../images/man-bg.png) no-repeat;
display: flex;
justify-content: center;
align-items: center;
}
.women {
margin: 20px;
width: 111px;
height: 115px;
background: url(../../images/woman-bg.png) no-repeat;
display: flex;
justify-content: center;
align-items: center;
}
}
.rate {
display: flex;
justify-content: center;
color: white;
p {
margin: 0 40px;
margin-top: 10px;
margin-bottom: -10px;
}
}
.charts {
height: 100px;
}
}
</style>
14.4.2 柱狀圖部分
import { ref, onMounted } from 'vue'
import * as echarts from 'echarts'
//獲取圖形圖示的DOM節點
let charts = ref()
onMounted(() => {
//初始化echarts例項
let mycharts = echarts.init(charts.value)
//設定配置項
mycharts.setOption({
//元件標題
title: {
// text: '男女比例', //主標題
textStyle: {
//主標題顏色
color: 'skyblue',
},
left: '40%',
},
//x|y
xAxis: {
show: false,
min: 0,
max: 100,
},
yAxis: {
show: false,
type: 'category',
},
series: [
// 這裡有倆個柱狀圖,下面的覆蓋上面的
{
type: 'bar',
data: [58],
barWidth: 20,
// 柱狀圖的層級
z: 100,
// 柱狀圖樣式
itemStyle: {
color: 'skyblue',
borderRadius: 20,
},
},
{
type: 'bar',
data: [100],
//柱狀圖寬度
barWidth: 20,
//調整女士柱條位置
barGap: '-100%',
itemStyle: {
color: 'pink',
borderRadius: 20,
},
},
],
grid: {
left: 60,
top: -20,
right: 60,
bottom: 0,
},
})
})
14.5 左側的下面部分
<template>
<div class="box2">
<div class="title">
<p>年齡比例</p>
<img src="../../images/dataScreen-title.png" alt="" />
</div>
<!-- 圖形圖示的容器 -->
<div class="charts" ref="charts"></div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
//引入echarts
import * as echarts from 'echarts'
let charts = ref()
//元件掛載完畢初始化圖形圖示
onMounted(() => {
let mychart = echarts.init(charts.value)
//設定配置項
let option = {
tooltip: {
trigger: 'item',
},
legend: {
right: 30,
top: 40,
orient: 'vertical', //圖例元件方向的設定
textStyle: {
color: 'white',
fontSize: 14,
},
},
series: [
{
name: 'Access From',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2,
},
label: {
show: true,
position: 'inside',
color: 'white',
},
labelLine: {
show: false,
},
data: [
{ value: 1048, name: '軍事' },
{ value: 735, name: '新聞' },
{ value: 580, name: '直播' },
{ value: 484, name: '娛樂' },
{ value: 300, name: '財經' },
],
},
],
//調整圖形圖示的位置
grid: {
left: 0,
top: 0,
right: 0,
bottom: 0,
},
}
mychart.setOption(option)
})
</script>
<style scoped lang="scss">
.box2 {
width: 100%;
height: 100%;
background: url(../../images/dataScreen-main-cb.png) no-repeat;
background-size: 100% 100%;
.title {
margin-left: 20px;
p {
color: white;
font-size: 20px;
}
}
.charts {
height: 260px;
}
}
</style>
14.6 中間的上面部分
<template>
<div class="box4" ref="map">我是地圖元件</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import * as echarts from 'echarts'
//獲取DOM元素
let map = ref()
//引入中國地圖的JSON資料
import chinaJSON from './china.json'
//註冊中國地圖
echarts.registerMap('china', chinaJSON as any)
onMounted(() => {
let mychart = echarts.init(map.value)
//設定配置項
mychart.setOption({
//地圖元件
geo: {
map: 'china', //中國地圖
roam: true, //滑鼠縮放的效果
//地圖的位置除錯
left: 100,
top: 150,
right: 100,
zoom: 1.2,
bottom: 0,
//地圖上的文字的設定
label: {
show: true, //文字顯示出來
color: 'white',
fontSize: 10,
},
itemStyle: {
//每一個多邊形的樣式
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: 'pink', // 0% 處的顏色
},
{
offset: 1,
color: 'hotpink', // 100% 處的顏色
},
],
global: false, // 預設為 false
},
opacity: 0.8,
},
//地圖高亮的效果
emphasis: {
itemStyle: {
color: 'red',
},
label: {
fontSize: 20,
},
},
},
//佈局位置
grid: {
left: 0,
top: 0,
right: 0,
bottom: 0,
},
series: [
{
type: 'lines', //航線的系列
data: [
{
coords: [
[87.617733, 43.792818], // 起點
[91.132212, 29.660361], // 終點
],
// 統一的樣式設定
lineStyle: {
color: 'red',
width: 5,
},
},
{
coords: [
[91.132212, 29.660361], // 起點
[100.132212, 25.660361], // 終點
],
// 統一的樣式設定
lineStyle: {
color: 'red',
width: 5,
},
},
{
coords: [
[100.132212, 25.660361], // 起點
[109.132212, 18.660361], // 終點
],
// 統一的樣式設定
lineStyle: {
color: 'red',
width: 5,
},
},
{
coords: [
[109.132212, 18.660361], // 起點
[117.132212, 25.660361], // 終點
],
// 統一的樣式設定
lineStyle: {
color: 'red',
width: 5,
},
},
{
coords: [
[117.132212, 25.660361], // 起點
[125.132212, 44.060361], // 終點
],
// 統一的樣式設定
lineStyle: {
color: 'red',
width: 5,
},
},
{
coords: [
[125.132212, 44.060361], // 起點
[116.405285, 39.904989], // 終點
],
// 統一的樣式設定
lineStyle: {
color: 'red',
width: 5,
},
},
{
coords: [
[116.405285, 39.904989], // 起點
[112.304436, 37.618179], // 終點
],
// 統一的樣式設定
lineStyle: {
color: 'red',
width: 5,
},
},
{
coords: [
[112.304436, 37.618179], // 起點
[106.504962, 29.533155], // 終點
],
// 統一的樣式設定
lineStyle: {
color: 'red',
width: 5,
},
},
{
coords: [
[106.504962, 29.533155], // 起點
[104.065735, 30.659462], // 終點
],
// 統一的樣式設定
lineStyle: {
color: 'red',
width: 5,
},
},
{
coords: [
[106.504962, 29.533155], // 起點
[104.065735, 30.659462], // 終點
],
// 統一的樣式設定
lineStyle: {
color: 'red',
width: 5,
},
},
{
coords: [
[104.065735, 30.659462], // 起點
[101.778916, 36.623178], // 終點
],
// 統一的樣式設定
lineStyle: {
color: 'red',
width: 5,
},
},
{
coords: [
[101.778916, 36.623178],
[87.617733, 43.792818],
],
// 統一的樣式設定
lineStyle: {
color: 'red',
width: 5,
},
},
],
//開啟動畫特效
effect: {
show: true,
symbol: 'arrow',
color: 'yellow',
symbolSize: 10,
},
},
],
})
})
</script>
<style lang="scss" scoped></style>
14.7 中間的下面部分
<template>
<div class="box5">
<div class="title">
<p>未來七天遊客數量趨勢圖</p>
<img src="../../images/dataScreen-title.png" alt="" />
</div>
<div class="charts" ref="line"></div>
</div>
</template>
<script setup lang="ts">
import * as echarts from 'echarts'
import { ref, onMounted } from 'vue'
//獲取圖形圖示的節點
let line = ref()
onMounted(() => {
let mycharts = echarts.init(line.value)
//設定配置項
mycharts.setOption({
//標題元件
title: {
text: '訪問量',
},
//x|y軸
xAxis: {
type: 'category',
//兩側不留白
boundaryGap: false,
//分割線不要
splitLine: {
show: false,
},
data: ['週一', '週二', '週三', '週四', '週五', '週六', '週日'],
//軸線的設定
axisLine: {
show: true,
},
//刻度
axisTick: {
show: true,
},
},
yAxis: {
splitLine: {
show: false,
},
//軸線的設定
axisLine: {
show: true,
},
//刻度
axisTick: {
show: true,
},
},
grid: {
left: 40,
top: 0,
right: 20,
bottom: 20,
},
//系列
series: [
{
type: 'line',
data: [120, 1240, 66, 2299, 321, 890, 1200],
//平滑曲線的設定
smooth: true,
//區域填充樣式
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: 'red', // 0% 處的顏色
},
{
offset: 1,
color: 'blue', // 100% 處的顏色
},
],
global: false, // 預設為 false
},
},
},
],
})
})
</script>
<style scoped lang="scss">
.box5 {
width: 100%;
height: 100%;
background: url(../../images/dataScreen-main-cb.png) no-repeat;
background-size: 100% 100%;
margin: 0px 20px;
.title {
margin-left: 10px;
p {
color: white;
font-size: 20px;
}
}
.charts {
height: calc(100% - 40px);
}
}
</style>
14.8 右側的上面部分
<template>
<div class="box6">
<div class="title">
<p>熱門景區排行</p>
<img src="../../images/dataScreen-title.png" alt="" />
</div>
<!-- 圖形圖示的容器 -->
<div class="charts" ref="charts"></div>
</div>
</template>
<script setup lang="ts">
import * as echarts from 'echarts'
import { ref, onMounted } from 'vue'
//獲取DOM節點
let charts = ref()
//元件掛載完畢
onMounted(() => {
//一個容器可以同時展示多種型別的圖形圖示
let mychart = echarts.init(charts.value)
//設定配置項
mychart.setOption({
//標題元件
title: {
//主標題
text: '景區排行',
link: 'http://www.baidu.com',
//標題的位置
left: '50%',
//主標題文字樣式
textStyle: {
color: 'yellowgreen',
fontSize: 20,
},
//子標題
subtext: '各大景區排行',
//子標題的樣式
subtextStyle: {
color: 'yellowgreen',
fontSize: 16,
},
},
//x|y軸元件
xAxis: {
type: 'category', //圖形圖示在x軸均勻分佈展示
},
yAxis: {},
//佈局元件
grid: {
left: 20,
bottom: 20,
right: 20,
},
//系列:決定顯示圖形圖示是哪一種的
series: [
{
type: 'bar',
data: [10, 20, 30, 40, 50, 60, 70],
//柱狀圖的:圖形上的文字標籤,
label: {
show: true,
//文字的位置
position: 'insideTop',
//文字顏色
color: 'yellowgreen',
},
//是否顯示背景顏色
showBackground: true,
backgroundStyle: {
//底部背景的顏色
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: 'black', // 0% 處的顏色
},
{
offset: 1,
color: 'blue', // 100% 處的顏色
},
],
global: false, // 預設為 false
},
},
//柱條的樣式
itemStyle: {
borderRadius: [10, 10, 0, 0],
//柱條顏色
color: function (data: any) {
//給每一個柱條這是背景顏色
let arr = [
'red',
'orange',
'yellowgreen',
'green',
'purple',
'hotpink',
'skyblue',
]
return arr[data.dataIndex]
},
},
},
{
type: 'line',
data: [10, 20, 30, 40, 50, 60, 90],
smooth: true, //平滑曲線
},
],
tooltip: {
backgroundColor: 'rgba(50,50,50,0.7)',
},
})
})
</script>
<style scoped lang="scss">
.box6 {
width: 100%;
height: 100%;
background: url(../../images/dataScreen-main-cb.png) no-repeat;
background-size: 100% 100%;
margin: 20px 0px;
.title {
margin-left: 5px;
p {
color: white;
font-size: 20px;
}
}
.charts {
height: calc(100% - 30px);
}
}
</style>
15 選單許可權
15.1 路由的拆分
15.1.1 路由分析
選單的許可權:
超級管理員賬號:admin atguigu123 擁有全部的選單、按鈕的許可權
飛行員賬號 矽谷333 111111 不包含許可權管理模組、按鈕的許可權並非全部按鈕
同一個專案:不同人(職位是不一樣的,他能訪問到的選單、按鈕的許可權是不一樣的)
一、目前整個專案一共多少個路由!!!
login(登入頁面)、
404(404一級路由)、
任意路由、
首頁(/home)、
資料大屏、
許可權管理(三個子路由)
商品管理模組(四個子路由)
1.1開發選單許可權
---第一步:拆分路由
靜態(常量)路由:大家都可以擁有的路由
login、首頁、資料大屏、404
非同步路由:不同的身份有的有這個路由、有的沒有
許可權管理(三個子路由)
商品管理模組(四個子路由)
任意路由:任意路由
1.2選單許可權開發思路
目前咱們的專案:任意使用者訪問大家能看見的、能操作的選單與按鈕都是一樣的(大家註冊的路由都是一樣的)
15.1.2 路由的拆分
//對外暴露配置路由(常量路由)
export const constantRoute = [
{
//登入路由
path: '/login',
component: () => import('@/views/login/index.vue'),
name: 'login', //命名路由
meta: {
title: '登入', //選單標題
hidden: true, //路由的標題在選單中是否隱藏
},
},
{
//登入成功以後展示資料的路由
path: '/',
component: () => import('@/layout/index.vue'),
name: 'layout',
meta: {
hidden: false,
},
redirect: '/home',
children: [
{
path: '/home',
component: () => import('@/views/home/index.vue'),
meta: {
title: '首頁',
hidden: false,
icon: 'HomeFilled',
},
},
],
},
{
path: '/404',
component: () => import('@/views/404/index.vue'),
name: '404',
meta: {
title: '404',
hidden: true,
},
},
{
path: '/screen',
component: () => import('@/views/screen/index.vue'),
name: 'Screen',
meta: {
hidden: false,
title: '資料大屏',
icon: 'Platform',
},
},
]
//非同步路由
export const asnycRoute = [
{
path: '/acl',
component: () => import('@/layout/index.vue'),
name: 'Acl',
meta: {
hidden: false,
title: '許可權管理',
icon: 'Lock',
},
redirect: '/acl/user',
children: [
{
path: '/acl/user',
component: () => import('@/views/acl/user/index.vue'),
name: 'User',
meta: {
hidden: false,
title: '使用者管理',
icon: 'User',
},
},
{
path: '/acl/role',
component: () => import('@/views/acl/role/index.vue'),
name: 'Role',
meta: {
hidden: false,
title: '角色管理',
icon: 'UserFilled',
},
},
{
path: '/acl/permission',
component: () => import('@/views/acl/permission/index.vue'),
name: 'Permission',
meta: {
hidden: false,
title: '選單管理',
icon: 'Monitor',
},
},
],
},
{
path: '/product',
component: () => import('@/layout/index.vue'),
name: 'Product',
meta: {
hidden: false,
title: '商品管理',
icon: 'Goods',
},
redirect: '/product/trademark',
children: [
{
path: '/product/trademark',
component: () => import('@/views/product/trademark/index.vue'),
name: 'Trademark',
meta: {
hidden: false,
title: '品牌管理',
icon: 'ShoppingCartFull',
},
},
{
path: '/product/attr',
component: () => import('@/views/product/attr/index.vue'),
name: 'Attr',
meta: {
hidden: false,
title: '屬性管理',
icon: 'CollectionTag',
},
},
{
path: '/product/spu',
component: () => import('@/views/product/spu/index.vue'),
name: 'Spu',
meta: {
hidden: false,
title: 'SPU管理',
icon: 'Calendar',
},
},
{
path: '/product/sku',
component: () => import('@/views/product/sku/index.vue'),
name: 'Sku',
meta: {
hidden: false,
title: 'SKU管理',
icon: 'Orange',
},
},
],
},
]
//任意路由
//任意路由
export const anyRoute = {
//任意路由
path: '/:pathMatch(.*)*',
redirect: '/404',
name: 'Any',
meta: {
title: '任意路由',
hidden: true,
icon: 'DataLine',
},
}
15.2 選單許可權的實現
15.2.1 獲取正確路由的方法
注意:這裡使用了遞迴。其次,這裡是淺複製,會改變原有的路由。因此還需要改進。
//矽谷333: routes['Product','Trademark','Sku']
let guigu333 = ['Product', 'Trademark', 'Sku'];
function filterAsyncRoute(asnycRoute, routes) {
return asnycRoute.filter(item => {
if (routes.includes(item.name)) {
if (item.children && item.children.length > 0) {
item.children = filterAsyncRoute(item.children, routes)
}
return true
}
})
}
//矽谷333需要展示的非同步路由
let guigu333Result = filterAsyncRoute(asnycRoute, guigu333);
console.log([...constRoute, ...guigu333Result, anyRoute], '矽谷333');
15.2.2 獲取路由
。。。。。。
import router from '@/router'
//引入路由(常量路由)
import { constantRoute, asnycRoute, anyRoute } from '@/router/routes'
//用於過濾當前使用者需要展示的非同步路由
function filterAsyncRoute(asnycRoute: any, routes: any) {
return asnycRoute.filter((item: any) => {
if (routes.includes(item.name)) {
if (item.children && item.children.length > 0) {
//矽谷333賬號:product\trademark\attr\sku
item.children = filterAsyncRoute(item.children, routes)
}
return true
}
})
}
//建立使用者小倉庫
const useUserStore = defineStore('User', {
//小倉庫儲存資料地方
state: (): UserState => {
return {
。。。。。。。
menuRoutes: constantRoute, //倉庫儲存生成選單需要陣列(路由)
us。。。。。。
}
},
//處理非同步|邏輯地方
actions: {
。。。。。。。
//獲取使用者資訊方法
async userInfo() {
//獲取使用者資訊進行儲存
const result: userInfoResponseData = await reqUserInfo()
if (result.code == 200) {
this.username = result.data.name
this.avatar = result.data.avatar
//計算當前使用者需要展示的非同步路由
const userAsyncRoute = filterAsyncRoute(asnycRoute, result.data.routes)
//選單需要的資料整理完畢
this.menuRoutes = [...constantRoute, ...userAsyncRoute, anyRoute]
//目前路由器管理的只有常量路由:使用者計算完畢非同步路由、任意路由動態追加
;[...userAsyncRoute, anyRoute].forEach((route: any) => {
router.addRoute(route)
})
return 'ok'
} else {
return Promise.reject(new Error(result.message))
}
},
。。。。。。
})
//對外暴露小倉庫
export default useUserStore
15.3 選單許可權的2個小問題
15.3.1 深複製
之前獲取需要的路由方法中使用的是淺複製,會改變原有的路由。因此我們這裡引入深複製的方法
//引入深複製方法
//@ts-expect-error
import cloneDeep from 'lodash/cloneDeep'
。。。。。。
//獲取使用者資訊方法
async userInfo() {
//獲取使用者資訊進行儲存
const result: userInfoResponseData = await reqUserInfo()
if (result.code == 200) {
this.username = result.data.name
this.avatar = result.data.avatar
//計算當前使用者需要展示的非同步路由
const userAsyncRoute = filterAsyncRoute(
cloneDeep(asnycRoute),
result.data.routes,
)
//選單需要的資料整理完畢
this.menuRoutes = [...constantRoute, ...userAsyncRoute, anyRoute]
//目前路由器管理的只有常量路由:使用者計算完畢非同步路由、任意路由動態追加
;[...userAsyncRoute, anyRoute].forEach((route: any) => {
router.addRoute(route)
})
return 'ok'
} else {
return Promise.reject(new Error(result.message))
}
},
15.3.2 路由載入問題
這樣配置路由後,如果你訪問的是非同步路由,會在重新整理的時候出現空白頁面。原因是非同步路由是非同步獲取的,載入的時候還沒有。因此我們可以在路由守衛檔案中改寫。這個的意思就是一直載入。
//使用者登入判斷
if (token) {
//登陸成功,訪問login。指向首頁
if (to.path == '/login') {
next('/')
} else {
//登陸成功訪問其餘的,放行
//有使用者資訊
if (username) {
//放行
next()
} else {
//如果沒有使用者資訊,在收尾這裡發請求獲取到了使用者資訊再放行
try {
//獲取使用者資訊
await userStore.userInfo()
//萬一重新整理的時候是非同步路由,有可能獲取到使用者資訊但是非同步路由沒有載入完畢,出現空白效果
next({ ...to })
} catch (error) {
//token過期|使用者手動處理token
//退出登陸->使用者相關的資料清空
await userStore.userLogout()
next({ path: '/login', query: { redirect: to.path } })
}
}
}
} else {
//使用者未登入
if (to.path == '/login') {
next()
} else {
next({ path: '/login', query: { redirect: to.path } })
}
}
16 按鈕許可權
對於不同的使用者,按鈕的的顯示與否
16.1 獲取使用者應有的按鈕
記得修改對應的type
//建立使用者相關的小倉庫
import { defineStore } from 'pinia'
//引入介面
import { reqLogin, reqUserInfo, reqLogout } from '@/api/user'
import type {
loginFormData,
loginResponseData,
userInfoResponseData,
} from '@/api/user/type'
import type { UserState } from './types/type'
import router from '@/router'
。。。。。。
//建立使用者小倉庫
const useUserStore = defineStore('User', {
//小倉庫儲存資料地方
state: (): UserState => {
return {
token: GET_TOKEN(), //使用者唯一標識token
menuRoutes: constantRoute, //倉庫儲存生成選單需要陣列(路由)
username: '',
avatar: '',
//儲存當前使用者是否包含某一個按鈕
buttons: [],
}
},
//處理非同步|邏輯地方
actions: {
。。。。。。
//獲取使用者資訊方法
async userInfo() {
//獲取使用者資訊進行儲存
const result: userInfoResponseData = await reqUserInfo()
if (result.code == 200) {
this.username = result.data.name
this.avatar = result.data.avatar
this.buttons = result.data.buttons
console.log(result)
//計算當前使用者需要展示的非同步路由
const userAsyncRoute = filterAsyncRoute(
cloneDeep(asnycRoute),
result.data.routes,
)
//選單需要的資料整理完畢
this.menuRoutes = [...constantRoute, ...userAsyncRoute, anyRoute]
//目前路由器管理的只有常量路由:使用者計算完畢非同步路由、任意路由動態追加
;[...userAsyncRoute, anyRoute].forEach((route: any) => {
router.addRoute(route)
})
return 'ok'
} else {
return Promise.reject(new Error(result.message))
}
},
。。。。。。
})
//對外暴露小倉庫
export default useUserStore
16.2 自定義指令指令
這個需要你在每個按鈕元素中使用v-has="btn.User.XXXX"去判斷。比v-if方便。不需要在元件內部引入倉庫
import pinia from '@/store'
import useUserStore from '@/store/modules/user'
const userStore = useUserStore(pinia)
export const isHasButton = (app: any) => {
//獲取對應的使用者倉庫
//全域性自定義指令:實現按鈕的許可權
app.directive('has', {
//代表使用這個全域性自定義指令的DOM|元件掛載完畢的時候會執行一次
mounted(el: any, options: any) {
//自定義指令右側的數值:如果在使用者資訊buttons陣列當中沒有
//從DOM樹上幹掉
//el就是dom元素
//options:傳入進來的值
if (!userStore.buttons.includes(options.value)) {
el.parentNode.removeChild(el)
}
},
})
}
17 打包成功
pnpm run build
注意,有些變數定義了未使用會報錯。
tsconfig.json:
本文由部落格一文多發平臺 OpenWrite 釋出!