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.RuleConfigQuery;
|
||||
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.RuleGraphVO;
|
||||
import com.solution.rule.service.IRuleService;
|
||||
@@ -93,6 +94,16 @@ import java.util.List;
|
||||
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')")
|
||||
@GetMapping("/config/graph/four-blocks")
|
||||
@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.RuleDictItem;
|
||||
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.RuleGraphVO;
|
||||
|
||||
@@ -38,6 +39,8 @@ public interface IRuleConfigService {
|
||||
*/
|
||||
RuleGraphVO buildKnowledgeGraph(List<RuleConfig> ruleConfigs);
|
||||
|
||||
RuleDecisionTreeVO buildDecisionTree(List<RuleConfig> ruleConfigs);
|
||||
|
||||
/**
|
||||
* 四块规则知识图谱(装备/目标/阵位/航迹):中枢 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.RuleParamOutputHint;
|
||||
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.RuleFourBlocksGraphVO;
|
||||
import com.solution.rule.domain.config.vo.RuleGraphEdgeVO;
|
||||
@@ -389,6 +392,58 @@ public class RuleConfigServiceImpl implements IRuleConfigService {
|
||||
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
|
||||
public RuleFourBlocksGraphVO buildFourBlocksKnowledgeGraph() {
|
||||
RuleFourBlocksGraphVO out = new RuleFourBlocksGraphVO();
|
||||
@@ -446,6 +501,272 @@ public class RuleConfigServiceImpl implements IRuleConfigService {
|
||||
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,
|
||||
Map<String, RuleConfig> ruleByCode,
|
||||
List<RuleConfigParam> plist,
|
||||
@@ -664,6 +985,35 @@ public class RuleConfigServiceImpl implements IRuleConfigService {
|
||||
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) {
|
||||
List<RuleDictItem> list = ruleConfigMapper.selectDictByType(dictType);
|
||||
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--four-blocks': density === 'four-blocks',
|
||||
'rule-knowledge-graph--decision-tree': density === 'decision-tree',
|
||||
'rule-knowledge-graph--fullscreen': isFullscreen,
|
||||
}"
|
||||
>
|
||||
<div class="rule-knowledge-graph__toolbar">
|
||||
<a-radio-group v-model:value="density" size="small" button-style="solid">
|
||||
<a-radio-button value="overview">简要结构</a-radio-button>
|
||||
<a-radio-button value="full">完整</a-radio-button>
|
||||
<a-radio-button value="decision-tree-simple">决策树(简化)</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-group>
|
||||
<span v-if="density !== 'four-blocks'" class="rule-knowledge-graph__hint">
|
||||
简要:仅层级→种类→模块→规则;完整:含参数、任务类型与执行顺序边;四块:业务运算步骤 + 规则项 + 参数(与 globalParams 一致)
|
||||
<span v-if="density === 'decision-tree-simple'" class="rule-knowledge-graph__hint">
|
||||
决策树(简化):把装备、目标、阵位、航迹四个规则块合并成一棵总树,只保留关键参数、核心规则和阶段输出
|
||||
</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 v-else class="rule-knowledge-graph__hint rule-knowledge-graph__hint--compact">
|
||||
四宫格 · 拖拽画布 / 滚轮缩放
|
||||
四块分区:可拖动画布,滚轮缩放
|
||||
</span>
|
||||
<a-button type="default" size="small" class="rule-knowledge-graph__fullscreen-btn" @click="toggleFullscreen">
|
||||
<template #icon>
|
||||
@@ -35,7 +43,9 @@
|
||||
{{ isFullscreen ? '退出全屏' : '全屏' }}
|
||||
</a-button>
|
||||
</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>
|
||||
<div v-if="errorMsg" class="rule-knowledge-graph__banner rule-knowledge-graph__banner--error">
|
||||
{{ errorMsg }}
|
||||
@@ -54,10 +64,12 @@ import { Graph } from '@antv/g6';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import { findRuleConfigGraph } from './api';
|
||||
import RuleDecisionTreeSimplePanel from './RuleDecisionTreeSimplePanel.vue';
|
||||
import RuleDecisionTreePanel from './RuleDecisionTreePanel.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<{
|
||||
densityChange: [RuleGraphDensityMode],
|
||||
@@ -73,7 +85,7 @@ const hostRef = ref<HTMLDivElement | null>(null);
|
||||
const isFullscreen = ref(false);
|
||||
const errorMsg = 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);
|
||||
|
||||
let graph: Graph | null = null;
|
||||
@@ -88,56 +100,6 @@ const NODE_COLORS: Record<string, string> = {
|
||||
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) => ({
|
||||
nodes: payload.nodes.map((n) => ({
|
||||
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();
|
||||
const el = hostRef.value;
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
const raw = mode === 'overview' ? buildOverviewPayload(payload) : payload;
|
||||
const data = toGraphData(raw);
|
||||
if (raw.nodes.length === 0) {
|
||||
const data = toGraphData(payload);
|
||||
if (!payload.nodes.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const width = Math.max(el.clientWidth, 280);
|
||||
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({
|
||||
container: el,
|
||||
width,
|
||||
height,
|
||||
data,
|
||||
layout,
|
||||
layout: {
|
||||
type: 'd3-force' as const,
|
||||
},
|
||||
node: {
|
||||
style: {
|
||||
size: (d: { data?: { nodeType?: string } }) => {
|
||||
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;
|
||||
},
|
||||
size: (d: { data?: { nodeType?: string } }) => (d.data?.nodeType === 'param' ? 5 : 11),
|
||||
fill: (d: { data?: { nodeType?: string } }) => NODE_COLORS[d.data?.nodeType ?? ''] ?? '#8B8B8B',
|
||||
labelText: (d: { data?: { label?: string }; id: string }) => d.data?.label ?? String(d.id),
|
||||
labelFill: '#e8f4f8',
|
||||
labelFontSize: (d: { data?: { nodeType?: string } }) =>
|
||||
(mode === 'overview' ? (d.data?.nodeType === 'rule' ? 11 : 12) : 9),
|
||||
labelMaxWidth: mode === 'overview' ? 200 : 100,
|
||||
labelFontSize: 9,
|
||||
labelMaxWidth: 100,
|
||||
labelWordWrap: true,
|
||||
lineWidth: 1.5,
|
||||
stroke: '#0d1f2c',
|
||||
@@ -270,19 +212,11 @@ const buildGraph = async (payload: RuleGraphPayload, mode: 'overview' | 'full')
|
||||
},
|
||||
edge: {
|
||||
style: {
|
||||
stroke: (d: { data?: { edgeType?: string } }) => {
|
||||
const t = d.data?.edgeType;
|
||||
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)';
|
||||
},
|
||||
stroke: (d: { data?: { edgeType?: string } }) =>
|
||||
(d.data?.edgeType === 'rule_exec_before' ? '#faad14' : 'rgba(150, 175, 190, 0.35)'),
|
||||
lineWidth: (d: { data?: { edgeType?: string } }) =>
|
||||
(d.data?.edgeType === 'rule_exec_before' ? 2 : 1),
|
||||
endArrow: mode !== 'overview',
|
||||
endArrow: true,
|
||||
},
|
||||
},
|
||||
behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],
|
||||
@@ -302,18 +236,11 @@ const buildGraph = async (payload: RuleGraphPayload, mode: 'overview' | 'full')
|
||||
};
|
||||
|
||||
const renderFromCache = async () => {
|
||||
if (density.value === 'four-blocks') {
|
||||
return;
|
||||
}
|
||||
if (!lastPayload.value) {
|
||||
return;
|
||||
}
|
||||
if (!lastPayload.value.nodes?.length) {
|
||||
if (density.value !== 'full' || !lastPayload.value || !lastPayload.value.nodes?.length) {
|
||||
return;
|
||||
}
|
||||
await nextTick();
|
||||
const mode: 'overview' | 'full' = density.value === 'full' ? 'full' : 'overview';
|
||||
await buildGraph(lastPayload.value, mode);
|
||||
await buildGraph(lastPayload.value);
|
||||
};
|
||||
|
||||
const load = async () => {
|
||||
@@ -336,13 +263,12 @@ const load = async () => {
|
||||
lastPayload.value = null;
|
||||
return;
|
||||
}
|
||||
const payload = r.data;
|
||||
if (!payload.nodes?.length) {
|
||||
if (!r.data.nodes?.length) {
|
||||
emptyHint.value = '当前页无规则数据,图谱为空';
|
||||
lastPayload.value = null;
|
||||
return;
|
||||
}
|
||||
lastPayload.value = payload;
|
||||
lastPayload.value = r.data;
|
||||
await renderFromCache();
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
@@ -355,7 +281,7 @@ const load = async () => {
|
||||
watch(
|
||||
() => [props.query.pageNum, props.query.pageSize, props.refreshKey],
|
||||
() => {
|
||||
if (density.value === 'four-blocks') {
|
||||
if (density.value !== 'full') {
|
||||
return;
|
||||
}
|
||||
void load();
|
||||
@@ -365,11 +291,11 @@ watch(
|
||||
|
||||
watch(density, async (mode, prev) => {
|
||||
emit('densityChange', mode);
|
||||
if (mode === 'four-blocks') {
|
||||
if (mode !== 'full') {
|
||||
await disposeGraph();
|
||||
return;
|
||||
}
|
||||
if (prev === 'four-blocks') {
|
||||
if (prev !== 'full') {
|
||||
await load();
|
||||
return;
|
||||
}
|
||||
@@ -418,42 +344,10 @@ onBeforeUnmount(() => {
|
||||
font-size: 11px;
|
||||
color: #7a8d96;
|
||||
line-height: 1.35;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.rule-knowledge-graph__hint--compact {
|
||||
font-size: 10px;
|
||||
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;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.rule-knowledge-graph__banner {
|
||||
@@ -470,4 +364,18 @@ onBeforeUnmount(() => {
|
||||
.rule-knowledge-graph__banner--error {
|
||||
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>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
import { HttpRequestClient, originalAxios } from '@/utils/request';
|
||||
import type { ApiDataResponse, BasicResponse } from '@/types';
|
||||
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>({
|
||||
baseURL: '/api',
|
||||
@@ -52,6 +52,10 @@ export const findRuleFourBlocksGraph = (): Promise<ApiDataResponse<RuleFourBlock
|
||||
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>> => {
|
||||
return originalAxios.post('/api/system/rule/config/export', query, {
|
||||
responseType: 'blob',
|
||||
|
||||
@@ -300,7 +300,7 @@ const clampRightPanelWidth = (n: number) => {
|
||||
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 (graphPanePercentBeforeFourBlocks.value === null) {
|
||||
graphPanePercentBeforeFourBlocks.value = graphPanePercent.value;
|
||||
|
||||
@@ -135,3 +135,26 @@ export interface RuleFourBlocksPayload {
|
||||
globalParamsPreview: Record<string, unknown>,
|
||||
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