一次用vue3简单封装table组件的实战过程

Vue
793
0
0
2023-07-08
目录
  • 前言:
  • 实现基础功能表格
  • 进一步定制化
  • 总结

前言:

对于一个业务前端来讲,工作中用的最多的组件我觉得大概率是table组件了,只要是数据展示就不可避免地用到这个组件。用久了就有点熟悉,就来锻炼自己的封装能力,为了更好的搬砖,封装table组件。

首先,我们要思考我们封装一个怎样的表格,怎样的形式更加方便。先定一个大概目标,往前走,剩下慢慢去完善。一般来说,我们更倾向于通过配置来实现表格,那就是说利用json格式来配置。

实现基础功能表格

el-table中用el-table-column的prop属性来对应对象中的键名即可填入数据,用 label 属性来定义表格的列名。那么这两个主要属性可以通过动态绑定来进行赋值,然后使用v-for来进行循环。

//app.vue
<script setup>
import TableVue from './components/Table.vue';
import {ref,reactive} from 'vue'
const tableData = [
  {
    date: '-05-03',
    name: 'Tom',
    address: 'No., Grove St, Los Angeles',
  },
]
const options = reactive({
  column:[
    {
      prop:'date',
      label:'日期',
      width:
    },
    {
      prop:'name',
      label:'名字',
      width:
    },
    {
      prop:'address',
      label:'地址',
    }
  ]
})
</script>
<template>
<TableVue :table-data="tableData" :options="options"></TableVue>
</template>
<style scoped></style>
//table.vue
<script setup>
import { ref,defineProps } from 'vue'
const props= defineProps({
    options:Object,
    tableData:Array
})
const {column} = props.options
</script>
<template>
<el-table :data="tableData" style="width:vw;">
    <el-table-column v-for="(item,index) in column" :prop="item.prop" :label="item.label" :width="item.width??''" />
  </el-table>
</template>
<style scoped></style>

好了,我们完成了通过json格式的最简单配置展示了数据,仅仅只有这样是远远不够的,连最基本的增删改查都没有。那么我们接下来就来实现增删改功能。对于增删改的方法基本上就是父子组件之间的方法调用值的传递使用,以及多了一个对话框来进行值的交互。这里也不难,下面是关键代码

//app.vue
//options里面新增以下属性
  index:true,//是否有序号 boolean  
  indexWidth:,//序号列表宽度 number
  indexFixed:true,//序号是否为固定列 boolean
  menu:true, //是否有操作栏 boolean
  menuWidth:,//操作栏宽度 number
  menuTitle:'操作',//操作栏标题 string
  menuFixed:true,//操作栏是否为固定列 boolean
  menuType:'text',//操作栏按钮样式 button/text
  
  //新增以下方法
  //删除
const rowDel = (index,row)=>{
  console.log('del',index,row)
}
//编辑
const rowEdit=(type,row)=>{
  console.log(type,row)
}

<TableVue :table-data="tableData" :options="options" @row-del="rowDel" @row-edit="rowEdit"></TableVue>
//table.vue新增以下方法
<script setup>
const emits = defineEmits(["rowDel", "rowEdit"])
//把属性解构出来减少代码量
const { options: op } = props
const { column } = props.options
//获取子组件实例
const edit = ref('edit')
//行数据删除时触发该事件
const rowDel = (index, row) => {
    emits("rowDel", index, row)
}
//更新数据后确定触发该事件
const editBefore = (row,type) => {
    //将行内属性转为普通对象传递
    edit.value.openDialog(type,row,toRaw(column))
}
const rowEdit=(type,form)=>{
    emits("rowEdit",type,form)
}
</script>

<template>
    <div class="menuOp">
        <el-button type="danger" :icon="Plus" :size="op.size??'small'" @click="editBefore(_,'add')">新增</el-button>
    </div>
    <el-table :data="tableData" style="width:vw;" :size="op.size??'small'">
        <el-table-column v-if="op.index" type="index" :width="op.indexWidth ??" />
        <el-table-column v-for="(item, index) in column" :prop="item.prop" :label="item.label" :width="item.width ?? ''" />
        <el-table-column :fixed="op.menuFixed ? 'right' : ''" :label="op.menuTitle" :width="op.menuWidth" v-if="op.menu">
            <template #default="scope">
                <el-button :type="op.menuType ?? 'primary'" size="small"
                    @click="editBefore(scope.row,'edit')">编辑</el-button>
                <el-button :type="op.menuType ?? 'primary'" size="small"
                    @click="rowDel(scope.$index, scope.row,'del')">删除</el-button>
            </template>
        </el-table-column>
    </el-table>
    <!-- 对话框 -->
    <editDialog ref="edit" @edit-submit="rowEdit"></editDialog>
