refactor: overhaul UI for streamlined user experience
Redesign navigation, home overview, user portrait, and valuation pages with improved functionality and responsive design. Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
This commit is contained in:
3
app/plans/new/loading.tsx
Normal file
3
app/plans/new/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
265
app/plans/new/page.tsx
Normal file
265
app/plans/new/page.tsx
Normal file
@@ -0,0 +1,265 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { ChevronLeft } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { cn } from "@/lib/utils"
|
||||
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: 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() {
|
||||
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,
|
||||
remarkType: "phone",
|
||||
remarkKeyword: "",
|
||||
greeting: "",
|
||||
addFriendTimeStart: "09:00",
|
||||
addFriendTimeEnd: "18:00",
|
||||
addFriendInterval: 1,
|
||||
maxDailyFriends: 20,
|
||||
messageInterval: 1,
|
||||
messageContent: "",
|
||||
tags: [],
|
||||
selectedDevices: [],
|
||||
messagePlans: [],
|
||||
importedTags: [],
|
||||
sourceWechatId: source || "",
|
||||
})
|
||||
|
||||
// 如果是从微信号好友转移过来,自动设置计划名称
|
||||
useEffect(() => {
|
||||
if (type === "order" && source) {
|
||||
const today = new Date().toLocaleDateString("zh-CN").replace(/\//g, "")
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
planName: `${source}好友转移${today}`,
|
||||
scenario: "order",
|
||||
}))
|
||||
|
||||
// 模拟加载好友数据
|
||||
setTimeout(() => {
|
||||
toast({
|
||||
title: "好友数据加载成功",
|
||||
description: `已从微信号 ${source} 导入好友数据`,
|
||||
})
|
||||
}, 1000)
|
||||
}
|
||||
}, [type, source])
|
||||
|
||||
const handleSave = () => {
|
||||
// 根据标签和计划名称自动判断场景
|
||||
const scenario = formData.scenario || determineScenario(formData.planName, formData.tags)
|
||||
|
||||
console.log("计划已创建:", { ...formData, scenario })
|
||||
toast({
|
||||
title: "创建成功",
|
||||
description: "获客计划已创建完成",
|
||||
})
|
||||
|
||||
// 跳转到首页
|
||||
router.push("/")
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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() || formData.selectedDevices?.length === 0) {
|
||||
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 renderStepContent = () => {
|
||||
switch (currentStep) {
|
||||
case 1:
|
||||
return <BasicSettings formData={formData} onChange={setFormData} onNext={handleNext} />
|
||||
case 2:
|
||||
return (
|
||||
<FriendRequestSettings formData={formData} onChange={setFormData} onNext={handleNext} onPrev={handlePrev} />
|
||||
)
|
||||
case 3:
|
||||
return <MessageSettings formData={formData} onChange={setFormData} onNext={handleNext} onPrev={handlePrev} />
|
||||
case 4:
|
||||
return <TagSettings formData={formData} onComplete={handleSave} onPrev={handlePrev} onChange={setFormData} />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是订单导入场景,直接跳到标签设置步骤
|
||||
useEffect(() => {
|
||||
// 只有在订单场景下,才自动开启步骤1,而不是直接跳到步骤4
|
||||
if (formData.scenario === "order" && currentStep === 1 && formData.planName) {
|
||||
// 保持在步骤1,不再自动跳转到步骤4
|
||||
// 之前的逻辑是直接跳到步骤4:setCurrentStep(4)
|
||||
}
|
||||
}, [formData.scenario, currentStep, formData.planName])
|
||||
|
||||
return (
|
||||
<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("/")}>
|
||||
<ChevronLeft 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="px-4 py-6">
|
||||
<div className="relative flex justify-between">
|
||||
{steps.map((step, index) => (
|
||||
<div
|
||||
key={step.id}
|
||||
className={cn(
|
||||
"flex flex-col items-center relative z-10",
|
||||
currentStep >= step.id ? "text-blue-600" : "text-gray-400",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"w-8 h-8 rounded-full flex items-center justify-center border-2",
|
||||
currentStep >= step.id
|
||||
? "border-blue-600 bg-blue-600 text-white"
|
||||
: "border-gray-300 bg-white text-gray-400",
|
||||
)}
|
||||
>
|
||||
{step.id}
|
||||
</div>
|
||||
<div className="text-xs mt-1">{step.title}</div>
|
||||
<div className="text-xs mt-0.5 font-medium">{step.subtitle}</div>
|
||||
{index < steps.length - 1 && (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute top-4 left-[32px] w-[calc(100%-32px)] h-[2px]",
|
||||
currentStep > step.id ? "bg-blue-600" : "bg-gray-200",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 px-4 pb-20">{renderStepContent()}</div>
|
||||
|
||||
<div className="sticky bottom-0 left-0 right-0 bg-white border-t p-4">
|
||||
<div className="flex justify-between max-w-[390px] mx-auto">
|
||||
{currentStep > 1 && (
|
||||
<Button variant="outline" onClick={handlePrev}>
|
||||
上一步
|
||||
</Button>
|
||||
)}
|
||||
<Button className={cn("min-w-[120px]", currentStep === 1 ? "w-full" : "ml-auto")} onClick={handleNext}>
|
||||
{currentStep === steps.length ? "完成" : "下一步"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
548
app/plans/new/steps/BasicSettings.tsx
Normal file
548
app/plans/new/steps/BasicSettings.tsx
Normal file
@@ -0,0 +1,548 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
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 { Switch } from "@/components/ui/switch"
|
||||
import { QrCode, X, ChevronDown, Plus, Maximize2, Upload, Download } from "lucide-react"
|
||||
import { TooltipProvider } from "@/components/ui/tooltip"
|
||||
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "@/components/ui/table"
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"
|
||||
|
||||
const scenarios = [
|
||||
{ id: "haibao", name: "海报", type: "material" },
|
||||
{ id: "order", name: "订单", type: "api" },
|
||||
{ id: "douyin", name: "抖音", type: "social" },
|
||||
{ id: "xiaohongshu", name: "小红书", type: "social" },
|
||||
{ id: "gongzhonghao", name: "公众号", type: "social" },
|
||||
{ id: "payment", name: "支付码", type: "material" },
|
||||
{ id: "weixinqun", name: "微信群", type: "social" },
|
||||
{ id: "api", name: "API", type: "api" },
|
||||
]
|
||||
|
||||
interface Account {
|
||||
id: string
|
||||
nickname: string
|
||||
avatar: string
|
||||
}
|
||||
|
||||
interface Material {
|
||||
id: string
|
||||
name: string
|
||||
type: "poster" | "payment"
|
||||
preview: string
|
||||
}
|
||||
|
||||
interface BasicSettingsProps {
|
||||
formData: any
|
||||
onChange: (data: any) => void
|
||||
onNext?: () => void
|
||||
}
|
||||
|
||||
const posterTemplates = [
|
||||
{
|
||||
id: "poster-1",
|
||||
name: "点击领取",
|
||||
preview:
|
||||
"https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E9%A2%86%E5%8F%961-tipd1HI7da6qooY5NkhxQnXBnT5LGU.gif",
|
||||
},
|
||||
{
|
||||
id: "poster-2",
|
||||
name: "点击合作",
|
||||
preview:
|
||||
"https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E5%90%88%E4%BD%9C-LPlMdgxtvhqCSr4IM1bZFEFDBF3ztI.gif",
|
||||
},
|
||||
{
|
||||
id: "poster-3",
|
||||
name: "点击咨询",
|
||||
preview:
|
||||
"https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E5%92%A8%E8%AF%A2-FTiyAMAPop2g9LvjLOLDz0VwPg3KVu.gif",
|
||||
},
|
||||
{
|
||||
id: "poster-4",
|
||||
name: "点击签到",
|
||||
preview:
|
||||
"https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E7%AD%BE%E5%88%B0-94TZIkjLldb4P2jTVlI6MkSDg0NbXi.gif",
|
||||
},
|
||||
{
|
||||
id: "poster-5",
|
||||
name: "点击了解",
|
||||
preview:
|
||||
"https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E4%BA%86%E8%A7%A3-6GCl7mQVdO4WIiykJyweSubLsTwj71.gif",
|
||||
},
|
||||
{
|
||||
id: "poster-6",
|
||||
name: "点击报名",
|
||||
preview:
|
||||
"https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E6%8A%A5%E5%90%8D-Mj0nnva0BiASeDAIhNNaRRAbjPgjEj.gif",
|
||||
},
|
||||
]
|
||||
|
||||
const generateRandomAccounts = (count: number): Account[] => {
|
||||
return Array.from({ length: count }, (_, index) => ({
|
||||
id: `account-${index + 1}`,
|
||||
nickname: `账号-${Math.random().toString(36).substring(2, 7)}`,
|
||||
avatar: `/placeholder.svg?height=40&width=40&text=${index + 1}`,
|
||||
}))
|
||||
}
|
||||
|
||||
const generatePosterMaterials = (): Material[] => {
|
||||
return posterTemplates.map((template) => ({
|
||||
id: template.id,
|
||||
name: template.name,
|
||||
type: "poster",
|
||||
preview: template.preview,
|
||||
}))
|
||||
}
|
||||
|
||||
export function BasicSettings({ formData, onChange, onNext }: BasicSettingsProps) {
|
||||
const [isAccountDialogOpen, setIsAccountDialogOpen] = useState(false)
|
||||
const [isMaterialDialogOpen, setIsMaterialDialogOpen] = useState(false)
|
||||
const [isQRCodeOpen, setIsQRCodeOpen] = useState(false)
|
||||
const [isPreviewOpen, setIsPreviewOpen] = useState(false)
|
||||
const [previewImage, setPreviewImage] = useState("")
|
||||
const [accounts] = useState<Account[]>(generateRandomAccounts(50))
|
||||
const [materials] = useState<Material[]>(generatePosterMaterials())
|
||||
const [selectedAccounts, setSelectedAccounts] = useState<Account[]>(
|
||||
formData.accounts?.length > 0 ? formData.accounts : [],
|
||||
)
|
||||
const [selectedMaterials, setSelectedMaterials] = useState<Material[]>(
|
||||
formData.materials?.length > 0 ? formData.materials : [],
|
||||
)
|
||||
const [showAllScenarios, setShowAllScenarios] = useState(false)
|
||||
const [isImportDialogOpen, setIsImportDialogOpen] = useState(false)
|
||||
const [importedTags, setImportedTags] = useState<
|
||||
Array<{
|
||||
phone: string
|
||||
wechat: string
|
||||
source?: string
|
||||
orderAmount?: number
|
||||
orderDate?: string
|
||||
}>
|
||||
>(formData.importedTags || [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!formData.planName) {
|
||||
if (formData.materials?.length > 0) {
|
||||
const today = new Date().toLocaleDateString("zh-CN").replace(/\//g, "")
|
||||
onChange({ ...formData, planName: `海报${today}` })
|
||||
} else {
|
||||
onChange({ ...formData, planName: "场景" })
|
||||
}
|
||||
}
|
||||
}, [formData, onChange])
|
||||
|
||||
const handleScenarioSelect = (scenarioId: string) => {
|
||||
onChange({ ...formData, scenario: scenarioId })
|
||||
}
|
||||
|
||||
const handleAccountSelect = (account: Account) => {
|
||||
const updatedAccounts = [...selectedAccounts, account]
|
||||
setSelectedAccounts(updatedAccounts)
|
||||
onChange({ ...formData, accounts: updatedAccounts })
|
||||
}
|
||||
|
||||
const handleMaterialSelect = (material: Material) => {
|
||||
const updatedMaterials = [material]
|
||||
setSelectedMaterials(updatedMaterials)
|
||||
onChange({ ...formData, materials: updatedMaterials })
|
||||
setIsMaterialDialogOpen(false)
|
||||
|
||||
// 更新计划名称
|
||||
const today = new Date().toLocaleDateString("zh-CN").replace(/\//g, "")
|
||||
onChange({ ...formData, planName: `海报${today}`, materials: updatedMaterials })
|
||||
}
|
||||
|
||||
const handleRemoveAccount = (accountId: string) => {
|
||||
const updatedAccounts = selectedAccounts.filter((a) => a.id !== accountId)
|
||||
setSelectedAccounts(updatedAccounts)
|
||||
onChange({ ...formData, accounts: updatedAccounts })
|
||||
}
|
||||
|
||||
const handleRemoveMaterial = (materialId: string) => {
|
||||
const updatedMaterials = selectedMaterials.filter((m) => m.id !== materialId)
|
||||
setSelectedMaterials(updatedMaterials)
|
||||
onChange({ ...formData, materials: updatedMaterials })
|
||||
}
|
||||
|
||||
const handlePreviewImage = (imageUrl: string) => {
|
||||
setPreviewImage(imageUrl)
|
||||
setIsPreviewOpen(true)
|
||||
}
|
||||
|
||||
const displayedScenarios = showAllScenarios ? scenarios : scenarios.slice(0, 3)
|
||||
|
||||
const handleFileImport = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0]
|
||||
if (file) {
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
const content = e.target?.result as string
|
||||
const rows = content.split("\n").filter((row) => row.trim())
|
||||
const tags = rows.slice(1).map((row) => {
|
||||
const [phone, wechat, source, orderAmount, orderDate] = row.split(",")
|
||||
return {
|
||||
phone: phone.trim(),
|
||||
wechat: wechat.trim(),
|
||||
source: source?.trim(),
|
||||
orderAmount: orderAmount ? Number(orderAmount) : undefined,
|
||||
orderDate: orderDate?.trim(),
|
||||
}
|
||||
})
|
||||
setImportedTags(tags)
|
||||
onChange({ ...formData, importedTags: tags })
|
||||
} catch (error) {
|
||||
console.error("导入失败:", error)
|
||||
}
|
||||
}
|
||||
reader.readAsText(file)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDownloadTemplate = () => {
|
||||
const template = "电话号码,微信号,来源,订单金额,下单日期\n13800138000,wxid_123,抖音,99.00,2024-03-03"
|
||||
const blob = new Blob([template], { type: "text/csv" })
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement("a")
|
||||
a.href = url
|
||||
a.download = "订单导入模板.csv"
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
window.URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Card className="p-6">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<Label className="text-base mb-4 block">获客场景</Label>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{displayedScenarios.map((scenario) => (
|
||||
<button
|
||||
key={scenario.id}
|
||||
className={`p-2 rounded-lg text-center transition-all ${
|
||||
formData.scenario === scenario.id
|
||||
? "bg-blue-100 text-blue-600 font-medium"
|
||||
: "bg-gray-50 text-gray-600 hover:bg-gray-100"
|
||||
}`}
|
||||
onClick={() => handleScenarioSelect(scenario.id)}
|
||||
>
|
||||
{scenario.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{!showAllScenarios && (
|
||||
<Button variant="ghost" className="mt-2 w-full text-blue-600" onClick={() => setShowAllScenarios(true)}>
|
||||
展开更多选项 <ChevronDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="planName">计划名称</Label>
|
||||
<Input
|
||||
id="planName"
|
||||
value={formData.planName}
|
||||
onChange={(e) => onChange({ ...formData, planName: e.target.value })}
|
||||
placeholder="请输入计划名称"
|
||||
className="mt-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{formData.scenario && (
|
||||
<>
|
||||
{scenarios.find((s) => s.id === formData.scenario)?.type === "social" && (
|
||||
<div>
|
||||
<Label>绑定账号</Label>
|
||||
<div className="flex gap-2 mt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1 justify-start"
|
||||
onClick={() => setIsAccountDialogOpen(true)}
|
||||
>
|
||||
{selectedAccounts.length > 0 ? `已选择 ${selectedAccounts.length} 个账号` : "选择账号"}
|
||||
</Button>
|
||||
<Button variant="outline" size="icon" onClick={() => setIsQRCodeOpen(true)}>
|
||||
<QrCode className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{selectedAccounts.length > 0 && (
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{selectedAccounts.map((account) => (
|
||||
<div key={account.id} className="flex items-center bg-gray-100 rounded-full px-3 py-1">
|
||||
<img
|
||||
src={account.avatar || "/placeholder.svg"}
|
||||
alt={account.nickname}
|
||||
className="w-4 h-4 rounded-full mr-2"
|
||||
/>
|
||||
<span className="text-sm">{account.nickname}</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="ml-2 p-0"
|
||||
onClick={() => handleRemoveAccount(account.id)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{scenarios.find((s) => s.id === formData.scenario)?.type === "material" && (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<Label>选择海报</Label>
|
||||
<Button variant="outline" onClick={() => setIsMaterialDialogOpen(true)}>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 海报展示区域 */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
|
||||
{materials.map((material) => (
|
||||
<div
|
||||
key={material.id}
|
||||
className={`relative cursor-pointer rounded-lg overflow-hidden group ${
|
||||
selectedMaterials.find((m) => m.id === material.id)
|
||||
? "ring-2 ring-blue-600"
|
||||
: "hover:ring-2 hover:ring-blue-600"
|
||||
}`}
|
||||
onClick={() => handleMaterialSelect(material)}
|
||||
>
|
||||
<img
|
||||
src={material.preview || "/placeholder.svg"}
|
||||
alt={material.name}
|
||||
className="w-full aspect-[9/16] object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-all">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handlePreviewImage(material.preview)
|
||||
}}
|
||||
>
|
||||
<Maximize2 className="h-4 w-4 text-white" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="absolute bottom-0 left-0 right-0 p-2 bg-black/50 text-white">
|
||||
<div className="text-sm truncate">{material.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{selectedMaterials.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<Label>已选择的海报</Label>
|
||||
<div className="mt-2">
|
||||
<div className="relative w-full max-w-[200px]">
|
||||
<img
|
||||
src={selectedMaterials[0].preview || "/placeholder.svg"}
|
||||
alt={selectedMaterials[0].name}
|
||||
className="w-full aspect-[9/16] object-cover rounded-lg cursor-pointer"
|
||||
onClick={() => handlePreviewImage(selectedMaterials[0].preview)}
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="absolute top-2 right-2"
|
||||
onClick={() => handleRemoveMaterial(selectedMaterials[0].id)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{scenarios.find((s) => s.id === formData.scenario)?.id === "order" && (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<Label>订单导入</Label>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={handleDownloadTemplate}>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
下载模板
|
||||
</Button>
|
||||
<Button onClick={() => setIsImportDialogOpen(true)}>
|
||||
<Upload className="h-4 w-4 mr-2" />
|
||||
导入订单
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{importedTags.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<h4 className="text-sm font-medium mb-2">已导入 {importedTags.length} 条数据</h4>
|
||||
<div className="max-h-[300px] overflow-auto border rounded-md">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>电话号码</TableHead>
|
||||
<TableHead>微信号</TableHead>
|
||||
<TableHead>来源</TableHead>
|
||||
<TableHead>订单金额</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{importedTags.slice(0, 5).map((tag, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>{tag.phone}</TableCell>
|
||||
<TableCell>{tag.wechat}</TableCell>
|
||||
<TableCell>{tag.source}</TableCell>
|
||||
<TableCell>{tag.orderAmount}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{importedTags.length > 5 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} className="text-center text-gray-500">
|
||||
还有 {importedTags.length - 5} 条数据未显示
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="enabled">是否启用</Label>
|
||||
<Switch
|
||||
id="enabled"
|
||||
checked={formData.enabled}
|
||||
onCheckedChange={(checked) => onChange({ ...formData, enabled: checked })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button className="w-full h-12 text-base" onClick={onNext}>
|
||||
下一步
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 账号选择对话框 */}
|
||||
<Dialog open={isAccountDialogOpen} onOpenChange={setIsAccountDialogOpen}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>选择账号</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="mt-4 max-h-[400px] overflow-y-auto">
|
||||
<div className="space-y-2">
|
||||
{accounts.map((account) => (
|
||||
<div
|
||||
key={account.id}
|
||||
className="flex items-center space-x-3 p-3 hover:bg-gray-100 rounded-lg cursor-pointer"
|
||||
onClick={() => handleAccountSelect(account)}
|
||||
>
|
||||
<img src={account.avatar || "/placeholder.svg"} alt="" className="w-10 h-10 rounded-full" />
|
||||
<span className="flex-1">{account.nickname}</span>
|
||||
{selectedAccounts.find((a) => a.id === account.id) && (
|
||||
<div className="w-4 h-4 rounded-full bg-blue-600" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 二维码对话框 */}
|
||||
<Dialog open={isQRCodeOpen} onOpenChange={setIsQRCodeOpen}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>绑定账号</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col items-center p-6">
|
||||
<div className="w-64 h-64 bg-gray-100 rounded-lg flex items-center justify-center">
|
||||
<img src="/placeholder.svg?height=256&width=256" alt="二维码" className="w-full h-full" />
|
||||
</div>
|
||||
<p className="mt-4 text-sm text-gray-600">请用相应的APP扫码</p>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 图片预览对话框 */}
|
||||
<Dialog open={isPreviewOpen} onOpenChange={setIsPreviewOpen}>
|
||||
<DialogContent className="sm:max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>海报预览</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex justify-center items-center p-4">
|
||||
<img src={previewImage || "/placeholder.svg"} alt="预览" className="max-h-[80vh] object-contain" />
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 订单导入对话框 */}
|
||||
<Dialog open={isImportDialogOpen} onOpenChange={setIsImportDialogOpen}>
|
||||
<DialogContent className="max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>导入订单标签</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Input type="file" accept=".csv" onChange={handleFileImport} className="flex-1" />
|
||||
</div>
|
||||
<div className="max-h-[400px] overflow-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>电话号码</TableHead>
|
||||
<TableHead>微信号</TableHead>
|
||||
<TableHead>来源</TableHead>
|
||||
<TableHead>订单金额</TableHead>
|
||||
<TableHead>下单日期</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{importedTags.map((tag, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>{tag.phone}</TableCell>
|
||||
<TableCell>{tag.wechat}</TableCell>
|
||||
<TableCell>{tag.source}</TableCell>
|
||||
<TableCell>{tag.orderAmount}</TableCell>
|
||||
<TableCell>{tag.orderDate}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsImportDialogOpen(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onChange({ ...formData, importedTags })
|
||||
setIsImportDialogOpen(false)
|
||||
}}
|
||||
>
|
||||
确认导入
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Card>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
227
app/plans/new/steps/FriendRequestSettings.tsx
Normal file
227
app/plans/new/steps/FriendRequestSettings.tsx
Normal file
@@ -0,0 +1,227 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
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 { DeviceSelectionDialog } from "@/app/components/device-selection-dialog"
|
||||
import { HelpCircle, ChevronDown, MessageSquare } 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"
|
||||
|
||||
interface FriendRequestSettingsProps {
|
||||
formData: any
|
||||
onChange: (data: any) => void
|
||||
onNext: () => void
|
||||
onPrev: () => void
|
||||
}
|
||||
|
||||
// 招呼语模板
|
||||
const greetingTemplates = [
|
||||
"你好,请通过",
|
||||
"你好,了解XX,请通过",
|
||||
"你好,我是XX产品的客服请通过",
|
||||
"你好,感谢关注我们的产品",
|
||||
"你好,很高兴为您服务",
|
||||
]
|
||||
|
||||
// 备注类型选项
|
||||
const remarkTypes = [
|
||||
{ value: "phone", label: "手机号" },
|
||||
{ value: "nickname", label: "昵称" },
|
||||
{ value: "source", label: "来源" },
|
||||
]
|
||||
|
||||
export function FriendRequestSettings({ formData, onChange, onNext, onPrev }: FriendRequestSettingsProps) {
|
||||
const [isDeviceDialogOpen, setIsDeviceDialogOpen] = useState(false)
|
||||
const [isTemplateDialogOpen, setIsTemplateDialogOpen] = useState(false)
|
||||
|
||||
// 使用useEffect设置默认值
|
||||
useEffect(() => {
|
||||
if (!formData.greeting) {
|
||||
onChange({
|
||||
...formData,
|
||||
greeting: "你好,请通过",
|
||||
remarkType: "phone", // 默认选择手机号
|
||||
maxDailyFriends: 20,
|
||||
addFriendInterval: 1,
|
||||
})
|
||||
}
|
||||
}, [formData, formData.greeting, onChange])
|
||||
|
||||
const handleDeviceSelect = (deviceIds: string[]) => {
|
||||
onChange({
|
||||
...formData,
|
||||
selectedDevices: deviceIds,
|
||||
})
|
||||
}
|
||||
|
||||
const handleTemplateSelect = (template: string) => {
|
||||
onChange({ ...formData, greeting: template })
|
||||
setIsTemplateDialogOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<Label className="text-base">选择设备</Label>
|
||||
<Button variant="outline" className="w-full mt-2 justify-between" onClick={() => setIsDeviceDialogOpen(true)}>
|
||||
{formData.selectedDevices?.length ? `已选择 ${formData.selectedDevices.length} 个设备` : "选择设备"}
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Label className="text-base">好友备注</Label>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<HelpCircle className="h-4 w-4 text-gray-400" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>设置添加好友时的备注格式</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<Select
|
||||
value={formData.remarkType || "phone"}
|
||||
onValueChange={(value) => onChange({ ...formData, remarkType: value })}
|
||||
>
|
||||
<SelectTrigger className="w-full mt-2">
|
||||
<SelectValue placeholder="选择备注类型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{remarkTypes.map((type) => (
|
||||
<SelectItem key={type.value} value={type.value}>
|
||||
{type.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{formData.remarkType && (
|
||||
<div className="mt-2 p-3 bg-gray-50 rounded-lg text-sm">
|
||||
<div className="text-gray-500 mb-1">备注格式预览:</div>
|
||||
{formData.remarkType === "phone" && "138****1234"}
|
||||
{formData.remarkType === "nickname" && "小红书用户2851"}
|
||||
{formData.remarkType === "source" && "抖音直播"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-base">招呼语</Label>
|
||||
<Button variant="ghost" size="sm" onClick={() => setIsTemplateDialogOpen(true)} className="text-blue-500">
|
||||
<MessageSquare className="h-4 w-4 mr-2" />
|
||||
参考模板
|
||||
</Button>
|
||||
</div>
|
||||
<Input
|
||||
value={formData.greeting}
|
||||
onChange={(e) => onChange({ ...formData, greeting: e.target.value })}
|
||||
placeholder="请输入招呼语"
|
||||
className="mt-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Label className="text-base">每个设备每日最大添加数量</Label>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<HelpCircle className="h-4 w-4 text-gray-400" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>为了账号安全,建议每个设备每日添加不超过20个好友</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<Input
|
||||
type="number"
|
||||
value={formData.maxDailyFriends || 20}
|
||||
onChange={(e) => onChange({ ...formData, maxDailyFriends: Number(e.target.value) })}
|
||||
max={20}
|
||||
min={1}
|
||||
className="w-32"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-base">添加间隔</Label>
|
||||
<div className="flex items-center space-x-2 mt-2">
|
||||
<Input
|
||||
type="number"
|
||||
value={formData.addFriendInterval || 1}
|
||||
onChange={(e) => onChange({ ...formData, addFriendInterval: Number(e.target.value) })}
|
||||
className="w-32"
|
||||
/>
|
||||
<span>分钟</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-base">允许加人的时间段</Label>
|
||||
<div className="flex items-center space-x-2 mt-2">
|
||||
<Input
|
||||
type="time"
|
||||
value={formData.addFriendTimeStart || "09:00"}
|
||||
onChange={(e) => onChange({ ...formData, addFriendTimeStart: e.target.value })}
|
||||
className="w-32"
|
||||
/>
|
||||
<span>至</span>
|
||||
<Input
|
||||
type="time"
|
||||
value={formData.addFriendTimeEnd || "18:00"}
|
||||
onChange={(e) => onChange({ ...formData, addFriendTimeEnd: e.target.value })}
|
||||
className="w-32"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between pt-4">
|
||||
<Button variant="outline" onClick={onPrev}>
|
||||
上一步
|
||||
</Button>
|
||||
<Button onClick={onNext}>下一步</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DeviceSelectionDialog
|
||||
open={isDeviceDialogOpen}
|
||||
onOpenChange={setIsDeviceDialogOpen}
|
||||
selectedDevices={formData.selectedDevices || []}
|
||||
onSelect={handleDeviceSelect}
|
||||
excludeUsedDevices={true}
|
||||
/>
|
||||
|
||||
<Dialog open={isTemplateDialogOpen} onOpenChange={setIsTemplateDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>招呼语模板</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-2">
|
||||
{greetingTemplates.map((template, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
variant="outline"
|
||||
className="w-full justify-start h-auto py-3 px-4"
|
||||
onClick={() => handleTemplateSelect(template)}
|
||||
>
|
||||
{template}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
406
app/plans/new/steps/MessageSettings.tsx
Normal file
406
app/plans/new/steps/MessageSettings.tsx
Normal file
@@ -0,0 +1,406 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
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 { Textarea } from "@/components/ui/textarea"
|
||||
import {
|
||||
MessageSquare,
|
||||
ImageIcon,
|
||||
Video,
|
||||
FileText,
|
||||
Link2,
|
||||
Users,
|
||||
AppWindowIcon as Window,
|
||||
Plus,
|
||||
X,
|
||||
Upload,
|
||||
} from "lucide-react"
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
|
||||
interface MessageContent {
|
||||
id: string
|
||||
type: "text" | "image" | "video" | "file" | "miniprogram" | "link" | "group"
|
||||
content: string
|
||||
sendInterval?: number
|
||||
intervalUnit?: "seconds" | "minutes"
|
||||
title?: string
|
||||
description?: string
|
||||
address?: string
|
||||
coverImage?: string
|
||||
groupId?: string
|
||||
}
|
||||
|
||||
interface DayPlan {
|
||||
day: number
|
||||
messages: MessageContent[]
|
||||
}
|
||||
|
||||
interface MessageSettingsProps {
|
||||
formData: any
|
||||
onChange: (data: any) => void
|
||||
onNext: () => void
|
||||
onPrev: () => void
|
||||
}
|
||||
|
||||
// 消息类型配置
|
||||
const messageTypes = [
|
||||
{ id: "text", icon: MessageSquare, label: "文本" },
|
||||
{ id: "image", icon: ImageIcon, label: "图片" },
|
||||
{ id: "video", icon: Video, label: "视频" },
|
||||
{ id: "file", icon: FileText, label: "文件" },
|
||||
{ id: "miniprogram", icon: Window, label: "小程序" },
|
||||
{ id: "link", icon: Link2, label: "链<><E993BE><EFBFBD>" },
|
||||
{ id: "group", icon: Users, label: "邀请入群" },
|
||||
]
|
||||
|
||||
// 模拟群组数据
|
||||
const mockGroups = [
|
||||
{ id: "1", name: "产品交流群1", memberCount: 156 },
|
||||
{ id: "2", name: "产品交流群2", memberCount: 234 },
|
||||
{ id: "3", name: "产品交流群3", memberCount: 89 },
|
||||
]
|
||||
|
||||
export function MessageSettings({ formData, onChange, onNext, onPrev }: MessageSettingsProps) {
|
||||
const [dayPlans, setDayPlans] = useState<DayPlan[]>([
|
||||
{
|
||||
day: 0,
|
||||
messages: [
|
||||
{
|
||||
id: "1",
|
||||
type: "text",
|
||||
content: "",
|
||||
sendInterval: 5,
|
||||
intervalUnit: "minutes",
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
const [isAddDayPlanOpen, setIsAddDayPlanOpen] = useState(false)
|
||||
const [isGroupSelectOpen, setIsGroupSelectOpen] = useState(false)
|
||||
const [selectedGroupId, setSelectedGroupId] = useState("")
|
||||
|
||||
// 添加新消息
|
||||
const handleAddMessage = (dayIndex: number, type = "text") => {
|
||||
const updatedPlans = [...dayPlans]
|
||||
updatedPlans[dayIndex].messages.push({
|
||||
id: Date.now().toString(),
|
||||
type: type as MessageContent["type"],
|
||||
content: "",
|
||||
sendInterval: 5,
|
||||
intervalUnit: "minutes",
|
||||
})
|
||||
setDayPlans(updatedPlans)
|
||||
onChange({ ...formData, messagePlans: updatedPlans })
|
||||
}
|
||||
|
||||
// 更新消息内容
|
||||
const handleUpdateMessage = (dayIndex: number, messageIndex: number, updates: Partial<MessageContent>) => {
|
||||
const updatedPlans = [...dayPlans]
|
||||
updatedPlans[dayIndex].messages[messageIndex] = {
|
||||
...updatedPlans[dayIndex].messages[messageIndex],
|
||||
...updates,
|
||||
}
|
||||
setDayPlans(updatedPlans)
|
||||
onChange({ ...formData, messagePlans: updatedPlans })
|
||||
}
|
||||
|
||||
// 删除消息
|
||||
const handleRemoveMessage = (dayIndex: number, messageIndex: number) => {
|
||||
const updatedPlans = [...dayPlans]
|
||||
updatedPlans[dayIndex].messages.splice(messageIndex, 1)
|
||||
setDayPlans(updatedPlans)
|
||||
onChange({ ...formData, messagePlans: updatedPlans })
|
||||
}
|
||||
|
||||
// 添加新的天数计划
|
||||
const handleAddDayPlan = () => {
|
||||
setDayPlans([
|
||||
...dayPlans,
|
||||
{
|
||||
day: dayPlans.length,
|
||||
messages: [
|
||||
{
|
||||
id: Date.now().toString(),
|
||||
type: "text",
|
||||
content: "",
|
||||
sendInterval: 5,
|
||||
intervalUnit: "minutes",
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
setIsAddDayPlanOpen(false)
|
||||
toast({
|
||||
title: "添加成功",
|
||||
description: `已添加第${dayPlans.length}天的消息计划`,
|
||||
})
|
||||
}
|
||||
|
||||
// 选择群组
|
||||
const handleSelectGroup = (groupId: string) => {
|
||||
setSelectedGroupId(groupId)
|
||||
setIsGroupSelectOpen(false)
|
||||
toast({
|
||||
title: "选择成功",
|
||||
description: `已选择群组:${mockGroups.find((g) => g.id === groupId)?.name}`,
|
||||
})
|
||||
}
|
||||
|
||||
// 处理文件上传
|
||||
const handleFileUpload = (dayIndex: number, messageIndex: number, type: "image" | "video" | "file") => {
|
||||
// 模拟文件上传
|
||||
toast({
|
||||
title: "上传成功",
|
||||
description: `${type === "image" ? "图片" : type === "video" ? "视频" : "文件"}上传成功`,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold">消息设置</h2>
|
||||
<Button variant="outline" size="icon" onClick={() => setIsAddDayPlanOpen(true)}>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="0" className="w-full">
|
||||
<TabsList className="w-full">
|
||||
{dayPlans.map((plan) => (
|
||||
<TabsTrigger key={plan.day} value={plan.day.toString()} className="flex-1">
|
||||
{plan.day === 0 ? "即时消息" : `第${plan.day}天`}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
|
||||
{dayPlans.map((plan, dayIndex) => (
|
||||
<TabsContent key={plan.day} value={plan.day.toString()}>
|
||||
<div className="space-y-4">
|
||||
{plan.messages.map((message, messageIndex) => (
|
||||
<div key={message.id} className="space-y-4 p-4 bg-gray-50 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Label>发送间隔</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={message.sendInterval}
|
||||
onChange={(e) =>
|
||||
handleUpdateMessage(dayIndex, messageIndex, { sendInterval: Number(e.target.value) })
|
||||
}
|
||||
className="w-20"
|
||||
/>
|
||||
<Select
|
||||
value={message.intervalUnit}
|
||||
onValueChange={(value) =>
|
||||
handleUpdateMessage(dayIndex, messageIndex, {
|
||||
intervalUnit: value as "seconds" | "minutes",
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[100px]">
|
||||
<SelectValue placeholder="选择单位" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="seconds">秒</SelectItem>
|
||||
<SelectItem value="minutes">分钟</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onClick={() => handleRemoveMessage(dayIndex, messageIndex)}>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2 bg-white p-2 rounded-lg">
|
||||
{messageTypes.map((type) => (
|
||||
<Button
|
||||
key={type.id}
|
||||
variant={message.type === type.id ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => handleUpdateMessage(dayIndex, messageIndex, { type: type.id as any })}
|
||||
className="flex flex-col items-center p-2 h-auto"
|
||||
>
|
||||
<type.icon className="h-4 w-4" />
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{message.type === "text" && (
|
||||
<Textarea
|
||||
value={message.content}
|
||||
onChange={(e) => handleUpdateMessage(dayIndex, messageIndex, { content: e.target.value })}
|
||||
placeholder="请输入消息内容"
|
||||
className="min-h-[100px]"
|
||||
/>
|
||||
)}
|
||||
|
||||
{message.type === "miniprogram" && (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>
|
||||
标题<span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
value={message.title}
|
||||
onChange={(e) => handleUpdateMessage(dayIndex, messageIndex, { title: e.target.value })}
|
||||
placeholder="请输入小程序标题"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>描述</Label>
|
||||
<Input
|
||||
value={message.description}
|
||||
onChange={(e) =>
|
||||
handleUpdateMessage(dayIndex, messageIndex, { description: e.target.value })
|
||||
}
|
||||
placeholder="请输入小程序描述"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>
|
||||
地址<span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
value={message.address}
|
||||
onChange={(e) => handleUpdateMessage(dayIndex, messageIndex, { address: e.target.value })}
|
||||
placeholder="请输入小程序路径"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>
|
||||
封面<span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<div className="border-2 border-dashed rounded-lg p-4 text-center">
|
||||
{message.coverImage ? (
|
||||
<div className="relative">
|
||||
<img
|
||||
src={message.coverImage || "/placeholder.svg"}
|
||||
alt="封面"
|
||||
className="max-w-[200px] mx-auto rounded-lg"
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="absolute top-2 right-2"
|
||||
onClick={() => handleUpdateMessage(dayIndex, messageIndex, { coverImage: undefined })}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full h-[120px]"
|
||||
onClick={() => handleFileUpload(dayIndex, messageIndex, "image")}
|
||||
>
|
||||
<Upload className="h-4 w-4 mr-2" />
|
||||
上传封面
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{message.type === "group" && (
|
||||
<div className="space-y-2">
|
||||
<Label>
|
||||
选择群聊<span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-start"
|
||||
onClick={() => setIsGroupSelectOpen(true)}
|
||||
>
|
||||
{selectedGroupId ? mockGroups.find((g) => g.id === selectedGroupId)?.name : "选择邀请入的群"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(message.type === "image" || message.type === "video" || message.type === "file") && (
|
||||
<div className="border-2 border-dashed rounded-lg p-4 text-center">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full h-[120px]"
|
||||
onClick={() => handleFileUpload(dayIndex, messageIndex, message.type as any)}
|
||||
>
|
||||
<Upload className="h-4 w-4 mr-2" />
|
||||
上传{message.type === "image" ? "图片" : message.type === "video" ? "视频" : "文件"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Button variant="outline" onClick={() => handleAddMessage(dayIndex)} className="w-full">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
添加消息
|
||||
</Button>
|
||||
</div>
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
|
||||
<div className="flex justify-between pt-4">
|
||||
<Button variant="outline" onClick={onPrev}>
|
||||
上一步
|
||||
</Button>
|
||||
<Button onClick={onNext}>下一步</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 添加天数计划弹窗 */}
|
||||
<Dialog open={isAddDayPlanOpen} onOpenChange={setIsAddDayPlanOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>添加消息计划</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
<p className="text-sm text-gray-500 mb-4">选择要添加的消息计划类型</p>
|
||||
<Button onClick={handleAddDayPlan} className="w-full">
|
||||
添加第 {dayPlans.length} 天计划
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 选择群聊弹窗 */}
|
||||
<Dialog open={isGroupSelectOpen} onOpenChange={setIsGroupSelectOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>选择群聊</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
<div className="space-y-2">
|
||||
{mockGroups.map((group) => (
|
||||
<div
|
||||
key={group.id}
|
||||
className={`p-4 rounded-lg cursor-pointer hover:bg-gray-100 ${
|
||||
selectedGroupId === group.id ? "bg-blue-50 border border-blue-200" : ""
|
||||
}`}
|
||||
onClick={() => handleSelectGroup(group.id)}
|
||||
>
|
||||
<div className="font-medium">{group.name}</div>
|
||||
<div className="text-sm text-gray-500">成员数:{group.memberCount}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsGroupSelectOpen(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={() => setIsGroupSelectOpen(false)}>确定</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
284
app/plans/new/steps/TagSettings.tsx
Normal file
284
app/plans/new/steps/TagSettings.tsx
Normal file
@@ -0,0 +1,284 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card } from "@/components/ui/card"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Users, UserPlus, X } from "lucide-react"
|
||||
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "@/components/ui/table"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
|
||||
interface TeamMember {
|
||||
id: string
|
||||
name: string
|
||||
role: string
|
||||
status: "active" | "inactive"
|
||||
}
|
||||
|
||||
interface OrderTag {
|
||||
phone: string
|
||||
wechat: string
|
||||
source?: string
|
||||
orderAmount?: number
|
||||
orderDate?: string
|
||||
}
|
||||
|
||||
interface WechatFriend {
|
||||
id: string
|
||||
nickname: string
|
||||
wechatId: string
|
||||
remark: string
|
||||
addedDate: string
|
||||
}
|
||||
|
||||
interface TagSettingsProps {
|
||||
formData: any
|
||||
onChange: (data: any) => void
|
||||
onComplete: () => void
|
||||
onPrev?: () => void
|
||||
}
|
||||
|
||||
export function TagSettings({ formData, onChange, onComplete, onPrev }: TagSettingsProps) {
|
||||
const router = useRouter()
|
||||
const [teamMembers, setTeamMembers] = useState<TeamMember[]>(formData.teamMembers || [])
|
||||
const [selectedRole, setSelectedRole] = useState("member")
|
||||
const [newMemberName, setNewMemberName] = useState("")
|
||||
const [importedTags, setImportedTags] = useState<OrderTag[]>([])
|
||||
const [wechatFriends, setWechatFriends] = useState<WechatFriend[]>([])
|
||||
const [activeTab, setActiveTab] = useState(formData.sourceWechatId ? "friends" : "team")
|
||||
|
||||
// 如果是从微信号好友转移过来,加载好友数据
|
||||
useEffect(() => {
|
||||
if (formData.sourceWechatId) {
|
||||
// 模拟加载微信好友数据
|
||||
const mockFriends = Array.from({ length: 50 }, (_, i) => ({
|
||||
id: `friend-${i + 1}`,
|
||||
nickname: `好友${i + 1}`,
|
||||
wechatId: `wxid_${Math.random().toString(36).substr(2, 8)}`,
|
||||
remark: Math.random() > 0.5 ? `备注${i + 1}` : "",
|
||||
addedDate: new Date(Date.now() - Math.floor(Math.random() * 10000000000)).toLocaleDateString(),
|
||||
}))
|
||||
setWechatFriends(mockFriends)
|
||||
setActiveTab("friends")
|
||||
}
|
||||
}, [formData.sourceWechatId])
|
||||
|
||||
const handleAddMember = () => {
|
||||
if (newMemberName.trim()) {
|
||||
const newMember: TeamMember = {
|
||||
id: Date.now().toString(),
|
||||
name: newMemberName,
|
||||
role: selectedRole,
|
||||
status: "active",
|
||||
}
|
||||
const updatedMembers = [...teamMembers, newMember]
|
||||
setTeamMembers(updatedMembers)
|
||||
onChange({ ...formData, teamMembers: updatedMembers })
|
||||
setNewMemberName("")
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemoveMember = (id: string) => {
|
||||
const updatedMembers = teamMembers.filter((member) => member.id !== id)
|
||||
setTeamMembers(updatedMembers)
|
||||
onChange({ ...formData, teamMembers: updatedMembers })
|
||||
}
|
||||
|
||||
const handleToggleStatus = (id: string) => {
|
||||
const updatedMembers = teamMembers.map((member) => {
|
||||
if (member.id === id) {
|
||||
return {
|
||||
...member,
|
||||
status: member.status === "active" ? "inactive" : "active",
|
||||
}
|
||||
}
|
||||
return member
|
||||
})
|
||||
setTeamMembers(updatedMembers)
|
||||
onChange({ ...formData, teamMembers: updatedMembers })
|
||||
}
|
||||
|
||||
const handleComplete = () => {
|
||||
// 根据当前标签页确定要保存的数据
|
||||
if (activeTab === "friends" && wechatFriends.length > 0) {
|
||||
// 将微信好友转换为标签
|
||||
const friendTags = wechatFriends.map((friend) => ({
|
||||
phone: "",
|
||||
wechat: friend.wechatId,
|
||||
source: formData.sourceWechatId,
|
||||
orderAmount: undefined,
|
||||
orderDate: friend.addedDate,
|
||||
}))
|
||||
onChange({ ...formData, importedTags: friendTags })
|
||||
} else if (activeTab === "import" && importedTags.length > 0) {
|
||||
onChange({ ...formData, importedTags })
|
||||
}
|
||||
|
||||
onComplete()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="team">打粉团队</TabsTrigger>
|
||||
<TabsTrigger value="import">标签导入</TabsTrigger>
|
||||
{formData.sourceWechatId && <TabsTrigger value="friends">微信好友</TabsTrigger>}
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="team" className="mt-4">
|
||||
<Card className="p-4">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
value={newMemberName}
|
||||
onChange={(e) => setNewMemberName(e.target.value)}
|
||||
placeholder="输入成员名称"
|
||||
/>
|
||||
</div>
|
||||
<Select value={selectedRole} onValueChange={setSelectedRole}>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectValue placeholder="选择角色" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="leader">组长</SelectItem>
|
||||
<SelectItem value="member">组员</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button onClick={handleAddMember}>
|
||||
<UserPlus className="h-4 w-4 mr-2" />
|
||||
添加成员
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{teamMembers.map((member) => (
|
||||
<div key={member.id} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">{member.name}</span>
|
||||
<Badge variant="outline">{member.role === "leader" ? "组长" : "组员"}</Badge>
|
||||
<Badge
|
||||
variant={member.status === "active" ? "default" : "secondary"}
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleToggleStatus(member.id)}
|
||||
>
|
||||
{member.status === "active" ? "已启用" : "已停用"}
|
||||
</Badge>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onClick={() => handleRemoveMember(member.id)}>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="import" className="mt-4">
|
||||
<Card className="p-4">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-medium">标签导入</h3>
|
||||
</div>
|
||||
|
||||
{importedTags.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<h4 className="text-sm font-medium mb-2">已导入 {importedTags.length} 条数据</h4>
|
||||
<div className="max-h-[300px] overflow-auto border rounded-md">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>电话号码</TableHead>
|
||||
<TableHead>微信号</TableHead>
|
||||
<TableHead>来源</TableHead>
|
||||
<TableHead>订单金额</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{importedTags.slice(0, 5).map((tag, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>{tag.phone}</TableCell>
|
||||
<TableCell>{tag.wechat}</TableCell>
|
||||
<TableCell>{tag.source}</TableCell>
|
||||
<TableCell>{tag.orderAmount}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{importedTags.length > 5 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} className="text-center text-gray-500">
|
||||
还有 {importedTags.length - 5} 条数据未显示
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{formData.sourceWechatId && (
|
||||
<TabsContent value="friends" className="mt-4">
|
||||
<Card className="p-4">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Users className="h-5 w-5 text-blue-500" />
|
||||
<h3 className="text-lg font-medium">微信好友列表</h3>
|
||||
</div>
|
||||
<Badge variant="outline">共 {wechatFriends.length} 位好友</Badge>
|
||||
</div>
|
||||
|
||||
<div className="max-h-[400px] overflow-auto border rounded-md">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>昵称</TableHead>
|
||||
<TableHead>微信号</TableHead>
|
||||
<TableHead>备注</TableHead>
|
||||
<TableHead>添加时间</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{wechatFriends.map((friend) => (
|
||||
<TableRow key={friend.id}>
|
||||
<TableCell>{friend.nickname}</TableCell>
|
||||
<TableCell>{friend.wechatId}</TableCell>
|
||||
<TableCell>{friend.remark || "-"}</TableCell>
|
||||
<TableCell>{friend.addedDate}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-gray-500">
|
||||
<p>• 这些好友将被导入为订单标签</p>
|
||||
<p>• 导入后可用于创建获客计划</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
)}
|
||||
</Tabs>
|
||||
|
||||
<div className="flex justify-between">
|
||||
{onPrev && (
|
||||
<Button variant="outline" onClick={onPrev}>
|
||||
上一步
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={handleComplete} className={onPrev ? "ml-auto" : "w-full"}>
|
||||
完成
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user