《Vue项目开发实战》第八章:组件封装--vxeGrid

系列文章

《Vue项目开发实战》https://blog.csdn.net/sen_shan/category_13075264.html

第七章:组件封装--vxeTablehttps://blog.csdn.net/sen_shan/article/details/154987916

文章目录

目录

系列文章

文章目录

前言

VxeGrid组件

一、定位与目标

二、功能总览

三、接口说明

四、业务规则与交互逻辑

五、用例示例

六、性能与边界


前言

本文详细介绍了基于Vue3和vxe-grid二次封装的通用表格组件ActionVxeGridCont.vue的实现。

该组件整合了数据展示、分页、搜索、多选、列过滤等功能,支持90%的后台列表场景开发。

文章从组件定位、功能总览、接口说明(Props、事件、插槽、方法)、业务规则与交互逻辑等方面进行了全面解析,并提供了具体使用示例。

组件特点包括:内置搜索栏和分页功能、支持本地和远程分页模式、提供多选和行操作功能、高度可定制化(列配置、样式、事件等)。

通过封装,开发者可以快速实现功能完善的表格页面,同时保留vxe-table的全部原生能力。

VxeGrid组件

新建文件components/ActionVxeGridCont.vue

html 复制代码
<template>
  <div class="flex flex-col" :style="{ height }">
    <!-- 工具栏 -->
    <vxe-toolbar v-if="showToolbar" class="flex-shrink-0">
      <template #buttons><slot name="toolbar-left" /></template>
      <template #tools><slot name="toolbar-right" /></template>
    </vxe-toolbar>
        <!-- 1. 内置搜索栏 -->
    <el-input
     v-if="showSearch"
      v-model="keyword"
      placeholder="请输入关键字"
      clearable
      style="width: 260px; margin-bottom: 8px"
      @clear="onClear"
      @keyup.enter="onSearch"
    >
    <!--
   
      <template #append>
        <el-button :icon="Search" @click="onSearch" />
      </template>
       -->
    </el-input>



    <!-- 2. 表格:所有属性写进开始标签 -->
    <vxe-grid
      ref="xTable"
      class="flex-1"
      :stripe="stripe"
      :row-class-name="props.rowClassName ?? tableRowClassName"
      :data="currentPageData"
      :loading="loading"
      :row-config="props.rowConfig"
      :column-config="{ resizable: true }"
      :sort-config="sortConfig"
      :checkbox-config="checkboxConfig"
      :edit-config="editConfig"
      :columns="processedColumns"
      @checkbox-change="handleSelectionChange"
      @checkbox-all="handleSelectionChange"
      @cell-dblclick="handleCellDbClick"
      @row-dblclick="handleRowDbClick"
      @row-click="handleRowClick"
      @cell-click="handleCellClick"
      @sort-change="handleSortChange"
    >
      <!-- 操作列插槽 -->
      <template #operate="{ row }" v-if="$slots.operate">
        <slot name="operate" :row="row" />
      </template>
    </vxe-grid>

    <!-- 分页:直接绑定父组件 page 对象 -->
    <vxe-pager
      v-if="showPager"
      class="flex-shrink-0"
      v-bind="localPage"
      :layouts="pagerLayouts"
      @page-change="onPageChange"
    />
  </div>
</template>

<script setup lang="ts">
import type { PropType } from 'vue'
import { Search } from '@element-plus/icons-vue'
import { ref, computed, useSlots } from 'vue'
import type {
  VxeGridInstance,
  VxeGridPropTypes,
  VxeTablePropTypes,
  VxeGridEvents,
  VxeGridProps
} from 'vxe-table'
import type { VxePagerEvents, VxePagerPropTypes } from 'vxe-pc-ui'
import type { PageInfo,Column } from '@/types'
import defaultConfig from '@config'


