Files
users/app/scenarios/phone/page.tsx
v0 2408d50cb0 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>
2025-07-18 13:47:12 +00:00

572 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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>
)
}