仪表盘重做:ECharts 曲线、常驻遥测、路由行直达详情

采集从仪表盘组件挪进模块级单例(web/telemetry.ts),流量曲线和每路
吞吐的历史跨页面留存——跟着组件走的话切一次页面就清零一次。每路吞吐
从 /connections 快照按连接逐条求差再按 inboundName 聚合:先求和再求差
会在连接关闭时算出负数。

图表换 ECharts(按需注册):手写 SVG 折线做不出平滑曲线和时间轴,
颜色在运行时从 CSS 变量取,跟随主题切换重取。

路由列表每行带走势、速率和可选的公网 IP 检测,点击跳路由页并选中
那一路(外壳传 focus 给 RouterPage 消费)。
This commit is contained in:
2026-08-11 22:26:36 +08:00
parent ce181e9ae5
commit eec5b791c8
15 changed files with 1385 additions and 376 deletions
+18 -55
View File
@@ -1,59 +1,5 @@
import { describe, expect, it } from 'bun:test'
import { WINDOW, chartPeak, kernelPorts, linePoints, modeLabel, pushSample } from './dashboard.ts'
import type { TrafficSample } from './dashboard.ts'
const sample = (up: number, down: number): TrafficSample => ({
up,
down,
upTotal: 0,
downTotal: 0,
})
describe('流量样本窗口', () => {
it('追加样本', () => {
expect(pushSample([sample(1, 2)], sample(3, 4))).toHaveLength(2)
})
it('窗口满了挤掉最旧的', () => {
const full = Array.from({ length: WINDOW }, (_, i) => sample(i, i))
const next = pushSample(full, sample(999, 999))
expect(next).toHaveLength(WINDOW)
expect(next.at(-1)?.up).toBe(999)
expect(next[0]?.up).toBe(1)
})
})
describe('纵轴上限', () => {
it('空窗口和全 0 都有下限 1,不会除出 NaN', () => {
expect(chartPeak([])).toBe(1)
expect(chartPeak([sample(0, 0)])).toBe(1)
})
it('取上行下行的共同峰值', () => {
expect(chartPeak([sample(10, 500), sample(800, 20)])).toBe(800)
})
})
describe('折线坐标', () => {
it('最新样本钉在右缘,最旧的往左排', () => {
const points = linePoints([5, 10], 10, 590, 100).split(' ')
expect(points).toHaveLength(2)
// 窗口 60,步长 10:两条样本占最右边两格
expect(points[0]).toBe('580.0,50.0')
expect(points[1]).toBe('590.0,0.0')
})
it('窗口不满不拉伸,左边留空', () => {
const points = linePoints([7], 10, 590, 100).split(' ')
expect(points).toEqual(['590.0,30.0'])
})
it('全是 0 时整条贴在底线上', () => {
for (const point of linePoints([0, 0, 0], 1, 590, 100).split(' ')) {
expect(point.endsWith(',100.0')).toBe(true)
}
})
})
import { firstPort, kernelPorts, modeLabel } from './dashboard.ts'
describe('运行模式', () => {
it('三个已知模式有中文名', () => {
@@ -85,3 +31,20 @@ describe('代理端口', () => {
expect(kernelPorts({ port: '7890' })).toEqual([{ name: 'port', port: 7890 }])
})
})
describe('探测端口', () => {
it('单个端口直接用', () => {
expect(firstPort('7890')).toBe(7890)
})
it('多端口和区间取第一个数', () => {
expect(firstPort('200,302')).toBe(200)
expect(firstPort('401-429')).toBe(401)
})
it('解析不出或越界返回 null', () => {
expect(firstPort('')).toBeNull()
expect(firstPort('abc')).toBeNull()
expect(firstPort('70000')).toBeNull()
})
})