Files
cunkebao_v3/Cunkebao/src/components/FriendSelection.tsx
笔记本里的永平 1c64a13058 feat: 本次提交更新内容如下
9、设备选择、微信好友选择、群组选择样式调整
2025-07-17 17:43:54 +08:00

382 lines
12 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, 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函数 - 添加 keyword 参数
const fetchFriendsList = async (params: {
page: number;
limit: number;
deviceIds?: string[];
keyword?: 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(",") || "";
const keywordParam = params?.keyword
? `&keyword=${encodeURIComponent(params.keyword)}`
: "";
return get<FriendsResponse>(
`/v1/friend?page=${params.page}&limit=${params.limit}&deviceIds=${deviceIdsParam}${keywordParam}`
);
};
// 组件属性接口
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);
setSearchQuery(""); // 重置搜索关键词
setDialogOpen(true);
fetchFriends(1, "");
};
// 当页码变化时,拉取对应页数据(弹窗已打开时)
useEffect(() => {
if (dialogOpen && currentPage !== 1) {
fetchFriends(currentPage, searchQuery);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentPage]);
// 搜索防抖
useEffect(() => {
if (!dialogOpen) return;
const timer = setTimeout(() => {
setCurrentPage(1); // 重置到第一页
fetchFriends(1, searchQuery);
}, 500); // 500 防抖
return () => clearTimeout(timer);
}, [searchQuery, dialogOpen]);
// 获取好友列表API - 添加 keyword 参数
const fetchFriends = async (page: number, keyword: string = "") => {
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,
keyword: keyword.trim() || undefined,
});
} else {
res = await fetchFriendsList({
page,
limit: 20,
keyword: keyword.trim() || undefined,
});
}
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 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);
};
// 清空搜索
const handleClearSearch = () => {
setSearchQuery("");
setCurrentPage(1);
fetchFriends(1, "");
};
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="w-full h-full max-w-none max-h-none flex flex-col p-0 gap-0 overflow-hidden bg-white"
aria-describedby="friend-selection-description"
>
<div id="friend-selection-description" className="sr-only">
</div>
<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" />
{searchQuery && (
<Button
variant="ghost"
size="icon"
className="absolute right-2 top-1/2 -translate-y-1/2 h-6 w-6 rounded-full"
onClick={handleClearSearch}
>
<X className="h-4 w-4" />
</Button>
)}
</div>
</div>
<div className="flex-1 overflow-y-auto">
{loading ? (
<div className="flex items-center justify-center h-full">
<div className="text-gray-500">...</div>
</div>
) : friends.length > 0 ? (
<div className="divide-y">
{friends.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
? "请先选择设备"
: searchQuery
? `没有找到包含"${searchQuery}"的好友`
: "没有找到好友"}
</div>
</div>
)}
</div>
<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>
</>
);
}