仪表盘重做:ECharts 曲线、常驻遥测、路由行直达详情
采集从仪表盘组件挪进模块级单例(web/telemetry.ts),流量曲线和每路 吞吐的历史跨页面留存——跟着组件走的话切一次页面就清零一次。每路吞吐 从 /connections 快照按连接逐条求差再按 inboundName 聚合:先求和再求差 会在连接关闭时算出负数。 图表换 ECharts(按需注册):手写 SVG 折线做不出平滑曲线和时间轴, 颜色在运行时从 CSS 变量取,跟随主题切换重取。 路由列表每行带走势、速率和可选的公网 IP 检测,点击跳路由页并选中 那一路(外壳传 focus 给 RouterPage 消费)。
This commit is contained in:
+14
-54
@@ -1,62 +1,11 @@
|
||||
/**
|
||||
* 仪表盘页的数据整理。
|
||||
*
|
||||
* 流量窗口、折线坐标、端口列表这些是「拿到内核数据之后怎么摆」的纯逻辑,
|
||||
* 放在这里而不是 DashboardPage.vue 的 computed 里,才能被 bun test 直接测到。
|
||||
* 「拿到内核数据之后怎么摆」的纯逻辑,放在这里而不是 DashboardPage.vue 的
|
||||
* computed 里,才能被 bun test 直接测到。流量窗口和每路吞吐的聚合在
|
||||
* shared/telemetry.ts——那些是采集层的事,跨页面共用。
|
||||
*/
|
||||
|
||||
/** 流量曲线保留的秒数。接口每秒吐一条,所以窗口长度就是样本数。 */
|
||||
export const WINDOW = 60
|
||||
|
||||
export interface TrafficSample {
|
||||
/** 当前每秒上行字节数 */
|
||||
up: number
|
||||
/** 当前每秒下行字节数 */
|
||||
down: number
|
||||
/** 累计上传字节数 */
|
||||
upTotal: number
|
||||
/** 累计下载字节数 */
|
||||
downTotal: number
|
||||
}
|
||||
|
||||
/** 追加一条样本,窗口满了就把最旧的挤掉。 */
|
||||
export function pushSample(samples: TrafficSample[], sample: TrafficSample): TrafficSample[] {
|
||||
return [...samples, sample].slice(-WINDOW)
|
||||
}
|
||||
|
||||
/**
|
||||
* 两条曲线共用的纵轴上限。
|
||||
*
|
||||
* 共用一个刻度,上行下行的差距才看得出来;各画各的刻度会让一条几乎不动的线
|
||||
* 看起来像在剧烈波动。下限取 1 只为避免全 0 时除以 0。
|
||||
*/
|
||||
export function chartPeak(samples: TrafficSample[]): number {
|
||||
let peak = 1
|
||||
for (const s of samples) {
|
||||
if (s.up > peak) peak = s.up
|
||||
if (s.down > peak) peak = s.down
|
||||
}
|
||||
return peak
|
||||
}
|
||||
|
||||
/**
|
||||
* 一条曲线的 SVG polyline 点串。0 映射到 height,peak 映射到 0。
|
||||
*
|
||||
* 最新样本钉在右缘,窗口不满时曲线从左边空白处长出来,而不是把已有数据拉伸到
|
||||
* 整个宽度——拉伸会让「刚开始采集」和「一直很闲」长得一样。
|
||||
*/
|
||||
export function linePoints(values: number[], peak: number, width: number, height: number): string {
|
||||
const scale = Math.max(peak, 1)
|
||||
const step = width / (WINDOW - 1)
|
||||
return values
|
||||
.map((value, i) => {
|
||||
const x = width - (values.length - 1 - i) * step
|
||||
const y = height - (value / scale) * height
|
||||
return `${x.toFixed(1)},${y.toFixed(1)}`
|
||||
})
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
/** 内核运行模式在界面上怎么念。不在表里的原样显示,不猜它的意思。 */
|
||||
export function modeLabel(mode: unknown): string {
|
||||
switch (mode) {
|
||||
@@ -89,3 +38,14 @@ export function kernelPorts(configs: Record<string, unknown>): KernelPort[] {
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 入站声明的端口在探测公网 IP 时用哪个数。
|
||||
* 新式入站的端口可以写 "200,302" 或 "401-429",探测取第一个数字;解析不出返回 null。
|
||||
*/
|
||||
export function firstPort(port: string): number | null {
|
||||
const match = /\d+/.exec(port)
|
||||
if (!match) return null
|
||||
const value = Number(match[0])
|
||||
return value >= 1 && value <= 65535 ? value : null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user