优化
This commit is contained in:
@@ -5,6 +5,9 @@ export interface DataTableColumn {
|
||||
prop?: string
|
||||
property?: string
|
||||
columnKey?: string
|
||||
width?: string | number
|
||||
minWidth?: string | number
|
||||
resizable?: boolean
|
||||
slotName?: string
|
||||
headerSlotName?: string
|
||||
hideable?: boolean
|
||||
@@ -52,11 +55,12 @@ export function createPaginationItems(page: number, pageCount: number): Paginati
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, nextTick, ref, useSlots, watch } from 'vue'
|
||||
import { Comment, Fragment, Text, computed, inject, isVNode, nextTick, ref, useSlots, watch } from 'vue'
|
||||
import { ChevronLeft, ChevronRight, Settings2 } from '@lucide/vue'
|
||||
import LayoutPreferenceList, { type LayoutPreferenceItem } from './LayoutPreferenceList.vue'
|
||||
import {
|
||||
UI_PREFERENCE_SCOPE,
|
||||
normalizeColumnWidth,
|
||||
readLayoutPreference,
|
||||
removeLayoutPreference,
|
||||
writeLayoutPreference,
|
||||
@@ -77,6 +81,8 @@ const props = withDefaults(defineProps<{
|
||||
columnSettingsMode?: 'auto' | 'show' | 'hide'
|
||||
border?: boolean
|
||||
stripe?: boolean
|
||||
appearance?: 'default' | 'integrated'
|
||||
resultsVisible?: boolean
|
||||
}>(), {
|
||||
loading: false,
|
||||
emptyText: '暂无数据',
|
||||
@@ -86,6 +92,8 @@ const props = withDefaults(defineProps<{
|
||||
columnSettingsMode: 'auto',
|
||||
border: true,
|
||||
stripe: true,
|
||||
appearance: 'default',
|
||||
resultsVisible: true,
|
||||
})
|
||||
|
||||
const page = defineModel<number>('page', { default: 1 })
|
||||
@@ -183,7 +191,19 @@ const preferenceItems = computed<LayoutPreferenceItem[]>(() => orderedColumns.va
|
||||
}))
|
||||
|
||||
function persistPreference(order: string[], hidden = preference.value.hidden) {
|
||||
preference.value = { version: 1, order, hidden }
|
||||
preference.value = { ...preference.value, order, hidden }
|
||||
writeLayoutPreference('table', preferenceScope.value, props.preferenceKey || '', preference.value)
|
||||
}
|
||||
|
||||
function saveColumnWidth(newWidth: number, _oldWidth: number, resizedColumn: { columnKey?: string }) {
|
||||
if (!preferenceEnabled.value || !resizedColumn.columnKey) return
|
||||
const column = configurableColumns.value.find(candidate => (
|
||||
(candidate.columnKey ?? columnId(candidate)) === resizedColumn.columnKey
|
||||
))
|
||||
const key = column && columnId(column)
|
||||
const width = normalizeColumnWidth(newWidth)
|
||||
if (!key || width === undefined || column?.resizable === false) return
|
||||
preference.value = { ...preference.value, widths: { ...preference.value.widths, [key]: width } }
|
||||
writeLayoutPreference('table', preferenceScope.value, props.preferenceKey || '', preference.value)
|
||||
}
|
||||
|
||||
@@ -232,6 +252,7 @@ const currentPage = computed(() => normalizePage(page.value, pageCount.value))
|
||||
const paginationItems = computed(() => createPaginationItems(currentPage.value, pageCount.value))
|
||||
|
||||
watch([pageSize, normalizedPageSizes], ([value, sizes]) => {
|
||||
if (slots.results) return
|
||||
const integerValue = Number.isFinite(value) ? Math.trunc(value) : sizes[0]
|
||||
const nextValue = sizes.includes(integerValue) ? integerValue : sizes[0]
|
||||
if (value === nextValue) return
|
||||
@@ -240,11 +261,16 @@ watch([pageSize, normalizedPageSizes], ([value, sizes]) => {
|
||||
}, { immediate: true })
|
||||
|
||||
watch([page, pageCount], ([value, count]) => {
|
||||
if (!props.resultsVisible || slots.state || slots.results) return
|
||||
const nextValue = normalizePage(value, count)
|
||||
if (value !== nextValue) page.value = nextValue
|
||||
}, { immediate: true })
|
||||
|
||||
function columnAttrs({ key, slotName, headerSlotName, hideable, movable, defaultVisible, singleLine, ...attrs }: DataTableColumn) {
|
||||
function columnAttrs(column: DataTableColumn) {
|
||||
const { key, slotName, headerSlotName, hideable, movable, defaultVisible, singleLine, ...attrs } = column
|
||||
const id = columnId(column)
|
||||
attrs.columnKey ??= id
|
||||
attrs.width = (preferenceEnabled.value && id ? preference.value.widths?.[id] : undefined) ?? attrs.width
|
||||
const hasDataProperty = attrs.prop !== undefined || attrs.property !== undefined
|
||||
const isSpecialColumn = slotName || ['selection', 'expand', 'index'].includes(String(attrs.type || ''))
|
||||
if (hasDataProperty || isSpecialColumn || key === undefined || key === null || key === '') return attrs
|
||||
@@ -284,6 +310,18 @@ function hasSlot(name?: string) {
|
||||
return Boolean(name && slots[name])
|
||||
}
|
||||
|
||||
function hasRenderableContent(value: unknown): boolean {
|
||||
if (Array.isArray(value)) return value.some(hasRenderableContent)
|
||||
if (!isVNode(value)) return typeof value === 'string' ? value.trim().length > 0 : value != null && typeof value !== 'boolean'
|
||||
if (value.type === Comment) return false
|
||||
if (value.type === Text || value.type === Fragment) return hasRenderableContent(value.children)
|
||||
return true
|
||||
}
|
||||
|
||||
function hasRegion(name: string) {
|
||||
return hasRenderableContent(slots[name]?.())
|
||||
}
|
||||
|
||||
function updatePageSize(value: number) {
|
||||
const nextValue = normalizedPageSizes.value.includes(value) ? value : normalizedPageSizes.value[0]
|
||||
if (nextValue === pageSize.value) return
|
||||
@@ -297,7 +335,7 @@ function setPage(value: number) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="data-table">
|
||||
<div class="data-table" :class="{ 'data-table--integrated': appearance === 'integrated' }">
|
||||
<div
|
||||
v-if="$slots['toolbar-start'] || $slots['toolbar-actions'] || preferenceEnabled || $slots['toolbar-end']"
|
||||
class="data-table__toolbar"
|
||||
@@ -309,8 +347,9 @@ function setPage(value: number) {
|
||||
<div v-if="preferenceEnabled || $slots['toolbar-end']" class="data-table__utility-actions">
|
||||
<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>
|
||||
<el-button aria-label="列设置" title="列设置"><Settings2 :size="16" /><span v-if="appearance === 'integrated'">字段设置</span></el-button>
|
||||
</template>
|
||||
<p class="data-table__preference-hint">拖动表头边界后自动保存列宽;恢复默认会同时重置列宽。</p>
|
||||
<LayoutPreferenceList
|
||||
:items="preferenceItems"
|
||||
allow-visibility
|
||||
@@ -323,7 +362,12 @@ function setPage(value: number) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-table ref="tableRef" v-bind="$attrs" v-loading="loading" :data="data" :empty-text="emptyText" :border="border" :stripe="stripe" :max-height="maxHeight" :aria-busy="loading">
|
||||
<div v-if="$slots.filters" class="data-table__filters"><slot name="filters" /></div>
|
||||
<div v-if="hasRegion('notices')" class="data-table__notices"><slot name="notices" /></div>
|
||||
<div v-if="resultsVisible && hasRegion('before-results')" class="data-table__summary"><slot name="before-results" /></div>
|
||||
<div v-if="$slots.state" class="data-table__state"><slot name="state" /></div>
|
||||
<slot v-else-if="resultsVisible" name="results">
|
||||
<el-table ref="tableRef" v-bind="$attrs" v-loading="loading" :data="data" :empty-text="emptyText" :border="border" :stripe="stripe" :max-height="maxHeight" :aria-busy="loading" @header-dragend="saveColumnWidth">
|
||||
<el-table-column
|
||||
v-for="(column, index) in visibleColumns"
|
||||
:key="columnRenderKey(column, index)"
|
||||
@@ -340,7 +384,8 @@ function setPage(value: number) {
|
||||
</el-table-column>
|
||||
<template v-if="$slots.empty" #empty><slot name="empty" /></template>
|
||||
</el-table>
|
||||
<footer v-if="total !== undefined" class="data-table__pagination" aria-label="表格分页">
|
||||
</slot>
|
||||
<footer v-if="resultsVisible && !$slots.state && total !== undefined" class="data-table__pagination" aria-label="表格分页">
|
||||
<div class="data-table__pagination-left">
|
||||
<slot name="pagination-left" :total="normalizedTotal" :page="currentPage" :page-size="effectivePageSize" :page-count="pageCount">
|
||||
<span v-if="pageCount > 0">共 {{ normalizedTotal }} 条 · 第 {{ currentPage }} / {{ pageCount }} 页</span>
|
||||
@@ -379,6 +424,7 @@ function setPage(value: number) {
|
||||
|
||||
<style scoped>
|
||||
.data-table { min-width: 0; }
|
||||
.data-table__preference-hint { margin: 0 0 10px; color: var(--cc-muted); font-size: 12px; line-height: 1.5; }
|
||||
.data-table :deep(.el-table) { font-size: var(--cc-list-table-font-size); }
|
||||
.data-table :deep(.el-table th.el-table__cell) { font-size: var(--cc-list-table-font-size); font-weight: var(--cc-list-table-header-font-weight); }
|
||||
.data-table :deep(.el-table .el-button) { font-size: var(--cc-list-action-font-size); }
|
||||
@@ -420,4 +466,94 @@ function setPage(value: number) {
|
||||
}
|
||||
|
||||
@media (max-width: 720px) { .data-table__toolbar, .data-table__toolbar.is-compact { min-height: 0; align-items: stretch; flex-direction: column; padding: 14px; } .data-table__toolbar-actions { width: 100%; align-items: flex-start; flex-wrap: wrap; justify-content: flex-start; } .data-table__toolbar-actions.has-business-actions { display: grid; grid-template-columns: minmax(0, 1fr) auto; } .data-table__business-actions { justify-content: flex-start; } .data-table__utility-actions { align-items: flex-start; justify-content: flex-end; } .data-table__toolbar-end { justify-content: flex-start; } .data-table__toolbar-actions :deep(.el-button) { max-width: 100%; min-height: 40px; } .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; } }
|
||||
/* Opt-in list surface: header, filters and results share one continuous card. */
|
||||
.data-table--integrated {
|
||||
--list-card-border: #e8edf5;
|
||||
--list-card-muted-bg: #f7f9fc;
|
||||
--list-card-radius: 12px;
|
||||
--list-card-accent: linear-gradient(135deg, #667eea, #764ba2);
|
||||
--el-component-size: 36px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--list-card-border);
|
||||
border-radius: var(--list-card-radius);
|
||||
background: var(--admin-surface, #fff);
|
||||
}
|
||||
.data-table--integrated > .data-table__toolbar {
|
||||
min-height: 64px;
|
||||
box-sizing: border-box;
|
||||
flex-wrap: wrap;
|
||||
padding: 14px 20px;
|
||||
border-color: var(--list-card-border);
|
||||
}
|
||||
.data-table--integrated .data-table__toolbar-start :deep(h1),
|
||||
.data-table--integrated .data-table__toolbar-start :deep(h2) {
|
||||
margin: 0;
|
||||
color: #1e293b;
|
||||
font-size: var(--admin-font-title, 16px);
|
||||
font-weight: 700;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.data-table--integrated .data-table__toolbar-actions :deep(.el-button) {
|
||||
min-height: 36px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.data-table--integrated .data-table__utility-actions :deep(.el-button > span) { gap: 6px; }
|
||||
.data-table--integrated .data-table__business-actions :deep(.el-button--primary:not(:disabled)),
|
||||
.data-table--integrated .data-table__page-button[aria-current="page"]:not(:disabled) {
|
||||
border-color: transparent;
|
||||
background: var(--list-card-accent);
|
||||
color: #fff;
|
||||
box-shadow: 0 4px 12px rgb(102 126 234 / 20%);
|
||||
}
|
||||
.data-table--integrated .data-table__business-actions :deep(.el-button--primary:hover:not(:disabled)),
|
||||
.data-table--integrated .data-table__page-button[aria-current="page"]:hover:not(:disabled) { filter: brightness(1.06); }
|
||||
.data-table--integrated > .data-table__filters {
|
||||
min-width: 0;
|
||||
background: var(--list-card-muted-bg);
|
||||
border-bottom: 1px solid var(--list-card-border);
|
||||
}
|
||||
.data-table--integrated .data-table__filters :deep(.query-toolbar) {
|
||||
margin: 0;
|
||||
padding: 16px 20px;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
.data-table--integrated .data-table__filters :deep(.el-input__wrapper),
|
||||
.data-table--integrated .data-table__filters :deep(.el-select__wrapper),
|
||||
.data-table--integrated .data-table__filters :deep(.el-button) { border-radius: 8px; }
|
||||
.data-table--integrated .data-table__filters :deep(.el-button--primary:not(:disabled)) {
|
||||
--el-button-bg-color: #1677ff;
|
||||
--el-button-border-color: #1677ff;
|
||||
--el-button-hover-bg-color: #4096ff;
|
||||
--el-button-hover-border-color: #4096ff;
|
||||
--el-button-active-bg-color: #0958d9;
|
||||
--el-button-active-border-color: #0958d9;
|
||||
}
|
||||
.data-table--integrated > :deep(.el-table) {
|
||||
--el-table-border-color: var(--list-card-border);
|
||||
--el-table-header-bg-color: var(--list-card-muted-bg);
|
||||
--el-table-row-hover-bg-color: #f5f8ff;
|
||||
--admin-row-height: 44px;
|
||||
color: #526078;
|
||||
}
|
||||
.data-table--integrated :deep(.el-table th.el-table__cell) { color: #526078; }
|
||||
.data-table.data-table--integrated > .data-table__pagination {
|
||||
min-height: 64px;
|
||||
box-sizing: border-box;
|
||||
justify-content: flex-end;
|
||||
gap: 16px;
|
||||
padding: 14px 20px;
|
||||
border-color: var(--list-card-border);
|
||||
}
|
||||
.data-table--integrated .data-table__pagination-left { flex: 0 1 auto; }
|
||||
.data-table--integrated .data-table__page-button { border-radius: 8px; }
|
||||
.data-table--integrated > .data-table__state { padding: 20px; }
|
||||
.data-table--integrated > .data-table__notices,
|
||||
.data-table--integrated > .data-table__summary { padding: 12px 20px; }
|
||||
.data-table--integrated .data-table__filters :deep(.surface-panel) { border: 0; border-radius: 0; box-shadow: none; }
|
||||
.data-table--integrated .data-table__filters :deep(.integrated-query-group) { display: grid; min-width: 0; gap: 0; }
|
||||
.data-table--integrated .data-table__filters :deep(.integrated-query-group > .el-radio-group) { padding: 12px 20px 0; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user