'use client' import { useEffect, useId, useMemo, useRef, useState } from 'react' import { pinyin } from 'pinyin-pro' import type { WorkReviewReviewerOption } from '@/types/work-review' export function WorkReviewReviewerSelect({ options, value, disabled, loading, onChange, }: { options: WorkReviewReviewerOption[] value: string disabled: boolean loading: boolean onChange: (employeeId: string) => void }) { const [open, setOpen] = useState(false) const [query, setQuery] = useState('') const [highlightedIndex, setHighlightedIndex] = useState(0) const containerRef = useRef(null) const inputRef = useRef(null) const listboxId = useId() const selected = options.find((option) => option.id === value) || null const isDisabled = disabled || loading const indexedOptions = useMemo(() => options.map((option) => ({ option, searchable: [ option.name, option.jobNumber, option.departmentName, option.positionTitle, pinyin(option.name, { toneType: 'none' }), pinyin(option.name, { pattern: 'first', toneType: 'none' }), ].filter(Boolean).join(' ').toLowerCase(), })), [options]) const filteredOptions = useMemo(() => { const keyword = query.trim().toLowerCase() if (!keyword) return indexedOptions.map((item) => item.option) return indexedOptions .filter((item) => item.searchable.includes(keyword)) .map((item) => item.option) }, [indexedOptions, query]) useEffect(() => { setHighlightedIndex((current) => Math.min(current, Math.max(0, filteredOptions.length - 1))) }, [filteredOptions.length]) useEffect(() => { const handleOutsideClick = (event: MouseEvent) => { if (containerRef.current && !containerRef.current.contains(event.target as Node)) { setOpen(false) setQuery('') } } document.addEventListener('mousedown', handleOutsideClick) return () => document.removeEventListener('mousedown', handleOutsideClick) }, []) const selectOption = (option: WorkReviewReviewerOption) => { onChange(option.id) setQuery('') setOpen(false) setHighlightedIndex(0) } const handleKeyDown = (event: React.KeyboardEvent) => { if (isDisabled) return if (!open && (event.key === 'Enter' || event.key === 'ArrowDown')) { event.preventDefault() setOpen(true) return } if (!open) return if (event.key === 'ArrowDown') { event.preventDefault() setHighlightedIndex((current) => Math.min(current + 1, filteredOptions.length - 1)) } else if (event.key === 'ArrowUp') { event.preventDefault() setHighlightedIndex((current) => Math.max(current - 1, 0)) } else if (event.key === 'Enter' && filteredOptions[highlightedIndex]) { event.preventDefault() selectOption(filteredOptions[highlightedIndex]) } else if (event.key === 'Escape') { event.preventDefault() event.stopPropagation() setOpen(false) setQuery('') } } return (
{ if (!event.currentTarget.contains(event.relatedTarget as Node | null)) { setOpen(false) setQuery('') } }} >
setQuery(event.target.value)} onFocus={() => { if (!isDisabled) { setOpen(true) setHighlightedIndex(0) } }} onKeyDown={handleKeyDown} disabled={isDisabled} placeholder={loading ? '正在加载人员...' : '输入姓名、花名、工号或部门搜索'} autoComplete="off" role="combobox" aria-autocomplete="list" aria-expanded={open && !isDisabled} aria-controls={listboxId} aria-activedescendant={open && filteredOptions[highlightedIndex] ? `${listboxId}-${filteredOptions[highlightedIndex].id}` : undefined} aria-busy={loading} className="h-11 w-full rounded border border-[#cfd6e1] bg-white px-3 pr-20 text-[14px] outline-none placeholder:text-[#667085] focus:border-[#0050cb] focus:ring-2 focus:ring-[#dcecff] disabled:cursor-not-allowed disabled:bg-[#f2f4f7]" />
{value && !isDisabled && ( )}
{selected && !open && (

{[selected.departmentName, selected.positionTitle, selected.jobNumber].filter(Boolean).join(' · ') || '考核人员档案'}

)} {open && !isDisabled && (
{filteredOptions.length === 0 ? (
没有找到匹配的评审人
) : filteredOptions.map((option, index) => { const metadata = [option.departmentName, option.positionTitle, option.jobNumber].filter(Boolean).join(' · ') const active = index === highlightedIndex const checked = option.id === value return ( ) })}
)}
) }