增加formily json schema生成

This commit is contained in:
dengqichen 2025-01-16 10:46:02 +08:00
parent 4c79000f30
commit a76685bbd6
9 changed files with 819 additions and 524 deletions

View File

@ -3,6 +3,8 @@ package com.qqchen.deploy.backend.deploy.dto.variables.build;
import com.qqchen.deploy.backend.framework.annotation.formily.*; import com.qqchen.deploy.backend.framework.annotation.formily.*;
import lombok.Data; import lombok.Data;
import java.util.Map;
/** /**
* Jenkins构建变量 * Jenkins构建变量
*/ */
@ -10,105 +12,136 @@ import lombok.Data;
@FormilyForm(name = "Jenkins构建配置") @FormilyForm(name = "Jenkins构建配置")
public class JenkinsBaseBuildVariables { public class JenkinsBaseBuildVariables {
@FormilyField( // @FormilyField(
title = "绑定三方Jenkins系统", // title = "绑定三方Jenkins系统",
type = "string", // component = "Select",
component = "Select", // order = 1,
props = @FormilyComponentProps( // props = @FormilyComponentProps(
api = "/api/v1/external-system/list?type=JENKINS", // labelField = "name",
labelField = "name", // valueField = "id",
valueField = "id", // allowClear = true,
showSearch = true, // showSearch = true,
allowClear = true, // placeholder = "请选择Jenkins系统"
placeholder = "请选择Jenkins系统" // ),
), // validators = {
validators = { // @FormilyValidator(
@FormilyValidator( // required = true,
required = true, // message = "请选择Jenkins系统"
message = "请选择Jenkins系统" // )
) // },
} // reactions = {
) // @FormilyReaction(
private String externalSystemId; // state = "dataSource",
// value = "{{$fetch('/api/v1/external-system/list?type=JENKINS').then(data => data.data)}}",
@FormilyField( // when = "{{$form.mounted && $form.values.__modalVisible}}"
title = "绑定Jenkins视图", // )
type = "string", // }
component = "Select", // )
props = @FormilyComponentProps( // private String externalSystemId;
labelField = "viewName", //
valueField = "id", // @FormilyField(
showSearch = true, // title = "绑定Jenkins视图",
allowClear = true, // component = "Select",
placeholder = "请选择Jenkins视图" // order = 2,
), // props = @FormilyComponentProps(
validators = { // labelField = "viewName",
@FormilyValidator( // valueField = "id",
required = true, // allowClear = true,
message = "请选择Jenkins视图" // showSearch = true,
) // placeholder = "请选择Jenkins视图"
}, // ),
reactions = { // validators = {
@FormilyReaction( // @FormilyValidator(
dependencies = {"externalSystemId"}, // required = true,
state = "dataSource", // message = "请选择Jenkins视图"
value = "{{$fetch('/api/v1/jenkins-view/list?externalSystemId=' + $deps.externalSystemId).then(data => data.data)}}" // )
) // },
} // reactions = {
) // @FormilyReaction(
private String viewId; // dependencies = {"externalSystemId"},
// state = "dataSource",
@FormilyField( // value = "{{$fetch('/api/v1/jenkins-view/list?externalSystemId=' + $deps.externalSystemId).then(data => data.data)}}"
title = "绑定Jenkins任务", // )
type = "string", // }
component = "Select", // )
props = @FormilyComponentProps( // private String viewId;
labelField = "jobName", //
valueField = "id", // @FormilyField(
showSearch = true, // title = "绑定Jenkins任务",
allowClear = true, // component = "Select",
placeholder = "请选择Jenkins任务" // order = 3,
), // props = @FormilyComponentProps(
validators = { // labelField = "jobName",
@FormilyValidator( // valueField = "id",
required = true, // allowClear = true,
message = "请选择Jenkins任务" // showSearch = true,
) // placeholder = "请选择Jenkins任务"
}, // ),
reactions = { // validators = {
@FormilyReaction( // @FormilyValidator(
dependencies = {"externalSystemId", "viewId"}, // required = true,
state = "dataSource", // message = "请选择Jenkins任务"
value = "{{$fetch('/api/v1/jenkins-job/list?externalSystemId=' + $deps.externalSystemId + '&viewId=' + $deps.viewId).then(data => data.data)}}" // )
) // },
} // reactions = {
) // @FormilyReaction(
private String jobId; // dependencies = {"externalSystemId", "viewId"},
// state = "dataSource",
@FormilyField( // value = "{{$fetch('/api/v1/jenkins-job/list?externalSystemId=' + $deps.externalSystemId + '&viewId=' + $deps.viewId).then(data => data.data)}}"
title = "Pipeline script", // )
type = "string", // }
component = "MonacoEditor", // )
props = @FormilyComponentProps( // private String jobId;
editor = @FormilyEditorProps( //
language = "groovy", // @FormilyField(
theme = "vs-dark", // title = "环境变量",
minimap = false, // type = "map",
lineNumbers = true, // order = 4,
wordWrap = true, // mapConfig = @FormilyMapConfig(
fontSize = 14, // keyTitle = "变量名",
tabSize = 4, // valueTitle = "变量值",
automaticLayout = true, // keyComponent = "Select",
folding = true, // valueComponent = "Input",
placeholder = "请输入Pipeline脚本" // keyProps = @FormilyComponentProps(
) //// api = "/api/v1/env-vars/keys",
), // showSearch = true,
validators = { // allowClear = true,
@FormilyValidator( // placeholder = "请选择或输入变量名"
required = true, // ),
message = "请输入Pipeline脚本" // valueProps = @FormilyComponentProps(
) // placeholder = "请输入变量值"
} // ),
) // addText = "添加环境变量",
private String script; // allowCustomKey = true
// )
// )
// private Map<String, String> envVars;
//
// @FormilyField(
// title = "Pipeline script",
// type = "string",
// component = "MonacoEditor",
// order = 5,
// props = @FormilyComponentProps(
// editor = @FormilyEditorProps(
// language = "groovy",
// theme = "vs-dark",
// minimap = false,
// lineNumbers = true,
// wordWrap = true,
// fontSize = 14,
// tabSize = 4,
// automaticLayout = true,
// folding = true,
// placeholder = "请输入Pipeline脚本"
// )
// ),
// validators = {
// @FormilyValidator(
// required = true,
// message = "请输入Pipeline脚本"
// )
// }
// )
// private String script;
} }

