feat: 接入深色配色(浅色 / 深色 / 跟随系统)

- settings.site 新增 theme,落在 <html data-scheme> 上
- 深色完全由组件库的令牌块接管,页面与组件都不用改
- 后台「页头 → 配色」可选,保存后前后台即时切换
- 顶部背景图是浅色渐变图,深色下隐藏(.site-background)
This commit is contained in:
2026-11-01 15:41:22 +08:00
parent 8a782099f5
commit 2f2ceb9ea0
5 changed files with 47 additions and 2 deletions
+14
View File
@@ -206,6 +206,17 @@ impl Default for ActionSettings {
} }
} }
/// 配色:浅色 / 深色 / 跟随系统。
/// 落到 `<html data-scheme>` 上,深色由组件库的令牌块接管,页面不用改组件。
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum ThemeSetting {
Light,
Dark,
#[default]
Auto,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(default)] #[serde(default)]
pub struct SiteMetaSettings { pub struct SiteMetaSettings {
@@ -213,6 +224,8 @@ pub struct SiteMetaSettings {
pub title: String, pub title: String,
/// favicon 地址;空字符串表示用内置图标 /// favicon 地址;空字符串表示用内置图标
pub favicon: String, pub favicon: String,
/// 配色
pub theme: ThemeSetting,
pub brand: BrandSettings, pub brand: BrandSettings,
pub actions: ActionSettings, pub actions: ActionSettings,
} }
@@ -222,6 +235,7 @@ impl Default for SiteMetaSettings {
Self { Self {
title: "Stash · 本地应用工作区".into(), title: "Stash · 本地应用工作区".into(),
favicon: String::new(), favicon: String::new(),
theme: ThemeSetting::Auto,
brand: BrandSettings::default(), brand: BrandSettings::default(),
actions: ActionSettings::default(), actions: ActionSettings::default(),
} }
+5
View File
@@ -34,6 +34,11 @@
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 {
display: none;
}
/* ---------- 页头 ---------- */ /* ---------- 页头 ---------- */
.site-header-inner { .site-header-inner {
+6
View File
@@ -284,6 +284,7 @@ export default function AppNext() {
site: { site: {
title: 'Stash · 本地应用工作区', title: 'Stash · 本地应用工作区',
favicon: '', favicon: '',
theme: 'auto',
brand: { name: 'STASH', mark: 'S', logo: '' }, brand: { name: 'STASH', mark: 'S', logo: '' },
actions: { refresh: true, settings: true, session: true }, actions: { refresh: true, settings: true, session: true },
}, },
@@ -291,6 +292,11 @@ export default function AppNext() {
const keywordLower = keyword.trim().toLowerCase() const keywordLower = keyword.trim().toLowerCase()
// 配色:写在 <html data-scheme> 上,深色由组件库的令牌块接管
useEffect(() => {
document.documentElement.dataset.scheme = site.site.theme
}, [site.site.theme])
// 站点级设置:浏览器标签标题与 favicon(空值回落到内置) // 站点级设置:浏览器标签标题与 favicon(空值回落到内置)
useEffect(() => { useEffect(() => {
const title = site.site.title.trim() const title = site.site.title.trim()
+17 -2
View File
@@ -33,6 +33,7 @@ import {
type SettingsDocument, type SettingsDocument,
type SettingsTone, type SettingsTone,
type SiteSettings, type SiteSettings,
type SiteTheme,
} from '../../settings' } from '../../settings'
import { import {
createCard, createCard,
@@ -126,13 +127,14 @@ export function AdminNext() {
void load() void load()
}, []) }, [])
// 后台壳也要吃站点级设置:保存后标签标题 / favicon 立刻变,否则会以为「设了没反应」 // 后台壳也要吃站点级设置:保存后标签标题 / favicon / 配色立刻变,否则会以为「设了没反应」
// 注意:这里的 `document` 是设置文档,浏览器文档要用 `window.document` // 注意:这里的 `document` 是设置文档,浏览器文档要用 `window.document`
useEffect(() => { useEffect(() => {
const meta = document?.settings.site const meta = document?.settings.site
if (meta === undefined) return if (meta === undefined) return
const title = meta.title.trim() const title = meta.title.trim()
if (title !== '') window.document.title = title if (title !== '') window.document.title = title
window.document.documentElement.dataset.scheme = meta.theme
const href = meta.favicon.trim() === '' ? '/favicon.svg' : meta.favicon.trim() const href = meta.favicon.trim() === '' ? '/favicon.svg' : meta.favicon.trim()
let link = window.document.head.querySelector<HTMLLinkElement>('link[rel="icon"]') let link = window.document.head.querySelector<HTMLLinkElement>('link[rel="icon"]')
if (link === null) { if (link === null) {
@@ -141,7 +143,7 @@ export function AdminNext() {
window.document.head.appendChild(link) window.document.head.appendChild(link)
} }
link.href = href link.href = href
}, [document?.settings.site.title, document?.settings.site.favicon]) }, [document?.settings.site.title, document?.settings.site.favicon, document?.settings.site.theme])
const authenticated = session?.authenticated === true const authenticated = session?.authenticated === true
@@ -1762,6 +1764,12 @@ function CardTable({
const TONE_SELECT = TONE_OPTIONS.map((option) => ({ value: option.value, label: option.label })) const TONE_SELECT = TONE_OPTIONS.map((option) => ({ value: option.value, label: option.label }))
const THEME_SELECT = [
{ value: 'auto', label: '跟随系统' },
{ value: 'light', label: '浅色' },
{ value: 'dark', label: '深色' },
]
function HeaderPanel({ function HeaderPanel({
value, value,
onChange, onChange,
@@ -1794,6 +1802,13 @@ function HeaderPanel({
onChange={(event) => onChangeSite({ ...site, favicon: event.target.value })} onChange={(event) => onChangeSite({ ...site, favicon: event.target.value })}
/> />
</FormField> </FormField>
<FormField label="配色" hint="深色由组件库的令牌块接管,页面与组件都不用改">
<Select
options={THEME_SELECT}
value={site.theme}
onChange={(theme) => onChangeSite({ ...site, theme: theme as SiteTheme })}
/>
</FormField>
<Text size="sm" strong> <Text size="sm" strong>
首页 Logo 首页 Logo
+5
View File
@@ -64,11 +64,15 @@ export type SiteSettings = {
site: SiteMetaSettings site: SiteMetaSettings
} }
export type SiteTheme = 'light' | 'dark' | 'auto'
export type SiteMetaSettings = { export type SiteMetaSettings = {
/** 浏览器标签标题 */ /** 浏览器标签标题 */
title: string title: string
/** favicon 地址;空表示用内置图标 */ /** favicon 地址;空表示用内置图标 */
favicon: string favicon: string
/** 配色:浅色 / 深色 / 跟随系统 */
theme: SiteTheme
brand: { brand: {
/** 页头品牌文字,可留空 */ /** 页头品牌文字,可留空 */
name: string name: string
@@ -137,6 +141,7 @@ export function defaultSettings(): SiteSettings {
site: { site: {
title: 'Stash · 本地应用工作区', title: 'Stash · 本地应用工作区',
favicon: '', favicon: '',
theme: 'auto',
brand: { name: 'STASH', mark: 'S', logo: '' }, brand: { name: 'STASH', mark: 'S', logo: '' },
actions: { refresh: true, settings: true, session: true }, actions: { refresh: true, settings: true, session: true },
}, },