feat: support switch menu when add sub models

This commit is contained in:
gchust 2025-06-30 23:21:01 +08:00
parent 3dbd75b9e4
commit dec669a94a
3 changed files with 335 additions and 160 deletions

View File

@ -14,7 +14,7 @@ import { FlowModel } from '../../models';
import { FlowModelOptions, ModelConstructor } from '../../types';
import { FlowSettingsButton } from '../common/FlowSettingsButton';
import { withFlowDesignMode } from '../common/withFlowDesignMode';
import { AddSubModelButton, SubModelItemsType, mergeSubModelItems } from './AddSubModelButton';
import { AddSubModelButton, SubModelItemsType, mergeSubModelItems, AddSubModelContext } from './AddSubModelButton';
export type BuildCreateModelOptionsType = {
defaultOptions: FlowModelOptions;
@ -129,12 +129,47 @@ const AddFieldButtonCore: React.FC<AddFieldButtonProps> = ({
key: field.name,
label: field.title,
icon: fieldClass.meta?.icon,
unique: true,
createModelOptions: buildCreateModelOptions({
defaultOptions,
collectionField: field,
fieldPath: field.name,
fieldModelClass: fieldClass,
}),
toggleDetector: (ctx: AddSubModelContext) => {
// 检测是否已存在该字段的子模型
const subModels = ctx.model.subModels[subModelKey];
const checkFieldInStepParams = (subModel: FlowModel): boolean => {
const stepParams = subModel.stepParams;
// 快速检查:如果 stepParams 为空,直接返回 false
if (!stepParams || Object.keys(stepParams).length === 0) {
return false;
}
// 遍历所有 flow 和 step 来查找 fieldPath 或 field 参数
for (const flowKey in stepParams) {
const flowSteps = stepParams[flowKey];
if (!flowSteps) continue;
for (const stepKey in flowSteps) {
const stepData = flowSteps[stepKey];
if (stepData?.fieldPath === field.name || stepData?.field === field.name) {
return true; // 找到匹配,立即返回
}
}
}
return false;
};
if (Array.isArray(subModels)) {
return subModels.some(checkFieldInStepParams);
} else if (subModels) {
return checkFieldInStepParams(subModels);
}
return false;
},
};
allFields.push(fieldItem);
}
@ -151,7 +186,7 @@ const AddFieldButtonCore: React.FC<AddFieldButtonProps> = ({
},
];
};
}, [model, subModelBaseClass, fields, buildCreateModelOptions]);
}, [model, subModelBaseClass, fields, buildCreateModelOptions, subModelKey]);
const fieldItems = useMemo(() => {
return mergeSubModelItems([buildFieldItems, appendItems], { addDividers: true });

View File

@ -8,12 +8,17 @@
*/
import React, { useMemo } from 'react';
import { Switch } from 'antd';
import { FlowModel } from '../../models';
import { ModelConstructor } from '../../types';
import { withFlowDesignMode } from '../common/withFlowDesignMode';
import LazyDropdown, { Item, ItemsType } from './LazyDropdown';
import _ from 'lodash';
// ============================================================================
// 类型定义
// ============================================================================
export interface AddSubModelContext {
model: FlowModel;
globals: Record<string, any>;
@ -22,85 +27,6 @@ export interface AddSubModelContext {
subModelBaseClass?: ModelConstructor;
}
export type SubModelItemsType =
| SubModelItem[]
| ((ctx: AddSubModelContext) => SubModelItem[] | Promise<SubModelItem[]>);
/**
* SubModelItemsType
*/
export interface MergeSubModelItemsOptions {
/**
* 线
*/
addDividers?: boolean;
}
/**
* SubModelItemsType
*
*
*
* @param sources - SubModelItemsType undefined null
* @param options -
* @returns SubModelItemsType
*
* @example
* ```typescript
* const mergedItems = mergeSubModelItems([
* fieldItems, // 字段 items静态数组
* customItems, // 自定义 items静态数组
* async (ctx) => [...], // 动态 items异步函数
* condition ? extraItems : null, // 条件性 items
* ], { addDividers: true });
* ```
*/
export function mergeSubModelItems(
sources: (SubModelItemsType | undefined | null)[],
options: MergeSubModelItemsOptions = {},
): SubModelItemsType {
const { addDividers = false } = options;
// 过滤掉空值
const validSources = sources.filter((source): source is SubModelItemsType => source !== undefined && source !== null);
if (validSources.length === 0) {
return [];
}
if (validSources.length === 1) {
return validSources[0];
}
// 统一返回异步函数处理所有情况
return async (ctx: AddSubModelContext) => {
const result: SubModelItem[] = [];
for (let i = 0; i < validSources.length; i++) {
const source = validSources[i];
let items: SubModelItem[] = [];
if (Array.isArray(source)) {
items = source;
} else {
items = await source(ctx);
}
// 添加分割线(除了第一个来源)
if (i > 0 && addDividers && items.length > 0) {
result.push({
key: `divider-${i}`,
type: 'divider',
} as SubModelItem);
}
result.push(...items);
}
return result;
};
}
export interface SubModelItem {
key?: string;
label?: string;
@ -111,68 +37,53 @@ export interface SubModelItem {
createModelOptions?:
| { use: string; stepParams?: Record<string, any> }
| ((item: SubModelItem) => { use: string; stepParams?: Record<string, any> });
/**
* group group
*/
searchable?: boolean;
/**
* group
*/
searchPlaceholder?: string;
/**
*
*/
keepDropdownOpen?: boolean;
unique?: boolean;
toggleDetector?: (ctx: AddSubModelContext) => boolean | Promise<boolean>;
removeModelOptions?: {
customRemove?: (ctx: AddSubModelContext, item: SubModelItem) => Promise<void>;
};
}
export type SubModelItemsType =
| SubModelItem[]
| ((ctx: AddSubModelContext) => SubModelItem[] | Promise<SubModelItem[]>);
export interface MergeSubModelItemsOptions {
addDividers?: boolean;
}
interface AddSubModelButtonProps {
/**
*
*/
model: FlowModel;
/**
*
*/
items: SubModelItemsType;
/**
* context items 使
*/
subModelBaseClass?: string | ModelConstructor;
/**
* 'object' 'array'
*/
subModelType?: 'object' | 'array';
/**
*
*/
subModelKey: string;
/**
*
*/
onModelCreated?: (subModel: FlowModel) => Promise<void>;
/**
*
*/
onSubModelAdded?: (subModel: FlowModel) => Promise<void>;
/**
* "Add"
*/
children?: React.ReactNode;
/**
*
* true
* keepDropdownOpen
*/
keepDropdownOpen?: boolean;
}
// ============================================================================
// 工具函数
// ============================================================================
// 预定义样式对象,避免重复创建
const SWITCH_CONTAINER_STYLE = {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
width: '100%',
padding: '0 4px',
} as const;
const SWITCH_STYLE = {
pointerEvents: 'none' as const,
};
/**
* createModelOptions
*/
@ -183,12 +94,10 @@ const validateCreateModelOptions = (
console.warn('No createModelOptions found for item');
return false;
}
if (!createOpts.use) {
console.warn('createModelOptions must specify "use" property:', createOpts);
return false;
}
return true;
};
@ -206,22 +115,143 @@ const handleModelCreationError = async (error: any, addedModel?: FlowModel) => {
}
};
/**
*
*/
const getCreateModelOptions = (item: SubModelItem) => {
let createOpts = item.createModelOptions;
if (typeof createOpts === 'function') {
createOpts = createOpts(item);
}
return createOpts;
};
/**
*
*/
const createBuildContext = (model: FlowModel, subModelBaseClass?: string | ModelConstructor): AddSubModelContext => {
const globalContext = model.flowEngine.getContext();
return {
model,
globals: globalContext,
subModelBaseClass:
typeof subModelBaseClass === 'string' ? model.flowEngine.getModelClass(subModelBaseClass) : subModelBaseClass,
};
};
/**
* SubModelItemsType
*/
export function mergeSubModelItems(
sources: (SubModelItemsType | undefined | null)[],
options: MergeSubModelItemsOptions = {},
): SubModelItemsType {
const { addDividers = false } = options;
const validSources = sources.filter((source): source is SubModelItemsType => source !== undefined && source !== null);
if (validSources.length === 0) return [];
if (validSources.length === 1) return validSources[0];
return async (ctx: AddSubModelContext) => {
const result: SubModelItem[] = [];
for (let i = 0; i < validSources.length; i++) {
const source = validSources[i];
const items: SubModelItem[] = Array.isArray(source) ? source : await source(ctx);
if (i > 0 && addDividers && items.length > 0) {
result.push({ key: `divider-${i}`, type: 'divider' } as SubModelItem);
}
result.push(...items);
}
return result;
};
}
// ============================================================================
// 转换器函数
// ============================================================================
/**
* Switch
*/
const createSwitchLabel = (originalLabel: string, isToggled: boolean) => (
<div style={SWITCH_CONTAINER_STYLE}>
<span>{originalLabel}</span>
<Switch size="small" checked={isToggled} style={SWITCH_STYLE} />
</div>
);
/**
* unique
*/
const hasUniqueItems = (items: SubModelItem[]): boolean => {
return items.some((item) => item.unique && item.toggleDetector && !item.children);
};
/**
* SubModelItem LazyDropdown Item
*/
const transformSubModelItems = (items: SubModelItem[], context: AddSubModelContext): Item[] => {
return items.map((item) => ({
...item,
children: item.children
? typeof item.children === 'function'
? async () => {
const childrenFn = item.children as (ctx: AddSubModelContext) => SubModelItem[] | Promise<SubModelItem[]>;
const result = await childrenFn(context);
return transformSubModelItems(result, context);
}
: transformSubModelItems(item.children as SubModelItem[], context)
: undefined,
}));
const transformSubModelItems = async (items: SubModelItem[], context: AddSubModelContext): Promise<Item[]> => {
if (items.length === 0) return [];
// 批量收集需要异步检测的 unique 项
const uniqueItems: Array<{ item: SubModelItem; index: number }> = [];
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.unique && item.toggleDetector && !item.children) {
uniqueItems.push({ item, index: i });
}
}
// 批量执行 toggleDetector
const toggleResults = await Promise.allSettled(uniqueItems.map(({ item }) => item.toggleDetector!(context)));
const toggleMap = new Map<number, boolean>();
uniqueItems.forEach(({ index }, i) => {
const result = toggleResults[i];
toggleMap.set(index, result.status === 'fulfilled' ? result.value : false);
});
// 并发转换所有项目
const transformPromises = items.map(async (item, index) => {
const transformedItem: Item = {
key: item.key,
label: item.label,
type: item.type,
disabled: item.disabled,
icon: item.icon,
searchable: item.searchable,
searchPlaceholder: item.searchPlaceholder,
keepDropdownOpen: item.keepDropdownOpen,
originalItem: item,
};
// 处理 children
if (item.children) {
if (typeof item.children === 'function') {
transformedItem.children = async () => {
const childrenFn = item.children as (ctx: AddSubModelContext) => SubModelItem[] | Promise<SubModelItem[]>;
const childrenResult = await childrenFn(context);
return transformSubModelItems(childrenResult, context);
};
} else {
transformedItem.children = await transformSubModelItems(item.children as SubModelItem[], context);
}
}
// 处理开关式菜单项
if (item.unique && item.toggleDetector && !item.children) {
const isToggled = toggleMap.get(index) || false;
const originalLabel = item.label || '';
transformedItem.label = createSwitchLabel(originalLabel, isToggled);
transformedItem.isToggled = isToggled;
transformedItem.unique = true;
}
return transformedItem;
});
return Promise.all(transformPromises);
};
/**
@ -234,9 +264,88 @@ const transformItems = (items: SubModelItemsType, context: AddSubModelContext):
return transformSubModelItems(result, context);
};
}
return transformSubModelItems(items, context);
const hasUnique = hasUniqueItems(items as SubModelItem[]);
if (hasUnique) {
return () => transformSubModelItems(items as SubModelItem[], context);
} else {
let cachedResult: Item[] | null = null;
return async () => {
if (!cachedResult) {
cachedResult = await transformSubModelItems(items as SubModelItem[], context);
}
return cachedResult;
};
}
};
// ============================================================================
// 删除处理器
// ============================================================================
/**
* stepParams
*/
const findFieldInStepParams = (subModel: FlowModel, fieldKey: string): boolean => {
const stepParams = subModel.stepParams;
if (!stepParams || Object.keys(stepParams).length === 0) return false;
for (const flowKey in stepParams) {
const flowSteps = stepParams[flowKey];
if (!flowSteps) continue;
for (const stepKey in flowSteps) {
const stepData = flowSteps[stepKey];
if (stepData?.fieldPath === fieldKey || stepData?.field === fieldKey) {
return true;
}
}
}
return false;
};
/**
*
*/
const createDefaultRemoveHandler = (config: {
model: FlowModel;
subModelKey: string;
subModelType: 'object' | 'array';
}) => {
return async (item: SubModelItem, _context: AddSubModelContext): Promise<void> => {
const { model, subModelKey, subModelType } = config;
if (subModelType === 'array') {
const subModels = (model.subModels as any)[subModelKey] as FlowModel[];
if (Array.isArray(subModels)) {
const createOpts = getCreateModelOptions(item);
const targetModel = subModels.find((subModel) => {
if (item.key && findFieldInStepParams(subModel, item.key)) return true;
return (
(subModel as any).constructor.name === createOpts?.use || (subModel as any).uid.includes(createOpts?.use)
);
});
if (targetModel) {
targetModel.remove();
const index = subModels.indexOf(targetModel);
if (index > -1) subModels.splice(index, 1);
}
}
} else {
const subModel = (model.subModels as any)[subModelKey] as FlowModel;
if (subModel) {
subModel.remove();
(model.subModels as any)[subModelKey] = undefined;
}
}
};
};
// ============================================================================
// 主组件
// ============================================================================
/**
* FlowModel
*
@ -244,7 +353,7 @@ const transformItems = (items: SubModelItemsType, context: AddSubModelContext):
* - items
* -
* - flowEngine
*
* - unique
*/
const AddSubModelButtonCore = function AddSubModelButton({
model,
@ -257,27 +366,47 @@ const AddSubModelButtonCore = function AddSubModelButton({
children = 'Add',
keepDropdownOpen = false,
}: AddSubModelButtonProps) {
// 构建上下文对象,从 flowEngine 的全局上下文中获取服务
const buildContext = useMemo((): AddSubModelContext => {
const globalContext = model.flowEngine.getContext();
return {
model,
globals: globalContext,
subModelBaseClass:
typeof subModelBaseClass === 'string' ? model.flowEngine.getModelClass(subModelBaseClass) : subModelBaseClass,
};
}, [model, model.flowEngine, subModelBaseClass]);
// 构建上下文对象
const buildContext = useMemo(
() => createBuildContext(model, subModelBaseClass),
[model, model.flowEngine, subModelBaseClass],
);
// 创建删除处理器
const removeHandler = useMemo(
() =>
createDefaultRemoveHandler({
model,
subModelKey,
subModelType,
}),
[model, subModelKey, subModelType],
);
// 点击处理逻辑
const onClick = async (info: any) => {
const item = info.originalItem as SubModelItem;
let createOpts = item.createModelOptions;
const clickedItem = info.originalItem || info;
const item = clickedItem.originalItem || (clickedItem as SubModelItem);
const isToggled = clickedItem.isToggled;
const isUnique = clickedItem.unique || item.unique;
// 如果 createModelOptions 是函数,则调用它获取实际的选项
if (typeof createOpts === 'function') {
createOpts = createOpts(item);
// 处理 unique 菜单项的开关操作
if (isUnique && item.toggleDetector && isToggled) {
try {
if (item.removeModelOptions?.customRemove) {
await item.removeModelOptions.customRemove(buildContext, item);
} else {
await removeHandler(item, buildContext);
}
} catch (error) {
console.error('Failed to remove sub model:', error);
}
return;
}
// 验证 createModelOptions 的有效性
// 处理添加操作
const createOpts = getCreateModelOptions(item);
if (!validateCreateModelOptions(createOpts)) {
return;
}
@ -293,7 +422,6 @@ const AddSubModelButtonCore = function AddSubModelButton({
});
addedModel.setParent(model);
await addedModel.configureRequiredSteps();
if (onModelCreated) {

View File

@ -22,6 +22,18 @@ export type Item = {
searchable?: boolean;
searchPlaceholder?: string;
keepDropdownOpen?: boolean;
/**
* 使
*/
isToggled?: boolean;
/**
* 使
*/
originalItem?: any;
/**
* 使
*/
unique?: boolean;
[key: string]: any;
};