实体补充

This commit is contained in:
MHW
2026-04-20 10:31:09 +08:00
parent 931804555f
commit 71bb45f6a0
9 changed files with 1061 additions and 0 deletions

View 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>

View 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>