604 lines
34 KiB
Vue
604 lines
34 KiB
Vue
<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>
|