From ff01acc184ce0d2577bf7b3c4a928a56623b1425 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=AC=94=E8=AE=B0=E6=9C=AC=E9=87=8C=E7=9A=84=E6=B0=B8?= =?UTF-8?q?=E5=B9=B3?= Date: Fri, 11 Jul 2025 17:06:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9C=AC=E6=AC=A1=E6=8F=90=E4=BA=A4?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=86=85=E5=AE=B9=E5=A6=82=E4=B8=8B=20?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F=E6=B2=A1=E9=97=AE=E9=A2=98=E4=BA=86=EF=BC=8C?= =?UTF-8?q?=E5=BA=93=E5=AD=98=E6=B5=81=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nkebao/src/components/BottomNav.tsx | 50 +- nkebao/src/components/UnifiedHeader.tsx | 296 +++++ nkebao/src/hooks/use-debounce.ts | 17 + nkebao/src/pages/Home.tsx | 503 ++++++-- nkebao/src/pages/profile/Profile.tsx | 261 ++-- nkebao/src/pages/scenarios/Scenarios.tsx | 27 +- nkebao/src/pages/traffic-pool/TrafficPool.tsx | 1132 +++++++++++++++-- nkebao/src/pages/workspace/Workspace.tsx | 5 +- 8 files changed, 1929 insertions(+), 362 deletions(-) create mode 100644 nkebao/src/components/UnifiedHeader.tsx create mode 100644 nkebao/src/hooks/use-debounce.ts diff --git a/nkebao/src/components/BottomNav.tsx b/nkebao/src/components/BottomNav.tsx index f8e42433e..ecf19aaad 100644 --- a/nkebao/src/components/BottomNav.tsx +++ b/nkebao/src/components/BottomNav.tsx @@ -1,12 +1,36 @@ import React from 'react'; import { Link, useLocation } from 'react-router-dom'; -import { Home, Users, User, Briefcase } from 'lucide-react'; +import { Home, Users, LayoutGrid, User } from 'lucide-react'; const navItems = [ - { href: "/", icon: Home, label: "首页", id: "home" }, - { href: "/scenarios", icon: Users, label: "场景获客", id: "scenarios" }, - { href: "/workspace", icon: Briefcase, label: "工作台", id: "workspace" }, - { href: "/profile", icon: User, label: "我的", id: "profile" }, + { + id: "home", + name: "首页", + href: "/", + icon: Home, + active: (pathname: string) => pathname === "/", + }, + { + id: "scenarios", + name: "场景获客", + href: "/scenarios", + icon: Users, + active: (pathname: string) => pathname.startsWith("/scenarios"), + }, + { + id: "workspace", + name: "工作台", + href: "/workspace", + icon: LayoutGrid, + active: (pathname: string) => pathname.startsWith("/workspace"), + }, + { + id: "profile", + name: "我的", + href: "/profile", + icon: User, + active: (pathname: string) => pathname.startsWith("/profile"), + }, ]; interface BottomNavProps { @@ -17,26 +41,26 @@ export default function BottomNav({ activeTab }: BottomNavProps) { const location = useLocation(); return ( - + ); } \ No newline at end of file diff --git a/nkebao/src/components/UnifiedHeader.tsx b/nkebao/src/components/UnifiedHeader.tsx new file mode 100644 index 000000000..862f1c463 --- /dev/null +++ b/nkebao/src/components/UnifiedHeader.tsx @@ -0,0 +1,296 @@ +import React from 'react'; +import { ChevronLeft, Settings, Bell, Search, RefreshCw, Filter, Plus, MoreVertical } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { useNavigate, useLocation } from 'react-router-dom'; + +interface HeaderAction { + type: 'button' | 'icon' | 'search' | 'custom'; + icon?: React.ComponentType; + label?: string; + onClick?: () => void; + variant?: 'default' | 'ghost' | 'outline' | 'destructive' | 'secondary'; + size?: 'default' | 'sm' | 'lg' | 'icon'; + className?: string; + content?: React.ReactNode; +} + +interface UnifiedHeaderProps { + /** 页面标题 */ + title: string; + /** 是否显示返回按钮 */ + showBack?: boolean; + /** 返回按钮文本 */ + backText?: string; + /** 自定义返回逻辑 */ + onBack?: () => void; + /** 默认返回路径 */ + defaultBackPath?: string; + /** 右侧操作按钮 */ + actions?: HeaderAction[]; + /** 自定义右侧内容 */ + rightContent?: React.ReactNode; + /** 是否显示搜索框 */ + showSearch?: boolean; + /** 搜索框占位符 */ + searchPlaceholder?: string; + /** 搜索值 */ + searchValue?: string; + /** 搜索回调 */ + onSearchChange?: (value: string) => void; + /** 是否显示底部边框 */ + showBorder?: boolean; + /** 背景样式 */ + background?: 'white' | 'transparent' | 'blur'; + /** 自定义CSS类名 */ + className?: string; + /** 标题样式类名 */ + titleClassName?: string; + /** 标题颜色 */ + titleColor?: 'default' | 'blue' | 'gray'; + /** 是否居中标题 */ + centerTitle?: boolean; + /** 头部高度 */ + height?: 'default' | 'compact' | 'tall'; +} + +const UnifiedHeader: React.FC = ({ + title, + showBack = true, + backText = '返回', + onBack, + defaultBackPath = '/', + actions = [], + rightContent, + showSearch = false, + searchPlaceholder = '搜索...', + searchValue = '', + onSearchChange, + showBorder = true, + background = 'white', + className = '', + titleClassName = '', + titleColor = 'default', + centerTitle = false, + height = 'default', +}) => { + const navigate = useNavigate(); + const location = useLocation(); + + const handleBack = () => { + if (onBack) { + onBack(); + } else if (defaultBackPath) { + navigate(defaultBackPath); + } else { + if (window.history.length > 1) { + navigate(-1); + } else { + navigate('/'); + } + } + }; + + // 背景样式 + const backgroundClasses = { + white: 'bg-white', + transparent: 'bg-transparent', + blur: 'bg-white/80 backdrop-blur-sm', + }; + + // 高度样式 + const heightClasses = { + default: 'h-14', + compact: 'h-12', + tall: 'h-16', + }; + + // 标题颜色样式 + const titleColorClasses = { + default: 'text-gray-900', + blue: 'text-blue-600', + gray: 'text-gray-600', + }; + + const headerClasses = [ + backgroundClasses[background], + heightClasses[height], + showBorder ? 'border-b border-gray-200' : '', + 'sticky top-0 z-50', + className, + ].filter(Boolean).join(' '); + + const titleClasses = [ + 'text-lg font-semibold', + titleColorClasses[titleColor], + centerTitle ? 'text-center' : '', + titleClassName, + ].filter(Boolean).join(' '); + + // 渲染操作按钮 + const renderAction = (action: HeaderAction, index: number) => { + if (action.type === 'custom' && action.content) { + return
{action.content}
; + } + + if (action.type === 'search') { + return ( +
+ + onSearchChange?.(e.target.value)} + className="pl-9 w-48" + /> +
+ ); + } + + const IconComponent = action.icon || MoreVertical; + + return ( + + ); + }; + + return ( +
+
+ {/* 左侧:返回按钮和标题 */} +
+ {showBack && ( + + )} + {!centerTitle && ( +

+ {title} +

+ )} +
+ + {/* 中间:居中标题 */} + {centerTitle && ( +
+

+ {title} +

+
+ )} + + {/* 右侧:搜索框、操作按钮、自定义内容 */} +
+ {showSearch && !actions.some(a => a.type === 'search') && ( +
+ + onSearchChange?.(e.target.value)} + className="pl-9 w-48" + /> +
+ )} + + {actions.map((action, index) => renderAction(action, index))} + + {rightContent && ( +
+ {rightContent} +
+ )} +
+
+
+ ); +}; + +// 预设的常用Header配置 +export const HeaderPresets = { + // 基础页面Header(有返回按钮) + basic: (title: string, onBack?: () => void): UnifiedHeaderProps => ({ + title, + showBack: true, + onBack, + titleColor: 'blue', + }), + + // 主页Header(无返回按钮) + main: (title: string, actions?: HeaderAction[]): UnifiedHeaderProps => ({ + title, + showBack: false, + titleColor: 'blue', + actions: actions || [ + { + type: 'icon', + icon: Bell, + onClick: () => console.log('Notifications'), + }, + ], + }), + + // 搜索页面Header + search: (title: string, searchValue: string, onSearchChange: (value: string) => void): UnifiedHeaderProps => ({ + title, + showBack: true, + showSearch: true, + searchValue, + onSearchChange, + titleColor: 'blue', + }), + + // 列表页面Header(带刷新和添加) + list: (title: string, onRefresh?: () => void, onAdd?: () => void): UnifiedHeaderProps => ({ + title, + showBack: true, + titleColor: 'blue', + actions: [ + ...(onRefresh ? [{ + type: 'icon' as const, + icon: RefreshCw, + onClick: onRefresh, + }] : []), + ...(onAdd ? [{ + type: 'button' as const, + icon: Plus, + label: '新建', + size: 'sm' as const, + onClick: onAdd, + }] : []), + ], + }), + + // 设置页面Header + settings: (title: string): UnifiedHeaderProps => ({ + title, + showBack: true, + titleColor: 'blue', + actions: [ + { + type: 'icon', + icon: Settings, + onClick: () => console.log('Settings'), + }, + ], + }), +}; + +export default UnifiedHeader; \ No newline at end of file diff --git a/nkebao/src/hooks/use-debounce.ts b/nkebao/src/hooks/use-debounce.ts new file mode 100644 index 000000000..6d63d199a --- /dev/null +++ b/nkebao/src/hooks/use-debounce.ts @@ -0,0 +1,17 @@ +import { useState, useEffect } from 'react'; + +export function useDebounce(value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [value, delay]); + + return debouncedValue; +} \ No newline at end of file diff --git a/nkebao/src/pages/Home.tsx b/nkebao/src/pages/Home.tsx index 495c83557..52994a853 100644 --- a/nkebao/src/pages/Home.tsx +++ b/nkebao/src/pages/Home.tsx @@ -1,87 +1,287 @@ -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { Bell, Smartphone, Users, Activity } from 'lucide-react'; +import { Bell, Smartphone, Users, Activity, MessageSquare, TrendingUp } from 'lucide-react'; import Chart from 'chart.js/auto'; import Layout from '@/components/Layout'; import BottomNav from '@/components/BottomNav'; +import UnifiedHeader, { HeaderPresets } from '@/components/UnifiedHeader'; +import { Card } from '@/components/ui/card'; +import { Progress } from '@/components/ui/progress'; import '@/components/Layout.css'; -// 模拟数据 -const stats = { - totalDevices: 12, - totalWechatAccounts: 8, - onlineWechatAccounts: 6, -}; +// API接口定义 +const API_BASE_URL = process.env.REACT_APP_API_BASE_URL || "https://ckbapi.quwanzhi.com"; -const scenarioFeatures = [ - { - id: "3", - name: "抖音获客", - value: 156, - icon: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-QR8ManuDplYTySUJsY4mymiZkDYnQ9.png", - color: "bg-red-100", - }, - { - id: "4", - name: "小红书获客", - value: 89, - icon: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-yvnMxpoBUzcvEkr8DfvHgPHEo1kmQ3.png", - color: "bg-pink-100", - }, - { - id: "6", - name: "公众号获客", - value: 234, - icon: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-Gsg0CMf5tsZb41mioszdjqU1WmsRxW.png", - color: "bg-green-100", - }, - { - id: "1", - name: "海报获客", - value: 167, - icon: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-x92XJgXy4MI7moNYlA1EAes2FqDxMH.png", - color: "bg-blue-100", - }, -]; +// 统一的API请求客户端 +async function apiRequest(url: string): Promise { + try { + const token = typeof window !== "undefined" ? localStorage.getItem("token") : null; + const headers: Record = { + "Content-Type": "application/json", + Accept: "application/json", + }; + + if (token) { + headers["Authorization"] = `Bearer ${token}`; + } + + console.log("发送API请求:", url); + + const response = await fetch(url, { + method: "GET", + headers, + mode: "cors", + }); + + console.log("API响应状态:", response.status, response.statusText); + + // 检查响应头的Content-Type + const contentType = response.headers.get("content-type"); + console.log("响应Content-Type:", contentType); + + if (!response.ok) { + // 如果是401未授权,清除本地存储 + if (response.status === 401) { + if (typeof window !== "undefined") { + localStorage.removeItem("token"); + localStorage.removeItem("userInfo"); + } + } + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + // 检查是否是JSON响应 + if (!contentType || !contentType.includes("application/json")) { + const text = await response.text(); + console.log("非JSON响应内容:", text.substring(0, 200)); + throw new Error("服务器返回了非JSON格式的数据,可能是HTML错误页面"); + } + + const data = await response.json(); + console.log("API响应数据:", data); + + // 检查业务状态码 + if (data.code && data.code !== 200 && data.code !== 0) { + throw new Error(data.message || "请求失败"); + } + + return data.data || data; + } catch (error) { + console.error("API请求失败:", error); + throw error; + } +} export default function Home() { const navigate = useNavigate(); const chartRef = useRef(null); const chartInstance = useRef(null); + // 统一设备数据 + const [stats, setStats] = useState({ + totalDevices: 0, + onlineDevices: 0, + totalWechatAccounts: 0, + onlineWechatAccounts: 0, + }); + + const [isLoading, setIsLoading] = useState(true); + const [apiError, setApiError] = useState(""); + + // 场景获客数据 + const scenarioFeatures = [ + { + id: "douyin", + name: "抖音获客", + icon: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-QR8ManuDplYTySUJsY4mymiZkDYnQ9.png", + color: "bg-blue-100 text-blue-600", + value: 156, + growth: 12, + }, + { + id: "xiaohongshu", + name: "小红书获客", + icon: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-yvnMxpoBUzcvEkr8DfvHgPHEo1kmQ3.png", + color: "bg-red-100 text-red-600", + value: 89, + growth: 8, + }, + { + id: "gongzhonghao", + name: "公众号获客", + icon: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-Gsg0CMf5tsZb41mioszdjqU1WmsRxW.png", + color: "bg-green-100 text-green-600", + value: 234, + growth: 15, + }, + { + id: "haibao", + name: "海报获客", + icon: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-x92XJgXy4MI7moNYlA1EAes2FqDxMH.png", + color: "bg-orange-100 text-orange-600", + value: 167, + growth: 10, + }, + ]; + + // 今日数据统计 + const todayStats = [ + { + title: "朋友圈同步", + value: "12", + icon: , + color: "text-purple-600", + path: "/workspace/moments-sync", + }, + { + title: "群发任务", + value: "8", + icon: , + color: "text-orange-600", + path: "/workspace/group-push", + }, + { + title: "获客转化", + value: "85%", + icon: , + color: "text-green-600", + path: "/scenarios", + }, + { + title: "系统活跃度", + value: "98%", + icon: , + color: "text-blue-600", + path: "/workspace", + }, + ]; + + useEffect(() => { + // 获取统计数据 + const fetchStats = async () => { + try { + setIsLoading(true); + setApiError(""); + + // 检查是否有token + const token = localStorage.getItem("token"); + if (!token) { + console.log("未找到登录token,使用默认数据"); + setStats({ + totalDevices: 42, + onlineDevices: 35, + totalWechatAccounts: 42, + onlineWechatAccounts: 35, + }); + setIsLoading(false); + return; + } + + // 尝试请求API数据 + try { + // 并行请求多个接口 + const [deviceStatsResult, wechatStatsResult] = await Promise.allSettled([ + apiRequest(`${API_BASE_URL}/v1/dashboard/device-stats`), + apiRequest(`${API_BASE_URL}/v1/dashboard/wechat-stats`), + ]); + + const newStats = { + totalDevices: 0, + onlineDevices: 0, + totalWechatAccounts: 0, + onlineWechatAccounts: 0, + }; + + // 处理设备统计数据 + if (deviceStatsResult.status === "fulfilled") { + const deviceData = deviceStatsResult.value as any; + newStats.totalDevices = deviceData.total || 0; + newStats.onlineDevices = deviceData.online || 0; + } else { + console.warn("设备统计API失败:", deviceStatsResult.reason); + } + + // 处理微信号统计数据 + if (wechatStatsResult.status === "fulfilled") { + const wechatData = wechatStatsResult.value as any; + newStats.totalWechatAccounts = wechatData.total || 0; + newStats.onlineWechatAccounts = wechatData.active || 0; + } else { + console.warn("微信号统计API失败:", wechatStatsResult.reason); + } + + setStats(newStats); + } catch (apiError) { + console.warn("API请求失败,使用默认数据:", apiError); + setApiError(apiError instanceof Error ? apiError.message : "API连接失败"); + + // 使用默认数据 + setStats({ + totalDevices: 42, + onlineDevices: 35, + totalWechatAccounts: 42, + onlineWechatAccounts: 35, + }); + } + } catch (error) { + console.error("获取统计数据失败:", error); + setApiError(error instanceof Error ? error.message : "数据加载失败"); + + // 使用默认数据 + setStats({ + totalDevices: 42, + onlineDevices: 35, + totalWechatAccounts: 42, + onlineWechatAccounts: 35, + }); + } finally { + setIsLoading(false); + } + }; + + fetchStats(); + + // 定时刷新数据(每30秒) + const interval = setInterval(fetchStats, 30000); + return () => clearInterval(interval); + }, []); // 移除stats依赖 + const handleDevicesClick = () => { - navigate('/devices'); + navigate('/profile/devices'); }; const handleWechatClick = () => { navigate('/wechat-accounts'); }; + // 使用Chart.js创建图表 useEffect(() => { - if (!chartRef.current) return; + if (chartRef.current && !isLoading) { + // 如果已经有图表实例,先销毁它 + if (chartInstance.current) { + chartInstance.current.destroy(); + } - // 销毁旧实例 - if (chartInstance.current) { - chartInstance.current.destroy(); - chartInstance.current = null; - } + const ctx = chartRef.current.getContext("2d"); + + // 添加null检查 + if (!ctx) return; - const ctx = chartRef.current.getContext('2d'); - if (ctx) { + // 创建新的图表实例 chartInstance.current = new Chart(ctx, { - type: 'line', + type: "line", data: { - labels: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'], + labels: ["周一", "周二", "周三", "周四", "周五", "周六", "周日"], datasets: [ { - label: '获客数量', - data: [12, 19, 15, 25, 22, 30, 28], - backgroundColor: 'rgba(59, 130, 246, 0.2)', - borderColor: 'rgba(59, 130, 246, 1)', + label: "获客数量", + data: [120, 150, 180, 200, 230, 210, 190], + backgroundColor: "rgba(59, 130, 246, 0.2)", + borderColor: "rgba(59, 130, 246, 1)", borderWidth: 2, tension: 0.3, pointRadius: 4, - pointBackgroundColor: 'rgba(59, 130, 246, 1)', + pointBackgroundColor: "rgba(59, 130, 246, 1)", pointHoverRadius: 6, }, ], @@ -94,15 +294,15 @@ export default function Home() { display: false, }, tooltip: { - backgroundColor: 'rgba(255, 255, 255, 0.9)', - titleColor: '#333', - bodyColor: '#666', - borderColor: '#ddd', + backgroundColor: "rgba(255, 255, 255, 0.9)", + titleColor: "#333", + bodyColor: "#666", + borderColor: "#ddd", borderWidth: 1, padding: 10, displayColors: false, callbacks: { - label: (context: any) => `获客数量: ${context.parsed.y}`, + label: (context) => `获客数量: ${context.parsed.y}`, }, }, }, @@ -111,21 +311,11 @@ export default function Home() { grid: { display: false, }, - ticks: { - font: { - size: 12, // 0.75rem - }, - }, }, y: { beginAtZero: true, grid: { - color: 'rgba(0, 0, 0, 0.05)', - }, - ticks: { - font: { - size: 12, // 0.75rem - }, + color: "rgba(0, 0, 0, 0.05)", }, }, }, @@ -133,91 +323,126 @@ export default function Home() { }); } - // 卸载时销毁 + // 组件卸载时清理图表实例 return () => { if (chartInstance.current) { chartInstance.current.destroy(); - chartInstance.current = null; } }; - }, []); + }, [isLoading]); + + if (isLoading) { + return ( + +
+

