import React, { useEffect, useState, useCallback } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { Tabs, Card, Descriptions, Table, Button, Modal, Form, Input, Select, DatePicker, InputNumber, Upload, Tag, Space, message, Popconfirm, } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import { PlusOutlined, UploadOutlined, DownloadOutlined, ArrowLeftOutlined, DeleteOutlined, EyeOutlined, } from '@ant-design/icons'; import dayjs from 'dayjs'; import api from '../../api'; import PermissionButton from '../../components/PermissionButton'; // ---- Types ---- interface StudentInfo { id: number; name: string; phone: string; idNumber: string; studentNo: string; status: string; } interface ProfileData { targetCollege?: string; targetMajor?: string; subjectDirection?: string; grade?: string; campusLocation?: string; profileDate?: string; notes?: string; } interface EnrollmentRecord { id: number; courseCategory: string; classType: string; className?: string; headTeacher?: string; subjectTeacher?: string; startDate?: string; endDate?: string; status: string; } interface ExamScoreRecord { id: number; examType: string; examName?: string; subject: string; score: number; classAvg?: number; rank?: number; examDate?: string; enrollmentId?: number; } interface LearningRecord { id: number; recordDate: string; recordType: string; content: string; followUpMethod?: string; nextStep?: string; } interface ResultData { cultureFinalScore?: number; professionalFinalScore?: number; admissionStatus?: string; admittedCollege?: string; admittedMajor?: string; } interface AttachmentRecord { id: number; category: string; fileName: string; fileSize: number; } interface StudentProfileAggregate { student: StudentInfo; profile: ProfileData | null; enrollments: EnrollmentRecord[]; examScores: ExamScoreRecord[]; learningRecords: LearningRecord[]; result: ResultData | null; attachments: AttachmentRecord[]; } // ---- Constants ---- const ADMISSION_STATUS_MAP: Record = { admitted: { text: '已录取', color: 'green' }, pending: { text: '待录取', color: 'orange' }, rejected: { text: '未录取', color: 'red' }, withdrawn: { text: '放弃', color: '#999' }, }; const EXAM_TYPE_OPTIONS = [ { value: 'monthly', label: '月考' }, { value: 'midterm', label: '期中' }, { value: 'final', label: '期末' }, { value: 'mock', label: '模拟考' }, { value: 'entrance', label: '入学测试' }, { value: 'other', label: '其他' }, ]; const RECORD_TYPE_OPTIONS = [ { value: 'study_feedback', label: '学习反馈' }, { value: 'parent_communication', label: '家长沟通' }, { value: 'behavior_note', label: '行为记录' }, { value: 'meeting', label: '会议记录' }, { value: 'other', label: '其他' }, ]; const COURSE_CATEGORY_OPTIONS = [ { value: 'culture', label: '文化课' }, { value: 'professional', label: '专业课' }, { value: 'comprehensive', label: '综合' }, ]; const CLASS_TYPE_OPTIONS = [ { value: 'one_on_one', label: '一对一' }, { value: 'small_group', label: '小班' }, { value: 'large_class', label: '大班' }, { value: 'online', label: '线上' }, { value: 'offline', label: '线下' }, ]; const ATTACHMENT_CATEGORY_OPTIONS = [ { value: 'id_card', label: '身份证' }, { value: 'transcript', label: '成绩单' }, { value: 'certificate', label: '证书' }, { value: 'contract', label: '合同' }, { value: 'photo', label: '照片' }, { value: 'other', label: '其他' }, ]; const formatFileSize = (bytes: number): string => { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; }; // ---- Tab Components ---- interface TabProps { studentId: number; onRefresh: () => void; } const ProfileTab: React.FC<{ data: ProfileData | null; studentId: number; onRefresh: () => void }> = ({ data, studentId, onRefresh, }) => { const [form] = Form.useForm(); const [saving, setSaving] = useState(false); const handleSave = async () => { try { const values = await form.validateFields(); setSaving(true); await api.put(`/archive/${studentId}/profile`, { ...values, profileDate: values.profileDate?.format('YYYY-MM-DD'), }); message.success('基础档案已保存'); onRefresh(); } catch (e: unknown) { const err = e as { message?: string }; if (err?.message) message.error(err.message); } finally { setSaving(false); } }; return (
); }; const EnrollmentsTab: React.FC = ({ data, studentId, onRefresh, }) => { const [modalOpen, setModalOpen] = useState(false); const [form] = Form.useForm(); const [saving, setSaving] = useState(false); const handleAdd = async () => { try { const values = await form.validateFields(); setSaving(true); await api.post(`/archive/${studentId}/enrollments`, { ...values, startDate: values.startDate?.format('YYYY-MM-DD'), endDate: values.endDate?.format('YYYY-MM-DD'), }); message.success('报读记录已添加'); setModalOpen(false); form.resetFields(); onRefresh(); } catch (e: unknown) { const err = e as { message?: string }; if (err?.message) message.error(err.message); } finally { setSaving(false); } }; const columns: ColumnsType = [ { title: '课程类别', dataIndex: 'courseCategory', render: (v: string) => v || '-' }, { title: '班型', dataIndex: 'classType', render: (v: string) => v || '-' }, { title: '班级名称', dataIndex: 'className', render: (v: string) => v || '-' }, { title: '班主任', dataIndex: 'headTeacher', render: (v: string) => v || '-' }, { title: '任课教师', dataIndex: 'subjectTeacher', render: (v: string) => v || '-' }, { title: '开始日期', dataIndex: 'startDate', render: (v: string) => v || '-' }, { title: '结束日期', dataIndex: 'endDate', render: (v: string) => v || '-' }, { title: '状态', dataIndex: 'status', render: (v: string) => { const colorMap: Record = { active: 'green', completed: 'blue', withdrawn: 'red', }; return {v || '-'}; }, }, ]; return (
columns={columns} dataSource={data} rowKey="id" pagination={{ pageSize: 15 }} /> setModalOpen(false)} confirmLoading={saving} >
); }; const ExamScoresTab: React.FC = ({ data, studentId, enrollments, onRefresh, }) => { const [modalOpen, setModalOpen] = useState(false); const [form] = Form.useForm(); const [saving, setSaving] = useState(false); const handleAdd = async () => { try { const values = await form.validateFields(); setSaving(true); await api.post(`/archive/${studentId}/exam-scores`, { ...values, examDate: values.examDate?.format('YYYY-MM-DD'), }); message.success('考试成绩已添加'); setModalOpen(false); form.resetFields(); onRefresh(); } catch (e: unknown) { const err = e as { message?: string }; if (err?.message) message.error(err.message); } finally { setSaving(false); } }; const columns: ColumnsType = [ { title: '考试类型', dataIndex: 'examType', render: (v: string) => EXAM_TYPE_OPTIONS.find((o) => o.value === v)?.label || v, }, { title: '考试名称', dataIndex: 'examName', render: (v: string) => v || '-' }, { title: '科目', dataIndex: 'subject' }, { title: '成绩', dataIndex: 'score' }, { title: '班级均分', dataIndex: 'classAvg', render: (v: number | undefined) => (v !== undefined ? v : '-') }, { title: '排名', dataIndex: 'rank', render: (v: number | undefined) => (v !== undefined ? v : '-') }, { title: '考试日期', dataIndex: 'examDate', render: (v: string) => v || '-' }, { title: '关联报读', dataIndex: 'enrollmentId', render: (v: number | undefined) => { if (v === undefined) return '-'; const enr = enrollments.find((e) => e.id === v); return enr ? `${enr.className || enr.courseCategory || v}` : String(v); }, }, ]; return (
columns={columns} dataSource={data} rowKey="id" pagination={{ pageSize: 15 }} /> setModalOpen(false)} confirmLoading={saving} >
); }; const ResultTab: React.FC = ({ data, studentId, onRefresh }) => { const [form] = Form.useForm(); const [saving, setSaving] = useState(false); const handleSave = async () => { try { const values = await form.validateFields(); setSaving(true); await api.put(`/archive/${studentId}/result`, values); message.success('录取结果已保存'); onRefresh(); } catch (e: unknown) { const err = e as { message?: string }; if (err?.message) message.error(err.message); } finally { setSaving(false); } }; return (
); }; const AttachmentsTab: React.FC = ({ data, studentId, onRefresh }) => { const [uploading, setUploading] = useState(false); const handleDelete = async (attachmentId: number) => { try { await api.delete(`/archive/${studentId}/attachments/${attachmentId}`); message.success('已删除'); onRefresh(); } catch (e: unknown) { const err = e as { message?: string }; message.error(err?.message || '删除失败'); } }; const columns: ColumnsType = [ { title: '类别', dataIndex: 'category', render: (v: string) => ATTACHMENT_CATEGORY_OPTIONS.find((o) => o.value === v)?.label || v, }, { title: '文件名', dataIndex: 'fileName' }, { title: '大小', dataIndex: 'fileSize', render: formatFileSize }, { title: '操作', render: (_: unknown, record: AttachmentRecord) => ( handleDelete(record.id)}> ), }, ]; return (
{ const formData = new FormData(); formData.append('file', options.file instanceof File ? options.file : new File([options.file as Blob], 'attachment')); setUploading(true); try { await api.post(`/archive/${studentId}/attachments`, formData, { headers: { 'Content-Type': 'multipart/form-data' }, }); message.success('上传成功'); options.onSuccess?.({}); onRefresh(); } catch (e: unknown) { const msg = e instanceof Error ? e.message : '上传失败'; message.error(msg); options.onError?.(e instanceof Error ? e : new Error(msg)); } finally { setUploading(false); } }} > columns={columns} dataSource={data} rowKey="id" pagination={{ pageSize: 15 }} style={{ marginTop: 16 }} />
); }; // ---- Main Page ---- const StudentProfilePage: React.FC = () => { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const [aggregateData, setAggregateData] = useState(null); const [loading, setLoading] = useState(false); const fetchData = useCallback(async () => { if (!id) return; setLoading(true); try { const res = await api.get(`/archive/${id}`); setAggregateData(res); } catch (e: unknown) { const err = e as { message?: string }; message.error(err?.message || '加载失败'); } finally { setLoading(false); } }, [id]); useEffect(() => { void fetchData(); }, [fetchData]); if (!aggregateData) return null; const studentId = Number(id); const { student, profile, enrollments, examScores, learningRecords, result, attachments } = aggregateData; const handleDownloadReport = () => { const token = localStorage.getItem('token'); window.open(`/api/archive/${id}/report?token=${token}`, '_blank'); }; return (