feat: 日志中心 + GRE协议支持 + 流量监控 + 部署脚本

- 新增日志中心功能 (api/logapi.go + Logs.vue)
- 新增GRE协议服务支持 (service/gre/)
- 新增流量监控组件 (TrafficChart.vue)
- 新增L2TP/IPSec测试用例
- 新增日志轮转服务 (logrotate.go)
- 新增部署脚本 (scripts/install.sh, uninstall.sh, service文件)
- 更新i18n国际化文件
- 优化后端API和前端路由
This commit is contained in:
opsx-tools committed 2026-07-25 08:50:34 +08:00
1 parent 7de1f70905
commit a14d9e62e6
25 files changed
+2285 -25

No files matched your search

+97
View File
@@ -0,0 +1,97 @@
<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>