Files
cunkebao_v3/Cunkebao/app/wechat-accounts/page.tsx
2025-04-09 09:31:09 +08:00

375 lines
14 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.

"use client"
import { useState, useEffect } from "react"
import { ChevronLeft, Filter, Search, RefreshCw, ArrowRightLeft, AlertCircle, Loader2 } from "lucide-react"
import { Card } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Badge } from "@/components/ui/badge"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { useRouter } from "next/navigation"
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"
import { toast } from "@/components/ui/use-toast"
import { Progress } from "@/components/ui/progress"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
import { fetchWechatAccountList, refreshWechatAccounts, transferWechatFriends, transformWechatAccount } from "@/api/wechat-accounts"
import { WechatAccount } from "@/types/wechat-account"
export default function WechatAccountsPage() {
const router = useRouter()
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 fetchAccounts = 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(transformWechatAccount);
setAccounts(wechatAccounts);
setTotalAccounts(response.data.total);
} else {
toast({
title: "获取微信账号失败",
description: response?.msg || "请稍后再试",
variant: "destructive"
});
// 如果API请求失败设置空数组
setAccounts([]);
setTotalAccounts(0);
}
} catch (error) {
console.error("获取微信账号列表失败:", error);
toast({
title: "获取微信账号失败",
description: "请检查网络连接或稍后再试",
variant: "destructive"
});
setAccounts([]);
setTotalAccounts(0);
} finally {
setIsLoading(false);
}
};
// 刷新微信账号状态
const handleRefresh = async () => {
try {
setIsRefreshing(true);
const response = await refreshWechatAccounts();
if (response && response.code === 200) {
toast({
title: "刷新成功",
description: "微信账号状态已更新"
});
// 重新获取数据
await fetchAccounts(currentPage, searchQuery);
} else {
toast({
title: "刷新失败",
description: response?.msg || "请稍后再试",
variant: "destructive"
});
}
} catch (error) {
console.error("刷新微信账号状态失败:", error);
toast({
title: "刷新失败",
description: "请检查网络连接或稍后再试",
variant: "destructive"
});
} finally {
setIsRefreshing(false);
}
};
// 初始加载和页码变化时获取数据
useEffect(() => {
fetchAccounts(currentPage, searchQuery);
}, [currentPage]);
// 搜索时重置页码并获取数据
const handleSearch = () => {
setCurrentPage(1);
fetchAccounts(1, searchQuery);
};
// 处理搜索框回车事件
const handleSearchKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
handleSearch();
}
};
const filteredAccounts = accounts;
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)
router.push("/scenarios")
} catch (error) {
console.error("好友转移失败:", error);
toast({
title: "好友转移失败",
description: "请稍后再试",
variant: "destructive"
});
}
}
return (
<div className="flex-1 bg-gradient-to-b from-blue-50 to-white min-h-screen">
<header className="sticky top-0 z-10 bg-white/80 backdrop-blur-sm border-b">
<div className="flex items-center p-4">
<Button variant="ghost" size="icon" onClick={() => router.back()}>
<ChevronLeft className="h-5 w-5" />
</Button>
<h1 className="ml-2 text-lg font-medium"></h1>
</div>
</header>
<div className="p-4">
<Card className="p-4 mb-4">
<div className="flex items-center space-x-4">
<div className="relative flex-1">
<Search className="w-4 h-4 absolute left-3 top-3 text-gray-400" />
<Input
className="pl-9"
placeholder="搜索微信号/昵称"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={handleSearchKeyDown}
/>
</div>
<Button variant="outline" size="icon">
<Filter className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="icon"
onClick={handleRefresh}
disabled={isRefreshing}
>
{isRefreshing ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<RefreshCw className="h-4 w-4" />
)}
</Button>
</div>
</Card>
{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
variant="outline"
size="sm"
className="mt-4"
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="grid gap-3">
{accounts.map((account) => (
<Card
key={account.id}
className="p-4 hover:shadow-lg transition-all cursor-pointer overflow-hidden"
onClick={() => router.push(`/wechat-accounts/${account.id}`)}
>
<div className="flex items-start space-x-4">
<Avatar className="h-12 w-12 ring-2 ring-offset-2 ring-blue-500/20">
<AvatarImage src={account.avatar} />
<AvatarFallback>{account.nickname[0]}</AvatarFallback>
</Avatar>
<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>
<Badge variant={account.status === "normal" ? "outline" : "destructive"}>
{account.status === "normal" ? "正常" : "异常"}
</Badge>
</div>
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation()
handleTransferFriends(account)
}}
>
<ArrowRightLeft className="h-4 w-4 mr-2" />
</Button>
</div>
<div className="mt-1 text-sm text-gray-500 space-y-1">
<div className="truncate">{account.wechatId}</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>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<AlertCircle className="h-4 w-4 text-gray-400" />
</TooltipTrigger>
<TooltipContent>
<p> {account.maxDailyAdds} </p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<span className="text-sm text-gray-500">
{account.todayAdded}/{account.maxDailyAdds}
</span>
</div>
<Progress value={(account.todayAdded / account.maxDailyAdds) * 100} className="h-2" />
</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>
</Card>
))}
</div>
)}
{!isLoading && accounts.length > 0 && totalPages > 1 && (
<div className="mt-4 flex justify-center">
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
href="#"
onClick={(e) => {
e.preventDefault()
if (currentPage > 1) {
setCurrentPage((prev) => prev - 1)
}
}}
/>
</PaginationItem>
{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 (
<PaginationItem key={pageToShow}>
<PaginationLink
href="#"
isActive={currentPage === pageToShow}
onClick={(e) => {
e.preventDefault()
setCurrentPage(pageToShow)
}}
>
{pageToShow}
</PaginationLink>
</PaginationItem>
);
})}
<PaginationItem>
<PaginationNext
href="#"
onClick={(e) => {
e.preventDefault()
if (currentPage < totalPages) {
setCurrentPage((prev) => prev + 1)
}
}}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
)}
</div>
<Dialog open={isTransferDialogOpen} onOpenChange={setIsTransferDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<div className="py-4">
<p className="text-sm text-gray-500">
{selectedAccount?.nickname} {selectedAccount?.friendCount}{" "}
</p>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsTransferDialogOpen(false)}>
</Button>
<Button onClick={handleConfirmTransfer}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}