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>
572 lines
23 KiB
TypeScript
572 lines
23 KiB
TypeScript
"use client"
|
||
|
||
import { useState } from "react"
|
||
import { ChevronLeft, MoreVertical, Copy, Pencil, Trash2, Phone, Mic, Clock, Settings } from "lucide-react"
|
||
import { Card } from "@/components/ui/card"
|
||
import { Button } from "@/components/ui/button"
|
||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts"
|
||
import { useRouter } from "next/navigation"
|
||
import { Badge } from "@/components/ui/badge"
|
||
import { toast } from "@/components/ui/use-toast"
|
||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||
import { Switch } from "@/components/ui/switch"
|
||
import { Label } from "@/components/ui/label"
|
||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"
|
||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "@/components/ui/table"
|
||
|
||
interface Task {
|
||
id: string
|
||
name: string
|
||
status: "running" | "paused" | "completed"
|
||
stats: {
|
||
calls: number
|
||
acquired: number
|
||
added: number
|
||
}
|
||
lastUpdated: string
|
||
trend: { date: string; calls: number }[]
|
||
}
|
||
|
||
interface CallRecord {
|
||
id: string
|
||
phoneNumber: string
|
||
duration: string
|
||
time: string
|
||
status: "已添加" | "未添加" | "处理中"
|
||
question: string
|
||
}
|
||
|
||
export default function PhoneAcquisitionPage() {
|
||
const router = useRouter()
|
||
|
||
const [tasks, setTasks] = useState<Task[]>([
|
||
{
|
||
id: "1",
|
||
name: "招商电话获客",
|
||
status: "running",
|
||
stats: {
|
||
calls: 42,
|
||
acquired: 38,
|
||
added: 31,
|
||
},
|
||
lastUpdated: "2024-03-18 15:30",
|
||
trend: Array.from({ length: 7 }, (_, i) => ({
|
||
date: `3月${String(i + 12)}日`,
|
||
calls: Math.floor(Math.random() * 10) + 5,
|
||
})),
|
||
},
|
||
])
|
||
|
||
const [callRecords, setCallRecords] = useState<CallRecord[]>([
|
||
{
|
||
id: "call-1",
|
||
phoneNumber: "138****1234",
|
||
duration: "3分42秒",
|
||
time: "2024-03-18 15:30",
|
||
status: "已添加",
|
||
question: "请问贵公司的产品有什么特点?",
|
||
},
|
||
{
|
||
id: "call-2",
|
||
phoneNumber: "139****5678",
|
||
duration: "5分18秒",
|
||
time: "2024-03-18 14:15",
|
||
status: "已添加",
|
||
question: "你们的合作方式是怎样的?",
|
||
},
|
||
{
|
||
id: "call-3",
|
||
phoneNumber: "137****9012",
|
||
duration: "2分55秒",
|
||
time: "2024-03-18 11:22",
|
||
status: "已添加",
|
||
question: "能详细介绍一下你们的服务吗?",
|
||
},
|
||
{
|
||
id: "call-4",
|
||
phoneNumber: "135****3456",
|
||
duration: "4分10秒",
|
||
time: "2024-03-17 16:45",
|
||
status: "已添加",
|
||
question: "你们有什么优惠政策?",
|
||
},
|
||
{
|
||
id: "call-5",
|
||
phoneNumber: "136****7890",
|
||
duration: "1分30秒",
|
||
time: "2024-03-17 10:20",
|
||
status: "未添加",
|
||
question: "未识别到有效问题",
|
||
},
|
||
])
|
||
|
||
const [isSettingsOpen, setIsSettingsOpen] = useState(false)
|
||
const [autoAddEnabled, setAutoAddEnabled] = useState(true)
|
||
const [speechToTextEnabled, setSpeechToTextEnabled] = useState(true)
|
||
const [questionExtractionEnabled, setQuestionExtractionEnabled] = useState(true)
|
||
const [isCallDetailOpen, setIsCallDetailOpen] = useState(false)
|
||
const [selectedCall, setSelectedCall] = useState<CallRecord | null>(null)
|
||
|
||
const toggleTaskStatus = (taskId: string) => {
|
||
setTasks(
|
||
tasks.map((task) => {
|
||
if (task.id === taskId) {
|
||
const newStatus = task.status === "running" ? "paused" : "running"
|
||
return { ...task, status: newStatus }
|
||
}
|
||
return task
|
||
}),
|
||
)
|
||
}
|
||
|
||
const handleEditPlan = (taskId: string) => {
|
||
router.push(`/scenarios/phone/edit/${taskId}`)
|
||
}
|
||
|
||
const handleCopyPlan = (taskId: string) => {
|
||
const taskToCopy = tasks.find((task) => task.id === taskId)
|
||
if (taskToCopy) {
|
||
const newTask = {
|
||
...taskToCopy,
|
||
id: `${Date.now()}`,
|
||
name: `${taskToCopy.name} (副本)`,
|
||
status: "paused" as const,
|
||
}
|
||
setTasks([...tasks, newTask])
|
||
toast({
|
||
title: "计划已复制",
|
||
description: `已成功复制"${taskToCopy.name}"`,
|
||
})
|
||
}
|
||
}
|
||
|
||
const handleDeletePlan = (taskId: string) => {
|
||
const taskToDelete = tasks.find((t) => t.id === taskId)
|
||
if (taskToDelete) {
|
||
setTasks(tasks.filter((t) => t.id !== taskId))
|
||
toast({
|
||
title: "计划已删除",
|
||
description: `已成功删除"${taskToDelete.name}"`,
|
||
})
|
||
}
|
||
}
|
||
|
||
const handleViewCallDetail = (call: CallRecord) => {
|
||
setSelectedCall(call)
|
||
setIsCallDetailOpen(true)
|
||
}
|
||
|
||
// 计算通过率
|
||
const calculatePassRate = (acquired: number, added: number) => {
|
||
if (acquired === 0) return 0
|
||
return Math.round((added / acquired) * 100)
|
||
}
|
||
|
||
return (
|
||
<div className="flex-1 bg-gradient-to-b from-blue-50 to-white min-h-screen">
|
||
<header className="sticky top-0 z-10 bg-white/80 backdrop-blur-sm border-b">
|
||
<div className="flex items-center p-4">
|
||
<div className="flex items-center space-x-3">
|
||
<Button variant="ghost" size="icon" onClick={() => router.back()}>
|
||
<ChevronLeft className="h-5 w-5" />
|
||
</Button>
|
||
<h1 className="text-xl font-semibold text-blue-600">电话获客</h1>
|
||
</div>
|
||
<Button variant="ghost" size="icon" className="ml-auto" onClick={() => setIsSettingsOpen(true)}>
|
||
<Settings className="h-5 w-5" />
|
||
</Button>
|
||
</div>
|
||
</header>
|
||
|
||
<div className="p-4 max-w-7xl mx-auto">
|
||
{tasks.map((task) => {
|
||
const { calls: callCount, acquired: acquiredCount, added: addedCount } = task.stats
|
||
const passRate = calculatePassRate(acquiredCount, addedCount)
|
||
|
||
return (
|
||
<Card key={task.id} className="p-6 hover:shadow-lg transition-all mb-4 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.id)}
|
||
>
|
||
{task.status === "running" ? "进行中" : "已暂停"}
|
||
</Badge>
|
||
</div>
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger asChild>
|
||
<Button variant="ghost" size="icon" className="h-8 w-8 hover:bg-gray-100 rounded-full z-10">
|
||
<MoreVertical className="h-4 w-4" />
|
||
</Button>
|
||
</DropdownMenuTrigger>
|
||
<DropdownMenuContent align="end" className="w-48 z-50">
|
||
<DropdownMenuItem
|
||
onClick={(e) => {
|
||
e.stopPropagation()
|
||
handleEditPlan(task.id)
|
||
}}
|
||
className="cursor-pointer hover:bg-blue-50"
|
||
>
|
||
<Pencil className="w-4 h-4 mr-2" />
|
||
编辑计划
|
||
</DropdownMenuItem>
|
||
<DropdownMenuItem
|
||
onClick={(e) => {
|
||
e.stopPropagation()
|
||
handleCopyPlan(task.id)
|
||
}}
|
||
className="cursor-pointer hover:bg-blue-50"
|
||
>
|
||
<Copy className="w-4 h-4 mr-2" />
|
||
复制计划
|
||
</DropdownMenuItem>
|
||
<DropdownMenuItem
|
||
onClick={(e) => {
|
||
e.stopPropagation()
|
||
handleDeletePlan(task.id)
|
||
}}
|
||
className="text-red-600 cursor-pointer hover:bg-red-50"
|
||
>
|
||
<Trash2 className="w-4 h-4 mr-2" />
|
||
删除计划
|
||
</DropdownMenuItem>
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-4 gap-2 mb-4">
|
||
<Card className="p-2">
|
||
<div className="text-sm text-gray-500 mb-1">来电数</div>
|
||
<div className="text-2xl font-semibold">{callCount}</div>
|
||
</Card>
|
||
|
||
<Card className="p-2">
|
||
<div className="text-sm text-gray-500 mb-1">已获客</div>
|
||
<div className="text-2xl font-semibold">{acquiredCount}</div>
|
||
</Card>
|
||
|
||
<Card className="p-2">
|
||
<div className="text-sm text-gray-500 mb-1">已添加</div>
|
||
<div className="text-2xl font-semibold">{addedCount}</div>
|
||
</Card>
|
||
|
||
<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="calls"
|
||
name="来电数"
|
||
stroke="#3b82f6"
|
||
strokeWidth={2}
|
||
dot={{ fill: "#3b82f6" }}
|
||
/>
|
||
</LineChart>
|
||
</ResponsiveContainer>
|
||
</div>
|
||
|
||
<div className="mt-6">
|
||
<Tabs defaultValue="all" className="w-full">
|
||
<TabsList className="w-full mb-4">
|
||
<TabsTrigger value="all" className="flex-1">
|
||
全部来电
|
||
</TabsTrigger>
|
||
<TabsTrigger value="added" className="flex-1">
|
||
已添加
|
||
</TabsTrigger>
|
||
<TabsTrigger value="pending" className="flex-1">
|
||
未添加
|
||
</TabsTrigger>
|
||
</TabsList>
|
||
|
||
<TabsContent value="all">
|
||
<div className="rounded-lg border overflow-hidden">
|
||
<Table>
|
||
<TableHeader>
|
||
<TableRow>
|
||
<TableHead>电话号码</TableHead>
|
||
<TableHead>通话时长</TableHead>
|
||
<TableHead>来电时间</TableHead>
|
||
<TableHead>状态</TableHead>
|
||
<TableHead>首句问题</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{callRecords.map((record) => (
|
||
<TableRow
|
||
key={record.id}
|
||
className="cursor-pointer hover:bg-gray-50"
|
||
onClick={() => handleViewCallDetail(record)}
|
||
>
|
||
<TableCell className="font-medium">{record.phoneNumber}</TableCell>
|
||
<TableCell>{record.duration}</TableCell>
|
||
<TableCell>{record.time}</TableCell>
|
||
<TableCell>
|
||
<Badge
|
||
variant={
|
||
record.status === "已添加"
|
||
? "success"
|
||
: record.status === "处理中"
|
||
? "outline"
|
||
: "secondary"
|
||
}
|
||
>
|
||
{record.status}
|
||
</Badge>
|
||
</TableCell>
|
||
<TableCell className="max-w-[200px] truncate">
|
||
{record.question || "未识别到问题"}
|
||
</TableCell>
|
||
</TableRow>
|
||
))}
|
||
</TableBody>
|
||
</Table>
|
||
</div>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="added">
|
||
<div className="rounded-lg border overflow-hidden">
|
||
<Table>
|
||
<TableHeader>
|
||
<TableRow>
|
||
<TableHead>电话号码</TableHead>
|
||
<TableHead>通话时长</TableHead>
|
||
<TableHead>来电时间</TableHead>
|
||
<TableHead>状态</TableHead>
|
||
<TableHead>首句问题</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{callRecords
|
||
.filter((record) => record.status === "已添加")
|
||
.map((record) => (
|
||
<TableRow
|
||
key={record.id}
|
||
className="cursor-pointer hover:bg-gray-50"
|
||
onClick={() => handleViewCallDetail(record)}
|
||
>
|
||
<TableCell className="font-medium">{record.phoneNumber}</TableCell>
|
||
<TableCell>{record.duration}</TableCell>
|
||
<TableCell>{record.time}</TableCell>
|
||
<TableCell>
|
||
<Badge variant="success">{record.status}</Badge>
|
||
</TableCell>
|
||
<TableCell className="max-w-[200px] truncate">
|
||
{record.question || "未识别到问题"}
|
||
</TableCell>
|
||
</TableRow>
|
||
))}
|
||
</TableBody>
|
||
</Table>
|
||
</div>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="pending">
|
||
<div className="rounded-lg border overflow-hidden">
|
||
<Table>
|
||
<TableHeader>
|
||
<TableRow>
|
||
<TableHead>电话号码</TableHead>
|
||
<TableHead>通话时长</TableHead>
|
||
<TableHead>来电时间</TableHead>
|
||
<TableHead>状态</TableHead>
|
||
<TableHead>首句问题</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{callRecords
|
||
.filter((record) => record.status !== "已添加")
|
||
.map((record) => (
|
||
<TableRow
|
||
key={record.id}
|
||
className="cursor-pointer hover:bg-gray-50"
|
||
onClick={() => handleViewCallDetail(record)}
|
||
>
|
||
<TableCell className="font-medium">{record.phoneNumber}</TableCell>
|
||
<TableCell>{record.duration}</TableCell>
|
||
<TableCell>{record.time}</TableCell>
|
||
<TableCell>
|
||
<Badge variant={record.status === "处理中" ? "outline" : "secondary"}>
|
||
{record.status}
|
||
</Badge>
|
||
</TableCell>
|
||
<TableCell className="max-w-[200px] truncate">
|
||
{record.question || "未识别到问题"}
|
||
</TableCell>
|
||
</TableRow>
|
||
))}
|
||
</TableBody>
|
||
</Table>
|
||
</div>
|
||
</TabsContent>
|
||
</Tabs>
|
||
</div>
|
||
</Card>
|
||
)
|
||
})}
|
||
</div>
|
||
|
||
{/* 设置对话框 */}
|
||
<Dialog open={isSettingsOpen} onOpenChange={setIsSettingsOpen}>
|
||
<DialogContent className="sm:max-w-md">
|
||
<DialogHeader>
|
||
<DialogTitle>电话获客设置</DialogTitle>
|
||
</DialogHeader>
|
||
<div className="py-4 space-y-4">
|
||
<div className="flex items-center justify-between">
|
||
<div className="space-y-0.5">
|
||
<Label htmlFor="auto-add">自动添加客户</Label>
|
||
<p className="text-sm text-gray-500">来电后自动将客户添加为微信好友</p>
|
||
</div>
|
||
<Switch id="auto-add" checked={autoAddEnabled} onCheckedChange={setAutoAddEnabled} />
|
||
</div>
|
||
|
||
<div className="flex items-center justify-between">
|
||
<div className="space-y-0.5">
|
||
<Label htmlFor="speech-to-text">语音转文字</Label>
|
||
<p className="text-sm text-gray-500">自动将通话内容转换为文字记录</p>
|
||
</div>
|
||
<Switch id="speech-to-text" checked={speechToTextEnabled} onCheckedChange={setSpeechToTextEnabled} />
|
||
</div>
|
||
|
||
<div className="flex items-center justify-between">
|
||
<div className="space-y-0.5">
|
||
<Label htmlFor="question-extraction">问题提取</Label>
|
||
<p className="text-sm text-gray-500">自动从通话中提取客户的首句问题</p>
|
||
</div>
|
||
<Switch
|
||
id="question-extraction"
|
||
checked={questionExtractionEnabled}
|
||
onCheckedChange={setQuestionExtractionEnabled}
|
||
/>
|
||
</div>
|
||
</div>
|
||
<DialogFooter>
|
||
<Button onClick={() => setIsSettingsOpen(false)}>保存设置</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
|
||
{/* 通话详情对话框 */}
|
||
<Dialog open={isCallDetailOpen} onOpenChange={setIsCallDetailOpen}>
|
||
<DialogContent className="sm:max-w-lg">
|
||
<DialogHeader>
|
||
<DialogTitle>通话详情</DialogTitle>
|
||
</DialogHeader>
|
||
{selectedCall && (
|
||
<div className="py-4 space-y-4">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center space-x-2">
|
||
<Phone className="h-5 w-5 text-blue-500" />
|
||
<span className="font-medium">{selectedCall.phoneNumber}</span>
|
||
</div>
|
||
<Badge
|
||
variant={
|
||
selectedCall.status === "已添加"
|
||
? "success"
|
||
: selectedCall.status === "处理中"
|
||
? "outline"
|
||
: "secondary"
|
||
}
|
||
>
|
||
{selectedCall.status}
|
||
</Badge>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div className="space-y-1">
|
||
<p className="text-sm text-gray-500">通话时长</p>
|
||
<p className="font-medium flex items-center">
|
||
<Clock className="h-4 w-4 mr-1 text-gray-400" />
|
||
{selectedCall.duration}
|
||
</p>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<p className="text-sm text-gray-500">来电时间</p>
|
||
<p className="font-medium">{selectedCall.time}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<p className="text-sm text-gray-500">首句问题</p>
|
||
<div className="p-3 bg-gray-50 rounded-lg">
|
||
<p className="font-medium">{selectedCall.question || "未识别到问题"}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<div className="flex items-center justify-between">
|
||
<p className="text-sm text-gray-500">通话记录</p>
|
||
<Badge variant="outline" className="flex items-center">
|
||
<Mic className="h-3 w-3 mr-1" />
|
||
AI转写
|
||
</Badge>
|
||
</div>
|
||
<div className="p-3 bg-gray-50 rounded-lg max-h-[200px] overflow-y-auto">
|
||
<p className="text-sm">
|
||
<span className="font-medium text-blue-600">客服:</span>{" "}
|
||
您好,这里是XX公司客服,请问有什么可以帮到您?
|
||
</p>
|
||
<p className="text-sm mt-2">
|
||
<span className="font-medium text-green-600">客户:</span> {selectedCall.question || "..."}
|
||
</p>
|
||
<p className="text-sm mt-2">
|
||
<span className="font-medium text-blue-600">客服:</span>{" "}
|
||
好的,关于这个问题,我们的产品确实有这方面的优势...
|
||
</p>
|
||
<p className="text-sm mt-2">
|
||
<span className="font-medium text-green-600">客户:</span> 那你们的价格是怎么样的?
|
||
</p>
|
||
<p className="text-sm mt-2">
|
||
<span className="font-medium text-blue-600">客服:</span> 我们有多种套餐可以选择,基础版每月只需...
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{selectedCall.status !== "已添加" && (
|
||
<Button
|
||
className="w-full"
|
||
onClick={() => {
|
||
setCallRecords(
|
||
callRecords.map((record) =>
|
||
record.id === selectedCall.id ? { ...record, status: "已添加" } : record,
|
||
),
|
||
)
|
||
toast({
|
||
title: "添加成功",
|
||
description: `已成功将 ${selectedCall.phoneNumber} 添加为好友`,
|
||
})
|
||
setIsCallDetailOpen(false)
|
||
}}
|
||
>
|
||
手动添加为好友
|
||
</Button>
|
||
)}
|
||
</div>
|
||
)}
|
||
</DialogContent>
|
||
</Dialog>
|
||
</div>
|
||
)
|
||
}
|