Files
school-developer/docs/tableHook使用方式.md
吴红兵 94c3473958 fix
2026-03-07 01:34:48 +08:00

15 KiB
Raw Blame History

useTableColumnControl Hook 使用文档

一、简介

useTableColumnControl 是一个用于统一管理表格列显示/隐藏和排序功能的 Vue 3 Composition API Hook。它封装了列配置的加载、保存、同步等逻辑让开发者只需几行代码即可实现完整的表格列控制功能。

主要功能

  • 列显示/隐藏控制
  • 列排序管理
  • 配置自动保存到本地存储
  • 配置同步到后端服务器
  • 自动加载已保存的配置
  • 类型安全支持

二、安装和引入

文件位置

src/hooks/tableColumn.ts

引入方式

import { useTableColumnControl, type TableColumn } from '/@/hooks/tableColumn';

三、基础使用

1. 引入公共样式(推荐)

首先在 <style> 中引入现代化页面布局样式:

<style scoped lang="scss">
@import '/@/assets/styles/modern-page.scss';
</style>

2. 定义表格列配置

import { User, Calendar, Phone } from '@element-plus/icons-vue';

const tableColumns = [
	{ prop: 'name', label: '姓名', icon: User },
	{ prop: 'age', label: '年龄', icon: Calendar },
	{ prop: 'phone', label: '电话', icon: Phone },
];

3. 使用 Hook

const { visibleColumns, visibleColumnsSorted, checkColumnVisible, handleColumnChange, handleColumnOrderChange } = useTableColumnControl(tableColumns);

4. 在模板中使用

<template>
	<div class="modern-page-container">
		<div class="page-wrapper">
			<!-- 搜索表单卡片 -->
			<el-card v-show="showSearch" class="search-card" shadow="never">
				<template #header>
					<div class="card-header">
						<span class="card-title">
							<el-icon class="title-icon"><Search /></el-icon>
							筛选条件
						</span>
					</div>
				</template>
				<el-form class="search-form">
					<!-- 搜索表单项 -->
				</el-form>
			</el-card>

			<!-- 内容卡片 -->
			<el-card class="content-card" shadow="never">
				<template #header>
					<div class="card-header">
						<span class="card-title">
							<el-icon class="title-icon"><Document /></el-icon>
							数据列表
						</span>
						<div class="header-actions">
							<!-- TableColumnControl 组件 -->
							<TableColumnControl
								:columns="tableColumns"
								v-model="visibleColumns"
								@change="handleColumnChange"
								@order-change="handleColumnOrderChange"
							/>
						</div>
					</div>
				</template>

				<!-- 表格 -->
				<el-table :data="dataList" stripe class="modern-table">
					<el-table-column type="index" label="序号" width="70">
						<template #default="{ $index }">
							{{ $index + 1 + ((pagination?.current || 1) - 1) * (pagination?.size || 10) }}
						</template>
					</el-table-column>

					<!-- 动态渲染列 -->
					<template v-for="col in visibleColumnsSorted" :key="col.prop">
						<el-table-column
							v-if="checkColumnVisible(col.prop || '')"
							:prop="col.prop"
							:label="col.label"
							:width="col.width"
							:min-width="col.minWidth"
						>
							<template #header>
								<el-icon v-if="col.icon">
									<component :is="col.icon" />
								</el-icon>
								<span style="margin-left: 4px">{{ col.label }}</span>
							</template>
						</el-table-column>
					</template>
				</el-table>

				<!-- 分页 -->
				<div class="pagination-wrapper">
					<pagination v-bind="pagination" />
				</div>
			</el-card>
		</div>
	</div>
</template>

四、完整示例

