用于增强 NodeSeek/DeepFlood 论坛体验的用户脚本:提供自动签到、下拉加载、快速评论、内容过滤、等级标记、浏览历史、Callout 渲染、图片预览、快捷键等功能,并带可视化设置面板可自由开关配置。
// ==UserScript==
// @name NodeSeek X
// @namespace http://www.nodeseek.com/
// @version 1.0.0-beta
// @description 用于增强 NodeSeek/DeepFlood 论坛体验的用户脚本:提供自动签到、下拉加载、快速评论、内容过滤、等级标记、浏览历史、Callout 渲染、图片预览、快捷键等功能,并带可视化设置面板可自由开关配置。
// @author dabao
// @match *://www.nodeseek.com/*
// @match *://www.deepflood.com/*
// @require https://s4.zstatic.net/ajax/libs/layui/2.9.9/layui.min.js
// @resource highlightStyle https://s4.zstatic.net/ajax/libs/highlight.js/11.9.0/styles/atom-one-light.min.css
// @resource highlightStyle_dark https://s4.zstatic.net/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_notification
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_getResourceURL
// @grant GM_addElement
// @grant GM_addStyle
// @grant GM_openInTab
// @grant GM_xmlhttpRequest
// @grant unsafeWindow
// @run-at document-idle
// @license GPL-3.0
// @supportURL https://www.nodeseek.com/post-36263-1
// @homepageURL https://www.nodeseek.com/post-36263-1
// ==/UserScript==
(function () {
'use strict';
// NSX Core - 核心
// 环境 + DOM + 网络 + 存储 + 模块管理
const SITES = [
{ host: "www.nodeseek.com", code: "ns", name: "NodeSeek" },
{ host: "www.deepflood.com", code: "df", name: "DeepFlood" }
];
const info = GM_info?.script || {};
const site = SITES.find(s => s.host === location.host);
let debug = false;
try { debug = GM_getValue("settings", {})?.debug?.enabled; } catch { }
// ===== 环境 =====
const env = {
info, site, BASE_URL: location.origin,
log: (...a) => debug && console.log(`[NSX]`, ...a),
warn: (...a) => debug && console.warn(`[NSX]`, ...a),
error: (...a) => console.error(`[NSX]`, ...a)
};
// ===== DOM =====
const $ = (s, r = document) => r?.querySelector(s);
const $$ = (s, r = document) => [...(r?.querySelectorAll(s) || [])];
function addStyle(id, val) {
if (document.getElementById(id)) return;
const isUrl = /^(https?:)?\/\//.test(val);
const el = document.createElement(isUrl ? "link" : "style");
el.id = id;
isUrl ? (el.rel = "stylesheet", el.href = val) : (el.textContent = val);
document.head?.appendChild(el);
}
function addScript(id, val) {
if (document.getElementById(id)) return;
const el = document.createElement("script");
el.id = id;
/^(https?:)?\/\//.test(val) ? (el.src = val) : (el.textContent = val);
document.body?.appendChild(el);
}
const debounce = (fn, ms) => {
let t; const d = (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); };
d.cancel = () => clearTimeout(t); return d;
};
const throttle = (fn, ms) => {
let last = 0;
return (...a) => { const now = Date.now(); if (now - last >= ms) { last = now; fn(...a); } };
};
// ===== 存储 =====
const cfgFragments = new Map(), metaFragments = new Map();
let cfgCache = null;
const isObj = v => v && typeof v === "object" && !Array.isArray(v);
const merge = (t, s) => { for (const k in s) isObj(s[k]) ? (isObj(t[k]) || (t[k] = {}), merge(t[k], s[k])) : t[k] === undefined && (t[k] = s[k]); };
const getPath = (o, p) => p.split(".").reduce((a, k) => a?.[k], o);
const setPath = (o, p, v) => { const ks = p.split("."), l = ks.pop(); ks.reduce((a, k) => a[k] ??= {}, o)[l] = v; };
const store = {
reg(id, cfg, meta) { cfg && cfgFragments.set(id, cfg); meta && metaFragments.set(id, meta); },
getDefaults() { const d = { version: info.version, debug: { enabled: false } }; cfgFragments.forEach(f => merge(d, f)); return d; },
getMeta() { const m = {}; metaFragments.forEach(f => merge(m, f)); return m; },
init() {
if (cfgCache) return cfgCache;
const def = this.getDefaults();
cfgCache = GM_getValue("settings", null) || {};
merge(cfgCache, def);
cfgCache.version = def.version;
GM_setValue("settings", cfgCache);
return cfgCache;
},
get(p, fb) { const v = getPath(this.init(), p); return v === undefined ? fb : v; },
set(p, v) { setPath(this.init(), p, v); GM_setValue("settings", cfgCache); }
};
// ===== 网络 =====
const net = {
async fetch(url, { method = "GET", data, headers = {}, type = "json" } = {}) {
const r = await fetch(url.startsWith("http") ? url : env.BASE_URL + url, {
method, credentials: "include",
headers: { ...(data ? { "Content-Type": "application/json" } : {}), ...headers },
body: data ? JSON.stringify(data) : undefined
});
return r[type]().catch(() => null);
},
get: (u, h, t) => net.fetch(u, { headers: h, type: t }),
post: (u, d, h, t) => net.fetch(u, { method: "POST", data: d, headers: h, type: t })
};
// ===== 模块管理 =====
const modules = new Map();
function define(cfg) {
if (!cfg?.id) throw new Error("id required");
cfg.deps ??= [];
cfg.order ??= 100;
modules.set(cfg.id, cfg);
cfg.cfg && store.reg(cfg.id, cfg.cfg, cfg.meta);
return cfg;
}
function boot(ctx) {
store.init();
// 拓扑排序
const list = [...modules.values()];
const indeg = new Map(list.map(m => [m.id, 0]));
const edges = new Map(list.map(m => [m.id, []]));
list.forEach(m => m.deps.forEach(d => { if (modules.has(d)) { edges.get(d).push(m.id); indeg.set(m.id, indeg.get(m.id) + 1); } }));
const q = list.filter(m => indeg.get(m.id) === 0).sort((a, b) => a.order - b.order);
const sorted = [];
while (q.length) {
const cur = q.shift(); sorted.push(cur);
edges.get(cur.id).forEach(n => { indeg.set(n, indeg.get(n) - 1); if (!indeg.get(n)) q.push(modules.get(n)); });
q.sort((a, b) => a.order - b.order);
}
// 初始化
sorted.forEach(m => {
try { if (m.match?.(ctx) !== false) m.init?.(ctx); } catch (e) { env.error(m.id, e); }
});
// watch
if (ctx.watch) sorted.forEach(m => {
const w = typeof m.watch === "function" ? m.watch(ctx) : m.watch;
[].concat(w || []).filter(Boolean).forEach(i => ctx.watch(i.sel, i.fn, i.opts));
});
}
// 自动跳转外部链接
const autoJump = {
id: "autoJump",
order: 60,
cfg: { auto_jump_external_links: { enabled: true } },
meta: { auto_jump_external_links: { label: "自动跳转外部链接", group: "基本设置" } },
match: ctx => ctx.store.get("auto_jump_external_links.enabled", true),
init(ctx) {
$$('a[href*="/jump?to="]').forEach(a => {
try {
const to = new URL(a.href).searchParams.get("to");
if (to) a.href = decodeURIComponent(to);
} catch { }
});
if (/^\/jump/.test(location.pathname)) ctx.$(".btn")?.click();
}
};
const __vite_glob_0_0 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: autoJump
}, Symbol.toStringTag, { value: 'Module' }));
// 下拉加载翻页
const PROFILES = {
list: { path: /^\/(categories\/|page|award|search|$)/, threshold: 1500, next: ".nsk-pager a.pager-next", list: "ul.post-list:not(.topic-carousel-panel)", pagerTop: "div.nsk-pager.pager-top", pagerBot: "div.nsk-pager.pager-bottom" },
post: { path: /^\/post-/, threshold: 690, next: ".nsk-pager a.pager-next", list: "ul.comments", pagerTop: "div.nsk-pager.post-top-pager", pagerBot: "div.nsk-pager.post-bottom-pager" }
};
const autoLoading = {
id: "autoLoading",
order: 100,
cfg: { loading_post: { enabled: true }, loading_comment: { enabled: true } },
meta: { loading_post: { label: "下拉加载翻页", group: "内容设置" }, loading_comment: { label: "加载评论", group: "内容设置" } },
match: ctx => ctx.store.get("loading_post.enabled", true) || ctx.store.get("loading_comment.enabled", true),
init(ctx) {
const profile = ctx.isList ? PROFILES.list : ctx.isPost ? PROFILES.post : null;
if (!profile) return;
let busy = false, prevY = scrollY;
const blockByLevel = (doc) => {
const lv = ctx.user?.rank || 0;
doc.querySelectorAll('.post-list-item use[href="#lock"]').forEach(el => {
const n = +(el.closest("span")?.textContent?.match(/\d+/)?.[0] || 0);
if (n > lv) el.closest(".post-list-item")?.classList.add("blocked-post");
});
};
const load = async () => {
if (busy) return;
const atBottom = document.documentElement.scrollHeight <= innerHeight + scrollY + profile.threshold;
if (!atBottom) return;
const nextUrl = ctx.$(profile.next)?.href;
if (!nextUrl) return;
busy = true;
try {
const html = await net.get(nextUrl, {}, "text");
const doc = new DOMParser().parseFromString(html, "text/html");
blockByLevel(doc);
// 评论数据同步
if (ctx.isPost) {
const json = doc.getElementById("temp-script")?.textContent;
if (json) try {
const cfg = JSON.parse(decodeURIComponent(atob(json).split("").map(c => "%" + c.charCodeAt(0).toString(16).padStart(2, "0")).join("")));
if (cfg?.postData?.comments) ctx.uw.__config__.postData.comments.push(...cfg.postData.comments);
} catch { }
}
const src = doc.querySelector(profile.list), dst = document.querySelector(profile.list);
if (src && dst) dst.append(...src.children);
[profile.pagerTop, profile.pagerBot].forEach(sel => {
const s = doc.querySelector(sel), d = document.querySelector(sel);
if (s && d) d.innerHTML = s.innerHTML;
});
history.pushState(null, null, nextUrl);
} catch (e) { ctx.env.error("autoLoading", e); }
busy = false;
};
const deb = debounce(load, 300);
addEventListener("scroll", throttle(() => { if (scrollY > prevY) deb(); prevY = scrollY; }, 200), { passive: true });
}
};
const __vite_glob_0_1 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: autoLoading
}, Symbol.toStringTag, { value: 'Module' }));
// 屏蔽用户
const blockMembers = {
id: "blockMembers",
deps: ["ui"],
order: 240,
cfg: { block_members: { enabled: true } },
meta: { block_members: { label: "屏蔽用户", group: "过滤设置" } },
match: ctx => ctx.loggedIn && ctx.store.get("block_members.enabled", true),
init(ctx) {
addStyle("nsx-block", ".usercard-button-group .btn{padding:0 .8rem}");
const block = name => net.post("/api/block-list/add", { block_member_name: name })
.then(r => ctx.ui.alert?.("提示", r?.success ? `屏蔽【${name}】成功` : `屏蔽失败:${r?.message || ""}`));
document.querySelectorAll(".post-list .post-list-item,.content-item").forEach(item => {
const avatar = item.querySelector(".avatar-normal");
if (!avatar) return;
avatar.addEventListener("click", () => {
const check = setInterval(() => {
const card = document.querySelector("div.user-card.hover-user-card");
const pm = card?.querySelector("a.btn");
if (!card || !pm) return;
clearInterval(check);
const name = card.querySelector("a.Username")?.textContent;
if (!name || card.querySelector(".nsx-block-btn")) return;
const btn = pm.cloneNode(false);
btn.className = "btn nsx-block-btn";
btn.textContent = "屏蔽";
btn.style.cssText = "float:left;background-color:rgba(0,0,0,.3)!important";
btn.onclick = e => { e.preventDefault(); ctx.ui.confirm?.(`屏蔽"${name}"?`, "可在设置=>屏蔽用户解除", () => block(name)); };
pm.after(btn);
}, 50);
});
});
}
};
const __vite_glob_0_2 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: blockMembers
}, Symbol.toStringTag, { value: 'Module' }));
// 屏蔽帖子(关键词)
const mark$3 = new WeakSet();
const run$1 = (els, ctx) => {
const kws = (ctx.store.get("block_posts.keywords", []) || []).map(k => String(k).trim().toLowerCase()).filter(Boolean);
els.forEach(item => {
if (mark$3.has(item)) return;
mark$3.add(item);
const title = item.querySelector(".post-title>a")?.textContent?.toLowerCase() || "";
if (kws.some(k => title.includes(k))) item.classList.add("blocked-post");
});
};
const blockPosts = {
id: "blockPosts",
order: 220,
cfg: { block_posts: { enabled: true, keywords: [] } },
meta: { block_posts: { label: "屏蔽帖子", group: "过滤设置", fields: { keywords: { type: "TEXTAREA", label: "关键词", placeholder: "每行一个", valueType: "array" } } } },
match: ctx => ctx.isList && ctx.store.get("block_posts.enabled", true),
init(ctx) { run$1($$(".post-list-item"), ctx); },
watch: ctx => ({ sel: ".post-list-item", fn: els => run$1(els, ctx), opts: { debounce: 80 } })
};
const __vite_glob_0_3 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: blockPosts
}, Symbol.toStringTag, { value: 'Module' }));
// 屏蔽低等级可见帖子
const mark$2 = new WeakSet();
const run = (els, ctx) => {
const lv = ctx.user?.rank || 0;
els.forEach(el => {
const item = el.closest(".post-list-item");
if (!item || mark$2.has(item)) return;
mark$2.add(item);
const n = +(el.closest("span")?.textContent?.match(/\d+/)?.[0] || 0);
if (n > lv) item.classList.add("blocked-post");
});
};
const blockViewLevel = {
id: "blockViewLevel",
order: 222,
match: ctx => ctx.isList,
init(ctx) { run($$('.post-list-item use[href="#lock"]'), ctx); },
watch: ctx => ({ sel: '.post-list-item use[href="#lock"]', fn: els => run(els, ctx), opts: { debounce: 80 } })
};
const __vite_glob_0_4 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: blockViewLevel
}, Symbol.toStringTag, { value: 'Module' }));
// Callout 支持 + 编辑器插入菜单
const CSS_BASE = `.post-content blockquote{border-left:none;border-radius:4px;margin:1em 0;box-shadow:inset 4px 0 0 0 rgba(0,0,0,.1)}.callout{--c:8,109,221;overflow:hidden;border-radius:4px;margin:1em 0;padding:12px 12px 12px 24px!important;box-shadow:inset 4px 0 0 0 rgba(var(--c),.5)}.callout.is-collapsible .callout-title{cursor:pointer}.callout-title{display:flex;gap:4px;color:rgb(var(--c));line-height:1.3;align-items:flex-start}.callout-content{overflow-x:auto}.callout-icon{flex:0 0 auto;display:flex;align-items:center}.callout-icon .svg-icon,.callout-fold .svg-icon{color:rgb(var(--c));height:18px;width:18px}.callout-title-inner{font-weight:600}.callout-fold{display:flex;align-items:center;padding-inline-end:8px}.callout-fold .svg-icon{transition:transform .1s}.callout-fold.is-collapsed .svg-icon{transform:rotate(-90deg)}.callout.is-collapsed .callout-content{display:none}.callout[data-callout="abstract"],.callout[data-callout="summary"],.callout[data-callout="tldr"]{--c:83,223,221}.callout[data-callout="info"],.callout[data-callout="todo"]{--c:8,109,221}.callout[data-callout="tip"],.callout[data-callout="hint"],.callout[data-callout="important"]{--c:83,223,221}.callout[data-callout="success"],.callout[data-callout="check"],.callout[data-callout="done"]{--c:68,207,110}.callout[data-callout="question"],.callout[data-callout="help"],.callout[data-callout="faq"]{--c:236,117,0}.callout[data-callout="warning"],.callout[data-callout="caution"],.callout[data-callout="attention"]{--c:236,117,0}.callout[data-callout="failure"],.callout[data-callout="fail"],.callout[data-callout="missing"]{--c:233,49,71}.callout[data-callout="danger"],.callout[data-callout="error"]{--c:233,49,71}.callout[data-callout="bug"]{--c:233,49,71}.callout[data-callout="example"]{--c:120,82,238}.callout[data-callout="quote"],.callout[data-callout="cite"]{--c:158,158,158}.callout-inserter-wrapper{position:relative;display:inline-flex;align-items:center}.callout-inserter-btn{padding:0;border:none;background:0 0;cursor:pointer;display:flex;color:currentColor}.callout-inserter-btn:hover{opacity:.7}.callout-inserter-dropdown{position:absolute;top:100%;left:50%;transform:translateX(-50%);margin-top:8px;border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,.15);z-index:1000;min-width:160px;display:none;overflow:auto;max-height:240px}.callout-inserter-dropdown.show{display:block}.callout-inserter-item{padding:8px 12px;cursor:pointer;display:flex;align-items:center;gap:8px;font-size:13px;transition:background .15s}.callout-inserter-item:hover{background:#f5f5f5}.callout-inserter-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}`;
const CSS_COLORFUL = `.callout{background:rgba(var(--c),.1)}`;
const ICONS = { note: "M21.17 6.81a1 1 0 0 0-3.99-3.99L3.84 16.17a2 2 0 0 0-.5.83l-1.32 4.35a.5.5 0 0 0 .62.62l4.35-1.32a2 2 0 0 0 .83-.5zm-6.17-1.81 4 4", abstract: "M8 2h8v4H8zM16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2M12 11h4M12 16h4M8 11h.01M8 16h.01", info: "M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm0 14v-4m0-4h.01", tip: "M12 3q1 4 4 6.5t3 5.5a1 1 0 0 1-14 0 5 5 0 0 1 1-3 1 1 0 0 0 5 0c0-2-1.5-3-1.5-5q0-2 2.5-4", success: "M20 6 9 17l-5-5", question: "M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zM9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3M12 17h.01", warning: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3M12 9v4m0 4h.01", failure: "M18 6 6 18M6 6l12 12", danger: "M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z", bug: "M12 20v-9m2-6a4 4 0 0 1 4 4v3a6 6 0 0 1-12 0v-3a4 4 0 0 1 4-4zM14.12 3.88 16 2M8 2l1.88 1.88M9 7.13V6a3 3 0 1 1 6 0v1.13", example: "M3 5h.01M3 12h.01M3 19h.01M8 5h13M8 12h13M8 19h13", quote: "M16 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2zM5 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2z", fold: "m6 9 6 6 6-6" };
const TYPE_MAP = { summary: "abstract", tldr: "abstract", hint: "tip", important: "tip", check: "success", done: "success", help: "question", faq: "question", caution: "warning", attention: "warning", fail: "failure", missing: "failure", error: "danger", cite: "quote" };
const MENUS = [{ k: "note", n: "笔记", c: "8,109,221" }, { k: "info", n: "信息", c: "8,109,221" }, { k: "tip", n: "提示", c: "83,223,221" }, { k: "warning", n: "警告", c: "236,117,0" }, { k: "danger", n: "危险", c: "233,49,71" }, { k: "success", n: "成功", c: "68,207,110" }, { k: "question", n: "问题", c: "236,117,0" }, { k: "example", n: "示例", c: "120,82,238" }];
const svg = d => `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon"><path d="${d}"/></svg>`;
const RE$1 = /^\[!(\w+)\]([+-])?(?:\s+([^<\n]+))?(?:<br\s*\/?>)?([\s\S]*)$/i;
const render = (els) => {
els.forEach(bq => {
if (bq.classList.contains("oc-done") || bq.closest("blockquote.oc-done")) return;
bq.classList.add("oc-done");
const p = bq.querySelector(":scope > p");
const m = (p?.innerHTML?.trim() || "").match(RE$1);
if (!m) return;
const [, type, fold, title, content] = m;
const t = type.toLowerCase(), base = TYPE_MAP[t] || t, icon = ICONS[base] || ICONS.note;
const isColl = fold === "+" || fold === "-", isCol = fold === "-";
const wrap = document.createElement("div");
wrap.className = `callout${isColl ? " is-collapsible" : ""}${isCol ? " is-collapsed" : ""}`;
wrap.dataset.callout = t;
const titleEl = document.createElement("div");
titleEl.className = "callout-title";
titleEl.innerHTML = `<div class="callout-icon">${svg(icon)}</div><div class="callout-title-inner">${title?.trim() || type[0].toUpperCase() + type.slice(1)}</div>`;
if (isColl) {
const foldEl = document.createElement("div");
foldEl.className = `callout-fold${isCol ? " is-collapsed" : ""}`;
foldEl.innerHTML = svg(ICONS.fold);
titleEl.appendChild(foldEl);
titleEl.onclick = () => { wrap.classList.toggle("is-collapsed"); foldEl.classList.toggle("is-collapsed"); };
}
wrap.appendChild(titleEl);
const cont = document.createElement("div");
cont.className = "callout-content";
if (content?.trim()) { const pp = document.createElement("p"); pp.innerHTML = content.trim(); cont.appendChild(pp); }
let sib = p.nextSibling;
while (sib) { const next = sib.nextSibling; cont.appendChild(sib); sib = next; }
if (cont.childNodes.length) wrap.appendChild(cont);
bq.replaceWith(wrap);
});
};
const insertCallout = (editor, type) => {
const cm = editor.querySelector(".CodeMirror")?.CodeMirror;
if (!cm) return;
const doc = cm.getDoc();
let cur = doc.getCursor();
const lvl = (doc.getLine(cur.line).match(/^(>\s*)+/)?.[0].match(/>/g) || []).length;
if (lvl > 0) {
let last = cur.line;
for (let i = cur.line + 1; i < doc.lineCount(); i++) { if (doc.getLine(i).match(/^>\s*/)) last = i; else break; }
cur = { line: last, ch: doc.getLine(last).length };
}
const pre = lvl > 0 ? ">".repeat(lvl + 1) + " " : "> ";
doc.replaceRange((lvl > 0 ? "\n" : "") + `${pre}[!${type}] \n${pre}`, cur);
doc.setCursor({ line: cur.line + (lvl > 0 ? 1 : 0), ch: `${pre}[!${type}] `.length });
cm.focus();
};
let clickBound = false;
const createInserter = () => {
const editor = $(".md-editor");
const bar = editor?.querySelector(".mde-toolbar");
if (!editor || !bar || bar.querySelector(".callout-inserter-wrapper")) return;
const vAttr = [...(bar.querySelector(".toolbar-item")?.attributes || [])].find(a => a.name.startsWith("data-v-"))?.name;
const setV = el => vAttr && el.setAttribute(vAttr, "");
const wrap = document.createElement("span");
wrap.className = "callout-inserter-wrapper toolbar-item";
wrap.title = "Callout - NodeSeek X";
setV(wrap);
const btn = document.createElement("span");
btn.className = "callout-inserter-btn i-icon";
btn.innerHTML = `<svg width="16" height="16" viewBox="0 0 48 48" fill="none"><path d="M44 8H4v30h15l5 5 5-5h15V8Z" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/><path d="M24 18v10" stroke="currentColor" stroke-width="4" stroke-linecap="round"/><circle cx="24" cy="33" r="2" fill="currentColor"/></svg>`;
setV(btn);
const drop = document.createElement("div");
drop.className = "callout-inserter-dropdown";
MENUS.forEach(t => {
const item = document.createElement("div");
item.className = "callout-inserter-item";
item.innerHTML = `<span class="callout-inserter-dot" style="background:rgb(${t.c})"></span>${t.n}[${t.k}]`;
item.onclick = e => { e.stopPropagation(); insertCallout(editor, t.k); drop.classList.remove("show"); };
drop.appendChild(item);
});
btn.onclick = e => { e.stopPropagation(); drop.classList.toggle("show"); };
if (!clickBound) { document.addEventListener("click", () => $$(".callout-inserter-dropdown.show").forEach(d => d.classList.remove("show"))); clickBound = true; }
const sep = document.createElement("div");
sep.className = "sep";
setV(sep);
wrap.append(btn, drop);
bar.append(sep, wrap);
};
const callout = {
id: "callout",
order: 360,
cfg: { callout: { enabled: true, style: "colorful" } },
meta: { callout: { label: "Callout 支持", group: "内容设置", fields: { style: { type: "RADIO", label: "风格", options: [{ value: "colorful", text: "彩色" }, { value: "clean", text: "清爽" }] } } } },
match: ctx => (ctx.isPost || /^\/new-discussion/.test(location.pathname)) && ctx.store.get("callout.enabled", true),
init(ctx) {
const style = ctx.store.get("callout.style", "colorful");
addStyle("nsx-callout", CSS_BASE + (style === "colorful" ? CSS_COLORFUL : ""));
render($$(".post-content blockquote"));
createInserter();
document.addEventListener("click", e => { if (e.target?.closest?.(".md-editor")) requestAnimationFrame(createInserter); });
},
watch: () => [{ sel: ".post-content blockquote", fn: render, opts: { debounce: 80 } }, { sel: ".mde-toolbar", fn: createInserter, opts: { debounce: 80 } }]
};
const __vite_glob_0_5 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: callout
}, Symbol.toStringTag, { value: 'Module' }));
// 代码高亮 + 复制按钮
const CSS$4 = `.post-content pre{position:relative}.post-content pre span.copy-code{position:absolute;right:.5em;top:.5em;cursor:pointer;color:#c1c7cd}.post-content pre .iconpark-icon{width:16px;height:16px;margin:3px}.post-content pre .iconpark-icon:hover{color:var(--link-hover-color)}.dark-layout .post-content pre code.hljs{padding:1em!important}`;
const mark$1 = new WeakSet();
const addCopyBtn = (els, ctx) => {
els.forEach(code => {
if (mark$1.has(code)) return;
mark$1.add(code);
const btn = document.createElement("span");
btn.className = "copy-code";
btn.title = "复制代码";
btn.innerHTML = `<svg class="iconpark-icon"><use href="#copy"></use></svg>`;
btn.onclick = () => {
const sel = getSelection(), range = document.createRange();
range.selectNodeContents(code);
sel.removeAllRanges();
sel.addRange(range);
document.execCommand("copy");
sel.removeAllRanges();
btn.querySelector("use")?.setAttribute("href", "#check");
setTimeout(() => btn.querySelector("use")?.setAttribute("href", "#copy"), 1000);
ctx.ui.tips?.("复制成功", btn, { tips: 4, time: 1000 });
};
code.after(btn);
});
};
const codeHighlight = {
id: "codeHighlight",
deps: ["ui"],
order: 140,
cfg: { code_highlight: { enabled: true } },
meta: { code_highlight: { label: "代码高亮", group: "内容设置" } },
match: ctx => ctx.store.get("code_highlight.enabled", true),
init(ctx) {
addStyle("nsx-hl-css", CSS$4);
addCopyBtn($$(".post-content pre code"), ctx);
},
watch: ctx => ({ sel: ".post-content pre code", fn: els => addCopyBtn(els, ctx), opts: { debounce: 80 } })
};
const __vite_glob_0_6 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: codeHighlight
}, Symbol.toStringTag, { value: 'Module' }));
// 快捷键发送评论 (Ctrl+Enter)
const commentShortcut = {
id: "commentShortcut",
order: 135,
cfg: { comment_shortcut: { enabled: true } },
meta: { comment_shortcut: { label: "快捷键发送评论", group: "内容设置" } },
match: ctx => ctx.isPost && ctx.store.get("comment_shortcut.enabled", true),
init(ctx) {
const getBtn = () => $(".md-editor button.submit.btn.focus-visible");
$$(".CodeMirror").forEach(cmEl => {
const cm = cmEl?.CodeMirror;
if (!cm || cm.__nsx) return;
cm.__nsx = true;
const bind = () => {
const btn = getBtn();
if (btn && !/Ctrl\+Enter/i.test(btn.textContent)) btn.textContent += "(Ctrl+Enter)";
if (btn && !cm.__nsxMap) {
cm.__nsxMap = { "Ctrl-Enter": () => getBtn()?.click() };
cm.addKeyMap(cm.__nsxMap);
} else if (!btn && cm.__nsxMap) {
cm.removeKeyMap(cm.__nsxMap);
cm.__nsxMap = null;
}
};
bind();
cmEl.addEventListener("focusin", bind, true);
cmEl.addEventListener("focusout", bind, true);
});
}
};
const __vite_glob_0_7 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: commentShortcut
}, Symbol.toStringTag, { value: 'Module' }));
// 暗色模式样式切换
const darkMode = {
id: "darkMode",
order: 180,
init(ctx) {
const body = document.body;
if (!body) return;
const darkCss = ctx.store.get("style.layui_dark_css", "https://s.cfn.pp.ua/layui/theme-dark/2.9.7/css/layui-theme-dark.css");
const lightHl = GM_getResourceURL("highlightStyle");
const darkHl = GM_getResourceURL("highlightStyle_dark");
const apply = () => {
const dark = body.classList.contains("dark-layout");
if (dark) {
addStyle("layuicss-theme-dark", darkCss);
document.getElementById("nsx-hl")?.remove();
addStyle("nsx-hl", darkHl);
} else {
document.getElementById("layuicss-theme-dark")?.remove();
document.getElementById("nsx-hl")?.remove();
addStyle("nsx-hl", lightHl);
}
};
apply();
new MutationObserver(() => apply()).observe(body, { attributes: true, attributeFilter: ["class"] });
}
};
const __vite_glob_0_8 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: darkMode
}, Symbol.toStringTag, { value: 'Module' }));
// 浏览历史
const CSS$3 = `#nsx-history-panel{position:fixed;right:12px;top:56px;width:min(380px,94vw);height:70vh;background:#fff;border:1px solid #e4e4e4;border-radius:12px;box-shadow:0 16px 32px rgba(0,0,0,.12);z-index:99999;display:none;flex-direction:column;font-size:13px;color:#1f1f1f;box-sizing:border-box;font-family:"Segoe UI","Microsoft YaHei",sans-serif}#nsx-history-panel.show{display:flex}.nsx-history-header{display:flex;align-items:center;justify-content:space-between;padding:12px 12px 6px}.nsx-history-title{font-size:15px;font-weight:600}.nsx-history-action{border:0;background:0;color:#666;cursor:pointer;font-size:12px;padding:4px 8px;border-radius:6px}.nsx-history-action:hover{background:#f2f3f5}.nsx-history-search{display:flex;align-items:center;gap:6px;margin:0 12px 8px;border:1px solid #e1e1e1;border-radius:8px;padding:6px 8px}.nsx-history-search input{border:0;background:0;outline:0;width:100%;font-size:13px}.nsx-history-tabs{display:flex;gap:16px;padding:0 12px 6px;border-bottom:1px solid #f0f0f0}.nsx-history-tab{border:0;background:0;cursor:pointer;color:#6b6b6b;font-size:12px;padding:6px 0;font-weight:600;border-bottom:2px solid transparent}.nsx-history-tab.is-active{color:#0a62ff;border-bottom-color:#0a62ff}.nsx-history-list{flex:1;overflow-y:auto;padding:6px 8px 12px}.nsx-history-group{margin-bottom:10px}.nsx-history-group-title{display:flex;align-items:center;justify-content:space-between;padding:4px;color:#666;font-size:12px}.nsx-history-items{list-style:none;margin:0;padding:0}.nsx-history-item{display:flex;align-items:center;gap:8px;padding:6px;border-radius:8px}.nsx-history-item:hover{background:#f5f7fb}.nsx-history-link{display:flex;align-items:center;gap:8px;flex:1;min-width:0;text-decoration:none;color:inherit}.nsx-history-icon{width:14px;height:14px;border-radius:3px;background:#f0f0f0}.nsx-history-item-title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.nsx-history-time{color:#9a9a9a;font-size:12px;margin-left:auto}.nsx-history-empty{padding:10px 6px;color:#999;font-size:12px}.nsx-history-close{border:0;background:0;color:#999;cursor:pointer;font-size:12px;padding:2px 4px;border-radius:6px;opacity:0}.nsx-history-item:hover .nsx-history-close{opacity:1}.nsx-history-close:hover{color:#ff4d4f}`;
const HKEY = "nsx_browsing_history", RKEY = "nsx_recently_closed";
const RE = /\/post-(\d+)-\d+.*$/;
const pad = n => String(n).padStart(2, "0");
const fmtDate = d => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
const fmtTime = d => `${pad(d.getHours())}:${pad(d.getMinutes())}`;
const now = () => new Date().toISOString();
const esc = s => String(s ?? "").replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
const history$1 = {
id: "history",
order: 300,
cfg: { history: { enabled: true, limit: 100, days: 7 } },
meta: { history: { label: "浏览历史", group: "显示设置", fields: { limit: { type: "NUMBER", label: "保存上限", valueType: "number" }, days: { type: "NUMBER", label: "保存天数", valueType: "number" } } } },
match: ctx => (ctx.isPost || ctx.isList) && ctx.store.get("history.enabled", true),
init(ctx) {
const maxItems = ctx.store.get("history.limit", 100) || 100;
const maxAge = (ctx.store.get("history.days", 7) || 7) * 864e5;
const prune = arr => {
const t = Date.now();
return (arr || []).filter(i => t - new Date(i.time).getTime() < maxAge).sort((a, b) => new Date(a.time) - new Date(b.time)).slice(-maxItems);
};
const load = k => { const r = JSON.parse(localStorage.getItem(k) || "[]"); const n = prune(r); if (n.length !== r.length) localStorage.setItem(k, JSON.stringify(n)); return n; };
const save = (k, a) => localStorage.setItem(k, JSON.stringify(prune(a)));
const getH = () => load(HKEY), saveH = a => save(HKEY, a);
const getR = () => load(RKEY), saveR = a => save(RKEY, a);
const add = (url, title) => { const m = url.match(RE); if (!m) return; const u = `/post-${m[1]}-1`; const h = getH(), i = h.findIndex(x => x.url === u); const e = { url: u, title, time: now() }; i > -1 ? h[i] = e : h.push(e); saveH(h); };
const addR = (url, title) => { const m = url.match(RE); if (!m) return; const u = `/post-${m[1]}-1`; const h = getR().filter(x => x.url !== u); h.push({ url: u, title, time: now() }); saveR(h); };
addStyle("nsx-hist", CSS$3);
let panel = null, trigger = null, state = { open: false, tab: "all", kw: "" };
const orig = $("#nsk-head .color-theme-switcher");
if (!orig) return;
trigger = orig.cloneNode(false);
trigger.classList.replace("color-theme-switcher", "history-dropdown-on");
trigger.title = "历史记录";
trigger.innerHTML = `<svg class="iconpark-icon" style="width:17px;height:17px"><use href="#history"></use></svg>`;
orig.before(trigger);
const open = () => {
if (!panel) {
panel = document.createElement("div");
panel.id = "nsx-history-panel";
panel.innerHTML = `<div class="nsx-history-header"><div class="nsx-history-title">历史记录</div><button class="nsx-history-action" data-a="clear">清空</button></div><div class="nsx-history-search">🔍<input placeholder="搜索"/></div><div class="nsx-history-tabs"><button class="nsx-history-tab is-active" data-t="all">全部</button><button class="nsx-history-tab" data-t="recent">最近关闭</button></div><div class="nsx-history-list"></div>`;
document.body.appendChild(panel);
panel.querySelector("input").oninput = e => { state.kw = e.target.value.toLowerCase(); render(); };
panel.onclick = e => {
e.stopPropagation();
const t = e.target.closest("[data-t]");
if (t) { state.tab = t.dataset.t; render(); return; }
const a = e.target.closest("[data-a]");
if (a?.dataset.a === "clear") { ctx.ui.confirm("确认", "确定要清空所有记录吗?", () => { localStorage.removeItem(state.tab === "recent" ? RKEY : HKEY); render(); }); }
if (a?.dataset.a === "del") { const u = decodeURIComponent(a.dataset.u); state.tab === "recent" ? saveR(getR().filter(x => x.url !== u)) : saveH(getH().filter(x => x.url !== u)); render(); }
};
document.addEventListener("click", e => { if (state.open && !panel.contains(e.target) && !trigger.contains(e.target)) close(); });
document.addEventListener("keydown", e => { if (state.open && e.key === "Escape") close(); });
}
const r = trigger.getBoundingClientRect();
panel.style.top = `${r.bottom + 8}px`;
panel.style.height = `${innerHeight - r.bottom - 16}px`;
render();
panel.classList.add("show");
state.open = true;
};
const close = () => { panel?.classList.remove("show"); state.open = false; };
const toggle = () => state.open ? close() : open();
const render = () => {
let list = (state.tab === "recent" ? getR() : getH()).sort((a, b) => new Date(b.time) - new Date(a.time));
if (state.kw) list = list.filter(i => (i.title || "").toLowerCase().includes(state.kw));
panel.querySelectorAll(".nsx-history-tab").forEach(b => b.classList.toggle("is-active", b.dataset.t === state.tab));
const lEl = panel.querySelector(".nsx-history-list");
if (!list.length) { lEl.innerHTML = `<div class="nsx-history-empty">暂无记录</div>`; return; }
const g = {};
list.forEach(i => { const d = fmtDate(new Date(i.time)); (g[d] ||= []).push(i); });
lEl.innerHTML = Object.entries(g).map(([day, items]) => `<div class="nsx-history-group"><div class="nsx-history-group-title"><span>${esc(day)}</span></div><ul class="nsx-history-items">${items.map(i => `<li class="nsx-history-item"><a class="nsx-history-link" href="${esc(i.url)}"><img class="nsx-history-icon" src="/favicon.ico"><span class="nsx-history-item-title">${esc((i.title || "").slice(0, 24))}</span></a><span class="nsx-history-time">${fmtTime(new Date(i.time))}</span><button class="nsx-history-close" data-a="del" data-u="${encodeURIComponent(i.url)}">✖</button></li>`).join("")}</ul></div>`).join("");
};
trigger.onclick = e => { e.preventDefault(); e.stopPropagation(); toggle(); };
add(location.href, document.title);
addEventListener("beforeunload", () => addR(location.href, document.title), { capture: true });
}
};
const __vite_glob_0_9 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: history$1
}, Symbol.toStringTag, { value: 'Module' }));
// 图片预览
const mark = new WeakSet();
const bind = (els, ctx) => {
els.forEach(img => {
const post = img.closest("article.post-content");
if (!post || mark.has(img)) return;
mark.add(img);
const newImg = img.cloneNode(true);
img.replaceWith(newImg);
mark.add(newImg);
newImg.addEventListener("click", e => {
e.preventDefault();
const imgs = [...post.querySelectorAll("img:not(.sticker)")];
const data = imgs.map((x, i) => ({ alt: x.alt, pid: i + 1, src: x.src }));
ctx.ui.layer?.photos({ photos: { title: "图片预览", start: imgs.indexOf(newImg), data } });
}, true);
});
};
const imageSlide = {
id: "imageSlide",
deps: ["ui"],
order: 160,
cfg: { image_slide: { enabled: true } },
meta: { image_slide: { label: "图片预览", group: "内容设置" } },
match: ctx => ctx.isPost && ctx.store.get("image_slide.enabled", true),
init(ctx) { bind($$("article.post-content img:not(.sticker)"), ctx); },
watch: ctx => ({ sel: "article.post-content img:not(.sticker)", fn: els => bind(els, ctx), opts: { debounce: 80 } })
};
const __vite_glob_0_10 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: imageSlide
}, Symbol.toStringTag, { value: 'Module' }));
// 悬停预加载
const instantPage = {
id: "instantPage",
order: 320,
cfg: { instant_page: { enabled: true } },
meta: { instant_page: { label: "悬停预加载", group: "内容设置" } },
match: ctx => ctx.store.get("instant_page.enabled", true),
init(ctx) {
const done = new Set();
const link = document.createElement("link");
link.rel = "prefetch";
document.body.addEventListener("mouseover", e => {
const a = e.target.closest("a");
if (!a?.href?.startsWith(`${location.origin}/post-`) || done.has(a.href)) return;
setTimeout(() => {
if (a.matches(":hover")) {
link.href = a.href;
document.head.appendChild(link);
done.add(a.href);
}
}, 65);
}, { passive: true });
}
};
const __vite_glob_0_11 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: instantPage
}, Symbol.toStringTag, { value: 'Module' }));
// 等级标签
const CSS$2 = `.role-tag.user-level{color:#fafafa}.user-lv0{background:#c7c2c2;border-color:#c7c2c2}.user-lv1{background:#ffb74d;border-color:#ffb74d}.user-lv2{background:#ff9400;border-color:#ff9400}.user-lv3{background:#ff5252;border-color:#ff5252}.user-lv4{background:#e53935;border-color:#e53935}.user-lv5{background:#ab47bc;border-color:#ab47bc}.user-lv6{background:#8e24aa;border-color:#8e24aa}.user-lv7{background:#42a5f5;border-color:#42a5f5}.user-lv8{background:#1e88e5;border-color:#1e88e5}.user-lv9{background:#66bb6a;border-color:#66bb6a}.user-lv10{background:#2e7d32;border-color:#2e7d32}.user-lv11{background:#ffca28;border-color:#ffca28}.user-lv12{background:#ffb300;border-color:#ffb300}.user-lv13{background:#b388ff;border-color:#b388ff}.user-lv14{background:#7c4dff;border-color:#7c4dff}.user-lv15{background:#000;border-color:#000;color:#ffd700}`;
const levelTag = {
id: "levelTag",
deps: ["ui"],
order: 260,
cfg: { level_tag: { enabled: true, low_lv_alarm: true, low_lv_max_days: 30 } },
meta: { level_tag: { label: "等级标签", group: "显示设置", fields: { low_lv_alarm: { type: "SWITCH", label: "低等级警告" }, low_lv_max_days: { type: "NUMBER", label: "注册天数", valueType: "number" } } } },
match: ctx => ctx.loggedIn && ctx.isPost && ctx.store.get("level_tag.enabled", true),
async init(ctx) {
addStyle("nsx-lv", CSS$2);
const opUid = ctx.uw?.__config__?.postData?.op?.uid;
if (!opUid) return;
let user;
try {
const r = await net.get(`/api/account/getInfo/${opUid}`);
if (!r?.success) return;
user = r.detail;
} catch { return; }
const days = Math.floor((Date.now() - new Date(user.created_at)) / 864e5);
const alarm = ctx.store.get("level_tag.low_lv_alarm") && days < (ctx.store.get("level_tag.low_lv_max_days", 30) || 30) ? "⚠️" : "";
const rank = Math.floor(Math.sqrt(user.coin || 0) / 10);
const span = document.createElement("span");
span.className = `nsk-badge role-tag user-level user-lv${rank}`;
span.innerHTML = `<span>${alarm}Lv ${rank}</span>`;
span.onmouseenter = () => ctx.ui.tips?.(`注册 <span class="layui-badge">${days}</span> 天;帖子 ${user.nPost};评论 ${user.nComment}`, span, { tips: 3, time: 0 });
span.onmouseleave = () => ctx.ui.layer?.closeAll?.();
ctx.$('#nsk-body .nsk-post .nsk-content-meta-info .author-info>a')?.after(span);
}
};
const __vite_glob_0_12 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: levelTag
}, Symbol.toStringTag, { value: 'Module' }));
// 菜单系统(油猴菜单 + 高级设置面板)
const CSS$1 = `#nsx-config-menu{height:100%;overflow-y:visible;border-right:1px solid #eee}#nsx-config-content{height:100%;overflow-y:auto;padding:0 15px;background:#f8f8f8}.nsx-config-card{margin-bottom:20px}.nsx-config-card .layui-card-header{display:flex;align-items:center;justify-content:space-between;font-weight:700}.nsx-config-card .header-checkbox{position:absolute;right:15px;top:50%;transform:translateY(-50%)}.nsx-config-card .layui-form-switch{margin-top:0!important}.nsx-config-card .layui-card-body:empty{padding-top:0;padding-bottom:0}.dark-layout #nsx-config-menu{border-right-color:#3a3a3a}.dark-layout #nsx-config-content{background:#1e1e1e}`;
const el = (t, c, p, s) => { const e = document.createElement(t); if (c) e.className = c; if (s) e.style.cssText = s; if (p) p.appendChild(e); return e; };
const menus = {
id: "menus",
deps: ["ui"],
order: 30,
cfg: { open_post_in_new_tab: { enabled: false } },
meta: { open_post_in_new_tab: { label: "新标签页打开帖子", group: "内容设置" } },
match: () => true,
init(ctx) {
const uw = ctx.uw, code = ctx.site?.code || "ns";
const ids = [];
const txt = (m, v) => `${m.text}: ${m.states[v].s1} ${m.states[v].s2}`;
const regMenus = () => {
ids.splice(0).forEach(i => GM_unregisterMenuCommand(i));
menus.forEach(m => {
let lbl = m.text;
if (m.states.length > 0) {
let v = 0;
if (m.name === "sign_in") v = store.get(`sign_in.${code}.method`, 0);
else v = store.get(`${m.name}.enabled`, true) === false ? 0 : 1;
lbl = txt(m, v);
}
const id = GM_registerMenuCommand(lbl, () => m.cb(m.name, m.states), { autoClose: m.autoClose ?? true });
ids.push(id || lbl);
});
};
const switchState = (n, states) => {
if (n === "sign_in") {
if (!ctx.site) return;
let cur = store.get(`sign_in.${code}.method`, 0);
cur = (cur + 1) % states.length;
store.set(`sign_in.${code}.enabled`, cur !== 0);
store.set(`sign_in.${code}.method`, cur);
} else if (n === "loading_post") {
const next = !store.get("loading_post.enabled", true);
store.set("loading_post.enabled", next);
store.set("loading_comment.enabled", next);
} else {
store.set(`${n}.enabled`, !store.get(`${n}.enabled`, true));
}
regMenus();
};
const reSign = () => {
if (!ctx.loggedIn || store.get(`sign_in.${code}.enabled`, true) === false) return ctx.ui.alert("提示", "签到已关闭");
store.set(`sign_in.${code}.last_date`, "1753/1/1");
location.reload();
};
const switchNewTab = () => {
const next = !store.get("open_post_in_new_tab.enabled", false);
try {
uw.indexedDB.open("ns-preference-db").onsuccess = e => {
const db = e.target.result;
const s = db.transaction("ns-preference-store", "readwrite").objectStore("ns-preference-store");
s.get("configuration").onsuccess = e2 => {
const c = e2.target.result || {};
c.openPostInNewPage = next;
s.put(c, "configuration");
store.set("open_post_in_new_tab.enabled", next);
regMenus();
ctx.ui.alert("", `已${next ? "开启" : "关闭"}新标签页打开链接`);
};
};
} catch { }
};
const advSettings = () => {
if (!ctx.ui.layer || !window.layui) return;
addStyle("nsx-cfg", CSS$1);
// 获取所有模块的 cfg 和 meta
const defs = store.getDefaults(), metas = store.getMeta();
const ignore = new Set(["version", "debug", "ui"]);
const entries = Object.entries(metas).filter(([k]) => defs[k] && !ignore.has(k)).map(([k, m]) => ({ key: k, meta: m }));
const groups = {};
entries.forEach(e => { const g = e.meta.group || "其他设置"; (groups[g] ||= []).push(e); });
const cont = document.createElement("div");
cont.className = "layui-row";
cont.style.cssText = "display:flex;height:100%";
const menuDiv = el("div", "layui-panel layui-col-xs3", cont);
menuDiv.id = "nsx-config-menu";
const menuList = el("ul", "layui-menu", menuDiv);
const wrapper = el("div", "layui-col-xs9", cont);
wrapper.id = "nsx-config-content";
const isObj = v => v && typeof v === "object" && !Array.isArray(v);
const inferType = (v, m) => m?.type || (Array.isArray(v) ? "TEXTAREA" : typeof v === "boolean" ? "SWITCH" : typeof v === "number" ? "NUMBER" : "TEXT");
const inferVT = (v, m) => m?.valueType || (Array.isArray(v) ? "array" : typeof v === "number" ? "number" : typeof v === "boolean" ? "boolean" : "string");
const makeField = (f, path, val) => {
const w = el("div", "layui-col-md12"), item = el("div", "layui-form-item", w);
const lbl = el("label", "layui-form-label", item); lbl.textContent = f.label || f.key;
const blk = el("div", "layui-input-block", item);
let inp;
if (f.type === "SWITCH") { inp = el("input", "", blk); inp.type = "checkbox"; if (val) inp.setAttribute("checked", ""); inp.setAttribute("lay-skin", "switch"); inp.setAttribute("lay-text", "开启|关闭"); inp.name = path; }
else if (f.type === "TEXTAREA") { inp = el("textarea", "layui-textarea", blk); inp.setAttribute("placeholder", f.placeholder || ""); inp.textContent = Array.isArray(val) ? val.join("\n") : (val ?? ""); inp.name = path; }
else if (f.type === "RADIO" && f.options) {
f.options.forEach(opt => {
const r = el("input", "", blk); r.type = "radio"; r.name = path; r.setAttribute("value", opt.value);
r.dataset.valueType = f.valueType || "";
if (String(val) === String(opt.value)) r.setAttribute("checked", "");
r.setAttribute("title", opt.text);
});
inp = blk.querySelector("input");
}
else { inp = el("input", "layui-input", blk); inp.type = f.type === "NUMBER" ? "number" : "text"; inp.setAttribute("value", val ?? ""); inp.name = path; }
if (inp) inp.dataset.valueType = f.valueType || "";
return w;
};
const makeCard = (entry, siteCode) => {
const m = entry.meta || {};
let base = entry.key, cfg = defs[entry.key];
if (entry.key === "sign_in") { cfg = defs.sign_in?.[siteCode] || defs.sign_in?.ns || {}; base = `sign_in.${siteCode}`; }
if (!isObj(cfg)) return null;
const card = el("div", "layui-card layui-form nsx-config-card");
const hdr = el("div", "layui-card-header", card); hdr.textContent = m.label || entry.key;
if (typeof cfg.enabled === "boolean") {
const cbW = el("div", "header-checkbox", hdr), cb = el("input", "", cbW);
cb.type = "checkbox"; cb.name = `${base}.enabled`; if (store.get(`${base}.enabled`, cfg.enabled)) cb.setAttribute("checked", "");
cb.setAttribute("lay-skin", "switch"); cb.setAttribute("lay-text", "开启|关闭");
}
const body = el("div", "layui-card-body layui-row layui-col-space10", card);
const fields = m.fields || {}, hidden = new Set(m.hidden || []);
Object.keys(cfg).filter(k => k !== "enabled" && !isObj(cfg[k]) && !hidden.has(k)).forEach(k => {
const fm = fields[k] || {};
const f = { key: k, label: fm.label || k, type: inferType(cfg[k], fm), options: fm.options, placeholder: fm.placeholder, valueType: inferVT(cfg[k], fm) };
const cur = store.get(`${base}.${k}`, cfg[k]);
const fe = makeField(f, `${base}.${k}`, cur);
if (fe) body.appendChild(fe);
});
return card;
};
Object.entries(groups).forEach(([g, list], i) => {
const fs = el("fieldset", "layui-elem-field layui-field-title", wrapper); fs.id = `group-${i}`;
const lg = el("legend", "", fs); lg.textContent = g;
const fd = el("div", "layui-form", wrapper);
list.forEach(e => { const c = makeCard(e, code); if (c) fd.appendChild(c); });
const mi = el("li", "", menuList); if (i === 0) mi.classList.add("layui-menu-item-checked");
const mb = el("div", "layui-menu-body-title", mi), a = el("a", "", mb); a.href = `#group-${i}`; a.textContent = g;
});
// 底部提示
const endFs = el("fieldset", "layui-elem-field layui-field-title", wrapper, "text-align:center");
const endLg = el("legend", "", endFs, "font-size:0.8em;opacity:0.5");
endLg.textContent = "到底了";
const w = window.layui.device().mobile ? "100%" : "620px";
ctx.ui.layer.open({
type: 1, offset: "r", anim: "slideLeft", area: [w, "100%"], scrollbar: false, shade: 0.1, shadeClose: false,
btn: ["保存设置"], btnAlign: "r", title: "NodeSeek X 设置", id: "setting-layer-direction-r", content: cont.outerHTML,
success: ly => { ly?.[0] || ly; try { window.layui.use?.(["form"], () => window.layui.form?.render()); } catch { } },
yes: (idx, ly) => {
const r = ly?.[0] || ly, sc = r?.querySelector ? r : document;
sc.querySelectorAll("input,select,textarea").forEach(el => {
if (!el.name) return;
// radio 只保存选中的那个
if (el.type === "radio" && !el.checked) return;
let v;
const vt = el.dataset.valueType;
if (el.type === "checkbox") v = el.checked;
else if (el.type === "radio") v = vt === "number" ? Number(el.value) : el.value;
else if (el.tagName === "TEXTAREA") v = vt === "array" ? el.value.split("\n").map(s => s.trim()).filter(Boolean) : el.value;
else if (el.type === "number" || vt === "number") { const n = Number(el.value); v = Number.isFinite(n) ? n : 0; }
else v = el.value;
if (v !== undefined) store.set(el.name, v);
});
ctx.ui.layer.msg("设置已保存,刷新生效");
ctx.ui.layer.close(idx);
}
});
};
const menus = [
{ name: "sign_in", cb: switchState, text: "自动签到", states: [{ s1: "❌", s2: "关闭" }, { s1: "🎲", s2: "随机🍗" }, { s1: "📌", s2: "5个🍗" }] },
{ name: "re_sign", cb: reSign, text: "🔂 重试签到", states: [] },
{ name: "loading_post", cb: switchState, text: "下拉加载翻页", states: [{ s1: "❌", s2: "关闭" }, { s1: "✅", s2: "开启" }] },
{ name: "open_post_in_new_tab", cb: switchNewTab, text: "新标签页打开帖子", states: [{ s1: "❌", s2: "关闭" }, { s1: "✅", s2: "开启" }] },
{ name: "advanced_settings", cb: advSettings, text: "⚙️ 高级设置", states: [] },
{ name: "feedback", cb: () => GM_openInTab("https://greasyfork.org/zh-CN/scripts/479426/feedback", { active: true, insert: true, setParent: true }), text: "💬 反馈 & 建议", states: [] }
];
regMenus();
}
};
const __vite_glob_0_13 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: menus
}, Symbol.toStringTag, { value: 'Module' }));
// 快捷评论
const quickComment = {
id: "quickComment",
order: 120,
cfg: { quick_comment: { enabled: true } },
meta: { quick_comment: { label: "快捷评论", group: "内容设置" } },
match: ctx => ctx.loggedIn && ctx.isPost && ctx.store.get("loading_comment.enabled", true) && ctx.store.get("quick_comment.enabled", true),
init(ctx) {
const editor = $(".md-editor"), parent = $("#back-to-parent"), group = $("#fast-nav-button-group");
if (!editor || !parent || !group) return;
let open = false;
const show = e => {
if (open) return;
e?.preventDefault?.();
editor.style.cssText = `position:fixed;bottom:0;margin:0;width:100%;max-width:${editor.clientWidth || 720}px;z-index:999`;
addClose();
open = true;
};
const btn = parent.cloneNode(true);
btn.id = "back-to-comment";
btn.innerHTML = `<svg class="iconpark-icon" style="width:24px;height:24px"><use href="#comments"></use></svg>`;
btn.onclick = show;
parent.before(btn);
$$(".nsk-post .comment-menu,.comment-container .comments").forEach(el => el.addEventListener("click", e => {
if (["引用", "回复", "编辑"].includes(e.target?.textContent)) show(e);
}, true));
function addClose() {
const tb = $("#editor-body .window_header > :last-child");
if (!tb || $(".nsx-close-editor")) return;
const cb = tb.cloneNode(true);
cb.classList.add("nsx-close-editor");
cb.title = "关闭";
const sp = cb.querySelector("span");
if (sp) {
sp.classList.replace("i-icon-full-screen-one", "i-icon-close");
sp.innerHTML = `<svg width="16" height="16" viewBox="0 0 48 48" fill="none"><path d="M8 8L40 40M8 40L40 8" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
}
cb.onclick = () => { editor.style.cssText = ""; cb.remove(); open = false; };
tb.after(cb);
}
}
};
const __vite_glob_0_14 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: quickComment
}, Symbol.toStringTag, { value: 'Module' }));
// 自动签到
const signIn = {
id: "signIn",
deps: ["ui"],
order: 80,
cfg: {
sign_in: {
ns: { enabled: true, method: 1, last_date: "" },
df: { enabled: true, method: 1, last_date: "" }
}
},
meta: {
sign_in: {
label: "自动签到", group: "基本设置",
fields: { method: { type: "RADIO", label: "签到方式", valueType: "number", options: [{ value: 1, text: "随机🍗" }, { value: 2, text: "5个🍗" }] } },
hidden: ["last_date"]
}
},
match: ctx => ctx.site && ctx.loggedIn && ctx.store.get(`sign_in.${ctx.site.code}.enabled`, true),
async init(ctx) {
const code = ctx.site.code;
const method = ctx.store.get(`sign_in.${code}.method`, 0);
const now = (() => {
const off = new Date().getTimezoneOffset() + 480;
const bj = new Date(Date.now() + off * 60000);
return `${bj.getFullYear()}/${bj.getMonth() + 1}/${bj.getDate()}`;
})();
if (ctx.store.get(`sign_in.${code}.last_date`) === now) return;
ctx.store.set(`sign_in.${code}.last_date`, now);
try {
const r = await net.post(`/api/attendance?random=${method === 1}`);
r?.success ? ctx.ui.success?.(`签到成功!+${r.gain}🍗,共${r.current}🍗`) : ctx.ui.info?.(r?.message || "签到失败");
} catch (e) { ctx.ui.info?.(e?.message || "签到错误"); }
}
};
const __vite_glob_0_15 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: signIn
}, Symbol.toStringTag, { value: 'Module' }));
// 签到提示
const CSS = `.nsplus-tip{background:rgba(255,217,0,.8);padding:3px;text-align:center;animation:blink 5s ease infinite}.nsplus-tip p,.nsplus-tip p a{color:#f00}.nsplus-tip p a:hover{color:#0ff}`;
const signinTips = {
id: "signinTips",
deps: ["ui"],
order: 82,
cfg: { signin_tips: { enabled: true } },
meta: { signin_tips: { label: "签到提示", group: "基本设置" } },
match(ctx) {
if (!ctx.site || !ctx.loggedIn || !ctx.store.get("signin_tips.enabled", true)) return false;
return ctx.store.get(`sign_in.${ctx.site.code}.enabled`, true) === false;
},
init(ctx) {
addStyle("nsx-signtip", CSS);
const code = ctx.site.code;
const now = (() => { const d = new Date(Date.now() + (new Date().getTimezoneOffset() + 480) * 6e4); return `${d.getFullYear()}/${d.getMonth() + 1}/${d.getDate()}`; })();
if (now === ctx.store.get(`sign_in.${code}.ignore_date`) || now === ctx.store.get(`sign_in.${code}.last_date`)) return;
const header = $("header");
if (!header) return;
const tip = document.createElement("div");
tip.className = "nsplus-tip";
tip.innerHTML = `<p>今天还没签到!【<a class="nsx-sign" data-r="1">随机🍗</a>】【<a class="nsx-sign" data-r="0">5个🍗</a>】【<a class="nsx-ign">今天不提示</a>】</p>`;
header.appendChild(tip);
$$(".nsx-sign", tip).forEach(a => a.onclick = async e => {
e.preventDefault();
try {
const r = await net.post(`/api/attendance?random=${a.dataset.r === "1"}`);
r?.success ? ctx.ui.success?.(`签到成功!+${r.gain}🍗`) : ctx.ui.info?.(r?.message || "签到失败");
} catch (e) { ctx.ui.warning?.(e?.message || "失败"); }
tip.remove();
ctx.store.set(`sign_in.${code}.last_date`, now);
});
$(".nsx-ign", tip).onclick = e => { e.preventDefault(); tip.remove(); ctx.store.set(`sign_in.${code}.ignore_date`, now); };
}
};
const __vite_glob_0_16 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: signinTips
}, Symbol.toStringTag, { value: 'Module' }));
// 平滑滚动
const smoothScroll = {
id: "smoothScroll",
order: 340,
cfg: { smooth_scroll: { enabled: true } },
meta: { smooth_scroll: { label: "平滑滚动", group: "显示设置" } },
match: ctx => ctx.store.get("smooth_scroll.enabled", true),
init() {
addStyle("nsx-smooth", "html{scroll-behavior:smooth}");
}
};
const __vite_glob_0_17 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: smoothScroll
}, Symbol.toStringTag, { value: 'Module' }));
// 用户卡片扩展 - 跨标签页同步未读消息
class Broadcast {
static ins = new Map();
constructor(name) {
if (Broadcast.ins.has(name)) return Broadcast.ins.get(name);
this.myId = `${Date.now()}-${Math.random()}`;
this.recv = [];
this.KEY = `nsx_tab_${name}`;
try { this.ch = new BroadcastChannel(name); this.ch.onmessage = e => this.recv.forEach(f => f(e.data)); } catch { this.ch = null; }
addEventListener("storage", e => { if (e.key === this.KEY) { e.newValue || localStorage.setItem(this.KEY, this.myId); this._up(); } });
addEventListener("beforeunload", () => { if (this.active) localStorage.removeItem(this.KEY); });
localStorage.setItem(this.KEY, this.myId);
this._up();
Broadcast.ins.set(name, this);
}
_up() { this.active = localStorage.getItem(this.KEY) === this.myId; }
on(fn) { this.recv.push(fn); }
send(data) { if (!this.ch) return; const m = { sender: this.myId, data }; this.ch.postMessage(m); this.recv.forEach(f => f(m)); }
task(fn, ms) { setInterval(async () => { if (!this.active) return; try { const d = await fn(); if (d !== undefined) this.send(d); } catch { } }, ms); }
}
const userCardExt = {
id: "userCardExt",
order: 200,
cfg: { user_card_ext: { enabled: true } },
meta: { user_card_ext: { label: "用户卡片扩展", group: "显示设置" } },
match: ctx => ctx.loggedIn && (ctx.isPost || ctx.isList) && ctx.store.get("user_card_ext.enabled", true),
async init(ctx) {
const bn = new Broadcast("nsx_notify");
const card = $(".user-card .user-stat");
const last = card?.querySelector(".stat-block:first-child > :last-child");
if (!card || !last) return;
const atEl = last.cloneNode(true), msgEl = last.cloneNode(true);
last.after(atEl);
card.querySelector(".stat-block:last-child")?.append(msgEl);
const up = (el, href, icon, text, cnt) => {
const a = el.querySelector("a");
if (!a) return;
a.href = href;
el.querySelector("a svg use")?.setAttribute("href", icon);
const t = el.querySelector("a > :nth-child(2)");
if (t) t.textContent = `${text} `;
const c = el.querySelector("a > :last-child");
if (c) { c.textContent = cnt; c.classList.toggle("notify-count", cnt > 0); }
};
const upAll = c => { up(atEl, "/notification#/atMe", "#at-sign", "我", c.atMe); up(msgEl, "/notification#/message?mode=list", "#envelope-one", "私信", c.message); up(last, "/notification#/reply", "#remind-6nce9p47", "回复", c.reply); };
bn.on(({ data }) => { if (data?.type === "unreadCount" && data.counts) upAll(data.counts); });
bn.send({ type: "unreadCount", counts: ctx.user?.unViewedCount || {}, timestamp: Date.now() });
bn.task(async () => {
const r = await fetch("/api/notification/unread-count", { credentials: "include" });
if (!r.ok) throw 0;
const d = await r.json();
if (d?.success && d.unreadCount) return { type: "unreadCount", counts: d.unreadCount, timestamp: Date.now() };
throw 0;
}, 5000);
}
};
const __vite_glob_0_18 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: userCardExt
}, Symbol.toStringTag, { value: 'Module' }));
// ===== SVG 图标 =====
const SVG_SPRITE = `<svg xmlns="http://www.w3.org/2000/svg" style="display:none">
<symbol id="copy" viewBox="0 0 48 48"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" d="M13 12.432v-4.62A2.813 2.813 0 0 1 15.813 5h24.374A2.813 2.813 0 0 1 43 7.813v24.375A2.813 2.813 0 0 1 40.188 35h-4.672M7.813 13h24.374A2.813 2.813 0 0 1 35 15.813v24.374A2.813 2.813 0 0 1 32.188 43H7.813A2.813 2.813 0 0 1 5 40.188V15.813A2.813 2.813 0 0 1 7.813 13Z"/></symbol>
<symbol id="check" viewBox="0 0 48 48"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" d="m4 24 5-5 10 10L39 9l5 5-25 25L4 24Z"/></symbol>
<symbol id="history" viewBox="0 0 48 48"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"><path d="M5.818 6.727V14h7.273"/><path d="M4 24c0 11.046 8.954 20 20 20s20-8.954 20-20S35.046 4 24 4c-7.32 0-13.715 3.932-17.192 9.8"/><path d="M24 12v14l9.33 9.33"/></g></symbol>
<symbol id="comments" viewBox="0 0 48 48"><g fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="4"><path d="M44 6H4v30h8.5v7l9-7H44V6Z"/><path stroke-linecap="round" d="M14 19.5h20M14 27.5h12"/></g></symbol>
<symbol id="at-sign" viewBox="0 0 48 48"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"><path d="M24 44c11.046 0 20-8.954 20-20S35.046 4 24 4 4 12.954 4 24s8.954 20 20 20"/><path d="M32 24c0 4.418-3.582 10-8 10s-8-5.582-8-10 3.582-8 8-8 8 3.582 8 8m0 0v10c0 3 3 6 6 6"/></g></symbol>
<symbol id="envelope-one" viewBox="0 0 48 48"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"><path d="M4 39h40V9H4z"/><path d="m4 9 20 15L44 9"/></g></symbol>
<symbol id="remind-6nce9p47" viewBox="0 0 48 48"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"><path d="M24 44c1.387 0 2.732-.123 4.023-.357M44 24a20 20 0 0 0-40 0c0 4.59 1.55 8.82 4.157 12.194L4 44l7.806-4.157A19.9 19.9 0 0 0 24 44a20 20 0 0 0 4.023-.357"/><path d="M33.805 40a6 6 0 1 0 5.857-9.805"/></g></symbol>
<symbol id="down" viewBox="0 0 48 48"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" d="m36 18-12 12-12-12"/></symbol>
</svg>`;
// ===== 基础 CSS =====
const BASE_CSS = `.blocked-post{display:none!important}#back-to-comment{display:flex}#fast-nav-button-group .nav-item-btn:nth-last-child(4){bottom:120px}header div.history-dropdown-on{color:var(--link-hover-color);cursor:pointer;padding:0 5px;position:absolute;right:50px}.post-list .post-title a:visited{color:var(--link-visited-color)}:root{--link-visited-color:#afb9c1}body.dark-layout{--link-visited-color:#393f4e}.msc-overlay{background-color:var(--bg-sub-color)}`;
// ===== Observer =====
class Observer {
constructor() { this.listeners = []; this.mo = null; }
watch(sel, fn, opts = {}) {
this.listeners.push({ sel, fn, opts });
if (!this.mo) {
this.mo = new MutationObserver(debounce(() => this._run(), 50));
this.mo.observe(document.body, { childList: true, subtree: true });
}
}
_run() {
this.listeners.forEach(({ sel, fn, opts }) => {
const els = $$(sel);
if (els.length) fn(els, opts);
});
}
}
// ===== 创建 ctx =====
function createCtx(obs) {
const uw = typeof unsafeWindow !== "undefined" ? unsafeWindow : window;
return {
env, $, $$, addStyle, store, net,
uw,
get loggedIn() { return !!uw?.__config__?.user; },
get user() { return uw?.__config__?.user; },
get uid() { return uw?.__config__?.user?.member_id; },
site: env.site,
isPost: /^\/post-/.test(location.pathname),
isList: /^\/(categories\/|page|award|search|$)/.test(location.pathname),
watch: obs.watch.bind(obs),
ui: {}
};
}
// ===== 启动 =====
function start() {
// 注入资源
document.body?.insertAdjacentHTML("beforeend", SVG_SPRITE);
addStyle("nsx-base", BASE_CSS);
// layui CSS
addStyle("nsx-layui-css", "https://s.cfn.pp.ua/layui/2.9.9/css/layui.css");
// highlight.js 脚本
addScript("nsx-hljs-script", "https://s4.zstatic.net/ajax/libs/highlight.js/11.9.0/highlight.min.js");
// highlight.js 样式
addStyle("hightlight-style", GM_getResourceURL("highlightStyle"));
// hljs 初始化
addScript("nsx-hljs-onload", `(()=>{const r=()=>{if(window.hljs&&typeof hljs.highlightAll==="function")hljs.highlightAll()};document.readyState==="complete"?r():window.addEventListener("load",r,{once:true})})()`);
// 加载模块
const mods = /* #__PURE__ */ Object.assign({"./features/autoJump.js": __vite_glob_0_0,"./features/autoLoading.js": __vite_glob_0_1,"./features/blockMembers.js": __vite_glob_0_2,"./features/blockPosts.js": __vite_glob_0_3,"./features/blockViewLevel.js": __vite_glob_0_4,"./features/callout.js": __vite_glob_0_5,"./features/codeHighlight.js": __vite_glob_0_6,"./features/commentShortcut.js": __vite_glob_0_7,"./features/darkMode.js": __vite_glob_0_8,"./features/history.js": __vite_glob_0_9,"./features/imageSlide.js": __vite_glob_0_10,"./features/instantPage.js": __vite_glob_0_11,"./features/levelTag.js": __vite_glob_0_12,"./features/menus.js": __vite_glob_0_13,"./features/quickComment.js": __vite_glob_0_14,"./features/signIn.js": __vite_glob_0_15,"./features/signinTips.js": __vite_glob_0_16,"./features/smoothScroll.js": __vite_glob_0_17,"./features/userCardExt.js": __vite_glob_0_18});
Object.values(mods).forEach(m => m.default && define(m.default));
// 创建 Observer & ctx
const obs = new Observer();
const ctx = createCtx(obs);
// 初始化 UI (依赖 layui)
const initUI = () => {
if (!window.layui?.layer) return (ctx.ui = {});
const layer = window.layui.layer, uw = ctx.uw;
ctx.ui = {
layer,
toast: (text, style) => { const idx = layer.msg(text, { offset: 't', area: ['100%', 'auto'], anim: 'slideDown' }); layer.style(idx, Object.assign({ opacity: 0.9 }, style)); return idx; },
info: msg => ctx.ui.toast(msg, { "background-color": "#4D82D6" }),
success: msg => ctx.ui.toast(msg, { "background-color": "#57BF57" }),
warning: msg => ctx.ui.toast(msg, { "background-color": "#D6A14D" }),
error: msg => ctx.ui.toast(msg, { "background-color": "#E1715B" }),
alert: (t, c, fn) => uw?.mscAlert ? (c === undefined ? uw.mscAlert(t) : uw.mscAlert(t, c)) : layer.alert(c, { title: t, icon: 0, btn: ["确定"] }, fn),
confirm: (t, c, y, n) => uw?.mscConfirm ? uw.mscConfirm(t, c, y, n) : layer.confirm(c, { title: t, icon: 0, btn: ["确定", "取消"] }, y, n),
tips: (msg, el, opts) => layer.tips(msg, el, opts)
};
};
initUI();
if (!ctx.ui.layer) {
const timer = setInterval(() => { if (window.layui?.layer) { initUI(); clearInterval(timer); } }, 100);
setTimeout(() => clearInterval(timer), 5000);
}
// 启动所有模块
boot(ctx);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", start);
} else {
start();
}
})();