From 54fd0edf84122b07c0af9acaa47f2899adfd3487 Mon Sep 17 00:00:00 2001 From: CNWei Date: Sat, 31 Oct 2026 15:40:08 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=B7=A6=E5=8F=B3=E4=B8=A4=E6=A0=8F?= =?UTF-8?q?=E5=85=B1=E7=94=A8=E6=BB=9A=E5=8A=A8=E5=AE=B9=E5=99=A8=EF=BC=8C?= =?UTF-8?q?=E6=BB=9A=E5=8A=A8=E4=BD=8D=E7=A7=BB=E4=BF=9D=E6=8C=81=E4=B8=80?= =?UTF-8?q?=E8=87=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 Layout body (#page-scroll) 作为唯一滚动容器,左右两栏同容器滚动 - 滚动区域从搜索框下方开始,搜索框固定 - 内容区 overflow visible、侧栏 height auto;返回顶部/路由归零改用 page-scroll - 验证:滚动 300px 后两侧 top 均为 -152,搜索框不动,返回顶部有效 --- HANDOFF.md | 1 + docs/ADMIN-PLAN.md | 7 ++ stash-web/src/AppNext.tsx | 81 ++++++++++--------- stash-web/src/components/common/BackToTop.tsx | 7 +- 4 files changed, 54 insertions(+), 42 deletions(-) diff --git a/HANDOFF.md b/HANDOFF.md index e8a15c7..f07643b 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -167,6 +167,7 @@ GET /api/backup SQLite 快照附件 需登录 - 当前 `PRAGMA user_version = 22`;`schema::migrate` 逐级升级:0 建全量、15→16→17→18→19→20→21→22、更高版本拒绝启动。 - 格式在 `entry.format`(apps / posters / articles),同组可混合;`node` 的旧 `view_mode` 仅保留兼容,不再决定渲染。 - `settings.sidebar.show_profile / show_service` 控制侧栏两张卡片显隐(默认 true)。 +- 首页滚动:`#page-scroll`(Layout 的 body)是唯一滚动容器,搜索框固定在其上方;左右两栏放在同一容器内,位移天然一致。 - 本地服务是运行时卡片,但 `card` 表里有一行 `local-service` 只用于排序;内容来自 `/api/projects`,后端禁止删除/改名/配置分组。 - `node` 是统一分组树(根节点 `kind='section'`,子节点 `kind='group'` 可多层);`enabled` 控制启停;`icon` 可为空。 - 主要表:`node`、`entry`、`entry_tag`、`section_block`、`admin`、`session`、`setting`、`audit`。 diff --git a/docs/ADMIN-PLAN.md b/docs/ADMIN-PLAN.md index f914066..d152e6d 100644 --- a/docs/ADMIN-PLAN.md +++ b/docs/ADMIN-PLAN.md @@ -147,6 +147,13 @@ DELETE /api/items/:id ## 11. 实施记录 +### P19(2026-10-31,左右两栏共用滚动容器) + +- 不再用 `sider.scrollTop = content.scrollTop` 模拟同步:把 Layout 的 body(`#page-scroll`)设为滚动容器,`margin-top: 88px`、高度 `calc(100% - 88px)`,左右两栏都在这个容器里; +- 搜索框仍 fixed 在页头下方;滚动内容从搜索框下方开始;左右两栏位移完全一致(滚动 300px 后两边 top 都是 -152); +- 内容区 `overflow: visible`,侧栏高度改为 auto;返回顶部与路由切换都改用 `#page-scroll`; +- 验证:滚动后两栏 top 相同、搜索框位置不变、返回顶部回到 0、切分组后滚动归零。 + ### P18(2026-10-31,搜索框下滚动、本地服务排序、拖拽末位修复) - 前台恢复「内容区内部滚动」:页头与搜索框固定,滚动区域从搜索框下方开始;侧栏不再独立滚动,内容区滚动时同步设置侧栏 `scrollTop`,滚轮在侧栏上也驱动内容区; diff --git a/stash-web/src/AppNext.tsx b/stash-web/src/AppNext.tsx index fb1caa3..7752e3d 100644 --- a/stash-web/src/AppNext.tsx +++ b/stash-web/src/AppNext.tsx @@ -80,7 +80,7 @@ function useRoute() { if (window.location.pathname === path) return window.history.pushState({}, '', path) setRoute(parseRoute(path)) - document.getElementById('content-scroll')?.scrollTo({ top: 0 }) + document.getElementById('page-scroll')?.scrollTo({ top: 0 }) window.scrollTo({ top: 0 }) } @@ -191,50 +191,51 @@ export default function AppNext() { return () => source.close() }, []) - useEffect(() => { - const sync = () => { - const inner = document.getElementById('content-scroll')?.scrollTop ?? 0 - setScrolled(Math.max(inner, window.scrollY) > 4) - } - sync() - window.addEventListener('scroll', sync, { passive: true }) - document.addEventListener('scroll', sync, { passive: true, capture: true }) - return () => { - window.removeEventListener('scroll', sync) - document.removeEventListener('scroll', sync, { capture: true }) - } - }, []) - useEffect(() => { const content = document.getElementById('content-scroll') - const sider = document.querySelector('aside') - if (!(content instanceof HTMLElement) || !(sider instanceof HTMLElement)) return - - // 左右两栏共用一个滚动位置:侧栏没有独立滚动条,滚轮也驱动内容区 - sider.id = 'sidebar-scroll' - sider.style.overflow = 'hidden' - let frame = 0 - const sync = () => { - if (frame !== 0) return - frame = window.requestAnimationFrame(() => { - frame = 0 - sider.scrollTop = content.scrollTop - }) - } - const onWheel = (event: WheelEvent) => { - event.preventDefault() - content.scrollTop += event.deltaY + const main = content?.parentElement + const body = main?.parentElement + if ( + !(content instanceof HTMLElement) || + !(main instanceof HTMLElement) || + !(body instanceof HTMLElement) + ) { + return } - content.addEventListener('scroll', sync, { passive: true }) - sider.addEventListener('wheel', onWheel, { passive: false }) + // 左右两栏放进同一个滚动容器:滚动区域从搜索框下方开始,两栏位移完全一致 + const sider = body.querySelector('aside') + body.id = 'page-scroll' + body.style.overflowY = 'auto' + body.style.overflowX = 'hidden' + body.style.marginTop = '88px' + body.style.height = 'calc(100% - 88px)' + content.style.overflow = 'visible' + content.style.marginTop = '0' + main.style.height = 'auto' + if (sider instanceof HTMLElement) { + sider.style.overflow = 'visible' + sider.style.height = 'auto' + } + + const onScroll = () => setScrolled(body.scrollTop > 4) + body.addEventListener('scroll', onScroll, { passive: true }) + onScroll() return () => { - content.removeEventListener('scroll', sync) - sider.removeEventListener('wheel', onWheel) - if (frame !== 0) window.cancelAnimationFrame(frame) - sider.id = '' - sider.style.overflow = '' + body.removeEventListener('scroll', onScroll) + body.id = '' + body.style.overflowY = '' + body.style.overflowX = '' + body.style.marginTop = '' + body.style.height = '' + content.style.overflow = '' + content.style.marginTop = '' + main.style.height = '' + if (sider instanceof HTMLElement) { + sider.style.overflow = '' + sider.style.height = '' + } } }, []) @@ -650,7 +651,7 @@ export default function AppNext() { - + ) } diff --git a/stash-web/src/components/common/BackToTop.tsx b/stash-web/src/components/common/BackToTop.tsx index 68056ff..3cc4b68 100644 --- a/stash-web/src/components/common/BackToTop.tsx +++ b/stash-web/src/components/common/BackToTop.tsx @@ -6,8 +6,11 @@ import { ScrollTop } from '@stash/ui' type BackToTopProps = { /** 页面是否已滚动(由 App 传入) */ visible: boolean + /** 滚动容器 id,默认旧版内容区 */ + targetId?: string } -export function BackToTop({ visible }: BackToTopProps) { - return +export function BackToTop({ visible, targetId = 'content-scroll' }: BackToTopProps) { + return } +