vue3+TS从0到1手撸后台管理系统

  • vue3+TS从0到1手撸后台管理系统已关闭评论
  • 39 次浏览
  • A+
所属分类:Web前端
摘要

srcviewshomeindex.vue(以home组件为例)
srcrouterindex.tssrcrouterrouter.tssrcApp.vue

目录

1.路由配置

1.1路由组件的雏形

srcviewshomeindex.vue(以home组件为例)
vue3+TS从0到1手撸后台管理系统

1.2路由配置

1.2.1路由index文件

srcrouterindex.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路由配置

srcrouterrouter.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路由出口

srcApp.vue
vue3+TS从0到1手撸后台管理系统

2.登录模块

2.1 登录路由静态组件

srcviewsloginindex.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 登录按钮绑定回调

vue3+TS从0到1手撸后台管理系统
回调应该做的事情

const login =  () => {   //点击登录按钮以后干什么   //通知仓库发起请求   //请求成功->路由跳转   //请求失败->弹出登陆失败信息 } 

2.2.2 仓库store初始化

  1. 大仓库(笔记只写一次)

安装pinia:pnpm i pinia@2.0.34
srcstoreindex.ts

//仓库大仓库 import { createPinia } from 'pinia' //创建大仓库 const pinia = createPinia() //对外暴露:入口文件需要安装仓库 export default pinia  
  1. 用户相关的小仓库

srcstoremodulesuser.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. 使用(引入后)

vue3+TS从0到1手撸后台管理系统

  1. 效果

vue3+TS从0到1手撸后台管理系统

2.5 表单校验规则

2.5.1 表单校验

  1. 表单绑定项

vue3+TS从0到1手撸后台管理系统
:model:绑定的数据

//收集账号与密码数据 let loginForm = reactive({ username: 'admin', password: '111111' }) 

:rules:对应要使用的规则

//定义表单校验需要的配置对象 const rules = {} 

ref="loginForms":获取表单元素

//获取表单元素 let loginForms = ref() 
  1. 表单元素绑定项

Form 组件提供了表单验证的功能,只需为 rules 属性传入约定的验证规则,并将 form-Item 的 prop 属性设置为需要验证的特殊键值即可
vue3+TS从0到1手撸后台管理系统

  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子组件的搭建

页面左上角的这部分,我们将它做成子组件,并且封装方便维护以及修改。
vue3+TS从0到1手撸后台管理系统

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. 动态菜单子组件:srclayoutmenuindex.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, //仓库存储生成菜单需要数组(路由) }  

vue3+TS从0到1手撸后台管理系统

  1. 父组件拿到仓库路由信息并传递给子组件
<script setup lang="ts"> 。。。。。。 //引入菜单组件 import Menu from './menu/index.vue' //获取用户相关的小仓库 import useUserStore from '@/store/modules/user' let userStore = useUserStore() </script> 

vue3+TS从0到1手撸后台管理系统

  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 菜单图标

vue3+TS从0到1手撸后台管理系统

  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> 

vue3+TS从0到1手撸后台管理系统

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个添加函数
vue3+TS从0到1手撸后台管理系统
vue3+TS从0到1手撸后台管理系统

<script setup lang="ts"> 。。。。。。 //获取路由器对象 let $router = useRouter() const goRoute = (vc: any) => {   //路由跳转   $router.push(vc.index) } </script> 
  1. layout组件

vue3+TS从0到1手撸后台管理系统

3.3.5 Bug&&总结

在这部分对router-link遇到一些bug,理解也更深了,特意写一个小结总结一下

bug:router-link不生效。
描述:当我点击跳转函数的时候,直接跳转到一个新页面,而不是layout组件展示的部分更新。
思路:首先输出了一下路径,发现路径没有错。其次,因为跳转到新页面,代表layout组件中的router-link不生效,删除router-link,发现没有影响。所以确定了是router-link没有生效。
解决:仔细检查了srcrouterroutes.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为当前路由。
srclayoutindex.vue
vue3+TS从0到1手撸后台管理系统

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" alt="vue3+TS从0到1手撸后台管理系统"         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" alt="vue3+TS从0到1手撸后台管理系统"     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状态来修改个子组件的样式(以左侧菜单为例)

vue3+TS从0到1手撸后台管理系统
绑定动态样式修改scss
vue3+TS从0到1手撸后台管理系统

  1. 左侧菜单使用element-plus折叠collapse属性

vue3+TS从0到1手撸后台管理系统
效果图:
vue3+TS从0到1手撸后台管理系统
注意:折叠文字的时候会把图标也折叠起来。在menu组件中吧图标放到template外面就可以。
vue3+TS从0到1手撸后台管理系统

3.4.3 顶部面包屑动态展示

  1. 引入$route

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

<script setup lang="ts"> import { useRoute } from 'vue-router' //获取路由对象 let $route = useRoute() //点击图标的切换  </script> 
  1. 结构展示

注意:使用了$route.matched函数,此函数能得到当前路由的信息
vue3+TS从0到1手撸后台管理系统

  1. 首页修改

访问首页时,因为它是二级路由,会遍历出layout面包屑,处理:删除layout路由的title。再加上一个判断
vue3+TS从0到1手撸后台管理系统

  1. 面包屑点击跳转

注意:将路由中的一级路由权限管理以及商品管理重定向到第一个孩子,这样点击跳转的时候会定向到第一个孩子。
vue3+TS从0到1手撸后台管理系统

3.4.4 刷新业务的实现

  1. 使用pinia定义一个变量作为标记

vue3+TS从0到1手撸后台管理系统

  1. 点击刷新按钮,修改标记

vue3+TS从0到1手撸后台管理系统

<script setup lang="ts"> //使用layout的小仓库 import useLayOutSettingStore from '@/store/modules/setting' let layoutSettingStore = useLayOutSettingStore() //刷新按钮点击的回调 const updateRefresh = () => {   layoutSettingStore.refresh = !layoutSettingStore.refresh } </script> 
  1. main组件检测标记销毁&重加载组件(nextTick

vue3+TS从0到1手撸后台管理系统

<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. 给全屏按钮绑定函数

vue3+TS从0到1手撸后台管理系统

  1. 实现全屏效果(利用docment根节点的方法)

vue3+TS从0到1手撸后台管理系统

//全屏按钮点击的回调 const fullScreen = () => {   //DOM对象的一个属性:可以用来判断当前是不是全屏的模式【全屏:true,不是全屏:false】   let full = document.fullscreenElement   //切换成全屏   if (!full) {     //文档根节点的方法requestFullscreen实现全屏     document.documentElement.requestFullscreen()   } else {     //退出全屏     document.exitFullscreen()   } 

4.部分功能处理完善

登录这一块大概逻辑,前端发送用户名密码到后端,后端返回token,前端保存,并且请求拦截器,请求头有token就要携带token

vue3+TS从0到1手撸后台管理系统

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声明

vue3+TS从0到1手撸后台管理系统

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的信息(记得先引入并创建实例)

srclayouttabbarsettingindex.vue
vue3+TS从0到1手撸后台管理系统
vue3+TS从0到1手撸后台管理系统

4.2 退出功能

  1. 退出登录绑定函数,调用仓库函数

vue3+TS从0到1手撸后台管理系统

//退出登陆点击的回调 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. 登录按钮进行判断

vue3+TS从0到1手撸后台管理系统

4.3 路由守卫

srcpermisstion.ts(新建文件)
main.ts引入
vue3+TS从0到1手撸后台管理系统

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?

vue3+TS从0到1手撸后台管理系统
个人理解:之前在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,三个环境文件下的服务器域名写为:
vue3+TS从0到1手撸后台管理系统

  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. 退出登录按钮的点击函数修改

退出成功后再跳转
vue3+TS从0到1手撸后台管理系统

  1. 路由跳转判断条件修改

srcpermisstion.ts
也是退出成功后再跳转
vue3+TS从0到1手撸后台管理系统

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

注意:在srcstoremodulesuser.ts以及srcapiuserindex.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行。vue3+TS从0到1手撸后台管理系统

  1. el-table-column的type属性:对应列的类型。 如果设置了selection则显示多选框; 如果设置了 index 则显示该行的索引(从 1 开始计算); 如果设置了 expand 则显示为一个可展开的按钮

vue3+TS从0到1手撸后台管理系统

  1. el-table-column的prop属性:字段名称 对应列内容的字段名, 也可以使用 property属性

注意:因为我们之前已经绑定了数据,所以在这里直接使用数据的属性tmName
vue3+TS从0到1手撸后台管理系统

  1. el-table-column的插槽

vue3+TS从0到1手撸后台管理系统
为什么要使用插槽呢?因为prop属性虽然能够展示数据,但是他默认是div,如果我们的图片使用prop展示的话,会展示图片的路径。因此如果想展示图片或者按钮,我们就要使用插槽
vue3+TS从0到1手撸后台管理系统
注意: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函数,因此能够跳转到正确的页码数
vue3+TS从0到1手撸后台管理系统

//获取已有品牌的接口封装为一个函数:在任何情况下向获取数据,调用次函数即可 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 每页展示数据条数

vue3+TS从0到1手撸后台管理系统
vue3+TS从0到1手撸后台管理系统

//当下拉菜单发生变化的时候触发此方法 //这个自定义事件,分页器组件会将下拉菜单选中数据返回 const sizeChange = () => {   //当前每一页的数据量发生变化的时候,当前页码归1   getHasTrademark()   console.log(123) } 

同样的这个函数也会返回一个参数。但是我们不需要使用这个参数,因此才另外写一个回调函数。

5.5 dialog对话框静态搭建

vue3+TS从0到1手撸后台管理系统

  1. 对话框的标题&&显示隐藏

v-model:属性用户控制对话框的显示与隐藏的 true显示 false隐藏
title:设置对话框左上角标题
vue3+TS从0到1手撸后台管理系统

  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. 收集品牌名称

vue3+TS从0到1手撸后台管理系统

  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. 上传图片后,用图片代替加号

vue3+TS从0到1手撸后台管理系统

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就是当前的数据
vue3+TS从0到1手撸后台管理系统

  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. 设置对话框标题

vue3+TS从0到1手撸后台管理系统

  1. 小问题

当我们修改操作之后再点击添加品牌,对话框的title依旧是修改品牌。怎么是因为对话框的title是根据trademarkParams.id来的,我们之前添加品牌按钮操作没有对id进行清除。修改为如下就可

//添加品牌按钮的回调 const addTrademark = () => {   //对话框显示   dialogFormVisible.value = true   //清空收集数据   trademarkParams.id = 0   trademarkParams.tmName = ''   trademarkParams.logoUrl = '' } 

5.7 品牌管理模块表单校验

5.7.1 表单校验(自定义规则校验,可以简略堪称三步走)

  1. 绑定参数

vue3+TS从0到1手撸后台管理系统
:model:校验的数据
:rules:校验规则
ref="formRef":表单实例
vue3+TS从0到1手撸后台管理系统
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绑定的就是回调函数
vue3+TS从0到1手撸后台管理系统

  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 属性管理模块的静态组件

vue3+TS从0到1手撸后台管理系统

属性管理分为上面部分的三级分类模块以及下面的添加属性部分。我们将三级分类模块单独提取出来做成全局组件

6.1.1 三级分类全局组件(静态)

注意:要在srccomponentsindex.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属性上
vue3+TS从0到1手撸后台管理系统
vue3+TS从0到1手撸后台管理系统

  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. 组件数据展示

vue3+TS从0到1手撸后台管理系统

  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 添加属性按钮禁用

在我们没选择好三级菜单之前,添加属性按钮应该处于禁用状态

srcviewsproductattrindex.vue(父组件)
vue3+TS从0到1手撸后台管理系统

6.5 已有属性与属性值展示vue3+TS从0到1手撸后台管理系统

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 添加属性页面的静态展示

当点击添加属性后:
vue3+TS从0到1手撸后台管理系统

6.6.1 定义变量控制页面展示与隐藏

//定义card组件内容切换变量 let scene = ref<number>(0) //scene=0,显示table,scene=1,展示添加与修改属性结构 

vue3+TS从0到1手撸后台管理系统

6.6.2 表单

vue3+TS从0到1手撸后台管理系统
vue3+TS从0到1手撸后台管理系统

6.6.3 按钮

vue3+TS从0到1手撸后台管理系统

6.6.4 表格

vue3+TS从0到1手撸后台管理系统

6.6.5按钮

vue3+TS从0到1手撸后台管理系统

6.6.6 三级分类禁用

当点击添加属性之后,三级分类应该被禁用。因此使用props给子组件传参
vue3+TS从0到1手撸后台管理系统
子组件:
二三级分类同理。
vue3+TS从0到1手撸后台管理系统

6.7 添加属性&&修改属性的接口类型

6.7.1修改属性

vue3+TS从0到1手撸后台管理系统

6.7.2 添加属性

vue3+TS从0到1手撸后台管理系统

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)

vue3+TS从0到1手撸后台管理系统

  1. 属性值数组(attrValueList)

我们给添加属性值按钮绑定一个回调,点击的时候会往attrParams.attrValueList中添加一个空数组。我们根据空数组的数量生成input框,再将input的值与数组中的值绑定。
vue3+TS从0到1手撸后台管理系统

//添加属性值按钮的回调 const addAttrValue = () => {   //点击添加属性值按钮的时候,向数组添加一个属性值对象   attrParams.attrValueList.push({     valueName: '',     flag: true, //控制每一个属性值编辑模式与切换模式的切换   }) } 

vue3+TS从0到1手撸后台管理系统

  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 发送请求&&更新页面

vue3+TS从0到1手撸后台管理系统
vue3+TS从0到1手撸后台管理系统

//保存按钮的回调 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来决定哪个展示。
vue3+TS从0到1手撸后台管理系统
注意:flag放在哪?由于每一个属性值对象都需要一个flag属性,因此将flag的添加放在添加属性值的按钮的回调上。(注意修改属性值的type)

//添加属性值按钮的回调 const addAttrValue = () => {   //点击添加属性值按钮的时候,向数组添加一个属性值对象   attrParams.attrValueList.push({     valueName: '',     flag: true, //控制每一个属性值编辑模式与切换模式的切换   })    } 

srcapiproductattrtype.ts
vue3+TS从0到1手撸后台管理系统

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

vue3+TS从0到1手撸后台管理系统

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 删除按钮

vue3+TS从0到1手撸后台管理系统

6.11属性修改业务

6.11.1属性修改业务

修改业务很简单:当我们点击修改按钮的时候,将修改的实例(row)传递给回调函数。回调函数:首先跳转到第二页面,第二页面是根据attrParams值生成的,我们跳转的时候将实例的值传递给attrParams
vue3+TS从0到1手撸后台管理系统

//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. 绑定点击函数&&气泡弹出框

vue3+TS从0到1手撸后台管理系统

  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模块的静态页面

vue3+TS从0到1手撸后台管理系统

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

vue3+TS从0到1手撸后台管理系统

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模块需要在三个场景进行切换,全都放在一个组件里面的话会显得很臃肿。因此我们将它放到三个组件当中。
vue3+TS从0到1手撸后台管理系统
使用v-show来展示页面:v-if是销毁组件,v-show是隐藏组件。在初加载的时候v-if比较快,但是在频繁切换的时候v-if任务重。
vue3+TS从0到1手撸后台管理系统

7.3.2 SPU场景一子组件静态

vue3+TS从0到1手撸后台管理系统

<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,因此涉及到父子组件通信。这里使用自定义事件。
父组件:
vue3+TS从0到1手撸后台管理系统
子组件:
vue3+TS从0到1手撸后台管理系统

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

7.4 SPU模块API&&TS类型(修改&&添加)

修改和添加的页面是差不多的。页面1的四个地方都需要发请求拿数据,我们在这一部分分别编写4个部分的API以及ts类型
vue3+TS从0到1手撸后台管理系统

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. 父组件拿到子组件实例

vue3+TS从0到1手撸后台管理系统
vue3+TS从0到1手撸后台管理系统

  1. 子组件暴露对外函数

vue3+TS从0到1手撸后台管理系统

  1. 修改按钮点击函数中调用子组件函数,并传递第一部分数据

vue3+TS从0到1手撸后台管理系统

//修改已有的SPU的按钮的回调 const updateSpu = (row: SpuData) => {   //切换为场景1:添加与修改已有SPU结构->SpuForm   scene.value = 1   //调用子组件实例方法获取完整已有的SPU的数据   spu.value.initHasSpuData(row) } 

7.5.2 其余数据

子组件中直接发起请求,并且将服务器返回的四个数据存储,加上参数传递的第一部分数据,这样子组件拿到了全部的数据。

//子组件书写一个方法 const initHasSpuData = async (spu: SpuData) => {   //spu:即为父组件传递过来的已有的SPU对象[不完整]   //获取全部品牌的数据   let result: AllTradeMark = await reqAllTradeMark()   //获取某一个品牌旗下全部售卖商品的图片   let result1: SpuHasImg = await reqSpuImageList(spu.id as number)   //获取已有的SPU销售属性的数据   let result2: SaleAttrResponseData = await reqSpuHasSaleAttr(spu.id as number)   //获取整个项目全部SPU的销售属性   let result3: HasSaleAttrResponseData = await reqAllSaleAttr()   //存储全部品牌的数据   MYAllTradeMark.value = result.data   //SPU对应商品图片   imgList.value = result1.data.map((item) => {     return {       name: item.imgName,       url: item.imgUrl,     }   })   //存储已有的SPU的销售属性   saleAttr.value = result2.data   //存储全部的销售属性   allSaleAttr.value = result3.data } 

7.6 修改与添加的接口&&TS

7.6.1 接口(API)

/追加一个新的SPU   ADDSPU_URL = '/admin/product/saveSpuInfo',   //更新已有的SPU   UPDATESPU_URL = '/admin/product/updateSpuInfo', //添加一个新的SPU的 //更新已有的SPU接口 //data:即为新增的SPU|或者已有的SPU对象 export const reqAddOrUpdateSpu = (data: any) => {   //如果SPU对象拥有ID,更新已有的SPU   if (data.id) {     return request.post<any, any>(API.UPDATESPU_URL, data)   } else {     return request.post<any, any>(API.ADDSPU_URL, data)   } } 

7.6.2 ts

//SPU数据的ts类型:需要修改 export interface SpuData {   category3Id: string | number   id?: number   spuName: string   tmId: number | string   description: string   spuImageList: null | SpuImg[]   spuSaleAttrList: null | SaleAttr[] } 

7.7 展示与收集已有的数据

7.7.1 存储父组件传递过来的数据

//存储已有的SPU对象 let SpuParams = ref<SpuData>({   category3Id: '', //收集三级分类的ID   spuName: '', //SPU的名字   description: '', //SPU的描述   tmId: '', //品牌的ID   spuImageList: [],   spuSaleAttrList: [], }) //子组件书写一个方法 const initHasSpuData = async (spu: SpuData) => {   //存储已有的SPU对象,将来在模板中展示   SpuParams.value = spu   。。。。。。 } 

7.7.2 展示SPU名称

vue3+TS从0到1手撸后台管理系统

7.7.3 展示SPU品牌

注意:下方的红框展示的是所有品牌,上方的绑定的是一个数字也就是下方的第几个
vue3+TS从0到1手撸后台管理系统

7.7.4 SPU描述

vue3+TS从0到1手撸后台管理系统

7.7.5 照片墙PART

照片墙部分我们使用了element-plus的el-upload组件。下面详细介绍组件的功能及作用

  1. 整体结构

vue3+TS从0到1手撸后台管理系统
上面el-upload是上传照片的照片墙,下面是查看照片的对话框

  1. v-model:file-list

vue3+TS从0到1手撸后台管理系统

//商品图片 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的数据中
vue3+TS从0到1手撸后台管理系统

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

vue3+TS从0到1手撸后台管理系统

  1. :on-preview

预览的钩子,预览照片时会触发。会注入对应图片的数据。
vue3+TS从0到1手撸后台管理系统
vue3+TS从0到1手撸后台管理系统

//控制对话框的显示与隐藏 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 展示已有的销售属性与属性值

数据结构如下:
vue3+TS从0到1手撸后台管理系统

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

vue3+TS从0到1手撸后台管理系统

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

vue3+TS从0到1手撸后台管理系统

7.9.2 收集你选择的属性的id以及name

vue3+TS从0到1手撸后台管理系统

7.9.3 添加属性按钮的回调

vue3+TS从0到1手撸后台管理系统

//添加销售属性的方法 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
vue3+TS从0到1手撸后台管理系统

//属性值按钮的点击事件 const toEdit = (row: SaleAttr) => {   //点击按钮的时候,input组件不就不出来->编辑模式   row.flag = true   row.saleAttrValue = '' } 

7.10.2 收集&&添加属性值

收集的数据有俩个
saleAttrValue:点击添加按钮时初始化为空,收集输入的信息
baseSaleAttrId:所在的数据的id。由row给出
其余做的事就是:非法数据的过滤
vue3+TS从0到1手撸后台管理系统

//表单元素失却焦点的事件回调 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 删除属性值

vue3+TS从0到1手撸后台管理系统

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的静态

vue3+TS从0到1手撸后台管理系统

7.14.1 绑定回调

vue3+TS从0到1手撸后台管理系统

//添加SKU按钮的回调 const addSku = (row: SpuData) => {   //点击添加SKU按钮切换场景为2   scene.value = 2 } 

7.14.2 静态页面

<template>   <el-form label-width="100px">     <el-form-item label="SKU名称">       <el-input placeholder="SKU名称"></el-input>     </el-form-item>     <el-form-item label="价格(元)">       <el-input placeholder="价格(元)" type="number"></el-input>     </el-form-item>     <el-form-item label="重量(g)">       <el-input placeholder="重量(g)" type="number"></el-input>     </el-form-item>     <el-form-item label="SKU描述">       <el-input placeholder="SKU描述" type="textarea"></el-input>     </el-form-item>     <el-form-item label="平台属性">       <el-form :inline="true">         <el-form-item label="内存" size="normal">           <el-select>             <el-option label="213"></el-option>             <el-option label="213"></el-option>             <el-option label="213"></el-option>             <el-option label="213"></el-option>           </el-select>         </el-form-item>         <el-form-item label="内存" size="normal">           <el-select>             <el-option label="213"></el-option>             <el-option label="213"></el-option>             <el-option label="213"></el-option>             <el-option label="213"></el-option>           </el-select>         </el-form-item>         <el-form-item label="内存" size="normal">           <el-select>             <el-option label="213"></el-option>             <el-option label="213"></el-option>             <el-option label="213"></el-option>             <el-option label="213"></el-option>           </el-select>         </el-form-item>         <el-form-item label="内存" size="normal">           <el-select>             <el-option label="213"></el-option>             <el-option label="213"></el-option>             <el-option label="213"></el-option>             <el-option label="213"></el-option>           </el-select>         </el-form-item>       </el-form>     </el-form-item>     <el-form-item label="销售属性">       <el-form :inline="true">         <el-form-item label="颜色" size="normal">           <el-select>             <el-option label="213"></el-option>             <el-option label="213"></el-option>             <el-option label="213"></el-option>             <el-option label="213"></el-option>           </el-select>         </el-form-item>       </el-form>     </el-form-item>     <el-form-item label="图片名称" size="normal">       <el-table border>         <el-table-column           type="selection"           width="80px"           align="center"           ></el-table-column>         <el-table-column label="图片"></el-table-column>         <el-table-column label="名称"></el-table-column>         <el-table-column label="操作"></el-table-column>       </el-table>     </el-form-item>     <el-form-item>       <el-button type="primary" size="default">保存</el-button>       <el-button type="primary" size="default" @click="cancel">取消</el-button>     </el-form-item>   </el-form> </template> 

7.14.3 取消按钮

//自定义事件的方法 let $emit = defineEmits(['changeScene']) //取消按钮的回调 const cancel = () => {   $emit('changeScene', { flag: 0, params: '' }) } 

7.15 获取添加SKU数据并展示

7.15.2 父组件添加按钮回调->调用子组件函数收集数据

父组件

//添加SKU按钮的回调 const addSku = (row: SpuData) => {   //点击添加SKU按钮切换场景为2   scene.value = 2   //调用子组件的方法初始化添加SKU的数据   sku.value.initSkuData(categoryStore.c1Id, categoryStore.c2Id,row) } 

子组件暴露:

//对外暴露方法 defineExpose({ initSkuData }) 

7.15.2 子组件函数收集数据(平台属性、销售属性、图片名称)

//当前子组件的方法对外暴露 const initSkuData = async (   c1Id: number | string,   c2Id: number | string,   spu: any, ) => {    //获取平台属性   let result: any = await reqAttr(c1Id, c2Id, spu.category3Id)   //获取对应的销售属性   let result1: any = await reqSpuHasSaleAttr(spu.id)   //获取照片墙的数据   let result2: any = await reqSpuImageList(spu.id)   //平台属性   attrArr.value = result.data   //销售属性   saleArr.value = result1.data   //图片   imgArr.value = result2.data } 

7.15.3 模板展示(以图片为例)

<el-form-item label="图片名称" size="normal">       <el-table border :data="imgArr" ref="table">         <el-table-column           type="selection"           width="80px"           align="center"         ></el-table-column>         <el-table-column label="图片">           <template #="{ row, $index }">             <img :src="row.imgUrl" alt="" style="width: 100px; height: 100px" />           </template>         </el-table-column>         <el-table-column label="名称" prop="imgName"></el-table-column>         <el-table-column label="操作">           <template #="{ row, $index }">             <el-button type="primary" size="small">设置默认</el-button>           </template>         </el-table-column>       </el-table>     </el-form-item> 

7.16 sku收集总数据

使用skuParams将sku模块的所有数据全都存储下来

7.16.1 API&&Ts

API:

//追加一个新增的SKU地址   ADDSKU_URL = '/admin/product/saveSkuInfo', } //添加SKU的请求方法 export const reqAddSku = (data: SkuData) => {   request.post<any, any>(API.ADDSKU_URL, data) } 

ts:

export interface Attr {   attrId: number | string //平台属性的ID   valueId: number | string //属性值的ID } export interface saleArr {   saleAttrId: number | string //属性ID   saleAttrValueId: number | string //属性值的ID } export interface SkuData {   category3Id: string | number //三级分类的ID   spuId: string | number //已有的SPU的ID   tmId: string | number //SPU品牌的ID   skuName: string //sku名字   price: string | number //sku价格   weight: string | number //sku重量   skuDesc: string //sku的描述   skuAttrValueList?: Attr[]   skuSaleAttrValueList?: saleArr[]   skuDefaultImg: string //sku图片地址 } 

7.16.2 收集父组件传递过来的数据

这部分数据包括三级id,spuid还有品牌id。由于是父组件传递过来的,我们可以直接在添加按钮调用的那个函数中收集

//当前子组件的方法对外暴露 const initSkuData = async (   c1Id: number | string,   c2Id: number | string,   spu: any, ) => {   //收集数据   skuParams.category3Id = spu.category3Id   skuParams.spuId = spu.id   skuParams.tmId = spu.tmId   。。。。。。 } 

7.16.3 input框收集数据

sku名称、价格、重量、sku描述都是收集的用户输入的数据。我们直接使用v-model
vue3+TS从0到1手撸后台管理系统vue3+TS从0到1手撸后台管理系统vue3+TS从0到1手撸后台管理系统
vue3+TS从0到1手撸后台管理系统

7.16.4 收集平台属性以及销售属性

vue3+TS从0到1手撸后台管理系统
我们在数据绑定的时候将这俩个属性所选择的数据绑定到自身。之后整合数据的时候通过遍历得到

7.16.5 img 数据&&设置默认图片

vue3+TS从0到1手撸后台管理系统

//设置默认图片的方法回调 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展示

vue3+TS从0到1手撸后台管理系统

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 绑定点击函数&&回调

vue3+TS从0到1手撸后台管理系统

//存储全部的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业务

vue3+TS从0到1手撸后台管理系统

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 绑定点击函数

vue3+TS从0到1手撸后台管理系统

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

vue3+TS从0到1手撸后台管理系统

//分页器下拉菜单发生变化触发 const handler = () => {   getHasSku() } 

注意:在这里切换页码和切换每页数据条数的回调不同是因为:它们都能对函数注入数据,切换页码注入的是点击的页码数,因此我们可以直接使用getHasSku作为他的回调。切换每页数据条数注入的是切换的页码条数,我们希望切换后跳转到第一页,因此使用handler,间接调用getHasSku。

8.3 上架下架按钮

vue3+TS从0到1手撸后台管理系统
vue3+TS从0到1手撸后台管理系统

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 按钮切换

根据数据切换
vue3+TS从0到1手撸后台管理系统

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已经做完了。防止业务逻辑混乱
vue3+TS从0到1手撸后台管理系统
vue3+TS从0到1手撸后台管理系统

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

8.5 商品详情静态搭建

vue3+TS从0到1手撸后台管理系统

8.5.1 Drawer 抽屉

描述:呼出一个临时的侧边栏, 可以从多个方向呼出
vue3+TS从0到1手撸后台管理系统
vue3+TS从0到1手撸后台管理系统

//控制抽屉显示与隐藏的字段 let drawer = ref<boolean>(false)   //查看商品详情按钮的回调 const findSku = async (row: SkuData) => {   //抽屉展示出来   drawer.value = true } 

8.5.2 Layout 布局

通过基础的 24 分栏,迅速简便地创建布局。
vue3+TS从0到1手撸后台管理系统
vue3+TS从0到1手撸后台管理系统
效果图:
vue3+TS从0到1手撸后台管理系统

8.5.3 轮播图 carousel

vue3+TS从0到1手撸后台管理系统
注意:把对应的style也复制过来
vue3+TS从0到1手撸后台管理系统

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 展示数据(销售属性为例)

vue3+TS从0到1手撸后台管理系统

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 模板展示数据

vue3+TS从0到1手撸后台管理系统

9.2.4 分页器俩个函数回调

vue3+TS从0到1手撸后台管理系统

//分页器下拉菜单的自定义事件的回调 const handler = () => {   getHasUser() } 

9.3 添加与修改用户静态

vue3+TS从0到1手撸后台管理系统

<!-- 抽屉结构:完成添加新的用户账号|更新已有的账号信息 --> <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 新账号添加业务

vue3+TS从0到1手撸后台管理系统

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

vue3+TS从0到1手撸后台管理系统

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也是要展示的数据。
vue3+TS从0到1手撸后台管理系统

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()
vue3+TS从0到1手撸后台管理系统

//获取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来决定我们的界面。
vue3+TS从0到1手撸后台管理系统
初始化用户id:
我们再修改的时候将row的值复制给userParams,因此在展示抽屉的时候就会变换

//更新已有的用户按钮的回调 //row:即为已有用户的账号信息 const updateUser = (row: User) => {   //抽屉显示出来   drawer.value = true   //存储收集已有的账号信息   Object.assign(userParams, row)   //清除上一次的错误的提示信息   nextTick(() => {     formRef.value.clearValidate('username')     formRef.value.clearValidate('name')   }) } 

vue3+TS从0到1手撸后台管理系统
vue3+TS从0到1手撸后台管理系统

9.6.1 其余工作

  1. 添加按钮回调

vue3+TS从0到1手撸后台管理系统

  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 更改当前账号再刷新这一步到底发生了什么?

首先,当你更改当前账号再刷新的时候,浏览器还是会往当前页面跳转vue3+TS从0到1手撸后台管理系统
这时候路由前置守卫就会发生作用:
vue3+TS从0到1手撸后台管理系统
你会发现,此时你的token存储在本地存储里面,所以是有的,username存储在仓库里面,所以刷新就没了。这也是之前说的仓库存储的问题。此时你的路由守卫就会走到下面这部分
vue3+TS从0到1手撸后台管理系统
它会向仓库发起获取用户信息的请求,获取成功后就放行了。
问题来了!!!为什么修改当前账户之后就会跳转到登陆页面呢?
首先我们创建一个用户
vue3+TS从0到1手撸后台管理系统
登陆后再修改:
vue3+TS从0到1手撸后台管理系统
跳转到了login界面
vue3+TS从0到1手撸后台管理系统
此时来看一下仓库:token和username都没了。这是为什么呢?
vue3+TS从0到1手撸后台管理系统
因此我们回过头来看一下路由守卫,可以看出走到了下面的位置,清除了用户相关的数据清空。也就是说:
结论:当我们修改了账户在刷新之后,我们再路由守卫里调用** await userStore.userInfo()**语句会失败(服务器端会阻止),因此我们走到了**next({ path: '/login', query: { redirect: to.path } })**这里,跳转到了login页面。
vue3+TS从0到1手撸后台管理系统

补充:证明一下我们修改了账户之后服务器会阻止我们登录。
vue3+TS从0到1手撸后台管理系统
此时修改一下路由守卫(做个标记)
vue3+TS从0到1手撸后台管理系统
刷新一下,证明路由确实是从这走的
vue3+TS从0到1手撸后台管理系统
此时在修改路由守卫以及用户信息方法
vue3+TS从0到1手撸后台管理系统
vue3+TS从0到1手撸后台管理系统
修改完之后再发请求:
vue3+TS从0到1手撸后台管理系统
此时可以得出结论,在修改用户信息之后,向服务器发起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> 

vue3+TS从0到1手撸后台管理系统

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:不确定状态,既没有全选也没有全不选
vue3+TS从0到1手撸后台管理系统
复选框部分:
v-for="(role, index) in allRole":遍历allRole。
:label="role":收集的数据(勾上的数据)
v-model="userRole":绑定收集的数据,也就是收集的数据存储到userRole中。
@change:勾选变化时的回调
vue3+TS从0到1手撸后台管理系统
全选框勾选的回调:
实现原理:函数会将勾选与否注入到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. 绑定点击函数

vue3+TS从0到1手撸后台管理系统

  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. 绑定点击函数

vue3+TS从0到1手撸后台管理系统

  1. table收集选中的数据

vue3+TS从0到1手撸后台管理系统

//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}`,   ) } 

收集数据:
vue3+TS从0到1手撸后台管理系统
发送请求
vue3+TS从0到1手撸后台管理系统

//搜索按钮的回调 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 搜索按钮

vue3+TS从0到1手撸后台管理系统
vue3+TS从0到1手撸后台管理系统

//搜索按钮的回调 const search = () => {   //再次发请求根据关键字   getHasRole()   keyword.value = '' } 

10.2.6 重置按钮

重置模块我在用户管理模块仔细解释过。
vue3+TS从0到1手撸后台管理系统

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 添加&&修改按钮绑定点击函数

vue3+TS从0到1手撸后台管理系统
vue3+TS从0到1手撸后台管理系统

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:绑定数据的属性
vue3+TS从0到1手撸后台管理系统

//自定义校验规则的回调 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 获取数据

分配权限按钮:
vue3+TS从0到1手撸后台管理系统
获取&&存储数据

//准备一个数组:数组用于存储勾选的节点的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', } 

vue3+TS从0到1手撸后台管理系统

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:可以把图标放在开关里面
vue3+TS从0到1手撸后台管理系统

13.1.2 暗黑模式

vue3+TS从0到1手撸后台管理系统

//暗黑模式需要的样式 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:预定义颜色(会在下面显示)
vue3+TS从0到1手撸后台管理系统

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

vue3+TS从0到1手撸后台管理系统

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 顶部静态

vue3+TS从0到1手撸后台管理系统

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

vue3+TS从0到1手撸后台管理系统

14.2.3 顶部按钮

vue3+TS从0到1手撸后台管理系统

//按钮的点击回调 const goHome = () => {   $router.push('/home') } 

14.3 左侧的上面部分

vue3+TS从0到1手撸后台管理系统

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 上面的样式部分

vue3+TS从0到1手撸后台管理系统

<template>   <div class="box1">     <div class="title">       <p>男女比例</p>       <img src="../../images/dataScreen-title.png" alt="vue3+TS从0到1手撸后台管理系统" alt="" />     </div>     <div class="sex">       <div class="man">         <img src="../../images/man.png" alt="vue3+TS从0到1手撸后台管理系统" alt="" />       </div>       <div class="women">         <img src="../../images/woman.png" alt="vue3+TS从0到1手撸后台管理系统" 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="vue3+TS从0到1手撸后台管理系统" 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 中间的上面部分

vue3+TS从0到1手撸后台管理系统

<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="vue3+TS从0到1手撸后台管理系统" 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="vue3+TS从0到1手撸后台管理系统" 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账号:producttrademarkattrsku         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 路由加载问题

这样配置路由后,如果你访问的是异步路由,会在刷新的时候出现空白页面。原因是异步路由是异步获取的,加载的时候还没有。因此我们可以在路由守卫文件中改写。这个的意思就是一直加载。
vue3+TS从0到1手撸后台管理系统

//用户登录判断   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)       }     },   }) } 

vue3+TS从0到1手撸后台管理系统

17 打包成功

pnpm run build
注意,有些变量定义了未使用会报错。
tsconfig.json:
vue3+TS从0到1手撸后台管理系统

本文由博客一文多发平台 OpenWrite 发布!