This commit is contained in:
2026-08-10 16:24:54 +08:00
parent e119b48662
commit 820432a381
52 changed files with 1015 additions and 705 deletions
+25 -3
View File
@@ -94,7 +94,7 @@ describe('DataTable column preferences', () => {
slots: {
'toolbar-start': '<h2>订单列表</h2>',
'toolbar-actions': '<button>新增订单</button>',
'toolbar-end': '<button>刷新</button>',
'toolbar-end': '<button>辅助操作</button>',
},
global: {
provide: { [UI_PREFERENCE_SCOPE as symbol]: computed(() => 'user-1') },
@@ -109,10 +109,32 @@ describe('DataTable column preferences', () => {
})
expect(wrapper.findAll('.data-table__toolbar')).toHaveLength(1)
expect(wrapper.get('.data-table__toolbar').classes()).not.toContain('is-compact')
expect(wrapper.get('.data-table__toolbar-start').text()).toBe('订单列表')
expect(wrapper.get('.data-table__toolbar-actions').text()).toContain('新增订单')
expect(wrapper.get('.data-table__toolbar-actions').text()).toContain('刷新')
expect(wrapper.get('.data-table__toolbar-actions').text()).toContain('辅助操作')
expect(wrapper.get('.data-table__business-actions').text()).toBe('新增订单')
expect(wrapper.get('.data-table__toolbar-end').text()).toBe('辅助操作')
})
it('uses a compact right-aligned toolbar when no leading content is provided', () => {
const wrapper = shallowMount(DataTable, {
props: { data: [], columns: configurableColumns, preferenceKey: 'orders-table' },
slots: { 'toolbar-actions': '<button>新增订单</button>' },
global: {
provide: { [UI_PREFERENCE_SCOPE as symbol]: computed(() => 'user-1') },
stubs: {
ElTable: TableStub,
ElTableColumn: ColumnStub,
ElButton: ButtonStub,
ElPopover: PopoverStub,
},
directives: { loading: {} },
},
})
expect(wrapper.get('.data-table__toolbar').classes()).toContain('is-compact')
expect(wrapper.find('.data-table__toolbar-start').exists()).toBe(false)
expect(wrapper.get('.data-table__business-actions').text()).toBe('新增订单')
expect(wrapper.get('.data-table__toolbar-end').text()).toBe('刷新')
})
})
+8 -3
View File
@@ -250,8 +250,12 @@ function setPage(value: number) {
<template>
<div class="data-table">
<div v-if="$slots['toolbar-start'] || $slots['toolbar-actions'] || preferenceEnabled || $slots['toolbar-end']" class="data-table__toolbar">
<div class="data-table__toolbar-start"><slot name="toolbar-start" /></div>
<div
v-if="$slots['toolbar-start'] || $slots['toolbar-actions'] || preferenceEnabled || $slots['toolbar-end']"
class="data-table__toolbar"
:class="{ 'is-compact': !$slots['toolbar-start'] }"
>
<div v-if="$slots['toolbar-start']" class="data-table__toolbar-start"><slot name="toolbar-start" /></div>
<div class="data-table__toolbar-actions">
<div v-if="$slots['toolbar-actions']" class="data-table__business-actions"><slot name="toolbar-actions" /></div>
<el-popover v-if="preferenceEnabled" placement="bottom-end" :width="340" trigger="click" popper-class="cc-layout-preference-popper">
@@ -324,6 +328,7 @@ function setPage(value: number) {
<style scoped>
.data-table { min-width: 0; }
.data-table__toolbar { display: flex; min-height: 72px; align-items: center; justify-content: space-between; gap: 16px; padding: 12px 18px; border-bottom: 1px solid var(--cc-border); }
.data-table__toolbar.is-compact { min-height: 56px; justify-content: flex-end; padding: 8px 14px; }
.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__toolbar-start :deep(p) { margin: 4px 0 0; color: var(--cc-muted); font-size: 12px; line-height: 1.5; }
@@ -338,5 +343,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__toolbar { align-items: stretch; flex-direction: column; padding: 14px; } .data-table__toolbar-actions { width: 100%; align-items: flex-start; flex-wrap: nowrap; justify-content: flex-start; } .data-table__business-actions { flex: 1 1 auto; justify-content: flex-start; } .data-table__toolbar-end { flex: 0 1 auto; justify-content: flex-start; } .data-table__toolbar-actions :deep(.el-button) { 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; } }
@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: nowrap; justify-content: flex-start; } .data-table__business-actions { flex: 1 1 auto; justify-content: flex-start; } .data-table__toolbar-end { flex: 0 1 auto; justify-content: flex-start; } .data-table__toolbar-actions :deep(.el-button) { 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; } }
</style>
+23 -4
View File
@@ -1,21 +1,40 @@
<script setup lang="ts">
import { Comment, Fragment, Text, isVNode, useSlots } from 'vue'
withDefaults(defineProps<{
ariaLabel?: string
}>(), {
ariaLabel: '列表页面',
})
const slots = useSlots()
type RegionName = 'toolbar' | 'metrics' | 'state' | 'default'
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) return hasRenderableContent(value.children)
if (value.type === Fragment) return hasRenderableContent(value.children)
return true
}
function hasRegion(name: RegionName) {
return hasRenderableContent(slots[name]?.())
}
</script>
<template>
<section class="list-page-layout" :aria-label="ariaLabel">
<div v-if="$slots.toolbar" class="list-page-layout__toolbar"><slot name="toolbar" /></div>
<div v-if="$slots.metrics" class="list-page-layout__metrics"><slot name="metrics" /></div>
<div v-if="$slots.state" class="list-page-layout__state"><slot name="state" /></div>
<div v-if="$slots.default" class="list-page-layout__content"><slot /></div>
<div v-if="hasRegion('toolbar')" class="list-page-layout__toolbar"><slot name="toolbar" /></div>
<div v-if="hasRegion('metrics')" class="list-page-layout__metrics"><slot name="metrics" /></div>
<div v-if="hasRegion('state')" class="list-page-layout__state"><slot name="state" /></div>
<div v-if="hasRegion('default')" class="list-page-layout__content"><slot /></div>
</section>
</template>
<style scoped>
.list-page-layout { display: flex; min-width: 0; flex-direction: column; gap: 16px; }
.list-page-layout__toolbar, .list-page-layout__metrics, .list-page-layout__state, .list-page-layout__content { min-width: 0; }
.list-page-layout__toolbar:empty, .list-page-layout__metrics:empty, .list-page-layout__state:empty, .list-page-layout__content:empty { display: none; }
</style>
+21
View File
@@ -37,6 +37,27 @@ describe('admin page layout components', () => {
expect(wrapper.text()).toContain('表格')
})
it('removes registered but empty list regions from flex layout', () => {
const wrapper = mount(ListPageLayout, {
slots: {
toolbar: '<div>筛选</div>',
state: '<template v-if="false"><div>错误</div></template>',
default: '<div>表格</div>',
},
})
expect(wrapper.find('.list-page-layout__state').exists()).toBe(false)
expect(wrapper.text()).toBe('筛选表格')
})
it('keeps plain-text list regions visible', () => {
const wrapper = mount(ListPageLayout, {
slots: { state: '正在同步', default: '表格内容' },
})
expect(wrapper.get('.list-page-layout__state').text()).toBe('正在同步')
expect(wrapper.get('.list-page-layout__content').text()).toBe('表格内容')
})
it('supports detail regions and form section descriptions', () => {
const detail = mount(DetailPageLayout, {
slots: { header: '<h1>详情</h1>', default: '<p>正文</p>', footer: '<button>保存</button>' },