JP4-7-MyLesson后台前端(一)

JP4-7-MyLesson后台前端(一)
Java道经 - 项目 - MyLesson - 后台前端(一)


传送门:JP4-7-MyLesson后台前端(一)
传送门:JP4-7-MyLesson后台前端(二)
传送门:JP4-7-MyLesson后台前端(三)
传送门:JP4-7-MyLesson后台前端(四)
传送门:JP4-7-MyLesson后台前端(五)

文章目录

心法:ml-web 是 MyLesson 项目后台管理的前端页面,用于给管理员提供管理界面。

项目相关功能模块

+-- @element-plus/[email protected] +-- @vitejs/[email protected] +-- @vueuse/[email protected] +-- [email protected] +-- [email protected] +-- [email protected] +-- [email protected] +-- [email protected] +-- [email protected] +-- [email protected] +-- [email protected] `-- [email protected] 

项目完整文件结构

|_ ml-web |_ node_modules # 项目依赖|_ public |_ ml-web.ico # 项目图标|_ src |_ api |_ ums/*.js # 封装用户微服务相关API请求接口|_ cms/*.js # 封装课程微服务相关API请求接口|_ sms/*.js # 封装营销微服务相关API请求接口|_ oms/*.js # 封装订单微服务相关API请求接口|_ index.js # 请求工具(封装基本 CRUD 请求和拦截器)|_ assets |_ image/*.png # 项目本地图片目录|_ components |_ MyForm.vue # 封装的EL表单组件|_ MyHead.vue # 封装的EL表头组件|_ MyIcon.vue # 封装的EL图标组件|_ MyNav.vue # 封装的EL导航组件|_ MyPlayer.vue # 封装的EL视频组件|_ MyTable.vue # 封装的EL表格组件|_ MyUpload.vue # 封装的EL上传组件|_ const/index.js # 项目常量|_ echarts/index.js # ECharts工具(封装了快速生成图标的 JS 方法)|_ request/index.js # 请求解析工具(封装了发送请求和解析响应的 JS 方法)|_ router/index.js # 路由配置文件(封装路由配置和路由守卫)|_ util/index.js # 通用工具(封装了项目中用到的 JS 方法)|_ vuex/index.js # Vuex配置文件(封装了登录状态变量|_ views/**/*.vue # Vuex页面|_ App.vue # 主 Vue 文件(提供了一个RouterView标签)|_ main.js # 主 JS 文件(用于绑定 App.vue 和 index.html 文件)|_ style.scss # 主 CSS 文件(已切换为 SCSS 文件格式)|_ .gitignore # git 忽略上传列表|_ index.html # 主 HTML 文件|_ package.json # 项目依赖配置文件|_ package-lock.json # 项目依赖配置文件(锁定版本)|_ README.md # 项目说明文档|_ vite.config.js # 项目配置文件(端口号等在这里配置)

S01. 基础环境搭建

武技:参考 JB3-8-Vue(一)- S01E01.3 创建前端项目。
  1. 基于 Vite 创建 Vue3 项目,项目名为 ml-web。
  2. 替换项目的 ICO 图标文件如 ml-web.ico 等。
  3. 删除无用目录和文件:
    1. 删除 .vscode 目录。
    2. 删除 assets 目录中的全部文件如 vue.svg 等。
    3. 删除 components 目录中的全部文件如 HelloWorld.vue 等。
    4. 清空 style.css 文件中的全部代码。
  4. 优化 index.html 首页文件如下:
<!DOCTYPEhtml><!--class="dark":全部页面使用暗黑模式--><htmllang="en"class="dark"><head><metacharset="UTF-8"/><linkrel="icon"href="/ml-web.ico"/><metaname="viewport"content="width=device-width, initial-scale=1.0"/><title>MyLesson管理平台</title></head><body><divid="app"></div><scripttype="module"src="src/main.js"></script></body></html>
  1. 优化 App.vue 文件如下:
<scriptsetup></script><template> Hello Vue! </template><stylescoped></style>
  1. 在 vite.config.js 配置项目端口号为 24108:
import{defineConfig}from'vite';import vue from'@vitejs/plugin-vue';// https://vitejs.dev/config/ exportdefaultdefineConfig({plugins:[vue()],// 配置用户前台项目的IP和端口号 server:{host:'localhost',port:24108,}})
  1. 启动前端项目:
npm run dev 

E01. 安装基础组件

1. 路由管理器Router

武技:参考 JB3-8-Vue(一)- S01E02.1 局部安装 VueRouter 组件。
  1. 在 App.vue 文件中添加 <router-view> 标签。
  2. 初始化 router/index.js 文件,内容如下:
import{createRouter, createWebHashHistory}from"vue-router";const router =createRouter({history:createWebHashHistory(),routes:[]});/* * 路由前置守卫:每次转发路由前执行的函数 * param to: 来源地址 * param from: 目标地址 * next: 放行函数 */ router.beforeEach((to, from, next)=>{// console.log(to, from); // 放行:支持使用 next('/ABC') 表示放行到指定页面 next();});exportdefault router;
  1. 在 main.js 文件中配置 VueRouter 组件,内容参考 JB3-8-Vue(一)- S01E02.1

2. 状态管理器Vuex

武技:参考 JB3-8-Vue(一)- S01E02.2 局部安装 Vuex 组件。
  1. 初始化 vuex/index.js 文件,内容如下:
import{createStore}from'vuex';const vuex =createStore({state:{// 用户登录状态变量: 若sessionStorage中存在token则为true,反之为falseloginFlag:!!sessionStorage.getItem('token')},mutations:{setLoginFlag:(state, loginFlag)=> state.loginFlag = loginFlag },actions:{setLoginFlag:async(context, loginFlag)=>await context.commit('setLoginFlag', loginFlag)}});exportdefault vuex;
  1. 在 main.js 文件中配置 Vuex 组件,内容参考 JB3-8-Vue(一)- S01E02.2

3. 样式预处理SCSS

武技:参考 JB3-8-Vue(一)- S01E02.3 局部安装 SCSS 组件并将 style.css 文件修改为 style.scss 文件。

优化 style.scss 文件内容如下:

::-webkit-scrollbar{display: none; // 隐藏滚动条 }html{font-family: Consolas, Avenir, Helvetica, Arial, sans-serif; // 字体 font-size: 14px; // 全局字号 }body{margin: 0; // 清空内边距 padding: 0; // 清空外边距 }

4. 异步请求Axios

武技:参考 JB3-8-Vue(二)- S02 安装 Axios 组件。

5. 框架ElementPlus

武技:参考 JB3-8-Vue(二)- S03E01 安装 ElementPlus 组件(基础库 + 图标库 + 暗黑库)。
  1. 在 main.js 文件中配置 ElementPlus 组件,内容参考 JB3-8-Vue(二)- S03E01
  2. 在 style.scss 文件中添加暗黑库适配样式,内容参考 JB3-8-Vue(二)- S03E01

6. 播放器XGPlayer

武技:参考 JB3-8-Vue(二)- S03E02 安装 XGPlayer 组件。
  1. 封装 components/MyPlayer.vue 通用组件。

7. 图表ECharts

武技:参考 JB3-8-Vue(二)- S03E03 安装 ECharts 组件。
  1. 封装 echarts/index.js 通用文件。

E02. 封装通用组件

武技:参考 JB3-8-Vue(三)- S04 封装 ElementPlus 组件。
  1. 封装 util/index.js 通用工具文件。
  2. 封装 request/index.js 通用请求文件。
  3. 封装 components/MyNav.vue 路径导航组件。
  4. 封装 components/MyIcon.vue 图标文字组件。
  5. 封装 components/MyHead.vue 数据页头组件。
  6. 封装 components/MyTable.vue 数据表格组件。
  7. 封装 components/MyForm.vue 数据表单组件。
  8. 封装 components/MyUpload.vue 数据表单组件。

1. 扩展通用工具util

武技:对 util/index.js 文件进行扩展(原内容参考 JB3-8-Vue(三)- S04 笔记)。

具体扩展内容如下:

import{ElMessage}from"element-plus";import router from"../router/index.js";/** * 性别代码处理:0->'女',1->'男',2->'保密' * * @param genderCode 性别代码 * @return string 对应的性别字符串 */exportfunctiongenderFormat(genderCode){if(genderCode ==='0'|| genderCode ===0)return'女孩';if(genderCode ==='1'|| genderCode ===1)return'男孩';if(genderCode ==='2'|| genderCode ===2)return'保密';return'性别代码异常';}/** * 秒杀活动状态代码处理:0->'未开始',1->'已开始',2->'已结束' * * @param status 秒杀活动状态代码 * @return string 对应的秒杀活动状态字符串 */exportfunctionseckillStatusFormat(status){if(status ==='0'|| status ===0)return'未开始';if(status ==='1'|| status ===1)return'已开始';if(status ==='2'|| status ===2)return'已结束';return'秒杀活动状态代码异常';}/** * 订单状态代码处理:0->'未付款',1->'已付款',2->'已取消',3->'其他' * * @param stateCode 订单状态代码 * @return string 对应的订单状态代码字符串 */exportfunctionorderStateFormat(stateCode){if(stateCode ==='0'|| stateCode ===0)return'未付款';if(stateCode ==='1'|| stateCode ===1)return'已付款';if(stateCode ==='2'|| stateCode ===2)return'已取消';if(stateCode ==='3'|| stateCode ===3)return'其他';return'订单状态代码异常';}/** * 订单支付方式代码处理:0->'未支付',1->'微信',2->'支付宝',3->'其他' * * @param typeCode 订单支付方式代码 * @return string 对应的订单支付方式代码字符串 */exportfunctionorderPayTypeFormat(typeCode){if(typeCode ==='0'|| typeCode ===0)return'未支付';if(typeCode ==='1'|| typeCode ===1)return'微信';if(typeCode ==='2'|| typeCode ===2)return'支付宝';if(typeCode ==='3'|| typeCode ===3)return'其他';return'订单支付方式代码异常';}/** * DML 操作后处理函数:添加,修改,删除或上传操作成功后,在指定延迟时间后,跳转到指定页面 * * @param path 跳转路径 * @param seconds 延迟时间,单位毫秒,默认 1000 毫秒 * @return void */functionsuccessTo(path, seconds =1000){ElMessage('操作成功!');setTimeout(()=> router.push(path), seconds);}

2. 封装常量工具const

武技:开发常量工具文件 const/index.js
// 环境IP地址constHOST='http://localhost';exportconstGATEWAY_HOST=`${HOST}:24101`;exportconstUSER_EXCEL_HOST=`${GATEWAY_HOST}/user-server/api/v1/user/excel`;exportconstEPISODE_EXCEL_HOST=`${GATEWAY_HOST}/course-server/v1/episode/excel`;exportconstORDER_EXCEL_HOST=`${GATEWAY_HOST}/order-server/api/v1/order/excel`;// Minio函数exportconstMINIO_HOST=`http://192.168.40.77:9001/mylesson`;exportconstMINIO_AVATAR=url=>MINIO_HOST+'/avatar/'+ url;exportconstMINIO_BANNER=url=>MINIO_HOST+'/banner/'+ url;exportconstMINIO_COURSE_COVER=url=>MINIO_HOST+'/course-cover/'+ url;exportconstMINIO_COURSE_SUMMARY=url=>MINIO_HOST+'/course-summary/'+ url;exportconstMINIO_EPISODE_VIDEO=url=>MINIO_HOST+'/episode-video/'+ url;exportconstMINIO_EPISODE_VIDEO_COVER=url=>MINIO_HOST+'/episode-video-cover/'+ url;// 表单规则exportconstRULE={TITLE:[{pattern:/^.{1,42}$/,message:'标题长度必须在1~42之间'}],AUTHOR:[{pattern:/^.{1,42}$/,message:'作者名称长度必须在1~42之间'}],INFO:[{pattern:/^.{1,170}$/,message:'描述长度必须在1~170之间'}],CONTENT:[{pattern:/^.{1,170}$/,message:'内容长度必须在1~170之间'}],VCODE:[{pattern:/^\d{6}$/,message:'验证码必须为6位数字'}],MENU_URL:[{pattern:/^\/[a-zA-Z]{0,256}$/,message:'跳转地址必须以 / 开头,后续内容仅支持0~256个英文字母'}],MENU_ICON:[{pattern:/^[a-zA-Z]{1,256}$/,message:'图标仅支持1~256个英文字母'}],USERNAME:[{pattern:/^[a-zA-Z0-9]{4,20}$/,message:'账号必须由4到20个英文字母或数字组成'}],PASSWORD:[{pattern:/^[a-zA-Z0-9]{4,20}$/,message:'密码必须由4到20个英文字母或数字组成'}],REALNAME:[{pattern:/^[\u4e00-\u9fa5]{2,6}$/,message:'真实姓名必须由2到6个中文组成'}],NICKNAME:[{pattern:/^[\u4e00-\u9fa5|_a-zA-Z0-9]{2,10}$/,message:'昵称必须由2到10个中文、英文或数字组成'}],IDCARD:[{pattern:/^[1-9]\d{5}(19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/,message:'身份证号格式不正确'}],PHONE:[{pattern:/^1(3[0-9]|4[01456879]|5[0-35-9]|6[2567]|7[0-8]|8[0-9]|9[0-35-9])\d{8}$/,message:'手机号码格式不正确'}],EMAIL:[{pattern:/^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/,message:'电子邮箱格式不正确'}],PROVINCE:[{pattern:/^[\u4e00-\u9fa5]{2,20}$/,message:'省份必须由2到20个中文组成'}],CODE:[{pattern:/^.{1,42}$/,message:'兑换口令长度必须在1~42之间'}],SN:[{pattern:/^.{1,42}$/,message:'订单编号长度必须在1~42之间'}],}/* ==================== 项目相关信息 ==================== */// 项目环境信息exportconstPROJECT_INFO={title:'《我的课堂》后台管理系统',author:'周航宇',version:'v1.0.0',gatewayHost:`${HOST}:24101`,userHost:`${HOST}:24102`,courseHost:`${HOST}:24103`,saleHost:`${HOST}:24104`,orderHost:`${HOST}:24105`,searchHost:`${HOST}:24106`,socketHost:`${HOST}:24107`,webHost:`${HOST}:24108`,put:22,post:25,get:63,delete:41,info:'MyLesson 项目,全称《我的在线课堂》,是一个精心设计和开发的线上学习平台,其灵感来源于网易云课堂、腾讯云课堂等知名在线教育平台。该项目的核心理念是为用户提供一个更加优雅、专注且简洁的学习环境,同时提供强有力的技术支持。通过这个平台,用户不仅能够享受到高质量的在线课程和互动体验,还能在任何时间、任何地点轻松获取所需的知识资源。 我们致力于打造一个高效、便捷且舒适的在线学习空间,使每一个渴望学习的人都能在这里找到适合自己的课程。无论是专业技能提升还是个人兴趣培养,我们的平台都能满足不同用户的需求。此外,我们还特别注重用户体验,确保界面友好、操作简便,并不断优化平台性能,以确保流畅的学习过程。MyLesson 项目全面采用了微服务架构,以面向对象和面向接口的方式进行开发。这种设计不仅提高了代码的可维护性和扩展性,还增强了系统的灵活性和性能。在项目的前端和后端开发中,无论是用户界面还是管理员界面,我们都选择了 SpringBoot + MyBatisFlex 框架,确保了数据处理和业务逻辑的高效实现。对于后台(管理员)的前端部分,我们选用了 Vue3 与 ElementPlus 组合,为管理员提供了直观且易于操作的界面。至于前台(用户)端,则通过微信小程序来构建,这样可以充分利用微信平台的优势,提供更加便捷和流畅的用户体验。整个项目的设计和实施都充分考虑到了用户体验和技术实现的最佳结合。总之,我们的目标是让学习变得更加简单和愉快,让用户在享受优质教育资源的同时,也能感受到学习的乐趣。无论你是学生、职场人士还是终身学习者,MyLesson 都将是你理想的在线学习伙伴。',};// 项目技术栈信息exportconstPROJECT_SKILLS=[{label:'底层操作系统',value:'Windows',version:'11'},{label:'语言开发环境',value:'JDK',version:'17.0.9'},{label:'集成开发工具',value:'IntelliJ IDEA',version:'2023.3.3.win Ultimate Edition'},{label:'项目管理工具',value:'Maven',version:'3.9.9'},{label:'版本控制工具',value:'Git',version:'2.28.0.windows.1'},{label:'代码托管中心',value:'GitEE',version:'latest'},{label:'前端服务容器',value:'Node',version:'20.12.0'},{label:'前端测试软件',value:'Edge',version:'120.0.2210.77'},{label:'压力测试工具',value:'JMeter',version:'5.4.1'},{label:'虚拟管理工具',value:'VmWare',version:'17.5.1 build-23298084'},{label:'虚拟操作系统',value:'OpenEuler',version:'24.03-LTS'},{label:'容器管理引擎',value:'Docker',version:'18.09.0'},{label:'数据存储仓库',value:'MySQL',version:'8.0.27'},{label:'对象存储仓库',value:'MinIO',version:'RELEASE.2023-08-31T15-31-16Z'},{label:'数据缓存仓库',value:'Redis',version:'7.0.5'},{label:'反向代理组件',value:'Nginx',version:'1.25.2'},{label:'搜索引擎组件',value:'ElasticSearch',version:'8.4.0'},{label:'搜索引擎界面',value:'Kibana',version:'8.4.0'},{label:'日志收集组件',value:'Logstash',version:'8.4.0'},{label:'单元测试',value:'junit',version:'4.13.2'},{label:'代码简化',value:'lombok',version:'1.18.24'},{label:'通用工具',value:'hutool',version:'5.8.25'},{label:'数据库驱动',value:'mysql-connector-j',version:'8.2.0'},{label:'持久层框架',value:'mybatis-flex-spring-boot3-starter',version:'1.10.2'},{label:'控制层框架',value:'spring-boot-starter-web',version:'3.1.5'},{label:'切面编程',value:'spring-boot-starter-aop',version:'3.1.5'},{label:'搜索引擎',value:'spring-boot-starter-data-elasticsearch',version:'3.1.5'},{label:'缓存容器',value:'spring-boot-starter-data-redis',version:'3.1.5'},{label:'缓存工具',value:'spring-boot-starter-cache',version:'3.1.5'},{label:'登录校验',value:'jjwt',version:'0.9.1'},{label:'参数校验',value:'hibernate-validator',version:'8.0.1.Final'},{label:'报表打印',value:'easyexcel',version:'3.3.4 '},{label:'对象存储',value:'minio',version:'3.0.10'},{label:'文档工具',value:'knife4j-openapi3-jakarta-spring-boot-starter',version:'4.4.0'},{label:'注册中心',value:'spring-cloud-starter-alibaba-nacos-discovery',version:'2022.0.0.0'},{label:'配置中心',value:'spring-cloud-starter-alibaba-nacos-config',version:'2022.0.0.0 '},{label:'服务容错',value:'spring-cloud-starter-alibaba-sentinel',version:'2022.0.0.0'},{label:'分布式事务',value:'spring-cloud-starter-alibaba-seata',version:'2022.0.0.0'},{label:'分布式调度',value:'xxl-job',version:'2.4.2'},{label:'远程调用',value:'spring-cloud-starter-openfeign',version:'4.0.4'},{label:'链路追踪',value:'micrometer-tracing',version:'1.11.5'},{label:'消息队列',value:'rocketmq-spring-boot-starter',version:'2.2.2'},{label:'页面布局',value:'HTML',version:'5'},{label:'页面美化',value:'CSS',version:'3'},{label:'脚本功能',value:'ECMAScript',version:''},{label:'前端服务器',value:'node',version:'20.12.0'},{label:'Vue脚手架',value:'vite',version:'5.5.1'},{label:'Vue路由',value:'vue-router',version:'4.0.3'},{label:'Vue样式预处理器',value:'sass-embedded',version:'1.77.8'},{label:'Vue状态管理',value:'vuex',version:'4.0.0'},{label:'AJAX产品',value:'axios',version:'1.6.7'},{label:'WEB框架',value:'element-plus',version:'2.5.3'},{label:'WEB框架图标库',value:'icons-vue',version:'2.3.1'},{label:'WEB框架暗黑库',value:'@vueuse/core',version:'10.7.2'},{label:'视频播放器',value:'xgplayer',version:'3.0.11'},{label:'图表库',value:'ApacheEcharts',version:'5.4.3'},{label:'用户前端',value:'微信小程序',version:'最新版'}];/* ==================== 下拉菜单预设选项 ==================== */// 下拉菜单选项 - 性别exportconstGENDER_OPTIONS=[{label:'女孩',value:0},{label:'男孩',value:1},{label:'保密',value:2},];// 下拉菜单选项 - 星座exportconstZODIAC_OPTIONS=[{label:'白羊座(Aries)',value:'白羊座'},{label:'金牛座(Taurus)',value:'金牛座'},{label:'双子座(Gemini)',value:'双子座'},{label:'巨蟹座(Cancer)',value:'巨蟹座'},{label:'狮子座(Leo)',value:'狮子座'},{label:'处女座(Virgo)',value:'处女座'},{label:'天秤座(Libra)',value:'天秤座'},{label:'天蝎座(Scorpio)',value:'天蝎座'},{label:'射手座(Sagittarius)',value:'射手座'},{label:'摩羯座(Capricorn)',value:'摩羯座'},{label:'水瓶座(Aquarius)',value:'水瓶座'},{label:'双鱼座(Pisces)',value:'双鱼座'},];// 下拉菜单选项 - 省份exportconstPROVINCE_OPTIONS=[{label:'北京',value:'北京'},{label:'上海',value:'上海'},{label:'天津',value:'天津'},{label:'重庆',value:'重庆'},{label:'河北',value:'河北'},{label:'山西',value:'山西'},{label:'辽宁',value:'辽宁'},{label:'吉林',value:'吉林'},{label:'黑龙江',value:'黑龙江'},{label:'江苏',value:'江苏'},{label:'浙江',value:'浙江'},{label:'安徽',value:'安徽'},{label:'福建',value:'福建'},{label:'江西',value:'江西'},{label:'山东',value:'山东'},{label:'河南',value:'河南'},{label:'湖北',value:'湖北'},{label:'湖南',value:'湖南'},{label:'广东',value:'广东'},{label:'广西',value:'广西'},{label:'海南',value:'海南'},{label:'四川',value:'四川'},{label:'贵州',value:'贵州'},{label:'云南',value:'云南'},{label:'西藏',value:'西藏'},{label:'陕西',value:'陕西'},{label:'甘肃',value:'甘肃'},{label:'青海',value:'青海'},{label:'宁夏',value:'宁夏'},{label:'新疆',value:'新疆'},{label:'香港',value:'香港'},{label:'澳门',value:'澳门'},{label:'台湾',value:'台湾'},{label:'其他',value:'其他'},];// 下拉菜单选项 - 秒杀活动状态exportconstSECKILL_STATUS_OPTIONS=[{label:'未开始',value:0},{label:'已开始',value:1},{label:'已结束',value:2},];// 下拉菜单选项 - 订单状态exportconstORDER_STATE_OPTIONS=[{label:'未付款',value:0},{label:'已付款',value:1},{label:'已取消',value:2},{label:'其它',value:3}];// 下拉菜单选项 - 订单支付方式exportconstORDER_PAY_TYPE_OPTIONS=[{label:'未支付',value:0},{label:'微信',value:1},{label:'支付宝',value:2},{label:'其它',value:3},];

