|
|
@@ -8,12 +8,22 @@
|
|
|
<span v-if="model" class="ai-graph__count">节点 {{ model.nodes.length }} · 关系 {{ model.edges.length }}</span>
|
|
|
</header>
|
|
|
|
|
|
- <div v-if="model?.truncated" class="ai-graph__notice" :title="model.note">
|
|
|
+ <div v-if="fullGraphError" class="ai-graph__notice" :title="fullGraphError">
|
|
|
+ 全量图谱加载失败,当前展示工具摘要(可能不完整):{{ fullGraphError }}
|
|
|
+ </div>
|
|
|
+ <div v-else-if="model?.truncated && !remotePayload" class="ai-graph__notice" :title="model.note">
|
|
|
{{ model.note || `关系过多,仅展示部分数据(共 ${model.totalEdges ?? '?'} 条)` }}
|
|
|
</div>
|
|
|
|
|
|
<div ref="bodyRef" class="ai-graph__body">
|
|
|
- <template v-if="model && renderable">
|
|
|
+ <div v-if="fullGraphLoading" class="ai-graph__placeholder">
|
|
|
+ <div class="ai-skeleton" :style="{ height: '18px', width: '38%' }"></div>
|
|
|
+ <div class="ai-skeleton" :style="{ height: '14px', width: '62%' }"></div>
|
|
|
+ <div class="ai-skeleton" :style="{ height: '14px', width: '48%' }"></div>
|
|
|
+ <span class="ai-graph__loading-text">正在加载全量图谱…</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <template v-else-if="model && renderable">
|
|
|
<AsyncGraphViewer v-if="visible" ref="viewerRef" :model="model" @toggle-fullscreen="toggleFullscreen" />
|
|
|
<div v-else class="ai-graph__placeholder">
|
|
|
<div class="ai-skeleton" :style="{ height: '18px', width: '38%' }"></div>
|
|
|
@@ -42,16 +52,19 @@
|
|
|
* 本组件只负责聊天里的容器与懒加载;画布/查找/过滤/明细在独立的
|
|
|
* GraphAnalysisViewer 中实现,不与对象关系分析页面共用组件。
|
|
|
*/
|
|
|
- import { computed, defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, ref } from 'vue';
|
|
|
+ import { computed, defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|
|
import Icon from '@/core/components/Icon/src/Icon.vue';
|
|
|
import { useBlockFullscreen } from '../../utils/useBlockFullscreen';
|
|
|
import { parseGraphPayload } from './graphData';
|
|
|
+ import { fetchFullGraph } from './graphFullApi';
|
|
|
|
|
|
const AsyncGraphViewer = defineAsyncComponent(() => import('./GraphAnalysisViewer.vue'));
|
|
|
|
|
|
const props = withDefaults(
|
|
|
defineProps<{
|
|
|
payload: unknown;
|
|
|
+ /** get_case_graph 的原始查询参数:用于直接取全量图谱,展示不经过模型裁剪 */
|
|
|
+ query?: Record<string, unknown>;
|
|
|
/** 查询摘要(人名 / 日期 / 口径) */
|
|
|
meta?: string;
|
|
|
/** 多张图时的序号(从 0 开始) */
|
|
|
@@ -65,8 +78,12 @@
|
|
|
const rootRef = ref<HTMLElement>();
|
|
|
const bodyRef = ref<HTMLElement>();
|
|
|
const viewerRef = ref<any>(null);
|
|
|
+ const remotePayload = ref<unknown>(null);
|
|
|
+ const fullGraphLoading = ref(false);
|
|
|
+ const fullGraphError = ref('');
|
|
|
|
|
|
- const model = computed(() => parseGraphPayload(props.payload));
|
|
|
+ const effectivePayload = computed(() => remotePayload.value ?? props.payload);
|
|
|
+ const model = computed(() => parseGraphPayload(effectivePayload.value));
|
|
|
const renderable = computed(() => Boolean(model.value && model.value.nodes.length && model.value.edges.length));
|
|
|
const metaText = computed(() => props.meta || '');
|
|
|
|
|
|
@@ -77,6 +94,30 @@
|
|
|
return '暂无可展示的图谱数据';
|
|
|
});
|
|
|
|
|
|
+ let fullGraphRequestToken = 0;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 工具结果为了模型上下文只带前 500 条边;这里按工具入参直接请求 /graph/view,
|
|
|
+ * 前端展示唯一数据源 = 后端原始全量图谱,不经过模型输出或模型过滤。
|
|
|
+ */
|
|
|
+ async function loadFullGraph() {
|
|
|
+ if (!props.query) return;
|
|
|
+ const token = ++fullGraphRequestToken;
|
|
|
+ fullGraphLoading.value = true;
|
|
|
+ fullGraphError.value = '';
|
|
|
+ try {
|
|
|
+ const payload = await fetchFullGraph(props.query);
|
|
|
+ if (token !== fullGraphRequestToken) return;
|
|
|
+ remotePayload.value = payload;
|
|
|
+ } catch (error: any) {
|
|
|
+ if (token !== fullGraphRequestToken) return;
|
|
|
+ remotePayload.value = null;
|
|
|
+ fullGraphError.value = error?.message || '请求失败';
|
|
|
+ } finally {
|
|
|
+ if (token === fullGraphRequestToken) fullGraphLoading.value = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
/* ---------------- 懒加载:首次进入视口才挂载 G6 ---------------- */
|
|
|
|
|
|
const visible = ref(false);
|
|
|
@@ -104,6 +145,24 @@
|
|
|
observer = null;
|
|
|
});
|
|
|
|
|
|
+ watch(
|
|
|
+ () => visible.value,
|
|
|
+ () => {
|
|
|
+ if (visible.value) void loadFullGraph();
|
|
|
+ },
|
|
|
+ { immediate: true },
|
|
|
+ );
|
|
|
+
|
|
|
+ watch(
|
|
|
+ () => props.query,
|
|
|
+ () => {
|
|
|
+ remotePayload.value = null;
|
|
|
+ fullGraphError.value = '';
|
|
|
+ if (visible.value) void loadFullGraph();
|
|
|
+ },
|
|
|
+ { deep: true },
|
|
|
+ );
|
|
|
+
|
|
|
/* ---------------- 全屏 ---------------- */
|
|
|
|
|
|
const { isFullscreen, toggle: toggleFullscreen } = useBlockFullscreen(async () => {
|
|
|
@@ -236,6 +295,12 @@
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ &__loading-text {
|
|
|
+ margin-top: 2px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: var(--ai-text-2, #64748b);
|
|
|
+ }
|
|
|
+
|
|
|
&__empty {
|
|
|
display: flex;
|
|
|
align-items: center;
|