feat(admin): 管理端 UI/UX 深度优化——错误/加载/空状态、表单快捷键、移动端适配与无障碍

This commit is contained in:
2026-08-08 15:05:33 +08:00
parent 57639a3869
commit a0829f17ce
54 changed files with 702 additions and 369 deletions

View File

@@ -1,5 +1,6 @@
import React, { useCallback, useMemo } from 'react';
import { Alert, Button, Card, Descriptions, Empty, Space, Spin, Table, Tag, Tooltip } from 'antd';
import {Alert, Button, Card, Descriptions, Empty, Space, Table, Tag, Tooltip, Skeleton} from 'antd';
import { QueryErrorState } from '../../components/QueryState';
import type { ColumnsType } from 'antd/es/table';
import { ArrowLeftOutlined, EyeOutlined } from '@ant-design/icons';
import { useNavigate, useParams } from 'react-router';
@@ -15,7 +16,6 @@ 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;
@@ -56,19 +56,10 @@ const ExamDetailPage: React.FC = () => {
const { id } = useParams();
const navigate = useNavigate();
const { data: detail, isLoading, isFetching } = useQuery<ExamDetail | null>({
const { data: detail, isLoading, isFetching, isError, refetch } = 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;
}
},
queryFn: async () =>
validateResponse<ExamDetail>(examDetailSchema, await api.get<ExamDetail>(`/exams/${id}`)),
});
const loading = isLoading || isFetching;
@@ -145,9 +136,18 @@ const ExamDetailPage: React.FC = () => {
if (loading && !detail)
return (
<div className="exam-detail-loading">
<Spin size="large" />
<Skeleton active paragraph={{ rows: 10 }} />
</div>
);
if (isError) {
return (
<QueryErrorState
title="考试详情加载失败"
description="请检查网络后重试。"
onRetry={() => void refetch()}
/>
);
}
if (!detail) return <Empty description="考试不存在或无权访问" />;
const average = detail.scores.find((row) => row.classAvg !== null)?.classAvg ?? null;