黑马大事件项目日志
github地址
文章目录
创建项目
pnpm
安装
npm install -g pnpm
创建项目
pnpm create vue
运行项目
pnpm dev
项目目录设置

路由配置
在router文件夹下创建一个index.js文件
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [],
})
export default router
两种模式,分别是hash和history模式
- history不传递#
- hash会传递一个#
关于参数
import.meta.env.BASE_URL
这个配置项在vite.config.js中,
能够配置一个基地址
plugins:[
base:'/'
]
main.js中导入并使用
import router from './router'
app.use(router)
组件中使用测试
App.vue
<template>
<el-button @click="$router.push('/home')">首页</el-button>
<el-button @click="goList">列表页</el-button>
</template>
<script setup>
import {useRouter} from 'vue-router'
const router = useRouter()
const goList = ()=>{
router.push('/list')
}
</script>
pinia配置
在store文件夹下创建modules文件夹,将每种需要存储的数据放在对应的js文件中,同时将pinia的初始配置写在index.js文件夹中,最后交给main.js使用
index.js
//这里是有关pinia的的配置
import { createPinia } from 'pinia'
//使用插件实现持久化存储
import persist from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(persist)
export default pinia
// import { useUserStore } from './modules/user'
// export { useUserStore }
// import { useCountStore } from './modules/count'
// export { useCountStore }
//对各个模块中的数据进行单独的导出和导入
export * from './modules/user'
export * from './modules/count'
main.js
import pinia from './stores'
app.use(pinia)
user.js
import { defineStore } from 'pinia'
import { ref } from 'vue'
// 用户模块
export const useUserStore = defineStore(
//唯一标识
'big-user',
() => {
const token = ref('') // 定义 token
const setToken = (newToken) => (token.value = newToken) // 设置 token
const removeToken = () => (token.value = '')
return { token, setToken, removeToken }
},
{
persist: true, // 持久化
},
)
为了对数据进行一个持久化的处理,我们需要安装一个pinia插件
pnpm add pinia-plugin-persistedstate -D
Element-Plus的基本配置
官方文档 https://element-plus.org/zh-CN/
- 安装
pnpm add element-plus
按需引入
- 安装插件
pnpm add -D unplugin-vue-components unplugin-auto-import
- 以下代码配置到vite或者webpack中
...
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()]
})
]
})
axios请求配置
项目接口根路径 http://big-event-vue-api-t.itheima.net
接口文档https://apifox.com/apidoc/shared-26c67aee-0233-4d23-aab7-08448fdf95ff/api-93850835
新建立utils/request.js文件
首先需要安装axios
pnpm add axios
request.js
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. 摘取核心响应数据
// res.data数据
if (res.data.code === 0) {
return res
}
//处理业务失败,给出错误提示,抛出错误
ElMessage.error(res.data.message || '服务异常')
return Promise.reject(res.data)
},
(err) => {
// TODO 5. 处理401错误
//错误的特殊情况401权限不足或者token过期,则拦截登录
if (err.response?.status === 401) {
router.push('./login')
}
//错误的默认情况,给提示就好
ElMessage.error(err.response.data.message || '服务异常')
return Promise.reject(err)
},
)
//导出实例
export default instance
//导出基地址
export { baseURL }
通过观看接口文档
header中需要传递Authorization
路由基本配置
- 登录——一级路由
- 架子——一级路由
- 文章分类——二级路由
- 文章管理——二级路由
- 基本资料——二级路由
- 更换头像——二级路由
- 重置密码——二级路由
| 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, createWebHashHistory } from 'vue-router'
//创建路由实例
//1 .createWebHashHistoryhash模式
//2 .createWebHistoryhistory模式
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
登录注册页面
element-plus表单
1. 注册登录 静态页面结构和i基本切换
2. 注册功能(校验和注册)
3. 登录功能(校验+登录+存储token)
安装element-plus图标库
pnpm i @element-plus/icons-vue
图标地址:https://element-plus.org/zh-CN/component/icon.html
登录注册静态页面结构
login/LoginPage.vue静态结构
<script setup>
//引入User和Lock图标
import { User, Lock } from '@element-plus/icons-vue'
import { ref } from 'vue'
//用来切换注册和登录页的
const isRegister = ref(true)
</script>
<template>
<!-- el-row表示的是一行,分成24分 -->
<!-- 左右el-col表示的是列各占12分
:span="12"表示代表在一行中占12分
:span="6" 表示在一行中占6分
:offset="3"表示在一行中 ,左侧的margin的分数
el-form :整个表单组件
el-form-item:表单的一行(一个表单域)
el-input 表单元素(输入框)
-->
<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>
注册表单验证规则
Element-plus表单文档
https://element-plus.org/zh-CN/component/form.html
校验相关
-
<el-form>标签<el-form :model='ruleForm'>绑定整个form表单数据对象{xxx,xxx,xxx}
-
<el-form>标签<el-form :rules='rules'>绑定整个表单rules检验规则对象{xxx,xxx,xxx}
-
表单元素标签<el-form v-model="ruleForm.xxx">绑定form的子属性
-
<el-form-item>标签<el-form-item prop="xxx"></el-form-item>
校验规则和校验对象
1. `检验表单数据对象`,(应该和接口文档中保持一致)
注册接口文档地址:https://apifox.com/apidoc/shared-26c67aee-0233-4d23-aab7-08448fdf95ff/api-93850058
从接口文档中查看参数,需保持一致
const formModel =ref({
username:'',
password:'',
repassword:''
})
-
校验规则-
非空校验
require:ture message:消息提示 trigger:触发时机(blur,change……) -
长度校验
min:xxx max:xxx -
正则校验
pattern:正则表达式 -
自定义校验
自己写逻辑校验(校验函数) validaor:(rule,value,callback) (1) rule 当前检验规则相关信息 (2) value 所校验的表单元素,目前的表单值 (3) callback 回调函数 callback() 校验成功 callback(new Error(错误信息)) 校验失败
-
配置规则和表单代码
const formModel = ref({
username: '',
password: '',
repassword: '',
})
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: (rule, value, callback) => {
//判断value 和当前form中收集的password是否一致
if (value !== formModel.value.password) {
callback(new Error('两次输入的密码不一致'))
} else {
callback() //校验成功需要正常回调,callback
}
},
trigger: 'blur',
},
注册表单元素的配置代码
<!-- 注册相关表单 -->
<el-form
:model="formModel"
:rules="rules"
ref="form"
size="large"
autocomplete="off"
v-if="isRegister"
>
<!--1注册标题-->
<el-form-item>
<h1>注册</h1>
</el-form-item>
<!--2用户名输入-->
<el-form-item prop="username">
<el-input
v-model="formModel.username"
:prefix-icon="User"
placeholder="请输入用户名"
></el-input>
</el-form-item>
<!--3密码输入-->
<el-form-item prop="password">
<el-input
v-model="formModel.password"
:prefix-icon="Lock"
type="password"
placeholder="请输入密码"
>
</el-input>
</el-form-item>
<!--4确认密码输入-->
<el-form-item prop="repassword">
<el-input
v-model="formModel.repassword"
:prefix-icon="Lock"
type="password"
placeholder="请输入再次密码"
></el-input>
</el-form-item>
<!--5注册-->
<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>
界面提示展示

