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

场景获客列表搞定
This commit is contained in:
笔记本里的永平
2025-07-07 17:08:27 +08:00
parent 6543da9167
commit 5ff15472f5
352 changed files with 24040 additions and 18575 deletions

View File

@@ -1,9 +1,11 @@
import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Plus, TrendingUp } from 'lucide-react';
import { Plus, TrendingUp, Loader2 } from 'lucide-react';
import PageHeader from '@/components/PageHeader';
import Layout from '@/components/Layout';
import BottomNav from '@/components/BottomNav';
import { fetchScenes, type SceneItem } from '@/api/scenarios';
import { useToast } from '@/components/ui/toast';
import '@/components/Layout.css';
interface Scenario {
@@ -13,93 +15,136 @@ interface Scenario {
description?: string;
count: number;
growth: string;
status: string;
}
export default function Scenarios() {
const navigate = useNavigate();
const { toast } = useToast();
const [scenarios, setScenarios] = useState<Scenario[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
// AI智能获客用本地 mock 数据(暂时注释掉,可以后续启用)
// const aiScenarios = [
// {
// id: "ai-friend",
// name: "AI智能加友",
// icon: "🤖",
// count: 245,
// growth: "+18.5%",
// description: "智能分析目标用户画像,自动筛选优质客户",
// path: "/scenarios/ai-friend",
// },
// {
// id: "ai-group",
// name: "AI群引流",
// icon: "🤖",
// count: 178,
// growth: "+15.2%",
// description: "智能群管理,提高群活跃度,增强获客效果",
// path: "/scenarios/ai-group",
// },
// {
// id: "ai-conversion",
// name: "AI场景转化",
// icon: "🤖",
// count: 134,
// growth: "+12.8%",
// description: "多场景智能营销,提升获客转化率",
// path: "/scenarios/ai-conversion",
// },
// ];
// 场景描述映射
const scenarioDescriptions: Record<string, string> = {
'douyin': '通过抖音平台进行精准获客',
'xiaohongshu': '利用小红书平台进行内容营销获客',
'gongzhonghao': '通过微信公众号进行获客',
'haibao': '通过海报分享进行获客',
'phone': '通过电话营销进行获客',
'weixinqun': '通过微信群进行获客',
'payment': '通过付款码进行获客',
'api': '通过API接口进行获客',
};
useEffect(() => {
const fetchScenarios = async () => {
setLoading(true);
setError('');
try {
// 这里可以调用实际的API
// const response = await fetch('/api/scenarios');
// const data = await response.json();
const response = await fetchScenes({ page: 1, limit: 20 });
// 模拟数据
if (response && response.code === 200 && response.data) {
// 转换API数据为前端需要的格式
const transformedScenarios: Scenario[] = response.data.map((item: SceneItem) => ({
id: item.id.toString(),
name: item.name,
image: item.image|| 'https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-api.png',
description: scenarioDescriptions[item.name.toLowerCase()] || '通过该平台进行获客',
count: Math.floor(Math.random() * 200) + 50, // 模拟今日数据
growth: `+${Math.floor(Math.random() * 20) + 5}%`, // 模拟增长率
status: item.status === 1 ? 'active' : 'inactive',
}));
setScenarios(transformedScenarios);
} else {
// API返回错误使用模拟数据作为降级方案
console.warn('API返回错误使用模拟数据:', response?.message);
const mockScenarios: Scenario[] = [
{
id: "douyin",
name: "抖音获客",
image: 'https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-QR8ManuDplYTySUJsY4mymiZkDYnQ9.png',
description: scenarioDescriptions['douyin'],
count: 156,
growth: "+12%",
status: "active",
},
{
id: "xiaohongshu",
name: "小红书获客",
image: 'https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-yvnMxpoBUzcvEkr8DfvHgPHEo1kmQ3.png',
description: scenarioDescriptions['xiaohongshu'],
count: 89,
growth: "+8%",
status: "active",
},
{
id: "gongzhonghao",
name: "公众号获客",
image: 'https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-Gsg0CMf5tsZb41mioszdjqU1WmsRxW.png',
description: scenarioDescriptions['gongzhonghao'],
count: 234,
growth: "+15%",
status: "active",
},
{
id: "haibao",
name: "海报获客",
image: 'https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-x92XJgXy4MI7moNYlA1EAes2FqDxMH.png',
description: scenarioDescriptions['haibao'],
count: 167,
growth: "+10%",
status: "active",
},
];
setScenarios(mockScenarios);
}
} catch (error) {
console.error('获取场景数据失败:', error);
setError('获取场景数据失败,请稍后重试');
// 网络错误时也使用模拟数据
const mockScenarios: Scenario[] = [
{
id: "douyin",
name: "抖音获客",
image: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-QR8ManuDplYTySUJsY4mymiZkDYnQ9.png",
description: "通过抖音平台进行精准获客",
image: 'https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-QR8ManuDplYTySUJsY4mymiZkDYnQ9.png',
description: scenarioDescriptions['douyin'],
count: 156,
growth: "+12%",
status: "active",
},
{
id: "xiaohongshu",
name: "小红书获客",
image: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-yvnMxpoBUzcvEkr8DfvHgPHEo1kmQ3.png",
description: "利用小红书平台进行内容营销获客",
image: 'https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-yvnMxpoBUzcvEkr8DfvHgPHEo1kmQ3.png',
description: scenarioDescriptions['xiaohongshu'],
count: 89,
growth: "+8%",
status: "active",
},
{
id: "gongzhonghao",
name: "公众号获客",
image: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-Gsg0CMf5tsZb41mioszdjqU1WmsRxW.png",
description: "通过微信公众号进行获客",
image: 'https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-Gsg0CMf5tsZb41mioszdjqU1WmsRxW.png',
description: scenarioDescriptions['gongzhonghao'],
count: 234,
growth: "+15%",
status: "active",
},
{
id: "haibao",
name: "海报获客",
image: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-x92XJgXy4MI7moNYlA1EAes2FqDxMH.png",
description: "通过海报分享进行获客",
image: 'https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-x92XJgXy4MI7moNYlA1EAes2FqDxMH.png',
description: scenarioDescriptions['haibao'],
count: 167,
growth: "+10%",
status: "active",
},
];
setScenarios(mockScenarios);
} catch (error) {
setError('获取场景数据失败');
console.error('获取场景数据失败:', error);
} finally {
setLoading(false);
}
@@ -108,6 +153,14 @@ export default function Scenarios() {
fetchScenarios();
}, []);
const handleScenarioClick = (scenarioId: string) => {
navigate(`/scenarios/${scenarioId}`);
};
const handleNewPlan = () => {
navigate('/scenarios/new');
};
if (loading) {
return (
<Layout
@@ -119,15 +172,17 @@ export default function Scenarios() {
}
footer={<BottomNav />}
>
<div className="bg-gray-50">
<div className="flex justify-center items-center h-40">
<div className="text-gray-500">...</div>
<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>
</Layout>
);
}
if (error) {
if (error && scenarios.length === 0) {
return (
<Layout
header={
@@ -138,12 +193,21 @@ export default function Scenarios() {
}
footer={<BottomNav />}
>
<div className="bg-gray-50">
<div className="text-red-500 text-center py-8">{error}</div>
<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>
</Layout>
);
}
return (
<Layout
header={
@@ -152,7 +216,7 @@ export default function Scenarios() {
showBack={false}
rightContent={
<button
onClick={() => navigate('/scenarios/new')}
onClick={handleNewPlan}
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" />
@@ -163,17 +227,33 @@ export default function Scenarios() {
}
footer={<BottomNav />}
>
<div className="bg-gray-50">
<div className="bg-gray-50 min-h-screen pb-20">
<div className="p-4">
{error && (
<div className="mb-4 p-3 bg-yellow-50 border border-yellow-200 rounded-md">
<p className="text-yellow-800 text-sm">{error}</p>
</div>
)}
<div className="grid grid-cols-2 gap-4">
{scenarios.map((scenario) => (
<div
key={scenario.id}
className="bg-white rounded-lg shadow overflow-hidden hover:shadow-md transition-shadow cursor-pointer"
onClick={() => navigate(`/scenarios/${scenario.id}/detail`)}
onClick={() => handleScenarioClick(scenario.id)}
>
<div className="p-4 flex flex-col items-center">
<img src={scenario.image} alt={scenario.name} className="w-12 h-12 mb-2 rounded" />
<div className="w-12 h-12 bg-gray-200 rounded-full flex items-center justify-center mb-2">
<img
src={scenario.image}
alt={scenario.name}
className="w-8 h-8"
onError={(e) => {
// 图片加载失败时使用默认图标
e.currentTarget.src = 'https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-api.png';
}}
/>
</div>
<h3 className="text-blue-600 font-medium text-center">{scenario.name}</h3>
{scenario.description && (
<p className="text-xs text-gray-500 text-center mt-1 line-clamp-2">{scenario.description}</p>
@@ -190,42 +270,6 @@ export default function Scenarios() {
</div>
))}
</div>
{/* AI智能获客部分暂时注释掉可以后续启用 */}
{/*
<div className="mt-6">
<div className="flex items-center mb-4">
<h2 className="text-lg font-medium">AI智能获客</h2>
<span className="ml-2 px-2 py-1 bg-blue-50 text-blue-600 text-xs rounded border">
Beta
</span>
</div>
<div className="grid grid-cols-2 gap-4">
{aiScenarios.map((scenario) => (
<div
key={scenario.id}
className="bg-white rounded-lg shadow overflow-hidden hover:shadow-md transition-shadow cursor-pointer"
onClick={() => navigate(scenario.path)}
>
<div className="p-4 flex flex-col items-center">
<div className="text-3xl mb-2">{scenario.icon}</div>
<h3 className="text-blue-600 font-medium text-center">{scenario.name}</h3>
<p className="text-xs text-gray-500 text-center mt-1 line-clamp-2">{scenario.description}</p>
<div className="flex items-center mt-2 text-gray-500 text-sm">
<span>今日: </span>
<span className="font-medium ml-1">{scenario.count}</span>
</div>
<div className="flex items-center mt-1 text-green-500 text-xs">
<TrendingUp className="h-3 w-3 mr-1" />
<span>{scenario.growth}</span>
</div>
</div>
</div>
))}
</div>
</div>
*/}
</div>
</div>
</Layout>

View File

@@ -25,6 +25,7 @@ import {
deletePlan,
type Task
} from '@/api/scenarios';
import { log } from 'console';
interface DeviceStats {
active: number;