View File

@ -2,22 +2,86 @@ package com.qqchen.deploy.backend.framework.annotation.formily;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
public @interface FormilyComponentProps { public @interface FormilyComponentProps {
String api() default ""; // 数据源API /**
String labelField() default "label"; // 标签字段 * 占位符
String valueField() default "value"; // 值字段 */
boolean allowClear() default false; // 允许清除 String placeholder() default "";
boolean showSearch() default false; // 显示搜索
String placeholder() default ""; // 占位符
String mode() default ""; // 模式(single/multiple等)
String[] enum_() default {}; // 枚举值
String[] enumNames() default {}; // 枚举显示值
// 编辑器属性 /**
FormilyEditorProps editor() default @FormilyEditorProps; // 编辑器配置 * 是否允许清除
*/
boolean allowClear() default false;
/**
* 是否允许搜索
*/
boolean showSearch() default false;
/**
* 选择模式 (single/multiple)
*/
String mode() default "single";
/**
* 是否启用过滤
*/
boolean filterOption() default true;
/**
* 标签字段名
*/
String labelField() default "label";
/**
* 值字段名
*/
String valueField() default "value";
/**
* 防抖时间(ms)
*/
int debounceTime() default 300;
/**
* 枚举值
*/
String[] enum_() default {};
/**
* 枚举标签
*/
String[] enumNames() default {};
/**
* 编辑器配置
*/
FormilyEditorProps editor() default @FormilyEditorProps;
/**
* 是否加载中
*/
boolean loading() default false;
/**
* 是否禁用
*/
boolean disabled() default false;
/**
* 最大长度
*/
int maxLength() default -1;
/**
* 最小长度
*/
int minLength() default -1;
/**
* 正则表达式
*/
String pattern() default "";
} }

View File

@ -1,25 +1,22 @@
package com.qqchen.deploy.backend.framework.annotation.formily; package com.qqchen.deploy.backend.framework.annotation.formily;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
public @interface FormilyEditorProps { public @interface FormilyEditorProps {
/** /**
* 辑器语言 * 语言
*/ */
String language() default "plaintext"; String language() default "javascript";
/** /**
* 编辑器主题 * 主题
*/ */
String theme() default "vs"; String theme() default "vs";
/** /**
* 是否启用小地图 * 是否显示小地图
*/ */
boolean minimap() default true; boolean minimap() default true;
@ -31,7 +28,7 @@ public @interface FormilyEditorProps {
/** /**
* 是否自动换行 * 是否自动换行
*/ */
boolean wordWrap() default false; String wordWrap() default "off";
/** /**
* 字体大小 * 字体大小
@ -54,7 +51,37 @@ public @interface FormilyEditorProps {
boolean folding() default true; boolean folding() default true;
/** /**
* 占位符文本 * 是否允许滚动到最后一行之后
*/ */
String placeholder() default ""; boolean scrollBeyondLastLine() default false;
/**
* 是否启用建议
*/
boolean suggestOnTriggerCharacters() default true;
/**
* 是否启用代码片段建议
*/
boolean snippetsPreventQuickSuggestions() default false;
/**
* 垂直滚动条
*/
String scrollbarVertical() default "auto";
/**
* 水平滚动条
*/
String scrollbarHorizontal() default "auto";
/**
* 防抖时间(ms)
*/
int debounceTime() default 300;
/**
* 是否只读
*/
boolean readOnly() default false;
} }

View File

@ -8,29 +8,93 @@ import java.lang.annotation.Target;
@Target(ElementType.FIELD) @Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
public @interface FormilyField { public @interface FormilyField {
// 基础属性 /**
String title(); // 字段标题 * 字段标题
String description() default ""; // 字段描述 */
String type() default "string"; // 字段类型(string/number/boolean/array/object/map) String title();
int order() default Integer.MAX_VALUE; // 字段顺序值越小越靠前
// 组件属性 /**
String component() default "Input"; // 组件类型 * 字段描述
String decorator() default "FormItem"; // 装饰器类型 */
FormilyComponentProps props() default @FormilyComponentProps; // 组件属性 String description() default "";
FormilyDecoratorProps decoratorProps() default @FormilyDecoratorProps; // 装饰器属性
// 校验规则 /**
FormilyValidator[] validators() default {}; // 校验规则 * 字段类型
*/
String type() default "string";
// 联动配置 /**
FormilyReaction[] reactions() default {}; // 联动规则 * 默认值
*/
String defaultValue() default "";
// 展示控制 /**
String pattern() default "editable"; // 展示模式(editable/disabled/readOnly) * 组件类型
String display() default "visible"; // 显示模式(visible/hidden/none) */
boolean hidden() default false; // 是否隐藏 String component() default "Input";
// Map类型配置 /**
FormilyMapConfig mapConfig() default @FormilyMapConfig; // Map配置 * 装饰器类型
*/
String decorator() default "FormItem";
/**
* 字段顺序
*/
int order() default 0;
/**
* 组件属性配置
*/
FormilyComponentProps props() default @FormilyComponentProps;
/**
* 装饰器属性配置
*/
FormilyDecoratorProps decoratorProps() default @FormilyDecoratorProps;
/**
* 验证规则
*/
FormilyValidator[] validators() default {};
/**
* 联动配置
*/
FormilyReaction[] reactions() default {};
/**
* Map类型配置
*/
FormilyMapConfig mapConfig() default @FormilyMapConfig;
/**
* 编辑模式
*/
String pattern() default "editable";
/**
* 展示模式
*/
String display() default "visible";
/**
* 是否隐藏
*/
boolean hidden() default false;
/**
* 提示信息
*/
String tooltip() default "";
/**
* 是否必填
*/
boolean required() default false;
/**
* 栅格跨度
*/
int gridSpan() default 1;
} }

View File

@ -1,13 +1,38 @@
package com.qqchen.deploy.backend.framework.annotation.formily; package com.qqchen.deploy.backend.framework.annotation.formily;
import java.lang.annotation.ElementType; import java.lang.annotation.*;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE) @Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FormilyForm { public @interface FormilyForm {
String name(); // 表单名称 /**
String description() default ""; // 表单描述 * 表单名称
*/
String name();
/**
* 表单描述
*/
String description() default "";
/**
* Schema版本号
*/
String version() default "2.0";
/**
* 是否在遇到第一个错误时立即停止验证
*/
boolean validateFirst() default true;
/**
* 标签列宽度
*/
int labelCol() default 6;
/**
* 内容列宽度
*/
int wrapperCol() default 18;
} }

