系统菜单部分功能及planToAdd文件中的config.ts文件修改

This commit is contained in:
duyufeng
2024-08-15 10:51:58 +08:00
parent 94e5079222
commit ee565449ab
931 changed files with 12966 additions and 17 deletions

View File

@@ -0,0 +1,10 @@
export { default as BasicTable } from './src/BasicTable.vue';
export { default as TableAction } from './src/components/TableAction.vue';
export { default as EditTableHeaderIcon } from './src/components/EditTableHeaderIcon.vue';
// export { default as TableImg } from './src/components/TableImg.vue';
export * from './src/types/table';
// export * from './src/types/pagination';
// export * from './src/types/tableAction';
// export { useTable } from './src/hooks/useTable';
// export type { FormSchema, FormProps } from '/@/components/Form/src/types/form';
// export type { EditRecordRow } from './src/components/editable';

View File

@@ -0,0 +1,605 @@
<template>
<div ref="wrapRef" :class="getWrapperClass">
<!-- <BasicForm
:class="{ 'table-search-area-hidden': !getBindValues.formConfig?.schemas?.length }"
submitOnReset
v-bind="getFormProps"
v-if="getBindValues.useSearchForm"
:tableAction="tableAction"
@register="registerForm"
@submit="handleSearchInfoChange"
@advanced-change="redoHeight"
>
<template #[replaceFormSlotKey(item)]="data" v-for="item in getFormSlotKeys">
<slot :name="item" v-bind="data || {}"></slot>
</template>
</BasicForm> -->
<!-- antd v3 升级兼容阻止数据的收集防止控制台报错 -->
<!-- https://antdv.com/docs/vue/migration-v3-cn -->
<a-form-item-rest>
<!-- TV360X-377关联记录必填影响到了table的输入框和页码样式 -->
<a-form-item>
<Table ref="tableElRef" v-bind="getBindValues" :rowClassName="getRowClassName" v-show="getEmptyDataIsShowTable" @resizeColumn="handleResizeColumn" @change="handleTableChange">
<!-- antd的原生插槽直接传递 -->
<template #[item]="data" v-for="item in slotNamesGroup.native" :key="item">
<!-- update-begin--author:liaozhiyang---date:20240424---forissues/1146BasicTable使用headerCell全选框出不来 -->
<template v-if="item === 'headerCell'">
<CustomSelectHeader v-if="isCustomSelection(data.column)" v-bind="selectHeaderProps" />
<slot v-else :name="item" v-bind="data || {}"></slot>
</template>
<slot v-else :name="item" v-bind="data || {}"></slot>
<!-- update-begin--author:liaozhiyang---date:20240424---forissues/1146BasicTable使用headerCell全选框出不来 -->
</template>
<template #headerCell="{ column }">
<!-- update-begin--author:sunjianlei---date:220230630---forQQYUN-5571自封装选择列解决数据行选择卡顿问题 -->
<CustomSelectHeader v-if="isCustomSelection(column)" v-bind="selectHeaderProps"/>
<HeaderCell v-else :column="column" />
<!-- update-end--author:sunjianlei---date:220230630---forQQYUN-5571自封装选择列解决数据行选择卡顿问题 -->
</template>
<!-- 增加对antdv3.x兼容 -->
<template #bodyCell="data">
<!-- update-begin--author:liaozhiyang---date:220230717---forissues-179antd3 一些警告以及报错(针对表格) -->
<!-- update-begin--author:liusq---date:20230921---forissues/770slotsBak异常报错的问题,增加判断column是否存在 -->
<template v-if="data.column?.slotsBak?.customRender">
<!-- update-end--author:liusq---date:20230921---forissues/770slotsBak异常报错的问题,增加判断column是否存在 -->
<slot :name="data.column.slotsBak.customRender" v-bind="data || {}"></slot>
</template>
<template v-else>
<slot name="bodyCell" v-bind="data || {}"></slot>
</template>
<!-- update-begin--author:liaozhiyang---date:22030717---forissues-179antd3 一些警告以及报错(针对表格) -->
</template>
<!-- update-begin--author:liaozhiyang---date:20240425---forpull/1201添加antd的TableSummary功能兼容老的summary表尾合计 -->
<template v-if="showSummaryRef && !getBindValues.showSummary" #summary="data">
<slot name="summary" v-bind="data || {}">
<TableSummary :data="data || {}" v-bind="getSummaryProps" />
</slot>
</template>
<!-- update-end--author:liaozhiyang---date:20240425---forpull/1201添加antd的TableSummary功能兼容老的summary表尾合计 -->
</Table>
</a-form-item>
</a-form-item-rest>
</div>
</template>
<script lang="ts">
import type { BasicTableProps, TableActionType, SizeType, ColumnChangeParam, BasicColumn } from './types/table';
import { defineComponent, ref, computed, unref, toRaw, inject, watchEffect, watch, onUnmounted, onMounted, nextTick } from 'vue';
import { Table } from 'ant-design-vue';
// import { BasicForm, useForm } from '/@/components/Form/index';
// import { PageWrapperFixedHeightKey } from '/@/components/Page/injectionKey';
import CustomSelectHeader from './components/CustomSelectHeader.vue'
import expandIcon from './components/ExpandIcon';
// import HeaderCell from './components/HeaderCell.vue';
import TableSummary from './components/TableSummary';
import { InnerHandlers } from './types/table';
import { usePagination } from './hooks/usePagination';
import { useColumns } from './hooks/useColumns';
import { useDataSource } from './hooks/useDataSource';
import { useLoading } from './hooks/useLoading';
import { useRowSelection } from './hooks/useRowSelection';
import { useTableScroll } from './hooks/useTableScroll';
import { useCustomRow } from './hooks/useCustomRow';
import { useTableStyle } from './hooks/useTableStyle';
import { useTableHeader } from './hooks/useTableHeader';
import { useTableExpand } from './hooks/useTableExpand';
import { createTableContext } from './hooks/useTableContext';
import { useTableFooter } from './hooks/useTableFooter';
import { useTableForm } from './hooks/useTableForm';
// import { useDesign } from '/@/hooks/web/useDesign';
import { useCustomSelection } from "./hooks/useCustomSelection";
import { omit, pick } from 'lodash-es';
import { basicProps } from './props';
// import { isFunction } from '/@/utils/is';
// import { warn } from '/@/utils/log';
export default defineComponent({
components: {
Table,
// BasicForm,
HeaderCell,
TableSummary,
CustomSelectHeader,
},
props: basicProps,
emits: [
'fetch-success',
'fetch-error',
'selection-change',
'register',
'row-click',
'row-dbClick',
'row-contextmenu',
'row-mouseenter',
'row-mouseleave',
'edit-end',
'edit-cancel',
'edit-row-end',
'edit-change',
'expanded-rows-change',
'change',
'columns-change',
'table-redo',
],
setup(props, { attrs, emit, slots, expose }) {
const tableElRef = ref(null);
const tableData = ref<Recordable[]>([]);
const wrapRef = ref(null);
const innerPropsRef = ref<Partial<BasicTableProps>>();
const { prefixCls } = useDesign('basic-table');
const [registerForm, formActions] = useForm();
const getProps = computed(() => {
return { ...props, ...unref(innerPropsRef) } as BasicTableProps;
});
const isFixedHeightPage = inject(PageWrapperFixedHeightKey, false);
watchEffect(() => {
unref(isFixedHeightPage) &&
props.canResize &&
warn("'canResize' of BasicTable may not work in PageWrapper with 'fixedHeight' (especially in hot updates)");
});
const { getLoading, setLoading } = useLoading(getProps);
const { getPaginationInfo, getPagination, setPagination, setShowPagination, getShowPagination } = usePagination(getProps);
// update-begin--author:sunjianlei---date:220230630---for【QQYUN-5571】自封装选择列解决数据行选择卡顿问题
// const { getRowSelection, getRowSelectionRef, getSelectRows, clearSelectedRowKeys, getSelectRowKeys, deleteSelectRowByKey, setSelectedRowKeys } =
// useRowSelection(getProps, tableData, emit);
// 子级列名
const childrenColumnName = computed(() => getProps.value.childrenColumnName || 'children');
// 自定义选择列
const {
getRowSelection,
getSelectRows,
getSelectRowKeys,
setSelectedRowKeys,
getRowSelectionRef,
selectHeaderProps,
isCustomSelection,
handleCustomSelectColumn,
clearSelectedRowKeys,
deleteSelectRowByKey,
getExpandIconColumnIndex,
} = useCustomSelection(
getProps,
emit,
wrapRef,
getPaginationInfo,
tableData,
childrenColumnName
)
// update-end--author:sunjianlei---date:220230630---for【QQYUN-5571】自封装选择列解决数据行选择卡顿问题
const {
handleTableChange: onTableChange,
getDataSourceRef,
getDataSource,
getRawDataSource,
setTableData,
updateTableDataRecord,
deleteTableDataRecord,
insertTableDataRecord,
findTableDataRecord,
fetch,
getRowKey,
reload,
getAutoCreateKey,
updateTableData,
} = useDataSource(
getProps,
{
tableData,
getPaginationInfo,
setLoading,
setPagination,
validate: formActions.validate,
clearSelectedRowKeys,
},
emit
);
function handleTableChange(...args) {
onTableChange.call(undefined, ...args);
emit('change', ...args);
// 解决通过useTable注册onChange时不起作用的问题
const { onChange } = unref(getProps);
onChange && isFunction(onChange) && onChange.call(undefined, ...args);
}
const { getViewColumns, getColumns, setCacheColumnsByField, setColumns, getColumnsRef, getCacheColumns } = useColumns(
getProps,
getPaginationInfo,
// update-begin--author:sunjianlei---date:220230630---for【QQYUN-5571】自封装选择列解决数据行选择卡顿问题
handleCustomSelectColumn,
// update-end--author:sunjianlei---date:220230630---for【QQYUN-5571】自封装选择列解决数据行选择卡顿问题
);
const { getScrollRef, redoHeight } = useTableScroll(getProps, tableElRef, getColumnsRef, getRowSelectionRef, getDataSourceRef);
const { customRow } = useCustomRow(getProps, {
setSelectedRowKeys,
getSelectRowKeys,
clearSelectedRowKeys,
getAutoCreateKey,
emit,
});
const { getRowClassName } = useTableStyle(getProps, prefixCls);
const { getExpandOption, expandAll, collapseAll } = useTableExpand(getProps, tableData, emit);
const handlers: InnerHandlers = {
onColumnsChange: (data: ColumnChangeParam[]) => {
emit('columns-change', data);
// support useTable
unref(getProps).onColumnsChange?.(data);
},
};
const { getHeaderProps } = useTableHeader(getProps, slots, handlers);
// update-begin--author:liaozhiyang---date:20240425---for【pull/1201】添加antd的TableSummary功能兼容老的summary表尾合计
const getSummaryProps = computed(() => {
return pick(unref(getProps), ['summaryFunc', 'summaryData', 'hasExpandedRow', 'rowKey']);
});
const getIsEmptyData = computed(() => {
return (unref(getDataSourceRef) || []).length === 0;
});
const showSummaryRef = computed(() => {
const summaryProps = unref(getSummaryProps);
return (summaryProps.summaryFunc || summaryProps.summaryData) && !unref(getIsEmptyData);
});
// update-end--author:liaozhiyang---date:20240425---for【pull/1201】添加antd的TableSummary功能兼容老的summary表尾合计
const { getFooterProps } = useTableFooter(getProps, slots, getScrollRef, tableElRef, getDataSourceRef);
const { getFormProps, replaceFormSlotKey, getFormSlotKeys, handleSearchInfoChange } = useTableForm(getProps, slots, fetch, getLoading);
const getBindValues = computed(() => {
const dataSource = unref(getDataSourceRef);
let propsData: Recordable = {
// ...(dataSource.length === 0 ? { getPopupContainer: () => document.body } : {}),
...attrs,
customRow,
//树列表展开使用AntDesignVue默认的加减图标 author:scott date:20210914
//expandIcon: slots.expandIcon ? null : expandIcon(),
...unref(getProps),
...unref(getHeaderProps),
scroll: unref(getScrollRef),
loading: unref(getLoading),
tableLayout: 'fixed',
rowSelection: unref(getRowSelectionRef),
rowKey: unref(getRowKey),
columns: toRaw(unref(getViewColumns)),
pagination: toRaw(unref(getPaginationInfo)),
dataSource,
footer: unref(getFooterProps),
...unref(getExpandOption),
// 【QQYUN-5837】动态计算 expandIconColumnIndex
expandIconColumnIndex: getExpandIconColumnIndex.value,
};
//update-begin---author:wangshuai ---date:20230214 for[QQYUN-4237]代码生成 内嵌子表模式 没有滚动条------------
//额外的展开行存在插槽时会将滚动移除掉,注释掉
/*if (slots.expandedRowRender) {
propsData = omit(propsData, 'scroll');
}*/
//update-end---author:wangshuai ---date:20230214 for[QQYUN-4237]代码生成 内嵌子表模式 没有滚动条------------
// update-begin--author:sunjianlei---date:220230630---for【QQYUN-5571】自封装选择列解决数据行选择卡顿问题
// 自定义选择列,需要去掉原生的
delete propsData.rowSelection
// update-end--author:sunjianlei---date:220230630---for【QQYUN-5571】自封装选择列解决数据行选择卡顿问题
// update-begin--author:liaozhiyang---date:20230919---for【QQYUN-6387】展开写法去掉报错
!propsData.isTreeTable && delete propsData.expandIconColumnIndex;
propsData.expandedRowKeys === null && delete propsData.expandedRowKeys;
// update-end--author:liaozhiyang---date:20230919---for【QQYUN-6387】展开写法去掉报错
propsData = omit(propsData, ['class', 'onChange']);
return propsData;
});
// 统一设置表格列宽度
const getMaxColumnWidth = computed(() => {
const values = unref(getBindValues);
return values.maxColumnWidth > 0 ? values.maxColumnWidth + 'px' : null;
});
const getWrapperClass = computed(() => {
const values = unref(getBindValues);
return [
prefixCls,
attrs.class,
{
[`${prefixCls}-form-container`]: values.useSearchForm,
[`${prefixCls}--inset`]: values.inset,
[`${prefixCls}-col-max-width`]: getMaxColumnWidth.value != null,
// 是否显示表尾合计
[`${prefixCls}--show-summary`]: values.showSummary,
},
];
});
const getEmptyDataIsShowTable = computed(() => {
const { emptyDataIsShowTable, useSearchForm } = unref(getProps);
if (emptyDataIsShowTable || !useSearchForm) {
return true;
}
return !!unref(getDataSourceRef).length;
});
function setProps(props: Partial<BasicTableProps>) {
innerPropsRef.value = { ...unref(innerPropsRef), ...props };
}
const tableAction: TableActionType = {
reload,
getSelectRows,
clearSelectedRowKeys,
getSelectRowKeys,
deleteSelectRowByKey,
setPagination,
setTableData,
updateTableDataRecord,
deleteTableDataRecord,
insertTableDataRecord,
findTableDataRecord,
redoHeight,
setSelectedRowKeys,
setColumns,
setLoading,
getDataSource,
getRawDataSource,
setProps,
getRowSelection,
getPaginationRef: getPagination,
getColumns,
getCacheColumns,
emit,
updateTableData,
setShowPagination,
getShowPagination,
setCacheColumnsByField,
expandAll,
collapseAll,
getSize: () => {
return unref(getBindValues).size as SizeType;
},
};
createTableContext({ ...tableAction, wrapRef, getBindValues });
// update-begin--author:sunjianlei---date:220230718---for【issues/179】兼容新老slots写法移除控制台警告
// 获取分组之后的slot名称
const slotNamesGroup = computed<{
// AntTable原生插槽
native: string[];
// 列自定义插槽
custom: string[];
}>(() => {
const native: string[] = [];
const custom: string[] = [];
const columns = unref<Recordable[]>(getViewColumns) as BasicColumn[];
const allCustomRender = columns.map<string>((column) => column.slotsBak?.customRender);
for (const name of Object.keys(slots)) {
// 过滤特殊的插槽
if (['bodyCell'].includes(name)) {
continue;
}
if (allCustomRender.includes(name)) {
custom.push(name);
} else {
native.push(name);
}
}
return { native, custom };
});
// update-end--author:sunjianlei---date:220230718---for【issues/179】兼容新老slots写法移除控制台警告
// update-begin--author:liaozhiyang---date:20231226---for【issues/945】BasicTable组件设置默认展开不生效
nextTick(() => {
getProps.value.defaultExpandAllRows && expandAll();
})
// update-end--author:sunjianlei---date:20231226---for【issues/945】BasicTable组件设置默认展开不生效
expose(tableAction);
emit('register', tableAction, formActions);
return {
tableElRef,
getBindValues,
getLoading,
registerForm,
handleSearchInfoChange,
getEmptyDataIsShowTable,
handleTableChange,
getRowClassName,
wrapRef,
tableAction,
redoHeight,
handleResizeColumn: (w, col) => {
console.log('col',col);
col.width = w;
},
getFormProps: getFormProps as any,
replaceFormSlotKey,
getFormSlotKeys,
getWrapperClass,
getMaxColumnWidth,
columns: getViewColumns,
// update-begin--author:sunjianlei---date:220230630---for【QQYUN-5571】自封装选择列解决数据行选择卡顿问题
selectHeaderProps,
isCustomSelection,
// update-end--author:sunjianlei---date:220230630---for【QQYUN-5571】自封装选择列解决数据行选择卡顿问题
slotNamesGroup,
// update-begin--author:liaozhiyang---date:20240425---for【pull/1201】添加antd的TableSummary功能兼容老的summary表尾合计
getSummaryProps,
showSummaryRef,
// update-end--author:liaozhiyang---date:20240425---for【pull/1201】添加antd的TableSummary功能兼容老的summary表尾合计
};
},
});
</script>
<style lang="less">
@border-color: #cecece4d;
@prefix-cls: ~'@{namespace}-basic-table';
[data-theme='dark'] {
.ant-table-tbody > tr:hover.ant-table-row-selected > td,
.ant-table-tbody > tr.ant-table-row-selected td {
background-color: #262626;
}
.@{prefix-cls} {
//表格选择工具栏样式
.alert {
// background-color: #323232;
// border-color: #424242;
}
}
}
.@{prefix-cls} {
max-width: 100%;
&-row__striped {
td {
background-color: @app-content-background;
}
}
// update-begin--author:liaozhiyang---date:20240613---for【TV360X-1232】查询区域隐藏后点击刷新不走请求了(采用css隐藏)
> .table-search-area-hidden {
display: none;
}
// update-end--author:liaozhiyang---date:20240613---for【TV360X-1232】查询区域隐藏后点击刷新不走请求了(采用css隐藏)
&-form-container {
padding: 10px;
.ant-form {
padding: 12px 10px 6px 10px;
margin-bottom: 8px;
background-color: @component-background;
border-radius: 2px;
}
}
.ant-tag {
margin-right: 0;
}
//update-begin-author:liusq---date:20230517--for: [issues/526]RangePicker 设置预设范围按钮样式问题---
.ant-picker-preset {
.ant-tag {
margin-right: 8px !important;
}
}
//update-end-author:liusq---date:20230517--for: [issues/526]RangePicker 设置预设范围按钮样式问题---
.ant-table-wrapper {
padding: 6px;
background-color: @component-background;
border-radius: 2px;
.ant-table-title {
min-height: 40px;
padding: 0 0 8px 0 !important;
}
.ant-table.ant-table-bordered .ant-table-title {
border: none !important;
}
}
.ant-table {
width: 100%;
overflow-x: hidden;
&-title {
display: flex;
padding: 8px 6px;
border-bottom: none;
justify-content: space-between;
align-items: center;
}
//定义行颜色
.trcolor {
background-color: rgba(255, 192, 203, 0.31);
color: red;
}
//.ant-table-tbody > tr.ant-table-row-selected td {
//background-color: fade(@primary-color, 8%) !important;
//}
}
.ant-pagination {
margin: 10px 0 0 0;
}
.ant-table-footer {
padding: 0;
.ant-table-wrapper {
padding: 0;
}
table {
border: none !important;
}
.ant-table-content {
overflow-x: hidden !important;
// overflow-y: scroll !important;
}
td {
padding: 12px 8px;
}
}
//表格选择工具栏样式
.alert {
height: 38px;
// background-color: #e6f7ff;
// border-color: #91d5ff;
}
&--inset {
.ant-table-wrapper {
padding: 0;
}
}
// ------ 统一设置表格列最大宽度 ------
&-col-max-width {
.ant-table-thead tr th,
.ant-table-tbody tr td {
max-width: v-bind(getMaxColumnWidth);
}
}
// ------ 统一设置表格列最大宽度 ------
// update-begin--author:sunjianlei---date:220230718---for【issues/622】修复表尾合计错位的问题
&--show-summary {
.ant-table > .ant-table-footer {
padding: 12px 0 0;
}
.ant-table.ant-table-bordered > .ant-table-footer {
border: 0;
}
}
// update-end--author:sunjianlei---date:220230718---for【issues/622】修复表尾合计错位的问题
// update-begin--author:liaozhiyang---date:20240604---for【TV360X-377】关联记录必填影响到了table的输入框和页码样式
> .ant-form-item {
margin-bottom: 0;
}
// update-end--author:liaozhiyang---date:20240604---for【TV360X-377】关联记录必填影响到了table的输入框和页码样式
}
</style>

View File

@@ -0,0 +1,26 @@
import type { Component } from 'vue';
import { Input, Select, Checkbox, InputNumber, Switch, DatePicker, TimePicker } from 'ant-design-vue';
import type { ComponentType } from './types/componentType';
import { ApiSelect, ApiTreeSelect } from '/@/components/Form';
const componentMap = new Map<ComponentType, Component>();
componentMap.set('Input', Input);
componentMap.set('InputNumber', InputNumber);
componentMap.set('Select', Select);
componentMap.set('ApiSelect', ApiSelect);
componentMap.set('ApiTreeSelect', ApiTreeSelect);
componentMap.set('Switch', Switch);
componentMap.set('Checkbox', Checkbox);
componentMap.set('DatePicker', DatePicker);
componentMap.set('TimePicker', TimePicker);
export function add(compName: ComponentType, component: Component) {
componentMap.set(compName, component);
}
export function del(compName: ComponentType) {
componentMap.delete(compName);
}
export { componentMap };

View File

@@ -0,0 +1,67 @@
<!-- 自定义选择列表头实现部分 -->
<template>
<!-- update-begin--author:liaozhiyang---date:20231130---forissues/5595BasicTable组件hideSelectAll: true无法隐藏全选框 -->
<template v-if="isRadio">
<!-- radio不存在全选所以放个空标签 -->
<span></span>
</template>
<template v-else>
<template v-if="hideSelectAll">
<span></span>
</template>
<a-checkbox :disabled="disabled" v-else :checked="checked" :indeterminate="isHalf" @update:checked="onChange" />
</template>
<!-- update-end--author:liaozhiyang---date:20231130---forissues/5595BasicTable组件hideSelectAll: true无法隐藏全选框 -->
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps({
isRadio: {
type: Boolean,
required: true,
},
selectedLength: {
type: Number,
required: true,
},
// 当前页条目数
pageSize: {
type: Number,
required: true,
},
hideSelectAll: {
type: Boolean,
default: false,
},
// update-begin--author:liaozhiyang---date:20231016---for【QQYUN-6774】解决checkbox禁用后全选仍能勾选问题
disabled: {
type: Boolean,
required: true,
},
// update-end--author:liaozhiyang---date:20231016---for【QQYUN-6774】解决checkbox禁用后全选仍能勾选问题
});
const emit = defineEmits(['select-all']);
// 是否全选
const checked = computed(() => {
if (props.isRadio) {
return false;
}
return props.selectedLength > 0 && props.selectedLength >= props.pageSize;
});
// 是否半选
const isHalf = computed(() => {
if (props.isRadio) {
return false;
}
return props.selectedLength > 0 && props.selectedLength < props.pageSize;
});
function onChange(checked: boolean) {
emit('select-all', checked);
}
</script>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,16 @@
<template>
<span>
<slot></slot>
{{ title }}
<FormOutlined />
</span>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { FormOutlined } from '@ant-design/icons-vue';
export default defineComponent({
name: 'EditTableHeaderIcon',
components: { FormOutlined },
props: { title: { type: String, default: '' } },
});
</script>

View File

@@ -0,0 +1,23 @@
import { BasicArrow } from '/@/components/Basic';
export default () => {
return (props: Recordable) => {
if (!props.expandable) {
if (props.needIndentSpaced) {
return <span class="ant-table-row-expand-icon ant-table-row-spaced" />;
} else {
return <span />;
}
}
return (
<BasicArrow
style="margin-right: 8px"
iconStyle="margin-top: -2px;"
onClick={(e: Event) => {
props.onExpand(props.record, e);
}}
expand={props.expanded}
/>
);
};
};

