Files
ckb-SuperAdmin/nkebao/src/pages/wechat-accounts/WechatAccountDetail.tsx
笔记本里的永平 86f7718d2b feat: 本次提交更新内容如下
更新代码内容
2025-07-07 17:28:11 +08:00

971 lines
42 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, useRef, useCallback } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import PageHeader from '@/components/PageHeader';
import {
ChevronLeft,
Smartphone,
Users,
Star,
Clock,
MessageSquare,
Shield,
Info,
UserPlus,
Search,
ChevronRight,
Loader2
} from 'lucide-react';
import { useWechatAccount } from '@/contexts/WechatAccountContext';
import { fetchWechatAccountSummary, fetchWechatFriends, fetchWechatFriendDetail } from '@/api/wechat-accounts';
import { useToast } from '@/components/ui/toast';
import Layout from '@/components/Layout';
import '@/components/Layout.css';
interface WechatAccountSummary {
accountAge: string;
activityLevel: {
allTimes: number;
dayTimes: number;
};
accountWeight: {
scope: number;
ageWeight: number;
activityWeigth: number;
restrictWeight: number;
realNameWeight: number;
};
statistics: {
todayAdded: number;
addLimit: number;
};
restrictions: {
id: number;
level: string;
reason: string;
date: string;
}[];
}
interface Friend {
id: string;
avatar: string;
nickname: string;
wechatId: string;
remark: string;
addTime: string;
lastInteraction: string;
tags: Array<{
id: string;
name: string;
color: string;
}>;
region: string;
source: string;
notes: string;
}
interface WechatFriendDetail {
id: number;
avatar: string;
nickname: string;
region: string;
wechatId: string;
addDate: string;
tags: string[];
memo: string;
source: string;
}
export default function WechatAccountDetail() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const { toast } = useToast();
const { currentAccount, clearCurrentAccount } = useWechatAccount();
const [accountSummary, setAccountSummary] = useState<WechatAccountSummary | null>(null);
const [showRestrictions, setShowRestrictions] = useState(false);
const [showTransferConfirm, setShowTransferConfirm] = useState(false);
const [showFriendDetail, setShowFriendDetail] = useState(false);
const [selectedFriend, setSelectedFriend] = useState<Friend | null>(null);
const [friendDetail, setFriendDetail] = useState<WechatFriendDetail | null>(null);
const [isLoadingFriendDetail, setIsLoadingFriendDetail] = useState(false);
const [friendDetailError, setFriendDetailError] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState("");
const [activeTab, setActiveTab] = useState("overview");
const [isLoading, setIsLoading] = useState(false);
// 好友列表相关状态
const [friends, setFriends] = useState<Friend[]>([]);
const [friendsPage, setFriendsPage] = useState(1);
const [friendsTotal, setFriendsTotal] = useState(0);
const [hasMoreFriends, setHasMoreFriends] = useState(true);
const [isFetchingFriends, setIsFetchingFriends] = useState(false);
const [hasFriendLoadError, setHasFriendLoadError] = useState(false);
const [isFriendsEmpty, setIsFriendsEmpty] = useState(false);
const friendsObserver = useRef<IntersectionObserver | null>(null);
const friendsLoadingRef = useRef<HTMLDivElement | null>(null);
// 如果没有账号数据,返回上一页
useEffect(() => {
if (!currentAccount) {
toast({
title: "数据错误",
description: "未找到账号信息,请重新选择",
variant: "destructive"
});
navigate('/wechat-accounts');
return;
}
}, [currentAccount, navigate, toast]);
// 获取账号概览信息
const fetchAccountSummary = useCallback(async () => {
if (!id) return;
try {
setIsLoading(true);
const response = await fetchWechatAccountSummary(id);
if (response && response.code === 200 && response.data) {
setAccountSummary(response.data);
} else {
toast({
title: "获取账号概览失败",
description: response?.msg || "请稍后重试",
variant: "destructive"
});
}
} catch (error) {
console.error("获取账号概览失败:", error);
toast({
title: "获取账号概览失败",
description: "请检查网络连接后重试",
variant: "destructive"
});
} finally {
setIsLoading(false);
}
}, [id, toast]);
// 获取好友列表
const fetchFriends = useCallback(async (page: number = 1, isNewSearch: boolean = false) => {
console.log('fetchFriends called:', { page, isNewSearch, isFetchingFriends, id, searchQuery });
if (!id || isFetchingFriends) {
console.log('fetchFriends early return:', { id, isFetchingFriends });
return;
}
try {
setIsFetchingFriends(true);
setHasFriendLoadError(false);
console.log('Making API request for friends:', { id, page, searchQuery });
const response = await fetchWechatFriends(id, page, 20, searchQuery);
console.log('API response:', response);
if (response && response.code === 200 && response.data) {
const newFriends = response.data.list.map((friend: any) => ({
id: friend.id.toString(),
avatar: friend.avatar || "/placeholder.svg",
nickname: friend.nickname || "未知用户",
wechatId: friend.wechatId || "",
remark: friend.memo || "",
addTime: friend.createTime || new Date().toISOString().split('T')[0],
lastInteraction: friend.lastInteraction || new Date().toISOString().split('T')[0],
tags: friend.tags ? friend.tags.map((tag: string, index: number) => ({
id: `tag-${index}`,
name: tag,
color: getRandomTagColor()
})) : [],
region: friend.region || "未知",
source: friend.source || "未知",
notes: friend.notes || ""
}));
console.log('Processed friends:', { newFriendsCount: newFriends.length, isNewSearch });
if (isNewSearch) {
setFriends(newFriends);
// 如果是新搜索且数据为空,设置空状态
if (newFriends.length === 0) {
console.log('Setting empty state for new search');
setIsFriendsEmpty(true);
setHasMoreFriends(false);
} else {
console.log('Setting normal state for new search');
setIsFriendsEmpty(false);
setHasMoreFriends(newFriends.length === 20);
}
} else {
setFriends(prev => [...prev, ...newFriends]);
setHasMoreFriends(newFriends.length === 20);
}
setFriendsTotal(response.data.total);
setFriendsPage(page);
} else {
console.log('API response error:', response);
setHasFriendLoadError(true);
if (isNewSearch) {
setFriends([]);
setIsFriendsEmpty(true);
setHasMoreFriends(false);
}
toast({
title: "获取好友列表失败",
description: response?.msg || "请稍后重试",
variant: "destructive"
});
}
} catch (error) {
console.error("获取好友列表失败:", error);
setHasFriendLoadError(true);
if (isNewSearch) {
setFriends([]);
setIsFriendsEmpty(true);
setHasMoreFriends(false);
}
toast({
title: "获取好友列表失败",
description: "请检查网络连接后重试",
variant: "destructive"
});
} finally {
console.log('Setting isFetchingFriends to false');
setIsFetchingFriends(false);
}
}, [id, searchQuery, toast]);
// 初始化数据
useEffect(() => {
if (id) {
fetchAccountSummary();
if (activeTab === "friends") {
fetchFriends(1, true);
}
}
}, [id, fetchAccountSummary]);
// 监听标签切换
useEffect(() => {
if (activeTab === "friends" && id) {
// 重置空状态,允许重新加载
setIsFriendsEmpty(false);
setHasFriendLoadError(false);
fetchFriends(1, true);
}
}, [activeTab, id, fetchFriends]);
// 无限滚动加载好友
useEffect(() => {
if (!friendsLoadingRef.current || !hasMoreFriends || isFetchingFriends || isFriendsEmpty) return;
friendsObserver.current = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasMoreFriends && !isFetchingFriends && !isFriendsEmpty) {
fetchFriends(friendsPage + 1, false);
}
},
{ threshold: 0.1 }
);
friendsObserver.current.observe(friendsLoadingRef.current);
return () => {
if (friendsObserver.current) {
friendsObserver.current.disconnect();
}
};
}, [hasMoreFriends, isFetchingFriends, friendsPage, fetchFriends, isFriendsEmpty]);
// 工具函数
const getRandomTagColor = (): string => {
const colors = [
"bg-blue-100 text-blue-800",
"bg-green-100 text-green-800",
"bg-red-100 text-red-800",
"bg-pink-100 text-pink-800",
"bg-emerald-100 text-emerald-800",
"bg-amber-100 text-amber-800",
];
return colors[Math.floor(Math.random() * colors.length)];
};
const calculateAccountAge = (registerTime: string) => {
const registerDate = new Date(registerTime);
const now = new Date();
const diffTime = Math.abs(now.getTime() - registerDate.getTime());
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
const years = Math.floor(diffDays / 365);
const months = Math.floor((diffDays % 365) / 30);
return { years, months };
};
const formatAccountAge = (age: { years: number; months: number }) => {
if (age.years > 0) {
return `${age.years}${age.months}个月`;
}
return `${age.months}个月`;
};
const getWeightColor = (weight: number) => {
if (weight >= 80) return "text-green-600";
if (weight >= 60) return "text-yellow-600";
return "text-red-600";
};
const getWeightDescription = (weight: number) => {
if (weight >= 80) return "账号质量优秀,可以正常使用";
if (weight >= 60) return "账号质量良好,需要注意使用频率";
return "账号质量较差,建议谨慎使用";
};
const handleTransferFriends = () => {
setShowTransferConfirm(true);
};
const confirmTransferFriends = () => {
toast({
title: "好友转移计划已创建",
description: "请在场景获客中查看详情",
});
setShowTransferConfirm(false);
navigate("/scenarios");
};
const handleBack = () => {
clearCurrentAccount();
navigate('/wechat-accounts');
};
const handleFriendClick = async (friend: Friend) => {
setSelectedFriend(friend);
setShowFriendDetail(true);
setIsLoadingFriendDetail(true);
setFriendDetailError(null);
try {
const response = await fetchWechatFriendDetail(friend.id);
if (response && response.code === 200 && response.data) {
setFriendDetail(response.data);
} else {
setFriendDetailError(response?.msg || "获取好友详情失败");
}
} catch (error) {
console.error("获取好友详情失败:", error);
setFriendDetailError("网络错误,请稍后重试");
} finally {
setIsLoadingFriendDetail(false);
}
};
const getRestrictionLevelColor = (level: string) => {
switch (level) {
case "high":
return "text-red-600";
case "medium":
return "text-yellow-600";
default:
return "text-gray-600";
}
};
const formatDateTime = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
}).replace(/\//g, '-');
};
const handleSearch = () => {
// 搜索时重置空状态
setIsFriendsEmpty(false);
setHasFriendLoadError(false);
fetchFriends(1, true);
};
const handleTabChange = (value: string) => {
setActiveTab(value);
};
if (!currentAccount) {
return (
<div className="flex justify-center items-center py-20">
<Loader2 className="h-8 w-8 animate-spin text-blue-500" />
</div>
);
}
return (
<Layout
header={
<PageHeader
title="账号详情"
defaultBackPath="/wechat-accounts"
/>
}
>
<div className="bg-gradient-to-b from-blue-50 to-white">
<div className="p-4 space-y-4">
{/* 账号基本信息卡片 */}
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100">
<div className="flex items-center space-x-4">
<div className="relative">
<img
src={currentAccount.avatar || "/placeholder.svg"}
alt={currentAccount.nickname}
className="w-16 h-16 rounded-full ring-4 ring-offset-2 ring-blue-500/20"
/>
<div className={`absolute -bottom-1 -right-1 w-4 h-4 rounded-full border-2 border-white ${
currentAccount.status === "normal" ? "bg-green-500" : "bg-red-500"
}`}></div>
</div>
<div className="flex-1">
<div className="flex items-center space-x-2">
<h2 className="text-xl font-semibold truncate max-w-[200px]">{currentAccount.nickname}</h2>
<span className={`px-2 py-1 text-xs rounded-full ${
currentAccount.status === "normal"
? "bg-green-500 text-white"
: "bg-red-500 text-white"
}`}>
{currentAccount.status === "normal" ? "正常" : "异常"}
</span>
</div>
<p className="text-sm text-gray-500 mt-1">{currentAccount.wechatAccount}</p>
<div className="flex gap-2 mt-2">
<button
className="px-3 py-1.5 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors text-sm flex items-center"
onClick={() => navigate(`/devices/${currentAccount.deviceId}`)}
>
<Smartphone className="w-4 h-4 mr-2" />
{currentAccount.deviceName || '未命名设备'}
</button>
<button
className="px-3 py-1.5 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors text-sm flex items-center"
onClick={handleTransferFriends}
>
<UserPlus className="w-4 h-4 mr-2" />
</button>
</div>
</div>
</div>
</div>
{/* 标签页 */}
<div className="bg-white rounded-xl shadow-sm border border-gray-100">
<div className="flex border-b border-gray-200">
<button
className={`flex-1 py-3 px-4 text-sm font-medium transition-colors ${
activeTab === "overview"
? "text-blue-600 border-b-2 border-blue-600"
: "text-gray-500 hover:text-gray-700"
}`}
onClick={() => handleTabChange("overview")}
>
</button>
<button
className={`flex-1 py-3 px-4 text-sm font-medium transition-colors ${
activeTab === "friends"
? "text-blue-600 border-b-2 border-blue-600"
: "text-gray-500 hover:text-gray-700"
}`}
onClick={() => handleTabChange("friends")}
>
{activeTab === "friends" && friendsTotal > 0 ? ` (${friendsTotal.toLocaleString()})` : ''}
</button>
</div>
<div className="p-4">
{activeTab === "overview" ? (
<div className="space-y-4">
{/* 账号基础信息 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div className="bg-gradient-to-br from-blue-50 to-indigo-50 p-3 rounded-xl border border-blue-100 shadow-sm hover:shadow-md transition-all duration-300">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<div className="p-1.5 bg-blue-100 rounded-lg">
<Clock className="w-4 h-4 text-blue-600" />
</div>
<div>
<div className="text-xs font-medium text-blue-700"></div>
{accountSummary && (
<div className="text-xs text-blue-600">
{new Date(accountSummary.accountAge).toLocaleDateString()}
</div>
)}
</div>
</div>
{accountSummary && (
<div className="text-right">
<div className="text-lg font-bold text-blue-800">
{formatAccountAge(calculateAccountAge(accountSummary.accountAge))}
</div>
</div>
)}
</div>
</div>
<div className="bg-gradient-to-br from-green-50 to-emerald-50 p-3 rounded-xl border border-green-100 shadow-sm hover:shadow-md transition-all duration-300">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<div className="p-1.5 bg-green-100 rounded-lg">
<MessageSquare className="w-4 h-4 text-green-600" />
</div>
<div>
<div className="text-xs font-medium text-green-700"></div>
{accountSummary && (
<div className="text-xs text-green-600">
{accountSummary.activityLevel.allTimes.toLocaleString()}
</div>
)}
</div>
</div>
{accountSummary && (
<div className="text-right">
<div className="text-lg font-bold text-green-800">
{accountSummary.activityLevel.dayTimes.toLocaleString()}
<span className="text-sm text-green-600 ml-1">/</span>
</div>
</div>
)}
</div>
</div>
</div>
{/* 账号权重评估 */}
{accountSummary && (
<div className="bg-gradient-to-br from-amber-50 to-yellow-50 p-4 rounded-xl border border-amber-100 shadow-sm hover:shadow-md transition-all duration-300">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-2">
<div className="p-1.5 bg-amber-100 rounded-lg">
<Star className="w-4 h-4 text-amber-600" />
</div>
<span className="font-semibold text-amber-800 text-base"></span>
</div>
<div className={`flex items-center space-x-2 px-3 py-1.5 rounded-full ${getWeightColor(accountSummary.accountWeight.scope).includes('green') ? 'bg-green-100 text-green-700' : getWeightColor(accountSummary.accountWeight.scope).includes('yellow') ? 'bg-yellow-100 text-yellow-700' : 'bg-red-100 text-red-700'}`}>
<span className="text-xl font-bold">{accountSummary.accountWeight.scope}</span>
<span className="text-xs font-medium"></span>
</div>
</div>
<p className="text-xs text-amber-700 mb-4 bg-amber-100 px-3 py-2 rounded-lg border border-amber-200">
{getWeightDescription(accountSummary.accountWeight.scope)}
</p>
<div className="space-y-3">
<div className="flex items-center">
<span className="flex-shrink-0 w-16 text-xs font-medium text-amber-700"></span>
<div className="flex-1 mx-3 bg-amber-200 rounded-full h-2 overflow-hidden">
<div
className="bg-gradient-to-r from-amber-400 to-amber-600 h-2 rounded-full transition-all duration-500"
style={{ width: `${accountSummary.accountWeight.ageWeight}%` }}
></div>
</div>
<span className="flex-shrink-0 w-10 text-xs font-medium text-amber-700 text-right">{accountSummary.accountWeight.ageWeight}%</span>
</div>
<div className="flex items-center">
<span className="flex-shrink-0 w-16 text-xs font-medium text-amber-700"></span>
<div className="flex-1 mx-3 bg-amber-200 rounded-full h-2 overflow-hidden">
<div
className="bg-gradient-to-r from-amber-400 to-amber-600 h-2 rounded-full transition-all duration-500"
style={{ width: `${accountSummary.accountWeight.activityWeigth}%` }}
></div>
</div>
<span className="flex-shrink-0 w-10 text-xs font-medium text-amber-700 text-right">{accountSummary.accountWeight.activityWeigth}%</span>
</div>
<div className="flex items-center">
<span className="flex-shrink-0 w-16 text-xs font-medium text-amber-700"></span>
<div className="flex-1 mx-3 bg-amber-200 rounded-full h-2 overflow-hidden">
<div
className="bg-gradient-to-r from-amber-400 to-amber-600 h-2 rounded-full transition-all duration-500"
style={{ width: `${accountSummary.accountWeight.restrictWeight}%` }}
></div>
</div>
<span className="flex-shrink-0 w-10 text-xs font-medium text-amber-700 text-right">{accountSummary.accountWeight.restrictWeight}%</span>
</div>
<div className="flex items-center">
<span className="flex-shrink-0 w-16 text-xs font-medium text-amber-700"></span>
<div className="flex-1 mx-3 bg-amber-200 rounded-full h-2 overflow-hidden">
<div
className="bg-gradient-to-r from-amber-400 to-amber-600 h-2 rounded-full transition-all duration-500"
style={{ width: `${accountSummary.accountWeight.realNameWeight}%` }}
></div>
</div>
<span className="flex-shrink-0 w-10 text-xs font-medium text-amber-700 text-right">{accountSummary.accountWeight.realNameWeight}%</span>
</div>
</div>
</div>
)}
{/* 添加好友统计 */}
{accountSummary && (
<div className="bg-gradient-to-br from-purple-50 to-indigo-50 p-4 rounded-xl border border-purple-100 shadow-sm hover:shadow-md transition-all duration-300">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-2">
<div className="p-1.5 bg-purple-100 rounded-lg">
<Users className="w-4 h-4 text-purple-600" />
</div>
<span className="font-semibold text-purple-800 text-base"></span>
</div>
<div className="relative group">
<div className="p-1.5 bg-purple-100 rounded-lg cursor-help">
<Info className="w-3 h-3 text-purple-600" />
</div>
<div className="absolute bottom-full right-0 mb-2 px-2 py-1.5 text-xs bg-purple-800 text-white rounded-lg opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap shadow-lg z-10">
<div className="absolute top-full right-4 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-purple-800"></div>
</div>
</div>
</div>
<div className="space-y-4">
<div className="flex items-center justify-between bg-white p-3 rounded-lg border border-purple-200">
<span className="text-xs font-medium text-purple-700"></span>
<span className="text-xl font-bold text-purple-800">{accountSummary.statistics.todayAdded}</span>
</div>
<div className="space-y-2">
<div className="flex justify-between text-xs">
<span className="text-purple-700 font-medium"></span>
<span className="text-purple-800 font-semibold">
{accountSummary.statistics.todayAdded}/{accountSummary.statistics.addLimit}
</span>
</div>
<div className="w-full bg-purple-200 rounded-full h-2 overflow-hidden">
<div
className="bg-gradient-to-r from-purple-400 to-purple-600 h-2 rounded-full transition-all duration-500"
style={{ width: `${Math.min((accountSummary.statistics.todayAdded / accountSummary.statistics.addLimit) * 100, 100)}%` }}
></div>
</div>
</div>
<div className="text-xs text-purple-700 bg-purple-100 px-3 py-2 rounded-lg border border-purple-200">
<span className="font-semibold text-purple-800">({accountSummary.accountWeight.scope})</span>{" "}
<span className="font-bold text-purple-800">{accountSummary.statistics.addLimit.toLocaleString()}</span>{" "}
</div>
</div>
</div>
)}
{/* 限制记录 */}
{accountSummary && (
<div className="bg-gradient-to-br from-red-50 to-pink-50 p-4 rounded-xl border border-red-100 shadow-sm hover:shadow-md transition-all duration-300">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-2">
<div className="p-1.5 bg-red-100 rounded-lg">
<Shield className="w-4 h-4 text-red-600" />
</div>
<span className="font-semibold text-red-800 text-base"></span>
</div>
{accountSummary.restrictions.length > 0 && (
<button
className="px-3 py-1.5 bg-red-100 hover:bg-red-200 text-red-700 rounded-full text-xs font-medium transition-colors duration-200 border border-red-200"
onClick={() => setShowRestrictions(true)}
>
{accountSummary.restrictions.length}
</button>
)}
</div>
{accountSummary.restrictions.length > 0 ? (
<div className="space-y-2">
{accountSummary.restrictions.slice(0, 2).map((record) => (
<div key={record.id} className="bg-white p-3 rounded-lg border border-red-200 hover:border-red-300 transition-colors">
<div className="flex items-center justify-between mb-1">
<span className={`text-xs font-medium ${getRestrictionLevelColor(record.level)}`}>
{record.reason}
</span>
<span className="text-xs text-red-600 bg-red-50 px-2 py-0.5 rounded-full">
{formatDateTime(record.date)}
</span>
</div>
<div className="text-xs text-red-500">
{formatDateTime(record.date)}
</div>
</div>
))}
</div>
) : (
<div className="text-center py-6">
<div className="flex flex-col items-center space-y-2">
<div className="p-2 bg-green-100 rounded-full">
<Shield className="w-5 h-5 text-green-600" />
</div>
<div className="text-green-700 font-medium text-sm"></div>
<div className="text-xs text-green-600">使</div>
</div>
</div>
)}
</div>
)}
</div>
) : (
<div className="space-y-4">
{/* 搜索栏 */}
<div className="flex items-center space-x-2 bg-white rounded-lg">
<div className="relative flex-1">
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
<input
placeholder="搜索好友昵称/微信号/备注/标签"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
className="w-full pl-9 pr-3 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white"
/>
</div>
</div>
{/* 好友列表 */}
<div className="space-y-2 min-h-[200px]">
{isFetchingFriends && friends.length === 0 ? (
<div className="flex items-center justify-center h-full">
<Loader2 className="h-8 w-8 animate-spin text-blue-500" />
</div>
) : hasFriendLoadError ? (
<div className="text-center py-8 text-red-500">
<p></p>
<button
className="mt-4 px-4 py-2 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors text-sm"
onClick={() => {
setHasFriendLoadError(false);
fetchFriends(1, true);
}}
>
</button>
</div>
) : isFriendsEmpty ? (
<div className="text-center py-8 text-gray-500">
<p></p>
{searchQuery && (
<button
className="mt-4 px-4 py-2 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors text-sm"
onClick={() => {
setSearchQuery("");
setIsFriendsEmpty(false);
fetchFriends(1, true);
}}
>
</button>
)}
</div>
) : friends.length === 0 ? (
<div className="text-center py-8 text-gray-500"></div>
) : (
<>
{friends.map((friend) => (
<div
key={friend.id}
className="flex items-center p-3 bg-white border rounded-lg hover:bg-gray-50 cursor-pointer transition-colors duration-200"
onClick={() => handleFriendClick(friend)}
>
<img
src={friend.avatar}
alt={friend.nickname}
className="w-10 h-10 rounded-full mr-3"
/>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between">
<div className="font-medium truncate max-w-[180px]">
{friend.nickname}
{friend.remark && <span className="text-gray-500 ml-1 truncate">({friend.remark})</span>}
</div>
<ChevronRight className="h-4 w-4 text-gray-400" />
</div>
<div className="text-sm text-gray-500 truncate">{friend.wechatId}</div>
<div className="flex flex-wrap gap-1 mt-1">
{friend.tags?.map((tag, index) => (
<span
key={index}
className="inline-flex items-center px-2 py-0.5 rounded text-xs bg-blue-100 text-blue-800"
>
{typeof tag === 'string' ? tag : tag.name}
</span>
))}
</div>
</div>
</div>
))}
{hasMoreFriends && !isFriendsEmpty && (
<div ref={friendsLoadingRef} className="flex justify-center py-4">
<Loader2 className="h-6 w-6 animate-spin text-blue-500" />
</div>
)}
</>
)}
</div>
</div>
)}
</div>
</div>
</div>
</div> {/* 这里补上闭合415行的<div className='p-4 space-y-4'> */}
{/* 限制记录详情弹窗 */}
{showRestrictions && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl p-6 max-w-md w-full max-h-[80vh] overflow-y-auto">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold"></h3>
<button
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
onClick={() => setShowRestrictions(false)}
>
<ChevronLeft className="h-5 w-5" />
</button>
</div>
<p className="text-sm text-gray-500 mb-4">24</p>
<div className="space-y-4">
{(accountSummary?.restrictions && accountSummary.restrictions.length > 0) ? (
accountSummary.restrictions.map((record) => (
<div key={record.id} className="border-b pb-4 last:border-0">
<div className="flex justify-between items-start">
<div className={`text-sm ${getRestrictionLevelColor(record.level)}`}>
{record.reason}
</div>
<span className="px-2 py-1 border border-gray-200 rounded text-xs">{formatDateTime(record.date)}</span>
</div>
<div className="text-sm text-gray-500 mt-1">{formatDateTime(record.date)}</div>
</div>
))
) : (
<div className="text-center py-8 text-green-500">
</div>
)}
</div>
</div>
</div>
)}
{/* 好友转移确认对话框 */}
{showTransferConfirm && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl p-6 max-w-md w-full">
<h3 className="text-lg font-semibold mb-2"></h3>
<p className="text-sm text-gray-500 mb-4"></p>
<div className="py-4">
<div className="flex items-center space-x-3 p-3 bg-blue-50 rounded-lg">
<img
src={currentAccount.avatar}
alt={currentAccount.nickname}
className="w-10 h-10 rounded-full"
/>
<div>
<div className="font-medium">{currentAccount.nickname}</div>
<div className="text-sm text-gray-500">{currentAccount.wechatId}</div>
</div>
</div>
<div className="mt-4 text-sm text-gray-500">
<p> </p>
<p> </p>
<p> </p>
</div>
</div>
<div className="flex space-x-3">
<button
className="flex-1 px-4 py-2 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors"
onClick={() => setShowTransferConfirm(false)}
>
</button>
<button
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
onClick={confirmTransferFriends}
>
</button>
</div>
</div>
</div>
)}
{/* 好友详情对话框 */}
{showFriendDetail && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl p-6 max-w-md w-full max-h-[80vh] overflow-y-auto">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold"></h3>
<button
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
onClick={() => setShowFriendDetail(false)}
>
<ChevronLeft className="h-5 w-5" />
</button>
</div>
{isLoadingFriendDetail ? (
<div className="flex justify-center items-center py-10">
<Loader2 className="h-8 w-8 animate-spin text-blue-500" />
</div>
) : friendDetailError ? (
<div className="text-center py-8 text-red-500">
<p>{friendDetailError}</p>
<button
className="mt-4 px-4 py-2 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors text-sm"
onClick={() => handleFriendClick(selectedFriend!)}
>
</button>
</div>
) : friendDetail && selectedFriend ? (
<div className="space-y-4">
<div className="flex items-center space-x-3">
<img
src={selectedFriend.avatar}
alt={selectedFriend.nickname}
className="w-12 h-12 rounded-full"
/>
<div>
<h4 className="font-medium">{selectedFriend.nickname}</h4>
<p className="text-sm text-gray-500">{selectedFriend.wechatId}</p>
</div>
</div>
<div className="space-y-3">
<div className="flex justify-between">
<span className="text-sm text-gray-500"></span>
<span className="text-sm">{friendDetail.region || "未知"}</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-gray-500"></span>
<span className="text-sm">{friendDetail.addDate}</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-gray-500"></span>
<span className="text-sm">{friendDetail.source || "未知"}</span>
</div>
{friendDetail.memo && (
<div className="flex justify-between">
<span className="text-sm text-gray-500"></span>
<span className="text-sm">{friendDetail.memo}</span>
</div>
)}
{friendDetail.tags && friendDetail.tags.length > 0 && (
<div>
<span className="text-sm text-gray-500 block mb-2"></span>
<div className="flex flex-wrap gap-1">
{friendDetail.tags.map((tag, index) => (
<span
key={index}
className="px-2 py-1 text-xs rounded-full bg-blue-100 text-blue-800"
>
{tag}
</span>
))}
</div>
</div>
)}
</div>
</div>
) : null}
</div>
</div>
)}
</Layout>
);
}