(function () { const blockedNavigationProtocol = /^(?:javascript|data|vbscript|mailto|tel):/i; const publicUserKey = "genealogy_public_user_v1"; let publicApiPromise; function getStorage() { try { return window.localStorage; } catch (error) { return null; } } function normalizeAvatarUrl(value) { try { const url = new URL(String(value || "").trim()); return url.protocol === "https:" || url.protocol === "http:" ? url.href : ""; } catch (error) { return ""; } } function normalizePublicUser(profile) { const nickName = String(profile && profile.nickName || "").trim(); const avatarUrl = normalizeAvatarUrl( profile && (profile.avatarUrl || profile.avatarFile && profile.avatarFile.accessUrl) ); if (!nickName) return null; return { nickName, avatarUrl }; } function readPublicUser() { const storage = getStorage(); if (!storage) return null; try { return normalizePublicUser(JSON.parse(storage.getItem(publicUserKey) || "null")); } catch (error) { return null; } } function getAuthToken() { const storage = getStorage(); try { return storage && storage.getItem("genealogy_auth_token") || ""; } catch (error) { return ""; } } function createUserEntry(user) { const link = document.createElement("a"); const avatar = document.createElement("span"); const fallback = document.createElement("span"); const name = document.createElement("span"); const initial = Array.from(user.nickName)[0] || "家"; link.className = "nav-user-entry"; link.href = "profile.html"; link.setAttribute("aria-label", `${user.nickName}的个人中心`); avatar.className = "nav-user-avatar"; fallback.className = "nav-user-avatar-fallback"; fallback.textContent = initial; avatar.appendChild(fallback); if (user.avatarUrl) { const image = document.createElement("img"); image.src = user.avatarUrl; image.alt = ""; image.loading = "lazy"; image.decoding = "async"; image.addEventListener("error", () => image.remove()); avatar.prepend(image); } name.className = "nav-user-name"; name.textContent = user.nickName; link.append(avatar, name); return link; } function loadScript(src) { return new Promise((resolve, reject) => { const existing = document.querySelector(`script[src="${src}"]`); if (existing && existing.dataset.loaded === "true") { resolve(); return; } if (existing) { resolve(); return; } const script = existing || document.createElement("script"); script.addEventListener("load", () => { script.dataset.loaded = "true"; resolve(); }, { once: true }); script.addEventListener("error", reject, { once: true }); if (!existing) { script.src = src; document.head.appendChild(script); } }); } function getPublicApi() { if (window.GenealogyApi && window.GenealogyApi.defaultClient) { return Promise.resolve(window.GenealogyApi.defaultClient); } if (!publicApiPromise) { publicApiPromise = [ "utils/StorageUtil.js", "utils/axios.js", "utils/AxiosRequestUtil.js", "utils/ApiClient.js" ].reduce((ready, src) => ready.then(() => loadScript(src)), Promise.resolve()) .then(() => window.GenealogyApi && window.GenealogyApi.defaultClient); } return publicApiPromise; } function shouldOpenInNewWindow(link) { if (!link || link.tagName !== "A" || link.hasAttribute("download")) return false; const href = (link.getAttribute("href") || "").trim(); return Boolean(href && href !== "#" && !href.startsWith("#") && !blockedNavigationProtocol.test(href)); } function prepareLink(link) { if (!shouldOpenInNewWindow(link)) return; if (link.classList.contains("brand") && link.getAttribute("href") !== "index.html") { link.setAttribute("href", "index.html"); } if (link.target !== "_blank") link.target = "_blank"; const relValues = (link.getAttribute("rel") || "").split(/\s+/).filter(Boolean); if (!relValues.includes("noopener")) { link.setAttribute("rel", [...relValues, "noopener"].join(" ")); } } function prepareLinks(container) { if (container.nodeType !== 1) return; if (container.matches("a[href]")) prepareLink(container); container.querySelectorAll("a[href]").forEach(prepareLink); } function installNewWindowNavigation() { prepareLinks(document.documentElement); new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === "attributes") { prepareLink(mutation.target); return; } mutation.addedNodes.forEach(prepareLinks); }); }).observe(document.documentElement, { subtree: true, childList: true, attributes: true, attributeFilter: ["href", "target", "download"] }); } function syncPublicNavigation() { const token = getAuthToken(); const user = readPublicUser() || { nickName: "我的账户", avatarUrl: "" }; if (!token) return; document.querySelectorAll(".nav-actions").forEach((actions) => { const loginLink = actions.querySelector('a[href="login.html"]'); const registerLink = actions.querySelector('a[href="register.html"]'); const accountLink = actions.querySelector('a[href="profile-data.html"]'); const currentUserLink = actions.querySelector(".nav-user-entry"); const createLink = actions.querySelector('a[href="create-genealogy.html"]'); const userEntry = createUserEntry(user); const insertionPoint = currentUserLink || loginLink || accountLink || createLink; if (insertionPoint) insertionPoint.replaceWith(userEntry); else actions.prepend(userEntry); if (loginLink && loginLink.isConnected) loginLink.remove(); if (accountLink && accountLink.isConnected) accountLink.remove(); if (currentUserLink && currentUserLink.isConnected) currentUserLink.remove(); if (registerLink) registerLink.remove(); if (createLink) createLink.href = "profile-create-family.html"; }); } window.PublicNavigation = { storeUser: function (profile) { const storage = getStorage(); const user = normalizePublicUser(profile); if (!storage || !user) return false; storage.setItem(publicUserKey, JSON.stringify(user)); syncPublicNavigation(); return true; }, clearUser: function () { const storage = getStorage(); if (storage) storage.removeItem(publicUserKey); } }; async function refreshPublicUser() { if (!getAuthToken() || readPublicUser() || document.body.matches(".page-profile, .page-profile-module")) return; try { const api = await getPublicApi(); if (api) window.PublicNavigation.storeUser(await api.currentProfile()); } catch (error) { // 公共页保持可用的账户入口;资料接口恢复后,登录或进入个人中心会重新同步。 window.PublicNavigation.clearUser(); } } function initSiteMenu() { const body = document.body; const header = document.querySelector(".site-header"); const nav = header && header.querySelector(".nav"); const navLinks = nav && nav.querySelector(".nav-links"); const navActions = nav && nav.querySelector(".nav-actions"); if (!body || body.matches(".page-profile, .page-profile-module")) return; if (!header || !nav || !navLinks || nav.querySelector("[data-site-menu-toggle]")) return; const menuId = "site-mobile-menu"; const toggle = document.createElement("button"); const menu = document.createElement("div"); const inner = document.createElement("div"); toggle.className = "site-menu-toggle"; toggle.type = "button"; toggle.textContent = "菜单"; toggle.setAttribute("data-site-menu-toggle", ""); toggle.setAttribute("aria-expanded", "false"); toggle.setAttribute("aria-controls", menuId); menu.className = "site-mobile-menu"; menu.id = menuId; menu.hidden = true; menu.setAttribute("data-site-mobile-menu", ""); menu.setAttribute("aria-hidden", "true"); inner.className = "container site-mobile-menu-inner"; [...navLinks.querySelectorAll("a"), ...(navActions ? navActions.querySelectorAll("a") : [])].forEach((link) => { inner.appendChild(link.cloneNode(true)); }); menu.appendChild(inner); nav.appendChild(toggle); header.appendChild(menu); body.classList.add("has-site-mobile-menu"); const setOpen = (isOpen) => { menu.hidden = !isOpen; menu.classList.toggle("is-open", isOpen); menu.setAttribute("aria-hidden", isOpen ? "false" : "true"); toggle.setAttribute("aria-expanded", isOpen ? "true" : "false"); }; toggle.addEventListener("click", (event) => { event.stopPropagation(); setOpen(toggle.getAttribute("aria-expanded") !== "true"); }); menu.addEventListener("click", (event) => { if (event.target.closest("a")) setOpen(false); event.stopPropagation(); }); document.addEventListener("click", () => setOpen(false)); document.addEventListener("keydown", (event) => { if (event.key !== "Escape" || menu.hidden) return; setOpen(false); toggle.focus(); }); } installNewWindowNavigation(); syncPublicNavigation(); initSiteMenu(); refreshPublicUser(); // 只在精确指针设备上启用微动效,触屏和减少动效偏好下保持静态。 const finePointer = window.matchMedia("(hover: hover) and (pointer: fine)").matches; const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; if (!finePointer || reduceMotion) return; document.querySelectorAll(".tilt-card").forEach((card) => { // 卡片倾斜角度通过 CSS 变量传递,具体视觉效果仍由 CSS 控制。 card.addEventListener("pointermove", (event) => { const rect = card.getBoundingClientRect(); const px = (event.clientX - rect.left) / rect.width - 0.5; const py = (event.clientY - rect.top) / rect.height - 0.5; card.style.setProperty("--tilt-y", `${px * 5}deg`); card.style.setProperty("--tilt-x", `${py * -5}deg`); }); card.addEventListener("pointerleave", () => { card.style.setProperty("--tilt-y", "0deg"); card.style.setProperty("--tilt-x", "0deg"); }); }); document.querySelectorAll(".magnetic").forEach((item) => { // 品牌 logo 不做磁吸,避免导航视觉跳动。 if (item.classList.contains("brand") || item.querySelector(".brand-logo")) return; item.addEventListener("pointermove", (event) => { const rect = item.getBoundingClientRect(); const x = event.clientX - rect.left - rect.width / 2; const y = event.clientY - rect.top - rect.height / 2; item.style.transform = `translate(${x * 0.08}px, ${y * 0.08}px)`; }); item.addEventListener("pointerleave", () => { item.style.transform = ""; }); }); })();