实体补充
This commit is contained in:
309
modeler/src/views/decision/rule-config/RuleBlockNeoGraph.vue
Normal file
309
modeler/src/views/decision/rule-config/RuleBlockNeoGraph.vue
Normal file
@@ -0,0 +1,309 @@
|
||||
<!--
|
||||
- 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-block-neo-graph">
|
||||
<div v-if="emptyHint" class="rule-block-neo-graph__empty">{{ emptyHint }}</div>
|
||||
<div v-show="!emptyHint" ref="hostRef" class="rule-block-neo-graph__host" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Graph } from '@antv/g6';
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import type { RuleGraphEdge, RuleGraphNode, RuleGraphPayload } from './types';
|
||||
|
||||
const props = defineProps<{
|
||||
payload: RuleGraphPayload,
|
||||
accent?: string,
|
||||
}>();
|
||||
|
||||
const hostRef = ref<HTMLDivElement | null>(null);
|
||||
const emptyHint = ref<string | null>(null);
|
||||
|
||||
let graph: Graph | null = null;
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
const toGraphData = (payload: RuleGraphPayload) => ({
|
||||
nodes: (payload.nodes ?? []).map((n: RuleGraphNode) => ({
|
||||
id: n.id,
|
||||
data: {
|
||||
label: n.label,
|
||||
nodeType: n.nodeType,
|
||||
payload: n.payload ?? {},
|
||||
},
|
||||
})),
|
||||
edges: (payload.edges ?? []).map((e: RuleGraphEdge) => ({
|
||||
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 nodeSize = (nodeType?: string) => {
|
||||
if (nodeType === 'drools_facade' || nodeType === 'logicCore') {
|
||||
return 36;
|
||||
}
|
||||
if (nodeType === 'compute_step') {
|
||||
return 30;
|
||||
}
|
||||
if (nodeType === 'rule_row') {
|
||||
return 26;
|
||||
}
|
||||
if (nodeType === 'param') {
|
||||
return 20;
|
||||
}
|
||||
if (nodeType === 'param_condition') {
|
||||
return 15;
|
||||
}
|
||||
if (nodeType === 'rule_output') {
|
||||
return 19;
|
||||
}
|
||||
return 22;
|
||||
};
|
||||
|
||||
const buildGraph = async () => {
|
||||
await disposeGraph();
|
||||
await nextTick();
|
||||
const el = hostRef.value;
|
||||
const raw = props.payload;
|
||||
if (!el || !raw?.nodes?.length) {
|
||||
emptyHint.value = raw && raw.nodes?.length === 0 ? '暂无图谱节点' : null;
|
||||
return;
|
||||
}
|
||||
emptyHint.value = null;
|
||||
|
||||
const accent = props.accent ?? '#5B8FF9';
|
||||
const fillFor = (nodeType?: string) => {
|
||||
if (nodeType === 'drools_facade' || nodeType === 'logicCore') {
|
||||
return accent;
|
||||
}
|
||||
if (nodeType === 'compute_step') {
|
||||
return '#ffe58f';
|
||||
}
|
||||
if (nodeType === 'rule_row') {
|
||||
return '#36cfc9';
|
||||
}
|
||||
if (nodeType === 'param') {
|
||||
return '#b37feb';
|
||||
}
|
||||
if (nodeType === 'param_condition') {
|
||||
return '#5cdbd3';
|
||||
}
|
||||
if (nodeType === 'rule_output') {
|
||||
return '#fa8c16';
|
||||
}
|
||||
return '#8c8c8c';
|
||||
};
|
||||
|
||||
const labelMaxFor = (nodeType?: string) => {
|
||||
if (nodeType === 'compute_step') {
|
||||
return 200;
|
||||
}
|
||||
if (nodeType === 'rule_output') {
|
||||
return 190;
|
||||
}
|
||||
if (nodeType === 'param_condition') {
|
||||
return 120;
|
||||
}
|
||||
return 140;
|
||||
};
|
||||
|
||||
const labelSizeFor = (nodeType?: string) => {
|
||||
if (nodeType === 'compute_step') {
|
||||
return 8;
|
||||
}
|
||||
if (nodeType === 'rule_output') {
|
||||
return 8;
|
||||
}
|
||||
if (nodeType === 'param_condition') {
|
||||
return 7;
|
||||
}
|
||||
if (nodeType === 'drools_facade' || nodeType === 'logicCore') {
|
||||
return 11;
|
||||
}
|
||||
return 9;
|
||||
};
|
||||
|
||||
const width = Math.max(el.clientWidth, 160);
|
||||
const height = Math.max(el.clientHeight, 260);
|
||||
|
||||
const data = toGraphData(raw);
|
||||
|
||||
const useRadial = raw.layoutHint === 'radial_hub' && !!raw.focusNodeId;
|
||||
const layoutOpts = useRadial
|
||||
? {
|
||||
type: 'radial' as const,
|
||||
focusNode: raw.focusNodeId,
|
||||
unitRadius: 88,
|
||||
preventOverlap: true,
|
||||
}
|
||||
: {
|
||||
type: 'd3-force' as const,
|
||||
};
|
||||
|
||||
graph = new Graph({
|
||||
container: el,
|
||||
width,
|
||||
height,
|
||||
data,
|
||||
layout: layoutOpts,
|
||||
node: {
|
||||
style: {
|
||||
size: (d: { data?: { nodeType?: string } }) => nodeSize(d.data?.nodeType),
|
||||
fill: (d: { data?: { nodeType?: string } }) => fillFor(d.data?.nodeType),
|
||||
labelText: (d: { data?: { label?: string }; id: string }) => d.data?.label ?? String(d.id),
|
||||
labelFill: (d: { data?: { nodeType?: string } }) => {
|
||||
const t = d.data?.nodeType;
|
||||
if (t === 'compute_step') {
|
||||
return '#3d2f00';
|
||||
}
|
||||
if (t === 'rule_output') {
|
||||
return '#2b1604';
|
||||
}
|
||||
if (t === 'param_condition') {
|
||||
return '#06302b';
|
||||
}
|
||||
return '#e8f4f8';
|
||||
},
|
||||
labelFontSize: (d: { data?: { nodeType?: string } }) => labelSizeFor(d.data?.nodeType),
|
||||
labelMaxWidth: (d: { data?: { nodeType?: string } }) => labelMaxFor(d.data?.nodeType),
|
||||
labelWordWrap: true,
|
||||
lineWidth: 1.2,
|
||||
stroke: '#050d14',
|
||||
},
|
||||
},
|
||||
edge: {
|
||||
style: {
|
||||
stroke: (d: { data?: { edgeType?: string } }) => {
|
||||
const t = d.data?.edgeType;
|
||||
if (t === 'compute_flow') {
|
||||
return 'rgba(149, 222, 100, 0.85)';
|
||||
}
|
||||
if (t === 'rule_priority_next') {
|
||||
return '#faad14';
|
||||
}
|
||||
if (t === 'drools_contains') {
|
||||
return 'rgba(91, 143, 249, 0.75)';
|
||||
}
|
||||
if (t === 'rule_has_param') {
|
||||
return 'rgba(179, 127, 235, 0.55)';
|
||||
}
|
||||
if (t === 'rule_produces') {
|
||||
return 'rgba(250, 173, 20, 0.75)';
|
||||
}
|
||||
if (t === 'condition_applies') {
|
||||
return 'rgba(92, 219, 211, 0.65)';
|
||||
}
|
||||
return 'rgba(150, 175, 190, 0.45)';
|
||||
},
|
||||
lineWidth: (d: { data?: { edgeType?: string } }) => {
|
||||
const t = d.data?.edgeType;
|
||||
if (t === 'rule_priority_next' || t === 'compute_flow') {
|
||||
return 2;
|
||||
}
|
||||
if (t === 'rule_produces') {
|
||||
return 1.5;
|
||||
}
|
||||
if (t === 'condition_applies') {
|
||||
return 1.2;
|
||||
}
|
||||
return 1;
|
||||
},
|
||||
endArrow: true,
|
||||
labelText: (d: { data?: { label?: string } }) => {
|
||||
const s = d.data?.label;
|
||||
return s && String(s).length > 0 ? String(s) : '';
|
||||
},
|
||||
labelFill: '#9fb8c4',
|
||||
labelFontSize: 9,
|
||||
},
|
||||
},
|
||||
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, 160);
|
||||
const h = Math.max(hostRef.value.clientHeight, 260);
|
||||
graph.setSize(w, h);
|
||||
void graph.fitView();
|
||||
});
|
||||
resizeObserver.observe(el);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => [props.payload, props.accent],
|
||||
() => {
|
||||
void buildGraph();
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
void buildGraph();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
void disposeGraph();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.rule-block-neo-graph {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.rule-block-neo-graph__host {
|
||||
flex: 1;
|
||||
min-height: 260px;
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
background: #0a1620;
|
||||
border: 1px solid rgba(80, 120, 150, 0.35);
|
||||
}
|
||||
|
||||
.rule-block-neo-graph__empty {
|
||||
flex: 1;
|
||||
min-height: 260px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
color: #7a8d96;
|
||||
border-radius: 8px;
|
||||
background: #0a1620;
|
||||
border: 1px dashed rgba(80, 120, 150, 0.35);
|
||||
}
|
||||
</style>
|
||||
294
modeler/src/views/decision/rule-config/RuleFourBlocksPanel.vue
Normal file
294
modeler/src/views/decision/rule-config/RuleFourBlocksPanel.vue
Normal file
@@ -0,0 +1,294 @@
|
||||
<!--
|
||||
- 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-four-blocks">
|
||||
<div class="rule-four-blocks__toolbar">
|
||||
<a-popover placement="bottomLeft" trigger="click" overlay-class-name="rule-four-blocks-snapshot-popover">
|
||||
<template #content>
|
||||
<pre class="rule-four-blocks__popover-json">{{ previewJson }}</pre>
|
||||
</template>
|
||||
<a-button type="link" size="small" class="rule-four-blocks__snapshot-btn">
|
||||
全局参数快照({{ previewEntryCount }})
|
||||
</a-button>
|
||||
</a-popover>
|
||||
</div>
|
||||
<div v-if="errorMsg" class="rule-four-blocks__banner rule-four-blocks__banner--error">
|
||||
{{ errorMsg }}
|
||||
</div>
|
||||
<div v-else-if="emptyHint" class="rule-four-blocks__banner">
|
||||
{{ emptyHint }}
|
||||
</div>
|
||||
<div v-else-if="payload" class="rule-four-blocks__grid">
|
||||
<div
|
||||
v-for="block in payload.blocks"
|
||||
:key="block.blockId"
|
||||
class="rule-four-blocks__pane"
|
||||
:class="{ 'rule-four-blocks__pane--fullscreen': isPaneFullscreen(block.blockId) }"
|
||||
:ref="(el) => registerPaneRef(block.blockId, el)"
|
||||
>
|
||||
<div class="rule-four-blocks__pane-bar">
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
class="rule-four-blocks__pane-fs-btn"
|
||||
:title="isPaneFullscreen(block.blockId) ? '退出本格全屏' : '本格全屏'"
|
||||
@click="togglePaneFullscreen(block.blockId)"
|
||||
>
|
||||
<template #icon>
|
||||
<FullscreenExitOutlined v-if="isPaneFullscreen(block.blockId)" />
|
||||
<FullscreenOutlined v-else />
|
||||
</template>
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="rule-four-blocks__pane-body">
|
||||
<RuleBlockNeoGraph
|
||||
class="rule-four-blocks__cell"
|
||||
:payload="block.graph"
|
||||
:accent="blockColor(block.blockId)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FullscreenExitOutlined, FullscreenOutlined } from '@ant-design/icons-vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import { findRuleFourBlocksGraph } from './api';
|
||||
import RuleBlockNeoGraph from './RuleBlockNeoGraph.vue';
|
||||
import type { RuleFourBlocksPayload } from './types';
|
||||
|
||||
const props = defineProps<{
|
||||
refreshKey: number,
|
||||
}>();
|
||||
|
||||
const payload = ref<RuleFourBlocksPayload | null>(null);
|
||||
const errorMsg = ref<string | null>(null);
|
||||
const emptyHint = ref<string | null>(null);
|
||||
|
||||
const paneRefs = new Map<string, HTMLElement>();
|
||||
const paneFsTick = ref(0);
|
||||
|
||||
const BLOCK_COLORS: Record<string, string> = {
|
||||
equipment: '#5B8FF9',
|
||||
target: '#F6903D',
|
||||
position: '#61DDAA',
|
||||
track: '#9270CA',
|
||||
};
|
||||
|
||||
const blockColor = (blockId: string) => BLOCK_COLORS[blockId] ?? '#65789B';
|
||||
|
||||
const previewEntryCount = computed(() => {
|
||||
const p = payload.value?.globalParamsPreview;
|
||||
return p ? Object.keys(p).length : 0;
|
||||
});
|
||||
|
||||
const previewJson = computed(() => {
|
||||
const p = payload.value?.globalParamsPreview;
|
||||
if (!p) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(p, null, 2);
|
||||
} catch {
|
||||
return String(p);
|
||||
}
|
||||
});
|
||||
|
||||
const registerPaneRef = (blockId: string, el: unknown) => {
|
||||
if (el == null) {
|
||||
paneRefs.delete(blockId);
|
||||
return;
|
||||
}
|
||||
if (el instanceof HTMLElement) {
|
||||
paneRefs.set(blockId, el);
|
||||
}
|
||||
};
|
||||
|
||||
const isPaneFullscreen = (blockId: string) => {
|
||||
void paneFsTick.value;
|
||||
const node = paneRefs.get(blockId);
|
||||
return Boolean(node && document.fullscreenElement === node);
|
||||
};
|
||||
|
||||
const onDocumentFullscreenChange = () => {
|
||||
paneFsTick.value += 1;
|
||||
void nextTick(() => {
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
window.setTimeout(() => {
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
}, 120);
|
||||
});
|
||||
};
|
||||
|
||||
const togglePaneFullscreen = async (blockId: string) => {
|
||||
const el = paneRefs.get(blockId);
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (document.fullscreenElement === el) {
|
||||
await document.exitFullscreen();
|
||||
} else {
|
||||
await el.requestFullscreen();
|
||||
}
|
||||
} catch {
|
||||
message.warning('无法切换本格全屏,请重试或使用 Chrome / Edge');
|
||||
}
|
||||
};
|
||||
|
||||
const load = async () => {
|
||||
errorMsg.value = null;
|
||||
emptyHint.value = null;
|
||||
payload.value = null;
|
||||
try {
|
||||
const r = await findRuleFourBlocksGraph();
|
||||
if (r.code !== 200 || !r.data) {
|
||||
errorMsg.value = r.msg ?? '加载四块图谱失败';
|
||||
return;
|
||||
}
|
||||
const data = r.data;
|
||||
if (!data.blocks?.length) {
|
||||
emptyHint.value = '暂无四块图谱数据';
|
||||
return;
|
||||
}
|
||||
payload.value = data;
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
errorMsg.value = `四块图谱请求失败:${msg}`;
|
||||
message.error(errorMsg.value);
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.refreshKey,
|
||||
() => {
|
||||
void load();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('fullscreenchange', onDocumentFullscreenChange);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('fullscreenchange', onDocumentFullscreenChange);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.rule-four-blocks {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.rule-four-blocks__toolbar {
|
||||
flex: 0 0 auto;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.rule-four-blocks__snapshot-btn {
|
||||
padding-left: 0;
|
||||
font-size: 11px;
|
||||
color: #8fafbd;
|
||||
}
|
||||
|
||||
.rule-four-blocks__banner {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px;
|
||||
font-size: 13px;
|
||||
color: #a2b1ba;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.rule-four-blocks__banner--error {
|
||||
color: #ff9c9c;
|
||||
}
|
||||
|
||||
.rule-four-blocks__grid {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rule-four-blocks__pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: max(0px, calc(50vh - 56px));
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(80, 120, 150, 0.25);
|
||||
background: rgba(8, 20, 30, 0.5);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.rule-four-blocks__pane--fullscreen {
|
||||
border-color: transparent;
|
||||
background: #0d1f2c;
|
||||
}
|
||||
|
||||
.rule-four-blocks__pane-bar {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
padding: 2px 4px 0;
|
||||
}
|
||||
|
||||
.rule-four-blocks__pane-fs-btn {
|
||||
color: #8fafbd;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.rule-four-blocks__pane-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 6px 6px;
|
||||
}
|
||||
|
||||
.rule-four-blocks__cell {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.rule-four-blocks-snapshot-popover {
|
||||
.ant-popover-inner-content {
|
||||
max-width: min(520px, 90vw);
|
||||
max-height: 70vh;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.rule-four-blocks__popover-json {
|
||||
margin: 0;
|
||||
font-size: 10px;
|
||||
line-height: 1.35;
|
||||
color: #c9dfe8;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user