Build lightweight AI agent admin
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
export { default, default as MultiDataView } from './multi-data-view.vue';
|
||||
|
||||
export * from './types';
|
||||
@@ -0,0 +1,635 @@
|
||||
<script lang="ts" setup generic="T extends Record<string, any>">
|
||||
import type { VbenFormProps } from '@vben/common-ui';
|
||||
|
||||
import type { PaginationProps } from './types';
|
||||
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
|
||||
import { useVbenForm } from '@vben/common-ui';
|
||||
import { LayoutGrid, List, RefreshCw } from '@vben/icons';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElButtonGroup,
|
||||
ElCheckbox,
|
||||
ElEmpty,
|
||||
ElPagination,
|
||||
ElScrollbar,
|
||||
ElTooltip,
|
||||
} from 'element-plus';
|
||||
|
||||
interface Props {
|
||||
data?: T[];
|
||||
api?: (params: any) => Promise<T[] | { items: T[]; total?: number }>;
|
||||
params?: Record<string, any>;
|
||||
autoLoad?: boolean;
|
||||
title?: string;
|
||||
mode?: 'card' | 'list';
|
||||
rowKey?: string;
|
||||
showToolbar?: boolean;
|
||||
selection?: boolean;
|
||||
pagination?: boolean | PaginationProps;
|
||||
cardGrid?: {
|
||||
gutter?: number;
|
||||
lg?: number;
|
||||
md?: number;
|
||||
sm?: number;
|
||||
xl?: number;
|
||||
xs?: number;
|
||||
};
|
||||
cardActionPosition?: 'footer' | 'overlay';
|
||||
searchForm?: VbenFormProps;
|
||||
/**
|
||||
* 内容区域高度,设置后分页器会固定在底部
|
||||
* 例如: '500px', 'calc(100vh - 200px)'
|
||||
* 如果设置为 'auto',则会自动计算剩余高度
|
||||
*/
|
||||
contentHeight?: string;
|
||||
/**
|
||||
* 自动计算高度时的底部偏移量(默认 50px,用于预留分页器和底部边距)
|
||||
*/
|
||||
bottomOffset?: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
data: () => [],
|
||||
api: undefined,
|
||||
params: () => ({}),
|
||||
autoLoad: true,
|
||||
title: '',
|
||||
mode: 'card',
|
||||
rowKey: 'id',
|
||||
showToolbar: true,
|
||||
selection: false,
|
||||
pagination: true,
|
||||
cardGrid: () => ({
|
||||
xs: 24,
|
||||
sm: 12,
|
||||
md: 8,
|
||||
lg: 6,
|
||||
xl: 6,
|
||||
gutter: 16,
|
||||
}),
|
||||
cardActionPosition: 'footer',
|
||||
searchForm: undefined,
|
||||
contentHeight: undefined,
|
||||
bottomOffset: 50,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
refresh: [];
|
||||
'selection-change': [selection: T[]];
|
||||
'update:mode': [mode: 'card' | 'list'];
|
||||
}>();
|
||||
|
||||
// --- State ---
|
||||
const loading = ref(false);
|
||||
const localMode = ref(props.mode);
|
||||
const items = ref<T[]>([]);
|
||||
const total = ref(0);
|
||||
const selectedKeys = ref<Set<number | string>>(new Set());
|
||||
const searchParams = ref<Record<string, any>>({});
|
||||
|
||||
// 动态高度计算
|
||||
const containerRef = ref<HTMLElement | null>(null);
|
||||
const calculatedHeight = ref<string>('');
|
||||
|
||||
const [Form, formApi] = useVbenForm(props.searchForm || {});
|
||||
|
||||
defineExpose({
|
||||
formApi,
|
||||
fetchData,
|
||||
});
|
||||
|
||||
// Pagination State
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(20);
|
||||
|
||||
// --- Computed ---
|
||||
const isCardMode = computed(() => localMode.value === 'card');
|
||||
|
||||
const displayItems = computed(() => {
|
||||
return props.api ? items.value : props.data;
|
||||
});
|
||||
|
||||
const showPagination = computed(() => {
|
||||
return props.pagination !== false;
|
||||
});
|
||||
|
||||
const paginationProps = computed(() => {
|
||||
const defaultProps = {
|
||||
currentPage: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
pageSizes: [10, 20, 50, 100],
|
||||
layout: 'total, sizes, prev, pager, next, jumper',
|
||||
total: total.value,
|
||||
};
|
||||
if (typeof props.pagination === 'object') {
|
||||
return { ...defaultProps, ...props.pagination };
|
||||
}
|
||||
return defaultProps;
|
||||
});
|
||||
|
||||
// Grid Class logic for Tailwind
|
||||
const gridClasses = computed(() => {
|
||||
// We'll use style binding for the grid instead of classes for dynamic cols if needed,
|
||||
// but mapping props to tailwind grid-cols classes is cleaner if we stick to standard breakpoints.
|
||||
// However, since Element Plus Col works with span (24 grid), we can also use ElRow/ElCol.
|
||||
// Let's use standard CSS Grid with styles for maximum flexibility.
|
||||
return {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `repeat(auto-fill, minmax(250px, 1fr))`,
|
||||
gap: `${props.cardGrid.gutter}px`,
|
||||
};
|
||||
});
|
||||
|
||||
const allSelected = computed(() => {
|
||||
const currentItems = displayItems.value || [];
|
||||
return (
|
||||
currentItems.length > 0 &&
|
||||
currentItems.every((item) => selectedKeys.value.has(item[props.rowKey]))
|
||||
);
|
||||
});
|
||||
|
||||
const isIndeterminate = computed(() => {
|
||||
const currentItems = displayItems.value || [];
|
||||
const selectedCount = currentItems.filter((item) =>
|
||||
selectedKeys.value.has(item[props.rowKey]),
|
||||
).length;
|
||||
return selectedCount > 0 && selectedCount < currentItems.length;
|
||||
});
|
||||
|
||||
// 实际使用的内容高度
|
||||
const actualContentHeight = computed(() => {
|
||||
if (props.contentHeight === 'auto') {
|
||||
return calculatedHeight.value || '400px';
|
||||
}
|
||||
return props.contentHeight;
|
||||
});
|
||||
|
||||
// 计算剩余高度
|
||||
function calculateHeight() {
|
||||
if (props.contentHeight !== 'auto' || !containerRef.value) return;
|
||||
|
||||
nextTick(() => {
|
||||
const rect = containerRef.value?.getBoundingClientRect();
|
||||
if (rect) {
|
||||
const viewportHeight = window.innerHeight;
|
||||
const remainingHeight = viewportHeight - rect.top - props.bottomOffset;
|
||||
calculatedHeight.value = `${Math.max(200, remainingHeight)}px`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --- Methods ---
|
||||
|
||||
function setMode(mode: 'card' | 'list') {
|
||||
localMode.value = mode;
|
||||
emit('update:mode', mode);
|
||||
}
|
||||
|
||||
// Data Fetching
|
||||
async function fetchData() {
|
||||
if (!props.api) return;
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const requestParams = {
|
||||
page: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
...props.params,
|
||||
...searchParams.value,
|
||||
};
|
||||
|
||||
const res = await props.api(requestParams);
|
||||
if (Array.isArray(res)) {
|
||||
items.value = res;
|
||||
total.value = res.length;
|
||||
} else {
|
||||
items.value = res.items || [];
|
||||
total.value = res.total || 0;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch data:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Selection Logic
|
||||
function toggleSelection(item: any) {
|
||||
if (!props.selection) return;
|
||||
const key = item[props.rowKey];
|
||||
if (selectedKeys.value.has(key)) {
|
||||
selectedKeys.value.delete(key);
|
||||
} else {
|
||||
selectedKeys.value.add(key);
|
||||
}
|
||||
emitSelectionChange();
|
||||
}
|
||||
|
||||
function toggleSelectAll(val: boolean | number | string) {
|
||||
if (!props.selection) return;
|
||||
const currentItems = displayItems.value || [];
|
||||
if (val) {
|
||||
currentItems.forEach((item) => selectedKeys.value.add(item[props.rowKey]));
|
||||
} else {
|
||||
currentItems.forEach((item) =>
|
||||
selectedKeys.value.delete(item[props.rowKey]),
|
||||
);
|
||||
}
|
||||
emitSelectionChange();
|
||||
}
|
||||
|
||||
function emitSelectionChange() {
|
||||
const currentItems = displayItems.value || [];
|
||||
const selectedItems = currentItems.filter((item) =>
|
||||
selectedKeys.value.has(item[props.rowKey]),
|
||||
);
|
||||
emit('selection-change', selectedItems as T[]);
|
||||
}
|
||||
|
||||
function isSelected(item: any) {
|
||||
return selectedKeys.value.has(item[props.rowKey]);
|
||||
}
|
||||
|
||||
function handlePageChange(val: number) {
|
||||
currentPage.value = val;
|
||||
fetchData();
|
||||
}
|
||||
|
||||
function handleSizeChange(val: number) {
|
||||
pageSize.value = val;
|
||||
currentPage.value = 1; // Reset to first page
|
||||
fetchData();
|
||||
}
|
||||
|
||||
function handleRefresh() {
|
||||
fetchData();
|
||||
emit('refresh');
|
||||
}
|
||||
|
||||
async function handleSearch(params: Record<string, any>) {
|
||||
searchParams.value = params;
|
||||
currentPage.value = 1;
|
||||
await fetchData();
|
||||
}
|
||||
|
||||
// --- Lifecycle & Watchers ---
|
||||
|
||||
watch(
|
||||
() => props.params,
|
||||
() => {
|
||||
if (props.api) {
|
||||
currentPage.value = 1;
|
||||
fetchData();
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.mode,
|
||||
(val) => {
|
||||
if (val) localMode.value = val;
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
if (props.api && props.autoLoad) {
|
||||
fetchData();
|
||||
} else if (props.data) {
|
||||
// If using static data, just init total if needed
|
||||
total.value = props.data.length;
|
||||
}
|
||||
|
||||
// 自动计算高度
|
||||
if (props.contentHeight === 'auto') {
|
||||
calculateHeight();
|
||||
window.addEventListener('resize', calculateHeight);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (props.contentHeight === 'auto') {
|
||||
window.removeEventListener('resize', calculateHeight);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="multi-data-view w-full">
|
||||
<!-- Search Form -->
|
||||
<div v-if="searchForm" class="bg-card mb-4 rounded-lg border p-4">
|
||||
<Form @submit="handleSearch" />
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<!-- Title Slot -->
|
||||
<div class="text-lg font-bold">
|
||||
<slot name="header">
|
||||
{{ title }}
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<!-- Selection Status -->
|
||||
<div
|
||||
v-if="selection && selectedKeys.size > 0"
|
||||
class="text-muted-foreground flex items-center gap-2 text-sm"
|
||||
>
|
||||
<span>已选择 {{ selectedKeys.size }} 项</span>
|
||||
<ElButton link type="primary" @click="toggleSelectAll(false)">
|
||||
清空
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Toolbar -->
|
||||
<div class="flex items-center gap-2">
|
||||
<slot name="toolbar"></slot>
|
||||
|
||||
<div v-if="showToolbar" class="flex items-center">
|
||||
<div class="bg-border mx-2 h-4 w-[1px]"></div>
|
||||
<ElButtonGroup>
|
||||
<ElTooltip content="卡片视图" placement="top">
|
||||
<ElButton
|
||||
:type="localMode === 'card' ? 'primary' : ''"
|
||||
:icon="LayoutGrid"
|
||||
@click="setMode('card')"
|
||||
/>
|
||||
</ElTooltip>
|
||||
<ElTooltip content="列表视图" placement="top">
|
||||
<ElButton
|
||||
:type="localMode === 'list' ? 'primary' : ''"
|
||||
:icon="List"
|
||||
@click="setMode('list')"
|
||||
/>
|
||||
</ElTooltip>
|
||||
<ElTooltip content="刷新" placement="top">
|
||||
<ElButton :icon="RefreshCw" @click="handleRefresh" />
|
||||
</ElTooltip>
|
||||
</ElButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Area -->
|
||||
<div ref="containerRef" v-loading="loading" class="min-h-[200px]">
|
||||
<ElScrollbar v-if="actualContentHeight" :height="actualContentHeight">
|
||||
<!-- Empty State -->
|
||||
<div
|
||||
v-if="!loading && (!displayItems || displayItems.length === 0)"
|
||||
class="py-8"
|
||||
>
|
||||
<slot name="empty">
|
||||
<ElEmpty description="暂无数据" />
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<!-- Card Mode -->
|
||||
<div v-if="isCardMode" class="grid-container" :style="gridClasses">
|
||||
<div
|
||||
v-for="item in displayItems"
|
||||
:key="item[rowKey]"
|
||||
class="bg-card group relative overflow-hidden rounded-xl border shadow-sm transition-all duration-200"
|
||||
:class="[
|
||||
isSelected(item) ? 'ring-primary ring-2' : 'hover:shadow-lg',
|
||||
]"
|
||||
>
|
||||
<!-- Card Selection Checkbox (Top Left) -->
|
||||
<div
|
||||
v-if="selection"
|
||||
class="absolute left-2 top-2 z-10"
|
||||
:class="{
|
||||
'opacity-0 group-hover:opacity-100': !isSelected(item),
|
||||
}"
|
||||
>
|
||||
<ElCheckbox
|
||||
:model-value="isSelected(item)"
|
||||
@change="toggleSelection(item)"
|
||||
@click.stop
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Card Content -->
|
||||
<div class="relative z-[1] p-4">
|
||||
<slot name="card" :item="item">
|
||||
<!-- Default Fallback -->
|
||||
<div class="font-medium">{{ item[title] || item.name }}</div>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<!-- Card Actions Overlay (Center) -->
|
||||
<div
|
||||
v-if="cardActionPosition === 'overlay'"
|
||||
class="absolute inset-0 z-[10] flex items-center justify-center opacity-0 transition-opacity duration-300 group-hover:opacity-100"
|
||||
style="background-color: rgb(0 0 0 / 60%)"
|
||||
>
|
||||
<div class="flex items-center gap-2" @click.stop>
|
||||
<slot name="actions" :item="item"></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card Footer Actions -->
|
||||
<div
|
||||
v-if="cardActionPosition === 'footer'"
|
||||
class="border-border flex items-center justify-end gap-2 border-t bg-gray-50 px-4 py-2 dark:bg-gray-800/50"
|
||||
>
|
||||
<slot name="actions" :item="item"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- List Mode -->
|
||||
<div v-else class="flex flex-col gap-2">
|
||||
<!-- List Header (Optional for Select All) -->
|
||||
<div
|
||||
v-if="selection"
|
||||
class="bg-muted/30 flex items-center rounded px-4 py-2"
|
||||
>
|
||||
<ElCheckbox
|
||||
:model-value="allSelected"
|
||||
:indeterminate="isIndeterminate"
|
||||
@change="toggleSelectAll"
|
||||
>
|
||||
全选
|
||||
</ElCheckbox>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="item in displayItems"
|
||||
:key="item[rowKey]"
|
||||
class="hover:bg-accent/50 group flex items-center justify-between rounded border p-3 transition-colors"
|
||||
:class="{ 'bg-primary/5 border-primary/30': isSelected(item) }"
|
||||
>
|
||||
<div class="flex flex-1 items-center gap-4">
|
||||
<!-- List Selection Checkbox -->
|
||||
<ElCheckbox
|
||||
v-if="selection"
|
||||
:model-value="isSelected(item)"
|
||||
@change="toggleSelection(item)"
|
||||
/>
|
||||
|
||||
<div class="flex-1">
|
||||
<slot name="list" :item="item">
|
||||
<!-- Default Fallback -->
|
||||
<div class="font-medium">
|
||||
{{ item[title] || item.name }}
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- List Actions -->
|
||||
<div class="ml-4 flex items-center gap-2">
|
||||
<slot name="actions" :item="item"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ElScrollbar>
|
||||
|
||||
<!-- Without Scrollbar (no contentHeight) -->
|
||||
<template v-else>
|
||||
<!-- Empty State -->
|
||||
<div
|
||||
v-if="!loading && (!displayItems || displayItems.length === 0)"
|
||||
class="py-8"
|
||||
>
|
||||
<slot name="empty">
|
||||
<ElEmpty description="暂无数据" />
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<!-- Card Mode -->
|
||||
<div v-if="isCardMode" class="grid-container" :style="gridClasses">
|
||||
<div
|
||||
v-for="item in displayItems"
|
||||
:key="item[rowKey]"
|
||||
class="bg-card group relative overflow-hidden rounded-xl border shadow-sm transition-all duration-200"
|
||||
:class="[
|
||||
isSelected(item) ? 'ring-primary ring-2' : 'hover:shadow-lg',
|
||||
]"
|
||||
>
|
||||
<!-- Card Selection Checkbox (Top Left) -->
|
||||
<div
|
||||
v-if="selection"
|
||||
class="absolute left-2 top-2 z-10"
|
||||
:class="{
|
||||
'opacity-0 group-hover:opacity-100': !isSelected(item),
|
||||
}"
|
||||
>
|
||||
<ElCheckbox
|
||||
:model-value="isSelected(item)"
|
||||
@change="toggleSelection(item)"
|
||||
@click.stop
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Card Content -->
|
||||
<div class="relative z-[1] p-4">
|
||||
<slot name="card" :item="item">
|
||||
<!-- Default Fallback -->
|
||||
<div class="font-medium">{{ item[title] || item.name }}</div>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<!-- Card Actions Overlay (Center) -->
|
||||
<div
|
||||
v-if="cardActionPosition === 'overlay'"
|
||||
class="absolute inset-0 z-[10] flex items-center justify-center opacity-0 transition-opacity duration-300 group-hover:opacity-100"
|
||||
style="background-color: rgb(0 0 0 / 60%)"
|
||||
>
|
||||
<div class="flex items-center gap-2" @click.stop>
|
||||
<slot name="actions" :item="item"></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card Footer Actions -->
|
||||
<div
|
||||
v-if="cardActionPosition === 'footer'"
|
||||
class="border-border flex items-center justify-end gap-2 border-t bg-gray-50 px-4 py-2 dark:bg-gray-800/50"
|
||||
>
|
||||
<slot name="actions" :item="item"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- List Mode -->
|
||||
<div v-else class="flex flex-col gap-2">
|
||||
<!-- List Header (Optional for Select All) -->
|
||||
<div
|
||||
v-if="selection"
|
||||
class="bg-muted/30 flex items-center rounded px-4 py-2"
|
||||
>
|
||||
<ElCheckbox
|
||||
:model-value="allSelected"
|
||||
:indeterminate="isIndeterminate"
|
||||
@change="toggleSelectAll"
|
||||
>
|
||||
全选
|
||||
</ElCheckbox>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="item in displayItems"
|
||||
:key="item[rowKey]"
|
||||
class="hover:bg-accent/50 group flex items-center justify-between rounded border p-3 transition-colors"
|
||||
:class="{ 'bg-primary/5 border-primary/30': isSelected(item) }"
|
||||
>
|
||||
<div class="flex flex-1 items-center gap-4">
|
||||
<!-- List Selection Checkbox -->
|
||||
<ElCheckbox
|
||||
v-if="selection"
|
||||
:model-value="isSelected(item)"
|
||||
@change="toggleSelection(item)"
|
||||
/>
|
||||
|
||||
<div class="flex-1">
|
||||
<slot name="list" :item="item">
|
||||
<!-- Default Fallback -->
|
||||
<div class="font-medium">
|
||||
{{ item[title] || item.name }}
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- List Actions -->
|
||||
<div class="ml-4 flex items-center gap-2">
|
||||
<slot name="actions" :item="item"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Footer Pagination -->
|
||||
<div v-if="showPagination" class="mt-4 flex justify-end">
|
||||
<slot name="footer">
|
||||
<ElPagination
|
||||
v-bind="paginationProps"
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.grid-container {
|
||||
display: grid;
|
||||
|
||||
/* Default responsive grid if no specific config provided via style binding overrides */
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,76 @@
|
||||
export interface MultiDataViewProps<T = any> {
|
||||
/**
|
||||
* 静态数据源
|
||||
*/
|
||||
data?: T[];
|
||||
/**
|
||||
* API 数据请求函数
|
||||
* (params: any) => Promise<{ items: T[]; total: number }>
|
||||
*/
|
||||
api?: (params: any) => Promise<T[] | { items: T[]; total?: number }>;
|
||||
/**
|
||||
* API 请求的额外参数
|
||||
*/
|
||||
params?: Record<string, any>;
|
||||
/**
|
||||
* 是否自动加载 API 数据
|
||||
* @default true
|
||||
*/
|
||||
autoLoad?: boolean;
|
||||
/**
|
||||
* 标题
|
||||
*/
|
||||
title?: string;
|
||||
/**
|
||||
* 显示模式
|
||||
* @default 'card'
|
||||
*/
|
||||
mode?: 'card' | 'list';
|
||||
/**
|
||||
* 数据主键字段名
|
||||
* @default 'id'
|
||||
*/
|
||||
rowKey?: string;
|
||||
/**
|
||||
* 是否显示工具栏
|
||||
* @default true
|
||||
*/
|
||||
showToolbar?: boolean;
|
||||
/**
|
||||
* 是否开启多选
|
||||
* @default false
|
||||
*/
|
||||
selection?: boolean;
|
||||
/**
|
||||
* 分页配置
|
||||
* boolean: 是否显示
|
||||
* object: ElPagination props
|
||||
* @default true
|
||||
*/
|
||||
pagination?: boolean | PaginationProps;
|
||||
/**
|
||||
* 卡片网格配置 (参考 Element Plus Row/Col 或 Tailwind)
|
||||
* 如果不传,使用默认响应式配置
|
||||
*/
|
||||
cardGrid?: {
|
||||
gutter?: number;
|
||||
lg?: number; // >=1200px span
|
||||
md?: number; // >=992px span
|
||||
sm?: number; // >=768px span
|
||||
xl?: number; // >=1920px span
|
||||
xs?: number; // <768px span
|
||||
};
|
||||
/**
|
||||
* 卡片模式下操作按钮位置
|
||||
* @default 'overlay'
|
||||
*/
|
||||
cardActionPosition?: 'footer' | 'overlay';
|
||||
}
|
||||
|
||||
export interface PaginationProps {
|
||||
currentPage?: number;
|
||||
pageSize?: number;
|
||||
total?: number;
|
||||
pageSizes?: number[];
|
||||
layout?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user