mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-07-23 10:05:42 +08:00
- 新增 typeMatchRules.ts,为编辑器自定义字段注册内置 typeMatch 校验规则 - 抽取 event.ts 事件工具函数 - form typeMatch 适配 TDesign 校验器签名,错误消息补充实际值 - rules.md 新增「Editor 字段内置规则」章节,16 个字段文档补充校验说明
272 lines
6.8 KiB
TypeScript
272 lines
6.8 KiB
TypeScript
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',
|
||
},
|
||
],
|
||
},
|
||
{
|
||
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[] } => {
|
||
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 };
|
||
}
|
||
field = currentFields.find((item) => item.name === name);
|
||
if (!field) {
|
||
return { ok: false, fields: currentFields };
|
||
}
|
||
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 || '';
|
||
};
|