封装api实现注册登录功能
- 首先注册前的预校验
需要绑定表单数据
const form = ref()
<el-form ref="form">
-
validate对整个表单的内容进行验证。 接收一个回调函数,或返回
Promise
文档链接
https://element-plus.org/zh-CN/component/form.html
查看接口文档
注册

登录

api/user.js
import request from '@utils/request'
//注册接口
export const userRegisterService =({username,password,repassword})=>{
return request.post('api/reg',{
username,
password,
repassword
})
}
//登录接口
export const userLoginService = ({
username,password})=>{
return request.post('api/login',{
username,
password
})
}
细节:命名需要见名知意,以后代码很多很多的时候也方便理解和查看
LoginPage.vue
为注册和登录按钮添加事件
- register
- login
<el-button
@click="login"
class="button"
type="primary"
auto-insert-space>
登录
</el-button>
<!------------------->
<el-button
@click="register"
class="button"
type="primary"
auto-insert-space>
注册
</el-button>
Loginpage.vue
import { userRegisterService, userLoginService } from '@/api/user'
//导入用户模块仓库(为了存储token)
import {userUserStore} from '@/stores'
//导入路由 (首页跳转)
import {useRouter} from 'vue-router'
//注册校验和提交
const register = async () =>{
await form.value.validate()
await userRegisterService(formModel.value)
ElMessage.success('注册成功')
//切换到登录
isRegister.value = false
}
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('/')
}
此时还有bug,当我们在登录界面输入信息后,此时切换到注册页面,此时表单中的数据还存在,需要将此时的数据清空来获得更好的体验
可以使用watch监视isRegister的变化
import {watch} from 'vue'//需要导入watch
watch(isRegister,()=>{
formModel.value={
username:'',
password:'',
repassword:''
}
})
注意:需要将注册和登录的表单数据都绑定v-model和prop
此时的LoginPage.vue代码
<script setup>
import { User, Lock } from '@element-plus/icons-vue'
import { ref, watch } from 'vue'
import { userRegisterService, userLoginService } from '@/api/user'
import { useUserStore } from '@/stores'
import { useRouter } from 'vue-router'
const isRegister = ref(false)
const form = ref()
const formModel = ref({
username: '',
password: '',
repassword: '',
})
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: (rule, value, callback) => {
//判断value 和当前form中收集的password是否一致
if (value !== formModel.value.password) {
callback(new Error('两次输入的密码不一致'))
} else {
callback() //校验成功需要正常回调,callback
}
},
trigger: 'blur',
},
],
}
//validate对整个表单的内容进行验证。 接收一个回调函数,或返回 Promise。
const register = async () => {
//注册成功之前先进行校验 校验成功->请求 校验失败->自动提示
await form.value.validate()
// console.log('开始注册请求')
await userRegisterService(formModel.value)
ElMessage.success('注册成功')
//切换到登录
isRegister.value = false
}
//导入store中的user仓库
//设置我们的token
const userStore = useUserStore()
const router = useRouter()
//登录之前进行一个预校验
const login = async () => {
await form.value.validate()
const res = await userLoginService(formModel.value)
userStore.setToken(res.data.token)
// console.log('开始登录', res)
ElMessage.success('登录成功')
//跳转到首页
router.push('/')
}
//切换的时候需要重置表单的内容
watch(isRegister, () => {
formModel.value = {
username: '',
password: '',
repassword: '',
}
})
</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
:model="formModel"
:rules="rules"
ref="form"
size="large"
autocomplete="off"
v-if="isRegister"
>
<el-form-item>
<h1>注册</h1>
</el-form-item>
<el-form-item prop="username">
<el-input
v-model="formModel.username"
:prefix-icon="User"
placeholder="请输入用户名"
></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="formModel.password"
:prefix-icon="Lock"
type="password"
placeholder="请输入密码"
>
</el-input>
</el-form-item>
<el-form-item prop="repassword">
<el-input
v-model="formModel.repassword"
:prefix-icon="Lock"
type="password"
placeholder="请输入再次密码"
></el-input>
</el-form-item>
<el-form-item>
<el-button @click="register" 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 :model="formModel" :rules="rules" ref="form" size="large" autocomplete="off" v-else>
<el-form-item>
<h1>登录</h1>
</el-form-item>
<el-form-item prop="username">
<el-input
v-model="formModel.username"
:prefix-icon="User"
placeholder="请输入用户名"
></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="formModel.password"
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 @click="login" 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>
………………样式省略
注册界面点击注册后的再次提示(登录界面页相同)

