feat: add ThemeEditor component with real-time preview and settings integration

This commit is contained in:
Linkon
2026-01-20 03:00:11 +08:00
parent 0c4877497b
commit 2403cfb588
3 changed files with 266 additions and 15 deletions
+11 -6
View File
@@ -5,7 +5,9 @@ import { Sidebar } from './components/Sidebar'
import { ContentArea } from './components/ContentArea'
import { Wizard } from './components/Wizard'
import { ThemeProvider } from './contexts/ThemeContext'
import { ThemeEditorProvider } from './contexts/ThemeEditorContext'
import { GlobalSidebar } from './components/GlobalSidebar'
import { ThemeEditor } from './components/ThemeEditor'
function MainContent(): React.JSX.Element {
const navigate = useNavigate()
@@ -143,12 +145,15 @@ function MainContent(): React.JSX.Element {
function App(): React.JSX.Element {
return (
<ThemeProvider>
<HashRouter>
<Routes>
<Route path="/global-sidebar" element={<GlobalSidebar />} />
<Route path="/*" element={<MainContent />} />
</Routes>
</HashRouter>
<ThemeEditorProvider>
<HashRouter>
<Routes>
<Route path="/global-sidebar" element={<GlobalSidebar />} />
<Route path="/*" element={<MainContent />} />
</Routes>
</HashRouter>
<ThemeEditor />
</ThemeEditorProvider>
</ThemeProvider>
)
}
+23 -9
View File
@@ -13,6 +13,7 @@ import {
MessagePlugin
} from 'tdesign-react'
import { useTheme } from '../contexts/ThemeContext'
import { useThemeEditor } from '../contexts/ThemeEditorContext'
type permissionLevel = 'admin' | 'points' | 'view'
type appSettings = {
@@ -23,6 +24,7 @@ type appSettings = {
export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission }) => {
const { themes, currentTheme, setTheme } = useTheme()
const { startEditing } = useThemeEditor()
const [activeTab, setActiveTab] = useState('appearance')
const [settings, setSettings] = useState<appSettings>({
is_wizard_completed: false,
@@ -261,15 +263,27 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
<Card style={{ backgroundColor: 'var(--ss-card-bg)', color: 'var(--ss-text-main)' }}>
<Form labelWidth={120}>
<Form.FormItem label="当前主题">
<Select
value={currentTheme?.id}
onChange={(v) => setTheme(v as string)}
style={{ width: '320px' }}
>
{themes.map((t) => (
<Select.Option key={t.id} value={t.id} label={t.name} />
))}
</Select>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<Select
value={currentTheme?.id}
onChange={(v) => setTheme(v as string)}
style={{ width: '200px' }}
>
{themes.map((t) => (
<Select.Option key={t.id} value={t.id} label={t.name} />
))}
</Select>
<Button
variant="outline"
theme="default"
onClick={() => startEditing(currentTheme || undefined)}
>
</Button>
<Button variant="text" theme="primary" onClick={() => startEditing()}>
</Button>
</div>
</Form.FormItem>
<Form.FormItem label="界面缩放">
+232
View File
@@ -0,0 +1,232 @@
import React, { useEffect } from 'react'
import {
Drawer,
Form,
Input,
Select,
ColorPicker,
Button,
Space,
Divider,
Row,
Col
} from 'tdesign-react'
import { useThemeEditor } from '../contexts/ThemeEditorContext'
import { useTheme } from '../contexts/ThemeContext'
import { generateColorMap } from '../utils/color'
import { themeConfig } from '../../../preload/types'
const variableGroups = {
background: {
title: '背景颜色',
items: [
{ key: '--ss-bg-color', label: '全局背景' },
{ key: '--ss-card-bg', label: '卡片背景' },
{ key: '--ss-header-bg', label: '顶部栏背景' },
{ key: '--ss-sidebar-bg', label: '侧边栏背景' }
]
},
text: {
title: '文字颜色',
items: [
{ key: '--ss-text-main', label: '主要文字' },
{ key: '--ss-text-secondary', label: '次要文字' },
{ key: '--ss-sidebar-active-text', label: '侧边栏选中文字' }
]
},
border: {
title: '边框与分割线',
items: [{ key: '--ss-border-color', label: '通用边框' }]
},
interaction: {
title: '交互状态',
items: [
{ key: '--ss-item-hover', label: '列表悬浮' },
{ key: '--ss-sidebar-active-bg', label: '侧边栏选中背景' }
]
}
}
export const ThemeEditor: React.FC = () => {
const {
isEditing,
editingTheme,
updateEditingTheme,
updateConfig,
saveEditingTheme,
cancelEditing
} = useThemeEditor()
const { currentTheme } = useTheme()
// 实时预览逻辑
useEffect(() => {
if (!isEditing || !editingTheme) return
const applyPreview = (theme: themeConfig) => {
const { tdesign, custom } = theme.config
const root = document.documentElement
// 1. 设置 TDesign 模式
root.setAttribute('theme-mode', theme.mode)
// 2. 设置 TDesign 品牌色
if (tdesign.brandColor) {
const colorMap = generateColorMap(tdesign.brandColor, theme.mode)
Object.entries(colorMap).forEach(([key, value]) => {
root.style.setProperty(key, value)
})
}
// 3. 应用自定义变量
Object.entries(custom).forEach(([key, value]) => {
root.style.setProperty(key, value)
})
}
applyPreview(editingTheme)
}, [editingTheme, isEditing])
// 关闭时恢复原有主题
useEffect(() => {
if (!isEditing && currentTheme) {
const { tdesign, custom } = currentTheme.config
const root = document.documentElement
root.setAttribute('theme-mode', currentTheme.mode)
if (tdesign.brandColor) {
const colorMap = generateColorMap(tdesign.brandColor, currentTheme.mode)
Object.entries(colorMap).forEach(([key, value]) => {
root.style.setProperty(key, value)
})
}
Object.entries(custom).forEach(([key, value]) => {
root.style.setProperty(key, value)
})
}
}, [isEditing, currentTheme])
if (!editingTheme) return null
return (
<Drawer
header="编辑主题"
visible={isEditing}
onClose={cancelEditing}
size="500px"
footer={
<Space>
<Button theme="primary" onClick={saveEditingTheme}>
</Button>
<Button theme="default" onClick={cancelEditing}>
</Button>
</Space>
}
destroyOnClose
>
<Form labelAlign="top">
<Space direction="vertical" style={{ width: '100%' }} size="large">
{/* 基本信息 */}
<div>
<Divider align="left"></Divider>
<Row gutter={[16, 16]}>
<Col span={12}>
<Form.Item label="主题名称">
<Input
value={editingTheme.name}
onChange={(v) => updateEditingTheme({ name: v })}
placeholder="请输入主题名称"
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="色彩模式">
<Select
value={editingTheme.mode}
onChange={(v) => updateEditingTheme({ mode: v as 'light' | 'dark' })}
options={[
{ label: '浅色 (Light)', value: 'light' },
{ label: '深色 (Dark)', value: 'dark' }
]}
/>
</Form.Item>
</Col>
<Col span={24}>
<Form.Item label="主题 ID (唯一标识)" help="建议使用英文,如 my-theme">
<Input
value={editingTheme.id}
onChange={(v) => updateEditingTheme({ id: v })}
placeholder="请输入主题 ID"
/>
</Form.Item>
</Col>
</Row>
</div>
{/* TDesign 品牌色 */}
<div>
<Divider align="left"> (Brand)</Divider>
<Form.Item label="主品牌色" help="将自动生成一系列色阶">
<ColorPicker
value={editingTheme.config.tdesign.brandColor}
onChange={(v) => updateConfig('tdesign', 'brandColor', v)}
enableAlpha={false}
format="HEX"
/>
</Form.Item>
</div>
{/* 业务自定义变量 */}
<div>
<Divider align="left"> (Custom)</Divider>
{Object.entries(variableGroups).map(([groupKey, group]) => (
<div key={groupKey} style={{ marginBottom: 24 }}>
<div
style={{
fontSize: '13px',
fontWeight: 600,
marginBottom: '12px',
color: 'var(--td-text-color-primary)'
}}
>
{group.title}
</div>
<Row gutter={[12, 12]}>
{group.items.map((item) => (
<Col span={6} key={item.key}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
<div
style={{
fontSize: '12px',
color: 'var(--td-text-color-secondary)',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}}
title={item.label}
>
{item.label}
</div>
<ColorPicker
value={editingTheme.config.custom[item.key] || '#ffffff'}
onChange={(v) => updateConfig('custom', item.key, v)}
enableAlpha
format="HEX"
size="small"
/>
</div>
</Col>
))}
</Row>
</div>
))}
</div>
</Space>
</Form>
</Drawer>
)
}