存客宝

+
+ + } + footer={} + > +
+
+
+ {[...Array(3)].map((_, i) => ( + +
+
+
+ ))} +
+
+
+
+ ); + } return ( -
-

存客宝

- -
- + + {apiError && ( +
+ API连接异常,显示默认数据 +
+ )} + + + } + /> } footer={} >
-
+
{/* 统计卡片 */} -
-
-
- 设备数量 -
- {stats.totalDevices} - +
+
+ +
+ 设备数量 +
+ {stats.totalDevices} + +
-
+
-
-
- 微信号数量 -
- {stats.totalWechatAccounts} - +
+ +
+ 微信号数量 +
+ {stats.totalWechatAccounts} + +
-
+
-
+
- 在线微信号 -
- {stats.onlineWechatAccounts} - -
-
-
+ 在线微信号 +
+ {stats.onlineWechatAccounts} +
+ 0 ? (stats.onlineWechatAccounts / stats.totalWechatAccounts) * 100 : 0 + } + className="h-1" + />
-
+
{/* 场景获客统计 */} -
-
-

场景获客统计

+ +
+

场景获客统计

{scenarioFeatures .sort((a, b) => b.value - a.value) + .slice(0, 4) // 只显示前4个 .map((scenario) => (
navigate(`/scenarios/${scenario.id}?name=${encodeURIComponent(scenario.name)}`)} > -
-
- {scenario.name} +
+
+ {scenario.name}
{scenario.value}
@@ -227,15 +452,37 @@ export default function Home() {
))}
-
+ + + {/* 今日数据统计 */} + +
+

