From c87378fc6653cbaab21cd5cb284482a3fbf34503 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=AC=94=E8=AE=B0=E6=9C=AC=E9=87=8C=E7=9A=84=E6=B0=B8?= =?UTF-8?q?=E5=B9=B3?= Date: Thu, 10 Jul 2025 16:36:51 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9C=AC=E6=AC=A1=E6=8F=90=E4=BA=A4?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=86=85=E5=AE=B9=E5=A6=82=E4=B8=8B=20api?= =?UTF-8?q?=E6=8E=A5=E5=85=A5=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nkebao/src/api/trafficDistribution.ts | 44 +++ .../src/components/TrafficPoolSelection.tsx | 309 ++++++++++++++++++ .../traffic-distribution/NewDistribution.tsx | 76 +---- 3 files changed, 366 insertions(+), 63 deletions(-) create mode 100644 nkebao/src/components/TrafficPoolSelection.tsx diff --git a/nkebao/src/api/trafficDistribution.ts b/nkebao/src/api/trafficDistribution.ts index 02bf2821..0e7df24e 100644 --- a/nkebao/src/api/trafficDistribution.ts +++ b/nkebao/src/api/trafficDistribution.ts @@ -36,6 +36,25 @@ export interface AccountListResponse { limit: number; } +// 流量池类型 +export interface TrafficPool { + id: string; + name: string; + count: number; + description?: string; + deviceIds: string[]; + createTime?: string; + updateTime?: string; +} + +// 流量池列表响应类型 +export interface TrafficPoolListResponse { + list: TrafficPool[]; + total: number; + page: number; + pageSize: number; +} + // 流量分发规则类型 export interface DistributionRule { id: string; @@ -96,6 +115,31 @@ export const fetchAccountList = async (params: { return get>(`/v1/workbench/account-list?${queryParams.toString()}`); }; +/** + * 获取设备标签(流量池)列表 + * @param params 查询参数 + * @returns 流量池列表 + */ +export const fetchDeviceLabels = async (params: { + deviceIds: string[]; // 设备ID列表 + page?: number; // 页码 + pageSize?: number; // 每页数量 + keyword?: string; // 搜索关键词 +}): Promise> => { + const { deviceIds, page = 1, pageSize = 10, keyword = "" } = params; + + const queryParams = new URLSearchParams(); + queryParams.append('deviceIds', deviceIds.join(',')); + queryParams.append('page', page.toString()); + queryParams.append('pageSize', pageSize.toString()); + + if (keyword) { + queryParams.append('keyword', keyword); + } + + return get>(`/v1/workbench/device-labels?${queryParams.toString()}`); +}; + /** * 获取流量分发规则列表 * @param params 查询参数 diff --git a/nkebao/src/components/TrafficPoolSelection.tsx b/nkebao/src/components/TrafficPoolSelection.tsx new file mode 100644 index 00000000..779e41cd --- /dev/null +++ b/nkebao/src/components/TrafficPoolSelection.tsx @@ -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([]); + 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 ( + <> + {/* 输入框 */} +
+ + + + +
+ + {/* 流量池选择弹窗 */} + + +
+ 选择流量池 + +
+ handleSearch(e.target.value)} + className="pl-10 py-2 rounded-full border-gray-200" + /> + +
+
+ + + {loading ? ( +
+
加载中...
+
+ ) : pools.length > 0 ? ( +
+ {pools.map((pool) => ( + + ))} +
+ ) : ( +
+
+ {deviceIds.length === 0 ? '请先选择设备' : '没有找到流量池'} +
+
+ )} +
+ +
+
+ 总计 {totalPools} 个流量池 +
+
+ + {currentPage} / {totalPages} + +
+
+ +
+ + +
+
+
+ + ); +} \ No newline at end of file diff --git a/nkebao/src/pages/workspace/traffic-distribution/NewDistribution.tsx b/nkebao/src/pages/workspace/traffic-distribution/NewDistribution.tsx index d08c074b..130fdaf0 100644 --- a/nkebao/src/pages/workspace/traffic-distribution/NewDistribution.tsx +++ b/nkebao/src/pages/workspace/traffic-distribution/NewDistribution.tsx @@ -19,6 +19,7 @@ import DeviceSelection from '@/components/DeviceSelection'; import { useToast } from '@/components/ui/toast'; import { fetchAccountList, Account } from '@/api/trafficDistribution'; import '@/components/Layout.css'; +import TrafficPoolSelection from '@/components/TrafficPoolSelection'; interface BasicInfoData { name: string; @@ -585,30 +586,8 @@ export default function NewDistribution() { // 流量池选择步骤组件 const TrafficPoolStep = ({ onSubmit, onBack, initialData = {} }: { onSubmit: (data: TrafficPoolData) => void; onBack: () => void; initialData?: Partial }) => { const [selectedPools, setSelectedPools] = useState(initialData.selectedPools || []); - const [searchTerm, setSearchTerm] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); - // 模拟流量池数据 - const trafficPools: TrafficPool[] = [ - { id: "1", name: "新客流量池", count: 1250, description: "新获取的客户流量" }, - { id: "2", name: "高意向流量池", count: 850, description: "有购买意向的客户" }, - { id: "3", name: "复购流量池", count: 620, description: "已购买过产品的客户" }, - { id: "4", name: "活跃流量池", count: 1580, description: "近期活跃的客户" }, - { id: "5", name: "沉睡流量池", count: 2300, description: "长期未活跃的客户" }, - ]; - - const filteredPools = trafficPools.filter( - pool => - pool.name.toLowerCase().includes(searchTerm.toLowerCase()) || - pool.description.toLowerCase().includes(searchTerm.toLowerCase()) - ); - - const togglePool = (id: string) => { - setSelectedPools(prev => - prev.includes(id) ? prev.filter(poolId => poolId !== id) : [...prev, id] - ); - }; - const handleSubmit = async () => { if (selectedPools.length === 0) { toast({ @@ -635,56 +614,27 @@ export default function NewDistribution() { } }; + // 从formData中获取选中的设备ID + const deviceIds = formData.targetSettings?.selectedDevices || []; + return (

流量池选择

- -
-
- - setSearchTerm(e.target.value)} - className="pl-10 h-12" - /> -
-
- -
- {filteredPools.map(pool => ( -
togglePool(pool.id)} - > -
-
-
- -
-
-

{pool.name}

-

{pool.description}

-
-
-
- {pool.count} 人 - togglePool(pool.id)} - /> -
-
-
- ))} + +
+
-