feat: 本次提交更新内容如下

This commit is contained in:
笔记本里的永平
2025-07-15 16:08:33 +08:00
parent 0e8bb566b5
commit df72e84ea1
8 changed files with 38 additions and 1262 deletions

View File

@@ -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;

View File

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

View File

@@ -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/*"

View File

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

View File

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

View File

@@ -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>namephone
<br />
<span className="font-medium"></span>sourceremarktags
</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>
);
}

View File

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

View File

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