View File

@ -2,12 +2,66 @@ package com.qqchen.deploy.backend.framework.annotation.formily;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({})
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
public @interface FormilyReaction { public @interface FormilyReaction {
String[] dependencies() default {}; // 依赖字段 /**
String state() default ""; // 状态表达式 dataSource * 依赖字段
String value() default ""; // 状态值 {{$fetch('/api/options/B?a=' + $deps.a).then(data => data.data)}} */
String[] dependencies() default {};
/**
* 触发条件
*/
String when() default "";
/**
* 目标状态
*/
String state() default "";
/**
* 目标值
*/
String value() default "";
/**
* 执行表达式
*/
String run() default "";
/**
* 是否立即执行
*/
boolean immediate() default false;
/**
* 是否在表单挂载时执行
*/
boolean runOnMounted() default false;
/**
* 防抖时间(ms)
*/
int debounceTime() default 100;
/**
* 是否显示加载状态
*/
boolean showLoading() default true;
/**
* 加载状态文本
*/
String loadingText() default "加载中...";
/**
* 错误处理
*/
String onError() default "";
/**
* 成功处理
*/
String onSuccess() default "";
} }

View File

@ -38,12 +38,22 @@ public class FormilySchemaFactory {
FormilyForm formDef = formClass.getAnnotation(FormilyForm.class); FormilyForm formDef = formClass.getAnnotation(FormilyForm.class);
ObjectNode schema = objectMapper.createObjectNode(); ObjectNode schema = objectMapper.createObjectNode();
// 基础属性
schema.put("type", "object"); schema.put("type", "object");
schema.put("version", formDef.version());
schema.put("name", formDef.name()); schema.put("name", formDef.name());
schema.put("validateFirst", formDef.validateFirst());
if (!formDef.description().isEmpty()) { if (!formDef.description().isEmpty()) {
schema.put("description", formDef.description()); schema.put("description", formDef.description());
} }
// 表单布局配置
ObjectNode formProps = schema.putObject("x-props");
formProps.put("labelCol", formDef.labelCol());
formProps.put("wrapperCol", formDef.wrapperCol());
ObjectNode properties = schema.putObject("properties"); ObjectNode properties = schema.putObject("properties");
boolean hasFields = false; boolean hasFields = false;
@ -70,41 +80,6 @@ public class FormilySchemaFactory {
return schema; return schema;
} }
/**
* 获取类的所有字段包括父类的字段
* @param clazz 要获取字段的类
* @return 所有字段的列表
*/
private static List<Field> getAllFields(Class<?> clazz) {
List<Field> fields = new ArrayList<>();
Class<?> currentClass = clazz;
while (currentClass != null && currentClass != Object.class) {
fields.addAll(Arrays.asList(currentClass.getDeclaredFields()));
currentClass = currentClass.getSuperclass();
}
return fields;
}
/**
* 查找最近的带有FormilyForm注解的类
* @param clazz 要查找的类
* @return 带有FormilyForm注解的最近的类如果没有找到则返回null
*/
private static Class<?> findNearestFormClass(Class<?> clazz) {
Class<?> currentClass = clazz;
while (currentClass != null && currentClass != Object.class) {
if (currentClass.isAnnotationPresent(FormilyForm.class)) {
return currentClass;
}
currentClass = currentClass.getSuperclass();
}
return null;
}
private static JsonNode generateFieldSchema(FormilyField field) { private static JsonNode generateFieldSchema(FormilyField field) {
ObjectNode fieldSchema = objectMapper.createObjectNode(); ObjectNode fieldSchema = objectMapper.createObjectNode();
@ -114,78 +89,42 @@ public class FormilySchemaFactory {
if (!field.description().isEmpty()) { if (!field.description().isEmpty()) {
fieldSchema.put("description", field.description()); fieldSchema.put("description", field.description());
} }
if (!field.defaultValue().isEmpty()) {
fieldSchema.put("default", field.defaultValue());
}
if (!field.tooltip().isEmpty()) {
fieldSchema.put("tooltip", field.tooltip());
}
// 组件属性 // 组件属性
fieldSchema.put("x-decorator", field.decorator()); fieldSchema.put("x-decorator", field.decorator());
if ("map".equals(field.type())) {
// 对于Map类型使用ArrayItems组件
fieldSchema.put("x-component", "ArrayItems");
// 生成Map配置
ObjectNode componentProps = generateMapComponentProps(field.mapConfig());
if (componentProps.size() > 0) {
fieldSchema.set("x-component-props", componentProps);
}
// 生成Map项的Schema
ObjectNode items = fieldSchema.putObject("items");
items.put("type", "object");
items.put("x-component", "ArrayItems.Item");
// 生成Map项的属性Schema
ObjectNode properties = items.putObject("properties");
// 键的Schema
ObjectNode keySchema = properties.putObject("key");
keySchema.put("type", "string");
keySchema.put("title", field.mapConfig().keyTitle());
keySchema.put("x-decorator", "FormItem");
keySchema.put("x-component", field.mapConfig().keyComponent());
ObjectNode keyProps = generateComponentProps(field.mapConfig().keyProps(), field.mapConfig().keyComponent());
if (keyProps.size() > 0) {
keySchema.set("x-component-props", keyProps);
}
// 值的Schema
ObjectNode valueSchema = properties.putObject("value");
valueSchema.put("type", "string");
valueSchema.put("title", field.mapConfig().valueTitle());
valueSchema.put("x-decorator", "FormItem");
valueSchema.put("x-component", field.mapConfig().valueComponent());
ObjectNode valueProps = generateComponentProps(field.mapConfig().valueProps(), field.mapConfig().valueComponent());
if (valueProps.size() > 0) {
valueSchema.set("x-component-props", valueProps);
}
// 操作按钮
ObjectNode operationSchema = properties.putObject("operations");
operationSchema.put("type", "void");
operationSchema.put("x-component", "ArrayItems.Remove");
operationSchema.put("x-component-props", field.mapConfig().removeText());
// 添加按钮
ObjectNode addition = fieldSchema.putObject("properties").putObject("addition");
addition.put("type", "void");
addition.put("title", field.mapConfig().addText());
addition.put("x-component", "ArrayItems.Addition");
} else {
fieldSchema.put("x-component", field.component()); fieldSchema.put("x-component", field.component());
// 组件属性配置
// 装饰器属性
ObjectNode decoratorProps = generateDecoratorProps(field);
if (decoratorProps.size() > 0) {
fieldSchema.set("x-decorator-props", decoratorProps);
}
// 组件属性
if ("map".equals(field.type())) {
generateMapSchema(fieldSchema, field.mapConfig());
} else {
ObjectNode componentProps = generateComponentProps(field.props(), field.component()); ObjectNode componentProps = generateComponentProps(field.props(), field.component());
if (componentProps.size() > 0) { if (componentProps.size() > 0) {
fieldSchema.set("x-component-props", componentProps); fieldSchema.set("x-component-props", componentProps);
} }
} }
// 装饰器属性配置 // 验证规则
ObjectNode decoratorProps = generateDecoratorProps(field.decoratorProps()); if (field.validators().length > 0 || field.required()) {
if (decoratorProps.size() > 0) {
fieldSchema.set("x-decorator-props", decoratorProps);
}
// 校验规则
if (field.validators().length > 0) {
ArrayNode validators = fieldSchema.putArray("x-validator"); ArrayNode validators = fieldSchema.putArray("x-validator");
if (field.required()) {
ObjectNode requiredValidator = objectMapper.createObjectNode();
requiredValidator.put("required", true);
requiredValidator.put("message", "此项为必填项");
validators.add(requiredValidator);
}
for (FormilyValidator validator : field.validators()) { for (FormilyValidator validator : field.validators()) {
validators.add(generateValidator(validator)); validators.add(generateValidator(validator));
} }
@ -199,7 +138,7 @@ public class FormilySchemaFactory {
} }
} }
// 展示控制 - 这些属性始终输出因为它们有默认值 // 展示控制
fieldSchema.put("x-pattern", field.pattern()); fieldSchema.put("x-pattern", field.pattern());
fieldSchema.put("x-display", field.display()); fieldSchema.put("x-display", field.display());
fieldSchema.put("x-hidden", field.hidden()); fieldSchema.put("x-hidden", field.hidden());
@ -207,12 +146,18 @@ public class FormilySchemaFactory {
return fieldSchema; return fieldSchema;
} }
private static ObjectNode generateMapComponentProps(FormilyMapConfig config) { private static ObjectNode generateDecoratorProps(FormilyField field) {
ObjectNode props = objectMapper.createObjectNode(); ObjectNode props = objectMapper.createObjectNode();
props.put("allowAdd", config.allowAdd()); if (field.gridSpan() > 1) {
props.put("allowRemove", config.allowRemove()); props.put("gridSpan", field.gridSpan());
props.put("allowCustomKey", config.allowCustomKey()); }
if (!field.tooltip().isEmpty()) {
props.put("tooltip", field.tooltip());
}
if (field.required()) {
props.put("asterisk", true);
}
return props; return props;
} }
@ -220,11 +165,19 @@ public class FormilySchemaFactory {
private static ObjectNode generateComponentProps(FormilyComponentProps props, String componentType) { private static ObjectNode generateComponentProps(FormilyComponentProps props, String componentType) {
ObjectNode node = objectMapper.createObjectNode(); ObjectNode node = objectMapper.createObjectNode();
// 通用属性
if (!props.placeholder().isEmpty()) {
node.put("placeholder", props.placeholder());
}
if (props.disabled()) {
node.put("disabled", true);
}
if (props.loading()) {
node.put("loading", true);
}
// Select组件属性 // Select组件属性
if ("Select".equals(componentType)) { if ("Select".equals(componentType)) {
if (!props.api().isEmpty()) {
node.put("api", props.api());
}
if (!props.labelField().equals("label")) { if (!props.labelField().equals("label")) {
node.put("labelField", props.labelField()); node.put("labelField", props.labelField());
} }
@ -237,12 +190,11 @@ public class FormilySchemaFactory {
if (props.showSearch()) { if (props.showSearch()) {
node.put("showSearch", true); node.put("showSearch", true);
} }
if (!props.placeholder().isEmpty()) {
node.put("placeholder", props.placeholder());
}
if (!props.mode().isEmpty()) { if (!props.mode().isEmpty()) {
node.put("mode", props.mode()); node.put("mode", props.mode());
} }
node.put("filterOption", props.filterOption());
if (props.enum_().length > 0) { if (props.enum_().length > 0) {
ArrayNode enumValues = node.putArray("enum"); ArrayNode enumValues = node.putArray("enum");
Arrays.stream(props.enum_()).forEach(enumValues::add); Arrays.stream(props.enum_()).forEach(enumValues::add);
@ -256,62 +208,86 @@ public class FormilySchemaFactory {
// MonacoEditor组件属性 // MonacoEditor组件属性
if ("MonacoEditor".equals(componentType)) { if ("MonacoEditor".equals(componentType)) {
FormilyEditorProps editorProps = props.editor(); FormilyEditorProps editorProps = props.editor();
if (editorProps != null) {
if (!editorProps.placeholder().isEmpty()) {
node.put("placeholder", editorProps.placeholder());
}
ObjectNode options = node.putObject("options"); ObjectNode options = node.putObject("options");
options.put("language", editorProps.language()); options.put("language", editorProps.language());
options.put("theme", editorProps.theme()); options.put("theme", editorProps.theme());
options.put("minimap", editorProps.minimap());
ObjectNode minimap = options.putObject("minimap"); options.put("lineNumbers", editorProps.lineNumbers());
minimap.put("enabled", editorProps.minimap()); options.put("wordWrap", editorProps.wordWrap());
options.put("lineNumbers", editorProps.lineNumbers() ? "on" : "off");
options.put("wordWrap", editorProps.wordWrap() ? "on" : "off");
options.put("fontSize", editorProps.fontSize()); options.put("fontSize", editorProps.fontSize());
options.put("tabSize", editorProps.tabSize()); options.put("tabSize", editorProps.tabSize());
options.put("automaticLayout", editorProps.automaticLayout()); options.put("automaticLayout", editorProps.automaticLayout());
options.put("folding", editorProps.folding()); options.put("folding", editorProps.folding());
} options.put("scrollBeyondLastLine", editorProps.scrollBeyondLastLine());
options.put("suggestOnTriggerCharacters", editorProps.suggestOnTriggerCharacters());
options.put("snippetsPreventQuickSuggestions", editorProps.snippetsPreventQuickSuggestions());
ObjectNode scrollbar = options.putObject("scrollbar");
scrollbar.put("vertical", editorProps.scrollbarVertical());
scrollbar.put("horizontal", editorProps.scrollbarHorizontal());
node.put("debounceTime", editorProps.debounceTime());
node.put("readOnly", editorProps.readOnly());
} }
return node; return node;
} }
private static ObjectNode generateDecoratorProps(FormilyDecoratorProps props) { private static void generateMapSchema(ObjectNode fieldSchema, FormilyMapConfig config) {
ObjectNode node = objectMapper.createObjectNode(); fieldSchema.put("x-component", "ArrayItems");
if (!props.tooltip().isEmpty()) { // Map组件属性
node.put("tooltip", props.tooltip()); ObjectNode componentProps = fieldSchema.putObject("x-component-props");
} componentProps.put("allowAdd", config.allowAdd());
if (props.labelCol() != 6) { componentProps.put("allowRemove", config.allowRemove());
node.put("labelCol", props.labelCol()); componentProps.put("allowCustomKey", config.allowCustomKey());
}
if (props.wrapperCol() != 12) { // 生成Map项的Schema
node.put("wrapperCol", props.wrapperCol()); ObjectNode items = fieldSchema.putObject("items");
} items.put("type", "object");
if (!props.labelAlign().equals("right")) { items.put("x-component", "ArrayItems.Item");
node.put("labelAlign", props.labelAlign());
} // 生成Map项的属性Schema
if (!props.size().equals("default")) { ObjectNode properties = items.putObject("properties");
node.put("size", props.size());
} // 键的Schema
if (props.asterisk()) { ObjectNode keySchema = properties.putObject("key");
node.put("asterisk", true); keySchema.put("type", "string");
} keySchema.put("title", config.keyTitle());
if (!props.bordered()) { keySchema.put("x-decorator", "FormItem");
node.put("bordered", false); keySchema.put("x-component", config.keyComponent());
}
if (!props.colon()) { ObjectNode keyProps = generateComponentProps(config.keyProps(), config.keyComponent());
node.put("colon", false); if (keyProps.size() > 0) {
keySchema.set("x-component-props", keyProps);
} }
return node; // 值的Schema
ObjectNode valueSchema = properties.putObject("value");
valueSchema.put("type", "string");
valueSchema.put("title", config.valueTitle());
valueSchema.put("x-decorator", "FormItem");
valueSchema.put("x-component", config.valueComponent());
ObjectNode valueProps = generateComponentProps(config.valueProps(), config.valueComponent());
if (valueProps.size() > 0) {
valueSchema.set("x-component-props", valueProps);
} }
private static ObjectNode generateValidator(FormilyValidator validator) { // 操作按钮
ObjectNode operationSchema = properties.putObject("operations");
operationSchema.put("type", "void");
operationSchema.put("x-component", "ArrayItems.Remove");
// 添加按钮
ObjectNode addition = fieldSchema.putObject("properties").putObject("addition");
addition.put("type", "void");
addition.put("title", config.addText());
addition.put("x-component", "ArrayItems.Addition");
}
private static JsonNode generateValidator(FormilyValidator validator) {
ObjectNode node = objectMapper.createObjectNode(); ObjectNode node = objectMapper.createObjectNode();
if (validator.required()) { if (validator.required()) {
@ -326,33 +302,110 @@ public class FormilySchemaFactory {
if (!validator.pattern().isEmpty()) { if (!validator.pattern().isEmpty()) {
node.put("pattern", validator.pattern()); node.put("pattern", validator.pattern());
} }
if (validator.min() != -1) { if (validator.whitespace()) {
node.put("whitespace", true);
}
if (validator.min() != Double.MIN_VALUE) {
node.put("min", validator.min()); node.put("min", validator.min());
} }
if (validator.max() != -1) { if (validator.max() != Double.MAX_VALUE) {
node.put("max", validator.max()); node.put("max", validator.max());
} }
if (validator.minLength() != -1) {
node.put("minLength", validator.minLength());
}
if (validator.maxLength() != -1) {
node.put("maxLength", validator.maxLength());
}
if (!validator.triggerType().isEmpty()) {
node.put("triggerType", validator.triggerType());
}
if (!validator.validator().isEmpty()) {
node.put("validator", validator.validator());
}
if (!validator.asyncValidator().isEmpty()) {
node.put("asyncValidator", validator.asyncValidator());
}
return node; return node;
} }
private static ObjectNode generateReaction(FormilyReaction reaction) { private static JsonNode generateReaction(FormilyReaction reaction) {
ObjectNode node = objectMapper.createObjectNode(); ObjectNode node = objectMapper.createObjectNode();
// 添加依赖字段
if (reaction.dependencies().length > 0) { if (reaction.dependencies().length > 0) {
ArrayNode deps = node.putArray("dependencies"); ArrayNode dependencies = node.putArray("dependencies");
Arrays.stream(reaction.dependencies()).forEach(deps::add); Arrays.stream(reaction.dependencies()).forEach(dependencies::add);
}
if (!reaction.when().isEmpty()) {
node.put("when", reaction.when());
} }
// 创建fulfill结构
ObjectNode fulfill = node.putObject("fulfill"); ObjectNode fulfill = node.putObject("fulfill");
// 处理run配置
if (!reaction.run().isEmpty()) {
fulfill.put("run", reaction.run());
}
ObjectNode state = fulfill.putObject("state"); ObjectNode state = fulfill.putObject("state");
if (!reaction.state().isEmpty() && !reaction.value().isEmpty()) { // 处理loading状态
if (reaction.showLoading()) {
state.put("loading", false);
}
// 处理数据源
if (!reaction.state().isEmpty()) {
state.put(reaction.state(), reaction.value()); state.put(reaction.state(), reaction.value());
} }
if (!reaction.onSuccess().isEmpty()) {
fulfill.put("onSuccess", reaction.onSuccess());
}
if (!reaction.onError().isEmpty()) {
fulfill.put("onError", reaction.onError());
}
if (reaction.immediate()) {
node.put("immediate", true);
}
if (reaction.runOnMounted()) {
node.put("runOnMounted", true);
}
if (reaction.debounceTime() != 100) {
node.put("debounceTime", reaction.debounceTime());
}
return node; return node;
} }
private static List<Field> getAllFields(Class<?> clazz) {
List<Field> fields = new ArrayList<>();
Class<?> currentClass = clazz;
while (currentClass != null && currentClass != Object.class) {
fields.addAll(Arrays.asList(currentClass.getDeclaredFields()));
currentClass = currentClass.getSuperclass();
}
return fields;
}
private static Class<?> findNearestFormClass(Class<?> clazz) {
Class<?> currentClass = clazz;
while (currentClass != null && currentClass != Object.class) {
if (currentClass.isAnnotationPresent(FormilyForm.class)) {
return currentClass;
}
currentClass = currentClass.getSuperclass();
}
return null;
}
} }

View File

@ -2,15 +2,66 @@ package com.qqchen.deploy.backend.framework.annotation.formily;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({})
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
public @interface FormilyValidator { public @interface FormilyValidator {
boolean required() default false; // 是否必填 /**
String message() default ""; // 错误信息 * 是否必填
String format() default ""; // 格式(email/url/phone等) */
String pattern() default ""; // 正则表达式 boolean required() default false;
int min() default -1; // 最小值/长度
int max() default -1; // 最大值/长度 /**
* 错误提示消息
*/
String message() default "";
/**
* 触发类型
*/
String triggerType() default "onChange";
/**
* 格式验证
*/
String format() default "";
/**
* 是否验证空白字符
*/
boolean whitespace() default false;
/**
* 最小值
*/
double min() default Double.MIN_VALUE;
/**
* 最大值
*/
double max() default Double.MAX_VALUE;
/**
* 最小长度
*/
int minLength() default -1;
/**
* 最大长度
*/
int maxLength() default -1;
/**
* 正则表达式
*/
String pattern() default "";
/**
* 自定义验证器
*/
String validator() default "";
/**
* 异步验证器
*/
String asyncValidator() default "";
} }

View File

@ -1,262 +1,186 @@
package com.qqchen.deploy.backend.framework.annotation.formily; package com.qqchen.deploy.backend.framework.annotation.formily;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.util.Map;
import java.util.Iterator;
import java.util.List;
import java.util.Arrays;
import java.util.ArrayList;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
public class FormilySchemaFactoryTest { public class FormilySchemaFactoryTest {
@Test @Test
public void testGenerateSchema() { public void testGenerateSchema() throws Exception {
JsonNode schema = FormilySchemaFactory.generateSchema(TestForm.class); JsonNode schema = FormilySchemaFactory.generateSchema(CascadeForm.class);
// 打印生成的Schema
ObjectMapper mapper = new ObjectMapper();
System.out.println("Generated Schema:");
System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(schema));
// 验证基本结构 // 验证基本结构
assertEquals("object", schema.get("type").asText()); assertEquals("object", schema.get("type").asText());
assertEquals("测试表单", schema.get("name").asText()); assertEquals("2.0", schema.get("version").asText());
assertTrue(schema.has("properties")); assertEquals("级联选择表单", schema.get("name").asText());
assertTrue(schema.get("validateFirst").asBoolean());
// 验证字段顺序 // 验证表单布局
Iterator<String> fieldNames = schema.get("properties").fieldNames(); JsonNode xProps = schema.get("x-props");
List<String> actualOrder = new ArrayList<>(); assertEquals(6, xProps.get("labelCol").asInt());
fieldNames.forEachRemaining(actualOrder::add); assertEquals(18, xProps.get("wrapperCol").asInt());
List<String> expectedOrder = Arrays.asList("a", "b", "c", "envVars", "script"); // 验证字段A系统选择
assertEquals(expectedOrder, actualOrder, "字段顺序应该按照order排序"); JsonNode fieldA = schema.get("properties").get("systemId");
// 验证字段A
JsonNode fieldA = schema.get("properties").get("a");
assertEquals("string", fieldA.get("type").asText()); assertEquals("string", fieldA.get("type").asText());
assertEquals("选择A", fieldA.get("title").asText()); assertEquals("选择系统", fieldA.get("title").asText());
assertEquals("Select", fieldA.get("x-component").asText()); assertEquals("Select", fieldA.get("x-component").asText());
assertEquals("FormItem", fieldA.get("x-decorator").asText()); assertEquals("FormItem", fieldA.get("x-decorator").asText());
// 验证装饰器属性
JsonNode decoratorPropsA = fieldA.get("x-decorator-props");
assertTrue(decoratorPropsA.get("asterisk").asBoolean());
// 验证组件属性
JsonNode propsA = fieldA.get("x-component-props");
assertEquals("请选择系统", propsA.get("placeholder").asText());
assertEquals("name", propsA.get("labelField").asText());
assertEquals("id", propsA.get("valueField").asText());
assertTrue(propsA.get("showSearch").asBoolean());
assertEquals("single", propsA.get("mode").asText());
assertTrue(propsA.get("filterOption").asBoolean());
// 验证验证规则
JsonNode validatorA = fieldA.get("x-validator").get(0);
assertTrue(validatorA.get("required").asBoolean());
assertEquals("此项为必填项", validatorA.get("message").asText());
// 验证展示控制
assertEquals("editable", fieldA.get("x-pattern").asText()); assertEquals("editable", fieldA.get("x-pattern").asText());
assertEquals("visible", fieldA.get("x-display").asText()); assertEquals("visible", fieldA.get("x-display").asText());
assertFalse(fieldA.get("x-hidden").asBoolean()); assertFalse(fieldA.get("x-hidden").asBoolean());
// 验证组件属性 // 验证字段B视图选择
JsonNode propsA = fieldA.get("x-component-props"); JsonNode fieldB = schema.get("properties").get("viewId");
assertEquals("/api/options/A", propsA.get("api").asText());
assertEquals("name", propsA.get("labelField").asText());
assertEquals("id", propsA.get("valueField").asText());
assertTrue(propsA.get("allowClear").asBoolean());
// 验证字段B的联动
JsonNode fieldB = schema.get("properties").get("b");
JsonNode reactionsB = fieldB.get("x-reactions"); JsonNode reactionsB = fieldB.get("x-reactions");
assertTrue(reactionsB.isArray());
JsonNode reactionB = reactionsB.get(0); JsonNode reactionB = reactionsB.get(0);
assertTrue(reactionB.get("dependencies").isArray());
assertEquals("a", reactionB.get("dependencies").get(0).asText());
// 验证B依赖于A
assertTrue(reactionB.has("dependencies"));
JsonNode depsB = reactionB.get("dependencies");
assertTrue(depsB.isArray());
assertEquals("systemId", depsB.get(0).asText());
// 验证B的条件
assertEquals("{{$deps.systemId}}", reactionB.get("when").asText());
// 验证B的数据源
JsonNode fulfillB = reactionB.get("fulfill"); JsonNode fulfillB = reactionB.get("fulfill");
JsonNode stateB = fulfillB.get("state"); JsonNode stateB = fulfillB.get("state");
assertEquals("{{$fetch('/api/v1/jenkins-view/list?externalSystemId=' + $deps.a).then(data => data.data)}}", assertFalse(stateB.get("loading").asBoolean());
assertEquals("{{$fetch('/api/v1/views?systemId=' + $deps.systemId).then(data => data.data)}}",
stateB.get("dataSource").asText()); stateB.get("dataSource").asText());
// 验证字段C的联动 // 验证字段C任务选择
JsonNode fieldC = schema.get("properties").get("c"); JsonNode fieldC = schema.get("properties").get("jobId");
JsonNode reactionsC = fieldC.get("x-reactions"); JsonNode reactionsC = fieldC.get("x-reactions");
assertTrue(reactionsC.isArray());
JsonNode reactionC = reactionsC.get(0); JsonNode reactionC = reactionsC.get(0);
assertTrue(reactionC.get("dependencies").isArray());
assertEquals(2, reactionC.get("dependencies").size());
assertEquals("a", reactionC.get("dependencies").get(0).asText());
assertEquals("b", reactionC.get("dependencies").get(1).asText());
// 验证C依赖于A和B
JsonNode depsC = reactionC.get("dependencies");
assertEquals(2, depsC.size());
assertEquals("systemId", depsC.get(0).asText());
assertEquals("viewId", depsC.get(1).asText());
// 验证C的条件
assertEquals("{{$deps.systemId && $deps.viewId}}", reactionC.get("when").asText());
// 验证C的数据源
JsonNode fulfillC = reactionC.get("fulfill"); JsonNode fulfillC = reactionC.get("fulfill");
JsonNode stateC = fulfillC.get("state"); JsonNode stateC = fulfillC.get("state");
assertEquals("{{$fetch('/api/v1/jenkins-job/list?externalSystemId=' + $deps.a + '&viewId=' + $deps.b).then(data => data.data)}}", assertFalse(stateC.get("loading").asBoolean());
assertEquals(
"{{$fetch('/api/v1/jobs?systemId=' + $deps.systemId + '&viewId=' + $deps.viewId).then(data => data.data)}}",
stateC.get("dataSource").asText()); stateC.get("dataSource").asText());
// 验证编辑器字段
JsonNode fieldScript = schema.get("properties").get("script");
assertEquals("string", fieldScript.get("type").asText());
assertEquals("Pipeline script", fieldScript.get("title").asText());
assertEquals("MonacoEditor", fieldScript.get("x-component").asText());
// 验证编辑器属性
JsonNode scriptProps = fieldScript.get("x-component-props");
JsonNode options = scriptProps.get("options");
assertEquals("groovy", options.get("language").asText());
assertEquals("vs-dark", options.get("theme").asText());
assertFalse(options.get("minimap").get("enabled").asBoolean());
assertEquals("on", options.get("lineNumbers").asText());
assertEquals("on", options.get("wordWrap").asText());
assertEquals(14, options.get("fontSize").asInt());
assertEquals(4, options.get("tabSize").asInt());
assertTrue(options.get("automaticLayout").asBoolean());
assertTrue(options.get("folding").asBoolean());
assertEquals("请输入Pipeline脚本", scriptProps.get("placeholder").asText());
// 验证Map字段
JsonNode mapField = schema.get("properties").get("envVars");
assertEquals("map", mapField.get("type").asText());
assertEquals("环境变量", mapField.get("title").asText());
assertEquals("ArrayItems", mapField.get("x-component").asText());
// 验证Map项配置
JsonNode items = mapField.get("items");
assertEquals("object", items.get("type").asText());
assertEquals("ArrayItems.Item", items.get("x-component").asText());
// 验证键值配置
JsonNode properties = items.get("properties");
// 验证键配置
JsonNode keySchema = properties.get("key");
assertEquals("string", keySchema.get("type").asText());
assertEquals("变量名", keySchema.get("title").asText());
assertEquals("Select", keySchema.get("x-component").asText());
// 验证键的组件属性
JsonNode keyProps = keySchema.get("x-component-props");
assertEquals("/api/v1/env-vars/keys", keyProps.get("api").asText());
assertTrue(keyProps.get("showSearch").asBoolean());
assertTrue(keyProps.get("allowClear").asBoolean());
// 验证值配置
JsonNode valueSchema = properties.get("value");
assertEquals("string", valueSchema.get("type").asText());
assertEquals("变量值", valueSchema.get("title").asText());
assertEquals("Input", valueSchema.get("x-component").asText());
// 验证操作按钮
JsonNode operations = properties.get("operations");
assertEquals("void", operations.get("type").asText());
assertEquals("ArrayItems.Remove", operations.get("x-component").asText());
// 验证添加按钮
JsonNode addition = mapField.get("properties").get("addition");
assertEquals("void", addition.get("type").asText());
assertEquals("添加环境变量", addition.get("title").asText());
assertEquals("ArrayItems.Addition", addition.get("x-component").asText());
// 打印生成的Schema以便查看
System.out.println(schema.toPrettyString());
} }
} }
@FormilyForm(name = "测试表单") @FormilyForm(
class TestForm { name = "级联选择表单",
version = "2.0",
validateFirst = true,
labelCol = 6,
wrapperCol = 18
)
class CascadeForm {
@FormilyField( @FormilyField(
title = "选择A", title = "选择系统",
component = "Select", component = "Select",
order = 1, order = 1,
required = true,
props = @FormilyComponentProps( props = @FormilyComponentProps(
api = "/api/options/A",
labelField = "name", labelField = "name",
valueField = "id", valueField = "id",
allowClear = true showSearch = true,
mode = "single",
filterOption = true,
placeholder = "请选择系统"
), ),
validators = { reactions = {
@FormilyValidator( @FormilyReaction(
required = true, state = "dataSource",
message = "请选择A" value = "{{$fetch('/api/v1/systems').then(data => data.data)}}",
showLoading = true
) )
} }
) )
private String a; private String systemId;
@FormilyField( @FormilyField(
title = "选择B", title = "选择视图",
component = "Select", component = "Select",
order = 2, order = 2,
props = @FormilyComponentProps(
labelField = "viewName",
valueField = "id"
),
validators = {
@FormilyValidator(
required = true, required = true,
message = "请选择B" props = @FormilyComponentProps(
) labelField = "name",
}, valueField = "id",
showSearch = true,
mode = "single",
filterOption = true,
placeholder = "请选择视图"
),
reactions = { reactions = {
@FormilyReaction( @FormilyReaction(
dependencies = {"a"}, dependencies = {"systemId"},
when = "{{$deps.systemId}}",
state = "dataSource", state = "dataSource",
value = "{{$fetch('/api/v1/jenkins-view/list?externalSystemId=' + $deps.a).then(data => data.data)}}" value = "{{$fetch('/api/v1/views?systemId=' + $deps.systemId).then(data => data.data)}}",
showLoading = true
) )
} }
) )
private String b; private String viewId;
@FormilyField( @FormilyField(
title = "选择C", title = "选择任务",
component = "Select", component = "Select",
order = 3, order = 3,
props = @FormilyComponentProps(
labelField = "jobName",
valueField = "id"
),
validators = {
@FormilyValidator(
required = true, required = true,
message = "请选择C" props = @FormilyComponentProps(
) labelField = "name",
}, valueField = "id",
showSearch = true,
mode = "single",
filterOption = true,
placeholder = "请选择任务"
),
reactions = { reactions = {
@FormilyReaction( @FormilyReaction(
dependencies = {"a", "b"}, dependencies = {"systemId", "viewId"},
when = "{{$deps.systemId && $deps.viewId}}",
state = "dataSource", state = "dataSource",
value = "{{$fetch('/api/v1/jenkins-job/list?externalSystemId=' + $deps.a + '&viewId=' + $deps.b).then(data => data.data)}}" value = "{{$fetch('/api/v1/jobs?systemId=' + $deps.systemId + '&viewId=' + $deps.viewId).then(data => data.data)}}",
showLoading = true
) )
} }
) )
private String c; private String jobId;
@FormilyField(
title = "环境变量",
type = "map",
order = 4,
mapConfig = @FormilyMapConfig(
keyTitle = "变量名",
valueTitle = "变量值",
keyComponent = "Select",
valueComponent = "Input",
keyProps = @FormilyComponentProps(
api = "/api/v1/env-vars/keys",
showSearch = true,
allowClear = true
),
addText = "添加环境变量",
allowCustomKey = true
)
)
private Map<String, String> envVars;
@FormilyField(
title = "Pipeline script",
type = "string",
component = "MonacoEditor",
order = 5,
props = @FormilyComponentProps(
editor = @FormilyEditorProps(
language = "groovy",
theme = "vs-dark",
minimap = false,
lineNumbers = true,
wordWrap = true,
fontSize = 14,
tabSize = 4,
automaticLayout = true,
folding = true,
placeholder = "请输入Pipeline脚本"
)
),
validators = {
@FormilyValidator(
required = true,
message = "请输入Pipeline脚本"
)
}
)
private String script;
} }