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:
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user