roymondchen 02eeb2e87f feat(editor): 统一列表字段添加按钮样式并支持新增后自动滚动
CodeSelect、EventSelect、DisplayConds 使用吸底全宽添加按钮;
group-list 新增后滚到最后一项,避开吸顶标题与吸底按钮。
2026-09-01 12:02:09 +08:00

273 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { DataSchema, DataSourceFieldType, DataSourceSchema } from '@tmagic/core';
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: TabConfig = {
type: 'tab',
items: [
{
title: '数据定义',
status: 'fields',
items: [
{
name: 'fields',
type: 'data-source-fields',
defaultValue: () => [],
},
],
},
{
title: '方法定义',
status: 'methods',
items: [
{
name: 'methods',
type: 'data-source-methods',
defaultValue: () => [],
},
],
},
{
title: '事件配置',
items: [
{
name: 'events',
src: 'datasource',
type: 'event-select',
defaultValue: [],
},
],
},
{
title: 'mock数据',
items: [
{
name: 'mocks',
type: 'data-source-mocks',
defaultValue: () => [],
},
],
},
{
title: '请求参数裁剪',
display: (_formState, { model }) => model.type === 'http',
items: [
{
name: 'beforeRequest',
type: 'vs-code',
parse: true,
autosize: { minRows: 10, maxRows: 30 },
},
],
},
{
title: '响应数据裁剪',
display: (_formStat, { model }) => model.type === 'http',
items: [
{
name: 'afterResponse',
type: 'vs-code',
parse: true,
autosize: { minRows: 10, maxRows: 30 },
},
],
},
],
};
const fillConfig = <T = never>(config: FormConfig<T>): FormConfig<T> => [
...BaseFormConfig(),
...config,
dataSourceFormConfig,
];
export const getFormConfig = (type: string, configs: Record<string, FormConfig>): FormConfig => {
switch (type) {
case 'base':
return fillConfig([]);
case 'http':
return fillConfig(HttpFormConfig);
default:
return fillConfig(configs[type] || []);
}
};
export const getFormValue = (type: string, values: Partial<DataSourceSchema>): Partial<DataSourceSchema> => {
if (type !== 'http') {
return values;
}
return {
beforeRequest: `(options, context) => {
/**
* 用户可以直接编写函数,在原始接口调用之前,会运行该函数,将这个函数的返回值作为该数据源接口的入参
*
* options: HttpOptions
*
* interface HttpOptions {
* // 请求链接
* url: string;
* // query参数
* params?: Record<string, string>;
* // body数据
* data?: Record<string, any>;
* // 请求头
* headers?: Record<string, string>;
* // 请求方法 GET/POST
* method?: Method;
* }
*
* context上下文对象
*
* interface Content {
* app: TMagicApp;
* dataSource: HttpDataSource;
* }
*
* return: HttpOptions
*/
// 此处的返回值会作为这个接口的入参
return options;
}`,
afterResponse: `(response, context) => {
/**
* 用户可以直接编写函数,在原始接口返回之后,会运行该函数,将这个函数的返回值作为该数据源接口的返回
* context上下文对象
*
* interface Content {
* app: TMagicApp;
* dataSource: HttpDataSource;
* }
*
*/
// 此处的返回值会作为这个接口的返回值
return response;
}`,
...values,
};
};
export const getDisplayField = (dataSources: DataSourceSchema[], key: string) => {
const displayState: { value: string; type: 'var' | 'text' }[] = [];
// 匹配es6字符串模块
const matches = key.matchAll(dataSourceTemplateRegExp);
let index = 0;
for (const match of matches) {
if (typeof match.index === 'undefined') break;
// 字符串常量
displayState.push({
type: 'text',
value: key.substring(index, match.index),
});
let dsText = '';
let ds: DataSourceSchema | undefined;
let fields: DataSchema[] | undefined;
// 将模块解析成数据源对应的值
getKeysArray(match[1]).forEach((item, index) => {
if (index === 0) {
ds = dataSources.find((ds) => ds.id === item);
dsText += ds?.title || item;
fields = ds?.fields;
return;
}
if (isNumber(item)) {
dsText += `[${item}]`;
} else {
const field = fields?.find((field) => field.name === item);
fields = field?.fields;
dsText += `.${field?.title || item}`;
}
});
displayState.push({
type: 'var',
value: dsText,
});
index = match.index + match[0].length;
}
if (index < key.length) {
displayState.push({
type: 'text',
value: key.substring(index),
});
}
return displayState;
};
export const getCascaderOptionsFromFields = (
fields: DataSchema[] = [],
dataSourceFieldType: DataSourceFieldType[] = ['any'],
): CascaderOption[] => {
const typeSet = new Set(dataSourceFieldType.length ? dataSourceFieldType : ['any']);
const includesAny = typeSet.has('any');
const result: CascaderOption[] = [];
for (const field of fields) {
const fieldType = field.type || 'any';
const isContainerType = fieldType === 'any' || fieldType === 'array' || fieldType === 'object';
const children = isContainerType ? getCascaderOptionsFromFields(field.fields, dataSourceFieldType) : [];
const matchesType = includesAny || typeSet.has(fieldType);
if (matchesType || (isContainerType && children.length)) {
result.push({
label: `${field.title || field.name}(${field.type})`,
value: field.name,
children,
});
}
}
return result;
};
/**
* 按字段名路径下钻 DataSchema。
* @param skipNumberIndices 为 true 时跳过数字段(模板路径中的数组下标,如 arr[0].x
*/
export const resolveFieldByPath = (
fields: DataSchema[] | undefined,
fieldNames: string[],
options: { skipNumberIndices?: boolean } = {},
): { ok: boolean; field?: DataSchema; fields: DataSchema[]; failedName?: string } => {
let currentFields = fields || [];
let field: DataSchema | undefined;
for (const name of fieldNames) {
if (options.skipNumberIndices && isNumber(name)) {
continue;
}
if (!currentFields.length) {
return { ok: false, fields: currentFields, failedName: name };
}
field = currentFields.find((item) => item.name === name);
if (!field) {
return { ok: false, fields: currentFields, failedName: name };
}
currentFields = field.fields || [];
}
return { field, ok: true, fields: currentFields };
};
export const getFieldType = (ds: DataSourceSchema | undefined, fieldNames: string[]) => {
const { ok, field } = resolveFieldByPath(ds?.fields, fieldNames);
if (!ok) return '';
return field?.type || '';
};