mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-19 19:04:23 +08:00
feat(主题): 添加主题编辑器功能并更新相关UI
- 新增主题编辑器组件和上下文管理 - 实现主题的保存、删除和自定义功能 - 更新主题相关API接口 - 调整主题样式透明度 - 替换应用logo为高清版本 - 更新README和package.json描述信息
This commit is contained in:
@@ -1,6 +1,23 @@
|
||||
# SecScore
|
||||
|
||||
一款本地离线(未来加入远程模式)的“教育积分管理”桌面软件(Electron + React + TypeScript),用于管理学生名单、记录加/扣分、查看排行榜与结算历史,并提供基础的权限保护与数据备份。
|
||||
<!-- GitHub 徽章 -->
|
||||
<p align="center">
|
||||
<a href="https://github.com/SECTL/SecScore/releases"><img alt="GitHub release (latest by date)" src="https://img.shields.io/github/v/release/SECTL/SecScore?style=flat-square"></a>
|
||||
<a href="https://github.com/SECTL/SecScore/actions/workflows/ci.yml"><img alt="CI" src="https://img.shields.io/github/actions/workflow/status/SECTL/SecScore/ci.yml?style=flat-square&logo=github&label=CI"></a>
|
||||
<a href="https://github.com/SECTL/SecScore/releases"><img alt="Downloads" src="https://img.shields.io/github/downloads/SECTL/SecScore/total?style=flat-square"></a>
|
||||
<a href="https://github.com/SECTL/SecScore/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/github/license/SECTL/SecScore?style=flat-square"></a>
|
||||
<a href="https://github.com/SECTL/SecScore/issues"><img alt="GitHub issues" src="https://img.shields.io/github/issues/SECTL/SecScore?style=flat-square"></a>
|
||||
<a href="https://github.com/SECTL/SecScore/pulls"><img alt="GitHub pull requests" src="https://img.shields.io/github/issues-pr/SECTL/SecScore?style=flat-square"></a>
|
||||
<img alt="Electron" src="https://img.shields.io/badge/Electron-39+-47848F?style=flat-square&logo=electron">
|
||||
<img alt="React" src="https://img.shields.io/badge/React-19+-61DAFB?style=flat-square&logo=react">
|
||||
<img alt="TypeScript" src="https://img.shields.io/badge/TypeScript-5+-3178C6?style=flat-square&logo=typescript">
|
||||
<img alt="Vite" src="https://img.shields.io/badge/Vite-7+-646CFF?style=flat-square&logo=vite">
|
||||
</p>
|
||||
|
||||
SecScore 是一款教育积分管理软件,基于 Electron + React + TypeScript 开发,用于管理学生名单、记录加/扣分、查看排行榜与结算历史,并提供权限保护与数据备份。
|
||||
|
||||
|
||||
SecScore 是一款教育积分管理软件,基于 Electron + React + TypeScript 开发,用于管理学生名单、记录加/扣分、查看排行榜与结算历史,并提供权限保护与数据备份。
|
||||
|
||||
## 主要功能
|
||||
|
||||
|
||||
+4
-3
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "secscore",
|
||||
"version": "1.0.0",
|
||||
"description": "An Electron application with React and TypeScript",
|
||||
"description": "SecScore – Your Education Points Management Expert",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "example.com",
|
||||
"homepage": "https://electron-vite.org",
|
||||
"homepage": "https://example.org",
|
||||
"scripts": {
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint --cache .",
|
||||
@@ -35,7 +35,8 @@
|
||||
"uuid": "^13.0.0",
|
||||
"winston": "^3.19.0",
|
||||
"winston-daily-rotate-file": "^5.0.0",
|
||||
"xlsx": "^0.18.5"
|
||||
"xlsx": "^0.18.5",
|
||||
"pnpm": "^8.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
|
||||
|
||||
@@ -13,19 +13,27 @@ const rawMessage =
|
||||
''
|
||||
const message = String(rawMessage || '').trim()
|
||||
|
||||
// 是否以 release: 开头
|
||||
const isReleasePrefix = /^release:/i.test(message)
|
||||
|
||||
const semverRe = /\bv?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)\b/
|
||||
const versionMatch = message.match(semverRe)
|
||||
const version = versionMatch ? versionMatch[1] : ''
|
||||
|
||||
const buildToken =
|
||||
message.match(/\b(build:(?:win|mac|linux|unpack|all))\b/i)?.[1]?.toLowerCase() ?? ''
|
||||
const buildScript =
|
||||
let buildScript =
|
||||
buildToken === 'build:all'
|
||||
? 'build:all'
|
||||
: buildToken && buildToken.startsWith('build:')
|
||||
? buildToken
|
||||
: ''
|
||||
|
||||
// 如果是 release: 开头且没有显式指定构建脚本,默认为 build:all
|
||||
if (isReleasePrefix && !buildScript) {
|
||||
buildScript = 'build:all'
|
||||
}
|
||||
|
||||
const supportedBuildScripts = new Set(['build:win', 'build:mac', 'build:linux', 'build:unpack'])
|
||||
const hasBuildScript = buildScript
|
||||
? buildScript === 'build:all'
|
||||
@@ -38,7 +46,7 @@ const fail = (text) => {
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const hasAnySignal = Boolean(version || buildScript)
|
||||
const hasAnySignal = Boolean(isReleasePrefix || version || buildScript)
|
||||
const shouldSkip = !hasAnySignal
|
||||
|
||||
if (!shouldSkip) {
|
||||
@@ -46,7 +54,7 @@ if (!shouldSkip) {
|
||||
fail(
|
||||
[
|
||||
'未在提交信息中检测到版本号。',
|
||||
'示例:release: v1.2.3 build:win',
|
||||
'示例:release: v1.2.3 build:win 或 release: 1.2.3',
|
||||
'支持格式:v1.2.3 或 1.2.3(可带 -beta.1 等后缀)'
|
||||
].join('\n')
|
||||
)
|
||||
|
||||
@@ -12,6 +12,10 @@ for (const name of targets) {
|
||||
console.log(`[clean-db] removed ${filePath}`)
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.code === 'EPERM' || e.code === 'EACCES') {
|
||||
console.warn(`[clean-db] skipped ${filePath}: file in use or permission denied`)
|
||||
} else {
|
||||
console.error(`[clean-db] failed to remove ${filePath}:`, e?.message || e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +91,8 @@ export class SettingsService extends Service {
|
||||
kind: 'string',
|
||||
defaultValue: 'mica',
|
||||
writePermission: 'admin',
|
||||
validate: (v) => ['mica', 'tabbed', 'acrylic', 'blur', 'transparent', 'none'].includes(v as string)
|
||||
validate: (v) =>
|
||||
['mica', 'tabbed', 'acrylic', 'blur', 'transparent', 'none'].includes(v as string)
|
||||
},
|
||||
window_radius: {
|
||||
kind: 'string',
|
||||
|
||||
@@ -88,11 +88,53 @@ export class ThemeService extends Service {
|
||||
return { success: true }
|
||||
})
|
||||
|
||||
this.mainCtx.handle('theme:set-custom', async (event, config: {
|
||||
this.mainCtx.handle('theme:save', async (event, theme: themeConfig) => {
|
||||
if (!this.mainCtx.permissions.requirePermission(event, 'admin'))
|
||||
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')
|
||||
this.notifyThemeUpdate()
|
||||
return { success: true }
|
||||
} catch (e) {
|
||||
return { success: false, message: String(e) }
|
||||
}
|
||||
})
|
||||
|
||||
this.mainCtx.handle('theme:delete', async (event, themeId: string) => {
|
||||
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)
|
||||
}
|
||||
if (this.currentThemeId === themeId) {
|
||||
this.currentThemeId = 'light' // Fallback
|
||||
}
|
||||
this.notifyThemeUpdate()
|
||||
return { success: true }
|
||||
} catch (e) {
|
||||
return { success: false, message: String(e) }
|
||||
}
|
||||
})
|
||||
|
||||
this.mainCtx.handle(
|
||||
'theme:set-custom',
|
||||
async (
|
||||
event,
|
||||
config: {
|
||||
effect: 'mica' | 'tabbed' | 'acrylic' | 'blur' | 'transparent' | 'none'
|
||||
theme: 'auto' | 'dark' | 'light'
|
||||
radius: 'small' | 'medium' | 'large'
|
||||
}) => {
|
||||
}
|
||||
) => {
|
||||
if (!this.mainCtx.permissions.requirePermission(event, 'admin'))
|
||||
return { success: false, message: 'Permission denied' }
|
||||
|
||||
@@ -100,7 +142,8 @@ export class ThemeService extends Service {
|
||||
this.applyMicaConfig(config)
|
||||
this.notifyThemeUpdate()
|
||||
return { success: true }
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private applyMicaEffect(themeId: string) {
|
||||
@@ -116,9 +159,9 @@ export class ThemeService extends Service {
|
||||
radius: 'small' | 'medium' | 'large'
|
||||
}) {
|
||||
const radiusMap: Record<string, 'rounded' | 'small' | 'square'> = {
|
||||
'small': 'small',
|
||||
'medium': 'rounded',
|
||||
'large': 'rounded'
|
||||
small: 'small',
|
||||
medium: 'rounded',
|
||||
large: 'rounded'
|
||||
}
|
||||
|
||||
const windows = BrowserWindow.getAllWindows()
|
||||
|
||||
@@ -7,6 +7,8 @@ const api = {
|
||||
getThemes: () => ipcRenderer.invoke('theme:list'),
|
||||
getCurrentTheme: () => ipcRenderer.invoke('theme:current'),
|
||||
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'
|
||||
|
||||
@@ -43,6 +43,8 @@ export interface electronApi {
|
||||
getThemes: () => Promise<ipcResponse<themeConfig[]>>
|
||||
getCurrentTheme: () => Promise<ipcResponse<themeConfig>>
|
||||
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'
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M347.599 422.773C398.361 422.773 439.511 381.622 439.511 330.861C439.511 280.099 398.361 238.949 347.599 238.949C296.838 238.949 255.687 280.099 255.687 330.861C255.687 381.622 296.838 422.773 347.599 422.773Z" fill="#FFE55C"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M364.35 306.249C363.022 303.941 360.478 299.332 358.422 295.213C356.367 291.096 354.798 287.469 352.986 283.485C351.174 279.501 349.119 275.158 346.49 277.166C343.86 279.172 340.656 287.525 337.978 293.814C335.301 300.103 333.149 304.326 332.073 306.438C330.997 308.549 330.997 308.549 330.997 308.549C330.997 308.549 330.997 308.549 327.792 309.017C324.588 309.484 318.179 310.418 311.109 311.34C304.039 312.262 296.309 313.17 295.326 315.969C294.344 318.766 300.11 323.456 305.399 327.79C310.689 332.125 315.502 336.105 317.908 338.095C320.315 340.086 320.315 340.086 320.315 340.086C320.315 340.086 320.315 340.086 319.888 342.737C319.46 345.389 318.605 350.692 317.231 357.502C315.857 364.311 313.965 372.625 316.005 375.086C318.044 377.546 324.016 374.152 330.051 370.591C336.086 367.031 342.184 363.304 345.232 361.44C348.281 359.576 348.281 359.576 348.281 359.576C348.281 359.576 348.281 359.576 350.867 361.151C353.452 362.725 358.623 365.875 364.247 369.335C369.87 372.793 375.947 376.56 378.408 375.041C380.87 373.52 379.718 366.71 378.755 360.003C377.793 353.296 377.02 346.69 376.633 343.388C376.246 340.086 376.246 340.086 381.854 335.636C387.46 331.188 398.673 322.291 399.072 317.226C399.471 312.162 389.055 310.932 383.661 310.294C378.268 309.656 377.896 309.611 376.582 309.493C375.267 309.374 373.009 309.183 371.073 309.019C369.137 308.853 367.524 308.716 366.659 308.643C365.795 308.57 365.68 308.559 364.35 306.249Z" fill="#FFF8A0"/>
|
||||
<path d="M306.099 90C351.526 90 388.353 126.826 388.354 172.254C388.354 217.682 351.526 254.509 306.099 254.509C290.933 254.509 276.727 250.403 264.529 243.245C282.109 224.549 292.857 199.517 292.857 172.01C292.857 144.657 282.229 119.753 264.825 101.091C276.957 94.0394 291.056 90.0001 306.099 90Z" fill="#5BA3F5"/>
|
||||
<path d="M185.101 255.017C230.453 255.017 267.217 218.252 267.217 172.901C267.217 127.55 230.453 90.7855 185.101 90.7855C139.75 90.7855 102.986 127.55 102.986 172.901C102.986 218.252 139.75 255.017 185.101 255.017Z" fill="#8BC4FA"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M244.159 278.368C243.114 280.54 241.051 284.886 239.302 289.379C237.553 293.872 236.117 298.513 234.576 305.948C233.034 313.382 231.387 323.612 231.977 334.181C232.566 344.749 235.393 355.657 237.846 363.371C240.3 371.086 242.38 375.605 243.42 377.866C244.461 380.125 244.461 380.125 244.461 380.125C244.461 380.125 244.461 380.125 219.735 380.125C195.01 380.125 145.56 380.125 120.835 380.125C96.1108 380.125 96.1108 380.125 94.9076 380.146C93.7044 380.166 91.2979 380.207 88.2191 379.441C85.1413 378.674 81.3911 377.101 78.6929 373.666C75.9957 370.229 74.3505 364.932 73.599 361.471C72.8467 358.01 72.9881 356.385 73.0592 354.158C73.1295 351.93 73.1295 349.098 73.1295 347.005C73.1295 344.912 73.1295 343.557 73.2629 340.858C73.3962 338.159 73.663 334.115 74.0935 330.854C74.5239 327.592 75.1179 325.114 78.0215 318.961C80.926 312.809 86.14 302.982 93.2926 295.779C100.447 288.575 109.539 283.996 117.658 281.169C125.776 278.343 132.921 277.269 137.108 276.732C141.295 276.195 142.527 276.195 143.697 276.195C144.866 276.195 145.974 276.195 162.632 276.195C179.289 276.195 211.496 276.195 227.628 276.195C243.76 276.195 243.817 276.195 243.876 276.195C243.936 276.195 243.998 276.195 244.147 276.195C244.296 276.195 244.53 276.195 244.708 276.195C244.887 276.195 245.008 276.195 245.062 276.195C245.117 276.195 245.105 276.195 245.126 276.195C245.147 276.195 245.202 276.195 245.217 276.195C245.231 276.195 245.204 276.195 244.159 278.368Z" fill="#5BA3F5" stroke="#7BB5F8" stroke-width="0.755906" stroke-miterlimit="22.9256"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
@@ -0,0 +1,7 @@
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M347.599 422.773C398.361 422.773 439.511 381.622 439.511 330.861C439.511 280.099 398.361 238.949 347.599 238.949C296.838 238.949 255.687 280.099 255.687 330.861C255.687 381.622 296.838 422.773 347.599 422.773Z" fill="#FFDA31"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M364.35 306.249C363.022 303.941 360.478 299.332 358.422 295.213C356.367 291.096 354.798 287.469 352.986 283.485C351.174 279.501 349.119 275.158 346.49 277.166C343.86 279.172 340.656 287.525 337.978 293.814C335.301 300.103 333.149 304.326 332.073 306.438C330.997 308.549 330.997 308.549 330.997 308.549C330.997 308.549 330.997 308.549 327.792 309.017C324.588 309.484 318.179 310.418 311.109 311.34C304.039 312.262 296.309 313.17 295.326 315.969C294.344 318.766 300.11 323.456 305.399 327.79C310.689 332.125 315.502 336.105 317.908 338.095C320.315 340.086 320.315 340.086 320.315 340.086C320.315 340.086 320.315 340.086 319.888 342.737C319.46 345.389 318.605 350.692 317.231 357.502C315.857 364.311 313.965 372.625 316.005 375.086C318.044 377.546 324.016 374.152 330.051 370.591C336.086 367.031 342.184 363.304 345.232 361.44C348.281 359.576 348.281 359.576 348.281 359.576C348.281 359.576 348.281 359.576 350.867 361.151C353.452 362.725 358.623 365.875 364.247 369.335C369.87 372.793 375.947 376.56 378.408 375.041C380.87 373.52 379.718 366.71 378.755 360.003C377.793 353.296 377.02 346.69 376.633 343.388C376.246 340.086 376.246 340.086 381.854 335.636C387.46 331.188 398.673 322.291 399.072 317.226C399.471 312.162 389.055 310.932 383.661 310.294C378.268 309.656 377.896 309.611 376.582 309.493C375.267 309.374 373.009 309.183 371.073 309.019C369.137 308.853 367.524 308.716 366.659 308.643C365.795 308.57 365.68 308.559 364.35 306.249Z" fill="#FFF270"/>
|
||||
<path d="M306.099 90C351.526 90 388.353 126.826 388.354 172.254C388.354 217.682 351.526 254.509 306.099 254.509C290.933 254.509 276.727 250.403 264.529 243.245C282.109 224.549 292.857 199.517 292.857 172.01C292.857 144.657 282.229 119.753 264.825 101.091C276.957 94.0394 291.056 90.0001 306.099 90Z" fill="#2389EC"/>
|
||||
<path d="M185.101 255.017C230.453 255.017 267.217 218.252 267.217 172.901C267.217 127.55 230.453 90.7855 185.101 90.7855C139.75 90.7855 102.986 127.55 102.986 172.901C102.986 218.252 139.75 255.017 185.101 255.017Z" fill="#75BDF7"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M244.159 278.368C243.114 280.54 241.051 284.886 239.302 289.379C237.553 293.872 236.117 298.513 234.576 305.948C233.034 313.382 231.387 323.612 231.977 334.181C232.566 344.749 235.393 355.657 237.846 363.371C240.3 371.086 242.38 375.605 243.42 377.866C244.461 380.125 244.461 380.125 244.461 380.125C244.461 380.125 244.461 380.125 219.735 380.125C195.01 380.125 145.56 380.125 120.835 380.125C96.1108 380.125 96.1108 380.125 94.9076 380.146C93.7044 380.166 91.2979 380.207 88.2191 379.441C85.1413 378.674 81.3911 377.101 78.6929 373.666C75.9957 370.229 74.3505 364.932 73.599 361.471C72.8467 358.01 72.9881 356.385 73.0592 354.158C73.1295 351.93 73.1295 349.098 73.1295 347.005C73.1295 344.912 73.1295 343.557 73.2629 340.858C73.3962 338.159 73.663 334.115 74.0935 330.854C74.5239 327.592 75.1179 325.114 78.0215 318.961C80.926 312.809 86.14 302.982 93.2926 295.779C100.447 288.575 109.539 283.996 117.658 281.169C125.776 278.343 132.921 277.269 137.108 276.732C141.295 276.195 142.527 276.195 143.697 276.195C144.866 276.195 145.974 276.195 162.632 276.195C179.289 276.195 211.496 276.195 227.628 276.195C243.76 276.195 243.817 276.195 243.876 276.195C243.936 276.195 243.998 276.195 244.147 276.195C244.296 276.195 244.53 276.195 244.708 276.195C244.887 276.195 245.008 276.195 245.062 276.195C245.117 276.195 245.105 276.195 245.126 276.195C245.147 276.195 245.202 276.195 245.217 276.195C245.231 276.195 245.204 276.195 244.159 278.368Z" fill="#2389EC" stroke="#2789ED" stroke-width="0.755906" stroke-miterlimit="22.9256"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
@@ -2,6 +2,7 @@ import React, { Suspense, lazy } from 'react'
|
||||
import { Layout, Space, Button, Tag, Loading } from 'tdesign-react'
|
||||
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(() =>
|
||||
@@ -125,6 +126,7 @@ export function ContentArea({
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</Content>
|
||||
<ThemeEditor />
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -55,7 +55,6 @@ export const GlobalSidebar: React.FC = () => {
|
||||
// 1. 先隐藏三角
|
||||
setShowToggle(false)
|
||||
|
||||
|
||||
// 2. 稍后扩大窗口
|
||||
setTimeout(() => {
|
||||
if ((window as any).api) {
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
ViewListIcon,
|
||||
HomeIcon
|
||||
} from 'tdesign-icons-react'
|
||||
import appLogo from '../assets/logo.svg'
|
||||
import appLogo from '../assets/logoHD.svg'
|
||||
|
||||
const { Aside } = Layout
|
||||
|
||||
|
||||
@@ -135,16 +135,16 @@ export const ThemeEditor: React.FC = () => {
|
||||
<Divider align="left">基本信息</Divider>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={12}>
|
||||
<Form.Item label="主题名称">
|
||||
<Form.FormItem label="主题名称">
|
||||
<Input
|
||||
value={editingTheme.name}
|
||||
onChange={(v) => updateEditingTheme({ name: v })}
|
||||
placeholder="请输入主题名称"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form.FormItem>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item label="色彩模式">
|
||||
<Form.FormItem label="色彩模式">
|
||||
<Select
|
||||
value={editingTheme.mode}
|
||||
onChange={(v) => updateEditingTheme({ mode: v as 'light' | 'dark' })}
|
||||
@@ -153,16 +153,16 @@ export const ThemeEditor: React.FC = () => {
|
||||
{ label: '深色 (Dark)', value: 'dark' }
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form.FormItem>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item label="主题 ID (唯一标识)" help="建议使用英文,如 my-theme">
|
||||
<Form.FormItem label="主题 ID (唯一标识)" help="建议使用英文,如 my-theme">
|
||||
<Input
|
||||
value={editingTheme.id}
|
||||
onChange={(v) => updateEditingTheme({ id: v })}
|
||||
placeholder="请输入主题 ID"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form.FormItem>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
@@ -170,14 +170,14 @@ export const ThemeEditor: React.FC = () => {
|
||||
{/* TDesign 品牌色 */}
|
||||
<div>
|
||||
<Divider align="left">品牌色 (Brand)</Divider>
|
||||
<Form.Item label="主品牌色" help="将自动生成一系列色阶">
|
||||
<Form.FormItem label="主品牌色" help="将自动生成一系列色阶">
|
||||
<ColorPicker
|
||||
value={editingTheme.config.tdesign.brandColor}
|
||||
onChange={(v) => updateConfig('tdesign', 'brandColor', v)}
|
||||
enableAlpha={false}
|
||||
format="HEX"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form.FormItem>
|
||||
</div>
|
||||
|
||||
{/* 业务自定义变量 */}
|
||||
@@ -216,7 +216,6 @@ export const ThemeEditor: React.FC = () => {
|
||||
onChange={(v) => updateConfig('custom', item.key, v)}
|
||||
enableAlpha
|
||||
format="HEX"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import React, { createContext, useContext, useState, useCallback } from 'react'
|
||||
import { themeConfig } from '../../../preload/types'
|
||||
import { useTheme } from './ThemeContext'
|
||||
|
||||
interface ThemeEditorContextType {
|
||||
isEditing: boolean
|
||||
editingTheme: themeConfig | null
|
||||
startEditing: (theme?: themeConfig) => void
|
||||
updateEditingTheme: (updates: Partial<themeConfig>) => void
|
||||
updateConfig: (type: 'tdesign' | 'custom', key: string, value: string) => void
|
||||
saveEditingTheme: () => Promise<void>
|
||||
cancelEditing: () => void
|
||||
}
|
||||
|
||||
const ThemeEditorContext = createContext<ThemeEditorContextType | undefined>(undefined)
|
||||
|
||||
export const ThemeEditorProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const { currentTheme } = useTheme()
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [editingTheme, setEditingTheme] = useState<themeConfig | null>(null)
|
||||
|
||||
const startEditing = useCallback(
|
||||
(theme?: themeConfig) => {
|
||||
if (theme) {
|
||||
setEditingTheme(JSON.parse(JSON.stringify(theme)))
|
||||
} else if (currentTheme) {
|
||||
const newTheme: themeConfig = JSON.parse(JSON.stringify(currentTheme))
|
||||
newTheme.id = `custom-${Date.now()}`
|
||||
newTheme.name = '新自定义主题'
|
||||
setEditingTheme(newTheme)
|
||||
}
|
||||
setIsEditing(true)
|
||||
},
|
||||
[currentTheme]
|
||||
)
|
||||
|
||||
const updateEditingTheme = useCallback((updates: Partial<themeConfig>) => {
|
||||
setEditingTheme((prev) => (prev ? { ...prev, ...updates } : null))
|
||||
}, [])
|
||||
|
||||
const updateConfig = useCallback((type: 'tdesign' | 'custom', key: string, value: string) => {
|
||||
setEditingTheme((prev) => {
|
||||
if (!prev) return null
|
||||
const next = { ...prev }
|
||||
next.config = { ...next.config }
|
||||
next.config[type] = { ...next.config[type], [key]: value }
|
||||
return next
|
||||
})
|
||||
}, [])
|
||||
|
||||
const saveEditingTheme = useCallback(async () => {
|
||||
if (!editingTheme) return
|
||||
const res = await (window as any).api.saveTheme(editingTheme)
|
||||
if (res.success) {
|
||||
await (window as any).api.setTheme(editingTheme.id)
|
||||
setIsEditing(false)
|
||||
setEditingTheme(null)
|
||||
} else {
|
||||
console.error('Failed to save theme:', res.message)
|
||||
}
|
||||
}, [editingTheme])
|
||||
|
||||
const cancelEditing = useCallback(() => {
|
||||
setIsEditing(false)
|
||||
setEditingTheme(null)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<ThemeEditorContext.Provider
|
||||
value={{
|
||||
isEditing,
|
||||
editingTheme,
|
||||
startEditing,
|
||||
updateEditingTheme,
|
||||
updateConfig,
|
||||
saveEditingTheme,
|
||||
cancelEditing
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ThemeEditorContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useThemeEditor = () => {
|
||||
const context = useContext(ThemeEditorContext)
|
||||
if (!context) throw new Error('useThemeEditor must be used within ThemeEditorProvider')
|
||||
return context
|
||||
}
|
||||
@@ -16,12 +16,12 @@
|
||||
},
|
||||
"custom": {
|
||||
"--ss-bg-color": "transparent",
|
||||
"--ss-card-bg": "rgba(255, 255, 255, 0.85)",
|
||||
"--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.9)",
|
||||
"--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)",
|
||||
|
||||
@@ -16,12 +16,12 @@
|
||||
},
|
||||
"custom": {
|
||||
"--ss-bg-color": "transparent",
|
||||
"--ss-card-bg": "rgba(255, 255, 255, 0.7)",
|
||||
"--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.85)",
|
||||
"--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)",
|
||||
|
||||
@@ -16,12 +16,12 @@
|
||||
},
|
||||
"custom": {
|
||||
"--ss-bg-color": "transparent",
|
||||
"--ss-card-bg": "rgba(30, 30, 30, 0.7)",
|
||||
"--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.85)",
|
||||
"--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)",
|
||||
|
||||
@@ -16,12 +16,12 @@
|
||||
},
|
||||
"custom": {
|
||||
"--ss-bg-color": "transparent",
|
||||
"--ss-card-bg": "rgba(255, 255, 255, 0.7)",
|
||||
"--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.85)",
|
||||
"--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)",
|
||||
|
||||
Reference in New Issue
Block a user