- 新增 queryKeys.ts 统一 QueryKey 工厂(覆盖全部模块,invalidate/refetch/prefetch 同源) - 新增 queryClient.ts 全局配置:retry 1、staleTime 30s、gcTime 5min、refetchOnWindowFocus false - 新增 useApiQuery:zod schema 校验 + 类型收敛 + select 转换,消除页面重复 validateResponse 样板 - useApiMutation 支持 onMutate/onSettled/context(乐观更新) - 示范迁移:Organizations(useApiQuery + 乐观更新归档/恢复)、Students(useApiQuery + queryKeys + 打开抽屉前 prefetch 档案聚合) - StudentProfileContent queryKey 统一为 queryKeys.archive.detail,prefetch 可命中缓存
146 lines
5.3 KiB
TypeScript
146 lines
5.3 KiB
TypeScript
/**
|
||
* 统一 QueryKey 工厂。
|
||
*
|
||
* 每个模块一个命名空间,key 由工厂函数生成:
|
||
* - 避免散落字符串字面量导致拼写不一致、缓存串扰
|
||
* - invalidate / refetch / prefetch 与 useQuery 使用同一来源,不会对不上
|
||
*
|
||
* 约定:
|
||
* - `all` 是该模块的「根 key」,用于整体失效(invalidateQueries 会匹配前缀)
|
||
* - 列表 key 按过滤条件展开;详情/子资源用固定段 + 参数
|
||
*/
|
||
export const queryKeys = {
|
||
students: {
|
||
all: ['students'] as const,
|
||
list: (filters: {
|
||
search?: string;
|
||
status?: string;
|
||
archived?: boolean;
|
||
organizationId?: number;
|
||
classId?: number;
|
||
teacherId?: number;
|
||
}) => ['students', filters] as const,
|
||
organizations: (canView: boolean) => ['students', 'organizations', canView] as const,
|
||
filterLookups: () => ['students', 'filter-lookups'] as const,
|
||
},
|
||
classes: {
|
||
all: ['classes'] as const,
|
||
list: (filters: { status?: string; type?: string; archived?: boolean }) =>
|
||
['classes', filters] as const,
|
||
detail: (id: number) => ['classes', 'detail', id] as const,
|
||
schedule: (id: number, dateRange: unknown) => ['classes', 'schedule', id, dateRange] as const,
|
||
attendanceSummary: (id: number, dateRange: unknown) =>
|
||
['classes', 'attendance-summary', id, dateRange] as const,
|
||
},
|
||
classSchedules: {
|
||
all: ['class-schedules'] as const,
|
||
list: (params: { startDate?: string; endDate?: string; classroomIds?: unknown[] }) =>
|
||
['class-schedules', params] as const,
|
||
},
|
||
classrooms: {
|
||
all: ['classrooms'] as const,
|
||
list: (archived: boolean) => ['classrooms', archived] as const,
|
||
},
|
||
classroomRentals: {
|
||
all: ['classroom-rentals'] as const,
|
||
list: (month?: string) => ['classroom-rentals', month] as const,
|
||
schedule: (year: number, month: number) =>
|
||
['classroom-rentals', 'schedule', year, month] as const,
|
||
meta: () => ['classroom-rentals', 'meta'] as const,
|
||
},
|
||
organizations: {
|
||
all: ['organizations'] as const,
|
||
list: () => ['organizations'] as const,
|
||
options: () => ['organizations', 'options'] as const,
|
||
},
|
||
rbac: {
|
||
all: ['rbac'] as const,
|
||
users: (archived: boolean) => ['rbac', 'users', archived] as const,
|
||
allUsers: () => ['rbac', 'users', 'all'] as const,
|
||
teachers: (params: { page: number; pageSize: number; search?: string }) =>
|
||
['rbac', 'teachers', params] as const,
|
||
teacherWorkspace: () => ['rbac', 'teacher-workspace'] as const,
|
||
roles: () => ['rbac', 'roles'] as const,
|
||
permissionTree: () => ['rbac', 'roles', 'permission-tree'] as const,
|
||
permissionsTree: () => ['rbac', 'permissions', 'tree'] as const,
|
||
},
|
||
expenses: {
|
||
all: ['expenses'] as const,
|
||
list: (archived: boolean) => ['expenses', archived ? 'archived' : 'active'] as const,
|
||
},
|
||
expenseTypes: {
|
||
map: () => ['expense-types', 'map'] as const,
|
||
},
|
||
expenseLookups: {
|
||
all: ['expense-lookups'] as const,
|
||
},
|
||
bills: {
|
||
all: ['bills'] as const,
|
||
list: (filters: { status?: string; expenseType?: string }) => ['bills', filters] as const,
|
||
},
|
||
wallets: {
|
||
all: ['wallets'] as const,
|
||
list: (params: { keyword?: string; debtOnly?: boolean; roomType?: string }) =>
|
||
['wallets', params] as const,
|
||
transactions: (studentId: number) => ['wallets', 'transactions', studentId] as const,
|
||
roomTypes: () => ['wallets', 'room-types'] as const,
|
||
},
|
||
deposits: {
|
||
all: ['deposits'] as const,
|
||
list: () => ['deposits'] as const,
|
||
eligible: (roomType?: string) => ['deposits', 'eligible', roomType] as const,
|
||
},
|
||
occupancies: {
|
||
all: ['occupancies'] as const,
|
||
list: (params: { viewMode?: string; dateRange?: unknown }) => ['occupancies', params] as const,
|
||
},
|
||
rooms: {
|
||
all: ['rooms'] as const,
|
||
overview: (archived: boolean) => ['rooms', 'overview', archived] as const,
|
||
visual: (params: { historical: boolean; asOf?: unknown }) =>
|
||
['rooms', 'visual', params] as const,
|
||
},
|
||
exams: {
|
||
all: ['exams'] as const,
|
||
detail: (id: number) => ['exams', 'detail', id] as const,
|
||
classes: () => ['exams', 'classes'] as const,
|
||
},
|
||
operationLogs: {
|
||
all: ['operation-logs'] as const,
|
||
list: (params: { page: number; pageSize: number; module?: string; dateRange?: unknown }) =>
|
||
['operation-logs', params] as const,
|
||
},
|
||
attendance: {
|
||
all: ['attendance'] as const,
|
||
workspace: () => ['attendance', 'workspace'] as const,
|
||
syncStatus: () => ['attendance', 'sync-status'] as const,
|
||
schedules: (classId: number, date: string) => ['attendance', 'schedules', classId, date] as const,
|
||
meta: {
|
||
periods: () => ['attendance', 'meta', 'periods'] as const,
|
||
classes: () => ['attendance', 'meta', 'classes'] as const,
|
||
alerts: () => ['attendance', 'meta', 'alerts'] as const,
|
||
},
|
||
},
|
||
attendanceDevices: {
|
||
all: ['attendance-devices'] as const,
|
||
},
|
||
dashboard: {
|
||
all: ['dashboard'] as const,
|
||
summary: (period: unknown) => ['dashboard', period] as const,
|
||
},
|
||
integration: {
|
||
config: () => ['integration', 'config'] as const,
|
||
},
|
||
sync: {
|
||
jinshujuRules: () => ['sync', 'jinshuju', 'rules'] as const,
|
||
},
|
||
archive: {
|
||
detail: (studentId: number) => ['archive', studentId] as const,
|
||
},
|
||
ai: {
|
||
config: () => ['ai', 'config'] as const,
|
||
},
|
||
} as const;
|
||
|
||
export type QueryKeys = typeof queryKeys;
|