目录:希望对大家有帮助
①项目压缩包:
1.要启动的指令:可以参考下面的文件
①登录、注册页面
src\views\login\LoginPage.vue
src\api\user.js
src\stores\modules\user.js
src\stores\index.js
②首页实现的页面
src\views\layout\LayoutContainer.vue
src\api\user.js
user下面的小模块:实现改用户信息、图像、密码
src\***ponents\PageContainer.vue
src\views\user\UserProfile.vue
src\views\user\UserAvatar.vue
src\views\user\UserPassword.vue
③文章的目录编辑:
src\***ponents\PageContainer.vue
src\views\article\ArticleChannel.vue
src\views\article\***ponents\ChannelEdit.vue
④文章的详情管理:
src\views\article\ArticleManage.vue
src\views\article\***ponents\ChannelSelect.vue
src\views\article\***ponents\ArticleEdit.vue
src\utils\format.js
⑤全局的路由:
①项目压缩包:
📎day13-大事件管理系统-Vue3-big-event-admin.zip
1.要启动的指令:可以参考下面的文件
📎day12~day13-大事件管理系统-new.pdf
📎11-day12-day14-大事件管理系统.zip
npm install -g pnpm
pnpm create vue
pnpm dlx husky-init && pnpm install
pnpm i lint-staged -D
2.自己原版的笔记:在语雀
https://www.yuque.***/wangruiquandayi3banrengongzhineng/bmgrcg/fgp0su8yx0bb5039?singleDoc# 《②big-event前端代码》# 《②big-event前端代码》
①登录、注册页面
src\views\login\LoginPage.vue
<script setup>
import { userRegisterService, userLoginService } from '@/api/user.js'
import { User, Lock } from '@element-plus/icons-vue'
import { ref, watch } from 'vue'
import { useUserStore } from '@/stores'
import { useRouter } from 'vue-router'
const isRegister = ref(false)
//获取整个表单的信息,进行统一的校验
//只有信息都输入正确后,才能进行注册的
const form = ref()
// 整个的用于提交的form数据对象
const formModel = ref({
username: '',
password: '',
repassword: ''
})
// 整个表单的校验规则
// 1. 非空校验 required: true message消息提示, trigger 触发校验的时机 blur change
// 2. 长度校验 min:xx, max: xx
// 3. 正则校验 pattern: 正则规则 \S 非空字符
// 4. 自定义校验 => 自己写逻辑校验 (校验函数)
// validator: (rule, value, callback)
// (1) rule 当前校验规则相关的信息
// (2) value 所校验的表单元素目前的表单值
// (3) callback 无论成功还是失败,都需要 callback 回调
// - callback() 校验成功
// - callback(new Error(错误信息)) 校验失败
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'
}
]
}
const register = async () => {
// 注册成功之前,先进行校验,校验成功 → 请求,校验失败 → 自动提示
//validate()是官网提供的方法,已经直接暴露,可以直接使用
await form.value.validate()
await userRegisterService(formModel.value)
ElMessage.su***ess('注册成功')
isRegister.value = false
}
const userStore = useUserStore()
const router = useRouter()
const login = async () => {
await form.value.validate()
const res = await userLoginService(formModel.value)
userStore.setToken(res.data.token)
ElMessage.su***ess('登录成功')
router.push('/')
}
// 切换的时候,重置表单内容
watch(isRegister, () => {
formModel.value = {
username: '',
password: '',
repassword: ''
}
})
</script>
<template>
<!--
1. 结构相关
el-row表示一行,一行分成24份
el-col表示列
(1) :span="12" 代表在一行中,占12份 (50%)
(2) :span="6" 表示在一行中,占6份 (25%)
(3) :offset="3" 代表在一行中,左侧margin份数
el-form 整个表单组件
el-form-item 表单的一行 (一个表单域)
el-input 表单元素(输入框)
2. 校验相关
(1) el-form => :model="ruleForm" 绑定的整个form的数据对象 { xxx, xxx, xxx }
(2) el-form => :rules="rules" 绑定的整个rules规则对象 { xxx, xxx, xxx }
(3) 表单元素 => v-model="ruleForm.xxx" 给表单元素,绑定form的子属性
(4) el-form-item => prop配置生效的是哪个校验规则 (和rules中的字段要对应)
-->
<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"
auto***plete="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"
auto***plete="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>
<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>
src\api\user.js
import request from '@/utils/request'
// 注册接口
export const userRegisterService = ({ username, password, repassword }) =>
request.post('/api/reg', { username, password, repassword })
// 登录接口
export const userLoginService = ({ username, password }) =>
request.post('/api/login', { username, password })
// 获取用户基本信息
export const userGetInfoService = () => request.get('/my/userinfo')
export const userUpdateInfoService = ({ id, nickname, email }) =>
request.put('/my/userinfo', { id, nickname, email })
export const userUploadAvatarService = (avatar) =>
request.patch('/my/update/avatar', { avatar })
export const userUpdatePassService = ({ old_pwd, new_pwd, re_pwd }) =>
request.patch('/my/updatepwd', { old_pwd, new_pwd, re_pwd })
src\stores\modules\user.js
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { userGetInfoService } from '../../api/user'
// 用户模块 token setToken removeToken
export const useUserStore = defineStore(
'big-user',
() => {
const token = ref('')
const setToken = (newToken) => {
token.value = newToken
}
const removeToken = () => {
token.value = ''
}
//当退出登录时,要进行置空
const user = ref({})
const getUser = async () => {
const res = await userGetInfoService() // 请求获取数据
user.value = res.data.data
}
const setUser = (obj) => {
user.value = obj
}
return {
token,
setToken,
removeToken,
user,
getUser,
setUser
}
},
{
persist: true
}
)
src\stores\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'
export * from './modules/counter'
// import { useUserStore } from './modules/user'
// export { useUserStore }
// import { useCountStore } from './modules/counter'
// export { useCountStore }
②首页实现的页面
src\views\layout\LayoutContainer.vue
<script setup>
import {
Management,
Promotion,
UserFilled,
User,
Crop,
EditPen,
SwitchButton,
CaretBottom
} from '@element-plus/icons-vue'
import avatar from '@/assets/default.png'
import { useUserStore } from '@/stores'
import { onMounted } from 'vue'
import { useRouter } from 'vue-router'
const userStore = useUserStore()
const router = useRouter()
// 开始就取得User的属性
onMounted(() => {
userStore.getUser()
})
const handle***mand = async (key) => {
if (key === 'logout') {
// 退出操作
await ElMessageBox.confirm('你确认要进行退出么', '温馨提示', {
type: 'warning',
confirmButtonText: '确认',
cancelButtonText: '取消'
})
// 清除本地的数据 (token + user信息)
userStore.removeToken() //移除key
userStore.setUser({})
router.push('/login')
} else {
// 跳转操作
router.push(`/user/${key}`)
}
}
</script>
<template>
<!--
el-menu 整个菜单组件
:default-active="$route.path" 配置默认高亮的菜单项
router router选项开启,el-menu-item 的 index 就是点击跳转的路径
el-menu-item 菜单项
index="/article/channel" 配置的是访问的跳转路径,配合default-active的值,实现高亮
如果路径一致,则要实现高亮
-->
<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">
<!-- 多级菜单的标题 - 具名插槽 title -->
<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>{{
userStore.user.nickname || userStore.user.username
}}</strong>
</div>
<el-dropdown placement="bottom-end" @***mand="handle***mand">
<!-- 展示给用户,默认看到的 -->
<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 ***mand="profile" :icon="User"
>基本资料</el-dropdown-item
>
<el-dropdown-item ***mand="avatar" :icon="Crop"
>更换头像</el-dropdown-item
>
<el-dropdown-item ***mand="password" :icon="EditPen"
>重置密码</el-dropdown-item
>
<el-dropdown-item ***mand="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>
src\api\user.js
- user作者路由的操作
import request from '@/utils/request'
// 注册接口
export const userRegisterService = ({ username, password, repassword }) =>
request.post('/api/reg', { username, password, repassword })
// 登录接口
export const userLoginService = ({ username, password }) =>
request.post('/api/login', { username, password })
// 获取用户基本信息→后端是存储到redis缓存的
export const userGetInfoService = () => request.get('/my/userinfo')
export const userUpdateInfoService = ({ id, nickname, email }) =>
request.put('/my/userinfo', { id, nickname, email })
export const userUploadAvatarService = (avatar) =>
request.patch('/my/update/avatar', { avatar })
export const userUpdatePassService = ({ old_pwd, new_pwd, re_pwd }) =>
request.patch('/my/updatepwd', { old_pwd, new_pwd, re_pwd })
user下面的小模块:实现改用户信息、图像、密码
src\***ponents\PageContainer.vue
- 设计插槽的方式:具名插槽、默认插槽
<script setup>
defineProps({
title: {
requiered: true,
type: String
}
})
</script>
<template>
<el-card class="page-container">
<template #header>
<div class="header">
<span>{{ title }}</span>
<!--具名插槽:指定了一个名为 "extra" 的插槽,父组件可以通过这个名称将内容插入到这个插槽中-->
<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>
src\views\user\UserProfile.vue
<script setup>
import PageContainer from '@/***ponents/PageContainer.vue'
import { useUserStore } from '@/stores'
import { ref } from 'vue'
import { userUpdateInfoService } from '@/api/user'
const formRef = ref()
// 快速从useUserStore里面解析出方法、属性
// getUser快速实现更新的动作
const {
user: { username, nickname, email, id, getUser }
} = useUserStore()
// 基于上面的字段进行初始化了
const userInfo = ref({ username, nickname, email, id })
const rules = {
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' }
]
}
const onSubmit = async () => {
// 进行全局的校验,
const valid = await formRef.value.validate()
if (valid) {
await userUpdateInfoService(userInfo.value)
//更新数据
await getUser()
ElMessage.su***ess('修改成功')
}
}
</script>
<template>
<page-container title="基本资料">
<el-row>
<el-col :span="12">
<el-form
:model="userInfo"
:rules="rules"
ref="formRef"
label-width="100px"
size="large"
>
<el-form-item label="登录名称">
<el-input v-model="userInfo.username" disabled></el-input>
</el-form-item>
<el-form-item label="用户昵称" prop="nickname">
<el-input v-model="userInfo.nickname"></el-input>
</el-form-item>
<el-form-item label="用户邮箱" prop="email">
<el-input v-model="userInfo.email"></el-input>
</el-form-item>
<el-form-item>
<el-button @click="onSubmit" type="primary">提交修改</el-button>
</el-form-item>
</el-form>
</el-col>
</el-row>
</page-container>
</template>
src\views\user\UserAvatar.vue
- 学会了借用别人的功能: 借用了上面上传图片的功能
<script setup>
import PageContainer from '@/***ponents/PageContainer.vue'
import { ref } from 'vue'
import { Plus, Upload } from '@element-plus/icons-vue'
import { useUserStore } from '@/stores'
import { userUploadAvatarService } from '../../api/user'
const userStore = useUserStore()
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
}
}
// 上传图片进行更新
const onUpdateAvatar = async () => {
await userUploadAvatarService(imgUrl.value)
//马上更新信息→更新图像,不用再进行刷新
await userStore.getUser()
ElMessage({ type: 'su***ess', message: '更换头像成功' })
}
</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
@click="uploadRef.$el.querySelector('input').click()"
type="primary"
:icon="Plus"
size="large"
>
选择图片
</el-button>
<el-button
@click="onUpdateAvatar"
type="su***ess"
: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>
src\views\user\UserPassword.vue
<script setup>
import { ref } from 'vue'
import { useRoute } from 'vue-router'
import { useUserStore } from '@/stores'
import { userUpdatePassService } from '@/api/user'
const formRef = ref()
const router = useRoute()
const userStore = useUserStore()
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' }
]
}
const onSubmit = async () => {
const valid = await formRef.value.validate()
if (valid) {
await userUpdatePassService(pwdForm.value)
ElMessage({ type: 'su***ess', message: '更换密码成功' })
userStore.setToken('')
userStore.setUser({})
router.push('/login')
}
}
const onReset = () => {
formRef.value.reseetFields()
}
</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>
③文章的目录编辑:
src\***ponents\PageContainer.vue
- 也是往里面进行填充的
<script setup>
defineProps({
title: {
requiered: true,
type: String
}
})
</script>
<template>
<el-card class="page-container">
<template #header>
<div class="header">
<span>{{ title }}</span>
<!--具名插槽:指定了一个名为 "extra" 的插槽,父组件可以通过这个名称将内容插入到这个插槽中-->
<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>
src\views\article\ArticleChannel.vue
<script setup>
import { ref } from 'vue'
import { Edit, Delete } from '@element-plus/icons-vue'
import { artGetChannelsService, artDelChannelService } from '../../api/article'
import ChannelEdit from './***ponents/ChannelEdit.vue'
const channelList = ref([])
// 是加载旋转的效果
const loading = ref(false)
//绑定了组件,可以获取里面的方法
const dialog = ref()
const getChannelList = async () => {
loading.value = true
const res = await artGetChannelsService()
channelList.value = res.data.data
loading.value = false
}
// 开始就调用获取数据
getChannelList()
// row是获取一行的数据
const onDelChannel = async (row) => {
await ElMessageBox.confirm('你确认要删除该分类么', '温馨提示', {
type: 'warning',
confirmButtonText: '确认',
cancelButtonText: '取消'
})
await artDelChannelService(row.id)
ElMessage.su***ess('删除成功')
getChannelList()
}
// 编辑文章分类要进行回显
const onEditChannel = (row) => {
dialog.value.open(row)
}
// 添加文章分类不需要→调用子组件的方法
const onAddChannel = () => {
dialog.value.open({})
}
// 在成功后,
const onSu***ess = () => {
getChannelList()
}
</script>
<template>
<page-container title="文章分类">
<template #extra>
<el-button @click="onAddChannel">添加分类</el-button>
</template>
<el-table v-loading="loading" :data="channelList" style="width: 100%">
<el-table-column type="index" 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 就是 channelList 的一项, $index 下标 -->
<template #default="{ row, $index }">
<el-button
:icon="Edit"
circle
plain
type="primary"
@click="onEditChannel(row, $index)"
></el-button>
<el-button
:icon="Delete"
circle
plain
type="danger"
@click="onDelChannel(row, $index)"
></el-button>
</template>
</el-table-column>
<template #empty>
<el-empty description="没有数据"></el-empty>
</template>
</el-table>
<channel-edit ref="dialog" @su***ess="onSu***ess"></channel-edit>
</page-container>
</template>
<style lang="scss" scoped></style>
src\views\article\***ponents\ChannelEdit.vue
- 进行文章分类的编辑→区分添加、编辑文章(编辑要进行回显)
<script setup>
import { ref } from 'vue'
import { artEditChannelService, artAddChannelService } from '@/api/article.js'
const dialogVisible = ref(false) //绑定窗口是否出现
const formRef = ref()
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(['su***ess']) //给父组件的方法,实现更新
const onSubmit = async () => {
// 先进行全局的校验
await formRef.value.validate()
//区分编辑、添加
const isEdit = formModel.value.id
if (isEdit) {
await artEditChannelService(formModel.value)
ElMessage.su***ess('编辑成功')
} else {
await artAddChannelService(formModel.value)
ElMessage.su***ess('添加成功')
}
//关闭弹层、
dialogVisible.value = false
//进行子传父
emit('su***ess')
}
// 组件对外暴露一个方法 open,基于open传来的参数,区分添加还是编辑
// open({}) => 表单无需渲染,说明是添加
// open({ id, cate_name, ... }) => 表单需要渲染,说明是编辑
// open调用后,可以打开弹窗
const open = (row) => {
dialogVisible.value = true
// 使用了展开运算符,进行一一赋值
formModel.value = { ...row } // 添加 → 重置了表单内容,编辑 → 存储了需要回显的数据
}
// 向外暴露方法
defineExpose({
open
})
</script>
<template>
<el-dialog
v-model="dialogVisible"
:title="formModel.id ? '编辑分类' : '添加分类'"
width="30%"
>
<el-form
ref="formRef"
: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>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="onSubmit"> 确认 </el-button>
</span>
</template>
</el-dialog>
</template>
④文章的详情管理:
src\views\article\ArticleManage.vue
- 重点知识: <!--在vue2里面 v-model :value和@input的简写
在vue3里面 (v-model :modelValue)和@update:modelValue的简写
下面是要绑定的复选框,实现里面的选择的更新-->
<script setup>
import { Delete, Edit } from '@element-plus/icons-vue'
import { ref } from 'vue'
import { formatTime } from '@/utils/format'
import { ElMessage, ElMessageBox } from 'element-plus'
import { artDelService, artGetListService } from '../../api/article'
import ArticleEdit from './***ponents/ArticleEdit.vue'
import channelSelect from './***ponents/ChannelSelect.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: null,
// cate_name: '体育'
// }
// ])
// const onEditArticle = (row) => {
// console.log(row)
// }
const onDeleteArticle = async (row) => {
await ElMessageBox.confirm('你确认删除文章吗?', '温馨提示', {
type: 'warning',
confirmButtonText: '确认',
cancelButtonText: '取消'
})
await artDelService(row.id)
ElMessage({ type: 'su***ess', message: '删除成功了' })
getArticleList()
}
const params = ref({
pagenum: 1,
pagesize: 5,
cate_id: '', // 复选框绑定的默认值
state: ''
})
const articleList = ref([])
const total = ref(0)
const loading = ref(false)
const getArticleList = async () => {
loading.value = true
const res = await artGetListService(params.value)
articleList.value = res.data.data
total.value = res.data.total
console.log(res.data.data)
loading.value = false //加载数据结束
}
// 开始进行初始化的数据
getArticleList()
// 处理分页的逻辑→每页数量发生改变
const onSizeChange = (size) => {
// 只有是没页数变化了,那么原本正在访问的当前页面意义不大,数据大概率不再那一页了
// 重新从第一页渲染即可
params.value.pagenum = 1
params.value.pagesize = size
getArticleList()
}
// 处理到了第几页
const onCurrentChange = (page) => {
params.value.pagenum = page
// 基于最新页面,进行渲染数据
getArticleList()
}
// 搜索逻辑=》按照最新的条件,重新检索,从第一页开始展示
const onSearch = () => {
params.value.pagenum = 1 // 重置页面
getArticleList()
}
// 重置逻辑→将筛选的条件清空,重新检索,从第一页开始展示
const onReset = () => {
params.value.pagenum = 1
params.value.cate_id = ''
params.value.state = ''
getArticleList()
}
// const visibleDrawer = ref(false)
// const onAddArticle = () => {
// visibleDrawer.value = true
// }
// 封装抽屉
const articleEditRef = ref()
// 编辑新增逻辑
const onAddArticle = () => {
articleEditRef.value.open({})
}
const onEditArticle = (row) => {
articleEditRef.value.open(row)
}
// 父组件监听事件,重新渲染
const onSu***ess = (type) => {
if (type === 'add') {
const lastPage = Math.ceil((total.value + 1) / params.value.pagesize)
params.value.pagenum = lastPage
}
getArticleList()
}
</script>
<template>
<page-container title="文章管理">
<template #extra>
<el-button type="primary" @click="onAddArticle">发布文章</el-button>
</template>
<el-form inline>
<el-form-item label="文章分类:">
<!--在vue2里面 v-model :value和@input的简写
在vue3里面 (v-model :modelValue)和@update:modelValue的简写
下面是要绑定的复选框,实现里面的选择的更新-->
<channel-select width="100%" v-model="params.cate_id"></channel-select>
</el-form-item>
<el-form-item label="发布状态:">
<el-select v-model="params.state">
<el-option label="已发布" value="已发布"></el-option>
<el-option label="草稿" value="草稿"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="onSearch" type="primary">搜索</el-button>
<el-button @click="onReset">重置</el-button>
</el-form-item>
</el-form>
<!--下面的部分-->
<el-table :data="articleList" style="width: 100%" v-loading="loading">
<el-table-column label="文章标题" width="400">
<!--里面是默认的链接插槽,row是一行的数据-->
<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">
<template #default="{ row }">
{{ formatTime(row.pub_data) }}
</template>
</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>
<!--分页模块-->
<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"
/>
</page-container>
<!-- 准备抽屉容器
<el-drawer v-model="visibleDrawer" title="大标题" direction="rtl" size="50%">
<span>Hi there!</span>
</el-drawer> -->
<!-- 弹窗 -->
<article-edit ref="articleEditRef" @su***ess="onSu***ess"></article-edit>
</template>
src\views\article\***ponents\ChannelSelect.vue
- 选择复选框:vue3实现数据的双向绑定
<script setup>
import { artGetChannelsService } from '@/api/article'
import { ref } from 'vue'
// 父传子进行接收
defineProps({
modelValue: {
type: [Number, String]
},
width: {
type: String
}
})
// 子传父进行更新
const emit = defineEmits(['update:modelValue'])
const channelList = ref([])
const getChannelList = async () => {
// 是获取文章的分类 channel
const res = await artGetChannelsService()
channelList.value = res.data.data
}
getChannelList()
</script>
<!--开始是没有选中的
把$event里面选中的值,传给父类,实现复选框的更新→实现了双向的绑定-->
<template>
<el-select
:modelValue="modelValue"
@update:modelValue="emit('update:modelValue', $event)"
:style="{ width }"
>
<el-option
v-for="channel in channelList"
:key="channel.id"
:label="channel.cate_name"
:value="channel.id"
></el-option>
</el-select>
</template>
src\views\article\***ponents\ArticleEdit.vue
<script setup>
import { ref } from 'vue'
import ChannelSelect from './channelselect.vue'
import { Plus } from '@element-plus/icons-vue'
import { QuillEditor } from '@vueup/vue-quill'
import '@vueup/vue-quill/dist/vue-quill.snow.css'
import {
artGetDetailService,
artPublishService,
artEditService
} from '../../../api/article'
import { ElMessage } from 'element-plus'
import { baseURL } from '@/utils/request'
import axios from 'axios'
const visibleDrawer = ref(false)
const defaultForm = {
title: '',
cate_id: '',
cover_img: '',
content: '',
state: ''
}
const formModel = ref({ ...defaultForm })
const formRef = ref()
const editorRef = ref()
// 将网络图片地址转换为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
}
}
const imgUrl = ref('')
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对象,存储起来,方便提交
formModel.value.cover_img = await imageUrlToFile(
imgUrl.value,
formModel.value.cover_img
)
} else {
console.log('添加功能')
formModel.value = { ...defaultForm }
imgUrl.value = ''
editorRef.value.setHTML('') //进行清空
}
}
defineExpose({
open
})
// 图片上传逻辑
// onSelectFile是element提供的函数,改变的钩子,图片上传
const onUploadFile = (uploadFile) => {
console.log(uploadFile)
// 预览图片
imgUrl.value = URL.createObjectURL(uploadFile.raw)
// 立刻将图片对象转入提交
formModel.value.cover_img = uploadFile.raw
}
const emit = defineEmits(['su***ess'])
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) {
if (formModel.value.id) {
await artEditService(fd)
ElMessage.su***ess('编辑成功')
visibleDrawer.value = false
emit('su***ess', 'edit')
} else {
// 添加请求
await artPublishService(fd)
ElMessage.su***ess('添加成功')
visibleDrawer.value = false
emit('su***ess', 'add')
}
}
}
</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"
width="100%"
></channel-select>
</el-form-item>
<!--此处不需要element-plus自动上传,不需要配置action参数
只需要做前端的本地预览即可,无需提交前上传图片,以免取消,浪费空间
语法:URL.createObjectURL(...)创建本地预览的地址,来预览-->
<el-form-item label="文章封面" prop="cover_img">
<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>
</el-form-item>
<el-form-item label="文章内容" prop="content">
<div class="editor">
<div class="editor">
<quill-editor
ref="editorRef"
theme="snow"
v-model:content="formModel.content"
contentType="html"
>
</quill-editor>
</div>
</div>
</el-form-item>
<el-form-item>
<el-button @click="onPublish('已发布')" type="primary">发布</el-button>
<el-button @click="onPublish('草稿')" type="info">草稿</el-button>
</el-form-item>
</el-form>
</el-drawer>
</template>
<style lang="scss" scoped>
.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;
}
}
}
.editor {
width: 100%;
:deep(.ql-editor) {
min-height: 200px;
}
}
</style>
src\utils\format.js
import { dayjs } from 'element-plus'
export const formatTime = (time) => dayjs(time).format('YYYY年MM月DD日')
⑤全局的路由:
import { createRouter, createWebHistory } from 'vue-router'
import { useUserStore } from '@/stores'
// createRouter 创建路由实例
// 配置 history 模式
// 1. history模式:createWebHistory 地址栏不带 #
// 2. hash模式: createWebHashHistory 地址栏带 #
// console.log(import.meta.env.DEV)
// vite 中的环境变量 import.meta.env.BASE_URL 就是 vite.config.js 中的 base 配置项
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{ path: '/login', ***ponent: () => import('@/views/login/LoginPage.vue') }, // 登录页
{
path: '/',
***ponent: () => import('@/views/layout/LayoutContainer.vue'),
redirect: '/article/manage',
children: [
{
path: '/article/manage',
***ponent: () => import('@/views/article/ArticleManage.vue')
},
{
path: '/article/channel',
***ponent: () => import('@/views/article/ArticleChannel.vue')
},
{
path: '/user/profile',
***ponent: () => import('@/views/user/UserProfile.vue')
},
{
path: '/user/avatar',
***ponent: () => import('@/views/user/UserAvatar.vue')
},
{
path: '/user/password',
***ponent: () => import('@/views/user/UserPassword.vue')
}
]
}
]
})
// 登录访问拦截 => 默认是直接放行的
// 根据返回值决定,是放行还是拦截
// 返回值:
// 1. undefined / true 直接放行
// 2. false 拦回from的地址页面
// 3. 具体路径 或 路径对象 拦截到对应的地址
// '/login' { name: 'login' }
router.beforeEach((to) => {
// 如果没有token, 且访问的是非登录页,拦截到登录,其他情况正常放行
const useStore = useUserStore()
if (!useStore.token && to.path !== '/login') return '/login'
})
export default router