登录成功后跳转到主页

首页layout架子
首先写好架子基本结构,这里还是使用的Element-plus组件来进行搭建
官方链接(布局容器)
https://element-plus.org/zh-CN/component/container.html
LayoutContainer.vue静态结构
<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-menu 整个菜单组件
:default-active="$route.path" 配置默认高亮的菜单项
router router选项开启,el-menu-item 的index就是要跳转的路径
el-sub-menu 多级菜单
-->
<!-- 顶级容器 -->
<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>小帅鹏</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>
基本结构的解释:
-
el-container 容器组件
-
el-aside 侧边栏
-
el-menu 菜单配置
-
<el-menu> active-text-color="#ffd04b" background-color="#232323" :default-active="$route.path" text-color="fff" router </el-menu>
-
-
el-menu-item 菜单中的选项
-
el-sub-menu 多级菜单
-
el-icon 图标
:default-active="$route.path" // 配置默认的高亮菜单项
router //router选项开启el-menu-item的index就是要跳转的路径
active-text-color="#ffd04b"//选中的选项颜色
登录拦截请求
在非登录界面中,我们没有token不能直接跳转
需要判断token是否存在,此时需要用到路由守卫,vue-3的路由守卫和vue-2有所不同
官方链接https://router.vuejs.org/zh/guide/advanced/navigation-guards.html
官方文档示例
router.beforeEach(async (to, from) => {
if (
// 检查用户是否已登录
!isAuthenticated &&
// ❗️ 避免无限重定向
to.name !== 'Login'
) {
// 将用户重定向到登录页面
return { name: 'Login' }
}
})
登录拦截请求 =>vue-3是直接放行的,根据返回值决定是放行还是拦截
- 返回值
- undefined/true 直接放行
- false 拦截from的地址页面
- 具体路径或者路径对象,拦截到对应的地址 ‘/login’ ‘{name:‘login’}’
router/index.js拦截配置
//导入pinia 的user仓库判断token
import {useUserStore} from '@/stores/index'
router.beforeEach((to)=>{
const userStore=useUserStore()
//如果没有token且访问的是非登录页,拦截到登录
//其他情况正常放行
if(!userStore.token&&to.path!=='/login')return '/login'
return true;//不返回默认返回值为undefined也可以
})
页面展示

首页用户基本信息获取和退出登录功能
接口文档用户信息相关

token已经在请求拦截器中进行配置了,在配置用户信息接口的时候就不用传递了
api/user.js
//获取用户信息接口
export const UserGetInfoService=()=>{
return request.get('/my/userinfo')
}
需要将用户信息进行 持久化存储
store/modules/user.js
(此处只是展示这一步的主要代码)
//头部导入此api
import { UserGetInfoService } from '@/api/user'
export const useUserStore = defineStore(()=>{
……………………其他存储的信息
//提供用户对象和getUser方法
const user =ref({})//设置为一空对象
const getUser =async()=>{
const res = await UserGetInfoService()
user.value=res.data.data
}
//设置用户信息,后续退出即可传入一个空对象清空
const setUser=(obj)=>{
user.value=obj//设置为此对象
}
return {
…………其他信息
//导出
user,getUser,setUser
},
{persist:true//持久化}
})
layoutContainer.vue的头像和切换结构分析
element-plus官方文档
https://element-plus.org/zh-CN/component/dropdown.html
<!-- 添加command事件 -->
<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>
command属性中的参数是我们指定的跳转的路由路径,通过点击下拉菜单来跳转到指定页面进行相关操作

前面为整个下拉菜单添加了@command="handleCommand"事件
LayoutContainer.vue
(此处只展示这一部分的主要代码)
//引入生命周期钩子
import {onMounted} from 'vue'
//引入用户信息仓库
import {useUserStore} from '@/stores'
//引入路由
import {useRouter} from 'vue-router'
const router = useRouter()
const userStore = useUserStore()
//挂载完毕导入用户信息
onMounted(()=>{
userStore.getUser()
})
//触发事件
const handleCommand =async(key)=>{
if(key === 'logout'){
//退出操作
//退出前的询问
await ElMessageBox.confirm(
'确认要退出吗?',
'温馨提示',
{
confirmButtonText: 'OK',
cancelButtonText: 'Cancel',
type: 'warning',
})
//清空本地的数据
userStore.removeToken()
userStore.setUser({})//设置为一个空对象
//跳转到登录页面
router.push('/login')
}else{
//跳转操作
router.push(`/user/${key}`)//模板字符串
}
}
此处的key指的是下拉菜单中绑定的command
<el-dropdown-item
command="profile"
:icon="User">基本资料
</el-dropdown-item>
即我们路由中配置的路径名,不是随便乱写的
首页用户昵称,有昵称名则传入nickname,没有则展示用户名
<strong>
{{ userStore.user.nickname || userStore.user.username }}
</strong>
首页用户头像,有头像则传入用户头像,没有头像则传入默认头像
<el-avatar
:src="userStore.user.user_pic || avatar" />
提示展示

首页 Pinia 用户和token信息

退出后则token和user信息为空

文章分类模块
基本架子
views/aticle/ArticleChannel.vue
<script setup></script>
<template>
<page-container title="文章分类">
<template #extra>
<el-button type="primary">添加分类</el-button>
</template>
</page-container>
</template>
<style lang="scss" scoped></style>
views/aticle/ArticleManage.vue
<script setup></script>
<template>
<page-container title="文章管理">
<template #extra>
<el-button type="primary">添加文章</el-button>
</template>
</page-container>
</template>
<style lang="scss" scoped></style>
新建components/PageContainer.vue,因为文章管理和文章分类管理的目录的结构类似,可以进行复用
<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;
justify-content: space-between;
align-items: center;
}
}
</style>
通过父传子,使用prop将title传过去
Element-plus的Card介绍
https://element-plus.org/zh-CN/component/card.html
这里使用了具名插槽和默认插槽
笔者学到这里有点忘记插槽怎么使用了,准备回去再复习一下插槽的用法,然后补上笔记
页面展示

