vue3+ts+element-plus el-table组件二次封装(2024-04-24 TTable组件多级表头支持单元格编辑功能)

2024-04-24 TTable组件多级表头支持单元格编辑功能

在这里插入图片描述

2024-03-20 TTable组件新增新增第一列既显示(复选、单选)和序列号

在这里插入图片描述

一、需求

对于后台管理系统项目必不可少的列表数据渲染;而element-plus的table组件还是需要写很多代码;为了方便因此封装了TTable组件(一行代码,可以实现表格编辑/分页/表格内/外按钮操作/排序/显示隐藏表格内操作按钮)

二、组件功能

1、基础表格
2、带斑马线、带边框、固定列/表头
3、多级表头
4、自定义表头
5、单个单元格编辑功能
6、可以设置表格标题
7、可集成了分页组件(复选框支持翻页选中)
8、表格可以自定义插槽渲染某列数据
9、表格可以render渲染某列数据(支持jsx方式)
10、集成了表格内操作和表格外操作
11、支持某列字典过滤渲染
12、支持列--显示隐藏及拖拽排序
13、支持整行--拖拽排序
14、支持单元格编辑键盘事件(向上、向下、回车横向的下一个输入框)
15、表格实现了双击复制单元格内容功能
16、表格实现单选框选中取消功能
17、单元格编辑新增表单校验功能
18、表格内操作按钮权限配置
19、展开行
20、多级表头支持单元格编辑功能
21、支持tree-table
22、第一列显示复选框和序列号

三、最终效果

在这里插入图片描述

四、参数配置

1、代码示例:

     <t-table
          :table="table"
          :columns="table.columns"
          @size-change="handlesSizeChange"
          @page-change="handlesCurrentChange"
        />

2、配置参数(Table Attributes)

参数说明类型默认值
table表格数据对象Object{}
—rules规则(可依据 elementPlus el-form 配置————对应 columns 的 prop 值)Object-
—data展示数据Array[]
—toolbar表格外操作栏选中表格某行,可以将其数据传出Array[]
—operator表格内操作栏数据Array[]
-------hasPermi表格内操作栏按钮权限资源(必须传btnPermissions属性才生效)String-
-------show表格内操作栏根据状态显示Object-
-------renderrender函数渲染使用的 Function(val) 可以用 tsx 方式Function-
-------noshow表格内操作栏根据多种状态不显示Array-
-------bind继承el-button所有Attributes(默认值{ type:‘primary’,link:true,text:true,size:‘small’,})Object-
-------fun事件名function-
—operatorConfig表格内操作栏样式Object-
--------fixed列是否固定在左侧或者右侧。 true 表示固定在左侧(true / ‘left’ / ‘right’)string / boolean-
--------label显示的标题string‘操作’
--------width对应列的宽度(固定的)string / number-
--------minWidth对应列的最小宽度(会把剩余宽度按比例分配给设置了 min-width 的列)string / number-
--------align对齐方式 (left / center / right)string‘center’
--------bindel-table-column AttributesObject-
—firstColumn表格首列(序号 index,复选框 selection,单选 radio,展开行 expand)排列object-
—total数据总条数Number-
—pageSize页数量Number-
—currentPage是否需要显示切换页条数Number-
columns表头信息Array[]
----sort排序 (设置:sort:true)Booleanfalse
----renderHeader列标题 Label 区域渲染使用的 Function(val) 可以用 jsx 方式Function-
----render某列render函数渲染使用的 Function(val) 可以用 jsx 方式Function-
----bindel-table-column AttributesObject-
----width对应列的宽度(固定的)string / number-
----minWidth对应列的最小宽度(会把剩余宽度按比例分配给设置了 min-width 的列)string / number-
----noShowTip是否换行 (设置:noShowTip:false换行,不设置自动隐藏)Boolean-
----slotName插槽显示此列数据(其值是具名作用域插槽String-
----isShowHidden是否动态显示隐藏列设置(隐藏/显示列)Booleanfalse
----slotNameMerge合并表头插槽显示此列数据(其值是具名作用域插槽)String-
----------scope具名插槽获取此行数据必须用解构接收{scope}.row 是当前行数据 }Object-
----canEdit是否开启单元格编辑功能Booleanfalse
----configEdit表格编辑配置(开启编辑功能有效)Object-
----------rules规则(可依据 elementPlus el-form 配置————对应 columns 的 prop 值)Object-
----------labelplaceholder 显示String-
----------editComponent组件名称可直接指定全局注册的组件,也可引入’element/abtd’如:‘a-input/el-input’String-
----------eventHandle第三方 UI 的 事件(返回两个参数,第一个自己自带,第二个 scope)Object-
----------bind第三方 UI 的 Attributes,如 el-input 中的 clearable 清空功能Object-
----------event触发 handleEvent 事件的标志String-
----------type下拉或者复选框显示(select-arr/select-obj/checkbox)String-
----------list下拉选择数据源名称String-
----------arrLabeltype:select-arr 时对应显示的中文字段String-
----------arrKeytype:select-arr 时对应显示的数字字段String-
----filters字典过滤Object-
----------listlistTypeInfo 里面对应的下拉数据源命名String-
----------key数据源的 key 字段String‘value’
----------label数据源的 label 字段String‘label’
btnPermissions按钮权限数据集(后台返回的按钮权限集合)Array-
listTypeInfo下拉选择数据源Object-
footer底部操作区(默认隐藏,使用插槽展示“保存”按钮)slot-
pagination分页器自定义内容 设置文案(table设置layout才生效)slot-
isKeyup单元格编辑是否开启键盘事件Booleanfalse
isShowFooterBtn是否显示保存按钮Booleanfalse
title表格左上标题String /slot-
isShowPagination是否显示分页(默认显示分页)Booleantrue
isPaginationCumulative序列号显示是否分页累加Booleanfalse
isTableColumnHidden是否开启合计行隐藏复选框/单选框Booleanfalse
isCopy是否允许双击单元格复制Booleanfalse
defaultRadioCol设置默认选中项(单选)defaultRadioCol 值必须大于 0!Number-
rowClickRadio是否开启点击整行选中单选框Booleantrue
columnSetting是否显示设置(隐藏/显示列)Booleanfalse
name与 columnSetting 配合使用标记隐藏/显示列唯一性Stringtitle
isRowSort是否开启行拖拽(row-key 需要设置)Booleanfalse
isTree是否开启Tree-table样式Booleanfalse
columnSetBind列设置按钮配置(继承el-button所有属性)Object-
----btnTxt按钮显示文字String‘列设置’
----title点击按钮下拉显示titleString‘列设置’
----sizeel-button的sizeString‘default’
----iconel-button的iconString‘Setting’