<template>
	<div>
		<right-toolbar>
			<TableColumnControl
				ref="columnControlRef"
				:columns="tableColumns"
				v-model="visibleColumns"
				trigger-type="default"
				trigger-circle
				@change="handleColumnChange"
				@order-change="handleColumnOrderChange"
			>
				<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="state.dataList" stripe>
			<el-table-column type="index" label="序号" width="70">
				<template #default="{ $index }">
					{{ $index + 1 + ((state.pagination?.current || 1) - 1) * (state.pagination?.size || 10) }}
				</template>
			</el-table-column>

			<template v-for="col in visibleColumnsSorted" :key="col.prop">
				<el-table-column
					v-if="checkColumnVisible(col.prop || '')"
					:prop="col.prop"
					:label="col.label"
					:width="col.width"
					:min-width="col.minWidth"
					show-overflow-tooltip
				>
					<template #header>
						<el-icon v-if="col.icon">
							<component :is="col.icon" />
						</el-icon>
						<span style="margin-left: 4px">{{ col.label }}</span>
					</template>
				</el-table-column>
			</template>

			<el-table-column label="操作" width="180" fixed="right">
				<!-- 操作列内容 -->
			</el-table-column>
		</el-table>
	</div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { useTableColumnControl } from '/@/hooks/tableColumn';
import { User, Calendar, Phone, Menu } from '@element-plus/icons-vue';
import TableColumnControl from '/@/components/TableColumnControl/index.vue';

// 定义表格列
const tableColumns = [
	{ prop: 'name', label: '姓名', icon: User },
	{ prop: 'age', label: '年龄', icon: Calendar },
	{ prop: 'phone', label: '电话', icon: Phone },
];

// 使用 Hook
const { visibleColumns, visibleColumnsSorted, checkColumnVisible, handleColumnChange, handleColumnOrderChange } = useTableColumnControl(tableColumns);

const columnControlRef = ref();
const state = ref({
	dataList: [],
	pagination: { current: 1, size: 10 },
});
</script>

五、API 参考

TableColumn 类型定义

interface TableColumn {
	prop?: string; // 列属性名
	label?: string; // 列标题
	icon?: any; // 列图标组件
	width?: number | string; // 列宽度
	minWidth?: number | string; // 最小宽度
	showOverflowTooltip?: boolean; // 是否显示溢出提示
	align?: 'left' | 'center' | 'right'; // 对齐方式
	alwaysShow?: boolean; // 是否始终显示(不参与列控制)
	fixed?: boolean | 'left' | 'right'; // 是否固定列
	[key: string]: any; // 其他自定义属性
}

Hook 返回值

属性/方法 类型 说明
visibleColumns Ref<string[]> 当前可见的列 key 数组
columnOrder Ref<string[]> 列排序顺序数组
visibleColumnsSorted ComputedRef<TableColumn[]> 排序后的可见列配置数组
checkColumnVisible (prop: string) => boolean 检查列是否可见
handleColumnChange (value: string[]) => void 处理列显示变化
handleColumnOrderChange (order: string[]) => void 处理列排序变化
loadSavedConfig () => void 手动加载保存的配置

Hook 选项参数

interface Options {
	autoLoad?: boolean; // 是否自动加载配置,默认 true
	storageKey?: string; // 自定义存储 key默认使用路由路径
}

六、高级用法

1. 自定义存储 Key

如果不想使用默认的路由路径作为存储 key可以自定义

const { visibleColumns, visibleColumnsSorted, checkColumnVisible, handleColumnChange, handleColumnOrderChange } = useTableColumnControl(
	tableColumns,
	{
		storageKey: 'custom-table-columns-key',
	}
);

2. 禁用自动加载

如果需要在特定时机手动加载配置:

const { visibleColumns, visibleColumnsSorted, checkColumnVisible, handleColumnChange, handleColumnOrderChange, loadSavedConfig } =
	useTableColumnControl(tableColumns, {
		autoLoad: false,
	});

// 在需要的时候手动加载
onMounted(() => {
	loadSavedConfig();
});

3. 固定列和始终显示的列

某些列(如操作列)不应该参与列控制:

