'use client'; import * as React from 'react'; import { createContext, useCallback, useContext, useEffect, useRef, useState, } from 'react'; import { toast } from 'sonner'; import type { AdminUser, CreateUserRequest, ListUsersRequest, UpdateUserRequest, } from '@/lib/services/admin'; import services from '@/lib/services'; /** 用户列表查询参数 */ export interface UserQueryParams { page: number; page_size: number; user_id?: string; username?: string; email?: string; status?: 'all' | 'active' | 'inactive'; } /** Admin Users Context State Interface */ interface AdminUsersContextState { users: AdminUser[]; total: number; loading: boolean; error: Error | null; // Params page: number; pageSize: number; searchUserId: string; searchUsername: string; searchEmail: string; statusFilter: 'all' | 'active' | 'inactive'; // Actions setPage: (page: number) => void; setPageSize: (size: number) => void; setSearchUserId: (userId: string) => void; setSearchUsername: (username: string) => void; setSearchEmail: (email: string) => void; setStatusFilter: (status: 'all' | 'active' | 'inactive') => void; fetchUsers: (force?: boolean) => Promise; refresh: () => Promise; getUserDetail: (id: string) => Promise; updateUserStatus: (user: AdminUser) => Promise; createUser: (req: CreateUserRequest) => Promise; updateUser: (id: string, req: UpdateUserRequest) => Promise; deleteUser: (user: AdminUser) => Promise; } const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes cache const AdminUsersContext = createContext(null); export function AdminUsersProvider({ children, }: { children: React.ReactNode; }) { // State const [users, setUsers] = useState([]); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); // Query Params State const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(20); const [searchUserId, setSearchUserId] = useState(''); const [searchUsername, setSearchUsername] = useState(''); const [searchEmail, setSearchEmail] = useState(''); const [statusFilter, setStatusFilter] = useState< 'all' | 'active' | 'inactive' >('all'); const [debouncedSearchUserId, setDebouncedSearchUserId] = useState(''); const [debouncedSearchUsername, setDebouncedSearchUsername] = useState(''); const [debouncedSearchEmail, setDebouncedSearchEmail] = useState(''); // Cache const cacheRef = useRef< Record >({}); const latestRequestIdRef = useRef(0); // Debounce search useEffect(() => { const timer = setTimeout(() => { setDebouncedSearchUserId(searchUserId); setDebouncedSearchUsername(searchUsername); setDebouncedSearchEmail(searchEmail); if ( searchUserId !== debouncedSearchUserId || searchUsername !== debouncedSearchUsername || searchEmail !== debouncedSearchEmail ) { setPage(1); // Reset to page 1 on search change } }, 500); return () => clearTimeout(timer); }, [ searchUserId, searchUsername, searchEmail, debouncedSearchUserId, debouncedSearchUsername, debouncedSearchEmail, ]); const generateCacheKey = (params: UserQueryParams) => { return JSON.stringify(params); }; const fetchUsers = useCallback( async (force = false) => { const params: UserQueryParams = { page, page_size: pageSize, user_id: debouncedSearchUserId || undefined, username: debouncedSearchUsername || undefined, email: debouncedSearchEmail || undefined, status: statusFilter, }; const cacheKey = generateCacheKey(params); const now = Date.now(); const cached = cacheRef.current[cacheKey]; // Use cache if valid and not forced if (!force && cached && now - cached.timestamp < CACHE_DURATION) { setUsers(cached.data); setTotal(cached.total); setLoading(false); setError(null); return; } setLoading(true); setError(null); const requestId = ++latestRequestIdRef.current; try { // Current API doesn't support status filter in listUsers? // The previous implementation did client-side filtering. // Ideally backend supports it. If not, we fetch and filter? // "services.adminUser.listUsers" in previous code only took page, page_size, username. // So status filter was client side. // However, caching client-filtered result is tricky if we don't have all data. // But previous implementation fetched *paged* data then filtered? No, that would be wrong (filtering 20 items might leave 0). // Let's check previous implementation: // "const data = await services.adminUser.listUsers(...) ... let filteredUsers = data.users ... if (statusFilter...) filtered..." // This means filtering happens ONLY on the current page of results! This is technically buggy if the user wants "all inactive users". // But preserving that behavior for now. const requestParams: ListUsersRequest = { page, page_size: pageSize, user_id: debouncedSearchUserId || undefined, username: debouncedSearchUsername || undefined, email: debouncedSearchEmail || undefined, }; const response = await services.adminUser.listUsers(requestParams); if (requestId !== latestRequestIdRef.current) return; let resultUsers = response.users; // Client-side filtering as per previous logic (though restrictive) if (statusFilter === 'active') { resultUsers = resultUsers.filter((u) => u.is_active); } else if (statusFilter === 'inactive') { resultUsers = resultUsers.filter((u) => !u.is_active); } setUsers(resultUsers); setTotal(response.total); // Note: Total might be inaccurate if we filter client side! But per previous code... // Update cache cacheRef.current[cacheKey] = { data: resultUsers, total: response.total, timestamp: now, }; } catch (err) { if (requestId !== latestRequestIdRef.current) return; setError( err instanceof Error ? err : new Error('Failed to fetch users'), ); } finally { if (requestId === latestRequestIdRef.current) { setLoading(false); } } }, [ page, pageSize, debouncedSearchUserId, debouncedSearchUsername, debouncedSearchEmail, statusFilter, ], ); // Removed auto-fetch useEffect. Consumer (UsersManager) should trigger fetch. const refresh = async () => { await fetchUsers(true); }; const updateUserStatus = async (user: AdminUser) => { const originalStatus = user.is_active; // Optimistic update setUsers((prev) => prev.map((u) => u.id === user.id ? { ...u, is_active: !u.is_active } : u, ), ); // Clear cache because data changed cacheRef.current = {}; try { await services.adminUser.updateUserStatus(user.id, { is_active: !user.is_active, }); toast.success( `已${!user.is_active ? '启用' : '禁用'}用户 ${user.username}`, ); } catch { // Revert on error setUsers((prev) => prev.map((u) => u.id === user.id ? { ...u, is_active: originalStatus } : u, ), ); toast.error('更新状态失败'); } }; const getUserDetail = async (id: string) => { return services.adminUser.getUser(id); }; const createUser = async (req: CreateUserRequest) => { try { const newUser = await services.adminUser.createUser(req); setUsers((prev) => [newUser, ...prev]); setTotal((prev) => prev + 1); // Clear cache because data changed cacheRef.current = {}; toast.success(`已成功创建用户 ${newUser.username}`); return newUser; } catch (err) { toast.error(err instanceof Error ? err.message : '创建用户失败'); throw err; } }; const deleteUser = async (user: AdminUser) => { try { await services.adminUser.deleteUser(user.id); setUsers((prev) => prev.filter((u) => u.id !== user.id)); setTotal((prev) => Math.max(0, prev - 1)); cacheRef.current = {}; toast.success(`已删除用户 ${user.username}`); } catch (err) { toast.error(err instanceof Error ? err.message : '删除用户失败'); throw err; } }; const updateUser = async (id: string, req: UpdateUserRequest) => { try { await services.adminUser.updateUser(id, req); setUsers((prev) => prev.map((u) => (u.id === id ? { ...u, ...req } : u))); cacheRef.current = {}; toast.success('更新用户信息成功'); } catch (err) { toast.error(err instanceof Error ? err.message : '更新用户信息失败'); throw err; } }; const value = { users, total, loading, error, page, pageSize, searchUserId, searchUsername, searchEmail, statusFilter, setPage, setPageSize, setSearchUserId, setSearchUsername, setSearchEmail, setStatusFilter, fetchUsers, refresh, getUserDetail, updateUserStatus, createUser, updateUser, deleteUser, }; return ( {children} ); } export function useAdminUsers() { const context = useContext(AdminUsersContext); if (!context) { throw new Error('useAdminUsers must be used within an AdminUsersProvider'); } return context; }