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

@@ -1930,6 +1930,13 @@
.ks-sidebar-header {
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 {
@@ -1973,6 +1980,9 @@
align-items: stretch;
overflow: hidden;
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 {
@@ -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 {
display: flex;
flex: 1 1 auto;
min-width: 0;
align-items: stretch;
background: transparent;
}
.rule-config-graph-placeholder__text {
@@ -2039,6 +2099,7 @@
}
.rule-config-right-panel {
position: relative;
flex: 0 0 min(480px, 42vw);
width: min(480px, 42vw);
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;
overflow: hidden;
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 {
@@ -2058,8 +2122,26 @@
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 {
height: 100%;
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.
*/
import { HttpRequestClient } from '@/utils/request';
import { HttpRequestClient, originalAxios } from '@/utils/request';
import type { ApiDataResponse, BasicResponse } from '@/types';
import type { AxiosResponse } from 'axios';
import type { RuleConfig, RuleConfigPageableResponse, RuleConfigRequest, RuleDictItem, RuleFourBlocksPayload, RuleGraphPayload, RuleParamMeta } from './types';
const req = HttpRequestClient.create<BasicResponse>({
@@ -50,3 +51,24 @@ export const findRuleConfigGraph = (query: Partial<RuleConfigRequest> = {}): Pro
export const findRuleFourBlocksGraph = (): Promise<ApiDataResponse<RuleFourBlocksPayload>> => {
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="text">规则聚合测试</span>
</a-flex>
<a-button class="ks-sidebar-add" size="small" @click="handleCreate">
<PlusOutlined />
新增
</a-button>
<a-space class="ks-sidebar-actions">
<a-button class="ks-sidebar-add" size="small" @click="handleCreate">
<PlusOutlined />
&#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>
<a-tree
@@ -72,7 +93,15 @@
<div
class="rule-config-right-panel"
: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">
<a-form
ref="formRef"
@@ -182,10 +211,10 @@
<script setup lang="ts">
import { nextTick, onMounted, ref } from '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 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';
const query = ref<RuleConfigRequest>({
@@ -225,6 +254,9 @@ const datasource = ref<RuleConfig[]>([]);
const datasourceTotal = ref<number>(0);
const selectedRuleConfig = ref<RuleConfig>(JSON.parse(JSON.stringify(defaultRuleConfig)));
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 selectedTreeKeys = 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_MIN = 18;
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 graphPanePercentBeforeFourBlocks = ref<number | null>(null);
const splitRootRef = ref<HTMLElement | null>(null);
const graphRevision = ref(0);
const rightPanelWidth = ref(480);
const clampGraphPercent = (n: number) =>
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') => {
if (mode === 'four-blocks') {
if (graphPanePercentBeforeFourBlocks.value === null) {
@@ -295,6 +337,28 @@ const onGraphSplitMouseDown = (e: MouseEvent) => {
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 = [
{ code: 'select', name: '选择' },
{ code: 'assign', name: '分配' },
@@ -581,6 +645,79 @@ const handleMinusParam = (index: number) => {
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(() => {
const raw = sessionStorage.getItem(GRAPH_PANE_STORAGE_KEY);
if (raw) {
@@ -589,6 +726,13 @@ onMounted(() => {
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());
});