split charts into separate routes (#1605)

This commit is contained in:
pa
2026-02-03 00:07:09 +09:00
parent 1cbafbeaeb
commit bbbb79eaca
9 changed files with 290 additions and 265 deletions

View File

@@ -1,41 +0,0 @@
<template>
<div id="chart" class="x-container">
<TabsUnderline v-model="activeTab" :items="chartTabs" :unmount-on-hide="false" class="charts-tabs">
<template #instance>
<InstanceActivity />
</template>
<template #mutual>
<MutualFriends />
</template>
</TabsUnderline>
<BackToTop target="#chart" :right="30" :bottom="30" />
</div>
</template>
<script setup>
import { computed, defineAsyncComponent } from 'vue';
import { TabsUnderline } from '@/components/ui/tabs';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import BackToTop from '@/components/BackToTop.vue';
import { useChartsStore } from '../../stores';
const InstanceActivity = defineAsyncComponent(() => import('./components/InstanceActivity.vue'));
const MutualFriends = defineAsyncComponent(() => import('./components/MutualFriends.vue'));
const { t } = useI18n();
const chartsStore = useChartsStore();
const { activeTab } = storeToRefs(chartsStore);
const chartTabs = computed(() => [
{ value: 'instance', label: t('view.charts.instance_activity.header') },
{ value: 'mutual', label: t('view.charts.mutual_friend.tab_label') }
]);
</script>
<style scoped>
:deep(.charts-tabs [data-slot='tabs-list']) {
margin: 0;
}
</style>

View File

@@ -1,165 +1,175 @@
<template>
<div ref="instanceActivityRef" class="pt-12">
<BackToTop :target="instanceActivityRef" :right="30" :bottom="30" :teleport="false" />
<div class="options-container instance-activity" style="margin-top: 0">
<div>
<span>{{ t('view.charts.instance_activity.header') }}</span>
<HoverCard>
<HoverCardTrigger as-child>
<Info style="margin-left: 4px; font-size: 12px; opacity: 0.7" />
</HoverCardTrigger>
<HoverCardContent side="bottom" align="start" class="w-75">
<div class="tips-popover">
<div>{{ t('view.charts.instance_activity.tips.online_time') }}</div>
<div>{{ t('view.charts.instance_activity.tips.click_Y_axis') }}</div>
<div>{{ t('view.charts.instance_activity.tips.click_instance_name') }}</div>
</div>
</HoverCardContent>
</HoverCard>
</div>
<div id="chart" class="x-container">
<div ref="instanceActivityRef" class="pt-12">
<BackToTop :target="instanceActivityRef" :right="30" :bottom="30" :teleport="false" />
<div class="options-container instance-activity" style="margin-top: 0">
<div>
<span>{{ t('view.charts.instance_activity.header') }}</span>
<HoverCard>
<HoverCardTrigger as-child>
<Info style="margin-left: 4px; font-size: 12px; opacity: 0.7" />
</HoverCardTrigger>
<HoverCardContent side="bottom" align="start" class="w-75">
<div class="tips-popover">
<div>{{ t('view.charts.instance_activity.tips.online_time') }}</div>
<div>{{ t('view.charts.instance_activity.tips.click_Y_axis') }}</div>
<div>{{ t('view.charts.instance_activity.tips.click_instance_name') }}</div>
</div>
</HoverCardContent>
</HoverCard>
</div>
<div>
<TooltipWrapper :content="t('view.charts.instance_activity.refresh')" side="top">
<Button
class="rounded-full"
size="icon"
variant="ghost"
style="margin-right: 5px"
@click="reloadData">
<RefreshCcw />
</Button>
</TooltipWrapper>
<div>
<TooltipWrapper :content="t('view.charts.instance_activity.refresh')" side="top">
<Button
class="rounded-full"
size="icon"
variant="ghost"
style="margin-right: 5px"
@click="reloadData">
<RefreshCcw />
</Button>
</TooltipWrapper>
<Popover>
<PopoverTrigger asChild>
<div>
<TooltipWrapper :content="t('view.charts.instance_activity.settings.header')" side="top">
<Button class="rounded-full" size="icon" variant="ghost" style="margin-right: 5px">
<Settings />
</Button>
</TooltipWrapper>
</div>
</PopoverTrigger>
<PopoverContent side="bottom" class="w-62.5">
<div class="settings">
<Popover>
<PopoverTrigger asChild>
<div>
<span>{{ t('view.charts.instance_activity.settings.bar_width') }}</span>
<TooltipWrapper
:content="t('view.charts.instance_activity.settings.header')"
side="top">
<Button class="rounded-full" size="icon" variant="ghost" style="margin-right: 5px">
<Settings />
</Button>
</TooltipWrapper>
</div>
</PopoverTrigger>
<PopoverContent side="bottom" class="w-62.5">
<div class="settings">
<div>
<Slider
v-model="barWidthDraftValue"
:max="50"
:min="1"
@valueCommit="handleBarWidthCommit"></Slider>
<span>{{ t('view.charts.instance_activity.settings.bar_width') }}</span>
<div>
<Slider
v-model="barWidthDraftValue"
:max="50"
:min="1"
@valueCommit="handleBarWidthCommit"></Slider>
</div>
</div>
<div>
<span>{{ t('view.charts.instance_activity.settings.show_detail') }}</span>
<Switch
v-model="isDetailVisible"
@update:modelValue="
(value) =>
changeIsDetailInstanceVisible(value, () => handleSettingsChange())
" />
</div>
<div v-if="isDetailVisible">
<span>{{ t('view.charts.instance_activity.settings.show_solo_instance') }}</span>
<Switch
v-model="isSoloInstanceVisible"
@update:modelValue="
(value) => changeIsSoloInstanceVisible(value, () => handleSettingsChange())
" />
</div>
<div v-if="isDetailVisible">
<span>{{
t('view.charts.instance_activity.settings.show_no_friend_instance')
}}</span>
<Switch
v-model="isNoFriendInstanceVisible"
@update:modelValue="
(value) =>
changeIsNoFriendInstanceVisible(value, () => handleSettingsChange())
" />
</div>
</div>
<div>
<span>{{ t('view.charts.instance_activity.settings.show_detail') }}</span>
<Switch
v-model="isDetailVisible"
@update:modelValue="
(value) => changeIsDetailInstanceVisible(value, () => handleSettingsChange())
" />
</div>
<div v-if="isDetailVisible">
<span>{{ t('view.charts.instance_activity.settings.show_solo_instance') }}</span>
<Switch
v-model="isSoloInstanceVisible"
@update:modelValue="
(value) => changeIsSoloInstanceVisible(value, () => handleSettingsChange())
" />
</div>
<div v-if="isDetailVisible">
<span>{{ t('view.charts.instance_activity.settings.show_no_friend_instance') }}</span>
<Switch
v-model="isNoFriendInstanceVisible"
@update:modelValue="
(value) => changeIsNoFriendInstanceVisible(value, () => handleSettingsChange())
" />
</div>
</div>
</PopoverContent>
</Popover>
<ButtonGroup class="mr-2">
<TooltipWrapper :content="t('view.charts.instance_activity.previous_day')" side="top">
<Button
variant="outline"
size="icon-sm"
:disabled="isPrevDayBtnDisabled"
@click="changeSelectedDateFromBtn(false)">
<ArrowLeft />
</Button>
</TooltipWrapper>
<TooltipWrapper :content="t('view.charts.instance_activity.next_day')" side="top">
<Button
variant="outline"
size="icon-sm"
:disabled="isNextDayBtnDisabled"
@click="changeSelectedDateFromBtn(true)">
<ArrowRight />
</Button>
</TooltipWrapper>
</ButtonGroup>
<Popover v-model:open="isDatePickerOpen">
<PopoverTrigger asChild>
<div>
</PopoverContent>
</Popover>
<ButtonGroup class="mr-2">
<TooltipWrapper :content="t('view.charts.instance_activity.previous_day')" side="top">
<Button
variant="outline"
class="w-50 justify-start text-left font-normal"
:disabled="isLoading">
<CalendarIcon class="mr-2 h-4 w-4" />
{{ dayjs(selectedDate).format('YYYY-MM-DD') }}
size="icon-sm"
:disabled="isPrevDayBtnDisabled"
@click="changeSelectedDateFromBtn(false)">
<ArrowLeft />
</Button>
</div>
</PopoverTrigger>
<PopoverContent class="w-auto p-0" align="end">
<Calendar
:model-value="calendarModelValue"
:default-placeholder="defaultCalendarPlaceholder"
:is-date-disabled="isCalendarDateDisabled"
:prevent-deselect="true"
initial-focus
@update:modelValue="handleCalendarModelUpdate" />
</PopoverContent>
</Popover>
</div>
</div>
<div class="status-online">
<div class="text-center">
<div class="text-sm text-muted-foreground">
{{ t('view.charts.instance_activity.online_time') }}
</div>
<div class="text-2xl font-semibold">
{{ timeToText(totalOnlineTime, true) }}
</TooltipWrapper>
<TooltipWrapper :content="t('view.charts.instance_activity.next_day')" side="top">
<Button
variant="outline"
size="icon-sm"
:disabled="isNextDayBtnDisabled"
@click="changeSelectedDateFromBtn(true)">
<ArrowRight />
</Button>
</TooltipWrapper>
</ButtonGroup>
<Popover v-model:open="isDatePickerOpen">
<PopoverTrigger asChild>
<div>
<Button
variant="outline"
class="w-50 justify-start text-left font-normal"
:disabled="isLoading">
<CalendarIcon class="mr-2 h-4 w-4" />
{{ dayjs(selectedDate).format('YYYY-MM-DD') }}
</Button>
</div>
</PopoverTrigger>
<PopoverContent class="w-auto p-0" align="end">
<Calendar
:model-value="calendarModelValue"
:default-placeholder="defaultCalendarPlaceholder"
:is-date-disabled="isCalendarDateDisabled"
:prevent-deselect="true"
initial-focus
@update:modelValue="handleCalendarModelUpdate" />
</PopoverContent>
</Popover>
</div>
</div>
<div class="status-online">
<div class="text-center">
<div class="text-sm text-muted-foreground">
{{ t('view.charts.instance_activity.online_time') }}
</div>
<div class="text-2xl font-semibold">
{{ timeToText(totalOnlineTime, true) }}
</div>
</div>
</div>
</div>
<div ref="activityChartRef" style="width: 100%"></div>
<div v-if="!isLoading && activityData.length === 0" class="nodata">
<DataTableEmpty type="nodata" />
</div>
<transition name="el-fade-in-linear">
<div v-show="isDetailVisible && !isLoading && activityData.length !== 0" class="divider">
<div class="flex items-center">
<Separator class="flex-1" />
<span class="px-2 text-muted-foreground">·</span>
<Separator class="flex-1" />
</div>
<div ref="activityChartRef" style="width: 100%"></div>
<div v-if="!isLoading && activityData.length === 0" class="nodata">
<DataTableEmpty type="nodata" />
</div>
</transition>
<template v-if="isDetailVisible && activityData.length !== 0">
<InstanceActivityDetail
v-for="arr in filteredActivityDetailData"
:key="arr[0].location + arr[0].created_at"
ref="activityDetailChartRef"
:activity-detail-data="arr"
:bar-width="barWidth" />
</template>
<transition name="el-fade-in-linear">
<div v-show="isDetailVisible && !isLoading && activityData.length !== 0" class="divider">
<div class="flex items-center">
<Separator class="flex-1" />
<span class="px-2 text-muted-foreground">·</span>
<Separator class="flex-1" />
</div>
</div>
</transition>
<template v-if="isDetailVisible && activityData.length !== 0">
<InstanceActivityDetail
v-for="arr in filteredActivityDetailData"
:key="arr[0].location + arr[0].created_at"
ref="activityDetailChartRef"
:activity-detail-data="arr"
:bar-width="barWidth" />
</template>
</div>
</div>
</template>
<script setup>
defineOptions({ name: 'ChartsInstance' });
import { computed, nextTick, onBeforeMount, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { ArrowLeft, ArrowRight, Calendar as CalendarIcon, Info, RefreshCcw, Settings } from 'lucide-vue-next';
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card';

View File

@@ -1,54 +1,61 @@
<template>
<div class="mt-0 flex min-h-[calc(100vh-140px)] flex-col items-center justify-betweenpt-12" ref="mutualGraphRef">
<div class="flex items-center w-full">
<div
class="options-container mt-2 mb-0 flex flex-wrap items-center gap-3 bg-transparent px-0 pb-2 shadow-none">
<div class="flex flex-wrap items-center gap-2">
<TooltipWrapper :content="fetchButtonLabel" side="top">
<Button :disabled="fetchButtonDisabled" @click="startFetch">
<Spinner v-if="isFetching" />
{{ fetchButtonLabel }}
</Button>
</TooltipWrapper>
<TooltipWrapper
v-if="isFetching"
:content="t('view.charts.mutual_friend.actions.stop_fetching')"
side="top">
<Button variant="destructive" :disabled="status.cancelRequested" @click="cancelFetch">
{{ t('view.charts.mutual_friend.actions.stop') }}
</Button>
</TooltipWrapper>
</div>
</div>
<div
v-if="isFetching"
class="mt-3 grid grid-cols-[repeat(auto-fit,minmax(150px,1fr))] items-center gap-x-3 gap-y-2 rounded-md bg-transparent p-3 ml-auto">
<div class="flex justify-between text-[13px]">
<span>{{ t('view.charts.mutual_friend.progress.friends_processed') }}</span>
<strong>{{ fetchState.processedFriends }} / {{ totalFriends }}</strong>
</div>
<Progress :model-value="progressPercent" class="h-3" />
</div>
</div>
<div id="chart" class="x-container">
<div
v-show="!(hasFetched && !isFetching && !graphReady)"
ref="graphContainerRef"
class="mt-3 h-[calc(100vh-260px)] min-h-[520px] w-full flex-1 rounded-lg bg-transparent"
:style="{ backgroundColor: canvasBackground }"></div>
class="mt-0 flex min-h-[calc(100vh-140px)] flex-col items-center justify-betweenpt-12"
ref="mutualGraphRef">
<div class="flex items-center w-full">
<div
class="options-container mt-2 mb-0 flex flex-wrap items-center gap-3 bg-transparent px-0 pb-2 shadow-none">
<div class="flex flex-wrap items-center gap-2">
<TooltipWrapper :content="fetchButtonLabel" side="top">
<Button :disabled="fetchButtonDisabled" @click="startFetch">
<Spinner v-if="isFetching" />
{{ fetchButtonLabel }}
</Button>
</TooltipWrapper>
<Empty v-if="hasFetched && !isFetching && !graphReady" class="mt-3 w-full flex-1">
<EmptyHeader>
<EmptyDescription>
{{ t('view.charts.mutual_friend.progress.no_relationships_discovered') }}
</EmptyDescription>
</EmptyHeader>
</Empty>
<TooltipWrapper
v-if="isFetching"
:content="t('view.charts.mutual_friend.actions.stop_fetching')"
side="top">
<Button variant="destructive" :disabled="status.cancelRequested" @click="cancelFetch">
{{ t('view.charts.mutual_friend.actions.stop') }}
</Button>
</TooltipWrapper>
</div>
</div>
<div
v-if="isFetching"
class="mt-3 grid grid-cols-[repeat(auto-fit,minmax(150px,1fr))] items-center gap-x-3 gap-y-2 rounded-md bg-transparent p-3 ml-auto">
<div class="flex justify-between text-[13px]">
<span>{{ t('view.charts.mutual_friend.progress.friends_processed') }}</span>
<strong>{{ fetchState.processedFriends }} / {{ totalFriends }}</strong>
</div>
<Progress :model-value="progressPercent" class="h-3" />
</div>
</div>
<div
v-show="!(hasFetched && !isFetching && !graphReady)"
ref="graphContainerRef"
class="mt-3 h-[calc(100vh-260px)] min-h-[520px] w-full flex-1 rounded-lg bg-transparent"
:style="{ backgroundColor: canvasBackground }"></div>
<Empty v-if="hasFetched && !isFetching && !graphReady" class="mt-3 w-full flex-1">
<EmptyHeader>
<EmptyDescription>
{{ t('view.charts.mutual_friend.progress.no_relationships_discovered') }}
</EmptyDescription>
</EmptyHeader>
</Empty>
</div>
<BackToTop target="#chart" :right="30" :bottom="30" />
</div>
</template>
<script setup>
defineOptions({ name: 'ChartsMutual' });
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { Empty, EmptyDescription, EmptyHeader } from '@/components/ui/empty';
import { Button } from '@/components/ui/button';
@@ -60,6 +67,7 @@
import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n';
import BackToTop from '@/components/BackToTop.vue';
import Graph from 'graphology';
import Sigma from 'sigma';
import forceAtlas2 from 'graphology-layout-forceatlas2';
@@ -86,7 +94,6 @@
const appearanceStore = useAppearanceSettingsStore();
const { friends } = storeToRefs(friendStore);
const { currentUser } = storeToRefs(userStore);
const { activeTab } = storeToRefs(chartsStore);
const { isDarkMode } = storeToRefs(appearanceStore);
const cachedUsers = userStore.cachedUsers;
const showUserDialog = (userId) => userStore.showUserDialog(userId);
@@ -201,21 +208,12 @@
if (mutualGraphResizeObserver) mutualGraphResizeObserver.disconnect();
});
watch(
activeTab,
(tab) => {
if (tab === 'mutual') loadGraphFromDatabase();
},
{ immediate: true }
);
watch(
() => watchState.isFriendsLoaded,
(isFriendsLoaded) => {
if (isFriendsLoaded && activeTab.value === 'mutual') {
loadGraphFromDatabase();
}
}
if (isFriendsLoaded) loadGraphFromDatabase();
},
{ immediate: true }
);
function showStatusMessage(message, type = 'info') {

View File

@@ -23,7 +23,7 @@
<template #default="{ layout }">
<ResizablePanel :default-size="mainDefaultSize" :order="1">
<RouterView v-slot="{ Component }">
<KeepAlive exclude="Charts">
<KeepAlive exclude="ChartsInstance, ChartsMutual">
<component :is="Component" />
</KeepAlive>
</RouterView>