improve dashboard wideget

This commit is contained in:
pa
2026-03-13 15:14:34 +09:00
parent 36ee0feb36
commit 4d131703e7
4 changed files with 157 additions and 65 deletions

View File

@@ -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 });
}

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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: {