StaffPicker.tsx 4.94 KB
import { useEffect, useRef, useState } from "react";
import { SearchOutlined } from "@ant-design/icons";
import { searchStaff, type StaffSearchVO } from "@/api/staff";

interface Props {
  value: string;
  onChange: (name: string, staffId: number | null) => void;
  disabled?: boolean;
  required?: boolean;
  placeholder?: string;
}

const fieldControl: React.CSSProperties = {
  flex: 1,
  minWidth: 0,
  height: "var(--input-h)",
  border: "1px solid var(--border-input)",
  background: "var(--bg-input)",
  padding: "0 26px 0 6px",
  fontSize: 12,
  color: "var(--text)",
  borderRadius: 0,
  outline: "none",
  fontFamily: "inherit",
};

export default function StaffPicker({
  value,
  onChange,
  disabled,
  required,
  placeholder,
}: Props) {
  const [open, setOpen] = useState(false);
  const [items, setItems] = useState<StaffSearchVO[]>([]);
  const [loading, setLoading] = useState(false);
  const wrapRef = useRef<HTMLDivElement>(null);
  const debounce = useRef<number | null>(null);

  useEffect(() => {
    function onDocClick(e: MouseEvent) {
      if (wrapRef.current && !wrapRef.current.contains(e.target as Node)) {
        setOpen(false);
      }
    }
    document.addEventListener("mousedown", onDocClick);
    return () => document.removeEventListener("mousedown", onDocClick);
  }, []);

  const fetchSuggestions = async (kw: string) => {
    setLoading(true);
    try {
      const list = await searchStaff(kw, 20);
      setItems(list);
    } catch {
      setItems([]);
    } finally {
      setLoading(false);
    }
  };

  const onFocus = () => {
    if (disabled) return;
    setOpen(true);
    void fetchSuggestions(value);
  };

  const onInputChange = (next: string) => {
    // Typing clears any previously bound staff id; user must re-select
    onChange(next, null);
    setOpen(true);
    if (debounce.current) window.clearTimeout(debounce.current);
    debounce.current = window.setTimeout(() => fetchSuggestions(next), 200);
  };

  const select = (s: StaffSearchVO) => {
    onChange(s.sStaffName, s.iIncrement);
    setOpen(false);
  };

  return (
    <div
      ref={wrapRef}
      style={{ position: "relative", flex: 1, minWidth: 0, display: "flex", alignItems: "center" }}
    >
      <input
        type="text"
        value={value}
        onChange={(e) => onInputChange(e.target.value)}
        onFocus={onFocus}
        disabled={disabled}
        placeholder={placeholder}
        style={{
          ...fieldControl,
          ...(disabled ? { background: "var(--bg-disabled)", color: "var(--text-muted)" } : {}),
          ...(required && !disabled ? { background: "#d4e8f7" } : {}),
        }}
      />
      <span
        style={{
          position: "absolute",
          right: 6,
          top: "50%",
          transform: "translateY(-50%)",
          color: "var(--text-faint)",
          pointerEvents: "none",
          fontSize: 12,
        }}
      >
        <SearchOutlined />
      </span>
      {open && !disabled && (
        <div
          style={{
            position: "absolute",
            top: "calc(100% + 1px)",
            left: 0,
            right: 0,
            zIndex: 50,
            background: "#fff",
            border: "1px solid var(--border-input)",
            boxShadow: "0 2px 8px rgba(0,0,0,0.08)",
            maxHeight: 240,
            overflow: "auto",
          }}
        >
          {loading && (
            <div style={{ padding: "8px 12px", fontSize: 12, color: "var(--text-faint)" }}>
              加载中…
            </div>
          )}
          {!loading && items.length === 0 && (
            <div style={{ padding: "8px 12px", fontSize: 12, color: "var(--text-faint)" }}>
              没有匹配的职员
            </div>
          )}
          {!loading &&
            items.map((s) => (
              <div
                key={s.iIncrement}
                onMouseDown={(e) => {
                  e.preventDefault();
                  select(s);
                }}
                style={{
                  padding: "6px 12px",
                  fontSize: 12,
                  cursor: "pointer",
                  display: "flex",
                  alignItems: "center",
                  gap: 8,
                  borderBottom: "1px solid #f0f2f5",
                }}
                onMouseEnter={(e) => {
                  e.currentTarget.style.background = "var(--bg-row-hover)";
                }}
                onMouseLeave={(e) => {
                  e.currentTarget.style.background = "transparent";
                }}
              >
                <span style={{ color: "var(--text)" }}>{s.sStaffName}</span>
                <span style={{ color: "var(--text-faint)", fontSize: 11 }} className="mono">
                  {s.sStaffNo}
                </span>
                <span style={{ flex: 1 }} />
                <span style={{ color: "var(--text-muted)", fontSize: 11 }}>
                  {s.sDepartment ?? ""}
                </span>
              </div>
            ))}
        </div>
      )}
    </div>
  );
}