feat: 本次提交更新内容如下
api接入了
This commit is contained in:
@@ -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<ApiResponse<AccountListResponse>>(`/v1/workbench/account-list?${queryParams.toString()}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取设备标签(流量池)列表
|
||||
* @param params 查询参数
|
||||
* @returns 流量池列表
|
||||
*/
|
||||
export const fetchDeviceLabels = async (params: {
|
||||
deviceIds: string[]; // 设备ID列表
|
||||
page?: number; // 页码
|
||||
pageSize?: number; // 每页数量
|
||||
keyword?: string; // 搜索关键词
|
||||
}): Promise<ApiResponse<TrafficPoolListResponse>> => {
|
||||
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<ApiResponse<TrafficPoolListResponse>>(`/v1/workbench/device-labels?${queryParams.toString()}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取流量分发规则列表
|
||||
* @param params 查询参数
|
||||
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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<TrafficPoolData> }) => {
|
||||
const [selectedPools, setSelectedPools] = useState<string[]>(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 (
|
||||
<div className="bg-white rounded-lg p-6">
|
||||
<h2 className="text-xl font-bold mb-6">流量池选择</h2>
|
||||
|
||||
<div className="mb-4">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={18} />
|
||||
<Input
|
||||
placeholder="搜索流量池"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10 h-12"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 mt-4">
|
||||
{filteredPools.map(pool => (
|
||||
<div
|
||||
key={pool.id}
|
||||
className={`cursor-pointer border rounded-lg ${selectedPools.includes(pool.id) ? "border-blue-500 bg-blue-50" : "border-gray-200"}`}
|
||||
onClick={() => togglePool(pool.id)}
|
||||
>
|
||||
<div className="p-4 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<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>
|
||||
<p className="font-medium">{pool.name}</p>
|
||||
<p className="text-sm text-gray-500">{pool.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<span className="text-sm text-gray-500">{pool.count} 人</span>
|
||||
<Checkbox
|
||||
checked={selectedPools.includes(pool.id)}
|
||||
onCheckedChange={() => togglePool(pool.id)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="mb-6">
|
||||
<TrafficPoolSelection
|
||||
selectedPools={selectedPools}
|
||||
onSelect={setSelectedPools}
|
||||
deviceIds={deviceIds}
|
||||
placeholder="选择流量池"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex justify-between">
|
||||
<Button variant="outline" onClick={onBack}>
|
||||
← 上一步
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} disabled={isSubmitting}>
|
||||
<Button onClick={handleSubmit} disabled={isSubmitting || selectedPools.length === 0}>
|
||||
{isSubmitting ? "提交中..." : "完成"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user