fix: 双栏非对称吸底滚动与对齐
- 去掉首页侧栏额外 padding,左右两栏初始顶部对齐 - 隐藏 #page-scroll 滚动条 - 左侧到底后用 translateY 补偿吸附在视口底部,右侧继续滚动;右侧先到底时两栏同步 - ResizeObserver 重算吸附阈值 - 验证:初始 top 148/148;滚动后左栏 top 144 固定、bottom 900;无滚动条
This commit is contained in:
+1
-1
@@ -167,7 +167,7 @@ GET /api/backup SQLite 快照附件 需登录
|
|||||||
- 当前 `PRAGMA user_version = 22`;`schema::migrate` 逐级升级:0 建全量、15→16→17→18→19→20→21→22、更高版本拒绝启动。
|
- 当前 `PRAGMA user_version = 22`;`schema::migrate` 逐级升级:0 建全量、15→16→17→18→19→20→21→22、更高版本拒绝启动。
|
||||||
- 格式在 `entry.format`(apps / posters / articles),同组可混合;`node` 的旧 `view_mode` 仅保留兼容,不再决定渲染。
|
- 格式在 `entry.format`(apps / posters / articles),同组可混合;`node` 的旧 `view_mode` 仅保留兼容,不再决定渲染。
|
||||||
- `settings.sidebar.show_profile / show_service` 控制侧栏两张卡片显隐(默认 true)。
|
- `settings.sidebar.show_profile / show_service` 控制侧栏两张卡片显隐(默认 true)。
|
||||||
- 首页滚动:`#page-scroll`(Layout 的 body)是唯一滚动容器,搜索框固定在其上方;左右两栏放在同一容器内,位移天然一致。
|
- 首页滚动:`#page-scroll`(Layout 的 body)是唯一滚动容器,搜索框固定在其上方;左右两栏顶部对齐,左侧到底后通过 `translateY` 吸底,右侧继续滚动;滚动条隐藏。
|
||||||
- 本地服务是运行时卡片,但 `card` 表里有一行 `local-service` 只用于排序;内容来自 `/api/projects`,后端禁止删除/改名/配置分组。
|
- 本地服务是运行时卡片,但 `card` 表里有一行 `local-service` 只用于排序;内容来自 `/api/projects`,后端禁止删除/改名/配置分组。
|
||||||
- `node` 是统一分组树(根节点 `kind='section'`,子节点 `kind='group'` 可多层);`enabled` 控制启停;`icon` 可为空。
|
- `node` 是统一分组树(根节点 `kind='section'`,子节点 `kind='group'` 可多层);`enabled` 控制启停;`icon` 可为空。
|
||||||
- 主要表:`node`、`entry`、`entry_tag`、`section_block`、`admin`、`session`、`setting`、`audit`。
|
- 主要表:`node`、`entry`、`entry_tag`、`section_block`、`admin`、`session`、`setting`、`audit`。
|
||||||
|
|||||||
@@ -147,6 +147,14 @@ DELETE /api/items/:id
|
|||||||
|
|
||||||
## 11. 实施记录
|
## 11. 实施记录
|
||||||
|
|
||||||
|
### P20(2026-10-31,双栏非对称吸底滚动)
|
||||||
|
|
||||||
|
- 两栏顶部对齐:去掉首页侧栏额外的 `padding-top`,两栏都从 `#page-scroll` 顶部(y=148)开始;
|
||||||
|
- 隐藏滚动条:`#page-scroll` 使用 `scrollbar-width: none` / `::-webkit-scrollbar { display:none }`;
|
||||||
|
- 非对称吸底:仍共用一个滚动容器;滚动时用 `translateY(max(0, scrollTop - (侧栏高度 - 视口高度)))` 补偿左栏,左侧内容到底后吸附在视口底部,右侧继续滚动;
|
||||||
|
- 右侧先到底、左侧更长时不补偿,两栏同步滚到整体底部;`ResizeObserver` 在内容变化时重算;
|
||||||
|
- 验证:初始左右 top 均为 148;滚动 120/300/600 时左侧 top 稳定在 144、bottom 贴住 900,右侧正常滚动;页面无滚动条。
|
||||||
|
|
||||||
### P19(2026-10-31,左右两栏共用滚动容器)
|
### P19(2026-10-31,左右两栏共用滚动容器)
|
||||||
|
|
||||||
- 不再用 `sider.scrollTop = content.scrollTop` 模拟同步:把 Layout 的 body(`#page-scroll`)设为滚动容器,`margin-top: 88px`、高度 `calc(100% - 88px)`,左右两栏都在这个容器里;
|
- 不再用 `sider.scrollTop = content.scrollTop` 模拟同步:把 Layout 的 body(`#page-scroll`)设为滚动容器,`margin-top: 88px`、高度 `calc(100% - 88px)`,左右两栏都在这个容器里;
|
||||||
|
|||||||
@@ -1299,6 +1299,21 @@ a.entry-href:hover {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 首页:左右栏共用一个滚动区,左侧到底后吸底 */
|
||||||
|
#page-scroll {
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#page-scroll::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-home {
|
||||||
|
padding-top: 0;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-divider {
|
.sidebar-divider {
|
||||||
margin: var(--su-space-2) 0;
|
margin: var(--su-space-2) 0;
|
||||||
border-top: 1px dashed var(--su-border-strong);
|
border-top: 1px dashed var(--su-border-strong);
|
||||||
|
|||||||
@@ -216,14 +216,33 @@ export default function AppNext() {
|
|||||||
if (sider instanceof HTMLElement) {
|
if (sider instanceof HTMLElement) {
|
||||||
sider.style.overflow = 'visible'
|
sider.style.overflow = 'visible'
|
||||||
sider.style.height = 'auto'
|
sider.style.height = 'auto'
|
||||||
|
sider.style.alignSelf = 'flex-start'
|
||||||
}
|
}
|
||||||
|
|
||||||
const onScroll = () => setScrolled(body.scrollTop > 4)
|
const onScroll = () => setScrolled(body.scrollTop > 4)
|
||||||
|
const updateSticky = () => {
|
||||||
|
const viewport = body.clientHeight
|
||||||
|
const maxLeft = Math.max(0, sider instanceof HTMLElement ? sider.offsetHeight - viewport : 0)
|
||||||
|
const offset = Math.max(0, body.scrollTop - maxLeft)
|
||||||
|
if (sider instanceof HTMLElement) {
|
||||||
|
sider.style.transform = offset > 0 ? `translateY(${offset}px)` : ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
body.addEventListener('scroll', onScroll, { passive: true })
|
body.addEventListener('scroll', onScroll, { passive: true })
|
||||||
|
body.addEventListener('scroll', updateSticky, { passive: true })
|
||||||
|
window.addEventListener('resize', updateSticky)
|
||||||
|
const observer = new ResizeObserver(updateSticky)
|
||||||
|
observer.observe(content)
|
||||||
|
if (sider instanceof HTMLElement) observer.observe(sider)
|
||||||
onScroll()
|
onScroll()
|
||||||
|
updateSticky()
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
body.removeEventListener('scroll', onScroll)
|
body.removeEventListener('scroll', onScroll)
|
||||||
|
body.removeEventListener('scroll', updateSticky)
|
||||||
|
window.removeEventListener('resize', updateSticky)
|
||||||
|
observer.disconnect()
|
||||||
body.id = ''
|
body.id = ''
|
||||||
body.style.overflowY = ''
|
body.style.overflowY = ''
|
||||||
body.style.overflowX = ''
|
body.style.overflowX = ''
|
||||||
@@ -235,6 +254,8 @@ export default function AppNext() {
|
|||||||
if (sider instanceof HTMLElement) {
|
if (sider instanceof HTMLElement) {
|
||||||
sider.style.overflow = ''
|
sider.style.overflow = ''
|
||||||
sider.style.height = ''
|
sider.style.height = ''
|
||||||
|
sider.style.alignSelf = ''
|
||||||
|
sider.style.transform = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
@@ -606,7 +627,7 @@ export default function AppNext() {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
sider={
|
sider={
|
||||||
<div className="sidebar">
|
<div className="sidebar sidebar-home">
|
||||||
{site.sidebar.show_profile !== false && <ProfileCard profile={site.profile} />}
|
{site.sidebar.show_profile !== false && <ProfileCard profile={site.profile} />}
|
||||||
{site.sidebar.show_service !== false && (
|
{site.sidebar.show_service !== false && (
|
||||||
<ServicePanel
|
<ServicePanel
|
||||||
|
|||||||
Reference in New Issue
Block a user