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

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