Files
HomeDash/web/navi.js
JianFeeeee 9eb8214fab fix(web): 搜索栏改为底部悬浮,补极简视图页脚与整体动效
- 搜索栏 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
2026-09-11 16:25:26 +08:00

129 lines
5.0 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.

(() => {
'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 });
})();