使用tsx封装动态table

纸上得来终觉浅,绝知此事要躬行。一直以来都没用过vuetsx,最近闲来无事,尝试用tsx封装一个动态表格。

技术栈

  • vue3
  • ts
  • element-plus

实现代码

定义数据类型

  • TableColumn: 描述表格项的配置
ts 复制代码
export interface TableColumnI {
  label: string;
  prop: string;
  width?: number | string;
  fixed?: string;
  align?: string;
  visible?: boolean;
  methods?: string;
  render?: RenderI;
  formatter?: string | Function;
  dict?: string   // 字典,如果存在的话会根据字典类型和值转成其描述,如
}
  • RenderI: 自定义表格渲染的内容或组件
ts 复制代码
interface RenderI {
  (row: TableData): VNode | JSX.Element
}
  • TableDataI: 表格行数据类型
ts 复制代码
export interface TableDataI {
  [key: string]: any;
}
  • OptionColumnI: 操作列配置项
ts 复制代码
  label: string;
  label: string;
  width?: number | string;
  fixed?: string;
  align?: string;
  visible?: boolean;
  buttons: OptionButtonI[]
  • OptionButtonI: 操作列中按钮配置项
ts 复制代码
  label: string;
  icon?: string;
  type?: ButtonType;
  method?: Function;
  disabled?: boolean;
  size?: string;
  popconfirm?: boolean;
  group?: OptionButtonI[]

tsx文件

tsx 复制代码
import { defineComponent, ref, type PropType } from 'vue'
import {
  type OptionButtonI,
  type OptionColumnI,
  type TableColumnI,
  type TableDataI
} from '@/types/dtable'
import '@/styles/dtable.scss'
import { formatDate, formatMoney } from '@/utils/format'
import { dictDataLabel } from '@/plugins/DictPlugin'  自定义翻译字典的插件
import Money from '../money'  // 自定义展示金额的组件

export default defineComponent({
  name: 'dtable',
  props: {
    columns: {
      type: Array as PropType<TableColumnI[]>,
      default: () => []
    },
    datas: {
      type: Array as PropType<TableDataI[]>,
      default: () => []
    },
    options: {
      type: Object as PropType<OptionColumnI>,
      default: () => {}
    },
    loading: {
      type: Boolean,
      default: false
    },
    /**
     * 动态绑定 key 值
     */
    keyId: {
      type: String,
      default: 'id'
    },
    /**
     * 行内自定义样式配置
     */
    rowStyle: {
      type: Object,
      default: () => {
        return {
          height: '40px'
        }
      }
    }
  },
  setup(props, ctx) {
    const { emit, expose, slots } = ctx
    const tableRef = ref(null)
    expose({
      tableRef
    })
    return () => (
      <div v-loading={props.loading}>
        <el-table
          class="d-table"
          ref={tableRef}
          data={props.datas}
          border
          row-style={props.rowStyle}
          header-row-class-name="d-table-header"
          row-key={props.keyId}
        >
          {props.columns.map((item: TableColumnI) => {
            if (!item.formatter) {
              // 格式化时间
              if (item.type === 'dateTime') {
                item.formatter = (row: TableDataI, column: TableColumnI, cellValue: string) =>
                  formatDate(cellValue)
              } else if (item.type === 'date') {
                item.formatter = (row: TableDataI, column: TableColumnI, cellValue: string) =>
                  formatDate(cellValue, 'YYYY-MM-DD')
              } else if (item.type === 'money') {
                // 格式化金额
                item.formatter = (
                  row: TableDataI,
                  column: TableColumnI,
                  cellValue: number | bigint
                ) => formatMoney(cellValue)
              }
            }

            // 根据字典类型翻译数据
            if (item.dict) {
              item.formatter = (
                row: TableDataI,
                column: TableColumnI,
                cellValue: string | number | boolean
              ) => {
                if (item.dict) {
                  return dictDataLabel(item.dict, cellValue).value
                }
              }
            }

            return (
              <el-table-column
                key={item.prop}
                width={item.width ?? ''}
                align={item.align ?? 'center'}
                label={item.label}
                fixed={item.fixed}
                prop={item.prop}
                formatter={item.formatter}
                v-slots={{
                  default: (scope: { row: TableDataI }) => {
                    if (item.render) {
                      return item.render(scope.row)
                    } else {
                      if (item.type === 'money') {
                        // tablecolumn的类型时'money'时使用自定义的组件显示
                        return <Money money={scope.row[item.prop]}></Money>
                      }
                    }
                  }
                }}
              ></el-table-column>
            )
          })}
          {props.options && (
            <el-table-column
              width={props.options.width}
              label={props.options.label}
              fixed={props.options.fixed}
              align={props.options.align ?? 'center'}
              v-slots={{
                default: (scope: { row: TableDataI }) => {
                  return (
                    props.options.buttons && (
                      <div class="flex-box">
                        {props.options.buttons.map((button: OptionButtonI) => {
                          if (button.group) {
                            return (
                              <el-button
                                size={button.size}
                                type={button.type ?? 'default'}
                                icon={button.icon}
                                disabled={button.disabled}
                                v-slots={{
                                  default: () => {
                                    const popRef = ref()
                                    return (
                                      <el-dropdown
                                        trigger="click"
                                        hide-on-click={false}
                                        ref={popRef}
                                        id={scope.row.id}
                                        v-slots={{
                                          dropdown: () => {
                                            return (
                                              <el-dropdown-menu>
                                                {button.group?.map((item1) => {
                                                  if (item1.type === 'danger' && item1.popconfirm) {
                                                    return (
                                                      <el-dropdown-item>
                                                        <el-popconfirm
                                                          title="确定删除这个条数据吗?"
                                                          onConfirm={() => {
                                                            if (item1.method) {
                                                              item1.method(scope.row)
                                                            }
                                                            popRef.value.handleClose()
                                                          }}
                                                          onCancel={() => {
                                                            popRef.value.handleClose()
                                                          }}
                                                          v-slots={{
                                                            reference: () => {
                                                              return (
                                                                <el-button
                                                                  size={item1.size}
                                                                  type={item1.type ?? 'default'}
                                                                  icon={item1.icon}
                                                                  disabled={item1.disabled}
                                                                >
                                                                  {item1.label}
                                                                </el-button>
                                                              )
                                                            }
                                                          }}
                                                        ></el-popconfirm>
                                                      </el-dropdown-item>
                                                    )
                                                  } else {
                                                    return (
                                                      <el-dropdown-item>
                                                        <el-button
                                                          size={item1.size}
                                                          type={item1.type ?? 'default'}
                                                          icon={item1.icon}
                                                          disabled={item1.disabled}
                                                          onClick={() => {
                                                            if (item1.method) {
                                                              return item1.method(scope.row)
                                                            }
                                                          }}
                                                        >
                                                          {item1.label}
                                                        </el-button>
                                                      </el-dropdown-item>
                                                    )
                                                  }
                                                })}
                                              </el-dropdown-menu>
                                            )
                                          }
                                        }}
                                      >
                                        <span class="el-dropdown-link">
                                          {button.label}
                                          <el-icon class="el-icon--right">
                                            <arrow-down />
                                          </el-icon>
                                        </span>
                                      </el-dropdown>
                                    )
                                  }
                                }}
                              ></el-button>
                            )
                          }

                          if (button.type === 'danger' && button.popconfirm) {
                            return (
                              <el-popconfirm
                                title="确定删除这个条数据吗?"
                                onConfirm={() => button.method(scope.row)}
                                v-slots={{
                                  reference: () => {
                                    return (
                                      <el-button
                                        size={button.size}
                                        type={button.type ?? 'default'}
                                        icon={button.icon}
                                        disabled={button.disabled}
                                      >
                                        {button.label}
                                      </el-button>
                                    )
                                  }
                                }}
                              ></el-popconfirm>
                            )
                          } else {
                            return (
                              <el-button
                                size={button.size}
                                type={button.type ?? 'default'}
                                icon={button.icon}
                                disabled={button.disabled}
                                onClick={() => button.method(scope.row)}
                              >
                                {button.label}
                              </el-button>
                            )
                          }
                        })}
                      </div>
                    )
                  )
                }
              }}
            ></el-table-column>
          )}
        </el-table>
      </div>
    )
  }
})