/* ---------- props ---------- */
const props = defineProps({
  tableColumns: { type: Array as PropType<Column[]>, required: true },
  tableData: { type: Array as PropType<any[]>, default: () => [] },
  fullData: { type: Array as PropType<any[]>, default: () => [] },
  showSearch: { type: Boolean, default: true },
  page: {
    type: Object as PropType<PageInfo>,
    default: () => ({
      currentPage: 1,
      pageSize: defaultConfig.tablePageSize,
      pageSizes: defaultConfig.tablePageSizes,
      total: 0
    })
  },
  pagerLayouts: {
    type: Array as PropType<VxePagerPropTypes.Layouts>,
    default: () => ['PrevPage', 'JumpNumber', 'NextPage', 'FullJump', 'Sizes', 'Total']
  },
  rowConfig: {
    type: Object as PropType<VxeGridProps['rowConfig']>,
    default: () => ({ isHover: true, keyField: 'id' })
  },
  showPager: { type: Boolean, default: true },
  height: { type: String, default: '600px' },
  showToolbar: { type: Boolean, default: true },
  operateWidth: { type: Number, default: 120 },
  rowKey: { type: String, default: 'id' },
  loading: { type: Boolean, default: false },
  stripe: { type: Boolean, default: false },
  rowClassName: { type: Function as PropType<(params: any) => string> },
  sortConfig: { type: Object as PropType<VxeTablePropTypes.SortConfig>, default: () => ({ remote: false }) },
  checkboxConfig: {
    type: Object as PropType<VxeTablePropTypes.CheckboxConfig>,
    default: (): VxeTablePropTypes.CheckboxConfig => ({
      highlight: true,
      range: true
    })
  },
  editConfig: { type: Object as PropType<VxeTablePropTypes.EditConfig> }
})

/* ---------- emit ---------- */
const emit = defineEmits([
  'update:page',
  'selection-change',
  'sort-change',
  'row-dblclick',
  'cell-dblclick',
  'row-click',
  'cell-click'
])

/* ---------- 计算属性 ---------- */
const slots = useSlots()
const xTable = ref<VxeGridInstance>()


const processedColumns = computed<VxeGridPropTypes.Columns>(() => {
  //const cols: VxeGridPropTypes.Columns = (props.tableColumns ?? []).map(col => {
  // const cols: VxeGridPropTypes.Columns = props.tableColumns.map(col => {
    const cols: VxeGridPropTypes.Columns = (props.tableColumns ?? [])
    .filter(col => !col.hide) // 👈 新增
    .map(col => {
    const item: any = { ...col }
    if (col.is_filter && col.field) {
      const dataSource = props.tableData.length ? props.tableData : []
      const unique = Array.from(new Set(dataSource.map((i: any) => i[col.field])))
        .filter(v => v != null)
        .map(v => ({ label: String(v), value: v }))
      item.filters = unique
      item.filterMethod = ({ value, row }: any) => value == null || row[col.field] == value
    }
    if ((col as any).field && slots[(col as any).field]) {
      item.slots = { default: ({ row }: any) => slots[(col as any).field]?.({ row }) }
    }
    return item
  })
  if (slots.operate) {
    cols.push({ title: '操作', fixed: 'right', width: props.operateWidth, slots: { default: 'operate' } })
  }
  return cols
})
const localPage = ref({
  currentPage: props.page.currentPage,
  pageSize: props.page.pageSize,
  pageSizes: props.page.pageSizes,
  total: props.page.total
})

/* ---------- 搜索属性与事件 ---------- */
const keyword = ref('')
/* 过滤后的全量数据 */
const filteredData = computed(() => {
 
  const kw = keyword.value.trim().toLowerCase()
  // 1. 先拿到过滤后的数组
  const list = !kw
    ? props.tableData
    : props.tableData.filter((v: any) =>
        Object.values(v)
          .filter(val => val !== null && val !== undefined)
          .some(val => String(val).toLowerCase().includes(kw))
      )

  // 2. 关键:把总条数实时同步给父组件
  //    只要搜索关键字变化,这里就会重新执行
  localPage.value.total = list.length
  localPage.value.currentPage = 1
  return list
})