3. 封装请求工具api

心法:api/index.js 中封装了通用的,对应后台增删改查 API 接口的请求发送方法,以及请求和响应的拦截器。

相关函数

函数(其中 args['module'] 必须传递)描述请求类型
insertApi(params, args)添加一条记录POST
selectApi(id, args)根据主键查询GET
pageApi(params, args)分页查询记录GET
simpleListApi(params, args)查询简单列表GET
updateApi(params, args)根据主键修改PUT
deleteApi(id, args)根据主键删除DELETE
deleteBatchApi(ids, args)根据主键批删DELETE
excelApi(url, fileName)下载Excel报表GET

目录结构

|_ api |_ cms |_ course.js |_ episode.js |_ oms |_ order.js |_ sms |_ banner.js |_ ums |_ menu.js |_ role.js |_ user.js |_ index.js 
武技:封装请求接口相关文件。
  1. 封装通用请求工具 api/index.js:
import axios from'axios';import{GATEWAY_HOST}from'../const';import{isNotEmpty}from"../util";import{STATUS}from"../request";/* =============== Axios实例配置 =============== */// 创建Axios实例: 配置请求前缀和超时时间 exportconstGATEWAY_AXIOS= axios.create({baseURL:GATEWAY_HOST,timeout:5000});/* =============== 基本Axios请求 =============== *//** * API前缀处理:根据模块名称返回API前缀 * * @param module 模块名称,如 user, course 等 * @return 对应的API前缀,如 /user-server/api/v1/user 等,末尾无 / 符号 * */exportfunctionapiPrefixFormat(module){// 微服务与模块的映射关系:key 为微服务名称,value 为模块名称数组 const serviceModuleMap ={'user-server':['menu','role','user'],'course-server':['category','comment','course','episode','report','season'],'sale-server':['article','banner','coupons','notice','seckill','seckillDetail'],'order-server':['cart','order','orderDetail']};// 查找匹配的微服务名称:遍历微服务与模块的映射关系,找到包含当前模块 module 的微服务名称 const microServiceName = Object.keys(serviceModuleMap).find(key=> serviceModuleMap[key].includes(module));// 返回拼接后的API前缀 return`/${microServiceName}/api/v1/${module}`;}// POST - 添加一条记录 exportfunctioninsertApi(params, args){returnGATEWAY_AXIOS.post(`${apiPrefixFormat(args['module'])}/insert`, params)}// GET - 根据主键查询 exportfunctionselectApi(id, args){returnGATEWAY_AXIOS.get(`${apiPrefixFormat(args['module'])}/select/${id}`)}// GET - 分页查询记录 exportfunctionpageApi(params, args){returnGATEWAY_AXIOS.get(`${apiPrefixFormat(args['module'])}/page`,{params: params});}// GET - 查询简单列表 exportfunctionsimpleListApi(params, args){returnGATEWAY_AXIOS.get(`${apiPrefixFormat(args['module'])}/simpleList`);}// PUT - 根据主键修改 exportfunctionupdateApi(params, args){returnGATEWAY_AXIOS.put(`${apiPrefixFormat(args['module'])}/update`, params)}// DELETE - 根据主键删除 exportfunctiondeleteApi(id, args){returnGATEWAY_AXIOS.delete(`${apiPrefixFormat(args['module'])}/delete/${id}`)}// DELETE - 根据主键批删 exportfunctiondeleteBatchApi(ids, args){returnGATEWAY_AXIOS.delete(`${apiPrefixFormat(args['module'])}/deleteBatch?ids=${ids}`)}// GET - 下载Excel报表 exportfunctionexcelApi(url, fileName){// 自动拼接后缀:若文件名未包含 .xlsx 后缀,则自动添加  fileName = fileName.endsWith('.xlsx')? fileName : fileName +'.xlsx';// 发送下载请求:响应类型为blob,返回Promise对象 returnGATEWAY_AXIOS.get(url,{responseType:'blob'}).then(res=>{// 借助超链接标签完成下载功能 let a = document.createElement('a'); a.style.display ='none'; a.href =URL.createObjectURL(newBlob([res.data])); a.setAttribute('download', fileName); document.body.appendChild(a); a.click(); document.body.removeChild(a); a =null;});}/* =============== 拦截器 =============== */// 在发送请求前执行 GATEWAY_AXIOS.interceptors.request.use(req=>{// 从 sessionStorage 中获取 Token,加入请求头并放行请求 const token = sessionStorage.getItem("token");if(isNotEmpty(token)) req.headers['token']= token;return req;},err=> Promise.reject(err));// 在接收到响应之后执行(直接放行) // 注01:token续期工作由后台网关使用redis完成,此处省略// 注02:状态码解析工作由 request/index.js 文件中的 getResponseData() 方法完成,此处省略GATEWAY_AXIOS.interceptors.response.use(resp=>{return resp;},err=> Promise.reject(err));
  1. 封装用户相关请求接口文件 api/ums/user.js:
