vue3+TS從0到1手擼後臺管理系統

Heymar-10發表於2024-07-16

1.路由配置

1.1路由元件的雛形

src\views\home\index.vue(以home元件為例)
image.png

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
image.png

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 登入按鈕繫結回撥

image.png
回撥應該做的事情

const login =  () => {
  //點選登入按鈕以後幹什麼
  //通知倉庫發起請求
  //請求成功->路由跳轉
  //請求失敗->彈出登陸失敗資訊
}

2.2.2 倉庫store初始化

  1. 大倉庫(筆記只寫一次)

安裝pinia:pnpm i pinia@2.0.34
src\store\index.ts

//倉庫大倉庫
import { createPinia } from 'pinia'
//建立大倉庫
const pinia = createPinia()
//對外暴露:入口檔案需要安裝倉庫
export default pinia

  1. 使用者相關的小倉庫

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 小結

  1. Element-plus中ElNotification用法(彈窗):

引入:import { ElNotification } from 'element-plus'
使用:

//登入失敗的提示資訊
    ElNotification({
      type: 'error',
      message: (error as Error).message,
    })
  1. Element-plus中el-buttonloading屬性。
  2. pinia使用actions、state的方式和vuex不同:需要引入函式建立例項
  3. $router的使用:也需要引入函式建立例項
  4. 在actions中使用state的token資料:this.token
  5. 型別定義需要注意。
  6. 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 登入時間的判斷

  1. 封裝函式
//封裝函式:獲取當前時間段
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
}

  1. 使用(引入後)

image.png

  1. 效果

image.png

2.5 表單校驗規則

2.5.1 表單校驗

  1. 表單繫結項

image.png
:model:繫結的資料

//收集賬號與密碼資料
let loginForm = reactive({ username: 'admin', password: '111111' })

:rules:對應要使用的規則

//定義表單校驗需要的配置物件
const rules = {}

ref="loginForms":獲取表單元素

//獲取表單元素
let loginForms = ref()
  1. 表單元素繫結項

Form 元件提供了表單驗證的功能,只需為 rules 屬性傳入約定的驗證規則,並將 form-Item 的 prop 屬性設定為需要驗證的特殊鍵值即可
image.png

  1. 使用規則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',
    }, 
  ],
}
  1. 校驗規則透過後執行
const login = async () => {
  //保證全部表單項校驗透過
  await loginForms.value.validate()
	。。。。。。
}

2.5.2自定義表單校驗

  1. 修改使用規則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 },
  ],
}
  1. 自定義校驗規則函式
//自定義校驗規則函式
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子元件的搭建

頁面左上角的這部分,我們將它做成子元件,並且封裝方便維護以及修改。
image.png

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 遞迴元件生成動態選單

在這一部分,我們要根據路由生成左側的選單欄

  1. 父元件中寫好的子元件結構提取出去
      <!-- 展示選單 -->
      <!-- 滾動元件 -->
      <el-scrollbar class="scrollbar">
        <!-- 選單元件 -->
        <el-menu background-color="#001529" text-color="white">
          <!-- 更具路由動態生成選單 -->
          <Menu></Menu>
        </el-menu>
      </el-scrollbar>
  1. 動態選單子元件:src\layout\menu\index.vue
  2. 處理路由

因為我們要根據路由以及其子路由作為我們選單的一級|二級標題。因此我們要獲取路由資訊。

給路由中加入了路由元資訊meta:它包含了2個屬性:title以及hidden

{
  //登入路由
  path: '/login',
    component: () => import('@/views/login/index.vue'),
    name: 'login', //命名路由
    meta: {
    	title: '登入', //選單標題
      hidden: true, //路由的標題在選單中是否隱藏
      },
      },
  1. 倉庫引入路由並對路由資訊型別宣告(vue-router有對應函式)
//引入路由(常量路由)
import { constantRoute } from '@/router/routes'
。。。。。
//小倉庫儲存資料地方
state: (): UserState => {
  return {
    token: GET_TOKEN(), //使用者唯一標識token
    menuRoutes: constantRoute, //倉庫儲存生成選單需要陣列(路由)
}

image.png

  1. 父元件拿到倉庫路由資訊並傳遞給子元件
<script setup lang="ts">
。。。。。。
//引入選單元件
import Menu from './menu/index.vue'
//獲取使用者相關的小倉庫
import useUserStore from '@/store/modules/user'
let userStore = useUserStore()
</script>

image.png

  1. 子元件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 選單圖示

image.png

  1. 註冊圖示元件

因為我們要根據路由配置對應的圖示,也要為了後續方便更改。因此我們將所有的圖示註冊為全域性元件。(使用之前將分頁器以及向量圖註冊全域性元件的自定義外掛)(所有圖示全域性註冊的方法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)
    }
  },
}

  1. 給路由元資訊新增屬性: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',
        },
      },
    ],
  },
  1. 選單元件使用