文章分类的渲染
需要获取文章分类的数据,我们需要观看接口,然后进行文章相关数据的api的创建
接口文档链接
https://apifox.com/apidoc/shared-26c67aee-0233-4d23-aab7-08448fdf95ff/api-93850053
api/article.js
import request from '@utils/request'
//获取文章分类
export const artGetChannelService=()=>{
return request.get('/mycate/list')
}
在ArticleChannel.vue中获取分类列表
import {artGetChannelService} from '@/api/article'
//初始化文章列表为一个空数组
const channelList =ref([])
//获取分类列表
const getChannelList = async ()=>{
const res = await artGetChannelService()
channelList.value=res.data.data
}
//调用
getChannelList()
我们需要在文章分类页生成一个表格,此时用到了Element-plus的表格table相关的组件
官方文档https://element-plus.org/zh-CN/component/table.html
此处列举重要的操作解释
<!-- type属性是用来显示序号 -->
<el-table-column type="index" width="50" />
<!-- prop属性是用来绑定数据,label属性是用来显示表头 -->
<el-table-column
prop="cate_name"
label="分类名称"></el-table-column>
<el-table-column
prop="cate_alias"
label="分类别名"></el-table-column>
注意此处的prop接受的参数是上面getChannelList请求回来的数据信息参数名
lable则是这一列的标题
删除编辑操作
<el-table-column label="操作" width="150">
<!-- 作用域插槽 row就是channel的每一项即item,$index是下标-->
<template #default="{ row, $index }">
<el-button
:icon="Edit"
circle
type="primary"
plain
@click="onEditChannel(row, $index)"
></el-button>
<el-button
:icon="Delete"
circle
type="danger"
plain
@click="onDelChannel(row, $index)"
></el-button>
</template>
</el-table-column>
其中row和$index表示的每一行数据的信息参数和其序号
为了优化用户体验我们还需要添加上一个加载效果,
在请求到数据之前我们需要进行一个加载效果,在请求到数据后加载效果消失,这个操作也需要用到ELement-plus提供的指令v-lodading
//获取分类列表
const getChannelList = async () => {
//发请求之前loading为true
loading.value = true
const res = await artGetChannelService()
channelList.value = res.data.data
console.log(channelList.value)
// channelList.value = [] //空状态显示
//请求结束loading为false
loading.value = false
}
同时我们希望在没有任何文章信息的时候页面显示空的提示,也需要用到Element-plus组件empty
官方文档https://element-plus.org/zh-CN/component/empty.html
这一步ArrticleChannel.vue最终的代码
<script setup>
import { ref } from 'vue'
import { artGetChannelService } from '@/api/article'
//导入图标
import { Edit, Delete } from '@element-plus/icons-vue'
//发送请求
const channelList = ref([])
//加载
const loading = ref(false)
//获取分类列表
const getChannelList = async () => {
//发请求之前loading为true
loading.value = true
const res = await artGetChannelService()
channelList.value = res.data.data
console.log(channelList.value)
// channelList.value = [] //空状态显示
//请求结束loading为false
loading.value = false
}
//调用
getChannelList()
/**
*
* @param row 每一行的数据
* @param $index 每一行的下标
*/
const onEditChannel = (row, $index) => {
console.log(row, $index)
}
const onDelChannel = (row, $index) => {
console.log(row, $index)
}
</script>
<template>
<page-container title="文章分类">
<template #extra>
<el-button type="primary">添加分类</el-button>
</template>
<el-table v-loading="loading" :data="channelList" style="width: 100%">
<!-- type属性是用来显示序号 -->
<el-table-column type="index" width="50" />
<!-- prop属性是用来绑定数据,label属性是用来显示表头 -->
<el-table-column label="序号" width="100"></el-table-column>
<el-table-column prop="cate_name" label="分类名称"></el-table-column>
<el-table-column prop="cate_alias" label="分类别名"></el-table-column>
<el-table-column label="操作" width="150">
<!-- 作用域插槽 row就是channel的每一项即item,$index是下标-->
<template #default="{ row, $index }">
<el-button
:icon="Edit"
circle
type="primary"
plain
@click="onEditChannel(row, $index)"
></el-button>
<el-button
:icon="Delete"
circle
type="danger"
plain
@click="onDelChannel(row, $index)"
></el-button>
</template>
</el-table-column>
<!-- 作用域插槽 表格没有数据的时候显示该信息 -->
<template #empty>
<el-empty description="没有数据" />
</template>
</el-table>
</page-container>
</template>
<style lang="scss" scoped></style>
页面显示效果

为空的数据显示

加载动画