import{GATEWAY_AXIOS, apiPrefixFormat}from"../index.js";import{GATEWAY_HOST}from"../../const/index.js";// 上传头像地址exportconstUPLOAD_AVATAR_URL=GATEWAY_HOST+'/user-server/api/v1/user/uploadAvatar/';// POST - 按账号密码登录exportfunctionloginByAccountApi(params){returnGATEWAY_AXIOS.post(`${apiPrefixFormat('user')}/loginByAccount`, params)}// GET - 用户统计数据exportfunctionstatisticsApi(params){returnGATEWAY_AXIOS.get(`${apiPrefixFormat('user')}/statistics`, params)}// GET - 获取手机验证码exportfunctiongetVcodeApi(phone){returnGATEWAY_AXIOS.get(`${apiPrefixFormat('user')}/getVcode/${phone}`)}// GET - 查询解绑验证码exportfunctiongetUnboundVcodeApi(id){returnGATEWAY_AXIOS.get(`${apiPrefixFormat('user')}/getUnboundVcode/${id}`)}// GET - 校验解绑验证码exportfunctioncheckUnboundVcodeApi(id, vcode){returnGATEWAY_AXIOS.get(`${apiPrefixFormat('user')}/checkUnboundVcode/${id}/${vcode}`)}// GET - 查询解绑验证码exportfunctiongetBoundVcodeApi(phone){returnGATEWAY_AXIOS.get(`${apiPrefixFormat('user')}/getBoundVcode/${phone}`)}// PUT - 修改用户手机号码exportfunctionupdatePhoneApi(params){returnGATEWAY_AXIOS.put(`${apiPrefixFormat('user')}/updatePhone`, params)}// PUT - 根据主键修改密码exportfunctionupdatePasswordApi(params){returnGATEWAY_AXIOS.put(`${apiPrefixFormat('user')}/updatePassword`, params)}// PUT - 根据主键重置密码exportfunctionresetPasswordApi(id){returnGATEWAY_AXIOS.put(`${apiPrefixFormat('user')}/resetPassword/${id}`)}
  1. 封装角色相关请求接口文件 api/ums/role.js:
