feat: 本次提交更新内容如下
流量分发构建完完成
This commit is contained in:
@@ -65,6 +65,7 @@ function App() {
|
|||||||
<Route path="/workspace/auto-like/:id/edit" element={<NewAutoLike />} />
|
<Route path="/workspace/auto-like/:id/edit" element={<NewAutoLike />} />
|
||||||
<Route path="/workspace/traffic-distribution" element={<TrafficDistribution />} />
|
<Route path="/workspace/traffic-distribution" element={<TrafficDistribution />} />
|
||||||
<Route path="/workspace/traffic-distribution/new" element={<NewDistribution />} />
|
<Route path="/workspace/traffic-distribution/new" element={<NewDistribution />} />
|
||||||
|
<Route path="/workspace/traffic-distribution/edit/:id" element={<NewDistribution />} />
|
||||||
<Route path="/workspace/auto-group" element={<AutoGroup />} />
|
<Route path="/workspace/auto-group" element={<AutoGroup />} />
|
||||||
<Route path="/workspace/auto-group/:id" element={<AutoGroupDetail />} />
|
<Route path="/workspace/auto-group/:id" element={<AutoGroupDetail />} />
|
||||||
<Route path="/workspace/group-push" element={<GroupPush />} />
|
<Route path="/workspace/group-push" element={<GroupPush />} />
|
||||||
|
|||||||
@@ -57,31 +57,39 @@ export interface TrafficPoolListResponse {
|
|||||||
|
|
||||||
// 流量分发规则类型
|
// 流量分发规则类型
|
||||||
export interface DistributionRule {
|
export interface DistributionRule {
|
||||||
id: string;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
type: number;
|
||||||
status: number;
|
status: number;
|
||||||
deviceCount: number;
|
autoStart: number;
|
||||||
totalTraffic: number;
|
|
||||||
distributedTraffic: number;
|
|
||||||
lastDistributionTime: string;
|
|
||||||
createTime: string;
|
createTime: string;
|
||||||
creator: string;
|
updateTime: string;
|
||||||
distributionInterval: number;
|
companyId: number;
|
||||||
maxDistributionPerDay: number;
|
|
||||||
timeRange: { start: string; end: string };
|
|
||||||
targetChannels?: string[];
|
|
||||||
distributionRatio?: Record<string, number>;
|
|
||||||
priority?: 'high' | 'medium' | 'low';
|
|
||||||
filterConditions?: string[];
|
|
||||||
config?: {
|
config?: {
|
||||||
total?: {
|
id: number;
|
||||||
|
workbenchId: number;
|
||||||
|
distributeType: number; // 1-均分配, 2-优先级分配, 3-比例分配
|
||||||
|
maxPerDay: number; // 每日最大分配量
|
||||||
|
timeType: number; // 1-全天, 2-自定义时间段
|
||||||
|
startTime: string; // 开始时间
|
||||||
|
endTime: string; // 结束时间
|
||||||
|
account: string[]; // 账号列表
|
||||||
|
devices: string[]; // 设备列表
|
||||||
|
pools: string[]; // 流量池列表
|
||||||
|
createTime: string;
|
||||||
|
updateTime: string;
|
||||||
|
lastUpdated: string;
|
||||||
|
total: {
|
||||||
dailyAverage: number; // 日均分发量
|
dailyAverage: number; // 日均分发量
|
||||||
deviceCount: number; // 分发设备
|
totalAccounts: number; // 分发账户总数
|
||||||
poolCount: string; // 流量池
|
deviceCount: number; // 分发设备数量
|
||||||
totalAccounts: number; // 分发账户
|
poolCount: number; // 流量池数量
|
||||||
totalUsers: number; // 总用户数
|
totalUsers: number; // 总用户数
|
||||||
}
|
};
|
||||||
};
|
};
|
||||||
|
auto_like?: any;
|
||||||
|
moments_sync?: any;
|
||||||
|
group_push?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 流量分发列表响应类型
|
// 流量分发列表响应类型
|
||||||
@@ -170,7 +178,7 @@ export const fetchDistributionRules = async (params: {
|
|||||||
* @returns 流量分发规则详情
|
* @returns 流量分发规则详情
|
||||||
*/
|
*/
|
||||||
export const fetchDistributionRuleDetail = async (id: string): Promise<ApiResponse<DistributionRule>> => {
|
export const fetchDistributionRuleDetail = async (id: string): Promise<ApiResponse<DistributionRule>> => {
|
||||||
return get<ApiResponse<DistributionRule>>(`/v1/workbench/detail/${id}`);
|
return get<ApiResponse<DistributionRule>>(`/v1/workbench/detail?id=${id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -191,8 +199,9 @@ export const createDistributionRule = async (params: any): Promise<ApiResponse<{
|
|||||||
* @param params 更新参数
|
* @param params 更新参数
|
||||||
* @returns 更新结果
|
* @returns 更新结果
|
||||||
*/
|
*/
|
||||||
export const updateDistributionRule = async (id: string, params: any): Promise<ApiResponse<any>> => {
|
export const updateDistributionRule = async (id : string, params: any): Promise<ApiResponse<any>> => {
|
||||||
return put<ApiResponse<any>>(`/v1/workbench/update/${id}`, {
|
return post<ApiResponse<any>>(`/v1/workbench/update`, {
|
||||||
|
id: id,
|
||||||
...params,
|
...params,
|
||||||
type: WorkbenchTaskType.TRAFFIC_DISTRIBUTION
|
type: WorkbenchTaskType.TRAFFIC_DISTRIBUTION
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { Steps, StepItem } from 'tdesign-mobile-react';
|
import { Steps, StepItem } from 'tdesign-mobile-react';
|
||||||
import {
|
import {
|
||||||
Users,
|
Users,
|
||||||
@@ -16,7 +16,7 @@ import Layout from '@/components/Layout';
|
|||||||
import PageHeader from '@/components/PageHeader';
|
import PageHeader from '@/components/PageHeader';
|
||||||
import DeviceSelection from '@/components/DeviceSelection';
|
import DeviceSelection from '@/components/DeviceSelection';
|
||||||
import { useToast } from '@/components/ui/toast';
|
import { useToast } from '@/components/ui/toast';
|
||||||
import { fetchAccountList, Account, createDistributionRule } from '@/api/trafficDistribution';
|
import { fetchAccountList, Account, createDistributionRule, updateDistributionRule, fetchDistributionRuleDetail } from '@/api/trafficDistribution';
|
||||||
import '@/components/Layout.css';
|
import '@/components/Layout.css';
|
||||||
import TrafficPoolSelection from '@/components/TrafficPoolSelection';
|
import TrafficPoolSelection from '@/components/TrafficPoolSelection';
|
||||||
|
|
||||||
@@ -248,7 +248,7 @@ const AccountSelectionDialog = ({
|
|||||||
>
|
>
|
||||||
确认
|
确认
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
@@ -256,8 +256,11 @@ const AccountSelectionDialog = ({
|
|||||||
|
|
||||||
export default function NewDistribution() {
|
export default function NewDistribution() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { id: ruleId } = useParams<{ id: string }>();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [currentStep, setCurrentStep] = useState(0);
|
const [currentStep, setCurrentStep] = useState(0);
|
||||||
|
const [isEditMode, setIsEditMode] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
const [formData, setFormData] = useState<FormData>({
|
const [formData, setFormData] = useState<FormData>({
|
||||||
basicInfo: {},
|
basicInfo: {},
|
||||||
targetSettings: {},
|
targetSettings: {},
|
||||||
@@ -270,6 +273,100 @@ export default function NewDistribution() {
|
|||||||
{ title: "流量池选择", content: "step3" },
|
{ title: "流量池选择", content: "step3" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 加载规则数据
|
||||||
|
const loadRuleData = useCallback(async (id: string) => {
|
||||||
|
console.log('开始加载规则数据,ID:', id);
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
console.log('调用API: /v1/workbench/detail?id=' + id);
|
||||||
|
const response = await fetchDistributionRuleDetail(id);
|
||||||
|
|
||||||
|
if (response.code === 200 && response.data) {
|
||||||
|
const rule = response.data;
|
||||||
|
const config = rule.config as any || {};
|
||||||
|
|
||||||
|
console.log('接收到的详情数据:', rule);
|
||||||
|
console.log('config数据:', config);
|
||||||
|
|
||||||
|
// 转换分配类型:1-均分配, 2-优先级分配, 3-比例分配
|
||||||
|
const getDistributionMethod = (distributeType: number) => {
|
||||||
|
switch (distributeType) {
|
||||||
|
case 2: return 'priority';
|
||||||
|
case 3: return 'ratio';
|
||||||
|
default: return 'equal';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 转换时间限制类型:1-全天, 2-自定义时间段
|
||||||
|
const timeRestriction = config?.timeType === 1 ? 'allDay' : 'custom';
|
||||||
|
|
||||||
|
// 转换后端数据为前端表单格式
|
||||||
|
setFormData({
|
||||||
|
basicInfo: {
|
||||||
|
name: rule.name || '',
|
||||||
|
distributionMethod: getDistributionMethod(config?.distributeType),
|
||||||
|
dailyLimit: config?.maxPerDay || 50,
|
||||||
|
timeRestriction: timeRestriction,
|
||||||
|
startTime: config?.startTime || '09:00',
|
||||||
|
endTime: config?.endTime || '18:00',
|
||||||
|
selectedAccounts: config?.account || [],
|
||||||
|
},
|
||||||
|
targetSettings: {
|
||||||
|
selectedDevices: config?.devices || [],
|
||||||
|
},
|
||||||
|
trafficPool: {
|
||||||
|
selectedPools: config?.pools || [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('转换后的表单数据:', {
|
||||||
|
basicInfo: {
|
||||||
|
name: rule.name || '',
|
||||||
|
distributionMethod: getDistributionMethod(config?.distributeType),
|
||||||
|
dailyLimit: config?.maxPerDay || 50,
|
||||||
|
timeRestriction: timeRestriction,
|
||||||
|
startTime: config?.startTime || '09:00',
|
||||||
|
endTime: config?.endTime || '18:00',
|
||||||
|
selectedAccounts: config?.account || [],
|
||||||
|
},
|
||||||
|
targetSettings: {
|
||||||
|
selectedDevices: config?.devices || [],
|
||||||
|
},
|
||||||
|
trafficPool: {
|
||||||
|
selectedPools: config?.pools || [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
title: "加载失败",
|
||||||
|
description: response.msg || "无法加载规则数据",
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
navigate('/workspace/traffic-distribution');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载规则数据失败:', error);
|
||||||
|
toast({
|
||||||
|
title: "加载失败",
|
||||||
|
description: "网络错误,请稍后重试",
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
navigate('/workspace/traffic-distribution');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [toast, navigate]);
|
||||||
|
|
||||||
|
// 检查是否为编辑模式并加载数据
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('检查编辑模式 - ruleId:', ruleId);
|
||||||
|
if (ruleId) {
|
||||||
|
console.log('进入编辑模式,加载数据...');
|
||||||
|
setIsEditMode(true);
|
||||||
|
loadRuleData(ruleId);
|
||||||
|
}
|
||||||
|
}, [ruleId, loadRuleData]);
|
||||||
|
|
||||||
// 生成默认计划名称
|
// 生成默认计划名称
|
||||||
const generateDefaultName = () => {
|
const generateDefaultName = () => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -341,18 +438,25 @@ export default function NewDistribution() {
|
|||||||
|
|
||||||
console.log('提交的数据:', apiParams);
|
console.log('提交的数据:', apiParams);
|
||||||
|
|
||||||
const response = await createDistributionRule(apiParams);
|
let response;
|
||||||
|
if (isEditMode && ruleId) {
|
||||||
|
// 编辑模式 - 使用更新接口
|
||||||
|
response = await updateDistributionRule(ruleId, apiParams);
|
||||||
|
} else {
|
||||||
|
// 创建模式 - 使用创建接口
|
||||||
|
response = await createDistributionRule(apiParams);
|
||||||
|
}
|
||||||
|
|
||||||
if (response.code === 200) {
|
if (response.code === 200) {
|
||||||
toast({
|
toast({
|
||||||
title: "创建成功",
|
title: isEditMode ? "更新成功" : "创建成功",
|
||||||
description: "流量分发规则已成功创建"
|
description: isEditMode ? "流量分发规则已成功更新" : "流量分发规则已成功创建"
|
||||||
});
|
});
|
||||||
|
|
||||||
navigate('/workspace/traffic-distribution');
|
navigate('/workspace/traffic-distribution');
|
||||||
} else {
|
} else {
|
||||||
toast({
|
toast({
|
||||||
title: "创建失败",
|
title: isEditMode ? "更新失败" : "创建失败",
|
||||||
description: response.msg || "请稍后重试",
|
description: response.msg || "请稍后重试",
|
||||||
variant: "destructive"
|
variant: "destructive"
|
||||||
});
|
});
|
||||||
@@ -360,27 +464,43 @@ export default function NewDistribution() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('提交失败:', error);
|
console.error('提交失败:', error);
|
||||||
toast({
|
toast({
|
||||||
title: "创建失败",
|
title: isEditMode ? "更新失败" : "创建失败",
|
||||||
description: "网络错误,请稍后重试",
|
description: "网络错误,请稍后重试",
|
||||||
variant: "destructive"
|
variant: "destructive"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 基本信息步骤组件
|
// 基本信息步骤组件
|
||||||
const BasicInfoStep = ({ onNext, initialData = {} }: { onNext: (data: BasicInfoData) => void; initialData?: Partial<BasicInfoData> }) => {
|
const BasicInfoStep = ({ onNext, initialData = {} }: { onNext: (data: BasicInfoData) => void; initialData?: Partial<BasicInfoData> }) => {
|
||||||
const [formData, setFormData] = useState<BasicInfoData>({
|
const [formData, setFormData] = useState<BasicInfoData>({
|
||||||
name: initialData.name || generateDefaultName(),
|
name: initialData.name || (isEditMode ? '' : generateDefaultName()),
|
||||||
distributionMethod: initialData.distributionMethod || "equal",
|
distributionMethod: initialData.distributionMethod || "equal",
|
||||||
dailyLimit: initialData.dailyLimit || 50,
|
dailyLimit: initialData.dailyLimit || 50,
|
||||||
timeRestriction: initialData.timeRestriction || "custom",
|
timeRestriction: initialData.timeRestriction || "custom",
|
||||||
startTime: initialData.startTime || "09:00",
|
startTime: initialData.startTime || "09:00",
|
||||||
endTime: initialData.endTime || "18:00",
|
endTime: initialData.endTime || "18:00",
|
||||||
selectedAccounts: initialData.selectedAccounts || [],
|
selectedAccounts: initialData.selectedAccounts || [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const [accountDialogOpen, setAccountDialogOpen] = useState(false);
|
const [accountDialogOpen, setAccountDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
// 当编辑模式下initialData变化时,更新表单数据
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialData.name) {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
name: initialData.name || '',
|
||||||
|
distributionMethod: initialData.distributionMethod || prev.distributionMethod,
|
||||||
|
dailyLimit: initialData.dailyLimit || prev.dailyLimit,
|
||||||
|
timeRestriction: initialData.timeRestriction || prev.timeRestriction,
|
||||||
|
startTime: initialData.startTime || prev.startTime,
|
||||||
|
endTime: initialData.endTime || prev.endTime,
|
||||||
|
selectedAccounts: initialData.selectedAccounts || prev.selectedAccounts,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}, [initialData]);
|
||||||
|
|
||||||
const handleChange = (field: keyof BasicInfoData, value: string | number | string[]) => {
|
const handleChange = (field: keyof BasicInfoData, value: string | number | string[]) => {
|
||||||
setFormData(prev => ({ ...prev, [field]: value }));
|
setFormData(prev => ({ ...prev, [field]: value }));
|
||||||
};
|
};
|
||||||
@@ -394,9 +514,9 @@ export default function NewDistribution() {
|
|||||||
return "请选择账号";
|
return "请选择账号";
|
||||||
}
|
}
|
||||||
return `已选择 ${formData.selectedAccounts.length} 个账号`;
|
return `已选择 ${formData.selectedAccounts.length} 个账号`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
if (!formData.name.trim()) {
|
if (!formData.name.trim()) {
|
||||||
toast({
|
toast({
|
||||||
title: "请填写计划名称",
|
title: "请填写计划名称",
|
||||||
@@ -411,26 +531,26 @@ export default function NewDistribution() {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onNext(formData);
|
onNext(formData);
|
||||||
};
|
};
|
||||||
|
|
||||||
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="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="name" className="flex items-center">
|
<Label htmlFor="name" className="flex items-center">
|
||||||
计划名称 <span className="text-red-500 ml-1">*</span>
|
计划名称 <span className="text-red-500 ml-1">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="name"
|
id="name"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(e) => handleChange("name", e.target.value)}
|
onChange={(e) => handleChange("name", e.target.value)}
|
||||||
placeholder="请输入计划名称"
|
placeholder="请输入计划名称"
|
||||||
className="h-12"
|
className="h-12"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Label className="flex items-center">
|
<Label className="flex items-center">
|
||||||
@@ -454,86 +574,86 @@ export default function NewDistribution() {
|
|||||||
<Users className="h-4 w-4 mr-1" />
|
<Users className="h-4 w-4 mr-1" />
|
||||||
已选账号:<span className="text-blue-600 font-medium ml-1">{formData.selectedAccounts.length} 个</span>
|
已选账号:<span className="text-blue-600 font-medium ml-1">{formData.selectedAccounts.length} 个</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>分配方式</Label>
|
||||||
|
<RadioGroup
|
||||||
|
value={formData.distributionMethod}
|
||||||
|
onValueChange={(value) => handleChange("distributionMethod", value as 'equal' | 'priority' | 'ratio')}
|
||||||
|
className="space-y-2"
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroupItem value="equal" id="equal" />
|
||||||
|
<Label htmlFor="equal" className="cursor-pointer">
|
||||||
|
均分配 <span className="text-gray-500 text-sm">(流量将均分配给所有客服)</span>
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroupItem value="priority" id="priority" />
|
||||||
|
<Label htmlFor="priority" className="cursor-pointer">
|
||||||
|
优先级分配 <span className="text-gray-500 text-sm">(按客服优先级顺序分配)</span>
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroupItem value="ratio" id="ratio" />
|
||||||
|
<Label htmlFor="ratio" className="cursor-pointer">
|
||||||
|
比例分配 <span className="text-gray-500 text-sm">(按设定比例分配流量)</span>
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Label>分配限制</Label>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>分配方式</Label>
|
<div className="flex justify-between items-center">
|
||||||
<RadioGroup
|
<span>每日最大分配量</span>
|
||||||
value={formData.distributionMethod}
|
<span className="font-medium">{formData.dailyLimit} 人/天</span>
|
||||||
onValueChange={(value) => handleChange("distributionMethod", value as 'equal' | 'priority' | 'ratio')}
|
</div>
|
||||||
className="space-y-2"
|
<input
|
||||||
>
|
type="range"
|
||||||
<div className="flex items-center space-x-2">
|
value={formData.dailyLimit}
|
||||||
<RadioGroupItem value="equal" id="equal" />
|
min={1}
|
||||||
<Label htmlFor="equal" className="cursor-pointer">
|
max={200}
|
||||||
均分配 <span className="text-gray-500 text-sm">(流量将均分配给所有客服)</span>
|
step={1}
|
||||||
</Label>
|
onChange={(e) => handleChange("dailyLimit", parseInt(e.target.value))}
|
||||||
</div>
|
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<RadioGroupItem value="priority" id="priority" />
|
|
||||||
<Label htmlFor="priority" className="cursor-pointer">
|
|
||||||
优先级分配 <span className="text-gray-500 text-sm">(按客服优先级顺序分配)</span>
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<RadioGroupItem value="ratio" id="ratio" />
|
|
||||||
<Label htmlFor="ratio" className="cursor-pointer">
|
|
||||||
比例分配 <span className="text-gray-500 text-sm">(按设定比例分配流量)</span>
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
</RadioGroup>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<Label>分配限制</Label>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<span>每日最大分配量</span>
|
|
||||||
<span className="font-medium">{formData.dailyLimit} 人/天</span>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
value={formData.dailyLimit}
|
|
||||||
min={1}
|
|
||||||
max={200}
|
|
||||||
step={1}
|
|
||||||
onChange={(e) => handleChange("dailyLimit", parseInt(e.target.value))}
|
|
||||||
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"
|
|
||||||
style={{
|
style={{
|
||||||
background: `linear-gradient(to right, #3b82f6 0%, #3b82f6 ${(formData.dailyLimit / 200) * 100}%, #e5e7eb ${(formData.dailyLimit / 200) * 100}%, #e5e7eb 100%)`
|
background: `linear-gradient(to right, #3b82f6 0%, #3b82f6 ${(formData.dailyLimit / 200) * 100}%, #e5e7eb ${(formData.dailyLimit / 200) * 100}%, #e5e7eb 100%)`
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<p className="text-sm text-gray-500">限制每天最多分配的流量数量</p>
|
<p className="text-sm text-gray-500">限制每天最多分配的流量数量</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4 pt-4">
|
<div className="space-y-4 pt-4">
|
||||||
<Label>时间限制</Label>
|
<Label>时间限制</Label>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
value={formData.timeRestriction}
|
value={formData.timeRestriction}
|
||||||
onValueChange={(value) => handleChange("timeRestriction", value as 'allDay' | 'custom')}
|
onValueChange={(value) => handleChange("timeRestriction", value as 'allDay' | 'custom')}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<RadioGroupItem value="allDay" id="allDay" />
|
<RadioGroupItem value="allDay" id="allDay" />
|
||||||
<Label htmlFor="allDay" className="cursor-pointer">
|
<Label htmlFor="allDay" className="cursor-pointer">
|
||||||
全天分配
|
全天分配
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<RadioGroupItem value="custom" id="custom" />
|
<RadioGroupItem value="custom" id="custom" />
|
||||||
<Label htmlFor="custom" className="cursor-pointer">
|
<Label htmlFor="custom" className="cursor-pointer">
|
||||||
自定义时间段
|
自定义时间段
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
|
|
||||||
{formData.timeRestriction === "custom" && (
|
{formData.timeRestriction === "custom" && (
|
||||||
<div className="grid grid-cols-2 gap-4 pt-2">
|
<div className="grid grid-cols-2 gap-4 pt-2">
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="startTime" className="mb-2 block">
|
<Label htmlFor="startTime" className="mb-2 block">
|
||||||
开始时间
|
开始时间
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="startTime"
|
id="startTime"
|
||||||
type="time"
|
type="time"
|
||||||
@@ -541,11 +661,11 @@ export default function NewDistribution() {
|
|||||||
onChange={(e) => handleChange("startTime", e.target.value)}
|
onChange={(e) => handleChange("startTime", e.target.value)}
|
||||||
className="h-12"
|
className="h-12"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="endTime" className="mb-2 block">
|
<Label htmlFor="endTime" className="mb-2 block">
|
||||||
结束时间
|
结束时间
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="endTime"
|
id="endTime"
|
||||||
type="time"
|
type="time"
|
||||||
@@ -553,18 +673,18 @@ export default function NewDistribution() {
|
|||||||
onChange={(e) => handleChange("endTime", e.target.value)}
|
onChange={(e) => handleChange("endTime", e.target.value)}
|
||||||
className="h-12"
|
className="h-12"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mt-8 flex justify-end">
|
<div className="mt-8 flex justify-end">
|
||||||
<Button onClick={handleSubmit} className="px-8">
|
<Button onClick={handleSubmit} className="px-8">
|
||||||
下一步 →
|
下一步 →
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AccountSelectionDialog
|
<AccountSelectionDialog
|
||||||
open={accountDialogOpen}
|
open={accountDialogOpen}
|
||||||
@@ -572,7 +692,7 @@ export default function NewDistribution() {
|
|||||||
selectedAccounts={formData.selectedAccounts}
|
selectedAccounts={formData.selectedAccounts}
|
||||||
onConfirm={handleAccountConfirm}
|
onConfirm={handleAccountConfirm}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -580,7 +700,7 @@ export default function NewDistribution() {
|
|||||||
const TargetSettingsStep = ({ onNext, onBack, initialData = {} }: { onNext: (data: TargetSettingsData) => void; onBack: () => void; initialData?: Partial<TargetSettingsData> }) => {
|
const TargetSettingsStep = ({ onNext, onBack, initialData = {} }: { onNext: (data: TargetSettingsData) => void; onBack: () => void; initialData?: Partial<TargetSettingsData> }) => {
|
||||||
const [selectedDevices, setSelectedDevices] = useState<string[]>(initialData.selectedDevices || []);
|
const [selectedDevices, setSelectedDevices] = useState<string[]>(initialData.selectedDevices || []);
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
if (selectedDevices.length === 0) {
|
if (selectedDevices.length === 0) {
|
||||||
toast({
|
toast({
|
||||||
title: "请选择至少一个设备",
|
title: "请选择至少一个设备",
|
||||||
@@ -589,14 +709,14 @@ export default function NewDistribution() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
onNext({
|
onNext({
|
||||||
selectedDevices,
|
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-6">
|
<div className="mb-6">
|
||||||
<DeviceSelection
|
<DeviceSelection
|
||||||
@@ -606,47 +726,47 @@ export default function NewDistribution() {
|
|||||||
/>
|
/>
|
||||||
</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={selectedDevices.length === 0}>
|
<Button onClick={handleSubmit} disabled={selectedDevices.length === 0}>
|
||||||
下一步 →
|
下一步 →
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 流量池选择步骤组件
|
// 流量池选择步骤组件
|
||||||
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 [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
onSubmit({ selectedPools });
|
onSubmit({ selectedPools });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("提交失败:", error);
|
console.error("提交失败:", error);
|
||||||
toast({
|
toast({
|
||||||
title: "创建失败",
|
title: "创建失败",
|
||||||
description: "请稍后重试",
|
description: "请稍后重试",
|
||||||
variant: "destructive"
|
variant: "destructive"
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 从formData中获取选中的设备ID
|
// 从formData中获取选中的设备ID
|
||||||
const deviceIds = formData.targetSettings?.selectedDevices || [];
|
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-6">
|
<div className="mb-6">
|
||||||
<TrafficPoolSelection
|
<TrafficPoolSelection
|
||||||
@@ -655,18 +775,18 @@ export default function NewDistribution() {
|
|||||||
deviceIds={deviceIds}
|
deviceIds={deviceIds}
|
||||||
placeholder="选择流量池"
|
placeholder="选择流量池"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-8 flex justify-between">
|
|
||||||
<Button variant="outline" onClick={onBack}>
|
|
||||||
← 上一步
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleSubmit} disabled={isSubmitting}>
|
|
||||||
{isSubmitting ? "提交中..." : "完成"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
|
||||||
|
<div className="mt-8 flex justify-between">
|
||||||
|
<Button variant="outline" onClick={onBack}>
|
||||||
|
← 上一步
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSubmit} disabled={isSubmitting}>
|
||||||
|
{isSubmitting ? (isEditMode ? "更新中..." : "提交中...") : (isEditMode ? "更新完成" : "完成")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const headerRightContent = (
|
const headerRightContent = (
|
||||||
@@ -684,21 +804,29 @@ export default function NewDistribution() {
|
|||||||
<Layout
|
<Layout
|
||||||
header={
|
header={
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="新建流量分发"
|
title={isEditMode ? "编辑流量分发" : "新建流量分发"}
|
||||||
defaultBackPath="/workspace/traffic-distribution"
|
defaultBackPath="/workspace/traffic-distribution"
|
||||||
rightContent={headerRightContent}
|
rightContent={headerRightContent}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="bg-gray-50 min-h-screen pb-16">
|
<div className="bg-gray-50 min-h-screen pb-16">
|
||||||
<div className="p-4">
|
{loading ? (
|
||||||
<div className="mb-6">
|
<div className="flex items-center justify-center h-64">
|
||||||
<Steps current={currentStep}>
|
<div className="text-center">
|
||||||
{steps.map((step, index) => (
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-2"></div>
|
||||||
<StepItem key={index} title={step.title} />
|
<div className="text-gray-500">加载中...</div>
|
||||||
))}
|
</div>
|
||||||
</Steps>
|
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="mb-6">
|
||||||
|
<Steps current={currentStep}>
|
||||||
|
{steps.map((step, index) => (
|
||||||
|
<StepItem key={index} title={step.title} />
|
||||||
|
))}
|
||||||
|
</Steps>
|
||||||
|
</div>
|
||||||
|
|
||||||
{currentStep === 0 && (
|
{currentStep === 0 && (
|
||||||
<BasicInfoStep
|
<BasicInfoStep
|
||||||
@@ -707,22 +835,23 @@ export default function NewDistribution() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{currentStep === 1 && (
|
{currentStep === 1 && (
|
||||||
<TargetSettingsStep
|
<TargetSettingsStep
|
||||||
onNext={handleTargetSettingsNext}
|
onNext={handleTargetSettingsNext}
|
||||||
onBack={handleTargetSettingsBack}
|
onBack={handleTargetSettingsBack}
|
||||||
initialData={formData.targetSettings}
|
initialData={formData.targetSettings}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{currentStep === 2 && (
|
{currentStep === 2 && (
|
||||||
<TrafficPoolStep
|
<TrafficPoolStep
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
onBack={handleTrafficPoolBack}
|
onBack={handleTrafficPoolBack}
|
||||||
initialData={formData.trafficPool}
|
initialData={formData.trafficPool}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
Trash2,
|
Trash2,
|
||||||
Pause,
|
Pause,
|
||||||
Play,
|
Play,
|
||||||
Users,
|
|
||||||
Filter,
|
Filter,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Card } from '@/components/ui/card';
|
import { Card } from '@/components/ui/card';
|
||||||
@@ -41,7 +40,7 @@ export default function TrafficDistribution() {
|
|||||||
const [tasks, setTasks] = useState<DistributionRule[]>([]);
|
const [tasks, setTasks] = useState<DistributionRule[]>([]);
|
||||||
|
|
||||||
// 处理删除
|
// 处理删除
|
||||||
const handleDelete = async (ruleId: string) => {
|
const handleDelete = async (ruleId: number) => {
|
||||||
const ruleToDelete = tasks.find((rule) => rule.id === ruleId);
|
const ruleToDelete = tasks.find((rule) => rule.id === ruleId);
|
||||||
if (!ruleToDelete) return;
|
if (!ruleToDelete) return;
|
||||||
|
|
||||||
@@ -49,7 +48,7 @@ export default function TrafficDistribution() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const response = await deleteDistributionRule(ruleId);
|
const response = await deleteDistributionRule(ruleId.toString());
|
||||||
|
|
||||||
if (response.code === 200) {
|
if (response.code === 200) {
|
||||||
setTasks(tasks.filter((rule) => rule.id !== ruleId));
|
setTasks(tasks.filter((rule) => rule.id !== ruleId));
|
||||||
@@ -76,15 +75,15 @@ export default function TrafficDistribution() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEdit = (ruleId: string) => {
|
const handleEdit = (ruleId: number) => {
|
||||||
navigate(`/workspace/traffic-distribution/${ruleId}/edit`);
|
navigate(`/workspace/traffic-distribution/edit/${ruleId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleView = (ruleId: string) => {
|
const handleView = (ruleId: number) => {
|
||||||
navigate(`/workspace/traffic-distribution/${ruleId}`);
|
navigate(`/workspace/traffic-distribution/${ruleId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleRuleStatus = async (ruleId: string) => {
|
const toggleRuleStatus = async (ruleId: number) => {
|
||||||
const rule = tasks.find((r) => r.id === ruleId);
|
const rule = tasks.find((r) => r.id === ruleId);
|
||||||
if (!rule) return;
|
if (!rule) return;
|
||||||
|
|
||||||
@@ -93,7 +92,7 @@ export default function TrafficDistribution() {
|
|||||||
// 根据当前状态决定新状态:1表示开启,0表示关闭
|
// 根据当前状态决定新状态:1表示开启,0表示关闭
|
||||||
const newStatus = rule.status === WorkbenchTaskStatus.RUNNING ? 0 : 1;
|
const newStatus = rule.status === WorkbenchTaskStatus.RUNNING ? 0 : 1;
|
||||||
|
|
||||||
const response = await toggleDistributionRuleStatus(ruleId, newStatus as 0 | 1);
|
const response = await toggleDistributionRuleStatus(ruleId.toString(), newStatus as 0 | 1);
|
||||||
|
|
||||||
if (response.code === 200) {
|
if (response.code === 200) {
|
||||||
// 更新本地状态:1对应RUNNING,0对应PAUSED
|
// 更新本地状态:1对应RUNNING,0对应PAUSED
|
||||||
@@ -363,9 +362,9 @@ export default function TrafficDistribution() {
|
|||||||
<div className="mt-4 text-sm text-gray-500 flex items-center justify-between">
|
<div className="mt-4 text-sm text-gray-500 flex items-center justify-between">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Clock className="h-4 w-4 mr-1" />
|
<Clock className="h-4 w-4 mr-1" />
|
||||||
上次执行: {rule.lastDistributionTime?.substring(0, 16) || '2025-07-02 09:00'}
|
上次执行: {rule.config?.lastUpdated?.substring(0, 16) || '2025-07-02 09:00'}
|
||||||
</div>
|
</div>
|
||||||
<div>创建人: {rule.creator || '售前'}</div>
|
<div>创建时间: {rule.createTime || '2025-07-02'}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
Reference in New Issue
Block a user