feat: 本次提交更新内容如下
流量池迁移
This commit is contained in:
354
nkebao/src/pages/traffic-pool/detail/index.module.scss
Normal file
354
nkebao/src/pages/traffic-pool/detail/index.module.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
469
nkebao/src/pages/traffic-pool/detail/index.tsx
Normal file
469
nkebao/src/pages/traffic-pool/detail/index.tsx
Normal file
@@ -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 (
|
||||||
|
<Layout>
|
||||||
|
<div className={styles.notFound}>
|
||||||
|
<div className={styles.notFoundText}>未找到该用户</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 <ClickOutline className={styles.interactionIcon} />;
|
||||||
|
case "message":
|
||||||
|
return <MessageOutline className={styles.interactionIcon} />;
|
||||||
|
case "purchase":
|
||||||
|
return <PayCircleOutline className={styles.interactionIcon} />;
|
||||||
|
case "view":
|
||||||
|
return <EyeOutline className={styles.interactionIcon} />;
|
||||||
|
default:
|
||||||
|
return <MessageOutline className={styles.interactionIcon} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Layout header={<NavCommon title="用户详情" />}>
|
||||||
|
<div className={styles.container}>
|
||||||
|
{/* 用户基本信息 */}
|
||||||
|
<Card className={styles.userCard}>
|
||||||
|
<div className={styles.userHeader}>
|
||||||
|
<Avatar src={user.avatar} className={styles.userAvatar}>
|
||||||
|
{user.nickname?.slice(0, 2) || "用户"}
|
||||||
|
</Avatar>
|
||||||
|
<div className={styles.userInfo}>
|
||||||
|
<div className={styles.userName}>
|
||||||
|
{user.nickname}
|
||||||
|
{user.rfmScore.priority === "high" && (
|
||||||
|
<StarFill className={styles.starIcon} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.userWechatId}>{user.wechatId}</div>
|
||||||
|
<div className={styles.userTags}>
|
||||||
|
{user.poolIds.length > 0 && (
|
||||||
|
<Tag color="primary" size="small">
|
||||||
|
{getPoolNames(user.poolIds)}
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
{user.status === "added" && (
|
||||||
|
<Tag color="success" size="small">
|
||||||
|
优先添加
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* RFM标签 */}
|
||||||
|
<div className={styles.rfmTags}>
|
||||||
|
{rfmSegment && (
|
||||||
|
<Tag color={rfmSegment.color} size="small">
|
||||||
|
{rfmSegment.name}
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
{user.status === "added" && (
|
||||||
|
<Tag color="warning" size="small">
|
||||||
|
优先添加
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Tab导航 */}
|
||||||
|
<Tabs
|
||||||
|
activeKey={activeTab}
|
||||||
|
onChange={setActiveTab}
|
||||||
|
className={styles.tabs}
|
||||||
|
>
|
||||||
|
<Tabs.Tab title="基本信息" key="base">
|
||||||
|
<div className={styles.tabContent}>
|
||||||
|
{/* 关键信息卡片 */}
|
||||||
|
<Card className={styles.infoCard}>
|
||||||
|
<div className={styles.cardTitle}>关键信息</div>
|
||||||
|
<Grid columns={2} gap={8}>
|
||||||
|
<div className={styles.infoItem}>
|
||||||
|
<div className={styles.infoLabel}>设备</div>
|
||||||
|
<div className={styles.infoValue}>
|
||||||
|
{device?.name || "--"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.infoItem}>
|
||||||
|
<div className={styles.infoLabel}>微信号</div>
|
||||||
|
<div className={styles.infoValue}>
|
||||||
|
{wechatAccount?.nickname || "--"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.infoItem}>
|
||||||
|
<div className={styles.infoLabel}>客服</div>
|
||||||
|
<div className={styles.infoValue}>
|
||||||
|
{customerService?.name || "--"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.infoItem}>
|
||||||
|
<div className={styles.infoLabel}>添加时间</div>
|
||||||
|
<div className={styles.infoValue}>
|
||||||
|
{formatDate(user.addTime)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.infoItem}>
|
||||||
|
<div className={styles.infoLabel}>最近互动</div>
|
||||||
|
<div className={styles.infoValue}>
|
||||||
|
{formatDate(user.lastInteraction)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Grid>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* RFM评分卡片 */}
|
||||||
|
<Card className={styles.rfmCard}>
|
||||||
|
<div className={styles.cardTitle}>RFM评分</div>
|
||||||
|
<div className={styles.rfmGrid}>
|
||||||
|
<div className={styles.rfmItem}>
|
||||||
|
<div className={styles.rfmValue}>
|
||||||
|
{user.rfmScore.recency}
|
||||||
|
</div>
|
||||||
|
<div className={styles.rfmLabel}>最近性(R)</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.rfmItem}>
|
||||||
|
<div className={styles.rfmValue}>
|
||||||
|
{user.rfmScore.frequency}
|
||||||
|
</div>
|
||||||
|
<div className={styles.rfmLabel}>频率(F)</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.rfmItem}>
|
||||||
|
<div className={styles.rfmValue}>
|
||||||
|
{user.rfmScore.monetary}
|
||||||
|
</div>
|
||||||
|
<div className={styles.rfmLabel}>金额(M)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 流量池按钮 */}
|
||||||
|
<div className={styles.poolButtons}>
|
||||||
|
<Button fill="outline" size="small">
|
||||||
|
潜在客户池
|
||||||
|
</Button>
|
||||||
|
<Button fill="outline" size="small">
|
||||||
|
流失预警池
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 统计数据卡片 */}
|
||||||
|
<Card className={styles.statsCard}>
|
||||||
|
<Grid columns={2} gap={16}>
|
||||||
|
<div className={styles.statItem}>
|
||||||
|
<div className={styles.statValue}>¥{user.totalSpent}</div>
|
||||||
|
<div className={styles.statLabel}>总消费</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.statItem}>
|
||||||
|
<div className={styles.statValue}>
|
||||||
|
{user.interactionCount}
|
||||||
|
</div>
|
||||||
|
<div className={styles.statLabel}>互动次数</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.statItem}>
|
||||||
|
<div className={styles.statValue}>
|
||||||
|
{user.conversionRate}%
|
||||||
|
</div>
|
||||||
|
<div className={styles.statLabel}>转化率</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.statItem}>
|
||||||
|
<div className={styles.statValue}>
|
||||||
|
{getStatusText(user.status)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.statLabel}>添加状态</div>
|
||||||
|
</div>
|
||||||
|
</Grid>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</Tabs.Tab>
|
||||||
|
|
||||||
|
<Tabs.Tab title="用户旅程" key="journey">
|
||||||
|
<div className={styles.tabContent}>
|
||||||
|
<Card className={styles.interactionCard}>
|
||||||
|
<div className={styles.cardTitle}>互动记录</div>
|
||||||
|
{user.interactions && user.interactions.length > 0 ? (
|
||||||
|
<List className={styles.interactionList}>
|
||||||
|
{user.interactions.slice(0, 4).map((interaction) => (
|
||||||
|
<List.Item
|
||||||
|
key={interaction.id}
|
||||||
|
className={styles.interactionItem}
|
||||||
|
prefix={getInteractionIcon(interaction.type)}
|
||||||
|
>
|
||||||
|
<div className={styles.interactionContent}>
|
||||||
|
<div className={styles.interactionTitle}>
|
||||||
|
{getInteractionTypeText(interaction.type)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.interactionDesc}>
|
||||||
|
{interaction.content}
|
||||||
|
{interaction.type === "purchase" &&
|
||||||
|
interaction.value && (
|
||||||
|
<span className={styles.interactionValue}>
|
||||||
|
¥{interaction.value}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.interactionTime}>
|
||||||
|
{formatDate(interaction.timestamp)}{" "}
|
||||||
|
{new Date(interaction.timestamp).toLocaleTimeString(
|
||||||
|
"zh-CN",
|
||||||
|
{
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</List.Item>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
) : (
|
||||||
|
<div className={styles.emptyState}>暂无互动记录</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</Tabs.Tab>
|
||||||
|
|
||||||
|
<Tabs.Tab title="用户标签" key="tags">
|
||||||
|
<div className={styles.tabContent}>
|
||||||
|
<Card className={styles.tagsCard}>
|
||||||
|
<div className={styles.cardTitle}>用户标签</div>
|
||||||
|
<div className={styles.tagsList}>
|
||||||
|
{user.tags.map((tag) => (
|
||||||
|
<Tag key={tag.id} color={tag.color} size="small">
|
||||||
|
{tag.name}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.valueTags}>
|
||||||
|
<div className={styles.valueTitle}>价值标签</div>
|
||||||
|
<div className={styles.valueTagItem}>
|
||||||
|
<Tag color="primary" size="small">
|
||||||
|
重要保持客户
|
||||||
|
</Tag>
|
||||||
|
<span className={styles.rfmScore}>
|
||||||
|
RFM总分:
|
||||||
|
{user.rfmScore.recency +
|
||||||
|
user.rfmScore.frequency +
|
||||||
|
user.rfmScore.monetary}
|
||||||
|
/15
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.valueTagItem}>
|
||||||
|
<span className={styles.valueLabel}>价值等级:</span>
|
||||||
|
<Tag color="danger" size="small">
|
||||||
|
高价值
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
block
|
||||||
|
fill="outline"
|
||||||
|
size="large"
|
||||||
|
className={styles.addTagButton}
|
||||||
|
>
|
||||||
|
➕ 添加新标签
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Tabs.Tab>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TrafficPoolDetail;
|
||||||
365
nkebao/src/pages/traffic-pool/list/index.module.scss
Normal file
365
nkebao/src/pages/traffic-pool/list/index.module.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
943
nkebao/src/pages/traffic-pool/list/index.tsx
Normal file
943
nkebao/src/pages/traffic-pool/list/index.tsx
Normal file
@@ -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<TrafficUser[]>([]);
|
||||||
|
const [devices] = useState<Device[]>(mockDevices);
|
||||||
|
const [wechatAccounts] = useState<WechatAccount[]>(mockWechatAccounts);
|
||||||
|
const [customerServices] = useState<CustomerService[]>(mockCustomerServices);
|
||||||
|
const [trafficPools] = useState<TrafficPool[]>(mockTrafficPools);
|
||||||
|
|
||||||
|
// UI状态
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [selectedUsers, setSelectedUsers] = useState<string[]>([]);
|
||||||
|
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 (
|
||||||
|
<Layout
|
||||||
|
header={
|
||||||
|
<NavCommon
|
||||||
|
title="流量池管理"
|
||||||
|
right={
|
||||||
|
<div className={styles.headerActions}>
|
||||||
|
<Button
|
||||||
|
fill="none"
|
||||||
|
size="small"
|
||||||
|
onClick={() => setShowAnalytics(!showAnalytics)}
|
||||||
|
>
|
||||||
|
<BarChartOutline />
|
||||||
|
{showAnalytics ? <ChevronUpOutline /> : <ChevronDownOutline />}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
fill="none"
|
||||||
|
size="small"
|
||||||
|
onClick={handleRefresh}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<RefreshOutline className={loading ? styles.spinning : ""} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
<div className={styles.container}>
|
||||||
|
{/* 数据分析面板 */}
|
||||||
|
{showAnalytics && (
|
||||||
|
<div className={styles.analyticsPanel}>
|
||||||
|
<div className={styles.statsGrid}>
|
||||||
|
<Card className={styles.statCard}>
|
||||||
|
<div className={styles.statContent}>
|
||||||
|
<div className={styles.statValue}>{filteredUsers.length}</div>
|
||||||
|
<div className={styles.statLabel}>总用户数</div>
|
||||||
|
</div>
|
||||||
|
<UserOutline className={styles.statIcon} />
|
||||||
|
</Card>
|
||||||
|
<Card className={styles.statCard}>
|
||||||
|
<div className={styles.statContent}>
|
||||||
|
<div className={styles.statValue}>{stats.highValue}</div>
|
||||||
|
<div className={styles.statLabel}>高价值用户</div>
|
||||||
|
</div>
|
||||||
|
<StarFill className={styles.statIcon} />
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
<Card className={styles.efficiencyCard}>
|
||||||
|
<div className={styles.efficiencyTitle}>添加效率</div>
|
||||||
|
<div className={styles.efficiencyGrid}>
|
||||||
|
<div className={styles.efficiencyItem}>
|
||||||
|
<div className={styles.efficiencyValue}>
|
||||||
|
{stats.addSuccessRate}%
|
||||||
|
</div>
|
||||||
|
<div className={styles.efficiencyLabel}>成功率</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.efficiencyItem}>
|
||||||
|
<div className={styles.efficiencyValue}>
|
||||||
|
¥{stats.avgSpent}
|
||||||
|
</div>
|
||||||
|
<div className={styles.efficiencyLabel}>平均消费</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.statusGrid}>
|
||||||
|
<div className={styles.statusItem}>
|
||||||
|
<div className={styles.statusValue}>{stats.added}</div>
|
||||||
|
<div className={styles.statusLabel}>已添加</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.statusItem}>
|
||||||
|
<div className={styles.statusValue}>{stats.pending}</div>
|
||||||
|
<div className={styles.statusLabel}>待添加</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.statusItem}>
|
||||||
|
<div className={styles.statusValue}>{stats.failed}</div>
|
||||||
|
<div className={styles.statusLabel}>添加失败</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 搜索和筛选 */}
|
||||||
|
<div className={styles.searchSection}>
|
||||||
|
<SearchBar
|
||||||
|
placeholder="搜索用户昵称、微信号、手机号"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={setSearchQuery}
|
||||||
|
style={{ "--border-radius": "8px" }}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
fill="outline"
|
||||||
|
size="small"
|
||||||
|
onClick={() => setShowFilters(true)}
|
||||||
|
className={styles.filterButton}
|
||||||
|
>
|
||||||
|
<FilterOutline />
|
||||||
|
筛选
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 操作栏 */}
|
||||||
|
<div className={styles.actionBar}>
|
||||||
|
<div className={styles.selectSection}>
|
||||||
|
<Checkbox
|
||||||
|
checked={
|
||||||
|
selectedUsers.length === paginatedUsers.length &&
|
||||||
|
paginatedUsers.length > 0
|
||||||
|
}
|
||||||
|
onChange={handleSelectAll}
|
||||||
|
>
|
||||||
|
全选
|
||||||
|
</Checkbox>
|
||||||
|
{selectedUsers.length > 0 && (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
color="primary"
|
||||||
|
onClick={handleAddToPool}
|
||||||
|
className={styles.addButton}
|
||||||
|
>
|
||||||
|
添加到流量池
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.totalCount}>
|
||||||
|
共 {filteredUsers.length} 个用户
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 用户列表 */}
|
||||||
|
<List className={styles.userList}>
|
||||||
|
{paginatedUsers.map((user) => {
|
||||||
|
const wechatAccount = getWechatAccount(user.wechatAccountId);
|
||||||
|
const customerService = getCustomerService(user.customerServiceId);
|
||||||
|
const device = getDevice(user.deviceId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<List.Item
|
||||||
|
key={user.id}
|
||||||
|
className={styles.userItem}
|
||||||
|
onClick={() => navigate(`/traffic-pool/detail/${user.id}`)}
|
||||||
|
prefix={
|
||||||
|
<div className={styles.userCheckbox}>
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedUsers.includes(user.id)}
|
||||||
|
onChange={(checked) => handleUserSelect(user.id, checked)}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
arrow={false}
|
||||||
|
>
|
||||||
|
<div className={styles.userContent}>
|
||||||
|
<div className={styles.userHeader}>
|
||||||
|
<div className={styles.userInfo}>
|
||||||
|
<Avatar src={user.avatar} className={styles.userAvatar}>
|
||||||
|
{user.nickname?.slice(0, 1) || "用户"}
|
||||||
|
</Avatar>
|
||||||
|
<div className={styles.userDetails}>
|
||||||
|
<div className={styles.userName}>
|
||||||
|
{user.nickname}
|
||||||
|
{user.rfmScore.priority === "high" && (
|
||||||
|
<StarFill className={styles.starIcon} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.userWechatId}>
|
||||||
|
{user.wechatId}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Tag color={getStatusColor(user.status)} size="small">
|
||||||
|
{getStatusText(user.status)}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.userMeta}>
|
||||||
|
<div className={styles.metaItem}>
|
||||||
|
<MobileOutline />
|
||||||
|
<span>{device?.name || "设备0"}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.metaItem}>
|
||||||
|
<UserOutline />
|
||||||
|
<span>{customerService?.name || "客服1"}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.userTags}>
|
||||||
|
{user.tags.slice(0, 3).map((tag) => (
|
||||||
|
<Tag key={tag.id} color={tag.color} size="small">
|
||||||
|
{tag.name}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
{user.tags.length > 3 && (
|
||||||
|
<Tag color="default" size="small">
|
||||||
|
+{user.tags.length - 3}
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{user.poolIds.length > 0 && (
|
||||||
|
<div className={styles.poolInfo}>
|
||||||
|
<TeamOutline />
|
||||||
|
<span>{getPoolNames(user.poolIds)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</List.Item>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</List>
|
||||||
|
|
||||||
|
{/* 无限滚动 */}
|
||||||
|
<InfiniteScroll loadMore={loadMore} hasMore={hasMore} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 筛选弹窗 */}
|
||||||
|
<Popup
|
||||||
|
visible={showFilters}
|
||||||
|
onMaskClick={() => setShowFilters(false)}
|
||||||
|
position="right"
|
||||||
|
bodyStyle={{ width: "80vw" }}
|
||||||
|
>
|
||||||
|
<div className={styles.filterPopup}>
|
||||||
|
<div className={styles.filterHeader}>
|
||||||
|
<span>筛选选项</span>
|
||||||
|
<Button
|
||||||
|
fill="none"
|
||||||
|
size="small"
|
||||||
|
onClick={() => setShowFilters(false)}
|
||||||
|
>
|
||||||
|
关闭
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.filterContent}>
|
||||||
|
<div className={styles.filterItem}>
|
||||||
|
<div className={styles.filterLabel}>设备</div>
|
||||||
|
<Selector
|
||||||
|
options={[
|
||||||
|
{ label: "全部设备", value: "all" },
|
||||||
|
...devices.map((device) => ({
|
||||||
|
label: `${device.name} - ${device.location}`,
|
||||||
|
value: device.id,
|
||||||
|
})),
|
||||||
|
]}
|
||||||
|
value={[deviceFilter]}
|
||||||
|
onChange={(arr) => setDeviceFilter(arr[0])}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.filterItem}>
|
||||||
|
<div className={styles.filterLabel}>流量池</div>
|
||||||
|
<Selector
|
||||||
|
options={[
|
||||||
|
{ label: "全部流量池", value: "all" },
|
||||||
|
{ label: "未分配", value: "none" },
|
||||||
|
...trafficPools.map((pool) => ({
|
||||||
|
label: pool.name,
|
||||||
|
value: pool.id,
|
||||||
|
})),
|
||||||
|
]}
|
||||||
|
value={[poolFilter]}
|
||||||
|
onChange={(arr) => setPoolFilter(arr[0])}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.filterItem}>
|
||||||
|
<div className={styles.filterLabel}>用户价值</div>
|
||||||
|
<Selector
|
||||||
|
options={[
|
||||||
|
{ label: "全部价值", value: "all" },
|
||||||
|
{ label: "高价值客户", value: "high" },
|
||||||
|
{ label: "中价值客户", value: "medium" },
|
||||||
|
{ label: "低价值客户", value: "low" },
|
||||||
|
]}
|
||||||
|
value={[valuationFilter]}
|
||||||
|
onChange={(arr) => setValuationFilter(arr[0])}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.filterItem}>
|
||||||
|
<div className={styles.filterLabel}>添加状态</div>
|
||||||
|
<Selector
|
||||||
|
options={[
|
||||||
|
{ label: "全部状态", value: "all" },
|
||||||
|
{ label: "待添加", value: "pending" },
|
||||||
|
{ label: "已添加", value: "added" },
|
||||||
|
{ label: "添加失败", value: "failed" },
|
||||||
|
{ label: "重复用户", value: "duplicate" },
|
||||||
|
]}
|
||||||
|
value={[statusFilter]}
|
||||||
|
onChange={(arr) => setStatusFilter(arr[0])}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.filterActions}>
|
||||||
|
<Button fill="outline" onClick={resetFilters}>
|
||||||
|
重置筛选
|
||||||
|
</Button>
|
||||||
|
<Button color="primary" onClick={() => setShowFilters(false)}>
|
||||||
|
应用筛选
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Popup>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TrafficPoolList;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import TrafficPool from "@/pages/traffic-pool/TrafficPool";
|
import TrafficPool from "@/pages/traffic-pool/list/index";
|
||||||
import TrafficPoolDetail from "@/pages/traffic-pool/TrafficPoolDetail";
|
import TrafficPoolDetail from "@/pages/traffic-pool/detail/index";
|
||||||
|
|
||||||
const trafficPoolRoutes = [
|
const trafficPoolRoutes = [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user