Files
ProxyPool/WebUI/src/components/ScoreDistribution.vue

269 lines
5.5 KiB
Vue

<template>
<el-card class="chart-card" shadow="hover">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="header-icon"><TrendCharts /></el-icon>
评分分布
</span>
<el-tooltip content="已验证可用代理的质量评分分布">
<el-icon class="help-icon"><InfoFilled /></el-icon>
</el-tooltip>
</div>
</template>
<div ref="chartRef" class="chart-container"></div>
</el-card>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue'
import { InfoFilled, TrendCharts } from '@element-plus/icons-vue'
import * as echarts from 'echarts'
import axios from '../api'
const chartRef = ref(null)
let chartInstance = null
let resizeTimer = null
const cachedColors = ref(null)
function loadColors() {
if (cachedColors.value) return cachedColors.value
const getCssVar = (name, fallback) =>
getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback
cachedColors.value = {
primary: getCssVar('--primary', '#927CFF'),
success: getCssVar('--success', '#22C55E'),
warning: getCssVar('--warning', '#F59E0B'),
info: getCssVar('--info', '#38BDF8'),
textPrimary: getCssVar('--text-primary', '#F5F7FA'),
textSecondary: getCssVar('--text-secondary', '#A5AEBD'),
surface: getCssVar('--surface', '#181C25')
}
return cachedColors.value
}
async function fetchData() {
try {
const res = await axios.get('/api/proxies/score-distribution')
if (res?.data?.ranges) {
updateChart(res.data)
}
} catch (err) {
console.error('Failed to fetch score distribution:', err)
}
}
function updateChart(data) {
if (!chartInstance) return
const colors = cachedColors.value
const option = {
tooltip: {
trigger: 'axis',
confine: true,
axisPointer: {
type: 'shadow'
},
backgroundColor: 'rgba(24, 28, 37, 0.95)',
borderColor: colors.primary,
borderWidth: 1,
textStyle: {
color: colors.textPrimary,
fontSize: 13
},
formatter: (params) => {
const item = params[0]
return `${item.name}<br/>代理数: <b>${item.value}</b>`
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
top: '10%',
containLabel: true
},
xAxis: {
type: 'category',
data: data.ranges || [],
axisLabel: {
color: colors.textSecondary,
fontSize: 11
},
axisLine: {
lineStyle: {
color: colors.textSecondary
}
},
axisTick: {
show: false
}
},
yAxis: {
type: 'value',
name: '代理数',
nameTextStyle: {
color: colors.textSecondary,
fontSize: 11
},
axisLabel: {
color: colors.textSecondary,
fontSize: 11
},
axisLine: {
show: false
},
axisTick: {
show: false
},
splitLine: {
lineStyle: {
color: 'rgba(255,255,255,0.1)'
}
}
},
series: [
{
name: '评分分布',
type: 'bar',
data: data.counts || [],
barWidth: '50%',
itemStyle: {
color: (params) => {
const colorList = [
colors.success,
colors.primary,
colors.info,
colors.warning,
colors.danger || '#EF4444'
]
return colorList[params.dataIndex] || colors.primary
},
borderRadius: [4, 4, 0, 0]
},
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowColor: 'rgba(146, 124, 255, 0.3)'
}
},
label: {
show: true,
position: 'top',
color: colors.textSecondary,
fontSize: 11,
formatter: '{c}'
}
}
],
animation: true,
animationDuration: 800,
animationEasing: 'cubicOut'
}
chartInstance.setOption(option, true)
}
function initChart() {
if (!chartRef.value) return
loadColors()
if (chartInstance) {
return
}
chartInstance = echarts.init(chartRef.value)
fetchData()
window.addEventListener('resize', handleResize)
}
function handleResize() {
if (resizeTimer) clearTimeout(resizeTimer)
resizeTimer = setTimeout(() => {
chartInstance?.resize()
}, 200)
}
function destroyChart() {
window.removeEventListener('resize', handleResize)
if (resizeTimer) {
clearTimeout(resizeTimer)
resizeTimer = null
}
chartInstance?.dispose()
chartInstance = null
}
onMounted(() => {
initChart()
})
onUnmounted(() => {
destroyChart()
})
watch(
() => chartInstance,
(val) => {
if (val) {
fetchData()
}
}
)
</script>
<style scoped>
.chart-card {
border-radius: var(--radius-lg);
min-height: 420px;
background: var(--surface);
border: 1px solid var(--border);
overflow: visible;
}
.chart-card:hover {
border-color: var(--border-light);
}
.header-icon {
margin-right: 8px;
color: var(--primary);
}
.help-icon {
color: var(--text-muted);
cursor: help;
transition: var(--transition-base);
}
.help-icon:hover {
color: var(--primary);
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.card-title {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
}
.chart-container {
height: 360px;
overflow: visible;
}
</style>
<style>
.chart-card .el-card__header {
border-bottom: none !important;
}
</style>