mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-05-07 06:56:04 +02:00
split charts into separate routes (#1605)
This commit is contained in:
@@ -374,7 +374,14 @@
|
|||||||
items: ['friend-log', 'friend-list', 'moderation']
|
items: ['friend-log', 'friend-list', 'moderation']
|
||||||
},
|
},
|
||||||
{ type: 'item', key: 'notification' },
|
{ type: 'item', key: 'notification' },
|
||||||
{ type: 'item', key: 'charts' },
|
{
|
||||||
|
type: 'folder',
|
||||||
|
id: 'default-folder-charts',
|
||||||
|
nameKey: 'nav_tooltip.charts',
|
||||||
|
name: t('nav_tooltip.charts'),
|
||||||
|
icon: 'ri-pie-chart-line',
|
||||||
|
items: ['charts-instance', 'charts-mutual']
|
||||||
|
},
|
||||||
{ type: 'item', key: 'tools' },
|
{ type: 'item', key: 'tools' },
|
||||||
{ type: 'item', key: 'direct-access' }
|
{ type: 'item', key: 'direct-access' }
|
||||||
];
|
];
|
||||||
@@ -504,6 +511,7 @@
|
|||||||
const sanitizeLayout = (layout) => {
|
const sanitizeLayout = (layout) => {
|
||||||
const usedKeys = new Set();
|
const usedKeys = new Set();
|
||||||
const normalized = [];
|
const normalized = [];
|
||||||
|
const chartsKeys = ['charts-instance', 'charts-mutual'];
|
||||||
|
|
||||||
const appendItemEntry = (key, target = normalized) => {
|
const appendItemEntry = (key, target = normalized) => {
|
||||||
if (!key || usedKeys.has(key) || !navDefinitionMap.has(key)) {
|
if (!key || usedKeys.has(key) || !navDefinitionMap.has(key)) {
|
||||||
@@ -513,9 +521,31 @@
|
|||||||
usedKeys.add(key);
|
usedKeys.add(key);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const appendChartsFolder = (target = normalized) => {
|
||||||
|
if (chartsKeys.some((key) => usedKeys.has(key))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!chartsKeys.every((key) => navDefinitionMap.has(key))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
chartsKeys.forEach((key) => usedKeys.add(key));
|
||||||
|
target.push({
|
||||||
|
type: 'folder',
|
||||||
|
id: 'default-folder-charts',
|
||||||
|
nameKey: 'nav_tooltip.charts',
|
||||||
|
name: t('nav_tooltip.charts'),
|
||||||
|
icon: 'ri-pie-chart-line',
|
||||||
|
items: [...chartsKeys]
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
if (Array.isArray(layout)) {
|
if (Array.isArray(layout)) {
|
||||||
layout.forEach((entry) => {
|
layout.forEach((entry) => {
|
||||||
if (entry?.type === 'item') {
|
if (entry?.type === 'item') {
|
||||||
|
if (entry.key === 'charts') {
|
||||||
|
appendChartsFolder();
|
||||||
|
return;
|
||||||
|
}
|
||||||
appendItemEntry(entry.key);
|
appendItemEntry(entry.key);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -550,11 +580,17 @@
|
|||||||
|
|
||||||
navDefinitions.forEach((item) => {
|
navDefinitions.forEach((item) => {
|
||||||
if (!usedKeys.has(item.key)) {
|
if (!usedKeys.has(item.key)) {
|
||||||
normalized.push({ type: 'item', key: item.key });
|
if (chartsKeys.includes(item.key)) {
|
||||||
usedKeys.add(item.key);
|
return;
|
||||||
|
}
|
||||||
|
appendItemEntry(item.key);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!chartsKeys.some((key) => usedKeys.has(key))) {
|
||||||
|
appendChartsFolder();
|
||||||
|
}
|
||||||
|
|
||||||
return normalized;
|
return normalized;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -670,7 +706,11 @@
|
|||||||
console.error('Failed to load custom nav', error);
|
console.error('Failed to load custom nav', error);
|
||||||
} finally {
|
} finally {
|
||||||
const fallbackLayout = layoutData?.length ? layoutData : createDefaultNavLayout();
|
const fallbackLayout = layoutData?.length ? layoutData : createDefaultNavLayout();
|
||||||
navLayout.value = sanitizeLayout(fallbackLayout);
|
const sanitized = sanitizeLayout(fallbackLayout);
|
||||||
|
navLayout.value = sanitized;
|
||||||
|
if (layoutData?.length && JSON.stringify(sanitized) !== JSON.stringify(fallbackLayout)) {
|
||||||
|
await saveNavLayout(sanitized);
|
||||||
|
}
|
||||||
navLayoutReady.value = true;
|
navLayoutReady.value = true;
|
||||||
navigateToFirstNavEntry();
|
navigateToFirstNavEntry();
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-5
@@ -2,8 +2,6 @@ import { createRouter, createWebHashHistory } from 'vue-router';
|
|||||||
|
|
||||||
import { watchState } from './../service/watchState';
|
import { watchState } from './../service/watchState';
|
||||||
|
|
||||||
import MainLayout from '../views/Layout/MainLayout.vue';
|
|
||||||
import Charts from './../views/Charts/Charts.vue';
|
|
||||||
import FavoritesAvatar from './../views/Favorites/FavoritesAvatar.vue';
|
import FavoritesAvatar from './../views/Favorites/FavoritesAvatar.vue';
|
||||||
import FavoritesFriend from './../views/Favorites/FavoritesFriend.vue';
|
import FavoritesFriend from './../views/Favorites/FavoritesFriend.vue';
|
||||||
import FavoritesWorld from './../views/Favorites/FavoritesWorld.vue';
|
import FavoritesWorld from './../views/Favorites/FavoritesWorld.vue';
|
||||||
@@ -11,16 +9,17 @@ import Feed from './../views/Feed/Feed.vue';
|
|||||||
import FriendList from './../views/FriendList/FriendList.vue';
|
import FriendList from './../views/FriendList/FriendList.vue';
|
||||||
import FriendLog from './../views/FriendLog/FriendLog.vue';
|
import FriendLog from './../views/FriendLog/FriendLog.vue';
|
||||||
import FriendsLocations from './../views/FriendsLocations/FriendsLocations.vue';
|
import FriendsLocations from './../views/FriendsLocations/FriendsLocations.vue';
|
||||||
|
import Gallery from './../views/Tools/Gallery.vue';
|
||||||
import GameLog from './../views/GameLog/GameLog.vue';
|
import GameLog from './../views/GameLog/GameLog.vue';
|
||||||
import Login from './../views/Login/Login.vue';
|
import Login from './../views/Login/Login.vue';
|
||||||
|
import MainLayout from '../views/Layout/MainLayout.vue';
|
||||||
import Moderation from './../views/Moderation/Moderation.vue';
|
import Moderation from './../views/Moderation/Moderation.vue';
|
||||||
import Notification from './../views/Notifications/Notification.vue';
|
import Notification from './../views/Notifications/Notification.vue';
|
||||||
import PlayerList from './../views/PlayerList/PlayerList.vue';
|
import PlayerList from './../views/PlayerList/PlayerList.vue';
|
||||||
|
import ScreenshotMetadata from './../views/Tools/ScreenshotMetadata.vue';
|
||||||
import Search from './../views/Search/Search.vue';
|
import Search from './../views/Search/Search.vue';
|
||||||
import Settings from './../views/Settings/Settings.vue';
|
import Settings from './../views/Settings/Settings.vue';
|
||||||
import Tools from './../views/Tools/Tools.vue';
|
import Tools from './../views/Tools/Tools.vue';
|
||||||
import Gallery from './../views/Tools/Gallery.vue';
|
|
||||||
import ScreenshotMetadata from './../views/Tools/ScreenshotMetadata.vue';
|
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
@@ -82,7 +81,19 @@ const routes = [
|
|||||||
{
|
{
|
||||||
path: 'charts',
|
path: 'charts',
|
||||||
name: 'charts',
|
name: 'charts',
|
||||||
component: Charts
|
redirect: { name: 'charts-instance' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'charts/instance',
|
||||||
|
name: 'charts-instance',
|
||||||
|
component: () =>
|
||||||
|
import('./../views/Charts/components/InstanceActivity.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'charts/mutual',
|
||||||
|
name: 'charts-mutual',
|
||||||
|
component: () =>
|
||||||
|
import('./../views/Charts/components/MutualFriends.vue')
|
||||||
},
|
},
|
||||||
{ path: 'tools', name: 'tools', component: Tools },
|
{ path: 'tools', name: 'tools', component: Tools },
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -84,11 +84,18 @@ const navDefinitions = [
|
|||||||
routeName: 'notification'
|
routeName: 'notification'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'charts',
|
key: 'charts-instance',
|
||||||
icon: 'ri-bar-chart-line',
|
icon: 'ri-bar-chart-horizontal-line',
|
||||||
tooltip: 'nav_tooltip.charts',
|
tooltip: 'view.charts.instance_activity.header',
|
||||||
labelKey: 'nav_tooltip.charts',
|
labelKey: 'view.charts.instance_activity.header',
|
||||||
routeName: 'charts'
|
routeName: 'charts-instance'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'charts-mutual',
|
||||||
|
icon: 'ri-group-2-line',
|
||||||
|
tooltip: 'view.charts.mutual_friend.tab_label',
|
||||||
|
labelKey: 'view.charts.mutual_friend.tab_label',
|
||||||
|
routeName: 'charts-mutual'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'tools',
|
key: 'tools',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { computed, reactive, ref, watch } from 'vue';
|
import { computed, reactive, watch } from 'vue';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
@@ -16,7 +16,6 @@ export const useChartsStore = defineStore('Charts', () => {
|
|||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const activeTab = ref('instance');
|
|
||||||
const mutualGraphFetchState = reactive(createDefaultFetchState());
|
const mutualGraphFetchState = reactive(createDefaultFetchState());
|
||||||
const mutualGraphStatus = reactive({
|
const mutualGraphStatus = reactive({
|
||||||
isFetching: false,
|
isFetching: false,
|
||||||
@@ -94,7 +93,6 @@ export const useChartsStore = defineStore('Charts', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
activeTab,
|
|
||||||
mutualGraphFetchState,
|
mutualGraphFetchState,
|
||||||
mutualGraphStatus,
|
mutualGraphStatus,
|
||||||
resetMutualGraphState
|
resetMutualGraphState
|
||||||
|
|||||||
@@ -97,11 +97,13 @@ export const useAppearanceSettingsStore = defineStore(
|
|||||||
const isNavCollapsed = ref(true);
|
const isNavCollapsed = ref(true);
|
||||||
const isSideBarTabShow = computed(() => {
|
const isSideBarTabShow = computed(() => {
|
||||||
const currentRouteName = router.currentRoute.value?.name;
|
const currentRouteName = router.currentRoute.value?.name;
|
||||||
return !(
|
return ![
|
||||||
currentRouteName === 'friends-locations' ||
|
'friends-locations',
|
||||||
currentRouteName === 'friend-list' ||
|
'friend-list',
|
||||||
currentRouteName === 'charts'
|
'charts',
|
||||||
);
|
'charts-instance',
|
||||||
|
'charts-mutual'
|
||||||
|
].includes(currentRouteName);
|
||||||
});
|
});
|
||||||
|
|
||||||
const isDataTableStriped = ref(false);
|
const isDataTableStriped = ref(false);
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -1,165 +1,175 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="instanceActivityRef" class="pt-12">
|
<div id="chart" class="x-container">
|
||||||
<BackToTop :target="instanceActivityRef" :right="30" :bottom="30" :teleport="false" />
|
<div ref="instanceActivityRef" class="pt-12">
|
||||||
<div class="options-container instance-activity" style="margin-top: 0">
|
<BackToTop :target="instanceActivityRef" :right="30" :bottom="30" :teleport="false" />
|
||||||
<div>
|
<div class="options-container instance-activity" style="margin-top: 0">
|
||||||
<span>{{ t('view.charts.instance_activity.header') }}</span>
|
<div>
|
||||||
<HoverCard>
|
<span>{{ t('view.charts.instance_activity.header') }}</span>
|
||||||
<HoverCardTrigger as-child>
|
<HoverCard>
|
||||||
<Info style="margin-left: 4px; font-size: 12px; opacity: 0.7" />
|
<HoverCardTrigger as-child>
|
||||||
</HoverCardTrigger>
|
<Info style="margin-left: 4px; font-size: 12px; opacity: 0.7" />
|
||||||
<HoverCardContent side="bottom" align="start" class="w-75">
|
</HoverCardTrigger>
|
||||||
<div class="tips-popover">
|
<HoverCardContent side="bottom" align="start" class="w-75">
|
||||||
<div>{{ t('view.charts.instance_activity.tips.online_time') }}</div>
|
<div class="tips-popover">
|
||||||
<div>{{ t('view.charts.instance_activity.tips.click_Y_axis') }}</div>
|
<div>{{ t('view.charts.instance_activity.tips.online_time') }}</div>
|
||||||
<div>{{ t('view.charts.instance_activity.tips.click_instance_name') }}</div>
|
<div>{{ t('view.charts.instance_activity.tips.click_Y_axis') }}</div>
|
||||||
</div>
|
<div>{{ t('view.charts.instance_activity.tips.click_instance_name') }}</div>
|
||||||
</HoverCardContent>
|
</div>
|
||||||
</HoverCard>
|
</HoverCardContent>
|
||||||
</div>
|
</HoverCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<TooltipWrapper :content="t('view.charts.instance_activity.refresh')" side="top">
|
<TooltipWrapper :content="t('view.charts.instance_activity.refresh')" side="top">
|
||||||
<Button
|
<Button
|
||||||
class="rounded-full"
|
class="rounded-full"
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
style="margin-right: 5px"
|
style="margin-right: 5px"
|
||||||
@click="reloadData">
|
@click="reloadData">
|
||||||
<RefreshCcw />
|
<RefreshCcw />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipWrapper>
|
</TooltipWrapper>
|
||||||
|
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<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">
|
|
||||||
<div>
|
<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>
|
<div>
|
||||||
<Slider
|
<span>{{ t('view.charts.instance_activity.settings.bar_width') }}</span>
|
||||||
v-model="barWidthDraftValue"
|
<div>
|
||||||
:max="50"
|
<Slider
|
||||||
:min="1"
|
v-model="barWidthDraftValue"
|
||||||
@valueCommit="handleBarWidthCommit"></Slider>
|
: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>
|
</div>
|
||||||
<div>
|
</PopoverContent>
|
||||||
<span>{{ t('view.charts.instance_activity.settings.show_detail') }}</span>
|
</Popover>
|
||||||
<Switch
|
<ButtonGroup class="mr-2">
|
||||||
v-model="isDetailVisible"
|
<TooltipWrapper :content="t('view.charts.instance_activity.previous_day')" side="top">
|
||||||
@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>
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
class="w-50 justify-start text-left font-normal"
|
size="icon-sm"
|
||||||
:disabled="isLoading">
|
:disabled="isPrevDayBtnDisabled"
|
||||||
<CalendarIcon class="mr-2 h-4 w-4" />
|
@click="changeSelectedDateFromBtn(false)">
|
||||||
{{ dayjs(selectedDate).format('YYYY-MM-DD') }}
|
<ArrowLeft />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</TooltipWrapper>
|
||||||
</PopoverTrigger>
|
<TooltipWrapper :content="t('view.charts.instance_activity.next_day')" side="top">
|
||||||
<PopoverContent class="w-auto p-0" align="end">
|
<Button
|
||||||
<Calendar
|
variant="outline"
|
||||||
:model-value="calendarModelValue"
|
size="icon-sm"
|
||||||
:default-placeholder="defaultCalendarPlaceholder"
|
:disabled="isNextDayBtnDisabled"
|
||||||
:is-date-disabled="isCalendarDateDisabled"
|
@click="changeSelectedDateFromBtn(true)">
|
||||||
:prevent-deselect="true"
|
<ArrowRight />
|
||||||
initial-focus
|
</Button>
|
||||||
@update:modelValue="handleCalendarModelUpdate" />
|
</TooltipWrapper>
|
||||||
</PopoverContent>
|
</ButtonGroup>
|
||||||
</Popover>
|
<Popover v-model:open="isDatePickerOpen">
|
||||||
</div>
|
<PopoverTrigger asChild>
|
||||||
</div>
|
<div>
|
||||||
<div class="status-online">
|
<Button
|
||||||
<div class="text-center">
|
variant="outline"
|
||||||
<div class="text-sm text-muted-foreground">
|
class="w-50 justify-start text-left font-normal"
|
||||||
{{ t('view.charts.instance_activity.online_time') }}
|
:disabled="isLoading">
|
||||||
</div>
|
<CalendarIcon class="mr-2 h-4 w-4" />
|
||||||
<div class="text-2xl font-semibold">
|
{{ dayjs(selectedDate).format('YYYY-MM-DD') }}
|
||||||
{{ timeToText(totalOnlineTime, true) }}
|
</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>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div ref="activityChartRef" style="width: 100%"></div>
|
<div ref="activityChartRef" style="width: 100%"></div>
|
||||||
<div v-if="!isLoading && activityData.length === 0" class="nodata">
|
<div v-if="!isLoading && activityData.length === 0" class="nodata">
|
||||||
<DataTableEmpty type="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>
|
</div>
|
||||||
</transition>
|
|
||||||
<template v-if="isDetailVisible && activityData.length !== 0">
|
<transition name="el-fade-in-linear">
|
||||||
<InstanceActivityDetail
|
<div v-show="isDetailVisible && !isLoading && activityData.length !== 0" class="divider">
|
||||||
v-for="arr in filteredActivityDetailData"
|
<div class="flex items-center">
|
||||||
:key="arr[0].location + arr[0].created_at"
|
<Separator class="flex-1" />
|
||||||
ref="activityDetailChartRef"
|
<span class="px-2 text-muted-foreground">·</span>
|
||||||
:activity-detail-data="arr"
|
<Separator class="flex-1" />
|
||||||
:bar-width="barWidth" />
|
</div>
|
||||||
</template>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
defineOptions({ name: 'ChartsInstance' });
|
||||||
|
|
||||||
import { computed, nextTick, onBeforeMount, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
import { computed, nextTick, onBeforeMount, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
import { ArrowLeft, ArrowRight, Calendar as CalendarIcon, Info, RefreshCcw, Settings } from 'lucide-vue-next';
|
import { ArrowLeft, ArrowRight, Calendar as CalendarIcon, Info, RefreshCcw, Settings } from 'lucide-vue-next';
|
||||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card';
|
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card';
|
||||||
|
|||||||
@@ -1,54 +1,61 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mt-0 flex min-h-[calc(100vh-140px)] flex-col items-center justify-betweenpt-12" ref="mutualGraphRef">
|
<div id="chart" class="x-container">
|
||||||
<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
|
<div
|
||||||
v-show="!(hasFetched && !isFetching && !graphReady)"
|
class="mt-0 flex min-h-[calc(100vh-140px)] flex-col items-center justify-betweenpt-12"
|
||||||
ref="graphContainerRef"
|
ref="mutualGraphRef">
|
||||||
class="mt-3 h-[calc(100vh-260px)] min-h-[520px] w-full flex-1 rounded-lg bg-transparent"
|
<div class="flex items-center w-full">
|
||||||
:style="{ backgroundColor: canvasBackground }"></div>
|
<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">
|
<TooltipWrapper
|
||||||
<EmptyHeader>
|
v-if="isFetching"
|
||||||
<EmptyDescription>
|
:content="t('view.charts.mutual_friend.actions.stop_fetching')"
|
||||||
{{ t('view.charts.mutual_friend.progress.no_relationships_discovered') }}
|
side="top">
|
||||||
</EmptyDescription>
|
<Button variant="destructive" :disabled="status.cancelRequested" @click="cancelFetch">
|
||||||
</EmptyHeader>
|
{{ t('view.charts.mutual_friend.actions.stop') }}
|
||||||
</Empty>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
defineOptions({ name: 'ChartsMutual' });
|
||||||
|
|
||||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
import { Empty, EmptyDescription, EmptyHeader } from '@/components/ui/empty';
|
import { Empty, EmptyDescription, EmptyHeader } from '@/components/ui/empty';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@@ -60,6 +67,7 @@
|
|||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
import BackToTop from '@/components/BackToTop.vue';
|
||||||
import Graph from 'graphology';
|
import Graph from 'graphology';
|
||||||
import Sigma from 'sigma';
|
import Sigma from 'sigma';
|
||||||
import forceAtlas2 from 'graphology-layout-forceatlas2';
|
import forceAtlas2 from 'graphology-layout-forceatlas2';
|
||||||
@@ -86,7 +94,6 @@
|
|||||||
const appearanceStore = useAppearanceSettingsStore();
|
const appearanceStore = useAppearanceSettingsStore();
|
||||||
const { friends } = storeToRefs(friendStore);
|
const { friends } = storeToRefs(friendStore);
|
||||||
const { currentUser } = storeToRefs(userStore);
|
const { currentUser } = storeToRefs(userStore);
|
||||||
const { activeTab } = storeToRefs(chartsStore);
|
|
||||||
const { isDarkMode } = storeToRefs(appearanceStore);
|
const { isDarkMode } = storeToRefs(appearanceStore);
|
||||||
const cachedUsers = userStore.cachedUsers;
|
const cachedUsers = userStore.cachedUsers;
|
||||||
const showUserDialog = (userId) => userStore.showUserDialog(userId);
|
const showUserDialog = (userId) => userStore.showUserDialog(userId);
|
||||||
@@ -201,21 +208,12 @@
|
|||||||
if (mutualGraphResizeObserver) mutualGraphResizeObserver.disconnect();
|
if (mutualGraphResizeObserver) mutualGraphResizeObserver.disconnect();
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
|
||||||
activeTab,
|
|
||||||
(tab) => {
|
|
||||||
if (tab === 'mutual') loadGraphFromDatabase();
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => watchState.isFriendsLoaded,
|
() => watchState.isFriendsLoaded,
|
||||||
(isFriendsLoaded) => {
|
(isFriendsLoaded) => {
|
||||||
if (isFriendsLoaded && activeTab.value === 'mutual') {
|
if (isFriendsLoaded) loadGraphFromDatabase();
|
||||||
loadGraphFromDatabase();
|
},
|
||||||
}
|
{ immediate: true }
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
function showStatusMessage(message, type = 'info') {
|
function showStatusMessage(message, type = 'info') {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<template #default="{ layout }">
|
<template #default="{ layout }">
|
||||||
<ResizablePanel :default-size="mainDefaultSize" :order="1">
|
<ResizablePanel :default-size="mainDefaultSize" :order="1">
|
||||||
<RouterView v-slot="{ Component }">
|
<RouterView v-slot="{ Component }">
|
||||||
<KeepAlive exclude="Charts">
|
<KeepAlive exclude="ChartsInstance, ChartsMutual">
|
||||||
<component :is="Component" />
|
<component :is="Component" />
|
||||||
</KeepAlive>
|
</KeepAlive>
|
||||||
</RouterView>
|
</RouterView>
|
||||||
|
|||||||
Reference in New Issue
Block a user