fix(theme): 消除首帧主题闪动并柔化明暗过渡
- index.html 首帧按本机偏好设置 dark/light,避免先浅后深 - 实际 scheme 只写 dark/light(之前 auto 会覆盖成浅色) - 切换时给所有表面加 240ms 颜色/边框/阴影过渡,结束后移除 - 背景图深色下淡出而不是 display:none - 页头操作图标与分组导航等设置加载后再渲染,避免先显示后消失
This commit is contained in:
@@ -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
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user