feat(dashboard): optimize dashboard layout and add new charts
This commit is contained in:
268
WebUI/src/components/ScoreDistribution.vue
Normal file
268
WebUI/src/components/ScoreDistribution.vue
Normal file
@@ -0,0 +1,268 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user