From 1b30be45a0c78e6d9544cb276df256fe48133f8f Mon Sep 17 00:00:00 2001 From: mrlee <470349233@qq.com> Date: Wed, 23 Sep 2026 18:08:31 +0800 Subject: [PATCH] =?UTF-8?q?fix(web):=20FRP=20=E6=97=A5=E5=BF=97=E9=A1=B5?= =?UTF-8?q?=E7=AD=BE=E8=A7=A3=E6=9E=90=E3=80=81=E7=BA=A7=E5=88=AB=E7=AD=9B?= =?UTF-8?q?=E9=80=89=E4=B8=8E=E5=91=88=E7=8E=B0=E5=AF=B9=E9=BD=90=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 解析:容忍行首 [组件] 前缀(真实行为 [web] …),来源取 [文件:行号]、缺失时回落组件名,级别统一大写 筛选:级别选项值 INFO/WARN/ERROR 与 frpc 单字母级别对齐(原先与中文标签比较永不相等,选中即过滤为空),并补筛选变化重置页码 呈现:级别/来源改小号标签、消息单行省略带 tooltip、时间走 logTimeCell,与「系统日志」页签一致;空态按筛选结果判定,区分「暂无日志」与「没有匹配的日志」 --- web/src/views/LogsView.vue | 68 ++++++++++++++++++++++++++++++++------ 1 file changed, 57 insertions(+), 11 deletions(-) diff --git a/web/src/views/LogsView.vue b/web/src/views/LogsView.vue index 5b1ba70..08923f3 100644 --- a/web/src/views/LogsView.vue +++ b/web/src/views/LogsView.vue @@ -60,9 +60,9 @@ :item-count="filteredFrpLogs.length" /> @@ -374,18 +374,31 @@ const frpPage = ref(1) const loadingFrp = ref(false) const frpPageSize = 50 const frpLevelText: Record = { I: '信息', W: '警告', E: '错误', D: '调试' } +// 工具条的级别选项值是 INFO / WARN / ERROR,而 frpc 的级别是单字母 I/W/E/D,需转换后再比较 +const frpLevelAlias: Record = { INFO: 'I', WARN: 'W', ERROR: 'E' } -// frpc 行形如:2026-09-23 15:20:31.123 [I] [service.go:295] login to server success +// frpc 行有两种形态: +// 2026-09-23 15:20:31.123 [I] [service.go:295] login to server success +// [web] 2026-09-20 19:38:51.771 [I] [client/control.go:174] [trace-id] [web-http] start proxy success +// 行首可能带一个 [组件] 前缀,来源优先取 [文件:行号],缺失时回落为该组件名 function parseFrpLogLine(line: string): FrpLogEntry { - const matched = line.match(/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?)\s*\[([A-Z])\]\s*(?:\[([^\]]+)\])?\s*(.*)$/) + const matched = line.match( + /^(?:\[([^\]]+)\]\s+)?(\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d+)?)\s*\[([A-Za-z])\]\s*(?:\[([^\]]+)\])?\s*(.*)$/, + ) if (!matched) return { time: '', level: '', source: '', message: line } - return { time: matched[1], level: matched[2], source: matched[3] ?? '', message: matched[4] ?? '' } + const component = matched[1] ?? '' + return { + time: matched[2], + level: matched[3].toUpperCase(), + source: matched[4] || component, + message: matched[5] ?? '', + } } const filteredFrpLogs = computed(() => { const keyword = frpKeyword.value.trim().toLowerCase() return frpLogs.value.filter((log) => { - if (frpLevel.value && frpLevelText[log.level] !== frpLevel.value) return false + if (frpLevel.value && frpLevelAlias[frpLevel.value] !== log.level) return false if (keyword && !`${log.source} ${log.message}`.toLowerCase().includes(keyword)) return false return true }) @@ -396,11 +409,40 @@ const pagedFrpLogs = computed(() => { return filteredFrpLogs.value.slice(start, start + frpPageSize) }) +// 与「系统日志」页签同一套渲染:时间走 logTimeCell,级别/来源用小号标签,消息单行省略并带 tooltip const frpColumns: DataTableColumns = [ - { title: '时间', key: 'time', width: 168, render: (row) => (row.time ? logTimeCell(row.time) : '—') }, - { title: '级别', key: 'level', width: 80, render: (row) => frpLevelText[row.level] ?? (row.level || '—') }, - { title: '来源', key: 'source', width: 170, render: (row) => row.source || '—' }, - { title: '消息', key: 'message', render: (row) => row.message }, + { title: '时间', key: 'time', width: 168, render: (row) => logTimeCell(row.time) }, + { + title: '级别', + key: 'level', + width: 80, + render: (row) => { + const label = frpLevelText[row.level] ?? (row.level || '—') + return h(NTag, { size: 'small', bordered: false, type: systemLevelTagType(label) }, () => label) + }, + }, + { + title: '来源', + key: 'source', + width: 170, + render: (row) => + h( + NTag, + { + size: 'small', + bordered: false, + type: systemModuleTagType(row.source), + class: 'log-module-tag', + }, + () => row.source || '—', + ), + }, + { + title: '消息', + key: 'message', + ellipsis: { tooltip: true }, + render: (row) => h('span', { class: 'log-message' }, row.message), + }, ] function frpRowClassName(row: FrpLogEntry) { @@ -595,6 +637,10 @@ watch([systemKeyword, systemLevel], () => { systemPage.value = 1 }) +watch([frpKeyword, frpLevel], () => { + frpPage.value = 1 +}) + watch(autoRefresh, (on) => { if (refreshTimer) clearInterval(refreshTimer) if (on) refreshTimer = setInterval(() => loadAccess(), 10000)