From a9789267e86c61ae3df3f54fd287881c55a1ed7a 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: Mon, 14 Jul 2025 10:39:13 +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=B5=81=E9=87=8F=E6=B1=A0=E9=80=BB=E8=BE=91=E6=9E=84=E5=BB=BA?= =?UTF-8?q?=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nkebao/src/App.tsx | 2 + nkebao/src/pages/traffic-pool/TrafficPool.tsx | 268 +++++++++--------- .../pages/traffic-pool/TrafficPoolDetail.tsx | 225 +++++++++++++++ 3 files changed, 359 insertions(+), 136 deletions(-) create mode 100644 nkebao/src/pages/traffic-pool/TrafficPoolDetail.tsx diff --git a/nkebao/src/App.tsx b/nkebao/src/App.tsx index f1e3347f7..8eebd60f5 100644 --- a/nkebao/src/App.tsx +++ b/nkebao/src/App.tsx @@ -36,6 +36,7 @@ import Orders from './pages/orders/Orders'; import TrafficPool from './pages/traffic-pool/TrafficPool'; import ContactImport from './pages/contact-import/ContactImport'; import Content from './pages/content/Content'; +import TrafficPoolDetail from './pages/traffic-pool/TrafficPoolDetail'; function App() { // 初始化HTTP拦截器 @@ -85,6 +86,7 @@ function App() { } /> } /> } /> + } /> } /> } /> {/* 你可以继续添加更多路由 */} diff --git a/nkebao/src/pages/traffic-pool/TrafficPool.tsx b/nkebao/src/pages/traffic-pool/TrafficPool.tsx index a2b486475..c0d27700a 100644 --- a/nkebao/src/pages/traffic-pool/TrafficPool.tsx +++ b/nkebao/src/pages/traffic-pool/TrafficPool.tsx @@ -20,9 +20,10 @@ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Badge } from '@/components/ui/badge'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import Layout from '@/components/Layout'; -// 接口定义 -interface Device { +// 1. 类型定义 +export interface Device { id: string; name: string; status: "online" | "offline" | "busy"; @@ -32,8 +33,7 @@ interface Device { dailyAddLimit: number; todayAdded: number; } - -interface WechatAccount { +export interface WechatAccount { id: string; nickname: string; wechatId: string; @@ -43,16 +43,14 @@ interface WechatAccount { friendCount: number; dailyAddLimit: number; } - -interface CustomerService { +export interface CustomerService { id: string; name: string; avatar: string; status: "online" | "offline" | "busy"; assignedUsers: number; } - -interface TrafficPool { +export interface TrafficPool { id: string; name: string; description: string; @@ -60,8 +58,7 @@ interface TrafficPool { tags: string[]; createdAt: string; } - -interface RFMScore { +export interface RFMScore { recency: number; frequency: number; monetary: number; @@ -69,23 +66,20 @@ interface RFMScore { segment: string; priority: "high" | "medium" | "low"; } - -interface UserTag { +export interface UserTag { id: string; name: string; color: string; source: string; } - -interface UserInteraction { +export interface UserInteraction { id: string; type: "message" | "purchase" | "view" | "click"; content: string; timestamp: string; value?: number; } - -interface TrafficUser { +export interface TrafficUser { id: string; avatar: string; nickname: string; @@ -113,8 +107,8 @@ interface TrafficUser { interactions: UserInteraction[]; } -// 场景数据 -const SCENARIOS = [ +// 2. mock数据和常量声明时直接export +export const SCENARIOS = [ { id: "poster", name: "海报获客", icon: "🎨" }, { id: "phone", name: "电话获客", icon: "📞" }, { id: "douyin", name: "抖音获客", icon: "🎵" }, @@ -124,8 +118,7 @@ const SCENARIOS = [ { id: "order", name: "订单获客", icon: "📦" }, { id: "payment", name: "付款码获客", icon: "💳" }, ]; - -const RFM_SEGMENTS = { +export const RFM_SEGMENTS = { "555": { name: "重要价值客户", color: "bg-gradient-to-r from-red-500 to-pink-500 text-white border-0", @@ -175,9 +168,7 @@ const RFM_SEGMENTS = { priority: "low", }, } as const; - -// 模拟数据生成函数 -const generateMockDevices = (): Device[] => { +export const generateMockDevices = (): Device[] => { return Array.from({ length: 8 }, (_, i) => ({ id: `device-${i + 1}`, name: `设备${i + 1}`, @@ -189,8 +180,7 @@ const generateMockDevices = (): Device[] => { todayAdded: Math.floor(Math.random() * 15), })); }; - -const generateMockWechatAccounts = (devices: Device[]): WechatAccount[] => { +export const generateMockWechatAccounts = (devices: Device[]): WechatAccount[] => { const accounts: WechatAccount[] = []; devices.forEach((device) => { for (let i = 0; i < device.wechatAccounts; i++) { @@ -208,8 +198,7 @@ const generateMockWechatAccounts = (devices: Device[]): WechatAccount[] => { }); return accounts; }; - -const generateMockCustomerServices = (): CustomerService[] => { +export const generateMockCustomerServices = (): CustomerService[] => { return Array.from({ length: 5 }, (_, i) => ({ id: `cs-${i + 1}`, name: `客服${i + 1}`, @@ -218,8 +207,7 @@ const generateMockCustomerServices = (): CustomerService[] => { assignedUsers: Math.floor(Math.random() * 100) + 50, })); }; - -const generateMockTrafficPools = (): TrafficPool[] => { +export const generateMockTrafficPools = (): TrafficPool[] => { return [ { id: "pool-1", @@ -247,8 +235,7 @@ const generateMockTrafficPools = (): TrafficPool[] => { }, ]; }; - -const generateRFMScore = (): RFMScore => { +export 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; @@ -271,7 +258,7 @@ const generateRFMScore = (): RFMScore => { return { recency, frequency, monetary, total, segment, priority }; }; -const generateMockInteractions = (): UserInteraction[] => { +export 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)]; @@ -287,7 +274,7 @@ const generateMockInteractions = (): UserInteraction[] => { }); }; -const generateUserTags = (rfmScore: RFMScore): UserTag[] => { +export 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" }, @@ -319,7 +306,7 @@ const mockWechatAccounts = generateMockWechatAccounts(mockDevices); const mockCustomerServices = generateMockCustomerServices(); const mockTrafficPools = generateMockTrafficPools(); -const generateMockUsers = ( +export const generateMockUsers = ( devices: Device[], wechatAccounts: WechatAccount[], customerServices: CustomerService[], @@ -582,8 +569,7 @@ export default function TrafficPool() {
{ - setSelectedUser(user); - setShowUserDetail(true); + navigate(`/traffic-pool/${user.id}`); }} > @@ -679,59 +665,60 @@ export default function TrafficPool() { }; return ( -
- {/* 顶部导航栏 */} -
-
-
- -

流量池管理

-
-
- {/* 数据分析按钮 */} - - -
-
- {/* 搜索栏 */} -
-
-
- - setSearchQuery(e.target.value)} - className="pl-9" - /> + +
+
+
+ +

