feat(admin): 深化 TanStack Query 数据层
- 新增 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 可命中缓存
This commit is contained in:
145
apps/admin/src/api/queryKeys.ts
Normal file
145
apps/admin/src/api/queryKeys.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* 统一 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;
|
||||
Reference in New Issue
Block a user