This commit is contained in:
2026-01-20 22:54:35 +08:00
parent 656c328aef
commit 8d4290e131
5 changed files with 1113 additions and 258 deletions

View File

@@ -1,349 +1,596 @@
<template>
<div class="sc-table-wrapper">
<!-- 表格操作 -->
<div class="table-toolbar" v-if="showToolbar">
<div class="sc-table-container" :class="{ 'is-full-screen': isFullScreen }">
<!-- 工具 -->
<div v-if="showToolbar" class="sc-table-toolbar">
<div class="toolbar-left">
<slot name="toolbar-left"></slot>
</div>
<div class="toolbar-right">
<!-- 列设置 -->
<a-dropdown v-if="showColumnSetting" :trigger="['click']">
<a-button size="small">
<SettingOutlined /> 列设置
</a-button>
<template #overlay>
<a-menu @click="handleColumnSetting">
<a-menu-item v-for="column in columns" :key="column.key || column.dataIndex">
<a-checkbox :checked="visibleColumns.includes(column.key || column.dataIndex)">
{{ column.title }}
</a-checkbox>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
<!-- 刷新按钮 -->
<a-button v-if="showRefresh" size="small" @click="handleRefresh">
<ReloadOutlined />
</a-button>
<slot name="toolbar-right"></slot>
<slot name="toolbar-right">
<a-tooltip :title="$t('common.refresh')">
<a-button @click="handleRefresh" :loading="loading">
<template #icon>
<ReloadOutlined />
</template>
</a-button>
</a-tooltip>
<a-tooltip :title="$t('common.columns')">
<a-button @click="openColumnSettings">
<template #icon>
<SettingOutlined />
</template>
</a-button>
</a-tooltip>
<a-tooltip :title="isFullScreen ? $t('common.exitFullScreen') : $t('common.fullScreen')">
<a-button @click="toggleFullScreen">
<template #icon>
<FullscreenOutlined v-if="!isFullScreen" />
<FullscreenExitOutlined v-else />
</template>
</a-button>
</a-tooltip>
</slot>
</div>
</div>
<!-- 表格主体 -->
<a-table v-bind="tableProps" :columns="processedColumns" :data-source="dataSource"
:pagination="paginationConfig" :loading="loading" :row-selection="rowSelectionConfig" :scroll="scrollConfig"
@change="handleTableChange" @row="handleRow" @row-click="handleRowClick" @row-dblclick="handleRowDblClick">
<!-- 自定义列插槽将通过customRender在processedColumns中处理 -->
<div class="sc-table-wrapper" :style="{ height: tableHeight }">
<a-table ref="tableRef" v-bind="$attrs" :columns="visibleColumns" :data-source="dataSource"
:loading="loading" :pagination="paginationConfig" :row-key="rowKey" :scroll="{ x: scrollX, y: scrollY }"
:size="tableSize" :bordered="bordered" :row-selection="rowSelectionConfig" :custom-row="customRow"
@change="handleTableChange" @resizeColumn="handleColumnResize">
<!-- 自定义列插槽 -->
<template v-for="column in columns" #[column.slot]="text, record, index" :key="column.dataIndex">
<slot :name="column.slot" :text="text" :record="record" :index="index"></slot>
</template>
<!-- 操作列插槽 -->
<template v-if="showActionColumn" #action="scope">
<slot name="action" v-bind="scope"></slot>
</template>
<!-- 操作列 -->
<template v-if="showRowActions" #action="{ record, index }">
<div class="row-actions">
<template v-for="(action, idx) in getVisibleActions(record)" :key="idx">
<a v-if="action.show !== false && (typeof action.show === 'function' ? action.show(record, index) : true)"
:style="{ color: action.danger ? '#ff4d4f' : '' }"
@click="handleAction(action, record, index)">
{{ action.label }}
</a>
<a-divider v-if="idx < getVisibleActions(record).length - 1" type="vertical" />
</template>
</div>
</template>
<!-- 空数据提示 -->
<template #empty>
<a-empty v-if="!error" :description="emptyText" />
<div v-else class="table-error">
<span>{{ errorMessage }}</span>
<a-button size="small" type="link" @click="handleRefresh">重试</a-button>
<!-- 空状态 -->
<template #emptyText>
<div class="table-empty">
<template v-if="error">
<a-result status="error" :title="$t('common.error')">
<template #subTitle>
{{ error }}
</template>
<template #extra>
<a-button type="primary" @click="handleRefresh">{{ $t('common.retry') }}</a-button>
</template>
</a-result>
</template>
<template v-else-if="!dataSource || dataSource.length === 0">
<a-empty :description="$t('common.noData')" />
</template>
</div>
</template>
</a-table>
</div>
<!-- 列设置抽屉 -->
<a-drawer v-model:open="columnSettingsVisible" :title="$t('common.columnSettings')" placement="right"
:width="300">
<div class="column-settings">
<a-checkbox-group v-model:value="selectedColumns" @change="handleColumnChange">
<div v-for="column in columns" :key="column.dataIndex" class="column-item">
<a-checkbox :value="column.dataIndex" :disabled="column.fixed || column.required">
{{ column.title }}
</a-checkbox>
</div>
</a-checkbox-group>
<div class="column-actions">
<a-button @click="selectAllColumns">{{ $t('common.selectAll') }}</a-button>
<a-button @click="unselectAllColumns">{{ $t('common.unselectAll') }}</a-button>
<a-button @click="resetColumns">{{ $t('common.reset') }}</a-button>
</div>
</template>
</a-table>
</div>
</a-drawer>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { SettingOutlined, ReloadOutlined } from '@ant-design/icons-vue'
import { ref, computed, watch, onMounted, nextTick } from 'vue'
import { useI18n } from 'vue-i18n'
import {
ReloadOutlined,
SettingOutlined,
FullscreenOutlined,
FullscreenExitOutlined
} from '@ant-design/icons-vue'
const props = defineProps({
// 表格列配置
columns: {
type: Array,
default: () => [],
required: true,
default: () => []
},
// 数据源
dataSource: {
type: Array,
default: () => [],
default: () => []
},
// 加载状态
loading: {
type: Boolean,
default: false,
default: false
},
// 错误状态
error: {
type: Boolean,
default: false,
},
// 错误信息
errorMessage: {
error: {
type: String,
default: '加载失败,请稍后重试',
default: ''
},
// 空数据提示
emptyText: {
type: String,
default: '暂无数据',
// 行键
rowKey: {
type: [String, Function],
default: 'id'
},
// 分页配置
pagination: {
type: Object,
default: () => ({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
pageSizeOptions: ['10', '20', '50', '100'],
showTotal: (total, range) => `${total} 条记录,第 ${range[0]}-${range[1]}`,
}),
},
// 是否显示分页
showPagination: {
type: Boolean,
default: true,
},
// 行选择配置
rowSelection: {
type: Object,
default: () => ({
type: 'checkbox',
selectedRowKeys: [],
onChange: () => { },
}),
},
// 是否显示行选择
showRowSelection: {
type: Boolean,
default: false,
},
// 滚动配置
scroll: {
type: Object,
default: () => ({}),
},
// 表格属性
tableProps: {
type: Object,
default: () => ({}),
},
// 操作列配置
actionColumn: {
type: Object,
default: () => ({
title: '操作',
key: 'action',
width: 150,
fixed: 'right',
}),
},
// 是否显示操作列
showActionColumn: {
type: Boolean,
default: false,
},
// 是否显示工具栏
showToolbar: {
type: Boolean,
default: true,
default: true
},
// 是否显示列设置
showColumnSetting: {
// 是否显示行操作
showRowActions: {
type: Boolean,
default: true,
default: false
},
// 是否显示刷新按钮
showRefresh: {
type: Boolean,
default: true,
},
// 可见列配置
visibleColumns: {
// 行操作配置
rowActions: {
type: Array,
default: () => [],
default: () => []
},
// 分页配置
pagination: {
type: [Object, Boolean],
default: () => ({
current: 1,
pageSize: 10,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total}`,
pageSizeOptions: ['10', '20', '50', '100']
})
},
// 表格尺寸
size: {
type: String,
default: 'middle',
validator: (value) => ['large', 'middle', 'small'].includes(value)
},
// 是否显示边框
bordered: {
type: Boolean,
default: false
},
// 行选择配置
rowSelection: {
type: [Object, Boolean],
default: false
},
// 表格高度
height: {
type: [String, Number],
default: 'auto'
},
// 是否启用数据缓存
enableCache: {
type: Boolean,
default: true
},
// 缓存键
cacheKey: {
type: String,
default: ''
},
// 自定义行属性
customRow: {
type: Function,
default: null
}
})
const emit = defineEmits(['change', 'row-click', 'row-dblclick', 'refresh', 'column-change', 'selection-change', 'page-change', 'size-change', 'sort-change', 'filter-change'])
const emit = defineEmits([
'refresh',
'change',
'action',
'selection-change',
'page-change',
'size-change'
])
// 处理后的列配置
const processedColumns = computed(() => {
let result = [...props.columns]
const { t } = useI18n()
const tableRef = ref(null)
const columnSettingsVisible = ref(false)
const selectedColumns = ref([])
const isFullScreen = ref(false)
const columnWidths = ref({})
// 过滤可见列
if (props.visibleColumns.length > 0) {
result = result.filter((column) => props.visibleColumns.includes(column.key || column.dataIndex))
// 响应式处理
const tableSize = computed(() => {
if (typeof window !== 'undefined') {
const width = window.innerWidth
if (width < 768) return 'small'
if (width < 1200) return 'middle'
}
return props.size
})
// 为每列添加customRender支持插槽
result = result.map((column) => {
const columnKey = column.key || column.dataIndex
return {
...column,
customRender: (_, record, index) => {
// 使用渲染函数返回空内容,实际内容通过插槽渲染
return null
},
}
})
// 添加操作列
if (props.showActionColumn) {
result.push({
...props.actionColumn,
customRender: (_, record, index) => {
// 使用渲染函数返回空内容,实际内容通过插槽渲染
return null
},
})
// 表格高度
const tableHeight = computed(() => {
if (isFullScreen.value) {
return 'calc(100vh - 64px)'
}
return typeof props.height === 'number' ? `${props.height}px` : props.height
})
return result
// 滚动配置
const scrollY = computed(() => {
if (tableHeight.value !== 'auto') {
return parseInt(tableHeight.value) - 100
}
return undefined
})
const scrollX = computed(() => {
const totalWidth = visibleColumns.value.reduce((sum, col) => {
return sum + (columnWidths.value[col.dataIndex] || col.width || 150)
}, 0)
return totalWidth > 0 ? totalWidth : undefined
})
// 分页配置
const paginationConfig = computed(() => {
if (!props.showPagination) return false
if (!props.pagination) return false
return {
...props.pagination,
onChange: (page, pageSize) => {
emit('page-change', page)
emit('change', { current: page, pageSize })
},
onShowSizeChange: (current, pageSize) => {
emit('size-change', pageSize)
emit('change', { current, pageSize })
},
current: props.pagination.current,
pageSize: props.pagination.pageSize,
total: props.pagination.total || 0
}
})
// 行选择配置
const rowSelectionConfig = computed(() => {
if (!props.showRowSelection) return null
if (!props.rowSelection) return undefined
return {
...props.rowSelection,
onChange: (selectedRowKeys, selectedRows) => {
emit('selection-change', selectedRowKeys, selectedRows)
},
onChange: handleSelectionChange
}
})
// 滚动配置
const scrollConfig = computed(() => {
return {
...props.scroll,
}
// 可见列
const visibleColumns = computed(() => {
return props.columns
.filter(col => selectedColumns.value.includes(col.dataIndex))
.map(col => ({
...col,
width: columnWidths.value[col.dataIndex] || col.width || 150
}))
})
// 处理表格变化
const handleTableChange = (pagination, filters, sorter, extra) => {
if (sorter.field) {
emit('sort-change', sorter)
}
if (Object.keys(filters).length > 0) {
emit('filter-change', filters)
}
emit('change', { pagination, filters, sorter, extra })
}
// 处理行点击
const handleRowClick = (record, event) => {
emit('row-click', record, event)
}
// 处理行双击
const handleRowDblClick = (record, event) => {
emit('row-dblclick', record, event)
}
// 处理行配置
const handleRow = (record) => {
return {
onClick: (event) => handleRowClick(record, event),
onDblclick: (event) => handleRowDblClick(record, event),
}
}
// 处理列设置
const handleColumnSetting = ({ key, domEvent }) => {
const checkbox = domEvent.target.closest('.ant-checkbox-wrapper').querySelector('.ant-checkbox-input')
const checked = checkbox.checked
emit('column-change', {
key,
visible: checked,
// 获取可见的操作按钮
const getVisibleActions = (record) => {
return props.rowActions.filter(action => {
if (typeof action.show === 'function') {
return action.show(record)
}
return action.show !== false
})
}
// 处理刷新
// 初始化
const init = () => {
// 初始化选中列
selectedColumns.value = props.columns.map(col => col.dataIndex)
// 加载缓存
if (props.enableCache && props.cacheKey) {
loadFromCache()
}
// 响应式监听
if (typeof window !== 'undefined') {
window.addEventListener('resize', handleResize)
}
}
// 处理列宽调整
const handleColumnResize = (width, column) => {
columnWidths.value[column.dataIndex] = width
saveToCache()
}
// 处理表格变化
const handleTableChange = (pagination, filters, sorter) => {
emit('change', { pagination, filters, sorter })
emit('page-change', pagination.current)
emit('size-change', pagination.pageSize)
}
// 处理选择变化
const handleSelectionChange = (selectedRowKeys, selectedRows) => {
emit('selection-change', selectedRowKeys, selectedRows)
}
// 处理行操作
const handleAction = (action, record, index) => {
if (action.handler) {
action.handler(record, index)
}
emit('action', action.key, record, index)
}
// 刷新
const handleRefresh = () => {
emit('refresh')
}
// 打开列设置
const openColumnSettings = () => {
columnSettingsVisible.value = true
}
// 列变化处理
const handleColumnChange = (checkedValues) => {
selectedColumns.value = checkedValues
saveToCache()
}
// 全选列
const selectAllColumns = () => {
selectedColumns.value = props.columns.map(col => col.dataIndex)
saveToCache()
}
// 取消全选
const unselectAllColumns = () => {
const requiredColumns = props.columns
.filter(col => col.fixed || col.required)
.map(col => col.dataIndex)
selectedColumns.value = [...requiredColumns]
saveToCache()
}
// 重置列
const resetColumns = () => {
selectedColumns.value = props.columns.map(col => col.dataIndex)
columnWidths.value = {}
saveToCache()
}
// 全屏切换
const toggleFullScreen = () => {
isFullScreen.value = !isFullScreen.value
}
// 响应式处理
const handleResize = () => {
// 响应式逻辑已通过 computed 处理
}
// 保存到缓存
const saveToCache = () => {
if (!props.enableCache || !props.cacheKey) return
const cacheData = {
selectedColumns: selectedColumns.value,
columnWidths: columnWidths.value
}
try {
localStorage.setItem(`sc-table-${props.cacheKey}`, JSON.stringify(cacheData))
} catch (e) {
console.warn('Failed to save table cache:', e)
}
}
// 从缓存加载
const loadFromCache = () => {
if (!props.enableCache || !props.cacheKey) return
try {
const cacheData = localStorage.getItem(`sc-table-${props.cacheKey}`)
if (cacheData) {
const { selectedColumns: cachedSelectedColumns, columnWidths: cachedWidths } = JSON.parse(cacheData)
// 验证缓存的列是否仍然存在
const validColumns = props.columns.map(col => col.dataIndex)
if (cachedSelectedColumns) {
selectedColumns.value = cachedSelectedColumns.filter(col => validColumns.includes(col))
}
if (cachedWidths) {
columnWidths.value = cachedWidths
}
}
} catch (e) {
console.warn('Failed to load table cache:', e)
}
}
// 清除缓存
const clearCache = () => {
if (!props.cacheKey) return
try {
localStorage.removeItem(`sc-table-${props.cacheKey}`)
} catch (e) {
console.warn('Failed to clear table cache:', e)
}
}
// 获取选中行
const getSelectedRows = () => {
if (!tableRef.value) return []
return tableRef.value.selectedRows || []
}
// 获取选中行键
const getSelectedRowKeys = () => {
if (!tableRef.value) return []
return tableRef.value.selectedRowKeys || []
}
// 暴露方法
defineExpose({
handleRefresh,
tableRef,
getSelectedRows,
getSelectedRowKeys,
clearCache,
refresh: handleRefresh
})
// 生命周期
onMounted(() => {
init()
})
// 监听列配置变化
watch(() => props.columns, (newColumns) => {
const validColumns = newColumns.map(col => col.dataIndex)
selectedColumns.value = selectedColumns.value.filter(col => validColumns.includes(col))
}, { deep: true })
</script>
<style scoped lang="scss">
<style lang="scss" scoped>
.sc-table-container {
width: 100%;
height: 100%;
&.is-full-screen {
position: fixed;
top: 64px;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
background: #fff;
padding: 16px;
}
}
.sc-table-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 12px 16px;
background: #fafafa;
border-radius: 4px;
.toolbar-left,
.toolbar-right {
display: flex;
align-items: center;
gap: 8px;
}
}
.sc-table-wrapper {
width: 100%;
background-color: #fff;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.table-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
:deep(.ant-table) {
font-size: 14px;
.ant-table-thead>tr>th {
background: #fafafa;
font-weight: 600;
transition: all 0.3s;
}
.ant-table-tbody>tr {
transition: all 0.3s;
&:hover {
background: #f5f5f5;
}
}
}
}
.row-actions {
display: flex;
align-items: center;
gap: 8px;
a {
font-size: 13px;
cursor: pointer;
transition: color 0.3s;
&:hover {
opacity: 0.8;
}
}
}
.table-empty {
padding: 40px 0;
}
.column-settings {
.column-item {
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
.toolbar-left,
.toolbar-right {
display: flex;
align-items: center;
gap: 8px;
&:last-child {
border-bottom: none;
}
}
.table-error {
.column-actions {
margin-top: 24px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 0;
gap: 8px;
flex-wrap: wrap;
}
}
.error-icon {
font-size: 48px;
color: #ff4d4f;
margin-bottom: 16px;
// 响应式布局
@media (max-width: 768px) {
.sc-table-toolbar {
flex-direction: column;
align-items: stretch;
gap: 12px;
.toolbar-left,
.toolbar-right {
justify-content: space-between;
}
}
:deep(.ant-table) {
font-size: 12px;
}
.row-actions {
flex-wrap: wrap;
gap: 4px;
}
}
@media (max-width: 576px) {
.sc-table-toolbar {
padding: 8px;
}
:deep(.ant-pagination) {
font-size: 12px;
.ant-pagination-item {
min-width: 28px;
height: 28px;
line-height: 26px;
}
}
}