- 搜索栏 position:fixed 悬浮在视口下侧,任何视图/滚动位置都可见; 原先是流式跟在页脚之后,手机上要滚到底才看得到 - 极简视图(/minimal)与工作空间不渲染 Dashy 页脚,备案号整段消失; navi.js 从只读 pageinfo 取回页脚补一份(DOMParser,不用 innerHTML) - 空分组不再渲染成空玻璃条(:not(:has(a.item))),修掉 mini 页底部一排空框 - 图标统一 56/48px 容器 + object-fit:contain,修 24px 与 56px 混排 - 备案号页脚改为胶囊样式 - 新增动效层:视图/卡片淡入、搜索栏滑入、悬停位移,全部只动 transform/opacity 并遵循 prefers-reduced-motion
129 lines
5.0 KiB
JavaScript
129 lines
5.0 KiB
JavaScript
(() => {
|
||
'use strict';
|
||
|
||
// 浏览器插件在 document_start 阶段写入这个属性(见 extension/*/detect.js)。
|
||
const EXT_ATTR = 'data-navi-ext';
|
||
const INSTALL_SELECTOR = 'a[href*="/extension/install"]';
|
||
|
||
function extensionInstalled() {
|
||
return document.documentElement.hasAttribute(EXT_ATTR);
|
||
}
|
||
|
||
// 已装插件的浏览器不需要安装入口:连外层网格单元一起隐藏,
|
||
// 否则 .item-wrapper 会留下一个空位。
|
||
function hideInstallEntry() {
|
||
if (!extensionInstalled()) return;
|
||
document.querySelectorAll(INSTALL_SELECTOR).forEach((link) => {
|
||
const cell = link.closest('.item-wrapper') || link.parentElement || link;
|
||
if (cell.style.display !== 'none') cell.style.display = 'none';
|
||
});
|
||
}
|
||
|
||
function mountSearch() {
|
||
if (document.querySelector('.navi-search-bar')) return;
|
||
const form = document.createElement('form');
|
||
form.className = 'navi-search-bar';
|
||
form.method = 'GET';
|
||
form.action = '/search';
|
||
form.target = '_blank';
|
||
form.setAttribute('role', 'search');
|
||
|
||
const icon = document.createElement('span');
|
||
const iconSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||
iconSvg.setAttribute('width', '18');
|
||
iconSvg.setAttribute('height', '18');
|
||
iconSvg.setAttribute('viewBox', '0 0 24 24');
|
||
iconSvg.setAttribute('fill', 'none');
|
||
iconSvg.setAttribute('stroke', 'currentColor');
|
||
iconSvg.setAttribute('stroke-width', '2.5');
|
||
iconSvg.setAttribute('stroke-linecap', 'round');
|
||
iconSvg.setAttribute('stroke-linejoin', 'round');
|
||
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
||
circle.setAttribute('cx', '11');
|
||
circle.setAttribute('cy', '11');
|
||
circle.setAttribute('r', '8');
|
||
const searchLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
||
searchLine.setAttribute('x1', '21');
|
||
searchLine.setAttribute('y1', '21');
|
||
searchLine.setAttribute('x2', '16.65');
|
||
searchLine.setAttribute('y2', '16.65');
|
||
iconSvg.append(circle, searchLine);
|
||
icon.append(iconSvg);
|
||
icon.setAttribute('aria-hidden', 'true');
|
||
|
||
const input = document.createElement('input');
|
||
input.type = 'search';
|
||
input.name = 'q';
|
||
input.placeholder = '搜索…';
|
||
input.autocomplete = 'off';
|
||
input.setAttribute('aria-label', '搜索');
|
||
|
||
const button = document.createElement('button');
|
||
button.type = 'submit';
|
||
const arrowSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||
arrowSvg.setAttribute('viewBox', '0 0 24 24');
|
||
arrowSvg.setAttribute('fill', 'none');
|
||
arrowSvg.setAttribute('stroke', 'currentColor');
|
||
arrowSvg.setAttribute('stroke-width', '2.4');
|
||
arrowSvg.setAttribute('stroke-linecap', 'round');
|
||
arrowSvg.setAttribute('stroke-linejoin', 'round');
|
||
arrowSvg.setAttribute('aria-hidden', 'true');
|
||
const arrowLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
||
arrowLine.setAttribute('x1', '5');
|
||
arrowLine.setAttribute('y1', '12');
|
||
arrowLine.setAttribute('x2', '19');
|
||
arrowLine.setAttribute('y2', '12');
|
||
const arrowHead = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
|
||
arrowHead.setAttribute('points', '13 6 19 12 13 18');
|
||
arrowSvg.append(arrowLine, arrowHead);
|
||
button.append(arrowSvg);
|
||
button.title = '搜索';
|
||
button.setAttribute('aria-label', '搜索');
|
||
|
||
form.append(icon, input, button);
|
||
document.body.append(form);
|
||
}
|
||
|
||
// 极简视图(/minimal)与工作空间视图不渲染 Dashy 页脚,备案号会整段消失。
|
||
// 这里从只读的 pageinfo 取回页脚 HTML 补一份,保证每个视图都能看到备案信息。
|
||
let footerPending = false;
|
||
async function mountFooter() {
|
||
if (footerPending || document.querySelector('.navi-footer')) return;
|
||
footerPending = true;
|
||
try {
|
||
const res = await fetch('/admin/api/pageinfo', { credentials: 'same-origin' });
|
||
if (!res.ok) return;
|
||
const info = await res.json();
|
||
const html = (info && info.footer) || '';
|
||
if (!html) return;
|
||
// 用 DOMParser 解析(不用 innerHTML),只取 .navi-footer 节点。
|
||
const parsed = new DOMParser().parseFromString(html, 'text/html');
|
||
const node = parsed.querySelector('.navi-footer');
|
||
if (!node || document.querySelector('.navi-footer')) return;
|
||
const host = document.createElement('div');
|
||
host.className = 'navi-footer-host';
|
||
host.append(node);
|
||
const bar = document.querySelector('.navi-search-bar');
|
||
if (bar && bar.parentNode) bar.parentNode.insertBefore(host, bar);
|
||
else document.body.append(host);
|
||
} catch (err) {
|
||
// 补页脚失败不影响搜索等主功能
|
||
} finally {
|
||
footerPending = false;
|
||
}
|
||
}
|
||
|
||
function run() {
|
||
mountSearch();
|
||
hideInstallEntry();
|
||
mountFooter();
|
||
}
|
||
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', run, { once: true });
|
||
} else {
|
||
run();
|
||
}
|
||
new MutationObserver(run).observe(document.documentElement, { childList: true, subtree: true });
|
||
})();
|