mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-21 18:19:03 +08:00
请输入文本:文本
This commit is contained in:
@@ -5,8 +5,6 @@ import { Sidebar } from './components/Sidebar'
|
||||
import { ContentArea } from './components/ContentArea'
|
||||
import { Wizard } from './components/Wizard'
|
||||
import { ThemeProvider, useTheme } from './contexts/ThemeContext'
|
||||
import { ThemeEditorProvider } from './contexts/ThemeEditorContext'
|
||||
import { ThemeEditor } from './components/ThemeEditor'
|
||||
|
||||
function MainContent(): React.JSX.Element {
|
||||
const navigate = useNavigate()
|
||||
@@ -110,13 +108,14 @@ function MainContent(): React.JSX.Element {
|
||||
}
|
||||
|
||||
const isDark = currentTheme?.mode === 'dark'
|
||||
const brandColor = currentTheme?.config?.tdesign?.brandColor || '#0052D9'
|
||||
|
||||
return (
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
algorithm: isDark ? antTheme.darkAlgorithm : antTheme.defaultAlgorithm,
|
||||
token: {
|
||||
colorPrimary: 'var(--ant-color-primary, #1890ff)'
|
||||
colorPrimary: brandColor
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -222,14 +221,11 @@ function getPlatform(): string {
|
||||
function App(): React.JSX.Element {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<ThemeEditorProvider>
|
||||
<HashRouter>
|
||||
<Routes>
|
||||
<Route path="/*" element={<MainContent />} />
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
<ThemeEditor />
|
||||
</ThemeEditorProvider>
|
||||
<HashRouter>
|
||||
<Routes>
|
||||
<Route path="/*" element={<MainContent />} />
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
</ThemeProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ body,
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
background: var(--ss-bg-color, #f5f5f5);
|
||||
}
|
||||
|
||||
input,
|
||||
@@ -29,11 +30,10 @@ textarea {
|
||||
|
||||
.ss-sidebar .ant-menu-item-selected {
|
||||
background-color: var(--ss-sidebar-active-bg, var(--ss-item-hover, transparent)) !important;
|
||||
color: var(--ss-sidebar-active-text, var(--ss-sidebar-text, var(--ss-text-main)));
|
||||
}
|
||||
|
||||
html[theme-mode='dark'] .ss-sidebar .ant-menu-item-selected {
|
||||
color: #ffffff !important;
|
||||
color: var(
|
||||
--ss-sidebar-active-text,
|
||||
var(--ant-color-primary, var(--ss-sidebar-text, var(--ss-text-main)))
|
||||
);
|
||||
}
|
||||
|
||||
@media (pointer: coarse) {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import React, { Suspense, lazy } from 'react'
|
||||
import { Layout, Space, Button, Tag, Spin } from 'antd'
|
||||
import { Layout, Space, Button, Tag } from 'antd'
|
||||
import { Routes, Route, Navigate } from 'react-router-dom'
|
||||
import { WindowControls } from './WindowControls'
|
||||
import { ThemeEditor } from './ThemeEditor'
|
||||
|
||||
const Home = lazy(() => import('./Home').then((m) => ({ default: m.Home })))
|
||||
const StudentManager = lazy(() =>
|
||||
@@ -51,17 +49,16 @@ export function ContentArea({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
backgroundColor: 'var(--ss-bg-color)'
|
||||
background: 'var(--ss-bg-color)'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
height: '32px',
|
||||
WebkitAppRegion: 'drag',
|
||||
height: '40px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'var(--ss-header-bg)',
|
||||
background: 'var(--ss-header-bg)',
|
||||
borderBottom: '1px solid var(--ss-border-color)',
|
||||
flexShrink: 0
|
||||
} as React.CSSProperties
|
||||
@@ -73,12 +70,11 @@ export function ContentArea({
|
||||
{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
paddingRight: '0px',
|
||||
WebkitAppRegion: 'no-drag'
|
||||
paddingRight: '12px'
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<Space size="small" style={{ marginRight: '12px' }}>
|
||||
<Space size="small">
|
||||
{permissionTag}
|
||||
{hasAnyPassword && (
|
||||
<>
|
||||
@@ -91,7 +87,6 @@ export function ContentArea({
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
<WindowControls />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -106,7 +101,16 @@ export function ContentArea({
|
||||
height: '100%'
|
||||
}}
|
||||
>
|
||||
<Spin size="large" description="正在载入页面..." />
|
||||
<div
|
||||
style={{
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
border: '3px solid var(--ss-border-color)',
|
||||
borderTopColor: 'var(--ant-color-primary)',
|
||||
borderRadius: '50%',
|
||||
animation: 'spin 1s linear infinite'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
@@ -129,7 +133,6 @@ export function ContentArea({
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</Content>
|
||||
<ThemeEditor />
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -786,7 +786,7 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
||||
<div
|
||||
style={{
|
||||
padding: '16px',
|
||||
backgroundColor:
|
||||
background:
|
||||
customScore > 0
|
||||
? 'var(--ant-color-success-bg, #f6ffed)'
|
||||
: customScore < 0
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Tabs, Card, Form, Select, Input, Button, Space, Divider, Tag, Modal, message } from 'antd'
|
||||
import { useTheme } from '../contexts/ThemeContext'
|
||||
import { useThemeEditor } from '../contexts/ThemeEditorContext'
|
||||
import { ThemeQuickSettings } from './ThemeQuickSettings'
|
||||
|
||||
type permissionLevel = 'admin' | 'points' | 'view'
|
||||
type appSettings = {
|
||||
@@ -12,8 +11,6 @@ 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,
|
||||
@@ -313,22 +310,11 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
|
||||
label: '外观',
|
||||
children: (
|
||||
<Card style={{ backgroundColor: 'var(--ss-card-bg)', color: 'var(--ss-text-main)' }}>
|
||||
<Form layout="horizontal" labelCol={{ span: 4 }} wrapperCol={{ span: 20 }}>
|
||||
<Form.Item label="当前主题">
|
||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||
<Select
|
||||
value={currentTheme?.id}
|
||||
onChange={(v) => setTheme(v as string)}
|
||||
style={{ width: '200px' }}
|
||||
options={themes.map((t) => ({ value: t.id, label: t.name }))}
|
||||
/>
|
||||
<Button onClick={() => startEditing(currentTheme || undefined)}>编辑</Button>
|
||||
<Button type="link" onClick={() => startEditing()}>
|
||||
新建主题
|
||||
</Button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
<ThemeQuickSettings />
|
||||
|
||||
<Divider />
|
||||
|
||||
<Form layout="horizontal" labelCol={{ span: 4 }} wrapperCol={{ span: 20 }}>
|
||||
<Form.Item label="界面缩放">
|
||||
<Select
|
||||
value={settings.window_zoom || '1.0'}
|
||||
@@ -690,7 +676,6 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
|
||||
>
|
||||
<div>secscore://settings</div>
|
||||
<div>secscore://score</div>
|
||||
<div>secscore://sidebar/toggle</div>
|
||||
</div>
|
||||
<Divider />
|
||||
<Space>
|
||||
@@ -767,7 +752,14 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
|
||||
]
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px', maxWidth: '900px', margin: '0 auto' }}>
|
||||
<div
|
||||
style={{
|
||||
padding: '24px',
|
||||
maxWidth: '900px',
|
||||
margin: '0 auto',
|
||||
color: 'var(--ss-text-main)'
|
||||
}}
|
||||
>
|
||||
{contextHolder}
|
||||
<div
|
||||
style={{
|
||||
@@ -777,7 +769,7 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
|
||||
marginBottom: '16px'
|
||||
}}
|
||||
>
|
||||
<h2 style={{ margin: 0, color: 'var(--ss-text-main)' }}>系统设置</h2>
|
||||
<h2 style={{ margin: 0 }}>系统设置</h2>
|
||||
{permissionTag}
|
||||
</div>
|
||||
|
||||
@@ -871,7 +863,7 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
<div>将把当前未结算的积分记录归档为一个阶段,并将所有学生当前积分清零。</div>
|
||||
<div style={{ color: 'var(--ss-text-secondary)', fontSize: '12px' }}>
|
||||
学生名单不变;结算后的历史在"结算历史"页面查看。
|
||||
学生名单不变;结算后的历史在“结算历史”页面查看。
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -158,7 +158,7 @@ export function Sidebar({ activeMenu, permission, onMenuChange }: SidebarProps):
|
||||
className="ss-sidebar"
|
||||
width={200}
|
||||
style={{
|
||||
backgroundColor: 'var(--ss-sidebar-bg)',
|
||||
background: 'var(--ss-sidebar-bg)',
|
||||
borderRight: '1px solid var(--ss-border-color)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
|
||||
@@ -178,7 +178,7 @@ export const ThemeEditor: React.FC = () => {
|
||||
</div>
|
||||
<Row gutter={[12, 12]}>
|
||||
{group.items.map((item) => (
|
||||
<Col span={6} key={item.key}>
|
||||
<Col span={groupKey === 'background' ? 12 : 6} key={item.key}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<div
|
||||
style={{
|
||||
@@ -192,13 +192,33 @@ export const ThemeEditor: React.FC = () => {
|
||||
>
|
||||
{item.label}
|
||||
</div>
|
||||
<ColorPicker
|
||||
value={editingTheme.config.custom[item.key] || '#ffffff'}
|
||||
onChange={(color: Color) =>
|
||||
updateConfig('custom', item.key, color.toHexString())
|
||||
}
|
||||
showText
|
||||
/>
|
||||
{groupKey === 'background' ? (
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||
<div
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 6,
|
||||
border: '1px solid var(--ss-border-color)',
|
||||
background: editingTheme.config.custom[item.key] || '#ffffff',
|
||||
flexShrink: 0
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
value={editingTheme.config.custom[item.key] || ''}
|
||||
onChange={(e) => updateConfig('custom', item.key, e.target.value)}
|
||||
placeholder="例如 #ffffff 或 linear-gradient(...)"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<ColorPicker
|
||||
value={editingTheme.config.custom[item.key] || '#ffffff'}
|
||||
onChange={(color: Color) =>
|
||||
updateConfig('custom', item.key, color.toHexString())
|
||||
}
|
||||
showText
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Col>
|
||||
))}
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { Button, ColorPicker, Input, Segmented, Space, Typography, message } from 'antd'
|
||||
import type { Color } from 'antd/es/color-picker'
|
||||
import { useTheme } from '../contexts/ThemeContext'
|
||||
import type { themeConfig } from '../../../preload/types'
|
||||
|
||||
type props = {
|
||||
compact?: boolean
|
||||
}
|
||||
|
||||
const presetPrimaryColors = [
|
||||
'#1677FF',
|
||||
'#2F54EB',
|
||||
'#722ED1',
|
||||
'#EB2F96',
|
||||
'#F5222D',
|
||||
'#FA8C16',
|
||||
'#FADB14',
|
||||
'#52C41A',
|
||||
'#13C2C2'
|
||||
]
|
||||
|
||||
const presetGradients: {
|
||||
id: string
|
||||
label: string
|
||||
light: string
|
||||
dark: string
|
||||
}[] = [
|
||||
{
|
||||
id: 'blue',
|
||||
label: '清爽蓝',
|
||||
light: 'linear-gradient(180deg, #f7fbff 0%, #f1f7ff 55%, #f8f9fc 100%)',
|
||||
dark: 'linear-gradient(180deg, #0f1220 0%, #101524 55%, #0b0d16 100%)'
|
||||
},
|
||||
{
|
||||
id: 'pink',
|
||||
label: '柔和粉',
|
||||
light: 'linear-gradient(180deg, #fff7f1 0%, #fff1f1 55%, #f7f7fb 100%)',
|
||||
dark: 'linear-gradient(180deg, #1a0f14 0%, #1d1218 55%, #120c10 100%)'
|
||||
},
|
||||
{
|
||||
id: 'cyan',
|
||||
label: '青蓝',
|
||||
light: 'linear-gradient(180deg, #f0f9ff 0%, #e0f2fe 55%, #f0f9ff 100%)',
|
||||
dark: 'linear-gradient(180deg, #050b10 0%, #06121a 55%, #05070a 100%)'
|
||||
},
|
||||
{
|
||||
id: 'purple',
|
||||
label: '紫韵',
|
||||
light: 'linear-gradient(180deg, #faf5ff 0%, #f3e8ff 55%, #faf5ff 100%)',
|
||||
dark: 'linear-gradient(180deg, #0f0a14 0%, #151020 55%, #0d0910 100%)'
|
||||
}
|
||||
]
|
||||
|
||||
const deepClone = <T,>(v: T): T => JSON.parse(JSON.stringify(v)) as T
|
||||
|
||||
const normalizeHex = (value: string): string | null => {
|
||||
const s = String(value || '').trim()
|
||||
if (!s) return null
|
||||
const m = s.match(/^#?([0-9a-fA-F]{6})$/)
|
||||
if (!m) return null
|
||||
return `#${m[1].toUpperCase()}`
|
||||
}
|
||||
|
||||
const buildGradient = (a: string, b: string, dir: 'v' | 'h' | 'd'): string => {
|
||||
const angle = dir === 'h' ? '90deg' : dir === 'd' ? '135deg' : '180deg'
|
||||
return `linear-gradient(${angle}, ${a} 0%, ${b} 100%)`
|
||||
}
|
||||
|
||||
export const ThemeQuickSettings: React.FC<props> = ({ compact }) => {
|
||||
const { currentTheme, setTheme, themes, applyTheme } = useTheme()
|
||||
const [messageApi, holder] = message.useMessage()
|
||||
|
||||
const [workingTheme, setWorkingTheme] = useState<themeConfig | null>(null)
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
const [primaryInput, setPrimaryInput] = useState('')
|
||||
const primaryColor = useMemo(() => {
|
||||
const fromTheme = workingTheme?.config?.tdesign?.brandColor
|
||||
return normalizeHex(primaryInput) || normalizeHex(fromTheme || '') || '#1677FF'
|
||||
}, [primaryInput, workingTheme])
|
||||
|
||||
const [gradientDir, setGradientDir] = useState<'v' | 'h' | 'd'>('v')
|
||||
const [g1, setG1] = useState<string>('#f7fbff')
|
||||
const [g2, setG2] = useState<string>('#f8f9fc')
|
||||
|
||||
const currentBg = workingTheme?.config?.custom?.['--ss-bg-color'] || ''
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentTheme) return
|
||||
const base = deepClone(currentTheme)
|
||||
const editable =
|
||||
base.id === 'custom-default' || base.id.startsWith('custom-')
|
||||
? base
|
||||
: { ...base, id: 'custom-default', name: '我的主题' }
|
||||
setWorkingTheme(editable)
|
||||
setPrimaryInput(editable.config?.tdesign?.brandColor || '')
|
||||
}, [currentTheme])
|
||||
|
||||
// 实时预览主题变化
|
||||
useEffect(() => {
|
||||
if (!workingTheme) return
|
||||
applyTheme(workingTheme)
|
||||
}, [workingTheme, applyTheme])
|
||||
|
||||
const ensureCustomThemeSelected = async (theme: themeConfig): Promise<boolean> => {
|
||||
if (!(window as any).api) return false
|
||||
const exists = themes.some((t) => t.id === theme.id)
|
||||
if (!exists) {
|
||||
const res = await (window as any).api.saveTheme(theme)
|
||||
if (!res?.success) return false
|
||||
}
|
||||
if (currentTheme?.id !== theme.id) {
|
||||
await setTheme(theme.id)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const patchThemeConfig = (type: 'tdesign' | 'custom', key: string, value: string) => {
|
||||
setWorkingTheme((prev) => {
|
||||
if (!prev) return prev
|
||||
const next = deepClone(prev)
|
||||
next.config = next.config || ({} as any)
|
||||
next.config[type] = { ...(next.config[type] || {}), [key]: value }
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const save = async () => {
|
||||
if (!(window as any).api) return
|
||||
if (!workingTheme) return
|
||||
setSaving(true)
|
||||
try {
|
||||
const ok = await ensureCustomThemeSelected(workingTheme)
|
||||
if (!ok) throw new Error('保存失败')
|
||||
const res = await (window as any).api.saveTheme(workingTheme)
|
||||
if (!res?.success) throw new Error(res?.message || '保存失败')
|
||||
messageApi.success('已保存')
|
||||
} catch (e: any) {
|
||||
messageApi.error(e?.message || '保存失败')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const setPrimary = (hex: string) => {
|
||||
setPrimaryInput(hex)
|
||||
patchThemeConfig('tdesign', 'brandColor', hex)
|
||||
}
|
||||
|
||||
const setGradientPreset = (value: string) => {
|
||||
patchThemeConfig('custom', '--ss-bg-color', value)
|
||||
}
|
||||
|
||||
const setGradientFromPickers = () => {
|
||||
const g = buildGradient(g1, g2, gradientDir)
|
||||
patchThemeConfig('custom', '--ss-bg-color', g)
|
||||
}
|
||||
|
||||
const setMode = async (mode: 'light' | 'dark') => {
|
||||
// 根据模式切换到对应的内置主题
|
||||
const targetThemeId = mode === 'light' ? 'light-default' : 'dark-default'
|
||||
await setTheme(targetThemeId)
|
||||
}
|
||||
|
||||
if (!workingTheme) return null
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: compact ? 12 : 16 }}>
|
||||
{holder}
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Typography.Text strong>颜色与背景</Typography.Text>
|
||||
<Space>
|
||||
<Button type="primary" loading={saving} onClick={save}>
|
||||
保存
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<Typography.Text>模式</Typography.Text>
|
||||
<Segmented
|
||||
value={workingTheme.mode}
|
||||
onChange={(v) => setMode(v as any)}
|
||||
options={[
|
||||
{ label: '浅色', value: 'light' },
|
||||
{ label: '深色', value: 'dark' }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<Typography.Text>主色</Typography.Text>
|
||||
<div style={{ display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<Input
|
||||
value={primaryInput}
|
||||
onChange={(e) => setPrimaryInput(e.target.value)}
|
||||
onBlur={() => {
|
||||
const n = normalizeHex(primaryInput)
|
||||
if (n) setPrimary(n)
|
||||
else setPrimary(primaryColor)
|
||||
}}
|
||||
style={{ width: 120 }}
|
||||
/>
|
||||
{presetPrimaryColors.map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
type="button"
|
||||
onClick={() => setPrimary(c)}
|
||||
style={{
|
||||
width: 18,
|
||||
height: 18,
|
||||
borderRadius: 999,
|
||||
border:
|
||||
c === primaryColor ? '2px solid rgba(0,0,0,0.35)' : '1px solid rgba(0,0,0,0.18)',
|
||||
background: c,
|
||||
cursor: 'pointer',
|
||||
padding: 0
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<ColorPicker
|
||||
value={primaryColor}
|
||||
onChange={(color: Color) => setPrimary(color.toHexString().toUpperCase())}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<Typography.Text>背景渐变</Typography.Text>
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))',
|
||||
gap: 10
|
||||
}}
|
||||
>
|
||||
{presetGradients.map((g) => {
|
||||
const gradientValue = g[workingTheme.mode]
|
||||
const active = currentBg === gradientValue
|
||||
return (
|
||||
<button
|
||||
key={g.id}
|
||||
type="button"
|
||||
onClick={() => setGradientPreset(gradientValue)}
|
||||
style={{
|
||||
borderRadius: 10,
|
||||
border: active
|
||||
? '2px solid var(--ant-color-primary)'
|
||||
: '1px solid var(--ss-border-color)',
|
||||
padding: 10,
|
||||
background: 'transparent',
|
||||
cursor: 'pointer',
|
||||
textAlign: 'left'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: 56,
|
||||
borderRadius: 8,
|
||||
border: '1px solid var(--ss-border-color)',
|
||||
background: gradientValue
|
||||
}}
|
||||
/>
|
||||
<div style={{ marginTop: 8, fontSize: 12, color: 'var(--ss-text-secondary)' }}>
|
||||
{g.label}
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<Segmented
|
||||
value={gradientDir}
|
||||
onChange={(v) => setGradientDir(v as any)}
|
||||
options={[
|
||||
{ label: '纵向', value: 'v' },
|
||||
{ label: '横向', value: 'h' },
|
||||
{ label: '对角', value: 'd' }
|
||||
]}
|
||||
/>
|
||||
<Space size="small">
|
||||
<ColorPicker value={g1} onChange={(c: Color) => setG1(c.toHexString())} />
|
||||
<ColorPicker value={g2} onChange={(c: Color) => setG2(c.toHexString())} />
|
||||
<Button onClick={setGradientFromPickers}>生成</Button>
|
||||
</Space>
|
||||
<div
|
||||
style={{
|
||||
width: 160,
|
||||
height: 32,
|
||||
borderRadius: 8,
|
||||
border: '1px solid var(--ss-border-color)',
|
||||
background: buildGradient(g1, g2, gradientDir)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Modal, Form, Select, message, Typography } from 'antd'
|
||||
import { useTheme } from '../contexts/ThemeContext'
|
||||
import { Modal, message, Typography } from 'antd'
|
||||
import { ThemeQuickSettings } from './ThemeQuickSettings'
|
||||
|
||||
interface wizardProps {
|
||||
visible: boolean
|
||||
@@ -8,7 +8,6 @@ interface wizardProps {
|
||||
}
|
||||
|
||||
export const Wizard: React.FC<wizardProps> = ({ visible, onComplete }) => {
|
||||
const { themes, currentTheme, setTheme } = useTheme()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [messageApi, contextHolder] = message.useMessage()
|
||||
|
||||
@@ -47,15 +46,7 @@ export const Wizard: React.FC<wizardProps> = ({ visible, onComplete }) => {
|
||||
感谢选择 SecScore。在开始之前,请花一分钟完成基础配置。
|
||||
</Typography.Paragraph>
|
||||
|
||||
<Form layout="horizontal" labelCol={{ span: 6 }} wrapperCol={{ span: 18 }}>
|
||||
<Form.Item label="外观主题">
|
||||
<Select
|
||||
value={currentTheme?.id}
|
||||
onChange={(v) => setTheme(v as string)}
|
||||
options={themes.map((t) => ({ value: t.id, label: t.name }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<ThemeQuickSettings compact />
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ interface themeContextType {
|
||||
currentTheme: themeConfig | null
|
||||
setTheme: (id: string) => Promise<void>
|
||||
themes: themeConfig[]
|
||||
applyTheme: (theme: themeConfig) => void
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<themeContextType | undefined>(undefined)
|
||||
@@ -25,21 +26,27 @@ export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ childre
|
||||
|
||||
root.setAttribute('theme-mode', theme.mode)
|
||||
|
||||
if (tdesign.brandColor) {
|
||||
const colorMap = generateColorMap(tdesign.brandColor, theme.mode)
|
||||
const brandColor = tdesign.brandColor || '#1677FF'
|
||||
const hex = brandColor.replace('#', '')
|
||||
const r = parseInt(hex.substring(0, 2), 16)
|
||||
const g = parseInt(hex.substring(2, 4), 16)
|
||||
const b = parseInt(hex.substring(4, 6), 16)
|
||||
|
||||
root.style.setProperty('--ss-primary-color', brandColor)
|
||||
root.style.setProperty('--ss-primary-rgb', `${r}, ${g}, ${b}`)
|
||||
nextKeys.push('--ss-primary-color')
|
||||
nextKeys.push('--ss-primary-rgb')
|
||||
|
||||
if (brandColor) {
|
||||
const colorMap = generateColorMap(brandColor, theme.mode)
|
||||
Object.entries(colorMap).forEach(([key, value]) => {
|
||||
root.style.setProperty(key, value)
|
||||
nextKeys.push(key)
|
||||
})
|
||||
|
||||
root.style.setProperty('--ant-color-primary', tdesign.brandColor)
|
||||
root.style.setProperty('--ant-color-primary', brandColor)
|
||||
nextKeys.push('--ant-color-primary')
|
||||
|
||||
const hex = tdesign.brandColor.replace('#', '')
|
||||
const r = parseInt(hex.substring(0, 2), 16)
|
||||
const g = parseInt(hex.substring(2, 4), 16)
|
||||
const b = parseInt(hex.substring(4, 6), 16)
|
||||
|
||||
root.style.setProperty('--ant-color-primary-hover', `rgba(${r}, ${g}, ${b}, 0.85)`)
|
||||
root.style.setProperty('--ant-color-primary-active', `rgba(${r}, ${g}, ${b}, 0.7)`)
|
||||
root.style.setProperty('--ant-color-primary-bg', `rgba(${r}, ${g}, ${b}, 0.1)`)
|
||||
@@ -71,6 +78,26 @@ export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ childre
|
||||
nextKeys.push(key)
|
||||
})
|
||||
|
||||
const bgLight = custom['--ss-bg-color-light']
|
||||
const bgDark = custom['--ss-bg-color-dark']
|
||||
if (bgLight || bgDark) {
|
||||
const resolved = theme.mode === 'dark' ? bgDark || bgLight : bgLight || bgDark
|
||||
if (resolved) {
|
||||
root.style.setProperty('--ss-bg-color', resolved)
|
||||
nextKeys.push('--ss-bg-color')
|
||||
}
|
||||
}
|
||||
|
||||
if (!custom['--ss-sidebar-active-bg']) {
|
||||
const alpha = theme.mode === 'dark' ? 0.22 : 0.12
|
||||
root.style.setProperty('--ss-sidebar-active-bg', `rgba(${r}, ${g}, ${b}, ${alpha})`)
|
||||
nextKeys.push('--ss-sidebar-active-bg')
|
||||
}
|
||||
if (!custom['--ss-sidebar-active-text']) {
|
||||
root.style.setProperty('--ss-sidebar-active-text', brandColor)
|
||||
nextKeys.push('--ss-sidebar-active-text')
|
||||
}
|
||||
|
||||
appliedStyleKeysRef.current = nextKeys
|
||||
}, [])
|
||||
|
||||
@@ -120,8 +147,15 @@ export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ childre
|
||||
}
|
||||
}
|
||||
|
||||
const applyTheme = useCallback(
|
||||
(theme: themeConfig) => {
|
||||
applyThemeConfig(theme)
|
||||
},
|
||||
[applyThemeConfig]
|
||||
)
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ currentTheme, setTheme, themes }}>
|
||||
<ThemeContext.Provider value={{ currentTheme, setTheme, themes, applyTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user