今日数据

+
+
+ {todayStats.map((stat, index) => ( +
stat.path && navigate(stat.path)} + > +
{stat.icon}
+
+
{stat.value}
+
{stat.title}
+
+
+ ))} +
+
{/* 每日获客趋势 */} -
-

每日获客趋势

-
+ +

每日获客趋势

+
-
+
diff --git a/nkebao/src/pages/profile/Profile.tsx b/nkebao/src/pages/profile/Profile.tsx index 913b32d46..bb302b3b9 100644 --- a/nkebao/src/pages/profile/Profile.tsx +++ b/nkebao/src/pages/profile/Profile.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; -import { ChevronRight, Settings, Bell, LogOut } from 'lucide-react'; -import { Card } from '@/components/ui/card'; +import { ChevronRight, Settings, Bell, LogOut, Smartphone, MessageCircle, Database, FolderOpen } from 'lucide-react'; +import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; @@ -9,21 +9,21 @@ import { useAuth } from '@/contexts/AuthContext'; import { useToast } from '@/components/ui/toast'; import Layout from '@/components/Layout'; import BottomNav from '@/components/BottomNav'; +import UnifiedHeader from '@/components/UnifiedHeader'; import '@/components/Layout.css'; -const menuItems = [ - { href: '/devices', label: '设备管理' }, - { href: '/wechat-accounts', label: '微信号管理' }, - { href: '/traffic-pool', label: '流量池' }, - { href: '/content', label: '内容库' }, -]; - export default function Profile() { const navigate = useNavigate(); const { user, logout, isAuthenticated } = useAuth(); const { toast } = useToast(); const [showLogoutDialog, setShowLogoutDialog] = useState(false); const [userInfo, setUserInfo] = useState(null); + const [stats, setStats] = useState({ + devices: 12, + wechat: 25, + traffic: 8, + content: 156, + }); // 从localStorage获取用户信息 useEffect(() => { @@ -33,6 +33,82 @@ export default function Profile() { } }, []); + // 用户信息 + const currentUserInfo = { + name: userInfo?.username || user?.username || "卡若", + email: userInfo?.email || "zhangsan@example.com", + role: "管理员", + joinDate: "2023-01-15", + lastLogin: "2024-01-20 14:30", + }; + + // 功能模块数据 + const functionModules = [ + { + id: "devices", + title: "设备管理", + description: "管理您的设备和微信账号", + icon: , + count: stats.devices, + path: "/devices", + bgColor: "bg-blue-50", + }, + { + id: "wechat", + title: "微信号管理", + description: "管理微信账号和好友", + icon: , + count: stats.wechat, + path: "/wechat-accounts", + bgColor: "bg-green-50", + }, + { + id: "traffic", + title: "流量池", + description: "管理用户流量池和分组", + icon: , + count: stats.traffic, + path: "/traffic-pool", + bgColor: "bg-purple-50", + }, + { + id: "content", + title: "内容库", + description: "管理营销内容和素材", + icon: , + count: stats.content, + path: "/content", + bgColor: "bg-orange-50", + }, + ]; + + // 加载统计数据 + const loadStats = async () => { + try { + // 这里可以调用实际的API + // const [deviceStats, wechatStats, trafficStats, contentStats] = await Promise.allSettled([ + // getDeviceStats(), + // getWechatStats(), + // getTrafficStats(), + // getContentStats(), + // ]); + + // 暂时使用模拟数据 + setStats({ + devices: 12, + wechat: 25, + traffic: 8, + content: 156, + }); + } catch (error) { + console.error("加载统计数据失败:", error); + } + }; + + useEffect(() => { + loadStats(); + }, []); + const handleLogout = () => { // 清除本地存储的用户信息 localStorage.removeItem('token'); @@ -43,11 +119,15 @@ export default function Profile() { logout(); navigate('/login'); toast({ - title: '已退出登录', - description: '感谢使用存客宝', + title: '退出成功', + description: '您已安全退出系统', }); }; + const handleFunctionClick = (path: string) => { + navigate(path); + }; + if (!isAuthenticated) { return (
@@ -55,88 +135,105 @@ export default function Profile() {
); } + return ( -
-

我的

-
- - -
-
-
+ console.log('Notifications'), + }, + { + type: 'icon', + icon: Settings, + onClick: () => console.log('Settings'), + }, + ]} + /> } footer={} > -
-
- {/* 用户信息卡片 */} - -
- - - - {(userInfo?.username || user?.username || '用户').slice(0, 2).toUpperCase()} - - -
-