以只有一個子路由的元件為例:

<!-- 有且只有一個子路由 -->
<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>

image.png

3.3.4 專案全部路由配置

  1. 全部路由配置(以許可權管理為例)
{
  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',
      },
    },
  ],
    },
  1. 新增路由跳轉函式

第三種情況我們使用元件遞迴,所以只需要給前面的2個新增函式
image.png
image.png

<script setup lang="ts">
。。。。。。
//獲取路由器物件
let $router = useRouter()
const goRoute = (vc: any) => {
  //路由跳轉
  $router.push(vc.index)
}
</script>
  1. layout元件

image.png

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 動畫 && 自動展示

  1. 將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>

  1. 自動展示

當頁面重新整理時,選單會自動收起。我們使用element-plus的**default-active **處理。$router.path為當前路由。
src\layout\index.vue
image.png

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 選單摺疊

  1. 摺疊變數

定義一個摺疊變數來判斷現在的狀態是否摺疊。因為這個變數同時給breadcrumb元件以及父元件layout使用,因此將這個變數定義在pinia中

//小倉庫:layout元件相關配置倉庫
import { defineStore } from 'pinia'

let useLayOutSettingStore = defineStore('SettingStore', {
  state: () => {
    return {
      fold: false, //使用者控制選單摺疊還是收起的控制
    }
  },
})

export default useLayOutSettingStore

  1. 麵包屑元件點選圖示切換狀態
<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>
。。。。。。

  1. layout元件根據fold狀態來修改個子元件的樣式(以左側選單為例)

image.png
繫結動態樣式修改scss
image.png

  1. 左側選單使用element-plus摺疊collapse屬性

image.png
效果圖:
image.png
注意:摺疊文字的時候會把圖示也摺疊起來。在menu元件中吧圖示放到template外面就可以。
image.png

3.4.3 頂部麵包屑動態展示

  1. 引入$route

注意$router和$route是不一樣的

<script setup lang="ts">
import { useRoute } from 'vue-router'
//獲取路由物件
let $route = useRoute()
//點選圖示的切換

</script>
  1. 結構展示

注意:使用了$route.matched函式,此函式能得到當前路由的資訊
image.png

  1. 首頁修改

訪問首頁時,因為它是二級路由,會遍歷出layout麵包屑,處理:刪除layout路由的title。再加上一個判斷
image.png

  1. 麵包屑點選跳轉

注意:將路由中的一級路由許可權管理以及商品管理重定向到第一個孩子,這樣點選跳轉的時候會定向到第一個孩子。
image.png

3.4.4 重新整理業務的實現

  1. 使用pinia定義一個變數作為標記

image.png

  1. 點選重新整理按鈕,修改標記

image.png

<script setup lang="ts">
//使用layout的小倉庫
import useLayOutSettingStore from '@/store/modules/setting'
let layoutSettingStore = useLayOutSettingStore()
//重新整理按鈕點選的回撥
const updateRefresh = () => {
  layoutSettingStore.refresh = !layoutSettingStore.refresh
}
</script>
  1. main元件檢測標記銷燬&重載入元件(nextTick

image.png

<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 全屏模式的實現

  1. 給全屏按鈕繫結函式

image.png

  1. 實現全屏效果(利用docment根節點的方法)

image-20240629231436529

//全屏按鈕點選的回撥
const fullScreen = () => {
  //DOM物件的一個屬性:可以用來判斷當前是不是全屏的模式【全屏:true,不是全屏:false】
  let full = document.fullscreenElement
  //切換成全屏
  if (!full) {
    //文件根節點的方法requestFullscreen實現全屏
    document.documentElement.requestFullscreen()
  } else {
    //退出全屏
    document.exitFullscreen()
  }

4.部分功能處理完善

登入這一塊大概邏輯,前端傳送使用者名稱密碼到後端,後端返回token,前端儲存,並且請求攔截器,請求頭有token就要攜帶token

image-20240629233534030

4.1 登入獲取使用者資訊(TOKEN)

登入之後頁面(home)上來就要獲取使用者資訊。並且將它使用到頁面中

  1. home元件掛載獲取使用者資訊
<script setup lang="ts">
//引入組合是API生命週期函式
import { onMounted } from 'vue'
import useUserStore from '@/store/modules/user'
let userStore = useUserStore()
onMounted(() => {
  userStore.userInfo()
})
</script>
  1. 小倉庫中定義使用者資訊以及type宣告

image.png

import type { RouteRecordRaw } from 'vue-router'
//定義小倉庫資料state型別
export interface UserState {
  token: string | null
  menuRoutes: RouteRecordRaw[]
  username: string
  avatar: string
}

  1. 請求頭新增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
})
  1. 小倉庫發請求並且拿到使用者資訊
    //獲取使用者資訊方法
    async userInfo() {
      //獲取使用者資訊進行儲存
      let result = await reqUserInfo()
      if (result.code == 200) {
        this.username = result.data.checkUser.username
        this.avatar = result.data.checkUser.avatar
      }
    },
  1. 更新tabbar的資訊(記得先引入並建立例項)

