feat(theme): 砂狼白子主题 —— 真实立绘/头像 + 自绘装饰

素材(第三方,仅个人自用):
- 立绘/头像来自 SchaleDB(schaledb.com),学生 Id 10010 = 砂狼白子
- 版权归 NEXON Games / Yostar(蔚蓝档案),来源与公开部署提示见 README「主题」章节

自绘装饰(SVG/CSS,按公开设定还原):
- 天蓝围巾飘带(顶栏底边)、公路车(左下角 + 加载车轮)
- 光环水印:倾斜椭圆环 + 四方位带圆头短杆(对齐官方造型,此前误画为正圆+三角)
- 倒十字发饰分组标识、迷你光环状态点

其他:主题切换(settings.theme / ?theme= / 顶栏按钮)、立绘位 settings.artwork
修复:dataFilter=uptimerobot 时平均延迟恒为「–」(改为纯外部来源时以其为唯一口径)
This commit is contained in:
tamakyi
2026-10-13 19:29:07 +08:00
parent 3db3ce570a
commit d88ba56156
21 changed files with 1037 additions and 583 deletions
+56 -4
View File
@@ -1,4 +1,4 @@
const VERSION = '0.6.0';
const VERSION = '0.7.0';
const HB_LIMIT = 48;
const UPTIME_DAYS = 90;
const BROWSER_CONCURRENCY = 6;
@@ -15,6 +15,7 @@ const state = {
source: null,
filter: 'all',
view: 'cards',
theme: 'default',
refreshTimer: null,
nextRefreshAt: 0,
loading: false,
@@ -537,6 +538,8 @@ function localStats() {
let latCount = 0;
let uptimeUp = 0;
let uptimeTotal = 0;
// 与探针口径一致:混合来源时平均延迟只统计自建探测;若展示集合只有外部数据源,则以其延迟为唯一口径
const hasProbe = monitors.some((m) => !isExternal(m));
for (const m of monitors) {
const st = state.status?.monitors?.[m.id];
if (!st || st.status === 'pending') {
@@ -545,8 +548,7 @@ function localStats() {
}
if (st.status === 'up') up++;
else down++;
// 与探针口径一致:平均延迟只统计自建探测目标
if (!isExternal(m) && st.latency != null) {
if (st.latency != null && (!isExternal(m) || !hasProbe)) {
latSum += st.latency;
latCount++;
}
@@ -666,7 +668,10 @@ function setLoading(on, text = '刷新中…') {
state.loading = on;
const btn = $('refresh-btn');
btn.disabled = on;
btn.textContent = on ? text : '刷新';
// 车轮图标(砂狼白子骑行意象,主题里会转起来)
btn.innerHTML = on
? `<svg class="wheel" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2" opacity="0.35"/><g stroke="currentColor" stroke-width="1.3"><line x1="12" y1="2" x2="12" y2="22"/><line x1="2" y1="12" x2="22" y2="12"/><line x1="4.9" y1="4.9" x2="19.1" y2="19.1"/><line x1="19.1" y1="4.9" x2="4.9" y2="19.1"/></g><circle cx="12" cy="12" r="2.2" fill="currentColor"/></svg><span>${escapeHtml(text)}</span>`
: '刷新';
}
async function refresh({ force = false } = {}) {
@@ -748,9 +753,55 @@ function startAutoRefresh() {
}, 1000);
}
/* ---------- 主题 ---------- */
const THEMES = { default: '默认', shiroko: '砂狼白子' };
function resolveTheme() {
const q = new URLSearchParams(location.search).get('theme');
let saved = null;
try {
saved = localStorage.getItem('shiroko.theme');
} catch { /* 隐私模式 */ }
return [q, saved, state.config?.settings?.theme].find((t) => t && THEMES[t]) ?? 'default';
}
// 角色立绘位:主题为白子且配置了 artwork 时才插入;文件不存在则静默移除(不留破图)
function applyArtwork() {
document.querySelector('.artwork')?.remove();
const src = state.config?.settings?.artwork;
if (state.theme !== 'shiroko' || !src) return;
const img = document.createElement('img');
img.className = 'artwork';
img.alt = '';
img.src = src;
img.addEventListener('error', () => img.remove());
document.body.appendChild(img);
}
function applyTheme(theme, { persist = true } = {}) {
state.theme = THEMES[theme] ? theme : 'default';
if (state.theme === 'default') delete document.body.dataset.theme;
else document.body.dataset.theme = state.theme;
if (persist) {
try {
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');
applyArtwork();
const btn = $('theme-btn');
if (btn) {
btn.classList.toggle('active', state.theme === 'shiroko');
btn.title = state.theme === 'shiroko' ? '当前主题:砂狼白子(点击切回默认)' : '当前主题:默认(点击切换到砂狼白子主题)';
}
}
function bindEvents() {
// 只有手动点「刷新」才会触发一次重新检测;自动刷新是只读的
$('refresh-btn').addEventListener('click', () => refresh({ force: true }));
$('theme-btn')?.addEventListener('click', () => applyTheme(state.theme === 'shiroko' ? 'default' : 'shiroko'));
}
/* ---------- 启动 ---------- */
@@ -767,6 +818,7 @@ async function init() {
$('site-desc').textContent = state.config.settings?.description || '';
const versionEl = document.querySelector('.version');
if (versionEl) versionEl.textContent = `shiroko-monitor v${VERSION}`;
applyTheme(resolveTheme());
await resolveSource();
state.view = resolveView();
bindEvents();