Files
admin-web/src/shared/components/DataTable.vue
T
2026-10-26 13:43:08 +08:00

604 lines
34 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.
<script lang="ts">
export interface DataTableColumn {
key?: string | number
label?: string
prop?: string
property?: string
columnKey?: string
width?: string | number
minWidth?: string | number
resizable?: boolean
slotName?: string
headerSlotName?: string
hideable?: boolean
movable?: boolean
defaultVisible?: boolean
singleLine?: boolean
showOverflowTooltip?: boolean
className?: string
[attribute: string]: unknown
}
export type PaginationItem = number | 'ellipsis-start' | 'ellipsis-end'
export function normalizePageSizes(values: readonly number[]) {
const sizes = values
.filter((value) => Number.isFinite(value))
.map((value) => Math.trunc(value))
.filter((value) => value > 0)
const uniqueSizes = [...new Set(sizes)]
return uniqueSizes.length > 0 ? uniqueSizes : [10]
}
export function normalizePage(value: number, pageCount: number) {
const maximum = Number.isFinite(pageCount) && pageCount > 0 ? Math.trunc(pageCount) : 1
if (!Number.isFinite(value)) return 1
return Math.min(Math.max(Math.trunc(value), 1), maximum)
}
export function normalizeTotal(value: unknown) {
const numericValue = typeof value === 'string' && value.trim() !== '' ? Number(value) : value
return typeof numericValue === 'number' && Number.isFinite(numericValue) && numericValue > 0
? Math.trunc(numericValue)
: 0
}
export function createPaginationItems(page: number, pageCount: number): PaginationItem[] {
if (pageCount <= 0) return []
if (pageCount <= 7) return Array.from({ length: pageCount }, (_, index) => index + 1)
if (page <= 4) return [1, 2, 3, 4, 5, 'ellipsis-end', pageCount]
if (page >= pageCount - 3) {
return [1, 'ellipsis-start', pageCount - 4, pageCount - 3, pageCount - 2, pageCount - 1, pageCount]
}
return [1, 'ellipsis-start', page - 1, page, page + 1, 'ellipsis-end', pageCount]
}
</script>
<script setup lang="ts">
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,
type LayoutPreferenceSnapshot,
} from '@/shared/utils/uiLayoutPreference'
defineOptions({ inheritAttrs: false })
const props = withDefaults(defineProps<{
data: object[]
columns: DataTableColumn[]
loading?: boolean
emptyText?: string
total?: number | string
pageSizes?: number[]
maxHeight?: string | number
preferenceKey?: string
columnSettingsMode?: 'auto' | 'show' | 'hide'
toolbarPlacement?: 'top' | 'filters'
border?: boolean
stripe?: boolean
appearance?: 'default' | 'integrated'
resultsVisible?: boolean
}>(), {
loading: false,
emptyText: '暂无数据',
total: undefined,
pageSizes: () => [10, 20, 50, 100],
maxHeight: 'calc(100vh - 300px)',
columnSettingsMode: 'auto',
toolbarPlacement: 'top',
border: true,
stripe: true,
appearance: 'default',
resultsVisible: true,
})
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 }>()
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))
const columnSettingsVisible = computed(() => (
props.columnSettingsMode === 'auto'
? configurableColumns.value.filter(columnHideable).length > 4
: props.columnSettingsMode === 'show'
))
const preferenceEnabled = computed(() => Boolean(props.preferenceKey && columnSettingsVisible.value))
function anchorSystemColumns(order: readonly string[]) {
const columnsByKey = new Map(configurableColumns.value.map((column) => [columnId(column) as string, column]))
const movableKeys = order.filter((key) => {
const column = columnsByKey.get(key)
return column && columnMovable(column)
})
let movableIndex = 0
return configurableColumns.value.map((column) => (
columnMovable(column) ? movableKeys[movableIndex++] : columnId(column) as string
))
}
function applyPreference() {
const defaultHidden = configurableColumns.value.flatMap((column) => column.defaultVisible === false ? [columnId(column) as string] : [])
const snapshot = readLayoutPreference(
'table',
preferenceScope.value,
props.preferenceKey || '',
columnKeys.value,
defaultHidden,
)
preference.value = { ...snapshot, order: anchorSystemColumns(snapshot.order) }
}
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 !preferenceEnabled.value || !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 = { ...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)
}
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 = orderedColumns.value.map((column) => {
const id = columnId(column) as string
return 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(() => normalizeTotal(props.total))
const normalizedPageSizes = computed(() => normalizePageSizes(props.pageSizes))
const effectivePageSize = computed(() => (
normalizedPageSizes.value.includes(pageSize.value) ? pageSize.value : normalizedPageSizes.value[0]
))
const pageCount = computed(() => (
normalizedTotal.value === 0 ? 0 : Math.ceil(normalizedTotal.value / effectivePageSize.value)
))
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
pageSize.value = nextValue
page.value = 1
}, { 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(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
const label = String(attrs.label || '')
return {
...attrs,
prop: String(key),
formatter: attrs.formatter || ((_row: unknown, _column: unknown, value: unknown) => {
if (value === null || value === undefined || value === '') return '—'
if (typeof value === 'number' && /CNY|金额|应收|应付|已收|已付|未收|待收|待付|开账/.test(label)) return value.toFixed(2)
return String(value)
}),
}
}
function singleLineColumn(column: DataTableColumn) {
return column.singleLine !== false && !systemColumn(column)
}
function columnOverflowTooltip(column: DataTableColumn) {
return singleLineColumn(column) ? column.showOverflowTooltip ?? true : column.showOverflowTooltip
}
function columnClassName(column: DataTableColumn) {
return [
column.className,
systemColumn(column) ? 'data-table__system-cell' : '',
singleLineColumn(column) ? 'data-table__single-line-cell' : '',
].filter(Boolean).join(' ')
}
function columnRenderKey(column: DataTableColumn, index: number) {
return String(column.key ?? column.columnKey ?? column.prop ?? column.property ?? index)
}
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
pageSize.value = nextValue
page.value = 1
}
function setPage(value: number) {
page.value = normalizePage(value, pageCount.value)
}
</script>
<template>
<div class="data-table" :class="{ 'data-table--integrated': appearance === 'integrated' }">
<div
v-if="$slots['toolbar-start'] || $slots['toolbar-actions'] || preferenceEnabled || $slots['toolbar-end'] || (toolbarPlacement === 'filters' && $slots.filters)"
class="data-table__toolbar"
:class="{ 'is-compact': !$slots['toolbar-start'], 'has-inline-filters': toolbarPlacement === 'filters' && $slots.filters }"
>
<div v-if="$slots['toolbar-start']" class="data-table__toolbar-start"><slot name="toolbar-start" /></div>
<div v-if="toolbarPlacement === 'filters' && $slots.filters" class="data-table__filters"><slot name="filters" /></div>
<div class="data-table__toolbar-actions" :class="{ 'has-business-actions': $slots['toolbar-actions'] }">
<div v-if="$slots['toolbar-actions']" class="data-table__business-actions"><slot name="toolbar-actions" /></div>
<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" /><span v-if="appearance === 'integrated'">字段设置</span></el-button>
</template>
<p class="data-table__preference-hint">拖动表头边界后自动保存列宽;恢复默认会同时重置列宽。</p>
<LayoutPreferenceList
:items="preferenceItems"
allow-visibility
@move="moveColumn"
@visibility="setColumnVisibility"
@reset="resetPreference"
/>
</el-popover>
<div v-if="$slots['toolbar-end']" class="data-table__toolbar-end"><slot name="toolbar-end" /></div>
</div>
</div>
</div>
<div v-if="$slots.filters && toolbarPlacement !== '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)"
v-bind="columnAttrs(column)"
:class-name="columnClassName(column)"
:show-overflow-tooltip="columnOverflowTooltip(column)"
>
<template v-if="hasSlot(column.headerSlotName)" #header="scope">
<slot :name="column.headerSlotName" v-bind="scope" />
</template>
<template v-if="hasSlot(column.slotName)" #default="scope">
<slot :name="column.slotName" v-bind="scope" />
</template>
</el-table-column>
<template v-if="$slots.empty" #empty><slot name="empty" /></template>
</el-table>
</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>
<span v-else>共 0 条</span>
</slot>
</div>
<div class="data-table__pagination-controls">
<label class="data-table__page-size">
<span>每页</span>
<el-select :model-value="effectivePageSize" :disabled="loading" aria-label="每页条数" @update:model-value="updatePageSize">
<el-option v-for="size in normalizedPageSizes" :key="size" :label="String(size)" :value="size" />
</el-select>
<span>条</span>
</label>
<nav v-if="pageCount > 0" class="data-table__page-buttons" aria-label="页码">
<el-button class="data-table__page-button" :disabled="loading || currentPage <= 1" aria-label="上一页" title="上一页" @click="setPage(currentPage - 1)"><ChevronLeft :size="16" /></el-button>
<template v-for="item in paginationItems" :key="item">
<span v-if="typeof item !== 'number'" class="data-table__page-ellipsis" aria-hidden="true">...</span>
<el-button
v-else
class="data-table__page-button"
:type="item === currentPage ? 'primary' : undefined"
:disabled="loading"
:aria-current="item === currentPage ? 'page' : undefined"
:aria-label="`第 ${item} 页`"
:title="`第 ${item} 页`"
@click="setPage(item)"
>{{ item }}</el-button>
</template>
<el-button class="data-table__page-button" :disabled="loading || currentPage >= pageCount" aria-label="下一页" title="下一页" @click="setPage(currentPage + 1)"><ChevronRight :size="16" /></el-button>
</nav>
</div>
</footer>
</div>
</template>
<style scoped>
.data-table { min-width: 0; }
.data-table > .data-table__filters {
min-width: 0;
margin-bottom: var(--cc-list-query-gap, 16px);
border: 1px solid var(--list-card-border, var(--cc-border));
border-radius: var(--cc-list-query-radius, 5px);
background: var(--list-card-muted-bg, #f7f9fc);
}
.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) { background: var(--cc-list-table-header-bg); color: var(--cc-list-table-header-text); 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); }
.data-table :deep(.el-table__empty-text) { font-size: var(--cc-list-table-font-size); }
.data-table__toolbar { display: flex; min-height: 44px; align-items: center; justify-content: space-between; gap: var(--admin-space-5, 12px); padding: var(--admin-space-2, 6px) var(--admin-space-5, 12px); border-bottom: 1px solid var(--cc-border); }
.data-table__toolbar.is-compact { justify-content: flex-end; }
.data-table__toolbar-start { min-width: 0; flex: 1; }
.data-table__toolbar-start :deep(h2) { margin: 0; color: var(--cc-text); font-size: 16px; line-height: 1.4; }
.data-table > .data-table__toolbar:has(+ .data-table__filters) { border-bottom: 0; }
.data-table__toolbar-start :deep(p) { margin: 4px 0 0; color: var(--cc-muted); font-size: 12px; line-height: 1.5; }
.data-table__toolbar-actions { display: flex; flex: 0 0 auto; align-items: center; flex-wrap: wrap; justify-content: flex-end; gap: 8px; }
.data-table__business-actions, .data-table__utility-actions, .data-table__toolbar-end { display: flex; min-width: 0; align-items: center; flex-wrap: wrap; justify-content: flex-end; gap: 8px; }
.data-table__utility-actions { flex: 0 0 auto; }
.data-table > .data-table__toolbar > .data-table__toolbar-actions > :is(.data-table__business-actions, .data-table__utility-actions) > :deep(.el-button + .el-button),
.data-table > .data-table__toolbar > .data-table__toolbar-actions > :is(.data-table__business-actions, .data-table__utility-actions) > :deep(:where(div, span):not(:where(.el-overlay, .el-dialog, .el-drawer, .el-popper, .data-table)) > .el-button + .el-button),
.data-table > .data-table__toolbar > .data-table__toolbar-actions > :is(.data-table__business-actions, .data-table__utility-actions) > :deep(:where(div, span):not(:where(.el-overlay, .el-dialog, .el-drawer, .el-popper, .data-table)) > :where(div, span):not(:where(.el-overlay, .el-dialog, .el-drawer, .el-popper, .data-table)) > .el-button + .el-button),
.data-table > .data-table__toolbar > .data-table__toolbar-actions > :is(.data-table__business-actions, .data-table__utility-actions) > :deep(:where(div, span):not(:where(.el-overlay, .el-dialog, .el-drawer, .el-popper, .data-table)) > :where(div, span):not(:where(.el-overlay, .el-dialog, .el-drawer, .el-popper, .data-table)) > :where(div, span):not(:where(.el-overlay, .el-dialog, .el-drawer, .el-popper, .data-table)) > .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: var(--cc-list-pagination-font-size); }
.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; }
.data-table__pagination-controls { min-width: 0; flex-wrap: wrap; justify-content: flex-end; }
.data-table__page-size :deep(.el-select) { width: 82px; }
.data-table__page-buttons { gap: 4px; }
.data-table__page-button { width: 32px; min-width: 32px; height: 32px; padding: 0; margin-left: 0; font-size: var(--cc-list-page-button-font-size); }
.data-table__page-ellipsis { display: inline-flex; width: 24px; height: 32px; align-items: center; justify-content: center; }
.data-table :deep(.data-table__single-line-cell .cell) { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.data-table :deep(.data-table__system-cell .cell) { white-space: nowrap; }
.data-table :deep(.data-table__system-cell .el-button + .el-button),
.data-table :deep(.data-table__system-cell .el-button + .el-dropdown) { margin-left: var(--cc-list-action-sibling-margin, 8px); }
@media (min-width: 1920px) {
.data-table__toolbar-actions,
.data-table__business-actions,
.data-table__utility-actions,
.data-table__toolbar-end { gap: 6px; }
.data-table__pagination { min-height: 52px; gap: 12px; padding: 10px 14px; }
.data-table__pagination-left,
.data-table__pagination-controls,
.data-table__page-size,
.data-table__page-buttons { gap: 6px; }
.data-table__page-button { width: 30px; min-width: 30px; height: 30px; }
.data-table__page-ellipsis { width: 22px; height: 30px; }
}
@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 > .data-table__toolbar > .data-table__toolbar-actions > :is(.data-table__business-actions, .data-table__utility-actions) > :deep(.el-button),
.data-table > .data-table__toolbar > .data-table__toolbar-actions > :is(.data-table__business-actions, .data-table__utility-actions) > :deep(:where(div, span):not(:where(.el-overlay, .el-dialog, .el-drawer, .el-popper, .data-table)) > .el-button),
.data-table > .data-table__toolbar > .data-table__toolbar-actions > :is(.data-table__business-actions, .data-table__utility-actions) > :deep(:where(div, span):not(:where(.el-overlay, .el-dialog, .el-drawer, .el-popper, .data-table)) > :where(div, span):not(:where(.el-overlay, .el-dialog, .el-drawer, .el-popper, .data-table)) > .el-button),
.data-table > .data-table__toolbar > .data-table__toolbar-actions > :is(.data-table__business-actions, .data-table__utility-actions) > :deep(:where(div, span):not(:where(.el-overlay, .el-dialog, .el-drawer, .el-popper, .data-table)) > :where(div, span):not(:where(.el-overlay, .el-dialog, .el-drawer, .el-popper, .data-table)) > :where(div, span):not(:where(.el-overlay, .el-dialog, .el-drawer, .el-popper, .data-table)) > .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; } }
/* The list surface contains a separate rounded filter block above the results. */
.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 > .data-table__toolbar-actions > :is(.data-table__business-actions, .data-table__utility-actions) > :deep(.el-button),
.data-table--integrated > .data-table__toolbar > .data-table__toolbar-actions > :is(.data-table__business-actions, .data-table__utility-actions) > :deep(:where(div, span):not(:where(.el-overlay, .el-dialog, .el-drawer, .el-popper, .data-table)) > .el-button),
.data-table--integrated > .data-table__toolbar > .data-table__toolbar-actions > :is(.data-table__business-actions, .data-table__utility-actions) > :deep(:where(div, span):not(:where(.el-overlay, .el-dialog, .el-drawer, .el-popper, .data-table)) > :where(div, span):not(:where(.el-overlay, .el-dialog, .el-drawer, .el-popper, .data-table)) > .el-button),
.data-table--integrated > .data-table__toolbar > .data-table__toolbar-actions > :is(.data-table__business-actions, .data-table__utility-actions) > :deep(:where(div, span):not(:where(.el-overlay, .el-dialog, .el-drawer, .el-popper, .data-table)) > :where(div, span):not(:where(.el-overlay, .el-dialog, .el-drawer, .el-popper, .data-table)) > :where(div, span):not(:where(.el-overlay, .el-dialog, .el-drawer, .el-popper, .data-table)) > .el-button) {
min-height: 36px;
border-radius: 10px;
}
.data-table--integrated > .data-table__toolbar > .data-table__toolbar-actions > .data-table__utility-actions > :deep(.el-button > span),
.data-table--integrated > .data-table__toolbar > .data-table__toolbar-actions > .data-table__utility-actions > :deep(:where(div, span):not(:where(.el-overlay, .el-dialog, .el-drawer, .el-popper, .data-table)) > .el-button > span),
.data-table--integrated > .data-table__toolbar > .data-table__toolbar-actions > .data-table__utility-actions > :deep(:where(div, span):not(:where(.el-overlay, .el-dialog, .el-drawer, .el-popper, .data-table)) > :where(div, span):not(:where(.el-overlay, .el-dialog, .el-drawer, .el-popper, .data-table)) > .el-button > span),
.data-table--integrated > .data-table__toolbar > .data-table__toolbar-actions > .data-table__utility-actions > :deep(:where(div, span):not(:where(.el-overlay, .el-dialog, .el-drawer, .el-popper, .data-table)) > :where(div, span):not(:where(.el-overlay, .el-dialog, .el-drawer, .el-popper, .data-table)) > :where(div, span):not(:where(.el-overlay, .el-dialog, .el-drawer, .el-popper, .data-table)) > .el-button > span) { gap: 6px; }
.data-table--integrated > .data-table__toolbar > .data-table__toolbar-actions > .data-table__business-actions > :deep(.el-button--primary:not(:disabled)),
.data-table--integrated > .data-table__toolbar > .data-table__toolbar-actions > .data-table__business-actions > :deep(:where(div, span):not(:where(.el-overlay, .el-dialog, .el-drawer, .el-popper, .data-table)) > .el-button--primary:not(:disabled)),
.data-table--integrated > .data-table__toolbar > .data-table__toolbar-actions > .data-table__business-actions > :deep(:where(div, span):not(:where(.el-overlay, .el-dialog, .el-drawer, .el-popper, .data-table)) > :where(div, span):not(:where(.el-overlay, .el-dialog, .el-drawer, .el-popper, .data-table)) > .el-button--primary:not(:disabled)),
.data-table--integrated > .data-table__toolbar > .data-table__toolbar-actions > .data-table__business-actions > :deep(:where(div, span):not(:where(.el-overlay, .el-dialog, .el-drawer, .el-popper, .data-table)) > :where(div, span):not(:where(.el-overlay, .el-dialog, .el-drawer, .el-popper, .data-table)) > :where(div, span):not(:where(.el-overlay, .el-dialog, .el-drawer, .el-popper, .data-table)) > .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__toolbar > .data-table__toolbar-actions > .data-table__business-actions > :deep(.el-button--primary:hover:not(:disabled)),
.data-table--integrated > .data-table__toolbar > .data-table__toolbar-actions > .data-table__business-actions > :deep(:where(div, span):not(:where(.el-overlay, .el-dialog, .el-drawer, .el-popper, .data-table)) > .el-button--primary:hover:not(:disabled)),
.data-table--integrated > .data-table__toolbar > .data-table__toolbar-actions > .data-table__business-actions > :deep(:where(div, span):not(:where(.el-overlay, .el-dialog, .el-drawer, .el-popper, .data-table)) > :where(div, span):not(:where(.el-overlay, .el-dialog, .el-drawer, .el-popper, .data-table)) > .el-button--primary:hover:not(:disabled)),
.data-table--integrated > .data-table__toolbar > .data-table__toolbar-actions > .data-table__business-actions > :deep(:where(div, span):not(:where(.el-overlay, .el-dialog, .el-drawer, .el-popper, .data-table)) > :where(div, span):not(:where(.el-overlay, .el-dialog, .el-drawer, .el-popper, .data-table)) > :where(div, span):not(:where(.el-overlay, .el-dialog, .el-drawer, .el-popper, .data-table)) > .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 :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; }
/* Select's default wrapper uses a fixed 32px, unlike input and button sizing. */
.data-table--integrated .data-table__filters :deep(.el-select:not(.el-select--small):not(.el-select--large) .el-select__wrapper) {
min-height: var(--el-component-size);
}
.data-table--integrated .data-table__filters :deep(.el-button--primary:not(:disabled)) {
--el-button-bg-color: var(--workbench-primary, #1677ff);
--el-button-border-color: var(--workbench-primary, #1677ff);
--el-button-hover-bg-color: var(--workbench-primary-hover, #4096ff);
--el-button-hover-border-color: var(--workbench-primary-hover, #4096ff);
--el-button-active-bg-color: var(--workbench-primary-active, #0958d9);
--el-button-active-border-color: var(--workbench-primary-active, #0958d9);
}
.data-table--integrated > :deep(.el-table) {
--el-table-border-color: var(--list-card-border);
--el-table-header-bg-color: var(--cc-list-table-header-bg);
--el-table-row-hover-bg-color: #f5f8ff;
--admin-row-height: 44px;
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; }
/* 筛选与操作共用一行,字段设置靠右,避免独占一行留白。 */
.data-table > .data-table__toolbar.has-inline-filters {
align-items: center;
flex-wrap: nowrap;
min-height: 0;
margin-bottom: var(--cc-list-query-gap, 16px);
padding: 16px 20px;
border-bottom: 1px solid var(--list-card-border, var(--cc-border));
background: var(--list-card-muted-bg, #f7f9fc);
}
.data-table__toolbar.has-inline-filters > .data-table__filters { flex: 1 1 0; min-width: 0; }
.data-table__toolbar.has-inline-filters > .data-table__filters :deep(.query-toolbar) { padding: 0; border: 0; background: transparent; }
.data-table__toolbar.has-inline-filters > .data-table__toolbar-actions { margin-left: auto; }
@media (max-width: 720px) {
.data-table > .data-table__toolbar.has-inline-filters { align-items: stretch; flex-direction: column; }
.data-table__toolbar.has-inline-filters > .data-table__filters { flex-basis: auto; }
}
</style>