navmini/xhs/tool/app.js
eric 518410edd0 feat: 游民导航 H5、微信小程序与小红书小工具
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-27 12:15:00 -05:00

471 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* 游民导航 · 小红书小工具(纯离线 classic script) */
(function () {
"use strict";
var data = window.NAV_DATA || {};
var categories = data.categories || [];
var state = {
theme: "light",
activeId: categories[0] ? categories[0].id : "",
drawerOpen: false,
sheetOpen: false,
currentLink: null,
jumping: false,
};
var els = {};
function $(id) {
return document.getElementById(id);
}
function qs(sel, root) {
return (root || document).querySelector(sel);
}
function escapeHtml(str) {
return String(str == null ? "" : str)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function applySafeArea() {
try {
var root = document.documentElement;
var probe = document.createElement("div");
probe.style.cssText =
"position:absolute;visibility:hidden;pointer-events:none;padding-top:constant(safe-area-inset-top);padding-top:env(safe-area-inset-top);";
document.body.appendChild(probe);
var envTop = parseFloat(window.getComputedStyle(probe).paddingTop) || 0;
document.body.removeChild(probe);
// 小红书宿主顶栏(返回 / 分享)常盖住内容;Android 上 env 常为 0
var status = envTop > 0 ? envTop : 28;
var hostBar = 44;
var padTop = Math.max(status + hostBar, 72);
root.style.setProperty("--app-pad-top", padTop + "px");
root.style.setProperty("--safe-area-inset-top", status + "px");
} catch (e) {
document.documentElement.style.setProperty("--app-pad-top", "80px");
}
}
function readTheme() {
try {
var saved = localStorage.getItem("navmini-xhs-theme");
if (saved === "light" || saved === "dark") return saved;
} catch (e) {}
return "light";
}
function setTheme(theme) {
state.theme = theme;
document.documentElement.setAttribute("data-theme", theme);
try {
localStorage.setItem("navmini-xhs-theme", theme);
} catch (e) {}
if (els.themeIcon) {
els.themeIcon.textContent = theme === "dark" ? "☀" : "☾";
}
}
function showToast(msg) {
if (!els.toast) return;
els.toast.textContent = msg;
els.toast.classList.add("show");
clearTimeout(showToast._t);
showToast._t = setTimeout(function () {
els.toast.classList.remove("show");
}, 1600);
}
/** 选中网址文本(容器禁用程序化写入剪贴板,仅做文本选中) */
function selectUrlText() {
try {
var node = els.sheetUrlBox;
if (!node) return false;
var range = document.createRange();
range.selectNodeContents(node);
var sel = window.getSelection();
if (!sel) return false;
sel.removeAllRanges();
sel.addRange(range);
return true;
} catch (e) {
return false;
}
}
function setGuideStep(step) {
var items = els.guideSteps
? els.guideSteps.querySelectorAll(".guide-step")
: [];
for (var i = 0; i < items.length; i++) {
var n = i + 1;
items[i].classList.toggle("is-active", n === step);
items[i].classList.toggle("is-done", n < step);
}
}
function setGuideUi(phase) {
// phase: idle | selected
var selected = phase === "selected";
if (els.sheetUrlBox) {
els.sheetUrlBox.classList.toggle("is-pulse", !selected);
els.sheetUrlBox.classList.toggle("is-selected", selected);
}
if (els.sheetSelect) {
els.sheetSelect.classList.toggle("pulse", !selected);
els.sheetSelect.classList.toggle("is-done", selected);
}
if (els.sheetSelectLabel) {
els.sheetSelectLabel.textContent = selected
? "② 已选中 · 再点可重选"
: "① 点我选中网址";
}
if (els.sheetSelectSub) {
els.sheetSelectSub.textContent = selected
? "下一步:手指按住蓝色网址区"
: "选中后请按住网址复制";
}
// 第一步:只显示向下指按钮的小手;第二步:小手上移指网址,文案在右边
if (els.fingerBtn) {
els.fingerBtn.classList.toggle("is-hidden", selected);
els.fingerBtn.setAttribute("aria-hidden", selected ? "true" : "false");
}
if (els.fingerUrl) {
els.fingerUrl.classList.toggle("is-hidden", !selected);
els.fingerUrl.setAttribute("aria-hidden", selected ? "false" : "true");
}
if (els.sheetUrlWrap) {
els.sheetUrlWrap.classList.toggle("is-pointing", selected);
}
setGuideStep(selected ? 2 : 1);
}
function findCategory(id) {
for (var i = 0; i < categories.length; i++) {
if (categories[i].id === id) return categories[i];
}
return categories[0] || null;
}
function countLinks(cat) {
var n = 0;
var groups = cat.groups || [];
for (var i = 0; i < groups.length; i++) {
n += (groups[i].links || []).length;
}
return n;
}
function renderDrawer() {
var html = "";
for (var i = 0; i < categories.length; i++) {
var cat = categories[i];
var active = cat.id === state.activeId ? " active" : "";
html +=
'<button type="button" class="drawer-item' +
active +
'" data-cat="' +
escapeHtml(cat.id) +
'">' +
'<span class="drawer-emoji" aria-hidden="true">' +
escapeHtml(cat.emoji || "·") +
"</span>" +
'<span class="drawer-item-title">' +
escapeHtml(cat.title) +
"</span>" +
'<span class="drawer-item-count">' +
countLinks(cat) +
"</span>" +
"</button>";
}
els.drawerNav.innerHTML = html;
}
function renderContent() {
var html = "";
for (var i = 0; i < categories.length; i++) {
var cat = categories[i];
html +=
'<section class="cat-panel" id="cat-' +
escapeHtml(cat.id) +
'">' +
'<header class="cat-heading"><h2>' +
escapeHtml(cat.title) +
'</h2><span class="cat-count">' +
countLinks(cat) +
"</span></header>";
var groups = cat.groups || [];
for (var g = 0; g < groups.length; g++) {
var group = groups[g];
html +=
'<div class="group-block"><h3 class="group-title">' +
escapeHtml(group.title) +
'</h3><div class="link-grid">';
var links = group.links || [];
for (var l = 0; l < links.length; l++) {
var link = links[l];
html +=
'<button type="button" class="link-card" data-cat="' +
escapeHtml(cat.id) +
'" data-group="' +
escapeHtml(group.id) +
'" data-id="' +
escapeHtml(link.id) +
'">' +
'<span class="link-icon-wrap"><img class="link-icon" src="' +
escapeHtml(link.icon) +
'" alt="" width="34" height="34" loading="lazy" /></span>' +
'<span class="link-meta"><span class="link-title">' +
escapeHtml(link.title) +
'</span><span class="link-desc">' +
escapeHtml(link.subtitle || "") +
"</span></span></button>";
}
html += "</div></div>";
}
html += "</section>";
}
els.content.innerHTML = html;
}
function setDrawer(open) {
state.drawerOpen = open;
els.drawer.classList.toggle("open", open);
els.drawer.setAttribute("aria-hidden", open ? "false" : "true");
document.body.style.overflow = open || state.sheetOpen ? "hidden" : "";
}
function setSheet(open) {
state.sheetOpen = open;
els.sheet.classList.toggle("open", open);
els.sheet.setAttribute("aria-hidden", open ? "false" : "true");
document.body.style.overflow = open || state.drawerOpen ? "hidden" : "";
}
function findLink(catId, groupId, linkId) {
var cat = findCategory(catId);
if (!cat) return null;
var groups = cat.groups || [];
for (var i = 0; i < groups.length; i++) {
if (groups[i].id !== groupId) continue;
var links = groups[i].links || [];
for (var j = 0; j < links.length; j++) {
if (links[j].id === linkId) return links[j];
}
}
return null;
}
function openLink(link) {
// 容器禁止打开外链 / 跳转其他小工具(见能力清单 §4.2),一律走复制引导
openLinkSheet(link);
}
function openLinkSheet(link) {
state.currentLink = link;
els.sheetIcon.src = link.icon || "./icons/_brand.webp";
els.sheetTitle.textContent = link.title || "";
els.sheetSub.textContent = link.subtitle || "";
els.sheetUrlBox.textContent = link.href || "";
els.sheetStatus.textContent = "按步骤操作:先点绿色按钮选中网址";
els.sheetStatus.classList.remove("is-error");
setGuideUi("idle");
setSheet(true);
}
function markSelected() {
if (!state.currentLink) return;
if (selectUrlText()) {
setGuideUi("selected");
els.sheetStatus.textContent =
"已选中蓝色区域 · 请用手指按住网址,在菜单里点「复制」";
els.sheetStatus.classList.remove("is-error");
showToast("已选中,请按住复制");
// 短暂强调第 3 步
setTimeout(function () {
setGuideStep(3);
var s2 = els.guideSteps && els.guideSteps.querySelector('[data-step="2"]');
if (s2) s2.classList.add("is-done");
}, 1600);
} else {
setGuideUi("idle");
els.sheetStatus.textContent = "自动选中失败,请手指直接按住蓝色网址文字";
els.sheetStatus.classList.add("is-error");
setGuideStep(2);
}
}
function jumpToCategory(id) {
state.activeId = id;
setPill();
renderDrawer();
setDrawer(false);
state.jumping = true;
var el = document.getElementById("cat-" + id);
if (el) {
el.scrollIntoView({ behavior: "smooth", block: "start" });
}
setTimeout(function () {
state.jumping = false;
}, 700);
}
function setPill() {
var cat = findCategory(state.activeId);
if (els.brandCurrent && cat) {
els.brandCurrent.textContent = cat.title;
}
}
function onScroll() {
var y = window.scrollY || document.documentElement.scrollTop || 0;
els.backTop.classList.toggle("show", y > 480);
if (state.jumping) return;
var marker = 100;
var current = categories[0] ? categories[0].id : "";
for (var i = 0; i < categories.length; i++) {
var el = document.getElementById("cat-" + categories[i].id);
if (!el) continue;
if (el.getBoundingClientRect().top <= marker) {
current = categories[i].id;
} else {
break;
}
}
if (current && current !== state.activeId) {
state.activeId = current;
setPill();
renderDrawer();
}
}
function bind() {
els.btnMenu.addEventListener("click", function () {
setDrawer(true);
});
els.drawerMask.addEventListener("click", function () {
setDrawer(false);
});
els.drawerClose.addEventListener("click", function () {
setDrawer(false);
});
els.btnTheme.addEventListener("click", function () {
setTheme(state.theme === "dark" ? "light" : "dark");
});
els.backTop.addEventListener("click", function () {
window.scrollTo({ top: 0, behavior: "smooth" });
});
els.drawerNav.addEventListener("click", function (e) {
var btn = e.target.closest(".drawer-item");
if (!btn) return;
jumpToCategory(btn.getAttribute("data-cat"));
});
els.content.addEventListener("click", function (e) {
var btn = e.target.closest(".link-card");
if (!btn) return;
var link = findLink(
btn.getAttribute("data-cat"),
btn.getAttribute("data-group"),
btn.getAttribute("data-id")
);
if (link) openLink(link);
});
els.sheetMask.addEventListener("click", function () {
setSheet(false);
});
els.sheetClose.addEventListener("click", function () {
setSheet(false);
});
els.sheetSelect.addEventListener("click", function () {
markSelected();
});
els.sheetUrlBox.addEventListener("click", function () {
markSelected();
});
// 用户开始触碰网址区时,推进到第 2/3 步提示
els.sheetUrlBox.addEventListener("touchstart", function () {
if (els.sheetUrlBox.classList.contains("is-selected")) {
setGuideStep(3);
var s2 = els.guideSteps && els.guideSteps.querySelector('[data-step="2"]');
if (s2) s2.classList.add("is-done");
els.sheetStatus.textContent = "按住不放,在弹出菜单中点「复制」";
}
}, { passive: true });
window.addEventListener("scroll", onScroll, { passive: true });
}
function init() {
els = {
brandName: $("brand-name"),
brandCurrent: $("brand-current"),
brandLogo: $("brand-logo"),
footer: $("footer"),
content: $("content"),
drawer: $("drawer"),
drawerNav: $("drawer-nav"),
drawerMask: $("drawer-mask"),
drawerClose: $("drawer-close"),
drawerBrand: $("drawer-brand"),
drawerSub: $("drawer-sub"),
btnMenu: $("btn-menu"),
btnTheme: $("btn-theme"),
themeIcon: $("theme-icon"),
backTop: $("back-top"),
sheet: $("sheet"),
sheetMask: $("sheet-mask"),
sheetIcon: $("sheet-icon"),
sheetTitle: $("sheet-title"),
sheetSub: $("sheet-sub"),
sheetUrlBox: $("sheet-url-box"),
sheetStatus: $("sheet-status"),
sheetSelect: $("sheet-select"),
sheetSelectLabel: $("sheet-select-label"),
sheetSelectSub: $("sheet-select-sub"),
sheetClose: $("sheet-close"),
guideSteps: $("guide-steps"),
sheetUrlWrap: $("sheet-url-wrap"),
fingerBtn: $("finger-btn"),
fingerUrl: $("finger-url"),
toast: $("toast"),
};
applySafeArea();
setTheme(readTheme());
var brand = data.brand || "游民导航";
els.brandName.textContent = brand;
els.footer.textContent = data.footer || "";
els.drawerBrand.textContent = brand;
els.drawerSub.textContent = data.tagline || "";
if (data.brandIcon && els.brandLogo) {
els.brandLogo.src = data.brandIcon;
}
renderDrawer();
renderContent();
setPill();
bind();
onScroll();
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();