e329d3398a
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>
147 lines
5.0 KiB
TypeScript
147 lines
5.0 KiB
TypeScript
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>
|
||
)
|
||
}
|