Files
get_wechat/chatlab-web/frontend/src/components/MemberSelector.jsx

132 lines
4.3 KiB
JavaScript
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 { 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>
)
}