import { useCallback, useEffect, useRef, useState } from "react"; import { ArrowLeft, Search } from "lucide-react"; import { type Employee } from "../../services/api"; import { Button } from "~/components/ui/button"; import { dialogSecondaryButtonClass } from "~/components/common/AppFormDialog"; import { primaryGradientButtonClass } from "~/components/common/buttonClasses"; import { useDebouncedValue } from "./useDebouncedValue"; export type LoadUsersFn = (input: { q: string; cursor: string | null; limit: number; }) => Promise<{ items: Employee[]; hasMore: boolean; nextCursor: string | null; }>; export function MemberAddPanel({ existingMemberIds, saving, loadUsers, onBack, onAddMembers, }: { existingMemberIds: string[]; saving: boolean; loadUsers: LoadUsersFn; onBack: () => void; onAddMembers: (userIds: string[]) => Promise | void; }) { const [keyword, setKeyword] = useState(""); const debouncedKeyword = useDebouncedValue(keyword, 300); const [users, setUsers] = useState([]); const [selectedIds, setSelectedIds] = useState([]); const [selectedCache, setSelectedCache] = useState>({}); const [loading, setLoading] = useState(false); const [loadingMore, setLoadingMore] = useState(false); const [hasMore, setHasMore] = useState(false); const nextCursorRef = useRef(null); const requestIdRef = useRef(0); const fetchPage = useCallback( async (cursor: string | null, append: boolean) => { const requestId = ++requestIdRef.current; if (append) setLoadingMore(true); else setLoading(true); try { const result = await loadUsers({ q: debouncedKeyword, cursor, limit: 10, }); if (requestId !== requestIdRef.current) return; setUsers((current) => (append ? [...current, ...result.items] : result.items)); setHasMore(result.hasMore); nextCursorRef.current = result.nextCursor; } finally { if (requestId === requestIdRef.current) { setLoading(false); setLoadingMore(false); } } }, [debouncedKeyword, loadUsers], ); useEffect(() => { nextCursorRef.current = null; void fetchPage(null, false); }, [debouncedKeyword, fetchPage]); const toggleUser = (user: Employee) => { if (existingMemberIds.includes(user.user_id)) return; setSelectedCache((cache) => ({ ...cache, [user.user_id]: user })); setSelectedIds((ids) => ids.includes(user.user_id) ? ids.filter((id) => id !== user.user_id) : [...ids, user.user_id], ); }; const removeSelected = (userId: string) => { setSelectedIds((ids) => ids.filter((id) => id !== userId)); }; const loadMore = () => { if (loadingMore || loading || !hasMore || !nextCursorRef.current) return; void fetchPage(nextCursorRef.current, true); }; const submitAdd = async () => { if (selectedIds.length === 0) return; try { await onAddMembers(selectedIds); onBack(); } catch { // stay on add panel; page already toasts } }; const selectedUsers = selectedIds .map((id) => selectedCache[id] ?? users.find((u) => u.user_id === id)) .filter((u): u is Employee => Boolean(u)); return ( <>
setKeyword(event.target.value)} placeholder="搜索姓名 / 账号" style={{ flex: 1, border: 0, outline: "none", background: "transparent", fontSize: 12, color: "#20364c", }} />
{selectedUsers.length > 0 && (
{selectedUsers.map((user) => ( {user.display_name} ))}
)}
{loading && users.length === 0 ? (

加载中…

) : users.length === 0 ? (

{debouncedKeyword.trim() ? "未找到匹配用户" : "暂无可选用户"}

) : ( users.map((user) => { const isExisting = existingMemberIds.includes(user.user_id); const isSelected = selectedIds.includes(user.user_id); return ( ); }) )} {hasMore && (
)}
); }