const tableColumns = [
	{ prop: 'name', label: '姓名', icon: User },
	{ prop: 'age', label: '年龄', icon: Calendar },
	{
		prop: 'action',
		label: '操作',
		alwaysShow: true, // 始终显示
		fixed: 'right', // 固定在右侧
	},
];

4. 特殊列模板

如果需要为某些列添加特殊模板:

<template v-for="col in visibleColumnsSorted" :key="col.prop">
	<el-table-column v-if="checkColumnVisible(col.prop || '')" :prop="col.prop" :label="col.label">
		<!-- 状态列特殊模板 -->
		<template v-if="col.prop === 'status'" #default="scope">
			<el-tag :type="scope.row.status === '1' ? 'success' : 'warning'">
				{{ scope.row.status }}
			</el-tag>
		</template>

		<!-- 其他列默认显示 -->
		<template v-else #default="scope">
			{{ scope.row[col.prop] }}
		</template>
	</el-table-column>
</template>

七、注意事项

1. 列 Key 的唯一性

确保每列的 proplabel 是唯一的,因为 Hook 使用它们作为标识:

// ✅ 正确
const tableColumns = [
	{ prop: 'name', label: '姓名' },
	{ prop: 'age', label: '年龄' },
];

// ❌ 错误:两个列都没有 prop
const tableColumns = [{ label: '姓名' }, { label: '年龄' }];

2. 响应式更新

tableColumns 应该是响应式的,如果需要在运行时动态修改列配置:

const tableColumns = ref([
	{ prop: 'name', label: '姓名' },
	{ prop: 'age', label: '年龄' },
]);

// 动态添加列
tableColumns.value.push({ prop: 'phone', label: '电话' });

3. 配置存储位置

  • 本地存储:使用 localStoragekey 为 user-table-configs-all
  • 后端存储:通过 API 同步key 为 user-table-configs
  • 页面存储:每个页面的配置通过路由路径生成唯一 key

4. 配置加载时机

Hook 默认在组件挂载时自动加载配置。如果需要在数据加载后重新加载:

const { loadSavedConfig } = useTableColumnControl(tableColumns);

watch(
	() => someData.value,
	() => {
		// 数据变化后重新加载配置
		loadSavedConfig();
	}
);

八、扩展方式

1. 添加列宽保存功能

如果需要保存列宽,可以扩展 Hook

// 扩展 Hook
function useTableColumnControlWithWidth(tableColumns: TableColumn[]) {
	const baseHook = useTableColumnControl(tableColumns);
	const columnWidths = ref<Record<string, number>>({});

	const handleColumnWidthChange = (prop: string, width: number) => {
		columnWidths.value[prop] = width;
		// 保存到本地存储
		const storageKey = getStorageKey();
		const config = getTableConfigFromLocal(storageKey) || {};
		config.columnWidths = columnWidths.value;
		saveTableConfigToLocal(storageKey, config);
	};

	return {
		...baseHook,
		columnWidths,
		handleColumnWidthChange,
	};
}

2. 添加列固定功能

如果需要保存列的固定状态:

function useTableColumnControlWithFixed(tableColumns: TableColumn[]) {
	const baseHook = useTableColumnControl(tableColumns);
	const fixedColumns = ref<string[]>([]);

	const toggleColumnFixed = (prop: string) => {
		const index = fixedColumns.value.indexOf(prop);
		if (index > -1) {
			fixedColumns.value.splice(index, 1);
		} else {
			fixedColumns.value.push(prop);
		}
		// 保存配置
	};

	return {
		...baseHook,
		fixedColumns,
		toggleColumnFixed,
	};
}

3. 添加列分组功能

如果需要按分组管理列:

