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:
@@ -5,7 +5,9 @@ import { CalendarOutlined, CloudSyncOutlined, LeftOutlined, RightOutlined } from
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import api from '../../api';
|
||||
import PermissionButton from '../../components/PermissionButton';
|
||||
import { QueryErrorState } from '../../components/QueryState';
|
||||
import { usePermission } from '../../hooks/usePermission';
|
||||
import { useVisibleRefetch } from '../../hooks/usePageVisible';
|
||||
import { message } from '../../ui/app-message';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useApiMutation } from '../../hooks/useApiMutation';
|
||||
@@ -64,6 +66,7 @@ const SchedulesPage: React.FC = () => {
|
||||
mappedClasses: number;
|
||||
totalClasses: number;
|
||||
} | null>(null);
|
||||
const [syncStatusError, setSyncStatusError] = useState(false);
|
||||
const [syncResult, setSyncResult] = useState<ScheduleSyncResult | null>(null);
|
||||
const [syncDateFrom, setSyncDateFrom] = useState<Dayjs>(dayjs);
|
||||
const [syncDays, setSyncDays] = useState(30);
|
||||
@@ -73,6 +76,7 @@ const SchedulesPage: React.FC = () => {
|
||||
const openSyncModal = async () => {
|
||||
setSyncModalOpen(true);
|
||||
setSyncResult(null);
|
||||
setSyncStatusError(false);
|
||||
try {
|
||||
const res = await api.get<{
|
||||
success: boolean;
|
||||
@@ -80,6 +84,7 @@ const SchedulesPage: React.FC = () => {
|
||||
}>('/sync/schedule/status');
|
||||
setSyncStatus(res.data);
|
||||
} catch {
|
||||
setSyncStatusError(true);
|
||||
setSyncStatus(null);
|
||||
}
|
||||
};
|
||||
@@ -154,6 +159,8 @@ const SchedulesPage: React.FC = () => {
|
||||
data: fetchResult = { classrooms: [], classes: [], matrix: {} },
|
||||
isLoading,
|
||||
isFetching,
|
||||
isError,
|
||||
refetch,
|
||||
} = useQuery<{
|
||||
classrooms: ClassroomItem[];
|
||||
classes: ClassItem[];
|
||||
@@ -161,51 +168,48 @@ const SchedulesPage: React.FC = () => {
|
||||
}>({
|
||||
queryKey: ['class-schedules', startDateStr, endDateStr, filterClassroomIds],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const [lookups, schedulesRes] = await Promise.all([
|
||||
api.get('/class-schedules/lookups') as Promise<{
|
||||
classrooms: ClassroomItem[];
|
||||
classes: ClassItem[];
|
||||
}>,
|
||||
api.get('/class-schedules/weekly', {
|
||||
params: {
|
||||
startDate: startDateStr,
|
||||
endDate: endDateStr,
|
||||
...(filterClassroomIds.length === 1 ? { classroomId: filterClassroomIds[0] } : {}),
|
||||
},
|
||||
}) as Promise<Record<string, Record<string, ClassScheduleItem[]>>>,
|
||||
]);
|
||||
const validatedLookups = validateResponse<{
|
||||
const [lookups, schedulesRes] = await Promise.all([
|
||||
api.get('/class-schedules/lookups') as Promise<{
|
||||
classrooms: ClassroomItem[];
|
||||
classes: ClassItem[];
|
||||
}>(scheduleLookupsSchema, lookups);
|
||||
const validatedWeekly = validateResponse<
|
||||
Record<string, Record<string, ClassScheduleItem[]>>
|
||||
>(weeklyScheduleSchema, schedulesRes);
|
||||
}>,
|
||||
api.get('/class-schedules/weekly', {
|
||||
params: {
|
||||
startDate: startDateStr,
|
||||
endDate: endDateStr,
|
||||
...(filterClassroomIds.length === 1 ? { classroomId: filterClassroomIds[0] } : {}),
|
||||
},
|
||||
}) as Promise<Record<string, Record<string, ClassScheduleItem[]>>>,
|
||||
]);
|
||||
const validatedLookups = validateResponse<{
|
||||
classrooms: ClassroomItem[];
|
||||
classes: ClassItem[];
|
||||
}>(scheduleLookupsSchema, lookups);
|
||||
const validatedWeekly = validateResponse<
|
||||
Record<string, Record<string, ClassScheduleItem[]>>
|
||||
>(weeklyScheduleSchema, schedulesRes);
|
||||
|
||||
const typedMatrix: Record<number, Record<number, ClassScheduleItem[]>> = {};
|
||||
for (const [cId, dayMap] of Object.entries(validatedWeekly)) {
|
||||
const classroomId = Number(cId);
|
||||
typedMatrix[classroomId] = {};
|
||||
for (const [wd, schedules] of Object.entries(dayMap)) {
|
||||
typedMatrix[classroomId][Number(wd)] = schedules;
|
||||
}
|
||||
const typedMatrix: Record<number, Record<number, ClassScheduleItem[]>> = {};
|
||||
for (const [cId, dayMap] of Object.entries(validatedWeekly)) {
|
||||
const classroomId = Number(cId);
|
||||
typedMatrix[classroomId] = {};
|
||||
for (const [wd, schedules] of Object.entries(dayMap)) {
|
||||
typedMatrix[classroomId][Number(wd)] = schedules;
|
||||
}
|
||||
return {
|
||||
classrooms: validatedLookups.classrooms,
|
||||
classes: validatedLookups.classes,
|
||||
matrix: typedMatrix,
|
||||
};
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e, '加载排课数据失败'));
|
||||
return { classrooms: [], classes: [], matrix: {} };
|
||||
}
|
||||
return {
|
||||
classrooms: validatedLookups.classrooms,
|
||||
classes: validatedLookups.classes,
|
||||
matrix: typedMatrix,
|
||||
};
|
||||
},
|
||||
});
|
||||
const classrooms = fetchResult.classrooms;
|
||||
const classes = fetchResult.classes;
|
||||
const matrix = fetchResult.matrix;
|
||||
const loading = isLoading || isFetching;
|
||||
// RouteKeeper 保活页面切回时刷新排课数据
|
||||
useVisibleRefetch(['class-schedules']);
|
||||
|
||||
const saveMutation = useApiMutation(
|
||||
async (payload: Record<string, unknown>) =>
|
||||
@@ -498,18 +502,26 @@ const SchedulesPage: React.FC = () => {
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<ScheduleGrid
|
||||
loading={loading}
|
||||
viewMode={viewMode}
|
||||
classrooms={classrooms}
|
||||
filteredClassrooms={filteredClassrooms}
|
||||
displayMatrix={displayMatrix}
|
||||
weeks={weeks}
|
||||
monthStart={monthStart}
|
||||
monthScheduleMap={monthScheduleMap}
|
||||
onCellClick={handleCellClick}
|
||||
onDateClick={handleDateClick}
|
||||
/>
|
||||
{isError ? (
|
||||
<QueryErrorState
|
||||
title="排课数据加载失败"
|
||||
description="请检查网络后重试。"
|
||||
onRetry={() => void refetch()}
|
||||
/>
|
||||
) : (
|
||||
<ScheduleGrid
|
||||
loading={loading}
|
||||
viewMode={viewMode}
|
||||
classrooms={classrooms}
|
||||
filteredClassrooms={filteredClassrooms}
|
||||
displayMatrix={displayMatrix}
|
||||
weeks={weeks}
|
||||
monthStart={monthStart}
|
||||
monthScheduleMap={monthScheduleMap}
|
||||
onCellClick={handleCellClick}
|
||||
onDateClick={handleDateClick}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ScheduleModal
|
||||
open={modalOpen}
|
||||
@@ -558,6 +570,7 @@ const SchedulesPage: React.FC = () => {
|
||||
open={syncModalOpen}
|
||||
syncing={syncing}
|
||||
syncStatus={syncStatus}
|
||||
syncStatusError={syncStatusError}
|
||||
syncResult={syncResult}
|
||||
syncDateFrom={syncDateFrom}
|
||||
syncDays={syncDays}
|
||||
@@ -566,6 +579,7 @@ const SchedulesPage: React.FC = () => {
|
||||
setSyncModalOpen(false);
|
||||
setSyncResult(null);
|
||||
}}
|
||||
onRetryStatus={() => void openSyncModal()}
|
||||
onSync={handleSyncSchedule}
|
||||
onDateChange={setSyncDateFrom}
|
||||
onDaysChange={setSyncDays}
|
||||
|
||||
Reference in New Issue
Block a user