</template>

<style scoped>
.menuOp{
text-align: left;
}
</style>

进一步定制化

虽然看着我们可以进行简单的增删改,但是如果编辑有下拉框或者其他类型呢。表格行内数据需要多样性展示,而不是单纯的纯文本又该怎么办呢。这时候我们需要用到vue里面的slot(插槽)来解决这个问题了。

插槽(slot)是 vue 为组件的封装者提供的能力。允许开发者在封装组件时,把不确定的、希望由用户指定的部分定义为插槽。

关键代码截图:

对话框我们利用插槽,那么同理,表格行内的数据展示我们也可以进行修改,来支持插槽

修改的关键代码为:

增删改基本功能到这里其实差不多,剩下就是一些细节性的判断,接下来我们就来完成查询部分的封装。方法也是一样,一般都是input框,其他有需求我们就利用插槽来实现。

//Search.vue
<script setup>
import { defineProps, onMounted, ref, defineEmits, toRaw, useSlots } from "vue";

const emits = defineEmits(["handleQuery", "handleReset"]);

const search = ref({});

const slots = useSlots();

const handleQuery = () => {
  emits("handleQuery", search.value);
};
const handleReset = () => {
  search.value = {};
  emits("handleReset");
};

const props = defineProps({
  row: {
    type: Object,
    default: () => {},
  },
  options: {
    type: Object,
    default: () => {},
  },
  search:{
    type:Object,
    default:()=>{}
  }
});

const column = toRaw(props.options.column);

onMounted(() => {
});
</script>

<template>
  <div style="text-align: left; margin-bottom:px">
    <el-form :inline="true" :model="search" class="demo-form-inline">
      <template v-for="(item, index) in props.row">
        <el-form-item
          :label="item.label"
          :label-width="`${item.searchLabel ?? options.searchLabel ??}px`"
        >
          <slot
            v-if="slots.hasOwnProperty(`${item?.prop}Search`)"
            :name="`${item.prop}Search`"
          >
            <el-input
              v-model="search[item.prop]"
              :style="{ width: item.searchWidth ?? options.searchWidth + 'px' }"
              :placeholder="`请输入${item.label}`"
            />
          </slot>
          <el-input
            v-else
            v-model="search[item.prop]"
            :style="{ width: item.searchWidth ?? options.searchWidth + 'px' }"
            :placeholder="`请输入${item.label}`"
          />
        </el-form-item>
      </template>
    </el-form>
    <div>
      <el-button type="primary" size="small" @click="handleQuery"
        >查询</el-button
      >
      <el-button type="primary" size="small" plain @click="handleReset"
        >重置</el-button
      >
    </div>
  </div>
</template>
//Table.vue
    <SearchVue
      :options="op"
      :row="slotCloumn"
      @handleReset="handleReset"
      @handleQuery="handleQuery"
      :search="search"
    >
      <template v-for="(item, index) in slotCloumn" #[item?.prop+`Search`]>
        <slot :name="`${item?.prop}Search`"></slot>
      </template>
    </SearchVue>

就暂时先写到这里了,最开始是想通过封装一个简单的组件,来巩固自己的vue3语法熟悉程度。后来发现因为水平有限加上是自己独立思考,有很多地方其实卡住了。比如值的传递是用provide(inject)比较好还是直接props来方便。值的改动需不需要用到computed或者watch。插槽的写法能否更加简便,不需要一级一级传递的那种,越想越多问题。自己技术能力不够这是最大的问题,想法过于宏大,能力却不行,打算日后精进自己的技术,然后约上几个伙伴继续去完善,只有思维碰撞才能产出好的代码。

最后,把全部代码附上

//App.vue
<script setup>
import TableVue from "./components/Table.vue";
import { ref, reactive,toRaw } from "vue";

const tableData = [
  {
    date: "-05-03",
    name: "Tom",
    address: "No., Grove St, Los Angeles",
  },
];

const options = reactive({
  index: true, //是否有序号 boolean
  indexWidth:, //序号列表宽度 number
  indexFixed: true, //序号是否为固定列 boolean
  menu: true, //是否有操作栏 boolean
  menuWidth:, //操作栏宽度 number
  menuTitle: "操作", //操作栏标题 string
  menuFixed: true, //操作栏是否为固定列 boolean
  menuType: "text", //操作栏按钮样式 button/text
  searchLabel:,//查询框label的宽度
  searchWidth:,//查询框组件的宽度
  column: [
    {
      prop: "date",
      label: "日期",
      width:,
      searchWidth:,
      searchLabel:,//行内的设置优先级高于全局
    },
    {
      prop: "name",
      label: "名字",
      width:,
      searchWidth:
    },
    {
      prop: "address",
      label: "地址",
      //是否在表单弹窗中显示
      editDisplay: false,
      searchWidth:
    },
  ],
});

