2025-10-30 16:34:29 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div
|
|
|
|
|
|
ref="containerRef"
|
|
|
|
|
|
class="dynamic-table-container"
|
|
|
|
|
|
:class="{ 'auto-height-enabled': autoHeight }"
|
|
|
|
|
|
>
|
|
|
|
|
|
<!-- 工具栏 -->
|
|
|
|
|
|
<TableToolbar
|
|
|
|
|
|
v-if="toolbar"
|
|
|
|
|
|
:buttons="toolbar.buttons"
|
|
|
|
|
|
:selected-keys="selectedRowKeys"
|
|
|
|
|
|
:data-source="dataSource"
|
|
|
|
|
|
:align="toolbar.align"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 表格 - 直接透传所有原生属性 -->
|
|
|
|
|
|
<el-table
|
|
|
|
|
|
ref="tableRef"
|
|
|
|
|
|
:data="dataSource"
|
|
|
|
|
|
:height="computedHeight"
|
|
|
|
|
|
v-bind="$attrs"
|
|
|
|
|
|
@selection-change="handleSelectionChange"
|
|
|
|
|
|
>
|
|
|
|
|
|
<!-- 渲染列 -->
|
|
|
|
|
|
<el-table-column
|
|
|
|
|
|
v-for="(col, idx) in processedColumns"
|
|
|
|
|
|
:key="col.prop || col.label || idx"
|
|
|
|
|
|
v-bind="getColumnProps(col)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<!-- 自定义渲染(render 或 slot) -->
|
|
|
|
|
|
<template v-if="col.render || col.slot" #default="scope">
|
2025-10-30 17:08:19 +08:00
|
|
|
|
<component
|
|
|
|
|
|
v-if="col.render"
|
|
|
|
|
|
:is="col.render(scope.row, scope.column, scope.$index)"
|
|
|
|
|
|
/>
|
2025-10-30 16:34:29 +08:00
|
|
|
|
<slot v-else-if="col.slot" :name="col.slot" v-bind="scope" />
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 透传所有插槽 -->
|
2025-10-30 17:08:19 +08:00
|
|
|
|
<template v-for="name in Object.keys($slots)" #[name]="scope">
|
|
|
|
|
|
<slot :name="name" v-bind="typeof scope === 'object' ? scope : {}" />
|
2025-10-30 16:34:29 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 分页 -->
|
|
|
|
|
|
<el-pagination
|
|
|
|
|
|
v-if="pagination"
|
|
|
|
|
|
:current-page="pagination.current"
|
|
|
|
|
|
:page-size="pagination.pageSize"
|
|
|
|
|
|
:total="pagination.total"
|
|
|
|
|
|
:page-sizes="pagination.pageSizes || [10, 20, 50, 100]"
|
|
|
|
|
|
:layout="pagination.layout || 'total, sizes, prev, pager, next, jumper'"
|
|
|
|
|
|
@current-change="handlePageChange"
|
|
|
|
|
|
@size-change="handleSizeChange"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
2025-10-30 17:08:19 +08:00
|
|
|
|
import { ref, computed } from "vue";
|
|
|
|
|
|
import TableToolbar from "./TableToolbar.vue";
|
|
|
|
|
|
import { useAutoHeight } from "./useAutoHeight";
|
2025-10-30 16:34:29 +08:00
|
|
|
|
|
|
|
|
|
|
defineOptions({
|
2025-10-30 17:08:19 +08:00
|
|
|
|
name: "DynamicTable",
|
|
|
|
|
|
inheritAttrs: false, // 手动控制属性透传
|
|
|
|
|
|
});
|
2025-10-30 16:34:29 +08:00
|
|
|
|
|
|
|
|
|
|
const props = defineProps({
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 数据源
|
|
|
|
|
|
*/
|
|
|
|
|
|
dataSource: {
|
|
|
|
|
|
type: Array,
|
2025-10-30 17:08:19 +08:00
|
|
|
|
default: () => [],
|
2025-10-30 16:34:29 +08:00
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 列配置
|
|
|
|
|
|
*/
|
|
|
|
|
|
columns: {
|
|
|
|
|
|
type: Array,
|
2025-10-30 17:08:19 +08:00
|
|
|
|
default: () => [],
|
2025-10-30 16:34:29 +08:00
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 工具栏配置
|
|
|
|
|
|
*/
|
|
|
|
|
|
toolbar: {
|
|
|
|
|
|
type: Object,
|
2025-10-30 17:08:19 +08:00
|
|
|
|
default: null,
|
2025-10-30 16:34:29 +08:00
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 是否启用自适应高度
|
|
|
|
|
|
*/
|
|
|
|
|
|
autoHeight: {
|
|
|
|
|
|
type: Boolean,
|
2025-10-30 17:08:19 +08:00
|
|
|
|
default: false,
|
2025-10-30 16:34:29 +08:00
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 分页配置(false 表示不分页)
|
|
|
|
|
|
*/
|
|
|
|
|
|
pagination: {
|
|
|
|
|
|
type: [Object, Boolean],
|
2025-10-30 17:08:19 +08:00
|
|
|
|
default: false,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
2025-10-30 16:34:29 +08:00
|
|
|
|
|
2025-10-30 17:08:19 +08:00
|
|
|
|
const emit = defineEmits(["selection-change"]);
|
2025-10-30 16:34:29 +08:00
|
|
|
|
|
2025-10-30 17:08:19 +08:00
|
|
|
|
const containerRef = ref(null);
|
|
|
|
|
|
const tableRef = ref(null);
|
|
|
|
|
|
const selectedRowKeys = ref([]);
|
2025-10-30 16:34:29 +08:00
|
|
|
|
|
|
|
|
|
|
// ==================== 自适应高度 ====================
|
|
|
|
|
|
const { tableHeight } = useAutoHeight(containerRef, {
|
|
|
|
|
|
enabled: props.autoHeight,
|
2025-10-30 17:08:19 +08:00
|
|
|
|
minHeight: 200,
|
|
|
|
|
|
});
|
2025-10-30 16:34:29 +08:00
|
|
|
|
|
|
|
|
|
|
const computedHeight = computed(() => {
|
2025-10-30 17:08:19 +08:00
|
|
|
|
return props.autoHeight ? tableHeight.value : undefined;
|
|
|
|
|
|
});
|
2025-10-30 16:34:29 +08:00
|
|
|
|
|
|
|
|
|
|
// ==================== 列配置处理 ====================
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 处理列配置,设置合理的默认值
|
|
|
|
|
|
* 原则: 仅设置默认值,不改变结构,用户配置优先
|
|
|
|
|
|
*/
|
|
|
|
|
|
const processedColumns = computed(() => {
|
2025-10-30 17:08:19 +08:00
|
|
|
|
return props.columns.map((col) => {
|
2025-10-30 16:34:29 +08:00
|
|
|
|
// 如果是特殊列类型(selection/index/expand),直接返回
|
2025-10-30 17:08:19 +08:00
|
|
|
|
if (col.type) return col;
|
2025-10-30 16:34:29 +08:00
|
|
|
|
|
|
|
|
|
|
// 为普通列添加默认值
|
|
|
|
|
|
return {
|
2025-10-30 17:08:19 +08:00
|
|
|
|
showOverflowTooltip: true, // 默认启用省略提示
|
|
|
|
|
|
align: "center", // 默认居中对齐
|
2025-10-30 16:34:29 +08:00
|
|
|
|
minWidth: col.width ? undefined : 100, // 无固定宽度时设置最小宽度
|
2025-10-30 17:08:19 +08:00
|
|
|
|
...col, // 用户配置覆盖默认值
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
2025-10-30 16:34:29 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取列的 props(用于 v-bind)
|
|
|
|
|
|
* 过滤掉自定义属性 (render, slot)
|
|
|
|
|
|
*/
|
|
|
|
|
|
const getColumnProps = (col) => {
|
2025-10-30 17:08:19 +08:00
|
|
|
|
const { render, slot, ...restProps } = col;
|
|
|
|
|
|
return restProps;
|
|
|
|
|
|
};
|
2025-10-30 16:34:29 +08:00
|
|
|
|
|
|
|
|
|
|
// ==================== 事件处理 ====================
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 处理行选择改变
|
|
|
|
|
|
*/
|
|
|
|
|
|
const handleSelectionChange = (selection) => {
|
|
|
|
|
|
// 提取选中行的 key
|
2025-10-30 17:08:19 +08:00
|
|
|
|
const rowKey = props.$attrs?.rowKey || props.$attrs?.["row-key"] || "id";
|
|
|
|
|
|
selectedRowKeys.value = selection.map((row) =>
|
|
|
|
|
|
typeof rowKey === "function" ? rowKey(row) : row[rowKey]
|
|
|
|
|
|
);
|
2025-10-30 16:34:29 +08:00
|
|
|
|
|
2025-10-30 17:08:19 +08:00
|
|
|
|
emit("selection-change", selection);
|
|
|
|
|
|
};
|
2025-10-30 16:34:29 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 处理分页页码改变
|
|
|
|
|
|
*/
|
|
|
|
|
|
const handlePageChange = (page) => {
|
2025-10-30 17:08:19 +08:00
|
|
|
|
props.pagination?.onChange?.(page, props.pagination.pageSize);
|
|
|
|
|
|
};
|
2025-10-30 16:34:29 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 处理每页条数改变
|
|
|
|
|
|
*/
|
|
|
|
|
|
const handleSizeChange = (size) => {
|
|
|
|
|
|
// 每页条数改变时,重置到第一页
|
2025-10-30 17:08:19 +08:00
|
|
|
|
props.pagination?.onChange?.(1, size);
|
|
|
|
|
|
};
|
2025-10-30 16:34:29 +08:00
|
|
|
|
|
|
|
|
|
|
// ==================== 暴露实例 ====================
|
|
|
|
|
|
defineExpose({
|
|
|
|
|
|
/**
|
|
|
|
|
|
* el-table 实例引用
|
|
|
|
|
|
*/
|
|
|
|
|
|
tableRef,
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 已选中的行 key 数组
|
|
|
|
|
|
*/
|
|
|
|
|
|
selectedRowKeys,
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 手动重新计算表格高度
|
|
|
|
|
|
*/
|
|
|
|
|
|
recalculate: () => {
|
|
|
|
|
|
if (props.autoHeight) {
|
|
|
|
|
|
setTimeout(() => {
|
2025-10-30 17:08:19 +08:00
|
|
|
|
tableHeight.recalculate?.();
|
|
|
|
|
|
}, 100);
|
2025-10-30 16:34:29 +08:00
|
|
|
|
}
|
2025-10-30 17:08:19 +08:00
|
|
|
|
},
|
|
|
|
|
|
});
|
2025-10-30 16:34:29 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.dynamic-table-container {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.auto-height-enabled {
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-pagination {
|
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|