重构流量池表单组件:添加预设方案选择功能,优化用户数据生成逻辑,增强提交状态管理,提升用户体验和代码可维护性。
This commit is contained in:
@@ -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");
|
||||
}
|
||||
|
||||
// 获取行业选项(固定筛选项)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user