业务需求:
1.表头自定义
2.表格伸缩列自定义
3.字段长度自适应伸缩列,超过宽度设置自动省略,popover气泡卡牌显示全部字段
4.表格排序自定义
5.可展开数据通过操作栏展开
自定义表头:
<template>
<a-modal
:visible="modelValue"
title="自定义表头"
:destroyOnClose="true"
:keyboard="false"
:maskClosable="false"
width="800px"
@ok="handleOk"
@cancel="handleCancel"
>
<div class="column-selector-grid">
<div class="selector-actions">
<a-button @click="resetToDefault">恢复默认</a-button>
<a-button @click="selectAll">全选</a-button>
<a-button @click="clearAll">清空</a-button>
</div>
<div class="grid-container">
<draggable
v-model="internalColumns"
group="columns"
item-key="dataIndex"
class="grid-layout"
:disabled="props.disabled"
@start="onDragStart"
@end="onDragEnd"
:move="onMove">
<template #item="{ element }">
<div
class="grid-item"
:class="{
'required-item': element.required,
'selected-item': element.visible,}">
<div class="drag-handle" >
<drag-outlined />
</div>
<div class="item-content">
<a-checkbox
v-model:checked="element.visible"
:disabled="element.required "
class="content-checkbox"
>
<span class="content-title">
{{ element.title }}
<a-tooltip v-if="element.tip" :title="element.tip">
<question-circle-outlined class="tip-icon" />
</a-tooltip>
</span>
</a-checkbox>
</div>
</div>
</template>
</draggable>
</div>
<div class="fixed-columns-info">
<p class="info-text">
<info-circle-outlined class="info-icon" />
<strong>固定表头申明:</strong>
<span class="fixed-first-text">序号</span> 固定显示在第一位,
<span class="fixed-last-text">操作</span> 固定显示在最后一位,以及其他所有被固定的表头都不能取消选择或调整位置。
</p>
</div>
</div>
</a-modal>
</template>
<script setup>
import { ref, watch, nextTick } from 'vue';
import { QuestionCircleOutlined, DragOutlined, InfoCircleOutlined } from '@ant-design/icons-vue';
import draggable from 'vuedraggable';
// 固定列配置
const FIXED_COLUMNS = {
FIRST: 'no', // 序号列固定在第一位
LAST: 'action' // 操作列固定在最后一位
};
const props = defineProps({
allColumns: Array, // 所有可用列,按照 tableColumns 顺序
defaultColumns: Array, // 当前选中的列
modelValue: Boolean, // 控制弹窗显示
disabled:{
type:Boolean,
default:false
}
});
const emit = defineEmits(['update:modelValue', 'confirm','closeSelector']);
// 内部维护的列数据
const internalColumns = ref([]);
// 拖拽状态
const isDragging = ref(false);
const draggedElement = ref(null);
// 拖拽开始事件
const onDragStart = (evt) => {
const draggedItem = evt.item;
const draggedIndex = evt.oldIndex;
const draggedData = internalColumns.value[draggedIndex];
// 检查是否是固定列
if (draggedData.required ) {
// 阻止拖拽固定列
evt.preventDefault();
return;
}
isDragging.value = true;
draggedElement.value = draggedData;
};
// 拖拽结束事件
const onDragEnd = (evt) => {
isDragging.value = false;
draggedElement.value = null;
// 延迟执行位置恢复,确保拖拽完全结束
nextTick(() => {
ensureFixedColumnsPosition();
});
};
// 拖拽移动事件
const onMove = (evt) => {
const draggedItem = evt?.dragged.__draggable_context?.element;
const targetIndex = evt.draggedContext.futureIndex;
// 检查被拖动列是否是固定列
if (draggedItem?.required ) {
// 阻止拖拽固定列
return false;
}
// 检查目标是否是固定列
const targetItem = internalColumns.value[targetIndex];
if (targetItem && targetItem.required) {
// 阻止拖拽到固定列
return false;
}
// 智能位置调整:确保拖拽后固定列位置仍然正确
const adjustedTargetIndex = adjustTargetIndex(targetIndex);
if (adjustedTargetIndex !== targetIndex) {
// 如果目标位置需要调整,更新拖拽目标
evt.draggedContext.futureIndex = adjustedTargetIndex;
}
// 允许正常列在固定列之间自由拖拽
return true;
};
// 智能调整目标位置,确保不破坏序号和操作的位置
const adjustTargetIndex = (targetIndex) => {
const columns = internalColumns.value;
// 找到序号和操作的位置
const noIndex = columns.findIndex(col => col.dataIndex === FIXED_COLUMNS.FIRST);
const actionIndex = columns.findIndex(col => col.dataIndex === FIXED_COLUMNS.LAST);
// 如果目标位置是序号列位置,调整到序号列后面
if (targetIndex === 0) {
return 1;
}
// 如果目标位置是操作列位置,调整到操作列前面
if (targetIndex === columns.length - 1) {
return columns.length - 2;
}
// 如果目标位置在序号列之前,调整到序号列后面
if (targetIndex < noIndex) {
return noIndex + 1;
}
// 如果目标位置在操作列之后,调整到操作列前面
if (targetIndex > actionIndex) {
return actionIndex - 1;
}
return targetIndex;
};
// 确保固定列在正确位置
const ensureFixedColumnsPosition = () => {
const columns = [...internalColumns.value];
let hasChanges = false;
// 确保序号列在第一位
const noColumn = columns.find(col => col.dataIndex === FIXED_COLUMNS.FIRST);
if (noColumn) {
const noIndex = columns.indexOf(noColumn);
if (noIndex !== 0) {
columns.splice(noIndex, 1);
columns.unshift(noColumn);
hasChanges = true;
}
}
// 确保操作列在最后一位
const actionColumn = columns.find(col => col.dataIndex === FIXED_COLUMNS.LAST);
if (actionColumn) {
const actionIndex = columns.indexOf(actionColumn);
if (actionIndex !== columns.length - 1) {
columns.splice(actionIndex, 1);
columns.push(actionColumn);
hasChanges = true;
}
}
// 如果有变化,更新列顺序
if (hasChanges) {
internalColumns.value = [...columns];
}
};
// 初始化列数据,保持 tableColumns 的顺序
const initColumns = () => {
// 按照 allColumns 的顺序初始化
internalColumns.value = props.allColumns.map(col => ({
...col,
visible: props.defaultColumns.includes(col.dataIndex) || col.required
}));
// 确保固定列在正确位置
ensureFixedColumnsPosition();
};
// 恢复默认
const resetToDefault = () => {
initColumns();
};
// 全选
const selectAll = () => {
internalColumns.value.forEach(col => {
col.visible = true;
});
};
// 清空(除了必需列和固定列)
const clearAll = () => {
internalColumns.value.forEach(col => {
if (!col.required ) {
col.visible = false;
}
});
};
// 确认保存
const handleOk = () => {
// 按照当前顺序返回可见列的 dataIndex 数组
const visibleColumns = internalColumns.value
.filter(col => col.visible)
.map(col => col.dataIndex);
emit('confirm', visibleColumns);
emit('update:modelValue', false);
};
const handleCancel = () => {
if(props.disabled){
internalColumns.value.forEach(col => {
col.visible = props.defaultColumns.includes(col.dataIndex) || col.required ;
});
}
emit('update:modelValue', false);
emit('closeSelector');
};
// 监听 props 变化重新初始化
watch(() => props.allColumns, initColumns, { immediate: true });
watch(() => props.defaultColumns, () => {
internalColumns.value.forEach(col => {
col.visible = props.defaultColumns.includes(col.dataIndex) || col.required ;
});
});
// 监听内部列顺序变化,确保固定列位置正确
watch(() => internalColumns.value, (newColumns) => {
if (newColumns && newColumns.length > 0) {
// 检查固定列位置
const noIndex = newColumns.findIndex(col => col.dataIndex === FIXED_COLUMNS.FIRST);
const actionIndex = newColumns.findIndex(col => col.dataIndex === FIXED_COLUMNS.LAST);
// 如果固定列位置不正确,立即恢复
if (noIndex !== 0 || actionIndex !== newColumns.length - 1) {
nextTick(() => {
ensureFixedColumnsPosition();
});
}
}
}, { deep: true });
</script>
<style scoped>
.column-selector-grid {
padding: 12px;
}
.selector-actions {
margin-bottom: 16px;
display: flex;
gap: 8px;
}
.grid-container {
border: 1px solid #f0f0f0;
border-radius: 4px;
padding: 12px;
background: #fafafa;
margin-bottom: 16px;
}
.grid-layout {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 12px;
}
.grid-item {
position: relative;
height: 80px;
border: 1px solid #d9d9d9;
border-radius: 4px;
background: #fff;
cursor: move;
transition: all 0.2s;
padding: 8px;
overflow: hidden;
}
.grid-item:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.grid-item.required-item {
background-color: #f6ffed;
}
.grid-item.selected-item {
background-color: #e6f7ff;
}
.drag-handle {
position: absolute;
top: 4px;
right: 4px;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
background: #f0f0f0;
border-radius: 2px;
color: #666;
cursor: move;
z-index: 1;
}
.grid-item.required-item .drag-handle {
background: #d9f7be;
}
.item-content {
height: 100%;
display: flex;
align-items: center;
}
.content-checkbox {
width: 100%;
display: flex;
align-items: center;
margin: 0;
padding-right: 20px;
}
.content-checkbox >>> .ant-checkbox {
flex-shrink: 0;
}
.content-checkbox >>> .ant-checkbox + span {
display: flex;
align-items: center;
flex: 1;
padding-right: 4px;
white-space: normal;
}
.content-title {
display: inline-flex;
align-items: center;
font-size: 12px;
line-height: 1.4;
word-break: break-word;
text-align: left;
}
.tip-icon {
margin-left: 4px;
color: #999;
font-size: 12px;
}
.fixed-columns-info {
background-color: #f6ffed;
border: 1px solid #b7eb8f;
border-radius: 4px;
padding: 12px;
}
.info-text {
margin: 0;
font-size: 12px;
color: #666;
line-height: 1.5;
}
.info-icon {
margin-right: 6px;
color: #52c41a;
}
.fixed-first-text {
color: #52c41a;
font-weight: bold;
}
.fixed-last-text {
color: #1890ff;
font-weight: bold;
}
</style>
表格封装:
<template>
<div class="list-page">
<div>
<slot name="header"/>
</div>
<div>
<a-table
bordered
row-key="id"
size="small"
ref="tableRef"
:loading="loading"
:columns="currentTableColumns"
:data-source="dataSource"
:expand-row-by-click="false"
:row-selection="rowSelection"
:expandIconColumnIndex="-1"
@expand="handleExpand"
:expanded-row-keys="expandedRowKeys"
@resizeColumn="handleResizeColumn"
:scroll="{ x: '120%', y: 'calc(100vh - 375px)' }"
:pagination="false">
<!-- 动态插槽处理 -->
<template v-for="slotName in Object.keys($slots)" #[slotName]="scope" :key="slotName">
<slot :name="slotName" v-bind="scope" />
</template>
<!--通用bodyCell插槽封装-->
<template #bodyCell="{text,record,column,index}">
<!-- 先检查是否有自定义的bodyCell插槽-->
<template v-if="$slots.bodyCell">
<slot name="bodyCell" :text="text" :record="record" :column="column" :index="index"></slot>
</template>
<!-- 无自定义的bodyCell插槽时的默认处理逻辑-->
<template v-else>
{{ formatCellText(text,record,column) }}
</template>
</template>
<!-- 通用操作列 -->
<template #action="{ record, index }">
<slot name="action" :record="record" :index="index">
<!-- 默认操作列 -->
</slot>
</template>
<!-- 扩展行插槽 -->
<template #expandedRowRender="{ record, index }">
<slot name="expandedRowRender" :record="record" :index="index" />
</template>
</a-table>
<div class="pagination-wrapper">
<a-pagination
show-size-changer
size="small"
v-model:current="pagination.page"
v-model:page-size="pagination.pageSize"
:show-total="(total) => `共 ${total} 条`"
:total="pagination.total"
:pageSizeOptions="['100', '200', '300', '500']"
@change="onChangePage"
>
</a-pagination>
</div>
</div>
<!-- 自定义表头模态框-->
<ColumnSelector
:visible="showColumnSelector"
:all-columns="enhancedColumns"
:default-columns="selectedColumns"
@confirm="handleColumnConfirm"
@closeSelector="closeSelector"
/>
</div>
</template>
<script setup>
import {computed, h, nextTick, onMounted, onUnmounted, onUpdated, reactive, ref} from "vue";
import {settingStore} from "../../store/index.js";
import {storeToRefs} from "pinia";
import ColumnSelector from "../../components/ColumnsSelector/index.vue";
import {debounce, deepClone} from "../../utils/util.js";
const settingStoreRef = settingStore();
const { tableOrderByName, tableAscOrDescOrNomal } = storeToRefs(settingStoreRef)
import customizeHeaderApi from '../../api/CustomizeHeader/index.js'
import {message} from "ant-design-vue";
import {useRoute} from "vue-router";
let tableRef = ref(null)
const props = defineProps({
//表格加载状态
loading:Boolean,
showColumnSelector:Boolean,
rowSelection: {
type: Object
},
pagination:{
type: Object,
default: { },
},
//需要排序的表格标题
sortTableList:{
type: Array,
default: [],
},
//补充必需的列
requiredColumns:{
type: Array,
default: [],
},
expandedRowKeys:{
type: Array,
default: [],
},
columns:Array,
dataSource:{
type: Array,
default: [],
},
})
const route = useRoute();
const emit = defineEmits(['onChangePage','closeSelector','resetPage','columnResized'])
//自定义表头拉伸
const handleResizeColumn=(w, col)=>{
col.width = w;
// 强制表格重新渲染
nextTick(() => {
tableRef.value?.$forceUpdate?.();
})
debounceResizeColumn({
dataIndex: col.dataIndex,
width: w,
routePath: route.path
})
}
const debounceResizeColumn = debounce((payload)=>{
emit('columnResized',payload)
checkAllCellsOverflow();
},500)
let DEFAULT_VISIBLE_COLUMNS = props.columns.map((item)=>{
return item.dataIndex
})
const handleExpand = (expanded, record) => {
emit('handleExpand',expanded, record)
};
const onChangePage=(data)=>{
emit('onChangePage', data)
}
const closeSelector=()=>{
emit('closeSelector')
}
//排序
// 当前排序状态
const currentSorter = ref({
field: tableOrderByName.value,
order: tableAscOrDescOrNomal.value ?
(tableAscOrDescOrNomal.value === '0' ? 'ascend' : 'descend') :
null
});
// 生成带有自定义排序图标的列配置
const currentSortTableColumns = computed(() => {
return props.columns.map(col => {
if (props.sortTableList.includes(col.dataIndex)) {
const isCurrentSorted = currentSorter.value.field === col.dataIndex;
const sortUpActive = isCurrentSorted && currentSorter.value.order === 'ascend';
const sortDownActive = isCurrentSorted && currentSorter.value.order === 'descend';
return {
...col,
// 禁用 Ant Design 的默认排序图标
sorter: false,
// 使用自定义标题渲染
title: () => h('div', {
class: 'sortable-header',
onClick: (event) => {
handleHeaderClick(col.dataIndex, event);
}
}, [
h('span', {
class: 'header-title',
}, col.title),
h('div', {
class: 'sort-icons',
style: 'display: flex; flex-direction: column; margin-left: 4px; line-height: 1;',
onClick: (event) => {
// 阻止事件冒泡,排序图标有自己的点击事件
event.stopPropagation();
}
}, [
// 升序三角形图标(正三角)
h('div', {
class: `sort-icon sort-up ${sortUpActive ? 'active' : ''}`,
onClick: () => handleSortClick(col.dataIndex, 'ascend')
}),
// 降序三角形图标(倒三角)
h('div', {
class: `sort-icon sort-down ${sortDownActive ? 'active' : ''}`,
onClick: () => handleSortClick(col.dataIndex, 'descend')
})
])
])
};
}
return col;
});
});
// 处理排序图标点击
const handleSortClick = (field, order) => {
// 如果点击的是当前排序字段且当前已经是该排序方式,则取消排序
if (currentSorter.value.field === field && currentSorter.value.order === order) {
settingStoreRef.$patch({
tableOrderByName: undefined,
tableAscOrDescOrNomal: undefined
});
currentSorter.value = {
field: undefined,
order: null
};
} else {
// 设置新的排序
const sortOrder = order === 'ascend' ? '0' : '1';
settingStoreRef.$patch({
tableOrderByName: field,
tableAscOrDescOrNomal: sortOrder
});
currentSorter.value = {
field: field,
order: order
};
}
// 重置页码并重新加载数据
emit('resetPage')
};
// 处理列标题点击(点击标题其他位置)
// 处理表头点击
const handleHeaderClick = (field, event) => {
const target = event.target;
// 判断是否点击了排序图标区域
const isSortIcon = target.closest('.sort-icons') ||
target.classList.contains('sort-icon') ||
target.classList.contains('sort-up') ||
target.classList.contains('sort-down') ||
target.tagName === 'DIV' && (
target.style.borderLeft.includes('transparent') ||
target.style.borderRight.includes('transparent')
);
// 如果点击的是排序图标区域,不处理(由 handleSortClick 处理)
if (isSortIcon) {
return;
}
// 获取当前列的排序状态
const isCurrentColumn = currentSorter.value.field === field;
const isAscending = currentSorter.value.order === 'ascend';
const isDescending = currentSorter.value.order === 'descend';
if (isCurrentColumn) {
// 如果当前列已经是排序状态,点击表头其他区域取消排序
if (isAscending || isDescending) {
settingStoreRef.$patch({
tableOrderByName: undefined,
tableAscOrDescOrNomal: undefined
});
currentSorter.value = {
field: undefined,
order: null
};
emit('resetPage')
}
} else {
// 如果当前列没有排序,点击表头设置为默认升序
settingStoreRef.$patch({
tableOrderByName: field,
tableAscOrDescOrNomal: '0' // 0 表示升序
});
currentSorter.value = {
field: field,
order: 'ascend'
};
emit('resetPage')
}
};
function initTableOrder(){
settingStoreRef.$patch({
tableOrderByName:undefined,
tableAscOrDescOrNomal:undefined
})
currentSorter.value = {
field: undefined,
order: null
};
}
//自定义表头 start
const visibleColumns = ref([...DEFAULT_VISIBLE_COLUMNS]);
let allDefaultColumns = ref([])
// 获取实际显示的表格列
const currentTableColumns = computed(() => {
const allColumns = currentSortTableColumns.value;
// 过滤出需要显示的列,并保持顺序
return visibleColumns.value
.map(dataIndex => allColumns.find(col => col.dataIndex === dataIndex))
.filter(Boolean);
});
// 当前选中的列
const selectedColumns = ref([...DEFAULT_VISIBLE_COLUMNS]);
const selectorColumns = computed(() => {
// 过滤出需要显示的列,并保持顺序
return allDefaultColumns.value
.map(dataIndex => props.columns.find(col => col.dataIndex === dataIndex))
.filter(Boolean);
});
function getColumnTip(dataIndex) {
let tips = {};
props.columns.forEach((item)=>{
tips[item.dataIndex] = item.title;
})
return tips[dataIndex];
}
// 增强列信息
const enhancedColumns = computed(() => {
let columns = selectorColumns.value;
// 确保顺序与 tableColumns 一致
return columns.map(col => ({
...col,
tip: getColumnTip(col.dataIndex),
required: props.requiredColumns.includes(col.dataIndex)
}));
});
// 保存列设置
async function handleColumnConfirm(columns) {
try {
// 更新visibleColumns和selectedColumns
visibleColumns.value = columns;
selectedColumns.value = columns;
// 接口保存
await customizeHeaderApi.update({
pageRoutePath: route.path,
tableColumsNameLst: columns
})
emit('closeSelector');
message.success('表头设置已保存');
} catch (e) {
console.error('保存列设置失败', e);
message.error('保存表头设置失败');
}
}
// 加载保存的设置
async function loadColumnPreference() {
try {
//获取已保存的设置
const res = await customizeHeaderApi.get({pageRoutePath:route?.path});
const saved = res.data?.tableColumsNameLst;
// 验证并处理保存的列设置
if (saved && saved.length > 0) {
// 1. 过滤掉不存在的列
const validColumns = saved.filter(dataIndex =>
props.columns.some(col => col.dataIndex === dataIndex)
);
// 2. 补充必需的列
props.requiredColumns.forEach(col => {
if (!validColumns.includes(col)) {
validColumns.unshift(col); // 必需列放在前面
}
});
visibleColumns.value = validColumns;
//必须列
let requiredColumns = deepClone(props.columns);
requiredColumns = requiredColumns.filter((col)=>props.requiredColumns.includes(col.dataIndex));
//已保存的必须列
let savedNonRequiredColumns = saved.filter(dataIndex => !props.requiredColumns.includes(dataIndex)).map(dataIndex=>
props.columns.find(col=> col.dataIndex == dataIndex)).filter(Boolean);
//未保存的列
let unsavedCols = props.columns.filter(col=>!props.requiredColumns.includes(col.dataIndex) && !saved.includes(col.dataIndex));
const orderColumns = [...requiredColumns,...savedNonRequiredColumns,...unsavedCols];
allDefaultColumns.value = orderColumns.map(col=>col.dataIndex);
} else {
// 使用默认值
visibleColumns.value = [...DEFAULT_VISIBLE_COLUMNS];
allDefaultColumns.value = [...DEFAULT_VISIBLE_COLUMNS];
}
// 更新selectedColumns(用于ColumnSelector)
selectedColumns.value = [...visibleColumns.value];
} catch (e) {
console.error('加载列设置失败', e);
visibleColumns.value = [...DEFAULT_VISIBLE_COLUMNS];
selectedColumns.value = [...DEFAULT_VISIBLE_COLUMNS];
}
}
//自定义表头 end
// 格式化单元格文本
const formatCellText = (text, record, column) => {
if (typeof text === 'object') {
return JSON.stringify(text)
}
return text || ''
}
// 检查文本是否溢出
const overflowMap = reactive(new Map());
const isOverflow = (columnKey, rowKey) => {
return overflowMap.get(`${rowKey}-${columnKey}`) || false;
};
const checkAllCellsOverflow =()=>{
nextTick(()=>{
const cells = document.querySelectorAll('.table-cell');
cells.forEach((cell)=>{
const columnKey = cell.getAttribute('data-column-key');
const rowKye = cell.getAttribute('data-row-key');
const key = `${rowKye}-${columnKey}`;
const isOverFlow = cell.scrollWidth > cell.clientWidth;
overflowMap.set(key,isOverFlow);
})
})
}
onMounted(()=>{
initTableOrder();
loadColumnPreference();
// 监听窗口变化
window.addEventListener('resize', checkAllCellsOverflow);
})
onUpdated(()=>{
checkAllCellsOverflow();
})
// 清理事件监听
onUnmounted(() => {
window.removeEventListener('resize', checkAllCellsOverflow);
});
// 暴露方法给父组件
defineExpose({
initTableOrder,
isOverflow,
});
</script>
<style scoped>
.pagination-wrapper {
text-align: right;
margin-top: 10px;
}
</style>
/** 防抖函数,规定时间内连续出发的函数只执行最后一次 */
function debounce(func, wait, immediate) {
let timeout; // 定义一个计时器变量,用于延迟执行函数
return function (...args) { // 返回一个包装后的函数
const context = this; // 保存函数执行上下文对象
const later = function () { // 定义延迟执行的函数
timeout = null; // 清空计时器变量
if (!immediate) func.apply(context, args); // 若非立即执行,则调用待防抖函数
};
const callNow = immediate && !timeout; // 是否立即调用函数的条件
clearTimeout(timeout); // 清空计时器
timeout = setTimeout(later, wait); // 创建新的计时器,延迟执行函数
if (callNow) func.apply(context, args); // 如果满足立即调用条件,则立即执行函数
};
}
import { defineStore } from "pinia";
export const columWidthStore = defineStore('columnWidthStore', {
state: () => ({
columnWidthList: [],
}),
actions: {
setColumnList(record){
const { routePath,dataIndex,width } = record;
let routeConfig = this.columnWidthList.find(item=>item.routePath == routePath);
if(routeConfig){
const columnConfig = routeConfig?.columnList.find(item=>item.dataIndex == dataIndex);
if(columnConfig){
columnConfig.width = width;
}else{
routeConfig.columnList.push({dataIndex,width})
}
}else{
this.columnWidthList.push({
routePath,columnList:[{dataIndex,width}]
})
}
},
getRouteColumnWidths(routePath) {
const routeConfig = this.columnWidthList.find(item=>item.routePath == routePath);
if(!routeConfig)return [];
return routeConfig.columnList;
},
deleteRouteConfig(routePath) {
let index = this.columnWidthList.findIndex((item)=>{return item.routePath == routePath});
this.columnWidthList.splice(index,1);
},
deleteAllRoute(){
this.columnWidthList = []
}
},
getters: {
},
persist: {
enabled: true,
// 自定义持久化参数
strategies: [
{
// 自定义key,默认就是仓库的key
key: "columWidthStore",
// 自定义存储方式,默认sessionStorage
storage: localStorage,
// 指定要持久化的数据,默认所有 state 都会进行缓存,可以通过 paths 指定要持久化的字段,其他的则不会进行持久化。
paths: [
"columnWidthList",
],
}
],
},
})
import { defineStore } from "pinia";
import { deepClone } from "../../utils/util.js";
const customTitlesStr = sessionStorage.getItem(import.meta.env.VITE_APP_TBAS_TITLES_KEY)
const customTitles = (customTitlesStr && JSON.parse(customTitlesStr)) || []
export const settingStore = defineStore('settingStore', {
state: () => ({
isMobile: false,
visible: false,
pageMinHeight: 0,
tableOrderByName:undefined,
tableAscOrDescOrNomal:undefined,
menuData: [],
menuList: {},
activeValue: '',
activatedFirst: undefined,
customTitles,
}),
actions: {
reset() {
this.isMobile = false;
this.tableOrderByName = undefined;
this.tableAscOrDescOrNomal = undefined;
this.visible = false;
this.pageMinHeight = 0;
this.menuData = [];
this.menuList = {};
this.activeValue = '';
this.activatedFirst = undefined;
this.customTitles = customTitles;
},
setDevice(state, isMobile) {
state.isMobile = isMobile
},
setVisible(state, visible) {
state.visible = visible
},
setMenuValue(value) {
this.menuData.push(value)
},
},
getters: {
},
persist: {
enabled: true,
// 自定义持久化参数
strategies: [
{
// 自定义key,默认就是仓库的key
key: "settingStore",
// 自定义存储方式,默认sessionStorage
storage: localStorage,
// 指定要持久化的数据,默认所有 state 都会进行缓存,可以通过 paths 指定要持久化的字段,其他的则不会进行持久化。
paths: [
"menuData",
"activeValue",
"menuList",
"tableOrderByName",
"tableAscOrDescOrNomal"
],
}
],
},
})
组件使用:
<template>
<div>
<customizeTable
ref="customizeTableRef"
:loading="loading"
:columns="tableColumns"
:pagination="pagination"
:data-source="dataSource"
:requiredColumns="requiredColumns"
:sortTableList="sortTableList"
:showColumnSelector="showColumnSelector"
:expandedRowKeys="expandedRowKeys"
@onChangePage="onChangePage"
@closeSelector="closeSelector"
@expand="handleExpand"
@resetPage="resetPage"
@columnResized="columnResized"
>
<template #header>
<div style="display: flex;margin-bottom: 10px">
<span>测试:</span>
<a-input size="small" style="width:120px;margin-right: 4px" v-model:value="testValue" ></a-input>
<a-button size="small" @click="testValue = undefined">清空</a-button>
</div>
</template>
<template #bodyCell="{ text, record, column }">
<template v-if="overFlowColumns.includes(column.dataIndex)">
<div class="table-cell"
:data-row-key="record.key"
:data-column-key="column.dataIndex"
@dblclick="handleCellDoubleClick(record, column)">
<a-popover placement="leftBottom" v-if="getCellOverflow(column.dataIndex, record.key)" :content="record[column.dataIndex]">
<span> {{ text }}</span>
</a-popover>
<div v-else>
{{ text }}
</div>
</div>
</template>
<template v-if="column.dataIndex === 'action'">
<a-dropdown placement="bottom" >
<a class="ant-dropdown-link" @click.prevent>
操作
</a>
<template #overlay>
<a-menu class="menu-active-background-color">
<a-menu-item >
<a-button
size="small"
@click="toggleExpand(record.id)"
style="padding: 0 4px;"
>
{{ expandedRowKeys.includes(record.id) ? '收起' : '详情' }}
</a-button>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</template>
</template>
<template #expandedRowRender="{ record }">
<div class="expanded-style">
<a-descriptions class="mt-10" title="详细信息" bordered :column="2">
<a-descriptions-item :contentStyle="contentColor" label="1">{{record.one}}</a-descriptions-item>
<a-descriptions-item :contentStyle="contentColor" label="2">{{record.two}}</a-descriptions-item>
<a-descriptions-item :contentStyle="contentColor" label="3">{{record.three}}</a-descriptions-item>
<a-descriptions-item :contentStyle="contentColor" label="4">{{record.four}}</a-descriptions-item>
<a-descriptions-item :contentStyle="contentColor" label="5">{{record.five}}</a-descriptions-item>
<a-descriptions-item :contentStyle="contentColor" label="6" >{{record.six}}</a-descriptions-item>
</a-descriptions>
</div>
</template>
</customizeTable>
</div>
</template>
<script setup>
import { onMounted, ref} from "vue";
import {columWidthStore} from "../src/store/index.js";
import customizeTable from './components/CustomizeTable/index.vue'
import {useRoute} from "vue-router";
import {copyToClipboard} from './utils/util.js'
let route = useRoute();
let customizeTableRef = ref(null)
let loading = ref(false)
let testValue = ref('')
let contentColor = ref({
backgroundColor:'#fff'
});
let tableColumns = ref([
{
title: "序号",
dataIndex: "no",
width: 46,
fixed:'left',
customRender: ({index}) => `${index + 1}`,
},
{
title: "1",
dataIndex: "one",
width: 80,
key:'one',
resizable: true,
ellipsis:true,
},
{
title: "2",
dataIndex: "two",
width: 80,
key:'one',
resizable: true,
ellipsis:true,
},
{
title: "3",
dataIndex: "three",
width: 80,
key:'one',
resizable: true,
ellipsis:true,
},
{
title: "4",
dataIndex: "four",
width: 80,
key:'one',
resizable: true,
ellipsis:true,
},
{
title: "5",
dataIndex: "five",
width: 80,
key:'one',
resizable: true,
ellipsis:true,
},
{
title: "6",
dataIndex: "six",
width: 80,
key:'one',
resizable: true,
ellipsis:true,
},
{
title: "操作",
dataIndex: "action",
fixed:'right',
key:'action',
width: 50,
},
])
let overFlowColumns = tableColumns.value.filter((item)=>!['no','action'].includes(item.dataIndex)).map((item)=>item.dataIndex);
let dataSource = ref([
{one:'111',two:'2222',three:'333',four:'444444444444444444444444444444444444444',five:'555',six:'666',id:1,key:1111}
])
const pagination = ref({
page: 1,
pageSize: 10,
total: 0,
size: "default",
});
let requiredColumns = ref(['no','one','two'])
let sortTableList = ref(['one','two','three'])
let showColumnSelector = ref(false)
function handleCellDoubleClick(record, column) {
const text = record[column.dataIndex];
copyToClipboard(text);
}
const onChangePage = (data) => {
pagination.value.page = data;
// getList();
};
const resetPage=()=>{
pagination.value.page = 1;
// getList();
}
const closeSelector=()=>{
showColumnSelector.value = false;
}
/* 表格详情展开关闭 start */
// 添加展开行的状态管理
let expandedRowKeys = ref([]);
// 切换展开状态的方法
const toggleExpand = (id) => {
const index = expandedRowKeys.value.indexOf(id);
if (index > -1) {
// 如果已经展开,则关闭
expandedRowKeys.value.splice(index, 1);
} else {
// 如果未展开,则展开并关闭其他已展开的行
expandedRowKeys.value = [id];
}
};
// 处理表格展开事件
const handleExpand = (expanded, record) => {
if (expanded) {
// 展开时关闭其他行
expandedRowKeys.value = [record.id];
} else {
// 收起时移除
const index = expandedRowKeys.value.indexOf(record.id);
if (index > -1) {
expandedRowKeys.value.splice(index, 1);
}
}
};
/* 表格详情展开关闭 end */
const columStore = columWidthStore();
const columnResized=(record)=>{
columStore.setColumnList({
routePath: record.routePath,
dataIndex: record.dataIndex,
width: record.width,
});
}
const initColumnWidth=()=>{
let temp = columStore.getRouteColumnWidths(route?.path)
if(temp?.length){
tableColumns.value.forEach((el)=>{
let index = temp.findIndex(item=>item.dataIndex == el.dataIndex);
if(index>=0)el.width = temp[index].width;
})
}
}
const getCellOverflow = (columnKey, rowKey) => {
if (!customizeTableRef.value?.isOverflow) {
console.warn('无法访问子组件的 isOverflow 方法');
return false;
}
try {
return customizeTableRef.value.isOverflow(columnKey, rowKey);
} catch (error) {
console.error('调用 isOverflow 方法出错:', error);
return false;
}
};
onMounted(()=>{
initColumnWidth();
})
</script>
<style scoped>
.table-cell {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
</style>
