存客宝 React

This commit is contained in:
柳清爽
2025-03-29 16:50:39 +08:00
parent caea0b4b99
commit 7e7c199996
388 changed files with 53282 additions and 2076 deletions

View File

@@ -0,0 +1,99 @@
"use client"
import { useState, useEffect } from "react"
import { ResponsiveContainer, Tooltip, XAxis, YAxis, CartesianGrid, Area, AreaChart, Legend } from "recharts"
interface AcquisitionPlanChartProps {
data: { date: string; customers: number }[]
}
export function AcquisitionPlanChart({ data }: AcquisitionPlanChartProps) {
const [chartData, setChartData] = useState<any[]>([])
// 生成更真实的数据
useEffect(() => {
if (!data || data.length === 0) return
// 使用获客计划中的获客数和添加数作为指标模拟近7天数据
const enhancedData = data.map((item) => {
// 添加数通常是获客数的一定比例这里使用70%-90%的随机比例
const addRate = 0.7 + Math.random() * 0.2
const addedCount = Math.round(item.customers * addRate)
return {
date: item.date,
获客数: item.customers,
添加数: addedCount,
}
})
setChartData(enhancedData)
}, [data])
// 如果没有数据,显示空状态
if (!data || data.length === 0 || chartData.length === 0) {
return <div className="h-[180px] flex items-center justify-center text-gray-400"></div>
}
return (
<div className="h-[180px]">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={chartData} margin={{ top: 5, right: 5, left: 0, bottom: 5 }}>
<defs>
<linearGradient id="colorCustomers" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.8} />
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0.1} />
</linearGradient>
<linearGradient id="colorAdded" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#8b5cf6" stopOpacity={0.8} />
<stop offset="95%" stopColor="#8b5cf6" stopOpacity={0.1} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#f0f0f0" />
<XAxis dataKey="date" axisLine={false} tickLine={false} tick={{ fontSize: 12, fill: "#6b7280" }} />
<YAxis axisLine={false} tickLine={false} tick={{ fontSize: 12, fill: "#6b7280" }} width={30} />
<Tooltip
contentStyle={{
backgroundColor: "rgba(255, 255, 255, 0.9)",
borderRadius: "6px",
boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)",
border: "none",
padding: "8px",
}}
labelStyle={{ fontWeight: "bold", marginBottom: "4px" }}
/>
<Legend
verticalAlign="top"
height={36}
iconType="circle"
iconSize={8}
formatter={(value) => <span style={{ color: "#6b7280", fontSize: "12px" }}>{value}</span>}
/>
<Area
type="monotone"
dataKey="获客数"
name="获客数"
stroke="#3b82f6"
fillOpacity={1}
fill="url(#colorCustomers)"
strokeWidth={2}
dot={{ r: 3, strokeWidth: 2 }}
activeDot={{ r: 5, strokeWidth: 0 }}
/>
<Area
type="monotone"
dataKey="添加数"
name="添加数"
stroke="#8b5cf6"
fillOpacity={0.5}
fill="url(#colorAdded)"
strokeWidth={2}
dot={{ r: 3, strokeWidth: 2 }}
activeDot={{ r: 5, strokeWidth: 0 }}
/>
</AreaChart>
</ResponsiveContainer>
</div>
)
}

View File

