|
|
import { defineStore } from 'pinia' |
|
|
import { ref, computed } from 'vue' |
|
|
import { apiClient } from '@/config/api' |
|
|
import { showToast } from '@/utils/toast' |
|
|
|
|
|
export const useDashboardStore = defineStore('dashboard', () => { |
|
|
|
|
|
const loading = ref(false) |
|
|
const dashboardData = ref({ |
|
|
totalApiKeys: 0, |
|
|
activeApiKeys: 0, |
|
|
totalAccounts: 0, |
|
|
normalAccounts: 0, |
|
|
abnormalAccounts: 0, |
|
|
pausedAccounts: 0, |
|
|
activeAccounts: 0, |
|
|
rateLimitedAccounts: 0, |
|
|
accountsByPlatform: { |
|
|
claude: { total: 0, normal: 0, abnormal: 0, paused: 0, rateLimited: 0 }, |
|
|
'claude-console': { total: 0, normal: 0, abnormal: 0, paused: 0, rateLimited: 0 }, |
|
|
gemini: { total: 0, normal: 0, abnormal: 0, paused: 0, rateLimited: 0 }, |
|
|
openai: { total: 0, normal: 0, abnormal: 0, paused: 0, rateLimited: 0 }, |
|
|
azure_openai: { total: 0, normal: 0, abnormal: 0, paused: 0, rateLimited: 0 }, |
|
|
bedrock: { total: 0, normal: 0, abnormal: 0, paused: 0, rateLimited: 0 } |
|
|
}, |
|
|
todayRequests: 0, |
|
|
totalRequests: 0, |
|
|
todayTokens: 0, |
|
|
todayInputTokens: 0, |
|
|
todayOutputTokens: 0, |
|
|
totalTokens: 0, |
|
|
totalInputTokens: 0, |
|
|
totalOutputTokens: 0, |
|
|
totalCacheCreateTokens: 0, |
|
|
totalCacheReadTokens: 0, |
|
|
todayCacheCreateTokens: 0, |
|
|
todayCacheReadTokens: 0, |
|
|
systemRPM: 0, |
|
|
systemTPM: 0, |
|
|
realtimeRPM: 0, |
|
|
realtimeTPM: 0, |
|
|
metricsWindow: 5, |
|
|
isHistoricalMetrics: false, |
|
|
systemStatus: '正常', |
|
|
uptime: 0, |
|
|
systemTimezone: 8 |
|
|
}) |
|
|
|
|
|
const costsData = ref({ |
|
|
todayCosts: { totalCost: 0, formatted: { totalCost: '$0.000000' } }, |
|
|
totalCosts: { totalCost: 0, formatted: { totalCost: '$0.000000' } } |
|
|
}) |
|
|
|
|
|
const modelStats = ref([]) |
|
|
const trendData = ref([]) |
|
|
const dashboardModelStats = ref([]) |
|
|
const apiKeysTrendData = ref({ |
|
|
data: [], |
|
|
topApiKeys: [], |
|
|
totalApiKeys: 0 |
|
|
}) |
|
|
const accountUsageTrendData = ref({ |
|
|
data: [], |
|
|
topAccounts: [], |
|
|
totalAccounts: 0, |
|
|
group: 'claude', |
|
|
groupLabel: 'Claude账户' |
|
|
}) |
|
|
|
|
|
|
|
|
const dateFilter = ref({ |
|
|
type: 'preset', |
|
|
preset: '7days', |
|
|
customStart: '', |
|
|
customEnd: '', |
|
|
customRange: null, |
|
|
presetOptions: [ |
|
|
{ value: 'today', label: '今日', days: 1 }, |
|
|
{ value: '7days', label: '7天', days: 7 }, |
|
|
{ value: '30days', label: '30天', days: 30 } |
|
|
] |
|
|
}) |
|
|
|
|
|
|
|
|
const trendGranularity = ref('day') |
|
|
const apiKeysTrendMetric = ref('requests') |
|
|
const accountUsageGroup = ref('claude') |
|
|
|
|
|
|
|
|
const defaultTime = ref([new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 2, 1, 23, 59, 59)]) |
|
|
|
|
|
|
|
|
const formattedUptime = computed(() => { |
|
|
const seconds = dashboardData.value.uptime |
|
|
const days = Math.floor(seconds / 86400) |
|
|
const hours = Math.floor((seconds % 86400) / 3600) |
|
|
const minutes = Math.floor((seconds % 3600) / 60) |
|
|
|
|
|
if (days > 0) { |
|
|
return `${days}天 ${hours}小时` |
|
|
} else if (hours > 0) { |
|
|
return `${hours}小时 ${minutes}分钟` |
|
|
} else { |
|
|
return `${minutes}分钟` |
|
|
} |
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function getSystemTimezoneDay(localDate, startOfDay = true) { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const year = localDate.getFullYear() |
|
|
const month = localDate.getMonth() |
|
|
const day = localDate.getDate() |
|
|
|
|
|
if (startOfDay) { |
|
|
|
|
|
|
|
|
|
|
|
return new Date(Date.UTC(year, month, day - 1, 16, 0, 0, 0)) |
|
|
} else { |
|
|
|
|
|
|
|
|
|
|
|
return new Date(Date.UTC(year, month, day, 15, 59, 59, 999)) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async function loadDashboardData(timeRange = null) { |
|
|
loading.value = true |
|
|
try { |
|
|
|
|
|
let costsParams = { today: 'today', all: 'all' } |
|
|
|
|
|
if (timeRange) { |
|
|
const periodMapping = { |
|
|
today: { today: 'today', all: 'today' }, |
|
|
'7days': { today: '7days', all: '7days' }, |
|
|
monthly: { today: 'monthly', all: 'monthly' }, |
|
|
all: { today: 'today', all: 'all' } |
|
|
} |
|
|
costsParams = periodMapping[timeRange] || costsParams |
|
|
} |
|
|
|
|
|
const [dashboardResponse, todayCostsResponse, totalCostsResponse] = await Promise.all([ |
|
|
apiClient.get('/admin/dashboard'), |
|
|
apiClient.get(`/admin/usage-costs?period=${costsParams.today}`), |
|
|
apiClient.get(`/admin/usage-costs?period=${costsParams.all}`) |
|
|
]) |
|
|
|
|
|
if (dashboardResponse.success) { |
|
|
const overview = dashboardResponse.data.overview || {} |
|
|
const recentActivity = dashboardResponse.data.recentActivity || {} |
|
|
const systemAverages = dashboardResponse.data.systemAverages || {} |
|
|
const realtimeMetrics = dashboardResponse.data.realtimeMetrics || {} |
|
|
const systemHealth = dashboardResponse.data.systemHealth || {} |
|
|
|
|
|
dashboardData.value = { |
|
|
totalApiKeys: overview.totalApiKeys || 0, |
|
|
activeApiKeys: overview.activeApiKeys || 0, |
|
|
|
|
|
totalAccounts: overview.totalAccounts || overview.totalClaudeAccounts || 0, |
|
|
normalAccounts: overview.normalAccounts || 0, |
|
|
abnormalAccounts: overview.abnormalAccounts || 0, |
|
|
pausedAccounts: overview.pausedAccounts || 0, |
|
|
activeAccounts: overview.activeAccounts || overview.activeClaudeAccounts || 0, |
|
|
rateLimitedAccounts: |
|
|
overview.rateLimitedAccounts || overview.rateLimitedClaudeAccounts || 0, |
|
|
|
|
|
accountsByPlatform: overview.accountsByPlatform || { |
|
|
claude: { total: 0, normal: 0, abnormal: 0, paused: 0, rateLimited: 0 }, |
|
|
'claude-console': { total: 0, normal: 0, abnormal: 0, paused: 0, rateLimited: 0 }, |
|
|
gemini: { total: 0, normal: 0, abnormal: 0, paused: 0, rateLimited: 0 }, |
|
|
openai: { total: 0, normal: 0, abnormal: 0, paused: 0, rateLimited: 0 }, |
|
|
azure_openai: { total: 0, normal: 0, abnormal: 0, paused: 0, rateLimited: 0 }, |
|
|
bedrock: { total: 0, normal: 0, abnormal: 0, paused: 0, rateLimited: 0 } |
|
|
}, |
|
|
todayRequests: recentActivity.requestsToday || 0, |
|
|
totalRequests: overview.totalRequestsUsed || 0, |
|
|
todayTokens: recentActivity.tokensToday || 0, |
|
|
todayInputTokens: recentActivity.inputTokensToday || 0, |
|
|
todayOutputTokens: recentActivity.outputTokensToday || 0, |
|
|
totalTokens: overview.totalTokensUsed || 0, |
|
|
totalInputTokens: overview.totalInputTokensUsed || 0, |
|
|
totalOutputTokens: overview.totalOutputTokensUsed || 0, |
|
|
totalCacheCreateTokens: overview.totalCacheCreateTokensUsed || 0, |
|
|
totalCacheReadTokens: overview.totalCacheReadTokensUsed || 0, |
|
|
todayCacheCreateTokens: recentActivity.cacheCreateTokensToday || 0, |
|
|
todayCacheReadTokens: recentActivity.cacheReadTokensToday || 0, |
|
|
systemRPM: systemAverages.rpm || 0, |
|
|
systemTPM: systemAverages.tpm || 0, |
|
|
realtimeRPM: realtimeMetrics.rpm || 0, |
|
|
realtimeTPM: realtimeMetrics.tpm || 0, |
|
|
metricsWindow: realtimeMetrics.windowMinutes || 5, |
|
|
isHistoricalMetrics: realtimeMetrics.isHistorical || false, |
|
|
systemStatus: systemHealth.redisConnected ? '正常' : '异常', |
|
|
uptime: systemHealth.uptime || 0, |
|
|
systemTimezone: dashboardResponse.data.systemTimezone || 8 |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (todayCostsResponse.success && totalCostsResponse.success) { |
|
|
costsData.value = { |
|
|
todayCosts: todayCostsResponse.data.totalCosts || { |
|
|
totalCost: 0, |
|
|
formatted: { totalCost: '$0.000000' } |
|
|
}, |
|
|
totalCosts: totalCostsResponse.data.totalCosts || { |
|
|
totalCost: 0, |
|
|
formatted: { totalCost: '$0.000000' } |
|
|
} |
|
|
} |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('加载仪表板数据失败:', error) |
|
|
} finally { |
|
|
loading.value = false |
|
|
} |
|
|
} |
|
|
|
|
|
async function loadUsageTrend(days = 7, granularity = 'day') { |
|
|
try { |
|
|
let url = '/admin/usage-trend?' |
|
|
|
|
|
if (granularity === 'hour') { |
|
|
|
|
|
url += `granularity=hour` |
|
|
|
|
|
if (dateFilter.value.customRange && dateFilter.value.customRange.length === 2) { |
|
|
|
|
|
const convertToUTC = (systemTzTimeStr) => { |
|
|
|
|
|
const systemTz = 8 |
|
|
|
|
|
const [datePart, timePart] = systemTzTimeStr.split(' ') |
|
|
const [year, month, day] = datePart.split('-').map(Number) |
|
|
const [hours, minutes, seconds] = timePart.split(':').map(Number) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const utcDate = new Date( |
|
|
Date.UTC(year, month - 1, day, hours - systemTz, minutes, seconds) |
|
|
) |
|
|
return utcDate.toISOString() |
|
|
} |
|
|
|
|
|
url += `&startDate=${encodeURIComponent(convertToUTC(dateFilter.value.customRange[0]))}` |
|
|
url += `&endDate=${encodeURIComponent(convertToUTC(dateFilter.value.customRange[1]))}` |
|
|
} else { |
|
|
|
|
|
const now = new Date() |
|
|
let startTime, endTime |
|
|
|
|
|
if (dateFilter.value.type === 'preset') { |
|
|
switch (dateFilter.value.preset) { |
|
|
case 'last24h': { |
|
|
|
|
|
endTime = new Date(now) |
|
|
startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000) |
|
|
break |
|
|
} |
|
|
case 'yesterday': { |
|
|
const yesterday = new Date() |
|
|
yesterday.setDate(yesterday.getDate() - 1) |
|
|
startTime = getSystemTimezoneDay(yesterday, true) |
|
|
endTime = getSystemTimezoneDay(yesterday, false) |
|
|
break |
|
|
} |
|
|
case 'dayBefore': { |
|
|
|
|
|
const dayBefore = new Date() |
|
|
dayBefore.setDate(dayBefore.getDate() - 2) |
|
|
startTime = getSystemTimezoneDay(dayBefore, true) |
|
|
endTime = getSystemTimezoneDay(dayBefore, false) |
|
|
break |
|
|
} |
|
|
default: { |
|
|
|
|
|
startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000) |
|
|
endTime = now |
|
|
} |
|
|
} |
|
|
} else { |
|
|
|
|
|
startTime = new Date(now.getTime() - days * 24 * 60 * 60 * 1000) |
|
|
endTime = now |
|
|
} |
|
|
|
|
|
url += `&startDate=${encodeURIComponent(startTime.toISOString())}` |
|
|
url += `&endDate=${encodeURIComponent(endTime.toISOString())}` |
|
|
} |
|
|
} else { |
|
|
|
|
|
url += `granularity=day&days=${days}` |
|
|
} |
|
|
|
|
|
const response = await apiClient.get(url) |
|
|
if (response.success) { |
|
|
trendData.value = response.data |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('加载使用趋势失败:', error) |
|
|
} |
|
|
} |
|
|
|
|
|
async function loadModelStats(period = 'daily') { |
|
|
try { |
|
|
let url = `/admin/model-stats?period=${period}` |
|
|
|
|
|
|
|
|
if (dateFilter.value.type === 'custom' || trendGranularity.value === 'hour') { |
|
|
if (dateFilter.value.customRange && dateFilter.value.customRange.length === 2) { |
|
|
|
|
|
const convertToUTC = (systemTzTimeStr) => { |
|
|
const systemTz = 8 |
|
|
const [datePart, timePart] = systemTzTimeStr.split(' ') |
|
|
const [year, month, day] = datePart.split('-').map(Number) |
|
|
const [hours, minutes, seconds] = timePart.split(':').map(Number) |
|
|
|
|
|
const utcDate = new Date( |
|
|
Date.UTC(year, month - 1, day, hours - systemTz, minutes, seconds) |
|
|
) |
|
|
return utcDate.toISOString() |
|
|
} |
|
|
|
|
|
url += `&startDate=${encodeURIComponent(convertToUTC(dateFilter.value.customRange[0]))}` |
|
|
url += `&endDate=${encodeURIComponent(convertToUTC(dateFilter.value.customRange[1]))}` |
|
|
} else if (trendGranularity.value === 'hour' && dateFilter.value.type === 'preset') { |
|
|
|
|
|
const now = new Date() |
|
|
let startTime, endTime |
|
|
|
|
|
switch (dateFilter.value.preset) { |
|
|
case 'last24h': { |
|
|
endTime = new Date(now) |
|
|
startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000) |
|
|
break |
|
|
} |
|
|
case 'yesterday': { |
|
|
const yesterday = new Date() |
|
|
yesterday.setDate(yesterday.getDate() - 1) |
|
|
startTime = getSystemTimezoneDay(yesterday, true) |
|
|
endTime = getSystemTimezoneDay(yesterday, false) |
|
|
break |
|
|
} |
|
|
case 'dayBefore': { |
|
|
const dayBefore = new Date() |
|
|
dayBefore.setDate(dayBefore.getDate() - 2) |
|
|
startTime = getSystemTimezoneDay(dayBefore, true) |
|
|
endTime = getSystemTimezoneDay(dayBefore, false) |
|
|
break |
|
|
} |
|
|
default: { |
|
|
startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000) |
|
|
endTime = now |
|
|
} |
|
|
} |
|
|
|
|
|
url += `&startDate=${encodeURIComponent(startTime.toISOString())}` |
|
|
url += `&endDate=${encodeURIComponent(endTime.toISOString())}` |
|
|
} |
|
|
} else if (dateFilter.value.type === 'preset' && trendGranularity.value === 'day') { |
|
|
|
|
|
const now = new Date() |
|
|
let startDate, endDate |
|
|
|
|
|
const option = dateFilter.value.presetOptions.find( |
|
|
(opt) => opt.value === dateFilter.value.preset |
|
|
) |
|
|
if (option) { |
|
|
if (dateFilter.value.preset === 'today') { |
|
|
|
|
|
startDate = getSystemTimezoneDay(now, true) |
|
|
endDate = getSystemTimezoneDay(now, false) |
|
|
} else { |
|
|
|
|
|
const daysAgo = new Date() |
|
|
daysAgo.setDate(daysAgo.getDate() - (option.days - 1)) |
|
|
startDate = getSystemTimezoneDay(daysAgo, true) |
|
|
endDate = getSystemTimezoneDay(now, false) |
|
|
} |
|
|
|
|
|
url += `&startDate=${encodeURIComponent(startDate.toISOString())}` |
|
|
url += `&endDate=${encodeURIComponent(endDate.toISOString())}` |
|
|
} |
|
|
} |
|
|
|
|
|
const response = await apiClient.get(url) |
|
|
if (response.success) { |
|
|
dashboardModelStats.value = response.data |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('加载模型统计失败:', error) |
|
|
} |
|
|
} |
|
|
|
|
|
async function loadApiKeysTrend(metric = 'requests') { |
|
|
try { |
|
|
let url = '/admin/api-keys-usage-trend?' |
|
|
let days = 7 |
|
|
|
|
|
if (trendGranularity.value === 'hour') { |
|
|
|
|
|
url += `granularity=hour` |
|
|
|
|
|
if (dateFilter.value.customRange && dateFilter.value.customRange.length === 2) { |
|
|
|
|
|
const convertToUTC = (systemTzTimeStr) => { |
|
|
|
|
|
const systemTz = 8 |
|
|
|
|
|
const [datePart, timePart] = systemTzTimeStr.split(' ') |
|
|
const [year, month, day] = datePart.split('-').map(Number) |
|
|
const [hours, minutes, seconds] = timePart.split(':').map(Number) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const utcDate = new Date( |
|
|
Date.UTC(year, month - 1, day, hours - systemTz, minutes, seconds) |
|
|
) |
|
|
return utcDate.toISOString() |
|
|
} |
|
|
|
|
|
url += `&startDate=${encodeURIComponent(convertToUTC(dateFilter.value.customRange[0]))}` |
|
|
url += `&endDate=${encodeURIComponent(convertToUTC(dateFilter.value.customRange[1]))}` |
|
|
} else { |
|
|
|
|
|
const now = new Date() |
|
|
let startTime, endTime |
|
|
|
|
|
if (dateFilter.value.type === 'preset') { |
|
|
switch (dateFilter.value.preset) { |
|
|
case 'last24h': { |
|
|
|
|
|
endTime = new Date(now) |
|
|
startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000) |
|
|
break |
|
|
} |
|
|
case 'yesterday': { |
|
|
|
|
|
const yesterday = new Date() |
|
|
yesterday.setDate(yesterday.getDate() - 1) |
|
|
startTime = getSystemTimezoneDay(yesterday, true) |
|
|
endTime = getSystemTimezoneDay(yesterday, false) |
|
|
break |
|
|
} |
|
|
case 'dayBefore': { |
|
|
|
|
|
const dayBefore = new Date() |
|
|
dayBefore.setDate(dayBefore.getDate() - 2) |
|
|
startTime = getSystemTimezoneDay(dayBefore, true) |
|
|
endTime = getSystemTimezoneDay(dayBefore, false) |
|
|
break |
|
|
} |
|
|
default: { |
|
|
|
|
|
startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000) |
|
|
endTime = now |
|
|
} |
|
|
} |
|
|
} else { |
|
|
|
|
|
startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000) |
|
|
endTime = now |
|
|
} |
|
|
|
|
|
url += `&startDate=${encodeURIComponent(startTime.toISOString())}` |
|
|
url += `&endDate=${encodeURIComponent(endTime.toISOString())}` |
|
|
} |
|
|
} else { |
|
|
|
|
|
days = |
|
|
dateFilter.value.type === 'preset' |
|
|
? dateFilter.value.preset === 'today' |
|
|
? 1 |
|
|
: dateFilter.value.preset === '7days' |
|
|
? 7 |
|
|
: 30 |
|
|
: calculateDaysBetween(dateFilter.value.customStart, dateFilter.value.customEnd) |
|
|
url += `granularity=day&days=${days}` |
|
|
} |
|
|
|
|
|
url += `&metric=${metric}` |
|
|
|
|
|
const response = await apiClient.get(url) |
|
|
if (response.success) { |
|
|
apiKeysTrendData.value = { |
|
|
data: response.data || [], |
|
|
topApiKeys: response.topApiKeys || [], |
|
|
totalApiKeys: response.totalApiKeys || 0 |
|
|
} |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('加载API Keys趋势失败:', error) |
|
|
} |
|
|
} |
|
|
|
|
|
async function loadAccountUsageTrend(group = accountUsageGroup.value) { |
|
|
try { |
|
|
let url = '/admin/account-usage-trend?' |
|
|
let days = 7 |
|
|
|
|
|
if (trendGranularity.value === 'hour') { |
|
|
url += `granularity=hour` |
|
|
|
|
|
if (dateFilter.value.customRange && dateFilter.value.customRange.length === 2) { |
|
|
const convertToUTC = (systemTzTimeStr) => { |
|
|
const systemTz = 8 |
|
|
const [datePart, timePart] = systemTzTimeStr.split(' ') |
|
|
const [year, month, day] = datePart.split('-').map(Number) |
|
|
const [hours, minutes, seconds] = timePart.split(':').map(Number) |
|
|
|
|
|
const utcDate = new Date( |
|
|
Date.UTC(year, month - 1, day, hours - systemTz, minutes, seconds) |
|
|
) |
|
|
return utcDate.toISOString() |
|
|
} |
|
|
|
|
|
url += `&startDate=${encodeURIComponent(convertToUTC(dateFilter.value.customRange[0]))}` |
|
|
url += `&endDate=${encodeURIComponent(convertToUTC(dateFilter.value.customRange[1]))}` |
|
|
} else { |
|
|
const now = new Date() |
|
|
let startTime |
|
|
let endTime |
|
|
|
|
|
if (dateFilter.value.type === 'preset') { |
|
|
switch (dateFilter.value.preset) { |
|
|
case 'last24h': { |
|
|
endTime = new Date(now) |
|
|
startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000) |
|
|
break |
|
|
} |
|
|
case 'yesterday': { |
|
|
const yesterday = new Date() |
|
|
yesterday.setDate(yesterday.getDate() - 1) |
|
|
startTime = getSystemTimezoneDay(yesterday, true) |
|
|
endTime = getSystemTimezoneDay(yesterday, false) |
|
|
break |
|
|
} |
|
|
case 'dayBefore': { |
|
|
const dayBefore = new Date() |
|
|
dayBefore.setDate(dayBefore.getDate() - 2) |
|
|
startTime = getSystemTimezoneDay(dayBefore, true) |
|
|
endTime = getSystemTimezoneDay(dayBefore, false) |
|
|
break |
|
|
} |
|
|
default: { |
|
|
startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000) |
|
|
endTime = now |
|
|
} |
|
|
} |
|
|
} else { |
|
|
startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000) |
|
|
endTime = now |
|
|
} |
|
|
|
|
|
url += `&startDate=${encodeURIComponent(startTime.toISOString())}` |
|
|
url += `&endDate=${encodeURIComponent(endTime.toISOString())}` |
|
|
} |
|
|
} else { |
|
|
days = |
|
|
dateFilter.value.type === 'preset' |
|
|
? dateFilter.value.preset === 'today' |
|
|
? 1 |
|
|
: dateFilter.value.preset === '7days' |
|
|
? 7 |
|
|
: 30 |
|
|
: calculateDaysBetween(dateFilter.value.customStart, dateFilter.value.customEnd) |
|
|
url += `granularity=day&days=${days}` |
|
|
} |
|
|
|
|
|
url += `&group=${group}` |
|
|
|
|
|
const response = await apiClient.get(url) |
|
|
if (response.success) { |
|
|
accountUsageTrendData.value = { |
|
|
data: response.data || [], |
|
|
topAccounts: response.topAccounts || [], |
|
|
totalAccounts: response.totalAccounts || 0, |
|
|
group: response.group || group, |
|
|
groupLabel: response.groupLabel || '' |
|
|
} |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('加载账号使用趋势失败:', error) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function setDateFilterPreset(preset) { |
|
|
dateFilter.value.type = 'preset' |
|
|
dateFilter.value.preset = preset |
|
|
|
|
|
|
|
|
const option = dateFilter.value.presetOptions.find((opt) => opt.value === preset) |
|
|
if (option) { |
|
|
const now = new Date() |
|
|
let startDate, endDate |
|
|
|
|
|
if (trendGranularity.value === 'hour') { |
|
|
|
|
|
switch (preset) { |
|
|
case 'last24h': { |
|
|
|
|
|
endDate = new Date(now) |
|
|
startDate = new Date(now.getTime() - 24 * 60 * 60 * 1000) |
|
|
break |
|
|
} |
|
|
case 'yesterday': { |
|
|
|
|
|
const yesterday = new Date() |
|
|
yesterday.setDate(yesterday.getDate() - 1) |
|
|
|
|
|
startDate = getSystemTimezoneDay(yesterday, true) |
|
|
endDate = getSystemTimezoneDay(yesterday, false) |
|
|
break |
|
|
} |
|
|
case 'dayBefore': { |
|
|
|
|
|
const dayBefore = new Date() |
|
|
dayBefore.setDate(dayBefore.getDate() - 2) |
|
|
|
|
|
startDate = getSystemTimezoneDay(dayBefore, true) |
|
|
endDate = getSystemTimezoneDay(dayBefore, false) |
|
|
break |
|
|
} |
|
|
} |
|
|
} else { |
|
|
|
|
|
startDate = new Date(now) |
|
|
endDate = new Date(now) |
|
|
|
|
|
if (preset === 'today') { |
|
|
|
|
|
startDate.setHours(0, 0, 0, 0) |
|
|
endDate.setHours(23, 59, 59, 999) |
|
|
} else { |
|
|
|
|
|
startDate.setDate(now.getDate() - (option.days - 1)) |
|
|
startDate.setHours(0, 0, 0, 0) |
|
|
endDate.setHours(23, 59, 59, 999) |
|
|
} |
|
|
} |
|
|
|
|
|
dateFilter.value.customStart = startDate.toISOString().split('T')[0] |
|
|
dateFilter.value.customEnd = endDate.toISOString().split('T')[0] |
|
|
|
|
|
|
|
|
|
|
|
if (trendGranularity.value === 'hour' && (preset === 'yesterday' || preset === 'dayBefore')) { |
|
|
|
|
|
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 { |
|
|
|
|
|
const formatDateForDisplay = (date) => { |
|
|
|
|
|
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) |
|
|
] |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
refreshChartsData() |
|
|
} |
|
|
|
|
|
function onCustomDateRangeChange(value) { |
|
|
if (value && value.length === 2) { |
|
|
dateFilter.value.type = 'custom' |
|
|
dateFilter.value.preset = '' |
|
|
dateFilter.value.customRange = value |
|
|
dateFilter.value.customStart = value[0].split(' ')[0] |
|
|
dateFilter.value.customEnd = value[1].split(' ')[0] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const parseSystemTime = (timeStr) => { |
|
|
const [datePart, timePart] = timeStr.split(' ') |
|
|
const [year, month, day] = datePart.split('-').map(Number) |
|
|
const [hours, minutes, seconds] = timePart.split(':').map(Number) |
|
|
return new Date(year, month - 1, day, hours, minutes, seconds) |
|
|
} |
|
|
|
|
|
const start = parseSystemTime(value[0]) |
|
|
const end = parseSystemTime(value[1]) |
|
|
|
|
|
if (trendGranularity.value === 'hour') { |
|
|
|
|
|
const hoursDiff = (end - start) / (1000 * 60 * 60) |
|
|
if (hoursDiff > 24) { |
|
|
showToast('小时粒度下日期范围不能超过24小时', 'warning') |
|
|
return |
|
|
} |
|
|
} else { |
|
|
|
|
|
const daysDiff = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1 |
|
|
if (daysDiff > 31) { |
|
|
showToast('日期范围不能超过 31 天', 'warning') |
|
|
return |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
refreshChartsData() |
|
|
} else if (value === null) { |
|
|
|
|
|
setDateFilterPreset(trendGranularity.value === 'hour' ? 'last24h' : '7days') |
|
|
} |
|
|
} |
|
|
|
|
|
function setTrendGranularity(granularity) { |
|
|
trendGranularity.value = granularity |
|
|
|
|
|
|
|
|
if (granularity === 'hour') { |
|
|
dateFilter.value.presetOptions = [ |
|
|
{ value: 'last24h', label: '近24小时', hours: 24 }, |
|
|
{ value: 'yesterday', label: '昨天', hours: 24 }, |
|
|
{ value: 'dayBefore', label: '前天', hours: 24 } |
|
|
] |
|
|
|
|
|
|
|
|
if ( |
|
|
dateFilter.value.type === 'custom' && |
|
|
dateFilter.value.customRange && |
|
|
dateFilter.value.customRange.length === 2 |
|
|
) { |
|
|
const start = new Date(dateFilter.value.customRange[0]) |
|
|
const end = new Date(dateFilter.value.customRange[1]) |
|
|
const hoursDiff = (end - start) / (1000 * 60 * 60) |
|
|
if (hoursDiff > 24) { |
|
|
showToast('小时粒度下日期范围不能超过24小时,已切换到近24小时', 'warning') |
|
|
setDateFilterPreset('last24h') |
|
|
return |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (['today', '7days', '30days'].includes(dateFilter.value.preset)) { |
|
|
setDateFilterPreset('last24h') |
|
|
return |
|
|
} |
|
|
} else { |
|
|
|
|
|
dateFilter.value.presetOptions = [ |
|
|
{ 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 |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
refreshChartsData() |
|
|
} |
|
|
|
|
|
async function refreshChartsData() { |
|
|
|
|
|
let days |
|
|
let modelPeriod = 'monthly' |
|
|
|
|
|
if (dateFilter.value.type === 'preset') { |
|
|
const option = dateFilter.value.presetOptions.find( |
|
|
(opt) => opt.value === dateFilter.value.preset |
|
|
) |
|
|
|
|
|
if (trendGranularity.value === 'hour') { |
|
|
|
|
|
days = 1 |
|
|
modelPeriod = 'daily' |
|
|
} else { |
|
|
|
|
|
days = option ? option.days : 7 |
|
|
|
|
|
if (dateFilter.value.preset === 'today') { |
|
|
modelPeriod = 'daily' |
|
|
} else { |
|
|
modelPeriod = 'monthly' |
|
|
} |
|
|
} |
|
|
} else { |
|
|
|
|
|
if (trendGranularity.value === 'hour') { |
|
|
|
|
|
const start = new Date(dateFilter.value.customRange[0]) |
|
|
const end = new Date(dateFilter.value.customRange[1]) |
|
|
const hoursDiff = Math.ceil((end - start) / (1000 * 60 * 60)) |
|
|
days = Math.ceil(hoursDiff / 24) || 1 |
|
|
} else { |
|
|
days = calculateDaysBetween(dateFilter.value.customStart, dateFilter.value.customEnd) |
|
|
} |
|
|
modelPeriod = 'daily' |
|
|
} |
|
|
|
|
|
await Promise.all([ |
|
|
loadUsageTrend(days, trendGranularity.value), |
|
|
loadModelStats(modelPeriod), |
|
|
loadApiKeysTrend(apiKeysTrendMetric.value), |
|
|
loadAccountUsageTrend(accountUsageGroup.value) |
|
|
]) |
|
|
} |
|
|
|
|
|
function setAccountUsageGroup(group) { |
|
|
accountUsageGroup.value = group |
|
|
return loadAccountUsageTrend(group) |
|
|
} |
|
|
|
|
|
function calculateDaysBetween(start, end) { |
|
|
if (!start || !end) return 7 |
|
|
const startDate = new Date(start) |
|
|
const endDate = new Date(end) |
|
|
const diffTime = Math.abs(endDate - startDate) |
|
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) |
|
|
return diffDays || 7 |
|
|
} |
|
|
|
|
|
function disabledDate(date) { |
|
|
return date > new Date() |
|
|
} |
|
|
|
|
|
return { |
|
|
|
|
|
loading, |
|
|
dashboardData, |
|
|
costsData, |
|
|
modelStats, |
|
|
trendData, |
|
|
dashboardModelStats, |
|
|
apiKeysTrendData, |
|
|
accountUsageTrendData, |
|
|
dateFilter, |
|
|
trendGranularity, |
|
|
apiKeysTrendMetric, |
|
|
accountUsageGroup, |
|
|
defaultTime, |
|
|
|
|
|
|
|
|
formattedUptime, |
|
|
|
|
|
|
|
|
loadDashboardData, |
|
|
loadUsageTrend, |
|
|
loadModelStats, |
|
|
loadApiKeysTrend, |
|
|
loadAccountUsageTrend, |
|
|
setDateFilterPreset, |
|
|
onCustomDateRangeChange, |
|
|
setTrendGranularity, |
|
|
refreshChartsData, |
|
|
setAccountUsageGroup, |
|
|
disabledDate |
|
|
} |
|
|
}) |
|
|
|