mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-21 18:19:03 +08:00
请输入文本:文本
This commit is contained in:
+1
-9
@@ -49,7 +49,6 @@ type mainAppConfig = {
|
|||||||
dataRoot: string
|
dataRoot: string
|
||||||
configDir: string
|
configDir: string
|
||||||
logDir: string
|
logDir: string
|
||||||
themeDir: string
|
|
||||||
dbPath: string
|
dbPath: string
|
||||||
pgConnectionString?: string
|
pgConnectionString?: string
|
||||||
window: windowManagerOptions
|
window: windowManagerOptions
|
||||||
@@ -188,9 +187,6 @@ app.whenReady().then(async () => {
|
|||||||
|
|
||||||
const logDir = is.dev ? join(process.cwd(), 'logs') : join(dataRoot, 'logs')
|
const logDir = is.dev ? join(process.cwd(), 'logs') : join(dataRoot, 'logs')
|
||||||
const configDir = is.dev ? join(process.cwd(), 'configs') : join(dataRoot, 'configs')
|
const configDir = is.dev ? join(process.cwd(), 'configs') : join(dataRoot, 'configs')
|
||||||
const themeDir = is.dev
|
|
||||||
? join(process.cwd(), 'themes')
|
|
||||||
: ensureWritableDir(join(appRoot, 'themes'), join(dataRoot, 'themes'))
|
|
||||||
const dbPath = is.dev ? join(process.cwd(), 'db.sqlite') : join(dataRoot, 'db.sqlite')
|
const dbPath = is.dev ? join(process.cwd(), 'db.sqlite') : join(dataRoot, 'db.sqlite')
|
||||||
|
|
||||||
const pgConnectionString = process.env['PG_CONNECTION_STRING'] || undefined
|
const pgConnectionString = process.env['PG_CONNECTION_STRING'] || undefined
|
||||||
@@ -201,7 +197,6 @@ app.whenReady().then(async () => {
|
|||||||
dataRoot,
|
dataRoot,
|
||||||
logDir,
|
logDir,
|
||||||
configDir,
|
configDir,
|
||||||
themeDir,
|
|
||||||
dbPath,
|
dbPath,
|
||||||
pgConnectionString,
|
pgConnectionString,
|
||||||
window: {
|
window: {
|
||||||
@@ -263,10 +258,7 @@ app.whenReady().then(async () => {
|
|||||||
(p) => new TagRepository((p.get(DbManagerToken) as DbManager).dataSource)
|
(p) => new TagRepository((p.get(DbManagerToken) as DbManager).dataSource)
|
||||||
)
|
)
|
||||||
|
|
||||||
services.addSingleton(
|
services.addSingleton(ThemeServiceToken, (p) => new ThemeService(p.get(MainContext)))
|
||||||
ThemeServiceToken,
|
|
||||||
(p) => new ThemeService(p.get(MainContext), config.themeDir)
|
|
||||||
)
|
|
||||||
services.addSingleton(
|
services.addSingleton(
|
||||||
WindowManagerToken,
|
WindowManagerToken,
|
||||||
(p) => new WindowManager(p.get(MainContext), config.window)
|
(p) => new WindowManager(p.get(MainContext), config.window)
|
||||||
|
|||||||
@@ -100,9 +100,7 @@ export class DbConnectionService extends Service {
|
|||||||
return { success: true }
|
return { success: true }
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (this.testDataSource) {
|
if (this.testDataSource) {
|
||||||
try {
|
await this.testDataSource.destroy().catch(() => null)
|
||||||
await this.testDataSource.destroy()
|
|
||||||
} catch {}
|
|
||||||
this.testDataSource = null
|
this.testDataSource = null
|
||||||
}
|
}
|
||||||
console.error('PostgreSQL connection test failed:', error)
|
console.error('PostgreSQL connection test failed:', error)
|
||||||
|
|||||||
@@ -60,24 +60,10 @@ export class SettingsService extends Service {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
window_theme: {
|
themes_custom: {
|
||||||
kind: 'string',
|
kind: 'json',
|
||||||
defaultValue: 'auto',
|
defaultValue: [],
|
||||||
writePermission: 'admin',
|
writePermission: 'admin'
|
||||||
validate: (v) => v === 'auto' || v === 'dark' || v === 'light'
|
|
||||||
},
|
|
||||||
window_effect: {
|
|
||||||
kind: 'string',
|
|
||||||
defaultValue: 'mica',
|
|
||||||
writePermission: 'admin',
|
|
||||||
validate: (v) =>
|
|
||||||
['mica', 'tabbed', 'acrylic', 'blur', 'transparent', 'none'].includes(v as string)
|
|
||||||
},
|
|
||||||
window_radius: {
|
|
||||||
kind: 'string',
|
|
||||||
defaultValue: 'rounded',
|
|
||||||
writePermission: 'admin',
|
|
||||||
validate: (v) => v === 'rounded' || v === 'small' || v === 'square'
|
|
||||||
},
|
},
|
||||||
auto_score_enabled: {
|
auto_score_enabled: {
|
||||||
kind: 'boolean',
|
kind: 'boolean',
|
||||||
|
|||||||
+163
-115
@@ -1,19 +1,11 @@
|
|||||||
import { Service } from '../../shared/kernel'
|
import { Service } from '../../shared/kernel'
|
||||||
import { MainContext } from '../context'
|
import { MainContext } from '../context'
|
||||||
import { BrowserWindow } from 'electron'
|
import { BrowserWindow } from 'electron'
|
||||||
import fs from 'fs'
|
|
||||||
import path from 'path'
|
|
||||||
import { FSWatcher, watch } from 'chokidar'
|
|
||||||
|
|
||||||
export interface themeConfig {
|
export interface themeConfig {
|
||||||
name: string
|
name: string
|
||||||
id: string
|
id: string
|
||||||
mode: 'light' | 'dark'
|
mode: 'light' | 'dark'
|
||||||
mica?: {
|
|
||||||
effect: 'mica' | 'tabbed' | 'acrylic' | 'blur' | 'transparent' | 'none'
|
|
||||||
theme: 'auto' | 'dark' | 'light'
|
|
||||||
radius: 'small' | 'medium' | 'large'
|
|
||||||
}
|
|
||||||
config: {
|
config: {
|
||||||
tdesign: Record<string, string>
|
tdesign: Record<string, string>
|
||||||
custom: Record<string, string>
|
custom: Record<string, string>
|
||||||
@@ -27,19 +19,118 @@ declare module '../../shared/kernel' {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ThemeService extends Service {
|
export class ThemeService extends Service {
|
||||||
private themeDir: string
|
|
||||||
private watcher: FSWatcher | null = null
|
|
||||||
private currentThemeId: string = 'light-default'
|
private currentThemeId: string = 'light-default'
|
||||||
|
private customThemes: themeConfig[] = []
|
||||||
|
private readonly builtinThemes: themeConfig[] = [
|
||||||
|
{
|
||||||
|
name: '极简浅色',
|
||||||
|
id: 'light-default',
|
||||||
|
mode: 'light',
|
||||||
|
config: {
|
||||||
|
tdesign: {
|
||||||
|
brandColor: '#0052D9',
|
||||||
|
warningColor: '#ED7B2F',
|
||||||
|
errorColor: '#D54941',
|
||||||
|
successColor: '#2BA471'
|
||||||
|
},
|
||||||
|
custom: {
|
||||||
|
'--ss-bg-color': 'linear-gradient(180deg, #f7fbff 0%, #f1f7ff 55%, #f8f9fc 100%)',
|
||||||
|
'--ss-card-bg': '#ffffff',
|
||||||
|
'--ss-text-main': '#181818',
|
||||||
|
'--ss-text-secondary': '#666666',
|
||||||
|
'--ss-border-color': '#dcdcdc',
|
||||||
|
'--ss-header-bg': 'linear-gradient(180deg, #ffffff 0%, rgba(255,255,255,0.7) 100%)',
|
||||||
|
'--ss-sidebar-bg': 'rgba(255, 255, 255, 0.88)',
|
||||||
|
'--ss-item-hover': '#f3f3f3',
|
||||||
|
'--ss-sidebar-text': '#181818',
|
||||||
|
'--ss-sidebar-active-bg': 'rgba(0, 0, 0, 0.06)',
|
||||||
|
'--ss-sidebar-active-text': '#181818'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '极客深蓝',
|
||||||
|
id: 'dark-default',
|
||||||
|
mode: 'dark',
|
||||||
|
config: {
|
||||||
|
tdesign: {
|
||||||
|
brandColor: '#0052D9',
|
||||||
|
warningColor: '#E37318',
|
||||||
|
errorColor: '#D32029',
|
||||||
|
successColor: '#248232'
|
||||||
|
},
|
||||||
|
custom: {
|
||||||
|
'--ss-bg-color': 'linear-gradient(180deg, #0f1220 0%, #101524 55%, #0b0d16 100%)',
|
||||||
|
'--ss-card-bg': '#1e1e1e',
|
||||||
|
'--ss-text-main': '#ffffff',
|
||||||
|
'--ss-text-secondary': '#a0a0a0',
|
||||||
|
'--ss-border-color': '#333333',
|
||||||
|
'--ss-header-bg': 'rgba(30, 30, 30, 0.92)',
|
||||||
|
'--ss-sidebar-bg': 'rgba(30, 30, 30, 0.92)',
|
||||||
|
'--ss-item-hover': '#2c2c2c',
|
||||||
|
'--ss-sidebar-text': '#ffffff',
|
||||||
|
'--ss-sidebar-active-bg': 'rgba(255, 255, 255, 0.10)',
|
||||||
|
'--ss-sidebar-active-text': '#ffffff'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '冷静青蓝',
|
||||||
|
id: 'dark-cyan',
|
||||||
|
mode: 'dark',
|
||||||
|
config: {
|
||||||
|
tdesign: {
|
||||||
|
brandColor: '#16A085',
|
||||||
|
warningColor: '#F39C12',
|
||||||
|
errorColor: '#E74C3C',
|
||||||
|
successColor: '#1ABC9C'
|
||||||
|
},
|
||||||
|
custom: {
|
||||||
|
'--ss-bg-color': 'linear-gradient(180deg, #050b10 0%, #06121a 55%, #05070a 100%)',
|
||||||
|
'--ss-card-bg': '#0f1a23',
|
||||||
|
'--ss-text-main': '#E5F7FF',
|
||||||
|
'--ss-text-secondary': '#7FA4B8',
|
||||||
|
'--ss-border-color': '#1F3645',
|
||||||
|
'--ss-header-bg': 'rgba(15, 26, 35, 0.92)',
|
||||||
|
'--ss-sidebar-bg': 'rgba(15, 26, 35, 0.92)',
|
||||||
|
'--ss-item-hover': '#182635',
|
||||||
|
'--ss-sidebar-text': '#E5F7FF',
|
||||||
|
'--ss-sidebar-active-bg': '#182635',
|
||||||
|
'--ss-sidebar-active-text': '#E5F7FF'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '清新马卡龙',
|
||||||
|
id: 'light-pastel',
|
||||||
|
mode: 'light',
|
||||||
|
config: {
|
||||||
|
tdesign: {
|
||||||
|
brandColor: '#FF9AA2',
|
||||||
|
warningColor: '#FFB347',
|
||||||
|
errorColor: '#FF6F69',
|
||||||
|
successColor: '#B5EAD7'
|
||||||
|
},
|
||||||
|
custom: {
|
||||||
|
'--ss-bg-color': 'linear-gradient(180deg, #fff7f1 0%, #fff1f1 55%, #f7f7fb 100%)',
|
||||||
|
'--ss-card-bg': '#ffffff',
|
||||||
|
'--ss-text-main': '#3A3A3A',
|
||||||
|
'--ss-text-secondary': '#8A8A8A',
|
||||||
|
'--ss-border-color': '#F1D3D3',
|
||||||
|
'--ss-header-bg': 'rgba(255, 255, 255, 0.88)',
|
||||||
|
'--ss-sidebar-bg': 'rgba(255, 255, 255, 0.90)',
|
||||||
|
'--ss-item-hover': '#FFE7E0',
|
||||||
|
'--ss-sidebar-text': '#3A3A3A',
|
||||||
|
'--ss-sidebar-active-bg': '#FFE7E0',
|
||||||
|
'--ss-sidebar-active-text': '#3A3A3A'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
constructor(ctx: MainContext, themeDir: string) {
|
constructor(ctx: MainContext) {
|
||||||
super(ctx, 'themes')
|
super(ctx, 'themes')
|
||||||
this.themeDir = themeDir
|
|
||||||
this.setupWatcher()
|
|
||||||
this.registerIpc()
|
this.registerIpc()
|
||||||
|
|
||||||
ctx.effect(() => {
|
|
||||||
if (this.watcher) this.watcher.close()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private get mainCtx() {
|
private get mainCtx() {
|
||||||
@@ -47,12 +138,14 @@ export class ThemeService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async init() {
|
public async init() {
|
||||||
|
await this.mainCtx.settings.initialize()
|
||||||
await this.loadSavedTheme()
|
await this.loadSavedTheme()
|
||||||
|
await this.loadCustomThemes()
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadSavedTheme() {
|
private async loadSavedTheme() {
|
||||||
try {
|
try {
|
||||||
const savedThemeId = await this.mainCtx.settings.getValue('current_theme_id' as any)
|
const savedThemeId = this.mainCtx.settings.getValue('current_theme_id')
|
||||||
if (savedThemeId && typeof savedThemeId === 'string') {
|
if (savedThemeId && typeof savedThemeId === 'string') {
|
||||||
const themes = this.getThemeList()
|
const themes = this.getThemeList()
|
||||||
const exists = themes.some((t) => t.id === savedThemeId)
|
const exists = themes.some((t) => t.id === savedThemeId)
|
||||||
@@ -67,26 +160,27 @@ export class ThemeService extends Service {
|
|||||||
|
|
||||||
private async saveCurrentTheme() {
|
private async saveCurrentTheme() {
|
||||||
try {
|
try {
|
||||||
await this.mainCtx.settings.setValue('current_theme_id' as any, this.currentThemeId)
|
await this.mainCtx.settings.setValue('current_theme_id', this.currentThemeId)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.mainCtx.logger.warn('Failed to save theme', { meta: e })
|
this.mainCtx.logger.warn('Failed to save theme', { meta: e })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupWatcher() {
|
private async loadCustomThemes() {
|
||||||
if (this.watcher) this.watcher.close()
|
try {
|
||||||
|
const v = this.mainCtx.settings.getValue('themes_custom')
|
||||||
this.watcher = watch(this.themeDir, {
|
if (Array.isArray(v)) {
|
||||||
ignored: /(^|[/\\])\../,
|
this.customThemes = v.filter((t) => t && typeof t === 'object') as any
|
||||||
persistent: true
|
} else {
|
||||||
})
|
this.customThemes = []
|
||||||
|
|
||||||
this.watcher.on('change', (filePath) => {
|
|
||||||
if (filePath.endsWith('.json')) {
|
|
||||||
this.mainCtx.logger.info('Theme file changed', { filePath })
|
|
||||||
this.notifyThemeUpdate()
|
|
||||||
}
|
}
|
||||||
})
|
} catch {
|
||||||
|
this.customThemes = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async saveCustomThemes() {
|
||||||
|
await this.mainCtx.settings.setValue('themes_custom', this.customThemes)
|
||||||
}
|
}
|
||||||
|
|
||||||
private registerIpc() {
|
private registerIpc() {
|
||||||
@@ -107,7 +201,6 @@ export class ThemeService extends Service {
|
|||||||
}
|
}
|
||||||
this.currentThemeId = themeId
|
this.currentThemeId = themeId
|
||||||
await this.saveCurrentTheme()
|
await this.saveCurrentTheme()
|
||||||
this.applyMicaEffect(themeId)
|
|
||||||
this.notifyThemeUpdate()
|
this.notifyThemeUpdate()
|
||||||
return { success: true }
|
return { success: true }
|
||||||
})
|
})
|
||||||
@@ -117,8 +210,29 @@ export class ThemeService extends Service {
|
|||||||
return { success: false, message: 'Permission denied' }
|
return { success: false, message: 'Permission denied' }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const filePath = path.join(this.themeDir, `${theme.id}.json`)
|
if (!theme?.id || !theme?.name) {
|
||||||
fs.writeFileSync(filePath, JSON.stringify(theme, null, 2), 'utf-8')
|
return { success: false, message: 'Invalid theme' }
|
||||||
|
}
|
||||||
|
const isBuiltin = this.builtinThemes.some((t) => t.id === theme.id)
|
||||||
|
if (isBuiltin) {
|
||||||
|
return { success: false, message: 'Cannot overwrite builtin themes' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized: themeConfig = {
|
||||||
|
name: String(theme.name),
|
||||||
|
id: String(theme.id),
|
||||||
|
mode: theme.mode === 'dark' ? 'dark' : 'light',
|
||||||
|
config: {
|
||||||
|
tdesign: { ...(theme.config?.tdesign || {}) },
|
||||||
|
custom: { ...(theme.config?.custom || {}) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const idx = this.customThemes.findIndex((t) => t.id === normalized.id)
|
||||||
|
if (idx >= 0) this.customThemes[idx] = normalized
|
||||||
|
else this.customThemes.unshift(normalized)
|
||||||
|
|
||||||
|
await this.saveCustomThemes()
|
||||||
this.notifyThemeUpdate()
|
this.notifyThemeUpdate()
|
||||||
return { success: true }
|
return { success: true }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -130,17 +244,23 @@ export class ThemeService extends Service {
|
|||||||
if (!this.mainCtx.permissions.requirePermission(event, 'admin'))
|
if (!this.mainCtx.permissions.requirePermission(event, 'admin'))
|
||||||
return { success: false, message: 'Permission denied' }
|
return { success: false, message: 'Permission denied' }
|
||||||
|
|
||||||
if (themeId.startsWith('default-')) {
|
try {
|
||||||
return { success: false, message: 'Cannot delete default themes' }
|
const id = String(themeId || '').trim()
|
||||||
|
if (!id) return { success: false, message: 'Invalid theme id' }
|
||||||
|
if (this.builtinThemes.some((t) => t.id === id)) {
|
||||||
|
return { success: false, message: 'Cannot delete builtin themes' }
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const before = this.customThemes.length
|
||||||
const filePath = path.join(this.themeDir, `${themeId}.json`)
|
this.customThemes = this.customThemes.filter((t) => t.id !== id)
|
||||||
if (fs.existsSync(filePath)) {
|
if (this.customThemes.length === before) {
|
||||||
fs.unlinkSync(filePath)
|
return { success: false, message: 'Theme not found' }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.saveCustomThemes()
|
||||||
if (this.currentThemeId === themeId) {
|
if (this.currentThemeId === themeId) {
|
||||||
this.currentThemeId = 'light' // Fallback
|
this.currentThemeId = 'light-default'
|
||||||
|
await this.saveCurrentTheme()
|
||||||
}
|
}
|
||||||
this.notifyThemeUpdate()
|
this.notifyThemeUpdate()
|
||||||
return { success: true }
|
return { success: true }
|
||||||
@@ -148,82 +268,10 @@ export class ThemeService extends Service {
|
|||||||
return { success: false, message: String(e) }
|
return { success: false, message: String(e) }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
this.mainCtx.handle(
|
|
||||||
'theme:set-custom',
|
|
||||||
async (
|
|
||||||
event,
|
|
||||||
config: {
|
|
||||||
effect: 'mica' | 'tabbed' | 'acrylic' | 'blur' | 'transparent' | 'none'
|
|
||||||
theme: 'auto' | 'dark' | 'light'
|
|
||||||
radius: 'small' | 'medium' | 'large'
|
|
||||||
}
|
|
||||||
) => {
|
|
||||||
if (!this.mainCtx.permissions.requirePermission(event, 'admin'))
|
|
||||||
return { success: false, message: 'Permission denied' }
|
|
||||||
|
|
||||||
this.currentThemeId = 'custom'
|
|
||||||
await this.saveCurrentTheme()
|
|
||||||
this.applyMicaConfig(config)
|
|
||||||
this.notifyThemeUpdate()
|
|
||||||
return { success: true }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private applyMicaEffect(themeId: string) {
|
|
||||||
const theme = this.getThemeById(themeId)
|
|
||||||
if (!theme?.mica) return
|
|
||||||
|
|
||||||
this.applyMicaConfig(theme.mica)
|
|
||||||
}
|
|
||||||
|
|
||||||
private applyMicaConfig(config: {
|
|
||||||
effect: 'mica' | 'tabbed' | 'acrylic' | 'blur' | 'transparent' | 'none'
|
|
||||||
theme: 'auto' | 'dark' | 'light'
|
|
||||||
radius: 'small' | 'medium' | 'large'
|
|
||||||
}) {
|
|
||||||
const radiusMap: Record<string, 'rounded' | 'small' | 'square'> = {
|
|
||||||
small: 'small',
|
|
||||||
medium: 'rounded',
|
|
||||||
large: 'rounded'
|
|
||||||
}
|
|
||||||
|
|
||||||
const windows = BrowserWindow.getAllWindows()
|
|
||||||
for (const win of windows) {
|
|
||||||
if (win.isDestroyed()) continue
|
|
||||||
|
|
||||||
this.mainCtx.windows.setMicaEffect(win, config.effect)
|
|
||||||
this.mainCtx.windows.setMicaTheme(win, config.theme)
|
|
||||||
this.mainCtx.windows.setMicaCorner(win, radiusMap[config.radius] || 'rounded')
|
|
||||||
}
|
|
||||||
|
|
||||||
this.mainCtx.settings.setValue('window_effect', config.effect)
|
|
||||||
this.mainCtx.settings.setValue('window_theme', config.theme)
|
|
||||||
this.mainCtx.settings.setValue('window_radius', radiusMap[config.radius] || 'rounded')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getThemeList(): themeConfig[] {
|
private getThemeList(): themeConfig[] {
|
||||||
try {
|
return [...this.builtinThemes, ...this.customThemes]
|
||||||
if (!fs.existsSync(this.themeDir)) return []
|
|
||||||
const files = fs.readdirSync(this.themeDir)
|
|
||||||
return files
|
|
||||||
.filter((f) => f.endsWith('.json'))
|
|
||||||
.map((f) => {
|
|
||||||
try {
|
|
||||||
const content = fs.readFileSync(path.join(this.themeDir, f), 'utf-8')
|
|
||||||
return JSON.parse(content) as themeConfig
|
|
||||||
} catch {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter((t): t is themeConfig => t !== null)
|
|
||||||
} catch (e) {
|
|
||||||
this.mainCtx.logger.error('Failed to read themes', {
|
|
||||||
meta: e instanceof Error ? { message: e.message, stack: e.stack } : { e }
|
|
||||||
})
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getThemeById(id: string): themeConfig | null {
|
private getThemeById(id: string): themeConfig | null {
|
||||||
|
|||||||
@@ -3,39 +3,6 @@ import { MainContext } from '../context'
|
|||||||
import { BrowserWindow, shell } from 'electron'
|
import { BrowserWindow, shell } from 'electron'
|
||||||
import type { BrowserWindowConstructorOptions } from 'electron'
|
import type { BrowserWindowConstructorOptions } from 'electron'
|
||||||
|
|
||||||
let micaElectron: typeof import('mica-electron') | null = null
|
|
||||||
let MicaBrowserWindow: any = null
|
|
||||||
let IS_WINDOWS_11 = false
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (process.platform === 'win32') {
|
|
||||||
const micaModule = require('mica-electron')
|
|
||||||
micaElectron = micaModule
|
|
||||||
MicaBrowserWindow = micaModule.MicaBrowserWindow
|
|
||||||
IS_WINDOWS_11 = micaModule.IS_WINDOWS_11
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('mica-electron not available:', error)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MicaWindow extends BrowserWindow {
|
|
||||||
setMicaEffect(): void
|
|
||||||
setMicaTabbedEffect(): void
|
|
||||||
setMicaAcrylicEffect(): void
|
|
||||||
setAcrylic(): void
|
|
||||||
setBlur(): void
|
|
||||||
setTransparent(): void
|
|
||||||
setDarkTheme(): void
|
|
||||||
setLightTheme(): void
|
|
||||||
setAutoTheme(): void
|
|
||||||
setRoundedCorner(): void
|
|
||||||
setSmallRoundedCorner(): void
|
|
||||||
setSquareCorner(): void
|
|
||||||
setBorderColor(color: string | null): void
|
|
||||||
setCaptionColor(color: string | null): void
|
|
||||||
setTitleTextColor(color: string | null): void
|
|
||||||
}
|
|
||||||
|
|
||||||
export type windowOpenInput = {
|
export type windowOpenInput = {
|
||||||
key: string
|
key: string
|
||||||
title?: string
|
title?: string
|
||||||
@@ -97,9 +64,9 @@ export class WindowManager extends Service {
|
|||||||
height: 680,
|
height: 680,
|
||||||
show: false,
|
show: false,
|
||||||
autoHideMenuBar: true,
|
autoHideMenuBar: true,
|
||||||
frame: false,
|
frame: true,
|
||||||
transparent: true,
|
transparent: false,
|
||||||
backgroundColor: '#00000000',
|
backgroundColor: '#ffffff',
|
||||||
icon: this.opts.icon,
|
icon: this.opts.icon,
|
||||||
title: input.title,
|
title: input.title,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
@@ -109,13 +76,7 @@ export class WindowManager extends Service {
|
|||||||
...input.options
|
...input.options
|
||||||
}
|
}
|
||||||
|
|
||||||
let win: BrowserWindow
|
const win = new BrowserWindow(baseOptions)
|
||||||
if (MicaBrowserWindow) {
|
|
||||||
win = new MicaBrowserWindow(baseOptions)
|
|
||||||
this.applyMicaEffect(win)
|
|
||||||
} else {
|
|
||||||
win = new BrowserWindow(baseOptions)
|
|
||||||
}
|
|
||||||
|
|
||||||
const zoomSettings = this.mainCtx.settings
|
const zoomSettings = this.mainCtx.settings
|
||||||
const zoom = zoomSettings ? Number(zoomSettings.getValue('window_zoom')) || 1.0 : 1.0
|
const zoom = zoomSettings ? Number(zoomSettings.getValue('window_zoom')) || 1.0 : 1.0
|
||||||
@@ -146,18 +107,6 @@ export class WindowManager extends Service {
|
|||||||
win.webContents.send('window:maximized-changed', false)
|
win.webContents.send('window:maximized-changed', false)
|
||||||
})
|
})
|
||||||
|
|
||||||
win.on('blur', () => {
|
|
||||||
if (input.key === 'main') {
|
|
||||||
this.applyMicaEffect(win)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
win.on('focus', () => {
|
|
||||||
if (input.key === 'main') {
|
|
||||||
this.applyMicaEffect(win)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
win.webContents.setWindowOpenHandler((details) => {
|
win.webContents.setWindowOpenHandler((details) => {
|
||||||
shell.openExternal(details.url)
|
shell.openExternal(details.url)
|
||||||
return { action: 'deny' }
|
return { action: 'deny' }
|
||||||
@@ -167,160 +116,6 @@ export class WindowManager extends Service {
|
|||||||
return win
|
return win
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyMicaEffect(win: BrowserWindow) {
|
|
||||||
if (!micaElectron) return
|
|
||||||
const settings = this.mainCtx.settings
|
|
||||||
if (!settings) return
|
|
||||||
const micaWin = win as MicaWindow
|
|
||||||
const theme = settings.getValue('window_theme')
|
|
||||||
switch (theme) {
|
|
||||||
case 'dark':
|
|
||||||
micaWin.setDarkTheme()
|
|
||||||
break
|
|
||||||
case 'light':
|
|
||||||
micaWin.setLightTheme()
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
micaWin.setAutoTheme()
|
|
||||||
}
|
|
||||||
|
|
||||||
const effect = settings.getValue('window_effect')
|
|
||||||
switch (effect) {
|
|
||||||
case 'mica':
|
|
||||||
micaWin.setMicaEffect()
|
|
||||||
break
|
|
||||||
case 'tabbed':
|
|
||||||
micaWin.setMicaTabbedEffect()
|
|
||||||
break
|
|
||||||
case 'acrylic':
|
|
||||||
if (IS_WINDOWS_11) {
|
|
||||||
micaWin.setMicaAcrylicEffect()
|
|
||||||
} else {
|
|
||||||
micaWin.setAcrylic()
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'blur':
|
|
||||||
if (!IS_WINDOWS_11) {
|
|
||||||
micaWin.setBlur()
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'transparent':
|
|
||||||
if (!IS_WINDOWS_11) {
|
|
||||||
micaWin.setTransparent()
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
const radius = settings.getValue('window_radius')
|
|
||||||
switch (radius) {
|
|
||||||
case 'small':
|
|
||||||
micaWin.setSmallRoundedCorner()
|
|
||||||
break
|
|
||||||
case 'square':
|
|
||||||
micaWin.setSquareCorner()
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
micaWin.setRoundedCorner()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public setMicaEffect(
|
|
||||||
win: BrowserWindow,
|
|
||||||
effect: 'mica' | 'tabbed' | 'acrylic' | 'blur' | 'transparent' | 'none' = 'mica'
|
|
||||||
) {
|
|
||||||
if (!micaElectron) return
|
|
||||||
const micaWin = win as MicaWindow
|
|
||||||
|
|
||||||
switch (effect) {
|
|
||||||
case 'mica':
|
|
||||||
micaWin.setMicaEffect()
|
|
||||||
break
|
|
||||||
case 'tabbed':
|
|
||||||
micaWin.setMicaTabbedEffect()
|
|
||||||
break
|
|
||||||
case 'acrylic':
|
|
||||||
if (IS_WINDOWS_11) {
|
|
||||||
micaWin.setMicaAcrylicEffect()
|
|
||||||
} else {
|
|
||||||
micaWin.setAcrylic()
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'blur':
|
|
||||||
if (!IS_WINDOWS_11) {
|
|
||||||
micaWin.setBlur()
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'transparent':
|
|
||||||
if (!IS_WINDOWS_11) {
|
|
||||||
micaWin.setTransparent()
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'none':
|
|
||||||
if (IS_WINDOWS_11) {
|
|
||||||
micaWin.setMicaEffect()
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public setMicaTheme(win: BrowserWindow, theme: 'auto' | 'dark' | 'light' = 'auto') {
|
|
||||||
if (!micaElectron) return
|
|
||||||
const micaWin = win as MicaWindow
|
|
||||||
|
|
||||||
switch (theme) {
|
|
||||||
case 'dark':
|
|
||||||
micaWin.setDarkTheme()
|
|
||||||
break
|
|
||||||
case 'light':
|
|
||||||
micaWin.setLightTheme()
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
micaWin.setAutoTheme()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public setMicaCorner(win: BrowserWindow, corner: 'rounded' | 'small' | 'square' = 'rounded') {
|
|
||||||
if (!micaElectron) return
|
|
||||||
const micaWin = win as MicaWindow
|
|
||||||
|
|
||||||
switch (corner) {
|
|
||||||
case 'small':
|
|
||||||
micaWin.setSmallRoundedCorner()
|
|
||||||
break
|
|
||||||
case 'square':
|
|
||||||
micaWin.setSquareCorner()
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
micaWin.setRoundedCorner()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public setMicaBorderColor(win: BrowserWindow, color: string | null) {
|
|
||||||
if (!micaElectron) return
|
|
||||||
const micaWin = win as MicaWindow
|
|
||||||
micaWin.setBorderColor(color)
|
|
||||||
}
|
|
||||||
|
|
||||||
public setMicaCaptionColor(win: BrowserWindow, color: string | null) {
|
|
||||||
if (!micaElectron) return
|
|
||||||
const micaWin = win as MicaWindow
|
|
||||||
micaWin.setCaptionColor(color)
|
|
||||||
}
|
|
||||||
|
|
||||||
public setMicaTitleTextColor(win: BrowserWindow, color: string | null) {
|
|
||||||
if (!micaElectron) return
|
|
||||||
const micaWin = win as MicaWindow
|
|
||||||
micaWin.setTitleTextColor(color)
|
|
||||||
}
|
|
||||||
|
|
||||||
public isMicaAvailable(): boolean {
|
|
||||||
return micaElectron !== null
|
|
||||||
}
|
|
||||||
|
|
||||||
public isWindows11(): boolean {
|
|
||||||
return IS_WINDOWS_11
|
|
||||||
}
|
|
||||||
|
|
||||||
public navigate(key: string, route: string) {
|
public navigate(key: string, route: string) {
|
||||||
const win = this.get(key)
|
const win = this.get(key)
|
||||||
if (!win) return false
|
if (!win) return false
|
||||||
@@ -436,33 +231,5 @@ export class WindowManager extends Service {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
this.mainCtx.handle('window:mica-effect', (_event, effect: string) => {
|
|
||||||
const win = BrowserWindow.fromWebContents(_event.sender)
|
|
||||||
if (win) {
|
|
||||||
this.setMicaEffect(win, effect as any)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
this.mainCtx.handle('window:mica-theme', (_event, theme: string) => {
|
|
||||||
const win = BrowserWindow.fromWebContents(_event.sender)
|
|
||||||
if (win) {
|
|
||||||
this.setMicaTheme(win, theme as any)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
this.mainCtx.handle('window:mica-corner', (_event, corner: string) => {
|
|
||||||
const win = BrowserWindow.fromWebContents(_event.sender)
|
|
||||||
if (win) {
|
|
||||||
this.setMicaCorner(win, corner as any)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
this.mainCtx.handle('window:mica-info', () => {
|
|
||||||
return {
|
|
||||||
available: this.isMicaAvailable(),
|
|
||||||
isWindows11: this.isWindows11()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
import { ConfigProvider, theme as antTheme, Layout, message } from 'antd'
|
import { ConfigProvider, theme as antTheme, message } from 'antd'
|
||||||
import { HashRouter, Routes, Route, Navigate } from 'react-router-dom'
|
import { HashRouter, Routes, Route, Navigate } from 'react-router-dom'
|
||||||
import { useState, useEffect, createContext, useContext } from 'react'
|
import { useState, useEffect, createContext, useContext } from 'react'
|
||||||
import { MobileHome } from './pages/Home'
|
import { MobileHome } from './pages/Home'
|
||||||
@@ -45,7 +45,7 @@ export const useApi = () => {
|
|||||||
function App(): React.JSX.Element {
|
function App(): React.JSX.Element {
|
||||||
const [isDark, setIsDark] = useState(false)
|
const [isDark, setIsDark] = useState(false)
|
||||||
const [apiConfig, setApiConfig] = useState<ApiConfig>({ baseUrl: '' })
|
const [apiConfig, setApiConfig] = useState<ApiConfig>({ baseUrl: '' })
|
||||||
const [messageApi, messageHolder] = message.useMessage()
|
const [, messageHolder] = message.useMessage()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const savedTheme = localStorage.getItem('theme')
|
const savedTheme = localStorage.getItem('theme')
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
SettingOutlined
|
SettingOutlined
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import { useTheme } from '../App'
|
import { useTheme } from '../App'
|
||||||
import { Switch } from 'antd'
|
|
||||||
|
|
||||||
const { Content } = Layout
|
const { Content } = Layout
|
||||||
|
|
||||||
@@ -21,7 +20,7 @@ const tabs = [
|
|||||||
export function MobileLayout(): React.JSX.Element {
|
export function MobileLayout(): React.JSX.Element {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { isDark, toggleTheme } = useTheme()
|
const { isDark } = useTheme()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout
|
<Layout
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export function MobileScore(): React.JSX.Element {
|
|||||||
setSelectedReason(null)
|
setSelectedReason(null)
|
||||||
setCustomScore('')
|
setCustomScore('')
|
||||||
setCustomReason('')
|
setCustomReason('')
|
||||||
} catch (e) {
|
} catch {
|
||||||
Toast.show({ content: '提交失败', icon: 'fail' })
|
Toast.show({ content: '提交失败', icon: 'fail' })
|
||||||
} finally {
|
} finally {
|
||||||
setSubmitting(false)
|
setSubmitting(false)
|
||||||
|
|||||||
@@ -9,11 +9,6 @@ const api = {
|
|||||||
setTheme: (themeId: string) => ipcRenderer.invoke('theme:set', themeId),
|
setTheme: (themeId: string) => ipcRenderer.invoke('theme:set', themeId),
|
||||||
saveTheme: (theme: themeConfig) => ipcRenderer.invoke('theme:save', theme),
|
saveTheme: (theme: themeConfig) => ipcRenderer.invoke('theme:save', theme),
|
||||||
deleteTheme: (themeId: string) => ipcRenderer.invoke('theme:delete', themeId),
|
deleteTheme: (themeId: string) => ipcRenderer.invoke('theme:delete', themeId),
|
||||||
setCustomTheme: (config: {
|
|
||||||
effect: 'mica' | 'tabbed' | 'acrylic' | 'blur' | 'transparent' | 'none'
|
|
||||||
theme: 'auto' | 'dark' | 'light'
|
|
||||||
radius: 'small' | 'medium' | 'large'
|
|
||||||
}) => ipcRenderer.invoke('theme:set-custom', config),
|
|
||||||
onThemeChanged: (callback: (theme: themeConfig) => void) => {
|
onThemeChanged: (callback: (theme: themeConfig) => void) => {
|
||||||
const subscription = (_event: any, theme: themeConfig) => callback(theme)
|
const subscription = (_event: any, theme: themeConfig) => callback(theme)
|
||||||
ipcRenderer.on('theme:updated', subscription)
|
ipcRenderer.on('theme:updated', subscription)
|
||||||
|
|||||||
+1
-13
@@ -11,11 +11,6 @@ export interface themeConfig {
|
|||||||
name: string
|
name: string
|
||||||
id: string
|
id: string
|
||||||
mode: 'light' | 'dark'
|
mode: 'light' | 'dark'
|
||||||
mica?: {
|
|
||||||
effect: 'mica' | 'tabbed' | 'acrylic' | 'blur' | 'transparent' | 'none'
|
|
||||||
theme: 'auto' | 'dark' | 'light'
|
|
||||||
radius: 'small' | 'medium' | 'large'
|
|
||||||
}
|
|
||||||
config: {
|
config: {
|
||||||
tdesign: Record<string, string>
|
tdesign: Record<string, string>
|
||||||
custom: Record<string, string>
|
custom: Record<string, string>
|
||||||
@@ -26,9 +21,7 @@ export type settingsSpec = {
|
|||||||
is_wizard_completed: boolean
|
is_wizard_completed: boolean
|
||||||
log_level: logLevel
|
log_level: logLevel
|
||||||
window_zoom: number
|
window_zoom: number
|
||||||
window_theme: 'auto' | 'dark' | 'light'
|
themes_custom: themeConfig[]
|
||||||
window_effect: 'mica' | 'tabbed' | 'acrylic' | 'blur' | 'transparent' | 'none'
|
|
||||||
window_radius: 'rounded' | 'small' | 'square'
|
|
||||||
auto_score_enabled: boolean
|
auto_score_enabled: boolean
|
||||||
auto_score_rules: any[]
|
auto_score_rules: any[]
|
||||||
current_theme_id: string
|
current_theme_id: string
|
||||||
@@ -63,11 +56,6 @@ export interface electronApi {
|
|||||||
setTheme: (themeId: string) => Promise<ipcResponse<void>>
|
setTheme: (themeId: string) => Promise<ipcResponse<void>>
|
||||||
saveTheme: (theme: themeConfig) => Promise<ipcResponse<void>>
|
saveTheme: (theme: themeConfig) => Promise<ipcResponse<void>>
|
||||||
deleteTheme: (themeId: string) => Promise<ipcResponse<void>>
|
deleteTheme: (themeId: string) => Promise<ipcResponse<void>>
|
||||||
setCustomTheme: (config: {
|
|
||||||
effect: 'mica' | 'tabbed' | 'acrylic' | 'blur' | 'transparent' | 'none'
|
|
||||||
theme: 'auto' | 'dark' | 'light'
|
|
||||||
radius: 'small' | 'medium' | 'large'
|
|
||||||
}) => Promise<ipcResponse<void>>
|
|
||||||
onThemeChanged: (callback: (theme: themeConfig) => void) => () => void
|
onThemeChanged: (callback: (theme: themeConfig) => void) => () => void
|
||||||
|
|
||||||
// DB - Student
|
// DB - Student
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ 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, useTheme } from './contexts/ThemeContext'
|
import { ThemeProvider, useTheme } from './contexts/ThemeContext'
|
||||||
import { ThemeEditorProvider } from './contexts/ThemeEditorContext'
|
|
||||||
import { ThemeEditor } from './components/ThemeEditor'
|
|
||||||
|
|
||||||
function MainContent(): React.JSX.Element {
|
function MainContent(): React.JSX.Element {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@@ -110,13 +108,14 @@ function MainContent(): React.JSX.Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isDark = currentTheme?.mode === 'dark'
|
const isDark = currentTheme?.mode === 'dark'
|
||||||
|
const brandColor = currentTheme?.config?.tdesign?.brandColor || '#0052D9'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfigProvider
|
<ConfigProvider
|
||||||
theme={{
|
theme={{
|
||||||
algorithm: isDark ? antTheme.darkAlgorithm : antTheme.defaultAlgorithm,
|
algorithm: isDark ? antTheme.darkAlgorithm : antTheme.defaultAlgorithm,
|
||||||
token: {
|
token: {
|
||||||
colorPrimary: 'var(--ant-color-primary, #1890ff)'
|
colorPrimary: brandColor
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -222,14 +221,11 @@ function getPlatform(): string {
|
|||||||
function App(): React.JSX.Element {
|
function App(): React.JSX.Element {
|
||||||
return (
|
return (
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<ThemeEditorProvider>
|
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/*" element={<MainContent />} />
|
<Route path="/*" element={<MainContent />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
<ThemeEditor />
|
|
||||||
</ThemeEditorProvider>
|
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ body,
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
background: var(--ss-bg-color, #f5f5f5);
|
||||||
}
|
}
|
||||||
|
|
||||||
input,
|
input,
|
||||||
@@ -29,11 +30,10 @@ textarea {
|
|||||||
|
|
||||||
.ss-sidebar .ant-menu-item-selected {
|
.ss-sidebar .ant-menu-item-selected {
|
||||||
background-color: var(--ss-sidebar-active-bg, var(--ss-item-hover, transparent)) !important;
|
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)));
|
color: var(
|
||||||
}
|
--ss-sidebar-active-text,
|
||||||
|
var(--ant-color-primary, var(--ss-sidebar-text, var(--ss-text-main)))
|
||||||
html[theme-mode='dark'] .ss-sidebar .ant-menu-item-selected {
|
);
|
||||||
color: #ffffff !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (pointer: coarse) {
|
@media (pointer: coarse) {
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import React, { Suspense, lazy } from 'react'
|
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 { 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 Home = lazy(() => import('./Home').then((m) => ({ default: m.Home })))
|
||||||
const StudentManager = lazy(() =>
|
const StudentManager = lazy(() =>
|
||||||
@@ -51,17 +49,16 @@ export function ContentArea({
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
backgroundColor: 'var(--ss-bg-color)'
|
background: 'var(--ss-bg-color)'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
height: '32px',
|
height: '40px',
|
||||||
WebkitAppRegion: 'drag',
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: 'var(--ss-header-bg)',
|
background: 'var(--ss-header-bg)',
|
||||||
borderBottom: '1px solid var(--ss-border-color)',
|
borderBottom: '1px solid var(--ss-border-color)',
|
||||||
flexShrink: 0
|
flexShrink: 0
|
||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
@@ -73,12 +70,11 @@ export function ContentArea({
|
|||||||
{
|
{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingRight: '0px',
|
paddingRight: '12px'
|
||||||
WebkitAppRegion: 'no-drag'
|
|
||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Space size="small" style={{ marginRight: '12px' }}>
|
<Space size="small">
|
||||||
{permissionTag}
|
{permissionTag}
|
||||||
{hasAnyPassword && (
|
{hasAnyPassword && (
|
||||||
<>
|
<>
|
||||||
@@ -91,7 +87,6 @@ export function ContentArea({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
<WindowControls />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -106,7 +101,16 @@ export function ContentArea({
|
|||||||
height: '100%'
|
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>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -129,7 +133,6 @@ export function ContentArea({
|
|||||||
</Routes>
|
</Routes>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</Content>
|
</Content>
|
||||||
<ThemeEditor />
|
|
||||||
</Layout>
|
</Layout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -786,7 +786,7 @@ export const Home: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
padding: '16px',
|
padding: '16px',
|
||||||
backgroundColor:
|
background:
|
||||||
customScore > 0
|
customScore > 0
|
||||||
? 'var(--ant-color-success-bg, #f6ffed)'
|
? 'var(--ant-color-success-bg, #f6ffed)'
|
||||||
: customScore < 0
|
: customScore < 0
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { Tabs, Card, Form, Select, Input, Button, Space, Divider, Tag, Modal, message } from 'antd'
|
import { Tabs, Card, Form, Select, Input, Button, Space, Divider, Tag, Modal, message } from 'antd'
|
||||||
import { useTheme } from '../contexts/ThemeContext'
|
import { ThemeQuickSettings } from './ThemeQuickSettings'
|
||||||
import { useThemeEditor } from '../contexts/ThemeEditorContext'
|
|
||||||
|
|
||||||
type permissionLevel = 'admin' | 'points' | 'view'
|
type permissionLevel = 'admin' | 'points' | 'view'
|
||||||
type appSettings = {
|
type appSettings = {
|
||||||
@@ -12,8 +11,6 @@ type appSettings = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission }) => {
|
export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission }) => {
|
||||||
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,
|
||||||
@@ -313,22 +310,11 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
|
|||||||
label: '外观',
|
label: '外观',
|
||||||
children: (
|
children: (
|
||||||
<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 layout="horizontal" labelCol={{ span: 4 }} wrapperCol={{ span: 20 }}>
|
<ThemeQuickSettings />
|
||||||
<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>
|
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<Form layout="horizontal" labelCol={{ span: 4 }} wrapperCol={{ span: 20 }}>
|
||||||
<Form.Item label="界面缩放">
|
<Form.Item label="界面缩放">
|
||||||
<Select
|
<Select
|
||||||
value={settings.window_zoom || '1.0'}
|
value={settings.window_zoom || '1.0'}
|
||||||
@@ -690,7 +676,6 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
|
|||||||
>
|
>
|
||||||
<div>secscore://settings</div>
|
<div>secscore://settings</div>
|
||||||
<div>secscore://score</div>
|
<div>secscore://score</div>
|
||||||
<div>secscore://sidebar/toggle</div>
|
|
||||||
</div>
|
</div>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Space>
|
<Space>
|
||||||
@@ -767,7 +752,14 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
|
|||||||
]
|
]
|
||||||
|
|
||||||
return (
|
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}
|
{contextHolder}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -777,7 +769,7 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
|
|||||||
marginBottom: '16px'
|
marginBottom: '16px'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<h2 style={{ margin: 0, color: 'var(--ss-text-main)' }}>系统设置</h2>
|
<h2 style={{ margin: 0 }}>系统设置</h2>
|
||||||
{permissionTag}
|
{permissionTag}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -871,7 +863,7 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
|
|||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||||
<div>将把当前未结算的积分记录归档为一个阶段,并将所有学生当前积分清零。</div>
|
<div>将把当前未结算的积分记录归档为一个阶段,并将所有学生当前积分清零。</div>
|
||||||
<div style={{ color: 'var(--ss-text-secondary)', fontSize: '12px' }}>
|
<div style={{ color: 'var(--ss-text-secondary)', fontSize: '12px' }}>
|
||||||
学生名单不变;结算后的历史在"结算历史"页面查看。
|
学生名单不变;结算后的历史在“结算历史”页面查看。
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ export function Sidebar({ activeMenu, permission, onMenuChange }: SidebarProps):
|
|||||||
className="ss-sidebar"
|
className="ss-sidebar"
|
||||||
width={200}
|
width={200}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'var(--ss-sidebar-bg)',
|
background: 'var(--ss-sidebar-bg)',
|
||||||
borderRight: '1px solid var(--ss-border-color)',
|
borderRight: '1px solid var(--ss-border-color)',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column'
|
flexDirection: 'column'
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ export const ThemeEditor: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<Row gutter={[12, 12]}>
|
<Row gutter={[12, 12]}>
|
||||||
{group.items.map((item) => (
|
{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={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -192,6 +192,25 @@ export const ThemeEditor: React.FC = () => {
|
|||||||
>
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
</div>
|
</div>
|
||||||
|
{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
|
<ColorPicker
|
||||||
value={editingTheme.config.custom[item.key] || '#ffffff'}
|
value={editingTheme.config.custom[item.key] || '#ffffff'}
|
||||||
onChange={(color: Color) =>
|
onChange={(color: Color) =>
|
||||||
@@ -199,6 +218,7 @@ export const ThemeEditor: React.FC = () => {
|
|||||||
}
|
}
|
||||||
showText
|
showText
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</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 React, { useState } from 'react'
|
||||||
import { Modal, Form, Select, message, Typography } from 'antd'
|
import { Modal, message, Typography } from 'antd'
|
||||||
import { useTheme } from '../contexts/ThemeContext'
|
import { ThemeQuickSettings } from './ThemeQuickSettings'
|
||||||
|
|
||||||
interface wizardProps {
|
interface wizardProps {
|
||||||
visible: boolean
|
visible: boolean
|
||||||
@@ -8,7 +8,6 @@ interface wizardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Wizard: React.FC<wizardProps> = ({ visible, onComplete }) => {
|
export const Wizard: React.FC<wizardProps> = ({ visible, onComplete }) => {
|
||||||
const { themes, currentTheme, setTheme } = useTheme()
|
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [messageApi, contextHolder] = message.useMessage()
|
const [messageApi, contextHolder] = message.useMessage()
|
||||||
|
|
||||||
@@ -47,15 +46,7 @@ export const Wizard: React.FC<wizardProps> = ({ visible, onComplete }) => {
|
|||||||
感谢选择 SecScore。在开始之前,请花一分钟完成基础配置。
|
感谢选择 SecScore。在开始之前,请花一分钟完成基础配置。
|
||||||
</Typography.Paragraph>
|
</Typography.Paragraph>
|
||||||
|
|
||||||
<Form layout="horizontal" labelCol={{ span: 6 }} wrapperCol={{ span: 18 }}>
|
<ThemeQuickSettings compact />
|
||||||
<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>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ interface themeContextType {
|
|||||||
currentTheme: themeConfig | null
|
currentTheme: themeConfig | null
|
||||||
setTheme: (id: string) => Promise<void>
|
setTheme: (id: string) => Promise<void>
|
||||||
themes: themeConfig[]
|
themes: themeConfig[]
|
||||||
|
applyTheme: (theme: themeConfig) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const ThemeContext = createContext<themeContextType | undefined>(undefined)
|
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)
|
root.setAttribute('theme-mode', theme.mode)
|
||||||
|
|
||||||
if (tdesign.brandColor) {
|
const brandColor = tdesign.brandColor || '#1677FF'
|
||||||
const colorMap = generateColorMap(tdesign.brandColor, theme.mode)
|
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]) => {
|
Object.entries(colorMap).forEach(([key, value]) => {
|
||||||
root.style.setProperty(key, value)
|
root.style.setProperty(key, value)
|
||||||
nextKeys.push(key)
|
nextKeys.push(key)
|
||||||
})
|
})
|
||||||
|
|
||||||
root.style.setProperty('--ant-color-primary', tdesign.brandColor)
|
root.style.setProperty('--ant-color-primary', brandColor)
|
||||||
nextKeys.push('--ant-color-primary')
|
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-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-active', `rgba(${r}, ${g}, ${b}, 0.7)`)
|
||||||
root.style.setProperty('--ant-color-primary-bg', `rgba(${r}, ${g}, ${b}, 0.1)`)
|
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)
|
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
|
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 (
|
return (
|
||||||
<ThemeContext.Provider value={{ currentTheme, setTheme, themes }}>
|
<ThemeContext.Provider value={{ currentTheme, setTheme, themes, applyTheme }}>
|
||||||
{children}
|
{children}
|
||||||
</ThemeContext.Provider>
|
</ThemeContext.Provider>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Acrylic-自动",
|
|
||||||
"id": "acrylic-auto",
|
|
||||||
"mode": "light",
|
|
||||||
"mica": {
|
|
||||||
"effect": "acrylic",
|
|
||||||
"theme": "auto",
|
|
||||||
"radius": "medium"
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"tdesign": {
|
|
||||||
"brandColor": "#0052D9",
|
|
||||||
"warningColor": "#E37318",
|
|
||||||
"errorColor": "#D32029",
|
|
||||||
"successColor": "#248232"
|
|
||||||
},
|
|
||||||
"custom": {
|
|
||||||
"--ss-bg-color": "transparent",
|
|
||||||
"--ss-card-bg": "rgba(255, 255, 255, 0.3)",
|
|
||||||
"--ss-text-main": "#1a1a1a",
|
|
||||||
"--ss-text-secondary": "#5e5e5e",
|
|
||||||
"--ss-border-color": "rgba(0, 0, 0, 0.08)",
|
|
||||||
"--ss-header-bg": "transparent",
|
|
||||||
"--ss-sidebar-bg": "rgba(255, 255, 255, 0.5)",
|
|
||||||
"--ss-item-hover": "rgba(0, 0, 0, 0.04)",
|
|
||||||
"--ss-sidebar-text": "#1a1a1a",
|
|
||||||
"--ss-sidebar-active-bg": "rgba(0, 0, 0, 0.06)",
|
|
||||||
"--ss-sidebar-active-text": "#1a1a1a"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Acrylic-深色",
|
|
||||||
"id": "acrylic-dark",
|
|
||||||
"mode": "dark",
|
|
||||||
"mica": {
|
|
||||||
"effect": "acrylic",
|
|
||||||
"theme": "dark",
|
|
||||||
"radius": "medium"
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"tdesign": {
|
|
||||||
"brandColor": "#4C8BF5",
|
|
||||||
"warningColor": "#E37318",
|
|
||||||
"errorColor": "#D54941",
|
|
||||||
"successColor": "#2BA471"
|
|
||||||
},
|
|
||||||
"custom": {
|
|
||||||
"--ss-bg-color": "transparent",
|
|
||||||
"--ss-card-bg": "rgba(30, 30, 30, 0.3)",
|
|
||||||
"--ss-text-main": "#e0e0e0",
|
|
||||||
"--ss-text-secondary": "#a0a0a0",
|
|
||||||
"--ss-border-color": "rgba(255, 255, 255, 0.08)",
|
|
||||||
"--ss-header-bg": "transparent",
|
|
||||||
"--ss-sidebar-bg": "rgba(30, 30, 30, 0.5)",
|
|
||||||
"--ss-item-hover": "rgba(255, 255, 255, 0.04)",
|
|
||||||
"--ss-sidebar-text": "#e0e0e0",
|
|
||||||
"--ss-sidebar-active-bg": "rgba(255, 255, 255, 0.06)",
|
|
||||||
"--ss-sidebar-active-text": "#e0e0e0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Acrylic-浅色",
|
|
||||||
"id": "acrylic-light",
|
|
||||||
"mode": "light",
|
|
||||||
"mica": {
|
|
||||||
"effect": "acrylic",
|
|
||||||
"theme": "light",
|
|
||||||
"radius": "medium"
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"tdesign": {
|
|
||||||
"brandColor": "#0052D9",
|
|
||||||
"warningColor": "#E37318",
|
|
||||||
"errorColor": "#D32029",
|
|
||||||
"successColor": "#248232"
|
|
||||||
},
|
|
||||||
"custom": {
|
|
||||||
"--ss-bg-color": "transparent",
|
|
||||||
"--ss-card-bg": "rgba(255, 255, 255, 0.3)",
|
|
||||||
"--ss-text-main": "#1a1a1a",
|
|
||||||
"--ss-text-secondary": "#5e5e5e",
|
|
||||||
"--ss-border-color": "rgba(0, 0, 0, 0.08)",
|
|
||||||
"--ss-header-bg": "transparent",
|
|
||||||
"--ss-sidebar-bg": "rgba(255, 255, 255, 0.5)",
|
|
||||||
"--ss-item-hover": "rgba(0, 0, 0, 0.04)",
|
|
||||||
"--ss-sidebar-text": "#1a1a1a",
|
|
||||||
"--ss-sidebar-active-bg": "rgba(0, 0, 0, 0.06)",
|
|
||||||
"--ss-sidebar-active-text": "#1a1a1a"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "暖阳琥珀",
|
|
||||||
"id": "light-amber",
|
|
||||||
"mode": "light",
|
|
||||||
"config": {
|
|
||||||
"tdesign": {
|
|
||||||
"brandColor": "#F39C12",
|
|
||||||
"warningColor": "#E67E22",
|
|
||||||
"errorColor": "#C0392B",
|
|
||||||
"successColor": "#27AE60"
|
|
||||||
},
|
|
||||||
"custom": {
|
|
||||||
"--ss-bg-color": "#FFF8E6",
|
|
||||||
"--ss-card-bg": "#FFFFFF",
|
|
||||||
"--ss-text-main": "#3A2A12",
|
|
||||||
"--ss-text-secondary": "#8A7350",
|
|
||||||
"--ss-border-color": "#F4D9A6",
|
|
||||||
"--ss-header-bg": "#FFFFFF",
|
|
||||||
"--ss-sidebar-bg": "#FFFFFF",
|
|
||||||
"--ss-item-hover": "#FFEAC2",
|
|
||||||
"--ss-sidebar-active-bg": "#FFEAC2",
|
|
||||||
"--ss-sidebar-active-text": "#3A2A12"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "冷静青蓝",
|
|
||||||
"id": "dark-cyan",
|
|
||||||
"mode": "dark",
|
|
||||||
"config": {
|
|
||||||
"tdesign": {
|
|
||||||
"brandColor": "#16A085",
|
|
||||||
"warningColor": "#F39C12",
|
|
||||||
"errorColor": "#E74C3C",
|
|
||||||
"successColor": "#1ABC9C"
|
|
||||||
},
|
|
||||||
"custom": {
|
|
||||||
"--ss-bg-color": "#050B10",
|
|
||||||
"--ss-card-bg": "#0F1A23",
|
|
||||||
"--ss-text-main": "#E5F7FF",
|
|
||||||
"--ss-text-secondary": "#7FA4B8",
|
|
||||||
"--ss-border-color": "#1F3645",
|
|
||||||
"--ss-header-bg": "#0F1A23",
|
|
||||||
"--ss-sidebar-bg": "#0F1A23",
|
|
||||||
"--ss-item-hover": "#182635",
|
|
||||||
"--ss-sidebar-active-bg": "#182635",
|
|
||||||
"--ss-sidebar-active-text": "#E5F7FF"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "极客深蓝",
|
|
||||||
"id": "dark-default",
|
|
||||||
"mode": "dark",
|
|
||||||
"config": {
|
|
||||||
"tdesign": {
|
|
||||||
"brandColor": "#0052D9",
|
|
||||||
"warningColor": "#E37318",
|
|
||||||
"errorColor": "#D32029",
|
|
||||||
"successColor": "#248232"
|
|
||||||
},
|
|
||||||
"custom": {
|
|
||||||
"--ss-bg-color": "#121212",
|
|
||||||
"--ss-card-bg": "#1e1e1e",
|
|
||||||
"--ss-text-main": "#ffffff",
|
|
||||||
"--ss-text-secondary": "#a0a0a0",
|
|
||||||
"--ss-border-color": "#333333",
|
|
||||||
"--ss-header-bg": "#1e1e1e",
|
|
||||||
"--ss-sidebar-bg": "#1e1e1e",
|
|
||||||
"--ss-item-hover": "#2c2c2c",
|
|
||||||
"--ss-sidebar-text": "#ffffff",
|
|
||||||
"--ss-sidebar-active-bg": "#2c2c2c",
|
|
||||||
"--ss-sidebar-active-text": "#ffffff"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "校园青绿",
|
|
||||||
"id": "light-green",
|
|
||||||
"mode": "light",
|
|
||||||
"config": {
|
|
||||||
"tdesign": {
|
|
||||||
"brandColor": "#1ABC9C",
|
|
||||||
"warningColor": "#F5B041",
|
|
||||||
"errorColor": "#E74C3C",
|
|
||||||
"successColor": "#27AE60"
|
|
||||||
},
|
|
||||||
"custom": {
|
|
||||||
"--ss-bg-color": "#F0F9F6",
|
|
||||||
"--ss-card-bg": "#FFFFFF",
|
|
||||||
"--ss-text-main": "#123D2B",
|
|
||||||
"--ss-text-secondary": "#5F7D6C",
|
|
||||||
"--ss-border-color": "#D1E9DD",
|
|
||||||
"--ss-header-bg": "#FFFFFF",
|
|
||||||
"--ss-sidebar-bg": "#FFFFFF",
|
|
||||||
"--ss-item-hover": "#E1F3EB",
|
|
||||||
"--ss-sidebar-active-bg": "#E1F3EB",
|
|
||||||
"--ss-sidebar-active-text": "#123D2B"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "极简浅色",
|
|
||||||
"id": "light-default",
|
|
||||||
"mode": "light",
|
|
||||||
"config": {
|
|
||||||
"tdesign": {
|
|
||||||
"brandColor": "#0052D9",
|
|
||||||
"warningColor": "#ED7B2F",
|
|
||||||
"errorColor": "#D54941",
|
|
||||||
"successColor": "#2BA471"
|
|
||||||
},
|
|
||||||
"custom": {
|
|
||||||
"--ss-bg-color": "#f3f3f3",
|
|
||||||
"--ss-card-bg": "#ffffff",
|
|
||||||
"--ss-text-main": "#181818",
|
|
||||||
"--ss-text-secondary": "#666666",
|
|
||||||
"--ss-border-color": "#dcdcdc",
|
|
||||||
"--ss-header-bg": "#ffffff",
|
|
||||||
"--ss-sidebar-bg": "#ffffff",
|
|
||||||
"--ss-item-hover": "#f3f3f3",
|
|
||||||
"--ss-sidebar-active-bg": "#f3f3f3",
|
|
||||||
"--ss-sidebar-active-text": "#181818"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Mica-自动",
|
|
||||||
"id": "mica-auto",
|
|
||||||
"mode": "light",
|
|
||||||
"mica": {
|
|
||||||
"effect": "mica",
|
|
||||||
"theme": "auto",
|
|
||||||
"radius": "medium"
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"tdesign": {
|
|
||||||
"brandColor": "#0052D9",
|
|
||||||
"warningColor": "#E37318",
|
|
||||||
"errorColor": "#D32029",
|
|
||||||
"successColor": "#248232"
|
|
||||||
},
|
|
||||||
"custom": {
|
|
||||||
"--ss-bg-color": "transparent",
|
|
||||||
"--ss-card-bg": "rgba(255, 255, 255, 0.3)",
|
|
||||||
"--ss-text-main": "#1a1a1a",
|
|
||||||
"--ss-text-secondary": "#5e5e5e",
|
|
||||||
"--ss-border-color": "rgba(0, 0, 0, 0.1)",
|
|
||||||
"--ss-header-bg": "transparent",
|
|
||||||
"--ss-sidebar-bg": "rgba(255, 255, 255, 0.5)",
|
|
||||||
"--ss-item-hover": "rgba(0, 0, 0, 0.05)",
|
|
||||||
"--ss-sidebar-text": "#1a1a1a",
|
|
||||||
"--ss-sidebar-active-bg": "rgba(0, 0, 0, 0.08)",
|
|
||||||
"--ss-sidebar-active-text": "#1a1a1a"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Mica-深色",
|
|
||||||
"id": "mica-dark",
|
|
||||||
"mode": "dark",
|
|
||||||
"mica": {
|
|
||||||
"effect": "mica",
|
|
||||||
"theme": "dark",
|
|
||||||
"radius": "medium"
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"tdesign": {
|
|
||||||
"brandColor": "#4C8BF5",
|
|
||||||
"warningColor": "#E37318",
|
|
||||||
"errorColor": "#D54941",
|
|
||||||
"successColor": "#2BA471"
|
|
||||||
},
|
|
||||||
"custom": {
|
|
||||||
"--ss-bg-color": "transparent",
|
|
||||||
"--ss-card-bg": "rgba(30, 30, 30, 0.3)",
|
|
||||||
"--ss-text-main": "#e0e0e0",
|
|
||||||
"--ss-text-secondary": "#a0a0a0",
|
|
||||||
"--ss-border-color": "rgba(255, 255, 255, 0.1)",
|
|
||||||
"--ss-header-bg": "transparent",
|
|
||||||
"--ss-sidebar-bg": "rgba(30, 30, 30, 0.5)",
|
|
||||||
"--ss-item-hover": "rgba(255, 255, 255, 0.05)",
|
|
||||||
"--ss-sidebar-text": "#e0e0e0",
|
|
||||||
"--ss-sidebar-active-bg": "rgba(255, 255, 255, 0.08)",
|
|
||||||
"--ss-sidebar-active-text": "#e0e0e0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Mica-浅色",
|
|
||||||
"id": "mica-light",
|
|
||||||
"mode": "light",
|
|
||||||
"mica": {
|
|
||||||
"effect": "mica",
|
|
||||||
"theme": "light",
|
|
||||||
"radius": "medium"
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"tdesign": {
|
|
||||||
"brandColor": "#0052D9",
|
|
||||||
"warningColor": "#E37318",
|
|
||||||
"errorColor": "#D32029",
|
|
||||||
"successColor": "#248232"
|
|
||||||
},
|
|
||||||
"custom": {
|
|
||||||
"--ss-bg-color": "transparent",
|
|
||||||
"--ss-card-bg": "rgba(255, 255, 255, 0.3)",
|
|
||||||
"--ss-text-main": "#1a1a1a",
|
|
||||||
"--ss-text-secondary": "#5e5e5e",
|
|
||||||
"--ss-border-color": "rgba(0, 0, 0, 0.1)",
|
|
||||||
"--ss-header-bg": "transparent",
|
|
||||||
"--ss-sidebar-bg": "rgba(255, 255, 255, 0.5)",
|
|
||||||
"--ss-item-hover": "rgba(0, 0, 0, 0.05)",
|
|
||||||
"--ss-sidebar-text": "#1a1a1a",
|
|
||||||
"--ss-sidebar-active-bg": "rgba(0, 0, 0, 0.08)",
|
|
||||||
"--ss-sidebar-active-text": "#1a1a1a"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "云母",
|
|
||||||
"id": "mica",
|
|
||||||
"mode": "light",
|
|
||||||
"config": {
|
|
||||||
"tdesign": {
|
|
||||||
"brandColor": "#0052D9",
|
|
||||||
"warningColor": "#E37318",
|
|
||||||
"errorColor": "#D32029",
|
|
||||||
"successColor": "#248232"
|
|
||||||
},
|
|
||||||
"custom": {
|
|
||||||
"--ss-bg-color": "transparent",
|
|
||||||
"--ss-card-bg": "rgba(255, 255, 255, 0.7)",
|
|
||||||
"--ss-text-main": "#1a1a1a",
|
|
||||||
"--ss-text-secondary": "#5e5e5e",
|
|
||||||
"--ss-border-color": "rgba(0, 0, 0, 0.1)",
|
|
||||||
"--ss-header-bg": "transparent",
|
|
||||||
"--ss-sidebar-bg": "rgba(255, 255, 255, 0.85)",
|
|
||||||
"--ss-item-hover": "rgba(0, 0, 0, 0.05)",
|
|
||||||
"--ss-sidebar-text": "#1a1a1a",
|
|
||||||
"--ss-sidebar-active-bg": "rgba(0, 0, 0, 0.08)",
|
|
||||||
"--ss-sidebar-active-text": "#1a1a1a"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "清新马卡龙",
|
|
||||||
"id": "light-pastel",
|
|
||||||
"mode": "light",
|
|
||||||
"config": {
|
|
||||||
"tdesign": {
|
|
||||||
"brandColor": "#FF9AA2",
|
|
||||||
"warningColor": "#FFB347",
|
|
||||||
"errorColor": "#FF6F69",
|
|
||||||
"successColor": "#B5EAD7"
|
|
||||||
},
|
|
||||||
"custom": {
|
|
||||||
"--ss-bg-color": "#FFF7F1",
|
|
||||||
"--ss-card-bg": "#FFFFFF",
|
|
||||||
"--ss-text-main": "#3A3A3A",
|
|
||||||
"--ss-text-secondary": "#8A8A8A",
|
|
||||||
"--ss-border-color": "#F1D3D3",
|
|
||||||
"--ss-header-bg": "#FFFFFF",
|
|
||||||
"--ss-sidebar-bg": "#FFFFFF",
|
|
||||||
"--ss-item-hover": "#FFE7E0",
|
|
||||||
"--ss-sidebar-active-bg": "#FFE7E0",
|
|
||||||
"--ss-sidebar-active-text": "#3A3A3A"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "星空紫蓝",
|
|
||||||
"id": "dark-purple",
|
|
||||||
"mode": "dark",
|
|
||||||
"config": {
|
|
||||||
"tdesign": {
|
|
||||||
"brandColor": "#8E44AD",
|
|
||||||
"warningColor": "#F1C40F",
|
|
||||||
"errorColor": "#E74C3C",
|
|
||||||
"successColor": "#2ECC71"
|
|
||||||
},
|
|
||||||
"custom": {
|
|
||||||
"--ss-bg-color": "#0B0A1A",
|
|
||||||
"--ss-card-bg": "#18152C",
|
|
||||||
"--ss-text-main": "#F5F3FF",
|
|
||||||
"--ss-text-secondary": "#A69AC7",
|
|
||||||
"--ss-border-color": "#332F4D",
|
|
||||||
"--ss-header-bg": "#18152C",
|
|
||||||
"--ss-sidebar-bg": "#18152C",
|
|
||||||
"--ss-item-hover": "#252046",
|
|
||||||
"--ss-sidebar-active-bg": "#252046",
|
|
||||||
"--ss-sidebar-active-text": "#F5F3FF"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user