mirror of
https://github.com/vrcx-team/VRCX.git
synced 2026-04-06 00:32:02 +02:00
feat: add hot worlds
This commit is contained in:
@@ -14,6 +14,7 @@ const testDefinitions = [
|
||||
{ key: 'tools', routeName: 'tools' },
|
||||
{ key: 'charts-instance', routeName: 'charts-instance' },
|
||||
{ key: 'charts-mutual', routeName: 'charts-mutual' },
|
||||
{ key: 'charts-hot-worlds', routeName: 'charts-hot-worlds' },
|
||||
{ key: 'notification', routeName: 'notification' },
|
||||
{ key: 'direct-access', action: 'direct-access' }
|
||||
];
|
||||
@@ -283,7 +284,8 @@ describe('sanitizeLayout', () => {
|
||||
expect(chartsFolder).toBeDefined();
|
||||
expect(chartsFolder.items).toEqual([
|
||||
'charts-instance',
|
||||
'charts-mutual'
|
||||
'charts-mutual',
|
||||
'charts-hot-worlds'
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ export function createBaseDefaultNavLayout(t) {
|
||||
nameKey: 'nav_tooltip.charts',
|
||||
name: t('nav_tooltip.charts'),
|
||||
icon: 'ri-pie-chart-line',
|
||||
items: ['charts-instance', 'charts-mutual']
|
||||
items: ['charts-instance', 'charts-mutual', 'charts-hot-worlds']
|
||||
},
|
||||
{ type: 'item', key: 'tools' },
|
||||
{ type: 'item', key: 'direct-access' }
|
||||
|
||||
@@ -44,7 +44,7 @@ export function sanitizeLayout(
|
||||
const normalizedHiddenKeys = normalizeHiddenKeys(hiddenKeys, definitionMap);
|
||||
const hiddenSet = new Set(normalizedHiddenKeys);
|
||||
const normalized = [];
|
||||
const chartsKeys = ['charts-instance', 'charts-mutual'];
|
||||
const chartsKeys = ['charts-instance', 'charts-mutual', 'charts-hot-worlds'];
|
||||
|
||||
const appendItemEntry = (key, target = normalized) => {
|
||||
if (!key || usedKeys.has(key) || !definitionMap.has(key)) {
|
||||
|
||||
@@ -542,6 +542,39 @@
|
||||
"exclude_friends_placeholder": "Select friends to exclude",
|
||||
"exclude_friends_help": "Selected friends will be hidden from the graph."
|
||||
}
|
||||
},
|
||||
"hot_worlds": {
|
||||
"tab_label": "Hot Worlds",
|
||||
"header": "Hot Worlds",
|
||||
"refresh": "Refresh",
|
||||
"sorted_by": "Sorted by unique friends",
|
||||
"tips": {
|
||||
"description": "Shows which worlds your friends have been visiting the most. Ranked by the number of unique friends who visited. Trend compares the first half vs second half of the selected period."
|
||||
},
|
||||
"period": {
|
||||
"days_7": "Last 7d",
|
||||
"days_30": "Last 30d",
|
||||
"days_90": "Last 90d"
|
||||
},
|
||||
"trend": {
|
||||
"rising": "Rising",
|
||||
"cooling": "Cooling"
|
||||
},
|
||||
"stats_line": {
|
||||
"friends": "{count} friends",
|
||||
"visits": "{count} visits"
|
||||
},
|
||||
"stats": {
|
||||
"top_friends": "top friends",
|
||||
"total_visits": "total visits",
|
||||
"rising": "rising",
|
||||
"cooling": "cooling"
|
||||
},
|
||||
"sheet": {
|
||||
"friends_who_visited": "Friends who visited"
|
||||
},
|
||||
"no_friend_data": "No friend visit data",
|
||||
"friend_visits": "{count} visits"
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
|
||||
@@ -109,6 +109,12 @@ const routes = [
|
||||
component: () =>
|
||||
import('./../views/Charts/components/MutualFriends.vue')
|
||||
},
|
||||
{
|
||||
path: 'charts/hot-worlds',
|
||||
name: 'charts-hot-worlds',
|
||||
component: () =>
|
||||
import('./../views/Charts/components/HotWorlds.vue')
|
||||
},
|
||||
{ path: 'tools', name: 'tools', component: Tools },
|
||||
{
|
||||
path: 'tools/gallery',
|
||||
|
||||
@@ -604,6 +604,133 @@ const feed = {
|
||||
{ '@userId': userId }
|
||||
);
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {number} days - Number of days to look back
|
||||
* @param {number} limit - Max number of worlds to return
|
||||
* @returns {Promise<Array>} Ranked list of hot worlds
|
||||
*/
|
||||
async getHotWorlds(days = 30, limit = 30) {
|
||||
const halfDays = Math.floor(days / 2);
|
||||
const results = [];
|
||||
await sqliteService.execute(
|
||||
(dbRow) => {
|
||||
results.push({
|
||||
worldId: dbRow[0],
|
||||
worldName: dbRow[1],
|
||||
visitCount: dbRow[2],
|
||||
uniqueFriends: dbRow[3],
|
||||
lastVisited: dbRow[4]
|
||||
});
|
||||
},
|
||||
`SELECT
|
||||
SUBSTR(location, 1, INSTR(location, ':') - 1) AS world_id,
|
||||
world_name,
|
||||
COUNT(*) AS visit_count,
|
||||
COUNT(DISTINCT user_id) AS unique_friends,
|
||||
MAX(created_at) AS last_visited
|
||||
FROM ${dbVars.userPrefix}_feed_gps
|
||||
WHERE created_at >= datetime('now', @daysOffset)
|
||||
AND location LIKE 'wrld_%'
|
||||
AND INSTR(location, ':') > 0
|
||||
AND world_name IS NOT NULL AND world_name != ''
|
||||
GROUP BY world_id
|
||||
ORDER BY unique_friends DESC, visit_count DESC
|
||||
LIMIT @limit`,
|
||||
{
|
||||
'@daysOffset': `-${days} days`,
|
||||
'@limit': limit
|
||||
}
|
||||
);
|
||||
|
||||
const trendMap = new Map();
|
||||
await sqliteService.execute(
|
||||
(dbRow) => {
|
||||
trendMap.set(dbRow[0], dbRow[1]);
|
||||
},
|
||||
`SELECT
|
||||
SUBSTR(location, 1, INSTR(location, ':') - 1) AS world_id,
|
||||
COUNT(DISTINCT user_id) AS unique_friends
|
||||
FROM ${dbVars.userPrefix}_feed_gps
|
||||
WHERE created_at >= datetime('now', @daysOffset)
|
||||
AND created_at < datetime('now', @halfOffset)
|
||||
AND location LIKE 'wrld_%'
|
||||
AND INSTR(location, ':') > 0
|
||||
AND world_name IS NOT NULL AND world_name != ''
|
||||
GROUP BY world_id`,
|
||||
{
|
||||
'@daysOffset': `-${days} days`,
|
||||
'@halfOffset': `-${halfDays} days`
|
||||
}
|
||||
);
|
||||
|
||||
const recentMap = new Map();
|
||||
await sqliteService.execute(
|
||||
(dbRow) => {
|
||||
recentMap.set(dbRow[0], dbRow[1]);
|
||||
},
|
||||
`SELECT
|
||||
SUBSTR(location, 1, INSTR(location, ':') - 1) AS world_id,
|
||||
COUNT(DISTINCT user_id) AS unique_friends
|
||||
FROM ${dbVars.userPrefix}_feed_gps
|
||||
WHERE created_at >= datetime('now', @halfOffset)
|
||||
AND location LIKE 'wrld_%'
|
||||
AND INSTR(location, ':') > 0
|
||||
AND world_name IS NOT NULL AND world_name != ''
|
||||
GROUP BY world_id`,
|
||||
{
|
||||
'@halfOffset': `-${halfDays} days`
|
||||
}
|
||||
);
|
||||
|
||||
for (const world of results) {
|
||||
const oldFriends = trendMap.get(world.worldId) || 0;
|
||||
const newFriends = recentMap.get(world.worldId) || 0;
|
||||
if (newFriends > oldFriends) {
|
||||
world.trend = 'rising';
|
||||
} else if (newFriends < oldFriends) {
|
||||
world.trend = 'cooling';
|
||||
} else {
|
||||
world.trend = 'stable';
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} worldId - The world ID (e.g. wrld_xxx)
|
||||
* @param {number} days - Number of days to look back
|
||||
* @returns {Promise<Array>} List of friends who visited
|
||||
*/
|
||||
async getHotWorldFriendDetail(worldId, days = 30) {
|
||||
const results = [];
|
||||
await sqliteService.execute(
|
||||
(dbRow) => {
|
||||
results.push({
|
||||
userId: dbRow[0],
|
||||
displayName: dbRow[1],
|
||||
visitCount: dbRow[2],
|
||||
lastVisit: dbRow[3]
|
||||
});
|
||||
},
|
||||
`SELECT
|
||||
user_id,
|
||||
display_name,
|
||||
COUNT(*) AS visit_count,
|
||||
MAX(created_at) AS last_visit
|
||||
FROM ${dbVars.userPrefix}_feed_gps
|
||||
WHERE SUBSTR(location, 1, INSTR(location, ':') - 1) = @worldId
|
||||
AND created_at >= datetime('now', @daysOffset)
|
||||
GROUP BY user_id
|
||||
ORDER BY visit_count DESC`,
|
||||
{
|
||||
'@worldId': worldId,
|
||||
'@daysOffset': `-${days} days`
|
||||
}
|
||||
);
|
||||
return results;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -106,6 +106,13 @@ const navDefinitions = [
|
||||
labelKey: 'view.charts.mutual_friend.tab_label',
|
||||
routeName: 'charts-mutual'
|
||||
},
|
||||
{
|
||||
key: 'charts-hot-worlds',
|
||||
icon: 'ri-fire-line',
|
||||
tooltip: 'view.charts.hot_worlds.tab_label',
|
||||
labelKey: 'view.charts.hot_worlds.tab_label',
|
||||
routeName: 'charts-hot-worlds'
|
||||
},
|
||||
{
|
||||
key: 'tools',
|
||||
icon: 'ri-tools-line',
|
||||
|
||||
@@ -109,7 +109,8 @@ export const useAppearanceSettingsStore = defineStore(
|
||||
'friends-locations',
|
||||
'friend-list',
|
||||
'charts-instance',
|
||||
'charts-mutual'
|
||||
'charts-mutual',
|
||||
'charts-hot-worlds'
|
||||
].includes(currentRouteName);
|
||||
});
|
||||
|
||||
|
||||
331
src/views/Charts/components/HotWorlds.vue
Normal file
331
src/views/Charts/components/HotWorlds.vue
Normal file
@@ -0,0 +1,331 @@
|
||||
<template>
|
||||
<div id="chart" class="x-container">
|
||||
<div ref="hotWorldsRef" class="pt-4">
|
||||
<BackToTop :target="hotWorldsRef" :right="30" :bottom="30" :teleport="false" />
|
||||
<div class="options-container mt-0 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<span class="shrink-0">{{ t('view.charts.hot_worlds.header') }}</span>
|
||||
<HoverCard>
|
||||
<HoverCardTrigger as-child>
|
||||
<Info class="ml-1 text-xs opacity-70" />
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent side="bottom" align="start" class="w-75">
|
||||
<div class="text-xs">
|
||||
{{ t('view.charts.hot_worlds.tips.description') }}
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<ToggleGroup
|
||||
variant="outline"
|
||||
type="single"
|
||||
:model-value="String(selectedDays)"
|
||||
@update:modelValue="handleDaysChange">
|
||||
<ToggleGroupItem value="7">
|
||||
{{ t('view.charts.hot_worlds.period.days_7') }}
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="30">
|
||||
{{ t('view.charts.hot_worlds.period.days_30') }}
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="90">
|
||||
{{ t('view.charts.hot_worlds.period.days_90') }}
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isLoading" class="mt-[100px] flex items-center justify-center">
|
||||
<RefreshCcw class="size-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="hotWorlds.length === 0" class="mt-[100px] flex items-center justify-center">
|
||||
<DataTableEmpty type="nodata" />
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="mx-auto mt-3 flex max-w-[1100px] items-center gap-3">
|
||||
|
||||
<div class="flex items-center gap-2 rounded-lg border px-3 py-2">
|
||||
<MapPin class="size-3.5 text-muted-foreground" />
|
||||
<span class="text-sm font-medium">{{ totalVisits.toLocaleString() }}</span>
|
||||
<span class="text-xs text-muted-foreground">{{ t('view.charts.hot_worlds.stats.total_visits') }}</span>
|
||||
</div>
|
||||
<div v-if="risingCount > 0" class="flex items-center gap-2 rounded-lg border px-3 py-2">
|
||||
<TrendingUp class="size-3.5 text-green-500/50" />
|
||||
<span class="text-sm font-medium">{{ risingCount }}</span>
|
||||
<span class="text-xs text-muted-foreground">{{ t('view.charts.hot_worlds.stats.rising') }}</span>
|
||||
</div>
|
||||
<div v-if="coolingCount > 0" class="flex items-center gap-2 rounded-lg border px-3 py-2">
|
||||
<TrendingDown class="size-3.5 text-blue-400/50" />
|
||||
<span class="text-sm font-medium">{{ coolingCount }}</span>
|
||||
<span class="text-xs text-muted-foreground">{{ t('view.charts.hot_worlds.stats.cooling') }}</span>
|
||||
</div>
|
||||
<span class="ml-auto text-xs text-muted-foreground/50">{{ t('view.charts.hot_worlds.sorted_by') }}</span>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto mt-3 flex max-w-[1100px] gap-x-6">
|
||||
<div
|
||||
v-for="(column, colIdx) in columns"
|
||||
:key="colIdx"
|
||||
class="min-w-0 flex-1">
|
||||
<button
|
||||
v-for="world in column"
|
||||
:key="world.worldId"
|
||||
type="button"
|
||||
class="group flex w-full items-start gap-3 rounded-lg px-3 py-2 text-left transition-colors hover:bg-accent"
|
||||
:class="world._rank === 1 ? 'bg-primary/[0.04]' : ''"
|
||||
@click="openDetail(world)">
|
||||
<span
|
||||
class="mt-0.5 w-6 shrink-0 text-right font-mono text-sm font-bold"
|
||||
:class="world._rank === 1 ? 'text-primary' : 'text-muted-foreground'">
|
||||
#{{ world._rank }}
|
||||
</span>
|
||||
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="block max-w-[380px] truncate text-sm font-medium">
|
||||
{{ world.worldName }}
|
||||
</span>
|
||||
<template v-if="world.trend === 'rising'">
|
||||
<TrendingUp class="size-3 shrink-0 text-green-500/50" />
|
||||
</template>
|
||||
<template v-else-if="world.trend === 'cooling'">
|
||||
<TrendingDown class="size-3 shrink-0 text-blue-400/50" />
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="mt-0.5 text-xs text-muted-foreground">
|
||||
{{ t('view.charts.hot_worlds.stats_line.friends', { count: world.uniqueFriends }) }}
|
||||
<span class="text-muted-foreground/50">
|
||||
({{ t('view.charts.hot_worlds.stats_line.visits', { count: world.visitCount }) }})
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mt-1.5 h-2 w-full overflow-hidden rounded-full"
|
||||
:class="isDarkMode ? 'bg-white/[0.08]' : 'bg-black/[0.06]'">
|
||||
<div
|
||||
class="h-full rounded-full transition-all duration-500"
|
||||
:class="isDarkMode ? 'bg-white/[0.45]' : 'bg-black/[0.25]'"
|
||||
:style="{ width: getBarWidth(world.uniqueFriends) }">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Sheet :open="isSheetOpen" @update:open="handleSheetClose">
|
||||
<SheetContent side="right" class="w-[340px] sm:max-w-[340px]">
|
||||
<SheetHeader class="px-5">
|
||||
<SheetTitle class="text-left">
|
||||
<button
|
||||
type="button"
|
||||
class="text-left text-base font-semibold hover:underline"
|
||||
@click="handleWorldClick">
|
||||
{{ selectedWorld?.worldName }}
|
||||
</button>
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
|
||||
<div v-if="selectedWorld" class="flex flex-col gap-4 overflow-y-auto px-5">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="inline-flex items-center gap-1 rounded-full bg-primary/10 px-2.5 py-1 text-xs font-medium">
|
||||
<Users class="size-3" />
|
||||
{{ t('view.charts.hot_worlds.stats_line.friends', { count: selectedWorld.uniqueFriends }) }}
|
||||
</span>
|
||||
<span class="inline-flex items-center gap-1 rounded-full bg-muted px-2.5 py-1 text-xs text-muted-foreground">
|
||||
<MapPin class="size-3" />
|
||||
{{ t('view.charts.hot_worlds.stats_line.visits', { count: selectedWorld.visitCount }) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="selectedWorld.trend === 'rising'"
|
||||
class="inline-flex items-center gap-1 rounded-full bg-green-500/10 px-2.5 py-1 text-xs text-green-500/70">
|
||||
<TrendingUp class="size-3" />
|
||||
{{ t('view.charts.hot_worlds.trend.rising') }}
|
||||
</span>
|
||||
<span
|
||||
v-else-if="selectedWorld.trend === 'cooling'"
|
||||
class="inline-flex items-center gap-1 rounded-full bg-blue-400/10 px-2.5 py-1 text-xs text-blue-400/70">
|
||||
<TrendingDown class="size-3" />
|
||||
{{ t('view.charts.hot_worlds.trend.cooling') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<div class="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground/70">
|
||||
{{ t('view.charts.hot_worlds.sheet.friends_who_visited') }}
|
||||
</div>
|
||||
<div v-if="isLoadingDetail" class="flex items-center justify-center py-8">
|
||||
<RefreshCcw class="size-4 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
<div v-else-if="friendDetail.length === 0" class="py-6 text-center text-xs text-muted-foreground">
|
||||
{{ t('view.charts.hot_worlds.no_friend_data') }}
|
||||
</div>
|
||||
<div v-else class="space-y-0.5">
|
||||
<button
|
||||
v-for="friend in friendDetail"
|
||||
:key="friend.userId"
|
||||
type="button"
|
||||
class="flex w-full items-center gap-2 rounded-md px-2.5 py-2 text-left text-sm transition-colors hover:bg-accent"
|
||||
@click="openUserDialog(friend.userId)">
|
||||
<span class="min-w-0 flex-1 truncate">{{ friend.displayName }}</span>
|
||||
<span class="shrink-0 rounded-md bg-muted px-1.5 py-0.5 text-[11px] tabular-nums text-muted-foreground">
|
||||
{{ friend.visitCount }}×
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineOptions({ name: 'ChartsHotWorlds' });
|
||||
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { Info, MapPin, RefreshCcw, TrendingDown, TrendingUp } from 'lucide-vue-next';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import BackToTop from '@/components/BackToTop.vue';
|
||||
|
||||
import { DataTableEmpty } from '@/components/ui/data-table';
|
||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
|
||||
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
||||
|
||||
import { showUserDialog } from '@/coordinators/userCoordinator';
|
||||
import { showWorldDialog } from '@/coordinators/worldCoordinator';
|
||||
import { database } from '@/services/database';
|
||||
import { useAppearanceSettingsStore } from '@/stores';
|
||||
|
||||
const { t } = useI18n();
|
||||
const { isDarkMode } = storeToRefs(useAppearanceSettingsStore());
|
||||
|
||||
const hotWorldsRef = ref(null);
|
||||
const isLoading = ref(true);
|
||||
const isLoadingDetail = ref(false);
|
||||
const selectedDays = ref(30);
|
||||
const hotWorlds = ref([]);
|
||||
const friendDetail = ref([]);
|
||||
|
||||
// Sheet state
|
||||
const isSheetOpen = ref(false);
|
||||
const selectedWorld = ref(null);
|
||||
|
||||
const containerResizeObserver = new ResizeObserver(() => {
|
||||
setContainerHeight();
|
||||
});
|
||||
|
||||
const displayed = computed(() => hotWorlds.value.slice(0, 20));
|
||||
|
||||
const columns = computed(() => {
|
||||
const items = displayed.value.map((w, i) => ({ ...w, _rank: i + 1 }));
|
||||
const mid = Math.ceil(items.length / 2);
|
||||
return [items.slice(0, mid), items.slice(mid)];
|
||||
});
|
||||
|
||||
const maxFriends = computed(() => {
|
||||
if (displayed.value.length === 0) return 1;
|
||||
return displayed.value[0].uniqueFriends || 1;
|
||||
});
|
||||
|
||||
const risingCount = computed(() => {
|
||||
return displayed.value.filter((w) => w.trend === 'rising').length;
|
||||
});
|
||||
|
||||
const coolingCount = computed(() => {
|
||||
return displayed.value.filter((w) => w.trend === 'cooling').length;
|
||||
});
|
||||
|
||||
const totalVisits = computed(() => {
|
||||
return displayed.value.reduce((sum, w) => sum + (w.visitCount || 0), 0);
|
||||
});
|
||||
|
||||
function getBarWidth(uniqueFriends) {
|
||||
return `${Math.max(4, (uniqueFriends / maxFriends.value) * 100)}%`;
|
||||
}
|
||||
|
||||
function setContainerHeight() {
|
||||
if (hotWorldsRef.value) {
|
||||
const availableHeight = window.innerHeight - 110;
|
||||
hotWorldsRef.value.style.height = `${availableHeight}px`;
|
||||
hotWorldsRef.value.style.overflowY = 'auto';
|
||||
}
|
||||
}
|
||||
|
||||
function handleDaysChange(value) {
|
||||
if (!value) return;
|
||||
selectedDays.value = parseInt(value, 10);
|
||||
handleSheetClose(false);
|
||||
loadData();
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
hotWorlds.value = await database.getHotWorlds(selectedDays.value);
|
||||
} catch (error) {
|
||||
console.error('Error loading hot worlds:', error);
|
||||
hotWorlds.value = [];
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function openDetail(world) {
|
||||
selectedWorld.value = world;
|
||||
isSheetOpen.value = true;
|
||||
isLoadingDetail.value = true;
|
||||
try {
|
||||
friendDetail.value = await database.getHotWorldFriendDetail(world.worldId, selectedDays.value);
|
||||
} catch (error) {
|
||||
console.error('Error loading friend detail:', error);
|
||||
friendDetail.value = [];
|
||||
} finally {
|
||||
isLoadingDetail.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSheetClose(open) {
|
||||
if (!open) {
|
||||
isSheetOpen.value = false;
|
||||
selectedWorld.value = null;
|
||||
friendDetail.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
function handleWorldClick() {
|
||||
if (selectedWorld.value?.worldId) {
|
||||
showWorldDialog(selectedWorld.value.worldId);
|
||||
}
|
||||
}
|
||||
|
||||
function openUserDialog(userId) {
|
||||
showUserDialog(userId);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (hotWorldsRef.value) {
|
||||
containerResizeObserver.observe(hotWorldsRef.value);
|
||||
}
|
||||
setContainerHeight();
|
||||
loadData();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (hotWorldsRef.value) {
|
||||
containerResizeObserver.unobserve(hotWorldsRef.value);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -35,6 +35,9 @@ export const panelComponentMap = {
|
||||
'charts-mutual': defineAsyncComponent(
|
||||
() => import('../../Charts/components/MutualFriends.vue')
|
||||
),
|
||||
'charts-hot-worlds': defineAsyncComponent(
|
||||
() => import('../../Charts/components/HotWorlds.vue')
|
||||
),
|
||||
tools: Tools,
|
||||
'widget:feed': defineAsyncComponent(
|
||||
() => import('../widgets/FeedWidget.vue')
|
||||
|
||||
Reference in New Issue
Block a user