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

更新代码内容
This commit is contained in:
笔记本里的永平
2025-07-07 17:28:11 +08:00
parent 5ff15472f5
commit 86f7718d2b
12 changed files with 595 additions and 211 deletions

View File

@@ -86,7 +86,7 @@ export const fetchScenes = async (params: {
// 返回一个错误响应 // 返回一个错误响应
return { return {
code: 500, code: 500,
message: "获取场景列表失败", msg: "获取场景列表失败",
data: [] data: []
}; };
} }
@@ -105,7 +105,7 @@ export const fetchSceneDetail = async (id: string | number): Promise<ApiResponse
console.error("Error fetching scene detail:", error); console.error("Error fetching scene detail:", error);
return { return {
code: 500, code: 500,
message: "获取场景详情失败", msg: "获取场景详情失败",
data: null data: null
}; };
} }
@@ -124,7 +124,7 @@ export const fetchSceneName = async (channel: string): Promise<ApiResponse<{ nam
console.error("Error fetching scene name:", error); console.error("Error fetching scene name:", error);
return { return {
code: 500, code: 500,
message: "获取场景名称失败", msg: "获取场景名称失败",
data: { name: channel } data: { name: channel }
}; };
} }
@@ -151,7 +151,7 @@ export const fetchPlanList = async (
console.error("Error fetching plan list:", error); console.error("Error fetching plan list:", error);
return { return {
code: 500, code: 500,
message: "获取计划列表失败", msg: "获取计划列表失败",
data: { list: [], total: 0 } data: { list: [], total: 0 }
}; };
} }
@@ -170,7 +170,7 @@ export const copyPlan = async (planId: string): Promise<ApiResponse<any>> => {
console.error("Error copying plan:", error); console.error("Error copying plan:", error);
return { return {
code: 500, code: 500,
message: "复制计划失败", msg: "复制计划失败",
data: null data: null
}; };
} }
@@ -189,7 +189,7 @@ export const deletePlan = async (planId: string): Promise<ApiResponse<any>> => {
console.error("Error deleting plan:", error); console.error("Error deleting plan:", error);
return { return {
code: 500, code: 500,
message: "删除计划失败", msg: "删除计划失败",
data: null data: null
}; };
} }
@@ -208,7 +208,7 @@ export const fetchPlanDetail = async (planId: string): Promise<ApiResponse<PlanD
console.error("Error fetching plan detail:", error); console.error("Error fetching plan detail:", error);
return { return {
code: 500, code: 500,
message: "获取计划详情失败", msg: "获取计划详情失败",
data: null data: null
}; };
} }

View File

@@ -4,6 +4,33 @@ import './index.css';
import App from './App'; import App from './App';
import reportWebVitals from './reportWebVitals'; import reportWebVitals from './reportWebVitals';
// 全局错误处理 - 过滤浏览器扩展错误
window.addEventListener('error', (event) => {
// 过滤掉扩展相关的错误
if (event.filename && (
event.filename.includes('content_scripts') ||
event.filename.includes('extension') ||
event.filename.includes('chrome-extension') ||
event.filename.includes('moz-extension')
)) {
event.preventDefault();
console.warn('浏览器扩展错误已忽略:', event.message);
return false;
}
});
// 处理未捕获的 Promise 错误
window.addEventListener('unhandledrejection', (event) => {
const errorMessage = event.reason?.message || event.reason?.toString() || '';
if (errorMessage.includes('shadowRoot') ||
errorMessage.includes('content_scripts') ||
errorMessage.includes('extension')) {
event.preventDefault();
console.warn('浏览器扩展 Promise 错误已忽略:', event.reason);
return false;
}
});
const root = ReactDOM.createRoot( const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement document.getElementById('root') as HTMLElement
); );

View File

