From 40e7593f99862a2d909148f1a090d24eb5d5fcd3 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: Thu, 24 Jul 2025 11:10:25 +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=E8=BF=81=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../traffic-pool/detail/index.module.scss | 354 +++++++ .../src/pages/traffic-pool/detail/index.tsx | 469 +++++++++ .../pages/traffic-pool/list/index.module.scss | 365 +++++++ nkebao/src/pages/traffic-pool/list/index.tsx | 943 ++++++++++++++++++ nkebao/src/router/module/traffic-pool.tsx | 4 +- 5 files changed, 2133 insertions(+), 2 deletions(-) create mode 100644 nkebao/src/pages/traffic-pool/detail/index.module.scss create mode 100644 nkebao/src/pages/traffic-pool/detail/index.tsx create mode 100644 nkebao/src/pages/traffic-pool/list/index.module.scss create mode 100644 nkebao/src/pages/traffic-pool/list/index.tsx diff --git a/nkebao/src/pages/traffic-pool/detail/index.module.scss b/nkebao/src/pages/traffic-pool/detail/index.module.scss new file mode 100644 index 00000000..84fa5f36 --- /dev/null +++ b/nkebao/src/pages/traffic-pool/detail/index.module.scss @@ -0,0 +1,354 @@ +.container { + padding: 16px; + background: #f5f5f5; + min-height: 100vh; +} + +.notFound { + display: flex; + align-items: center; + justify-content: center; + height: 100vh; +} + +.notFoundText { + color: #999; + font-size: 16px; +} + +.userCard { + margin-bottom: 16px; + padding: 16px; + border-radius: 8px; + background: #fff; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.userHeader { + display: flex; + align-items: flex-start; + gap: 16px; + margin-bottom: 12px; +} + +.userAvatar { + width: 64px; + height: 64px; + border-radius: 50%; + flex-shrink: 0; +} + +.userInfo { + flex: 1; + min-width: 0; +} + +.userName { + font-size: 18px; + font-weight: 600; + color: #333; + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 4px; + line-height: 1.2; +} + +.starIcon { + color: #ff4d4f; + font-size: 16px; +} + +.userWechatId { + font-size: 14px; + color: #1677ff; + font-weight: 500; + margin-bottom: 8px; +} + +.userTags { + display: flex; + flex-wrap: wrap; + gap: 4px; +} + +.rfmTags { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.tabs { + background: #fff; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.tabContent { + padding: 16px; +} + +.cardTitle { + font-size: 14px; + font-weight: 500; + color: #333; + margin-bottom: 12px; +} + +.infoCard { + margin-bottom: 16px; + padding: 16px; + border-radius: 8px; + background: #fff; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.infoItem { + padding: 8px 0; +} + +.infoLabel { + font-size: 12px; + color: #666; + margin-bottom: 4px; +} + +.infoValue { + font-size: 14px; + color: #333; + font-weight: 500; +} + +.rfmCard { + margin-bottom: 16px; + padding: 16px; + border-radius: 8px; + background: #fff; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.rfmGrid { + display: flex; + justify-content: space-around; + text-align: center; +} + +.rfmItem { + flex: 1; +} + +.rfmValue { + font-size: 24px; + font-weight: bold; + line-height: 1; + margin-bottom: 4px; +} + +.rfmLabel { + font-size: 12px; + color: #666; +} + +.rfmItem:nth-child(1) .rfmValue { + color: #1677ff; +} + +.rfmItem:nth-child(2) .rfmValue { + color: #52c41a; +} + +.rfmItem:nth-child(3) .rfmValue { + color: #722ed1; +} + +.poolButtons { + display: flex; + gap: 12px; + margin-bottom: 16px; +} + +.statsCard { + padding: 16px; + border-radius: 8px; + background: #fff; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.statItem { + text-align: center; + padding: 8px 0; +} + +.statValue { + font-size: 18px; + font-weight: bold; + line-height: 1; + margin-bottom: 4px; +} + +.statLabel { + font-size: 12px; + color: #666; +} + +.statItem:nth-child(1) .statValue { + color: #52c41a; +} + +.statItem:nth-child(2) .statValue { + color: #1677ff; +} + +.statItem:nth-child(3) .statValue { + color: #faad14; +} + +.statItem:nth-child(4) .statValue { + color: #ff4d4f; +} + +.interactionCard { + padding: 16px; + border-radius: 8px; + background: #fff; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.interactionList { + margin: 0; + padding: 0; +} + +.interactionItem { + padding: 12px 0; + border-bottom: 1px solid #f0f0f0; + + &:last-child { + border-bottom: none; + } +} + +.interactionIcon { + font-size: 20px; + color: #1677ff; + margin-right: 12px; +} + +.interactionContent { + flex: 1; + min-width: 0; +} + +.interactionTitle { + font-size: 14px; + font-weight: 500; + color: #333; + margin-bottom: 4px; +} + +.interactionDesc { + font-size: 12px; + color: #666; + margin-bottom: 4px; + line-height: 1.4; +} + +.interactionValue { + color: #52c41a; + font-weight: bold; + margin-left: 4px; +} + +.interactionTime { + font-size: 11px; + color: #999; +} + +.emptyState { + text-align: center; + color: #999; + padding: 32px 0; + font-size: 14px; +} + +.tagsCard { + margin-bottom: 16px; + padding: 16px; + border-radius: 8px; + background: #fff; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.tagsList { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 16px; +} + +.valueTags { + border-top: 1px solid #f0f0f0; + padding-top: 16px; +} + +.valueTitle { + font-size: 14px; + font-weight: 500; + color: #333; + margin-bottom: 12px; +} + +.valueTagItem { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; +} + +.rfmScore { + font-size: 12px; + color: #666; +} + +.valueLabel { + font-size: 12px; + color: #666; +} + +.addTagButton { + margin-top: 16px; +} + +// 响应式设计 +@media (max-width: 375px) { + .container { + padding: 12px; + } + + .userHeader { + flex-direction: column; + align-items: center; + text-align: center; + } + + .userAvatar { + width: 80px; + height: 80px; + } + + .rfmGrid { + flex-direction: column; + gap: 16px; + } + + .poolButtons { + flex-direction: column; + } + + .statsCard { + .adm-grid { + grid-template-columns: 1fr 1fr; + } + } +} diff --git a/nkebao/src/pages/traffic-pool/detail/index.tsx b/nkebao/src/pages/traffic-pool/detail/index.tsx new file mode 100644 index 00000000..7389b966 --- /dev/null +++ b/nkebao/src/pages/traffic-pool/detail/index.tsx @@ -0,0 +1,469 @@ +import React, { useState } from "react"; +import { useParams } from "react-router-dom"; +import { Card, Tabs, Tag, Avatar, Button, List, Grid } from "antd-mobile"; +import { + UserOutline, + MobileOutline, + TeamOutline, + StarFill, + MessageOutline, + EyeOutline, + ClickOutline, + PayCircleOutline, +} from "antd-mobile-icons"; +import Layout from "@/components/Layout/Layout"; +import NavCommon from "@/components/NavCommon"; +import styles from "./index.module.scss"; + +// 复用类型定义和Mock数据生成函数 +import { + Device, + WechatAccount, + CustomerService, + TrafficPool, + RFMScore, + UserTag, + UserInteraction, + TrafficUser, + RFM_SEGMENTS, + generateMockDevices, + generateMockWechatAccounts, + generateMockCustomerServices, + generateMockTrafficPools, + generateRFMScore, + generateMockInteractions, + generateUserTags, +} from "../list"; + +const mockDevices = generateMockDevices(); +const mockWechatAccounts = generateMockWechatAccounts(mockDevices); +const mockCustomerServices = generateMockCustomerServices(); +const mockTrafficPools = generateMockTrafficPools(); + +// 生成Mock用户数据 +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: "海报获客", + scenario: "poster", + 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 users = generateMockUsers( + mockDevices, + mockWechatAccounts, + mockCustomerServices, + mockTrafficPools +); + +const TrafficPoolDetail: React.FC = () => { + const { id } = useParams(); + const [activeTab, setActiveTab] = useState("base"); + + const user = users.find((u: TrafficUser) => u.id === id); + + if (!user) { + return ( + +
+
未找到该用户
+
+
+ ); + } + + const wechatAccount = mockWechatAccounts.find( + (acc) => acc.id === user.wechatAccountId + ); + const customerService = mockCustomerServices.find( + (cs) => cs.id === user.customerServiceId + ); + const device = mockDevices.find((device) => device.id === user.deviceId); + const rfmSegment = Object.values(RFM_SEGMENTS).find( + (seg: any) => seg.name === user.rfmScore.segment + ); + + // 辅助函数 + const getPoolNames = (poolIds: string[]) => { + return poolIds + .map((id) => mockTrafficPools.find((pool) => pool.id === id)?.name) + .filter(Boolean) + .join(", "); + }; + + const formatDate = (dateString: string) => { + if (!dateString) return "--"; + try { + const date = new Date(dateString); + return date.toLocaleDateString("zh-CN"); + } catch (error) { + return dateString; + } + }; + + const getInteractionIcon = (type: string) => { + switch (type) { + case "click": + return ; + case "message": + return ; + case "purchase": + return ; + case "view": + return ; + default: + return ; + } + }; + + const getInteractionTypeText = (type: string) => { + switch (type) { + case "click": + return "点击行为"; + case "message": + return "消息互动"; + case "purchase": + return "购买行为"; + case "view": + return "页面浏览"; + default: + return type; + } + }; + + const getStatusText = (status: string) => { + switch (status) { + case "added": + return "已添加"; + case "pending": + return "未添加"; + case "failed": + return "添加失败"; + case "duplicate": + return "重复"; + default: + return status; + } + }; + + const getStatusColor = (status: string) => { + switch (status) { + case "added": + return "success"; + case "pending": + return "default"; + case "failed": + return "danger"; + case "duplicate": + return "warning"; + default: + return "default"; + } + }; + + return ( + }> +
+ {/* 用户基本信息 */} + +
+ + {user.nickname?.slice(0, 2) || "用户"} + +
+
+ {user.nickname} + {user.rfmScore.priority === "high" && ( + + )} +
+
{user.wechatId}
+
+ {user.poolIds.length > 0 && ( + + {getPoolNames(user.poolIds)} + + )} + {user.status === "added" && ( + + 优先添加 + + )} +
+
+
+ + {/* RFM标签 */} +
+ {rfmSegment && ( + + {rfmSegment.name} + + )} + {user.status === "added" && ( + + 优先添加 + + )} +
+
+ + {/* Tab导航 */} + + +
+ {/* 关键信息卡片 */} + +
关键信息
+ +
+
设备
+
+ {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}% +
+
转化率
+
+
+
+ {getStatusText(user.status)} +
+
添加状态
+
+
+
+
+
+ + +
+ +
互动记录
+ {user.interactions && user.interactions.length > 0 ? ( + + {user.interactions.slice(0, 4).map((interaction) => ( + +
+
+ {getInteractionTypeText(interaction.type)} +
+
+ {interaction.content} + {interaction.type === "purchase" && + interaction.value && ( + + ¥{interaction.value} + + )} +
+
+ {formatDate(interaction.timestamp)}{" "} + {new Date(interaction.timestamp).toLocaleTimeString( + "zh-CN", + { + hour: "2-digit", + minute: "2-digit", + } + )} +
+
+
+ ))} +
+ ) : ( +
暂无互动记录
+ )} +
+
+
+ + +
+ +
用户标签
+
+ {user.tags.map((tag) => ( + + {tag.name} + + ))} +
+ +
+
价值标签
+
+ + 重要保持客户 + + + RFM总分: + {user.rfmScore.recency + + user.rfmScore.frequency + + user.rfmScore.monetary} + /15 + +
+
+ 价值等级: + + 高价值 + +
+
+
+ + +
+
+
+
+
+ ); +}; + +export default TrafficPoolDetail; diff --git a/nkebao/src/pages/traffic-pool/list/index.module.scss b/nkebao/src/pages/traffic-pool/list/index.module.scss new file mode 100644 index 00000000..6c5bf3c0 --- /dev/null +++ b/nkebao/src/pages/traffic-pool/list/index.module.scss @@ -0,0 +1,365 @@ +.container { + padding: 0; + background: #f5f5f5; + min-height: 100vh; +} + +.headerActions { + display: flex; + align-items: center; + gap: 8px; +} + +.spinning { + animation: spin 1s linear infinite; +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +.analyticsPanel { + background: #fff; + padding: 16px; + margin-bottom: 8px; +} + +.statsGrid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; + margin-bottom: 16px; +} + +.statCard { + padding: 16px; + border-radius: 8px; + background: #fff; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + display: flex; + align-items: center; + justify-content: space-between; +} + +.statContent { + flex: 1; +} + +.statValue { + font-size: 24px; + font-weight: bold; + color: #1677ff; + line-height: 1; + margin-bottom: 4px; +} + +.statLabel { + font-size: 12px; + color: #666; +} + +.statIcon { + font-size: 24px; + color: #1677ff; +} + +.efficiencyCard { + padding: 16px; + border-radius: 8px; + background: #fff; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.efficiencyTitle { + font-size: 14px; + font-weight: 500; + color: #333; + margin-bottom: 12px; +} + +.efficiencyGrid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + margin-bottom: 16px; +} + +.efficiencyItem { + text-align: center; +} + +.efficiencyValue { + font-size: 18px; + font-weight: bold; + color: #1677ff; + line-height: 1; + margin-bottom: 4px; +} + +.efficiencyLabel { + font-size: 12px; + color: #666; +} + +.statusGrid { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 8px; + padding-top: 12px; + border-top: 1px solid #f0f0f0; +} + +.statusItem { + text-align: center; +} + +.statusValue { + font-size: 16px; + font-weight: bold; + line-height: 1; + margin-bottom: 4px; +} + +.statusLabel { + font-size: 12px; + color: #666; +} + +.statusItem:nth-child(1) .statusValue { + color: #52c41a; +} + +.statusItem:nth-child(2) .statusValue { + color: #faad14; +} + +.statusItem:nth-child(3) .statusValue { + color: #ff4d4f; +} + +.searchSection { + background: #fff; + padding: 12px 16px; + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 8px; +} + +.filterButton { + flex-shrink: 0; + display: flex; + align-items: center; + gap: 4px; + padding: 8px 12px; + border-radius: 6px; + font-size: 14px; +} + +.actionBar { + background: #fff; + padding: 12px 16px; + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; + border-bottom: 1px solid #f0f0f0; +} + +.selectSection { + display: flex; + align-items: center; + gap: 12px; +} + +.addButton { + font-size: 12px; + padding: 4px 8px; + height: 28px; +} + +.totalCount { + font-size: 12px; + color: #666; +} + +.userList { + background: #fff; + margin: 0; + padding: 0; +} + +.userItem { + padding: 16px; + border-bottom: 1px solid #f0f0f0; + cursor: pointer; + transition: background-color 0.2s; + + &:hover { + background-color: #fafafa; + } + + &:last-child { + border-bottom: none; + } +} + +.userCheckbox { + margin-right: 12px; +} + +.userContent { + flex: 1; + min-width: 0; +} + +.userHeader { + display: flex; + align-items: flex-start; + justify-content: space-between; + margin-bottom: 8px; +} + +.userInfo { + display: flex; + align-items: center; + gap: 12px; + flex: 1; + min-width: 0; +} + +.userAvatar { + width: 48px; + height: 48px; + border-radius: 50%; + flex-shrink: 0; +} + +.userDetails { + flex: 1; + min-width: 0; +} + +.userName { + font-size: 16px; + font-weight: 500; + color: #333; + display: flex; + align-items: center; + gap: 4px; + margin-bottom: 4px; + line-height: 1.2; +} + +.starIcon { + color: #ff4d4f; + font-size: 14px; +} + +.userWechatId { + font-size: 14px; + color: #1677ff; + font-weight: 500; +} + +.userMeta { + display: flex; + align-items: center; + gap: 16px; + margin-bottom: 8px; + font-size: 12px; + color: #666; +} + +.metaItem { + display: flex; + align-items: center; + gap: 4px; +} + +.userTags { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-bottom: 8px; +} + +.poolInfo { + display: flex; + align-items: center; + gap: 4px; + font-size: 12px; + color: #666; +} + +.filterPopup { + height: 100vh; + display: flex; + flex-direction: column; + background: #fff; +} + +.filterHeader { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px; + border-bottom: 1px solid #f0f0f0; + font-size: 16px; + font-weight: 500; +} + +.filterContent { + flex: 1; + padding: 16px; + overflow-y: auto; +} + +.filterItem { + margin-bottom: 24px; +} + +.filterLabel { + font-size: 14px; + font-weight: 500; + color: #333; + margin-bottom: 8px; +} + +.filterActions { + display: flex; + gap: 12px; + padding: 16px; + border-top: 1px solid #f0f0f0; + margin-top: auto; + + .adm-button { + flex: 1; + } +} + +// 响应式设计 +@media (max-width: 375px) { + .statsGrid { + grid-template-columns: 1fr; + } + + .efficiencyGrid { + grid-template-columns: 1fr; + } + + .statusGrid { + grid-template-columns: 1fr 1fr; + } + + .userMeta { + flex-direction: column; + align-items: flex-start; + gap: 8px; + } +} diff --git a/nkebao/src/pages/traffic-pool/list/index.tsx b/nkebao/src/pages/traffic-pool/list/index.tsx new file mode 100644 index 00000000..f4d1da91 --- /dev/null +++ b/nkebao/src/pages/traffic-pool/list/index.tsx @@ -0,0 +1,943 @@ +import React, { useState, useEffect, useCallback } from "react"; +import { useNavigate } from "react-router-dom"; +import { + Card, + List, + Button, + SearchBar, + Checkbox, + Tag, + Avatar, + Toast, + SpinLoading, + Popup, + Selector, + InfiniteScroll, +} from "antd-mobile"; +import { + SearchOutline, + FilterOutline, + RefreshOutline, + StarOutline, + StarFill, + UserOutline, + MobileOutline, + TeamOutline, + BarChartOutline, + ChevronDownOutline, + ChevronUpOutline, +} from "antd-mobile-icons"; +import Layout from "@/components/Layout/Layout"; +import NavCommon from "@/components/NavCommon"; +import styles from "./index.module.scss"; + +// 类型定义 +export interface Device { + id: string; + name: string; + status: "online" | "offline" | "busy"; + battery: number; + location: string; + wechatAccounts: number; + dailyAddLimit: number; + todayAdded: number; +} + +export interface WechatAccount { + id: string; + nickname: string; + wechatId: string; + avatar: string; + deviceId: string; + status: "normal" | "limited" | "blocked"; + friendCount: number; + dailyAddLimit: number; +} + +export interface CustomerService { + id: string; + name: string; + avatar: string; + status: "online" | "offline" | "busy"; + assignedUsers: number; +} + +export interface TrafficPool { + id: string; + name: string; + description: string; + userCount: number; + tags: string[]; + createdAt: string; +} + +export interface RFMScore { + recency: number; + frequency: number; + monetary: number; + total: number; + segment: string; + priority: "high" | "medium" | "low"; +} + +export interface UserTag { + id: string; + name: string; + color: string; + source: string; +} + +export interface UserInteraction { + id: string; + type: "message" | "purchase" | "view" | "click"; + content: string; + timestamp: string; + value?: number; +} + +export interface TrafficUser { + id: string; + avatar: string; + nickname: string; + wechatId: string; + phone: string; + region: string; + note: string; + status: "pending" | "added" | "failed" | "duplicate"; + addTime: string; + source: string; + 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[]; +} + +// 常量定义 +export 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: "💳" }, +]; + +export const RFM_SEGMENTS = { + "555": { + name: "重要价值客户", + color: "red", + icon: "👑", + priority: "high", + }, + "554": { + name: "重要保持客户", + color: "purple", + icon: "💎", + priority: "high", + }, + "544": { + name: "重要发展客户", + color: "blue", + icon: "🚀", + priority: "high", + }, + "455": { + name: "重要挽留客户", + color: "orange", + icon: "⚠️", + priority: "medium", + }, + "444": { + name: "一般价值客户", + color: "green", + icon: "👤", + priority: "medium", + }, + "333": { + name: "一般保持客户", + color: "yellow", + icon: "📈", + priority: "medium", + }, + "222": { + name: "新用户", + color: "cyan", + icon: "🌟", + priority: "low", + }, + "111": { + name: "流失预警客户", + color: "gray", + icon: "😴", + priority: "low", + }, +} as const; + +// Mock数据生成函数 +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: "success", source: "system" }, + { id: "tag-2", name: "高消费", color: "danger", source: "system" }, + { id: "tag-3", name: "忠实客户", color: "primary", source: "system" }, + { id: "tag-4", name: "新用户", color: "warning", source: "system" }, + { id: "tag-5", name: "VIP客户", color: "purple", source: "manual" }, + { id: "tag-6", name: "潜在客户", color: "default", source: "system" }, + ]; + + const tags: UserTag[] = []; + + if (rfmScore.priority === "high") { + tags.push(allTags[1], allTags[2]); + if (Math.random() > 0.5) tags.push(allTags[4]); + } 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 TrafficPoolList: React.FC = () => { + const navigate = useNavigate(); + + // 基础数据状态 + const [users, setUsers] = useState([]); + 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 [showAnalytics, setShowAnalytics] = useState(false); + const [hasMore, setHasMore] = useState(true); + const [page, setPage] = useState(1); + + // 筛选状态 + const [deviceFilter, setDeviceFilter] = useState("all"); + const [poolFilter, setPoolFilter] = useState("all"); + const [valuationFilter, setValuationFilter] = useState("all"); + const [statusFilter, setStatusFilter] = useState("all"); + const [searchQuery, setSearchQuery] = useState(""); + + // 初始化数据 + useEffect(() => { + const mockUsers = generateMockUsers( + devices, + wechatAccounts, + customerServices, + trafficPools + ); + setUsers(mockUsers); + }, [devices, wechatAccounts, customerServices, trafficPools]); + + // 计算统计数据 + 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 = users.filter((user) => { + const matchesSearch = + !searchQuery || + user.nickname.toLowerCase().includes(searchQuery.toLowerCase()) || + user.wechatId.toLowerCase().includes(searchQuery.toLowerCase()) || + user.phone.includes(searchQuery); + + 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 + ); + }); + + // 按优先级排序 + const sortedUsers = filteredUsers.sort((a, b) => { + const priorityOrder = { high: 3, medium: 2, low: 1 }; + return ( + priorityOrder[b.rfmScore.priority] - priorityOrder[a.rfmScore.priority] + ); + }); + + // 分页数据 + const pageSize = 20; + const paginatedUsers = sortedUsers.slice(0, page * pageSize); + + // 处理用户选择 + 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.show({ content: "刷新成功" }); + }, 800); + }, [devices, wechatAccounts, customerServices, trafficPools]); + + // 添加到流量池 + const handleAddToPool = useCallback(() => { + if (selectedUsers.length === 0) { + Toast.show({ content: "请先选择要添加到流量池的用户" }); + return; + } + Toast.show({ content: `已将 ${selectedUsers.length} 个用户添加到流量池` }); + setSelectedUsers([]); + }, [selectedUsers.length]); + + // 加载更多 + const loadMore = async () => { + if (page * pageSize >= sortedUsers.length) { + setHasMore(false); + return; + } + setPage((prev) => prev + 1); + }; + + // 辅助函数 + 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.toLocaleDateString("zh-CN"); + } catch (error) { + return dateString; + } + }; + + const getStatusText = (status: string) => { + switch (status) { + case "added": + return "已添加"; + case "pending": + return "未添加"; + case "failed": + return "添加失败"; + case "duplicate": + return "重复"; + default: + return status; + } + }; + + const getStatusColor = (status: string) => { + switch (status) { + case "added": + return "success"; + case "pending": + return "default"; + case "failed": + return "danger"; + case "duplicate": + return "warning"; + default: + return "default"; + } + }; + + return ( + + + + + } + /> + } + loading={loading} + > +
+ {/* 数据分析面板 */} + {showAnalytics && ( +
+
+ +
+
{filteredUsers.length}
+
总用户数
+
+ +
+ +
+
{stats.highValue}
+
高价值用户
+
+ +
+
+ +
添加效率
+
+
+
+ {stats.addSuccessRate}% +
+
成功率
+
+
+
+ ¥{stats.avgSpent} +
+
平均消费
+
+
+
+
+
{stats.added}
+
已添加
+
+
+
{stats.pending}
+
待添加
+
+
+
{stats.failed}
+
添加失败
+
+
+
+
+ )} + + {/* 搜索和筛选 */} +
+ + +
+ + {/* 操作栏 */} +
+
+ 0 + } + onChange={handleSelectAll} + > + 全选 + + {selectedUsers.length > 0 && ( + + )} +
+
+ 共 {filteredUsers.length} 个用户 +
+
+ + {/* 用户列表 */} + + {paginatedUsers.map((user) => { + const wechatAccount = getWechatAccount(user.wechatAccountId); + const customerService = getCustomerService(user.customerServiceId); + const device = getDevice(user.deviceId); + + return ( + navigate(`/traffic-pool/detail/${user.id}`)} + prefix={ +
+ handleUserSelect(user.id, checked)} + onClick={(e) => e.stopPropagation()} + /> +
+ } + arrow={false} + > +
+
+
+ + {user.nickname?.slice(0, 1) || "用户"} + +
+
+ {user.nickname} + {user.rfmScore.priority === "high" && ( + + )} +
+
+ {user.wechatId} +
+
+
+ + {getStatusText(user.status)} + +
+ +
+
+ + {device?.name || "设备0"} +
+
+ + {customerService?.name || "客服1"} +
+
+ +
+ {user.tags.slice(0, 3).map((tag) => ( + + {tag.name} + + ))} + {user.tags.length > 3 && ( + + +{user.tags.length - 3} + + )} +
+ + {user.poolIds.length > 0 && ( +
+ + {getPoolNames(user.poolIds)} +
+ )} +
+
+ ); + })} +
+ + {/* 无限滚动 */} + +
+ + {/* 筛选弹窗 */} + setShowFilters(false)} + position="right" + bodyStyle={{ width: "80vw" }} + > +
+
+ 筛选选项 + +
+ +
+
+
设备
+ ({ + label: `${device.name} - ${device.location}`, + value: device.id, + })), + ]} + value={[deviceFilter]} + onChange={(arr) => setDeviceFilter(arr[0])} + /> +
+ +
+
流量池
+ ({ + label: pool.name, + value: pool.id, + })), + ]} + value={[poolFilter]} + onChange={(arr) => setPoolFilter(arr[0])} + /> +
+ +
+
用户价值
+ setValuationFilter(arr[0])} + /> +
+ +
+
添加状态
+ setStatusFilter(arr[0])} + /> +
+ +
+ + +
+
+
+
+
+ ); +}; + +export default TrafficPoolList; diff --git a/nkebao/src/router/module/traffic-pool.tsx b/nkebao/src/router/module/traffic-pool.tsx index d0dcd70d..99872ab7 100644 --- a/nkebao/src/router/module/traffic-pool.tsx +++ b/nkebao/src/router/module/traffic-pool.tsx @@ -1,5 +1,5 @@ -import TrafficPool from "@/pages/traffic-pool/TrafficPool"; -import TrafficPoolDetail from "@/pages/traffic-pool/TrafficPoolDetail"; +import TrafficPool from "@/pages/traffic-pool/list/index"; +import TrafficPoolDetail from "@/pages/traffic-pool/detail/index"; const trafficPoolRoutes = [ {