mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-05-07 06:56:04 +02:00
debounce
This commit is contained in:
@@ -69,7 +69,7 @@
|
|||||||
<div class="flex items-center justify-between mb-2">
|
<div class="flex items-center justify-between mb-2">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-sm font-medium">{{ t('dialog.user.activity.overlap.header') }}</span>
|
<span class="text-sm font-medium">{{ t('dialog.user.activity.overlap.header') }}</span>
|
||||||
<Spinner v-if="isOverlapLoading" class="h-3.5 w-3.5" />
|
<Spinner v-if="isOverlapLoadingVisible" class="h-3.5 w-3.5" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="hasOverlapData" class="flex items-center gap-1.5 shrink-0">
|
<div v-if="hasOverlapData" class="flex items-center gap-1.5 shrink-0">
|
||||||
<Switch :model-value="excludeHoursEnabled" class="scale-75" @update:model-value="onExcludeToggle" />
|
<Switch :model-value="excludeHoursEnabled" class="scale-75" @update:model-value="onExcludeToggle" />
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
{{ t('dialog.user.activity.overlap.exclude_hours') }}
|
{{ t('dialog.user.activity.overlap.exclude_hours') }}
|
||||||
</span>
|
</span>
|
||||||
<Select v-model="excludeStartHour" @update:model-value="onExcludeRangeChange">
|
<Select v-model="excludeStartHour" @update:model-value="onExcludeRangeChange">
|
||||||
<SelectTrigger size="sm" class="w-[78px] h-6 text-xs px-2" @click.stop>
|
<SelectTrigger size="sm" class="w-[78px] h-6 text-sm px-2" @click.stop>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -88,7 +88,7 @@
|
|||||||
</Select>
|
</Select>
|
||||||
<span class="text-xs text-muted-foreground">–</span>
|
<span class="text-xs text-muted-foreground">–</span>
|
||||||
<Select v-model="excludeEndHour" @update:model-value="onExcludeRangeChange">
|
<Select v-model="excludeEndHour" @update:model-value="onExcludeRangeChange">
|
||||||
<SelectTrigger size="sm" class="w-[78px] h-6 text-xs px-2" @click.stop>
|
<SelectTrigger size="sm" class="w-[78px] h-6 text-sm px-2" @click.stop>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!isOverlapLoading && hasOverlapData" class="flex flex-col gap-1 mb-2">
|
<div v-if="!isOverlapLoadingVisible && hasOverlapData" class="flex flex-col gap-1 mb-2">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span
|
<span
|
||||||
class="text-sm font-medium"
|
class="text-sm font-medium"
|
||||||
@@ -123,7 +123,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-show="hasOverlapData"
|
v-show="hasOverlapData || isOverlapLoadingVisible"
|
||||||
ref="overlapChartRef"
|
ref="overlapChartRef"
|
||||||
class="min-w-0 overflow-hidden"
|
class="min-w-0 overflow-hidden"
|
||||||
style="width: 100%; height: 240px"
|
style="width: 100%; height: 240px"
|
||||||
@@ -140,7 +140,7 @@
|
|||||||
<span class="text-sm font-medium">
|
<span class="text-sm font-medium">
|
||||||
{{ t('dialog.user.activity.most_visited_worlds.header') }}
|
{{ t('dialog.user.activity.most_visited_worlds.header') }}
|
||||||
</span>
|
</span>
|
||||||
<Spinner v-if="topWorldsLoading" class="h-3.5 w-3.5" />
|
<Spinner v-if="topWorldsLoadingVisible" class="h-3.5 w-3.5" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="topWorlds.length > 0" class="flex items-center gap-2">
|
<div v-if="topWorlds.length > 0" class="flex items-center gap-2">
|
||||||
<span class="text-muted-foreground text-sm">{{ t('common.sort_by') }}</span>
|
<span class="text-muted-foreground text-sm">{{ t('common.sort_by') }}</span>
|
||||||
@@ -160,13 +160,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="topWorldsLoading && topWorlds.length === 0"
|
v-if="topWorldsLoadingVisible && topWorlds.length === 0"
|
||||||
class="flex items-center gap-2 text-sm text-muted-foreground py-2">
|
class="flex items-center gap-2 text-sm text-muted-foreground py-2">
|
||||||
<Spinner class="h-4 w-4" />
|
<Spinner class="h-4 w-4" />
|
||||||
<span>{{ t('dialog.user.activity.most_visited_worlds.loading') }}</span>
|
<span>{{ t('dialog.user.activity.most_visited_worlds.loading') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="topWorlds.length === 0 && !isLoading"
|
v-else-if="topWorlds.length === 0 && !isLoading && !topWorldsLoading"
|
||||||
class="text-sm text-muted-foreground py-2">
|
class="text-sm text-muted-foreground py-2">
|
||||||
{{ t('dialog.user.activity.no_data_in_period') }}
|
{{ t('dialog.user.activity.no_data_in_period') }}
|
||||||
</div>
|
</div>
|
||||||
@@ -262,10 +262,12 @@
|
|||||||
const peakDayText = ref('');
|
const peakDayText = ref('');
|
||||||
const peakTimeText = ref('');
|
const peakTimeText = ref('');
|
||||||
const isOverlapLoading = ref(false);
|
const isOverlapLoading = ref(false);
|
||||||
|
const isOverlapLoadingVisible = ref(false);
|
||||||
const hasOverlapData = ref(false);
|
const hasOverlapData = ref(false);
|
||||||
const overlapPercent = ref(0);
|
const overlapPercent = ref(0);
|
||||||
const bestOverlapTime = ref('');
|
const bestOverlapTime = ref('');
|
||||||
const topWorldsLoading = ref(false);
|
const topWorldsLoading = ref(false);
|
||||||
|
const topWorldsLoadingVisible = ref(false);
|
||||||
const topWorlds = ref([]);
|
const topWorlds = ref([]);
|
||||||
const topWorldsSortBy = ref('time');
|
const topWorldsSortBy = ref('time');
|
||||||
const excludeHoursEnabled = ref(false);
|
const excludeHoursEnabled = ref(false);
|
||||||
@@ -284,7 +286,13 @@
|
|||||||
let activeOverlapRequestId = 0;
|
let activeOverlapRequestId = 0;
|
||||||
let activeTopWorldsRequestId = 0;
|
let activeTopWorldsRequestId = 0;
|
||||||
let lastLoadedUserId = '';
|
let lastLoadedUserId = '';
|
||||||
|
let topWorldsLoadingTimer = null;
|
||||||
|
let overlapLoadingTimer = null;
|
||||||
|
let overlapRenderTimer = null;
|
||||||
const pendingWorldThumbnailFetches = new Set();
|
const pendingWorldThumbnailFetches = new Set();
|
||||||
|
const TOP_WORLDS_LOADING_DELAY = 150;
|
||||||
|
const OVERLAP_LOADING_DELAY = 120;
|
||||||
|
const OVERLAP_RENDER_DELAY = 80;
|
||||||
|
|
||||||
const isSelf = computed(() => userDialog.value.id === currentUser.value.id);
|
const isSelf = computed(() => userDialog.value.id === currentUser.value.id);
|
||||||
const sortedTopWorlds = computed(() => topWorlds.value);
|
const sortedTopWorlds = computed(() => topWorlds.value);
|
||||||
@@ -316,23 +324,103 @@
|
|||||||
peakDayText.value = '';
|
peakDayText.value = '';
|
||||||
peakTimeText.value = '';
|
peakTimeText.value = '';
|
||||||
selectedPeriod.value = '30';
|
selectedPeriod.value = '30';
|
||||||
isOverlapLoading.value = false;
|
|
||||||
hasOverlapData.value = false;
|
hasOverlapData.value = false;
|
||||||
overlapPercent.value = 0;
|
overlapPercent.value = 0;
|
||||||
bestOverlapTime.value = '';
|
bestOverlapTime.value = '';
|
||||||
topWorldsLoading.value = false;
|
topWorldsLoading.value = false;
|
||||||
|
topWorldsLoadingVisible.value = false;
|
||||||
topWorlds.value = [];
|
topWorlds.value = [];
|
||||||
|
isOverlapLoading.value = false;
|
||||||
|
isOverlapLoadingVisible.value = false;
|
||||||
mainHeatmapView.value = { rawBuckets: [], normalizedBuckets: [] };
|
mainHeatmapView.value = { rawBuckets: [], normalizedBuckets: [] };
|
||||||
overlapHeatmapView.value = { rawBuckets: [], normalizedBuckets: [] };
|
overlapHeatmapView.value = { rawBuckets: [], normalizedBuckets: [] };
|
||||||
|
clearOverlapLoadingTimer();
|
||||||
|
clearOverlapRenderTimer();
|
||||||
activeRequestId++;
|
activeRequestId++;
|
||||||
activeOverlapRequestId++;
|
activeOverlapRequestId++;
|
||||||
activeTopWorldsRequestId++;
|
activeTopWorldsRequestId++;
|
||||||
lastLoadedUserId = '';
|
lastLoadedUserId = '';
|
||||||
|
clearTimeout(topWorldsLoadingTimer);
|
||||||
|
topWorldsLoadingTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearOverlapLoadingTimer() {
|
||||||
|
if (overlapLoadingTimer !== null) {
|
||||||
|
clearTimeout(overlapLoadingTimer);
|
||||||
|
overlapLoadingTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearOverlapRenderTimer() {
|
||||||
|
if (overlapRenderTimer !== null) {
|
||||||
|
clearTimeout(overlapRenderTimer);
|
||||||
|
overlapRenderTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function beginOverlapLoading(requestId) {
|
||||||
|
isOverlapLoading.value = true;
|
||||||
|
isOverlapLoadingVisible.value = false;
|
||||||
|
clearOverlapLoadingTimer();
|
||||||
|
overlapLoadingTimer = setTimeout(() => {
|
||||||
|
overlapLoadingTimer = null;
|
||||||
|
if (requestId === activeOverlapRequestId && isOverlapLoading.value) {
|
||||||
|
isOverlapLoadingVisible.value = true;
|
||||||
|
}
|
||||||
|
}, OVERLAP_LOADING_DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishOverlapLoading(requestId) {
|
||||||
|
if (requestId !== activeOverlapRequestId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearOverlapLoadingTimer();
|
||||||
|
isOverlapLoading.value = false;
|
||||||
|
isOverlapLoadingVisible.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleOverlapChartRender() {
|
||||||
|
clearOverlapRenderTimer();
|
||||||
|
overlapRenderTimer = setTimeout(() => {
|
||||||
|
overlapRenderTimer = null;
|
||||||
|
renderOverlapChart();
|
||||||
|
}, OVERLAP_RENDER_DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyOverlapView(overlapView) {
|
||||||
|
overlapHeatmapView.value = {
|
||||||
|
rawBuckets: overlapView.rawBuckets,
|
||||||
|
normalizedBuckets: overlapView.normalizedBuckets
|
||||||
|
};
|
||||||
|
hasOverlapData.value = overlapView.hasOverlapData;
|
||||||
|
overlapPercent.value = overlapView.overlapPercent;
|
||||||
|
bestOverlapTime.value = overlapView.bestOverlapTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleTopWorldsLoading(requestId) {
|
||||||
|
clearTimeout(topWorldsLoadingTimer);
|
||||||
|
topWorldsLoadingTimer = setTimeout(() => {
|
||||||
|
topWorldsLoadingTimer = null;
|
||||||
|
if (requestId === activeTopWorldsRequestId) {
|
||||||
|
topWorldsLoadingVisible.value = true;
|
||||||
|
}
|
||||||
|
}, TOP_WORLDS_LOADING_DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishTopWorldsLoading(requestId) {
|
||||||
|
if (requestId !== activeTopWorldsRequestId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearTimeout(topWorldsLoadingTimer);
|
||||||
|
topWorldsLoadingTimer = null;
|
||||||
|
topWorldsLoadingVisible.value = false;
|
||||||
|
topWorldsLoading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadTopWorldsSection({ userId, rangeDays, sortBy, period }) {
|
async function loadTopWorldsSection({ userId, rangeDays, sortBy, period }) {
|
||||||
const requestId = ++activeTopWorldsRequestId;
|
const requestId = ++activeTopWorldsRequestId;
|
||||||
topWorldsLoading.value = true;
|
topWorldsLoading.value = true;
|
||||||
|
scheduleTopWorldsLoading(requestId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await activityStore.loadTopWorldsView({
|
const result = await activityStore.loadTopWorldsView({
|
||||||
@@ -353,9 +441,7 @@
|
|||||||
topWorlds.value = result;
|
topWorlds.value = result;
|
||||||
void fetchMissingTopWorldThumbnails(topWorlds.value);
|
void fetchMissingTopWorldThumbnails(topWorlds.value);
|
||||||
} finally {
|
} finally {
|
||||||
if (requestId === activeTopWorldsRequestId) {
|
finishTopWorldsLoading(requestId);
|
||||||
topWorldsLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,7 +452,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const requestId = ++activeRequestId;
|
const requestId = ++activeRequestId;
|
||||||
++activeOverlapRequestId;
|
const overlapRequestId = ++activeOverlapRequestId;
|
||||||
if (!silent) {
|
if (!silent) {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
}
|
}
|
||||||
@@ -415,7 +501,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
isOverlapLoading.value = true;
|
beginOverlapLoading(overlapRequestId);
|
||||||
const overlapView = await activityStore.loadOverlapView({
|
const overlapView = await activityStore.loadOverlapView({
|
||||||
currentUserId: currentUser.value.id,
|
currentUserId: currentUser.value.id,
|
||||||
targetUserId: userId,
|
targetUserId: userId,
|
||||||
@@ -431,18 +517,12 @@
|
|||||||
if (requestId !== activeRequestId || userDialog.value.id !== userId) {
|
if (requestId !== activeRequestId || userDialog.value.id !== userId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
overlapHeatmapView.value = {
|
applyOverlapView(overlapView);
|
||||||
rawBuckets: overlapView.rawBuckets,
|
|
||||||
normalizedBuckets: overlapView.normalizedBuckets
|
|
||||||
};
|
|
||||||
hasOverlapData.value = overlapView.hasOverlapData;
|
|
||||||
overlapPercent.value = overlapView.overlapPercent;
|
|
||||||
bestOverlapTime.value = overlapView.bestOverlapTime;
|
|
||||||
} finally {
|
} finally {
|
||||||
if (requestId === activeRequestId) {
|
if (requestId === activeRequestId) {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
isOverlapLoading.value = false;
|
|
||||||
}
|
}
|
||||||
|
finishOverlapLoading(overlapRequestId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -472,7 +552,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const requestId = ++activeOverlapRequestId;
|
const requestId = ++activeOverlapRequestId;
|
||||||
isOverlapLoading.value = true;
|
beginOverlapLoading(requestId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const rangeDays = parseInt(selectedPeriod.value, 10) || 30;
|
const rangeDays = parseInt(selectedPeriod.value, 10) || 30;
|
||||||
@@ -491,17 +571,9 @@
|
|||||||
if (requestId !== activeOverlapRequestId || userDialog.value.id !== userId) {
|
if (requestId !== activeOverlapRequestId || userDialog.value.id !== userId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
overlapHeatmapView.value = {
|
applyOverlapView(overlapView);
|
||||||
rawBuckets: overlapView.rawBuckets,
|
|
||||||
normalizedBuckets: overlapView.normalizedBuckets
|
|
||||||
};
|
|
||||||
hasOverlapData.value = overlapView.hasOverlapData;
|
|
||||||
overlapPercent.value = overlapView.overlapPercent;
|
|
||||||
bestOverlapTime.value = overlapView.bestOverlapTime;
|
|
||||||
} finally {
|
} finally {
|
||||||
if (requestId === activeOverlapRequestId) {
|
finishOverlapLoading(requestId);
|
||||||
isOverlapLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -658,7 +730,9 @@
|
|||||||
|
|
||||||
function renderOverlapChart() {
|
function renderOverlapChart() {
|
||||||
if (!hasOverlapData.value || overlapHeatmapView.value.normalizedBuckets.length === 0) {
|
if (!hasOverlapData.value || overlapHeatmapView.value.normalizedBuckets.length === 0) {
|
||||||
overlapChart?.clear();
|
if (!isOverlapLoading.value) {
|
||||||
|
overlapChart?.clear();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ensureOverlapChart();
|
ensureOverlapChart();
|
||||||
@@ -696,6 +770,7 @@
|
|||||||
|
|
||||||
function rebuildCharts() {
|
function rebuildCharts() {
|
||||||
disposeCharts();
|
disposeCharts();
|
||||||
|
clearOverlapRenderTimer();
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
renderActivityChart();
|
renderActivityChart();
|
||||||
renderOverlapChart();
|
renderOverlapChart();
|
||||||
@@ -772,7 +847,7 @@
|
|||||||
watch(
|
watch(
|
||||||
() => overlapHeatmapView.value,
|
() => overlapHeatmapView.value,
|
||||||
() => {
|
() => {
|
||||||
nextTick(() => renderOverlapChart());
|
scheduleOverlapChartRender();
|
||||||
},
|
},
|
||||||
{ deep: true }
|
{ deep: true }
|
||||||
);
|
);
|
||||||
@@ -807,6 +882,9 @@
|
|||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
clearTimeout(easterEggTimer);
|
clearTimeout(easterEggTimer);
|
||||||
|
clearTimeout(topWorldsLoadingTimer);
|
||||||
|
clearOverlapLoadingTimer();
|
||||||
|
clearOverlapRenderTimer();
|
||||||
disposeCharts();
|
disposeCharts();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user