import{GATEWAY_AXIOS, apiPrefixFormat}from"../index.js";// GET - 按用户主键查询用户的全部角色ID列表exportfunctionlistRoleIdsByUserIdApi(userId){returnGATEWAY_AXIOS.get(`${apiPrefixFormat('role')}/listRoleIdsByUserId/${userId}`)}// PUT - 按用户主键修改用户的角色列表exportfunctionupdateRolesByUserIdApi(userId, roleIds){returnGATEWAY_AXIOS.put(`${apiPrefixFormat('role')}/updateRolesByUserId?userId=${userId}&roleIds=${roleIds}`)}
  1. 封装菜单相关请求接口文件 api/ums/menu.js:
import{GATEWAY_AXIOS, apiPrefixFormat}from"../index.js";// GET - 按角色主键查询角色的全部菜单ID列表exportfunctionlistMenuIdsByRoleIdApi(roleId){returnGATEWAY_AXIOS.get(`${apiPrefixFormat('menu')}/listMenuIdsByRoleId/${roleId}`)}// PUT - 按角色主键修改角色的菜单列表exportfunctionupdateMenusByRoleIdApi(roleId, menuIds){returnGATEWAY_AXIOS.put(`${apiPrefixFormat('menu')}/updateMenusByRoleId?roleId=${roleId}&menuIds=${menuIds}`)}
  1. 封装课程相关请求接口文件 api/cms/course.js:
import{GATEWAY_HOST}from"../../const/index.js";// 上传课程封面图片地址exportconstUPLOAD_COURSE_COVER_URL=GATEWAY_HOST+'/course-server/api/v1/course/uploadCover/';// 上传课程摘要图片地址exportconstUPLOAD_COURSE_SUMMARY_URL=GATEWAY_HOST+'/course-server/api/v1/course/uploadSummary/';
  1. 封装集次相关请求接口文件 api/cms/episode.js:
import{GATEWAY_HOST}from"../../const/index.js";// 上传集次视频地址exportconstUPLOAD_EPISODE_VIDEO_URL=GATEWAY_HOST+'/course-server/api/v1/episode/uploadVideo/';// 上传集次视频封面图片地址exportconstUPLOAD_EPISODE_VIDEO_COVER_URL=GATEWAY_HOST+'/course-server/api/v1/episode/uploadVideoCover/';
  1. 封装横幅相关请求接口文件 api/sms/banner.js:
import{GATEWAY_HOST}from"../../const/index.js";// 上传轮播图片地址exportconstUPLOAD_BANNER_URL=GATEWAY_HOST+'/sale-server/api/v1/banner/uploadBanner/';
  1. 封装订单相关请求接口文件 api/oms/order.js:
import{GATEWAY_AXIOS, apiPrefixFormat}from"../index.js";// GET - 订单统计数据exportfunctionstatisticsApi(params){returnGATEWAY_AXIOS.get(`${apiPrefixFormat('order')}/statistics`, params)}

S02. 登录业务模块

E01. 系统登录模块

武技:搭建起始文件环境和对应的路由代码。
  1. 创建全部相关 Vue 页面,结构如下:
|_ views |_ Login.vue |_ Main.vue |_ Dashboard.vue |_ personal |_ Personal.vue |_ PersonalUpdate.vue |_ PersonalUpdatePhone.vue 
  1. 在 router/index.js 文件中开发全部相关页面路由配置:
import Login from"../views/Login.vue";import Main from"../views/Main.vue";import Dashboard from"../views/Dashboard.vue";import Personal from"../views/personal/Personal.vue";import PersonalUpdate from"../views/personal/PersonalUpdate.vue";import PersonalUpdatePhone from'../views/personal/PersonalUpdatePhone.vue';// 在 routes: [] 中添加:{path:'/',name:'Login',component: Login},{path:'/Main',name:'Main',component: Main,redirect:'/Dashboard',children:[{path:'/Dashboard',name:'Dashboard',component: Dashboard},{path:'/Personal',name:'Personal',component: Personal},{path:'/PersonalUpdate',name:'PersonalUpdate',component: PersonalUpdate},{path:'/PersonalUpdatePhone',name:'PersonalUpdatePhone',component: PersonalUpdatePhone},]}

1. 系统登录页面

心法:系统登录页面。

页面功能

功能描述
登录系统点击 “管理员登录” 按钮,登录系统
忘记密码点击 “忘记密码” 按钮,右上角提示管理员账号密码
重置内容点击 “重置内容” 按钮,将账号密码恢复为默认值

效果图

在这里插入图片描述
武技:开发系统登录页面 views/Login.vue

需要自行引入 /assets/image/loginBackground.jpg 背景图片。

