import React, { useRef, useState, useEffect, useMemo } from 'react'; import './Cascader.css'; /** * Kole UI Cascader(React) * 对齐 组件7.txt Cascader 规范:多级下拉、搜索、路径显示、多选 * * props: * - value / defaultValue : 单选为路径数组(或 null),多选为路径数组的数组 * - options : [{ value, label, children? }] * - multiple * - filterable * - placeholder * - onChange(value) */ export default function Cascader({ value, defaultValue = [], options = [], multiple = false, 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 [activePath, setActivePath] = useState([]); const [query, setQuery] = useState(''); const pathLabels = (values) => { let nodes = options, labels = []; for (const v of (values || [])) { const n = nodes.find(x => x.value === v); if (!n) break; labels.push(n.label); nodes = n.children || []; } return labels; }; const columns = useMemo(() => { const cols = []; let nodes = options; for (let lv = 0; lv <= activePath.length; lv++) { cols.push(nodes); if (lv < activePath.length) { const next = nodes.find(x => x.value === activePath[lv]); nodes = next.children || []; } else break; } return cols; }, [options, activePath]); const allLeaves = useMemo(() => { const out = []; (function walk(list, acc) { list.forEach(n => { const p = [...acc, n.value]; if (n.children && n.children.length) walk(n.children, p); else out.push(p); }); })(options, []); return out; }, [options]); const results = query ? allLeaves.filter(p => pathLabels(p).join('/').includes(query)) : []; const commit = (next) => { if (value === undefined) setInner(next); onChange && onChange(next); }; const choose = (n, lv) => { const full = [...activePath.slice(0, lv), n.value]; if (n.children && n.children.length) setActivePath(full); else if (multiple) { const k = JSON.stringify(full); const arr = Array.isArray(current) ? current : []; commit(arr.some(p => JSON.stringify(p) === k) ? arr.filter(p => JSON.stringify(p) !== k) : [...arr, full]); } else { commit(full); setOpen(false); } }; const pickResult = (p) => { if (multiple) { const k = JSON.stringify(p); const arr = Array.isArray(current) ? current : []; commit(arr.some(x => JSON.stringify(x) === k) ? arr.filter(x => JSON.stringify(x) !== k) : [...arr, p]); } else { commit(p); setOpen(false); } }; const isChecked = (p) => Array.isArray(current) && current.some(x => JSON.stringify(x) === JSON.stringify(p)); 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(''); }}> {multiple ? ( current.length ? ( {current.map((p, i) => {pathLabels(p).join(' / ')})} ) : {placeholder} ) : ( {current && current.length ? pathLabels(current).join(' / ') : placeholder} )} ▾
{open && filterable && (
setQuery(e.target.value)} placeholder="搜索" />
)} {open && (
{query ? (
{results.length ? results.map((p, i) => (
pickResult(p)}>{pathLabels(p).join(' / ')}
)) :
无匹配结果
}
) : ( columns.map((col, ci) => (
{col.map(n => (
choose(n, ci)} > {multiple && !n.children && {isChecked([...activePath.slice(0, ci), n.value]) ? '✓' : ''}} {n.label} {n.children && n.children.length && ›}
))}
)) )}
)}
); } /* 用法示例: import Cascader from './Cascader'; const options = [{ value: 'zj', label: '浙江', children: [{ value: 'hz', label: '杭州', children: [{ value: 'xh', label: '西湖区' }] }] }]; */