mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-21 11:49:02 +08:00
New: 加了个Http服务器 现在可以用接口获取分数了 //apidoc稍后补上(100%AI 无人工添加 事实上是主播不会写Service没找了哈哈)
This commit is contained in:
@@ -15,7 +15,8 @@ export {
|
||||
ThemeServiceToken,
|
||||
WindowManagerToken,
|
||||
TrayServiceToken,
|
||||
AutoScoreServiceToken
|
||||
AutoScoreServiceToken,
|
||||
HttpServerServiceToken
|
||||
} from './tokens'
|
||||
export type {
|
||||
appRuntimeContext,
|
||||
|
||||
@@ -14,3 +14,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')
|
||||
+8
-1
@@ -35,8 +35,10 @@ import {
|
||||
ThemeServiceToken,
|
||||
WindowManagerToken,
|
||||
TrayServiceToken,
|
||||
AutoScoreServiceToken
|
||||
AutoScoreServiceToken,
|
||||
HttpServerServiceToken
|
||||
} from './hosting'
|
||||
import { HttpServerService } from './services/HttpServerService'
|
||||
|
||||
type mainAppConfig = {
|
||||
isDev: boolean
|
||||
@@ -257,6 +259,10 @@ app.whenReady().then(async () => {
|
||||
AutoScoreServiceToken,
|
||||
(p) => new AutoScoreService(p.get(MainContext))
|
||||
)
|
||||
services.addSingleton(
|
||||
HttpServerServiceToken,
|
||||
(p) => new HttpServerService(p.get(MainContext))
|
||||
)
|
||||
})
|
||||
.configure(async (_builderContext, appCtx) => {
|
||||
const services = appCtx.services
|
||||
@@ -279,6 +285,7 @@ app.whenReady().then(async () => {
|
||||
tray.initialize()
|
||||
const autoScore = services.get(AutoScoreServiceToken) as AutoScoreService
|
||||
autoScore.initialize?.()
|
||||
services.get(HttpServerServiceToken)
|
||||
})
|
||||
.configure(async (_builderContext, appCtx) => {
|
||||
const services = appCtx.services
|
||||
|
||||
@@ -0,0 +1,355 @@
|
||||
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<HttpServerConfig>) => {
|
||||
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<HttpServerConfig>): Promise<void> {
|
||||
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<void> {
|
||||
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<boolean> {
|
||||
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'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 404处理
|
||||
this.app.use((_, res: Response) => {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: 'Endpoint not found'
|
||||
})
|
||||
})
|
||||
|
||||
// 错误处理中间件
|
||||
this.app.use((error: Error, _: Request, res: Response, __: NextFunction) => {
|
||||
this.mainCtx.logger.error(`HTTP server error: ${error.message}`)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Internal server error'
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -103,8 +103,14 @@ const api = {
|
||||
writeLog: (payload: { level: string; message: string; meta?: any }) =>
|
||||
ipcRenderer.invoke('log:write', payload),
|
||||
|
||||
registerUrlProtocol: () => ipcRenderer.invoke('app:register-url-protocol')
|
||||
,
|
||||
registerUrlProtocol: () => ipcRenderer.invoke('app:register-url-protocol'),
|
||||
|
||||
// HTTP Server
|
||||
httpServerStart: (config?: { port?: number; host?: string; corsOrigin?: string }) =>
|
||||
ipcRenderer.invoke('http:server:start', config),
|
||||
httpServerStop: () => ipcRenderer.invoke('http:server:stop'),
|
||||
httpServerStatus: () => ipcRenderer.invoke('http:server:status'),
|
||||
|
||||
// Generic invoke wrapper for backward compatibility with callers using `api.invoke`
|
||||
invoke: (channel: string, ...args: any[]) => ipcRenderer.invoke(channel, ...args)
|
||||
}
|
||||
|
||||
@@ -154,6 +154,20 @@ export interface electronApi {
|
||||
}) => Promise<ipcResponse<void>>
|
||||
|
||||
registerUrlProtocol: () => Promise<ipcResponse<{ registered?: boolean }>>
|
||||
|
||||
// HTTP Server
|
||||
httpServerStart: (config?: { port?: number; host?: string; corsOrigin?: string }) => Promise<
|
||||
ipcResponse<{ url: string; config: { port: number; host: string; corsOrigin?: string } }>
|
||||
>
|
||||
httpServerStop: () => Promise<ipcResponse<void>>
|
||||
httpServerStatus: () => Promise<
|
||||
ipcResponse<{
|
||||
isRunning: boolean
|
||||
config: { port: number; host: string; corsOrigin?: string }
|
||||
url: string | null
|
||||
}>
|
||||
>
|
||||
|
||||
// Generic invoke wrapper (minimal compatibility API)
|
||||
invoke?: (channel: string, ...args: any[]) => Promise<any>
|
||||
}
|
||||
|
||||
@@ -62,6 +62,17 @@ 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'
|
||||
|
||||
@@ -92,6 +103,7 @@ 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) => {
|
||||
@@ -247,6 +259,72 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
|
||||
if (!(window as any).api) return
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px', maxWidth: '900px', margin: '0 auto' }}>
|
||||
@@ -539,6 +617,114 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
|
||||
</Card>
|
||||
</Tabs.TabPanel>
|
||||
|
||||
<Tabs.TabPanel value="http" label="HTTP服务器">
|
||||
<Card style={{ backgroundColor: 'var(--ss-card-bg)', color: 'var(--ss-text-main)' }}>
|
||||
<div style={{ fontSize: '16px', fontWeight: 600, marginBottom: '8px' }}>
|
||||
HTTP服务器 API
|
||||
</div>
|
||||
<Divider />
|
||||
<div
|
||||
style={{
|
||||
marginBottom: '12px',
|
||||
fontSize: '13px',
|
||||
color: 'var(--ss-text-secondary)'
|
||||
}}
|
||||
>
|
||||
通过HTTP API提供学生分数查询服务,可在局域网内访问。
|
||||
</div>
|
||||
|
||||
<Form labelWidth={120}>
|
||||
<Form.FormItem label="服务器状态">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<Tag
|
||||
theme={httpServerStatus?.isRunning ? 'success' : 'default'}
|
||||
variant="light"
|
||||
>
|
||||
{httpServerStatus?.isRunning ? '运行中' : '已停止'}
|
||||
</Tag>
|
||||
{httpServerStatus?.isRunning && (
|
||||
<div style={{ fontSize: '12px', color: 'var(--ss-text-secondary)' }}>
|
||||
访问地址: {httpServerStatus.url}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Form.FormItem>
|
||||
|
||||
<Form.FormItem label="端口设置">
|
||||
<Input
|
||||
value={httpPort}
|
||||
onChange={(v) => setHttpPort(v)}
|
||||
placeholder="端口号 (默认: 3000)"
|
||||
style={{ width: '200px' }}
|
||||
disabled={!canAdmin || httpLoading}
|
||||
/>
|
||||
</Form.FormItem>
|
||||
|
||||
<Form.FormItem label="主机地址">
|
||||
<Input
|
||||
value={httpHost}
|
||||
onChange={(v) => setHttpHost(v)}
|
||||
placeholder="主机地址 (默认: localhost)"
|
||||
style={{ width: '200px' }}
|
||||
disabled={!canAdmin || httpLoading}
|
||||
/>
|
||||
</Form.FormItem>
|
||||
|
||||
<Form.FormItem label="CORS来源">
|
||||
<Input
|
||||
value={httpCorsOrigin}
|
||||
onChange={(v) => setHttpCorsOrigin(v)}
|
||||
placeholder="CORS来源 (默认: *)"
|
||||
style={{ width: '200px' }}
|
||||
disabled={!canAdmin || httpLoading}
|
||||
/>
|
||||
<div style={{ marginTop: '4px', fontSize: '12px', color: 'var(--ss-text-secondary)' }}>
|
||||
* 表示允许所有来源,建议在生产环境中设置具体域名
|
||||
</div>
|
||||
</Form.FormItem>
|
||||
|
||||
<Form.FormItem label="操作">
|
||||
<Space>
|
||||
<Button
|
||||
theme="primary"
|
||||
loading={httpLoading}
|
||||
disabled={!canAdmin}
|
||||
onClick={handleHttpServerStart}
|
||||
>
|
||||
{httpServerStatus?.isRunning ? '重启服务器' : '启动服务器'}
|
||||
</Button>
|
||||
<Button
|
||||
theme="danger"
|
||||
variant="outline"
|
||||
loading={httpLoading}
|
||||
disabled={!canAdmin || !httpServerStatus?.isRunning}
|
||||
onClick={handleHttpServerStop}
|
||||
>
|
||||
停止服务器
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={!canAdmin || httpLoading}
|
||||
onClick={refreshHttpServerStatus}
|
||||
>
|
||||
刷新状态
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.FormItem>
|
||||
|
||||
<Form.FormItem label="API端点">
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px', fontSize: '12px' }}>
|
||||
<div>• GET /health - 健康检查</div>
|
||||
<div>• GET /api/students/scores - 获取所有学生分数</div>
|
||||
<div>• GET /api/students/:id/score - 获取单个学生分数</div>
|
||||
<div>• GET /api/students/search?name=姓名 - 按姓名搜索</div>
|
||||
<div>• GET /api/leaderboard?limit=10 - 获取排行榜</div>
|
||||
</div>
|
||||
</Form.FormItem>
|
||||
</Form>
|
||||
</Card>
|
||||
</Tabs.TabPanel>
|
||||
|
||||
<Tabs.TabPanel value="url" label="URL 链接">
|
||||
<Card style={{ backgroundColor: 'var(--ss-card-bg)', color: 'var(--ss-text-main)' }}>
|
||||
<div style={{ fontSize: '16px', fontWeight: 600, marginBottom: '8px' }}>
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
RootListIcon,
|
||||
ViewListIcon,
|
||||
HomeIcon,
|
||||
MoneyIcon
|
||||
ReplayIcon
|
||||
} from 'tdesign-icons-react'
|
||||
import appLogo from '../assets/logoHD.svg'
|
||||
|
||||
@@ -78,7 +78,7 @@ export function Sidebar({ activeMenu, permission, onMenuChange }: SidebarProps):
|
||||
</Menu.MenuItem>
|
||||
<Menu.MenuItem value="leaderboard" icon={<ViewListIcon />}> 排行榜
|
||||
</Menu.MenuItem>
|
||||
<Menu.MenuItem value="auto-score" icon={<MoneyIcon />}> 自动加分
|
||||
<Menu.MenuItem value="auto-score" icon={<ReplayIcon />}> 自动加分
|
||||
</Menu.MenuItem>
|
||||
<Menu.MenuItem value="settlements" icon={<HistoryIcon />}> 结算历史
|
||||
</Menu.MenuItem>
|
||||
|
||||
Reference in New Issue
Block a user