纸上得来终觉浅,绝知此事要躬行
。一直以来都没用过vue
的tsx
,最近闲来无事,尝试用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)
效果
遇到的问题
- 绑定事件 绑定事件时需要加
on
, 如给<el-button>
绑定click
事件时需要使用onClick
, 给<el-popconfirm>
组件绑定confirm
事件时需要使用onConfirm
。
- 向插槽提供内容时使用
v-slot
向default
插槽提供内容时如下: