Initial upload for secondary development
This commit is contained in:
131
chatlab-web/frontend/src/components/MemberSelector.jsx
Normal file
131
chatlab-web/frontend/src/components/MemberSelector.jsx
Normal file
@@ -0,0 +1,131 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user