From 85872d656418e5077383f722f2f416b8dc43159f Mon Sep 17 00:00:00 2001 From: wangziqi Date: Mon, 6 Jul 2026 17:22:09 +0800 Subject: [PATCH] refactor(admin): extract StudentProfileContent component and reuse in drawer --- .../StudentProfileContent/index.tsx | 906 ++++++++++++++++++ apps/admin/src/pages/StudentProfile/index.tsx | 811 +--------------- apps/admin/src/pages/Students/index.tsx | 112 +-- 3 files changed, 927 insertions(+), 902 deletions(-) create mode 100644 apps/admin/src/components/StudentProfileContent/index.tsx diff --git a/apps/admin/src/components/StudentProfileContent/index.tsx b/apps/admin/src/components/StudentProfileContent/index.tsx new file mode 100644 index 0000000..8b4db9c --- /dev/null +++ b/apps/admin/src/components/StudentProfileContent/index.tsx @@ -0,0 +1,906 @@ +import React, { useEffect, useState, useCallback } from 'react'; +import { + Tabs, + Card, + Descriptions, + Table, + Button, + Modal, + Form, + Input, + Select, + DatePicker, + InputNumber, + Upload, + Tag, + Space, + message, + Popconfirm, + Empty, + Row, + Col, + Statistic, +} from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import { + PlusOutlined, + UploadOutlined, + DeleteOutlined, + EyeOutlined, + CloseOutlined, + FileTextOutlined, + FilePdfOutlined, + CameraOutlined, + ReloadOutlined, +} from '@ant-design/icons'; +import dayjs from 'dayjs'; +import api from '../../api'; + +// ---- 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[]; +} + +export interface StudentProfileContentProps { + studentId: number; + inDrawer?: boolean; + onClose?: () => void; +} + +// ---- 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 Component ---- + +const StudentProfileContent: React.FC = ({ + studentId, + inDrawer, + onClose, +}) => { + const [aggregateData, setAggregateData] = useState(null); + const [loading, setLoading] = useState(false); + + const fetchData = useCallback(async () => { + setLoading(true); + try { + const res = await api.get(`/archive/${studentId}`); + setAggregateData(res); + } catch (e: unknown) { + const err = e as { message?: string }; + message.error(err?.message || '加载失败'); + } finally { + setLoading(false); + } + }, [studentId]); + + useEffect(() => { + void fetchData(); + }, [fetchData]); + + const handleDownloadReport = useCallback(() => { + const token = localStorage.getItem('token'); + window.open(`/api/archive/${studentId}/report?token=${token}`, '_blank'); + }, [studentId]); + + const handlePreviewReport = useCallback(() => { + const token = localStorage.getItem('token'); + window.open(`/api/archive/${studentId}/report?token=${token}`, '_blank'); + }, [studentId]); + + const handleDownloadPdf = useCallback(async () => { + const token = localStorage.getItem('token'); + try { + const res = await fetch(`/api/archive/${studentId}/report`, { + headers: { Authorization: `Bearer ${token}` }, + }); + if (!res.ok) throw new Error('下载失败'); + const blob = await res.blob(); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `学员档案_${studentId}.pdf`; + a.click(); + URL.revokeObjectURL(url); + } catch (e: unknown) { + const err = e as { message?: string }; + message.error(err?.message || 'PDF下载失败'); + } + }, [studentId]); + + if (!aggregateData) return null; + + const { student, profile, enrollments, examScores, learningRecords, result, attachments } = aggregateData; + + return ( +
+ {inDrawer && ( + + + + + + + + + )} + + + {[ + { title: '入学测试总分' }, + { title: '阶段最高分' }, + { title: '阶段提升分' }, + { title: '出勤率' }, + ].map((item) => ( + + + + + + ))} + + + + {student.studentNo || '-'} + {student.phone || '-'} + {student.idNumber || '-'} + + {student.status || '-'} + + {profile?.targetCollege && ( + {profile.targetCollege} + )} + {profile?.targetMajor && ( + {profile.targetMajor} + )} + {profile?.grade && ( + {profile.grade} + )} + {profile?.subjectDirection && ( + {profile.subjectDirection} + )} + {profile?.campusLocation && ( + {profile.campusLocation} + )} + + + , + }, + { + key: 'enrollments', + label: `报读班型 (${enrollments.length})`, + children: ( + + ), + }, + { + key: 'exams', + label: `考试成绩 (${examScores.length})`, + children: ( + + ), + }, + { + key: 'attendance', + label: '出勤记录', + children: , + }, + { + key: 'learning', + label: `课堂回访 (${learningRecords.length})`, + children: ( + + ), + }, + { + key: 'result', + label: '录取归档', + children: , + }, + { + key: 'attachments', + label: `附件 (${attachments.length})`, + children: ( + + ), + }, + { + key: 'reports', + label: '报告版本', + children: , + }, + ]} + /> +
+ ); +}; + +export default StudentProfileContent; diff --git a/apps/admin/src/pages/StudentProfile/index.tsx b/apps/admin/src/pages/StudentProfile/index.tsx index 3caf4c8..d88fe70 100644 --- a/apps/admin/src/pages/StudentProfile/index.tsx +++ b/apps/admin/src/pages/StudentProfile/index.tsx @@ -1,742 +1,21 @@ -import React, { useEffect, useState, useCallback } from 'react'; +import React 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 { Card, Button, Space } from 'antd'; +import { ArrowLeftOutlined, DownloadOutlined } from '@ant-design/icons'; +import StudentProfileContent from '../../components/StudentProfileContent'; 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; + if (!id) 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'); + window.open(`/api/archive/${studentId}/report?token=${token}`, '_blank'); }; return ( @@ -744,12 +23,9 @@ const StudentProfilePage: React.FC = () => { title={