From 80b0e5f51e2fab01fac6182dc263d8f0213ba8d1 Mon Sep 17 00:00:00 2001 From: HomeDash Bot Date: Fri, 11 Sep 2026 17:03:33 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E6=90=9C=E7=B4=A2=E6=A1=86?= =?UTF-8?q?=E6=94=B6=E8=B5=B7=E6=80=81=20+=20admin=20=E5=85=A5=E5=8F=A3=20?= =?UTF-8?q?+=20=E4=BF=AE=E6=AD=A3=E5=A4=9A=E4=BD=99=E6=BB=9A=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 搜索栏默认收起为左下角放大镜悬浮球(52/56px),点它展开成输入框, 输入框内左侧仍是同一个放大镜,点它收起;Esc 也可收起 - Dashy 页面补右下角 /admin/ 悬浮入口(原先没有任何管理入口) - 短页面不再凭空可滚动:body 留白 104->84px,并给 #dashy 扣掉同样高度 (Dashy 的 #dashy 本已撑满 100vh,叠加留白就多出约 134px 滚动) - 补的备案号页脚改为固定在搜索栏正上方,不再参与文档流撑高页面 - 弹层打开时搜索栏与管理入口一并让位 --- web/navi.css | 185 +++++++++++++++++++++++++++++++++++++++++---------- web/navi.js | 141 ++++++++++++++++++++++++++------------- 2 files changed, 246 insertions(+), 80 deletions(-) diff --git a/web/navi.css b/web/navi.css index 95e8390..f2bf999 100644 --- a/web/navi.css +++ b/web/navi.css @@ -10,7 +10,13 @@ html[data-theme] body { min-width: 0; overflow-x: hidden; /* 悬浮搜索栏固定在底部,给内容留出空间,避免遮挡页脚备案号。 */ - padding-bottom: 104px !important; + padding-bottom: 84px !important; +} + +/* Dashy 的 #dashy 本身撑满 100vh;不扣掉留给悬浮搜索栏的高度, + 短页面(极简视图/工作空间)就会凭空多出约 134px 的纵向滚动。 */ +html[data-theme] #dashy { + min-height: calc(100vh - 84px) !important; } /* Dashy's native filter/settings strip duplicates the portal search and breaks on phones. */ @@ -19,7 +25,8 @@ html[data-theme] .minimal-home .title-and-search > form { display: none !important; } -html[data-theme] body:has(.modal-overlay) > .navi-search-bar { +html[data-theme] body:has(.modal-overlay) > .navi-search-bar, +html[data-theme] body:has(.modal-overlay) > .navi-footer-host { visibility: hidden !important; pointer-events: none !important; } @@ -84,35 +91,102 @@ html[data-theme] a.item .item-url { white-space: nowrap; } -/* 底部搜索:悬浮固定在视口下侧,任何页面/任何滚动位置都可见。 */ +/* ── 底部搜索:左下角放大镜悬浮球 ── + 收起态只显示一个放大镜图标;点它展开成输入框; + 展开后输入框内左侧仍是同一个放大镜按钮,点它收起。 */ html[data-theme] .navi-search-bar { box-sizing: border-box !important; position: fixed !important; - left: 50% !important; + left: calc(16px + env(safe-area-inset-left, 0px)) !important; bottom: calc(16px + env(safe-area-inset-bottom, 0px)) !important; - transform: translateX(-50%) !important; - width: min(760px, calc(100% - 28px)) !important; - min-height: 58px !important; margin: 0 !important; - padding: 7px 8px 7px 18px !important; display: flex !important; align-items: center !important; - gap: 12px !important; + gap: 8px !important; z-index: 900 !important; color: #475569 !important; - background: rgba(255, 255, 255, .68) !important; - border: 1px solid rgba(255, 255, 255, .88) !important; - border-radius: 20px !important; - box-shadow: 0 14px 38px rgba(15, 23, 42, .14), inset 0 1px 0 rgba(255, 255, 255, .95) !important; + background: rgba(255, 255, 255, .72) !important; + border: 1px solid rgba(255, 255, 255, .9) !important; + box-shadow: 0 14px 38px rgba(15, 23, 42, .16), inset 0 1px 0 rgba(255, 255, 255, .95) !important; backdrop-filter: blur(22px) saturate(180%) !important; -webkit-backdrop-filter: blur(22px) saturate(180%) !important; + overflow: hidden !important; } -html[data-theme] .navi-search-bar > span { - flex: 0 0 20px; - height: 20px; - display: grid; - place-items: center; +html[data-theme] .navi-search-bar[data-state="collapsed"] { + width: 56px !important; + height: 56px !important; + padding: 0 !important; + border-radius: 50% !important; +} + +html[data-theme] .navi-search-bar[data-state="open"] { + width: min(560px, calc(100% - 96px)) !important; + min-height: 58px !important; + padding: 7px 8px !important; + border-radius: 20px !important; +} + +html[data-theme] .navi-search-bar[data-state="collapsed"] input, +html[data-theme] .navi-search-bar[data-state="collapsed"] .navi-search-submit { + display: none !important; +} + +html[data-theme] .navi-search-toggle { + flex: 0 0 42px !important; + width: 42px !important; + height: 42px !important; + display: grid !important; + place-items: center !important; + padding: 0 !important; + color: #0f172a !important; + background: transparent !important; + border: 0 !important; + border-radius: 50% !important; + cursor: pointer; + transition: background .2s ease, color .2s ease, transform .2s cubic-bezier(.22, 1, .36, 1) !important; +} + +html[data-theme] .navi-search-toggle:hover { + color: #0284c7 !important; + background: rgba(14, 165, 233, .12) !important; + transform: scale(1.06); +} + +/* 右侧管理入口:Dashy 页面本身没有去 /admin/ 的链接 */ +html[data-theme] .navi-admin-entry { + position: fixed !important; + right: calc(16px + env(safe-area-inset-right, 0px)) !important; + bottom: calc(16px + env(safe-area-inset-bottom, 0px)) !important; + width: 52px !important; + height: 52px !important; + display: grid !important; + place-items: center !important; + z-index: 900 !important; + color: #334155 !important; + background: rgba(255, 255, 255, .72) !important; + border: 1px solid rgba(255, 255, 255, .9) !important; + border-radius: 50% !important; + box-shadow: 0 14px 38px rgba(15, 23, 42, .16), inset 0 1px 0 rgba(255, 255, 255, .95) !important; + backdrop-filter: blur(22px) saturate(180%) !important; + -webkit-backdrop-filter: blur(22px) saturate(180%) !important; + transition: transform .22s cubic-bezier(.22, 1, .36, 1), color .2s ease, background .2s ease !important; +} + +html[data-theme] .navi-admin-entry:hover { + color: #0284c7 !important; + background: rgba(255, 255, 255, .94) !important; + transform: translateY(-1px) scale(1.05); +} + +html[data-theme] .navi-admin-entry svg { + width: 22px !important; + height: 22px !important; +} + +html[data-theme] body:has(.modal-overlay) > .navi-admin-entry { + visibility: hidden !important; + pointer-events: none !important; } html[data-theme] .navi-search-bar input { @@ -134,7 +208,7 @@ html[data-theme] .navi-search-bar input::placeholder { opacity: 1; } -html[data-theme] .navi-search-bar button { +html[data-theme] .navi-search-submit { flex: 0 0 44px !important; width: 44px !important; height: 44px !important; @@ -149,7 +223,8 @@ html[data-theme] .navi-search-bar button { cursor: pointer; } -html[data-theme] .navi-search-bar button svg { +html[data-theme] .navi-search-toggle svg, +html[data-theme] .navi-search-submit svg { width: 20px; height: 20px; } @@ -334,12 +409,39 @@ html[data-theme] .minimal-section-inner a.item { } html[data-theme] .navi-search-bar { - width: calc(100% - 20px) !important; - min-height: 56px !important; + left: calc(10px + env(safe-area-inset-left, 0px)) !important; bottom: calc(12px + env(safe-area-inset-bottom, 0px)) !important; - padding-left: 15px !important; + } + + html[data-theme] .navi-search-bar[data-state="collapsed"] { + width: 52px !important; + height: 52px !important; + } + + html[data-theme] .navi-search-bar[data-state="open"] { + width: calc(100% - 86px) !important; + min-height: 56px !important; border-radius: 18px !important; } + + html[data-theme] .navi-admin-entry { + width: 48px !important; + height: 48px !important; + right: calc(10px + env(safe-area-inset-right, 0px)) !important; + bottom: calc(12px + env(safe-area-inset-bottom, 0px)) !important; + } + + html[data-theme] body { + padding-bottom: 78px !important; + } + + html[data-theme] #dashy { + min-height: calc(100vh - 78px) !important; + } + + html[data-theme] .navi-footer-host { + bottom: calc(78px + env(safe-area-inset-bottom, 0px)) !important; + } } /* ── 页脚:备案号胶囊 ── @@ -356,10 +458,22 @@ html[data-theme] footer > div { padding: 0 !important; } +/* 极简/工作空间视图补的备案号页脚:固定悬浮在搜索栏正上方。 + 它是「底部固定家具」的一部分,不能再参与文档流,否则会把页面撑高导致多余滚动。 */ html[data-theme] .navi-footer-host { + position: fixed !important; + left: 0 !important; + right: 0 !important; + bottom: calc(84px + env(safe-area-inset-bottom, 0px)) !important; + z-index: 890 !important; display: flex !important; justify-content: center !important; - padding: 0 8px 6px !important; + padding: 0 8px !important; + pointer-events: none !important; +} + +html[data-theme] .navi-footer-host .navi-footer { + pointer-events: auto !important; } html[data-theme] .navi-footer { @@ -400,11 +514,6 @@ html[data-theme] .navi-footer-sep { } /* ── 工作空间:侧栏与内容区同样走玻璃圆角 ── */ -html[data-theme] .work-space, -html[data-theme] .web-content { - padding-bottom: 104px !important; -} - html[data-theme] nav.side-bar { background: rgba(255, 255, 255, .62) !important; border-right: 1px solid rgba(255, 255, 255, .6) !important; @@ -439,8 +548,8 @@ html[data-theme] nav.side-bar .side-bar-item:hover { } @keyframes navi-bar-in { - from { opacity: 0; transform: translate(-50%, 16px); } - to { opacity: 1; transform: translate(-50%, 0); } + from { opacity: 0; transform: translateY(16px); } + to { opacity: 1; transform: translateY(0); } } html[data-theme] html, @@ -458,20 +567,25 @@ html[data-theme] .work-space { /* 悬浮搜索栏:从下侧滑入 */ html[data-theme] .navi-search-bar { animation: navi-bar-in .36s cubic-bezier(.22, 1, .36, 1) both; - transition: box-shadow .24s ease, border-color .24s ease, background .24s ease !important; + transition: box-shadow .24s ease, border-color .24s ease, background .24s ease, + width .28s cubic-bezier(.22, 1, .36, 1), border-radius .28s ease !important; } -html[data-theme] .navi-search-bar button { +html[data-theme] .navi-admin-entry { + animation: navi-fade-in .42s ease both; +} + +html[data-theme] .navi-search-submit { transition: transform .2s cubic-bezier(.22, 1, .36, 1), box-shadow .2s ease, filter .2s ease !important; } -html[data-theme] .navi-search-bar button:hover { +html[data-theme] .navi-search-submit:hover { filter: brightness(1.06); transform: translateY(-1px) scale(1.04); box-shadow: 0 12px 26px rgba(37, 99, 235, .34) !important; } -html[data-theme] .navi-search-bar button:active { +html[data-theme] .navi-search-submit:active { transform: translateY(0) scale(.96); } @@ -557,6 +671,7 @@ html[data-theme] .navi-footer a { html[data-theme] .minimal-section-inner.selected, html[data-theme] .work-space, html[data-theme] .navi-search-bar, + html[data-theme] .navi-admin-entry, html[data-theme] .navi-footer-host, html[data-theme] div.collapsible-content .item-wrapper, html[data-theme] .minimal-section-inner .item-wrapper { diff --git a/web/navi.js b/web/navi.js index dcf6838..fa6f2a9 100644 --- a/web/navi.js +++ b/web/navi.js @@ -5,6 +5,44 @@ const EXT_ATTR = 'data-navi-ext'; const INSTALL_SELECTOR = 'a[href*="/extension/install"]'; + const SVG_NS = 'http://www.w3.org/2000/svg'; + + function svg(paths, opts = {}) { + const el = document.createElementNS(SVG_NS, 'svg'); + el.setAttribute('viewBox', opts.viewBox || '0 0 24 24'); + el.setAttribute('fill', 'none'); + el.setAttribute('stroke', 'currentColor'); + el.setAttribute('stroke-width', opts.width || '2.2'); + el.setAttribute('stroke-linecap', 'round'); + el.setAttribute('stroke-linejoin', 'round'); + el.setAttribute('aria-hidden', 'true'); + if (opts.size) { + el.setAttribute('width', opts.size); + el.setAttribute('height', opts.size); + } + paths.forEach((p) => { + const node = document.createElementNS(SVG_NS, p.tag); + Object.keys(p.attrs).forEach((k) => node.setAttribute(k, p.attrs[k])); + el.append(node); + }); + return el; + } + + const magnifierIcon = () => svg([ + { tag: 'circle', attrs: { cx: '11', cy: '11', r: '7.5' } }, + { tag: 'line', attrs: { x1: '20.5', y1: '20.5', x2: '16.2', y2: '16.2' } }, + ], { size: '20' }); + + const arrowIcon = () => svg([ + { tag: 'line', attrs: { x1: '5', y1: '12', x2: '19', y2: '12' } }, + { tag: 'polyline', attrs: { points: '13 6 19 12 13 18' } }, + ]); + + const gearIcon = () => svg([ + { tag: 'circle', attrs: { cx: '12', cy: '12', r: '3.2' } }, + { tag: 'path', attrs: { d: 'M19.4 15a1.7 1.7 0 0 0 .34 1.87l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.7 1.7 0 0 0-1.87-.34 1.7 1.7 0 0 0-1.03 1.56V21a2 2 0 1 1-4 0v-.09A1.7 1.7 0 0 0 8.9 19.3a1.7 1.7 0 0 0-1.87.34l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.7 1.7 0 0 0 4.6 15a1.7 1.7 0 0 0-1.56-1.03H3a2 2 0 1 1 0-4h.09A1.7 1.7 0 0 0 4.7 8.9a1.7 1.7 0 0 0-.34-1.87l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.7 1.7 0 0 0 9 4.6h.09A1.7 1.7 0 0 0 10.6 3.04V3a2 2 0 1 1 4 0v.09a1.7 1.7 0 0 0 1.03 1.56 1.7 1.7 0 0 0 1.87-.34l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.7 1.7 0 0 0 19.4 9v.09a1.7 1.7 0 0 0 1.56 1.03H21a2 2 0 1 1 0 4h-.09a1.7 1.7 0 0 0-1.51 1.03z' } }, + ], { size: '20', width: '1.9' }); + function extensionInstalled() { return document.documentElement.hasAttribute(EXT_ATTR); } @@ -19,71 +57,83 @@ }); } + // 底部搜索栏:默认收起成一个放大镜悬浮球(左下角), + // 点放大镜展开成输入框,再点输入框里的放大镜收起。 function mountSearch() { if (document.querySelector('.navi-search-bar')) return; + const form = document.createElement('form'); form.className = 'navi-search-bar'; + form.dataset.state = 'collapsed'; 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 toggle = document.createElement('button'); + toggle.type = 'button'; + toggle.className = 'navi-search-toggle'; + toggle.setAttribute('aria-label', '展开搜索'); + toggle.setAttribute('aria-expanded', 'false'); + toggle.append(magnifierIcon()); const input = document.createElement('input'); input.type = 'search'; input.name = 'q'; input.placeholder = '搜索…'; input.autocomplete = 'off'; - input.setAttribute('aria-label', '搜索'); + input.tabIndex = -1; + 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', '搜索'); + const submit = document.createElement('button'); + submit.type = 'submit'; + submit.className = 'navi-search-submit'; + submit.setAttribute('aria-label', '开始搜索'); + submit.append(arrowIcon()); - form.append(icon, input, button); + const setState = (state) => { + const open = state === 'open'; + form.dataset.state = open ? 'open' : 'collapsed'; + toggle.setAttribute('aria-expanded', String(open)); + toggle.setAttribute('aria-label', open ? '收起搜索' : '展开搜索'); + input.tabIndex = open ? 0 : -1; + if (open) { + input.focus({ preventScroll: true }); + } else { + input.value = ''; + input.blur(); + } + }; + + toggle.addEventListener('click', (evt) => { + evt.preventDefault(); + setState(form.dataset.state === 'open' ? 'collapsed' : 'open'); + }); + + // Esc 收起(不阻止其它默认行为) + form.addEventListener('keydown', (evt) => { + if (evt.key === 'Escape' && form.dataset.state === 'open') { + evt.stopPropagation(); + setState('collapsed'); + } + }); + + form.append(toggle, input, submit); document.body.append(form); } + // Dashy 页面没有管理入口:右下角补一个悬浮链接到 /admin/。 + function mountAdminEntry() { + if (document.querySelector('.navi-admin-entry')) return; + const link = document.createElement('a'); + link.className = 'navi-admin-entry'; + link.href = '/admin/'; + link.title = '导航管理'; + link.setAttribute('aria-label', '导航管理'); + link.append(gearIcon()); + document.body.append(link); + } + // 极简视图(/minimal)与工作空间视图不渲染 Dashy 页脚,备案号会整段消失。 // 这里从只读的 pageinfo 取回页脚 HTML 补一份,保证每个视图都能看到备案信息。 let footerPending = false; @@ -115,6 +165,7 @@ function run() { mountSearch(); + mountAdminEntry(); hideInstallEntry(); mountFooter(); }