330 lines
11 KiB
JavaScript
330 lines
11 KiB
JavaScript
(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, button"), ...(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 = !document.body.classList.contains("page-home-portal") &&
|
|
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 = "";
|
|
});
|
|
});
|
|
})();
|