mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-15 12:53:51 +02:00
improve dashboard wideget
This commit is contained in:
@@ -32,6 +32,24 @@
|
||||
{{ filterType }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<label
|
||||
v-if="widgetType === 'game-log'"
|
||||
class="flex items-center gap-1 text-xs cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="panelConfig.showDetail || false"
|
||||
@change="toggleBooleanConfig('showDetail')" />
|
||||
Show Detail
|
||||
</label>
|
||||
<label v-if="widgetType === 'feed'" class="flex items-center gap-1 text-xs cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="panelConfig.showType || false"
|
||||
@change="toggleBooleanConfig('showType')" />
|
||||
Show Type
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Instance: column visibility -->
|
||||
@@ -89,7 +107,15 @@
|
||||
import { panelComponentMap } from './panelRegistry';
|
||||
|
||||
const FEED_TYPES = ['GPS', 'Online', 'Offline', 'Status', 'Avatar', 'Bio'];
|
||||
const GAMELOG_TYPES = ['Location', 'OnPlayerJoined', 'OnPlayerLeft', 'VideoPlay', 'PortalSpawn', 'Event', 'External'];
|
||||
const GAMELOG_TYPES = [
|
||||
'Location',
|
||||
'OnPlayerJoined',
|
||||
'OnPlayerLeft',
|
||||
'VideoPlay',
|
||||
'PortalSpawn',
|
||||
'Event',
|
||||
'External'
|
||||
];
|
||||
const INSTANCE_COLUMNS = ['icon', 'displayName', 'rank', 'timer', 'platform', 'language', 'status'];
|
||||
|
||||
const props = defineProps({
|
||||
@@ -189,19 +215,19 @@
|
||||
emitConfigUpdate({ ...panelConfig.value, filters });
|
||||
}
|
||||
|
||||
const availableColumns = computed(() => INSTANCE_COLUMNS);
|
||||
const availableColumns = computed(() => INSTANCE_COLUMNS);
|
||||
|
||||
function isColumnActive(col) {
|
||||
const columns = panelConfig.value.columns;
|
||||
if (!columns || !Array.isArray(columns) || columns.length === 0) {
|
||||
return ['icon', 'displayName', 'rank', 'timer'].includes(col);
|
||||
return ['icon', 'displayName', 'timer'].includes(col);
|
||||
}
|
||||
return columns.includes(col);
|
||||
}
|
||||
|
||||
function toggleColumn(col) {
|
||||
if (col === 'displayName') return; // Always visible
|
||||
const currentColumns = panelConfig.value.columns || ['icon', 'displayName', 'rank', 'timer'];
|
||||
const currentColumns = panelConfig.value.columns || ['icon', 'displayName', 'timer'];
|
||||
let columns;
|
||||
if (currentColumns.includes(col)) {
|
||||
columns = currentColumns.filter((c) => c !== col);
|
||||
@@ -211,6 +237,10 @@
|
||||
emitConfigUpdate({ ...panelConfig.value, columns });
|
||||
}
|
||||
|
||||
function toggleBooleanConfig(key) {
|
||||
emitConfigUpdate({ ...panelConfig.value, [key]: !panelConfig.value[key] });
|
||||
}
|
||||
|
||||
function emitConfigUpdate(newConfig) {
|
||||
emit('select', { key: panelKey.value, config: newConfig });
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
<template>
|
||||
<div class="flex h-full min-h-0 flex-col">
|
||||
<WidgetHeader
|
||||
:title="t('dashboard.widget.feed')"
|
||||
icon="ri-rss-line"
|
||||
route-name="feed" />
|
||||
<WidgetHeader :title="t('dashboard.widget.feed')" icon="ri-rss-line" route-name="feed" />
|
||||
|
||||
<div class="min-h-0 flex-1 overflow-y-auto" ref="listRef">
|
||||
<Table v-if="filteredData.length" class="is-compact-table">
|
||||
@@ -13,15 +10,22 @@
|
||||
:key="`${item.type}-${item.created_at}-${index}`"
|
||||
class="cursor-default"
|
||||
:class="{ 'border-l-2 border-l-chart-4': item.isFavorite }">
|
||||
<TableCell class="w-14 text-[11px] tabular-nums text-muted-foreground">
|
||||
<TableCell class="w-28 text-[11px] tabular-nums text-muted-foreground">
|
||||
<TooltipWrapper :content="formatExactTime(item.created_at)" side="top">
|
||||
<span>{{ timeAgo(item.created_at) }}</span>
|
||||
<span>{{ formatTime(item.created_at) }}</span>
|
||||
</TooltipWrapper>
|
||||
</TableCell>
|
||||
<TableCell v-if="showType" class="w-16 text-[11px] text-muted-foreground">
|
||||
{{ item.type }}
|
||||
</TableCell>
|
||||
<TableCell class="truncate">
|
||||
<template v-if="item.type === 'GPS'">
|
||||
<MapPin class="mr-1 inline-block h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
<span class="cursor-pointer font-medium hover:underline" @click="openUser(item.userId)">{{ item.displayName }}</span>
|
||||
<span
|
||||
class="cursor-pointer font-medium hover:underline"
|
||||
@click="openUser(item.userId)"
|
||||
>{{ item.displayName }}</span
|
||||
>
|
||||
<span class="text-muted-foreground"> → </span>
|
||||
<Location
|
||||
class="inline [&>div]:inline-flex"
|
||||
@@ -32,7 +36,11 @@
|
||||
</template>
|
||||
<template v-else-if="item.type === 'Online'">
|
||||
<i class="x-user-status online mr-1"></i>
|
||||
<span class="cursor-pointer font-medium hover:underline" @click="openUser(item.userId)">{{ item.displayName }}</span>
|
||||
<span
|
||||
class="cursor-pointer font-medium hover:underline"
|
||||
@click="openUser(item.userId)"
|
||||
>{{ item.displayName }}</span
|
||||
>
|
||||
<template v-if="item.location">
|
||||
<span class="text-muted-foreground"> → </span>
|
||||
<Location
|
||||
@@ -45,25 +53,45 @@
|
||||
</template>
|
||||
<template v-else-if="item.type === 'Offline'">
|
||||
<i class="x-user-status mr-1"></i>
|
||||
<span class="cursor-pointer font-medium text-muted-foreground/70 hover:underline" @click="openUser(item.userId)">{{ item.displayName }}</span>
|
||||
<span
|
||||
class="cursor-pointer font-medium text-muted-foreground/70 hover:underline"
|
||||
@click="openUser(item.userId)"
|
||||
>{{ item.displayName }}</span
|
||||
>
|
||||
</template>
|
||||
<template v-else-if="item.type === 'Status'">
|
||||
<i class="x-user-status mr-1" :class="statusClass(item.status)"></i>
|
||||
<span class="cursor-pointer font-medium hover:underline" @click="openUser(item.userId)">{{ item.displayName }}</span>
|
||||
<span
|
||||
class="cursor-pointer font-medium hover:underline"
|
||||
@click="openUser(item.userId)"
|
||||
>{{ item.displayName }}</span
|
||||
>
|
||||
<span class="text-muted-foreground"> {{ item.statusDescription }}</span>
|
||||
</template>
|
||||
<template v-else-if="item.type === 'Avatar'">
|
||||
<Box class="mr-1 inline-block h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
<span class="cursor-pointer font-medium hover:underline" @click="openUser(item.userId)">{{ item.displayName }}</span>
|
||||
<span
|
||||
class="cursor-pointer font-medium hover:underline"
|
||||
@click="openUser(item.userId)"
|
||||
>{{ item.displayName }}</span
|
||||
>
|
||||
<span class="text-muted-foreground"> → {{ item.avatarName }}</span>
|
||||
</template>
|
||||
<template v-else-if="item.type === 'Bio'">
|
||||
<Pencil class="mr-1 inline-block h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
<span class="cursor-pointer font-medium hover:underline" @click="openUser(item.userId)">{{ item.displayName }}</span>
|
||||
<span
|
||||
class="cursor-pointer font-medium hover:underline"
|
||||
@click="openUser(item.userId)"
|
||||
>{{ item.displayName }}</span
|
||||
>
|
||||
<span class="ml-1 text-muted-foreground">{{ t('dashboard.widget.feed_bio') }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="cursor-pointer font-medium hover:underline" @click="openUser(item.userId)">{{ item.displayName }}</span>
|
||||
<span
|
||||
class="cursor-pointer font-medium hover:underline"
|
||||
@click="openUser(item.userId)"
|
||||
>{{ item.displayName }}</span
|
||||
>
|
||||
<span class="text-muted-foreground"> {{ item.type }}</span>
|
||||
</template>
|
||||
</TableCell>
|
||||
@@ -83,7 +111,7 @@
|
||||
import { Box, MapPin, Pencil } from 'lucide-vue-next';
|
||||
|
||||
import { statusClass } from '@/shared/utils/user';
|
||||
import { timeToText, formatDateFilter } from '@/shared/utils';
|
||||
import { formatDateFilter } from '@/shared/utils';
|
||||
import { showUserDialog } from '@/coordinators/userCoordinator';
|
||||
import { useFeedStore } from '@/stores';
|
||||
|
||||
@@ -112,20 +140,17 @@
|
||||
return FEED_TYPES;
|
||||
});
|
||||
|
||||
const showType = computed(() => {
|
||||
return props.config.showType || false;
|
||||
});
|
||||
|
||||
const filteredData = computed(() => {
|
||||
const filters = activeFilters.value;
|
||||
return feedStore.feedTableData.filter((item) => filters.includes(item.type)).slice(0, 100);
|
||||
});
|
||||
|
||||
function timeAgo(dateStr) {
|
||||
if (!dateStr) return '';
|
||||
let diff = Date.now() - new Date(dateStr).getTime();
|
||||
if (diff < 0) return 'now';
|
||||
// Over 1 hour: drop minutes
|
||||
if (diff >= 3600000) {
|
||||
diff = Math.floor(diff / 3600000) * 3600000;
|
||||
}
|
||||
return t('dashboard.widget.time_ago', { time: timeToText(diff) });
|
||||
function formatTime(dateStr) {
|
||||
return formatDateFilter(dateStr, 'short');
|
||||
}
|
||||
|
||||
function formatExactTime(dateStr) {
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
<template>
|
||||
<div class="flex h-full min-h-0 flex-col">
|
||||
<WidgetHeader
|
||||
:title="t('dashboard.widget.game_log')"
|
||||
icon="ri-history-line"
|
||||
route-name="game-log" />
|
||||
<WidgetHeader :title="t('dashboard.widget.game_log')" icon="ri-history-line" route-name="game-log" />
|
||||
|
||||
<div class="min-h-0 flex-1 overflow-y-auto">
|
||||
<Table v-if="filteredData.length" class="is-compact-table">
|
||||
@@ -13,14 +10,14 @@
|
||||
:key="`${item.type}-${item.created_at}-${index}`"
|
||||
class="cursor-default"
|
||||
:class="{ 'border-l-2 border-l-chart-4': item.isFavorite }">
|
||||
<TableCell class="w-14 text-[11px] tabular-nums text-muted-foreground">
|
||||
<TableCell class="w-28 text-[11px] tabular-nums text-muted-foreground">
|
||||
<TooltipWrapper :content="formatExactTime(item.created_at)" side="top">
|
||||
<span>{{ timeAgo(item.created_at) }}</span>
|
||||
<span>{{ formatTime(item.created_at) }}</span>
|
||||
</TooltipWrapper>
|
||||
</TableCell>
|
||||
<TableCell class="truncate">
|
||||
<template v-if="item.type === 'Location'">
|
||||
<Globe class="mr-1 inline-block h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
<MapPin class="mr-1 inline-block h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
<Location
|
||||
class="inline [&>div]:inline-flex"
|
||||
:location="item.location"
|
||||
@@ -28,18 +25,22 @@
|
||||
disable-tooltip />
|
||||
</template>
|
||||
<template v-else-if="item.type === 'OnPlayerJoined'">
|
||||
<i class="x-user-status online mr-1"></i>
|
||||
<LogIn class="mr-1 inline-block h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
<span
|
||||
class="cursor-pointer font-medium hover:underline"
|
||||
:style="item.tagColour ? { color: item.tagColour } : null"
|
||||
@click="openUser(item.userId)">{{ item.displayName }}</span>
|
||||
@click="openUser(item.userId)"
|
||||
>{{ item.displayName }}</span
|
||||
>
|
||||
</template>
|
||||
<template v-else-if="item.type === 'OnPlayerLeft'">
|
||||
<i class="x-user-status mr-1"></i>
|
||||
<LogOut class="mr-1 inline-block h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
<span
|
||||
class="cursor-pointer font-medium text-muted-foreground/70 hover:underline"
|
||||
:style="item.tagColour ? { color: item.tagColour } : null"
|
||||
@click="openUser(item.userId)">{{ item.displayName }}</span>
|
||||
@click="openUser(item.userId)"
|
||||
>{{ item.displayName }}</span
|
||||
>
|
||||
</template>
|
||||
<template v-else-if="item.type === 'VideoPlay'">
|
||||
<Video class="mr-1 inline-block h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
@@ -47,7 +48,11 @@
|
||||
</template>
|
||||
<template v-else-if="item.type === 'PortalSpawn'">
|
||||
<Waypoints class="mr-1 inline-block h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
<span class="cursor-pointer font-medium hover:underline" @click="openUser(item.userId)">{{ item.displayName }}</span>
|
||||
<span
|
||||
class="cursor-pointer font-medium hover:underline"
|
||||
@click="openUser(item.userId)"
|
||||
>{{ item.displayName }}</span
|
||||
>
|
||||
<span class="text-muted-foreground"> → </span>
|
||||
<Location
|
||||
v-if="item.location"
|
||||
@@ -58,8 +63,22 @@
|
||||
<span v-else class="text-muted-foreground">{{ item.worldName || '' }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="font-medium">{{ item.displayName }}</span>
|
||||
<span class="text-muted-foreground"> {{ item.type }}</span>
|
||||
<TooltipWrapper
|
||||
v-if="!showDetail"
|
||||
:content="item.data || item.message || ''"
|
||||
side="top">
|
||||
<span>
|
||||
<span class="font-medium">{{ item.displayName }}</span>
|
||||
<span class="text-muted-foreground"> {{ item.type }}</span>
|
||||
</span>
|
||||
</TooltipWrapper>
|
||||
<template v-else>
|
||||
<span class="font-medium">{{ item.displayName }}</span>
|
||||
<span class="text-muted-foreground"> {{ item.type }}</span>
|
||||
<span v-if="item.data || item.message" class="ml-1 text-muted-foreground"
|
||||
>— {{ item.data || item.message }}</span
|
||||
>
|
||||
</template>
|
||||
</template>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -75,12 +94,12 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, shallowRef, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { Globe, Video, Waypoints } from 'lucide-vue-next';
|
||||
import { LogIn, LogOut, MapPin, Video, Waypoints } from 'lucide-vue-next';
|
||||
|
||||
import { database } from '@/services/database';
|
||||
import { showUserDialog } from '@/coordinators/userCoordinator';
|
||||
import { useFriendStore, useGameLogStore } from '@/stores';
|
||||
import { timeToText, formatDateFilter } from '@/shared/utils';
|
||||
import { formatDateFilter } from '@/shared/utils';
|
||||
import { watchState } from '@/services/watchState';
|
||||
|
||||
import Location from '@/components/Location.vue';
|
||||
@@ -88,7 +107,15 @@
|
||||
import WidgetHeader from './WidgetHeader.vue';
|
||||
import { Table, TableBody, TableRow, TableCell } from '@/components/ui/table';
|
||||
|
||||
const GAMELOG_TYPES = ['Location', 'OnPlayerJoined', 'OnPlayerLeft', 'VideoPlay', 'PortalSpawn', 'Event', 'External'];
|
||||
const GAMELOG_TYPES = [
|
||||
'Location',
|
||||
'OnPlayerJoined',
|
||||
'OnPlayerLeft',
|
||||
'VideoPlay',
|
||||
'PortalSpawn',
|
||||
'Event',
|
||||
'External'
|
||||
];
|
||||
|
||||
const props = defineProps({
|
||||
config: {
|
||||
@@ -111,6 +138,10 @@
|
||||
return GAMELOG_TYPES;
|
||||
});
|
||||
|
||||
const showDetail = computed(() => {
|
||||
return props.config.showDetail || false;
|
||||
});
|
||||
|
||||
const filteredData = computed(() => {
|
||||
const filters = activeFilters.value;
|
||||
return widgetData.value.filter((item) => filters.includes(item.type)).slice(0, maxEntries);
|
||||
@@ -161,15 +192,8 @@
|
||||
}
|
||||
});
|
||||
|
||||
function timeAgo(dateStr) {
|
||||
if (!dateStr) return '';
|
||||
let diff = Date.now() - new Date(dateStr).getTime();
|
||||
if (diff < 0) return 'now';
|
||||
// Over 1 hour: drop minutes
|
||||
if (diff >= 3600000) {
|
||||
diff = Math.floor(diff / 3600000) * 3600000;
|
||||
}
|
||||
return t('dashboard.widget.time_ago', { time: timeToText(diff) });
|
||||
function formatTime(dateStr) {
|
||||
return formatDateFilter(dateStr, 'short');
|
||||
}
|
||||
|
||||
function formatExactTime(dateStr) {
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
<template>
|
||||
<div class="flex h-full min-h-0 flex-col">
|
||||
<WidgetHeader
|
||||
:title="t('dashboard.widget.instance')"
|
||||
icon="ri-group-3-line"
|
||||
route-name="player-list" />
|
||||
<WidgetHeader :title="t('dashboard.widget.instance')" icon="ri-group-3-line" route-name="player-list" />
|
||||
|
||||
<template v-if="hasPlayers">
|
||||
<!-- Info bar -->
|
||||
@@ -28,20 +25,32 @@
|
||||
<span v-if="player.isFriend">💚</span>
|
||||
<span v-if="player.isBlocked" class="text-destructive">⛔</span>
|
||||
<span v-if="player.isMuted" class="text-muted-foreground">🔇</span>
|
||||
<IdCard
|
||||
v-if="player.ageVerified"
|
||||
class="inline-block h-3.5 w-3.5 x-tag-age-verification" />
|
||||
</TableCell>
|
||||
|
||||
<TableCell class="truncate font-medium" :class="player.ref?.$trustClass">
|
||||
{{ player.displayName }}
|
||||
</TableCell>
|
||||
|
||||
<TableCell v-if="isColumnVisible('rank')" class="w-24 truncate text-[11px]" :class="player.ref?.$trustClass">
|
||||
<TableCell
|
||||
v-if="isColumnVisible('rank')"
|
||||
class="w-24 truncate text-[11px]"
|
||||
:class="player.ref?.$trustClass">
|
||||
{{ player.ref?.$trustLevel || '' }}
|
||||
</TableCell>
|
||||
|
||||
<TableCell v-if="isColumnVisible('platform')" class="w-10 text-center">
|
||||
<Monitor v-if="player.ref?.$platform === 'standalonewindows'" class="inline-block h-3 w-3 x-tag-platform-pc" />
|
||||
<Smartphone v-else-if="player.ref?.$platform === 'android'" class="inline-block h-3 w-3 x-tag-platform-quest" />
|
||||
<Apple v-else-if="player.ref?.$platform === 'ios'" class="inline-block h-3 w-3 x-tag-platform-ios" />
|
||||
<Monitor
|
||||
v-if="player.ref?.$platform === 'standalonewindows'"
|
||||
class="inline-block h-3 w-3 x-tag-platform-pc" />
|
||||
<Smartphone
|
||||
v-else-if="player.ref?.$platform === 'android'"
|
||||
class="inline-block h-3 w-3 x-tag-platform-quest" />
|
||||
<Apple
|
||||
v-else-if="player.ref?.$platform === 'ios'"
|
||||
class="inline-block h-3 w-3 x-tag-platform-ios" />
|
||||
</TableCell>
|
||||
|
||||
<TableCell v-if="isColumnVisible('language')" class="w-14 text-center">
|
||||
@@ -53,10 +62,14 @@
|
||||
</TableCell>
|
||||
|
||||
<TableCell v-if="isColumnVisible('status')" class="w-10 text-center">
|
||||
<i class="x-user-status" :class="player.ref?.status ? statusClass(player.ref.status) : ''"></i>
|
||||
<i
|
||||
class="x-user-status"
|
||||
:class="player.ref?.status ? statusClass(player.ref.status) : ''"></i>
|
||||
</TableCell>
|
||||
|
||||
<TableCell v-if="isColumnVisible('timer')" class="w-20 text-right text-[11px] tabular-nums text-muted-foreground">
|
||||
<TableCell
|
||||
v-if="isColumnVisible('timer')"
|
||||
class="w-20 text-right text-[11px] tabular-nums text-muted-foreground">
|
||||
<Timer v-if="player.timer" :epoch="player.timer" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -72,7 +85,7 @@
|
||||
|
||||
<script setup>
|
||||
import { computed, onActivated, onMounted } from 'vue';
|
||||
import { Apple, Monitor, Smartphone } from 'lucide-vue-next';
|
||||
import { Apple, IdCard, Monitor, Smartphone } from 'lucide-vue-next';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
@@ -86,7 +99,7 @@
|
||||
import { Table, TableBody, TableRow, TableCell } from '@/components/ui/table';
|
||||
|
||||
const ALL_COLUMNS = ['icon', 'displayName', 'rank', 'timer', 'platform', 'language', 'status'];
|
||||
const DEFAULT_COLUMNS = ['icon', 'displayName', 'rank', 'timer'];
|
||||
const DEFAULT_COLUMNS = ['icon', 'displayName', 'timer'];
|
||||
|
||||
const props = defineProps({
|
||||
config: {
|
||||
|
||||
Reference in New Issue
Block a user