Files
hunjing/frontend/design/_theme.js
T
2026-08-21 16:00:13 +08:00

209 lines
7.3 KiB
JavaScript

/* ============================================================
* design/_theme.js — 原型共享脚本层
* ============================================================
* 职责:
* 1. fetch _nav.html → 注入各页 .sidebar-mount
* 2. 按 mount 的 data-active 高亮当前页 nav-item
* 3. logo 点击 → index.html(目录入口)
* 4. #theme-toggle 切换暗/亮,localStorage 持久化
*
* file:// 限制:fetch 在 file:// 协议下被 CORS 拦截,会显示 fallback
* 提示。请用 `pnpm design` 启动本地预览服务器获得完整导航。
*
* 用法:页面末尾 <script src="_theme.js" defer></script>
* 页面 body 内放 <div class="sidebar-mount" data-active="页面名"></div>
* ============================================================ */
(function () {
"use strict";
var ROOT = document.documentElement;
/* iframe 缩略图态:index.html 以 live <iframe> 嵌入各原型页做缩略图;
检测嵌入态加 .in-frame,由 _base.css 把动画瞬间完成,避免 25 个
缩略图同时持续重绘而耗 CPU。 */
if (window.self !== window.top) ROOT.classList.add("in-frame");
function currentTheme() {
return ROOT.dataset.theme === "light" ? "light" : "dark";
}
function applyTheme(t) {
ROOT.dataset.theme = t;
try {
localStorage.setItem("design-theme", t);
} catch (e) {}
document.querySelectorAll("#theme-label").forEach(function (l) {
l.textContent = t === "dark" ? "夜铜" : "素绢";
});
}
function toggleTheme() {
applyTheme(currentTheme() === "dark" ? "light" : "dark");
}
/* 恢复上次主题(减轻刷新闪屏;data-theme 默认 dark 在 <html> 上已设) */
try {
var saved = localStorage.getItem("design-theme");
if (saved === "light" || saved === "dark") ROOT.dataset.theme = saved;
} catch (e) {}
/* 事件委托:logo / 主题按钮(nav 注入前后都生效,index 页独立按钮也覆盖) */
document.addEventListener("click", function (e) {
var t = e.target;
if (!t || !t.closest) return;
if (t.closest("#theme-toggle")) {
e.preventDefault();
toggleTheme();
return;
}
if (t.closest("#sidebar-fold")) {
e.preventDefault();
toggleFold();
return;
}
if (t.closest(".sidebar .logo")) {
e.preventDefault();
onLogoClick();
return;
}
});
/* 折叠侧栏(chat 默认折叠让对话区扩展,其他页默认展开;localStorage 持久化) */
function applyFold(state) {
document.querySelectorAll(".sidebar-wrap").forEach(function (w) {
w.classList.toggle("collapsed", state === "collapsed");
});
}
function toggleFold() {
var wrap = document.querySelector(".sidebar-wrap");
if (!wrap) return;
var collapsed = wrap.classList.toggle("collapsed");
try {
localStorage.setItem(
"design-sidebar",
collapsed ? "collapsed" : "expanded",
);
} catch (e) {}
}
function initFold() {
var mount = document.querySelector(".sidebar-mount");
var isChat = mount && mount.dataset.active === "对话";
var saved = null;
try {
saved = localStorage.getItem("design-sidebar");
} catch (e) {}
applyFold(saved || (isChat ? "collapsed" : "expanded"));
}
/* ── logo:单击即时回目录;350ms 内连点 5 次触发「镜启」彩蛋(不跳转)── */
var EGG_VERSES = [
"镜中有镜,心外无心。",
"汝来看此镜时,此镜亦照汝。",
"一念起,万镜生。",
"镜非鉴物,乃鉴心也。",
"幻由心生,镜随心转。",
"九枢归一,一归于何处?",
"铜镜本无字,照尽往来人。",
"且向镜中看,那个是谁?",
];
var logoClickSeq = 0;
var lastLogoTime = 0;
function showEggToast(msg) {
var toast = document.createElement("div");
toast.textContent = msg;
toast.style.cssText =
"position:fixed;bottom:48px;left:50%;transform:translateX(-50%);padding:12px 28px;border-radius:12px;background:var(--patina-wash);color:var(--patina);font-family:'Songti SC',serif;font-size:14px;border:1px solid var(--patina-dim);z-index:9999;pointer-events:none;opacity:0;transition:opacity 0.4s;";
document.body.appendChild(toast);
requestAnimationFrame(function () {
toast.style.opacity = "1";
});
setTimeout(function () {
toast.style.opacity = "0";
setTimeout(function () {
toast.remove();
}, 400);
}, 2200);
}
function onLogoClick() {
var logoEl = document.querySelector(".sidebar .logo");
var now = Date.now();
var fast = now - lastLogoTime < 350; /* 350ms 内视为连点 */
lastLogoTime = now;
if (fast) {
logoClickSeq += 1;
if (logoEl) {
logoEl.classList.add("tick");
setTimeout(function () {
logoEl.classList.remove("tick");
}, 300);
}
if (logoClickSeq >= 5) {
/* 5 连点:镜启彩蛋,不跳转 */
logoClickSeq = 0;
var v = EGG_VERSES[Math.floor(Math.random() * EGG_VERSES.length)];
if (logoEl) {
logoEl.classList.add("egg-pulse");
setTimeout(function () {
logoEl.classList.remove("egg-pulse");
}, 1400);
}
showEggToast("镜启 · " + v);
}
return;
}
/* 单击(间隔 > 350ms):tick 反馈后立即回目录 */
logoClickSeq = 0;
if (logoEl) {
logoEl.classList.add("tick");
setTimeout(function () {
logoEl.classList.remove("tick");
}, 300);
}
location.href = "index.html";
}
function highlightActive(mount) {
var key = mount.dataset.active;
if (!key) return;
var item = mount.querySelector('[data-nav="' + key + '"]');
if (item) item.classList.add("active");
}
/* file:// fallback:提示需启动本地服务器 */
function showFallback(mount) {
mount.innerHTML =
'<div class="sidebar-wrap"><aside class="sidebar" style="padding:48px 20px;text-align:center;display:flex;flex-direction:column;gap:18px;align-items:center">' +
"<div style=\"font-family:'Songti SC',serif;font-size:18px;font-weight:700;letter-spacing:0.1em;color:var(--fg)\">魂鏡</div>" +
'<div style="font-size:12px;color:var(--fg-dim);line-height:1.7">导航需本地预览服务器<br/><code style="font-family:\'JetBrains Mono\',monospace;font-size:11px;color:var(--patina)">pnpm design</code></div>' +
'<a class="nav-item" href="index.html" style="justify-content:center;border:1px solid var(--rule);padding:7px 16px">← 目录</a>' +
"</aside></div>";
}
function injectNav() {
var mounts = document.querySelectorAll(".sidebar-mount");
if (!mounts.length) return;
fetch("_nav.html", { cache: "no-cache" })
.then(function (res) {
if (!res.ok) throw new Error(String(res.status));
return res.text();
})
.then(function (html) {
mounts.forEach(function (m) {
m.innerHTML = html;
highlightActive(m);
});
applyTheme(currentTheme()); /* 注入后同步主题标签文案 */
initFold(); /* 恢复折叠态 */
})
.catch(function () {
mounts.forEach(showFallback);
});
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", injectNav);
} else {
injectNav();
}
})();