mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-19 19:04:23 +08:00
请输入文本:文本
This commit is contained in:
+1
-9
@@ -49,7 +49,6 @@ type mainAppConfig = {
|
||||
dataRoot: string
|
||||
configDir: string
|
||||
logDir: string
|
||||
themeDir: string
|
||||
dbPath: string
|
||||
pgConnectionString?: string
|
||||
window: windowManagerOptions
|
||||
@@ -188,9 +187,6 @@ app.whenReady().then(async () => {
|
||||
|
||||
const logDir = is.dev ? join(process.cwd(), 'logs') : join(dataRoot, 'logs')
|
||||
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 pgConnectionString = process.env['PG_CONNECTION_STRING'] || undefined
|
||||
@@ -201,7 +197,6 @@ app.whenReady().then(async () => {
|
||||
dataRoot,
|
||||
logDir,
|
||||
configDir,
|
||||
themeDir,
|
||||
dbPath,
|
||||
pgConnectionString,
|
||||
window: {
|
||||
@@ -263,10 +258,7 @@ app.whenReady().then(async () => {
|
||||
(p) => new TagRepository((p.get(DbManagerToken) as DbManager).dataSource)
|
||||
)
|
||||
|
||||
services.addSingleton(
|
||||
ThemeServiceToken,
|
||||
(p) => new ThemeService(p.get(MainContext), config.themeDir)
|
||||
)
|
||||
services.addSingleton(ThemeServiceToken, (p) => new ThemeService(p.get(MainContext)))
|
||||
services.addSingleton(
|
||||
WindowManagerToken,
|
||||
(p) => new WindowManager(p.get(MainContext), config.window)
|
||||
|
||||
@@ -100,9 +100,7 @@ export class DbConnectionService extends Service {
|
||||
return { success: true }
|
||||
} catch (error: any) {
|
||||
if (this.testDataSource) {
|
||||
try {
|
||||
await this.testDataSource.destroy()
|
||||
} catch {}
|
||||
await this.testDataSource.destroy().catch(() => null)
|
||||
this.testDataSource = null
|
||||
}
|
||||
console.error('PostgreSQL connection test failed:', error)
|
||||
|
||||
@@ -60,24 +60,10 @@ export class SettingsService extends Service {
|
||||
})
|
||||
}
|
||||
},
|
||||
window_theme: {
|
||||
kind: 'string',
|
||||
defaultValue: 'auto',
|
||||
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'
|
||||
themes_custom: {
|
||||
kind: 'json',
|
||||
defaultValue: [],
|
||||
writePermission: 'admin'
|
||||
},
|
||||
auto_score_enabled: {
|
||||
kind: 'boolean',
|
||||
|
||||
+164
-116
@@ -1,19 +1,11 @@
|
||||
import { Service } from '../../shared/kernel'
|
||||
import { MainContext } from '../context'
|
||||
import { BrowserWindow } from 'electron'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { FSWatcher, watch } from 'chokidar'
|
||||
|
||||
export interface themeConfig {
|
||||
name: string
|
||||
id: string
|
||||
mode: 'light' | 'dark'
|
||||
mica?: {
|
||||
effect: 'mica' | 'tabbed' | 'acrylic' | 'blur' | 'transparent' | 'none'
|
||||
theme: 'auto' | 'dark' | 'light'
|
||||
radius: 'small' | 'medium' | 'large'
|
||||
}
|
||||
config: {
|
||||
tdesign: Record<string, string>
|
||||
custom: Record<string, string>
|
||||
@@ -27,19 +19,118 @@ declare module '../../shared/kernel' {
|
||||
}
|
||||
|
||||
export class ThemeService extends Service {
|
||||
private themeDir: string
|
||||
private watcher: FSWatcher | null = null
|
||||
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')
|
||||
this.themeDir = themeDir
|
||||
this.setupWatcher()
|
||||
this.registerIpc()
|
||||
|
||||
ctx.effect(() => {
|
||||
if (this.watcher) this.watcher.close()
|
||||
})
|
||||
}
|
||||
|
||||
private get mainCtx() {
|
||||
@@ -47,12 +138,14 @@ export class ThemeService extends Service {
|
||||
}
|
||||
|
||||
public async init() {
|
||||
await this.mainCtx.settings.initialize()
|
||||
await this.loadSavedTheme()
|
||||
await this.loadCustomThemes()
|
||||
}
|
||||
|
||||
private async loadSavedTheme() {
|
||||
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') {
|
||||
const themes = this.getThemeList()
|
||||
const exists = themes.some((t) => t.id === savedThemeId)
|
||||
@@ -67,26 +160,27 @@ export class ThemeService extends Service {
|
||||
|
||||
private async saveCurrentTheme() {
|
||||
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) {
|
||||
this.mainCtx.logger.warn('Failed to save theme', { meta: e })
|
||||
}
|
||||
}
|
||||
|
||||
private setupWatcher() {
|
||||
if (this.watcher) this.watcher.close()
|
||||
|
||||
this.watcher = watch(this.themeDir, {
|
||||
ignored: /(^|[/\\])\../,
|
||||
persistent: true
|
||||
})
|
||||
|
||||
this.watcher.on('change', (filePath) => {
|
||||
if (filePath.endsWith('.json')) {
|
||||
this.mainCtx.logger.info('Theme file changed', { filePath })
|
||||
this.notifyThemeUpdate()
|
||||
private async loadCustomThemes() {
|
||||
try {
|
||||
const v = this.mainCtx.settings.getValue('themes_custom')
|
||||
if (Array.isArray(v)) {
|
||||
this.customThemes = v.filter((t) => t && typeof t === 'object') as any
|
||||
} else {
|
||||
this.customThemes = []
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
this.customThemes = []
|
||||
}
|
||||
}
|
||||
|
||||
private async saveCustomThemes() {
|
||||
await this.mainCtx.settings.setValue('themes_custom', this.customThemes)
|
||||
}
|
||||
|
||||
private registerIpc() {
|
||||
@@ -107,7 +201,6 @@ export class ThemeService extends Service {
|
||||
}
|
||||
this.currentThemeId = themeId
|
||||
await this.saveCurrentTheme()
|
||||
this.applyMicaEffect(themeId)
|
||||
this.notifyThemeUpdate()
|
||||
return { success: true }
|
||||
})
|
||||
@@ -117,8 +210,29 @@ export class ThemeService extends Service {
|
||||
return { success: false, message: 'Permission denied' }
|
||||
|
||||
try {
|
||||
const filePath = path.join(this.themeDir, `${theme.id}.json`)
|
||||
fs.writeFileSync(filePath, JSON.stringify(theme, null, 2), 'utf-8')
|
||||
if (!theme?.id || !theme?.name) {
|
||||
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()
|
||||
return { success: true }
|
||||
} catch (e) {
|
||||
@@ -130,17 +244,23 @@ export class ThemeService extends Service {
|
||||
if (!this.mainCtx.permissions.requirePermission(event, 'admin'))
|
||||
return { success: false, message: 'Permission denied' }
|
||||
|
||||
if (themeId.startsWith('default-')) {
|
||||
return { success: false, message: 'Cannot delete default themes' }
|
||||
}
|
||||
|
||||
try {
|
||||
const filePath = path.join(this.themeDir, `${themeId}.json`)
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath)
|
||||
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' }
|
||||
}
|
||||
|
||||
const before = this.customThemes.length
|
||||
this.customThemes = this.customThemes.filter((t) => t.id !== id)
|
||||
if (this.customThemes.length === before) {
|
||||
return { success: false, message: 'Theme not found' }
|
||||
}
|
||||
|
||||
await this.saveCustomThemes()
|
||||
if (this.currentThemeId === themeId) {
|
||||
this.currentThemeId = 'light' // Fallback
|
||||
this.currentThemeId = 'light-default'
|
||||
await this.saveCurrentTheme()
|
||||
}
|
||||
this.notifyThemeUpdate()
|
||||
return { success: true }
|
||||
@@ -148,82 +268,10 @@ export class ThemeService extends Service {
|
||||
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[] {
|
||||
try {
|
||||
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 []
|
||||
}
|
||||
return [...this.builtinThemes, ...this.customThemes]
|
||||
}
|
||||
|
||||
private getThemeById(id: string): themeConfig | null {
|
||||
|
||||
@@ -3,39 +3,6 @@ import { MainContext } from '../context'
|
||||
import { BrowserWindow, shell } 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 = {
|
||||
key: string
|
||||
title?: string
|
||||
@@ -97,9 +64,9 @@ export class WindowManager extends Service {
|
||||
height: 680,
|
||||
show: false,
|
||||
autoHideMenuBar: true,
|
||||
frame: false,
|
||||
transparent: true,
|
||||
backgroundColor: '#00000000',
|
||||
frame: true,
|
||||
transparent: false,
|
||||
backgroundColor: '#ffffff',
|
||||
icon: this.opts.icon,
|
||||
title: input.title,
|
||||
webPreferences: {
|
||||
@@ -109,13 +76,7 @@ export class WindowManager extends Service {
|
||||
...input.options
|
||||
}
|
||||
|
||||
let win: BrowserWindow
|
||||
if (MicaBrowserWindow) {
|
||||
win = new MicaBrowserWindow(baseOptions)
|
||||
this.applyMicaEffect(win)
|
||||
} else {
|
||||
win = new BrowserWindow(baseOptions)
|
||||
}
|
||||
const win = new BrowserWindow(baseOptions)
|
||||
|
||||
const zoomSettings = this.mainCtx.settings
|
||||
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.on('blur', () => {
|
||||
if (input.key === 'main') {
|
||||
this.applyMicaEffect(win)
|
||||
}
|
||||
})
|
||||
|
||||
win.on('focus', () => {
|
||||
if (input.key === 'main') {
|
||||
this.applyMicaEffect(win)
|
||||
}
|
||||
})
|
||||
|
||||
win.webContents.setWindowOpenHandler((details) => {
|
||||
shell.openExternal(details.url)
|
||||
return { action: 'deny' }
|
||||
@@ -167,160 +116,6 @@ export class WindowManager extends Service {
|
||||
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) {
|
||||
const win = this.get(key)
|
||||
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 { useState, useEffect, createContext, useContext } from 'react'
|
||||
import { MobileHome } from './pages/Home'
|
||||
@@ -45,7 +45,7 @@ export const useApi = () => {
|
||||
function App(): React.JSX.Element {
|
||||
const [isDark, setIsDark] = useState(false)
|
||||
const [apiConfig, setApiConfig] = useState<ApiConfig>({ baseUrl: '' })
|
||||
const [messageApi, messageHolder] = message.useMessage()
|
||||
const [, messageHolder] = message.useMessage()
|
||||
|
||||
useEffect(() => {
|
||||
const savedTheme = localStorage.getItem('theme')
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
SettingOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { useTheme } from '../App'
|
||||
import { Switch } from 'antd'
|
||||
|
||||
const { Content } = Layout
|
||||
|
||||
@@ -21,7 +20,7 @@ const tabs = [
|
||||
export function MobileLayout(): React.JSX.Element {
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const { isDark, toggleTheme } = useTheme()
|
||||
const { isDark } = useTheme()
|
||||
|
||||
return (
|
||||
<Layout
|
||||
|
||||
@@ -82,7 +82,7 @@ export function MobileScore(): React.JSX.Element {
|
||||
setSelectedReason(null)
|
||||
setCustomScore('')
|
||||
setCustomReason('')
|
||||
} catch (e) {
|
||||
} catch {
|
||||
Toast.show({ content: '提交失败', icon: 'fail' })
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
|
||||
@@ -9,11 +9,6 @@ const api = {
|
||||
setTheme: (themeId: string) => ipcRenderer.invoke('theme:set', themeId),
|
||||
saveTheme: (theme: themeConfig) => ipcRenderer.invoke('theme:save', theme),
|
||||
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) => {
|
||||
const subscription = (_event: any, theme: themeConfig) => callback(theme)
|
||||
ipcRenderer.on('theme:updated', subscription)
|
||||
|
||||
+1
-13
@@ -11,11 +11,6 @@ export interface themeConfig {
|
||||
name: string
|
||||
id: string
|
||||
mode: 'light' | 'dark'
|
||||
mica?: {
|
||||
effect: 'mica' | 'tabbed' | 'acrylic' | 'blur' | 'transparent' | 'none'
|
||||
theme: 'auto' | 'dark' | 'light'
|
||||
radius: 'small' | 'medium' | 'large'
|
||||
}
|
||||
config: {
|
||||
tdesign: Record<string, string>
|
||||
custom: Record<string, string>
|
||||
@@ -26,9 +21,7 @@ export type settingsSpec = {
|
||||
is_wizard_completed: boolean
|
||||
log_level: logLevel
|
||||
window_zoom: number
|
||||
window_theme: 'auto' | 'dark' | 'light'
|
||||
window_effect: 'mica' | 'tabbed' | 'acrylic' | 'blur' | 'transparent' | 'none'
|
||||
window_radius: 'rounded' | 'small' | 'square'
|
||||
themes_custom: themeConfig[]
|
||||
auto_score_enabled: boolean
|
||||
auto_score_rules: any[]
|
||||
current_theme_id: string
|
||||
@@ -63,11 +56,6 @@ export interface electronApi {
|
||||
setTheme: (themeId: string) => Promise<ipcResponse<void>>
|
||||
saveTheme: (theme: themeConfig) => 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
|
||||
|
||||
// DB - Student
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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