508 lines
16 KiB
Vue
508 lines
16 KiB
Vue
<template>
|
||
<div class="table-column-control">
|
||
<el-button
|
||
:type="triggerType"
|
||
:icon="Setting"
|
||
:size="triggerSize"
|
||
:circle="triggerCircle"
|
||
:link="triggerLink"
|
||
@click="visible = true"
|
||
>
|
||
<slot name="trigger">
|
||
{{ triggerText || '列设置' }}
|
||
</slot>
|
||
</el-button>
|
||
|
||
<el-dialog
|
||
v-model="visible"
|
||
title="列显示设置"
|
||
:width="dialogWidth"
|
||
append-to-body
|
||
>
|
||
<div class="column-control-content">
|
||
<div class="column-control-header">
|
||
<div class="header-actions">
|
||
<el-button
|
||
type="primary"
|
||
link
|
||
@click="handleToggleSelectAll"
|
||
>
|
||
{{ isAllSelected ? '取消全选' : '全选' }}
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="column-control-body">
|
||
<el-checkbox-group v-model="checkedColumns" @change="handleColumnChange" class="column-checkbox-group">
|
||
<div
|
||
v-for="column in actualColumns"
|
||
:key="column.prop || column.label"
|
||
class="column-item"
|
||
>
|
||
<el-checkbox
|
||
:label="column.prop || column.label"
|
||
:disabled="!!column.fixed || column.alwaysShow"
|
||
>
|
||
{{ column.label }}
|
||
</el-checkbox>
|
||
<el-tag v-if="column.fixed !== undefined" size="small" type="info">
|
||
{{ column.fixed === 'left' ? '固定左侧' : column.fixed === 'right' ? '固定右侧' : '固定' }}
|
||
</el-tag>
|
||
</div>
|
||
</el-checkbox-group>
|
||
</div>
|
||
</div>
|
||
|
||
<template #footer>
|
||
<div class="column-control-footer">
|
||
<el-button @click="handleReset">重置</el-button>
|
||
<el-button type="primary" @click="handleConfirm">确定</el-button>
|
||
</div>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, watch, onMounted, nextTick, type Ref } from 'vue'
|
||
import { Setting } from '@element-plus/icons-vue'
|
||
import type { TableInstance } from 'element-plus'
|
||
import { useTableColumns, type ColumnConfig } from '/@/composables/useTableColumns'
|
||
|
||
interface Column {
|
||
prop?: string
|
||
label: string
|
||
fixed?: boolean | 'left' | 'right'
|
||
alwaysShow?: boolean // 始终显示的列,不可隐藏
|
||
[key: string]: any
|
||
}
|
||
|
||
interface Props {
|
||
columns?: Column[] // 手动配置的列(可选,如果提供了 tableRef 则自动提取)
|
||
tableRef?: Ref<TableInstance | undefined> // el-table 的 ref,用于自动提取列配置
|
||
modelValue?: string[] // 当前显示的列
|
||
storageKey?: string // localStorage 存储的 key,用于持久化
|
||
triggerType?: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info' | 'text'
|
||
triggerSize?: 'large' | 'default' | 'small'
|
||
triggerCircle?: boolean
|
||
triggerText?: string
|
||
triggerLink?: boolean
|
||
dialogWidth?: string // 对话框宽度
|
||
// 自动提取时的配置选项
|
||
autoExtractOptions?: {
|
||
defaultHidden?: string[]
|
||
alwaysShow?: string[]
|
||
columnMap?: Record<string, Partial<ColumnConfig>>
|
||
}
|
||
}
|
||
|
||
const props = withDefaults(defineProps<Props>(), {
|
||
triggerType: 'default',
|
||
triggerSize: 'default',
|
||
triggerCircle: false,
|
||
triggerText: '',
|
||
triggerLink: false,
|
||
dialogWidth: '600px'
|
||
})
|
||
|
||
const emit = defineEmits<{
|
||
'update:modelValue': [value: string[]]
|
||
'change': [value: string[]]
|
||
}>()
|
||
|
||
const visible = ref(false)
|
||
const checkedColumns = ref<string[]>([])
|
||
|
||
// 如果提供了 tableRef,使用自动提取;否则使用手动配置的 columns
|
||
const tableColumnsResult = props.tableRef
|
||
? useTableColumns(props.tableRef, props.storageKey, props.autoExtractOptions)
|
||
: {
|
||
columns: computed(() => []),
|
||
visibleColumns: computed(() => []),
|
||
updateVisibleColumns: () => {},
|
||
refreshColumns: () => {},
|
||
isColumnVisible: () => true
|
||
}
|
||
|
||
const {
|
||
columns: autoColumns,
|
||
visibleColumns: autoVisibleColumns,
|
||
updateVisibleColumns: updateAutoVisibleColumns,
|
||
refreshColumns: refreshAutoColumns,
|
||
isColumnVisible: autoIsColumnVisible
|
||
} = tableColumnsResult
|
||
|
||
// 实际使用的列配置
|
||
const actualColumns = computed(() => {
|
||
const result = props.tableRef && autoColumns.value.length > 0
|
||
? autoColumns.value
|
||
: props.columns || []
|
||
return result
|
||
})
|
||
|
||
// 获取所有列(包括固定列和 alwaysShow 列)
|
||
const getAllColumns = (): string[] => {
|
||
return actualColumns.value.map(col => col.prop || col.label)
|
||
}
|
||
|
||
// 初始化选中的列
|
||
const initCheckedColumns = () => {
|
||
if (props.modelValue && props.modelValue.length > 0) {
|
||
checkedColumns.value = [...props.modelValue]
|
||
} else if (props.tableRef && autoVisibleColumns.value.length > 0) {
|
||
// 使用自动提取的可见列,但需要确保包含所有列(包括固定列)
|
||
// 合并已保存的可见列和固定列/alwaysShow列
|
||
const fixedAndAlwaysShow = actualColumns.value
|
||
.filter(col => col.alwaysShow || !!col.fixed)
|
||
.map(col => col.prop || col.label)
|
||
checkedColumns.value = [...new Set([...autoVisibleColumns.value, ...fixedAndAlwaysShow])]
|
||
} else if (props.storageKey) {
|
||
// 从 localStorage 读取
|
||
const saved = localStorage.getItem(props.storageKey)
|
||
if (saved) {
|
||
try {
|
||
const savedColumns = JSON.parse(saved)
|
||
// 确保固定列和 alwaysShow 列始终在选中列表中
|
||
const fixedAndAlwaysShow = actualColumns.value
|
||
.filter(col => col.alwaysShow || !!col.fixed)
|
||
.map(col => col.prop || col.label)
|
||
checkedColumns.value = [...new Set([...savedColumns, ...fixedAndAlwaysShow])]
|
||
} catch (e) {
|
||
// 如果解析失败,使用默认值(所有列)
|
||
checkedColumns.value = getAllColumns()
|
||
}
|
||
} else {
|
||
checkedColumns.value = getAllColumns()
|
||
}
|
||
} else {
|
||
checkedColumns.value = getAllColumns()
|
||
}
|
||
}
|
||
|
||
// 监听 actualColumns 变化,更新选中状态
|
||
watch(actualColumns, (newColumns) => {
|
||
if (newColumns.length > 0 && checkedColumns.value.length === 0) {
|
||
// 如果列数据已加载但选中列表为空,初始化选中所有列
|
||
initCheckedColumns()
|
||
} else if (newColumns.length > 0) {
|
||
// 确保固定列和 alwaysShow 列始终在选中列表中
|
||
const fixedAndAlwaysShow = newColumns
|
||
.filter(col => col.alwaysShow || !!col.fixed)
|
||
.map(col => col.prop || col.label)
|
||
const currentChecked = checkedColumns.value
|
||
const missingFixed = fixedAndAlwaysShow.filter(col => !currentChecked.includes(col))
|
||
if (missingFixed.length > 0) {
|
||
checkedColumns.value = [...currentChecked, ...missingFixed]
|
||
}
|
||
}
|
||
}, { deep: true })
|
||
|
||
// 监听弹窗打开,触发列配置重新提取
|
||
watch(visible, (newVal) => {
|
||
console.log('[TableColumnControl] 弹窗状态变化:', newVal)
|
||
if (newVal) {
|
||
// 弹窗打开时,确保选中状态正确初始化
|
||
if (actualColumns.value.length > 0) {
|
||
initCheckedColumns()
|
||
}
|
||
}
|
||
if (newVal && props.tableRef) {
|
||
console.log('[TableColumnControl] 弹窗打开,tableRef 存在:', props.tableRef)
|
||
console.log('[TableColumnControl] tableRef.value:', props.tableRef.value)
|
||
console.log('[TableColumnControl] tableRef.value 类型:', typeof props.tableRef.value)
|
||
|
||
// 尝试多种方式获取表格实例
|
||
const getTableInstance = (): TableInstance | null => {
|
||
// 方法1: 直接从 props.tableRef.value 获取
|
||
if (props.tableRef?.value) {
|
||
console.log('[TableColumnControl] 从 props.tableRef.value 获取表格实例')
|
||
return props.tableRef.value
|
||
}
|
||
|
||
// 方法2: 尝试从 props.tableRef 本身获取(可能是直接的 ref 对象)
|
||
if ((props.tableRef as any).value) {
|
||
console.log('[TableColumnControl] 从 props.tableRef 的 value 属性获取表格实例')
|
||
return (props.tableRef as any).value
|
||
}
|
||
|
||
// 方法3: 如果 props.tableRef 本身就是表格实例(不应该发生,但作为备用)
|
||
if ((props.tableRef as any).$el) {
|
||
console.log('[TableColumnControl] props.tableRef 本身就是表格实例')
|
||
return props.tableRef as any
|
||
}
|
||
|
||
return null
|
||
}
|
||
|
||
// 如果 tableRef.value 已经有值,直接提取
|
||
const tableInstance = getTableInstance()
|
||
if (tableInstance) {
|
||
const tableEl = (tableInstance as any).$el
|
||
if (tableEl) {
|
||
console.log('[TableColumnControl] 表格实例已就绪,立即提取列配置')
|
||
nextTick(() => {
|
||
refreshAutoColumns()
|
||
console.log('[TableColumnControl] 刷新后列数:', actualColumns.value.length)
|
||
})
|
||
return
|
||
}
|
||
}
|
||
|
||
// 等待 tableRef.value 有值后再提取
|
||
let waitCount = 0
|
||
const maxWaitCount = 50 // 最多等待 5 秒(50 * 100ms)
|
||
let waitTimer: ReturnType<typeof setTimeout> | null = null
|
||
|
||
const waitForTableRef = () => {
|
||
// 尝试获取表格实例
|
||
const tableInstance = getTableInstance()
|
||
if (tableInstance) {
|
||
const tableEl = (tableInstance as any).$el
|
||
if (tableEl) {
|
||
console.log('[TableColumnControl] 表格实例已就绪,开始提取列配置')
|
||
nextTick(() => {
|
||
refreshAutoColumns()
|
||
console.log('[TableColumnControl] 刷新后列数:', actualColumns.value.length)
|
||
|
||
// 如果还是没有数据,多次重试
|
||
let retryCount = 0
|
||
const maxRetries = 10
|
||
const retryInterval = setInterval(() => {
|
||
retryCount++
|
||
console.log(`[TableColumnControl] 第 ${retryCount} 次重试刷新列配置`)
|
||
refreshAutoColumns()
|
||
console.log(`[TableColumnControl] 重试后列数:`, actualColumns.value.length)
|
||
if (actualColumns.value.length > 0 || retryCount >= maxRetries) {
|
||
console.log('[TableColumnControl] 停止重试,最终列数:', actualColumns.value.length)
|
||
clearInterval(retryInterval)
|
||
}
|
||
}, 200)
|
||
})
|
||
return // 成功获取,退出
|
||
}
|
||
}
|
||
|
||
// 继续等待
|
||
waitCount++
|
||
if (waitCount < maxWaitCount) {
|
||
console.log(`[TableColumnControl] tableRef.value 还未就绪,等待中... (${waitCount}/${maxWaitCount})`)
|
||
waitTimer = setTimeout(waitForTableRef, 100)
|
||
} else {
|
||
console.warn('[TableColumnControl] 等待超时,tableRef.value 仍未就绪')
|
||
console.warn('[TableColumnControl] props.tableRef:', props.tableRef)
|
||
console.warn('[TableColumnControl] props.tableRef?.value:', props.tableRef?.value)
|
||
// 即使超时,也尝试提取一次(可能表格已经渲染了,只是 ref 没有正确绑定)
|
||
console.log('[TableColumnControl] 尝试强制提取列配置')
|
||
refreshAutoColumns()
|
||
console.log('[TableColumnControl] 强制提取后列数:', actualColumns.value.length)
|
||
}
|
||
}
|
||
|
||
// 延迟一下,确保表格已渲染
|
||
setTimeout(() => {
|
||
waitForTableRef()
|
||
}, 300)
|
||
|
||
// 清理函数:弹窗关闭时清除等待定时器
|
||
return () => {
|
||
if (waitTimer) {
|
||
clearTimeout(waitTimer)
|
||
waitTimer = null
|
||
}
|
||
}
|
||
} else {
|
||
console.warn('[TableColumnControl] 弹窗打开但 tableRef 不存在或为空')
|
||
}
|
||
})
|
||
|
||
// 监听 tableRef.value 的变化,当它被赋值时触发列配置提取
|
||
if (props.tableRef) {
|
||
// 尝试多种方式监听 tableRef.value 的变化
|
||
watch(() => {
|
||
// 尝试多种方式获取 tableRef.value
|
||
if (props.tableRef?.value) {
|
||
return props.tableRef.value
|
||
}
|
||
if ((props.tableRef as any).value) {
|
||
return (props.tableRef as any).value
|
||
}
|
||
return null
|
||
}, (newVal, oldVal) => {
|
||
if (newVal && newVal !== oldVal) {
|
||
console.log('[TableColumnControl] tableRef.value 已赋值,触发列配置提取')
|
||
// 延迟一下,确保表格完全渲染
|
||
setTimeout(() => {
|
||
nextTick(() => {
|
||
refreshAutoColumns()
|
||
console.log('[TableColumnControl] 列配置提取完成,列数:', actualColumns.value.length)
|
||
})
|
||
}, 200)
|
||
}
|
||
}, { immediate: true }) // 立即检查一次,如果 tableRef.value 已经有值,立即触发
|
||
}
|
||
|
||
// 暴露给父组件使用
|
||
defineExpose({
|
||
isColumnVisible: autoIsColumnVisible,
|
||
visibleColumns: autoVisibleColumns,
|
||
refreshColumns: refreshAutoColumns
|
||
})
|
||
|
||
// 获取默认显示的列(所有可隐藏的列)- 用于重置功能
|
||
const getDefaultColumns = (): string[] => {
|
||
return actualColumns.value
|
||
.filter(col => !col.alwaysShow && !col.fixed)
|
||
.map(col => col.prop || col.label)
|
||
}
|
||
|
||
// 获取所有可选择的列
|
||
const selectableColumns = computed(() => {
|
||
return actualColumns.value
|
||
.filter(col => !col.alwaysShow && !col.fixed)
|
||
.map(col => col.prop || col.label)
|
||
})
|
||
|
||
// 判断是否全选
|
||
const isAllSelected = computed(() => {
|
||
const selectable = selectableColumns.value
|
||
if (selectable.length === 0) return false
|
||
return selectable.every(col => checkedColumns.value.includes(col))
|
||
})
|
||
|
||
// 切换全选/全不选
|
||
const handleToggleSelectAll = () => {
|
||
if (isAllSelected.value) {
|
||
// 当前全选,执行全不选(但保留固定列和 alwaysShow 列)
|
||
const fixedAndAlwaysShow = actualColumns.value
|
||
.filter(col => col.alwaysShow || !!col.fixed)
|
||
.map(col => col.prop || col.label)
|
||
checkedColumns.value = [...fixedAndAlwaysShow]
|
||
} else {
|
||
// 当前未全选,执行全选(所有列)
|
||
checkedColumns.value = getAllColumns()
|
||
}
|
||
}
|
||
|
||
// 重置为默认值
|
||
const handleReset = () => {
|
||
checkedColumns.value = getDefaultColumns()
|
||
handleColumnChange(checkedColumns.value)
|
||
}
|
||
|
||
// 确认
|
||
const handleConfirm = () => {
|
||
handleColumnChange(checkedColumns.value)
|
||
visible.value = false
|
||
}
|
||
|
||
// 列变化处理
|
||
const handleColumnChange = (value: string[]) => {
|
||
// 确保固定列和 alwaysShow 列始终在选中列表中
|
||
const fixedAndAlwaysShow = actualColumns.value
|
||
.filter(col => col.alwaysShow || !!col.fixed)
|
||
.map(col => col.prop || col.label)
|
||
const finalValue = [...new Set([...value, ...fixedAndAlwaysShow])]
|
||
|
||
emit('update:modelValue', finalValue)
|
||
emit('change', finalValue)
|
||
|
||
// 如果使用自动提取,同步更新(只更新可选择的列)
|
||
if (props.tableRef) {
|
||
const selectableValue = finalValue.filter(col => {
|
||
const column = actualColumns.value.find(c => (c.prop || c.label) === col)
|
||
return column && !column.alwaysShow && !column.fixed
|
||
})
|
||
updateAutoVisibleColumns(selectableValue)
|
||
} else {
|
||
// 保存到 localStorage(只保存可选择的列)
|
||
if (props.storageKey) {
|
||
const selectableValue = finalValue.filter(col => {
|
||
const column = actualColumns.value.find(c => (c.prop || c.label) === col)
|
||
return column && !column.alwaysShow && !column.fixed
|
||
})
|
||
localStorage.setItem(props.storageKey, JSON.stringify(selectableValue))
|
||
}
|
||
}
|
||
}
|
||
|
||
// 监听外部 modelValue 变化
|
||
watch(() => props.modelValue, (newVal) => {
|
||
if (newVal && newVal.length > 0) {
|
||
checkedColumns.value = [...newVal]
|
||
}
|
||
}, { immediate: true })
|
||
|
||
// 初始化
|
||
onMounted(() => {
|
||
initCheckedColumns()
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.table-column-control {
|
||
display: inline-block;
|
||
}
|
||
|
||
.column-control-content {
|
||
.column-control-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding-bottom: 12px;
|
||
margin-bottom: 12px;
|
||
border-bottom: 1px solid #ebeef5;
|
||
|
||
.header-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
}
|
||
|
||
.header-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
}
|
||
|
||
.column-control-body {
|
||
max-height: 400px;
|
||
overflow-y: auto;
|
||
margin-bottom: 12px;
|
||
|
||
.column-checkbox-group {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 12px 16px;
|
||
}
|
||
|
||
.column-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
min-width: 120px;
|
||
|
||
:deep(.el-checkbox) {
|
||
margin-right: 0;
|
||
|
||
.el-checkbox__label {
|
||
font-size: 14px;
|
||
color: #606266;
|
||
padding-left: 8px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.column-control-footer {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 8px;
|
||
padding-top: 12px;
|
||
border-top: 1px solid #ebeef5;
|
||
}
|
||
}
|
||
</style>
|
||
|
||
|
||
|