- {userInfo?.username || user?.username || '用户'} -

-

- 账号: {userInfo?.account || user?.account || Math.floor(10000000 + Math.random() * 90000000).toString()} -

-
- +
+
+ {/* 用户信息卡片 */} + + +
+ + + + {currentUserInfo.name.charAt(0)} + + +
+
+

{currentUserInfo.name}

+ + {currentUserInfo.role} + +
+

{currentUserInfo.email}

+
+
最近登录: {currentUserInfo.lastLogin}
+
+
+
+ + +
-
-
- + + - {/* 功能菜单 */} - - {menuItems.map((item) => ( -
(item.href ? navigate(item.href) : null)} - > -
- {item.label} + {/* 我的功能 */} + + +
+ {functionModules.map((module) => ( +
handleFunctionClick(module.path)} + > +
{module.icon}
+
+
{module.title}
+
{module.description}
+
+
+ + {module.count} + + +
+
+ ))}
- -
- ))} - + + - {/* 退出登录按钮 */} - -
+ {/* 退出登录 */} + +
+ {/* 退出登录确认对话框 */} diff --git a/nkebao/src/pages/scenarios/Scenarios.tsx b/nkebao/src/pages/scenarios/Scenarios.tsx index 307bf0792..721fd1989 100644 --- a/nkebao/src/pages/scenarios/Scenarios.tsx +++ b/nkebao/src/pages/scenarios/Scenarios.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import { Plus, TrendingUp, Loader2 } from 'lucide-react'; -import PageHeader from '@/components/PageHeader'; +import UnifiedHeader from '@/components/UnifiedHeader'; import Layout from '@/components/Layout'; import BottomNav from '@/components/BottomNav'; import { fetchScenes, type SceneItem } from '@/api/scenarios'; @@ -80,7 +80,7 @@ export default function Scenarios() { return ( @@ -101,7 +101,7 @@ export default function Scenarios() { return ( @@ -126,18 +126,19 @@ export default function Scenarios() { return ( - - 新建计划 - - } + titleColor="blue" + actions={[ + { + type: 'button', + icon: Plus, + label: '新建计划', + size: 'sm', + onClick: handleNewPlan, + }, + ]} /> } footer={} diff --git a/nkebao/src/pages/traffic-pool/TrafficPool.tsx b/nkebao/src/pages/traffic-pool/TrafficPool.tsx index 9b7d833ab..a2b486475 100644 --- a/nkebao/src/pages/traffic-pool/TrafficPool.tsx +++ b/nkebao/src/pages/traffic-pool/TrafficPool.tsx @@ -1,18 +1,88 @@ import React, { useState, useEffect, useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; import { Card } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; -import { Search, RefreshCw } from 'lucide-react'; -import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { + ChevronLeft, + Search, + RefreshCw, + Users, + Star, + X, + ChevronDown, + ChevronUp, + BarChart3, +} from 'lucide-react'; import { useToast } from '@/components/ui/toast'; +import { useDebounce } from '@/hooks/use-debounce'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Badge } from '@/components/ui/badge'; -import { get } from '@/api/request'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; + +// 接口定义 +interface Device { + id: string; + name: string; + status: "online" | "offline" | "busy"; + battery: number; + location: string; + wechatAccounts: number; + dailyAddLimit: number; + todayAdded: number; +} + +interface WechatAccount { + id: string; + nickname: string; + wechatId: string; + avatar: string; + deviceId: string; + status: "normal" | "limited" | "blocked"; + friendCount: number; + dailyAddLimit: number; +} + +interface CustomerService { + id: string; + name: string; + avatar: string; + status: "online" | "offline" | "busy"; + assignedUsers: number; +} + +interface TrafficPool { + id: string; + name: string; + description: string; + userCount: number; + tags: string[]; + createdAt: string; +} + +interface RFMScore { + recency: number; + frequency: number; + monetary: number; + total: number; + segment: string; + priority: "high" | "medium" | "low"; +} interface UserTag { id: string; name: string; color: string; + source: string; +} + +interface UserInteraction { + id: string; + type: "message" | "purchase" | "view" | "click"; + content: string; + timestamp: string; + value?: number; } interface TrafficUser { @@ -23,151 +93,965 @@ interface TrafficUser { phone: string; region: string; note: string; - status: number; + status: "pending" | "added" | "failed" | "duplicate"; addTime: string; source: string; - assignedTo: string; - category: 'potential' | 'customer' | 'lost'; + scenario: string; + deviceId: string; + wechatAccountId: string; + customerServiceId: string; + poolIds: string[]; tags: UserTag[]; + rfmScore: RFMScore; + lastInteraction: string; + totalSpent: number; + interactionCount: number; + conversionRate: number; + isDuplicate: boolean; + mergedAccounts: string[]; + addStatus: "not_added" | "adding" | "added" | "failed"; + interactions: UserInteraction[]; } -interface ApiResponse { - code: number; - msg: string; - data: T; -} +// 场景数据 +const SCENARIOS = [ + { id: "poster", name: "海报获客", icon: "🎨" }, + { id: "phone", name: "电话获客", icon: "📞" }, + { id: "douyin", name: "抖音获客", icon: "🎵" }, + { id: "xiaohongshu", name: "小红书获客", icon: "📖" }, + { id: "weixinqun", name: "微信群获客", icon: "👥" }, + { id: "api", name: "API获客", icon: "🔗" }, + { id: "order", name: "订单获客", icon: "📦" }, + { id: "payment", name: "付款码获客", icon: "💳" }, +]; -interface TrafficPoolResponse { - list: TrafficUser[]; - pagination: { - total: number; - current: number; - pageSize: number; - totalPages: number; - }; -} +const RFM_SEGMENTS = { + "555": { + name: "重要价值客户", + color: "bg-gradient-to-r from-red-500 to-pink-500 text-white border-0", + icon: "👑", + priority: "high", + }, + "554": { + name: "重要保持客户", + color: "bg-gradient-to-r from-purple-500 to-indigo-500 text-white border-0", + icon: "💎", + priority: "high", + }, + "544": { + name: "重要发展客户", + color: "bg-gradient-to-r from-blue-500 to-cyan-500 text-white border-0", + icon: "🚀", + priority: "high", + }, + "455": { + name: "重要挽留客户", + color: "bg-gradient-to-r from-orange-500 to-red-500 text-white border-0", + icon: "⚠️", + priority: "medium", + }, + "444": { + name: "一般价值客户", + color: "bg-gradient-to-r from-green-500 to-emerald-500 text-white border-0", + icon: "👤", + priority: "medium", + }, + "333": { + name: "一般保持客户", + color: "bg-gradient-to-r from-yellow-500 to-orange-500 text-white border-0", + icon: "📈", + priority: "medium", + }, + "222": { + name: "新用户", + color: "bg-gradient-to-r from-cyan-500 to-blue-500 text-white border-0", + icon: "🌟", + priority: "low", + }, + "111": { + name: "流失预警客户", + color: "bg-gradient-to-r from-gray-500 to-slate-500 text-white border-0", + icon: "😴", + priority: "low", + }, +} as const; + +// 模拟数据生成函数 +const generateMockDevices = (): Device[] => { + return Array.from({ length: 8 }, (_, i) => ({ + id: `device-${i + 1}`, + name: `设备${i + 1}`, + status: ["online", "offline", "busy"][Math.floor(Math.random() * 3)] as "online" | "offline" | "busy", + battery: Math.floor(Math.random() * 100), + location: ["北京", "上海", "广州", "深圳"][Math.floor(Math.random() * 4)], + wechatAccounts: Math.floor(Math.random() * 5) + 1, + dailyAddLimit: Math.random() > 0.5 ? 20 : 10, + todayAdded: Math.floor(Math.random() * 15), + })); +}; + +const generateMockWechatAccounts = (devices: Device[]): WechatAccount[] => { + const accounts: WechatAccount[] = []; + devices.forEach((device) => { + for (let i = 0; i < device.wechatAccounts; i++) { + accounts.push({ + id: `wx-${device.id}-${i + 1}`, + nickname: `微信${device.id.split("-")[1]}-${i + 1}`, + wechatId: `wxid_${Math.random().toString(36).substr(2, 8)}`, + avatar: `/placeholder.svg?height=40&width=40&query=wx${Math.floor(Math.random() * 10)}`, + deviceId: device.id, + status: ["normal", "limited", "blocked"][Math.floor(Math.random() * 3)] as "normal" | "limited" | "blocked", + friendCount: Math.floor(Math.random() * 4000) + 1000, + dailyAddLimit: Math.random() > 0.5 ? 20 : 10, + }); + } + }); + return accounts; +}; + +const generateMockCustomerServices = (): CustomerService[] => { + return Array.from({ length: 5 }, (_, i) => ({ + id: `cs-${i + 1}`, + name: `客服${i + 1}`, + avatar: `/placeholder.svg?height=40&width=40&query=cs${i}`, + status: ["online", "offline", "busy"][Math.floor(Math.random() * 3)] as "online" | "offline" | "busy", + assignedUsers: Math.floor(Math.random() * 100) + 50, + })); +}; + +const generateMockTrafficPools = (): TrafficPool[] => { + return [ + { + id: "pool-1", + name: "高价值客户池", + description: "包含所有高价值客户,优先添加", + userCount: 156, + tags: ["高价值", "优先添加", "重要客户"], + createdAt: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(), + }, + { + id: "pool-2", + name: "潜在客户池", + description: "有潜力的用户,需要进一步培养", + userCount: 289, + tags: ["潜在客户", "需培养"], + createdAt: new Date(Date.now() - 15 * 24 * 60 * 60 * 1000).toISOString(), + }, + { + id: "pool-3", + name: "新用户池", + description: "新注册或新添加的用户", + userCount: 432, + tags: ["新用户", "待分类"], + createdAt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(), + }, + ]; +}; + +const generateRFMScore = (): RFMScore => { + const recency = Math.floor(Math.random() * 5) + 1; + const frequency = Math.floor(Math.random() * 5) + 1; + const monetary = Math.floor(Math.random() * 5) + 1; + const total = recency + frequency + monetary; + + let segment: string; + let priority: "high" | "medium" | "low"; + + if (total >= 12) { + segment = Object.values(RFM_SEGMENTS)[Math.floor(Math.random() * 3)].name; + priority = "high"; + } else if (total >= 8) { + segment = Object.values(RFM_SEGMENTS)[3 + Math.floor(Math.random() * 3)].name; + priority = "medium"; + } else { + segment = Object.values(RFM_SEGMENTS)[6 + Math.floor(Math.random() * 2)].name; + priority = "low"; + } + + return { recency, frequency, monetary, total, segment, priority }; +}; + +const generateMockInteractions = (): UserInteraction[] => { + const types = ["message", "purchase", "view", "click"] as const; + return Array.from({ length: Math.floor(Math.random() * 10) + 1 }, (_, i) => { + const type = types[Math.floor(Math.random() * types.length)]; + return { + id: `interaction-${i + 1}`, + type, + content: type === "message" ? "用户发送了消息" : + type === "purchase" ? "用户购买了产品" : + type === "view" ? "用户查看了产品" : "用户点击了链接", + timestamp: new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000).toISOString(), + value: type === "purchase" ? Math.floor(Math.random() * 1000) + 100 : undefined, + }; + }); +}; + +const generateUserTags = (rfmScore: RFMScore): UserTag[] => { + const allTags = [ + { id: "tag-1", name: "活跃用户", color: "bg-green-100 text-green-800", source: "system" }, + { id: "tag-2", name: "高消费", color: "bg-red-100 text-red-800", source: "system" }, + { id: "tag-3", name: "忠实客户", color: "bg-blue-100 text-blue-800", source: "system" }, + { id: "tag-4", name: "新用户", color: "bg-yellow-100 text-yellow-800", source: "system" }, + { id: "tag-5", name: "VIP客户", color: "bg-purple-100 text-purple-800", source: "manual" }, + { id: "tag-6", name: "潜在客户", color: "bg-gray-100 text-gray-800", source: "system" }, + ]; + + // 根据RFM评分分配标签 + const tags: UserTag[] = []; + + if (rfmScore.priority === "high") { + tags.push(allTags[1], allTags[2]); // 高消费,忠实客户 + if (Math.random() > 0.5) tags.push(allTags[4]); // VIP客户 + } else if (rfmScore.priority === "medium") { + tags.push(allTags[0]); // 活跃用户 + if (Math.random() > 0.5) tags.push(allTags[5]); // 潜在客户 + } else { + tags.push(allTags[3]); // 新用户 + if (Math.random() > 0.3) tags.push(allTags[5]); // 潜在客户 + } + + return tags; +}; + +const mockDevices = generateMockDevices(); +const mockWechatAccounts = generateMockWechatAccounts(mockDevices); +const mockCustomerServices = generateMockCustomerServices(); +const mockTrafficPools = generateMockTrafficPools(); + +const generateMockUsers = ( + devices: Device[], + wechatAccounts: WechatAccount[], + customerServices: CustomerService[], + trafficPools: TrafficPool[], +): TrafficUser[] => { + return Array.from({ length: 500 }, (_, i) => { + const rfmScore = generateRFMScore(); + const tags = generateUserTags(rfmScore); + const interactions = generateMockInteractions(); + + const user: TrafficUser = { + id: `user-${i + 1}`, + avatar: `/placeholder.svg?height=40&width=40&query=user${Math.floor(Math.random() * 100)}`, + nickname: `用户${i + 1}`, + wechatId: `wx_${Math.random().toString(36).substr(2, 8)}`, + phone: `1${Math.floor(Math.random() * 9) + 1}${Math.random().toString().substr(2, 9)}`, + region: ["北京", "上海", "广州", "深圳", "杭州", "成都"][Math.floor(Math.random() * 6)], + note: Math.random() > 0.7 ? `这是用户${i + 1}的备注信息` : "", + status: ["pending", "added", "failed", "duplicate"][Math.floor(Math.random() * 4)] as any, + addTime: new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000).toISOString(), + source: SCENARIOS[Math.floor(Math.random() * SCENARIOS.length)].name, + scenario: SCENARIOS[Math.floor(Math.random() * SCENARIOS.length)].id, + deviceId: devices[Math.floor(Math.random() * devices.length)].id, + wechatAccountId: wechatAccounts[Math.floor(Math.random() * wechatAccounts.length)].id, + customerServiceId: customerServices[Math.floor(Math.random() * customerServices.length)].id, + poolIds: Math.random() > 0.5 ? [trafficPools[Math.floor(Math.random() * trafficPools.length)].id] : [], + tags, + rfmScore, + lastInteraction: new Date(Date.now() - Math.random() * 7 * 24 * 60 * 60 * 1000).toISOString(), + totalSpent: Math.floor(Math.random() * 10000), + interactionCount: Math.floor(Math.random() * 50) + 1, + conversionRate: Math.floor(Math.random() * 100), + isDuplicate: Math.random() > 0.9, + mergedAccounts: [], + addStatus: ["not_added", "adding", "added", "failed"][Math.floor(Math.random() * 4)] as any, + interactions, + }; + + return user; + }); +}; + +const mockUsers = generateMockUsers(mockDevices, mockWechatAccounts, mockCustomerServices, mockTrafficPools); export default function TrafficPool() { - const [users, setUsers] = useState([]); - const [activeCategory, setActiveCategory] = useState('potential'); - const [searchQuery, setSearchQuery] = useState(''); + const navigate = useNavigate(); + const { toast } = useToast(); - // 格式化时间 - const formatDateTime = (dateString: string) => { - if (!dateString) return '--'; + // 基础数据状态 + const [users, setUsers] = useState(mockUsers); + const [devices] = useState(mockDevices); + const [wechatAccounts] = useState(mockWechatAccounts); + const [customerServices] = useState(mockCustomerServices); + const [trafficPools] = useState(mockTrafficPools); + + // UI状态 + const [loading, setLoading] = useState(false); + const [selectedUsers, setSelectedUsers] = useState([]); + const [showFilters, setShowFilters] = useState(false); + const [showUserDetail, setShowUserDetail] = useState(false); + const [showAddToPool, setShowAddToPool] = useState(false); + const [selectedUser, setSelectedUser] = useState(null); + const [selectedPool, setSelectedPool] = useState(null); + + // 筛选状态 + const [deviceFilter, setDeviceFilter] = useState("all"); + const [poolFilter, setPoolFilter] = useState("all"); + const [valuationFilter, setValuationFilter] = useState("all"); + const [statusFilter, setStatusFilter] = useState("all"); + const [searchQuery, setSearchQuery] = useState(""); + + // 分页状态 + const [currentPage, setCurrentPage] = useState(1); + const usersPerPage = 10; + + // 数据分析展开状态 + const [showAnalytics, setShowAnalytics] = useState(false); + + const debouncedSearchQuery = useDebounce(searchQuery, 300); + + // 计算统计数据 + const stats = { + total: users.length, + highValue: users.filter((u) => u.rfmScore.priority === "high").length, + mediumValue: users.filter((u) => u.rfmScore.priority === "medium").length, + lowValue: users.filter((u) => u.rfmScore.priority === "low").length, + duplicates: users.filter((u) => u.isDuplicate).length, + pending: users.filter((u) => u.status === "pending").length, + added: users.filter((u) => u.status === "added").length, + failed: users.filter((u) => u.status === "failed").length, + avgSpent: Math.round(users.reduce((sum, u) => sum + u.totalSpent, 0) / users.length), + addSuccessRate: Math.round((users.filter((u) => u.status === "added").length / users.length) * 100), + duplicateRate: Math.round((users.filter((u) => u.isDuplicate).length / users.length) * 100), + }; + + // 过滤用户 + const filteredUsers = useCallback(() => { + return users.filter((user) => { + const matchesSearch = + !debouncedSearchQuery || + user.nickname.toLowerCase().includes(debouncedSearchQuery.toLowerCase()) || + user.wechatId.toLowerCase().includes(debouncedSearchQuery.toLowerCase()) || + user.phone.includes(debouncedSearchQuery); + + const matchesDevice = deviceFilter === "all" || user.deviceId === deviceFilter; + const matchesValuation = valuationFilter === "all" || user.rfmScore.priority === valuationFilter; + const matchesStatus = statusFilter === "all" || user.status === statusFilter; + const matchesPool = + poolFilter === "all" || (poolFilter === "none" ? user.poolIds.length === 0 : user.poolIds.includes(poolFilter)); + + return matchesSearch && matchesDevice && matchesValuation && matchesStatus && matchesPool; + }); + }, [users, debouncedSearchQuery, deviceFilter, valuationFilter, statusFilter, poolFilter])(); + + // 按优先级排序 + const sortedUsers = filteredUsers.sort((a, b) => { + const priorityOrder = { high: 3, medium: 2, low: 1 }; + return priorityOrder[b.rfmScore.priority] - priorityOrder[a.rfmScore.priority]; + }); + + // 计算分页 + const totalPages = Math.ceil(sortedUsers.length / usersPerPage); + const paginatedUsers = sortedUsers.slice((currentPage - 1) * usersPerPage, currentPage * usersPerPage); + + // 重置到第一页当筛选条件改变时 + useEffect(() => { + setCurrentPage(1); + }, [debouncedSearchQuery, deviceFilter, valuationFilter, statusFilter, poolFilter]); + + // 处理用户选择 + const handleUserSelect = useCallback((userId: string, checked: boolean) => { + setSelectedUsers((prev) => (checked ? [...prev, userId] : prev.filter((id) => id !== userId))); + }, []); + + // 处理全选 + const handleSelectAll = useCallback( + (checked: boolean) => { + if (checked) { + setSelectedUsers(paginatedUsers.map((user) => user.id)); + } else { + setSelectedUsers([]); + } + }, + [paginatedUsers], + ); + + // 重置筛选器 + const resetFilters = useCallback(() => { + setDeviceFilter("all"); + setPoolFilter("all"); + setValuationFilter("all"); + setStatusFilter("all"); + setSearchQuery(""); + setShowFilters(false); + }, []); + + // 刷新数据 + const handleRefresh = useCallback(() => { + setLoading(true); + setTimeout(() => { + const refreshedUsers = generateMockUsers(devices, wechatAccounts, customerServices, trafficPools); + setUsers(refreshedUsers); + setLoading(false); + toast({ + title: "刷新成功", + description: "流量池数据已更新", + }); + }, 800); + }, [devices, wechatAccounts, customerServices, trafficPools, toast]); + + // 添加到流量池 + const handleAddToPool = useCallback(() => { + if (selectedUsers.length === 0) { + toast({ + title: "请选择用户", + description: "请先选择要添加到流量池的用户", + }); + return; + } + setShowAddToPool(true); + }, [selectedUsers.length, toast]); + + // 确认添加到流量池 + const confirmAddToPool = useCallback(() => { + if (!selectedPool) { + toast({ + title: "请选择流量池", + description: "请选择要添加到的流量池", + }); + return; + } + + const updatedUsers = users.map((user) => { + if (selectedUsers.includes(user.id)) { + if (!user.poolIds.includes(selectedPool)) { + return { + ...user, + poolIds: [...user.poolIds, selectedPool], + addStatus: "added" as const, + }; + } + } + return user; + }); + + setUsers(updatedUsers); + setShowAddToPool(false); + setSelectedPool(null); + setSelectedUsers([]); + + const poolName = trafficPools.find((pool) => pool.id === selectedPool)?.name || "未知流量池"; + + toast({ + title: "添加成功", + description: `已将 ${selectedUsers.length} 个用户添加到 ${poolName}`, + }); + }, [selectedPool, selectedUsers, users, trafficPools, toast]); + + // 辅助函数 + const getRFMSegmentInfo = (segment: string) => { + const segmentEntry = Object.entries(RFM_SEGMENTS).find(([_, info]) => info.name === segment); + return segmentEntry ? segmentEntry[1] : { name: segment, color: "bg-gray-100 text-gray-800", icon: "👤" }; + }; + + const getWechatAccount = (accountId: string) => { + return wechatAccounts.find((acc) => acc.id === accountId); + }; + + const getCustomerService = (csId: string) => { + return customerServices.find((cs) => cs.id === csId); + }; + + const getDevice = (deviceId: string) => { + return devices.find((device) => device.id === deviceId); + }; + + const getPoolNames = (poolIds: string[]) => { + return poolIds.map((id) => trafficPools.find((pool) => pool.id === id)?.name).filter(Boolean).join(", "); + }; + + const formatDate = (dateString: string) => { + if (!dateString) return "--"; try { const date = new Date(dateString); - return date.toLocaleString('zh-CN', { - year: 'numeric', month: '2-digit', day: '2-digit', - hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false - }).replace(/\//g, '-'); + return date.toLocaleDateString("zh-CN"); } catch (error) { return dateString; } }; - const { toast } = useToast(); - // 获取流量池用户 - const fetchUsers = useCallback(async () => { - try { - const params = new URLSearchParams({ - page: '1', - limit: '30', - ...(searchQuery ? { keyword: searchQuery } : {}), - }); - const endpoint = activeCategory === 'customer' ? '/v1/traffic/pool/converted' : '/v1/traffic/pool'; - const response = await get>(`${endpoint}?${params.toString()}`); - if (response.code === 200 && response.data) { - setUsers(response.data.list); - } else { - toast({ title: '获取失败', description: response.msg || '获取流量池失败' }); - } - } catch (error: any) { - toast({ title: '网络错误', description: error?.message || '请检查网络连接' }); - } - }, [activeCategory, searchQuery, toast]); - useEffect(() => { - fetchUsers(); - }, [fetchUsers]); + // 用户卡片组件 + const UserCard = ({ user }: { user: TrafficUser }) => { + const wechatAccount = getWechatAccount(user.wechatAccountId); + const customerService = getCustomerService(user.customerServiceId); + const device = getDevice(user.deviceId); - return ( -
-
-
-

流量池

- -
-
-
-
- setSearchQuery(e.target.value)} - className="w-64" - /> - -
- - - 潜在客户 - 已转化 - 已流失 - - - -
- - - - - - - - - - - - - - - {users.map(user => ( - - - - - - - - - - + return ( +
{ + setSelectedUser(user); + setShowUserDetail(true); + }} + > + +
+ {/* 复选框和头像 */} +
+ handleUserSelect(user.id, e.target.checked)} + onClick={(e) => e.stopPropagation()} + className="mb-2 rounded border-gray-300 w-4 h-4" + /> + + + {user.nickname?.slice(0, 1) || "用户"} + +
+ + {/* 用户信息 */} +
+ {/* 用户名和状态 */} +
+
+ {user.nickname} + {user.rfmScore.priority === "high" && ( + + )} +
+ + {user.status === "added" + ? "已添加" + : user.status === "pending" + ? "未添加" + : user.status === "failed" + ? "添加失败" + : "重复"} + +
+ + {/* 微信号 */} +
+ {user.wechatId} +
+ + {/* 设备和客服信息 */} +
+ 📱 + {device?.name || "设备0"} + 👤 + {customerService?.name || "客服1"} +
+ + {/* 用户标签 */} +
+ {user.tags.slice(0, 3).map((tag) => ( + + {tag.name} + ))} - {users.length === 0 && ( -
- - + {user.tags.length > 3 && ( + + +{user.tags.length - 3} + )} - -
头像昵称微信号手机号地区标签备注添加时间
- - - {user.nickname?.slice(0, 2) || '用户'} - - {user.nickname}{user.wechatId}{user.phone}{user.region} - {user.tags?.map(tag => ( - {tag.name} - ))} - {user.note}{formatDateTime(user.addTime)}
暂无数据
+
+ + {/* 流量池信息 */} + {user.poolIds.length > 0 && ( +
+ 🏊 + {getPoolNames(user.poolIds)} +
+ )} +
+ ); + }; + + return ( +
+ {/* 顶部导航栏 */} +
+
+
+ +

流量池管理

+
+
+ {/* 数据分析按钮 */} + + +
+
+ + {/* 搜索栏 */} +
+
+
+ + setSearchQuery(e.target.value)} + className="pl-9" + /> +
+ +
+
+
+ + {/* 数据分析面板 - 可折叠 */} + {showAnalytics && ( +
+ {/* 核心指标 */} +
+ +
+
+
{filteredUsers.length}
+
总用户数
+
+ +
+
+ +
+
+
{stats.highValue}
+
高价值用户
+
+ +
+
+
+ + {/* 添加效率 */} + +
添加效率
+
+
+
{stats.addSuccessRate}%
+
成功率
+
+
+
¥{stats.avgSpent}
+
平均消费
+
+
+
+
+
{stats.added}
+
已添加
+
+
+
{stats.pending}
+
待添加
+
+
+
{stats.failed}
+
添加失败
+
+
+
+
+ )} + + {/* 筛选器侧边栏 */} + {showFilters && ( +
setShowFilters(false)}> +
e.stopPropagation()}> +
+
+

筛选选项

+ +
+
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+
+
+ )} + + {/* 主内容区域 */} +
+ {/* 操作栏 */} +
+
+
+ 0} + onChange={(e) => handleSelectAll(e.target.checked)} + className="rounded border-gray-300" + /> + + 已选择 {selectedUsers.length} 个用户 + + {selectedUsers.length > 0 && ( + + )} +
+
+ 共 {filteredUsers.length} 个用户,第 {currentPage}/{totalPages} 页 +
+
+
+ + {/* 用户列表 */} +
+ {loading ? ( + // 加载状态 + Array.from({ length: 10 }).map((_, i) => ( + +
+
+
+
+
+
+
+
+
+ )) + ) : paginatedUsers.length > 0 ? ( + paginatedUsers.map((user) => ) + ) : ( + +
没有找到符合条件的用户
+
+ )} +
+ + {/* 分页 */} + {totalPages > 1 && ( +
+
+ + + {currentPage} / {totalPages} + + +
+
+ )} +
+ + {/* 用户详情弹窗 */} + + + + 用户详情 + + {selectedUser && ( +
+
+ + + {selectedUser.nickname?.slice(0, 2) || "用户"} + +
+

{selectedUser.nickname}

+

{selectedUser.wechatId}

+

{selectedUser.phone}

+
+
+ +
+
+
总消费
+
¥{selectedUser.totalSpent.toLocaleString()}
+
+
+
互动次数
+
{selectedUser.interactionCount}
+
+
+
转化率
+
{selectedUser.conversionRate}%
+
+
+
最后互动
+
{formatDate(selectedUser.lastInteraction)}
+
+
+ +
+
用户标签
+
+ {selectedUser.tags.map((tag) => ( + + {tag.name} + + ))} +
+
+ + {selectedUser.note && ( +
+
备注
+
+ {selectedUser.note} +
+
+ )} +
+ )} +
+
+ + {/* 添加到流量池弹窗 */} + + + + 添加到流量池 + +
+
+ + +
+
+ + +
+
+
+
); } \ No newline at end of file diff --git a/nkebao/src/pages/workspace/Workspace.tsx b/nkebao/src/pages/workspace/Workspace.tsx index 60e90395a..6f38edd3d 100644 --- a/nkebao/src/pages/workspace/Workspace.tsx +++ b/nkebao/src/pages/workspace/Workspace.tsx @@ -5,7 +5,7 @@ import { Card, CardContent } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Progress } from '@/components/ui/progress'; import Layout from '@/components/Layout'; -import PageHeader from '@/components/PageHeader'; +import UnifiedHeader from '@/components/UnifiedHeader'; import BottomNav from '@/components/BottomNav'; import '@/components/Layout.css'; @@ -106,8 +106,9 @@ export default function Workspace() { return ( }