重构流量池表单组件:添加预设方案选择功能,优化用户数据生成逻辑,增强提交状态管理,提升用户体验和代码可维护性。

This commit is contained in:
超级老白兔
2025-10-20 15:04:41 +08:00
parent a775596719
commit 4df670d1c9
5 changed files with 266 additions and 140 deletions

View File

@@ -61,7 +61,62 @@ export interface PresetScheme {
}
export async function getPresetSchemes(): Promise<PresetScheme[]> {
return request("/v1/traffic/pool/schemes", {}, "GET");
// 模拟数据
return new Promise(resolve => {
setTimeout(() => {
resolve([
{
id: "scheme_1",
name: "高价值客户方案",
description: "针对高消费、高活跃度的客户群体",
conditions: [
{ id: "rfm_high", type: "rfm", label: "RFM评分", value: "high" },
{
id: "consumption_high",
type: "consumption",
label: "消费能力",
value: "high",
},
],
userCount: 1250,
color: "#ff4d4f",
},
{
id: "scheme_2",
name: "新用户激活方案",
description: "针对新注册用户的激活策略",
conditions: [
{ id: "new_user", type: "tag", label: "新用户", value: true },
{
id: "low_activity",
type: "activity",
label: "活跃度",
value: "low",
},
],
userCount: 890,
color: "#52c41a",
},
{
id: "scheme_3",
name: "流失挽回方案",
description: "针对流失风险用户的挽回策略",
conditions: [
{ id: "churn_risk", type: "tag", label: "流失风险", value: true },
{
id: "last_active",
type: "time",
label: "最后活跃",
value: "30天前",
},
],
userCount: 567,
color: "#faad14",
},
]);
}, 500);
});
// return request("/v1/traffic/pool/schemes", {}, "GET");
}
// 获取行业选项(固定筛选项)

View File