<scriptsetup>import router from"../router/index.js";import vuex from"../vuex/index.js";import{ElMessage, ElNotification}from"element-plus";import{RULE}from"../const/index.js";import{onMounted, reactive, ref}from"vue";import{getResponseData}from"../request/index.js";import{isNotNull}from"../util/index.js";import{loginByAccountApi}from"../api/ums/user.js";// 表单 + 表单数据 + 表单规则 todo: 上线后将默认值删除let loginForm =ref();let loginFormData =reactive({username:'admin',password:'admin'});let loginFormRules ={username:RULE.USERNAME,password:RULE.PASSWORD};/** * 登录系统 * * 1. 验证表单,验证通过后发送登录请求。 * 2. vuex修改登录状态。 * 3. 存储Token令牌,该用户的个人信息,该用户的菜单列表和该用户的角色列表。 * 4. 路由到 Main 页面。 */functionlogin(){ loginForm.value.validate(valid=>{if(valid){// 同步发送登录请求loginByAccountApi(loginFormData).then(res=>{let data =getResponseData(res);if(isNotNull(data)){ ElMessage.success('登录成功!'); vuex.dispatch('setLoginFlag',true); sessionStorage.setItem('token', data['token']); sessionStorage.setItem('loginUser',JSON.stringify(data['user'])); sessionStorage.setItem('loginMenus',JSON.stringify(data['menus'])); sessionStorage.setItem('loginRoleTitles',JSON.stringify(data['roleTitles'])); router.push('/Main');}});}});}/** * 重置表单 * * 1. 清空表单数据。 */functionresetForm(){ loginForm.value.resetFields();}/** * 忘记密码 * * 1. 使用 ElNotification 在右上角通知:“员工测试账号: admin / 123456789” */functionforgetPassword(){ ElNotification.info({title:'通知列表',message:'测试账号: admin / admin',position:'top-right',});}/** * 加载函数 * * 1. 清空SessionStorage 中的所有信息。 * 2. vuex修改登录状态为 false。 */onMounted(()=>{ sessionStorage.clear(); vuex.dispatch('setLoginFlag',false);});</script><template><sectionclass="login-body"><el-cardclass="login-card"header="《我的课堂》后台管理系统"><el-formclass="login-form"ref="loginForm":model="loginFormData":rules="loginFormRules"status-icon><el-form-itemprop="username"required><el-inputv-model="loginFormData['username']"prefix-icon="User"suffix-icon="User"clearableplaceholder="输入账号 .."/></el-form-item><el-form-itemprop="password"required><el-inputv-model="loginFormData['password']"prefix-icon="Lock"suffix-icon="Lock"clearableplaceholder="输入密码 .."show-password/></el-form-item><el-buttonclass="login-btn"@click="login"type="primary"> 管理员登录 </el-button><el-checkboxclass="remember-cbx"label="记住账号"size="small"/><el-buttonclass="forget-btn"@click="forgetPassword"linksize="small"> 忘记密码 </el-button><el-buttonclass="reset-btn"@click="resetForm"linksize="small"type="warning"> 重置内容 </el-button></el-form></el-card></section></template><stylescopedlang="scss">.login-body{height: 100vh; // 高度 background:url("../assets/image/loginBackground.png") no-repeat; // 背景图片(不平铺) background-size: 100% 100%; // 上下 左右 padding-top: 200px; // 上内边距 box-sizing: border-box;// 忽略内边距影响 .login-card{margin: auto; // 自居中 width: 50vh; // 宽度 opacity: 0.95; // 透明度 }.login-btn{width: 100%; // 宽度 margin: 0 auto 10px; // 外边距 letter-spacing: 2px; // 字母间距 }.forget-btn, .reset-btn{float: right; // 右浮动 line-height: 18px; // 行高 }}</style>

2. 添加路由守卫

心法:路由守卫函数可以作用在每次路由转发之前,我们可以在该函数中判断用户是否具有本次路由操作的权限,若有则放行,若没有则给出友好提示,并使系统自动跳入登录页面。

路由守卫函数参数

位置参数描述
1to目标路由,如本次路由想要从 /A 跳转到 /B,则 to 指向 /B
2from来源路由,,如本次路由想要从 /A 跳转到 /B,则 from 指向 /A
3nextnext():表示放行,即允许本次路由
next('/C'):表示阻止本次路由,并跳转到 /C 页面
武技:在 router/index.js 文件中添加路由守卫代码。
import{ElMessage}from"element-plus";import vuex from"../vuex/index.js";/** * 路由前置守卫:每次转发路由前执行如下判断: * 1. 若页面为登录页面,或已登录状态,均直接放行。 * 2. 其余情况均给出 “请先登录” 提示,并在两秒后跳转回登录页面。 * * @param to: 目标地址 * @param from: 来源地址 */ router.beforeEach((to, from, next)=>{if(to.path ==='/'|| vuex.state['loginFlag']){next();}else{ ElMessage.warning('请先登录!');setTimeout(()=>next('/'),2000);}});

E02. 系统主体页面

1. 系统主体页面

心法:系统主体页面。

页面功能

功能描述
查看项目基本信息点击右上角对应图标按钮,展示项目基本信息抽屉
查看项目技术栈信息点击右上角对应图标按钮,展示项目技术栈信息抽屉
查看日历点击右上角对应图标按钮,展示日历抽屉
查看系统通知点击右上角对应图标按钮,展示系统通知弹窗(右下角)
查看个人信息点击右上角头像 → “查看个人信息” 按钮,跳转到查看个人信息页面
修改个人信息点击右上角头像 → “修改个人信息” 按钮,跳转到修改个人信息页面
换绑手机号码点击右上角头像 → “换绑手机号码” 按钮,跳转到修改手机号码页面
退出登录点击右上角头像 → “退出登录” 按钮,跳转到登录页面

效果图

在这里插入图片描述
武技:开发开发系统主体页面 views/Main.vue
<scriptsetup>import router from"../router/index.js";import{ElNotification}from"element-plus";import{MINIO_HOST,PROJECT_INFO,PROJECT_SKILLS}from"../const/index.js";import MyIcon from"../components/MyIcon.vue";import{ref}from"vue";// 当前登录的用户信息const loginUser =JSON.parse(sessionStorage.getItem('loginUser'));// 当前登录的菜单列表const menus =JSON.parse(sessionStorage.getItem('loginMenus'));// 当前登录的用户头像const avatar =MINIO_HOST+'/avatar/'+ loginUser['avatar'];// 项目LOGOconst logo =MINIO_HOST+'/logo.jpg';// 当前选中菜单的index值:默认选中当前路由路径let currentMenuIndex =ref(router.currentRoute.value['path']);// 左侧菜单列表是否折叠:向左收缩const isCollapse =ref(false);// 项目信息抽屉 + 项目技术栈抽屉 + 日历抽屉const projectInfoDrawer =ref();const projectSkillDrawer =ref();const calendarDrawer =ref();// 日历数据(本地时间)let calendarData =ref(newDate());/** * 打开信息抽屉 */functionopenProjectInfoDrawer(){ projectInfoDrawer.value =true;}/** * 打开技术栈抽屉 */functionopenProjectSkillDrawer(){ projectSkillDrawer.value =true;}/** * 打开日历抽屉 */functionopenCalendarDrawer(){ calendarDrawer.value =true;}/** * 系统通知 * * 1. 使用 ElNotification 在右下角通知:“暂无通知消息” */functionnotify(){ ElNotification.info({title:'通知列表',message:'暂无通知消息',position:'bottom-right'});}/** * 跳入Personal页面 */functiontoPersonal(){ router.push('/Personal');}/** * 跳入PersonalUpdate页面 */functiontoPersonalUpdate(){ router.push('/PersonalUpdate');}/** * 跳入PersonalUpdatePhone页面 */functiontoPersonalUpdatePhone(){ router.push('/PersonalUpdatePhone');}/** * 退出登录 * * 1. 跳转到登录页面(登录页面的加载函数中会清空sessionStorage信息并使用 vuex 修改登录状态,此处无需处理) */functionlogout(){ router.push('/');}</script><template><el-containerclass="main-body"v-if="menus"><el-asideclass="main-body-left"width="collapse"max-width="200px"><el-menuclass="menus-menu el-menu-vertical-demo":collapse="isCollapse":default-active="currentMenuIndex"unique-openedrouter><el-imageclass="logo":src="logo"/><el-menu-itemclass="house-item"index="/DashBoard"title="回到后台项目首页"><my-iconicon="House"label="DashBoard"/></el-menu-item><el-sub-menuclass="menus"v-for="(menu, i) in menus":key="menu['id']":index="i.toString()":title="menu['info']"><template#title><my-icon:icon="menu['icon']":label="menu['title']"/></template><el-menu-itemclass="sub-menus"v-for="subMenu in menu['subMenus']":key="subMenu['id']":index="subMenu['url']":title="subMenu['info']"><my-icon:icon="subMenu['icon']":label="subMenu['title']"/></el-menu-item></el-sub-menu></el-menu></el-aside><el-containerclass="main-body-right"><el-headerclass="main-body-right-head"><el-rowclass="is-align-middle"><el-colclass="fold-expand":span="2"><el-radio-groupv-model="isCollapse"><el-radio-button:label="!isCollapse"><my-iconsize="20":icon="!isCollapse ? 'Fold' : 'Expand'"/></el-radio-button></el-radio-group></el-col><el-colclass="project-title-col":span="7"><el-popover:content="PROJECT_INFO.info"width="500"placement="bottom-start"trigger="click"><template#reference> {{ PROJECT_INFO.title }} </template></el-popover></el-col><el-colclass="operation-btn-col":span="6":offset="5"><el-dividerdirection="vertical"/><el-tooltipcontent="全局搜索"><el-buttonicon="search"size="small"round@click=""/></el-tooltip><el-tooltipcontent="系统通知"><el-buttonicon="bell"@click="notify"size="small"round/></el-tooltip><el-tooltipcontent="项目基本信息"><el-buttonicon="list"@click="openProjectInfoDrawer"size="small"round/></el-tooltip><el-tooltipcontent="项目技术信息"><el-buttonicon="management"@click="openProjectSkillDrawer"size="small"round/></el-tooltip><el-tooltipcontent="系统日历"><el-buttonicon="calendar"@click="openCalendarDrawer"size="small"round/></el-tooltip><el-dividerdirection="vertical"/></el-col><el-colclass="nickname-col":span="3"v-if="loginUser['nickname']"> {{ loginUser['nickname'] }} </el-col><el-colclass="avatar-col":span="1"v-if="loginUser['avatar']"><el-dropdowntrigger="click"><spanclass="el-dropdown-link"><el-avatarclass="avatar":src="avatar":size="45"/></span><template#dropdown><el-dropdown-menu><el-dropdown-itemicon="InfoFilled"@click="toPersonal">查看个人信息</el-dropdown-item><el-dropdown-itemicon="Edit"@click="toPersonalUpdate">修改个人信息</el-dropdown-item><el-dropdown-itemicon="Phone"@click="toPersonalUpdatePhone">换绑手机号码</el-dropdown-item><el-dropdown-itemicon="WarnTriangleFilled"@click="logout"><el-texttype="danger">退出登录</el-text></el-dropdown-item></el-dropdown-menu></template></el-dropdown></el-col></el-row></el-header><el-mainclass="main-body-right-main"><router-view/></el-main></el-container></el-container><el-drawertitle="项目系统信息"v-model="projectInfoDrawer"size="50%"><el-descriptionsbordercolumn="1"><el-descriptions-itemv-for="(v, k) in PROJECT_INFO":key="k":label="k"> {{ v }} </el-descriptions-item></el-descriptions></el-drawer><el-drawertitle="项目技术栈信息"v-model="projectSkillDrawer"size="50%"><el-descriptionsbordercolumn="1"><el-descriptions-itemv-for="item in PROJECT_SKILLS":key="item['label']":label="item['label']"> {{ item['value'] }} ({{ item['version'] }}) </el-descriptions-item></el-descriptions></el-drawer><el-drawertitle="系统日历"v-model="calendarDrawer"size="50%"><el-calendarv-model="calendarData"/></el-drawer></template><stylescopedlang="scss">.main-body-left{height: 100vh; // 高度 border-right: 1px solid #cccccc;// 右边框 .logo{padding: 10px; // 内边距 }.el-menu-vertical-demo:not(.el-menu--collapse){width: 200px; // 宽度 height: 100vh; // 高度 letter-spacing: 2px; // 字间距 }.el-icon{margin: 0 10px; // 上下外边距 左右外边距 }}.main-body-right-head{.project-title-col{font-weight: bolder; // 加粗 font-size: 1.5rem; // 字号倍率 }.nickname-col{text-align: right; // 右对齐 height: 50px; // 高度 display: inline-block; // 内联块 text-shadow: 2px 2px 2px gray; // 文字阴影 line-height: 50px; // 行高 }.avatar-col{text-align: right; // 右对齐 }.avatar{margin: 10px; // 外边距 outline: 1px solid #854040; // 边框 border: 1px solid #854040; // 边框 }}</style>

