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
+64 -7
View File
@@ -25,6 +25,7 @@
- *接入*:把 UptimeRobot 已有的监控结果当数据源拉进来,与自建探测并列展示(异地视角),状态、延迟、90 天可用率、心率图全部映射自 UR 官方数据;UR 读取失败时保留上次数据,绝不把服务误判为离线
- *输出*:提供 `/api/v2/getMonitors`,字段与状态码编码对齐 UptimeRobot v2,既有脚本/看板可直接消费
- **滥用防护与信息泄露控制**:刷新接口仅内网/回环可触发、按来源冷却(429 + Retry-After)、可整体关闭;支持 `hideTargets` / `redactDetails` 与 `local.monitors.json` 私有配置分层,公开配置里可以只有服务名 —— 详见 [docs/SECURITY.md](docs/SECURITY.md)
- **主题:砂狼白子**(默认启用,可一键切回默认主题):她的官方立绘与头像、青色光环(含"外圈先亮、内圈后亮"的呼吸动画)、天蓝围巾飘带顶栏、倒十字发饰、公路车轮加载态。素材来源与版权说明见 [主题](#主题砂狼白子)
- **三种数据源自动解析**:边缘 API(实时)→ 静态快照 → 浏览器直连,也可用 `?source=` 强制指定
## 快速开始(本地)
@@ -60,9 +61,15 @@ shiroko-monitor/
│ ├── monitors.json # ★ 监控目标配置(改这里)
│ ├── status.json # 探针输出的状态快照(面板读取)
│ └── assets/
│ ├── style.css # 浅色主题 + 暗色自适应
│ ├── app.js # 数据源解析 / 渲染 / 浏览器直连探测
│ └── favicon.svg
│ ├── style.css # 基础样式(浅色 + 暗色自适应)
│ ├── theme-shiroko.css # 砂狼白子主题(仅在 body[data-theme=shiroko] 下生效)
│ ├── shiroko-portrait.webp # 立绘(SchaleDB 素材,见「主题」章节的版权说明)
│ ├── shiroko-icon.webp # 方形头像(同上)
│ ├── favicon-shiroko.png # 白子主题图标(由头像生成)
│ ├── deco-scarf.svg # 自绘:天蓝围巾飘带
│ ├── deco-bicycle.svg # 自绘:公路车
│ ├── app.js # 数据源解析 / 渲染 / 浏览器直连探测 / 主题切换
│ └── favicon.svg # 默认主题图标
├── prober/node/ # Node 探针(本地 / CI 用)
│ ├── index.js # 巡检循环 + 静态服务器 + /api/* + /api/v2/getMonitors + --once/--demo/--out/--config
│ └── lib/
@@ -98,6 +105,8 @@ shiroko-monitor/
"title": "Shiroko Monitor", // 面板标题
"description": "轻量级服务监控面板",
"view": "table", // 默认视图:table | cards
"theme": "shiroko", // shiroko(砂狼白子)| default
"artwork": "", // 可选:立绘图片路径(相对 public/),如 assets/character.png
"dataFilter": "both", // both | config | uptimerobot(详见「面板形态与数据源」)
"refreshInterval": 30, // 面板检查数据的最大间隔(秒,最小 5);实际对齐服务端更新节奏
"dataSource": "auto", // auto | api | static | browser
@@ -284,6 +293,45 @@ UptimeRobot 的探测来自其多个节点,适合作为**异地视角**与自
效果:无论你什么时候打开页面,倒计时都显示「距服务端下一次更新还有多久」——刚更新完打开是 300s,过了 4 分钟打开就只剩 60s。静态快照与浏览器直连模式没有可对齐的服务端节奏,仍按 `refreshInterval` 轮询。
## 主题(砂狼白子)
面板名称叫 shiroko-monitor,主题自然取了《蔚蓝档案》的**砂狼白子**:她的光环、围巾、狼耳与配色都被做进了界面。
### 主题里的元素
| 位置 | 元素 | 取自设定 |
| --- | --- | --- |
| 左上 logo / 浏览器图标 | **她的头像**(真实素材)+ 青色光环环(呼吸式外扩动画) | 头像为白子的官方方形头像素材 |
| 右下角 | **她的官方立绘**(透明底,约 56vh,顶部渐隐;宽屏下表格会自动让位,不遮挡数据) | 白子原版(阿拜多斯校服)立绘 |
| 顶栏底边 | **天蓝色围巾飘带**:波浪形状 + 5 处褶皱纹路(自绘) | 天蓝色围巾是星野送给她的信物 |
| 背景 | 天蓝渐变 + 斜向细排线 + 右下**光环水印**(自绘:倾斜椭圆环 + 四方位带圆头的短杆) | 白子的光环是倾斜的椭圆环,四个方位各有一条带圆头的短杆,整体形似全息瞄准镜准星 |
| 状态点 | **迷你光环**(外圈 + 中心点,在线时缓慢脉冲外扩) | 光环是基沃托斯学生的标志 |
| 分组标题 | **青色倒十字发饰**(旋转 45°,自绘) | 她左侧刘海上别的青色十字发夹 |
| 刷新按钮加载态 | **公路车轮**(轮圈 + 辐条 + 花鼓)旋转 | 她最爱的公路车(Bianchi ARIA 原型) |
| 左下角 | 公路车侧影(自绘) | 同上 |
| 配色 | 天蓝 / 青 + 藏蓝 + 灰白;「正常」色改为青绿(`#12b0a2`) | 阿拜多斯藏蓝校服 + 天蓝围巾 + 灰白发色 |
### 素材来源与版权
| 类别 | 内容 | 来源 |
| --- | --- | --- |
| **第三方素材**(真实立绘/头像) | `public/assets/shiroko-portrait.webp`(540×1024 原图缩至 460 宽)、`shiroko-icon.webp`(120×120)、`favicon-shiroko.png`(64×64) | [SchaleDB](https://schaledb.com)(社区数据库):`images/student/portrait/10010.webp`、`images/student/icon/10010.webp`;学生 Id `10010` = 砂狼白子(校服原版) |
| 设定资料(光环形状、发饰、围巾、配色、车型) | 设计依据 | [萌娘百科 · 砂狼白子](https://mzh.moegirl.org.cn/%E7%A0%82%E7%8B%BC%E7%99%BD%E5%AD%90)、[Blue Archive Wiki](https://bluearchive.fandom.com/zh/wiki/%E7%A0%82%E7%8B%BC%E7%99%BD%E5%AD%90)、[百度百科](https://baike.baidu.com/item/%E7%A0%82%E7%8B%BC%E7%99%BD%E5%AD%90/62839164) |
| 自绘装饰(SVG/CSS) | 围巾飘带 `deco-scarf.svg`、公路车 `deco-bicycle.svg`、光环水印、迷你光环状态点、倒十字图标、车轮加载图标 | 本仓库原创,按上述公开设定还原 |
> ⚠️ **版权提示**:立绘与头像素材版权归 **NEXON Games / Yostar(《蔚蓝档案》)** 所有,此处仅作个人自用面板的风格美化。如果要把本仓库或 GitHub Pages **公开发布**,建议删除 `public/assets/shiroko-portrait.webp`、`shiroko-icon.webp`、`favicon-shiroko.png` 并在配置里清空 `artwork`,或替换为你自己的图片(其余装饰均为自绘,可保留)。
### 切换与配置
```jsonc
"theme": "shiroko", // shiroko | default(默认主题);也可用地址栏 ?theme=default 临时覆盖
"artwork": "assets/shiroko-portrait.webp" // 立绘图片路径(相对 public/),留空则不显示
```
优先级:`?theme=` → 浏览器上次选择(localStorage)→ `settings.theme`。顶栏右侧的光环图标按钮可一键切换。切回默认主题后,立绘、光环、斜纹、围巾飘带、公路车全部消失,回到最初的简洁样式。
**换成你自己的图**:把图片放进 `public/assets/`,再把 `artwork` 指向它即可(透明底 PNG/WebP 效果最好)。文件不存在时静默不显示,不会出现破图。
## 90 天可用率
探针每完成一次探测,就把它计入「当日分桶」:`monitors.<id>.daily["YYYY-MM-DD"] = { total, up }`,只保留最近 90 天(超出自动淘汰)。面板据此渲染 90 格色条与百分比。
@@ -388,6 +436,13 @@ UptimeRobot 的探测来自其多个节点,适合作为**异地视角**与自
- [x] 页脚改为展示「数据更新于 … + 下次检查 Ns」,tooltip 内含服务端更新节奏与快照生成时间
- [x] `scripts/verify.cjs` 适配纯外部数据源部署(自动跳过不适用的探测类断言),并新增「倒计时对齐服务端」断言
### v0.7.0 已实现
- [x] **砂狼白子主题**(默认启用):官方立绘(右下角,宽屏自动为它让位)+ 头像 logo/favicon(光环环呼吸动画)+ 天蓝围巾飘带顶栏 + 倒十字发饰分组标识 + 公路车轮加载态 + 按官方造型还原的倾斜椭圆光环水印;配套青绿主色调
- [x] 主题切换:`settings.theme` / `?theme=` / 顶栏光环按钮(localStorage 记忆),默认主题完整保留
- [x] `settings.artwork` 立绘位,默认指向内置立绘;未配置或文件缺失时静默不显示
- [x] 修复:`dataFilter=uptimerobot`(只展示外部数据源)时「平均延迟」恒为「–」——原规则一律排除 UR 延迟,改为「混合来源时排除、纯外部来源时以其为唯一口径」,服务端与面板同步调整
### 路线图(后续迭代)
- [ ] 通知渠道:Webhook / Server 酱 / Telegram / Bark / 邮件
@@ -423,13 +478,15 @@ UPTIMEROBOT_API_KEY=ur... node scripts/verify-uptimerobot.cjs # UR 映
## 界面截图
| 表格(默认视图) | 卡片视图 |
| 砂狼白子主题(默认,表格视图) | 默认主题 |
| --- | --- |
| ![table](docs/screenshot-desktop.png) | ![cards](docs/screenshot-view-cards.png) |
| ![shiroko](docs/screenshot-desktop.png) | ![default](docs/screenshot-theme-default.png) |
UptimeRobot 数据源(`?filter=uptimerobot`,截图中含真实监控名与域名,公开发布前可自行删除):`docs/screenshot-uptimerobot.png`
| 卡片视图(白子主题) | 移动端 |
| --- | --- |
| ![cards](docs/screenshot-view-cards.png) | ![mobile](docs/screenshot-mobile.png) |
演示数据(90 天满档)与移动端:`docs/screenshot-demo.png`、`docs/screenshot-mobile.png`
UptimeRobot 数据源(`?filter=uptimerobot`,截图中含真实监控名与域名,公开发布前可自行删除):`docs/screenshot-uptimerobot.png`;演示数据 90 天满档:`docs/screenshot-demo.png`
## 致谢与定位
Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 371 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 321 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 295 KiB

+1 -1
View File
@@ -11,7 +11,7 @@
import { connect } from 'cloudflare:sockets';
const VERSION = '0.6.0';
const VERSION = '0.7.0';
const HISTORY_LIMIT = 48;
const UPTIME_DAYS = 90;
const USER_AGENT = 'shiroko-monitor/0.3 (lightweight uptime panel)';
+5 -3
View File
@@ -35,7 +35,7 @@ const CONCURRENCY = 8;
const WRITE_DEBOUNCE_MS = 300;
const DEFAULT_INTERVAL_SEC = 60;
const PORT = Number(process.env.PORT || 8787);
const VERSION = '0.6.0';
const VERSION = '0.7.0';
const UR_DEFAULT_POLL_SEC = 300;
const UR_MIN_REFRESH_GAP_MS = 60000;
@@ -348,6 +348,9 @@ function snapshot(runtime, demo = false) {
const hideUrls = config.settings?.hideTargets === true;
const includeConfig = filter !== 'uptimerobot';
const includeExternal = filter !== 'config';
// 混合来源时平均延迟只统计自建探测(UR 的延迟来自其探测节点,与本机视角不可比);
// 若展示集合只有外部数据源,则 UR 延迟就是唯一口径,照常统计
const latencyEligible = (isUr) => !isUr || filter === 'uptimerobot';
const monitors = {};
let up = 0;
let down = 0;
@@ -376,8 +379,7 @@ function snapshot(runtime, demo = false) {
}
if (r.status === 'up') {
up++;
// 平均延迟只统计自建探针:UR 的延迟来自其探测节点,与本机视角不可比
if (!isUr && r.latency != null) {
if (latencyEligible(isUr) && r.latency != null) {
latencySum += r.latency;
latencyCount++;
}
+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();
+35
View File
@@ -0,0 +1,35 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 260 150" fill="none">
<title>公路车(简笔画 · 自绘,白子的爱车意象)</title>
<g stroke="#2ea8dc" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
<!-- 车轮 -->
<circle cx="58" cy="96" r="42"/>
<circle cx="58" cy="96" r="4" fill="#2ea8dc" stroke="none"/>
<circle cx="202" cy="96" r="42"/>
<circle cx="202" cy="96" r="4" fill="#2ea8dc" stroke="none"/>
</g>
<g stroke="#8fd3f2" stroke-width="1.6" stroke-linecap="round">
<!-- 辐条 -->
<line x1="58" y1="56" x2="58" y2="136"/><line x1="18" y1="96" x2="98" y2="96"/>
<line x1="30" y1="68" x2="86" y2="124"/><line x1="86" y1="68" x2="30" y2="124"/>
<line x1="202" y1="56" x2="202" y2="136"/><line x1="162" y1="96" x2="242" y2="96"/>
<line x1="174" y1="68" x2="230" y2="124"/><line x1="230" y1="68" x2="174" y2="124"/>
</g>
<g stroke="#1c2f4d" stroke-width="4" stroke-linecap="round" stroke-linejoin="round">
<!-- 车架 -->
<path d="M58 96 L110 96 L142 52 L202 96"/>
<path d="M110 96 L142 52"/>
<path d="M110 96 L146 100"/>
<!-- 前叉 + 车把 -->
<path d="M202 96 L176 46"/>
<path d="M164 40 L190 46"/>
<!-- 座管 + 车座 -->
<path d="M142 52 L130 34"/>
<path d="M118 32 L142 34"/>
<!-- 曲柄 + 脚踏 -->
<circle cx="146" cy="100" r="10"/>
<path d="M146 100 L162 116"/>
</g>
<g stroke="#2ea8dc" stroke-width="3" stroke-linecap="round">
<path d="M96 100 L104 100" opacity="0.9"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+21
View File
@@ -0,0 +1,21 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 60" fill="none" preserveAspectRatio="none">
<title>天蓝色围巾飘带(自绘)</title>
<defs>
<linearGradient id="sg" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#a9e9ff"/>
<stop offset="0.45" stop-color="#2ea8dc"/>
<stop offset="1" stop-color="#1c2f4d"/>
</linearGradient>
</defs>
<!-- 波浪形的围巾:上边缘平直贴合顶栏,下边缘起波 -->
<path d="M0 6 H600 V22 C560 38 520 12 480 26 C440 40 400 14 360 28 C320 42 280 12 240 26 C200 40 160 14 120 28 C80 42 40 16 0 30 Z"
fill="url(#sg)"/>
<!-- 围巾的褶皱纹路 -->
<g stroke="#ffffff" stroke-opacity="0.35" stroke-width="2" stroke-linecap="round">
<path d="M60 16 C58 22 58 26 60 30"/>
<path d="M180 16 C178 22 178 26 180 30"/>
<path d="M300 16 C298 22 298 26 300 30"/>
<path d="M420 16 C418 22 418 26 420 30"/>
<path d="M540 16 C538 22 538 26 540 30"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 987 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

+28
View File
@@ -110,6 +110,8 @@ body {
.stat-down b { color: var(--down); }
#refresh-btn {
display: inline-flex;
align-items: center;
padding: 6px 14px;
border: 1px solid var(--border);
border-radius: 8px;
@@ -123,6 +125,32 @@ body {
#refresh-btn:hover { background: var(--bg); }
#refresh-btn:disabled { opacity: 0.6; cursor: default; }
/* 加载态用的车轮图标(砂狼白子骑行意象) */
.wheel { width: 14px; height: 14px; margin-right: 6px; animation: spin 0.9s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
/* 图标按钮(主题切换) */
.icon-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
padding: 0;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--surface);
color: var(--text-2);
cursor: pointer;
transition: color 0.15s, border-color 0.15s, box-shadow 0.15s;
}
.icon-btn svg { width: 16px; height: 16px; }
.icon-btn:hover { color: var(--accent); border-color: var(--accent); }
.icon-btn.active { color: var(--accent); border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 12%, transparent); }
/* 光环仅在白子主题下显示 */
.logo .halo { display: none; }
/* ---------- 视图切换 ---------- */
.view-switch { display: flex; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
.view-btn {
+231
View File
@@ -0,0 +1,231 @@
/* ============================================================
砂狼白子主题(shiroko)
真实素材(头像 / 立绘)来自社区数据库 SchaleDB,版权归 NEXON Games · Yostar;
其余装饰为自绘几何图形,均按其公开设定还原:
· 光环:倾斜的椭圆环 + 四方位带圆头的短杆(全息瞄准镜准星意象)
· 发饰:青色倒十字(旋转 45°)
· 围巾:天蓝色飘带(星野的信物)
· 座驾:公路车
仅在 body[data-theme="shiroko"] 下生效,不影响默认主题。
============================================================ */
body[data-theme="shiroko"] {
--bg: #eef7fd;
--surface: #ffffff;
--border: #d3e6f5;
--text: #17253d;
--text-2: #5d7b9a;
--up: #12b0a2;
--up-bg: #e4faf6;
--down: #e0524e;
--down-bg: #fdecec;
--pending: #d99413;
--pending-bg: #fff7e6;
--accent: #1e9fd4;
--uptime-ok: #12b0a2;
--uptime-warn: #e5a52c;
--uptime-bad: #e0524e;
--uptime-none: #dbe8f3;
--shadow: 0 1px 2px rgb(23 60 100 / 0.05), 0 10px 26px -20px rgb(23 60 100 / 0.35);
--sh-sky: #8fe0ff;
--sh-sky-deep: #2ea8dc;
--sh-navy: #1c2f4d;
}
@media (prefers-color-scheme: dark) {
body[data-theme="shiroko"] {
--bg: #0a1622;
--surface: #101d2c;
--border: #1d3247;
--text: #e4eef7;
--text-2: #8ba6bf;
--up: #2fd4c2;
--up-bg: #0d2a2a;
--down: #f4736c;
--down-bg: #2c1518;
--pending: #e8b344;
--pending-bg: #2a2113;
--accent: #55c2ef;
--uptime-ok: #2fd4c2;
--uptime-none: #1a2b3d;
--shadow: none;
}
}
/* ---------- 背景:天蓝渐变 + 淡斜纹 ---------- */
body[data-theme="shiroko"] {
background-color: var(--bg);
background-image:
repeating-linear-gradient(135deg, rgb(46 168 220 / 4%) 0 1px, transparent 1px 9px),
radial-gradient(120% 60% at 50% -20%, rgb(143 224 255 / 26%), transparent 70%),
linear-gradient(180deg, rgb(255 255 255 / 70%), transparent 320px);
background-attachment: fixed;
}
/* 右下角:按官方造型还原的光环水印(倾斜椭圆环 + 四方位圆头短杆) */
body[data-theme="shiroko"]::after {
content: "";
position: fixed;
right: -70px;
bottom: 40px;
width: 460px;
height: 260px;
pointer-events: none;
z-index: 0;
opacity: 0.55;
background-repeat: no-repeat;
background-size: contain;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 240 130'><g stroke='%232ea8dc' stroke-opacity='0.11' fill='none' stroke-linecap='round'><ellipse cx='120' cy='64' rx='86' ry='38' stroke-width='11'/><ellipse cx='120' cy='70' rx='48' ry='21' stroke-width='6'/><g stroke-width='7'><line x1='22' y1='64' x2='6' y2='64'/><line x1='218' y1='64' x2='234' y2='64'/><line x1='120' y1='18' x2='120' y2='2'/><line x1='120' y1='110' x2='120' y2='126'/></g></g><g fill='%232ea8dc' fill-opacity='0.11'><circle cx='6' cy='64' r='4.6'/><circle cx='234' cy='64' r='4.6'/><circle cx='120' cy='2' r='4.6'/><circle cx='120' cy='126' r='4.6'/></g></svg>");
}
/* 左下角:公路车(自绘) */
body[data-theme="shiroko"]::before {
content: "";
position: fixed;
left: 14px;
bottom: 10px;
width: 156px;
height: 90px;
pointer-events: none;
z-index: 0;
opacity: 0.5;
background: url("deco-bicycle.svg") no-repeat center / contain;
}
/* ---------- 顶栏:天蓝到白 + 底部围巾飘带 ---------- */
body[data-theme="shiroko"] .topbar {
background: linear-gradient(180deg, #ffffff 0%, #f6fcff 100%);
border-bottom: 0;
box-shadow: var(--shadow);
overflow: hidden;
}
body[data-theme="shiroko"] .topbar::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 12px;
background: url("deco-scarf.svg") repeat-x bottom left / 300px 12px;
pointer-events: none;
}
/* ---------- 品牌:真实头像 + 光环环 ---------- */
body[data-theme="shiroko"] .logo {
width: 34px;
height: 34px;
border-radius: 50%;
background: #eaf7ff url("shiroko-icon.webp") center / cover no-repeat;
box-shadow: 0 0 0 2px #fff, 0 0 0 4px var(--sh-sky-deep), 0 0 12px rgb(46 168 220 / 55%);
animation: avatar-halo 3.4s ease-out infinite;
}
@keyframes avatar-halo {
0% { box-shadow: 0 0 0 2px #fff, 0 0 0 4px var(--sh-sky-deep), 0 0 12px rgb(46 168 220 / 55%); }
65% { box-shadow: 0 0 0 2px #fff, 0 0 0 8px rgb(46 168 220 / 0%), 0 0 16px rgb(46 168 220 / 30%); }
100% { box-shadow: 0 0 0 2px #fff, 0 0 0 4px var(--sh-sky-deep), 0 0 12px rgb(46 168 220 / 55%); }
}
body[data-theme="shiroko"] .brand-text h1 {
color: var(--sh-navy);
letter-spacing: 0.04em;
}
/* 分组标题前的青色倒十字发饰(旋转 45°) */
body[data-theme="shiroko"] .group-title::before,
body[data-theme="shiroko"] .group-row td::before {
content: "";
display: inline-block;
width: 11px;
height: 11px;
margin-right: 7px;
vertical-align: middle;
background-repeat: no-repeat;
background-size: contain;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'><g transform='rotate(45 10 10)' fill='%232ea8dc'><rect x='8.5' y='0.5' width='3' height='19' rx='1.4'/><rect x='0.5' y='8.5' width='19' height='3' rx='1.4'/></g><circle cx='10' cy='10' r='2.6' fill='%23bfeaff'/></svg>");
}
/* ---------- 状态点 → 迷你光环 ---------- */
body[data-theme="shiroko"] [data-status="up"] { --sh-state: var(--up); }
body[data-theme="shiroko"] [data-status="down"] { --sh-state: var(--down); }
body[data-theme="shiroko"] [data-status="pending"] { --sh-state: var(--pending); }
body[data-theme="shiroko"] .dot {
width: 12px;
height: 12px;
background: transparent;
border: 2px solid var(--sh-state, var(--pending));
border-radius: 50%;
position: relative;
box-shadow: 0 0 7px -1px var(--sh-state, var(--pending));
}
body[data-theme="shiroko"] .dot::after {
content: "";
position: absolute;
inset: 2.5px;
border-radius: 50%;
background: var(--sh-state, var(--pending));
}
body[data-theme="shiroko"] .card[data-status="up"] .dot { animation: halo-pulse 2.6s ease-out infinite; }
@keyframes halo-pulse {
0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--up) 45%, transparent); }
70% { box-shadow: 0 0 0 7px color-mix(in srgb, var(--up) 0%, transparent); }
100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--up) 0%, transparent); }
}
/* ---------- 卡片 / 表格 / 交互 ---------- */
body[data-theme="shiroko"] .card {
border-left-width: 3px;
background: linear-gradient(180deg, #ffffff, #fbfdff);
}
body[data-theme="shiroko"] .tab.active,
body[data-theme="shiroko"] .view-btn.active {
background: linear-gradient(135deg, var(--sh-sky-deep), #1b7fb4);
box-shadow: 0 3px 10px -4px rgb(46 168 220 / 70%);
}
body[data-theme="shiroko"] #refresh-btn:hover { border-color: var(--accent); color: var(--accent); }
body[data-theme="shiroko"] .monitor-table th {
background: linear-gradient(180deg, #f7fcff, #eef7fd);
color: var(--sh-navy);
border-bottom: 1px solid var(--border);
}
body[data-theme="shiroko"] .monitor-table tbody tr:hover { background: rgb(46 168 220 / 6%); }
body[data-theme="shiroko"] .group-row td {
background: linear-gradient(90deg, #eaf6fd, #f7fcff);
border-left: 3px solid var(--sh-sky-deep);
color: var(--sh-navy);
}
body[data-theme="shiroko"] .table-wrap { box-shadow: var(--shadow); }
body[data-theme="shiroko"] .uptime-cell.lv-none {
background-image: repeating-linear-gradient(135deg, rgb(46 168 220 / 16%) 0 1px, transparent 1px 3px);
background-color: var(--uptime-none);
}
/* ---------- 刷新按钮的公路车轮加载态 ---------- */
body[data-theme="shiroko"] #refresh-btn .wheel { animation: wheel-spin 0.9s linear infinite; }
@keyframes wheel-spin { to { transform: rotate(360deg); } }
/* ---------- 角色立绘(settings.artwork,默认指向内置立绘) ---------- */
body[data-theme="shiroko"] .artwork {
position: fixed;
right: 8px;
bottom: 0;
width: auto;
height: min(56vh, 520px);
z-index: 0;
pointer-events: none;
user-select: none;
filter: drop-shadow(0 8px 24px rgb(23 60 100 / 18%));
mask-image: linear-gradient(180deg, transparent 0, #000 14%, #000 100%);
-webkit-mask-image: linear-gradient(180deg, transparent 0, #000 14%, #000 100%);
}
@media (max-width: 900px) {
body[data-theme="shiroko"] .artwork { display: none; }
}
/* 宽屏时为立绘留出空间,避免遮住表格右侧的可用率列 */
@media (min-width: 1100px) {
body[data-theme="shiroko"] .grid,
body[data-theme="shiroko"] .footer { padding-right: 240px; }
}
/* 内容层置于装饰之上 */
body[data-theme="shiroko"] .grid,
body[data-theme="shiroko"] .footer,
body[data-theme="shiroko"] .group-tabs { position: relative; z-index: 1; }
+12 -1
View File
@@ -7,6 +7,7 @@
<title>Shiroko Monitor</title>
<link rel="icon" type="image/svg+xml" href="assets/favicon.svg">
<link rel="stylesheet" href="assets/style.css">
<link rel="stylesheet" href="assets/theme-shiroko.css">
</head>
<body>
<header class="topbar">
@@ -25,6 +26,16 @@
<div class="stat"><b id="stat-latency">–</b><label>平均延迟</label></div>
<div class="stat"><b id="stat-uptime">–</b><label>90天可用率</label></div>
<div class="view-switch" id="view-switch" role="group" aria-label="视图切换"></div>
<button id="theme-btn" type="button" class="icon-btn" title="切换主题">
<svg viewBox="0 0 100 100" aria-hidden="true">
<circle cx="50" cy="50" r="31" fill="none" stroke="currentColor" stroke-width="8"/>
<circle cx="50" cy="50" r="15" fill="none" stroke="currentColor" stroke-width="4" opacity="0.7"/>
<polygon points="46.4,4 53.6,4 51.2,15 48.8,15" fill="currentColor"/>
<polygon points="46.4,96 53.6,96 51.2,85 48.8,85" fill="currentColor"/>
<polygon points="4,46.4 4,53.6 15,51.2 15,48.8" fill="currentColor"/>
<polygon points="96,46.4 96,53.6 85,51.2 85,48.8" fill="currentColor"/>
</svg>
</button>
<button id="refresh-btn" type="button">刷新</button>
</div>
</header>
@@ -41,7 +52,7 @@
<span id="ur-chip" class="footer-item warn-chip" hidden>UptimeRobot 同步异常</span>
<span id="updated-at" class="footer-item">–</span>
<span id="countdown" class="footer-item">–</span>
<span class="footer-item version">shiroko-monitor v0.6.0</span>
<span class="footer-item version">shiroko-monitor v0.7.0</span>
</footer>
<script type="module" src="assets/app.js"></script>
+2
View File
@@ -3,6 +3,8 @@
"title": "Shiroko Monitor",
"description": "轻量级服务监控面板",
"view": "table",
"theme": "shiroko",
"artwork": "assets/shiroko-portrait.webp",
"dataFilter": "uptimerobot",
"refreshInterval": 300,
"dataSource": "auto",
+582 -567
View File
File diff suppressed because it is too large Load Diff