refactor(form): 抽出 TableGroupList 父组件统一管理 Table/GroupList 切换

- 新增 TableGroupList 父组件集中持有 displayMode 状态并派生两种形态的
  config,避免原实现通过直接改写 props.config 来触发视图切换
- 将公共的 toggle/add 按钮上移到 TableGroupList,Table/GroupList 通过
  具名 slot + scoped slot 暴露位置与业务钩子(newHandler/addHandler)
- m-form-table、m-form-group-list 统一注册为 TableGroupList,对外导出
  的 MTable/MGroupList 也指向它,新增 MTableGroupList 显式导出
- useAdd 移除重复的 addable 计算,由父组件统一管理

Made-with: Cursor
This commit is contained in:
roymondchen 2026-04-23 17:03:04 +08:00
parent b46b571214
commit 9f21f8f1d5
6 changed files with 184 additions and 110 deletions

View File

@ -27,30 +27,19 @@
></MFieldsGroupListItem>
<div class="m-fields-group-list-footer">
<TMagicButton v-if="config.enableToggleMode" :icon="Grid" size="small" @click="toggleMode"
>切换为表格</TMagicButton
>
<slot name="toggle-button"></slot>
<div style="display: flex; justify-content: flex-end; flex: 1">
<TMagicButton
v-if="addable"
:size="config.enableToggleMode ? 'small' : 'default'"
:icon="Plus"
v-bind="config.addButtonConfig?.props || { type: 'primary' }"
:disabled="disabled"
@click="addHandler"
>{{ config.addButtonConfig?.text || '新增' }}</TMagicButton
>
<slot name="add-button" :trigger="addHandler"></slot>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, inject } from 'vue';
import { Grid, Plus } from '@element-plus/icons-vue';
import { inject } from 'vue';
import { cloneDeep } from 'lodash-es';
import { TMagicButton, tMagicMessage } from '@tmagic/design';
import { tMagicMessage } from '@tmagic/design';
import type { ContainerChangeEventData, FormState, GroupListConfig } from '../schema';
import { initValue } from '../utils/form';
@ -80,21 +69,6 @@ const emit = defineEmits<{
const mForm = inject<FormState | undefined>('mForm');
const addable = computed(() => {
if (!props.name) return false;
if (typeof props.config.addable === 'function') {
return props.config.addable(mForm, {
model: props.model[props.name],
formValue: mForm?.values,
prop: props.prop,
config: props.config,
});
}
return typeof props.config.addable === 'undefined' ? true : props.config.addable;
});
const changeHandler = (v: any, eventData: ContainerChangeEventData) => {
emit('change', props.model, eventData);
};
@ -167,17 +141,6 @@ const swapHandler = (idx1: number, idx2: number) => {
emit('change', props.model[props.name]);
};
const toggleMode = () => {
props.config.type = 'table';
props.config.groupItems = props.config.items;
props.config.items = (props.config.tableItems ||
props.config.items.map((item: any) => ({
...item,
label: item.label || item.text,
text: null,
}))) as any;
};
const onAddDiffCount = () => emit('addDiffCount');
const getLastValues = (item: any, index: number) => item?.[index] || {};

View File

@ -0,0 +1,169 @@
<template>
<component
:is="displayMode === 'table' ? MFormTable : MFormGroupList"
v-bind="$attrs"
:model="model"
:name="`${name}`"
:config="currentConfig"
:disabled="disabled"
:size="size"
:is-compare="isCompare"
:last-values="lastValues"
:prop="prop"
:label-width="labelWidth"
@change="onChange"
@select="onSelect"
@addDiffCount="onAddDiffCount"
>
<template #toggle-button>
<TMagicButton v-if="config.enableToggleMode !== false" :icon="Grid" size="small" @click="toggleDisplayMode">
{{ displayMode === 'table' ? '展开配置' : '切换为表格' }}
</TMagicButton>
</template>
<template #add-button="{ trigger }">
<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"
>
{{ currentConfig.addButtonConfig?.text || (displayMode === 'table' ? '新增一行' : '新增') }}
</TMagicButton>
</template>
</component>
</template>
<script setup lang="ts">
import { computed, inject, ref } 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 { ContainerChangeEventData } from '../schema';
import MFormTable from '../table/Table.vue';
import MFormGroupList from './GroupList.vue';
defineOptions({
name: 'MFormTableGroupList',
inheritAttrs: false,
});
const props = defineProps<{
model: any;
lastValues?: any;
isCompare?: boolean;
config: TableConfig | GroupListConfig;
name: string;
prop?: string;
labelWidth?: string;
disabled?: boolean;
size?: string;
}>();
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 isGroupListType = (type: string | undefined) => type === 'groupList' || type === 'group-list';
const displayMode = ref<'table' | 'groupList'>(isGroupListType(props.config.type) ? 'groupList' : 'table');
const calcLabelWidth = (label: string) => {
if (!label) return '0px';
const zhLength = label.match(/[^\x00-\xff]/g)?.length || 0;
const chLength = label.length - zhLength;
return `${Math.max(chLength * 8 + zhLength * 20, 80)}px`;
};
// config table table
// groupList table config
const tableConfig = computed<TableConfig>(() => {
if (!isGroupListType(props.config.type)) {
return props.config as TableConfig;
}
const source = props.config as GroupListConfig;
return {
...props.config,
type: 'table',
groupItems: source.items,
items:
source.tableItems ||
(source.items as any[]).map((item: any) => ({
...item,
label: item.label || item.text,
text: null,
})),
} as any as TableConfig;
});
// groupList config
const groupListConfig = computed<GroupListConfig>(() => {
if (isGroupListType(props.config.type)) {
return props.config as GroupListConfig;
}
const source = props.config as TableConfig;
return {
...props.config,
type: 'groupList',
tableItems: source.items,
items:
source.groupItems ||
(source.items as any[]).map((item: any) => {
const text = item.text || item.label;
return {
...item,
text,
labelWidth: calcLabelWidth(text),
span: item.span || 12,
};
}),
} as any as GroupListConfig;
});
// displayMode `<component :is>` any
const currentConfig = computed<any>(() => (displayMode.value === 'table' ? tableConfig.value : groupListConfig.value));
// Table/GroupList
const addButtonSize = computed(() => {
if (displayMode.value === 'table') return 'small';
return props.config.enableToggleMode !== false ? 'small' : 'default';
});
const toggleDisplayMode = () => {
displayMode.value = displayMode.value === 'table' ? 'groupList' : 'table';
};
const onChange = (v: any, eventData?: ContainerChangeEventData) => emit('change', v, eventData);
const onSelect = (...args: any[]) => emit('select', ...args);
const onAddDiffCount = () => emit('addDiffCount');
</script>

View File

@ -32,8 +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 './table/Table.vue';
export { default as MGroupList } from './containers/GroupList.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 MText } from './fields/Text.vue';
export { default as MNumber } from './fields/Number.vue';
export { default as MNumberRange } from './fields/NumberRange.vue';

View File

@ -21,10 +21,10 @@ import { type App } from 'vue';
import Container from './containers/Container.vue';
import Fieldset from './containers/Fieldset.vue';
import FlexLayout from './containers/FlexLayout.vue';
import GroupList from './containers/GroupList.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 Tabs from './containers/Tabs.vue';
import Cascader from './fields/Cascader.vue';
import Checkbox from './fields/Checkbox.vue';
@ -46,7 +46,6 @@ import Text from './fields/Text.vue';
import Textarea from './fields/Textarea.vue';
import Time from './fields/Time.vue';
import Timerange from './fields/Timerange.vue';
import Table from './table/Table.vue';
import { setConfig } from './utils/config';
import Form from './Form.vue';
import FormDialog from './FormDialog.vue';
@ -70,11 +69,11 @@ export default {
app.component('m-form-dialog', FormDialog);
app.component('m-form-container', Container);
app.component('m-form-fieldset', Fieldset);
app.component('m-form-group-list', GroupList);
app.component('m-form-group-list', TableGroupList);
app.component('m-form-panel', Panel);
app.component('m-form-row', Row);
app.component('m-form-step', MStep);
app.component('m-form-table', Table);
app.component('m-form-table', TableGroupList);
app.component('m-form-tab', Tabs);
app.component('m-form-flex-layout', FlexLayout);
app.component('m-fields-text', Text);

View File

@ -34,13 +34,7 @@
<div style="display: flex; justify-content: space-between; margin: 10px 0">
<div style="display: flex">
<TMagicButton
:icon="Grid"
size="small"
@click="toggleMode"
v-if="enableToggleMode && config.enableToggleMode !== false && !isFullscreen"
>展开配置</TMagicButton
>
<slot name="toggle-button" v-if="enableToggleMode && !isFullscreen"></slot>
<TMagicButton
:icon="FullScreen"
size="small"
@ -64,17 +58,7 @@
>清空</TMagicButton
>
</div>
<TMagicButton
v-if="addable"
class="m-form-table-add-button"
size="small"
plain
:icon="Plus"
v-bind="config.addButtonConfig?.props || { type: 'primary' }"
:disabled="disabled"
@click="newHandler()"
>{{ config.addButtonConfig?.text || '新增一行' }}</TMagicButton
>
<slot name="add-button" :trigger="newHandler"></slot>
</div>
<div class="bottom" style="text-align: right" v-if="config.pagination">
@ -97,7 +81,7 @@
<script setup lang="ts">
import { computed, ref, useTemplateRef } from 'vue';
import { FullScreen, Grid, Plus } from '@element-plus/icons-vue';
import { FullScreen } from '@element-plus/icons-vue';
import { TMagicButton, TMagicPagination, TMagicTable, TMagicTooltip, TMagicUpload, useZIndex } from '@tmagic/design';
@ -139,7 +123,7 @@ const { pageSize, currentPage, paginationData, handleSizeChange, handleCurrentCh
const { nextZIndex } = useZIndex();
const updateKey = ref(1);
const { addable, newHandler } = useAdd(props, emit);
const { newHandler } = useAdd(props, emit);
const { columns } = useTableColumns(props, emit, currentPage, pageSize, modelName);
useSortable(props, emit, tMagicTableRef, modelName, updateKey);
const { isFullscreen, toggleFullscreen } = useFullscreen();
@ -148,32 +132,6 @@ const { selectHandle, toggleRowSelection } = useSelection(props, emit, tMagicTab
const data = computed(() => (props.config.pagination ? paginationData.value : props.model[modelName.value]));
const toggleMode = () => {
const calcLabelWidth = (label: string) => {
if (!label) return '0px';
const zhLength = label.match(/[^\x00-\xff]/g)?.length || 0;
const chLength = label.length - zhLength;
return `${Math.max(chLength * 8 + zhLength * 20, 80)}px`;
};
// groupList
props.config.type = 'groupList';
props.config.enableToggleMode = true;
props.config.tableItems = props.config.items;
props.config.items =
props.config.groupItems ||
props.config.items.map((item: any) => {
const text = item.text || item.label;
const labelWidth = calcLabelWidth(text);
return {
...item,
text,
labelWidth,
span: item.span || 12,
};
});
};
const sortChangeHandler = (sortOptions: SortProp) => {
const modelName = props.name || props.config.name || '';
sortChange(props.model[modelName], sortOptions);

View File

@ -1,4 +1,4 @@
import { computed, inject } from 'vue';
import { inject } from 'vue';
import { tMagicMessage } from '@tmagic/design';
import type { FormConfig, FormState } from '@tmagic/form-schema';
@ -13,21 +13,6 @@ export const useAdd = (
) => {
const mForm = inject<FormState | undefined>('mForm');
const addable = computed(() => {
const modelName = props.name || props.config.name || '';
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,
});
}
return typeof props.config.addable === 'undefined' ? true : props.config.addable;
});
const newHandler = async (row?: any) => {
const modelName = props.name || props.config.name || '';
@ -106,7 +91,6 @@ export const useAdd = (
};
return {
addable,
newHandler,
};
};