请输入文本:文本

This commit is contained in:
Fox_block
2026-03-01 21:27:24 +08:00
parent 38240a42ab
commit 7b5fd3a067
34 changed files with 600 additions and 877 deletions
+1 -9
View File
@@ -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)
+1 -3
View File
@@ -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)
+4 -18
View File
@@ -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
View File
@@ -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 {
+4 -237
View File
@@ -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
View File
@@ -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')
+1 -2
View File
@@ -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
+1 -1
View File
@@ -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)
-5
View File
@@ -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
View File
@@ -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
+7 -11
View File
@@ -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>
)
}
+5 -5
View File
@@ -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) {
+16 -13
View File
@@ -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>
)
}
+1 -1
View File
@@ -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
+15 -23
View File
@@ -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>
+1 -1
View File
@@ -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'
+28 -8
View File
@@ -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>
)
}
+3 -12
View File
@@ -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>
)
}
+43 -9
View File
@@ -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>
)
-31
View File
@@ -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"
}
}
}
-31
View File
@@ -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"
}
}
}
-31
View File
@@ -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"
}
}
}
-25
View File
@@ -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"
}
}
}
-25
View File
@@ -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"
}
}
}
-26
View File
@@ -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"
}
}
}
-25
View File
@@ -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"
}
}
}
-25
View File
@@ -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"
}
}
}
-31
View File
@@ -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"
}
}
}
-31
View File
@@ -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"
}
}
}
-31
View File
@@ -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"
}
}
}
-26
View File
@@ -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"
}
}
}
-25
View File
@@ -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"
}
}
}
-25
View File
@@ -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"
}
}
}