From 5a78de6aa66c0ca2cca3b231ffc71242042cb741 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A0=81=E5=86=9C=E9=98=BF=E6=A5=A0?= Date: Wed, 9 Sep 2026 00:12:21 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E5=90=8E=E5=8F=B0=E4=B8=BB?= =?UTF-8?q?=E9=A2=98=E5=88=87=E6=8D=A2=E4=B8=8E=E7=A7=BB=E5=8A=A8=E7=AB=AF?= =?UTF-8?q?=E6=BA=A2=E5=87=BA=E8=8F=9C=E5=8D=95=EF=BC=8C=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E4=BF=9D=E5=AD=98=E6=8C=89=E9=92=AE=E4=B8=8A=E7=A7=BB=20topbar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后台 topbar 右侧常驻主题切换按钮(复用博客端三态循环逻辑), tooltip 新增 tip-below 朝下变体适配近视口顶部的 topbar - <768px 侧栏隐藏时主题/返回博客/退出登录收进「···」溢出菜单; 博客 <640px 首页/标签收进 header 溢出菜单(带 active 高亮) - 新增通用 MoreMenu 组件:点击外部/Escape/选中项自动关闭, 面板用 --popover 令牌 + 0.15s 淡入,与现有卡片风格一致 - 站点设置保存按钮移至 topbar 标题右侧,删除底部 sticky 保存条 - 修复两处回归:tip-below 气泡居中锚定越界产生页面横向滚动条 (改右对齐);pn-site-menu-btn 基础 display 规则源码顺序在 640px MQ 之后导致移动端菜单按钮不显示(前移至博客 header 区) - 已用 Playwright 在 1440×900 / 390×844 实测后台四页与博客 菜单展开/跳转均无溢出 --- web/src/components/Layout.tsx | 96 ++++++++++++++++++-- web/src/index.css | 149 +++++++++++++++++++++++++------- web/src/pages/AdminSettings.tsx | 25 +++--- 3 files changed, 220 insertions(+), 50 deletions(-) diff --git a/web/src/components/Layout.tsx b/web/src/components/Layout.tsx index 14e7e79..126c32a 100644 --- a/web/src/components/Layout.tsx +++ b/web/src/components/Layout.tsx @@ -1,20 +1,72 @@ +import { useEffect, useRef, useState } from 'react' import { Link, NavLink, Outlet, useLocation, useNavigate } from 'react-router' -import { Moon, Sun, Monitor, ExternalLink, House, PenLine, Tags, Trash2, Settings, LogOut } from 'lucide-react' +import { + Moon, Sun, Monitor, ExternalLink, House, PenLine, MoreHorizontal, Tags, Trash2, Settings, LogOut, +} from 'lucide-react' import { useSiteTitle } from '../features/site' import { useTheme } from '../lib/theme' import { useAuth } from '../lib/auth' +/* ── 溢出菜单:触发按钮 + 下拉面板(点击外部 / Esc / 选中项关闭) ── */ + +function MoreMenu({ triggerClassName, triggerLabel, children }: { + triggerClassName: string + triggerLabel: string + children: React.ReactNode +}) { + const [open, setOpen] = useState(false) + const ref = useRef(null) + useEffect(() => { + if (!open) return + const onPointer = (e: PointerEvent) => { + if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false) + } + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') setOpen(false) + } + document.addEventListener('pointerdown', onPointer) + document.addEventListener('keydown', onKey) + return () => { + document.removeEventListener('pointerdown', onPointer) + document.removeEventListener('keydown', onKey) + } + }, [open]) + return ( +
+ + {open && ( +
setOpen(false)}> + {children} +
+ )} +
+ ) +} + /* ── 博客端:居中单栏 720px 布局(博客系列设计稿) ─────────── */ -function ThemeButton() { +function ThemeButton({ className = 'pn-site-navlink', tip = 'native' }: { + className?: string + tip?: 'native' | 'css' +}) { const { mode, setMode } = useTheme() const next = mode === 'light' ? 'dark' : mode === 'dark' ? 'system' : 'light' const title = `主题:${mode === 'light' ? '浅色' : mode === 'dark' ? '深色' : '跟随系统'}(点击切换)` return ( + + 返回博客 + + + + ) } diff --git a/web/src/index.css b/web/src/index.css index dadfd1e..57d56a1 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -489,6 +489,11 @@ button:focus-visible { width: 14px; height: 14px; } +/* 博客端溢出菜单触发按钮:<640px 显示(基础规则须在 640px MQ 之前, + 否则同特异性下源码顺序靠后者覆盖 MQ 内的 display:inline-flex) */ +.pn-site-menu-btn { + display: none; +} .pn-site-main { flex: 1; width: 100%; @@ -547,6 +552,9 @@ button:focus-visible { .pn-site-navlink.hide-sm { display: none; } + .pn-site-menu-btn { + display: inline-flex; + } } /* ============================================================ @@ -1658,6 +1666,99 @@ button:focus-visible { .pn-admin-iconbtn:focus-visible::before { opacity: 1; } +/* tooltip 朝下变体(topbar 贴近视口顶部,气泡须向下弹出)。 + 气泡右对齐按钮:居中锚定会越过 topbar 右缘产生页面横向滚动条 */ +.pn-admin-iconbtn.tip-below::after { + bottom: auto; + top: calc(100% + 10px); + left: auto; + right: 0; + transform: translate(0, -2px); +} +.pn-admin-iconbtn.tip-below::before { + bottom: auto; + top: 100%; + border-top-color: transparent; + border-bottom-color: var(--primary); +} +.pn-admin-iconbtn.tip-below:hover::after, +.pn-admin-iconbtn.tip-below:focus-visible::after { + transform: translate(0, 0); +} +/* 侧栏隐藏时的移动端兜底按钮(<768px 显示) */ +.pn-admin-mobile-act { + display: none; +} +/* 溢出菜单:面板与菜单项(博客 header / 后台 topbar 共用) */ +.pn-moremenu { + position: relative; +} +.pn-moremenu-panel { + position: absolute; + top: calc(100% + 8px); + right: 0; + z-index: 60; + min-width: 150px; + display: flex; + flex-direction: column; + gap: 2px; + padding: 6px; + border: 1px solid var(--border); + border-radius: var(--radius-md); + background: var(--popover); + box-shadow: var(--shadow-sm); + animation: pn-moremenu-in 0.15s ease; +} +@keyframes pn-moremenu-in { + from { + opacity: 0; + transform: translateY(-4px); + } + to { + opacity: 1; + transform: none; + } +} +.pn-moremenu-item { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 10px; + border: 0; + border-radius: var(--radius-sm); + background: transparent; + color: var(--foreground); + font-size: 13px; + font-weight: 500; + line-height: 1.4; + text-align: left; + text-decoration: none; + white-space: nowrap; + cursor: pointer; + transition: background-color 0.15s ease; +} +.pn-moremenu-item:hover, +.pn-moremenu-item.active { + background: var(--muted); + color: var(--foreground); +} +.pn-moremenu-item.active { + font-weight: 600; +} +.pn-moremenu-item svg { + width: 15px; + height: 15px; + flex: none; + color: var(--muted-foreground); +} +.pn-moremenu-item.active svg { + color: var(--foreground); +} +/* 无 data-tip 的图标按钮不渲染空 tooltip 气泡(如菜单触发按钮) */ +.pn-admin-iconbtn:not([data-tip])::after, +.pn-admin-iconbtn:not([data-tip])::before { + display: none; +} .pn-admin-avatar { width: 28px; height: 28px; @@ -1799,6 +1900,23 @@ button:focus-visible { } .pn-admin-topbar h1 { font-size: 16px; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .pn-admin-actions { + gap: 6px; + } + .pn-admin-actions .btn { + padding-left: 10px; + padding-right: 10px; + } + .pn-admin-mobile-act { + display: inline-flex; + } + .pn-admin-desktop-act { + display: none; } .pn-admin-search { display: none; @@ -2588,41 +2706,10 @@ textarea.pn-as-input { padding-top: 16px; border-top: 1px solid var(--border); } -.pn-as-save-bar { - position: sticky; - bottom: 20px; - display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; - padding: 12px 16px; - background: var(--card); - border: 1px solid var(--border); - border-radius: var(--radius-md); - box-shadow: var(--shadow-sm); -} -.pn-as-save-hint { - font-size: 12.5px; - color: var(--muted-foreground); -} -.pn-as-save-bar .btn { - min-height: 36px; - padding: 0 16px; - font-size: 13.5px; - flex: none; -} -.pn-as-save-bar .btn svg { - width: 15px; - height: 15px; -} @media (max-width: 767px) { .pn-as-card { padding: 20px; } - .pn-as-save-bar { - bottom: 88px; - flex-wrap: wrap; - } } /* ============================================================ diff --git a/web/src/pages/AdminSettings.tsx b/web/src/pages/AdminSettings.tsx index e4902df..38ed033 100644 --- a/web/src/pages/AdminSettings.tsx +++ b/web/src/pages/AdminSettings.tsx @@ -47,7 +47,17 @@ export default function AdminSettings() { return ( <> - + + +
@@ -104,19 +114,6 @@ export default function AdminSettings() {
- -
- 更改将在保存后应用到公开站点 - -
setToast(null)} />