fix(analysis): preserve galaxy canvas during refresh

This commit is contained in:
digua committed 2026-08-09 10:57:24 +08:00
1 parent 0630ed1b7b
commit 3aee46df28
3 files changed
+77 -7

No files matched your search

@@ -13,7 +13,10 @@ import LazyAvatar from '@/components/common/avatar/LazyAvatar.vue'
import RelationshipGalaxyThreeCanvas from '@/components/charts/relationship-galaxy/RelationshipGalaxyThreeCanvas.vue'
import { useDataService } from '@/services'
import { reportError } from '@/services/log-report'
import { buildGroupRelationshipGalaxyConnections } from './group-relationship-galaxy-view'
import {
buildGroupRelationshipGalaxyConnections,
resolveGroupRelationshipGalaxyDisplayState,
} from './group-relationship-galaxy-view'
type GalaxyCanvasInstance = {
focusNode: (key: string) => boolean
@@ -43,6 +46,14 @@ const selectedMember = computed(() => {
})
const connections = computed(() => buildGroupRelationshipGalaxyConnections(galaxyData.value, selectedKey.value))
const hasGraph = computed(() => (galaxyData.value?.graph.nodes.length ?? 0) > 0)
const displayState = computed(() =>
resolveGroupRelationshipGalaxyDisplayState({
isLoading: isLoading.value,
hasGraph: hasGraph.value,
hasError: Boolean(loadError.value),
webglUnavailable: webglUnavailable.value,
})
)
const safeInsetRight = computed(() => (selectedMember.value && isWideLayout.value ? 392 : 0))
const numberFormatter = computed(
() => new Intl.NumberFormat(locale.value, { maximumFractionDigits: 1, notation: 'compact' })
@@ -143,9 +154,13 @@ onBeforeUnmount(() => {
<template>
<div class="relative h-full min-h-[420px] w-full overflow-hidden bg-[#050302] text-white">
<LoadingState v-if="isLoading" variant="page" :text="t('views.groupRelationshipGalaxy.loading')" />
<LoadingState
v-if="displayState.content === 'loading'"
variant="page"
:text="t('views.groupRelationshipGalaxy.loading')"
/>
<div v-else-if="loadError" class="flex h-full items-center justify-center px-6 text-center">
<div v-else-if="displayState.content === 'error'" class="flex h-full items-center justify-center px-6 text-center">
<div class="max-w-sm">
<UIcon name="i-lucide-circle-alert" class="mx-auto h-10 w-10 text-rose-300" />
<h3 class="mt-3 text-base font-semibold">{{ t('views.groupRelationshipGalaxy.loadFailed') }}</h3>
@@ -156,7 +171,10 @@ onBeforeUnmount(() => {
</div>
</div>
<div v-else-if="webglUnavailable" class="flex h-full items-center justify-center px-6 text-center">
<div
v-else-if="displayState.content === 'webgl-unavailable'"
class="flex h-full items-center justify-center px-6 text-center"
>
<div class="max-w-sm">
<UIcon name="i-lucide-orbit" class="mx-auto h-11 w-11 text-amber-200/80" />
<h3 class="mt-3 text-base font-semibold">{{ t('views.groupRelationshipGalaxy.webglUnavailable') }}</h3>
@@ -166,7 +184,7 @@ onBeforeUnmount(() => {
</div>
</div>
<div v-else-if="!hasGraph" class="flex h-full items-center justify-center px-6 text-center">
<div v-else-if="displayState.content === 'empty'" class="flex h-full items-center justify-center px-6 text-center">
<div class="max-w-sm">
<UIcon name="i-lucide-sparkles" class="mx-auto h-11 w-11 text-sky-200/70" />
<h3 class="mt-3 text-base font-semibold">{{ t('views.groupRelationshipGalaxy.emptyTitle') }}</h3>
@@ -176,7 +194,7 @@ onBeforeUnmount(() => {
</div>
</div>
<template v-else-if="galaxyData">
<template v-else-if="displayState.content === 'canvas' && galaxyData">
<RelationshipGalaxyThreeCanvas
ref="canvasRef"
:graph="galaxyData.graph"
@@ -188,7 +206,14 @@ onBeforeUnmount(() => {
@select-node="selectNode"
/>
<LoadingState
v-if="displayState.showLoadingOverlay"
variant="overlay"
:text="t('views.groupRelationshipGalaxy.loading')"
/>
<UButton
v-if="!displayState.showLoadingOverlay"
icon="i-lucide-scan"
color="neutral"
variant="soft"
@@ -1,7 +1,10 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import type { GroupRelationshipGalaxyData } from '@openchatlab/shared-types'
import { buildGroupRelationshipGalaxyConnections } from './group-relationship-galaxy-view'
import {
buildGroupRelationshipGalaxyConnections,
resolveGroupRelationshipGalaxyDisplayState,
} from './group-relationship-galaxy-view'
const data = {
members: [
@@ -35,3 +38,27 @@ test('limits the detail list without including unrelated strong edges', () => {
)
assert.deepEqual(buildGroupRelationshipGalaxyConnections(data, null), [])
})
test('keeps the existing galaxy canvas mounted while a new time range is loading', () => {
assert.deepEqual(
resolveGroupRelationshipGalaxyDisplayState({
isLoading: true,
hasGraph: true,
hasError: false,
webglUnavailable: false,
}),
{ content: 'canvas', showLoadingOverlay: true }
)
})
test('uses the page loading state when no galaxy has been rendered yet', () => {
assert.deepEqual(
resolveGroupRelationshipGalaxyDisplayState({
isLoading: true,
hasGraph: false,
hasError: false,
webglUnavailable: false,
}),
{ content: 'loading', showLoadingOverlay: false }
)
})
@@ -11,6 +11,24 @@ export interface GroupRelationshipGalaxyConnection {
edge: GroupRelationshipGalaxyEdgeDetail
}
export interface GroupRelationshipGalaxyDisplayState {
content: 'loading' | 'error' | 'webgl-unavailable' | 'empty' | 'canvas'
showLoadingOverlay: boolean
}
export function resolveGroupRelationshipGalaxyDisplayState(options: {
isLoading: boolean
hasGraph: boolean
hasError: boolean
webglUnavailable: boolean
}): GroupRelationshipGalaxyDisplayState {
if (options.hasError) return { content: 'error', showLoadingOverlay: false }
if (options.webglUnavailable) return { content: 'webgl-unavailable', showLoadingOverlay: false }
if (options.hasGraph) return { content: 'canvas', showLoadingOverlay: options.isLoading }
if (options.isLoading) return { content: 'loading', showLoadingOverlay: false }
return { content: 'empty', showLoadingOverlay: false }
}
export function buildGroupRelationshipGalaxyConnections(
data: GroupRelationshipGalaxyData | null,
selectedKey: string | null,