mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
feat(usage): 管理员用量统计支持筛选刷新与失败保留旧数据
- 用量统计/聚合接口新增 skipCache 选项与 120s 超时,便于强制绕过缓存 - loadStats 增加 force/preserveOnFailure 选项,背景刷新失败时保留旧数据 - 管理员页面在用户/模型/Provider 筛选变化时强制刷新统计,并将筛选条件传入统计接口 - 手动刷新与自动刷新分离,自动刷新不再重载长期热力图相关聚合
This commit is contained in:
@@ -114,4 +114,43 @@ describe('usageApi contract alignment', () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('uses an extended timeout and cache bypass option for admin analytics', async () => {
|
||||||
|
getMock
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
data: {
|
||||||
|
total_requests: 7,
|
||||||
|
total_tokens: 99,
|
||||||
|
total_cost: 12.34,
|
||||||
|
avg_response_time: 456,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
data: [{ provider: 'OpenAI', request_count: 7 }],
|
||||||
|
})
|
||||||
|
|
||||||
|
await usageApi.getUsageStats({ preset: 'last30days' }, { skipCache: true })
|
||||||
|
await usageApi.getUsageByProvider({ preset: 'last30days' }, { skipCache: true })
|
||||||
|
|
||||||
|
expect(cachedRequestMock).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
expect.stringContaining(':fresh'),
|
||||||
|
expect.any(Function),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
expect(cachedRequestMock).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
expect.stringContaining(':fresh'),
|
||||||
|
expect.any(Function),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
expect(getMock).toHaveBeenNthCalledWith(1, '/api/admin/usage/stats', {
|
||||||
|
params: { preset: 'last30days' },
|
||||||
|
timeout: 120000,
|
||||||
|
})
|
||||||
|
expect(getMock).toHaveBeenNthCalledWith(2, '/api/admin/usage/aggregation/stats', {
|
||||||
|
params: { group_by: 'provider', preset: 'last30days' },
|
||||||
|
timeout: 120000,
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import type { ActivityHeatmap } from '@/types/activity'
|
|||||||
import type { ImageProgress } from './requestTrace'
|
import type { ImageProgress } from './requestTrace'
|
||||||
|
|
||||||
const ACTIVITY_HEATMAP_CACHE_TTL_MS = 30 * 60 * 1000
|
const ACTIVITY_HEATMAP_CACHE_TTL_MS = 30 * 60 * 1000
|
||||||
|
const USAGE_ANALYTICS_CACHE_TTL_MS = 30 * 1000
|
||||||
|
const USAGE_ANALYTICS_REQUEST_TIMEOUT_MS = 120 * 1000
|
||||||
|
|
||||||
export interface UsageRecord {
|
export interface UsageRecord {
|
||||||
id: string // UUID
|
id: string // UUID
|
||||||
@@ -117,6 +119,10 @@ export interface UsageFilters {
|
|||||||
page_size?: number
|
page_size?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UsageRequestOptions {
|
||||||
|
skipCache?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
type UsageListResponse = {
|
type UsageListResponse = {
|
||||||
records?: unknown
|
records?: unknown
|
||||||
pagination?: {
|
pagination?: {
|
||||||
@@ -362,16 +368,19 @@ export const usageApi = {
|
|||||||
return normalizeUsageRecordPage(response.data, pagination)
|
return normalizeUsageRecordPage(response.data, pagination)
|
||||||
},
|
},
|
||||||
|
|
||||||
async getUsageStats(filters?: UsageFilters): Promise<UsageStats> {
|
async getUsageStats(filters?: UsageFilters, options?: UsageRequestOptions): Promise<UsageStats> {
|
||||||
// 为统计数据添加30秒缓存
|
// 为统计数据添加30秒缓存
|
||||||
const cacheKey = `usage-stats-${JSON.stringify(filters || {})}`
|
const cacheKey = `usage-stats-${JSON.stringify(filters || {})}${options?.skipCache ? ':fresh' : ''}`
|
||||||
return cachedRequest(
|
return cachedRequest(
|
||||||
cacheKey,
|
cacheKey,
|
||||||
async () => {
|
async () => {
|
||||||
const response = await apiClient.get<UsageStats>('/api/admin/usage/stats', { params: filters })
|
const response = await apiClient.get<UsageStats>('/api/admin/usage/stats', {
|
||||||
|
params: filters,
|
||||||
|
timeout: USAGE_ANALYTICS_REQUEST_TIMEOUT_MS,
|
||||||
|
})
|
||||||
return response.data
|
return response.data
|
||||||
},
|
},
|
||||||
30000 // 30秒缓存
|
options?.skipCache ? 0 : USAGE_ANALYTICS_CACHE_TTL_MS
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -382,36 +391,50 @@ export const usageApi = {
|
|||||||
*/
|
*/
|
||||||
async getUsageAggregation<T = UsageByModel[] | UsageByUser[] | UsageByProvider[] | UsageByApiFormat[]>(
|
async getUsageAggregation<T = UsageByModel[] | UsageByUser[] | UsageByProvider[] | UsageByApiFormat[]>(
|
||||||
groupBy: 'model' | 'user' | 'provider' | 'api_format',
|
groupBy: 'model' | 'user' | 'provider' | 'api_format',
|
||||||
filters?: UsageFilters & { limit?: number }
|
filters?: UsageFilters & { limit?: number },
|
||||||
|
options?: UsageRequestOptions
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const cacheKey = `usage-aggregation-${groupBy}-${JSON.stringify(filters || {})}`
|
const cacheKey = `usage-aggregation-${groupBy}-${JSON.stringify(filters || {})}${options?.skipCache ? ':fresh' : ''}`
|
||||||
return cachedRequest(
|
return cachedRequest(
|
||||||
cacheKey,
|
cacheKey,
|
||||||
async () => {
|
async () => {
|
||||||
const response = await apiClient.get<T>('/api/admin/usage/aggregation/stats', {
|
const response = await apiClient.get<T>('/api/admin/usage/aggregation/stats', {
|
||||||
params: { group_by: groupBy, ...filters }
|
params: { group_by: groupBy, ...filters },
|
||||||
|
timeout: USAGE_ANALYTICS_REQUEST_TIMEOUT_MS,
|
||||||
})
|
})
|
||||||
return response.data
|
return response.data
|
||||||
},
|
},
|
||||||
30000 // 30秒缓存
|
options?.skipCache ? 0 : USAGE_ANALYTICS_CACHE_TTL_MS
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
// Shorthand methods using getUsageAggregation
|
// Shorthand methods using getUsageAggregation
|
||||||
async getUsageByModel(filters?: UsageFilters & { limit?: number }): Promise<UsageByModel[]> {
|
async getUsageByModel(
|
||||||
return this.getUsageAggregation<UsageByModel[]>('model', filters)
|
filters?: UsageFilters & { limit?: number },
|
||||||
|
options?: UsageRequestOptions
|
||||||
|
): Promise<UsageByModel[]> {
|
||||||
|
return this.getUsageAggregation<UsageByModel[]>('model', filters, options)
|
||||||
},
|
},
|
||||||
|
|
||||||
async getUsageByUser(filters?: UsageFilters & { limit?: number }): Promise<UsageByUser[]> {
|
async getUsageByUser(
|
||||||
return this.getUsageAggregation<UsageByUser[]>('user', filters)
|
filters?: UsageFilters & { limit?: number },
|
||||||
|
options?: UsageRequestOptions
|
||||||
|
): Promise<UsageByUser[]> {
|
||||||
|
return this.getUsageAggregation<UsageByUser[]>('user', filters, options)
|
||||||
},
|
},
|
||||||
|
|
||||||
async getUsageByProvider(filters?: UsageFilters & { limit?: number }): Promise<UsageByProvider[]> {
|
async getUsageByProvider(
|
||||||
return this.getUsageAggregation<UsageByProvider[]>('provider', filters)
|
filters?: UsageFilters & { limit?: number },
|
||||||
|
options?: UsageRequestOptions
|
||||||
|
): Promise<UsageByProvider[]> {
|
||||||
|
return this.getUsageAggregation<UsageByProvider[]>('provider', filters, options)
|
||||||
},
|
},
|
||||||
|
|
||||||
async getUsageByApiFormat(filters?: UsageFilters & { limit?: number }): Promise<UsageByApiFormat[]> {
|
async getUsageByApiFormat(
|
||||||
return this.getUsageAggregation<UsageByApiFormat[]>('api_format', filters)
|
filters?: UsageFilters & { limit?: number },
|
||||||
|
options?: UsageRequestOptions
|
||||||
|
): Promise<UsageByApiFormat[]> {
|
||||||
|
return this.getUsageAggregation<UsageByApiFormat[]>('api_format', filters, options)
|
||||||
},
|
},
|
||||||
|
|
||||||
async getUserUsage(userId: string, filters?: UsageFilters): Promise<{
|
async getUserUsage(userId: string, filters?: UsageFilters): Promise<{
|
||||||
|
|||||||
@@ -219,4 +219,51 @@ describe('useUsageData', () => {
|
|||||||
expect(providerStats.value.map(item => item.provider)).toEqual(['OpenAI'])
|
expect(providerStats.value.map(item => item.provider)).toEqual(['OpenAI'])
|
||||||
expect(availableProviders.value).toEqual(['OpenAI'])
|
expect(availableProviders.value).toEqual(['OpenAI'])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('keeps previous admin provider stats when a background refresh fails', async () => {
|
||||||
|
const isAdminPage = ref(true)
|
||||||
|
const { loadStats, providerStats, availableProviders } = useUsageData({ isAdminPage })
|
||||||
|
const dateRange = { preset: 'last7days', tz_offset_minutes: 0 }
|
||||||
|
|
||||||
|
getUsageStatsMock.mockResolvedValueOnce({
|
||||||
|
total_requests: 3,
|
||||||
|
total_tokens: 300,
|
||||||
|
total_cost: 1,
|
||||||
|
avg_response_time: 0,
|
||||||
|
})
|
||||||
|
getUsageByProviderMock.mockResolvedValueOnce([
|
||||||
|
{
|
||||||
|
provider: 'OpenAI',
|
||||||
|
request_count: 3,
|
||||||
|
total_tokens: 300,
|
||||||
|
total_cost: 1.23,
|
||||||
|
actual_cost: 1.5,
|
||||||
|
avg_response_time_ms: 1250,
|
||||||
|
success_rate: 100,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
await loadStats(dateRange)
|
||||||
|
|
||||||
|
getUsageStatsMock.mockResolvedValueOnce({
|
||||||
|
total_requests: 4,
|
||||||
|
total_tokens: 400,
|
||||||
|
total_cost: 2,
|
||||||
|
avg_response_time: 0,
|
||||||
|
})
|
||||||
|
getUsageByProviderMock.mockRejectedValueOnce({
|
||||||
|
response: { status: 500 },
|
||||||
|
message: 'provider aggregation failed',
|
||||||
|
})
|
||||||
|
|
||||||
|
const hadFailure = await loadStats(dateRange, { preserveOnFailure: true })
|
||||||
|
|
||||||
|
expect(hadFailure).toBe(true)
|
||||||
|
expect(providerStats.value).toHaveLength(1)
|
||||||
|
expect(providerStats.value[0]).toMatchObject({
|
||||||
|
provider: 'OpenAI',
|
||||||
|
requests: 3,
|
||||||
|
})
|
||||||
|
expect(availableProviders.value).toEqual(['OpenAI'])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ export interface UseUsageDataOptions {
|
|||||||
isAdminPage: Ref<boolean>
|
isAdminPage: Ref<boolean>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LoadStatsOptions {
|
||||||
|
force?: boolean
|
||||||
|
preserveOnFailure?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export interface PaginationParams {
|
export interface PaginationParams {
|
||||||
page: number
|
page: number
|
||||||
pageSize: number
|
pageSize: number
|
||||||
@@ -76,7 +81,7 @@ export function useUsageData(options: UseUsageDataOptions) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 加载统计数据(不加载记录)
|
// 加载统计数据(不加载记录)
|
||||||
async function loadStats(dateRange?: DateRangeParams): Promise<boolean> {
|
async function loadStats(dateRange?: DateRangeParams, options: LoadStatsOptions = {}): Promise<boolean> {
|
||||||
const requestId = ++loadStatsRequestId
|
const requestId = ++loadStatsRequestId
|
||||||
isLoadingStats.value = true
|
isLoadingStats.value = true
|
||||||
currentDateRange.value = dateRange
|
currentDateRange.value = dateRange
|
||||||
@@ -84,14 +89,17 @@ export function useUsageData(options: UseUsageDataOptions) {
|
|||||||
try {
|
try {
|
||||||
if (isAdminPage.value) {
|
if (isAdminPage.value) {
|
||||||
// 管理员页面顺序加载统计数据,避免刷新使用记录时瞬时打满后端 worker。
|
// 管理员页面顺序加载统计数据,避免刷新使用记录时瞬时打满后端 worker。
|
||||||
stats.value = createDefaultStats()
|
if (!options.preserveOnFailure) {
|
||||||
modelStats.value = []
|
stats.value = createDefaultStats()
|
||||||
providerStats.value = []
|
modelStats.value = []
|
||||||
apiFormatStats.value = []
|
providerStats.value = []
|
||||||
availableModels.value = []
|
apiFormatStats.value = []
|
||||||
availableProviders.value = []
|
availableModels.value = []
|
||||||
|
availableProviders.value = []
|
||||||
|
}
|
||||||
|
|
||||||
let hadFailure = false
|
let hadFailure = false
|
||||||
|
const requestOptions = options.force ? { skipCache: true } : undefined
|
||||||
const markFailure = (error: unknown) => {
|
const markFailure = (error: unknown) => {
|
||||||
hadFailure = true
|
hadFailure = true
|
||||||
if (getErrorStatus(error) !== 403) {
|
if (getErrorStatus(error) !== 403) {
|
||||||
@@ -100,7 +108,7 @@ export function useUsageData(options: UseUsageDataOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const statsData = await usageApi.getUsageStats(dateRange)
|
const statsData = await usageApi.getUsageStats(dateRange, requestOptions)
|
||||||
if (requestId !== loadStatsRequestId) {
|
if (requestId !== loadStatsRequestId) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -127,7 +135,7 @@ export function useUsageData(options: UseUsageDataOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const modelData = await usageApi.getUsageByModel(dateRange)
|
const modelData = await usageApi.getUsageByModel(dateRange, requestOptions)
|
||||||
if (requestId !== loadStatsRequestId) {
|
if (requestId !== loadStatsRequestId) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -158,7 +166,7 @@ export function useUsageData(options: UseUsageDataOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const providerData = await usageApi.getUsageByProvider(dateRange)
|
const providerData = await usageApi.getUsageByProvider(dateRange, requestOptions)
|
||||||
if (requestId !== loadStatsRequestId) {
|
if (requestId !== loadStatsRequestId) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -191,7 +199,7 @@ export function useUsageData(options: UseUsageDataOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const apiFormatData = await usageApi.getUsageByApiFormat(dateRange)
|
const apiFormatData = await usageApi.getUsageByApiFormat(dateRange, requestOptions)
|
||||||
if (requestId !== loadStatsRequestId) {
|
if (requestId !== loadStatsRequestId) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -312,9 +320,9 @@ export function useUsageData(options: UseUsageDataOptions) {
|
|||||||
if (getErrorStatus(error) !== 403) {
|
if (getErrorStatus(error) !== 403) {
|
||||||
log.error('加载统计数据失败:', error)
|
log.error('加载统计数据失败:', error)
|
||||||
}
|
}
|
||||||
stats.value = createDefaultStats()
|
|
||||||
modelStats.value = []
|
|
||||||
if (!isAdminPage.value) {
|
if (!isAdminPage.value) {
|
||||||
|
stats.value = createDefaultStats()
|
||||||
|
modelStats.value = []
|
||||||
// 用户页的 records 依赖 stats 一起加载;管理员页的 records 是独立分页,不应被统计失败清空。
|
// 用户页的 records 依赖 stats 一起加载;管理员页的 records 是独立分页,不应被统计失败清空。
|
||||||
currentRecords.value = []
|
currentRecords.value = []
|
||||||
totalRecords.value = 0
|
totalRecords.value = 0
|
||||||
|
|||||||
@@ -111,7 +111,7 @@
|
|||||||
@update:current-page="handlePageChange"
|
@update:current-page="handlePageChange"
|
||||||
@update:page-size="handlePageSizeChange"
|
@update:page-size="handlePageSizeChange"
|
||||||
@update:auto-refresh="handleAutoRefreshChange"
|
@update:auto-refresh="handleAutoRefreshChange"
|
||||||
@refresh="refreshData"
|
@refresh="handleManualRefresh"
|
||||||
@prefetch-detail="prefetchRequestDetail"
|
@prefetch-detail="prefetchRequestDetail"
|
||||||
@show-detail="showRequestDetail"
|
@show-detail="showRequestDetail"
|
||||||
/>
|
/>
|
||||||
@@ -289,7 +289,7 @@ async function loadAdminUsers() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshAdminAnalytics(options: { force?: boolean } = {}) {
|
async function refreshAdminAnalytics(options: { force?: boolean; preserveOnFailure?: boolean } = {}) {
|
||||||
if (!isAdminPage.value) return
|
if (!isAdminPage.value) return
|
||||||
if (!options.force && !isPageVisible.value) return
|
if (!options.force && !isPageVisible.value) return
|
||||||
|
|
||||||
@@ -300,13 +300,19 @@ async function refreshAdminAnalytics(options: { force?: boolean } = {}) {
|
|||||||
if (!options.force && adminAnalyticsRefreshInFlight) {
|
if (!options.force && adminAnalyticsRefreshInFlight) {
|
||||||
return adminAnalyticsRefreshInFlight
|
return adminAnalyticsRefreshInFlight
|
||||||
}
|
}
|
||||||
|
if (!options.force) {
|
||||||
|
lastAdminAnalyticsRefreshAt = now
|
||||||
|
}
|
||||||
|
|
||||||
const refreshGeneration = ++adminAnalyticsRefreshGeneration
|
const refreshGeneration = ++adminAnalyticsRefreshGeneration
|
||||||
const refreshPromise = (async () => {
|
const refreshPromise = (async () => {
|
||||||
let hasSuccessfulRefresh = false
|
let hasSuccessfulRefresh = false
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const hadFailure = await loadStats(timeRange.value)
|
const hadFailure = await loadStats(getCurrentStatsFilters(), {
|
||||||
|
force: options.force,
|
||||||
|
preserveOnFailure: options.preserveOnFailure,
|
||||||
|
})
|
||||||
if (refreshGeneration !== adminAnalyticsRefreshGeneration) {
|
if (refreshGeneration !== adminAnalyticsRefreshGeneration) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -337,6 +343,21 @@ async function refreshAdminAnalytics(options: { force?: boolean } = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCurrentStatsFilters() {
|
||||||
|
const filters = getCurrentFilters()
|
||||||
|
return {
|
||||||
|
...timeRange.value,
|
||||||
|
user_id: filters.user_id,
|
||||||
|
model: filters.model,
|
||||||
|
provider: filters.provider,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshAdminAnalyticsForSelectionChange() {
|
||||||
|
if (!isAdminPage.value) return
|
||||||
|
await refreshAdminAnalytics({ force: true, preserveOnFailure: false })
|
||||||
|
}
|
||||||
|
|
||||||
// 用户页面需要前端筛选
|
// 用户页面需要前端筛选
|
||||||
const filteredRecords = computed(() => {
|
const filteredRecords = computed(() => {
|
||||||
if (!isAdminPage.value) {
|
if (!isAdminPage.value) {
|
||||||
@@ -740,7 +761,7 @@ onMounted(async () => {
|
|||||||
timeRange.value
|
timeRange.value
|
||||||
)
|
)
|
||||||
void (async () => {
|
void (async () => {
|
||||||
await refreshAdminAnalytics({ force: true })
|
await refreshAdminAnalytics({ force: true, preserveOnFailure: false })
|
||||||
await loadHeatmapData()
|
await loadHeatmapData()
|
||||||
await loadAdminUsers()
|
await loadAdminUsers()
|
||||||
})()
|
})()
|
||||||
@@ -772,7 +793,7 @@ async function handleTimeRangeChange(value: DateRangeParams) {
|
|||||||
currentPage.value = 1 // 重置到第一页
|
currentPage.value = 1 // 重置到第一页
|
||||||
if (isAdminPage.value) {
|
if (isAdminPage.value) {
|
||||||
await loadRecords({ page: 1, pageSize: pageSize.value }, getCurrentFilters(), timeRange.value)
|
await loadRecords({ page: 1, pageSize: pageSize.value }, getCurrentFilters(), timeRange.value)
|
||||||
await refreshAdminAnalytics({ force: true })
|
await refreshAdminAnalyticsForSelectionChange()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await loadStats(timeRange.value)
|
await loadStats(timeRange.value)
|
||||||
@@ -829,6 +850,7 @@ async function handleFilterUserChange(value: string) {
|
|||||||
|
|
||||||
if (isAdminPage.value) {
|
if (isAdminPage.value) {
|
||||||
await loadRecords({ page: 1, pageSize: pageSize.value }, getCurrentFilters(), timeRange.value)
|
await loadRecords({ page: 1, pageSize: pageSize.value }, getCurrentFilters(), timeRange.value)
|
||||||
|
await refreshAdminAnalyticsForSelectionChange()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -838,6 +860,7 @@ async function handleFilterModelChange(value: string) {
|
|||||||
|
|
||||||
if (isAdminPage.value) {
|
if (isAdminPage.value) {
|
||||||
await loadRecords({ page: 1, pageSize: pageSize.value }, getCurrentFilters(), timeRange.value)
|
await loadRecords({ page: 1, pageSize: pageSize.value }, getCurrentFilters(), timeRange.value)
|
||||||
|
await refreshAdminAnalyticsForSelectionChange()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -847,6 +870,7 @@ async function handleFilterProviderChange(value: string) {
|
|||||||
|
|
||||||
if (isAdminPage.value) {
|
if (isAdminPage.value) {
|
||||||
await loadRecords({ page: 1, pageSize: pageSize.value }, getCurrentFilters(), timeRange.value)
|
await loadRecords({ page: 1, pageSize: pageSize.value }, getCurrentFilters(), timeRange.value)
|
||||||
|
await refreshAdminAnalyticsForSelectionChange()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -889,8 +913,6 @@ async function refreshData() {
|
|||||||
getCurrentFilters(),
|
getCurrentFilters(),
|
||||||
timeRange.value
|
timeRange.value
|
||||||
)
|
)
|
||||||
// 热力图反映长期活跃分布,不跟随自动刷新链路一起重载。
|
|
||||||
void refreshAdminAnalytics()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -905,6 +927,11 @@ async function refreshData() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleManualRefresh() {
|
||||||
|
if (!isPageVisible.value) return
|
||||||
|
await refreshData()
|
||||||
|
}
|
||||||
|
|
||||||
// 显示请求详情
|
// 显示请求详情
|
||||||
function showRequestDetail(id: string) {
|
function showRequestDetail(id: string) {
|
||||||
if (!isAdminPage.value) return
|
if (!isAdminPage.value) return
|
||||||
|
|||||||
Reference in New Issue
Block a user