const login = async () => {
await form.value.validate()
console.log('开始登录')
}
```
##### 1.6.2 封装接口 API
```js
export const userLoginService = ({ username, password }) =>
request.post('api/login', { username, password })
```
##### 1.6.3 调用方法将 token 存入 pinia 并 自动持久化本地
```js
const userStore = useUserStore()
const router = useRouter()
const login = async () => {
await form.value.validate()
const res = await userLoginService(formModel.value)
userStore.setToken(res.data.token)
ElMessage.success('登录成功')
router.push('/')
}
```
### 2. 首页layout布局 [element-plus 菜单组件]
功能需求说明:
- 基本布局拆解 (菜单组件的使用)
- 登录访问拦截
- 用户基本信息获取&渲染
- 退出功能 [element-plus 确认框]
#### 2.1 布局组件列表
> el-container
>> - el-aside 左侧
>>> el-menu 左侧边栏菜单
>> - el-container 右侧
>>> el-header 右侧头部
>>> el-dropdown
>>> el-main 右侧主体 router-view
```js
文章分类
文章管理
个人中心
基本资料
更换头像
重置密码
用户:windstarry
基本资料
更换头像
重置密码
退出登录
后台管理系统 ©2023 Created by Windstarry
```
#### 2.2 登录访问拦截
需求:只有登录页,可以未授权的时候访问,其他所有页面,都需要先登录再访问
```js
// 登陆访问拦截 => 默认是直接放行的
// 根据返回值决定,是放行还是拦截
// 返回值:
// 1.undefined / true 直接放行
// 2.false 拦回from的地址页面
// 3.具体路径 或 路径对象 拦截到对应的地址
// '/login' {name: 'login'}
router.beforeEach((to) => {
// 如果没有token,且访问的是非登录页,拦截到登录,其他情况正常放行
const useStore = useUserStore()
if (!useStore.token && to.path !== '/login') return '/login'
return true
})
export default router
```
#### 2.3 用户基本信息获取&渲染
##### 2.3.1 api/user.js封装接口
```js
export const userGetInfoService = () => request.get('/my/userinfo')
```
##### 2.3.2 stores/modules/user.js 定义数据
```js
const user = ref({})
const getUser = async () => {
const res = await userGetInfoService() // 请求获取数据
user.value = res.data.data
}
```
##### 2.3.3 layout/LayoutContainer页面中调用
```js
import { useUserStore } from '@/stores'
const userStore = useUserStore()
onMounted(() => {
userStore.getUser()
})
```
##### 2.3.4 动态渲染
```js
用户:{{ userStore.user.nickname || userStore.user.username }}
```
#### 2.4 退出功能 [element-plus 确认框]
##### 2.4.1 注册点击事件
```js
基本资料
更换头像
重置密码
退出登录
```
##### 2.4.2 添加退出功能
```js
const onCommand = async (command) => {
if (command === 'logout') {
await ElMessageBox.confirm('确认退出?', '温馨提示', {
type: 'warning',
confirmButtonText: '确认',
cancelButtonText: '取消'
})
userStore.removeToken()
userStore.setUser({})
router.push(`/login`)
} else {
router.push(`/user/${command}`)
}
}
```
##### 2.4.3 pinia user.js 模块 提供 setUser 方法
```js
const setUser = (obj) => (user.value = obj)
```
### 3. 文章分类页面 [element-plus 表格]
功能需求说明:
- 基本布局 - PageContainer 封装
- 文章分类渲染 & loading 处理
- 文章分类添加编辑 [element-plus 弹层]
- 文章分类删除
#### 3.1 文章分类页面 - [element-plus 表格]
```js
...
```
考虑到多个页面复用,封装成组件
- props 定制标题
- 默认插槽 default 定制内容主体
- 具名插槽 extra 定制头部右侧额外的按钮
在compnents文件夹下创建PageContainer.vue
```js
```
页面中直接使用测试 ( unplugin-vue-components 会自动注册)
- 增加图标自动导入
```js
pnpm add -D unplugin-icons unplugin-vue-components
```
- 配置vite.config.js
```js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import Icons from 'unplugin-icons/vite'
import IconsResolver from 'unplugin-icons/resolver'
import Components from 'unplugin-vue-components/vite'
export default defineConfig({
plugins: [
vue(),
Components({
resolvers: [
// 自动注册图标组件
IconsResolver(),
],
}),
Icons(),
],
})
```
- 文章分类测试
```js
添加分类
主体部分
```
- 文章管理测试
```js
发布文章
主体部分
```
#### 3.2 文章分类渲染
##### 3.2.1 封装API - 请求获取表格数据
- 新建 api/article.js 封装获取频道列表的接口
```js
import request from '@/utils/request'
export const artGetChannelsService = () => request.get('/my/cate/list')
```
- 页面中调用接口,获取数据存储
```js
const channelList = ref([])
const getChannelList = async () => {
const res = await artGetChannelsService()
channelList.value = res.data.data
}
```
##### 3.2.2 el-table 表格动态渲染
修改views/article/ArticleChannel.vue
```js
const onEditChannel = (row) => {
console.log(row)
}
const onDelChannel = (row) => {
console.log(row)
}
```
##### 3.2.3 el-table 表格 loading 效果
- 定义变量,v-loading绑定
```js
const loading = ref(false)
```
- 发送请求前开启,请求结束关闭
```js
const getChannelList = async () => {
loading.value = true
const res = await artGetChannelsService()
channelList.value = res.data.data
loading.value = false
}
```
#### 3.3 准备弹层表单
##### 3.3.1 准备数据 和 校验规则
```js
const formModel = ref({
cate_name: '',
cate_alias: ''
})
const rules = {
cate_name: [
{ required: true, message: '请输入分类名称', trigger: 'blur' },
{
pattern: /^\S{1,10}$/,
message: '分类名必须是1-10位的非空字符',
trigger: 'blur'
}
],
cate_alias: [
{ required: true, message: '请输入分类别名', trigger: 'blur' },
{
pattern: /^[a-zA-Z0-9]{1,15}$/,
message: '分类别名必须是1-15位的字母数字',
trigger: 'blur'
}
]
}
```
##### 3.3.2 准备表单
```js
```
##### 3.3.3 编辑需要回显,表单数据需要初始化
```js
const open = async (row) => {
dialogVisible.value = true
formModel.value = { ...row }
}
```
##### 3.3.4 基于传过来的表单数据,进行标题控制,有 id 的是编辑
```js
:title="formModel.id ? '编辑分类' : '添加分类'"
```
#### 3.4 确认提交
##### 3.4.1 封装 api/article.js 封装请求 API
```js
// 添加文章分类
export const artAddChannelService = (data) => request.post('/my/cate/add', data)
// 编辑文章分类
export const artEditChannelService = (data) =>
request.put('/my/cate/info', data)
```
##### 3.4.2 页面中校验,判断,提交请求
```js
```
```js
const formRef = ref()
const onSubmit = async () => {
await formRef.value.validate()
formModel.value.id
? await artEditChannelService(formModel.value)
: await artAddChannelService(formModel.value)
ElMessage({
type: 'success',
message: formModel.value.id ? '编辑成功' : '添加成功'
})
dialogVisible.value = false
}
```
##### 3.4.3 通知父组件进行回显
```js
const emit = defineEmits(['success'])
const onSubmit = async () => {
...
emit('success')
}
```
##### 3.4.4 父组件监听 success 事件,进行调用回显
```js
const onSuccess = () => {
getChannelList()
}
```
#### 3.5 文章分类删除
##### 3.5.1 封装api/article.js封装接口 api
```js
// 删除文章分类
export const artDelChannelService = (id) =>
request.delete('/my/cate/del', {
params: { id }
})
```
##### 3.5.2 页面中添加确认框,调用接口进行提示
```js
const onDelChannel = async (row) => {
await ElMessageBox.confirm('你确认删除该分类信息吗?', '温馨提示', {
type: 'warning',
confirmButtonText: '确认',
cancelButtonText: '取消'
})
await artDelChannelService(row.id)
ElMessage({ type: 'success', message: '删除成功' })
getChannelList()
}
```
### 4. 文章列表渲染
#### 4.1 基本框架搭建
搜索表单
表格准备,模拟假数据渲染
```js
import { Delete, Edit } from '@element-plus/icons-vue'
import { ref } from 'vue'
// mock数据
const articleList = ref([
{
id: 5961,
title: '新的文章啊',
pub_date: '2022-07-10 14:53:52.604',
state: '已发布',
cate_name: '体育'
},
{
id: 5962,
title: '新的文章啊',
pub_date: '2022-07-10 14:54:30.904',
state: null,
cate_name: '体育'
}
])
```
```js
{{ row.title }}
const onEditArticle = (row) => {
console.log(row)
}
const onDeleteArticle = (row) => {
console.log(row)
}
```
#### 4.2 中英国际化处理
默认是英文的,由于这里不涉及切换, 所以在 App.vue 中直接导入设置成中文即可
```js
```
#### 4.3 文章分类选择
为了便于维护,直接拆分成一个小组件 ChannelSelect.vue
- 新建 article/components/ChannelSelect.vue
```js
```
- 页面中导入渲染
```js
import ChannelSelect from './components/ChannelSelect.vue'
```
- 调用接口,动态渲染下拉分类,设计成 v-model 的使用方式
```js
```
- 父组件定义参数绑定
```js
const params = ref({
pagenum: 1,
pagesize: 5,
cate_id: '',
state: ''
})
```
- 发布状态,也绑定一下,便于将来提交表单
```js
```
#### 4.4 封装 API 接口,请求渲染
- api/article.js封装接口
```js
export const artGetListService = (params) =>
request.get('/my/article/list', { params })
```
- 页面中调用保存数据
```js
const articleList = ref([])
const total = ref(0)
const getArticleList = async () => {
const res = await artGetListService(params.value)
articleList.value = res.data.data
total.value = res.data.total
}
getArticleList()
```
- 新建 utils/format.js 封装格式化日期函数
```js
import { dayjs } from 'element-plus'
export const formatTime = (time) => dayjs(time).format('YYYY年MM月DD日')
```
- 导入使用
```js
import { formatTime } from '@/utils/format'
{{ formatTime(row.pub_date) }}
```
#### 4.5 分页渲染 [element-plus 分页]
- 分页组件
```js
```
- 提供分页修改逻辑
```js
const onSizeChange = (size) => {
params.value.pagenum = 1
params.value.pagesize = size
getArticleList()
}
const onCurrentChange = (page) => {
params.value.pagenum = page
getArticleList()
}
```
#### 4.6 添加 loading 处理
- 准备数据
```js
const loading = ref(false)
```
- el-table上面绑定
```js
...
```
- 发送请求时添加 loading
```js
const getArticleList = async () => {
loading.value = true
...
loading.value = false
}
getArticleList()
```
#### 4.7 搜索 和 重置功能
- 注册事件
```js
搜索
重置
```
- 绑定处理
```js
const onSearch = () => {
params.value.pagenum = 1
getArticleList()
}
const onReset = () => {
params.value.pagenum = 1
params.value.cate_id = ''
params.value.state = ''
getArticleList()
}
```
### 5. 文章发布&修改 [element-plus - 抽屉]
#### 5.1 点击显示抽屉
- 准备数据
```js
import { ref } from 'vue'
const visibleDrawer = ref(false)
```
- 准备抽屉容器
```js
Hi there!
```
- 点击修改布尔值显示抽屉
```js
发布文章
const visibleDrawer = ref(false)
const onAddArticle = () => {
visibleDrawer.value = true
}
```
#### 5.2 封装抽屉组件 ArticleEdit
添加 和 编辑,可以共用一个抽屉,所以可以将抽屉封装成一个组件
组件对外暴露一个方法 open, 基于 open 的参数,初始化表单数据,并判断区分是添加 还是 编辑
> open({ }) => 添加操作,添加表单初始化无数据
> open({ id: xx, … }) => 编辑操作,编辑表单初始化需回显
- 封装组件 article/components/ArticleEdit.vue
```js
Hi there!
```
- 通过 ref 绑定
```js
const articleEditRef = ref()
```
- 点击调用方法显示弹窗
```js
// 编辑新增逻辑
const onAddArticle = () => {
articleEditRef.value.open({})
}
const onEditArticle = (row) => {
articleEditRef.value.open(row)
}
```
#### 5.3 完善抽屉表单结构
- 准备数据
```js
const formModel = ref({
title: '',
cate_id: '',
cover_img: '',
content: '',
state: ''
})
const open = async (row) => {
visibleDrawer.value = true
if (row.id) {
console.log('编辑回显')
} else {
console.log('添加功能')
}
}
```
- 准备 form 表单结构
```js
import ChannelSelect from './ChannelSelect.vue'
文件上传
富文本编辑器
发布
草稿
```
- 打开默认重置添加的 form 表单数据
```js
const defaultForm = {
title: '',
cate_id: '',
cover_img: '',
content: '',
state: ''
}
const formModel = ref({ ...defaultForm })
const open = async (row) => {
visibleDrawer.value = true
if (row.id) {
console.log('编辑回显')
} else {
console.log('添加功能')
formModel.value = { ...defaultForm }
}
}
```
- 扩展 下拉菜单 width props
修改ChannelSelect.vue
```js
defineProps({
modelValue: {
type: [Number, String]
},
width: {
type: String
}
})
```
#### 5.4 上传文件 [element-plus - 文件预览]
- 关闭自动上传,准备结构
>此处需要关闭 element-plus 的自动上传,不需要配置 action 等参数只需要做前端的本地预览图片即可,无需在提交前上传图标
预览图片的语法:URL.createObjectURL(...) 创建本地预览的地址预览
```js
import { Plus } from '@element-plus/icons-vue'
```
- 准备数据 和 选择图片的处理逻辑
```js
const imgUrl = ref('')
const onUploadFile = (uploadFile) => {
imgUrl.value = URL.createObjectURL(uploadFile.raw)
formModel.value.cover_img = uploadFile.raw
}
```
- 样式美化
```js
.avatar-uploader {
:deep() {
.avatar {
width: 178px;
height: 178px;
display: block;
}
.el-upload {
border: 1px dashed var(--el-border-color);
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
}
.el-upload:hover {
border-color: var(--el-color-primary);
}
.el-icon.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
text-align: center;
}
}
}
```
#### 5.5 富文本编辑器 [ vue-quill ]
- 安装
```shell
```
- 注册成局部组件
```js
```
- 页面中使用绑定
```js
import { QuillEditor } from '@vueup/vue-quill'
import '@vueup/vue-quill/dist/vue-quill.snow.css'
```
- 样式美化
```js
.editor {
width: 100%;
:deep(.ql-editor) {
min-height: 200px;
}
}
```
#### 5.6 添加文章功能
##### 5.6.1 封装添加接口
```js
export const artPublishService = (data) =>
request.post('/my/article/add', data)
```
##### 5.6.2 注册点击事件调用
```js
发布
草稿
// 发布文章
const emit = defineEmits(['success'])
const onPublish = async (state) => {
// 将已发布还是草稿状态,存入 state
formModel.value.state = state
// 转换 formData 数据
const fd = new FormData()
for (let key in formModel.value) {
fd.append(key, formModel.value[key])
}
if (formModel.value.id) {
console.log('编辑操作')
} else {
// 添加请求
await artPublishService(fd)
ElMessage.success('添加成功')
visibleDrawer.value = false
emit('success', 'add')
}
}
```
##### 5.6.3 父组件监听事件,重新渲染
```js
// 添加修改成功
const onSuccess = (type) => {
if (type === 'add') {
// 如果是添加,需要跳转渲染最后一页,编辑直接渲染当前页
const lastPage = Math.ceil((total.value + 1) / params.value.pagesize)
params.value.pagenum = lastPage
}
getArticleList()
}
```
#### 5.7 添加完成后的内容重置
```js
const formRef = ref()
const editorRef = ref()
const open = async (row) => {
visibleDrawer.value = true
if (row.id) {
console.log('编辑回显')
} else {
formModel.value = { ...defaultForm }
imgUrl.value = ''
editorRef.value.setHTML('')
}
}
```
#### 5.8 编辑文章回显
如果是编辑操作,一打开抽屉,就需要发送请求,获取数据进行回显
##### 5.8.1 封装接口,根据 id 获取详情数据
```js
export const artGetDetailService = (id) =>
request.get('my/article/info', { params: { id } })
```
##### 5.8.2 页面中调用渲染
```js
const open = async (row) => {
visibleDrawer.value = true
if (row.id) {
console.log('编辑回显')
const res = await artGetDetailService(row.id)
formModel.value = res.data.data
imgUrl.value = baseURL + formModel.value.cover_img
// 提交给后台,需要的是 file 格式的,将网络图片,转成 file 格式
// 网络图片转成 file 对象, 需要转换一下
formModel.value.cover_img = await imageUrlToFile(imgUrl.value, formModel.value.cover_img)
} else {
console.log('添加功能')
...
}
}
```
##### 5.8.3 chatGPT prompt:封装一个函数,基于 axios, 网络图片地址,转 file 对象, 请注意:写中文注释
```js
// 将网络图片地址转换为File对象
async function imageUrlToFile(url, fileName) {
try {
// 第一步:使用axios获取网络图片数据
const response = await axios.get(url, { responseType: 'arraybuffer' });
const imageData = response.data;
// 第二步:将图片数据转换为Blob对象
const blob = new Blob([imageData], { type: response.headers['content-type'] });
// 第三步:创建一个新的File对象
const file = new File([blob], fileName, { type: blob.type });
return file;
} catch (error) {
console.error('将图片转换为File对象时发生错误:', error);
throw error;
}
}
```
#### 5.9 编辑文章功能
##### 5.9.1 封装编辑接口
```js
export const artEditService = (data) => request.put('my/article/info', data)
```
##### 5.9.2 页面中调用渲染
```js
const onPublish = async (state) => {
...
if (formModel.value.id) {
await artEditService(fd)
ElMessage.success('编辑成功')
visibleDrawer.value = false
emit('success', 'edit')
} else {
// 添加请求
...
}
}
```
#### 5.10 文章删除
- 封装删除接口
```js
export const artDelService = (id) => request.delete('my/article/info', { params: { id } })
```
- 页面中添加确认框调用
```js
const onDeleteArticle = async (row) => {
await ElMessageBox.confirm('你确认删除该文章信息吗?', '温馨提示', {
type: 'warning',
confirmButtonText: '确认',
cancelButtonText: '取消'
})
await artDelService(row.id)
ElMessage({ type: 'success', message: '删除成功' })
getArticleList()
}
```
### 6. 个人中心项目实战 - 基本资料
#### 6.1 静态结构 + 校验处理
- chatgpt prompt 提示词参考
```text
请基于 elementPlus 和 Vue3 的语法,生成组件代码
要求:
一、表单结构要求
1. 组件中包含一个el-form表单,有四行内容,前三行是输入框,第四行是按钮
2. 第一行 label 登录名称,输入框禁用不可输入状态
3. 第二行 label 用户昵称,输入框可输入
4. 第三行 label 用户邮箱,输入框可输入
5. 第四行按钮,提交修改
二、校验需求
给昵称 和 邮箱添加校验
1. 昵称 nickname 必须是2-10位的非空字符串
2. 邮箱 email 符合邮箱格式即可,且不能为空
```
- 参考目标代码
```js
提交修改
```
#### 6.2 封装接口,更新个人信息
- 封装接口
```js
export const userUpdateInfoService = ({ id, nickname, email }) =>
request.put('/my/userinfo', { id, nickname, email })
```
- 页面中校验后,封装调用
```js
const formRef = ref()
const onSubmit = async () => {
const valid = await formRef.value.validate()
if (valid) {
await userUpdateInfoService(userInfo.value)
await getUser()
ElMessage.success('修改成功')
}
}
```
### 7. 个人中心项目实战 - 更换头像
#### 7.1 静态结构
```js
选择图片
上传头像
```
#### 7.2 选择预览图片
```js
const uploadRef = ref()
const imgUrl = ref(userStore.user.user_pic)
const onUploadFile = (file) => {
const reader = new FileReader()
reader.readAsDataURL(file.raw)
reader.onload = () => {
imgUrl.value = reader.result
}
}
选择图片
```
#### 7.3 上传头像
- 封装接口
```js
export const userUploadAvatarService = (avatar) => request.patch('/my/update/avatar', { avatar })
```
- 调用接口
```js
const onUpdateAvatar = async () => {
await userUploadAvatarService(imgUrl.value)
await userStore.getUser()
ElMessage({ type: 'success', message: '更换头像成功' })
}
```
### 8. 个人中心项目实战 - 重置密码
- chatgpt prompt提示词
```text
请基于 elementPlus 和 Vue3 的语法,生成组件代码
要求:
一、表单结构要求
组件中包含一个el-form表单,有四行内容,前三行是表单输入框,第四行是两个按钮
第一行 label 原密码
第二行 label 新密码
第三行 label 确认密码
第四行两个按钮,修改密码 和 重置
二、form绑定字段如下:
const pwdForm = ref({
old_pwd: ‘’,
new_pwd: ‘’,
re_pwd: ‘’
})
三、校验需求
所有字段,都是 6-15位 非空
自定义校验1:原密码 和 新密码不能一样
自定义校验2:新密码 和 确认密码必须一样
```
#### 8.1 静态结构 + 校验处理
```js
修改密码
重置
```
#### 8.2 封装接口,更新密码信息
- 封装接口
```js
export const userUpdatePassService = ({ old_pwd, new_pwd, re_pwd }) =>
request.patch('/my/updatepwd', { old_pwd, new_pwd, re_pwd })
```
- 页面中调用
```js
const formRef = ref()
const router = useRouter()
const userStore = useUserStore()
const onSubmit = async () => {
const valid = await formRef.value.validate()
if (valid) {
await userUpdatePassService(pwdForm.value)
ElMessage({ type: 'success', message: '更换密码成功' })
userStore.setToken('')
userStore.setUser({})
router.push('/login')
}
}
const onReset = () => {
formRef.value.resetFields()
}
```