173 lines
5.4 KiB
TypeScript
173 lines
5.4 KiB
TypeScript
import { useEffect, useState, useRef } from "react";
|
||
|
||
import { type Employee } from "../../services/api";
|
||
|
||
// 多选用户下拉框组件 - 带 tag 显示
|
||
export function UserMultiSelect({
|
||
users,
|
||
selectedUserIds,
|
||
onChange,
|
||
existingMemberIds,
|
||
}: {
|
||
users: Employee[];
|
||
selectedUserIds: string[];
|
||
onChange: (ids: string[]) => void;
|
||
existingMemberIds?: string[];
|
||
}) {
|
||
const [isOpen, setIsOpen] = useState(false);
|
||
const containerRef = useRef<HTMLDivElement>(null);
|
||
|
||
// 点击外部关闭下拉框
|
||
useEffect(() => {
|
||
const handleClickOutside = (event: MouseEvent) => {
|
||
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
||
setIsOpen(false);
|
||
}
|
||
};
|
||
document.addEventListener("mousedown", handleClickOutside);
|
||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||
}, []);
|
||
|
||
const selectedUsers = users.filter((user) => selectedUserIds.includes(user.user_id));
|
||
|
||
const toggleUser = (userId: string) => {
|
||
if (selectedUserIds.includes(userId)) {
|
||
onChange(selectedUserIds.filter((id) => id !== userId));
|
||
} else {
|
||
onChange([...selectedUserIds, userId]);
|
||
}
|
||
};
|
||
|
||
const removeUser = (userId: string, event: React.MouseEvent) => {
|
||
event.stopPropagation();
|
||
onChange(selectedUserIds.filter((id) => id !== userId));
|
||
};
|
||
|
||
return (
|
||
<div ref={containerRef} style={{ position: "relative" }}>
|
||
{/* 选择框 - 显示 tag */}
|
||
<div
|
||
className="admin-toolbar__input"
|
||
onClick={() => setIsOpen(!isOpen)}
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
flexWrap: "wrap",
|
||
gap: "4px",
|
||
padding: "6px 32px 6px 8px",
|
||
minHeight: "36px",
|
||
border: "1px solid #ddd",
|
||
borderRadius: "4px",
|
||
cursor: "pointer",
|
||
backgroundColor: "#fff",
|
||
}}
|
||
>
|
||
{selectedUsers.length === 0 ? (
|
||
<span style={{ color: "#999" }}>请选择用户</span>
|
||
) : (
|
||
selectedUsers.map((user) => (
|
||
<span
|
||
key={user.user_id}
|
||
style={{
|
||
display: "inline-flex",
|
||
alignItems: "center",
|
||
backgroundColor: "#e6f7ff",
|
||
color: "#1890ff",
|
||
border: "1px solid #91d5ff",
|
||
borderRadius: "2px",
|
||
padding: "2px 6px",
|
||
fontSize: "13px",
|
||
}}
|
||
>
|
||
{user.display_name}
|
||
<span
|
||
onClick={(e) => removeUser(user.user_id, e)}
|
||
style={{
|
||
marginLeft: "4px",
|
||
cursor: "pointer",
|
||
fontWeight: "bold",
|
||
fontSize: "14px",
|
||
}}
|
||
>
|
||
×
|
||
</span>
|
||
</span>
|
||
))
|
||
)}
|
||
<span
|
||
style={{
|
||
position: "absolute",
|
||
right: "12px",
|
||
top: "50%",
|
||
transform: "translateY(-50%)",
|
||
color: "#999",
|
||
pointerEvents: "none",
|
||
}}
|
||
>
|
||
{isOpen ? "▲" : "▼"}
|
||
</span>
|
||
</div>
|
||
|
||
{/* 下拉列表 */}
|
||
{isOpen && (
|
||
<div
|
||
style={{
|
||
position: "absolute",
|
||
top: "100%",
|
||
left: 0,
|
||
right: 0,
|
||
maxHeight: "136px",
|
||
overflowY: "auto",
|
||
border: "1px solid #1890ff",
|
||
borderRadius: "4px",
|
||
backgroundColor: "#fff",
|
||
zIndex: 100,
|
||
marginTop: "2px",
|
||
boxShadow: "0 2px 8px rgba(0,0,0,0.15)",
|
||
}}
|
||
>
|
||
{users.length === 0 ? (
|
||
<div style={{ padding: "12px", color: "#999", textAlign: "center", fontSize: "13px" }}>
|
||
所有可选用户已在项目中
|
||
</div>
|
||
) : (
|
||
users.map((user) => {
|
||
const isSelected = selectedUserIds.includes(user.user_id);
|
||
const isExisting = existingMemberIds?.includes(user.user_id);
|
||
const isDisabled = isExisting;
|
||
return (
|
||
<div
|
||
key={user.user_id}
|
||
onClick={() => !isDisabled && toggleUser(user.user_id)}
|
||
style={{
|
||
display: "flex",
|
||
justifyContent: "space-between",
|
||
alignItems: "center",
|
||
padding: "6px 12px",
|
||
cursor: isDisabled ? "not-allowed" : "pointer",
|
||
backgroundColor: isSelected ? "#e6f7ff" : isExisting ? "#f5f5f5" : "transparent",
|
||
borderBottom: "1px solid #f0f0f0",
|
||
opacity: isDisabled ? 0.6 : 1,
|
||
}}
|
||
>
|
||
<span>
|
||
{user.display_name} <span style={{ color: "#999" }}>({user.username})</span>
|
||
</span>
|
||
<div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
|
||
{isExisting && (
|
||
<span style={{ fontSize: "11px", color: "#999", backgroundColor: "#f0f0f0", padding: "1px 6px", borderRadius: "2px" }}>
|
||
已在项目中
|
||
</span>
|
||
)}
|
||
{isSelected && <span style={{ color: "#1890ff" }}>✓</span>}
|
||
</div>
|
||
</div>
|
||
);
|
||
})
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|