2. 系统仪表盘

心法:系统仪表盘组件。

页面功能

功能描述
今日用户总数页面加载时,查询 “今日用户总数” 并展示在统计面板中
今年用户总数页面加载时,查询 “今年用户总数” 并展示在统计面板中
今日订单总数页面加载时,查询 “今日订单总数” 并展示在统计面板中
今年用户总数页面加载时,查询 “今年用户总数” 并展示在统计面板中
男女比例分布页面加载时,查询 “用户男女比例” 并展示在饼状图中
支付方式分布页面加载时,查询 “支付方式和订单数关系” 并展示在柱状图中

效果图

在这里插入图片描述
武技:开发系统仪表盘组件 views/DashBoard.vue
<scriptsetup>import{onMounted, ref}from"vue";import{genderFormat, orderPayTypeFormat}from"../util/index.js";import{bar, pie}from"../echarts/index.js";import{getResponseData}from"../request/index.js";import{statisticsApi as userStatisticsApi}from"../api/ums/user.js";import{statisticsApi as orderStatisticsApi}from"../api/oms/order.js";import{isNotNull}from"../util/index.js";// 今日用户 + 今年用户 + 用户日增 + 用户年增let todayUserCount =ref(0), thisYearUserCount =ref(0);let userDayIncrease =ref(0), userYearIncrease =ref(0);// 今日订单 + 今年订单 + 订单日增 + 订单年增let todayOrderCount =ref(0), thisYearOrderCount =ref(0);let orderDayIncrease =ref(0), orderYearIncrease =ref(0);// 统计组件数据列表let statisticCards =ref([{value: todayUserCount,title:'今日用户总数(人)',increaseLabel:'对比昨日',increase: userDayIncrease},{value: thisYearUserCount,title:'今年用户总数(人)',increaseLabel:'对比去年',increase: userYearIncrease},{value: todayOrderCount,title:'今日订单总数(单)',increaseLabel:'对比昨日',increase: orderDayIncrease},{value: thisYearOrderCount,title:'今年订单总数(单)',increaseLabel:'对比去年',increase: orderYearIncrease}]);/** * 获取用户统计组件数据 * * 1. 获取今日用户,今年用户,用户日增,用户年增和用户性别比例数据 * 2. 处理饼图数据 * 3. 绘制饼图 */asyncfunctionselectUserStatistics(){// 获取用户统计组件数据,包括今日用户,今年用户,用户日增,用户年增和用户性别比例数据let userStatisticsData =getResponseData(awaituserStatisticsApi());if(isNotNull(userStatisticsData)){ todayUserCount.value = userStatisticsData['todayCount']; thisYearUserCount.value = userStatisticsData['thisYearCount']; userDayIncrease.value = userStatisticsData['dayIncrease']; userYearIncrease.value = userStatisticsData['yearIncrease'];}// 处理饼图数据let pieData = userStatisticsData['genderCount'];for(let i in pieData){ pieData[i]['name']=genderFormat(pieData[i]['name']);}// 绘制饼图pie({dom: document.querySelector('#genderBoard'),data: pieData,name:'性别 - 用户数'});}/** * 获取订单统计组件数据 * * 1. 获取今日订单,今年订单,订单日增,订单年增和支付方式统计数据 * 2. 准备柱图数据 * 3. 绘制柱图 */asyncfunctionselectOrderStatistics(){// 获取订单统计组件数据,包括今日订单,今年订单,订单日增,订单年增和支付方式统计数据let orderStatisticsData =getResponseData(awaitorderStatisticsApi());if(isNotNull(orderStatisticsData)){ todayOrderCount.value = orderStatisticsData['todayCount']; thisYearOrderCount.value = orderStatisticsData['thisYearCount']; orderDayIncrease.value = orderStatisticsData['dayIncrease']; orderYearIncrease.value = orderStatisticsData['yearIncrease'];}// 准备柱图数据let barData = orderStatisticsData['payTypeCount'];let xData =[], yData =[];for(let i in barData){ xData.push(orderPayTypeFormat(barData[i]['name'])); yData.push(barData[i]['value']);}// 绘制柱图bar({dom: document.querySelector('#payTypeBoard'),xData: xData,yData: yData,name:'支付方式 - 订单数',xName:'方式',yName:'订单数',});}/** * 加载函数 * * 1. 获取用户统计数据 * 2. 获取订单统计数据 */onMounted(()=>{selectUserStatistics();selectOrderStatistics();});</script><template><sectionclass="board-head"><el-row:gutter="16"><el-col:span="6"v-for="statisticCard in statisticCards"><divclass="statistic-card"><el-statisticclass="statistic-body":value="statisticCard['value']"><template#title><el-linkicon="InfoFilled":underline="false">&nbsp;{{ statisticCard['title'] }} </el-link></template></el-statistic><divclass="statistic-footer"><divclass="footer-item"> {{ statisticCard['increaseLabel'] }} <span:class="statisticCard['increase'] > 0 ? 'green': statisticCard['increase'] < 0 ? 'red' : ''"> {{ statisticCard['increase'] === '0' ? '持平' : statisticCard['increase'] }} <el-icon><component:is="statisticCard['increase'] > 0 ? 'CaretTop': statisticCard['increase'] < 0 ? 'CaretBottom' : ''"/></el-icon></span></div></div></div></el-col></el-row></section><sectionclass="board-body"><el-row:gutter="16"><el-colclass="chart-board":span="12"><el-divider><el-buttonlinkicon="PieChart">性别 - 用户数 · 图例</el-button></el-divider><divid="genderBoard"class="board"/></el-col><el-colclass="chart-board":span="12"><el-divider><el-buttonlinkicon="Histogram">支付方式 - 订单数 · 图例</el-button></el-divider><divid="payTypeBoard"class="board"/></el-col></el-row></section></template><stylescopedlang="scss">.board-head{margin-bottom: 30px;// 下外边距 .el-statistic{--el-statistic-content-font-size: 30px;// value字号 .el-icon{margin-left: 4px; // 左外边距 }}.statistic-card{height: 100%; // 高度 border-radius: 4px; // 圆角 border: 1px solid #1D1E1F; // 边框 background-color:var(--el-bg-color-overlay); // 背景色 padding: 20px 20px 0; // 上内边距,左右内边距,下内边距 text-align: center; // 内容居中 }.statistic-footer{display: flex; // flex布局 justify-content: center; // 左右居中 flex-wrap: wrap; // flex环绕 font-size: 12px; // 字号 color:var(--el-text-color-regular); // 前景色 margin-top: 16px; // 上外边距 }.statistic-footer .footer-item span:last-child{display: inline-flex; // flex布局 align-items: center; // 居中 margin-left: 4px; // 左外边距 }.green{color:var(--el-color-success); // 绿色 }.red{color:var(--el-color-error); // 红色 }}.board-body{height: 500px;// 高度 .board{width: 100%; // 宽度 height: 447px; // 高度 border: 1px solid #5470C6; // 边框 }.foot-divider-tip{padding-bottom: 50px; // 下外边距 }}</style>

