文章目录
- 零.关于两个相似接口的说明
- 一.文章分类页
- 二.文章管理页的静态布局和动态渲染
- 三.文章管理页的增删改查功能
零.关于两个相似接口的说明
区别 | 文章分类页ArticleChannel | 文章管理页ArticleManage |
---|---|---|
获取数据的接口 | 获取文章分类export const articleGetChannelService = () => {return request.get('/my/cate/list')} | 获取文章管理列表export const articleGetListService = (params) => {return request.get('/my/article/list', { params }) } |
返回参数 | id,cate_name,cate_alias | id,title,pub_date,state,cate_name |
页面调用接口的函数名 | getArticleChannel=async()=>{} | getArticleList=async()=>{} |
在哪些页面调用 | 文章分类页ArticleChannel和下拉菜单子组件ChannelSelect | 文章管理页ArticleManage |
用什么数组去接返回数据 | channelList或channelArr | articleList |
总结:这两个页面的命名非常混乱和随意,如果让我再做一次这个项目,我会做如下优化
- 把第一个二级路由页面叫做
文章分类页ArticleCategory
,不要用channel了,非常不相关 - 把第二个二级路由页面叫做
文章列表页ArticleList
,因为它本质上就是把当前有哪些文章列出来渲染而已,所谓管理就是对这个文章列表的增删改查,但很不直白 - 为避免混淆,分别用命名为
categoryArr和articleArr
的数组去接收返回数据来动态渲染
一.文章分类页
article/ArticleChannel.vue
1.封装pageContainer组件
把页面中头部+主体的页面结构具有通用性,封装到components/PageContainer.vue
组件中,
后续在文章分类,文章管理都能调用
- 封装该组件使用到了element-plus的el-card组件
<template>
<el-card class="page-container">
<template #header>
<div class="header">
<span>文章分类</span>
<div class="extra">
<el-button type="primary">添加分类</el-button>
</div>
</div>
</template>
</el-card>
</template>
<style lang="scss" scoped>
.page-container {
min-height: 100%;
box-sizing: border-box;
.header {
display: flex;
align-items: center;
justify-content: space-between;
}
}
</style>
- 子组件的数据从父组件传过来,用props去接
<script setup>
defineProps({
title: {
required: true,
type: String
}
})
</script>
- 设置默认插槽 default 定制内容主体.并设置具名插槽 extra 定制头部右侧额外的按钮
<el-card class="page-container">
<template #header>
<div class="header">
<!-- **1.定制标题:*父组件传过来的title*** -->
<span>{{ title }}</span>
<div class="extra">
<!-- **3.定制额外按钮:具名插槽** -->
<slot name="extra"></slot>
</div>
</div>
</template>
<!-- ** 2.定制内容:默认插槽** -->
<slot></slot>
</el-card>
2.调用PageContainer组件
- 在文章分类页ArticleChannel调用
<page-container title="文章分类">
<template #extra>
<el-button type="primary"> 添加分类 </el-button>
</template>
主体部分是表格
</page-container>
</template>
- 在文章管理页ArticleManage调用
<template>
<page-container title="文章管理">
<template #extra>
<el-button type="primary">发布文章</el-button>
</template>
主体部分是表格el-table
</page-container>
</template>
3.渲染文章分类页ArticleChannel
封装api接口
//新建api/article.js
import request from "@/utils/request";
// 获取文章分类
export const articleGetChannelService = () => {
return request.get('/my/cate/list')
}
页面调用
import { ref } from 'vue'
import { articleGetChannelService } from '@/api/article'
const channelList = ref([])// 存放文章分类列表的数据
//获取文章分类列表数据
const getArticleChannel = async () => {
const res = await articleGetChannelService()
channelList.value = res.data.data
}
//直接调用
getArticleChannel()
动态渲染+父传子
使用到了Element Plus的el-table
组件,它是用于数据展示的核心表格组件,支持复杂数据表格的创建和交互
如何实现数据绑定:通过:data
属性绑定数组数据源,并自动根据数据量生成对应行数
// 要按需引入icon图标
import { Edit, Delete } from '@element-plus/icons-vue'
// 点击编辑按钮时触发的事件
const onEditChannel = (row, $index) => {
console.log(row, $index)
}
// 点击删除按钮时触发的事件
const onDelChannel = (row) => {
console.log(row)
}
<!-- 主体部分是表格 -->
<el-table :data="channelList" style="width: 100%">
<!-- 序号来自--type="index" -->
<el-table-column label="序号" width="100" type="index"> </el-table-column>
<!-- prop:去data对象中找对应的属性名 -->
<el-table-column label="分类名称" prop="cate_name"></el-table-column>
<el-table-column label="分类别名" prop="cate_alias"></el-table-column>
<el-table-column label="操作" width="100">
<!-- 自定义列:写成作用域插槽 -->
<!-- row就是ChannelList的每一项(相当于遍历数组时的item),$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)"></el-button>
</template>
</el-table-column>
<!-- 优化:没有数据返回时的页面渲染 -->
<template #empty>
<el-empty description="没有数据" />
</template>
</el-table>
此处为了有数据可以渲染,应该去在线演示链接中给自己的账户添加数据,
优化:添加loading效果
// 声明变量,控制loading状态
const loading = ref(false)
//获取文章分类列表数据
const getArticleChannel = async () => {
// 一进入页面时,先显示loading状态
loading.value = true
// 调用接口获取数据,并赋值给channelList
const res = await articleGetChannelService()
// 数据获取成功后,隐藏loading状态
loading.value = false
channelList.value = res.data.data
}
<!-- 主体部分是表格 -->
<el-table v-loading="loading" :data="channelList" style="width: 100%">
.....
</el-table>
优化:无数据返回时的页面渲染
使用到了element-plus的空状态Emtpy组件和#emtpy插槽
<!-- 优化:没有数据返回时的页面渲染 -->
<template #empty>
<el-empty description="没有数据" />
</template>
4.添加弹层并显示弹层组件
使用到了el-dialog
组件作为用户点击"编辑"或"添加"
时,跳出来的弹层
由于具有复用性,应考虑封装成一个组件,然后分别在两个点击事件中控制其显隐
封装ChannelEdit.vue
组件
//article/components/ChannelEdit.vue==>注意不是全局的components文件夹
<script setup>
import { ref } from 'vue'
const dialogVisible = ref(false)
//组件对外暴露一个方法open:并基于传来的参数来区分是编辑还是添加
const open = async (row) => {
dialogVisible.value = true//open需要有打开弹层的功能
console.log("编辑还是添加:", row)
}
//将来调用open的时候,如果没有传参,说明是添加,传参说明是编辑
//open({})==>添加:无需渲染
//open({id,cate_name,...})==>编辑,需要渲染
defineExpose({//对外暴露方法
open
})
</script>
<template>
<el-dialog v-model="dialogVisible" title="添加弹层" width="30%">
<div>渲染表单</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary"> 确认 </el-button>
</span>
</template>
</el-dialog>
</template>
在文章分类页ArticleChannel
调用ChannelEdit.vue
组件
- 绑定"添加分类按钮"
<template #extra>
<el-button type="primary" @click='onAddChannel'> 添加分类 </el-button>
</template>
- 引入和调用
ChannelEdit.vue
组件
// 引入弹层子组件
import ChannelEdit from '@/views/article/components/ChannelEdit.vue'
......
<!-- 优化:没有数据返回时的页面渲染 -->
<template #empty>
<el-empty description="没有数据" />
</template>
<!-- 弹层组件 -->
<ChannelEdit ref="dialog" />
- 模板引用:使用ref标识获得组件实例对象
//模板引用子组件,并通过ref属性获取到子组件实例赋给dialog
const dialog = ref(null)
// 点击"添加分类"按钮时触发的事件
const onAddChannel = () => {
dialog.value.open({})//点击添加按钮,调用open但不传参
}
// 点击编辑按钮时触发的事件
const onEditChannel = (row, $index) => {
console.log(row, $index)
dialog.value.open(row)//点击编辑按钮,调用open并传参
}
// 点击删除按钮时触发的事件
const onDelChannel = (row) => {
console.log(row)
}
5.添加和编辑功能的实现
回顾:为el-form表单添加校验规则的四个步骤
先准备一个rulerForm对象,代表整个的用于提交的form数据对象
const formModel=ref({})
再准备校验规则rules
const rules={
username:[//写成数组,添加几条规则都可以
{required:true,message:'请输入用户名',trigger:'blur'}//失焦时校验,也可以尝试改成change
]
}
绑定el-form中的:model和:rules
<el-form
:model='formModel'
:rules='rules'
ref="form" size="large" autocomplete="off" v-if="isRegister"></el-form>
在与username相关的el-input中v-model,并配置prop以示生效的是哪条规则
<el-form-item prop="username">
表单元素input--配置图标prefix-icon
<el-input v-model="formModel.username" :prefix-icon="User" placeholder="请输入用户名"></el-input>
</el-form-item>
为弹层(ChannelEdit
子组件)的表单添加校验规则
//article/components/ChannelEdit.vue==>注意不是全局的components文件夹
<script setup>
import { ref } from 'vue'
const dialogVisible = ref(false)
//组件对外暴露一个方法open:并基于传来的参数来区分是编辑还是添加
const open = async (row) => {
dialogVisible.value = true//open需要有打开弹层的功能
console.log("编辑还是添加:", row)
}
//将来调用open的时候,如果没有传参,说明是添加,传参说明是编辑
//open({})==>添加:无需渲染
//open({id,cate_name,...})==>编辑,需要渲染
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'
}
]
}
defineExpose({//对外暴露方法
open
})
</script>
<template>
<el-dialog v-model="dialogVisible" title="添加弹层" width="30%">
<div>渲染表单</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary"> 确认 </el-button>
</span>
</template>
</el-dialog>
<el-form :model="formModel" :rules="rules" label-width="100px" style="padding-right: 30px">
<el-form-item label="分类名称" prop="cate_name">
<el-input v-model="formModel.cate_name" minlength="1" maxlength="10"></el-input>
</el-form-item>
<el-form-item label="分类别名" prop="cate_alias">
<el-input v-model="formModel.cate_alias" minlength="1" maxlength="15"></el-input>
</el-form-item>
</el-form>
</template>
优化:实现编辑回显功能
编辑回显是指将已存在的数据展示在编辑界面中,让用户可以查看和修改原有数据的操作流程。
其核心流程为:获取数据 → 填充表单 → 修改提交
//ChannelEdit.vue
//组件对外暴露一个方法open:并基于传来的参数来区分是编辑还是添加
const open = async (row) => {
dialogVisible.value = true//open需要有打开弹层的功能
console.log("编辑还是添加:", row)
// 编辑回显
formModel.value = {
...row//是"添加"就重置,是"编辑"就存回显数据
}
}
优化:动态绑定标题内容
//ChannelEdit.vue
<el-dialog v-model="dialogVisible" title="添加弹层" width="30%">
改为:
<el-dialog v-model="dialogVisible" :title="formModel.id?'编辑分类':'添加分类'" width="30%">
6.提交功能的实现
不论是添加还是编辑,最后都要提交,主要是弹层中的确认按钮的功能实现
封装api接口
- 增加文章
- 更新文章
//api/article.js
// 添加文章分类
export const articleAddChannelService = (data) => {
return request.post('/my/cate/add', data)
}
// 编辑文章分类
export const articleEditChannelService = (data) => {
return request.put('/my/cate/info', data)
}
页面调用:在弹层ChannelEdit中调用
- 表单绑定ref标识获取DOM属性,并在添加按钮中绑定点击事件
const formRef = ref(null)//表单ref,用于校验
import { ref } from 'vue'
const dialogVisible = ref(false)
......
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click='onSubmit'> 确认 </el-button>
.....
<el-form ref='formRef' :model="formModel" :rules="rules" label-width="100px" style="padding-right: 30px">
...
</el-form>
- 提交前校验
import { articleEditChannelService, articleAddChannelService } from '@/api/article'
//表单提交事件
const onSubmit = async () => {
//直接校验,通过才继续往下
await formRef.value.validate()
//区分是编辑还是添加
formModel.value.id ? await articleEditChannelService(formModel.value) : await articleAddChannelService(formModel.value)
ElMessage({
type: 'success',
message: formModel.value.id ? '编辑成功' : '添加成功',
duration: 1000
})
dialogVisible.value = false//关闭弹层
emit('success')//通知父组件刷新列表
}
- 子传父:通知父组件刷新列表
//子:ChannelEdit
import { defineEmits } from 'vue'
const emit = defineEmits(['success'])
...
emit('success')//通知父组件刷新列表
//父:ArticleChannel
// 编辑成功后刷新列表数据
const onSuccess = () => {
getArticleChannel()//重新调用:获取文章分类
}
<!-- 弹层组件 -->
<ChannelEdit @click='onSuccess' ref="dialog" />
7.删除功能的实现
封装api接口
// 删除文章分类
export const articleDeleteChannelService = (id) => {
return request.delete('/my/cate/del', {
params: {
id
}
})
}
页面调用
import { articleGetChannelService, articleDeleteChannelService } from '@/api/article'
const onDelChannel = async (row) => {
// 弹窗确认提示框
await ElMessageBox.confirm('你确认删除该分类信息吗?', '温馨提示', {
type: 'warning',
confirmButtonText: '确认',
cancelButtonText: '取消'
})
// 调用删除接口,传入要删除的分类id
await articleDeleteChannelService(row.id)
ElMessage({ type: 'success', message: '删除成功' })
getArticleChannel()//重新调用:获取文章分类
}
<!-- 删除按钮的图标 -->
<el-button :icon="Delete" circle plain type="danger" @click="onDelChannel(row)"></el-button>
二.文章管理页的静态布局和动态渲染
文章管理页ArticleManage.vue
1.静态页面布局
表单部分
<!-- 表单区域,其中label是展示给用户看的,value是提供给后台的,值通常是id -->
<el-form inline>
<!-- 分成三个el-form-item,分别是文章分类的下拉菜单,发布状态的下拉菜单和按钮区 -->
<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>
表格部分
- 准备模拟数据
//按需引入编辑和删除的图标组件
import { Delete, Edit } from '@element-plus/icons-vue'
import { ref } from '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: '体育'
}
])
- 表格静态结构+用模拟数据渲染表格
<!-- 表格区域:文章列表管理 -->
<!-- 使用到了el-table的data属性绑定表格数据,el-table-column是列的定义,prop是绑定的字段名 --> -->
<el-table :data="articleList" style="width: 100%">
<!-- 1.文章标题 -->
<el-table-column label="文章标题" width="400">
<!-- 作用域插槽--解构出row(一行的数据) -->
<template #default="{ row }">
<!-- el-link小组件:相当于a标签 -->
<el-link type="primary" :underline="false">{{ row.title }}</el-link>
</template>
</el-table-column>
<!-- 2.分类,3.发表时间.4.状态 -->
<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>
<!-- 5.操作:两个按钮添加点击事件 -->
<el-table-column label="操作" width="100">
<!-- 作用域插槽--解构出row -->
<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>
- 绑定"编辑"和"删除"图标的点击事件
//5.操作
const onEditArticle = (row) => {
console.log(row)
}
const onDeleteArticle = (row) => {
console.log(row)
}
分页部分
使用到了element-plus的el-pagination组件
<el-pagination />
优化:下拉菜单默认语言设为中文
当前的下拉菜单默认显示信息是英文的,若改成中文,应该如何处理?
处理方法-----官网位置:配置组件>Config Provider全局配置>i18n配置--配置多语言
//在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>
<style scoped></style>
2."文章分类"的下拉菜单:封装成组件并动态渲染
该下拉菜单在后续的抽屉中也用到,故封装成一个通用组件,其数据都来自"获取文章列表"接口,
直接调用该接口即可,不需要在不同的父组件中调用时父传子
2.1.封装并引入新建的ChannelSelect
组件
//新建components/ChannelSelect.vue组件
<template>
<el-select style="width: 240px">
<el-option label="新闻" value="111"></el-option>
<el-option label="体育" value="222"></el-option>
</el-select>
</template>
// ChannelManage.vue中引入
import ChannelSelect from '@/components/ChannelSelect.vue'
<el-form-item label="文章分类:">
<!-- <el-select style="width: 240px">
<el-option label="新闻" value="111"></el-option>
<el-option label="体育" value="222"></el-option>
</el-select> -->
<channel-select></channel-select>
</el-form-item>
2.2.调用"获取文章分类"接口并动态渲染下拉菜单
<script setup>
import { articleGetChannelService } from '@/api/article';
import { ref } from 'vue';
const channelArr= ref([]);
//获取文章分类
const getArticleChannel = async () => {
const res = await articleGetChannelService();
channelArr.value = res.data.data;//用channelArr动态渲染
console.log("res:", res.data.data);//渲染下拉框数据:id,cate_name,cate_alias
};
getArticleChannel();
</script>
<template>
<el-select style="width: 240px">
<!-- <el-option label="新闻" value="111"></el-option>
<el-option label="体育" value="222"></el-option> -->
<el-option v-for="channel in channelArr" :key="channel.id" :label="channel.cate_name"
:value="channel.id"></el-option>
</el-select>
</template>
2.3.父子通信:动态绑定el-select
功能需求:用户选择了下拉菜单中的某一项后,el-select选中并显示
- 父组件
// 定义请求参数对象
//const cateId = ref('45007');//效果:默认选中体育作为下拉菜单的默认选中项
<!-- <channel-select v-model="cateId"></channel-select> -->
/*后续会有其他类型的数据,因此做法是把这些数据放到一个对象中进行维护(根据接口"获取-文章列表"中的参数有:pagenum,pagesize,cate_id,state)
优化如下:
*/
//定义请求参数对象
const params=ref({
pagenum:1,
pagesize:5,
cate_id:'',
state:''//状态为空,表示未选中
})
<channel-select v-model="params.cate_id"></channel-select>
- 子组件
<script setup>
import { articleGetChannelService } from '@/api/article';
import { ref } from 'vue';
const channelList = ref([]);
// 获取文章列表
const getArticleChannel = async () => {
const res = await articleGetChannelService();
channelList.value = res.data.data;
console.log("res:", res.data.data);//用channellist渲染下拉框数据
};
getArticleChannel();
// 父传子
defineProps({
modelValue: {
type: [Number, String]//id可以是数字也可以是字符串
}
})
// 子传父:把触发事件得到的值更新给父组件的modelValue
const emit = defineEmits(['update:modelValue'])
</script>
<template>
<!-- 双向绑定el-select,把v-model拆解成:modelValue属性和@update:modelValue事件 -->
<el-select :modelValue="modelValue" @update:modelValue="emit('update:modelValue', $event)" style="width: 240px">
<!-- 使用channelList渲染下拉框数据 -->
<el-option v-for="channel in channelList" :key="channel.id" :label="channel.cate_name"
:value="channel.id"></el-option>
</el-select>
</template>
3."发布状态"的下拉菜单:直接写死
发布状态下拉菜单并不需要动态绑定
<el-select v-model="params.state">
<el-option label="已发布" value="已发布"></el-option>
<el-option label="草稿" value="草稿"></el-option>
</el-select>
4.Vue3中对v-model的拆分有哪些优点?
为什么说Vue3中对v-model的拆分成modelValue和@update.modelValue,是让其更方便使用了?
上述代码中,
<el-select v-model="params.state">
<el-option label="已发布" value="已发布"></el-option>
<el-option label="草稿" value="草稿"></el-option>
</el-select>
等价于
<el-select v-model.modelValue="params.state">
<el-option label="已发布" value="已发布"></el-option>
<el-option label="草稿" value="草稿"></el-option>
</el-select>
实际上把.modelValue
给省略了
这说明:
Vue3中,把.sync
语法和:value
做了合并
上例中,可以不写成.modelValue,而写成其他属性名,也可以
若写成v-model:cateId,此时它就是:cateId和update.cateId的简写
当然子组件也需要做相应地改写:
父传子:接收父组件传过来的id
defineProps({
cateId:{
type:[Number,String]//id可以是数字或字符串
}
})
//子传父:将触发事件得到的值更新给父组件
const emit=defineEmits(['undate:cateId'],$event)
<el-select//二次封装
:modelValue="cateId"//拆解成:modelValue
@update:modelValue="emit('update:cateId', $event)"//和@update.modelValue
>
......
</el-select>
可以看到除了 :modelValue=和@update:modelValue,都改成了cid
显然,cateId是父传子时,父组件的自定义属性名,随便改
至于为什么非要写成modelValue,因为在父组件中,此时可以省略:
<el-select v-model.modelValue="params.state">
<! --后面的.modelValue可以不写 -->
<el-select v-model"params.state">
5.动态渲染文章列表(el-table)
封装api接口
/******文章管理接口***** */
// 获取文章列表
export const articleGetListService = (params) => {
return request.get('/my/article/list', { params })
}
页面调用
//把之前articleList的模拟数据删掉
const articleList = ref([])
//引入文章管理列表的请求方法
import { articleGetListService } from '@/api/article'
//获取文章管理列表的方法
//这个接口获得的数据是:{id: 66555, title: '比亚迪', pub_date: 'Tue May 27 xxxx)', state: '已发布', cate_name: '国产汽车'}
const getArticleList = async () => {
const res = await articleGetListService(params.value)
console.log("输出文章管理列表的res:", res.data.data)
// total.value = res.data.data.totalcount
articleList.value = res.data.data
}
getArticleList()//调用获取文章列表的方法
动态渲染(取代表格中的模拟数据)
不用改,之前用模拟数据的时候已经布置完毕
<el-table :data="articleList" style="width: 100%">
......
<el-table>
优化:下载插件+封装日期代码+对日期格式格式化
此时日期格式是乱的,需要对日期进行格式化
使用到了element-plus的内置小插件:dayjs
由于日期也具有通用性,考虑把它封装成工具函数
- 新建
utils/format.js
import { dayjs } from 'element-plus'
export const formatTime = (time) => dayjs(time).format('YYYY年MM月DD日')
- 导入并使用
import { formatTime } from '@/utils/format'
<el-table-column label="发表时间" prop="pub_date">
<template #default="{ row }">
{{ formatTime(row.pub_date) }}
</template>
</el-table-column>
6.分页的实现
使用到了element-plus的el-pagination组件,
相关属性的说明
v-model:current-page="params.pagenum"//当前页
v-model:page-size="params.pagesize"//每页条数
:page-sizes="[2, 3, 5, 10]"//可供用户选择的每页条数
layout="jumper, total, sizes, prev, pager, next"//控制工具栏:显示哪些工具
background='true'//添加背景色
:total="total"//动态绑定已有属性total
@size-change="onSizeChange"//每页条数变化时触发
@current-change="onCurrentChange"//当前页变化时触发
style="margin-top: 20px; justify-content: flex-end"//添加css样式,默认flex布局
总条数和两个触发函数
代码有重复,只看更新部分,比如total和两个函数
const total = ref(0)//总条数
//定义请求参数对象
const params = ref({
pagenum: 1,
pagesize: 5,
cate_id: '',
state: ''//状态为空,表示未选中
})
import { articleGetListService } from '@/api/article'
const getArticleList = async () => {
const res = await articleGetListService(params.value)
console.log("输出文章管理列表的res:", res.data.data)
total.value = res.data.total//总条数
articleList.value = res.data.data
console.log("total = ", res.data.total);
}
getArticleList()//调用获取文章列表的方法
// 每页条数改变时触发
const onSizeChange = (size) => {
params.value.pagenum = 1// 重置页码为1
params.value.pagesize = size// 更新每页显示的数量为传入的size
getArticleList()//调用获取文章管理列表的方法
}
// 当前页码改变时触发
const onCurrentChange = (page) => {
params.value.pagenum = page// 更新页码为传入的page
getArticleList()//调用获取文章管理列表的方法
}
<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" />
7.优化:为el-table添加loading效果
//声明变量控制是否loading
const loading = ref(false)
//发送请求时添加loading效果,并在请求结束时关闭loading
const getArticleList = async () => {
loading.value = true
...
loading.value = false
}
getArticleList()
//在el-table中直接绑定v-loading效果
<el-table v-loading="loading" > ... </el-table>
三.文章管理页的增删改查功能
1.搜索和重置功能的实现
搜索的逻辑就是按照最新的条件重新检索,因此:
- 当条件变更,就从第一页开始展示
- 重新渲染页面
- 别的不需要做,因为表单和表格之间的数据已经绑定好了
重置的逻辑就是将筛选条件清零,然后重新渲染
<el-form-item>
<el-button type="primary" @click="onSearch">搜索</el-button>
<el-button @click="onReset">重置</el-button>
</el-form-item>
// 搜索和重置的逻辑
const onSearch = () => {
params.value.pagenum = 1// 重置页码为1
getArticleList()//调用获取文章管理列表的方法
}
const onReset = () => {
params.value.cate_id = ''// 清空分类的选中项
params.value.state = ''// 清空发布状态的选中项
onSearch()//调用搜索方法==>或者把搜索功能那两句代码写进来
}
2.添加文章的实现
实现添加文章的功能需要分为三个部分
- 封装一个抽屉组件,可以视为进化版本的
el-dialog
,用户点击"添加文章"后,会在弹出的抽屉中输入新文章的标题,图片和正文等信息(该抽屉组件在编辑功能时也能调用) - 上传文件功能的实现:上传图片
- 文章正文使用到了富文本功能
2.1.引入和封装抽屉组件
使用到了el-drawer
抽屉,它是el-dialog
的更丰富版本,两者有着几乎相同的api
由于添加文章和编辑文章复用该组件(ArticleEdit.vue
),因此有需求如下:
- 向外暴露open方法:区分是在"添加"还是在"编辑"
- 添加或编辑成功后要触发ElMessage.success
- 添加或编辑成功后要重新渲染页面
封装成子组件并导入
- 新建
article/components/ArticleEdit.vue
<script setup>
import { ref } from 'vue'
const visibleDrawer = ref(false)//抽屉的显示隐藏状态
const open = (row) => {//根据传过来的值是空对象(添加)还是带参数对象(编辑)
visibleDrawer.value = true//打开抽屉
console.log(row)//接收
}
defineExpose({//对外暴露open方法
open
})
</script>
<template>
<!-- 抽屉:内容稍后再填充 -->
<el-drawer v-model="visibleDrawer" title="大标题" direction="rtl" size="50%">
<span>Hi there!</span>
</el-drawer>
</template>
- 在
ArticleManage
页中导入抽屉并绑定点击事件
//引入抽屉组件
import ArticleEdit from "@/views/article/components/ArticleEdit.vue"
//引用模板:ref标识获取组件实例
const articleEditRef = ref(null)
// 添加文章和编辑文章的逻辑
const onAddArticle = () => {
articleEditRef.value.open()//调用子组件的open方法
}
const onEditArticle = (row) => {//已存在
articleEditRef.value.open(row)//调用子组件的open方法,并传入当前行的数据
}
<page-container title="文章管理">
<template #extra>
<!-- <el-button type="primary">发布文章</el-button> -->
<el-button type="primary" @click='onAddArticle'>添加文章</el-button>
</template>
......
<!-- 绑定事件:编辑和删除文章--使用row.id作为传参 -->
<el-button :icon="Edit" circle plain type="primary" @click="onEditArticle(row)"></el-button>
......
<!-- 抽屉区域 -->
<article-edit ref='articleEditRef'></article-edit>
填充抽屉子组件ArticleEdit
的页面布局
结构分为四个部分:表单(文章标题),下拉菜单(文章分类),文章封面(添加图片),文章内容(富文本编辑器)
- 准备模拟数据(根据接口文档明确需要收集的参数)
//定义默认数据
const defaultForm = ref({
title: '',//标题
cate_id: '',//分类id
cover_img: '',//封面图片 file对象
content: '',//string内容
state: ''//状态
})
//准备模拟数据
const formModel = ref({
...defaultForm.value//踩坑:别忘了.value
})
//open:编辑的时候需要回显填充,添加时不需要
const open = async (row) => {
visibleDrawer.value = true//打开抽屉
// 由于获取到的数据没有cover_img和content,不能直接回显
// 因此需要做判断如下
/*需要基于row.id发送请求,获取编辑对应的详情数据进行回显*/
if (row.id) {//有id,是编辑状态
console.log('编辑回显,要发请求')
} else {
console.log('添加功能,要重置抽屉,重置表单数据')
formModel.value = {//基于默认的数据,重置抽屉中的表单数据
...defaultForm
}
}
}
//open:编辑的时候需要回显填充,添加时不需要
const open = async (row) => {
visibleDrawer.value = true//打开抽屉
// 由于获取到的数据没有cover_img和content,不能直接回显
// 因此需要做判断如下
/*需要基于row.id发送请求,获取编辑对应的详情数据进行回显*/
if (row.id) {//有id,是编辑状态
console.log('编辑回显,要发请求')
} else {
console.log('添加功能,要重置抽屉,重置表单数据')
formModel.value = {//基于默认的数据,重置抽屉中的表单数据
...defaultForm
}
}
}
- 抽屉的静态结构:准备表单结构
(在"文章分类"的下拉菜单中引入之前封装好的ChannelSelect
组件)
<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,用于获取文章分类的下拉菜单 -->
<channel-select v-model="formModel.cate_id" width="100%"></channel-select>
</el-form-item>
<el-form-item label="文章封面" prop="cover_img"> 文件上传 </el-form-item>
<el-form-item label="文章内容" prop="content">
<div class="editor">富文本编辑器</div>
</el-form-item>
<el-form-item>
<el-button type="primary">发布</el-button>
<el-button type="info">草稿</el-button>
</el-form-item>
</el-form>
</el-drawer>
2.2.优化:如何在引入的子组件中设置样式?
上例中,试图对导入组件设置宽度但不成功
<channel-select
v-model="formModel.cate_id"
width="100%"//这个为啥没有生效?
></channel-select>
- 原因:封装成组件之后,在上面加的所有东西都变成属性
(此时相当于添加了个值是100%的width属性,这并没起到什么作用)
- 解决方法:配置逻辑,以使得属性生效
//ChannelSelect.vue
//为defineProps拓展一个width属性
defineProps({
cateId: {
type: [Number, String]
},
width: {
type: String
}
//使用配置的width:
<el-select :style="{width}">
.....
</el-select>
2.3.上传文件功能的实现
一般上传功能有两种情况:
1.上传文件和提交按钮是分离的,上传的时候已经上传完成
2.点击提交时才和其他信息一起上传
此处选择第二种上传方式,通过formData
统一提交上传
- 查看"发布-文章"接口,确认
cover_img
的属性值类型
- 查看element-plus官网是如何支持图片上传的
官网位置:upload上传>用户头像
- 将代码粘贴到抽屉组件中的上传文件位置
注意事项:1.关闭自动提交(删action
参数,且auto-upload='false'
);2.实现本地预览(语法:URL.createObjectURL(...)
)
//导入"+"号图标
import { Plus } from '@element-plus/icons-vue'
const imgUrl = ref('')//图片的地址,用于本地预览
//使用官网上提供的onchange钩子来监听选择文件:状态改变时触发这个方法,并在传参中拿到选择的项
const onUploadFile = (uploadFile) => {
//实现图片的本地预览--语法:URL.createObjectURL(...),得到一个地址,赋给imgUrl
imgUrl.value = URL.createObjectURL(uploadFile.raw)
formModel.value.cover_img = uploadFile.raw
console.log("uploadFile:", uploadFile.raw)
}
<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>
- 优化:修改预览图片的尺寸
<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;
}
}
}
</style>
2.3.文章内容功能的实现:富文本编辑器
2.3.1.下载和引入富文本编辑器
- 官网地址:https://vueup.github.io/vue-quill/
- 安装命令:
pnpm add @vueup/vue-quill@latest
- 在抽屉组件中局部导入:
//ArticleEdit.vue
//导包
import { QuillEditor } from '@vueup/vue-quill'
import '@vueup/vue-quill/dist/vue-quill.snow.css'
export default{
components:{
QuillEditor
}
}
2.3.2.在页面中使用
位置:文章内容部分
<el-form-item label="文章内容" prop="content">
<div class="editor">
<!-- themse:snow ----添加主题样式,还需绑定数据和设置宽高样式 -->
<!-- v-model:content="formModel.content"----双向绑定 -->
<!-- contentType="html"--指定内容格式类型 -->
<quill-editor theme="snow" v-model:content="formModel.content" contentType="html"></quill-editor>
</div>
</el-form-item>
.editor {
width: 100%;
:deep(.ql-editor) {
min-height: 200px;
}
}
2.4.实现添加文章的功能
封装接口
// 发布文章(添加文章)
export const articlePublishService = (data) => {//注意:data需要是一个formData格式的对象
return request.post('/my/article/add', data)
}
页面调用
<!-- 注册点击事件:onPublish,发布和草稿的区别在于状态state的不同 -->
<el-form-item>
<el-button @click="onPublish('已发布')" type="primary">发布</el-button>
<el-button @click="onPublish('草稿')" type="info">草稿</el-button>
</el-form-item>
import { articlePublishService } from '@/api/article'
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 articlePublishService(fd)//添加操作:调用封装的添加接口
ElMessage.success('添加成功')//提示信息
visibleDrawer.value = false//关掉抽屉
/*此时数据成功显示为页面的最后一条(翻页去看)*/
emit('success', 'add')//通知父组件ArticleManage.vue添加成功了,额外传参add为了与编辑做区分
}
}
注册成功时触发点击事件:绑定在抽屉组件占位符
//ArticleManage.vue
<article-edit ref="articleEditRef" @success="onSuccess"></article-edit>
// 添加修改成功
const onSuccess = (type) => {
if (type === 'add') {
// 如果是添加,需要跳转渲染最后一页,编辑直接渲染当前页
//如何知道最后一页是哪一页?total总条数
const lastPage = Math.ceil((total.value + 1) / params.value.pagesize)//最后一页=当前总共total条数据/每页pagesize条数据(为啥要加1?添加操作,后台新增1条但还没同步,手动+1以保证准确性)
params.value.pagenum = lastPage
}
getArticleList()//重新渲染页面
}
优化:手动重置"文章封面"和"文章正文"
此时成功添加文章后,只有"文章标题"和"文章分类"自动重置
思路:如何重置封面和内容—一打开抽屉组件就先重置
//ArticleEdit.vue
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('')//setHTML(html)是vue-quill的方法,作用是设置富文本编辑器的内容
}
}
3.编辑文章的实现
编辑功能只需在添加功能的基础上对两个地方做修改即可实现
#### 1.open方法中,编辑的回显
const open = async (row) => {
visibleDrawer.value = true
if (row.id) {
console.log('编辑回显')
} else {
.....
}
}
#### 2.编辑提交:提交时发送编辑请求
if (formModel.value.id) {//编辑操作
console.log('编辑操作')//先留坑,后续补上此功能
} else {// 添加操作
......
}
3.1.实现编辑的回显
如果是编辑操作,一打开抽屉,就需要发送请求,获取数据进行回显
3.1.1.文章标题,文章分类和文章正文的回显
- 封装接口
// 获取文章详情
export const articleGetDetailService = (id) => {
return request.get('/my/article/info', {
params: {
id
}
})
}
- 页面调用
import { articlePublishService, articleGetDetailService } from '@/api/article'
const open = async (row) => {
visibleDrawer.value = true//打开抽屉
if (row) {//有id,是编辑状态
console.log('编辑回显,要发请求')
/**************在此处实现编辑回显************/
const res= await articleGetDetailService(row.id)
formModel.value = res.data.data//获得数据后,赋值给formModel做回显
} else {
.....
}
}
此时点击编辑按钮后,除了文章封面,其他部分能成功回显
3.1.2.文章封面的回显
- 导入基地址进行拼接
//utils/request.js
//1.配置基地址
const baseURL = 'http://big-event-vue-api-t.itheima.net'
export { baseURL }//导出基地址
//抽屉组件article/components/ArticleEdit.vue
// 导入基地址
import { baseURL } from "@/utils/request"
const imgUrl = ref('')//图片的地址,用于本地预览
//open方法编辑回显部分:
if (row) {//有id,是编辑状态
const res = await articleGetDetailService(row.id)
formModel.value = res.data.data//获得数据后,赋值给formModel做回显
// 文章封面的回显:拼接imgUrl
imgUrl.value = baseURL + formModel.value.cover_img
} else {}
成功实现封面图片的回显,至此,编辑回显的功能已完成
3.2.提交请求之前的处理:借助AI实现把cover_img的格式从网络地址转换成file对象的业务功能
在接下来要实现的编辑提交功能中使用到了"更新-文章详情"接口,但是其返回的属性中cover_img
要求是file对象,而当前在open编辑功能中获得的res.data.data中,cover_img
是URL网络地址
需求:要把cover_img
的格式从网络地址转换成file对象
- deepseek搜索语句如下:
封装一个函数,基于 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;
}
}
- 粘贴并调用方法
//ArticleEdit.vue
// 引入axios
import axios from 'axios'//不要加花括号会报错
// 将网络图片地址转换为File对象
async function imageUrlToFile(url, fileName) {
......
}
//open方法编辑回显部分:
if (row) {//有id,是编辑状态
const res = await articleGetDetailService(row.id)
formModel.value = res.data.data//获得数据后,赋值给formModel做回显
// 文章封面的回显:拼接imgUrl
imgUrl.value = baseURL + formModel.value.cover_img
// 调用方法:将imgUrl的格式从网络地址转化为file对象
// 调用方法:将imgUrl的格式从网络地址转化为file对象
const file=imageUrlToFile(imgUrl.value, formModel.value.cover_img)//形参分别是:Url地址,文件名
formModel.value.cover_img = file//将file对象赋值给cover_img
} else {}
3.3.实现编辑的提交
- 封装接口(请求方式,路径,参数等见上面3.2的截图)
//api/article.js
//更新文章详情:编辑提交
export const articleEditService = (data) => {
return request.put('/my/article/info', data)
}
- 页面调用
//ArticleEdit.vue
const onPublish=async (state) =>{
......
if (formModel.value.id) {//编辑操作
console.log('编辑操作')//先留坑,后续补上此功能
await articleEditService(fd)//编辑操作:调用封装的编辑接口
ElMessage.success('编辑成功')//提示信息
visibleDrawer.value = false//关掉抽屉
emit('success', 'edit')//通知父组件ArticleManage.vue添加成功了,额外传参add为了与编辑做区分
} else {// 添加操作
......
}
}
4.删除文章的实现
- 封装接口
//删除文章
export const articleDeleteService = (id) => {
return request.delete('/my/article/info', {
params: {
id
}
})
}
- 页面调用
//ArticleManage.vue
import { articleDeleteService } from '@/api/article'
// 删除文章
const onDeleteArticle = async (row) => {
await ElMessageBox.confirm('你确认删除该文章信息吗?', '温馨提示', {
type: 'warning',
confirmButtonText: '确认',
cancelButtonText: '取消'
})
await articleDeleteService(row.id)
ElMessage({ type: 'success', message: '删除成功' })
getArticleList()
}