1.先了解页面
- 页面是一个带有筛选功能的表格,筛选部分是一个表单;渲染数据的是一个表格,并带有分页功能;当然还有表格数据的增删改查部分是由一个抽屉组件弹出的。这是这个页面的大概情况
- 做这个文档的原因就是这个页面集成了一个相对复杂的功能模块,在学习的时候跟着视频教程亦步亦趋,产生一种当局者迷的感觉,需要对此页面做一个复盘,须对老师的代码进行分析和对老师的思路进行梳理,形成自己对于这个页面的思考和收获。对于一个初级前端来说,还是值得去理清思路的
- 页面布局如下:
- 页面使用的是vue3+element-plus来实现的,主体组件是el-form和el-table和el-drawer三个组件,除此之外还有图片预览和集成一个富文本编辑器,算是一个相对综合的页面
- 自己需要的成长:关注父子组件之间的通信、封装子组件需要注意的点、element-plus组件的使用、还有就是图片上传与预览知识点以及富文本编辑器的使用
2.页面代码初步分析
- 第一步只需要根据ui把页面的静态结构搭建并保证页面运行不报错即可,直接使用element-plus组件来搭建,并把方法和用不到的属性暂时删除,页面使用临时的假数据填充表格。如下为ChannelManage.vue组件的静态页面结构,抽屉结构先忽略,需要绑定事件:
xml
<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": null, "cate_name": "体育" }
]) // 文章列表的假数据
const loading = ref(false) // loading状态
// 定义请求参数对象,这个是根据接口数据定义的对象格式
const params = ref({
pagenum: 1, // 当前页
pagesize: 5, // 当前生效的每页条数
cate_id: '',
state: ''
})
// const articleEditRef = ref()
</script>
<template>
<page-container title="文章管理">
<template #extra>
<el-button type="primary">添加文章</el-button>
</template>
<!-- 表单区域 -->
<el-form inline>
<el-form-item label="文章分类:">
<!-- label 展示给用户看的,value 收集起来提交给后台的 -->
<el-select>
<el-option label="新闻" value="110"></el-option>
<el-option label="体育" value="137"></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>
<!-- 表格区域 -->
<!-- v-loading="loading"表示表格加载样式 -->
<el-table :data="articleList" v-loading="loading">
<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="操作">
<!-- 操作按钮的样式调整,其中row表示:data循环的每一项,表示上一层colown的数据 -->
<template #default="{ row }">
<el-button
circle
plain
type="primary"
:icon="Edit"
></el-button>
<el-button
circle
plain
type="danger"
:icon="Delete"
></el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页区域 -->
<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"
style="margin-top: 20px; justify-content: flex-end"
/>
<!-- 添加编辑的抽屉 -->
<!-- <article-edit ref="articleEditRef" @success="onSuccess"></article-edit> -->
</page-container>
</template>
<style lang="scss" scoped></style>
- 样式调整el-form表单结构为一行使用inline属性、表格文章标题一栏的链接样式、表格未加载完成的转圈样式、调整表格操作按钮的样式、全局配置组件语言包
- 全局配置组件语言包,就是使用配置组件包裹App.vue的一级路由即可,并在配置组件上设置导入的语言包,相关设置在element-plus官网有例子:
xml
<script setup>
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
</script>
<template>
<el-config-provider :locale='zhCn'>
<router-view></router-view>
</el-config-provider>
</template>
3.组件封装
- 首先组件封装,可以由于减少单个模块代码量、也可以考虑组件的复用性,都是利于维护项目
- 对于页面结构封装成单个组件,需要考虑父子之间的传值
- 发现这里面表单部分的文章分类选项框两个位置都有,考虑封装在当前目录下的components/channelSelect.vue中,并且需要考虑父组件绑定子组件中的数据。这里面使用的方式是子向父传值,并且使用v-model绑定子组件维护的数据,具体代码如下:
xml
<script setup>
import { artGetChannelsService } from '@/api/article.js'
import { ref } from 'vue'
// 实现父子组件数据的双向绑定
defineProps({
modelValue: {
type: [Number, String]
}
})
const emit = defineEmits(['update:modelValue'])
// 从接口获取数据
const channelList = ref([])
const getChannelList = async () => {
const res = await artGetChannelsService()
channelList.value = res.data.data
}
getChannelList()
</script>
<template>
<!-- :modelValue和@update:modelValue是配合父组件中子组件标签的v-model的双向绑定 -->
<el-select
:modelValue="modelValue"
@update:modelValue="emit('update:modelValue', $event)"
>
<!-- 注意:label 展示给用户看的,value 收集起来提交给后台的,这个是el-option规定的 -->
<el-option
v-for="channel in channelList"
:key="channel.id"
:label="channel.cate_name"
:value="channel.id"
></el-option>
</el-select>
</template>
4.由于后端并没有对时间进行处理,封装dayjs函数,补充:element-plus内置了dayjs:
javascript
import { dayjs } from 'element-plus'
export const formatTime = (time) => dayjs(time).format('YYYY年MM月DD日')
5.合并新增和编辑功能的dialog或者drawer对话框组件,在数据传输方面本来是父向子传。但是可以封装一个open方法,然后可以利用底层组件往顶层组件传open方法的方式defineExpose()达到父向子传的作用。总结一句话:本来是需要父向子传template里面的数据,但是利用子向父传open方法的方式来达成,具体代码如下:
xml
<script setup>
import { ref } from 'vue'
const visibleDrawer = ref(false)
const open = (row) => {
visibleDrawer.value = true
console.log(row)
}
defineExpose({
open
})
</script>
<template>
<!-- 抽屉 -->
<el-drawer v-model="visibleDrawer" title="大标题" direction="rtl" size="50%">
<span>Hi there!</span>
</el-drawer>
</template>
4.添加功能且组件封装完成页面代码
- 页面代码如下:
xml
<script setup>
import { ref } from 'vue'
import { Delete, Edit } from '@element-plus/icons-vue'
import ChannelSelect from './components/ChannelSelect.vue'
import ArticleEdit from './components/ArticleEdit.vue'
import { artGetListService, artDelService } from '@/api/article.js'
import { formatTime } from '@/utils/format.js'
const articleList = ref([]) // 文章列表
const total = ref(0) // 总条数
const loading = ref(false) // loading状态
// 定义请求参数对象
const params = ref({
pagenum: 1, // 当前页
pagesize: 5, // 当前生效的每页条数
cate_id: '',
state: ''
})
// 基于params参数,获取文章列表
const getArticleList = async () => {
loading.value = true
const res = await artGetListService(params.value)
articleList.value = res.data.data
total.value = res.data.total
loading.value = false
}
getArticleList()
// 处理分页逻辑
const onSizeChange = (size) => {
// console.log('当前每页条数', 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 articleEditRef = ref()
// 添加逻辑
const onAddArticle = () => {
articleEditRef.value.open({})
}
// 编辑逻辑
const onEditArticle = (row) => {
articleEditRef.value.open(row)
}
// 删除逻辑
const onDeleteArticle = async (row) => {
// 提示用户是否要删除
await ElMessageBox.confirm('此操作将永久删除该文件, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await artDelService(row.id)
ElMessage.success('删除成功')
// 重新渲染列表
getArticleList()
}
// 添加或者编辑 成功的回调
const onSuccess = (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 v-model="params.cate_id"></channel-select>
<!-- Vue3 => v-model:cid :cid 和 @update:cid 的简写 -->
<!-- <channel-select v-model:cid="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" v-loading="loading">
<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">
<template #default="{ row }">
{{ formatTime(row.pub_date) }}
</template>
</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
plain
type="primary"
:icon="Edit"
@click="onEditArticle(row)"
></el-button>
<el-button
circle
plain
type="danger"
:icon="Delete"
@click="onDeleteArticle(row)"
></el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页区域 -->
<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; justify-content: flex-end"
/>
<!-- 添加编辑的抽屉 -->
<article-edit ref="articleEditRef" @success="onSuccess"></article-edit>
</page-container>
</template>
<style lang="scss" scoped></style>
5.其他知识
- 父组件和子组件width传递给子组件:
css
defineProps({
modelValue: {
type: [Number, String]
},
width: {
type: String
}
})
<el-select
...
:style="{ width }"
>
- 关于el-form表单的搜索和重置按钮为什么只需要重新获取数据就能按照条件获取:后面才发现筛选条件被双向绑定的数据发送给了接口。
- 分页相关知识点:分页可以前端处理,需要计算并且循环渲染数据;当然分页也可以后端处理,后端处理前端只需要重新调用接口数据即可,本项目就是使用后端处理的方式。前端处理还是后端处理就看相关接口是否有页数 和每页渲染数量两个参数
- 文件上传组件el-upload认识:action是文件上传的接口地址、:show-file-list是否多文件上传、:on-success是成功上传钩子、:before-upload是对于上传文件限制钩子、:on-change是文件状态改变包括添加和成功上传和失败上传都会调用的钩子、:auto-upload是控制自动上传属性。如果把action和on-success删除并且:auto-upload='false'表示文件预览
文件预览代码,本项目采用的方式:
xml
<script setup>
import {ref} from 'vue'
// 图片上传相关逻辑
const imgUrl = ref('')
const onSelectFile = (uploadFile) => {
imgUrl.value = URL.createObjectURL(uploadFile.raw) // 预览图片
// 立刻将图片对象,存入 formModel.value.cover_img 将来用于提交
formModel.value.cover_img = uploadFile.raw
}
</script>
<el-form-item label="文章封面" prop="cover_img">
<!-- 此处需要关闭 element-plus 的自动上传,不需要配置 action 等参数
只需要做前端的本地预览图片即可,无需在提交前上传图标
语法:URL.createObjectURL(...) 创建本地预览的地址,来预览
-->
<el-upload
class="avatar-uploader"
:show-file-list="false"
:auto-upload="false"
:on-change="onSelectFile"
>
<img v-if="imgUrl" :src="imgUrl" class="avatar" />
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
</el-form-item>
文件上传代码:
ruby
<el-form-item label="文章封面" prop="cover_img">
<el-upload
class="avatar-uploader"
:show-file-list="false"
action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15"
:on-success="handleAvatarSuccess"
:auto-upload="true"
:on-change="onSelectFile"
>
<img v-if="imgUrl" :src="imgUrl" class="avatar" />
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
</el-form-item>
- 富文本编辑器vue-quill:是一个适配vue3的富文本编辑器,具体使用可查看官网
安装包:pnpm add @vueup/vue-quill@latest
xml
<script setup>
import { QuillEditor } from '@vueup/vue-quill'
import '@vueup/vue-quill/dist/vue-quill.snow.css'
import {ref} from 'vue'
const content = ref('')
</script>
<template>
<div class="editor">
<!-- v-model:content双向绑定content值,contentType='html'表示转化为html格式 -->
<quill-editor
v-model:content='content'
theme="snow"
v-model:content="formModel.content"
contentType="html"
>
</quill-editor>
</div>
</template>
<style scoped>
.editor {
width: 100%;
:deep(.ql-editor) {
min-height: 200px;
}
}
</style>
- formData数据格式可以在MDN了解
- 一般预览图片和富文本编辑器内容在添加新表单内容的时候需要手动重置。vue-quill的设置值需要使用官方提供的API为setHTML(html)置空,如:先使用ref获取到vue-quill的dom对象editorRef,editorRef.value.setHTML('')
- 为表格添加一条数据需要把分页调到最后一页,需要注意这一个逻辑
- 封装接口中有一个get方法也要传参数,使用的是对象形式:request.get(url,{params}),post为request.post(url, data)
- 编辑表单数据可利用open方法做父组件数据回显到子组件中
- 基于axios将网络图片地址转换为file对象方式:可以搜gpt