fix(ui): 表格行高统一 + 事件区块改为与表格同一套样式
1) 行高不一致(「狼的评论系统」明显矮一截) 实测在线行 58px / 离线行 43px。根因是可用率列的 90 格 mini 条: 它是 inline-flex,参与行内布局时基线把行盒抬高约 15px,于是「有可用率 数据的行」比不带条的行高出一截。 修复:单元格内容统一 vertical-align: middle + mini 条容器改 flex; 实测 6 行全部 43px。 2) 「最近事件」与表格样式不统一 去掉独立的时间线卡片风格,改为与监控表格同构的表格(相同容器圆角、 表头、行分隔、hover、状态底色);故障行淡红、证书行淡琥珀、恢复行不染。 刻意不共用 .monitor-table 类名 —— 共用时面板条目统计被多算 1~8 个 (测试当场抓到),改为独立 .events-table 并显式声明所需样式。 说明:内容长度相当时保证等高;名称/地址特别长时允许换行(上一版按用户 要求做的「完整显示」),此时行更高属于数据本身需要,不再用截断换整齐。 测试:verify-views 新增「内容等长时各行等高」「事件表与监控表同构」 「事件行复用状态底色」断言(并修正了断言脚本改错文件的问题 —— 名称来自 monitors.json 而非 status.json)。六套 171 项全通过。
@@ -540,6 +540,16 @@ UptimeRobot 的探测来自其多个节点,适合作为**异地视角**与自
|
||||
- [x] 页脚改为展示「数据更新于 … + 下次检查 Ns」,tooltip 内含服务端更新节奏与快照生成时间
|
||||
- [x] `scripts/verify.cjs` 适配纯外部数据源部署(自动跳过不适用的探测类断言),并新增「倒计时对齐服务端」断言
|
||||
|
||||
### v0.11.2 已实现
|
||||
|
||||
**修复你反馈的两个问题**
|
||||
|
||||
- [x] **「狼的评论系统」那一条高度/样式与其他行不一致**:实测在线行 **58px**、离线行 **43px**。根因是**可用率列的 90 格 mini 条**——它是 `inline-flex` 元素,参与行内布局时其基线会把行盒抬高约 15px,于是「当天有可用率数据、条正常渲染」的行比不带条的行高出一截。修复:给单元格内容统一 `vertical-align: middle`、mini 条容器改 flex 布局,现在**所有行等高(实测 43px)**
|
||||
- [x] **「最近事件」改为与表格同一套样式**:不再用独立的「时间线卡片」风格,改为**与监控表格完全同构的表格**(相同的圆角容器、表头样式、行分隔线、hover、状态底色)——故障行淡红、证书行淡琥珀、恢复行不染;类型以小徽标呈现,详情列完整显示
|
||||
- 实现上刻意**不共用 `.monitor-table` 类名**,避免被其他按类名统计/定位的逻辑误算(这一点被测试当场抓到:共用类名时面板条目数被多算 1~8 个)
|
||||
|
||||
> 说明:行高在「内容长度相当」时保证一致;名称或地址特别长时允许换行(这是上一版你要求的「数据完整显示」),此时行自然更高——属于数据本身需要的空间,不再靠截断换取整齐。
|
||||
|
||||
### v0.11.1 已实现
|
||||
|
||||
**修复你反馈的三个问题**
|
||||
|
||||
|
Before Width: | Height: | Size: 239 KiB After Width: | Height: | Size: 243 KiB |
|
Before Width: | Height: | Size: 441 KiB After Width: | Height: | Size: 450 KiB |
|
Before Width: | Height: | Size: 239 KiB After Width: | Height: | Size: 243 KiB |
|
Before Width: | Height: | Size: 265 KiB After Width: | Height: | Size: 267 KiB |
|
Before Width: | Height: | Size: 219 KiB After Width: | Height: | Size: 222 KiB |
@@ -11,7 +11,7 @@
|
||||
|
||||
import { connect } from 'cloudflare:sockets';
|
||||
|
||||
const VERSION = '0.11.1';
|
||||
const VERSION = '0.11.2';
|
||||
const HISTORY_LIMIT = 48;
|
||||
const UPTIME_DAYS = 90;
|
||||
const USER_AGENT = 'shiroko-monitor/0.3 (lightweight uptime panel)';
|
||||
|
||||
@@ -37,7 +37,7 @@ const CONCURRENCY = 8;
|
||||
const WRITE_DEBOUNCE_MS = 300;
|
||||
const DEFAULT_INTERVAL_SEC = 60;
|
||||
const PORT = Number(process.env.PORT || 8787);
|
||||
const VERSION = '0.11.1';
|
||||
const VERSION = '0.11.2';
|
||||
const UR_DEFAULT_POLL_SEC = 300;
|
||||
const UR_MIN_REFRESH_GAP_MS = 60000;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const VERSION = '0.11.1';
|
||||
const VERSION = '0.11.2';
|
||||
const HB_LIMIT = 48;
|
||||
const UPTIME_DAYS = 90;
|
||||
const BROWSER_CONCURRENCY = 6;
|
||||
@@ -263,14 +263,16 @@ function renderEvents() {
|
||||
if (count) count.textContent = all.length > shown.length ? `最近 ${shown.length} / ${all.length} 条` : `最近 ${all.length} 条`;
|
||||
|
||||
const label = { down: '故障', up: '恢复', tls: '证书', maintenance: '维护' };
|
||||
// 与监控表格同一套结构(tr/td + data-status),因此行高、边框、hover、状态底色天然一致
|
||||
const statusOf = { down: 'down', up: 'up', tls: 'pending', maintenance: 'up' };
|
||||
list.innerHTML = shown.map((e) => {
|
||||
const maint = e.maintenance ? '<span class="ev-maint" title="维护窗口内,未计入可用率、未发送告警">维护</span>' : '';
|
||||
return `<li class="ev ev-${escapeHtml(e.type)}">
|
||||
<span class="ev-time" title="${escapeHtml(fmtTime(e.t))}">${escapeHtml(fmtRelative(e.t))}</span>
|
||||
<span class="ev-type">${escapeHtml(label[e.type] ?? e.type)}${maint}</span>
|
||||
<span class="ev-name" title="${escapeHtml(e.name)}">${escapeHtml(e.name)}</span>
|
||||
<span class="ev-detail" title="${escapeHtml(e.detail ?? '')}">${escapeHtml(e.detail ?? '')}</span>
|
||||
</li>`;
|
||||
return `<tr class="ev" data-status="${escapeHtml(statusOf[e.type] ?? 'up')}">
|
||||
<td class="ev-time" title="${escapeHtml(fmtTime(e.t))}">${escapeHtml(fmtRelative(e.t))}</td>
|
||||
<td class="ev-type"><span class="ev-badge">${escapeHtml(label[e.type] ?? e.type)}</span>${maint}</td>
|
||||
<td class="ev-name" title="${escapeHtml(e.name)}">${escapeHtml(e.name)}</td>
|
||||
<td class="ev-detail" title="${escapeHtml(e.detail ?? '')}">${escapeHtml(e.detail ?? '')}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
|
||||
const more = $('events-more');
|
||||
|
||||
@@ -390,7 +390,12 @@ body[data-view="table"] .grid { display: block; }
|
||||
background: var(--bg);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.monitor-table td { padding: 9px 12px; border-bottom: 1px solid var(--border); vertical-align: middle; overflow: hidden; }
|
||||
.monitor-table td { padding: 9px 12px; border-bottom: 1px solid var(--border); vertical-align: middle; overflow: hidden; line-height: 1.4; }
|
||||
/* 所有行统一基线:可用率的 mini 条是 inline-flex,其基线会把行盒撑高十几像素,
|
||||
导致带条的行比不带的行高出一截(实测 58px vs 43px) */
|
||||
.monitor-table td > * { vertical-align: middle; }
|
||||
.u-mini { display: inline-flex; align-items: center; gap: 6px; vertical-align: middle; }
|
||||
.monitor-table tbody tr[data-status="pending"] { background: color-mix(in srgb, var(--pending) 6%, transparent); }
|
||||
.monitor-table tbody tr:last-child td { border-bottom: 0; }
|
||||
.monitor-table tbody tr:hover { background: color-mix(in srgb, var(--accent) 5%, transparent); }
|
||||
.monitor-table tbody tr[data-status="down"] { background: color-mix(in srgb, var(--down) 6%, transparent); }
|
||||
@@ -432,7 +437,7 @@ tr[data-status="pending"] .t-state { color: var(--pending-text); }
|
||||
.t-hb svg { display: block; width: 100%; max-width: 150px; height: 24px; }
|
||||
.t-uptime { white-space: normal; }
|
||||
/* 可用率条随视口伸缩;极窄时只留百分比,避免挤压其他列 */
|
||||
.uptime-bar-mini { display: inline-flex; width: clamp(32px, 6vw, 84px); height: 8px; margin-right: 6px; vertical-align: middle; }
|
||||
.uptime-bar-mini { display: flex; flex: none; width: clamp(32px, 6vw, 84px); height: 8px; margin-right: 0; vertical-align: middle; }
|
||||
.t-pct { font-size: 12px; color: var(--text-2); font-variant-numeric: tabular-nums; }
|
||||
@media (max-width: 640px) {
|
||||
.uptime-bar-mini { display: none; }
|
||||
@@ -459,69 +464,52 @@ tr[data-status="pending"] .t-state { color: var(--pending-text); }
|
||||
}
|
||||
.events-more:hover { background: color-mix(in srgb, var(--accent) 16%, transparent); }
|
||||
|
||||
/* 左侧竖线 + 每行圆点,构成时间线;行本身仍是浅色卡片 */
|
||||
.events-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 2px 0 2px 16px;
|
||||
position: relative;
|
||||
}
|
||||
.events-list::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 5px;
|
||||
top: 8px;
|
||||
bottom: 8px;
|
||||
width: 1px;
|
||||
background: var(--border);
|
||||
}
|
||||
.ev {
|
||||
position: relative;
|
||||
display: grid;
|
||||
/* 名称列按内容自适应,详情列吃掉剩余宽度 —— 避免短名称后面拖出一大片空白 */
|
||||
grid-template-columns: 62px 46px auto minmax(0, 1fr);
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 6px 12px;
|
||||
border-radius: 9px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
/* 事件表:直接复用监控表格的外观(圆角、边框、行分隔、hover、状态底色),
|
||||
只额外定义列宽与类型徽标,保证两处视觉完全一致 */
|
||||
/* 事件表:显式声明与监控表格相同的观感(不共用 .monitor-table 类,
|
||||
避免被其他按类名统计/定位的逻辑误算),列宽与类型徽标单独定义 */
|
||||
.events-table { width: 100%; table-layout: fixed; border-collapse: collapse; font-size: 13px; }
|
||||
.events-table th {
|
||||
padding: 10px 12px;
|
||||
text-align: left;
|
||||
font-size: 12px;
|
||||
min-width: 0;
|
||||
font-weight: 600;
|
||||
color: var(--text-2);
|
||||
background: var(--bg);
|
||||
border-bottom: 1px solid var(--border);
|
||||
white-space: normal;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.ev + .ev { margin-top: 4px; }
|
||||
.ev::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -14px;
|
||||
top: 50%;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
margin-top: -3.5px;
|
||||
border-radius: 50%;
|
||||
background: var(--text-2);
|
||||
box-shadow: 0 0 0 3px var(--bg);
|
||||
}
|
||||
.ev-down { border-left: 3px solid var(--down); }
|
||||
.ev-up { border-left: 3px solid var(--up); }
|
||||
.ev-tls { border-left: 3px solid var(--pending); }
|
||||
.ev-down::before { background: var(--down); }
|
||||
.ev-up::before { background: var(--up); }
|
||||
.ev-tls::before { background: var(--pending); }
|
||||
.events-table td { padding: 11px 12px; border-bottom: 1px solid var(--border); vertical-align: middle; line-height: 1.4; overflow: hidden; }
|
||||
.events-table tbody tr:last-child td { border-bottom: 0; }
|
||||
.events-table tbody tr:hover { background: color-mix(in srgb, var(--accent) 5%, transparent); }
|
||||
.events-table tbody tr[data-status="down"] { background: color-mix(in srgb, var(--down) 6%, transparent); }
|
||||
.events-table tbody tr[data-status="pending"] { background: color-mix(in srgb, var(--pending) 6%, transparent); }
|
||||
.events-wrap { margin-top: 0; }
|
||||
.events-table th:nth-child(1) { width: 11%; }
|
||||
.events-table th:nth-child(2) { width: 9%; }
|
||||
.events-table th:nth-child(3) { width: 26%; }
|
||||
.events-table th:nth-child(4) { width: 54%; }
|
||||
.ev-time { color: var(--text-2); font-variant-numeric: tabular-nums; white-space: nowrap; }
|
||||
.ev-type { font-weight: 650; text-align: center; border-radius: 999px; padding: 2px 0; font-size: 11px; }
|
||||
.ev-down .ev-type { background: var(--down-bg); color: var(--down-text); }
|
||||
.ev-up .ev-type { background: var(--up-bg); color: var(--up-text); }
|
||||
.ev-tls .ev-type { background: var(--pending-bg); color: var(--pending-text); }
|
||||
.ev-badge {
|
||||
display: inline-block;
|
||||
padding: 1px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 650;
|
||||
}
|
||||
tr[data-status="down"] .ev-badge { background: var(--down-bg); color: var(--down-text); }
|
||||
tr[data-status="up"] .ev-badge { background: var(--up-bg); color: var(--up-text); }
|
||||
tr[data-status="pending"] .ev-badge { background: var(--pending-bg); color: var(--pending-text); }
|
||||
.ev-maint { margin-left: 6px; border-radius: 999px; padding: 0 6px; font-size: 10px; font-weight: 650; background: color-mix(in srgb, var(--accent) 14%, transparent); color: var(--accent); }
|
||||
.ev-name { font-weight: 600; max-width: 340px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
|
||||
.ev-detail { color: var(--text-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
|
||||
.events-empty { margin: 0; padding: 12px; font-size: 12px; color: var(--text-2); background: var(--surface); border: 1px dashed var(--border); border-radius: 10px; }
|
||||
.ev-name { font-weight: 600; }
|
||||
.ev-detail { color: var(--text-2); }
|
||||
.events-empty { margin: 0; padding: 12px; font-size: 12px; color: var(--text-2); background: var(--surface); border: 1px dashed var(--border); border-radius: 12px; }
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.ev { grid-template-columns: 56px 44px minmax(0, 1fr); gap: 8px; }
|
||||
.ev-detail { display: none; }
|
||||
.events { padding: 0 14px; }
|
||||
.events-table th:nth-child(4), .ev-detail { display: none; }
|
||||
}
|
||||
|
||||
/* ---------- 卡片:90 天日均延迟趋势 ---------- */
|
||||
|
||||
@@ -57,7 +57,14 @@
|
||||
<span class="events-count" id="events-count"></span>
|
||||
<button type="button" class="events-more" id="events-more" hidden>展开全部</button>
|
||||
</div>
|
||||
<ul class="events-list" id="events-list"></ul>
|
||||
<div class="table-wrap events-wrap">
|
||||
<table class="events-table">
|
||||
<thead>
|
||||
<tr><th>时间</th><th>类型</th><th>目标</th><th>详情</th></tr>
|
||||
</thead>
|
||||
<tbody id="events-list"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="events-empty" id="events-empty" hidden>暂无事件记录(故障、恢复与证书提醒会出现在这里)</p>
|
||||
</section>
|
||||
|
||||
@@ -68,7 +75,7 @@
|
||||
<span id="notify-badge" class="footer-item" hidden>通知 –</span>
|
||||
<span id="updated-at" class="footer-item">–</span>
|
||||
<span id="countdown" class="footer-item">–</span>
|
||||
<span class="footer-item version">shiroko-monitor v0.11.1</span>
|
||||
<span class="footer-item version">shiroko-monitor v0.11.2</span>
|
||||
</footer>
|
||||
|
||||
<script type="module" src="assets/app.js"></script>
|
||||
|
||||
@@ -300,15 +300,31 @@ async function main() {
|
||||
const dt = await dp.evaluate(() => document.querySelector('tr[data-status] .t-uptime')?.title ?? '');
|
||||
check('表格可用率 tooltip 含 90 天日均延迟', /90 天日均延迟 \d+ ms/.test(dt), dt.slice(0, 60));
|
||||
|
||||
// ---- 极端内容:表格不得把数据截断,同时不得出现横向滚动 ----
|
||||
// 行高一致性要在「内容长度相当」的前提下检查:先把配置里的名称统一成等长短名
|
||||
// (注意:名称来自 monitors.json,不是 status.json),再单独注入超长内容验证不截断
|
||||
const statusPath = path.join(tmpDir, 'status.json');
|
||||
const snap = JSON.parse(fs.readFileSync(statusPath, 'utf8'));
|
||||
const ids = Object.keys(snap.monitors);
|
||||
const cfgPath2 = path.join(tmpDir, 'monitors.json');
|
||||
const cfgJson = JSON.parse(fs.readFileSync(cfgPath2, 'utf8'));
|
||||
let ids = Object.keys(snap.monitors);
|
||||
cfgJson.monitors.forEach((m, i) => { m.name = `目标 ${i + 1}`; });
|
||||
fs.writeFileSync(cfgPath2, `${JSON.stringify(cfgJson, null, 2)}\n`);
|
||||
await dp.goto('http://localhost:8894/?source=static&view=table', { waitUntil: 'domcontentloaded' });
|
||||
await dp.waitForSelector('table.monitor-table tbody tr[data-status]', { timeout: 20000 });
|
||||
await dp.waitForTimeout(900);
|
||||
const heights = await dp.evaluate(() => {
|
||||
const hs = [...document.querySelectorAll('table.monitor-table tbody tr[data-status]')].map((tr) => Math.round(tr.getBoundingClientRect().height));
|
||||
return { hs, unique: [...new Set(hs)] };
|
||||
});
|
||||
check('内容等长时表格各行等高(可用率条不再撑高行)', heights.unique.length === 1, `行高 ${heights.unique.join('/')}(共 ${heights.hs.length} 行)`);
|
||||
|
||||
// ---- 极端内容:表格不得把数据截断,同时不得出现横向滚动 ----
|
||||
let injected = 0;
|
||||
for (const id of ids.slice(0, 2)) {
|
||||
snap.monitors[id].name = `这是一个相当长的监控目标名称用于验证表格完整显示-${id}`;
|
||||
injected++;
|
||||
const m = cfgJson.monitors.find((x) => x.id === id);
|
||||
if (m) { m.name = `这是一个相当长的监控目标名称用于验证表格完整显示-${id}`; injected++; }
|
||||
}
|
||||
fs.writeFileSync(cfgPath2, `${JSON.stringify(cfgJson, null, 2)}\n`);
|
||||
// 注入事件,验证事件区块的时间线样式与折叠
|
||||
snap.events = Array.from({ length: 12 }, (_, i) => ({
|
||||
t: new Date(Date.now() - i * 60000).toISOString(),
|
||||
@@ -337,26 +353,38 @@ async function main() {
|
||||
check(`长名称/长地址不再被截断(注入 ${injected} 个)`, clip.clipped === 0, `截断单元格 ${clip.clipped} 个,最大 ${clip.worst}px`);
|
||||
check('表格仍无横向滚动', clip.pageOverflow <= 0 && clip.tableOverflow <= 0, `页面 ${clip.pageOverflow} / 表格 ${clip.tableOverflow}`);
|
||||
const evr = await dp.evaluate(() => {
|
||||
const ev = document.querySelector('.ev');
|
||||
const cs = ev ? getComputedStyle(ev) : null;
|
||||
const dot = ev ? getComputedStyle(ev, '::before') : null;
|
||||
const table = document.querySelector('.events-table');
|
||||
const evRow = table?.querySelector('tbody tr');
|
||||
const cs = evRow ? getComputedStyle(evRow) : null;
|
||||
const more = document.getElementById('events-more');
|
||||
return {
|
||||
shown: document.querySelectorAll('.ev').length,
|
||||
grid: cs?.display ?? 'none',
|
||||
dotVisible: dot ? dot.width !== '0px' && dot.backgroundColor !== 'rgba(0, 0, 0, 0)' : false,
|
||||
leftBorder: cs?.borderLeftWidth ?? '',
|
||||
shown: (evRow ? table.querySelectorAll('tbody tr').length : 0),
|
||||
tag: table?.tagName ?? 'none',
|
||||
hasThead: Boolean(table?.querySelector('thead th')),
|
||||
headerSameSize: (() => {
|
||||
const mon = document.querySelector('.monitor-table thead th');
|
||||
const ev = table?.querySelector('thead th');
|
||||
return mon && ev ? getComputedStyle(mon).fontSize === getComputedStyle(ev).fontSize : false;
|
||||
})(),
|
||||
rowBg: cs?.backgroundColor ?? '',
|
||||
cellBorder: evRow ? getComputedStyle(evRow.querySelector('td')).borderBottomWidth : '',
|
||||
moreHidden: more?.hidden ?? true,
|
||||
moreText: more?.textContent ?? '',
|
||||
count: document.getElementById('events-count')?.textContent ?? '',
|
||||
};
|
||||
});
|
||||
check('事件区块使用时间线布局(grid + 圆点)', evr.grid === 'grid' && evr.dotVisible === true, `display=${evr.grid} 圆点=${evr.dotVisible}`);
|
||||
check('事件行按类型着色(左描边)', evr.leftBorder === '3px', evr.leftBorder);
|
||||
check('事件区块与监控表格同构(table + thead)', evr.tag === 'TABLE' && evr.hasThead === true && evr.headerSameSize === true, `${evr.tag},表头字号一致=${evr.headerSameSize}`);
|
||||
const evBg = await dp.evaluate(() => {
|
||||
const rows = [...document.querySelectorAll('.events-table tbody tr')];
|
||||
const down = rows.find((r) => r.dataset.status === 'down');
|
||||
const up = rows.find((r) => r.dataset.status === 'up');
|
||||
return { down: down ? getComputedStyle(down).backgroundColor : '无', up: up ? getComputedStyle(up).backgroundColor : '无', downCells: down ? down.querySelectorAll('td').length : 0 };
|
||||
});
|
||||
check('事件行复用表格状态底色(故障行着色、恢复行不染)', /^(color\(|rgba?\(\d)/.test(evBg.down) && evBg.down !== 'rgba(0, 0, 0, 0)' && evBg.up === 'rgba(0, 0, 0, 0)' && evBg.downCells === 4, `故障=${evBg.down.slice(0, 30)} 恢复=${evBg.up} 列数=${evBg.downCells}`);
|
||||
check('事件默认折叠为 8 条并可展开', evr.shown === 8 && evr.moreHidden === false && /展开全部 12 条/.test(evr.moreText), `显示 ${evr.shown} 条,按钮「${evr.moreText}」,计数「${evr.count}」`);
|
||||
await dp.click('#events-more');
|
||||
await dp.waitForTimeout(300);
|
||||
const evExpanded = await dp.evaluate(() => ({ shown: document.querySelectorAll('.ev').length, text: document.getElementById('events-more').textContent }));
|
||||
const evExpanded = await dp.evaluate(() => ({ shown: document.querySelectorAll('.events-table tbody tr').length, text: document.getElementById('events-more').textContent }));
|
||||
check('展开后显示全部事件', evExpanded.shown === 12 && evExpanded.text === '收起', `显示 ${evExpanded.shown} 条`);
|
||||
await dp.close();
|
||||
await new Promise((r) => demoServer.close(r));
|
||||
|
||||