Add exam score import and management workflows

This commit is contained in:
biss authored and biss committed 2026-10-03 21:38:14 +08:00
1 parent 71ee21b063
commit 24d2a96144
26 files changed
+3865

No files matched your search

+11
View File
@@ -0,0 +1,11 @@
interface BrandMarkProps {
large?: boolean
}
export default function BrandMark({ large = false }: BrandMarkProps) {
return (
<span className={`brand-mark${large ? ' brand-mark--large' : ''}`} aria-hidden="true">
{Array.from({ length: 9 }, (_, index) => <span key={index} />)}
</span>
)
}
+107
View File
@@ -0,0 +1,107 @@
import { useCallback, useEffect, useState } from 'react'
import { Alert, Button, Input, Skeleton } from 'antd'
import http from '../api/http'
interface CaptchaChoice {
id: string
label: string
}
interface CaptchaChallenge {
captchaId: string
type: 'TextImage' | 'MathImage' | 'Selection' | 'OneClick'
prompt?: string
choices?: CaptchaChoice[]
imageData?: string
}
interface LoginCaptchaProps {
refreshKey: number
onChange: (captchaId: string, code: string) => void
}
export default function LoginCaptcha({ refreshKey, onChange }: LoginCaptchaProps) {
const [captcha, setCaptcha] = useState<CaptchaChallenge | null>(null)
const [code, setCode] = useState('')
const [loading, setLoading] = useState(false)
const [loadError, setLoadError] = useState(false)
const refresh = useCallback(async () => {
setLoading(true)
setLoadError(false)
setCode('')
onChange('', '')
try {
const { data } = await http.post<CaptchaChallenge>('/auth/login/captcha', {}, { skipAuthSession: true })
setCaptcha(data)
} catch {
setCaptcha(null)
setLoadError(true)
} finally {
setLoading(false)
}
}, [onChange])
useEffect(() => { void refresh() }, [refresh, refreshKey])
useEffect(() => { onChange(captcha?.captchaId ?? '', code) }, [captcha, code, onChange])
const isImageCaptcha = captcha?.type === 'TextImage' || captcha?.type === 'MathImage'
return (
<section className="captcha" aria-label="验证码">
<div className="captcha__heading">
<span>验证码</span>
<button className="captcha__refresh" type="button" disabled={loading} onClick={() => void refresh()}>
换一张
</button>
</div>
{loadError ? (
<Alert
type="warning"
showIcon
message="验证码暂时无法加载"
action={<Button size="small" onClick={() => void refresh()}>重新加载</Button>}
/>
) : loading && !captcha ? <Skeleton.Input active block style={{ height: 50 }} /> : (
<div className="captcha__body" aria-busy={loading}>
{isImageCaptcha && (
<>
{captcha?.imageData && (
<img className="captcha__image" src={captcha.imageData} alt="验证码图片,点击可更换" onClick={() => void refresh()} />
)}
<Input
value={code}
maxLength={8}
autoComplete="off"
placeholder="输入图中字符"
size="large"
onChange={(event) => setCode(event.target.value)}
/>
</>
)}
{captcha && !isImageCaptcha && (
<div className="captcha__question">
<p>{captcha.prompt}</p>
{captcha.imageData && (
<img className="captcha__image" src={captcha.imageData} alt="验证码图片,点击可更换" onClick={() => void refresh()} />
)}
{captcha.type === 'Selection' ? (
<div className="captcha__choices" role="group" aria-label="验证码选项">
{(captcha.choices ?? []).map((choice) => (
<Button key={choice.id} type={code === choice.id ? 'primary' : 'default'} onClick={() => setCode(choice.id)}>
{choice.label}
</Button>
))}
</div>
) : (
<Button type={code === 'CONFIRM' ? 'primary' : 'default'} onClick={() => setCode('CONFIRM')}>
我确认是本人操作
</Button>
)}
</div>
)}
</div>
)}
</section>
)
}