Merge branch 'refs/heads/develop'
This commit is contained in:
@@ -5,8 +5,10 @@ import com.solution.common.core.controller.BaseController;
|
|||||||
import com.solution.common.core.domain.AjaxResult;
|
import com.solution.common.core.domain.AjaxResult;
|
||||||
import com.solution.common.core.page.TableDataInfo;
|
import com.solution.common.core.page.TableDataInfo;
|
||||||
import com.solution.common.enums.BusinessType;
|
import com.solution.common.enums.BusinessType;
|
||||||
|
import com.solution.common.utils.poi.ExcelUtil;
|
||||||
import com.solution.rule.domain.Rule;
|
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.RuleConfigExcelRow;
|
||||||
import com.solution.rule.domain.config.RuleConfigQuery;
|
import com.solution.rule.domain.config.RuleConfigQuery;
|
||||||
import com.solution.rule.domain.config.RuleParamMeta;
|
import com.solution.rule.domain.config.RuleParamMeta;
|
||||||
import com.solution.rule.domain.config.vo.RuleFourBlocksGraphVO;
|
import com.solution.rule.domain.config.vo.RuleFourBlocksGraphVO;
|
||||||
@@ -18,7 +20,9 @@ import io.swagger.annotations.ApiOperation;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Api("红蓝对抗规则管理")
|
@Api("红蓝对抗规则管理")
|
||||||
@@ -142,4 +146,26 @@ import java.util.List;
|
|||||||
List<RuleParamMeta> metas = ruleConfigService.selectParamMetaList();
|
List<RuleParamMeta> metas = ruleConfigService.selectParamMetaList();
|
||||||
return success(metas);
|
return success(metas);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('system:rule:query')")
|
||||||
|
@Log(title = "Rule Config", businessType = BusinessType.EXPORT)
|
||||||
|
@PostMapping("/config/export")
|
||||||
|
@ApiOperation("Export rule config")
|
||||||
|
public void exportConfig(HttpServletResponse response, @RequestBody(required = false) RuleConfigQuery query) {
|
||||||
|
List<RuleConfigExcelRow> rows = ruleConfigService.exportRuleConfigRows(query == null ? new RuleConfigQuery() : query);
|
||||||
|
ExcelUtil<RuleConfigExcelRow> util = new ExcelUtil<>(RuleConfigExcelRow.class);
|
||||||
|
util.exportExcel(response, rows, "rule-config");
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('system:rule:edit')")
|
||||||
|
@Log(title = "Rule Config", businessType = BusinessType.IMPORT)
|
||||||
|
@PostMapping("/config/importData")
|
||||||
|
@ApiOperation("Import rule config")
|
||||||
|
public AjaxResult importConfig(MultipartFile file,
|
||||||
|
@RequestParam(value = "updateSupport", defaultValue = "true") boolean updateSupport) throws Exception {
|
||||||
|
ExcelUtil<RuleConfigExcelRow> util = new ExcelUtil<>(RuleConfigExcelRow.class);
|
||||||
|
List<RuleConfigExcelRow> rows = util.importExcel(file.getInputStream());
|
||||||
|
String result = ruleConfigService.importRuleConfigRows(rows, updateSupport);
|
||||||
|
return success(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package com.solution.rule.domain.config;
|
||||||
|
|
||||||
|
import com.solution.common.annotation.Excel;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class RuleConfigExcelRow {
|
||||||
|
|
||||||
|
@Excel(name = "规则编码")
|
||||||
|
private String ruleCode;
|
||||||
|
|
||||||
|
@Excel(name = "规则名称")
|
||||||
|
private String ruleName;
|
||||||
|
|
||||||
|
@Excel(name = "层级编码")
|
||||||
|
private String levelCode;
|
||||||
|
|
||||||
|
@Excel(name = "类别编码")
|
||||||
|
private String kindCode;
|
||||||
|
|
||||||
|
@Excel(name = "模块编码")
|
||||||
|
private String moduleCode;
|
||||||
|
|
||||||
|
@Excel(name = "优先级", cellType = Excel.ColumnType.NUMERIC)
|
||||||
|
private Integer priorityNo;
|
||||||
|
|
||||||
|
@Excel(name = "条件表达式", width = 24)
|
||||||
|
private String conditionExpr;
|
||||||
|
|
||||||
|
@Excel(name = "动作表达式", width = 24)
|
||||||
|
private String actionExpr;
|
||||||
|
|
||||||
|
@Excel(name = "版本号", cellType = Excel.ColumnType.NUMERIC)
|
||||||
|
private Integer versionNo;
|
||||||
|
|
||||||
|
@Excel(name = "规则启用", cellType = Excel.ColumnType.NUMERIC, prompt = "1=启用,0=停用")
|
||||||
|
private Integer enabled;
|
||||||
|
|
||||||
|
@Excel(name = "备注", width = 20)
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
@Excel(name = "任务类型列表", width = 20, prompt = "多个任务类型用英文逗号分隔")
|
||||||
|
private String taskTypesCsv;
|
||||||
|
|
||||||
|
@Excel(name = "参数名称")
|
||||||
|
private String paramName;
|
||||||
|
|
||||||
|
@Excel(name = "参数键")
|
||||||
|
private String paramKey;
|
||||||
|
|
||||||
|
@Excel(name = "参数值", width = 24)
|
||||||
|
private String paramVal;
|
||||||
|
|
||||||
|
@Excel(name = "参数值类型")
|
||||||
|
private String valType;
|
||||||
|
|
||||||
|
@Excel(name = "参数排序", cellType = Excel.ColumnType.NUMERIC)
|
||||||
|
private Integer sortNo;
|
||||||
|
|
||||||
|
@Excel(name = "参数启用", cellType = Excel.ColumnType.NUMERIC, prompt = "1=启用,0=停用")
|
||||||
|
private Integer paramEnabled;
|
||||||
|
|
||||||
|
@Excel(name = "参数备注", width = 20)
|
||||||
|
private String paramRemark;
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.solution.rule.service;
|
package com.solution.rule.service;
|
||||||
|
|
||||||
import com.solution.rule.domain.config.RuleConfig;
|
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.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;
|
||||||
@@ -28,6 +29,10 @@ public interface IRuleConfigService {
|
|||||||
|
|
||||||
List<RuleParamMeta> selectParamMetaList();
|
List<RuleParamMeta> selectParamMetaList();
|
||||||
|
|
||||||
|
List<RuleConfigExcelRow> exportRuleConfigRows(RuleConfigQuery query);
|
||||||
|
|
||||||
|
String importRuleConfigRows(List<RuleConfigExcelRow> rows, boolean updateSupport);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据当前页规则主数据构建知识图谱(节点与边),参数与任务类型从库批量加载。
|
* 根据当前页规则主数据构建知识图谱(节点与边),参数与任务类型从库批量加载。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
|
|||||||
import cn.hutool.core.util.ObjectUtil;
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
import com.solution.common.constant.ExceptionConstants;
|
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.RuleConfigExcelRow;
|
||||||
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.RuleConfigTaskTypeRow;
|
||||||
@@ -131,6 +132,118 @@ public class RuleConfigServiceImpl implements IRuleConfigService {
|
|||||||
return new ArrayList<>(metaMap().values());
|
return new ArrayList<>(metaMap().values());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<RuleConfigExcelRow> exportRuleConfigRows(RuleConfigQuery query) {
|
||||||
|
List<RuleConfig> configs = selectRuleConfigList(query == null ? new RuleConfigQuery() : query);
|
||||||
|
List<RuleConfigExcelRow> rows = new ArrayList<>();
|
||||||
|
for (RuleConfig config : configs) {
|
||||||
|
if (config == null || ObjectUtil.isEmpty(config.getRuleCode())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
RuleConfig detail = selectRuleConfigByCode(config.getRuleCode());
|
||||||
|
if (detail == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<RuleConfigParam> params = CollUtil.isNotEmpty(detail.getParams()) ? detail.getParams() : Collections.singletonList(new RuleConfigParam());
|
||||||
|
String taskTypesCsv = joinTaskTypes(detail.getTaskTypes());
|
||||||
|
for (RuleConfigParam param : params) {
|
||||||
|
RuleConfigExcelRow row = new RuleConfigExcelRow();
|
||||||
|
row.setRuleCode(detail.getRuleCode());
|
||||||
|
row.setRuleName(detail.getRuleName());
|
||||||
|
row.setLevelCode(detail.getLevelCode());
|
||||||
|
row.setKindCode(detail.getKindCode());
|
||||||
|
row.setModuleCode(detail.getModuleCode());
|
||||||
|
row.setPriorityNo(detail.getPriorityNo());
|
||||||
|
row.setConditionExpr(detail.getConditionExpr());
|
||||||
|
row.setActionExpr(detail.getActionExpr());
|
||||||
|
row.setVersionNo(detail.getVersionNo());
|
||||||
|
row.setEnabled(detail.getEnabled());
|
||||||
|
row.setRemark(detail.getRemark());
|
||||||
|
row.setTaskTypesCsv(taskTypesCsv);
|
||||||
|
if (param != null) {
|
||||||
|
row.setParamName(param.getParamName());
|
||||||
|
row.setParamKey(param.getParamKey());
|
||||||
|
row.setParamVal(param.getParamVal());
|
||||||
|
row.setValType(param.getValType());
|
||||||
|
row.setSortNo(param.getSortNo());
|
||||||
|
row.setParamEnabled(param.getEnabled());
|
||||||
|
row.setParamRemark(param.getRemark());
|
||||||
|
}
|
||||||
|
rows.add(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public String importRuleConfigRows(List<RuleConfigExcelRow> rows, boolean updateSupport) {
|
||||||
|
if (CollUtil.isEmpty(rows)) {
|
||||||
|
throw new RuntimeException("Import data cannot be empty");
|
||||||
|
}
|
||||||
|
Map<String, RuleConfig> grouped = new LinkedHashMap<>();
|
||||||
|
for (RuleConfigExcelRow row : rows) {
|
||||||
|
if (row == null || isEmptyImportRow(row)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String ruleCode = trimmed(row.getRuleCode());
|
||||||
|
if (ObjectUtil.isEmpty(ruleCode)) {
|
||||||
|
throw new RuntimeException("Import failed: ruleCode is required");
|
||||||
|
}
|
||||||
|
RuleConfig config = grouped.computeIfAbsent(ruleCode, code -> {
|
||||||
|
RuleConfig rc = new RuleConfig();
|
||||||
|
rc.setRuleCode(code);
|
||||||
|
rc.setParams(new ArrayList<>());
|
||||||
|
rc.setTaskTypes(new ArrayList<>());
|
||||||
|
return rc;
|
||||||
|
});
|
||||||
|
mergeRuleBase(config, row);
|
||||||
|
if (ObjectUtil.isNotEmpty(trimmed(row.getParamKey()))) {
|
||||||
|
RuleConfigParam param = new RuleConfigParam();
|
||||||
|
param.setRuleCode(ruleCode);
|
||||||
|
param.setParamName(trimmed(row.getParamName()));
|
||||||
|
param.setParamKey(trimmed(row.getParamKey()));
|
||||||
|
param.setParamVal(trimmed(row.getParamVal()));
|
||||||
|
param.setValType(trimmed(row.getValType()));
|
||||||
|
param.setSortNo(row.getSortNo());
|
||||||
|
param.setEnabled(row.getParamEnabled());
|
||||||
|
param.setRemark(trimmed(row.getParamRemark()));
|
||||||
|
config.getParams().add(param);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (grouped.isEmpty()) {
|
||||||
|
throw new RuntimeException("Import data cannot be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
int inserted = 0;
|
||||||
|
int updated = 0;
|
||||||
|
for (RuleConfig config : grouped.values()) {
|
||||||
|
fillImportDefaults(config);
|
||||||
|
validateBase(config);
|
||||||
|
if (CollUtil.isEmpty(config.getParams())) {
|
||||||
|
throw new RuntimeException("Rule " + config.getRuleCode() + " must contain at least one param row");
|
||||||
|
}
|
||||||
|
boolean exists = ruleConfigMapper.countByRuleCode(config.getRuleCode()) > 0;
|
||||||
|
if (exists) {
|
||||||
|
if (!updateSupport) {
|
||||||
|
throw new RuntimeException("Rule already exists: " + config.getRuleCode());
|
||||||
|
}
|
||||||
|
ruleConfigMapper.updateRuleConfig(fillDefault(config));
|
||||||
|
String[] ruleCodes = {config.getRuleCode()};
|
||||||
|
ruleConfigMapper.deleteParamsByRuleCodes(ruleCodes);
|
||||||
|
ruleConfigMapper.deleteTaskTypesByRuleCodes(ruleCodes);
|
||||||
|
saveChildren(config);
|
||||||
|
updated++;
|
||||||
|
} else {
|
||||||
|
ruleConfigMapper.insertRuleConfig(fillDefault(config));
|
||||||
|
saveChildren(config);
|
||||||
|
inserted++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
syncDrlAfterCrud();
|
||||||
|
return "Import completed. inserted=" + inserted + ", updated=" + updated;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RuleGraphVO buildKnowledgeGraph(List<RuleConfig> ruleConfigs) {
|
public RuleGraphVO buildKnowledgeGraph(List<RuleConfig> ruleConfigs) {
|
||||||
RuleGraphVO graph = new RuleGraphVO();
|
RuleGraphVO graph = new RuleGraphVO();
|
||||||
@@ -595,6 +708,107 @@ public class RuleConfigServiceImpl implements IRuleConfigService {
|
|||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isEmptyImportRow(RuleConfigExcelRow row) {
|
||||||
|
return ObjectUtil.isAllEmpty(
|
||||||
|
trimmed(row.getRuleCode()),
|
||||||
|
trimmed(row.getRuleName()),
|
||||||
|
trimmed(row.getParamKey()),
|
||||||
|
trimmed(row.getParamVal()),
|
||||||
|
trimmed(row.getTaskTypesCsv()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void mergeRuleBase(RuleConfig config, RuleConfigExcelRow row) {
|
||||||
|
config.setRuleName(mergeStringField(config.getRuleCode(), "ruleName", config.getRuleName(), row.getRuleName()));
|
||||||
|
config.setLevelCode(mergeStringField(config.getRuleCode(), "levelCode", config.getLevelCode(), row.getLevelCode()));
|
||||||
|
config.setKindCode(mergeStringField(config.getRuleCode(), "kindCode", config.getKindCode(), row.getKindCode()));
|
||||||
|
config.setModuleCode(mergeStringField(config.getRuleCode(), "moduleCode", config.getModuleCode(), row.getModuleCode()));
|
||||||
|
config.setConditionExpr(mergeStringField(config.getRuleCode(), "conditionExpr", config.getConditionExpr(), row.getConditionExpr()));
|
||||||
|
config.setActionExpr(mergeStringField(config.getRuleCode(), "actionExpr", config.getActionExpr(), row.getActionExpr()));
|
||||||
|
config.setRemark(mergeStringField(config.getRuleCode(), "remark", config.getRemark(), row.getRemark()));
|
||||||
|
config.setPriorityNo(mergeIntegerField(config.getRuleCode(), "priorityNo", config.getPriorityNo(), row.getPriorityNo()));
|
||||||
|
config.setVersionNo(mergeIntegerField(config.getRuleCode(), "versionNo", config.getVersionNo(), row.getVersionNo()));
|
||||||
|
config.setEnabled(mergeIntegerField(config.getRuleCode(), "enabled", config.getEnabled(), row.getEnabled()));
|
||||||
|
String taskTypesCsv = trimmed(row.getTaskTypesCsv());
|
||||||
|
if (ObjectUtil.isNotEmpty(taskTypesCsv)) {
|
||||||
|
List<String> parsed = parseTaskTypes(taskTypesCsv);
|
||||||
|
if (CollUtil.isNotEmpty(config.getTaskTypes()) && !config.getTaskTypes().equals(parsed)) {
|
||||||
|
throw new RuntimeException("Rule " + config.getRuleCode() + " has conflicting taskTypesCsv values");
|
||||||
|
}
|
||||||
|
config.setTaskTypes(parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fillImportDefaults(RuleConfig config) {
|
||||||
|
if (config.getParams() == null) {
|
||||||
|
config.setParams(new ArrayList<>());
|
||||||
|
}
|
||||||
|
if (config.getTaskTypes() == null) {
|
||||||
|
config.setTaskTypes(new ArrayList<>());
|
||||||
|
}
|
||||||
|
int sortNo = 0;
|
||||||
|
for (RuleConfigParam param : config.getParams()) {
|
||||||
|
if (param.getSortNo() == null) {
|
||||||
|
param.setSortNo(sortNo);
|
||||||
|
}
|
||||||
|
if (param.getEnabled() == null) {
|
||||||
|
param.setEnabled(1);
|
||||||
|
}
|
||||||
|
sortNo++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String mergeStringField(String ruleCode, String fieldName, String currentValue, String newValue) {
|
||||||
|
String current = trimmed(currentValue);
|
||||||
|
String incoming = trimmed(newValue);
|
||||||
|
if (ObjectUtil.isEmpty(incoming)) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
if (ObjectUtil.isEmpty(current)) {
|
||||||
|
return incoming;
|
||||||
|
}
|
||||||
|
if (!Objects.equals(current, incoming)) {
|
||||||
|
throw new RuntimeException("Rule " + ruleCode + " has conflicting values for field " + fieldName);
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer mergeIntegerField(String ruleCode, String fieldName, Integer currentValue, Integer newValue) {
|
||||||
|
if (newValue == null) {
|
||||||
|
return currentValue;
|
||||||
|
}
|
||||||
|
if (currentValue == null) {
|
||||||
|
return newValue;
|
||||||
|
}
|
||||||
|
if (!Objects.equals(currentValue, newValue)) {
|
||||||
|
throw new RuntimeException("Rule " + ruleCode + " has conflicting values for field " + fieldName);
|
||||||
|
}
|
||||||
|
return currentValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> parseTaskTypes(String taskTypesCsv) {
|
||||||
|
if (ObjectUtil.isEmpty(taskTypesCsv)) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
return Arrays.stream(taskTypesCsv.split(","))
|
||||||
|
.map(this::trimmed)
|
||||||
|
.filter(ObjectUtil::isNotEmpty)
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String joinTaskTypes(List<String> taskTypes) {
|
||||||
|
if (CollUtil.isEmpty(taskTypes)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return taskTypes.stream()
|
||||||
|
.filter(ObjectUtil::isNotEmpty)
|
||||||
|
.collect(Collectors.joining(","));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String trimmed(String value) {
|
||||||
|
return value == null ? null : value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
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<>();
|
||||||
|
|||||||
@@ -1930,6 +1930,13 @@
|
|||||||
|
|
||||||
.ks-sidebar-header {
|
.ks-sidebar-header {
|
||||||
background: url('@/assets/icons/bg-fk-title.png') center / 100% 100%;
|
background: url('@/assets/icons/bg-fk-title.png') center / 100% 100%;
|
||||||
|
min-height: 104px;
|
||||||
|
padding: 8px 10px 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
line-height: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-tree {
|
.ant-tree {
|
||||||
@@ -1973,6 +1980,9 @@
|
|||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(1, 12, 28, 0.94) 0%, rgba(2, 16, 36, 0.92) 100%),
|
||||||
|
url('@/assets/icons/page-card-body.png') center / 100% 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rule-config-graph-placeholder {
|
.rule-config-graph-placeholder {
|
||||||
@@ -2008,11 +2018,61 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rule-config-sidebar .ks-sidebar-actions {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid rgba(71, 95, 113, 0.78);
|
||||||
|
border-radius: 2px;
|
||||||
|
background: linear-gradient(180deg, rgba(8, 29, 54, 0.88) 0%, rgba(6, 21, 40, 0.92) 100%);
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(18, 70, 120, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-config-sidebar .ks-sidebar-add {
|
||||||
|
position: static;
|
||||||
|
right: auto;
|
||||||
|
top: auto;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
.anticon {
|
||||||
|
display: block;
|
||||||
|
float: left;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-config-sidebar-action.ant-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 62px;
|
||||||
|
padding-inline: 10px;
|
||||||
|
border-color: #1c468b;
|
||||||
|
background: linear-gradient(0deg, #14223b 0, #1c468b 100%);
|
||||||
|
color: #eee;
|
||||||
|
|
||||||
|
.anticon {
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:disabled):hover,
|
||||||
|
&:hover,
|
||||||
|
&:active {
|
||||||
|
border-color: #166094;
|
||||||
|
background: linear-gradient(90deg, #3687bc 0%, #074375 100%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.rule-config-right-cluster {
|
.rule-config-right-cluster {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rule-config-graph-placeholder__text {
|
.rule-config-graph-placeholder__text {
|
||||||
@@ -2039,6 +2099,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rule-config-right-panel {
|
.rule-config-right-panel {
|
||||||
|
position: relative;
|
||||||
flex: 0 0 min(480px, 42vw);
|
flex: 0 0 min(480px, 42vw);
|
||||||
width: min(480px, 42vw);
|
width: min(480px, 42vw);
|
||||||
min-width: 320px;
|
min-width: 320px;
|
||||||
@@ -2046,6 +2107,9 @@
|
|||||||
transition: flex-basis 0.2s ease, width 0.2s ease, opacity 0.2s ease, min-width 0.2s ease;
|
transition: flex-basis 0.2s ease, width 0.2s ease, opacity 0.2s ease, min-width 0.2s ease;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-left: 1px solid rgba(71, 95, 113, 0.6);
|
border-left: 1px solid rgba(71, 95, 113, 0.6);
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(4, 24, 50, 0.96) 0%, rgba(3, 17, 37, 0.94) 100%);
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(18, 70, 120, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rule-config-right-panel--collapsed {
|
.rule-config-right-panel--collapsed {
|
||||||
@@ -2058,8 +2122,26 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rule-config-right-panel-edge {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 8px;
|
||||||
|
bottom: 8px;
|
||||||
|
width: 8px;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
cursor: col-resize;
|
||||||
|
background: linear-gradient(180deg, rgba(55, 126, 173, 0.15) 0%, rgba(55, 126, 173, 0.5) 50%, rgba(55, 126, 173, 0.15) 100%);
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: linear-gradient(180deg, rgba(55, 126, 173, 0.3) 0%, rgba(55, 126, 173, 0.72) 50%, rgba(55, 126, 173, 0.3) 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.rule-config-right-panel__inner {
|
.rule-config-right-panel__inner {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 10px 10px 10px 4px;
|
padding: 10px 10px 10px 12px;
|
||||||
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,9 @@
|
|||||||
* that was distributed with this source code.
|
* that was distributed with this source code.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { HttpRequestClient } from '@/utils/request';
|
import { HttpRequestClient, originalAxios } from '@/utils/request';
|
||||||
import type { ApiDataResponse, BasicResponse } from '@/types';
|
import type { ApiDataResponse, BasicResponse } from '@/types';
|
||||||
|
import type { AxiosResponse } from 'axios';
|
||||||
import type { RuleConfig, RuleConfigPageableResponse, RuleConfigRequest, RuleDictItem, RuleFourBlocksPayload, RuleGraphPayload, RuleParamMeta } from './types';
|
import type { RuleConfig, RuleConfigPageableResponse, RuleConfigRequest, RuleDictItem, RuleFourBlocksPayload, RuleGraphPayload, RuleParamMeta } from './types';
|
||||||
|
|
||||||
const req = HttpRequestClient.create<BasicResponse>({
|
const req = HttpRequestClient.create<BasicResponse>({
|
||||||
@@ -50,3 +51,24 @@ export const findRuleConfigGraph = (query: Partial<RuleConfigRequest> = {}): Pro
|
|||||||
export const findRuleFourBlocksGraph = (): Promise<ApiDataResponse<RuleFourBlocksPayload>> => {
|
export const findRuleFourBlocksGraph = (): Promise<ApiDataResponse<RuleFourBlocksPayload>> => {
|
||||||
return req.get('/system/rule/config/graph/four-blocks');
|
return req.get('/system/rule/config/graph/four-blocks');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const exportRuleConfig = (query: Partial<RuleConfigRequest> = {}): Promise<AxiosResponse<Blob>> => {
|
||||||
|
return originalAxios.post('/api/system/rule/config/export', query, {
|
||||||
|
responseType: 'blob',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json;charset=utf-8',
|
||||||
|
'Accept': 'application/octet-stream',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const importRuleConfig = (file: File, updateSupport: boolean = true): Promise<BasicResponse> => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
formData.append('updateSupport', String(updateSupport));
|
||||||
|
return originalAxios.post('/api/system/rule/config/importData', formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
}).then((response) => response.data as BasicResponse);
|
||||||
|
};
|
||||||
|
|||||||
@@ -6,10 +6,31 @@
|
|||||||
<span class="icon"></span>
|
<span class="icon"></span>
|
||||||
<span class="text">规则聚合测试</span>
|
<span class="text">规则聚合测试</span>
|
||||||
</a-flex>
|
</a-flex>
|
||||||
<a-button class="ks-sidebar-add" size="small" @click="handleCreate">
|
<a-space class="ks-sidebar-actions">
|
||||||
<PlusOutlined />
|
<a-button class="ks-sidebar-add" size="small" @click="handleCreate">
|
||||||
新增
|
<PlusOutlined />
|
||||||
</a-button>
|
新增
|
||||||
|
</a-button>
|
||||||
|
<a-tooltip title="导入 Excel">
|
||||||
|
<a-button class="rule-config-sidebar-action" size="small" :loading="importLoading" @click="handleImportTrigger">
|
||||||
|
<UploadOutlined />
|
||||||
|
导入
|
||||||
|
</a-button>
|
||||||
|
</a-tooltip>
|
||||||
|
<a-tooltip title="导出 Excel">
|
||||||
|
<a-button class="rule-config-sidebar-action" size="small" :loading="exportLoading" @click="handleExport">
|
||||||
|
<DownloadOutlined />
|
||||||
|
导出
|
||||||
|
</a-button>
|
||||||
|
</a-tooltip>
|
||||||
|
</a-space>
|
||||||
|
<input
|
||||||
|
ref="importInputRef"
|
||||||
|
type="file"
|
||||||
|
accept=".xls,.xlsx"
|
||||||
|
style="display: none;"
|
||||||
|
@change="handleImportChange"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a-tree
|
<a-tree
|
||||||
@@ -72,7 +93,15 @@
|
|||||||
<div
|
<div
|
||||||
class="rule-config-right-panel"
|
class="rule-config-right-panel"
|
||||||
:class="{ 'rule-config-right-panel--collapsed': !rightPanelExpanded }"
|
:class="{ 'rule-config-right-panel--collapsed': !rightPanelExpanded }"
|
||||||
|
:style="rightPanelExpanded ? { flexBasis: `${rightPanelWidth}px`, width: `${rightPanelWidth}px` } : undefined"
|
||||||
>
|
>
|
||||||
|
<button
|
||||||
|
v-if="rightPanelExpanded"
|
||||||
|
type="button"
|
||||||
|
class="rule-config-right-panel-edge"
|
||||||
|
title="左右拖动调整参数配置区域宽度"
|
||||||
|
@mousedown="onRightPanelResizeMouseDown"
|
||||||
|
/>
|
||||||
<div class="rule-config-right-panel__inner">
|
<div class="rule-config-right-panel__inner">
|
||||||
<a-form
|
<a-form
|
||||||
ref="formRef"
|
ref="formRef"
|
||||||
@@ -182,10 +211,10 @@
|
|||||||
<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 { LeftOutlined, MinusCircleOutlined, PlusCircleOutlined, PlusOutlined, RightOutlined } from '@ant-design/icons-vue';
|
import { DownloadOutlined, LeftOutlined, MinusCircleOutlined, PlusCircleOutlined, PlusOutlined, RightOutlined, UploadOutlined } from '@ant-design/icons-vue';
|
||||||
import Layout from '../layout.vue';
|
import Layout from '../layout.vue';
|
||||||
import RuleKnowledgeGraph from './RuleKnowledgeGraph.vue';
|
import RuleKnowledgeGraph from './RuleKnowledgeGraph.vue';
|
||||||
import { createRuleConfig, deleteRuleConfig, findRuleConfigByCode, findRuleConfigByQuery, findRuleParamMeta, updateRuleConfig } from './api';
|
import { createRuleConfig, deleteRuleConfig, exportRuleConfig, findRuleConfigByCode, findRuleConfigByQuery, findRuleParamMeta, importRuleConfig, updateRuleConfig } from './api';
|
||||||
import type { RuleConfig, RuleConfigParam, RuleConfigRequest, RuleParamMeta } from './types';
|
import type { RuleConfig, RuleConfigParam, RuleConfigRequest, RuleParamMeta } from './types';
|
||||||
|
|
||||||
const query = ref<RuleConfigRequest>({
|
const query = ref<RuleConfigRequest>({
|
||||||
@@ -225,6 +254,9 @@ const datasource = ref<RuleConfig[]>([]);
|
|||||||
const datasourceTotal = ref<number>(0);
|
const datasourceTotal = ref<number>(0);
|
||||||
const selectedRuleConfig = ref<RuleConfig>(JSON.parse(JSON.stringify(defaultRuleConfig)));
|
const selectedRuleConfig = ref<RuleConfig>(JSON.parse(JSON.stringify(defaultRuleConfig)));
|
||||||
const formRef = ref<FormInstance | null>(null);
|
const formRef = ref<FormInstance | null>(null);
|
||||||
|
const importInputRef = ref<HTMLInputElement | null>(null);
|
||||||
|
const importLoading = ref(false);
|
||||||
|
const exportLoading = ref(false);
|
||||||
const treeData = ref<any[]>([]);
|
const treeData = ref<any[]>([]);
|
||||||
const selectedTreeKeys = ref<string[]>([]);
|
const selectedTreeKeys = ref<string[]>([]);
|
||||||
const expandedTreeKeys = ref<string[]>([]);
|
const expandedTreeKeys = ref<string[]>([]);
|
||||||
@@ -250,14 +282,24 @@ const rightPanelExpanded = ref(true);
|
|||||||
const GRAPH_PANE_STORAGE_KEY = 'rule-config-graph-pane-percent';
|
const GRAPH_PANE_STORAGE_KEY = 'rule-config-graph-pane-percent';
|
||||||
const GRAPH_PANE_MIN = 18;
|
const GRAPH_PANE_MIN = 18;
|
||||||
const GRAPH_PANE_MAX = 70;
|
const GRAPH_PANE_MAX = 70;
|
||||||
|
const RIGHT_PANEL_WIDTH_STORAGE_KEY = 'rule-config-right-panel-width';
|
||||||
|
const RIGHT_PANEL_MIN = 360;
|
||||||
|
const RIGHT_PANEL_MAX = 960;
|
||||||
const graphPanePercent = ref(32);
|
const graphPanePercent = ref(32);
|
||||||
const graphPanePercentBeforeFourBlocks = ref<number | null>(null);
|
const graphPanePercentBeforeFourBlocks = ref<number | null>(null);
|
||||||
const splitRootRef = ref<HTMLElement | null>(null);
|
const splitRootRef = ref<HTMLElement | null>(null);
|
||||||
const graphRevision = ref(0);
|
const graphRevision = ref(0);
|
||||||
|
const rightPanelWidth = ref(480);
|
||||||
|
|
||||||
const clampGraphPercent = (n: number) =>
|
const clampGraphPercent = (n: number) =>
|
||||||
Math.min(GRAPH_PANE_MAX, Math.max(GRAPH_PANE_MIN, Math.round(n)));
|
Math.min(GRAPH_PANE_MAX, Math.max(GRAPH_PANE_MIN, Math.round(n)));
|
||||||
|
|
||||||
|
const clampRightPanelWidth = (n: number) => {
|
||||||
|
const rootWidth = splitRootRef.value?.getBoundingClientRect().width ?? window.innerWidth;
|
||||||
|
const dynamicMax = Math.min(RIGHT_PANEL_MAX, Math.max(420, Math.floor(rootWidth * 0.72)));
|
||||||
|
return Math.min(dynamicMax, Math.max(RIGHT_PANEL_MIN, Math.round(n)));
|
||||||
|
};
|
||||||
|
|
||||||
const onRuleGraphDensityChange = (mode: 'overview' | 'full' | 'four-blocks') => {
|
const onRuleGraphDensityChange = (mode: 'overview' | 'full' | 'four-blocks') => {
|
||||||
if (mode === 'four-blocks') {
|
if (mode === 'four-blocks') {
|
||||||
if (graphPanePercentBeforeFourBlocks.value === null) {
|
if (graphPanePercentBeforeFourBlocks.value === null) {
|
||||||
@@ -295,6 +337,28 @@ const onGraphSplitMouseDown = (e: MouseEvent) => {
|
|||||||
window.addEventListener('mouseup', onUp);
|
window.addEventListener('mouseup', onUp);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onRightPanelResizeMouseDown = (e: MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const root = splitRootRef.value;
|
||||||
|
if (!root) return;
|
||||||
|
const rect = root.getBoundingClientRect();
|
||||||
|
const onMove = (ev: MouseEvent) => {
|
||||||
|
const width = rect.right - ev.clientX;
|
||||||
|
rightPanelWidth.value = clampRightPanelWidth(width);
|
||||||
|
};
|
||||||
|
const onUp = () => {
|
||||||
|
window.removeEventListener('mousemove', onMove);
|
||||||
|
window.removeEventListener('mouseup', onUp);
|
||||||
|
document.body.style.removeProperty('cursor');
|
||||||
|
document.body.style.removeProperty('user-select');
|
||||||
|
sessionStorage.setItem(RIGHT_PANEL_WIDTH_STORAGE_KEY, String(rightPanelWidth.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: '分配' },
|
||||||
@@ -581,6 +645,79 @@ const handleMinusParam = (index: number) => {
|
|||||||
selectedRuleConfig.value.params = params;
|
selectedRuleConfig.value.params = params;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const extractFilename = (contentDisposition?: string) => {
|
||||||
|
if (!contentDisposition) {
|
||||||
|
return 'rule-config.xlsx';
|
||||||
|
}
|
||||||
|
const match = /filename\*=UTF-8''([^;]+)|filename="?([^";]+)"?/i.exec(contentDisposition);
|
||||||
|
const raw = match?.[1] ?? match?.[2] ?? 'rule-config.xlsx';
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(raw.replace(/\+/g, '%20'));
|
||||||
|
} catch {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadBlob = (blob: Blob, filename: string) => {
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const anchor = document.createElement('a');
|
||||||
|
anchor.href = url;
|
||||||
|
anchor.download = filename;
|
||||||
|
document.body.appendChild(anchor);
|
||||||
|
anchor.click();
|
||||||
|
anchor.remove();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExport = async () => {
|
||||||
|
exportLoading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await exportRuleConfig(query.value as Partial<RuleConfigRequest>);
|
||||||
|
const filename = extractFilename(response.headers['content-disposition']);
|
||||||
|
downloadBlob(response.data, filename);
|
||||||
|
message.success('导出成功');
|
||||||
|
} catch (err: any) {
|
||||||
|
message.error(err?.message ?? '导出失败');
|
||||||
|
} finally {
|
||||||
|
exportLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImportTrigger = () => {
|
||||||
|
if (importInputRef.value) {
|
||||||
|
importInputRef.value.value = '';
|
||||||
|
importInputRef.value.click();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImportChange = async (event: Event) => {
|
||||||
|
const input = event.target as HTMLInputElement | null;
|
||||||
|
const file = input?.files?.[0];
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!/\.(xls|xlsx)$/i.test(file.name)) {
|
||||||
|
message.error('请选择 Excel 文件');
|
||||||
|
if (input) {
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
importLoading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await importRuleConfig(file, true);
|
||||||
|
await load();
|
||||||
|
message.success(response.msg ?? '导入成功');
|
||||||
|
} catch (err: any) {
|
||||||
|
message.error(err?.message ?? '导入失败');
|
||||||
|
} finally {
|
||||||
|
importLoading.value = false;
|
||||||
|
if (input) {
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const raw = sessionStorage.getItem(GRAPH_PANE_STORAGE_KEY);
|
const raw = sessionStorage.getItem(GRAPH_PANE_STORAGE_KEY);
|
||||||
if (raw) {
|
if (raw) {
|
||||||
@@ -589,6 +726,13 @@ onMounted(() => {
|
|||||||
graphPanePercent.value = clampGraphPercent(v);
|
graphPanePercent.value = clampGraphPercent(v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const rightPanelRaw = sessionStorage.getItem(RIGHT_PANEL_WIDTH_STORAGE_KEY);
|
||||||
|
if (rightPanelRaw) {
|
||||||
|
const width = Number(rightPanelRaw);
|
||||||
|
if (!Number.isNaN(width)) {
|
||||||
|
rightPanelWidth.value = clampRightPanelWidth(width);
|
||||||
|
}
|
||||||
|
}
|
||||||
loadParamMeta().then(() => load());
|
loadParamMeta().then(() => load());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user