Files
front/src/components/data-table/index.vue

328 lines
8.5 KiB
Vue
Raw Normal View History

2026-03-07 20:11:25 +08:00
<template>
<div class="data-table">
<!-- 工具栏 -->
<a-row v-if="showToolbar" style="margin-bottom: 16px">
<a-col :span="12">
<a-space>
<slot name="toolbar-left" />
</a-space>
</a-col>
<a-col :span="12" style="display: flex; align-items: center; justify-content: end">
<slot name="toolbar-right" />
<a-button v-if="showDownload" @click="handleDownload">
<template #icon>
<icon-download />
</template>
{{ downloadButtonText }}
</a-button>
<a-tooltip v-if="showRefresh" :content="refreshTooltipText">
<div class="action-icon" @click="handleRefresh"><icon-refresh size="18" /></div>
</a-tooltip>
<a-dropdown v-if="showDensity" @select="handleSelectDensity">
<a-tooltip :content="densityTooltipText">
<div class="action-icon"><icon-line-height size="18" /></div>
</a-tooltip>
<template #content>
<a-doption v-for="item in densityList" :key="item.value" :value="item.value" :class="{ active: item.value === size }">
<span>{{ item.name }}</span>
</a-doption>
</template>
</a-dropdown>
<a-tooltip v-if="showColumnSetting" :content="columnSettingTooltipText">
<a-popover trigger="click" position="bl" @popup-visible-change="popupVisibleChange">
<div class="action-icon"><icon-settings size="18" /></div>
<template #content>
<div ref="columnSettingRef" class="column-setting-container">
<div v-for="(item, idx) in showColumns" :key="item.dataIndex" class="setting">
<div style="margin-right: 4px; cursor: move">
<icon-drag-arrow />
</div>
<div>
<a-checkbox v-model="item.checked" @change="handleChange($event, item, idx)"></a-checkbox>
</div>
<div class="title">
{{ item.title === '#' ? '序列号' : item.title }}
</div>
</div>
</div>
</template>
</a-popover>
</a-tooltip>
</a-col>
</a-row>
<!-- 表格 -->
<a-table
row-key="id"
:loading="loading"
:pagination="pagination"
:columns="cloneColumns"
:data="data"
:bordered="bordered"
:size="size"
2026-03-21 17:39:39 +08:00
:row-selection="rowSelection"
:scroll="scroll"
2026-03-07 20:11:25 +08:00
@page-change="onPageChange"
2026-03-21 17:39:39 +08:00
@page-size-change="onPageSizeChange"
@selection-change="onSelectionChange"
@row-click="onRowClick"
2026-03-07 20:11:25 +08:00
>
<!-- 动态插槽根据 columns slotName 动态渲染 -->
<template v-for="col in slotColumns" :key="col.dataIndex" #[String(col.slotName)]="slotProps">
<slot :name="col.slotName" v-bind="slotProps" />
</template>
</a-table>
</div>
</template>
<script lang="ts" setup>
import { computed, ref, watch, nextTick, onUnmounted, PropType } from 'vue'
2026-03-21 17:39:39 +08:00
import type { TableColumnData, TableRowSelection } from '@arco-design/web-vue/es/table/interface'
2026-03-07 20:11:25 +08:00
import cloneDeep from 'lodash/cloneDeep'
import Sortable from 'sortablejs'
type SizeProps = 'mini' | 'small' | 'medium' | 'large'
type Column = TableColumnData & { checked?: boolean }
const props = defineProps({
data: {
type: Array as PropType<any[]>,
default: () => [],
},
columns: {
type: Array as PropType<TableColumnData[]>,
required: true,
},
loading: {
type: Boolean,
default: false,
},
pagination: {
type: Object as PropType<{
current: number
pageSize: number
total?: number
}>,
default: () => ({
current: 1,
pageSize: 20,
}),
},
bordered: {
type: Boolean,
default: false,
},
2026-03-21 17:39:39 +08:00
rowSelection: {
type: Object as PropType<TableRowSelection | undefined>,
default: undefined,
},
scroll: {
type: Object as PropType<{ x?: number | string; y?: number | string } | undefined>,
default: undefined,
},
2026-03-07 20:11:25 +08:00
showToolbar: {
type: Boolean,
default: true,
},
showDownload: {
type: Boolean,
default: true,
},
showRefresh: {
type: Boolean,
default: true,
},
showDensity: {
type: Boolean,
default: true,
},
showColumnSetting: {
type: Boolean,
default: true,
},
downloadButtonText: {
type: String,
default: '下载',
},
refreshTooltipText: {
type: String,
default: '刷新',
},
densityTooltipText: {
type: String,
default: '密度',
},
columnSettingTooltipText: {
type: String,
default: '列设置',
},
})
const emit = defineEmits<{
(e: 'page-change', current: number): void
2026-03-21 17:39:39 +08:00
(e: 'page-size-change', pageSize: number): void
(e: 'selection-change', rowKeys: (string | number)[]): void
(e: 'row-click', record: any, ev: Event): void
2026-03-07 20:11:25 +08:00
(e: 'refresh'): void
(e: 'download'): void
(e: 'density-change', size: SizeProps): void
(e: 'column-change', columns: TableColumnData[]): void
}>()
const size = ref<SizeProps>('medium')
const cloneColumns = ref<Column[]>([])
const showColumns = ref<Column[]>([])
const columnSettingRef = ref<HTMLElement | null>(null)
// Sortable 实例缓存
let sortableInstance: Sortable | null = null
// 密度列表
const densityList = [
{ name: '迷你', value: 'mini' as SizeProps },
{ name: '偏小', value: 'small' as SizeProps },
{ name: '中等', value: 'medium' as SizeProps },
{ name: '偏大', value: 'large' as SizeProps },
]
// 计算需要插槽的列(只在 columns 变化时重新计算)
const slotColumns = computed(() => {
return props.columns.filter(col => col.slotName)
})
const onPageChange = (current: number) => {
emit('page-change', current)
}
2026-03-21 17:39:39 +08:00
const onPageSizeChange = (pageSize: number) => {
emit('page-size-change', pageSize)
}
const onSelectionChange = (rowKeys: (string | number)[]) => {
emit('selection-change', rowKeys)
}
const onRowClick = (record: any, ev: Event) => {
emit('row-click', record, ev)
}
2026-03-07 20:11:25 +08:00
const handleRefresh = () => {
emit('refresh')
}
const handleDownload = () => {
emit('download')
}
const handleSelectDensity = (val: string | number | Record<string, any> | undefined) => {
size.value = val as SizeProps
emit('density-change', size.value)
}
const handleChange = (checked: boolean | (string | boolean | number)[], column: Column, index: number) => {
if (!checked) {
cloneColumns.value = showColumns.value.filter((item) => item.dataIndex !== column.dataIndex)
} else {
cloneColumns.value.splice(index, 0, column)
}
emit('column-change', cloneColumns.value)
}
const exchangeArray = <T extends Array<any>>(array: T, beforeIdx: number, newIdx: number): T => {
if (beforeIdx > -1 && newIdx > -1 && beforeIdx !== newIdx) {
const temp = array[beforeIdx]
array.splice(beforeIdx, 1)
array.splice(newIdx, 0, temp)
}
return array
}
const popupVisibleChange = (val: boolean) => {
if (val) {
nextTick(() => {
if (columnSettingRef.value && !sortableInstance) {
sortableInstance = new Sortable(columnSettingRef.value, {
animation: 150,
onEnd(e: any) {
const { oldIndex, newIndex } = e
if (oldIndex !== undefined && newIndex !== undefined) {
exchangeArray(cloneColumns.value, oldIndex, newIndex)
exchangeArray(showColumns.value, oldIndex, newIndex)
emit('column-change', cloneColumns.value)
}
},
})
}
})
}
}
// 初始化列配置
const initColumns = () => {
const cols = props.columns.map(item => ({
...item,
checked: true,
}))
cloneColumns.value = cols
showColumns.value = cloneDeep(cols)
}
// 监听列配置变化
watch(
() => props.columns,
(val, oldVal) => {
if (val !== oldVal || cloneColumns.value.length === 0) {
initColumns()
}
},
{ immediate: true }
)
// 组件卸载时销毁 Sortable 实例
onUnmounted(() => {
if (sortableInstance) {
sortableInstance.destroy()
sortableInstance = null
}
})
</script>
<script lang="ts">
export default {
name: 'DataTable',
}
</script>
<style scoped lang="less">
.data-table {
:deep(.arco-table-th) {
&:last-child {
.arco-table-th-item-title {
margin-left: 16px;
}
}
}
}
.action-icon {
margin-left: 12px;
cursor: pointer;
}
.active {
color: #0960bd;
background-color: #e3f4fc;
}
.column-setting-container {
max-height: 300px;
overflow-y: auto;
}
.setting {
display: flex;
align-items: center;
width: 200px;
.title {
margin-left: 12px;
cursor: pointer;
}
}
</style>