Merge branch 'refs/heads/develop'
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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 />
|
||||
新增
|
||||
</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());
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user