/* 搜索 / 清空 */
function onSearch() {
  /* 只要关键字变化,filteredData 会自动重新计算;
     把页码拉回第一页即可 */
  emit('update:page', { ...props.page, currentPage: 1 })
}
function onClear() {
  keyword.value = ''
  emit('update:page', { ...props.page, currentPage: 1 })
}

/* 总条数 = 过滤后 */
const total = computed(() => filteredData.value.length)

/* ---------- 事件处理 ---------- */
const handleSelectionChange: VxeGridEvents.CheckboxChange | VxeGridEvents.CheckboxAll = () => {
  const records = xTable.value?.getCheckboxRecords() ?? []
  emit('selection-change', records)
}
/*
const onPageChange: VxePagerEvents.PageChange = ({ currentPage, pageSize }) => {
  emit('update:page', { ...props.page, currentPage, pageSize })
}
  */
const onPageChange: VxePagerEvents.PageChange = ({ currentPage, pageSize }) => {
  localPage.value.currentPage = currentPage
  localPage.value.pageSize = pageSize
  //emit('update:page', { ...props.page, currentPage: 1, total: list.length })
}
const handleCellDbClick: VxeGridEvents.CellDblclick = ({ row, column, cell, $event }) => {
  console.log('cell double clicked:', row, column, cell, $event)
  emit('cell-dblclick', row, column, cell, $event)
}
const handleRowDbClick: any = ({ row, $event }) => {
  console.log('row double clicked:', row, $event)
  emit('row-dblclick', row, $event)
}
const handleRowClick = ({ row, $event }) => {
  console.log('row clicked:', row, $event)
  emit('row-click', row, $event)
}
const handleCellClick: VxeGridEvents.CellClick = ({ row, column, cell, $event }) => {
   
  emit('cell-click', row, column, cell, $event)
}
const handleSortChange: VxeGridEvents.SortChange = (params) => {
  emit('sort-change', params)
}

/* ---------- 新增:当前页数据 ---------- */
const currentPageData = computed(() => {
  const { currentPage, pageSize } =localPage.value // props.page
  const start = (currentPage - 1) * pageSize
  const end = start + pageSize
  return filteredData.value.slice(start, end)
  //return props.tableData.slice(start, end)   // 本地分页核心
})

function tableRowClassName({ rowIndex }: any) {
  return rowIndex % 2 === 0 ? 'bg-gray-50' : 'bg-white'
}

/* ---------- 暴露方法 ---------- */
defineExpose({
  getTableRef: () => xTable.value,
  getCheckboxRecords: () => xTable.value?.getCheckboxRecords() ?? [],
  clearCheckboxRow: () => xTable.value?.clearCheckboxRow()
})
</script>

组件名称

通用数据表格 / 通用分页表格

一、定位与目标

  1. 定位:Vue3 + vxe-table 二次封装的高阶表格组件,同时解决"数据展示、分页、搜索、多选、列过滤、操作列、插槽扩展"等 90% 后台列表场景。

  2. 目标:让开发快速完成一个"可搜索、可分页、可筛选、带操作按钮"的表格,同时保留 vxe-grid 的全部原生能力。

二、功能总览

快速判定

只想"放数据就能看" → 传 columns + data 即可

需要"后端翻页" → 监听 @update:page 并把新数据塞进 tableData

需要"批量操作" → 用 getCheckboxRecords() 拿行,再调业务接口

三、接口说明

  1. Props(24 项)