src\layout\tabbar\setting\index.vue
image.png
image.png

4.2 退出功能

  1. 退出登入繫結函式,呼叫倉庫函式

image.png

//退出登陸點選的回撥
const logout = () => {
  //第一件事:需要項伺服器發請求【退出登入介面】(我們這裡沒有)
  //第二件事:倉庫當中和關於使用者的相關的資料清空
  userStore.userLogout()
  //第三件事:跳轉到登陸頁面
}
  1. pinia倉庫
    //退出登入
    userLogout() {
      //當前沒有mock介面(不做):伺服器資料token失效
      //本地資料清空
      this.token = ''
      this.username = ''
      this.avatar = ''
      REMOVE_TOKEN()
    },
  1. 退出登入,路由跳轉

注意:攜帶的query引數方便下次登陸時直接跳轉到當時推出的介面
個人覺得這個功能沒什麼作用。但是可以學習方法

//退出登陸點選的回撥
const logout = () => {
  //第一件事:需要項伺服器發請求【退出登入介面】(我們這裡沒有)
  //第二件事:倉庫當中和關於使用者的相關的資料清空
  userStore.userLogout()
  //第三件事:跳轉到登陸頁面
  $router.push({ path: '/login', query: { redirect: $route.path } })
}
  1. 登入按鈕進行判斷

image.png

4.3 路由守衛

src\permisstion.ts(新建檔案)
main.ts引入
image.png

4.3.1 進度條

  1. 安裝

pnpm i nprogress

  1. 引入並使用
//路由鑑權:鑑權:專案當中路由能不能被訪問的許可權
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 其餘都可以

路由鑑權幾個注意點

  1. 獲取使用者小倉庫為什麼要匯入pinia?

image.png
個人理解:之前在app中是不需要匯入pinia的,是因為我們這次的檔案時寫在和main.ts同級的下面,所以我們使用的時候是沒有pinia的。而之前使用時app已經使用了pinia了,所以我們不需要匯入pina。

  1. 全域性路由守衛將獲取使用者資訊的請求放在了跳轉之前。實現了重新整理後使用者資訊丟失的功能。

4.4 真實介面替代mock介面

介面文件:
http://139.198.104.58:8209/swagger-ui.html
http://139.198.104.58:8212/swagger-ui.html#/

  1. 修改伺服器域名