const form = ref({})
const search = ref({})

//删除
const rowDel = (index, row) => {
  console.log("del", index, row);
};
//编辑
const rowEdit = (type, row) => {
  // console.log(type, row);
  //这里因为没有思考明白到底如何利用v-model属性进行所有组件绑定传递,就使用这种蹩脚方法
  console.log(Object.assign(row.value,form.value))
};
const tip=(row)=>{
  console.log(row)
}
const handleReset=()=>{
  console.log('reset')
}
const handleQuery=(param)=>{
  let params = Object.assign(search.value,param)
  console.log(params)
}

</script>

<template>
  <TableVue
    :table-data="tableData"
    :options="options"
    @row-del="rowDel"
    @row-edit="rowEdit"
    v-model="form"
    @handleQuery="handleQuery"
    @handleReset="handleReset"
    :search="search"
  >
  <!-- 查询框插槽 -->
  <template #dateSearch>
    <el-date-picker
        v-model="search.date"
        type="datetime"
        placeholder="Select date and time"
      />
  </template>
  <!-- 表格内的插槽,插槽名为字段名 -->
  <template #date="{scope}">
    <el-tag>{{scope.row.date}}</el-tag>
  </template>
  <!-- 操作栏插槽 -->
  <template #menu="{scope}" >
    <el-button icon="el-icon-check"  @click="tip(scope.row)">自定义菜单按钮</el-button>
  </template>
  <!-- 对话框插槽,插槽名字为对应的字段名加上Form -->
    <template #dateForm>
      <el-date-picker
        v-model="form.date"
        type="datetime"
        placeholder="Select date and time"
      />
    </template>
  </TableVue>
</template>

<style scoped></style>
//Search.vue
<script setup>
import { defineProps, onMounted, ref, defineEmits, toRaw, useSlots } from "vue";
const emits = defineEmits(["handleQuery", "handleReset"]);
const search = ref({});
const slots = useSlots();
const handleQuery = () => {
  emits("handleQuery", search.value);
};
const handleReset = () => {
  search.value = {};
  emits("handleReset");
};
const props = defineProps({
  row: {
    type: Object,
    default: () => {},
  },
  options: {
    type: Object,
    default: () => {},
  },
  search:{
    type:Object,
    default:()=>{}
  }
});
const column = toRaw(props.options.column);
onMounted(() => {
});
</script>
<template>
  <div style="text-align: left; margin-bottom:px">
    <el-form :inline="true" :model="search" class="demo-form-inline">
      <template v-for="(item, index) in props.row">
        <el-form-item
          :label="item.label"
          :label-width="`${item.searchLabel ?? options.searchLabel ??}px`"
        >
          <slot
            v-if="slots.hasOwnProperty(`${item?.prop}Search`)"
            :name="`${item.prop}Search`"
          >
            <el-input
              v-model="search[item.prop]"
              :style="{ width: item.searchWidth ?? options.searchWidth + 'px' }"
              :placeholder="`请输入${item.label}`"
            />
          </slot>
          <el-input
            v-else
            v-model="search[item.prop]"
            :style="{ width: item.searchWidth ?? options.searchWidth + 'px' }"
            :placeholder="`请输入${item.label}`"
          />
        </el-form-item>
      </template>
    </el-form>
    <div>
      <el-button type="primary" size="small" @click="handleQuery"
        >查询</el-button
      >
      <el-button type="primary" size="small" plain @click="handleReset"
        >重置</el-button
      >
    </div>
  </div>