3、events 其他事件按照 el-table 直接使用(如 sort-change 排序事件)

事件名说明返回值
page-change当前页码当前选中的页码
save保存按钮编辑后的所有数据
handleEvent单个输入触发事件configEdit 中的 event 值和对应输入的 value 值
radioChange单选选中事件返回当前选中的整行数据
rowSort行拖拽排序后触发事件返回排序后的table数据
validateError单元格编辑保存校验不通过触发返回校验不通过的 prop–label 集合

4、Methods 方法继承el-table所有方法

事件名说明参数
clearSelection用于多选表格,清空用户的选择-
clearSort清空排序条件-
toggleRowSelection取消某一项选中项-
toggleAllSelection全部选中-
saveMethod单元格编辑保存方法callback(tableData)
resetFields对表单进行重置,并移除校验结果(单元格编辑时生效)
clearValidate清空校验规则(单元格编辑时生效)-

5、Slots插槽

插槽名说明参数
titleTTable 左侧Title-
toolbarTTable 右侧toolbar-
expandtable.firstColumn.type:expand 展开行插槽scope
-el-table-column某列自定义插槽(slotName命名)scope
-el-table-column单元格编辑插槽(editSlotName命名)scope
-el-table-column表头合并插槽(slotNameMerge命名)scope
-操作列前一列自定义默认内容插槽-
footer底部操作区(默认隐藏,使用插槽展示“保存”按钮)-
pagination分页器自定义内容 设置文案(table设置layout才生效)-

五、源码

