feat(GiTable): 增强列设置功能

This commit is contained in:
KAI
2025-03-20 07:56:53 +00:00
committed by Charles7c
parent 4bd083eb91
commit 11d54572e9
2 changed files with 590 additions and 85 deletions

View File

@@ -33,35 +33,14 @@
</a-doption>
</template>
</a-dropdown>
<a-popover
v-if="showSettingColumnBtn" trigger="click" position="br"
:content-style="{ minWidth: '120px', padding: '6px 8px 10px' }"
>
<a-tooltip content="列设置">
<a-button>
<template #icon>
<icon-settings />
</template>
</a-button>
</a-tooltip>
<template #content>
<div class="gi-table__draggable">
<VueDraggable v-model="settingColumnList">
<div v-for="item in settingColumnList" :key="item.title" class="drag-item">
<div class="drag-item__move"><icon-drag-dot-vertical /></div>
<a-checkbox v-model:model-value="item.show" :disabled="item.disabled">{{ item.title }}</a-checkbox>
</div>
</VueDraggable>
</div>
<a-divider :margin="6" />
<a-row justify="center">
<a-button type="primary" size="mini" long @click="resetSettingColumns">
<template #icon><icon-refresh /></template>
<template #default>重置</template>
</a-button>
</a-row>
</template>
</a-popover>
<ColumnSetting
v-if="showSettingColumnBtn"
ref="columnSettingRef"
v-model:columns="innerColumns"
:disabled-keys="disabledColumnKeys"
:table-id="tableId"
@visible-columns-change="handleVisibleColumnsChange"
/>
<a-tooltip content="全屏">
<a-button v-if="showFullscreenBtn" @click="toggleFullscreen">
<template #icon>
@@ -100,9 +79,9 @@
<script setup lang="ts" generic="T extends TableData">
import { computed, ref, watch } from 'vue'
import type { DropdownInstance, TableColumnData, TableData, TableInstance } from '@arco-design/web-vue'
import { VueDraggable } from 'vue-draggable-plus'
import { omit } from 'lodash-es'
import type { TableProps } from './type'
import ColumnSetting from './components/ColumnSetting.vue'
defineOptions({ name: 'GiTable' })
@@ -117,6 +96,7 @@ const props = withDefaults(defineProps<Props>(), {
/** Emits 类型定义 */
const emit = defineEmits<{
(e: 'refresh'): void
(e: 'update:columns', columns: TableColumnData[]): void
}>()
/** Slots 类型定义 */
@@ -153,19 +133,16 @@ interface Props extends TableProps {
disabledTools?: string[]
/** 表格数据 */
data: T[]
/** 表格标识,用于存储列设置 */
tableId?: string
}
const slots = useSlots()
const attrs = useAttrs()
/** 表格属性计算 */
const tableProps = computed(() => ({
...omit(props, ['title', 'disabledColumnKeys', 'disabledTools']),
...attrs,
}))
/** 组件状态 */
const tableRef = useTemplateRef('tableRef')
const columnSettingRef = ref<InstanceType<typeof ColumnSetting> | null>(null)
const stripe = ref(false)
const size = ref<TableInstance['size']>('large')
const isBordered = ref(false)
@@ -205,66 +182,46 @@ const showSettingColumnBtn = computed(() => {
return !props.disabledTools?.includes('setting') && Boolean(columns?.length)
})
/** 列设置项类型 */
interface SettingColumnItem {
/** 列标题 */
title: string
/** 列标识 */
key: string
/** 是否显示 */
show: boolean
/** 是否禁用 */
disabled: boolean
}
/** 内部维护列数据 */
const innerColumns = ref<TableColumnData[]>([])
const settingColumnList = ref<SettingColumnItem[]>([])
/** 重置列设置 */
const resetSettingColumns = () => {
if (!props.columns) {
settingColumnList.value = []
return
/** 监听 props.columns 变化 */
watch(() => props.columns, (newColumns) => {
if (newColumns && innerColumns.value.length === 0) {
innerColumns.value = [...newColumns]
}
}, { immediate: true })
const columns = props.columns as TableColumnData[]
settingColumnList.value = columns.map((column) => {
const key = column.dataIndex || (typeof column.title === 'string' ? column.title : '')
return {
key,
title: typeof column.title === 'string' ? column.title : '',
show: column.show ?? true,
disabled: props.disabledColumnKeys.includes(key),
}
})
/** 实际显示的列由ColumnSetting组件计算 */
const tableColumns = ref<TableColumnData[]>([])
/** 处理列设置组件的可见列变化 */
const handleVisibleColumnsChange = (columns: TableColumnData[]) => {
tableColumns.value = columns
}
/** 监听属性变化,重置列设置 */
watch(
() => props.columns,
() => resetSettingColumns(),
{ immediate: true },
)
/** 表格属性计算 */
const tableProps = computed(() => ({
...omit(props, ['title', 'disabledColumnKeys', 'disabledTools']),
...attrs,
}))
/** 计算显示的列 */
const visibleColumns = computed(() => {
if (!props.columns) return []
// 如果tableColumns有值使用tableColumns
if (tableColumns.value && tableColumns.value.length > 0) {
return tableColumns.value
}
const columns = props.columns as TableColumnData[]
const columnMap = new Map(
columns.map((col) => [
col.dataIndex || (typeof col.title === 'string' ? col.title : ''),
col,
]),
)
// 按照设置列表的顺序返回可见列
return settingColumnList.value
.filter((item) => item.show)
.map((item) => columnMap.get(item.key))
.filter(Boolean) as TableColumnData[]
// 否则使用原始的columns
return props.columns?.filter((col) => col.show !== false) || []
})
defineExpose({ tableRef })
defineExpose({
tableRef,
resetColumns: () => columnSettingRef.value?.resetColumns?.(),
saveColumns: () => columnSettingRef.value?.saveColumns?.(),
})
</script>
<style lang="scss" scoped>

View File

@@ -0,0 +1,548 @@
<template>
<a-popover
v-model:open="popoverVisible"
trigger="click"
position="br"
:content-style="{ minWidth: '240px', padding: '6px 8px 10px' }"
@open="handleOpen"
>
<a-button>
<template #icon><icon-settings /></template>
</a-button>
<template #content>
<!-- 顶部控制区域 -->
<div class="gi-table__setting-header">
<div class="gi-table__setting-select-all">
<a-checkbox
:model-value="isAllSelected"
:indeterminate="isIndeterminate"
@change="handleSelectAll"
>
全选
</a-checkbox>
</div>
<div class="gi-table__setting-reset">
<a-link @click="handleReset">重置</a-link>
</div>
</div>
<a-divider :margin="6" />
<!-- 列拖拽排序区域 -->
<div class="gi-table__draggable">
<VueDraggable v-model="localColumns" @end="handleDragEnd">
<div
v-for="item in localColumns"
:key="item.key"
class="gi-table__draggable-item"
>
<div class="gi-table__draggable-item-move">
<icon-drag-dot-vertical />
</div>
<a-checkbox
v-model="item.show"
:disabled="item.disabled"
@change="() => handleColumnChange(item)"
>
{{ item.title }}
</a-checkbox>
<div class="gi-table__draggable-item-fixed">
<span
class="gi-table__fixed-icon"
:class="[
{
'gi-table__fixed-icon--active': item.fixed === 'left',
'gi-table__fixed-icon--disabled': !item.show,
},
]"
@click="handleFixedColumn(item, 'left')"
>
<icon-left />
</span>
<span
class="gi-table__fixed-icon"
:class="[
{
'gi-table__fixed-icon--active': item.fixed === 'right',
'gi-table__fixed-icon--disabled': !item.show,
},
]"
@click="handleFixedColumn(item, 'right')"
>
<icon-right />
</span>
</div>
</div>
</VueDraggable>
</div>
<a-divider :margin="6" />
<!-- 底部保存按钮 -->
<a-row justify="center">
<a-button type="primary" long @click="handleSave">
保存
</a-button>
</a-row>
</template>
</a-popover>
</template>
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import { VueDraggable } from 'vue-draggable-plus'
import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
interface ColumnItem {
title: string
key: string
dataIndex?: string
show: boolean
disabled: boolean
fixed?: 'left' | 'right'
width?: number
}
const props = defineProps<{
columns: TableColumnData[]
disabledKeys: string[]
tableId?: string
}>()
const emit = defineEmits<{
(e: 'update:columns', columns: TableColumnData[]): void
(e: 'visible-columns-change', columns: TableColumnData[]): void
}>()
const route = useRoute()
const popoverVisible = ref(false)
const localColumns = ref<ColumnItem[]>([])
// 保存原始列配置
const originalColumns = ref<TableColumnData[]>([])
// 提取列排序逻辑
const sortColumnsByLocalOrder = (columns: TableColumnData[]) => {
const keyOrderMap = new Map(
localColumns.value.map((col, index) => [col.key, index]),
)
return columns.sort((a, b) => {
const keyA = a.dataIndex || (typeof a.title === 'string' ? a.title : '')
const keyB = b.dataIndex || (typeof b.title === 'string' ? b.title : '')
const orderA = keyOrderMap.get(keyA) ?? 999
const orderB = keyOrderMap.get(keyB) ?? 999
return orderA - orderB
})
}
// 更新列变化
const emitColumnsChange = () => {
// 将当前列设置应用到原始列
const updatedColumns = props.columns.map((originalCol) => {
const key = originalCol.dataIndex || (typeof originalCol.title === 'string' ? originalCol.title : '')
const localCol = localColumns.value.find((col) => col.key === key)
if (localCol) {
return {
...originalCol,
show: localCol.show,
fixed: localCol.fixed,
width: localCol.width || originalCol.width,
}
}
return originalCol
})
// 根据拖拽后的顺序重新排序列
const sortedColumns = sortColumnsByLocalOrder(updatedColumns)
emit('update:columns', sortedColumns)
}
// 缓存可选列
const selectableColumns = computed(() => {
return localColumns.value.filter((col) => !col.disabled)
})
// 计算全选状态
const isAllSelected = computed(() => {
if (selectableColumns.value.length === 0) return false
return selectableColumns.value.every((col) => col.show)
})
// 计算半选状态
const isIndeterminate = computed(() => {
if (selectableColumns.value.length === 0) return false
const selectedCount = selectableColumns.value.filter((col) => col.show).length
return selectedCount > 0 && selectedCount < selectableColumns.value.length
})
// 全选/取消全选处理
const handleSelectAll = (checked: boolean) => {
selectableColumns.value.forEach((col) => {
col.show = checked
if (!checked) {
col.fixed = undefined
}
})
emitColumnsChange()
}
/** 获取存储键 */
const getStorageKey = computed(() => {
const pathKey = route.path.replace(/\//g, ':')
return props.tableId
? `table-columns-settings-${pathKey}:${props.tableId}`
: `table-columns-settings-${pathKey}`
})
// 计算可见列
const visibleColumns = computed(() => {
// 首先根据 show 属性过滤要显示的列
const showColumns = props.columns
.filter((col) => {
const key = col.dataIndex || (typeof col.title === 'string' ? col.title : '')
const localCol = localColumns.value.find((item) => item.key === key)
return localCol?.show !== false
})
.map((col) => {
const key = col.dataIndex || (typeof col.title === 'string' ? col.title : '')
const localCol = localColumns.value.find((item) => item.key === key)
if (localCol) {
return {
...col,
fixed: localCol.fixed,
width: localCol.width || col.width,
}
}
return col
})
// 根据拖拽后的顺序重新排序列
const sortedShowColumns = sortColumnsByLocalOrder(showColumns)
// 然后根据固定状态进行排序(左固定 -> 未固定 -> 右固定)
const leftFixedColumns = sortedShowColumns.filter((col) => col.fixed === 'left')
const unfixedColumns = sortedShowColumns.filter((col) => !col.fixed)
const rightFixedColumns = sortedShowColumns.filter((col) => col.fixed === 'right')
// 返回排序后的列
return [...leftFixedColumns, ...unfixedColumns, ...rightFixedColumns]
})
// 监听本地列变化,发出可见列变化事件
watch([localColumns], () => {
if (localColumns.value.length > 0) {
emit('visible-columns-change', visibleColumns.value)
}
}, { immediate: true, deep: true })
// 将列转换为本地列格式
const transformColumns = (columns = props.columns) => {
if (!columns || columns.length === 0) {
return []
}
// 按固定位置分类列
const leftColumns: ColumnItem[] = []
const centerColumns: ColumnItem[] = []
const rightColumns: ColumnItem[] = []
columns.forEach((column) => {
const key = column.dataIndex || (typeof column.title === 'string' ? column.title : '')
const item: ColumnItem = {
key,
dataIndex: column.dataIndex as string,
title: typeof column.title === 'string' ? column.title : String(key),
show: column.show ?? true,
disabled: props.disabledKeys.includes(key),
fixed: typeof column.fixed === 'boolean' ? 'left' : column.fixed as 'left' | 'right' | undefined,
width: column.width as number,
}
if (item.fixed === 'left') {
leftColumns.push(item)
} else if (item.fixed === 'right') {
rightColumns.push(item)
} else {
centerColumns.push(item)
}
})
// 组合列表:左固定 + 中间 + 右固定
return [...leftColumns, ...centerColumns, ...rightColumns]
}
// 从localStorage恢复设置
const loadSettingsFromStorage = () => {
try {
const settingsJson = localStorage.getItem(getStorageKey.value)
if (!settingsJson) return false
const settings = JSON.parse(settingsJson)
if (!settings || !settings.columns || !Array.isArray(settings.columns)) return false
// 获取原始列
const columnsMap = new Map(
props.columns.map((col) => [
col.dataIndex || (typeof col.title === 'string' ? col.title : ''),
col,
]),
)
// 按固定位置分类列
const leftColumns: ColumnItem[] = []
const centerColumns: ColumnItem[] = []
const rightColumns: ColumnItem[] = []
settings.columns.forEach((item: ColumnItem) => {
const originalColumn = columnsMap.get(item.key)
if (originalColumn) {
const newItem: ColumnItem = {
...item,
title: typeof originalColumn.title === 'string' ? originalColumn.title : String(item.key),
dataIndex: originalColumn.dataIndex as string,
disabled: props.disabledKeys.includes(item.key),
width: item.width || originalColumn.width as number,
}
if (newItem.fixed === 'left') {
leftColumns.push(newItem)
} else if (newItem.fixed === 'right') {
rightColumns.push(newItem)
} else {
centerColumns.push(newItem)
}
}
})
// 组合列表:左固定 + 中间 + 右固定
localColumns.value = [...leftColumns, ...centerColumns, ...rightColumns]
// 检查是否有新增的列,将它们添加到末尾
const existingKeys = new Set(localColumns.value.map((col) => col.key))
const newColumns = props.columns
.filter((col) => {
const key = col.dataIndex || (typeof col.title === 'string' ? col.title : '')
return !existingKeys.has(key)
})
.map((col) => {
const key = col.dataIndex || (typeof col.title === 'string' ? col.title : '')
return {
key,
dataIndex: col.dataIndex as string,
title: typeof col.title === 'string' ? col.title : String(key),
show: col.show ?? true,
disabled: props.disabledKeys.includes(key),
fixed: typeof col.fixed === 'boolean' ? 'left' : col.fixed as 'left' | 'right' | undefined,
width: col.width as number,
}
})
if (newColumns.length > 0) {
localColumns.value = [...localColumns.value, ...newColumns]
}
// 发送更新事件
emitColumnsChange()
return true
} catch (e) {
console.error('Failed to load column settings from localStorage', e)
return false
}
}
// 初始化列表和原始列配置
watch(() => props.columns, (newColumns) => {
if (newColumns && newColumns.length > 0) {
// 第一次接收到非空列数据时,保存原始配置
if (originalColumns.value.length === 0) {
originalColumns.value = JSON.parse(JSON.stringify(newColumns))
}
// 如果本地列数组为空,初始化本地列
if (localColumns.value.length === 0) {
localColumns.value = transformColumns()
}
}
}, { immediate: true })
// 处理popover打开事件
const handleOpen = () => {
if (!localColumns.value.length) {
localColumns.value = transformColumns()
}
}
// 处理拖拽结束
const handleDragEnd = () => {
emitColumnsChange()
}
// 处理单列变更
const handleColumnChange = (item: ColumnItem) => {
// 如果是隐藏列,取消固定
if (!item.show) {
item.fixed = undefined
}
emitColumnsChange()
}
// 处理列固定
const handleFixedColumn = (item: ColumnItem, position: 'left' | 'right') => {
// 如果列没有显示,则不能固定
if (!item.show) return
// 检查列是否已经固定在当前位置,是则取消固定
const isCurrentlyFixed = item.fixed === position
// 只修改当前列的固定状态,不改变位置
item.fixed = isCurrentlyFixed ? undefined : position
// 如果被固定但没有宽度设置默认宽度100
if (item.fixed && !item.width) {
item.width = 100
}
// 发出列变更事件
emitColumnsChange()
}
// 重置
const handleReset = () => {
try {
// 清除本地存储
localStorage.removeItem(getStorageKey.value)
// 使用原始列配置重新创建本地列
if (originalColumns.value.length > 0) {
// 将原始列配置重新应用(深拷贝避免共享引用)
const columnsToReset = JSON.parse(JSON.stringify(originalColumns.value))
// 使用父组件传入的列更新内部状态
emit('update:columns', columnsToReset)
// 更新本地列配置
localColumns.value = transformColumns(columnsToReset)
} else {
// 如果没有原始配置则使用当前props
localColumns.value = transformColumns()
}
// 发送更新事件
emitColumnsChange()
// 显示成功消息
Message.success('已重置表格列')
} catch (e) {
console.error('Failed to reset column settings', e)
Message.error('重置表格列失败')
}
}
// 保存
const handleSave = () => {
if (!getStorageKey.value) return
try {
const settings = {
columns: localColumns.value,
}
localStorage.setItem(getStorageKey.value, JSON.stringify(settings))
popoverVisible.value = false
Message.success('保存成功')
} catch (e) {
console.error('Failed to save column settings', e)
Message.error('保存失败')
}
}
// 初始化时加载存储设置
onMounted(() => {
loadSettingsFromStorage()
})
// 导出供外部使用
defineExpose({
visibleColumns,
resetColumns: handleReset,
saveColumns: handleSave,
})
</script>
<style lang="scss" scoped>
.gi-table {
&__setting-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
&-select-all {
font-size: 14px;
}
&-reset {
font-size: 14px;
}
}
&__draggable {
padding: 1px 0;
max-height: 250px;
box-sizing: border-box;
overflow: hidden auto;
}
&__draggable-item {
display: flex;
align-items: center;
cursor: pointer;
padding: 4px 0;
&:hover {
background-color: var(--color-fill-2);
}
&-move {
padding: 0 4px;
cursor: move;
}
&-fixed {
margin-left: auto;
display: flex;
align-items: center;
gap: 8px;
}
}
&__fixed-icon {
display: inline-flex;
align-items: center;
padding: 2px 6px;
border-radius: 2px;
cursor: pointer;
transition: all 0.2s ease;
font-size: 12px;
border: 1px solid var(--color-border-2);
&--active {
border-color: rgb(var(--primary-6));
background-color: rgb(var(--primary-1));
color: rgb(var(--primary-6));
}
&--disabled {
opacity: 0.5;
cursor: not-allowed;
}
&:hover:not(&--disabled) {
border-color: rgb(var(--primary-6));
color: rgb(var(--primary-6));
}
}
}
</style>