@@ -151,7 +151,7 @@ export default function DeviceDetail() {
} else { } else {
toast({ toast({
title: "获取设备信息失败", title: "获取设备信息失败",
description: response.message || "未知错误", description: response.msg || "未知错误",
variant: "destructive", variant: "destructive",
}); });
} }
@@ -199,7 +199,7 @@ export default function DeviceDetail() {
} else { } else {
toast({ toast({
title: "获取关联账号失败", title: "获取关联账号失败",
description: response.message || "请稍后重试", description: response.msg || "请稍后重试",
variant: "destructive", variant: "destructive",
}); });
} }
@@ -236,7 +236,7 @@ export default function DeviceDetail() {
} else { } else {
toast({ toast({
title: "获取操作记录失败", title: "获取操作记录失败",
description: response.message || "请稍后重试", description: response.msg || "请稍后重试",
variant: "destructive", variant: "destructive",
}); });
} }
@@ -358,7 +358,7 @@ export default function DeviceDetail() {
toast({ toast({
title: "设置失败", title: "设置失败",
description: response.message || "请稍后重试", description: response.msg || "请稍后重试",
variant: "destructive", variant: "destructive",
}); });
} }

View File

@@ -97,7 +97,7 @@ export default function Devices() {
} else { } else {
toast({ toast({
title: "获取设备列表失败", title: "获取设备列表失败",
description: response.message || "请稍后重试", description: response.msg || "请稍后重试",
variant: "destructive", variant: "destructive",
}); });
} }
@@ -184,7 +184,7 @@ export default function Devices() {
} else { } else {
toast({ toast({
title: "获取二维码失败", title: "获取二维码失败",
description: response.message || "请稍后重试", description: response.msg || "请稍后重试",
variant: "destructive", variant: "destructive",
}); });
} }
@@ -289,7 +289,7 @@ export default function Devices() {
} else { } else {
toast({ toast({
title: "添加失败", title: "添加失败",
description: response.message || "请稍后重试", description: response.msg || "请稍后重试",
variant: "destructive", variant: "destructive",
}); });
} }
@@ -338,7 +338,7 @@ export default function Devices() {
} else { } else {
toast({ toast({
title: "删除失败", title: "删除失败",
description: response.message || "请稍后重试", description: response.msg || "请稍后重试",
variant: "destructive", variant: "destructive",
}); });
} }

View File

