feat: 本次提交更新内容如下
存
This commit is contained in:
@@ -38,7 +38,7 @@ export interface Plan {
|
||||
export interface Task {
|
||||
id: string;
|
||||
name: string;
|
||||
status: "running" | "paused" | "completed";
|
||||
status: number;
|
||||
stats: {
|
||||
devices: number;
|
||||
acquired: number;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import { Upload } from 'tdesign-mobile-react';
|
||||
import type { UploadFile as TDesignUploadFile } from 'tdesign-mobile-react/es/upload/type';
|
||||
import { uploadImage } from '@/api/upload';
|
||||
@@ -12,14 +12,6 @@ interface UploadImageProps {
|
||||
}
|
||||
|
||||
const UploadImage: React.FC<UploadImageProps> = ({ value = [], onChange, ...props }) => {
|
||||
// 受控 files 状态
|
||||
const [files, setFiles] = useState<TDesignUploadFile[]>(value.map(url => ({ url })));
|
||||
|
||||
// value 变化时同步 files
|
||||
useEffect(() => {
|
||||
setFiles(value.map(url => ({ url })));
|
||||
}, [value]);
|
||||
|
||||
// 处理上传
|
||||
const requestMethod = async (file: TDesignUploadFile) => {
|
||||
try {
|
||||
@@ -42,14 +34,13 @@ const UploadImage: React.FC<UploadImageProps> = ({ value = [], onChange, ...prop
|
||||
|
||||
// 处理文件变更
|
||||
const handleChange = (newFiles: TDesignUploadFile[]) => {
|
||||
setFiles(newFiles);
|
||||
const urls = newFiles.map(f => f.url).filter((url): url is string => Boolean(url));
|
||||
onChange?.(urls);
|
||||
};
|
||||
|
||||
return (
|
||||
<Upload
|
||||
files={files}
|
||||
files={value.map(url => ({ url }))}
|
||||
requestMethod={requestMethod}
|
||||
onChange={handleChange}
|
||||
multiple
|
||||
|
||||
@@ -9,10 +9,6 @@ import { toast } from '@/components/ui/toast';
|
||||
import Layout from '@/components/Layout';
|
||||
import UnifiedHeader from '@/components/UnifiedHeader';
|
||||
import { get, post } from '@/api/request';
|
||||
import { UploadCloud } from 'lucide-react';
|
||||
import { Upload } from 'tdesign-mobile-react';
|
||||
import type { UploadFile as TDesignUploadFile } from 'tdesign-mobile-react/es/upload/type';
|
||||
import { uploadImage } from '@/api/upload';
|
||||
import UploadImage from '@/components/UploadImage';
|
||||
import UploadVideo from '@/components/UploadVideo';
|
||||
|
||||
@@ -32,7 +28,7 @@ export default function NewMaterial() {
|
||||
const [images, setImages] = useState<string[]>([]);
|
||||
const [isFirstLoad, setIsFirstLoad] = useState(true);
|
||||
// 优化图片上传逻辑,确保每次选择图片后立即上传并回显
|
||||
const [uploadFiles, setUploadFiles] = useState<TDesignUploadFile[]>([]);
|
||||
|
||||
|
||||
// 判断模式并拉取详情
|
||||
useEffect(() => {
|
||||
@@ -121,15 +117,7 @@ export default function NewMaterial() {
|
||||
}
|
||||
};
|
||||
|
||||
// 上传图片,接入接口
|
||||
const handleUploadImage = async (file: File) => {
|
||||
try {
|
||||
const url = await uploadImage(file);
|
||||
setImages(prev => [...prev, url]);
|
||||
} catch (e: any) {
|
||||
toast({ title: '上传失败', description: e.message || '图片上传失败', variant: 'destructive' });
|
||||
}
|
||||
};
|
||||
// 移除未用的 handleUploadImage 及 uploadImage 相关代码
|
||||
|
||||
return (
|
||||
<Layout
|
||||
@@ -199,7 +187,6 @@ export default function NewMaterial() {
|
||||
value={images}
|
||||
onChange={urls => {
|
||||
setCoverImage(urls[0]);
|
||||
setUploadFiles(urls.map(url => ({ url })));
|
||||
}}
|
||||
max={1}
|
||||
accept="image/*"
|
||||
@@ -237,7 +224,6 @@ export default function NewMaterial() {
|
||||
value={images}
|
||||
onChange={urls => {
|
||||
setImages(urls);
|
||||
setUploadFiles(urls.map(url => ({ url })));
|
||||
}}
|
||||
max={9}
|
||||
accept="image/*"
|
||||
@@ -255,7 +241,6 @@ export default function NewMaterial() {
|
||||
value={images}
|
||||
onChange={urls => {
|
||||
setImages(urls);
|
||||
setUploadFiles(urls.map(url => ({ url })));
|
||||
}}
|
||||
max={9}
|
||||
accept="image/*"
|
||||
|
||||
@@ -91,23 +91,16 @@ export default function ScenarioDetail() {
|
||||
setTasks([]);
|
||||
}
|
||||
|
||||
// 计算统计数据 - 添加安全检查
|
||||
const planList = response?.data?.list || [];
|
||||
const totalCustomers = planList.reduce((sum, task) => {
|
||||
return sum + (task.stats?.acquired || 0);
|
||||
}, 0);
|
||||
const todayCustomers = Math.floor(totalCustomers * 0.1); // 模拟今日数据
|
||||
|
||||
// 构建场景数据(无论是否有计划都要创建)
|
||||
const scenarioData: ScenarioData = {
|
||||
id: scenarioId,
|
||||
name: getScenarioName(),
|
||||
image: '', // 可以根据需要设置图片
|
||||
description: getScenarioDescription(scenarioId),
|
||||
totalPlans: planList.length,
|
||||
totalCustomers,
|
||||
todayCustomers,
|
||||
growth: `+${Math.floor(Math.random() * 20) + 5}%`,
|
||||
totalPlans: response?.data?.list?.length || 0,
|
||||
totalCustomers: 0, // 移除统计
|
||||
todayCustomers: 0, // 移除统计
|
||||
growth: '', // 移除增长
|
||||
};
|
||||
|
||||
setScenario(scenarioData);
|
||||
@@ -122,7 +115,7 @@ export default function ScenarioDetail() {
|
||||
totalPlans: 0,
|
||||
totalCustomers: 0,
|
||||
todayCustomers: 0,
|
||||
growth: '+0%',
|
||||
growth: '',
|
||||
};
|
||||
setScenario(scenarioData);
|
||||
setTasks([]);
|
||||
@@ -148,21 +141,16 @@ export default function ScenarioDetail() {
|
||||
|
||||
// 更新场景数据中的名称
|
||||
useEffect(() => {
|
||||
if (scenario) {
|
||||
setScenario(prev => prev ? {
|
||||
...prev,
|
||||
name: (() => {
|
||||
const urlName = searchParams.get('name');
|
||||
if (urlName) return urlName;
|
||||
return getChannelName(scenarioId || '');
|
||||
})()
|
||||
} : null);
|
||||
}
|
||||
}, [searchParams, scenarioId, scenario]);
|
||||
setScenario(prev => prev ? {
|
||||
...prev,
|
||||
name: (() => {
|
||||
const urlName = searchParams.get('name');
|
||||
if (urlName) return urlName;
|
||||
return getChannelName(scenarioId || '');
|
||||
})()
|
||||
} : null);
|
||||
}, [searchParams, scenarioId]);
|
||||
|
||||
// const handleEditPlan = (taskId: string) => {
|
||||
// navigate(`/scenarios/${scenarioId}/edit/${taskId}`);
|
||||
// };
|
||||
|
||||
const handleCopyPlan = async (taskId: string) => {
|
||||
const taskToCopy = tasks.find((task) => task.id === taskId);
|
||||
@@ -170,7 +158,7 @@ export default function ScenarioDetail() {
|
||||
|
||||
try {
|
||||
const response = await copyPlan(taskId);
|
||||
if (response && response.code === 0) {
|
||||
if (response && response.code === 200) {
|
||||
toast({
|
||||
title: '计划已复制',
|
||||
description: `已成功复制"${taskToCopy.name}"`,
|
||||
@@ -178,7 +166,7 @@ export default function ScenarioDetail() {
|
||||
|
||||
// 重新加载数据
|
||||
const refreshResponse = await fetchPlanList(scenarioId!, 1, 20);
|
||||
if (refreshResponse && refreshResponse.code === 0 && refreshResponse.data) {
|
||||
if (refreshResponse && refreshResponse.code === 200 && refreshResponse.data) {
|
||||
setTasks(refreshResponse.data.list);
|
||||
}
|
||||
} else {
|
||||
@@ -202,7 +190,7 @@ export default function ScenarioDetail() {
|
||||
|
||||
try {
|
||||
const response = await deletePlan(taskId);
|
||||
if (response && response.code === 0) {
|
||||
if (response && response.code === 200) {
|
||||
toast({
|
||||
title: '计划已删除',
|
||||
description: `已成功删除"${taskToDelete.name}"`,
|
||||
@@ -210,7 +198,7 @@ export default function ScenarioDetail() {
|
||||
|
||||
// 重新加载数据
|
||||
const refreshResponse = await fetchPlanList(scenarioId!, 1, 20);
|
||||
if (refreshResponse && refreshResponse.code === 0 && refreshResponse.data) {
|
||||
if (refreshResponse && refreshResponse.code === 200 && refreshResponse.data) {
|
||||
setTasks(refreshResponse.data.list);
|
||||
}
|
||||
} else {
|
||||
@@ -226,7 +214,7 @@ export default function ScenarioDetail() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleStatusChange = async (taskId: string, newStatus: 'running' | 'paused') => {
|
||||
const handleStatusChange = async (taskId: string, newStatus: 1 | 0) => {
|
||||
try {
|
||||
// 这里应该调用状态切换API,暂时模拟
|
||||
setTasks(prev => prev.map(task =>
|
||||
@@ -235,7 +223,7 @@ export default function ScenarioDetail() {
|
||||
|
||||
toast({
|
||||
title: '状态已更新',
|
||||
description: `计划已${newStatus === 'running' ? '启动' : '暂停'}`,
|
||||
description: `计划已${newStatus === 1 ? '启动' : '暂停'}`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('状态切换失败:', error);
|
||||
@@ -250,7 +238,7 @@ export default function ScenarioDetail() {
|
||||
const handleOpenApiSettings = async (taskId: string) => {
|
||||
try {
|
||||
const response = await fetchPlanDetail(taskId);
|
||||
if (response && response.code === 0 && response.data) {
|
||||
if (response && response.code === 200 && response.data) {
|
||||
setCurrentApiSettings({
|
||||
apiKey: response.data.apiKey || 'demo-api-key-123456',
|
||||
webhookUrl: response.data.textUrl?.fullUrl || `https://api.example.com/webhook/${taskId}`,
|
||||
@@ -282,27 +270,23 @@ export default function ScenarioDetail() {
|
||||
navigate(`/scenarios/new?scenario=${scenarioId}`);
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
const getStatusColor = (status: number) => {
|
||||
switch (status) {
|
||||
case 'running':
|
||||
case 1:
|
||||
return 'text-green-600 bg-green-50';
|
||||
case 'paused':
|
||||
case 0:
|
||||
return 'text-yellow-600 bg-yellow-50';
|
||||
case 'completed':
|
||||
return 'text-gray-600 bg-gray-50';
|
||||
default:
|
||||
return 'text-gray-600 bg-gray-50';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
const getStatusText = (status: number) => {
|
||||
switch (status) {
|
||||
case 'running':
|
||||
case 1:
|
||||
return '进行中';
|
||||
case 'paused':
|
||||
case 0:
|
||||
return '已暂停';
|
||||
case 'completed':
|
||||
return '已完成';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
@@ -403,7 +387,7 @@ export default function ScenarioDetail() {
|
||||
</div>
|
||||
|
||||
{/* 数据统计 */}
|
||||
<div className="grid grid-cols-2 gap-4 mb-6">
|
||||
{/* <div className="grid grid-cols-2 gap-4 mb-6">
|
||||
<div className="bg-white rounded-lg p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
@@ -430,7 +414,7 @@ export default function ScenarioDetail() {
|
||||
<span>活跃计划: {scenario.totalPlans}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
{/* 计划列表 */}
|
||||
<div className="bg-white rounded-lg">
|
||||
@@ -438,7 +422,7 @@ export default function ScenarioDetail() {
|
||||
<h2 className="text-lg font-medium">获客计划</h2>
|
||||
</div>
|
||||
|
||||
{tasks.length === 0 ? (
|
||||
{tasks.length === 200 ? (
|
||||
<div className="p-8 text-center">
|
||||
<div className="mb-4">
|
||||
<Users className="h-12 w-12 text-gray-300 mx-auto mb-3" />
|
||||
@@ -476,14 +460,14 @@ export default function ScenarioDetail() {
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={() => handleStatusChange(task.id, task.status === 'running' ? 'paused' : 'running')}
|
||||
onClick={() => handleStatusChange(task.id, task.status === 1 ? 0 : 1)}
|
||||
className={`p-2 rounded-md ${
|
||||
task.status === 'running'
|
||||
task.status === 1
|
||||
? 'text-yellow-600 hover:bg-yellow-50'
|
||||
: 'text-green-600 hover:bg-green-50'
|
||||
}`}
|
||||
>
|
||||
{task.status === 'running' ? <Pause className="h-4 w-4" /> : <Play className="h-4 w-4" />}
|
||||
{task.status === 1 ? <Pause className="h-4 w-4" /> : <Play className="h-4 w-4" />}
|
||||
</button>
|
||||
|
||||
<button
|
||||
|
||||
@@ -1,249 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Plus, Copy, Trash2, Play, Pause, TrendingUp, Users, Clock } from 'lucide-react';
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import { useToast } from '@/components/ui/toast';
|
||||
import Layout from '@/components/Layout';
|
||||
import '@/components/Layout.css';
|
||||
|
||||
interface Task {
|
||||
id: string;
|
||||
name: string;
|
||||
status: "running" | "paused" | "completed";
|
||||
stats: {
|
||||
devices: number;
|
||||
acquired: number;
|
||||
added: number;
|
||||
};
|
||||
lastUpdated: string;
|
||||
executionTime: string;
|
||||
nextExecutionTime: string;
|
||||
trend: { date: string; customers: number }[];
|
||||
dailyData: { date: string; acquired: number; added: number }[];
|
||||
}
|
||||
|
||||
export default function DouyinScenario() {
|
||||
const navigate = useNavigate();
|
||||
const { toast } = useToast();
|
||||
const channel = "douyin";
|
||||
|
||||
const [tasks, setTasks] = useState<Task[]>([
|
||||
{
|
||||
id: "1",
|
||||
name: "抖音直播获客计划",
|
||||
status: "running",
|
||||
stats: {
|
||||
devices: 3,
|
||||
acquired: 45,
|
||||
added: 32,
|
||||
},
|
||||
lastUpdated: "2024-03-18 15:30",
|
||||
executionTime: "2024-03-18 15:30",
|
||||
nextExecutionTime: "预计30分钟后",
|
||||
trend: Array.from({ length: 7 }, (_, i) => ({
|
||||
date: `3月${String(i + 12)}日`,
|
||||
customers: Math.floor(Math.random() * 10) + 5,
|
||||
})),
|
||||
dailyData: [
|
||||
{ date: "3/12", acquired: 12, added: 8 },
|
||||
{ date: "3/13", acquired: 15, added: 10 },
|
||||
{ date: "3/14", acquired: 8, added: 6 },
|
||||
{ date: "3/15", acquired: 10, added: 7 },
|
||||
{ date: "3/16", acquired: 14, added: 11 },
|
||||
{ date: "3/17", acquired: 9, added: 7 },
|
||||
{ date: "3/18", acquired: 11, added: 9 },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
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 getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'running':
|
||||
return 'text-green-600 bg-green-50';
|
||||
case 'paused':
|
||||
return 'text-yellow-600 bg-yellow-50';
|
||||
case 'completed':
|
||||
return 'text-gray-600 bg-gray-50';
|
||||
default:
|
||||
return 'text-gray-600 bg-gray-50';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
case 'running':
|
||||
return '进行中';
|
||||
case 'paused':
|
||||
return '已暂停';
|
||||
case 'completed':
|
||||
return '已完成';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'running':
|
||||
return <Play className="h-4 w-4" />;
|
||||
case 'paused':
|
||||
return <Pause className="h-4 w-4" />;
|
||||
case 'completed':
|
||||
return <Clock className="h-4 w-4" />;
|
||||
default:
|
||||
return <Clock className="h-4 w-4" />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<PageHeader
|
||||
title="抖音获客"
|
||||
defaultBackPath="/scenarios"
|
||||
rightContent={
|
||||
<button
|
||||
onClick={() => navigate('/scenarios/new?scenario=douyin')}
|
||||
className="flex items-center px-3 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors text-sm"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
新建计划
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="bg-gradient-to-b from-blue-50 to-white">
|
||||
<div className="p-4 max-w-7xl mx-auto">
|
||||
{tasks.map((task) => (
|
||||
<div key={task.id} className="bg-white rounded-lg shadow-sm border border-gray-100 mb-4 overflow-hidden">
|
||||
{/* 任务头部 */}
|
||||
<div className="p-4 border-b border-gray-100">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className={`p-2 rounded-lg ${getStatusColor(task.status)}`}>
|
||||
{getStatusIcon(task.status)}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-gray-900">{task.name}</h3>
|
||||
<div className="flex items-center space-x-4 text-sm text-gray-500 mt-1">
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(task.status)}`}>
|
||||
{getStatusText(task.status)}
|
||||
</span>
|
||||
<span>最后更新: {task.lastUpdated}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={() => handleCopyPlan(task.id)}
|
||||
className="p-2 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
|
||||
title="复制计划"
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeletePlan(task.id)}
|
||||
className="p-2 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
||||
title="删除计划"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 统计信息 */}
|
||||
<div className="p-4">
|
||||
<div className="grid grid-cols-3 gap-4 mb-4">
|
||||
<div className="text-center">
|
||||
<div className="flex items-center justify-center text-gray-500 mb-1">
|
||||
<Users className="h-4 w-4 mr-1" />
|
||||
<span className="text-sm">设备数</span>
|
||||
</div>
|
||||
<div className="text-xl font-bold text-blue-600">{task.stats.devices}</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="flex items-center justify-center text-gray-500 mb-1">
|
||||
<TrendingUp className="h-4 w-4 mr-1" />
|
||||
<span className="text-sm">已获客</span>
|
||||
</div>
|
||||
<div className="text-xl font-bold text-green-600">{task.stats.acquired}</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="flex items-center justify-center text-gray-500 mb-1">
|
||||
<Users className="h-4 w-4 mr-1" />
|
||||
<span className="text-sm">已添加</span>
|
||||
</div>
|
||||
<div className="text-xl font-bold text-purple-600">{task.stats.added}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 趋势图表 */}
|
||||
<div className="mb-4">
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-2">7天趋势</h4>
|
||||
<div className="flex items-end space-x-1 h-20">
|
||||
{task.trend.map((item, index) => (
|
||||
<div key={index} className="flex-1 flex flex-col items-center">
|
||||
<div
|
||||
className="w-full bg-blue-200 rounded-t"
|
||||
style={{ height: `${(item.customers / 15) * 100}%` }}
|
||||
></div>
|
||||
<span className="text-xs text-gray-500 mt-1">{item.date}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 执行信息 */}
|
||||
<div className="bg-gray-50 rounded-lg p-3">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-600">执行时间: {task.executionTime}</span>
|
||||
<span className="text-blue-600">{task.nextExecutionTime}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* 设备树图表 */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-100 p-4">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">设备分布</h3>
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<Users className="h-12 w-12 mx-auto mb-2 text-gray-300" />
|
||||
<p>设备树图表功能开发中...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
@@ -1,442 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Copy, Link, HelpCircle, Shield, Plus } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useToast } from '@/components/ui/toast';
|
||||
import { ScenarioAcquisitionCard } from '@/components/ScenarioAcquisitionCard';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import Layout from '@/components/Layout';
|
||||
import '@/components/Layout.css';
|
||||
import {
|
||||
fetchSceneName,
|
||||
fetchPlanList,
|
||||
copyPlan,
|
||||
deletePlan,
|
||||
type Task
|
||||
} from '@/api/scenarios';
|
||||
import { log } from 'console';
|
||||
|
||||
interface DeviceStats {
|
||||
active: number;
|
||||
}
|
||||
|
||||
const API_BASE_URL = process.env.REACT_APP_API_BASE_URL || 'http://yishi.com';
|
||||
|
||||
// API文档提示组件
|
||||
function ApiDocumentationTooltip() {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="h-4 w-4 text-gray-400 cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-xs">
|
||||
<p className="text-xs">
|
||||
计划接口允许您通过API将外部系统的客户数据直接导入到存客宝。支持多种编程语言和第三方平台集成。
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default function GongzhonghaoScenario() {
|
||||
const navigate = useNavigate();
|
||||
const { toast } = useToast();
|
||||
const channel = "gongzhonghao";
|
||||
const [channelName, setChannelName] = useState<string>("");
|
||||
|
||||
// 1. tasks 初始值设为 []
|
||||
const [tasks, setTasks] = useState<Task[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [total, setTotal] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSceneName(channel)
|
||||
.then((res) => {
|
||||
if (res.code === 200 && res.data?.name) {
|
||||
setChannelName(res.data.name);
|
||||
} else {
|
||||
setChannelName(channel);
|
||||
}
|
||||
})
|
||||
.catch(() => setChannelName(channel));
|
||||
}, [channel]);
|
||||
|
||||
// 抽出请求列表的函数
|
||||
const fetchTasks = () => {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
fetchPlanList(channel, page, pageSize)
|
||||
.then((res) => {
|
||||
if (res.code === 200 && res.data && Array.isArray(res.data.list)) {
|
||||
setTasks(res.data.list);
|
||||
setTotal(res.data.total || 0);
|
||||
} else {
|
||||
setError(res.msg || "接口返回异常");
|
||||
}
|
||||
})
|
||||
.catch((err: any) => setError(err?.message || "接口请求失败"))
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchTasks();
|
||||
}, [channel, page, pageSize]);
|
||||
|
||||
const [deviceStats, setDeviceStats] = useState<DeviceStats>({
|
||||
active: 5,
|
||||
});
|
||||
|
||||
const [showApiDialog, setShowApiDialog] = useState(false);
|
||||
const [currentApiSettings, setCurrentApiSettings] = useState({
|
||||
apiKey: "",
|
||||
webhookUrl: "",
|
||||
taskId: "",
|
||||
fullUrl: ""
|
||||
});
|
||||
|
||||
const handleEditPlan = (taskId: string) => {
|
||||
navigate(`/scenarios/${channel}/edit/${taskId}`);
|
||||
};
|
||||
|
||||
const handleCopyPlan = (taskId: string) => {
|
||||
const taskToCopy = tasks.find((task) => task.id === taskId);
|
||||
if (!taskToCopy) return;
|
||||
copyPlan(taskId)
|
||||
.then((res) => {
|
||||
if (res.code === 200) {
|
||||
toast({
|
||||
title: "计划已复制",
|
||||
description: `已成功复制"${taskToCopy.name}"`,
|
||||
variant: "default",
|
||||
});
|
||||
setPage(1);
|
||||
fetchTasks();
|
||||
} else {
|
||||
toast({
|
||||
title: "复制失败",
|
||||
description: res.msg || "复制计划失败,请重试",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((err: any) => {
|
||||
toast({
|
||||
title: "复制失败",
|
||||
description: err?.message || "复制计划失败,请重试",
|
||||
variant: "destructive",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeletePlan = (taskId: string) => {
|
||||
const taskToDelete = tasks.find((t) => t.id === taskId);
|
||||
if (!taskToDelete) return;
|
||||
deletePlan(taskId)
|
||||
.then((res) => {
|
||||
if (res.code === 200) {
|
||||
setTasks(tasks.filter((t) => t.id !== taskId));
|
||||
toast({
|
||||
title: "计划已删除",
|
||||
description: `已成功删除"${taskToDelete.name}"`,
|
||||
variant: "default",
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: "删除失败",
|
||||
description: res.msg || "删除计划失败,请重试",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((err: any) => {
|
||||
toast({
|
||||
title: "删除失败",
|
||||
description: err?.message || "删除计划失败,请重试",
|
||||
variant: "destructive",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleStatusChange = (taskId: string, newStatus: "running" | "paused") => {
|
||||
setTasks(tasks.map((task) => (task.id === taskId ? { ...task, status: newStatus } : task)));
|
||||
|
||||
toast({
|
||||
title: newStatus === "running" ? "计划已启动" : "计划已暂停",
|
||||
description: `已${newStatus === "running" ? "启动" : "暂停"}获客计划`,
|
||||
variant: "default",
|
||||
});
|
||||
};
|
||||
|
||||
const handleOpenApiSettings = (taskId: string) => {
|
||||
const task = tasks.find((t) => t.id === taskId);
|
||||
if (task) {
|
||||
// 直接使用列表数据,不调用详情API
|
||||
setCurrentApiSettings({
|
||||
apiKey: `api_key_${taskId}`, // 使用任务ID生成API密钥
|
||||
webhookUrl: `${API_BASE_URL}/v1/api/scenarios/${taskId}`,
|
||||
fullUrl: `${API_BASE_URL}/v1/api/scenarios/${taskId}/text`,
|
||||
taskId,
|
||||
});
|
||||
setShowApiDialog(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyApiUrl = (url: string, withParams = false) => {
|
||||
let copyUrl = url;
|
||||
if (withParams) {
|
||||
copyUrl = `${url}?name=张三&phone=13800138000&source=外部系统&remark=测试数据`;
|
||||
}
|
||||
navigator.clipboard.writeText(copyUrl);
|
||||
toast({
|
||||
title: "已复制",
|
||||
description: withParams ? "接口地址(含示例参数)已复制到剪贴板" : "接口地址已复制到剪贴板",
|
||||
variant: "default",
|
||||
});
|
||||
};
|
||||
|
||||
const handleCreateNewPlan = () => {
|
||||
navigate(`/scenarios/new?type=${channel}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<PageHeader
|
||||
title={channelName}
|
||||
defaultBackPath="/scenarios"
|
||||
rightContent={
|
||||
<Button onClick={handleCreateNewPlan} size="sm" className="bg-blue-600 hover:bg-blue-700 text-white">
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
新建计划
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="bg-gradient-to-b from-blue-50 to-white">
|
||||
<div className="p-4 md:p-6 lg:p-8 max-w-7xl mx-auto">
|
||||
<div className="space-y-4">
|
||||
{loading ? (
|
||||
<div className="text-center py-12 text-gray-400">加载中...</div>
|
||||
) : error ? (
|
||||
<div className="text-center py-12 text-red-500">{error}</div>
|
||||
) : 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="flex justify-center mt-6 gap-2">
|
||||
<Button disabled={page === 1} onClick={() => setPage(page - 1)}>上一页</Button>
|
||||
<span className="px-2">第 {page} 页 / 共 {Math.max(1, Math.ceil(total / pageSize))} 页</span>
|
||||
<Button disabled={page * pageSize >= total} onClick={() => setPage(page + 1)}>下一页</Button>
|
||||
</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" />
|
||||
新建计划
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* API接口设置对话框 */}
|
||||
<Dialog open={showApiDialog} onOpenChange={setShowApiDialog}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<DialogTitle>计划接口</DialogTitle>
|
||||
<ApiDocumentationTooltip />
|
||||
</div>
|
||||
<DialogDescription>使用此接口直接导入客资到该获客计划,支持多种编程语言。</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-5 py-4">
|
||||
{/* API密钥部分 */}
|
||||
<div className="space-y-2">
|
||||
<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">
|
||||
<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" className="text-sm font-medium">
|
||||
接口地址
|
||||
</Label>
|
||||
<button
|
||||
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">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{/* 接口文档部分 */}
|
||||
<div className="space-y-3 pt-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<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?planId=${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?planId=${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?planId=${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}?${currentApiSettings.fullUrl}`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
@@ -1,249 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Plus, Copy, Trash2, Play, Pause, TrendingUp, Users, Clock, Image } from 'lucide-react';
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import { useToast } from '@/components/ui/toast';
|
||||
import Layout from '@/components/Layout';
|
||||
import '@/components/Layout.css';
|
||||
|
||||
interface Task {
|
||||
id: string;
|
||||
name: string;
|
||||
status: "running" | "paused" | "completed";
|
||||
stats: {
|
||||
devices: number;
|
||||
acquired: number;
|
||||
added: number;
|
||||
};
|
||||
lastUpdated: string;
|
||||
executionTime: string;
|
||||
nextExecutionTime: string;
|
||||
trend: { date: string; customers: number }[];
|
||||
dailyData: { date: string; acquired: number; added: number }[];
|
||||
}
|
||||
|
||||
export default function HaibaoScenario() {
|
||||
const navigate = useNavigate();
|
||||
const { toast } = useToast();
|
||||
const channel = "haibao";
|
||||
|
||||
const [tasks, setTasks] = useState<Task[]>([
|
||||
{
|
||||
id: "1",
|
||||
name: "海报分享获客计划",
|
||||
status: "running",
|
||||
stats: {
|
||||
devices: 5,
|
||||
acquired: 89,
|
||||
added: 62,
|
||||
},
|
||||
lastUpdated: "2024-03-18 17:30",
|
||||
executionTime: "2024-03-18 17:30",
|
||||
nextExecutionTime: "预计15分钟后",
|
||||
trend: Array.from({ length: 7 }, (_, i) => ({
|
||||
date: `3月${String(i + 12)}日`,
|
||||
customers: Math.floor(Math.random() * 20) + 10,
|
||||
})),
|
||||
dailyData: [
|
||||
{ date: "3/12", acquired: 25, added: 18 },
|
||||
{ date: "3/13", acquired: 32, added: 22 },
|
||||
{ date: "3/14", acquired: 18, added: 12 },
|
||||
{ date: "3/15", acquired: 28, added: 19 },
|
||||
{ date: "3/16", acquired: 35, added: 24 },
|
||||
{ date: "3/17", acquired: 22, added: 15 },
|
||||
{ date: "3/18", acquired: 30, added: 21 },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
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 getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'running':
|
||||
return 'text-green-600 bg-green-50';
|
||||
case 'paused':
|
||||
return 'text-yellow-600 bg-yellow-50';
|
||||
case 'completed':
|
||||
return 'text-gray-600 bg-gray-50';
|
||||
default:
|
||||
return 'text-gray-600 bg-gray-50';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
case 'running':
|
||||
return '进行中';
|
||||
case 'paused':
|
||||
return '已暂停';
|
||||
case 'completed':
|
||||
return '已完成';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'running':
|
||||
return <Play className="h-4 w-4" />;
|
||||
case 'paused':
|
||||
return <Pause className="h-4 w-4" />;
|
||||
case 'completed':
|
||||
return <Clock className="h-4 w-4" />;
|
||||
default:
|
||||
return <Clock className="h-4 w-4" />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<PageHeader
|
||||
title="海报获客"
|
||||
defaultBackPath="/scenarios"
|
||||
rightContent={
|
||||
<button
|
||||
onClick={() => navigate('/scenarios/new?scenario=haibao')}
|
||||
className="flex items-center px-3 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors text-sm"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
新建计划
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="bg-gradient-to-b from-blue-50 to-white">
|
||||
<div className="p-4 max-w-7xl mx-auto">
|
||||
{tasks.map((task) => (
|
||||
<div key={task.id} className="bg-white rounded-lg shadow-sm border border-gray-100 mb-4 overflow-hidden">
|
||||
{/* 任务头部 */}
|
||||
<div className="p-4 border-b border-gray-100">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className={`p-2 rounded-lg ${getStatusColor(task.status)}`}>
|
||||
{getStatusIcon(task.status)}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-gray-900">{task.name}</h3>
|
||||
<div className="flex items-center space-x-4 text-sm text-gray-500 mt-1">
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(task.status)}`}>
|
||||
{getStatusText(task.status)}
|
||||
</span>
|
||||
<span>最后更新: {task.lastUpdated}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={() => handleCopyPlan(task.id)}
|
||||
className="p-2 text-gray-400 hover:text-orange-600 hover:bg-orange-50 rounded-lg transition-colors"
|
||||
title="复制计划"
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeletePlan(task.id)}
|
||||
className="p-2 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
||||
title="删除计划"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 统计信息 */}
|
||||
<div className="p-4">
|
||||
<div className="grid grid-cols-3 gap-4 mb-4">
|
||||
<div className="text-center">
|
||||
<div className="flex items-center justify-center text-gray-500 mb-1">
|
||||
<Users className="h-4 w-4 mr-1" />
|
||||
<span className="text-sm">设备数</span>
|
||||
</div>
|
||||
<div className="text-xl font-bold text-orange-600">{task.stats.devices}</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="flex items-center justify-center text-gray-500 mb-1">
|
||||
<TrendingUp className="h-4 w-4 mr-1" />
|
||||
<span className="text-sm">已获客</span>
|
||||
</div>
|
||||
<div className="text-xl font-bold text-green-600">{task.stats.acquired}</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="flex items-center justify-center text-gray-500 mb-1">
|
||||
<Users className="h-4 w-4 mr-1" />
|
||||
<span className="text-sm">已添加</span>
|
||||
</div>
|
||||
<div className="text-xl font-bold text-purple-600">{task.stats.added}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 趋势图表 */}
|
||||
<div className="mb-4">
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-2">7天趋势</h4>
|
||||
<div className="flex items-end space-x-1 h-20">
|
||||
{task.trend.map((item, index) => (
|
||||
<div key={index} className="flex-1 flex flex-col items-center">
|
||||
<div
|
||||
className="w-full bg-orange-200 rounded-t"
|
||||
style={{ height: `${(item.customers / 30) * 100}%` }}
|
||||
></div>
|
||||
<span className="text-xs text-gray-500 mt-1">{item.date}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 执行信息 */}
|
||||
<div className="bg-gray-50 rounded-lg p-3">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-600">执行时间: {task.executionTime}</span>
|
||||
<span className="text-orange-600">{task.nextExecutionTime}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* 海报管理 */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-100 p-4">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">海报管理</h3>
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<Image className="h-12 w-12 mx-auto mb-2 text-gray-300" />
|
||||
<p>海报管理功能开发中...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
@@ -1,244 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Plus, Copy, Trash2, Play, Pause, TrendingUp, Users, Clock } from 'lucide-react';
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import { useToast } from '@/components/ui/toast';
|
||||
|
||||
interface Task {
|
||||
id: string;
|
||||
name: string;
|
||||
status: "running" | "paused" | "completed";
|
||||
stats: {
|
||||
devices: number;
|
||||
acquired: number;
|
||||
added: number;
|
||||
};
|
||||
lastUpdated: string;
|
||||
executionTime: string;
|
||||
nextExecutionTime: string;
|
||||
trend: { date: string; customers: number }[];
|
||||
dailyData: { date: string; acquired: number; added: number }[];
|
||||
}
|
||||
|
||||
export default function XiaohongshuScenario() {
|
||||
const navigate = useNavigate();
|
||||
const { toast } = useToast();
|
||||
const channel = "xiaohongshu";
|
||||
|
||||
const [tasks, setTasks] = useState<Task[]>([
|
||||
{
|
||||
id: "1",
|
||||
name: "小红书内容营销计划",
|
||||
status: "running",
|
||||
stats: {
|
||||
devices: 2,
|
||||
acquired: 28,
|
||||
added: 19,
|
||||
},
|
||||
lastUpdated: "2024-03-18 14:20",
|
||||
executionTime: "2024-03-18 14:20",
|
||||
nextExecutionTime: "预计45分钟后",
|
||||
trend: Array.from({ length: 7 }, (_, i) => ({
|
||||
date: `3月${String(i + 12)}日`,
|
||||
customers: Math.floor(Math.random() * 8) + 3,
|
||||
})),
|
||||
dailyData: [
|
||||
{ date: "3/12", acquired: 8, added: 5 },
|
||||
{ date: "3/13", acquired: 12, added: 8 },
|
||||
{ date: "3/14", acquired: 6, added: 4 },
|
||||
{ date: "3/15", acquired: 9, added: 6 },
|
||||
{ date: "3/16", acquired: 11, added: 7 },
|
||||
{ date: "3/17", acquired: 7, added: 5 },
|
||||
{ date: "3/18", acquired: 10, added: 7 },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
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 getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'running':
|
||||
return 'text-green-600 bg-green-50';
|
||||
case 'paused':
|
||||
return 'text-yellow-600 bg-yellow-50';
|
||||
case 'completed':
|
||||
return 'text-gray-600 bg-gray-50';
|
||||
default:
|
||||
return 'text-gray-600 bg-gray-50';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
case 'running':
|
||||
return '进行中';
|
||||
case 'paused':
|
||||
return '已暂停';
|
||||
case 'completed':
|
||||
return '已完成';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'running':
|
||||
return <Play className="h-4 w-4" />;
|
||||
case 'paused':
|
||||
return <Pause className="h-4 w-4" />;
|
||||
case 'completed':
|
||||
return <Clock className="h-4 w-4" />;
|
||||
default:
|
||||
return <Clock className="h-4 w-4" />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-1 bg-gradient-to-b from-pink-50 to-white min-h-screen">
|
||||
<PageHeader
|
||||
title="小红书获客"
|
||||
defaultBackPath="/scenarios"
|
||||
rightContent={
|
||||
<button
|
||||
onClick={() => navigate('/scenarios/new?scenario=xiaohongshu')}
|
||||
className="flex items-center px-3 py-2 bg-pink-600 text-white rounded-md hover:bg-pink-700 transition-colors text-sm"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
新建计划
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 添加pt-16来避免被固定导航栏遮挡 */}
|
||||
<div className="pt-16 p-4 max-w-7xl mx-auto">
|
||||
{tasks.map((task) => (
|
||||
<div key={task.id} className="bg-white rounded-lg shadow-sm border border-gray-100 mb-4 overflow-hidden">
|
||||
{/* 任务头部 */}
|
||||
<div className="p-4 border-b border-gray-100">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className={`p-2 rounded-lg ${getStatusColor(task.status)}`}>
|
||||
{getStatusIcon(task.status)}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-gray-900">{task.name}</h3>
|
||||
<div className="flex items-center space-x-4 text-sm text-gray-500 mt-1">
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(task.status)}`}>
|
||||
{getStatusText(task.status)}
|
||||
</span>
|
||||
<span>最后更新: {task.lastUpdated}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={() => handleCopyPlan(task.id)}
|
||||
className="p-2 text-gray-400 hover:text-pink-600 hover:bg-pink-50 rounded-lg transition-colors"
|
||||
title="复制计划"
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeletePlan(task.id)}
|
||||
className="p-2 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
||||
title="删除计划"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 统计信息 */}
|
||||
<div className="p-4">
|
||||
<div className="grid grid-cols-3 gap-4 mb-4">
|
||||
<div className="text-center">
|
||||
<div className="flex items-center justify-center text-gray-500 mb-1">
|
||||
<Users className="h-4 w-4 mr-1" />
|
||||
<span className="text-sm">设备数</span>
|
||||
</div>
|
||||
<div className="text-xl font-bold text-pink-600">{task.stats.devices}</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="flex items-center justify-center text-gray-500 mb-1">
|
||||
<TrendingUp className="h-4 w-4 mr-1" />
|
||||
<span className="text-sm">已获客</span>
|
||||
</div>
|
||||
<div className="text-xl font-bold text-green-600">{task.stats.acquired}</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="flex items-center justify-center text-gray-500 mb-1">
|
||||
<Users className="h-4 w-4 mr-1" />
|
||||
<span className="text-sm">已添加</span>
|
||||
</div>
|
||||
<div className="text-xl font-bold text-purple-600">{task.stats.added}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 趋势图表 */}
|
||||
<div className="mb-4">
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-2">7天趋势</h4>
|
||||
<div className="flex items-end space-x-1 h-20">
|
||||
{task.trend.map((item, index) => (
|
||||
<div key={index} className="flex-1 flex flex-col items-center">
|
||||
<div
|
||||
className="w-full bg-pink-200 rounded-t"
|
||||
style={{ height: `${(item.customers / 12) * 100}%` }}
|
||||
></div>
|
||||
<span className="text-xs text-gray-500 mt-1">{item.date}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 执行信息 */}
|
||||
<div className="bg-gray-50 rounded-lg p-3">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-600">执行时间: {task.executionTime}</span>
|
||||
<span className="text-pink-600">{task.nextExecutionTime}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* 内容分析 */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-100 p-4">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">内容分析</h3>
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<TrendingUp className="h-12 w-12 mx-auto mb-2 text-gray-300" />
|
||||
<p>内容分析功能开发中...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user