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

@@ -28,6 +28,8 @@ import {
} from './DepositModals';
import type { DepositRecord, EligibleStudent } from './DepositModals';
import { DepositTable } from './DepositTable';
import { QueryErrorState } from '../../components/QueryState';
import { useVisibleRefetch } from '../../hooks/usePageVisible';
const DepositsPage: React.FC = () => {
const { hasPermission } = usePermission();
@@ -55,27 +57,26 @@ const DepositsPage: React.FC = () => {
data: fetchResult = { data: [], students: [] },
isLoading,
isFetching,
isError,
refetch,
} = useQuery<{ data: DepositRecord[]; students: DepositStudentLookup[] }>({
queryKey: ['deposits'],
queryFn: async () => {
try {
const [d, s] = await Promise.all([
api.get<DepositRecord[]>('/deposits'),
api.get<DepositStudentLookup[]>('/deposits/student-lookups'),
]);
return {
data: validateResponse<DepositRecord[]>(depositsSchema, d),
students: validateResponse<DepositStudentLookup[]>(depositStudentLookupsSchema, s),
};
} catch (e: any) {
message.error(e?.message || '加载失败');
return { data: [], students: [] };
}
const [d, s] = await Promise.all([
api.get<DepositRecord[]>('/deposits'),
api.get<DepositStudentLookup[]>('/deposits/student-lookups'),
]);
return {
data: validateResponse<DepositRecord[]>(depositsSchema, d),
students: validateResponse<DepositStudentLookup[]>(depositStudentLookupsSchema, s),
};
},
});
const data = fetchResult.data;
const students = fetchResult.students;
const loading = isLoading || isFetching;
// RouteKeeper 保活页面切回时刷新押金列表
useVisibleRefetch(['deposits']);
const invalidateDeposits: QueryKey[] = [['deposits'], ['deposits', 'eligible']];
const createMutation = useApiMutation(
@@ -132,6 +133,8 @@ const DepositsPage: React.FC = () => {
const {
data: eligibleStudents = [],
isFetching: eligibleFetching,
isError: eligibleError,
refetch: refetchEligible,
} = useQuery<EligibleStudent[]>({
queryKey: ['deposits', 'eligible', eligibleRoomType],
queryFn: async () => {
@@ -224,6 +227,9 @@ const DepositsPage: React.FC = () => {
const handleBatchRoomTypeChange = (roomType: string) => {
setBatchRoomType(roomType);
// 切换房型后候选学生列表会变化,重置勾选状态,避免把上一房型的选择提交到新房型
setSelectionTouched(false);
setSelectedEligibleStudentIds([]);
batchForm.setFieldsValue({
amount: suggestedDepositByRoomType[roomType] ?? batchForm.getFieldValue('amount') ?? 100,
});
@@ -231,6 +237,7 @@ const DepositsPage: React.FC = () => {
};
const handleCreate = async () => {
setSaving(true);
try {
const values = await createForm.validateFields();
await createMutation.mutateAsync({
@@ -244,10 +251,13 @@ const DepositsPage: React.FC = () => {
createForm.resetFields();
} catch {
// 错误提示由 useApiMutation 统一处理
} finally {
setSaving(false);
}
};
const handleBatchCreate = async () => {
setSaving(true);
try {
const values = await batchForm.validateFields();
await batchCreateMutation.mutateAsync({
@@ -263,6 +273,8 @@ const DepositsPage: React.FC = () => {
setSelectionTouched(false);
} catch {
// 错误提示由 useApiMutation 统一处理
} finally {
setSaving(false);
}
};
@@ -290,6 +302,7 @@ const DepositsPage: React.FC = () => {
const handleAddInstallment = async () => {
if (installmentModal == null) return;
setSaving(true);
try {
const values = await installmentForm.validateFields();
await addInstallmentMutation.mutateAsync({
@@ -304,6 +317,8 @@ const DepositsPage: React.FC = () => {
installmentForm.resetFields();
} catch {
// 错误提示由 useApiMutation 统一处理
} finally {
setSaving(false);
}
};
@@ -426,16 +441,30 @@ const DepositsPage: React.FC = () => {
</PermissionButton>
</Space>
</div>
<DepositTable
data={filteredData}
loading={loading || (!!filterRoomType && eligibleLoading)}
canPurgeDeposit={canPurgeDeposit}
refundForm={refundForm}
onDetail={(record) => setDetailModal(record)}
onRefund={(record) => setRefundModal(record)}
onArchive={(id) => archiveMutation.mutateAsync(id)}
onPurge={(id) => purgeMutation.mutateAsync(id)}
/>
{isError ? (
<QueryErrorState
title="押金数据加载失败"
description="请检查网络后重试。"
onRetry={() => void refetch()}
/>
) : filterRoomType && eligibleError ? (
<QueryErrorState
title="可收取押金的学生加载失败"
description="请检查网络后重试。"
onRetry={() => void refetchEligible()}
/>
) : (
<DepositTable
data={filteredData}
loading={loading || (!!filterRoomType && eligibleLoading)}
canPurgeDeposit={canPurgeDeposit}
refundForm={refundForm}
onDetail={(record) => setDetailModal(record)}
onRefund={(record) => setRefundModal(record)}
onArchive={(id) => archiveMutation.mutateAsync(id)}
onPurge={(id) => purgeMutation.mutateAsync(id)}
/>
)}
<DepositModals
batchModal={batchModal}
createModal={createModal}