私域操盘手 - 修复流量池页面点击”全部来源“和全部状态的select框,页面被压缩的问题

This commit is contained in:
柳清爽
2025-05-13 11:15:34 +08:00
parent 2da11bbbc9
commit 05c5f6c8e3
2 changed files with 234 additions and 231 deletions

View File

@@ -85,7 +85,7 @@ export default function TrafficPoolPage() {
}) })
// 检查是否有来源参数 // 检查是否有来源参数
const sourceParam = searchParams.get("source") const sourceParam = searchParams?.get("source")
if (sourceParam) { if (sourceParam) {
params.append("wechatSource", sourceParam) params.append("wechatSource", sourceParam)
} }
@@ -162,7 +162,7 @@ export default function TrafficPoolPage() {
} }
return ( return (
<div className="flex-1 bg-white min-h-screen"> <div className="flex-1 bg-white min-h-screen flex flex-col">
<header className="sticky top-0 z-10 bg-white border-b"> <header className="sticky top-0 z-10 bg-white border-b">
<div className="flex items-center justify-between p-4"> <div className="flex items-center justify-between p-4">
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
@@ -177,195 +177,197 @@ export default function TrafficPoolPage() {
</div> </div>
</header> </header>
<div className="p-4 space-y-6"> <div className="flex-1 overflow-y-auto">
{/* 搜索和筛选区域 */} <div className="p-4 space-y-6">
<div className="flex items-center space-x-2"> {/* 搜索和筛选区域 */}
<div className="relative flex-1"> <div className="flex items-center space-x-2">
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" /> <div className="relative flex-1">
<Input <Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
placeholder="搜索用户" <Input
value={searchQuery} placeholder="搜索用户"
onChange={(e) => { value={searchQuery}
setSearchQuery(e.target.value) onChange={(e) => {
setSearchQuery(e.target.value)
setCurrentPage(1)
}}
className="pl-9"
/>
</div>
<Button variant="outline" size="icon">
<Filter className="h-4 w-4" />
</Button>
</div>
{/* 统计卡片 */}
<div className="grid grid-cols-2 gap-4">
<Card className="p-4">
<div className="text-sm text-gray-500"></div>
<div className="text-2xl font-bold text-blue-600">{stats.total}</div>
</Card>
<Card className="p-4">
<div className="text-sm text-gray-500"></div>
<div className="text-2xl font-bold text-green-600">{stats.todayNew}</div>
</Card>
</div>
{/* 分类标签页 */}
<Tabs
defaultValue="potential"
value={activeCategory}
onValueChange={(value) => {
setActiveCategory(value)
setCurrentPage(1)
}}
>
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="potential"></TabsTrigger>
<TabsTrigger value="customer"></TabsTrigger>
</TabsList>
</Tabs>
{/* 筛选器 */}
<div className="flex space-x-2">
<Select
value={sourceFilter}
onValueChange={(value) => {
setSourceFilter(value)
setCurrentPage(1) setCurrentPage(1)
}} }}
className="pl-9" >
/> <SelectTrigger className="w-[120px]">
<SelectValue placeholder="来源" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="抖音直播"></SelectItem>
<SelectItem value="小红书"></SelectItem>
<SelectItem value="微信朋友圈"></SelectItem>
<SelectItem value="视频号"></SelectItem>
<SelectItem value="公众号"></SelectItem>
</SelectContent>
</Select>
<Select
value={statusFilter}
onValueChange={(value) => {
setStatusFilter(value)
setCurrentPage(1)
}}
>
<SelectTrigger className="w-[120px]">
<SelectValue placeholder="状态" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="pending"></SelectItem>
<SelectItem value="added"></SelectItem>
<SelectItem value="failed"></SelectItem>
</SelectContent>
</Select>
</div> </div>
<Button variant="outline" size="icon">
<Filter className="h-4 w-4" />
</Button>
</div>
{/* 统计卡片 */} {/* 用户列表 */}
<div className="grid grid-cols-2 gap-4"> <div className="space-y-2">
<Card className="p-4"> {loading ? (
<div className="text-sm text-gray-500"></div> <div className="flex flex-col items-center justify-center py-12">
<div className="text-2xl font-bold text-blue-600">{stats.total}</div> <RefreshCw className="h-8 w-8 text-blue-500 animate-spin mb-4" />
</Card> <div className="text-gray-500">...</div>
<Card className="p-4"> </div>
<div className="text-sm text-gray-500"></div> ) : users.length === 0 ? (
<div className="text-2xl font-bold text-green-600">{stats.todayNew}</div> <div className="text-center py-12 bg-gray-50 rounded-lg">
</Card> <div className="text-gray-500"></div>
</div> <Button variant="outline" className="mt-4" onClick={fetchUsers}>
</Button>
</div>
) : (
users.map((user) => (
<Card
key={user.id}
className="p-3 cursor-pointer hover:shadow-md transition-shadow"
onClick={() => handleUserClick(user)}
>
<div className="flex items-center space-x-3">
<img src={user.avatar || "/placeholder.svg"} alt="" className="w-10 h-10 rounded-full bg-gray-100" />
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between">
<div className="font-medium truncate">{user.nickname}</div>
<div
className={`text-xs px-2 py-1 rounded-full ${
user.status === "added"
? "bg-green-100 text-green-800"
: user.status === "pending"
? "bg-yellow-100 text-yellow-800"
: "bg-red-100 text-red-800"
}`}
>
{user.status === "added" ? "已添加" : user.status === "pending" ? "待处理" : "已失败"}
</div>
</div>
<div className="text-sm text-gray-500">: {user.wechatId}</div>
<div className="text-sm text-gray-500">: {user.source}</div>
<div className="text-sm text-gray-500">: {new Date(user.addTime).toLocaleString()}</div>
{/* 分类标签页 - 移除了"全部"选项 */} {/* 标签展示 */}
<Tabs <div className="flex flex-wrap gap-1 mt-2">
defaultValue="potential" {user.tags.slice(0, 2).map((tag) => (
value={activeCategory} <span key={tag.id} className={`text-xs px-2 py-0.5 rounded-full ${tag.color}`}>
onValueChange={(value) => { {tag.name}
setActiveCategory(value) </span>
setCurrentPage(1) ))}
}} {user.tags.length > 2 && (
> <span className="text-xs px-2 py-0.5 rounded-full bg-gray-100 text-gray-800">
<TabsList className="grid w-full grid-cols-2"> +{user.tags.length - 2}
<TabsTrigger value="potential"></TabsTrigger> </span>
<TabsTrigger value="customer"></TabsTrigger> )}
</TabsList>
</Tabs>
{/* 筛选器 */}
<div className="flex space-x-2">
<Select
value={sourceFilter}
onValueChange={(value) => {
setSourceFilter(value)
setCurrentPage(1)
}}
>
<SelectTrigger className="w-[120px]">
<SelectValue placeholder="来源" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="抖音直播"></SelectItem>
<SelectItem value="小红书"></SelectItem>
<SelectItem value="微信朋友圈"></SelectItem>
<SelectItem value="视频号"></SelectItem>
<SelectItem value="公众号"></SelectItem>
</SelectContent>
</Select>
<Select
value={statusFilter}
onValueChange={(value) => {
setStatusFilter(value)
setCurrentPage(1)
}}
>
<SelectTrigger className="w-[120px]">
<SelectValue placeholder="状态" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="pending"></SelectItem>
<SelectItem value="added"></SelectItem>
<SelectItem value="failed"></SelectItem>
</SelectContent>
</Select>
</div>
{/* 用户列表 - 改进加载状态显示 */}
<div className="space-y-2">
{loading ? (
<div className="flex flex-col items-center justify-center py-12">
<RefreshCw className="h-8 w-8 text-blue-500 animate-spin mb-4" />
<div className="text-gray-500">...</div>
</div>
) : users.length === 0 ? (
<div className="text-center py-12 bg-gray-50 rounded-lg">
<div className="text-gray-500"></div>
<Button variant="outline" className="mt-4" onClick={fetchUsers}>
</Button>
</div>
) : (
users.map((user) => (
<Card
key={user.id}
className="p-3 cursor-pointer hover:shadow-md transition-shadow"
onClick={() => handleUserClick(user)}
>
<div className="flex items-center space-x-3">
<img src={user.avatar || "/placeholder.svg"} alt="" className="w-10 h-10 rounded-full bg-gray-100" />
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between">
<div className="font-medium truncate">{user.nickname}</div>
<div
className={`text-xs px-2 py-1 rounded-full ${
user.status === "added"
? "bg-green-100 text-green-800"
: user.status === "pending"
? "bg-yellow-100 text-yellow-800"
: "bg-red-100 text-red-800"
}`}
>
{user.status === "added" ? "已添加" : user.status === "pending" ? "待处理" : "已失败"}
</div> </div>
</div> </div>
<div className="text-sm text-gray-500">: {user.wechatId}</div>
<div className="text-sm text-gray-500">: {user.source}</div>
<div className="text-sm text-gray-500">: {new Date(user.addTime).toLocaleString()}</div>
{/* 标签展示 */}
<div className="flex flex-wrap gap-1 mt-2">
{user.tags.slice(0, 2).map((tag) => (
<span key={tag.id} className={`text-xs px-2 py-0.5 rounded-full ${tag.color}`}>
{tag.name}
</span>
))}
{user.tags.length > 2 && (
<span className="text-xs px-2 py-0.5 rounded-full bg-gray-100 text-gray-800">
+{user.tags.length - 2}
</span>
)}
</div>
</div> </div>
</div> </Card>
</Card> ))
)) )}
)} </div>
</div>
{/* 分页 */} {/* 分页 */}
{!loading && users.length > 0 && ( {!loading && users.length > 0 && (
<Pagination> <Pagination>
<PaginationContent> <PaginationContent>
<PaginationItem> <PaginationItem>
<PaginationPrevious <PaginationPrevious
href="#"
onClick={(e) => {
e.preventDefault()
setCurrentPage((prev) => Math.max(1, prev - 1))
}}
/>
</PaginationItem>
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
<PaginationItem key={page}>
<PaginationLink
href="#" href="#"
isActive={currentPage === page}
onClick={(e) => { onClick={(e) => {
e.preventDefault() e.preventDefault()
setCurrentPage(page) setCurrentPage((prev) => Math.max(1, prev - 1))
}} }}
> />
{page}
</PaginationLink>
</PaginationItem> </PaginationItem>
))} {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
<PaginationItem> <PaginationItem key={page}>
<PaginationNext <PaginationLink
href="#" href="#"
onClick={(e) => { isActive={currentPage === page}
e.preventDefault() onClick={(e) => {
setCurrentPage((prev) => Math.min(totalPages, prev + 1)) e.preventDefault()
}} setCurrentPage(page)
/> }}
</PaginationItem> >
</PaginationContent> {page}
</Pagination> </PaginationLink>
)} </PaginationItem>
))}
<PaginationItem>
<PaginationNext
href="#"
onClick={(e) => {
e.preventDefault()
setCurrentPage((prev) => Math.min(totalPages, prev + 1))
}}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
)}
</div>
</div> </div>
{/* 用户详情弹窗 */} {/* 用户详情弹窗 */}