將.env.development,.env.production .env.test,三個環境檔案下的伺服器域名寫為:
image.png

  1. 代理跨域
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/, ''),
        },
      },
    },
  }
})

  1. 修改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)
}

  1. 小倉庫(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

  1. 退出登入按鈕的點選函式修改

退出成功後再跳轉
image.png

  1. 路由跳轉判斷條件修改

src\permisstion.ts
也是退出成功後再跳轉
image.png

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

  1. 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}`)

  1. 獲取資料

我們獲取資料沒有放在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,下面組要講解屬性和注意點。

  1. data屬性:顯示的資料

比如我們這裡繫結的trademarkArr是個三個物件的陣列,就會多出來3行。image.png

  1. el-table-column的type屬性:對應列的型別。 如果設定了selection則顯示多選框; 如果設定了 index 則顯示該行的索引(從 1 開始計算); 如果設定了 expand 則顯示為一個可展開的按鈕

image.png

  1. el-table-column的prop屬性:欄位名稱 對應列內容的欄位名, 也可以使用 property屬性

注意:因為我們之前已經繫結了資料,所以在這裡直接使用資料的屬性tmName
image.png

  1. el-table-column的插槽

image.png
為什麼要使用插槽呢?因為prop屬性雖然能夠展示資料,但是他預設是div,如果我們的圖片使用prop展示的話,會展示圖片的路徑。因此如果想展示圖片或者按鈕,我們就要使用插槽
image.png
注意: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函式,因此能夠跳轉到正確的頁碼數
image.png

//獲取已有品牌的介面封裝為一個函式:在任何情況下向獲取資料,呼叫次函式即可
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 每頁展示資料條數

image.png
image.png

//當下拉選單發生變化的時候觸發此方法
//這個自定義事件,分頁器元件會將下拉選單選中資料返回
const sizeChange = () => {
  //當前每一頁的資料量發生變化的時候,當前頁碼歸1
  getHasTrademark()
  console.log(123)
}

同樣的這個函式也會返回一個引數。但是我們不需要使用這個引數,因此才另外寫一個回撥函式。

5.5 dialog對話方塊靜態搭建

image.png

  1. 對話方塊的標題&&顯示隱藏

v-model:屬性使用者控制對話方塊的顯示與隱藏的 true顯示 false隱藏
title:設定對話方塊左上角標題
image.png

  1. 表單項
    <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>
  1. 確定與取消按鈕
<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 收集新增品牌資料

  1. 定義資料
import type {
。。。。。。。
TradeMark,
} from '@/api/product/trademark/type'
//定義收集新增品牌資料
let trademarkParams = reactive<TradeMark>({
  tmName: '',
  logoUrl: '',
})
  1. 收集品牌名稱

image.png

  1. 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')
}
  1. 上傳圖片後,用圖片代替加號

image.png

5.4.3 新增品牌

  1. 點選確定按鈕回撥
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
  }
}
  1. 每次點選新增品牌的時候先情況之前的資料
//新增品牌按鈕的回撥
const addTrademark = () => {
  //對話方塊顯示
  dialogFormVisible.value = true
  //清空收集資料
  trademarkParams.tmName = ''
  trademarkParams.logoUrl = ''
}

5.6 修改品牌資料

  1. 繫結點選函式

其中的row就是當前的資料
image.png

  1. 回撥函式
//修改已有品牌的按鈕的回撥
//row:row即為當前已有的品牌
const updateTrademark = (row: TradeMark) => {
  //對話方塊顯示
  dialogFormVisible.value = true
  //ES6語法合併物件
  Object.assign(trademarkParams, row)
}
  1. 對確認按鈕回撥修改
const confirm = async () => {
  。。。。。。。
  if (result.code == 200) {
   。。。
    //彈出提示資訊
    ElMessage({
      。。。。
      message: trademarkParams.id ? '修改品牌成功' : '新增品牌成功',
    })
    //再次發請求獲取已有全部的品牌資料
    getHasTrademark(trademarkParams.id ? pageNo.value : 1)
  } else {
    //新增品牌失敗
    ElMessage({
      。。。。
      message: trademarkParams.id ? '修改品牌失敗' : '新增品牌失敗',
    })
    。。。。
  }
}
  1. 設定對話方塊標題

image.png

  1. 小問題

當我們修改操作之後再點選新增品牌,對話方塊的title依舊是修改品牌。怎麼是因為對話方塊的title是根據trademarkParams.id來的,我們之前新增品牌按鈕操作沒有對id進行清除。修改為如下就可

//新增品牌按鈕的回撥
const addTrademark = () => {
  //對話方塊顯示
  dialogFormVisible.value = true
  //清空收集資料
  trademarkParams.id = 0
  trademarkParams.tmName = ''
  trademarkParams.logoUrl = ''
}

5.7 品牌管理模組表單校驗

5.7.1 表單校驗(自定義規則校驗,可以簡略堪稱三步走)

  1. 繫結引數

image.png
:model:校驗的資料
:rules:校驗規則
ref="formRef":表單例項
image.png
prop:表單元素校驗的資料,可以直接使用表單繫結的資料。

  1. Rules
//表單校驗規則物件
const rules = {
  tmName: [
    //required:這個欄位務必校驗,表單項前面出來五角星
    //trigger:代表觸發校驗規則時機[blur、change]
    { required: true, trigger: 'blur', validator: validatorTmName },
  ],
  logoUrl: [{ required: true, validator: validatorLogoUrl }],
}
  1. 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 存在的一些問題

  1. 圖片校驗時機

因為img是圖片,不好判斷。因此使用表單的validate屬性,全部校驗,放在確認按鈕的回撥函式中

const confirm = async () => {
  //在你發請求之前,要對於整個表單進行校驗
  //呼叫這個方法進行全部表單相校驗,如果校驗全部透過,在執行後面的語法
  await formRef.value.validate()
 。。。。。。
}
  1. 清除校驗資訊

當圖片沒有上傳點選確認後會出來校驗的提示資訊,我們上傳圖片後校驗資訊應該消失。使用表單的clearValidate屬性

//圖片上傳成功鉤子
const handleAvatarSuccess: UploadProps['onSuccess'] = (
  。。。。。。
) => {
  。。。。。。。
  //圖片上傳成功,清除掉對應圖片校驗結果
  formRef.value.clearValidate('logoUrl')
}
  1. 清除校驗資訊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以及發請求。不過有些點要注意

  1. 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)

  1. 繫結函式

這裡使用了一個氣泡元件,@confirm繫結的就是回撥函式
image.png

  1. 回撥函式
//氣泡確認框確定按鈕的回撥
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 屬性管理模組的靜態元件

image.png

屬性管理分為上面部分的三級分類模組以及下面的新增屬性部分。我們將三級分類模組單獨提取出來做成全域性元件

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 二級分類流程

  1. 繫結函式

二級分類不是一上來就發生變化,而是要等一級分類確定好之後再傳送請求獲得資料。於是我們將這個傳送請求的回撥函式繫結在了一級分類的change屬性上
image.png
image.png

  1. 回撥函式
//此方法即為一級分類下拉選單的change事件(選中值的時候會觸發,保證一級分類ID有了)
const handler = () => {
  //通知倉庫獲取二級分類的資料
  categoryStore.getC2()
}
  1. pinia
//獲取二級分類的資料
    async getC2() {
      //獲取對應一級分類的下二級分類的資料
      const result: CategoryResponseData = await reqC2(this.c1Id)
      if (result.code == 200) {
        this.c2Arr = result.data
      }
    },
  1. 元件資料展示

image.png

  1. 三級元件同理

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(父元件)
image.png

6.5 已有屬性與屬性值展示image.png

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 新增屬性頁面的靜態展示

當點選新增屬性後:
image.png

6.6.1 定義變數控制頁面展示與隱藏

//定義card元件內容切換變數
let scene = ref<number>(0) //scene=0,顯示table,scene=1,展示新增與修改屬性結構

image.png

6.6.2 表單

image.png
image.png

6.6.3 按鈕

image.png

6.6.4 表格

image.png

6.6.5按鈕

image.png

6.6.6 三級分類禁用

當點選新增屬性之後,三級分類應該被禁用。因此使用props給子元件傳參
image.png
子元件:
二三級分類同理。
image.png

6.7 新增屬性&&修改屬性的介面型別

6.7.1修改屬性

image.png

6.7.2 新增屬性

image.png

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)

  1. 屬性名稱(attrName)

image.png

  1. 屬性值陣列(attrValueList)

我們給新增屬性值按鈕繫結一個回撥,點選的時候會往attrParams.attrValueList中新增一個空陣列。我們根據空陣列的數量生成input框,再將input的值與陣列中的值繫結。
image.png

//新增屬性值按鈕的回撥
const addAttrValue = () => {
  //點選新增屬性值按鈕的時候,向陣列新增一個屬性值物件
  attrParams.attrValueList.push({
    valueName: '',
    flag: true, //控制每一個屬性值編輯模式與切換模式的切換
  })
}

image.png

  1. 三級分類的id(categoryId)

三級分類的id(c3Id)在頁面1的新增屬性按鈕之前就有了,因此我們把它放到新增屬性按鈕的回撥身上
注意:每一次點選的時候,先清空一下資料再收集資料。防止下次點選時會顯示上次的資料

//新增屬性按鈕的回撥
const addAttr = () => {
  //每一次點選的時候,先清空一下資料再收集資料
  Object.assign(attrParams, {
    attrName: '', //新增的屬性的名字
    attrValueList: [
      //新增的屬性值陣列
    ],
    categoryId: categoryStore.c3Id, //三級分類的ID
    categoryLevel: 3, //代表的是三級分類
  })


  //切換為新增與修改屬性的結構
  scene.value = 1
}
  1. categoryLevel(固定的,無需收集)

6.8.2 傳送請求&&更新頁面

image.png
image.png

//儲存按鈕的回撥
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來決定哪個展示。
image.png
注意:flag放在哪?由於每一個屬性值物件都需要一個flag屬性,因此將flag的新增放在新增屬性值的按鈕的回撥上。(注意修改屬性值的type)

//新增屬性值按鈕的回撥
const addAttrValue = () => {
  //點選新增屬性值按鈕的時候,向陣列新增一個屬性值物件
  attrParams.attrValueList.push({
    valueName: '',
    flag: true, //控制每一個屬性值編輯模式與切換模式的切換
  })
  
}

src\api\product\attr\type.ts
image.png

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>([])

image.png

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 刪除按鈕

image.png

6.11屬性修改業務

6.11.1屬性修改業務

修改業務很簡單:當我們點選修改按鈕的時候,將修改的例項(row)傳遞給回撥函式。回撥函式:首先跳轉到第二頁面,第二頁面是根據attrParams值生成的,我們跳轉的時候將例項的值傳遞給attrParams
image.png

//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刪除按鈕

  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)
  1. 繫結點選函式&&氣泡彈出框

image.png

  1. 回撥函式(功能實現&&重新整理頁面)
//刪除某一個已有的屬性方法回撥
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模組的靜態頁面

image.png

<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按鈕

image.png

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模組需要在三個場景進行切換,全都放在一個元件裡面的話會顯得很臃腫。因此我們將它放到三個元件當中。
image.png
使用v-show來展示頁面:v-if是銷燬元件,v-show是隱藏元件。在初載入的時候v-if比較快,但是在頻繁切換的時候v-if任務重。
image.png

7.3.2 SPU場景一子元件靜態

image.png

<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,因此涉及到父子元件通訊。這裡使用自定義事件。
父元件:
image.png
子元件:
image.png

//取消按鈕的回撥
const cancel = () => {
  $emit('changeScene', 0)
}

7.4 SPU模組API&&TS型別(修改&&新增)

修改和新增的頁面是差不多的。頁面1的四個地方都需要發請求拿資料,我們在這一部分分別編寫4個部分的API以及ts型別
image.png

7.4.1 SPU品牌

  1. API:
//獲取全部品牌的資料
  ALLTRADEMARK_URL = '/admin/product/baseTrademark/getTrademarkList',
//獲取全部的SPU的品牌的資料
export const reqAllTradeMark = () => {
  return request.get<any, AllTradeMark>(API.ALLTRADEMARK_URL)
}
  1. ts
//品牌資料的TS型別
export interface Trademark {
  id: number
  tmName: string
  logoUrl: string
}
//品牌介面返回的資料ts型別
export interface AllTradeMark extends ResponseData {
  data: Trademark[]
}

7.4.2 SPU圖片

  1. API
//獲取某個SPU下的全部的售賣商品的圖片資料
  IMAGE_URL = '/admin/product/spuImageList/',
//獲取某一個已有的SPU下全部商品的圖片地址
export const reqSpuImageList = (spuId: number) => {
  return request.get<any, SpuHasImg>(API.IMAGE_URL + spuId)
}
  1. 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 全部銷售屬性

  1. API
//獲取整個專案全部的銷售屬性[顏色、版本、尺碼]
  ALLSALEATTR_URL = '/admin/product/baseSaleAttrList',
//獲取全部的銷售屬性
export const reqAllSaleAttr = () => {
  return request.get<any, HasSaleAttrResponseData>(API.ALLSALEATTR_URL)
}

  1. ts
//已有的全部SPU的返回資料ts型別
export interface HasSaleAttr {
  id: number
  name: string
}
export interface HasSaleAttrResponseData extends ResponseData {
  data: HasSaleAttr[]
}

7.4.4 已有的銷售屬性

  1. API
//獲取某一個SPU下全部的已有的銷售屬性介面地址
  SPUHASSALEATTR_URL = '/admin/product/spuSaleAttrList/',
//獲取某一個已有的SPU擁有多少個銷售屬性
export const reqSpuHasSaleAttr = (spuId: number) => {
  return request.get<any, SaleAttrResponseData>(API.SPUHASSALEATTR_URL + spuId)
}
  1. 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 第一部分資料的傳遞

  1. 父元件拿到子元件例項

image.png
image.png

  1. 子元件暴露對外函式

image.png

  1. 修改按鈕點選函式中呼叫子元件函式,並傳遞第一部分資料

image.png

//修改已有的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名稱

image.png

7.7.3 展示SPU品牌

注意:下方的紅框展示的是所有品牌,上方的繫結的是一個數字也就是下方的第幾個
image.png

7.7.4 SPU描述

image.png

7.7.5 照片牆PART

照片牆部分我們使用了element-plus的el-upload元件。下面詳細介紹元件的功能及作用

  1. 整體結構

image.png
上面el-upload是上傳照片的照片牆,下面是檢視照片的對話方塊

  1. v-model:file-list

image.png

//商品圖片
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方法,這是因為元件對於資料的格式有要求。

  1. action

action是指圖片上傳的地址。元件還會將返回的資料放到對應的img的資料中
image.png

  1. list-type:照片牆的形式

image.png

  1. :on-preview

預覽的鉤子,預覽照片時會觸發。會注入對應圖片的資料。
image.png
image.png

//控制對話方塊的顯示與隱藏
let dialogVisible = ref<boolean>(false)
//儲存預覽圖片地址
let dialogImageUrl = ref<string>('')
//照片牆點選預覽按鈕的時候觸發的鉤子
const handlePictureCardPreview = (file: any) => {
  dialogImageUrl.value = file.url
  //對話方塊彈出來
  dialogVisible.value = true
}
  1. :on-remove

移除圖片前的鉤子

  1. :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 展示已有的銷售屬性與屬性值

資料結構如下:
image.png

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 刪除操作

image.png

<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
})

image.png

7.9.2 收集你選擇的屬性的id以及name

image.png

7.9.3 新增屬性按鈕的回撥

image.png

//新增銷售屬性的方法
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
image.png

//屬性值按鈕的點選事件
const toEdit = (row: SaleAttr) => {
  //點選按鈕的時候,input元件不就不出來->編輯模式
  row.flag = true
  row.saleAttrValue = ''
}

7.10.2 收集&&新增屬性值

收集的資料有倆個
saleAttrValue:點選新增按鈕時初始化為空,收集輸入的資訊
baseSaleAttrId:所在的資料的id。由row給出
其餘做的事就是:非法資料的過濾
image.png

//表單元素失卻焦點的事件回撥
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 刪除屬性值

image.png

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業務我們要做什麼?收集資料(發請求得到的、自己新增的)放到對應的資料(儲存資料用的容器)中,發起請求(儲存按鈕已經做完了),更新頁面

  1. 父元件新增按鈕回撥

新增和修改按鈕不同的地方在於對於資料的來源不同,修改按鈕是一部分(spuParams)來源於父元件傳遞的資料,將他們與元件繫結,在資料上展示。新增按鈕父元件只需要傳遞category3Id就行,其他的自己收集。

//新增新的SPU按鈕的回撥
const addSpu = () => {
  //切換為場景1:新增與修改已有SPU結構->SpuForm
  scene.value = 1
  //點選新增SPU按鈕,呼叫子元件的方法初始化資料
  spu.value.initAddSpu(categoryStore.c3Id)
}
  1. 子元件收集資料

注意要對外暴露,讓父元件可以使用

//新增一個新的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 })
  1. 整理資料與傳送請求

這部分透過儲存按鈕的回撥已經做完了。

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的靜態

image.png

7.14.1 繫結回撥

image.png

//新增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
image.pngimage.pngimage.png
image.png

7.16.4 收集平臺屬性以及銷售屬性

image.png
我們在資料繫結的時候將這倆個屬性所選擇的資料繫結到自身。之後整合資料的時候透過遍歷得到

7.16.5 img 資料&&設定預設圖片

image.png

//設定預設圖片的方法回撥
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展示

image.png

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 繫結點選函式&&回撥

image.png

//儲存全部的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業務

image.png

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 繫結點選函式

image.png

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 分頁器

image.png

//分頁器下拉選單發生變化觸發
const handler = () => {
  getHasSku()
}

注意:在這裡切換頁碼和切換每頁資料條數的回撥不同是因為:它們都能對函式注入資料,切換頁碼注入的是點選的頁碼數,因此我們可以直接使用getHasSku作為他的回撥。切換每頁資料條數注入的是切換的頁碼條數,我們希望切換後跳轉到第一頁,因此使用handler,間接呼叫getHasSku。

8.3 上架下架按鈕

image.png
image.png

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 按鈕切換

根據資料切換
image.png

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已經做完了。防止業務邏輯混亂
image.png
image.png

//更新已有的SKU
const updateSku = () => {
  ElMessage({ type: 'success', message: '程式設計師在努力的更新中....' })
}

8.5 商品詳情靜態搭建

image.png

8.5.1 Drawer 抽屜

描述:撥出一個臨時的側邊欄, 可以從多個方向撥出
image.png
image.png

//控制抽屜顯示與隱藏的欄位
let drawer = ref<boolean>(false)
  //檢視商品詳情按鈕的回撥
const findSku = async (row: SkuData) => {
  //抽屜展示出來
  drawer.value = true
}

8.5.2 Layout 佈局

透過基礎的 24 分欄,迅速簡便地建立佈局。
image.png
image.png
效果圖:
image.png

image.png
注意:把對應的style也複製過來
image.png

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 展示資料(銷售屬性為例)

image.png

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 模板展示資料

image.png

9.2.4 分頁器倆個函式回撥

image.png

//分頁器下拉選單的自定義事件的回撥
const handler = () => {
  getHasUser()
}

9.3 新增與修改使用者靜態

image.png

<!-- 抽屜結構:完成新增新的使用者賬號|更新已有的賬號資訊 -->
<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 新賬號新增業務

image.png

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: '',
})

image.png

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也是要展示的資料。
image.png

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()
image.png

//獲取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來決定我們的介面。
image.png
初始化使用者id:
我們再修改的時候將row的值複製給userParams,因此在展示抽屜的時候就會變換

//更新已有的使用者按鈕的回撥
//row:即為已有使用者的賬號資訊
const updateUser = (row: User) => {
  //抽屜顯示出來
  drawer.value = true
  //儲存收集已有的賬號資訊
  Object.assign(userParams, row)
  //清除上一次的錯誤的提示資訊
  nextTick(() => {
    formRef.value.clearValidate('username')
    formRef.value.clearValidate('name')
  })
}

image.png
image.png

9.6.1 其餘工作

  1. 新增按鈕回撥

image.png

  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 更改當前賬號再重新整理這一步到底發生了什麼?

首先,當你更改當前賬號再重新整理的時候,瀏覽器還是會往當前頁面跳轉image.png
這時候路由前置守衛就會發生作用:
image.png
你會發現,此時你的token儲存在本地儲存裡面,所以是有的,username儲存在倉庫裡面,所以重新整理就沒了。這也是之前說的倉庫儲存的問題。此時你的路由守衛就會走到下面這部分
image.png
它會向倉庫發起獲取使用者資訊的請求,獲取成功後就放行了。
問題來了!!!為什麼修改當前賬戶之後就會跳轉到登陸頁面呢?
首先我們建立一個使用者
image.png
登陸後再修改:
image.png
跳轉到了login介面
image.png
此時來看一下倉庫:token和username都沒了。這是為什麼呢?
image.png
因此我們回過頭來看一下路由守衛,可以看出走到了下面的位置,清除了使用者相關的資料清空。也就是說:
結論:當我們修改了賬戶在重新整理之後,我們再路由守衛裡呼叫** await userStore.userInfo()**語句會失敗(伺服器端會阻止),因此我們走到了**next({ path: '/login', query: { redirect: to.path } })**這裡,跳轉到了login頁面。
image.png

補充:證明一下我們修改了賬戶之後伺服器會阻止我們登入。
image.png
此時修改一下路由守衛(做個標記)
image.png
重新整理一下,證明路由確實是從這走的
image.png
此時在修改路由守衛以及使用者資訊方法
image.png
image.png
修改完之後再發請求:
image.png
此時可以得出結論,在修改使用者資訊之後,向伺服器發起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>

image.png

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:不確定狀態,既沒有全選也沒有全不選
image.png
核取方塊部分:
v-for="(role, index) in allRole":遍歷allRole。
:label="role":收集的資料(勾上的資料)
v-model="userRole":繫結收集的資料,也就是收集的資料儲存到userRole中。
@change:勾選變化時的回撥
image.png
全選框勾選的回撥:
實現原理:函式會將勾選與否注入到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 分配角色業務(給伺服器發請求)

  1. api&&type
//分配職位
export const reqSetUserRole = (data: SetRoleData) => {
  return request.post<any, any>(API.SETROLE_URL, data)
}
//給使用者分配職位介面攜帶引數的ts型別
export interface SetRoleData {
  roleIdList: number[]
  userId: number
}
  1. 元件傳送請求

回撥綁在確認按鈕身上就可以了

//確定按鈕的回撥(分配職位)
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 刪除業務

  1. 繫結點選函式

image.png

  1. 回撥函式
//刪除某一個使用者
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 批次刪除業務

  1. 繫結點選函式

image.png

  1. table收集選中的資料

image.png

//table核取方塊勾選的時候會觸發的事件
const selectChange = (value: any) => {
  selectIdArr.value = value
}
  1. 批次刪除回撥
//批次刪除按鈕的回撥
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}`,
  )
}

