私域操盘手 - 账号详情/账号概览 对接

This commit is contained in:
柳清爽
2025-05-12 11:58:13 +08:00
parent 5ee85f7d7f
commit bd9f070195
4 changed files with 261 additions and 313 deletions

View File

@@ -2,9 +2,40 @@ import { api } from "@/lib/api";
import {
ServerWechatAccountsResponse,
QueryWechatAccountParams,
WechatAccountDetailResponse
} from "@/types/wechat-account";
// 添加接口返回数据类型定义
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 WechatAccountSummaryResponse {
code: number;
msg: string;
data: WechatAccountSummary;
}
/**
* 获取微信账号列表
* @param params 查询参数
@@ -24,15 +55,6 @@ export const fetchWechatAccountList = async (params: QueryWechatAccountParams =
return api.get<ServerWechatAccountsResponse>(`/v1/device/wechats?${queryParams.toString()}`);
};
/**
* 获取微信账号详情
* @param id 微信账号ID
* @returns 微信账号详情响应
*/
export const fetchWechatAccountDetail = async (id: string | number): Promise<WechatAccountDetailResponse> => {
return api.get<WechatAccountDetailResponse>(`/v1/device/wechats/${id}`);
};
/**
* 刷新微信账号状态
* @returns 刷新结果
@@ -115,173 +137,35 @@ export const transformWechatAccount = (serverAccount: any): import("@/types/wech
};
};
/**
* 将服务端的微信账号详情转换为前端详情页面所需的格式
* @param detailResponse 服务端微信账号详情响应
* @returns 前端页面所需的微信账号详情格式
*/
export const transformWechatAccountDetail = (detailResponse: WechatAccountDetailResponse): any => {
if (!detailResponse || !detailResponse.data) {
return null;
}
const { basicInfo, statistics, accountInfo, restrictions, friends } = detailResponse.data;
// 设备信息处理 - 改进处理方式
let deviceId = '';
let deviceName = '';
if (basicInfo.deviceInfo) {
// 尝试解析设备信息字符串
const deviceInfoParts = basicInfo.deviceInfo.split(' ');
if (deviceInfoParts.length > 0) {
// 提取数字部分作为设备ID确保是整数
const possibleId = deviceInfoParts[0].trim();
// 验证是否为数字
deviceId = /^\d+$/.test(possibleId) ? possibleId : '';
// 提取设备名称
if (deviceInfoParts.length > 1) {
deviceName = deviceInfoParts[1].replace(/[()]/g, '').trim();
}
}
}
// 如果从deviceInfo无法获取有效的设备ID直接使用微信账号ID作为备选
if (!deviceId && basicInfo.id) {
deviceId = basicInfo.id.toString();
}
// 如果没有设备名称,使用备用名称
if (!deviceName) {
deviceName = '未命名设备';
}
// 账号年龄计算
let accountAgeYears = 0;
let accountAgeMonths = 0;
if (accountInfo.createTime) {
const createDate = new Date(accountInfo.createTime);
const currentDate = new Date();
const diffInMonths = (currentDate.getFullYear() - createDate.getFullYear()) * 12 +
(currentDate.getMonth() - createDate.getMonth());
accountAgeYears = Math.floor(diffInMonths / 12);
accountAgeMonths = diffInMonths % 12;
}
// 转换限制记录
const restrictionRecords = restrictions?.map((restriction, index) => ({
id: `${index}`,
date: restriction.startTime,
reason: restriction.reason,
recoveryTime: restriction.endTime,
type: mapRestrictionType(restriction.type)
})) || [];
// 转换好友数据
const transformedFriends = friends?.map(friend => ({
id: friend.id.toString(),
avatar: friend.avatar || `/placeholder.svg?height=40&width=40&text=${friend.nickname?.[0] || ''}`,
nickname: friend.nickname,
wechatId: friend.wechatId,
remark: '', // 服务端未提供
addTime: friend.createTime,
lastInteraction: '', // 服务端未提供
tags: [], // 服务端未提供
region: friend.region || '',
source: '', // 服务端未提供
notes: '', // 服务端未提供
})) || [];
// 创建每周统计数据(模拟数据,服务端未提供)
const weeklyStats = Array.from({ length: 7 }, (_, i) => ({
date: `Day ${i + 1}`,
friends: Math.floor(Math.random() * 50) + 50,
messages: Math.floor(Math.random() * 100) + 100,
}));
return {
id: basicInfo.id.toString(),
avatar: basicInfo.avatar || '',
nickname: basicInfo.nickname || '',
wechatId: basicInfo.wechatId || '',
deviceId,
deviceName,
friendCount: statistics.totalFriend || 0,
todayAdded: 0, // 服务端未提供默认为0
status: basicInfo.status === '在线' ? 'normal' : 'abnormal',
lastActive: accountInfo.lastUpdateTime || new Date().toLocaleString(),
messageCount: statistics.thirtyDayMsgCount || 0,
activeRate: 0, // 服务端未提供默认为0
accountAge: {
years: accountAgeYears,
months: accountAgeMonths,
},
totalChats: statistics.sevenDayMsgCount + statistics.yesterdayMsgCount || 0,
chatFrequency: Math.floor((statistics.sevenDayMsgCount || 0) / 7), // 每日平均聊天次数
restrictionRecords,
isVerified: true, // 服务端未提供默认为true
firstMomentDate: accountInfo.createTime || '',
accountWeight: accountInfo.weight || 50,
weightFactors: {
restrictionFactor: restrictionRecords.length > 0 ? 0.8 : 1.0,
verificationFactor: 1.0,
ageFactor: Math.min(1.0, accountAgeYears * 0.1 + 0.5),
activityFactor: statistics.totalFriend > 0 ? 0.9 : 0.7,
},
weeklyStats,
friends: transformedFriends,
};
};
/**
* 将服务端的限制类型映射为前端类型
* @param type 服务端限制类型
* @returns 前端限制类型
*/
const mapRestrictionType = (type: string): "friend_limit" | "marketing" | "spam" | "other" => {
const typeMap: Record<string, "friend_limit" | "marketing" | "spam" | "other"> = {
'friend': 'friend_limit',
'marketing': 'marketing',
'spam': 'spam'
};
return typeMap[type] || 'other';
};
/**
* 获取微信好友列表
* @param wechatId 微信账号ID
* @param page 页码
* @param limit 每页数量
* @param keyword 搜索关键词
* @param pageSize 每页数量
* @param searchQuery 搜索关键词
* @returns 好友列表数据
*/
export const fetchWechatFriends = async (
wechatId: string | number,
page: number = 1,
limit: number = 20,
keyword: string = ""
): Promise<{ code: number; msg: string; data: any }> => {
export const fetchWechatFriends = async (wechatId: string, page: number = 1, pageSize: number = 20, searchQuery: string = '') => {
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;
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/v1/device/wechats/${wechatId}/friends?page=${page}&pageSize=${pageSize}&search=${searchQuery}`);
const data = await response.json();
return data;
} catch (error) {
console.error('获取微信好友列表失败:', error);
console.error("获取好友列表失败:", error);
throw error;
}
}
};
/**
* 获取微信账号概览信息
* @param id 微信账号ID
* @returns 微信账号概览信息
*/
export const fetchWechatAccountSummary = async (id: string): Promise<WechatAccountSummaryResponse> => {
try {
return api.get<WechatAccountSummaryResponse>(`/v1/device/wechats/${id}/summary`);
} catch (error) {
console.error("获取账号概览失败:", error);
throw error;
}
};

View File

@@ -44,7 +44,7 @@ import {
PaginationPrevious,
} from "@/components/ui/pagination"
import { toast } from "@/components/ui/use-toast"
import { fetchWechatAccountDetail, transformWechatAccountDetail, fetchWechatFriends } from "@/api/wechat-accounts"
import { fetchWechatFriends, fetchWechatAccountSummary } from "@/api/wechat-accounts"
interface RestrictionRecord {
id: string
@@ -87,13 +87,12 @@ interface WechatAccountDetail {
lastActive: string
messageCount: number
activeRate: number
// 新增和修改的字段
accountAge: {
years: number
months: number
}
totalChats: number
chatFrequency: number // 每日平均聊天次数
chatFrequency: number
restrictionRecords: RestrictionRecord[]
isVerified: boolean
firstMomentDate: string
@@ -112,9 +111,35 @@ interface WechatAccountDetail {
friends: WechatFriend[]
}
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;
}[];
}
export default function WechatAccountDetailPage({ params }: { params: { id: string } }) {
const router = useRouter()
const [account, setAccount] = useState<WechatAccountDetail | null>(null)
const [accountSummary, setAccountSummary] = useState<WechatAccountSummary | null>(null)
const [showRestrictions, setShowRestrictions] = useState(false)
const [showTransferConfirm, setShowTransferConfirm] = useState(false)
const [showFriendDetail, setShowFriendDetail] = useState(false)
@@ -151,9 +176,28 @@ export default function WechatAccountDetailPage({ params }: { params: { id: stri
try {
const decodedData = JSON.parse(decodeURIComponent(dataParam));
setInitialData(decodedData);
// 使用初始数据设置account
const mockData = generateMockAccountData();
if (decodedData) {
mockData.avatar = decodedData.avatar;
mockData.nickname = decodedData.nickname;
mockData.status = decodedData.status;
mockData.wechatId = decodedData.wechatId;
mockData.deviceName = decodedData.deviceName;
}
setAccount(mockData);
setFriendsTotal(mockData.friendCount);
setIsLoading(false);
} catch (error) {
console.error('解析初始数据失败:', error);
setIsLoading(false);
}
} else {
// 如果没有初始数据,使用模拟数据
const mockData = generateMockAccountData();
setAccount(mockData);
setFriendsTotal(mockData.friendCount);
setIsLoading(false);
}
}, []);
@@ -422,79 +466,73 @@ export default function WechatAccountDetailPage({ params }: { params: { id: stri
};
}, [friendsLoadingRef.current, friendsObserver.current]);
useEffect(() => {
// 模拟API调用获取账号详情
const fetchAccount = async () => {
try {
setIsLoading(true)
// 调用API获取微信账号详情
const response = await fetchWechatAccountDetail(params.id)
if (response && response.code === 200) {
// 转换数据格式
const transformedAccount = transformWechatAccountDetail(response)
// 使用初始数据覆盖API返回的部分字段
if (initialData) {
transformedAccount.avatar = initialData.avatar;
transformedAccount.nickname = initialData.nickname;
transformedAccount.status = initialData.status;
transformedAccount.wechatId = initialData.wechatId;
transformedAccount.deviceName = initialData.deviceName;
}
setAccount(transformedAccount)
// 如果有好友总数更新friendsTotal状态
if (transformedAccount && transformedAccount.friendCount > 0) {
setFriendsTotal(transformedAccount.friendCount);
}
} else {
toast({
title: "获取微信账号详情失败",
description: response?.msg || "请稍后再试",
variant: "destructive"
})
// 获取失败时使用模拟数据
const mockData = generateMockAccountData();
// 使用初始数据覆盖模拟数据的部分字段
if (initialData) {
mockData.avatar = initialData.avatar;
mockData.nickname = initialData.nickname;
mockData.status = initialData.status;
mockData.wechatId = initialData.wechatId;
mockData.deviceName = initialData.deviceName;
}
setAccount(mockData);
// 更新好友总数
setFriendsTotal(mockData.friendCount);
}
} catch (error) {
console.error("获取微信账号详情失败:", error)
toast({
title: "获取微信账号详情失败",
description: "请检查网络连接或稍后再试",
variant: "destructive"
})
// 请求出错时使用模拟数据
const mockData = generateMockAccountData();
// 使用初始数据覆盖模拟数据的部分字段
if (initialData) {
mockData.avatar = initialData.avatar;
mockData.nickname = initialData.nickname;
mockData.status = initialData.status;
mockData.wechatId = initialData.wechatId;
mockData.deviceName = initialData.deviceName;
}
setAccount(mockData);
// 更新好友总数
setFriendsTotal(mockData.friendCount);
} finally {
setIsLoading(false)
}
// 计算账号年龄
const calculateAccountAge = (registerTime: string) => {
const register = new Date(registerTime);
const now = new Date();
const years = now.getFullYear() - register.getFullYear();
const months = now.getMonth() - register.getMonth();
if (months < 0) {
return {
years: years - 1,
months: months + 12
};
}
return {
years,
months
};
};
fetchAccount()
}, [params.id, initialData])
// 获取账号概览数据
const fetchSummaryData = useCallback(async () => {
try {
setIsLoading(true);
const response = await fetchWechatAccountSummary(params.id);
if (response.code === 200) {
setAccountSummary(response.data);
} else {
toast({
title: "获取账号概览失败",
description: response.msg || "请稍后再试",
variant: "destructive"
});
}
} catch (error) {
console.error("获取账号概览失败:", error);
toast({
title: "获取账号概览失败",
description: "请检查网络连接或稍后再试",
variant: "destructive"
});
} finally {
setIsLoading(false);
}
}, [params.id]);
// 在页面加载和切换到概览标签时获取数据
useEffect(() => {
if (activeTab === "overview") {
fetchSummaryData();
}
}, [activeTab, fetchSummaryData]);
// 在初始加载时获取数据
useEffect(() => {
if (activeTab === "overview") {
fetchSummaryData();
}
}, [fetchSummaryData, activeTab]);
// 处理标签切换
const handleTabChange = (value: string) => {
setActiveTab(value);
if (value === "overview") {
fetchSummaryData();
}
};
if (!account) {
return <div>...</div>
@@ -618,7 +656,7 @@ export default function WechatAccountDetailPage({ params }: { params: { id: stri
</div>
</Card>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<Tabs value={activeTab} onValueChange={handleTabChange} className="w-full">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="overview"></TabsTrigger>
<TabsTrigger value="friends">
@@ -634,8 +672,16 @@ export default function WechatAccountDetailPage({ params }: { params: { id: stri
<Clock className="w-4 h-4" />
<span className="text-sm"></span>
</div>
<div className="text-2xl font-bold text-blue-600">{formatAccountAge(account.accountAge)}</div>
<div className="text-sm text-gray-500 mt-1">{account.firstMomentDate}</div>
{accountSummary && (
<>
<div className="text-2xl font-bold text-blue-600">
{formatAccountAge(calculateAccountAge(accountSummary.accountAge))}
</div>
<div className="text-sm text-gray-500 mt-1">
{new Date(accountSummary.accountAge).toLocaleDateString()}
</div>
</>
)}
</Card>
<Card className="p-4">
@@ -643,8 +689,12 @@ export default function WechatAccountDetailPage({ params }: { params: { id: stri
<MessageSquare className="w-4 h-4" />
<span className="text-sm"></span>
</div>
<div className="text-2xl font-bold text-blue-600">{account.chatFrequency}/</div>
<div className="text-sm text-gray-500 mt-1">{account.totalChats.toLocaleString()}</div>
{accountSummary && (
<>
<div className="text-2xl font-bold text-blue-600">{accountSummary.activityLevel.dayTimes}/</div>
<div className="text-sm text-gray-500 mt-1">{accountSummary.activityLevel.allTimes.toLocaleString()}</div>
</>
)}
</Card>
</div>
@@ -655,34 +705,40 @@ export default function WechatAccountDetailPage({ params }: { params: { id: stri
<Star className="w-4 h-4 text-yellow-500" />
<span className="font-medium"></span>
</div>
<div className={`flex items-center space-x-2 ${getWeightColor(account.accountWeight)}`}>
<span className="text-2xl font-bold">{account.accountWeight}</span>
<span className="text-sm"></span>
</div>
</div>
<p className="text-sm text-gray-500 mb-4">{getWeightDescription(account.accountWeight)}</p>
<div className="space-y-3">
<div className="flex items-center justify-between text-sm">
<span className="flex-shrink-0"></span>
<Progress value={account.weightFactors.ageFactor * 100} className="flex-1 min-w-0 mx-2" />
<span className="flex-shrink-0">{(account.weightFactors.ageFactor * 100).toFixed(0)}%</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="flex-shrink-0"></span>
<Progress value={account.weightFactors.activityFactor * 100} className="flex-1 min-w-0 mx-2" />
<span className="flex-shrink-0">{(account.weightFactors.activityFactor * 100).toFixed(0)}%</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="flex-shrink-0"></span>
<Progress value={account.weightFactors.restrictionFactor * 100} className="flex-1 min-w-0 mx-2" />
<span className="flex-shrink-0">{(account.weightFactors.restrictionFactor * 100).toFixed(0)}%</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="flex-shrink-0"></span>
<Progress value={account.weightFactors.verificationFactor * 100} className="flex-1 min-w-0 mx-2" />
<span className="flex-shrink-0">{(account.weightFactors.verificationFactor * 100).toFixed(0)}%</span>
</div>
{accountSummary && (
<div className={`flex items-center space-x-2 ${getWeightColor(accountSummary.accountWeight.scope)}`}>
<span className="text-2xl font-bold">{accountSummary.accountWeight.scope}</span>
<span className="text-sm"></span>
</div>
)}
</div>
{accountSummary && (
<>
<p className="text-sm text-gray-500 mb-4">{getWeightDescription(accountSummary.accountWeight.scope)}</p>
<div className="space-y-3">
<div className="flex items-center justify-between text-sm">
<span className="flex-shrink-0"></span>
<Progress value={accountSummary.accountWeight.ageWeight} className="flex-1 min-w-0 mx-2" />
<span className="flex-shrink-0">{accountSummary.accountWeight.ageWeight}%</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="flex-shrink-0"></span>
<Progress value={accountSummary.accountWeight.activityWeigth} className="flex-1 min-w-0 mx-2" />
<span className="flex-shrink-0">{accountSummary.accountWeight.activityWeigth}%</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="flex-shrink-0"></span>
<Progress value={accountSummary.accountWeight.restrictWeight} className="flex-1 min-w-0 mx-2" />
<span className="flex-shrink-0">{accountSummary.accountWeight.restrictWeight}%</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="flex-shrink-0"></span>
<Progress value={accountSummary.accountWeight.realNameWeight} className="flex-1 min-w-0 mx-2" />
<span className="flex-shrink-0">{accountSummary.accountWeight.realNameWeight}%</span>
</div>
</div>
</>
)}
</Card>
{/* 添加好友统计 */}
@@ -701,29 +757,31 @@ export default function WechatAccountDetailPage({ params }: { params: { id: stri
</TooltipContent>
</UITooltip>
</div>
<div className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-sm text-gray-500"></span>
<span className="text-xl font-bold text-blue-600">{account.todayAdded}</span>
</div>
<div>
<div className="flex justify-between text-sm mb-2">
<span className="text-gray-500"></span>
<span>
{account.todayAdded}/{calculateMaxDailyAdds(account.accountWeight)}
</span>
{accountSummary && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-sm text-gray-500"></span>
<span className="text-xl font-bold text-blue-600">{accountSummary.statistics.todayAdded}</span>
</div>
<div>
<div className="flex justify-between text-sm mb-2">
<span className="text-gray-500"></span>
<span>
{accountSummary.statistics.todayAdded}/{accountSummary.statistics.addLimit}
</span>
</div>
<Progress
value={(accountSummary.statistics.todayAdded / accountSummary.statistics.addLimit) * 100}
className="h-2"
/>
</div>
<div className="text-sm text-gray-500">
({accountSummary.accountWeight.scope}){" "}
<span className="font-medium text-blue-600">{accountSummary.statistics.addLimit}</span>{" "}
</div>
<Progress
value={(account.todayAdded / calculateMaxDailyAdds(account.accountWeight)) * 100}
className="h-2"
/>
</div>
<div className="text-sm text-gray-500">
({account.accountWeight}){" "}
<span className="font-medium text-blue-600">{calculateMaxDailyAdds(account.accountWeight)}</span>{" "}
</div>
</div>
)}
</Card>
{/* 限制记录 */}
@@ -733,20 +791,26 @@ export default function WechatAccountDetailPage({ params }: { params: { id: stri
<Shield className="w-4 h-4 text-red-500" />
<span className="font-medium"></span>
</div>
<Badge variant="outline" className="cursor-pointer" onClick={() => setShowRestrictions(true)}>
{account.restrictionRecords.length}
</Badge>
{accountSummary && (
<Badge variant="outline" className="cursor-pointer" onClick={() => setShowRestrictions(true)}>
{accountSummary.restrictions.length}
</Badge>
)}
</div>
<div className="space-y-2">
{account.restrictionRecords.slice(0, 2).map((record) => (
<div key={record.id} className="text-sm">
<div className="flex items-center justify-between">
<span className={getRestrictionTypeColor(record.type)}>{record.reason}</span>
<span className="text-gray-500">{record.date}</span>
{accountSummary && (
<div className="space-y-2">
{accountSummary.restrictions.slice(0, 2).map((record) => (
<div key={record.id} className="text-sm">
<div className="flex items-center justify-between">
<span className={`text-${record.level === "2" ? "yellow" : "red"}-600`}>
{record.reason}
</span>
<span className="text-gray-500">{new Date(record.date).toLocaleDateString()}</span>
</div>
</div>
</div>
))}
</div>
))}
</div>
)}
</Card>
</TabsContent>