setKeyword(event.target.value)} onPressEnter={() => { setSearch(keyword.trim()); setPage(1) }} />
{wallets.isError ? : rowKey="id" loading={wallets.isLoading} pagination={false} dataSource={wallets.data?.items} locale={{ emptyText: }} columns={[{ title: '学生', render: (_, item) => {item.studentNumber} · {item.name}{item.collegeName} · {item.majorName} · {item.className}
}, { title: '当前余额', dataIndex: 'balance', width: 150, render: (value: number) => ¥{value.toFixed(2)} }, { title: '最后变动', dataIndex: 'lastChangedAt', width: 190, render: (value?: string) => value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '从未调整' }, { title: '操作', width: 110, fixed: 'right', render: (_, item) => }]} />}{(wallets.data?.total ?? 0) > 20 && } setAdjusting(undefined)} onOk={() => form.submit()}>
+}
diff --git a/web-react/src/pages/CourseEnrollmentManagementPage.tsx b/web-react/src/pages/CourseEnrollmentManagementPage.tsx
new file mode 100644
index 0000000..2544977
--- /dev/null
+++ b/web-react/src/pages/CourseEnrollmentManagementPage.tsx
@@ -0,0 +1,37 @@
+import { DownloadOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons'
+import { Alert, App, Button, Empty, Input, Pagination, Radio, Select, Skeleton, Table, Tag } from 'antd'
+import { useQuery, useQueryClient } from '@tanstack/react-query'
+import { useEffect, useMemo, useState } from 'react'
+import { downloadApiFile } from '../api/files'
+import http, { apiErrorMessage } from '../api/http'
+import PageHeader from '../components/PageHeader'
+import { useAuthStore } from '../stores/auth'
+import './admin-ledgers.css'
+
+type Mode = 'course' | 'student'
+interface Option { id: string; name: string; code?: string; collegeId?: string; majorId?: string; grade?: number; enrollmentYear?: number }
+interface Paged { items: T[]; total: number }
+interface Enrollment { id: string; courseSelectionOfferingId: string; termName: string; courseCode: string; courseName: string; taskNumber: string; credits: number; studentNumber: string; studentName: string; className: string; majorName: string; status: string; exemptionStatus?: string }
+interface StudentSummary { studentId: string; studentNumber: string; studentName: string; majorName: string; className: string; grade: number; courseCount: number; activeCredits: number; courses: Enrollment[] }
+interface Filters { academicTermId?: string; courseId?: string; collegeId?: string; majorId?: string; administrativeClassId?: string; grade?: number; enrollmentYear?: number; status?: string; enrollmentType?: string; exemptionStatus?: string; keyword?: string }
+const statusLabels: Record = { Enrolled: '已修读', Waitlisted: '候补中', Withdrawn: '已退选', Cancelled: '已取消', Expired: '已失效' }
+const exemptionLabels: Record = { Submitted: '审核中', Approved: '已批准', Rejected: '已驳回' }
+export default function CourseEnrollmentManagementPage() {
+ const { message, modal } = App.useApp(); const client = useQueryClient(); const user = useAuthStore((state) => state.user); const canAdjust = user?.roles.some((role) => ['SuperAdmin', 'AcademicAdmin'].includes(role)) ?? false; const scopedCollegeId = user?.roles.includes('CollegeAdmin') ? user.collegeId : undefined
+ const [mode, setMode] = useState('course'); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(20); const [draft, setDraft] = useState({ collegeId: scopedCollegeId }); const [filters, setFilters] = useState({ collegeId: scopedCollegeId })
+ useEffect(() => { if (scopedCollegeId) { setDraft((item) => ({ ...item, collegeId: scopedCollegeId })); setFilters((item) => ({ ...item, collegeId: scopedCollegeId })) } }, [scopedCollegeId])
+ const options = useQuery({ queryKey: ['enrollment-management-options'], queryFn: async () => { const [terms, courses, colleges, majors, classes] = await Promise.all([http.get