import { ref, computed, watch, onMounted, nextTick, type Ref } from 'vue' import type { TableInstance } from 'element-plus' export interface ColumnConfig { prop?: string label: string width?: number | string minWidth?: number | string fixed?: boolean | 'left' | 'right' alwaysShow?: boolean [key: string]: any } /** * 自动从 el-table 提取列配置的 composable * @param tableRef el-table 的 ref * @param storageKey localStorage 存储的 key,用于持久化 * @param options 额外配置选项 */ export function useTableColumns( tableRef: Ref | any, // 支持多种类型的 ref storageKey?: string, options?: { // 默认隐藏的列(prop 或 label) defaultHidden?: string[] // 始终显示的列(prop 或 label) alwaysShow?: string[] // 列配置映射(用于自定义列的显示名称等) columnMap?: Record> } ) { const columns = ref([]) const visibleColumns = ref([]) // 获取表格实例的辅助函数 const getTableInstance = (): TableInstance | null => { // 方法1: 直接从 tableRef.value 获取 if (tableRef?.value) { return tableRef.value } // 方法2: 如果 tableRef 本身有 value 属性(可能是直接的 ref 对象) if ((tableRef as any).value) { return (tableRef as any).value } // 方法3: 如果 tableRef 本身就是表格实例(不应该发生,但作为备用) if ((tableRef as any).$el) { return tableRef as any } return null } // 从 el-table 提取列配置 const extractColumns = (): ColumnConfig[] => { console.log('[useTableColumns] extractColumns 开始执行') const tableInstance = getTableInstance() if (!tableInstance) { console.warn('[useTableColumns] tableRef.value 为空') console.warn('[useTableColumns] tableRef:', tableRef) console.warn('[useTableColumns] tableRef?.value:', tableRef?.value) return [] } const table = tableInstance console.log('[useTableColumns] tableInstance 存在:', table) const extracted: ColumnConfig[] = [] try { // 方法1: 从 table.store 中提取(Element Plus 内部实现) const store = (table as any).store console.log('[useTableColumns] store:', store) if (store) { // 尝试多种路径访问列数据 let tableColumns: any[] = [] if (store.states && store.states.columns) { tableColumns = store.states.columns.value || [] console.log('[useTableColumns] 从 store.states.columns 获取到列数:', tableColumns.length) console.log('[useTableColumns] store.states.columns 内容:', tableColumns) } else if (store.columns) { tableColumns = Array.isArray(store.columns) ? store.columns : (store.columns.value || []) console.log('[useTableColumns] 从 store.columns 获取到列数:', tableColumns.length) } else if ((table as any).columns) { tableColumns = Array.isArray((table as any).columns) ? (table as any).columns : ((table as any).columns.value || []) console.log('[useTableColumns] 从 table.columns 获取到列数:', tableColumns.length) } if (tableColumns.length > 0) { tableColumns.forEach((col: any, idx: number) => { console.log(`[useTableColumns] store 列 ${idx}:`, { type: col.type, label: col.label, property: col.property, prop: col.prop, fixed: col.fixed, fullCol: col }) // 跳过序号列 if (col.type === 'index' || col.type === 'selection') { console.log(`[useTableColumns] 列 ${idx} 被跳过(类型: ${col.type})`) return } // 尝试多种方式获取 label const label = col.label || col.columnKey || col.property || col.prop || '' // 如果没有 label,尝试从其他属性推断 if (!label) { console.log(`[useTableColumns] 列 ${idx} 没有 label,尝试从其他属性推断`) // 如果还是没有,跳过这一列 return } const config: ColumnConfig = { prop: col.property || col.prop || '', label: label, width: col.width, minWidth: col.minWidth, // Element Plus 中非固定列的 fixed 通常是 false,这里统一将 false 归一为 undefined fixed: col.fixed ? col.fixed : undefined, } // 应用自定义映射 if (options?.columnMap && config.prop) { const mapped = options.columnMap[config.prop] if (mapped) { Object.assign(config, mapped) } } // 应用 alwaysShow 配置 if (options?.alwaysShow) { const key = config.prop || config.label if (key && options.alwaysShow.includes(key)) { config.alwaysShow = true } } console.log(`[useTableColumns] 从 store 提取到列配置:`, config) extracted.push(config) }) console.log('[useTableColumns] 从 store 总共提取到列数:', extracted.length) if (extracted.length > 0) { return extracted } } else { console.warn('[useTableColumns] store 中没有找到列数据') } } // 方法2: 从 DOM 中提取(备用方案,更可靠) const tableEl = (table as any).$el console.log('[useTableColumns] tableEl:', tableEl) if (tableEl) { // 尝试多种选择器来查找表头 let columnHeaders: NodeListOf | null = null // 方法2.1: 从主表格头部查找 const headerWrapper = tableEl.querySelector('.el-table__header-wrapper') console.log('[useTableColumns] headerWrapper:', headerWrapper) if (headerWrapper) { columnHeaders = headerWrapper.querySelectorAll('th') console.log('[useTableColumns] 从 headerWrapper 找到列数:', columnHeaders?.length || 0) } // 方法2.2: 如果找不到,从整个表格查找 if (!columnHeaders || columnHeaders.length === 0) { columnHeaders = tableEl.querySelectorAll('th') console.log('[useTableColumns] 从整个表格找到列数:', columnHeaders?.length || 0) } // 方法2.3: 如果还是找不到,尝试查找固定列 if (!columnHeaders || columnHeaders.length === 0) { const fixedLeft = tableEl.querySelector('.el-table__fixed-left') console.log('[useTableColumns] fixedLeft:', fixedLeft) if (fixedLeft) { columnHeaders = fixedLeft.querySelectorAll('th') console.log('[useTableColumns] 从 fixedLeft 找到列数:', columnHeaders?.length || 0) } } if (!columnHeaders || columnHeaders.length === 0) { console.warn('[useTableColumns] 未找到任何列头元素') return [] } console.log('[useTableColumns] 最终找到列数量:', columnHeaders.length) // 创建一个映射,通过列索引匹配 prop const propMap = new Map() // 尝试从表格的 slot 或配置中获取 prop // 通过检查表格的列定义来匹配 if (store && store.states && store.states.columns) { const tableColumns = store.states.columns.value || [] tableColumns.forEach((col: any, idx: number) => { if (col.property || col.prop) { propMap.set(idx, col.property || col.prop) } }) } columnHeaders.forEach((th: HTMLElement, index: number) => { // 尝试多种方式获取 label 文本 // 1. 从 .cell 元素 const cell = th.querySelector('.cell') as HTMLElement | null // 2. 从所有可能的文本节点 let rawLabel = '' if (cell) { rawLabel = cell.innerText || cell.textContent || '' } else { // 尝试从 th 的所有子元素中查找文本 const textNodes: string[] = [] const walker = document.createTreeWalker( th, NodeFilter.SHOW_TEXT, null ) let node: Node | null while ((node = walker.nextNode())) { const text = node.textContent?.trim() if (text) { textNodes.push(text) } } rawLabel = textNodes.join(' ') || th.innerText || th.textContent || '' } const label = rawLabel.trim() // 调试:打印 th 的完整结构 console.log(`[useTableColumns] DOM 列 ${index}:`, { label, rawLabel, thHTML: th.innerHTML.substring(0, 100), hasCell: !!cell, cellText: cell?.innerText || cell?.textContent || '', thInnerText: th.innerText, thTextContent: th.textContent }) // 排除序号列和空列 if (!label || label === '序号') { console.log(`[useTableColumns] 列 ${index} 被跳过(序号列或空列)`) return } // 检查是否是固定列 let fixed: 'left' | 'right' | undefined const thParent = th.closest('table') if (thParent) { if (thParent.classList.contains('el-table__fixed-left') || th.closest('.el-table__fixed-left')) { fixed = 'left' } else if (thParent.classList.contains('el-table__fixed-right') || th.closest('.el-table__fixed-right')) { fixed = 'right' } else if (th.classList.contains('is-left')) { fixed = 'left' } else if (th.classList.contains('is-right')) { fixed = 'right' } } // 尝试从属性中获取宽度 const widthAttr = th.getAttribute('width') || th.style.width let width: number | string | undefined if (widthAttr) { const numWidth = parseInt(widthAttr) width = isNaN(numWidth) ? widthAttr : numWidth } // 尝试从对应的 body cell 中获取 data-key 或其他属性来匹配 prop let prop = propMap.get(index) // 如果没有找到 prop,尝试从 label 推断(简单匹配) if (!prop) { // 对于常见的列,可以通过 label 推断 prop const labelToPropMap: Record = { '是否退休': 'tied', '姓名/工号': 'nameNo', '性别': 'sex', '部门': 'deptName', '学历学位': 'dgreeName', '职称等级': 'professionalTitle', '岗位级别': 'stationLevelName', '职业资格等级': 'levelName', '职业资格工种': 'workName', '用工性质': 'employmentNatureName', '手机': 'telPhone', '家庭住址': 'homeAddress', '授课类型': 'teacherCate', '操作': 'action', } prop = labelToPropMap[label] } const columnConfig = { prop: prop || `column_${index}`, label, width, // DOM 提取的 fixed 只有 left/right,这里也归一为 undefined 或 'left'/'right' fixed: fixed ? fixed : undefined, } console.log(`[useTableColumns] 提取到列配置:`, columnConfig) extracted.push(columnConfig) }) console.log('[useTableColumns] 总共提取到列数:', extracted.length) if (extracted.length > 0) { console.log('[useTableColumns] 提取到的所有列:', extracted) return extracted } } } catch (error) { // 提取失败,输出错误信息 console.error('[useTableColumns] 提取列配置时出错:', error) } console.warn('[useTableColumns] 提取失败,返回空数组') return [] } // 初始化列配置 const initColumns = async () => { // 等待多个渲染周期,确保表格完全渲染 await nextTick() await new Promise(resolve => setTimeout(resolve, 300)) await nextTick() let extracted = extractColumns() // 如果第一次提取失败,多次重试 if (extracted.length === 0) { for (let i = 0; i < 5; i++) { await new Promise(resolve => setTimeout(resolve, 200)) extracted = extractColumns() if (extracted.length > 0) break } } if (extracted.length > 0) { // 合并列配置: // - 第一次提取时直接赋值 // - 后续提取时与已有列做并集,避免因为列被隐藏导致配置丢失, // 从而在“列设置”弹窗中看不到已隐藏的列 if (columns.value.length === 0) { columns.value = extracted } else { const keyOf = (col: ColumnConfig) => col.prop || col.label const map = new Map() // 先放入已有列 columns.value.forEach(col => { const key = keyOf(col) if (key) { map.set(key, { ...col }) } }) // 再合并本次提取的列(更新宽度、fixed 等信息,避免旧配置过时) extracted.forEach(col => { const key = keyOf(col) if (!key) return const exist = map.get(key) if (exist) { map.set(key, { ...exist, ...col, }) } else { map.set(key, { ...col }) } }) columns.value = Array.from(map.values()) } // 初始化可见列 if (storageKey) { const saved = localStorage.getItem(storageKey) if (saved) { try { visibleColumns.value = JSON.parse(saved) // 验证保存的列是否仍然存在 const validColumns = columns.value .filter(col => !col.alwaysShow && col.fixed === undefined) .map(col => col.prop || col.label) visibleColumns.value = visibleColumns.value.filter(col => validColumns.includes(col)) } catch (e) { initDefaultVisibleColumns() } } else { initDefaultVisibleColumns() } } else { initDefaultVisibleColumns() } } else { console.warn('[useTableColumns] initColumns: 提取失败,未设置 columns.value') } } // 初始化默认可见列 const initDefaultVisibleColumns = () => { const defaultHidden = options?.defaultHidden || [] // 默认显示所有列(除了默认隐藏的列和固定列/alwaysShow列) // 注意:固定列和 alwaysShow 列不需要在 visibleColumns 中,因为它们始终显示 visibleColumns.value = columns.value .filter(col => { const key = col.prop || col.label return !col.alwaysShow && !col.fixed && !defaultHidden.includes(key) }) .map(col => col.prop || col.label) // 如果所有列都被隐藏了,至少显示所有非固定列 if (visibleColumns.value.length === 0 && columns.value.length > 0) { visibleColumns.value = columns.value .filter(col => !col.alwaysShow && !col.fixed) .map(col => col.prop || col.label) } } // 判断列是否可见 const isColumnVisible = (propOrLabel: string): boolean => { // 如果列配置还没有提取完成,默认显示所有列(避免初始渲染时所有列被隐藏) if (columns.value.length === 0) { return true } const column = columns.value.find(col => (col.prop || col.label) === propOrLabel ) // 如果找不到对应的列配置,默认显示(可能是新添加的列) if (!column) { return true } // 固定列和始终显示的列始终显示 if (column.fixed || column.alwaysShow) { return true } // 如果可见列列表为空,默认显示所有列(初始状态) if (visibleColumns.value.length === 0) { return true } // 检查是否在可见列列表中 return visibleColumns.value.includes(propOrLabel) } // 更新可见列 const updateVisibleColumns = (newColumns: string[]) => { visibleColumns.value = newColumns if (storageKey) { localStorage.setItem(storageKey, JSON.stringify(newColumns)) } } // 监听表格变化,重新提取列配置 watch(() => { // 尝试多种方式获取 tableRef.value if (tableRef?.value) { return tableRef.value } if ((tableRef as any).value) { return (tableRef as any).value } return null }, (newVal, oldVal) => { if (newVal && newVal !== oldVal) { console.log('[useTableColumns] tableRef.value 变化,开始提取列配置') // 延迟一下,确保表格已经渲染 setTimeout(() => { initColumns() }, 200) } else if (newVal && !oldVal) { // 如果从 undefined 变为有值,也触发提取 console.log('[useTableColumns] tableRef.value 从 undefined 变为有值,开始提取列配置') setTimeout(() => { initColumns() }, 200) } }, { immediate: true }) // 改为 true,立即检查一次 // 组件挂载后初始化 onMounted(() => { // 延迟初始化,确保表格已经渲染 // 如果 tableRef.value 已经有值,立即初始化;否则等待 watch 触发 const tableInstance = getTableInstance() if (tableInstance) { setTimeout(() => { initColumns() }, 300) } else { console.log('[useTableColumns] onMounted: tableRef.value 还未就绪,等待 watch 触发') } }) return { columns: computed(() => columns.value), visibleColumns: computed(() => visibleColumns.value), isColumnVisible, updateVisibleColumns, refreshColumns: initColumns, } }