# vue3-big-event **Repository Path**: windstarry/vue3-big-event ## Basic Information - **Project Name**: vue3-big-event - **Description**: 学习黑马程序大事件后台管理系统源码 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2023-12-14 - **Last Updated**: 2024-01-24 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # vue3-big-event项目从零搭建 ## 一、前端环境搭建 ### 1. 使用pnpm创建项目 ```shell pnpm create vue ``` 配置项目名称、选择默认模板、选择默认风格 ```text √ 请输入项目名称: ... vue3-big-event √ 是否使用 TypeScript 语法? ... 否 / 是 √ 是否启用 JSX 支持? ... 否 / 是 √ 是否引入 Vue Router 进行单页面应用开发? ... 否 / 是 √ 是否引入 Pinia 用于状态管理? ... 否 / 是 √ 是否引入 Vitest 用于单元测试? ... 否 / 是 √ 是否要引入一款端到端(End to End)测试工具? » 不需要 √ 是否引入 ESLint 用于代码质量检测? ... 否 / 是 √ 是否引入 Prettier 用于代码格式化? ... 否 / 是 ``` 项目构建完成,可执行以下命令进行初始化 ```shell cd vue3-big-event pnpm install pnpm run dev ``` ### 2. 配置项目参数 #### 2.1 Eslint 配置代码风格 配置文件 .eslintrc.cjs 1. [prettier风格配置](https://prettier.io) - 单引号 - 不使用分号 - 宽度80字符 - 不加对象|数组最后逗号 - 换行符号不限制(win mac 不一致) 2. vue组件名称多单词组成(忽略index.vue) 3. props解构(关闭) **提示:安装Eslint且配置保存修复,不要开启默认的自动保存格式化** .eslintrc.cjs ```js rules: { 'prettier/prettier': [ 'warn', { singleQuote: true, // 单引号 semi: false, // 无分号 printWidth: 120, // 每行宽度至多80字符 trailingComma: 'none', // 不加对象|数组最后逗号 endOfLine: 'auto' // 换行符号不限制(win mac 不一致) } ], 'vue/multi-word-component-names': [ 'warn', { ignores: ['index'] // vue组件名称多单词组成(忽略index.vue) } ], 'vue/no-setup-props-destructure': ['off'], // 关闭 props 解构的校验 // 💡 添加未定义变量错误提示,create-vue@3.6.3 关闭,这里加上是为了支持下一个章节演示。 'no-undef': 'error' } ``` vscode环境同步: - 安装插件 ESlint,开启保存自动修复 - 禁用插件 Prettier,并关闭保存自动格式化 - 设置里面进行配置: ```json // ESlint插件 + Vscode配置实现自动格式化修复 { "editor.codeActionsOnSave": { "source.fixAll": true }, "editor.formatOnSave": false, } ``` #### 2.2 配置代码检查工作流 ##### 2.2.1 提交前做代码检查 1. 初始化 git 仓库,执行 git init 即可 ```shell git init ``` 2. 初始化 [husky](https://typicode.github.io/husky/) 工具配置,执行 pnpm dlx husky-init && pnpm install 即可 ```shell pnpm dlx husky-init pnpm install ``` 3. 修改 .husky/pre-commit 文件 npm test 修改为 pnpm lint **问题:pnpm lint 是全量检查,耗时问题,历史问题** ##### 2.2.2 暂存区 eslint 校验 1. 安装 lint-staged包 ```shell pnpm i lint-staged -D ``` 2. package.json 配置 lint-staged 命令 ```json { // ... 省略 ... "lint-staged": { "*.{js,ts,vue}": [ "eslint --fix" ] } } { "scripts": { // ... 省略 ... "lint-staged": "lint-staged" } } ``` 3. .husky/pre-commit 文件修改 ``` pnpm lint-staged ``` #### 2.3 目录调整 默认生成的目录结构不满足我们的开发需求,所以这里需要做一些自定义改动。 1. 删除一些初始化的默认文件 - 删除assets下的文件 - 删除components下的文件 - 删除views下的文件 2. 修改剩余代码内容 - 修改router下index.js文件 - 修改App.vue文件 - 修改main.js文件 3. 新增调整我们需要的目录结构 - 增加utils文件夹 - 增加api文件夹 4. 拷贝全局样式和图片,安装预处理器支持 - 增加样式和图片文件到assets文件夹 - main.js中引入全局样式 - 安装 sass 依赖 main.js ```js import '@/assets/main.scss' ``` ```shell pnpm add sass -D ``` #### 2.4 vue-router路由代码解析 - 路由初始化 ```js import { createRouter, createWebHistory } from 'vue-router' // createRouter 创建路由实例 // 配置history模式 // history模式: 地址栏后面带# // hash模式: 地址栏后面不带# // 参数是基础路径,默认/ const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [] }) export default router ``` - 创建一个路由实例,路由模式是history模式 - 路由的基础地址是 vite.config.js中的 base 配置的值,默认是/ import.meta.url是[环境变量地址](https://cn.vitejs.dev/guide/env-and-mode.html) vite.config.js ```js import { fileURLToPath, URL } from 'node:url' import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' // https://vitejs.dev/config/ export default defineConfig({ plugins: [vue()], // 访问基本url base: '/', resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } } }) ``` - 在 Vue3 CompositionAPI中 1. 获取路由对象 ```js const router = useRouter() ``` 2. 获取路由参数 ```js const route = useRoute() ``` 3. App.vue中使用 ```vue ``` #### 2.5 引入Element Plus组件库 - 安装 ```shell pnpm add element-plus ``` - 配置按需导入:[官方文档](https://element-plus.org/zh-CN/guide/quickstart.html) ```shell pnpm add -D unplugin-vue-components unplugin-auto-import ``` - 配置文件vite.config.js ```js ... import AutoImport from 'unplugin-auto-import/vite' import Components from 'unplugin-vue-components/vite' import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' // https://vitejs.dev/config/ export default defineConfig({ plugins: [ ... AutoImport({ resolvers: [ElementPlusResolver()] }), Components({ resolvers: [ElementPlusResolver()] }) ] }) ``` - 直接使用组件 ```vue ``` **默认 components 下的文件也会被 自动注册** #### 2.6Pinia构建仓库和持久化 [官方文档](https://prazdevs.github.io/pinia-plugin-persistedstate/zh/) - 安装插件 pinia-plugin-persistedstate ```shell pnpm add pinia-plugin-persistedstate -D ``` - 使用 main.js ```js import persist from 'pinia-plugin-persistedstate' ... app.use(createPinia().use(persist)) ``` - 配置 stores/user.js ```js import { defineStore } from 'pinia' import { ref } from 'vue' // 用户模块 export const useUserStore = defineStore( 'big-user', () => { const token = ref('') // 定义 token const setToken = (t) => (token.value = t) // 设置 token return { token, setToken } }, { persist: true // 持久化 } ) ``` #### 2.7Pinia 仓库统一管理 ##### 2.7.1pinia 独立维护 - 现在:初始化代码在 main.js 中,仓库代码在 stores 中,代码分散职能不单一 - 优化:由stores统一维护,在stores/index.js 中完成pini 初始化,交付main.js使用 stores => index.js ```js import { createPinia } from 'pinia' import persist from 'pinia-plugin-persistedstate' const pinia = createPinia() pinia.use(persist) export default pinia ``` main.js ```js import { createApp } from 'vue' import App from './App.vue' import router from './router' import pinia from './stores/index' import '@/assets/main.scss' const app = createApp(App) app.use(pinia) app.use(router) app.mount('#app') ``` ##### 2.7.2仓库 统一导出 - 现在:使用一个仓库 import { useUserStore } from ./stores/user.js 不同仓库路径不一致 - 优化:由 stores/index.js 统一导出,导入路径统一 ./stores,而且仓库维护在 stores/modules 中 stores => index.js ```js export * from './modules/user' export * from './modules/counter' ``` App.vue ```vue import { useUserStore, useCountStore } from '@/stores' ``` #### 2.8 数据交互 - 请求工具设计 1. 安装 axios ```shell pnpm add axios ``` 2. 新建 utils/request.js 封装 [axios](http://www.axios-js.com/zh-cn/docs/#axios-create-config)模块 利用 axios.create 创建一个自定义的 axios 来使用 request.js ```js import axios from 'axios' const baseURL = 'http://big-event-vue-api-t.itheima.net' const instance = axios.create({ // TODO 1. 基础地址,超时时间 }) instance.interceptors.request.use( (config) => { // TODO 2. 携带token return config }, (err) => Promise.reject(err) ) instance.interceptors.response.use( (res) => { // TODO 3. 处理业务失败 // TODO 4. 摘取核心响应数据 return res }, (err) => { // TODO 5. 处理401错误 return Promise.reject(err) } ) export default instance ``` 3. 完成 axios 基本配置 request.js ```js import { useUserStore } from '@/stores/user' import axios from 'axios' import router from '@/router' import { ElMessage } from 'element-plus' const baseURL = 'http://big-event-vue-api-t.itheima.net' const instance = axios.create({ baseURL, timeout: 100000 }) instance.interceptors.request.use( (config) => { const userStore = useUserStore() if (userStore.token) { config.headers.Authorization = userStore.token } return config }, (err) => Promise.reject(err) ) instance.interceptors.response.use( (res) => { if (res.data.code === 0) { return res } ElMessage({ message: res.data.message || '服务异常', type: 'error' }) return Promise.reject(res.data) }, (err) => { ElMessage({ message: err.response.data.message || '服务异常', type: 'error' }) console.log(err) if (err.response?.status === 401) { router.push('/login') } return Promise.reject(err) } ) export default instance export { baseURL } ``` #### 2.9 整体路由 ![整体路由分析](./img/router.png) 登陆 一级路由 框架 一级路由 - 文章分类 二级路由 - 文章管理 二级路由 - 基本资料 二级路由 - 更换头像 二级路由 - 重置密码 二级路由 创建相应views页面 router => index.js ```js import { createRouter, createWebHistory } from 'vue-router' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: '/login', component: () => import('@/views/login/LoginPage.vue') }, //登录页 { path: '/', component: () => import('@/views/layout/LayoutContainer.vue'), redirect: '/article/manage', children: [ { path: '/article/manage', component: () => import('@/views/article/ArticleManage.vue') }, { path: '/article/channel', component: () => import('@/views/article/ArticleChannel.vue') }, { path: '/user/profile', component: () => import('@/views/user/UserProfile.vue') }, { path: '/user/avatar', component: () => import('@/views/user/UserAvatar.vue') }, { path: '/user/password', component: () => import('@/views/user/UserPassword.vue') } ] } ] }) export default router ``` ## 二、具体业务功能实现 ### 1. 登录注册页面 [element-plus 表单 & 表单校验] 功能需求说明: - 注册登录 静态结构 & 基本切换 - 注册功能 (校验 + 注册) - 登录功能 (校验 + 登录 + 存token) #### 1.1 注册登录 静态结构 & 基本切换 - 安装 element-plus 图标库 ```shell pnpm i @element-plus/icons-vue ``` - 静态结构准备 LoginPage.vue ```vue ``` #### 1.2 实现注册校验 功能需求说明 - 用户名非空,长度校验5-10位 - 密码非空,长度校验6-15位 - 再次输入密码,非空,长度校验6-15位 - 再次输入密码需要自定义校验规则,和密码框值一致(可选) ##### 1.2.1 model 属性绑定 form 数据对象 ```js const registeModel = ref({ username: '', password: '', repassword: '' }) ``` ##### 1.2.2 v-model 绑定 form 数据对象的子属性 ```js ... //其他两个也要绑定 ``` ##### 1.2.3 rules 配置校验规则 **整个表单的校验规则** 1. 非空校验 required: true message消息提示,trigger触发校验的时机 blur change 2. 长度校验 min: xx, max: xx 3. 正则校验 pattern: 正则规则 \S 非空字符 4. 自定义校验 => 自己写逻辑校验(校验函数) validator: (rules, value, callback) (1)rule 当前校验规则的相关信息 (2)value 所校验的表单元素目前的表单值 (3)callback 无论成功还是失败,都需要 callback 回调 - callback() 校验成功 - callback(new Error(错误信息)) ```js const rules = { username: [ { required: true, message: '请输入用户名', trigger: 'blur' }, { min: 5, max: 10, message: '用户名必须是 5-10 位 的字符', trigger: 'blur' } ], password: [ { required: true, message: '请输入密码', trigger: 'blur' }, { pattern: /^\S{6,15}$/, message: '密码必须是6-15位的非空字符', trigger: 'blur' } ], repassword: [ { required: true, message: '请输入密码', trigger: 'blur' }, { pattern: /^\S{6,15}$/, message: '密码必须是6-15位的非空字符', trigger: 'blur' }, { validator: (rules, value, callback) => { // 判断 value 和 当前 form 中收集的 password 是否一致 if (value !== registeModel.value.password) { callback(new Error('两次输入的密码不一致')) } else { callback() //就算校验成功,也需要callback } }, trigger: 'blur' } ] } ``` ##### 1.2.4 prop 绑定校验规则 ```js ... //其他两个也要绑定prop ``` #### 1.3 注册前的预校验 需求:点击注册按钮,注册之前,需要先校验 ##### 1.3.1 通过 ref 获取到 表单组件 ```js const form = ref() ``` ##### 1.3.2 注册之前进行校验 ```js 注册 const register = async () => { await form.value.validate() console.log('开始注册请求') } ``` #### 1.4 封装 api 实现注册功能 需求:封装注册api,进行注册,注册成功切换到登录 ##### 1.4.1 新建 api/user.js 封装 ```js import request from '@/utils/request' export const userRegisterService = ({ username, password, repassword }) => request.post('/api/reg', { username, password, repassword }) ``` ##### 1.4.2 页面中调用 ```js const register = async () => { await form.value.validate() await userRegisterService(formModel.value) ElMessage.success('注册成功') // 切换到登录 isRegister.value = false } ``` ##### 1.4.3 eslintrc 中声明全局变量名, 解决 ElMessage 报错问题 ```js module.exports = { ... globals: { ElMessage: 'readonly', ElMessageBox: 'readonly', ElLoading: 'readonly' } } ``` #### 1.5 实现登录校验 需求说明: - 用户名非空,长度校验5-10位 - 密码非空,长度校验6-15位给输入框添加表单校验 **失去焦点和修改内容时触发校验** 1. model 属性绑定 form 数据对象,直接绑定之前提供好的数据对象即可 ```js ``` 2. rules 配置校验规则,共用注册的规则即可 ```js ``` 3. v-model 绑定 form 数据对象的子属性 ```js ``` 4. prop 绑定校验规则 ```js ... ``` 5. 切换的时候重置 ```js watch(isRegister, () => { formModel.value = { username: '', password: '', repassword: '' } }) ``` #### 1.6 登录前的预校验 & 登录成功 - 需求说明1 登录之前的预校验 登录请求之前,需要对用户的输入内容,进行校验 校验通过才发送请求 - 需求说明2 登录功能 封装登录API,点击按钮发送登录请求 登录成功存储token,存入pinia 和 持久化本地storage 跳转到首页,给提示 ```text 登录的测试账号: shuaipeng 登录测试密码: 123456 ``` ##### 1.6.1 注册事件,进行登录前的预校验 (获取到组件调用方法) ```js const login = async () => { await form.value.validate() console.log('开始登录') } ``` ##### 1.6.2 封装接口 API ```js export const userLoginService = ({ username, password }) => request.post('api/login', { username, password }) ``` ##### 1.6.3 调用方法将 token 存入 pinia 并 自动持久化本地 ```js const userStore = useUserStore() const router = useRouter() const login = async () => { await form.value.validate() const res = await userLoginService(formModel.value) userStore.setToken(res.data.token) ElMessage.success('登录成功') router.push('/') } ``` ### 2. 首页layout布局 [element-plus 菜单组件] 功能需求说明: - 基本布局拆解 (菜单组件的使用) - 登录访问拦截 - 用户基本信息获取&渲染 - 退出功能 [element-plus 确认框] #### 2.1 布局组件列表 > el-container >> - el-aside 左侧 >>> el-menu 左侧边栏菜单 >> - el-container 右侧 >>> el-header 右侧头部 >>> el-dropdown >>> el-main 右侧主体 router-view ```js ``` #### 2.2 登录访问拦截 需求:只有登录页,可以未授权的时候访问,其他所有页面,都需要先登录再访问 ```js // 登陆访问拦截 => 默认是直接放行的 // 根据返回值决定,是放行还是拦截 // 返回值: // 1.undefined / true 直接放行 // 2.false 拦回from的地址页面 // 3.具体路径 或 路径对象 拦截到对应的地址 // '/login' {name: 'login'} router.beforeEach((to) => { // 如果没有token,且访问的是非登录页,拦截到登录,其他情况正常放行 const useStore = useUserStore() if (!useStore.token && to.path !== '/login') return '/login' return true }) export default router ``` #### 2.3 用户基本信息获取&渲染 ##### 2.3.1 api/user.js封装接口 ```js export const userGetInfoService = () => request.get('/my/userinfo') ``` ##### 2.3.2 stores/modules/user.js 定义数据 ```js const user = ref({}) const getUser = async () => { const res = await userGetInfoService() // 请求获取数据 user.value = res.data.data } ``` ##### 2.3.3 layout/LayoutContainer页面中调用 ```js import { useUserStore } from '@/stores' const userStore = useUserStore() onMounted(() => { userStore.getUser() }) ``` ##### 2.3.4 动态渲染 ```js
用户:{{ userStore.user.nickname || userStore.user.username }}
``` #### 2.4 退出功能 [element-plus 确认框] ##### 2.4.1 注册点击事件 ```js 基本资料 更换头像 重置密码 退出登录 ``` ##### 2.4.2 添加退出功能 ```js const onCommand = async (command) => { if (command === 'logout') { await ElMessageBox.confirm('确认退出?', '温馨提示', { type: 'warning', confirmButtonText: '确认', cancelButtonText: '取消' }) userStore.removeToken() userStore.setUser({}) router.push(`/login`) } else { router.push(`/user/${command}`) } } ``` ##### 2.4.3 pinia user.js 模块 提供 setUser 方法 ```js const setUser = (obj) => (user.value = obj) ``` ### 3. 文章分类页面 [element-plus 表格] 功能需求说明: - 基本布局 - PageContainer 封装 - 文章分类渲染 & loading 处理 - 文章分类添加编辑 [element-plus 弹层] - 文章分类删除 #### 3.1 文章分类页面 - [element-plus 表格] ```js ``` 考虑到多个页面复用,封装成组件 - props 定制标题 - 默认插槽 default 定制内容主体 - 具名插槽 extra 定制头部右侧额外的按钮 在compnents文件夹下创建PageContainer.vue ```js ``` 页面中直接使用测试 ( unplugin-vue-components 会自动注册) - 增加图标自动导入 ```js pnpm add -D unplugin-icons unplugin-vue-components ``` - 配置vite.config.js ```js import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import Icons from 'unplugin-icons/vite' import IconsResolver from 'unplugin-icons/resolver' import Components from 'unplugin-vue-components/vite' export default defineConfig({ plugins: [ vue(), Components({ resolvers: [ // 自动注册图标组件 IconsResolver(), ], }), Icons(), ], }) ``` - 文章分类测试 ```js ``` - 文章管理测试 ```js ``` #### 3.2 文章分类渲染 ##### 3.2.1 封装API - 请求获取表格数据 - 新建 api/article.js 封装获取频道列表的接口 ```js import request from '@/utils/request' export const artGetChannelsService = () => request.get('/my/cate/list') ``` - 页面中调用接口,获取数据存储 ```js const channelList = ref([]) const getChannelList = async () => { const res = await artGetChannelsService() channelList.value = res.data.data } ``` ##### 3.2.2 el-table 表格动态渲染 修改views/article/ArticleChannel.vue ```js const onEditChannel = (row) => { console.log(row) } const onDelChannel = (row) => { console.log(row) } ``` ##### 3.2.3 el-table 表格 loading 效果 - 定义变量,v-loading绑定 ```js const loading = ref(false) ``` - 发送请求前开启,请求结束关闭 ```js const getChannelList = async () => { loading.value = true const res = await artGetChannelsService() channelList.value = res.data.data loading.value = false } ``` #### 3.3 准备弹层表单 ##### 3.3.1 准备数据 和 校验规则 ```js const formModel = ref({ cate_name: '', cate_alias: '' }) const rules = { cate_name: [ { required: true, message: '请输入分类名称', trigger: 'blur' }, { pattern: /^\S{1,10}$/, message: '分类名必须是1-10位的非空字符', trigger: 'blur' } ], cate_alias: [ { required: true, message: '请输入分类别名', trigger: 'blur' }, { pattern: /^[a-zA-Z0-9]{1,15}$/, message: '分类别名必须是1-15位的字母数字', trigger: 'blur' } ] } ``` ##### 3.3.2 准备表单 ```js ``` ##### 3.3.3 编辑需要回显,表单数据需要初始化 ```js const open = async (row) => { dialogVisible.value = true formModel.value = { ...row } } ``` ##### 3.3.4 基于传过来的表单数据,进行标题控制,有 id 的是编辑 ```js :title="formModel.id ? '编辑分类' : '添加分类'" ``` #### 3.4 确认提交 ##### 3.4.1 封装 api/article.js 封装请求 API ```js // 添加文章分类 export const artAddChannelService = (data) => request.post('/my/cate/add', data) // 编辑文章分类 export const artEditChannelService = (data) => request.put('/my/cate/info', data) ``` ##### 3.4.2 页面中校验,判断,提交请求 ```js ``` ```js const formRef = ref() const onSubmit = async () => { await formRef.value.validate() formModel.value.id ? await artEditChannelService(formModel.value) : await artAddChannelService(formModel.value) ElMessage({ type: 'success', message: formModel.value.id ? '编辑成功' : '添加成功' }) dialogVisible.value = false } ``` ##### 3.4.3 通知父组件进行回显 ```js const emit = defineEmits(['success']) const onSubmit = async () => { ... emit('success') } ``` ##### 3.4.4 父组件监听 success 事件,进行调用回显 ```js const onSuccess = () => { getChannelList() } ``` #### 3.5 文章分类删除 ##### 3.5.1 封装api/article.js封装接口 api ```js // 删除文章分类 export const artDelChannelService = (id) => request.delete('/my/cate/del', { params: { id } }) ``` ##### 3.5.2 页面中添加确认框,调用接口进行提示 ```js const onDelChannel = async (row) => { await ElMessageBox.confirm('你确认删除该分类信息吗?', '温馨提示', { type: 'warning', confirmButtonText: '确认', cancelButtonText: '取消' }) await artDelChannelService(row.id) ElMessage({ type: 'success', message: '删除成功' }) getChannelList() } ``` ### 4. 文章列表渲染 #### 4.1 基本框架搭建 搜索表单 表格准备,模拟假数据渲染 ```js import { Delete, Edit } from '@element-plus/icons-vue' import { ref } from 'vue' // mock数据 const articleList = ref([ { id: 5961, title: '新的文章啊', pub_date: '2022-07-10 14:53:52.604', state: '已发布', cate_name: '体育' }, { id: 5962, title: '新的文章啊', pub_date: '2022-07-10 14:54:30.904', state: null, cate_name: '体育' } ]) ``` ```js const onEditArticle = (row) => { console.log(row) } const onDeleteArticle = (row) => { console.log(row) } ``` #### 4.2 中英国际化处理 默认是英文的,由于这里不涉及切换, 所以在 App.vue 中直接导入设置成中文即可 ```js ``` #### 4.3 文章分类选择 为了便于维护,直接拆分成一个小组件 ChannelSelect.vue - 新建 article/components/ChannelSelect.vue ```js ``` - 页面中导入渲染 ```js import ChannelSelect from './components/ChannelSelect.vue' ``` - 调用接口,动态渲染下拉分类,设计成 v-model 的使用方式 ```js ``` - 父组件定义参数绑定 ```js const params = ref({ pagenum: 1, pagesize: 5, cate_id: '', state: '' }) ``` - 发布状态,也绑定一下,便于将来提交表单 ```js ``` #### 4.4 封装 API 接口,请求渲染 - api/article.js封装接口 ```js export const artGetListService = (params) => request.get('/my/article/list', { params }) ``` - 页面中调用保存数据 ```js const articleList = ref([]) const total = ref(0) const getArticleList = async () => { const res = await artGetListService(params.value) articleList.value = res.data.data total.value = res.data.total } getArticleList() ``` - 新建 utils/format.js 封装格式化日期函数 ```js import { dayjs } from 'element-plus' export const formatTime = (time) => dayjs(time).format('YYYY年MM月DD日') ``` - 导入使用 ```js import { formatTime } from '@/utils/format' ``` #### 4.5 分页渲染 [element-plus 分页] - 分页组件 ```js ``` - 提供分页修改逻辑 ```js const onSizeChange = (size) => { params.value.pagenum = 1 params.value.pagesize = size getArticleList() } const onCurrentChange = (page) => { params.value.pagenum = page getArticleList() } ``` #### 4.6 添加 loading 处理 - 准备数据 ```js const loading = ref(false) ``` - el-table上面绑定 ```js ... ``` - 发送请求时添加 loading ```js const getArticleList = async () => { loading.value = true ... loading.value = false } getArticleList() ``` #### 4.7 搜索 和 重置功能 - 注册事件 ```js 搜索 重置 ``` - 绑定处理 ```js const onSearch = () => { params.value.pagenum = 1 getArticleList() } const onReset = () => { params.value.pagenum = 1 params.value.cate_id = '' params.value.state = '' getArticleList() } ``` ### 5. 文章发布&修改 [element-plus - 抽屉] #### 5.1 点击显示抽屉 - 准备数据 ```js import { ref } from 'vue' const visibleDrawer = ref(false) ``` - 准备抽屉容器 ```js Hi there! ``` - 点击修改布尔值显示抽屉 ```js 发布文章 const visibleDrawer = ref(false) const onAddArticle = () => { visibleDrawer.value = true } ``` #### 5.2 封装抽屉组件 ArticleEdit 添加 和 编辑,可以共用一个抽屉,所以可以将抽屉封装成一个组件 组件对外暴露一个方法 open, 基于 open 的参数,初始化表单数据,并判断区分是添加 还是 编辑 > open({ }) => 添加操作,添加表单初始化无数据 > open({ id: xx, … }) => 编辑操作,编辑表单初始化需回显 - 封装组件 article/components/ArticleEdit.vue ```js ``` - 通过 ref 绑定 ```js const articleEditRef = ref() ``` - 点击调用方法显示弹窗 ```js // 编辑新增逻辑 const onAddArticle = () => { articleEditRef.value.open({}) } const onEditArticle = (row) => { articleEditRef.value.open(row) } ``` #### 5.3 完善抽屉表单结构 - 准备数据 ```js const formModel = ref({ title: '', cate_id: '', cover_img: '', content: '', state: '' }) const open = async (row) => { visibleDrawer.value = true if (row.id) { console.log('编辑回显') } else { console.log('添加功能') } } ``` - 准备 form 表单结构 ```js import ChannelSelect from './ChannelSelect.vue' ``` - 打开默认重置添加的 form 表单数据 ```js const defaultForm = { title: '', cate_id: '', cover_img: '', content: '', state: '' } const formModel = ref({ ...defaultForm }) const open = async (row) => { visibleDrawer.value = true if (row.id) { console.log('编辑回显') } else { console.log('添加功能') formModel.value = { ...defaultForm } } } ``` - 扩展 下拉菜单 width props 修改ChannelSelect.vue ```js defineProps({ modelValue: { type: [Number, String] }, width: { type: String } }) ``` #### 5.4 上传文件 [element-plus - 文件预览] - 关闭自动上传,准备结构 >此处需要关闭 element-plus 的自动上传,不需要配置 action 等参数只需要做前端的本地预览图片即可,无需在提交前上传图标 预览图片的语法:URL.createObjectURL(...) 创建本地预览的地址预览 ```js import { Plus } from '@element-plus/icons-vue' ``` - 准备数据 和 选择图片的处理逻辑 ```js const imgUrl = ref('') const onUploadFile = (uploadFile) => { imgUrl.value = URL.createObjectURL(uploadFile.raw) formModel.value.cover_img = uploadFile.raw } ``` - 样式美化 ```js .avatar-uploader { :deep() { .avatar { width: 178px; height: 178px; display: block; } .el-upload { border: 1px dashed var(--el-border-color); border-radius: 6px; cursor: pointer; position: relative; overflow: hidden; transition: var(--el-transition-duration-fast); } .el-upload:hover { border-color: var(--el-color-primary); } .el-icon.avatar-uploader-icon { font-size: 28px; color: #8c939d; width: 178px; height: 178px; text-align: center; } } } ``` #### 5.5 富文本编辑器 [ vue-quill ] - 安装 ```shell ``` - 注册成局部组件 ```js ``` - 页面中使用绑定 ```js import { QuillEditor } from '@vueup/vue-quill' import '@vueup/vue-quill/dist/vue-quill.snow.css' ``` - 样式美化 ```js .editor { width: 100%; :deep(.ql-editor) { min-height: 200px; } } ``` #### 5.6 添加文章功能 ##### 5.6.1 封装添加接口 ```js export const artPublishService = (data) => request.post('/my/article/add', data) ``` ##### 5.6.2 注册点击事件调用 ```js 发布 草稿 // 发布文章 const emit = defineEmits(['success']) const onPublish = async (state) => { // 将已发布还是草稿状态,存入 state formModel.value.state = state // 转换 formData 数据 const fd = new FormData() for (let key in formModel.value) { fd.append(key, formModel.value[key]) } if (formModel.value.id) { console.log('编辑操作') } else { // 添加请求 await artPublishService(fd) ElMessage.success('添加成功') visibleDrawer.value = false emit('success', 'add') } } ``` ##### 5.6.3 父组件监听事件,重新渲染 ```js // 添加修改成功 const onSuccess = (type) => { if (type === 'add') { // 如果是添加,需要跳转渲染最后一页,编辑直接渲染当前页 const lastPage = Math.ceil((total.value + 1) / params.value.pagesize) params.value.pagenum = lastPage } getArticleList() } ``` #### 5.7 添加完成后的内容重置 ```js const formRef = ref() const editorRef = ref() const open = async (row) => { visibleDrawer.value = true if (row.id) { console.log('编辑回显') } else { formModel.value = { ...defaultForm } imgUrl.value = '' editorRef.value.setHTML('') } } ``` #### 5.8 编辑文章回显 如果是编辑操作,一打开抽屉,就需要发送请求,获取数据进行回显 ##### 5.8.1 封装接口,根据 id 获取详情数据 ```js export const artGetDetailService = (id) => request.get('my/article/info', { params: { id } }) ``` ##### 5.8.2 页面中调用渲染 ```js const open = async (row) => { visibleDrawer.value = true if (row.id) { console.log('编辑回显') const res = await artGetDetailService(row.id) formModel.value = res.data.data imgUrl.value = baseURL + formModel.value.cover_img // 提交给后台,需要的是 file 格式的,将网络图片,转成 file 格式 // 网络图片转成 file 对象, 需要转换一下 formModel.value.cover_img = await imageUrlToFile(imgUrl.value, formModel.value.cover_img) } else { console.log('添加功能') ... } } ``` ##### 5.8.3 chatGPT prompt:封装一个函数,基于 axios, 网络图片地址,转 file 对象, 请注意:写中文注释 ```js // 将网络图片地址转换为File对象 async function imageUrlToFile(url, fileName) { try { // 第一步:使用axios获取网络图片数据 const response = await axios.get(url, { responseType: 'arraybuffer' }); const imageData = response.data; // 第二步:将图片数据转换为Blob对象 const blob = new Blob([imageData], { type: response.headers['content-type'] }); // 第三步:创建一个新的File对象 const file = new File([blob], fileName, { type: blob.type }); return file; } catch (error) { console.error('将图片转换为File对象时发生错误:', error); throw error; } } ``` #### 5.9 编辑文章功能 ##### 5.9.1 封装编辑接口 ```js export const artEditService = (data) => request.put('my/article/info', data) ``` ##### 5.9.2 页面中调用渲染 ```js const onPublish = async (state) => { ... if (formModel.value.id) { await artEditService(fd) ElMessage.success('编辑成功') visibleDrawer.value = false emit('success', 'edit') } else { // 添加请求 ... } } ``` #### 5.10 文章删除 - 封装删除接口 ```js export const artDelService = (id) => request.delete('my/article/info', { params: { id } }) ``` - 页面中添加确认框调用 ```js const onDeleteArticle = async (row) => { await ElMessageBox.confirm('你确认删除该文章信息吗?', '温馨提示', { type: 'warning', confirmButtonText: '确认', cancelButtonText: '取消' }) await artDelService(row.id) ElMessage({ type: 'success', message: '删除成功' }) getArticleList() } ``` ### 6. 个人中心项目实战 - 基本资料 #### 6.1 静态结构 + 校验处理 - chatgpt prompt 提示词参考 ```text 请基于 elementPlus 和 Vue3 的语法,生成组件代码 要求: 一、表单结构要求 1. 组件中包含一个el-form表单,有四行内容,前三行是输入框,第四行是按钮 2. 第一行 label 登录名称,输入框禁用不可输入状态 3. 第二行 label 用户昵称,输入框可输入 4. 第三行 label 用户邮箱,输入框可输入 5. 第四行按钮,提交修改 二、校验需求 给昵称 和 邮箱添加校验 1. 昵称 nickname 必须是2-10位的非空字符串 2. 邮箱 email 符合邮箱格式即可,且不能为空 ``` - 参考目标代码 ```js ``` #### 6.2 封装接口,更新个人信息 - 封装接口 ```js export const userUpdateInfoService = ({ id, nickname, email }) => request.put('/my/userinfo', { id, nickname, email }) ``` - 页面中校验后,封装调用 ```js const formRef = ref() const onSubmit = async () => { const valid = await formRef.value.validate() if (valid) { await userUpdateInfoService(userInfo.value) await getUser() ElMessage.success('修改成功') } } ``` ### 7. 个人中心项目实战 - 更换头像 #### 7.1 静态结构 ```js ``` #### 7.2 选择预览图片 ```js const uploadRef = ref() const imgUrl = ref(userStore.user.user_pic) const onUploadFile = (file) => { const reader = new FileReader() reader.readAsDataURL(file.raw) reader.onload = () => { imgUrl.value = reader.result } } 选择图片 ``` #### 7.3 上传头像 - 封装接口 ```js export const userUploadAvatarService = (avatar) => request.patch('/my/update/avatar', { avatar }) ``` - 调用接口 ```js const onUpdateAvatar = async () => { await userUploadAvatarService(imgUrl.value) await userStore.getUser() ElMessage({ type: 'success', message: '更换头像成功' }) } ``` ### 8. 个人中心项目实战 - 重置密码 - chatgpt prompt提示词 ```text 请基于 elementPlus 和 Vue3 的语法,生成组件代码 要求: 一、表单结构要求 组件中包含一个el-form表单,有四行内容,前三行是表单输入框,第四行是两个按钮 第一行 label 原密码 第二行 label 新密码 第三行 label 确认密码 第四行两个按钮,修改密码 和 重置 二、form绑定字段如下: const pwdForm = ref({ old_pwd: ‘’, new_pwd: ‘’, re_pwd: ‘’ }) 三、校验需求 所有字段,都是 6-15位 非空 自定义校验1:原密码 和 新密码不能一样 自定义校验2:新密码 和 确认密码必须一样 ``` #### 8.1 静态结构 + 校验处理 ```js ``` #### 8.2 封装接口,更新密码信息 - 封装接口 ```js export const userUpdatePassService = ({ old_pwd, new_pwd, re_pwd }) => request.patch('/my/updatepwd', { old_pwd, new_pwd, re_pwd }) ``` - 页面中调用 ```js const formRef = ref() const router = useRouter() const userStore = useUserStore() const onSubmit = async () => { const valid = await formRef.value.validate() if (valid) { await userUpdatePassService(pwdForm.value) ElMessage({ type: 'success', message: '更换密码成功' }) userStore.setToken('') userStore.setUser({}) router.push('/login') } } const onReset = () => { formRef.value.resetFields() } ```