日志改常驻收集,加清空按钮;原生 select 统一换成 SelectBox
日志收集从请求页组件搬进模块级单例(web/log-store.ts,模式同 telemetry), 切走页面不再清空;离线时保留已收的日志,只提示不再更新。清空只清界面。 五处原生 <select> 在三个文件里各自重复同一套自绘箭头样式,而且原生控件 没有「展开中」状态可读,箭头做不了开合动画——提成 SelectBox 组件统一替换: 箭头随开合旋转 180 度,弹层带出入场动画,下方视口不够时向上弹。背景色 留 --select-bg 口子,对话框里沉一档、页面上浮一档,一个值满足不了两边。
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* 内核日志的常驻收集。
|
||||
*
|
||||
* 住在模块级单例里而不是请求页组件里,是因为日志要跨页面留存——收集跟着组件
|
||||
* 走的话,切走一次列表就清空一次。外壳启动时点火,之后跟着 kernel.online 起落。
|
||||
*
|
||||
* 节奏和缓冲:收到的行先进 pending,每 200ms 一批刷进 rows——日志密集时逐条
|
||||
* 触发响应式更新会把界面拖死。暂停就是不刷,缓冲照收(也设上限),恢复时补上。
|
||||
*/
|
||||
import { ref, watch } from 'vue'
|
||||
import { kernel } from './runtime'
|
||||
import { LOG_CAP, pushCapped, streamLogs } from './logs'
|
||||
import type { LogLevel, LogLine } from './logs'
|
||||
|
||||
export interface LogRow extends LogLine {
|
||||
id: number
|
||||
/** 收到那一刻的时间。日志行本身不带时间戳 */
|
||||
time: string
|
||||
}
|
||||
|
||||
/** 日志级别。改它会断开重连:内核的级别是建立连接时定的,中途改不了。 */
|
||||
export const level = ref<LogLevel>('info')
|
||||
export const paused = ref(false)
|
||||
export const rows = ref<LogRow[]>([])
|
||||
export const failure = ref('')
|
||||
/** 内核那边把流断了(比如重启)。和主动中止区分开,这个要告诉用户 */
|
||||
export const ended = ref(false)
|
||||
/** 每刷进一批加一。rows 是原地增改,组件要跟随滚动只能看这个信号 */
|
||||
export const revision = ref(0)
|
||||
|
||||
let pending: LogRow[] = []
|
||||
let nextId = 0
|
||||
let aborter: AbortController | null = null
|
||||
let retryTimer = 0
|
||||
let started = false
|
||||
|
||||
const clock = (d: Date): string =>
|
||||
[d.getHours(), d.getMinutes(), d.getSeconds()]
|
||||
.map((n) => String(n).padStart(2, '0'))
|
||||
.join(':')
|
||||
|
||||
function flush() {
|
||||
if (paused.value) {
|
||||
// 暂停时继续接收但不渲染。缓冲也设上限,暂停再久内存也不涨
|
||||
if (pending.length > LOG_CAP) pending.splice(0, pending.length - LOG_CAP)
|
||||
return
|
||||
}
|
||||
if (pending.length === 0) return
|
||||
pushCapped(rows.value, pending, LOG_CAP)
|
||||
pending = []
|
||||
revision.value += 1
|
||||
}
|
||||
|
||||
/** 只清界面上攒下的,内核那边的日志不受影响。 */
|
||||
export function clearLogs() {
|
||||
rows.value = []
|
||||
pending = []
|
||||
revision.value += 1
|
||||
}
|
||||
|
||||
/**
|
||||
* 断流不等于内核没了:配置重载会掐断流,而内核几秒内就回来、online 从未翻转。
|
||||
* 断了 5 秒后自动重连,失败会再次走到这里形成重试节奏;真离线时交给下面等
|
||||
* online 翻转的 watch。
|
||||
*/
|
||||
function scheduleRetry() {
|
||||
window.clearTimeout(retryTimer)
|
||||
retryTimer = window.setTimeout(() => {
|
||||
if (kernel.value.online) void pump()
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
async function pump() {
|
||||
// 切换级别或内核恢复在线都从这里重连。旧流先停,不然两条流一起往缓冲里倒
|
||||
aborter?.abort()
|
||||
window.clearTimeout(retryTimer)
|
||||
const controller = new AbortController()
|
||||
aborter = controller
|
||||
failure.value = ''
|
||||
ended.value = false
|
||||
try {
|
||||
for await (const line of streamLogs(level.value, controller.signal)) {
|
||||
pending.push({ ...line, id: nextId++, time: clock(new Date()) })
|
||||
}
|
||||
if (!controller.signal.aborted) {
|
||||
ended.value = true
|
||||
scheduleRetry()
|
||||
}
|
||||
} catch (error) {
|
||||
if (controller.signal.aborted) return
|
||||
failure.value = error instanceof Error ? error.message : String(error)
|
||||
scheduleRetry()
|
||||
}
|
||||
}
|
||||
|
||||
/** 外壳启动时调一次。之后跟着内核在线状态和级别自己重连,应用关掉前不停。 */
|
||||
export function startLogStream(): void {
|
||||
if (started) return
|
||||
started = true
|
||||
// 刷新节拍常驻,缓冲空转一次是零开销;应用关掉前不需要清
|
||||
window.setInterval(flush, 200)
|
||||
if (kernel.value.online) void pump()
|
||||
|
||||
// 挂载时内核不在线就不连;等它上线再开始。反方向(流到一半内核掉了)
|
||||
// 由流自己报错或结束来呈现
|
||||
watch(
|
||||
() => kernel.value.online,
|
||||
(online) => {
|
||||
if (online) void pump()
|
||||
},
|
||||
)
|
||||
|
||||
watch(level, () => {
|
||||
if (kernel.value.online) void pump()
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user