Initial upload for secondary development

This commit is contained in:
2026-06-08 19:00:03 +08:00
commit b913b8c78c
81 changed files with 27139 additions and 0 deletions

View 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>
)
}