feat: 本次提交更新内容如下
api接入了
This commit is contained in:
309
nkebao/src/components/TrafficPoolSelection.tsx
Normal file
309
nkebao/src/components/TrafficPoolSelection.tsx
Normal file
@@ -0,0 +1,309 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Search, Database } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { useToast } from '@/components/ui/toast';
|
||||
import { fetchDeviceLabels, type TrafficPool, type TrafficPoolListResponse } from '@/api/trafficDistribution';
|
||||
import type { ApiResponse } from '@/types/common';
|
||||
|
||||
// 组件属性接口
|
||||
interface TrafficPoolSelectionProps {
|
||||
selectedPools: string[];
|
||||
onSelect: (pools: string[]) => void;
|
||||
deviceIds: string[];
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function TrafficPoolSelection({
|
||||
selectedPools,
|
||||
onSelect,
|
||||
deviceIds,
|
||||
placeholder = "选择流量池",
|
||||
className = ""
|
||||
}: TrafficPoolSelectionProps) {
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [pools, setPools] = useState<TrafficPool[]>([]);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [totalPools, setTotalPools] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { toast } = useToast();
|
||||
|
||||
// 当弹窗打开时获取流量池列表
|
||||
useEffect(() => {
|
||||
if (dialogOpen && deviceIds.length > 0) {
|
||||
// 弹窗打开时重置搜索和页码,然后立即请求第一页数据
|
||||
setSearchQuery('');
|
||||
setCurrentPage(1);
|
||||
fetchPools(1, '');
|
||||
}
|
||||
}, [dialogOpen, deviceIds]);
|
||||
|
||||
// 监听页码变化,重新请求数据
|
||||
useEffect(() => {
|
||||
if (dialogOpen && deviceIds.length > 0 && currentPage > 1) {
|
||||
fetchPools(currentPage, searchQuery);
|
||||
}
|
||||
}, [currentPage]);
|
||||
|
||||
// 当设备ID变化时,清空已选择的流量池(如果需要的话)
|
||||
useEffect(() => {
|
||||
if (deviceIds.length === 0) {
|
||||
setPools([]);
|
||||
setTotalPools(0);
|
||||
setTotalPages(1);
|
||||
}
|
||||
}, [deviceIds]);
|
||||
|
||||
// 获取流量池列表API
|
||||
const fetchPools = async (page: number, keyword: string = '') => {
|
||||
if (deviceIds.length === 0) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetchDeviceLabels({
|
||||
deviceIds,
|
||||
page,
|
||||
pageSize: 10,
|
||||
keyword
|
||||
});
|
||||
|
||||
if (res && res.code === 200 && res.data) {
|
||||
setPools(res.data.list || []);
|
||||
setTotalPools(res.data.total || 0);
|
||||
setTotalPages(Math.ceil((res.data.total || 0) / 10));
|
||||
} else {
|
||||
toast({
|
||||
title: "获取流量池列表失败",
|
||||
description: res?.msg || "请稍后重试",
|
||||
variant: "destructive"
|
||||
});
|
||||
|
||||
// 使用模拟数据作为降级处理
|
||||
const mockData: TrafficPool[] = [
|
||||
{ id: "1", name: "新客流量池", count: 1250, description: "新获取的客户流量", deviceIds },
|
||||
{ id: "2", name: "高意向流量池", count: 850, description: "有购买意向的客户", deviceIds },
|
||||
{ id: "3", name: "复购流量池", count: 620, description: "已购买过产品的客户", deviceIds },
|
||||
{ id: "4", name: "活跃流量池", count: 1580, description: "近期活跃的客户", deviceIds },
|
||||
{ id: "5", name: "沉睡流量池", count: 2300, description: "长期未活跃的客户", deviceIds },
|
||||
{ id: "6", name: "VIP客户池", count: 156, description: "VIP等级客户", deviceIds },
|
||||
{ id: "7", name: "潜在客户池", count: 3200, description: "有潜在购买可能的客户", deviceIds },
|
||||
{ id: "8", name: "游戏玩家池", count: 890, description: "游戏类产品感兴趣客户", deviceIds },
|
||||
];
|
||||
|
||||
// 根据关键词过滤模拟数据
|
||||
const filteredData = keyword
|
||||
? mockData.filter(pool =>
|
||||
pool.name.toLowerCase().includes(keyword.toLowerCase()) ||
|
||||
(pool.description && pool.description.toLowerCase().includes(keyword.toLowerCase()))
|
||||
)
|
||||
: mockData;
|
||||
|
||||
// 分页处理模拟数据
|
||||
const startIndex = (page - 1) * 10;
|
||||
const endIndex = startIndex + 10;
|
||||
const paginatedData = filteredData.slice(startIndex, endIndex);
|
||||
|
||||
setPools(paginatedData);
|
||||
setTotalPools(filteredData.length);
|
||||
setTotalPages(Math.ceil(filteredData.length / 10));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取流量池列表失败:', error);
|
||||
toast({
|
||||
title: "网络错误",
|
||||
description: "请检查网络连接后重试",
|
||||
variant: "destructive"
|
||||
});
|
||||
|
||||
// 网络错误时使用模拟数据
|
||||
const mockData: TrafficPool[] = [
|
||||
{ id: "1", name: "新客流量池", count: 1250, description: "新获取的客户流量", deviceIds },
|
||||
{ id: "2", name: "高意向流量池", count: 850, description: "有购买意向的客户", deviceIds },
|
||||
{ id: "3", name: "复购流量池", count: 620, description: "已购买过产品的客户", deviceIds },
|
||||
{ id: "4", name: "活跃流量池", count: 1580, description: "近期活跃的客户", deviceIds },
|
||||
{ id: "5", name: "沉睡流量池", count: 2300, description: "长期未活跃的客户", deviceIds },
|
||||
];
|
||||
|
||||
setPools(mockData);
|
||||
setTotalPools(mockData.length);
|
||||
setTotalPages(1);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理搜索
|
||||
const handleSearch = (keyword: string) => {
|
||||
setSearchQuery(keyword);
|
||||
setCurrentPage(1);
|
||||
// 立即搜索,不管弹窗是否打开(因为这个函数只在弹窗内调用)
|
||||
if (deviceIds.length > 0) {
|
||||
fetchPools(1, keyword);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理流量池选择
|
||||
const handlePoolToggle = (poolId: string) => {
|
||||
if (selectedPools.includes(poolId)) {
|
||||
onSelect(selectedPools.filter(id => id !== poolId));
|
||||
} else {
|
||||
onSelect([...selectedPools, poolId]);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取显示文本
|
||||
const getDisplayText = () => {
|
||||
if (selectedPools.length === 0) return '';
|
||||
return `已选择 ${selectedPools.length} 个流量池`;
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
setDialogOpen(false);
|
||||
};
|
||||
|
||||
// 处理弹窗关闭
|
||||
const handleDialogClose = (open: boolean) => {
|
||||
setDialogOpen(open);
|
||||
if (!open) {
|
||||
// 弹窗关闭时可以选择性清理状态,这里保留搜索状态以便下次打开时使用
|
||||
// setSearchQuery('');
|
||||
// setCurrentPage(1);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理输入框点击
|
||||
const handleInputClick = () => {
|
||||
if (deviceIds.length === 0) {
|
||||
toast({
|
||||
title: "请先选择设备",
|
||||
description: "需要先选择设备才能选择流量池",
|
||||
variant: "destructive"
|
||||
});
|
||||
return;
|
||||
}
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 输入框 */}
|
||||
<div className={`relative ${className}`}>
|
||||
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400">
|
||||
<Database className="w-5 h-5" />
|
||||
</span>
|
||||
<Input
|
||||
placeholder={placeholder}
|
||||
className="pl-10 h-12 rounded-xl border-gray-200 text-base"
|
||||
readOnly
|
||||
onClick={handleInputClick}
|
||||
value={getDisplayText()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 流量池选择弹窗 */}
|
||||
<Dialog open={dialogOpen} onOpenChange={handleDialogClose}>
|
||||
<DialogContent className="max-w-sm w-[90vw] max-h-[90vh] flex flex-col p-0 gap-0 overflow-hidden">
|
||||
<div className="p-6">
|
||||
<DialogTitle className="text-center text-xl font-medium mb-6">选择流量池</DialogTitle>
|
||||
|
||||
<div className="relative mb-4">
|
||||
<Input
|
||||
placeholder="搜索流量池"
|
||||
value={searchQuery}
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
className="pl-10 py-2 rounded-full border-gray-200"
|
||||
/>
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="flex-1 overflow-y-auto">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-gray-500">加载中...</div>
|
||||
</div>
|
||||
) : pools.length > 0 ? (
|
||||
<div className="divide-y">
|
||||
{pools.map((pool) => (
|
||||
<label
|
||||
key={pool.id}
|
||||
className="flex items-center px-6 py-4 hover:bg-gray-50 cursor-pointer"
|
||||
onClick={() => handlePoolToggle(pool.id)}
|
||||
>
|
||||
<div className="mr-3 flex items-center justify-center">
|
||||
<div className={`w-5 h-5 rounded-full border ${selectedPools.includes(pool.id) ? 'border-blue-600' : 'border-gray-300'} flex items-center justify-center`}>
|
||||
{selectedPools.includes(pool.id) && (
|
||||
<div className="w-3 h-3 rounded-full bg-blue-600"></div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3 flex-1">
|
||||
<div className="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center">
|
||||
<Database className="h-5 w-5 text-blue-600" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium">{pool.name}</div>
|
||||
{pool.description && (
|
||||
<div className="text-sm text-gray-500">{pool.description}</div>
|
||||
)}
|
||||
<div className="text-sm text-gray-400">{pool.count} 人</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-gray-500">
|
||||
{deviceIds.length === 0 ? '请先选择设备' : '没有找到流量池'}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
|
||||
<div className="border-t p-4 flex items-center justify-between bg-white">
|
||||
<div className="text-sm text-gray-500">
|
||||
总计 {totalPools} 个流量池
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
|
||||
disabled={currentPage === 1 || loading}
|
||||
className="px-2 py-0 h-8 min-w-0"
|
||||
>
|
||||
<
|
||||
</Button>
|
||||
<span className="text-sm">{currentPage} / {totalPages}</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}
|
||||
disabled={currentPage === totalPages || loading}
|
||||
className="px-2 py-0 h-8 min-w-0"
|
||||
>
|
||||
>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t p-4 flex items-center justify-between bg-white">
|
||||
<Button variant="outline" onClick={() => setDialogOpen(false)} className="px-6 rounded-full border-gray-300">
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={handleConfirm} className="px-6 bg-blue-600 hover:bg-blue-700 rounded-full">
|
||||
确定 ({selectedPools.length})
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user