编辑和添加弹层显示搭建
Element-plus对话框介绍
https://element-plus.org/zh-CN/component/dialog.html#dialog-%E5%AF%B9%E8%AF%9D%E6%A1%86
介于添加文章和编辑都需要弹出对话框,所以我们可以在gai目录下建立一个componets文件夹,在该文件中新建立文件ChannelEdit.vue,
这样也便于管理和页面层次,将对话框组件写到这里
views/article/components/ChannelEdit.vue
<template>
<!-- 弹层部分 -->
<el-dialog v-model="dialogVisible" title="添加弹层" width="500">
<span>我是内容部分(表单渲染)</span>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="dialogVisible = false"> 确认 </el-button>
</div>
</template>
</el-dialog>
</template>
span便签内是我们的内容区域,后续需要将表单元素放在里面,以进行一个编辑和添加文章分类
在这个组件内,我们可以向外暴露一个方法open,基于open传来的参数,区分是进行编辑还是添加,
- open({}) 添加功能
- open({id:xxx,name:xxxx}) 编辑功能(表单渲染,)
- open调用后会打开弹层
views/article/components/ChannelEdit.vue
<script setup>
import {ref} from 'vue'
//弹窗
const dialogVisible = ref(false)
//方法
const open=(row)=>{
//打开弹层
dialogVisible.value=true
}
//向外暴露这个方法
defineExpose({
open,
})
</script>
open函数中传入的row指的是文章分类的对象
- cate_name 名字
- cate_alias 类别
在ArticleChannel.vue中导入该组件
<script setup>
//引入组件ChannelEdit
import ChannelEdit from '@/views/article/components/ChannelEdit.vue'
/*其他代码*/
//编辑显示弹层
const onEditChannel =(row)>{
dialog.value.open(row)
}
//添加显示弹层
const onAddChannel =()=>{
dialog.value.open({})
}
</script>
<template>
<page-container title="文章分类">
<!--编辑按钮-->
<template #extra>
………………
</template>
<!--表格部分-->
<el-table>
………………
</el-table>
<!--弹层组件部分-->
<ChannelEdit ref="dialog"></ChannelEdit>
</page-container>
</template>
对组件ChannelEdit添加了一个ref,通过ref找到该组件,能够方便的调用暴露出来的open函数
(写到这里又开始懵了,前面学习的ref和defineExpose都已经忘记了,还需要继续复习)
关于上一部分的插槽问题,这里画图解释一下

弹层数据的添加和编辑api

在article.js中添加这两个接口
//添加文章分类
export const artAddChannelService = (data) => {
return request.post('/my/cate/add', data)
}
//更新文章分类
export const artEditChannelService = (data) => {
return request.put('/my/cate/info', data)
}
在弹层中添加内容部分添加一个表单,我们需要对这个表单填写进行验证、更新列表内容和数据提交
操作方式和8.2节一样
ChannelEdit.vue表单
<!-- 表单主要内容部分 -->
<el-form
ref="fromRef"
: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" placeholder="请输入分类名称"></el-input>
</el-form-item>
<el-form-item label="分类别名" prop="cate_alias">
<el-input v-model="formModel.cate_alias" placeholder="请输入分类别名"></el-input>
</el-form-item>
</el-form>
open方法的更新
const open =(row)=>{
dialogVisible.value=true
formModel.value={...row}
//将传过来的row数据绑定到表单上,这里使用了展开运算符,将row的所有属性都赋值给formModel
}
表单数据的声明
//导入编辑和添加的服务
import { artEditChannelService, artAddChannelService } from '@/api/article'
//绑定到表单上
const formRef = ref()
//这里声明form中的数据,是从接口文档中看到的命名
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' },
],
}
//设置一个触发事件
const emit = defineEmits(['success'])
//为确认按钮添加提交事件
const onSubmit= async()=>{
//表单提交预校验
await formRef.value.validate()
//确认是添加还是编辑事件
//确认是否存在id
const idEdit=formModel.value.id?true:false;
if(isEdit){
await artEditChannelService(formModel.value)
ElMessage.success('编辑成功')
}
else{
await artAddChannelService(formModel.value)
ElMessage.success('编辑成功')
}
//关闭弹层
dialogVisible.value.false
emit('success')//触发父亲组件的emit事件
}
ArticleChannel.vue中为弹层注册success事件
<!-- 弹层部分 -->
<ChannelEdit ref="dialog" @success="onSuccess"></ChannelEdit>
触发事件
//通知刷新
const onSuccess = () => {
getChannelList()
}
做这一步是为了让添加或者更新数据后,页面进行一个从新的渲染,不然数据改变了,但是页面中仍然显示的是之前的数据。
预校验展示

新增分类

此时加载的数据

编辑操作数学

由于之前是通过v-model双向绑定的,此时打开后表单元素中即使此元素的信息
删除文章分类
查看接口文档

这里是查询参数
api/article.js配置
//删除文章分类
export const artDeleteChannelService = (id) => {
return request.delete('/my/cate/del', {
params: {
id,
},
})
}
ArticleChannel.vue里面删除功能添加
const onDelChannel = async(row)=>{
//删除之前的确认框
await ElMessageBox.confirm(
'确认删除该分类吗?',
'温馨提示',
type:'warning',
confirmButtonText:'确定',
cancelButtonText:'取消'
)
//发送删除请求
await artDeleteChannelService(row.id)
//成功提示
ElMessage.success('删除成功')
//删除成功从新渲染
getChannelList()
}
删除提示

