chore(spec): 归档 blog-5n——八任务收敛(后台深度优化全家桶)

Spec: 2026-11-05_blog-5n-admin-deep-optimization

Co-authored-by: GLM-5.3
This commit is contained in:
wait4xandGLM-5.3 committed 2026-11-05 20:13:09 +08:00
1 parent a555a3e450
commit 5c0e6ed900
29 files changed
+1559 -512

No files matched your search

+161
View File
@@ -0,0 +1,161 @@
/**
* 后台部署状态条(5n B1):Sveltia 管理页右下角浮层,轮询站点构建状态。
* spec:.spec/specs/2026-11-05_blog-5n-admin-deep-optimization(T4)
*
* JSON 接口契约(build.sh 每次部署写 /admin/build-status.json):
* { "sha": "a555a3e…", // 部署 commit(展示取前 7 位)
* "time": "2026-11-05T19:20:25+08:00", // 构建完成时刻(展示取 HH:MM)
* "state": "ok" | "building" | "fail",
* "pages": 53 } // 页面数(仪表盘消费,本条不读)
* 文件缺失 / 响应非 200 / JSON 解析失败 / state 非法 → 视为「不可知」:
* 胶囊淡出、不报错,下一轮照常再试——状态条是增益件,接口坏了不能吵主人。
*
* 为什么轮询而不是 SSE/WebSocket:本站是纯静态托管(CDN 上没有可驻留的
* 服务端进程,无推送通道);「保存→看部署结果」的节奏里 10s 间隔已足够
* 感知,且零服务端成本、零额外基础设施。
*/
(function () {
'use strict';
if (window.DEPLOY_STATUS) return; // 防重复初始化:脚本被引入两次时静默退出
var POLL_MS = 10000;
var timer = null; // null = 停表中(初始 / 页面隐藏)
var STATES = {
ok: { bg: 'rgba(28,30,34,.92)', label: '已上线' },
building: { bg: 'rgba(52,84,209,.92)', label: '构建中…' },
fail: { bg: 'rgba(179,32,32,.92)', label: '构建失败' },
};
/** 构建中呼吸动画的 keyframes 无法用元素内联样式表达,注入一次 <style>(仍零外部资源) */
function injectKeyframes() {
var style = document.createElement('style');
style.id = 'deploy-status-style';
style.textContent = '@keyframes deploy-status-breath{0%,100%{opacity:.7}50%{opacity:1}}';
document.head.appendChild(style);
return style;
}
function createPill() {
var el = document.createElement('div');
el.setAttribute('role', 'status'); // 状态变化对读屏器温和播报
// 全部样式内联在 JS 里:不依赖外部 CSS,单文件即可被任意页面引入
el.style.cssText = [
'position:fixed',
'right:14px',
'bottom:14px',
'z-index:10000',
'padding:6px 12px',
'font:12px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif',
'color:#fff',
'border-radius:999px',
'box-shadow:0 2px 10px rgba(0,0,0,.18)',
'user-select:none',
'opacity:0', // 初始隐身,拿到合法数据才淡入
'transition:opacity .2s,background-color .2s', // 出现与状态切换均为 200ms 过渡
'pointer-events:none', // 默认不挡下层 UI;仅 fail 态(可点击)放开
].join(';');
el.addEventListener('click', function () {
// 只有 fail 态放开 pointer-events,此监听只会在「构建失败」时被触发
location.reload();
});
document.body.appendChild(el);
return el;
}
/** 从 ISO 串直接截 HH:MM 而不走 Date 本地化:展示构建机墙钟时间,不随浏览器时区漂移 */
function hhmm(iso) {
var m = /T(\d{2}:\d{2})/.exec(String(iso || ''));
if (m) return m[1];
var d = new Date(iso); // 兜底:万一 build.sh 改用非 ISO 格式,退本地时区展示
if (isNaN(d.getTime())) return '';
var pad = function (n) { return (n < 10 ? '0' : '') + n; };
return pad(d.getHours()) + ':' + pad(d.getMinutes());
}
function boot() {
var el = createPill();
function hide() {
el.style.opacity = '0';
el.style.pointerEvents = 'none';
el.style.animation = 'none';
el.title = '';
}
function render(data) {
var conf = STATES[data && data.state];
if (!conf) { // 不可知 = 淡出静默,不区分缺文件/坏 JSON/非法枚举(对用户都是同一件事)
hide();
return;
}
if (data.state === 'ok') {
var parts = [];
var sha7 = String(data.sha || '').slice(0, 7);
if (sha7) parts.push(sha7);
var t = hhmm(data.time);
if (t) parts.push(t);
el.textContent = parts.length ? conf.label + ' · ' + parts.join(' · ') : conf.label;
} else {
el.textContent = conf.label;
}
el.style.backgroundColor = conf.bg;
// 呼吸仅限 building;离开时置 none,状态间观感交给 background-color 的 200ms 过渡
el.style.animation = data.state === 'building'
? 'deploy-status-breath 1.6s ease-in-out infinite'
: 'none';
if (data.state === 'fail') {
el.style.pointerEvents = 'auto';
el.style.cursor = 'pointer';
el.title = '点击刷新页面'; // fail 的唯一动作是刷新:取回最新静态产物(含本文件与状态 JSON)
} else {
el.style.pointerEvents = 'none';
el.style.cursor = 'default';
el.title = '';
}
el.style.opacity = '1';
}
function poll() {
fetch('/admin/build-status.json', { cache: 'no-store' }) // no-store:状态文件必须每次拿实况
.then(function (r) { return r.ok ? r.json() : null; }) // 404 等非 200 → 当不可知处理
.then(function (data) { render(data && typeof data === 'object' ? data : {}); })
.catch(function () { hide(); }); // 网络层与 JSON 解析失败同路:淡出,下一轮再试
}
function loop() {
poll();
timer = setTimeout(loop, POLL_MS);
}
document.addEventListener('visibilitychange', function () {
if (document.visibilityState === 'visible') {
// 回来立即拉(隐藏期间可能已构建完成),并重置 10s 节拍
if (timer) clearTimeout(timer);
loop();
} else if (timer) {
// 隐藏即停:省请求,也避免后台标签被浏览器节流的定时器醒来后集中补发
clearTimeout(timer);
timer = null;
}
});
window.DEPLOY_STATUS = {
// 挂载即去重标记;stop 供控制台/测试卸载(页面自身是 SPA,正常导航不会触发)
stop: function () {
if (timer) clearTimeout(timer);
timer = null;
el.remove();
var style = document.getElementById('deploy-status-style');
if (style) style.remove();
delete window.DEPLOY_STATUS;
},
};
if (document.visibilityState === 'visible') loop();
// 初始就隐藏(预渲染标签页):等 visibilitychange 回来再首拉
}
if (document.body) boot();
else document.addEventListener('DOMContentLoaded', boot); // 若被放到 <head> 且未加 defer
})();