mirror of
https://github.com/fawney19/Aether.git
synced 2026-01-03 00:02:28 +08:00
209 lines
5.8 KiB
TypeScript
209 lines
5.8 KiB
TypeScript
/**
|
||
* TTL 分析 composable
|
||
* 封装缓存亲和性 TTL 分析相关的状态和逻辑
|
||
*/
|
||
import { ref, computed, watch } from 'vue'
|
||
import { useToast } from '@/composables/useToast'
|
||
import {
|
||
cacheAnalysisApi,
|
||
type TTLAnalysisResponse,
|
||
type CacheHitAnalysisResponse,
|
||
type IntervalTimelineResponse
|
||
} from '@/api/cache'
|
||
import type { ChartData } from 'chart.js'
|
||
|
||
// 时间范围选项
|
||
export const ANALYSIS_HOURS_OPTIONS = [
|
||
{ value: '12', label: '12 小时' },
|
||
{ value: '24', label: '24 小时' },
|
||
{ value: '72', label: '3 天' },
|
||
{ value: '168', label: '7 天' },
|
||
{ value: '336', label: '14 天' },
|
||
{ value: '720', label: '30 天' }
|
||
] as const
|
||
|
||
// 间隔颜色配置
|
||
export const INTERVAL_COLORS = {
|
||
short: 'rgba(34, 197, 94, 0.6)', // green: 0-5 分钟
|
||
medium: 'rgba(59, 130, 246, 0.6)', // blue: 5-15 分钟
|
||
normal: 'rgba(168, 85, 247, 0.6)', // purple: 15-30 分钟
|
||
long: 'rgba(249, 115, 22, 0.6)', // orange: 30-60 分钟
|
||
veryLong: 'rgba(239, 68, 68, 0.6)' // red: >60 分钟
|
||
} as const
|
||
|
||
/**
|
||
* 根据间隔时间获取对应的颜色
|
||
*/
|
||
export function getIntervalColor(interval: number): string {
|
||
if (interval <= 5) return INTERVAL_COLORS.short
|
||
if (interval <= 15) return INTERVAL_COLORS.medium
|
||
if (interval <= 30) return INTERVAL_COLORS.normal
|
||
if (interval <= 60) return INTERVAL_COLORS.long
|
||
return INTERVAL_COLORS.veryLong
|
||
}
|
||
|
||
/**
|
||
* 获取 TTL 推荐的 Badge 样式
|
||
*/
|
||
export function getTTLBadgeVariant(ttl: number): 'default' | 'secondary' | 'outline' | 'destructive' {
|
||
if (ttl <= 5) return 'default'
|
||
if (ttl <= 15) return 'secondary'
|
||
if (ttl <= 30) return 'outline'
|
||
return 'destructive'
|
||
}
|
||
|
||
/**
|
||
* 获取使用频率标签
|
||
*/
|
||
export function getFrequencyLabel(ttl: number): string {
|
||
if (ttl <= 5) return '高频'
|
||
if (ttl <= 15) return '中高频'
|
||
if (ttl <= 30) return '中频'
|
||
return '低频'
|
||
}
|
||
|
||
/**
|
||
* 获取使用频率样式类名
|
||
*/
|
||
export function getFrequencyClass(ttl: number): string {
|
||
if (ttl <= 5) return 'text-success font-medium'
|
||
if (ttl <= 15) return 'text-blue-500 font-medium'
|
||
if (ttl <= 30) return 'text-muted-foreground'
|
||
return 'text-destructive'
|
||
}
|
||
|
||
export function useTTLAnalysis() {
|
||
const { error: showError, info: showInfo } = useToast()
|
||
|
||
// 状态
|
||
const ttlAnalysis = ref<TTLAnalysisResponse | null>(null)
|
||
const hitAnalysis = ref<CacheHitAnalysisResponse | null>(null)
|
||
const ttlAnalysisLoading = ref(false)
|
||
const hitAnalysisLoading = ref(false)
|
||
const analysisHours = ref('24')
|
||
|
||
// 用户散点图展开状态
|
||
const expandedUserId = ref<string | null>(null)
|
||
const userTimelineData = ref<IntervalTimelineResponse | null>(null)
|
||
const userTimelineLoading = ref(false)
|
||
|
||
// 计算属性:是否正在加载
|
||
const isLoading = computed(() => ttlAnalysisLoading.value || hitAnalysisLoading.value)
|
||
|
||
// 获取 TTL 分析数据
|
||
async function fetchTTLAnalysis() {
|
||
ttlAnalysisLoading.value = true
|
||
try {
|
||
const hours = parseInt(analysisHours.value)
|
||
const result = await cacheAnalysisApi.analyzeTTL({ hours })
|
||
ttlAnalysis.value = result
|
||
|
||
if (result.total_users_analyzed === 0) {
|
||
const periodText = hours >= 24 ? `${hours / 24} 天` : `${hours} 小时`
|
||
showInfo(`未找到符合条件的数据(最近 ${periodText})`)
|
||
}
|
||
} catch (error) {
|
||
showError('获取 TTL 分析失败')
|
||
console.error(error)
|
||
} finally {
|
||
ttlAnalysisLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 获取缓存命中分析数据
|
||
async function fetchHitAnalysis() {
|
||
hitAnalysisLoading.value = true
|
||
try {
|
||
hitAnalysis.value = await cacheAnalysisApi.analyzeHit({
|
||
hours: parseInt(analysisHours.value)
|
||
})
|
||
} catch (error) {
|
||
showError('获取缓存命中分析失败')
|
||
console.error(error)
|
||
} finally {
|
||
hitAnalysisLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 获取指定用户的时间线数据
|
||
async function fetchUserTimeline(userId: string) {
|
||
userTimelineLoading.value = true
|
||
try {
|
||
userTimelineData.value = await cacheAnalysisApi.getIntervalTimeline({
|
||
hours: parseInt(analysisHours.value),
|
||
limit: 2000,
|
||
user_id: userId
|
||
})
|
||
} catch (error) {
|
||
showError('获取用户时间线数据失败')
|
||
console.error(error)
|
||
} finally {
|
||
userTimelineLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 切换用户行展开状态
|
||
async function toggleUserExpand(userId: string) {
|
||
if (expandedUserId.value === userId) {
|
||
expandedUserId.value = null
|
||
userTimelineData.value = null
|
||
} else {
|
||
expandedUserId.value = userId
|
||
await fetchUserTimeline(userId)
|
||
}
|
||
}
|
||
|
||
// 刷新所有分析数据
|
||
async function refreshAnalysis() {
|
||
expandedUserId.value = null
|
||
userTimelineData.value = null
|
||
await Promise.all([fetchTTLAnalysis(), fetchHitAnalysis()])
|
||
}
|
||
|
||
// 用户时间线散点图数据
|
||
const userTimelineChartData = computed<ChartData<'scatter'>>(() => {
|
||
if (!userTimelineData.value || userTimelineData.value.points.length === 0) {
|
||
return { datasets: [] }
|
||
}
|
||
|
||
const points = userTimelineData.value.points
|
||
|
||
return {
|
||
datasets: [{
|
||
label: '请求间隔',
|
||
data: points.map(p => ({ x: p.x, y: p.y })),
|
||
backgroundColor: points.map(p => getIntervalColor(p.y)),
|
||
borderColor: points.map(p => getIntervalColor(p.y).replace('0.6', '1')),
|
||
pointRadius: 3,
|
||
pointHoverRadius: 5
|
||
}]
|
||
}
|
||
})
|
||
|
||
// 监听时间范围变化
|
||
watch(analysisHours, () => {
|
||
refreshAnalysis()
|
||
})
|
||
|
||
return {
|
||
// 状态
|
||
ttlAnalysis,
|
||
hitAnalysis,
|
||
ttlAnalysisLoading,
|
||
hitAnalysisLoading,
|
||
analysisHours,
|
||
expandedUserId,
|
||
userTimelineData,
|
||
userTimelineLoading,
|
||
isLoading,
|
||
userTimelineChartData,
|
||
|
||
// 方法
|
||
fetchTTLAnalysis,
|
||
fetchHitAnalysis,
|
||
fetchUserTimeline,
|
||
toggleUserExpand,
|
||
refreshAnalysis
|
||
}
|
||
}
|