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

@@ -12,8 +12,10 @@ import api from '../../api';
import { downloadBlob } from '../../utils/download';
import { message } from '../../ui/app-message';
import { usePermission } from '../../hooks/usePermission';
import { useVisibleRefetch } from '../../hooks/usePageVisible';
import { selectArchiveRecords } from '../archive-view';
import { getErrorMessage } from '../../utils/error';
import { QueryErrorState } from '../../components/QueryState';
import { useRoomColumns, type BedItem, type LockerItem } from './RoomColumns';
import { RoomDetailArea } from './RoomModals';
import { RoomsToolbar } from './RoomsToolbar';
@@ -51,28 +53,29 @@ const RoomsPage: React.FC = () => {
const [savingBed, setSavingBed] = useState(false);
const [savingLocker, setSavingLocker] = useState(false);
const [batchLoading, setBatchLoading] = useState(false);
const [bedsError, setBedsError] = useState(false);
const [lockersError, setLockersError] = useState(false);
const {
data = [],
isLoading,
isFetching,
isError,
refetch,
} = useQuery<any[]>({
queryKey: ['rooms', 'overview', showArchived],
queryFn: async () => {
try {
const params: any = { includeArchived: showArchived ? 'true' : undefined };
const res: any = await api.get('/rooms/overview', { params });
return selectArchiveRecords(
validateResponse<any[]>(roomsOverviewSchema, res),
showArchived ? 'archived' : 'active',
);
} catch (e: unknown) {
message.error(getErrorMessage(e, '加载失败,请稍后重试'));
return [];
}
const params: any = { includeArchived: showArchived ? 'true' : undefined };
const res: any = await api.get('/rooms/overview', { params });
return selectArchiveRecords(
validateResponse<any[]>(roomsOverviewSchema, res),
showArchived ? 'archived' : 'active',
);
},
});
const loading = isLoading || isFetching;
// RouteKeeper 保活页面切回时刷新列表,避免看到陈旧数据
useVisibleRefetch(['rooms']);
const {
saveMutation,
@@ -223,7 +226,9 @@ const RoomsPage: React.FC = () => {
try {
const res = await api.get<BedItem[]>(`/rooms/${roomId}/beds`);
setBeds(res);
setBedsError(false);
} catch (e: unknown) {
setBedsError(true);
message.error(getErrorMessage(e, '加载床位失败'));
}
}, []);
@@ -232,7 +237,9 @@ const RoomsPage: React.FC = () => {
try {
const res = await api.get<LockerItem[]>(`/rooms/${roomId}/lockers`);
setLockers(res);
setLockersError(false);
} catch (e: unknown) {
setLockersError(true);
message.error(getErrorMessage(e, '加载柜子失败'));
}
}, []);
@@ -466,13 +473,21 @@ const RoomsPage: React.FC = () => {
onExport={handleExport}
/>
<RoomsTable
columns={columns}
data={filteredData}
loading={loading}
selectedRowKeys={selectedRowKeys}
onSelect={setSelectedRowKeys}
/>
{isError ? (
<QueryErrorState
title="宿舍列表加载失败"
description="请检查网络后重试。"
onRetry={() => void refetch()}
/>
) : (
<RoomsTable
columns={columns}
data={filteredData}
loading={loading}
selectedRowKeys={selectedRowKeys}
onSelect={setSelectedRowKeys}
/>
)}
<RoomDetailArea
modalOpen={modalOpen}
@@ -489,6 +504,14 @@ const RoomsPage: React.FC = () => {
drawerRoom={drawerRoom}
beds={beds}
lockers={lockers}
bedsError={bedsError}
lockersError={lockersError}
onRetryBeds={() => {
if (drawerRoom) void fetchBeds(drawerRoom.id);
}}
onRetryLockers={() => {
if (drawerRoom) void fetchLockers(drawerRoom.id);
}}
canEditRooms={canEditRooms}
remainingBedSlots={remainingBedSlots}
defaultBatchBedCount={defaultBatchBedCount}