feat(admin): 用户体验体系化提升与高危缺陷修复

UX 缺陷修复:
- 校验失败不再卡死弹窗按钮(Users/Roles/Bills)
- 押金收取/批量收取/添加分期防重复提交;切换房型重置勾选
- AI 表单/批量确认不再出现"假成功"
- Dashboard 各数据模块独立加载,单接口失败不再整页清零
- 房间可视化加载失败显示错误态而非永久转圈
- 学生编辑表单回填前重置,避免字段残留污染
- 覆盖式导入增加二次确认;恢复默认考勤时段确认并同步表单
- 金数据匹配关闭前确认,同步中禁止误关

体验提升:
- 新增统一 QueryErrorState/QueryEmpty,20+ 页面加载失败显示错误态与重试
- 全局 ErrorBoundary + RouteKeeper 逐页兜底
- 新增 usePageVisible/useVisibleRefetch,保活页面切回自动刷新数据
- 新增首次登录角色引导 RoleTour 与业务闭环 NextStepHint 引导卡
- 重构 A2UI:useSubmissionState/useXCardSurface 收敛状态与命令生命周期,
  ArtifactErrorBoundary 渲染降级,图表空数据占位
- AI 助手欢迎语与建议话术按角色定制,会话列表空态引导
- 更新 a2ui-contract.md 契约文档说明实现现状
This commit is contained in:
2026-08-07 17:23:23 +08:00
parent db0d972633
commit 67435e46ca
54 changed files with 2298 additions and 1177 deletions

View File

@@ -39,6 +39,8 @@ import { useApiMutation } from '../../hooks/useApiMutation';
import { validateResponse } from '../../utils/validate';
import { classOptionsSchema, examsSchema } from '../../api/schemas';
import { getErrorMessage } from '../../utils/error';
import { QueryErrorState } from '../../components/QueryState';
import { useVisibleRefetch } from '../../hooks/usePageVisible';
const ExamsPage: React.FC = () => {
const { modal } = App.useApp();
@@ -76,7 +78,7 @@ const ExamsPage: React.FC = () => {
},
});
const { data = [], isFetching } = useQuery<ExamItem[]>({
const { data = [], isFetching, isError, refetch } = useQuery<ExamItem[]>({
queryKey: [
'exams',
debouncedFilters.keyword,
@@ -85,26 +87,23 @@ const ExamsPage: React.FC = () => {
debouncedFilters.showArchived,
],
queryFn: async () => {
try {
const params = new URLSearchParams();
if (debouncedFilters.keyword.trim())
params.set('keyword', debouncedFilters.keyword.trim());
if (debouncedFilters.examType) params.set('examType', debouncedFilters.examType);
if (debouncedFilters.classId) params.set('classId', String(debouncedFilters.classId));
params.set('isArchived', String(debouncedFilters.showArchived));
return (
validateResponse<ExamItem[]>(
examsSchema,
await api.get<ExamItem[]>(`/exams?${params.toString()}`),
) ?? []
);
} catch (error) {
message.error(getErrorMessage(error, '加载考试失败'));
return [];
}
const params = new URLSearchParams();
if (debouncedFilters.keyword.trim())
params.set('keyword', debouncedFilters.keyword.trim());
if (debouncedFilters.examType) params.set('examType', debouncedFilters.examType);
if (debouncedFilters.classId) params.set('classId', String(debouncedFilters.classId));
params.set('isArchived', String(debouncedFilters.showArchived));
return (
validateResponse<ExamItem[]>(
examsSchema,
await api.get<ExamItem[]>(`/exams?${params.toString()}`),
) ?? []
);
},
});
const loading = isFetching;
// RouteKeeper 保活页面切回时刷新考试列表
useVisibleRefetch(['exams']);
const saveMutation = useApiMutation(
async (payload: Record<string, unknown>) => api.post('/exams', payload),
@@ -343,7 +342,13 @@ const ExamsPage: React.FC = () => {
</Space>
</div>
{data.length === 0 && !loading ? (
{isError ? (
<QueryErrorState
title="考试数据加载失败"
description="请检查网络后重试。"
onRetry={() => void refetch()}
/>
) : data.length === 0 && !loading ? (
<div className="exam-empty">
<Empty description="暂无考试" />
</div>