Initial commit: secondary school grade archive system.

Add FastAPI/React app with Docker deployment, Ubuntu one-click install, and docs for junior/senior high score tracking and mistake bank.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-28 11:18:58 +08:00
commit e329d3398a
76 changed files with 8506 additions and 0 deletions
+146
View File
@@ -0,0 +1,146 @@
import { LogoutOutlined, PlusOutlined, UserOutlined } from '@ant-design/icons'
import { Button, Card, Col, Form, Input, Modal, Row, Select, Space, Spin, Tag, Typography, message } from 'antd'
import { useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
import { studentApi } from '../api/client'
import { formatStudentMeta, GRADE_OPTIONS, SCHOOL_LEVEL_LABELS } from '../constants/school'
import { useAuth } from '../context/AuthContext'
import type { SchoolLevel, Student } from '../types'
export default function StudentsPage() {
const { user, logout } = useAuth()
const [students, setStudents] = useState<Student[]>([])
const [loading, setLoading] = useState(true)
const [modalOpen, setModalOpen] = useState(false)
const [form] = Form.useForm()
const schoolLevel = Form.useWatch('school_level', form) as SchoolLevel | undefined
const load = async () => {
setLoading(true)
try {
const { data } = await studentApi.list()
setStudents(data)
} finally {
setLoading(false)
}
}
useEffect(() => {
load()
}, [])
const openCreate = () => {
form.setFieldsValue({ school_level: 'junior_high', grade: undefined })
setModalOpen(true)
}
const handleCreate = async () => {
const values = await form.validateFields()
await studentApi.create(values)
message.success('学生已添加')
setModalOpen(false)
form.resetFields()
load()
}
return (
<div style={{ maxWidth: 960, margin: '0 auto', padding: '16px 16px 32px' }}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 24,
flexWrap: 'wrap',
gap: 12,
}}
>
<div>
<Typography.Title level={3} style={{ margin: 0 }}>
</Typography.Title>
<Typography.Text type="secondary">{user?.username}</Typography.Text>
</div>
<Space wrap>
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
</Button>
<Button icon={<LogoutOutlined />} onClick={logout}>
退
</Button>
</Space>
</div>
<Spin spinning={loading}>
<Row gutter={[16, 16]}>
{students.map((s) => (
<Col xs={24} sm={12} md={8} key={s.id}>
<Link to={`/students/${s.id}`} style={{ textDecoration: 'none' }}>
<Card hoverable>
<Space align="start">
<UserOutlined style={{ fontSize: 24, color: '#1677ff' }} />
<div>
<Space size={4}>
<Typography.Text strong>{s.name}</Typography.Text>
<Tag color={s.school_level === 'senior_high' ? 'purple' : 'blue'}>
{SCHOOL_LEVEL_LABELS[s.school_level]}
</Tag>
</Space>
<br />
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{formatStudentMeta(s)}
</Typography.Text>
</div>
</Space>
</Card>
</Link>
</Col>
))}
{!loading && students.length === 0 && (
<Col span={24}>
<Card>
<Typography.Text type="secondary"></Typography.Text>
</Card>
</Col>
)}
</Row>
</Spin>
<Modal
title="添加学生"
open={modalOpen}
onCancel={() => setModalOpen(false)}
onOk={handleCreate}
destroyOnHidden
>
<Form form={form} layout="vertical" initialValues={{ school_level: 'junior_high' }}>
<Form.Item name="name" label="姓名" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item name="school_level" label="学段" rules={[{ required: true }]}>
<Select
options={Object.entries(SCHOOL_LEVEL_LABELS).map(([value, label]) => ({
value,
label,
}))}
onChange={() => form.setFieldValue('grade', undefined)}
/>
</Form.Item>
<Form.Item name="grade" label="年级">
<Select
allowClear
placeholder={schoolLevel === 'senior_high' ? '如:高一' : '如:初二'}
options={(GRADE_OPTIONS[schoolLevel || 'junior_high'] || []).map((g) => ({
value: g,
label: g,
}))}
/>
</Form.Item>
<Form.Item name="class_name" label="班级">
<Input placeholder="如:3班" />
</Form.Item>
</Form>
</Modal>
</div>
)
}