Files
ckb-SuperAdmin/nkebao/src/pages/scenarios/NewPlan.tsx
笔记本里的永平 5ca8050fc5 feat: 本次提交更新内容如下
全局组件梳理开始
2025-07-07 16:06:33 +08:00

419 lines
14 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';
// 步骤定义
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));
};
// 步骤指示器组件
const StepIndicator = ({ steps, currentStep }: { steps: any[], currentStep: number }) => (
<div className="flex items-center justify-between mb-6">
{steps.map((step, index) => (
<div key={step.id} className="flex items-center">
<div className={`flex items-center justify-center w-8 h-8 rounded-full text-sm font-medium ${
currentStep > step.id
? 'bg-green-500 text-white'
: currentStep === step.id
? 'bg-blue-500 text-white'
: 'bg-gray-200 text-gray-500'
}`}>
{currentStep > step.id ? <Check className="h-4 w-4" /> : step.id}
</div>
<div className="ml-2">
<div className="text-sm font-medium">{step.title}</div>
<div className="text-xs text-gray-500">{step.subtitle}</div>
</div>
{index < steps.length - 1 && (
<div className={`w-12 h-0.5 mx-4 ${
currentStep > step.id ? 'bg-green-500' : 'bg-gray-200'
}`} />
)}
</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 (
<div className="min-h-screen bg-gray-50">
<div className="max-w-[390px] mx-auto bg-white min-h-screen flex flex-col">
<PageHeader
title="新建获客计划"
defaultBackPath="/scenarios"
rightContent={
<button className="p-2 hover:bg-gray-100 rounded-lg transition-colors">
<Settings className="h-5 w-5" />
</button>
}
/>
{/* 添加pt-16来避免被固定导航栏遮挡 */}
<div className="flex-1 flex flex-col pt-16">
<div className="px-4 py-6">
<StepIndicator steps={steps} currentStep={currentStep} />
</div>
<div className="flex-1 px-4 pb-20">{renderStepContent()}</div>
</div>
</div>
</div>
);
}