|----------------|------------|---------------------------------------|--------------------------------|
| 名称 | 类型 | 默认值 | 说明 |
| tableColumns | Column[] | required | 列配置,详情见 3.2 |
| tableData | any[] | [] | 当前页数据(本地分页时=全量数据) |
| fullData | any[] | [] | 远程分页时传入总数据,用于列过滤枚举 |
| showSearch | boolean | TRUE | 是否显示顶部搜索框 |
| showPager | boolean | TRUE | 是否显示底部分页 |
| showToolbar | boolean | TRUE | 是否显示顶部工具栏插槽 |
| page | PageInfo | {currentPage:1, pageSize:30, total:0} | 分页对象,支持 .sync |
| pagerLayouts | string[] | 默认阿里云风格 | 分页栏布局 |
| height | string | '600px' | 表格总高,支持 100%、calc(100vh-200px) |
| rowKey | string | 'id' | 行唯一键,用于多选跨页 |
| loading | boolean | FALSE | 加载遮罩 |
| stripe | boolean | FALSE | 斑马纹 |
| rowClassName | function | - | 自定义行样式 |
| sortConfig | object | {remote:false} | 排序配置 |
| checkboxConfig | object | {highlight:true,range:true} | 多选配置 |
| editConfig | object | - | 行编辑配置,直接透传 vxe |
| operateWidth | number | 120 | 操作列宽度 |

  1. Column 配置(tableColumns 单项)
  1. 事件(emit)

|------------------|-------------------------|-------|
| 事件名 | 回调参数 | 触发时机 |
| update:page | PageInfo | 分页变化 |
| selection-change | records:[] | 勾选行变化 |
| row-click | {row, event} | 行单击 |
| row-dblclick | {row, event} | 行双击 |
| cell-click | {row,column,cell,event} | 单元格单击 |
| cell-dblclick | {row,column,cell,event} | 单元格双击 |
| sort-change | Vxe 原生参数 | 排序 |

  1. 插槽
  1. 方法(expose)

四、业务规则与交互逻辑

  1. 搜索规则

1.1 全局模糊匹配:对当前 tableData 中所有字段值做 String.includes,区分大小写。

1.2 实时过滤:输入框每输入 1 字符立即重新计算 filteredData,并同步更新 total。

1.3 回车 / 清空:自动重置 currentPage = 1,并触发 update:page 事件。

  1. 分页规则

2.1 本地模式:showPager=true 且 data 长度 ≤ pageSize 时,自动隐藏分页栏。

2.2 远程模式:父组件监听 update:page,自行拉取数据后把新数据传入 tableData,组件内部不做分页切片。

2.3 页码溢出自愈:当搜索后总页数减少导致当前页码溢出,组件自动把 currentPage 重置为最大页。

  1. 列过滤规则

3.1 仅对 is_filter=true 且 field 存在的列生效。

3.2 枚举值来源优先级:fullData > tableData > [],空数组自动隐藏筛选图标。

3.3 多列过滤取交集。

  1. 多选规则

4.1 默认开启跨页多选(reserve=true),调用 getCheckboxRecords() 可拿到所有页勾选行。

4.2 父组件重置数据后,如需清空勾选,可调用 clearCheckboxRow()。

  1. 操作列规则

5.1 仅在父组件写了 <template #operate="{row}"> 时渲染,固定右侧,冻结滚动。

5.2 宽度通过 operateWidth 控制,默认 120px。

  1. 高度自适应规则

6.1 支持 calc(100vh-200px) 写法,组件内部 flex 布局自动撑满剩余空间。

6.2 当表格数据为空时,自动显示 vxe 内置空数据图,高度不变。

五、用例示例

html 复制代码
<template>
  <el-card shadow="never">
    <template #header><span>员工列表(静态 12 条)</span></template>
 


    <!-- 2. 表格 -->
    <MyVxeGrid
      :show-toolbar="false"
      :show-search="true"
      :table-data="ALL_DATA"
      :table-columns="columns"
      v-model:page="pagerConfig"     
      :row-class-name="handleMyRowClass"
      @selection-change="handleSelectionChange"
      height="700"
    >
      <template #toolbar-left>
        <el-button type="primary" icon="Plus">新增</el-button>
      </template>
      <template #toolbar-right>
        <el-button icon="Refresh">刷新</el-button>
      </template>
    </MyVxeGrid>
  </el-card>
</template>