<template>
  <div class="t-table" ref="TTableBox">
    <div class="header_wrap">
      <div class="header_title">
        {{ title }}
        <slot name="title" />
      </div>
      <div class="toolbar_top">
        <!-- 表格外操作 -->
        <slot name="toolbar"></slot>
        <!--列设置按钮-->
        <div
          class="header_right_wrap"
          :style="{ marginLeft: isShow('toolbar') ? '12px' : 0 }"
        >
          <slot name="btn" />
          <column-set
            v-if="columnSetting"
            v-bind="$attrs"
            :columns="renderColumns"
            @columnSetting="(v) => (state.columnSet = v)"
          />
        </div>
      </div>
    </div>
    <el-table
      ref="TTable"
      :data="state.tableData"
      :class="{
        cursor: isCopy,
        row_sort: isRowSort,
        tree_style: isTree,
        highlightCurrentRow: highlightCurrentRow,
        radioStyle: table.firstColumn && table.firstColumn.type === 'radio',
      }"
      v-bind="$attrs"
      :highlight-current-row="highlightCurrentRow"
      :border="table.border || isTableBorder"
      @cell-dblclick="cellDblclick"
      @row-click="rowClick"
      :cell-class-name="cellClassNameFuc"
    >
      <!-- 复选框/单选框/序列号 -->
      <template v-if="table.firstColumn">
        <!-- 复选框 -->
        <el-table-column
          v-if="table.firstColumn.type === 'selection'"
          v-bind="{
            type: 'selection',
            width: table.firstColumn.width || 55,
            label: table.firstColumn.label,
            fixed: table.firstColumn.fixed,
            align: table.firstColumn.align || 'center',
            'reserve-selection': table.firstColumn.isPaging || false,
            selectable: table.firstColumn.selectable,
            ...table.firstColumn.bind,
          }"
        />
        <el-table-column
          v-else
          v-bind="{
            type: table.firstColumn.type,
            width: table.firstColumn.width || 55,
            label:
              table.firstColumn.label ||
              (table.firstColumn.type === 'radio' && '单选') ||
              (table.firstColumn.type === 'index' && '序号') ||
              (table.firstColumn.type === 'expand' && '') ||
              '',
            fixed: table.firstColumn.fixed,
            align: table.firstColumn.align || 'center',
            ...table.firstColumn.bind,
          }"
        >
          <template
            #default="scope"
            v-if="table.firstColumn.type !== 'selection'"
          >
            <el-radio
              v-if="table.firstColumn.type === 'radio'"
              v-model="radioVal"
              :label="scope.$index + 1"
              @click.stop="radioChange($event, scope.row, scope.$index + 1)"
            ></el-radio>
            <template v-if="table.firstColumn.type === 'index'">
              <span v-if="isPaginationCumulative && isShowPagination">
                {{
                  (table.currentPage - 1) * table.pageSize + scope.$index + 1
                }}
              </span>
              <span v-else>{{ scope.$index + 1 }}</span>
            </template>
            <template v-if="table.firstColumn.type === 'expand'">
              <slot name="expand" :scope="scope"></slot>
            </template>
          </template>
        </el-table-column>
      </template>
      <!-- 主体内容 -->
      <template v-for="(item, index) in renderColumns">
        <template v-if="!item.children">
          <!-- 常规列 -->
          <el-table-column
            v-if="item.isShowCol === false ? item.isShowCol : true"
            :key="index + 'i'"
            :type="item.type"
            :label="item.label"
            :prop="item.prop"
            :min-width="item['min-width'] || item.minWidth"
            :width="item.width"
            :sortable="item.sort || sortable"
            :align="item.align || 'center'"
            :fixed="item.fixed"
            :show-overflow-tooltip="
              item.noShowTip === false ? item.noShowTip : true
            "
            v-bind="{ ...item.bind, ...$attrs }"
          >
            <template #header v-if="item.headerRequired || item.renderHeader">
              <render-header
                v-if="item.renderHeader"
                :column="item"
                :render="item.renderHeader"
              />
              <div style="display: inline" v-if="item.headerRequired">
                <span style="color: #f56c6c; fontsize: 16px; marginright: 3px"
                  >*</span
                >
                <span>{{ item.label }}</span>
              </div>
            </template>
            <template #default="scope">
              <!-- render渲染 -->
              <template v-if="item.render">
                <render-col
                  :column="item"
                  :row="scope.row"
                  :render="item.render"
                  :index="scope.$index"
                />
              </template>
              <!-- 自定义插槽 -->
              <template v-if="item.slotName">
                <slot :name="item.slotName" :scope="scope"></slot>
              </template>
              <!-- 单个单元格编辑 -->
              <template v-if="item.canEdit">
                <el-form
                  :model="state.tableData[scope.$index]"
                  :rules="isEditRules ? table.rules : {}"
                  class="t_edit_cell_form"
                  :ref="(el:any) => handleRef(el, scope,item)"
                  @submit.prevent
                >
                  <single-edit-cell
                    :canEdit="item.canEdit"
                    :configEdit="item.configEdit"
                    v-model="scope.row[scope.column.property]"
                    :prop="item.prop"
                    :scope="scope"
                    @handleEvent="handleEvent($event, scope.$index)"
                    @keyup-handle="handleKeyup"
                    v-bind="$attrs"
                    ref="editCell"
                  >
                    <template
                      v-for="(index, name) in slots"
                      v-slot:[name]="data"
                    >
                      <slot :name="name" v-bind="data"></slot>
                    </template>
                  </single-edit-cell>
                </el-form>
              </template>
              <!-- 字典过滤 -->
              <template v-if="item.filters && item.filters.list">
                {{
                  constantEscape(
                    scope.row[item.prop],
                    table.listTypeInfo[item.filters.list],
                    item.filters.key || 'value',
                    item.filters.label || 'label'
                  )
                }}
              </template>
              <div
                v-if="
                  !item.render &&
                  !item.slotName &&
                  !item.canEdit &&
                  !item.filters
                "
              >
                <span>{{ scope.row[item.prop] }}</span>
              </div>
            </template>
          </el-table-column>
        </template>
        <!-- 表头合并单元格 -->
        <t-table-column v-else :key="index + 'm'" :item="item">
          <template v-for="(index, name) in slots" v-slot:[name]="data">
            <slot :name="name" v-bind="data"></slot>
          </template>
        </t-table-column>
      </template>
      <slot></slot>
      <!-- 操作按钮 -->
      <el-table-column
        v-if="table.operator"
        :fixed="table.operatorConfig && table.operatorConfig.fixed"
        :label="(table.operatorConfig && table.operatorConfig.label) || '操作'"
        :min-width="table.operatorConfig && table.operatorConfig.minWidth"
        :width="table.operatorConfig && table.operatorConfig.width"
        :align="
          (table.operatorConfig && table.operatorConfig.align) || 'center'
        "
        v-bind="table.operatorConfig && table.operatorConfig.bind"
        class-name="operator"
      >
        <template #default="scope">
          <div
            class="operator_btn"
            :style="table.operatorConfig && table.operatorConfig.style"
          >
            <template v-for="(item, index) in table.operator" :key="index">
              <el-button
                @click="
                  item.fun && item.fun(scope.row, scope.$index, state.tableData)
                "
                v-bind="{
                  type: 'primary',
                  link: true,
                  text: true,
                  size: 'small',
                  ...item.bind,
                  ...$attrs,
                }"
                v-if="checkIsShow(scope, item)"
              >
                <!-- render渲染 -->
                <template v-if="item.render">
                  <render-col
                    :column="item"
                    :row="scope.row"
                    :render="item.render"
                    :index="scope.$index"
                  />
                </template>
                <span v-if="!item.render">{{ item.text }}</span>
              </el-button>
            </template>
          </div>
        </template>
      </el-table-column>
    </el-table>
    <!-- 分页器 -->
    <el-pagination
      v-if="state.tableData && state.tableData.length && isShowPagination"
      small
      v-model:current-page="table.currentPage"
      @current-change="handlesCurrentChange"
      :page-sizes="[10, 20, 50, 100]"
      v-model:page-size="table.pageSize"
      :layout="table.layout || 'total,sizes, prev, pager, next, jumper'"
      :prev-text="table.prevText"
      :next-text="table.nextText"
      :total="table.total || 0"
      v-bind="$attrs"
      background
    >
      <slot name="pagination"></slot>
    </el-pagination>
    <!-- 表格底部按钮 -->
    <footer
      class="handle_wrap"
      v-if="isShowFooterBtn && state.tableData && state.tableData.length > 0"
    >
      <slot name="footer" />
      <div v-if="!slots.footer">
        <el-button type="primary" @click="save">保存</el-button>
      </div>
    </footer>
  </div>
