Files
aurora-admin/frameworks/Select.jsx
T

178 lines
5.7 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, { 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;选中项品牌色 + 勾选图标
* 受控:<Select value={v} onChange={setV} options={...} />
* 非受控:<Select defaultValue="bj" options={...} />
*/
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 (
<div className={classes} ref={rootRef} {...rest}>
<div className="aa-select-trigger" onClick={() => !disabled && setOpen((o) => !o)} tabIndex={0}>
{multiple ? (
selectedArr.length ? (
selectedArr.map((v) => (
<span className="tag" key={v}>
{labelOf(v)}
<span className="x" onClick={(e) => { e.stopPropagation(); remove(v); }}>✕</span>
</span>
))
) : (
<span className="placeholder">{placeholder}</span>
)
) : selectedVal == null ? (
<span className="placeholder">{placeholder}</span>
) : (
<span>{labelOf(selectedVal)}</span>
)}
<span className="caret">▾</span>
</div>
{open && (
<div className="aa-select-panel">
{searchable && (
<div className="aa-select-search">
<input
placeholder="输入关键字"
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
onClick={(e) => e.stopPropagation()}
/>
</div>
)}
{groups ? (
filteredGroups.map((g) => (
<React.Fragment key={g.label}>
<div className="aa-select-group-label">{g.label}</div>
{g.options.map((o) => (
<div
key={o.value}
className={`aa-select-option${isSelected(o.value) ? ' is-selected' : ''}${o.disabled ? ' is-disabled' : ''}`}
onClick={() => choose(o)}
>
{o.label}<span className="check">✓</span>
</div>
))}
</React.Fragment>
))
) : (
filteredOptions.map((o) => (
<div
key={o.value}
className={`aa-select-option${isSelected(o.value) ? ' is-selected' : ''}${o.disabled ? ' is-disabled' : ''}`}
onClick={() => choose(o)}
>
{o.label}<span className="check">✓</span>
</div>
))
)}
{isEmpty && <div className="aa-select-empty">无匹配选项</div>}
</div>
)}
</div>
);
}
/*
用法示例:
import Select from './Select';
const cities = [{ value: 'bj', label: '北京' }, { value: 'sh', label: '上海' }];
<Select options={cities} placeholder="请选择城市" />
<Select multiple options={cities} defaultValue={['bj']} />
<Select searchable options={cities} />
<Select groups={[{ label: '技术中心', options: cities }]} />
*/