diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/annotation/formily/FormilyComponentProps.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/annotation/formily/FormilyComponentProps.java new file mode 100644 index 00000000..07cbbbfc --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/annotation/formily/FormilyComponentProps.java @@ -0,0 +1,19 @@ +package com.qqchen.deploy.backend.framework.annotation.formily; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({}) +@Retention(RetentionPolicy.RUNTIME) +public @interface FormilyComponentProps { + String api() default ""; // 数据源API + String labelField() default "label"; // 标签字段 + String valueField() default "value"; // 值字段 + boolean allowClear() default false; // 允许清除 + boolean showSearch() default false; // 显示搜索 + String placeholder() default ""; // 占位符 + String mode() default ""; // 模式(single/multiple等) + String[] enum_() default {}; // 枚举值 + String[] enumNames() default {}; // 枚举显示值 +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/annotation/formily/FormilyDecoratorProps.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/annotation/formily/FormilyDecoratorProps.java new file mode 100644 index 00000000..47993b49 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/annotation/formily/FormilyDecoratorProps.java @@ -0,0 +1,18 @@ +package com.qqchen.deploy.backend.framework.annotation.formily; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({}) +@Retention(RetentionPolicy.RUNTIME) +public @interface FormilyDecoratorProps { + String tooltip() default ""; // 提示文本 + int labelCol() default 6; // 标签列宽 + int wrapperCol() default 12; // 组件列宽 + String labelAlign() default "right"; // 标签对齐方式 + String size() default "default"; // 尺寸 + boolean asterisk() default false; // 星号标记 + boolean bordered() default true; // 是否有边框 + boolean colon() default true; // 是否有冒号 +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/annotation/formily/FormilyField.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/annotation/formily/FormilyField.java new file mode 100644 index 00000000..5a7b7d7a --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/annotation/formily/FormilyField.java @@ -0,0 +1,32 @@ +package com.qqchen.deploy.backend.framework.annotation.formily; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface FormilyField { + // 基础属性 + String title(); // 字段标题 + String description() default ""; // 字段描述 + String type() default "string"; // 字段类型(string/number/boolean/array/object) + + // 组件属性 + String component() default "Input"; // 组件类型 + String decorator() default "FormItem"; // 装饰器类型 + FormilyComponentProps props() default @FormilyComponentProps; // 组件属性 + FormilyDecoratorProps decoratorProps() default @FormilyDecoratorProps; // 装饰器属性 + + // 校验规则 + FormilyValidator[] validators() default {}; // 校验规则 + + // 联动配置 + FormilyReaction[] reactions() default {}; // 联动规则 + + // 展示控制 + String pattern() default "editable"; // 展示模式(editable/disabled/readOnly) + String display() default "visible"; // 显示模式(visible/hidden/none) + boolean hidden() default false; // 是否隐藏 +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/annotation/formily/FormilyForm.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/annotation/formily/FormilyForm.java new file mode 100644 index 00000000..6b47ceaf --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/annotation/formily/FormilyForm.java @@ -0,0 +1,13 @@ +package com.qqchen.deploy.backend.framework.annotation.formily; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface FormilyForm { + String name(); // 表单名称 + String description() default ""; // 表单描述 +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/annotation/formily/FormilyReaction.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/annotation/formily/FormilyReaction.java new file mode 100644 index 00000000..d972f299 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/annotation/formily/FormilyReaction.java @@ -0,0 +1,13 @@ +package com.qqchen.deploy.backend.framework.annotation.formily; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({}) +@Retention(RetentionPolicy.RUNTIME) +public @interface FormilyReaction { + String[] dependencies() default {}; // 依赖字段 + String state() default ""; // 状态表达式,如 dataSource + String value() default ""; // 状态值,如 {{$fetch('/api/options/B?a=' + $deps.a).then(data => data.data)}} +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/annotation/formily/FormilySchemaFactory.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/annotation/formily/FormilySchemaFactory.java new file mode 100644 index 00000000..7e3e7521 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/annotation/formily/FormilySchemaFactory.java @@ -0,0 +1,205 @@ +package com.qqchen.deploy.backend.framework.annotation.formily; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import java.lang.reflect.Field; +import java.util.Arrays; + +public class FormilySchemaFactory { + private static final ObjectMapper objectMapper = new ObjectMapper(); + + /** + * 生成Formily JSON Schema + * @param clazz 带有FormilyForm和FormilyField注解的类 + * @return Formily JSON Schema + */ + public static JsonNode generateSchema(Class clazz) { + FormilyForm formDef = clazz.getAnnotation(FormilyForm.class); + if (formDef == null) { + throw new IllegalArgumentException("Class must be annotated with @FormilyForm"); + } + + ObjectNode schema = objectMapper.createObjectNode(); + schema.put("type", "object"); + schema.put("name", formDef.name()); + if (!formDef.description().isEmpty()) { + schema.put("description", formDef.description()); + } + + ObjectNode properties = schema.putObject("properties"); + + for (Field field : clazz.getDeclaredFields()) { + FormilyField fieldDef = field.getAnnotation(FormilyField.class); + if (fieldDef != null) { + properties.set(field.getName(), generateFieldSchema(fieldDef)); + } + } + + return schema; + } + + private static JsonNode generateFieldSchema(FormilyField field) { + ObjectNode fieldSchema = objectMapper.createObjectNode(); + + // 基础属性 + fieldSchema.put("type", field.type()); + fieldSchema.put("title", field.title()); + if (!field.description().isEmpty()) { + fieldSchema.put("description", field.description()); + } + + // 组件属性 + fieldSchema.put("x-decorator", field.decorator()); + fieldSchema.put("x-component", field.component()); + + // 组件属性配置 + ObjectNode componentProps = generateComponentProps(field.props()); + if (componentProps.size() > 0) { + fieldSchema.set("x-component-props", componentProps); + } + + // 装饰器属性配置 + ObjectNode decoratorProps = generateDecoratorProps(field.decoratorProps()); + if (decoratorProps.size() > 0) { + fieldSchema.set("x-decorator-props", decoratorProps); + } + + // 校验规则 + if (field.validators().length > 0) { + ArrayNode validators = fieldSchema.putArray("x-validator"); + for (FormilyValidator validator : field.validators()) { + validators.add(generateValidator(validator)); + } + } + + // 联动配置 + if (field.reactions().length > 0) { + ArrayNode reactions = fieldSchema.putArray("x-reactions"); + for (FormilyReaction reaction : field.reactions()) { + reactions.add(generateReaction(reaction)); + } + } + + // 展示控制 - 这些属性始终输出,因为它们有默认值 + fieldSchema.put("x-pattern", field.pattern()); + fieldSchema.put("x-display", field.display()); + fieldSchema.put("x-hidden", field.hidden()); + + return fieldSchema; + } + + private static ObjectNode generateComponentProps(FormilyComponentProps props) { + ObjectNode node = objectMapper.createObjectNode(); + + if (!props.api().isEmpty()) { + node.put("api", props.api()); + } + if (!props.labelField().equals("label")) { + node.put("labelField", props.labelField()); + } + if (!props.valueField().equals("value")) { + node.put("valueField", props.valueField()); + } + if (props.allowClear()) { + node.put("allowClear", true); + } + if (props.showSearch()) { + node.put("showSearch", true); + } + if (!props.placeholder().isEmpty()) { + node.put("placeholder", props.placeholder()); + } + if (!props.mode().isEmpty()) { + node.put("mode", props.mode()); + } + if (props.enum_().length > 0) { + ArrayNode enumValues = node.putArray("enum"); + Arrays.stream(props.enum_()).forEach(enumValues::add); + } + if (props.enumNames().length > 0) { + ArrayNode enumNames = node.putArray("enumNames"); + Arrays.stream(props.enumNames()).forEach(enumNames::add); + } + + return node; + } + + private static ObjectNode generateDecoratorProps(FormilyDecoratorProps props) { + ObjectNode node = objectMapper.createObjectNode(); + + if (!props.tooltip().isEmpty()) { + node.put("tooltip", props.tooltip()); + } + if (props.labelCol() != 6) { + node.put("labelCol", props.labelCol()); + } + if (props.wrapperCol() != 12) { + node.put("wrapperCol", props.wrapperCol()); + } + if (!props.labelAlign().equals("right")) { + node.put("labelAlign", props.labelAlign()); + } + if (!props.size().equals("default")) { + node.put("size", props.size()); + } + if (props.asterisk()) { + node.put("asterisk", true); + } + if (!props.bordered()) { + node.put("bordered", false); + } + if (!props.colon()) { + node.put("colon", false); + } + + return node; + } + + private static ObjectNode generateValidator(FormilyValidator validator) { + ObjectNode node = objectMapper.createObjectNode(); + + if (validator.required()) { + node.put("required", true); + } + if (!validator.message().isEmpty()) { + node.put("message", validator.message()); + } + if (!validator.format().isEmpty()) { + node.put("format", validator.format()); + } + if (!validator.pattern().isEmpty()) { + node.put("pattern", validator.pattern()); + } + if (validator.min() != -1) { + node.put("min", validator.min()); + } + if (validator.max() != -1) { + node.put("max", validator.max()); + } + + return node; + } + + private static ObjectNode generateReaction(FormilyReaction reaction) { + ObjectNode node = objectMapper.createObjectNode(); + + // 添加依赖字段 + if (reaction.dependencies().length > 0) { + ArrayNode deps = node.putArray("dependencies"); + Arrays.stream(reaction.dependencies()).forEach(deps::add); + } + + // 创建fulfill结构 + ObjectNode fulfill = node.putObject("fulfill"); + ObjectNode state = fulfill.putObject("state"); + + if (!reaction.state().isEmpty() && !reaction.value().isEmpty()) { + state.put(reaction.state(), reaction.value()); + } + + return node; + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/annotation/formily/FormilyValidator.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/annotation/formily/FormilyValidator.java new file mode 100644 index 00000000..c905a596 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/annotation/formily/FormilyValidator.java @@ -0,0 +1,16 @@ +package com.qqchen.deploy.backend.framework.annotation.formily; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({}) +@Retention(RetentionPolicy.RUNTIME) +public @interface FormilyValidator { + boolean required() default false; // 是否必填 + String message() default ""; // 错误信息 + String format() default ""; // 格式(email/url/phone等) + String pattern() default ""; // 正则表达式 + int min() default -1; // 最小值/长度 + int max() default -1; // 最大值/长度 +} \ No newline at end of file diff --git a/backend/src/test/java/com/qqchen/deploy/backend/framework/annotation/formily/FormilySchemaFactoryTest.java b/backend/src/test/java/com/qqchen/deploy/backend/framework/annotation/formily/FormilySchemaFactoryTest.java new file mode 100644 index 00000000..c99a5e13 --- /dev/null +++ b/backend/src/test/java/com/qqchen/deploy/backend/framework/annotation/formily/FormilySchemaFactoryTest.java @@ -0,0 +1,133 @@ +package com.qqchen.deploy.backend.framework.annotation.formily; + +import com.fasterxml.jackson.databind.JsonNode; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +public class FormilySchemaFactoryTest { + + @Test + public void testGenerateSchema() { + JsonNode schema = FormilySchemaFactory.generateSchema(TestForm.class); + + // 验证基本结构 + assertEquals("object", schema.get("type").asText()); + assertEquals("测试表单", schema.get("name").asText()); + assertTrue(schema.has("properties")); + + // 验证字段A + JsonNode fieldA = schema.get("properties").get("a"); + assertEquals("string", fieldA.get("type").asText()); + assertEquals("选择A", fieldA.get("title").asText()); + assertEquals("Select", fieldA.get("x-component").asText()); + assertEquals("FormItem", fieldA.get("x-decorator").asText()); + assertEquals("editable", fieldA.get("x-pattern").asText()); + assertEquals("visible", fieldA.get("x-display").asText()); + assertFalse(fieldA.get("x-hidden").asBoolean()); + + // 验证组件属性 + JsonNode propsA = fieldA.get("x-component-props"); + 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"); + assertTrue(reactionsB.isArray()); + JsonNode reactionB = reactionsB.get(0); + assertTrue(reactionB.get("dependencies").isArray()); + assertEquals("a", reactionB.get("dependencies").get(0).asText()); + + JsonNode fulfillB = reactionB.get("fulfill"); + JsonNode stateB = fulfillB.get("state"); + assertEquals("{{$fetch('/api/options/B?a=' + $deps.a).then(data => data.data)}}", + stateB.get("dataSource").asText()); + + // 验证字段C的联动 + JsonNode fieldC = schema.get("properties").get("c"); + JsonNode reactionsC = fieldC.get("x-reactions"); + assertTrue(reactionsC.isArray()); + 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()); + + JsonNode fulfillC = reactionC.get("fulfill"); + JsonNode stateC = fulfillC.get("state"); + assertEquals("{{$fetch('/api/options/C?a=' + $deps.a + '&b=' + $deps.b).then(data => data.data)}}", + stateC.get("dataSource").asText()); + + // 打印生成的Schema以便查看 + System.out.println(schema.toPrettyString()); + } +} + +@FormilyForm(name = "测试表单") +class TestForm { + @FormilyField( + title = "选择A", + component = "Select", + props = @FormilyComponentProps( + api = "/api/options/A", + labelField = "name", + valueField = "id", + allowClear = true + ), + validators = { + @FormilyValidator( + required = true, + message = "请选择A" + ) + } + ) + private String a; + + @FormilyField( + title = "选择B", + component = "Select", + props = @FormilyComponentProps( + labelField = "name", + valueField = "id" + ), + validators = { + @FormilyValidator( + required = true, + message = "请选择B" + ) + }, + reactions = { + @FormilyReaction( + dependencies = {"a"}, + state = "dataSource", + value = "{{$fetch('/api/options/B?a=' + $deps.a).then(data => data.data)}}" + ) + } + ) + private String b; + + @FormilyField( + title = "选择C", + component = "Select", + props = @FormilyComponentProps( + labelField = "name", + valueField = "id" + ), + validators = { + @FormilyValidator( + required = true, + message = "请选择C" + ) + }, + reactions = { + @FormilyReaction( + dependencies = {"a", "b"}, + state = "dataSource", + value = "{{$fetch('/api/options/C?a=' + $deps.a + '&b=' + $deps.b).then(data => data.data)}}" + ) + } + ) + private String c; +} \ No newline at end of file