3. 查看个人信息

心法:查看个人信息页面。

页面功能:仅支持查看当前个人详细信息。

效果图

在这里插入图片描述
武技:开发查看个人信息页面 views/personal/Personal.vue
<scriptsetup>import{MINIO_AVATAR}from"../../const/index.js";import{dateFormat, genderFormat}from"../../util/index.js";import MyNav from"../../components/MyNav.vue";// 当前登录的用户信息const loginUser =JSON.parse(sessionStorage.getItem('loginUser'));// 当前登录的用户头像const avatar =MINIO_AVATAR(loginUser['avatar']);// 路径导航const navItems =[{icon:'House',label:'DashBoard',url:'/DashBoard'},{icon:'View',label:'个人信息'},];</script><template><my-nav:items="navItems"/><divclass="personal-body"><el-row:gutter="100"><el-col:span="8"><el-imageclass="avatar":src="avatar"></el-image><el-divider/><el-descriptionscolumn="1"><el-descriptions-itemlabel="登录账号">{{ loginUser['username'] }}</el-descriptions-item><el-descriptions-itemlabel="真实姓名">{{ loginUser['realname'] }}</el-descriptions-item><el-descriptions-itemlabel="用户昵称">{{ loginUser['nickname'] }}</el-descriptions-item><el-descriptions-itemlabel="创建时间">{{ dateFormat(loginUser['created']) }}</el-descriptions-item><el-descriptions-itemlabel="修改时间">{{ dateFormat(loginUser['updated']) }}</el-descriptions-item></el-descriptions></el-col><el-col:span="16"><el-descriptionstitle="当前用户详细信息"bordercolumn="2"><el-descriptions-itemlabel="用户年龄"width="120">{{ loginUser['age'] }}</el-descriptions-item><el-descriptions-itemlabel="用户性别"width="120">{{genderFormat(loginUser['gender'])}}</el-descriptions-item><el-descriptions-itemlabel="用户星座">{{ loginUser['zodiac'] }}</el-descriptions-item><el-descriptions-itemlabel="所属省份">{{ loginUser['province'] }}</el-descriptions-item><el-descriptions-itemlabel="手机号码">{{ loginUser['phone'] }}</el-descriptions-item><el-descriptions-itemlabel="电子邮件">{{ loginUser['email'] }}</el-descriptions-item><el-descriptions-itemlabel="身份证号":span="2">{{ loginUser['idcard'] }}</el-descriptions-item><el-descriptions-itemlabel="用户描述":span="2"><el-cardstyle="height: 300px">{{ loginUser['info'] }}</el-card></el-descriptions-item></el-descriptions></el-col></el-row></div></template><stylescopedlang="scss">.personal-body{width: 80%; // 宽度 margin: 65px auto 0;//外边距 .avatar{width: 278px; // 宽度 height: 278px; // 高度 border-radius: 10%; // 圆角 }}</style>

4. 修改个人信息

心法:修改个人信息页面。

页面功能

功能描述
修改个人信息点击基本信息面板中的 “确认修改” 按钮修改个人信息
修改个人头像点击 “上传图标” 或直接拖拽文件,修改个人信息
修改账号密码点击修改密码面板中的 “确认修改” 按钮修改密码

效果图

在这里插入图片描述
武技:开发修改个人信息页面 views/personal/PersonalUpdate.vue
<scriptsetup>import router from"../../router/index.js";import MyForm from"../../components/MyForm.vue";import MyNav from"../../components/MyNav.vue";import MyUpload from"../../components/MyUpload.vue";import{reactive, ref}from"vue";import{updateApi}from"../../api/index.js";import{updatePasswordApi,UPLOAD_AVATAR_URL}from"../../api/ums/user.js";import{GENDER_OPTIONS,PROVINCE_OPTIONS,RULE,ZODIAC_OPTIONS}from"../../const/index.js";import{ElMessage}from"element-plus";// 获取当前登录的用户记录const loginUser =JSON.parse(sessionStorage.getItem('loginUser'));// 路径导航const navItems =[{icon:'House',label:'DashBoard',url:'/DashBoard'},{icon:'Edit',label:'修改个人信息'},];// 修改基本信息表单:表单项 + 表单值 + 表单规则let items =ref([{label:'账号',prop:'username',required:true,disabled:true},{label:'姓名',prop:'realname',required:true,disabled:true},{label:'手机号码',prop:'phone',required:true,disabled:true},{label:'身份证号',prop:'idcard',required:true,disabled:true},{label:'昵称',prop:'nickname',required:true,span:12},{label:'邮箱',prop:'email',required:true,span:12},{label:'性别',prop:'gender',required:true,type:'select',options:GENDER_OPTIONS,span:12},{label:'年龄',prop:'age',required:true,span:12,type:'number'},{label:'星座',prop:'zodiac',required:true,type:'select',options:ZODIAC_OPTIONS,span:12},{label:'省份',prop:'province',required:true,type:'select',options:PROVINCE_OPTIONS,span:12},{label:'描述',prop:'info',type:'textarea',rows:8},]);let params =reactive(loginUser);let rules ={nickname:RULE.NICKNAME,email:RULE.EMAIL,province:RULE.PROVINCE,info:RULE.INFO};// 修改密码表单:表单项 + 表单值 + 表单规则let updatePasswordItems =ref([{label:'原密码',prop:'oldPassword',type:'password',required:true,placeholder:'请输入原密码'},{label:'新密码',prop:'newPassword',type:'password',required:true,placeholder:'请输入新密码'},{label:'确认密码',prop:'rePassword',type:'password',required:true,placeholder:'请确认新密码'},]);let updatePasswordParams =reactive({id: loginUser['id']});let updatePasswordRules ={oldPassword:RULE.PASSWORD,newPassword:[RULE.PASSWORD[0],{validator:(rule, value, callback)=>{if(value === updatePasswordParams['oldPassword'])callback('新旧密码不能相同');elsecallback();},trigger:['blur','input']}],rePassword:[RULE.PASSWORD[0],{validator:(rule, value, callback)=>{if(value !== updatePasswordParams['newPassword'])callback('两次密码不一致');elsecallback();},trigger:['blur','input']}],};/** * 修改成功后 */functionupdateSuccess(){ElMessage('修改个人信息后需要重新登录!');setTimeout(()=> router.push('/'),1000);}</script><template><my-nav:items="navItems"/><el-rowclass="personal-update-body":gutter="20"><el-col:span="16"><el-cardclass="update-card"header="修改基本信息"><my-formtype="update":items="items":params="params":rules="rules":api="updateApi":args="{'module': 'user'}":callback="updateSuccess"/></el-card></el-col><el-col:span="8"><el-cardclass="upload-avatar-card"header="上传头像"><my-uploadname="avatarFile":url="UPLOAD_AVATAR_URL + '/' + loginUser['id']":callback="updateSuccess":autoUpload="true"/></el-card><el-cardclass="update-password-card"header="修改密码"><my-formtype="update":items="updatePasswordItems":params="updatePasswordParams":rules="updatePasswordRules":api="updatePasswordApi":callback="updateSuccess"/></el-card></el-col></el-row></template><stylescopedlang="scss">.personal-update-body{padding: 0 100px; // 内边距 margin-top: 22px; //外边距 overflow-y: scroll;// Y轴溢出滚动 .update-password-card{margin-top: 25px; // 上外边距 }}</style>

5. 换绑手机号码

心法:换绑手机号码页面。

页面功能

