feat: 重构各业务模块管理页面与服务

This commit is contained in:
2026-08-05 17:12:00 +08:00
parent 80e6fccf05
commit fd39e1686a
163 changed files with 18409 additions and 13449 deletions

View File

@@ -1,16 +1,21 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useMemo } from 'react';
import { Alert, Button, Card, Descriptions, Empty, Space, Spin, Table, Tag, Tooltip } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { ArrowLeftOutlined, EyeOutlined } from '@ant-design/icons';
import { useNavigate, useParams } from 'react-router-dom';
import { useNavigate, useParams } from 'react-router';
import api from '../../api';
import EditableCell from '../../components/EditableCell';
import { useViewSensitive } from '../../hooks/useViewSensitive';
import { maskPhone } from '../../utils/sensitive';
import { message } from '../../ui/app-message';
import { useQuery } from '@tanstack/react-query';
import { useApiMutation } from '../../hooks/useApiMutation';
import { validateResponse } from '../../utils/validate';
import { examDetailSchema } from '../../api/schemas';
import { usePermission } from '../../hooks/usePermission';
import type { ExamItem } from './types';
import './style.css';
import { getErrorMessage } from '../../utils/error';
interface ScoreRow {
id: number;
@@ -50,30 +55,38 @@ const PhoneCell: React.FC<{ row: ScoreRow }> = ({ row }) => {
const ExamDetailPage: React.FC = () => {
const { id } = useParams();
const navigate = useNavigate();
const [detail, setDetail] = useState<ExamDetail | null>(null);
const [loading, setLoading] = useState(true);
const load = useCallback(async () => {
setLoading(true);
try {
setDetail(await api.get<ExamDetail>(`/exams/${id}`));
} catch (error) {
message.error((error as { message?: string })?.message || '加载考试失败');
} finally {
setLoading(false);
}
}, [id]);
const { data: detail, isLoading, isFetching } = useQuery<ExamDetail | null>({
queryKey: ['exams', 'detail', id],
queryFn: async () => {
try {
return validateResponse<ExamDetail>(
examDetailSchema,
await api.get<ExamDetail>(`/exams/${id}`),
);
} catch (error) {
message.error(getErrorMessage(error, '加载考试失败'));
return null;
}
},
});
const loading = isLoading || isFetching;
useEffect(() => {
void load();
}, [load]);
const saveScoreMutation = useApiMutation(
async ({ rowId, score }: { rowId: number; score: number | null }) =>
api.put(`/exams/${id}/scores/${rowId}`, { score }),
{ invalidate: [['exams', 'detail', id]] },
);
const saveScore = useCallback(
async (row: ScoreRow, value: number | undefined) => {
await api.put(`/exams/${id}/scores/${row.id}`, { score: value ?? null });
message.success('成绩已保存');
await load();
try {
await saveScoreMutation.mutateAsync({ rowId: row.id, score: value ?? null });
message.success('成绩已保存');
} catch {
// 错误提示由 useApiMutation 统一处理
}
},
[id, load],
[saveScoreMutation],
);
const columns = useMemo<ColumnsType<ScoreRow>>(() => {