fix(theme): 消除首帧主题闪动并柔化明暗过渡

- index.html 首帧按本机偏好设置 dark/light,避免先浅后深
- 实际 scheme 只写 dark/light(之前 auto 会覆盖成浅色)
- 切换时给所有表面加 240ms 颜色/边框/阴影过渡,结束后移除
- 背景图深色下淡出而不是 display:none
- 页头操作图标与分组导航等设置加载后再渲染,避免先显示后消失
This commit is contained in:
2026-11-03 20:09:24 +08:00
parent dacb30e548
commit 3330af818e
5 changed files with 94 additions and 37 deletions
+9 -1
View File
@@ -4,7 +4,15 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="light" /> <meta name="color-scheme" content="light dark" />
<script>
// 首帧先按本机偏好上色,避免先浅色再切深色的闪动;站点默认值随后由设置接管。
try {
var saved = localStorage.getItem('stash-theme')
var dark = saved ? saved === 'dark' : matchMedia('(prefers-color-scheme: dark)').matches
document.documentElement.dataset.scheme = dark ? 'dark' : 'light'
} catch (e) {}
</script>
<!-- 与 Alright 主题一致的正文字体:霞鹜文楷 --> <!-- 与 Alright 主题一致的正文字体:霞鹜文楷 -->
<link <link
rel="stylesheet" rel="stylesheet"
+27 -2
View File
@@ -22,6 +22,7 @@
height: 65vh; height: 65vh;
background: url('./assets/site-bg.jpg') center top / cover no-repeat; background: url('./assets/site-bg.jpg') center top / cover no-repeat;
pointer-events: none; pointer-events: none;
transition: opacity 240ms var(--su-ease-in-out);
} }
.site-background::after { .site-background::after {
@@ -34,9 +35,9 @@
background: linear-gradient(to bottom, transparent, var(--su-bg-page)); background: linear-gradient(to bottom, transparent, var(--su-bg-page));
} }
/* 这张背景图是浅色渐变图,深色下留着会糊成一层脏灰,直接不显示 */ /* 这张背景图是浅色渐变图,深色下留着会糊成一层脏灰,淡出而不是直接消失 */
[data-scheme='dark'] .site-background { [data-scheme='dark'] .site-background {
display: none; opacity: 0;
} }
/* ---------- 页头 ---------- */ /* ---------- 页头 ---------- */
@@ -1328,3 +1329,27 @@ a.entry-href:hover {
color: var(--su-color-danger); color: var(--su-color-danger);
background-color: color-mix(in srgb, var(--su-color-danger) 10%, transparent); background-color: color-mix(in srgb, var(--su-color-danger) 10%, transparent);
} }
/* ---------- 明暗切换:短暂给所有表面补颜色过渡 ---------- */
/* 只在切换的 260ms 内加 !important,避免覆盖组件平时的 hover/展开动画。 */
html.theme-transition,
html.theme-transition *,
html.theme-transition *::before,
html.theme-transition *::after {
transition:
background-color 240ms var(--su-ease-in-out),
border-color 240ms var(--su-ease-in-out),
color 240ms var(--su-ease-in-out),
fill 240ms var(--su-ease-in-out),
stroke 240ms var(--su-ease-in-out),
box-shadow 240ms var(--su-ease-in-out) !important;
}
@media (prefers-reduced-motion: reduce) {
html.theme-transition,
html.theme-transition *,
html.theme-transition *::before,
html.theme-transition *::after {
transition: none !important;
}
}
+15 -6
View File
@@ -300,13 +300,21 @@ export default function AppNext() {
readThemeOverride(), readThemeOverride(),
) )
const effectiveTheme = themeOverride ?? site.site.theme const effectiveTheme = themeOverride ?? site.site.theme
useEffect(() => {
document.documentElement.dataset.scheme = effectiveTheme
}, [effectiveTheme])
/** 当前实际是深色吗(auto 时按系统偏好算) */ /** 当前实际是深色吗(auto 时按系统偏好算) */
const isDark = isDarkScheme(effectiveTheme) const isDark = isDarkScheme(effectiveTheme)
const scheme = isDark ? 'dark' : 'light'
useEffect(() => {
const root = document.documentElement
if (root.dataset.scheme === scheme) return
root.classList.add('theme-transition')
root.dataset.scheme = scheme
const timer = window.setTimeout(() => root.classList.remove('theme-transition'), 260)
return () => {
window.clearTimeout(timer)
root.classList.remove('theme-transition')
}
}, [scheme])
/** 页头的明暗切换:只写本机偏好,不需要登录;站点默认值在后台改 */ /** 页头的明暗切换:只写本机偏好,不需要登录;站点默认值在后台改 */
const toggleTheme = () => { const toggleTheme = () => {
@@ -663,6 +671,7 @@ export default function AppNext() {
headerSpeed={site.header.speed} headerSpeed={site.header.speed}
headerGap={site.header.gap} headerGap={site.header.gap}
site={site.site} site={site.site}
ready={settings !== null}
authenticated={session?.authenticated === true} authenticated={session?.authenticated === true}
username={session?.username ?? undefined} username={session?.username ?? undefined}
refreshing={refreshing} refreshing={refreshing}
@@ -707,7 +716,7 @@ export default function AppNext() {
onOpenApi={() => navigate('/interface')} onOpenApi={() => navigate('/interface')}
/> />
)} )}
{settings !== null && site.sidebar.show_nav !== false && ( {settings !== null && !loading && site.sidebar.show_nav !== false && (
<div className="nav-card"> <div className="nav-card">
<Menu <Menu
items={sidebarItems} items={sidebarItems}
+39 -28
View File
@@ -25,6 +25,8 @@ type SiteHeaderProps = {
headerGap?: number headerGap?: number
/** 品牌与操作工具配置 */ /** 品牌与操作工具配置 */
site: SiteMetaSettings site: SiteMetaSettings
/** 站点设置是否已加载;未加载时不渲染依赖配置的图标,避免先显示再消失 */
ready?: boolean
/** 是否已登录:决定右侧是「登录」还是「退出登录」 */ /** 是否已登录:决定右侧是「登录」还是「退出登录」 */
authenticated: boolean authenticated: boolean
username?: string username?: string
@@ -55,6 +57,7 @@ export function SiteHeader({
headerSpeed, headerSpeed,
headerGap, headerGap,
site, site,
ready = true,
authenticated, authenticated,
username, username,
refreshing = false, refreshing = false,
@@ -77,7 +80,7 @@ export function SiteHeader({
/> />
<div className="header-ticker-slot"> <div className="header-ticker-slot">
{headerEnabled && ( {ready && headerEnabled && (
<Ticker speed={headerSpeed} gap={headerGap} label="运行状态" className="header-ticker"> <Ticker speed={headerSpeed} gap={headerGap} label="运行状态" className="header-ticker">
{headerItems.map((item, index) => ( {headerItems.map((item, index) => (
<span className="header-ticker-item" key={`${index}-${item.text}`}> <span className="header-ticker-item" key={`${index}-${item.text}`}>
@@ -94,34 +97,42 @@ export function SiteHeader({
</div> </div>
<div className="header-actions"> <div className="header-actions">
{site.actions.theme && ( {ready && (
<IconButton <>
label={dark ? '切换到浅色' : '切换到深色'} {site.actions.theme && (
icon={<Icon name={dark ? 'sun_line' : 'moon_line'} />} <IconButton
onClick={onToggleTheme} label={dark ? '切换到浅色' : '切换到深色'}
/> icon={<Icon name={dark ? 'sun_line' : 'moon_line'} />}
onClick={onToggleTheme}
/>
)}
{site.actions.refresh && (
<IconButton
label="刷新(重扫项目并重拉内容)"
icon={<Icon name="lightning_line" />}
spinning={refreshing}
onClick={onRefresh}
/>
)}
{site.actions.settings && authenticated && (
<IconButton
label="站点设置"
icon={<Icon name="settings_1_line" />}
onClick={onSettings}
/>
)}
{site.actions.session &&
(authenticated ? (
<IconButton
label={username === undefined ? '退出登录' : `退出登录(${username})`}
icon={<Icon name="exit_line" />}
onClick={onLogout}
/>
) : (
<IconButton label="登录" icon={<Icon name="user_3_line" />} onClick={onLogin} />
))}
</>
)} )}
{site.actions.refresh && (
<IconButton
label="刷新(重扫项目并重拉内容)"
icon={<Icon name="lightning_line" />}
spinning={refreshing}
onClick={onRefresh}
/>
)}
{site.actions.settings && authenticated && (
<IconButton label="站点设置" icon={<Icon name="settings_1_line" />} onClick={onSettings} />
)}
{site.actions.session &&
(authenticated ? (
<IconButton
label={username === undefined ? '退出登录' : `退出登录(${username})`}
icon={<Icon name="exit_line" />}
onClick={onLogout}
/>
) : (
<IconButton label="登录" icon={<Icon name="user_3_line" />} onClick={onLogin} />
))}
</div> </div>
</div> </div>
) )
+4
View File
@@ -11,6 +11,10 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
html[data-scheme='dark'] {
color-scheme: dark;
}
* { * {
box-sizing: border-box; box-sizing: border-box;
margin: 0; margin: 0;