mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-21 18:19:03 +08:00
feat: add ThemeEditor component with real-time preview and settings integration
This commit is contained in:
@@ -5,7 +5,9 @@ import { Sidebar } from './components/Sidebar'
|
|||||||
import { ContentArea } from './components/ContentArea'
|
import { ContentArea } from './components/ContentArea'
|
||||||
import { Wizard } from './components/Wizard'
|
import { Wizard } from './components/Wizard'
|
||||||
import { ThemeProvider } from './contexts/ThemeContext'
|
import { ThemeProvider } from './contexts/ThemeContext'
|
||||||
|
import { ThemeEditorProvider } from './contexts/ThemeEditorContext'
|
||||||
import { GlobalSidebar } from './components/GlobalSidebar'
|
import { GlobalSidebar } from './components/GlobalSidebar'
|
||||||
|
import { ThemeEditor } from './components/ThemeEditor'
|
||||||
|
|
||||||
function MainContent(): React.JSX.Element {
|
function MainContent(): React.JSX.Element {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@@ -143,12 +145,15 @@ function MainContent(): React.JSX.Element {
|
|||||||
function App(): React.JSX.Element {
|
function App(): React.JSX.Element {
|
||||||
return (
|
return (
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<HashRouter>
|
<ThemeEditorProvider>
|
||||||
<Routes>
|
<HashRouter>
|
||||||
<Route path="/global-sidebar" element={<GlobalSidebar />} />
|
<Routes>
|
||||||
<Route path="/*" element={<MainContent />} />
|
<Route path="/global-sidebar" element={<GlobalSidebar />} />
|
||||||
</Routes>
|
<Route path="/*" element={<MainContent />} />
|
||||||
</HashRouter>
|
</Routes>
|
||||||
|
</HashRouter>
|
||||||
|
<ThemeEditor />
|
||||||
|
</ThemeEditorProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
MessagePlugin
|
MessagePlugin
|
||||||
} from 'tdesign-react'
|
} from 'tdesign-react'
|
||||||
import { useTheme } from '../contexts/ThemeContext'
|
import { useTheme } from '../contexts/ThemeContext'
|
||||||
|
import { useThemeEditor } from '../contexts/ThemeEditorContext'
|
||||||
|
|
||||||
type permissionLevel = 'admin' | 'points' | 'view'
|
type permissionLevel = 'admin' | 'points' | 'view'
|
||||||
type appSettings = {
|
type appSettings = {
|
||||||
@@ -23,6 +24,7 @@ type appSettings = {
|
|||||||
|
|
||||||
export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission }) => {
|
export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission }) => {
|
||||||
const { themes, currentTheme, setTheme } = useTheme()
|
const { themes, currentTheme, setTheme } = useTheme()
|
||||||
|
const { startEditing } = useThemeEditor()
|
||||||
const [activeTab, setActiveTab] = useState('appearance')
|
const [activeTab, setActiveTab] = useState('appearance')
|
||||||
const [settings, setSettings] = useState<appSettings>({
|
const [settings, setSettings] = useState<appSettings>({
|
||||||
is_wizard_completed: false,
|
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)' }}>
|
<Card style={{ backgroundColor: 'var(--ss-card-bg)', color: 'var(--ss-text-main)' }}>
|
||||||
<Form labelWidth={120}>
|
<Form labelWidth={120}>
|
||||||
<Form.FormItem label="当前主题">
|
<Form.FormItem label="当前主题">
|
||||||
<Select
|
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||||
value={currentTheme?.id}
|
<Select
|
||||||
onChange={(v) => setTheme(v as string)}
|
value={currentTheme?.id}
|
||||||
style={{ width: '320px' }}
|
onChange={(v) => setTheme(v as string)}
|
||||||
>
|
style={{ width: '200px' }}
|
||||||
{themes.map((t) => (
|
>
|
||||||
<Select.Option key={t.id} value={t.id} label={t.name} />
|
{themes.map((t) => (
|
||||||
))}
|
<Select.Option key={t.id} value={t.id} label={t.name} />
|
||||||
</Select>
|
))}
|
||||||
|
</Select>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
theme="default"
|
||||||
|
onClick={() => startEditing(currentTheme || undefined)}
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
<Button variant="text" theme="primary" onClick={() => startEditing()}>
|
||||||
|
新建主题
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</Form.FormItem>
|
</Form.FormItem>
|
||||||
|
|
||||||
<Form.FormItem label="界面缩放">
|
<Form.FormItem label="界面缩放">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user