feat(admin): 增加背景管理

- settings 新增 background.light / background.dark,留空使用内置图
- 后台首页管理新增「背景管理」,两栏 URL 输入与实时预览,可一键恢复内置图
- 前台按设置覆盖明暗背景图,仍保留 65vh 与交叉淡入
- 保存/撤销沿用现有设置乐观锁流程
This commit is contained in:
2026-11-03 20:23:57 +08:00
parent b9005dd514
commit 5439ebf3be
6 changed files with 166 additions and 5 deletions
+43
View File
@@ -1379,3 +1379,46 @@ html.theme-transition .site-background {
transition: none !important;
}
}
/* 后台:背景管理预览 */
.background-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--su-space-5);
}
.background-section {
display: flex;
flex-direction: column;
gap: var(--su-space-3);
min-width: 0;
}
.background-preview {
position: relative;
height: 180px;
border: 1px solid var(--su-border);
border-radius: var(--su-radius-md);
background-color: var(--su-bg-subtle);
background-position: center top;
background-repeat: no-repeat;
background-size: cover;
overflow: hidden;
}
.background-preview span {
position: absolute;
bottom: var(--su-space-3);
left: var(--su-space-3);
padding: 2px var(--su-space-2);
border-radius: var(--su-radius-pill);
background-color: rgb(0 0 0 / 55%);
color: #fff;
font-size: var(--su-font-size-xs);
}
@media (width <= 900px) {
.background-grid {
grid-template-columns: minmax(0, 1fr);
}
}
+19 -2
View File
@@ -276,6 +276,7 @@ export default function AppNext() {
show_service: true,
show_nav: true,
},
background: { light: '', dark: '' },
profile: {
name: '本地管理员',
role: '管理员',
@@ -644,8 +645,24 @@ export default function AppNext() {
}}
/>
<div className="site-background site-background-light" aria-hidden="true" />
<div className="site-background site-background-dark" aria-hidden="true" />
<div
className="site-background site-background-light"
style={
site.background.light.trim() === ''
? undefined
: { backgroundImage: `url(${JSON.stringify(site.background.light.trim())})` }
}
aria-hidden="true"
/>
<div
className="site-background site-background-dark"
style={
site.background.dark.trim() === ''
? undefined
: { backgroundImage: `url(${JSON.stringify(site.background.dark.trim())})` }
}
aria-hidden="true"
/>
<Layout
contained
+73 -1
View File
@@ -40,6 +40,7 @@ import {
saveSettings,
TONE_OPTIONS,
type ProfileSettings,
type BackgroundSettings,
type SettingsDocument,
type SettingsTone,
type SiteSettings,
@@ -63,6 +64,8 @@ import {
import { createItem, deleteItem, fetchItem, saveItem } from '../../items'
import { useProjects } from '../../useProjects'
import type { Project } from '../../api'
import siteBgLight from '../../assets/site-bg.jpg'
import siteBgDark from '../../assets/site-bg-dark.jpg'
/**
* 新版后台:原子模型。
@@ -77,6 +80,7 @@ type TabKey =
| 'home-sidebar'
| 'home-cards'
| 'home-header'
| 'home-background'
| 'profile'
| 'system'
@@ -89,6 +93,7 @@ const ADMIN_NAV: MenuItem[] = [
label: '首页管理',
children: [
{ key: 'home-header', label: '页头' },
{ key: 'home-background', label: '背景管理' },
{ key: 'home-sidebar', label: '侧边栏' },
{ key: 'home-cards', label: '内容卡片' },
],
@@ -379,6 +384,13 @@ export function AdminNext() {
/>
)}
{tab === 'home-background' && (
<BackgroundPanel
value={draft.background}
onChange={(background) => setDraft({ ...draft, background })}
/>
)}
{tab === 'profile' && (
<ProfilePanel
value={draft.profile}
@@ -388,7 +400,9 @@ export function AdminNext() {
{tab === 'system' && <SystemPanel />}
{(tab === 'home-header' || tab === 'profile') && (
{(tab === 'home-header' ||
tab === 'home-background' ||
tab === 'profile') && (
<Space gap="sm" justify="end" className="admin-actions">
<Button
variant="secondary"
@@ -2037,6 +2051,64 @@ function HeaderPanel({
)
}
function BackgroundPanel({
value,
onChange,
}: {
value: BackgroundSettings
onChange: (background: BackgroundSettings) => void
}) {
const update = (patch: Partial<BackgroundSettings>) => onChange({ ...value, ...patch })
const preview = (url: string, fallback: string) => ({
backgroundImage: `url(${JSON.stringify(url.trim() || fallback)})`,
})
return (
<Space direction="column" gap="lg" align="stretch" className="admin-form">
<Title level={4}>背景管理</Title>
<Text size="sm" tone="muted">
说明:分别设置亮色与暗色模式顶部的背景图,留空使用内置图;站内图片请放到托管目录 public/ 后填写路径,或填完整 URL。
</Text>
<div className="background-grid">
<div className="background-section">
<FormField label="亮色背景图" htmlFor="background-light" hint="留空使用内置亮色背景">
<Input
id="background-light"
value={value.light}
placeholder="/bg-light.jpg 或 https://…"
onChange={(event) => update({ light: event.target.value })}
/>
</FormField>
<div className="background-preview" style={preview(value.light, siteBgLight)}>
<span>亮色预览</span>
</div>
</div>
<div className="background-section">
<FormField label="暗色背景图" htmlFor="background-dark" hint="留空使用内置暗色背景">
<Input
id="background-dark"
value={value.dark}
placeholder="/bg-dark.jpg 或 https://…"
onChange={(event) => update({ dark: event.target.value })}
/>
</FormField>
<div className="background-preview" style={preview(value.dark, siteBgDark)}>
<span>暗色预览</span>
</div>
</div>
</div>
<div>
<Button variant="secondary" onClick={() => onChange({ light: '', dark: '' })}>
恢复内置背景
</Button>
</div>
</Space>
)
}
function ProfilePanel({
value,
onChange,
+9
View File
@@ -23,6 +23,13 @@ export type ProfileSettings = {
avatar: string
}
export type BackgroundSettings = {
/** 亮色背景图地址;空表示使用内置图 */
light: string
/** 暗色背景图地址;空表示使用内置图 */
dark: string
}
export type SiteSettings = {
header: {
enabled: boolean
@@ -40,6 +47,7 @@ export type SiteSettings = {
/** 是否显示「分组导航」卡片(首页 / 分组树) */
show_nav: boolean
}
background: BackgroundSettings
profile: ProfileSettings
/** 站点级:标签标题 / favicon / 品牌 / 页头操作工具 */
site: SiteMetaSettings
@@ -107,6 +115,7 @@ export function defaultSettings(): SiteSettings {
show_service: true,
show_nav: true,
},
background: { light: '', dark: '' },
profile: {
name: '本地管理员',
role: '管理员',