src\views\home\index.vue
(以home组件为例)
src\router\index.ts
//通过vue-router插件实现模板路由配置 import { createRouter, createWebHashHistory } from 'vue-router' import { constantRoute } from './router' //创建路由器 const router = createRouter({ //路由模式hash history: createWebHashHistory(), routes: constantRoute, //滚动行为 scrollBehavior() { return { left: 0, top: 0, } }, }) export default router
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', }, ]
src\App.vue
src\views\login\index.vue
Hello
欢迎来到硅谷甄选
登录
注意:el-col是24份的,在此左右分为了12份。我们在右边放置我们的结构。:xs="0"
是为了响应式。el-form
下的element-plus元素都用el-form-item
包裹起来。
回调应该做的事情
const login = () => { //点击登录按钮以后干什么 //通知仓库发起请求 //请求成功->路由跳转 //请求失败->弹出登陆失败信息 }
安装pinia:pnpm i pinia@2.0.34
src\store\index.ts
//仓库大仓库 import { createPinia } from 'pinia' //创建大仓库 const pinia = createPinia() //对外暴露:入口文件需要安装仓库 export default pinia
src\store\modules\user.ts
//创建用户相关的小仓库 import { defineStore } from 'pinia' //创建用户小仓库 const useUserStore = defineStore('User', { //小仓库存储数据地方 state: () => {}, //处理异步|逻辑地方 actions: {}, getters: {}, }) //对外暴露小仓库 export default useUserStore
//登录按钮的回调 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, }) } }
//创建用户相关的小仓库 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
引入:import { ElNotification } from 'element-plus'
使用:
//登录失败的提示信息 ElNotification({ type: 'error', message: (error as Error).message, })
interface dataType { token?: string message?: string } //登录接口返回的数据类型 export interface loginResponseData { code: number data: dataType }
//定义小仓库数据state类型 export interface UserState { token: string | null }
将本地存储的方法封装到一起
//封装本地存储存储数据与读取数据方法 export const SET_TOKEN = (token: string) => { localStorage.setItem('TOKEN', token) } export const GET_TOKEN = () => { return localStorage.getItem('TOKEN') }
//封装函数:获取当前时间段 export const getTime = () => { let message = '' //通过内置构造函数Date const hour = new Date().getHours() if (hour <= 9) { message = '早上' } else if (hour <= 14) { message = '上午' } else if (hour <= 18) { message = '下午' } else { message = '晚上' } return message }
:model:绑定的数据
//收集账号与密码数据 let loginForm = reactive({ username: 'admin', password: '111111' })
:rules:对应要使用的规则
//定义表单校验需要的配置对象 const rules = {}
ref="loginForms":获取表单元素
//获取表单元素 let loginForms = ref()
Form 组件提供了表单验证的功能,只需为 rules 属性传入约定的验证规则,并将 form-Item 的 prop 属性设置为需要验证的特殊键值即可
//定义表单校验需要的配置对象 const rules = { username: [ //规则对象属性: { required: true, // required,代表这个字段务必要校验的 min: 5, //min:文本长度至少多少位 max: 10, // max:文本长度最多多少位 message: '长度应为6-10位', // message:错误的提示信息 trigger: 'change', //trigger:触发校验表单的时机 change->文本发生变化触发校验, blur:失去焦点的时候触发校验规则 }, ], password: [ { required: true, min: 6, max: 10, message: '长度应为6-15位', trigger: 'change', }, ], }
const login = async () => { //保证全部表单项校验通过 await loginForms.value.validate() 。。。。。。 }
使用自己编写的函数作为规则校验。
//定义表单校验需要的配置对象 const rules = { username: [ //规则对象属性: /* { required: true, // required,代表这个字段务必要校验的 min: 5, //min:文本长度至少多少位 max: 10, // max:文本长度最多多少位 message: '长度应为6-10位', // message:错误的提示信息 trigger: 'change', //trigger:触发校验表单的时机 change->文本发生变化触发校验, blur:失去焦点的时候触发校验规则 }, */ { trigger: 'change', validator: validatorUserName }, ], password: [ { trigger: 'change', validator: validatorPassword }, ], }
//自定义校验规则函数 const validatorUserName = (rule: any, value: any, callback: any) => { //rule:校验规则对象 //value:表单元素文本内容 //callback:符合条件,callback放行通过,不符合:注入错误提示信息 if (value.length >= 5) { callback() } else { callback(new Error('账号长度至少5位')) } } const validatorPassword = (rule: any, value: any, callback: any) => { if (value.length >= 6) { callback() } else { callback(new Error('密码长度至少6位')) } }
注意:我们将主界面单独放一个文件夹(顶替原来的home路由组件)。注意修改一下路由配置
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; }
页面左上角的这部分,我们将它做成子组件,并且封装方便维护以及修改。
在这里我们引用了封装好的setting
{{ setting.title }}
为了方便我们以后对logo以及标题的修改。
//用于项目logo|标题配置 export default { title: '硅谷甄选运营平台', //项目的标题 logo: '/public/logo.png', //项目logo设置 logoHidden: true, //logo组件是否隐藏 }
在layout组件中引入并使用
主要使用到了element-plus的menu组件。附带使用了滚动组件
首页 数据大屏 权限管理 用户管理 角色管理 菜单管理
在这一部分,我们要根据路由生成左侧的菜单栏
因为我们要根据路由以及其子路由作为我们菜单的一级|二级标题。因此我们要获取路由信息。
给路由中加入了路由元信息meta:它包含了2个属性:title以及hidden
{ //登录路由 path: '/login', component: () => import('@/views/login/index.vue'), name: 'login', //命名路由 meta: { title: '登录', //菜单标题 hidden: true, //路由的标题在菜单中是否隐藏 }, },
//引入路由(常量路由) import { constantRoute } from '@/router/routes' 。。。。。 //小仓库存储数据地方 state: (): UserState => { return { token: GET_TOKEN(), //用户唯一标识token menuRoutes: constantRoute, //仓库存储生成菜单需要数组(路由) }
标 {{ item.meta.title }} 标 {{ item.children[0].meta.title }} = 2" > {{ item.meta.title }}
注意: 1:因为每一个项我们要判断俩次(是否要隐藏,以及子组件个数),所以在el-menu-item外面又套了一层模板 2:当子路由个数大于等于一个时,并且或许子路由还有后代路由时。这里我们使用了递归组件。递归组件需要命名(另外使用一个script标签,vue2格式)。
因为我们要根据路由配置对应的图标,也要为了后续方便更改。因此我们将所有的图标注册为全局组件。(使用之前将分页器以及矢量图注册全局组件的自定义插件)(所有图标全局注册的方法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) } }, }
以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', }, }, ], },
以只有一个子路由的组件为例:
{{ item.children[0].meta.title }}
{ path: '/acl', component: () => import('@/layout/index.vue'), name: 'Acl', meta: { hidden: false, title: '权限管理', icon: 'Lock', }, children: [ { path: '/acl/user', component: () => import('@/views/acl/user/index.vue'), name: 'User', meta: { hidden: false, title: '用户管理', icon: 'User', }, }, { path: '/acl/role', component: () => import('@/views/acl/role/index.vue'), name: 'Role', meta: { hidden: false, title: '角色管理', icon: 'UserFilled', }, }, { path: '/acl/permission', component: () => import('@/views/acl/permission/index.vue'), name: 'Permission', meta: { hidden: false, title: '菜单管理', icon: 'Monitor', }, }, ], },
第三种情况我们使用组件递归,所以只需要给前面的2个添加函数
在这部分对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是所有一级路由组件的父组件
当页面刷新时,菜单会自动收起。我们使用element-plus的*default-active *处理。$router.path为当前路由。 src\layout\index.vue
element-plus:breadcrumb el-button el-dropdown
权限挂历 用户管理 admin 退出登陆
组件拆分:
权限挂历 用户管理
admin 退出登陆
定义一个折叠变量来判断现在的状态是否折叠。因为这个变量同时给breadcrumb组件以及父组件layout使用,因此将这个变量定义在pinia中
//小仓库:layout组件相关配置仓库 import { defineStore } from 'pinia' let useLayOutSettingStore = defineStore('SettingStore', { state: () => { return { fold: false, //用户控制菜单折叠还是收起的控制 } }, }) export default useLayOutSettingStore
。。。。。。。 。。。。。。
绑定动态样式修改scss
效果图: 注意:折叠文字的时候会把图标也折叠起来。在menu组件中吧图标放到template外面就可以。
注意$router和$route是不一样的
注意:使用了$route.matched函数,此函数能得到当前路由的信息
访问首页时,因为它是二级路由,会遍历出layout面包屑,处理:删除layout路由的title。再加上一个判断
注意:将路由中的一级路由权限管理以及商品管理重定向到第一个孩子,这样点击跳转的时候会定向到第一个孩子。
//全屏按钮点击的回调 const fullScreen = () => { //DOM对象的一个属性:可以用来判断当前是不是全屏的模式【全屏:true,不是全屏:false】 let full = document.fullscreenElement //切换成全屏 if (!full) { //文档根节点的方法requestFullscreen实现全屏 document.documentElement.requestFullscreen() } else { //退出全屏 document.exitFullscreen() }
==登录这一块大概逻辑,前端发送用户名密码到后端,后端返回token,前端保存,并且请求拦截器,请求头有token就要携带token==
登录之后页面(home)上来就要获取用户信息。并且将它使用到页面中
import type { RouteRecordRaw } from 'vue-router' //定义小仓库数据state类型 export interface UserState { token: string | null menuRoutes: RouteRecordRaw[] username: string avatar: string }
//引入用户相关的仓库 import useUserStore from '@/store/modules/user' 。。。。。。 //请求拦截器 request.interceptors.request.use((config) => { //获取用户相关的小仓库,获取token,登录成功以后携带个i服务器 const userStore = useUserStore() if (userStore.token) { config.headers.token = userStore.token } //config配置对象,headers请求头,经常给服务器端携带公共参数 //返回配置对象 return config })
//获取用户信息方法 async userInfo() { //获取用户信息进行存储 let result = await reqUserInfo() if (result.code == 200) { this.username = result.data.checkUser.username this.avatar = result.data.checkUser.avatar } },
src\layout\tabbar\setting\index.vue
//退出登陆点击的回调 const logout = () => { //第一件事:需要项服务器发请求【退出登录接口】(我们这里没有) //第二件事:仓库当中和关于用户的相关的数据清空 userStore.userLogout() //第三件事:跳转到登陆页面 }
//退出登录 userLogout() { //当前没有mock接口(不做):服务器数据token失效 //本地数据清空 this.token = '' this.username = '' this.avatar = '' REMOVE_TOKEN() },
注意:携带的query参数方便下次登陆时直接跳转到当时推出的界面 个人觉得这个功能没什么作用。但是可以学习方法
//退出登陆点击的回调 const logout = () => { //第一件事:需要项服务器发请求【退出登录接口】(我们这里没有) //第二件事:仓库当中和关于用户的相关的数据清空 userStore.userLogout() //第三件事:跳转到登陆页面 $router.push({ path: '/login', query: { redirect: $route.path } }) }
src\permisstion.ts
(新建文件) main.ts引入
pnpm i nprogress
//路由鉴权:鉴权:项目当中路由能不能被访问的权限 import router from '@/router' import nprogress from 'nprogress' //引入进度条样式 import 'nprogress/nprogress.css' //全局前置守卫 router.beforeEach((to: any, from: any, next: any) => { //访问某一个路由之前的守卫 nprogress.start() next() }) //全局后置守卫 router.afterEach((to: any, from: any) => { // to and from are both route objects. nprogress.done() }) //第一个问题:任意路由切换实现进度条业务 ----nprogress
//路由鉴权:鉴权:项目当中路由能不能被访问的权限 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 其余都可以
路由鉴权几个注意点:
个人理解:之前在app中是不需要导入pinia的,是因为我们这次的文件时写在和main.ts同级的下面,所以我们使用的时候是没有pinia的。而之前使用时app已经使用了pinia了,所以我们不需要导入pina。
接口文档: http://139.198.104.58:8209/swagger-ui.htmlhttp://139.198.104.58:8212/swagger-ui.html
将.env.development,.env.production .env.test,三个环境文件下的服务器域名写为:
import { loadEnv } from 'vite' 。。。。。。 export default defineConfig(({ command, mode }) => { //获取各种环境下的对应的变量 let env = loadEnv(mode, process.cwd()) return { 。。。。。。。 //代理跨域 server: { proxy: { [env.VITE_APP_BASE_API]: { //获取数据服务器地址的设置 target: env.VITE_SERVE, //需要代理跨域 changeOrigin: true, //路径重写 rewrite: (path) => path.replace(/^\/api/, ''), }, }, }, } })
在这里退出登录有了自己的api
//统一管理项目用户相关的接口 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(API.LOGIN_URL, data) } //获取用户信息接口方法 export const reqUserInfo = () => { return request.get(API.USERINFO_URL) } //退出登录 export const reqLogout = () => { return request.post(API.LOGOUT_URL) }
替换原有的请求接口函数,以及修改退出登录函数。以及之前引入的类型显示我们展示都设置为any
//创建用户相关的小仓库 import { defineStore } from 'pinia' //引入接口 import { reqLogin, reqUserInfo, reqLogout } from '@/api/user' import type { UserState } from './types/type' //引入操作本地存储的工具方法 import { SET_TOKEN, GET_TOKEN, REMOVE_TOKEN } from '@/utils/token' //引入路由(常量路由) import { constantRoute } from '@/router/routes' //创建用户小仓库 const useUserStore = defineStore('User', { //小仓库存储数据地方 state: (): UserState => { return { token: GET_TOKEN(), //用户唯一标识token menuRoutes: constantRoute, //仓库存储生成菜单需要数组(路由) username: '', avatar: '', } }, //处理异步|逻辑地方 actions: { //用户登录的方法 async userLogin(data: any) { //登录请求 const result: any = await reqLogin(data) if (result.code == 200) { //pinia仓库存储token //由于pinia|vuex存储数据其实利用js对象 this.token = result.data as string //本地存储持久化存储一份 SET_TOKEN(result.data as string) //保证当前async函数返回一个成功的promise函数 return 'ok' } else { return Promise.reject(new Error(result.data)) } }, //获取用户信息方法 async userInfo() { //获取用户信息进行存储 const result = await reqUserInfo() console.log(result) if (result.code == 200) { this.username = result.data.name this.avatar = result.data.avatar return 'ok' } else { return Promise.reject(new Error(result.message)) } }, //退出登录 async userLogout() { const result = await reqLogout() if (result.code == 200) { //本地数据清空 this.token = '' this.username = '' this.avatar = '' REMOVE_TOKEN() return 'ok' } else { return Promise.reject(new Error(result.message)) } }, }, getters: {}, }) //对外暴露小仓库 export default useUserStore
退出成功后再跳转
src\permisstion.ts
也是退出成功后再跳转
//登录接口需要携带参数类型 export interface loginFormData { username: string password: string } //定义全部接口返回数据都有的数据类型 export interface ResponseData { code: number message: string ok: boolean } //定义登录接口返回数据类型 export interface loginResponseData extends ResponseData { data: string } //定义获取用户信息返回的数据类型 export interface userInfoResponseData extends ResponseData { data: { routes: string[] button: string[] roles: string[] name: string avatar: string } }
注意:在src\store\modules\user.ts以及src\api\user\index.ts文件中对发请求时的参数以及返回的数据添加类型定义
使用element-plus。
添加品牌 ,total, sizes," :total="400" />
//书写品牌管理模块接口 import request from '@/utils/request' //品牌管理模块接口地址 enum API { //获取已有品牌接口 TRADEMARK_URL = '/admin/product/baseTrademark/', } //获取一样偶品牌的接口方法 //page:获取第几页 ---默认第一页 //limit:获取几个已有品牌的数据 export const reqHasTrademark = (page: number, limit: number) => request.get(API.TRADEMARK_URL + `${page}/${limit}`)
我们获取数据没有放在pinia中,二是放在组件中挂载时获取数据
在数据展示模块,我们使用了element-plus的el-table,下面组要讲解属性和注意点。
比如我们这里绑定的trademarkArr是个三个对象的数组,就会多出来3行。
注意:因为我们之前已经绑定了数据,所以在这里直接使用数据的属性tmName
为什么要使用插槽呢?因为prop属性虽然能够展示数据,但是他默认是div,如果我们的图片使用prop展示的话,会展示图片的路径。因此如果想展示图片或者按钮,我们就要使用插槽 注意:row就是我们的trademarkArr的每一个数据(对象)
API中的以及组件中。
export interface ResponseData { code: number message: string ok: boolean } //已有的品牌的ts数据类型 export interface TradeMark { id?: number tmName: string logoUrl: string } //包含全部品牌数据的ts类型 export type Records = TradeMark[] //获取的已有全部品牌的数据ts类型 export interface TradeMarkResponseData extends ResponseData { data: { records: Records total: number size: number current: number searchCount: boolean pages: number } }
此部分主要是俩个功能,第一个是当点击分页器页数时能跳转到对应的页数。第二个是每页展示的数据条数能正确显示
这里我们绑定的点击回调直接用的是之前写好的发送请求的回调。可以看出,发送请求的回调函数是有默认的参数:1. 注意:因为current-change方法时element-plus封装好的,它会给父组件传递并注入一个参数(点击的页码),所以相当于把这个参数传递给了getHasTrademark函数,因此能够跳转到正确的页码数
//获取已有品牌的接口封装为一个函数:在任何情况下向获取数据,调用次函数即可 const getHasTrademark = async (pager = 1) => { //当前页码 pageNo.value = pager let result: TradeMarkResponseData = await reqHasTrademark( pageNo.value, limit.value, ) if (result.code == 200) { //存储已有品牌总个数 total.value = result.data.total trademarkArr.value = result.data.records } }
//当下拉菜单发生变化的时候触发此方法 //这个自定义事件,分页器组件会将下拉菜单选中数据返回 const sizeChange = () => { //当前每一页的数据量发生变化的时候,当前页码归1 getHasTrademark() console.log(123) }
同样的这个函数也会返回一个参数。但是我们不需要使用这个参数,因此才另外写一个回调函数。
v-model:属性用户控制对话框的显示与隐藏的 true显示 false隐藏 title:设置对话框左上角标题
取消 确定
因为这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(API.UPDATETRADEMARK_URL, data) } else { //新增品牌 return request.post(API.ADDTRADEMARK_URL, data) } }
import type { 。。。。。。。 TradeMark, } from '@/api/product/trademark/type' //定义收集新增品牌数据 let trademarkParams = reactive({ tmName: '', logoUrl: '', })
class:带的一些样式,需复制到style中 action:图片上传路径需要书写/api,否则代理服务器不发送这次post请求 :show-file-list:是否展示已经上传的文件 :before-upload:上传图片之前的钩子函数
//上传图片组件->上传图片之前触发的钩子函数 const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => { //钩子是在图片上传成功之前触发,上传文件之前可以约束文件类型与大小 //要求:上传文件格式png|jpg|gif 4M if ( rawFile.type == 'image/png' || rawFile.type == 'image/jpeg' || rawFile.type == 'image/gif' ) { if (rawFile.size / 1024 / 1024 < 4) { return true } else { ElMessage({ type: 'error', message: '上传文件大小小于4M', }) return false } } else { ElMessage({ type: 'error', message: '上传文件格式务必PNG|JPG|GIF', }) return false } }
:on-success:图片上传成功钩子(收集了上传图片的地址) 在这里,你将本地的图片上传到之前el-upload组件的action="/api/admin/product/fileUpload"
这个地址上,然后on-success钩子会将上传后图片的地址返回
//图片上传成功钩子 const handleAvatarSuccess: UploadProps['onSuccess'] = ( response, uploadFile, ) => { //response:即为当前这次上传图片post请求服务器返回的数据 //收集上传图片的地址,添加一个新的品牌的时候带给服务器 trademarkParams.logoUrl = response.data //图片上传成功,清除掉对应图片校验结果 formRef.value.clearValidate('logoUrl') }
const confirm = async () => { //在你发请求之前,要对于整个表单进行校验 //调用这个方法进行全部表单相校验,如果校验全部通过,在执行后面的语法 // await formRef.value.validate() let result: any = await reqAddOrUpdateTrademark(trademarkParams) //添加|修改已有品牌 if (result.code == 200) { //关闭对话框 dialogFormVisible.value = false //弹出提示信息 ElMessage({ type: 'success', message: trademarkParams.id ? '修改品牌成功' : '添加品牌成功', }) //再次发请求获取已有全部的品牌数据 getHasTrademark(trademarkParams.id ? pageNo.value : 1) } else { //添加品牌失败 ElMessage({ type: 'error', message: trademarkParams.id ? '修改品牌失败' : '添加品牌失败', }) //关闭对话框 dialogFormVisible.value = false } }
//添加品牌按钮的回调 const addTrademark = () => { //对话框显示 dialogFormVisible.value = true //清空收集数据 trademarkParams.tmName = '' trademarkParams.logoUrl = '' }
其中的row就是当前的数据
//修改已有品牌的按钮的回调 //row:row即为当前已有的品牌 const updateTrademark = (row: TradeMark) => { //对话框显示 dialogFormVisible.value = true //ES6语法合并对象 Object.assign(trademarkParams, row) }
const confirm = async () => { 。。。。。。。 if (result.code == 200) { 。。。 //弹出提示信息 ElMessage({ 。。。。 message: trademarkParams.id ? '修改品牌成功' : '添加品牌成功', }) //再次发请求获取已有全部的品牌数据 getHasTrademark(trademarkParams.id ? pageNo.value : 1) } else { //添加品牌失败 ElMessage({ 。。。。 message: trademarkParams.id ? '修改品牌失败' : '添加品牌失败', }) 。。。。 } }
当我们修改操作之后再点击添加品牌,对话框的title依旧是修改品牌。怎么是因为对话框的title是根据trademarkParams.id来的,我们之前添加品牌按钮操作没有对id进行清除。修改为如下就可
//添加品牌按钮的回调 const addTrademark = () => { //对话框显示 dialogFormVisible.value = true //清空收集数据 trademarkParams.id = 0 trademarkParams.tmName = '' trademarkParams.logoUrl = '' }
:model:校验的数据 :rules:校验规则 ref="formRef":表单实例 prop:表单元素校验的数据,可以直接使用表单绑定的数据。
//表单校验规则对象 const rules = { tmName: [ //required:这个字段务必校验,表单项前面出来五角星 //trigger:代表触发校验规则时机[blur、change] { required: true, trigger: 'blur', validator: validatorTmName }, ], logoUrl: [{ required: true, validator: validatorLogoUrl }], }
//品牌自定义校验规则方法 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图片务必上传')) } }
因为img是图片,不好判断。因此使用表单的validate属性,全部校验,放在确认按钮的回调函数中
const confirm = async () => { //在你发请求之前,要对于整个表单进行校验 //调用这个方法进行全部表单相校验,如果校验全部通过,在执行后面的语法 await formRef.value.validate() 。。。。。。 }
当图片没有上传点击确认后会出来校验的提示信息,我们上传图片后校验信息应该消失。使用表单的clearValidate属性
//图片上传成功钩子 const handleAvatarSuccess: UploadProps['onSuccess'] = ( 。。。。。。 ) => { 。。。。。。。 //图片上传成功,清除掉对应图片校验结果 formRef.value.clearValidate('logoUrl') }
当我们未填写信息去点击确认按钮时,会弹出2个校验信息。当我们关闭后再打开,校验信息还在。因为,我们需要在添加品牌按钮时清除校验信息。但是因为点击添加品牌,表单还没有加载,所以我们需要换个写法。
//添加品牌按钮的回调 const addTrademark = () => { //对话框显示 dialogFormVisible.value = true //清空收集数据 trademarkParams.id = 0 trademarkParams.tmName = '' trademarkParams.logoUrl = '' //第一种写法:ts的问号语法 formRef.value?.clearValidate('tmName') formRef.value?.clearValidate('logoUrl') /* nextTick(() => { formRef.value.clearValidate('tmName') formRef.value.clearValidate('logoUrl') }) */ }
同理修改按钮
//修改已有品牌的按钮的回调 //row:row即为当前已有的品牌 const updateTrademark = (row: TradeMark) => { //清空校验规则错误提示信息 nextTick(() => { formRef.value.clearValidate('tmName') formRef.value.clearValidate('logoUrl') }) 。。。。。。 }
删除业务要做的事情不多,包括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(API.DELETE_URL + id)
这里使用了一个气泡组件,@confirm绑定的就是回调函数
//气泡确认框确定按钮的回调 const removeTradeMark = async (id: number) => { //点击确定按钮删除已有品牌请求 let result = await reqDeleteTrademark(id) if (result.code == 200) { //删除成功提示信息 ElMessage({ type: 'success', message: '删除品牌成功', }) //再次获取已有的品牌数据 getHasTrademark( trademarkArr.value.length > 1 ? pageNo.value : pageNo.value - 1, ) } else { ElMessage({ type: 'error', message: '删除品牌失败', }) } }
属性管理分为上面部分的三级分类模块以及下面的添加属性部分。我们将三级分类模块单独提取出来做成全局组件
注意:要在src\components\index.ts
下引入。
添加属性
一级分类的流程时:API->pinia->组件 为什么要使用pinia呢?因为在下面的添加属性那部分,父组件要用到三级分类组件的信息(id),所以将数据放在pinia中是最方便的。
//这里书写属性相关的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(API.C1_URL) //获取二级分类的接口方法 export const reqC2 = (category1Id: number | string) => { return request.get(API.C2_URL + category1Id) } //获取三级分类的接口方法 export const reqC3 = (category2Id: number | string) => { return request.get(API.C3_URL + category2Id) }
//商品分类全局组件的小仓库 import { defineStore } from 'pinia' import { reqC1, } from '@/api/product/attr' const useCategoryStore = defineStore('Category', { state: () => { return { //存储一级分类的数据 c1Arr: [], //存储一级分类的ID c1Id: '', } }, actions: { //获取一级分类的方法 async getC1() { //发请求获取一级分类的数据 const result = await reqC1() if (result.code == 200) { this.c1Arr = result.data } }, }, getters: {}, }) export default useCategoryStore
注意:el-option中的:value属性,它将绑定的值传递给el-select中的v-model绑定的值
。。。。。。
//分类相关的数据ts类型 export interface ResponseData { code: number message: string ok: boolean } //分类ts类型 export interface CategoryObj { id: number | string name: string category1Id?: number category2Id?: number } //相应的分类接口返回数据的类型 export interface CategoryResponseData extends ResponseData { data: CategoryObj[] }
使用:仓库中的result,API中的接口返回的数据
import type { CategoryObj } from '@/api/product/attr/type' 。。。。。 //定义分类仓库state对象的ts类型 export interface CategoryState { c1Id: string | number c1Arr: CategoryObj[] c2Arr: CategoryObj[] c2Id: string | number c3Arr: CategoryObj[] c3Id: string | number }
使用:仓库中的state数据类型
分类组件就是以及组件上来就拿到数据,通过用户选择后我们会拿到id,通过id发送请求之后二级分类就会拿到数据。以此类推三级组件。我们以二级分类为例。
二级分类不是一上来就发生变化,而是要等一级分类确定好之后再发送请求获得数据。于是我们将这个发送请求的回调函数绑定在了一级分类的change属性上
//此方法即为一级分类下拉菜单的change事件(选中值的时候会触发,保证一级分类ID有了) const handler = () => { //通知仓库获取二级分类的数据 categoryStore.getC2() }
//获取二级分类的数据 async getC2() { //获取对应一级分类的下二级分类的数据 const result: CategoryResponseData = await reqC2(this.c1Id) if (result.code == 200) { this.c2Arr = result.data } },
当我们选择好三级菜单后,此时修改一级菜单。二、三级菜单应该清空 清空id之后就不会显示了。
//此方法即为一级分类下拉菜单的change事件(选中值的时候会触发,保证一级分类ID有了) const handler = () => { //需要将二级、三级分类的数据清空 categoryStore.c2Id = '' categoryStore.c3Arr = [] categoryStore.c3Id = '' //通知仓库获取二级分类的数据 categoryStore.getC2() }
//此方法即为二级分类下拉菜单的change事件(选中值的时候会触发,保证二级分类ID有了) const handler1 = () => { //清理三级分类的数据 categoryStore.c3Id = '' categoryStore.getC3() }
在我们没选择好三级菜单之前,添加属性按钮应该处于禁用状态
src\views\product\attr\index.vue
(父组件)
//属性值对象的ts类型 export interface AttrValue { id?: number valueName: string attrId?: number flag?: boolean } //存储每一个属性值的数组类型 export type AttrValueList = AttrValue[] //属性对象 export interface Attr { id?: number attrName: string categoryId: number | string categoryLevel: number attrValueList: AttrValueList } //存储每一个属性对象的数组ts类型 export type AttrList = Attr[] //属性接口返回的数据ts类型 export interface AttrResponseData extends ResponseData { data: Attr[] }
//这里书写属性相关的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( API.ATTR_URL + `${category1Id}/${category2Id}/${category3Id}`, ) }
注意:通过watch监听c3Id,来适时的获取数据。
添加属性 {{ item.valueName }}
当我们获取数据并展示以后,此时修改一级分类或者二级分类,由于watch的存在,同样会发送请求。但是此时没有c3Id,请求会失败。因此将watch改为如下
//监听仓库三级分类ID变化 watch( () => categoryStore.c3Id, () => { //清空上一次查询的属性与属性值 attrArr.value = [] //保证三级分类得有才能发请求 if (!categoryStore.c3Id) return //获取分类的ID getAttr() }, )
当点击添加属性后:
//定义card组件内容切换变量 let scene = ref(0) //scene=0,显示table,scene=1,展示添加与修改属性结构
当点击添加属性之后,三级分类应该被禁用。因此使用props给子组件传参 子组件: 二三级分类同理。
//属性值对象的ts类型 export interface AttrValue { id?: number valueName: string attrId?: number flag?: boolean } //存储每一个属性值的数组类型 export type AttrValueList = AttrValue[] //属性对象 export interface Attr { id?: number attrName: string categoryId: number | string categoryLevel: number attrValueList: AttrValueList }
//收集新增的属性的数据 let attrParams = reactive({ attrName: '', //新增的属性的名字 attrValueList: [ //新增的属性值数组 ], categoryId: '', //三级分类的ID categoryLevel: 3, //代表的是三级分类 })
一个操作最重要的是理清楚思路。添加属性值的总体思路是:收集表单的数据(绑定对应的表单项等)->发送请求(按钮回调函数,携带的参数)->更新页面
我们给添加属性值按钮绑定一个回调,点击的时候会往attrParams.attrValueList中添加一个空数组。我们根据空数组的数量生成input框,再将input的值与数组中的值绑定。
//添加属性值按钮的回调 const addAttrValue = () => { //点击添加属性值按钮的时候,向数组添加一个属性值对象 attrParams.attrValueList.push({ valueName: '', flag: true, //控制每一个属性值编辑模式与切换模式的切换 }) }
三级分类的id(c3Id)在页面1的添加属性按钮之前就有了,因此我们把它放到添加属性按钮的回调身上 注意:每一次点击的时候,先清空一下数据再收集数据。防止下次点击时会显示上次的数据
//添加属性按钮的回调 const addAttr = () => { //每一次点击的时候,先清空一下数据再收集数据 Object.assign(attrParams, { attrName: '', //新增的属性的名字 attrValueList: [ //新增的属性值数组 ], categoryId: categoryStore.c3Id, //三级分类的ID categoryLevel: 3, //代表的是三级分类 }) //切换为添加与修改属性的结构 scene.value = 1 }
//保存按钮的回调 const save = async () => { //发请求 let result: any = await reqAddOrUpdateAttr(attrParams) //添加属性|修改已有的属性已经成功 if (result.code == 200) { //切换场景 scene.value = 0 //提示信息 ElMessage({ type: 'success', message: attrParams.id ? '修改成功' : '添加成功', }) //获取全部已有的属性与属性值(更新页面) getAttr() } else { ElMessage({ type: 'error', message: attrParams.id ? '修改失败' : '添加失败', }) } }
在input下面添加了一个div,使用flag来决定哪个展示。 注意:flag放在哪?由于每一个属性值对象都需要一个flag属性,因此将flag的添加放在添加属性值的按钮的回调上。(注意修改属性值的type)
//添加属性值按钮的回调 const addAttrValue = () => { //点击添加属性值按钮的时候,向数组添加一个属性值对象 attrParams.attrValueList.push({ valueName: '', flag: true, //控制每一个属性值编辑模式与切换模式的切换 }) }
src\api\product\attr\type.ts
//属性值表单元素失却焦点事件回调 const toLook = (row: AttrValue, $index: number) => { 。。。。。。 //相应的属性值对象flag:变为false,展示div row.flag = false } //属性值div点击事件 const toEdit = (row: AttrValue, $index: number) => { //相应的属性值对象flag:变为true,展示input row.flag = true 。。。。。。 }
//属性值表单元素失却焦点事件回调 const toLook = (row: AttrValue, $index: number) => { //非法情况判断1 if (row.valueName.trim() == '') { //删除调用对应属性值为空的元素 attrParams.attrValueList.splice($index, 1) //提示信息 ElMessage({ type: 'error', message: '属性值不能为空', }) return } //非法情况2 let repeat = attrParams.attrValueList.find((item) => { //切记把当前失却焦点属性值对象从当前数组扣除判断 if (item != row) { return item.valueName === row.valueName } }) if (repeat) { //将重复的属性值从数组当中干掉 attrParams.attrValueList.splice($index, 1) //提示信息 ElMessage({ type: 'error', message: '属性值不能重复', }) return } //相应的属性值对象flag:变为false,展示div row.flag = false }
表单聚焦可以直接调用input提供foces方法:当选择器的输入框获得焦点时触发
使用ref的函数形式,每有一个input就将其存入inputArr中
//准备一个数组:将来存储对应的组件实例el-input let inputArr = ref([])
注意:使用nextTick是因为点击后,组件需要加载,没办法第一时间拿到组件实例。所以使用nextTick会等到组件加载完毕后才调用,达到聚焦效果。
//属性值div点击事件 const toEdit = (row: AttrValue, $index: number) => { //相应的属性值对象flag:变为true,展示input row.flag = true //nextTick:响应式数据发生变化,获取更新的DOM(组件实例) nextTick(() => { inputArr.value[$index].focus() }) }
//添加属性值按钮的回调 const addAttrValue = () => { //点击添加属性值按钮的时候,向数组添加一个属性值对象 attrParams.attrValueList.push({ valueName: '', flag: true, //控制每一个属性值编辑模式与切换模式的切换 }) //获取最后el-input组件聚焦 nextTick(() => { inputArr.value[attrParams.attrValueList.length - 1].focus() }) }
修改业务很简单:当我们点击修改按钮的时候,将修改的实例(row)传递给回调函数。回调函数:首先跳转到第二页面,第二页面是根据attrParams值生成的,我们跳转的时候将实例的值传递给attrParams
//table表格修改已有属性按钮的回调 const updateAttr = (row: Attr) => { //切换为添加与修改属性的结构 scene.value = 1 //将已有的属性对象赋值给attrParams对象即为 //ES6->Object.assign进行对象的合并 Object.assign(attrParams, JSON.parse(JSON.stringify(row))) }
深拷贝和浅拷贝的区别1.浅拷贝: 将原对象或原数组的引用直接赋给新对象,新数组,新对象/数组只是原对象的一个引用2.深拷贝: 创建一个新的对象和数组,将原对象的各项属性的“值”(数组的所有元素)拷贝过来,是“值”而不是“引用”
这里存在一个问题,也就是当我们修改属性值后,并没有保存(发请求),但是界面还是改了。这是因为我们的赋值语句:Object.assign(attrParams, row)
是浅拷贝。相当于我们在修改服务器发回来的数据并展示在页面上。服务器内部并没有修改。 解决:将浅拷贝改为深拷贝:Object.assign(attrParams, JSON.parse(JSON.stringify(row)))
//这里书写属性相关的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(API.DELETEATTR_URL + attrId)
//删除某一个已有的属性方法回调 const deleteAttr = async (attrId: number) => { //发相应的删除已有的属性的请求 let result: any = await reqRemoveAttr(attrId) //删除成功 if (result.code == 200) { ElMessage({ type: 'success', message: '删除成功', }) //获取一次已有的属性与属性值 getAttr() } else { ElMessage({ type: 'error', message: '删除失败', }) } }
//路由组件销毁的时候,把仓库分类相关的数据清空 onBeforeUnmount(() => { //清空仓库的数据 categoryStore.$reset() })
SPU(Standard Product Unit):标准化产品单元。是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。通俗点讲,属性值、特性相同的商品就可以称为一个SPU。
添加SPU , sizes,total" :total="400" />
//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( API.HASSPU_URL + `${page}/${limit}?category3Id=${category3Id}`, ) }
//服务器全部接口返回的数据类型 export interface ResponseData { code: number message: string ok: boolean } //SPU数据的ts类型:需要修改 export interface SpuData { category3Id: string | number id?: number spuName: string tmId: number | string description: string spuImageList: null spuSaleAttrList: null } //数组:元素都是已有SPU数据类型 export type Records = SpuData[] //定义获取已有的SPU接口返回的数据ts类型 export interface HasSpuResponseData extends ResponseData { data: { records: Records total: number size: number current: number searchCount: boolean pages: number } }
注意getHasSpu函数携带的参数。默认为1
, sizes,total" :total="total" @current-change="getHasSpu" @size-change="changeSize" />
//此方法执行:可以获取某一个三级分类下全部的已有的SPU const getHasSpu = async (pager = 1) => { //修改当前页码 pageNo.value = pager let result: HasSpuResponseData = await reqHasSpu( pageNo.value, pageSize.value, categoryStore.c3Id, ) if (result.code == 200) { records.value = result.data.records total.value = result.data.total } } //分页器下拉菜单发生变化的时候触发 const changeSize = () => { getHasSpu() }
//监听三级分类ID变化 watch( () => categoryStore.c3Id, () => { //当三级分类发生变化的时候清空对应的数据 records.value = [] //务必保证有三级分类ID if (!categoryStore.c3Id) return getHasSpu() }, )
由于SPU模块需要在三个场景进行切换,全都放在一个组件里面的话会显得很臃肿。因此我们将它放到三个组件当中。 使用v-show来展示页面:v-if是销毁组件,v-show是隐藏组件。在初加载的时候v-if比较快,但是在频繁切换的时候v-if任务重。
添加属性 保存 取消
这两个按钮都是跳转到场景一.下面是对应的回调
//添加新的SPU按钮的回调 const addSpu = () => { //切换为场景1:添加与修改已有SPU结构->SpuForm scene.value = 1 } //修改已有的SPU的按钮的回调 const updateSpu = () => { //切换为场景1:添加与修改已有SPU结构->SpuForm scene.value = 1 }
需要改变的是父组件中的scene,因此涉及到父子组件通信。这里使用自定义事件。 父组件: 子组件:
//取消按钮的回调 const cancel = () => { $emit('changeScene', 0) }
修改和添加的页面是差不多的。页面1的四个地方都需要发请求拿数据,我们在这一部分分别编写4个部分的API以及ts类型
//获取全部品牌的数据 ALLTRADEMARK_URL = '/admin/product/baseTrademark/getTrademarkList', //获取全部的SPU的品牌的数据 export const reqAllTradeMark = () => { return request.get(API.ALLTRADEMARK_URL) }
//品牌数据的TS类型 export interface Trademark { id: number tmName: string logoUrl: string } //品牌接口返回的数据ts类型 export interface AllTradeMark extends ResponseData { data: Trademark[] }
//获取某个SPU下的全部的售卖商品的图片数据 IMAGE_URL = '/admin/product/spuImageList/', //获取某一个已有的SPU下全部商品的图片地址 export const reqSpuImageList = (spuId: number) => { return request.get(API.IMAGE_URL + spuId) }
//商品图片的ts类型 export interface SpuImg { id?: number imgName?: string imgUrl?: string createTime?: string updateTime?: string spuId?: number name?: string url?: string } //已有的SPU的照片墙数据的类型 export interface SpuHasImg extends ResponseData { data: SpuImg[] }
//获取整个项目全部的销售属性[颜色、版本、尺码] ALLSALEATTR_URL = '/admin/product/baseSaleAttrList', //获取全部的销售属性 export const reqAllSaleAttr = () => { return request.get(API.ALLSALEATTR_URL) }
//已有的全部SPU的返回数据ts类型 export interface HasSaleAttr { id: number name: string } export interface HasSaleAttrResponseData extends ResponseData { data: HasSaleAttr[] }
//获取某一个SPU下全部的已有的销售属性接口地址 SPUHASSALEATTR_URL = '/admin/product/spuSaleAttrList/', //获取某一个已有的SPU拥有多少个销售属性 export const reqSpuHasSaleAttr = (spuId: number) => { return request.get(API.SPUHASSALEATTR_URL + spuId) }
//销售属性对象ts类型 export interface SaleAttr { id?: number createTime?: null updateTime?: null spuId?: number baseSaleAttrId: number | string saleAttrName: string spuSaleAttrValueList: SpuSaleAttrValueList flag?: boolean saleAttrValue?: string } //SPU已有的销售属性接口返回数据ts类型 export interface SaleAttrResponseData extends ResponseData { data: SaleAttr[] }
首先:SPU的数据应该分为5部分:第一部分:是父组件里的展示的数据,也是我们点击修改按钮时的那个数据。其余4个部分的数据需要我们发请求得到。 问题1:子组件需要用到父组件中的数据,应该怎么办?答:要传递的数据是指定的,也就是我们点击修改时的数据。通过ref的方式,拿到子组件时的实例,再调用子组件暴露的方法将数据做为参数传递过去。(有点类似于反向的自定义事件) 问题2:其余4个部分的数据什么时候获取。答:同样的在点击修改按钮时获取,问题一中通过调用子组件的函数传递数据,我们同时也在这个函数中发请求得到数据
//修改已有的SPU的按钮的回调 const updateSpu = (row: SpuData) => { //切换为场景1:添加与修改已有SPU结构->SpuForm scene.value = 1 //调用子组件实例方法获取完整已有的SPU的数据 spu.value.initHasSpuData(row) }
子组件中直接发起请求,并且将服务器返回的四个数据存储,加上参数传递的第一部分数据,这样子组件拿到了全部的数据。
//子组件书写一个方法 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 }
/追加一个新的SPU ADDSPU_URL = '/admin/product/saveSpuInfo', //更新已有的SPU UPDATESPU_URL = '/admin/product/updateSpuInfo', //添加一个新的SPU的 //更新已有的SPU接口 //data:即为新增的SPU|或者已有的SPU对象 export const reqAddOrUpdateSpu = (data: any) => { //如果SPU对象拥有ID,更新已有的SPU if (data.id) { return request.post(API.UPDATESPU_URL, data) } else { return request.post(API.ADDSPU_URL, data) } }
//SPU数据的ts类型:需要修改 export interface SpuData { category3Id: string | number id?: number spuName: string tmId: number | string description: string spuImageList: null | SpuImg[] spuSaleAttrList: null | SaleAttr[] }
//存储已有的SPU对象 let SpuParams = ref({ category3Id: '', //收集三级分类的ID spuName: '', //SPU的名字 description: '', //SPU的描述 tmId: '', //品牌的ID spuImageList: [], spuSaleAttrList: [], }) //子组件书写一个方法 const initHasSpuData = async (spu: SpuData) => { //存储已有的SPU对象,将来在模板中展示 SpuParams.value = spu 。。。。。。 }
注意:下方的红框展示的是所有品牌,上方的绑定的是一个数字也就是下方的第几个
照片墙部分我们使用了element-plus的el-upload组件。下面详细介绍组件的功能及作用
上面el-upload是上传照片的照片墙,下面是查看照片的对话框
//商品图片 let imgList = ref([]) //子组件书写一个方法 const initHasSpuData = async (spu: SpuData) => { 。。。。。。 //获取某一个品牌旗下全部售卖商品的图片 let result1: SpuHasImg = await reqSpuImageList(spu.id as number) ...... //SPU对应商品图片 imgList.value = result1.data.map((item) => { return { name: item.imgName, url: item.imgUrl, } }) ...... }
这部分是一个双向绑定的数据,我们从服务器得到数据会展示到照片墙上。得到数据的过程我们使用了数组的map方法,这是因为组件对于数据的格式有要求。
action是指图片上传的地址。组件还会将返回的数据放到对应的img的数据中
预览的钩子,预览照片时会触发。会注入对应图片的数据。
//控制对话框的显示与隐藏 let dialogVisible = ref(false) //存储预览图片地址 let dialogImageUrl = ref('') //照片墙点击预览按钮的时候触发的钩子 const handlePictureCardPreview = (file: any) => { dialogImageUrl.value = file.url //对话框弹出来 dialogVisible.value = true }
移除图片前的钩子
上传前的钩子,我们用来对数据做预处理
//照片钱上传成功之前的钩子约束文件的大小与类型 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 } }
数据结构如下:
其实就是4列,对应好每一列以及对应的数据就好
{{ item.saleAttrValueName }}
//计算出当前SPU还未拥有的销售属性 let unSelectSaleAttr = computed(() => { //全部销售属性:颜色、版本、尺码 //已有的销售属性:颜色、版本 let unSelectArr = allSaleAttr.value.filter((item) => { return saleAttr.value.every((item1) => { return item.name != item1.saleAttrName }) }) return unSelectArr })
//添加销售属性的方法 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 = '' }
其实销售属性值和之前的添加属性业务差不多。最重要的是熟悉数据的结构。步骤分为:组件收集数据->回调中将数据整理后push到对应的数组中。
通过flag属性。一上来是没有的,点击按钮添加。输入框输入完毕blur时再将flag变为false
//属性值按钮的点击事件 const toEdit = (row: SaleAttr) => { //点击按钮的时候,input组件不就不出来->编辑模式 row.flag = true row.saleAttrValue = '' }
收集的数据有俩个 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 }
整理数据+发送请求+通知父组件更新页面
//保存按钮的回调 const save = async () => { //整理参数 //发请求:添加SPU|更新已有的SPU //成功 //失败 //1:照片墙的数据 SpuParams.value.spuImageList = imgList.value.map((item: any) => { return { imgName: item.name, //图片的名字 imgUrl: (item.response && item.response.data) || item.url, } }) //2:整理销售属性的数据 SpuParams.value.spuSaleAttrList = saleAttr.value let result = await reqAddOrUpdateSpu(SpuParams.value) if (result.code == 200) { ElMessage({ type: 'success', message: SpuParams.value.id ? '更新成功' : '添加成功', }) //通知父组件切换场景为0 $emit('changeScene', { flag: 0, params: SpuParams.value.id ? 'update' : 'add', }) } else { ElMessage({ type: 'success', message: SpuParams.value.id ? '更新成功' : '添加成功', }) } }
添加spu业务我们要做什么?收集数据(发请求得到的、自己添加的)放到对应的数据(存储数据用的容器)中,发起请求(保存按钮已经做完了),更新页面
添加和修改按钮不同的地方在于对于数据的来源不同,修改按钮是一部分(spuParams)来源于父组件传递的数据,将他们与组件绑定,在数据上展示。添加按钮父组件只需要传递category3Id就行,其他的自己收集。
//添加新的SPU按钮的回调 const addSpu = () => { //切换为场景1:添加与修改已有SPU结构->SpuForm scene.value = 1 //点击添加SPU按钮,调用子组件的方法初始化数据 spu.value.initAddSpu(categoryStore.c3Id) }
注意要对外暴露,让父组件可以使用
//添加一个新的SPU初始化请求方法 const initAddSpu = async (c3Id: number | string) => { //存储三级分类的ID SpuParams.value.category3Id = c3Id //获取全部品牌的数据 let result: AllTradeMark = await reqAllTradeMark() let result1: HasSaleAttrResponseData = await reqAllSaleAttr() //存储数据 MYAllTradeMark.value = result.data allSaleAttr.value = result1.data } //对外暴露 defineExpose({ initHasSpuData, initAddSpu })
这部分通过保存按钮的回调已经做完了。
我们应该在每次添加spu前清空上次的数据。
//添加一个新的SPU初始化请求方法 const initAddSpu = async (c3Id: number | string) => { //清空数据 Object.assign(SpuParams.value, { category3Id: '', //收集三级分类的ID spuName: '', //SPU的名字 description: '', //SPU的描述 tmId: '', //品牌的ID spuImageList: [], spuSaleAttrList: [], }) //清空照片 imgList.value = [] //清空销售属性 saleAttr.value = [] saleAttrIdAndValueName.value = '' 、、、、、、 }
在添加和修改spu属性后,跳转的页面不一样。修改应该跳转到当前页面,添加应该跳转到第一页。如何区分?SpuParams.value.id属性修改按钮的SpuParams是自带这个属性的,而添加按钮没有这个属性。因此在保存的时候通过这个属性告知父组件。 子组件:
//保存按钮的回调 const save = async () => { 。。。。。。。 //通知父组件切换场景为0 $emit('changeScene', { flag: 0, params: SpuParams.value.id ? 'update' : 'add', }) 。。。。。。 }
父组件:
//子组件SpuForm绑定自定义事件:目前是让子组件通知父组件切换场景为0 const changeScene = (obj: any) => { //子组件Spuform点击取消变为场景0:展示已有的SPU scene.value = obj.flag if (obj.params == 'update') { //更新留在当前页 getHasSpu(pageNo.value) } else { //添加留在第一页 getHasSpu() } }
//添加SKU按钮的回调 const addSku = (row: SpuData) => { //点击添加SKU按钮切换场景为2 scene.value = 2 }
保存 取消
//自定义事件的方法 let $emit = defineEmits(['changeScene']) //取消按钮的回调 const cancel = () => { $emit('changeScene', { flag: 0, params: '' }) }
父组件
//添加SKU按钮的回调 const addSku = (row: SpuData) => { //点击添加SKU按钮切换场景为2 scene.value = 2 //调用子组件的方法初始化添加SKU的数据 sku.value.initSkuData(categoryStore.c1Id, categoryStore.c2Id,row) }
子组件暴露:
//对外暴露方法 defineExpose({ initSkuData })
//当前子组件的方法对外暴露 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 }
设置默认
使用skuParams将sku模块的所有数据全都存储下来
API:
//追加一个新增的SKU地址 ADDSKU_URL = '/admin/product/saveSkuInfo', } //添加SKU的请求方法 export const reqAddSku = (data: SkuData) => { request.post(API.ADDSKU_URL, data) }
ts:
export interface Attr { attrId: number | string //平台属性的ID valueId: number | string //属性值的ID } export interface saleArr { saleAttrId: number | string //属性ID saleAttrValueId: number | string //属性值的ID } export interface SkuData { category3Id: string | number //三级分类的ID spuId: string | number //已有的SPU的ID tmId: string | number //SPU品牌的ID skuName: string //sku名字 price: string | number //sku价格 weight: string | number //sku重量 skuDesc: string //sku的描述 skuAttrValueList?: Attr[] skuSaleAttrValueList?: saleArr[] skuDefaultImg: string //sku图片地址 }
这部分数据包括三级id,spuid还有品牌id。由于是父组件传递过来的,我们可以直接在添加按钮调用的那个函数中收集
//当前子组件的方法对外暴露 const initSkuData = async ( c1Id: number | string, c2Id: number | string, spu: any, ) => { //收集数据 skuParams.category3Id = spu.category3Id skuParams.spuId = spu.id skuParams.tmId = spu.tmId 。。。。。。 }
sku名称、价格、重量、sku描述都是收集的用户输入的数据。我们直接使用v-model
我们在数据绑定的时候将这俩个属性所选择的数据绑定到自身。之后整合数据的时候通过遍历得到
//设置默认图片的方法回调 const handler = (row: any) => { //点击的时候,全部图片的的复选框不勾选 imgArr.value.forEach((item: any) => { table.value.toggleRowSelection(item, false) }) //选中的图片才勾选 table.value.toggleRowSelection(row, true) //收集图片地址 skuParams.skuDefaultImg = row.imgUrl }
//收集SKU的参数 let skuParams = reactive({ //父组件传递过来的数据 category3Id: '', //三级分类的ID spuId: '', //已有的SPU的ID tmId: '', //SPU品牌的ID //v-model收集 skuName: '', //sku名字 price: '', //sku价格 weight: '', //sku重量 skuDesc: '', //sku的描述 skuAttrValueList: [ //平台属性的收集 ], skuSaleAttrValueList: [ //销售属性 ], skuDefaultImg: '', //sku图片地址 })
//保存按钮的方法 const save = async () => { //整理参数 //平台属性 skuParams.skuAttrValueList = attrArr.value.reduce((prev: any, next: any) => { if (next.attrIdAndValueId) { let [attrId, valueId] = next.attrIdAndValueId.split(':') prev.push({ attrId, valueId, }) } return prev }, []) //销售属性 skuParams.skuSaleAttrValueList = saleArr.value.reduce( (prev: any, next: any) => { if (next.saleIdAndValueId) { let [saleAttrId, saleAttrValueId] = next.saleIdAndValueId.split(':') prev.push({ saleAttrId, saleAttrValueId, }) } return prev }, [], ) //添加SKU的请求 let result: any = await reqAddSku(skuParams) if (result.code == 200) { ElMessage({ type: 'success', message: '添加SKU成功', }) //通知父组件切换场景为零 $emit('changeScene', { flag: 0, params: '' }) } else { ElMessage({ type: 'error', message: '添加SKU失败', }) } }
bug1:在发送请求的时候返回时undefined:注意;这种情况一般是由于API的请求函数没有写返回值(格式化之后) bug2:平台属性和销售属性收集不到。可能时element-plus自带的table校验。前面数据填的格式不对(比如重量和价格input确定是数字但是可以输入字母e,这时候会导致错误)或者没有填写会导致后面的数据出问题。
API:
//查看某一个已有的SPU下全部售卖的商品 SKUINFO_URL = '/admin/product/findBySpuId/', //获取SKU数据 export const reqSkuList = (spuId: number | string) => { return request.get(API.SKUINFO_URL + spuId) }
TYPE
//获取SKU数据接口的ts类型 export interface SkuInfoData extends ResponseData { data: SkuData[] }
//存储全部的SKU数据 let skuArr = ref([]) let show = ref(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 } }
其实就是弹出一个对话框dialog,然后里面是一个form
type为any,因此没有写专门的type
//删除已有的SPU REMOVESPU_URL = '/admin/product/deleteSpu/', //删除已有的SPU export const reqRemoveSpu = (spuId: number | string) => { return request.delete(API.REMOVESPU_URL + spuId) }
//删除已有的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: '删除失败', }) } }
//路由组件销毁前,清空仓库关于分类的数据 onBeforeUnmount(() => { categoryStore.$reset() })
,sizes,total " :total="400" />
API:
//SKU模块接口管理 import request from '@/utils/request' import type { SkuResponseData} from './type' //枚举地址 enum API { //获取已有的商品的数据-SKU SKU_URL = '/admin/product/list/', } //获取商品SKU的接口 export const reqSkuList = (page: number, limit: number) => { return request.get(API.SKU_URL + `${page}/${limit}`) }
type:
export interface ResponseData { code: number message: string ok: boolean } //定义SKU对象的ts类型 export interface Attr { id?: number attrId: number | string //平台属性的ID valueId: number | string //属性值的ID } export interface saleArr { id?: number saleAttrId: number | string //属性ID saleAttrValueId: number | string //属性值的ID } export interface SkuData { category3Id?: string | number //三级分类的ID spuId?: string | number //已有的SPU的ID tmId?: string | number //SPU品牌的ID skuName?: string //sku名字 price?: string | number //sku价格 weight?: string | number //sku重量 skuDesc?: string //sku的描述 skuAttrValueList?: Attr[] skuSaleAttrValueList?: saleArr[] skuDefaultImg?: string //sku图片地址 isSale?: number //控制商品的上架与下架 id?: number } //获取SKU接口返回的数据ts类型 export interface SkuResponseData extends ResponseData { data: { records: SkuData[] total: number size: number current: number orders: [] optimizeCountSql: boolean hitCount: boolean countId: null maxLimit: null searchCount: boolean pages: number } }
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(1) //每一页展示几条数据 let pageSize = ref(10) let total = ref(0) let skuArr = ref([]) //组件挂载完毕 onMounted(() => { getHasSku() }) const getHasSku = async (pager = 1) => { //当前分页器的页码 pageNo.value = pager let result: SkuResponseData = await reqSkuList(pageNo.value, pageSize.value) if (result.code == 200) { total.value = result.data.total skuArr.value = result.data.records } }
//分页器下拉菜单发生变化触发 const handler = () => { getHasSku() }
注意:在这里切换页码和切换每页数据条数的回调不同是因为:它们都能对函数注入数据,切换页码注入的是点击的页码数,因此我们可以直接使用getHasSku作为他的回调。切换每页数据条数注入的是切换的页码条数,我们希望切换后跳转到第一页,因此使用handler,间接调用getHasSku。
//上架 SALE_URL = '/admin/product/onSale/', //下架的接口 CANCELSALE_URL = '/admin/product/cancelSale/', //已有商品上架的请求 export const reqSaleSku = (skuId: number) => { return request.get(API.SALE_URL + skuId) } //下架的请求 export const reqCancelSale = (skuId: number) => { return request.get(API.CANCELSALE_URL + skuId) }
type都是any
根据数据切换
流程:发请求->更新页面
//商品的上架与下架的操作 const updateSale = async (row: SkuData) => { //如果当前商品的isSale==1,说明当前商品是上架的额状态->更新为下架 //否则else情况与上面情况相反 if (row.isSale == 1) { //下架操作 await reqCancelSale(row.id as number) //提示信息 ElMessage({ type: 'success', message: '下架成功' }) //发请求获取当前更新完毕的全部已有的SKU getHasSku(pageNo.value) } else { //下架操作 await reqSaleSku(row.id as number) //提示信息 ElMessage({ type: 'success', message: '上架成功' }) //发请求获取当前更新完毕的全部已有的SKU getHasSku(pageNo.value) } }
更新按钮这里没有业务。个人觉得是因为SKU的编写在SPU已经做完了。防止业务逻辑混乱
//更新已有的SKU const updateSku = () => { ElMessage({ type: 'success', message: '程序员在努力的更新中....' }) }
描述:呼出一个临时的侧边栏, 可以从多个方向呼出
//控制抽屉显示与隐藏的字段 let drawer = ref(false) //查看商品详情按钮的回调 const findSku = async (row: SkuData) => { //抽屉展示出来 drawer.value = true }
通过基础的 24 分栏,迅速简便地创建布局。 、 效果图:
注意:把对应的style也复制过来
API
//获取商品详情的接口 SKUINFO_URL = '/admin/product/getSkuInfo/', //获取商品详情的接口 export const reqSkuInfo = (skuId: number) => { return request.get(API.SKUINFO_URL + skuId) }
type
//获取SKU商品详情接口的ts类型 export interface SkuInfoData extends ResponseData { data: SkuData }
let skuInfo = ref({}) //查看商品详情按钮的回调 const findSku = async (row: SkuData) => { //抽屉展示出来 drawer.value = true //获取已有商品详情数据 let result: SkuInfoData = await reqSkuInfo(row.id as number) //存储已有的SKU skuInfo.value = result.data }
注:忘记写了,后面才想起来。简短写一下思路 API->绑定点击事件->发请求 比较简单。
这模块的思路其实都比较简单。无外乎API(type),组件内发请求拿数据、将数据放到模板中。再加上一个对仓库的处理。 这部分真正的难点也是最值得学习的点在于 1:type的书写 2:对数据结构的理解(可以将请求回来的数据放到正确的位置上) 3:element-plus组件的使用。 其实现在看来这部分模块做的事情就是我们前端人的一些缩影。思路不难,难在琐碎的工作中要处理的各种各样的东西。
主要是el-form、el-pagination
搜索 重置 添加用户 批量删除 ,sizes,total" :total="400" />
//用户管理模块的接口 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( API.ALLUSER_URL + `${page}/${limit}`, ) }
//账号信息的ts类型 export interface ResponseData { code: number message: string ok: boolean } //代表一个账号信息的ts类型 export interface User { id?: number createTime?: string updateTime?: string username?: string password?: string name?: string phone?: null roleName?: string } //数组包含全部的用户信息 export type Records = User[] //获取全部用户信息接口返回的数据ts类型 export interface UserResponseData extends ResponseData { data: { records: Records total: number size: number current: number pages: number } }
//用户总个数 let total = ref(0) //存储全部用户的数组 let userArr = ref([]) onMounted(() => { getHasUser() }) //获取全部已有的用户信息 const getHasUser = async (pager = 1) => { //收集当前页码 pageNo.value = pager let result: UserResponseData = await reqUserInfo( pageNo.value, pageSize.value, /* keyword.value, */ ) if (result.code == 200) { total.value = result.data.total userArr.value = result.data.records } }
//分页器下拉菜单的自定义事件的回调 const handler = () => { getHasUser() }
添加用户
取消 确定
注意绑定的是添加用户以及修改用户的回调
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(API.UPDATEUSER_URL, data) } else { return request.post(API.ADDUSER_URL, data) } }
type
//代表一个账号信息的ts类型 export interface User { id?: number createTime?: string updateTime?: string username?: string password?: string name?: string phone?: null roleName?: string }
//收集用户信息的响应式数据 let userParams = reactive({ username: '', name: '', password: '', })
//保存按钮的回调 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 ? '更新失败' : '添加失败', }) } }
添加用户按钮:我们在点击添加用户按钮的时候,先把之前的用户数据清空
//添加用户按钮的回调 const addUser = () => { //抽屉显示出来 drawer.value = true //清空数据 Object.assign(userParams, { id: 0, username: '', name: '', password: '', }) }
取消按钮: 点击取消按钮之后:关闭抽屉
//取消按钮的回调 const cancel = () => { //关闭抽屉 drawer.value = false }
注意点:注意表单FORM与表格Table的区别。 主要还是收集与展示数据的区别。 表单绑定的:model="userParams"是数据,prop="username"是属性,绑定是为了对表单进行验证。 表格绑定的data是要显示的数据,item项的prop也是要展示的数据。
//校验用户名字回调函数 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 }], }
先获取form组件的实例,在调用form组件的方法validate()
//获取form组件实例 let formRef = ref()
//保存按钮的回调 const save = async () => { //点击保存按钮的时候,务必需要保证表单全部复合条件在去发请求 await formRef.value.validate() 。。。。。。 }
使用nextTick是因为第一次的时候还没有formRef实例。
//添加用户按钮的回调 const addUser = () => { 。。。。。。 //清除上一次的错误的提示信息 nextTick(() => { formRef.value.clearValidate('username') formRef.value.clearValidate('name') formRef.value.clearValidate('password') }) }
标题应该该为更新用户,没有输入密码。因为修改业务时我们需要用到用户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') }) }
//更新已有的用户按钮的回调 //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() } 。。。。。。。 }
首先,当你更改当前账号再刷新的时候,浏览器还是会往当前页面跳转 这时候路由前置守卫就会发生作用: 你会发现,此时你的token存储在本地存储里面,所以是有的,username存储在仓库里面,所以刷新就没了。这也是之前说的仓库存储的问题。此时你的路由守卫就会走到下面这部分 它会向仓库发起获取用户信息的请求,获取成功后就放行了。 问题来了!!!为什么修改当前账户之后就会跳转到登陆页面呢? 首先我们创建一个用户 登陆后再修改: 跳转到了login界面 此时来看一下仓库:token和username都没了。这是为什么呢? 因此我们回过头来看一下路由守卫,可以看出走到了下面的位置,清除了用户相关的数据清空。也就是说: 结论:当我们修改了账户在刷新之后,我们再路由守卫里调用** await userStore.userInfo()**
语句会失败(服务器端会阻止),因此我们走到了**next({ path: '/login', query: { redirect: to.path } })**
这里,跳转到了login页面。
补充:证明一下我们修改了账户之后服务器会阻止我们登录。 此时修改一下路由守卫(做个标记) 刷新一下,证明路由确实是从这走的 此时在修改路由守卫以及用户信息方法 修改完之后再发请求: 此时可以得出结论,在修改用户信息之后,向服务器发起userInfo()请求确实会失败,导致我们跳转到login界面
全选 {{ index }}
//获取全部职位以及包含当前用户的已有的职位 export const reqAllRole = (userId: number) => { return request.get(API.ALLROLEURL + userId) }
//代表一个职位的ts类型 export interface RoleData { id?: number createTime?: string updateTime?: string roleName: string remark: null } //全部职位的列表 export type AllRole = RoleData[] //获取全部职位的接口返回的数据ts类型 export interface AllRoleResponseData extends ResponseData { data: { assignRoles: AllRole allRolesList: AllRole } }
//收集顶部复选框全选数据 let checkAll = ref(false) //控制顶部全选复选框不确定的样式 let isIndeterminate = ref(true) //存储全部职位的数据 let allRole = ref([]) //当前用户已有的职位 let userRole = ref([]) //分配角色按钮的回调 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 } }
分配角色(职位)
全选 {{ role.roleName }}
详细解释: 全选部分: @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 }
//分配职位 export const reqSetUserRole = (data: SetRoleData) => { return request.post(API.SETROLE_URL, data) }
//给用户分配职位接口携带参数的ts类型 export interface SetRoleData { roleIdList: number[] userId: number }
回调绑在确认按钮身上就可以了
//确定按钮的回调(分配职位) const confirmClick = async () => { //收集参数 let data: SetRoleData = { userId: userParams.id as number, roleIdList: userRole.value.map((item) => { return item.id as number }), } //分配用户的职位 let result: any = await reqSetUserRole(data) if (result.code == 200) { //提示信息 ElMessage({ type: 'success', message: '分配职务成功' }) //关闭抽屉 drawer1.value = false //获取更新完毕用户的信息,更新完毕留在当前页 getHasUser(pageNo.value) } }
//删除某一个账号 DELETEUSER_URL = '/admin/acl/user/remove/', //批量删除的接口 DELETEALLUSER_URL = '/admin/acl/user/batchRemove', //删除某一个账号的信息 export const reqRemoveUser = (userId: number) => { return request.delete(API.DELETEUSER_URL + userId) } //批量删除的接口 export const reqSelectUser = (idList: number[]) => { return request.delete(API.DELETEALLUSER_URL, { data: idList }) }
//删除某一个用户 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) } }
//table复选框勾选的时候会触发的事件 const selectChange = (value: any) => { selectIdArr.value = value }
//批量删除按钮的回调 const deleteSelectUser = async () => { //整理批量删除的参数 let idsList: any = selectIdArr.value.map((item) => { return item.id }) //批量删除的请求 let result: any = await reqSelectUser(idsList) if (result.code == 200) { ElMessage({ type: 'success', message: '删除成功' }) getHasUser(userArr.value.length > 1 ? pageNo.value : pageNo.value - 1) } }
个人觉得这里的批量删除有个小bug,假设所有数据都可以删除的话,那么把最后一页的数据都删除掉,会使得页面跳转到当前页而不是前一页。在这里因为admin不可删除,如果以后遇到这样的问题的时候要注意!。
搜索业务与获取初始数据的请求是同一个,因此我们修改一下获取初始业务的请求。更具是否写道username来判断。
//获取用户账号信息的接口 export const reqUserInfo = (page: number, limit: number, username: string) => { return request.get( API.ALLUSER_URL + `${page}/${limit}/?username=${username}`, ) }
收集数据: 发送请求
//搜索按钮的回调 const search = () => { //根据关键字获取相应的用户数据 getHasUser() //清空关键字 keyword.value = '' }
重置业务是通过调用setting仓库实现的
import useLayOutSettingStore from '@/store/modules/setting' //获取模板setting仓库 let settingStore = useLayOutSettingStore() //重置按钮 const reset = () => { settingStore.refresh = !settingStore.refresh }
具体的功能实现是在之前写好的main组件里实现的,通过监听销毁重建组件。
//监听仓库内部的数据是否发生变化,如果发生变化,说明用户点击过刷新按钮 watch( () => layOutSettingStore.refresh, () => { //点击刷新按钮:路由组件销毁 flag.value = false nextTick(() => { flag.value = true }) }, )
还是熟悉的组件:el-card、el-table 、el-pagination、el-form
搜索 重置 添加职位 分配权限 编辑 删除 ,sizes,total" :total="400" @current-change="getHasRole" @size-change="sizeChange" />
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( API.ALLROLE_URL + `${page}/${limit}/?roleName=${roleName}`, ) }
type:
export interface ResponseData { code: number message: string ok: boolean } //职位数据类型 export interface RoleData { id?: number createTime?: string updateTime?: string roleName: string remark?: null } //全部职位的数组的ts类型 export type Records = RoleData[] //全部职位数据的相应的ts类型 export interface RoleResponseData extends ResponseData { data: { records: Records total: number size: number current: number orders: [] optimizeCountSql: boolean hitCount: boolean countId: null maxLimit: null searchCount: boolean pages: number } }
//当前页码 let pageNo = ref(1) //一页展示几条数据 let pageSize = ref(10) //搜索职位关键字 let keyword = ref('') //组件挂载完毕 onMounted(() => { //获取职位请求 getHasRole() }) //获取全部用户信息的方法|分页器当前页码发生变化的回调 const getHasRole = async (pager = 1) => { //修改当前页码 pageNo.value = pager let result: RoleResponseData = await reqAllRoleList( pageNo.value, pageSize.value, keyword.value, ) if (result.code == 200) { total.value = result.data.total allRole.value = result.data.records } }
分配权限 编辑 删除
同样的@current-change与@size-change函数回调。
,sizes,total" :total="total" @current-change="getHasRole" @size-change="sizeChange" />
//下拉菜单的回调 const sizeChange = () => { getHasRole() }
//搜索按钮的回调 const search = () => { //再次发请求根据关键字 getHasRole() keyword.value = '' }
重置模块我在用户管理模块仔细解释过。
import useLayOutSettingStore from '@/store/modules/setting' let settingStore = useLayOutSettingStore() //重置按钮的回调 const reset = () => { settingStore.refresh = !settingStore.refresh }
取消 确定
//新增岗位的接口地址 ADDROLE_URL = '/admin/acl/role/save', //更新已有的职位 UPDATEROLE_URL = '/admin/acl/role/update', //添加职位与更新已有职位接口 export const reqAddOrUpdateRole = (data: RoleData) => { if (data.id) { return request.put(API.UPDATEROLE_URL, data) } else { return request.post(API.ADDROLE_URL, data) } }
//添加职位按钮的回调 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') }) }
: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 }], }
//确定按钮的回调 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) } }
//获取全部的菜单与按钮的数据 ALLPERMISSTION = '/admin/acl/permission/toAssign/', //获取全部菜单与按钮权限数据 export const reqAllMenuList = (roleId: number) => { return request.get(API.ALLPERMISSTION + roleId) }
注意:type这里MenuData与MenuList互相调用,适合这种树状的数据结构
//菜单与按钮数据的ts类型 export interface MenuData { id: number createTime: string updateTime: string pid: number name: string code: string toCode: string type: number status: null level: number children?: MenuList select: boolean } export type MenuList = MenuData[]
分配权限按钮: 获取&&存储数据
//准备一个数组:数组用于存储勾选的节点的ID(四级的) let selectArr = ref([]) //已有的职位的数据 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, []) } }
我们重点关注el-tree组件 data:展示的数据 show-checkbox:节点是否可被选择 node-key:每个树节点用来作为唯一标识的属性,整棵树应该是唯一的 default-expand-all:默认展开所有节点 default-checked-keys:默认勾选的节点的 key 的数组 props:属性: label:指定节点标签为节点对象的某个属性值 children:指定子树为节点对象的某个属性值
const defaultProps = { //子树为节点对象的children children: 'children', //节点标签为节点对象的name属性 label: 'name', }
获取已分配权限的id,这里我们只需要收集最后一层的id即可,因为组件会自动更具最后一层的选择情况决定上层的选择状况。 注意:获取最后一层id的函数filterSelectArr使用了递归。
//分配权限按钮的回调 //已有的职位的数据 const setPermisstion = async (row: RoleData) => { //抽屉显示出来 drawer.value = true //收集当前要分类权限的职位的数据 Object.assign(RoleParams, row) //根据职位获取权限的数据 let result: MenuResponseData = await reqAllMenuList(RoleParams.id as number) if (result.code == 200) { menuArr.value = result.data selectArr.value = filterSelectArr(menuArr.value, []) } } const filterSelectArr = (allData: any, initArr: any) => { allData.forEach((item: any) => { if (item.select && item.level == 4) { initArr.push(item.id) } if (item.children && item.children.length > 0) { filterSelectArr(item.children, initArr) } }) return initArr }
//给相应的职位分配权限 SETPERMISTION_URL = '/admin/acl/permission/doAssign/?', //给相应的职位下发权限 export const reqSetPermisstion = (roleId: number, permissionId: number[]) => { return request.post( API.SETPERMISTION_URL + `roleId=${roleId}&permissionId=${permissionId}`, ) }
我们这里收集主要用到了2个方法:getCheckedKeys、getHalfCheckedKeys。它们会返回已选择以及半选择用户的id数组
//抽屉确定按钮的回调 const handler = async () => { //职位的ID const roleId = RoleParams.id as number //选中节点的ID let arr = tree.value.getCheckedKeys() //半选的ID let arr1 = tree.value.getHalfCheckedKeys() let permissionId = arr.concat(arr1) //下发权限 let result: any = await reqSetPermisstion(roleId, permissionId) if (result.code == 200) { //抽屉关闭 drawer.value = false //提示信息 ElMessage({ type: 'success', message: '分配权限成功' }) //页面刷新 window.location.reload() } }
API&&TYPE
//删除已有的职位 export const reqRemoveRole = (roleId: number) => { return request.delete(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) } }
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(API.ALLPERMISSTION_URL) }
TYPE: 注意:type这里使用了嵌套
//数据类型定义 export interface ResponseData { code: number message: string ok: boolean } //菜单数据与按钮数据的ts类型 export interface Permisstion { id?: number createTime: string updateTime: string pid: number name: string code: null toCode: null type: number status: null level: number children?: PermisstionList select: boolean } export type PermisstionList = Permisstion[] //菜单接口返回的数据类型 export interface PermisstionResponseData extends ResponseData { data: PermisstionList }
//存储菜单的数据 let PermisstionArr = ref([]) //组件挂载完毕 onMounted(() => { getHasPermisstion() }) //获取菜单数据的方法 const getHasPermisstion = async () => { let result: PermisstionResponseData = await reqAllPermisstion() if (result.code == 200) { PermisstionArr.value = result.data } }
{{ row.level == 3 ? '添加功能' : '添加菜单' }} 编辑 删除
API:
//给某一级菜单新增一个子菜单 ADDMENU_URL = '/admin/acl/permission/save', //更新某一个已有的菜单 UPDATE_URL = '/admin/acl/permission/update', //添加与更新菜单的方法 export const reqAddOrUpdateMenu = (data: MenuParams) => { if (data.id) { return request.put(API.UPDATE_URL, data) } else { return request.post(API.ADDMENU_URL, data) } }
取消 确定
需要的参数一共是4个,其中code、name由v-model绑定的对话框收集。其余俩个通过点击按钮传递的参数收集。
//携带的参数 let menuData = reactive({ 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) }
//确定按钮的回调 const save = async () => { //发请求:新增子菜单|更新某一个已有的菜单的数据 let result: any = await reqAddOrUpdateMenu(menuData) if (result.code == 200) { //对话框隐藏 dialogVisible.value = false //提示信息 ElMessage({ type: 'success', message: menuData.id ? '更新成功' : '添加成功', }) //再次获取全部最新的菜单的数据 getHasPermisstion() } }
//删除已有的菜单 DELETEMENU_URL = '/admin/acl/permission/remove/', //删除某一个已有的菜单 export const reqRemoveMenu = (id: number) => { return request.delete(API.DELETEMENU_URL + id) }
删除
//删除按钮回调 const removeMenu = async (id: number) => { let result = await reqRemoveMenu(id) if (result.code == 200) { ElMessage({ type: 'success', message: '删除成功' }) getHasPermisstion() } }
首页模块比较简单,代码量也少。这里直接放上源代码
{{ getTime() }}好呀{{ userStore.username }}
硅谷甄选运营平台
这里使用了el-switch组件,下面介绍一下属性 @change:点击切换时的回调 v-model:双向绑定的数据,用来控制开关的切换 class:默认的类 style:样式 active-ico、inactive-icon:开和关的图标 inline-prompt:可以把图标放在开关里面
//暗黑模式需要的样式 import 'element-plus/theme-chalk/dark/css-vars.css'
//收集开关的数据 let dark = ref(false) //switch开关的chang事件进行暗黑模式的切换 const changeDark = () => { //获取HTML根节点 let html = document.documentElement //判断HTML标签是否有类名dark dark.value ? (html.className = 'dark') : (html.className = '') }
Element Plus 默认提供一套主题,也提供了相应的修改主题颜色的方法。我们要使用的时通过js来修改主题颜色
使用了el-color-picker组件 @change:切换的回调 v-model:绑定的数据 show-alpha:是否支持透明度选择 predefine:预定义颜色(会在下面显示)
//主题颜色的设置 const setColor = () => { //通知js修改根节点的样式对象的属性与属性值 const html = document.documentElement html.style.setProperty('--el-color-primary', color.value) }
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', ])
左侧 中间 右侧
首页 智慧旅游可视化大数据平台 统计报告 当前时间:{{ time }}
pnpm i moment
import moment from 'moment' let timer = ref(0) //存储当前时间 let time = ref(moment().format('YYYY年MM月DD日 hh:mm:ss')) //组件挂载完毕更新当前的事件 onMounted(() => { timer.value = setInterval(() => { time.value = moment().format('YYYY年MM月DD日 hh:mm:ss') }, 1000) }) onBeforeUnmount(() => { clearInterval(timer.value) })
//按钮的点击回调 const goHome = () => { $router.push('/home') }
父组件中对左侧使用了垂直方向的弹性盒
.bottom { display: flex; .left { flex: 1; height: 1040px; display: flex; // 弹性方向:列方向 flex-direction: column; .tourist { flex: 1.2; } .sex { flex: 1; } .age { flex: 1; } } }
注意:在“可预约总量99999人”那里使用了float: right;
,float对上面的块级元素不会产生影响,因此不会飘上去。
实时游客统计
可预约总量 99999 人
{{ item }} 123
pnpm i echarts
pnpm i echarts-liquidfill
onMounted(() => { //获取echarts类的实例 let mycharts = echarts.init(charts.value) //设置实例的配置项 mycharts.setOption({ //标题组件 title: { text: '水球图', }, //x|y轴组件 xAxis: {}, yAxis: {}, //系列:决定你展示什么样的图形图标 series: { type: 'liquidFill', //系列 data: [0.6, 0.4, 0.2], //展示的数据 waveAnimation: true, //动画 animationDuration: 3, animationDurationUpdate: 0, radius: '90%', //半径 outline: { //外层边框设置 show: true, borderDistance: 8, itemStyle: { color: 'skyblue', borderColor: '#294D99', borderWidth: 8, shadowBlur: 20, shadowColor: 'rgba(0, 0, 0, 0.25)', }, }, }, //布局组件 grid: { left: 0, right: 0, top: 0, bottom: 0, }, }) })
男女比例
男士58%
女士42%
import { ref, onMounted } from 'vue' import * as echarts from 'echarts' //获取图形图标的DOM节点 let charts = ref() onMounted(() => { //初始化echarts实例 let mycharts = echarts.init(charts.value) //设置配置项 mycharts.setOption({ //组件标题 title: { // text: '男女比例', //主标题 textStyle: { //主标题颜色 color: 'skyblue', }, left: '40%', }, //x|y xAxis: { show: false, min: 0, max: 100, }, yAxis: { show: false, type: 'category', }, series: [ // 这里有俩个柱状图,下面的覆盖上面的 { type: 'bar', data: [58], barWidth: 20, // 柱状图的层级 z: 100, // 柱状图样式 itemStyle: { color: 'skyblue', borderRadius: 20, }, }, { type: 'bar', data: [100], //柱状图宽度 barWidth: 20, //调整女士柱条位置 barGap: '-100%', itemStyle: { color: 'pink', borderRadius: 20, }, }, ], grid: { left: 60, top: -20, right: 60, bottom: 0, }, }) })
年龄比例
我是地图组件
未来七天游客数量趋势图
热门景区排行
菜单的权限: 超级管理员账号:admin atguigu123 拥有全部的菜单、按钮的权限 飞行员账号 硅谷333 111111 不包含权限管理模块、按钮的权限并非全部按钮 同一个项目:不同人(职位是不一样的,他能访问到的菜单、按钮的权限是不一样的) 一、目前整个项目一共多少个路由!!! login(登录页面)、 404(404一级路由)、 任意路由、 首页(/home)、 数据大屏、 权限管理(三个子路由) 商品管理模块(四个子路由) 1.1开发菜单权限 ---第一步:拆分路由 静态(常量)路由:大家都可以拥有的路由 login、首页、数据大屏、404 异步路由:不同的身份有的有这个路由、有的没有 权限管理(三个子路由) 商品管理模块(四个子路由) 任意路由:任意路由 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', }, }
注意:这里使用了递归。其次,这里是浅拷贝,会改变原有的路由。因此还需要改进。
//硅谷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');
。。。。。。 import router from '@/router' //引入路由(常量路由) import { constantRoute, asnycRoute, anyRoute } from '@/router/routes' //用于过滤当前用户需要展示的异步路由 function filterAsyncRoute(asnycRoute: any, routes: any) { return asnycRoute.filter((item: any) => { if (routes.includes(item.name)) { if (item.children && item.children.length > 0) { //硅谷333账号:product\trademark\attr\sku item.children = filterAsyncRoute(item.children, routes) } return true } }) } //创建用户小仓库 const useUserStore = defineStore('User', { //小仓库存储数据地方 state: (): UserState => { return { 。。。。。。。 menuRoutes: constantRoute, //仓库存储生成菜单需要数组(路由) us。。。。。。 } }, //处理异步|逻辑地方 actions: { 。。。。。。。 //获取用户信息方法 async userInfo() { //获取用户信息进行存储 const result: userInfoResponseData = await reqUserInfo() if (result.code == 200) { this.username = result.data.name this.avatar = result.data.avatar //计算当前用户需要展示的异步路由 const userAsyncRoute = filterAsyncRoute(asnycRoute, result.data.routes) //菜单需要的数据整理完毕 this.menuRoutes = [...constantRoute, ...userAsyncRoute, anyRoute] //目前路由器管理的只有常量路由:用户计算完毕异步路由、任意路由动态追加 ;[...userAsyncRoute, anyRoute].forEach((route: any) => { router.addRoute(route) }) return 'ok' } else { return Promise.reject(new Error(result.message)) } }, 。。。。。。 }) //对外暴露小仓库 export default useUserStore
之前获取需要的路由方法中使用的是浅拷贝,会改变原有的路由。因此我们这里引入深拷贝的方法
//引入深拷贝方法 //@ts-expect-error import cloneDeep from 'lodash/cloneDeep' 。。。。。。 //获取用户信息方法 async userInfo() { //获取用户信息进行存储 const result: userInfoResponseData = await reqUserInfo() if (result.code == 200) { this.username = result.data.name this.avatar = result.data.avatar //计算当前用户需要展示的异步路由 const userAsyncRoute = filterAsyncRoute( cloneDeep(asnycRoute), result.data.routes, ) //菜单需要的数据整理完毕 this.menuRoutes = [...constantRoute, ...userAsyncRoute, anyRoute] //目前路由器管理的只有常量路由:用户计算完毕异步路由、任意路由动态追加 ;[...userAsyncRoute, anyRoute].forEach((route: any) => { router.addRoute(route) }) return 'ok' } else { return Promise.reject(new Error(result.message)) } },
这样配置路由后,如果你访问的是异步路由,会在刷新的时候出现空白页面。原因是异步路由是异步获取的,加载的时候还没有。因此我们可以在路由守卫文件中改写。这个的意思就是一直加载。
//用户登录判断 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 } }) } }
对于不同的用户,按钮的的显示与否
记得修改对应的type
//创建用户相关的小仓库 import { defineStore } from 'pinia' //引入接口 import { reqLogin, reqUserInfo, reqLogout } from '@/api/user' import type { loginFormData, loginResponseData, userInfoResponseData, } from '@/api/user/type' import type { UserState } from './types/type' import router from '@/router' 。。。。。。 //创建用户小仓库 const useUserStore = defineStore('User', { //小仓库存储数据地方 state: (): UserState => { return { token: GET_TOKEN(), //用户唯一标识token menuRoutes: constantRoute, //仓库存储生成菜单需要数组(路由) username: '', avatar: '', //存储当前用户是否包含某一个按钮 buttons: [], } }, //处理异步|逻辑地方 actions: { 。。。。。。 //获取用户信息方法 async userInfo() { //获取用户信息进行存储 const result: userInfoResponseData = await reqUserInfo() if (result.code == 200) { this.username = result.data.name this.avatar = result.data.avatar this.buttons = result.data.buttons console.log(result) //计算当前用户需要展示的异步路由 const userAsyncRoute = filterAsyncRoute( cloneDeep(asnycRoute), result.data.routes, ) //菜单需要的数据整理完毕 this.menuRoutes = [...constantRoute, ...userAsyncRoute, anyRoute] //目前路由器管理的只有常量路由:用户计算完毕异步路由、任意路由动态追加 ;[...userAsyncRoute, anyRoute].forEach((route: any) => { router.addRoute(route) }) return 'ok' } else { return Promise.reject(new Error(result.message)) } }, 。。。。。。 }) //对外暴露小仓库 export default useUserStore
这个需要你在每个按钮元素中使用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) } }, }) }
pnpm run build
注意,有些变量定义了未使用会报错。 tsconfig.json:
本文由博客一文多发平台 OpenWrite 发布!