import React, { useRef, useState, useEffect, useMemo } from 'react'; import './ListPicker.css'; /** * Kole UI ListPicker(React) * 对齐 组件7.txt ListPicker 规范:下拉列表、多选、分组、搜索、全选、标签 * * props: * - value / defaultValue : 选中 value 数组 * - options : 扁平 [{value,label}] 或 分组 [{label, children}] * - filterable * - placeholder * - onChange(keys) */ export default function ListPicker({ value, defaultValue = [], options = [], filterable = true, placeholder = '请选择', onChange }) { const rootRef = useRef(null); const [inner, setInner] = useState(defaultValue); const current = value !== undefined ? value : inner; const [open, setOpen] = useState(false); const [query, setQuery] = useState(''); const isGrouped = options.length && options[0] && options[0].children; const flat = isGrouped ? options.flatMap(g => g.children) : options; const labelMap = Object.fromEntries(flat.map(o => [o.value, o.label])); const flatVisible = useMemo(() => flat.filter(o => o.label.includes(query)), [flat, query]); const visibleGroups = useMemo(() => { if (!isGrouped) return [{ children: flatVisible }]; return options .map(g => ({ label: g.label, children: g.children.filter(o => o.label.includes(query)) })) .filter(g => g.children.length); }, [options, flatVisible, query]); const commit = (next) => { if (value === undefined) setInner(next); onChange && onChange(next); }; const toggleOpt = (o) => { if (o.disabled) return; commit(current.includes(o.value) ? current.filter(x => x !== o.value) : [...current, o.value]); }; const remove = (v) => commit(current.filter(x => x !== v)); const selectAll = () => commit([...new Set([...current, ...flatVisible.map(o => o.value)])]); useEffect(() => { const onDoc = (e) => { if (rootRef.current && !rootRef.current.contains(e.target)) { setOpen(false); setQuery(''); } }; document.addEventListener('click', onDoc, true); return () => document.removeEventListener('click', onDoc, true); }, []); return (
{ setOpen(!open); if (open) setQuery(''); }}> {current.length ? current.map(v => ( {labelMap[v] || v} { e.stopPropagation(); remove(v); }}>× )) : {placeholder}} ▾
{open && (
{filterable && (
setQuery(e.target.value)} placeholder="搜索" />
)}
全选
{visibleGroups.map((g, gi) => (
{g.label &&
{g.label}
} {g.children.map(o => (
toggleOpt(o)} > {current.includes(o.value) ? '✓' : ''}{o.label}
))}
))} {!flatVisible.length &&
无匹配项
}
)}
); } /* 用法示例: import ListPicker from './ListPicker'; const options = [{ label: '前端', children: [{ value: 'vue', label: 'Vue' }, { value: 'react', label: 'React' }] }]; */