front/src/components/TablePro/index.vue
2026-09-05 18:37:50 +08:00

997 lines
24 KiB
Vue
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.

<script lang="ts" setup>
import { useI18n } from 'vue-i18n';
import {
computed,
nextTick,
onMounted,
onUnmounted,
PropType,
reactive,
ref,
unref,
useAttrs,
watch,
} from 'vue';
import Sortable from 'sortablejs';
import cloneDeep from 'lodash/cloneDeep';
import { VxeGridProps, VxeGridPropTypes } from 'vxe-table';
import useAutoHeight from '@/components/TablePro/autoHeight';
import { isFunction } from 'lodash';
import FormPro from '@/components/FormPro/index.vue';
import { $baseMessage } from '@/utils';
// import emptyImage from '@/assets/images/empty.png';
import type { TableProMethods } from './types';
const { t } = useI18n();
const props = defineProps({
tableConfig: {
type: Object,
default: () => ({}),
},
api: {
type: Function as PropType<(...arg: any[]) => Promise<any>>,
default: null,
},
useForm: {
type: Boolean,
default: true,
},
formConfig: {
type: Object,
default: () => ({}),
},
autoHeight: {
type: Boolean,
default: true,
},
immediate: {
type: Boolean,
default: true,
},
showPager: {
type: Boolean,
default: true,
},
searchInfo: {
type: Object,
default: () => ({}),
},
tableClassName: {
type: String,
default: '',
},
maxPageSize: {
type: Number,
default: null, // null 表示不限制
},
defaultPageSize: {
type: Number,
default: null, // null 表示使用统一默认值 50
},
});
const attrs = useAttrs();
const proOptions = reactive<VxeGridProps>({
columnConfig: {
useKey: true,
resizable: true,
},
showOverflow: 'tooltip',
showHeaderOverflow: 'tooltip',
toolbarConfig: {
slots: {
tools: 'toolbar_right',
buttons: 'toolbar_buttons',
},
enabled: true,
perfect: true,
// enable built-in column show/hide configurator
custom: false, // 禁用自定义工具栏按钮
...props.tableConfig?.toolbarConfig,
},
...props.tableConfig,
...attrs,
});
// Compute column minWidth based on header title length when width not provided
const applyAutoHeaderWidth = (columns: any[] = []) => {
const basePadding = 36; // more space for sort/filters
const minCell = 110; // avoid 2-line headers
const charPx = 18; // wider per-char to avoid wrapping
return columns.map((col: any) => {
const next = { ...col };
if (!next.width && !next.minWidth && next.title && !['checkbox', 'seq', 'radio'].includes(next.type)) {
const textLen = String(next.title).length;
next.minWidth = Math.max(minCell, textLen * charPx + basePadding);
}
if (next.children && Array.isArray(next.children)) {
next.children = applyAutoHeaderWidth(next.children);
}
return next;
});
};
// apply once on init
if (props.tableConfig?.columns && Array.isArray(props.tableConfig.columns)) {
proOptions.columns = applyAutoHeaderWidth(props.tableConfig.columns as any[]);
}
const tableRef = ref();
const formProRef = ref();
// 根据 maxPageSize 限制生成 pageSizes 选项(统一按 10 / 30 / 50 / 100)
const generatePageSizes = () => {
const baseSizes = [10, 30, 50, 100];
if (props.maxPageSize) {
// 按照统一规则,只保留不超过 maxPageSize 的选项
return baseSizes.filter((size) => size <= props.maxPageSize);
}
return baseSizes;
};
// 确定默认 pageSize(按统一分页规则,默认 50)
const getDefaultPageSize = () => {
if (props.defaultPageSize) {
return props.defaultPageSize;
}
if (props.maxPageSize) {
// 有最大限制时,默认取 50,但不超过 maxPageSize
return Math.min(50, props.maxPageSize);
}
return 50;
};
const tablePage = reactive<VxeGridPropTypes.PagerConfig>({
currentPage: 1,
pageSize: getDefaultPageSize(),
total: 0,
pagerCount: 5,
layouts: [
'Total',
// 'PrevJump',
'PrevPage',
'JumpNumber',
'NextPage',
// 'NextJump',
'Sizes',
'FullJump',
],
size: 'medium',
pageSizes: generatePageSizes(),
});
// 已选行数量
const rowsLength = ref(0);
const { contentHeight, calcContentHeight } = useContentHeight(
tableRef,
true,
12
);
const getData = async () => {
if (isFunction(props.api)) {
const formValues = formProRef.value?.getValues() || {};
// 限制 pageSize 不超过 maxPageSize
let pageSize = tablePage.pageSize;
if (props.maxPageSize && pageSize > props.maxPageSize) {
pageSize = props.maxPageSize;
tablePage.pageSize = pageSize;
}
const d = await props.api({
...formValues,
...props.searchInfo,
pageIndex: tablePage.currentPage,
pageSize: pageSize,
});
if (d) {
if (props.showPager) {
tablePage.total = Number(d.totalCount) || 0;
const items = d.items || d?.data?.list || [];
const dataArray = Array.isArray(items) ? items : [];
if (tableRef.value) {
// 先清空数据,再加载新数据,确保表格更新
tableRef.value.reloadData([]);
nextTick(() => {
tableRef.value.reloadData(dataArray);
// 强制刷新表格
nextTick(() => {
// 强制触发表格更新
if (tableRef.value && typeof tableRef.value.updateData === 'function') {
tableRef.value.updateData();
}
validatePage();
});
});
} else {
console.error('TablePro tableRef 不存在');
}
} else {
const data = d.data || d;
const items = Array.isArray(data) ? data : [];
tableRef.value.reloadData(items);
tablePage.total = Number(items.length) || 0;
}
if (props.autoHeight) {
proOptions.height = contentHeight.value;
}
}
}
};
const submitFn = () => {
getData();
};
const resetFn = () => {
tablePage.currentPage = 1;
getData();
};
const tableClassName = computed(() => {
return `px-16px ${props.tableClassName}`;
});
const divRef = ref();
let sortable: any = null;
const defaultColumns2 = ref([]);
const getDefault = () => {
defaultColumns2.value = cloneDeep(proOptions.columns || props.tableConfig.columns);
};
// 使用sortablejs实现aroc table拖拽排序,并且支持固定列不允许拖动,获取拖动完成后的列排序数据
const handleSortable = () => {
const $grid = tableRef.value;
if (!$grid) return;
sortable = Sortable.create(
$grid.$el.querySelector(
'.body--wrapper>.vxe-table--header .vxe-header--row'
),
{
handle: '.vxe-header--column:not(.col--fixed)',
onEnd: (sortableEvent: any) => {
// const { showIndex } = props;
const defaultColumns = cloneDeep(defaultColumns2.value);
const targetThElem = sortableEvent.item;
const newIndex = sortableEvent.newIndex as number;
const oldIndex = sortableEvent.oldIndex as number;
const { fullColumn, tableColumn } = $grid.getTableColumn();
const wrapperElem = targetThElem.parentNode as HTMLElement;
const newColumn = fullColumn[newIndex];
const oldColumn = fullColumn[oldIndex];
if (oldColumn.fixed) {
// $baseMessage('固定列不允许拖动!', 'error')
return;
}
if (newColumn.fixed) {
// 错误的移动
const oldThElem = wrapperElem.children[oldIndex] as HTMLElement;
if (newIndex > oldIndex) {
wrapperElem.insertBefore(targetThElem, oldThElem);
} else {
wrapperElem.insertBefore(
targetThElem,
oldThElem ? oldThElem.nextElementSibling : oldThElem
);
}
// $baseMessage('固定列不允许拖动!', 'error')
return;
}
// 获取列索引 columnIndex > fullColumn
const oldColumnIndex = $grid.getColumnIndex(tableColumn[oldIndex]);
const newColumnIndex = $grid.getColumnIndex(tableColumn[newIndex]);
// 移动到目标列
const currRow = fullColumn.splice(oldColumnIndex, 1)[0];
fullColumn.splice(newColumnIndex, 0, currRow);
$grid.loadColumn(fullColumn);
// 保存默认的表格列
const splielength = 1;
const dataCurrRow = defaultColumns.splice(
oldColumnIndex - splielength,
1
)[0];
defaultColumns.splice(newColumnIndex - splielength, 0, dataCurrRow);
defaultColumns2.value = defaultColumns;
// setLocalColumns(columnsKey, defaultColumns);
// columnStorage.value = defaultColumns
},
}
);
};
const { height } = useAutoHeight(divRef, {
subtractHeight: 60, // 为分页组件预留空间
});
watch(
height,
() => {
if (props.autoHeight) {
proOptions.height = height.value;
} else {
proOptions.height = props.tableConfig.height;
}
},
{
immediate: true,
deep: true,
}
);
// 计算最大页码
const maxPage = computed(() => {
if (tablePage.total === 0 || tablePage.pageSize === 0) {
return 1;
}
return Math.ceil(tablePage.total / tablePage.pageSize);
});
// 验证并限制页码
const validatePage = () => {
const max = maxPage.value;
if (tablePage.currentPage > max && max > 0) {
tablePage.currentPage = max;
$baseMessage(t('pagination.pageExceeded', { max }), 'warning');
return true; // 返回 true 表示页码被调整了
}
if (tablePage.currentPage < 1) {
tablePage.currentPage = 1;
return true; // 返回 true 表示页码被调整了
}
return false; // 返回 false 表示页码正常
};
watch(
() => [tablePage.currentPage, tablePage.pageSize],
() => {
const pageAdjusted = validatePage();
// 如果页码被调整了,不需要再次调用 getData,因为调整页码会再次触发这个 watch
if (!pageAdjusted) {
getData();
}
},
{ deep: true }
);
// 监听 total 变化,重新验证页码
watch(
() => tablePage.total,
() => {
const pageAdjusted = validatePage();
// 如果页码被调整了,会触发上面的 watch,自动调用 getData
// 如果页码正常,不需要额外操作
}
);
onMounted(() => {
getDefault();
if (props.immediate) {
getData();
}
setTimeout(() => {
handleSortable();
}, 500);
});
onUnmounted(() => {
if (sortable) {
sortable.destroy();
}
});
// 实现所有方法
/**
* 清除选择
*/
const clearRowKeys = (): void => {
if (unref(tableRef)) {
unref(tableRef).clearCheckboxRow();
unref(tableRef).clearRadioRow();
}
};
/**
* 重新加载数据,可以传入额外的参数
* @param params 额外的查询参数
*/
const reload = (params?: Record<string, any>): void => {
if (params) {
tablePage.currentPage = params.page || 1;
}
getData();
};
/**
* 删除勾选行
*/
const removeCheckboxRow = async (): Promise<void> => {
await tableRef.value.removeCheckboxRow();
const records = unref(tableRef) && unref(tableRef).getCheckboxRecords();
rowsLength.value = records.length;
};
/**
* 切换某一行的选择状态
*/
const toggleCheckboxRow = (row: any): void => {
if (unref(tableRef)) {
unref(tableRef).toggleCheckboxRow(row);
}
};
/**
* 设置所有行的选中状态
*/
const setAllCheckboxRow = (checked: boolean): void => {
if (unref(tableRef)) {
unref(tableRef).setAllCheckboxRow(checked);
}
};
/**
* 设置行为选中状态
*/
const setCheckboxRow = (rows: any, checked: boolean): void => {
if (unref(tableRef)) {
unref(tableRef).setCheckboxRow(rows, checked);
}
};
/**
* 设置某一行为选中状态
*/
const setRadioRow = (row: any): void => {
if (unref(tableRef)) {
unref(tableRef).setRadioRow(row);
}
};
/**
* 获取当前已选中的行数据
*/
const checkAllRows = (isFull?: boolean): any[] => {
return unref(tableRef)?.getCheckboxRecords(isFull) || [];
};
/**
* 获取列信息
*/
const getColumnByField = (name: string): any => {
return unref(tableRef).getColumnByField(name);
};
/**
* 隐藏列
*/
const hideColumn = (name: string): void => {
const field = getColumnByField(name);
if (unref(tableRef)) {
unref(tableRef).hideColumn(field || '');
}
};
/**
* 显示列
*/
const showColumn = (name: string): void => {
const field = getColumnByField(name);
if (unref(tableRef)) {
unref(tableRef).showColumn(field || '');
}
};
/**
* 新增行
*/
const addRow = async (args: any): Promise<any> => {
return unref(tableRef)?.insert({ ...args });
};
/**
* 新增多行到指定位置
*/
const addRowsAt = async (record: any | any[], row?: any): Promise<any> => {
const $table = unref(tableRef);
return $table.insertAt(record, row || null);
};
/**
* 激活编辑行
*/
const editRowEvent = async (
row: any,
fieldOrColumn?: string | any
): Promise<any> => {
return unref(tableRef)?.setEditRow(row, fieldOrColumn);
};
/**
* 设置展开树形节点
*/
const setTreeExpand = async (rows: any, checked: boolean): Promise<any> => {
return unref(tableRef)?.setTreeExpand(rows, checked);
};
/**
* 选择编辑单元格
*/
const editRowCellEvent = (
row: any,
fieldOrColumn: string | any
): Promise<any> => {
return unref(tableRef)?.setEditCell(row, fieldOrColumn);
};
/**
* 取消编辑状态
*/
const clearEdit = (): Promise<any> => {
return unref(tableRef)?.clearEdit();
};
/**
* 取消当前编辑状态并还原数据
*/
const cancelRowEvent = (row: any): void => {
clearEdit().then(() => {
// 还原行数据
unref(tableRef)?.revertData(row);
});
};
/**
* 获取表格数据集
*/
const getActionRows = (): {
insertRecords: any;
removeRecords: any;
updateRecords: any;
} => {
const { insertRecords, removeRecords, updateRecords } =
unref(tableRef)?.getRecordset() || {};
return {
insertRecords: insertRecords || [],
removeRecords: removeRecords || [],
updateRecords: updateRecords || [],
};
};
/**
* 表格验证
*/
const validate = async (): Promise<boolean> => {
const flag = await unref(tableRef).validate(true);
return !flag;
};
/**
* 表格完整验证
*/
const fullValidate = async (): Promise<boolean> => {
const flag = await unref(tableRef).fullValidate(true);
return !flag;
};
/**
* 获取表格数据
*/
const getTableData = (): any => {
return unref(tableRef).getTableData();
};
/**
* 获取表格数据 -> table数据
*/
const getTableData2 = (): any => {
return getTableData().tableData;
};
/**
* 获取表格数据 -> 全部数据
*/
const getFullData = (): any => {
return getTableData().fullData;
};
/**
* 获取表格数据 -> 显示数据
*/
const getVisibleData = (): any => {
return getTableData().visibleData;
};
/**
* 获取表格数据 -> 底部数据
*/
const getFooterData = (): any => {
return getTableData().footerData;
};
/**
* 判断 row 是否为编辑状态
*/
const isEditByRow = (row: any): boolean => {
return unref(tableRef).isEditByRow(row) || false;
};
/**
* 删除行
*/
const removeRow = (row?: any): any => {
unref(tableRef).remove(row);
};
/**
* 更改某一行的状态
*/
const updateStatus = (row: any): any => {
unref(tableRef).updateStatus(row);
};
/**
* 重载表格数据并清除所有状态
*/
const reloadData = (data: any[]): void => {
if (unref(tableRef)) {
unref(tableRef).reloadData(data);
}
rowsLength.value = 0;
};
/**
* 加载数据
*/
const loadData = (data: any[]): void => {
if (unref(tableRef)) {
unref(tableRef)?.loadData(data);
}
rowsLength.value = 0;
};
/**
* 获取表格数据编辑
*/
const getUpdateRecords = (): any[] => {
return unref(tableRef).getUpdateRecords() || [];
};
/**
* 设置某行高亮
*/
const setCurrentRow = async (row: any): Promise<void> => {
if (unref(tableRef)) {
return unref(tableRef).setCurrentRow(row);
}
return Promise.resolve();
};
/**
* 获取插入数据
*/
const getInsertRecords = (): any[] => {
return unref(tableRef).getInsertRecords() || [];
};
/**
* 获取某一行数据
* @param rowIndex 行索引
*/
const getRowData = (rowIndex: number): any => {
return unref(tableRef).getData(rowIndex);
};
/**
* 清空数据
*/
const clearData = async (): Promise<void> => {
await unref(tableRef).clearData();
};
/**
* 获取高亮行
*/
const getCurrentRecordRow = (): any => {
return unref(tableRef).getCurrentRecord();
};
/**
* 获取当前表格的列
*/
const getTableColumn = (): {
collectColumn: any;
fullColumn: any;
visibleColumn: any;
tableColumn: any;
} => {
const { collectColumn, fullColumn, visibleColumn, tableColumn } =
unref(tableRef)?.getTableColumn() || {};
return {
collectColumn: collectColumn || [],
fullColumn: fullColumn || [],
visibleColumn: visibleColumn || [],
tableColumn: tableColumn || [],
};
};
/**
* 清空高亮
*/
const clearCurrentRow = async (): Promise<void> => {
await unref(tableRef).clearCurrentRow();
};
/**
* 获取鼠标选择的所有区域
*/
const getCellAreas = (): any[] => {
return unref(tableRef).getCellAreas() || [];
};
/**
* 获取区域中的活动单元格
*/
const getActiveCellArea = (): any => {
return unref(tableRef).getActiveCellArea();
};
/**
* 选择指定区域的单元格
*/
const setCellAreas = (areaConfigs: any[], activeArea?: any): Promise<any> => {
return unref(tableRef).setCellAreas(areaConfigs, activeArea);
};
/**
* 清除鼠标选择的区域
*/
const clearCellAreas = (area?: any): Promise<any> => {
return unref(tableRef).clearCellAreas(area);
};
/**
* 清除标记为复制粘贴的区域
*/
const clearCopyCellArea = (): Promise<any> => {
return unref(tableRef).clearCopyCellArea();
};
/**
* 手动关闭查找与替换窗口
*/
const closeFNR = (): Promise<any> => {
return unref(tableRef).closeFNR();
};
/**
* 打开单元格查找功能
*/
const openFind = (): Promise<any> => {
return unref(tableRef).openFind();
};
/**
* 打开单元格替换功能
*/
const openReplace = (): Promise<any> => {
return unref(tableRef).openReplace();
};
/**
* 保存行
*/
const saveRow = (row: any): Promise<any> => {
return unref(tableRef).saveRow(row);
};
const setHeight = (height: number) => {
proOptions.height = height;
};
// 暴露所有方法
defineExpose({
xGrid: tableRef,
setHeight,
clearRowKeys,
removeCheckboxRow,
toggleCheckboxRow,
setAllCheckboxRow,
setCheckboxRow,
setRadioRow,
checkAllRows,
getColumnByField,
hideColumn,
showColumn,
addRow,
addRowsAt,
editRowEvent,
setTreeExpand,
editRowCellEvent,
clearEdit,
cancelRowEvent,
getActionRows,
validate,
fullValidate,
getTableData,
getTableData2,
getFullData,
getVisibleData,
getFooterData,
isEditByRow,
removeRow,
updateStatus,
reloadData,
loadData,
getUpdateRecords,
setCurrentRow,
getInsertRecords,
getRowData,
clearData,
getCurrentRecordRow,
getTableColumn,
clearCurrentRow,
getCellAreas,
getActiveCellArea,
setCellAreas,
clearCellAreas,
clearCopyCellArea,
closeFNR,
openFind,
openReplace,
saveRow,
reload,
} as TableProMethods);
</script>
<template>
<div class="table-pro h-full w-full">
<div v-if="props.useForm" class="mb-12px table-pro-surface">
<FormPro
ref="formProRef"
:button-config="{
showBtn: true,
okText: t('common.search'),
resetText: t('common.reset'),
showReset: true,
showOk: true,
span: 6,
}"
class="px-16px pt-12px"
v-bind="props.formConfig"
@reset="resetFn"
@submit="submitFn"
/>
</div>
<div ref="divRef" class="table-pro-surface">
<vxe-grid
ref="tableRef"
:sort-config="{}"
border
:class="tableClassName"
v-bind="proOptions"
>
<template v-for="item in Object.keys($slots)" :key="item" #[item]="row">
<slot :name="item" v-bind="row" />
</template>
<!--
<template #empty>
<div class="flex justify-center empty-image">
<img :src="emptyImage" alt="空数据" class="w-68px mb-17px" />
</div>
<div v-if="showText" class="empty-text">
{{ props.emptyText }}
</div>
</template> -->
<template #toolbar_buttons>
<div class="toolbar-buttons">
<slot name="toolbar_buttons" />
</div>
</template>
<template #toolbar_right>
<div class="toolbar-right">
<slot name="toolbar_right" />
</div>
</template>
<template v-if="props.showPager" #pager>
<vxe-pager
v-model:current-page="tablePage.currentPage"
v-model:page-size="tablePage.pageSize"
:layouts="tablePage.layouts"
:page-sizes="tablePage.pageSizes"
:pager-count="tablePage.pagerCount"
:size="tablePage.size"
:total="tablePage.total"
>
<template #left>
<!-- <div v class="absolute" style="left: 0">-->
<!-- {{ $t('已选择') }}-->
<!-- <span>{{ rowsLength }}</span-->
<!-- >{{ $t('条') }}-->
<!-- </div>-->
<slot name="buttom_left" />
</template>
</vxe-pager>
</template>
</vxe-grid>
</div>
</div>
</template>
<style lang="less" scoped>
.table-pro-surface {
background-color: var(--app-card-bg);
}
.table-pro {
:deep(.vxe-toolbar) {
padding: 0;
background-color: var(--color-bg-2);
border: 0 !important;
border-bottom: none;
}
:deep(.vxe-table) {
border-color: var(--color-neutral-3);
.vxe-table--header-wrapper {
background-color: var(--color-fill-2);
}
.vxe-table--render-default.size--small {
font-size: 12px;
}
// 全局设置表格行高度为小尺寸
.vxe-table--render-default {
font-size: 12px;
.vxe-body--row {
height: 32px !important;
line-height: 32px !important;
}
.vxe-header--row {
height: 40px !important;
line-height: 40px !important;
}
}
.vxe-body--row {
background-color: var(--color-bg-2);
&:hover {
background-color: var(--color-fill-2);
}
}
}
.empty-image {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px 0;
font-size: 12px;
color: #b8babc;
img {
// width: 180px;
user-select: none;
}
}
.toolbar-buttons {
display: flex;
align-items: center;
}
.toolbar-right {
display: flex;
align-items: center;
justify-content: flex-end;
}
}
</style>