"use client"
import type React from "react"
import { useState, useEffect, useRef } 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, Settings, Minus } 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,
DialogDescription,
} from "@/components/ui/dialog"
import { toast } from "@/components/ui/use-toast"
import { useSearchParams } from "next/navigation"
interface BasicSettingsProps {
formData: any
onChange: (data: any) => void
onNext?: () => void
scenarios: any[]
}
interface Account {
id: string
nickname: string
avatar: string
}
interface Material {
id: string
name: string
type: string
preview: string
}
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,
}))
}
// 颜色池分为更浅的未选中和深色的选中
const tagColorPoolLight = [
"bg-blue-100 text-blue-600",
"bg-green-100 text-green-600",
"bg-purple-100 text-purple-600",
"bg-red-100 text-red-600",
"bg-orange-100 text-orange-600",
"bg-yellow-100 text-yellow-600",
"bg-gray-100 text-gray-600",
"bg-pink-100 text-pink-600",
];
const tagColorPoolDark = [
"bg-blue-100 text-blue-600",
"bg-green-100 text-green-600",
"bg-purple-100 text-purple-600",
"bg-red-100 text-red-600",
"bg-orange-100 text-orange-600",
"bg-yellow-100 text-yellow-600",
"bg-gray-100 text-gray-600",
"bg-pink-100 text-pink-600",
];
function getTagColorIdx(tag: string) {
let hash = 0;
for (let i = 0; i < tag.length; i++) {
hash = tag.charCodeAt(i) + ((hash << 5) - hash);
}
return Math.abs(hash) % tagColorPoolLight.length;
}
// Section组件示例
const PosterSection = ({ materials, selectedMaterials, onUpload, onSelect, uploading, fileInputRef, onFileChange, onPreview, onRemove }) => (
{materials.map((material) => (
m.id === material.id)
? "ring-2 ring-blue-600"
: "hover:ring-2 hover:ring-blue-600"
}`}
onClick={() => onSelect(material)}
>
))}
{selectedMaterials.length > 0 && (
![{selectedMaterials[0].name}]({selectedMaterials[0].preview)
onPreview(selectedMaterials[0].preview)}
/>
)}
)
const OrderSection = ({ materials, onUpload, uploading, fileInputRef, onFileChange }) => (
{materials.map((item) => (
))}
)
const DouyinSection = ({ materials, onUpload, uploading, fileInputRef, onFileChange }) => (
{materials.map((item) => (
))}
)
const PlaceholderSection = ({ title }) => (
{title}功能区待开发
)
export function BasicSettings({ formData, onChange, onNext, scenarios }: BasicSettingsProps) {
const [isAccountDialogOpen, setIsAccountDialogOpen] = useState(false)
const [isMaterialDialogOpen, setIsMaterialDialogOpen] = useState(false)
const [isQRCodeOpen, setIsQRCodeOpen] = useState(false)
const [isPreviewOpen, setIsPreviewOpen] = useState(false)
const [isPhoneSettingsOpen, setIsPhoneSettingsOpen] = useState(false)
const [previewImage, setPreviewImage] = useState("")
const [accounts] = useState(generateRandomAccounts(50))
const [materials, setMaterials] = useState(generatePosterMaterials())
const [selectedAccounts, setSelectedAccounts] = useState(
formData.accounts?.length > 0 ? formData.accounts : [],
)
const [selectedMaterials, setSelectedMaterials] = useState(
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 || [])
const [selectedScenarioTags, setSelectedScenarioTags] = useState(formData.scenarioTags || [])
const [customTagInput, setCustomTagInput] = useState("")
const [customTags, setCustomTags] = useState(formData.customTags || [])
// 初始化电话获客设置
const [phoneSettings, setPhoneSettings] = useState({
autoAdd: formData.phoneSettings?.autoAdd ?? true,
speechToText: formData.phoneSettings?.speechToText ?? true,
questionExtraction: formData.phoneSettings?.questionExtraction ?? true,
})
const [selectedPhoneTags, setSelectedPhoneTags] = useState(formData.phoneTags || [])
const [phoneCallType, setPhoneCallType] = useState(formData.phoneCallType || "both")
const fileInputRef = useRef(null)
const [uploadingPoster, setUploadingPoster] = useState(false)
// 新增不同场景的materials和上传逻辑
const [orderMaterials, setOrderMaterials] = useState([])
const [douyinMaterials, setDouyinMaterials] = useState([])
const orderFileInputRef = useRef(null)
const douyinFileInputRef = useRef(null)
const [uploadingOrder, setUploadingOrder] = useState(false)
const [uploadingDouyin, setUploadingDouyin] = useState(false)
// 新增小程序和链接封面上传相关state和ref
const [miniAppCover, setMiniAppCover] = useState(formData.miniAppCover || "")
const [uploadingMiniAppCover, setUploadingMiniAppCover] = useState(false)
const miniAppFileInputRef = useRef(null)
const [linkCover, setLinkCover] = useState(formData.linkCover || "")
const [uploadingLinkCover, setUploadingLinkCover] = useState(false)
const linkFileInputRef = useRef(null)
const searchParams = useSearchParams()
const type = searchParams.get("type")
// 类型映射表
const typeMap: Record = {
haibao: "poster",
douyin: "douyin",
kuaishou: "kuaishou",
xiaohongshu: "xiaohongshu",
weibo: "weibo",
phone: "phone",
gongzhonghao: "gongzhonghao",
weixinqun: "weixinqun",
payment: "payment",
api: "api",
order: "order"
}
const realType = typeMap[type] || type
const filteredScenarios = scenarios.filter(scene => scene.type === realType)
// 只在有唯一匹配时自动选中,否则不自动选中
useEffect(() => {
if (filteredScenarios.length === 1 && formData.sceneId !== filteredScenarios[0].id) {
onChange({ sceneId: filteredScenarios[0].id })
}
}, [filteredScenarios, formData.sceneId, onChange])
// 展示所有场景
const displayedScenarios = scenarios
const handleTagToggle = (tag: string) => {
const newTags = selectedPhoneTags.includes(tag)
? selectedPhoneTags.filter((t) => t !== tag)
: [...selectedPhoneTags, tag]
setSelectedPhoneTags(newTags)
onChange({ ...formData, phoneTags: newTags })
}
const handleCallTypeChange = (type: string) => {
setPhoneCallType(type)
onChange({ ...formData, phoneCallType: type })
}
useEffect(() => {
if (!formData.scenario) {
onChange({ ...formData, scenario: "haibao" })
}
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) => {
if (scenarioId === "phone") {
const today = new Date().toLocaleDateString("zh-CN").replace(/\//g, "")
onChange({ ...formData, scenario: scenarioId, planName: `电话获客${today}` })
} else {
onChange({ ...formData, scenario: scenarioId })
}
}
const handleScenarioTagToggle = (tag: string) => {
const newTags = selectedScenarioTags.includes(tag)
? selectedScenarioTags.filter((t) => t !== tag)
: [...selectedScenarioTags, tag]
setSelectedScenarioTags(newTags)
onChange({ ...formData, scenarioTags: newTags })
}
const handleAddCustomTag = () => {
if (!customTagInput.trim()) return
const newTag = customTagInput.trim()
if (customTags.includes(newTag)) return
const updatedCustomTags = [...customTags, newTag]
setCustomTags(updatedCustomTags)
setCustomTagInput("")
onChange({ ...formData, customTags: updatedCustomTags })
}
const handleRemoveCustomTag = (tag: string) => {
const updatedCustomTags = customTags.filter((t) => t !== tag)
setCustomTags(updatedCustomTags)
onChange({ ...formData, customTags: updatedCustomTags })
// 同时从选中标签中移除
const updatedSelectedTags = selectedScenarioTags.filter((t) => t !== tag)
setSelectedScenarioTags(updatedSelectedTags)
onChange({ ...formData, scenarioTags: updatedSelectedTags, customTags: updatedCustomTags })
}
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 handleFileImport = (event: React.ChangeEvent) => {
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)
}
const handlePhoneSettingsUpdate = () => {
onChange({ ...formData, phoneSettings })
setIsPhoneSettingsOpen(false)
}
const currentScenario = scenarios.find((s: any) => s.id === formData.scenario);
const handleUploadPoster = () => {
fileInputRef.current?.click()
}
const handlePosterFileChange = async (event: React.ChangeEvent) => {
const file = event.target.files?.[0]
if (!file) return
if (!file.type.startsWith('image/')) {
toast({ title: '请选择图片文件', variant: 'destructive' })
return
}
setUploadingPoster(true)
const formData = new FormData()
formData.append('file', file)
try {
const token = localStorage.getItem('token')
const headers: HeadersInit = {}
if (token) headers['Authorization'] = `Bearer ${token}`
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/v1/attachment/upload`, {
method: 'POST',
headers,
body: formData,
})
const result = await response.json()
if (result.code === 200 && result.data?.url) {
const newPoster = {
id: `custom_${Date.now()}`,
name: result.data.name || '自定义海报',
preview: result.data.url,
}
setMaterials(prev => [newPoster, ...prev])
toast({ title: '上传成功', description: '海报已添加' })
} else {
toast({ title: '上传失败', description: result.msg || '请重试', variant: 'destructive' })
}
} catch (e: any) {
toast({ title: '上传失败', description: e?.message || '请重试', variant: 'destructive' })
} finally {
setUploadingPoster(false)
if (fileInputRef.current) fileInputRef.current.value = ''
}
}
const handleUploadOrder = () => { orderFileInputRef.current?.click() }
const handleUploadDouyin = () => { douyinFileInputRef.current?.click() }
const handleOrderFileChange = async (event: React.ChangeEvent) => {
const file = event.target.files?.[0]
if (!file) return
setUploadingOrder(true)
const formData = new FormData()
formData.append('file', file)
try {
const token = localStorage.getItem('token')
const headers: HeadersInit = {}
if (token) headers['Authorization'] = `Bearer ${token}`
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/v1/attachment/upload`, {
method: 'POST', headers, body: formData,
})
const result = await response.json()
if (result.code === 200 && result.data?.url) {
const newItem = { id: `order_${Date.now()}`, name: result.data.name || '自定义订单', preview: result.data.url }
setOrderMaterials(prev => [newItem, ...prev])
toast({ title: '上传成功', description: '订单模板已添加' })
} else {
toast({ title: '上传失败', description: result.msg || '请重试', variant: 'destructive' })
}
} catch (e: any) {
toast({ title: '上传失败', description: e?.message || '请重试', variant: 'destructive' })
} finally {
setUploadingOrder(false)
if (orderFileInputRef.current) orderFileInputRef.current.value = ''
}
}
const handleDouyinFileChange = async (event: React.ChangeEvent) => {
const file = event.target.files?.[0]
if (!file) return
setUploadingDouyin(true)
const formData = new FormData()
formData.append('file', file)
try {
const token = localStorage.getItem('token')
const headers: HeadersInit = {}
if (token) headers['Authorization'] = `Bearer ${token}`
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/v1/attachment/upload`, {
method: 'POST', headers, body: formData,
})
const result = await response.json()
if (result.code === 200 && result.data?.url) {
const newItem = { id: `douyin_${Date.now()}`, name: result.data.name || '自定义抖音内容', preview: result.data.url }
setDouyinMaterials(prev => [newItem, ...prev])
toast({ title: '上传成功', description: '抖音内容已添加' })
} else {
toast({ title: '上传失败', description: result.msg || '请重试', variant: 'destructive' })
}
} catch (e: any) {
toast({ title: '上传失败', description: e?.message || '请重试', variant: 'destructive' })
} finally {
setUploadingDouyin(false)
if (douyinFileInputRef.current) douyinFileInputRef.current.value = ''
}
}
// 上传小程序封面
const handleUploadMiniAppCover = () => {
miniAppFileInputRef.current?.click()
}
const handleMiniAppFileChange = async (event: React.ChangeEvent) => {
const file = event.target.files?.[0]
if (!file) return
setUploadingMiniAppCover(true)
const formDataObj = new FormData()
formDataObj.append('file', file)
try {
const token = localStorage.getItem('token')
const headers: HeadersInit = {}
if (token) headers['Authorization'] = `Bearer ${token}`
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/v1/attachment/upload`, {
method: 'POST', headers, body: formDataObj,
})
const result = await response.json()
if (result.code === 200 && result.data?.url) {
setMiniAppCover(result.data.url)
onChange({ ...formData, miniAppCover: result.data.url })
toast({ title: '上传成功', description: '小程序封面已添加' })
} else {
toast({ title: '上传失败', description: result.msg || '请重试', variant: 'destructive' })
}
} catch (e: any) {
toast({ title: '上传失败', description: e?.message || '请重试', variant: 'destructive' })
} finally {
setUploadingMiniAppCover(false)
if (miniAppFileInputRef.current) miniAppFileInputRef.current.value = ''
}
}
// 上传链接封面
const handleUploadLinkCover = () => {
linkFileInputRef.current?.click()
}
const handleLinkFileChange = async (event: React.ChangeEvent) => {
const file = event.target.files?.[0]
if (!file) return
setUploadingLinkCover(true)
const formDataObj = new FormData()
formDataObj.append('file', file)
try {
const token = localStorage.getItem('token')
const headers: HeadersInit = {}
if (token) headers['Authorization'] = `Bearer ${token}`
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/v1/attachment/upload`, {
method: 'POST', headers, body: formDataObj,
})
const result = await response.json()
if (result.code === 200 && result.data?.url) {
setLinkCover(result.data.url)
onChange({ ...formData, linkCover: result.data.url })
toast({ title: '上传成功', description: '链接封面已添加' })
} else {
toast({ title: '上传失败', description: result.msg || '请重试', variant: 'destructive' })
}
} catch (e: any) {
toast({ title: '上传失败', description: e?.message || '请重试', variant: 'destructive' })
} finally {
setUploadingLinkCover(false)
if (linkFileInputRef.current) linkFileInputRef.current.value = ''
}
}
const renderSceneExtra = () => {
switch (currentScenario?.name) {
case "海报获客":
return (
)
case "订单获客":
return (
)
case "抖音获客":
return (
)
case "小红书获客":
return
case "电话获客":
return
case "公众号获客":
return
case "微信群获客":
return
case "付款码获客":
return
case "API获客":
return
case "小程序获客":
return
case "链接获客":
return
default:
return null
}
}
// 新增小程序和链接场景的功能区
const MiniAppSection = () => (
)
const LinkSection = () => (
)
return (
{displayedScenarios.map((scenario) => (
))}
{!showAllScenarios && (
)}
onChange({ ...formData, name: e.target.value })}
placeholder="请输入计划名称"
className="w-full"
/>
{formData.scenario && (
{(scenarios.find((s) => s.id === formData.scenario)?.scenarioTags || []).map((tag: string) => {
const idx = getTagColorIdx(tag);
const selected = selectedScenarioTags.includes(tag);
return (
handleScenarioTagToggle(tag)}
>
{tag}
);
})}
{customTags.length > 0 && (
{customTags.map((tag) => (
handleScenarioTagToggle(tag)}
>
{tag}
))}
)}
{selectedScenarioTags.length > 0 && (
已选择 {selectedScenarioTags.length} 个标签
)}
)}
{formData.scenario && (
<>
{scenarios.find((s) => s.id === formData.scenario)?.type === "social" &&
formData.scenario !== "phone" && (
{selectedAccounts.length > 0 && (
{selectedAccounts.map((account) => (
{account.nickname}
))}
)}
)}
{formData.scenario === "phone" && (
{phoneSettings.autoAdd ? "已开启" : "已关闭"}
{phoneSettings.speechToText ? "已开启" : "已关闭"}
{phoneSettings.questionExtraction ? "已开启" : "已关闭"}
提示:电话获客功能将自动记录来电信息,并根据设置执行相应操作
)}
{formData.scenario === "phone" && (
<>
handleCallTypeChange(phoneCallType === "inbound" ? "both" : "outbound")}
>
发起外呼
主动向客户发起电话
handleCallTypeChange(phoneCallType === "outbound" ? "both" : "inbound")}
>
接收来电
接听客户的来电
{(scenarios.find((s: any) => s.id === formData.scenario)?.scenarioTags || []).map((tag: string) => {
const idx = getTagColorIdx(tag);
const selected = selectedPhoneTags.includes(tag);
return (
handleTagToggle(tag)}
>
{tag}
);
})}
>
)}
{((currentScenario?.type === "material" || currentScenario?.name === "海报获客" || currentScenario?.id === 1) && (
{renderSceneExtra()}
))}
{scenarios.find((s: any) => s.id === formData.scenario)?.id === "order" && (
{importedTags.length > 0 && (
已导入 {importedTags.length} 条数据
电话号码
微信号
来源
订单金额
{importedTags.slice(0, 5).map((tag, index) => (
{tag.phone}
{tag.wechat}
{tag.source}
{tag.orderAmount}
))}
{importedTags.length > 5 && (
还有 {importedTags.length - 5} 条数据未显示
)}
)}
)}
{formData.scenario === "weixinqun" && (
<>
onChange({ ...formData, autoWelcome: checked })}
/>
onChange({ ...formData, activityMonitor: checked })}
/>
{formData.syncCount || 3}
条内容到群
建议每日推送3-5条内容,避免过度打扰群成员
{["上午 9:00-12:00", "下午 14:00-17:00", "晚上 19:00-21:00"].map((time, index) => (
{
const currentTimes = formData.pushTimes || []
const newTimes = currentTimes.includes(index)
? currentTimes.filter((t: number) => t !== index)
: [...currentTimes, index]
onChange({ ...formData, pushTimes: newTimes })
}}
>
{time}
))}
>
)}
>
)}
onChange({ ...formData, enabled: checked })}
/>
)
}