plan pages

This commit is contained in:
xavier
2025-06-03 16:37:39 +08:00
parent 0d33ba7e34
commit 1f2a1dab00
62 changed files with 12508 additions and 1984 deletions

View File

@@ -0,0 +1,372 @@
"use client"
import { useState, useEffect } from "react"
import { useParams, useRouter } from "next/navigation"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Skeleton } from "@/components/ui/skeleton"
import { ArrowLeft, Edit, Phone, MessageSquare, FileText } from "lucide-react"
import { Switch } from "@/components/ui/switch"
import { toast } from "@/components/ui/use-toast"
import { LineChart, Line, XAxis, YAxis, CartesianGrid, ResponsiveContainer } from "recharts"
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"
interface ScenarioDetail {
id: string
name: string
type: string
status: "active" | "inactive" | "draft"
createdAt: string
description: string
devices: {
total: number
online: number
}
stats: {
total: number
today: number
conversion: number
history: Array<{
date: string
value: number
}>
}
settings: {
[key: string]: any
}
}
export default function ScenarioDetailPage() {
const params = useParams()
const router = useRouter()
const { channel, id } = params
const [isLoading, setIsLoading] = useState(true)
const [scenario, setScenario] = useState<ScenarioDetail | null>(null)
useEffect(() => {
// 模拟API请求
const fetchScenarioDetail = async () => {
setIsLoading(true)
await new Promise((resolve) => setTimeout(resolve, 1000))
// 生成过去30天的数据
const historyData = Array.from({ length: 30 }, (_, i) => {
const date = new Date()
date.setDate(date.getDate() - (29 - i))
return {
date: date.toISOString().split("T")[0],
value: Math.floor(Math.random() * 20) + 1,
}
})
const mockScenario: ScenarioDetail = {
id: id as string,
name: `${channel === "haibao" ? "海报" : channel === "douyin" ? "抖音" : channel === "phone" ? "电话" : "其他"}获客计划`,
type: channel as string,
status: "active",
createdAt: "2023-05-15",
description: "这是一个用于获取新客户的场景获客计划",
devices: {
total: 12,
online: 8,
},
stats: {
total: 342,
today: 18,
conversion: 0.32,
history: historyData,
},
settings: {
greeting: "你好,请通过我的好友请求",
remarkType: "phone",
addFriendInterval: 60,
enableMessage: true,
message: "您好,很高兴认识您!",
delayTime: 5,
},
}
setScenario(mockScenario)
setIsLoading(false)
}
fetchScenarioDetail()
}, [id, channel])
const handleToggleStatus = () => {
if (!scenario) return
const newStatus = scenario.status === "active" ? "inactive" : "active"
setScenario({ ...scenario, status: newStatus as "active" | "inactive" | "draft" })
toast({
title: `${newStatus === "active" ? "启用" : "停用"}成功`,
description: `场景获客计划已${newStatus === "active" ? "启用" : "停用"}`,
})
}
const handleEdit = () => {
router.push(`/scenarios/${channel}/edit/${id}`)
}
const getScenarioTypeIcon = () => {
switch (channel) {
case "phone":
return <Phone className="h-5 w-5" />
case "order":
return <FileText className="h-5 w-5" />
case "douyin":
return <MessageSquare className="h-5 w-5" />
default:
return <FileText className="h-5 w-5" />
}
}
const getScenarioTypeName = () => {
switch (channel) {
case "haibao":
return "海报获客"
case "douyin":
return "抖音获客"
case "phone":
return "电话获客"
case "xiaohongshu":
return "小红书获客"
case "order":
return "订单获客"
case "weixinqun":
return "微信群获客"
case "gongzhonghao":
return "公众号获客"
default:
return "其他获客"
}
}
const getStatusBadge = (status: string) => {
switch (status) {
case "active":
return <Badge className="bg-green-100 text-green-800 hover:bg-green-100"></Badge>
case "inactive":
return <Badge className="bg-gray-100 text-gray-800 hover:bg-gray-100"></Badge>
case "draft":
return <Badge className="bg-yellow-100 text-yellow-800 hover:bg-yellow-100">稿</Badge>
default:
return null
}
}
if (isLoading) {
return (
<div className="flex-1 bg-gray-50">
<div className="max-w-[390px] mx-auto bg-white min-h-screen">
<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.back()}>
<ArrowLeft className="h-5 w-5" />
</Button>
<Skeleton className="h-6 w-40 ml-2" />
</div>
</header>
<div className="p-4 space-y-4">
<Skeleton className="h-32 w-full" />
<Skeleton className="h-64 w-full" />
<Skeleton className="h-48 w-full" />
</div>
</div>
</div>
)
}
if (!scenario) {
return (
<div className="flex-1 bg-gray-50">
<div className="max-w-[390px] mx-auto bg-white min-h-screen">
<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.back()}>
<ArrowLeft className="h-5 w-5" />
</Button>
<h1 className="ml-2 text-lg font-medium"></h1>
</div>
</header>
<div className="flex flex-col items-center justify-center p-4 h-[80vh]">
<p className="text-gray-500 mb-4"></p>
<Button onClick={() => router.push("/scenarios")}></Button>
</div>
</div>
</div>
)
}
return (
<div className="flex-1 bg-gray-50">
<div className="max-w-[390px] mx-auto bg-white min-h-screen">
<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.back()}>
<ArrowLeft className="h-5 w-5" />
</Button>
<h1 className="ml-2 text-lg font-medium">{scenario.name}</h1>
<div className="ml-auto flex items-center">
<Switch checked={scenario.status === "active"} onCheckedChange={handleToggleStatus} className="mr-2" />
<Button size="sm" variant="outline" onClick={handleEdit}>
<Edit className="h-4 w-4 mr-1" />
</Button>
</div>
</div>
</header>
<div className="p-4 space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center">
{getScenarioTypeIcon()}
<span className="ml-1 text-sm text-gray-500">{getScenarioTypeName()}</span>
</div>
<div className="flex items-center space-x-2">
<span className="text-xs text-gray-500"> {scenario.createdAt}</span>
{getStatusBadge(scenario.status)}
</div>
</div>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-3 gap-2">
<div className="bg-gray-50 p-2 rounded-lg text-center">
<p className="text-xs text-gray-500"></p>
<p className="text-lg font-semibold">{scenario.stats.total}</p>
</div>
<div className="bg-gray-50 p-2 rounded-lg text-center">
<p className="text-xs text-gray-500"></p>
<p className="text-lg font-semibold">{scenario.stats.today}</p>
</div>
<div className="bg-gray-50 p-2 rounded-lg text-center">
<p className="text-xs text-gray-500"></p>
<p className="text-lg font-semibold">{(scenario.stats.conversion * 100).toFixed(1)}%</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-base"></CardTitle>
<CardDescription className="text-xs">30</CardDescription>
</CardHeader>
<CardContent>
<div className="h-48">
<ChartContainer
config={{
value: {
label: "获客数量",
color: "hsl(var(--chart-1))",
},
}}
className="h-full"
>
<ResponsiveContainer width="100%" height="100%">
<LineChart data={scenario.stats.history} margin={{ top: 5, right: 5, left: 0, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" tick={{ fontSize: 10 }} />
<YAxis tick={{ fontSize: 10 }} />
<ChartTooltip content={<ChartTooltipContent />} />
<Line type="monotone" dataKey="value" stroke="var(--color-value)" name="获客数量" />
</LineChart>
</ResponsiveContainer>
</ChartContainer>
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-500"></span>
<span className="font-medium">{scenario.devices.total}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-500">线</span>
<span className="font-medium">{scenario.devices.online}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-500">线</span>
<span className="font-medium">
{((scenario.devices.online / scenario.devices.total) * 100).toFixed(1)}%
</span>
</div>
<Button
variant="outline"
className="w-full mt-2"
size="sm"
onClick={() => router.push(`/scenarios/${channel}/devices`)}
>
</Button>
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2 text-sm">
{scenario.settings.greeting && (
<div className="flex justify-between items-center">
<span className="text-gray-500"></span>
<span className="font-medium">{scenario.settings.greeting}</span>
</div>
)}
{scenario.settings.remarkType && (
<div className="flex justify-between items-center">
<span className="text-gray-500"></span>
<span className="font-medium">
{scenario.settings.remarkType === "phone"
? "手机号"
: scenario.settings.remarkType === "nickname"
? "昵称"
: "来源"}
</span>
</div>
)}
{scenario.settings.addFriendInterval && (
<div className="flex justify-between items-center">
<span className="text-gray-500"></span>
<span className="font-medium">{scenario.settings.addFriendInterval} /</span>
</div>
)}
{scenario.settings.enableMessage && (
<>
<div className="flex justify-between items-center">
<span className="text-gray-500"></span>
<Badge className="bg-green-100 text-green-800 hover:bg-green-100"></Badge>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-500"></span>
<span className="font-medium">{scenario.settings.message}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-500"></span>
<span className="font-medium">{scenario.settings.delayTime} </span>
</div>
</>
)}
</div>
</CardContent>
</Card>
</div>
</div>
</div>
)
}

