|
|
@@ -13,26 +13,6 @@
|
|
|
</div>
|
|
|
|
|
|
<div class="control-tools-box">
|
|
|
- <div class="custom-atlas-tools-container">
|
|
|
- <ul>
|
|
|
- <Tooltip
|
|
|
- v-for="item in analysisToolbarItems"
|
|
|
- :key="item.key"
|
|
|
- :title="item.tipsContent ? `${item.label}: ${item.tipsContent}` : item.label"
|
|
|
- >
|
|
|
- <li
|
|
|
- class="tool-item"
|
|
|
- :class="{ 'gap-item': item.gap, 'is-active': isToolbarItemActive(item) }"
|
|
|
- @click="handleToolbarClick(item)"
|
|
|
- >
|
|
|
- <span class="tool-item-icon">
|
|
|
- <component :is="item.icon" class="icon-style" />
|
|
|
- </span>
|
|
|
- </li>
|
|
|
- </Tooltip>
|
|
|
- </ul>
|
|
|
- </div>
|
|
|
-
|
|
|
<div v-if="activePanel" class="tool-panel">
|
|
|
<div class="tool-panel__header">
|
|
|
<div class="tool-panel__title">{{ activePanelLabel }}</div>
|
|
|
@@ -173,6 +153,27 @@
|
|
|
</div>
|
|
|
|
|
|
<div class="canvas-toolbar">
|
|
|
+ <Tooltip title="查找">
|
|
|
+ <Button
|
|
|
+ type="text"
|
|
|
+ class="canvas-toolbar__button"
|
|
|
+ :class="{ 'is-active': activePanel === 'find' }"
|
|
|
+ @click="togglePanel('find')"
|
|
|
+ >
|
|
|
+ <SearchOutlined />
|
|
|
+ </Button>
|
|
|
+ </Tooltip>
|
|
|
+ <Tooltip title="过滤">
|
|
|
+ <Button
|
|
|
+ type="text"
|
|
|
+ class="canvas-toolbar__button"
|
|
|
+ :class="{ 'is-active': activePanel === 'filter' }"
|
|
|
+ @click="togglePanel('filter')"
|
|
|
+ >
|
|
|
+ <FilterOutlined />
|
|
|
+ </Button>
|
|
|
+ </Tooltip>
|
|
|
+ <span class="canvas-toolbar__divider"></span>
|
|
|
<Tooltip title="缩小">
|
|
|
<Button type="text" class="canvas-toolbar__button" @click="zoomOut">
|
|
|
<MinusOutlined />
|
|
|
@@ -194,6 +195,16 @@
|
|
|
</Button>
|
|
|
</Tooltip>
|
|
|
<span class="canvas-toolbar__divider"></span>
|
|
|
+ <Tooltip title="扩散关系">
|
|
|
+ <Button type="text" class="canvas-toolbar__button" @click="selectNeighborElements">
|
|
|
+ <ShareAltOutlined />
|
|
|
+ </Button>
|
|
|
+ </Tooltip>
|
|
|
+ <Tooltip title="框选">
|
|
|
+ <Button type="text" class="canvas-toolbar__button" @click="showBoxSelectTip">
|
|
|
+ <BorderOutlined />
|
|
|
+ </Button>
|
|
|
+ </Tooltip>
|
|
|
<Tooltip title="重新布局">
|
|
|
<Button type="text" class="canvas-toolbar__button" @click="relayout">
|
|
|
<ApartmentOutlined />
|
|
|
@@ -340,27 +351,9 @@
|
|
|
const FILTER_OPERATORS = ['>', '>=', '=', '<', '<='] as const;
|
|
|
const filterOperators = FILTER_OPERATORS;
|
|
|
|
|
|
- interface AnalysisToolbarItem {
|
|
|
- key: 'find' | 'filter' | 'selectNeighbor' | 'boxSelect' | 'forceLayout' | 'fullScreen';
|
|
|
- label: string;
|
|
|
- icon: any;
|
|
|
- tipsContent?: string;
|
|
|
- panelKey?: 'find' | 'filter';
|
|
|
- gap?: boolean;
|
|
|
- }
|
|
|
-
|
|
|
- /** 只读版工具条:保留原页面的查找/过滤/扩散关系/框选/智能布局/全屏,去掉编辑类按钮 */
|
|
|
- const analysisToolbarItems: AnalysisToolbarItem[] = [
|
|
|
- { key: 'find', label: '查找', icon: SearchOutlined, panelKey: 'find' },
|
|
|
- { key: 'filter', label: '过滤', icon: FilterOutlined, panelKey: 'filter' },
|
|
|
- { key: 'selectNeighbor', label: '扩散关系', icon: ShareAltOutlined, tipsContent: '选中当前实体的相邻实体和关系' },
|
|
|
- { key: 'boxSelect', label: '框选', icon: BorderOutlined, tipsContent: 'G6 当前通过 Shift 拖拽框选' },
|
|
|
- { key: 'forceLayout', label: '智能布局', icon: ApartmentOutlined, tipsContent: '按对象关系分析的力导向重新布局' },
|
|
|
- { key: 'fullScreen', label: '全屏预览', icon: FullscreenOutlined, gap: true },
|
|
|
- ];
|
|
|
-
|
|
|
const stageRef = ref<HTMLDivElement>();
|
|
|
const graphRef = shallowRef<Graph | null>(null);
|
|
|
+ let stageResizeObserver: ResizeObserver | null = null;
|
|
|
const building = ref(false);
|
|
|
const zoomPercent = ref(100);
|
|
|
const performanceMode = ref(false);
|
|
|
@@ -1008,28 +1001,12 @@
|
|
|
|
|
|
/* ---------------- 工具条行为 ---------------- */
|
|
|
|
|
|
- const isToolbarItemActive = (item: AnalysisToolbarItem) => item.panelKey === activePanel.value;
|
|
|
+ const togglePanel = (panel: 'find' | 'filter') => {
|
|
|
+ activePanel.value = activePanel.value === panel ? null : panel;
|
|
|
+ };
|
|
|
|
|
|
- const handleToolbarClick = (item: AnalysisToolbarItem) => {
|
|
|
- if (item.panelKey) {
|
|
|
- activePanel.value = activePanel.value === item.panelKey ? null : item.panelKey;
|
|
|
- return;
|
|
|
- }
|
|
|
- if (item.key === 'selectNeighbor') {
|
|
|
- selectNeighborElements();
|
|
|
- return;
|
|
|
- }
|
|
|
- if (item.key === 'boxSelect') {
|
|
|
- showMessage('按住 Shift 拖动画布即可框选', 'info');
|
|
|
- return;
|
|
|
- }
|
|
|
- if (item.key === 'forceLayout') {
|
|
|
- void relayout();
|
|
|
- return;
|
|
|
- }
|
|
|
- if (item.key === 'fullScreen') {
|
|
|
- emit('toggle-fullscreen');
|
|
|
- }
|
|
|
+ const showBoxSelectTip = () => {
|
|
|
+ showMessage('按住 Shift 拖动画布即可框选', 'info');
|
|
|
};
|
|
|
|
|
|
const selectNeighborElements = () => {
|
|
|
@@ -1085,6 +1062,8 @@
|
|
|
}
|
|
|
|
|
|
function destroyGraph() {
|
|
|
+ stageResizeObserver?.disconnect();
|
|
|
+ stageResizeObserver = null;
|
|
|
const graph = graphRef.value;
|
|
|
graphRef.value = null;
|
|
|
try {
|
|
|
@@ -1094,6 +1073,28 @@
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * G6 自带 autoResize 在聊天流式布局/全屏切换时偶尔拿不到最终尺寸,
|
|
|
+ * 这里再用 ResizeObserver 显式 resize,保证画布始终铺满容器,缩放后不留白。
|
|
|
+ */
|
|
|
+ function observeStageSize() {
|
|
|
+ if (typeof ResizeObserver === 'undefined' || !stageRef.value) return;
|
|
|
+ stageResizeObserver?.disconnect();
|
|
|
+ stageResizeObserver = new ResizeObserver(() => {
|
|
|
+ const element = stageRef.value;
|
|
|
+ const graph = graphRef.value;
|
|
|
+ if (!element || !graph) return;
|
|
|
+ const width = Math.max(1, element.clientWidth);
|
|
|
+ const height = Math.max(1, element.clientHeight);
|
|
|
+ try {
|
|
|
+ graph.resize(width, height);
|
|
|
+ } catch {
|
|
|
+ /* 忽略尺寸同步异常 */
|
|
|
+ }
|
|
|
+ });
|
|
|
+ stageResizeObserver.observe(stageRef.value);
|
|
|
+ }
|
|
|
+
|
|
|
function createNodeOptions() {
|
|
|
return {
|
|
|
type: performanceMode.value ? 'ai-graph-performance-node' : 'ai-graph-node',
|
|
|
@@ -1254,6 +1255,7 @@
|
|
|
graphRef.value = graph;
|
|
|
bindGraphEvents(graph);
|
|
|
await graph.render();
|
|
|
+ observeStageSize();
|
|
|
await graph.fitView({ when: 'always' }, false);
|
|
|
refreshZoom();
|
|
|
} catch (error) {
|
|
|
@@ -1704,6 +1706,7 @@
|
|
|
.ai-graph-viewer {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
+ width: 100%;
|
|
|
height: 100%;
|
|
|
min-height: 0;
|
|
|
background: var(--ai-bg, #fff);
|
|
|
@@ -1933,12 +1936,14 @@
|
|
|
|
|
|
.relation-stage {
|
|
|
flex: 1 1 auto;
|
|
|
+ width: 100%;
|
|
|
min-height: 0;
|
|
|
overflow: hidden;
|
|
|
}
|
|
|
|
|
|
.relation-stage__canvas {
|
|
|
position: relative;
|
|
|
+ width: 100%;
|
|
|
height: 100%;
|
|
|
min-height: 0;
|
|
|
overflow: hidden;
|
|
|
@@ -2220,7 +2225,10 @@
|
|
|
z-index: 1;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
+ justify-content: flex-end;
|
|
|
+ flex-wrap: wrap;
|
|
|
gap: 4px;
|
|
|
+ max-width: calc(100vw - 48px);
|
|
|
padding: 7px;
|
|
|
border-radius: 16px;
|
|
|
border: 1px solid rgba(148, 163, 184, 0.2);
|
|
|
@@ -2243,6 +2251,11 @@
|
|
|
color: #2563eb;
|
|
|
background: rgba(59, 130, 246, 0.1);
|
|
|
}
|
|
|
+
|
|
|
+ &.is-active {
|
|
|
+ color: #2563eb;
|
|
|
+ background: rgba(59, 130, 246, 0.12);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
.canvas-toolbar__divider {
|