文章管理模块
静态页面搭建
ArticleManage.vue静态页面展示
<script setup>
import { ref } from 'vue'
import { Delete, Edit } from '@element-plus/icons-vue'
// 假数据
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: '草稿',
cate_name: '体育',
},
])
//编辑
const onEditArticle = (row) => {
console.log(row)
}
//删除
const onDeleteArticle = (row) => {
console.log(row)
}
</script>
<template>
<page-container title="文章管理">
<template #extra>
<el-button type="primary">添加文章</el-button>
</template>
<!-- 表单区域 -->
<el-form inline>
<el-form-item label="文章分类:">
<el-select>
<el-option label="新闻" value="111"></el-option>
<el-option label="体育" value="222"></el-option>
</el-select>
</el-form-item>
<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>
<!-- 表格区域 -->
<el-table :data="articleList">
<el-table-column label="文章标题" prop="title">
<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>
<!-- 利用作用域插槽row可以获取当前行的数据 可以理解为v-for中item -->
<el-table-column label="操作">
<template #default="{ row }">
<el-button
circle
plan
:icon="Edit"
type="primary"
@click="onEditArticle(row)"
></el-button>
<el-button
circle
plan
:icon="Delete"
type="danger"
@click="onDeleteArticle(row)"
></el-button>
</template>
</el-table-column>
</el-table>
</page-container>
</template>
<style lang="scss" scoped></style>

关键解释
inline能够让以下表单元素一排排列
<el-form inline></el-form>
label是展示出来给用户看的
<el-form-item label="文章分类:"></el-form-item>
value是返回给后台的数据
<el-option value='111'></el-option>
el-link是让文章标题能够链接显示,通过作用域插槽传入数据
<template #default="{ row }">
<el-link
type="primary"
:underline="false">
{{ row.title }}
</el-link>
</template>
中英文切换
Element-plus默认是使用的英文版本,我们可以导入中文包,让文字变为中文显示
官方文档
https://element-plus.org/zh-CN/component/config-provider.html
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>
分类管理下拉菜单数据绑定
在文章管理、和添加文章都使用到了这个下拉菜单,所以可以封装成一个单独的组件来完成
article/components/ChannelSelect.vue
<script setup>
//导入获取文章列表api
import { artGetChannelService } from '@/api/article'
import { ref } from 'vue'
defineProps({
modeValue: {
type: [String, Number],
},
})
//定义触发事件
const emit = defineEmits(['update:modeValue'])
const channelList = ref([])
const getChannelList = async () => {
const res = await artGetChannelService()
channelList.value = res.data.data
// console.log(channelList.value)
}
//进入页面就触发
getChannelList()
</script>
<template>
<!-- 将下拉菜单封装成组件 -->
<el-select
:modeValu="modeValue" @updata:modelValue="emit('update:modeValue', $event)">
<el-option
v-for="channel in channelList"
:label="channel.cate_name"
:value="channel.id"
:key="channel.id"
></el-option>
</el-select>
</template>
<style></style>
接口文档

在ArticleManage.vue中定义一个params对象封装
const params =ref({
pagenum:1,
pagesize:5,
cate_id:'',
state:''
})
//导入下拉组件
import ChannelSelect from './components/ChannelSelect.vue'
…………
<ChannelSelect v-model="params.cate_id"></ChannelSelect>
vue3的v-model和vue2的有所区别
- v-model 是:value 和@input 的简写vue2
- v-model 是:modeValue和@update:modeValue的简写 vue3
为了实现这里下来框中的数据是我们在文章分类中添加的分类数据,
在ChannelSelect.vue中
//接受父组件传过来的数据
defineProp({
modeValue:{
type:[String,Number],
}
})
//定义触发事件
const emit = defineEmits(['update:modeValue'])
//定义一个空数组,用来渲染下拉列表中的数据
const channelList = ref([])
const getChannelList =async()=>{
const res = await artGetChannelService()
channelList.value=res.data.data
}
//进入页面则触发渲染列表
getChannelList()
利用v-for渲染下拉列表
<template>
<el-select :modeValue='modeValue'
@updata:modeValue=
"emit('update:modeValue',$event)">
<!--选项渲染-->
<el-option
v-for='channel in channelList'
:label='channel.cate_name'
:value='channel.id'
:key='channel.id'
>
</el-option>
</el-select>
</template>

tips:学到这里又懵逼了,好像Vue-3的数据传递我又忘记了,这里需要回去再复习一下了。
文章管理数据动态渲染
查看接口文档https://apifox.com/apidoc/shared-26c67aee-0233-4d23-aab7-08448fdf95ff/api-93850788

前面两个参数是用来分页操作的,后两个参数用来渲染文章数据

我们需要使用到的数据有data和total
api/article.js
//文章:获取文章列表
export const artGetArticleService=(params)=>{
return request.get('/my/article/list',{
params
})
}
ArticleManage.vue
<script setup>
//导入文章列表api
import {artGetArticleService} from "@/api/article"
const articleList =ref([])//文章列表
const total=ref(0)//总条数
//获取文章列表
const getArticleList-async()=>{
const res =await artGetArticleService(params,value)
articleList.value =res.data.data
total.value=res.data.total
}
//进入页面立即调用
getArticleList()
</script>

我们发现此时时间需要进行一个格式化,Element-plus内置类dayjs,
通过dayjs来格式化时间
utils/format.js
import { dayjs } from 'element-plus'
export const formatTime = (time) => dayjs(time).format('YYYY年MM月DD日')
ArticleManage.vue
<script setup>
//导入格式化方法
import { formatTime } from '@/utils/format'
</script>
<template>
<!--在发布时间这一行-->
<el-table-column label="发表时间" prop="pub_date">
<template #default="{ row }">
{{ formatTime(row.pub_date) }}
</template>
</el-table-column>
</template>
通过默认插槽格式化文章发布时间,这里格式化后的数据会渲染到界面上

