This commit is contained in:
2026-01-22 13:38:10 +08:00
parent b350322626
commit 313fe64475
151 changed files with 13060 additions and 4411 deletions

View File

@@ -1,6 +1,6 @@
<template>
<div class="top-right-btn" :style="style">
<el-row>
<el-row :gutter="0">
<!-- 搜索框控制 -->
<el-tooltip
class="item"
@@ -103,6 +103,25 @@ const isExport = () => {
</script>
<style lang="scss" scoped>
.top-right-btn {
display: flex;
align-items: center;
:deep(.el-row) {
display: flex;
align-items: center;
gap: 8px;
}
:deep(.el-button) {
margin-left: 0;
}
:deep(.el-tooltip) {
margin-left: 0;
}
}
:deep(.el-transfer__button) {
border-radius: 50%;
display: block;

View File

@@ -0,0 +1,272 @@
# TableColumnControl 集成指南
## 概述
`TableColumnControl` 是一个通用的表格列显隐控制组件,可以为任何表格页面添加列显示/隐藏和排序功能。
## 集成步骤
### 1. 导入必要的依赖
```typescript
import { ref, computed, onMounted, nextTick } from 'vue'
import { useRoute } from 'vue-router'
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import { Menu } from '@element-plus/icons-vue'
```
### 2. 定义表格列配置
```typescript
// 表格列配置
const tableColumns = [
{ prop: 'schoolYear', label: '学年' },
{ prop: 'schoolTerm', label: '学期' },
{ prop: 'title', label: '标题' },
{ prop: 'author', label: '作者' },
{ prop: '操作', label: '操作', alwaysShow: true, fixed: true }
]
// 列配置映射(用于图标,可选)
const columnConfigMap: Record<string, { icon: any }> = {
schoolYear: { icon: Calendar },
schoolTerm: { icon: Clock },
title: { icon: Document },
author: { icon: User }
}
```
### 3. 添加状态变量
```typescript
const route = useRoute()
const columnControlRef = ref<any>()
// 当前显示的列
const visibleColumns = ref<string[]>([])
// 列排序顺序
const columnOrder = ref<string[]>([])
```
### 4. 添加配置加载和保存逻辑
```typescript
// 立即从 localStorage 加载配置
const loadSavedConfig = () => {
const routePath = route.path.replace(/^\//, '').replace(/\//g, '-')
const storageKey = `table-columns-${routePath}`
const saved = localStorage.getItem(storageKey)
if (saved) {
try {
const savedColumns = JSON.parse(saved)
const validColumns = tableColumns
.filter(col => !col.alwaysShow && !col.fixed)
.map(col => col.prop || col.label)
const filteredSaved = savedColumns.filter((col: string) => validColumns.includes(col))
visibleColumns.value = filteredSaved.length > 0 ? filteredSaved : validColumns
} catch (e) {
console.error('解析列配置失败:', e)
visibleColumns.value = tableColumns
.filter(col => !col.alwaysShow && !col.fixed)
.map(col => col.prop || col.label)
}
} else {
visibleColumns.value = tableColumns
.filter(col => !col.alwaysShow && !col.fixed)
.map(col => col.prop || col.label)
}
// 加载列排序配置
const orderKey = `${storageKey}-order`
const savedOrder = localStorage.getItem(orderKey)
if (savedOrder) {
try {
const parsedOrder = JSON.parse(savedOrder)
const validColumns = tableColumns
.filter(col => !col.alwaysShow && !col.fixed)
.map(col => col.prop || col.label)
columnOrder.value = parsedOrder.filter((key: string) => validColumns.includes(key))
validColumns.forEach(key => {
if (!columnOrder.value.includes(key)) {
columnOrder.value.push(key)
}
})
} catch (e) {
console.error('解析列排序失败:', e)
columnOrder.value = tableColumns
.filter(col => !col.alwaysShow && !col.fixed)
.map(col => col.prop || col.label)
}
} else {
columnOrder.value = tableColumns
.filter(col => !col.alwaysShow && !col.fixed)
.map(col => col.prop || col.label)
}
}
// 立即加载保存的配置
loadSavedConfig()
```
### 5. 添加排序后的表格列计算属性
```typescript
// 排序后的表格列
const sortedTableColumns = computed(() => {
const columns = tableColumns.filter(col => {
const key = col.prop || col.label
return col.alwaysShow || col.fixed || visibleColumns.value.includes(key)
})
if (columnOrder.value.length > 0) {
const orderedColumns: typeof tableColumns = []
const unorderedColumns: typeof tableColumns = []
columnOrder.value.forEach(key => {
const col = columns.find(c => (c.prop || c.label) === key)
if (col) {
orderedColumns.push(col)
}
})
columns.forEach(col => {
const key = col.prop || col.label
if (!columnOrder.value.includes(key)) {
unorderedColumns.push(col)
}
})
return [...orderedColumns, ...unorderedColumns]
}
return columns
})
```
### 6. 添加列显示控制函数
```typescript
// 列显示控制函数
const checkColumnVisible = (prop: string): boolean => {
if (visibleColumns.value.length === 0) {
return true
}
return visibleColumns.value.includes(prop)
}
```
### 7. 添加事件处理函数
```typescript
// 监听列变化
const handleColumnChange = (columns: string[]) => {
visibleColumns.value = columns
const routePath = route.path.replace(/^\//, '').replace(/\//g, '-')
const storageKey = `table-columns-${routePath}`
const selectableColumns = columns.filter(col => {
const column = tableColumns.find(c => (c.prop || c.label) === col)
return column && !column.alwaysShow && !column.fixed
})
localStorage.setItem(storageKey, JSON.stringify(selectableColumns))
}
// 监听列排序变化
const handleColumnOrderChange = (order: string[]) => {
columnOrder.value = order
const routePath = route.path.replace(/^\//, '').replace(/\//g, '-')
const storageKey = `table-columns-${routePath}-order`
localStorage.setItem(storageKey, JSON.stringify(order))
}
```
### 8. 在模板中添加 TableColumnControl 组件
`right-toolbar` 组件内添加:
```vue
<right-toolbar
v-model:showSearch="showSearch"
class="ml10"
style="float: right;"
@queryTable="getDataList">
<TableColumnControl
ref="columnControlRef"
:columns="tableColumns"
v-model="visibleColumns"
trigger-type="default"
trigger-circle
@change="handleColumnChange"
@order-change="handleColumnOrderChange"
>
<template #trigger>
<el-tooltip class="item" effect="dark" content="列设置" placement="top">
<el-button circle style="margin-left: 0;">
<el-icon><Menu /></el-icon>
</el-button>
</el-tooltip>
</template>
</TableColumnControl>
</right-toolbar>
```
### 9. 修改表格列渲染方式
将原来的静态 `el-table-column` 替换为动态渲染:
```vue
<el-table-column type="index" label="序号" width="60" align="center">
<template #header>
<el-icon><List /></el-icon>
</template>
</el-table-column>
<template v-for="col in sortedTableColumns" :key="col.prop || col.label">
<el-table-column
v-if="checkColumnVisible(col.prop || '')"
:prop="col.prop"
:label="col.label"
show-overflow-tooltip>
<template #header>
<el-icon><component :is="columnConfigMap[col.prop]?.icon || Calendar" /></el-icon>
<span style="margin-left: 4px">{{ col.label }}</span>
</template>
<!-- 特殊列的模板可以在这里添加 -->
<template v-if="col.prop === 'schoolTerm'" #default="scope">
<el-tag size="small" type="primary" effect="plain">
{{ scope.row.schoolTerm === 1 ? '上学期' : scope.row.schoolTerm === 2 ? '下学期' : scope.row.schoolTerm }}
</el-tag>
</template>
</el-table-column>
</template>
<el-table-column label="操作" align="center" fixed="right">
<!-- 操作列内容 -->
</el-table-column>
```
### 10. 在 onMounted 中确保配置已加载
```typescript
onMounted(() => {
// 其他初始化代码...
// 确保配置已同步
nextTick(() => {
if (visibleColumns.value.length === 0) {
loadSavedConfig()
}
})
})
```
## 注意事项
1. **列配置中的 prop 必须与 el-table-column 的 prop 属性一致**
2. **操作列应该标记为 `alwaysShow: true` 和 `fixed: true`**
3. **序号列type="index")不需要添加到 tableColumns 中**
4. **localStorage 的 key 会根据路由自动生成,确保每个页面都有独立的配置**
## 示例
参考 `src/views/stuwork/weekPlan/index.vue``src/views/stuwork/classroomhygienemonthly/index.vue` 的完整实现。

View File

@@ -0,0 +1,293 @@
# TableColumnControl 快速开始指南
## 简介
`TableColumnControl` 是一个通用的表格列控制组件,支持:
- ✅ 列的显示/隐藏控制
- ✅ 列的拖拽排序
- ✅ 配置的自动保存和恢复(基于路由)
- ✅ 两种使用方式:自动提取或手动配置
## 快速集成3 步)
### 步骤 1: 在页面中引入组件
```vue
<script setup lang="ts">
import { ref } from 'vue'
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import type { TableInstance } from 'element-plus'
const tableRef = ref<TableInstance>()
</script>
```
### 步骤 2: 添加列设置按钮
```vue
<template>
<div>
<!-- 列设置按钮放在表格上方 -->
<right-toolbar>
<TableColumnControl
:table-ref="tableRef"
trigger-circle
>
<template #trigger>
<el-tooltip content="列设置" placement="top">
<el-button circle style="margin-left: 0;">
<el-icon><Menu /></el-icon>
</el-button>
</el-tooltip>
</template>
</TableColumnControl>
</right-toolbar>
<!-- 表格 -->
<el-table ref="tableRef" :data="tableData">
<el-table-column prop="name" label="姓名" />
<el-table-column prop="age" label="年龄" />
<el-table-column prop="email" label="邮箱" />
<el-table-column label="操作" fixed="right">
<template #default="scope">
<el-button @click="handleEdit(scope.row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
```
### 步骤 3: 完成!
就这么简单!组件会自动:
- 从表格中提取列配置
- 根据当前路由自动生成存储 key
- 保存和恢复用户的列设置
## 手动配置方式(可选)
如果你的表格列是动态生成的,或者需要自定义列配置,可以使用手动配置:
```vue
<template>
<div>
<right-toolbar>
<TableColumnControl
:columns="tableColumns"
v-model="visibleColumns"
trigger-circle
>
<template #trigger>
<el-tooltip content="列设置" placement="top">
<el-button circle>
<el-icon><Menu /></el-icon>
</el-button>
</el-tooltip>
</template>
</TableColumnControl>
</right-toolbar>
<el-table :data="tableData">
<el-table-column
v-for="col in sortedTableColumns"
v-if="checkColumnVisible(col.prop || '')"
:key="col.prop"
:prop="col.prop"
:label="col.label"
/>
</el-table>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useRoute } from 'vue-router'
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
const route = useRoute()
const visibleColumns = ref<string[]>([])
const columnOrder = ref<string[]>([])
const tableColumns = [
{ prop: 'name', label: '姓名' },
{ prop: 'age', label: '年龄' },
{ prop: 'email', label: '邮箱' },
{ prop: 'action', label: '操作', fixed: true, alwaysShow: true }
]
// 检查列是否可见
const checkColumnVisible = (prop: string): boolean => {
if (visibleColumns.value.length === 0) return true
return visibleColumns.value.includes(prop)
}
// 排序后的列
const sortedTableColumns = computed(() => {
if (columnOrder.value.length === 0) return tableColumns
const ordered: typeof tableColumns = []
const unordered: typeof tableColumns = []
columnOrder.value.forEach(key => {
const col = tableColumns.find(c => (c.prop || c.label) === key)
if (col) ordered.push(col)
})
tableColumns.forEach(col => {
const key = col.prop || col.label
if (!columnOrder.value.includes(key)) {
unordered.push(col)
}
})
return [...ordered, ...unordered]
})
// 加载保存的配置
const loadSavedConfig = () => {
const routePath = route.path.replace(/^\//, '').replace(/\//g, '-')
const storageKey = `table-columns-${routePath}`
const saved = localStorage.getItem(storageKey)
if (saved) {
try {
const savedColumns = JSON.parse(saved)
const validColumns = tableColumns
.filter(col => !col.alwaysShow && !col.fixed)
.map(col => col.prop || col.label)
const filteredSaved = savedColumns.filter((col: string) => validColumns.includes(col))
if (filteredSaved.length > 0) {
visibleColumns.value = filteredSaved
} else {
visibleColumns.value = validColumns
}
} catch (e) {
visibleColumns.value = tableColumns
.filter(col => !col.alwaysShow && !col.fixed)
.map(col => col.prop || col.label)
}
} else {
visibleColumns.value = tableColumns
.filter(col => !col.alwaysShow && !col.fixed)
.map(col => col.prop || col.label)
}
// 加载排序
const orderKey = `${storageKey}-order`
const savedOrder = localStorage.getItem(orderKey)
if (savedOrder) {
try {
columnOrder.value = JSON.parse(savedOrder)
} catch (e) {
columnOrder.value = tableColumns
.filter(col => !col.alwaysShow && !col.fixed)
.map(col => col.prop || col.label)
}
} else {
columnOrder.value = tableColumns
.filter(col => !col.alwaysShow && !col.fixed)
.map(col => col.prop || col.label)
}
}
// 监听列变化
const handleColumnChange = (columns: string[]) => {
visibleColumns.value = columns
const routePath = route.path.replace(/^\//, '').replace(/\//g, '-')
const storageKey = `table-columns-${routePath}`
const selectableColumns = columns.filter(col => {
const column = tableColumns.find(c => (c.prop || c.label) === col)
return column && !column.alwaysShow && !column.fixed
})
localStorage.setItem(storageKey, JSON.stringify(selectableColumns))
}
// 监听列排序变化
const handleColumnOrderChange = (order: string[]) => {
columnOrder.value = order
const routePath = route.path.replace(/^\//, '').replace(/\//g, '-')
const storageKey = `table-columns-${routePath}-order`
localStorage.setItem(storageKey, JSON.stringify(order))
}
// 立即加载配置
loadSavedConfig()
</script>
```
## 使用 Composable推荐
为了简化手动配置方式,可以使用 `useTableColumnControl` composable
```vue
<script setup lang="ts">
import { computed } from 'vue'
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import { useTableColumnControl } from '/@/composables/useTableColumnControl'
const tableColumns = [
{ prop: 'name', label: '姓名' },
{ prop: 'age', label: '年龄' },
{ prop: 'email', label: '邮箱' },
{ prop: 'action', label: '操作', fixed: true, alwaysShow: true }
]
const {
visibleColumns,
columnOrder,
checkColumnVisible,
saveColumnConfig,
saveColumnOrder
} = useTableColumnControl({
columns: tableColumns
})
const sortedTableColumns = computed(() => {
// ... 排序逻辑
})
</script>
```
## 常见问题
### Q: 如何自定义存储 key
```vue
<TableColumnControl
:table-ref="tableRef"
storage-key="my-custom-key"
/>
```
### Q: 如何设置始终显示的列?
```vue
<TableColumnControl
:table-ref="tableRef"
:auto-extract-options="{
alwaysShow: ['name', 'action']
}"
/>
```
### Q: 如何设置默认隐藏的列?
```vue
<TableColumnControl
:table-ref="tableRef"
:auto-extract-options="{
defaultHidden: ['remark', 'description']
}"
/>
```
### Q: 固定列会自动隐藏吗?
不会。使用 `fixed="left"``fixed="right"` 的列会自动标记为不可隐藏。
## 完整示例
查看 `src/views/stuwork/classroomhygienemonthly/index.vue` 了解完整的使用示例。

View File

@@ -1,16 +1,65 @@
# TableColumnControl 表格列显隐控制组件
# TableColumnControl 表格列控制组件
一个通用的表格列显示/隐藏控制组件,支持动态控制表格列的显示状态
一个**完全通用**的表格列控制组件,支持列的显示/隐藏控制和拖拽排序,配置会自动保存到 localStorage并在页面重新加载时自动恢复
## 功能特性
## 功能特性
-动态控制表格列的显示/隐藏
-支持全选/全不选
-支持重置为默认值
-支持 localStorage 持久化
-支持固定列(不可隐藏)
-支持始终显示的列
- ✅ 可自定义触发按钮样式
-**列的显示/隐藏控制**:用户可以自定义显示哪些列
-**列的拖拽排序**:用户可以自定义列的显示顺序
-**自动保存和恢复**:配置自动保存到 localStorage基于路由自动生成存储 key
-**两种使用方式**:支持自动提取或手动配置
-**完全通用**:可以在任何页面使用,无需额外配置
-**固定列保护**固定列fixed和始终显示的列alwaysShow自动不可隐藏
## 🚀 快速开始3 步集成)
### 步骤 1: 引入组件
```vue
<script setup lang="ts">
import { ref } from 'vue'
import TableColumnControl from '/@/components/TableColumnControl/index.vue'
import type { TableInstance } from 'element-plus'
const tableRef = ref<TableInstance>()
</script>
```
### 步骤 2: 添加列设置按钮
```vue
<template>
<right-toolbar>
<TableColumnControl
:table-ref="tableRef"
trigger-circle
>
<template #trigger>
<el-tooltip content="列设置" placement="top">
<el-button circle style="margin-left: 0;">
<el-icon><Menu /></el-icon>
</el-button>
</el-tooltip>
</template>
</TableColumnControl>
</right-toolbar>
<el-table ref="tableRef" :data="tableData">
<el-table-column prop="name" label="姓名" />
<el-table-column prop="age" label="年龄" />
<el-table-column label="操作" fixed="right" />
</el-table>
</template>
```
### 步骤 3: 完成!
就这么简单!组件会自动:
- ✅ 从表格中提取列配置
- ✅ 根据当前路由自动生成存储 key
- ✅ 保存和恢复用户的列设置
> 📖 更多使用方式请查看 [快速开始指南](./QUICK_START.md)
## 使用方法

View File

@@ -16,28 +16,30 @@
<el-dialog
v-model="visible"
title="列显隐设置"
:width="dialogWidth"
:width="props.dialogWidth"
append-to-body
:close-on-click-modal="false"
>
<div class="column-control-content">
<div class="column-control-body">
<el-checkbox-group v-model="checkedColumns" @change="handleColumnChange" class="column-checkbox-group">
<div class="column-switch-group" ref="sortableContainerRef">
<div
v-for="column in actualColumns"
v-for="column in sortedColumns"
:key="column.prop || column.label"
class="column-item"
:data-key="column.prop || column.label"
>
<el-checkbox
:label="column.prop || column.label"
<div class="column-drag-handle">
<el-icon class="drag-icon"><Rank /></el-icon>
</div>
<span class="column-label">{{ column.label }}</span>
<el-switch
v-model="columnVisibleMap[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> -->
@change="handleSwitchChange(column.prop || column.label, $event)"
/>
</div>
</el-checkbox-group>
</div>
</div>
</div>
</el-dialog>
@@ -46,11 +48,14 @@
<script setup lang="ts">
import { ref, computed, watch, onMounted, nextTick, useSlots, type Ref } from 'vue'
import { Menu } from '@element-plus/icons-vue'
import { useRoute } from 'vue-router'
import { Menu, Rank } from '@element-plus/icons-vue'
import type { TableInstance } from 'element-plus'
import { useTableColumns, type ColumnConfig } from '/@/composables/useTableColumns'
import Sortable from 'sortablejs'
const slots = useSlots()
const route = useRoute()
interface Column {
prop?: string
@@ -85,20 +90,40 @@ const props = withDefaults(defineProps<Props>(), {
triggerCircle: false,
triggerText: '',
triggerLink: false,
dialogWidth: '800px'
dialogWidth: '500px'
})
const emit = defineEmits<{
'update:modelValue': [value: string[]]
'change': [value: string[]]
'order-change': [value: string[]]
}>()
const visible = ref(false)
const checkedColumns = ref<string[]>([])
const columnVisibleMap = ref<Record<string, boolean>>({})
const sortableContainerRef = ref<HTMLElement>()
const columnOrder = ref<string[]>([])
let sortableInstance: any = null
// 根据路由自动生成 storageKey如果没有提供
const getStorageKey = (): string => {
if (props.storageKey) {
return props.storageKey
}
// 使用路由路径生成 key移除开头的 / 并替换 / 为 -
const routePath = route.path.replace(/^\//, '').replace(/\//g, '-')
return `table-columns-${routePath}`
}
// 获取排序配置的 storageKey
const getOrderStorageKey = (): string => {
return `${getStorageKey()}-order`
}
// 如果提供了 tableRef使用自动提取否则使用手动配置的 columns
const tableColumnsResult = props.tableRef
? useTableColumns(props.tableRef, props.storageKey, props.autoExtractOptions)
? useTableColumns(props.tableRef, getStorageKey(), props.autoExtractOptions)
: {
columns: computed(() => []),
visibleColumns: computed(() => []),
@@ -123,6 +148,41 @@ const actualColumns = computed(() => {
return result
})
// 排序后的列配置(排除序号和操作列)
const sortedColumns = computed(() => {
const columns = actualColumns.value.filter(col => {
const key = col.prop || col.label
// 排除序号列type === 'index')和操作列(固定右侧或 alwaysShow
return col.type !== 'index' && !(col.fixed === 'right' && col.alwaysShow) && key !== '操作'
})
// 如果有保存的排序顺序,按照顺序排序
if (columnOrder.value.length > 0) {
const orderedColumns: ColumnConfig[] = []
const unorderedColumns: ColumnConfig[] = []
// 先按照保存的顺序添加列
columnOrder.value.forEach(key => {
const col = columns.find(c => (c.prop || c.label) === key)
if (col) {
orderedColumns.push(col)
}
})
// 添加未在排序列表中的列(新增的列)
columns.forEach(col => {
const key = col.prop || col.label
if (!columnOrder.value.includes(key)) {
unorderedColumns.push(col)
}
})
return [...orderedColumns, ...unorderedColumns]
}
return columns
})
// 获取所有列(包括固定列和 alwaysShow 列)
const getAllColumns = (): string[] => {
return actualColumns.value.map(col => col.prop || col.label)
@@ -130,6 +190,37 @@ const getAllColumns = (): string[] => {
// 初始化选中的列
const initCheckedColumns = () => {
// 如果 actualColumns 为空,无法初始化
if (actualColumns.value.length === 0) {
return
}
// 优先从 localStorage 读取配置
const storageKey = getStorageKey()
const saved = localStorage.getItem(storageKey)
if (saved) {
try {
const savedColumns = JSON.parse(saved)
const allSelectableColumns = actualColumns.value
.filter(col => !col.alwaysShow && !col.fixed)
.map(col => col.prop || col.label)
// 使用保存的列配置(即使数量少于所有列,也使用保存的配置)
// 但确保包含固定列和 alwaysShow 列
const fixedAndAlwaysShow = actualColumns.value
.filter(col => col.alwaysShow || !!col.fixed)
.map(col => col.prop || col.label)
checkedColumns.value = [...new Set([...savedColumns, ...fixedAndAlwaysShow])]
// 同步到外部 modelValue
emit('update:modelValue', checkedColumns.value)
return // 已从 localStorage 加载,直接返回
} catch (e) {
// 如果解析失败,继续使用其他方式初始化
}
}
// 如果 localStorage 中没有保存的配置,使用其他方式初始化
if (props.modelValue && props.modelValue.length > 0) {
checkedColumns.value = [...props.modelValue]
} else if (props.tableRef && autoVisibleColumns.value.length > 0) {
@@ -151,60 +242,118 @@ const initCheckedColumns = () => {
.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)
const allSelectableColumns = actualColumns.value
.filter(col => !col.alwaysShow && !col.fixed)
.map(col => col.prop || col.label)
// 如果保存的列数量少于所有列,默认全部选中
if (savedColumns.length < allSelectableColumns.length) {
const fixedAndAlwaysShow = actualColumns.value
.filter(col => col.alwaysShow || !!col.fixed)
.map(col => col.prop || col.label)
checkedColumns.value = [...new Set([...allSelectableColumns, ...fixedAndAlwaysShow])]
} else {
// 使用保存的列,但确保包含固定列和 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 {
// 没有 storageKey默认全部选中
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]
// 初始化列可见性映射
const initColumnVisibleMap = () => {
const map: Record<string, boolean> = {}
actualColumns.value.forEach(col => {
const key = col.prop || col.label
// 固定列和 alwaysShow 列始终为 true
if (col.fixed || col.alwaysShow) {
map[key] = true
} else {
// 其他列根据 checkedColumns 判断
map[key] = checkedColumns.value.includes(key)
}
})
columnVisibleMap.value = map
}
// 初始化列排序顺序
const initColumnOrder = () => {
const storageKey = getOrderStorageKey()
// 先获取所有可排序的列(不依赖 sortedColumns computed
const allSortableColumns = actualColumns.value.filter(col => {
const key = col.prop || col.label
return col.type !== 'index' && !(col.fixed === 'right' && col.alwaysShow) && key !== '操作'
})
const validColumns = allSortableColumns.map(col => col.prop || col.label)
const saved = localStorage.getItem(storageKey)
if (saved) {
try {
const savedOrder = JSON.parse(saved)
// 验证保存的列是否仍然存在
columnOrder.value = savedOrder.filter((key: string) => validColumns.includes(key))
// 添加新列到排序列表末尾
validColumns.forEach(key => {
if (!columnOrder.value.includes(key)) {
columnOrder.value.push(key)
}
})
} catch (e) {
// 如果解析失败,使用默认顺序
columnOrder.value = [...validColumns]
}
} else {
// 首次使用,使用默认顺序
columnOrder.value = [...validColumns]
}
}
// 保存列排序顺序
const saveColumnOrder = () => {
const storageKey = getOrderStorageKey()
if (columnOrder.value.length > 0) {
localStorage.setItem(storageKey, JSON.stringify(columnOrder.value))
}
}, { deep: true })
}
// 初始化拖拽排序
const initSortable = () => {
if (!sortableContainerRef.value) {
return
}
// 销毁旧的实例
if (sortableInstance) {
sortableInstance.destroy()
sortableInstance = null
}
// 等待 DOM 更新完成
nextTick(() => {
if (!sortableContainerRef.value) return
// 创建新的拖拽实例(整行可拖拽)
sortableInstance = Sortable.create(sortableContainerRef.value, {
animation: 150,
ghostClass: 'column-item-ghost',
dragClass: 'column-item-drag',
forceFallback: false,
filter: '.el-switch', // 排除开关元素,避免拖拽时触发开关
preventOnFilter: false,
onEnd: (evt) => {
const { newIndex, oldIndex } = evt
if (newIndex !== undefined && oldIndex !== undefined && newIndex !== oldIndex) {
// 获取当前排序后的列
const currentOrder = sortedColumns.value.map(col => col.prop || col.label)
// 更新列顺序
const movedKey = currentOrder[oldIndex]
currentOrder.splice(oldIndex, 1)
currentOrder.splice(newIndex, 0, movedKey)
// 更新 columnOrder
columnOrder.value = currentOrder
// 保存排序顺序
saveColumnOrder()
// 触发 change 事件,通知父组件列顺序已改变
emit('order-change', [...columnOrder.value])
}
}
})
})
}
// 监听弹窗打开,触发列配置重新提取
watch(visible, (newVal) => {
@@ -225,6 +374,22 @@ watch(visible, (newVal) => {
checkedColumns.value = [...currentChecked, ...missingFixed]
}
}
// 更新列可见性映射
initColumnVisibleMap()
// 初始化列排序顺序(只在 columnOrder 为空时初始化,避免覆盖已保存的配置)
if (columnOrder.value.length === 0) {
initColumnOrder()
}
// 初始化拖拽排序
nextTick(() => {
initSortable()
})
}
} else {
// 弹窗关闭时销毁拖拽实例
if (sortableInstance) {
sortableInstance.destroy()
sortableInstance = null
}
}
if (newVal && props.tableRef) {
@@ -345,7 +510,51 @@ defineExpose({
refreshColumns: refreshAutoColumns
})
// 变化处理(实时生效)
// 开关变化处理
const handleSwitchChange = (columnKey: string, value: boolean) => {
// 更新 columnVisibleMap
columnVisibleMap.value[columnKey] = value
// 更新 checkedColumns
if (value) {
// 开启:添加到选中列表
if (!checkedColumns.value.includes(columnKey)) {
checkedColumns.value.push(columnKey)
}
} else {
// 关闭:从选中列表移除
checkedColumns.value = checkedColumns.value.filter(col => col !== columnKey)
}
// 确保固定列和 alwaysShow 列始终在选中列表中
const fixedAndAlwaysShow = actualColumns.value
.filter(col => col.alwaysShow || !!col.fixed)
.map(col => col.prop || col.label)
const finalValue = [...new Set([...checkedColumns.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只保存可选择的列
const storageKey = getStorageKey()
const selectableValue = finalValue.filter(col => {
const column = actualColumns.value.find(c => (c.prop || c.label) === col)
return column && !column.alwaysShow && !column.fixed
})
localStorage.setItem(storageKey, JSON.stringify(selectableValue))
}
}
// 列变化处理(实时生效)- 保留用于兼容性
const handleColumnChange = (value: string[]) => {
// 确保固定列和 alwaysShow 列始终在选中列表中
const fixedAndAlwaysShow = actualColumns.value
@@ -366,26 +575,64 @@ const handleColumnChange = (value: string[]) => {
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))
}
const storageKey = getStorageKey()
const selectableValue = finalValue.filter(col => {
const column = actualColumns.value.find(c => (c.prop || c.label) === col)
return column && !column.alwaysShow && !column.fixed
})
localStorage.setItem(storageKey, JSON.stringify(selectableValue))
}
}
// 监听外部 modelValue 变化
watch(() => props.modelValue, (newVal) => {
// 只有在外部传入的值与当前值不同时才更新,避免覆盖已加载的配置
if (newVal && newVal.length > 0) {
checkedColumns.value = [...newVal]
const currentSorted = [...checkedColumns.value].sort()
const newSorted = [...newVal].sort()
if (JSON.stringify(currentSorted) !== JSON.stringify(newSorted)) {
// 检查是否是从 localStorage 加载的配置
const storageKey = getStorageKey()
const saved = localStorage.getItem(storageKey)
// 如果 checkedColumns 为空,或者没有保存的配置,才使用外部传入的值
if (checkedColumns.value.length === 0 || !saved) {
checkedColumns.value = [...newVal]
initColumnVisibleMap()
}
}
}
}, { immediate: true })
}, { immediate: false })
// 初始化
onMounted(() => {
initCheckedColumns()
// 等待 actualColumns 准备好
nextTick(() => {
// 优先从 localStorage 加载配置
if (actualColumns.value.length > 0) {
initCheckedColumns()
initColumnVisibleMap()
initColumnOrder()
// 如果外部传入了 modelValue且 checkedColumns 为空,才使用外部传入的值
if (props.modelValue && props.modelValue.length > 0 && checkedColumns.value.length === 0) {
checkedColumns.value = [...props.modelValue]
initColumnVisibleMap()
}
} else {
// 如果 actualColumns 为空,等待它准备好
// 使用 nextTick 确保所有函数都已定义
nextTick(() => {
const unwatch = watch(actualColumns, (newColumns) => {
if (newColumns.length > 0) {
initCheckedColumns()
initColumnVisibleMap()
initColumnOrder()
unwatch() // 停止监听
}
}, { immediate: true })
})
}
})
})
</script>
@@ -396,31 +643,84 @@ onMounted(() => {
.column-control-content {
.column-control-body {
max-height: 400px;
overflow-y: auto;
margin-bottom: 12px;
.column-checkbox-group {
.column-switch-group {
display: flex;
flex-wrap: wrap;
gap: 12px 16px;
flex-direction: column;
gap: 12px;
width: 100%;
}
.column-item {
display: flex;
align-items: center;
gap: 6px;
min-width: 120px;
gap: 12px;
padding: 10px 12px;
border-radius: 4px;
transition: all 0.2s;
cursor: move;
width: 100%;
box-sizing: border-box;
user-select: none;
:deep(.el-checkbox) {
margin-right: 0;
&:hover {
background-color: #f5f7fa;
}
&:active {
cursor: grabbing;
}
&.sortable-ghost {
opacity: 0.5;
background-color: #e6f7ff;
}
&.sortable-drag {
opacity: 0.8;
}
.column-drag-handle {
display: flex;
align-items: center;
justify-content: center;
color: #909399;
flex-shrink: 0;
width: 20px;
height: 20px;
pointer-events: none; // 让图标不阻止拖拽
.el-checkbox__label {
font-size: 14px;
color: #606266;
padding-left: 8px;
.drag-icon {
font-size: 18px;
}
}
.column-label {
font-size: 14px;
color: #606266;
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
pointer-events: none; // 让文本不阻止拖拽
}
// 开关元素可以正常交互
.el-switch {
pointer-events: auto;
cursor: pointer;
}
}
.column-item-ghost {
opacity: 0.5;
background-color: #e6f7ff;
}
.column-item-drag {
opacity: 0.8;
}
}