(function () { function querySelector(selector, root) { return (root || document).querySelector(selector); } function querySelectorAll(selector, root) { return (root || document).querySelectorAll(selector); } function normalizePageIdFromPath(pathname) { var path = pathname || ""; var noTrailing = path.replace(/\/+$/, ""); var trimmed = noTrailing || "/"; if (trimmed === "" || trimmed === "/") { return "index.html"; } var segments = trimmed.split("/").filter(Boolean); var filename = segments.length ? segments[segments.length - 1] : ""; if (!filename) { return "index.html"; } var lower = filename.toLowerCase(); if (lower === "index.html") { return "index.html"; } if (lower === "index") { return "index.html"; } if (lower.indexOf(".") === -1) { return lower + ".html"; } return lower; } function getCurrentPage() { var pathname = window.location && window.location.pathname ? window.location.pathname : ""; return normalizePageIdFromPath(pathname); } function normalizeHrefToPageId(href) { var trimmed = (href || "").replace(/^\s+|\s+$/g, ""); var pathname = trimmed; var hasProtocol = trimmed.indexOf("://") !== -1; var hasScheme = hasProtocol ? trimmed.slice(0, trimmed.indexOf("://")).toLowerCase() : ""; if (hasProtocol && hasScheme === "http") { return ""; } if (trimmed.indexOf("https://") === 0) { try { return normalizePageIdFromPath(new URL(trimmed).pathname); } catch (error) { return ""; } } pathname = trimmed.replace(/\/+$/, "").replace(/^\.\/+/, "").replace(/^\/+/, ""); if (!pathname) { return "index.html"; } return normalizePageIdFromPath("/" + pathname); } function linkNodeFromItem(item, isCurrent) { var link = document.createElement("a"); link.href = item.href; link.textContent = item.label; if (isCurrent) { link.setAttribute("aria-current", "page"); } return link; } function childLinksFromItem(item) { var children = Array.isArray(item.children) ? item.children : []; var result = []; var i; for (i = 0; i < children.length; i = i + 1) { if (children[i] && typeof children[i].href === "string" && typeof children[i].label === "string" && isSafeHref(children[i].href)) { result.push(children[i]); } } return result; } function isSafeHref(href) { var trimmed = (href || "").replace(/^\s+|\s+$/g, ""); var lower = trimmed.toLowerCase(); if (!trimmed) { return false; } if (lower.indexOf("javascript:") === 0 || lower.indexOf("vbscript:") === 0 || lower.indexOf("data:") === 0 || lower.indexOf("file:") === 0 || lower.indexOf("mailto:") === 0 || lower.indexOf("blob:") === 0 || lower.indexOf("ftp:") === 0 || lower.indexOf("//") === 0) { return false; } if (lower.indexOf("https://") === 0) { return true; } if (lower.indexOf("http://") === 0) { return false; } if (lower[0] === "#") { return false; } return /^[a-z0-9._/-]+\.html$/i.test(trimmed.replace(/^\.\/+/, "").replace(/^\/+/, "")); } function getSafeNavItems() { var content = window.YX_CONTENT; var result = []; var i; var item; var href; var label; if (!content || !Array.isArray(content.nav)) { return result; } for (i = 0; i < content.nav.length; i = i + 1) { item = content.nav[i]; href = item && item.href; label = item && item.label; if (!item || typeof href !== "string" || typeof label !== "string") { continue; } if (!isSafeHref(href)) { continue; } result.push(item); } return result; } function isItemCurrent(item, currentPage) { var pageId = normalizeHrefToPageId(item.href); var children = childLinksFromItem(item); var i; if (pageId === currentPage) { return true; } for (i = 0; i < children.length; i = i + 1) { if (normalizeHrefToPageId(children[i].href) === currentPage) { return true; } } return false; } function renderNav() { var navRoot = querySelector("[data-nav]"); var footerNavRoot = querySelector("[data-footer-nav]"); var navItems = getSafeNavItems(); if (!navItems.length || (!navRoot && !footerNavRoot)) { return; } var currentPage = getCurrentPage(); function renderInto(root) { if (!root) { return; } while (root.firstChild) { root.removeChild(root.firstChild); } var i; for (i = 0; i < navItems.length; i = i + 1) { var item = navItems[i]; var isCurrent = isItemCurrent(item, currentPage); var children = childLinksFromItem(item); if (children.length && root.classList.contains("site-nav")) { var group = document.createElement("div"); var menu = document.createElement("div"); var label = linkNodeFromItem(item, isCurrent); var j; group.className = "nav-group"; menu.className = "nav-submenu"; label.className = "nav-parent"; label.setAttribute("aria-haspopup", "true"); for (j = 0; j < children.length; j = j + 1) { menu.appendChild(linkNodeFromItem(children[j], normalizeHrefToPageId(children[j].href) === currentPage)); } group.appendChild(label); group.appendChild(menu); root.appendChild(group); } else { root.appendChild(linkNodeFromItem(item, isCurrent)); } } } renderInto(navRoot); renderInto(footerNavRoot); } /** * 资源根地址。 * * 部署流程会把 HTML 里的资源路径改写成 CDN 绝对地址(如 * //cdn.example.com/site/assets/styles-x.css),但不会改写 JS 字符串里的 * 路径。页脚二维码的路径写在 content.js 里,若直接使用就会相对当前域名 * 解析、指向不存在的地址而 404。 * * 这里从任一已被改写的资源地址反推资源根,让 JS 里的相对路径也落到同一 * 位置。本地开发时样式表是 "assets/css/styles.css"(前缀为 0),返回空串, * 行为与改动前一致。 */ function assetBase() { var link = document.querySelector('link[href*="assets/"]'); if (!link) { return ""; } var href = link.getAttribute("href") || ""; var index = href.indexOf("assets/"); return index > 0 ? href.slice(0, index) : ""; } /** * 修正 上的资源链接(论文、白皮书等文档)。 * * 构建与部署流程只改写 Vite 处理过的资源路径, 里的相对路径不会 * 被改写,线上会相对当前域名解析而 404。这里在运行时统一补上资源根。 * 已被改写为绝对地址的链接不匹配选择器,因此不会重复加前缀。 */ /** * 兜底加载 ip-chat.js(品牌 IP 咨询对话)。 * * 线上出现过同目录下 content.js / site.js 正常、ip-chat.js 却 404 的情况 * (部署时漏传单个文件),导致 IP 点击后没有问答面板。这里在页面加载完成 * 后检测就绪标记,缺失则按资源根重新加载一次。 */ function ensureIpChat() { var float = document.querySelector("[data-ip-float]"); if (!float) { return; } // 判断依据用 ip-chat.js 加载成功后必然产生的副作用(给形象加 role="button"), // 而不是仅在最新版里才有的标记 —— 否则线上是旧版时会重复注入第二次。 var figure = float.querySelector(".yx-ip-figure"); if (window.YX_IP_CHAT_READY || (figure && figure.getAttribute("role") === "button")) { return; } var script = document.createElement("script"); script.src = assetBase() + "assets/js/ip-chat.js"; script.async = false; document.body.appendChild(script); } function fixAssetLinks() { var base = assetBase(); if (!base) { return; } var links = document.querySelectorAll('a[href^="assets/"], a[href^="./assets/"]'); var i; for (i = 0; i < links.length; i = i + 1) { var href = links[i].getAttribute("href") || ""; links[i].setAttribute("href", base + href.replace(/^\.\//, "")); } } function renderFooterDetails() { var root = querySelector("[data-footer-details]"); var content = window.YX_CONTENT; if (!root || !content || !content.footer) { return; } var groups = [ { title: "社媒账号", items: content.footer.social }, { title: "销售渠道", items: content.footer.channels }, { title: "联系我们", items: content.footer.contact } ]; while (root.firstChild) { root.removeChild(root.firstChild); } groups.forEach(function (group) { // 该组没有条目时整组不渲染,避免出现只有标题的空列 if (!group.items || !group.items.length) { return; } var section = document.createElement("section"); var title = document.createElement("h3"); var list = document.createElement("ul"); title.textContent = group.title; section.className = "footer-detail-group"; section.appendChild(title); (group.items || []).forEach(function (entry) { // 兼容两种写法:纯字符串,或 { label, href } var text = typeof entry === "string" ? entry : (entry && entry.label) || ""; var href = typeof entry === "string" ? "" : (entry && entry.href) || ""; var qr = typeof entry === "string" ? "" : (entry && entry.qr) || ""; if (!text) { return; } // 公众号等没有网页地址的条目用二维码承载,悬停/聚焦时显示 if (qr) { var qrItem = document.createElement("li"); qrItem.className = "footer-detail-qr"; var qrTrigger = document.createElement("span"); qrTrigger.className = "footer-detail-qr-label"; qrTrigger.textContent = text; qrTrigger.setAttribute("tabindex", "0"); var qrImg = document.createElement("img"); qrImg.src = assetBase() + qr; qrImg.alt = text + " 二维码"; qrImg.loading = "lazy"; qrItem.appendChild(qrTrigger); qrItem.appendChild(qrImg); list.appendChild(qrItem); return; } // 页脚只允许站外 https 与电话拨号,其余一律降级为纯文字 var lowerHref = href.toLowerCase(); var isExternal = lowerHref.indexOf("https://") === 0; var isTel = /^tel:\+?[0-9-]{3,20}$/.test(lowerHref); if (href && !isExternal && !isTel) { href = ""; } var item = document.createElement("li"); if (href) { var link = document.createElement("a"); link.className = "footer-detail-link"; link.href = href; link.textContent = text; // 站外地址在新标签打开,避免用户离开官网 if (isExternal) { link.target = "_blank"; link.rel = "noreferrer"; } item.appendChild(link); } else { item.textContent = text; } list.appendChild(item); }); section.appendChild(list); root.appendChild(section); }); } function setupMenuToggle() { var toggle = querySelector("[data-menu-toggle]"); var navRoot = querySelector("[data-nav]"); if (!toggle || !navRoot) { return; } if (!navRoot.id) { navRoot.id = "site-nav"; } toggle.setAttribute("aria-controls", navRoot.id); if (!toggle.hasAttribute("aria-expanded")) { toggle.setAttribute("aria-expanded", "false"); } toggle.addEventListener("click", function () { var expanded = toggle.getAttribute("aria-expanded") === "true"; var nextExpanded = !expanded; toggle.setAttribute("aria-expanded", String(nextExpanded)); navRoot.classList.toggle("is-open", nextExpanded); }); } function applyReducedMotion() { if (!window.matchMedia) { return; } var query = window.matchMedia("(prefers-reduced-motion: reduce)"); if (query.matches) { document.documentElement.classList.add("reduce-motion"); } } function fillYear() { var yearNodes = querySelectorAll("[data-year]"); var year = String(new Date().getFullYear()); var i; for (i = 0; i < yearNodes.length; i = i + 1) { yearNodes[i].textContent = year; } } function setupScrollReveal() { var targets = querySelectorAll(".section, .statement-item, .heritage-item, .product-feature, .news-row, .wide-image"); var i; if (!("IntersectionObserver" in window)) { for (i = 0; i < targets.length; i = i + 1) { targets[i].classList.add("reveal"); } return; } var observer = new IntersectionObserver(function (entries) { var j; for (j = 0; j < entries.length; j = j + 1) { if (entries[j].isIntersecting) { entries[j].target.classList.add("reveal"); observer.unobserve(entries[j].target); } } }, { rootMargin: "0px 0px -12% 0px", threshold: 0.12 }); for (i = 0; i < targets.length; i = i + 1) { observer.observe(targets[i]); } } function setupCertLightbox() { var triggers = querySelectorAll("[data-lightbox]"); if (!triggers.length) { return; } var overlay = document.createElement("div"); overlay.className = "lightbox"; overlay.setAttribute("hidden", ""); overlay.setAttribute("role", "dialog"); overlay.setAttribute("aria-modal", "true"); overlay.setAttribute("aria-label", "证书放大查看"); var figure = document.createElement("figure"); figure.className = "lightbox-figure"; var img = document.createElement("img"); img.alt = ""; var caption = document.createElement("figcaption"); caption.className = "lightbox-figcaption"; var closeBtn = document.createElement("button"); closeBtn.type = "button"; closeBtn.className = "lightbox-close"; closeBtn.setAttribute("aria-label", "关闭"); closeBtn.textContent = "×"; figure.appendChild(img); figure.appendChild(caption); overlay.appendChild(figure); overlay.appendChild(closeBtn); document.body.appendChild(overlay); function open(trigger) { // data-lightbox 是自定义属性,Vite 构建不会重写其路径; // 触发器内部 的 src 已被构建正确重写(含 CDN 地址),优先取用,避免线上放大图 404 var thumb = trigger.querySelector("img"); img.src = (thumb && thumb.src) || trigger.getAttribute("data-lightbox") || ""; img.alt = trigger.getAttribute("data-lightbox-title") || ""; caption.textContent = trigger.getAttribute("data-lightbox-title") || ""; overlay.hidden = false; document.body.style.overflow = "hidden"; } function close() { overlay.hidden = true; document.body.style.overflow = ""; } var i; for (i = 0; i < triggers.length; i = i + 1) { triggers[i].addEventListener("click", function (event) { open(event.currentTarget); }); } overlay.addEventListener("click", function (event) { if (event.target === overlay || event.target === closeBtn) { close(); } }); document.addEventListener("keydown", function (event) { if (event.key === "Escape" && !overlay.hidden) { close(); } }); } /** * 品牌 IP 悬浮(天天 / 成成) * * 随页面滚动切换形象:滚到科学 / 产品相关区块时换成戴眼镜的严博士, * 滚到品类、品牌主张、动态等日常区块时换成温暖治愈形象。 * 形象与文案由页面上的 [data-ip-float] 容器通过 data 属性声明。 */ function setupIpFloat() { var root = querySelector("[data-ip-float]"); if (!root) { return; } var stage = querySelector(".yx-ip-stage", root); var figure = querySelector(".yx-ip-figure", root); var bubble = querySelector(".yx-ip-bubble", root); var closeBtn = querySelector(".yx-ip-close", root); var images = querySelectorAll("img[data-ip-mood]", root); if (!figure || !images.length) { return; } // 声明各区块对应的形象:data-ip-mood="science" / "daily" var zones = []; var zoneNodes = querySelectorAll("[data-ip-zone]"); var i; for (i = 0; i < zoneNodes.length; i = i + 1) { zones.push({ el: zoneNodes[i], mood: zoneNodes[i].getAttribute("data-ip-zone") || "daily", text: zoneNodes[i].getAttribute("data-ip-text") || "" }); } var dismissed = false; var currentMood = ""; var bubbleTimer = null; function setMood(mood, text) { if (mood === currentMood) { return; } currentMood = mood; var j; var matched = false; for (j = 0; j < images.length; j = j + 1) { var isTarget = images[j].getAttribute("data-ip-mood") === mood; if (isTarget) { matched = true; } images[j].classList.toggle("is-active", isTarget); } // 没有对应形象时回落到第一张,避免整块空白 if (!matched) { images[0].classList.add("is-active"); } if (!bubble) { return; } if (text) { bubble.textContent = text; bubble.classList.add("is-shown"); if (bubbleTimer) { window.clearTimeout(bubbleTimer); } bubbleTimer = window.setTimeout(function () { bubble.classList.remove("is-shown"); }, 4200); } else { bubble.classList.remove("is-shown"); } } function currentZone() { // 取视口中线所处的区块,滚动到哪个区块就用哪个形象 var mid = window.innerHeight / 2; var found = null; var j; for (j = 0; j < zones.length; j = j + 1) { var rect = zones[j].el.getBoundingClientRect(); if (rect.top <= mid && rect.bottom >= mid) { found = zones[j]; } } return found; } function update() { if (dismissed) { return; } // 首屏不打扰,往下滚一点再出现 var scrolled = window.pageYOffset || document.documentElement.scrollTop || 0; var show = scrolled > window.innerHeight * 0.45; root.classList.toggle("is-visible", show); var zone = currentZone(); if (zone) { setMood(zone.mood, zone.text); } } var ticking = false; function onScroll() { if (ticking) { return; } ticking = true; window.requestAnimationFrame(function () { update(); ticking = false; }); } if (closeBtn) { closeBtn.addEventListener("click", function () { dismissed = true; root.classList.remove("is-visible"); }); } if (stage && bubble) { stage.addEventListener("mouseenter", function () { var zone = currentZone(); if (zone && zone.text) { bubble.textContent = zone.text; bubble.classList.add("is-shown"); } }); stage.addEventListener("mouseleave", function () { bubble.classList.remove("is-shown"); }); } window.addEventListener("scroll", onScroll, { passive: true }); window.addEventListener("resize", onScroll); update(); } /** * 品牌故事页板块切换 * * 顶部标签控制 [data-story-panel] 的显示,URL hash 可直达某个板块, * 浏览器前进/后退也能正确切换。 */ function setupStoryTabs() { var tabsRoot = querySelector("[data-story-tabs]"); if (!tabsRoot) { return; } var tabs = querySelectorAll("[data-story-tab]", tabsRoot); var panels = querySelectorAll("[data-story-panel]"); if (!tabs.length || !panels.length) { return; } function activate(name, updateHash) { var matched = false; var i; for (i = 0; i < panels.length; i = i + 1) { var isTarget = panels[i].getAttribute("data-story-panel") === name; panels[i].hidden = !isTarget; if (isTarget) { matched = true; } } // hash 指向不存在的板块时回落到第一个,避免整页空白 if (!matched) { panels[0].hidden = false; name = panels[0].getAttribute("data-story-panel"); } for (i = 0; i < tabs.length; i = i + 1) { var on = tabs[i].getAttribute("data-story-tab") === name; tabs[i].classList.toggle("is-active", on); tabs[i].setAttribute("aria-selected", on ? "true" : "false"); tabs[i].setAttribute("tabindex", on ? "0" : "-1"); } if (updateHash && window.history && window.history.replaceState) { window.history.replaceState(null, "", "#" + name); } } var i; for (i = 0; i < tabs.length; i = i + 1) { tabs[i].addEventListener("click", function (event) { event.preventDefault(); activate(event.currentTarget.getAttribute("data-story-tab"), true); tabsRoot.scrollIntoView({ block: "start" }); }); } // 左右方向键在标签间移动,符合 tablist 无障碍惯例 tabsRoot.addEventListener("keydown", function (event) { if (event.key !== "ArrowRight" && event.key !== "ArrowLeft") { return; } var list = []; var j; for (j = 0; j < tabs.length; j = j + 1) { list.push(tabs[j]); } var current = list.indexOf(document.activeElement); if (current === -1) { return; } event.preventDefault(); var step = event.key === "ArrowRight" ? 1 : -1; var next = list[(current + step + list.length) % list.length]; next.focus(); activate(next.getAttribute("data-story-tab"), true); }); window.addEventListener("hashchange", function () { activate((window.location.hash || "").replace("#", ""), false); }); activate((window.location.hash || "").replace("#", ""), false); } function init() { renderNav(); renderFooterDetails(); fixAssetLinks(); // ip-chat.js 在 site.js 之后引入,故延到 load 再判断是否已就绪 if (document.readyState === "complete") { ensureIpChat(); } else { window.addEventListener("load", ensureIpChat); } setupMenuToggle(); applyReducedMotion(); fillYear(); setupScrollReveal(); setupCertLightbox(); setupIpFloat(); setupStoryTabs(); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } })();