存进度

This commit is contained in:
笔记本里的永平
2025-07-29 10:07:09 +08:00
parent 9facfe2ff8
commit 3a444a29b9
3 changed files with 319 additions and 230 deletions

View File

@@ -1,32 +1,46 @@
// 用户详情类型
export interface TrafficPoolUserDetail {
id: number;
nickname: string;
avatar: string;
wechatId: string;
status: number | string;
addTime: string;
lastInteraction: string;
deviceName?: string;
wechatAccountName?: string;
customerServiceName?: string;
poolNames?: string[];
rfmScore?: {
recency: number;
frequency: number;
monetary: number;
segment?: string;
userInfo: {
wechatId: string;
weight: number | null;
activity: {
totalMsgCount: number;
sevenDayMsgCount: number;
thirtyDayMsgCount: number;
yesterdayMsgCount: number;
};
friendShip: {
maleFriend: number;
groupNumber: number;
totalFriend: number;
femaleFriend: number;
unknowFriend: number;
};
nickname: string;
alias: string;
avatar: string;
gender: number; // 0-未知, 1-男, 2-女
};
totalSpent?: number;
interactionCount?: number;
conversionRate?: number;
tags?: string[];
packages?: string[];
interactions?: Array<{
id: string;
type: string;
content: string;
timestamp: string;
value?: number;
accountAge: string;
activityLevel: {
allTimes: number;
dayTimes: number;
};
accountWeight: {
ageWeight: number;
activityWeigth: number;
restrictWeight: number;
realNameWeight: number;
scope: number;
};
statistics: {
todayAdded: number;
addLimit: number;
};
restrictions: Array<{
id: number;
date: number | null;
level: number; // 1-轻微, 2-中等, 3-严重
reason: string;
}>;
}

View File

@@ -41,6 +41,13 @@
.wechatId {
font-size: 14px;
color: #1677ff;
margin-bottom: 4px;
line-height: 1.2;
}
.alias {
font-size: 12px;
color: #666;
margin-bottom: 8px;
line-height: 1.2;
}
@@ -51,7 +58,13 @@
gap: 6px;
}
.packageTag {
.genderTag {
font-size: 12px;
padding: 2px 8px;
border-radius: 12px;
}
.weightTag {
font-size: 12px;
padding: 2px 8px;
border-radius: 12px;
@@ -113,30 +126,6 @@
}
}
.rfmGrid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
padding: 16px;
}
.rfmItem {
text-align: center;
}
.rfmValue {
font-size: 20px;
font-weight: 700;
line-height: 1.2;
margin-bottom: 4px;
}
.rfmLabel {
font-size: 12px;
color: #666;
line-height: 1.2;
}
.statsGrid {
display: grid;
grid-template-columns: repeat(2, 1fr);
@@ -161,43 +150,32 @@
line-height: 1.2;
}
.interactionContent {
.restrictionTitle {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
font-size: 13px;
color: #666;
font-size: 14px;
font-weight: 500;
color: #333;
line-height: 1.4;
}
.purchaseValue {
color: #22c55e;
font-weight: 600;
}
.tagsContainer {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 16px;
padding: 16px;
}
.userTag {
font-size: 12px;
padding: 4px 12px;
border-radius: 16px;
}
.addTagButton {
margin: 0 16px 16px;
height: 40px;
.restrictionLevel {
font-size: 10px;
padding: 2px 6px;
border-radius: 8px;
font-size: 14px;
:global(.antd-mobile-icon) {
margin-right: 4px;
}
flex-shrink: 0;
}
.restrictionContent {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 12px;
color: #666;
line-height: 1.4;
margin-top: 4px;
}
.emptyState {
@@ -239,19 +217,14 @@
font-size: 13px;
}
.alias {
font-size: 11px;
}
.tabContent {
padding: 12px;
}
.rfmGrid {
gap: 12px;
padding: 12px;
}
.rfmValue {
font-size: 18px;
}
.statsGrid {
gap: 12px;
padding: 12px;
@@ -261,14 +234,12 @@
font-size: 16px;
}
.tagsContainer {
padding: 12px;
.restrictionTitle {
font-size: 13px;
}
.addTagButton {
margin: 0 12px 12px;
height: 36px;
font-size: 13px;
.restrictionContent {
font-size: 11px;
}
}
@@ -293,16 +264,27 @@
color: #4a9eff;
}
.alias {
color: #999;
}
.infoCard :global(.adm-card-header) {
color: #fff;
border-bottom-color: #3a3a3a;
}
.rfmLabel,
.statLabel {
color: #999;
}
.restrictionTitle {
color: #fff;
}
.restrictionContent {
color: #999;
}
.emptyText {
color: #666;
}

View File

@@ -4,8 +4,9 @@ import { Card, Button, Avatar, Tag, Tabs, List, Badge } from "antd-mobile";
import {
UserOutlined,
MessageOutlined,
ShoppingOutlined,
EyeOutlined,
TeamOutlined,
ClockCircleOutlined,
ExclamationCircleOutlined,
PlusOutlined,
} from "@ant-design/icons";
import Layout from "@/components/Layout/Layout";
@@ -28,46 +29,72 @@ const TrafficPoolDetail: React.FC = () => {
.finally(() => setLoading(false));
}, [id]);
const getInteractionIcon = (type: string) => {
switch (type) {
case "click":
return <UserOutlined style={{ color: "#ff6b35" }} />;
case "message":
return <MessageOutlined style={{ color: "#3b82f6" }} />;
case "purchase":
return <ShoppingOutlined style={{ color: "#22c55e" }} />;
case "view":
return <EyeOutlined style={{ color: "#8b5cf6" }} />;
const getGenderText = (gender: number) => {
switch (gender) {
case 1:
return "男";
case 2:
return "女";
default:
return <UserOutlined />;
return "未知";
}
};
const getInteractionTitle = (type: string) => {
switch (type) {
case "click":
return "点击行为";
case "message":
return "消息互动";
case "purchase":
return "购买行为";
case "view":
return "页面浏览";
const getGenderColor = (gender: number) => {
switch (gender) {
case 1:
return "#1677ff";
case 2:
return "#eb2f96";
default:
return "未知行为";
return "#999";
}
};
const getStatusText = (status: string | number) => {
if (status === "failed" || status === 0) return "添加失败";
if (status === "added" || status === 1) return "添加成功";
return "未添加";
const getRestrictionLevelText = (level: number) => {
switch (level) {
case 1:
return "轻微";
case 2:
return "中等";
case 3:
return "严重";
default:
return "未知";
}
};
const getStatusColor = (status: string | number) => {
if (status === "failed" || status === 0) return "danger";
if (status === "added" || status === 1) return "success";
return "default";
const getRestrictionLevelColor = (level: number) => {
switch (level) {
case 1:
return "warning";
case 2:
return "danger";
case 3:
return "danger";
default:
return "default";
}
};
const formatDate = (timestamp: number | null) => {
if (!timestamp) return "--";
try {
const date = new Date(timestamp * 1000);
return date.toLocaleDateString("zh-CN");
} catch (error) {
return "--";
}
};
const formatAccountAge = (dateString: string) => {
if (!dateString) return "--";
try {
const date = new Date(dateString);
return date.toLocaleDateString("zh-CN");
} catch (error) {
return dateString;
}
};
if (!user) {
@@ -87,24 +114,32 @@ const TrafficPoolDetail: React.FC = () => {
<Card className={styles.userCard}>
<div className={styles.userInfo}>
<Avatar
src={user.avatar}
src={user.userInfo.avatar}
className={styles.avatar}
fallback={<UserOutlined />}
/>
<div className={styles.userDetails}>
<div className={styles.nickname}>{user.nickname}</div>
<div className={styles.wechatId}>{user.wechatId}</div>
<div className={styles.nickname}>{user.userInfo.nickname}</div>
<div className={styles.wechatId}>{user.userInfo.wechatId}</div>
<div className={styles.alias}>{user.userInfo.alias}</div>
<div className={styles.tags}>
{user.packages?.map((pkg) => (
<Tag
color="primary"
fill="outline"
className={styles.genderTag}
style={{ color: getGenderColor(user.userInfo.gender) }}
>
{getGenderText(user.userInfo.gender)}
</Tag>
{user.userInfo.weight && (
<Tag
key={pkg}
color="primary"
color="success"
fill="outline"
className={styles.packageTag}
className={styles.weightTag}
>
{pkg}
: {user.userInfo.weight}
</Tag>
))}
)}
</div>
</div>
</div>
@@ -114,166 +149,224 @@ const TrafficPoolDetail: React.FC = () => {
<Tabs className={styles.tabs}>
<Tabs.Tab title="基本信息" key="base">
<div className={styles.tabContent}>
{/* 关键信息 */}
<Card title="关键信息" className={styles.infoCard}>
{/* 账户信息 */}
<Card title="账户信息" className={styles.infoCard}>
<List>
<List.Item extra={user.deviceName || "--"}></List.Item>
<List.Item extra={user.wechatAccountName || "--"}>
<List.Item extra={formatAccountAge(user.accountAge)}>
</List.Item>
<List.Item extra={user.customerServiceName || "--"}>
<List.Item
extra={`${user.statistics.todayAdded}/${user.statistics.addLimit}`}
>
</List.Item>
<List.Item extra={user.addTime || "--"}></List.Item>
<List.Item extra={user.lastInteraction || "--"}>
<List.Item extra={user.activityLevel.allTimes}>
</List.Item>
<List.Item extra={user.activityLevel.dayTimes}>
</List.Item>
</List>
</Card>
{/* RFM评分 */}
<Card title="RFM评分" className={styles.infoCard}>
<div className={styles.rfmGrid}>
<div className={styles.rfmItem}>
{/* 好友统计 */}
<Card title="好友统计" className={styles.infoCard}>
<div className={styles.statsGrid}>
<div className={styles.statItem}>
<div
className={styles.rfmValue}
className={styles.statValue}
style={{ color: "#1677ff" }}
>
{user.rfmScore?.recency ?? "-"}
{user.userInfo.friendShip.totalFriend}
</div>
<div className={styles.rfmLabel}>(R)</div>
<div className={styles.statLabel}></div>
</div>
<div className={styles.rfmItem}>
<div className={styles.statItem}>
<div
className={styles.rfmValue}
style={{ color: "#52c41a" }}
className={styles.statValue}
style={{ color: "#1677ff" }}
>
{user.rfmScore?.frequency ?? "-"}
{user.userInfo.friendShip.maleFriend}
</div>
<div className={styles.rfmLabel}>(F)</div>
<div className={styles.statLabel}></div>
</div>
<div className={styles.rfmItem}>
<div className={styles.statItem}>
<div
className={styles.rfmValue}
className={styles.statValue}
style={{ color: "#eb2f96" }}
>
{user.rfmScore?.monetary ?? "-"}
{user.userInfo.friendShip.femaleFriend}
</div>
<div className={styles.rfmLabel}>(M)</div>
<div className={styles.statLabel}></div>
</div>
<div className={styles.statItem}>
<div className={styles.statValue} style={{ color: "#999" }}>
{user.userInfo.friendShip.unknowFriend}
</div>
<div className={styles.statLabel}></div>
</div>
<div className={styles.statItem}>
<div
className={styles.statValue}
style={{ color: "#52c41a" }}
>
{user.userInfo.friendShip.groupNumber}
</div>
<div className={styles.statLabel}></div>
</div>
</div>
</Card>
{/* 统计数据 */}
<Card title="统计数据" className={styles.infoCard}>
{/* 活跃度统计 */}
<Card title="活跃度统计" className={styles.infoCard}>
<div className={styles.statsGrid}>
<div className={styles.statItem}>
<div
className={styles.statValue}
style={{ color: "#52c41a" }}
>
¥{user.totalSpent ?? "-"}
{user.userInfo.activity.totalMsgCount}
</div>
<div className={styles.statLabel}></div>
</div>
<div className={styles.statItem}>
<div
className={styles.statValue}
style={{ color: "#1677ff" }}
>
{user.interactionCount ?? "-"}
</div>
<div className={styles.statLabel}></div>
<div className={styles.statLabel}></div>
</div>
<div className={styles.statItem}>
<div
className={styles.statValue}
style={{ color: "#faad14" }}
>
{user.conversionRate ?? "-"}
{user.userInfo.activity.sevenDayMsgCount}
</div>
<div className={styles.statLabel}></div>
<div className={styles.statLabel}>7</div>
</div>
<div className={styles.statItem}>
<div
className={styles.statValue}
style={{ color: "#ff4d4f" }}
style={{ color: "#722ed1" }}
>
{getStatusText(user.status)}
{user.userInfo.activity.thirtyDayMsgCount}
</div>
<div className={styles.statLabel}></div>
<div className={styles.statLabel}>30</div>
</div>
<div className={styles.statItem}>
<div
className={styles.statValue}
style={{ color: "#13c2c2" }}
>
{user.userInfo.activity.yesterdayMsgCount}
</div>
<div className={styles.statLabel}></div>
</div>
</div>
</Card>
{/* 账户权重 */}
<Card title="账户权重" className={styles.infoCard}>
<div className={styles.statsGrid}>
<div className={styles.statItem}>
<div
className={styles.statValue}
style={{ color: "#1677ff" }}
>
{user.accountWeight.ageWeight}
</div>
<div className={styles.statLabel}></div>
</div>
<div className={styles.statItem}>
<div
className={styles.statValue}
style={{ color: "#52c41a" }}
>
{user.accountWeight.activityWeigth}
</div>
<div className={styles.statLabel}></div>
</div>
<div className={styles.statItem}>
<div
className={styles.statValue}
style={{ color: "#faad14" }}
>
{user.accountWeight.restrictWeight}
</div>
<div className={styles.statLabel}></div>
</div>
<div className={styles.statItem}>
<div
className={styles.statValue}
style={{ color: "#722ed1" }}
>
{user.accountWeight.realNameWeight}
</div>
<div className={styles.statLabel}></div>
</div>
<div className={styles.statItem}>
<div
className={styles.statValue}
style={{ color: "#13c2c2" }}
>
{user.accountWeight.scope}
</div>
<div className={styles.statLabel}></div>
</div>
</div>
</Card>
</div>
</Tabs.Tab>
<Tabs.Tab title="用户旅程" key="journey">
<Tabs.Tab title="限制记录" key="restrictions">
<div className={styles.tabContent}>
<Card title="互动记录" className={styles.infoCard}>
{user.interactions && user.interactions.length > 0 ? (
<Card title="限制记录" className={styles.infoCard}>
{user.restrictions && user.restrictions.length > 0 ? (
<List>
{user.interactions.slice(0, 4).map((interaction) => (
{user.restrictions.map((restriction) => (
<List.Item
key={interaction.id}
prefix={getInteractionIcon(interaction.type)}
title={getInteractionTitle(interaction.type)}
description={
<div className={styles.interactionContent}>
<span>{interaction.content}</span>
{interaction.type === "purchase" &&
interaction.value && (
<span className={styles.purchaseValue}>
¥{interaction.value}
</span>
key={restriction.id}
prefix={
<ExclamationCircleOutlined
style={{ color: "#ff4d4f" }}
/>
}
title={
<div className={styles.restrictionTitle}>
<span>{restriction.reason || "未知原因"}</span>
<Tag
color={getRestrictionLevelColor(
restriction.level
)}
fill="outline"
className={styles.restrictionLevel}
>
{getRestrictionLevelText(restriction.level)}
</Tag>
</div>
}
description={
<div className={styles.restrictionContent}>
<span>ID: {restriction.id}</span>
{restriction.date && (
<span>
: {formatDate(restriction.date)}
</span>
)}
</div>
}
extra={interaction.timestamp}
/>
))}
</List>
) : (
<div className={styles.emptyState}>
<div className={styles.emptyText}></div>
<div className={styles.emptyText}></div>
</div>
)}
</Card>
</div>
</Tabs.Tab>
<Tabs.Tab title="用户标签" key="tags">
<Tabs.Tab title="操作记录" key="actions">
<div className={styles.tabContent}>
<Card title="用户标签" className={styles.infoCard}>
<div className={styles.tagsContainer}>
{user.tags && user.tags.length > 0 ? (
user.tags.map((tag) => (
<Tag
key={tag}
color="primary"
fill="outline"
className={styles.userTag}
>
{tag}
</Tag>
))
) : (
<div className={styles.emptyText}></div>
)}
<Card title="操作记录" className={styles.infoCard}>
<div className={styles.emptyState}>
<div className={styles.emptyText}></div>
</div>
<Button
block
color="default"
fill="outline"
className={styles.addTagButton}
onClick={() => {
// TODO: 实现添加标签功能
console.log("添加新标签");
}}
>
<PlusOutlined />
</Button>
</Card>
</div>
</Tabs.Tab>