fix: 左右两栏共用滚动容器,滚动位移保持一致
- 将 Layout body (#page-scroll) 作为唯一滚动容器,左右两栏同容器滚动 - 滚动区域从搜索框下方开始,搜索框固定 - 内容区 overflow visible、侧栏 height auto;返回顶部/路由归零改用 page-scroll - 验证:滚动 300px 后两侧 top 均为 -152,搜索框不动,返回顶部有效
This commit is contained in:
@@ -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`。
|
||||
|
||||
@@ -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`,滚轮在侧栏上也驱动内容区;
|
||||
|
||||
+40
-39
@@ -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 = ''
|
||||
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() {
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<BackToTop visible={scrolled} />
|
||||
<BackToTop visible={scrolled} targetId="page-scroll" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,8 +6,11 @@ import { ScrollTop } from '@stash/ui'
|
||||
type BackToTopProps = {
|
||||
/** 页面是否已滚动(由 App 传入) */
|
||||
visible: boolean
|
||||
/** 滚动容器 id,默认旧版内容区 */
|
||||
targetId?: string
|
||||
}
|
||||
|
||||
export function BackToTop({ visible }: BackToTopProps) {
|
||||
return <ScrollTop visible={visible} targetId="content-scroll" offset={30} />
|
||||
export function BackToTop({ visible, targetId = 'content-scroll' }: BackToTopProps) {
|
||||
return <ScrollTop visible={visible} targetId={targetId} offset={30} />
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user