vue3封装table组件及属性介绍
组件封装GxTable.vue
<!-- 表格 -->
<template><div class="gx-table"><div class="gx-table-header" :class="{ 'gx-table-header-small': props.size === 'small' }"><div class="gx-table-row-item" v-if="props.select === 'checkBox'"><gx-checkbox v-model="checkAll" :indeterminate="indeterminate" /></div><div class="gx-table-header-item" v-if="props.select === 'radio'" style="width: 32px"></div><divclass="gx-table-header-item"v-for="(column, index) in props.columns":key="index":style="{ width: column.width }"><div class="divider" v-if="index !== 0"></div><div class="text">{{ column.label }}</div><div class="sort" v-if="column.sortable"><gx-popoverplacement="top":content="sortKey === column.prop && sortDirection === 'asc' ? '取消排序' : '点击升序'"><template #reference><divclass="arrow-up"@click="handleSort(column.prop, 'asc')":class="{ asc: sortKey === column.prop && sortDirection === 'asc' }"></div></template></gx-popover><gx-popoverplacement="top":content="sortKey === column.prop && sortDirection === 'desc' ? '取消排序' : '点击降序'"><template #reference><divclass="arrow-down"@click="handleSort(column.prop, 'desc')":class="{ desc: sortKey === column.prop && sortDirection === 'desc' }"></div></template></gx-popover></div></div><div class="gx-table-header-item" v-if="props.action" :style="{ width: props.actionWidth }"><div class="divider" v-if="props.columns.length > 0"></div><div class="text">操作</div></div></div><divclass="gx-table-body":style="bodyHeight ? { maxHeight: bodyHeight, overflowY: 'auto' } : {}"><template v-if="sortedData.length > 0"><divclass="gx-table-row"v-for="(row, index) in sortedData":key="index":class="{'selected-row': isRowSelected(row),'row-selected': row.selected,'gx-table-row-small': props.size === 'small',}"@click="handleRowClick(row)"><div class="gx-table-row-item" v-if="props.select === 'checkBox'"><gx-checkbox:model-value="innerSelectedKeys.includes(row.id)"@click.stop@change="toggleRowSelection(row)"/></div><div class="gx-table-row-item" v-if="props.select === 'radio'"><gx-radio v-model="selectedRowId" :value="row.id" @click.stop="handleRowClick(row)" /></div><divclass="gx-table-row-item"v-for="(column, columnIndex) in props.columns":key="columnIndex":style="{width: column.width,paddingLeft: props.noCellPadding?.includes(column.slot || column.prop)? columnIndex === 0? '3px': '0px': undefined,}"@click="handleCell(row[column.prop])"><template v-if="column.slot"><slot :name="column.slot" :row="row"></slot></template><template v-else><gx-popoverstyle="width: 100%"v-if="column.showOverflowTooltip":content="row[column.prop]"><template #reference><div class="text-ellipsis">{{ row[column.prop] }}</div></template></gx-popover><template v-else>{{ row[column.prop] }}</template></template></div><divclass="gx-table-row-item action-column"v-if="props.action":style="{ width: props.actionWidth }"><div class="action-buttons"><gx-button type="link" @click.stop="handleEdit(row)">编辑</gx-button><gx-button type="link" @click.stop="handleDelete(row)">删除</gx-button></div></div></div></template><template v-else><div class="gx-table-empty" :style="{ height: bodyHeight }">暂无数据 </div></template></div></div><div class="add" @click="handleAdd" v-if="showAdd"><div class="add-icon"></div><div class="add-text">添加</div></div>
</template><script lang="ts" setup>import { ref, computed, watch } from 'vue';type TableSize = 'default' | 'small';type Select = 'default' | 'radio' | 'checkBox';interface TableColumn {label: string;prop: string;width: string;sortable?: boolean;slot?: string;showOverflowTooltip?: boolean;}interface TableRow {id: string | number;[key: string]: any;}interface Props {modelValue: TableRow[];columns: TableColumn[];select?: Select;size?: TableSize;action?: boolean;actionWidth?: string;noCellPadding?: string[];bodyHeight?: string;selectedKeys?: (string | number)[];showAdd?: boolean;pageSize?: number; // 每页条数currentPage?: number; // 当前页}const emit = defineEmits<{(e: 'row-selected', val: TableRow | null): void;(e: 'selection', val: (string | number)[]): void;(e: 'update:selectedKeys', val: (string | number)[]): void;(e: 'add'): void;(e: 'edit', val: TableRow): void;(e: 'delete', val: TableRow): void;(e: 'copy', val: string): void;}>();const props = withDefaults(defineProps<Props>(), {columns: () => [],modelValue: () => [],select: 'default',size: 'default',actionWidth: '100px',noCellPadding: () => [],selectedKeys: () => [],showAdd: false,pageSize: 10,currentPage: 1,});const sortKey = ref('');const sortDirection = ref<'asc' | 'desc' | ''>('');const selectedRowId = ref<string | number | null>(null);// 内部双向绑定选中行const innerSelectedKeys = ref<(string | number)[]>([...(props.selectedKeys || [])]);watch(() => props.selectedKeys,(val) => {innerSelectedKeys.value = [...(val || [])];},{ immediate: true });/** 排序数据 */const sortedData = computed(() => {if (!sortKey.value) return props.modelValue;return [...props.modelValue].sort((a, b) => {const aVal = a[sortKey.value];const bVal = b[sortKey.value];if (aVal < bVal) return sortDirection.value === 'asc' ? -1 : 1;if (aVal > bVal) return sortDirection.value === 'asc' ? 1 : -1;return 0;});});/** 排序点击 */const handleSort = (prop: string, direction: 'asc' | 'desc') => {if (sortKey.value === prop && sortDirection.value === direction) {sortKey.value = '';sortDirection.value = '';} else {sortKey.value = prop;sortDirection.value = direction;}};/** 是否选中行 */const isRowSelected = (row: TableRow) => {return selectedRowId.value !== null && row.id === selectedRowId.value;};/** 点击行 */const handleRowClick = (row: TableRow) => {if (props.select === 'radio') {selectedRowId.value = selectedRowId.value === row.id ? null : row.id;innerSelectedKeys.value = selectedRowId.value !== null ? [selectedRowId.value] : [];emit('row-selected', selectedRowId.value ? row : null);emit('update:selectedKeys', innerSelectedKeys.value);}if (props.select === 'checkBox') {toggleRowSelection(row);}};/** 多选勾选行 */const toggleRowSelection = (row: TableRow) => {const idx = innerSelectedKeys.value.indexOf(row.id);if (idx >= 0) {innerSelectedKeys.value.splice(idx, 1);} else {innerSelectedKeys.value.push(row.id);}// 触发事件,通知父组件emit('selection', [...innerSelectedKeys.value]);emit('update:selectedKeys', [...innerSelectedKeys.value]);};/** 当前页 id,用于表头全选 */const currentPageIds = computed(() => {const start = (props.currentPage! - 1) * props.pageSize!;const end = props.currentPage! * props.pageSize!;return props.modelValue.slice(start, end).map((row) => row.id);});/** 表头全选状态(只作用于当前页) */const checkAll = computed({get() {const selectedOnPage = currentPageIds.value.filter((id) =>innerSelectedKeys.value.includes(id));return (selectedOnPage.length === currentPageIds.value.length && currentPageIds.value.length > 0);},set(val: boolean) {if (val) {// 当前页全选const newIds = currentPageIds.value.filter((id) => !innerSelectedKeys.value.includes(id));innerSelectedKeys.value = [...innerSelectedKeys.value, ...newIds];} else {// 取消当前页全选innerSelectedKeys.value = innerSelectedKeys.value.filter((id) => !currentPageIds.value.includes(id));}emit('selection', [...innerSelectedKeys.value]);emit('update:selectedKeys', [...innerSelectedKeys.value]);},});/** 当前页半选状态 */const indeterminate = computed(() => {const selectedOnPage = currentPageIds.value.filter((id) =>innerSelectedKeys.value.includes(id));return selectedOnPage.length > 0 && selectedOnPage.length < currentPageIds.value.length;});const handleAdd = () => emit('add');const handleEdit = (row: TableRow) => emit('edit', row);const handleDelete = (row: TableRow) => emit('delete', row);const handleCell = (text: string) => emit('copy', text);
</script><style scoped>.gx-table {display: flex;flex-direction: column;}.gx-table-header {display: flex;flex-direction: row;align-items: center;background-color: var(--gx-color-border-divider-2);height: 32px;flex-shrink: 0;}.gx-table-header-small {height: 26px;}.gx-table-header.fixed {position: sticky;top: 0;z-index: 1;}.gx-table-header-item {display: flex;align-items: center;color: var(--gx-color-text-secondary);}.gx-table-header-item .text {padding-left: 8px;box-sizing: border-box;font-weight: 600;}.gx-table-header-item:first-child .text {padding-left: 12px;box-sizing: border-box;}.sort {display: flex;flex-direction: column;align-items: center;justify-content: center;width: 8px;height: 16px;gap: 2px;padding: 4px;}.arrow-up,.arrow-down {width: 9px;height: 5px;}.arrow-up:hover,.arrow-down:hover {cursor: pointer;}.arrow-up {background: url('../../assets/img/components/data/gx-table/arrow-up.svg') center no-repeat;}.arrow-down {background: url('../../assets/img/components/data/gx-table/arrow-down.svg') center no-repeat;}.arrow-up:hover,.arrow-up.asc {background: url('../../assets/img/components/data/gx-table/arrow-up-active.svg') centerno-repeat;}.arrow-down:hover,.arrow-down.desc {background: url('../../assets/img/components/data/gx-table/arrow-down-active.svg') centerno-repeat;}.gx-table-row {display: flex;flex-direction: row;align-items: center;border-bottom: 1px solid var(--gx-color-border-divider-1);height: 40px;}.gx-table-row-small {height: 30px;}.gx-table-row:hover {background-color: var(--gx-color-bg-table-hover);}.gx-table-row-item {height: 100%;min-width: 32px;padding-left: 9px;box-sizing: border-box;display: flex;align-items: center;color: var(--gx-color-text-brand);}.gx-table-row-item:first-child {padding-left: 12px;box-sizing: border-box;}.selected-row {background-color: var(--gx-color-bg-table-selected);}.row-selected {background-color: var(--gx-color-bg-table-selected);}.add {display: flex;align-items: center;padding: 10px 0;gap: 5px;width: 50px;cursor: pointer;}.add-icon {position: relative;width: 14px;height: 14px;background: url('../../assets/img/components/data/gx-table/add.svg') center no-repeat;}.add:hover .add-icon {background: url('../../assets/img/components/data/gx-table/add-hover.svg') center no-repeat;}.add:hover .add-text {color: var(--gx-color-primary-default);}.add:hover .add-icon::after {background-color: var(--gx-color-primary-default);}.add-text {color: var(--gx-color-text-placeholder);}.text-ellipsis {white-space: nowrap;overflow: hidden;text-overflow: ellipsis;position: relative;}.divider {background-color: var(--gx-color-border-default);height: 14px;width: 1px;}.action-buttons {display: flex;gap: 10px;}.gx-table {display: flex;flex-direction: column;}.gx-table-header.fixed {position: sticky;top: 0;z-index: 10;background-color: var(--gx-color-border-divider-2);}.gx-table-body {overflow-y: auto;}.gx-table-empty {display: flex;align-items: center;justify-content: center;color: var(--gx-color-text-placeholder);}
</style>
使用方法示例:
1、多选带分页(表头的复选框只是选中当前页面的全部行,不是所有表格数据)
<template><div><gx-table:columns="tableHeader":model-value="tableRow"v-model:selectedKeys="checkedIds"select="checkBox"@selection="handleSelection"/></div></template>
<script setup lang="ts"> const handleSelection = (selectedIds: number[]) => {console.log('选中的行id:', selectedIds);};
</script>
表格属性介绍:
const attrData = ref<any[]>([{attrName: 'columns',brief: '表格的列信息',type: `TableColumn[]`,default: '-',},{attrName: 'model-value',brief: '表格的数据源',type: `TableRow[]`,default: '-',},{attrName: 'size',brief: '表格尺寸',type: `enum,'default' | 'small'`,default: 'default',},{attrName: 'select',brief: '控制表格的选择模式',type: `enum,'default' | 'radio' | 'chechBox'`,default: 'default',},{attrName: 'label',brief: '表格列标题文本',type: `string`,default: '-',},{attrName: 'prop',brief: '对应表格数据(model-value)中每一行对象的属性名,用于获取该行的具体数据。',type: `string`,default: '-',},{attrName: 'width',brief: '表格列的宽度。',type: `string`,default: '-',},{attrName: 'sortable',brief: '对应列是否可以进行排序。',type: `boolean`,default: '-',},{attrName: 'slot',brief: '替代 prop 的默认渲染。',type: `boolean`,default: '-',},{attrName: 'noCellPadding',brief: '指定需要去除padding-left的列',type: `string[]`,default: '[]',},{attrName: 'bodyHeight',brief: '表格内容区域的高度,超出该高度时会显示竖向滚动条。',type: `string`,default: '-',},{attrName: 'v-model:checkedKeys',brief: '默认选中节点的 id,仅在多选模式下有效',type: 'Array<string | number>',default: '[]',},]);const attrColumnData = ref<any[]>([{attrName: 'label',brief: '表格列标题文本',type: `string`,default: '-',},{attrName: 'prop',brief: '对应表格数据(model-value)中每一行对象的属性名,用于获取该行的具体数据。',type: `string`,default: '-',},{attrName: 'width',brief: '表格列的宽度。',type: `string`,default: '-',},{attrName: 'sortable',brief: '对应列是否可以进行排序。',type: `boolean`,default: '-',},{attrName: 'slot',brief: '替代 prop 的默认渲染。',type: `boolean`,default: '-',},]);const slotData = ref<any[]>([{slotName: 'column.slot(动态名称)',brief: '列的自定义内容插槽,名称与 columns 中配置的 slot 值对应,如 "action"',},]);const eventData = ref<any[]>([{attrName: 'click',brief: '原生 DOM 元素的点击监听和组件内部自定义行为的触发',type: `Function`,},{attrName: 'row-selected',brief: '单选模式下,行选中状态变化时触发的事件',type: `Function`,},{attrName: 'selection',brief: '多选模式下,选中项变化时触发的事件',type: `Function`,},]);
