Files
OpenFlare/frontend/contexts/admin-users-context.tsx
2026-07-13 15:44:03 +08:00

337 lines
9.7 KiB
TypeScript

'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<void>;
refresh: () => Promise<void>;
getUserDetail: (id: string) => Promise<AdminUser>;
updateUserStatus: (user: AdminUser) => Promise<void>;
createUser: (req: CreateUserRequest) => Promise<AdminUser>;
updateUser: (id: string, req: UpdateUserRequest) => Promise<void>;
deleteUser: (user: AdminUser) => Promise<void>;
}
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes cache
const AdminUsersContext = createContext<AdminUsersContextState | null>(null);
export function AdminUsersProvider({
children,
}: {
children: React.ReactNode;
}) {
// State
const [users, setUsers] = useState<AdminUser[]>([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(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<string, { data: AdminUser[]; total: number; timestamp: number }>
>({});
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 (
<AdminUsersContext.Provider value={value}>
{children}
</AdminUsersContext.Provider>
);
}
export function useAdminUsers() {
const context = useContext(AdminUsersContext);
if (!context) {
throw new Error('useAdminUsers must be used within an AdminUsersProvider');
}
return context;
}