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

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

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

文章目录

  • S01. 基础环境搭建
    • E01. 安装基础组件
      • 1. 路由管理器Router
      • 2. 状态管理器Vuex
      • 3. 样式预处理SCSS
      • 4. 异步请求Axios
      • 5. 框架ElementPlus
      • 6. 播放器XGPlayer
      • 7. 图表ECharts
    • E02. 封装通用组件
      • 1. 扩展通用工具util
      • 2. 封装常量工具const
    • E03. 封装API接口文件
      • 1. 封装用户相关请求
      • 2. 封装角色相关请求
      • 3. 封装菜单相关请求
      • 4. 封装课程相关请求
      • 5. 封装集次相关请求
      • 6. 封装横幅相关请求
      • 7. 封装订单相关请求
  • S02. 登录业务模块
    • E01. 系统登录模块
      • 1. 系统登录页面
      • 2. 添加路由守卫
    • E02. 系统主体页面
      • 1. 系统主体页面
      • 2. 系统仪表盘
      • 3. 查看个人信息
      • 4. 修改个人信息
      • 5. 换绑手机号码

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

项目相关模块

+-- @element-plus/icons-vue@2.3.1
+-- @vitejs/plugin-vue@5.1.5
+-- @vueuse/core@10.7.2
+-- axios@1.6.7
+-- echarts@5.4.3
+-- element-plus@2.5.3
+-- qs@6.13.0
+-- sass-embedded@1.77.8
+-- vite@5.4.11
+-- vue-router@4.0.3
+-- vue@3.5.12
+-- vuex@4.0.0
`-- xgplayer@3.0.11

项目起始结构

|_ ml-web|_ node_modules         # 项目依赖|_ public               # 存放第三方资源|_ src|   |_ api|   |   |_ index.js     # 请求工具(封装基本 CRUD 请求和拦截器)|   |_ assets|   |   |_ ml-web.ico   # 项目图标|   |_ 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配置文件(封装了登录状态变量)|   |_ 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 图标文件。
  3. 删除无用目录和文件。
  4. 优化 index.html 首页文件如下:
<!DOCTYPE html>  
<html lang="en" class="dark">  
<head>  <meta charset="UTF-8"/>  <link rel="icon" href="/ml-web.ico"/>  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>  <title>MyLesson管理平台</title>  
</head>  
<body>  
<div id="app"></div>  
<script type="module" src="src/main.js"></script>  
</body>  
</html>
  1. 在 vite.config.js 配置项目端口号为 24108:
import {defineConfig} from 'vite'  
import vue from '@vitejs/plugin-vue'  // https://vitejs.dev/config/  
export default defineConfig({  plugins: [vue()],  // 配置用户前台项目的IP和端口号  server: {  host: 'localhost',  port: 24108,  }  
})

E01. 安装基础组件

1. 路由管理器Router

武技:参考 JB3-8-Vue(一)- S01E02 局部安装 VueRouter 组件。

  1. 在 main.js 文件中配置 VueRouter 组件。
  2. 在 App.vue 文件中添加 <router-view> 标签。
  3. 初始化 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();  
});  export default router

2. 状态管理器Vuex

武技:参考 JB3-8-Vue(一)- S01E02 局部安装 Vuex 组件。

  1. 在 main.js 文件中配置 Vuex 组件。
  2. 初始化 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)}
});export default vuex;

3. 样式预处理SCSS

武技:参考 JB3-8-Vue(一)- S01E02 局部安装 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(二)- S02E01 安装 Axios 组件。

5. 框架ElementPlus

武技:参考 JB3-8-Vue(二)- S03E01 安装 ElementPlus 组件(基础库 + 图标库 + 暗黑库)。

  1. 在 main.js 文件中配置 ElementPlus 组件。
  2. 在 style.scss 文件中添加暗黑库适配样式,具体内容如下:
html.dark {img, .dark-img-bg {filter: brightness(0.8) saturate(1.25) // 滤镜: 暗黑模式图片的亮度和饱和度}.el-table th {background: #18222C !important; // th 背景色}
}

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 上传文件组件。

1. 扩展通用工具util

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

具体扩展内容如下:

/*** 性别代码处理:0->'女',1->'男',2->'保密'** @param genderCode 性别代码* @return string 对应的性别字符串,0女孩,1男孩,2保密* */
export function genderFormat(genderCode) {if (genderCode === '0' || genderCode === 0) return '女孩';if (genderCode === '1' || genderCode === 1) return '男孩';if (genderCode === '2' || genderCode === 2) return '保密';return '性别代码异常';
}/** 秒杀活动状态代码处理:根据秒杀活动状态代码返回秒杀活动状态字符串** @param status 秒杀活动状态代码* @return 对应的秒杀活动状态字符串,0未开始,1已开始,2已结束* */
export function seckillStatusFormat(status) {if (status === '0' || status === 0) return '未开始';if (status === '1' || status === 1) return '已开始';if (status === '2' || status === 2) return '已结束';return '秒杀活动状态代码异常';
}/** 订单状态代码处理:根据订单状态代码返回订单状态字符串** @param stateCode 订单状态代码* @return 对应的字符串文案,0未付款,1已付款* */
export function orderStateFormat(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 '订单状态代码异常';
}/** 订单支付方式代码处理:根据订单支付方式代码返回订单支付方式字符串** @param typeCode 订单支付方式代码* @return 对应的字符串文案,0未支付,1支付宝,2微信,3其他* */
export function orderPayTypeFormat(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 '银行卡';if (typeCode === '4' || typeCode === 4) return '其他';return '订单支付方式代码异常';
}

2. 封装常量工具const

武技:开发常量工具 const/index.js 文件

// 环境IP地址
const HOST = 'http://localhost';
export const GATEWAY_HOST = `${HOST}:24101`;
export const SOCKET_SERVER = `${HOST}:24107/api/v1/barrage`;
export const USER_EXCEL_HOST = `${GATEWAY_HOST}/user-server/api/v1/user/excel`;
export const EPISODE_EXCEL_HOST = `${GATEWAY_HOST}/course-server/v1/episode/excel`;
export const ORDER_EXCEL_HOST = `${GATEWAY_HOST}/order-server/api/v1/order/excel`;// Minio函数
export const MINIO_HOST = `http://192.168.40.77:9001/mylesson`;
export const MINIO_AVATAR = url => MINIO_HOST + '/avatar/' + url;
export const MINIO_BANNER = url => MINIO_HOST + '/banner/' + url;
export const MINIO_COURSE_COVER = url => MINIO_HOST + '/course-cover/' + url;
export const MINIO_COURSE_SUMMARY = url => MINIO_HOST + '/course-summary/' + url;
export const MINIO_EPISODE_VIDEO = url => MINIO_HOST + '/episode-video/' + url;
export const MINIO_EPISODE_VIDEO_COVER = url => MINIO_HOST + '/episode-video-cover/' + url;// 表单规则
export const RULE = {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之间'}],
}// 项目环境信息
export const PROJECT_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 都将是你理想的在线学习伙伴。',
};// 项目技术栈信息
export const PROJECT_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: '最新版'}
];// 用户性别下拉菜单选项
export const GENDER_OPTIONS = [{label: '女孩', value: 0},{label: '男孩', value: 1},{label: '保密', value: 2},
];// 用户星座下拉菜单选项
export const ZODIAC_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: '双鱼座'},
];// 秒杀活动状态下拉菜单选项
export const SECKILL_STATUS_OPTIONS = [{label: '未开始', value: 0},{label: '已开始', value: 1},{label: '已结束', value: 2},
];// 省份下拉菜单可选项(包含中国全部省级行政区)
export const PROVINCE_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: '其他'},
];

