feat: 本次提交更新内容如下
api接入了
This commit is contained in:
@@ -36,6 +36,25 @@ export interface AccountListResponse {
|
|||||||
limit: number;
|
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 {
|
export interface DistributionRule {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -96,6 +115,31 @@ export const fetchAccountList = async (params: {
|
|||||||
return get<ApiResponse<AccountListResponse>>(`/v1/workbench/account-list?${queryParams.toString()}`);
|
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 查询参数
|
* @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 { useToast } from '@/components/ui/toast';
|
||||||
import { fetchAccountList, Account } from '@/api/trafficDistribution';
|
import { fetchAccountList, Account } from '@/api/trafficDistribution';
|
||||||
import '@/components/Layout.css';
|
import '@/components/Layout.css';
|
||||||
|
import TrafficPoolSelection from '@/components/TrafficPoolSelection';
|
||||||
|
|
||||||
interface BasicInfoData {
|
interface BasicInfoData {
|
||||||
name: string;
|
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 TrafficPoolStep = ({ onSubmit, onBack, initialData = {} }: { onSubmit: (data: TrafficPoolData) => void; onBack: () => void; initialData?: Partial<TrafficPoolData> }) => {
|
||||||
const [selectedPools, setSelectedPools] = useState<string[]>(initialData.selectedPools || []);
|
const [selectedPools, setSelectedPools] = useState<string[]>(initialData.selectedPools || []);
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
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 () => {
|
const handleSubmit = async () => {
|
||||||
if (selectedPools.length === 0) {
|
if (selectedPools.length === 0) {
|
||||||
toast({
|
toast({
|
||||||
@@ -635,56 +614,27 @@ export default function NewDistribution() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 从formData中获取选中的设备ID
|
||||||
|
const deviceIds = formData.targetSettings?.selectedDevices || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-lg p-6">
|
<div className="bg-white rounded-lg p-6">
|
||||||
<h2 className="text-xl font-bold mb-6">流量池选择</h2>
|
<h2 className="text-xl font-bold mb-6">流量池选择</h2>
|
||||||
|
|
||||||
<div className="mb-4">
|
<div className="mb-6">
|
||||||
<div className="relative">
|
<TrafficPoolSelection
|
||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={18} />
|
selectedPools={selectedPools}
|
||||||
<Input
|
onSelect={setSelectedPools}
|
||||||
placeholder="搜索流量池"
|
deviceIds={deviceIds}
|
||||||
value={searchTerm}
|
placeholder="选择流量池"
|
||||||
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>
|
</div>
|
||||||
|
|
||||||
<div className="mt-8 flex justify-between">
|
<div className="mt-8 flex justify-between">
|
||||||
<Button variant="outline" onClick={onBack}>
|
<Button variant="outline" onClick={onBack}>
|
||||||
← 上一步
|
← 上一步
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleSubmit} disabled={isSubmitting}>
|
<Button onClick={handleSubmit} disabled={isSubmitting || selectedPools.length === 0}>
|
||||||
{isSubmitting ? "提交中..." : "完成"}
|
{isSubmitting ? "提交中..." : "完成"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user