@@ -0,0 +1,55 @@
"use client"
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts"
interface DailyAcquisitionData {
date: string
acquired: number
added: number
}
interface DailyAcquisitionChartProps {
data: DailyAcquisitionData[]
height?: number
}
export function DailyAcquisitionChart({ data, height = 200 }: DailyAcquisitionChartProps) {
return (
<div className="w-full" style={{ height: `${height}px` }}>
<ResponsiveContainer width="100%" height="100%">
<LineChart data={data} margin={{ top: 10, right: 30, left: 0, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
<XAxis dataKey="date" stroke="#666" />
<YAxis stroke="#666" />
<Tooltip
contentStyle={{
backgroundColor: "white",
border: "1px solid #e5e7eb",
borderRadius: "6px",
}}
/>
<Legend />
<Line
type="monotone"
dataKey="acquired"
name="获客数量"
stroke="#3b82f6"
strokeWidth={2}
dot={{ fill: "#3b82f6" }}
activeDot={{ r: 6 }}
/>
<Line
type="monotone"
dataKey="added"
name="添加成功"
stroke="#10b981"
strokeWidth={2}
dot={{ fill: "#10b981" }}
activeDot={{ r: 6 }}
/>
</LineChart>
</ResponsiveContainer>
</div>
)
}

View File

@@ -0,0 +1,75 @@
"use client"
import { useState } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis, CartesianGrid } from "recharts"
// 模拟数据
const weeklyData = [
{ day: "周一", 获客数: 12, 添加好友: 8 },
{ day: "周二", 获客数: 18, 添加好友: 12 },
{ day: "周三", 获客数: 15, 添加好友: 10 },
{ day: "周四", 获客数: 25, 添加好友: 18 },
{ day: "周五", 获客数: 30, 添加好友: 22 },
{ day: "周六", 获客数: 18, 添加好友: 14 },
{ day: "周日", 获客数: 15, 添加好友: 11 },
]
const monthlyData = [
{ day: "1月", 获客数: 120, 添加好友: 85 },
{ day: "2月", 获客数: 180, 添加好友: 130 },
{ day: "3月", 获客数: 150, 添加好友: 110 },
{ day: "4月", 获客数: 250, 添加好友: 180 },
{ day: "5月", 获客数: 300, 添加好友: 220 },
{ day: "6月", 获客数: 280, 添加好友: 210 },
]
export function DeviceTreeChart() {
const [period, setPeriod] = useState("week")
const data = period === "week" ? weeklyData : monthlyData
return (
<Card className="w-full mt-4">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-base font-medium"></CardTitle>
<Tabs defaultValue="week" value={period} onValueChange={setPeriod} className="h-9">
<TabsList className="grid w-[180px] grid-cols-2">
<TabsTrigger value="week"></TabsTrigger>
<TabsTrigger value="month"></TabsTrigger>
</TabsList>
</Tabs>
</div>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={250}>
<LineChart data={data} margin={{ top: 5, right: 10, left: 0, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" vertical={false} />
<XAxis dataKey="day" axisLine={false} tickLine={false} tick={{ fontSize: 12 }} />
<YAxis axisLine={false} tickLine={false} tick={{ fontSize: 12 }} />
<Tooltip />
<Line
type="monotone"
dataKey="获客数"
stroke="#4f46e5"
strokeWidth={2}
dot={{ r: 4 }}
activeDot={{ r: 6 }}
/>
<Line
type="monotone"
dataKey="添加好友"
stroke="#10b981"
strokeWidth={2}
dot={{ r: 4 }}
activeDot={{ r: 6 }}
/>
</LineChart>
</ResponsiveContainer>
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,205 @@
"use client"
import { useState } from "react"
import { ChevronDown, ChevronUp, MoreVertical, Copy, Pencil, Trash2, Clock, Link } from "lucide-react"
import { Button } from "@/components/ui/button"
import { useRouter } from "next/navigation"
import { Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis, CartesianGrid } from "recharts"
import { Card } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { DailyAcquisitionChart } from "./DailyAcquisitionChart"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
interface Task {
id: string
name: string
status: "running" | "paused" | "completed"
stats: {
devices: number
acquired: number
added: number
}
lastUpdated: string
executionTime: string
nextExecutionTime: string
trend: { date: string; customers: number }[]
dailyData?: { date: string; acquired: number; added: number }[]
}
interface ExpandableAcquisitionCardProps {
task: Task
channel: string
onCopy: (taskId: string) => void
onDelete: (taskId: string) => void
onOpenSettings?: (taskId: string) => void
onStatusChange?: (taskId: string, newStatus: "running" | "paused") => void
}
export function ExpandableAcquisitionCard({
task,
channel,
onCopy,
onDelete,
onOpenSettings,
onStatusChange,
}: ExpandableAcquisitionCardProps) {
const router = useRouter()
const [expanded, setExpanded] = useState(false)
const { devices: deviceCount, acquired: acquiredCount, added: addedCount } = task.stats
const passRate = calculatePassRate(acquiredCount, addedCount)
const handleEdit = (taskId: string) => {
router.push(`/scenarios/${channel}/edit/${taskId}`)
}
const toggleTaskStatus = () => {
if (onStatusChange) {
onStatusChange(task.id, task.status === "running" ? "paused" : "running")
}
}
return (
<div className="mb-6">
<Card className="p-6 hover:shadow-lg transition-all bg-white/80 backdrop-blur-sm">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center space-x-3">
<h3 className="font-medium text-lg">{task.name}</h3>
<Badge
variant={task.status === "running" ? "success" : "secondary"}
className="cursor-pointer hover:opacity-80"
onClick={toggleTaskStatus}
>
{task.status === "running" ? "进行中" : "已暂停"}
</Badge>
</div>
<div className="relative z-20">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8 hover:bg-gray-100 rounded-full">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuItem onClick={() => handleEdit(task.id)}>
<Pencil className="w-4 h-4 mr-2" />
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onCopy(task.id)}>
<Copy className="w-4 h-4 mr-2" />
</DropdownMenuItem>
{onOpenSettings && (
<DropdownMenuItem onClick={() => onOpenSettings(task.id)}>
<Link className="w-4 h-4 mr-2" />
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={() => onDelete(task.id)} className="text-red-600">
<Trash2 className="w-4 h-4 mr-2" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<div className="grid grid-cols-4 gap-2 mb-4">
<a href={`/scenarios/${channel}/devices`}>
<Card className="p-2 hover:bg-gray-50 transition-colors cursor-pointer">
<div className="text-sm text-gray-500 mb-1"></div>
<div className="text-2xl font-semibold">{deviceCount}</div>
</Card>
</a>
<a href={`/scenarios/${channel}/acquired`}>
<Card className="p-2 hover:bg-gray-50 transition-colors cursor-pointer">
<div className="text-sm text-gray-500 mb-1"></div>
<div className="text-2xl font-semibold">{acquiredCount}</div>
</Card>
</a>
<a href={`/scenarios/${channel}/added`}>
<Card className="p-2 hover:bg-gray-50 transition-colors cursor-pointer">
<div className="text-sm text-gray-500 mb-1"></div>
<div className="text-2xl font-semibold">{addedCount}</div>
</Card>
</a>
<Card className="p-2">
<div className="text-sm text-gray-500 mb-1"></div>
<div className="text-2xl font-semibold">{passRate}%</div>
</Card>
</div>
<div className="h-48 bg-white rounded-lg p-4 mb-4">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={task.trend}>
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
<XAxis dataKey="date" stroke="#666" />
<YAxis stroke="#666" />
<Tooltip
contentStyle={{
backgroundColor: "white",
border: "1px solid #e5e7eb",
borderRadius: "6px",
}}
/>
<Line
type="monotone"
dataKey="customers"
name="获客数"
stroke="#3b82f6"
strokeWidth={2}
dot={{ fill: "#3b82f6" }}
/>
</LineChart>
</ResponsiveContainer>
</div>
<div className="flex items-center justify-between text-sm border-t pt-4 text-gray-500">
<div className="flex items-center space-x-2">
<Clock className="w-4 h-4" />
<span>{task.lastUpdated}</span>
</div>
<div className="flex items-center space-x-2">
<Clock className="w-4 h-4" />
<span>{task.nextExecutionTime}</span>
</div>
</div>
</Card>
{expanded && task.dailyData && (
<div className="mt-4 bg-white p-6 rounded-lg shadow-sm">
<h4 className="text-lg font-medium mb-4"></h4>
<div className="h-64">
<DailyAcquisitionChart data={task.dailyData} />
</div>
</div>
)}
<div className="flex justify-center mt-2">
<Button variant="ghost" size="sm" onClick={() => setExpanded(!expanded)} className="text-gray-500">
{expanded ? (
<>
<ChevronUp className="h-4 w-4 mr-1" />
</>
) : (
<>
<ChevronDown className="h-4 w-4 mr-1" />
</>
)}
</Button>
</div>
</div>
)
}
// 计算通过率
function calculatePassRate(acquired: number, added: number) {
if (acquired === 0) return 0
return Math.round((added / acquired) * 100)
}

View File

@@ -0,0 +1,97 @@
"use client"
import type React from "react"
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 { TrafficTeamSettings } from "@/app/components/TrafficTeamSettings"
interface NewAcquisitionPlanFormProps {
onSubmit: (data: any) => void
onCancel: () => void
}
export function NewAcquisitionPlanForm({ onSubmit, onCancel }: NewAcquisitionPlanFormProps) {
const [formData, setFormData] = useState({
name: "",
description: "",
trafficTeams: [], // Initialize trafficTeams as an empty array
})
const [currentStep, setCurrentStep] = useState(1)
const handleChange = (data: any) => {
setFormData(data)
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
onSubmit(formData)
}
return (
<form onSubmit={handleSubmit} className="space-y-6">
<div className="steps-container">
{/* 步骤指示器 */}
<div className="step-indicators flex justify-between mb-8">
{[1, 2, 3, 4].map((step) => (
<div
key={step}
className={`flex flex-col items-center ${currentStep >= step ? "text-primary" : "text-gray-400"}`}
>
<div
className={`flex items-center justify-center w-8 h-8 rounded-full mb-2 ${
currentStep >= step ? "bg-primary text-white" : "bg-gray-100 text-gray-400"
}`}
>
{step}
</div>
<div className="text-sm font-medium">
{step === 1 && "基础设置"}
{step === 2 && "好友设置"}
{step === 3 && "消息设置"}
{step === 4 && "流量标签"}
</div>
</div>
))}
</div>
</div>
<Card className="p-6">
<div className="space-y-4">
<h2 className="text-lg font-semibold"></h2>
<div className="space-y-2">
<Label></Label>
<Input
placeholder="输入计划名称"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
/>
</div>
<div className="space-y-2">
<Label></Label>
<Input
placeholder="输入计划描述"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
/>
</div>
</div>
</Card>
<TrafficTeamSettings formData={formData} onChange={handleChange} />
<div className="flex justify-end space-x-4">
<Button type="button" variant="outline" onClick={onCancel}>
</Button>
<Button type="submit"></Button>
</div>
</form>
)
}

View File

@@ -0,0 +1,202 @@
"use client"
import { Card } from "@/components/ui/card"
import type React from "react"
import { useState, useRef, useEffect } from "react"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { MoreHorizontal, Copy, Pencil, Trash2, Clock, Link } from "lucide-react"
interface Task {
id: string
name: string
status: "running" | "paused" | "completed"
stats: {
devices: number
acquired: number
added: number
}
lastUpdated: string
executionTime: string
nextExecutionTime: string
trend: { date: string; customers: number }[]
}
interface ScenarioAcquisitionCardProps {
task: Task
channel: string
onEdit: (taskId: string) => void
onCopy: (taskId: string) => void
onDelete: (taskId: string) => void
onOpenSettings?: (taskId: string) => void
onStatusChange?: (taskId: string, newStatus: "running" | "paused") => void
}
export function ScenarioAcquisitionCard({
task,
channel,
onEdit,
onCopy,
onDelete,
onOpenSettings,
onStatusChange,
}: ScenarioAcquisitionCardProps) {
const { devices: deviceCount, acquired: acquiredCount, added: addedCount } = task.stats
const passRate = calculatePassRate(acquiredCount, addedCount)
const [menuOpen, setMenuOpen] = useState(false)
const menuRef = useRef<HTMLDivElement>(null)
const handleStatusChange = (e: React.MouseEvent) => {
e.stopPropagation()
if (onStatusChange) {
onStatusChange(task.id, task.status === "running" ? "paused" : "running")
}
}
const handleEdit = (e: React.MouseEvent) => {
e.stopPropagation()
setMenuOpen(false)
onEdit(task.id)
}
const handleCopy = (e: React.MouseEvent) => {
e.stopPropagation()
setMenuOpen(false)
onCopy(task.id)
}
const handleOpenSettings = (e: React.MouseEvent) => {
e.stopPropagation()
setMenuOpen(false)
if (onOpenSettings) {
onOpenSettings(task.id)
}
}
const handleDelete = (e: React.MouseEvent) => {
e.stopPropagation()
setMenuOpen(false)
onDelete(task.id)
}
const toggleMenu = (e: React.MouseEvent) => {
e.stopPropagation()
setMenuOpen(!menuOpen)
}
// 点击外部关闭菜单
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setMenuOpen(false)
}
}
document.addEventListener("mousedown", handleClickOutside)
return () => {
document.removeEventListener("mousedown", handleClickOutside)
}
}, [])
return (
<Card className="p-6 hover:shadow-lg transition-all mb-4 bg-white/80">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center space-x-3">
<h3 className="font-medium text-lg">{task.name}</h3>
<Badge
variant={task.status === "running" ? "success" : "secondary"}
className="cursor-pointer hover:opacity-80"
onClick={handleStatusChange}
>
{task.status === "running" ? "进行中" : "已暂停"}
</Badge>
</div>
<div className="relative z-20" ref={menuRef}>
<Button variant="ghost" size="icon" className="h-8 w-8 hover:bg-gray-100 rounded-full" onClick={toggleMenu}>
<MoreHorizontal className="h-4 w-4" />
</Button>
{menuOpen && (
<div className="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg z-50 py-1 border">
<button
className="flex items-center w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
onClick={handleEdit}
>
<Pencil className="w-4 h-4 mr-2" />
</button>
<button
className="flex items-center w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
onClick={handleCopy}
>
<Copy className="w-4 h-4 mr-2" />
</button>
{onOpenSettings && (
<button
className="flex items-center w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
onClick={handleOpenSettings}
>
<Link className="w-4 h-4 mr-2" />
</button>
)}
<button
className="flex items-center w-full px-4 py-2 text-sm text-red-600 hover:bg-gray-100"
onClick={handleDelete}
>
<Trash2 className="w-4 h-4 mr-2" />
</button>
</div>
)}
</div>
</div>
<div className="grid grid-cols-4 gap-2 mb-4">
<a href={`/scenarios/${channel}/devices`} className="block">
<Card className="p-2 hover:bg-gray-50 transition-colors cursor-pointer">
<div className="text-sm text-gray-500 mb-1"></div>
<div className="text-2xl font-semibold">{deviceCount}</div>
</Card>
</a>
<a href={`/scenarios/${channel}/acquired`} className="block">
<Card className="p-2 hover:bg-gray-50 transition-colors cursor-pointer">
<div className="text-sm text-gray-500 mb-1"></div>
<div className="text-2xl font-semibold">{acquiredCount}</div>
</Card>
</a>
<a href={`/scenarios/${channel}/added`} className="block">
<Card className="p-2 hover:bg-gray-50 transition-colors cursor-pointer">
<div className="text-sm text-gray-500 mb-1"></div>
<div className="text-2xl font-semibold">{addedCount}</div>
</Card>
</a>
<Card className="p-2">
<div className="text-sm text-gray-500 mb-1"></div>
<div className="text-2xl font-semibold">{passRate}%</div>
</Card>
</div>
<div className="flex items-center justify-between text-sm border-t pt-4 text-gray-500">
<div className="flex items-center space-x-2">
<Clock className="w-4 h-4" />
<span>{task.lastUpdated}</span>
</div>
<div className="flex items-center space-x-2">
<Clock className="w-4 h-4" />
<span>{task.nextExecutionTime}</span>
</div>
</div>
</Card>
)
}
// 计算通过率
function calculatePassRate(acquired: number, added: number) {
if (acquired === 0) return 0
return Math.round((added / acquired) * 100)
}