Files
ckb-SuperAdmin/nkebao/src/components/FriendSelection.tsx
笔记本里的永平 9e1dc36853 feat: 本次提交更新内容如下
保存下场景建设数据
2025-07-16 15:18:54 +08:00

348 lines
11 KiB
TypeScript
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 React, { useState, useEffect } from "react";
import { Search, X } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { get } from "@/api/request";
// 微信好友接口类型
interface WechatFriend {
id: string;
nickname: string;
wechatId: string;
avatar: string;
customer: string;
}
// 好友列表API响应类型
interface FriendsResponse {
code: number;
msg: string;
data: {
list: Array<{
id: number;
nickname: string;
wechatId: string;
avatar?: string;
customer?: string;
}>;
total: number;
page: number;
limit: number;
};
}
// 获取好友列表API函数
const fetchFriendsList = async (params: {
page: number;
limit: number;
deviceIds?: string[];
}): Promise<FriendsResponse> => {
if (params.deviceIds && params.deviceIds.length === 0) {
return {
code: 200,
msg: "success",
data: {
list: [],
total: 0,
page: params.page,
limit: params.limit,
},
};
}
const deviceIdsParam = params?.deviceIds?.join(",") || "";
return get<FriendsResponse>(
`/v1/friend?page=${params.page}&limit=${params.limit}&deviceIds=${deviceIdsParam}`
);
};
// 组件属性接口
interface FriendSelectionProps {
selectedFriends: string[];
onSelect: (friends: string[]) => void;
onSelectDetail?: (friends: WechatFriend[]) => void; // 新增
deviceIds?: string[];
enableDeviceFilter?: boolean; // 新增开关默认true
placeholder?: string;
className?: string;
}
export default function FriendSelection({
selectedFriends,
onSelect,
onSelectDetail,
deviceIds = [],
enableDeviceFilter = true,
placeholder = "选择微信好友",
className = "",
}: FriendSelectionProps) {
const [dialogOpen, setDialogOpen] = useState(false);
const [friends, setFriends] = useState<WechatFriend[]>([]);
const [searchQuery, setSearchQuery] = useState("");
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [totalFriends, setTotalFriends] = useState(0);
const [loading, setLoading] = useState(false);
// 打开弹窗并请求第一页好友
const openDialog = () => {
setCurrentPage(1);
setDialogOpen(true);
fetchFriends(1);
};
// 当页码变化时,拉取对应页数据(弹窗已打开时)
useEffect(() => {
if (dialogOpen && currentPage !== 1) {
fetchFriends(currentPage);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentPage]);
// 获取好友列表API
const fetchFriends = async (page: number) => {
setLoading(true);
try {
let res;
if (enableDeviceFilter) {
if (deviceIds.length === 0) {
setFriends([]);
setTotalFriends(0);
setTotalPages(1);
setLoading(false);
return;
}
res = await fetchFriendsList({ page, limit: 20, deviceIds: deviceIds });
} else {
res = await fetchFriendsList({ page, limit: 20 });
}
if (res && res.code === 200 && res.data) {
setFriends(
res.data.list.map((friend) => ({
id: friend.id?.toString() || "",
nickname: friend.nickname || "",
wechatId: friend.wechatId || "",
avatar: friend.avatar || "",
customer: friend.customer || "",
}))
);
setTotalFriends(res.data.total || 0);
setTotalPages(Math.ceil((res.data.total || 0) / 20));
}
} catch (error) {
console.error("获取好友列表失败:", error);
} finally {
setLoading(false);
}
};
// 过滤好友
const filteredFriends = friends.filter(
(friend) =>
friend.nickname.toLowerCase().includes(searchQuery.toLowerCase()) ||
friend.wechatId.toLowerCase().includes(searchQuery.toLowerCase())
);
// 处理好友选择
const handleFriendToggle = (friendId: string) => {
let newIds: string[];
if (selectedFriends.includes(friendId)) {
newIds = selectedFriends.filter((id) => id !== friendId);
} else {
newIds = [...selectedFriends, friendId];
}
onSelect(newIds);
if (onSelectDetail) {
const selectedObjs = friends.filter((f) => newIds.includes(f.id));
onSelectDetail(selectedObjs);
}
};
// 获取显示文本
const getDisplayText = () => {
if (selectedFriends.length === 0) return "";
return `已选择 ${selectedFriends.length} 个好友`;
};
const handleConfirm = () => {
setDialogOpen(false);
};
return (
<>
{/* 输入框 */}
<div className={`relative ${className}`}>
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400">
<svg
width="20"
height="20"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
</span>
<Input
placeholder={placeholder}
className="pl-10 h-12 rounded-xl border-gray-200 text-base"
readOnly
onClick={openDialog}
value={getDisplayText()}
/>
</div>
{/* 微信好友选择弹窗 */}
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent className="max-w-xl max-h-[90vh] flex flex-col p-0 gap-0 overflow-hidden bg-white">
<div className="p-6">
<DialogTitle className="text-center text-xl font-medium mb-6">
</DialogTitle>
<div className="relative mb-4">
<Input
placeholder="搜索好友"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 py-2 rounded-full border-gray-200"
/>
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
<Button
variant="ghost"
size="icon"
className="absolute right-2 top-1/2 -translate-y-1/2 h-6 w-6 rounded-full"
onClick={() => setSearchQuery("")}
>
<X className="h-4 w-4" />
</Button>
</div>
</div>
<ScrollArea className="flex-1 overflow-y-auto">
{loading ? (
<div className="flex items-center justify-center h-full">
<div className="text-gray-500">...</div>
</div>
) : filteredFriends.length > 0 ? (
<div className="divide-y">
{filteredFriends.map((friend) => (
<label
key={friend.id}
className="flex items-center px-6 py-4 hover:bg-gray-50 cursor-pointer"
onClick={() => handleFriendToggle(friend.id)}
>
<div className="mr-3 flex items-center justify-center">
<div
className={`w-5 h-5 rounded-full border ${
selectedFriends.includes(friend.id)
? "border-blue-600"
: "border-gray-300"
} flex items-center justify-center`}
>
{selectedFriends.includes(friend.id) && (
<div className="w-3 h-3 rounded-full bg-blue-600"></div>
)}
</div>
</div>
<div className="flex items-center space-x-3 flex-1">
<div className="w-10 h-10 rounded-full bg-gradient-to-r from-blue-400 to-purple-500 flex items-center justify-center text-white text-sm font-medium overflow-hidden">
{friend.avatar ? (
<img
src={friend.avatar}
alt={friend.nickname}
className="w-full h-full object-cover"
/>
) : (
friend.nickname.charAt(0)
)}
</div>
<div className="flex-1">
<div className="font-medium">{friend.nickname}</div>
<div className="text-sm text-gray-500">
ID: {friend.wechatId}
</div>
{friend.customer && (
<div className="text-sm text-gray-400">
: {friend.customer}
</div>
)}
</div>
</div>
</label>
))}
</div>
) : (
<div className="flex items-center justify-center h-full">
<div className="text-gray-500">
{deviceIds.length === 0 ? "请先选择设备" : "没有找到好友"}
</div>
</div>
)}
</ScrollArea>
<div className="border-t p-4 flex items-center justify-between bg-white">
<div className="text-sm text-gray-500">
{totalFriends}
</div>
<div className="flex items-center space-x-2">
<Button
variant="ghost"
size="sm"
onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
disabled={currentPage === 1 || loading}
className="px-2 py-0 h-8 min-w-0"
>
&lt;
</Button>
<span className="text-sm">
{currentPage} / {totalPages}
</span>
<Button
variant="ghost"
size="sm"
onClick={() =>
setCurrentPage(Math.min(totalPages, currentPage + 1))
}
disabled={currentPage === totalPages || loading}
className="px-2 py-0 h-8 min-w-0"
>
&gt;
</Button>
</div>
</div>
<div className="border-t p-4 flex items-center justify-between bg-white">
<Button
variant="outline"
onClick={() => setDialogOpen(false)}
className="px-6 rounded-full border-gray-300"
>
</Button>
<Button
onClick={handleConfirm}
className="px-6 bg-blue-600 hover:bg-blue-700 rounded-full"
>
({selectedFriends.length})
</Button>
</div>
</DialogContent>
</Dialog>
</>
);
}