feat: 桌面端日志级别改为按钮组

This commit is contained in:
jxxghp
2026-07-03 18:06:57 +08:00
parent d2634095e4
commit c076be868a

View File

@@ -139,6 +139,7 @@ const lastVisibleLogId = computed(() => {
return filteredGroups.value.at(-1)?.items.at(-1)?.id ?? 0
})
/** 规范化日志级别名称。 */
function normalizeLevel(level: string) {
const normalizedLevel = level.trim().replace(/:$/, '').toUpperCase()
@@ -153,14 +154,17 @@ function normalizeLevel(level: string) {
return normalizedLevel
}
/** 移除日志文本中的 ANSI 颜色控制符。 */
function stripAnsi(text: string) {
return text.replace(ANSI_PATTERN, '')
}
/** 从日志文本中提取时间戳。 */
function extractTimestamp(text: string) {
return text.match(TIMESTAMP_PATTERN)?.[0] ?? ''
}
/** 将日志时间戳转换为毫秒值。 */
function getTimestampMs(timestamp: string) {
if (!timestamp) {
return null
@@ -172,6 +176,7 @@ function getTimestampMs(timestamp: string) {
return Number.isNaN(parsedTimestamp) ? null : parsedTimestamp
}
/** 获取用于日志分组的秒级时间键。 */
function getSecondKey(timestamp: string) {
if (!timestamp) {
return ''
@@ -180,10 +185,12 @@ function getSecondKey(timestamp: string) {
return timestamp.replace('T', ' ').slice(0, 19)
}
/** 格式化秒级时间键用于界面展示。 */
function getSecondDisplay(secondKey: string) {
return secondKey ? secondKey.replaceAll('-', '/') : ''
}
/** 提取日志时间戳中的时分秒部分。 */
function getTimeDisplay(timestamp: string) {
if (!timestamp) {
return ''
@@ -192,6 +199,7 @@ function getTimeDisplay(timestamp: string) {
return timestamp.split(' ').at(-1) ?? timestamp
}
/** 从结构化日志正文中提取消息内容。 */
function extractMessage(text: string) {
return (
text
@@ -201,6 +209,7 @@ function extractMessage(text: string) {
)
}
/** 根据原始日志和解析结果创建展示条目。 */
function createLogEntry(raw: string, parsed?: ParsedLog | null): LogEntry {
const level = parsed?.level ?? ''
const appName = parsed?.appName ?? ''
@@ -224,6 +233,7 @@ function createLogEntry(raw: string, parsed?: ParsedLog | null): LogEntry {
}
}
/** 解析 Python 风格的日志行。 */
function parsePythonStyleLog(raw: string): ParsedLog | null {
const match = raw.match(/^([A-Za-z]+):\s+(.*)$/)
if (!match) {
@@ -251,6 +261,7 @@ function parsePythonStyleLog(raw: string): ParsedLog | null {
}
}
/** 解析中文方括号标记的日志行。 */
function parseBracketStyleLog(raw: string): ParsedLog | null {
const match = raw.match(/^【([^】]+)】\s*(.*)$/)
if (!match) {
@@ -274,6 +285,7 @@ function parseBracketStyleLog(raw: string): ParsedLog | null {
}
}
/** 解析以时间戳开头的日志行。 */
function parseTimestampFirstLog(raw: string): ParsedLog | null {
const match = raw.match(new RegExp(`^(${TIMESTAMP_PATTERN.source})\\s+\\[?([A-Za-z]+)\\]?\\s*(.*)$`))
if (!match) {
@@ -299,6 +311,7 @@ function parseTimestampFirstLog(raw: string): ParsedLog | null {
}
}
/** 解析以内联级别开头的日志行。 */
function parseInlineLevelLog(raw: string): ParsedLog | null {
const match = raw.match(/^\[?([A-Za-z]+)\]?:?\s+(.*)$/)
if (!match) {
@@ -326,6 +339,7 @@ function parseInlineLevelLog(raw: string): ParsedLog | null {
}
}
/** 将单行原始日志解析为展示条目。 */
function parseLogLine(log: string): LogEntry {
const raw = stripAnsi(log).replace(/\r/g, '').trimEnd()
const parsed =
@@ -334,6 +348,7 @@ function parseLogLine(log: string): LogEntry {
return createLogEntry(raw, parsed)
}
/** 判断日志条目是否符合当前级别和关键字筛选。 */
function matchesLogFilter(item: LogEntry) {
const matchesLevel = selectedLevel.value === 'ALL' || item.level === selectedLevel.value
if (!matchesLevel) {
@@ -350,6 +365,7 @@ function matchesLogFilter(item: LogEntry) {
.includes(normalizedSearchQuery.value)
}
/** 判断日志条目能否合并进当前时间分组。 */
function canMergeIntoGroup(group: LogGroup, item: LogEntry) {
if (!group.secondKey || !item.secondKey) {
return false
@@ -366,6 +382,7 @@ function canMergeIntoGroup(group: LogGroup, item: LogEntry) {
return true
}
/** 判断日志视口是否接近底部。 */
function isNearBottom() {
if (!logViewportRef.value) {
return true
@@ -375,6 +392,7 @@ function isNearBottom() {
return scrollHeight - scrollTop - clientHeight <= SCROLL_BOTTOM_THRESHOLD
}
/** 将日志视口滚动到底部。 */
function scrollToBottom(behavior: ScrollBehavior = 'auto') {
if (!logViewportRef.value) {
return
@@ -386,6 +404,7 @@ function scrollToBottom(behavior: ScrollBehavior = 'auto') {
})
}
/** 启用日志末尾跟随并清空待查看计数。 */
function enableFollow(behavior: ScrollBehavior = 'auto') {
followTail.value = true
pendingLogCount.value = 0
@@ -395,6 +414,7 @@ function enableFollow(behavior: ScrollBehavior = 'auto') {
})
}
/** 将缓冲区中的日志批量写入展示列表。 */
function flushBuffer() {
if (timeoutId) {
clearTimeout(timeoutId)
@@ -430,6 +450,7 @@ function flushBuffer() {
pendingLogCount.value += incomingLogs.length
}
/** 安排一次延迟缓冲区刷新。 */
function scheduleFlush() {
if (timeoutId) {
return
@@ -440,6 +461,7 @@ function scheduleFlush() {
}, FLUSH_DELAY)
}
/** 接收并缓存 SSE 日志消息。 */
function handleSSEMessage(event: MessageEvent) {
if (!event.data) {
return
@@ -462,6 +484,7 @@ const { manager, isConnected } = useSSE(
},
)
/** 暂停实时日志流。 */
function pauseStream() {
if (isStreamPaused.value) {
return
@@ -473,6 +496,7 @@ function pauseStream() {
manager.removeMessageListener(listenerId)
}
/** 恢复实时日志流。 */
function resumeStream() {
if (!isStreamPaused.value) {
return
@@ -483,6 +507,7 @@ function resumeStream() {
manager.addMessageListener(listenerId, handleSSEMessage)
}
/** 切换实时日志流的暂停状态。 */
function toggleStreamState() {
if (isStreamPaused.value) {
resumeStream()
@@ -492,6 +517,7 @@ function toggleStreamState() {
pauseStream()
}
/** 根据滚动位置更新日志末尾跟随状态。 */
function handleScroll() {
if (isNearBottom()) {
followTail.value = true
@@ -535,7 +561,29 @@ onUnmounted(() => {
:style="loggingViewStyle"
>
<div class="logging-toolbar px-3">
<VBtnToggle
v-if="display.mdAndUp.value"
v-model="selectedLevel"
mandatory
divided
density="compact"
variant="text"
selected-class="logging-level-toggle__button--active"
class="logging-level-toggle"
>
<VBtn
v-for="level in levelOptions"
:key="level"
:value="level"
:class="[`logging-level-toggle__button--${level.toLowerCase()}`]"
class="logging-level-toggle__button"
>
{{ level === 'ALL' ? t('common.all') : level }}
</VBtn>
</VBtnToggle>
<VSelect
v-else
v-model="selectedLevel"
:items="levelOptions"
density="compact"
@@ -716,6 +764,52 @@ onUnmounted(() => {
min-inline-size: 7rem;
}
.logging-level-toggle {
flex: 0 0 auto;
overflow: hidden;
border: 1px solid rgba(var(--v-theme-on-surface), 0.14);
border-radius: var(--app-control-radius);
background: transparent;
block-size: 2rem;
box-shadow: none !important;
}
.logging-level-toggle :deep(.logging-level-toggle__button) {
border-color: rgba(var(--v-theme-on-surface), 0.1);
border-radius: 0;
block-size: 100%;
color: var(--logging-muted);
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0;
min-inline-size: auto;
padding-inline: 1.125rem;
text-transform: none;
}
.logging-level-toggle :deep(.logging-level-toggle__button--trace),
.logging-level-toggle :deep(.logging-level-toggle__button--debug) {
color: rgb(var(--v-theme-secondary));
}
.logging-level-toggle :deep(.logging-level-toggle__button--info) {
color: rgb(var(--v-theme-success));
}
.logging-level-toggle :deep(.logging-level-toggle__button--warning) {
color: rgb(var(--v-theme-warning));
}
.logging-level-toggle :deep(.logging-level-toggle__button--error),
.logging-level-toggle :deep(.logging-level-toggle__button--critical) {
color: rgb(var(--v-theme-error));
}
.logging-level-toggle :deep(.logging-level-toggle__button--active) {
background: rgba(var(--v-theme-primary), 0.16);
color: rgb(var(--v-theme-primary));
}
.logging-level-select :deep(.v-field) {
border-radius: var(--app-field-radius);
background: transparent !important;