Compare commits
10
Commits
54fd0edf84
...
bb5a615ed2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb5a615ed2 | ||
|
|
6c8eaa9f76 | ||
|
|
762de4a4e1 | ||
|
|
2a344ae534 | ||
|
|
ddef75f862 | ||
|
|
e13c2b2ce6 | ||
|
|
a2155a4c89 | ||
|
|
7a61acfa02 | ||
|
|
50aea29ea6 | ||
|
|
4304fd75b4 |
+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、更高版本拒绝启动。
|
||||
- 格式在 `entry.format`(apps / posters / articles),同组可混合;`node` 的旧 `view_mode` 仅保留兼容,不再决定渲染。
|
||||
- `settings.sidebar.show_profile / show_service` 控制侧栏两张卡片显隐(默认 true)。
|
||||
- 首页滚动:`#page-scroll`(Layout 的 body)是唯一滚动容器,搜索框固定在其上方;左右两栏放在同一容器内,位移天然一致。
|
||||
- 首页滚动:`#page-scroll`(Layout 的 body)是唯一滚动容器,搜索框固定在其上方;左右两栏顶部对齐,左侧到底后通过 `translateY` 吸底,右侧继续滚动;滚动条隐藏;页头实底,搜索框 50% 透明,顶部另有一层上下渐隐的软虚化(`blur(8px)`)。
|
||||
- 本地服务是运行时卡片,但 `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,75 @@ DELETE /api/items/:id
|
||||
|
||||
## 11. 实施记录
|
||||
|
||||
### P29(2026-10-31,搜索框不受虚化/透明度影响)
|
||||
|
||||
- 移除 `.search-row > *:first-child` 的半透明与 `backdrop-filter` 覆盖,搜索框回到组件默认实底白色;
|
||||
- 顶部软虚化只作用于搜索框周围区域,不再透过输入框;
|
||||
- 验证:搜索框背景 `rgb(255,255,255)`、`backdrop-filter: none`、opacity=1。
|
||||
|
||||
### P28(2026-10-31,搜索框 30% 透明与分组块底部留白)
|
||||
|
||||
- 搜索框背景透明度从 50% 调到 30%(`rgba(255,255,255,0.3)`),保留 blur;
|
||||
- 侧栏 `.sidebar-home` 增加 24px 底部内边距,分组导航块滚到底时距离视口底部保留 24px;
|
||||
- 验证:搜索框 alpha=0.30;页面滚到底时 navBottom=856、viewportBottom=880,间距 24px。
|
||||
|
||||
### P27(2026-10-31,分类块圆角与顶部软虚化)
|
||||
|
||||
- 侧栏导航 `.nav-card` 是普通 div(不是 Card),之前没有圆角;补 `border-radius: 12px` + `overflow: hidden`,和资料/服务卡一致;
|
||||
- 搜索框透明度提到 50%,模糊提到 14px;
|
||||
- 新增 `.top-blur`:固定在 header 下方(top 60、height 120),`blur(8px)` + 18% 页面色渐变,并用上下 `mask-image` 渐隐,避免再次出现分割线;
|
||||
- 验证:nav 圆角 12px、top-blur 存在且 blur 生效、搜索框仍可点击、滚动时顶部软虚化无硬边。
|
||||
|
||||
### P26(2026-10-31,顶部留白加高)
|
||||
|
||||
- 首页顶部偏移 `TOP_OFFSET` 从 88 调到 104,搜索框与内容卡片之间的空白从 28px 增加到 44px;
|
||||
- Layout `contentOffset` 与 `#page-scroll` 的 `margin-top`/`height` 使用同一个常量,左右两栏仍从 164 对齐开始;
|
||||
- 验证:content/sider top=164,search top=76 / bottom=120。
|
||||
|
||||
### P25(2026-10-31,去掉顶部块的接缝)
|
||||
|
||||
- 删除 `.top-dock` 整块:不再有 60–148 的半透明填充与底边,避免出现一条横向分割线;
|
||||
- 搜索框继续 fixed 悬浮,页头保持实底;顶部与内容之间只剩页面背景的自然渐变;
|
||||
- 背景图渐隐保持 80%,与内容起点无硬边;
|
||||
- 验证:`.top-dock` 数量 0,左侧与右侧顶部背景连续。
|
||||
|
||||
### P24(2026-10-31,独立的半透明顶部块)
|
||||
|
||||
- 页头恢复实底(`headerVariant="solid"`),去掉 `.app > header` 的透明度覆盖;
|
||||
- 页头下方新增独立 `.top-dock`:`position: fixed; top:60px; height:88px`,背景 28% 页面色 + `blur(10px)` + 半透明底边,作为承载搜索框的单独块,而不是让整条看起来像背景填充;
|
||||
- 搜索框保持 60% 半透明;
|
||||
- 背景图渐隐层恢复 80%(撤掉上一轮为露出背景做的 65%);
|
||||
- 验证:dock top=60、height=88、alpha=0.28;header 纯白;搜索框可见;无 mask 元素。
|
||||
|
||||
### P23(2026-10-31,去掉遮罩、顶部透明)
|
||||
|
||||
- 删除 `scroll-fade` 整条遮罩及创建逻辑,页面不再有顶部渐隐/模糊层;
|
||||
- 页头改为 `headerVariant="blur"`,并用 `.app > header` 把底色降到 55%;搜索框底色降到 60% 并保留轻微 blur;
|
||||
- 背景图渐隐层高度从 80% 调到 65%,让页头到内容之间那一条保持背景原色,不再发白;
|
||||
- 验证:`.scroll-fade` 数量为 0;header 背景 alpha=0.55;搜索框 alpha=0.60。
|
||||
|
||||
### P22(2026-10-31,遮罩合并为整条)
|
||||
|
||||
- 去掉内容区自带的 `.content-veil`,只保留一条固定遮罩;
|
||||
- 遮罩宽度从侧栏左边缘一直覆盖到内容区右边缘(实测 left=50、width=1340),左右同一水平高度(top=148),不再像两个块拼接;
|
||||
- 渐变、模糊参数不变,滚动后整体淡入。
|
||||
|
||||
### 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)开始;
|
||||
- 隐藏滚动条:`#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,左右两栏共用滚动容器)
|
||||
|
||||
- 不再用 `sider.scrollTop = content.scrollTop` 模拟同步:把 Layout 的 body(`#page-scroll`)设为滚动容器,`margin-top: 88px`、高度 `calc(100% - 88px)`,左右两栏都在这个容器里;
|
||||
|
||||
@@ -149,6 +149,33 @@
|
||||
box-shadow: var(--su-shadow-card);
|
||||
}
|
||||
|
||||
/* 搜索框保持实底清晰,透明度/虚化只作用于周围区域 */
|
||||
|
||||
/* 搜索区域软虚化:上下都用 mask 渐隐,不出现横向分割线 */
|
||||
.top-blur {
|
||||
position: fixed;
|
||||
top: 60px;
|
||||
right: 0;
|
||||
left: 0;
|
||||
z-index: 4;
|
||||
height: 120px;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
color-mix(in srgb, var(--su-bg-page) 22%, transparent) 0%,
|
||||
transparent 80%
|
||||
);
|
||||
backdrop-filter: blur(8px);
|
||||
mask-image: linear-gradient(to bottom, transparent 0%, #000 28%, #000 62%, transparent 100%);
|
||||
-webkit-mask-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0%,
|
||||
#000 28%,
|
||||
#000 62%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- 两栏骨架 ---------- */
|
||||
|
||||
.content {
|
||||
@@ -1299,6 +1326,46 @@ a.entry-href:hover {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 首页:左右栏共用一个滚动区,左侧到底后吸底 */
|
||||
#page-scroll {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
#page-scroll::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar-home {
|
||||
padding-top: 0;
|
||||
padding-bottom: 24px;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.sidebar-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 侧栏到内容区共用一条滚动虚化遮罩,同一水平高度 */
|
||||
|
||||
.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-home .nav-card {
|
||||
overflow: hidden;
|
||||
border-radius: var(--su-radius-md);
|
||||
}
|
||||
|
||||
.sidebar-divider {
|
||||
margin: var(--su-space-2) 0;
|
||||
border-top: 1px dashed var(--su-border-strong);
|
||||
|
||||
+57
-24
@@ -161,6 +161,8 @@ function matches(entry: Record<string, unknown>, keyword: string): boolean {
|
||||
return text.includes(keyword)
|
||||
}
|
||||
|
||||
const TOP_OFFSET = 104
|
||||
|
||||
export default function AppNext() {
|
||||
const { status, projects, error, live, refreshing, refresh } = useProjects()
|
||||
const { route, navigate } = useRoute()
|
||||
@@ -208,22 +210,50 @@ export default function AppNext() {
|
||||
body.id = 'page-scroll'
|
||||
body.style.overflowY = 'auto'
|
||||
body.style.overflowX = 'hidden'
|
||||
body.style.marginTop = '88px'
|
||||
body.style.height = 'calc(100% - 88px)'
|
||||
body.style.marginTop = `${TOP_OFFSET}px`
|
||||
body.style.height = `calc(100% - ${TOP_OFFSET}px)`
|
||||
content.style.overflow = 'visible'
|
||||
content.style.marginTop = '0'
|
||||
main.style.height = 'auto'
|
||||
if (sider instanceof HTMLElement) {
|
||||
sider.style.overflow = 'visible'
|
||||
sider.style.height = 'auto'
|
||||
sider.style.alignSelf = 'flex-start'
|
||||
}
|
||||
|
||||
const onScroll = () => setScrolled(body.scrollTop > 4)
|
||||
|
||||
// 顶部软虚化层:覆盖搜索区域与内容顶部,边缘用 mask 渐隐,避免硬分割线
|
||||
const blur = document.createElement('div')
|
||||
blur.className = 'top-blur'
|
||||
blur.setAttribute('aria-hidden', 'true')
|
||||
const appRoot = document.querySelector('.app')
|
||||
appRoot?.appendChild(blur)
|
||||
|
||||
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', updateSticky, { passive: true })
|
||||
window.addEventListener('resize', updateSticky)
|
||||
const observer = new ResizeObserver(updateSticky)
|
||||
observer.observe(content)
|
||||
if (sider instanceof HTMLElement) observer.observe(sider)
|
||||
onScroll()
|
||||
updateSticky()
|
||||
|
||||
return () => {
|
||||
body.removeEventListener('scroll', onScroll)
|
||||
body.removeEventListener('scroll', updateSticky)
|
||||
window.removeEventListener('resize', updateSticky)
|
||||
observer.disconnect()
|
||||
blur.remove()
|
||||
body.id = ''
|
||||
body.style.overflowY = ''
|
||||
body.style.overflowX = ''
|
||||
@@ -235,6 +265,8 @@ export default function AppNext() {
|
||||
if (sider instanceof HTMLElement) {
|
||||
sider.style.overflow = ''
|
||||
sider.style.height = ''
|
||||
sider.style.alignSelf = ''
|
||||
sider.style.transform = ''
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
@@ -581,7 +613,7 @@ export default function AppNext() {
|
||||
headerVariant="solid"
|
||||
siderBordered={false}
|
||||
contentPadding="none"
|
||||
contentOffset={88}
|
||||
contentOffset={TOP_OFFSET}
|
||||
height="screen"
|
||||
scrollable
|
||||
hideScrollbar
|
||||
@@ -606,32 +638,33 @@ export default function AppNext() {
|
||||
/>
|
||||
}
|
||||
sider={
|
||||
<div className="sidebar">
|
||||
{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>
|
||||
}
|
||||
>
|
||||
<div className="content">
|
||||
<div className={`content-veil${scrolled ? ' is-visible' : ''}`} aria-hidden="true" />
|
||||
<SearchBar value={keyword} onChange={setKeyword} inputRef={searchRef} />
|
||||
<h1 className="sr-only">Stash 本地应用工作区</h1>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user