<script setup lang="ts">
import { ref,computed } from 'vue'
import MyVxeGrid from '@/components/ActionVxeGridCont.vue'


/* ---------- 全部 12 条(一次性给 grid) ---------- */
const ALL_DATA = ref([
  { id: 1, name: '张三', age: 28, department: '技术部' },
  { id: 2, name: '李四', age: 32, department: '市场部' },
  { id: 3, name: '王五', age: 24, department: '技术部' },
  { id: 4, name: '赵六', age: 38, department: '人事部' },
  { id: 5, name: '钱七', age: 29, department: '技术部' },
  { id: 6, name: '孙八', age: 26, department: '市场部' },
  { id: 7, name: '周九', age: 31, department: '人事部' },
  { id: 8, name: '吴十', age: 27, department: '技术部' },
  { id: 9, name: '郑一', age: 33, department: '技术部' },
  { id: 10, name: '王二', age: 25, department: '市场部' },
  { id: 11, name: '张三丰', age: 36, department: '人事部' },
  { id: 12, name: '李小明', age: 29, department: '技术部' }
])

/* ---------- 列配置 ---------- */
const columns  = [
  { type: 'checkbox', width: 60 },
  { type: 'seq', title: '序号', width: 60 , hide: true},
  { field: 'name', title: '姓名', width: 120, sortable: true },
  { field: 'age', title: '年龄', width: 100, sortable: true },
  { field: 'department', title: '部门', width: 140 }
]

 

 

/* ---------- 分页参数 ---------- */
const pagerConfig = ref({
  currentPage: 1,
  pageSize: 5,
  pageSizes: [5, 10, 20],
  //total: computed(() => filteredData.value.length)
   total: ALL_DATA.value.length
})

const handleSelectionChange = (selection) => {
  console.log('选中:', selection)

  // 在这里添加您需要的业务逻辑
  // 例如:
  // - 显示行详情
  // - 跳转到编辑页面
  // - 弹出操作菜单等
}
 
// 行类名函数
const handleMyRowClass = ({ row, rowIndex }) => {
 //  stripe="true" 下面语句会失效,若下面生效必须改stripe="false"
// 年龄大于38岁的行 - 高亮为黄色
  if (row.age >= 38) {
    return 'bg-yellow-300'
  }
  // 奇数行
  else if (rowIndex % 2 === 0) {
    return 'bg-gray-100'
  }
  // 偶数行
  else {
    return 'bg-white'
  }
}


</script>

六、性能与边界

  1. 本地分页一次性渲染数据上限:测试 10 k 行仍保持滚动流畅(Chrome 120)。

  2. 列过滤枚举值:单列枚举值超过 500 项时,建议关闭 is_filter 改为自定义搜索。

  3. 高度计算:在父容器 display:flex;flex-direction:column 时才能自动撑满,否则需给固定高度。

相关推荐
EnCi Zheng17 分钟前
M5-markconv自定义CSS样式指南 [特殊字符]
前端·css·python
kyriewen21 分钟前
你的网页慢,用户不说直接走——前端性能监控教你“读心术”
前端·性能优化·监控
广州华水科技21 分钟前
北斗GNSS变形监测在大坝安全监测中的应用与优势分析
前端
前端老石人32 分钟前
前端开发中的 URL 完全指南
开发语言·前端·javascript·css·html
CAE虚拟与现实33 分钟前
五一假期闲来无事,来个前段、后端的说明吧
前端·后端·vtk·three.js·前后端
Sarvartha44 分钟前
三目运算符
linux·服务器·前端
晓晨的博客1 小时前
ROS1录制的bag包转换为ROS2格式
前端·chrome
Wect1 小时前
LeetCode 72. 编辑距离:动态规划经典题解
前端·算法·typescript
donecoding1 小时前
别再让 pnpm 跟着 nvm 跑了!独立安装终极指南
前端·node.js·前端工程化
不可能的是1 小时前
从 /simplify 指令深挖 Claude Code 多 Agent 协同机制
javascript