Files
school-developer/src/composables/useTableColumns.ts
guochunsi 98fcd368f9 ren
2026-01-08 19:00:25 +08:00

455 lines
15 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 isInitialized = ref(false) // 标记是否已经初始化过(用户是否操作过)
const hasInitializedVisibleColumns = ref(false) // 标记是否已经初始化过可见列(避免重复初始化)
// 获取表格实例的辅助函数
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[] => {
const tableInstance = getTableInstance()
if (!tableInstance) {
return []
}
const table = tableInstance
const extracted: ColumnConfig[] = []
try {
// 方法1: 从 table.store 中提取Element Plus 内部实现)
const store = (table as any).store
if (store) {
// 尝试多种路径访问列数据
let tableColumns: any[] = []
if (store.states && store.states.columns) {
tableColumns = store.states.columns.value || []
} else if (store.columns) {
tableColumns = Array.isArray(store.columns) ? store.columns : (store.columns.value || [])
} else if ((table as any).columns) {
tableColumns = Array.isArray((table as any).columns) ? (table as any).columns : ((table as any).columns.value || [])
}
if (tableColumns.length > 0) {
tableColumns.forEach((col: any) => {
// 跳过序号列
if (col.type === 'index' || col.type === 'selection') {
return
}
// 尝试多种方式获取 label
const label = col.label || col.columnKey || col.property || col.prop || ''
// 如果没有 label跳过这一列
if (!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
}
}
extracted.push(config)
})
if (extracted.length > 0) {
return extracted
}
}
}
// 方法2: 从 DOM 中提取(备用方案,更可靠)
const tableEl = (table as any).$el
if (tableEl) {
// 尝试多种选择器来查找表头
let columnHeaders: NodeListOf<HTMLElement> | null = null
// 方法2.1: 从主表格头部查找
const headerWrapper = tableEl.querySelector('.el-table__header-wrapper')
if (headerWrapper) {
columnHeaders = headerWrapper.querySelectorAll('th')
}
// 方法2.2: 如果找不到,从整个表格查找
if (!columnHeaders || columnHeaders.length === 0) {
columnHeaders = tableEl.querySelectorAll('th')
}
// 方法2.3: 如果还是找不到,尝试查找固定列
if (!columnHeaders || columnHeaders.length === 0) {
const fixedLeft = tableEl.querySelector('.el-table__fixed-left')
if (fixedLeft) {
columnHeaders = fixedLeft.querySelectorAll('th')
}
}
if (!columnHeaders || columnHeaders.length === 0) {
return []
}
// 创建一个映射,通过列索引匹配 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()
// 排除序号列和空列
if (!label || label === '序号') {
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 作为 propisColumnVisible 会同时匹配 prop 和 label
// 或者使用默认的 column_${index}
if (!prop) {
prop = `column_${index}`
}
const columnConfig = {
prop: prop,
label,
width,
// DOM 提取的 fixed 只有 left/right这里也归一为 undefined 或 'left'/'right'
fixed: fixed ? fixed : undefined,
}
extracted.push(columnConfig)
})
if (extracted.length > 0) {
return extracted
}
}
} catch (error) {
// 提取失败,静默处理
}
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 (!hasInitializedVisibleColumns.value) {
if (storageKey) {
const saved = localStorage.getItem(storageKey)
if (saved) {
try {
const savedColumns = JSON.parse(saved)
// 验证保存的列是否仍然存在
const validColumns = columns.value
.filter(col => !col.alwaysShow && col.fixed === undefined)
.map(col => col.prop || col.label)
const filteredSaved = savedColumns.filter((col: string) => validColumns.includes(col))
// 如果保存的列数量少于所有列,说明是旧数据,默认显示所有列
if (filteredSaved.length < validColumns.length) {
visibleColumns.value = validColumns
isInitialized.value = false // 旧数据,视为未初始化
} else {
visibleColumns.value = filteredSaved
isInitialized.value = true // 有保存的数据,视为已初始化
}
} catch (e) {
initDefaultVisibleColumns()
isInitialized.value = false
}
} else {
// 首次使用,默认显示所有列
initDefaultVisibleColumns()
isInitialized.value = false
}
} else {
// 没有 storageKey默认显示所有列
initDefaultVisibleColumns()
isInitialized.value = false
}
hasInitializedVisibleColumns.value = true
}
}
}
// 初始化默认可见列
const initDefaultVisibleColumns = () => {
// 默认显示所有可选择的列(除了固定列和 alwaysShow 列)
// 注意:固定列和 alwaysShow 列不需要在 visibleColumns 中,因为它们始终显示
// 默认全部选中,展示所有列
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 (!isInitialized.value) {
return true
}
// 如果已经初始化,严格按照可见列列表判断
// 如果可见列列表为空,说明用户取消了所有列,应该隐藏所有非固定列
if (visibleColumns.value.length === 0) {
return false
}
// 检查是否在可见列列表中
return visibleColumns.value.includes(propOrLabel)
}
// 更新可见列
const updateVisibleColumns = (newColumns: string[]) => {
visibleColumns.value = newColumns
isInitialized.value = true // 标记已经初始化(用户已操作)
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) {
// 延迟一下,确保表格已经渲染
setTimeout(() => {
initColumns()
}, 200)
} else if (newVal && !oldVal) {
// 如果从 undefined 变为有值,也触发提取
setTimeout(() => {
initColumns()
}, 200)
}
}, { immediate: true })
// 组件挂载后初始化
onMounted(() => {
// 延迟初始化,确保表格已经渲染
// 如果 tableRef.value 已经有值,立即初始化;否则等待 watch 触发
const tableInstance = getTableInstance()
if (tableInstance) {
setTimeout(() => {
initColumns()
}, 300)
}
})
return {
columns: computed(() => columns.value),
visibleColumns: computed(() => visibleColumns.value),
isColumnVisible,
updateVisibleColumns,
refreshColumns: initColumns,
}
}