Files
aurora-admin/frameworks/Cascader.jsx
T

153 lines
5.5 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useRef, useState, useEffect, useMemo } from 'react';
import './Cascader.css';
/**
* Aurora Admin 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 (
<div className="aa-cascader" ref={rootRef}>
<div className={`aa-cascader-trigger${open ? ' is-open' : ''}`} onClick={() => { setOpen(!open); if (open) setQuery(''); }}>
{multiple ? (
current.length ? (
<span className="aa-cascader-tags">
{current.map((p, i) => <span key={i} className="aa-cascader-tag">{pathLabels(p).join(' / ')}</span>)}
</span>
) : <span className="placeholder">{placeholder}</span>
) : (
<span>{current && current.length ? pathLabels(current).join(' / ') : placeholder}</span>
)}
<span className="caret">▾</span>
</div>
{open && filterable && (
<div className="aa-cascader-search">
<input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="搜索" />
</div>
)}
{open && (
<div className="aa-cascader-panel">
{query ? (
<div className="aa-cascader-results">
{results.length ? results.map((p, i) => (
<div key={i} className="aa-cascader-result" onClick={() => pickResult(p)}>{pathLabels(p).join(' / ')}</div>
)) : <div className="aa-cascader-empty">无匹配结果</div>}
</div>
) : (
columns.map((col, ci) => (
<div key={ci} className="aa-cascader-col">
{col.map(n => (
<div
key={n.value}
className={`aa-cascader-opt${activePath[ci] === n.value ? ' is-active' : ''}${multiple && !n.children && isChecked([...activePath.slice(0, ci), n.value]) ? ' is-checked' : ''}`}
onClick={() => choose(n, ci)}
>
{multiple && !n.children && <span className="box">{isChecked([...activePath.slice(0, ci), n.value]) ? '✓' : ''}</span>}
<span>{n.label}</span>
{n.children && n.children.length && <span className="arrow">›</span>}
</div>
))}
</div>
))
)}
</div>
)}
</div>
);
}
/*
用法示例:
import Cascader from './Cascader';
const options = [{ value: 'zj', label: '浙江', children: [{ value: 'hz', label: '杭州', children: [{ value: 'xh', label: '西湖区' }] }] }];
<Cascader options={options} defaultValue={['zj', 'hz', 'xh']} onChange={setPath} />
*/