|
@@ -0,0 +1,236 @@
|
|
|
|
|
+<script setup lang="ts">
|
|
|
|
|
+import Pagination from '@/components/Pagination/index.vue'
|
|
|
|
|
+import { Setting, Refresh } from '@element-plus/icons-vue'
|
|
|
|
|
+
|
|
|
|
|
+interface BaseColumn {
|
|
|
|
|
+ width?: number | string
|
|
|
|
|
+ minWidth?: number | string
|
|
|
|
|
+ fixed?: 'left' | 'right' | boolean
|
|
|
|
|
+ sortable?: boolean | string
|
|
|
|
|
+ align?: 'left' | 'center' | 'right'
|
|
|
|
|
+ formatter?: (row: any, column: any, cellValue: any, index: number) => any
|
|
|
|
|
+ hide?: boolean
|
|
|
|
|
+ isAction?: boolean
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface TypeColumn extends BaseColumn {
|
|
|
|
|
+ type: 'selection' | 'index' | 'expand'
|
|
|
|
|
+ label?: string
|
|
|
|
|
+ prop?: string
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface PropColumn extends BaseColumn {
|
|
|
|
|
+ type?: never
|
|
|
|
|
+ prop?: string
|
|
|
|
|
+ label: string
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+type TableColumn = TypeColumn | PropColumn
|
|
|
|
|
+
|
|
|
|
|
+interface TableCardProps {
|
|
|
|
|
+ data: any[]
|
|
|
|
|
+ columns: TableColumn[]
|
|
|
|
|
+ loading?: boolean
|
|
|
|
|
+ stripe?: boolean
|
|
|
|
|
+ border?: boolean
|
|
|
|
|
+ showPagination?: boolean
|
|
|
|
|
+ showColumnFilter?: boolean
|
|
|
|
|
+ pageNo?: number
|
|
|
|
|
+ pageSize?: number
|
|
|
|
|
+ total?: number
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const props = withDefaults(defineProps<TableCardProps>(), {
|
|
|
|
|
+ data: () => [],
|
|
|
|
|
+ columns: () => [],
|
|
|
|
|
+ loading: false,
|
|
|
|
|
+ stripe: true,
|
|
|
|
|
+ border: false,
|
|
|
|
|
+ showPagination: true,
|
|
|
|
|
+ showColumnFilter: true,
|
|
|
|
|
+ pageNo: 1,
|
|
|
|
|
+ pageSize: 10,
|
|
|
|
|
+ total: 0
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const emit = defineEmits<{
|
|
|
|
|
+ (e: 'update:pageNo', value: number): void
|
|
|
|
|
+ (e: 'update:pageSize', value: number): void
|
|
|
|
|
+ (e: 'change', pageNo: number, pageSize: number): void
|
|
|
|
|
+ (e: 'selection-change', selection: any[]): void
|
|
|
|
|
+ (e: 'sort-change', sortInfo: { prop: string; order: string }): void
|
|
|
|
|
+}>()
|
|
|
|
|
+
|
|
|
|
|
+const visibleColumns = ref<string[]>([])
|
|
|
|
|
+
|
|
|
|
|
+const filterableColumns = computed(() => {
|
|
|
|
|
+ return props.columns.filter(col => col.prop && !col.type && !col.isAction)
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const displayColumns = computed(() => {
|
|
|
|
|
+ return props.columns.filter(col => {
|
|
|
|
|
+ if (col.type) return true // selection/index/expand 始终显示
|
|
|
|
|
+ if (col.isAction) return true // 操作列始终显示
|
|
|
|
|
+ if (!col.prop) return false
|
|
|
|
|
+ return visibleColumns.value.includes(col.prop)
|
|
|
|
|
+ })
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+function initVisibleColumns() {
|
|
|
|
|
+ visibleColumns.value = filterableColumns.value
|
|
|
|
|
+ .filter(col => !col.hide)
|
|
|
|
|
+ .map(col => col.prop as string)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function handleCheckAll(val: boolean | string | number) {
|
|
|
|
|
+ if (val) {
|
|
|
|
|
+ visibleColumns.value = filterableColumns.value.map(col => col.prop as string)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ visibleColumns.value = []
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const isAllChecked = computed(() => {
|
|
|
|
|
+ return filterableColumns.value.length > 0 &&
|
|
|
|
|
+ visibleColumns.value.length === filterableColumns.value.length
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const isIndeterminate = computed(() => {
|
|
|
|
|
+ const len = visibleColumns.value.length
|
|
|
|
|
+ return len > 0 && len < filterableColumns.value.length
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+function handlePageChange(pageNo: number, pageSize: number) {
|
|
|
|
|
+ emit('update:pageNo', pageNo)
|
|
|
|
|
+ emit('update:pageSize', pageSize)
|
|
|
|
|
+ emit('change', pageNo, pageSize)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function handleSelectionChange(selection: any[]) {
|
|
|
|
|
+ emit('selection-change', selection)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function handleSortChange({ prop, order }: { prop: string; order: string | null }) {
|
|
|
|
|
+ emit('sort-change', { prop, order: order || '' })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+watch(
|
|
|
|
|
+ () => props.columns,
|
|
|
|
|
+ () => { initVisibleColumns() },
|
|
|
|
|
+ { immediate: true, deep: true }
|
|
|
|
|
+)
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<template>
|
|
|
|
|
+ <el-card class="table-card" shadow="never">
|
|
|
|
|
+ <div class="table-toolbar">
|
|
|
|
|
+ <div class="toolbar-left">
|
|
|
|
|
+ <slot name="toolbar-left" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="toolbar-right">
|
|
|
|
|
+ <slot name="toolbar-right" />
|
|
|
|
|
+
|
|
|
|
|
+ <el-popover v-if="showColumnFilter && filterableColumns.length > 0" placement="bottom-end" :width="200"
|
|
|
|
|
+ trigger="click">
|
|
|
|
|
+ <template #reference>
|
|
|
|
|
+ <el-button :icon="Setting" circle />
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="column-filter">
|
|
|
|
|
+ <div class="filter-header">
|
|
|
|
|
+ <el-checkbox :model-value="isAllChecked" :indeterminate="isIndeterminate" @change="handleCheckAll">
|
|
|
|
|
+ 全部
|
|
|
|
|
+ </el-checkbox>
|
|
|
|
|
+ <el-button type="primary" link size="small" :icon="Refresh" @click="initVisibleColumns">
|
|
|
|
|
+ 重置
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <el-checkbox-group v-model="visibleColumns" class="filter-list">
|
|
|
|
|
+ <el-checkbox v-for="col in filterableColumns" :key="col.prop" :value="col.prop">
|
|
|
|
|
+ {{ col.label }}
|
|
|
|
|
+ </el-checkbox>
|
|
|
|
|
+ </el-checkbox-group>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-popover>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <el-table :data="data" :stripe="stripe" :border="border" v-loading="loading"
|
|
|
|
|
+ @selection-change="handleSelectionChange" @sort-change="handleSortChange">
|
|
|
|
|
+ <template v-for="col in displayColumns" :key="col.prop ?? col.type ?? col.label">
|
|
|
|
|
+ <el-table-column v-if="col.type" :type="col.type" :width="col.width" :align="col.align ?? 'center'" />
|
|
|
|
|
+ <el-table-column v-else :prop="col.prop" :label="col.label" :width="col.width" :min-width="col.minWidth"
|
|
|
|
|
+ :sortable="col.sortable" :align="col.align ?? 'center'"
|
|
|
|
|
+ :fixed="col.isAction ? (col.fixed ?? 'right') : col.fixed">
|
|
|
|
|
+ <template #default="scope">
|
|
|
|
|
+ <slot :name="`column-${col.prop ?? col.label}`" :row="scope.row" :$index="scope.$index">
|
|
|
|
|
+ {{ col.formatter
|
|
|
|
|
+ ? col.formatter(scope.row, scope.column, col.prop ? scope.row[String(col.prop)] : undefined, scope.$index)
|
|
|
|
|
+ : scope.row[String(col.prop)]
|
|
|
|
|
+ }}
|
|
|
|
|
+ </slot>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <template #empty>
|
|
|
|
|
+ <slot name="empty">
|
|
|
|
|
+ <el-empty description="暂无数据" />
|
|
|
|
|
+ </slot>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+
|
|
|
|
|
+ <Pagination v-if="showPagination" :current-page="pageNo" :page-size="pageSize" :total="total"
|
|
|
|
|
+ @update:current-page="(val) => emit('update:pageNo', val)"
|
|
|
|
|
+ @update:page-size="(val) => emit('update:pageSize', val)" @change="handlePageChange" />
|
|
|
|
|
+
|
|
|
|
|
+ <slot name="footer" />
|
|
|
|
|
+ </el-card>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<style lang="scss" scoped>
|
|
|
|
|
+.table-card {
|
|
|
|
|
+ margin-top: 10px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.table-toolbar {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ margin-bottom: 16px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.toolbar-left,
|
|
|
|
|
+.toolbar-right {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.column-filter {
|
|
|
|
|
+
|
|
|
|
|
+ // ✅ 全部 + 重置:左右分布,垂直居中
|
|
|
|
|
+ .filter-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ padding-bottom: 8px;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+ border-bottom: 1px solid var(--el-border-color-lighter);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ✅ 一行一个 checkbox
|
|
|
|
|
+ .filter-list {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ max-height: 300px;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+
|
|
|
|
|
+ :deep(.el-checkbox) {
|
|
|
|
|
+ height: 32px;
|
|
|
|
|
+ margin-right: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|