Files
cunkebao_v3/nkebao/src/pages/scenarios/NewPlan.tsx
笔记本里的永平 5d68a41931 feat: 本次提交更新内容如下
先存一下
2025-07-07 22:25:24 +08:00

443 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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">&#123;name&#125;</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>
);
}