日期变成我们想要的样子了
添加页码切换
Element-plus官方文档
https://element-plus.org/zh-CN/component/pagination.html
ArticleManage.vue分页区域
<template>
<el-pagination
v-model:current-page="params.pagenum"
v-model:page-size="params.pagesize"
:page-sizes="[2, 3, 5, 10]"
:background="true"
layout="jumper,total, sizes, prev, pager, next "
:total="total"
@size-change="onSizeChange"
@current-change="onCurrentChange"
style="margin-top: 20px; display: flex; justify-content: flex-end"
/>
</template>
- current-page 绑定的当前页
- page-size 表示的当前页的容量
- page-sizes 表示用户能够选择的每页存放的数据数量
- layout 表示用户能够进行的操作
- total 表示数据总数
- @size-change和current-change,能够检测到当前页码和当前每一页的条数
ArticleManage.vue
<script setup>
//处理分页逻辑
const onSizeChange = size=>{
//console.log('当前每一页的条数',size)
//只要是每一页条数变化了,原本正在访问的当前页的意义就不大了,数据已经不再原来那一页了
//从新的第一页渲染即可
params.value.pagenum=1
params.value.pagesize=size
//从新渲染
getArticleList()
}
const onCurrentChange=page=>{
//console.log('页码变化了',page)
//更新当前页,基于最新的当前页渲染
params.value.pagenum=page
//从新渲染
getArticleList()
}
</script>
添加分页切换的loading效果
<script setup>
//loading状态
const loading ref(false)//默认为false
const getArticleList = async () => {
//loading状态
loading.value = true //开始loading
const res = await artGetArticleService(params.value)
articleList.value = res.data.data
total.value = res.data.total
//请求介乎
loading.value = false //结束loading
}
</script>
<template>
<!--为表格添加loading-->
<el-table :data="articleList" v-loading="loading">
</el-table>
</template>
搜索和重置添加事件
<template>
<!--首先为表单搜索和重置按钮添加事件-->
<el-form-item>
<el-button type="primary" @click="onSearch">搜索</el-button>
<el-button @click="onReset">重置</el-button>
</el-form-item>
</template>
<script setup>
//搜索
const onSearch =()=>{
//搜索就是按照最新的条件进行检索,从第一页开始展示
//重置页码
params.value.pagenum=1
getArticleList()
}
const onReset = () => {
//重置
//就是将筛选条件清空从新检索,从第一页开始展示
params.value.pagenum = 1
params.value.cate_id = ''
params.value.state = ''
getArticleList()
}
</script>
抽屉盒子
将抽屉封装成一个组件,设置一个变量来让抽屉显示和消失
views/article/components/ArticleEdit.vue
<script setup>
import { ref } from 'vue'
//控制抽题显示隐藏
const visibleDrawer = ref(false)
//组件对外暴露一个方法 open 基于open传来的参数,区分添加还是编辑
//open({})=>表单无需渲染,说明添加
//open({id:1,name:'xxx',…………})=>表单渲染,说明编辑
//open调用后会打开抽屉
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</span>
</el-drawer>
</template>
解释
- v-model=“visibleDrawer” 控制抽屉的显示隐藏
- title 抽屉的标题
- direction 控制抽屉的出现方向
- size 抽屉显示的范围
显示效果

ArticleManage.vue
<script setup>
//导入抽屉组件
import ArticleEdit from './components/ArticleEdit.vue'
const articleEditRef =ref()
//添加文章
const onAddArticle =()=>{
articleEditRef.value.open({})
}
//编辑文章
const onEditArticle =row=>{
articleEditRef.valu.open(row)
}
</script>
<template>
<!--抽屉区域-->
<ArticleEdit ref="articleEditRef"> </ArticleEdit>
</template>

点击编辑和添加分别能获得一个对象,通过判断传入的是否是空对象,来判断是添加还是编辑操作
抽屉组件封装内容
查看接口文档中发布文章
https://apifox.com/apidoc/shared-26c67aee-0233-4d23-aab7-08448fdf95ff/api-93850059

ArticleEdit.vue
<script setup>
//导入下俩列表组件
import ChannelSelect from '@/views/article/components/ChannelSelect.vue'
//提供默认数据
//默认数据
const defaultForm = {
title:'',//标题
cate_id:'',//分类id
cover_img:'',//封面图片 file对象
content:'',//string内容
state:''//状态
}
//提供数据
const formModel =ref({
...defaultForm
})
const open = (row) => {
//显示抽屉
visibleDrawer.value = true
//
if(row.id){
//需要基于row.id发送请求,获取编辑对应的详情数据,进行回显
console.log('编辑回显')
}else{
//添加之前重置数据
formModel.value= {
...defaultForm
}//基于默默人数据重置form数据
console.log('添加')
}
// console.log(row)
}
//暴露方法
defineExpose({
open,
})
</script>
抽屉结构
<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"
></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>
下拉菜单组件中设置下拉菜单宽度占满
ChannelSelect.vue
<script setup>
defineProps({
width:{
type:String
}
})
</script>
<template>
<el-select
:modeValue="modeValue"
@updata:modelValue="emit('update:modeValue', $event)"
:style="{width}">
<el-option
v-for="channel in channelList"
:label="channel.cate_name"
:value="channel.id"
:key="channel.id"
></el-option>
</el-select>
</template>
文件新增、上传文件
官方文档
https://element-plus.org/zh-CN/component/upload.html#upload-%E4%B8%8A%E4%BC%A0

ArticleEdit.vue
- 关闭自动上传,准备结构
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 将来用于提交
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;
}
}
}

富文本编辑器的导入
使用的是这个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'
<QuillEditor
theme="snow"
v-model:content="formModel.content"
contentType="html"
></QuillEditor>
v-model:content 绑定内容,
contentType=“html” 绑定内容格式为html
展示结果