View File

@@ -1,7 +1,7 @@
"use client"
import { useState } from "react"
import { ChevronLeft, Plus } from "lucide-react"
import { ChevronLeft } from "lucide-react"
import { Card } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
@@ -50,17 +50,13 @@ export default function AcquiredCustomersPage({ params }: { params: { channel: s
return (
<div className="min-h-screen bg-gray-50">
<header className="sticky top-0 z-10 bg-white/80 backdrop-blur-sm border-b">
<div className="flex items-center justify-between p-4">
<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">{channelName}</h1>
</div>
<Button variant="default" onClick={() => router.push(`/scenarios/new`)} className="flex items-center gap-1">
<Plus className="h-4 w-4" />
</Button>
</div>
</header>
@@ -116,4 +112,3 @@ export default function AcquiredCustomersPage({ params }: { params: { channel: s
</div>
)
}

View File

@@ -1,7 +1,7 @@
"use client"
import { useState } from "react"
import { ChevronLeft, Plus } from "lucide-react"
import { ChevronLeft } from "lucide-react"
import { Card } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
@@ -53,17 +53,13 @@ export default function AddedCustomersPage({ params }: { params: { channel: stri
return (
<div className="min-h-screen bg-gray-50">
<header className="sticky top-0 z-10 bg-white/80 backdrop-blur-sm border-b">
<div className="flex items-center justify-between p-4">
<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">{channelName}</h1>
</div>
<Button variant="default" onClick={() => router.push(`/scenarios/new`)} className="flex items-center gap-1">
<Plus className="h-4 w-4" />
</Button>
</div>
</header>
@@ -120,4 +116,3 @@ export default function AddedCustomersPage({ params }: { params: { channel: stri
</div>
)
}

View File

@@ -1,12 +1,10 @@
"use client"
import { useState } from "react"
import { ChevronLeft, Copy, Plus, Trash2 } from "lucide-react"
import { ChevronLeft, Copy, FileText, Play, Info, Link } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Card, CardContent } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { useRouter } from "next/navigation"
import { toast } from "@/components/ui/use-toast"
import {
@@ -17,7 +15,8 @@ import {
DialogDescription,
DialogFooter,
} from "@/components/ui/dialog"
import { Switch } from "@/components/ui/switch"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
// 获取渠道中文名称
const getChannelName = (channel: string) => {
@@ -32,432 +31,361 @@ const getChannelName = (channel: string) => {
return channelMap[channel] || channel
}
interface ApiKey {
id: string
name: string
key: string
createdAt: string
lastUsed: string | null
status: "active" | "inactive"
}
interface Webhook {
id: string
name: string
url: string
events: string[]
createdAt: string
lastTriggered: string | null
status: "active" | "inactive"
}
export default function ApiManagementPage({ params }: { params: { channel: string } }) {
const router = useRouter()
const channel = params.channel
const channelName = getChannelName(channel)
// 模拟API密钥数据
const [apiKeys, setApiKeys] = useState<ApiKey[]>([
{
id: "1",
name: `${channelName}获客API密钥`,
key: `api_${channel}_${Math.random().toString(36).substring(2, 10)}`,
createdAt: "2024-03-20 14:30:00",
lastUsed: "2024-03-21 09:15:22",
status: "active",
},
])
// 模拟Webhook数据
const [webhooks, setWebhooks] = useState<Webhook[]>([
{
id: "1",
name: `${channelName}获客回调`,
url: `https://api.example.com/webhooks/${channel}`,
events: ["customer.created", "customer.updated", "tag.added"],
createdAt: "2024-03-20 14:35:00",
lastTriggered: "2024-03-21 09:16:45",
status: "active",
},
])
// 示例数据
const apiKey = `api_1_b9805j8q`
const apiUrl = `https://kzmoqjnwgjc9q2xbj4np.lite.vusercontent.net/api/scenarios/${channel}/1/webhook`
const testUrl = `${apiUrl}?name=测试客户&phone=13800138000`
// 对话框状态
const [showNewApiKeyDialog, setShowNewApiKeyDialog] = useState(false)
const [showNewWebhookDialog, setShowNewWebhookDialog] = useState(false)
const [newApiKeyName, setNewApiKeyName] = useState("")
const [newWebhookData, setNewWebhookData] = useState({
name: "",
url: "",
events: ["customer.created", "customer.updated", "tag.added"],
})
// 创建新API密钥
const handleCreateApiKey = () => {
if (!newApiKeyName.trim()) {
toast({
title: "错误",
description: "请输入API密钥名称",
variant: "destructive",
})
return
}
const newKey: ApiKey = {
id: `${Date.now()}`,
name: newApiKeyName,
key: `api_${channel}_${Math.random().toString(36).substring(2, 15)}`,
createdAt: new Date().toLocaleString(),
lastUsed: null,
status: "active",
}
setApiKeys([...apiKeys, newKey])
setNewApiKeyName("")
setShowNewApiKeyDialog(false)
toast({
title: "创建成功",
description: "新的API密钥已创建",
variant: "success",
})
}
// 创建新Webhook
const handleCreateWebhook = () => {
if (!newWebhookData.name.trim() || !newWebhookData.url.trim()) {
toast({
title: "错误",
description: "请填写所有必填字段",
variant: "destructive",
})
return
}
const newWebhook: Webhook = {
id: `${Date.now()}`,
name: newWebhookData.name,
url: newWebhookData.url,
events: newWebhookData.events,
createdAt: new Date().toLocaleString(),
lastTriggered: null,
status: "active",
}
setWebhooks([...webhooks, newWebhook])
setNewWebhookData({
name: "",
url: "",
events: ["customer.created", "customer.updated", "tag.added"],
})
setShowNewWebhookDialog(false)
toast({
title: "创建成功",
description: "新的Webhook已创建",
variant: "success",
})
}
// 删除API密钥
const handleDeleteApiKey = (id: string) => {
setApiKeys(apiKeys.filter((key) => key.id !== id))
toast({
title: "删除成功",
description: "API密钥已删除",
variant: "success",
})
}
// 删除Webhook
const handleDeleteWebhook = (id: string) => {
setWebhooks(webhooks.filter((webhook) => webhook.id !== id))
toast({
title: "删除成功",
description: "Webhook已删除",
variant: "success",
})
}
// 切换API密钥状态
const toggleApiKeyStatus = (id: string) => {
setApiKeys(
apiKeys.map((key) => (key.id === id ? { ...key, status: key.status === "active" ? "inactive" : "active" } : key)),
)
}
// 切换Webhook状态
const toggleWebhookStatus = (id: string) => {
setWebhooks(
webhooks.map((webhook) =>
webhook.id === id ? { ...webhook, status: webhook.status === "active" ? "inactive" : "active" } : webhook,
),
)
}
const [showDocDialog, setShowDocDialog] = useState(false)
const [showTestDialog, setShowTestDialog] = useState(false)
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-1 bg-white min-h-screen pb-20">
<header className="sticky top-0 z-10 bg-white shadow-sm">
<div className="flex items-center p-4">
<Button variant="ghost" size="icon" onClick={() => router.back()}>
<ChevronLeft className="h-5 w-5" />
</Button>
<h1 className="text-xl font-semibold text-blue-600 ml-2">{channelName}</h1>
<h1 className="text-lg font-medium ml-2">{channelName}</h1>
</div>
</header>
<div className="p-4 max-w-7xl mx-auto">
<Tabs defaultValue="api-keys" className="w-full">
<div className="p-4 max-w-md mx-auto space-y-5">
{/* 接口位置提示 */}
<div className="bg-blue-50 rounded-lg p-3 flex items-start">
<Info className="h-5 w-5 text-blue-500 mt-0.5 flex-shrink-0" />
<div className="ml-2">
<p className="text-sm text-blue-700">
<span className="font-medium"></span> {channelName}
</p>
</div>
</div>
<Tabs defaultValue="api" className="w-full">
<TabsList className="grid w-full grid-cols-2 mb-4">
<TabsTrigger value="api-keys">API密钥</TabsTrigger>
<TabsTrigger value="webhooks">Webhook</TabsTrigger>
<TabsTrigger value="api"></TabsTrigger>
<TabsTrigger value="params"></TabsTrigger>
</TabsList>
<TabsContent value="api-keys" className="space-y-4">
<div className="flex justify-between items-center">
<h2 className="text-lg font-medium">API密钥管理</h2>
<Button onClick={() => setShowNewApiKeyDialog(true)}>
<Plus className="w-4 h-4 mr-2" />
API密钥
</Button>
</div>
<div className="space-y-4">
{apiKeys.map((apiKey) => (
<Card key={apiKey.id}>
<CardHeader className="pb-2">
<div className="flex justify-between items-start">
<div>
<CardTitle>{apiKey.name}</CardTitle>
<CardDescription> {apiKey.createdAt}</CardDescription>
</div>
<div className="flex items-center space-x-2">
<div className="flex items-center space-x-2">
<Switch
checked={apiKey.status === "active"}
onCheckedChange={() => toggleApiKeyStatus(apiKey.id)}
/>
<span className="text-sm text-gray-500">{apiKey.status === "active" ? "启用" : "禁用"}</span>
</div>
<Button
variant="ghost"
size="icon"
className="text-red-500 hover:text-red-700 hover:bg-red-50"
onClick={() => handleDeleteApiKey(apiKey.id)}
>
<Trash2 className="w-4 h-4" />
<TabsContent value="api" className="space-y-4">
{/* API密钥部分 */}
<div className="space-y-2">
<div className="flex justify-between items-center">
<div className="flex items-center">
<h2 className="text-base font-medium">API密钥</h2>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" className="h-6 w-6 ml-1">
<Info className="h-4 w-4 text-gray-400" />
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Input value={apiKey.key} readOnly className="font-mono text-sm" />
</TooltipTrigger>
<TooltipContent>
<p className="text-xs"></p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className="flex space-x-2">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="icon"
onClick={() => {
navigator.clipboard.writeText(apiKey.key)
toast({
title: "已复制",
description: "API密钥已复制到剪贴板",
variant: "success",
})
}}
className="h-8 w-8"
onClick={() => setShowDocDialog(true)}
>
<Copy className="h-4 w-4" />
<FileText className="h-4 w-4" />
</Button>
</div>
{apiKey.lastUsed && <p className="text-sm text-gray-500">使: {apiKey.lastUsed}</p>}
</div>
</CardContent>
</Card>
))}
</TooltipTrigger>
<TooltipContent>
<p></p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
{apiKeys.length === 0 && (
<div className="text-center py-8 bg-white rounded-lg shadow-sm">
<p className="text-gray-500">API密钥</p>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="icon"
className="h-8 w-8"
onClick={() => setShowTestDialog(true)}
>
<Play className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p></p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
)}
</div>
<Card className="overflow-hidden border-gray-200">
<CardContent className="p-3">
<div className="relative w-full">
<Input value={apiKey} readOnly className="font-mono text-sm pr-10 border-gray-200" />
<Button
variant="ghost"
size="icon"
className="absolute right-0 top-0 h-full"
onClick={() => {
navigator.clipboard.writeText(apiKey)
toast({
title: "已复制",
description: "API密钥已复制到剪贴板",
})
}}
>
<Copy className="h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
</div>
{/* 接口地址部分 */}
<div className="space-y-2">
<div className="flex justify-between items-center">
<div className="flex items-center">
<h2 className="text-base font-medium"></h2>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" className="h-6 w-6 ml-1">
<Link className="h-4 w-4 text-gray-400" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p className="text-xs">POST请求到此地址</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<Button
variant="outline"
size="sm"
className="h-7 text-xs px-2 border-blue-200 text-blue-600 hover:bg-blue-50"
onClick={() => {
navigator.clipboard.writeText(apiUrl)
toast({
title: "已复制",
description: "接口地址已复制到剪贴板",
})
}}
>
<Copy className="h-3 w-3 mr-1" />
</Button>
</div>
<Card className="overflow-hidden border-gray-200">
<CardContent className="p-3">
<div className="w-full">
<div className="font-mono text-xs bg-gray-50 p-2 rounded border border-gray-200 break-all">
{apiUrl}
</div>
</div>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="webhooks" className="space-y-4">
<div className="flex justify-between items-center">
<h2 className="text-lg font-medium">Webhook管理</h2>
<Button onClick={() => setShowNewWebhookDialog(true)}>
<Plus className="w-4 h-4 mr-2" />
Webhook
</Button>
<TabsContent value="params" className="space-y-4">
{/* 必要参数部分 */}
<div className="space-y-2">
<h2 className="text-base font-medium flex items-center">
<span className="text-xs text-red-500 ml-1">*</span>
</h2>
<Card className="overflow-hidden border-gray-200">
<CardContent className="p-3">
<div className="space-y-3">
<div className="flex items-center justify-between">
<div>
<span className="font-medium text-blue-600">name</span>
<span className="text-sm text-gray-500 ml-2">()</span>
</div>
<span className="text-xs bg-blue-50 text-blue-700 px-2 py-1 rounded"></span>
</div>
<div className="flex items-center justify-between">
<div>
<span className="font-medium text-blue-600">phone</span>
<span className="text-sm text-gray-500 ml-2">()</span>
</div>
<span className="text-xs bg-blue-50 text-blue-700 px-2 py-1 rounded"></span>
</div>
</div>
</CardContent>
</Card>
</div>
<div className="space-y-4">
{webhooks.map((webhook) => (
<Card key={webhook.id}>
<CardHeader className="pb-2">
<div className="flex justify-between items-start">
{/* 可选参数部分 */}
<div className="space-y-2">
<h2 className="text-base font-medium"></h2>
<Card className="overflow-hidden border-gray-200">
<CardContent className="p-3">
<div className="space-y-3">
<div className="flex items-center justify-between">
<div>
<CardTitle>{webhook.name}</CardTitle>
<CardDescription> {webhook.createdAt}</CardDescription>
</div>
<div className="flex items-center space-x-2">
<div className="flex items-center space-x-2">
<Switch
checked={webhook.status === "active"}
onCheckedChange={() => toggleWebhookStatus(webhook.id)}
/>
<span className="text-sm text-gray-500">{webhook.status === "active" ? "启用" : "禁用"}</span>
</div>
<Button
variant="ghost"
size="icon"
className="text-red-500 hover:text-red-700 hover:bg-red-50"
onClick={() => handleDeleteWebhook(webhook.id)}
>
<Trash2 className="w-4 h-4" />
</Button>
<span className="font-medium text-gray-600">source</span>
<span className="text-sm text-gray-500 ml-2">()</span>
</div>
<span className="text-xs bg-gray-100 text-gray-700 px-2 py-1 rounded"></span>
</div>
</CardHeader>
<CardContent>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Input value={webhook.url} readOnly className="font-mono text-sm" />
<Button
variant="outline"
size="icon"
onClick={() => {
navigator.clipboard.writeText(webhook.url)
toast({
title: "已复制",
description: "Webhook URL已复制到剪贴板",
variant: "success",
})
}}
>
<Copy className="h-4 w-4" />
</Button>
<div className="flex items-center justify-between">
<div>
<span className="font-medium text-gray-600">remark</span>
<span className="text-sm text-gray-500 ml-2">()</span>
</div>
<div className="flex flex-wrap gap-2 mt-2">
{webhook.events.map((event) => (
<span key={event} className="px-2 py-1 bg-blue-100 text-blue-800 rounded-full text-xs">
{event}
</span>
))}
</div>
{webhook.lastTriggered && (
<p className="text-sm text-gray-500">: {webhook.lastTriggered}</p>
)}
<span className="text-xs bg-gray-100 text-gray-700 px-2 py-1 rounded"></span>
</div>
</CardContent>
</Card>
))}
<div className="flex items-center justify-between">
<div>
<span className="font-medium text-gray-600">tags</span>
<span className="text-sm text-gray-500 ml-2">()</span>
</div>
<span className="text-xs bg-gray-100 text-gray-700 px-2 py-1 rounded"></span>
</div>
</div>
</CardContent>
</Card>
</div>
{webhooks.length === 0 && (
<div className="text-center py-8 bg-white rounded-lg shadow-sm">
<p className="text-gray-500">Webhook</p>
</div>
)}
{/* 示例代码部分 */}
<div className="space-y-2">
<h2 className="text-base font-medium"></h2>
<Card className="overflow-hidden border-gray-200">
<CardContent className="p-3">
<pre className="text-xs bg-gray-50 p-2 rounded border border-gray-200 overflow-x-auto">
{`POST ${apiUrl}
Content-Type: application/json
Authorization: Bearer ${apiKey}
{
"name": "张三",
"phone": "13800138000",
"source": "官网",
"remark": "有意向",
"tags": ["高意向", "新客户"]
}`}
</pre>
</CardContent>
</Card>
</div>
</TabsContent>
</Tabs>
</div>
{/* 创建API密钥对话框 */}
<Dialog open={showNewApiKeyDialog} onOpenChange={setShowNewApiKeyDialog}>
{/* 接口文档对话框 */}
<Dialog open={showDocDialog} onOpenChange={setShowDocDialog}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>API密钥</DialogTitle>
<DialogDescription>API密钥用于访问{channelName}</DialogDescription>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="api-key-name">API密钥名称</Label>
<Input
id="api-key-name"
placeholder="例如:获客系统集成"
value={newApiKeyName}
onChange={(e) => setNewApiKeyName(e.target.value)}
/>
<h3 className="font-medium"></h3>
<p className="text-sm text-gray-600">
{channelName}HTTP POST请求发送客户数据
</p>
</div>
<div className="space-y-2">
<h3 className="font-medium"></h3>
<pre className="text-xs bg-gray-50 p-3 rounded border overflow-x-auto">
POST {apiUrl}
<br />
Content-Type: application/json
<br />
Authorization: Bearer {apiKey}
<br />
<br />
{`{
"name": "客户姓名",
"phone": "13800138000",
"source": "广告投放",
"remark": "有意向购买",
"tags": ["高意向", "新客户"]
}`}
</pre>
</div>
<div className="space-y-2">
<h3 className="font-medium"></h3>
<pre className="text-xs bg-gray-50 p-3 rounded border overflow-x-auto">
{`{
"success": true,
"message": "客户添加成功",
"data": {
"id": "cust_123456",
"name": "客户姓名",
"phone": "13800138000",
"created_at": "2024-03-21T09:15:22Z"
}
}`}
</pre>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowNewApiKeyDialog(false)}>
</Button>
<Button onClick={handleCreateApiKey}></Button>
<Button onClick={() => setShowDocDialog(false)}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 创建Webhook对话框 */}
<Dialog open={showNewWebhookDialog} onOpenChange={setShowNewWebhookDialog}>
{/* 快速测试对话框 */}
<Dialog open={showTestDialog} onOpenChange={setShowTestDialog}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Webhook</DialogTitle>
<DialogDescription>Webhook用于接收{channelName}</DialogDescription>
<DialogTitle></DialogTitle>
<DialogDescription>使URL快速测试接口是否正常工作</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="webhook-name">Webhook名称</Label>
<Input
id="webhook-name"
placeholder="例如CRM系统集成"
value={newWebhookData.name}
onChange={(e) => setNewWebhookData({ ...newWebhookData, name: e.target.value })}
/>
<h3 className="font-medium">URL</h3>
<pre className="text-xs bg-gray-50 p-3 rounded border overflow-x-auto whitespace-pre-wrap break-all">
{testUrl}
</pre>
<p className="text-xs text-gray-500 mt-2">URL复制到浏览器中打开</p>
</div>
<div className="space-y-2">
<Label htmlFor="webhook-url">Webhook URL</Label>
<Input
id="webhook-url"
placeholder="https://example.com/webhook"
value={newWebhookData.url}
onChange={(e) => setNewWebhookData({ ...newWebhookData, url: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label></Label>
<div className="grid grid-cols-1 gap-2">
{["customer.created", "customer.updated", "tag.added", "tag.removed"].map((event) => (
<div key={event} className="flex items-center space-x-2">
<Switch
checked={newWebhookData.events.includes(event)}
onCheckedChange={(checked) => {
if (checked) {
setNewWebhookData({
...newWebhookData,
events: [...newWebhookData.events, event],
})
} else {
setNewWebhookData({
...newWebhookData,
events: newWebhookData.events.filter((e) => e !== event),
})
}
}}
/>
<span>{event}</span>
</div>
))}
</div>
</div>
<Button
className="w-full"
onClick={() => {
navigator.clipboard.writeText(testUrl)
toast({
title: "已复制",
description: "测试URL已复制到剪贴板",
})
}}
>
<Copy className="h-4 w-4 mr-2" />
</Button>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowNewWebhookDialog(false)}>
<Button variant="outline" onClick={() => setShowTestDialog(false)}>
</Button>
<Button onClick={handleCreateWebhook}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}

View File

@@ -1,4 +1,3 @@
export default function Loading() {
return null
}

View File

@@ -1,26 +1,14 @@
"use client"
import { useState, useEffect } from "react"
import { useState } from "react"
import { useRouter } from "next/navigation"
import { Card } from "@/components/ui/card"
import { DeviceSelector } from "@/app/components/common/DeviceSelector"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { ChevronLeft, Filter, Search, RefreshCw } from "lucide-react"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Badge } from "@/components/ui/badge"
import { Checkbox } from "@/components/ui/checkbox"
import { toast } from "@/components/ui/use-toast"
import type { Device } from "@/types/device"
import { ChevronLeft } from "lucide-react"
export default function ScenarioDevicesPage({ params }: { params: { channel: string } }) {
const router = useRouter()
const [devices, setDevices] = useState<Device[]>([])
const [searchQuery, setSearchQuery] = useState("")
const [statusFilter, setStatusFilter] = useState("all")
const [currentPage, setCurrentPage] = useState(1)
const [selectedDevices, setSelectedDevices] = useState<string[]>([])
const devicesPerPage = 10
const maxDevices = 5
// 获取渠道中文名称
const getChannelName = (channel: string) => {
@@ -29,96 +17,29 @@ export default function ScenarioDevicesPage({ params }: { params: { channel: str
kuaishou: "快手",
xiaohongshu: "小红书",
weibo: "微博",
haibao: "海报",
phone: "电话",
order: "订单",
weixinqun: "微信群",
gongzhonghao: "公众号",
payment: "付款码",
api: "API",
}
return channelMap[channel] || channel
}
const channelName = getChannelName(params.channel)
useEffect(() => {
// 模拟API调用
const fetchDevices = async () => {
const mockDevices = Array.from({ length: 15 }, (_, i) => ({
id: `device-${i + 1}`,
imei: `sd${123123 + i}`,
name: `设备 ${i + 1}`,
remark: `${channelName}获客设备 ${i + 1}`,
status: Math.random() > 0.2 ? "online" : "offline",
battery: Math.floor(Math.random() * 100),
wechatId: `wxid_${Math.random().toString(36).substr(2, 8)}`,
friendCount: Math.floor(Math.random() * 1000),
todayAdded: Math.floor(Math.random() * 50),
messageCount: Math.floor(Math.random() * 200),
lastActive: new Date(Date.now() - Math.random() * 86400000).toLocaleString(),
addFriendStatus: Math.random() > 0.2 ? "normal" : "abnormal",
}))
setDevices(mockDevices)
}
fetchDevices()
}, [channelName])
const handleRefresh = () => {
toast({
title: "刷新成功",
description: "设备列表已更新",
})
}
const handleSelectAll = () => {
if (selectedDevices.length === devices.length || selectedDevices.length === maxDevices) {
setSelectedDevices([])
} else {
const newSelection = devices.slice(0, maxDevices).map((d) => d.id)
setSelectedDevices(newSelection)
}
}
const handleDeviceSelect = (deviceId: string) => {
if (selectedDevices.includes(deviceId)) {
setSelectedDevices(selectedDevices.filter((id) => id !== deviceId))
} else {
if (selectedDevices.length >= maxDevices) {
toast({
title: "选择超出限制",
description: `最多可选择${maxDevices}个设备`,
variant: "destructive",
})
return
}
setSelectedDevices([...selectedDevices, deviceId])
}
}
const handleSave = async () => {
try {
// 这里应该是实际的API调用来保存选中的设备
await new Promise((resolve) => setTimeout(resolve, 1000))
toast({
title: "保存成功",
description: "已更新计划设备",
})
router.back()
} catch (error) {
toast({
title: "保存失败",
description: "更新设备失败,请重试",
variant: "destructive",
})
console.error("保存失败:", error)
}
}
const filteredDevices = devices.filter((device) => {
const matchesSearch =
device.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
device.imei.toLowerCase().includes(searchQuery.toLowerCase()) ||
device.wechatId.toLowerCase().includes(searchQuery.toLowerCase())
const matchesStatus = statusFilter === "all" || device.status === statusFilter
return matchesSearch && matchesStatus
})
const paginatedDevices = filteredDevices.slice((currentPage - 1) * devicesPerPage, currentPage * devicesPerPage)
return (
<div className="flex-1 bg-gray-50 min-h-screen">
<header className="sticky top-0 z-10 bg-white/80 backdrop-blur-sm border-b">
@@ -127,129 +48,20 @@ export default function ScenarioDevicesPage({ params }: { params: { channel: str
<Button variant="ghost" size="icon" onClick={() => router.back()}>
<ChevronLeft className="h-5 w-5" />
</Button>
<h1 className="text-xl font-semibold text-blue-600">{channelName}</h1>
<h1 className="text-xl font-semibold text-blue-600">{channelName}</h1>
</div>
</div>
</header>
<div className="p-4 space-y-4">
<Card className="p-4">
<div className="space-y-3">
<div className="flex items-center space-x-2">
<div className="relative flex-1">
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
<Input
placeholder="搜索设备IMEI/备注"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9"
/>
</div>
<Button variant="outline" size="icon">
<Filter className="h-4 w-4" />
</Button>
<Button variant="outline" size="icon" onClick={handleRefresh}>
<RefreshCw className="h-4 w-4" />
</Button>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-[120px]">
<SelectValue placeholder="全部状态" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="online">线</SelectItem>
<SelectItem value="offline">线</SelectItem>
</SelectContent>
</Select>
</div>
<div className="text-sm text-gray-500">
{selectedDevices.length}/{maxDevices}
</div>
</div>
<div className="flex items-center space-x-2">
<Checkbox
checked={
selectedDevices.length > 0 &&
(selectedDevices.length === devices.length || selectedDevices.length === maxDevices)
}
onCheckedChange={handleSelectAll}
/>
<span className="text-sm"></span>
</div>
<div className="space-y-2">
{paginatedDevices.length === 0 ? (
<div className="text-center py-8 text-gray-500"></div>
) : (
paginatedDevices.map((device) => (
<Card
key={device.id}
className={`p-3 hover:shadow-md transition-shadow cursor-pointer ${
selectedDevices.includes(device.id) ? "ring-2 ring-primary" : ""
}`}
onClick={() => handleDeviceSelect(device.id)}
>
<div className="flex items-center space-x-3">
<Checkbox
checked={selectedDevices.includes(device.id)}
className="mt-1"
onClick={(e) => {
e.stopPropagation()
handleDeviceSelect(device.id)
}}
/>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-1">
<div className="font-medium truncate">{device.name}</div>
<Badge variant={device.status === "online" ? "success" : "secondary"}>
{device.status === "online" ? "在线" : "离线"}
</Badge>
</div>
<div className="text-sm text-gray-500">IMEI: {device.imei}</div>
<div className="text-sm text-gray-500">: {device.wechatId}</div>
<div className="flex items-center justify-between mt-1 text-sm">
<span className="text-gray-500">: {device.friendCount}</span>
<span className="text-gray-500">: +{device.todayAdded}</span>
</div>
</div>
</div>
</Card>
))
)}
</div>
{filteredDevices.length > devicesPerPage && (
<div className="flex justify-between items-center pt-2">
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage((prev) => Math.max(1, prev - 1))}
disabled={currentPage === 1}
>
</Button>
<span className="text-sm text-gray-500">
{currentPage} / {Math.ceil(filteredDevices.length / devicesPerPage)}
</span>
<Button
variant="outline"
size="sm"
onClick={() =>
setCurrentPage((prev) => Math.min(Math.ceil(filteredDevices.length / devicesPerPage), prev + 1))
}
disabled={currentPage === Math.ceil(filteredDevices.length / devicesPerPage)}
>
</Button>
</div>
)}
</div>
</Card>
<div className="p-4">
<DeviceSelector
title={`${channelName}设备选择`}
selectedDevices={selectedDevices}
onDevicesChange={setSelectedDevices}
multiple={true}
maxSelection={5}
className="mb-4"
/>
<div className="fixed bottom-0 left-0 right-0 bg-white p-4 border-t flex justify-end space-x-2">
<Button variant="outline" onClick={() => router.back()}>
@@ -263,4 +75,3 @@ export default function ScenarioDevicesPage({ params }: { params: { channel: str
</div>
)
}

View File

@@ -238,4 +238,3 @@ export default function EditAcquisitionPlan({ params }: { params: { channel: str
</div>
)
}

View File

@@ -1,7 +1,7 @@
"use client"
import { useState, useEffect, useRef } from "react"
import { ChevronLeft, Copy, Link, HelpCircle } from "lucide-react"
import { use, useState } from "react"
import { Copy, Link, HelpCircle, Shield, ChevronLeft, Plus } from "lucide-react"
import { Button } from "@/components/ui/button"
import { useRouter } from "next/navigation"
import { toast } from "@/components/ui/use-toast"
@@ -26,6 +26,7 @@ const getChannelName = (channel: string) => {
xiaohongshu: "小红书",
weibo: "微博",
haibao: "海报",
poster: "海报",
phone: "电话",
gongzhonghao: "公众号",
weixinqun: "微信群",
@@ -35,7 +36,6 @@ const getChannelName = (channel: string) => {
return channelMap[channel] || channel
}
// 恢复Task接口定义
interface Task {
id: string
name: string
@@ -51,21 +51,6 @@ interface Task {
trend: { date: string; customers: number }[]
}
interface PlanItem {
id: number;
name: string;
status: number;
statusText: string;
createTime: number;
createTimeFormat: string;
deviceCount: number;
customerCount: number;
addedCount: number;
passRate: number;
lastExecutionTime: string;
nextExecutionTime: string;
}
interface DeviceStats {
active: number
}
@@ -88,50 +73,23 @@ function ApiDocumentationTooltip() {
)
}
export default function ChannelPage({ params }: { params: { channel: string } }) {
// export default function ChannelPage({ params }: { params: { channel: string } }) {
export default function ChannelPage({ params }: { params: Promise<{ channel: string }> }) {
const router = useRouter()
const channel = params.channel
const channelName = getChannelName(params.channel)
// 从URL query参数获取场景ID
const [sceneId, setSceneId] = useState<number | null>(null);
// 使用ref追踪sceneId值避免重复请求
const sceneIdRef = useRef<number | null>(null);
// 追踪组件是否已挂载
const isMounted = useRef(true);
// 组件卸载时更新挂载状态
useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
// 获取URL中的查询参数
useEffect(() => {
// 组件未挂载,不执行操作
if (!isMounted.current) return;
// 从URL获取id参数
const urlParams = new URLSearchParams(window.location.search);
const idParam = urlParams.get('id');
if (idParam && !isNaN(Number(idParam))) {
setSceneId(Number(idParam));
sceneIdRef.current = Number(idParam);
} else {
// 如果没有传递有效的ID使用函数获取默认ID
const defaultId = getSceneIdFromChannel(channel);
setSceneId(defaultId);
sceneIdRef.current = defaultId;
}
}, [channel]);
// const unwrappedParams = use(params)
const resolvedParams = use(params)
// const channel = params.channel
// const channelName = getChannelName(params.channel)
// const channel = unwrappedParams.channel
// const channelName = getChannelName(unwrappedParams.channel)
const channel = resolvedParams.channel
const channelName = getChannelName(resolvedParams.channel)
const initialTasks = [
const initialTasks: Task[] = [
{
id: "1",
name: `${channelName}直播获客计划`,
status: "running" as const,
status: "running",
stats: {
devices: 5,
acquired: 31,
@@ -148,7 +106,7 @@ export default function ChannelPage({ params }: { params: { channel: string } })
{
id: "2",
name: `${channelName}评论区获客计划`,
status: "paused" as const,
status: "paused",
stats: {
devices: 3,
acquired: 15,
@@ -162,11 +120,9 @@ export default function ChannelPage({ params }: { params: { channel: string } })
customers: Math.floor(Math.random() * 20) + 20,
})),
},
] as Task[];
]
const [tasks, setTasks] = useState<Task[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [tasks, setTasks] = useState<Task[]>(initialTasks)
const [deviceStats, setDeviceStats] = useState<DeviceStats>({
active: 5,
@@ -248,146 +204,54 @@ export default function ChannelPage({ params }: { params: { channel: string } })
})
}
// 修改API数据处理部分
useEffect(() => {
// 组件未挂载,不执行操作
if (!isMounted.current) return;
// 防止重复请求如果sceneId没有变化且已经加载过数据则不重新请求
if (sceneId === sceneIdRef.current && tasks.length > 0 && !loading) {
return;
}
const fetchPlanList = async () => {
try {
setLoading(true);
// 如果sceneId还未确定则等待
if (sceneId === null) return;
// 调用API获取计划列表
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_BASE_URL}/v1/plan/list?id=${sceneId}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
}
);
const data = await response.json();
if (data.code === 1 && data.data && data.data.list) {
// 将API返回的数据转换为前端展示格式
const transformedTasks = data.data.list.map((item: PlanItem) => {
// 确保返回的对象完全符合Task类型
const status: "running" | "paused" | "completed" =
item.status === 1 ? "running" : "paused";
return {
id: item.id.toString(),
name: item.name,
status: status,
stats: {
devices: item.deviceCount,
acquired: item.customerCount,
added: item.addedCount,
},
lastUpdated: item.createTimeFormat,
executionTime: item.lastExecutionTime || "--",
nextExecutionTime: item.nextExecutionTime || "--",
trend: Array.from({ length: 7 }, (_, i) => ({
date: `2月${String(i + 1)}`,
customers: Math.floor(Math.random() * 20) + 10, // 模拟数据
})),
};
});
// 使用类型断言解决类型冲突
setTasks(transformedTasks as Task[]);
setError(null);
} else {
setError(data.msg || "获取计划列表失败");
// 如果API返回错误使用初始数据
setTasks(initialTasks);
}
} catch (err) {
console.error("获取计划列表失败:", err);
setError("网络错误,无法获取计划列表");
// 出错时使用初始数据
setTasks(initialTasks);
} finally {
setLoading(false);
}
};
fetchPlanList();
}, [sceneId]); // 只依赖sceneId变化触发请求
// 辅助函数根据渠道获取场景ID
const getSceneIdFromChannel = (channel: string): number => {
const channelMap: Record<string, number> = {
'douyin': 1,
'xiaohongshu': 2,
'weixinqun': 3,
'gongzhonghao': 4,
'kuaishou': 5,
'weibo': 6,
'haibao': 7,
'phone': 8,
'api': 9
};
return channelMap[channel] || 6;
};
const handleCreateNewPlan = () => {
// router.push(`/plans/new?type=${channel}`)
router.push(`/scenarios/new?type=${channel}`)
}
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()}>
<div className="flex items-center justify-between p-4">
<div className="flex items-center gap-3">
<Button variant="ghost" size="icon" onClick={() => router.push("/scenarios")} className="h-8 w-8">
<ChevronLeft className="h-5 w-5" />
</Button>
<h1 className="text-xl font-semibold text-blue-600">{channelName}</h1>
</div>
<Button onClick={handleCreateNewPlan} size="sm" className="bg-blue-600 hover:bg-blue-700 text-white">
<Plus className="h-4 w-4 mr-1" />
{channelName}
</Button>
</div>
</header>
<div className="p-4 max-w-7xl mx-auto">
{loading ? (
// 添加加载状态
<div className="text-center py-12 bg-white rounded-lg shadow-sm">
<div className="w-8 h-8 border-2 border-blue-600 border-t-transparent rounded-full animate-spin mx-auto mb-4"></div>
<div className="text-gray-500">...</div>
</div>
) : error ? (
// 添加错误提示
<div className="text-center py-12 bg-white rounded-lg shadow-sm">
<div className="text-red-500 mb-4">{error}</div>
<Button variant="outline" onClick={() => window.location.reload()}>
</Button>
</div>
) : tasks.length > 0 ? (
tasks.map((task) => (
<div key={task.id} className="mb-6">
<ScenarioAcquisitionCard
task={task}
channel={channel}
onEdit={() => handleEditPlan(task.id)}
onCopy={handleCopyPlan}
onDelete={handleDeletePlan}
onStatusChange={handleStatusChange}
onOpenSettings={handleOpenApiSettings}
/>
<div className="p-4 md:p-6 lg:p-8 max-w-7xl mx-auto">
<div className="space-y-4">
{tasks.length > 0 ? (
tasks.map((task) => (
<div key={task.id}>
<ScenarioAcquisitionCard
task={task}
channel={channel}
onEdit={() => handleEditPlan(task.id)}
onCopy={handleCopyPlan}
onDelete={handleDeletePlan}
onStatusChange={handleStatusChange}
onOpenSettings={handleOpenApiSettings}
/>
</div>
))
) : (
<div className="text-center py-12 bg-white rounded-lg shadow-sm md:col-span-2 lg:col-span-3">
<div className="text-gray-400 mb-4"></div>
<Button onClick={handleCreateNewPlan} className="bg-blue-600 hover:bg-blue-700 text-white">
<Plus className="h-4 w-4 mr-1" />
{channelName}
</Button>
</div>
))
) : (
<div className="text-center py-12 bg-white rounded-lg shadow-sm">
<div className="text-gray-400 mb-4"></div>
</div>
)}
)}
</div>
</div>
{/* API接口设置对话框 */}
<Dialog open={showApiDialog} onOpenChange={setShowApiDialog}>
@@ -400,81 +264,163 @@ export default function ChannelPage({ params }: { params: { channel: string } })
<DialogDescription>使</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-5 py-4">
{/* API密钥部分 */}
<div className="space-y-2">
<Label htmlFor="api-key">API密钥</Label>
<div className="flex items-center justify-between">
<Label htmlFor="api-key" className="text-sm font-medium flex items-center gap-1">
API密钥
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="h-3.5 w-3.5 text-gray-400 cursor-help" />
</TooltipTrigger>
<TooltipContent className="max-w-xs">
<p className="text-xs">API密钥用于身份验证</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</Label>
<span className="text-xs text-gray-500"></span>
</div>
<div className="flex items-center space-x-2">
<Input id="api-key" value={currentApiSettings.apiKey} readOnly className="flex-1" />
<Button
variant="outline"
size="icon"
onClick={() => {
navigator.clipboard.writeText(currentApiSettings.apiKey)
toast({
title: "已复制",
description: "API密钥已复制到剪贴板",
variant: "default",
})
}}
>
<Copy className="h-4 w-4" />
</Button>
<div className="relative flex-1">
<Input
id="api-key"
value={currentApiSettings.apiKey}
readOnly
className="pr-10 font-mono text-sm bg-gray-50"
/>
<Button
variant="ghost"
size="icon"
className="absolute right-0 top-0 h-full"
onClick={() => {
navigator.clipboard.writeText(currentApiSettings.apiKey)
toast({
title: "已复制",
description: "API密钥已复制到剪贴板",
variant: "default",
})
}}
>
<Copy className="h-4 w-4" />
</Button>
</div>
</div>
</div>
{/* 接口地址部分 */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label htmlFor="webhook-url"></Label>
<Label htmlFor="webhook-url" className="text-sm font-medium">
</Label>
<button
className="text-xs text-blue-600 hover:underline"
className="text-xs text-blue-600 hover:underline flex items-center gap-1"
onClick={() => handleCopyApiUrl(currentApiSettings.webhookUrl, true)}
>
<Copy className="h-3 w-3" />
</button>
</div>
<div className="flex items-center space-x-2">
<Input id="webhook-url" value={currentApiSettings.webhookUrl} readOnly className="flex-1" />
<Button variant="outline" size="icon" onClick={() => handleCopyApiUrl(currentApiSettings.webhookUrl)}>
<Copy className="h-4 w-4" />
</Button>
<div className="relative flex-1">
<Input
id="webhook-url"
value={currentApiSettings.webhookUrl}
readOnly
className="pr-10 font-mono text-sm bg-gray-50 text-gray-700"
/>
<Button
variant="ghost"
size="icon"
className="absolute right-0 top-0 h-full"
onClick={() => handleCopyApiUrl(currentApiSettings.webhookUrl)}
>
<Copy className="h-4 w-4" />
</Button>
</div>
</div>
<div className="bg-blue-50 p-2 rounded-md">
<p className="text-xs text-blue-700">
<span className="font-medium"></span>namephone
<br />
<span className="font-medium"></span>sourceremarktags
</p>
</div>
<p className="text-xs text-gray-500">GET/POST请求namephone</p>
</div>
<div className="space-y-2">
<Label></Label>
{/* 接口文档部分 */}
<div className="space-y-3 pt-2">
<div className="flex items-center justify-between">
<Button
className="w-full flex items-center justify-center gap-2"
onClick={() => {
window.open(`/api/docs/scenarios/${channel}/${currentApiSettings.taskId}`, "_blank")
}}
>
<Link className="h-4 w-4" />
</Button>
<Label className="text-sm font-medium"></Label>
</div>
<div className="bg-gray-50 p-4 rounded-lg border border-gray-100">
<div className="flex flex-col space-y-3">
<Button
variant="outline"
className="w-full flex items-center justify-center gap-2 bg-white"
onClick={() => {
window.open(`/api/docs/scenarios/${channel}/${currentApiSettings.taskId}`, "_blank")
}}
>
<Link className="h-4 w-4" />
</Button>
<div className="grid grid-cols-2 gap-2">
<Button
variant="ghost"
size="sm"
className="text-xs"
onClick={() => {
window.open(`/api/docs/scenarios/${channel}/${currentApiSettings.taskId}#examples`, "_blank")
}}
>
<span className="text-blue-600"></span>
</Button>
<Button
variant="ghost"
size="sm"
className="text-xs"
onClick={() => {
window.open(`/api/docs/scenarios/${channel}/${currentApiSettings.taskId}#integration`, "_blank")
}}
>
<span className="text-blue-600"></span>
</Button>
</div>
</div>
</div>
</div>
{/* 快速测试部分 */}
<div className="space-y-2 pt-1">
<div className="flex items-center justify-between">
<Label className="text-sm font-medium"></Label>
</div>
<div className="bg-gray-50 p-3 rounded-md border border-gray-100">
<p className="text-xs text-gray-600 mb-2">使URL可以快速测试接口是否正常工作</p>
<div className="text-xs font-mono bg-white p-2 rounded border border-gray-200 overflow-x-auto">
{`${currentApiSettings.webhookUrl}?name=测试客户&phone=13800138000`}
</div>
</div>
<p className="text-xs text-gray-500 text-center">
<a
href={`/api/docs/scenarios/${channel}/${currentApiSettings.taskId}#examples`}
target="_blank"
className="text-blue-600 hover:underline"
rel="noreferrer"
>
PythonJava等多语言示例代码
</a>
</p>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowApiDialog(false)}>
</Button>
<DialogFooter className="flex justify-between items-center">
<div className="text-xs text-gray-500">
<span className="inline-flex items-center">
<Shield className="h-3 w-3 mr-1" />
</span>
</div>
<Button onClick={() => setShowApiDialog(false)}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}

View File

@@ -1,4 +1,3 @@
export default function Loading() {
return null
}

View File

@@ -233,4 +233,3 @@ export default function ChannelTrafficPage({
</div>
)
}