132 lines
4.3 KiB
JavaScript
132 lines
4.3 KiB
JavaScript
import { useState, useRef, useEffect } from 'react'
|
||
import { Users, Check, X } from 'lucide-react'
|
||
|
||
/**
|
||
* 多选成员选择器
|
||
* 支持搜索、已选 Tag 展示、点击外部关闭
|
||
*/
|
||
export default function MemberSelector({ members = [], selected = [], onChange }) {
|
||
const [open, setOpen] = useState(false)
|
||
const [search, setSearch] = useState('')
|
||
const ref = useRef(null)
|
||
|
||
// 点击外部关闭
|
||
useEffect(() => {
|
||
function handler(e) {
|
||
if (ref.current && !ref.current.contains(e.target)) setOpen(false)
|
||
}
|
||
document.addEventListener('mousedown', handler)
|
||
return () => document.removeEventListener('mousedown', handler)
|
||
}, [])
|
||
|
||
const filtered = members.filter((m) => {
|
||
if (!search) return true
|
||
return (
|
||
m.accountName?.includes(search) ||
|
||
m.groupNickname?.includes(search) ||
|
||
m.platformId?.includes(search)
|
||
)
|
||
})
|
||
|
||
const toggle = (id) => {
|
||
if (selected.includes(id)) {
|
||
onChange(selected.filter((s) => s !== id))
|
||
} else {
|
||
onChange([...selected, id])
|
||
}
|
||
}
|
||
|
||
const getMember = (id) => members.find((m) => m.platformId === id)
|
||
|
||
return (
|
||
<div className="member-selector" ref={ref}>
|
||
<div
|
||
className={`member-selector-trigger ${open ? 'open' : ''}`}
|
||
onClick={() => setOpen(!open)}
|
||
id="member-selector-trigger"
|
||
>
|
||
<Users size={14} />
|
||
{selected.length === 0 ? (
|
||
<span>全部成员</span>
|
||
) : (
|
||
<span style={{ color: 'var(--accent-light)' }}>已选 {selected.length} 人</span>
|
||
)}
|
||
<span style={{ marginLeft: 'auto', color: 'var(--text-muted)', fontSize: 10 }}>▾</span>
|
||
</div>
|
||
|
||
{open && (
|
||
<div className="member-selector-dropdown">
|
||
{/* 搜索框 */}
|
||
<div className="member-search">
|
||
<input
|
||
className="member-search-input"
|
||
placeholder="🔍 搜索成员名称..."
|
||
value={search}
|
||
onChange={(e) => setSearch(e.target.value)}
|
||
autoFocus
|
||
/>
|
||
</div>
|
||
|
||
{/* 列表 */}
|
||
<div className="member-list-scroll">
|
||
{filtered.length === 0 && (
|
||
<div style={{ padding: '12px', textAlign: 'center', color: 'var(--text-muted)', fontSize: 12 }}>
|
||
无匹配成员
|
||
</div>
|
||
)}
|
||
{filtered.map((m) => {
|
||
const checked = selected.includes(m.platformId)
|
||
return (
|
||
<div
|
||
key={m.platformId}
|
||
className={`member-option ${checked ? 'checked' : ''}`}
|
||
onClick={() => toggle(m.platformId)}
|
||
>
|
||
<div className="member-checkbox">
|
||
{checked && <Check size={10} />}
|
||
</div>
|
||
<div style={{ flex: 1, minWidth: 0 }}>
|
||
<div className="member-option-name truncate">{m.accountName}</div>
|
||
{m.groupNickname && m.groupNickname !== m.accountName && (
|
||
<div className="member-option-nick truncate">{m.groupNickname}</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
|
||
{/* 已选 + 清空 */}
|
||
<div className="member-footer">
|
||
<div className="member-selected-tags">
|
||
{selected.length === 0 && (
|
||
<span style={{ fontSize: 11, color: 'var(--text-muted)' }}>未选择(显示全部)</span>
|
||
)}
|
||
{selected.map((id) => {
|
||
const m = getMember(id)
|
||
return (
|
||
<div key={id} className="member-tag">
|
||
{m?.accountName || id}
|
||
<span className="member-tag-remove" onClick={(e) => { e.stopPropagation(); toggle(id) }}>
|
||
<X size={9} />
|
||
</span>
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
{selected.length > 0 && (
|
||
<button
|
||
className="btn btn-ghost btn-sm"
|
||
style={{ flexShrink: 0 }}
|
||
onClick={(e) => { e.stopPropagation(); onChange([]) }}
|
||
>
|
||
清空
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|