vue3项目之大事件管理系统(三) 二级路由:文章分类页的实现,文章管理页的实现

文章目录

零.关于两个相似接口的说明

区别文章分类页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_aliasid,title,pub_date,state,cate_name
页面调用接口的函数名getArticleChannel=async()=>{}getArticleList=async()=>{}
在哪些页面调用文章分类页ArticleChannel和下拉菜单子组件ChannelSelect文章管理页ArticleManage
用什么数组去接返回数据channelList或channelArrarticleList

总结:这两个页面的命名非常混乱和随意,如果让我再做一次这个项目,我会做如下优化

  • 把第一个二级路由页面叫做文章分类页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.下载和引入富文本编辑器
//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_imgURL网络地址

需求:要把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()
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前端OnTheRun

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值