Merge branch 'refs/heads/develop'
This commit is contained in:
@@ -11,6 +11,7 @@ import com.solution.rule.domain.config.RuleConfig;
|
|||||||
import com.solution.rule.domain.config.RuleConfigExcelRow;
|
import com.solution.rule.domain.config.RuleConfigExcelRow;
|
||||||
import com.solution.rule.domain.config.RuleConfigQuery;
|
import com.solution.rule.domain.config.RuleConfigQuery;
|
||||||
import com.solution.rule.domain.config.RuleParamMeta;
|
import com.solution.rule.domain.config.RuleParamMeta;
|
||||||
|
import com.solution.rule.domain.config.vo.RuleDecisionTreeVO;
|
||||||
import com.solution.rule.domain.config.vo.RuleFourBlocksGraphVO;
|
import com.solution.rule.domain.config.vo.RuleFourBlocksGraphVO;
|
||||||
import com.solution.rule.domain.config.vo.RuleGraphVO;
|
import com.solution.rule.domain.config.vo.RuleGraphVO;
|
||||||
import com.solution.rule.service.IRuleService;
|
import com.solution.rule.service.IRuleService;
|
||||||
@@ -93,6 +94,16 @@ import java.util.List;
|
|||||||
return success(graph);
|
return success(graph);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('system:rule:list')")
|
||||||
|
@GetMapping("/config/decision-tree")
|
||||||
|
@ApiOperation("规则决策树(围绕装备/目标/阵位/航迹四类规则,展示参数进入后的决策路径)")
|
||||||
|
public AjaxResult configDecisionTree(RuleConfigQuery query) {
|
||||||
|
startPage();
|
||||||
|
List<RuleConfig> list = ruleConfigService.selectRuleConfigList(query);
|
||||||
|
RuleDecisionTreeVO tree = ruleConfigService.buildDecisionTree(list);
|
||||||
|
return success(tree);
|
||||||
|
}
|
||||||
|
|
||||||
@PreAuthorize("@ss.hasPermi('system:rule:list')")
|
@PreAuthorize("@ss.hasPermi('system:rule:list')")
|
||||||
@GetMapping("/config/graph/four-blocks")
|
@GetMapping("/config/graph/four-blocks")
|
||||||
@ApiOperation("四块规则知识图谱(装备/目标/阵位/航迹;参数值与运行时 globalParams 一致)")
|
@ApiOperation("四块规则知识图谱(装备/目标/阵位/航迹;参数值与运行时 globalParams 一致)")
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.solution.rule.domain.config.vo;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@ApiModel("单个规则决策树分块")
|
||||||
|
public class RuleDecisionBlockVO implements Serializable {
|
||||||
|
|
||||||
|
@ApiModelProperty("分块标识:equipment/target/position/track")
|
||||||
|
private String blockId;
|
||||||
|
|
||||||
|
@ApiModelProperty("模块编码")
|
||||||
|
private String moduleCode;
|
||||||
|
|
||||||
|
@ApiModelProperty("标题")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@ApiModelProperty("Drools 规则名")
|
||||||
|
private String droolsRuleName;
|
||||||
|
|
||||||
|
@ApiModelProperty("salience")
|
||||||
|
private Integer salience;
|
||||||
|
|
||||||
|
@ApiModelProperty("树节点")
|
||||||
|
private List<RuleDecisionNodeVO> nodes;
|
||||||
|
|
||||||
|
public List<RuleDecisionNodeVO> safeNodes() {
|
||||||
|
if (nodes == null) {
|
||||||
|
nodes = new ArrayList<>();
|
||||||
|
}
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.solution.rule.domain.config.vo;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@ApiModel("规则决策树节点")
|
||||||
|
public class RuleDecisionNodeVO implements Serializable {
|
||||||
|
|
||||||
|
@ApiModelProperty("节点唯一键")
|
||||||
|
private String key;
|
||||||
|
|
||||||
|
@ApiModelProperty("节点标题")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@ApiModelProperty("节点类型")
|
||||||
|
private String nodeType;
|
||||||
|
|
||||||
|
@ApiModelProperty("补充说明")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@ApiModelProperty("右侧值文本")
|
||||||
|
private String valueText;
|
||||||
|
|
||||||
|
@ApiModelProperty("子节点")
|
||||||
|
private List<RuleDecisionNodeVO> children;
|
||||||
|
|
||||||
|
public List<RuleDecisionNodeVO> safeChildren() {
|
||||||
|
if (children == null) {
|
||||||
|
children = new ArrayList<>();
|
||||||
|
}
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.solution.rule.domain.config.vo;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@ApiModel("规则决策树")
|
||||||
|
public class RuleDecisionTreeVO implements Serializable {
|
||||||
|
|
||||||
|
@ApiModelProperty("与运行时 globalParams 一致的参数快照")
|
||||||
|
private Map<String, Object> globalParamsPreview;
|
||||||
|
|
||||||
|
@ApiModelProperty("四大规则分块")
|
||||||
|
private List<RuleDecisionBlockVO> blocks;
|
||||||
|
|
||||||
|
public Map<String, Object> safeGlobalParamsPreview() {
|
||||||
|
if (globalParamsPreview == null) {
|
||||||
|
globalParamsPreview = new LinkedHashMap<>();
|
||||||
|
}
|
||||||
|
return globalParamsPreview;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<RuleDecisionBlockVO> safeBlocks() {
|
||||||
|
if (blocks == null) {
|
||||||
|
blocks = new ArrayList<>();
|
||||||
|
}
|
||||||
|
return blocks;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import com.solution.rule.domain.config.RuleConfigExcelRow;
|
|||||||
import com.solution.rule.domain.config.RuleConfigQuery;
|
import com.solution.rule.domain.config.RuleConfigQuery;
|
||||||
import com.solution.rule.domain.config.RuleDictItem;
|
import com.solution.rule.domain.config.RuleDictItem;
|
||||||
import com.solution.rule.domain.config.RuleParamMeta;
|
import com.solution.rule.domain.config.RuleParamMeta;
|
||||||
|
import com.solution.rule.domain.config.vo.RuleDecisionTreeVO;
|
||||||
import com.solution.rule.domain.config.vo.RuleFourBlocksGraphVO;
|
import com.solution.rule.domain.config.vo.RuleFourBlocksGraphVO;
|
||||||
import com.solution.rule.domain.config.vo.RuleGraphVO;
|
import com.solution.rule.domain.config.vo.RuleGraphVO;
|
||||||
|
|
||||||
@@ -38,6 +39,8 @@ public interface IRuleConfigService {
|
|||||||
*/
|
*/
|
||||||
RuleGraphVO buildKnowledgeGraph(List<RuleConfig> ruleConfigs);
|
RuleGraphVO buildKnowledgeGraph(List<RuleConfig> ruleConfigs);
|
||||||
|
|
||||||
|
RuleDecisionTreeVO buildDecisionTree(List<RuleConfig> ruleConfigs);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 四块规则知识图谱(装备/目标/阵位/航迹):中枢 If/Then + 环形参数,参数值与 {@link #loadEnabledGlobalParams()} 一致。
|
* 四块规则知识图谱(装备/目标/阵位/航迹):中枢 If/Then + 环形参数,参数值与 {@link #loadEnabledGlobalParams()} 一致。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ import com.solution.rule.domain.config.graph.RuleFourBlockDefinition;
|
|||||||
import com.solution.rule.domain.config.graph.RuleFourBlockRuleOutputCatalog;
|
import com.solution.rule.domain.config.graph.RuleFourBlockRuleOutputCatalog;
|
||||||
import com.solution.rule.domain.config.graph.RuleParamOutputHint;
|
import com.solution.rule.domain.config.graph.RuleParamOutputHint;
|
||||||
import com.solution.rule.domain.config.vo.RuleFourBlockClusterVO;
|
import com.solution.rule.domain.config.vo.RuleFourBlockClusterVO;
|
||||||
|
import com.solution.rule.domain.config.vo.RuleDecisionBlockVO;
|
||||||
|
import com.solution.rule.domain.config.vo.RuleDecisionNodeVO;
|
||||||
|
import com.solution.rule.domain.config.vo.RuleDecisionTreeVO;
|
||||||
import com.solution.rule.domain.config.vo.RuleFourBlockParamRowVO;
|
import com.solution.rule.domain.config.vo.RuleFourBlockParamRowVO;
|
||||||
import com.solution.rule.domain.config.vo.RuleFourBlocksGraphVO;
|
import com.solution.rule.domain.config.vo.RuleFourBlocksGraphVO;
|
||||||
import com.solution.rule.domain.config.vo.RuleGraphEdgeVO;
|
import com.solution.rule.domain.config.vo.RuleGraphEdgeVO;
|
||||||
@@ -389,6 +392,58 @@ public class RuleConfigServiceImpl implements IRuleConfigService {
|
|||||||
return graph;
|
return graph;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RuleDecisionTreeVO buildDecisionTree(List<RuleConfig> ruleConfigs) {
|
||||||
|
RuleDecisionTreeVO out = new RuleDecisionTreeVO();
|
||||||
|
Map<String, Object> globalPreview = loadEnabledGlobalParams();
|
||||||
|
out.setGlobalParamsPreview(new LinkedHashMap<>(globalPreview));
|
||||||
|
|
||||||
|
List<RuleConfig> source = ruleConfigs != null ? ruleConfigs : Collections.emptyList();
|
||||||
|
List<String> ruleCodes = source.stream()
|
||||||
|
.filter(r -> r != null && ObjectUtil.isNotEmpty(r.getRuleCode()))
|
||||||
|
.map(RuleConfig::getRuleCode)
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
Map<String, List<RuleConfigParam>> paramsByRule = ruleCodes.isEmpty()
|
||||||
|
? new LinkedHashMap<>()
|
||||||
|
: ruleConfigMapper.selectParamsByRuleCodes(ruleCodes).stream()
|
||||||
|
.filter(p -> p != null && ObjectUtil.isNotEmpty(p.getRuleCode()))
|
||||||
|
.collect(Collectors.groupingBy(RuleConfigParam::getRuleCode, LinkedHashMap::new, Collectors.toList()));
|
||||||
|
|
||||||
|
Map<String, List<String>> taskTypesByRule = new LinkedHashMap<>();
|
||||||
|
if (!ruleCodes.isEmpty()) {
|
||||||
|
List<RuleConfigTaskTypeRow> taskRows = ruleConfigMapper.selectTaskTypesByRuleCodes(ruleCodes);
|
||||||
|
for (RuleConfigTaskTypeRow row : taskRows) {
|
||||||
|
if (row == null || ObjectUtil.isEmpty(row.getRuleCode()) || ObjectUtil.isEmpty(row.getTaskTypeCode())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
taskTypesByRule.computeIfAbsent(row.getRuleCode(), k -> new ArrayList<>()).add(row.getTaskTypeCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, List<RuleConfig>> byModule = source.stream()
|
||||||
|
.filter(r -> r != null && ObjectUtil.isNotEmpty(r.getModuleCode()))
|
||||||
|
.collect(Collectors.groupingBy(RuleConfig::getModuleCode, LinkedHashMap::new, Collectors.toList()));
|
||||||
|
|
||||||
|
List<RuleDecisionBlockVO> blocks = new ArrayList<>();
|
||||||
|
for (RuleFourBlockDefinition def : RuleFourBlockDefinition.ordered()) {
|
||||||
|
List<RuleConfig> moduleRules = new ArrayList<>(byModule.getOrDefault(def.getModuleCode(), Collections.emptyList()));
|
||||||
|
moduleRules.sort(Comparator.comparing(RuleConfig::getPriorityNo, Comparator.nullsLast(Integer::compareTo)));
|
||||||
|
|
||||||
|
RuleDecisionBlockVO block = new RuleDecisionBlockVO();
|
||||||
|
block.setBlockId(def.getBlockId());
|
||||||
|
block.setModuleCode(def.getModuleCode());
|
||||||
|
block.setTitle(def.getDroolsRuleName());
|
||||||
|
block.setDroolsRuleName(def.getDroolsRuleName());
|
||||||
|
block.setSalience(def.getSalience());
|
||||||
|
block.setNodes(buildDecisionBlockNodes(def, moduleRules, paramsByRule, taskTypesByRule, globalPreview));
|
||||||
|
blocks.add(block);
|
||||||
|
}
|
||||||
|
out.setBlocks(blocks);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RuleFourBlocksGraphVO buildFourBlocksKnowledgeGraph() {
|
public RuleFourBlocksGraphVO buildFourBlocksKnowledgeGraph() {
|
||||||
RuleFourBlocksGraphVO out = new RuleFourBlocksGraphVO();
|
RuleFourBlocksGraphVO out = new RuleFourBlocksGraphVO();
|
||||||
@@ -446,6 +501,272 @@ public class RuleConfigServiceImpl implements IRuleConfigService {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<RuleDecisionNodeVO> buildDecisionBlockNodes(RuleFourBlockDefinition def,
|
||||||
|
List<RuleConfig> moduleRules,
|
||||||
|
Map<String, List<RuleConfigParam>> paramsByRule,
|
||||||
|
Map<String, List<String>> taskTypesByRule,
|
||||||
|
Map<String, Object> globalPreview) {
|
||||||
|
List<RuleDecisionNodeVO> nodes = new ArrayList<>();
|
||||||
|
nodes.add(buildDecisionInputNode(def, moduleRules, paramsByRule, globalPreview));
|
||||||
|
nodes.add(buildDecisionStepNode(def));
|
||||||
|
nodes.add(buildDecisionRulesNode(def, moduleRules, paramsByRule, taskTypesByRule, globalPreview));
|
||||||
|
nodes.add(buildDecisionOutcomeNode(def, moduleRules, paramsByRule, globalPreview));
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private RuleDecisionNodeVO buildDecisionInputNode(RuleFourBlockDefinition def,
|
||||||
|
List<RuleConfig> moduleRules,
|
||||||
|
Map<String, List<RuleConfigParam>> paramsByRule,
|
||||||
|
Map<String, Object> globalPreview) {
|
||||||
|
RuleDecisionNodeVO root = decisionNode(
|
||||||
|
"decision:" + def.getBlockId() + ":input",
|
||||||
|
"输入参数",
|
||||||
|
"input_group",
|
||||||
|
"当前块启用参数会先进入 globalParams,再被规则与 Java 算子读取",
|
||||||
|
"共 " + countEnabledParams(moduleRules, paramsByRule) + " 个");
|
||||||
|
|
||||||
|
Map<String, RuleDecisionNodeVO> uniqueByKey = new LinkedHashMap<>();
|
||||||
|
for (RuleConfig rule : moduleRules) {
|
||||||
|
if (rule == null || ObjectUtil.isEmpty(rule.getRuleCode())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<RuleConfigParam> plist = paramsByRule.getOrDefault(rule.getRuleCode(), Collections.emptyList());
|
||||||
|
for (RuleConfigParam param : plist) {
|
||||||
|
if (param == null || ObjectUtil.isEmpty(param.getParamKey())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (param.getEnabled() != null && param.getEnabled() == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
uniqueByKey.computeIfAbsent(param.getParamKey(), key -> {
|
||||||
|
RuleParamMeta meta = resolveMeta(key);
|
||||||
|
Object effective = globalPreview.get(key);
|
||||||
|
String description = meta != null && ObjectUtil.isNotEmpty(meta.getDescription())
|
||||||
|
? meta.getDescription()
|
||||||
|
: "由命中的规则参数写入运行时参数池";
|
||||||
|
String valueText = "生效值: " + String.valueOf(effective != null ? effective : param.getParamVal());
|
||||||
|
return decisionNode(
|
||||||
|
"decision:" + def.getBlockId() + ":input:" + key,
|
||||||
|
ObjectUtil.defaultIfBlank(param.getParamName(), key),
|
||||||
|
"input_param",
|
||||||
|
description,
|
||||||
|
valueText);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (uniqueByKey.isEmpty()) {
|
||||||
|
root.safeChildren().add(decisionNode(
|
||||||
|
"decision:" + def.getBlockId() + ":input:empty",
|
||||||
|
"当前没有启用参数",
|
||||||
|
"empty",
|
||||||
|
"导入或新增参数后,这里会展示参与该块运算的参数",
|
||||||
|
null));
|
||||||
|
} else {
|
||||||
|
root.safeChildren().addAll(uniqueByKey.values());
|
||||||
|
}
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
private RuleDecisionNodeVO buildDecisionStepNode(RuleFourBlockDefinition def) {
|
||||||
|
RuleDecisionNodeVO root = decisionNode(
|
||||||
|
"decision:" + def.getBlockId() + ":steps",
|
||||||
|
"运算步骤",
|
||||||
|
"step_group",
|
||||||
|
"展示该规则块在运算中的主路径",
|
||||||
|
"salience=" + def.getSalience());
|
||||||
|
List<String> steps = def.getComputationSteps();
|
||||||
|
for (int i = 0; i < steps.size(); i++) {
|
||||||
|
String step = steps.get(i);
|
||||||
|
root.safeChildren().add(decisionNode(
|
||||||
|
"decision:" + def.getBlockId() + ":step:" + i,
|
||||||
|
"步骤 " + (i + 1),
|
||||||
|
"step",
|
||||||
|
step,
|
||||||
|
null));
|
||||||
|
}
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
private RuleDecisionNodeVO buildDecisionRulesNode(RuleFourBlockDefinition def,
|
||||||
|
List<RuleConfig> moduleRules,
|
||||||
|
Map<String, List<RuleConfigParam>> paramsByRule,
|
||||||
|
Map<String, List<String>> taskTypesByRule,
|
||||||
|
Map<String, Object> globalPreview) {
|
||||||
|
RuleDecisionNodeVO root = decisionNode(
|
||||||
|
"decision:" + def.getBlockId() + ":rules",
|
||||||
|
"规则链",
|
||||||
|
"rule_group",
|
||||||
|
"按优先级展示该块会如何判断、读取参数并产生产出",
|
||||||
|
"共 " + moduleRules.size() + " 条");
|
||||||
|
|
||||||
|
if (CollUtil.isEmpty(moduleRules)) {
|
||||||
|
root.safeChildren().add(decisionNode(
|
||||||
|
"decision:" + def.getBlockId() + ":rules:empty",
|
||||||
|
"当前没有规则项",
|
||||||
|
"empty",
|
||||||
|
"该块暂无可展示的规则配置",
|
||||||
|
null));
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (RuleConfig rule : moduleRules) {
|
||||||
|
if (rule == null || ObjectUtil.isEmpty(rule.getRuleCode())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String ruleCode = rule.getRuleCode();
|
||||||
|
RuleDecisionNodeVO ruleNode = decisionNode(
|
||||||
|
"decision:" + def.getBlockId() + ":rule:" + ruleCode,
|
||||||
|
ObjectUtil.defaultIfBlank(rule.getRuleName(), ruleCode),
|
||||||
|
"rule",
|
||||||
|
ObjectUtil.defaultIfBlank(rule.getRemark(), "根据条件命中后执行当前规则"),
|
||||||
|
"优先级 " + String.valueOf(rule.getPriorityNo() != null ? rule.getPriorityNo() : "-"));
|
||||||
|
|
||||||
|
ruleNode.safeChildren().add(decisionNode(
|
||||||
|
"decision:" + def.getBlockId() + ":rule:" + ruleCode + ":condition",
|
||||||
|
"命中条件",
|
||||||
|
"condition",
|
||||||
|
ObjectUtil.defaultIfBlank(rule.getConditionExpr(), def.getWhenExpr()),
|
||||||
|
null));
|
||||||
|
|
||||||
|
List<String> taskTypes = taskTypesByRule.getOrDefault(ruleCode, Collections.emptyList());
|
||||||
|
ruleNode.safeChildren().add(decisionNode(
|
||||||
|
"decision:" + def.getBlockId() + ":rule:" + ruleCode + ":task-types",
|
||||||
|
"适用任务",
|
||||||
|
"task_types",
|
||||||
|
taskTypes.isEmpty() ? "未单独限定任务类型" : joinTaskTypes(taskTypes),
|
||||||
|
taskTypes.isEmpty() ? "全部/默认" : "共 " + taskTypes.size() + " 类"));
|
||||||
|
|
||||||
|
RuleDecisionNodeVO paramGroup = decisionNode(
|
||||||
|
"decision:" + def.getBlockId() + ":rule:" + ruleCode + ":params",
|
||||||
|
"读取参数",
|
||||||
|
"param_group",
|
||||||
|
"规则命中后会读取这些参数参与计算",
|
||||||
|
null);
|
||||||
|
List<RuleConfigParam> plist = new ArrayList<>(paramsByRule.getOrDefault(ruleCode, Collections.emptyList()));
|
||||||
|
plist.sort(Comparator
|
||||||
|
.comparing((RuleConfigParam p) -> p.getSortNo() == null ? Integer.MAX_VALUE : p.getSortNo())
|
||||||
|
.thenComparing(RuleConfigParam::getParamKey, Comparator.nullsLast(String::compareTo)));
|
||||||
|
for (RuleConfigParam param : plist) {
|
||||||
|
if (param == null || ObjectUtil.isEmpty(param.getParamKey())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (param.getEnabled() != null && param.getEnabled() == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String key = param.getParamKey();
|
||||||
|
RuleParamMeta meta = resolveMeta(key);
|
||||||
|
String desc = meta != null && ObjectUtil.isNotEmpty(meta.getDescription())
|
||||||
|
? meta.getDescription()
|
||||||
|
: RuleParamOutputHint.effectLine(key, def.getModuleCode());
|
||||||
|
Object effective = globalPreview.get(key);
|
||||||
|
paramGroup.safeChildren().add(decisionNode(
|
||||||
|
"decision:" + def.getBlockId() + ":rule:" + ruleCode + ":param:" + key,
|
||||||
|
ObjectUtil.defaultIfBlank(param.getParamName(), key),
|
||||||
|
"param",
|
||||||
|
desc,
|
||||||
|
"参数键=" + key + ";生效值=" + String.valueOf(effective != null ? effective : param.getParamVal())));
|
||||||
|
}
|
||||||
|
if (CollUtil.isEmpty(paramGroup.getChildren())) {
|
||||||
|
paramGroup.safeChildren().add(decisionNode(
|
||||||
|
"decision:" + def.getBlockId() + ":rule:" + ruleCode + ":params:empty",
|
||||||
|
"无参数",
|
||||||
|
"empty",
|
||||||
|
"当前规则未配置可展示参数",
|
||||||
|
null));
|
||||||
|
}
|
||||||
|
ruleNode.safeChildren().add(paramGroup);
|
||||||
|
|
||||||
|
RuleDecisionNodeVO outputGroup = decisionNode(
|
||||||
|
"decision:" + def.getBlockId() + ":rule:" + ruleCode + ":outputs",
|
||||||
|
"产出结果",
|
||||||
|
"output_group",
|
||||||
|
ObjectUtil.defaultIfBlank(rule.getActionExpr(), def.getThenAction()),
|
||||||
|
null);
|
||||||
|
List<String> outputs = RuleFourBlockRuleOutputCatalog.outputsForRule(def.getModuleCode(), ruleCode);
|
||||||
|
if (CollUtil.isEmpty(outputs)) {
|
||||||
|
outputGroup.safeChildren().add(decisionNode(
|
||||||
|
"decision:" + def.getBlockId() + ":rule:" + ruleCode + ":outputs:default",
|
||||||
|
"执行块运算",
|
||||||
|
"output",
|
||||||
|
def.getThenAction(),
|
||||||
|
"写入结果对象"));
|
||||||
|
} else {
|
||||||
|
for (int i = 0; i < outputs.size(); i++) {
|
||||||
|
outputGroup.safeChildren().add(decisionNode(
|
||||||
|
"decision:" + def.getBlockId() + ":rule:" + ruleCode + ":output:" + i,
|
||||||
|
"结果 " + (i + 1),
|
||||||
|
"output",
|
||||||
|
outputs.get(i),
|
||||||
|
null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ruleNode.safeChildren().add(outputGroup);
|
||||||
|
root.safeChildren().add(ruleNode);
|
||||||
|
}
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
private RuleDecisionNodeVO buildDecisionOutcomeNode(RuleFourBlockDefinition def,
|
||||||
|
List<RuleConfig> moduleRules,
|
||||||
|
Map<String, List<RuleConfigParam>> paramsByRule,
|
||||||
|
Map<String, Object> globalPreview) {
|
||||||
|
RuleDecisionNodeVO root = decisionNode(
|
||||||
|
"decision:" + def.getBlockId() + ":outcome",
|
||||||
|
"最终结果",
|
||||||
|
"outcome_group",
|
||||||
|
"汇总该块在命中规则后可能产出的业务结果",
|
||||||
|
null);
|
||||||
|
|
||||||
|
LinkedHashSet<String> lines = new LinkedHashSet<>();
|
||||||
|
for (RuleConfig rule : moduleRules) {
|
||||||
|
if (rule == null || ObjectUtil.isEmpty(rule.getRuleCode())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<String> outputs = RuleFourBlockRuleOutputCatalog.outputsForRule(def.getModuleCode(), rule.getRuleCode());
|
||||||
|
lines.addAll(outputs);
|
||||||
|
for (RuleConfigParam param : paramsByRule.getOrDefault(rule.getRuleCode(), Collections.emptyList())) {
|
||||||
|
if (param == null || ObjectUtil.isEmpty(param.getParamKey())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (param.getEnabled() != null && param.getEnabled() == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Object effective = globalPreview.get(param.getParamKey());
|
||||||
|
lines.add(ObjectUtil.defaultIfBlank(param.getParamName(), param.getParamKey())
|
||||||
|
+ " -> "
|
||||||
|
+ RuleParamOutputHint.effectLine(param.getParamKey(), def.getModuleCode())
|
||||||
|
+ (effective != null ? "(当前值: " + effective + ")" : ""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lines.isEmpty()) {
|
||||||
|
root.safeChildren().add(decisionNode(
|
||||||
|
"decision:" + def.getBlockId() + ":outcome:empty",
|
||||||
|
"暂无结果描述",
|
||||||
|
"empty",
|
||||||
|
"当前块暂无可汇总的产出",
|
||||||
|
null));
|
||||||
|
} else {
|
||||||
|
int index = 0;
|
||||||
|
for (String line : lines) {
|
||||||
|
root.safeChildren().add(decisionNode(
|
||||||
|
"decision:" + def.getBlockId() + ":outcome:" + index,
|
||||||
|
"结果说明 " + (index + 1),
|
||||||
|
"outcome",
|
||||||
|
line,
|
||||||
|
null));
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
root.safeChildren().add(decisionNode(
|
||||||
|
"decision:" + def.getBlockId() + ":outcome:end",
|
||||||
|
"输出 / 结束",
|
||||||
|
"outcome",
|
||||||
|
"当前规则块的决策链在这里收束,结果写入执行对象或作为后续环节输入",
|
||||||
|
null));
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
private List<RuleFourBlockParamRowVO> buildFourBlockParamRows(RuleFourBlockDefinition def,
|
private List<RuleFourBlockParamRowVO> buildFourBlockParamRows(RuleFourBlockDefinition def,
|
||||||
Map<String, RuleConfig> ruleByCode,
|
Map<String, RuleConfig> ruleByCode,
|
||||||
List<RuleConfigParam> plist,
|
List<RuleConfigParam> plist,
|
||||||
@@ -664,6 +985,35 @@ public class RuleConfigServiceImpl implements IRuleConfigService {
|
|||||||
return graph;
|
return graph;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int countEnabledParams(List<RuleConfig> moduleRules, Map<String, List<RuleConfigParam>> paramsByRule) {
|
||||||
|
int count = 0;
|
||||||
|
for (RuleConfig rule : moduleRules) {
|
||||||
|
if (rule == null || ObjectUtil.isEmpty(rule.getRuleCode())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (RuleConfigParam param : paramsByRule.getOrDefault(rule.getRuleCode(), Collections.emptyList())) {
|
||||||
|
if (param == null || ObjectUtil.isEmpty(param.getParamKey())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (param.getEnabled() != null && param.getEnabled() == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private RuleDecisionNodeVO decisionNode(String key, String title, String nodeType, String description, String valueText) {
|
||||||
|
RuleDecisionNodeVO node = new RuleDecisionNodeVO();
|
||||||
|
node.setKey(key);
|
||||||
|
node.setTitle(title);
|
||||||
|
node.setNodeType(nodeType);
|
||||||
|
node.setDescription(description);
|
||||||
|
node.setValueText(valueText);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
private List<RuleDictItem> safeDict(String dictType) {
|
private List<RuleDictItem> safeDict(String dictType) {
|
||||||
List<RuleDictItem> list = ruleConfigMapper.selectDictByType(dictType);
|
List<RuleDictItem> list = ruleConfigMapper.selectDictByType(dictType);
|
||||||
return list != null ? list : Collections.emptyList();
|
return list != null ? list : Collections.emptyList();
|
||||||
|
|||||||
415
modeler/src/views/decision/rule-config/RuleDecisionTreePanel.vue
Normal file
415
modeler/src/views/decision/rule-config/RuleDecisionTreePanel.vue
Normal file
@@ -0,0 +1,415 @@
|
|||||||
|
<template>
|
||||||
|
<div class="rule-decision-tree">
|
||||||
|
<div class="rule-decision-tree__toolbar">
|
||||||
|
<a-popover placement="bottomLeft" trigger="click" overlay-class-name="rule-decision-tree-snapshot-popover">
|
||||||
|
<template #content>
|
||||||
|
<pre class="rule-decision-tree__popover-json">{{ previewJson }}</pre>
|
||||||
|
</template>
|
||||||
|
<a-button type="link" size="small" class="rule-decision-tree__snapshot-btn">
|
||||||
|
全局参数快照({{ previewEntryCount }})
|
||||||
|
</a-button>
|
||||||
|
</a-popover>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="errorMsg" class="rule-decision-tree__banner rule-decision-tree__banner--error">
|
||||||
|
{{ errorMsg }}
|
||||||
|
</div>
|
||||||
|
<div v-else-if="emptyHint" class="rule-decision-tree__banner">
|
||||||
|
{{ emptyHint }}
|
||||||
|
</div>
|
||||||
|
<div v-else-if="payload" class="rule-decision-tree__grid">
|
||||||
|
<section
|
||||||
|
v-for="block in payload.blocks"
|
||||||
|
:key="block.blockId"
|
||||||
|
class="rule-decision-tree__pane"
|
||||||
|
>
|
||||||
|
<header class="rule-decision-tree__pane-header">
|
||||||
|
<div class="rule-decision-tree__pane-title">{{ block.title || block.blockId }}</div>
|
||||||
|
<div class="rule-decision-tree__pane-meta">
|
||||||
|
模块:{{ block.moduleCode || '-' }} / salience:{{ block.salience ?? '-' }}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div
|
||||||
|
:ref="(el) => registerHost(block.blockId, el)"
|
||||||
|
class="rule-decision-tree__canvas"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Graph } from '@antv/g6';
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue';
|
||||||
|
import { findRuleDecisionTree } from './api';
|
||||||
|
import type { RuleConfigRequest, RuleDecisionBlock, RuleDecisionNode, RuleDecisionTreePayload } from './types';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
query: RuleConfigRequest,
|
||||||
|
refreshKey: number,
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const payload = ref<RuleDecisionTreePayload | null>(null);
|
||||||
|
const errorMsg = ref<string | null>(null);
|
||||||
|
const emptyHint = ref<string | null>(null);
|
||||||
|
|
||||||
|
const hostMap = new Map<string, HTMLDivElement>();
|
||||||
|
const graphMap = new Map<string, Graph>();
|
||||||
|
const resizeMap = new Map<string, ResizeObserver>();
|
||||||
|
const renderVersion = ref(0);
|
||||||
|
|
||||||
|
const previewEntryCount = computed(() => {
|
||||||
|
const p = payload.value?.globalParamsPreview;
|
||||||
|
return p ? Object.keys(p).length : 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const previewJson = computed(() => {
|
||||||
|
const p = payload.value?.globalParamsPreview;
|
||||||
|
if (!p) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.stringify(p, null, 2);
|
||||||
|
} catch {
|
||||||
|
return String(p);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const BLOCK_COLORS: Record<string, string> = {
|
||||||
|
equipment: '#5B8FF9',
|
||||||
|
target: '#F6903D',
|
||||||
|
position: '#61DDAA',
|
||||||
|
track: '#9270CA',
|
||||||
|
};
|
||||||
|
|
||||||
|
const NODE_COLORS: Record<string, string> = {
|
||||||
|
root: '#5B8FF9',
|
||||||
|
input_group: '#5B8FF9',
|
||||||
|
input_param: '#4fb3ff',
|
||||||
|
step_group: '#61DDAA',
|
||||||
|
step: '#49c59c',
|
||||||
|
rule_group: '#F6903D',
|
||||||
|
rule: '#ff9c4a',
|
||||||
|
condition: '#ffd166',
|
||||||
|
task_types: '#9ad0f5',
|
||||||
|
param_group: '#3db2ff',
|
||||||
|
param: '#5ab9ff',
|
||||||
|
output_group: '#f08c6c',
|
||||||
|
output: '#ffb366',
|
||||||
|
outcome_group: '#9270CA',
|
||||||
|
outcome: '#a98cf0',
|
||||||
|
end: '#e8684a',
|
||||||
|
empty: '#6f8194',
|
||||||
|
};
|
||||||
|
|
||||||
|
const registerHost = (blockId: string, el: unknown) => {
|
||||||
|
if (!(el instanceof HTMLDivElement)) {
|
||||||
|
disposeBlock(blockId);
|
||||||
|
hostMap.delete(blockId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hostMap.set(blockId, el);
|
||||||
|
};
|
||||||
|
|
||||||
|
const flattenTree = (block: RuleDecisionBlock) => {
|
||||||
|
const nodes: Array<{ id: string; label: string; nodeType: string; valueText?: string | null; description?: string | null }> = [];
|
||||||
|
const edges: Array<{ source: string; target: string }> = [];
|
||||||
|
const rootId = `block:${block.blockId}:root`;
|
||||||
|
|
||||||
|
nodes.push({
|
||||||
|
id: rootId,
|
||||||
|
label: block.title || block.blockId,
|
||||||
|
nodeType: 'root',
|
||||||
|
valueText: block.salience != null ? `salience=${block.salience}` : undefined,
|
||||||
|
description: block.droolsRuleName || undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const visit = (node: RuleDecisionNode, parentId: string) => {
|
||||||
|
const desc = node.description ? (node.valueText ? `${node.description} | ${node.valueText}` : node.description) : (node.valueText ?? '');
|
||||||
|
nodes.push({
|
||||||
|
id: node.key,
|
||||||
|
label: node.title,
|
||||||
|
nodeType: node.nodeType,
|
||||||
|
description: desc || undefined,
|
||||||
|
});
|
||||||
|
edges.push({ source: parentId, target: node.key });
|
||||||
|
(node.children ?? []).forEach((child) => visit(child, node.key));
|
||||||
|
};
|
||||||
|
|
||||||
|
(block.nodes ?? []).forEach((node) => visit(node, rootId));
|
||||||
|
return { nodes, edges };
|
||||||
|
};
|
||||||
|
|
||||||
|
const disposeBlock = (blockId: string) => {
|
||||||
|
resizeMap.get(blockId)?.disconnect();
|
||||||
|
resizeMap.delete(blockId);
|
||||||
|
const graph = graphMap.get(blockId);
|
||||||
|
if (graph) {
|
||||||
|
try {
|
||||||
|
graph.destroy();
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
graphMap.delete(blockId);
|
||||||
|
}
|
||||||
|
const host = hostMap.get(blockId);
|
||||||
|
if (host) {
|
||||||
|
host.replaceChildren();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderBlock = async (block: RuleDecisionBlock, version: number) => {
|
||||||
|
const host = hostMap.get(block.blockId);
|
||||||
|
if (!host) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
disposeBlock(block.blockId);
|
||||||
|
if (version !== renderVersion.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const width = Math.max(host.clientWidth, 320);
|
||||||
|
const height = Math.max(host.clientHeight, 240);
|
||||||
|
const accent = BLOCK_COLORS[block.blockId] ?? '#5B8FF9';
|
||||||
|
const data = flattenTree(block);
|
||||||
|
|
||||||
|
const graph = new Graph({
|
||||||
|
container: host,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
data: {
|
||||||
|
nodes: data.nodes.map((node) => ({
|
||||||
|
id: node.id,
|
||||||
|
data: node,
|
||||||
|
})),
|
||||||
|
edges: data.edges,
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
type: 'antv-dagre',
|
||||||
|
rankdir: 'TB',
|
||||||
|
nodesep: 20,
|
||||||
|
ranksep: 42,
|
||||||
|
},
|
||||||
|
node: {
|
||||||
|
type: (model: { data?: { nodeType?: string } }) => {
|
||||||
|
const t = model.data?.nodeType;
|
||||||
|
if (t === 'input_group') return 'rect';
|
||||||
|
if (t === 'end') return 'diamond';
|
||||||
|
return 'circle';
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
size: (model: { data?: { nodeType?: string } }) => {
|
||||||
|
const t = model.data?.nodeType;
|
||||||
|
if (t === 'root') return 18;
|
||||||
|
if (t === 'input_group' || t === 'end') return 20;
|
||||||
|
return 14;
|
||||||
|
},
|
||||||
|
fill: (model: { data?: { nodeType?: string } }) => NODE_COLORS[model.data?.nodeType ?? ''] ?? accent,
|
||||||
|
stroke: '#0d1f2c',
|
||||||
|
lineWidth: 1.5,
|
||||||
|
labelText: (model: { data?: { label?: string; description?: string } }) => {
|
||||||
|
const label = model.data?.label ?? '';
|
||||||
|
const description = model.data?.description;
|
||||||
|
return description ? `${label}\n${description}` : label;
|
||||||
|
},
|
||||||
|
labelFill: '#e8f4f8',
|
||||||
|
labelFontSize: 11,
|
||||||
|
labelLineHeight: 15,
|
||||||
|
labelWordWrap: true,
|
||||||
|
labelMaxWidth: 160,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
edge: {
|
||||||
|
style: {
|
||||||
|
stroke: 'rgba(120, 170, 200, 0.52)',
|
||||||
|
lineWidth: 1.2,
|
||||||
|
endArrow: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
behaviors: ['drag-canvas', 'zoom-canvas'],
|
||||||
|
});
|
||||||
|
|
||||||
|
await graph.render();
|
||||||
|
if (version !== renderVersion.value) {
|
||||||
|
try {
|
||||||
|
graph.destroy();
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await graph.fitView({ when: 'always' });
|
||||||
|
graphMap.set(block.blockId, graph);
|
||||||
|
|
||||||
|
const observer = new ResizeObserver(() => {
|
||||||
|
const nextWidth = Math.max(host.clientWidth, 320);
|
||||||
|
const nextHeight = Math.max(host.clientHeight, 240);
|
||||||
|
graph.setSize(nextWidth, nextHeight);
|
||||||
|
void graph.fitView({ when: 'overflow' });
|
||||||
|
});
|
||||||
|
observer.observe(host);
|
||||||
|
resizeMap.set(block.blockId, observer);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderAll = async () => {
|
||||||
|
if (!payload.value?.blocks?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await nextTick();
|
||||||
|
const version = renderVersion.value;
|
||||||
|
for (const block of payload.value.blocks) {
|
||||||
|
await renderBlock(block, version);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
errorMsg.value = null;
|
||||||
|
emptyHint.value = null;
|
||||||
|
payload.value = null;
|
||||||
|
renderVersion.value += 1;
|
||||||
|
Array.from(graphMap.keys()).forEach(disposeBlock);
|
||||||
|
try {
|
||||||
|
const r = await findRuleDecisionTree({
|
||||||
|
pageNum: props.query.pageNum,
|
||||||
|
pageSize: props.query.pageSize,
|
||||||
|
ruleCode: props.query.ruleCode ?? undefined,
|
||||||
|
ruleName: props.query.ruleName ?? undefined,
|
||||||
|
levelCode: props.query.levelCode ?? undefined,
|
||||||
|
kindCode: props.query.kindCode ?? undefined,
|
||||||
|
moduleCode: props.query.moduleCode ?? undefined,
|
||||||
|
enabled: props.query.enabled ?? undefined,
|
||||||
|
});
|
||||||
|
if (r.code !== 200 || !r.data) {
|
||||||
|
errorMsg.value = r.msg ?? '加载决策树失败';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!r.data.blocks?.length) {
|
||||||
|
emptyHint.value = '当前没有可展示的规则决策树数据';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
payload.value = r.data;
|
||||||
|
await renderAll();
|
||||||
|
} catch (e: unknown) {
|
||||||
|
const msg = e instanceof Error ? e.message : String(e);
|
||||||
|
errorMsg.value = `决策树请求失败:${msg}`;
|
||||||
|
message.error(errorMsg.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.query.pageNum, props.query.pageSize, props.refreshKey],
|
||||||
|
() => {
|
||||||
|
void load();
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
renderVersion.value += 1;
|
||||||
|
Array.from(graphMap.keys()).forEach(disposeBlock);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.rule-decision-tree {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-decision-tree__toolbar {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-decision-tree__snapshot-btn {
|
||||||
|
padding-left: 0;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #8fafbd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-decision-tree__banner {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #a2b1ba;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-decision-tree__banner--error {
|
||||||
|
color: #ff9c9c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-decision-tree__grid {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
grid-template-rows: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-decision-tree__pane {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
border: 1px solid rgba(80, 120, 150, 0.25);
|
||||||
|
background: linear-gradient(180deg, rgba(8, 20, 30, 0.78) 0%, rgba(5, 16, 29, 0.9) 100%);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-decision-tree__pane-header {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: 8px 10px 6px;
|
||||||
|
border-bottom: 1px solid rgba(80, 120, 150, 0.2);
|
||||||
|
background: rgba(10, 28, 40, 0.78);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-decision-tree__pane-title {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #d8edf6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-decision-tree__pane-meta {
|
||||||
|
margin-top: 2px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #7f9aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-decision-tree__canvas {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
width: 100%;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.rule-decision-tree-snapshot-popover {
|
||||||
|
.ant-popover-inner-content {
|
||||||
|
max-width: min(520px, 90vw);
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow: auto;
|
||||||
|
background: #081e3b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popover-inner {
|
||||||
|
background: #081e3b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-decision-tree__popover-json {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1.35;
|
||||||
|
color: #c9dfe8;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,466 @@
|
|||||||
|
<template>
|
||||||
|
<div class="rule-decision-tree-simple">
|
||||||
|
<div class="rule-decision-tree-simple__toolbar">
|
||||||
|
<a-popover placement="bottomLeft" trigger="click" overlay-class-name="rule-decision-tree-simple-snapshot-popover">
|
||||||
|
<template #content>
|
||||||
|
<pre class="rule-decision-tree-simple__popover-json">{{ previewJson }}</pre>
|
||||||
|
</template>
|
||||||
|
<a-button type="link" size="small" class="rule-decision-tree-simple__snapshot-btn">
|
||||||
|
全局参数快照({{ previewEntryCount }})
|
||||||
|
</a-button>
|
||||||
|
</a-popover>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="errorMsg" class="rule-decision-tree-simple__banner rule-decision-tree-simple__banner--error">
|
||||||
|
{{ errorMsg }}
|
||||||
|
</div>
|
||||||
|
<div v-else-if="emptyHint" class="rule-decision-tree-simple__banner">
|
||||||
|
{{ emptyHint }}
|
||||||
|
</div>
|
||||||
|
<div v-else ref="hostRef" class="rule-decision-tree-simple__canvas" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Graph } from '@antv/g6';
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue';
|
||||||
|
import { findRuleDecisionTree } from './api';
|
||||||
|
import type { RuleConfigRequest, RuleDecisionBlock, RuleDecisionNode, RuleDecisionTreePayload } from './types';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
query: RuleConfigRequest,
|
||||||
|
refreshKey: number,
|
||||||
|
}>();
|
||||||
|
|
||||||
|
type GraphNode = {
|
||||||
|
id: string,
|
||||||
|
label: string,
|
||||||
|
nodeType: string,
|
||||||
|
description?: string,
|
||||||
|
fullText?: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
const hostRef = ref<HTMLDivElement | null>(null);
|
||||||
|
const errorMsg = ref<string | null>(null);
|
||||||
|
const emptyHint = ref<string | null>(null);
|
||||||
|
const payload = ref<RuleDecisionTreePayload | null>(null);
|
||||||
|
const renderVersion = ref(0);
|
||||||
|
|
||||||
|
let graph: Graph | null = null;
|
||||||
|
let resizeObserver: ResizeObserver | null = null;
|
||||||
|
|
||||||
|
const previewEntryCount = computed(() => {
|
||||||
|
const p = payload.value?.globalParamsPreview;
|
||||||
|
return p ? Object.keys(p).length : 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const previewJson = computed(() => {
|
||||||
|
const p = payload.value?.globalParamsPreview;
|
||||||
|
if (!p) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.stringify(p, null, 2);
|
||||||
|
} catch {
|
||||||
|
return String(p);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const NODE_COLORS: Record<string, string> = {
|
||||||
|
start: '#4fb3ff',
|
||||||
|
block: '#5B8FF9',
|
||||||
|
params: '#49c59c',
|
||||||
|
rules: '#ff9c4a',
|
||||||
|
outputs: '#a98cf0',
|
||||||
|
end: '#e8684a',
|
||||||
|
};
|
||||||
|
|
||||||
|
const BLOCK_TITLE: Record<string, string> = {
|
||||||
|
equipment: '装备规则',
|
||||||
|
target: '目标规则',
|
||||||
|
position: '阵位规则',
|
||||||
|
track: '航迹规则',
|
||||||
|
};
|
||||||
|
|
||||||
|
const flatten = (nodes: RuleDecisionNode[] | null | undefined): RuleDecisionNode[] => {
|
||||||
|
const out: RuleDecisionNode[] = [];
|
||||||
|
const visit = (items: RuleDecisionNode[] | null | undefined) => {
|
||||||
|
(items ?? []).forEach((item) => {
|
||||||
|
out.push(item);
|
||||||
|
visit(item.children ?? []);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
visit(nodes);
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
|
const scoreParamNode = (node: RuleDecisionNode) => {
|
||||||
|
const text = `${node.title ?? ''} ${node.description ?? ''} ${node.valueText ?? ''}`.toLowerCase();
|
||||||
|
let score = 0;
|
||||||
|
const keywords = [
|
||||||
|
'模式', '类型', '目标', '装备', '阵位', '航迹', '选择', '分配', '距离', '阈值', '权重',
|
||||||
|
'比例', '数量', '规则', 'mode', 'type', 'target', 'equipment', 'position', 'track',
|
||||||
|
'select', 'assign', 'distance', 'threshold', 'weight', 'ratio', 'count', 'limit', 'min', 'max',
|
||||||
|
];
|
||||||
|
keywords.forEach((keyword) => {
|
||||||
|
if (text.includes(keyword)) {
|
||||||
|
score += 2;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (text.includes('生效值')) {
|
||||||
|
score += 1;
|
||||||
|
}
|
||||||
|
return score;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pickImportantParams = (flatNodes: RuleDecisionNode[]) => {
|
||||||
|
return flatNodes
|
||||||
|
.filter((node) => node.nodeType === 'input_param' || node.nodeType === 'param')
|
||||||
|
.sort((a, b) => scoreParamNode(b) - scoreParamNode(a))
|
||||||
|
.slice(0, 4);
|
||||||
|
};
|
||||||
|
|
||||||
|
const pickCoreRules = (flatNodes: RuleDecisionNode[]) => {
|
||||||
|
return flatNodes
|
||||||
|
.filter((node) => node.nodeType === 'rule')
|
||||||
|
.slice(0, 2);
|
||||||
|
};
|
||||||
|
|
||||||
|
const pickOutputs = (flatNodes: RuleDecisionNode[]) => {
|
||||||
|
const list = flatNodes.filter((node) => node.nodeType === 'output' || node.nodeType === 'outcome');
|
||||||
|
return list.slice(0, 3);
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildMergedTree = (tree: RuleDecisionTreePayload) => {
|
||||||
|
const nodes: GraphNode[] = [];
|
||||||
|
const edges: Array<{ source: string; target: string }> = [];
|
||||||
|
|
||||||
|
const startId = 'simple:start';
|
||||||
|
nodes.push({
|
||||||
|
id: startId,
|
||||||
|
label: '输入参数',
|
||||||
|
nodeType: 'start',
|
||||||
|
description: `汇总四类规则块共享的运行时参数(${Object.keys(tree.globalParamsPreview ?? {}).length}项)`,
|
||||||
|
});
|
||||||
|
|
||||||
|
let previousMainId = startId;
|
||||||
|
const blocks = [...(tree.blocks ?? [])];
|
||||||
|
for (const block of blocks) {
|
||||||
|
const mainId = `simple:block:${block.blockId}`;
|
||||||
|
nodes.push({
|
||||||
|
id: mainId,
|
||||||
|
label: BLOCK_TITLE[block.blockId] ?? (block.title || block.blockId),
|
||||||
|
nodeType: 'block',
|
||||||
|
description: `salience=${block.salience ?? '-'};保留核心规则与关键参数`,
|
||||||
|
});
|
||||||
|
edges.push({ source: previousMainId, target: mainId });
|
||||||
|
|
||||||
|
const flatNodes = flatten(block.nodes);
|
||||||
|
const importantParams = pickImportantParams(flatNodes);
|
||||||
|
const coreRules = pickCoreRules(flatNodes);
|
||||||
|
const outputs = pickOutputs(flatNodes);
|
||||||
|
|
||||||
|
const paramId = `simple:block:${block.blockId}:params`;
|
||||||
|
nodes.push({
|
||||||
|
id: paramId,
|
||||||
|
label: '关键参数',
|
||||||
|
nodeType: 'params',
|
||||||
|
description: importantParams.length > 0
|
||||||
|
? importantParams.map((item) => `${item.title}${item.valueText ? `:${item.valueText}` : ''}`).join(';')
|
||||||
|
: '无关键参数',
|
||||||
|
fullText: importantParams.length > 0
|
||||||
|
? `关键参数:${importantParams.map((item) => `${item.title}${item.valueText ? `=${item.valueText}` : ''}`).join(';')}`
|
||||||
|
: '关键参数:无',
|
||||||
|
});
|
||||||
|
edges.push({ source: mainId, target: paramId });
|
||||||
|
|
||||||
|
const ruleId = `simple:block:${block.blockId}:rules`;
|
||||||
|
nodes.push({
|
||||||
|
id: ruleId,
|
||||||
|
label: '核心规则',
|
||||||
|
nodeType: 'rules',
|
||||||
|
description: coreRules.length > 0
|
||||||
|
? coreRules.map((item) => item.title).join(';')
|
||||||
|
: '无核心规则',
|
||||||
|
fullText: coreRules.length > 0
|
||||||
|
? `核心规则:${coreRules.map((item) => item.title).join(';')}`
|
||||||
|
: '核心规则:无',
|
||||||
|
});
|
||||||
|
edges.push({ source: mainId, target: ruleId });
|
||||||
|
|
||||||
|
const outputId = `simple:block:${block.blockId}:outputs`;
|
||||||
|
nodes.push({
|
||||||
|
id: outputId,
|
||||||
|
label: '阶段输出',
|
||||||
|
nodeType: 'outputs',
|
||||||
|
description: outputs.length > 0
|
||||||
|
? outputs.map((item) => String(item.title || item.description || '')).join(';')
|
||||||
|
: '无阶段输出',
|
||||||
|
fullText: outputs.length > 0
|
||||||
|
? `阶段输出:${outputs.map((item) => String(item.title || item.description || '')).join(';')}`
|
||||||
|
: '阶段输出:无',
|
||||||
|
});
|
||||||
|
edges.push({ source: mainId, target: outputId });
|
||||||
|
|
||||||
|
previousMainId = mainId;
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes.push({
|
||||||
|
id: 'simple:end',
|
||||||
|
label: '输出 / 结束',
|
||||||
|
nodeType: 'end',
|
||||||
|
description: '四类规则块汇总后输出最终结果',
|
||||||
|
fullText: '输出 / 结束:四类规则块汇总后输出最终结果',
|
||||||
|
});
|
||||||
|
edges.push({ source: previousMainId, target: 'simple:end' });
|
||||||
|
|
||||||
|
return { nodes, edges };
|
||||||
|
};
|
||||||
|
|
||||||
|
const disposeGraph = () => {
|
||||||
|
resizeObserver?.disconnect();
|
||||||
|
resizeObserver = null;
|
||||||
|
if (graph) {
|
||||||
|
try {
|
||||||
|
graph.destroy();
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
graph = null;
|
||||||
|
}
|
||||||
|
hostRef.value?.replaceChildren();
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderGraph = async (tree: RuleDecisionTreePayload, version: number) => {
|
||||||
|
const host = hostRef.value;
|
||||||
|
if (!host) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
disposeGraph();
|
||||||
|
if (version !== renderVersion.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = buildMergedTree(tree);
|
||||||
|
graph = new Graph({
|
||||||
|
container: host,
|
||||||
|
width: Math.max(host.clientWidth, 320),
|
||||||
|
height: Math.max(host.clientHeight, 240),
|
||||||
|
data: {
|
||||||
|
nodes: data.nodes.map((node) => ({
|
||||||
|
id: node.id,
|
||||||
|
data: node,
|
||||||
|
})),
|
||||||
|
edges: data.edges,
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
type: 'antv-dagre',
|
||||||
|
rankdir: 'LR',
|
||||||
|
nodesep: 28,
|
||||||
|
ranksep: 56,
|
||||||
|
},
|
||||||
|
node: {
|
||||||
|
type: (model: { data?: { nodeType?: string } }) => {
|
||||||
|
const t = model.data?.nodeType;
|
||||||
|
if (t === 'start') return 'rect';
|
||||||
|
if (t === 'end') return 'diamond';
|
||||||
|
return 'circle';
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
size: (model: { data?: { nodeType?: string } }) => {
|
||||||
|
const t = model.data?.nodeType;
|
||||||
|
if (t === 'start' || t === 'end') return 26;
|
||||||
|
if (t === 'block') return 20;
|
||||||
|
if (t === 'params' || t === 'rules' || t === 'outputs') return 16;
|
||||||
|
return 16;
|
||||||
|
},
|
||||||
|
fill: (model: { data?: { nodeType?: string } }) => NODE_COLORS[model.data?.nodeType ?? 'block'] ?? '#5B8FF9',
|
||||||
|
stroke: '#0d1f2c',
|
||||||
|
lineWidth: 1.5,
|
||||||
|
labelText: (model: { data?: { label?: string; description?: string; fullText?: string; nodeType?: string } }) => {
|
||||||
|
const text = model.data?.fullText;
|
||||||
|
if (text) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
const label = model.data?.label ?? '';
|
||||||
|
const description = model.data?.description;
|
||||||
|
return description ? `${label}:${description}` : label;
|
||||||
|
},
|
||||||
|
labelFill: '#e8f4f8',
|
||||||
|
labelFontSize: 11,
|
||||||
|
labelLineHeight: 15,
|
||||||
|
labelWordWrap: true,
|
||||||
|
labelMaxWidth: 360,
|
||||||
|
labelPlacement: (model: { data?: { nodeType?: string } }) => {
|
||||||
|
const t = model.data?.nodeType;
|
||||||
|
if (t === 'params' || t === 'rules' || t === 'outputs') {
|
||||||
|
return 'right';
|
||||||
|
}
|
||||||
|
return 'bottom';
|
||||||
|
},
|
||||||
|
labelOffsetX: (model: { data?: { nodeType?: string } }) => {
|
||||||
|
const t = model.data?.nodeType;
|
||||||
|
if (t === 'params' || t === 'rules' || t === 'outputs') {
|
||||||
|
return 14;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
labelTextAlign: (model: { data?: { nodeType?: string } }) => {
|
||||||
|
const t = model.data?.nodeType;
|
||||||
|
if (t === 'params' || t === 'rules' || t === 'outputs') {
|
||||||
|
return 'left';
|
||||||
|
}
|
||||||
|
return 'center';
|
||||||
|
},
|
||||||
|
labelBackground: true,
|
||||||
|
labelBackgroundFill: 'rgba(6, 21, 40, 0.88)',
|
||||||
|
labelBackgroundRadius: 4,
|
||||||
|
labelPadding: [6, 8],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
edge: {
|
||||||
|
style: {
|
||||||
|
stroke: 'rgba(120, 170, 200, 0.52)',
|
||||||
|
lineWidth: 1.2,
|
||||||
|
endArrow: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
behaviors: ['drag-canvas', 'zoom-canvas'],
|
||||||
|
});
|
||||||
|
|
||||||
|
await graph.render();
|
||||||
|
if (version !== renderVersion.value) {
|
||||||
|
disposeGraph();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await graph.fitView({ when: 'always' });
|
||||||
|
|
||||||
|
resizeObserver = new ResizeObserver(() => {
|
||||||
|
if (!graph || !hostRef.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
graph.setSize(Math.max(hostRef.value.clientWidth, 320), Math.max(hostRef.value.clientHeight, 240));
|
||||||
|
void graph.fitView({ when: 'overflow' });
|
||||||
|
});
|
||||||
|
resizeObserver.observe(host);
|
||||||
|
};
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
errorMsg.value = null;
|
||||||
|
emptyHint.value = null;
|
||||||
|
payload.value = null;
|
||||||
|
renderVersion.value += 1;
|
||||||
|
disposeGraph();
|
||||||
|
try {
|
||||||
|
const r = await findRuleDecisionTree({
|
||||||
|
pageNum: props.query.pageNum,
|
||||||
|
pageSize: props.query.pageSize,
|
||||||
|
ruleCode: props.query.ruleCode ?? undefined,
|
||||||
|
ruleName: props.query.ruleName ?? undefined,
|
||||||
|
levelCode: props.query.levelCode ?? undefined,
|
||||||
|
kindCode: props.query.kindCode ?? undefined,
|
||||||
|
moduleCode: props.query.moduleCode ?? undefined,
|
||||||
|
enabled: props.query.enabled ?? undefined,
|
||||||
|
});
|
||||||
|
if (r.code !== 200 || !r.data) {
|
||||||
|
errorMsg.value = r.msg ?? '加载简化决策树失败';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!r.data.blocks?.length) {
|
||||||
|
emptyHint.value = '当前没有可展示的简化决策树数据';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
payload.value = r.data;
|
||||||
|
await nextTick();
|
||||||
|
await renderGraph(r.data, renderVersion.value);
|
||||||
|
} catch (e: unknown) {
|
||||||
|
const msg = e instanceof Error ? e.message : String(e);
|
||||||
|
errorMsg.value = `简化决策树请求失败:${msg}`;
|
||||||
|
message.error(errorMsg.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.query.pageNum, props.query.pageSize, props.refreshKey],
|
||||||
|
() => {
|
||||||
|
void load();
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
renderVersion.value += 1;
|
||||||
|
disposeGraph();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.rule-decision-tree-simple {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-decision-tree-simple__toolbar {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-decision-tree-simple__snapshot-btn {
|
||||||
|
padding-left: 0;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #8fafbd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-decision-tree-simple__banner {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #a2b1ba;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-decision-tree-simple__banner--error {
|
||||||
|
color: #ff9c9c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-decision-tree-simple__canvas {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid rgba(80, 120, 150, 0.22);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba(4, 18, 28, 0.35);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.rule-decision-tree-simple-snapshot-popover {
|
||||||
|
.ant-popover-inner-content {
|
||||||
|
max-width: min(520px, 90vw);
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow: auto;
|
||||||
|
background: #081e3b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popover-inner {
|
||||||
|
background: #081e3b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-decision-tree-simple__popover-json {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1.35;
|
||||||
|
color: #c9dfe8;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -12,20 +12,28 @@
|
|||||||
class="rule-knowledge-graph"
|
class="rule-knowledge-graph"
|
||||||
:class="{
|
:class="{
|
||||||
'rule-knowledge-graph--four-blocks': density === 'four-blocks',
|
'rule-knowledge-graph--four-blocks': density === 'four-blocks',
|
||||||
|
'rule-knowledge-graph--decision-tree': density === 'decision-tree',
|
||||||
'rule-knowledge-graph--fullscreen': isFullscreen,
|
'rule-knowledge-graph--fullscreen': isFullscreen,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div class="rule-knowledge-graph__toolbar">
|
<div class="rule-knowledge-graph__toolbar">
|
||||||
<a-radio-group v-model:value="density" size="small" button-style="solid">
|
<a-radio-group v-model:value="density" size="small" button-style="solid">
|
||||||
<a-radio-button value="overview">简要结构</a-radio-button>
|
<a-radio-button value="decision-tree-simple">决策树(简化)</a-radio-button>
|
||||||
<a-radio-button value="full">完整</a-radio-button>
|
<a-radio-button value="decision-tree">决策树</a-radio-button>
|
||||||
|
<a-radio-button value="full">完整图谱</a-radio-button>
|
||||||
<a-radio-button value="four-blocks">四块分区</a-radio-button>
|
<a-radio-button value="four-blocks">四块分区</a-radio-button>
|
||||||
</a-radio-group>
|
</a-radio-group>
|
||||||
<span v-if="density !== 'four-blocks'" class="rule-knowledge-graph__hint">
|
<span v-if="density === 'decision-tree-simple'" class="rule-knowledge-graph__hint">
|
||||||
简要:仅层级→种类→模块→规则;完整:含参数、任务类型与执行顺序边;四块:业务运算步骤 + 规则项 + 参数(与 globalParams 一致)
|
决策树(简化):把装备、目标、阵位、航迹四个规则块合并成一棵总树,只保留关键参数、核心规则和阶段输出
|
||||||
|
</span>
|
||||||
|
<span v-else-if="density === 'decision-tree'" class="rule-knowledge-graph__hint">
|
||||||
|
决策树:按装备、目标、阵位、航迹四个规则块展示输入参数、运算步骤、命中规则与结果产出
|
||||||
|
</span>
|
||||||
|
<span v-else-if="density === 'full'" class="rule-knowledge-graph__hint">
|
||||||
|
完整图谱包含参数、任务类型与执行顺序
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="rule-knowledge-graph__hint rule-knowledge-graph__hint--compact">
|
<span v-else class="rule-knowledge-graph__hint rule-knowledge-graph__hint--compact">
|
||||||
四宫格 · 拖拽画布 / 滚轮缩放
|
四块分区:可拖动画布,滚轮缩放
|
||||||
</span>
|
</span>
|
||||||
<a-button type="default" size="small" class="rule-knowledge-graph__fullscreen-btn" @click="toggleFullscreen">
|
<a-button type="default" size="small" class="rule-knowledge-graph__fullscreen-btn" @click="toggleFullscreen">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
@@ -35,7 +43,9 @@
|
|||||||
{{ isFullscreen ? '退出全屏' : '全屏' }}
|
{{ isFullscreen ? '退出全屏' : '全屏' }}
|
||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
<RuleFourBlocksPanel v-if="density === 'four-blocks'" :refresh-key="refreshKey" />
|
<RuleDecisionTreeSimplePanel v-if="density === 'decision-tree-simple'" :query="query" :refresh-key="refreshKey" />
|
||||||
|
<RuleDecisionTreePanel v-else-if="density === 'decision-tree'" :query="query" :refresh-key="refreshKey" />
|
||||||
|
<RuleFourBlocksPanel v-else-if="density === 'four-blocks'" :refresh-key="refreshKey" />
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div v-if="errorMsg" class="rule-knowledge-graph__banner rule-knowledge-graph__banner--error">
|
<div v-if="errorMsg" class="rule-knowledge-graph__banner rule-knowledge-graph__banner--error">
|
||||||
{{ errorMsg }}
|
{{ errorMsg }}
|
||||||
@@ -54,10 +64,12 @@ import { Graph } from '@antv/g6';
|
|||||||
import { message } from 'ant-design-vue';
|
import { message } from 'ant-design-vue';
|
||||||
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
import { findRuleConfigGraph } from './api';
|
import { findRuleConfigGraph } from './api';
|
||||||
|
import RuleDecisionTreeSimplePanel from './RuleDecisionTreeSimplePanel.vue';
|
||||||
|
import RuleDecisionTreePanel from './RuleDecisionTreePanel.vue';
|
||||||
import RuleFourBlocksPanel from './RuleFourBlocksPanel.vue';
|
import RuleFourBlocksPanel from './RuleFourBlocksPanel.vue';
|
||||||
import type { RuleConfigRequest, RuleGraphEdge, RuleGraphNode, RuleGraphPayload } from './types';
|
import type { RuleConfigRequest, RuleGraphPayload } from './types';
|
||||||
|
|
||||||
type RuleGraphDensityMode = 'overview' | 'full' | 'four-blocks';
|
type RuleGraphDensityMode = 'decision-tree-simple' | 'decision-tree' | 'full' | 'four-blocks';
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
densityChange: [RuleGraphDensityMode],
|
densityChange: [RuleGraphDensityMode],
|
||||||
@@ -73,7 +85,7 @@ const hostRef = ref<HTMLDivElement | null>(null);
|
|||||||
const isFullscreen = ref(false);
|
const isFullscreen = ref(false);
|
||||||
const errorMsg = ref<string | null>(null);
|
const errorMsg = ref<string | null>(null);
|
||||||
const emptyHint = ref<string | null>(null);
|
const emptyHint = ref<string | null>(null);
|
||||||
const density = ref<RuleGraphDensityMode>('overview');
|
const density = ref<RuleGraphDensityMode>('decision-tree-simple');
|
||||||
const lastPayload = ref<RuleGraphPayload | null>(null);
|
const lastPayload = ref<RuleGraphPayload | null>(null);
|
||||||
|
|
||||||
let graph: Graph | null = null;
|
let graph: Graph | null = null;
|
||||||
@@ -88,56 +100,6 @@ const NODE_COLORS: Record<string, string> = {
|
|||||||
taskType: '#269A99',
|
taskType: '#269A99',
|
||||||
};
|
};
|
||||||
|
|
||||||
const str = (v: unknown): string => (v === null || v === undefined ? '' : String(v));
|
|
||||||
|
|
||||||
/** 简要:只保留结构主干,边改为 level → kind → module → rule 便于分层布局 */
|
|
||||||
const buildOverviewPayload = (payload: RuleGraphPayload): RuleGraphPayload => {
|
|
||||||
const keepTypes = new Set(['level', 'kind', 'module', 'rule']);
|
|
||||||
const nodes = payload.nodes.filter((n) => keepTypes.has(n.nodeType));
|
|
||||||
const nodeIds = new Set(nodes.map((n) => n.id));
|
|
||||||
|
|
||||||
const edgeSeen = new Set<string>();
|
|
||||||
const edges: RuleGraphEdge[] = [];
|
|
||||||
|
|
||||||
const addEdge = (source: string, target: string, suffix: string) => {
|
|
||||||
if (!nodeIds.has(source) || !nodeIds.has(target)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const key = `${source}|${target}`;
|
|
||||||
if (edgeSeen.has(key)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
edgeSeen.add(key);
|
|
||||||
edges.push({
|
|
||||||
id: `ov:${suffix}:${key}`,
|
|
||||||
source,
|
|
||||||
target,
|
|
||||||
edgeType: 'overview_hierarchy',
|
|
||||||
label: null,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const r of nodes) {
|
|
||||||
if (r.nodeType !== 'rule' || !r.payload) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const levelCode = str(r.payload.levelCode);
|
|
||||||
const kindCode = str(r.payload.kindCode);
|
|
||||||
const moduleCode = str(r.payload.moduleCode);
|
|
||||||
if (!levelCode || !kindCode || !moduleCode) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const lid = `level:${levelCode}`;
|
|
||||||
const kid = `kind:${levelCode}:${kindCode}`;
|
|
||||||
const mid = `module:${moduleCode}`;
|
|
||||||
addEdge(lid, kid, 'lk');
|
|
||||||
addEdge(kid, mid, 'km');
|
|
||||||
addEdge(mid, r.id, 'mr');
|
|
||||||
}
|
|
||||||
|
|
||||||
return { nodes, edges };
|
|
||||||
};
|
|
||||||
|
|
||||||
const toGraphData = (payload: RuleGraphPayload) => ({
|
const toGraphData = (payload: RuleGraphPayload) => ({
|
||||||
nodes: payload.nodes.map((n) => ({
|
nodes: payload.nodes.map((n) => ({
|
||||||
id: n.id,
|
id: n.id,
|
||||||
@@ -213,56 +175,36 @@ const disposeGraph = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildGraph = async (payload: RuleGraphPayload, mode: 'overview' | 'full') => {
|
const buildGraph = async (payload: RuleGraphPayload) => {
|
||||||
await disposeGraph();
|
await disposeGraph();
|
||||||
const el = hostRef.value;
|
const el = hostRef.value;
|
||||||
if (!el) {
|
if (!el) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const raw = mode === 'overview' ? buildOverviewPayload(payload) : payload;
|
const data = toGraphData(payload);
|
||||||
const data = toGraphData(raw);
|
if (!payload.nodes.length) {
|
||||||
if (raw.nodes.length === 0) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const width = Math.max(el.clientWidth, 280);
|
const width = Math.max(el.clientWidth, 280);
|
||||||
const height = Math.max(el.clientHeight, 240);
|
const height = Math.max(el.clientHeight, 240);
|
||||||
|
|
||||||
const layout =
|
|
||||||
mode === 'overview'
|
|
||||||
? {
|
|
||||||
type: 'antv-dagre' as const,
|
|
||||||
rankdir: 'TB',
|
|
||||||
ranksep: 56,
|
|
||||||
nodesep: 36,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
type: 'd3-force' as const,
|
|
||||||
};
|
|
||||||
|
|
||||||
graph = new Graph({
|
graph = new Graph({
|
||||||
container: el,
|
container: el,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
data,
|
data,
|
||||||
layout,
|
layout: {
|
||||||
|
type: 'd3-force' as const,
|
||||||
|
},
|
||||||
node: {
|
node: {
|
||||||
style: {
|
style: {
|
||||||
size: (d: { data?: { nodeType?: string } }) => {
|
size: (d: { data?: { nodeType?: string } }) => (d.data?.nodeType === 'param' ? 5 : 11),
|
||||||
const t = d.data?.nodeType;
|
|
||||||
if (mode === 'overview') {
|
|
||||||
if (t === 'rule') return 20;
|
|
||||||
if (t === 'module') return 18;
|
|
||||||
return 16;
|
|
||||||
}
|
|
||||||
return t === 'param' ? 5 : 11;
|
|
||||||
},
|
|
||||||
fill: (d: { data?: { nodeType?: string } }) => NODE_COLORS[d.data?.nodeType ?? ''] ?? '#8B8B8B',
|
fill: (d: { data?: { nodeType?: string } }) => NODE_COLORS[d.data?.nodeType ?? ''] ?? '#8B8B8B',
|
||||||
labelText: (d: { data?: { label?: string }; id: string }) => d.data?.label ?? String(d.id),
|
labelText: (d: { data?: { label?: string }; id: string }) => d.data?.label ?? String(d.id),
|
||||||
labelFill: '#e8f4f8',
|
labelFill: '#e8f4f8',
|
||||||
labelFontSize: (d: { data?: { nodeType?: string } }) =>
|
labelFontSize: 9,
|
||||||
(mode === 'overview' ? (d.data?.nodeType === 'rule' ? 11 : 12) : 9),
|
labelMaxWidth: 100,
|
||||||
labelMaxWidth: mode === 'overview' ? 200 : 100,
|
|
||||||
labelWordWrap: true,
|
labelWordWrap: true,
|
||||||
lineWidth: 1.5,
|
lineWidth: 1.5,
|
||||||
stroke: '#0d1f2c',
|
stroke: '#0d1f2c',
|
||||||
@@ -270,19 +212,11 @@ const buildGraph = async (payload: RuleGraphPayload, mode: 'overview' | 'full')
|
|||||||
},
|
},
|
||||||
edge: {
|
edge: {
|
||||||
style: {
|
style: {
|
||||||
stroke: (d: { data?: { edgeType?: string } }) => {
|
stroke: (d: { data?: { edgeType?: string } }) =>
|
||||||
const t = d.data?.edgeType;
|
(d.data?.edgeType === 'rule_exec_before' ? '#faad14' : 'rgba(150, 175, 190, 0.35)'),
|
||||||
if (t === 'overview_hierarchy') {
|
|
||||||
return 'rgba(120, 170, 200, 0.55)';
|
|
||||||
}
|
|
||||||
if (t === 'rule_exec_before') {
|
|
||||||
return '#faad14';
|
|
||||||
}
|
|
||||||
return 'rgba(150, 175, 190, 0.35)';
|
|
||||||
},
|
|
||||||
lineWidth: (d: { data?: { edgeType?: string } }) =>
|
lineWidth: (d: { data?: { edgeType?: string } }) =>
|
||||||
(d.data?.edgeType === 'rule_exec_before' ? 2 : 1),
|
(d.data?.edgeType === 'rule_exec_before' ? 2 : 1),
|
||||||
endArrow: mode !== 'overview',
|
endArrow: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],
|
behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],
|
||||||
@@ -302,18 +236,11 @@ const buildGraph = async (payload: RuleGraphPayload, mode: 'overview' | 'full')
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderFromCache = async () => {
|
const renderFromCache = async () => {
|
||||||
if (density.value === 'four-blocks') {
|
if (density.value !== 'full' || !lastPayload.value || !lastPayload.value.nodes?.length) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!lastPayload.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!lastPayload.value.nodes?.length) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await nextTick();
|
await nextTick();
|
||||||
const mode: 'overview' | 'full' = density.value === 'full' ? 'full' : 'overview';
|
await buildGraph(lastPayload.value);
|
||||||
await buildGraph(lastPayload.value, mode);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
@@ -336,13 +263,12 @@ const load = async () => {
|
|||||||
lastPayload.value = null;
|
lastPayload.value = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const payload = r.data;
|
if (!r.data.nodes?.length) {
|
||||||
if (!payload.nodes?.length) {
|
|
||||||
emptyHint.value = '当前页无规则数据,图谱为空';
|
emptyHint.value = '当前页无规则数据,图谱为空';
|
||||||
lastPayload.value = null;
|
lastPayload.value = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
lastPayload.value = payload;
|
lastPayload.value = r.data;
|
||||||
await renderFromCache();
|
await renderFromCache();
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
const msg = e instanceof Error ? e.message : String(e);
|
const msg = e instanceof Error ? e.message : String(e);
|
||||||
@@ -355,7 +281,7 @@ const load = async () => {
|
|||||||
watch(
|
watch(
|
||||||
() => [props.query.pageNum, props.query.pageSize, props.refreshKey],
|
() => [props.query.pageNum, props.query.pageSize, props.refreshKey],
|
||||||
() => {
|
() => {
|
||||||
if (density.value === 'four-blocks') {
|
if (density.value !== 'full') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
void load();
|
void load();
|
||||||
@@ -365,11 +291,11 @@ watch(
|
|||||||
|
|
||||||
watch(density, async (mode, prev) => {
|
watch(density, async (mode, prev) => {
|
||||||
emit('densityChange', mode);
|
emit('densityChange', mode);
|
||||||
if (mode === 'four-blocks') {
|
if (mode !== 'full') {
|
||||||
await disposeGraph();
|
await disposeGraph();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (prev === 'four-blocks') {
|
if (prev !== 'full') {
|
||||||
await load();
|
await load();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -418,42 +344,10 @@ onBeforeUnmount(() => {
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #7a8d96;
|
color: #7a8d96;
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
max-width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.rule-knowledge-graph__hint--compact {
|
.rule-knowledge-graph__hint--compact {
|
||||||
font-size: 10px;
|
letter-spacing: 0.2px;
|
||||||
color: #6d8290;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rule-knowledge-graph--four-blocks {
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rule-knowledge-graph--fullscreen {
|
|
||||||
box-sizing: border-box;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
max-height: 100vh;
|
|
||||||
padding: 10px 12px;
|
|
||||||
gap: 10px;
|
|
||||||
background: #0d1f2c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rule-knowledge-graph--fullscreen .rule-knowledge-graph__host {
|
|
||||||
min-height: 0;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rule-knowledge-graph--fullscreen :deep(.rule-four-blocks) {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rule-knowledge-graph__host {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 200px;
|
|
||||||
position: relative;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.rule-knowledge-graph__banner {
|
.rule-knowledge-graph__banner {
|
||||||
@@ -470,4 +364,18 @@ onBeforeUnmount(() => {
|
|||||||
.rule-knowledge-graph__banner--error {
|
.rule-knowledge-graph__banner--error {
|
||||||
color: #ff9c9c;
|
color: #ff9c9c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rule-knowledge-graph__host {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid rgba(80, 120, 150, 0.18);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba(4, 18, 28, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-knowledge-graph--fullscreen {
|
||||||
|
background: #061522;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
import { HttpRequestClient, originalAxios } from '@/utils/request';
|
import { HttpRequestClient, originalAxios } from '@/utils/request';
|
||||||
import type { ApiDataResponse, BasicResponse } from '@/types';
|
import type { ApiDataResponse, BasicResponse } from '@/types';
|
||||||
import type { AxiosResponse } from 'axios';
|
import type { AxiosResponse } from 'axios';
|
||||||
import type { RuleConfig, RuleConfigPageableResponse, RuleConfigRequest, RuleDictItem, RuleFourBlocksPayload, RuleGraphPayload, RuleParamMeta } from './types';
|
import type { RuleConfig, RuleConfigPageableResponse, RuleConfigRequest, RuleDecisionTreePayload, RuleDictItem, RuleFourBlocksPayload, RuleGraphPayload, RuleParamMeta } from './types';
|
||||||
|
|
||||||
const req = HttpRequestClient.create<BasicResponse>({
|
const req = HttpRequestClient.create<BasicResponse>({
|
||||||
baseURL: '/api',
|
baseURL: '/api',
|
||||||
@@ -52,6 +52,10 @@ export const findRuleFourBlocksGraph = (): Promise<ApiDataResponse<RuleFourBlock
|
|||||||
return req.get('/system/rule/config/graph/four-blocks');
|
return req.get('/system/rule/config/graph/four-blocks');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const findRuleDecisionTree = (query: Partial<RuleConfigRequest> = {}): Promise<ApiDataResponse<RuleDecisionTreePayload>> => {
|
||||||
|
return req.get('/system/rule/config/decision-tree', query);
|
||||||
|
};
|
||||||
|
|
||||||
export const exportRuleConfig = (query: Partial<RuleConfigRequest> = {}): Promise<AxiosResponse<Blob>> => {
|
export const exportRuleConfig = (query: Partial<RuleConfigRequest> = {}): Promise<AxiosResponse<Blob>> => {
|
||||||
return originalAxios.post('/api/system/rule/config/export', query, {
|
return originalAxios.post('/api/system/rule/config/export', query, {
|
||||||
responseType: 'blob',
|
responseType: 'blob',
|
||||||
|
|||||||
@@ -300,7 +300,7 @@ const clampRightPanelWidth = (n: number) => {
|
|||||||
return Math.min(dynamicMax, Math.max(RIGHT_PANEL_MIN, Math.round(n)));
|
return Math.min(dynamicMax, Math.max(RIGHT_PANEL_MIN, Math.round(n)));
|
||||||
};
|
};
|
||||||
|
|
||||||
const onRuleGraphDensityChange = (mode: 'overview' | 'full' | 'four-blocks') => {
|
const onRuleGraphDensityChange = (mode: 'decision-tree-simple' | 'decision-tree' | 'full' | 'four-blocks') => {
|
||||||
if (mode === 'four-blocks') {
|
if (mode === 'four-blocks') {
|
||||||
if (graphPanePercentBeforeFourBlocks.value === null) {
|
if (graphPanePercentBeforeFourBlocks.value === null) {
|
||||||
graphPanePercentBeforeFourBlocks.value = graphPanePercent.value;
|
graphPanePercentBeforeFourBlocks.value = graphPanePercent.value;
|
||||||
|
|||||||
@@ -135,3 +135,26 @@ export interface RuleFourBlocksPayload {
|
|||||||
globalParamsPreview: Record<string, unknown>,
|
globalParamsPreview: Record<string, unknown>,
|
||||||
blocks: RuleFourBlockCluster[],
|
blocks: RuleFourBlockCluster[],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RuleDecisionNode {
|
||||||
|
key: string,
|
||||||
|
title: string,
|
||||||
|
nodeType: string,
|
||||||
|
description?: string | null,
|
||||||
|
valueText?: string | null,
|
||||||
|
children?: RuleDecisionNode[] | null,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RuleDecisionBlock {
|
||||||
|
blockId: string,
|
||||||
|
moduleCode: NullableString,
|
||||||
|
title: NullableString,
|
||||||
|
droolsRuleName: NullableString,
|
||||||
|
salience: number | null,
|
||||||
|
nodes: RuleDecisionNode[],
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RuleDecisionTreePayload {
|
||||||
|
globalParamsPreview: Record<string, unknown>,
|
||||||
|
blocks: RuleDecisionBlock[],
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user