function useTableColumnControlWithGroup(tableColumns: TableColumn[], groups: Record<string, string[]>) {
	const baseHook = useTableColumnControl(tableColumns);

	const getColumnsByGroup = (groupName: string) => {
		return tableColumns.filter((col) => groups[groupName]?.includes(col.prop || ''));
	};

	return {
		...baseHook,
		getColumnsByGroup,
	};
}

4. 自定义存储策略

如果需要使用不同的存储策略(如 IndexedDB

function useTableColumnControlWithCustomStorage(
	tableColumns: TableColumn[],
	storageAdapter: {
		get: (key: string) => Promise<any>;
		set: (key: string, value: any) => Promise<void>;
	}
) {
	// 实现自定义存储逻辑
}

九、常见问题

Q1: 为什么配置没有保存?

A: 检查以下几点:

  1. 确保 handleColumnChangehandleColumnOrderChange 已正确绑定到组件
  2. 检查浏览器控制台是否有错误
  3. 确认 API 调用是否成功(检查网络请求)

Q2: 为什么某些列无法隐藏?

A: 检查列配置中是否设置了 alwaysShow: truefixed 属性:

// 这些列无法隐藏
{ prop: 'action', label: '操作', alwaysShow: true }
{ prop: 'id', label: 'ID', fixed: 'left' }

Q3: 如何重置列配置?

A: 可以手动清除本地存储或调用 API 删除配置:

// 清除本地存储
localStorage.removeItem('user-table-configs-all');

// 或通过 API 删除
import { deleteUserTableConfig } from '/@/api/admin/usertable';
deleteUserTableConfig(storageKey);

Q4: 如何在多个页面共享列配置?

A: 使用相同的 storageKey

const { visibleColumns } = useTableColumnControl(tableColumns, {
	storageKey: 'shared-table-columns',
});

十、最佳实践

1. 列配置管理

将列配置提取到单独的文件中,便于维护:

// src/views/xxx/columns.ts
import { User, Calendar } from '@element-plus/icons-vue';

export const tableColumns = [
	{ prop: 'name', label: '姓名', icon: User },
	{ prop: 'age', label: '年龄', icon: Calendar },
];

2. 类型定义

为列配置添加类型约束:

import type { TableColumn } from '/@/hooks/tableColumn';

const tableColumns: TableColumn[] = [{ prop: 'name', label: '姓名', icon: User }];

3. 统一命名

保持列 prop 与后端字段名一致,便于数据处理。

4. 性能优化

对于大量列的表格,考虑使用虚拟滚动或分页加载列配置。


十一、迁移指南

从旧实现迁移到 Hook

旧代码(~90 行):

const visibleColumns = ref<string[]>([]);
const columnOrder = ref<string[]>([]);
const loadSavedConfig = () => {
	/* ... */
};
const handleColumnChange = () => {
	/* ... */
};
// ... 更多代码

新代码(~5 行):

import { useTableColumnControl } from '/@/hooks/tableColumn';

const { visibleColumns, visibleColumnsSorted, checkColumnVisible, handleColumnChange, handleColumnOrderChange } = useTableColumnControl(tableColumns);

迁移步骤

  1. 引入 Hook
  2. 删除旧的列控制逻辑(loadSavedConfighandleColumnChange 等)
  3. 使用 Hook 返回值替换原有变量
  4. 测试功能是否正常

十二、更新日志

v1.0.0 (2024-01-XX)

  • 初始版本发布
  • 支持列显示/隐藏控制
  • 支持列排序管理
  • 支持本地存储和服务器同步
  • 支持自动加载配置

十三、相关资源

  • Hook 源码src/hooks/tableColumn.ts
  • API 接口src/api/admin/usertable.ts
  • 组件源码src/components/TableColumnControl/index.vue
  • 示例页面src/views/stuwork/classmasterresume/index.vue

十四、贡献指南

如果发现 bug 或有改进建议,请:

  1. 提交 Issue 描述问题
  2. 提交 Pull Request 提供修复方案
  3. 更新本文档说明变更

最后更新2024-01-XX