View File

@@ -0,0 +1,57 @@
<template>
<EditTableHeaderCell v-if="getIsEdit">
{{ getTitle }}
</EditTableHeaderCell>
<span v-else>{{ getTitle }}</span>
<BasicHelp v-if="getHelpMessage" :text="getHelpMessage" :class="`${prefixCls}__help`" />
</template>
<script lang="ts">
import type { PropType } from 'vue';
import type { BasicColumn } from '../types/table';
import { defineComponent, computed } from 'vue';
import BasicHelp from '/@/components/Basic/src/BasicHelp.vue';
import EditTableHeaderCell from './EditTableHeaderIcon.vue';
import { useDesign } from '/@/hooks/web/useDesign';
export default defineComponent({
name: 'TableHeaderCell',
components: {
EditTableHeaderCell,
BasicHelp,
},
props: {
column: {
type: Object as PropType<BasicColumn>,
default: () => ({}),
},
},
setup(props) {
const { prefixCls } = useDesign('basic-table-header-cell');
const getIsEdit = computed(() => !!props.column?.edit);
const getTitle = computed(() => {
// update-begin--author:liaozhiyang---date:20231218---for【QQYUN-6366】升级到antd4.x
const result = props.column?.customTitle || props.column?.title;
if (typeof result === 'string') {
return result;
} else {
return '';
}
// update-end--author:liaozhiyang---date:20231218---for【QQYUN-6366】升级到antd4.x
});
const getHelpMessage = computed(() => props.column?.helpMessage);
return { prefixCls, getIsEdit, getTitle, getHelpMessage };
},
});
</script>
<style lang="less">
@prefix-cls: ~'@{namespace}-basic-table-header-cell';
.@{prefix-cls} {
&__help {
margin-left: 8px;
color: rgb(0 0 0 / 65%) !important;
}
}
</style>

View File

@@ -0,0 +1,283 @@
<template>
<div :class="[prefixCls, getAlign]" @click="onCellClick">
<template v-for="(action, index) in getActions" :key="`${index}-${action.label}`">
<template v-if="action.slot">
<slot name="customButton"></slot>
</template>
<template v-else>
<Tooltip v-if="action.tooltip" v-bind="getTooltip(action.tooltip)">
<PopConfirmButton v-bind="action">
<Icon :icon="action.icon" :class="{ 'mr-1': !!action.label }" v-if="action.icon" />
<template v-if="action.label">{{ action.label }}</template>
</PopConfirmButton>
</Tooltip>
<PopConfirmButton v-else v-bind="action">
<Icon :icon="action.icon" :class="{ 'mr-1': !!action.label }" v-if="action.icon" />
<template v-if="action.label">{{ action.label }}</template>
</PopConfirmButton>
</template>
<Divider type="vertical" class="action-divider" v-if="divider && index < getActions.length - 1" />
</template>
<Dropdown
:overlayClassName="dropdownCls"
:trigger="['hover']"
:dropMenuList="getDropdownList"
popconfirm
v-if="dropDownActions && getDropdownList.length > 0"
>
<slot name="more"></slot>
<!-- 设置插槽 -->
<template v-slot:[item.slot] v-for="(item, index) in getDropdownSlotList" :key="`${index}-${item.label}`">
<slot :name="item.slot"></slot>
</template>
<a-button type="link" size="small" v-if="!$slots.more"> 更多 <Icon icon="mdi-light:chevron-down"></Icon> </a-button>
</Dropdown>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, computed, toRaw, unref } from 'vue';
import { MoreOutlined } from '@ant-design/icons-vue';
import { Divider, Tooltip, TooltipProps } from 'ant-design-vue';
import Icon from '/@/components/Icon/index';
import { ActionItem, TableActionType } from '/@/components/Table';
import { PopConfirmButton } from '/@/components/Button';
import { Dropdown } from '/@/components/Dropdown';
import { useDesign } from '/@/hooks/web/useDesign';
import { useTableContext } from '../hooks/useTableContext';
import { usePermission } from '/@/hooks/web/usePermission';
import { isBoolean, isFunction, isString } from '/@/utils/is';
import { propTypes } from '/@/utils/propTypes';
import { ACTION_COLUMN_FLAG } from '../const';
export default defineComponent({
name: 'TableAction',
components: { Icon, PopConfirmButton, Divider, Dropdown, MoreOutlined, Tooltip },
props: {
actions: {
type: Array as PropType<ActionItem[]>,
default: null,
},
dropDownActions: {
type: Array as PropType<ActionItem[]>,
default: null,
},
divider: propTypes.bool.def(true),
outside: propTypes.bool,
stopButtonPropagation: propTypes.bool.def(false),
},
setup(props) {
const { prefixCls } = useDesign('basic-table-action');
const dropdownCls = `${prefixCls}-dropdown`;
let table: Partial<TableActionType> = {};
if (!props.outside) {
table = useTableContext();
}
const { hasPermission } = usePermission();
function isIfShow(action: ActionItem): boolean {
const ifShow = action.ifShow;
let isIfShow = true;
if (isBoolean(ifShow)) {
isIfShow = ifShow;
}
if (isFunction(ifShow)) {
isIfShow = ifShow(action);
}
return isIfShow;
}
const getActions = computed(() => {
return (toRaw(props.actions) || [])
.filter((action) => {
return hasPermission(action.auth) && isIfShow(action);
})
.map((action) => {
const { popConfirm } = action;
// update-begin--author:liaozhiyang---date:20240105---for【issues/951】table删除记录时按钮显示错位
if (popConfirm) {
const overlayClassName = popConfirm.overlayClassName;
popConfirm.overlayClassName = `${overlayClassName ? overlayClassName : ''} ${prefixCls}-popconfirm`;
}
// update-end--author:liaozhiyang---date:20240105---for【issues/951】table删除记录时按钮显示错位
return {
getPopupContainer: () => unref((table as any)?.wrapRef.value) ?? document.body,
type: 'link',
size: 'small',
...action,
...(popConfirm || {}),
// update-begin--author:liaozhiyang---date:20240108---for【issues/936】表格操作栏删除当接口失败时气泡确认框不会消失
onConfirm: handelConfirm(popConfirm?.confirm),
// update-end--author:liaozhiyang---date:20240108---for【issues/936】表格操作栏删除当接口失败时气泡确认框不会消失
onCancel: popConfirm?.cancel,
enable: !!popConfirm,
};
});
});
const getDropdownList = computed((): any[] => {
//过滤掉隐藏的dropdown,避免出现多余的分割线
const list = (toRaw(props.dropDownActions) || []).filter((action) => {
return hasPermission(action.auth) && isIfShow(action);
});
return list.map((action, index) => {
const { label, popConfirm } = action;
// update-begin--author:liaozhiyang---date:20240105---for【issues/951】table删除记录时按钮显示错位
if (popConfirm) {
const overlayClassName = popConfirm.overlayClassName;
popConfirm.overlayClassName = `${overlayClassName ? overlayClassName : ''} ${prefixCls}-popconfirm`;
}
// update-end--author:liaozhiyang---date:20240105---for【issues/951】table删除记录时按钮显示错位
// update-begin--author:liaozhiyang---date:20240108---for【issues/936】表格操作栏删除当接口失败时气泡确认框不会消失
if (popConfirm) {
popConfirm.confirm = handelConfirm(popConfirm?.confirm);
}
// update-end--author:liaozhiyang---date:20240108---for【issues/936】表格操作栏删除当接口失败时气泡确认框不会消失
return {
...action,
...popConfirm,
onConfirm: handelConfirm(popConfirm?.confirm),
onCancel: popConfirm?.cancel,
text: label,
divider: index < list.length - 1 ? props.divider : false,
};
});
});
/*
2023-01-08
liaozhiyang
给传进来的函数包一层promise
*/
const handelConfirm = (fn) => {
if (typeof fn !== 'function') return fn;
const anyc = () => {
return new Promise<void>((resolve) => {
const result = fn();
if (Object.prototype.toString.call(result) === '[object Promise]') {
result
.finally(() => {
resolve();
})
.catch((err) => {
console.log(err);
});
} else {
resolve();
}
});
};
return anyc;
};
const getDropdownSlotList = computed((): any[] => {
return unref(getDropdownList).filter((item) => item.slot);
});
const getAlign = computed(() => {
const columns = (table as TableActionType)?.getColumns?.() || [];
const actionColumn = columns.find((item) => item.flag === ACTION_COLUMN_FLAG);
return actionColumn?.align ?? 'left';
});
function getTooltip(data: string | TooltipProps): TooltipProps {
return {
getPopupContainer: () => unref((table as any)?.wrapRef.value) ?? document.body,
placement: 'bottom',
...(isString(data) ? { title: data } : data),
};
}
function onCellClick(e: MouseEvent) {
if (!props.stopButtonPropagation) return;
const path = e.composedPath() as HTMLElement[];
const isInButton = path.find((ele) => {
return ele.tagName?.toUpperCase() === 'BUTTON';
});
isInButton && e.stopPropagation();
}
return { prefixCls, getActions, getDropdownList, getDropdownSlotList, getAlign, onCellClick, getTooltip, dropdownCls };
},
});
</script>
<style lang="less">
@prefix-cls: ~'@{namespace}-basic-table-action';
.@{prefix-cls} {
display: flex;
align-items: center;
/* update-begin-author:taoyan date:2022-11-18 for: 表格默认行高比官方示例多出2px*/
height: 22px;
/* update-end-author:taoyan date:2022-11-18 for: 表格默认行高比官方示例多出2px*/
.action-divider {
display: table;
}
&.left {
justify-content: flex-start;
}
&.center {
justify-content: center;
}
&.right {
justify-content: flex-end;
}
button {
display: flex;
align-items: center;
span {
margin-left: 0 !important;
}
}
button.ant-btn-circle {
span {
margin: auto !important;
}
}
.ant-divider,
.ant-divider-vertical {
margin: 0 2px;
}
.icon-more {
transform: rotate(90deg);
svg {
font-size: 1.1em;
font-weight: 700;
}
}
&-popconfirm {
.ant-popconfirm-buttons {
min-width: 120px;
// update-begin--author:liaozhiyang---date:20240124---for【issues/1019】popConfirm确认框待端后端返回过程中处理中样式错乱
display: flex;
align-items: center;
justify-content: center;
// update-end--author:liaozhiyang---date:20240124---for【issues/1019】popConfirm确认框待端后端返回过程中处理中样式错乱
}
}
// update-begin--author:liaozhiyang---date:20240407---for【QQYUN-8762】调整table操作栏ant-dropdown样式
&-dropdown {
.ant-dropdown-menu .ant-dropdown-menu-item-divider {
margin: 2px 0;
}
.ant-dropdown-menu .ant-dropdown-menu-item {
padding: 3px 8px;
font-size: 13.6px;
}
.dropdown-event-area {
padding: 0 !important;
}
}
// update-end--author:liaozhiyang---date:20240407---for【QQYUN-8762】调整table操作栏ant-dropdown样式
}
</style>

View File

@@ -0,0 +1,134 @@
<template>
<Table
v-if="summaryFunc || summaryData"
:showHeader="false"
:bordered="bordered"
:pagination="false"
:dataSource="getDataSource"
:rowKey="(r) => r[rowKey]"
:columns="getColumns"
tableLayout="fixed"
:scroll="scroll"
/>
</template>
<script lang="ts">
import type { PropType } from 'vue';
import { defineComponent, unref, computed, toRaw } from 'vue';
import { Table } from 'ant-design-vue';
import { cloneDeep } from 'lodash-es';
import { isFunction } from '/@/utils/is';
import type { BasicColumn } from '../types/table';
import { INDEX_COLUMN_FLAG } from '../const';
import { propTypes } from '/@/utils/propTypes';
import { useTableContext } from '../hooks/useTableContext';
const SUMMARY_ROW_KEY = '_row';
const SUMMARY_INDEX_KEY = '_index';
export default defineComponent({
name: 'BasicTableFooter',
components: { Table },
props: {
bordered: {
type: Boolean,
default: false,
},
summaryFunc: {
type: Function as PropType<Fn>,
},
summaryData: {
type: Array as PropType<Recordable[]>,
},
scroll: {
type: Object as PropType<Recordable>,
},
rowKey: propTypes.string.def('key'),
// 是否有展开列
hasExpandedRow: propTypes.bool,
},
setup(props) {
const table = useTableContext();
const getDataSource = computed((): Recordable[] => {
const { summaryFunc, summaryData } = props;
if (summaryData?.length) {
summaryData.forEach((item, i) => (item[props.rowKey] = `${i}`));
return summaryData;
}
if (!isFunction(summaryFunc)) {
return [];
}
// update-begin--author:liaozhiyang---date:20230227---for【QQYUN-8172】可编辑单元格编辑完以后不更新合计值
let dataSource = cloneDeep(unref(table.getDataSource()));
// update-end--author:liaozhiyang---date:20230227---for【QQYUN-8172】可编辑单元格编辑完以后不更新合计值
dataSource = summaryFunc(dataSource);
dataSource.forEach((item, i) => {
item[props.rowKey] = `${i}`;
});
return dataSource;
});
const getColumns = computed(() => {
const dataSource = unref(getDataSource);
let columns: BasicColumn[] = cloneDeep(table.getColumns());
// update-begin--author:liaozhiyang---date:220230804---for【issues/638】表格合计列自定义隐藏或展示时合计栏会错位
columns = columns.filter((item) => !item.defaultHidden);
// update-begin--author:liaozhiyang---date:220230804---for【issues/638】表格合计列自定义隐藏或展示时合计栏会错位
const index = columns.findIndex((item) => item.flag === INDEX_COLUMN_FLAG);
const hasRowSummary = dataSource.some((item) => Reflect.has(item, SUMMARY_ROW_KEY));
const hasIndexSummary = dataSource.some((item) => Reflect.has(item, SUMMARY_INDEX_KEY));
// 是否有序号列
let hasIndexCol = false;
// 是否有选择列
let hasSelection = table.getRowSelection() && hasRowSummary
if (index !== -1) {
if (hasIndexSummary) {
hasIndexCol = true;
columns[index].customRender = ({ record }) => record[SUMMARY_INDEX_KEY];
columns[index].ellipsis = false;
} else {
Reflect.deleteProperty(columns[index], 'customRender');
}
}
if (hasSelection) {
// update-begin--author:liaozhiyang---date:20231009---for【issues/776】显示100条/页复选框只能显示3个的问题(fixed也有可能设置true)
const isFixed = columns.some((col) => col.fixed === 'left' || col.fixed === true);
// update-begin--author:liaozhiyang---date:20231009---for【issues/776】显示100条/页复选框只能显示3个的问题(fixed也有可能设置true)
columns.unshift({
width: 50,
title: 'selection',
key: 'selectionKey',
align: 'center',
...(isFixed ? { fixed: 'left' } : {}),
customRender: ({ record }) => hasIndexCol ? '' : record[SUMMARY_ROW_KEY],
});
}
if (props.hasExpandedRow) {
const isFixed = columns.some((col) => col.fixed === 'left');
columns.unshift({
width: 50,
title: 'expandedRow',
key: 'expandedRowKey',
align: 'center',
...(isFixed ? { fixed: 'left' } : {}),
customRender: () => '',
});
}
return columns;
});
return { getColumns, getDataSource };
},
});
</script>
<style lang="less" scoped>
// update-begin--author:liaozhiyang---date:20231009---for【issues/776】显示100条/页复选框只能显示3个的问题(隐藏合计的滚动条)
.ant-table-wrapper {
:deep(.ant-table-body) {
overflow-x: hidden !important;
}
}
// update-end--author:liaozhiyang---date:20231009---for【issues/776】显示100条/页复选框只能显示3个的问题(隐藏合计的滚动条)
</style>

View File

