"use client"; import { useCallback, useEffect, useState } from "react"; export async function readApiResponse( response: Response, fallback: string, ) { const text = await response.text(); const contentType = response.headers.get("content-type") ?? ""; if (!contentType.includes("json")) { if (response.status === 403) { throw new Error("请求被安全网关拦截,请刷新页面后重试"); } throw new Error(`${fallback}(服务返回异常,HTTP ${response.status})`); } try { return JSON.parse(text) as T & { error?: string }; } catch { throw new Error(`${fallback}(服务返回的数据无法解析)`); } } export type InternalWecomContact = { userid: string; name: string; departmentNames: string[]; }; let internalWecomContactsCache: { contacts: InternalWecomContact[]; fetchedAt: number; } | null = null; const INTERNAL_WECOM_CONTACTS_TTL_MS = 5 * 60 * 1000; export function WecomUserPickerModal({ mode, selected, onClose, onConfirm, }: { mode: "single" | "multi"; selected: string[]; onClose: () => void; onConfirm: (userids: string[]) => void; }) { const [contacts, setContacts] = useState( internalWecomContactsCache?.contacts ?? [], ); const [fetchedAt, setFetchedAt] = useState(0); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [search, setSearch] = useState(""); const [checked, setChecked] = useState(selected.filter(Boolean)); const loadContacts = useCallback(async (force = false) => { const snapshot = internalWecomContactsCache; if ( !force && snapshot && Date.now() - snapshot.fetchedAt <= INTERNAL_WECOM_CONTACTS_TTL_MS ) { setContacts(snapshot.contacts); setFetchedAt(snapshot.fetchedAt); setLoading(false); return; } setLoading(true); setError(""); try { const response = await fetch("/api/action", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "wecom_list_internal_contacts" }), }); const parsed = await readApiResponse<{ contacts?: InternalWecomContact[]; error?: string; }>(response, "获取企业微信通讯录失败"); if (!response.ok) { throw new Error(parsed.error || "获取企业微信通讯录失败"); } const list = Array.isArray(parsed.contacts) ? parsed.contacts : []; internalWecomContactsCache = { contacts: list, fetchedAt: Date.now() }; setContacts(list); setFetchedAt(internalWecomContactsCache.fetchedAt); } catch (reason) { setError( reason instanceof Error ? reason.message : "获取企业微信通讯录失败", ); } finally { setLoading(false); } }, []); useEffect(() => { const timer = window.setTimeout(() => { void loadContacts(); }, 0); return () => window.clearTimeout(timer); }, [loadContacts]); const keyword = search.trim().toLowerCase(); const filteredContacts = keyword.length === 0 ? contacts : contacts.filter((contact) => `${contact.name} ${contact.userid}`.toLowerCase().includes(keyword), ); const toggleChecked = (userid: string) => { setChecked((previous) => previous.includes(userid) ? previous.filter((item) => item !== userid) : [...previous, userid], ); }; return (
event.stopPropagation()} >

选择企业微信成员

从企业微信通讯录(自建应用可见范围)中挑选, {mode === "multi" ? "勾选后点击确定" : "点击成员即填入"}。

setSearch(event.target.value)} disabled={loading} /> {fetchedAt > 0 && !loading && ( 缓存于 {new Date(fetchedAt).toLocaleTimeString()}(5 分钟内复用) )}
{error && (

{error} 可关闭此弹窗,直接在输入框手动填写 userid。

)}
{loading && contacts.length === 0 ? (
正在拉取企业微信通讯录…
) : contacts.length === 0 ? (
{error ? "拉取失败,请检查应用配置(通讯录权限 / 可信 IP)后重试" : "未拉取到通讯录成员——仅显示自建应用可见范围内的成员,也可手动填写 userid"}
) : filteredContacts.length === 0 ? (
没有匹配的成员
) : ( filteredContacts.map((contact) => { const isSelected = mode === "multi" ? checked.includes(contact.userid) : selected.includes(contact.userid); const rowBody = ( <> {contact.name || contact.userid} {contact.departmentNames.join(" / ") || "—"} {contact.userid} ); return mode === "multi" ? ( ) : ( ); }) )}
{mode === "multi" && (
)}
); }