feat: 仪表盘日期筛选默认今日并记忆用户偏好

This commit is contained in:
atoz03
2025-12-04 16:48:11 +08:00
parent 354d8da13f
commit 4919e392a5

View File

@@ -67,22 +67,72 @@ export const useDashboardStore = defineStore('dashboard', () => {
groupLabel: 'Claude账户' groupLabel: 'Claude账户'
}) })
// 本地偏好
const STORAGE_KEYS = {
preset: 'dashboard:date:preset',
granularity: 'dashboard:trend:granularity'
}
const defaultPreset = 'today'
const defaultGranularity = 'day'
const getPresetOptions = (granularity) =>
granularity === 'hour'
? [
{ value: 'last24h', label: '近24小时', hours: 24 },
{ value: 'yesterday', label: '昨天', hours: 24 },
{ value: 'dayBefore', label: '前天', hours: 24 }
]
: [
{ value: 'today', label: '今日', days: 1 },
{ value: '7days', label: '7天', days: 7 },
{ value: '30days', label: '30天', days: 30 }
]
const readFromStorage = (key, fallback) => {
try {
const value = localStorage.getItem(key)
return value || fallback
} catch (error) {
return fallback
}
}
const saveToStorage = (key, value) => {
try {
localStorage.setItem(key, value)
} catch (error) {
// 忽略存储错误,避免影响渲染
}
}
const normalizePresetForGranularity = (preset, granularity) => {
const options = getPresetOptions(granularity)
const hasPreset = options.some((opt) => opt.value === preset)
if (hasPreset) return preset
return granularity === 'hour' ? 'last24h' : defaultPreset
}
const storedGranularity = readFromStorage(STORAGE_KEYS.granularity, defaultGranularity)
const initialGranularity = ['day', 'hour'].includes(storedGranularity)
? storedGranularity
: defaultGranularity
const initialPreset = normalizePresetForGranularity(
readFromStorage(STORAGE_KEYS.preset, defaultPreset),
initialGranularity
)
// 日期筛选 // 日期筛选
const dateFilter = ref({ const dateFilter = ref({
type: 'preset', // preset 或 custom type: 'preset', // preset 或 custom
preset: '7days', // today, 7days, 30days preset: initialPreset, // today, 7days, 30days
customStart: '', customStart: '',
customEnd: '', customEnd: '',
customRange: null, customRange: null,
presetOptions: [ presetOptions: getPresetOptions(initialGranularity)
{ value: 'today', label: '今日', days: 1 },
{ value: '7days', label: '7天', days: 7 },
{ value: '30days', label: '30天', days: 30 }
]
}) })
// 趋势图粒度 // 趋势图粒度
const trendGranularity = ref('day') // 'day' 或 'hour' const trendGranularity = ref(initialGranularity) // 'day' 或 'hour'
const apiKeysTrendMetric = ref('requests') // 'requests' 或 'tokens' const apiKeysTrendMetric = ref('requests') // 'requests' 或 'tokens'
const accountUsageGroup = ref('claude') // claude | openai | gemini const accountUsageGroup = ref('claude') // claude | openai | gemini
@@ -138,6 +188,21 @@ export const useDashboardStore = defineStore('dashboard', () => {
} }
} }
const persistDatePreferences = (
preset = dateFilter.value.preset,
granularity = trendGranularity.value
) => {
saveToStorage(STORAGE_KEYS.preset, preset)
saveToStorage(STORAGE_KEYS.granularity, granularity)
}
const getEffectiveGranularity = () =>
dateFilter.value.type === 'preset' &&
dateFilter.value.preset === 'today' &&
trendGranularity.value === 'day'
? 'hour'
: trendGranularity.value
// 方法 // 方法
async function loadDashboardData(timeRange = null) { async function loadDashboardData(timeRange = null) {
loading.value = true loading.value = true
@@ -232,7 +297,7 @@ export const useDashboardStore = defineStore('dashboard', () => {
} }
} }
async function loadUsageTrend(days = 7, granularity = 'day') { async function loadUsageTrend(days = 7, granularity = getEffectiveGranularity()) {
try { try {
let url = '/admin/usage-trend?' let url = '/admin/usage-trend?'
@@ -268,6 +333,12 @@ export const useDashboardStore = defineStore('dashboard', () => {
if (dateFilter.value.type === 'preset') { if (dateFilter.value.type === 'preset') {
switch (dateFilter.value.preset) { switch (dateFilter.value.preset) {
case 'today': {
// 今日使用系统时区的当日0点-23:59
startTime = getSystemTimezoneDay(now, true)
endTime = getSystemTimezoneDay(now, false)
break
}
case 'last24h': { case 'last24h': {
// 近24小时从当前时间往前推24小时 // 近24小时从当前时间往前推24小时
endTime = new Date(now) endTime = new Date(now)
@@ -318,12 +389,13 @@ export const useDashboardStore = defineStore('dashboard', () => {
} }
} }
async function loadModelStats(period = 'daily') { async function loadModelStats(period = 'daily', granularity = null) {
const currentGranularity = granularity || getEffectiveGranularity()
try { try {
let url = `/admin/model-stats?period=${period}` let url = `/admin/model-stats?period=${period}`
// 如果是自定义时间范围或小时粒度,传递具体的时间参数 // 如果是自定义时间范围或小时粒度,传递具体的时间参数
if (dateFilter.value.type === 'custom' || trendGranularity.value === 'hour') { if (dateFilter.value.type === 'custom' || currentGranularity === 'hour') {
if (dateFilter.value.customRange && dateFilter.value.customRange.length === 2) { if (dateFilter.value.customRange && dateFilter.value.customRange.length === 2) {
// 将系统时区时间转换为UTC // 将系统时区时间转换为UTC
const convertToUTC = (systemTzTimeStr) => { const convertToUTC = (systemTzTimeStr) => {
@@ -340,12 +412,17 @@ export const useDashboardStore = defineStore('dashboard', () => {
url += `&startDate=${encodeURIComponent(convertToUTC(dateFilter.value.customRange[0]))}` url += `&startDate=${encodeURIComponent(convertToUTC(dateFilter.value.customRange[0]))}`
url += `&endDate=${encodeURIComponent(convertToUTC(dateFilter.value.customRange[1]))}` url += `&endDate=${encodeURIComponent(convertToUTC(dateFilter.value.customRange[1]))}`
} else if (trendGranularity.value === 'hour' && dateFilter.value.type === 'preset') { } else if (currentGranularity === 'hour' && dateFilter.value.type === 'preset') {
// 小时粒度的预设时间范围 // 小时粒度的预设时间范围
const now = new Date() const now = new Date()
let startTime, endTime let startTime, endTime
switch (dateFilter.value.preset) { switch (dateFilter.value.preset) {
case 'today': {
startTime = getSystemTimezoneDay(now, true)
endTime = getSystemTimezoneDay(now, false)
break
}
case 'last24h': { case 'last24h': {
endTime = new Date(now) endTime = new Date(now)
startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000) startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000)
@@ -374,7 +451,7 @@ export const useDashboardStore = defineStore('dashboard', () => {
url += `&startDate=${encodeURIComponent(startTime.toISOString())}` url += `&startDate=${encodeURIComponent(startTime.toISOString())}`
url += `&endDate=${encodeURIComponent(endTime.toISOString())}` url += `&endDate=${encodeURIComponent(endTime.toISOString())}`
} }
} else if (dateFilter.value.type === 'preset' && trendGranularity.value === 'day') { } else if (dateFilter.value.type === 'preset' && currentGranularity === 'day') {
// 天粒度的预设时间范围需要传递startDate和endDate参数 // 天粒度的预设时间范围需要传递startDate和endDate参数
const now = new Date() const now = new Date()
let startDate, endDate let startDate, endDate
@@ -409,12 +486,13 @@ export const useDashboardStore = defineStore('dashboard', () => {
} }
} }
async function loadApiKeysTrend(metric = 'requests') { async function loadApiKeysTrend(metric = 'requests', granularity = null) {
const currentGranularity = granularity || getEffectiveGranularity()
try { try {
let url = '/admin/api-keys-usage-trend?' let url = '/admin/api-keys-usage-trend?'
let days = 7 let days = 7
if (trendGranularity.value === 'hour') { if (currentGranularity === 'hour') {
// 小时粒度,计算时间范围 // 小时粒度,计算时间范围
url += `granularity=hour` url += `granularity=hour`
@@ -446,6 +524,11 @@ export const useDashboardStore = defineStore('dashboard', () => {
if (dateFilter.value.type === 'preset') { if (dateFilter.value.type === 'preset') {
switch (dateFilter.value.preset) { switch (dateFilter.value.preset) {
case 'today': {
startTime = getSystemTimezoneDay(now, true)
endTime = getSystemTimezoneDay(now, false)
break
}
case 'last24h': { case 'last24h': {
// 近24小时从当前时间往前推24小时 // 近24小时从当前时间往前推24小时
endTime = new Date(now) endTime = new Date(now)
@@ -511,12 +594,13 @@ export const useDashboardStore = defineStore('dashboard', () => {
} }
} }
async function loadAccountUsageTrend(group = accountUsageGroup.value) { async function loadAccountUsageTrend(group = accountUsageGroup.value, granularity = null) {
const currentGranularity = granularity || getEffectiveGranularity()
try { try {
let url = '/admin/account-usage-trend?' let url = '/admin/account-usage-trend?'
let days = 7 let days = 7
if (trendGranularity.value === 'hour') { if (currentGranularity === 'hour') {
url += `granularity=hour` url += `granularity=hour`
if (dateFilter.value.customRange && dateFilter.value.customRange.length === 2) { if (dateFilter.value.customRange && dateFilter.value.customRange.length === 2) {
@@ -541,6 +625,11 @@ export const useDashboardStore = defineStore('dashboard', () => {
if (dateFilter.value.type === 'preset') { if (dateFilter.value.type === 'preset') {
switch (dateFilter.value.preset) { switch (dateFilter.value.preset) {
case 'today': {
startTime = getSystemTimezoneDay(now, true)
endTime = getSystemTimezoneDay(now, false)
break
}
case 'last24h': { case 'last24h': {
endTime = new Date(now) endTime = new Date(now)
startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000) startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000)
@@ -603,110 +692,90 @@ export const useDashboardStore = defineStore('dashboard', () => {
} }
// 日期筛选相关方法 // 日期筛选相关方法
function setDateFilterPreset(preset) { function setDateFilterPreset(preset, options = {}) {
const { silent = false, skipSave = false } = options
const normalizedPreset = normalizePresetForGranularity(preset, trendGranularity.value)
dateFilter.value.type = 'preset' dateFilter.value.type = 'preset'
dateFilter.value.preset = preset dateFilter.value.preset = normalizedPreset
// 根据预设计算并设置具体的日期范围 const option = dateFilter.value.presetOptions.find((opt) => opt.value === normalizedPreset)
const option = dateFilter.value.presetOptions.find((opt) => opt.value === preset) const now = new Date()
if (option) { let startDate
const now = new Date() let endDate
let startDate, endDate
if (trendGranularity.value === 'hour') { if (trendGranularity.value === 'hour') {
// 小时粒度的预设 switch (normalizedPreset) {
switch (preset) { case 'today': {
case 'last24h': { startDate = getSystemTimezoneDay(now, true)
// 近24小时从当前时间往前推24小时 endDate = getSystemTimezoneDay(now, false)
endDate = new Date(now) break
startDate = new Date(now.getTime() - 24 * 60 * 60 * 1000)
break
}
case 'yesterday': {
// 昨天:获取本地时间的昨天
const yesterday = new Date()
yesterday.setDate(yesterday.getDate() - 1)
// 转换为系统时区的昨天0点和23:59
startDate = getSystemTimezoneDay(yesterday, true)
endDate = getSystemTimezoneDay(yesterday, false)
break
}
case 'dayBefore': {
// 前天:获取本地时间的前天
const dayBefore = new Date()
dayBefore.setDate(dayBefore.getDate() - 2)
// 转换为系统时区的前天0点和23:59
startDate = getSystemTimezoneDay(dayBefore, true)
endDate = getSystemTimezoneDay(dayBefore, false)
break
}
} }
} else { case 'last24h': {
// 天粒度的预设 endDate = new Date(now)
startDate = new Date(now) startDate = new Date(now.getTime() - 24 * 60 * 60 * 1000)
endDate = new Date(now) break
}
if (preset === 'today') { case 'yesterday': {
// 今日:从凌晨开始 const yesterday = new Date()
startDate.setHours(0, 0, 0, 0) yesterday.setDate(yesterday.getDate() - 1)
endDate.setHours(23, 59, 59, 999) startDate = getSystemTimezoneDay(yesterday, true)
} else { endDate = getSystemTimezoneDay(yesterday, false)
// 其他预设:按天数计算 break
startDate.setDate(now.getDate() - (option.days - 1)) }
startDate.setHours(0, 0, 0, 0) case 'dayBefore': {
endDate.setHours(23, 59, 59, 999) const dayBefore = new Date()
dayBefore.setDate(dayBefore.getDate() - 2)
startDate = getSystemTimezoneDay(dayBefore, true)
endDate = getSystemTimezoneDay(dayBefore, false)
break
}
default: {
endDate = new Date(now)
startDate = new Date(now.getTime() - 24 * 60 * 60 * 1000)
} }
} }
} else {
startDate = new Date(now)
endDate = new Date(now)
dateFilter.value.customStart = startDate.toISOString().split('T')[0] if (normalizedPreset === 'today') {
dateFilter.value.customEnd = endDate.toISOString().split('T')[0] startDate.setHours(0, 0, 0, 0)
endDate.setHours(23, 59, 59, 999)
// 设置 customRange 为 Element Plus 需要的格式 } else if (option?.days) {
// 对于小时粒度的昨天/前天,需要特殊处理显示 startDate.setDate(now.getDate() - (option.days - 1))
if (trendGranularity.value === 'hour' && (preset === 'yesterday' || preset === 'dayBefore')) { startDate.setHours(0, 0, 0, 0)
// 获取本地日期 endDate.setHours(23, 59, 59, 999)
const targetDate = new Date()
if (preset === 'yesterday') {
targetDate.setDate(targetDate.getDate() - 1)
} else {
targetDate.setDate(targetDate.getDate() - 2)
}
// 显示系统时区的完整一天
const year = targetDate.getFullYear()
const month = String(targetDate.getMonth() + 1).padStart(2, '0')
const day = String(targetDate.getDate()).padStart(2, '0')
dateFilter.value.customRange = [
`${year}-${month}-${day} 00:00:00`,
`${year}-${month}-${day} 23:59:59`
]
} else {
// 其他情况近24小时或天粒度
const formatDateForDisplay = (date) => {
// 固定使用UTC+8来显示时间
const systemTz = 8
const tzOffset = systemTz * 60 * 60 * 1000
const localTime = new Date(date.getTime() + tzOffset)
const year = localTime.getUTCFullYear()
const month = String(localTime.getUTCMonth() + 1).padStart(2, '0')
const day = String(localTime.getUTCDate()).padStart(2, '0')
const hours = String(localTime.getUTCHours()).padStart(2, '0')
const minutes = String(localTime.getUTCMinutes()).padStart(2, '0')
const seconds = String(localTime.getUTCSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
dateFilter.value.customRange = [
formatDateForDisplay(startDate),
formatDateForDisplay(endDate)
]
} }
} }
// 触发数据刷新 const formatDateForDisplay = (date) => {
refreshChartsData() // 固定使用UTC+8来显示时间
const systemTz = 8
const tzOffset = systemTz * 60 * 60 * 1000
const localTime = new Date(date.getTime() + tzOffset)
const year = localTime.getUTCFullYear()
const month = String(localTime.getUTCMonth() + 1).padStart(2, '0')
const day = String(localTime.getUTCDate()).padStart(2, '0')
const hours = String(localTime.getUTCHours()).padStart(2, '0')
const minutes = String(localTime.getUTCMinutes()).padStart(2, '0')
const seconds = String(localTime.getUTCSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
dateFilter.value.customStart = startDate ? startDate.toISOString().split('T')[0] : ''
dateFilter.value.customEnd = endDate ? endDate.toISOString().split('T')[0] : ''
dateFilter.value.customRange =
startDate && endDate ? [formatDateForDisplay(startDate), formatDateForDisplay(endDate)] : null
if (!skipSave) {
persistDatePreferences(dateFilter.value.preset, trendGranularity.value)
}
if (!silent) {
refreshChartsData()
}
} }
function onCustomDateRangeChange(value) { function onCustomDateRangeChange(value) {
@@ -751,20 +820,17 @@ export const useDashboardStore = defineStore('dashboard', () => {
refreshChartsData() refreshChartsData()
} else if (value === null) { } else if (value === null) {
// 清空时恢复默认 // 清空时恢复默认
setDateFilterPreset(trendGranularity.value === 'hour' ? 'last24h' : '7days') setDateFilterPreset(trendGranularity.value === 'hour' ? 'last24h' : defaultPreset)
} }
} }
function setTrendGranularity(granularity) { function setTrendGranularity(granularity, options = {}) {
const { silent = false, skipSave = false, presetOverride } = options
trendGranularity.value = granularity trendGranularity.value = granularity
// 根据粒度更新预设选项 // 根据粒度更新预设选项
if (granularity === 'hour') { if (granularity === 'hour') {
dateFilter.value.presetOptions = [ dateFilter.value.presetOptions = getPresetOptions('hour')
{ value: 'last24h', label: '近24小时', hours: 24 },
{ value: 'yesterday', label: '昨天', hours: 24 },
{ value: 'dayBefore', label: '前天', hours: 24 }
]
// 检查当前自定义日期范围是否超过24小时 // 检查当前自定义日期范围是否超过24小时
if ( if (
@@ -777,46 +843,53 @@ export const useDashboardStore = defineStore('dashboard', () => {
const hoursDiff = (end - start) / (1000 * 60 * 60) const hoursDiff = (end - start) / (1000 * 60 * 60)
if (hoursDiff > 24) { if (hoursDiff > 24) {
showToast('小时粒度下日期范围不能超过24小时已切换到近24小时', 'warning') showToast('小时粒度下日期范围不能超过24小时已切换到近24小时', 'warning')
setDateFilterPreset('last24h') setDateFilterPreset('last24h', { silent, skipSave })
return return
} }
} }
// 如果当前是天粒度的预设,切换到小时粒度的默认预设
if (['today', '7days', '30days'].includes(dateFilter.value.preset)) {
setDateFilterPreset('last24h')
return
}
} else { } else {
// 天粒度 // 天粒度
dateFilter.value.presetOptions = [ dateFilter.value.presetOptions = getPresetOptions('day')
{ value: 'today', label: '今日', days: 1 },
{ value: '7days', label: '7天', days: 7 },
{ value: '30days', label: '30天', days: 30 }
]
// 如果当前是小时粒度的预设,切换到天粒度的默认预设
if (['last24h', 'yesterday', 'dayBefore'].includes(dateFilter.value.preset)) {
setDateFilterPreset('7days')
return
}
} }
// 触发数据刷新 if (dateFilter.value.type === 'custom') {
refreshChartsData() if (!skipSave) {
persistDatePreferences(dateFilter.value.preset || defaultPreset, trendGranularity.value)
}
if (!silent) {
refreshChartsData()
}
return
}
const nextPreset =
presetOverride ||
normalizePresetForGranularity(dateFilter.value.preset, trendGranularity.value)
setDateFilterPreset(nextPreset, { silent: true, skipSave: true })
if (!skipSave) {
persistDatePreferences(dateFilter.value.preset, trendGranularity.value)
}
if (!silent) {
refreshChartsData()
}
} }
async function refreshChartsData() { async function refreshChartsData() {
// 根据当前筛选条件刷新数据 // 根据当前筛选条件刷新数据
let days let days
let modelPeriod = 'monthly' let modelPeriod = 'monthly'
const effectiveGranularity = getEffectiveGranularity()
if (dateFilter.value.type === 'preset') { if (dateFilter.value.type === 'preset') {
const option = dateFilter.value.presetOptions.find( const option = dateFilter.value.presetOptions.find(
(opt) => opt.value === dateFilter.value.preset (opt) => opt.value === dateFilter.value.preset
) )
if (trendGranularity.value === 'hour') { if (effectiveGranularity === 'hour') {
// 小时粒度 // 小时粒度
days = 1 // 小时粒度默认查看1天的数据 days = 1 // 小时粒度默认查看1天的数据
modelPeriod = 'daily' // 小时粒度使用日统计 modelPeriod = 'daily' // 小时粒度使用日统计
@@ -832,7 +905,7 @@ export const useDashboardStore = defineStore('dashboard', () => {
} }
} else { } else {
// 自定义日期范围 // 自定义日期范围
if (trendGranularity.value === 'hour') { if (effectiveGranularity === 'hour') {
// 小时粒度下的自定义范围,计算小时数 // 小时粒度下的自定义范围,计算小时数
const start = new Date(dateFilter.value.customRange[0]) const start = new Date(dateFilter.value.customRange[0])
const end = new Date(dateFilter.value.customRange[1]) const end = new Date(dateFilter.value.customRange[1])
@@ -845,16 +918,16 @@ export const useDashboardStore = defineStore('dashboard', () => {
} }
await Promise.all([ await Promise.all([
loadUsageTrend(days, trendGranularity.value), loadUsageTrend(days, effectiveGranularity),
loadModelStats(modelPeriod), loadModelStats(modelPeriod, effectiveGranularity),
loadApiKeysTrend(apiKeysTrendMetric.value), loadApiKeysTrend(apiKeysTrendMetric.value, effectiveGranularity),
loadAccountUsageTrend(accountUsageGroup.value) loadAccountUsageTrend(accountUsageGroup.value, effectiveGranularity)
]) ])
} }
function setAccountUsageGroup(group) { function setAccountUsageGroup(group) {
accountUsageGroup.value = group accountUsageGroup.value = group
return loadAccountUsageTrend(group) return loadAccountUsageTrend(group, getEffectiveGranularity())
} }
function calculateDaysBetween(start, end) { function calculateDaysBetween(start, end) {
@@ -870,6 +943,10 @@ export const useDashboardStore = defineStore('dashboard', () => {
return date > new Date() return date > new Date()
} }
// 初始化日期筛选:同步本地偏好并填充范围
setDateFilterPreset(dateFilter.value.preset, { silent: true, skipSave: true })
persistDatePreferences(dateFilter.value.preset, trendGranularity.value)
return { return {
// 状态 // 状态
loading, loading,