/** * MotherShip 登录后一级:地址键入联想下拉(Axel suggest) */ "use client"; import { useEffect, useId, useRef, useState } from "react"; import { CaretRight, MagnifyingGlass, X } from "@phosphor-icons/react"; import { hostSuggestMothershipAddress } from "@/lib/frontend/api-client"; import type { MothershipAddressCandidate } from "@/lib/frontend/types"; import { LoadingSpinner } from "@/components/ui/loading-spinner"; const DEBOUNCE_MS = 300; const MIN_QUERY_LEN = 3; export interface MothershipAddressSuggestFieldProps { label: string; required?: boolean; placeholder?: string; disabled?: boolean; /** 必填未满足时高亮边框 */ invalid?: boolean; testId?: string; customerId: string; apiBaseUrl?: string; serviceToken?: string; value: string; confirmed: MothershipAddressCandidate | null; onQueryChange: (query: string) => void; onConfirm: (candidate: MothershipAddressCandidate | null) => void; } function splitDisplay(label: string): { primary: string; secondary: string } { const parts = label.split(",").map((p) => p.trim()).filter(Boolean); if (parts.length <= 1) { return { primary: label, secondary: "" }; } return { primary: parts[0]!, secondary: parts.slice(1).join(", "), }; } export function MothershipAddressSuggestField({ label, required, placeholder = "按邮编、地址或公司名搜索", disabled, invalid, testId, customerId, apiBaseUrl = "", serviceToken, value, confirmed, onQueryChange, onConfirm, }: MothershipAddressSuggestFieldProps) { const listId = useId(); const wrapRef = useRef(null); const [open, setOpen] = useState(false); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [candidates, setCandidates] = useState( [], ); const reqSeq = useRef(0); useEffect(() => { const onDoc = (e: MouseEvent) => { if (!wrapRef.current?.contains(e.target as Node)) { setOpen(false); } }; document.addEventListener("mousedown", onDoc); return () => document.removeEventListener("mousedown", onDoc); }, []); useEffect(() => { const q = value.trim(); if (q.length < MIN_QUERY_LEN) { setCandidates([]); setError(null); setLoading(false); return; } // 已确认且文案未变:不重复请求 if (confirmed && confirmed.display_label === value.trim()) { return; } const seq = ++reqSeq.current; setLoading(true); setError(null); const timer = window.setTimeout(() => { void (async () => { try { const res = await hostSuggestMothershipAddress( apiBaseUrl, serviceToken, customerId, q, ); if (seq !== reqSeq.current) return; if (res.code !== 0 || !res.data) { setCandidates([]); setError(res.message || "地址联想失败"); return; } setCandidates( res.data.candidates.filter( (c) => c.selectable !== false && Boolean(c.option_id?.trim()) && Boolean( (c.display_label || c.formatted_address || "").trim(), ), ), ); setOpen(true); } catch (err) { if (seq !== reqSeq.current) return; setCandidates([]); setError(err instanceof Error ? err.message : "地址联想失败"); } finally { if (seq === reqSeq.current) { setLoading(false); } } })(); }, DEBOUNCE_MS); return () => window.clearTimeout(timer); }, [value, apiBaseUrl, serviceToken, customerId, confirmed]); const handleInput = (next: string) => { onQueryChange(next); if (confirmed) { onConfirm(null); } setOpen(true); }; const handleClear = () => { onQueryChange(""); onConfirm(null); setCandidates([]); setError(null); setOpen(false); }; const handlePick = (item: MothershipAddressCandidate) => { onQueryChange(item.display_label || item.formatted_address); onConfirm(item); setOpen(false); setError(null); }; return (
handleInput(e.target.value)} onFocus={() => { if (candidates.length > 0 || value.trim().length >= MIN_QUERY_LEN) { setOpen(true); } }} placeholder={placeholder} role="combobox" aria-expanded={open} aria-controls={listId} aria-autocomplete="list" aria-invalid={invalid || undefined} className={[ "h-10 w-full rounded-md border bg-surface pl-10 pr-16 text-sm focus:outline-none focus:ring-2 disabled:opacity-60", invalid ? "border-[#EF4444] focus:border-[#EF4444] focus:ring-[#EF4444]/20" : "border-border focus:border-[#1890FF] focus:ring-[#1890FF]/20", ].join(" ")} />
{loading && ( )} {value && !disabled && ( )}
{confirmed && (

已选择 MotherShip 地址:{confirmed.display_label}

)} {error &&

{error}

} {open && !disabled && value.trim().length >= MIN_QUERY_LEN && (

搜索结果

{loading && candidates.length === 0 && (

正在搜索…

)} {!loading && candidates.length === 0 && !error && (

未找到匹配地址,请调整关键词

)} {candidates.map((item) => { const { primary, secondary } = splitDisplay( item.display_label || item.formatted_address, ); return ( ); })}
)}
); }