- 新增日志中心功能 (api/logapi.go + Logs.vue) - 新增GRE协议服务支持 (service/gre/) - 新增流量监控组件 (TrafficChart.vue) - 新增L2TP/IPSec测试用例 - 新增日志轮转服务 (logrotate.go) - 新增部署脚本 (scripts/install.sh, uninstall.sh, service文件) - 更新i18n国际化文件 - 优化后端API和前端路由
98 lines
3.2 KiB
Vue
98 lines
3.2 KiB
Vue
<template>
|
|
<div>
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
|
|
<h4 style="margin:0">{{ $t('tunnels.trafficTrend') }}</h4>
|
|
<el-radio-group v-model="hours" size="small" @change="load">
|
|
<el-radio-button :value="1">1h</el-radio-button>
|
|
<el-radio-button :value="6">6h</el-radio-button>
|
|
<el-radio-button :value="24">24h</el-radio-button>
|
|
<el-radio-button :value="168">7d</el-radio-button>
|
|
</el-radio-group>
|
|
</div>
|
|
<div ref="chartRef" style="height:220px" v-loading="loading"></div>
|
|
<el-empty v-if="!loading && samples.length === 0" :description="$t('tunnels.noTrafficData')" :image-size="60" />
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
|
import * as echarts from 'echarts'
|
|
import api from '../api'
|
|
import { fmtBytes } from '../utils/format'
|
|
|
|
const props = defineProps({ tunnelId: { type: Number, required: true } })
|
|
|
|
const chartRef = ref(null)
|
|
const loading = ref(false)
|
|
const hours = ref(24)
|
|
const samples = ref([])
|
|
let chart = null
|
|
|
|
function granularity(h) {
|
|
if (h <= 1) return 'minute'
|
|
if (h <= 168) return 'hour'
|
|
return 'day'
|
|
}
|
|
|
|
async function load() {
|
|
if (!props.tunnelId) return
|
|
loading.value = true
|
|
try {
|
|
const { data } = await api.get(`/tunnels/${props.tunnelId}/traffic/history`, {
|
|
params: { hours: hours.value, granularity: granularity(hours.value) }
|
|
})
|
|
samples.value = data.samples || []
|
|
renderChart()
|
|
} catch (e) {
|
|
samples.value = []
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function renderChart() {
|
|
if (!chartRef.value || samples.value.length === 0) return
|
|
if (!chart) chart = echarts.init(chartRef.value)
|
|
const times = samples.value.map(s => {
|
|
const d = new Date(s.time)
|
|
return hours.value <= 24 ? d.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
|
|
: d.toLocaleDateString('zh-CN', { month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' })
|
|
})
|
|
chart.setOption({
|
|
tooltip: {
|
|
trigger: 'axis',
|
|
formatter(params) {
|
|
const t = params[0].axisValue
|
|
const rx = params[0] ? fmtBytes(params[0].value) : '0'
|
|
const tx = params[1] ? fmtBytes(params[1].value) : '0'
|
|
return `${t}<br/>↓ RX: ${rx}<br/>↑ TX: ${tx}`
|
|
}
|
|
},
|
|
legend: { data: ['RX', 'TX'], right: 0, top: 0, textStyle: { fontSize: 11 } },
|
|
grid: { left: 50, right: 16, top: 30, bottom: 24 },
|
|
xAxis: { type: 'category', data: times, axisLabel: { fontSize: 10 } },
|
|
yAxis: {
|
|
type: 'value',
|
|
axisLabel: { fontSize: 10, formatter: v => fmtBytes(v) }
|
|
},
|
|
series: [
|
|
{ name: 'RX', type: 'line', smooth: true, areaStyle: { opacity: 0.15 }, data: samples.value.map(s => s.delta_rx), lineStyle: { width: 2 } },
|
|
{ name: 'TX', type: 'line', smooth: true, areaStyle: { opacity: 0.15 }, data: samples.value.map(s => s.delta_tx), lineStyle: { width: 2 } }
|
|
]
|
|
})
|
|
}
|
|
|
|
let resizeHandler = null
|
|
onMounted(() => {
|
|
load()
|
|
resizeHandler = () => chart?.resize()
|
|
window.addEventListener('resize', resizeHandler)
|
|
})
|
|
onBeforeUnmount(() => {
|
|
window.removeEventListener('resize', resizeHandler)
|
|
chart?.dispose()
|
|
})
|
|
|
|
watch(() => props.tunnelId, () => { hours.value = 24; load() })
|
|
</script>
|