E03. 封装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/index.js

import axios from 'axios';
import {GATEWAY_HOST} from '../const';
import {isNotEmpty} from "../util";
import {STATUS} from "../request";/* =============== Axios实例配置 =============== */// 创建Axios实例: 配置请求前缀和超时时间
export const GATEWAY_AXIOS = axios.create({baseURL: GATEWAY_HOST,timeout: 5000
});/* =============== 基本Axios请求 =============== *//** API前缀处理:根据模块名称返回API前缀** @param module 模块名称,如 user, course 等* @return 对应的API前缀,如 /user-server/api/v1/user 等,末尾无 / 符号* */
export function apiPrefixFormat(module) {const USER_MODULES = ['menu', 'role', 'user'];const COURSE_MODULES = ['category', 'comment', 'course', 'episode', 'report', 'season'];const SALE_MODULES = ['article', 'banner', 'coupons', 'notice', 'seckill', 'seckillDetail'];const ORDER_MODULES = ['cart', 'order', 'orderDetail'];let microServiceName = '';if (USER_MODULES.indexOf(module) !== -1) {microServiceName = `/user-server`;} else if (COURSE_MODULES.indexOf(module) !== -1) {microServiceName = `/course-server`;} else if (SALE_MODULES.indexOf(module) !== -1) {microServiceName = `/sale-server`;} else if (ORDER_MODULES.indexOf(module) !== -1) {microServiceName = `/order-server`;}return `${microServiceName}/api/v1/${module}`;
}// POST - 添加一条记录
export function insertApi(params, args) {return GATEWAY_AXIOS.post(`${apiPrefixFormat(args['module'])}/insert`, params)
}// GET - 根据主键查询
export function selectApi(id, args) {return GATEWAY_AXIOS.get(`${apiPrefixFormat(args['module'])}/select/${id}`)
}// GET - 分页查询记录
export function pageApi(params, args) {return GATEWAY_AXIOS.get(`${apiPrefixFormat(args['module'])}/page`, {params: params});
}// GET - 查询简单列表
export function simpleListApi(params, args) {return GATEWAY_AXIOS.get(`${apiPrefixFormat(args['module'])}/simpleList`);
}// PUT - 根据主键修改
export function updateApi(params, args) {return GATEWAY_AXIOS.put(`${apiPrefixFormat(args['module'])}/update`, params)
}// DELETE - 根据主键删除
export function deleteApi(id, args) {return GATEWAY_AXIOS.delete(`${apiPrefixFormat(args['module'])}/delete/${id}`)
}// DELETE - 根据主键批删
export function deleteBatchApi(ids, args) {return GATEWAY_AXIOS.delete(`${apiPrefixFormat(args['module'])}/deleteBatch?ids=${ids}`)
}// GET - 下载Excel报表
export function excelApi(url, fileName) {// 自动拼接后缀fileName = fileName.endsWith('.xlsx') ? fileName : fileName + '.xlsx';// 发送下载请求:响应类型为blobreturn GATEWAY_AXIOS.get(url, {responseType: 'blob'}).then(res => {// 借助超链接标签完成下载功能let a = document.createElement('a');a.style.display = 'none';a.href = URL.createObjectURL(new Blob([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)
);// 在接收到响应之后执行
GATEWAY_AXIOS.interceptors.response.use(resp => {// 若后台响应 “Token即将过期”,则续期 Token 并重新发送请求if (resp.data.code === STATUS.TOKEN_EXPIRING_SOON) {const newToken = resp.data.data;sessionStorage.setItem("token", newToken);GATEWAY_AXIOS.defaults.headers['token'] = newToken;return GATEWAY_AXIOS.request(resp.config);}// 若不需要 Token 续期,则直接放行响应return resp;},err => Promise.reject(err)
);

测试代码 App.vue(测试完毕记得删除,但 router-view 标签需要保留):

<script setup>
import {insertApi, pageApi, selectApi} from "./api";
import {simpleListApi, deleteApi, deleteBatchApi} from "./api";
const args = {module: 'role'};
</script><template><el-button @click="insertApi({title: 'title', info: 'info', idx: 999}, args)">添加</el-button><el-button @click="selectApi(1, args)">查询</el-button><el-button @click="simpleListApi(null, args)">全查</el-button><el-button @click="pageApi({pageNum: 1, pageSize: 2}, args)">分页</el-button><el-button @click="deleteApi(9, args)">删除</el-button><el-button @click="deleteBatchApi([9, 10], args)">批删</el-button><router-view/>
</template>

1. 封装用户相关请求

武技:封装用户相关请求接口文件 api/ums/user.js

import {GATEWAY_AXIOS, apiPrefixFormat} from "../index.js";
import {GATEWAY_HOST} from "../../const";// 上传头像地址
export const UPLOAD_AVATAR_URL = GATEWAY_HOST + '/user-server/api/v1/user/uploadAvatar/';// POST - 按账号密码登录
export function loginByAccountApi(params) {return GATEWAY_AXIOS.post(`${apiPrefixFormat('user')}/loginByAccount`, params)
}// GET - 用户统计数据
export function statisticsApi(params) {return GATEWAY_AXIOS.get(`${apiPrefixFormat('user')}/statistics`, params)
}// GET - 获取手机验证码
export function getVcodeApi(phone) {return GATEWAY_AXIOS.get(`${apiPrefixFormat('user')}/getVcode/${phone}`)
}// GET - 查询解绑验证码
export function getUnboundVcodeApi(id) {return GATEWAY_AXIOS.get(`${apiPrefixFormat('user')}/getUnboundVcode/${id}`)
}// GET - 校验解绑验证码
export function checkUnboundVcodeApi(id, vcode) {return GATEWAY_AXIOS.get(`${apiPrefixFormat('user')}/checkUnboundVcode/${id}/${vcode}`)
}// GET - 查询解绑验证码
export function getBoundVcodeApi(phone) {return GATEWAY_AXIOS.get(`${apiPrefixFormat('user')}/getBoundVcode/${phone}`)
}// PUT - 修改用户手机号码
export function updatePhoneApi(params) {return GATEWAY_AXIOS.put(`${apiPrefixFormat('user')}/updatePhone`, params)
}// PUT - 根据主键修改密码
export function updatePasswordApi(params) {return GATEWAY_AXIOS.put(`${apiPrefixFormat('user')}/updatePassword`, params)
}// PUT - 根据主键重置密码
export function resetPasswordApi(id) {return GATEWAY_AXIOS.put(`${apiPrefixFormat('user')}/resetPassword/${id}`)
}

2. 封装角色相关请求

武技:封装角色相关请求接口文件 api/ums/role.js

import {GATEWAY_AXIOS, apiPrefixFormat} from "../index.js";// GET - 按用户主键查询用户的全部角色ID列表
export function listRoleIdsByUserIdApi(userId) {return GATEWAY_AXIOS.get(`${apiPrefixFormat('role')}/listRoleIdsByUserId/${userId}`)
}// PUT - 按用户主键修改用户的角色列表
export function updateRolesByUserIdApi(userId, roleIds) {return GATEWAY_AXIOS.put(`${apiPrefixFormat('role')}/updateRolesByUserId?userId=${userId}&roleIds=${roleIds}`)
}

3. 封装菜单相关请求

武技:封装菜单相关请求接口文件 api/ums/menu.js

import {GATEWAY_AXIOS, apiPrefixFormat} from "../index.js";
// GET - 按角色主键查询角色的全部菜单ID列表
export function listMenuIdsByRoleIdApi(roleId) {return GATEWAY_AXIOS.get(`${apiPrefixFormat('menu')}/listMenuIdsByRoleId/${roleId}`)
}// PUT - 按角色主键修改角色的菜单列表
export function updateMenusByRoleIdApi(roleId, menuIds) {return GATEWAY_AXIOS.put(`${apiPrefixFormat('menu')}/updateMenusByRoleId?roleId=${roleId}&menuIds=${menuIds}`)
}

4. 封装课程相关请求

武技:封装课程相关请求接口文件 api/cms/course.js

import {GATEWAY_HOST} from "../../const";// 上传课程封面图片地址
export const UPLOAD_COURSE_COVER_URL = GATEWAY_HOST + '/course-server/api/v1/course/uploadCover/';
// 上传课程摘要图片地址
export const UPLOAD_COURSE_SUMMARY_URL = GATEWAY_HOST + '/course-server/api/v1/course/uploadSummary/';

5. 封装集次相关请求

武技:封装集次相关请求接口文件 api/cms/episode.js

import {GATEWAY_HOST} from "../../const";// 上传集次视频地址
export const UPLOAD_EPISODE_VIDEO_URL = GATEWAY_HOST + '/course-server/api/v1/episode/uploadVideo/';
// 上传集次视频封面图片地址
export const UPLOAD_EPISODE_VIDEO_COVER_URL = GATEWAY_HOST + '/course-server/api/v1/episode/uploadVideoCover/';

6. 封装横幅相关请求

武技:封装横幅相关请求接口文件 api/sms/banner.js

import {GATEWAY_HOST} from "../../const";// 上传轮播图片地址
export const UPLOAD_BANNER_URL = GATEWAY_HOST + '/sale-server/api/v1/banner/uploadBanner/';

7. 封装订单相关请求

武技:封装订单相关请求接口文件 api/oms/order.js

import {GATEWAY_AXIOS, apiPrefixFormat} from "../index.js";// GET - 订单统计数据
export function statisticsApi(params) {return GATEWAY_AXIOS.get(`${apiPrefixFormat('order')}/statistics`, params)
}

S02. 登录业务模块

E01. 系统登录模块

武技:在 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';const router = createRouter({history: createWebHashHistory(),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 背景图片。

<script setup>
import router from "../router";
import vuex from "../vuex";
import {ElMessage, ElNotification} from "element-plus";
import {RULE} from "../const/index.js";
import {onMounted, shallowReactive, shallowRef} from "vue";
import {useDark, useToggle} from "@vueuse/core";
import {getResponseData} from "../request/index.js";
import {isNotNull} from "../util/index.js";
import {loginByAccountApi} from "../api/ums/user.js";/* ==================== 用户登录 ==================== */// 表单 + 表单数据 + 表单规则 todo: 上线后将默认值删除
let loginForm = shallowRef();
let loginFormData = shallowReactive({username: 'admin', password: '123456789'});
let loginFormRules = {username: RULE.USERNAME, password: RULE.PASSWORD};/*** 根据账号密码登录系统** 1. 验证表单,验证通过后发送登录请求。* 2. vuex修改登录状态。* 3. 存储Token令牌,用户记录,用户角色列表和用户菜单记录。* 4. 路由到 Main 页面。*/
function login() {loginForm.value.validate(valid => {if (valid) {// 同步发送登录请求loginByAccountApi(loginFormData).then(res => {let data = getResponseData(res);if (isNotNull(data)) {ElMessage.success('登录成功!');let token = data['token'];let loginUser = JSON.stringify(data['user']);let loginMenus = JSON.stringify(data['menus']);let loginRoleTitles = JSON.stringify(data['roleTitles']);vuex.dispatch('setLoginFlag', true);sessionStorage.setItem('token', token);sessionStorage.setItem('loginUser', loginUser);sessionStorage.setItem('loginMenus', loginMenus);sessionStorage.setItem('loginRoleTitles', loginRoleTitles);router.push('/Main');}});}});
}/* ==================== 重置表单 ==================== *//** 重置表单 */
function resetForm() {loginForm.value.resetFields();
}/* ==================== 忘记密码 ==================== *//** ElNotification右上角通知:'员工测试账号: admin / 123456789' */
function forgetPassword() {ElNotification.info({title: '通知列表',message: '测试账号: admin / 123456789',position: 'top-right',});
}/* ==================== 加载函数 ==================== *//*** 1. 使用暗黑模式。* 2. 清空SessionStorage。* 3. vuex修改登录状态。*/
onMounted(() => {useToggle(useDark());sessionStorage.clear();vuex.dispatch('setLoginFlag', false);
});
</script><template><section class="login-body"><el-card class="login-card" header="《我的课堂》后台管理系统"><el-form class="login-form" ref="loginForm" status-icon :model="loginFormData" :rules="loginFormRules"><el-form-item prop="username" required><el-input v-model="loginFormData['username']" suffix-icon="User" clearable placeholder="输入账号 .."/></el-form-item><el-form-item prop="password" required><el-input v-model="loginFormData['password']" suffix-icon="Lock" clearable placeholder="输入密码 .." show-password/></el-form-item><el-button class="login-btn" type="primary" @click="login">管理员登录</el-button><el-checkbox class="remember-cbx" label="记住账号" size="small"/><el-button class="forget-btn" link size="small" @click="forgetPassword">忘记密码</el-button><el-button class="reset-btn" link size="small" type="warning" @click="resetForm">重置内容</el-button></el-form></el-card></section>
</template><style scoped lang="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. 添加路由守卫

import {ElMessage} from "element-plus";  
import vuex from "../vuex/index.js";/** 路由前置守卫:每次转发路由前执行的函数* * param to: 来源地址* param from: 目标地址* next: 放行函数*/
router.beforeEach((to, from, next) => {// 登录页面,测试页面,或已登录状态,均直接放行if (to.path === '/' || to.path === '/Test' || vuex.state['loginFlag']) {next();}// 其余情况直接跳转回登录页面else {ElMessage.warning('请先登录!');setTimeout(() => next('/'), 2000);}
});

E02. 系统主体页面

1. 系统主体页面

心法:系统主体页面

在这里插入图片描述

武技:开发开发系统主体页面 views/Main.vue

<script setup>
import router from "../router/index.js";
import MyIcon from "../components/MyIcon.vue";
import {ElNotification} from "element-plus";
import {MINIO_HOST, PROJECT_INFO, PROJECT_SKILLS} from "../const/index.js";
import {shallowRef} from "vue";// 当前登录的用户信息
const loginUser = JSON.parse(sessionStorage.getItem('loginUser'));
// 当前登录的菜单列表
const menus = JSON.parse(sessionStorage.getItem('loginMenus'));
// 当前登录的用户头像
const avatar = MINIO_HOST + '/avatar/' + loginUser['avatar'];
// 项目LOGO
const logo = MINIO_HOST + '/logo.jpg';/* ==================== 功能菜单 ==================== */// 当前选中菜单的index值:默认选中当前路由路径
let currentMenuIndex = shallowRef(router.currentRoute.value['path']);
// 左侧菜单列表是否折叠:向左收缩
const isCollapse = shallowRef(false);/* ==================== 项目信息 ==================== */// 项目信息抽屉
const projectInfoDrawer = shallowRef();/** 打开项目信息抽屉 */
function openProjectInfoDrawer() {projectInfoDrawer.value = true;
}/* ==================== 项目技术栈 ==================== */// 项目技术栈抽屉
const projectSkillDrawer = shallowRef();/** 打开项目技术栈抽屉 */
function openProjectSkillDrawer() {projectSkillDrawer.value = true;
}/* ==================== 系统日历 ==================== */// 日历抽屉,日历数据(本地时间)
const calendarDrawer = shallowRef();
let calendarData = shallowRef(new Date());/** 打开日历抽屉 */
function openCalendarDrawer() {calendarDrawer.value = true;
}/* ==================== 系统通知 ==================== *//** ElNotification右下角通知:'暂无通知消息' */
function notify() {ElNotification.info({title: '通知列表', message: '暂无通知消息', position: 'bottom-right'});
}/* ==================== 路径跳转 ==================== */function toPersonal() {router.push('/Personal');
}function toPersonalUpdate() {router.push('/PersonalUpdate');
}function toPersonalUpdatePhone() {router.push('/PersonalUpdatePhone');
}/* ==================== 退出登录 ==================== */function logout() {router.push('/');
}</script><template><el-container class="main-body" v-if="menus"><el-aside class="main-body-left" width="collapse" max-width="200px"><el-menu class="menus-menu el-menu-vertical-demo" unique-opened router :collapse="isCollapse" :default-active="currentMenuIndex"><el-image class="logo" :src="logo"/><el-menu-item class="house-item" index="/DashBoard" title="回到后台项目首页"><my-icon icon="House" label="DashBoard"/></el-menu-item><el-sub-menu class="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-item class="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-container class="main-body-right"><el-header class="main-body-right-head"><el-row class="is-align-middle"><el-col class="fold-expand" :span="2"><el-radio-group v-model="isCollapse"><el-radio-button :label="!isCollapse"><my-icon size="20" :icon="!isCollapse ? 'Fold' : 'Expand'"/></el-radio-button></el-radio-group></el-col><el-col class="project-title-col" :span="7"><el-popover width="500" :content="PROJECT_INFO.info" placement="bottom-start" trigger="click"><template #reference>{{ PROJECT_INFO.title }}</template></el-popover></el-col><el-col class="operation-btn-col" :span="6" :offset="5"><el-divider direction="vertical"/><el-tooltip content="全局搜索"><el-button icon="search" size="small" round @click=""/></el-tooltip><el-tooltip content="系统通知"><el-button icon="bell" size="small" round @click="notify"/></el-tooltip><el-tooltip content="项目基本信息"><el-button icon="list" size="small" round @click="openProjectInfoDrawer"/></el-tooltip><el-tooltip content="项目技术信息"><el-button icon="management" size="small" round @click="openProjectSkillDrawer"/></el-tooltip><el-tooltip content="系统日历"><el-button icon="calendar" size="small" round @click="openCalendarDrawer"/></el-tooltip><el-divider direction="vertical"/></el-col><el-col class="nickname-col" :span="3" v-if="loginUser['nickname']">{{ loginUser['nickname'] }}</el-col><el-col class="avatar-col" :span="1" v-if="loginUser['avatar']"><el-dropdown trigger="click"><span class="el-dropdown-link"><el-avatar class="avatar" :size="45" :src="avatar"/></span><template #dropdown><el-dropdown-menu><el-dropdown-item icon="InfoFilled" @click="toPersonal">查看个人信息</el-dropdown-item><el-dropdown-item icon="Edit" @click="toPersonalUpdate">修改个人信息</el-dropdown-item><el-dropdown-item icon="Phone" @click="toPersonalUpdatePhone">换绑手机号码</el-dropdown-item><el-dropdown-item icon="WarnTriangleFilled" @click="logout"><el-text type="danger">退出登录</el-text></el-dropdown-item></el-dropdown-menu></template></el-dropdown></el-col></el-row></el-header><el-main class="main-body-right-main"><router-view/></el-main></el-container></el-container><el-drawer title="项目系统信息" v-model="projectInfoDrawer" size="50%"><el-descriptions border column="1"><el-descriptions-item v-for="(v, k) in PROJECT_INFO" :key="k" :label="k">{{ v }}</el-descriptions-item></el-descriptions></el-drawer><el-drawer title="项目技术栈信息" v-model="projectSkillDrawer" size="50%"><el-descriptions border column="1"><el-descriptions-item v-for="item in PROJECT_SKILLS" :key="item['label']" :label="item['label']">{{ item['value'] }} ({{item['version']}})</el-descriptions-item></el-descriptions></el-drawer><el-drawer title="系统日历" v-model="calendarDrawer" size="50%"><el-calendar v-model="calendarData"/></el-drawer>
</template><style scoped lang="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

<script setup>
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. 绘制饼图*/
async function selectUserStatistics() {// 获取用户统计组件数据,包括今日用户,今年用户,用户日增,用户年增和用户性别比例数据let userStatisticsData = getResponseData(await userStatisticsApi());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. 绘制柱图*/
async function selectOrderStatistics() {// 获取订单统计组件数据,包括今日订单,今年订单,订单日增,订单年增和支付方式统计数据let orderStatisticsData = getResponseData(await orderStatisticsApi());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><section class="board-head"><el-row :gutter="16"><el-col :span="6" v-for="statisticCard in statisticCards"><div class="statistic-card"><el-statistic class="statistic-body" :value="statisticCard['value']"><template #title><el-link icon="InfoFilled" :underline="false">&nbsp;{{ statisticCard['title'] }}</el-link></template></el-statistic><div class="statistic-footer"><div class="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><section class="board-body"><el-row :gutter="16"><el-col class="chart-board" :span="12"><el-divider><el-button link icon="PieChart">性别 - 用户数 · 图例</el-button></el-divider><div id="genderBoard" class="board"/></el-col><el-col class="chart-board" :span="12"><el-divider><el-button link icon="Histogram">支付方式 - 订单数 · 图例</el-button></el-divider><div id="payTypeBoard" class="board"/></el-col></el-row></section>
</template><style scoped lang="scss">
.board-head {margin-bottom: 40px; // 下外边距.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

<script setup>
import {MINIO_AVATAR} from "../../const";
import {dateFormat, genderFormat} from "../../util";
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"/><el-divider/><div class="personal-body"><el-row :gutter="50"><el-col :span="8"><el-image class="avatar" :src="avatar"></el-image><el-divider/><el-descriptions column="1"><el-descriptions-item label="登录账号">{{ loginUser['username'] }}</el-descriptions-item><el-descriptions-item label="真实姓名">{{ loginUser['realname'] }}</el-descriptions-item><el-descriptions-item label="用户昵称">{{ loginUser['nickname'] }}</el-descriptions-item><el-descriptions-item label="创建时间">{{ dateFormat(loginUser['created']) }}</el-descriptions-item><el-descriptions-item label="修改时间">{{ dateFormat(loginUser['updated']) }}</el-descriptions-item></el-descriptions></el-col><el-col :span="16"><el-descriptions title="当前用户详细信息" border column="2"><el-descriptions-item label="用户年龄" width="120">{{ loginUser['age'] }}</el-descriptions-item><el-descriptions-item label="用户性别" width="120">{{genderFormat(loginUser['gender'])}}</el-descriptions-item><el-descriptions-item label="用户星座">{{ loginUser['zodiac'] }}</el-descriptions-item><el-descriptions-item label="所属省份">{{ loginUser['province'] }}</el-descriptions-item><el-descriptions-item label="手机号码">{{ loginUser['phone'] }}</el-descriptions-item><el-descriptions-item label="电子邮件">{{ loginUser['email'] }}</el-descriptions-item><el-descriptions-item label="身份证号" :span="2">{{ loginUser['idcard'] }}</el-descriptions-item><el-descriptions-item label="用户描述" :span="2"><el-card style="height: 300px">{{ loginUser['info'] }}</el-card></el-descriptions-item></el-descriptions></el-col></el-row></div>
</template><style scoped lang="scss">
.personal-body {margin: 20px 100px 0; // 20px上外边距,自动水平居中.avatar {width: 278px; // 宽度height: 278px; // 高度border-radius: 10%; // 圆角}
}
</style>

4. 修改个人信息

心法:修改个人信息页面

在这里插入图片描述

武技:开发修改个人信息页面 views/personal/PersonalUpdate.vue

<script setup>
import MyForm from "../../components/MyForm.vue";
import MyNav from "../../components/MyNav.vue";
import MyUpload from "../../components/MyUpload.vue";
import {GENDER_OPTIONS, PROVINCE_OPTIONS, RULE, ZODIAC_OPTIONS} from "../../const";
import {getResponseData} from "../../request";
import {onMounted, reactive, ref} from "vue";
import {simpleListApi, updateApi} from "../../api/index.js";
import {ElMessage} from "element-plus";
import {updatePasswordApi, UPLOAD_AVATAR_URL} from "../../api/ums/user.js";
import router from "../../router";// 获取当前登录的用户记录
const loginUser = JSON.parse(sessionStorage.getItem('loginUser'));
// 所在部门下拉菜单选项
let deptOptions = ref([]);// 路径导航
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 = {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('新旧密码不能相同');else callback();},trigger: ['blur', 'input']}],rePassword: [RULE.PASSWORD[0], {validator: (rule, value, callback) => {if (value !== updatePasswordParams['newPassword']) callback('两次密码不一致');else callback();},trigger: ['blur', 'input']}],
};/* ==================== 修改成功后 ==================== */function updateSuccess() {ElMessage('修改个人信息后需要重新登录!');setTimeout(() => router.push('/'), 1000);
}</script><template><my-nav :items="navItems"/><el-row :gutter="20" class="personal-update-body"><el-col :span="16"><el-card class="update-card" header="修改基本信息"><my-form type="update" :items="items" :params="params" :rules="rules" :api="updateApi" :args="{'module': 'user'}" :callback="updateSuccess"/></el-card></el-col><el-col :span="8"><el-card class="upload-avatar-card" header="上传头像"><my-upload :url="UPLOAD_AVATAR_URL + '/' + loginUser['id']" name="avatarFile" :callback="updateSuccess" :autoUpload="true"/></el-card><el-card class="update-password-card" header="修改密码"><my-form type="update" :items="updatePasswordItems" :params="updatePasswordParams" :rules="updatePasswordRules" :api="updatePasswordApi" :callback="updateSuccess"/></el-card></el-col></el-row>
</template><style scoped lang="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

<script setup>
import router from "../../router/index.js";
import MyNav from "../../components/MyNav.vue";
import {ref, shallowReactive, shallowRef} from "vue";
import {RULE} from "../../const/index.js";
import {ElMessage} from "element-plus";
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 = shallowRef();
let unboundFormData = shallowReactive({id: loginUser['id'], phone: loginUser['phone']});
let unboundFormRules = {vcode: RULE.CODE};/* 获取解绑验证码 */
function getUnboundVcode() {getUnboundVcodeApi(loginUser['id']).then(res => {let data = getResponseData(res);if (isNotNull(data)) {unboundFormData.vcode = data;}});
}/* 解绑旧手机 */
function unboundPhone() {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 = shallowRef();
let boundFormData = shallowReactive({id: loginUser['id']});
let boundFormRules = {phone: RULE.PHONE, vcode: RULE.CODE};/* 获取绑定验证码 */
function getBoundVcode() {if (isEmpty(boundFormData['phone'])) {ElMessage.warning('请输入手机号码!');return;}getBoundVcodeApi(boundFormData['phone']).then(res => {let data = getResponseData(res);if (isNotNull(data)) {boundFormData.vcode = data;}});
}/* 绑定旧手机 */
function boundPhone() {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-divider/><el-steps class="update-steps" :active="stepActive" finish-status="success"><el-step title="步骤 1" description="解绑旧手机"/><el-step title="步骤 2" description="绑定新手机"/><el-step title="步骤 3" description="修改完成"/></el-steps><div class="personal-update-phone-body"><el-card v-if="stepActive === 1" header="解绑旧手机"><el-form class="update-form" ref="unboundForm" status-icon :model="unboundFormData" :rules="unboundFormRules"><el-form-item prop="phone" required><el-input v-model="unboundFormData['phone']" prefix-icon="Phone" disabled/></el-form-item><el-form-item prop="vcode" required><el-input v-model="unboundFormData['vcode']" prefix-icon="Lock" clearable placeholder="输入验证码 .."><template #append><el-button type="primary" @click="getUnboundVcode">获取验证码</el-button></template></el-input></el-form-item><el-button type="primary" @click="unboundPhone">确认解绑</el-button></el-form></el-card><el-card v-if="stepActive === 2" header="绑定新手机"><el-form class="update-form" ref="boundForm" status-icon :model="boundFormData" :rules="boundFormRules"><el-form-item prop="phone" required><el-input v-model="boundFormData['phone']" prefix-icon="Phone" clearable placeholder="输入新手机号 .."/></el-form-item><el-form-item prop="vcode" required><el-input v-model="boundFormData['vcode']" prefix-icon="Lock" clearable placeholder="输入验证码 .."><template #append><el-button type="primary" @click="getBoundVcode">获取验证码</el-button></template></el-input></el-form-item><el-button type="primary" @click="boundPhone">确认绑定</el-button></el-form></el-card><el-card v-if="stepActive === 3"><h1 class="success-tip">修改成功!3秒后自动跳转到登录页面 ..</h1></el-card></div>
</template><style scoped lang="scss">.update-steps {margin: auto; // 外边距width: 80%; // 宽度
}.personal-update-phone-body {width: 50%; // 宽度margin: 70px auto 0; // 外边距.success-tip {text-align: center; // 文本居中}
}
</style>

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


文章转载自:

http://RG2dqOXB.ymwcs.cn
http://mRMGcz6Q.ymwcs.cn
http://Xknuc6c1.ymwcs.cn
http://3bPO1fTb.ymwcs.cn
http://0LOJ9IUq.ymwcs.cn
http://ehoKVYU1.ymwcs.cn
http://p2IRV1hu.ymwcs.cn
http://y3rnMQPb.ymwcs.cn
http://o7pdygom.ymwcs.cn
http://GXBJjD6Z.ymwcs.cn
http://5z1bSlYS.ymwcs.cn
http://5jqKce9l.ymwcs.cn
http://ra2J6AAe.ymwcs.cn
http://OWCZDBJa.ymwcs.cn
http://aWkwkSH4.ymwcs.cn
http://Eq2toONq.ymwcs.cn
http://gWEEDZk6.ymwcs.cn
http://GD7EZd4M.ymwcs.cn
http://beONaFSo.ymwcs.cn
http://k8jgBpRD.ymwcs.cn
http://xRZULn5B.ymwcs.cn
http://EpiA1whw.ymwcs.cn
http://8psE6fp6.ymwcs.cn
http://CIEN376B.ymwcs.cn
http://hVdUou0i.ymwcs.cn
http://HqfAFiuB.ymwcs.cn
http://qKLUVtZR.ymwcs.cn
http://it2M11f8.ymwcs.cn
http://iQn0nuCX.ymwcs.cn
http://rZuow1yz.ymwcs.cn
http://www.dtcms.com/a/369071.html

相关文章:

  • PPIO上线kimi-k2-0905,编码能力大幅提升
  • UniApp 页面通讯方案全解析:从 API 到状态管理的最佳实践
  • 嵌入式|Linux中打开视频流的两种方式V4l2和opencv
  • VBA 中的 Excel 工作表函数
  • Unix/Linux 平台通过 IP 地址获取接口名的 C++ 实现
  • EXCEL列数据前面补零
  • Big Data Analysis
  • 拿到一组数据在mars3d上渲染报错排查思路
  • 力扣hot100:搜索二维矩阵 II(常见误区与高效解法详解)(240)
  • 《从报错到运行:STM32G4 工程在 Keil 中的头文件配置与调试实战》
  • Meta AI眼镜Hypernova量产临近,微美全息构筑护城河引领人机交互变革浪潮
  • SQL表一共有几种写入方式
  • Vue3源码reactivity响应式篇之ReactiveEffect类
  • C++中的Reactor和Proactor模型进行系统性解析
  • 调试技巧:Chrome DevTools 与 Node.js Inspector
  • 双碳目标下的24小时分时综合能源系统低碳优化调度:基于 Matlab/YALMIP/CPLEX的方法与仿真
  • 告别 “无效阅读”!2025 开学季超赞科技书单,带孩子解锁 AI、编程新技能
  • 鸿蒙Next的UI国际化与无障碍适老化实践:构建全球包容的数字世界
  • react 全屏页面自适应操作,注意问题
  • 计算机毕设选题:基于Python数据挖掘的高考志愿推荐系统
  • PCL中的特征提取
  • 2025年TOP8最佳GNSS位移监测设备权威推荐榜单
  • 告别研发内耗!这款免费项目管理工具,让团队效率实现 3 倍跃升
  • 【智慧城市】2025年中国地质大学(武汉)暑期实训优秀作品(3):基于Mapbox GL JS 构建的城市三维可视化系统
  • 图像处理:实现多图点重叠效果
  • 在Kingbase数据库中指定用户模式并查看拥有的数据库模式
  • 【TXT】用 Python 实现超漂亮的 HTML 两栏文本对比工具(支持行内差异高亮)
  • VOGUE二十周年女演员群像封面
  • 使用pytorch创建/训练/推理OCR模型
  • 从音频到文本实现高精度离线语音识别