plan pages
This commit is contained in:
@@ -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>name(姓名)、phone(电话)
|
||||
<br />
|
||||
<span className="font-medium">可选参数:</span>source(来源)、remark(备注)、tags(标签)
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">支持GET/POST请求,必要参数:name(姓名)、phone(电话)</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"
|
||||
>
|
||||
查看Python、Java等多语言示例代码
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user