import React, { useState, useRef, useEffect, useMemo } from 'react'; import './Select.css'; /** * Aurora Admin Select(React) * 对齐 select.json 契约: * - mode: single | multiple(multiple 属性) * - feature: searchable | grouped(groups 属性) * - state: default | open | disabled * 触发器样式同 Input;面板圆角4px / 阴影中 / 最大高度256px;选中项品牌色 + 勾选图标 * 受控: */ function flatten(groups) { return (groups || []).flatMap((g) => g.options); } export default function Select({ value, defaultValue = null, options = [], groups = null, multiple = false, searchable = false, disabled = false, placeholder = '请选择', className = '', onChange, ...rest }) { const isControlled = value !== undefined; const [inner, setInner] = useState(defaultValue); const [open, setOpen] = useState(false); const [keyword, setKeyword] = useState(''); const rootRef = useRef(null); const current = isControlled ? value : inner; const selectedArr = Array.isArray(current) ? current : []; const selectedVal = Array.isArray(current) ? null : current; const filteredOptions = useMemo(() => { if (!searchable || !keyword) return options; const q = keyword.trim().toLowerCase(); return options.filter((o) => String(o.label).toLowerCase().includes(q)); }, [options, searchable, keyword]); const filteredGroups = useMemo(() => { if (!groups) return []; if (!searchable || !keyword) return groups; const q = keyword.trim().toLowerCase(); return groups .map((g) => ({ label: g.label, options: g.options.filter((o) => String(o.label).toLowerCase().includes(q)) })) .filter((g) => g.options.length); }, [groups, searchable, keyword]); const isEmpty = groups ? filteredGroups.length === 0 : filteredOptions.length === 0; const flat = options.length ? options : flatten(groups); function labelOf(v) { const found = flat.find((o) => o.value === v); return found ? found.label : v; } function isSelected(v) { return multiple ? selectedArr.includes(v) : selectedVal === v; } function emit(next) { if (!isControlled) setInner(next); onChange && onChange(next); } function choose(o) { if (o.disabled) return; if (multiple) { const next = selectedArr.includes(o.value) ? selectedArr.filter((x) => x !== o.value) : [...selectedArr, o.value]; emit(next); } else { emit(o.value); setOpen(false); } } function remove(v) { emit(selectedArr.filter((x) => x !== v)); } useEffect(() => { function onDocClick(e) { if (rootRef.current && !rootRef.current.contains(e.target)) setOpen(false); } document.addEventListener('click', onDocClick); return () => document.removeEventListener('click', onDocClick); }, []); const classes = ['aa-select', open ? 'is-open' : '', disabled ? 'is-disabled' : '', className] .filter(Boolean) .join(' '); return (
!disabled && setOpen((o) => !o)} tabIndex={0}> {multiple ? ( selectedArr.length ? ( selectedArr.map((v) => ( {labelOf(v)} { e.stopPropagation(); remove(v); }}>✕ )) ) : ( {placeholder} ) ) : selectedVal == null ? ( {placeholder} ) : ( {labelOf(selectedVal)} )} ▾
{open && (
{searchable && (
setKeyword(e.target.value)} onClick={(e) => e.stopPropagation()} />
)} {groups ? ( filteredGroups.map((g) => (
{g.label}
{g.options.map((o) => (
choose(o)} > {o.label}✓
))}
)) ) : ( filteredOptions.map((o) => (
choose(o)} > {o.label}✓
)) )} {isEmpty &&
无匹配选项
}
)}
); } /* 用法示例: import Select from './Select'; const cities = [{ value: 'bj', label: '北京' }, { value: 'sh', label: '上海' }]; */