refactor(form): 重构 table/group-list 目录结构与新增行逻辑

- 将 table 相关文件迁移至 containers/table 与 containers/table-group-list 目录
- 将新增行处理统一上移至 TableGroupList,通过 add 事件触发
- 抽取 TableGroupListCommonConfig 公共配置类型
- useFullscreen 内聚管理 zIndex
- TableColumnConfig 支持 text 作为 label 别名

Made-with: Cursor
This commit is contained in:
roymondchen 2026-04-24 15:45:15 +08:00
parent b5af91f86c
commit 3c41091f96
18 changed files with 148 additions and 184 deletions

View File

@ -1,11 +1,11 @@
import type { DataSchema, DataSourceFieldType, DataSourceSchema } from '@tmagic/core';
import { type CascaderOption, defineFormItem, type FormConfig } from '@tmagic/form';
import { type CascaderOption, type FormConfig, type TabConfig } from '@tmagic/form';
import { dataSourceTemplateRegExp, getKeysArray, isNumber } from '@tmagic/utils';
import BaseFormConfig from './formConfigs/base';
import HttpFormConfig from './formConfigs/http';
const dataSourceFormConfig = defineFormItem({
const dataSourceFormConfig: TabConfig = {
type: 'tab',
items: [
{
@ -73,9 +73,13 @@ const dataSourceFormConfig = defineFormItem({
],
},
],
});
};
const fillConfig = (config: FormConfig): FormConfig => [...BaseFormConfig(), ...config, dataSourceFormConfig];
const fillConfig = <T = never>(config: FormConfig<T>): FormConfig<T> => [
...BaseFormConfig(),
...config,
dataSourceFormConfig,
];
export const getFormConfig = (type: string, configs: Record<string, FormConfig>): FormConfig => {
switch (type) {

View File

@ -701,28 +701,43 @@ export interface PanelConfig<T = never> extends FormItem, ContainerCommonConfig<
schematic?: string;
}
export interface TableColumnConfig extends FormItem {
export interface TableGroupListCommonConfig extends FormItem {
type: 'table' | 'groupList' | 'group-list';
enableToggleMode?: boolean;
/** 最大行数 */
max?: number;
enum?: any[];
/** 是否显示添加按钮 */
addable?: (mForm: FormState | undefined, data: any) => boolean | 'undefined' | boolean;
/** 新增的默认行,可以是函数动态生成或静态对象 */
defaultAdd?: ((mForm: FormState | undefined, data: any) => any) | Record<string, any>;
/** table 新增行时前置回调 */
beforeAddRow?: (mForm: FormState | undefined, data: any) => boolean | Promise<boolean>;
}
export interface TableColumnConfig<T = never> extends FormItem {
name?: string;
label: string;
label?: string;
text?: string;
width?: string | number;
sortable?: boolean;
items?: FormConfig;
itemsFunction?: (row: any) => FormConfig;
items?: FormConfig<T>;
itemsFunction?: (row: any) => FormConfig<T>;
titleTip?: FilterFunction<string>;
type?: string;
addButtonConfig?: {
props?: Record<string, any>;
text?: string;
};
}
/**
*
*/
export interface TableConfig extends FormItem {
type: 'table' | 'groupList' | 'group-list';
items: TableColumnConfig[];
tableItems?: TableColumnConfig[];
groupItems?: TableColumnConfig[];
enableToggleMode?: boolean;
/** 最大行数 */
max?: number;
export interface TableConfig<T = never> extends TableGroupListCommonConfig {
items: TableColumnConfig<T>[];
tableItems?: TableColumnConfig<T>[];
groupItems?: TableColumnConfig<T>[];
/** 最大高度 */
maxHeight?: number | string;
border?: boolean;
@ -731,9 +746,6 @@ export interface TableConfig extends FormItem {
/** 操作栏宽度 */
operateColWidth?: number | string;
pagination?: boolean;
enum?: any[];
/** 是否显示添加按钮 */
addable?: (mForm: FormState | undefined, data: any) => boolean | 'undefined' | boolean;
/** 是否显示删除按钮 */
delete?: (model: any, index: number, values: any) => boolean | boolean;
copyable?: (model: any, data: any) => boolean | boolean;
@ -741,8 +753,6 @@ export interface TableConfig extends FormItem {
importable?: (mForm: FormState | undefined, data: any) => boolean | 'undefined' | boolean;
/** 是否显示checkbox */
selection?: (mForm: FormState | undefined, data: any) => boolean | boolean | 'single';
/** 新增的默认行,可以是函数动态生成或静态对象 */
defaultAdd?: ((mForm: FormState | undefined, data: any) => any) | Record<string, any>;
copyHandler?: (mForm: FormState | undefined, data: any) => any;
onSelect?: (mForm: FormState | undefined, data: any) => any;
/** @deprecated 请使用 defaultSort */
@ -760,20 +770,12 @@ export interface TableConfig extends FormItem {
itemExtra?: string | FilterFunction<string>;
titleTip?: FilterFunction<string>;
rowKey?: string;
/** table 新增行时前置回调 */
beforeAddRow?: (mForm: FormState | undefined, data: any) => boolean | Promise<boolean>;
addButtonConfig?: {
props?: Record<string, any>;
text?: string;
};
sort?: boolean;
sortKey?: string;
}
export interface GroupListConfig<T = never> extends FormItem {
type: 'table' | 'groupList' | 'group-list';
export interface GroupListConfig<T = never> extends TableGroupListCommonConfig {
span?: number;
enableToggleMode?: boolean;
items: FormConfig<T>;
groupItems?: FormConfig<T>;
tableItems?: FormConfig<T>;
@ -788,9 +790,6 @@ export interface GroupListConfig<T = never> extends FormItem {
*
*/
defaultExpandQuantity?: number;
addable?: (mForm: FormState | undefined, data: any) => boolean | 'undefined' | boolean;
/** 新增的默认值,可以是函数动态生成或静态对象 */
defaultAdd?: ((mForm: FormState | undefined, data: any) => any) | Record<string, any>;
delete?: (model: any, index: number | string | symbol, values: any) => boolean | boolean;
copyable?: FilterFunction<boolean>;
movable?: (
@ -800,13 +799,6 @@ export interface GroupListConfig<T = never> extends FormItem {
groupModel: any,
) => boolean | boolean;
moveSpecifyLocation?: boolean;
addButtonConfig?: {
props?: Record<string, any>;
text?: string;
};
/** 最大行数 */
max?: number;
beforeAddRow?: (mForm: FormState | undefined, data: any) => boolean;
}
interface StepItemConfig<T = never> extends FormItem, ContainerCommonConfig<T> {

View File

@ -29,20 +29,16 @@
<div class="m-fields-group-list-footer">
<slot name="toggle-button"></slot>
<div style="display: flex; justify-content: flex-end; flex: 1">
<slot name="add-button" :trigger="addHandler"></slot>
<slot name="add-button"></slot>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { inject } from 'vue';
import { cloneDeep } from 'lodash-es';
import { tMagicMessage } from '@tmagic/design';
import type { ContainerChangeEventData, FormState, GroupListConfig } from '../schema';
import { initValue } from '../utils/form';
import type { ContainerChangeEventData, GroupListConfig } from '../schema';
import MFieldsGroupListItem from './GroupListItem.vue';
@ -68,59 +64,10 @@ const emit = defineEmits<{
addDiffCount: [];
}>();
const mForm = inject<FormState | undefined>('mForm');
const changeHandler = (v: any, eventData: ContainerChangeEventData) => {
emit('change', props.model, eventData);
};
const addHandler = async () => {
if (!props.name) return false;
if (props.config.max && props.model[props.name].length >= props.config.max) {
tMagicMessage.error(`最多新增配置不能超过${props.config.max}`);
return;
}
if (typeof props.config.beforeAddRow === 'function') {
const beforeCheckRes = await props.config.beforeAddRow(mForm, {
model: props.model[props.name],
formValue: mForm?.values,
prop: props.prop,
});
if (!beforeCheckRes) return;
}
let initValues = {};
if (typeof props.config.defaultAdd === 'function') {
initValues = await props.config.defaultAdd(mForm, {
model: props.model[props.name],
formValue: mForm?.values,
prop: props.prop,
config: props.config,
});
} else if (props.config.defaultAdd) {
initValues = props.config.defaultAdd;
}
const groupValue = await initValue(mForm, {
config: props.config.items,
initValues,
});
props.model[props.name].push(groupValue);
emit('change', props.model[props.name], {
changeRecords: [
{
propPath: `${props.prop}.${props.model[props.name].length - 1}`,
value: groupValue,
},
],
});
};
const removeHandler = (index: number) => {
if (!props.name) return false;

View File

@ -1,6 +1,7 @@
<template>
<component
:is="displayMode === 'table' ? MFormTable : MFormGroupList"
ref="tableGroupList"
v-bind="$attrs"
:model="model"
:name="`${name}`"
@ -17,6 +18,7 @@
@change="onChange"
@select="onSelect"
@addDiffCount="onAddDiffCount"
@add="onAdd"
>
<template #toggle-button>
<TMagicButton
@ -29,16 +31,15 @@
</TMagicButton>
</template>
<template #add-button="{ trigger }">
<template #add-button v-if="addable">
<TMagicButton
v-if="addable"
:class="displayMode === 'table' ? 'm-form-table-add-button' : ''"
:size="addButtonSize"
:plain="displayMode === 'table'"
:icon="Plus"
:disabled="disabled"
v-bind="currentConfig.addButtonConfig?.props || { type: 'primary' }"
@click="trigger"
@click="newHandler"
>
{{ currentConfig.addButtonConfig?.text || (displayMode === 'table' ? '新增一行' : '新增') }}
</TMagicButton>
@ -47,16 +48,17 @@
</template>
<script setup lang="ts">
import { computed, inject, ref } from 'vue';
import { computed, ref, useTemplateRef } from 'vue';
import { Grid, Plus } from '@element-plus/icons-vue';
import { TMagicButton } from '@tmagic/design';
import type { FormState, GroupListConfig, TableConfig } from '@tmagic/form-schema';
import type { GroupListConfig, TableConfig } from '@tmagic/form-schema';
import type { ContainerChangeEventData } from '../schema';
import type { ContainerChangeEventData } from '../../schema';
import MFormGroupList from '../GroupList.vue';
import MFormTable from '../table/Table.vue';
import MFormGroupList from './GroupList.vue';
import { useAdd } from './useAdd';
defineOptions({
name: 'MFormTableGroupList',
@ -81,28 +83,7 @@ const props = defineProps<{
const emit = defineEmits(['change', 'select', 'addDiffCount']);
const mForm = inject<FormState | undefined>('mForm');
const addable = computed(() => {
const modelName = props.name || props.config.name || '';
if (!modelName) return false;
if (!props.model[modelName].length) {
return true;
}
if (typeof props.config.addable === 'function') {
return props.config.addable(mForm, {
model: props.model[modelName],
formValue: mForm?.values,
prop: props.prop,
config: props.config,
});
}
return typeof props.config.addable === 'undefined' ? true : props.config.addable;
});
const { addable, newHandler } = useAdd(props, emit);
const isGroupListType = (type: string | undefined) => type === 'groupList' || type === 'group-list';
@ -178,4 +159,15 @@ const toggleDisplayMode = () => {
const onChange = (v: any, eventData?: ContainerChangeEventData) => emit('change', v, eventData);
const onSelect = (...args: any[]) => emit('select', ...args);
const onAddDiffCount = () => emit('addDiffCount');
const onAdd = (rows: any[]) => {
rows.forEach((row: any) => {
newHandler(row);
});
};
const tableGroupListRef = useTemplateRef<InstanceType<typeof MFormTable>>('tableGroupList');
defineExpose({
toggleRowSelection: (row: any, selected: boolean) => tableGroupListRef.value?.toggleRowSelection?.(row, selected),
});
</script>

View File

@ -1,18 +1,43 @@
import { inject } from 'vue';
import { computed, inject } from 'vue';
import { tMagicMessage } from '@tmagic/design';
import type { FormConfig, FormState } from '@tmagic/form-schema';
import type { FormConfig, FormState, TableConfig, TableGroupListCommonConfig } from '@tmagic/form-schema';
import { initValue } from '../utils/form';
import type { TableProps } from './type';
import { initValue } from '../../utils/form';
import type { TableProps } from '../table/type';
export const useAdd = (
props: TableProps,
emit: (event: 'select' | 'change' | 'addDiffCount', ...args: any[]) => void,
props: Pick<TableProps, 'name' | 'model' | 'prop' | 'sortKey'> & {
config: Pick<TableGroupListCommonConfig, 'addable' | 'max' | 'beforeAddRow' | 'defaultAdd' | 'enum'> &
Pick<TableConfig, 'key' | 'name'> & {
items: { name?: string | number }[];
};
},
emit: (event: 'change', ...args: any[]) => void,
) => {
const mForm = inject<FormState | undefined>('mForm');
const addable = computed(() => {
const modelName = props.name || props.config.name || '';
if (!modelName) return false;
if (!props.model[modelName].length) {
return true;
}
if (typeof props.config.addable === 'function') {
return props.config.addable(mForm, {
model: props.model[modelName],
formValue: mForm?.values,
prop: props.prop,
config: props.config,
});
}
return typeof props.config.addable === 'undefined' ? true : props.config.addable;
});
const newHandler = async (row?: any) => {
const modelName = props.name || props.config.name || '';
@ -91,6 +116,7 @@ export const useAdd = (
};
return {
addable,
newHandler,
};
};

View File

@ -38,7 +38,7 @@ import { cloneDeep } from 'lodash-es';
import { TMagicButton, TMagicTooltip } from '@tmagic/design';
import type { FormState, TableConfig } from '../schema';
import type { FormState, TableConfig } from '../../schema';
const emit = defineEmits(['change']);

View File

@ -58,7 +58,7 @@
>清空</TMagicButton
>
</div>
<slot name="add-button" :trigger="newHandler"></slot>
<slot name="add-button"></slot>
</div>
<div class="bottom" style="text-align: right" v-if="config.pagination">
@ -80,16 +80,15 @@
</template>
<script setup lang="ts">
import { computed, ref, useTemplateRef, watch } from 'vue';
import { computed, ref, useTemplateRef } from 'vue';
import { FullScreen } from '@element-plus/icons-vue';
import { TMagicButton, TMagicPagination, TMagicTable, TMagicTooltip, TMagicUpload, useZIndex } from '@tmagic/design';
import { TMagicButton, TMagicPagination, TMagicTable, TMagicTooltip, TMagicUpload } from '@tmagic/design';
import type { SortProp } from '../schema';
import { sortChange } from '../utils/form';
import type { SortProp } from '../../schema';
import { sortChange } from '../../utils/form';
import type { TableProps } from './type';
import { useAdd } from './useAdd';
import { useFullscreen } from './useFullscreen';
import { useImport } from './useImport';
import { usePagination } from './usePagination';
@ -109,7 +108,7 @@ const props = withDefaults(defineProps<TableProps>(), {
isCompare: false,
});
const emit = defineEmits(['change', 'select', 'addDiffCount']);
const emit = defineEmits(['change', 'select', 'addDiffCount', 'add']);
const modelName = computed(() => props.name || props.config.name || '');
const tMagicTableRef = useTemplateRef<InstanceType<typeof TMagicTable>>('tMagicTable');
@ -119,22 +118,13 @@ const { pageSize, currentPage, paginationData, handleSizeChange, handleCurrentCh
modelName,
);
const { nextZIndex } = useZIndex();
const updateKey = ref(1);
const fullscreenZIndex = ref(nextZIndex());
const { newHandler } = useAdd(props, emit);
const { columns } = useTableColumns(props, emit, currentPage, pageSize, modelName);
useSortable(props, emit, tMagicTableRef, modelName, updateKey);
const { isFullscreen, toggleFullscreen } = useFullscreen();
const { isFullscreen, fullscreenZIndex, toggleFullscreen } = useFullscreen();
watch(isFullscreen, (value) => {
if (value) {
fullscreenZIndex.value = nextZIndex();
}
});
const { importable, excelHandler, clearHandler } = useImport(props, emit, newHandler);
const { importable, excelHandler, clearHandler } = useImport(props, emit);
const { selectHandle, toggleRowSelection } = useSelection(props, emit, tMagicTableRef);
const data = computed(() => (props.config.pagination ? paginationData.value : props.model[modelName.value]));

View File

@ -0,0 +1,31 @@
import { ref, watch } from 'vue';
import { useZIndex } from '@tmagic/design';
export const useFullscreen = () => {
const { nextZIndex } = useZIndex();
const fullscreenZIndex = ref(nextZIndex());
const isFullscreen = ref(false);
const toggleFullscreen = () => {
if (isFullscreen.value) {
isFullscreen.value = false;
} else {
isFullscreen.value = true;
}
};
watch(isFullscreen, (value) => {
if (value) {
fullscreenZIndex.value = nextZIndex();
}
});
return {
isFullscreen,
fullscreenZIndex,
toggleFullscreen,
};
};

View File

@ -8,8 +8,7 @@ import type { TableProps } from './type';
export const useImport = (
props: TableProps,
emit: (event: 'select' | 'change' | 'addDiffCount', ...args: any[]) => void,
newHandler: (row: any) => void,
emit: (event: 'select' | 'change' | 'addDiffCount' | 'add', ...args: any[]) => void,
) => {
const mForm = inject<FormState | undefined>('mForm');
const modelName = computed(() => props.name || props.config.name || '');
@ -41,9 +40,7 @@ export const useImport = (
pdata.SheetNames.forEach((sheetName: string) => {
const arr = (globalThis as any).XLSX.utils.sheet_to_json(pdata.Sheets[sheetName], { header: 1 });
if (arr?.[0]) {
arr.forEach((row: any) => {
newHandler(row);
});
emit('add', arr);
}
setTimeout(() => {
excelBtn.value?.clearFiles();

View File

@ -1,6 +1,6 @@
import { computed, type Ref, ref } from 'vue';
import { getDataByPage } from '../utils/form';
import { getDataByPage } from '../../utils/form';
import type { TableProps } from './type';

View File

@ -4,7 +4,7 @@ import type { default as SortableType, SortableEvent } from 'sortablejs';
import { type TMagicTable } from '@tmagic/design';
import type { FormState } from '@tmagic/form-schema';
import { sortArray } from '../utils/form';
import { sortArray } from '../../utils/form';
import type { TableProps } from './type';

View File

@ -5,9 +5,9 @@ import { cloneDeep } from 'lodash-es';
import { type TableColumnOptions, TMagicIcon, TMagicTooltip } from '@tmagic/design';
import type { FormItemConfig, FormState, TableColumnConfig } from '@tmagic/form-schema';
import Container from '../containers/Container.vue';
import type { ContainerChangeEventData } from '../schema';
import { display as displayFunc, getDataByPage, sortArray } from '../utils/form';
import type { ContainerChangeEventData } from '../../schema';
import { display as displayFunc, getDataByPage, sortArray } from '../../utils/form';
import Container from '../Container.vue';
import ActionsColumn from './ActionsColumn.vue';
import SortColumn from './SortColumn.vue';
@ -187,7 +187,7 @@ export const useTableColumns = (
columns.push({
props: {
prop: column.name,
label: column.label,
label: column.label || column.text,
width: column.width,
sortable: column.sortable,
sortOrders: ['ascending', 'descending'],
@ -223,7 +223,10 @@ export const useTableColumns = (
gap: '5px',
},
},
[h('span', column.label), h(TMagicIcon, {}, { default: () => h(WarningFilled) })],
[
h('span', column.label || column.text),
h(TMagicIcon, {}, { default: () => h(WarningFilled) }),
],
),
content: () =>
h('div', {

View File

@ -32,9 +32,9 @@ export { default as MFlexLayout } from './containers/FlexLayout.vue';
export { default as MPanel } from './containers/Panel.vue';
export { default as MRow } from './containers/Row.vue';
export { default as MTabs } from './containers/Tabs.vue';
export { default as MTable } from './containers/TableGroupList.vue';
export { default as MGroupList } from './containers/TableGroupList.vue';
export { default as MTableGroupList } from './containers/TableGroupList.vue';
export { default as MTable } from './containers/table-group-list/TableGroupList.vue';
export { default as MGroupList } from './containers/table-group-list/TableGroupList.vue';
export { default as MTableGroupList } from './containers/table-group-list/TableGroupList.vue';
export { default as MText } from './fields/Text.vue';
export { default as MNumber } from './fields/Number.vue';
export { default as MNumberRange } from './fields/NumberRange.vue';

View File

@ -24,7 +24,7 @@ import FlexLayout from './containers/FlexLayout.vue';
import Panel from './containers/Panel.vue';
import Row from './containers/Row.vue';
import MStep from './containers/Step.vue';
import TableGroupList from './containers/TableGroupList.vue';
import TableGroupList from './containers/table-group-list/TableGroupList.vue';
import Tabs from './containers/Tabs.vue';
import Cascader from './fields/Cascader.vue';
import Checkbox from './fields/Checkbox.vue';

View File

@ -1,18 +0,0 @@
import { ref } from 'vue';
export const useFullscreen = () => {
const isFullscreen = ref(false);
const toggleFullscreen = () => {
if (isFullscreen.value) {
isFullscreen.value = false;
} else {
isFullscreen.value = true;
}
};
return {
isFullscreen,
toggleFullscreen,
};
};