feat(ui): add log level filter in log view

Allow filtering logs by level (INFO, WARNING, ERROR, DEBUG) with
color-coded filter chips. Multiple levels can be selected simultaneously.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Estrella Pan
2026-01-25 10:43:43 +01:00
parent d2bf733a3e
commit ef45681ce3
3 changed files with 192 additions and 3 deletions

View File

@@ -147,8 +147,10 @@
},
"log": {
"bug_repo": "Bug Report",
"clear_filters": "Clear",
"contact_info": "Contact Infomation",
"copy": "Copy",
"filter_level": "Level",
"go": "Go",
"join": "Join",
"reset": "Reset",
@@ -241,13 +243,20 @@
"sun": "Sun"
},
"unknown": "Unknown",
"unknown_title": "Unknown Title",
"today": "Today",
"empty": "No anime",
"refresh": "Refresh schedule",
"no_data": "No schedule data available",
"loading": "Loading...",
"tracked": "Tracked",
"tracked_count": "{count} tracked",
"total_count": "{count} total",
"search_title": "Search and Add Anime",
"no_search_results": "No results found. Try different keywords.",
"empty_state": {
"title": "No Schedule Yet",
"subtitle": "Add anime from RSS to see your weekly schedule"
"subtitle": "Click refresh to fetch this season's broadcast data"
}
},
"setup": {

View File

@@ -147,8 +147,10 @@
},
"log": {
"bug_repo": "Bug 反馈",
"clear_filters": "清除",
"contact_info": "联系方式",
"copy": "复制",
"filter_level": "级别",
"go": "访问",
"join": "加入",
"reset": "重置",
@@ -241,13 +243,20 @@
"sun": "日"
},
"unknown": "未知",
"unknown_title": "未知标题",
"today": "今天",
"empty": "今日无番",
"refresh": "刷新放送表",
"no_data": "暂无放送数据",
"loading": "加载中...",
"tracked": "已追踪",
"tracked_count": "已追踪 {count} 部",
"total_count": "共 {count} 部",
"search_title": "搜索并添加番剧",
"no_search_results": "未找到搜索结果,请尝试其他关键词",
"empty_state": {
"title": "暂无放送表",
"subtitle": "从 RSS 添加番剧后即可查看每周放送时间"
"subtitle": "点击刷新按钮获取本季度放送数据"
}
},
"setup": {

View File

@@ -9,6 +9,12 @@ const { onUpdate, offUpdate, reset, copy, getLog } = useLogStore();
const { log } = storeToRefs(useLogStore());
const { version } = useAppInfo();
// Filter states
const selectedLevels = ref<string[]>([]);
// Log levels
const logLevels = ['INFO', 'WARNING', 'ERROR', 'DEBUG'];
const formatLog = computed(() => {
const list = log.value
.trim()
@@ -30,6 +36,31 @@ const formatLog = computed(() => {
});
});
// Filtered logs based on selected levels
const filteredLog = computed(() => {
if (selectedLevels.value.length === 0) {
return formatLog.value;
}
return formatLog.value.filter((entry) =>
selectedLevels.value.includes(entry.type)
);
});
// Toggle level filter
function toggleLevel(level: string) {
const index = selectedLevels.value.indexOf(level);
if (index === -1) {
selectedLevels.value.push(level);
} else {
selectedLevels.value.splice(index, 1);
}
}
// Clear all filters
function clearFilters() {
selectedLevels.value = [];
}
function typeColor(type: string) {
const M: Record<string, string> = {
INFO: 'var(--color-primary)',
@@ -74,9 +105,38 @@ onDeactivated(() => {
<div class="page-log">
<div class="log-layout">
<ab-container :title="$t('log.title')" class="log-main">
<!-- Level Filter Section -->
<div class="log-filters">
<div class="filter-group">
<span class="filter-label">{{ $t('log.filter_level') }}</span>
<div class="filter-chips">
<button
v-for="level in logLevels"
:key="level"
class="filter-chip"
:class="{
active: selectedLevels.includes(level),
[`level-${level.toLowerCase()}`]: true,
}"
@click="toggleLevel(level)"
>
{{ level }}
</button>
</div>
</div>
<button
v-if="selectedLevels.length > 0"
class="clear-filters"
@click="clearFilters"
>
{{ $t('log.clear_filters') }}
</button>
</div>
<div ref="logContainer" class="log-viewer">
<div class="log-content">
<template v-for="i in formatLog" :key="i.index">
<template v-for="i in filteredLog" :key="i.index">
<div
class="log-entry"
:style="{ color: typeColor(i.type) }"
@@ -252,6 +312,117 @@ onDeactivated(() => {
font-size: 13px;
}
.log-filters {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid var(--color-border);
}
.filter-group {
display: flex;
flex-direction: column;
gap: 8px;
@include forDesktop {
flex-direction: row;
align-items: center;
}
}
.filter-label {
font-size: 13px;
font-weight: 500;
color: var(--color-text-muted);
min-width: 60px;
}
.filter-chips {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.filter-chip {
padding: 4px 12px;
border-radius: var(--radius-sm);
border: 1px solid var(--color-border);
background: transparent;
font-size: 12px;
cursor: pointer;
transition: all var(--transition-fast);
color: var(--color-text);
&:hover {
border-color: var(--color-primary);
}
&.active {
background: var(--color-primary);
border-color: var(--color-primary);
color: white;
}
&.level-info {
&:hover,
&.active {
border-color: var(--color-primary);
}
&.active {
background: var(--color-primary);
}
}
&.level-warning {
&:hover,
&.active {
border-color: var(--color-warning);
}
&.active {
background: var(--color-warning);
}
}
&.level-error {
&:hover,
&.active {
border-color: var(--color-danger);
}
&.active {
background: var(--color-danger);
}
}
&.level-debug {
&:hover,
&.active {
border-color: var(--color-text-muted);
}
&.active {
background: var(--color-text-muted);
}
}
}
.clear-filters {
align-self: flex-start;
padding: 4px 12px;
border-radius: var(--radius-sm);
border: none;
background: var(--color-danger-light);
color: var(--color-danger);
font-size: 12px;
cursor: pointer;
transition: all var(--transition-fast);
&:hover {
background: var(--color-danger);
color: white;
}
}
.log-actions {
display: flex;
justify-content: flex-end;