Merge branch 'refs/heads/develop'

This commit is contained in:
MHW
2026-05-07 15:45:50 +08:00
7 changed files with 567 additions and 9 deletions

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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);
/** /**
* 根据当前页规则主数据构建知识图谱(节点与边),参数与任务类型从库批量加载。 * 根据当前页规则主数据构建知识图谱(节点与边),参数与任务类型从库批量加载。
*/ */

View File

@@ -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<>();

View File

@@ -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;
} }

View File

@@ -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);
};

View File

@@ -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> &#26032;&#22686;
</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());
}); });