使用方式

vue 复制代码
<dtable :columns="columns" :datas="tableDatas" :options="options" />
ts 复制代码
const columns: TableColumn[] = [
  {
    label: '名称',
    prop: 'name'
  },
  {
    label: '类型',
    prop: 'type'
  },
  {
    label: '更新时间',
    prop: 'updateTime',
    type: 'date'
  },
  {
    label: '系统内置',
    prop: 'system',
    dict: 'YesOrNo'
  },
  {
    label: '状态',
    prop: 'status',
    dict: 'AvailableStatus'
  }
]

const options: OptionColumn = {
  label: '操作',
  width: '320px',
  buttons: [
    {
      label: '编辑',
      type: 'primary',
      method: (row: any) => {
        dialogObj.dialogVisible = true
        dialogObj.dialogTitle = '编辑字典'
        dictId.value = row.id
      }
    },
    {
      label: '字典项',
      type: 'warning',
      method: (row: any) => {
        itemDrawerVisible.value = true
        dictId.value = row.id
      }
    },
    {
      label: '更多操作',
      group: [
        {
          label: '删除',
          type: 'danger',
          popconfirm: true,
          method: (row: any) => {
            delDictApi(row.id).then(() => {
              handleQuery()
            })
          }
        }
      ]
    }
  ]
}

let tableDatas = ref([])
const loading = ref(false)

效果

遇到的问题

  1. 绑定事件 绑定事件时需要加on, 如给<el-button> 绑定click事件时需要使用onClick, 给<el-popconfirm>组件绑定confirm事件时需要使用onConfirm
  1. 向插槽提供内容时使用v-slotdefault插槽提供内容时如下:
相关推荐
炒毛豆1 小时前
vue3.4中的v-model的用法~
前端·vue.js
阳火锅2 小时前
都2025年了,来看看前端如何给刘亦菲加个水印吧!
前端·vue.js·面试
夕水2 小时前
ew-vue-component:Vue 3 动态组件渲染解决方案的使用介绍
前端·vue.js
codehub3 小时前
TypeScript 高频面试题与核心知识总结
typescript
我麻烦大了3 小时前
实现一个简单的Vue响应式
前端·vue.js
aklry3 小时前
uniapp三步完成一维码的生成
前端·vue.js
张志鹏PHP全栈4 小时前
TypeScript 第一天,认识TypeScript
typescript
用户26124583401615 小时前
vue学习路线(11.watch对比computed)
前端·vue.js
阑梦清川6 小时前
Java后端项目前端基础Vue(二)
vue.js
雪碧聊技术6 小时前
深入解析Vue中v-model的双向绑定实现原理
前端·javascript·vue.js·v-model