Files
cunkebao_v3/nkebao/src/components/FriendSelection.tsx
笔记本里的永平 53ea1e8395 feat: 本次提交更新内容如下
构建完成
2025-07-10 16:19:16 +08:00

275 lines
9.5 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 (page: number = 1, limit: number = 20, deviceIds: string[]): Promise<FriendsResponse> => {
if (deviceIds.length === 0) {
return {
code: 200,
msg: 'success',
data: {
list: [],
total: 0,
page,
limit
}
};
}
const deviceIdsParam = deviceIds.join(',');
return get<FriendsResponse>(`/v1/friend?page=${page}&limit=${limit}&deviceIds=${deviceIdsParam}`);
};
// 组件属性接口
interface FriendSelectionProps {
selectedFriends: string[];
onSelect: (friends: string[]) => void;
deviceIds: string[];
placeholder?: string;
className?: string;
}
export default function FriendSelection({
selectedFriends,
onSelect,
deviceIds,
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);
// 当弹窗打开时获取好友列表
useEffect(() => {
if (dialogOpen && deviceIds.length > 0) {
fetchFriends(currentPage);
}
}, [dialogOpen, currentPage, deviceIds]);
// 当设备ID变化时重置页码
useEffect(() => {
if (deviceIds.length > 0) {
setCurrentPage(1);
}
}, [deviceIds]);
// 获取好友列表API
const fetchFriends = async (page: number) => {
if (deviceIds.length === 0) return;
setLoading(true);
try {
const res = await fetchFriendsList(page, 20, deviceIds);
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) => {
if (selectedFriends.includes(friendId)) {
onSelect(selectedFriends.filter(id => id !== friendId));
} else {
onSelect([...selectedFriends, friendId]);
}
};
// 获取显示文本
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={() => setDialogOpen(true)}
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">
<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>
</>
);
}