Files
ckb-SuperAdmin/nkebao/src/pages/wechat-accounts/WechatAccounts.tsx
笔记本里的永平 6543da9167 feat: 本次提交更新内容如下
存一版样式问题
2025-07-07 16:34:06 +08:00

383 lines
15 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 { useNavigate } from 'react-router-dom';
import { Search, RefreshCw, Loader2 } from 'lucide-react';
import { fetchWechatAccountList, transformWechatAccount } from '@/api/wechat-accounts';
import { useToast } from '@/components/ui/toast';
import { useWechatAccount } from '@/contexts/WechatAccountContext';
import PageHeader from '@/components/PageHeader';
import Layout from '@/components/Layout';
import '@/components/Layout.css';
interface WechatAccount {
id: string;
wechatId: string;
wechatAccount: string;
nickname: string;
avatar: string;
remainingAdds: number;
todayAdded: number;
status: "normal" | "abnormal";
friendCount: number;
deviceName: string;
lastActive: string;
maxDailyAdds: number;
deviceId: string;
}
export default function WechatAccounts() {
const navigate = useNavigate();
const { toast } = useToast();
const { setCurrentAccount } = useWechatAccount();
const [accounts, setAccounts] = useState<WechatAccount[]>([]);
const [searchQuery, setSearchQuery] = useState("");
const [currentPage, setCurrentPage] = useState(1);
const [isTransferDialogOpen, setIsTransferDialogOpen] = useState(false);
const [selectedAccount, setSelectedAccount] = useState<WechatAccount | null>(null);
const [totalAccounts, setTotalAccounts] = useState(0);
const [isLoading, setIsLoading] = useState(true);
const [isRefreshing, setIsRefreshing] = useState(false);
const accountsPerPage = 10;
const mounted = useRef(false);
// 获取微信账号列表
const fetchAccounts = useCallback(async (page: number = 1, keyword: string = "") => {
try {
setIsLoading(true);
const response = await fetchWechatAccountList({
page,
limit: accountsPerPage,
keyword,
sort: 'id',
order: 'desc'
});
if (response && response.code === 200 && response.data) {
// 转换数据格式
const wechatAccounts = response.data.list.map((item: any) => transformWechatAccount(item));
setAccounts(wechatAccounts);
setTotalAccounts(response.data.total);
} else {
toast({
title: "获取微信账号失败",
description: response?.message || "请稍后再试",
variant: "destructive"
});
setAccounts([]);
setTotalAccounts(0);
}
} catch (error) {
console.error("获取微信账号列表失败:", error);
toast({
title: "获取微信账号失败",
description: "请检查网络连接或稍后再试",
variant: "destructive"
});
setAccounts([]);
setTotalAccounts(0);
} finally {
setIsLoading(false);
}
}, [accountsPerPage, toast]);
// 初始化数据加载
useEffect(() => {
if (!mounted.current) {
mounted.current = true;
fetchAccounts(currentPage, searchQuery);
}
}, []);
// 处理页码和搜索变化
useEffect(() => {
if (mounted.current) {
fetchAccounts(currentPage, searchQuery);
}
}, [currentPage, searchQuery, fetchAccounts]);
// 搜索处理
const handleSearch = () => {
setCurrentPage(1);
};
// 刷新处理
const handleRefresh = async () => {
try {
setIsRefreshing(true);
// 重新获取微信账号列表数据
await fetchAccounts(currentPage, searchQuery);
toast({
title: "刷新成功",
description: "微信账号列表已更新"
});
} catch (error) {
console.error("刷新微信账号状态失败:", error);
toast({
title: "刷新失败",
description: "请检查网络连接或稍后再试",
variant: "destructive"
});
} finally {
setIsRefreshing(false);
}
};
const totalPages = Math.ceil(totalAccounts / accountsPerPage);
const handleTransferFriends = (account: WechatAccount) => {
setSelectedAccount(account);
setIsTransferDialogOpen(true);
};
const handleConfirmTransfer = async () => {
if (!selectedAccount) return;
try {
// 实际实现好友转移功能,这里需要另一个账号作为目标
// 现在只是模拟效果
toast({
title: "好友转移计划已创建",
description: "请在场景获客中查看详情",
});
setIsTransferDialogOpen(false);
navigate("/scenarios");
} catch (error) {
console.error("好友转移失败:", error);
toast({
title: "好友转移失败",
description: "请稍后再试",
variant: "destructive"
});
}
};
return (
<Layout
header={
<PageHeader
title="微信号"
defaultBackPath="/"
/>
}
>
<div className="bg-gray-50">
<div className="p-4">
{/* 搜索和操作栏 */}
<div className="bg-white p-4 rounded-xl shadow-sm border border-gray-100 mb-4 ">
<div className="flex items-center space-x-3">
<div className="relative flex-1">
<Search className="w-4 h-4 absolute left-3 top-3 text-gray-400" />
<input
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"
placeholder="搜索微信号/昵称"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
/>
</div>
<button
className="p-2 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors"
onClick={handleRefresh}
disabled={isRefreshing}
>
{isRefreshing ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<RefreshCw className="h-4 w-4" />
)}
</button>
</div>
</div>
{/* 账号列表 */}
{isLoading ? (
<div className="flex justify-center items-center py-20">
<Loader2 className="h-8 w-8 animate-spin text-blue-500" />
</div>
) : accounts.length === 0 ? (
<div className="text-center py-20 text-gray-500">
<p></p>
<button
className="mt-4 px-4 py-2 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors"
onClick={handleRefresh}
disabled={isRefreshing}
>
{isRefreshing ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : (
<RefreshCw className="h-4 w-4 mr-2" />
)}
</button>
</div>
) : (
<div className="space-y-3">
{accounts.map((account) => (
<div
key={account.id}
className="bg-white p-4 rounded-xl shadow-sm border border-gray-100 hover:shadow-lg transition-all cursor-pointer"
onClick={() => {
// 使用Context存储数据而不是URL参数
setCurrentAccount({
id: account.id,
avatar: account.avatar,
nickname: account.nickname,
status: account.status,
wechatId: account.wechatId,
wechatAccount: account.wechatAccount,
deviceName: account.deviceName,
deviceId: account.deviceId,
});
navigate(`/wechat-accounts/${account.id}`);
}}
>
<div className="flex items-start space-x-4">
<div className="relative">
<img
src={account.avatar || "/placeholder.svg"}
alt={account.nickname}
className="w-12 h-12 rounded-full ring-2 ring-offset-2 ring-blue-500/20"
/>
<div className={`absolute -bottom-1 -right-1 w-4 h-4 rounded-full border-2 border-white ${
account.status === "normal" ? "bg-green-500" : "bg-red-500"
}`}></div>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<h3 className="font-medium truncate max-w-[180px]">{account.nickname}</h3>
<span className={`px-2 py-1 text-xs rounded-full ${
account.status === "normal"
? "bg-green-100 text-green-700"
: "bg-red-100 text-red-700"
}`}>
{account.status === "normal" ? "正常" : "异常"}
</span>
</div>
<button
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
onClick={(e) => {
e.stopPropagation();
handleTransferFriends(account);
}}
>
{/* ArrowRightLeft className="h-4 w-4" */}
</button>
</div>
<div className="mt-2 text-sm text-gray-500 space-y-1">
<div className="truncate">{account.wechatAccount}</div>
<div className="flex items-center justify-between flex-wrap gap-1">
<div>{account.friendCount}</div>
<div className="text-green-600">+{account.todayAdded}</div>
</div>
<div className="space-y-1">
<div className="flex items-center justify-between text-sm">
<div className="flex items-center space-x-1">
<span></span>
<span className="font-medium">{account.remainingAdds}</span>
<div className="relative group">
{/* AlertCircle className="h-4 w-4 text-gray-400" */}
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 text-xs bg-gray-800 text-white rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap">
{account.maxDailyAdds}
</div>
</div>
</div>
<span className="text-sm text-gray-500">
{account.todayAdded}/{account.maxDailyAdds}
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all"
style={{ width: `${(account.todayAdded / account.maxDailyAdds) * 100}%` }}
></div>
</div>
</div>
<div className="flex items-center justify-between text-xs text-gray-500 pt-2 flex-wrap gap-1">
<div className="truncate max-w-[150px]">{account.deviceName || '未知设备'}</div>
<div className="whitespace-nowrap">{account.lastActive}</div>
</div>
</div>
</div>
</div>
</div>
))}
</div>
)}
{/* 分页 */}
{!isLoading && accounts.length > 0 && totalPages > 1 && (
<div className="mt-6 flex justify-center">
<div className="flex items-center space-x-2">
<button
className="px-3 py-2 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
disabled={currentPage === 1}
>
</button>
<div className="flex items-center space-x-1">
{Array.from({ length: Math.min(totalPages, 5) }, (_, i) => {
let pageToShow = i + 1;
if (currentPage > 3 && totalPages > 5) {
pageToShow = Math.min(currentPage - 2 + i, totalPages);
if (pageToShow > totalPages - 4) {
pageToShow = totalPages - 4 + i;
}
}
return (
<button
key={pageToShow}
className={`px-3 py-2 rounded-lg transition-colors ${
currentPage === pageToShow
? "bg-blue-600 text-white"
: "border border-gray-200 hover:bg-gray-50"
}`}
onClick={() => setCurrentPage(pageToShow)}
>
{pageToShow}
</button>
);
})}
</div>
<button
className="px-3 py-2 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
disabled={currentPage === totalPages}
>
</button>
</div>
</div>
)}
</div>
</div>
{/* 好友转移确认对话框 */}
{isTransferDialogOpen && (
<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-4"></h3>
<p className="text-sm text-gray-500 mb-6">
{selectedAccount?.nickname} {selectedAccount?.friendCount}{" "}
</p>
<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={() => setIsTransferDialogOpen(false)}
>
</button>
<button
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
onClick={handleConfirmTransfer}
>
</button>
</div>
</div>
</div>
)}
</Layout>
);
}