微信号的好友列表
This commit is contained in:
@@ -204,4 +204,39 @@ const mapRestrictionType = (type: string): "friend_limit" | "marketing" | "spam"
|
|||||||
};
|
};
|
||||||
|
|
||||||
return typeMap[type] || 'other';
|
return typeMap[type] || 'other';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取微信好友列表
|
||||||
|
* @param wechatId 微信账号ID
|
||||||
|
* @param page 页码
|
||||||
|
* @param limit 每页数量
|
||||||
|
* @param keyword 搜索关键词
|
||||||
|
* @returns 好友列表数据
|
||||||
|
*/
|
||||||
|
export const fetchWechatFriends = async (
|
||||||
|
wechatId: string | number,
|
||||||
|
page: number = 1,
|
||||||
|
limit: number = 20,
|
||||||
|
keyword: string = ""
|
||||||
|
): Promise<{ code: number; msg: string; data: any }> => {
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
wechatId: String(wechatId),
|
||||||
|
page: String(page),
|
||||||
|
limit: String(limit)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (keyword) {
|
||||||
|
params.append('keyword', keyword);
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `/v1/device/wechats/friends?${params.toString()}`;
|
||||||
|
const response = await api.get<{ code: number; msg: string; data: any }>(url);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取微信好友列表失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState, useEffect } from "react"
|
import { useState, useEffect, useRef, useCallback } from "react"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { Card } from "@/components/ui/card"
|
import { Card } from "@/components/ui/card"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
@@ -44,7 +44,7 @@ import {
|
|||||||
PaginationPrevious,
|
PaginationPrevious,
|
||||||
} from "@/components/ui/pagination"
|
} from "@/components/ui/pagination"
|
||||||
import { toast } from "@/components/ui/use-toast"
|
import { toast } from "@/components/ui/use-toast"
|
||||||
import { fetchWechatAccountDetail, transformWechatAccountDetail } from "@/api/wechat-accounts"
|
import { fetchWechatAccountDetail, transformWechatAccountDetail, fetchWechatFriends } from "@/api/wechat-accounts"
|
||||||
|
|
||||||
interface RestrictionRecord {
|
interface RestrictionRecord {
|
||||||
id: string
|
id: string
|
||||||
@@ -120,11 +120,274 @@ export default function WechatAccountDetailPage({ params }: { params: { id: stri
|
|||||||
const [showFriendDetail, setShowFriendDetail] = useState(false)
|
const [showFriendDetail, setShowFriendDetail] = useState(false)
|
||||||
const [selectedFriend, setSelectedFriend] = useState<WechatFriend | null>(null)
|
const [selectedFriend, setSelectedFriend] = useState<WechatFriend | null>(null)
|
||||||
const [searchQuery, setSearchQuery] = useState("")
|
const [searchQuery, setSearchQuery] = useState("")
|
||||||
const [currentPage, setCurrentPage] = useState(1)
|
|
||||||
const [activeTab, setActiveTab] = useState("overview")
|
const [activeTab, setActiveTab] = useState("overview")
|
||||||
const friendsPerPage = 10
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
|
// 好友列表相关状态
|
||||||
|
const [friends, setFriends] = useState<any[]>([])
|
||||||
|
const [friendsPage, setFriendsPage] = useState(1)
|
||||||
|
const [friendsTotal, setFriendsTotal] = useState(0)
|
||||||
|
const [hasMoreFriends, setHasMoreFriends] = useState(true)
|
||||||
|
const [isFetchingFriends, setIsFetchingFriends] = useState(false)
|
||||||
|
const friendsObserver = useRef<IntersectionObserver | null>(null)
|
||||||
|
const friendsLoadingRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
const friendsContainerRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
|
// 计算好友列表容器高度
|
||||||
|
const getFriendsContainerHeight = () => {
|
||||||
|
// 最少显示一条记录的高度,最多显示十条记录的高度
|
||||||
|
const minHeight = 80; // 单条记录高度
|
||||||
|
const maxHeight = 800; // 十条记录高度
|
||||||
|
|
||||||
|
if (friends.length === 0) return minHeight;
|
||||||
|
return Math.min(Math.max(friends.length * 80, minHeight), maxHeight);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 生成模拟账号数据(作为备用,服务器请求失败时使用)
|
||||||
|
const generateMockAccountData = (): WechatAccountDetail => {
|
||||||
|
// 生成随机标签
|
||||||
|
const generateRandomTags = (count: number): FriendTag[] => {
|
||||||
|
const tagPool = [
|
||||||
|
{ name: "潜在客户", color: "bg-blue-100 text-blue-800" },
|
||||||
|
{ name: "高意向", color: "bg-green-100 text-green-800" },
|
||||||
|
{ name: "已成交", color: "bg-purple-100 text-purple-800" },
|
||||||
|
{ name: "需跟进", color: "bg-yellow-100 text-yellow-800" },
|
||||||
|
{ name: "活跃用户", color: "bg-indigo-100 text-indigo-800" },
|
||||||
|
{ name: "沉默用户", color: "bg-gray-100 text-gray-800" },
|
||||||
|
{ name: "企业客户", color: "bg-red-100 text-red-800" },
|
||||||
|
{ name: "个人用户", color: "bg-pink-100 text-pink-800" },
|
||||||
|
{ name: "新增好友", color: "bg-emerald-100 text-emerald-800" },
|
||||||
|
{ name: "老客户", color: "bg-amber-100 text-amber-800" },
|
||||||
|
];
|
||||||
|
|
||||||
|
return Array.from({ length: Math.floor(Math.random() * count) + 1 }, () => {
|
||||||
|
const randomTag = tagPool[Math.floor(Math.random() * tagPool.length)];
|
||||||
|
return {
|
||||||
|
id: `tag-${Math.random().toString(36).substring(2, 9)}`,
|
||||||
|
name: randomTag.name,
|
||||||
|
color: randomTag.color,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 生成随机好友
|
||||||
|
const friendCount = Math.floor(Math.random() * (300 - 150)) + 150;
|
||||||
|
const generateFriends = (count: number): WechatFriend[] => {
|
||||||
|
return Array.from({ length: count }, (_, i) => {
|
||||||
|
const firstName = ["张", "王", "李", "赵", "陈", "刘", "杨", "黄", "周", "吴"][Math.floor(Math.random() * 10)];
|
||||||
|
const secondName = ["小", "大", "明", "华", "强", "伟", "芳", "娜", "秀", "英"][
|
||||||
|
Math.floor(Math.random() * 10)
|
||||||
|
];
|
||||||
|
const lastName = ["明", "华", "强", "伟", "芳", "娜", "秀", "英", "军", "杰"][Math.floor(Math.random() * 10)];
|
||||||
|
const nickname = firstName + secondName + lastName;
|
||||||
|
|
||||||
|
// 生成随机的添加时间(过去1年内)
|
||||||
|
const addDate = new Date();
|
||||||
|
addDate.setDate(addDate.getDate() - Math.floor(Math.random() * 365));
|
||||||
|
|
||||||
|
// 生成随机的最后互动时间(过去30天内)
|
||||||
|
const lastDate = new Date();
|
||||||
|
lastDate.setDate(lastDate.getDate() - Math.floor(Math.random() * 30));
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: `friend-${i}`,
|
||||||
|
avatar: `/placeholder.svg?height=40&width=40&text=${nickname[0]}`,
|
||||||
|
nickname,
|
||||||
|
wechatId: `wxid_${Math.random().toString(36).substring(2, 9)}`,
|
||||||
|
remark:
|
||||||
|
Math.random() > 0.5
|
||||||
|
? `${nickname}(${["同事", "客户", "朋友", "同学"][Math.floor(Math.random() * 4)]})`
|
||||||
|
: "",
|
||||||
|
addTime: addDate.toISOString().split("T")[0],
|
||||||
|
lastInteraction: lastDate.toISOString().split("T")[0],
|
||||||
|
tags: generateRandomTags(3),
|
||||||
|
region: ["广东", "北京", "上海", "浙江", "江苏", "四川", "湖北", "福建", "山东", "河南"][
|
||||||
|
Math.floor(Math.random() * 10)
|
||||||
|
],
|
||||||
|
source: ["抖音", "小红书", "朋友介绍", "搜索添加", "群聊", "附近的人", "名片分享"][
|
||||||
|
Math.floor(Math.random() * 7)
|
||||||
|
],
|
||||||
|
notes:
|
||||||
|
Math.random() > 0.7
|
||||||
|
? ["对产品很感兴趣", "需要进一步跟进", "已购买过产品", "价格敏感", "需要更多信息"][
|
||||||
|
Math.floor(Math.random() * 5)
|
||||||
|
]
|
||||||
|
: "",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const friends = generateFriends(friendCount);
|
||||||
|
|
||||||
|
const mockAccount: WechatAccountDetail = {
|
||||||
|
id: params.id,
|
||||||
|
avatar:
|
||||||
|
"https://hebbkx1anhila5yf.public.blob.vercel-storage.com/img_v3_02jn_e7fcc2a4-3560-478d-911a-4ccd69c6392g.jpg-a8zVtwxMuSrPWN9dfWH93EBY0yM3Dh.jpeg",
|
||||||
|
nickname: "卡若-25vig",
|
||||||
|
wechatId: `wxid_${Math.random().toString(36).substr(2, 8)}`,
|
||||||
|
deviceId: "device-1",
|
||||||
|
deviceName: "设备1",
|
||||||
|
friendCount: friends.length,
|
||||||
|
todayAdded: 12,
|
||||||
|
status: "normal",
|
||||||
|
lastActive: new Date().toLocaleString(),
|
||||||
|
messageCount: 1234,
|
||||||
|
activeRate: 87,
|
||||||
|
accountAge: {
|
||||||
|
years: 2,
|
||||||
|
months: 8,
|
||||||
|
},
|
||||||
|
totalChats: 15234,
|
||||||
|
chatFrequency: 42,
|
||||||
|
restrictionRecords: [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
date: "2024-02-25",
|
||||||
|
reason: "添加好友过于频繁",
|
||||||
|
recoveryTime: "2024-02-26",
|
||||||
|
type: "friend_limit",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
date: "2024-01-15",
|
||||||
|
reason: "营销内容违规",
|
||||||
|
recoveryTime: "2024-01-16",
|
||||||
|
type: "marketing",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isVerified: true,
|
||||||
|
firstMomentDate: "2021-06-15",
|
||||||
|
accountWeight: 85,
|
||||||
|
weightFactors: {
|
||||||
|
restrictionFactor: 0.8,
|
||||||
|
verificationFactor: 1.0,
|
||||||
|
ageFactor: 0.9,
|
||||||
|
activityFactor: 0.85,
|
||||||
|
},
|
||||||
|
weeklyStats: Array.from({ length: 7 }, (_, i) => ({
|
||||||
|
date: `Day ${i + 1}`,
|
||||||
|
friends: Math.floor(Math.random() * 50) + 50,
|
||||||
|
messages: Math.floor(Math.random() * 100) + 100,
|
||||||
|
})),
|
||||||
|
friends: friends,
|
||||||
|
};
|
||||||
|
return mockAccount;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 随机生成标签颜色
|
||||||
|
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 fetchFriends = useCallback(async (page: number = 1, isNewSearch: boolean = false) => {
|
||||||
|
if (!account || isFetchingFriends) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsFetchingFriends(true);
|
||||||
|
|
||||||
|
// 调用API获取好友列表
|
||||||
|
const response = await fetchWechatFriends(account.wechatId, page, 20, searchQuery);
|
||||||
|
|
||||||
|
if (response && response.code === 200) {
|
||||||
|
const newFriends = response.data.list.map((friend: any) => ({
|
||||||
|
id: friend.wechatId,
|
||||||
|
avatar: friend.avatar,
|
||||||
|
nickname: friend.nickname || '未设置昵称',
|
||||||
|
wechatId: friend.wechatId,
|
||||||
|
remark: friend.remark || '',
|
||||||
|
addTime: '2024-01-01', // 接口未返回,使用默认值
|
||||||
|
lastInteraction: '2024-01-01', // 接口未返回,使用默认值
|
||||||
|
tags: (friend.labels || []).map((label: string, index: number) => ({
|
||||||
|
id: `tag-${index}`,
|
||||||
|
name: label,
|
||||||
|
color: getRandomTagColor(),
|
||||||
|
})),
|
||||||
|
region: friend.region || '未知地区',
|
||||||
|
source: '微信好友', // 接口未返回,使用默认值
|
||||||
|
notes: '',
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 更新状态
|
||||||
|
if (isNewSearch) {
|
||||||
|
setFriends(newFriends);
|
||||||
|
} else {
|
||||||
|
setFriends(prev => [...prev, ...newFriends]);
|
||||||
|
}
|
||||||
|
|
||||||
|
setFriendsTotal(response.data.total);
|
||||||
|
setFriendsPage(page);
|
||||||
|
setHasMoreFriends(page * 20 < response.data.total);
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
title: "获取好友列表失败",
|
||||||
|
description: response?.msg || "请稍后再试",
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取好友列表失败:", error);
|
||||||
|
toast({
|
||||||
|
title: "获取好友列表失败",
|
||||||
|
description: "请检查网络连接或稍后再试",
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsFetchingFriends(false);
|
||||||
|
}
|
||||||
|
}, [account, searchQuery]);
|
||||||
|
|
||||||
|
// 处理搜索
|
||||||
|
const handleSearch = useCallback(() => {
|
||||||
|
setFriends([]);
|
||||||
|
setFriendsPage(1);
|
||||||
|
setHasMoreFriends(true);
|
||||||
|
fetchFriends(1, true);
|
||||||
|
}, [fetchFriends]);
|
||||||
|
|
||||||
|
// 处理标签切换
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeTab === "friends" && account && friends.length === 0) {
|
||||||
|
fetchFriends(1, true);
|
||||||
|
}
|
||||||
|
}, [activeTab, account, friends.length, fetchFriends]);
|
||||||
|
|
||||||
|
// 设置IntersectionObserver用于懒加载
|
||||||
|
useEffect(() => {
|
||||||
|
friendsObserver.current = new IntersectionObserver((entries) => {
|
||||||
|
if (entries[0].isIntersecting && hasMoreFriends && !isFetchingFriends) {
|
||||||
|
fetchFriends(friendsPage + 1);
|
||||||
|
}
|
||||||
|
}, { threshold: 0.5 });
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (friendsObserver.current) {
|
||||||
|
friendsObserver.current.disconnect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [fetchFriends, friendsPage, hasMoreFriends, isFetchingFriends]);
|
||||||
|
|
||||||
|
// 观察加载指示器
|
||||||
|
useEffect(() => {
|
||||||
|
if (friendsLoadingRef.current && friendsObserver.current) {
|
||||||
|
friendsObserver.current.observe(friendsLoadingRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (friendsLoadingRef.current && friendsObserver.current) {
|
||||||
|
friendsObserver.current.unobserve(friendsLoadingRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [friendsLoadingRef.current, friendsObserver.current]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 模拟API调用获取账号详情
|
// 模拟API调用获取账号详情
|
||||||
const fetchAccount = async () => {
|
const fetchAccount = async () => {
|
||||||
@@ -199,23 +462,23 @@ export default function WechatAccountDetailPage({ params }: { params: { id: stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
const formatAccountAge = (age: { years: number; months: number }) => {
|
const formatAccountAge = (age: { years: number; months: number }) => {
|
||||||
if (age.years === 0) {
|
if (age.years > 0) {
|
||||||
return `${age.months}个月`
|
return `${age.years}年${age.months}个月`;
|
||||||
}
|
}
|
||||||
if (age.months === 0) {
|
return `${age.months}个月`;
|
||||||
return `${age.years}年`
|
};
|
||||||
}
|
|
||||||
return `${age.years}年${age.months}个月`
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleTransferFriends = () => {
|
const handleTransferFriends = () => {
|
||||||
setShowTransferConfirm(true)
|
setShowTransferConfirm(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmTransferFriends = () => {
|
const confirmTransferFriends = () => {
|
||||||
|
// 模拟API调用
|
||||||
|
toast({
|
||||||
|
title: "好友转移成功",
|
||||||
|
description: `已成功转移 ${account?.friends.length} 个好友`,
|
||||||
|
});
|
||||||
setShowTransferConfirm(false)
|
setShowTransferConfirm(false)
|
||||||
// 跳转到新建计划的订单导入场景
|
|
||||||
router.push(`/scenarios/new?type=order&source=${account.wechatId}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleFriendClick = (friend: WechatFriend) => {
|
const handleFriendClick = (friend: WechatFriend) => {
|
||||||
@@ -223,150 +486,6 @@ export default function WechatAccountDetailPage({ params }: { params: { id: stri
|
|||||||
setShowFriendDetail(true)
|
setShowFriendDetail(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 过滤好友
|
|
||||||
const filteredFriends = account.friends.filter(
|
|
||||||
(friend) =>
|
|
||||||
friend.nickname.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
||||||
friend.wechatId.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
||||||
friend.remark.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
||||||
friend.tags.some((tag) => tag.name.toLowerCase().includes(searchQuery.toLowerCase())),
|
|
||||||
)
|
|
||||||
|
|
||||||
// 分页
|
|
||||||
const totalPages = Math.ceil(filteredFriends.length / friendsPerPage)
|
|
||||||
const paginatedFriends = filteredFriends.slice((currentPage - 1) * friendsPerPage, currentPage * friendsPerPage)
|
|
||||||
|
|
||||||
// 生成模拟账号数据(作为备用,服务器请求失败时使用)
|
|
||||||
const generateMockAccountData = () => {
|
|
||||||
// 生成随机标签
|
|
||||||
const generateRandomTags = (count: number) => {
|
|
||||||
const tagPool = [
|
|
||||||
{ name: "潜在客户", color: "bg-blue-100 text-blue-800" },
|
|
||||||
{ name: "高意向", color: "bg-green-100 text-green-800" },
|
|
||||||
{ name: "已成交", color: "bg-purple-100 text-purple-800" },
|
|
||||||
{ name: "需跟进", color: "bg-yellow-100 text-yellow-800" },
|
|
||||||
{ name: "活跃用户", color: "bg-indigo-100 text-indigo-800" },
|
|
||||||
{ name: "沉默用户", color: "bg-gray-100 text-gray-800" },
|
|
||||||
{ name: "企业客户", color: "bg-red-100 text-red-800" },
|
|
||||||
{ name: "个人用户", color: "bg-pink-100 text-pink-800" },
|
|
||||||
{ name: "新增好友", color: "bg-emerald-100 text-emerald-800" },
|
|
||||||
{ name: "老客户", color: "bg-amber-100 text-amber-800" },
|
|
||||||
]
|
|
||||||
|
|
||||||
return Array.from({ length: Math.floor(Math.random() * count) + 1 }, () => {
|
|
||||||
const randomTag = tagPool[Math.floor(Math.random() * tagPool.length)]
|
|
||||||
return {
|
|
||||||
id: `tag-${Math.random().toString(36).substring(2, 9)}`,
|
|
||||||
name: randomTag.name,
|
|
||||||
color: randomTag.color,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成随机好友
|
|
||||||
const friendCount = Math.floor(Math.random() * (300 - 150)) + 150
|
|
||||||
const generateFriends = (count: number) => {
|
|
||||||
return Array.from({ length: count }, (_, i) => {
|
|
||||||
const firstName = ["张", "王", "李", "赵", "陈", "刘", "杨", "黄", "周", "吴"][Math.floor(Math.random() * 10)]
|
|
||||||
const secondName = ["小", "大", "明", "华", "强", "伟", "芳", "娜", "秀", "英"][
|
|
||||||
Math.floor(Math.random() * 10)
|
|
||||||
]
|
|
||||||
const lastName = ["明", "华", "强", "伟", "芳", "娜", "秀", "英", "军", "杰"][Math.floor(Math.random() * 10)]
|
|
||||||
const nickname = firstName + secondName + lastName
|
|
||||||
|
|
||||||
// 生成随机的添加时间(过去1年内)
|
|
||||||
const addDate = new Date()
|
|
||||||
addDate.setDate(addDate.getDate() - Math.floor(Math.random() * 365))
|
|
||||||
|
|
||||||
// 生成随机的最后互动时间(过去30天内)
|
|
||||||
const lastDate = new Date()
|
|
||||||
lastDate.setDate(lastDate.getDate() - Math.floor(Math.random() * 30))
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: `friend-${i}`,
|
|
||||||
avatar: `/placeholder.svg?height=40&width=40&text=${nickname[0]}`,
|
|
||||||
nickname,
|
|
||||||
wechatId: `wxid_${Math.random().toString(36).substring(2, 9)}`,
|
|
||||||
remark:
|
|
||||||
Math.random() > 0.5
|
|
||||||
? `${nickname}(${["同事", "客户", "朋友", "同学"][Math.floor(Math.random() * 4)]})`
|
|
||||||
: "",
|
|
||||||
addTime: addDate.toISOString().split("T")[0],
|
|
||||||
lastInteraction: lastDate.toISOString().split("T")[0],
|
|
||||||
tags: generateRandomTags(3),
|
|
||||||
region: ["广东", "北京", "上海", "浙江", "江苏", "四川", "湖北", "福建", "山东", "河南"][
|
|
||||||
Math.floor(Math.random() * 10)
|
|
||||||
],
|
|
||||||
source: ["抖音", "小红书", "朋友介绍", "搜索添加", "群聊", "附近的人", "名片分享"][
|
|
||||||
Math.floor(Math.random() * 7)
|
|
||||||
],
|
|
||||||
notes:
|
|
||||||
Math.random() > 0.7
|
|
||||||
? ["对产品很感兴趣", "需要进一步跟进", "已购买过产品", "价格敏感", "需要更多信息"][
|
|
||||||
Math.floor(Math.random() * 5)
|
|
||||||
]
|
|
||||||
: "",
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const friends = generateFriends(friendCount)
|
|
||||||
|
|
||||||
const mockAccount: WechatAccountDetail = {
|
|
||||||
id: params.id,
|
|
||||||
avatar:
|
|
||||||
"https://hebbkx1anhila5yf.public.blob.vercel-storage.com/img_v3_02jn_e7fcc2a4-3560-478d-911a-4ccd69c6392g.jpg-a8zVtwxMuSrPWN9dfWH93EBY0yM3Dh.jpeg",
|
|
||||||
nickname: "卡若-25vig",
|
|
||||||
wechatId: `wxid_${Math.random().toString(36).substr(2, 8)}`,
|
|
||||||
deviceId: "device-1",
|
|
||||||
deviceName: "设备1",
|
|
||||||
friendCount: friends.length,
|
|
||||||
todayAdded: 12,
|
|
||||||
status: "normal",
|
|
||||||
lastActive: new Date().toLocaleString(),
|
|
||||||
messageCount: 1234,
|
|
||||||
activeRate: 87,
|
|
||||||
accountAge: {
|
|
||||||
years: 2,
|
|
||||||
months: 8,
|
|
||||||
},
|
|
||||||
totalChats: 15234,
|
|
||||||
chatFrequency: 42,
|
|
||||||
restrictionRecords: [
|
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
date: "2024-02-25",
|
|
||||||
reason: "添加好友过于频繁",
|
|
||||||
recoveryTime: "2024-02-26",
|
|
||||||
type: "friend_limit",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "2",
|
|
||||||
date: "2024-01-15",
|
|
||||||
reason: "营销内容违规",
|
|
||||||
recoveryTime: "2024-01-16",
|
|
||||||
type: "marketing",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
isVerified: true,
|
|
||||||
firstMomentDate: "2021-06-15",
|
|
||||||
accountWeight: 85,
|
|
||||||
weightFactors: {
|
|
||||||
restrictionFactor: 0.8,
|
|
||||||
verificationFactor: 1.0,
|
|
||||||
ageFactor: 0.9,
|
|
||||||
activityFactor: 0.85,
|
|
||||||
},
|
|
||||||
weeklyStats: Array.from({ length: 7 }, (_, i) => ({
|
|
||||||
date: `Day ${i + 1}`,
|
|
||||||
friends: Math.floor(Math.random() * 50) + 50,
|
|
||||||
messages: Math.floor(Math.random() * 100) + 100,
|
|
||||||
})),
|
|
||||||
friends: friends,
|
|
||||||
}
|
|
||||||
return mockAccount
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
@@ -561,107 +680,82 @@ export default function WechatAccountDetailPage({ params }: { params: { id: stri
|
|||||||
placeholder="搜索好友昵称/微信号/备注/标签"
|
placeholder="搜索好友昵称/微信号/备注/标签"
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||||
className="pl-9"
|
className="pl-9"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" size="icon">
|
<Button variant="outline" size="icon" onClick={handleSearch}>
|
||||||
<Filter className="h-4 w-4" />
|
<Filter className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 好友列表 */}
|
{/* 好友列表 */}
|
||||||
<div className="space-y-2">
|
<div
|
||||||
{paginatedFriends.length === 0 ? (
|
ref={friendsContainerRef}
|
||||||
|
className="space-y-2 transition-all duration-300"
|
||||||
|
style={{
|
||||||
|
minHeight: '80px',
|
||||||
|
height: `${getFriendsContainerHeight()}px`,
|
||||||
|
overflowY: 'auto'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{friends.length === 0 && !isFetchingFriends ? (
|
||||||
<div className="text-center py-8 text-gray-500">未找到匹配的好友</div>
|
<div className="text-center py-8 text-gray-500">未找到匹配的好友</div>
|
||||||
) : (
|
) : (
|
||||||
paginatedFriends.map((friend) => (
|
<>
|
||||||
<div
|
{friends.map((friend) => (
|
||||||
key={friend.id}
|
<div
|
||||||
className="flex items-center p-3 border rounded-lg hover:bg-gray-50 cursor-pointer"
|
key={friend.id}
|
||||||
onClick={() => handleFriendClick(friend)}
|
className="flex items-center p-3 border rounded-lg hover:bg-gray-50 cursor-pointer"
|
||||||
>
|
onClick={() => handleFriendClick(friend)}
|
||||||
<Avatar className="h-10 w-10 mr-3">
|
>
|
||||||
<AvatarImage src={friend.avatar} />
|
<Avatar className="h-10 w-10 mr-3">
|
||||||
<AvatarFallback>{friend.nickname[0]}</AvatarFallback>
|
<AvatarImage src={friend.avatar} />
|
||||||
</Avatar>
|
<AvatarFallback>{friend.nickname?.[0] || 'U'}</AvatarFallback>
|
||||||
<div className="flex-1 min-w-0">
|
</Avatar>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="font-medium truncate max-w-[180px]">
|
<div className="flex items-center justify-between">
|
||||||
{friend.nickname}
|
<div className="font-medium truncate max-w-[180px]">
|
||||||
{friend.remark && <span className="text-gray-500 ml-1 truncate">({friend.remark})</span>}
|
{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.slice(0, 3).map((tag: FriendTag) => (
|
||||||
|
<span key={tag.id} className={`text-xs px-2 py-0.5 rounded-full ${tag.color}`}>
|
||||||
|
{tag.name}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
{friend.tags.length > 3 && (
|
||||||
|
<span className="text-xs px-2 py-0.5 rounded-full bg-gray-100 text-gray-800">
|
||||||
|
+{friend.tags.length - 3}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</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.slice(0, 3).map((tag) => (
|
|
||||||
<span key={tag.id} className={`text-xs px-2 py-0.5 rounded-full ${tag.color}`}>
|
|
||||||
{tag.name}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
{friend.tags.length > 3 && (
|
|
||||||
<span className="text-xs px-2 py-0.5 rounded-full bg-gray-100 text-gray-800">
|
|
||||||
+{friend.tags.length - 3}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))}
|
||||||
))
|
|
||||||
|
{/* 懒加载指示器 */}
|
||||||
|
{hasMoreFriends && (
|
||||||
|
<div ref={friendsLoadingRef} className="py-4 flex justify-center">
|
||||||
|
{isFetchingFriends && <Loader2 className="h-6 w-6 animate-spin text-blue-500" />}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 分页 */}
|
{/* 显示加载状态和总数 */}
|
||||||
{totalPages > 1 && (
|
<div className="text-sm text-gray-500 text-center">
|
||||||
<Pagination>
|
{friendsTotal > 0 && (
|
||||||
<PaginationContent>
|
<span>
|
||||||
<PaginationItem>
|
已加载 {Math.min(friends.length, friendsTotal)} / {friendsTotal} 条记录
|
||||||
<PaginationPrevious
|
</span>
|
||||||
href="#"
|
)}
|
||||||
onClick={(e) => {
|
</div>
|
||||||
e.preventDefault()
|
|
||||||
setCurrentPage((prev) => Math.max(1, prev - 1))
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</PaginationItem>
|
|
||||||
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
|
|
||||||
let pageNumber
|
|
||||||
if (totalPages <= 5) {
|
|
||||||
pageNumber = i + 1
|
|
||||||
} else if (currentPage <= 3) {
|
|
||||||
pageNumber = i + 1
|
|
||||||
} else if (currentPage >= totalPages - 2) {
|
|
||||||
pageNumber = totalPages - 4 + i
|
|
||||||
} else {
|
|
||||||
pageNumber = currentPage - 2 + i
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<PaginationItem key={pageNumber}>
|
|
||||||
<PaginationLink
|
|
||||||
href="#"
|
|
||||||
isActive={currentPage === pageNumber}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
setCurrentPage(pageNumber)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{pageNumber}
|
|
||||||
</PaginationLink>
|
|
||||||
</PaginationItem>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
<PaginationItem>
|
|
||||||
<PaginationNext
|
|
||||||
href="#"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
setCurrentPage((prev) => Math.min(totalPages, prev + 1))
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</PaginationItem>
|
|
||||||
</PaginationContent>
|
|
||||||
</Pagination>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
@@ -770,7 +864,7 @@ export default function WechatAccountDetailPage({ params }: { params: { id: stri
|
|||||||
标签
|
标签
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{selectedFriend.tags.map((tag) => (
|
{selectedFriend.tags.map((tag: FriendTag) => (
|
||||||
<span key={tag.id} className={`text-sm px-2 py-1 rounded-full ${tag.color}`}>
|
<span key={tag.id} className={`text-sm px-2 py-1 rounded-full ${tag.color}`}>
|
||||||
{tag.name}
|
{tag.name}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ Route::group('v1/', function () {
|
|||||||
|
|
||||||
// 设备微信相关
|
// 设备微信相关
|
||||||
Route::group('device/wechats', function () {
|
Route::group('device/wechats', function () {
|
||||||
|
Route::get('friends', 'app\\devices\\controller\\DeviceWechat@getFriends'); // 获取微信好友列表
|
||||||
Route::get('count', 'app\\devices\\controller\\DeviceWechat@count'); // 获取在线微信账号数量
|
Route::get('count', 'app\\devices\\controller\\DeviceWechat@count'); // 获取在线微信账号数量
|
||||||
Route::get('device-count', 'app\\devices\\controller\\DeviceWechat@deviceCount'); // 获取有登录微信的设备数量
|
Route::get('device-count', 'app\\devices\\controller\\DeviceWechat@deviceCount'); // 获取有登录微信的设备数量
|
||||||
Route::get('', 'app\\devices\\controller\\DeviceWechat@index'); // 获取在线微信账号列表
|
Route::get('', 'app\\devices\\controller\\DeviceWechat@index'); // 获取在线微信账号列表
|
||||||
|
|||||||
@@ -274,23 +274,6 @@ class DeviceWechat extends Controller
|
|||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
// 获取微信好友列表
|
|
||||||
$friends = Db::table('tk_wechat_friend')
|
|
||||||
->where('wechatAccountId', $id)
|
|
||||||
->where('isDeleted', 0)
|
|
||||||
->field([
|
|
||||||
'id',
|
|
||||||
'wechatId',
|
|
||||||
'nickname',
|
|
||||||
'avatar',
|
|
||||||
'gender',
|
|
||||||
'region',
|
|
||||||
'signature',
|
|
||||||
'labels',
|
|
||||||
'createTime'
|
|
||||||
])
|
|
||||||
->select();
|
|
||||||
|
|
||||||
// 处理返回数据
|
// 处理返回数据
|
||||||
$data = [
|
$data = [
|
||||||
'basicInfo' => [
|
'basicInfo' => [
|
||||||
@@ -322,7 +305,6 @@ class DeviceWechat extends Controller
|
|||||||
'lastUpdateTime' => $wechat['updateTime']
|
'lastUpdateTime' => $wechat['updateTime']
|
||||||
],
|
],
|
||||||
'restrictions' => $restrictions,
|
'restrictions' => $restrictions,
|
||||||
'friends' => $friends
|
|
||||||
];
|
];
|
||||||
|
|
||||||
return json([
|
return json([
|
||||||
@@ -528,4 +510,73 @@ class DeviceWechat extends Controller
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取微信好友列表
|
||||||
|
* 根据wechatId查询微信好友,支持分页和关键词筛选
|
||||||
|
*
|
||||||
|
* @return \think\response\Json
|
||||||
|
*/
|
||||||
|
public function getFriends()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// 获取请求参数
|
||||||
|
$wechatId = Request::param('wechatId');
|
||||||
|
$page = (int)Request::param('page', 1);
|
||||||
|
$limit = (int)Request::param('limit', 20);
|
||||||
|
$keyword = Request::param('keyword', '');
|
||||||
|
|
||||||
|
// 参数验证
|
||||||
|
if (empty($wechatId)) {
|
||||||
|
return json([
|
||||||
|
'code' => 400,
|
||||||
|
'msg' => '参数错误:微信ID不能为空'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询参数
|
||||||
|
$params = [];
|
||||||
|
if (!empty($keyword)) {
|
||||||
|
$params['keyword'] = $keyword;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用模型方法获取好友列表
|
||||||
|
$result = \app\devices\model\WechatFriend::getFriendsByWechatId($wechatId, $params, $page, $limit);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 处理返回的数据
|
||||||
|
$friendsList = [];
|
||||||
|
foreach ($result['list'] as $friend) {
|
||||||
|
$friendsList[] = [
|
||||||
|
'wechatId' => $friend['wechatId'],
|
||||||
|
'avatar' => $friend['avatar'] ?: '/placeholder.svg',
|
||||||
|
'labels' => $friend['labels'] ?: [],
|
||||||
|
'accountNickname' => $friend['accountNickname'] ?: '',
|
||||||
|
'accountRealName' => $friend['accountRealName'] ?: '',
|
||||||
|
'nickname' => $friend['nickname'] ?: '',
|
||||||
|
'remark' => $friend['conRemark'] ?: '',
|
||||||
|
'alias' => $friend['alias'] ?: '',
|
||||||
|
'gender' => $friend['gender'] ?: 0,
|
||||||
|
'region' => $friend['region'] ?: ''
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return json([
|
||||||
|
'code' => 200,
|
||||||
|
'msg' => '获取成功',
|
||||||
|
'data' => [
|
||||||
|
'total' => $result['total'],
|
||||||
|
'page' => $result['page'],
|
||||||
|
'limit' => $result['limit'],
|
||||||
|
'list' => $friendsList
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return json([
|
||||||
|
'code' => 500,
|
||||||
|
'msg' => '获取失败:' . $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
84
Server/application/devices/model/WechatFriend.php
Normal file
84
Server/application/devices/model/WechatFriend.php
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
namespace app\devices\model;
|
||||||
|
|
||||||
|
use think\Model;
|
||||||
|
use think\Db;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信好友模型类
|
||||||
|
*/
|
||||||
|
class WechatFriend extends Model
|
||||||
|
{
|
||||||
|
// 设置表名
|
||||||
|
protected $name = 'wechat_friend';
|
||||||
|
protected $prefix = 'tk_';
|
||||||
|
|
||||||
|
// 设置主键
|
||||||
|
protected $pk = 'id';
|
||||||
|
|
||||||
|
// 自动写入时间戳
|
||||||
|
protected $autoWriteTimestamp = 'datetime';
|
||||||
|
|
||||||
|
// 定义时间戳字段名
|
||||||
|
protected $createTime = 'createTime';
|
||||||
|
protected $updateTime = 'updateTime';
|
||||||
|
|
||||||
|
// 定义字段类型
|
||||||
|
protected $type = [
|
||||||
|
'id' => 'integer',
|
||||||
|
'wechatAccountId' => 'integer',
|
||||||
|
'gender' => 'integer',
|
||||||
|
'addFrom' => 'integer',
|
||||||
|
'isDeleted' => 'integer',
|
||||||
|
'isPassed' => 'integer',
|
||||||
|
'accountId' => 'integer',
|
||||||
|
'groupId' => 'integer',
|
||||||
|
'labels' => 'json',
|
||||||
|
'deleteTime' => 'datetime',
|
||||||
|
'passTime' => 'datetime',
|
||||||
|
'createTime' => 'datetime'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据微信账号ID获取好友列表
|
||||||
|
*
|
||||||
|
* @param string $ownerWechatId 所有者微信ID
|
||||||
|
* @param array $params 查询条件参数
|
||||||
|
* @param int $page 页码
|
||||||
|
* @param int $limit 每页数量
|
||||||
|
* @return array 好友列表和总数
|
||||||
|
*/
|
||||||
|
public static function getFriendsByWechatId($ownerWechatId, $params = [], $page = 1, $limit = 20)
|
||||||
|
{
|
||||||
|
// 构建基础查询
|
||||||
|
$query = self::where('ownerWechatId', $ownerWechatId)
|
||||||
|
->where('isDeleted', 0);
|
||||||
|
|
||||||
|
// 添加筛选条件(昵称、备注、微信号、标签)
|
||||||
|
if (!empty($params['keyword'])) {
|
||||||
|
$keyword = $params['keyword'];
|
||||||
|
$query->where(function($q) use ($keyword) {
|
||||||
|
$q->whereOr('nickname', 'like', "%{$keyword}%")
|
||||||
|
->whereOr('conRemark', 'like', "%{$keyword}%")
|
||||||
|
->whereOr('alias', 'like', "%{$keyword}%")
|
||||||
|
->whereOr("JSON_SEARCH(labels, 'one', '%{$keyword}%') IS NOT NULL");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算总数
|
||||||
|
$total = $query->count();
|
||||||
|
|
||||||
|
// 分页查询数据
|
||||||
|
$friends = $query->page($page, $limit)
|
||||||
|
->order('createTime desc')
|
||||||
|
->field('wechatId, alias, avatar, labels, accountNickname, accountRealName, nickname, conRemark, gender, region')
|
||||||
|
->select();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'list' => $friends,
|
||||||
|
'total' => $total,
|
||||||
|
'page' => $page,
|
||||||
|
'limit' => $limit
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user