View File

@@ -398,17 +398,17 @@ export default function WechatAccountDetailPage() {
setIsFetchingFriends(true); setIsFetchingFriends(true);
setHasFriendLoadError(false); setHasFriendLoadError(false);
const response = await api.get<ApiResponse<FriendsResponse>>(`/v1/wechats/${id}/friends?page=${page}&limit=30${searchQuery ? `&search=${encodeURIComponent(searchQuery)}` : ''}`, true); const data = await api.get<ApiResponse<FriendsResponse>>(`/v1/wechats/${id}/friends?page=${page}&limit=30`, true);
if (response && response.code === 200 && response.data) { if (data && data.code === 200) {
// 更新总数计数 // 更新总数计数
if (isNewSearch || friendsTotal === 0) { if (isNewSearch || friendsTotal === 0) {
setFriendsTotal(response.data.total || 0); setFriendsTotal(data.data.total || 0);
} }
const newFriends = response.data.list.map((friend) => ({ const newFriends = data.data.list.map((friend) => ({
id: friend.id.toString(), id: friend.id.toString(),
avatar: friend.avatar || '/placeholder.svg', avatar: friend.avatar,
nickname: friend.nickname, nickname: friend.nickname,
wechatId: friend.wechatId, wechatId: friend.wechatId,
remark: friend.memo || '', remark: friend.memo || '',
@@ -433,12 +433,13 @@ export default function WechatAccountDetailPage() {
setFriendsPage(page); setFriendsPage(page);
// 判断是否还有更多数据 // 判断是否还有更多数据
setHasMoreFriends(page * 30 < response.data.total); setHasMoreFriends(page * 30 < data.data.total);
} else { } else {
setHasFriendLoadError(true); setHasFriendLoadError(true);
toast({ toast({
title: "获取好友列表失败", title: "获取好友列表失败",
description: response?.msg || "请稍后再试", description: data?.msg || "请稍后再试",
variant: "destructive" variant: "destructive"
}); });
} }
@@ -453,7 +454,7 @@ export default function WechatAccountDetailPage() {
} finally { } finally {
setIsFetchingFriends(false); setIsFetchingFriends(false);
} }
}, [account, id, friendsTotal, searchQuery]); }, [account, id, friendsTotal]);
// 处理搜索 // 处理搜索
const handleSearch = useCallback(() => { const handleSearch = useCallback(() => {
@@ -528,21 +529,21 @@ export default function WechatAccountDetailPage() {
const response = await fetchWechatAccountSummary(id); const response = await fetchWechatAccountSummary(id);
if (response.code === 200) { if (response.code === 200) {
setAccountSummary(response.data); setAccountSummary(response.data);
} else { } else {
toast({ toast({
title: "获取账号概览失败", title: "获取账号概览失败",
description: response.msg || "请稍后再试", description: response.msg || "请稍后再试",
variant: "destructive" variant: "destructive"
}); });
} }
} catch (error) { } catch (error) {
console.error("获取账号概览失败:", error); console.error("获取账号概览失败:", error);
toast({ toast({
title: "获取账号概览失败", title: "获取账号概览失败",
description: "请检查网络连接或稍后再试", description: "请检查网络连接或稍后再试",
variant: "destructive" variant: "destructive"
}); });
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}, [id]); }, [id]);
@@ -726,9 +727,9 @@ export default function WechatAccountDetailPage() {
<Tabs value={activeTab} onValueChange={handleTabChange} className="w-full"> <Tabs value={activeTab} onValueChange={handleTabChange} className="w-full">
<TabsList className="grid w-full grid-cols-2"> <TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="overview"></TabsTrigger> <TabsTrigger value="overview"></TabsTrigger>
<TabsTrigger value="friends"> <TabsTrigger value="friends">
{activeTab === "friends" && friendsTotal > 0 ? ` (${friendsTotal})` : ''} {activeTab === "friends" && friendsTotal > 0 ? ` (${friendsTotal})` : ''}
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="overview" className="space-y-4 mt-4"> <TabsContent value="overview" className="space-y-4 mt-4">
@@ -775,33 +776,33 @@ export default function WechatAccountDetailPage() {
{accountSummary && ( {accountSummary && (
<div className={`flex items-center space-x-2 ${getWeightColor(accountSummary.accountWeight.scope)}`}> <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-2xl font-bold">{accountSummary.accountWeight.scope}</span>
<span className="text-sm"></span> <span className="text-sm"></span>
</div> </div>
)} )}
</div> </div>
{accountSummary && ( {accountSummary && (
<> <>
<p className="text-sm text-gray-500 mb-4">{getWeightDescription(accountSummary.accountWeight.scope)}</p> <p className="text-sm text-gray-500 mb-4">{getWeightDescription(accountSummary.accountWeight.scope)}</p>
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center"> <div className="flex items-center">
<span className="flex-shrink-0 w-16 text-sm"></span> <span className="flex-shrink-0 w-16 text-sm"></span>
<div className="flex-1 mx-4"> <div className="flex-1 mx-4">
<Progress value={accountSummary.accountWeight.ageWeight} className="h-2" /> <Progress value={accountSummary.accountWeight.ageWeight} className="h-2" />
</div> </div>
<span className="flex-shrink-0 w-12 text-sm text-right">{accountSummary.accountWeight.ageWeight}%</span> <span className="flex-shrink-0 w-12 text-sm text-right">{accountSummary.accountWeight.ageWeight}%</span>
</div> </div>
<div className="flex items-center"> <div className="flex items-center">
<span className="flex-shrink-0 w-16 text-sm"></span> <span className="flex-shrink-0 w-16 text-sm"></span>
<div className="flex-1 mx-4"> <div className="flex-1 mx-4">
<Progress value={accountSummary.accountWeight.activityWeigth} className="h-2" /> <Progress value={accountSummary.accountWeight.activityWeigth} className="h-2" />
</div> </div>
<span className="flex-shrink-0 w-12 text-sm text-right">{accountSummary.accountWeight.activityWeigth}%</span> <span className="flex-shrink-0 w-12 text-sm text-right">{accountSummary.accountWeight.activityWeigth}%</span>
</div> </div>
<div className="flex items-center"> <div className="flex items-center">
<span className="flex-shrink-0 w-16 text-sm"></span> <span className="flex-shrink-0 w-16 text-sm"></span>
<div className="flex-1 mx-4"> <div className="flex-1 mx-4">
<Progress value={accountSummary.accountWeight.restrictWeight} className="h-2" /> <Progress value={accountSummary.accountWeight.restrictWeight} className="h-2" />
</div> </div>
<span className="flex-shrink-0 w-12 text-sm text-right">{accountSummary.accountWeight.restrictWeight}%</span> <span className="flex-shrink-0 w-12 text-sm text-right">{accountSummary.accountWeight.restrictWeight}%</span>
</div> </div>
<div className="flex items-center"> <div className="flex items-center">
@@ -833,29 +834,29 @@ export default function WechatAccountDetailPage() {
</UITooltip> </UITooltip>
</div> </div>
{accountSummary && ( {accountSummary && (
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm text-gray-500"></span> <span className="text-sm text-gray-500"></span>
<span className="text-xl font-bold text-blue-600">{accountSummary.statistics.todayAdded}</span> <span className="text-xl font-bold text-blue-600">{accountSummary.statistics.todayAdded}</span>
</div> </div>
<div> <div>
<div className="flex justify-between text-sm mb-2"> <div className="flex justify-between text-sm mb-2">
<span className="text-gray-500"></span> <span className="text-gray-500"></span>
<span> <span>
{accountSummary.statistics.todayAdded}/{accountSummary.statistics.addLimit} {accountSummary.statistics.todayAdded}/{accountSummary.statistics.addLimit}
</span> </span>
</div>
<Progress
value={(accountSummary.statistics.todayAdded / accountSummary.statistics.addLimit) * 100}
className="h-2"
/>
</div> </div>
<div className="text-sm text-gray-500"> <Progress
value={(accountSummary.statistics.todayAdded / accountSummary.statistics.addLimit) * 100}
className="h-2"
/>
</div>
<div className="text-sm text-gray-500">
({accountSummary.accountWeight.scope}){" "} ({accountSummary.accountWeight.scope}){" "}
<span className="font-medium text-blue-600">{accountSummary.statistics.addLimit}</span>{" "} <span className="font-medium text-blue-600">{accountSummary.statistics.addLimit}</span>{" "}
</div>
</div> </div>
</div>
)} )}
</Card> </Card>
@@ -867,24 +868,24 @@ export default function WechatAccountDetailPage() {
<span className="font-medium"></span> <span className="font-medium"></span>
</div> </div>
{accountSummary && ( {accountSummary && (
<Badge variant="outline" className="cursor-pointer" onClick={() => setShowRestrictions(true)}> <Badge variant="outline" className="cursor-pointer" onClick={() => setShowRestrictions(true)}>
{accountSummary.restrictions.length} {accountSummary.restrictions.length}
</Badge> </Badge>
)} )}
</div> </div>
{accountSummary && ( {accountSummary && (
<div className="space-y-2"> <div className="space-y-2">
{accountSummary.restrictions.slice(0, 2).map((record) => ( {accountSummary.restrictions.slice(0, 2).map((record) => (
<div key={record.id} className="text-sm"> <div key={record.id} className="text-sm">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className={`${getRestrictionLevelColor(record.level)}`}> <span className={`${getRestrictionLevelColor(record.level)}`}>
{record.reason} {record.reason}
</span> </span>
<span className="text-gray-500">{formatDateTime(record.date)}</span> <span className="text-gray-500">{formatDateTime(record.date)}</span>
</div>
</div> </div>
))} </div>
</div> ))}
</div>
)} )}
</Card> </Card>
</TabsContent> </TabsContent>
@@ -1010,7 +1011,7 @@ export default function WechatAccountDetailPage() {
<div className="flex justify-between items-start"> <div className="flex justify-between items-start">
<div className={`text-sm ${getRestrictionLevelColor(record.level)}`}> <div className={`text-sm ${getRestrictionLevelColor(record.level)}`}>
{record.reason} {record.reason}
</div> </div>
<Badge variant="outline">{formatDateTime(record.date)}</Badge> <Badge variant="outline">{formatDateTime(record.date)}</Badge>
</div> </div>
<div className="text-sm text-gray-500 mt-1">{formatDateTime(record.date)}</div> <div className="text-sm text-gray-500 mt-1">{formatDateTime(record.date)}</div>
@@ -1186,7 +1187,7 @@ export default function WechatAccountDetailPage() {
</div> </div>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{selectedFriend.tags.map((tag: FriendTag) => ( {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>