Files
front/src/components/data-table/index.vue
2026-03-21 17:39:39 +08:00

328 lines
8.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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"
:row-selection="rowSelection"
:scroll="scroll"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
@selection-change="onSelectionChange"
@row-click="onRowClick"
>
<!-- 动态插槽根据 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'
import type { TableColumnData, TableRowSelection } from '@arco-design/web-vue/es/table/interface'
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,
},
rowSelection: {
type: Object as PropType<TableRowSelection | undefined>,
default: undefined,
},
scroll: {
type: Object as PropType<{ x?: number | string; y?: number | string } | undefined>,
default: undefined,
},
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
(e: 'page-size-change', pageSize: number): void
(e: 'selection-change', rowKeys: (string | number)[]): void
(e: 'row-click', record: any, ev: Event): void
(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)
}
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)
}
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>