@@ -0,0 +1,165 @@
<template>
<div style="width: 100%">
<div v-if="$slots.headerTop" style="margin: 5px">
<slot name="headerTop"></slot>
</div>
<div :class="`flex items-center ${prefixCls}__table-title-box`">
<div :class="`${prefixCls}__tableTitle`">
<slot name="tableTitle" v-if="$slots.tableTitle"></slot>
<!--修改标题插槽位置-->
<TableTitle :helpMessage="titleHelpMessage" :title="title" v-if="!$slots.tableTitle && title" />
</div>
<div :class="`${prefixCls}__toolbar`">
<slot name="toolbar"></slot>
<Divider type="vertical" v-if="$slots.toolbar && showTableSetting" />
<TableSetting :class="`${prefixCls}__toolbar-desktop`" style="white-space: nowrap;" :setting="tableSetting" v-if="showTableSetting" @columns-change="handleColumnChange" />
<a-popover :overlayClassName="`${prefixCls}__toolbar-mobile`" trigger="click" placement="left" :getPopupContainer="(n) => n?.parentElement">
<template #content>
<TableSetting mode="mobile" :setting="tableSetting" v-if="showTableSetting" @columns-change="handleColumnChange" />
</template>
<a-button :class="`${prefixCls}__toolbar-mobile`" v-if="showTableSetting" type="text" preIcon="ant-design:menu" shape="circle" />
</a-popover>
</div>
</div>
<!--添加tableTop插槽-->
<div style="margin: -4px 0 -2px; padding-top: 5px">
<slot name="tableTop">
<a-alert type="info" show-icon class="alert" v-if="openRowSelection != null">
<template #message>
<template v-if="selectRowKeys.length > 0">
<span>
<span>已选中 {{ selectRowKeys.length }} 条记录</span>
<span v-if="isAcrossPage">(可跨页)</span>
</span>
<a-divider type="vertical" />
<a @click="setSelectedRowKeys([])">清空</a>
<slot name="alertAfter" />
</template>
<template v-else>
<span>未选中任何数据</span>
</template>
</template>
</a-alert>
</slot>
</div>
<!--添加tableTop插槽-->
</div>
</template>
<script lang="ts">
import type { TableSetting, ColumnChangeParam } from '../types/table';
import type { PropType } from 'vue';
import { defineComponent, computed } from 'vue';
import { Divider } from 'ant-design-vue';
import TableSettingComponent from './settings/index.vue';
import TableTitle from './TableTitle.vue';
import { useDesign } from '/@/hooks/web/useDesign';
import { useTableContext } from '../hooks/useTableContext';
export default defineComponent({
name: 'BasicTableHeader',
components: {
Divider,
TableTitle,
TableSetting: TableSettingComponent,
},
props: {
title: {
type: [Function, String] as PropType<string | ((data: Recordable) => string)>,
},
tableSetting: {
type: Object as PropType<TableSetting>,
},
showTableSetting: {
type: Boolean,
},
titleHelpMessage: {
type: [String, Array] as PropType<string | string[]>,
default: '',
},
},
emits: ['columns-change'],
setup(_, { emit }) {
const { prefixCls } = useDesign('basic-table-header');
function handleColumnChange(data: ColumnChangeParam[]) {
emit('columns-change', data);
}
const { getSelectRowKeys, setSelectedRowKeys, getRowSelection } = useTableContext();
const selectRowKeys = computed(() => getSelectRowKeys());
const openRowSelection = computed(() => getRowSelection());
// 是否允许跨页选择
const isAcrossPage = computed(() => openRowSelection.value?.preserveSelectedRowKeys === true);
return { prefixCls, handleColumnChange, selectRowKeys, setSelectedRowKeys, openRowSelection, isAcrossPage };
},
});
</script>
<style lang="less">
@prefix-cls: ~'@{namespace}-basic-table-header';
.@{prefix-cls} {
&__toolbar {
//flex: 1;
width: 140px;
display: flex;
align-items: center;
justify-content: flex-end;
> * {
margin-right: 8px;
}
&-desktop {
display: block;
}
&-mobile {
display: none;
}
}
&__tableTitle {
flex: 1;
display: flex;
flex-wrap: wrap;
align-content: flex-start;
> * {
margin-right: 4px;
margin-bottom: 4px;
}
}
@media (max-width: @screen-lg) {
&__table-title-box {
align-items: flex-end;
}
&__toolbar {
width: 30px;
text-align: center;
> * {
margin-right: 0;
}
.table-settings > * {
margin-right: 0;
margin-bottom: 6px;
}
&-desktop {
display: none;
}
&-mobile {
display: block;
.table-settings > * {
margin-right: 6px;
margin-bottom: 0;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,76 @@
<template>
<div :class="prefixCls" class="flex items-center mx-auto" v-if="imgList && imgList.length" :style="getWrapStyle">
<Badge :count="!showBadge || imgList.length == 1 ? 0 : imgList.length" v-if="simpleShow">
<div class="img-div">
<PreviewGroup>
<template v-for="(img, index) in imgList" :key="img">
<Image
:width="size"
:style="{
display: index === 0 ? '' : 'none !important',
}"
:src="srcPrefix + img"
/>
</template>
</PreviewGroup>
</div>
</Badge>
<PreviewGroup v-else>
<template v-for="(img, index) in imgList" :key="img">
<Image :width="size" :style="{ marginLeft: index === 0 ? 0 : margin }" :src="srcPrefix + img" />
</template>
</PreviewGroup>
</div>
</template>
<script lang="ts">
import type { CSSProperties } from 'vue';
import { defineComponent, computed } from 'vue';
import { useDesign } from '/@/hooks/web/useDesign';
import { Image, Badge } from 'ant-design-vue';
import { propTypes } from '/@/utils/propTypes';
export default defineComponent({
name: 'TableImage',
components: { Image, PreviewGroup: Image.PreviewGroup, Badge },
props: {
imgList: propTypes.arrayOf(propTypes.string),
size: propTypes.number.def(40),
// 是否简单显示(只显示第一张图片)
simpleShow: propTypes.bool,
// 简单模式下是否显示图片数量的badge
showBadge: propTypes.bool.def(true),
// 图片间距
margin: propTypes.number.def(4),
// src前缀将会附加在imgList中每一项之前
srcPrefix: propTypes.string.def(''),
},
setup(props) {
const getWrapStyle = computed((): CSSProperties => {
const { size } = props;
const s = `${size}px`;
return { height: s, width: s };
});
const { prefixCls } = useDesign('basic-table-img');
return { prefixCls, getWrapStyle };
},
});
</script>
<style lang="less">
@prefix-cls: ~'@{namespace}-basic-table-img';
.@{prefix-cls} {
.ant-image {
margin-right: 4px;
cursor: zoom-in;
img {
border-radius: 2px;
}
}
.img-div {
display: inline-grid;
}
}
</style>

View File

@@ -0,0 +1,163 @@
import type { PropType, VNode } from 'vue';
import { defineComponent, unref, computed, isVNode } from 'vue';
import { cloneDeep, pick } from 'lodash-es';
import { isFunction } from '/@/utils/is';
import type { BasicColumn } from '../types/table';
import { INDEX_COLUMN_FLAG } from '../const';
import { propTypes } from '/@/utils/propTypes';
import { useTableContext } from '../hooks/useTableContext';
import { TableSummary, TableSummaryRow, TableSummaryCell } from 'ant-design-vue';
const SUMMARY_ROW_KEY = '_row';
const SUMMARY_INDEX_KEY = '_index';
export default defineComponent({
name: 'BasicTableSummary',
components: { TableSummary, TableSummaryRow, TableSummaryCell },
props: {
summaryFunc: {
type: Function as PropType<Fn>,
},
summaryData: {
type: Array as PropType<Recordable[]>,
},
rowKey: propTypes.string.def('key'),
// 是否有展开列
hasExpandedRow: propTypes.bool,
data: {
type: Object as PropType<Recordable>,
default: () => {},
},
},
setup(props) {
const table = useTableContext();
const getDataSource = computed((): Recordable[] => {
const {
summaryFunc,
summaryData,
data: { pageData },
} = props;
if (summaryData?.length) {
summaryData.forEach((item, i) => (item[props.rowKey] = `${i}`));
return summaryData;
}
if (!isFunction(summaryFunc)) {
return [];
}
let dataSource = cloneDeep(unref(pageData));
dataSource = summaryFunc(dataSource);
dataSource.forEach((item, i) => {
item[props.rowKey] = `${i}`;
});
return dataSource;
});
const getColumns = computed(() => {
const dataSource = unref(getDataSource);
let columns: BasicColumn[] = cloneDeep(table.getColumns({ sort: true }));
columns = columns.filter((item) => !item.defaultHidden);
const index = columns.findIndex((item) => item.flag === INDEX_COLUMN_FLAG);
const hasRowSummary = dataSource.some((item) => Reflect.has(item, SUMMARY_ROW_KEY));
const hasIndexSummary = dataSource.some((item) => Reflect.has(item, SUMMARY_INDEX_KEY));
// 是否有序号列
let hasIndexCol = false;
// 是否有选择列
const hasSelection = table.getRowSelection() && hasRowSummary;
if (index !== -1) {
if (hasIndexSummary) {
hasIndexCol = true;
columns[index].customSummaryRender = ({ record }) => record[SUMMARY_INDEX_KEY];
columns[index].ellipsis = false;
} else {
Reflect.deleteProperty(columns[index], 'customSummaryRender');
}
}
if (hasSelection) {
const isFixed = columns.some((col) => col.fixed === 'left' || col.fixed === true);
columns.unshift({
width: 60,
title: 'selection',
key: 'selectionKey',
align: 'center',
...(isFixed ? { fixed: 'left' } : {}),
customSummaryRender: ({ record }) => (hasIndexCol ? '' : record[SUMMARY_ROW_KEY]),
});
}
if (props.hasExpandedRow) {
const isFixed = columns.some((col) => col.fixed === 'left');
columns.unshift({
width: 50,
title: 'expandedRow',
key: 'expandedRowKey',
align: 'center',
...(isFixed ? { fixed: 'left' } : {}),
customSummaryRender: () => '',
});
}
return columns;
});
function isRenderCell(data: any) {
return data && typeof data === 'object' && !Array.isArray(data) && !isVNode(data);
}
const getValues = (row: Recordable, col: BasicColumn, index: number) => {
const value = row[col.dataIndex as string];
let childNode: VNode | JSX.Element | string | number | undefined | null;
childNode = value;
if (col.customSummaryRender) {
const renderData = col.customSummaryRender({
text: value,
value,
record: row,
index,
column: cloneDeep(col),
});
if (isRenderCell(renderData)) {
childNode = renderData.children;
} else {
childNode = renderData;
}
if (typeof childNode === 'object' && !Array.isArray(childNode) && !isVNode(childNode)) {
childNode = null;
}
if (Array.isArray(childNode) && childNode.length === 1) {
childNode = childNode[0];
}
return childNode;
}
return childNode;
};
const getCellProps = (col: BasicColumn) => {
const cellProps = pick(col, ['colSpan', 'rowSpan', 'align']);
return {
...cellProps,
};
};
return () => {
return (
<TableSummary fixed>
{(unref(getDataSource) || []).map((row) => {
return (
<TableSummaryRow key={row[props.rowKey]}>
{unref(getColumns).map((col, index) => {
return (
<TableSummaryCell {...getCellProps(col)} index={index} key={`${row[props.rowKey]}_${col.dataIndex}_${index}`}>
{getValues(row, col, index)}
</TableSummaryCell>
);
})}
</TableSummaryRow>
);
})}
</TableSummary>
);
};
},
});

View File

@@ -0,0 +1,53 @@
<template>
<BasicTitle :class="prefixCls" v-if="getTitle" :helpMessage="helpMessage">
{{ getTitle }}
</BasicTitle>
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue';
import { BasicTitle } from '/@/components/Basic/index';
import { useDesign } from '/@/hooks/web/useDesign';
import { isFunction } from '/@/utils/is';
export default defineComponent({
name: 'BasicTableTitle',
components: { BasicTitle },
props: {
title: {
type: [Function, String] as PropType<string | ((data: Recordable) => string)>,
},
getSelectRows: {
type: Function as PropType<() => Recordable[]>,
},
helpMessage: {
type: [String, Array] as PropType<string | string[]>,
},
},
setup(props) {
const { prefixCls } = useDesign('basic-table-title');
const getTitle = computed(() => {
const { title, getSelectRows = () => {} } = props;
let tit = title;
if (isFunction(title)) {
tit = title({
selectRows: getSelectRows(),
});
}
return tit;
});
return { getTitle, prefixCls };
},
});
</script>
<style lang="less">
@prefix-cls: ~'@{namespace}-basic-table-title';
.@{prefix-cls} {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

View File

@@ -0,0 +1,38 @@
import type { FunctionalComponent, defineComponent } from 'vue';
import type { ComponentType } from '../../types/componentType';
import { componentMap } from '/@/components/Table/src/componentMap';
import { Popover } from 'ant-design-vue';
import { h } from 'vue';
export interface ComponentProps {
component: ComponentType;
rule: boolean;
popoverVisible: boolean;
ruleMessage: string;
getPopupContainer?: Fn;
}
export const CellComponent: FunctionalComponent = (
{ component = 'Input', rule = true, ruleMessage, popoverVisible, getPopupContainer }: ComponentProps,
{ attrs }
) => {
const Comp = componentMap.get(component) as typeof defineComponent;
const DefaultComp = h(Comp, attrs);
if (!rule) {
return DefaultComp;
}
return h(
Popover,
{
overlayClassName: 'edit-cell-rule-popover',
open: !!popoverVisible,
...(getPopupContainer ? { getPopupContainer } : {}),
},
{
default: () => DefaultComp,
content: () => ruleMessage,
}
);
};

View File

@@ -0,0 +1,508 @@
<template>
<div :class="prefixCls">
<div v-show="!isEdit" :class="{ [`${prefixCls}__normal`]: true, 'ellipsis-cell': column.ellipsis }" @click="handleEdit">
<div class="cell-content" :title="column.ellipsis ? getValues ?? '' : ''">
<!-- update-begin--author:liaozhiyang---date:20240731---forissues/6957editableCell组件值长度为0无法编辑 -->
<!-- update-begin--author:liaozhiyang---date:20240709---forissues/6851editableCell组件值为0时不展示 -->
{{ typeof getValues === 'string' && getValues.length === 0 ? '&nbsp;' : getValues ?? '&nbsp;' }}
<!-- update-end--author:liaozhiyang---date:20240709---forissues/6851editableCell组件值为0时不展示 -->
<!-- update-end--author:liaozhiyang---date:20240731---forissues/6957editableCell组件值长度为0无法编辑 -->
</div>
<FormOutlined :class="`${prefixCls}__normal-icon`" v-if="!column.editRow" />
</div>
<a-spin v-if="isEdit" :spinning="spinning">
<div :class="`${prefixCls}__wrapper`" v-click-outside="onClickOutside">
<CellComponent
v-bind="getComponentProps"
:component="getComponent"
:style="getWrapperStyle"
:popoverVisible="getRuleVisible"
:rule="getRule"
:ruleMessage="ruleMessage"
:class="getWrapperClass"
ref="elRef"
@change="handleChange"
@options-change="handleOptionsChange"
@pressEnter="handleEnter"
/>
<div :class="`${prefixCls}__action`" v-if="!getRowEditable">
<CheckOutlined :class="[`${prefixCls}__icon`, 'mx-2']" @click="handleSubmitClick" />
<CloseOutlined :class="`${prefixCls}__icon `" @click="handleCancel" />
</div>
</div>
</a-spin>
</div>
</template>
<script lang="ts">
import type { CSSProperties, PropType } from 'vue';
import { computed, defineComponent, nextTick, ref, toRaw, unref, watchEffect } from 'vue';
import type { BasicColumn } from '../../types/table';
import type { EditRecordRow } from './index';
import { CheckOutlined, CloseOutlined, FormOutlined } from '@ant-design/icons-vue';
import { CellComponent } from './CellComponent';
import { useDesign } from '/@/hooks/web/useDesign';
import { useTableContext } from '../../hooks/useTableContext';
import clickOutside from '/@/directives/clickOutside';
import { propTypes } from '/@/utils/propTypes';
import { isArray, isBoolean, isFunction, isNumber, isString } from '/@/utils/is';
import { createPlaceholderMessage } from './helper';
import { omit, pick, set } from 'lodash-es';
import { treeToList } from '/@/utils/helper/treeHelper';
import { Spin } from 'ant-design-vue';
export default defineComponent({
name: 'EditableCell',
components: { FormOutlined, CloseOutlined, CheckOutlined, CellComponent, ASpin: Spin },
directives: {
clickOutside,
},
props: {
value: {
type: [String, Number, Boolean, Object] as PropType<string | number | boolean | Recordable>,
default: '',
},
record: {
type: Object as PropType<EditRecordRow>,
},
column: {
type: Object as PropType<BasicColumn>,
default: () => ({}),
},
index: propTypes.number,
},
setup(props) {
const table = useTableContext();
const isEdit = ref(false);
const elRef = ref();
const ruleVisible = ref(false);
const ruleMessage = ref('');
const optionsRef = ref<LabelValueOptions>([]);
const currentValueRef = ref<any>(props.value);
const defaultValueRef = ref<any>(props.value);
const spinning = ref<boolean>(false);
const { prefixCls } = useDesign('editable-cell');
const getComponent = computed(() => props.column?.editComponent || 'Input');
const getRule = computed(() => props.column?.editRule);
const getRuleVisible = computed(() => {
return unref(ruleMessage) && unref(ruleVisible);
});
const getIsCheckComp = computed(() => {
const component = unref(getComponent);
return ['Checkbox', 'Switch'].includes(component);
});
const getComponentProps = computed(() => {
const compProps = props.column?.editComponentProps ?? {};
const component = unref(getComponent);
const apiSelectProps: Recordable = {};
if (component === 'ApiSelect') {
apiSelectProps.cache = true;
}
const isCheckValue = unref(getIsCheckComp);
const valueField = isCheckValue ? 'checked' : 'value';
const val = unref(currentValueRef);
const value = isCheckValue ? (isNumber(val) && isBoolean(val) ? val : !!val) : val;
return {
size: 'small',
getPopupContainer: () => unref(table?.wrapRef.value) ?? document.body,
getCalendarContainer: () => unref(table?.wrapRef.value) ?? document.body,
placeholder: createPlaceholderMessage(unref(getComponent)),
...apiSelectProps,
...omit(compProps, 'onChange'),
[valueField]: value,
};
});
const getValues = computed(() => {
const { editComponentProps, editValueMap } = props.column;
const value = unref(currentValueRef);
if (editValueMap && isFunction(editValueMap)) {
return editValueMap(value);
}
const component = unref(getComponent);
if (!component.includes('Select')) {
return value;
}
const options: LabelValueOptions = editComponentProps?.options ?? (unref(optionsRef) || []);
const option = options.find((item) => `${item.value}` === `${value}`);
return option?.label ?? value;
});
const getWrapperStyle = computed((): CSSProperties => {
if (unref(getIsCheckComp) || unref(getRowEditable)) {
return {};
}
return {
width: 'calc(100% - 48px)',
};
});
const getWrapperClass = computed(() => {
const { align = 'center' } = props.column;
return `edit-cell-align-${align}`;
});
const getRowEditable = computed(() => {
const { editable } = props.record || {};
return !!editable;
});
watchEffect(() => {
defaultValueRef.value = props.value;
currentValueRef.value = props.value;
});
watchEffect(() => {
const { editable } = props.column;
if (isBoolean(editable) || isBoolean(unref(getRowEditable))) {
isEdit.value = !!editable || unref(getRowEditable);
}
});
function handleEdit() {
if (unref(getRowEditable) || unref(props.column?.editRow)) return;
ruleMessage.value = '';
isEdit.value = true;
nextTick(() => {
const el = unref(elRef);
el?.focus?.();
});
}
async function handleChange(e: any) {
const component = unref(getComponent);
if (!e) {
currentValueRef.value = e;
} else if (e?.target && Reflect.has(e.target, 'value')) {
currentValueRef.value = (e as ChangeEvent).target.value;
} else if (component === 'Checkbox') {
currentValueRef.value = (e as ChangeEvent).target.checked;
} else if (isString(e) || isBoolean(e) || isNumber(e) || isArray(e)) {
currentValueRef.value = e;
}
const onChange = props.column?.editComponentProps?.onChange;
if (onChange && isFunction(onChange)) onChange(...arguments);
table.emit?.('edit-change', {
column: props.column,
value: unref(currentValueRef),
record: toRaw(props.record),
});
handleSubmiRule();
}
async function handleSubmiRule() {
const { column, record } = props;
const { editRule } = column;
const currentValue = unref(currentValueRef);
if (editRule) {
if (isBoolean(editRule) && !currentValue && !isNumber(currentValue)) {
ruleVisible.value = true;
const component = unref(getComponent);
ruleMessage.value = createPlaceholderMessage(component);
return false;
}
if (isFunction(editRule)) {
const res = await editRule(currentValue, record as Recordable);
if (!!res) {
ruleMessage.value = res;
ruleVisible.value = true;
return false;
} else {
ruleMessage.value = '';
return true;
}
}
}
ruleMessage.value = '';
return true;
}
async function handleSubmit(needEmit = true, valid = true) {
if (valid) {
const isPass = await handleSubmiRule();
if (!isPass) return false;
}
const { column, index, record } = props;
if (!record) return false;
const { key, dataIndex } = column;
const value = unref(currentValueRef);
if (!key || !dataIndex) return;
const dataKey = (dataIndex || key) as string;
if (!record.editable) {
const { getBindValues } = table;
const { beforeEditSubmit, columns } = unref(getBindValues);
if (beforeEditSubmit && isFunction(beforeEditSubmit)) {
spinning.value = true;
const keys: string[] = columns.map((_column) => _column.dataIndex).filter((field) => !!field) as string[];
let result: any = true;
try {
result = await beforeEditSubmit({
record: pick(record, keys),
index,
key,
value,
});
} catch (e) {
result = false;
} finally {
spinning.value = false;
}
if (result === false) {
return;
}
}
}
set(record, dataKey, value);
//const record = await table.updateTableData(index, dataKey, value);
needEmit && table.emit?.('edit-end', { record, index, key, value });
isEdit.value = false;
}
async function handleEnter() {
if (props.column?.editRow) {
return;
}
handleSubmit();
}
function handleSubmitClick() {
handleSubmit();
}
function handleCancel() {
isEdit.value = false;
currentValueRef.value = defaultValueRef.value;
const { column, index, record } = props;
const { key, dataIndex } = column;
table.emit?.('edit-cancel', {
record,
index,
key: dataIndex || key,
value: unref(currentValueRef),
});
}
function onClickOutside() {
if (props.column?.editable || unref(getRowEditable)) {
return;
}
const component = unref(getComponent);
if (component.includes('Input')) {
handleCancel();
}
}
// only ApiSelect or TreeSelect
function handleOptionsChange(options: LabelValueOptions) {
const { replaceFields } = props.column?.editComponentProps ?? {};
const component = unref(getComponent);
if (component === 'ApiTreeSelect') {
const { title = 'title', value = 'value', children = 'children' } = replaceFields || {};
let listOptions: Recordable[] = treeToList(options, { children });
listOptions = listOptions.map((item) => {
return {
label: item[title],
value: item[value],
};
});
optionsRef.value = listOptions as LabelValueOptions;
} else {
optionsRef.value = options;
}
}
function initCbs(cbs: 'submitCbs' | 'validCbs' | 'cancelCbs', handle: Fn) {
if (props.record) {
/* eslint-disable */
// update-begin--author:liaozhiyang---date:20240424---for【issues/1165】解决canResize为true时第一行校验不过
const { dataIndex, key } = props.column;
const field: any = dataIndex || key;
if (isArray(props.record[cbs])) {
const findItem = props.record[cbs]?.find((item) => item[field]);
if (findItem) {
findItem[field] = handle;
} else {
props.record[cbs]?.push({ [field]: handle });
}
} else {
props.record[cbs] = [{ [field]: handle }];
}
// update-end--author:liaozhiyang---date:20240424---for【issues/1165】解决canResize为true时第一行校验不过
}
}
if (props.record) {
initCbs('submitCbs', handleSubmit);
initCbs('validCbs', handleSubmiRule);
initCbs('cancelCbs', handleCancel);
if (props.column.dataIndex) {
if (!props.record.editValueRefs) props.record.editValueRefs = {};
props.record.editValueRefs[props.column.dataIndex] = currentValueRef;
}
/* eslint-disable */
props.record.onCancelEdit = () => {
// update-begin--author:liaozhiyang---date:20240424---for【issues/1165】解决canResize为true时第一行校验不过
isArray(props.record?.cancelCbs) &&
props.record?.cancelCbs.forEach((item) => {
const [fn] = Object.values(item);
fn();
});
// update-end--author:liaozhiyang---date:20240424---for【issues/1165】解决canResize为true时第一行校验不过
};
/* eslint-disable */
props.record.onSubmitEdit = async () => {
if (isArray(props.record?.submitCbs)) {
if (!props.record?.onValid?.()) return;
const submitFns = props.record?.submitCbs || [];
// update-begin--author:liaozhiyang---date:20240424---for【issues/1165】解决canResize为true时第一行校验不过
submitFns.forEach((item) => {
const [fn] = Object.values(item);
fn(false, false);
});
// update-end--author:liaozhiyang---date:20240424---for【issues/1165】解决canResize为true时第一行校验不过
table.emit?.('edit-row-end');
return true;
}
};
}
return {
isEdit,
prefixCls,
handleEdit,
currentValueRef,
handleSubmit,
handleChange,
handleCancel,
elRef,
getComponent,
getRule,
onClickOutside,
ruleMessage,
getRuleVisible,
getComponentProps,
handleOptionsChange,
getWrapperStyle,
getWrapperClass,
getRowEditable,
getValues,
handleEnter,
handleSubmitClick,
spinning,
};
},
});
</script>
<style lang="less">
@prefix-cls: ~'@{namespace}-editable-cell';
.edit-cell-align-left {
text-align: left;
input:not(.ant-calendar-picker-input, .ant-time-picker-input) {
text-align: left;
}
}
.edit-cell-align-center {
text-align: center;
input:not(.ant-calendar-picker-input, .ant-time-picker-input) {
text-align: center;
}
}
.edit-cell-align-right {
text-align: right;
input:not(.ant-calendar-picker-input, .ant-time-picker-input) {
text-align: right;
}
}
.edit-cell-rule-popover {
.ant-popover-inner-content {
padding: 4px 8px;
color: @error-color;
// border: 1px solid @error-color;
border-radius: 2px;
}
}
.@{prefix-cls} {
position: relative;
&__wrapper {
display: flex;
align-items: center;
justify-content: center;
> .ant-select {
min-width: calc(100% - 50px);
}
}
&__icon {
&:hover {
transform: scale(1.2);
svg {
color: @primary-color;
}
}
}
.ellipsis-cell {
.cell-content {
overflow-wrap: break-word;
word-break: break-word;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
&__normal {
&-icon {
position: absolute;
top: 4px;
right: 0;
display: none;
width: 20px;
cursor: pointer;
}
}
&:hover {
.@{prefix-cls}__normal-icon {
display: inline-block;
}
}
}
</style>

View File

@@ -0,0 +1,28 @@
import { ComponentType } from '../../types/componentType';
import { useI18n } from '/@/hooks/web/useI18n';
const { t } = useI18n();
/**
* @description: 生成placeholder
*/
export function createPlaceholderMessage(component: ComponentType) {
if (component.includes('Input')) {
return t('common.inputText');
}
if (component.includes('Picker')) {
return t('common.chooseText');
}
if (
component.includes('Select') ||
component.includes('Checkbox') ||
component.includes('Radio') ||
component.includes('Switch') ||
component.includes('DatePicker') ||
component.includes('TimePicker')
) {
return t('common.chooseText');
}
return '';
}

View File

@@ -0,0 +1,78 @@
import type { BasicColumn } from '/@/components/Table/src/types/table';
import { h, Ref, toRaw } from 'vue';
import EditableCell from './EditableCell.vue';
import { isArray } from '/@/utils/is';
interface Params {
text: string;
record: Recordable;
index: number;
}
export function renderEditCell(column: BasicColumn) {
return ({ text: value, record, index }: Params) => {
toRaw(record).onValid = async () => {
if (isArray(record?.validCbs)) {
// update-begin--author:liaozhiyang---date:20240424---for【issues/1165】解决canResize为true时第一行校验不过
const validFns = (record?.validCbs || []).map((item) => {
const [fn] = Object.values(item);
// @ts-ignore
return fn();
});
// update-end--author:liaozhiyang---date:20240424---for【issues/1165】解决canResize为true时第一行校验不过
const res = await Promise.all(validFns);
return res.every((item) => !!item);
} else {
return false;
}
};
toRaw(record).onEdit = async (edit: boolean, submit = false) => {
if (!submit) {
record.editable = edit;
}
if (!edit && submit) {
if (!(await record.onValid())) return false;
const res = await record.onSubmitEdit?.();
if (res) {
record.editable = false;
return true;
}
return false;
}
// cancel
if (!edit && !submit) {
record.onCancelEdit?.();
}
return true;
};
return h(EditableCell, {
value,
record,
column,
index,
});
};
}
interface Cbs {
[key: string]: Fn;
}
export type EditRecordRow<T = Recordable> = Partial<
{
onEdit: (editable: boolean, submit?: boolean) => Promise<boolean>;
onValid: () => Promise<boolean>;
editable: boolean;
onCancel: Fn;
onSubmit: Fn;
submitCbs: Cbs[];
cancelCbs: Cbs[];
validCbs: Cbs[];
editValueRefs: Recordable<Ref>;
} & T
>;

View File

@@ -0,0 +1,536 @@
<template>
<Tooltip placement="top" v-bind="getBindProps" >
<template #title>
<span>{{ t('component.table.settingColumn') }}</span>
</template>
<Popover
v-model:open="popoverVisible"
placement="bottomLeft"
trigger="click"
@open-change="handleVisibleChange"
:overlayClassName="`${prefixCls}__cloumn-list`"
:getPopupContainer="getPopupContainer"
>
<template #title>
<div :class="`${prefixCls}__popover-title`">
<Checkbox :indeterminate="indeterminate" v-model:checked="checkAll" @change="onCheckAllChange">
{{ t('component.table.settingColumnShow') }}
</Checkbox>
<Checkbox v-model:checked="checkIndex" @change="handleIndexCheckChange">
{{ t('component.table.settingIndexColumnShow') }}
</Checkbox>
<!-- <Checkbox-->
<!-- v-model:checked="checkSelect"-->
<!-- @change="handleSelectCheckChange"-->
<!-- :disabled="!defaultRowSelection"-->
<!-- >-->
<!-- {{ t('component.table.settingSelectColumnShow') }}-->
<!-- </Checkbox>-->
</div>
</template>
<template #content>
<ScrollContainer>
<CheckboxGroup v-model:value="checkedList" @change="onChange" ref="columnListRef">
<template v-for="item in plainOptions" :key="item.value">
<div :class="`${prefixCls}__check-item`" v-if="!('ifShow' in item && !item.ifShow)">
<DragOutlined class="table-column-drag-icon" />
<Checkbox :value="item.value">
{{ item.label }}
</Checkbox>
<Tooltip placement="bottomLeft" :mouseLeaveDelay="0.4" :getPopupContainer="getPopupContainer">
<template #title>
{{ t('component.table.settingFixedLeft') }}
</template>
<Icon
icon="line-md:arrow-align-left"
:class="[
`${prefixCls}__fixed-left`,
{
active: item.fixed === 'left',
disabled: !checkedList.includes(item.value),
},
]"
@click="handleColumnFixed(item, 'left')"
/>
</Tooltip>
<Divider type="vertical" />
<Tooltip placement="bottomLeft" :mouseLeaveDelay="0.4" :getPopupContainer="getPopupContainer">
<template #title>
{{ t('component.table.settingFixedRight') }}
</template>
<Icon
icon="line-md:arrow-align-left"
:class="[
`${prefixCls}__fixed-right`,
{
active: item.fixed === 'right',
disabled: !checkedList.includes(item.value),
},
]"
@click="handleColumnFixed(item, 'right')"
/>
</Tooltip>
</div>
</template>
</CheckboxGroup>
</ScrollContainer>
<div :class="`${prefixCls}__popover-footer`">
<a-button size="small" @click="reset">
{{ t('common.resetText') }}
</a-button>
<a-button size="small" type="primary" @click="saveSetting"> 保存 </a-button>
</div>
</template>
<SettingOutlined />
</Popover>
</Tooltip>
</template>
<script lang="ts">
import type { BasicColumn, ColumnChangeParam } from '../../types/table';
import { defineComponent, ref, reactive, toRefs, watchEffect, nextTick, unref, computed } from 'vue';
import { Tooltip, Popover, Checkbox, Divider } from 'ant-design-vue';
import type { CheckboxChangeEvent } from 'ant-design-vue/lib/checkbox/interface';
import { SettingOutlined, DragOutlined } from '@ant-design/icons-vue';
import { Icon } from '/@/components/Icon';
import { ScrollContainer } from '/@/components/Container';
import { useI18n } from '/@/hooks/web/useI18n';
import { useTableContext } from '../../hooks/useTableContext';
import { useColumnsCache } from '../../hooks/useColumnsCache';
import { useDesign } from '/@/hooks/web/useDesign';
// import { useSortable } from '/@/hooks/web/useSortable';
import { isFunction, isNullAndUnDef } from '/@/utils/is';
import { getPopupContainer as getParentContainer } from '/@/utils';
import { cloneDeep, omit } from 'lodash-es';
import Sortablejs from 'sortablejs';
import type Sortable from 'sortablejs';
interface State {
checkAll: boolean;
isInit?: boolean;
checkedList: string[];
defaultCheckList: string[];
}
interface Options {
label: string;
value: string;
fixed?: boolean | 'left' | 'right';
}
export default defineComponent({
name: 'ColumnSetting',
props: {
isMobile: Boolean,
},
components: {
SettingOutlined,
Popover,
Tooltip,
Checkbox,
CheckboxGroup: Checkbox.Group,
DragOutlined,
ScrollContainer,
Divider,
Icon,
},
emits: ['columns-change'],
setup(props, { emit, attrs }) {
const { t } = useI18n();
const table = useTableContext();
const popoverVisible = ref(false);
// update-begin--author:sunjianlei---date:20221101---for: 修复第一次进入时列表配置不能拖拽
// nextTick(() => popoverVisible.value = false);
// update-end--author:sunjianlei---date:20221101---for: 修复第一次进入时列表配置不能拖拽
const defaultRowSelection = omit(table.getRowSelection(), 'selectedRowKeys');
let inited = false;
const cachePlainOptions = ref<Options[]>([]);
const plainOptions = ref<Options[] | any>([]);
const plainSortOptions = ref<Options[]>([]);
const columnListRef = ref<ComponentRef>(null);
const state = reactive<State>({
checkAll: true,
checkedList: [],
defaultCheckList: [],
});
const checkIndex = ref(false);
const checkSelect = ref(false);
const { prefixCls } = useDesign('basic-column-setting');
const getValues = computed(() => {
return unref(table?.getBindValues) || {};
});
const getBindProps = computed(() => {
let obj = {};
if (props.isMobile) {
obj['open'] = false;
}
return obj;
});
let sortable: Sortable;
const sortableOrder = ref<string[]>();
// 列表字段配置缓存
const { saveSetting, resetSetting } = useColumnsCache(
{
state,
popoverVisible,
plainOptions,
plainSortOptions,
sortableOrder,
checkIndex,
},
setColumns,
handleColumnFixed
);
watchEffect(() => {
setTimeout(() => {
const columns = table.getColumns();
if (columns.length && !state.isInit) {
init();
}
}, 0);
});
watchEffect(() => {
const values = unref(getValues);
checkIndex.value = !!values.showIndexColumn;
checkSelect.value = !!values.rowSelection;
});
function getColumns() {
const ret: Options[] = [];
table.getColumns({ ignoreIndex: true, ignoreAction: true }).forEach((item) => {
ret.push({
label: (item.title as string) || (item.customTitle as string),
value: (item.dataIndex || item.title) as string,
...item,
});
});
return ret;
}
function init() {
const columns = getColumns();
const checkList = table
.getColumns({ ignoreAction: true })
.map((item) => {
if (item.defaultHidden) {
return '';
}
return item.dataIndex || item.title;
})
.filter(Boolean) as string[];
if (!plainOptions.value.length) {
plainOptions.value = columns;
plainSortOptions.value = columns;
cachePlainOptions.value = columns;
state.defaultCheckList = checkList;
} else {
// const fixedColumns = columns.filter((item) =>
// Reflect.has(item, 'fixed')
// ) as BasicColumn[];
unref(plainOptions).forEach((item: BasicColumn) => {
const findItem = columns.find((col: BasicColumn) => col.dataIndex === item.dataIndex);
if (findItem) {
item.fixed = findItem.fixed;
}
});
}
state.isInit = true;
state.checkedList = checkList;
}
// checkAll change
function onCheckAllChange(e: CheckboxChangeEvent) {
const checkList = plainOptions.value.map((item) => item.value);
if (e.target.checked) {
state.checkedList = checkList;
setColumns(checkList);
} else {
state.checkedList = [];
setColumns([]);
}
}
const indeterminate = computed(() => {
const len = plainOptions.value.length;
let checkedLen = state.checkedList.length;
unref(checkIndex) && checkedLen--;
return checkedLen > 0 && checkedLen < len;
});
// Trigger when check/uncheck a column
function onChange(checkedList: string[]) {
const len = plainSortOptions.value.length;
state.checkAll = checkedList.length === len;
const sortList = unref(plainSortOptions).map((item) => item.value);
checkedList.sort((prev, next) => {
return sortList.indexOf(prev) - sortList.indexOf(next);
});
setColumns(checkedList);
}
// reset columns
function reset() {
// state.checkedList = [...state.defaultCheckList];
// update-begin--author:liaozhiyang---date:20231103---for【issues/825】tabel的列设置隐藏列保存后切换路由问题[重置没勾选]
state.checkedList = table
.getColumns({ ignoreAction: true })
.map((item) => {
return item.dataIndex || item.title;
})
.filter(Boolean) as string[];
// update-end--author:liaozhiyang---date:20231103---for【issues/825】tabel的列设置隐藏列保存后切换路由问题[重置没勾选]
state.checkAll = true;
plainOptions.value = unref(cachePlainOptions);
plainSortOptions.value = unref(cachePlainOptions);
setColumns(table.getCacheColumns());
if (sortableOrder.value) {
sortable.sort(sortableOrder.value);
}
resetSetting();
}
// Open the pop-up window for drag and drop initialization
function handleVisibleChange() {
if (inited) return;
// update-begin--author:liaozhiyang---date:20240529---for【TV360X-254】列设置闪现及苹果浏览器弹窗过长
setTimeout(() => {
// update-begin--author:liaozhiyang---date:20240529---for【TV360X-254】列设置闪现及苹果浏览器弹窗过长
const columnListEl = unref(columnListRef);
if (!columnListEl) return;
const el = columnListEl.$el as any;
if (!el) return;
// Drag and drop sort
sortable = Sortablejs.create(unref(el), {
animation: 500,
delay: 400,
delayOnTouchOnly: true,
handle: '.table-column-drag-icon ',
onEnd: (evt) => {
const { oldIndex, newIndex } = evt;
if (isNullAndUnDef(oldIndex) || isNullAndUnDef(newIndex) || oldIndex === newIndex) {
return;
}
// Sort column
const columns = cloneDeep(plainSortOptions.value);
if (oldIndex > newIndex) {
columns.splice(newIndex, 0, columns[oldIndex]);
columns.splice(oldIndex + 1, 1);
} else {
columns.splice(newIndex + 1, 0, columns[oldIndex]);
columns.splice(oldIndex, 1);
}
plainSortOptions.value = columns;
// update-begin--author:liaozhiyang---date:20230904---for【QQYUN-6424】table字段列表设置不显示后再拖拽字段顺序原本不显示的又显示了
// update-begin--author:liaozhiyang---date:20240522---for【TV360X-108】刷新后勾选之前未勾选的字段拖拽之后该字段对应的表格列消失了
const cols = columns.map((item) => item.value);
const arr = cols.filter((cItem) => state.checkedList.find((lItem) => lItem === cItem));
setColumns(arr);
// 最开始的代码
// setColumns(columns);
// update-end--author:liaozhiyang---date:20240522---for【TV360X-108】刷新后勾选之前未勾选的字段拖拽之后该字段对应的表格列消失了
// update-end--author:liaozhiyang---date:20230904---for【QQYUN-6424】table字段列表设置不显示后再拖拽字段顺序原本不显示的又显示了
},
});
// 记录原始 order 序列
if (!sortableOrder.value) {
sortableOrder.value = sortable.toArray();
}
inited = true;
}, 2000);
}
// Control whether the serial number column is displayed
function handleIndexCheckChange(e: CheckboxChangeEvent) {
table.setProps({
showIndexColumn: e.target.checked,
});
}
// Control whether the check box is displayed
function handleSelectCheckChange(e: CheckboxChangeEvent) {
table.setProps({
rowSelection: e.target.checked ? defaultRowSelection : undefined,
});
}
function handleColumnFixed(item: BasicColumn, fixed?: 'left' | 'right') {
if (!state.checkedList.includes(item.dataIndex as string)) return;
const columns = getColumns() as BasicColumn[];
const isFixed = item.fixed === fixed ? false : fixed;
const index = columns.findIndex((col) => col.dataIndex === item.dataIndex);
if (index !== -1) {
columns[index].fixed = isFixed;
}
item.fixed = isFixed;
if (isFixed && !item.width) {
item.width = 100;
}
table.setCacheColumnsByField?.(item.dataIndex as string, { fixed: isFixed });
setColumns(columns);
}
function setColumns(columns: BasicColumn[] | string[]) {
table.setColumns(columns);
const data: ColumnChangeParam[] = unref(plainSortOptions).map((col) => {
const visible =
columns.findIndex((c: BasicColumn | string) => c === col.value || (typeof c !== 'string' && c.dataIndex === col.value)) !== -1;
return { dataIndex: col.value, fixed: col.fixed, visible };
});
emit('columns-change', data);
}
function getPopupContainer() {
return isFunction(attrs.getPopupContainer) ? attrs.getPopupContainer() : getParentContainer();
}
return {
getBindProps,
t,
...toRefs(state),
popoverVisible,
indeterminate,
onCheckAllChange,
onChange,
plainOptions,
reset,
saveSetting,
prefixCls,
columnListRef,
handleVisibleChange,
checkIndex,
checkSelect,
handleIndexCheckChange,
handleSelectCheckChange,
defaultRowSelection,
handleColumnFixed,
getPopupContainer,
};
},
});
</script>
<style lang="less">
@prefix-cls: ~'@{namespace}-basic-column-setting';
.table-column-drag-icon {
margin: 0 5px;
cursor: move;
}
.@{prefix-cls} {
&__popover-title {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
}
/* 卡片底部样式 */
&__popover-footer {
position: relative;
top: 7px;
text-align: right;
padding: 4px 0 0;
border-top: 1px solid #f0f0f0;
.ant-btn {
margin-right: 6px;
}
}
&__check-item {
display: flex;
align-items: center;
min-width: 100%;
padding: 4px 16px 8px 0;
.ant-checkbox-wrapper {
width: 100%;
&:hover {
color: @primary-color;
}
}
}
&__fixed-left,
&__fixed-right {
color: rgba(0, 0, 0, 0.45);
cursor: pointer;
&.active,
&:hover {
color: @primary-color;
}
&.disabled {
color: @disabled-color;
cursor: not-allowed;
}
}
&__fixed-right {
transform: rotate(180deg);
}
&__cloumn-list {
svg {
width: 1em !important;
height: 1em !important;
}
.ant-popover-inner-content {
// max-height: 360px;
padding-right: 0;
padding-left: 0;
// overflow: auto;
}
.ant-checkbox-group {
// update-begin--author:liaozhiyang---date:20240118---for【QQYUN-7887】表格列设置宽度过长
// width: 100%;
min-width: 260px;
max-width: min-content;
// update-end--author:liaozhiyang---date:20240118---for【QQYUN-7887】表格列设置宽度过长
// flex-wrap: wrap;
}
// update-begin--author:liaozhiyang---date:20240529---for【TV360X-254】列设置闪现及苹果浏览器弹窗过长
&.ant-popover,
.ant-popover-content,
.ant-popover-inner,
.ant-popover-inner-content,
.scroll-container,
.scrollbar__wrap {
max-width: min-content;
}
// update-end--author:liaozhiyang---date:20240529---for【TV360X-254】列设置闪现及苹果浏览器弹窗过长
.scrollbar {
height: 220px;
}
}
}
</style>

View File

@@ -0,0 +1,48 @@
<template>
<Tooltip placement="top" v-bind="getBindProps">
<template #title>
<span>{{ t('component.table.settingFullScreen') }}</span>
</template>
<FullscreenOutlined @click="toggle" v-if="!isFullscreen" />
<FullscreenExitOutlined @click="toggle" v-else />
</Tooltip>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue';
import { Tooltip } from 'ant-design-vue';
import { FullscreenOutlined, FullscreenExitOutlined } from '@ant-design/icons-vue';
import { useFullscreen } from '@vueuse/core';
import { useI18n } from '/@/hooks/web/useI18n';
import { useTableContext } from '../../hooks/useTableContext';
export default defineComponent({
name: 'FullScreenSetting',
props: {
isMobile: Boolean,
},
components: {
FullscreenExitOutlined,
FullscreenOutlined,
Tooltip,
},
setup(props) {
const table = useTableContext();
const { t } = useI18n();
const { toggle, isFullscreen } = useFullscreen(table.wrapRef);
const getBindProps = computed(() => {
let obj = {};
if (props.isMobile) {
obj['visible'] = false;
}
return obj;
});
return {
getBindProps,
toggle,
isFullscreen,
t,
};
},
});
</script>

View File

@@ -0,0 +1,45 @@
<template>
<Tooltip placement="top" v-bind="getBindProps">
<template #title>
<span>{{ t('common.redo') }}</span>
</template>
<RedoOutlined @click="redo" />
</Tooltip>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue';
import { Tooltip } from 'ant-design-vue';
import { RedoOutlined } from '@ant-design/icons-vue';
import { useI18n } from '/@/hooks/web/useI18n';
import { useTableContext } from '../../hooks/useTableContext';
export default defineComponent({
name: 'RedoSetting',
props: {
isMobile: Boolean,
},
components: {
RedoOutlined,
Tooltip,
},
setup(props) {
const table = useTableContext();
const { t } = useI18n();
const getBindProps = computed(() => {
let obj = {};
if (props.isMobile) {
obj['visible'] = false;
}
return obj;
});
function redo() {
table.reload();
table.emit!('table-redo');
}
return { getBindProps, redo, t };
},
});
</script>

View File

@@ -0,0 +1,99 @@
<template>
<Tooltip placement="top" v-bind="getBindProps">
<template #title>
<span>{{ t('component.table.settingDens') }}</span>
</template>
<Dropdown placement="bottom" :trigger="['click']" :getPopupContainer="getPopupContainer">
<ColumnHeightOutlined />
<template #overlay>
<Menu @click="handleTitleClick" selectable v-model:selectedKeys="selectedKeysRef">
<MenuItem key="large">
<span>{{ t('component.table.settingDensLarge') }}</span>
</MenuItem>
<MenuItem key="middle">
<span>{{ t('component.table.settingDensMiddle') }}</span>
</MenuItem>
<MenuItem key="small">
<span>{{ t('component.table.settingDensSmall') }}</span>
</MenuItem>
</Menu>
</template>
</Dropdown>
</Tooltip>
</template>
<script lang="ts">
import type { SizeType } from '../../types/table';
import { computed, defineComponent, ref } from 'vue';
import { Tooltip, Dropdown, Menu } from 'ant-design-vue';
import { ColumnHeightOutlined } from '@ant-design/icons-vue';
import { useI18n } from '/@/hooks/web/useI18n';
import { useTableContext } from '../../hooks/useTableContext';
import { getPopupContainer } from '/@/utils';
import { useRoute } from 'vue-router';
import { createLocalStorage } from '/@/utils/cache';
export default defineComponent({
name: 'SizeSetting',
props: {
isMobile: Boolean,
},
components: {
ColumnHeightOutlined,
Tooltip,
Dropdown,
Menu,
MenuItem: Menu.Item,
},
setup(props) {
const table = useTableContext();
const { t } = useI18n();
const $ls = createLocalStorage();
const route = useRoute();
const selectedKeysRef = ref<SizeType[]>([table.getSize()]);
const getBindProps = computed(() => {
let obj = {};
if (props.isMobile) {
obj['visible'] = false;
}
return obj;
});
function handleTitleClick({ key }: { key: SizeType }) {
selectedKeysRef.value = [key];
table.setProps({
size: key,
});
// update-begin--author:liaozhiyang---date:20240604---for【TV360X-100】缓存表格密度
$ls.set(cacheKey.value, key);
// update-end--author:liaozhiyang---date:20240604---for【TV360X-100】缓存表格密度
}
// update-begin--author:liaozhiyang---date:20240604---for【TV360X-100】缓存表格密度
const cacheKey = computed(() => {
const path = route.path;
let key = path.replace(/[\/\\]/g, '_');
let cacheKey = table.getBindValues.value.tableSetting?.cacheKey;
if (cacheKey) {
key += ':' + cacheKey;
}
return 'tableSizeCache:' + key;
});
const local: SizeType | null = $ls.get(cacheKey.value);
if (local) {
selectedKeysRef.value = [local];
table.setProps({
size: local,
});
}
// update-end--author:liaozhiyang---date:20240604---for【TV360X-100】缓存表格密度
return {
getBindProps,
handleTitleClick,
selectedKeysRef,
getPopupContainer,
t,
};
},
});
</script>

View File

@@ -0,0 +1,74 @@
<template>
<div class="table-settings">
<RedoSetting v-if="getSetting.redo" :isMobile="isMobile" :getPopupContainer="getTableContainer" />
<SizeSetting v-if="getSetting.size" :isMobile="isMobile" :getPopupContainer="getTableContainer" />
<ColumnSetting v-if="getSetting.setting" :isMobile="isMobile" @columns-change="handleColumnChange" :getPopupContainer="getTableContainer" />
<FullScreenSetting v-if="getSetting.fullScreen" :isMobile="isMobile" :getPopupContainer="getTableContainer" />
</div>
</template>
<script lang="ts">
import type { PropType } from 'vue';
import type { TableSetting, ColumnChangeParam } from '../../types/table';
import { defineComponent, computed, unref } from 'vue';
import ColumnSetting from './ColumnSetting.vue';
import SizeSetting from './SizeSetting.vue';
import RedoSetting from './RedoSetting.vue';
import FullScreenSetting from './FullScreenSetting.vue';
import { useI18n } from '/@/hooks/web/useI18n';
import { useTableContext } from '../../hooks/useTableContext';
export default defineComponent({
name: 'TableSetting',
components: {
ColumnSetting,
SizeSetting,
RedoSetting,
FullScreenSetting,
},
props: {
setting: {
type: Object as PropType<TableSetting>,
default: () => ({}),
},
mode: String,
},
emits: ['columns-change'],
setup(props, { emit }) {
const { t } = useI18n();
const table = useTableContext();
const getSetting = computed((): TableSetting => {
return {
redo: true,
size: true,
setting: true,
fullScreen: false,
...props.setting,
};
});
const isMobile = computed(() => props.mode === 'mobile');
function handleColumnChange(data: ColumnChangeParam[]) {
emit('columns-change', data);
}
function getTableContainer() {
return table ? unref(table.wrapRef) : document.body;
}
return { getSetting, t, handleColumnChange, getTableContainer, isMobile };
},
});
</script>
<style lang="less">
.table-settings {
& > * {
margin-right: 12px;
}
svg {
width: 1.3em;
height: 1.3em;
}
}
</style>

View File

@@ -0,0 +1,30 @@
import componentSetting from '/@/settings/componentSetting';
const { table } = componentSetting;
const { pageSizeOptions, defaultPageSize, defaultSize, fetchSetting, defaultSortFn, defaultFilterFn } = table;
export const ROW_KEY = 'key';
// Optional display number per page;
export const PAGE_SIZE_OPTIONS = pageSizeOptions;
// Number of items displayed per page
export const PAGE_SIZE = defaultPageSize;
// Common interface field settings
export const FETCH_SETTING = fetchSetting;
// Configure general sort function
export const DEFAULT_SORT_FN = defaultSortFn;
export const DEFAULT_FILTER_FN = defaultFilterFn;
// Default layout of table cells
export const DEFAULT_ALIGN = 'center';
// Default Size
export const DEFAULT_SIZE = defaultSize;
export const INDEX_COLUMN_FLAG = 'INDEX';
export const ACTION_COLUMN_FLAG = 'ACTION';

View File

@@ -0,0 +1,348 @@
import type { BasicColumn, BasicTableProps, CellFormat, GetColumnsParams } from '../types/table';
import type { PaginationProps } from '../types/pagination';
import type { ComputedRef } from 'vue';
import { Table } from 'ant-design-vue';
import { computed, Ref, ref, toRaw, unref, watch, reactive } from 'vue';
import { renderEditCell } from '../components/editable';
import { usePermission } from '/@/hooks/web/usePermission';
import { useI18n } from '/@/hooks/web/useI18n';
import { isArray, isBoolean, isFunction, isMap, isString } from '/@/utils/is';
import { cloneDeep, isEqual } from 'lodash-es';
import { formatToDate } from '/@/utils/dateUtil';
import { ACTION_COLUMN_FLAG, DEFAULT_ALIGN, INDEX_COLUMN_FLAG, PAGE_SIZE } from '../const';
import { CUS_SEL_COLUMN_KEY } from './useCustomSelection';
function handleItem(item: BasicColumn, ellipsis: boolean) {
const { key, dataIndex, children } = item;
item.align = item.align || DEFAULT_ALIGN;
if (ellipsis) {
if (!key) {
item.key = dataIndex;
}
if (!isBoolean(item.ellipsis)) {
Object.assign(item, {
ellipsis,
});
}
}
if (children && children.length) {
handleChildren(children, !!ellipsis);
}
}
function handleChildren(children: BasicColumn[] | undefined, ellipsis: boolean) {
if (!children) return;
children.forEach((item) => {
const { children } = item;
handleItem(item, ellipsis);
handleChildren(children, ellipsis);
});
}
function handleIndexColumn(propsRef: ComputedRef<BasicTableProps>, getPaginationRef: ComputedRef<boolean | PaginationProps>, columns: BasicColumn[]) {
const { t } = useI18n();
const { showIndexColumn, indexColumnProps, isTreeTable } = unref(propsRef);
let pushIndexColumns = false;
if (unref(isTreeTable)) {
return;
}
columns.forEach(() => {
const indIndex = columns.findIndex((column) => column.flag === INDEX_COLUMN_FLAG);
if (showIndexColumn) {
pushIndexColumns = indIndex === -1;
} else if (!showIndexColumn && indIndex !== -1) {
columns.splice(indIndex, 1);
}
});
if (!pushIndexColumns) return;
const isFixedLeft = columns.some((item) => item.fixed === 'left');
columns.unshift({
flag: INDEX_COLUMN_FLAG,
width: 50,
title: t('component.table.index'),
align: 'center',
customRender: ({ index }) => {
const getPagination = unref(getPaginationRef);
if (isBoolean(getPagination)) {
return `${index + 1}`;
}
const { current = 1, pageSize = PAGE_SIZE } = getPagination;
return ((current < 1 ? 1 : current) - 1) * pageSize + index + 1;
},
...(isFixedLeft
? {
fixed: 'left',
}
: {}),
...indexColumnProps,
});
}
function handleActionColumn(propsRef: ComputedRef<BasicTableProps>, columns: BasicColumn[]) {
const { actionColumn, showActionColumn } = unref(propsRef);
if (!actionColumn || !showActionColumn) return;
const hasIndex = columns.findIndex((column) => column.flag === ACTION_COLUMN_FLAG);
if (hasIndex === -1) {
columns.push({
...columns[hasIndex],
...actionColumn,
flag: ACTION_COLUMN_FLAG,
});
}
}
export function useColumns(
propsRef: ComputedRef<BasicTableProps>,
getPaginationRef: ComputedRef<boolean | PaginationProps>,
handleCustomSelectColumn: Fn
) {
const columnsRef = ref(unref(propsRef).columns) as unknown as Ref<BasicColumn[]>;
let cacheColumns = unref(propsRef).columns;
const getColumnsRef = computed(() => {
const columns = cloneDeep(unref(columnsRef));
handleIndexColumn(propsRef, getPaginationRef, columns);
handleActionColumn(propsRef, columns);
// update-begin--author:sunjianlei---date:220230630---for【QQYUN-5571】自封装选择列解决数据行选择卡顿问题
handleCustomSelectColumn(columns);
// update-end--author:sunjianlei---date:220230630---for【QQYUN-5571】自封装选择列解决数据行选择卡顿问题
if (!columns) {
return [];
}
const { ellipsis } = unref(propsRef);
columns.forEach((item) => {
const { customRender, slots } = item;
handleItem(item, Reflect.has(item, 'ellipsis') ? !!item.ellipsis : !!ellipsis && !customRender && !slots);
});
return columns;
});
function isIfShow(column: BasicColumn): boolean {
const ifShow = column.ifShow;
let isIfShow = true;
if (isBoolean(ifShow)) {
isIfShow = ifShow;
}
if (isFunction(ifShow)) {
isIfShow = ifShow(column);
}
return isIfShow;
}
const { hasPermission } = usePermission();
const getViewColumns = computed(() => {
const viewColumns = sortFixedColumn(unref(getColumnsRef));
const columns = cloneDeep(viewColumns);
const result = columns
.filter((column) => {
return hasPermission(column.auth) && isIfShow(column);
})
.map((column) => {
// update-begin--author:liaozhiyang---date:20230718---for: 【issues-179】antd3 一些警告以及报错(针对表格)
if(column.slots?.customRender) {
// slots的备份兼容老的写法转成新写法避免控制台警告
column.slotsBak = column.slots;
delete column.slots;
}
// update-end--author:liaozhiyang---date:20230718---for: 【issues-179】antd3 一些警告以及报错(针对表格)
const { slots, customRender, format, edit, editRow, flag, title: metaTitle } = column;
if (!slots || !slots?.title) {
// column.slots = { title: `header-${dataIndex}`, ...(slots || {}) };
column.customTitle = column.title as string;
Reflect.deleteProperty(column, 'title');
}
//update-begin-author:taoyan date:20211203 for:【online报表】分组标题显示错误都显示成了联系信息 LOWCOD-2343
if (column.children) {
column.title = metaTitle;
}
//update-end-author:taoyan date:20211203 for:【online报表】分组标题显示错误都显示成了联系信息 LOWCOD-2343
const isDefaultAction = [INDEX_COLUMN_FLAG, ACTION_COLUMN_FLAG].includes(flag!);
if (!customRender && format && !edit && !isDefaultAction) {
column.customRender = ({ text, record, index }) => {
return formatCell(text, format, record, index);
};
}
// edit table
if ((edit || editRow) && !isDefaultAction) {
column.customRender = renderEditCell(column);
}
return reactive(column);
});
// update-begin--author:liaozhiyang---date:20230919---for【QQYUN-6387】展开写法去掉报错
if (propsRef.value.expandedRowKeys && !propsRef.value.isTreeTable) {
let index = 0;
const findIndex = result.findIndex((item) => item.key === CUS_SEL_COLUMN_KEY);
if (findIndex != -1) {
index = findIndex + 1;
}
const next: any = result[index + 1];
let expand = Table.EXPAND_COLUMN;
if (next && (next['fixed'] == true || next['fixed'] == 'left')) {
expand = Object.assign(expand, { fixed: 'left' });
}
result.splice(index, 0, expand);
}
return result;
// update-end--author:liaozhiyang---date:20230919---for【QQYUN-6387】展开写法去掉报错
});
watch(
() => unref(propsRef).columns,
(columns) => {
columnsRef.value = columns;
cacheColumns = columns?.filter((item) => !item.flag) ?? [];
}
);
function setCacheColumnsByField(dataIndex: string | undefined, value: Partial<BasicColumn>) {
if (!dataIndex || !value) {
return;
}
cacheColumns.forEach((item) => {
if (item.dataIndex === dataIndex) {
Object.assign(item, value);
return;
}
});
}
// update-begin--author:sunjianlei---date:20220523---for: 【VUEN-1089】合并vben最新版代码解决表格字段排序问题
/**
* set columns
* @param columnList keycolumn
*/
function setColumns(columnList: Partial<BasicColumn>[] | (string | string[])[]) {
const columns = cloneDeep(columnList);
if (!isArray(columns)) return;
if (columns.length <= 0) {
columnsRef.value = [];
return;
}
const firstColumn = columns[0];
const cacheKeys = cacheColumns.map((item) => item.dataIndex);
if (!isString(firstColumn) && !isArray(firstColumn)) {
columnsRef.value = columns as BasicColumn[];
} else {
const columnKeys = (columns as (string | string[])[]).map((m) => m.toString());
const newColumns: BasicColumn[] = [];
cacheColumns.forEach((item) => {
newColumns.push({
...item,
defaultHidden: !columnKeys.includes(item.dataIndex?.toString() || (item.key as string)),
});
});
// Sort according to another array
if (!isEqual(cacheKeys, columns)) {
newColumns.sort((prev, next) => {
return columnKeys.indexOf(prev.dataIndex?.toString() as string) - columnKeys.indexOf(next.dataIndex?.toString() as string);
});
}
columnsRef.value = newColumns;
}
}
// update-end--author:sunjianlei---date:20220523---for: 【VUEN-1089】合并vben最新版代码解决表格字段排序问题
function getColumns(opt?: GetColumnsParams) {
const { ignoreIndex, ignoreAction, sort } = opt || {};
let columns = toRaw(unref(getColumnsRef));
if (ignoreIndex) {
columns = columns.filter((item) => item.flag !== INDEX_COLUMN_FLAG);
}
if (ignoreAction) {
columns = columns.filter((item) => item.flag !== ACTION_COLUMN_FLAG);
}
// update-begin--author:sunjianlei---date:220230630---for【QQYUN-5571】自封装选择列解决数据行选择卡顿问题
// 过滤自定义选择列
columns = columns.filter((item) => item.key !== CUS_SEL_COLUMN_KEY);
// update-enb--author:sunjianlei---date:220230630---for【QQYUN-5571】自封装选择列解决数据行选择卡顿问题
if (sort) {
columns = sortFixedColumn(columns);
}
return columns;
}
function getCacheColumns() {
return cacheColumns;
}
return {
getColumnsRef,
getCacheColumns,
getColumns,
setColumns,
getViewColumns,
setCacheColumnsByField,
};
}
function sortFixedColumn(columns: BasicColumn[]) {
const fixedLeftColumns: BasicColumn[] = [];
const fixedRightColumns: BasicColumn[] = [];
const defColumns: BasicColumn[] = [];
for (const column of columns) {
if (column.fixed === 'left') {
fixedLeftColumns.push(column);
continue;
}
if (column.fixed === 'right') {
fixedRightColumns.push(column);
continue;
}
defColumns.push(column);
}
return [...fixedLeftColumns, ...defColumns, ...fixedRightColumns].filter((item) => !item.defaultHidden);
}
// format cell
export function formatCell(text: string, format: CellFormat, record: Recordable, index: number) {
if (!format) {
return text;
}
// custom function
if (isFunction(format)) {
return format(text, record, index);
}
try {
// date type
const DATE_FORMAT_PREFIX = 'date|';
if (isString(format) && format.startsWith(DATE_FORMAT_PREFIX)) {
const dateFormat = format.replace(DATE_FORMAT_PREFIX, '');
if (!dateFormat) {
return text;
}
return formatToDate(text, dateFormat);
}
// Map
if (isMap(format)) {
return format.get(text);
}
} catch (error) {
return text;
}
}

View File

@@ -0,0 +1,141 @@
import { computed, nextTick, unref, watchEffect } from 'vue';
import { router } from '/@/router';
import { useRoute } from 'vue-router';
import { createLocalStorage } from '/@/utils/cache';
import { useTableContext } from './useTableContext';
import { useMessage } from '/@/hooks/web/useMessage';
/**
* 列表配置缓存
*/
export function useColumnsCache(opt, setColumns, handleColumnFixed) {
let isInit = false;
const table = useTableContext();
const $ls = createLocalStorage();
const { createMessage: $message } = useMessage();
const route = useRoute();
// 列表配置缓存key
const cacheKey = computed(() => {
// update-begin--author:liaozhiyang---date:20240226---for【QQYUN-8367】online报表配置列展示保存影响到其他页面的table字段的显示隐藏开发环境热更新会有此问题生产环境无问题
const path = route.path;
let key = path.replace(/[\/\\]/g, '_');
// update-end--author:liaozhiyang---date:20240226---for【QQYUN-8367】online报表配置列展示保存影响到其他页面的table字段的显示隐藏开发环境热更新会有此问题生产环境无问题
let cacheKey = table.getBindValues.value.tableSetting?.cacheKey;
if (cacheKey) {
key += ':' + cacheKey;
}
return 'columnCache:' + key;
});
watchEffect(() => {
const columns = table.getColumns();
if (columns.length) {
init();
}
});
async function init() {
if (isInit) {
return;
}
isInit = true;
let columnCache = $ls.get(cacheKey.value);
if (columnCache && columnCache.checkedList) {
const { checkedList, sortedList, sortableOrder, checkIndex } = columnCache;
await nextTick();
// checkbox的排序缓存
opt.sortableOrder.value = sortableOrder;
// checkbox的选中缓存
opt.state.checkedList = checkedList;
// tableColumn的排序缓存
opt.plainSortOptions.value.sort((prev, next) => {
return sortedList.indexOf(prev.value) - sortedList.indexOf(next.value);
});
// 重新排序tableColumn
checkedList.sort((prev, next) => sortedList.indexOf(prev) - sortedList.indexOf(next));
// 是否显示行号列
if (checkIndex) {
table.setProps({ showIndexColumn: true });
}
setColumns(checkedList);
// 设置固定列
setColumnFixed(columnCache);
}
}
/** 设置被固定的列 */
async function setColumnFixed(columnCache) {
const { fixedColumns } = columnCache;
const columns = opt.plainOptions.value;
for (const column of columns) {
let fixedCol = fixedColumns.find((fc) => fc.key === (column.key || column.dataIndex));
if (fixedCol) {
await nextTick();
handleColumnFixed(column, fixedCol.fixed);
}
}
}
// 判断列固定状态
const fixedReg = /^(true|left|right)$/;
/** 获取被固定的列 */
function getFixedColumns() {
let fixedColumns: any[] = [];
const columns = opt.plainOptions.value;
for (const column of columns) {
if (fixedReg.test((column.fixed ?? '').toString())) {
fixedColumns.push({
key: column.key || column.dataIndex,
fixed: column.fixed === true ? 'left' : column.fixed,
});
}
}
return fixedColumns;
}
/** 保存列配置 */
function saveSetting() {
const { checkedList } = opt.state;
const sortedList = unref(opt.plainSortOptions).map((item) => item.value);
$ls.set(cacheKey.value, {
// 保存的列
checkedList,
// 排序后的列
sortedList,
// 是否显示行号列
checkIndex: unref(opt.checkIndex),
// checkbox原始排序
sortableOrder: unref(opt.sortableOrder),
// 固定列
fixedColumns: getFixedColumns(),
});
$message.success('保存成功');
// 保存之后直接关闭
opt.popoverVisible.value = false;
}
/** 重置(删除)列配置 */
async function resetSetting() {
// 重置固定列
await resetFixedColumn();
$ls.remove(cacheKey.value);
$message.success('重置成功');
}
async function resetFixedColumn() {
const columns = opt.plainOptions.value;
for (const column of columns) {
column.fixed;
if (fixedReg.test((column.fixed ?? '').toString())) {
await nextTick();
handleColumnFixed(column, null);
}
}
}
return {
saveSetting,
resetSetting,
};
}

View File

@@ -0,0 +1,108 @@
import type { ComputedRef } from 'vue';
import type { BasicTableProps } from '../types/table';
import { unref } from 'vue';
import { ROW_KEY } from '../const';
import { isString, isFunction } from '/@/utils/is';
interface Options {
setSelectedRowKeys: (keys: string[]) => void;
getSelectRowKeys: () => string[];
clearSelectedRowKeys: () => void;
emit: EmitType;
getAutoCreateKey: ComputedRef<boolean | undefined>;
}
function getKey(record: Recordable, rowKey: string | ((record: Record<string, any>) => string) | undefined, autoCreateKey?: boolean) {
if (!rowKey || autoCreateKey) {
return record[ROW_KEY];
}
if (isString(rowKey)) {
return record[rowKey];
}
if (isFunction(rowKey)) {
return record[rowKey(record)];
}
return null;
}
export function useCustomRow(
propsRef: ComputedRef<BasicTableProps>,
{ setSelectedRowKeys, getSelectRowKeys, getAutoCreateKey, clearSelectedRowKeys, emit }: Options
) {
const customRow = (record: Recordable, index: number) => {
return {
onClick: (e: Event) => {
e?.stopPropagation();
function handleClick() {
const { rowSelection, rowKey, clickToRowSelect } = unref(propsRef);
if (!rowSelection || !clickToRowSelect) return;
const keys = getSelectRowKeys();
const key = getKey(record, rowKey, unref(getAutoCreateKey));
if (!key) return;
const isCheckbox = rowSelection.type === 'checkbox';
if (isCheckbox) {
// 找到tr
const tr: HTMLElement = (e as MouseEvent).composedPath?.().find((dom: HTMLElement) => dom.tagName === 'TR') as HTMLElement;
if (!tr) return;
// 找到Checkbox检查是否为disabled
const checkBox = tr.querySelector('input[type=checkbox]');
if (!checkBox || checkBox.hasAttribute('disabled')) return;
if (!keys.includes(key)) {
setSelectedRowKeys([...keys, key]);
return;
}
const keyIndex = keys.findIndex((item) => item === key);
keys.splice(keyIndex, 1);
setSelectedRowKeys(keys);
return;
}
const isRadio = rowSelection.type === 'radio';
if (isRadio) {
// update-begin--author:liaozhiyang---date:20231016---for【QQYUN-6794】table列表增加radio禁用功能
const rowSelection = propsRef.value.rowSelection;
if (rowSelection.getCheckboxProps) {
const result = rowSelection.getCheckboxProps(record);
if (result.disabled) {
return;
}
}
// update-end--author:liaozhiyang---date:20231016---for【QQYUN-6794】table列表增加radio禁用功能
if (!keys.includes(key)) {
if (keys.length) {
clearSelectedRowKeys();
}
setSelectedRowKeys([key]);
return;
} else {
// update-begin--author:liaozhiyang---date:20240527---for【TV360X-359】erp主表点击已选中的选到了最后一个
// 点击已经选中的直接return不在做操作
return;
// update-end--author:liaozhiyang---date:20240527---for【TV360X-359】erp主表点击已选中的选到了最后一个
}
clearSelectedRowKeys();
}
}
handleClick();
emit('row-click', record, index, e);
},
onDblclick: (event: Event) => {
emit('row-dbClick', record, index, event);
},
onContextmenu: (event: Event) => {
emit('row-contextmenu', record, index, event);
},
onMouseenter: (event: Event) => {
emit('row-mouseenter', record, index, event);
},
onMouseleave: (event: Event) => {
emit('row-mouseleave', record, index, event);
},
};
};
return {
customRow,
};
}

View File

@@ -0,0 +1,636 @@
import type { BasicColumn } from '/@/components/Table';
import type { Ref, ComputedRef } from 'vue';
import type { BasicTableProps, PaginationProps, TableRowSelection } from '/@/components/Table';
import { computed, nextTick, onUnmounted, ref, toRaw, unref, watch, watchEffect } from 'vue';
import { omit, isEqual } from 'lodash-es';
import { throttle } from 'lodash-es';
import { Checkbox, Radio } from 'ant-design-vue';
import { isFunction } from '/@/utils/is';
import { findNodeAll } from '/@/utils/helper/treeHelper';
import { ROW_KEY } from '/@/components/Table/src/const';
import { onMountedOrActivated } from '/@/hooks/core/onMountedOrActivated';
import { useMessage } from '/@/hooks/web/useMessage';
import { ModalFunc } from 'ant-design-vue/lib/modal/Modal';
// 自定义选择列的key
export const CUS_SEL_COLUMN_KEY = 'j-custom-selected-column';
/**
* 自定义选择列
*/
export function useCustomSelection(
propsRef: ComputedRef<BasicTableProps>,
emit: EmitType,
wrapRef: Ref<null | HTMLDivElement>,
getPaginationRef: ComputedRef<boolean | PaginationProps>,
tableData: Ref<Recordable[]>,
childrenColumnName: ComputedRef<string>
) {
const { createConfirm } = useMessage();
// 表格body元素
const bodyEl = ref<HTMLDivElement>();
// body元素高度
const bodyHeight = ref<number>(0);
// 表格tr高度
const rowHeight = ref<number>(0);
// body 滚动高度
const scrollTop = ref(0);
// 选择的key
const selectedKeys = ref<string[]>([]);
// 选择的行
const selectedRows = ref<Recordable[]>([]);
// 变更的行
let changeRows: Recordable[] = [];
let allSelected: boolean = false;
// 扁平化数据children数据也会放到一起
const flattedData = computed(() => {
// update-begin--author:liaozhiyang---date:20231016---for【QQYUN-6774】解决checkbox禁用后全选仍能勾选问题
const data = flattenData(tableData.value, childrenColumnName.value);
const rowSelection = propsRef.value.rowSelection;
if (rowSelection?.type === 'checkbox' && rowSelection.getCheckboxProps) {
for (let i = 0, len = data.length; i < len; i++) {
const record = data[i];
const result = rowSelection.getCheckboxProps(record);
if (result.disabled) {
data.splice(i, 1);
i--;
len--;
}
}
}
return data;
// update-end--author:liaozhiyang---date:20231016---for【QQYUN-6774】解决checkbox禁用后全选仍能勾选问题
});
const getRowSelectionRef = computed((): TableRowSelection | null => {
const { rowSelection } = unref(propsRef);
if (!rowSelection) {
return null;
}
return {
preserveSelectedRowKeys: true,
// selectedRowKeys: unref(selectedKeys),
// onChange: (selectedRowKeys: string[]) => {
// setSelectedRowKeys(selectedRowKeys);
// },
...omit(rowSelection, ['onChange', 'selectedRowKeys']),
};
});
// 是否是单选
const isRadio = computed(() => {
return getRowSelectionRef.value?.type === 'radio';
});
const getAutoCreateKey = computed(() => {
return unref(propsRef).autoCreateKey && !unref(propsRef).rowKey;
});
// 列key字段
const getRowKey = computed(() => {
const { rowKey } = unref(propsRef);
return unref(getAutoCreateKey) ? ROW_KEY : rowKey;
});
// 获取行的key字段数据
const getRecordKey = (record) => {
if (!getRowKey.value) {
return record[ROW_KEY];
} else if (isFunction(getRowKey.value)) {
return getRowKey.value(record);
} else {
return record[getRowKey.value];
}
};
// 分页配置
const getPagination = computed<PaginationProps>(() => {
return typeof getPaginationRef.value === 'boolean' ? {} : getPaginationRef.value;
});
// 当前页条目数量
const currentPageSize = computed(() => {
const { pageSize = 10, total = flattedData.value.length } = getPagination.value;
return pageSize > total ? total : pageSize;
});
// 选择列表头props
const selectHeaderProps = computed(() => {
return {
onSelectAll,
isRadio: isRadio.value,
selectedLength: flattedData.value.filter((data) => selectedKeys.value.includes(getRecordKey(data))).length,
// update-begin--author:liaozhiyang---date:20240511---for【QQYUN-9289】解决表格条数不足pageSize数量时行数全部勾选但是全选框不勾选
// 【TV360X-53】为空时会报错加强判断
pageSize: tableData.value?.length ?? 0,
// update-end--author:liaozhiyang---date:20240511---for【QQYUN-9289】解决表格条数不足pageSize数量时行数全部勾选但是全选框不勾选
// 【QQYUN-6774】解决checkbox禁用后全选仍能勾选问题
disabled: flattedData.value.length == 0,
hideSelectAll: unref(propsRef)?.rowSelection?.hideSelectAll,
};
});
// 监听传入的selectedRowKeys
// update-begin--author:liaozhiyang---date:20240306---for【QQYUN-8390】部门人员组件点击重置未清空selectedRowKeys.value=[]watch没监听到加deep
watch(
() => unref(propsRef)?.rowSelection?.selectedRowKeys,
(val: string[]) => {
// 解决selectedRowKeys在页面调用处使用ref失效
const value = unref(val);
if (Array.isArray(value) && !sameArray(value, selectedKeys.value)) {
setSelectedRowKeys(value);
}
},
{
immediate: true,
deep: true
}
);
// update-end--author:liaozhiyang---date:20240306---for【QQYUN-8390】部门人员组件点击重置未清空selectedRowKeys.value=[]watch没监听到加deep
/**
* 2024-03-06
* liaozhiyang
* 判断是否同一个数组 (引用地址,长度,元素位置信息相同才是同一个数组。数组元素只有字符串)
*/
function sameArray(a, b) {
if (a === b) {
if (a.length === b.length) {
return a.toString() === b.toString();
} else {
return false;
}
} else {
// update-begin--author:liaozhiyang---date:20240425---for【QQYUN-9123】popupdict打开弹窗打开程序运行
if (isEqual(a, b)) {
return true;
}
// update-end--author:liaozhiyang---date:20240425---for【QQYUN-9123】popupdict打开弹窗打开程序运行
return false;
}
}
// 当任意一个变化时,触发同步检测
watch([selectedKeys, selectedRows], () => {
nextTick(() => {
syncSelectedRows();
});
});
// 监听滚动条事件
const onScrollTopChange = throttle((e) => (scrollTop.value = e?.target?.scrollTop), 150);
let bodyResizeObserver: Nullable<ResizeObserver> = null;
// 获取首行行高
watchEffect(() => {
if (bodyEl.value) {
// 监听div高度变化
bodyResizeObserver = new ResizeObserver((entries) => {
for (let entry of entries) {
if (entry.target === bodyEl.value && entry.contentRect) {
const { height } = entry.contentRect;
bodyHeight.value = Math.ceil(height);
}
}
});
bodyResizeObserver.observe(bodyEl.value);
const el = bodyEl.value?.querySelector('tbody.ant-table-tbody tr.ant-table-row') as HTMLDivElement;
if (el) {
rowHeight.value = el.offsetHeight;
return;
}
}
rowHeight.value = 50;
// 这种写法是为了监听到 size 的变化
propsRef.value.size && void 0;
});
onMountedOrActivated(async () => {
bodyEl.value = await getTableBody(wrapRef.value!);
bodyEl.value.addEventListener('scroll', onScrollTopChange);
});
onUnmounted(() => {
if (bodyEl.value) {
bodyEl.value?.removeEventListener('scroll', onScrollTopChange);
}
if (bodyResizeObserver != null) {
bodyResizeObserver.disconnect();
}
});
// 选择全部
function onSelectAll(checked: boolean) {
// update-begin--author:liaozhiyang---date:20231122---for【issues/5577】BasicTable组件全选和取消全选时不触发onSelectAll事件
if (unref(propsRef)?.rowSelection?.onSelectAll) {
allSelected = checked;
changeRows = getInvertRows(selectedRows.value);
}
// update-end--author:liaozhiyang---date:20231122---for【issues/5577】BasicTable组件全选和取消全选时不触发onSelectAll事件
// 取消全选
if (!checked) {
// update-begin--author:liaozhiyang---date:20240510---for【issues/1173】取消全选只是当前页面取消
// selectedKeys.value = [];
// selectedRows.value = [];
// emitChange('all');
flattedData.value.forEach((item) => {
updateSelected(item, false);
});
// update-end--author:liaozhiyang---date:20240510---for【issues/1173】取消全选只是当前页面取消
return;
}
let modal: Nullable<ReturnType<ModalFunc>> = null;
// 全选
const checkAll = () => {
if (modal != null) {
modal.update({
content: '正在分批全选,请稍后……',
cancelButtonProps: { disabled: true },
});
}
let showCount = 0;
// 最小选中数量
let minSelect = 100;
const hidden: Recordable[] = [];
flattedData.value.forEach((item, index, array) => {
if (array.length > 120) {
if (showCount <= minSelect && recordIsShow(index, Math.max((minSelect - 10) / 2, 3))) {
showCount++;
updateSelected(item, checked);
} else {
hidden.push(item);
}
} else {
updateSelected(item, checked);
}
});
if (hidden.length > 0) {
return batchesSelectAll(hidden, checked, minSelect);
} else {
emitChange('all');
}
};
// 当数据量大于120条时全选会导致页面卡顿需进行慢速全选
if (flattedData.value.length > 120) {
modal = createConfirm({
title: '全选',
content: '当前数据量较大,全选可能会导致页面卡顿,确定要执行此操作吗?',
iconType: 'warning',
onOk: () => checkAll(),
});
} else {
checkAll();
}
}
// 分批全选
function batchesSelectAll(hidden: Recordable[], checked: boolean, minSelect: number) {
return new Promise<void>((resolve) => {
(function call() {
// 每隔半秒钟选择100条数据
setTimeout(() => {
const list = hidden.splice(0, minSelect);
if (list.length > 0) {
list.forEach((item) => {
updateSelected(item, checked);
});
call();
} else {
setTimeout(() => {
emitChange('all');
// update-begin--author:liaozhiyang---date:20230811---for【QQYUN-5687】批量选择提示成功后又来一个提示
setTimeout(() =>resolve(), 0);
// update-end--author:liaozhiyang---date:20230811---for【QQYUN-5687】批量选择提示成功后又来一个提示
}, 500);
}
}, 300);
})();
});
}
// 选中单个
function onSelect(record, checked) {
updateSelected(record, checked);
emitChange();
}
function updateSelected(record, checked) {
const recordKey = getRecordKey(record);
if (isRadio.value) {
selectedKeys.value = [recordKey];
selectedRows.value = [record];
return;
}
const index = selectedKeys.value.findIndex((key) => key === recordKey);
if (checked) {
if (index === -1) {
selectedKeys.value.push(recordKey);
selectedRows.value.push(record);
}
} else {
if (index !== -1) {
selectedKeys.value.splice(index, 1);
selectedRows.value.splice(index, 1);
}
}
}
// 调用用户自定义的onChange事件
function emitChange(mode = 'single') {
const { rowSelection } = unref(propsRef);
if (rowSelection) {
const { onChange } = rowSelection;
if (onChange && isFunction(onChange)) {
setTimeout(() => {
onChange(selectedKeys.value, selectedRows.value);
}, 0);
}
}
emit('selection-change', {
keys: getSelectRowKeys(),
rows: getSelectRows(),
});
// update-begin--author:liaozhiyang---date:20231122---for【issues/5577】BasicTable组件全选和取消全选时不触发onSelectAll事件
if (mode == 'all') {
const rowSelection = unref(propsRef)?.rowSelection;
if (rowSelection?.onSelectAll) {
rowSelection.onSelectAll(allSelected, toRaw(getSelectRows()), toRaw(changeRows));
}
}
// update-end--author:liaozhiyang---date:20231122---for【issues/5577】BasicTable组件全选和取消全选时不触发
}
// 用于判断是否是自定义选择列
function isCustomSelection(column: BasicColumn) {
return column.key === CUS_SEL_COLUMN_KEY;
}
/**
* 判断当前行是否可视,虚拟滚动用
* @param index 行下标
* @param threshold 前后阈值默认可视区域前后显示3条
*/
function recordIsShow(index: number, threshold = 3) {
// 只有数据量大于50条时才会进行虚拟滚动
const isVirtual = flattedData.value.length > 50;
if (isVirtual) {
// 根据 scrollTop、bodyHeight、rowHeight 计算出当前行是否可视阈值前后3条
// flag1 = 判断当前行是否在可视区域上方3条
const flag1 = scrollTop.value - rowHeight.value * threshold < index * rowHeight.value;
// flag2 = 判断当前行是否在可视区域下方3条
const flag2 = index * rowHeight.value < scrollTop.value + bodyHeight.value + rowHeight.value * threshold;
// 全部条件满足时,才显示当前行
return flag1 && flag2;
}
return true;
}
// 自定义渲染Body
function bodyCustomRender(params) {
const { index } = params;
// update-begin--author:liaozhiyang---date:20231009--for【issues/776】显示100条/页复选框只能显示3个的问题
if (propsRef.value.canResize && !recordIsShow(index)) {
return '';
}
if (isRadio.value) {
return renderRadioComponent(params);
} else {
return renderCheckboxComponent(params);
}
// update-end--author:liaozhiyang---date:20231009---for【issues/776】显示100条/页复选框只能显示3个的问题
}
/**
* 渲染checkbox组件
*/
function renderCheckboxComponent({ record }) {
const recordKey = getRecordKey(record);
// 获取用户自定义checkboxProps
const checkboxProps = ((getCheckboxProps) => {
if (typeof getCheckboxProps === 'function') {
try {
return getCheckboxProps(record) ?? {};
} catch (error) {
console.error(error);
}
}
return {};
})(propsRef.value.rowSelection?.getCheckboxProps);
return (
<Checkbox
{...checkboxProps}
key={'j-select__' + recordKey}
checked={selectedKeys.value.includes(recordKey)}
onUpdate:checked={(checked) => onSelect(record, checked)}
// update-begin--author:liaozhiyang---date:20230326---for【QQYUN-8694】BasicTable在使用clickToRowSelect=true下selection-change 事件在触发多次
onClick={(e) => e.stopPropagation()}
// update-end--author:liaozhiyang---date:20230326---for【QQYUN-8694】BasicTable在使用clickToRowSelect=true下selection-change 事件在触发多次
/>
);
}
/**
* 渲染radio组件
*/
function renderRadioComponent({ record }) {
const recordKey = getRecordKey(record);
// update-begin--author:liaozhiyang---date:20231016---for【QQYUN-6794】table列表增加radio禁用功能
// 获取用户自定义radioProps
const checkboxProps = (() => {
const rowSelection = propsRef.value.rowSelection;
if (rowSelection?.getCheckboxProps) {
return rowSelection.getCheckboxProps(record);
}
return {};
})();
// update-end--author:liaozhiyang---date:20231016---for【QQYUN-6794】table列表增加radio禁用功能
return (
<Radio
{...checkboxProps}
key={'j-select__' + recordKey}
checked={selectedKeys.value.includes(recordKey)}
onUpdate:checked={(checked) => onSelect(record, checked)}
// update-begin--author:liaozhiyang---date:20230326---for【QQYUN-8694】BasicTable在使用clickToRowSelect=true下selection-change 事件在触发多次
onClick={(e) => e.stopPropagation()}
// update-end--author:liaozhiyang---date:20230326---for【QQYUN-8694】BasicTable在使用clickToRowSelect=true下selection-change 事件在触发多次
/>
);
}
// 创建选择列
function handleCustomSelectColumn(columns: BasicColumn[]) {
// update-begin--author:liaozhiyang---date:20230919---for【issues/757】JPopup表格的选择列固定配置不生效
const rowSelection = propsRef.value.rowSelection;
if (!rowSelection) {
return;
}
const isFixedLeft = rowSelection.fixed || columns.some((item) => item.fixed === 'left');
// update-begin--author:liaozhiyang---date:20230919---for【issues/757】JPopup表格的选择列固定配置不生效
columns.unshift({
title: '选择列',
flag: 'CHECKBOX',
key: CUS_SEL_COLUMN_KEY,
width: 50,
minWidth: 50,
maxWidth: 50,
align: 'center',
...(isFixedLeft ? { fixed: 'left' } : {}),
customRender: bodyCustomRender,
});
}
// 清空所有选择
function clearSelectedRowKeys() {
onSelectAll(false);
}
// 通过 selectedKeys 同步 selectedRows
function syncSelectedRows() {
if (selectedKeys.value.length !== selectedRows.value.length) {
setSelectedRowKeys(selectedKeys.value);
}
}
// 设置选择的key
function setSelectedRowKeys(rowKeys: string[]) {
const isSomeRowKeys = selectedKeys.value === rowKeys;
selectedKeys.value = rowKeys;
const allSelectedRows = findNodeAll(
toRaw(unref(flattedData)).concat(toRaw(unref(selectedRows))),
(item) => rowKeys.includes(getRecordKey(item)),
{
children: propsRef.value.childrenColumnName ?? 'children',
}
);
const trueSelectedRows: any[] = [];
rowKeys.forEach((key: string) => {
const found = allSelectedRows.find((item) => getRecordKey(item) === key);
found && trueSelectedRows.push(found);
});
// update-begin--author:liaozhiyang---date:20231103---for【issues/828】解决卡死问题
if (!(isSomeRowKeys && equal(selectedRows.value, trueSelectedRows))) {
selectedRows.value = trueSelectedRows;
emitChange();
}
// update-end--author:liaozhiyang---date:20231103---for【issues/828】解决卡死问题
}
/**
*2023-11-03
*廖志阳
*检测selectedRows.value和trueSelectedRows是否相等防止死循环
*/
function equal(oldVal, newVal) {
let oldKeys = [],
newKeys = [];
if (oldVal.length === newVal.length) {
oldKeys = oldVal.map((item) => getRecordKey(item));
newKeys = newVal.map((item) => getRecordKey(item));
for (let i = 0, len = oldKeys.length; i < len; i++) {
const findItem = newKeys.find((item) => item === oldKeys[i]);
if (!findItem) {
return false;
}
}
return true;
}
return false;
}
/**
*2023-11-22
*廖志阳
*根据全选或者反选返回源数据中这次需要变更的数据
*/
function getInvertRows(rows: any): any {
const allRows = findNodeAll(toRaw(unref(flattedData)), () => true, {
children: propsRef.value.childrenColumnName ?? 'children',
});
if (rows.length === 0 || rows.length === allRows.length) {
return allRows;
} else {
const allRowsKey = allRows.map((item) => getRecordKey(item));
rows.forEach((rItem) => {
const rItemKey = getRecordKey(rItem);
const findIndex = allRowsKey.findIndex((item) => rItemKey === item);
if (findIndex != -1) {
allRowsKey.splice(findIndex, 1);
allRows.splice(findIndex, 1);
}
});
}
return allRows;
}
function getSelectRows<T = Recordable>() {
return unref(selectedRows) as T[];
}
function getSelectRowKeys() {
return unref(selectedKeys);
}
function getRowSelection() {
return unref(getRowSelectionRef)!;
}
function deleteSelectRowByKey(key: string) {
const index = selectedKeys.value.findIndex((item) => item === key);
if (index !== -1) {
selectedKeys.value.splice(index, 1);
selectedRows.value.splice(index, 1);
}
}
// 【QQYUN-5837】动态计算 expandIconColumnIndex
const getExpandIconColumnIndex = computed(() => {
const { expandIconColumnIndex } = unref(propsRef);
// 未设置选择列,则保持不变
if (getRowSelectionRef.value == null) {
return expandIconColumnIndex;
}
// 设置了选择列,并且未传入 index 参数,则返回 1
if (expandIconColumnIndex == null) {
return 1;
}
return expandIconColumnIndex;
});
return {
getRowSelection,
getRowSelectionRef,
getSelectRows,
getSelectRowKeys,
setSelectedRowKeys,
deleteSelectRowByKey,
selectHeaderProps,
isCustomSelection,
handleCustomSelectColumn,
clearSelectedRowKeys,
getExpandIconColumnIndex,
};
}
function getTableBody(wrap: HTMLDivElement) {
return new Promise<HTMLDivElement>((resolve) => {
(function fn() {
const bodyEl = wrap.querySelector('.ant-table-wrapper .ant-table-body') as HTMLDivElement;
if (bodyEl) {
resolve(bodyEl);
} else {
setTimeout(fn, 100);
}
})();
});
}
function flattenData<RecordType>(data: RecordType[] | undefined, childrenColumnName: string): RecordType[] {
let list: RecordType[] = [];
(data || []).forEach((record) => {
list.push(record);
if (record && typeof record === 'object' && childrenColumnName in record) {
list = [...list, ...flattenData<RecordType>((record as any)[childrenColumnName], childrenColumnName)];
}
});
return list;
}

View File

@@ -0,0 +1,349 @@
import type { BasicTableProps, FetchParams, SorterResult } from '../types/table';
import type { PaginationProps } from '../types/pagination';
import { ref, unref, ComputedRef, computed, onMounted, watch, reactive, Ref, watchEffect } from 'vue';
import { useTimeoutFn } from '/@/hooks/core/useTimeout';
import { buildUUID } from '/@/utils/uuid';
import { isFunction, isBoolean } from '/@/utils/is';
import { get, cloneDeep } from 'lodash-es';
import { FETCH_SETTING, ROW_KEY, PAGE_SIZE } from '../const';
interface ActionType {
getPaginationInfo: ComputedRef<boolean | PaginationProps>;
setPagination: (info: Partial<PaginationProps>) => void;
setLoading: (loading: boolean) => void;
// update-begin--author:sunjianlei---date:220220419---for由于 getFieldsValue 返回的不是逗号分割的数据,所以改用 validate
validate: () => Recordable;
// update-end--author:sunjianlei---date:220220419---for由于 getFieldsValue 返回的不是逗号分割的数据,所以改用 validate
clearSelectedRowKeys: () => void;
tableData: Ref<Recordable[]>;
}
interface SearchState {
sortInfo: Recordable;
filterInfo: Record<string, string[]>;
}
export function useDataSource(
propsRef: ComputedRef<BasicTableProps>,
{ getPaginationInfo, setPagination, setLoading, validate, clearSelectedRowKeys, tableData }: ActionType,
emit: EmitType
) {
const searchState = reactive<SearchState>({
sortInfo: {},
filterInfo: {},
});
const dataSourceRef = ref<Recordable[]>([]);
const rawDataSourceRef = ref<Recordable>({});
watchEffect(() => {
tableData.value = unref(dataSourceRef);
});
watch(
() => unref(propsRef).dataSource,
() => {
const { dataSource, api } = unref(propsRef);
!api && dataSource && (dataSourceRef.value = dataSource);
},
{
immediate: true,
}
);
function handleTableChange(pagination: PaginationProps, filters: Partial<Recordable<string[]>>, sorter: SorterResult) {
const { clearSelectOnPageChange, sortFn, filterFn } = unref(propsRef);
if (clearSelectOnPageChange) {
clearSelectedRowKeys();
}
setPagination(pagination);
const params: Recordable = {};
if (sorter && isFunction(sortFn)) {
const sortInfo = sortFn(sorter);
searchState.sortInfo = sortInfo;
params.sortInfo = sortInfo;
}
if (filters && isFunction(filterFn)) {
const filterInfo = filterFn(filters);
searchState.filterInfo = filterInfo;
params.filterInfo = filterInfo;
}
fetch(params);
}
function setTableKey(items: any[]) {
if (!items || !Array.isArray(items)) return;
items.forEach((item) => {
if (!item[ROW_KEY]) {
item[ROW_KEY] = buildUUID();
}
if (item.children && item.children.length) {
setTableKey(item.children);
}
});
}
const getAutoCreateKey = computed(() => {
return unref(propsRef).autoCreateKey && !unref(propsRef).rowKey;
});
const getRowKey = computed(() => {
const { rowKey } = unref(propsRef);
return unref(getAutoCreateKey) ? ROW_KEY : rowKey;
});
const getDataSourceRef = computed(() => {
const dataSource = unref(dataSourceRef);
if (!dataSource || dataSource.length === 0) {
return unref(dataSourceRef);
}
if (unref(getAutoCreateKey)) {
const firstItem = dataSource[0];
const lastItem = dataSource[dataSource.length - 1];
if (firstItem && lastItem) {
if (!firstItem[ROW_KEY] || !lastItem[ROW_KEY]) {
const data = cloneDeep(unref(dataSourceRef));
data.forEach((item) => {
if (!item[ROW_KEY]) {
item[ROW_KEY] = buildUUID();
}
if (item.children && item.children.length) {
setTableKey(item.children);
}
});
dataSourceRef.value = data;
}
}
}
return unref(dataSourceRef);
});
async function updateTableData(index: number, key: string, value: any) {
const record = dataSourceRef.value[index];
if (record) {
dataSourceRef.value[index][key] = value;
}
return dataSourceRef.value[index];
}
function updateTableDataRecord(rowKey: string | number, record: Recordable): Recordable | undefined {
const row = findTableDataRecord(rowKey);
if (row) {
for (const field in row) {
if (Reflect.has(record, field)) row[field] = record[field];
//update-begin---author:wangshuai---date:2024-06-11---for:【TV360X-437】树表 部分组件编辑完后,列表未刷新---
if (Reflect.has(record, field + '_dictText')) {
row[field + '_dictText'] = record[field + '_dictText'];
}
//update-end---author:wangshuai---date:2024-06-11---for:【TV360X-437】树表 部分组件编辑完后,列表未刷新---
}
return row;
}
}
function deleteTableDataRecord(rowKey: string | number | string[] | number[]) {
if (!dataSourceRef.value || dataSourceRef.value.length == 0) return;
const rowKeyName = unref(getRowKey);
if (!rowKeyName) return;
const rowKeys = !Array.isArray(rowKey) ? [rowKey] : rowKey;
for (const key of rowKeys) {
let index: number | undefined = dataSourceRef.value.findIndex((row) => {
let targetKeyName: string;
if (typeof rowKeyName === 'function') {
targetKeyName = rowKeyName(row);
} else {
targetKeyName = rowKeyName as string;
}
return row[targetKeyName] === key;
});
if (index >= 0) {
dataSourceRef.value.splice(index, 1);
}
index = unref(propsRef).dataSource?.findIndex((row) => {
let targetKeyName: string;
if (typeof rowKeyName === 'function') {
targetKeyName = rowKeyName(row);
} else {
targetKeyName = rowKeyName as string;
}
return row[targetKeyName] === key;
});
if (typeof index !== 'undefined' && index !== -1) unref(propsRef).dataSource?.splice(index, 1);
}
setPagination({
total: unref(propsRef).dataSource?.length,
});
}
function insertTableDataRecord(record: Recordable, index: number): Recordable | undefined {
// if (!dataSourceRef.value || dataSourceRef.value.length == 0) return;
index = index ?? dataSourceRef.value?.length;
unref(dataSourceRef).splice(index, 0, record);
return unref(dataSourceRef);
}
function findTableDataRecord(rowKey: string | number) {
if (!dataSourceRef.value || dataSourceRef.value.length == 0) return;
const rowKeyName = unref(getRowKey);
if (!rowKeyName) return;
const { childrenColumnName = 'children' } = unref(propsRef);
const findRow = (array: any[]) => {
let ret;
array.some(function iter(r) {
if (typeof rowKeyName === 'function') {
if ((rowKeyName(r) as string) === rowKey) {
ret = r;
return true;
}
} else {
if (Reflect.has(r, rowKeyName) && r[rowKeyName] === rowKey) {
ret = r;
return true;
}
}
return r[childrenColumnName] && r[childrenColumnName].some(iter);
});
return ret;
};
// const row = dataSourceRef.value.find(r => {
// if (typeof rowKeyName === 'function') {
// return (rowKeyName(r) as string) === rowKey
// } else {
// return Reflect.has(r, rowKeyName) && r[rowKeyName] === rowKey
// }
// })
return findRow(dataSourceRef.value);
}
async function fetch(opt?: FetchParams) {
const { api, searchInfo, defSort, fetchSetting, beforeFetch, afterFetch, useSearchForm, pagination } = unref(propsRef);
if (!api || !isFunction(api)) return;
try {
setLoading(true);
const { pageField, sizeField, listField, totalField } = Object.assign({}, FETCH_SETTING, fetchSetting);
let pageParams: Recordable = {};
const { current = 1, pageSize = PAGE_SIZE } = unref(getPaginationInfo) as PaginationProps;
if ((isBoolean(pagination) && !pagination) || isBoolean(getPaginationInfo)) {
pageParams = {};
} else {
pageParams[pageField] = (opt && opt.page) || current;
pageParams[sizeField] = pageSize;
}
const { sortInfo = {}, filterInfo } = searchState;
let params: Recordable = {
...pageParams,
// 由于 getFieldsValue 返回的不是逗号分割的数据,所以改用 validate
...(useSearchForm ? await validate() : {}),
...searchInfo,
...defSort,
...(opt?.searchInfo ?? {}),
...sortInfo,
...filterInfo,
...(opt?.sortInfo ?? {}),
...(opt?.filterInfo ?? {}),
};
if (beforeFetch && isFunction(beforeFetch)) {
params = (await beforeFetch(params)) || params;
}
// update-begin--author:liaozhiyang---date:20240227---for【QQYUN-8316】table查询条件,请求剔除空字符串字段
for (let item of Object.entries(params)) {
const [key, val] = item;
if (val === '') {
delete params[key];
};
};
// update-end--author:liaozhiyang---date:20240227---for【QQYUN-8316】table查询条件,请求剔除空字符串字段
const res = await api(params);
rawDataSourceRef.value = res;
const isArrayResult = Array.isArray(res);
let resultItems: Recordable[] = isArrayResult ? res : get(res, listField);
const resultTotal: number = isArrayResult ? 0 : get(res, totalField);
// 假如数据变少导致总页数变少并小于当前选中页码通过getPaginationRef获取到的页码是不正确的需获取正确的页码再次执行
if (resultTotal) {
const currentTotalPage = Math.ceil(Number(resultTotal) / pageSize);
if (current > currentTotalPage) {
setPagination({
current: currentTotalPage,
});
return await fetch(opt);
}
}
if (afterFetch && isFunction(afterFetch)) {
resultItems = (await afterFetch(resultItems)) || resultItems;
}
dataSourceRef.value = resultItems;
setPagination({
total: Number(resultTotal) || 0,
});
if (opt && opt.page) {
setPagination({
current: opt.page || 1,
});
}
emit('fetch-success', {
items: unref(resultItems),
total: Number(resultTotal),
});
return resultItems;
} catch (error) {
emit('fetch-error', error);
dataSourceRef.value = [];
setPagination({
total: 0,
});
} finally {
setLoading(false);
}
}
function setTableData<T = Recordable>(values: T[]) {
dataSourceRef.value = values;
}
function getDataSource<T = Recordable>() {
return getDataSourceRef.value as T[];
}
function getRawDataSource<T = Recordable>() {
return rawDataSourceRef.value as T;
}
async function reload(opt?: FetchParams) {
return await fetch(opt);
}
onMounted(() => {
useTimeoutFn(() => {
unref(propsRef).immediate && fetch();
}, 16);
});
return {
getDataSourceRef,
getDataSource,
getRawDataSource,
getRowKey,
setTableData,
getAutoCreateKey,
fetch,
reload,
updateTableData,
updateTableDataRecord,
deleteTableDataRecord,
insertTableDataRecord,
findTableDataRecord,
handleTableChange,
};
}

View File

@@ -0,0 +1,21 @@
import { ref, ComputedRef, unref, computed, watch } from 'vue';
import type { BasicTableProps } from '../types/table';
export function useLoading(props: ComputedRef<BasicTableProps>) {
const loadingRef = ref(unref(props).loading);
watch(
() => unref(props).loading,
(loading) => {
loadingRef.value = loading;
}
);
const getLoading = computed(() => unref(loadingRef));
function setLoading(loading: boolean) {
loadingRef.value = loading;
}
return { getLoading, setLoading };
}

View File

@@ -0,0 +1,85 @@
import type { PaginationProps } from '../types/pagination';
import type { BasicTableProps } from '../types/table';
import { computed, unref, ref, ComputedRef, watch } from 'vue';
import { LeftOutlined, RightOutlined } from '@ant-design/icons-vue';
import { isBoolean } from '/@/utils/is';
import { PAGE_SIZE, PAGE_SIZE_OPTIONS } from '../const';
import { useI18n } from '/@/hooks/web/useI18n';
interface ItemRender {
page: number;
type: 'page' | 'prev' | 'next';
originalElement: any;
}
function itemRender({ page, type, originalElement }: ItemRender) {
if (type === 'prev') {
return page === 0 ? null : <LeftOutlined />;
} else if (type === 'next') {
return page === 1 ? null : <RightOutlined />;
}
return originalElement;
}
export function usePagination(refProps: ComputedRef<BasicTableProps>) {
const { t } = useI18n();
const configRef = ref<PaginationProps>({});
const show = ref(true);
watch(
() => unref(refProps).pagination,
(pagination) => {
if (!isBoolean(pagination) && pagination) {
configRef.value = {
...unref(configRef),
...(pagination ?? {}),
};
}
}
);
const getPaginationInfo = computed((): PaginationProps | boolean => {
const { pagination } = unref(refProps);
if (!unref(show) || (isBoolean(pagination) && !pagination)) {
return false;
}
return {
current: 1,
pageSize: PAGE_SIZE,
size: 'small',
defaultPageSize: PAGE_SIZE,
showTotal: (total) => t('component.table.total', { total }),
showSizeChanger: true,
pageSizeOptions: PAGE_SIZE_OPTIONS,
itemRender: itemRender,
showQuickJumper: true,
...(isBoolean(pagination) ? {} : pagination),
...unref(configRef),
};
});
function setPagination(info: Partial<PaginationProps>) {
const paginationInfo = unref(getPaginationInfo);
configRef.value = {
...(!isBoolean(paginationInfo) ? paginationInfo : {}),
...info,
};
}
function getPagination() {
return unref(getPaginationInfo);
}
function getShowPagination() {
return unref(show);
}
async function setShowPagination(flag: boolean) {
show.value = flag;
}
return { getPagination, getPaginationInfo, setShowPagination, getShowPagination, setPagination };
}

View File

@@ -0,0 +1,127 @@
import { isFunction } from '/@/utils/is';
import type { BasicTableProps, TableRowSelection } from '../types/table';
import { computed, ComputedRef, nextTick, Ref, ref, toRaw, unref, watch } from 'vue';
import { ROW_KEY } from '../const';
import { omit } from 'lodash-es';
import { findNodeAll } from '/@/utils/helper/treeHelper';
export function useRowSelection(propsRef: ComputedRef<BasicTableProps>, tableData: Ref<Recordable[]>, emit: EmitType) {
const selectedRowKeysRef = ref<string[]>([]);
const selectedRowRef = ref<Recordable[]>([]);
const getRowSelectionRef = computed((): TableRowSelection | null => {
const { rowSelection } = unref(propsRef);
if (!rowSelection) {
return null;
}
return {
// AntDV3.0 之后使用远程加载数据进行分页时,
// 默认会清空上一页选择的行数据(导致无法跨页选择),
// 将此属性设置为 true 即可解决。
preserveSelectedRowKeys: true,
selectedRowKeys: unref(selectedRowKeysRef),
onChange: (selectedRowKeys: string[]) => {
setSelectedRowKeys(selectedRowKeys);
},
...omit(rowSelection, ['onChange']),
};
});
watch(
() => unref(propsRef).rowSelection?.selectedRowKeys,
(v: string[]) => {
setSelectedRowKeys(v);
}
);
watch(
() => unref(selectedRowKeysRef),
() => {
nextTick(() => {
const { rowSelection } = unref(propsRef);
if (rowSelection) {
const { onChange } = rowSelection;
if (onChange && isFunction(onChange)) onChange(getSelectRowKeys(), getSelectRows());
}
//update-begin---author:scott ---date:2023-06-19 for【issues/503】table行选择时卡顿明显 #503---
//table行选择时卡顿明显 #503
if (unref(tableData).length > 0) {
emit('selection-change', {
keys: getSelectRowKeys(),
rows: getSelectRows(),
});
}
//update-end---author:scott ---date::2023-06-19 for【issues/503】table行选择时卡顿明显 #503---
});
},
{ deep: true }
);
const getAutoCreateKey = computed(() => {
return unref(propsRef).autoCreateKey && !unref(propsRef).rowKey;
});
const getRowKey = computed(() => {
const { rowKey } = unref(propsRef);
return unref(getAutoCreateKey) ? ROW_KEY : rowKey;
});
function setSelectedRowKeys(rowKeys: string[]) {
selectedRowKeysRef.value = rowKeys;
const allSelectedRows = findNodeAll(
toRaw(unref(tableData)).concat(toRaw(unref(selectedRowRef))),
(item) => rowKeys.includes(item[unref(getRowKey) as string]),
{
children: propsRef.value.childrenColumnName ?? 'children',
}
);
const trueSelectedRows: any[] = [];
rowKeys.forEach((key: string) => {
const found = allSelectedRows.find((item) => item[unref(getRowKey) as string] === key);
found && trueSelectedRows.push(found);
});
selectedRowRef.value = trueSelectedRows;
}
function setSelectedRows(rows: Recordable[]) {
selectedRowRef.value = rows;
}
function clearSelectedRowKeys() {
selectedRowRef.value = [];
selectedRowKeysRef.value = [];
}
function deleteSelectRowByKey(key: string) {
const selectedRowKeys = unref(selectedRowKeysRef);
const index = selectedRowKeys.findIndex((item) => item === key);
if (index !== -1) {
unref(selectedRowKeysRef).splice(index, 1);
}
}
function getSelectRowKeys() {
return unref(selectedRowKeysRef);
}
function getSelectRows<T = Recordable>() {
// const ret = toRaw(unref(selectedRowRef)).map((item) => toRaw(item));
return unref(selectedRowRef) as T[];
}
function getRowSelection() {
return unref(getRowSelectionRef)!;
}
return {
getRowSelection,
getRowSelectionRef,
getSelectRows,
getSelectRowKeys,
setSelectedRowKeys,
clearSelectedRowKeys,
deleteSelectRowByKey,
setSelectedRows,
};
}

View File

@@ -0,0 +1,168 @@
import type { BasicTableProps, TableActionType, FetchParams, BasicColumn } from '../types/table';
import type { PaginationProps } from '../types/pagination';
import type { DynamicProps } from '/#/utils';
import type { FormActionType } from '/@/components/Form';
import type { WatchStopHandle } from 'vue';
import { getDynamicProps } from '/@/utils';
import { ref, onUnmounted, unref, watch, toRaw } from 'vue';
import { isProdMode } from '/@/utils/env';
import { error } from '/@/utils/log';
type Props = Partial<DynamicProps<BasicTableProps>>;
type UseTableMethod = TableActionType & {
getForm: () => FormActionType;
};
export function useTable(tableProps?: Props): [
(instance: TableActionType, formInstance: UseTableMethod) => void,
TableActionType & {
getForm: () => FormActionType;
}
] {
const tableRef = ref<Nullable<TableActionType>>(null);
const loadedRef = ref<Nullable<boolean>>(false);
const formRef = ref<Nullable<UseTableMethod>>(null);
let stopWatch: WatchStopHandle;
function register(instance: TableActionType, formInstance: UseTableMethod) {
isProdMode() &&
onUnmounted(() => {
tableRef.value = null;
loadedRef.value = null;
});
if (unref(loadedRef) && isProdMode() && instance === unref(tableRef)) return;
tableRef.value = instance;
formRef.value = formInstance;
tableProps && instance.setProps(getDynamicProps(tableProps));
loadedRef.value = true;
stopWatch?.();
stopWatch = watch(
() => tableProps,
() => {
tableProps && instance.setProps(getDynamicProps(tableProps));
},
{
immediate: true,
deep: true,
}
);
}
function getTableInstance(): TableActionType {
const table = unref(tableRef);
if (!table) {
error('The table instance has not been obtained yet, please make sure the table is presented when performing the table operation!');
}
return table as TableActionType;
}
function getTableRef(){
return tableRef;
}
const methods: TableActionType & {
getForm: () => FormActionType;
} & {
getTableRef: () => any;
} = {
reload: async (opt?: FetchParams) => {
return await getTableInstance().reload(opt);
},
setProps: (props: Partial<BasicTableProps>) => {
getTableInstance().setProps(props);
},
redoHeight: () => {
getTableInstance().redoHeight();
},
setLoading: (loading: boolean) => {
getTableInstance().setLoading(loading);
},
getDataSource: () => {
return getTableInstance().getDataSource();
},
getRawDataSource: () => {
return getTableInstance().getRawDataSource();
},
getColumns: ({ ignoreIndex = false }: { ignoreIndex?: boolean } = {}) => {
const columns = getTableInstance().getColumns({ ignoreIndex }) || [];
return toRaw(columns);
},
setColumns: (columns: BasicColumn[]) => {
getTableInstance().setColumns(columns);
},
setTableData: (values: any[]) => {
return getTableInstance().setTableData(values);
},
setPagination: (info: Partial<PaginationProps>) => {
return getTableInstance().setPagination(info);
},
deleteSelectRowByKey: (key: string) => {
getTableInstance().deleteSelectRowByKey(key);
},
getSelectRowKeys: () => {
return toRaw(getTableInstance().getSelectRowKeys());
},
getSelectRows: () => {
return toRaw(getTableInstance().getSelectRows());
},
clearSelectedRowKeys: () => {
getTableInstance().clearSelectedRowKeys();
},
setSelectedRowKeys: (keys: string[] | number[]) => {
getTableInstance().setSelectedRowKeys(keys);
},
getPaginationRef: () => {
return getTableInstance().getPaginationRef();
},
getSize: () => {
return toRaw(getTableInstance().getSize());
},
updateTableData: (index: number, key: string, value: any) => {
return getTableInstance().updateTableData(index, key, value);
},
deleteTableDataRecord: (rowKey: string | number | string[] | number[]) => {
return getTableInstance().deleteTableDataRecord(rowKey);
},
insertTableDataRecord: (record: Recordable | Recordable[], index?: number) => {
return getTableInstance().insertTableDataRecord(record, index);
},
updateTableDataRecord: (rowKey: string | number, record: Recordable) => {
return getTableInstance().updateTableDataRecord(rowKey, record);
},
findTableDataRecord: (rowKey: string | number) => {
return getTableInstance().findTableDataRecord(rowKey);
},
getRowSelection: () => {
return toRaw(getTableInstance().getRowSelection());
},
getCacheColumns: () => {
return toRaw(getTableInstance().getCacheColumns());
},
getForm: () => {
return unref(formRef) as unknown as FormActionType;
},
setShowPagination: async (show: boolean) => {
getTableInstance().setShowPagination(show);
},
getShowPagination: () => {
return toRaw(getTableInstance().getShowPagination());
},
expandAll: () => {
getTableInstance().expandAll();
},
collapseAll: () => {
getTableInstance().collapseAll();
},
getTableRef: () => {
return getTableRef();
}
};
return [register, methods];
}

View File

@@ -0,0 +1,22 @@
import type { Ref } from 'vue';
import type { BasicTableProps, TableActionType } from '../types/table';
import { provide, inject, ComputedRef } from 'vue';
const key = Symbol('basic-table');
type Instance = TableActionType & {
wrapRef: Ref<Nullable<HTMLElement>>;
getBindValues: ComputedRef<Recordable>;
};
type RetInstance = Omit<Instance, 'getBindValues'> & {
getBindValues: ComputedRef<BasicTableProps>;
};
export function createTableContext(instance: Instance) {
provide(key, instance);
}
export function useTableContext(): RetInstance {
return inject(key) as RetInstance;
}

View File

@@ -0,0 +1,54 @@
import type { ComputedRef, Ref } from 'vue';
import type { BasicTableProps } from '../types/table';
import { computed, unref, ref, toRaw } from 'vue';
import { ROW_KEY } from '../const';
export function useTableExpand(propsRef: ComputedRef<BasicTableProps>, tableData: Ref<Recordable[]>, emit: EmitType) {
const expandedRowKeys = ref<string[]>([]);
const getAutoCreateKey = computed(() => {
return unref(propsRef).autoCreateKey && !unref(propsRef).rowKey;
});
const getRowKey = computed(() => {
const { rowKey } = unref(propsRef);
return unref(getAutoCreateKey) ? ROW_KEY : rowKey;
});
const getExpandOption = computed(() => {
const { isTreeTable } = unref(propsRef);
if (!isTreeTable) return {};
return {
expandedRowKeys: unref(expandedRowKeys),
onExpandedRowsChange: (keys: string[]) => {
expandedRowKeys.value = keys;
emit('expanded-rows-change', keys);
},
};
});
function expandAll() {
const keys = getAllKeys();
expandedRowKeys.value = keys;
}
function getAllKeys(data?: Recordable[]) {
const keys: string[] = [];
const { childrenColumnName } = unref(propsRef);
toRaw(data || unref(tableData)).forEach((item) => {
keys.push(item[unref(getRowKey) as string]);
const children = item[childrenColumnName || 'children'];
if (children?.length) {
keys.push(...getAllKeys(children));
}
});
return keys;
}
function collapseAll() {
expandedRowKeys.value = [];
}
return { getExpandOption, expandAll, collapseAll };
}

View File

@@ -0,0 +1,62 @@
import type { ComputedRef, Ref, Slots } from 'vue';
import type { BasicTableProps } from '../types/table';
import { unref, computed, h, nextTick, watchEffect } from 'vue';
import TableFooter from '../components/TableFooter.vue';
import { useEventListener } from '/@/hooks/event/useEventListener';
export function useTableFooter(
propsRef: ComputedRef<BasicTableProps>,
slots: Slots,
scrollRef: ComputedRef<{
x: string | number | true;
y: Nullable<number>;
scrollToFirstRowOnChange: boolean;
}>,
tableElRef: Ref<ComponentRef>,
getDataSourceRef: ComputedRef<Recordable>
) {
const getIsEmptyData = computed(() => {
return (unref(getDataSourceRef) || []).length === 0;
});
// 是否有展开行
const hasExpandedRow = computed(() => Object.keys(slots).includes('expandedRowRender'))
const getFooterProps = computed((): Recordable | undefined => {
const { summaryFunc, showSummary, summaryData, bordered } = unref(propsRef);
return showSummary && !unref(getIsEmptyData) ? () => h(TableFooter, {
bordered,
summaryFunc,
summaryData,
scroll: unref(scrollRef),
hasExpandedRow: hasExpandedRow.value
}) : undefined;
});
watchEffect(() => {
handleSummary();
});
function handleSummary() {
const { showSummary } = unref(propsRef);
if (!showSummary || unref(getIsEmptyData)) return;
nextTick(() => {
const tableEl = unref(tableElRef);
if (!tableEl) return;
const bodyDom = tableEl.$el.querySelector('.ant-table-content');
useEventListener({
el: bodyDom,
name: 'scroll',
listener: () => {
const footerBodyDom = tableEl.$el.querySelector('.ant-table-footer .ant-table-content') as HTMLDivElement;
if (!footerBodyDom || !bodyDom) return;
footerBodyDom.scrollLeft = bodyDom.scrollLeft;
},
wait: 0,
options: true,
});
});
}
return { getFooterProps };
}

View File

@@ -0,0 +1,51 @@
import type { ComputedRef, Slots } from 'vue';
import type { BasicTableProps, FetchParams } from '../types/table';
import { unref, computed } from 'vue';
import type { FormProps } from '/@/components/Form';
import { isFunction } from '/@/utils/is';
export function useTableForm(
propsRef: ComputedRef<BasicTableProps>,
slots: Slots,
fetch: (opt?: FetchParams | undefined) => Promise<void>,
getLoading: ComputedRef<boolean | undefined>
) {
const getFormProps = computed((): Partial<FormProps> => {
const { formConfig } = unref(propsRef);
const { submitButtonOptions, autoSubmitOnEnter} = formConfig || {};
return {
showAdvancedButton: true,
...formConfig,
submitButtonOptions: { loading: unref(getLoading), ...submitButtonOptions },
compact: true,
//update-begin-author:liusq---date:20230605--for: [issues/568]设置 autoSubmitOnEnter: false 不生效 ---
autoSubmitOnEnter: autoSubmitOnEnter,
//update-end-author:liusq---date:20230605--for: [issues/568]设置 autoSubmitOnEnter: false 不生效 ---
};
});
const getFormSlotKeys: ComputedRef<string[]> = computed(() => {
const keys = Object.keys(slots);
return keys.map((item) => (item.startsWith('form-') ? item : null)).filter((item) => !!item) as string[];
});
function replaceFormSlotKey(key: string) {
if (!key) return '';
return key?.replace?.(/form\-/, '') ?? '';
}
function handleSearchInfoChange(info: Recordable) {
const { handleSearchInfoFn } = unref(propsRef);
if (handleSearchInfoFn && isFunction(handleSearchInfoFn)) {
info = handleSearchInfoFn(info) || info;
}
fetch({ searchInfo: info, page: 1 });
}
return {
getFormProps,
replaceFormSlotKey,
getFormSlotKeys,
handleSearchInfoChange,
};
}

View File

@@ -0,0 +1,58 @@
import type { ComputedRef, Slots } from 'vue';
import type { BasicTableProps, InnerHandlers } from '../types/table';
import { unref, computed, h } from 'vue';
import TableHeader from '../components/TableHeader.vue';
import { isString } from '/@/utils/is';
import { getSlot } from '/@/utils/helper/tsxHelper';
export function useTableHeader(propsRef: ComputedRef<BasicTableProps>, slots: Slots, handlers: InnerHandlers) {
const getHeaderProps = computed((): Recordable => {
const { title, showTableSetting, titleHelpMessage, tableSetting } = unref(propsRef);
const hideTitle = !slots.tableTitle && !title && !slots.toolbar && !showTableSetting;
if (hideTitle && !isString(title)) {
return {};
}
return {
title: hideTitle
? null
: () =>
h(
TableHeader,
{
title,
titleHelpMessage,
showTableSetting,
tableSetting,
onColumnsChange: handlers.onColumnsChange,
} as Recordable,
{
...(slots.toolbar
? {
toolbar: () => getSlot(slots, 'toolbar'),
}
: {}),
...(slots.tableTitle
? {
tableTitle: () => getSlot(slots, 'tableTitle'),
}
: {}),
...(slots.headerTop
? {
headerTop: () => getSlot(slots, 'headerTop'),
}
: {}),
//添加tableTop插槽
...(slots.tableTop
? {
tableTop: () => getSlot(slots, 'tableTop'),
}
: {}),
// 添加alertAfter插槽
...(slots.alertAfter ? { alertAfter: () => getSlot(slots, 'alertAfter') } : {}),
}
),
};
});
return { getHeaderProps };
}

View File

@@ -0,0 +1,199 @@
import type { BasicTableProps, TableRowSelection, BasicColumn } from '../types/table';
import type { Ref, ComputedRef } from 'vue';
import { computed, unref, ref, nextTick, watch } from 'vue';
import { getViewportOffset } from '/@/utils/domUtils';
import { isBoolean } from '/@/utils/is';
import { useWindowSizeFn } from '/@/hooks/event/useWindowSizeFn';
import { useModalContext } from '/@/components/Modal';
import { onMountedOrActivated } from '/@/hooks/core/onMountedOrActivated';
import { useDebounceFn } from '@vueuse/core';
import componentSetting from '/@/settings/componentSetting';
export function useTableScroll(
propsRef: ComputedRef<BasicTableProps>,
tableElRef: Ref<ComponentRef>,
columnsRef: ComputedRef<BasicColumn[]>,
rowSelectionRef: ComputedRef<TableRowSelection<any> | null>,
getDataSourceRef: ComputedRef<Recordable[]>
) {
const tableHeightRef: Ref<Nullable<number>> = ref(null);
const modalFn = useModalContext();
// Greater than animation time 280
const debounceRedoHeight = useDebounceFn(redoHeight, 100);
const getCanResize = computed(() => {
const { canResize, scroll } = unref(propsRef);
return canResize && !(scroll || {}).y;
});
watch(
() => [unref(getCanResize), unref(getDataSourceRef)?.length],
() => {
debounceRedoHeight();
},
{
flush: 'post',
}
);
function redoHeight() {
nextTick(() => {
calcTableHeight();
});
}
function setHeight(heigh: number) {
tableHeightRef.value = heigh;
// Solve the problem of modal adaptive height calculation when the form is placed in the modal
modalFn?.redoModalHeight?.();
}
// No need to repeat queries
let paginationEl: HTMLElement | null;
let footerEl: HTMLElement | null;
let bodyEl: HTMLElement | null;
async function calcTableHeight() {
const { resizeHeightOffset, pagination, maxHeight, minHeight } = unref(propsRef);
const tableData = unref(getDataSourceRef);
const table = unref(tableElRef);
if (!table) return;
const tableEl: Element = table.$el;
if (!tableEl) return;
if (!bodyEl) {
//update-begin-author:taoyan date:2023-2-11 for: issues/355 前端-jeecgboot-vue3 3.4.4版本,BasicTable高度自适应功能失效,设置BasicTable组件maxHeight失效; 原因已找到,请看详情
bodyEl = tableEl.querySelector('.ant-table-tbody');
//update-end-author:taoyan date:2023-2-11 for: issues/355 前端-jeecgboot-vue3 3.4.4版本,BasicTable高度自适应功能失效,设置BasicTable组件maxHeight失效; 原因已找到,请看详情
if (!bodyEl) return;
}
const hasScrollBarY = bodyEl.scrollHeight > bodyEl.clientHeight;
const hasScrollBarX = bodyEl.scrollWidth > bodyEl.clientWidth;
if (hasScrollBarY) {
tableEl.classList.contains('hide-scrollbar-y') && tableEl.classList.remove('hide-scrollbar-y');
} else {
!tableEl.classList.contains('hide-scrollbar-y') && tableEl.classList.add('hide-scrollbar-y');
}
if (hasScrollBarX) {
tableEl.classList.contains('hide-scrollbar-x') && tableEl.classList.remove('hide-scrollbar-x');
} else {
!tableEl.classList.contains('hide-scrollbar-x') && tableEl.classList.add('hide-scrollbar-x');
}
bodyEl!.style.height = 'unset';
if (!unref(getCanResize) || ( !tableData || tableData.length === 0)) return;
await nextTick();
//Add a delay to get the correct bottomIncludeBody paginationHeight footerHeight headerHeight
const headEl = tableEl.querySelector('.ant-table-thead');
if (!headEl) return;
// Table height from bottom
const { bottomIncludeBody } = getViewportOffset(headEl);
// Table height from bottom height-custom offset
const paddingHeight = 32;
// Pager height
let paginationHeight = 2;
if (!isBoolean(pagination)) {
paginationEl = tableEl.querySelector('.ant-pagination') as HTMLElement;
if (paginationEl) {
const offsetHeight = paginationEl.offsetHeight;
paginationHeight += offsetHeight || 0;
} else {
// TODO First fix 24
paginationHeight += 24;
}
} else {
paginationHeight = -8;
}
let footerHeight = 0;
// update-begin--author:liaozhiyang---date:20240424---for【issues/1137】BasicTable自适应高度计算没有减去尾部高度
footerEl = tableEl.querySelector('.ant-table-footer');
if (footerEl) {
const offsetHeight = footerEl.offsetHeight;
footerHeight = offsetHeight || 0;
}
// update-end--author:liaozhiyang---date:20240424---for【issues/1137】BasicTable自适应高度计算没有减去尾部高度
let headerHeight = 0;
if (headEl) {
headerHeight = (headEl as HTMLElement).offsetHeight;
}
let height = bottomIncludeBody - (resizeHeightOffset || 0) - paddingHeight - paginationHeight - footerHeight - headerHeight;
// update-begin--author:liaozhiyang---date:20240603---for【TV360X-861】列表查询区域不可往上滚动
// 10+6(外层边距padding:10 + 内层padding-bottom:6)
height -= 16;
// update-end--author:liaozhiyang---date:20240603---for【TV360X-861】列表查询区域不可往上滚动
height = (height < minHeight! ? (minHeight as number) : height) ?? height;
height = (height > maxHeight! ? (maxHeight as number) : height) ?? height;
setHeight(height);
bodyEl!.style.height = `${height}px`;
}
useWindowSizeFn(calcTableHeight, 280);
onMountedOrActivated(() => {
calcTableHeight();
nextTick(() => {
debounceRedoHeight();
});
});
const getScrollX = computed(() => {
let width = 0;
// update-begin--author:liaozhiyang---date:20230922---for【QQYUN-6391】在线表单列表字段过多时,列头和数据对不齐
// if (unref(rowSelectionRef)) {
// width += 60;
// }
// update-end--author:liaozhiyang---date:20230922---for【QQYUN-6391】在线表单列表字段过多时,列头和数据对不齐
// update-begin--author:liaozhiyang---date:20230925---for【issues/5411】BasicTable 配置maxColumnWidth 未生效
const { maxColumnWidth } = unref(propsRef);
// TODO props ?? 0;
const NORMAL_WIDTH = maxColumnWidth ?? 150;
// update-end--author:liaozhiyang---date:20230925---for【issues/5411】BasicTable 配置maxColumnWidth 未生效
const columns = unref(columnsRef).filter((item) => !item.defaultHidden);
columns.forEach((item) => {
width += Number.parseInt(item.width as string) || 0;
});
const unsetWidthColumns = columns.filter((item) => !Reflect.has(item, 'width'));
const len = unsetWidthColumns.length;
if (len !== 0) {
width += len * NORMAL_WIDTH;
}
const table = unref(tableElRef);
const tableWidth = table?.$el?.offsetWidth ?? 0;
return tableWidth > width ? '100%' : width;
});
const getScrollRef = computed(() => {
const tableHeight = unref(tableHeightRef);
const { canResize, scroll } = unref(propsRef);
const { table } = componentSetting;
return {
x: unref(getScrollX),
y: canResize ? tableHeight : null,
// update-begin--author:liaozhiyang---date:20240424---for【issues/1188】BasicTable加上scrollToFirstRowOnChange类型定义
scrollToFirstRowOnChange: table.scrollToFirstRowOnChange,
// update-end--author:liaozhiyang---date:20240424---for【issues/1188】BasicTable加上scrollToFirstRowOnChange类型定义
...scroll,
};
});
return { getScrollRef, redoHeight };
}

View File

@@ -0,0 +1,20 @@
import type { ComputedRef } from 'vue';
import type { BasicTableProps, TableCustomRecord } from '../types/table';
import { unref } from 'vue';
import { isFunction } from '/@/utils/is';
export function useTableStyle(propsRef: ComputedRef<BasicTableProps>, prefixCls: string) {
function getRowClassName(record: TableCustomRecord, index: number) {
const { striped, rowClassName } = unref(propsRef);
const classNames: string[] = [];
if (striped) {
classNames.push((index || 0) % 2 === 1 ? `${prefixCls}-row__striped` : '');
}
if (rowClassName && isFunction(rowClassName)) {
classNames.push(rowClassName(record, index));
}
return classNames.filter((cls) => !!cls).join(' ');
}
return { getRowClassName };
}

View File

@@ -0,0 +1,147 @@
import type { PropType } from 'vue';
import type { PaginationProps } from './types/pagination';
import type { BasicColumn, FetchSetting, TableSetting, SorterResult, TableCustomRecord, TableRowSelection, SizeType } from './types/table';
import type { FormProps } from '/@/components/Form';
import { DEFAULT_FILTER_FN, DEFAULT_SORT_FN, FETCH_SETTING, DEFAULT_SIZE } from './const';
import { propTypes } from '/@/utils/propTypes';
export const basicProps = {
clickToRowSelect: propTypes.bool.def(true),
isTreeTable: propTypes.bool.def(false),
tableSetting: propTypes.shape<TableSetting>({}),
inset: propTypes.bool,
sortFn: {
type: Function as PropType<(sortInfo: SorterResult) => any>,
default: DEFAULT_SORT_FN,
},
filterFn: {
type: Function as PropType<(data: Partial<Recordable<string[]>>) => any>,
default: DEFAULT_FILTER_FN,
},
showTableSetting: propTypes.bool,
autoCreateKey: propTypes.bool.def(true),
striped: propTypes.bool.def(false),
showSummary: propTypes.bool,
summaryFunc: {
type: [Function, Array] as PropType<(...arg: any[]) => any[]>,
default: null,
},
summaryData: {
type: Array as PropType<Recordable[]>,
default: null,
},
indentSize: propTypes.number.def(24),
canColDrag: propTypes.bool.def(true),
api: {
type: Function as PropType<(...arg: any[]) => Promise<any>>,
default: null,
},
beforeFetch: {
type: Function as PropType<Fn>,
default: null,
},
afterFetch: {
type: Function as PropType<Fn>,
default: null,
},
handleSearchInfoFn: {
type: Function as PropType<Fn>,
default: null,
},
fetchSetting: {
type: Object as PropType<FetchSetting>,
default: () => {
return FETCH_SETTING;
},
},
// 立即请求接口
immediate: propTypes.bool.def(true),
emptyDataIsShowTable: propTypes.bool.def(true),
// 额外的请求参数
searchInfo: {
type: Object as PropType<Recordable>,
default: null,
},
// 默认的排序参数
defSort: {
type: Object as PropType<Recordable>,
default: null,
},
// 使用搜索表单
useSearchForm: propTypes.bool,
// 表单配置
formConfig: {
type: Object as PropType<Partial<FormProps>>,
default: null,
},
columns: {
type: [Array] as PropType<BasicColumn[]>,
default: () => [],
},
showIndexColumn: propTypes.bool.def(true),
indexColumnProps: {
type: Object as PropType<BasicColumn>,
default: null,
},
showActionColumn: {
type: Boolean,
default: true,
},
actionColumn: {
type: Object as PropType<BasicColumn>,
default: null,
},
ellipsis: propTypes.bool.def(true),
canResize: propTypes.bool.def(true),
clearSelectOnPageChange: propTypes.bool,
resizeHeightOffset: propTypes.number.def(0),
rowSelection: {
type: Object as PropType<TableRowSelection | null>,
default: null,
},
title: {
type: [String, Function] as PropType<string | ((data: Recordable) => string)>,
default: null,
},
titleHelpMessage: {
type: [String, Array] as PropType<string | string[]>,
},
minHeight: propTypes.number,
maxHeight: propTypes.number,
// 统一设置列最大宽度
maxColumnWidth: propTypes.number,
dataSource: {
type: Array as PropType<Recordable[]>,
default: null,
},
rowKey: {
type: [String, Function] as PropType<string | ((record: Recordable) => string)>,
default: '',
},
bordered: propTypes.bool,
pagination: {
type: [Object, Boolean] as PropType<PaginationProps | boolean>,
default: null,
},
loading: propTypes.bool,
rowClassName: {
type: Function as PropType<(record: TableCustomRecord<any>, index: number) => string>,
},
scroll: {
// update-begin--author:liaozhiyang---date:20240424---for【issues/1188】BasicTable加上scrollToFirstRowOnChange类型定义
type: Object as PropType<{ x?: number | true; y?: number; scrollToFirstRowOnChange?: boolean }>,
// update-end--author:liaozhiyang---date:20240424---for【issues/1188】BasicTable加上scrollToFirstRowOnChange类型定义
default: null,
},
beforeEditSubmit: {
type: Function as PropType<(data: { record: Recordable; index: number; key: string | number; value: any }) => Promise<any>>,
},
size: {
type: String as PropType<SizeType>,
default: DEFAULT_SIZE,
},
expandedRowKeys: {
type: Array,
default: null,
},
};

View File

@@ -0,0 +1,198 @@
import { VNodeChild } from 'vue';
export interface ColumnFilterItem {
text?: string;
value?: string;
children?: any;
}
export declare type SortOrder = 'ascend' | 'descend';
export interface RecordProps<T> {
text: any;
record: T;
index: number;
}
export interface FilterDropdownProps {
prefixCls?: string;
setSelectedKeys?: (selectedKeys: string[]) => void;
selectedKeys?: string[];
confirm?: () => void;
clearFilters?: () => void;
filters?: ColumnFilterItem[];
getPopupContainer?: (triggerNode: HTMLElement) => HTMLElement;
visible?: boolean;
}
export declare type CustomRenderFunction<T> = (record: RecordProps<T>) => VNodeChild | JSX.Element;
export interface ColumnProps<T> {
/**
* specify how content is aligned
* @default 'left'
* @type string
*/
align?: 'left' | 'right' | 'center';
/**
* ellipsize cell content, not working with sorter and filters for now.
* tableLayout would be fixed when ellipsis is true.
* @default false
* @type boolean
*/
ellipsis?: boolean;
/**
* Span of this column's title
* @type number
*/
colSpan?: number;
/**
* Display field of the data record, could be set like a.b.c
* @type string
*/
dataIndex?: string;
/**
* Default filtered values
* @type string[]
*/
defaultFilteredValue?: string[];
/**
* Default order of sorted values: 'ascend' 'descend' null
* @type string
*/
defaultSortOrder?: SortOrder;
/**
* Customized filter overlay
* @type any (slot)
*/
filterDropdown?: VNodeChild | JSX.Element | ((props: FilterDropdownProps) => VNodeChild | JSX.Element);
/**
* Whether filterDropdown is visible
* @type boolean
*/
filterDropdownOpen?: boolean;
/**
* Whether the dataSource is filtered
* @default false
* @type boolean
*/
filtered?: boolean;
/**
* Controlled filtered value, filter icon will highlight
* @type string[]
*/
filteredValue?: string[];
/**
* Customized filter icon
* @default false
* @type any
*/
filterIcon?: boolean | VNodeChild | JSX.Element;
/**
* Whether multiple filters can be selected
* @default true
* @type boolean
*/
filterMultiple?: boolean;
/**
* Filter menu config
* @type object[]
*/
filters?: ColumnFilterItem[];
/**
* Set column to be fixed: true(same as left) 'left' 'right'
* @default false
* @type boolean | string
*/
fixed?: boolean | 'left' | 'right';
/**
* Unique key of this column, you can ignore this prop if you've set a unique dataIndex
* @type string
*/
key?: string;
/**
* Renderer of the table cell. The return value should be a VNode, or an object for colSpan/rowSpan config
* @type Function | ScopedSlot
*/
customRender?: CustomRenderFunction<T> | VNodeChild | JSX.Element;
/**
* Sort function for local sort, see Array.sort's compareFunction. If you need sort buttons only, set to true
* @type boolean | Function
*/
sorter?: boolean | Function;
/**
* Order of sorted values: 'ascend' 'descend' false
* @type boolean | string
*/
sortOrder?: boolean | SortOrder;
/**
* supported sort way, could be 'ascend', 'descend'
* @default ['ascend', 'descend']
* @type string[]
*/
sortDirections?: SortOrder[];
/**
* Title of this column
* @type any (string | slot)
*/
title?: VNodeChild | JSX.Element;
/**
* Width of this column
* @type string | number
*/
width?: string | number;
/**
* Set props on per cell
* @type Function
*/
customCell?: (record: T, rowIndex: number) => object;
/**
* Set props on per header cell
* @type object
*/
customHeaderCell?: (column: ColumnProps<T>) => object;
// update-begin--author:liaozhiyang---date:20240425---for【pull/1201】添加antd的TableSummary功能兼容老的summary表尾合计
customSummaryRender?: CustomRenderFunction<T> | VNodeChild | JSX.Element;
// update-end--author:liaozhiyang---date:20240425---for【pull/1201】添加antd的TableSummary功能兼容老的summary表尾合计
/**
* Callback executed when the confirm filter button is clicked, Use as a filter event when using template or jsx
* @type Function
*/
onFilter?: (value: any, record: T) => boolean;
/**
* Callback executed when filterDropdownOpen is changed, Use as a filterDropdownVisible event when using template or jsx
* @type Function
*/
onFilterDropdownVisibleChange?: (visible: boolean) => void;
/**
* When using columns, you can setting this property to configure the properties that support the slot,
* such as slots: { filterIcon: 'XXX'}
* @type object
*/
slots?: Recordable<string>;
}

View File

@@ -0,0 +1 @@
export type ComponentType = 'Input' | 'InputNumber' | 'Select' | 'ApiSelect' | 'ApiTreeSelect' | 'Checkbox' | 'Switch' | 'DatePicker' | 'TimePicker';

View File

@@ -0,0 +1,99 @@
import Pagination from 'ant-design-vue/lib/pagination';
import { VNodeChild } from 'vue';
interface PaginationRenderProps {
page: number;
type: 'page' | 'prev' | 'next';
originalElement: any;
}
export declare class PaginationConfig extends Pagination {
position?: 'top' | 'bottom' | 'both';
}
export interface PaginationProps {
/**
* total number of data items
* @default 0
* @type number
*/
total?: number;
/**
* default initial page number
* @default 1
* @type number
*/
defaultCurrent?: number;
/**
* current page number
* @type number
*/
current?: number;
/**
* default number of data items per page
* @default 10
* @type number
*/
defaultPageSize?: number;
/**
* number of data items per page
* @type number
*/
pageSize?: number;
/**
* Whether to hide pager on single page
* @default false
* @type boolean
*/
hideOnSinglePage?: boolean;
/**
* determine whether pageSize can be changed
* @default false
* @type boolean
*/
showSizeChanger?: boolean;
/**
* specify the sizeChanger options
* @default ['10', '20', '30', '40']
* @type string[]
*/
pageSizeOptions?: string[];
/**
* determine whether you can jump to pages directly
* @default false
* @type boolean
*/
showQuickJumper?: boolean | object;
/**
* to display the total number and range
* @type Function
*/
showTotal?: (total: number, range: [number, number]) => any;
/**
* specify the size of Pagination, can be set to small
* @default ''
* @type string
*/
size?: string;
/**
* whether to setting simple mode
* @type boolean
*/
simple?: boolean;
/**
* to customize item innerHTML
* @type Function
*/
itemRender?: (props: PaginationRenderProps) => VNodeChild | JSX.Element;
}

View File

@@ -0,0 +1,478 @@
import type { VNodeChild } from 'vue';
import type { PaginationProps } from './pagination';
import type { FormProps } from '/@/components/Form';
import type { TableRowSelection as ITableRowSelection } from 'ant-design-vue/lib/table/interface';
import type { ColumnProps } from 'ant-design-vue/lib/table';
import { ComponentType } from './componentType';
import { VueNode } from '/@/utils/propTypes';
import { RoleEnum } from '/@/enums/roleEnum';
export declare type SortOrder = 'ascend' | 'descend';
export interface TableCurrentDataSource<T = Recordable> {
currentDataSource: T[];
}
export interface TableRowSelection<T = any> extends ITableRowSelection {
/**
* Callback executed when selected rows change
* @type Function
*/
onChange?: (selectedRowKeys: string[] | number[], selectedRows: T[]) => any;
/**
* Callback executed when select/deselect one row
* @type Function
*/
onSelect?: (record: T, selected: boolean, selectedRows: Object[], nativeEvent: Event) => any;
/**
* Callback executed when select/deselect all rows
* @type Function
*/
onSelectAll?: (selected: boolean, selectedRows: T[], changeRows: T[]) => any;
/**
* Callback executed when row selection is inverted
* @type Function
*/
onSelectInvert?: (selectedRows: string[] | number[]) => any;
}
export interface TableCustomRecord<T> {
record?: T;
index?: number;
}
export interface ExpandedRowRenderRecord<T> extends TableCustomRecord<T> {
indent?: number;
expanded?: boolean;
}
export interface ColumnFilterItem {
text?: string;
value?: string;
children?: any;
}
export interface TableCustomRecord<T = Recordable> {
record?: T;
index?: number;
}
export interface SorterResult {
column: ColumnProps;
order: SortOrder;
field: string;
columnKey: string;
}
export interface FetchParams {
searchInfo?: Recordable;
page?: number;
sortInfo?: Recordable;
filterInfo?: Recordable;
}
export interface GetColumnsParams {
ignoreIndex?: boolean;
ignoreAction?: boolean;
sort?: boolean;
}
export type SizeType = 'middle' | 'small' | 'large';
export interface TableActionType {
reload: (opt?: FetchParams) => Promise<void>;
getSelectRows: <T = Recordable>() => T[];
clearSelectedRowKeys: () => void;
expandAll: () => void;
collapseAll: () => void;
getSelectRowKeys: () => string[];
deleteSelectRowByKey: (key: string) => void;
setPagination: (info: Partial<PaginationProps>) => void;
setTableData: <T = Recordable>(values: T[]) => void;
updateTableDataRecord: (rowKey: string | number, record: Recordable) => Recordable | void;
deleteTableDataRecord: (rowKey: string | number | string[] | number[]) => void;
insertTableDataRecord: (record: Recordable, index?: number) => Recordable | void;
findTableDataRecord: (rowKey: string | number) => Recordable | void;
getColumns: (opt?: GetColumnsParams) => BasicColumn[];
setColumns: (columns: BasicColumn[] | string[]) => void;
getDataSource: <T = Recordable>() => T[];
getRawDataSource: <T = Recordable>() => T;
setLoading: (loading: boolean) => void;
setProps: (props: Partial<BasicTableProps>) => void;
redoHeight: () => void;
setSelectedRowKeys: (rowKeys: string[] | number[]) => void;
getPaginationRef: () => PaginationProps | boolean;
getSize: () => SizeType;
getRowSelection: () => TableRowSelection<Recordable>;
getCacheColumns: () => BasicColumn[];
emit?: EmitType;
updateTableData: (index: number, key: string, value: any) => Recordable;
setShowPagination: (show: boolean) => Promise<void>;
getShowPagination: () => boolean;
setCacheColumnsByField?: (dataIndex: string | undefined, value: BasicColumn) => void;
}
export interface FetchSetting {
// 请求接口当前页数
pageField: string;
// 每页显示多少条
sizeField: string;
// 请求结果列表字段 支持 a.b.c
listField: string;
// 请求结果总数字段 支持 a.b.c
totalField: string;
}
export interface TableSetting {
// 是否显示刷新按钮
redo?: boolean;
// 是否显示尺寸调整按钮
size?: boolean;
// 是否显示字段调整按钮
setting?: boolean;
// 缓存“字段调整”配置的key用于页面上有多个表格需要区分的情况
cacheKey?: string;
// 是否显示全屏按钮
fullScreen?: boolean;
}
export interface BasicTableProps<T = any> {
// 点击行选中
clickToRowSelect?: boolean;
isTreeTable?: boolean;
// 自定义排序方法
sortFn?: (sortInfo: SorterResult) => any;
// 排序方法
filterFn?: (data: Partial<Recordable<string[]>>) => any;
// 取消表格的默认padding
inset?: boolean;
// 显示表格设置
showTableSetting?: boolean;
// 表格上方操作按钮设置
tableSetting?: TableSetting;
// 斑马纹
striped?: boolean;
// 是否自动生成key
autoCreateKey?: boolean;
// 计算合计行的方法
summaryFunc?: (...arg: any) => Recordable[];
// 自定义合计表格内容
summaryData?: Recordable[];
// 是否显示合计行
showSummary?: boolean;
// 是否可拖拽列
canColDrag?: boolean;
// 接口请求对象
api?: (...arg: any) => Promise<any>;
// 请求之前处理参数
beforeFetch?: Fn;
// 自定义处理接口返回参数
afterFetch?: Fn;
// 查询条件请求之前处理
handleSearchInfoFn?: Fn;
// 请求接口配置
fetchSetting?: Partial<FetchSetting>;
// 立即请求接口
immediate?: boolean;
// 在开起搜索表单的时候,如果没有数据是否显示表格
emptyDataIsShowTable?: boolean;
// 额外的请求参数
searchInfo?: Recordable;
// 默认的排序参数
defSort?: Recordable;
// 使用搜索表单
useSearchForm?: boolean;
// 表单配置
formConfig?: Partial<FormProps>;
// 列配置
columns: BasicColumn[];
// 统一设置列最大宽度
maxColumnWidth?: number;
// 是否显示序号列
showIndexColumn?: boolean;
// 序号列配置
indexColumnProps?: BasicColumn;
// 是否显示操作列
showActionColumn?: boolean;
// 操作列配置
actionColumn?: BasicColumn;
// 文本超过宽度是否显示。。。
ellipsis?: boolean;
// 是否可以自适应高度
canResize?: boolean;
// 自适应高度偏移, 计算结果-偏移量
resizeHeightOffset?: number;
// 在分页改变的时候清空选项
clearSelectOnPageChange?: boolean;
//
rowKey?: string | ((record: Recordable) => string);
// 数据
dataSource?: Recordable[];
// 标题右侧提示
titleHelpMessage?: string | string[];
// 表格最小高度
minHeight?: number;
// 表格滚动最大高度
maxHeight?: number;
// 是否显示边框
bordered?: boolean;
// 分页配置
pagination?: PaginationProps | boolean;
// loading加载
loading?: boolean;
/**
* The column contains children to display
* @default 'children'
* @type string | string[]
*/
childrenColumnName?: string;
/**
* Override default table elements
* @type object
*/
components?: object;
/**
* Expand all rows initially
* @default false
* @type boolean
*/
defaultExpandAllRows?: boolean;
/**
* Initial expanded row keys
* @type string[]
*/
defaultExpandedRowKeys?: string[];
/**
* Current expanded row keys
* @type string[]
*/
expandedRowKeys?: string[];
/**
* Expanded container render for each row
* @type Function
*/
expandedRowRender?: (record?: ExpandedRowRenderRecord<T>) => VNodeChild | JSX.Element;
/**
* Customize row expand Icon.
* @type Function | VNodeChild
*/
expandIcon?: Function | VNodeChild | JSX.Element;
/**
* Whether to expand row by clicking anywhere in the whole row
* @default false
* @type boolean
*/
expandRowByClick?: boolean;
/**
* The index of `expandIcon` which column will be inserted when `expandIconAsCell` is false. default 0
*/
expandIconColumnIndex?: number;
/**
* Table footer renderer
* @type Function | VNodeChild
*/
footer?: Function | VNodeChild | JSX.Element;
/**
* Indent size in pixels of tree data
* @default 15
* @type number
*/
indentSize?: number;
/**
* i18n text including filter, sort, empty text, etc
* @default { filterConfirm: 'Ok', filterReset: 'Reset', emptyText: 'No Data' }
* @type object
*/
locale?: object;
/**
* Row's className
* @type Function
*/
rowClassName?: (record: TableCustomRecord<T>, index: number) => string;
/**
* Row selection config
* @type object
*/
rowSelection?: TableRowSelection;
/**
* Set horizontal or vertical scrolling, can also be used to specify the width and height of the scroll area.
* It is recommended to set a number for x, if you want to set it to true,
* you need to add style .ant-table td { white-space: nowrap; }.
* @type object
*/
// update-begin--author:liaozhiyang---date:20240424---for【issues/1188】BasicTable加上scrollToFirstRowOnChange类型定义
scroll?: { x?: number | true | 'max-content'; y?: number; scrollToFirstRowOnChange?: boolean };
// update-end--author:liaozhiyang---date:20240424---for【issues/1188】BasicTable加上scrollToFirstRowOnChange类型定义
/**
* Whether to show table header
* @default true
* @type boolean
*/
showHeader?: boolean;
/**
* Size of table
* @default 'default'
* @type string
*/
size?: SizeType;
/**
* Table title renderer
* @type Function | ScopedSlot
*/
title?: VNodeChild | JSX.Element | string | ((data: Recordable) => string);
/**
* Set props on per header row
* @type Function
*/
customHeaderRow?: (column: ColumnProps, index: number) => object;
/**
* Set props on per row
* @type Function
*/
customRow?: (record: T, index: number) => object;
/**
* `table-layout` attribute of table element
* `fixed` when header/columns are fixed, or using `column.ellipsis`
*
* @see https://developer.mozilla.org/en-US/docs/Web/CSS/table-layout
* @version 1.5.0
*/
tableLayout?: 'auto' | 'fixed' | string;
/**
* the render container of dropdowns in table
* @param triggerNode
* @version 1.5.0
*/
getPopupContainer?: (triggerNode?: HTMLElement) => HTMLElement;
/**
* Data can be changed again before rendering.
* The default configuration of general user empty data.
* You can configured globally through [ConfigProvider](https://antdv.com/components/config-provider-cn/)
*
* @version 1.5.4
*/
transformCellText?: Function;
/**
* Callback executed before editable cell submit value, not for row-editor
*
* The cell will not submit data while callback return false
*/
beforeEditSubmit?: (data: { record: Recordable; index: number; key: string | number; value: any }) => Promise<any>;
/**
* Callback executed when pagination, filters or sorter is changed
* @param pagination
* @param filters
* @param sorter
* @param currentDataSource
*/
onChange?: (pagination: any, filters: any, sorter: any, extra: any) => void;
/**
* Callback executed when the row expand icon is clicked
*
* @param expanded
* @param record
*/
onExpand?: (expande: boolean, record: T) => void;
/**
* Callback executed when the expanded rows change
* @param expandedRows
*/
onExpandedRowsChange?: (expandedRows: string[] | number[]) => void;
onColumnsChange?: (data: ColumnChangeParam[]) => void;
}
export type CellFormat = string | ((text: string, record: Recordable, index: number) => string | number) | Map<string | number, any>;
// @ts-ignore
export interface BasicColumn extends ColumnProps<Recordable> {
children?: BasicColumn[];
filters?: {
text: string;
value: string;
children?: unknown[] | (((props: Record<string, unknown>) => unknown[]) & (() => unknown[]) & (() => unknown[]));
}[];
//
flag?: 'INDEX' | 'DEFAULT' | 'CHECKBOX' | 'RADIO' | 'ACTION';
customTitle?: VueNode;
slots?: Recordable;
// slots的备份兼容老的写法转成新写法避免控制台警告
slotsBak?: Recordable;
// Whether to hide the column by default, it can be displayed in the column configuration
defaultHidden?: boolean;
// Help text for table column header
helpMessage?: string | string[];
format?: CellFormat;
// Editable
edit?: boolean;
editRow?: boolean;
editable?: boolean;
editComponent?: ComponentType;
editComponentProps?: Recordable;
editRule?: boolean | ((text: string, record: Recordable) => Promise<string>);
editValueMap?: (value: any) => string;
onEditRow?: () => void;
// 权限编码控制是否显示
auth?: RoleEnum | RoleEnum[] | string | string[];
// 业务控制是否显示
ifShow?: boolean | ((column: BasicColumn) => boolean);
//compType-用于记录类型
compType?: string;
// update-begin--author:liaozhiyang---date:20240425---for【pull/1201】添加antd的TableSummary功能兼容老的summary表尾合计
customSummaryRender?: (opt: {
value: any;
text: any;
record: Recordable;
index: number;
renderIndex?: number;
column: BasicColumn;
}) => any | VNodeChild | JSX.Element;
// update-end--author:liaozhiyang---date:20240425---for【pull/1201】添加antd的TableSummary功能兼容老的summary表尾合计
}
export type ColumnChangeParam = {
dataIndex: string;
fixed: boolean | 'left' | 'right' | undefined;
visible: boolean;
};
export interface InnerHandlers {
onColumnsChange: (data: ColumnChangeParam[]) => void;
}

View File

@@ -0,0 +1,32 @@
import { ButtonProps } from 'ant-design-vue/es/button/buttonTypes';
import { TooltipProps } from 'ant-design-vue/es/tooltip/Tooltip';
import { RoleEnum } from '/@/enums/roleEnum';
export interface ActionItem extends ButtonProps {
onClick?: Fn;
label?: string;
color?: 'success' | 'error' | 'warning';
icon?: string;
popConfirm?: PopConfirm;
disabled?: boolean;
divider?: boolean;
// 权限编码控制是否显示
auth?: RoleEnum | RoleEnum[] | string | string[];
// 业务控制是否显示
ifShow?: boolean | ((action: ActionItem) => boolean);
tooltip?: string | TooltipProps;
// 自定义类名
class?: string | Record<string, boolean> | any[];
// 自定义图标颜色
iconColor?: string;
}
export interface PopConfirm {
title: string;
okText?: string;
cancelText?: string;
confirm: Fn;
cancel?: Fn;
icon?: string;
placement?: string;
overlayClassName?: string;
}