feat: 本次提交更新内容如下

api接入了
This commit is contained in:
笔记本里的永平
2025-07-10 16:36:51 +08:00
parent 53ea1e8395
commit c87378fc66
3 changed files with 366 additions and 63 deletions

View File

@@ -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 查询参数

View 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"
>
&lt;
</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"
>
&gt;
</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>
</>
);
}

View File

@@ -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>