fix: 侧栏顶部虚化遮罩与半透明卡片

- 新增固定侧栏遮罩层,滚动后淡入,blur(14px) + 透明渐变,不随吸底 transform 移动
- 侧栏资料/服务/导航卡改为 64% 半透明 + 毛玻璃
- 修正只输出 -webkit-backdrop-filter 导致不生效的问题
- 验证:遮罩 top=148、blur 生效、卡片 alpha=0.64
This commit is contained in:
2026-10-31 16:00:45 +08:00
parent 4304fd75b4
commit 50aea29ea6
4 changed files with 83 additions and 21 deletions
+1 -1
View File
@@ -167,7 +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)是唯一滚动容器,搜索框固定在其上方;左右两栏顶部对齐,左侧到底后通过 `translateY` 吸底,右侧继续滚动;滚动条隐藏。
- 首页滚动:`#page-scroll`(Layout 的 body)是唯一滚动容器,搜索框固定在其上方;左右两栏顶部对齐,左侧到底后通过 `translateY` 吸底,右侧继续滚动;滚动条隐藏;侧栏顶部有独立 fixed 模糊遮罩(`backdrop-filter`),卡片半透明。
- 本地服务是运行时卡片,但 `card` 表里有一行 `local-service` 只用于排序;内容来自 `/api/projects`,后端禁止删除/改名/配置分组。
- `node` 是统一分组树(根节点 `kind='section'`,子节点 `kind='group'` 可多层);`enabled` 控制启停;`icon` 可为空。
- 主要表:`node`、`entry`、`entry_tag`、`section_block`、`admin`、`session`、`setting`、`audit`。
+8
View File
@@ -147,6 +147,14 @@ DELETE /api/items/:id
## 11. 实施记录
### P21(2026-10-31,侧栏虚化遮罩与透明度)
- 侧栏顶部增加独立 fixed 遮罩层:位置跟随侧栏左右/宽度,固定在滚动区顶部(y=148),滚动后淡入;
- 遮罩用 `backdrop-filter: blur(14px)` + 45%→20%→透明的页面色渐变,既虚化又不挡内容;不能挂在侧栏内部(会被吸底 `translateY` 带走);
- 左侧资料卡 / 本地服务 / 导航卡改成 64% 不透明 + `backdrop-filter` 毛玻璃,露出页面背景;
- 修复 Tailwind/构建器只保留 `-webkit-backdrop-filter` 导致 Chromium 不生效的问题:只写标准属性;
- 验证:滚动后遮罩 top=148、opacity=1、backdrop-filter=blur(14px) saturate(1.1),卡片背景 alpha=0.64。
### P20(2026-10-31,双栏非对称吸底滚动)
- 两栏顶部对齐:去掉首页侧栏额外的 `padding-top`,两栏都从 `#page-scroll` 顶部(y=148)开始;
+34
View File
@@ -1314,6 +1314,40 @@ a.entry-href:hover {
will-change: transform;
}
.sidebar-wrap {
position: relative;
}
/* 侧栏进入滚动后,顶部做半透明虚化遮罩,避免内容被硬切 */
.sidebar-fade {
position: fixed;
z-index: 20;
height: 56px;
pointer-events: none;
opacity: 0;
background: linear-gradient(
to bottom,
color-mix(in srgb, var(--su-bg-page) 45%, transparent) 0%,
color-mix(in srgb, var(--su-bg-page) 20%, transparent) 55%,
transparent 100%
);
backdrop-filter: blur(14px) saturate(1.1);
transition: opacity var(--su-duration-fast) var(--su-ease-in-out);
}
.sidebar-fade.is-visible {
opacity: 1;
}
/* 左侧卡片半透明,露出页面背景 */
.sidebar-home .profile-card,
.sidebar-home .service-panel,
.sidebar-home .nav-card {
background-color: color-mix(in srgb, var(--su-bg-card) 64%, transparent);
border-color: color-mix(in srgb, var(--su-card-border) 55%, transparent);
backdrop-filter: blur(14px) saturate(1.15);
}
.sidebar-divider {
margin: var(--su-space-2) 0;
border-top: 1px dashed var(--su-border-strong);
+40 -20
View File
@@ -220,6 +220,22 @@ export default function AppNext() {
}
const onScroll = () => setScrolled(body.scrollTop > 4)
// 独立渐隐层:固定在滚动区顶部,不随侧栏 transform 移动
const fade = document.createElement('div')
fade.className = 'sidebar-fade'
fade.setAttribute('aria-hidden', 'true')
document.body.appendChild(fade)
const updateFade = () => {
if (!(sider instanceof HTMLElement)) return
const rect = sider.getBoundingClientRect()
fade.style.left = `${rect.left}px`
fade.style.width = `${rect.width}px`
fade.style.top = `${body.getBoundingClientRect().top}px`
fade.classList.toggle('is-visible', body.scrollTop > 4 && rect.width > 0)
}
const updateSticky = () => {
const viewport = body.clientHeight
const maxLeft = Math.max(0, sider instanceof HTMLElement ? sider.offsetHeight - viewport : 0)
@@ -227,6 +243,7 @@ export default function AppNext() {
if (sider instanceof HTMLElement) {
sider.style.transform = offset > 0 ? `translateY(${offset}px)` : ''
}
updateFade()
}
body.addEventListener('scroll', onScroll, { passive: true })
@@ -243,6 +260,7 @@ export default function AppNext() {
body.removeEventListener('scroll', updateSticky)
window.removeEventListener('resize', updateSticky)
observer.disconnect()
fade.remove()
body.id = ''
body.style.overflowY = ''
body.style.overflowX = ''
@@ -627,26 +645,28 @@ export default function AppNext() {
/>
}
sider={
<div className="sidebar sidebar-home">
{site.sidebar.show_profile !== false && <ProfileCard profile={site.profile} />}
{site.sidebar.show_service !== false && (
<ServicePanel
projects={projects}
live={live}
refreshing={refreshing}
onRefresh={refresh}
/>
)}
<div className="nav-card">
<Menu
items={sidebarItems}
activeKey={activeKey}
itemHeight={39}
onSelect={(key) => {
if (key === 'home') navigate('/')
else navigate(`/g/${encodeURIComponent(key.slice(2))}`)
}}
/>
<div className="sidebar-wrap">
<div className="sidebar sidebar-home">
{site.sidebar.show_profile !== false && <ProfileCard profile={site.profile} />}
{site.sidebar.show_service !== false && (
<ServicePanel
projects={projects}
live={live}
refreshing={refreshing}
onRefresh={refresh}
/>
)}
<div className="nav-card">
<Menu
items={sidebarItems}
activeKey={activeKey}
itemHeight={39}
onSelect={(key) => {
if (key === 'home') navigate('/')
else navigate(`/g/${encodeURIComponent(key.slice(2))}`)
}}
/>
</div>
</div>
</div>
}