当前位置: 首页 > news >正文

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

1.路由配置

1.1路由组件的雏形

src\views\home\index.vue(以home组件为例)
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

1.2路由配置

1.2.1路由index文件

src\router\index.ts

//通过vue-router插件实现模板路由配置
import { createRouter, createWebHashHistory } from 'vue-router'
import { constantRoute } from './router'
//创建路由器
const router = createRouter({//路由模式hashhistory: createWebHashHistory(),routes: constantRoute,//滚动行为scrollBehavior() {return {left: 0,top: 0,}},
})
export default router
1.2.2路由配置

src\router\router.ts

//对外暴露配置路由(常量路由)
export const constantRoute = [{//登录路由path: '/login',component: () => import('@/views/login/index.vue'),name: 'login', //命名路由},{//登录成功以后展示数据的路由path: '/',component: () => import('@/views/home/index.vue'),name: 'layout',},{path: '/404',component: () => import('@/views/404/index.vue'),name: '404',},{//重定向path: '/:pathMatch(.*)*',redirect: '/404',name: 'Any',},
]

1.3路由出口

src\App.vue
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

2.登录模块

2.1 登录路由静态组件

src\views\login\index.vue

<template><div class="login_container"><el-row><el-col :span="12" :xs="0"></el-col><el-col :span="12" :xs="24"><el-form class="login_form"><h1>Hello</h1><h2>欢迎来到硅谷甄选</h2><el-form-item><el-input:prefix-icon="User"v-model="loginForm.username"></el-input></el-form-item><el-form-item><el-inputtype="password":prefix-icon="Lock"v-model="loginForm.password"show-password></el-input></el-form-item><el-form-item><el-button class="login_btn" type="primary" size="default">登录</el-button></el-form-item></el-form></el-col></el-row></div>
</template><script setup lang="ts">import { User, Lock } from '@element-plus/icons-vue'import { reactive } from 'vue'//收集账号与密码数据let loginForm = reactive({ username: 'admin', password: '111111' })
</script><style lang="scss" scoped>.login_container {width: 100%;height: 100vh;background: url('@/assets/images/background.jpg') no-repeat;background-size: cover;.login_form {position: relative;width: 80%;top: 30vh;background: url('@/assets/images/login_form.png') no-repeat;background-size: cover;padding: 40px;h1 {color: white;font-size: 40px;}h2 {color: white;font-size: 20px;margin: 20px 0px;}.login_btn {width: 100%;}}}
</style>

注意:el-col是24份的,在此左右分为了12份。我们在右边放置我们的结构。:xs="0"是为了响应式。el-form下的element-plus元素都用el-form-item包裹起来。

2.2 登陆业务实现

2.2.1 登录按钮绑定回调

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
回调应该做的事情

const login =  () => {//点击登录按钮以后干什么//通知仓库发起请求//请求成功->路由跳转//请求失败->弹出登陆失败信息
}
2.2.2 仓库store初始化
  1. 大仓库(笔记只写一次)

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

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

src\store\modules\user.ts

//创建用户相关的小仓库
import { defineStore } from 'pinia'
//创建用户小仓库
const useUserStore = defineStore('User', {//小仓库存储数据地方state: () => {},//处理异步|逻辑地方actions: {},getters: {},
})
//对外暴露小仓库
export default useUserStore
2.2.3 按钮回调
//登录按钮的回调
const login = async () => {//按钮加载效果loading.value = true//点击登录按钮以后干什么//通知仓库发起请求//请求成功->路由跳转//请求失败->弹出登陆失败信息try {//也可以书写.then语法await useStore.userLogin(loginForm)//编程式导航跳转到展示数据的首页$router.push('/')//登录成功的提示信息ElNotification({type: 'success',message: '登录成功!',})//登录成功,加载效果也消失loading.value = false} catch (error) {//登陆失败加载效果消失loading.value = false//登录失败的提示信息ElNotification({type: 'error',message: (error as Error).message,})}
}
2.2.4 用户仓库
//创建用户相关的小仓库
import { defineStore } from 'pinia'
//引入接口
import { reqLogin } from '@/api/user'
//引入数据类型
import type { loginForm } from '@/api/user/type'
//创建用户小仓库
const useUserStore = defineStore('User', {//小仓库存储数据地方state: () => {return {token: localStorage.getItem('TOKEN'), //用户唯一标识token}},//处理异步|逻辑地方actions: {//用户登录的方法async userLogin(data: loginForm) {//登录请求const result: any = await reqLogin(data)if (result.code == 200) {//pinia仓库存储token//由于pinia|vuex存储数据其实利用js对象this.token = result.data.token//本地存储持久化存储一份localStorage.setItem('TOKEN', result.data.token)//保证当前async函数返回一个成功的promise函数return 'ok'} else {return Promise.reject(new Error(result.data.message))}},},getters: {},
})
//对外暴露小仓库
export default useUserStore
2.2.5 小结
  1. Element-plus中ElNotification用法(弹窗):

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

//登录失败的提示信息ElNotification({type: 'error',message: (error as Error).message,})
  1. Element-plus中el-buttonloading属性。
  2. pinia使用actions、state的方式和vuex不同:需要引入函数创建实例
  3. $router的使用:也需要引入函数创建实例
  4. 在actions中使用state的token数据:this.token
  5. 类型定义需要注意。
  6. promise的使用和vue2现在看来是一样的。

2.3模板封装登陆业务

2.3.1 result返回类型封装
interface dataType {token?: stringmessage?: string
}//登录接口返回的数据类型
export interface loginResponseData {code: numberdata: 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 = ''//通过内置构造函数Dateconst hour = new Date().getHours()if (hour <= 9) {message = '早上'} else if (hour <= 14) {message = '上午'} else if (hour <= 18) {message = '下午'} else {message = '晚上'}return message
}
  1. 使用(引入后)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 效果

image.png

2.5 表单校验规则

2.5.1 表单校验
  1. 表单绑定项

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
**:model:**绑定的数据

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

:rules:对应要使用的规则

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

ref=“loginForms”:获取表单元素

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

Form 组件提供了表单验证的功能,只需为 rules 属性传入约定的验证规则,并将 form-Item 的 prop 属性设置为需要验证的特殊键值即可
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

页面左上角的这部分,我们将它做成子组件,并且封装方便维护以及修改。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

3.2.1 Logo子组件

在这里我们引用了封装好的setting

<template><div class="logo" v-if="setting.logoHidden"><img :src="setting.logo" alt="" /><p>{{ setting.title }}</p></div>
</template><script setup lang="ts">//引入设置标题与logo配置文件import setting from '@/setting'
</script><style lang="scss" scoped>.logo {width: 100%;height: $base-menu-logo-height;color: white;display: flex;align-items: center;padding: 20px;img {width: 40px;height: 40px;}p {font-size: $base-logo-title-fontSize;margin-left: 10px;}}
</style>
3.2.2 封装setting

为了方便我们以后对logo以及标题的修改。

//用于项目logo|标题配置
export default {title: '硅谷甄选运营平台', //项目的标题logo: '/public/logo.png', //项目logo设置logoHidden: true, //logo组件是否隐藏
}
3.2.3 使用

在layout组件中引入并使用

3.3 左侧菜单组件

3.3.1静态页面(未封装)

主要使用到了element-plus的**menu组件。**附带使用了滚动组件

<!-- 左侧菜单 -->
<div class="layout_slider"><Logo></Logo><!-- 展示菜单 --><!-- 滚动组件 --><el-scrollbar class="scrollbar"><!-- 菜单组件 --><el-menu background-color="#001529" text-color="white"><el-menu-item index="1">首页</el-menu-item><el-menu-item index="2">数据大屏</el-menu-item><!-- 折叠菜单 --><el-sub-menu index="3"><template #title><span>权限管理</span></template><el-menu-item index="3-1">用户管理</el-menu-item><el-menu-item index="3-2">角色管理</el-menu-item><el-menu-item index="3-3">菜单管理</el-menu-item></el-sub-menu></el-menu></el-scrollbar>
</div>
3.3.2 递归组件生成动态菜单

在这一部分,我们要根据路由生成左侧的菜单栏

  1. 父组件中写好的子组件结构提取出去
      <!-- 展示菜单 --><!-- 滚动组件 --><el-scrollbar class="scrollbar"><!-- 菜单组件 --><el-menu background-color="#001529" text-color="white"><!-- 更具路由动态生成菜单 --><Menu></Menu></el-menu></el-scrollbar>
  1. 动态菜单子组件:src\layout\menu\index.vue
  2. 处理路由

因为我们要根据路由以及其子路由作为我们菜单的一级|二级标题。因此我们要获取路由信息。

给路由中加入了路由元信息meta:它包含了2个属性:title以及hidden

{//登录路由path: '/login',component: () => import('@/views/login/index.vue'),name: 'login', //命名路由meta: {title: '登录', //菜单标题hidden: true, //路由的标题在菜单中是否隐藏},},
  1. 仓库引入路由并对路由信息类型声明(vue-router有对应函数)
//引入路由(常量路由)
import { constantRoute } from '@/router/routes'
。。。。。
//小仓库存储数据地方
state: (): UserState => {return {token: GET_TOKEN(), //用户唯一标识tokenmenuRoutes: constantRoute, //仓库存储生成菜单需要数组(路由)
}

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  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-itemindex="item.children[0].path"v-if="!item.children[0].meta.hidden"><template #title><el-icon><component :is="item.children[0].meta.icon"></component></el-icon><span>{{ item.children[0].meta.title }}</span></template></el-menu-item>
</template>

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

3.3.4 项目全部路由配置
  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个添加函数
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

3.3.5 Bug&&总结

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

bug:router-link不生效。
描述:当我点击跳转函数的时候,直接跳转到一个新页面,而不是layout组件展示的部分更新。
思路:首先输出了一下路径,发现路径没有错。其次,因为跳转到新页面,代表layout组件中的router-link不生效,删除router-link,发现没有影响。所以确定了是router-link没有生效。
解决:仔细检查了src\router\routes.ts文件,最后发现一级路由的**component关键字写错。**导致下面的二级路由没有和以及路由构成父子关系。所以会跳转到APP组件下的router-link
总结:router-link会根据下面的子路由来进行展示。如果发生了路由跳转不对的情况,去仔细检查一下路由关系有没有写对。APP是所有一级路由组件的父组件

3.3.6 动画 && 自动展示
  1. 将router-link封装成单独的文件并且添加一些动画
<template><!-- 路由组件出口的位置 --><router-view v-slot="{ Component }"><transition name="fade"><!-- 渲染layout一级路由的子路由 --><component :is="Component" /></transition></router-view>
</template><script setup lang="ts"></script><style lang="scss" scoped>.fade-enter-from {opacity: 0;}.fade-enter-active {transition: all 0.3s;}.fade-enter-to {opacity: 1;}
</style>
  1. 自动展示

当页面刷新时,菜单会自动收起。我们使用element-plus的**default-active **处理。$router.path为当前路由。
src\layout\index.vue
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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><imgsrc="../../../public/logo.png"style="width: 24px; height: 24px; margin: 0px 10px"/><!-- 下拉菜单 --><el-dropdown><span class="el-dropdown-link">admin<el-icon class="el-icon--right"><arrow-down /></el-icon></span><template #dropdown><el-dropdown-menu><el-dropdown-item>退出登陆</el-dropdown-item></el-dropdown-menu></template></el-dropdown></div></div>
</template><script setup lang="ts"></script><style lang="scss" scoped>
.tabbar {width: 100%;height: 100%;display: flex;justify-content: space-between;background-image: linear-gradient(to right,rgb(236, 229, 229),rgb(151, 136, 136),rgb(240, 234, 234));.tabbar_left {display: flex;align-items: center;margin-left: 20px;}.tabbar_right {display: flex;align-items: center;}
}
</style>

组件拆分:

<template><!-- 顶部左侧的图标 --><el-icon style="margin-right: 10px"><Expand></Expand></el-icon><!-- 左侧的面包屑 --><el-breadcrumb separator-icon="ArrowRight"><el-breadcrumb-item>权限挂历</el-breadcrumb-item><el-breadcrumb-item>用户管理</el-breadcrumb-item></el-breadcrumb>
</template><script setup lang="ts"></script><style lang="scss" scoped></style>
<template><el-button size="small" icon="Refresh" circle></el-button><el-button size="small" icon="FullScreen" circle></el-button><el-button size="small" icon="Setting" circle></el-button><imgsrc="../../../../public/logo.png"style="width: 24px; height: 24px; margin: 0px 10px"/><!-- 下拉菜单 --><el-dropdown><span class="el-dropdown-link">admin<el-icon class="el-icon--right"><arrow-down /></el-icon></span><template #dropdown><el-dropdown-menu><el-dropdown-item>退出登陆</el-dropdown-item></el-dropdown-menu></template></el-dropdown>
</template><script setup lang="ts"></script><style lang="scss" scoped></style>
3.4.2 菜单折叠
  1. 折叠变量

定义一个折叠变量来判断现在的状态是否折叠。因为这个变量同时给breadcrumb组件以及父组件layout使用,因此将这个变量定义在pinia中

//小仓库:layout组件相关配置仓库
import { defineStore } from 'pinia'let useLayOutSettingStore = defineStore('SettingStore', {state: () => {return {fold: false, //用户控制菜单折叠还是收起的控制}},
})export default useLayOutSettingStore
  1. 面包屑组件点击图标切换状态
<template><!-- 顶部左侧的图标 --><el-icon style="margin-right: 10px" @click="changeIcon"><component :is="LayOutSettingStore.fold ? 'Fold' : 'Expand'"></component></el-icon>。。。。。。。
</template><script setup lang="ts">
import useLayOutSettingStore from '@/store/modules/setting'
//获取layout配置相关的仓库
let LayOutSettingStore = useLayOutSettingStore()//点击图标的切换
const changeIcon = () => {//图标进行切换LayOutSettingStore.fold = !LayOutSettingStore.fold
}
</script>
。。。。。。
  1. layout组件根据fold状态来修改个子组件的样式(以左侧菜单为例)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
绑定动态样式修改scss
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
效果图:
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
注意:折叠文字的时候会把图标也折叠起来。在menu组件中吧图标放到template外面就可以。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

3.4.3 顶部面包屑动态展示
  1. 引入$route

注意 r o u t e r 和 router和 routerroute是不一样的

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

注意:使用了$route.matched函数,此函数能得到当前路由的信息
image.png

  1. 首页修改

访问首页时,因为它是二级路由,会遍历出layout面包屑,处理:删除layout路由的title。再加上一个判断
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 面包屑点击跳转

注意:将路由中的一级路由权限管理以及商品管理重定向到第一个孩子,这样点击跳转的时候会定向到第一个孩子。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

3.4.4 刷新业务的实现
  1. 使用pinia定义一个变量作为标记

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

<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 = falsenextTick(() => {flag.value = true})},
)
</script>
3.4.5 全屏模式的实现
  1. 给全屏按钮绑定函数

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

4.部分功能处理完善

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

4.1 登录获取用户信息(TOKEN)

登录之后页面(home)上来就要获取用户信息。并且将它使用到页面中

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

import type { RouteRecordRaw } from 'vue-router'
//定义小仓库数据state类型
export interface UserState {token: string | nullmenuRoutes: RouteRecordRaw[]username: stringavatar: 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.usernamethis.avatar = result.data.checkUser.avatar}},
  1. 更新tabbar的信息(记得先引入并创建实例)

src\layout\tabbar\setting\index.vue
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

4.2 退出功能

  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. 登录按钮进行判断

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

4.3 路由守卫

src\permisstion.ts(新建文件)
main.ts引入
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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?

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
个人理解:之前在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,三个环境文件下的服务器域名写为:
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  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(), //用户唯一标识tokenmenuRoutes: 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.namethis.avatar = result.data.avatarreturn '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. 退出登录按钮的点击函数修改

退出成功后再跳转
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

src\permisstion.ts
也是退出成功后再跳转
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

4.5 接口类型定义

//登录接口需要携带参数类型
export interface loginFormData {username: stringpassword: string
}//定义全部接口返回数据都有的数据类型
export interface ResponseData {code: numbermessage: stringok: boolean
}
//定义登录接口返回数据类型
export interface loginResponseData extends ResponseData {data: string
}//定义获取用户信息返回的数据类型
export interface userInfoResponseData extends ResponseData {data: {routes: string[]button: string[]roles: string[]name: stringavatar: string}
}

注意:在src\store\modules\user.ts以及src\api\user\index.ts文件中对发请求时的参数以及返回的数据添加类型定义

5.品牌管理模块

5.1 静态组件

使用element-plus。

<template><el-card class="box-card"><!-- 卡片顶部添加品牌按钮 --><el-button type="primary" size="default" icon="Plus">添加品牌</el-button><!-- 表格组件,用于展示已有的数据 --><!-- table---border:是否有纵向的边框table-column---lable:某一个列表---width:设置这一列的宽度---align:设置这一列对齐方式--><el-table style="margin: 10px 0px" border><el-table-columnlabel="序号"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-paginationv-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 = pagerlet result = await reqHasTrademark(pageNo.value, limit.value)console.log(result)if (result.code == 200) {//存储已有品牌总个数total.value = result.data.totaltrademarkArr.value = result.data.recordsconsole.log(trademarkArr)}
}
//组件挂载完毕钩子---发一次请求,获取第一页、一页三个已有品牌数据
onMounted(() => {getHasTrademark()
})
</script>
5.2.2 数据展示

在数据展示模块,我们使用了element-plus的**el-table,**下面组要讲解属性和注意点。

  1. data属性:显示的数据

比如我们这里绑定的trademarkArr是个三个对象的数组,就会多出来3行。外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

注意:因为我们之前已经绑定了数据,所以在这里直接使用数据的属性tmName
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. el-table-column的插槽

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
为什么要使用插槽呢?因为prop属性虽然能够展示数据,但是他默认是div,如果我们的图片使用prop展示的话,会展示图片的路径。因此如果想展示图片或者按钮,我们就要使用插槽
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
注意:row就是我们的trademarkArr的每一个数据(对象)

5.3 品牌类型定义

API中的以及组件中。

export interface ResponseData {code: numbermessage: stringok: boolean
}//已有的品牌的ts数据类型
export interface TradeMark {id?: numbertmName: stringlogoUrl: string
}//包含全部品牌数据的ts类型
export type Records = TradeMark[]//获取的已有全部品牌的数据ts类型
export interface TradeMarkResponseData extends ResponseData {data: {records: Recordstotal: numbersize: numbercurrent: numbersearchCount: booleanpages: number}
}

5.4 分页展示数据

此部分主要是俩个功能,第一个是当点击分页器页数时能跳转到对应的页数。第二个是每页展示的数据条数能正确显示

5.4.1 跳转页数函数

这里我们绑定的点击回调直接用的是之前写好的发送请求的回调。可以看出,发送请求的回调函数是有默认的参数:1.
注意:因为current-change方法时element-plus封装好的,它会给父组件传递并注入一个参数(点击的页码),所以相当于把这个参数传递给了getHasTrademark函数,因此能够跳转到正确的页码数
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

//获取已有品牌的接口封装为一个函数:在任何情况下向获取数据,调用次函数即可
const getHasTrademark = async (pager = 1) => {//当前页码pageNo.value = pagerlet result: TradeMarkResponseData = await reqHasTrademark(pageNo.value,limit.value,)if (result.code == 200) {//存储已有品牌总个数total.value = result.data.totaltrademarkArr.value = result.data.records}
}
5.4.2 每页展示数据条数

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

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

5.5 dialog对话框静态搭建

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

v-model:属性用户控制对话框的显示与隐藏的 true显示 false隐藏
title:设置对话框左上角标题
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 表单项
    <el-form style="width: 80%"><el-form-item label="品牌名称" label-width="100px" prop="tmName"><el-inputplaceholder="请您输入品牌名称"v-model="trademarkParams.tmName"></el-input></el-form-item><el-form-item label="品牌LOGO" label-width="100px" prop="logoUrl"><!-- upload组件属性:action图片上传路径书写/api,代理服务器不发送这次post请求  --><el-uploadclass="avatar-uploader"action="/api/admin/product/fileUpload":show-file-list="false":on-success="handleAvatarSuccess":before-upload="beforeAvatarUpload"><imgv-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. 收集品牌名称

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. upload组件的属性介绍
<el-uploadclass="avatar-uploader"action="/api/admin/product/fileUpload":show-file-list="false":on-success="handleAvatarSuccess":before-upload="beforeAvatarUpload"><imgv-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 4Mif (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. 上传图片后,用图片代替加号

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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就是当前的数据
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 小问题

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

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

5.7 品牌管理模块表单校验

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
:model:校验的数据
:rules:校验规则
ref=“formRef”:表单实例
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
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 = 0trademarkParams.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绑定的就是回调函数
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

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

注意:要在src\components\index.ts下引入。

<template><el-card><el-form inline><el-form-item label="一级分类"><el-select><el-option label="北京"></el-option><el-option label="深圳"></el-option><el-option label="广州"></el-option></el-select></el-form-item><el-form-item label="二级分类"><el-select><el-option label="北京"></el-option><el-option label="深圳"></el-option><el-option label="广州"></el-option></el-select></el-form-item><el-form-item label="三级分类"><el-select><el-option label="北京"></el-option><el-option label="深圳"></el-option><el-option label="广州"></el-option></el-select></el-form-item></el-form></el-card>
</template><script setup lang="ts"></script><style lang="" scoped></style>
6.1.2 添加属性模块(静态)
<template><!-- 三级分类全局组件--><Category></Category><el-card style="margin: 10px 0px"><el-button type="primary" size="default" icon="Plus">添加属性</el-button><el-table border style="margin: 10px 0px"><el-table-columnlabel="序号"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: [],//存储一级分类的IDc1Id: '',}},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-optionv-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: numbermessage: stringok: boolean
}//分类ts类型
export interface CategoryObj {id: number | stringname: stringcategory1Id?: numbercategory2Id?: 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 | numberc1Arr: CategoryObj[]c2Arr: CategoryObj[]c2Id: string | numberc3Arr: CategoryObj[]c3Id: string | number
}

使用:仓库中的state数据类型

6.4 完成分类组件业务

分类组件就是以及组件上来就拿到数据,通过用户选择后我们会拿到id,通过id发送请求之后二级分类就会拿到数据。以此类推三级组件。我们以二级分类为例。

6.4.1 二级分类流程
  1. 绑定函数

二级分类不是一上来就发生变化,而是要等一级分类确定好之后再发送请求获得数据。于是我们将这个发送请求的回调函数绑定在了一级分类的change属性上
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 三级组件同理
6.4.2 小问题

当我们选择好三级菜单后,此时修改一级菜单。二、三级菜单应该清空
清空id之后就不会显示了。

//此方法即为一级分类下拉菜单的change事件(选中值的时候会触发,保证一级分类ID有了)
const handler = () => {//需要将二级、三级分类的数据清空categoryStore.c2Id = ''categoryStore.c3Arr = []categoryStore.c3Id = ''//通知仓库获取二级分类的数据categoryStore.getC2()
}
//此方法即为二级分类下拉菜单的change事件(选中值的时候会触发,保证二级分类ID有了)
const handler1 = () => {//清理三级分类的数据categoryStore.c3Id = ''categoryStore.getC3()
}
6.4.3 添加属性按钮禁用

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

src\views\product\attr\index.vue(父组件)
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

6.5 已有属性与属性值展示外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

6.5.1 返回type类型
//属性值对象的ts类型
export interface AttrValue {id?: numbervalueName: stringattrId?: numberflag?: boolean
}//存储每一个属性值的数组类型
export type AttrValueList = AttrValue[]
//属性对象
export interface Attr {id?: numberattrName: stringcategoryId: number | stringcategoryLevel: numberattrValueList: 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,() => {//获取分类的IDgetAttr()},
)
//获取已有的属性与属性值方法
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-buttontype="primary"size="default"icon="Plus":disabled="categoryStore.c3Id ? false : true">添加属性</el-button><el-table border style="margin: 10px 0px" :data="attrArr"><el-table-columnlabel="序号"type="index"align="center"width="80px"></el-table-column><el-table-columnlabel="属性名称"width="120px"prop="attrName"></el-table-column><el-table-column label="属性值名称"><!-- row:已有的属性对象 --><template #="{ row, $index }"><el-tagstyle="margin: 5px"v-for="(item, index) in row.attrValueList":key="item.id">{{ item.valueName }}</el-tag></template></el-table-column><el-table-column label="操作" width="120px"><!-- row:已有的属性对象 --><template #="{ row, $index }"><!-- 修改已有属性的按钮 --><el-button type="primary" size="small" icon="Edit"></el-button><el-button type="primary" size="small" icon="Delete"></el-button></template></el-table-column></el-table></el-card>
6.5.5 小问题

当我们获取数据并展示以后,此时修改一级分类或者二级分类,由于watch的存在,同样会发送请求。但是此时没有c3Id,请求会失败。因此将watch改为如下

//监听仓库三级分类ID变化
watch(() => categoryStore.c3Id,() => {//清空上一次查询的属性与属性值attrArr.value = []//保证三级分类得有才能发请求if (!categoryStore.c3Id) return//获取分类的ID getAttr()},
)

6.6 添加属性页面的静态展示

当点击添加属性后:
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

6.6.2 表单

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

6.6.3 按钮

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

6.6.4 表格

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

6.6.5按钮

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

6.6.6 三级分类禁用

当点击添加属性之后,三级分类应该被禁用。因此使用props给子组件传参
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
子组件:
二三级分类同理。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

6.7.1修改属性

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

6.7.2 添加属性

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

6.7.3 type
//属性值对象的ts类型
export interface AttrValue {id?: numbervalueName: stringattrId?: numberflag?: boolean
}//存储每一个属性值的数组类型
export type AttrValueList = AttrValue[]
//属性对象
export interface Attr {id?: numberattrName: stringcategoryId: number | stringcategoryLevel: numberattrValueList: AttrValueList
}
6.7.4 组件收集新增的属性的数据
//收集新增的属性的数据
let attrParams = reactive<Attr>({attrName: '', //新增的属性的名字attrValueList: [//新增的属性值数组],categoryId: '', //三级分类的IDcategoryLevel: 3, //代表的是三级分类
})

6.8 添加属性值

一个操作最重要的是理清楚思路。添加属性值的总体思路是:收集表单的数据(绑定对应的表单项等)->发送请求(按钮回调函数,携带的参数)->更新页面

6.8.1 收集表单的数据(attrParams)
  1. 属性名称(attrName)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 属性值数组(attrValueList)

我们给添加属性值按钮绑定一个回调,点击的时候会往attrParams.attrValueList中添加一个空数组。我们根据空数组的数量生成input框,再将input的值与数组中的值绑定。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 三级分类的id(categoryId)

三级分类的id(c3Id)在页面1的添加属性按钮之前就有了,因此我们把它放到添加属性按钮的回调身上
注意:每一次点击的时候,先清空一下数据再收集数据。防止下次点击时会显示上次的数据

//添加属性按钮的回调
const addAttr = () => {//每一次点击的时候,先清空一下数据再收集数据Object.assign(attrParams, {attrName: '', //新增的属性的名字attrValueList: [//新增的属性值数组],categoryId: categoryStore.c3Id, //三级分类的IDcategoryLevel: 3, //代表的是三级分类})//切换为添加与修改属性的结构scene.value = 1
}
  1. categoryLevel(固定的,无需收集)
6.8.2 发送请求&&更新页面

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

//保存按钮的回调
const save = async () => {//发请求let result: any = await reqAddOrUpdateAttr(attrParams)//添加属性|修改已有的属性已经成功if (result.code == 200) {//切换场景scene.value = 0//提示信息ElMessage({type: 'success',message: attrParams.id ? '修改成功' : '添加成功',})//获取全部已有的属性与属性值(更新页面)getAttr()} else {ElMessage({type: 'error',message: attrParams.id ? '修改失败' : '添加失败',})}
}

6.9 属性值的编辑与查看模式

6.9.1 模板的切换

在input下面添加了一个div,使用flag来决定哪个展示。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
注意:flag放在哪?由于每一个属性值对象都需要一个flag属性,因此将flag的添加放在添加属性值的按钮的回调上。(注意修改属性值的type)

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

src\api\product\attr\type.ts
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

6.9.2 切换的回调
//属性值表单元素失却焦点事件回调
const toLook = (row: AttrValue, $index: number) => {。。。。。。//相应的属性值对象flag:变为false,展示divrow.flag = false
}//属性值div点击事件
const toEdit = (row: AttrValue, $index: number) => {//相应的属性值对象flag:变为true,展示inputrow.flag = true。。。。。。
}
6.9.3 处理非法属性值
//属性值表单元素失却焦点事件回调
const toLook = (row: AttrValue, $index: number) => {//非法情况判断1if (row.valueName.trim() == '') {//删除调用对应属性值为空的元素attrParams.attrValueList.splice($index, 1)//提示信息ElMessage({type: 'error',message: '属性值不能为空',})return}//非法情况2let 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,展示divrow.flag = false
}

6.10 表单聚焦&&删除按钮

表单聚焦可以直接调用input提供foces方法:当选择器的输入框获得焦点时触发

6.10.1 存储组件实例

使用ref的函数形式,每有一个input就将其存入inputArr中

//准备一个数组:将来存储对应的组件实例el-input
let inputArr = ref<any>([])

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

6.10.2 点击div转换成input框后的自动聚焦

注意:使用nextTick是因为点击后,组件需要加载,没办法第一时间拿到组件实例。所以使用nextTick会等到组件加载完毕后才调用,达到聚焦效果。

//属性值div点击事件
const toEdit = (row: AttrValue, $index: number) => {//相应的属性值对象flag:变为true,展示inputrow.flag = true//nextTick:响应式数据发生变化,获取更新的DOM(组件实例)nextTick(() => {inputArr.value[$index].focus()})
}
6.10.3 添加属性值自动聚焦
//添加属性值按钮的回调
const addAttrValue = () => {//点击添加属性值按钮的时候,向数组添加一个属性值对象attrParams.attrValueList.push({valueName: '',flag: true, //控制每一个属性值编辑模式与切换模式的切换})//获取最后el-input组件聚焦nextTick(() => {inputArr.value[attrParams.attrValueList.length - 1].focus()})
}
6.10.4 删除按钮

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

6.11属性修改业务

6.11.1属性修改业务

修改业务很简单:当我们点击修改按钮的时候,将修改的实例(row)传递给回调函数。回调函数:首先跳转到第二页面,第二页面是根据attrParams值生成的,我们跳转的时候将实例的值传递给attrParams
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

//table表格修改已有属性按钮的回调
const updateAttr = (row: Attr) => {//切换为添加与修改属性的结构scene.value = 1//将已有的属性对象赋值给attrParams对象即为//ES6->Object.assign进行对象的合并Object.assign(attrParams, JSON.parse(JSON.stringify(row)))
}
6.11.2 深拷贝与浅拷贝

深拷贝和浅拷贝的区别
1.浅拷贝: 将原对象或原数组的引用直接赋给新对象,新数组,新对象/数组只是原对象的一个引用
2.深拷贝: 创建一个新的对象和数组,将原对象的各项属性的“值”(数组的所有元素)拷贝过来,是“值”而不是“引用”

这里存在一个问题,也就是当我们修改属性值后,并没有保存(发请求),但是界面还是改了。这是因为我们的赋值语句:Object.assign(attrParams, row)是浅拷贝。相当于我们在修改服务器发回来的数据并展示在页面上。服务器内部并没有修改。
解决:将浅拷贝改为深拷贝:Object.assign(attrParams, JSON.parse(JSON.stringify(row)))

6.12 删除按钮&&清空数据

6.12.1删除按钮
  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. 绑定点击函数&&气泡弹出框

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

<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-columnlabel="序号"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-paginationv-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: numbermessage: stringok: boolean
}//SPU数据的ts类型:需要修改
export interface SpuData {category3Id: string | numberid?: numberspuName: stringtmId: number | stringdescription: stringspuImageList: nullspuSaleAttrList: null
}
//数组:元素都是已有SPU数据类型
export type Records = SpuData[]
//定义获取已有的SPU接口返回的数据ts类型
export interface HasSpuResponseData extends ResponseData {data: {records: Recordstotal: numbersize: numbercurrent: numbersearchCount: booleanpages: number}
}
7.2.3 添加SPU按钮

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

7.2.4 表单数据
<el-table border style="margin: 10px 10px" :data="records"><el-table-columnlabel="序号"type="index"align="center"width="80px"></el-table-column><el-table-column label="SPU名称" prop="spuName"></el-table-column><el-table-columnlabel="SPU描述"prop="description"show-overflow-tooltip></el-table-column><el-table-column label="SPU操作"><!-- row:即为已有的SPU对象 --><template #="{ row, $index }"><el-buttontype="primary"size="small"icon="Plus"title="添加SKU"></el-button><el-buttontype="primary"size="small"icon="Edit"title="修改SPU"></el-button><el-buttontype="primary"size="small"icon="View"title="查看SKU列表"></el-button><el-popconfirm :title="`你确定删除${row.spuName}?`" width="200px"><template #reference><el-buttontype="primary"size="small"icon="Delete"title="删除SPU"></el-button></template></el-popconfirm></template></el-table-column>
</el-table>
7.2.5 分页器

注意getHasSpu函数携带的参数。默认为1

<!-- 分页器 --><el-paginationv-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 = pagerlet result: HasSpuResponseData = await reqHasSpu(pageNo.value,pageSize.value,categoryStore.c3Id,)if (result.code == 200) {records.value = result.data.recordstotal.value = result.data.total}
}
//分页器下拉菜单发生变化的时候触发
const changeSize = () => {getHasSpu()
}
7.2.6 watch监听
//监听三级分类ID变化
watch(() => categoryStore.c3Id,() => {//当三级分类发生变化的时候清空对应的数据records.value = []//务必保证有三级分类IDif (!categoryStore.c3Id) returngetHasSpu()},
)

7.3 SPU场景一的静态&&场景切换

7.3.1 子组件搭建

由于SPU模块需要在三个场景进行切换,全都放在一个组件里面的话会显得很臃肿。因此我们将它放到三个组件当中。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
使用v-show来展示页面:v-if是销毁组件,v-show是隐藏组件。在初加载的时候v-if比较快,但是在频繁切换的时候v-if任务重。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

7.3.2 SPU场景一子组件静态

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

<template><el-form label-width="100px"><el-form-item label="SPU名称"><el-input placeholder="请你输入SPU名称"></el-input></el-form-item><el-form-item label="SPU品牌"><el-select><el-option label="华为"></el-option><el-option label="oppo"></el-option><el-option label="vivo"></el-option></el-select></el-form-item><el-form-item label="SPU描述"><el-input type="textarea" placeholder="请你输入SPU描述"></el-input></el-form-item><el-form-item label="SPU图片"><el-uploadv-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-buttonstyle="margin-left: 10px"type="primary"size="default"icon="Plus">添加属性</el-button><!-- table展示销售属性与属性值的地方 --><el-table border style="margin: 10px 0px"><el-table-columnlabel="序号"type="index"align="center"width="80px"></el-table-column><el-table-columnlabel="销售属性名字"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结构->SpuFormscene.value = 1
}//修改已有的SPU的按钮的回调
const updateSpu = () => {//切换为场景1:添加与修改已有SPU结构->SpuFormscene.value = 1
}
7.3.4 子组件中取消按钮的回调

需要改变的是父组件中的scene,因此涉及到父子组件通信。这里使用自定义事件。
父组件:
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
子组件:
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

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

修改和添加的页面是差不多的。页面1的四个地方都需要发请求拿数据,我们在这一部分分别编写4个部分的API以及ts类型
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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: numbertmName: stringlogoUrl: 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?: numberimgName?: stringimgUrl?: stringcreateTime?: stringupdateTime?: stringspuId?: numbername?: stringurl?: 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: numbername: 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?: numbercreateTime?: nullupdateTime?: nullspuId?: numberbaseSaleAttrId: number | stringsaleAttrName: stringspuSaleAttrValueList: SpuSaleAttrValueListflag?: booleansaleAttrValue?: string
}
//SPU已有的销售属性接口返回数据ts类型
export interface SaleAttrResponseData extends ResponseData {data: SaleAttr[]
}

7.5 获取SPU的数据

首先:SPU的数据应该分为5部分:第一部分:是父组件里的展示的数据,也是我们点击修改按钮时的那个数据。其余4个部分的数据需要我们发请求得到。
问题1:子组件需要用到父组件中的数据,应该怎么办?答:要传递的数据是指定的,也就是我们点击修改时的数据。通过ref的方式,拿到子组件时的实例,再调用子组件暴露的方法将数据做为参数传递过去。(有点类似于反向的自定义事件)
问题2:其余4个部分的数据什么时候获取。答:同样的在点击修改按钮时获取,问题一中通过调用子组件的函数传递数据,我们同时也在这个函数中发请求得到数据

7.5.1 第一部分数据的传递
  1. 父组件拿到子组件实例

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 子组件暴露对外函数

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

//修改已有的SPU的按钮的回调
const updateSpu = (row: SpuData) => {//切换为场景1:添加与修改已有SPU结构->SpuFormscene.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)
/追加一个新的SPUADDSPU_URL = '/admin/product/saveSpuInfo',//更新已有的SPUUPDATESPU_URL = '/admin/product/updateSpuInfo',
//添加一个新的SPU的
//更新已有的SPU接口
//data:即为新增的SPU|或者已有的SPU对象
export const reqAddOrUpdateSpu = (data: any) => {//如果SPU对象拥有ID,更新已有的SPUif (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 | numberid?: numberspuName: stringtmId: number | stringdescription: stringspuImageList: null | SpuImg[]spuSaleAttrList: null | SaleAttr[]
}

7.7 展示与收集已有的数据

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

7.7.3 展示SPU品牌

注意:下方的红框展示的是所有品牌,上方的绑定的是一个数字也就是下方的第几个
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

7.7.4 SPU描述

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

7.7.5 照片墙PART

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

  1. 整体结构

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
上面el-upload是上传照片的照片墙,下面是查看照片的对话框

  1. v-model:file-list

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

//商品图片
let imgList = ref<SpuImg[]>([])
//子组件书写一个方法
const initHasSpuData = async (spu: SpuData) => {。。。。。。//获取某一个品牌旗下全部售卖商品的图片let result1: SpuHasImg = await reqSpuImageList(spu.id as number)......//SPU对应商品图片imgList.value = result1.data.map((item) => {return {name: item.imgName,url: item.imgUrl,}})......
}

这部分是一个双向绑定的数据,我们从服务器得到数据会展示到照片墙上。得到数据的过程我们使用了数组的map方法,这是因为组件对于数据的格式有要求。

  1. action

action是指图片上传的地址。组件还会将返回的数据放到对应的img的数据中
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. :on-preview

预览的钩子,预览照片时会触发。会注入对应图片的数据。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

数据结构如下:
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

7.8.1 展示销售属性与属性值

其实就是4列,对应好每一列以及对应的数据就好

<!-- table展示销售属性与属性值的地方 --><el-table border style="margin: 10px 0px" :data="saleAttr"><el-table-columnlabel="序号"type="index"align="center"width="80px"></el-table-column><el-table-columnlabel="销售属性名字"width="120px"prop="saleAttrName"></el-table-column><el-table-column label="销售属性值"><!-- row:即为当前SPU已有的销售属性对象 --><template #="{ row, $index }"><el-tagclass="mx-1"closablestyle="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-buttontype="primary"size="small"icon="Delete"@click="saleAttr.splice($index, 1)"></el-button></template></el-table-column></el-table>
7.8.2 删除操作

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

<el-table-column label="操作" width="120px"><template #="{ row, $index }"><el-buttontype="primary"size="small"icon="Delete"@click="saleAttr.splice($index, 1)"></el-button></template></el-table-column>

7.9 完成收集新增销售属性业务

7.9.1 计算出还未拥有的销售属性
//计算出当前SPU还未拥有的销售属性
let unSelectSaleAttr = computed(() => {//全部销售属性:颜色、版本、尺码//已有的销售属性:颜色、版本let unSelectArr = allSaleAttr.value.filter((item) => {return saleAttr.value.every((item1) => {return item.name != item1.saleAttrName})})return unSelectArr
})

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

7.9.3 添加属性按钮的回调

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

//添加销售属性的方法
const addSaleAttr = () => {/*"baseSaleAttrId": number,"saleAttrName": string,"spuSaleAttrValueList": SpuSaleAttrValueList*/const [baseSaleAttrId, saleAttrName] = saleAttrIdAndValueName.value.split(':')//准备一个新的销售属性对象:将来带给服务器即可let newSaleAttr: SaleAttr = {baseSaleAttrId,saleAttrName,spuSaleAttrValueList: [],}//追加到数组当中saleAttr.value.push(newSaleAttr)//清空收集的数据saleAttrIdAndValueName.value = ''
}

7.10 销售属性值的添加删除业务

其实销售属性值和之前的添加属性业务差不多。最重要的是熟悉数据的结构。步骤分为:组件收集数据->回调中将数据整理后push到对应的数组中。

7.10.1 添加按钮与input框的切换

通过flag属性。一上来是没有的,点击按钮添加。输入框输入完毕blur时再将flag变为false
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

收集的数据有俩个
saleAttrValue:点击添加按钮时初始化为空,收集输入的信息
baseSaleAttrId:所在的数据的id。由row给出
其余做的事就是:非法数据的过滤
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

//表单元素失却焦点的事件回调
const toLook = (row: SaleAttr) => {//整理收集的属性的ID与属性值的名字const { baseSaleAttrId, saleAttrValue } = row//整理成服务器需要的属性值形式let newSaleAttrValue: SaleAttrValue = {baseSaleAttrId,saleAttrValueName: saleAttrValue as string,}//非法情况判断if ((saleAttrValue as string).trim() == '') {ElMessage({type: 'error',message: '属性值不能为空的',})return}//判断属性值是否在数组当中存在let repeat = row.spuSaleAttrValueList.find((item) => {return item.saleAttrValueName == saleAttrValue})if (repeat) {ElMessage({type: 'error',message: '属性值重复',})return}//追加新的属性值对象row.spuSaleAttrValueList.push(newSaleAttrValue)//切换为查看模式row.flag = false
}
7.10.3 删除属性值

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

7.12 保存

整理数据+发送请求+通知父组件更新页面

//保存按钮的回调
const save = async () => {//整理参数//发请求:添加SPU|更新已有的SPU//成功//失败//1:照片墙的数据SpuParams.value.spuImageList = imgList.value.map((item: any) => {return {imgName: item.name, //图片的名字imgUrl: (item.response && item.response.data) || item.url,}})//2:整理销售属性的数据SpuParams.value.spuSaleAttrList = saleAttr.valuelet 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结构->SpuFormscene.value = 1//点击添加SPU按钮,调用子组件的方法初始化数据spu.value.initAddSpu(categoryStore.c3Id)
}
  1. 子组件收集数据

注意要对外暴露,让父组件可以使用

//添加一个新的SPU初始化请求方法
const initAddSpu = async (c3Id: number | string) => {//存储三级分类的IDSpuParams.value.category3Id = c3Id//获取全部品牌的数据let result: AllTradeMark = await reqAllTradeMark()let result1: HasSaleAttrResponseData = await reqAllSaleAttr()//存储数据MYAllTradeMark.value = result.dataallSaleAttr.value = result1.data
}
//对外暴露
defineExpose({ initHasSpuData, initAddSpu })
  1. 整理数据与发送请求

这部分通过保存按钮的回调已经做完了。

7.13.2 清空数据

我们应该在每次添加spu前清空上次的数据。

//添加一个新的SPU初始化请求方法
const initAddSpu = async (c3Id: number | string) => {//清空数据Object.assign(SpuParams.value, {category3Id: '', //收集三级分类的IDspuName: '', //SPU的名字description: '', //SPU的描述tmId: '', //品牌的IDspuImageList: [],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:展示已有的SPUscene.value = obj.flagif (obj.params == 'update') {//更新留在当前页getHasSpu(pageNo.value)} else {//添加留在第一页getHasSpu()}
}

7.14添加SKU的静态

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

7.14.1 绑定回调

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

//添加SKU按钮的回调
const addSku = (row: SpuData) => {//点击添加SKU按钮切换场景为2scene.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-columntype="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按钮切换场景为2scene.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-columntype="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 //平台属性的IDvalueId: number | string //属性值的ID
}
export interface saleArr {saleAttrId: number | string //属性IDsaleAttrValueId: number | string //属性值的ID
}
export interface SkuData {category3Id: string | number //三级分类的IDspuId: string | number //已有的SPU的IDtmId: string | number //SPU品牌的IDskuName: 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.category3IdskuParams.spuId = spu.idskuParams.tmId = spu.tmId。。。。。。
}
7.16.3 input框收集数据

sku名称、价格、重量、sku描述都是收集的用户输入的数据。我们直接使用v-model
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
我们在数据绑定的时候将这俩个属性所选择的数据绑定到自身。之后整合数据的时候通过遍历得到

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

//设置默认图片的方法回调
const handler = (row: any) => {//点击的时候,全部图片的的复选框不勾选imgArr.value.forEach((item: any) => {table.value.toggleRowSelection(item, false)})//选中的图片才勾选table.value.toggleRowSelection(row, true)//收集图片地址skuParams.skuDefaultImg = row.imgUrl
}

7.17 完成添加sku

7.17.1 整合数据&&发请求
//收集SKU的参数
let skuParams = reactive<SkuData>({//父组件传递过来的数据category3Id: '', //三级分类的IDspuId: '', //已有的SPU的IDtmId: '', //SPU品牌的ID//v-model收集skuName: '', //sku名字price: '', //sku价格weight: '', //sku重量skuDesc: '', //sku的描述skuAttrValueList: [//平台属性的收集],skuSaleAttrValueList: [//销售属性],skuDefaultImg: '', //sku图片地址
})
//保存按钮的方法
const save = async () => {//整理参数//平台属性skuParams.skuAttrValueList = attrArr.value.reduce((prev: any, next: any) => {if (next.attrIdAndValueId) {let [attrId, valueId] = next.attrIdAndValueId.split(':')prev.push({attrId,valueId,})}return prev}, [])//销售属性skuParams.skuSaleAttrValueList = saleArr.value.reduce((prev: any, next: any) => {if (next.saleIdAndValueId) {let [saleAttrId, saleAttrValueId] = next.saleIdAndValueId.split(':')prev.push({saleAttrId,saleAttrValueId,})}return prev},[],)//添加SKU的请求let result: any = await reqAddSku(skuParams)if (result.code == 200) {ElMessage({type: 'success',message: '添加SKU成功',})//通知父组件切换场景为零$emit('changeScene', { flag: 0, params: '' })} else {ElMessage({type: 'error',message: '添加SKU失败',})}
}
7.17.2 bug

bug1:在发送请求的时候返回时undefined:注意;这种情况一般是由于API的请求函数没有写返回值(格式化之后)
bug2:平台属性和销售属性收集不到。可能时element-plus自带的table校验。前面数据填的格式不对(比如重量和价格input确定是数字但是可以输入字母e,这时候会导致错误)或者没有填写会导致后面的数据出问题。

7.18 sku展示

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

7.18.1 API&&type

API:

//查看某一个已有的SPU下全部售卖的商品SKUINFO_URL = '/admin/product/findBySpuId/',
//获取SKU数据
export const reqSkuList = (spuId: number | string) => {return request.get<any, SkuInfoData>(API.SKUINFO_URL + spuId)
}

TYPE

//获取SKU数据接口的ts类型
export interface SkuInfoData extends ResponseData {data: SkuData[]
}
7.18.2 绑定点击函数&&回调

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

//存储全部的SKU数据
let skuArr = ref<SkuData[]>([])
let show = ref<boolean>(false)
//查看SKU列表的数据
const findSku = async (row: SpuData) => {let result: SkuInfoData = await reqSkuList(row.id as number)if (result.code == 200) {skuArr.value = result.data//对话框显示出来show.value = true}
}
7.18.3 模板展示

其实就是弹出一个对话框dialog,然后里面是一个form

<!-- dialog对话框:展示已有的SKU数据 --><el-dialog v-model="show" title="SKU列表"><el-table border :data="skuArr"><el-table-column label="SKU名字" prop="skuName"></el-table-column><el-table-column label="SKU价格" prop="price"></el-table-column><el-table-column label="SKU重量" prop="weight"></el-table-column><el-table-column label="SKU图片"><template #="{ row, $index }"><img:src="row.skuDefaultImg"style="width: 100px; height: 100px"/></template></el-table-column></el-table></el-dialog>

7.19 删除spu业务

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

7.19.1 API

type为any,因此没有写专门的type

//删除已有的SPU
REMOVESPU_URL = '/admin/product/deleteSpu/',
//删除已有的SPU
export const reqRemoveSpu = (spuId: number | string) => {return request.delete<any, any>(API.REMOVESPU_URL + spuId)
}
7.19.2 绑定点击函数

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

7.19.3 回调函数
//删除已有的SPU按钮的回调
const deleteSpu = async (row: SpuData) => {let result: any = await reqRemoveSpu(row.id as number)if (result.code == 200) {ElMessage({type: 'success',message: '删除成功',})//获取剩余SPU数据getHasSpu(records.value.length > 1 ? pageNo.value : pageNo.value - 1)} else {ElMessage({type: 'error',message: '删除失败',})}
}

7.20 spu业务完成

//路由组件销毁前,清空仓库关于分类的数据
onBeforeUnmount(() => {categoryStore.$reset()
})

8 SKU模块

8.1 SKU静态

<template><el-card><el-table border style="margin: 10px 0px"><el-table-column type="index" label="序号" width="80px"></el-table-column><el-table-columnlabel="名称"width="80px"show-overflow-tooltip></el-table-column><el-table-columnlabel="描述"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-columnlabel="操作"width="300px"fixed="right"></el-table-column></el-table><el-paginationv-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 {//获取已有的商品的数据-SKUSKU_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: numbermessage: stringok: boolean
}
//定义SKU对象的ts类型
export interface Attr {id?: numberattrId: number | string //平台属性的IDvalueId: number | string //属性值的ID
}
export interface saleArr {id?: numbersaleAttrId: number | string //属性IDsaleAttrValueId: number | string //属性值的ID
}
export interface SkuData {category3Id?: string | number //三级分类的IDspuId?: string | number //已有的SPU的IDtmId?: string | number //SPU品牌的IDskuName?: 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: numbersize: numbercurrent: numberorders: []optimizeCountSql: booleanhitCount: booleancountId: nullmaxLimit: nullsearchCount: booleanpages: 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 = pagerlet result: SkuResponseData = await reqSkuList(pageNo.value, pageSize.value)if (result.code == 200) {total.value = result.data.totalskuArr.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-columnprop="skuName"label="名称"width="80px"show-overflow-tooltip></el-table-column><el-table-columnprop="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-columnlabel="重量"width="300px"prop="weight"></el-table-column><el-table-columnlabel="价格"width="300px"prop="price"></el-table-column><el-table-column label="操作" width="300px" fixed="right"><el-button type="primary" size="small" icon="Top"></el-button><el-button type="primary" size="small" icon="Edit"></el-button><el-button type="primary" size="small" icon="InfoFilled"></el-button><el-button type="primary" size="small" icon="Delete"></el-button></el-table-column></el-table>
8.2.4 分页器

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

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

8.3 上架下架按钮

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

8.3.1 API&&TYPE
//上架
SALE_URL = '/admin/product/onSale/',
//下架的接口
CANCELSALE_URL = '/admin/product/cancelSale/',//已有商品上架的请求
export const reqSaleSku = (skuId: number) => {return request.get<any, any>(API.SALE_URL + skuId)
}
//下架的请求
export const reqCancelSale = (skuId: number) => {return request.get<any, any>(API.CANCELSALE_URL + skuId)
}

type都是any

8.3.2 按钮切换

根据数据切换
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

8.3.2 上架下架回调

流程:发请求->更新页面

//商品的上架与下架的操作
const updateSale = async (row: SkuData) => {//如果当前商品的isSale==1,说明当前商品是上架的额状态->更新为下架//否则else情况与上面情况相反if (row.isSale == 1) {//下架操作await reqCancelSale(row.id as number)//提示信息ElMessage({ type: 'success', message: '下架成功' })//发请求获取当前更新完毕的全部已有的SKUgetHasSku(pageNo.value)} else {//下架操作await reqSaleSku(row.id as number)//提示信息ElMessage({ type: 'success', message: '上架成功' })//发请求获取当前更新完毕的全部已有的SKUgetHasSku(pageNo.value)}
}

8.4 更新按钮

更新按钮这里没有业务。个人觉得是因为SKU的编写在SPU已经做完了。防止业务逻辑混乱
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

8.5 商品详情静态搭建

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

8.5.1 Drawer 抽屉

描述:呼出一个临时的侧边栏, 可以从多个方向呼出
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

通过基础的 24 分栏,迅速简便地创建布局。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
效果图:
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

8.5.3 轮播图 carousel

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
注意:把对应的style也复制过来
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

8.6 商品详情展示业务

8.6.1 API&&TYPE

API

//获取商品详情的接口SKUINFO_URL = '/admin/product/getSkuInfo/',
//获取商品详情的接口
export const reqSkuInfo = (skuId: number) => {return request.get<any, SkuInfoData>(API.SKUINFO_URL + skuId)
}

type

//获取SKU商品详情接口的ts类型
export interface SkuInfoData extends ResponseData {data: SkuData
}
8.6.2 发请求&&存储数据
let skuInfo = ref<any>({})
//查看商品详情按钮的回调
const findSku = async (row: SkuData) => {//抽屉展示出来drawer.value = true//获取已有商品详情数据let result: SkuInfoData = await reqSkuInfo(row.id as number)//存储已有的SKUskuInfo.value = result.data
}
8.6.3 展示数据(销售属性为例)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

8.7 删除模块

注:忘记写了,后面才想起来。简短写一下思路
API->绑定点击事件->发请求
比较简单。

8.8 小结

这模块的思路其实都比较简单。无外乎API(type),组件内发请求拿数据、将数据放到模板中。再加上一个对仓库的处理。
这部分真正的难点也是最值得学习的点在于
1:type的书写
2:对数据结构的理解(可以将请求回来的数据放到正确的位置上)
3:element-plus组件的使用。
其实现在看来这部分模块做的事情就是我们前端人的一些缩影。思路不难,难在琐碎的工作中要处理的各种各样的东西。

9 用户管理模块

9.1 静态搭建

主要是el-form、el-pagination

<template><el-card style="height: 80px"><el-form :inline="true" class="form"><el-form-item label="用户名:"><el-input placeholder="请你输入搜索用户名"></el-input></el-form-item><el-form-item><el-button type="primary" size="default">搜索</el-button><el-button type="primary" size="default" @click="reset">重置</el-button></el-form-item></el-form></el-card><el-card style="margin: 10px 0px"><el-button type="primary" size="default">添加用户</el-button><el-button type="primary" size="default">批量删除</el-button><!-- table展示用户信息 --><el-table style="margin: 10px 0px" border><el-table-column type="selection" align="center"></el-table-column><el-table-column label="#" align="center" type="index"></el-table-column><el-table-column label="ID" align="center"></el-table-column><el-table-columnlabel="用户名字"align="center"show-overflow-tooltip></el-table-column><el-table-columnlabel="用户名称"align="center"show-overflow-tooltip></el-table-column><el-table-columnlabel="用户角色"align="center"show-overflow-tooltip></el-table-column><el-table-columnlabel="创建时间"align="center"show-overflow-tooltip></el-table-column><el-table-columnlabel="更新时间"align="center"show-overflow-tooltip></el-table-column><el-table-columnlabel="操作"width="300px"align="center"></el-table-column></el-table><!-- 分页器 --><el-paginationv-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: numbermessage: stringok: boolean
}
//代表一个账号信息的ts类型
export interface User {id?: numbercreateTime?: stringupdateTime?: stringusername?: stringpassword?: stringname?: stringphone?: nullroleName?: string
}
//数组包含全部的用户信息
export type Records = User[]
//获取全部用户信息接口返回的数据ts类型
export interface UserResponseData extends ResponseData {data: {records: Recordstotal: numbersize: numbercurrent: numberpages: number}
}
9.2.2 发送请求(onMounted)
//用户总个数
let total = ref<number>(0)
//存储全部用户的数组
let userArr = ref<Records>([])
onMounted(() => {getHasUser()
})
//获取全部已有的用户信息
const getHasUser = async (pager = 1) => {//收集当前页码pageNo.value = pagerlet result: UserResponseData = await reqUserInfo(pageNo.value,pageSize.value,/* keyword.value, */)if (result.code == 200) {total.value = result.data.totaluserArr.value = result.data.records}
}
9.2.3 模板展示数据

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

9.2.4 分页器俩个函数回调

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

9.3 添加与修改用户静态

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

<!-- 抽屉结构:完成添加新的用户账号|更新已有的账号信息 -->
<el-drawer v-model="drawer"><!-- 头部标题:将来文字内容应该动态的 --><template #header><h4>添加用户</h4></template><!-- 身体部分 --><template #default><el-form><el-form-item label="用户姓名"><el-input placeholder="请您输入用户姓名"></el-input></el-form-item><el-form-item label="用户昵称"><el-input placeholder="请您输入用户昵称"></el-input></el-form-item><el-form-item label="用户密码"><el-input placeholder="请您输入用户密码"></el-input></el-form-item></el-form></template><template #footer><div style="flex: auto"><el-button>取消</el-button><el-button type="primary">确定</el-button></div></template>
</el-drawer>

注意绑定的是添加用户以及修改用户的回调

9.4 新账号添加业务

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

9.4.1 API&&TYPE

API:
添加和修改的请求封装成一个。

//添加一个新的用户账号
ADDUSER_URL = '/admin/acl/user/save',
//更新已有的用户账号
UPDATEUSER_URL = '/admin/acl/user/update',
//添加用户与更新已有用户的接口
export const reqAddOrUpdateUser = (data: User) => {//携带参数有ID更新if (data.id) {return request.put<any, any>(API.UPDATEUSER_URL, data)} else {return request.post<any, any>(API.ADDUSER_URL, data)}
}

type

//代表一个账号信息的ts类型
export interface User {id?: numbercreateTime?: stringupdateTime?: stringusername?: stringpassword?: stringname?: stringphone?: nullroleName?: string
}
9.4.2 组件收集数据
//收集用户信息的响应式数据
let userParams = reactive<User>({username: '',name: '',password: '',
})

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

9.4.3 发起请求
//保存按钮的回调
const save = async () => {//保存按钮:添加新的用户|更新已有的用户账号信息let result: any = await reqAddOrUpdateUser(userParams)//添加或者更新成功if (result.code == 200) {//关闭抽屉drawer.value = false//提示消息ElMessage({type: 'success',message: userParams.id ? '更新成功' : '添加成功',})//获取最新的全部账号的信息getHasUser(userParams.id ? pageNo.value : 1)} else {//关闭抽屉drawer.value = false//提示消息ElMessage({type: 'error',message: userParams.id ? '更新失败' : '添加失败',})}
}
9.4.4 添加用户按钮&&取消按钮

添加用户按钮:我们在点击添加用户按钮的时候,先把之前的用户数据清空

//添加用户按钮的回调
const addUser = () => {//抽屉显示出来drawer.value = true//清空数据Object.assign(userParams, {id: 0,username: '',name: '',password: '',})}

取消按钮:
点击取消按钮之后:关闭抽屉

//取消按钮的回调
const cancel = () => {//关闭抽屉drawer.value = false
}

9.5 表单校验功能

9.5.1 表单绑定校验信息

注意点:注意表单FORM与表格Table的区别。
主要还是收集与展示数据的区别。
表单绑定的:model="userParams"是数据,prop="username"是属性,绑定是为了对表单进行验证。
表格绑定的data是要显示的数据,item项的prop也是要展示的数据。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

9.5.2 校验规则
//校验用户名字回调函数
const validatorUsername = (rule: any, value: any, callBack: any) => {//用户名字|昵称,长度至少五位if (value.trim().length >= 5) {callBack()} else {callBack(new Error('用户名字至少五位'))}
}
//校验用户名字回调函数
const validatorName = (rule: any, value: any, callBack: any) => {//用户名字|昵称,长度至少五位if (value.trim().length >= 5) {callBack()} else {callBack(new Error('用户昵称至少五位'))}
}
const validatorPassword = (rule: any, value: any, callBack: any) => {//用户名字|昵称,长度至少五位if (value.trim().length >= 6) {callBack()} else {callBack(new Error('用户密码至少六位'))}
}
//表单校验的规则对象
const rules = {//用户名字username: [{ required: true, trigger: 'blur', validator: validatorUsername }],//用户昵称name: [{ required: true, trigger: 'blur', validator: validatorName }],//用户的密码password: [{ required: true, trigger: 'blur', validator: validatorPassword }],
}
9.5.3 确保校验通过再发请求

先获取form组件的实例,在调用form组件的方法validate()
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

//获取form组件实例
let formRef = ref<any>()
//保存按钮的回调
const save = async () => {//点击保存按钮的时候,务必需要保证表单全部复合条件在去发请求await formRef.value.validate()。。。。。。
}
9.5.4 再次校验前先清空上次的校验展示

使用nextTick是因为第一次的时候还没有formRef实例。

//添加用户按钮的回调
const addUser = () => {。。。。。。//清除上一次的错误的提示信息nextTick(() => {formRef.value.clearValidate('username')formRef.value.clearValidate('name')formRef.value.clearValidate('password')})
}

9.6 更新账号业务

9.6.1 抽屉结构变化分析

标题应该该为更新用户,没有输入密码。因为修改业务时我们需要用到用户id,因此再修改按钮存储账号信息赋值了用户的id。
我们根据这个id来决定我们的界面。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
初始化用户id:
我们再修改的时候将row的值复制给userParams,因此在展示抽屉的时候就会变换

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

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

补充:证明一下我们修改了账户之后服务器会阻止我们登录。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
此时修改一下路由守卫(做个标记)
image.png
刷新一下,证明路由确实是从这走的
image.png
此时在修改路由守卫以及用户信息方法
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
修改完之后再发请求:
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
此时可以得出结论,在修改用户信息之后,向服务器发起userInfo()请求确实会失败,导致我们跳转到login界面

9.7 分配角色静态搭建

          <el-form-item label="用户姓名"><el-input v-model="userParams.username" :disabled="true"></el-input></el-form-item><el-form-item label="职位列表"><el-checkbox>全选</el-checkbox><!-- 显示职位的的复选框 --><el-checkbox-group><el-checkboxv-for="(role, index) in 10":key="index":label="index">{{ index }}</el-checkbox></el-checkbox-group></el-form-item>

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

9.8 分配角色业务

9.8.1 API&&TYPE
//获取全部职位以及包含当前用户的已有的职位
export const reqAllRole = (userId: number) => {return request.get<any, AllRoleResponseData>(API.ALLROLEURL + userId)
}
//代表一个职位的ts类型
export interface RoleData {id?: numbercreateTime?: stringupdateTime?: stringroleName: stringremark: null
}
//全部职位的列表
export type AllRole = RoleData[]
//获取全部职位的接口返回的数据ts类型
export interface AllRoleResponseData extends ResponseData {data: {assignRoles: AllRoleallRolesList: 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-groupv-model="userRole"@change="handleCheckedCitiesChange"><el-checkboxv-for="(role, index) in allRole":key="index":label="role">{{ role.roleName }}</el-checkbox></el-checkbox-group></el-form-item></el-form></template></el-drawer>

详细解释:
全选部分:
@change:全选框点击时的回调
v-model:绑定的数据,根据这个值决定是否全选
:indeterminate:不确定状态,既没有全选也没有全不选
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
复选框部分:
v-for="(role, index) in allRole":遍历allRole。
:label="role":收集的数据(勾上的数据)
v-model="userRole":绑定收集的数据,也就是收集的数据存储到userRole中。
@change:勾选变化时的回调
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
全选框勾选的回调:
实现原理:函数会将勾选与否注入到val中,如果是,就将全部数据(allRole)赋值给选中的数据(userRole),选中的数据通过v-model实现页面的同步变化。

//顶部的全部复选框的change事件
const handleCheckAllChange = (val: boolean) => {//val:true(全选)|false(没有全选)userRole.value = val ? allRole.value : []//不确定的样式(确定样式)isIndeterminate.value = false
}

复选框

//顶部全部的复选框的change事件
const handleCheckedCitiesChange = (value: string[]) => {//顶部复选框的勾选数据//代表:勾选上的项目个数与全部的职位个数相等,顶部的复选框勾选上checkAll.value = value.length === allRole.value.length//不确定的样式isIndeterminate.value = value.length !== allRole.value.length
}
9.8.4 分配角色业务(给服务器发请求)
  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. 绑定点击函数

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. table收集选中的数据

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

收集数据:
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
发送请求
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

//搜索按钮的回调
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 = falsenextTick(() => {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-columnlabel="职位名称"align="center"prop="roleName"show-overflow-tooltip></el-table-column><el-table-columnlabel="创建时间"align="center"show-overflow-tooltipprop="createTime"></el-table-column><el-table-columnlabel="更新时间"align="center"show-overflow-tooltipprop="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-paginationv-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: numbermessage: stringok: boolean
}
//职位数据类型
export interface RoleData {id?: numbercreateTime?: stringupdateTime?: stringroleName: stringremark?: null
}//全部职位的数组的ts类型
export type Records = RoleData[]
//全部职位数据的相应的ts类型
export interface RoleResponseData extends ResponseData {data: {records: Recordstotal: numbersize: numbercurrent: numberorders: []optimizeCountSql: booleanhitCount: booleancountId: nullmaxLimit: nullsearchCount: booleanpages: 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 = pagerlet result: RoleResponseData = await reqAllRoleList(pageNo.value,pageSize.value,keyword.value,)if (result.code == 200) {total.value = result.data.totalallRole.value = result.data.records}
}
10.2.3 表格数据
<el-table border style="margin: 10px 0px" :data="allRole"><el-table-columntype="index"align="center"label="#"></el-table-column><el-table-column label="ID" align="center" prop="id"></el-table-column><el-table-columnlabel="职位名称"align="center"prop="roleName"show-overflow-tooltip></el-table-column><el-table-columnlabel="创建时间"align="center"show-overflow-tooltipprop="createTime"></el-table-column><el-table-columnlabel="更新时间"align="center"show-overflow-tooltipprop="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-paginationv-model:current-page="pageNo"v-model:page-size="pageSize":page-sizes="[10, 20, 30, 40]":background="true"layout="prev, pager, next, jumper,->,sizes,total":total="total"@current-change="getHasRole"@size-change="sizeChange"/>
//下拉菜单的回调
const sizeChange = () => {getHasRole()
}
10.2.5 搜索按钮

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

重置模块我在用户管理模块仔细解释过。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

import useLayOutSettingStore from '@/store/modules/setting'
let settingStore = useLayOutSettingStore()
//重置按钮的回调
const reset = () => {settingStore.refresh = !settingStore.refresh
}

10.3 添加&&修改职位

10.3.1 静态
<!-- 添加职位与更新已有职位的结构:对话框 --><el-dialog v-model="dialogVisite" title="添加职位"><el-form><el-form-item label="职位名称"><el-input placeholder="请你输入职位名称"></el-input></el-form-item></el-form><template #footer><el-button type="primary" size="default" @click="dialogVisite = false">取消</el-button><el-button type="primary" size="default">确定</el-button></template></el-dialog>
10.3.2 API&&TYPE
//新增岗位的接口地址ADDROLE_URL = '/admin/acl/role/save',//更新已有的职位UPDATEROLE_URL = '/admin/acl/role/update',
//添加职位与更新已有职位接口
export const reqAddOrUpdateRole = (data: RoleData) => {if (data.id) {return request.put<any, any>(API.UPDATEROLE_URL, data)} else {return request.post<any, any>(API.ADDROLE_URL, data)}
}
10.3.3 添加&&修改按钮绑定点击函数

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

10.3.4 添加&&修改按钮回调
//添加职位按钮的回调
const addRole = () => {//对话框显示出来dialogVisite.value = true//清空数据Object.assign(RoleParams, {roleName: '',id: 0,})//清空上一次表单校验错误结果nextTick(() => {form.value.clearValidate('roleName')})
}
//更新已有的职位按钮的回调
const updateRole = (row: RoleData) => {//显示出对话框dialogVisite.value = true//存储已有的职位----带有ID的Object.assign(RoleParams, row)//清空上一次表单校验错误结果nextTick(() => {form.value.clearValidate('roleName')})
}
10.3.5 表单校验

:model:要校验的数据
:rules:校验的规则
ref:获取表单实例,方便后面调用validate函数来确保校验通过才放行
prop:绑定数据的属性
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

//自定义校验规则的回调
const validatorRoleName = (rule: any, value: any, callBack: any) => {if (value.trim().length >= 2) {callBack()} else {callBack(new Error('职位名称至少两位'))}
}
//职位校验规则
const rules = {roleName: [{ required: true, trigger: 'blur', validator: validatorRoleName }],
}
10.3.6 保存按钮的回调
//确定按钮的回调
const save = async () => {//表单校验结果,结果通过在发请求、结果没有通过不应该在发生请求await form.value.validate()//添加职位|更新职位的请求let result: any = await reqAddOrUpdateRole(RoleParams)if (result.code == 200) {//提示文字ElMessage({type: 'success',message: RoleParams.id ? '更新成功' : '添加成功',})//对话框显示dialogVisite.value = false//再次获取全部的已有的职位getHasRole(RoleParams.id ? pageNo.value : 1)}
}

10.4 分配角色权限业务

10.4.1 API&&type(获取全部菜单)
//获取全部的菜单与按钮的数据ALLPERMISSTION = '/admin/acl/permission/toAssign/',//获取全部菜单与按钮权限数据
export const reqAllMenuList = (roleId: number) => {return request.get<any, MenuResponseData>(API.ALLPERMISSTION + roleId)
}

注意:type这里MenuData与MenuList互相调用,适合这种树状的数据结构

//菜单与按钮数据的ts类型
export interface MenuData {id: numbercreateTime: stringupdateTime: stringpid: numbername: stringcode: stringtoCode: stringtype: numberstatus: nulllevel: numberchildren?: MenuListselect: boolean
}
export type MenuList = MenuData[]
10.4.2 获取数据

分配权限按钮:
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
获取&&存储数据

//准备一个数组:数组用于存储勾选的节点的ID(四级的)
let selectArr = ref<number[]>([])
//已有的职位的数据
const setPermisstion = async (row: RoleData) => {//抽屉显示出来drawer.value = true//收集当前要分类权限的职位的数据Object.assign(RoleParams, row)//根据职位获取权限的数据let result: MenuResponseData = await reqAllMenuList(RoleParams.id as number)if (result.code == 200) {menuArr.value = result.data// selectArr.value = filterSelectArr(menuArr.value, [])}
}
10.4.3 展示数据

我们重点关注el-tree组件
data:展示的数据
show-checkbox:节点是否可被选择
node-key:每个树节点用来作为唯一标识的属性,整棵树应该是唯一的
default-expand-all:默认展开所有节点
default-checked-keys:默认勾选的节点的 key 的数组
props:属性: label:指定节点标签为节点对象的某个属性值 children:指定子树为节点对象的某个属性值

const defaultProps = {//子树为节点对象的childrenchildren: 'children',//节点标签为节点对象的name属性label: 'name',
}

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

10.4.4 展示数据(已分配的权限)

获取已分配权限的id,这里我们只需要收集最后一层的id即可,因为组件会自动更具最后一层的选择情况决定上层的选择状况。
注意:获取最后一层id的函数filterSelectArr使用了递归。

//分配权限按钮的回调
//已有的职位的数据
const setPermisstion = async (row: RoleData) => {//抽屉显示出来drawer.value = true//收集当前要分类权限的职位的数据Object.assign(RoleParams, row)//根据职位获取权限的数据let result: MenuResponseData = await reqAllMenuList(RoleParams.id as number)if (result.code == 200) {menuArr.value = result.dataselectArr.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 () => {//职位的IDconst roleId = RoleParams.id as number//选中节点的IDlet arr = tree.value.getCheckedKeys()//半选的IDlet 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: numbermessage: stringok: boolean
}
//菜单数据与按钮数据的ts类型
export interface Permisstion {id?: numbercreateTime: stringupdateTime: stringpid: numbername: stringcode: nulltoCode: nulltype: numberstatus: nulllevel: numberchildren?: PermisstionListselect: 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-buttontype="primary"size="small":disabled="row.level == 4 ? true : false">{{ row.level == 3 ? '添加功能' : '添加菜单' }}</el-button><el-buttontype="primary"size="small":disabled="row.level == 1 ? true : false">编辑</el-button><el-buttontype="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-dialogv-model="dialogVisible"><!-- 表单组件:收集新增与已有的菜单的数据 --><el-form><el-form-item label="名称"><el-inputplaceholder="请你输入菜单名称"></el-input></el-form-item><el-form-item label="权限"><el-inputplaceholder="请你输入权限数值"></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-buttontype="primary"size="small":disabled="row.level == 1 ? true : false">删除</el-button></template></el-popconfirm>
11.3.3 删除的回调
//删除按钮回调
const removeMenu = async (id: number) => {let result = await reqRemoveMenu(id)if (result.code == 200) {ElMessage({ type: 'success', message: '删除成功' })getHasPermisstion()}
}

12 首页模块

首页模块比较简单,代码量也少。这里直接放上源代码

<template><div><el-card><div class="box"><img :src="userStore.avatar" alt="" class="avatar" /><div class="bottom"><h3 class="title">{{ getTime() }}好呀{{ userStore.username }}</h3><p class="subtitle">硅谷甄选运营平台</p></div></div></el-card><div class="bottoms"><svg-icon name="welcome" width="800px" height="400px"></svg-icon></div></div>
</template><script setup lang="ts">
import { getTime } from '@/utils/time'
//引入用户相关的仓库,获取当前用户的头像、昵称
import useUserStore from '@/store/modules/user'
//获取存储用户信息的仓库对象
let userStore = useUserStore()
</script><style lang="scss" scoped>
.box {display: flex;.avatar {width: 100px;height: 100px;border-radius: 50%;}.bottom {margin-left: 20px;.title {font-size: 30px;font-weight: 900;margin-bottom: 30px;}.subtitle {font-style: italic;color: skyblue;}}
}
.bottoms {margin-top: 10px;display: flex;justify-content: center;
}
</style>

13 setting按钮模块

13.1 暗黑模式设置

13.1.1 暗黑模式静态

这里使用了el-switch组件,下面介绍一下属性
@change:点击切换时的回调
v-model:双向绑定的数据,用来控制开关的切换
class:默认的类
style:样式
active-ico、inactive-icon:开和关的图标
inline-prompt:可以把图标放在开关里面
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

13.1.2 暗黑模式

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

//暗黑模式需要的样式
import 'element-plus/theme-chalk/dark/css-vars.css'
13.1.3 切换的回调
//收集开关的数据
let dark = ref<boolean>(false)
//switch开关的chang事件进行暗黑模式的切换
const changeDark = () => {//获取HTML根节点let html = document.documentElement//判断HTML标签是否有类名darkdark.value ? (html.className = 'dark') : (html.className = '')
}

13.2 主题颜色切换

Element Plus 默认提供一套主题,也提供了相应的修改主题颜色的方法。我们要使用的时通过js来修改主题颜色

13.2.1 静态搭建

使用了el-color-picker组件
@change:切换的回调
v-model:绑定的数据
show-alpha:是否支持透明度选择
predefine:预定义颜色(会在下面显示)
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

13.2.2 点击切换回调
//主题颜色的设置
const setColor = () => {//通知js修改根节点的样式对象的属性与属性值const html = document.documentElementhtml.style.setProperty('--el-color-primary', color.value)
}
13.2.3 预定义颜色展示

predefine:预定义颜色

const predefineColors = ref(['#ff4500','#ff8c00','#ffd700','#90ee90','#00ced1','#1e90ff','#c71585','rgba(255, 69, 0, 0.68)','rgb(255, 120, 0)','hsv(51, 100, 98)','hsva(120, 40, 94, 0.5)','hsl(181, 100%, 37%)','hsla(209, 100%, 56%, 0.73)','#c7158577',
])

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

14 数据大屏

14.1 数据大屏初始静态

14.1.1初始静态
<div class="container"><!-- 数据大屏展示内容区域 --><div class="screen" ref="screen"><div class="top"><Top /></div><div class="bottom"><div class="left">左侧</div><div class="center">中间</div><div class="right">右侧</div></div></div></div>
<style lang="scss" scoped>
.container {width: 100vw;height: 100vh;background: url(./images/bg.png) no-repeat;background-size: cover;.screen {position: fixed;width: 1920px;height: 1080px;left: 50%;top: 50%;transform-origin: left top;.top {width: 100%;height: 40px;}.bottom {display: flex;.right {flex: 1;display: flex;flex-direction: column;margin-left: 40px;}.left {flex: 1;height: 1040px;display: flex;flex-direction: column;}.center {flex: 1.5;display: flex;flex-direction: column;}}}
}
</style>
14.1.2 大屏适配的解决方案
<script setup lang="ts">
import { ref, onMounted } from 'vue'
//获取数据大屏展示内容盒子的DOM元素
//引入顶部的子组件
import Top from './components/top/index.vue'
let screen = ref()
onMounted(() => {screen.value.style.transform = `scale(${getScale()}) translate(-50%,-50%)`
})
//定义大屏缩放比例
function getScale(w = 1920, h = 1080) {const ww = window.innerWidth / wconst wh = window.innerHeight / hreturn ww < wh ? ww : wh
}
//监听视口变化
window.onresize = () => {screen.value.style.transform = `scale(${getScale()}) translate(-50%,-50%)`
}
</script>

14.2 顶部静态

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

14.2.1 顶部静态
<template><div class="top"><div class="left"><span class="lbtn" @click="goHome">首页</span></div><div class="center"><div class="title">智慧旅游可视化大数据平台</div></div><div class="right"><span class="rbtn">统计报告</span><span class="time">当前时间:{{ time }}</span></div></div>
</template>
<style lang="scss" scoped>
.top {width: 100%;height: 40px;display: flex;.left {flex: 1.5;background: url(../../images/dataScreen-header-left-bg.png) no-repeat;background-size: cover;.lbtn {width: 150px;height: 40px;float: right;background: url(../../images/dataScreen-header-btn-bg-l.png) no-repeat;background-size: 100% 100%;text-align: center;line-height: 40px;color: #29fcff;font-size: 20px;}}.center {flex: 2;.title {width: 100%;height: 74px;background: url(../../images/dataScreen-header-center-bg.png) no-repeat;background-size: 100% 100%;text-align: center;line-height: 74px;color: #29fcff;font-size: 30px;}}.right {flex: 1.5;background: url(../../images/dataScreen-header-left-bg.png) no-repeat;background-size: cover;display: flex;justify-content: space-between;align-items: center;.rbtn {width: 150px;height: 40px;background: url(../../images/dataScreen-header-btn-bg-r.png) no-repeat;background-size: 100% 100%;text-align: center;line-height: 40px;color: #29fcff;}.time {color: #29fcff;font-size: 20px;}}
}
</style>
14.2.2 当前时间
  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. 模板使用

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

14.2.3 顶部按钮

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

14.3 左侧的上面部分

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

14.3.1 左侧部分划分

父组件中对左侧使用了垂直方向的弹性盒

.bottom {display: flex;.left {flex: 1;height: 1040px;display: flex;// 弹性方向:列方向flex-direction: column;.tourist {flex: 1.2;}.sex {flex: 1;}.age {flex: 1;}}
}
14.3.2 左侧上面部分的静态

注意:在“可预约总量99999人”那里使用了float: right;,float对上面的块级元素不会产生影响,因此不会飘上去。

<template><div class="box"><div class="top"><p class="title">实时游客统计</p><p class="bg"></p><p class="right">可预约总量<span>99999</span>人</p></div><div class="number"><span v-for="(item, index) in people" :key="index">{{ item }}</span></div><!-- 盒子将来echarts展示图形图标的节点 --><div class="charts" ref="charts">123</div></div>
</template>
<style lang="scss" scoped>
.box {background: url(../../images/dataScreen-main-lb.png) no-repeat;background-size: 100% 100%;margin-top: 10px;.top {margin-left: 20px;.title {color: white;font-size: 20px;}.bg {width: 68px;height: 7px;background: url(../../images/dataScreen-title.png) no-repeat;background-size: 100% 100%;margin-top: 10px;}.right {float: right;color: white;font-size: 20px;span {color: yellowgreen;}}}.number {padding: 10px;margin-top: 30px;display: flex;span {flex: 1;height: 40px;text-align: center;line-height: 40px;background: url(../../images/total.png) no-repeat;background-size: 100% 100%;color: #29fcff;}}.charts {width: 100%;height: 270px;}
}
</style>
14.3.3 水球图
  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 上面的样式部分

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

<template><div class="box1"><div class="title"><p>男女比例</p><img src="../../images/dataScreen-title.png" alt="" /></div><div class="sex"><div class="man"><img src="../../images/man.png" alt="" /></div><div class="women"><img src="../../images/woman.png" alt="" /></div></div><div class="rate"><p>男士58%</p><p>女士42%</p></div><div class="charts" ref="charts"></div></div>
</template>
<style scoped lang="scss">
.box1 {width: 100%;height: 100%;background: url(../../images/dataScreen-main-cb.png) no-repeat;background-size: 100% 100%;margin: 20px 0px;.title {margin-left: 20px;p {color: white;font-size: 20px;}}.sex {display: flex;justify-content: center;.man {margin: 20px;width: 111px;height: 115px;background: url(../../images/man-bg.png) no-repeat;display: flex;justify-content: center;align-items: center;}.women {margin: 20px;width: 111px;height: 115px;background: url(../../images/woman-bg.png) no-repeat;display: flex;justify-content: center;align-items: center;}}.rate {display: flex;justify-content: center;color: white;p {margin: 0 40px;margin-top: 10px;margin-bottom: -10px;}}.charts {height: 100px;}
}
</style>
14.4.2 柱状图部分
import { ref, onMounted } from 'vue'
import * as echarts from 'echarts'
//获取图形图标的DOM节点
let charts = ref()
onMounted(() => {//初始化echarts实例let mycharts = echarts.init(charts.value)//设置配置项mycharts.setOption({//组件标题title: {//   text: '男女比例', //主标题textStyle: {//主标题颜色color: 'skyblue',},left: '40%',},//x|yxAxis: {show: false,min: 0,max: 100,},yAxis: {show: false,type: 'category',},series: [// 这里有俩个柱状图,下面的覆盖上面的{type: 'bar',data: [58],barWidth: 20,// 柱状图的层级z: 100,// 柱状图样式itemStyle: {color: 'skyblue',borderRadius: 20,},},{type: 'bar',data: [100],//柱状图宽度barWidth: 20,//调整女士柱条位置barGap: '-100%',itemStyle: {color: 'pink',borderRadius: 20,},},],grid: {left: 60,top: -20,right: 60,bottom: 0,},})
})

14.5 左侧的下面部分

<template><div class="box2"><div class="title"><p>年龄比例</p><img src="../../images/dataScreen-title.png" alt="" /></div><!-- 图形图标的容器 --><div class="charts" ref="charts"></div></div>
</template><script setup lang="ts">
import { ref, onMounted } from 'vue'
//引入echarts
import * as echarts from 'echarts'
let charts = ref()
//组件挂载完毕初始化图形图标
onMounted(() => {let mychart = echarts.init(charts.value)//设置配置项let option = {tooltip: {trigger: 'item',},legend: {right: 30,top: 40,orient: 'vertical', //图例组件方向的设置textStyle: {color: 'white',fontSize: 14,},},series: [{name: 'Access From',type: 'pie',radius: ['40%', '70%'],avoidLabelOverlap: false,itemStyle: {borderRadius: 10,borderColor: '#fff',borderWidth: 2,},label: {show: true,position: 'inside',color: 'white',},labelLine: {show: false,},data: [{ value: 1048, name: '军事' },{ value: 735, name: '新闻' },{ value: 580, name: '直播' },{ value: 484, name: '娱乐' },{ value: 300, name: '财经' },],},],//调整图形图标的位置grid: {left: 0,top: 0,right: 0,bottom: 0,},}mychart.setOption(option)
})
</script><style scoped lang="scss">
.box2 {width: 100%;height: 100%;background: url(../../images/dataScreen-main-cb.png) no-repeat;background-size: 100% 100%;.title {margin-left: 20px;p {color: white;font-size: 20px;}}.charts {height: 260px;}
}
</style>

14.6 中间的上面部分

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

<template><div class="box4" ref="map">我是地图组件</div>
</template><script setup lang="ts">
import { ref, onMounted } from 'vue'
import * as echarts from 'echarts'
//获取DOM元素
let map = ref()
//引入中国地图的JSON数据
import chinaJSON from './china.json'
//注册中国地图
echarts.registerMap('china', chinaJSON as any)
onMounted(() => {let mychart = echarts.init(map.value)//设置配置项mychart.setOption({//地图组件geo: {map: 'china', //中国地图roam: true, //鼠标缩放的效果//地图的位置调试left: 100,top: 150,right: 100,zoom: 1.2,bottom: 0,//地图上的文字的设置label: {show: true, //文字显示出来color: 'white',fontSize: 10,},itemStyle: {//每一个多边形的样式color: {type: 'linear',x: 0,y: 0,x2: 0,y2: 1,colorStops: [{offset: 0,color: 'pink', // 0% 处的颜色},{offset: 1,color: 'hotpink', // 100% 处的颜色},],global: false, // 缺省为 false},opacity: 0.8,},//地图高亮的效果emphasis: {itemStyle: {color: 'red',},label: {fontSize: 20,},},},//布局位置grid: {left: 0,top: 0,right: 0,bottom: 0,},series: [{type: 'lines', //航线的系列data: [{coords: [[87.617733, 43.792818], // 起点[91.132212, 29.660361], // 终点],// 统一的样式设置lineStyle: {color: 'red',width: 5,},},{coords: [[91.132212, 29.660361], // 起点[100.132212, 25.660361], // 终点],// 统一的样式设置lineStyle: {color: 'red',width: 5,},},{coords: [[100.132212, 25.660361], // 起点[109.132212, 18.660361], // 终点],// 统一的样式设置lineStyle: {color: 'red',width: 5,},},{coords: [[109.132212, 18.660361], // 起点[117.132212, 25.660361], // 终点],// 统一的样式设置lineStyle: {color: 'red',width: 5,},},{coords: [[117.132212, 25.660361], // 起点[125.132212, 44.060361], // 终点],// 统一的样式设置lineStyle: {color: 'red',width: 5,},},{coords: [[125.132212, 44.060361], // 起点[116.405285, 39.904989], // 终点],// 统一的样式设置lineStyle: {color: 'red',width: 5,},},{coords: [[116.405285, 39.904989], // 起点[112.304436, 37.618179], // 终点],// 统一的样式设置lineStyle: {color: 'red',width: 5,},},{coords: [[112.304436, 37.618179], // 起点[106.504962, 29.533155], // 终点],// 统一的样式设置lineStyle: {color: 'red',width: 5,},},{coords: [[106.504962, 29.533155], // 起点[104.065735, 30.659462], // 终点],// 统一的样式设置lineStyle: {color: 'red',width: 5,},},{coords: [[106.504962, 29.533155], // 起点[104.065735, 30.659462], // 终点],// 统一的样式设置lineStyle: {color: 'red',width: 5,},},{coords: [[104.065735, 30.659462], // 起点[101.778916, 36.623178], // 终点],// 统一的样式设置lineStyle: {color: 'red',width: 5,},},{coords: [[101.778916, 36.623178],[87.617733, 43.792818],],// 统一的样式设置lineStyle: {color: 'red',width: 5,},},],//开启动画特效effect: {show: true,symbol: 'arrow',color: 'yellow',symbolSize: 10,},},],})
})
</script><style lang="scss" scoped></style>

14.7 中间的下面部分

<template><div class="box5"><div class="title"><p>未来七天游客数量趋势图</p><img src="../../images/dataScreen-title.png" alt="" /></div><div class="charts" ref="line"></div></div>
</template><script setup lang="ts">
import * as echarts from 'echarts'
import { ref, onMounted } from 'vue'
//获取图形图标的节点
let line = ref()
onMounted(() => {let mycharts = echarts.init(line.value)//设置配置项mycharts.setOption({//标题组件title: {text: '访问量',},//x|y轴xAxis: {type: 'category',//两侧不留白boundaryGap: false,//分割线不要splitLine: {show: false,},data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],//轴线的设置axisLine: {show: true,},//刻度axisTick: {show: true,},},yAxis: {splitLine: {show: false,},//轴线的设置axisLine: {show: true,},//刻度axisTick: {show: true,},},grid: {left: 40,top: 0,right: 20,bottom: 20,},//系列series: [{type: 'line',data: [120, 1240, 66, 2299, 321, 890, 1200],//平滑曲线的设置smooth: true,//区域填充样式areaStyle: {color: {type: 'linear',x: 0,y: 0,x2: 0,y2: 1,colorStops: [{offset: 0,color: 'red', // 0% 处的颜色},{offset: 1,color: 'blue', // 100% 处的颜色},],global: false, // 缺省为 false},},},],})
})
</script><style scoped lang="scss">
.box5 {width: 100%;height: 100%;background: url(../../images/dataScreen-main-cb.png) no-repeat;background-size: 100% 100%;margin: 0px 20px;.title {margin-left: 10px;p {color: white;font-size: 20px;}}.charts {height: calc(100% - 40px);}
}
</style>

14.8 右侧的上面部分

<template><div class="box6"><div class="title"><p>热门景区排行</p><img src="../../images/dataScreen-title.png" alt="" /></div><!-- 图形图标的容器 --><div class="charts" ref="charts"></div></div>
</template><script setup lang="ts">
import * as echarts from 'echarts'
import { ref, onMounted } from 'vue'
//获取DOM节点
let charts = ref()
//组件挂载完毕
onMounted(() => {//一个容器可以同时展示多种类型的图形图标let mychart = echarts.init(charts.value)//设置配置项mychart.setOption({//标题组件title: {//主标题text: '景区排行',link: 'http://www.baidu.com',//标题的位置left: '50%',//主标题文字样式textStyle: {color: 'yellowgreen',fontSize: 20,},//子标题subtext: '各大景区排行',//子标题的样式subtextStyle: {color: 'yellowgreen',fontSize: 16,},},//x|y轴组件xAxis: {type: 'category', //图形图标在x轴均匀分布展示},yAxis: {},//布局组件grid: {left: 20,bottom: 20,right: 20,},//系列:决定显示图形图标是哪一种的series: [{type: 'bar',data: [10, 20, 30, 40, 50, 60, 70],//柱状图的:图形上的文本标签,label: {show: true,//文字的位置position: 'insideTop',//文字颜色color: 'yellowgreen',},//是否显示背景颜色showBackground: true,backgroundStyle: {//底部背景的颜色color: {type: 'linear',x: 0,y: 0,x2: 0,y2: 1,colorStops: [{offset: 0,color: 'black', // 0% 处的颜色},{offset: 1,color: 'blue', // 100% 处的颜色},],global: false, // 缺省为 false},},//柱条的样式itemStyle: {borderRadius: [10, 10, 0, 0],//柱条颜色color: function (data: any) {//给每一个柱条这是背景颜色let arr = ['red','orange','yellowgreen','green','purple','hotpink','skyblue',]return arr[data.dataIndex]},},},{type: 'line',data: [10, 20, 30, 40, 50, 60, 90],smooth: true, //平滑曲线},],tooltip: {backgroundColor: 'rgba(50,50,50,0.7)',},})
})
</script><style scoped lang="scss">
.box6 {width: 100%;height: 100%;background: url(../../images/dataScreen-main-cb.png) no-repeat;background-size: 100% 100%;margin: 20px 0px;.title {margin-left: 5px;p {color: white;font-size: 20px;}}.charts {height: calc(100% - 30px);}
}
</style>

15 菜单权限

15.1 路由的拆分

15.1.1 路由分析
菜单的权限:
超级管理员账号:admin atguigu123   拥有全部的菜单、按钮的权限
飞行员账号  硅谷333  111111       不包含权限管理模块、按钮的权限并非全部按钮
同一个项目:不同人(职位是不一样的,他能访问到的菜单、按钮的权限是不一样的)一、目前整个项目一共多少个路由!!!
login(登录页面)、
404(404一级路由)、
任意路由、
首页(/home)、
数据大屏、
权限管理(三个子路由)
商品管理模块(四个子路由)1.1开发菜单权限
---第一步:拆分路由
静态(常量)路由:大家都可以拥有的路由
login、首页、数据大屏、404异步路由:不同的身份有的有这个路由、有的没有
权限管理(三个子路由)
商品管理模块(四个子路由)任意路由:任意路由1.2菜单权限开发思路
目前咱们的项目:任意用户访问大家能看见的、能操作的菜单与按钮都是一样的(大家注册的路由都是一样的)
15.1.2 路由的拆分
//对外暴露配置路由(常量路由)
export const constantRoute = [{//登录路由path: '/login',component: () => import('@/views/login/index.vue'),name: 'login', //命名路由meta: {title: '登录', //菜单标题hidden: true, //路由的标题在菜单中是否隐藏},},{//登录成功以后展示数据的路由path: '/',component: () => import('@/layout/index.vue'),name: 'layout',meta: {hidden: false,},redirect: '/home',children: [{path: '/home',component: () => import('@/views/home/index.vue'),meta: {title: '首页',hidden: false,icon: 'HomeFilled',},},],},{path: '/404',component: () => import('@/views/404/index.vue'),name: '404',meta: {title: '404',hidden: true,},},{path: '/screen',component: () => import('@/views/screen/index.vue'),name: 'Screen',meta: {hidden: false,title: '数据大屏',icon: 'Platform',},},
]//异步路由
export const asnycRoute = [{path: '/acl',component: () => import('@/layout/index.vue'),name: 'Acl',meta: {hidden: false,title: '权限管理',icon: 'Lock',},redirect: '/acl/user',children: [{path: '/acl/user',component: () => import('@/views/acl/user/index.vue'),name: 'User',meta: {hidden: false,title: '用户管理',icon: 'User',},},{path: '/acl/role',component: () => import('@/views/acl/role/index.vue'),name: 'Role',meta: {hidden: false,title: '角色管理',icon: 'UserFilled',},},{path: '/acl/permission',component: () => import('@/views/acl/permission/index.vue'),name: 'Permission',meta: {hidden: false,title: '菜单管理',icon: 'Monitor',},},],},{path: '/product',component: () => import('@/layout/index.vue'),name: 'Product',meta: {hidden: false,title: '商品管理',icon: 'Goods',},redirect: '/product/trademark',children: [{path: '/product/trademark',component: () => import('@/views/product/trademark/index.vue'),name: 'Trademark',meta: {hidden: false,title: '品牌管理',icon: 'ShoppingCartFull',},},{path: '/product/attr',component: () => import('@/views/product/attr/index.vue'),name: 'Attr',meta: {hidden: false,title: '属性管理',icon: 'CollectionTag',},},{path: '/product/spu',component: () => import('@/views/product/spu/index.vue'),name: 'Spu',meta: {hidden: false,title: 'SPU管理',icon: 'Calendar',},},{path: '/product/sku',component: () => import('@/views/product/sku/index.vue'),name: 'Sku',meta: {hidden: false,title: 'SKU管理',icon: 'Orange',},},],},
]//任意路由
//任意路由
export const anyRoute = {//任意路由path: '/:pathMatch(.*)*',redirect: '/404',name: 'Any',meta: {title: '任意路由',hidden: true,icon: 'DataLine',},
}

15.2 菜单权限的实现

15.2.1 获取正确路由的方法

注意:这里使用了递归。其次,这里是浅拷贝,会改变原有的路由。因此还需要改进。

//硅谷333: routes['Product','Trademark','Sku']
let guigu333 = ['Product', 'Trademark', 'Sku'];
function filterAsyncRoute(asnycRoute, routes) {return asnycRoute.filter(item => {if (routes.includes(item.name)) {if (item.children && item.children.length > 0) {item.children = filterAsyncRoute(item.children, routes)}return true}})
}
//硅谷333需要展示的异步路由
let guigu333Result = filterAsyncRoute(asnycRoute, guigu333);
console.log([...constRoute, ...guigu333Result, anyRoute], '硅谷333');
15.2.2 获取路由
。。。。。。import router from '@/router'
//引入路由(常量路由)
import { constantRoute, asnycRoute, anyRoute } from '@/router/routes'
//用于过滤当前用户需要展示的异步路由
function filterAsyncRoute(asnycRoute: any, routes: any) {return asnycRoute.filter((item: any) => {if (routes.includes(item.name)) {if (item.children && item.children.length > 0) {//硅谷333账号:product\trademark\attr\skuitem.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.namethis.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.namethis.avatar = result.data.avatar//计算当前用户需要展示的异步路由const userAsyncRoute = filterAsyncRoute(cloneDeep(asnycRoute),result.data.routes,)//菜单需要的数据整理完毕this.menuRoutes = [...constantRoute, ...userAsyncRoute, anyRoute]//目前路由器管理的只有常量路由:用户计算完毕异步路由、任意路由动态追加;[...userAsyncRoute, anyRoute].forEach((route: any) => {router.addRoute(route)})return 'ok'} else {return Promise.reject(new Error(result.message))}},
15.3.2 路由加载问题

这样配置路由后,如果你访问的是异步路由,会在刷新的时候出现空白页面。原因是异步路由是异步获取的,加载的时候还没有。因此我们可以在路由守卫文件中改写。这个的意思就是一直加载。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

//用户登录判断if (token) {//登陆成功,访问login。指向首页if (to.path == '/login') {next('/')} else {//登陆成功访问其余的,放行//有用户信息if (username) {//放行next()} else {//如果没有用户信息,在收尾这里发请求获取到了用户信息再放行try {//获取用户信息await userStore.userInfo()//万一刷新的时候是异步路由,有可能获取到用户信息但是异步路由没有加载完毕,出现空白效果next({ ...to })} catch (error) {//token过期|用户手动处理token//退出登陆->用户相关的数据清空await userStore.userLogout()next({ path: '/login', query: { redirect: to.path } })}}}} else {//用户未登录if (to.path == '/login') {next()} else {next({ path: '/login', query: { redirect: to.path } })}}

16 按钮权限

对于不同的用户,按钮的的显示与否

16.1 获取用户应有的按钮

记得修改对应的type

//创建用户相关的小仓库
import { defineStore } from 'pinia'
//引入接口
import { reqLogin, reqUserInfo, reqLogout } from '@/api/user'
import type {loginFormData,loginResponseData,userInfoResponseData,
} from '@/api/user/type'
import type { UserState } from './types/type'import router from '@/router'
。。。。。。
//创建用户小仓库
const useUserStore = defineStore('User', {//小仓库存储数据地方state: (): UserState => {return {token: GET_TOKEN(), //用户唯一标识tokenmenuRoutes: constantRoute, //仓库存储生成菜单需要数组(路由)username: '',avatar: '',//存储当前用户是否包含某一个按钮buttons: [],}},//处理异步|逻辑地方actions: {。。。。。。//获取用户信息方法async userInfo() {//获取用户信息进行存储const result: userInfoResponseData = await reqUserInfo()if (result.code == 200) {this.username = result.data.namethis.avatar = result.data.avatarthis.buttons = result.data.buttonsconsole.log(result)//计算当前用户需要展示的异步路由const userAsyncRoute = filterAsyncRoute(cloneDeep(asnycRoute),result.data.routes,)//菜单需要的数据整理完毕this.menuRoutes = [...constantRoute, ...userAsyncRoute, anyRoute]//目前路由器管理的只有常量路由:用户计算完毕异步路由、任意路由动态追加;[...userAsyncRoute, anyRoute].forEach((route: any) => {router.addRoute(route)})return 'ok'} else {return Promise.reject(new Error(result.message))}},。。。。。。
})
//对外暴露小仓库
export default useUserStore

16.2 自定义指令指令

这个需要你在每个按钮元素中使用v-has="btn.User.XXXX"去判断。比v-if方便。不需要在组件内部引入仓库

import pinia from '@/store'
import useUserStore from '@/store/modules/user'
const userStore = useUserStore(pinia)
export const isHasButton = (app: any) => {//获取对应的用户仓库//全局自定义指令:实现按钮的权限app.directive('has', {//代表使用这个全局自定义指令的DOM|组件挂载完毕的时候会执行一次mounted(el: any, options: any) {//自定义指令右侧的数值:如果在用户信息buttons数组当中没有//从DOM树上干掉//el就是dom元素//options:传入进来的值if (!userStore.buttons.includes(options.value)) {el.parentNode.removeChild(el)}},})
}

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

17 打包成功

pnpm run build
注意,有些变量定义了未使用会报错。
tsconfig.json:
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

相关文章:

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

1.路由配置 1.1路由组件的雏形 src\views\home\index.vue&#xff08;以home组件为例&#xff09; 1.2路由配置 1.2.1路由index文件 src\router\index.ts //通过vue-router插件实现模板路由配置 import { createRouter, createWebHashHistory } from vue-router import …...

黑马头条-环境搭建、SpringCloud

一、项目介绍 1. 项目背景介绍 项目概述 类似于今日头条&#xff0c;是一个新闻资讯类项目。 随着智能手机的普及&#xff0c;人们更加习惯于通过手机来看新闻。由于生活节奏的加快&#xff0c;很多人只能利用碎片时间来获取信息&#xff0c;因此&#xff0c;对于移动资讯客…...

基于centos2009搭建openstack-t版-ovs网络-脚本运行

openstackT版脚本 环境变量ip初始化 controlleriaas-pre.shiaas-install-mysql.shiaas-install-keystone.shiaas-install-glance.shiaas-install-placement.shiaas-install-nova-controller.shiaas-install-neutron-controller.shiaas-install-dashboard.sh computeiaas-instal…...

buuctf-web

查看后端源码 得到base64编码&#xff0c;解码得flag...

UBUNTU22 安装QT5.15.2 记录

安装QT预置安装软件包 sudo apt install gcc sudo apt install g sudo apt install clang sudo apt install clang sudo apt install make sudo snap install cmake --classic sudo apt-get install build-essential sudo apt install libxcb-xinerama0 #安装OpenGL核心库 su…...

C++基础知识:C++内存分区模型,全局变量和静态变量以及常量,常量区,字符串常量和其他常量,栈区,堆区,代码区和全局区

1.C内存分区模型 C程序在执行时&#xff0c;将内存大方向划分为4个区域 代码区:存放函数体的二进制代码&#xff0c;由操作系统进行管理的&#xff08;在编译器中所书写的代码都会存放在这个空间。&#xff09; 全局区:存放全局变量和静态变量以及常量 栈区:由编译器自动分…...

MySQL面试题-重难点

mysql中有哪些锁&#xff1f;举出所有例子&#xff0c;各个锁的作用是什么&#xff1f;区别是什么&#xff1f; 共享锁&#xff1a;也叫读锁&#xff0c;简称S锁&#xff0c;在事务要读取一条记录时&#xff0c;先获取该记录的S锁&#xff0c;别的事务也可以继续获取该记录的S…...

【Linux杂货铺】期末总结篇3:用户账户管理命令 | 组账户管理命令

&#x1f308;个人主页&#xff1a;聆风吟_ &#x1f525;系列专栏&#xff1a;Linux杂货铺、Linux实践室 &#x1f516;少年有梦不应止于心动&#xff0c;更要付诸行动。 文章目录 第五章5.1 ⛳️Linux 账户5.2 ⛳️用户配置文件和目录&#xff08;未完待续&#xff09;5.2.1 …...

基于STM32设计的超声波测距仪(微信小程序)(186)

基于STM32设计的超声波测距仪(微信小程序)(186) 文章目录 一、前言1.1 项目介绍【1】项目功能介绍【2】项目硬件模块组成1.2 设计思路【1】整体设计思路【2】ESP8266工作模式配置1.3 项目开发背景【1】选题的意义【2】可行性分析【3】参考文献1.4 开发工具的选择1.5 系统框架图…...

Web前端-Web开发HTML基础2-list

一. 基础 1. 写一个列表标签&#xff0c;生成一个有三条记录的无序列表&#xff1b; 2. 写一个列表标签&#xff0c;生成一个有四条记录的无序列表&#xff1b; 3. 写一个列表标签&#xff0c;生成一个有五条记录的无序列表&#xff1b; 4. 写一个列表标签&#xff0c;生成一个…...

MAVSDK-Java安卓客户端编译与使用完整示例

效果&#xff1a; 1.启动PX4容器 2.监听QGC连接端口 3.手机与QGC连接到同一局域网&#xff08;此例QGC为&#xff1a;192.168.6.250 手机为&#xff1a;192.168.6.86&#xff09; 4.监听手机mavsdk_server连接端口 5.使用Android Studio打开MAVSDK-JAVA下的examples/android-c…...

JavaEE:Spring Web简单小项目实践二(用户登录实现)

学习目的&#xff1a; 1、理解前后端交互过程 2、学习接口传参&#xff0c;数据返回以及页面展示 1、准备工作 创建SpringBoot项目&#xff0c;引入Spring Web依赖&#xff0c;添加前端页面到项目中。 前端代码&#xff1a; login.html <!DOCTYPE html> <html lang&…...

深度学习 | CNN 基本原理

目录 1 什么是 CNN2 输入层3 卷积层3.1 卷积操作3.2 Padding 零填充3.3 处理彩色图像 4 池化层4.1 池化操作4.2 池化的平移不变性 5 全连接层6 输出层 前言 这篇博客不够详细&#xff0c;因为没有介绍卷积操作的具体计算&#xff1b;但是它介绍了 CNN 各层次的功能…...

解读|http和https的区别,谁更好用

在日常我们浏览网页时&#xff0c;有些网站会看到www前面是http&#xff0c;有些是https&#xff0c;这两种有什么区别呢&#xff1f;为什么单单多了“s”&#xff0c;会有人说这个网页会更安全些&#xff1f; HTTP&#xff08;超文本传输协议&#xff09;和HTTPS&#xff08;…...

汽车零部件制造企业MES系统主要功能介绍

随着汽车工业的不断发展&#xff0c;汽车零部件制造企业面临着越来越高的生产效率、质量控制和成本管理要求。MES系统作为一种综合信息系统&#xff0c;能够帮助企业实现从订单接收到产品交付的全流程数字化管理&#xff0c;优化资源配置&#xff0c;提高生产效率&#xff0c;确…...

常见的五种聚类算法总结

常见的聚类算法总结 1. K-Means 聚类 描述 K-Means 是一种迭代优化的聚类算法&#xff0c;它通过最小化样本点到质心的距离平方和来进行聚类。 思想 随机选择 K 个初始质心。分配每个数据点到最近的质心&#xff0c;形成 K 个簇。重新计算每个簇的质心。重复上述步骤&…...

智能车存在网络安全隐患,如何应设计出更好的安全防护技术?

智能车网络安全防护技术的研究与设计 摘要&#xff1a;随着智能车技术的迅速发展&#xff0c;车辆的网络连接性不断增强&#xff0c;然而这也带来了诸多网络安全隐患。本文深入探讨了智能车面临的网络安全威胁&#xff0c;并提出了一系列创新的安全防护技术设计&#xff0c;旨…...

通讯的概念

通讯的概念 文章目录 通讯的概念1.通讯的基本概念2. 串行通讯与并行通讯2. 全双工、半双工及单工通讯3. 同步通讯与异步通讯4. 通讯速率 1.通讯的基本概念 通讯是指在嵌入式系统中实现数据交换的技术手段&#xff0c;它涉及到硬件与硬件、硬件与软件之间的信息传输。基本概念包…...

Centos7 rpm 安装 Mysql 8.0.28

Centos7 rpm 安装 Mysql 8.0.28 一、检查系统是否已经安装了Mysql 如果安装了则卸载 [rootiZbp1byzaznzn9jncxr010Z /]# rpm -qa | grep mysql[rootiZbp1byzaznzn9jncxr010Z /]# rpm -qa | grep mariadb mariadb-libs-5.5.68-1.el7.x86_64如果安装了 mysql &#xff0c;maria…...

Linux 多进程编程详解

Linux 多进程编程详解 多进程编程是现代操作系统中一种重要的并发编程技术。通过在同一程序中运行多个独立的进程&#xff0c;可以实现并发处理&#xff0c;充分利用多核处理器的优势&#xff0c;提高程序的运行效率。本文将详细介绍Linux多进程的基本概念、创建方法、进程间通…...

C语言之大小端理解

目录 1前言2 大小端理解与区分3 大小端的识别和基本切换操作4 总结 1前言 在汽车CAN通讯报文中往往会接触到Intel类型和motorola类型&#xff0c;实际项目中涉及到多机通讯也会接触到大小端问题 2 大小端理解与区分 大端(Big_Endian) :低字节放在高地址小端(Little_Endian):…...

GIT相关操作,推送本地分支到远程仓库流程记录学习

git流程 切换到源文件夹&#xff1a;cd 源文件夹克隆远程仓库&#xff1a;git clone [ssh]进入项目文件夹&#xff1a;cd .\project\查看本地分支&#xff1a;git branch获取远程仓库更新&#xff0c;使远程同步&#xff1a;git fetch查看所有分支&#xff08;包括远程分支&am…...

网络协议 — Keepalived 高可用方案

目录 文章目录 目录Keepalived 是实现了 VRRP 协议的软件Keepalived 的软件架构VRRP StackCheckersKeepalived 的配置Global configurationvrrp_scriptVRRP Configurationvrrp synchroization groupvrrp instancevirtual ip addressesvirtual routesLVS Configurationvirtual_s…...

前端报错adding CSS “touch-action: none“ to this element解决方案

目录 如图所示控制台出现报错&#xff1a; 原因&#xff1a; touch-action 介绍&#xff1a; 解决方案&#xff1a; 1.手动设置touch-action&#xff1a; 2.使用条件渲染&#xff1a; 3.CSS样式隔离&#xff1a; 4.浏览器兼容性&#xff1a; 5. 忽略警告 如图所示控制台…...

使用phpMyAdmin操作MYSQL(四)

一. 学会phpMyAdmin&#xff1f; phpMyAdminhttp://water.ve-techsz.cn/phpmyadmin/ 虽然我我们可以用命令行操作数据库&#xff0c;但这样难免没有那么直观&#xff0c;方便。所以接下来我们使用phpMyAdmin来操作MySQL&#xff0c;phpMyAdmin是众多MySQL图形化管理工具中使用…...

webpack配置代理请求

在 Webpack 中&#xff0c;可以通过配置devServer中的proxy选项来设置代理请求&#xff0c;以解决开发环境中的跨域问题或实现特定的请求转发逻辑。以下是一个常见的 Webpack 配置示例&#xff0c;展示了如何设置代理&#xff1a; module.exports {// 其他配置项...devServer…...

热门软件缺陷管理工具2024:专业评测与建议

国内外主流的10款软件缺陷管理工具软件对比&#xff1a;PingCode、Worktile、禅道、Tapd、Teambition、Tower、JIRA、Bugzilla、MantisBT、Trac。 在软件开发过程中&#xff0c;管理缺陷和漏洞常常成为一项挑战&#xff0c;尤其是在项目规模庞大时。选择一个高效的软件缺陷管理…...

冒泡,选择,插入,希尔排序

目录 一. 冒泡排序 1. 算法思想 2. 时间复杂度与空间复杂度 3. 代码实现 二. 选择排序 1. 算法思想 2. 时间复杂度与空间复杂度 3. 代码实现 三.插入排序 1. 直接插入排序 (1). 算法思想 (2). 时间复杂度与空间复杂度 (3). 代码实现 2. 希尔排序 (1). 算法思想 …...

【HarmonyOS学习】Calendar Kit日历管理

简介 Calendar Kit提供日历与日程管理能力&#xff0c;包括日历的获取和日程的创建能力。 Calendar Kit为用户提供了一系列接口来获取日历账户&#xff0c;并使用特定的接口向日历账户中写入日程。 如果写入的日程带有提醒时间则系统会在时间到达时向用户发送提醒。 约束点…...

RDMA 高性能架构基本原理与设计方案

RDMA的主要优点包括低延迟、高吞吐量、减少CPU负担和支持零拷贝网络。它允许数据直接在网络接口卡&#xff08;NIC&#xff09;和内存之间传输&#xff0c;减少了数据传输过程中的中间环节&#xff0c;从而显著降低了延迟。RDMA技术能够实现高速的数据传输&#xff0c;适用于需…...