</template>

<script setup lang="ts" name="TTable">
import { computed, ref, watch, useSlots, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import Sortable from 'sortablejs'
import SingleEditCell from './singleEditCell.vue'
import ColumnSet from './ColumnSet.vue'
import RenderCol from './renderCol.vue'
import RenderHeader from './renderHeader.vue'
import TTableColumn from './TTableColumn.vue'
const props = defineProps({
  // table所需数据
  table: {
    type: Object,
    default: () => {
      return {}
    },
    required: true,
  },
  // 表头数据
  columns: {
    type: Array,
    default: () => {
      return []
    },
    // required: true
  },
  // 按钮权限数据集
  btnPermissions: {
    type: Array,
    default: () => {
      return []
    },
  },
  // 表格标题
  title: {
    type: String,
  },
  // 是否开启Tree-table
  isTree: {
    type: Boolean,
    default: false,
  },
  // 是否开启行拖拽
  isRowSort: {
    type: Boolean,
    default: false,
  },
  // 是否复制单元格
  isCopy: {
    type: Boolean,
    default: false,
  },
  // 是否开启点击整行选中单选框
  rowClickRadio: {
    type: Boolean,
    default: true,
  },
  // 设置默认选中项(单选)defaultRadioCol值必须大于0!
  defaultRadioCol: Number,
  // 序列号显示是否分页累加
  isPaginationCumulative: {
    type: Boolean,
    default: false,
  },
  // 是否显示分页
  isShowPagination: {
    type: Boolean,
    default: true,
  },
  // 是否开启编辑保存按钮
  isShowFooterBtn: {
    type: Boolean,
    default: false,
  },
  // 是否显示设置(隐藏/显示列)
  columnSetting: {
    type: Boolean,
    default: false,
  },
  // 是否高亮选中行
  highlightCurrentRow: {
    type: Boolean,
    default: false,
  },
  // 是否开启合计行隐藏复选框/单选框/序列
  isTableColumnHidden: {
    type: Boolean,
    default: false,
  },
  // 如果设置为 'custom',则代表用户希望远程排序,需要监听 Table 的 sort-change 事件
  sortable: {
    type: [Boolean, String],
  },
  // 单元格编辑是否开启键盘事件
  isKeyup: {
    type: Boolean,
    default: false,
  },
})
// 初始化数据
let state = reactive({
  tableData: props.table.data,
  columnSet: [],
  copyTableData: [], // 键盘事件
})
// 单选框
const radioVal = ref(null)
// 判断单选选中及取消选中
const forbidden = ref(true)
// 获取el-table ref
const TTable: any = ref<HTMLElement | null>(null)
// 获取t-table ref
const TTableBox: any = ref<HTMLElement | null>(null)
// 获取form ref
const formRef: any = ref({})
// 动态ref
const handleRef = (el, scope, item) => {
  if (el) {
    formRef.value[
      `formRef-${scope.$index}-${item.prop || scope.column.property}`
    ] = el
  }
}
// 抛出事件
const emits = defineEmits([
  'save',
  'page-change',
  'handleEvent',
  'radioChange',
  'rowSort',
  'validateError',
])
// 获取所有插槽
const slots = useSlots()
watch(
  () => props.table.data,
  (val) => {
    // console.log(111, val)
    state.tableData = val
  },
  { deep: true }
)
onMounted(() => {
  // console.log('onMounted', props.table.firstColumn)
  // 设置默认选中项(单选)
  if (props.defaultRadioCol) {
    defaultRadioSelect(props.defaultRadioCol)
  }
  initSort()
})
// 默认选中(单选项)---index必须是大于等于1(且只能默认选中第一页的数据)
const defaultRadioSelect = (index) => {
  radioVal.value = index
  emits('radioChange', state.tableData[index - 1], radioVal.value)
}
// 行拖拽
const initSort = () => {
  if (!props.isRowSort) return
  const el = TTableBox.value.querySelector('.el-table__body-wrapper tbody')
  // console.log('3333', el)
  Sortable.create(el, {
    animation: 150, // 动画
    // handle: '.move', // 指定拖拽目标,点击此目标才可拖拽元素(此例中设置操作按钮拖拽)
    // filter: '.disabled', // 指定不可拖动的类名(el-table中可通过row-class-name设置行的class)
    // dragClass: 'dragClass', // 设置拖拽样式类名
    // ghostClass: 'ghostClass', // 设置拖拽停靠样式类名
    // chosenClass: 'chosenClass', // 设置选中样式类名
    onEnd: (evt) => {
      const curRow = state.tableData.splice(evt.oldIndex, 1)[0]
      state.tableData.splice(evt.newIndex, 0, curRow)
      emits('rowSort', state.tableData)
    },
  })
}
// 过滤字典
/**
 * 下拉数据回显中文过滤器
 * @param [String,Number] value 需要转中文的key值
 * @param {String} list  数据源
 * @param [String,Number] key  数据源的key字段(默认:value)
 * @param {String} label  数据源的label字段(默认:label)
 */
const constantEscape = (value, list, key, label) => {
  const res = list.find((item) => {
    return item[key] === value
  })
  return res && res[label]
}
// 单元格编辑是否存在校验
const isEditRules = computed(() => {
  return (
    (props.table.rules && Object.keys(props.table.rules).length > 0) ||
    props.columns.some((item: any) => item?.configEdit?.rules)
  )
})
// 所有列(表头数据)
const renderColumns = computed(() => {
  return state.columnSet.length > 0
    ? state.columnSet.reduce((acc: any, cur: any) => {
        if (!cur.hidden) {
          let columnByProp: any = props.columns.reduce((acc: any, cur: any) => {
            acc[cur.prop] = cur
            return acc
          }, {})
          acc.push(columnByProp[cur.prop])
        }
        // console.log('columnSet', acc)
        return acc
      }, [])
    : props.columns
})
// 判断如果有表头合并就自动开启单元格缩放
const isTableBorder = computed(() => {
  return props.columns.some((item: any) => item.children)
})
// 单元格编辑键盘事件
const handleKeyup = (event, index, key) => {
  if (!props.isKeyup) return
  state.copyTableData = JSON.parse(JSON.stringify(state.tableData))
  // 向上键
  if (event.keyCode === 38) {
    let doms = document.getElementsByClassName(key)
    if (!index) {
      index = state.copyTableData.length
    }
    if (doms.length) {
      let dom
      if (doms[index - 1].getElementsByTagName('input')[0]) {
        dom = doms[index - 1].getElementsByTagName('input')[0]
      } else {
        dom = doms[index - 1].getElementsByTagName('textarea')[0]
      }
      dom.focus()
      // dom.select()
    }
  }
  // 向下键
  if (event.keyCode === 40) {
    let doms = document.getElementsByClassName(key)
    if (+index === state.copyTableData.length - 1) {
      index = -1
    }
    if (doms.length) {
      let dom
      if (doms[index + 1].getElementsByTagName('input')[0]) {
        dom = doms[index + 1].getElementsByTagName('input')[0]
      } else {
        dom = doms[index + 1].getElementsByTagName('textarea')[0]
      }
      dom.focus()
      // dom.select()
    }
  }
  // 回车横向 向右移动
  if (event.keyCode === 13) {
    let keyName = props.columns.map((val: any) => val.prop)
    let num = 0
    if (key === keyName[keyName.length - 1]) {
      if (index === state.copyTableData.length - 1) {
        index = 0
      } else {
        ++index
      }
    } else {
      keyName.map((v, i) => {
        if (v === key) {
          num = i + 1
        }
      })
    }
    let doms = document.getElementsByClassName(keyName[num])
    if (doms.length) {
      let dom
      if (doms[index].getElementsByTagName('input')[0]) {
        dom = doms[index].getElementsByTagName('input')[0]
      } else {
        dom = doms[index].getElementsByTagName('textarea')[0]
      }
      dom.focus()
      // dom.select()
    }
  }
}
// 合并行隐藏复选框/单选框
const cellClassNameFuc = ({ row }) => {
  if (!props.isTableColumnHidden) {
    return false
  }
  if (
    state.tableData.length -
      (state.tableData.length - props.table.pageSize < 0
        ? 1
        : state.tableData.length - props.table.pageSize) <=
    row.rowIndex
  ) {
    return 'table_column_hidden'
  }
}
// forbidden取值(选择单选或取消单选)
const isForbidden = () => {
  forbidden.value = false
  setTimeout(() => {
    forbidden.value = true
  }, 0)
}
// 单选抛出事件radioChange
const radioClick = (row, index) => {
  forbidden.value = !!forbidden.value
  if (radioVal.value) {
    if (radioVal.value === index) {
      radioVal.value = null
      isForbidden()
      // 取消勾选就把回传数据清除
      emits('radioChange', null, radioVal.value)
    } else {
      isForbidden()
      radioVal.value = index
      emits('radioChange', row, radioVal.value)
    }
  } else {
    isForbidden()
    radioVal.value = index
    emits('radioChange', row, radioVal.value)
  }
}
// 点击单选框单元格触发事件
const radioChange = (e, row, index) => {
  if (props.rowClickRadio) {
    return
  }
  e.preventDefault()
  radioClick(row, index)
}
// 点击某行事件
const rowClick = (row) => {
  if (!props.rowClickRadio) {
    return
  }
  radioClick(row, state.tableData.indexOf(row) + 1)
}
// 复制内容
const copyDomText = (val) => {
  // 获取需要复制的元素以及元素内的文本内容
  const text = val
  // 添加一个input元素放置需要的文本内容
  const input = document.createElement('input')
  input.value = text
  document.body.appendChild(input)
  // 选中并复制文本到剪切板
  input.select()
  document.execCommand('copy')
  // 移除input元素
  document.body.removeChild(input)
}
// 双击复制单元格内容
const cellDblclick = (row, column) => {
  if (!props.isCopy) {
    return false
  }
  try {
    copyDomText(row[column.property])
    ElMessage.success('复制成功')
  } catch (e) {
    ElMessage.error('复制失败')
  }
}
// 判断是否使用漏了某个插槽
const isShow = (name) => {
  return Object.keys(slots).includes(name)
}
// 整行编辑返回数据
const save = () => {
  // emits('save', state.tableData)
  // return state.tableData
  if (!isEditRules.value) {
    emits('save', state.tableData)
    return state.tableData
  }
  // 表单规则校验
  let successLength = 0
  let rulesList: any = []
  let rulesError: any = []
  let propError: any = []
  let propLabelError: any = []
  // 获取所有的form ref
  const refList = Object.keys(formRef.value).filter((item) =>
    item.includes('formRef')
  )
  // 获取单独设置规则项
  const arr = renderColumns.value
    .filter((val) => {
      if (val.configEdit?.rules) {
        return val
      }
    })
    .map((item) => item.prop)
  // 获取整体设置规则
  const arr1 = props.table.rules && Object.keys(props.table.rules)
  // 获取最终设置了哪些规则(其值是设置的--prop)
  const newArr = [...arr, ...arr1]
  // 最终需要校验的ref
  newArr.map((val) => {
    refList.map((item: any) => {
      if (item.includes(val)) {
        rulesList.push(item)
      }
    })
  })
  console.log('最终需要校验的数据', rulesList, formRef.value)
  // 表单都校验
  rulesList.map((val) => {
    formRef.value[val].validate((valid) => {
      if (valid) {
        successLength = successLength + 1
      } else {
        rulesError.push(val)
      }
    })
  })
  // 所有表单都校验成功
  setTimeout(() => {
    if (successLength === rulesList.length) {
      if (isEditRules.value) {
        emits('save', state.tableData)
        return state.tableData
      }
    } else {
      // 校验未通过的prop
      rulesError.map((item) => {
        newArr.map((val) => {
          if (item.includes(val)) {
            propError.push(val)
          }
        })
      })
      // 去重获取校验未通过的prop--label
      Array.from(new Set(propError)).map((item) => {
        renderColumns.value.map((val) => {
          if (item === val.prop) {
            propLabelError.push(val.label)
          }
        })
      })
      console.log('校验未通过的prop--label', propLabelError)
      emits('validateError', propLabelError)
    }
  }, 300)
}
// 是否显示表格操作按钮
const checkIsShow = (scope, item) => {
  let isNoshow = false
  if (item.noshow) {
    // 解决双重判断循环递归
    let nushowFun = JSON.parse(JSON.stringify(item.noshow))
    // 双重判断
    nushowFun.map((rs) => {
      rs.isShow =
        typeof rs.val === 'string'
          ? rs.val === 'isHasVal'
            ? scope.row[rs.key]
              ? 'true'
              : 'false'
            : 'true'
          : rs.val.includes(scope.row[rs.key])
          ? 'false'
          : 'true'
    })
    isNoshow = nushowFun.every((key) => {
      return key.isShow === 'true'
    })
  } else {
    isNoshow = true
  }
  // 单独判断
  let isShow = !item.show || item.show.val.includes(scope.row[item.show.key])
  // 按钮权限
  let isPermission = item.hasPermi
    ? props.btnPermissions?.includes(item.hasPermi)
    : true
  // table页面合计
  let totalTxt = Object.values(scope.row).every((key) => {
    return key !== '当页合计'
  })
  // table页面合计
  let totalTxt1 = Object.values(scope.row).every((key) => {
    return key !== '全部合计'
  })
  return (
    isShow &&
    isNoshow &&
    !scope.row[item.field] &&
    (item.isField ? scope.row[item.isField] : true) &&
    totalTxt &&
    totalTxt1 &&
    isPermission
  )
}
// 单个编辑事件
const handleEvent = ({ type, val }, index) => {
  emits('handleEvent', type, val, index)
}
// 当前页码
const handlesCurrentChange = (val) => {
  emits('page-change', val)
}
/**
 * 公共方法
 */
// 清空复选框
const clearSelection = () => {
  return TTable.value.clearSelection()
}
// 返回当前选中的行
const getSelectionRows = () => {
  return TTable.value.getSelectionRows()
}
// 取消某一项选中项
const toggleRowSelection = (row, selected = false) => {
  return TTable.value.toggleRowSelection(row, selected)
}
// 全部选中
const toggleAllSelection = () => {
  return TTable.value.toggleAllSelection()
}
// 用于可扩展的表格或树表格,如果某行被扩展,则切换。 使用第二个参数,您可以直接设置该行应该被扩展或折叠。
const toggleRowExpansion = (row, expanded) => {
  return TTable.value.toggleRowExpansion(row, expanded)
}
// 用于单选表格,设定某一行为选中行, 如果调用时不加参数,则会取消目前高亮行的选中状态。
const setCurrentRow = (row) => {
  return TTable.value.setCurrentRow(row)
}
// 清空排序条件
const clearSort = () => {
  return TTable.value.clearSort()
}
// 传入由columnKey 组成的数组以清除指定列的过滤条件。 如果没有参数,清除所有过滤器
const clearFilter = (columnKey) => {
  return TTable.value.clearFilter(columnKey)
}
//  Table 进行重新布局
const doLayout = (columnKey) => {
  return TTable.value.doLayout(columnKey)
}
//  手动排序表格。 参数 prop 属性指定排序列,order 指定排序顺序。
const sort = (prop: string, order: string) => {
  return TTable.value.sort(prop, order)
}
//  滚动到一组特定坐标。
const scrollTo = (options: any, yCoord: any) => {
  return TTable.value.scrollTo(options, yCoord)
}
//  设置垂直滚动位置
const setScrollTop = (top) => {
  return TTable.value.setScrollTop(top)
}
//  设置水平滚动位置
const setScrollLeft = (left) => {
  return TTable.value.setScrollLeft(left)
}

// 清空校验规则
const clearValidate = () => {
  const refList = Object.keys(formRef.value).filter((item) =>
    item.includes('formRef')
  )
  refList.map((val) => {
    formRef.value[val].clearValidate()
  })
}
// 表单进行重置并移除校验结果
const resetFields = () => {
  const refList = Object.keys(formRef.value).filter((item) =>
    item.includes('formRef')
  )
  refList.map((val) => {
    formRef.value[val].resetFields()
  })
}
// 暴露方法出去
defineExpose({
  clearSelection,
  getSelectionRows,
  toggleRowSelection,
  toggleAllSelection,
  toggleRowExpansion,
  setCurrentRow,
  clearSort,
  clearFilter,
  doLayout,
  sort,
  scrollTo,
  setScrollTop,
  setScrollLeft,
  state,
  radioVal,
  clearValidate,
  resetFields,
  save,
})
</script>
<style lang="scss" scoped>
.t-table {
  z-index: 0;
  background-color: var(--el-bg-color);
  :deep(.el-table__header-wrapper) {
    .el-table__header {
      margin: 0;
    }
  }
  :deep(.el-table__body-wrapper) {
    .el-table__body {
      margin: 0;
    }
  }
  :deep(.el-pagination) {
    display: flex;
    justify-content: flex-end;
    align-items: center;
    margin-top: 20px;
    // margin-right: 60px;
    margin-right: calc(2% - 20px);
    background-color: var(--el-bg-color);
  }
  // ttable过长省略号
  .el-table {
    .el-tooltip {
      div {
        -webkit-box-sizing: border-box;
        box-sizing: border-box;
        overflow: hidden;
        text-overflow: ellipsis;
        word-break: break-all;
        padding-left: 10px;
        padding-right: 10px;
      }
      .single_edit_cell {
        overflow: visible;
      }
    }
  }
  // 某行隐藏复选框/单选框
  .el-table {
    .el-table__row {
      :deep(.table_column_hidden) {
        .cell {
          .el-radio__input,
          .el-checkbox__input {
            display: none;
          }

          & > span {
            display: none;
          }
        }
      }
    }
  }

  .el-table th,
  .el-table td {
    padding: 8px 0;
  }

  .el-table--border th:first-child .cell,
  .el-table--border td:first-child .cell {
    padding-left: 5px;
  }

  .el-table .cell {
    padding: 0 5px;
  }

  .el-table--scrollable-y .el-table__fixed-right {
    right: 8px !important;
  }

  .header_wrap {
    display: flex;
    align-items: center;

    .toolbar_top {
      flex: 0 70%;
      display: flex;
      padding: 10px 0;
      align-items: center;
      justify-content: flex-end;

      .toolbar {
        display: flex;
        justify-content: flex-end;
        width: 100%;
      }

      .el-button--small {
        height: 32px;
      }

      .el-button--success {
        background-color: #355db4;
        border: 1px solid #355db4;
      }
    }

    .header_title {
      display: flex;
      align-items: center;
      flex: 0 30%;
      padding: 10px 0;
      font-size: 16px;
      font-weight: bold;
      line-height: 35px;
      margin-left: 10px;
      color: var(--el-text-color-primary);
    }
  }

  .marginBttom {
    margin-bottom: -8px;
  }

  // 单选样式
  .radioStyle {
    :deep(.el-radio) {
      .el-radio__label {
        display: none;
      }

      &:focus:not(.is-focus):not(:active):not(.is-disabled) .el-radio__inner {
        box-shadow: none;
      }
    }

    :deep(tbody) {
      .el-table__row {
        cursor: pointer;
      }
    }
  }

  // 复制功能样式
  .cursor {
    :deep(tbody) {
      .el-table__row {
        cursor: pointer;
      }
    }
  }
  // 行拖动
  .row_sort {
    :deep(tbody) {
      .el-table__row {
        cursor: move;
      }
    }
  }

  // 每行高度设置
  .el-table {
    .el-table__body {
      .el-table__row {
        :deep(.el-table__cell) {
          padding: 8px 0;

          .cell {
            min-height: 32px;
            line-height: 32px;
            // display: flex;
            // align-items: center;
            // justify-content: center;
          }
        }
      }
    }
  }
  // treeTable样式
  .tree_style {
    :deep(.el-table__body-wrapper) {
      .el-table__body {
        .cell {
          display: flex;
          align-items: center;
          .el-table__expand-icon {
            display: flex;
            align-items: center;
            justify-content: center;
          }
        }
      }
    }
  }
  .operator {
    // 操作样式
    .operator_btn {
      .el-button {
        margin: 0;
        margin-right: 10px;
      }
    }
  }

  // 页面缓存时,表格内操作栏每行高度撑满
  :deep(.el-table__fixed-right) {
    height: 100% !important;
  }

  // 选中行样式
  .highlightCurrentRow {
    :deep(.current-row) {
      color: var(--el-color-primary);
      cursor: pointer;
      background-color: #355db4 !important;
    }
  }

  .el-table--scrollable-y .el-table__body-wrapper {
    overflow-x: auto;
  }

  .handle_wrap {
    position: sticky;
    z-index: 10;
    right: 0;
    bottom: -8px;
    margin: 0 -8px -8px;
    padding: 12px 16px;
    background-color: var(--el-bg-color);
    border-top: 1px solid #ebeef5;
    text-align: right;

    .el-btn {
      margin-left: 8px;
    }
  }
}
</style>


六、组件地址

gitHub组件地址

gitee码云组件地址

vue3+ts基于Element-plus再次封装基础组件文档

七、相关文章

基于ElementUi再次封装基础组件文档


vue+element-ui的table组件二次封装

  • 4
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 7
    评论
好的,我可以为你提供一个基本的ElDialog封装,供你参考: ```vue <template> <el-dialog :title="title" :visible.sync="dialogVisible" :before-close="handleClose" :close-on-click-modal="false" :custom-class="customClass" :width="width" :lock-scroll="lockScroll" :modal-append-to-body="modalAppendToBody" :destroy-on-close="destroyOnClose" :center="center" @opened="handleOpen" @closed="handleClosed" v-bind="$attrs" v-on="$listeners" > <slot></slot> </el-dialog> </template> <script lang="ts"> import { defineComponent } from 'vue'; import { ElDialog } from 'element-plus'; export default defineComponent({ name: 'MyDialog', props: { title: { type: String, default: '', }, dialogVisible: { type: Boolean, default: false, }, customClass: { type: String, default: '', }, width: { type: String, default: '50%', }, lockScroll: { type: Boolean, default: true, }, modalAppendToBody: { type: Boolean, default: true, }, destroyOnClose: { type: Boolean, default: false, }, center: { type: Boolean, default: true, }, }, emits: ['update:dialogVisible', 'opened', 'closed'], methods: { handleClose(done: () => void) { // 自定义关闭操作 done(); }, handleOpen() { this.$emit('opened'); }, handleClosed() { this.$emit('closed'); }, }, components: { ElDialog, }, }); </script> ``` 这里我们使用了Vue3的Composition API,使用`defineComponent`定义了一个组件,并引入了Element Plus的ElDialog组件。 我们将ElDialog组件的属性和事件通过props和emits暴露出来,并在组件内部进行了一些自定义操作,如自定义关闭操作和自定义事件触发。 你可以根据自己的需求对组件进行进一步封装和定制化。
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

wocwin

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

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

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

打赏作者

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

抵扣说明:

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

余额充值