文章目录
- 一.文章分类页
- 1.封装pageContainer组件
- 2.调用PageContainer组件
- 3.渲染文章分类页ArticleChannel
- 封装api接口
- 页面调用
- 动态渲染+父传子
- 优化:添加loading效果
- 优化:无数据返回时的页面渲染
- 4.添加弹层并显示弹层组件
- 封装`ChannelEdit.vue`组件
- 在文章列表页`ArticleChannel`调用`ChannelEdit.vue`组件
- 5.添加和编辑功能的实现
- 回顾:为el-form表单添加校验规则的四个步骤
- 为弹层(`ChannelEdit`子组件)的表单添加校验规则
- 优化:实现编辑回显功能
- 优化:动态绑定标题内容
- 6.提交功能的实现
- 封装api接口
- 页面调用:在弹层ChannelEdit中调用
- 7.删除功能的实现
- 封装api接口
- 页面调用
- 二.文章管理页
- 1.静态页面布局
- 表单部分
- 表格部分
- 分页部分
- 优化:下拉菜单默认语言设为中文
- 2."文章分类"的下拉菜单:封装成组件并动态渲染
- 2.1.封装并引入新建的`ChannelSelect`组件
- 2.1.调用"获取文章列表"接口并动态渲染下拉菜单
- 2.3.父子通信:动态绑定el-select
- 3."发布状态"的下拉菜单:直接写死
- 4.Vue3中对v-model的拆分有哪些优点?
- 5.动态渲染文章列表
- 封装api接口
- 页面调用
- 动态渲染(取代表格中的模拟数据)
- 优化:下载插件+封装日期代码+对日期格式格式化
- 6.分页功能的实现
- Pagination组件的使用
- 两个触发函数
- 7.loading功能的实现
- 8.搜索和重置功能的实现
- 三.添加文章的实现
- 1.引入和封装抽屉组件
- 从element-plus中引入抽屉
- 封装成子组件并导入
- 填充抽屉子组件的页面布局
- 2.上传文件功能的实现
- 使用URL.createObjectURL(...))实现上传
- 修改预览图片的尺寸
- 3.文章内容功能的实现:富文本编辑器
- 3.1.下载和引入富文本编辑器
- 3.2.实现添加文章的功能
- 四.编辑文章的实现
- 1.实现编辑的回显
- 2.借助AI实现功能:把cover_img的格式从网络地址转换成file对象
- 3.实现编辑的提交
- 五.删除文章的实现
- 六.个人中心页
一.文章分类页
article/ArticleManage.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.定制标题:*父传子*** --><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 getChannelList = async () => {const res = await articleGetChannelService()channelList.value = res.data.data
}
//直接调用
getChannelList()
动态渲染+父传子
使用到了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 getChannelList = async () => {// 一进入页面时,先显示loading状态loading.value = true// 调用接口获取数据,并赋值给channelListconst res = await articleGetChannelService()// 数据获取成功后,隐藏loading状态loading.value = falsechannelList.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 = () => {getChannelList()//重新调用:获取文章分类
}
<!-- 弹层组件 -->
<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: '取消'})// 调用删除接口,传入要删除的分类idawait articleDeleteChannelService(row.id)ElMessage({ type: 'success', message: '删除成功' })getChannelList()//重新调用:获取文章分类
}<!-- 删除按钮的图标 -->
<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.1.调用"获取文章列表"接口并动态渲染下拉菜单
<script setup>
import { articleGetChannelService } from '@/api/article';
import { ref } from 'vue';
const channelList = ref([]);
const getChannelList = async () => {const res = await articleGetChannelService();channelList.value = res.data.data;console.log("res:", res.data.data);//用channellist渲染下拉框数据
};
getChannelList();
</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 channelList" :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 getChannelList = async () => {const res = await articleGetChannelService();channelList.value = res.data.data;console.log("res:", res.data.data);//用channellist渲染下拉框数据
};
getChannelList();
// 父传子
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:cid,此时它就是:cid和update.cid的简写当然子组件也需要做相应地改写:
父传子:接收父组件传过来的id
defineProps({cid:{type:[Number,String]//id可以是数字或字符串}
})
//子传父:将触发事件得到的值更新给父组件
const emit=defineEmits(['undate:modelValue'],$event)<el-select//二次封装:modelValue="cid"//拆解成:modelValue@update:modelValue="emit('update:cid', $event)"//和@update.modelValue
><el-optionv-for="channel in channelList":key="channel.id":label="channel.cate_name":value="channel.id"></el-option>
</el-select>可以看到除了 :modelValue=和@update:modelValue,都改成了cid
显然,cid是父传子时,父组件的自定义属性名,随便改
至于为什么非要写成modelValue,因为在父组件中,此时可以省略:
<el-select v-model.modelValue="params.state">
<! --后面的.modelValue可以不写 -->
<el-select v-model"params.state">
5.动态渲染文章列表
封装api接口
/******文章管理接口***** */
// 获取文章列表
export const articleGetListService = (params) => {return request.get('/my/article/list', { params })
}
页面调用
//把之前articleList的模拟数据删掉
const articleList = ref([])
动态渲染(取代表格中的模拟数据)
优化:下载插件+封装日期代码+对日期格式格式化
6.分页功能的实现
Pagination组件的使用
两个触发函数
7.loading功能的实现
8.搜索和重置功能的实现
三.添加文章的实现
1.引入和封装抽屉组件
从element-plus中引入抽屉
封装成子组件并导入
填充抽屉子组件的页面布局
2.上传文件功能的实现
使用URL.createObjectURL(…))实现上传
修改预览图片的尺寸
3.文章内容功能的实现:富文本编辑器
3.1.下载和引入富文本编辑器
3.2.实现添加文章的功能
四.编辑文章的实现
#### 1.编辑提交:提交时发送编辑请求
#### 2.open方法中,编辑的回显