diff --git a/package.json b/package.json index 525620a..6f3204f 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,6 @@ "mica-electron": "^1.5.16", "os": "^0.1.2", "pinyin-pro": "^3.27.0", - "prismjs": "^1.30.0", "react-router-dom": "^6.28.0", "reflect-metadata": "^0.2.2", "tdesign-react": "^1.16.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a5cd097..f70d654 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,9 +33,6 @@ importers: pinyin-pro: specifier: ^3.27.0 version: 3.27.0 - prismjs: - specifier: ^1.30.0 - version: 1.30.0 react-router-dom: specifier: ^6.28.0 version: 6.30.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -2636,10 +2633,6 @@ packages: engines: {node: '>=14'} hasBin: true - prismjs@1.30.0: - resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} - engines: {node: '>=6'} - proc-log@5.0.0: resolution: {integrity: sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==} engines: {node: ^18.17.0 || >=20.5.0} @@ -6179,8 +6172,6 @@ snapshots: prettier@3.7.4: {} - prismjs@1.30.0: {} - proc-log@5.0.0: {} progress@2.0.3: {} diff --git a/src/main/hosting/index.ts b/src/main/hosting/index.ts index 3529f80..9fc74c5 100644 --- a/src/main/hosting/index.ts +++ b/src/main/hosting/index.ts @@ -17,7 +17,6 @@ export { WindowManagerToken, TrayServiceToken, AutoScoreServiceToken, - HttpServerServiceToken, FileSystemServiceToken } from './tokens' export type { diff --git a/src/main/hosting/tokens.ts b/src/main/hosting/tokens.ts index 4b37ad8..4574e3e 100644 --- a/src/main/hosting/tokens.ts +++ b/src/main/hosting/tokens.ts @@ -15,5 +15,4 @@ export const ThemeServiceToken = Symbol.for('secscore.themeService') export const WindowManagerToken = Symbol.for('secscore.windowManager') export const TrayServiceToken = Symbol.for('secscore.trayService') export const AutoScoreServiceToken = Symbol.for('secscore.autoScoreService') -export const HttpServerServiceToken = Symbol.for('secscore.httpServerService') export const FileSystemServiceToken = Symbol.for('secscore.fileSystemService') diff --git a/src/main/index.ts b/src/main/index.ts index 51a54f2..d01d31d 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -13,7 +13,6 @@ import { PermissionService } from './services/PermissionService' import { AuthService } from './services/AuthService' import { DataService } from './services/DataService' import { ThemeService } from './services/ThemeService' -import { HttpServerService } from './services/HttpServerService' import { WindowManager, type windowManagerOptions } from './services/WindowManager' import { TrayService } from './services/TrayService' import { AutoScoreService } from './services/AutoScoreService' @@ -40,7 +39,6 @@ import { WindowManagerToken, TrayServiceToken, AutoScoreServiceToken, - HttpServerServiceToken, FileSystemServiceToken } from './hosting' @@ -274,10 +272,6 @@ app.whenReady().then(async () => { (p) => new FileSystemService(p.get(MainContext), config.configDir) ) services.addSingleton(AutoScoreServiceToken, (p) => new AutoScoreService(p.get(MainContext))) - services.addSingleton( - HttpServerServiceToken, - (p) => new HttpServerService(p.get(MainContext)) - ) }) .configure(async (_builderContext, appCtx) => { const services = appCtx.services @@ -303,7 +297,6 @@ app.whenReady().then(async () => { const tray = services.get(TrayServiceToken) as TrayService tray.initialize() } - services.get(HttpServerServiceToken) services.get(FileSystemServiceToken) const autoScore = services.get(AutoScoreServiceToken) as AutoScoreService await autoScore.initialize?.() diff --git a/src/main/services/AutoScoreService.ts b/src/main/services/AutoScoreService.ts index 5337f2c..bcc2a6f 100644 --- a/src/main/services/AutoScoreService.ts +++ b/src/main/services/AutoScoreService.ts @@ -619,6 +619,25 @@ export class AutoScoreService extends Service { return true } + async sortRules(ruleIds: number[]): Promise { + const ruleMap = new Map(this.rules.map((r) => [r.id, r])) + const sortedRules: AutoScoreRule[] = [] + for (const id of ruleIds) { + const rule = ruleMap.get(id) + if (rule) { + sortedRules.push(rule) + } + } + for (const rule of this.rules) { + if (!ruleIds.includes(rule.id)) { + sortedRules.push(rule) + } + } + this.rules = sortedRules + await this.saveRulesToFile() + return true + } + getRules(): AutoScoreRule[] { return [...this.rules] } diff --git a/src/main/services/HttpServerService.ts b/src/main/services/HttpServerService.ts deleted file mode 100644 index a30090f..0000000 --- a/src/main/services/HttpServerService.ts +++ /dev/null @@ -1,359 +0,0 @@ -import { Service } from '../../shared/kernel' -import { MainContext } from '../context' -import express, { type Express, type Request, type Response, type NextFunction } from 'express' -import { StudentEntity } from '../db/entities' -import net from 'net' - -declare module '../../shared/kernel' { - interface Context { - httpServer: HttpServerService - } -} - -export interface HttpServerConfig { - port: number - host: string - corsOrigin?: string -} - -export class HttpServerService extends Service { - private app: Express | null = null - private server: any = null - private config: HttpServerConfig = { - port: 3000, - host: 'localhost', - corsOrigin: '*' // 默认允许所有跨域请求 - } - - constructor(ctx: MainContext) { - super(ctx, 'httpServer') - this.registerIpc() - } - - private get mainCtx() { - return this.ctx as MainContext - } - - private get permissions() { - return this.ctx.permissions - } - - private registerIpc() { - // 启动HTTP服务器 - this.mainCtx.handle('http:server:start', async (event, config?: Partial) => { - if (!this.permissions.requirePermission(event, 'admin')) { - return { success: false, message: 'Permission denied' } - } - - try { - await this.start(config) - return { - success: true, - data: { - url: `http://${this.config.host}:${this.config.port}`, - config: this.config - } - } - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - return { success: false, message: `Failed to start HTTP server: ${message}` } - } - }) - - // 停止HTTP服务器 - this.mainCtx.handle('http:server:stop', async (event) => { - if (!this.permissions.requirePermission(event, 'admin')) { - return { success: false, message: 'Permission denied' } - } - - try { - await this.stop() - return { success: true } - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error' - return { success: false, message: `Failed to stop HTTP server: ${message}` } - } - }) - - // 获取HTTP服务器状态 - this.mainCtx.handle('http:server:status', async (event) => { - if (!this.permissions.requirePermission(event, 'admin')) { - return { success: false, message: 'Permission denied' } - } - - return { - success: true, - data: { - isRunning: this.isRunning(), - config: this.config, - url: this.isRunning() ? `http://${this.config.host}:${this.config.port}` : null - } - } - }) - } - - async start(config?: Partial): Promise { - if (this.isRunning()) { - throw new Error('HTTP server is already running') - } - - // 合并配置 - this.config = { ...this.config, ...config } - - // 检查端口是否可用,如果不可用则自动寻找可用端口 - const originalPort = this.config.port - let attempts = 0 - const maxAttempts = 100 // 最多尝试100个端口 - - while (attempts < maxAttempts) { - if (await this.isPortAvailable(this.config.port)) { - break - } - - // 端口被占用,尝试下一个端口 - this.config.port++ - attempts++ - } - - if (attempts === maxAttempts) { - throw new Error( - `Could not find available port after ${maxAttempts} attempts starting from ${originalPort}` - ) - } - - if (attempts > 0) { - this.mainCtx.logger.warn( - `Original port ${originalPort} was unavailable. Using port ${this.config.port} instead.` - ) - } - - // 创建Express应用 - this.app = express() - - // 配置中间件 - this.app.use(express.json()) - this.app.use(express.urlencoded({ extended: true })) - - // 简单的CORS处理 - this.app.use((_: Request, res: Response, next: NextFunction) => { - res.header('Access-Control-Allow-Origin', this.config.corsOrigin || '*') - res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept') - res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS') - next() - }) - - // 设置路由 - this.setupRoutes() - - // 启动服务器 - return new Promise((resolve, reject) => { - this.server = this.app!.listen(this.config.port, this.config.host, () => { - this.mainCtx.logger.info( - `HTTP server started at http://${this.config.host}:${this.config.port}` - ) - resolve() - }) - - this.server.on('error', (error: Error) => { - this.mainCtx.logger.error(`HTTP server error: ${error.message}`) - reject(error) - }) - }) - } - - async stop(): Promise { - if (!this.isRunning()) { - return - } - - return new Promise((resolve, reject) => { - this.server!.close((error?: Error) => { - if (error) { - this.mainCtx.logger.error(`Failed to stop HTTP server: ${error.message}`) - reject(error) - } else { - this.mainCtx.logger.info('HTTP server stopped') - this.server = null - this.app = null - resolve() - } - }) - }) - } - - isRunning(): boolean { - return this.server !== null && this.app !== null - } - - private async isPortAvailable(port: number): Promise { - return new Promise((resolve) => { - const server = net.createServer() - server.listen(port, '127.0.0.1') - server.on('error', () => { - resolve(false) - }) - server.on('listening', () => { - server.close(() => { - resolve(true) - }) - }) - }) - } - - private setupRoutes() { - if (!this.app) return - - // 健康检查端点 - this.app.get('/health', (_, res: Response) => { - res.json({ status: 'ok', timestamp: new Date().toISOString() }) - }) - - // 获取所有学生分数 - this.app.get('/api/students/scores', async (_req: Request, res: Response) => { - try { - const studentRepo = this.mainCtx.db.dataSource.getRepository(StudentEntity) - const students = await studentRepo.find({ - select: ['id', 'name', 'score'], - order: { score: 'DESC', name: 'ASC' } - }) - - res.json({ - success: true, - data: students.map((student) => ({ - id: student.id, - name: student.name, - score: student.score - })) - }) - } catch (error) { - this.mainCtx.logger.error(`Failed to fetch student scores: ${error}`) - res.status(500).json({ - success: false, - message: 'Failed to fetch student scores' - }) - } - }) - - // 获取单个学生分数 - this.app.get('/api/students/:id/score', async (_req: Request, res: Response) => { - try { - const id = parseInt(_req.params.id) - if (isNaN(id)) { - return res.status(400).json({ - success: false, - message: 'Invalid student ID' - }) - } - - const studentRepo = this.mainCtx.db.dataSource.getRepository(StudentEntity) - const student = await studentRepo.findOne({ - where: { id }, - select: ['id', 'name', 'score'] - }) - - if (!student) { - return res.status(404).json({ - success: false, - message: 'Student not found' - }) - } - - res.json({ - success: true, - data: { - id: student.id, - name: student.name, - score: student.score - } - }) - } catch (error) { - this.mainCtx.logger.error(`Failed to fetch student score: ${error}`) - res.status(500).json({ - success: false, - message: 'Failed to fetch student score' - }) - } - }) - - // 按姓名搜索学生分数 - this.app.get('/api/students/search', async (req: Request, res: Response) => { - try { - const name = req.query.name as string - if (!name || name.trim() === '') { - return res.status(400).json({ - success: false, - message: 'Name parameter is required' - }) - } - - const studentRepo = this.mainCtx.db.dataSource.getRepository(StudentEntity) - const students = await studentRepo.find({ - where: { name }, - select: ['id', 'name', 'score'] - }) - - res.json({ - success: true, - data: students.map((student) => ({ - id: student.id, - name: student.name, - score: student.score - })) - }) - } catch (error) { - this.mainCtx.logger.error(`Failed to search student: ${error}`) - res.status(500).json({ - success: false, - message: 'Failed to search student' - }) - } - }) - - // 获取排行榜(前N名学生) - this.app.get('/api/leaderboard', async (req: Request, res: Response) => { - try { - const limit = parseInt(req.query.limit as string) || 10 - const studentRepo = this.mainCtx.db.dataSource.getRepository(StudentEntity) - const students = await studentRepo.find({ - select: ['id', 'name', 'score'], - order: { score: 'DESC', name: 'ASC' }, - take: limit - }) - - res.json({ - success: true, - data: students.map((student) => ({ - id: student.id, - name: student.name, - score: student.score - })) - }) - } catch (error) { - this.mainCtx.logger.error(`Failed to fetch leaderboard: ${error}`) - res.status(500).json({ - success: false, - message: 'Failed to fetch leaderboard' - }) - } - }) - - // stray expression removed - // 404处理 - this.app.use((_req: Request, res: Response) => { - res.status(404).json({ - success: false, - message: 'Endpoint not found' - }) - }) - - // 错误处理中间件 - // eslint-disable-next-line @typescript-eslint/no-unused-vars - this.app.use((error: Error, _: Request, res: Response, _next: NextFunction) => { - this.mainCtx.logger.error(`HTTP server error: ${error.message}`) - res.status(500).json({ - success: false, - message: 'Internal server error' - }) - }) - } -} diff --git a/src/renderer/src/components/AutoScoreManager.tsx b/src/renderer/src/components/AutoScoreManager.tsx index 7113720..a8b00b3 100644 --- a/src/renderer/src/components/AutoScoreManager.tsx +++ b/src/renderer/src/components/AutoScoreManager.tsx @@ -18,7 +18,6 @@ import { Select, TooltipLite } from 'tdesign-react' -import Code from './Code' interface AutoScoreRule { id: number @@ -568,41 +567,6 @@ export const AutoScoreManager: React.FC = () => { style={{ color: 'var(--ss-text-main)' }} /> - - - { - if (editingRuleId !== null) { - const values = form.getFieldsValue(true) as unknown as AutoScoreRuleFormValues - const studentNames = Array.isArray(values.studentNames) ? values.studentNames : [] - const triggersPayload = triggerList.map((t) => ({ - event: t.eventName, - value: t.value, - relation: t.relation - })) - const actionsPayload = actionList.map((a) => ({ - event: a.eventName, - value: a.value, - reason: a.reason - })) - - const currentRule = { - id: editingRuleId, - enabled: true, - name: values.name || '', - studentNames, - triggers: triggersPayload, - actions: actionsPayload - } - - return JSON.stringify(currentRule, null, 2) - } else { - return JSON.stringify(rules, null, 2) - } - })()} - language={'json'} - /> - ) } diff --git a/src/renderer/src/components/Code.tsx b/src/renderer/src/components/Code.tsx deleted file mode 100644 index 555fea0..0000000 --- a/src/renderer/src/components/Code.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { useEffect } from 'react' - -import Prism from 'prismjs' -import { useTheme } from '../contexts/ThemeContext' - -import 'prismjs/themes/prism-okaidia.min.css' - -const Code = ({ code, language }) => { - const { currentTheme } = useTheme() - - useEffect(() => { - // 重新高亮代码 - Prism.highlightAll() - }, [currentTheme, code, language]) - - // 根据主题设置不同的类名 - const isDark = currentTheme?.mode === 'dark' - - return ( -
-
-        {code}
-      
-
- ) -} - -export default Code diff --git a/src/renderer/src/components/Settings.tsx b/src/renderer/src/components/Settings.tsx index c1c34d5..69d1d3b 100644 --- a/src/renderer/src/components/Settings.tsx +++ b/src/renderer/src/components/Settings.tsx @@ -62,18 +62,6 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission const [settleDialogVisible, setSettleDialogVisible] = useState(false) const [urlRegisterLoading, setUrlRegisterLoading] = useState(false) - - // HTTP Server状态 - const [httpServerStatus, setHttpServerStatus] = useState<{ - isRunning: boolean - config: { port: number; host: string; corsOrigin?: string } - url: string | null - } | null>(null) - const [httpLoading, setHttpLoading] = useState(false) - const [httpPort, setHttpPort] = useState('3000') - const [httpHost, setHttpHost] = useState('localhost') - const [httpCorsOrigin, setHttpCorsOrigin] = useState('*') - const canAdmin = permission === 'admin' const permissionTag = useMemo(() => { @@ -103,7 +91,6 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission useEffect(() => { loadAll() - refreshHttpServerStatus() if (!(window as any).api) return const unsubscribe = (window as any).api.onSettingChanged((change: any) => { setSettings((prev) => { @@ -261,71 +248,6 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission setSettleDialogVisible(true) } - // HTTP Server函数 - const refreshHttpServerStatus = async () => { - if (!(window as any).api) return - try { - const res = await (window as any).api.httpServerStatus() - if (res.success && res.data) { - setHttpServerStatus(res.data) - setHttpPort(res.data.config.port.toString()) - setHttpHost(res.data.config.host) - setHttpCorsOrigin(res.data.config.corsOrigin || '*') - } - } catch (error) { - console.error('Failed to refresh HTTP server status:', error) - } - } - - const handleHttpServerStart = async () => { - if (!(window as any).api) return - setHttpLoading(true) - try { - const config: any = {} - const portNum = parseInt(httpPort) - if (!isNaN(portNum) && portNum > 0 && portNum <= 65535) { - config.port = portNum - } - if (httpHost.trim()) { - config.host = httpHost.trim() - } - if (httpCorsOrigin.trim()) { - config.corsOrigin = httpCorsOrigin.trim() - } - - const res = await (window as any).api.httpServerStart(config) - if (res.success) { - MessagePlugin.success(`HTTP服务器已启动: ${res.data.url}`) - await refreshHttpServerStatus() - } else { - MessagePlugin.error(res.message || '启动失败') - } - } catch (error) { - MessagePlugin.error('启动HTTP服务器时发生错误') - console.error(error) - } finally { - setHttpLoading(false) - } - } - - const handleHttpServerStop = async () => { - if (!(window as any).api) return - setHttpLoading(true) - try { - const res = await (window as any).api.httpServerStop() - if (res.success) { - MessagePlugin.success('HTTP服务器已停止') - await refreshHttpServerStatus() - } else { - MessagePlugin.error(res.message || '停止失败') - } - } catch (error) { - MessagePlugin.error('停止HTTP服务器时发生错误') - console.error(error) - } finally { - setHttpLoading(false) - } - } const currentYear = new Date().getFullYear() return (
@@ -618,115 +540,6 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission - - -
- HTTP服务器 API -
- -
- 通过HTTP API提供学生分数查询服务,可在局域网内访问。 -
- -
- -
- - {httpServerStatus?.isRunning ? '运行中' : '已停止'} - - {httpServerStatus?.isRunning && ( -
- 访问地址: {httpServerStatus.url} -
- )} -
-
- - - setHttpPort(v)} - placeholder="端口号 (默认: 3000)" - style={{ width: '200px' }} - disabled={!canAdmin || httpLoading} - /> - - - - setHttpHost(v)} - placeholder="主机地址 (默认: localhost)" - style={{ width: '200px' }} - disabled={!canAdmin || httpLoading} - /> - - - - setHttpCorsOrigin(v)} - placeholder="CORS来源 (默认: *)" - style={{ width: '200px' }} - disabled={!canAdmin || httpLoading} - /> -
- * 表示允许所有来源,建议在生产环境中设置具体域名 -
-
- - - - - - - - - - -
-
• GET /health - 健康检查
-
• GET /api/students/scores - 获取所有学生分数
-
• GET /api/students/:id/score - 获取单个学生分数
-
• GET /api/students/search?name=姓名 - 按姓名搜索
-
• GET /api/leaderboard?limit=10 - 获取排行榜
-
-
-
-
-
-
diff --git a/src/renderer/src/services/AutoScoreService.ts b/src/renderer/src/services/AutoScoreService.ts index 2ccefd5..15deeb2 100644 --- a/src/renderer/src/services/AutoScoreService.ts +++ b/src/renderer/src/services/AutoScoreService.ts @@ -64,6 +64,12 @@ export class AutoScoreService extends Service { return await (window as any).api.invoke('auto-score:toggleRule', { ruleId, enabled }) } + async sortRules( + ruleIds: number[] + ): Promise<{ success: boolean; data?: boolean; message?: string }> { + return await (window as any).api.invoke('auto-score:sortRules', ruleIds) + } + async getStatus(): Promise<{ success: boolean; data?: { enabled: boolean }; message?: string }> { return await (window as any).api.invoke('auto-score:getStatus', {}) }