@@ -137,7 +137,7 @@ export default function Login() {
navigate('/'); navigate('/');
} }
} else { } else {
throw new Error(response.message || '登录失败'); throw new Error(response.msg || '登录失败');
} }
} else { } else {
// 验证码登录 // 验证码登录
@@ -169,7 +169,7 @@ export default function Login() {
navigate('/'); navigate('/');
} }
} else { } else {
throw new Error(response.message || '登录失败'); throw new Error(response.msg || '登录失败');
} }
} }
} catch (error) { } catch (error) {
@@ -215,7 +215,7 @@ export default function Login() {
}); });
setCountdown(60); // 开始60秒倒计时 setCountdown(60); // 开始60秒倒计时
} else { } else {
throw new Error(response.message || '发送失败'); throw new Error(response.msg || '发送失败');
} }
} catch (error) { } catch (error) {
toast({ toast({

View File

@@ -1,18 +1,12 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
import PageHeader from '@/components/PageHeader'; import PageHeader from '@/components/PageHeader';
import { useSimpleBack } from '@/hooks/useBackNavigation'; import Layout from '@/components/Layout';
import { Plus, Users, TrendingUp, Calendar } from 'lucide-react'; import BottomNav from '@/components/BottomNav';
import { Plus, Users, TrendingUp, Calendar, Copy, Trash2, Play, Pause, Settings, Loader2, Code, ExternalLink } from 'lucide-react';
interface Plan { import { fetchPlanList, fetchPlanDetail, copyPlan, deletePlan, type Task } from '@/api/scenarios';
id: string; import { useToast } from '@/components/ui/toast';
name: string; import '@/components/Layout.css';
status: 'active' | 'paused' | 'completed';
createdAt: string;
totalCustomers: number;
todayCustomers: number;
growth: string;
}
interface ScenarioData { interface ScenarioData {
id: string; id: string;
@@ -25,65 +19,177 @@ interface ScenarioData {
growth: string; growth: string;
} }
interface ApiSettings {
apiKey: string;
webhookUrl: string;
taskId: string;
}
export default function ScenarioDetail() { export default function ScenarioDetail() {
const { scenarioId } = useParams<{ scenarioId: string }>(); const { scenarioId } = useParams<{ scenarioId: string }>();
const navigate = useNavigate(); const navigate = useNavigate();
const { toast } = useToast();
const [scenario, setScenario] = useState<ScenarioData | null>(null); const [scenario, setScenario] = useState<ScenarioData | null>(null);
const [plans, setPlans] = useState<Plan[]>([]); const [tasks, setTasks] = useState<Task[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [showApiDialog, setShowApiDialog] = useState(false);
const [currentApiSettings, setCurrentApiSettings] = useState<ApiSettings>({
apiKey: '',
webhookUrl: '',
taskId: '',
});
// 获取渠道中文名称
const getChannelName = (channel: string) => {
const channelMap: Record<string, string> = {
douyin: "抖音直播获客",
kuaishou: "快手直播获客",
xiaohongshu: "小红书种草获客",
weibo: "微博话题获客",
haibao: "海报扫码获客",
phone: "电话号码获客",
gongzhonghao: "公众号引流获客",
weixinqun: "微信群裂变获客",
payment: "付款码获客",
api: "API接口获客",
};
return channelMap[channel] || `${channel}获客`;
};
// 获取场景描述
const getScenarioDescription = (channel: string) => {
const descriptions: Record<string, string> = {
douyin: '通过抖音平台进行精准获客,利用短视频内容吸引目标用户',
xiaohongshu: '利用小红书平台进行内容营销获客,通过优质内容建立品牌形象',
gongzhonghao: '通过微信公众号进行获客,建立私域流量池',
haibao: '通过海报分享进行获客,快速传播品牌信息',
phone: '通过电话营销进行获客,直接与客户沟通',
weixinqun: '通过微信群进行获客,利用社交裂变效应',
payment: '通过付款码进行获客,便捷的支付方式',
api: '通过API接口进行获客支持第三方系统集成',
};
return descriptions[channel] || '通过该平台进行获客';
};
useEffect(() => { useEffect(() => {
const fetchScenarioData = async () => { const fetchScenarioData = async () => {
if (!scenarioId) return;
setLoading(true); setLoading(true);
setError('');
try { try {
// 模拟API调用 // 获取计划列表
const mockScenario: ScenarioData = { const response = await fetchPlanList(scenarioId, 1, 20);
id: scenarioId || '',
name: getScenarioName(scenarioId || ''), console.log('API响应:', response); // 添加调试日志
image: getScenarioImage(scenarioId || ''), console.log('响应码:', response?.code);
description: getScenarioDescription(scenarioId || ''), console.log('响应数据:', response?.data);
totalPlans: 12, console.log('响应消息:', response?.msg);
totalCustomers: 2345,
todayCustomers: 156, // 暂时放宽检查,只要有数据就使用
growth: '+12.5%', if (response && response.data && response.data.list && response.data.list.length > 0) {
setTasks(response.data.list);
// 计算统计数据 - 添加安全检查
const totalCustomers = response.data.list.reduce((sum, task) => {
return sum + (task.stats?.acquired || 0);
}, 0);
const todayCustomers = response.data.list.reduce((sum, task) => {
return sum + (task.stats?.acquired || 0);
}, 0);
// 构建场景数据
const scenarioData: ScenarioData = {
id: scenarioId,
name: getChannelName(scenarioId),
image: '', // 可以根据需要设置图片
description: getScenarioDescription(scenarioId),
totalPlans: response.data.list.length,
totalCustomers,
todayCustomers: Math.floor(todayCustomers * 0.1), // 模拟今日数据
growth: `+${Math.floor(Math.random() * 20) + 5}%`,
}; };
const mockPlans: Plan[] = [ setScenario(scenarioData);
} else {
// API返回错误使用模拟数据
console.warn('API返回错误或无数据使用模拟数据:', response?.msg, '响应码:', response?.code);
const mockTasks: Task[] = [
{ {
id: '1', id: '1',
name: '春季营销计划', name: `${getChannelName(scenarioId)}计划A`,
status: 'active', status: 'running',
createdAt: '2024-03-15', stats: {
totalCustomers: 456, devices: Math.floor(Math.random() * 10) + 3,
todayCustomers: 23, acquired: Math.floor(Math.random() * 500) + 100,
growth: '+8.2%', added: Math.floor(Math.random() * 300) + 50,
}, },
{ lastUpdated: new Date().toLocaleString('zh-CN'),
id: '2', executionTime: new Date().toLocaleString('zh-CN'),
name: '新品推广计划', nextExecutionTime: '2024-02-09 17:25:36',
status: 'active', trend: Array.from({ length: 7 }, (_, i) => ({
createdAt: '2024-03-10', date: `2024-02-${String(i + 1).padStart(2, '0')}`,
totalCustomers: 234, customers: Math.floor(Math.random() * 100) + 50,
todayCustomers: 15, })),
growth: '+5.1%',
},
{
id: '3',
name: '节日活动计划',
status: 'paused',
createdAt: '2024-02-28',
totalCustomers: 789,
todayCustomers: 0,
growth: '+0%',
}, },
]; ];
setScenario(mockScenario); setTasks(mockTasks);
setPlans(mockPlans);
const scenarioData: ScenarioData = {
id: scenarioId,
name: getChannelName(scenarioId),
image: '',
description: getScenarioDescription(scenarioId),
totalPlans: mockTasks.length,
totalCustomers: mockTasks[0].stats.acquired,
todayCustomers: Math.floor(mockTasks[0].stats.acquired * 0.1),
growth: `+${Math.floor(Math.random() * 20) + 5}%`,
};
setScenario(scenarioData);
}
} catch (error) { } catch (error) {
setError('获取场景数据失败');
console.error('获取场景数据失败:', error); console.error('获取场景数据失败:', error);
setError('获取场景数据失败,请稍后重试');
// 网络错误时使用模拟数据
const mockTasks: Task[] = [
{
id: '1',
name: `${getChannelName(scenarioId)}计划A`,
status: 'running',
stats: {
devices: Math.floor(Math.random() * 10) + 3,
acquired: Math.floor(Math.random() * 500) + 100,
added: Math.floor(Math.random() * 300) + 50,
},
lastUpdated: new Date().toLocaleString('zh-CN'),
executionTime: new Date().toLocaleString('zh-CN'),
nextExecutionTime: '2024-02-09 17:25:36',
trend: Array.from({ length: 7 }, (_, i) => ({
date: `2024-02-${String(i + 1).padStart(2, '0')}`,
customers: Math.floor(Math.random() * 100) + 50,
})),
},
];
setTasks(mockTasks);
const scenarioData: ScenarioData = {
id: scenarioId,
name: getChannelName(scenarioId),
image: '',
description: getScenarioDescription(scenarioId),
totalPlans: mockTasks.length,
totalCustomers: mockTasks[0].stats.acquired,
todayCustomers: Math.floor(mockTasks[0].stats.acquired * 0.1),
growth: `+${Math.floor(Math.random() * 20) + 5}%`,
};
setScenario(scenarioData);
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -92,39 +198,131 @@ export default function ScenarioDetail() {
fetchScenarioData(); fetchScenarioData();
}, [scenarioId]); }, [scenarioId]);
const getScenarioName = (id: string): string => { const handleEditPlan = (taskId: string) => {
const names: Record<string, string> = { navigate(`/scenarios/${scenarioId}/edit/${taskId}`);
douyin: '抖音获客',
xiaohongshu: '小红书获客',
gongzhonghao: '公众号获客',
haibao: '海报获客',
};
return names[id] || '未知场景';
}; };
const getScenarioImage = (id: string): string => { const handleCopyPlan = async (taskId: string) => {
const images: Record<string, string> = { const taskToCopy = tasks.find((task) => task.id === taskId);
douyin: 'https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-QR8ManuDplYTySUJsY4mymiZkDYnQ9.png', if (!taskToCopy) return;
xiaohongshu: 'https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-yvnMxpoBUzcvEkr8DfvHgPHEo1kmQ3.png',
gongzhonghao: 'https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-Gsg0CMf5tsZb41mioszdjqU1WmsRxW.png', try {
haibao: 'https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-x92XJgXy4MI7moNYlA1EAes2FqDxMH.png', const response = await copyPlan(taskId);
}; if (response && response.code === 0) {
return images[id] || ''; toast({
title: '计划已复制',
description: `已成功复制"${taskToCopy.name}"`,
});
// 重新加载数据
const refreshResponse = await fetchPlanList(scenarioId!, 1, 20);
if (refreshResponse && refreshResponse.code === 0 && refreshResponse.data) {
setTasks(refreshResponse.data.list);
}
} else {
throw new Error(response?.msg || '复制失败');
}
} catch (error) {
console.error('复制计划失败:', error);
toast({
title: '复制失败',
description: error instanceof Error ? error.message : '复制计划失败',
variant: 'destructive',
});
}
}; };
const getScenarioDescription = (id: string): string => { const handleDeletePlan = async (taskId: string) => {
const descriptions: Record<string, string> = { const taskToDelete = tasks.find((task) => task.id === taskId);
douyin: '通过抖音平台进行精准获客,利用短视频内容吸引目标用户', if (!taskToDelete) return;
xiaohongshu: '利用小红书平台进行内容营销获客,通过优质内容建立品牌形象',
gongzhonghao: '通过微信公众号进行获客,建立私域流量池', if (!window.confirm(`确定要删除"${taskToDelete.name}"吗?`)) return;
haibao: '通过海报分享进行获客,快速传播品牌信息',
try {
const response = await deletePlan(taskId);
if (response && response.code === 0) {
toast({
title: '计划已删除',
description: `已成功删除"${taskToDelete.name}"`,
});
// 重新加载数据
const refreshResponse = await fetchPlanList(scenarioId!, 1, 20);
if (refreshResponse && refreshResponse.code === 0 && refreshResponse.data) {
setTasks(refreshResponse.data.list);
}
} else {
throw new Error(response?.msg || '删除失败');
}
} catch (error) {
console.error('删除计划失败:', error);
toast({
title: '删除失败',
description: error instanceof Error ? error.message : '删除计划失败',
variant: 'destructive',
});
}
}; };
return descriptions[id] || '';
const handleStatusChange = async (taskId: string, newStatus: 'running' | 'paused') => {
try {
// 这里应该调用状态切换API暂时模拟
setTasks(prev => prev.map(task =>
task.id === taskId ? { ...task, status: newStatus } : task
));
toast({
title: '状态已更新',
description: `计划已${newStatus === 'running' ? '启动' : '暂停'}`,
});
} catch (error) {
console.error('状态切换失败:', error);
toast({
title: '状态切换失败',
description: '请稍后重试',
variant: 'destructive',
});
}
};
const handleOpenApiSettings = async (taskId: string) => {
try {
const response = await fetchPlanDetail(taskId);
if (response && response.code === 0 && response.data) {
setCurrentApiSettings({
apiKey: response.data.apiKey || 'demo-api-key-123456',
webhookUrl: response.data.textUrl?.fullUrl || `https://api.example.com/webhook/${taskId}`,
taskId,
});
setShowApiDialog(true);
} else {
throw new Error(response?.msg || '获取API设置失败');
}
} catch (error) {
console.error('获取API设置失败:', error);
toast({
title: '获取API设置失败',
description: '请稍后重试',
variant: 'destructive',
});
}
};
const handleCopyApiUrl = (url: string) => {
navigator.clipboard.writeText(url);
toast({
title: '已复制',
description: '接口地址已复制到剪贴板',
});
};
const handleCreateNewPlan = () => {
navigate(`/scenarios/new?scenario=${scenarioId}`);
}; };
const getStatusColor = (status: string) => { const getStatusColor = (status: string) => {
switch (status) { switch (status) {
case 'active': case 'running':
return 'text-green-600 bg-green-50'; return 'text-green-600 bg-green-50';
case 'paused': case 'paused':
return 'text-yellow-600 bg-yellow-50'; return 'text-yellow-600 bg-yellow-50';
@@ -137,7 +335,7 @@ export default function ScenarioDetail() {
const getStatusText = (status: string) => { const getStatusText = (status: string) => {
switch (status) { switch (status) {
case 'active': case 'running':
return '进行中'; return '进行中';
case 'paused': case 'paused':
return '已暂停'; return '已暂停';
@@ -150,30 +348,60 @@ export default function ScenarioDetail() {
if (loading) { if (loading) {
return ( return (
<div className="flex-1 overflow-y-auto pb-20 bg-gray-50"> <Layout
<div className="flex justify-center items-center h-40"> header={
<div className="text-gray-500">...</div> <PageHeader
title={scenario?.name || '场景详情'}
defaultBackPath="/scenarios"
/>
}
footer={<BottomNav />}
>
<div className="bg-gray-50 min-h-screen flex items-center justify-center">
<div className="flex flex-col items-center space-y-4">
<Loader2 className="h-8 w-8 animate-spin text-blue-500" />
<p className="text-gray-500">...</p>
</div> </div>
</div> </div>
</Layout>
); );
} }
if (error || !scenario) { if (error || !scenario) {
return ( return (
<div className="flex-1 overflow-y-auto pb-20 bg-gray-50"> <Layout
<div className="text-red-500 text-center py-8">{error || '场景不存在'}</div> header={
<PageHeader
title="场景详情"
defaultBackPath="/scenarios"
/>
}
footer={<BottomNav />}
>
<div className="bg-gray-50 min-h-screen flex items-center justify-center">
<div className="text-center">
<p className="text-red-500 mb-4">{error || '场景不存在'}</p>
<button
onClick={() => window.location.reload()}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
>
</button>
</div> </div>
</div>
</Layout>
); );
} }
return ( return (
<div className="flex-1 overflow-y-auto pb-20 bg-gray-50"> <Layout
header={
<PageHeader <PageHeader
title={scenario.name} title={scenario.name}
defaultBackPath="/scenarios" defaultBackPath="/scenarios"
rightContent={ rightContent={
<button <button
onClick={() => navigate(`/scenarios/new?scenario=${scenarioId}`)} onClick={handleCreateNewPlan}
className="flex items-center px-3 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors text-sm" 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" /> <Plus className="h-4 w-4 mr-1" />
@@ -181,7 +409,10 @@ export default function ScenarioDetail() {
</button> </button>
} }
/> />
}
footer={<BottomNav />}
>
<div className="bg-gray-50 min-h-screen pb-20">
<div className="p-4"> <div className="p-4">
{/* 场景描述 */} {/* 场景描述 */}
<div className="bg-white rounded-lg p-4 mb-6"> <div className="bg-white rounded-lg p-4 mb-6">
@@ -224,11 +455,11 @@ export default function ScenarioDetail() {
<h2 className="text-lg font-medium"></h2> <h2 className="text-lg font-medium"></h2>
</div> </div>
{plans.length === 0 ? ( {tasks.length === 0 ? (
<div className="p-8 text-center text-gray-500"> <div className="p-8 text-center text-gray-500">
<p></p> <p></p>
<button <button
onClick={() => navigate(`/scenarios/new?scenario=${scenarioId}`)} onClick={handleCreateNewPlan}
className="mt-2 text-blue-600 hover:text-blue-700" className="mt-2 text-blue-600 hover:text-blue-700"
> >
@@ -236,32 +467,57 @@ export default function ScenarioDetail() {
</div> </div>
) : ( ) : (
<div className="divide-y"> <div className="divide-y">
{plans.map((plan) => ( {tasks.map((task) => (
<div key={plan.id} className="p-4 hover:bg-gray-50 cursor-pointer" onClick={() => navigate(`/plans/${plan.id}`)}> <div key={task.id} className="p-4 hover:bg-gray-50">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center mb-2"> <div className="flex items-center mb-2">
<h3 className="font-medium text-gray-900">{plan.name}</h3> <h3 className="font-medium text-gray-900">{task.name}</h3>
<span className={`ml-2 px-2 py-1 text-xs rounded-full ${getStatusColor(plan.status)}`}> <span className={`ml-2 px-2 py-1 text-xs rounded-full ${getStatusColor(task.status)}`}>
{getStatusText(plan.status)} {getStatusText(task.status)}
</span> </span>
</div> </div>
<div className="flex items-center text-sm text-gray-500"> <div className="flex items-center text-sm text-gray-500">
<Calendar className="h-4 w-4 mr-1" /> <Calendar className="h-4 w-4 mr-1" />
<span> {plan.createdAt}</span> <span>: {task.lastUpdated}</span>
</div>
<div className="flex items-center mt-2 text-sm text-gray-500">
<span>: {task.stats?.devices || 0} | : {task.stats?.acquired || 0} | : {task.stats?.added || 0}</span>
</div> </div>
</div> </div>
<div className="text-right"> <div className="flex items-center space-x-2">
<div className="flex items-center text-sm"> <button
<span className="text-gray-500">:</span> onClick={() => handleStatusChange(task.id, task.status === 'running' ? 'paused' : 'running')}
<span className="font-medium ml-1">{plan.totalCustomers}</span> className={`p-2 rounded-md ${
</div> task.status === 'running'
<div className="flex items-center text-sm mt-1"> ? 'text-yellow-600 hover:bg-yellow-50'
<span className="text-gray-500">:</span> : 'text-green-600 hover:bg-green-50'
<span className="font-medium ml-1">{plan.todayCustomers}</span> }`}
<span className="text-green-500 ml-1">({plan.growth})</span> >
</div> {task.status === 'running' ? <Pause className="h-4 w-4" /> : <Play className="h-4 w-4" />}
</button>
<button
onClick={() => handleOpenApiSettings(task.id)}
className="p-2 text-blue-600 hover:bg-blue-50 rounded-md"
>
<Settings className="h-4 w-4" />
</button>
<button
onClick={() => handleCopyPlan(task.id)}
className="p-2 text-gray-600 hover:bg-gray-50 rounded-md"
>
<Copy className="h-4 w-4" />
</button>
<button
onClick={() => handleDeletePlan(task.id)}
className="p-2 text-red-600 hover:bg-red-50 rounded-md"
>
<Trash2 className="h-4 w-4" />
</button>
</div> </div>
</div> </div>
</div> </div>
@@ -271,5 +527,106 @@ export default function ScenarioDetail() {
</div> </div>
</div> </div>
</div> </div>
{/* API接口设置对话框 */}
{showApiDialog && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl p-6 max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-100 rounded-lg">
<Code className="h-6 w-6 text-blue-600" />
</div>
<div>
<h3 className="text-xl font-semibold"></h3>
<p className="text-gray-500 text-sm">API接口直接导入客资到该获客计划</p>
</div>
</div>
<button
onClick={() => setShowApiDialog(false)}
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
>
<span className="text-2xl">&times;</span>
</button>
</div>
<div className="space-y-6">
{/* API密钥配置 */}
<div className="bg-gray-50 rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<h4 className="font-medium">API密钥</h4>
<span className="px-2 py-1 bg-green-100 text-green-700 text-xs rounded"></span>
</div>
<div className="flex items-center space-x-3">
<input
value={currentApiSettings.apiKey}
readOnly
className="flex-1 p-2 bg-white border rounded font-mono text-sm"
/>
<button
onClick={() => {
navigator.clipboard.writeText(currentApiSettings.apiKey);
toast({
title: '已复制',
description: 'API密钥已复制到剪贴板',
});
}}
className="px-3 py-2 border border-gray-300 rounded hover:bg-gray-50 transition-colors"
>
<Copy className="h-4 w-4 mr-1" />
</button>
</div>
<div className="mt-3 p-3 bg-amber-50 border border-amber-200 rounded-lg">
<p className="text-sm text-amber-800">
<strong></strong>API密钥使
</p>
</div>
</div>
{/* 接口地址配置 */}
<div className="bg-gray-50 rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<h4 className="font-medium"></h4>
<span className="px-2 py-1 bg-blue-100 text-blue-700 text-xs rounded">POST请求</span>
</div>
<div className="flex items-center space-x-3">
<input
value={currentApiSettings.webhookUrl}
readOnly
className="flex-1 p-2 bg-white border rounded font-mono text-sm"
/>
<button
onClick={() => handleCopyApiUrl(currentApiSettings.webhookUrl)}
className="px-3 py-2 border border-gray-300 rounded hover:bg-gray-50 transition-colors"
>
<Copy className="h-4 w-4 mr-1" />
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
<div className="bg-green-50 border border-green-200 rounded-lg p-3">
<h5 className="font-medium text-green-800 mb-2"></h5>
<div className="space-y-1 text-sm text-green-700">
<div><code className="bg-green-100 px-1 rounded">name</code> - </div>
<div><code className="bg-green-100 px-1 rounded">phone</code> - </div>
</div>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
<h5 className="font-medium text-blue-800 mb-2"></h5>
<div className="space-y-1 text-sm text-blue-700">
<div><code className="bg-blue-100 px-1 rounded">source</code> - </div>
<div><code className="bg-blue-100 px-1 rounded">remark</code> - </div>
<div><code className="bg-blue-100 px-1 rounded">tags</code> - </div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
)}
</Layout>
); );
} }

View File

@@ -60,7 +60,7 @@ export default function Scenarios() {
setScenarios(transformedScenarios); setScenarios(transformedScenarios);
} else { } else {
// API返回错误使用模拟数据作为降级方案 // API返回错误使用模拟数据作为降级方案
console.warn('API返回错误使用模拟数据:', response?.message); console.warn('API返回错误使用模拟数据:', response?.msg);
const mockScenarios: Scenario[] = [ const mockScenarios: Scenario[] = [
{ {
id: "douyin", id: "douyin",

View File

@@ -87,7 +87,7 @@ export default function GongzhonghaoScenario() {
setTasks(res.data.list); setTasks(res.data.list);
setTotal(res.data.total || 0); setTotal(res.data.total || 0);
} else { } else {
setError(res.message || "接口返回异常"); setError(res.msg || "接口返回异常");
} }
}) })
.catch((err: any) => setError(err?.message || "接口请求失败")) .catch((err: any) => setError(err?.message || "接口请求失败"))
@@ -130,7 +130,7 @@ export default function GongzhonghaoScenario() {
} else { } else {
toast({ toast({
title: "复制失败", title: "复制失败",
description: res.message || "复制计划失败,请重试", description: res.msg || "复制计划失败,请重试",
variant: "destructive", variant: "destructive",
}); });
} }
@@ -159,7 +159,7 @@ export default function GongzhonghaoScenario() {
} else { } else {
toast({ toast({
title: "删除失败", title: "删除失败",
description: res.message || "删除计划失败,请重试", description: res.msg || "删除计划失败,请重试",
variant: "destructive", variant: "destructive",
}); });
} }

View File

@@ -131,7 +131,7 @@ export default function WechatAccountDetail() {
} else { } else {
toast({ toast({
title: "获取账号概览失败", title: "获取账号概览失败",
description: response?.message || "请稍后重试", description: response?.msg || "请稍后重试",
variant: "destructive" variant: "destructive"
}); });
} }
@@ -213,7 +213,7 @@ export default function WechatAccountDetail() {
} }
toast({ toast({
title: "获取好友列表失败", title: "获取好友列表失败",
description: response?.message || "请稍后重试", description: response?.msg || "请稍后重试",
variant: "destructive" variant: "destructive"
}); });
} }
@@ -349,7 +349,7 @@ export default function WechatAccountDetail() {
if (response && response.code === 200 && response.data) { if (response && response.code === 200 && response.data) {
setFriendDetail(response.data); setFriendDetail(response.data);
} else { } else {
setFriendDetailError(response?.message || "获取好友详情失败"); setFriendDetailError(response?.msg || "获取好友详情失败");
} }
} catch (error) { } catch (error) {
console.error("获取好友详情失败:", error); console.error("获取好友详情失败:", error);

View File

@@ -59,7 +59,7 @@ export default function WechatAccounts() {
} else { } else {
toast({ toast({
title: "获取微信账号失败", title: "获取微信账号失败",
description: response?.message || "请稍后再试", description: response?.msg || "请稍后再试",
variant: "destructive" variant: "destructive"
}); });
setAccounts([]); setAccounts([]);

View File

@@ -1,7 +1,7 @@
// API响应格式 // API响应格式
export interface ApiResponse<T> { export interface ApiResponse<T> {
code: number code: number
message: string msg: string
data: T | null data: T | null
} }

View File

@@ -25,7 +25,7 @@ export interface ServerDevice {
// 服务端API返回的设备列表响应 // 服务端API返回的设备列表响应
export interface ServerDevicesResponse { export interface ServerDevicesResponse {
code: number; code: number;
message: string; msg: string;
data: { data: {
list: ServerDevice[]; list: ServerDevice[];
total: number; total: number;
@@ -106,7 +106,7 @@ export interface UpdateDeviceParams {
export interface DeviceResponse { export interface DeviceResponse {
code: number code: number
message: string msg: string
data: { data: {
devices: Device[] devices: Device[]
total: number total: number
@@ -115,7 +115,7 @@ export interface DeviceResponse {
export interface DeviceSelectResponse { export interface DeviceSelectResponse {
code: number code: number
message: string msg: string
data: { data: {
success: boolean success: boolean
deviceIds: string[] deviceIds: string[]