Files
school-developer/src/composables/useTableColumns.ts
guochunsi 9e3e775b0f ren
2026-01-07 18:33:03 +08:00

516 lines
18 KiB
TypeScript
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.

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<TableInstance | undefined> | any, // 支持多种类型的 ref
storageKey?: string,
options?: {
// 默认隐藏的列prop 或 label
defaultHidden?: string[]
// 始终显示的列prop 或 label
alwaysShow?: string[]
// 列配置映射(用于自定义列的显示名称等)
columnMap?: Record<string, Partial<ColumnConfig>>
}
) {
const columns = ref<ColumnConfig[]>([])
const visibleColumns = ref<string[]>([])
// 获取表格实例的辅助函数
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<HTMLElement> | 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<number, string>()
// 尝试从表格的 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<string, string> = {
'是否退休': '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<string, ColumnConfig>()
// 先放入已有列
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,
}
}