功能描述
解绑旧手机点击 “获取验证码” 按钮,向旧手机发送一条解绑验证码
点击 “确认解绑” 按钮,解绑旧手机
绑定新手机点击 “获取验证码” 按钮,向新手机发送一条解绑验证码
点击 “确认绑定” 按钮,绑定新手机

效果图

在这里插入图片描述
武技:开发换绑手机号码页面 views/personal/PersonalUpdatePhone.vue
<scriptsetup>import router from"../../router/index.js";import MyNav from"../../components/MyNav.vue";import{RULE}from"../../const/index.js";import{ElMessage}from"element-plus";import{ref, reactive}from"vue";import{checkUnboundVcodeApi, getBoundVcodeApi}from"../../api/ums/user.js";import{getUnboundVcodeApi, updatePhoneApi}from"../../api/ums/user.js";import{getResponseData}from"../../request/index.js";import{isEmpty, isNotNull}from"../../util/index.js";// 获取当前登录的用户记录let loginUser =JSON.parse(sessionStorage.getItem('loginUser'));// 路径导航const navItems =[{icon:'House',label:'DashBoard',url:'/DashBoard'},{icon:'Edit',label:'换绑手机号码'},];// 当前进度条步骤const stepActive =ref(1);// 解绑旧手机:表单 + 表单项 + 表单值 + 表单规则let unboundForm =ref();let unboundFormData =reactive({id: loginUser['id'],phone: loginUser['phone']});let unboundFormRules ={vcode:RULE.CODE};/** * 获取验证码 - 解绑旧手机 */functiongetUnboundVcode(){getUnboundVcodeApi(loginUser['id']).then(res=>{let data =getResponseData(res);if(isNotNull(data)){ unboundFormData.vcode = data;}});}/** * 解绑旧手机 */functionunboundPhone(){ unboundForm.value.validate(valid=>{if(valid){checkUnboundVcodeApi(loginUser['id'], unboundFormData.vcode).then(res=>{let data =getResponseData(res);if(data){ ElMessage.success('解绑成功!'); stepActive.value =2;}else{ ElMessage.warning('验证码错误!');}});}});}// 绑定新手机:表单 + 表单项 + 表单值 + 表单规则let boundForm =ref();let boundFormData =reactive({id: loginUser['id']});let boundFormRules ={phone:RULE.PHONE,vcode:RULE.CODE};/** * 获取验证码 - 绑定新手机 */functiongetBoundVcode(){if(isEmpty(boundFormData['phone'])){ ElMessage.warning('请输入手机号码!');return;}getBoundVcodeApi(boundFormData['phone']).then(res=>{let data =getResponseData(res);if(isNotNull(data)){ boundFormData.vcode = data;}});}/** * 绑定新手机 */functionboundPhone(){ boundForm.value.validate(valid=>{if(valid){let params ={id: loginUser['id'],phone: boundFormData['phone'],vcode: boundFormData['vcode'],};updatePhoneApi(params).then(res=>{let data =getResponseData(res);if(data){ ElMessage.success('绑定成功!'); stepActive.value =3;setTimeout(()=> router.push('/'),3000);}else{ ElMessage.warning('验证码错误!');}});}});}</script><template><my-nav:items="navItems"/><el-stepsclass="update-steps":active="stepActive"finish-status="success"><el-steptitle="步骤 1"description="解绑旧手机"/><el-steptitle="步骤 2"description="绑定新手机"/><el-steptitle="步骤 3"description="修改完成"/></el-steps><divclass="personal-update-phone-body"><el-cardv-if="stepActive === 1"header="解绑旧手机"><el-formclass="update-form":model="unboundFormData":rules="unboundFormRules"ref="unboundForm"status-icon><el-form-itemprop="phone"required><el-inputv-model="unboundFormData['phone']"prefix-icon="Phone"disabled/></el-form-item><el-form-itemprop="vcode"required><el-inputv-model="unboundFormData['vcode']"prefix-icon="Lock"clearableplaceholder="输入验证码 .."><template#append><el-buttontype="primary"@click="getUnboundVcode">获取验证码</el-button></template></el-input></el-form-item><el-buttontype="primary"@click="unboundPhone">确认解绑</el-button></el-form></el-card><el-cardv-if="stepActive === 2"header="绑定新手机"><el-formclass="update-form":model="boundFormData":rules="boundFormRules"ref="boundForm"status-icon><el-form-itemprop="phone"required><el-inputv-model="boundFormData['phone']"prefix-icon="Phone"clearableplaceholder="输入新手机号 .."/></el-form-item><el-form-itemprop="vcode"required><el-inputv-model="boundFormData['vcode']"prefix-icon="Lock"clearableplaceholder="输入验证码 .."><template#append><el-buttontype="primary"@click="getBoundVcode">获取验证码</el-button></template></el-input></el-form-item><el-buttontype="primary"@click="boundPhone">确认绑定</el-button></el-form></el-card><el-cardv-if="stepActive === 3"><h1class="success-tip">修改成功!3秒后自动跳转到登录页面 ..</h1></el-card></div></template><stylescopedlang="scss">.update-steps{margin: 65px auto 0; // 外边距 width: 80%; // 宽度 }.personal-update-phone-body{width: 50%; // 宽度 margin: 70px auto 0;// 外边距 .success-tip{text-align: center; // 文本居中 }}</style>
Java道经 - 项目 - MyLesson - 后台前端(一)


传送门:JP4-7-MyLesson后台前端(一)
传送门:JP4-7-MyLesson后台前端(二)
传送门:JP4-7-MyLesson后台前端(三)
传送门:JP4-7-MyLesson后台前端(四)
传送门:JP4-7-MyLesson后台前端(五)

Read more

面试必懂:流式数据前端渲染全指南(SSE/WebSocket+逐段渲染+问题兜底)

面试必懂:流式数据前端渲染全指南(SSE/WebSocket+逐段渲染+问题兜底) 在AI对话、实时日志、行情推送等场景中,流式数据渲染已成为提升用户体验的核心技术——它打破了“全量加载完再展示”的传统模式,通过服务端分批次推送、前端逐段渲染,实现类似“打字机”的即时反馈效果。本文结合实战经验,从技术选型、核心实现、优化方案到问题处理,全方位拆解流式渲染,同时适配面试答题逻辑,帮你既能落地实践,又能从容应对面试提问。 一、技术选型:WebSocket vs SSE 怎么选? 流式渲染的核心是服务端与前端的持续数据传输,主流方案有WebSocket和SSE(Server-Sent Events),二者适用场景差异显著,面试中需清晰说明选型逻辑。 对比维度WebSocketSSE(Server-Sent Events)面试选型结论通信方向双向交互(客户端↔服务端)单向推送(服务端→客户端)仅下行流式场景(AI回复、日志)

Unity WebGL 全屏与自适应踩坑实录:为什么你点两次才全屏?

在 Windows / Editor 环境里,我们通常会这样控制全屏: Screen.fullScreen = !Screen.fullScreen; 但当项目切到 WebGL 后,就会遇到各种奇怪问题: * 第一次点击没反应 * 有时需要点两次才能全屏 * 偶尔直接 abort(-1) * 不同浏览器行为还不一致 很多人第一反应是“是不是 Unity 的 bug”,但其实原因只有一个:WebGL 的全屏是浏览器行为,而不是 Unity 行为。 一、为什么 WebGL 下不能直接用 Screen.fullScreen? 浏览器对“进入全屏”有严格限制: * 必须由用户手势触发(点击 / 按键) * 不能在任意时机调用 * 不允许 Unity 在后台随意请求全屏 Screen.fullScreen 在 WebGL

黑马程序员java web学习笔记--后端进阶(二)SpringBoot原理

目录 1 配置优先级 2 Bean的管理 2.1 Bean的作用域 2.2 第三方Bean 3 SpringBoot原理 3.1 起步依赖 3.2 自动配置 3.2.1 实现方案 3.2.2 原理分析 3.2.3 自定义starter 1 配置优先级 SpringBoot项目当中支持的三类配置文件: * application.properties * application.yml ❤ * application.yaml 配置文件优先级排名(从高到低):properties配置文件 > yml配置文件 > yaml配置文件 虽然springboot支持多种格式配置文件,但是在项目开发时,推荐统一使用一种格式的配置。

【测试理论与实践】(十)Web 项目自动化测试实战:从 0 到 1 搭建博客系统 UI 自动化框架

【测试理论与实践】(十)Web 项目自动化测试实战:从 0 到 1 搭建博客系统 UI 自动化框架

目录 前言 一、项目背景与测试规划:先明确 "测什么" 和 "怎么测" 1.1 项目介绍 1.2 测试目标 1.3 测试范围与用例设计 编辑 二、环境搭建:3 步搞定自动化测试前置准备 2.1 安装核心依赖包 2.2 浏览器配置 2.3 项目目录结构设计 三、核心模块开发:封装公共工具,提高代码复用性 3.1 驱动管理与截图工具封装(common/Utils.py) 3.2 代码说明与优化点 四、测试用例开发: