新增仪表盘页:流量曲线、流量与内存数字、配置概览、内核状态

流量曲线手画 SVG polyline 而不引图表库:60 个点两条线,一个图表库几百 KB,
决策记录 0006 留的例外用在这里不划算。/traffic 和 /memory 是永不结束的流,
api.request() 会把整个响应读完等于挂死,所以新增 api.openStream() 返回响应体,
交给 web/stream.ts 的 readLines 逐行读,离开页面中止 fetch 断流。
导航入口要在 App.vue 里挂,按约定留给集成者统一改。
This commit is contained in:
2026-08-11 18:36:00 +08:00
parent 6c8a1c4e0e
commit 97c3943403
4 changed files with 594 additions and 0 deletions
+23
View File
@@ -126,6 +126,29 @@ export const api = {
kernel: () => request<{ online: boolean; version?: string }>('GET', '/api/kernel'),
/** 内核的运行时设置:运行模式、各代理端口、allow-lan 等。 */
kernelConfigs: () => request<Record<string, unknown>>('GET', '/api/kernel/configs'),
/**
* 打开一个持续吐数据的流式接口(/traffic、/memory),每秒一行 JSON。
*
* 不能用上面的 request():它会把整个响应读完,而对永不结束的流来说那等于挂死。
* 调用方拿到响应体后用 web/stream.ts 的 readLines() 逐行读;中止 signal 即断流。
*/
openStream: async (path: string, signal: AbortSignal): Promise<ReadableStream<Uint8Array>> => {
const res = await fetch(`/api/kernel${path}`, {
headers: { Authorization: `Bearer ${getToken()}` },
signal,
})
if (!res.ok) {
const text = await res.text()
const payload = text ? (JSON.parse(text) as Record<string, unknown>) : {}
throw new ApiError(res.status, payload)
}
if (!res.body) throw new Error('响应没有内容')
return res.body
},
/** 入站类型的字段表,从内核源码提取,用来驱动表单。 */
listenerFields: () => request<ListenerFields>('GET', '/api/meta/listener-fields'),