流量池管理

+
+
+ {/* 数据分析按钮 */} + +
-
-
-
- {/* 数据分析面板 - 可折叠 */} + {/* 搜索栏 */} +
+
+
+ + setSearchQuery(e.target.value)} + className="pl-9" + /> +
+ +
+
+ + {/* 数据分析面板 - 可折叠 */} {showAnalytics && (
{/* 核心指标 */} @@ -786,6 +773,63 @@ export default function TrafficPool() {
)} + {/* 操作栏 */} +
+
+
+ 0} + onChange={(e) => handleSelectAll(e.target.checked)} + className="rounded border-gray-300" + /> + 全选 + {selectedUsers.length > 0 && ( + + )} +
+
+ 共 {filteredUsers.length} 个用户 +
+
+
+ + } + footer={ + <> + {/* 分页 */} + {totalPages > 1 && ( +
+
+ + + {currentPage} / {totalPages} + + +
+
+ )} + + } + > + + {/* 筛选器侧边栏 */} {showFilters && ( @@ -878,30 +922,7 @@ export default function TrafficPool() { {/* 主内容区域 */}
- {/* 操作栏 */} -
-
-
- 0} - onChange={(e) => handleSelectAll(e.target.checked)} - className="rounded border-gray-300" - /> - - 已选择 {selectedUsers.length} 个用户 - - {selectedUsers.length > 0 && ( - - )} -
-
- 共 {filteredUsers.length} 个用户,第 {currentPage}/{totalPages} 页 -
-
-
+ {/* 用户列表 */}
@@ -928,32 +949,6 @@ export default function TrafficPool() { )}
- {/* 分页 */} - {totalPages > 1 && ( -
-
- - - {currentPage} / {totalPages} - - -
-
- )}
{/* 用户详情弹窗 */} @@ -1052,6 +1047,7 @@ export default function TrafficPool() {
-
+ + ); } \ No newline at end of file diff --git a/nkebao/src/pages/traffic-pool/TrafficPoolDetail.tsx b/nkebao/src/pages/traffic-pool/TrafficPoolDetail.tsx new file mode 100644 index 000000000..59796bd23 --- /dev/null +++ b/nkebao/src/pages/traffic-pool/TrafficPoolDetail.tsx @@ -0,0 +1,225 @@ +import React from 'react'; +import { useParams } from 'react-router-dom'; +import Layout from '@/components/Layout'; +import UnifiedHeader from '@/components/UnifiedHeader'; +import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Card } from '@/components/ui/card'; +import { useState } from 'react'; +// 复用mock数据生成 +import { + generateMockDevices, + generateMockWechatAccounts, + generateMockCustomerServices, + generateMockTrafficPools, + generateMockUsers, + RFM_SEGMENTS, + TrafficUser, +} from './TrafficPool'; + +const devices = generateMockDevices(); +const wechatAccounts = generateMockWechatAccounts(devices); +const customerServices = generateMockCustomerServices(); +const trafficPools = generateMockTrafficPools(); +const users = generateMockUsers(devices, wechatAccounts, customerServices, trafficPools); + +function getUserById(id: string): TrafficUser | undefined { + return users.find((u: TrafficUser) => u.id === id); +} + +function getWechatAccount(accountId: string) { + return wechatAccounts.find((acc) => acc.id === accountId); +} +function getCustomerService(csId: string) { + return customerServices.find((cs) => cs.id === csId); +} +function getDevice(deviceId: string) { + return devices.find((device) => device.id === deviceId); +} +function getPoolNames(poolIds: string[]) { + return poolIds.map(id => trafficPools.find((pool) => pool.id === id)?.name).filter(Boolean).join(', '); +} +function formatDate(dateString: string) { + if (!dateString) return '--'; + try { + const date = new Date(dateString); + return date.toLocaleDateString('zh-CN'); + } catch (error) { + return dateString; + } +} + +export default function TrafficPoolDetail() { + const { id } = useParams(); + const user = getUserById(id as string); + if (!user) { + return
未找到该用户
; + } + const wechatAccount = getWechatAccount(user.wechatAccountId); + const customerService = getCustomerService(user.customerServiceId); + const device = getDevice(user.deviceId); + // RFM分段 + const rfmSegment = Object.values(RFM_SEGMENTS).find((seg: any) => seg.name === user.rfmScore.segment) as { name: string; color: string } | undefined; + const [activeTab, setActiveTab] = useState<'base' | 'journey' | 'tags'>('base'); + + return ( + + } + > +
+ {/* 头像与基本信息 */} +
+ + + {user.nickname?.slice(0, 2) || '用户'} + +
+
+ {user.nickname} + {user.poolIds.length > 0 && ( + {getPoolNames(user.poolIds)} + )} + {user.status === 'added' && 优先添加} +
+
{user.wechatId}
+
+
+ {/* 重要保持客户/优先添加等 */} +
+ {rfmSegment && ( + {rfmSegment.name} + )} + {user.status === 'added' && 优先添加} +
+ {/* Tab栏 */} +
+
setActiveTab('base')} + >基本信息
+
setActiveTab('journey')} + >用户旅程
+
setActiveTab('tags')} + >用户标签
+
+ + {/* Tab内容区 */} + {activeTab === 'base' && ( + <> + {/* 关键信息卡片 */} + +
关键信息
+
+
设备:{device?.name || '--'}
+
微信号:{wechatAccount?.nickname || '--'}
+
客服:{customerService?.name || '--'}
+
添加时间:{formatDate(user.addTime)}
+
最近互动:{formatDate(user.lastInteraction)}
+
+
+ {/* RFM评分卡片 */} + +
RFM评分
+
+
+
{user.rfmScore.recency}
+
最近性(R)
+
+
+
{user.rfmScore.frequency}
+
频率(F)
+
+
+
{user.rfmScore.monetary}
+
金额(M)
+
+
+
+ {/* 流量池按钮 */} +
+ + +
+ {/* 统计数据卡片 */} + +
+
¥{user.totalSpent}
+
总消费
+
+
+
{user.interactionCount}
+
互动次数
+
+
+
{user.conversionRate}%
+
转化率
+
+
+
{user.status === 'failed' ? '添加失败' : user.status === 'added' ? '添加成功' : '未添加'}
+
添加状态
+
+
+ + )} + {activeTab === 'journey' && ( + +
互动记录
+ {user.interactions && user.interactions.length > 0 ? ( + user.interactions.slice(0, 4).map((it, idx) => ( +
+
+ {it.type === 'click' && 📱} + {it.type === 'message' && 💬} + {it.type === 'purchase' && 💲} + {it.type === 'view' && 👁️} +
+
+
+ {it.type === 'click' && '点击行为'} + {it.type === 'message' && '消息互动'} + {it.type === 'purchase' && '购买行为'} + {it.type === 'view' && '页面浏览'} +
+
{it.content}{it.type === 'purchase' && it.value && ¥{it.value}}
+
+
{formatDate(it.timestamp)} {new Date(it.timestamp).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}
+
+ )) + ) : ( +
暂无互动记录
+ )} +
+ )} + {activeTab === 'tags' && ( +
+ +
用户标签
+
+ {user.tags.map(tag => ( + {tag.name} + ))} +
+
价值标签
+
+ 重要保持客户 + RFM总分:{user.rfmScore.recency + user.rfmScore.frequency + user.rfmScore.monetary}/15 +
+
+ 价值等级: + 高价值 +
+
+ +
+ )} +
+
+ ); +} \ No newline at end of file