Files
school-developer/src/components/TableColumnControl/index.vue
guochunsi e1cb334fbf ren
2026-01-06 19:23:18 +08:00

508 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>