443 lines
15 KiB
TypeScript
443 lines
15 KiB
TypeScript
import React, { useState } from 'react';
|
||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||
import { Check, Settings } from 'lucide-react';
|
||
import PageHeader from '@/components/PageHeader';
|
||
import { useToast } from '@/components/ui/toast';
|
||
import Layout from '@/components/Layout';
|
||
import '@/components/Layout.css';
|
||
|
||
// 步骤定义
|
||
const steps = [
|
||
{ id: 1, title: "步骤一", subtitle: "基础设置" },
|
||
{ id: 2, title: "步骤二", subtitle: "好友申请设置" },
|
||
{ id: 3, title: "步骤三", subtitle: "消息设置" },
|
||
];
|
||
|
||
interface ScenarioOption {
|
||
id: string;
|
||
name: string;
|
||
icon: string;
|
||
description: string;
|
||
image: string;
|
||
}
|
||
|
||
const scenarioOptions: ScenarioOption[] = [
|
||
{
|
||
id: "douyin",
|
||
name: "抖音获客",
|
||
icon: "🎵",
|
||
description: "通过抖音平台进行精准获客",
|
||
image: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-QR8ManuDplYTySUJsY4mymiZkDYnQ9.png",
|
||
},
|
||
{
|
||
id: "xiaohongshu",
|
||
name: "小红书获客",
|
||
icon: "📖",
|
||
description: "利用小红书平台进行内容营销获客",
|
||
image: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-yvnMxpoBUzcvEkr8DfvHgPHEo1kmQ3.png",
|
||
},
|
||
{
|
||
id: "gongzhonghao",
|
||
name: "公众号获客",
|
||
icon: "📱",
|
||
description: "通过微信公众号进行获客",
|
||
image: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-Gsg0CMf5tsZb41mioszdjqU1WmsRxW.png",
|
||
},
|
||
{
|
||
id: "haibao",
|
||
name: "海报获客",
|
||
icon: "🖼️",
|
||
description: "通过海报分享进行获客",
|
||
image: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-x92XJgXy4MI7moNYlA1EAes2FqDxMH.png",
|
||
},
|
||
];
|
||
|
||
interface FormData {
|
||
planName: string;
|
||
posters: any[];
|
||
device: any[];
|
||
remarkType: string;
|
||
greeting: string;
|
||
addInterval: number;
|
||
startTime: string;
|
||
endTime: string;
|
||
enabled: boolean;
|
||
sceneId: string;
|
||
scenario: string;
|
||
planNameEdited: boolean;
|
||
}
|
||
|
||
export default function NewPlan() {
|
||
const navigate = useNavigate();
|
||
const searchParams = useSearchParams();
|
||
const { toast } = useToast();
|
||
const [currentStep, setCurrentStep] = useState(1);
|
||
const [loading, setLoading] = useState(false);
|
||
|
||
|
||
const [formData, setFormData] = useState<FormData>({
|
||
planName: "",
|
||
posters: [],
|
||
device: [],
|
||
remarkType: "default",
|
||
greeting: "",
|
||
addInterval: 60,
|
||
startTime: "09:00",
|
||
endTime: "18:00",
|
||
enabled: true,
|
||
sceneId: searchParams[0].get("scenario") || "",
|
||
scenario: searchParams[0].get("scenario") || "",
|
||
planNameEdited: false
|
||
});
|
||
|
||
// 更新表单数据
|
||
const onChange = (data: Partial<FormData>) => {
|
||
if ('planName' in data) {
|
||
setFormData(prev => ({ ...prev, planNameEdited: true, ...data }));
|
||
} else {
|
||
setFormData(prev => ({ ...prev, ...data }));
|
||
}
|
||
};
|
||
|
||
// 处理保存
|
||
const handleSave = async () => {
|
||
try {
|
||
setLoading(true);
|
||
// 模拟API调用
|
||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||
|
||
toast({
|
||
title: "创建成功",
|
||
description: "获客计划已创建",
|
||
});
|
||
navigate("/scenarios");
|
||
} catch (error: any) {
|
||
toast({
|
||
title: "创建失败",
|
||
description: error?.message || "创建计划失败,请重试",
|
||
variant: "destructive",
|
||
});
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
// 下一步
|
||
const handleNext = () => {
|
||
if (currentStep === steps.length) {
|
||
handleSave();
|
||
} else {
|
||
setCurrentStep((prev) => prev + 1);
|
||
}
|
||
};
|
||
|
||
// 上一步
|
||
const handlePrev = () => {
|
||
setCurrentStep((prev) => Math.max(prev - 1, 1));
|
||
};
|
||
|
||
// 步骤指示器组件(方案A:简洁线性进度条风格)
|
||
const StepIndicator = ({ steps, currentStep }: { steps: any[], currentStep: number }) => {
|
||
const percent = ((currentStep - 1) / (steps.length - 1)) * 100;
|
||
return (
|
||
<div className="mb-8 px-2 pt-2">
|
||
{/* 进度条 */}
|
||
<div className="relative h-2 mb-7">
|
||
<div className="absolute top-1/2 left-0 w-full h-1 bg-gray-200 rounded-full -translate-y-1/2" />
|
||
<div
|
||
className="absolute top-1/2 left-0 h-1 bg-gradient-to-r from-blue-500 to-indigo-500 rounded-full -translate-y-1/2 transition-all duration-300"
|
||
style={{ width: `${percent}%` }}
|
||
/>
|
||
{/* 圆点 */}
|
||
<div className="absolute top-1/2 left-0 w-full flex justify-between -translate-y-1/2">
|
||
{steps.map((step, idx) => {
|
||
const isActive = currentStep === step.id;
|
||
const isDone = currentStep > step.id;
|
||
return (
|
||
<div key={step.id} className="flex flex-col items-center w-1/3">
|
||
<div
|
||
className={`w-7 h-7 flex items-center justify-center rounded-full border-2 transition-all duration-300 text-sm font-bold shadow-sm leading-none
|
||
${isActive ? 'bg-blue-500 border-blue-500 text-white scale-110' :
|
||
isDone ? 'bg-indigo-500 border-indigo-500 text-white' :
|
||
'bg-white border-gray-300 text-gray-400'}
|
||
`}
|
||
>
|
||
{isDone ? <span className="flex items-center justify-center h-full w-full"><Check className="w-4 h-4" /></span> : <span className="flex items-center justify-center h-full w-full">{step.id}</span>}
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
{/* 步骤文字 */}
|
||
<div className="flex justify-between">
|
||
{steps.map((step, idx) => {
|
||
const isActive = currentStep === step.id;
|
||
const isDone = currentStep > step.id;
|
||
return (
|
||
<div key={step.id} className="flex flex-col items-center w-1/3">
|
||
<span className={`text-xs font-semibold ${isActive ? 'text-blue-600' : isDone ? 'text-indigo-500' : 'text-gray-400'}`}>{step.title}</span>
|
||
<span className={`text-[11px] mt-0.5 ${isActive || isDone ? 'text-gray-500' : 'text-gray-400'}`}>{step.subtitle}</span>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// 基础设置步骤
|
||
const BasicSettings = () => {
|
||
return (
|
||
<div className="space-y-6">
|
||
<div>
|
||
<h3 className="text-lg font-medium mb-4">选择获客场景</h3>
|
||
<div className="grid grid-cols-2 gap-4">
|
||
{scenarioOptions.map((scenario) => (
|
||
<div
|
||
key={scenario.id}
|
||
className={`relative p-4 border-2 rounded-lg cursor-pointer transition-all ${
|
||
formData.sceneId === scenario.id
|
||
? 'border-blue-500 bg-blue-50'
|
||
: 'border-gray-200 bg-white hover:border-gray-300'
|
||
}`}
|
||
onClick={() => onChange({ sceneId: scenario.id, scenario: scenario.id })}
|
||
>
|
||
{formData.sceneId === scenario.id && (
|
||
<div className="absolute top-2 right-2 w-6 h-6 bg-blue-500 rounded-full flex items-center justify-center">
|
||
<Check className="h-4 w-4 text-white" />
|
||
</div>
|
||
)}
|
||
<div className="flex flex-col items-center text-center">
|
||
<img
|
||
src={scenario.image}
|
||
alt={scenario.name}
|
||
className="w-12 h-12 mb-2 rounded"
|
||
/>
|
||
<h3 className="font-medium text-gray-900">{scenario.name}</h3>
|
||
<p className="text-xs text-gray-500 mt-1">{scenario.description}</p>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-4">
|
||
<h3 className="text-lg font-medium">计划信息</h3>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
计划名称 *
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={formData.planName}
|
||
onChange={(e) => onChange({ planName: e.target.value })}
|
||
placeholder="请输入计划名称"
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
执行时间
|
||
</label>
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="block text-xs text-gray-500 mb-1">开始时间</label>
|
||
<input
|
||
type="time"
|
||
value={formData.startTime}
|
||
onChange={(e) => onChange({ startTime: e.target.value })}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-xs text-gray-500 mb-1">结束时间</label>
|
||
<input
|
||
type="time"
|
||
value={formData.endTime}
|
||
onChange={(e) => onChange({ endTime: e.target.value })}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="pt-4">
|
||
<button
|
||
onClick={handleNext}
|
||
disabled={!formData.sceneId || !formData.planName.trim()}
|
||
className="w-full py-3 px-4 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors"
|
||
>
|
||
下一步
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// 好友申请设置步骤
|
||
const FriendRequestSettings = () => (
|
||
<div className="space-y-6">
|
||
<div>
|
||
<h3 className="text-lg font-medium mb-4">好友申请设置</h3>
|
||
|
||
<div className="space-y-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
添加间隔(秒)
|
||
</label>
|
||
<input
|
||
type="number"
|
||
value={formData.addInterval}
|
||
onChange={(e) => onChange({ addInterval: parseInt(e.target.value) || 60 })}
|
||
min="30"
|
||
max="3600"
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
/>
|
||
<p className="text-xs text-gray-500 mt-1">建议设置60-300秒,避免被限制</p>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
备注类型
|
||
</label>
|
||
<select
|
||
value={formData.remarkType}
|
||
onChange={(e) => onChange({ remarkType: e.target.value })}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
>
|
||
<option value="default">默认备注</option>
|
||
<option value="custom">自定义备注</option>
|
||
<option value="none">不添加备注</option>
|
||
</select>
|
||
</div>
|
||
|
||
{formData.remarkType === 'custom' && (
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
自定义备注
|
||
</label>
|
||
<input
|
||
type="text"
|
||
placeholder="请输入自定义备注内容"
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
/>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex gap-4 pt-4">
|
||
<button
|
||
onClick={handlePrev}
|
||
className="flex-1 py-3 px-4 border border-gray-300 text-gray-700 rounded-md hover:bg-gray-50 transition-colors"
|
||
>
|
||
上一步
|
||
</button>
|
||
<button
|
||
onClick={handleNext}
|
||
className="flex-1 py-3 px-4 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
|
||
>
|
||
下一步
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
|
||
// 消息设置步骤
|
||
const MessageSettings = () => (
|
||
<div className="space-y-6">
|
||
<div>
|
||
<h3 className="text-lg font-medium mb-4">消息设置</h3>
|
||
|
||
<div className="space-y-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
打招呼消息
|
||
</label>
|
||
<textarea
|
||
value={formData.greeting}
|
||
onChange={(e) => onChange({ greeting: e.target.value })}
|
||
placeholder="请输入打招呼消息内容"
|
||
rows={4}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"
|
||
/>
|
||
<p className="text-xs text-gray-500 mt-1">支持变量:{name}(对方昵称)</p>
|
||
</div>
|
||
|
||
<div className="flex items-center">
|
||
<input
|
||
type="checkbox"
|
||
id="enabled"
|
||
checked={formData.enabled}
|
||
onChange={(e) => onChange({ enabled: e.target.checked })}
|
||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||
/>
|
||
<label htmlFor="enabled" className="ml-2 block text-sm text-gray-700">
|
||
立即启用计划
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex gap-4 pt-4">
|
||
<button
|
||
onClick={handlePrev}
|
||
className="flex-1 py-3 px-4 border border-gray-300 text-gray-700 rounded-md hover:bg-gray-50 transition-colors"
|
||
>
|
||
上一步
|
||
</button>
|
||
<button
|
||
onClick={handleSave}
|
||
disabled={loading}
|
||
className="flex-1 py-3 px-4 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors"
|
||
>
|
||
{loading ? '创建中...' : '创建计划'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
|
||
// 渲染当前步骤内容
|
||
const renderStepContent = () => {
|
||
switch (currentStep) {
|
||
case 1:
|
||
return <BasicSettings />;
|
||
case 2:
|
||
return <FriendRequestSettings />;
|
||
case 3:
|
||
return <MessageSettings />;
|
||
default:
|
||
return null;
|
||
}
|
||
};
|
||
|
||
return (
|
||
<Layout
|
||
header={
|
||
<PageHeader
|
||
title="新建获客计划"
|
||
defaultBackPath="/scenarios"
|
||
rightContent={
|
||
<button className="p-2 hover:bg-gray-100 rounded-lg transition-colors">
|
||
<Settings className="h-5 w-5" />
|
||
</button>
|
||
}
|
||
/>
|
||
}
|
||
>
|
||
<div className="max-w-[390px] mx-auto w-full bg-white min-h-screen flex flex-col">
|
||
<div className="flex-1 flex flex-col">
|
||
<div className="px-4 pt-8">
|
||
<StepIndicator steps={steps} currentStep={currentStep} />
|
||
</div>
|
||
<div className="flex-1 px-4 pb-20">{renderStepContent()}</div>
|
||
</div>
|
||
</div>
|
||
</Layout>
|
||
);
|
||
}
|