feat: refine attendance and finance workflows

This commit is contained in:
2026-07-18 20:46:36 +08:00
parent 7439663302
commit 1201837865
22 changed files with 1460 additions and 554 deletions

View File

@@ -74,6 +74,11 @@ interface StudentUpdateImportResult {
skipped?: number;
}
interface StudentFilterLookups {
classes: Array<{ id: number; name: string; code?: string }>;
teachers: Array<{ id: number; name: string; username: string }>;
}
const StudentsPage: React.FC = () => {
const { modal } = App.useApp();
const [data, setData] = useState<any[]>([]);
@@ -84,6 +89,10 @@ const StudentsPage: React.FC = () => {
const [searchName, setSearchName] = useState('');
const [filterStatus, setFilterStatus] = useState<string | undefined>(undefined);
const [filterOrganizationId, setFilterOrganizationId] = useState<number | undefined>(undefined);
const [filterClassId, setFilterClassId] = useState<number | undefined>(undefined);
const [filterTeacherId, setFilterTeacherId] = useState<number | undefined>(undefined);
const [classOptions, setClassOptions] = useState<StudentFilterLookups['classes']>([]);
const [teacherOptions, setTeacherOptions] = useState<StudentFilterLookups['teachers']>([]);
const [showArchived, setShowArchived] = useState(false);
const [archivedCount, setArchivedCount] = useState(0);
const [selectedRowKeys, setSelectedRowKeys] = useState<number[]>([]);
@@ -149,6 +158,8 @@ const StudentsPage: React.FC = () => {
};
if (filterStatus) params.status = filterStatus;
if (filterOrganizationId) params.organizationId = filterOrganizationId;
if (filterClassId) params.classId = filterClassId;
if (filterTeacherId) params.teacherId = filterTeacherId;
const res = (await api.get('/students', { params })) as Array<Record<string, unknown>>;
const list = res as Array<Record<string, unknown>>;
const archived = list.filter((r) => r.status === 'archived');
@@ -159,7 +170,7 @@ const StudentsPage: React.FC = () => {
message.error(err?.message || '加载失败,请稍后重试');
}
setLoading(false);
}, [searchName, showArchived, filterStatus, filterOrganizationId]);
}, [searchName, showArchived, filterStatus, filterOrganizationId, filterClassId, filterTeacherId]);
useEffect(() => {
fetchData();
@@ -172,6 +183,13 @@ const StudentsPage: React.FC = () => {
setOrganizations(res as Array<{ id: number; name: string }>);
})
.catch(() => {});
api
.get<StudentFilterLookups>('/students/filter-lookups')
.then((res) => {
setClassOptions(res.classes || []);
setTeacherOptions(res.teachers || []);
})
.catch(() => {});
}, []);
const handleSave = async () => {
const values = await form.validateFields();
@@ -329,8 +347,15 @@ const StudentsPage: React.FC = () => {
? '/api'
: `http://localhost:${import.meta.env.VITE_API_PORT || 3002}/api`;
const token = localStorage.getItem('token');
const params = showArchived ? '?includeArchived=true' : '';
fetch(`${baseURL}/students/export${params}`, { headers: { Authorization: `Bearer ${token}` } })
const params = new URLSearchParams();
if (searchName) params.set('name', searchName);
if (filterStatus) params.set('status', filterStatus);
if (filterOrganizationId) params.set('organizationId', String(filterOrganizationId));
if (showArchived) params.set('includeArchived', 'true');
if (filterClassId) params.set('classId', String(filterClassId));
if (filterTeacherId) params.set('teacherId', String(filterTeacherId));
const query = params.toString() ? `?${params.toString()}` : '';
fetch(`${baseURL}/students/export${query}`, { headers: { Authorization: `Bearer ${token}` } })
.then((res) => res.blob())
.then((blob) => {
const url = URL.createObjectURL(blob);
@@ -567,6 +592,36 @@ const StudentsPage: React.FC = () => {
</Select.Option>
))}
</Select>
<Select
placeholder="所属班级"
allowClear
showSearch
optionFilterProp="label"
style={{ width: 160 }}
value={filterClassId}
onChange={(v) => {
setFilterClassId(v);
}}
options={classOptions.map((item) => ({
value: item.id,
label: item.code ? `${item.name}${item.code}` : item.name,
}))}
/>
<Select
placeholder="所属老师"
allowClear
showSearch
optionFilterProp="label"
style={{ width: 160 }}
value={filterTeacherId}
onChange={(v) => {
setFilterTeacherId(v);
}}
options={teacherOptions.map((item) => ({
value: item.id,
label: item.name === item.username ? item.name : `${item.name}${item.username}`,
}))}
/>
<Button
type={showArchived ? 'primary' : 'default'}
onClick={() => setShowArchived(!showArchived)}