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

@@ -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] || {};

View File

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