收集資料:
image.png
傳送請求
image.png

//搜尋按鈕的回撥
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 搜尋按鈕

image.png
image.png

//搜尋按鈕的回撥
const search = () => {
  //再次發請求根據關鍵字
  getHasRole()
  keyword.value = ''
}

10.2.6 重置按鈕

重置模組我在使用者管理模組仔細解釋過。
image.png

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 新增&&修改按鈕繫結點選函式

image.png
image.png

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:繫結資料的屬性
image.png

//自定義校驗規則的回撥
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 獲取資料

分配許可權按鈕:
image.png
獲取&&儲存資料

//準備一個陣列:陣列用於儲存勾選的節點的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',
}

image.png

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:可以把圖示放在開關裡面
image.png

13.1.2 暗黑模式

image.png

//暗黑模式需要的樣式
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:預定義顏色(會在下面顯示)
image.png

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',
])

image.png

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 頂部靜態

image.png

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 當前時間

  1. 安裝moment外掛

pnpm i moment

  1. 使用
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)
})
  1. 模板使用

image.png

14.2.3 頂部按鈕

image.png

//按鈕的點選回撥
const goHome = () => {
  $router.push('/home')
}

14.3 左側的上面部分

image.png

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 水球圖

  1. 安裝

pnpm i echarts
pnpm i echarts-liquidfill

  1. 使用
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 上面的樣式部分

image.png

<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 中間的上面部分

image.png

<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 路由載入問題

這樣配置路由後,如果你訪問的是非同步路由,會在重新整理的時候出現空白頁面。原因是非同步路由是非同步獲取的,載入的時候還沒有。因此我們可以在路由守衛檔案中改寫。這個的意思就是一直載入。
image.png

//使用者登入判斷
  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)
      }
    },
  })
}

image.png

17 打包成功

pnpm run build
注意,有些變數定義了未使用會報錯。
tsconfig.json:
image.png

本文由部落格一文多發平臺 OpenWrite 釋出!

相關文章