@@ -20,13 +20,21 @@
color: #333;
}
.schemeBtn {
.schemeRow {
display: flex;
gap: 12px;
align-items: center;
}
.addSchemeBtn {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
padding: 4px 8px;
height: 28px;
height: 32px;
white-space: nowrap;
flex-shrink: 0;
}
.section {

View File

@@ -1,12 +1,16 @@
import React, { useEffect, useState } from "react";
import { Card, Button } from "antd-mobile";
import { Select } from "antd";
import { EditSOutline } from "antd-mobile-icons";
import { PlusOutlined } from "@ant-design/icons";
import CustomConditionModal from "./CustomConditionModal";
import SchemeRecommendation from "./SchemeRecommendation";
import ConditionList from "./ConditionList";
import styles from "./AudienceFilter.module.scss";
import { getIndustryOptions, IndustryOption } from "../api";
import {
getIndustryOptions,
getPresetSchemes,
IndustryOption,
PresetScheme,
} from "../api";
interface FilterCondition {
id: string;
@@ -19,26 +23,31 @@ interface FilterCondition {
interface AudienceFilterProps {
conditions: FilterCondition[];
onChange: (conditions: FilterCondition[]) => void;
onGenerate: (users: any[]) => void;
}
const AudienceFilter: React.FC<AudienceFilterProps> = ({
conditions,
onChange,
onGenerate,
}) => {
const [showCustomModal, setShowCustomModal] = useState(false);
const [showSchemeModal, setShowSchemeModal] = useState(false);
const [industryOptions, setIndustryOptions] = useState<IndustryOption[]>([]);
const [presetSchemes, setPresetSchemes] = useState<PresetScheme[]>([]);
const [selectedIndustry, setSelectedIndustry] = useState<
string | number | undefined
>(undefined);
const [selectedScheme, setSelectedScheme] = useState<string | undefined>(
undefined,
);
// 加载行业选项(固定筛选项)
// 加载行业选项和方案列表
useEffect(() => {
getIndustryOptions()
.then(res => setIndustryOptions(res || []))
.catch(() => setIndustryOptions([]));
getPresetSchemes()
.then(res => setPresetSchemes(res || []))
.catch(() => setPresetSchemes([]));
}, []);
const handleAddCondition = (condition: FilterCondition) => {
@@ -58,15 +67,23 @@ const AudienceFilter: React.FC<AudienceFilterProps> = ({
onChange(newConditions);
};
const handleApplyScheme = (schemeConditions: FilterCondition[]) => {
onChange(schemeConditions);
setShowSchemeModal(false);
const handleSchemeChange = (schemeId: string) => {
setSelectedScheme(schemeId);
if (schemeId) {
// 找到选中的方案并应用其条件
const scheme = presetSchemes.find(s => s.id === schemeId);
if (scheme) {
onChange(scheme.conditions);
}
} else {
// 清空方案选择时,清空条件
onChange([]);
}
};
const handleGenerate = () => {
// 模拟生成用户数据
const mockUsers = generateMockUsers(conditions);
onGenerate(mockUsers);
const handleAddScheme = () => {
// 这里可以打开添加方案的弹窗或跳转到方案管理页面
console.log("添加新方案");
};
return (
@@ -74,99 +91,96 @@ const AudienceFilter: React.FC<AudienceFilterProps> = ({
<Card className={styles.card}>
<div className={styles.header}>
<div className={styles.title}></div>
<Button
size="small"
fill="outline"
onClick={() => setShowSchemeModal(true)}
className={styles.schemeBtn}
>
<EditSOutline />
</Button>
</div>
{/* 行业筛选(固定项,接口获取选项) */}
{/* 方案推荐选择 */}
<div className={styles.section}>
<div className={styles.sectionTitle}></div>
<Select
style={{ width: "100%" }}
placeholder="选择行业"
value={selectedIndustry}
onChange={value => setSelectedIndustry(value)}
options={industryOptions.map(opt => ({
label: opt.label,
value: opt.value,
}))}
allowClear
/>
</div>
{/* 标签筛选 */}
<div className={styles.section}>
<div className={styles.sectionTitle}></div>
<div className={styles.tagGrid}>
{[
{ name: "高价值用户", color: "#1677ff" },
{ name: "新用户", color: "#52c41a" },
{ name: "活跃用户", color: "#faad14" },
{ name: "流失风险", color: "#eb2f96" },
{ name: "复购率高", color: "#722ed1" },
{ name: "高潜力", color: "#eb2f96" },
{ name: "已沉睡", color: "#bfbfbf" },
{ name: "价格敏感", color: "#13c2c2" },
].map((tag, index) => (
<div
key={index}
className={styles.tag}
style={{ backgroundColor: tag.color }}
>
{tag.name}
</div>
))}
<div className={styles.sectionTitle}></div>
<div className={styles.schemeRow}>
<Select
style={{ flex: 1 }}
placeholder="选择预设方案"
value={selectedScheme}
onChange={handleSchemeChange}
options={presetSchemes.map(scheme => ({
label: `${scheme.name} (${scheme.userCount}人)`,
value: scheme.id,
}))}
allowClear
/>
<Button
size="small"
fill="outline"
onClick={handleAddScheme}
className={styles.addSchemeBtn}
>
<PlusOutlined />
</Button>
</div>
</div>
{/* 自定义条件列表 */}
<ConditionList
conditions={conditions}
onRemove={handleRemoveCondition}
onUpdate={handleUpdateCondition}
/>
{/* 条件筛选区域 - 当未选择方案时显示 */}
{!selectedScheme && (
<>
{/* 行业筛选(固定项,接口获取选项) */}
<div className={styles.section}>
<div className={styles.sectionTitle}></div>
<Select
style={{ width: "100%" }}
placeholder="选择行业"
value={selectedIndustry}
onChange={value => setSelectedIndustry(value)}
options={industryOptions.map(opt => ({
label: opt.label,
value: opt.value,
}))}
allowClear
/>
</div>
{/* 添加自定义条件 */}
<Button
fill="outline"
onClick={() => setShowCustomModal(true)}
className={styles.addConditionBtn}
>
+
</Button>
{/* 标签筛选 */}
<div className={styles.section}>
<div className={styles.sectionTitle}></div>
<div className={styles.tagGrid}>
{[
{ name: "高价值用户", color: "#1677ff" },
{ name: "新用户", color: "#52c41a" },
{ name: "活跃用户", color: "#faad14" },
{ name: "流失风险", color: "#eb2f96" },
{ name: "复购率高", color: "#722ed1" },
{ name: "高潜力", color: "#eb2f96" },
{ name: "已沉睡", color: "#bfbfbf" },
{ name: "价格敏感", color: "#13c2c2" },
].map((tag, index) => (
<div
key={index}
className={styles.tag}
style={{ backgroundColor: tag.color }}
>
{tag.name}
</div>
))}
</div>
</div>
{/* 生成按钮 */}
<Button
color="primary"
block
onClick={() => {
// 将行业条件合并到自定义条件,便于统一生成
const mergedConditions =
selectedIndustry !== undefined
? [
...conditions,
{
id: `industry`,
type: "select",
label: "行业",
value: selectedIndustry,
},
]
: conditions;
const mockUsers = generateMockUsers(mergedConditions as any);
onGenerate(mockUsers);
}}
className={styles.generateBtn}
>
</Button>
{/* 自定义条件列表 */}
<ConditionList
conditions={conditions}
onRemove={handleRemoveCondition}
onUpdate={handleUpdateCondition}
/>
{/* 添加自定义条件 */}
<Button
fill="outline"
onClick={() => setShowCustomModal(true)}
className={styles.addConditionBtn}
>
+
</Button>
</>
)}
</Card>
{/* 自定义条件弹窗 */}
@@ -175,35 +189,8 @@ const AudienceFilter: React.FC<AudienceFilterProps> = ({
onClose={() => setShowCustomModal(false)}
onAdd={handleAddCondition}
/>
{/* 方案推荐弹窗 */}
<SchemeRecommendation
visible={showSchemeModal}
onClose={() => setShowSchemeModal(false)}
onApply={handleApplyScheme}
/>
</div>
);
};
// 模拟生成用户数据
const generateMockUsers = (conditions: FilterCondition[]) => {
const mockUsers = [];
const userCount = Math.floor(Math.random() * 1000) + 100; // 100-1100个用户
for (let i = 1; i <= userCount; i++) {
mockUsers.push({
id: `U${String(i).padStart(8, "0")}`,
name: `用户${i}`,
avatar: `https://api.dicebear.com/7.x/avataaars/svg?seed=${i}`,
tags: ["高价值用户", "活跃用户"],
rfmScore: Math.floor(Math.random() * 15) + 1,
lastActive: "7天内",
consumption: Math.floor(Math.random() * 5000) + 100,
});
}
return mockUsers;
};
export default AudienceFilter;

View File

@@ -10,6 +10,7 @@ import StepIndicator from "@/components/StepIndicator";
const CreateTrafficPackage: React.FC = () => {
const [currentStep, setCurrentStep] = useState(1); // 1 基础信息 2 人群筛选 3 用户列表
const [submitting, setSubmitting] = useState(false); // 添加提交状态
const [formData, setFormData] = useState({
// 基本信息
name: "",
@@ -37,7 +38,6 @@ const CreateTrafficPackage: React.FC = () => {
const handleGenerateUsers = (users: any[]) => {
setFormData(prev => ({ ...prev, filteredUsers: users }));
setCurrentStep(3);
};
// 初始化模拟数据
@@ -94,13 +94,53 @@ const CreateTrafficPackage: React.FC = () => {
}
}, [currentStep, formData.filteredUsers.length]);
const handleSubmit = () => {
// 提交逻辑
console.log("提交数据:", formData);
const handleSubmit = async () => {
// 防止重复提交
if (submitting) {
return;
}
setSubmitting(true);
try {
// 提交逻辑
console.log("提交数据:", formData);
// 这里可以调用实际的 API
// await createTrafficPackage(formData);
// 模拟 API 调用
await new Promise(resolve => setTimeout(resolve, 1000));
// 提交成功后可以跳转或显示成功消息
console.log("流量包创建成功");
} catch (error) {
console.error("创建流量包失败:", error);
} finally {
setSubmitting(false);
}
};
const canSubmit = formData.name && formData.filterConditions.length > 0;
// 模拟生成用户数据
const generateMockUsers = (conditions: any[]) => {
const mockUsers = [];
const userCount = Math.floor(Math.random() * 1000) + 100; // 100-1100个用户
for (let i = 1; i <= userCount; i++) {
mockUsers.push({
id: `U${String(i).padStart(8, "0")}`,
name: `用户${i}`,
avatar: `https://api.dicebear.com/7.x/avataaars/svg?seed=${i}`,
tags: ["高价值用户", "活跃用户"],
rfmScore: Math.floor(Math.random() * 15) + 1,
lastActive: "7天内",
consumption: Math.floor(Math.random() * 5000) + 100,
});
}
return mockUsers;
};
const renderFooter = () => {
return (
<div className={styles.footer}>
@@ -109,6 +149,7 @@ const CreateTrafficPackage: React.FC = () => {
<Button
className={styles.prevButton}
onClick={() => setCurrentStep(s => Math.max(1, s - 1))}
disabled={submitting}
>
</Button>
@@ -117,7 +158,17 @@ const CreateTrafficPackage: React.FC = () => {
<Button
color="primary"
className={styles.nextButton}
onClick={() => setCurrentStep(s => Math.min(3, s + 1))}
onClick={() => {
if (currentStep === 2) {
// 在第二步时生成用户列表
const mockUsers = generateMockUsers(
formData.filterConditions,
);
handleGenerateUsers(mockUsers);
}
setCurrentStep(s => Math.min(3, s + 1));
}}
disabled={submitting}
>
</Button>
@@ -125,10 +176,11 @@ const CreateTrafficPackage: React.FC = () => {
<Button
color="primary"
className={styles.submitButton}
disabled={!canSubmit}
disabled={!canSubmit || submitting}
loading={submitting}
onClick={handleSubmit}
>
{submitting ? "创建中..." : "创建流量包"}
</Button>
)}
</div>
@@ -155,7 +207,6 @@ const CreateTrafficPackage: React.FC = () => {
<AudienceFilter
conditions={formData.filterConditions}
onChange={handleFilterChange}
onGenerate={handleGenerateUsers}
/>
)}

View File

@@ -20,6 +20,7 @@ export default function NewPlan() {
const router = useNavigate();
const [currentStep, setCurrentStep] = useState(1);
const [formData, setFormData] = useState<FormData>(defFormData);
const [submitting, setSubmitting] = useState(false); // 添加提交状态
const [sceneList, setSceneList] = useState<any[]>([]);
const [sceneLoading, setSceneLoading] = useState(true);
@@ -110,6 +111,12 @@ export default function NewPlan() {
};
// 处理保存
const handleSave = async () => {
// 防止重复提交
if (submitting) {
return;
}
setSubmitting(true);
try {
if (isEdit && planId) {
// 编辑:拼接后端需要的完整参数
@@ -140,13 +147,18 @@ export default function NewPlan() {
? "更新计划失败,请重试"
: "创建计划失败,请重试",
);
} finally {
setSubmitting(false);
}
};
// 下一步
const handleNext = () => {
if (currentStep === steps.length) {
handleSave();
// 最后一步时调用保存,防止重复点击
if (!submitting) {
handleSave();
}
} else {
setCurrentStep(prev => prev + 1);
}
@@ -186,7 +198,12 @@ export default function NewPlan() {
return (
<div style={{ padding: "16px", display: "flex", gap: "12px" }}>
{currentStep > 1 && (
<Button onClick={handlePrev} size="large" style={{ flex: 1 }}>
<Button
onClick={handlePrev}
size="large"
style={{ flex: 1 }}
disabled={submitting}
>
</Button>
)}
@@ -195,8 +212,16 @@ export default function NewPlan() {
size="large"
onClick={handleNext}
style={{ flex: 1 }}
loading={submitting}
disabled={submitting}
>
{currentStep === steps.length ? "完成" : "下一步"}
{submitting
? isEdit
? "更新中..."
: "创建中..."
: currentStep === steps.length
? "完成"
: "下一步"}
</Button>
</div>
);