添加文章
- 封装文章接口
ArticleEdit.vue
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>
<script>
// 发布文章
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')
}
}
</script>
- 父组件监听事件,重新渲染
<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()
}
- 添加完成后的内容重置
const formRef = ref()
const editorRef = ref()
const open = async (row) => {
visibleDrawer.value = true
if (row.id) {
console.log('编辑回显')
} else {
formModel.value = { ...defaultForm }
//这里重置我们还需要重置图片上传img地址
//富文本编辑器内容需要手动重置
imgUrl.value = ''
editorRef.value.setHTML('')
}
}
文章编辑
- 封装文章接口
article.js
//文章:获取文章详情
export const artGetDetailService = (id) => {
return request.get("/my/article/info", {
params: { id },
});
};
- 编辑按钮点击回显
//导入及地址
import { baseURL } from "@/utils/request";
const open = async (row) => {
//显示抽屉
visibleDrawer.value = true;
//
if (row.id) {
//需要基于row.id发送请求,获取编辑对应的详情数据,进行回显
// console.log('编辑回显')
const res = await artGetDetailService(row.id);
// console.log(res)
formModel.value = res.data.data;
//图片需要单独处理回显
imgUrl.value = baseURL + formModel.value.cover_img;
//注意:提交给后台,需要的格式是file对象格式
//需要将我们的网络图片地址=》转换成file对象,存储起来,将来便于提交
const file = await imageUrlToFile(imgUrl.value, formModel.value.cover_img);
formModel.value.cover_img = file;
} else {
//添加之前重置数据
formModel.value = {
...defaultForm,
}; //基于默默人数据重置form数据
//这里重置我们还需要重置图片上传img地址
//富文本编辑器内容需要手动重置
imgUrl.value = "";
editRef.value.setHTML("");
// console.log('添加')
}
// console.log(row)
}; // 将网络图片地址转换为File对象
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;
}
}
编辑提交
- 封装文章接口
article.js
//文章:编辑文章
export const artEditArticleService = (data) => {
return request.put("/my/article/info", data);
};
ArticleEdit.vue
const onPublish = async (state) => {
//将已发布还是草稿状态都存入formModel
formModel.value.state = state;
//注意当前接口需要的是formData对象
//将普通对象转化为formData对象
const formData = new FormData();
for (let key in formModel.value) {
//将formModel的key-value对添加到formData中
formData.append(key, formModel.value[key]);
}
//发送请求
if (formModel.value.id) {
//编辑
// console.log('编辑')
await artEditArticleService(formData);
ElMessage.success("修改成功");
visibleDrawer.value = false;
//通知父组件渲染当前页
emit("success", "edit");
} else {
//添加
await artPublishArticleService(formData);
ElMessage.success("添加成功");
//关闭抽屉
visibleDrawer.value = false;
//添加成功通知父组件
emit("success", "add");
}
};
删除文章
- 封装文章接口
article.js
//文章:删除文章
export const artDeleteArticleService = (id) => {
return request.delete("/my/article/info", {
params: {
id,
},
});
};
ArticleManage.vue
//删除
const onDeleteArticle = async (row) => {
//如何删除
// articleEditRef.value.open(row)
await ElMessageBox.confirm("确认删除该文章吗?", "温馨提示", {
type: "warning",
confirmButtonText: "确定",
cancelButtonText: "取消",
});
await artDeleteArticleService(row.id);
ElMessage.success("删除成功");
getArticleList();
};
个人中心
基本资料
UserProfile.vue
基于 ai 提问生成
请基于 elementPlus 和 Vue3 的语法,生成组件代码
要求:
一、表单结构要求
1. 组件中包含一个el-form表单,有四行内容,前三行是输入框,第四行是按钮
2. 第一行 label 登录名称,输入框禁用不可输入状态
3. 第二行 label 用户昵称,输入框可输入
4. 第三行 label 用户邮箱,输入框可输入
5. 第四行按钮,提交修改
二、校验需求
给昵称 和 邮箱添加校验
1. 昵称 nickname 必须是2-10位的非空字符串
2. 邮箱 email 符合邮箱格式即可,且不能为空
<script setup>
import PageContainer from "@/components/PageContainer.vue";
import { ref } from "vue";
import { useUserStore } from "@/stores";
import { userUpdateInfoService } from "@/api/user";
const formRef = ref();
// 是在使用仓库中数据的初始值 (无需响应式) 解构无问题
const {
user: { email, id, nickname, username },
getUser,
} = useUserStore();
const form = ref({
id,
username,
nickname,
email,
});
const rules = ref({
nickname: [
{ required: true, message: "请输入用户昵称", trigger: "blur" },
{
pattern: /^\S{2,10}/,
message: "昵称长度在2-10个非空字符",
trigger: "blur",
},
],
email: [
{ required: true, message: "请输入用户邮箱", trigger: "blur" },
{
type: "email",
message: "请输入正确的邮箱格式",
trigger: ["blur", "change"],
},
],
});
const submitForm = async () => {
// 等待校验结果
await formRef.value.validate();
// 提交修改
await userUpdateInfoService(form.value);
// 通知 user 模块,进行数据的更新
getUser();
// 提示用户
ElMessage.success("修改成功");
};
</script>
<template>
<page-container title="基本资料">
<!-- 表单部分 -->
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="100px"
>
<el-form-item label="登录名称">
<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"
@click="submitForm"
>提交修改</el-button
>
</el-form-item>
</el-form>
</page-container>
</template>
头像
头像静态结构
<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({ type: "success", message: "更换头像成功" });
};
重置密码
请基于 elementPlus 和 Vue3 的语法,生成组件代码
要求:
一、表单结构要求
1. 组件中包含一个el-form表单,有四行内容,前三行是表单输入框,第四行是两个按钮
2. 第一行 label 原密码
3. 第二行 label 新密码
4. 第三行 label 确认密码
5. 第四行两个按钮,修改密码 和 重置
二、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 });
- 页面中调用
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();
};



2393

被折叠的 条评论
为什么被折叠?