</template>
//table.vue
<script setup>
import { ref, defineProps, defineEmits, toRaw, onMounted, useSlots } from "vue";
import { Delete, Plus } from "@element-plus/icons-vue";
import editDialog from "./Dialog.vue";
import SearchVue from "./Search.vue";
const props = defineProps({
  options: Object,
  tableData: Array,
  modelValue: {
    type: Object,
    default: () => {},
  },
});
const emits = defineEmits([
  "rowDel",
  "rowEdit",
  "update:modelValue",
  "handleQuery",
]);
//把属性解构出来减少代码量
const { options: op } = props;
const { column } = props.options;
//获取编辑对话框里面所有属性,用于动态生成插槽
const slotCloumn = toRaw(column);
//获取子组件实例
const edit = ref("edit");
//获取插槽实例
const slots = useSlots();
//行数据删除时触发该事件
const rowDel = (index, row) => {
  emits("rowDel", index, row);
};
//更新数据后确定触发该事件
const editBefore = (row, type) => {
  //将行内属性转为普通对象传递
  edit.value.openDialog(type, row, toRaw(column));
};
const rowEdit = (type, form) => {
  emits("rowEdit", type, form);
  emits("update:modelValue", form);
};
const handleQuery = (search) => {
  emits("handleQuery", search);
};
const handleReset = () => {
  emits("handleReset");
};
onMounted(() => {
  console.log("slots", slots);
});
</script>
<template>
  <div>
    <SearchVue
      :options="op"
      :row="slotCloumn"
      @handleReset="handleReset"
      @handleQuery="handleQuery"
      :search="search"
    >
      <template v-for="(item, index) in slotCloumn" #[item?.prop+`Search`]>
        <slot :name="`${item?.prop}Search`"></slot>
      </template>
    </SearchVue>
  </div>
  <div class="menuOp">
    <el-button
      type="danger"
      :icon="Plus"
      :size="op.size ?? 'small'"
      @click="editBefore(_, 'add')"
      >新增</el-button
    >
  </div>
  <el-table :data="tableData" style="width:vw" :size="op.size ?? 'small'">
    <el-table-column
      v-if="op.index"
      type="index"
      :width="op.indexWidth ??"
    />
    <template v-for="(item, index) in column">
      <el-table-column
        :label="item.label"
        v-if="slots.hasOwnProperty(item?.prop)"
      >
        <template #default="scope">
          <slot :name="item.prop" :scope="scope"></slot>
        </template>
      </el-table-column>
      <el-table-column
        v-else
        :prop="item.prop"
        :label="item.label"
        :width="item.width ?? ''"
      />
    </template>
    <el-table-column
      :fixed="op.menuFixed ? 'right' : ''"
      :label="op.menuTitle"
      :width="op.menuWidth"
      v-if="op.menu"
    >
      <template #default="scope">
        <el-button
          :type="op.menuType ?? 'primary'"
          size="small"
          @click="editBefore(scope.row, 'edit')"
          >编辑</el-button
        >
        <el-button
          :type="op.menuType ?? 'primary'"
          size="small"
          @click="rowDel(scope.$index, scope.row, 'del')"
          >删除</el-button
        >
        <!-- 利用作用域插槽将数据传递过去 -->
        <slot name="menu" :scope="scope"></slot>
      </template>
    </el-table-column>
  </el-table>
  <!-- 对话框 -->
  <editDialog ref="edit" @edit-submit="rowEdit">
    <template v-for="(item, index) in slotCloumn" #[item?.prop+`Form`]="scope">
      <slot :name="`${item?.prop}Form`" v-bind="scope"></slot>
    </template>
  </editDialog>
</template>

<style scoped>
.menuOp {
  text-align: left;
}
</style>
//Dialog.vue
<script setup>
import { ref, defineExpose, defineEmits, useSlots, onMounted } from "vue";
const emits = defineEmits(["editSubmit"]);
const dialogFormVisible = ref(false);
const form = ref({});
const tp = ref("");
let columns = [];
//获取当前已实例化的插槽,然后去判断插槽是否使用了
const slots = useSlots();

const openDialog = (type, row, column) => {
  dialogFormVisible.value = true;
  columns = column;
  tp.value = type;
  if (type === "edit") {
    //如果编辑框设置了editDisplay为false,则删除该属性
    columns.map((x) => {
      if (x.editDisplay === false) {
        delete row[x.prop];
      }
    });
    form.value = JSON.parse(JSON.stringify(row));
  }else{
    form.value={}
  }
};
const handleSubmit = () => {
  emits("editSubmit", tp, form);
};
onMounted(() => {
  console.log("===", slots);
});
defineExpose({
  openDialog,
});
</script>
<template>
  <el-dialog v-model="dialogFormVisible" append-to-body :title="tp==='add'?'新增':'编辑'">
    <el-form :model="form">
      <template v-for="(item, index) in columns">
        <el-form-item
          v-if="tp==='add'?item.addDisplay??true:item.editDisplay ?? true"
          :key="index"
          :label="item.label"
          label-width="px"
        >
          <slot
            :name="`${item?.prop}Form`"
            v-if="slots.hasOwnProperty(`${item?.prop}Form`)"
          >
            <!-- 因为在table组件已经开始生成所有关于编辑的插槽,所以全部都有实例,需要给个默认显示,否则会空白 -->
            <el-input v-model="form[item?.prop]" />
          </slot>
          <el-input v-model="form[item?.prop]" v-else />
        </el-form-item>
      </template>
    </el-form>
    <template #footer>
      <span class="dialog-footer">
        <el-button @click="dialogFormVisible = false">取消</el-button>
        <el-button type="primary" @click="handleSubmit"> 确认 </el-button>
      </span>
    </template>
  </el-dialog>
</template>