vue3项目【黑马大事件】笔记
目录
- 1.项目架构
- 1.1.pnpm包管理器
- 1.2.ESLint & prettier
- 1.3.husky 代码检查
- 1.4.调整项目目录
- 1.5.VueRouter4 路由
- 1.6.Element Puls组件库
- 1.7.Pinia - 构建用户仓库 和 持久化
- 1.8.Pinia - 配置仓库统一管理
- 2.数据交互 - 请求工具设计
- 2.1.创建 axios 实例
- 2.2.首页整体路由设计
- 3.登录注册页面
- 3.1.el-row和el-col
- 3.2.表单校验
- 3.3.注册前的预校验
- 3.4.封装 api 实现注册功能
- 3.5.登录页面
- 4.首页 layout 架子
- 4.1.登录访问拦截(导航守卫)
- 4.2.用户基本信息获取&渲染
- 4.3.退出登录
- 5.文章分类页面
- 5.1.基本架子
- 5.2.文章分类渲染
- 5.3.el-table 表格动态渲染
- 5.4.el-table 表格 loading 效果
- 5.5.添加分类弹窗
- 5.6.封装弹层组件
- 5.7.准备弹层表单
- 5.8.确认提交
- 5.9.删除分类
- 6.文章管理页面
- 6.1.表单区域
- 6.2.表格区域
- 6.3.中英国际化处理
- 6.4.封装【分类选择】
- 6.5.封装请求渲染文章API
- 6.6.分页渲染 [element-plus 分页]
- 6.7.添加 loading 处理
- 6.8.搜索 和 重置功能
- 7.文章管理【抽屉区域】
- 7.1.点击显示抽屉
- 7.2.封装抽屉组件
- 7.3.完善抽屉表单结构
- 7.4.上传文件
- 7.5.富文本编辑器 【vue-quill】
- 7.6.添加文章功能
- 7.7.添加完成后的内容重置
- 7.8.编辑文章回显
- 7.9.编辑文章功能
- 7.10文章删除
- 8.AI编码
- 8.1.AI编码的基本使用
- 8.2.通意灵码
- 8.3.【实践】静态结构 + 校验处理
- 8.4.【实践】更新个人信息
- 8.5.【实践】更换头像
- 8.6.【实践】重置密码
1.项目架构
1.1.pnpm包管理器
-
安装方式:
npm install -g pnpm
-
创建项目:
pnpm create vue
-
切换到项目根目录
cd .\big-event\
-
安装项目依赖
pnpm install
-
启动项目
pnpm dev
1.2.ESLint & prettier
进入项目后,VSCode会提醒安装 prettier的相关插件(我这里已经安装好了)
安装后,当代码保存,会自动根据prettier规范修改代码。
1.3.husky 代码检查
husky 是一个 git hooks 工具 ( git的钩子工具,可以在特定时机执行特定的命令 )
-
git初始化
git init
(在bash终端) -
初始化 husky 工具配置
pnpm dlx husky-init && pnpm install
【注意】上面代码在git bash执行
初始化完毕后,会在根目录下生成一个husky文件夹
-
修改文件夹下的pre-commit文件,将
npm test
修改为pnpm lint
-
在package.json中,配置需要校验的文件
当我们提交代码,就会基于eslint将配置的文件进行校验并尝试修复
手动尝试调用pnpm lint,报错,配置成功
1.4.调整项目目录
- 清空部分目录,保留index.js、App.vue、main.js
-
index.js部分保留
import { createRouter, createWebHistory } from 'vue-router' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [], }) export default router
-
App.vue保留骨架
<script setup></script> <template> <div>app</div> </template> <style scoped></style>
-
mian.js部分保留
import { createApp } from 'vue' import { createPinia } from 'pinia' import App from './App.vue' import router from './router' const app = createApp(App) app.use(createPinia()) app.use(router) app.mount('#app')
-
新增目录,在src下添加目录api(用于封装api)和utils(用于封装api和存放公共方法)
-
将项目需要的图片复制到assets文件夹
-
安装 sass 依赖
pnpm add sass -D
-
将全局样式在main.js中引入
import '@/assets/main.scss'
1.5.VueRouter4 路由
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: []
})
export default router
- history模式: createWebHistory() http://xxx/user
- hash模式: createWebHashHistory() http://xxx/#/use
-
createWebHistory() 和: createWebHashHistory()的参数是路由的基地址,默认是 ‘/’
-
import.meta.env.BASE_URL
是vite中的环境变量,我们可以在vite.config.js中配置,import { fileURLToPath, URL } from 'node:url' import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' // https://vite.dev/config/ export default defineConfig({ plugins: [vue()], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)), }, }, // 配置基地址 base: '/jd', })
配置成功后,每个路由前都会添加基地址
-
路由跳转
<script setup> const goList = () => { this.$router.push('/list') } </script> <template> <div> <button @click="$router.push('/home')">首页</button> <button @click="goList">列表页</button> </div> </template> <style scoped></style>
执行上面代码会报错,因为vue3没有this,如果我们想在事件处理方法中跳转路由,应该利用路由对象router
<script setup> import { useRoute, useRouter } from 'vue-router' const route = useRoute() const router = useRouter() const goList = () => { // this.$router.push('/list') console.log('当前路由参数', route) console.log('路由对象', router) // 跳转路由 router.push('/list') } </script> <template> <div> <button @click="$router.push('/home')">首页</button> <button @click="goList">列表页</button> </div> </template> <style scoped></style>
-
在控制台,打印出了获取路由参数useRoute和获取路由对象useRouter,在useRouter上有push方法,我们可以通过push方法跳转路由。
1.6.Element Puls组件库
- 官方文档: https://element-plus.org/zh-CN/
-
安装组件库
pnpm add element-plus
-
安装让组件按需导入的插件:
pnpm add -D unplugin-vue-components unplugin-auto-import
-
在vite.config.js配置文件
import { fileURLToPath, URL } from 'node:url' import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import AutoImport from 'unplugin-auto-import/vite' import Components from 'unplugin-vue-components/vite' import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' // https://vite.dev/config/ export default defineConfig({ plugins: [ vue(), AutoImport({ resolvers: [ElementPlusResolver()], }), Components({ resolvers: [ElementPlusResolver()], }), ], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)), }, }, // 配置基地址 base: '/jd', })
重启项目,就可以使用组件了(将button改为el-button,默认样式改变)
<div> <el-button @click="$router.push('/home')">首页</el-button> <el-button @click="goList">列表页</el-button> </div>
1.7.Pinia - 构建用户仓库 和 持久化
-
安装插件 pinia-plugin-persistedstate
pnpm add pinia-plugin-persistedstate -D
-
导入并使用pinia(在main.js中)
import persist from 'pinia-plugin-persistedstate' app.use(createPinia().use(persist))
-
配置 stores/user.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 // 持久化 } )
1.8.Pinia - 配置仓库统一管理
-
统一管理前mian.js代码:
import { createApp } from 'vue' import { createPinia } from 'pinia' import persist from 'pinia-plugin-persistedstate' import App from './App.vue' import router from './router' import '@/assets/main.scss' const app = createApp(App) app.use(createPinia()) app.use(createPinia().use(persist)) app.use(router) app.mount('#app')
我们可以在 stores/index.js 中完成 pinia 初始化,再交付 main.js 使用,方便代码后期维护。
-
统一管理后index.js代码:
import { createPinia } from 'pinia' import persist from 'pinia-plugin-persistedstate' const pinia = createPinia() pinia.use(persist) // 默认导出 export default pinia
-
统一管理后mian.js代码:
import { createApp } from 'vue' import App from './App.vue' import router from './router' import '@/assets/main.scss' import pinia from '@/stores/index' const app = createApp(App) app.use(pinia) app.use(router) app.mount('#app')
我们将模块用于储存的js文件放入文件夹modules下,并导出到index.js,要使用导入index.js即可(以index.js作为唯一出口)。
-
index.js
import { createPinia } from 'pinia' import persist from 'pinia-plugin-persistedstate' const pinia = createPinia() pinia.use(persist) // 默认导出 export default pinia // 统一导出 export * from './modules/user'
如果在App.vue使用存储(这里可以在user.js中给token一个值,这样才可以打印出来)
// 统一导入 import { useUserStore } from '@/stores' const userStore = useUserStore() console.log(userStore.token)
2.数据交互 - 请求工具设计
2.1.创建 axios 实例
-
安装 axios
pnpm add axios
-
新建
utils/request.js
封装 axios 模块import axios from 'axios' import { useUserStore } from '@/stores' import { ElMessage } from 'element-plus' import router from '@/router' const baseURL = 'http://big-event-vue-api-t.itheima.net' const instance = axios.create({ // TODO 1. 基础地址,超时时间 baseURL, timeout: 10000, }) // 请求拦截器 instance.interceptors.request.use( config => { // TODO 2. 携带token const useStore = useUserStore() if (useStore.token) { config.headers.Authorization = useStore.token } return config }, err => Promise.reject(err), ) // 响应拦截器 instance.interceptors.response.use( res => { // TODO 3. 处理业务失败 // TODO 4. 摘取核心响应数据 if (res.data.code === 0) { return res } ElMessage.error(res.data.message || '服务异常') // 处理业务失败,提示 return Promise.reject(res.data) }, err => { // 401拦截 if (err.response?.status === 401) { router.push('/login') } // TODO 5. 处理401错误 ElMessage.error(err.response.data.message || '服务异常') return Promise.reject(err) }, ) export default instance
2.2.首页整体路由设计
路由规则
path | 文件 | 功能 | 组件名 | 路由级别 |
---|---|---|---|---|
/login | views/login/LoginPage.vue | 登录&注册 | LoginPage | 一级路由 |
/ | views/layout/LayoutContainer.vue | 布局架子 | LayoutContainer | 一级路由 |
├─ /article/manage | views/article/ArticleManage.vue | 文章管理 | ArticleManage | 二级路由 |
├─ /article/channel | views/article/ArticleChannel.vue | 频道管理 | ArticleChannel | 二级路由 |
├─ /user/profile | views/user/UserProfile.vue | 个人详情 | UserProfile | 二级路由 |
├─ /user/avatar | views/user/UserAvatar.vue | 更换头像 | UserAvatar | 二级路由 |
├─ /user/password | views/user/UserPassword.vue | 重置密码 | UserPassword | 二级路由 |
创建组件
-
在router/index.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'), 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
-
在App.vue配置一级路由出口
<template> <div>路由出口</div> <hr /> <router-view /> </template>
-
在LayoutContainer.vue配置二级路由出口
<template> <div>二级路由出口</div> <hr /> <router-view></router-view> </template>
-
路由和路由出口的关系图
【小优化】网页打开时, url 默认是 / 路径,未匹配到组件时,会出现空白,我们可以通过重定向,匹配 / 后, 强制跳转到文章分类页{ path: '/', redirect: '/article/channel', ... }
3.登录注册页面
-
安装 element-plus 图标库
pnpm i @element-plus/icons-vue
-
静态结构准备(LoginPage.vue)
<script setup> import { User, Lock } from '@element-plus/icons-vue' import { ref } from 'vue' const isRegister = ref(true) </script> <template> <el-row class="login-page"> <el-col :span="12" class="bg"></el-col> <el-col :span="6" :offset="3" class="form"> <!-- 注册表单 --> <el-form ref="form" size="large" autocomplete="off" v-if="isRegister"> <el-form-item> <h1>注册</h1> </el-form-item> <el-form-item> <el-input :prefix-icon="User" placeholder="请输入用户名"></el-input> </el-form-item> <el-form-item> <el-input :prefix-icon="Lock" type="password" placeholder="请输入密码" ></el-input> </el-form-item> <el-form-item> <el-input :prefix-icon="Lock" type="password" placeholder="请输入再次密码" ></el-input> </el-form-item> <el-form-item> <el-button class="button" type="primary" auto-insert-space> 注册 </el-button> </el-form-item> <el-form-item class="flex"> <el-link type="info" :underline="false" @click="isRegister = false"> ← 返回 </el-link> </el-form-item> </el-form> <!-- 注册表单 --> <el-form ref="form" size="large" autocomplete="off" v-else> <el-form-item> <h1>登录</h1> </el-form-item> <el-form-item> <el-input :prefix-icon="User" placeholder="请输入用户名"></el-input> </el-form-item> <el-form-item> <el-input name="password" :prefix-icon="Lock" type="password" placeholder="请输入密码" ></el-input> </el-form-item> <el-form-item class="flex"> <div class="flex"> <el-checkbox>记住我</el-checkbox> <el-link type="primary" :underline="false">忘记密码?</el-link> </div> </el-form-item> <el-form-item> <el-button class="button" type="primary" auto-insert-space >登录</el-button > </el-form-item> <el-form-item class="flex"> <el-link type="info" :underline="false" @click="isRegister = true"> 注册 → </el-link> </el-form-item> </el-form> </el-col> </el-row> </template> <style lang="scss" scoped> .login-page { height: 100vh; background-color: #fff; .bg { background: url('@/assets/logo2.png') no-repeat 60% center / 240px auto, url('@/assets/login_bg.jpg') no-repeat center / cover; border-radius: 0 20px 20px 0; } .form { display: flex; flex-direction: column; justify-content: center; user-select: none; .title { margin: 0 auto; } .button { width: 100%; } .flex { width: 100%; display: flex; justify-content: space-between; } } } </style>
3.1.el-row和el-col
类似于栅格布局
)
3.2.表单校验
-
数据绑定和校验规则配置
const formModel = ref({ username: '', password: '', repassword: '', }) const rules = { username: [ { required: true, message: '请输入用户名', trigger: 'blur', }, { min: 5, max: 10, message: '用户名必须是5-10位字符', trigger: 'blur', }, ], password: [ { // 1.非空校验 required: true, message: '请输入密码', trigger: 'blur', }, { // 2.正则校验 pattern: /^\S{6,15}$/, message: '密码必须是6-15位非空字符', trigger: 'blur', }, ], repassword: [ { required: true, message: '请输入密码', trigger: 'blur', }, { pattern: /^\S{6,15}$/, message: '密码必须是6-15位非空字符', trigger: 'blur', }, { // 3.自定义校验 validator: (rules, value, callback) => { if (value !== formModel.value.password) { callback(new Error('两次输入密码不一致')) } else { callback() } }, }, ], }
-
表单绑定
<el-form ref="form" size="large" autocomplete="off" v-if="isRegister" :rules="rules" :model="formModel" > </el-form>
-
输入框绑定
<el-form-item prop="username"> <el-input :prefix-icon="User" placeholder="请输入用户名" v-model="formModel.username" ></el-input> </el-form-item>
- 在el-form上,绑定:rules和:model对象
- 在el-form-item的prop绑定校验规则
- el-input绑定数据
3.3.注册前的预校验
需求:点击注册按钮需要对表单进行预校验,失败则显示校验提示
-
通过 ref 获取到 表单组件
const form = ref() <el-form ref="form">
-
注册之前进行校验
<el-button @click="register" class="button" type="primary" auto-insert-space > 注册 </el-button> const register = async () => { await form.value.validate() console.log('开始注册请求') }
3.4.封装 api 实现注册功能
在api文件夹下创建user.js封装api
-
api/user.js中
import request from '@/utils/request' // 注册接口 export const userRegisterService = ({ username, password, repassword }) => request.post('/api/reg', { username, password, repassword })
-
在LoginPage.vue中调用api
import { userRegisterService } from '@/api/user' const register = async () => { await form.value.validate() await userRegisterService(formModel.value) ElMessage.success('注册成功') isRegister.value = false }
-
绑定注册事件
<el-button class="button" type="primary" auto-insert-space @click="register"> 注册 </el-button>
3.5.登录页面
登录页面与首页实现基本相同
-
登录页校验和首页校验基本相同
表单绑定
<el-form ref="form" size="large" autocomplete="off" v-else :rules="rules" :model="formModel" >
输入框绑定
<el-form-item prop="username"> <el-input :prefix-icon="User" placeholder="请输入用户名" v-model="formModel.username" ></el-input> </el-form-item> <el-form-item prop="password"> <el-input name="password" :prefix-icon="Lock" type="password" placeholder="请输入密码" v-model="formModel.password" ></el-input> </el-form-item>
-
登录页与注册页共享一个formModule,不过注册成功后从登录页跳转到注册页时,清空formModule内容
// 切换到登录,并重置表单内容 watch(isRegister, () => { formModel.value = { username: '', password: '', repassword: '', } })
在使用watch之前,我们应该要导入
import { watch } from 'vue'
-
封装登录接口
// 登录接口 export const userLoginService = ({ username, password }) => request.post('/api/login', { username, password })
-
点击登录按钮,调用登录api,向服务器发送请求,保存响应结果中的token并跳转首页
import { useUserStore } from '@/stores' import { useRouter } from 'vue-router' // 实例化 const userStore = useUserStore() const router = useRouter() const login = async () => { await form.value.validate() const res = await userLoginService(formModel.value) // 保存token userStore.setToken(res.data.token) ElMessage.success('登录成功') // 跳转首页 router.push('/') }
-
绑定登录事件
<el-button class="button" type="primary" auto-insert-space @click="login">登录</el-button>
注意:跳转到首页后,提示框可能没有样式,这是因为vscode在你使用
ElMessage
时帮你自动生成了导入语句,但因为我们在前面已经配置了按需导入,所以不用写导入语句。解决方法:检查导入语句中是否有以下代码,删掉即可
import { ElMessage } from 'element-plus'
4.首页 layout 架子
-
结构、样式预备代码
<script setup> import { Management, Promotion, UserFilled, User, Crop, EditPen, SwitchButton, CaretBottom } from '@element-plus/icons-vue' import avatar from '@/assets/default.png' </script> <template> <el-container class="layout-container"> <el-aside width="200px"> <div class="el-aside__logo"></div> <el-menu active-text-color="#ffd04b" background-color="#232323" :default-active="$route.path" text-color="#fff" router > <el-menu-item index="/article/channel"> <el-icon><Management /></el-icon> <span>文章分类</span> </el-menu-item> <el-menu-item index="/article/manage"> <el-icon><Promotion /></el-icon> <span>文章管理</span> </el-menu-item> <el-sub-menu index="/user"> <template #title> <el-icon><UserFilled /></el-icon> <span>个人中心</span> </template> <el-menu-item index="/user/profile"> <el-icon><User /></el-icon> <span>基本资料</span> </el-menu-item> <el-menu-item index="/user/avatar"> <el-icon><Crop /></el-icon> <span>更换头像</span> </el-menu-item> <el-menu-item index="/user/password"> <el-icon><EditPen /></el-icon> <span>重置密码</span> </el-menu-item> </el-sub-menu> </el-menu> </el-aside> <el-container> <el-header> <div>黑马程序员:<strong>xiaojia</strong></div> <el-dropdown placement="bottom-end"> <span class="el-dropdown__box"> <el-avatar :src="avatar" /> <el-icon><CaretBottom /></el-icon> </span> <template #dropdown> <el-dropdown-menu> <el-dropdown-item command="profile" :icon="User" >基本资料</el-dropdown-item > <el-dropdown-item command="avatar" :icon="Crop" >更换头像</el-dropdown-item > <el-dropdown-item command="password" :icon="EditPen" >重置密码</el-dropdown-item > <el-dropdown-item command="logout" :icon="SwitchButton" >退出登录</el-dropdown-item > </el-dropdown-menu> </template> </el-dropdown> </el-header> <el-main> <router-view></router-view> </el-main> <el-footer>大事件 ©2023 Created by 黑马程序员</el-footer> </el-container> </el-container> </template> <style lang="scss" scoped> .layout-container { height: 100vh; .el-aside { background-color: #232323; &__logo { height: 120px; background: url('@/assets/logo.png') no-repeat center / 120px auto; } .el-menu { border-right: none; } } .el-header { background-color: #fff; display: flex; align-items: center; justify-content: space-between; .el-dropdown__box { display: flex; align-items: center; .el-icon { color: #999; margin-left: 10px; } &:active, &:focus { outline: none; } } } .el-footer { display: flex; align-items: center; justify-content: center; font-size: 14px; color: #666; } } </style>
如果对以上element UI标签不熟悉可以查看官方文档
4.1.登录访问拦截(导航守卫)
没有登录的用户,只能浏览登录页,所以我们应该对没有登录的用户做拦截
-
在router/index.js中
import { useUserStore } from '@/stores' // 登录访问拦截(这段代码要写在router后面) router.beforeEach(to => { const useStore = useUserStore() if (!useStore.token && to.path !== '/login') { return '/login' } })
router.beforeEach有(to,from)两个参数
to:即将进入的路由
from:当前导航正要离开的路由
默认是直接放行,根据返回值决定,是放行还是拦截
返回值 操作 undefined / true 直接放行 false 拦回from的地址页面 具体路径 或 路径对象 拦截到对应的地址
)
4.2.用户基本信息获取&渲染
-
api/user.js
封装接口// 获取用户信息 export const userGetInfoService = () => request.get('/my/userinfo')
-
stores/modules/user.js 定义数据
const user = ref({}) const getUser = async () => { const res = await userGetInfoService() // 请求获取数据 user.value = res.data.data } const setUser = (obj) => (user.value = obj) // 导出(和token一起导出的,这里忽略了关于token的代码) return { user, getUser, setUser }
这里大家不用忘了导入api
import { userGetInfoService } from '@/api/user'
-
layout/LayoutContainer
页面中调用import { useUserStore } from '@/stores' const userStore = useUserStore() onMounted(() => { userStore.getUser() })
如果这里要用onMounted,别忘了导入
import { onMounted } from 'vue'
-
动态渲染
<div> 黑马程序员:<strong>{{ userStore.user.nickname || userStore.user.username }}</strong> </div> <!-- 如果没有头像,可以登录已经写好的网页上传 --> <el-avatar :src="userStore.user.user_pic || avatar" />
4.3.退出登录
-
注册点击事件
<el-dropdown placement="bottom-end" @command="handleCommand"> <span class="el-dropdown__box"> <!-- 如果没有头像,可以登录已经写好的网页上传 --> <el-avatar :src="userStore.user.user_pic || avatar" /> <el-icon><CaretBottom /></el-icon> </span> <template #dropdown> <el-dropdown-menu> <el-dropdown-item command="profile" :icon="User">基本资料</el-dropdown-item> <el-dropdown-item command="avatar" :icon="Crop">更换头像</el-dropdown-item> <el-dropdown-item command="password" :icon="EditPen">重置密码</el-dropdown-item> <el-dropdown-item command="logout" :icon="SwitchButton">退出登录</el-dropdown-item> </el-dropdown-menu> </template> </el-dropdown>
-
导入router并实例化
import { useRouter } from 'vue-router' const router = useRouter()
-
在
api/modules/user.js
中添加removeTokenconst removeToken = () => { token.value = '' }
-
添加相应功能
// 下拉框菜单 const handleCommand = (key) => { if (key === 'logout') { // 退出确认 ElMessageBox.confirm('确定要退出登录吗?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning', }).then(() => { ElMessage.success('退出登录成功') // 退出 userStore.removeToken() userStore.setUser({}) router.push('/login') }) } else { router.push(`/user/${key}`) } }
5.文章分类页面
5.1.基本架子
-
基本结构样式,用到了 el-card 组件
<template> <el-card class="page-container"> <template #header> <div class="header"> <span>文章分类</span> <div class="extra"> <el-button type="primary">添加分类</el-button> </div> </div> </template> ... </el-card> </template> <style lang="scss" scoped> .page-container { min-height: 100%; box-sizing: border-box; .header { display: flex; align-items: center; justify-content: space-between; } } </style>
-
考虑到多个页面复用,封装成组件(在
@/components/PageContainer.vue
)- props 定制标题
- 默认插槽 default 定制内容主体
- 具名插槽 extra 定制头部右侧额外的按钮
<script setup> defineProps({ title: { required: true, type: String } }) </script> <template> <el-card class="page-container"> <template #header> <div class="header"> <span>{{ title }}</span> <div class="extra"> <slot name="extra"></slot> </div> </div> </template> <slot></slot> </el-card> </template> <style lang="scss" scoped> .page-container { min-height: 100%; box-sizing: border-box; .header { display: flex; align-items: center; justify-content: space-between; } } </style>
-
页面中直接使用 ( unplugin-vue-components 会自动注册)
文章分类测试:
<template> <page-container title="文章分类"> <template #extra> <el-button type="primary"> 添加分类 </el-button> </template> 主体部分 </page-container> </template>
文章管理测试:
<template> <page-container title="文章管理"> <template #extra> <el-button type="primary">发布文章</el-button> </template> 主体部分 </page-container> </template>
5.2.文章分类渲染
-
新建
api/article.js
封装获取频道列表的接口// 获取频道列表 export const artGetChannelsService = () => request.get('/my/cate/list')
-
页面中调用接口,获取数据存储
const channelList = ref([]) const getChannelList = async () => { const res = await artGetChannelsService() channelList.value = res.data.data }
5.3.el-table 表格动态渲染
- 代码(替换主体部分):
<script setup> const onEditChannel = (row) => { console.log(row) } const onDelChannel = (row) => { console.log(row) } <script> <el-table :data="channelList" style="width: 100%"> <el-table-column label="序号" width="100" type="index"> </el-table-column> <el-table-column label="分类名称" prop="cate_name"></el-table-column> <el-table-column label="分类别名" prop="cate_alias"></el-table-column> <el-table-column label="操作" width="100"> <template #default="{ row }"> <el-button :icon="Edit" circle plain type="primary" @click="onEditChannel(row)" ></el-button> <el-button :icon="Delete" circle plain type="danger" @click="onDelChannel(row)" ></el-button> </template> </el-table-column> <template #empty> <el-empty description="没有数据" /> </template> </el-table>
5.4.el-table 表格 loading 效果
-
定义变量,v-loading绑定
const loading = ref(false) <el-table v-loading="loading">
-
发送请求前开启,请求结束关闭
const getChannelList = async () => { loading.value = true const res = await artGetChannelsService() channelList.value = res.data.data loading.value = false }
5.5.添加分类弹窗
-
创建变量控制弹窗显示,并创建方法控制变量
const dialogVisible = ref(false) const onAddChnanel = () => { dialogVisible.value = true }
-
创建弹窗html结构(与el-table平级位置),并将显示和变量绑定
<el-dialog v-model="dialogVisible" title="添加弹层" width="30%"> <div>哒哒哒</div> <template #footer> <span class="dialog-footer"> <el-button @click="dialogVisible = false">取消</el-button> <el-button type="primary"> 确认 </el-button> </span> </template> </el-dialog>
-
将按钮和方法绑定
<el-button @click="onAddChnanel">添加分类</el-button>
5.6.封装弹层组件
-
封装组件
article/components/ChannelEdit.vue
添加 和 编辑,可以共用一个弹层,所以可以将弹层封装成一个组件,组件对外暴露一个方法 open, 基于 open 的参数,初始化表单数据,并判断区分是添加 还是 编辑
传参 操作 open({ }) 添加操作,添加表单初始化无数据 open({ id: xx, … }) 编辑操作,编辑表单初始化需回显 <script setup> import { ref } from 'vue' const dialogVisible = ref(false) const open = async (row) => { dialogVisible.value = true console.log(row) } // 对外暴露open方法 defineExpose({ open }) </script> <template> <el-dialog v-model="dialogVisible" title="添加弹层" width="30%"> <div>我是内容部分</div> <!-- 取消确认 --> <template #footer> <span class="dialog-footer"> <el-button @click="dialogVisible = false">取消</el-button> <el-button type="primary"> 确认 </el-button> </span> </template> </el-dialog> </template>
-
通过 ref 绑定(用channel-edit组件替换el-dialog)
const dialog = ref() <!-- 弹窗 --> <channel-edit ref="dialog"></channel-edit>
-
导入组件(因为我们没有在
@/components/
创建ChannelEdit.vue组件,不会被unplugin-vue-components 自动注册)import ChannelEdit from './components/ChannelEdit.vue'
-
点击调用方法显示弹窗
const onAddChannel = () => { dialog.value.open({}) } const onEditChannel = (row) => { dialog.value.open(row) }
注意,这里可能会报一些错误,重启项目就好了
5.7.准备弹层表单
-
准备数据 和 校验规则
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' } ] }
-
准备表单
<!-- 内容部分 --> <el-form :model="formModel" :rules="rules" label-width="100px" style="padding-right: 30px" > <el-form-item label="分类名称" prop="cate_name"> <el-input v-model="formModel.cate_name" minlength="1" maxlength="10" ></el-input> </el-form-item> <el-form-item label="分类别名" prop="cate_alias"> <el-input v-model="formModel.cate_alias" minlength="1" maxlength="15" ></el-input> </el-form-item> </el-form> <!-- 取消确认 --> <template #footer> <span class="dialog-footer"> <el-button @click="dialogVisible = false">取消</el-button> <el-button type="primary" @click="onSubmit"> 确认 </el-button> </span> </template>
-
编辑需要回显,表单数据需要初始化
const open = async (row) => { dialogVisible.value = true // 这里用展开运算符拷贝数据 formModel.value = { ...row } }
-
基于传过来的表单数据,进行标题控制,有 id 的是编辑
<el-dialog v-model="dialogVisible" :title="formModel.id ? '编辑分类' : '添加分类'" width="30%" >
5.8.确认提交
-
api/article.js
封装请求 API// 添加文章分类 export const artAddChannelService = (data) => request.post('/my/cate/add', data) // 编辑文章分类 export const artEditChannelService = (data) => request.put('/my/cate/info', data)
-
页面中校验,判断,提交请求
<el-form ref="formRef">
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 }
-
通知父组件进行回显(即刷新页面)
const emit = defineEmits(['success']) const onSubmit = async () => { ... emit('success') }
-
父组件监听 success 事件,进行调用回显
<channel-edit ref="dialog" @success="onSuccess"></channel-edit> const onSuccess = () => { getChannelList() }
5.9.删除分类
-
api/article.js
封装接口 api// 删除文章分类 export const artDelChannelService = (id) => request.delete('/my/cate/del', { params: { id } })
-
页面中添加确认框,调用接口进行提示
const onDelChannel = async (row) => { await ElMessageBox.confirm('你确认删除该分类信息吗?', '温馨提示', { type: 'warning', confirmButtonText: '确认', cancelButtonText: '取消' }) await artDelChannelService(row.id) ElMessage({ type: 'success', message: '删除成功' }) getChannelList() }
6.文章管理页面
6.1.表单区域
-
基本架子
<!-- 表单区域 --> <el-form inline> <el-form-item label="文章分类:"> <el-select style="width: 200px"> <el-option label="新闻" value="111"></el-option> <el-option label="体育" value="222"></el-option> </el-select> </el-form-item style="width: 200px"> <el-form-item label="发布状态:"> <el-select> <el-option label="已发布" value="已发布"></el-option> <el-option label="草稿" value="草稿"></el-option> </el-select> </el-form-item> <el-form-item> <el-button type="primary">搜索</el-button> <el-button>重置</el-button> </el-form-item> </el-form>
表单外面还嵌套了上面封装的
page-container
组件,基本布局如下<page-container title="文章管理"> <template #extra> <el-button @click="onAddArticle">添加文章</el-button> </template> <!-- 表单区域 --> <el-form inline> ... </el-form> <!-- 表格区域 --> ... <!-- 分页区域 --> ... <!-- 抽屉区域 --> ... </page-container>
-
表格准备,模拟假数据渲染
import { Delete, Edit } from '@element-plus/icons-vue' import { ref } from 'vue' // 假数据 const articleList = ref([ { id: 5961, title: '文章1', pub_date: '2024-07-10 14:53:52.604', state: '已发布', cate_name: '体育' }, { id: 5962, title: '文章2', pub_date: '2024-07-10 14:54:30.904', state: null, cate_name: '体育' } ])
6.2.表格区域
-
基本架子
<el-table :data="articleList" style="width: 100%"> <el-table-column label="文章标题" width="400"> <template #default="{ row }"> <el-link type="primary" :underline="false">{{ row.title }}</el-link> </template> </el-table-column> <el-table-column label="分类" prop="cate_name"></el-table-column> <el-table-column label="发表时间" prop="pub_date"> </el-table-column> <el-table-column label="状态" prop="state"></el-table-column> <el-table-column label="操作" width="100"> <template #default="{ row }"> <el-button :icon="Edit" circle plain type="primary" @click="onEditArticle(row)" ></el-button> <el-button :icon="Delete" circle plain type="danger" @click="onDeleteArticle(row)" ></el-button> </template> </el-table-column> <template #empty> <el-empty description="没有数据" /> </template> </el-table> const onEditArticle = (row) => { console.log(row) } const onDeleteArticle = (row) => { console.log(row) }
6.3.中英国际化处理
-
默认是英文的,由于这里不涉及切换, 所以在 App.vue 中直接导入设置成中文即可
<script setup> import zh from 'element-plus/es/locale/lang/zh-cn.mjs' </script> <template> <!-- 国际化处理 --> <el-config-provider :locale="zh"> <router-view /> </el-config-provider> </template>
6.4.封装【分类选择】
如图。由于文章分类选择下拉框在表单区域和抽屉区域都用到了,我们可以将其封装成组件
-
新建 article/components/ChannelSelect.vue
<template> <el-select style="width: 200px"> <el-option label="新闻" value="新闻"></el-option> <el-option label="体育" value="体育"></el-option> </el-select> </template>
-
在页面中导入使用
// 导入 import ChannelSelect from './components/ChannelSelect.vue' // 使用 <el-form-item label="文章分类:"> <channel-select></channel-select> </el-form-item>
-
在src/api/ariticle.js中封装获取文章分类的api
// 获取文章分类 export const artGetChannelsService = () => request.get('/my/cate/list')
-
调用接口,动态渲染下拉分类
// ChannelSelect.vue,js部分 import { artGetChannelsService } from '@/api/article' import { ref } from 'vue' defineProps({ modelValue: { type: [Number, String] } }) const emit = defineEmits(['update:modelValue']) const channelList = ref([]) const getChannelList = async () => { const res = await artGetChannelsService() channelList.value = res.data.data } getChannelList()
<!-- ChannelSelect.vue,html部分 --> <el-select style="width: 200px" :modelValue="modelValue" @update:modelValue="emit('update:modelValue', $event)" > <el-option v-for="channel in channelList" :key="channel.id" :label="channel.cate_name" :value="channel.id" ></el-option> </el-select>
-
父组件定义参数绑定
const params = ref({ pagenum: 1, pagesize: 5, cate_id: '', state: '' }) <channel-select v-model="params.cate_id"></channel-select>
-
绑定发布状态,便于将来提交表单
<el-select v-model="params.state"> <el-option label="已发布" value="已发布"></el-option> <el-option label="草稿" value="草稿"></el-option> </el-select>
6.5.封装请求渲染文章API
如果没有数据,可以登录已完成的系统,添加几条数据
-
api/article.js
封装接口// 请求渲染文章 export const artGetListService = (params) => request.get('/my/article/list', { params })
-
页面中调用接口
import { artGetListService } from '@/api/article' // 这里把前面的假数据articleList清空 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()
如果这里报错
不能识别params
,请把params定义写到getArticleList方法前面 -
新建
utils/format.js
封装格式化日期函数import { dayjs } from 'element-plus' export const formatTime = (time) => dayjs(time).format('YYYY年MM月DD日')
-
导入使用
import { formatTime } from '@/utils/format' <el-table-column label="发表时间"> <template #default="{ row }"> {{ formatTime(row.pub_date) }} </template> </el-table-column>
6.6.分页渲染 [element-plus 分页]
-
分页组件
<el-pagination v-model:current-page="params.pagenum" v-model:page-size="params.pagesize" :page-sizes="[2, 3, 4, 5, 10]" layout="jumper, total, sizes, prev, pager, next" background :total="total" @size-change="onSizeChange" @current-change="onCurrentChange" style="margin-top: 20px; justify-content: flex-end" />
-
提供分页修改逻辑
const onSizeChange = (size) => { params.value.pagenum = 1 params.value.pagesize = size getArticleList() } const onCurrentChange = (page) => { params.value.pagenum = page getArticleList() }
6.7.添加 loading 处理
-
准备数据
const loading = ref(false)
-
el-table上面绑定
<el-table v-loading="loading" > ... </el-table>
-
发送请求时添加 loading
const getArticleList = async () => { loading.value = true ... loading.value = false } getArticleList()
6.8.搜索 和 重置功能
-
注册事件
<el-form-item> <el-button @click="onSearch" type="primary">搜索</el-button> <el-button @click="onReset">重置</el-button> </el-form-item>
-
绑定处理
const onSearch = () => { params.value.pagenum = 1 getArticleList() } const onReset = () => { params.value.pagenum = 1 params.value.cate_id = '' params.value.state = '' getArticleList() }
7.文章管理【抽屉区域】
7.1.点击显示抽屉
-
准备数据
import { ref } from 'vue' const visibleDrawer = ref(false)
-
准备抽屉容器
<el-drawer v-model="visibleDrawer" title="大标题" direction="rtl" size="50%" > <span>Hi there!</span> </el-drawer>
-
点击修改布尔值显示抽屉
<el-button type="primary" @click="onAddArticle">发布文章</el-button> const visibleDrawer = ref(false) const onAddArticle = () => { visibleDrawer.value = true }
7.2.封装抽屉组件
添加 和 编辑,可以共用一个抽屉,所以可以将抽屉封装成一个组件
组件对外暴露一个方法 open, 基于 open 的参数,初始化表单数据,并判断区分是添加 还是 编辑
-
封装组件
article/components/ArticleEdit.vue
<script setup> import { ref } from 'vue' const visibleDrawer = ref(false) const open = (row) => { visibleDrawer.value = true console.log(row) } defineExpose({ open }) </script> <template> <!-- 抽屉 --> <el-drawer v-model="visibleDrawer" title="大标题" direction="rtl" size="50%"> <span>Hi there!</span> </el-drawer> </template>
-
导入组件
import ArticleEdit from './components/ArticleEdit.vue'
-
通过 ref 绑定
const articleEditRef = ref() <!-- 弹窗 --> <article-edit ref="articleEditRef"></article-edit>
-
点击显示抽屉
// 编辑新增逻辑 const onAddArticle = () => { articleEditRef.value.open({}) } const onEditArticle = (row) => { articleEditRef.value.open(row) }
7.3.完善抽屉表单结构
-
准备数据
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 表单结构
import ChannelSelect from './ChannelSelect.vue' <template> <el-drawer v-model="visibleDrawer" :title="formModel.id ? '编辑文章' : '添加文章'" direction="rtl" size="50%" > <!-- 发表文章表单 --> <el-form :model="formModel" ref="formRef" label-width="100px"> <el-form-item label="文章标题" prop="title"> <el-input v-model="formModel.title" placeholder="请输入标题"></el-input> </el-form-item> <el-form-item label="文章分类" prop="cate_id"> <channel-select v-model="formModel.cate_id" width="100%" ></channel-select> </el-form-item> <el-form-item label="文章封面" prop="cover_img"> 文件上传 </el-form-item> <el-form-item label="文章内容" prop="content"> <div class="editor">富文本编辑器</div> </el-form-item> <el-form-item> <el-button type="primary">发布</el-button> <el-button type="info">草稿</el-button> </el-form-item> </el-form> </el-drawer> </template>
-
一打开默认重置添加的 form 表单数据
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 设置
defineProps({ modelValue: { type: [Number, String] }, width: { type: String } }) <el-select ... :style="{ width }"
这里是通过父传子,在不同地方使用同一个组件的宽度不一样,也可以为ChannelSelec组件t设置
style="width: 100%"
,再为包裹
7.4.上传文件
-
关闭自动上传,准备结构
import { Plus } from '@element-plus/icons-vue' <el-upload class="avatar-uploader" :auto-upload="false" :show-file-list="false" :on-change="onUploadFile" > <img v-if="imgUrl" :src="imgUrl" class="avatar" /> <el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon> </el-upload>
-
准备数据 和 选择图片的处理逻辑
const imgUrl = ref('') const onUploadFile = (uploadFile) => { imgUrl.value = URL.createObjectURL(uploadFile.raw) formModel.value.cover_img = uploadFile.raw }
-
样式美化
.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; } } }
7.5.富文本编辑器 【vue-quill】
官网地址:https://vueup.github.io/vue-quill/
-
安装包
pnpm add @vueup/vue-quill@latest
-
注册成局部组件
import { QuillEditor } from '@vueup/vue-quill' import '@vueup/vue-quill/dist/vue-quill.snow.css'
-
页面中使用绑定
<div class="editor"> <quill-editor theme="snow" v-model:content="formModel.content" contentType="html" > </quill-editor> </div>
-
样式美化
.editor { width: 100%; :deep(.ql-editor) { min-height: 200px; } }
7.6.添加文章功能
-
封装添加接口
export const artPublishService = (data) => request.post('/my/article/add', data)
-
注册点击事件调用
<el-form-item> <el-button @click="onPublish('已发布')" type="primary">发布</el-button> <el-button @click="onPublish('草稿')" type="info">草稿</el-button> </el-form-item> // 发布文章 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') } }
-
父组件监听事件,重新渲染
<article-edit ref="articleEditRef" @success="onSuccess"></article-edit> // 添加修改成功 const onSuccess = (type) => { if (type === 'add') { // 如果是添加,需要跳转渲染最后一页,编辑直接渲染当前页 const lastPage = Math.ceil((total.value + 1) / params.value.pagesize) params.value.pagenum = lastPage } getArticleList() }
7.7.添加完成后的内容重置
-
代码:
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('') } }
7.8.编辑文章回显
如果是编辑操作,一打开抽屉,就需要发送请求,获取数据进行回显
-
封装接口,根据 id 获取详情数据
export const artGetDetailService = (id) => request.get('my/article/info', { params: { id } })
-
页面中调用渲染
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('添加功能') ... } }
chatGPT prompt:封装一个函数,基于 axios, 网络图片地址,转 file 对象, 请注意:写中文注释
// 将网络图片地址转换为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; } }
7.9.编辑文章功能
-
封装编辑接口
export const artEditService = (data) => request.put('my/article/info', data)
-
提交时调用
const onPublish = async (state) => { ... if (formModel.value.id) { await artEditService(fd) ElMessage.success('编辑成功') visibleDrawer.value = false emit('success', 'edit') } else { // 添加请求 ... } }
7.10文章删除
-
封装删除接口
export const artDelService = (id) => request.delete('my/article/info', { params: { id } })
-
页面中添加确认框调用
const onDeleteArticle = async (row) => { await ElMessageBox.confirm('你确认删除该文章信息吗?', '温馨提示', { type: 'warning', confirmButtonText: '确认', cancelButtonText: '取消' }) await artDelService(row.id) ElMessage({ type: 'success', message: '删除成功' }) getArticleList() }
8.AI编码
8.1.AI编码的基本使用
三方整合产品
- 谷歌搜索:chatgpt 免费网站列表
- https://github.com/LiLittleCat/awesome-free-chatgpt
我们可以尝试以下策略来优化我们的提问:
-
明确提问:
确保问题表述清晰明确,关键字的准确度,决定了AI 对于需求的理解。
-
细化需求:
将问题拆分成多个小问题,可以帮助AI更具针对性地回答,也利于即时纠错。
-
添加背景信息:
提供有关问题背景的详细信息,也可以给 AI 预设一个角色,将有助于AI生成更具深度和价值的回答。
-
适当引导:
比如:“例如”、“请注意”、“请使用”等,来告诉模型你期望它做什么 或者 不做什么
-
限制范围:
通过限定回答的范围和长度,可以引导AI生成更精炼的回答
8.2.通意灵码
打开 vscode, 搜索并安装插件TONGYI Lingma
8.3.【实践】静态结构 + 校验处理
-
提示词参考:
请基于 elementPlus 和 Vue3 的语法,生成组件代码
要求:
一、表单结构要求:- 组件中包含一个el-form表单,有四行内容,前三行是输入框,第四行是按钮
- 第一行 label 登录名称,输入框禁用不可输入状态
- 第二行 label 用户昵称,输入框可输入
- 第三行 label 用户邮箱,输入框可输入
- 第四行按钮,提交修改
二、校验需求:给昵称 和 邮箱添加校验
- 昵称 nickname 必须是2-10位的非空字符串
- 邮箱 email 符合邮箱格式即可,且不能为空
-
参考目标代码:
<script setup> import { ref } from 'vue' import { ElMessage } from 'element-plus' import { useUserStore } from '@/stores' import { userUpdateInfoService } from '@/api/user' // 只用仓库中初始值(无需响应式) const { user: { email, id, nickname, username }, } = useUserStore() const form = ref({ username, nickname, email, id, }) // 校验规则 const rules = { nickname: [ { required: true, message: '用户昵称不能为空', trigger: 'blur' }, { min: 2, max: 10, message: '用户昵称必须是2-10位字符', trigger: 'blur', }, ], email: [ { required: true, message: '用户邮箱不能为空', trigger: 'blur' }, { type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change'], }, ], } const formRef = ref(null) const resetForm = () => { formRef.value.resetFields() } </script> <template> <page-container title="用户信息"> <div class="custom-form"> <el-form :model="form" :rules="rules" ref="formRef" label-width="120px"> <el-form-item label="登录名称" prop="username"> <el-input v-model="form.username" disabled></el-input> </el-form-item> <el-form-item label="用户昵称" prop="nickname"> <el-input v-model="form.nickname"></el-input> </el-form-item> <el-form-item label="用户邮箱" prop="email"> <el-input v-model="form.email"></el-input> </el-form-item> <el-form-item> <el-button type="primary" >提交修改</el-button> <el-button @click="resetForm('formRef')">重置</el-button> </el-form-item> </el-form> </div> </page-container> </template> <style scoped> .custom-form { width: 100%; margin: 10px auto; } </style>
8.4.【实践】更新个人信息
-
封装接口
// 更新个人信息 export const userUpdateInfoService = ({ id, nickname, email }) => request.put('/my/userinfo', { id, nickname, email })
-
页面中校验后,封装调用
const onSubmit = async () => { // 调用表单引用上的validate方法进行表单验证 await formRef.value.validate(valid => { if (valid) { ElMessage.success('提交成功!') userUpdateInfoService(form.value) } else { ElMessage.error('提交信息错误!') } }) }
-
绑定按钮
<el-button type="primary" @click="onSubmit">提交修改</el-button>
8.5.【实践】更换头像
-
静态结构
<script setup> import { ref } from 'vue' import { Plus, Upload } from '@element-plus/icons-vue' import { useUserStore } from '@/stores' const userStore = useUserStore() const imgUrl = ref(userStore.user.user_pic) const onUploadFile = (file) => { console.log(file) } </script> <template> <page-container title="更换头像"> <el-row> <el-col :span="12"> <el-upload ref="uploadRef" class="avatar-uploader" :auto-upload="false" :show-file-list="false" :on-change="onUploadFile" > <img v-if="imgUrl" :src="imgUrl" class="avatar" /> <img v-else src="@/assets/avatar.jpg" width="278" /> </el-upload> <br /> <el-button type="primary" :icon="Plus" size="large"> 选择图片 </el-button> <el-button type="success" :icon="Upload" size="large"> 上传头像 </el-button> </el-col> </el-row> </page-container> </template> <style lang="scss" scoped> .avatar-uploader { :deep() { .avatar { width: 278px; height: 278px; 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: 278px; height: 278px; text-align: center; } } } </style>
-
选择预览图片
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 } } <el-upload ref="uploadRef"></el-upload> <el-button @click="uploadRef.$el.querySelector('input').click()" type="primary" :icon="Plus" size="large" >选择图片</el-button >
-
上传头像
-
封装接口
// 上传头像 export const userUploadAvatarService = (avatar) => request.patch('/my/update/avatar', { avatar })
-
调用接口
const onUpdateAvatar = async () => { await userUploadAvatarService(imgUrl.value) await userStore.getUser() ElMessage.success('上传成功') }
-
绑定按钮
<el-button type="success" :icon="Upload" size="large" @click="onUpdateAvatar"> 上传头像 </el-button> </el-col>
-
8.6.【实践】重置密码
-
提示词参考:
请基于 elementPlus 和 Vue3 的语法,生成组件代码
要求:
一、表单结构要求- 组件中包含一个el-form表单,有四行内容,前三行是表单输入框,第四行是两个按钮
- 第一行 label 原密码
- 第二行 label 新密码
- 第三行 label 确认密码
- 第四行两个按钮,修改密码 和 重置
二、form绑定字段如下:
const pwdForm = ref({
old_pwd: ‘’,
new_pwd: ‘’,
re_pwd: ‘’
})三、校验需求
所有字段,都是 6-15位 非空
自定义校验1:原密码 和 新密码不能一样
自定义校验2:新密码 和 确认密码必须一样 -
静态结构 + 校验处理
<script setup> import { ref } from 'vue' const pwdForm = ref({ old_pwd: '', new_pwd: '', re_pwd: '' }) const checkOldSame = (rule, value, cb) => { if (value === pwdForm.value.old_pwd) { cb(new Error('原密码和新密码不能一样!')) } else { cb() } } const checkNewSame = (rule, value, cb) => { if (value !== pwdForm.value.new_pwd) { cb(new Error('新密码和确认再次输入的新密码不一样!')) } else { cb() } } const rules = { // 原密码 old_pwd: [ { required: true, message: '请输入密码', trigger: 'blur' }, { pattern: /^\S{6,15}$/, message: '密码长度必须是6-15位的非空字符串', trigger: 'blur' } ], // 新密码 new_pwd: [ { required: true, message: '请输入新密码', trigger: 'blur' }, { pattern: /^\S{6,15}$/, message: '密码长度必须是6-15位的非空字符串', trigger: 'blur' }, { validator: checkOldSame, trigger: 'blur' } ], // 确认新密码 re_pwd: [ { required: true, message: '请再次确认新密码', trigger: 'blur' }, { pattern: /^\S{6,15}$/, message: '密码长度必须是6-15位的非空字符串', trigger: 'blur' }, { validator: checkNewSame, trigger: 'blur' } ] } </script> <template> <page-container title="重置密码"> <el-row> <el-col :span="12"> <el-form :model="pwdForm" :rules="rules" ref="formRef" label-width="100px" size="large" > <el-form-item label="原密码" prop="old_pwd"> <el-input v-model="pwdForm.old_pwd" type="password"></el-input> </el-form-item> <el-form-item label="新密码" prop="new_pwd"> <el-input v-model="pwdForm.new_pwd" type="password"></el-input> </el-form-item> <el-form-item label="确认新密码" prop="re_pwd"> <el-input v-model="pwdForm.re_pwd" type="password"></el-input> </el-form-item> <el-form-item> <el-button @click="onSubmit" type="primary">修改密码</el-button> <el-button @click="onReset">重置</el-button> </el-form-item> </el-form> </el-col> </el-row> </page-container> </template>
-
更新密码信息
-
封装接口
// 更改密码 export const userUpdatePassService = ({ old_pwd, new_pwd, re_pwd }) => request.patch('/my/updatepwd', { old_pwd, new_pwd, re_pwd })
-
页面中调用
import { userUpdatePassService } from '@/api/user' import { useRouter } from 'vue-router' import { useUserStore } from '@/stores' import { ref } from 'vue' 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() }
-