plan pages

This commit is contained in:
xavier
2025-06-03 16:37:39 +08:00
parent 0d33ba7e34
commit 1f2a1dab00
62 changed files with 12508 additions and 1984 deletions

View File

@@ -1,4 +1,3 @@
export default function Loading() {
return null
}

View File

@@ -1,344 +1,117 @@
"use client"
import { useState, useEffect } from "react"
import { ChevronLeft } from "lucide-react"
import { useState } from "react"
import { useRouter } from "next/navigation"
import { ChevronLeft, Settings } from "lucide-react"
import { Button } from "@/components/ui/button"
import { cn } from "@/lib/utils"
import { toast } from "@/components/ui/use-toast"
import { StepIndicator } from "@/app/components/ui-templates/step-indicator"
import { BasicSettings } from "./steps/BasicSettings"
import { FriendRequestSettings } from "./steps/FriendRequestSettings"
import { MessageSettings } from "./steps/MessageSettings"
import { TagSettings } from "./steps/TagSettings"
import { useRouter, useSearchParams } from "next/navigation"
import { toast } from "@/components/ui/use-toast"
// 步骤定义 - 只保留三个步骤
const steps = [
{ id: 1, title: "步骤一", subtitle: "基础设置" },
{ id: 2, title: "步骤二", subtitle: "好友申请" },
{ id: 2, title: "步骤二", subtitle: "好友申请设置" },
{ id: 3, title: "步骤三", subtitle: "消息设置" },
{ id: 4, title: "步骤四", subtitle: "流量标签" },
]
// 场景分类规则
const scenarioRules = {
LIVE: ["直播", "直播间", "主播", "抖音"],
COMMENT: ["评论", "互动", "回复", "小红书"],
GROUP: ["群", "社群", "群聊", "微信群"],
ARTICLE: ["文章", "笔记", "内容", "公众号"],
}
// 根据计划名称和标签自动判断场景
const determineScenario = (planName: string, tags: any[]) => {
// 优先使用标签进行分类
if (tags && tags.length > 0) {
const firstTag = tags[0]
if (firstTag.name?.includes("直播") || firstTag.name?.includes("抖音")) return "douyin"
if (firstTag.name?.includes("评论") || firstTag.name?.includes("小红书")) return "xiaohongshu"
if (firstTag.name?.includes("群") || firstTag.name?.includes("微信")) return "weixinqun"
if (firstTag.name?.includes("文章") || firstTag.name?.includes("公众号")) return "gongzhonghao"
}
// 如果没有标签,使用计划名称进行分类
const planNameLower = planName.toLowerCase()
if (planNameLower.includes("直播") || planNameLower.includes("抖音")) return "douyin"
if (planNameLower.includes("评论") || planNameLower.includes("小红书")) return "xiaohongshu"
if (planNameLower.includes("群") || planNameLower.includes("微信")) return "weixinqun"
if (planNameLower.includes("文章") || planNameLower.includes("公众号")) return "gongzhonghao"
return "other"
}
export default function NewAcquisitionPlan() {
export default function NewPlan() {
const router = useRouter()
const searchParams = useSearchParams()
const type = searchParams.get("type")
const source = searchParams.get("source")
const [currentStep, setCurrentStep] = useState(1)
const [formData, setFormData] = useState({
planName: "",
scenario: type === "order" ? "order" : "",
accounts: [],
materials: [],
enabled: true,
scenario: "haibao",
posters: [],
device: "",
remarkType: "phone",
remarkKeyword: "",
greeting: "",
addFriendTimeStart: "09:00",
addFriendTimeEnd: "18:00",
addFriendInterval: 1,
maxDailyFriends: 20,
messageInterval: 1,
messageContent: "",
tags: [],
selectedDevices: [],
messagePlans: [],
importedTags: [],
sourceWechatId: source || "",
teams: [], // 添加 teams 字段
greeting: "你好,请通过",
addInterval: 1,
startTime: "09:00",
endTime: "18:00",
enabled: true,
// 移除tags字段
})
// 如果是从微信号好友转移过来,自动设置计划名称
useEffect(() => {
if (type === "order" && source) {
const today = new Date().toLocaleDateString("zh-CN").replace(/\//g, "")
setFormData((prev) => ({
...prev,
planName: `${source}好友转移${today}`,
scenario: "order",
}))
// 更新表单数据
const onChange = (data: any) => {
setFormData((prev) => ({ ...prev, ...data }))
}
// 模拟加载好友数据
setTimeout(() => {
toast({
title: "好友数据加载成功",
description: `已从微信号 ${source} 导入好友数据`,
})
}, 1000)
}
}, [type, source])
// 处理保存
const handleSave = async () => {
try {
// 这里应该是实际的API调用
await new Promise((resolve) => setTimeout(resolve, 1000))
// 根据URL参数设置场景类型
useEffect(() => {
if (type && type !== "order" && !formData.scenario) {
const validScenarios = [
"douyin",
"kuaishou",
"xiaohongshu",
"weibo",
"haibao",
"phone",
"weixinqun",
"gongzhonghao",
]
if (validScenarios.includes(type)) {
const today = new Date().toLocaleDateString("zh-CN").replace(/\//g, "")
setFormData((prev) => ({
...prev,
scenario: type,
planName: `${type === "phone" ? "电话获客" : type}${today}`,
}))
}
}
}, [type, formData.scenario])
const handleSave = () => {
// 根据标签和计划名称自动判断场景
const scenario = formData.scenario || determineScenario(formData.planName, formData.tags)
// 准备请求数据
const requestData = {
sceneId: getSceneIdFromScenario(scenario), // 获取场景ID
name: formData.planName,
status: formData.enabled ? 1 : 0,
reqConf: JSON.stringify({
remarkType: formData.remarkType,
remarkKeyword: formData.remarkKeyword,
greeting: formData.greeting,
addFriendTimeStart: formData.addFriendTimeStart,
addFriendTimeEnd: formData.addFriendTimeEnd,
addFriendInterval: formData.addFriendInterval,
maxDailyFriends: formData.maxDailyFriends,
selectedDevices: formData.selectedDevices,
}),
msgConf: JSON.stringify({
messageInterval: formData.messageInterval,
messagePlans: formData.messagePlans,
}),
tagConf: JSON.stringify({
tags: formData.tags,
importedTags: formData.importedTags,
toast({
title: "创建成功",
description: "获客计划已创建",
})
}
// 调用API创建获客计划
fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/v1/plan/create`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify(requestData)
})
.then(response => response.json())
.then(data => {
if (data.code === 200) {
toast({
title: "创建成功",
description: "获客计划已创建完成",
})
// 跳转到首页
router.push("/")
} else {
toast({
title: "创建失败",
description: data.msg || "服务器错误,请稍后重试",
variant: "destructive"
})
}
})
.catch(error => {
console.error("创建获客计划失败:", error);
router.push("/plans")
} catch (error) {
toast({
title: "创建失败",
description: "网络错误,请稍后重试",
variant: "destructive"
description: "创建计划失败,请重试",
variant: "destructive",
})
})
}
// 将场景类型转换为场景ID
const getSceneIdFromScenario = (scenario: string): number => {
const scenarioMap: Record<string, number> = {
'douyin': 1,
'xiaohongshu': 2,
'weixinqun': 3,
'gongzhonghao': 4,
'kuaishou': 5,
'weibo': 6,
'haibao': 7,
'phone': 8,
'order': 9,
'other': 10
}
return scenarioMap[scenario] || 1 // 默认返回1
}
const handlePrev = () => {
setCurrentStep((prevStep) => Math.max(prevStep - 1, 1))
}
// 下一步
const handleNext = () => {
if (isStepValid()) {
if (currentStep === steps.length) {
handleSave()
} else {
setCurrentStep((prevStep) => Math.min(prevStep + 1, steps.length))
}
if (currentStep === steps.length) {
handleSave()
} else {
setCurrentStep((prev) => prev + 1)
}
}
const isStepValid = () => {
switch (currentStep) {
case 1:
if (!formData.planName.trim()) {
toast({
title: "请完善信息",
description: "请填写计划名称",
variant: "destructive",
})
return false
}
return true
case 2:
// 如果是订单导入场景,跳过好友申请设置验证
if (formData.scenario === "order") {
return true
}
// 修改:不再要求必须选择设备
if (!formData.greeting?.trim()) {
toast({
title: "请完善信息",
description: "请填写好友申请信息",
variant: "destructive",
})
return false
}
return true
case 3:
// 如果是订单导入场景,跳过消息设置验证
if (formData.scenario === "order") {
return true
}
if (formData.messagePlans?.length === 0) {
toast({
title: "请完善信息",
description: "请设置至少一条消息",
variant: "destructive",
})
return false
}
return true
case 4:
return true
default:
return true
}
// 上一步
const handlePrev = () => {
setCurrentStep((prev) => Math.max(prev - 1, 1))
}
// 渲染当前步骤内容
const renderStepContent = () => {
switch (currentStep) {
case 1:
return <BasicSettings formData={formData} onChange={setFormData} onNext={handleNext} />
return <BasicSettings formData={formData} onChange={onChange} onNext={handleNext} />
case 2:
return (
<FriendRequestSettings formData={formData} onChange={setFormData} onNext={handleNext} onPrev={handlePrev} />
)
return <FriendRequestSettings formData={formData} onChange={onChange} onNext={handleNext} onPrev={handlePrev} />
case 3:
return <MessageSettings formData={formData} onChange={setFormData} onNext={handleNext} onPrev={handlePrev} />
case 4:
return <TagSettings formData={formData} onNext={handleSave} onPrev={handlePrev} onChange={setFormData} />
return <MessageSettings formData={formData} onChange={onChange} onNext={handleSave} onPrev={handlePrev} />
default:
return null
}
}
// 如果是订单导入场景,直接跳到标签设置步骤
useEffect(() => {
// 只有在订单场景下才自动开启步骤1而不是直接跳到步骤4
if (formData.scenario === "order" && currentStep === 1 && formData.planName) {
// 保持在步骤1不再自动跳转到步骤4
// 之前的逻辑是直接跳到步骤4setCurrentStep(4)
}
}, [formData.scenario, currentStep, formData.planName])
return (
<div className="min-h-screen bg-gradient-to-b from-blue-50 to-white">
<div className="w-full bg-white min-h-screen flex flex-col">
<div className="min-h-screen bg-gray-50">
<div className="max-w-[390px] mx-auto bg-white min-h-screen flex flex-col">
<header className="sticky top-0 z-10 bg-white border-b">
<div className="flex items-center h-14 px-4">
<Button variant="ghost" size="icon" onClick={() => router.push("/scenarios")}>
<ChevronLeft className="h-5 w-5" />
<div className="flex items-center justify-between h-14 px-4">
<div className="flex items-center">
<Button variant="ghost" size="icon" onClick={() => router.push("/plans")}>
<ChevronLeft className="h-5 w-5" />
</Button>
<h1 className="ml-2 text-lg font-medium"></h1>
</div>
<Button variant="ghost" size="icon">
<Settings className="h-5 w-5" />
</Button>
<h1 className="ml-2 text-lg font-medium">{formData.sourceWechatId ? "好友转移" : "新建获客计划"}</h1>
</div>
</header>
<div className="flex-1 flex flex-col">
{/* 步骤指示器样式 */}
<div className="bg-white border-gray-200">
<div className="px-5 py-5">
<div className="flex justify-between">
{steps.map((step) => (
<div key={step.id} className="flex flex-col items-center">
<div
className={`rounded-full h-8 w-8 flex items-center justify-center ${
currentStep >= step.id ? "bg-blue-600 text-white" : "bg-gray-200 text-gray-600"
}`}
>
{step.id}
</div>
<div className="text-xs font-medium mt-2 text-center">{step.subtitle}</div>
</div>
))}
</div>
<div className="flex justify-between mt-4 px-4">
{steps.slice(0, steps.length - 1).map((step, index) => (
<div
key={`line-${step.id}`}
className={`h-1 w-full ${currentStep > step.id ? "bg-blue-600" : "bg-gray-200"}`}
style={{
width: `${100 / (steps.length - 1)}%`,
marginLeft: index === 0 ? "10px" : "0",
marginRight: index === steps.length - 2 ? "10px" : "0",
}}
></div>
))}
</div>
</div>
<div className="px-4 py-6">
<StepIndicator steps={steps} currentStep={currentStep} />
</div>
<div className="flex-1 px-4">{renderStepContent()}</div>
<div className="flex-1 px-4 pb-20">{renderStepContent()}</div>
</div>
</div>
</div>
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -5,15 +5,13 @@ import { Card } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { HelpCircle, MessageSquare, AlertCircle, RefreshCw } from "lucide-react"
import { HelpCircle, MessageSquare, AlertCircle } from "lucide-react"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Alert, AlertDescription } from "@/components/ui/alert"
import { ChevronsUpDown } from "lucide-react"
import { Checkbox } from "@/components/ui/checkbox"
import { fetchDeviceList } from "@/api/devices"
import type { ServerDevice } from "@/types/device"
interface FriendRequestSettingsProps {
formData: any
@@ -38,15 +36,20 @@ const remarkTypes = [
{ value: "source", label: "来源" },
]
// 模拟设备数据
const mockDevices = [
{ id: "1", name: "iPhone 13 Pro", status: "online" },
{ id: "2", name: "Xiaomi 12", status: "online" },
{ id: "3", name: "Huawei P40", status: "offline" },
{ id: "4", name: "OPPO Find X3", status: "online" },
{ id: "5", name: "Samsung S21", status: "online" },
]
export function FriendRequestSettings({ formData, onChange, onNext, onPrev }: FriendRequestSettingsProps) {
const [isTemplateDialogOpen, setIsTemplateDialogOpen] = useState(false)
const [hasWarnings, setHasWarnings] = useState(false)
const [isDeviceSelectorOpen, setIsDeviceSelectorOpen] = useState(false)
const [selectedDevices, setSelectedDevices] = useState<ServerDevice[]>(formData.selectedDevices || [])
const [devices, setDevices] = useState<ServerDevice[]>([])
const [loadingDevices, setLoadingDevices] = useState(false)
const [deviceError, setDeviceError] = useState<string | null>(null)
const [searchKeyword, setSearchKeyword] = useState("")
const [selectedDevices, setSelectedDevices] = useState<any[]>(formData.selectedDevices || [])
// 获取场景标题
const getScenarioTitle = () => {
@@ -64,33 +67,6 @@ export function FriendRequestSettings({ formData, onChange, onNext, onPrev }: Fr
}
}
// 加载设备列表
const loadDevices = async () => {
try {
setLoadingDevices(true)
setDeviceError(null)
const response = await fetchDeviceList(1, 100, searchKeyword)
if (response.code === 200 && response.data?.list) {
setDevices(response.data.list)
} else {
setDeviceError(response.msg || "获取设备列表失败")
console.error("获取设备列表失败:", response.msg)
}
} catch (err) {
console.error("获取设备列表失败:", err)
setDeviceError("获取设备列表失败,请稍后重试")
} finally {
setLoadingDevices(false)
}
}
// 初始化时加载设备列表
useEffect(() => {
loadDevices()
}, [])
// 使用useEffect设置默认值
useEffect(() => {
if (!formData.greeting) {
@@ -120,7 +96,7 @@ export function FriendRequestSettings({ formData, onChange, onNext, onPrev }: Fr
onNext()
}
const toggleDeviceSelection = (device: ServerDevice) => {
const toggleDeviceSelection = (device: any) => {
const isSelected = selectedDevices.some((d) => d.id === device.id)
let newSelectedDevices
@@ -134,13 +110,8 @@ export function FriendRequestSettings({ formData, onChange, onNext, onPrev }: Fr
onChange({ ...formData, selectedDevices: newSelectedDevices })
}
// 根据关键词搜索设备
const handleSearch = () => {
loadDevices()
}
return (
<div className="w-full p-4 bg-gray-50">
<Card className="p-6">
<div className="space-y-6">
<div>
<Label className="text-base"></Label>
@@ -157,77 +128,31 @@ export function FriendRequestSettings({ formData, onChange, onNext, onPrev }: Fr
{isDeviceSelectorOpen && (
<div className="absolute z-10 w-full mt-1 bg-white border rounded-md shadow-lg">
<div className="p-2">
<div className="flex gap-2 mb-2">
<Input
placeholder="搜索设备..."
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
/>
<Button variant="outline" size="icon" onClick={handleSearch}>
<RefreshCw className="h-4 w-4" />
</Button>
</div>
{loadingDevices ? (
<div className="flex justify-center items-center py-4">
<div className="animate-spin h-5 w-5 border-2 border-blue-500 rounded-full border-t-transparent"></div>
</div>
) : deviceError ? (
<div className="text-center text-red-500 py-4">
{deviceError}
<Button variant="outline" size="sm" onClick={loadDevices} className="ml-2">
</Button>
</div>
) : devices.length === 0 ? (
<div className="text-center text-gray-500 py-4">
</div>
) : (
<div className="max-h-60 overflow-auto">
{devices.map((device) => (
<div
key={device.id}
className="flex items-center justify-between p-2 hover:bg-gray-100 cursor-pointer"
onClick={() => toggleDeviceSelection(device)}
>
<div className="flex items-center space-x-2">
<Checkbox
checked={selectedDevices.some((d) => d.id === device.id)}
onCheckedChange={() => toggleDeviceSelection(device)}
/>
<span>{device.memo}</span>
</div>
<span className={`text-xs ${device.alive === 1 ? "text-green-500" : "text-gray-400"}`}>
{device.alive === 1 ? "在线" : "离线"}
</span>
<Input placeholder="搜索设备..." className="mb-2" />
<div className="max-h-60 overflow-auto">
{mockDevices.map((device) => (
<div
key={device.id}
className="flex items-center justify-between p-2 hover:bg-gray-100 cursor-pointer"
onClick={() => toggleDeviceSelection(device)}
>
<div className="flex items-center space-x-2">
<Checkbox
checked={selectedDevices.some((d) => d.id === device.id)}
onCheckedChange={() => toggleDeviceSelection(device)}
/>
<span>{device.name}</span>
</div>
))}
</div>
)}
<span className={`text-xs ${device.status === "online" ? "text-green-500" : "text-gray-400"}`}>
{device.status === "online" ? "在线" : "离线"}
</span>
</div>
))}
</div>
</div>
</div>
)}
</div>
{selectedDevices.length > 0 && (
<div className="mt-2 flex flex-wrap gap-2">
{selectedDevices.map((device) => (
<div key={device.id} className="flex items-center bg-gray-100 rounded-full px-3 py-1">
<span className="text-sm">{device.memo}</span>
<Button
variant="ghost"
size="sm"
className="ml-2 p-0"
onClick={() => toggleDeviceSelection(device)}
>
<AlertCircle className="h-4 w-4" />
</Button>
</div>
))}
</div>
)}
</div>
<div>
@@ -314,7 +239,7 @@ export function FriendRequestSettings({ formData, onChange, onNext, onPrev }: Fr
</div>
{hasWarnings && (
<Alert variant="destructive" className="bg-amber-50 border-amber-200">
<Alert variant="warning" className="bg-amber-50 border-amber-200">
<AlertCircle className="h-4 w-4 text-amber-500" />
<AlertDescription></AlertDescription>
</Alert>
@@ -347,7 +272,6 @@ export function FriendRequestSettings({ formData, onChange, onNext, onPrev }: Fr
</div>
</DialogContent>
</Dialog>
</div>
</Card>
)
}

View File

@@ -82,7 +82,7 @@ export function MessageSettings({ formData, onChange, onNext, onPrev }: MessageS
type: "text",
content: "",
sendInterval: 5,
intervalUnit: "minutes",
intervalUnit: "seconds", // 默认改为秒
},
],
},
@@ -103,7 +103,7 @@ export function MessageSettings({ formData, onChange, onNext, onPrev }: MessageS
if (dayPlans[dayIndex].day === 0) {
// 即时消息使用间隔设置
newMessage.sendInterval = 5
newMessage.intervalUnit = "minutes"
newMessage.intervalUnit = "seconds" // 默认改为秒
} else {
// 非即时消息使用具体时间设置
newMessage.scheduledTime = {
@@ -192,7 +192,7 @@ export function MessageSettings({ formData, onChange, onNext, onPrev }: MessageS
}
return (
<div className="w-full p-4 bg-gray-50">
<Card className="p-6">
<div className="space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold"></h2>
@@ -549,7 +549,6 @@ export function MessageSettings({ formData, onChange, onNext, onPrev }: MessageS
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</Card>
)
}