Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -93,7 +93,7 @@ public class BehaviortreeController extends BaseController
|
|||||||
@PreAuthorize("@ss.hasPermi('system:behaviortree:add')")
|
@PreAuthorize("@ss.hasPermi('system:behaviortree:add')")
|
||||||
@Log(title = "行为树主", businessType = BusinessType.INSERT)
|
@Log(title = "行为树主", businessType = BusinessType.INSERT)
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public AjaxResult add(@RequestBody Behaviortree behaviortree)
|
public AjaxResult add(@RequestBody Behaviortree behaviortree)
|
||||||
{
|
{
|
||||||
return toAjax(behaviortreeProcessor.create(behaviortree));
|
return toAjax(behaviortreeProcessor.create(behaviortree));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import com.solution.rule.domain.Rule;
|
|||||||
import com.solution.rule.domain.config.RuleConfig;
|
import com.solution.rule.domain.config.RuleConfig;
|
||||||
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.RuleGraphVO;
|
||||||
import com.solution.rule.service.IRuleService;
|
import com.solution.rule.service.IRuleService;
|
||||||
import com.solution.rule.service.IRuleConfigService;
|
import com.solution.rule.service.IRuleConfigService;
|
||||||
import io.swagger.annotations.Api;
|
import io.swagger.annotations.Api;
|
||||||
@@ -77,6 +78,16 @@ import java.util.List;
|
|||||||
return getDataTable(ruleConfigService.selectRuleConfigList(query));
|
return getDataTable(ruleConfigService.selectRuleConfigList(query));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('system:rule:list')")
|
||||||
|
@GetMapping("/config/graph")
|
||||||
|
@ApiOperation("规则知识图谱(与列表相同的分页与筛选条件)")
|
||||||
|
public AjaxResult configGraph(RuleConfigQuery query) {
|
||||||
|
startPage();
|
||||||
|
List<RuleConfig> list = ruleConfigService.selectRuleConfigList(query);
|
||||||
|
RuleGraphVO graph = ruleConfigService.buildKnowledgeGraph(list);
|
||||||
|
return success(graph);
|
||||||
|
}
|
||||||
|
|
||||||
@PreAuthorize("@ss.hasPermi('system:rule:query')")
|
@PreAuthorize("@ss.hasPermi('system:rule:query')")
|
||||||
@GetMapping("/config/{ruleCode}")
|
@GetMapping("/config/{ruleCode}")
|
||||||
@ApiOperation("查询规则聚合详情")
|
@ApiOperation("查询规则聚合详情")
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<select id="selectTemplateparameterdefList" parameterType="templateparameterdef" resultMap="TemplateparameterdefResult">
|
<select id="selectTemplateparameterdefList" parameterType="templateparameterdef" resultMap="TemplateparameterdefResult">
|
||||||
<include refid="selectTemplateparameterdefVo"/>
|
<include refid="selectTemplateparameterdefVo"/>
|
||||||
<where>
|
<where>
|
||||||
|
<if test="id != null"> and id = #{id}</if>
|
||||||
<if test="templateId != null "> and template_id = #{templateId}</if>
|
<if test="templateId != null "> and template_id = #{templateId}</if>
|
||||||
<if test="paramKey != null and paramKey != ''"> and param_key = #{paramKey}</if>
|
<if test="paramKey != null and paramKey != ''"> and param_key = #{paramKey}</if>
|
||||||
<if test="dataType != null and dataType != ''"> and data_type = #{dataType}</if>
|
<if test="dataType != null and dataType != ''"> and data_type = #{dataType}</if>
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.solution.rule.domain.config;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* rule_item_task_type 批量查询行
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class RuleConfigTaskTypeRow implements Serializable {
|
||||||
|
|
||||||
|
private String ruleCode;
|
||||||
|
|
||||||
|
private String taskTypeCode;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.solution.rule.domain.config.vo;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@ApiModel("规则知识图谱边")
|
||||||
|
public class RuleGraphEdgeVO implements Serializable {
|
||||||
|
|
||||||
|
@ApiModelProperty("边 id")
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@ApiModelProperty("起点节点 id")
|
||||||
|
private String source;
|
||||||
|
|
||||||
|
@ApiModelProperty("终点节点 id")
|
||||||
|
private String target;
|
||||||
|
|
||||||
|
@ApiModelProperty("边类型")
|
||||||
|
private String edgeType;
|
||||||
|
|
||||||
|
@ApiModelProperty("边上展示文案")
|
||||||
|
private String label;
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
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.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@ApiModel("规则知识图谱节点")
|
||||||
|
public class RuleGraphNodeVO implements Serializable {
|
||||||
|
|
||||||
|
@ApiModelProperty("全局唯一节点 id")
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@ApiModelProperty("展示标题")
|
||||||
|
private String label;
|
||||||
|
|
||||||
|
@ApiModelProperty("节点类型:level/kind/module/rule/param/taskType")
|
||||||
|
private String nodeType;
|
||||||
|
|
||||||
|
@ApiModelProperty("附加属性(如 ruleCode、paramKey、字典编码等)")
|
||||||
|
private Map<String, Object> payload;
|
||||||
|
|
||||||
|
public Map<String, Object> safePayload() {
|
||||||
|
if (payload == null) {
|
||||||
|
payload = new HashMap<>();
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
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 RuleGraphVO implements Serializable {
|
||||||
|
|
||||||
|
@ApiModelProperty("节点列表")
|
||||||
|
private List<RuleGraphNodeVO> nodes;
|
||||||
|
|
||||||
|
@ApiModelProperty("边列表")
|
||||||
|
private List<RuleGraphEdgeVO> edges;
|
||||||
|
|
||||||
|
public List<RuleGraphNodeVO> safeNodes() {
|
||||||
|
if (nodes == null) {
|
||||||
|
nodes = new ArrayList<>();
|
||||||
|
}
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<RuleGraphEdgeVO> safeEdges() {
|
||||||
|
if (edges == null) {
|
||||||
|
edges = new ArrayList<>();
|
||||||
|
}
|
||||||
|
return edges;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package com.solution.rule.mapper;
|
|||||||
import com.solution.rule.domain.config.RuleConfig;
|
import com.solution.rule.domain.config.RuleConfig;
|
||||||
import com.solution.rule.domain.config.RuleConfigParam;
|
import com.solution.rule.domain.config.RuleConfigParam;
|
||||||
import com.solution.rule.domain.config.RuleConfigQuery;
|
import com.solution.rule.domain.config.RuleConfigQuery;
|
||||||
|
import com.solution.rule.domain.config.RuleConfigTaskTypeRow;
|
||||||
import com.solution.rule.domain.config.RuleDictItem;
|
import com.solution.rule.domain.config.RuleDictItem;
|
||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
@@ -24,12 +25,16 @@ public interface RuleConfigMapper {
|
|||||||
|
|
||||||
List<RuleConfigParam> selectParamsByRuleCode(@Param("ruleCode") String ruleCode);
|
List<RuleConfigParam> selectParamsByRuleCode(@Param("ruleCode") String ruleCode);
|
||||||
|
|
||||||
|
List<RuleConfigParam> selectParamsByRuleCodes(@Param("ruleCodes") List<String> ruleCodes);
|
||||||
|
|
||||||
int deleteParamsByRuleCodes(@Param("ruleCodes") String[] ruleCodes);
|
int deleteParamsByRuleCodes(@Param("ruleCodes") String[] ruleCodes);
|
||||||
|
|
||||||
int insertParamsBatch(@Param("params") List<RuleConfigParam> params);
|
int insertParamsBatch(@Param("params") List<RuleConfigParam> params);
|
||||||
|
|
||||||
List<String> selectTaskTypesByRuleCode(@Param("ruleCode") String ruleCode);
|
List<String> selectTaskTypesByRuleCode(@Param("ruleCode") String ruleCode);
|
||||||
|
|
||||||
|
List<RuleConfigTaskTypeRow> selectTaskTypesByRuleCodes(@Param("ruleCodes") List<String> ruleCodes);
|
||||||
|
|
||||||
int deleteTaskTypesByRuleCodes(@Param("ruleCodes") String[] ruleCodes);
|
int deleteTaskTypesByRuleCodes(@Param("ruleCodes") String[] ruleCodes);
|
||||||
|
|
||||||
int insertTaskTypesBatch(@Param("ruleCode") String ruleCode, @Param("taskTypes") List<String> taskTypes);
|
int insertTaskTypesBatch(@Param("ruleCode") String ruleCode, @Param("taskTypes") List<String> taskTypes);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.solution.rule.domain.config.RuleConfig;
|
|||||||
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.RuleGraphVO;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -25,4 +26,9 @@ public interface IRuleConfigService {
|
|||||||
Map<String, Object> loadEnabledGlobalParams();
|
Map<String, Object> loadEnabledGlobalParams();
|
||||||
|
|
||||||
List<RuleParamMeta> selectParamMetaList();
|
List<RuleParamMeta> selectParamMetaList();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据当前页规则主数据构建知识图谱(节点与边),参数与任务类型从库批量加载。
|
||||||
|
*/
|
||||||
|
RuleGraphVO buildKnowledgeGraph(List<RuleConfig> ruleConfigs);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,12 @@ import com.solution.common.constant.ExceptionConstants;
|
|||||||
import com.solution.rule.domain.config.RuleConfig;
|
import com.solution.rule.domain.config.RuleConfig;
|
||||||
import com.solution.rule.domain.config.RuleConfigParam;
|
import com.solution.rule.domain.config.RuleConfigParam;
|
||||||
import com.solution.rule.domain.config.RuleConfigQuery;
|
import com.solution.rule.domain.config.RuleConfigQuery;
|
||||||
|
import com.solution.rule.domain.config.RuleConfigTaskTypeRow;
|
||||||
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.RuleGraphEdgeVO;
|
||||||
|
import com.solution.rule.domain.config.vo.RuleGraphNodeVO;
|
||||||
|
import com.solution.rule.domain.config.vo.RuleGraphVO;
|
||||||
import com.solution.rule.mapper.RuleConfigMapper;
|
import com.solution.rule.mapper.RuleConfigMapper;
|
||||||
import com.solution.rule.service.IRuleConfigService;
|
import com.solution.rule.service.IRuleConfigService;
|
||||||
import com.solution.rule.service.RuleDrlSyncService;
|
import com.solution.rule.service.RuleDrlSyncService;
|
||||||
@@ -17,6 +21,7 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class RuleConfigServiceImpl implements IRuleConfigService {
|
public class RuleConfigServiceImpl implements IRuleConfigService {
|
||||||
@@ -24,6 +29,18 @@ public class RuleConfigServiceImpl implements IRuleConfigService {
|
|||||||
private static final Pattern RULE_SLOT_KEYS = Pattern.compile("^(blueRuleKeywords|redRuleKeywords|ruleScore)_\\d+$");
|
private static final Pattern RULE_SLOT_KEYS = Pattern.compile("^(blueRuleKeywords|redRuleKeywords|ruleScore)_\\d+$");
|
||||||
private static final boolean ALLOW_UNKNOWN_PARAM_KEY = false;
|
private static final boolean ALLOW_UNKNOWN_PARAM_KEY = false;
|
||||||
|
|
||||||
|
private static final Map<String, String> MODULE_LABELS;
|
||||||
|
|
||||||
|
static {
|
||||||
|
Map<String, String> m = new LinkedHashMap<>();
|
||||||
|
m.put("equipment", "装备匹配");
|
||||||
|
m.put("target", "目标分配");
|
||||||
|
m.put("position", "阵位部署");
|
||||||
|
m.put("track", "航迹生成");
|
||||||
|
m.put("group", "编组");
|
||||||
|
MODULE_LABELS = Collections.unmodifiableMap(m);
|
||||||
|
}
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private RuleConfigMapper ruleConfigMapper;
|
private RuleConfigMapper ruleConfigMapper;
|
||||||
@Autowired
|
@Autowired
|
||||||
@@ -119,6 +136,193 @@ public class RuleConfigServiceImpl implements IRuleConfigService {
|
|||||||
return new ArrayList<>(metaMap().values());
|
return new ArrayList<>(metaMap().values());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RuleGraphVO buildKnowledgeGraph(List<RuleConfig> ruleConfigs) {
|
||||||
|
RuleGraphVO graph = new RuleGraphVO();
|
||||||
|
List<RuleGraphNodeVO> nodes = graph.safeNodes();
|
||||||
|
List<RuleGraphEdgeVO> edges = graph.safeEdges();
|
||||||
|
if (CollUtil.isEmpty(ruleConfigs)) {
|
||||||
|
return graph;
|
||||||
|
}
|
||||||
|
List<String> ruleCodes = ruleConfigs.stream()
|
||||||
|
.map(RuleConfig::getRuleCode)
|
||||||
|
.filter(ObjectUtil::isNotEmpty)
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
List<RuleConfigParam> allParams = ruleCodes.isEmpty()
|
||||||
|
? Collections.emptyList()
|
||||||
|
: ruleConfigMapper.selectParamsByRuleCodes(ruleCodes);
|
||||||
|
List<RuleConfigTaskTypeRow> allTaskRows = ruleCodes.isEmpty()
|
||||||
|
? Collections.emptyList()
|
||||||
|
: ruleConfigMapper.selectTaskTypesByRuleCodes(ruleCodes);
|
||||||
|
|
||||||
|
Map<String, List<RuleConfigParam>> paramsByRule = allParams.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<>();
|
||||||
|
for (RuleConfigTaskTypeRow row : allTaskRows) {
|
||||||
|
if (row == null || ObjectUtil.isEmpty(row.getRuleCode()) || ObjectUtil.isEmpty(row.getTaskTypeCode())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
taskTypesByRule.computeIfAbsent(row.getRuleCode(), k -> new ArrayList<>()).add(row.getTaskTypeCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
List<RuleDictItem> levelDict = safeDict("level");
|
||||||
|
List<RuleDictItem> kindDict = safeDict("kind");
|
||||||
|
List<RuleDictItem> taskTypeDict = safeDict("task_type");
|
||||||
|
|
||||||
|
Map<String, RuleGraphNodeVO> nodeById = new LinkedHashMap<>();
|
||||||
|
Set<String> edgeIds = new LinkedHashSet<>();
|
||||||
|
|
||||||
|
for (RuleConfig rule : ruleConfigs) {
|
||||||
|
if (rule == null || ObjectUtil.isEmpty(rule.getRuleCode())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String ruleCode = rule.getRuleCode();
|
||||||
|
String levelCode = ObjectUtil.defaultIfBlank(rule.getLevelCode(), "unknown");
|
||||||
|
String kindCode = ObjectUtil.defaultIfBlank(rule.getKindCode(), "unknown");
|
||||||
|
String moduleCode = ObjectUtil.defaultIfBlank(rule.getModuleCode(), "unknown");
|
||||||
|
|
||||||
|
String levelId = "level:" + levelCode;
|
||||||
|
Map<String, Object> levelPayload = new LinkedHashMap<>();
|
||||||
|
levelPayload.put("dictType", "level");
|
||||||
|
levelPayload.put("dictCode", levelCode);
|
||||||
|
putNode(nodeById, levelId, node(levelId, dictLabel(levelDict, levelCode, levelCode), "level", levelPayload));
|
||||||
|
|
||||||
|
String kindId = "kind:" + levelCode + ":" + kindCode;
|
||||||
|
Map<String, Object> kindPayload = new LinkedHashMap<>();
|
||||||
|
kindPayload.put("levelCode", levelCode);
|
||||||
|
kindPayload.put("kindCode", kindCode);
|
||||||
|
putNode(nodeById, kindId, node(kindId, dictLabel(kindDict, kindCode, kindCode), "kind", kindPayload));
|
||||||
|
|
||||||
|
String moduleId = "module:" + moduleCode;
|
||||||
|
String moduleLabel = MODULE_LABELS.getOrDefault(moduleCode, moduleCode);
|
||||||
|
Map<String, Object> modulePayload = new LinkedHashMap<>();
|
||||||
|
modulePayload.put("moduleCode", moduleCode);
|
||||||
|
putNode(nodeById, moduleId, node(moduleId, moduleLabel, "module", modulePayload));
|
||||||
|
|
||||||
|
String ruleId = "rule:" + ruleCode;
|
||||||
|
String ruleLabel = ObjectUtil.defaultIfBlank(rule.getRuleName(), ruleCode);
|
||||||
|
Map<String, Object> rulePayload = new LinkedHashMap<>();
|
||||||
|
rulePayload.put("ruleCode", ruleCode);
|
||||||
|
rulePayload.put("priorityNo", rule.getPriorityNo());
|
||||||
|
rulePayload.put("moduleCode", moduleCode);
|
||||||
|
rulePayload.put("levelCode", levelCode);
|
||||||
|
rulePayload.put("kindCode", kindCode);
|
||||||
|
if (ObjectUtil.isNotEmpty(rule.getConditionExpr())) {
|
||||||
|
rulePayload.put("conditionExpr", rule.getConditionExpr());
|
||||||
|
}
|
||||||
|
if (ObjectUtil.isNotEmpty(rule.getActionExpr())) {
|
||||||
|
rulePayload.put("actionExpr", rule.getActionExpr());
|
||||||
|
}
|
||||||
|
RuleGraphNodeVO ruleNode = node(ruleId, ruleLabel, "rule", rulePayload);
|
||||||
|
putNode(nodeById, ruleId, ruleNode);
|
||||||
|
|
||||||
|
addEdge(edges, edgeIds, "belongs_level:" + ruleCode, ruleId, levelId, "rule_belongs_level", null);
|
||||||
|
addEdge(edges, edgeIds, "belongs_kind:" + ruleCode, ruleId, kindId, "rule_belongs_kind", null);
|
||||||
|
addEdge(edges, edgeIds, "has_module:" + ruleCode, ruleId, moduleId, "rule_has_module", null);
|
||||||
|
|
||||||
|
List<RuleConfigParam> plist = paramsByRule.getOrDefault(ruleCode, Collections.emptyList());
|
||||||
|
for (RuleConfigParam param : plist) {
|
||||||
|
if (param == null || ObjectUtil.isEmpty(param.getParamKey())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String pk = param.getParamKey();
|
||||||
|
String paramId = "param:" + ruleCode + ":" + pk;
|
||||||
|
String pLabel = ObjectUtil.defaultIfBlank(param.getParamName(), pk);
|
||||||
|
Map<String, Object> paramPayload = new LinkedHashMap<>();
|
||||||
|
paramPayload.put("ruleCode", ruleCode);
|
||||||
|
paramPayload.put("paramKey", pk);
|
||||||
|
paramPayload.put("paramVal", param.getParamVal());
|
||||||
|
paramPayload.put("valType", param.getValType());
|
||||||
|
RuleGraphNodeVO pNode = node(paramId, pLabel, "param", paramPayload);
|
||||||
|
putNode(nodeById, paramId, pNode);
|
||||||
|
addEdge(edges, edgeIds, "has_param:" + ruleCode + ":" + pk, ruleId, paramId, "rule_has_param", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> tlist = taskTypesByRule.getOrDefault(ruleCode, Collections.emptyList());
|
||||||
|
for (String tt : tlist) {
|
||||||
|
if (ObjectUtil.isEmpty(tt)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String taskId = "task:" + tt;
|
||||||
|
Map<String, Object> taskPayload = new LinkedHashMap<>();
|
||||||
|
taskPayload.put("taskTypeCode", tt);
|
||||||
|
putNode(nodeById, taskId, node(taskId, dictLabel(taskTypeDict, tt, tt), "taskType", taskPayload));
|
||||||
|
addEdge(edges, edgeIds, "applies_task:" + ruleCode + ":" + tt, ruleId, taskId, "rule_applies_task", null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Comparator<RuleConfig> byPriority = Comparator.comparing(RuleConfig::getPriorityNo,
|
||||||
|
Comparator.nullsLast(Integer::compareTo));
|
||||||
|
Map<String, List<RuleConfig>> execGroups = ruleConfigs.stream()
|
||||||
|
.filter(r -> r != null && ObjectUtil.isNotEmpty(r.getRuleCode()))
|
||||||
|
.collect(Collectors.groupingBy(
|
||||||
|
r -> ObjectUtil.defaultIfBlank(r.getLevelCode(), "") + "\0" + ObjectUtil.defaultIfBlank(r.getModuleCode(), ""),
|
||||||
|
LinkedHashMap::new,
|
||||||
|
Collectors.toList()));
|
||||||
|
for (List<RuleConfig> group : execGroups.values()) {
|
||||||
|
List<RuleConfig> sorted = new ArrayList<>(group);
|
||||||
|
sorted.sort(byPriority);
|
||||||
|
for (int i = 0; i < sorted.size() - 1; i++) {
|
||||||
|
String from = "rule:" + sorted.get(i).getRuleCode();
|
||||||
|
String to = "rule:" + sorted.get(i + 1).getRuleCode();
|
||||||
|
if (from.equals(to)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
addEdge(edges, edgeIds, "exec:" + sorted.get(i).getRuleCode() + "->" + sorted.get(i + 1).getRuleCode(),
|
||||||
|
from, to, "rule_exec_before", "执行顺序");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes.addAll(nodeById.values());
|
||||||
|
return graph;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<RuleDictItem> safeDict(String dictType) {
|
||||||
|
List<RuleDictItem> list = ruleConfigMapper.selectDictByType(dictType);
|
||||||
|
return list != null ? list : Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void putNode(Map<String, RuleGraphNodeVO> map, String id, RuleGraphNodeVO n) {
|
||||||
|
map.putIfAbsent(id, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RuleGraphNodeVO node(String id, String label, String nodeType, Map<String, Object> payload) {
|
||||||
|
RuleGraphNodeVO n = new RuleGraphNodeVO();
|
||||||
|
n.setId(id);
|
||||||
|
n.setLabel(label);
|
||||||
|
n.setNodeType(nodeType);
|
||||||
|
n.setPayload(payload);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addEdge(List<RuleGraphEdgeVO> edges, Set<String> edgeIds, String id,
|
||||||
|
String source, String target, String edgeType, String label) {
|
||||||
|
if (!edgeIds.add(id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
RuleGraphEdgeVO e = new RuleGraphEdgeVO();
|
||||||
|
e.setId(id);
|
||||||
|
e.setSource(source);
|
||||||
|
e.setTarget(target);
|
||||||
|
e.setEdgeType(edgeType);
|
||||||
|
e.setLabel(label);
|
||||||
|
edges.add(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String dictLabel(List<RuleDictItem> dict, String code, String fallback) {
|
||||||
|
if (CollUtil.isEmpty(dict) || ObjectUtil.isEmpty(code)) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
for (RuleDictItem item : dict) {
|
||||||
|
if (item != null && code.equals(item.getDictCode())) {
|
||||||
|
return ObjectUtil.defaultIfBlank(item.getDictName(), fallback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
private void saveChildren(RuleConfig ruleConfig) {
|
private void saveChildren(RuleConfig ruleConfig) {
|
||||||
if (CollUtil.isNotEmpty(ruleConfig.getParams())) {
|
if (CollUtil.isNotEmpty(ruleConfig.getParams())) {
|
||||||
Set<String> keys = new HashSet<>();
|
Set<String> keys = new HashSet<>();
|
||||||
|
|||||||
@@ -41,6 +41,11 @@
|
|||||||
<result property="remark" column="remark"/>
|
<result property="remark" column="remark"/>
|
||||||
</resultMap>
|
</resultMap>
|
||||||
|
|
||||||
|
<resultMap id="RuleConfigTaskTypeRowMap" type="com.solution.rule.domain.config.RuleConfigTaskTypeRow">
|
||||||
|
<result property="ruleCode" column="rule_code"/>
|
||||||
|
<result property="taskTypeCode" column="task_type_code"/>
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
<select id="selectRuleConfigList" resultMap="RuleConfigMap">
|
<select id="selectRuleConfigList" resultMap="RuleConfigMap">
|
||||||
SELECT id, rule_code, rule_name, level_code, kind_code, module_code, priority_no,
|
SELECT id, rule_code, rule_name, level_code, kind_code, module_code, priority_no,
|
||||||
condition_expr, action_expr, version_no, enabled, remark, created_at, updated_at
|
condition_expr, action_expr, version_no, enabled, remark, created_at, updated_at
|
||||||
@@ -123,6 +128,23 @@
|
|||||||
ORDER BY sort_no ASC, id ASC
|
ORDER BY sort_no ASC, id ASC
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="selectParamsByRuleCodes" resultMap="RuleConfigParamMap">
|
||||||
|
SELECT rule_code, param_key, param_val, val_type, param_name, sort_no, enabled, remark
|
||||||
|
FROM rule_item_param
|
||||||
|
<where>
|
||||||
|
<if test="ruleCodes != null and ruleCodes.size() > 0">
|
||||||
|
rule_code IN
|
||||||
|
<foreach collection="ruleCodes" item="c" open="(" separator="," close=")">
|
||||||
|
#{c}
|
||||||
|
</foreach>
|
||||||
|
</if>
|
||||||
|
<if test="ruleCodes == null or ruleCodes.size() == 0">
|
||||||
|
1 = 0
|
||||||
|
</if>
|
||||||
|
</where>
|
||||||
|
ORDER BY rule_code ASC, sort_no ASC, id ASC
|
||||||
|
</select>
|
||||||
|
|
||||||
<delete id="deleteParamsByRuleCodes">
|
<delete id="deleteParamsByRuleCodes">
|
||||||
DELETE FROM rule_item_param
|
DELETE FROM rule_item_param
|
||||||
WHERE rule_code IN
|
WHERE rule_code IN
|
||||||
@@ -148,6 +170,23 @@
|
|||||||
ORDER BY id ASC
|
ORDER BY id ASC
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="selectTaskTypesByRuleCodes" resultMap="RuleConfigTaskTypeRowMap">
|
||||||
|
SELECT rule_code, task_type_code
|
||||||
|
FROM rule_item_task_type
|
||||||
|
<where>
|
||||||
|
<if test="ruleCodes != null and ruleCodes.size() > 0">
|
||||||
|
rule_code IN
|
||||||
|
<foreach collection="ruleCodes" item="c" open="(" separator="," close=")">
|
||||||
|
#{c}
|
||||||
|
</foreach>
|
||||||
|
</if>
|
||||||
|
<if test="ruleCodes == null or ruleCodes.size() == 0">
|
||||||
|
1 = 0
|
||||||
|
</if>
|
||||||
|
</where>
|
||||||
|
ORDER BY rule_code ASC, id ASC
|
||||||
|
</select>
|
||||||
|
|
||||||
<delete id="deleteTaskTypesByRuleCodes">
|
<delete id="deleteTaskTypesByRuleCodes">
|
||||||
DELETE FROM rule_item_task_type
|
DELETE FROM rule_item_task_type
|
||||||
WHERE rule_code IN
|
WHERE rule_code IN
|
||||||
|
|||||||
880
modeler/package-lock.json
generated
880
modeler/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@antv/g6": "^5.0.49",
|
||||||
"@antv/x6": "^3.1.2",
|
"@antv/x6": "^3.1.2",
|
||||||
"@antv/x6-vue-shape": "^3.0.2",
|
"@antv/x6-vue-shape": "^3.0.2",
|
||||||
"ant-design-vue": "^4.2.6",
|
"ant-design-vue": "^4.2.6",
|
||||||
|
|||||||
@@ -1917,3 +1917,149 @@
|
|||||||
padding-left: 5px;
|
padding-left: 5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* rule-config 页:侧栏与主内容区视觉统一(仅带 .rule-config-sidebar 时生效) */
|
||||||
|
.rule-config-sidebar.ks-layout-sidebar {
|
||||||
|
background-color: #020a14;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(180deg, rgba(2, 10, 20, 0.94) 0%, rgba(2, 12, 24, 0.9) 50%, rgba(1, 8, 18, 0.92) 100%),
|
||||||
|
url('@/assets/icons/bg-fk.png');
|
||||||
|
background-size: 100% 100%, 100% 100%;
|
||||||
|
background-position: center, center;
|
||||||
|
border-inline-end: 1px solid rgba(55, 126, 173, 0.35);
|
||||||
|
|
||||||
|
.ks-sidebar-header {
|
||||||
|
background: url('@/assets/icons/bg-fk-title.png') center / 100% 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tree {
|
||||||
|
background: transparent;
|
||||||
|
color: #eee;
|
||||||
|
|
||||||
|
.ant-tree-node-content-wrapper {
|
||||||
|
color: #eee;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(9, 38, 75, 0.55);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tree-node-content-wrapper.ant-tree-node-selected {
|
||||||
|
background: rgba(17, 55, 126, 0.65);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tree-switcher {
|
||||||
|
color: #a2b1ba;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tree-title {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-pagination {
|
||||||
|
color: #a2b1ba;
|
||||||
|
|
||||||
|
.ant-pagination-item-link,
|
||||||
|
.ant-pagination-item a {
|
||||||
|
color: #a2b1ba;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-config-main-split {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: stretch;
|
||||||
|
overflow: hidden;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-config-graph-placeholder {
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px dashed rgba(55, 126, 173, 0.45);
|
||||||
|
margin: 8px 0 8px 8px;
|
||||||
|
border-radius: 2px;
|
||||||
|
color: #a2b1ba;
|
||||||
|
background: rgba(8, 29, 54, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-config-graph-placeholder--chart {
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: stretch;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-config-main-split-resizer {
|
||||||
|
flex: 0 0 6px;
|
||||||
|
width: 6px;
|
||||||
|
margin: 8px 0;
|
||||||
|
cursor: col-resize;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: rgba(55, 126, 173, 0.25);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(55, 126, 173, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-config-right-cluster {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-config-graph-placeholder__text {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-config-panel-toggle {
|
||||||
|
flex: 0 0 20px;
|
||||||
|
width: 20px;
|
||||||
|
margin: 8px 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 2px 0 0 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #eee;
|
||||||
|
background: rgba(9, 38, 75, 0.75);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(17, 55, 126, 0.85);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-config-right-panel {
|
||||||
|
flex: 0 0 min(480px, 42vw);
|
||||||
|
width: min(480px, 42vw);
|
||||||
|
min-width: 320px;
|
||||||
|
max-width: 55%;
|
||||||
|
transition: flex-basis 0.2s ease, width 0.2s ease, opacity 0.2s ease, min-width 0.2s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
border-left: 1px solid rgba(71, 95, 113, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-config-right-panel--collapsed {
|
||||||
|
flex: 0 0 0;
|
||||||
|
width: 0;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
border-left: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-config-right-panel__inner {
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 10px 10px 10px 4px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<Header />
|
<Header />
|
||||||
<a-layout class="ks-layout-body">
|
<a-layout class="ks-layout-body">
|
||||||
<slot name="body">
|
<slot name="body">
|
||||||
<a-layout-sider class="ks-layout-sidebar" width="300">
|
<a-layout-sider class="ks-layout-sidebar" :class="sidebarClass" width="300">
|
||||||
<slot name="sidebar" />
|
<slot name="sidebar" />
|
||||||
</a-layout-sider>
|
</a-layout-sider>
|
||||||
<a-layout-content class="ks-layout-main">
|
<a-layout-content class="ks-layout-main">
|
||||||
@@ -21,4 +21,9 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { Wrapper } from '@/components/wrapper';
|
import { Wrapper } from '@/components/wrapper';
|
||||||
import Header from './header.vue';
|
import Header from './header.vue';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
/** 追加在 ks-layout-sidebar 上的类名,用于单页侧栏皮肤 */
|
||||||
|
sidebarClass?: string;
|
||||||
|
}>();
|
||||||
</script>
|
</script>
|
||||||
341
modeler/src/views/decision/rule-config/RuleKnowledgeGraph.vue
Normal file
341
modeler/src/views/decision/rule-config/RuleKnowledgeGraph.vue
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
<!--
|
||||||
|
- This file is part of the kernelstudio package.
|
||||||
|
-
|
||||||
|
- (c) 2014-2026 zlin <admin@kernelstudio.com>
|
||||||
|
-
|
||||||
|
- For the full copyright and license information, please view the LICENSE file
|
||||||
|
- that was distributed with this source code.
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<div class="rule-knowledge-graph">
|
||||||
|
<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-group>
|
||||||
|
<span class="rule-knowledge-graph__hint">
|
||||||
|
简要:仅层级→种类→模块→规则;完整:含参数、任务类型与执行顺序边
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="errorMsg" class="rule-knowledge-graph__banner rule-knowledge-graph__banner--error">
|
||||||
|
{{ errorMsg }}
|
||||||
|
</div>
|
||||||
|
<div v-else-if="emptyHint" class="rule-knowledge-graph__banner">
|
||||||
|
{{ emptyHint }}
|
||||||
|
</div>
|
||||||
|
<div v-show="!errorMsg && !emptyHint" ref="hostRef" class="rule-knowledge-graph__host" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Graph } from '@antv/g6';
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
import { nextTick, onBeforeUnmount, ref, watch } from 'vue';
|
||||||
|
import { findRuleConfigGraph } from './api';
|
||||||
|
import type { RuleConfigRequest, RuleGraphEdge, RuleGraphNode, RuleGraphPayload } from './types';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
query: RuleConfigRequest,
|
||||||
|
refreshKey: number,
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const hostRef = ref<HTMLDivElement | null>(null);
|
||||||
|
const errorMsg = ref<string | null>(null);
|
||||||
|
const emptyHint = ref<string | null>(null);
|
||||||
|
const density = ref<'overview' | 'full'>('overview');
|
||||||
|
const lastPayload = ref<RuleGraphPayload | null>(null);
|
||||||
|
|
||||||
|
let graph: Graph | null = null;
|
||||||
|
let resizeObserver: ResizeObserver | null = null;
|
||||||
|
|
||||||
|
const NODE_COLORS: Record<string, string> = {
|
||||||
|
level: '#5B8FF9',
|
||||||
|
kind: '#61DDAA',
|
||||||
|
module: '#65789B',
|
||||||
|
rule: '#F6903D',
|
||||||
|
param: '#9270CA',
|
||||||
|
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,
|
||||||
|
data: {
|
||||||
|
label: n.label,
|
||||||
|
nodeType: n.nodeType,
|
||||||
|
payload: n.payload ?? {},
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
edges: payload.edges.map((e) => ({
|
||||||
|
id: e.id,
|
||||||
|
source: e.source,
|
||||||
|
target: e.target,
|
||||||
|
data: {
|
||||||
|
edgeType: e.edgeType ?? '',
|
||||||
|
label: e.label ?? '',
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
const disposeGraph = async () => {
|
||||||
|
resizeObserver?.disconnect();
|
||||||
|
resizeObserver = null;
|
||||||
|
if (graph) {
|
||||||
|
try {
|
||||||
|
graph.destroy();
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
graph = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildGraph = async (payload: RuleGraphPayload, mode: 'overview' | 'full') => {
|
||||||
|
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) {
|
||||||
|
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,
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
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,
|
||||||
|
labelWordWrap: true,
|
||||||
|
lineWidth: 1.5,
|
||||||
|
stroke: '#0d1f2c',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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)';
|
||||||
|
},
|
||||||
|
lineWidth: (d: { data?: { edgeType?: string } }) =>
|
||||||
|
(d.data?.edgeType === 'rule_exec_before' ? 2 : 1),
|
||||||
|
endArrow: mode !== 'overview',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],
|
||||||
|
});
|
||||||
|
await graph.render();
|
||||||
|
await graph.fitView();
|
||||||
|
resizeObserver = new ResizeObserver(() => {
|
||||||
|
if (!graph || !hostRef.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const w = Math.max(hostRef.value.clientWidth, 280);
|
||||||
|
const h = Math.max(hostRef.value.clientHeight, 240);
|
||||||
|
graph.setSize(w, h);
|
||||||
|
void graph.fitView();
|
||||||
|
});
|
||||||
|
resizeObserver.observe(el);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderFromCache = async () => {
|
||||||
|
if (!lastPayload.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!lastPayload.value.nodes?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await nextTick();
|
||||||
|
await buildGraph(lastPayload.value, density.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
errorMsg.value = null;
|
||||||
|
emptyHint.value = null;
|
||||||
|
await disposeGraph();
|
||||||
|
try {
|
||||||
|
const r = await findRuleConfigGraph({
|
||||||
|
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 ?? '加载知识图谱失败';
|
||||||
|
lastPayload.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const payload = r.data;
|
||||||
|
if (!payload.nodes?.length) {
|
||||||
|
emptyHint.value = '当前页无规则数据,图谱为空';
|
||||||
|
lastPayload.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastPayload.value = payload;
|
||||||
|
await renderFromCache();
|
||||||
|
} catch (e: unknown) {
|
||||||
|
const msg = e instanceof Error ? e.message : String(e);
|
||||||
|
errorMsg.value = `图谱请求失败:${msg}`;
|
||||||
|
lastPayload.value = null;
|
||||||
|
message.error(errorMsg.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.query.pageNum, props.query.pageSize, props.refreshKey],
|
||||||
|
() => {
|
||||||
|
void load();
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(density, () => {
|
||||||
|
void renderFromCache();
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
void disposeGraph();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.rule-knowledge-graph {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-knowledge-graph__toolbar {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0 2px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-knowledge-graph__hint {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #7a8d96;
|
||||||
|
line-height: 1.35;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-knowledge-graph__host {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 200px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-knowledge-graph__banner {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #a2b1ba;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-knowledge-graph__banner--error {
|
||||||
|
color: #ff9c9c;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
import { HttpRequestClient } from '@/utils/request';
|
import { HttpRequestClient } from '@/utils/request';
|
||||||
import type { ApiDataResponse, BasicResponse } from '@/types';
|
import type { ApiDataResponse, BasicResponse } from '@/types';
|
||||||
import type { RuleConfig, RuleConfigPageableResponse, RuleConfigRequest, RuleDictItem, RuleParamMeta } from './types';
|
import type { RuleConfig, RuleConfigPageableResponse, RuleConfigRequest, RuleDictItem, RuleGraphPayload, RuleParamMeta } from './types';
|
||||||
|
|
||||||
const req = HttpRequestClient.create<BasicResponse>({
|
const req = HttpRequestClient.create<BasicResponse>({
|
||||||
baseURL: '/api',
|
baseURL: '/api',
|
||||||
@@ -42,3 +42,7 @@ export const findRuleDictByType = (dictType: string): Promise<ApiDataResponse<Ru
|
|||||||
export const findRuleParamMeta = (): Promise<ApiDataResponse<RuleParamMeta[]>> => {
|
export const findRuleParamMeta = (): Promise<ApiDataResponse<RuleParamMeta[]>> => {
|
||||||
return req.get('/system/rule/config/param-meta');
|
return req.get('/system/rule/config/param-meta');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const findRuleConfigGraph = (query: Partial<RuleConfigRequest> = {}): Promise<ApiDataResponse<RuleGraphPayload>> => {
|
||||||
|
return req.get('/system/rule/config/graph', query);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<Layout>
|
<Layout sidebar-class="rule-config-sidebar">
|
||||||
<template #sidebar>
|
<template #sidebar>
|
||||||
<div class="ks-sidebar-header">
|
<div class="ks-sidebar-header">
|
||||||
<a-flex class="ks-sidebar-title">
|
<a-flex class="ks-sidebar-title">
|
||||||
@@ -39,9 +39,41 @@
|
|||||||
</a-space>
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="ks-scrollable" style="height: 80.5vh;overflow-y: auto;padding-right: 10px">
|
<div
|
||||||
<a-row :gutter="15">
|
ref="splitRootRef"
|
||||||
<a-col :span="16">
|
class="ks-scrollable rule-config-main-split"
|
||||||
|
style="height: 80.5vh;"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="rule-config-graph-placeholder rule-config-graph-placeholder--chart"
|
||||||
|
:style="{
|
||||||
|
flex: `0 0 ${graphPanePercent}%`,
|
||||||
|
minWidth: '140px',
|
||||||
|
maxWidth: '78%',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<RuleKnowledgeGraph :query="query" :refresh-key="graphRevision" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="rule-config-main-split-resizer"
|
||||||
|
title="拖动调节图谱与参数区占比"
|
||||||
|
@mousedown="onGraphSplitMouseDown"
|
||||||
|
/>
|
||||||
|
<div class="rule-config-right-cluster">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rule-config-panel-toggle"
|
||||||
|
:title="rightPanelExpanded ? '收起参数面板' : '展开参数面板'"
|
||||||
|
@click="rightPanelExpanded = !rightPanelExpanded"
|
||||||
|
>
|
||||||
|
<LeftOutlined v-if="rightPanelExpanded" />
|
||||||
|
<RightOutlined v-else />
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
class="rule-config-right-panel"
|
||||||
|
:class="{ 'rule-config-right-panel--collapsed': !rightPanelExpanded }"
|
||||||
|
>
|
||||||
|
<div class="rule-config-right-panel__inner">
|
||||||
<a-form
|
<a-form
|
||||||
ref="formRef"
|
ref="formRef"
|
||||||
:label-col="{ span: 6 }"
|
:label-col="{ span: 6 }"
|
||||||
@@ -138,8 +170,9 @@
|
|||||||
</a-space>
|
</a-space>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
</a-col>
|
</div>
|
||||||
</a-row>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a-card>
|
</a-card>
|
||||||
</div>
|
</div>
|
||||||
@@ -149,8 +182,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { nextTick, onMounted, ref } from 'vue';
|
import { nextTick, onMounted, ref } from 'vue';
|
||||||
import { type FormInstance, message } from 'ant-design-vue';
|
import { type FormInstance, message } from 'ant-design-vue';
|
||||||
import { MinusCircleOutlined, PlusCircleOutlined, PlusOutlined } from '@ant-design/icons-vue';
|
import { LeftOutlined, MinusCircleOutlined, PlusCircleOutlined, PlusOutlined, RightOutlined } from '@ant-design/icons-vue';
|
||||||
import Layout from '../layout.vue';
|
import Layout from '../layout.vue';
|
||||||
|
import RuleKnowledgeGraph from './RuleKnowledgeGraph.vue';
|
||||||
import { createRuleConfig, deleteRuleConfig, findRuleConfigByCode, findRuleConfigByQuery, findRuleParamMeta, updateRuleConfig } from './api';
|
import { createRuleConfig, deleteRuleConfig, findRuleConfigByCode, findRuleConfigByQuery, findRuleParamMeta, updateRuleConfig } from './api';
|
||||||
import type { RuleConfig, RuleConfigParam, RuleConfigRequest, RuleParamMeta } from './types';
|
import type { RuleConfig, RuleConfigParam, RuleConfigRequest, RuleParamMeta } from './types';
|
||||||
|
|
||||||
@@ -207,10 +241,45 @@ const paramMetaOptions = ref<{ label: string; value: string }[]>([]);
|
|||||||
|
|
||||||
const levelOptions = [
|
const levelOptions = [
|
||||||
{ code: 'task', name: '任务级' },
|
{ code: 'task', name: '任务级' },
|
||||||
{ code: 'platform', name: '平台级' },
|
|
||||||
{ code: 'action', name: '行动级' },
|
{ code: 'action', name: '行动级' },
|
||||||
|
{ code: 'platform', name: '平台级' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const rightPanelExpanded = ref(true);
|
||||||
|
|
||||||
|
const GRAPH_PANE_STORAGE_KEY = 'rule-config-graph-pane-percent';
|
||||||
|
const GRAPH_PANE_MIN = 18;
|
||||||
|
const GRAPH_PANE_MAX = 70;
|
||||||
|
const graphPanePercent = ref(32);
|
||||||
|
const splitRootRef = ref<HTMLElement | null>(null);
|
||||||
|
const graphRevision = ref(0);
|
||||||
|
|
||||||
|
const clampGraphPercent = (n: number) =>
|
||||||
|
Math.min(GRAPH_PANE_MAX, Math.max(GRAPH_PANE_MIN, Math.round(n)));
|
||||||
|
|
||||||
|
const onGraphSplitMouseDown = (e: MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const root = splitRootRef.value;
|
||||||
|
if (!root) return;
|
||||||
|
const rect = root.getBoundingClientRect();
|
||||||
|
const onMove = (ev: MouseEvent) => {
|
||||||
|
const x = ev.clientX - rect.left;
|
||||||
|
const p = (x / rect.width) * 100;
|
||||||
|
graphPanePercent.value = clampGraphPercent(p);
|
||||||
|
};
|
||||||
|
const onUp = () => {
|
||||||
|
window.removeEventListener('mousemove', onMove);
|
||||||
|
window.removeEventListener('mouseup', onUp);
|
||||||
|
document.body.style.removeProperty('cursor');
|
||||||
|
document.body.style.removeProperty('user-select');
|
||||||
|
sessionStorage.setItem(GRAPH_PANE_STORAGE_KEY, String(graphPanePercent.value));
|
||||||
|
};
|
||||||
|
document.body.style.cursor = 'col-resize';
|
||||||
|
document.body.style.userSelect = 'none';
|
||||||
|
window.addEventListener('mousemove', onMove);
|
||||||
|
window.addEventListener('mouseup', onUp);
|
||||||
|
};
|
||||||
|
|
||||||
const kindOptions = [
|
const kindOptions = [
|
||||||
{ code: 'select', name: '选择' },
|
{ code: 'select', name: '选择' },
|
||||||
{ code: 'assign', name: '分配' },
|
{ code: 'assign', name: '分配' },
|
||||||
@@ -233,12 +302,14 @@ const buildTreeData = () => {
|
|||||||
const params = detail?.params?.filter((item) => item.paramKey) ?? [];
|
const params = detail?.params?.filter((item) => item.paramKey) ?? [];
|
||||||
if (params.length === 0) return;
|
if (params.length === 0) return;
|
||||||
|
|
||||||
group[levelCode] = group[levelCode] ?? {};
|
const levelEntry = group[levelCode] ?? {};
|
||||||
group[levelCode][kindCode] = group[levelCode][kindCode] ?? [];
|
group[levelCode] = levelEntry;
|
||||||
|
const kindList = levelEntry[kindCode] ?? [];
|
||||||
|
levelEntry[kindCode] = kindList;
|
||||||
|
|
||||||
params.forEach((param, index) => {
|
params.forEach((param, index) => {
|
||||||
const paramTitle = param.paramName || param.paramKey || `参数${index + 1}`;
|
const paramTitle = param.paramName || param.paramKey || `参数${index + 1}`;
|
||||||
group[levelCode][kindCode].push({
|
kindList.push({
|
||||||
title: paramTitle,
|
title: paramTitle,
|
||||||
key: `param:${rule.ruleCode}:${param.paramKey || index}`,
|
key: `param:${rule.ruleCode}:${param.paramKey || index}`,
|
||||||
isLeaf: true,
|
isLeaf: true,
|
||||||
@@ -302,6 +373,7 @@ const load = async () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
buildTreeData();
|
buildTreeData();
|
||||||
|
graphRevision.value += 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadParamMeta = async () => {
|
const loadParamMeta = async () => {
|
||||||
@@ -476,8 +548,9 @@ const handleChange = async (page: number, pageSize: number) => {
|
|||||||
|
|
||||||
const handleAddParam = () => {
|
const handleAddParam = () => {
|
||||||
const row = defaultParam();
|
const row = defaultParam();
|
||||||
if (paramMetaOptions.value.length > 0) {
|
const firstOpt = paramMetaOptions.value[0];
|
||||||
row.paramKey = paramMetaOptions.value[0].value;
|
if (firstOpt) {
|
||||||
|
row.paramKey = firstOpt.value;
|
||||||
handleParamKeyChange(row);
|
handleParamKeyChange(row);
|
||||||
}
|
}
|
||||||
selectedRuleConfig.value.params.push(row);
|
selectedRuleConfig.value.params.push(row);
|
||||||
@@ -494,6 +567,13 @@ const handleMinusParam = (index: number) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
const raw = sessionStorage.getItem(GRAPH_PANE_STORAGE_KEY);
|
||||||
|
if (raw) {
|
||||||
|
const v = Number(raw);
|
||||||
|
if (!Number.isNaN(v)) {
|
||||||
|
graphPanePercent.value = clampGraphPercent(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
loadParamMeta().then(() => load());
|
loadParamMeta().then(() => load());
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -517,7 +597,10 @@ const handleParamKeyChange = (item: RuleConfigParam) => {
|
|||||||
item.valType = 'string';
|
item.valType = 'string';
|
||||||
if (meta.valueType === 'enum' && Array.isArray(meta.enumOptions) && meta.enumOptions.length > 0) {
|
if (meta.valueType === 'enum' && Array.isArray(meta.enumOptions) && meta.enumOptions.length > 0) {
|
||||||
if (!meta.enumOptions.includes(String(item.paramVal ?? ''))) {
|
if (!meta.enumOptions.includes(String(item.paramVal ?? ''))) {
|
||||||
item.paramVal = meta.enumOptions[0];
|
const first = meta.enumOptions[0];
|
||||||
|
if (first !== undefined) {
|
||||||
|
item.paramVal = first;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -86,3 +86,23 @@ export interface RuleConfigRequest extends Partial<RuleConfig> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface RuleConfigPageableResponse extends PageableResponse<RuleConfig> {}
|
export interface RuleConfigPageableResponse extends PageableResponse<RuleConfig> {}
|
||||||
|
|
||||||
|
export interface RuleGraphNode {
|
||||||
|
id: string,
|
||||||
|
label: string,
|
||||||
|
nodeType: string,
|
||||||
|
payload?: Record<string, unknown> | null,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RuleGraphEdge {
|
||||||
|
id: string,
|
||||||
|
source: string,
|
||||||
|
target: string,
|
||||||
|
edgeType?: string | null,
|
||||||
|
label?: string | null,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RuleGraphPayload {
|
||||||
|
nodes: RuleGraphNode[],
|
||||||
|
edges: RuleGraphEdge[],
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user