This commit is contained in:
2026-08-06 19:03:13 +08:00
parent 7034a40aa8
commit 8695eee8c8
28 changed files with 1100 additions and 162 deletions
+144 -6
View File
@@ -7,6 +7,9 @@ export interface DataTableColumn {
columnKey?: string
slotName?: string
headerSlotName?: string
hideable?: boolean
movable?: boolean
defaultVisible?: boolean
[attribute: string]: unknown
}
@@ -39,8 +42,16 @@ export function createPaginationItems(page: number, pageCount: number): Paginati
</script>
<script setup lang="ts">
import { computed, useSlots, watch } from 'vue'
import { ChevronLeft, ChevronRight } from '@lucide/vue'
import { computed, inject, nextTick, ref, useSlots, watch } from 'vue'
import { ChevronLeft, ChevronRight, Settings2 } from '@lucide/vue'
import LayoutPreferenceList, { type LayoutPreferenceItem } from './LayoutPreferenceList.vue'
import {
UI_PREFERENCE_SCOPE,
readLayoutPreference,
removeLayoutPreference,
writeLayoutPreference,
type LayoutPreferenceSnapshot,
} from '@/shared/utils/uiLayoutPreference'
defineOptions({ inheritAttrs: false })
@@ -51,6 +62,7 @@ const props = withDefaults(defineProps<{
emptyText?: string
total?: number
pageSizes?: number[]
preferenceKey?: string
}>(), {
loading: false,
emptyText: '暂无数据',
@@ -61,6 +73,114 @@ const props = withDefaults(defineProps<{
const page = defineModel<number>('page', { default: 1 })
const pageSize = defineModel<number>('pageSize', { default: 10 })
const slots = useSlots()
const preferenceScope = inject(UI_PREFERENCE_SCOPE, computed(() => 'anonymous'))
const preference = ref<LayoutPreferenceSnapshot>({ version: 1, order: [], hidden: [] })
const tableRef = ref<{ doLayout: () => void }>()
const preferenceEnabled = computed(() => Boolean(props.preferenceKey))
function columnId(column: DataTableColumn) {
const value = column.key ?? column.columnKey ?? column.prop ?? column.property
return value === undefined || value === null || value === '' ? undefined : String(value)
}
function systemColumn(column: DataTableColumn) {
const id = columnId(column)
return id === 'actions'
|| column.fixed === 'left'
|| ['selection', 'expand', 'index'].includes(String(column.type || ''))
}
function columnMovable(column: DataTableColumn) {
return column.movable ?? !systemColumn(column)
}
function columnHideable(column: DataTableColumn) {
return column.hideable ?? !systemColumn(column)
}
const configurableColumns = computed(() => props.columns.filter((column) => columnId(column)))
const columnKeys = computed(() => configurableColumns.value.map((column) => columnId(column) as string))
function applyPreference() {
const defaultHidden = configurableColumns.value.flatMap((column) => column.defaultVisible === false ? [columnId(column) as string] : [])
preference.value = readLayoutPreference(
'table',
preferenceScope.value,
props.preferenceKey || '',
columnKeys.value,
defaultHidden,
)
}
watch(
[preferenceScope, () => props.preferenceKey, () => columnKeys.value.join('\u0000')],
applyPreference,
{ immediate: true },
)
const orderedColumns = computed(() => {
if (!preferenceEnabled.value) return props.columns
const columnsByKey = new Map(configurableColumns.value.map((column) => [columnId(column) as string, column]))
return preference.value.order.flatMap((key) => {
const column = columnsByKey.get(key)
return column ? [column] : []
})
})
const visibleColumns = computed(() => orderedColumns.value.filter((column) => {
const id = columnId(column)
return !id || !columnHideable(column) || !preference.value.hidden.includes(id)
}))
const visibleHideableCount = computed(() => visibleColumns.value.filter(columnHideable).length)
const preferenceItems = computed<LayoutPreferenceItem[]>(() => orderedColumns.value.flatMap((column) => {
const key = columnId(column)
if (!key) return []
const visible = !preference.value.hidden.includes(key)
const hideable = columnHideable(column)
return [{
key,
label: column.label || (String(column.type || '') === 'selection' ? '选择' : key),
visible,
hideable,
movable: columnMovable(column),
hideDisabled: visible && hideable && visibleHideableCount.value <= 1,
}]
}))
function persistPreference(order: string[], hidden = preference.value.hidden) {
preference.value = { version: 1, order, hidden }
writeLayoutPreference('table', preferenceScope.value, props.preferenceKey || '', preference.value)
}
function moveColumn(key: string, targetIndex: number) {
const movableKeys = orderedColumns.value.filter(columnMovable).map((column) => columnId(column) as string)
const currentIndex = movableKeys.indexOf(key)
if (currentIndex < 0) return
movableKeys.splice(currentIndex, 1)
movableKeys.splice(Math.min(Math.max(targetIndex, 0), movableKeys.length), 0, key)
const movableSet = new Set(movableKeys)
let nextMovableIndex = 0
const order = preference.value.order.map((id) => movableSet.has(id) ? movableKeys[nextMovableIndex++] : id)
persistPreference(order)
}
function setColumnVisibility(key: string, visible: boolean) {
const column = configurableColumns.value.find((candidate) => columnId(candidate) === key)
if (!column || !columnHideable(column)) return
if (!visible && visibleHideableCount.value <= 1) return
const hidden = visible
? preference.value.hidden.filter((id) => id !== key)
: [...new Set([...preference.value.hidden, key])]
persistPreference(preference.value.order, hidden)
}
function resetPreference() {
removeLayoutPreference('table', preferenceScope.value, props.preferenceKey || '')
applyPreference()
}
watch(visibleColumns, () => {
void nextTick(() => tableRef.value?.doLayout())
})
const normalizedTotal = computed(() => {
const value = props.total ?? 0
return Number.isFinite(value) && value > 0 ? Math.trunc(value) : 0
@@ -88,7 +208,7 @@ watch([page, pageCount], ([value, count]) => {
if (value !== nextValue) page.value = nextValue
}, { immediate: true })
function columnAttrs({ key, slotName, headerSlotName, ...attrs }: DataTableColumn) {
function columnAttrs({ key, slotName, headerSlotName, hideable, movable, defaultVisible, ...attrs }: DataTableColumn) {
return attrs
}
@@ -114,9 +234,24 @@ function setPage(value: number) {
<template>
<div class="data-table">
<el-table v-bind="$attrs" v-loading="loading" :data="data" :empty-text="emptyText" :aria-busy="loading">
<div v-if="$slots['toolbar-start'] || preferenceEnabled" class="data-table__toolbar">
<div class="data-table__toolbar-start"><slot name="toolbar-start" /></div>
<el-popover v-if="preferenceEnabled" placement="bottom-end" :width="340" trigger="click" popper-class="cc-layout-preference-popper">
<template #reference>
<el-button aria-label="列设置" title="列设置"><Settings2 :size="16" /></el-button>
</template>
<LayoutPreferenceList
:items="preferenceItems"
allow-visibility
@move="moveColumn"
@visibility="setColumnVisibility"
@reset="resetPreference"
/>
</el-popover>
</div>
<el-table ref="tableRef" v-bind="$attrs" v-loading="loading" :data="data" :empty-text="emptyText" :aria-busy="loading">
<el-table-column
v-for="(column, index) in columns"
v-for="(column, index) in visibleColumns"
:key="columnRenderKey(column, index)"
v-bind="columnAttrs(column)"
>
@@ -168,6 +303,9 @@ function setPage(value: number) {
<style scoped>
.data-table { min-width: 0; }
.data-table__toolbar { display: flex; min-height: 48px; align-items: center; justify-content: space-between; gap: 12px; padding: 8px 16px; border-bottom: 1px solid var(--cc-border); }
.data-table__toolbar-start { display: flex; min-width: 0; flex: 1; align-items: center; flex-wrap: wrap; gap: 8px; }
.data-table__toolbar :deep(.el-button + .el-button) { margin-left: 0; }
.data-table__pagination { display: flex; min-height: 58px; align-items: center; justify-content: space-between; gap: 16px; padding: 12px 18px; border-top: 1px solid var(--cc-border); color: var(--cc-muted); font-size: 12px; }
.data-table__pagination-left { display: flex; min-width: 0; flex: 1; align-items: center; gap: 8px; }
.data-table__pagination-controls, .data-table__page-size, .data-table__page-buttons { display: flex; align-items: center; gap: 8px; }
@@ -176,5 +314,5 @@ function setPage(value: number) {
.data-table__page-buttons { gap: 4px; }
.data-table__page-button { width: 32px; min-width: 32px; height: 32px; padding: 0; margin-left: 0; }
.data-table__page-ellipsis { display: inline-flex; width: 24px; height: 32px; align-items: center; justify-content: center; }
@media (max-width: 720px) { .data-table__pagination { align-items: flex-start; flex-direction: column; } .data-table__pagination-left, .data-table__pagination-controls, .data-table__page-buttons { width: 100%; } .data-table__pagination-controls { justify-content: flex-start; } .data-table__page-buttons { flex-wrap: wrap; } .data-table__page-button { width: 44px; min-width: 44px; height: 44px; } .data-table__page-ellipsis { height: 44px; } }
@media (max-width: 720px) { .data-table__toolbar { align-items: flex-start; } .data-table__toolbar > :deep(.el-button) { width: 44px; min-width: 44px; height: 44px; padding: 0; } .data-table__pagination { align-items: flex-start; flex-direction: column; } .data-table__pagination-left, .data-table__pagination-controls, .data-table__page-buttons { width: 100%; } .data-table__pagination-controls { justify-content: flex-start; } .data-table__page-buttons { flex-wrap: wrap; } .data-table__page-button { width: 44px; min-width: 44px; height: 44px; } .data-table__page-ellipsis { height: 44px; } }
</style>