feat(insight): add group relationship galaxy view

This commit is contained in:
digua committed 2026-08-09 10:57:24 +08:00
1 parent 03520b4cc0
commit 2f5f97f0bf
14 files changed
+515 -23

No files matched your search

@@ -3,7 +3,7 @@ import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { SectionTabs } from '@/components/navigation'
import ClusterView from '@/components/analysis/relationships/ClusterView.vue'
import InteractionView from '@/components/analysis/relationships/InteractionView.vue'
import GroupRelationshipGalaxyView from '@/components/analysis/relationships/GroupRelationshipGalaxyView.vue'
import MentionRankingView from '@/components/analysis/relationships/MentionRankingView.vue'
import type { TimeFilter } from '@openchatlab/shared-types'
import { getProximityTimeFilter } from './group-relationship-filter'
@@ -14,13 +14,13 @@ const props = defineProps<{
}>()
const { t } = useI18n()
const activeSubTab = ref('mention-graph')
const activeSubTab = ref('relationship-galaxy')
const proximityTimeFilter = computed(() => getProximityTimeFilter(props.timeFilter))
const subTabs = computed(() => [
{
id: 'mention-graph',
label: t('analysis.subTabs.groupRelationships.mentionGraph'),
icon: 'i-heroicons-arrows-right-left',
id: 'relationship-galaxy',
label: t('analysis.subTabs.groupRelationships.relationshipGalaxy'),
icon: 'i-lucide-orbit',
},
{
id: 'mention-ranking',
@@ -41,8 +41,8 @@ const subTabs = computed(() => [
<div class="min-h-0 flex-1 overflow-y-auto">
<Transition name="fade" mode="out-in">
<InteractionView
v-if="activeSubTab === 'mention-graph'"
<GroupRelationshipGalaxyView
v-if="activeSubTab === 'relationship-galaxy'"
:session-id="props.sessionId"
:time-filter="props.timeFilter"
/>
@@ -0,0 +1,337 @@
<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import type {
GroupRelationshipGalaxyData,
GroupRelationshipGalaxyMemberDetail,
RelationshipGalaxyRenderNode,
TimeFilter,
} from '@openchatlab/shared-types'
import { LoadingState } from '@/components/UI'
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'
type GalaxyCanvasInstance = {
focusNode: (key: string) => boolean
fitView: () => void
}
const props = defineProps<{
sessionId: string
timeFilter?: TimeFilter
}>()
const { t, locale } = useI18n()
const dataService = useDataService()
const canvasRef = ref<GalaxyCanvasInstance | null>(null)
const galaxyData = ref<GroupRelationshipGalaxyData | null>(null)
const selectedKey = ref<string | null>(null)
const isLoading = ref(false)
const loadError = ref('')
const webglUnavailable = ref(false)
const isWideLayout = ref(false)
let requestId = 0
let detailMediaQuery: MediaQueryList | null = null
const selectedMember = computed(() => {
if (!selectedKey.value) return null
return galaxyData.value?.members.find((member) => member.key === selectedKey.value) ?? null
})
const connections = computed(() => buildGroupRelationshipGalaxyConnections(galaxyData.value, selectedKey.value))
const hasGraph = computed(() => (galaxyData.value?.graph.nodes.length ?? 0) > 0)
const safeInsetRight = computed(() => (selectedMember.value && isWideLayout.value ? 392 : 0))
const numberFormatter = computed(
() => new Intl.NumberFormat(locale.value, { maximumFractionDigits: 1, notation: 'compact' })
)
const dateFormatter = computed(
() => new Intl.DateTimeFormat(locale.value, { year: 'numeric', month: 'short', day: 'numeric' })
)
async function loadData() {
if (!props.sessionId) return
const currentRequestId = ++requestId
isLoading.value = true
loadError.value = ''
webglUnavailable.value = false
selectedKey.value = null
try {
const result = await dataService.getGroupRelationshipGalaxy(props.sessionId, props.timeFilter)
if (currentRequestId !== requestId) return
galaxyData.value = result
} catch (error) {
if (currentRequestId !== requestId) return
const message = error instanceof Error ? error.message : String(error)
loadError.value = message
galaxyData.value = null
reportError(`Group relationship galaxy load failed: ${message}`, error instanceof Error ? error.stack : undefined)
} finally {
if (currentRequestId === requestId) isLoading.value = false
}
}
async function selectNode(node: RelationshipGalaxyRenderNode) {
selectedKey.value = node.key
await nextTick()
canvasRef.value?.focusNode(node.key)
}
async function selectMember(member: GroupRelationshipGalaxyMemberDetail) {
selectedKey.value = member.key
await nextTick()
canvasRef.value?.focusNode(member.key)
}
function clearSelection() {
selectedKey.value = null
}
async function resetView() {
selectedKey.value = null
await nextTick()
canvasRef.value?.fitView()
}
function handleCanvasFallback() {
webglUnavailable.value = true
selectedKey.value = null
}
function syncDetailLayout() {
isWideLayout.value = detailMediaQuery?.matches ?? false
}
function avatarText(member: GroupRelationshipGalaxyMemberDetail): string {
return (member.displayName || member.platformId || '?').slice(0, 1)
}
function formatNumber(value: number): string {
return numberFormatter.value.format(value)
}
function formatScore(value: number): string {
return `${Math.round(value * 100)}`
}
function formatTime(value: number | null): string {
if (!value) return '—'
return dateFormatter.value.format(new Date(value * 1000))
}
watch(
() => [props.sessionId, props.timeFilter?.startTs, props.timeFilter?.endTs, props.timeFilter?.memberId],
() => void loadData(),
{ immediate: true }
)
onMounted(() => {
if (typeof window === 'undefined') return
detailMediaQuery = window.matchMedia('(min-width: 768px)')
syncDetailLayout()
detailMediaQuery.addEventListener('change', syncDetailLayout)
})
onBeforeUnmount(() => {
requestId++
detailMediaQuery?.removeEventListener('change', syncDetailLayout)
})
</script>
<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')" />
<div v-else-if="loadError" 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>
<p class="mt-1 line-clamp-2 text-sm text-white/50">{{ loadError }}</p>
<UButton class="mt-4" color="neutral" variant="soft" icon="i-lucide-refresh-cw" @click="loadData">
{{ t('common.retry') }}
</UButton>
</div>
</div>
<div v-else-if="webglUnavailable" 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>
<p class="mt-1 text-sm leading-6 text-white/50">
{{ t('views.groupRelationshipGalaxy.webglUnavailableDescription') }}
</p>
</div>
</div>
<div v-else-if="!hasGraph" 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>
<p class="mt-1 text-sm leading-6 text-white/50">
{{ t('views.groupRelationshipGalaxy.emptyDescription') }}
</p>
</div>
</div>
<template v-else-if="galaxyData">
<RelationshipGalaxyThreeCanvas
ref="canvasRef"
:graph="galaxyData.graph"
:selected-key="selectedKey"
:safe-inset-right="safeInsetRight"
:label="t('views.groupRelationshipGalaxy.canvasLabel')"
@clear-selection="clearSelection"
@fallback="handleCanvasFallback"
@select-node="selectNode"
/>
<UButton
icon="i-lucide-scan"
color="neutral"
variant="soft"
size="xs"
class="absolute left-4 top-4 z-20 border border-white/8 bg-black/25 text-white/75 backdrop-blur-md hover:bg-white/10 hover:text-white"
:aria-label="t('views.groupRelationshipGalaxy.resetView')"
@click="resetView"
>
{{ t('views.groupRelationshipGalaxy.resetView') }}
</UButton>
<aside
v-if="selectedMember"
class="dark absolute inset-x-3 bottom-3 z-20 flex max-h-[72vh] flex-col overflow-hidden rounded-2xl border border-white/8 bg-[#0b0c12]/88 text-white shadow-overlay backdrop-blur-xl md:inset-x-auto md:bottom-4 md:right-4 md:top-4 md:max-h-none md:w-[360px]"
>
<UButton
icon="i-lucide-x"
color="neutral"
variant="ghost"
size="xs"
class="absolute right-2.5 top-2.5 z-10 text-white/60 hover:bg-white/8 hover:text-white"
:aria-label="t('views.groupRelationshipGalaxy.closeDetail')"
@click="clearSelection"
/>
<div class="flex min-h-0 flex-1 flex-col gap-4 px-4 pb-4 pt-5">
<header class="flex shrink-0 items-center gap-3 pr-8">
<LazyAvatar
:src="selectedMember.avatar"
:alt="selectedMember.displayName"
:text="avatarText(selectedMember)"
root-class="h-11 w-11 shrink-0 overflow-hidden rounded-xl border border-white/10"
image-class="h-11 w-11 rounded-xl object-cover"
fallback-class="flex h-11 w-11 items-center justify-center rounded-xl bg-sky-400/12 text-sm font-bold text-sky-200"
/>
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2">
<h3 class="truncate text-base font-semibold">{{ selectedMember.displayName }}</h3>
<span class="rounded-full bg-white/8 px-2 py-0.5 font-mono text-[10px] text-white/55">
#{{ selectedMember.rank }}
</span>
</div>
<p class="mt-0.5 truncate text-xs text-white/40">{{ selectedMember.platformId }}</p>
</div>
</header>
<section class="grid shrink-0 grid-cols-3 gap-1.5">
<div class="rounded-xl border border-white/6 bg-white/3 px-2.5 py-2">
<p class="text-[9px] uppercase tracking-wider text-white/35">
{{ t('views.groupRelationshipGalaxy.messageCount') }}
</p>
<p class="mt-1 font-mono text-sm font-semibold">{{ formatNumber(selectedMember.messageCount) }}</p>
</div>
<div class="rounded-xl border border-white/6 bg-white/3 px-2.5 py-2">
<p class="text-[9px] uppercase tracking-wider text-white/35">
{{ t('views.groupRelationshipGalaxy.relationshipScore') }}
</p>
<p class="mt-1 font-mono text-sm font-semibold text-sky-200">
{{ formatScore(selectedMember.relationshipScore) }}
</p>
</div>
<div class="rounded-xl border border-white/6 bg-white/3 px-2.5 py-2">
<p class="text-[9px] uppercase tracking-wider text-white/35">
{{ t('views.groupRelationshipGalaxy.lastInteraction') }}
</p>
<p class="mt-1 truncate text-xs font-semibold">{{ formatTime(selectedMember.lastInteractionTs) }}</p>
</div>
</section>
<section class="shrink-0">
<h4 class="mb-2 text-[10px] font-semibold uppercase tracking-[0.16em] text-white/40">
{{ t('views.groupRelationshipGalaxy.signalComposition') }}
</h4>
<div class="grid grid-cols-3 gap-1.5">
<div class="rounded-xl bg-violet-400/8 px-2.5 py-2 text-violet-100">
<p class="text-[10px] text-violet-100/50">{{ t('views.groupRelationshipGalaxy.replies') }}</p>
<p class="mt-0.5 font-mono text-sm font-semibold">
{{ formatNumber(selectedMember.replyInteractionCount) }}
</p>
</div>
<div class="rounded-xl bg-sky-400/8 px-2.5 py-2 text-sky-100">
<p class="text-[10px] text-sky-100/50">{{ t('views.groupRelationshipGalaxy.mentions') }}</p>
<p class="mt-0.5 font-mono text-sm font-semibold">
{{ formatNumber(selectedMember.mentionInteractionCount) }}
</p>
</div>
<div class="rounded-xl bg-amber-300/8 px-2.5 py-2 text-amber-100">
<p class="text-[10px] text-amber-100/50">{{ t('views.groupRelationshipGalaxy.proximity') }}</p>
<p class="mt-0.5 font-mono text-sm font-semibold">
{{ formatNumber(selectedMember.coOccurrenceCount) }}
</p>
</div>
</div>
</section>
<section class="flex min-h-0 flex-1 flex-col">
<div class="mb-2 flex items-center justify-between">
<h4 class="text-[10px] font-semibold uppercase tracking-[0.16em] text-white/40">
{{ t('views.groupRelationshipGalaxy.strongConnections') }}
</h4>
<span class="font-mono text-[10px] text-white/30">{{ connections.length }}</span>
</div>
<div v-if="connections.length" class="min-h-0 flex-1 space-y-1 overflow-y-auto pr-1 scrollbar-thin">
<button
v-for="connection in connections"
:key="connection.edge.id"
type="button"
class="group flex w-full items-center gap-2.5 rounded-xl border border-transparent px-2 py-2 text-left transition-colors hover:border-white/6 hover:bg-white/5"
@click="selectMember(connection.member)"
>
<LazyAvatar
:src="connection.member.avatar"
:alt="connection.member.displayName"
:text="avatarText(connection.member)"
root-class="h-7 w-7 shrink-0 overflow-hidden rounded-lg border border-white/8"
image-class="h-7 w-7 rounded-lg object-cover"
fallback-class="flex h-7 w-7 items-center justify-center rounded-lg bg-white/6 text-[10px] font-semibold text-white/65"
/>
<div class="min-w-0 flex-1">
<p class="truncate text-xs font-semibold text-white/85 group-hover:text-white">
{{ connection.member.displayName }}
</p>
<p class="mt-0.5 truncate text-[10px] text-white/35">
{{
t('views.groupRelationshipGalaxy.connectionSignals', {
replies: connection.edge.replyInteractionCount,
mentions: connection.edge.mentionInteractionCount,
proximity: connection.edge.coOccurrenceCount,
})
}}
</p>
</div>
<span class="shrink-0 font-mono text-xs font-semibold text-sky-200/80">
{{ formatNumber(connection.edge.weight) }}
</span>
</button>
</div>
<p v-else class="py-5 text-center text-xs text-white/35">
{{ t('views.groupRelationshipGalaxy.noConnections') }}
</p>
</section>
</div>
</aside>
</template>
</div>
</template>
@@ -0,0 +1,37 @@
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'
const data = {
members: [
{ key: 'a', rank: 1 },
{ key: 'b', rank: 2 },
{ key: 'c', rank: 3 },
],
edges: [
{ id: 'a-c', sourceKey: 'a', targetKey: 'c', weight: 2, lastInteractionTs: 200 },
{ id: 'a-b', sourceKey: 'b', targetKey: 'a', weight: 5, lastInteractionTs: 100 },
{ id: 'b-c', sourceKey: 'b', targetKey: 'c', weight: 8, lastInteractionTs: 300 },
],
} as GroupRelationshipGalaxyData
test('builds the selected member connection list from either edge direction', () => {
const connections = buildGroupRelationshipGalaxyConnections(data, 'a')
assert.deepEqual(
connections.map((item) => [item.member.key, item.edge.id]),
[
['b', 'a-b'],
['c', 'a-c'],
]
)
})
test('limits the detail list without including unrelated strong edges', () => {
assert.deepEqual(
buildGroupRelationshipGalaxyConnections(data, 'a', 1).map((item) => item.member.key),
['b']
)
assert.deepEqual(buildGroupRelationshipGalaxyConnections(data, null), [])
})
@@ -0,0 +1,34 @@
import type {
GroupRelationshipGalaxyData,
GroupRelationshipGalaxyEdgeDetail,
GroupRelationshipGalaxyMemberDetail,
} from '@openchatlab/shared-types'
export interface GroupRelationshipGalaxyConnection {
member: GroupRelationshipGalaxyMemberDetail
edge: GroupRelationshipGalaxyEdgeDetail
}
export function buildGroupRelationshipGalaxyConnections(
data: GroupRelationshipGalaxyData | null,
selectedKey: string | null,
limit = 10
): GroupRelationshipGalaxyConnection[] {
if (!data || !selectedKey) return []
const memberByKey = new Map(data.members.map((member) => [member.key, member]))
return data.edges
.flatMap((edge): GroupRelationshipGalaxyConnection[] => {
if (edge.sourceKey !== selectedKey && edge.targetKey !== selectedKey) return []
const otherKey = edge.sourceKey === selectedKey ? edge.targetKey : edge.sourceKey
const member = memberByKey.get(otherKey)
return member ? [{ member, edge }] : []
})
.sort(
(a, b) =>
b.edge.weight - a.edge.weight ||
(b.edge.lastInteractionTs ?? 0) - (a.edge.lastInteractionTs ?? 0) ||
a.member.rank - b.member.rank ||
a.member.key.localeCompare(b.member.key)
)
.slice(0, Math.max(0, limit))
}
@@ -71,6 +71,7 @@ const props = withDefaults(
const emit = defineEmits<{
(event: 'select-node', node: RelationshipGalaxyRenderNode): void
(event: 'clear-selection'): void
(event: 'fallback'): void
}>()
@@ -491,7 +492,10 @@ function handlePointerLeave() {
function handleClick() {
const key = hoveredKey.value
if (!key) return
if (!key) {
emit('clear-selection')
return
}
const object = nodeObjects.get(key)
if (!object) return
emit('select-node', object.sceneNode.node)
+1 -1
View File
@@ -81,7 +81,7 @@
"catchphrase": "Catchphrases"
},
"groupRelationships": {
"mentionGraph": "Mention Graph",
"relationshipGalaxy": "Relationship Galaxy",
"mentionRanking": "Mention Ranking",
"proximity": "Proximity"
},
+21
View File
@@ -118,6 +118,27 @@
"graphHint": "{nodes} members, {links} interactions",
"noInteraction": "No mention interaction data"
},
"groupRelationshipGalaxy": {
"canvasLabel": "Group interaction relationship galaxy",
"loading": "Mapping group relationships...",
"loadFailed": "Failed to load the relationship galaxy",
"webglUnavailable": "The relationship galaxy is unavailable",
"webglUnavailableDescription": "Enable hardware acceleration or try an environment that supports WebGL.",
"emptyTitle": "Not enough interaction data",
"emptyDescription": "At least two members need replies, mentions, or nearby messages in this time range.",
"resetView": "Reset view",
"closeDetail": "Close member details",
"messageCount": "Group messages",
"relationshipScore": "Relationship strength",
"lastInteraction": "Last interaction",
"signalComposition": "Interaction signals",
"replies": "Replies",
"mentions": "Mentions",
"proximity": "Proximity",
"strongConnections": "Closest connections",
"connectionSignals": "{replies} replies · {mentions} mentions · {proximity} nearby",
"noConnections": "No direct connections to display"
},
"relationship": {
"empty": {
"title": "No Relationship Data",
+1 -1
View File
@@ -81,7 +81,7 @@
"catchphrase": "口癖"
},
"groupRelationships": {
"mentionGraph": "{'@'} 関係図",
"relationshipGalaxy": "関係ギャラクシー",
"mentionRanking": "{'@'} ランキング",
"proximity": "発言の近接度"
},
+21
View File
@@ -118,6 +118,27 @@
"graphHint": "全 {nodes} 名のメンバー、{links} 件のインタラクション",
"noInteraction": "メンションデータがありません"
},
"groupRelationshipGalaxy": {
"canvasLabel": "グループの交流関係ギャラクシー",
"loading": "グループ内の関係を整理しています...",
"loadFailed": "関係ギャラクシーを読み込めませんでした",
"webglUnavailable": "この環境ではギャラクシーを表示できません",
"webglUnavailableDescription": "ハードウェアアクセラレーションを有効にするか、WebGL 対応環境で再試行してください。",
"emptyTitle": "交流データが不足しています",
"emptyDescription": "この期間には、2人以上の返信、メンション、または連続した発言が必要です。",
"resetView": "表示をリセット",
"closeDetail": "メンバー詳細を閉じる",
"messageCount": "グループ発言数",
"relationshipScore": "関係の強さ",
"lastInteraction": "最近の交流",
"signalComposition": "交流の内訳",
"replies": "返信",
"mentions": "メンション",
"proximity": "発言の近さ",
"strongConnections": "親しいつながり",
"connectionSignals": "返信 {replies} · メンション {mentions} · 近接 {proximity}",
"noConnections": "表示できる直接のつながりはありません"
},
"relationship": {
"empty": {
"title": "関係データがありません",
+1 -1
View File
@@ -82,7 +82,7 @@
"catchphrase": "口头禅"
},
"groupRelationships": {
"mentionGraph": "{'@'} 关系图",
"relationshipGalaxy": "关系星空图",
"mentionRanking": "{'@'} 排行",
"proximity": "发言临近度"
},
+21
View File
@@ -118,6 +118,27 @@
"graphHint": "共 {nodes} 位成员,{links} 条互动关系",
"noInteraction": "暂无艾特互动数据"
},
"groupRelationshipGalaxy": {
"canvasLabel": "群聊互动关系星空图",
"loading": "正在整理群内关系...",
"loadFailed": "关系星空图加载失败",
"webglUnavailable": "当前环境无法显示星空图",
"webglUnavailableDescription": "请启用浏览器硬件加速,或更换支持 WebGL 的环境后重试。",
"emptyTitle": "暂无足够的互动关系",
"emptyDescription": "当前时间范围内至少需要两位成员产生回复、@ 或相邻发言。",
"resetView": "重置视角",
"closeDetail": "关闭成员详情",
"messageCount": "本群消息",
"relationshipScore": "关系强度",
"lastInteraction": "最近互动",
"signalComposition": "互动构成",
"replies": "回复",
"mentions": "@ 提及",
"proximity": "发言临近",
"strongConnections": "关系最密切",
"connectionSignals": "回复 {replies} · @ {mentions} · 临近 {proximity}",
"noConnections": "暂无可展示的直接关系"
},
"relationship": {
"empty": {
"title": "暂无关系数据",
+1 -1
View File
@@ -81,7 +81,7 @@
"catchphrase": "口頭禪"
},
"groupRelationships": {
"mentionGraph": "{'@'} 關係圖",
"relationshipGalaxy": "關係星空圖",
"mentionRanking": "{'@'} 排行",
"proximity": "發言臨近度"
},
+21
View File
@@ -118,6 +118,27 @@
"graphHint": "共 {nodes} 位成員,{links} 條互動關係",
"noInteraction": "暫無艾特互動資料"
},
"groupRelationshipGalaxy": {
"canvasLabel": "群聊互動關係星空圖",
"loading": "正在整理群內關係...",
"loadFailed": "關係星空圖載入失敗",
"webglUnavailable": "目前環境無法顯示星空圖",
"webglUnavailableDescription": "請啟用瀏覽器硬體加速,或改用支援 WebGL 的環境後重試。",
"emptyTitle": "暫無足夠的互動關係",
"emptyDescription": "目前時間範圍內至少需要兩位成員產生回覆、@ 或相鄰發言。",
"resetView": "重設視角",
"closeDetail": "關閉成員詳情",
"messageCount": "本群訊息",
"relationshipScore": "關係強度",
"lastInteraction": "最近互動",
"signalComposition": "互動構成",
"replies": "回覆",
"mentions": "@ 提及",
"proximity": "發言臨近",
"strongConnections": "關係最密切",
"connectionSignals": "回覆 {replies} · @ {mentions} · 臨近 {proximity}",
"noConnections": "暫無可顯示的直接關係"
},
"relationship": {
"empty": {
"title": "暫無關係資料",
@@ -1,12 +1,8 @@
<script setup lang="ts">
/**
* 群关系 - 合并视图
* 将 @ 关系图、@ 排行、发言临近度 三个分析整合到一个 Tab 中
*/
import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { SectionTabs } from '@/components/navigation'
import InteractionView from '@/components/analysis/relationships/InteractionView.vue'
import GroupRelationshipGalaxyView from '@/components/analysis/relationships/GroupRelationshipGalaxyView.vue'
import MentionRankingView from '@/components/analysis/relationships/MentionRankingView.vue'
import ClusterView from '@/components/analysis/relationships/ClusterView.vue'
import type { TimeFilter } from '@openchatlab/shared-types'
@@ -18,13 +14,13 @@ const props = defineProps<{
timeFilter?: TimeFilter
}>()
const activeSubTab = ref('mention-graph')
const activeSubTab = ref('relationship-galaxy')
const subTabs = computed(() => [
{
id: 'mention-graph',
label: t('analysis.subTabs.groupRelationships.mentionGraph'),
icon: 'i-heroicons-arrows-right-left',
id: 'relationship-galaxy',
label: t('analysis.subTabs.groupRelationships.relationshipGalaxy'),
icon: 'i-lucide-orbit',
},
{ id: 'mention-ranking', label: t('analysis.subTabs.groupRelationships.mentionRanking'), icon: 'i-heroicons-heart' },
{ id: 'proximity', label: t('analysis.subTabs.groupRelationships.proximity'), icon: 'i-heroicons-user-group' },
@@ -37,8 +33,8 @@ const subTabs = computed(() => [
<div class="min-h-0 flex-1 overflow-y-auto">
<Transition name="fade" mode="out-in">
<InteractionView
v-if="activeSubTab === 'mention-graph'"
<GroupRelationshipGalaxyView
v-if="activeSubTab === 'relationship-galaxy'"
:session-id="props.sessionId"
:time-filter="props.timeFilter"
/>