feat(ui): 最近事件随分组页签联动过滤 + favicon 提供 WebP

① 最近事件随筛选过滤

此前事件只受后端 dataFilter 约束,与面板上方的分组页签无关 —— 切页签后
监控列表变了,事件列表却纹丝不动。

两个真实缺陷(都是实测发现,不是推断):
1. setFilter() 只调 renderTabs() + renderView(),**没调 renderEvents()**
2. 过滤依据用 allMonitors(),它只受 dataFilter 约束、不受分组影响
   → 改为 visibleGroups().flatMap(...)
3. 无匹配事件时提前 return,没清空 list.innerHTML → 旧行仍留在 DOM 里,
   换个分组再切回来会看到过期内容

实测:「全部」8 条 → 「UptimeRobot」1 条(正是「狼的评论系统 /
UptimeRobot:403 Forbidden」)→「基础设施」0 条且区块自动隐藏。

过滤用「当前可见目标 id 集合」反查,而不是事件自身的 group 字段 ——
外部数据源(UptimeRobot)的事件在快照里没有 group 字段,按名字反查才准。

② favicon 提供 WebP(保留原图与 PNG)

先核实了实际情况:assets-src 下三个原图(bike 1.8MB / figure 366KB /
icon 277KB)此前已转成 WebP,public/assets 里唯一的 PNG 是 favicon-shiroko.png。
因此本次只处理 favicon:新增 64×64 WebP(2.7KB,比 PNG 的 5.6KB 省一半)。

**PNG 不删** —— favicon 是浏览器最早的请求,WebP 覆盖虽广但旧浏览器会直接
不显示图标,留 PNG 作回退代价只有几 KB,换来「任何浏览器都有图标」更稳妥。
原图 assets-src/ 完全未动,prepare-artwork.py 可随时重新生成。

页面用两个 <link rel="icon" type=...> 让浏览器按格式自动挑选
(认识 WebP 就取 WebP,否则回落 PNG / SVG)。

③ 顺带修一个既存缺陷
探针的 MIME 表**缺 .webp**,导致三张 webp 素材一直被当作
application/octet-stream 伺服(能显示但类型不对)。已补。

测试:verify-views 49→52 项(事件随页签过滤、切回全部恢复、无事件时清空 DOM)。
verify.cjs 实为 24 项(此前 README 记的 17 是旧数据)。全项目 8 套 346 项通过。
This commit is contained in:
tamakyi
2026-10-14 17:35:01 +08:00
parent 20b499c350
commit bf27f7b385
11 changed files with 1918 additions and 664 deletions
+39 -5
View File
@@ -283,14 +283,25 @@ function renderEvents() {
if (!box) return;
const list = $('events-list');
const empty = $('events-empty');
const all = state.status?.events ?? [];
const count = $('events-count');
// 事件随分组页签联动:只展示当前页签下目标的事件。
// 用「当前可见目标 id 集合」反查,而不是事件自身的 group 字段 ——
// 外部数据源(UptimeRobot)的事件在快照里没有 group,按名字反查才准。
const visibleIds = new Set(visibleGroups().flatMap(([, list]) => list.map((m) => m.id)));
const all = (state.status?.events ?? []).filter((e) => visibleIds.has(e.id));
box.hidden = all.length === 0;
empty.hidden = all.length > 0;
if (!all.length) return;
if (!all.length) {
// 必须清空列表:仅隐藏区块的话旧行仍留在 DOM 里(换个分组再切回来会看到过期内容)
list.innerHTML = '';
if (count) count.textContent = '';
const more = $('events-more');
if (more) more.hidden = true;
return;
}
const expanded = state.eventsExpanded === true;
const shown = expanded ? all : all.slice(0, EVENTS_COLLAPSED);
const count = $('events-count');
if (count) count.textContent = all.length > shown.length ? `最近 ${shown.length} / ${all.length} 条` : `最近 ${all.length} 条`;
const label = { down: '故障', up: '恢复', tls: '证书', maintenance: '维护' };
@@ -488,6 +499,8 @@ function setFilter(g) {
state.filter = g;
renderTabs();
renderView();
// 事件按当前页签过滤,切页签后必须一并重渲染
renderEvents();
}
function visibleGroups() {
@@ -943,6 +956,28 @@ function applyArtwork() {
document.body.appendChild(img);
}
// favicon 格式协商:同尺寸的 WebP 体积更小,浏览器按 type 自动挑第一个认识的;
// 不认识 WebP 的旧浏览器会跳到 PNG,两者并存保证任何环境都有图标。
function setFavicon(shiroko) {
const head = document.head;
if (!head) return;
for (const el of head.querySelectorAll('link[data-favicon]')) el.remove();
const links = shiroko
? [
{ href: 'assets/favicon-shiroko.webp', type: 'image/webp' },
{ href: 'assets/favicon-shiroko.png', type: 'image/png' },
]
: [{ href: 'assets/favicon.svg', type: 'image/svg+xml' }];
for (const l of links) {
const el = document.createElement('link');
el.rel = 'icon';
el.href = l.href;
el.type = l.type;
el.dataset.favicon = '1';
head.appendChild(el);
}
}
function applyTheme(theme, { persist = true } = {}) {
state.theme = THEMES[theme] ? theme : 'default';
if (state.theme === 'default') delete document.body.dataset.theme;
@@ -955,8 +990,7 @@ function applyTheme(theme, { persist = true } = {}) {
localStorage.setItem('shiroko.theme', state.theme);
} catch { /* 忽略 */ }
}
const icon = document.querySelector('link[rel="icon"]');
if (icon) icon.setAttribute('href', state.theme === 'shiroko' ? 'assets/favicon-shiroko.png' : 'assets/favicon.svg');
setFavicon(state.theme === 'shiroko');
applyArtwork();
const btn = $('theme-btn');
if (btn) {
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB