feat: 接入深色配色(浅色 / 深色 / 跟随系统)
- settings.site 新增 theme,落在 <html data-scheme> 上 - 深色完全由组件库的令牌块接管,页面与组件都不用改 - 后台「页头 → 配色」可选,保存后前后台即时切换 - 顶部背景图是浅色渐变图,深色下隐藏(.site-background)
This commit is contained in:
@@ -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(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 },
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user