diff --git a/public/css/style.css b/public/css/style.css index 919d80c..3b1d734 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -1,162 +1,183 @@ :root { - /* === Background Layers === */ - --color-space-950: #09090b; - --color-space-900: #0f0f11; - --color-space-850: #121214; - --color-space-800: #18181b; - --color-space-border: #27272a; + /* === Background Layers === */ + --color-space-950: #09090b; + --color-space-900: #0f0f11; + --color-space-850: #121214; + --color-space-800: #18181b; + --color-space-border: #27272a; - /* === Neon Accents (Full Saturation) === */ - --color-neon-purple: #a855f7; - --color-neon-green: #22c55e; - --color-neon-cyan: #06b6d4; - --color-neon-yellow: #eab308; - --color-neon-red: #ef4444; + /* === Neon Accents (Full Saturation) === */ + --color-neon-purple: #a855f7; + --color-neon-green: #22c55e; + --color-neon-cyan: #06b6d4; + --color-neon-yellow: #eab308; + --color-neon-red: #ef4444; - /* === Soft Neon (Reduced Saturation for Fills) === */ - --color-neon-purple-soft: #9333ea; - --color-neon-green-soft: #16a34a; - --color-neon-cyan-soft: #0891b2; + /* === Soft Neon (Reduced Saturation for Fills) === */ + --color-neon-purple-soft: #9333ea; + --color-neon-green-soft: #16a34a; + --color-neon-cyan-soft: #0891b2; - /* === Text Hierarchy (WCAG AA Compliant) === */ - --color-text-primary: #ffffff; /* Emphasis: Titles, Key Numbers */ - --color-text-secondary: #d4d4d8; /* Content: Body Text (zinc-300) */ - --color-text-tertiary: #a1a1aa; /* Metadata: Timestamps, Labels (zinc-400) */ - --color-text-quaternary: #71717a; /* Subtle: Decorative (zinc-500) */ + /* === Text Hierarchy (WCAG AA Compliant) === */ + --color-text-primary: #ffffff; /* Emphasis: Titles, Key Numbers */ + --color-text-secondary: #d4d4d8; /* Content: Body Text (zinc-300) */ + --color-text-tertiary: #a1a1aa; /* Metadata: Timestamps, Labels (zinc-400) */ + --color-text-quaternary: #71717a; /* Subtle: Decorative (zinc-500) */ - /* === Legacy Aliases (Backward Compatibility) === */ - --color-text-main: var(--color-text-secondary); - --color-text-dim: var(--color-text-tertiary); - --color-text-muted: var(--color-text-tertiary); - --color-text-bright: var(--color-text-primary); + /* === Legacy Aliases (Backward Compatibility) === */ + --color-text-main: var(--color-text-secondary); + --color-text-dim: var(--color-text-tertiary); + --color-text-muted: var(--color-text-tertiary); + --color-text-bright: var(--color-text-primary); - /* Gradient Accents */ - --color-green-400: #4ade80; - --color-yellow-400: #facc15; - --color-red-400: #f87171; + /* Gradient Accents */ + --color-green-400: #4ade80; + --color-yellow-400: #facc15; + --color-red-400: #f87171; - /* Chart Colors */ - --color-chart-1: #a855f7; - --color-chart-2: #c084fc; - --color-chart-3: #e879f9; - --color-chart-4: #d946ef; - --color-chart-5: #22c55e; - --color-chart-6: #4ade80; - --color-chart-7: #86efac; - --color-chart-8: #10b981; - --color-chart-9: #06b6d4; - --color-chart-10: #f59e0b; - --color-chart-11: #ef4444; - --color-chart-12: #ec4899; - --color-chart-13: #8b5cf6; - --color-chart-14: #14b8a6; - --color-chart-15: #f97316; - --color-chart-16: #6366f1; + /* Chart Colors */ + --color-chart-1: #a855f7; + --color-chart-2: #c084fc; + --color-chart-3: #e879f9; + --color-chart-4: #d946ef; + --color-chart-5: #22c55e; + --color-chart-6: #4ade80; + --color-chart-7: #86efac; + --color-chart-8: #10b981; + --color-chart-9: #06b6d4; + --color-chart-10: #f59e0b; + --color-chart-11: #ef4444; + --color-chart-12: #ec4899; + --color-chart-13: #8b5cf6; + --color-chart-14: #14b8a6; + --color-chart-15: #f97316; + --color-chart-16: #6366f1; } [x-cloak] { - display: none !important; + display: none !important; } /* Custom Scrollbar */ ::-webkit-scrollbar { - width: 8px; - height: 8px; + width: 8px; + height: 8px; } ::-webkit-scrollbar-track { - background: rgba(9, 9, 11, 0.3); - border-radius: 4px; + background: rgba(9, 9, 11, 0.3); + border-radius: 4px; } ::-webkit-scrollbar-thumb { - background: linear-gradient(180deg, #27272a 0%, #18181b 100%); - border-radius: 4px; - border: 1px solid rgba(255, 255, 255, 0.05); - transition: background 0.2s ease; + background: linear-gradient(180deg, #27272a 0%, #18181b 100%); + border-radius: 4px; + border: 1px solid rgba(255, 255, 255, 0.05); + transition: background 0.2s ease; } ::-webkit-scrollbar-thumb:hover { - background: linear-gradient(180deg, #3f3f46 0%, #27272a 100%); - border-color: rgba(168, 85, 247, 0.3); + background: linear-gradient(180deg, #3f3f46 0%, #27272a 100%); + border-color: rgba(168, 85, 247, 0.3); } /* Animations */ .fade-enter-active, .fade-leave-active { - transition: opacity 0.2s ease; + transition: opacity 0.2s ease; } .fade-enter-from, .fade-leave-to { - opacity: 0; + opacity: 0; } @keyframes fadeIn { - from { opacity: 0; transform: translateY(5px); } - to { opacity: 1; transform: translateY(0); } + from { + opacity: 0; + transform: translateY(5px); + } + to { + opacity: 1; + transform: translateY(0); + } } .animate-fade-in { - animation: fadeIn 0.4s ease-out forwards; + animation: fadeIn 0.4s ease-out forwards; } /* Utility */ .glass-panel { - background: linear-gradient(135deg, - rgba(15, 15, 17, 0.75) 0%, - rgba(18, 18, 20, 0.70) 100% - ); - backdrop-filter: blur(12px); - border: 1px solid rgba(255, 255, 255, 0.08); - box-shadow: - 0 0 0 1px rgba(255, 255, 255, 0.02) inset, - 0 4px 24px rgba(0, 0, 0, 0.4); - transition: border-color 0.3s ease, box-shadow 0.3s ease; + background: linear-gradient( + 135deg, + rgba(15, 15, 17, 0.75) 0%, + rgba(18, 18, 20, 0.7) 100% + ); + backdrop-filter: blur(12px); + border: 1px solid rgba(255, 255, 255, 0.08); + box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.02) inset, + 0 4px 24px rgba(0, 0, 0, 0.4); + transition: border-color 0.3s ease, box-shadow 0.3s ease; } .glass-panel:hover { - border-color: rgba(255, 255, 255, 0.12); - box-shadow: - 0 0 0 1px rgba(255, 255, 255, 0.04) inset, - 0 8px 32px rgba(0, 0, 0, 0.5); + border-color: rgba(255, 255, 255, 0.12); + box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.04) inset, + 0 8px 32px rgba(0, 0, 0, 0.5); } .nav-item.active { - background: linear-gradient(90deg, theme('colors.neon.purple / 15%') 0%, transparent 100%); - @apply border-l-4 border-neon-purple text-white; + background: linear-gradient( + 90deg, + theme("colors.neon.purple / 15%") 0%, + transparent 100% + ); + @apply border-l-4 border-neon-purple text-white; } .nav-item { - @apply border-l-4 border-transparent transition-all duration-200; + @apply border-l-4 border-transparent transition-all duration-200; } .progress-gradient-success::-webkit-progress-value { - background-image: linear-gradient(to right, var(--color-neon-green), var(--color-green-400)); + background-image: linear-gradient( + to right, + var(--color-neon-green), + var(--color-green-400) + ); } .progress-gradient-warning::-webkit-progress-value { - background-image: linear-gradient(to right, var(--color-neon-yellow), var(--color-yellow-400)); + background-image: linear-gradient( + to right, + var(--color-neon-yellow), + var(--color-yellow-400) + ); } .progress-gradient-error::-webkit-progress-value { - background-image: linear-gradient(to right, var(--color-neon-red), var(--color-red-400)); + background-image: linear-gradient( + to right, + var(--color-neon-red), + var(--color-red-400) + ); } /* Dashboard Grid */ .stats-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); - gap: 1.5rem; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 1.5rem; } /* Tooltip Customization */ .tooltip:before { - @apply bg-space-800 border border-space-border text-gray-200 font-mono text-xs; + @apply bg-space-800 border border-space-border text-gray-200 font-mono text-xs; } .tooltip-left:before { - margin-right: 0.5rem; + margin-right: 0.5rem; } /* -------------------------------------------------------------------------- */ @@ -165,21 +186,25 @@ /* Standard Layout Constants */ :root { - --view-padding: 2rem; /* 32px - Standard Padding */ - --view-gap: 1.5rem; /* 24px - Standard component gap */ - --card-radius: 0.75rem; /* 12px */ + --view-padding: 2rem; /* 32px - Standard Padding */ + --view-gap: 2rem; /* 32px - Standard component gap */ + --card-radius: 0.75rem; /* 12px */ } @media (max-width: 768px) { - :root { - --view-padding: 1rem; - --view-gap: 1rem; - } + :root { + --view-padding: 1rem; + --view-gap: 1.25rem; + } } /* Base View Container */ .view-container { - @apply mx-auto w-full animate-fade-in flex flex-col; + display: flex; + flex-direction: column; + margin-left: auto; + margin-right: auto; + width: 100%; padding: var(--view-padding); gap: var(--view-gap); min-height: calc(100vh - 56px); /* Align with navbar height */ @@ -189,128 +214,159 @@ /* Specialized container for data-heavy pages (Logs) */ .view-container-full { - @apply w-full animate-fade-in flex flex-col; - padding: var(--view-padding); - gap: var(--view-gap); - min-height: calc(100vh - 56px); - max-width: 100%; + @apply w-full animate-fade-in flex flex-col; + padding: var(--view-padding); + gap: var(--view-gap); + min-height: calc(100vh - 56px); + max-width: 100%; } /* Centered container for form-heavy pages (Settings/Accounts) */ .view-container-centered { - @apply mx-auto w-full animate-fade-in flex flex-col; - padding: var(--view-padding); - gap: var(--view-gap); - min-height: calc(100vh - 56px); - max-width: 900px; /* Comfortable reading width for forms */ + @apply mx-auto w-full animate-fade-in flex flex-col; + padding: var(--view-padding); + gap: var(--view-gap); + min-height: calc(100vh - 56px); + max-width: 900px; /* Comfortable reading width for forms */ } /* Standard Section Header */ .view-header { - @apply flex flex-col md:flex-row md:items-end justify-between mb-2; + display: flex; + flex-direction: column; + justify-content: space-between; + margin-bottom: 0.5rem; gap: 1rem; } +@media (min-width: 768px) { + .view-header { + flex-direction: row; + align-items: flex-end; + } +} + .view-header-title { - @apply flex flex-col; + @apply flex flex-col; } .view-header-title h2 { - @apply text-2xl font-bold text-white tracking-tight; + @apply text-2xl font-bold text-white tracking-tight; } .view-header-title p { - @apply text-sm text-gray-500 mt-1; + @apply text-sm text-gray-500 mt-1; } .view-header-actions { - @apply flex items-center gap-3; + @apply flex items-center gap-3; } /* Standard Card Panel */ .view-card { - @apply glass-panel rounded-xl p-6 border border-space-border/50 relative overflow-hidden; + position: relative; + overflow: hidden; + border-radius: var(--card-radius); + padding: 1.5rem; + border: 1px solid rgba(255, 255, 255, 0.08); + background: linear-gradient(135deg, + rgba(15, 15, 17, 0.75) 0%, + rgba(18, 18, 20, 0.70) 100% + ); + backdrop-filter: blur(12px); + box-shadow: + 0 0 0 1px rgba(255, 255, 255, 0.02) inset, + 0 4px 24px rgba(0, 0, 0, 0.4); + transition: border-color 0.3s ease, box-shadow 0.3s ease; +} + +.view-card:hover { + border-color: rgba(255, 255, 255, 0.12); + box-shadow: + 0 0 0 1px rgba(255, 255, 255, 0.04) inset, + 0 8px 32px rgba(0, 0, 0, 0.5); } .view-card-header { - @apply flex items-center justify-between mb-4 pb-4 border-b border-space-border/30; + @apply flex items-center justify-between mb-4 pb-4 border-b border-space-border/30; } /* Component Unification */ .standard-table { - @apply table w-full border-separate border-spacing-0; + @apply table w-full border-separate border-spacing-0; } .standard-table thead { - @apply bg-space-900/50 text-gray-500 font-mono text-xs uppercase border-b border-space-border; + @apply bg-space-900/50 text-gray-500 font-mono text-xs uppercase border-b border-space-border; } .standard-table tbody tr { - @apply transition-all duration-200 border-b border-space-border/30 last:border-0; + @apply transition-all duration-200 border-b border-space-border/30 last:border-0; } .standard-table tbody tr:hover { - background: linear-gradient(90deg, - rgba(255, 255, 255, 0.03) 0%, - rgba(255, 255, 255, 0.05) 50%, - rgba(255, 255, 255, 0.03) 100% - ); - border-color: rgba(255, 255, 255, 0.08); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); + background: linear-gradient( + 90deg, + rgba(255, 255, 255, 0.03) 0%, + rgba(255, 255, 255, 0.05) 50%, + rgba(255, 255, 255, 0.03) 100% + ); + border-color: rgba(255, 255, 255, 0.08); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); } /* Custom Range Slider - Simplified */ .custom-range { - -webkit-appearance: none; - appearance: none; - width: 100%; - height: 4px; - background: var(--color-space-800); - border-radius: 999px; - outline: none; - cursor: pointer; + -webkit-appearance: none; + appearance: none; + width: 100%; + height: 4px; + background: var(--color-space-800); + border-radius: 999px; + outline: none; + cursor: pointer; } .custom-range::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - width: 14px; - height: 14px; - border-radius: 50%; - background: var(--range-color, var(--color-neon-purple)); - cursor: pointer; - transition: transform 0.1s ease; + -webkit-appearance: none; + appearance: none; + width: 14px; + height: 14px; + border-radius: 50%; + background: var(--range-color, var(--color-neon-purple)); + cursor: pointer; + transition: transform 0.1s ease; } .custom-range::-webkit-slider-thumb:hover { - transform: scale(1.15); + transform: scale(1.15); } .custom-range::-moz-range-thumb { - width: 14px; - height: 14px; - border: none; - border-radius: 50%; - background: var(--range-color, var(--color-neon-purple)); - cursor: pointer; - transition: transform 0.1s ease; + width: 14px; + height: 14px; + border: none; + border-radius: 50%; + background: var(--range-color, var(--color-neon-purple)); + cursor: pointer; + transition: transform 0.1s ease; } .custom-range::-moz-range-thumb:hover { - transform: scale(1.15); + transform: scale(1.15); } /* Color Variants */ .custom-range-purple { - --range-color: var(--color-neon-purple); + --range-color: var(--color-neon-purple); } .custom-range-green { - --range-color: var(--color-neon-green); + --range-color: var(--color-neon-green); } .custom-range-cyan { - --range-color: var(--color-neon-cyan); + --range-color: var(--color-neon-cyan); } .custom-range-yellow { - --range-color: var(--color-neon-yellow); + --range-color: var(--color-neon-yellow); } .custom-range-accent { - --range-color: var(--color-neon-cyan); + --range-color: var(--color-neon-cyan); } diff --git a/public/js/components/dashboard.js b/public/js/components/dashboard.js index 3757e19..74ccd4b 100644 --- a/public/js/components/dashboard.js +++ b/public/js/components/dashboard.js @@ -8,6 +8,7 @@ window.Components = window.Components || {}; window.Components.dashboard = () => ({ // Core state stats: { total: 0, active: 0, limited: 0, overallHealth: 0, hasTrendData: false }, + hasFilteredTrendData: true, charts: { quotaDistribution: null, usageTrend: null }, usageStats: { total: 0, today: 0, thisHour: 0 }, historyData: {}, @@ -165,6 +166,14 @@ window.Components.dashboard = () => ({ window.DashboardFilters.setDisplayMode(this, mode); }, + setTimeRange(range) { + window.DashboardFilters.setTimeRange(this, range); + }, + + getTimeRangeLabel() { + return window.DashboardFilters.getTimeRangeLabel(this); + }, + toggleFamily(family) { window.DashboardFilters.toggleFamily(this, family); }, diff --git a/public/js/components/dashboard/charts.js b/public/js/components/dashboard/charts.js index 71befda..0767865 100644 --- a/public/js/components/dashboard/charts.js +++ b/public/js/components/dashboard/charts.js @@ -337,13 +337,44 @@ window.DashboardCharts.updateTrendChart = function (component) { "[updateTrendChart] Canvas is ready, proceeding with chart creation" ); - const history = component.historyData; + // Use filtered history data based on time range + const history = window.DashboardFilters.getFilteredHistoryData(component); if (!history || Object.keys(history).length === 0) { - console.warn("No history data available for trend chart"); + console.warn("No history data available for trend chart (after filtering)"); + component.hasFilteredTrendData = false; _trendChartUpdateLock = false; return; } + component.hasFilteredTrendData = true; + + // Sort entries by timestamp for correct order + const sortedEntries = Object.entries(history).sort( + ([a], [b]) => new Date(a).getTime() - new Date(b).getTime() + ); + + // Determine if data spans multiple days (for smart label formatting) + const timestamps = sortedEntries.map(([iso]) => new Date(iso)); + const isMultiDay = timestamps.length > 1 && + timestamps[0].toDateString() !== timestamps[timestamps.length - 1].toDateString(); + + // Helper to format X-axis labels based on time range and multi-day status + const formatLabel = (date) => { + const timeRange = component.timeRange || '24h'; + + if (timeRange === '7d') { + // Week view: show MM/DD + return date.toLocaleDateString([], { month: '2-digit', day: '2-digit' }); + } else if (isMultiDay || timeRange === 'all') { + // Multi-day data: show MM/DD HH:MM + return date.toLocaleDateString([], { month: '2-digit', day: '2-digit' }) + ' ' + + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + } else { + // Same day: show HH:MM only + return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + } + }; + const labels = []; const datasets = []; @@ -354,11 +385,9 @@ window.DashboardCharts.updateTrendChart = function (component) { dataByFamily[family] = []; }); - Object.entries(history).forEach(([iso, hourData]) => { + sortedEntries.forEach(([iso, hourData]) => { const date = new Date(iso); - labels.push( - date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) - ); + labels.push(formatLabel(date)); component.selectedFamilies.forEach((family) => { const familyData = hourData[family]; @@ -394,11 +423,9 @@ window.DashboardCharts.updateTrendChart = function (component) { }); }); - Object.entries(history).forEach(([iso, hourData]) => { + sortedEntries.forEach(([iso, hourData]) => { const date = new Date(iso); - labels.push( - date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) - ); + labels.push(formatLabel(date)); component.families.forEach((family) => { const familyData = hourData[family] || {}; diff --git a/public/js/components/dashboard/filters.js b/public/js/components/dashboard/filters.js index 26f2752..a420518 100644 --- a/public/js/components/dashboard/filters.js +++ b/public/js/components/dashboard/filters.js @@ -10,10 +10,13 @@ window.DashboardFilters = window.DashboardFilters || {}; */ window.DashboardFilters.getInitialState = function() { return { + timeRange: '24h', // '1h', '6h', '24h', '7d', 'all' displayMode: 'model', selectedFamilies: [], selectedModels: {}, - showModelFilter: false + showModelFilter: false, + showTimeRangeDropdown: false, + showDisplayModeDropdown: false }; }; @@ -26,6 +29,7 @@ window.DashboardFilters.loadPreferences = function(component) { const saved = localStorage.getItem('dashboard_chart_prefs'); if (saved) { const prefs = JSON.parse(saved); + component.timeRange = prefs.timeRange || '24h'; component.displayMode = prefs.displayMode || 'model'; component.selectedFamilies = prefs.selectedFamilies || []; component.selectedModels = prefs.selectedModels || {}; @@ -42,6 +46,7 @@ window.DashboardFilters.loadPreferences = function(component) { window.DashboardFilters.savePreferences = function(component) { try { localStorage.setItem('dashboard_chart_prefs', JSON.stringify({ + timeRange: component.timeRange, displayMode: component.displayMode, selectedFamilies: component.selectedFamilies, selectedModels: component.selectedModels @@ -58,11 +63,78 @@ window.DashboardFilters.savePreferences = function(component) { */ window.DashboardFilters.setDisplayMode = function(component, mode) { component.displayMode = mode; + component.showDisplayModeDropdown = false; window.DashboardFilters.savePreferences(component); // updateTrendChart uses debounce internally, call directly component.updateTrendChart(); }; +/** + * Set time range filter + * @param {object} component - Dashboard component instance + * @param {string} range - '1h', '6h', '24h', '7d', 'all' + */ +window.DashboardFilters.setTimeRange = function(component, range) { + component.timeRange = range; + component.showTimeRangeDropdown = false; + window.DashboardFilters.savePreferences(component); + component.updateTrendChart(); +}; + +/** + * Get time range cutoff timestamp + * @param {string} range - Time range code + * @returns {number|null} Cutoff timestamp or null for 'all' + */ +window.DashboardFilters.getTimeRangeCutoff = function(range) { + const now = Date.now(); + switch (range) { + case '1h': return now - 1 * 60 * 60 * 1000; + case '6h': return now - 6 * 60 * 60 * 1000; + case '24h': return now - 24 * 60 * 60 * 1000; + case '7d': return now - 7 * 24 * 60 * 60 * 1000; + default: return null; // 'all' + } +}; + +/** + * Get filtered history data based on time range + * @param {object} component - Dashboard component instance + * @returns {object} Filtered history data + */ +window.DashboardFilters.getFilteredHistoryData = function(component) { + const history = component.historyData; + if (!history || Object.keys(history).length === 0) return {}; + + const cutoff = window.DashboardFilters.getTimeRangeCutoff(component.timeRange); + if (!cutoff) return history; // 'all' - return everything + + const filtered = {}; + Object.entries(history).forEach(([iso, data]) => { + const timestamp = new Date(iso).getTime(); + if (timestamp >= cutoff) { + filtered[iso] = data; + } + }); + return filtered; +}; + +/** + * Get time range label for display + * @param {object} component - Dashboard component instance + * @returns {string} Translated label + */ +window.DashboardFilters.getTimeRangeLabel = function(component) { + const store = Alpine.store('global'); + switch (component.timeRange) { + case '1h': return store.t('last1Hour'); + case '6h': return store.t('last6Hours'); + case '24h': return store.t('last24Hours'); + case '7d': return store.t('last7Days'); + default: return store.t('allTime'); + } +}; + /** * Toggle family selection * @param {object} component - Dashboard component instance diff --git a/public/js/store.js b/public/js/store.js index 59f61a6..15ee217 100644 --- a/public/js/store.js +++ b/public/js/store.js @@ -160,9 +160,15 @@ document.addEventListener('alpine:init', () => { claudeEmpty: "Claude Empty", geminiActive: "Gemini Active", geminiEmpty: "Gemini Empty", - fix: "FIX", synced: "SYNCED", syncing: "SYNCING...", + // Time range labels + last1Hour: "Last 1H", + last6Hours: "Last 6H", + last24Hours: "Last 24H", + last7Days: "Last 7D", + allTime: "All Time", + groupBy: "Group By", // Additional reloading: "Reloading...", reloaded: "Reloaded", @@ -401,9 +407,15 @@ document.addEventListener('alpine:init', () => { claudeEmpty: "Claude 耗尽", geminiActive: "Gemini 活跃", geminiEmpty: "Gemini 耗尽", - fix: "修复", synced: "已同步", syncing: "正在同步...", + // 时间范围标签 + last1Hour: "最近 1 小时", + last6Hours: "最近 6 小时", + last24Hours: "最近 24 小时", + last7Days: "最近 7 天", + allTime: "最后全部", + groupBy: "分组方式", // Additional reloading: "正在重载...", reloaded: "已重载", diff --git a/public/views/dashboard.html b/public/views/dashboard.html index 29d3232..af18c95 100644 --- a/public/views/dashboard.html +++ b/public/views/dashboard.html @@ -144,9 +144,9 @@
-
-
-
+
+
+
-
-
+
+
Total:
-
+
Today:
-
+
1H:
-
- -
- - + + + + +
+
+ + +
+ +
-