feat(dashboard): comprehensive filter enhancement and UI layout fixes

- Add time range selector (1H/6H/24H/7D/All) with preference persistence
- Implement smart X-axis label formatting for multi-day usage data
- Standardize global component spacing and fix CSS @apply limitations
- Add elegant empty state UI for charts when filtered data is absent
- Update i18n translations for all new dashboard features
This commit is contained in:
Wha1eChai
2026-01-09 22:33:11 +08:00
parent 40d3d3f3b6
commit 48ad476b5f
10 changed files with 450 additions and 198 deletions

View File

@@ -144,9 +144,9 @@
<!-- Usage Trend Chart -->
<div class="view-card">
<!-- Header with Stats and Filter -->
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 mb-6">
<div class="flex flex-wrap items-center gap-4">
<div class="flex items-center gap-2">
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-6 mb-8">
<div class="flex flex-wrap items-center gap-5">
<div class="flex items-center gap-2.5">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"
class="w-4 h-4 text-neon-purple">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
@@ -157,40 +157,95 @@
</div>
<!-- Usage Stats Pills -->
<div class="flex flex-wrap gap-2 text-[10px] font-mono">
<div class="px-2.5 py-1 rounded bg-space-800 border border-space-border/50 whitespace-nowrap">
<div class="flex flex-wrap gap-2.5 text-[10px] font-mono">
<div class="px-2.5 py-1 rounded bg-space-850 border border-space-border/60 whitespace-nowrap">
<span class="text-gray-500" x-text="$store.global.t('totalColon')">Total:</span>
<span class="text-white ml-1 font-bold" x-text="usageStats.total"></span>
</div>
<div class="px-2.5 py-1 rounded bg-space-800 border border-space-border/50 whitespace-nowrap">
<div class="px-2.5 py-1 rounded bg-space-850 border border-space-border/60 whitespace-nowrap">
<span class="text-gray-500" x-text="$store.global.t('todayColon')">Today:</span>
<span class="text-neon-cyan ml-1 font-bold" x-text="usageStats.today"></span>
</div>
<div class="px-2.5 py-1 rounded bg-space-800 border border-space-border/50 whitespace-nowrap">
<div class="px-2.5 py-1 rounded bg-space-850 border border-space-border/60 whitespace-nowrap">
<span class="text-gray-500" x-text="$store.global.t('hour1Colon')">1H:</span>
<span class="text-neon-green ml-1 font-bold" x-text="usageStats.thisHour"></span>
</div>
</div>
</div>
<div class="flex items-center gap-3 w-full sm:w-auto justify-end">
<!-- Display Mode Toggle -->
<div class="join">
<button @click="setDisplayMode('family')"
class="join-item btn btn-xs px-3 border-space-border/50 bg-space-800 text-gray-400 hover:text-white transition-all"
:class="{'bg-neon-purple/20 text-neon-purple border-neon-purple/50': displayMode === 'family'}"
x-text="$store.global.t('family')">
<div class="flex items-center gap-3 w-full sm:w-auto justify-end flex-wrap">
<!-- Time Range Dropdown -->
<div class="relative">
<button @click="showTimeRangeDropdown = !showTimeRangeDropdown; showDisplayModeDropdown = false; showModelFilter = false"
class="flex items-center gap-2 px-3 py-1.5 text-[10px] font-mono text-gray-400 bg-space-800 border border-space-border/50 rounded hover:border-neon-cyan/50 transition-colors whitespace-nowrap">
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span x-text="getTimeRangeLabel()"></span>
<svg class="w-3 h-3 transition-transform" :class="{'rotate-180': showTimeRangeDropdown}" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
<button @click="setDisplayMode('model')"
class="join-item btn btn-xs px-3 border-space-border/50 bg-space-800 text-gray-400 hover:text-white transition-all"
:class="{'bg-neon-purple/20 text-neon-purple border-neon-purple/50': displayMode === 'model'}"
x-text="$store.global.t('model')">
<div x-show="showTimeRangeDropdown" @click.outside="showTimeRangeDropdown = false"
x-transition:enter="transition ease-out duration-100"
x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="opacity-100 scale-100" x-transition:leave-end="opacity-0 scale-95"
class="absolute right-0 mt-1 w-36 bg-space-900 border border-space-border rounded-lg shadow-xl z-50 overflow-hidden py-1"
style="display: none;">
<button @click="setTimeRange('1h')" class="w-full px-3 py-1.5 text-left text-[10px] font-mono hover:bg-white/5 transition-colors"
:class="timeRange === '1h' ? 'text-neon-cyan' : 'text-gray-400'"
x-text="$store.global.t('last1Hour')"></button>
<button @click="setTimeRange('6h')" class="w-full px-3 py-1.5 text-left text-[10px] font-mono hover:bg-white/5 transition-colors"
:class="timeRange === '6h' ? 'text-neon-cyan' : 'text-gray-400'"
x-text="$store.global.t('last6Hours')"></button>
<button @click="setTimeRange('24h')" class="w-full px-3 py-1.5 text-left text-[10px] font-mono hover:bg-white/5 transition-colors"
:class="timeRange === '24h' ? 'text-neon-cyan' : 'text-gray-400'"
x-text="$store.global.t('last24Hours')"></button>
<button @click="setTimeRange('7d')" class="w-full px-3 py-1.5 text-left text-[10px] font-mono hover:bg-white/5 transition-colors"
:class="timeRange === '7d' ? 'text-neon-cyan' : 'text-gray-400'"
x-text="$store.global.t('last7Days')"></button>
<button @click="setTimeRange('all')" class="w-full px-3 py-1.5 text-left text-[10px] font-mono hover:bg-white/5 transition-colors"
:class="timeRange === 'all' ? 'text-neon-cyan' : 'text-gray-400'"
x-text="$store.global.t('allTime')"></button>
</div>
</div>
<!-- Display Mode Dropdown -->
<div class="relative">
<button @click="showDisplayModeDropdown = !showDisplayModeDropdown; showTimeRangeDropdown = false; showModelFilter = false"
class="flex items-center gap-2 px-3 py-1.5 text-[10px] font-mono text-gray-400 bg-space-800 border border-space-border/50 rounded hover:border-neon-purple/50 transition-colors whitespace-nowrap">
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
<span x-text="displayMode === 'family' ? $store.global.t('family') : $store.global.t('model')"></span>
<svg class="w-3 h-3 transition-transform" :class="{'rotate-180': showDisplayModeDropdown}" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
<div x-show="showDisplayModeDropdown" @click.outside="showDisplayModeDropdown = false"
x-transition:enter="transition ease-out duration-100"
x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="opacity-100 scale-100" x-transition:leave-end="opacity-0 scale-95"
class="absolute right-0 mt-1 w-32 bg-space-900 border border-space-border rounded-lg shadow-xl z-50 overflow-hidden py-1"
style="display: none;">
<button @click="setDisplayMode('family')" class="w-full px-3 py-1.5 text-left text-[10px] font-mono hover:bg-white/5 transition-colors"
:class="displayMode === 'family' ? 'text-neon-purple' : 'text-gray-400'"
x-text="$store.global.t('family')"></button>
<button @click="setDisplayMode('model')" class="w-full px-3 py-1.5 text-left text-[10px] font-mono hover:bg-white/5 transition-colors"
:class="displayMode === 'model' ? 'text-neon-purple' : 'text-gray-400'"
x-text="$store.global.t('model')"></button>
</div>
</div>
<!-- Filter Dropdown -->
<div class="relative">
<button @click="showModelFilter = !showModelFilter"
<button @click="showModelFilter = !showModelFilter; showTimeRangeDropdown = false; showDisplayModeDropdown = false"
class="flex items-center gap-2 px-3 py-1.5 text-[10px] font-mono text-gray-400 bg-space-800 border border-space-border/50 rounded hover:border-neon-purple/50 transition-colors whitespace-nowrap">
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
@@ -293,7 +348,7 @@
</div>
<!-- Dynamic Legend -->
<div class="flex flex-wrap gap-3 mb-3"
<div class="flex flex-wrap gap-3 mb-5"
x-show="displayMode === 'family' ? selectedFamilies.length > 0 : Object.values(selectedModels).flat().length > 0">
<!-- Family Mode Legend -->
<template x-if="displayMode === 'family'">
@@ -322,7 +377,8 @@
<!-- Chart -->
<div class="h-48 w-full relative">
<canvas id="usageTrendChart"></canvas>
<!-- Loading/Empty State -->
<!-- Overall Loading State -->
<div x-show="!stats.hasTrendData"
class="absolute inset-0 flex items-center justify-center bg-space-900/50 backdrop-blur-sm z-10"
style="display: none;">
@@ -331,9 +387,29 @@
<span x-text="$store.global.t('syncing')">SYNCING...</span>
</div>
</div>
<!-- No Selection -->
<div x-show="stats.hasTrendData && (displayMode === 'family' ? selectedFamilies.length === 0 : Object.values(selectedModels).flat().length === 0)"
class="absolute inset-0 flex items-center justify-center bg-space-900/30 z-10">
<!-- Empty State (After Filtering) -->
<div x-show="stats.hasTrendData && !hasFilteredTrendData"
class="absolute inset-0 flex flex-col items-center justify-center bg-space-900/30 z-10"
style="display: none;">
<div class="flex flex-col items-center gap-4 animate-fade-in">
<div class="w-12 h-12 rounded-full bg-space-850 flex items-center justify-center text-gray-600 border border-space-border/50">
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</div>
<div class="text-xs font-mono text-gray-500 text-center">
<p x-text="$store.global.t('noDataTracked')">No data tracked yet</p>
<p class="text-[10px] opacity-60 mt-1" x-text="'[' + getTimeRangeLabel() + ']'"></p>
</div>
</div>
</div>
<!-- No Selection State -->
<div x-show="stats.hasTrendData && hasFilteredTrendData && (displayMode === 'family' ? selectedFamilies.length === 0 : Object.values(selectedModels).flat().length === 0)"
class="absolute inset-0 flex items-center justify-center bg-space-900/30 z-10"
style="display: none;">
<div class="text-xs font-mono text-gray-500"
x-text="displayMode === 'family' ? $store.global.t('selectFamilies') : $store.global.t('selectModels')">
</div>