FEAT => 本次更新项目为:

用户详情
This commit is contained in:
超级老白兔
2025-07-29 19:08:50 +08:00
parent 7e9b8e2341
commit dce6910371
8 changed files with 1018 additions and 502 deletions

View File

@@ -1,5 +1,27 @@
import request from "@/api/request";
import type { TrafficPoolUserDetail, UserJourneyResponse } from "./data";
export function getTrafficPoolDetail(wechatId: string): Promise<any> {
export function getTrafficPoolDetail(
wechatId: string
): Promise<TrafficPoolUserDetail> {
return request("/v1/wechats/getWechatInfo", { wechatId }, "GET");
}
// 获取用户旅程记录
export function getUserJourney(params: {
page: number;
pageSize: number;
userId: string;
}): Promise<UserJourneyResponse> {
return request("/v1/traffic/pool/getUserJourney", params, "GET");
}
// 获取用户标签
export function getUserTags(userId: string): Promise<any> {
return request("/v1/user/tags", { userId }, "GET");
}
// 添加用户标签
export function addUserTag(userId: string, tagData: any): Promise<any> {
return request("/v1/user/tags", { userId, ...tagData }, "POST");
}

View File

@@ -1,4 +1,4 @@
// 用户详情类型
// 用户详情类型 - 基于实际API返回数据
export interface TrafficPoolUserDetail {
userInfo: {
wechatId: string;
@@ -44,3 +44,67 @@ export interface TrafficPoolUserDetail {
reason: string;
}>;
}
// 用户旅程记录类型
export interface UserJourneyRecord {
id: number;
type: number; // 0-浏览, 2-提交订单, 3-注册
trafficPoolId: number;
remark: string;
count: number;
createTime: string;
updateTime: string;
}
// 用户旅程响应类型
export interface UserJourneyResponse {
list: UserJourneyRecord[];
total: number;
}
// 扩展的用户详情类型 - 用于前端展示
export interface ExtendedUserDetail extends TrafficPoolUserDetail {
// 前端计算或模拟的数据
rfmScore?: {
recency: number;
frequency: number;
monetary: number;
totalScore: number;
};
trafficPools?: {
currentPool: string;
availablePools: string[];
};
userJourney?: InteractionRecord[];
userTags?: UserTag[];
valueTags?: ValueTag[];
}
// 用户标签
export interface UserTag {
id: string;
name: string;
color: string;
icon?: string;
type: "user" | "value";
}
// 价值标签
export interface ValueTag {
id: string;
name: string;
color: string;
icon?: string;
rfmScore: number;
valueLevel: string;
}
// 互动记录
export interface InteractionRecord {
id: string;
type: "click" | "view" | "purchase";
action: string;
description: string;
timestamp: string;
icon: string;
}

View File

@@ -1,291 +1,420 @@
.container {
padding: 12px;
background: #f5f5f5;
min-height: 100vh;
}
.userCard {
margin-bottom: 12px;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.userInfo {
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
}
.avatar {
width: 64px;
height: 64px;
border-radius: 50%;
flex-shrink: 0;
}
.userDetails {
flex: 1;
min-width: 0;
}
.nickname {
font-size: 18px;
font-weight: 600;
color: #333;
margin-bottom: 4px;
line-height: 1.2;
}
.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;
}
.tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.genderTag {
font-size: 12px;
padding: 2px 8px;
border-radius: 12px;
}
.weightTag {
font-size: 12px;
padding: 2px 8px;
border-radius: 12px;
}
.tabs {
background: transparent;
:global(.adm-tabs-header) {
background: white;
border-radius: 12px 12px 0 0;
margin-bottom: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
:global(.adm-tabs-tab) {
font-size: 14px;
font-weight: 500;
}
:global(.adm-tabs-tab-active) {
color: #1677ff;
}
:global(.adm-tabs-tab-line) {
background: #1677ff;
}
:global(.adm-tabs-content) {
background: white;
border-radius: 0 0 12px 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
}
.tabContent {
padding: 16px;
}
.infoCard {
margin-bottom: 12px;
border-radius: 8px;
overflow: hidden;
&:last-child {
margin-bottom: 0;
}
:global(.adm-card-header) {
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
font-size: 14px;
font-weight: 600;
color: #333;
}
:global(.adm-card-body) {
padding: 0;
}
}
.statsGrid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
padding: 16px;
}
.statItem {
text-align: center;
}
.statValue {
font-size: 18px;
font-weight: 700;
line-height: 1.2;
margin-bottom: 4px;
}
.statLabel {
font-size: 12px;
color: #666;
line-height: 1.2;
}
.restrictionTitle {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
font-size: 14px;
font-weight: 500;
color: #333;
line-height: 1.4;
}
.restrictionLevel {
font-size: 10px;
padding: 2px 6px;
border-radius: 8px;
flex-shrink: 0;
}
.restrictionContent {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 12px;
color: #666;
line-height: 1.4;
margin-top: 4px;
}
.emptyState {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px 16px;
text-align: center;
}
.emptyText {
color: #999;
font-size: 14px;
line-height: 1.4;
}
// 响应式设计
@media (max-width: 375px) {
.container {
padding: 8px;
}
.userInfo {
padding: 12px;
gap: 12px;
}
.avatar {
width: 56px;
height: 56px;
}
.nickname {
font-size: 16px;
}
.wechatId {
font-size: 13px;
}
.alias {
font-size: 11px;
}
.tabContent {
padding: 12px;
}
.statsGrid {
gap: 12px;
padding: 12px;
}
.statValue {
font-size: 16px;
}
.restrictionTitle {
font-size: 13px;
}
.restrictionContent {
font-size: 11px;
}
}
// 暗色模式支持
@media (prefers-color-scheme: dark) {
.container {
background: #1a1a1a;
}
.userCard,
.tabs :global(.adm-tabs-header),
.tabs :global(.adm-tabs-content) {
background: #2a2a2a;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.nickname {
color: #fff;
}
.wechatId {
color: #4a9eff;
}
.alias {
color: #999;
}
.infoCard :global(.adm-card-header) {
color: #fff;
border-bottom-color: #3a3a3a;
}
.statLabel {
color: #999;
}
.restrictionTitle {
color: #fff;
}
.restrictionContent {
color: #999;
}
.emptyText {
color: #666;
}
}
.container {
padding: 0;
background: #f5f5f5;
min-height: 100vh;
}
// 头部样式
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
background: #fff;
border-bottom: 1px solid #f0f0f0;
.title {
font-size: 18px;
font-weight: 600;
color: #333;
}
.closeBtn {
padding: 8px;
border: none;
background: transparent;
color: #999;
font-size: 16px;
}
}
// 用户卡片
.userCard {
margin: 16px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.userInfo {
display: flex;
align-items: flex-start;
gap: 16px;
}
.avatar {
width: 60px;
height: 60px;
border-radius: 50%;
flex-shrink: 0;
}
.userDetails {
flex: 1;
min-width: 0;
}
.nickname {
font-size: 18px;
font-weight: 600;
color: #333;
margin-bottom: 4px;
}
.wechatId {
font-size: 14px;
color: #666;
margin-bottom: 8px;
}
.tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.userTag {
font-size: 12px;
padding: 4px 8px;
border-radius: 12px;
}
}
// 标签导航
.tabNav {
display: flex;
background: #fff;
margin: 0 16px;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
.tabItem {
flex: 1;
padding: 12px 16px;
text-align: center;
font-size: 14px;
color: #666;
cursor: pointer;
transition: all 0.3s ease;
border-bottom: 2px solid transparent;
&.active {
color: var(--primary-color);
border-bottom-color: var(--primary-color);
background: rgba(24, 142, 238, 0.05);
}
&:hover {
background: rgba(24, 142, 238, 0.05);
}
}
}
// 内容区域
.content {
padding: 16px;
}
.tabContent {
display: flex;
flex-direction: column;
gap: 16px;
}
// 信息卡片
.infoCard {
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
:global(.adm-card-header) {
padding: 16px;
border-bottom: 1px solid #f0f0f0;
font-weight: 600;
color: #333;
}
:global(.adm-card-body) {
padding: 0;
}
}
// RFM评分网格
.rfmGrid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
padding: 16px;
}
.rfmItem {
text-align: center;
padding: 12px;
background: #f8f9fa;
border-radius: 8px;
}
.rfmLabel {
font-size: 12px;
color: #666;
margin-bottom: 4px;
}
.rfmValue {
font-size: 18px;
font-weight: 600;
}
// 流量池区域
.poolSection {
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.currentPool,
.availablePools {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.poolLabel {
font-size: 14px;
color: #666;
white-space: nowrap;
}
// 统计数据网格
.statsGrid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
padding: 16px;
}
.statItem {
text-align: center;
padding: 12px;
background: #f8f9fa;
border-radius: 8px;
}
.statValue {
font-size: 18px;
font-weight: 600;
margin-bottom: 4px;
}
.statLabel {
font-size: 12px;
color: #666;
}
// 用户旅程
.journeyItem {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: #666;
margin-top: 4px;
}
.timestamp {
color: #999;
}
// 加载状态
.loadingContainer {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 16px;
text-align: center;
}
.loadingText {
font-size: 14px;
color: #999;
margin-top: 8px;
}
.loadingMore {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 16px;
color: #666;
font-size: 14px;
}
.loadMoreBtn {
display: flex;
justify-content: center;
padding: 16px;
}
// 标签区域
.tagsSection {
padding: 16px;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.valueTagsSection {
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.tagItem {
font-size: 12px;
padding: 6px 12px;
border-radius: 16px;
}
.valueTagContainer {
display: flex;
flex-direction: column;
gap: 8px;
}
.valueTagRow {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
}
.rfmScoreText {
font-size: 12px;
color: #666;
white-space: nowrap;
}
.valueLevelLabel {
font-size: 12px;
color: #666;
white-space: nowrap;
}
.valueTagItem {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
}
.valueInfo {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: #666;
}
// 添加标签按钮
.addTagBtn {
margin-top: 16px;
border-radius: 8px;
height: 48px;
font-size: 16px;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
// 空状态
.emptyState {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 16px;
text-align: center;
}
.emptyIcon {
margin-bottom: 16px;
opacity: 0.6;
}
.emptyText {
font-size: 16px;
color: #666;
margin-bottom: 8px;
font-weight: 500;
}
.emptyDesc {
font-size: 14px;
color: #999;
line-height: 1.4;
}
// 限制记录样式
.restrictionTitle {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
font-size: 14px;
font-weight: 500;
color: #333;
line-height: 1.4;
}
.restrictionLevel {
font-size: 10px;
padding: 2px 6px;
border-radius: 8px;
flex-shrink: 0;
}
.restrictionContent {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 12px;
color: #666;
line-height: 1.4;
margin-top: 4px;
}
// 响应式设计
@media (max-width: 375px) {
.rfmGrid,
.statsGrid {
grid-template-columns: 1fr;
}
.userInfo {
flex-direction: column;
text-align: center;
}
.avatar {
align-self: center;
}
.restrictionTitle {
font-size: 13px;
}
.restrictionContent {
font-size: 11px;
}
}

View File

@@ -1,33 +1,191 @@
import React, { useEffect, useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { Card, Button, Avatar, Tag, Tabs, List, Badge } from "antd-mobile";
import {
Card,
Button,
Avatar,
Tag,
Tabs,
List,
Badge,
SpinLoading,
} from "antd-mobile";
import {
UserOutlined,
MessageOutlined,
TeamOutlined,
ClockCircleOutlined,
ExclamationCircleOutlined,
CrownOutlined,
PlusOutlined,
CloseOutlined,
EyeOutlined,
DollarOutlined,
MobileOutlined,
TagOutlined,
FileTextOutlined,
UserAddOutlined,
} from "@ant-design/icons";
import Layout from "@/components/Layout/Layout";
import NavCommon from "@/components/NavCommon";
import { getTrafficPoolDetail } from "./api";
import type { TrafficPoolUserDetail } from "./data";
import { getTrafficPoolDetail, getUserJourney } from "./api";
import type {
TrafficPoolUserDetail,
ExtendedUserDetail,
InteractionRecord,
UserJourneyRecord,
} from "./data";
import styles from "./index.module.scss";
const TrafficPoolDetail: React.FC = () => {
const { id } = useParams();
const { wxid, userId } = useParams();
const navigate = useNavigate();
const [loading, setLoading] = useState(true);
const [user, setUser] = useState<TrafficPoolUserDetail | null>(null);
const [user, setUser] = useState<ExtendedUserDetail | null>(null);
const [activeTab, setActiveTab] = useState("basic");
// 用户旅程相关状态
const [journeyLoading, setJourneyLoading] = useState(false);
const [journeyList, setJourneyList] = useState<UserJourneyRecord[]>([]);
const [journeyPage, setJourneyPage] = useState(1);
const [journeyTotal, setJourneyTotal] = useState(0);
const pageSize = 10;
useEffect(() => {
if (!id) return;
if (!wxid) return;
setLoading(true);
getTrafficPoolDetail(id as string)
.then((res) => setUser(res))
getTrafficPoolDetail(wxid as string)
.then(res => {
// 将API数据转换为扩展的用户详情数据
const extendedUser: ExtendedUserDetail = {
...res,
// 模拟RFM评分数据
rfmScore: {
recency: 5,
frequency: 5,
monetary: 5,
totalScore: 15,
},
// 模拟流量池数据
trafficPools: {
currentPool: "新用户池",
availablePools: ["高价值客户池", "活跃用户池"],
},
// 模拟用户标签数据
userTags: [
{ id: "1", name: "近期活跃", color: "success", type: "user" },
{ id: "2", name: "高频互动", color: "primary", type: "user" },
{ id: "3", name: "高消费", color: "warning", type: "user" },
{ id: "4", name: "老客户", color: "danger", type: "user" },
],
// 模拟价值标签数据
valueTags: [
{
id: "1",
name: "重要保持客户",
color: "primary",
icon: "crown",
rfmScore: 14,
valueLevel: "高价值",
},
],
};
setUser(extendedUser);
})
.finally(() => setLoading(false));
}, [id]);
}, [wxid]);
// 获取用户旅程数据
const fetchUserJourney = async (page: number = 1) => {
if (!userId) return;
setJourneyLoading(true);
try {
const response = await getUserJourney({
page,
pageSize,
userId: userId,
});
if (page === 1) {
setJourneyList(response.list);
} else {
setJourneyList(prev => [...prev, ...response.list]);
}
setJourneyTotal(response.total);
setJourneyPage(page);
} catch (error) {
console.error("获取用户旅程失败:", error);
} finally {
setJourneyLoading(false);
}
};
// 标签切换处理
const handleTabChange = (tab: string) => {
setActiveTab(tab);
if (tab === "journey" && journeyList.length === 0) {
fetchUserJourney(1);
}
};
const handleClose = () => {
navigate(-1);
};
const getJourneyTypeIcon = (type: number) => {
switch (type) {
case 0: // 浏览
return <EyeOutlined style={{ color: "#722ed1" }} />;
case 2: // 提交订单
return <FileTextOutlined style={{ color: "#52c41a" }} />;
case 3: // 注册
return <UserAddOutlined style={{ color: "#1677ff" }} />;
default:
return <MobileOutlined style={{ color: "#999" }} />;
}
};
const getJourneyTypeText = (type: number) => {
switch (type) {
case 0:
return "浏览行为";
case 2:
return "提交订单";
case 3:
return "注册行为";
default:
return "其他行为";
}
};
const formatDateTime = (dateTime: string) => {
try {
const date = new Date(dateTime);
return date.toLocaleString("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
} catch (error) {
return dateTime;
}
};
const getActionIcon = (type: string) => {
switch (type) {
case "click":
return <MobileOutlined style={{ color: "#1677ff" }} />;
case "view":
return <EyeOutlined style={{ color: "#722ed1" }} />;
case "purchase":
return <DollarOutlined style={{ color: "#52c41a" }} />;
default:
return <MobileOutlined style={{ color: "#999" }} />;
}
};
const formatCurrency = (amount: number) => {
return `¥${amount.toLocaleString()}`;
};
const getGenderText = (gender: number) => {
switch (gender) {
@@ -87,16 +245,6 @@ const TrafficPoolDetail: React.FC = () => {
}
};
const formatAccountAge = (dateString: string) => {
if (!dateString) return "--";
try {
const date = new Date(dateString);
return date.toLocaleDateString("zh-CN");
} catch (error) {
return dateString;
}
};
if (!user) {
return (
<Layout header={<NavCommon title="用户详情" />} loading={loading}>
@@ -108,8 +256,21 @@ const TrafficPoolDetail: React.FC = () => {
}
return (
<Layout header={<NavCommon title="用户详情" />} loading={loading}>
<Layout loading={loading}>
<div className={styles.container}>
{/* 头部 */}
<div className={styles.header}>
<div className={styles.title}></div>
<Button
className={styles.closeBtn}
onClick={handleClose}
fill="none"
size="small"
>
<CloseOutlined />
</Button>
</div>
{/* 用户基本信息 */}
<Card className={styles.userCard}>
<div className={styles.userInfo}>
@@ -121,54 +282,167 @@ const TrafficPoolDetail: React.FC = () => {
<div className={styles.userDetails}>
<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}>
<Tag
color="primary"
fill="outline"
className={styles.genderTag}
style={{ color: getGenderColor(user.userInfo.gender) }}
>
{getGenderText(user.userInfo.gender)}
<Tag color="warning" fill="outline" className={styles.userTag}>
<CrownOutlined />
</Tag>
<Tag color="danger" fill="outline" className={styles.userTag}>
</Tag>
{user.userInfo.weight && (
<Tag
color="success"
fill="outline"
className={styles.weightTag}
>
: {user.userInfo.weight}
</Tag>
)}
</div>
</div>
</div>
</Card>
{/* Tab内容 */}
<Tabs className={styles.tabs}>
<Tabs.Tab title="基本信息" key="base">
{/* 导航标签 */}
<div className={styles.tabNav}>
<div
className={`${styles.tabItem} ${
activeTab === "basic" ? styles.active : ""
}`}
onClick={() => handleTabChange("basic")}
>
</div>
<div
className={`${styles.tabItem} ${
activeTab === "journey" ? styles.active : ""
}`}
onClick={() => handleTabChange("journey")}
>
</div>
<div
className={`${styles.tabItem} ${
activeTab === "tags" ? styles.active : ""
}`}
onClick={() => handleTabChange("tags")}
>
</div>
</div>
{/* 内容区域 */}
<div className={styles.content}>
{activeTab === "basic" && (
<div className={styles.tabContent}>
{/* 账户信息 */}
<Card title="账户信息" className={styles.infoCard}>
{/* 关联信息 */}
<Card title="关联信息" className={styles.infoCard}>
<List>
<List.Item extra={formatAccountAge(user.accountAge)}>
</List.Item>
<List.Item
extra={`${user.statistics.todayAdded}/${user.statistics.addLimit}`}
>
</List.Item>
<List.Item extra={user.activityLevel.allTimes}>
</List.Item>
<List.Item extra={user.activityLevel.dayTimes}>
</List.Item>
<List.Item extra="设备4"></List.Item>
<List.Item extra="微信4-1"></List.Item>
<List.Item extra="客服1"></List.Item>
<List.Item extra="2025/07/21"></List.Item>
<List.Item extra="2025/07/25"></List.Item>
</List>
</Card>
{/* RFM评分 */}
{user.rfmScore && (
<Card title="RFM评分" className={styles.infoCard}>
<div className={styles.rfmGrid}>
<div className={styles.rfmItem}>
<div className={styles.rfmLabel}>(R)</div>
<div
className={styles.rfmValue}
style={{ color: "#1677ff" }}
>
{user.rfmScore.recency}
</div>
</div>
<div className={styles.rfmItem}>
<div className={styles.rfmLabel}>(F)</div>
<div
className={styles.rfmValue}
style={{ color: "#52c41a" }}
>
{user.rfmScore.frequency}
</div>
</div>
<div className={styles.rfmItem}>
<div className={styles.rfmLabel}>(M)</div>
<div
className={styles.rfmValue}
style={{ color: "#722ed1" }}
>
{user.rfmScore.monetary}
</div>
</div>
<div className={styles.rfmItem}>
<div className={styles.rfmLabel}></div>
<div
className={styles.rfmValue}
style={{ color: "#ff4d4f" }}
>
{user.rfmScore.totalScore}/15
</div>
</div>
</div>
</Card>
)}
{/* 流量池 */}
{user.trafficPools && (
<Card title="流量池" className={styles.infoCard}>
<div className={styles.poolSection}>
<div className={styles.currentPool}>
<span className={styles.poolLabel}></span>
<Tag color="primary" fill="outline">
{user.trafficPools.currentPool}
</Tag>
</div>
<div className={styles.availablePools}>
<span className={styles.poolLabel}></span>
{user.trafficPools.availablePools.map((pool, index) => (
<Tag key={index} color="default" fill="outline">
{pool}
</Tag>
))}
</div>
</div>
</Card>
)}
{/* 统计数据 */}
<Card title="统计数据" className={styles.infoCard}>
<div className={styles.statsGrid}>
<div className={styles.statItem}>
<div
className={styles.statValue}
style={{ color: "#52c41a" }}
>
¥9561
</div>
<div className={styles.statLabel}></div>
</div>
<div className={styles.statItem}>
<div
className={styles.statValue}
style={{ color: "#1677ff" }}
>
6
</div>
<div className={styles.statLabel}></div>
</div>
<div className={styles.statItem}>
<div
className={styles.statValue}
style={{ color: "#722ed1" }}
>
3%
</div>
<div className={styles.statLabel}></div>
</div>
<div className={styles.statItem}>
<div className={styles.statValue} style={{ color: "#999" }}>
</div>
<div className={styles.statLabel}></div>
</div>
</div>
</Card>
{/* 好友统计 */}
<Card title="好友统计" className={styles.infoCard}>
<div className={styles.statsGrid}>
@@ -205,126 +479,16 @@ const TrafficPoolDetail: React.FC = () => {
</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}>
<div className={styles.statsGrid}>
<div className={styles.statItem}>
<div
className={styles.statValue}
style={{ color: "#52c41a" }}
>
{user.userInfo.activity.totalMsgCount}
</div>
<div className={styles.statLabel}></div>
</div>
<div className={styles.statItem}>
<div
className={styles.statValue}
style={{ color: "#faad14" }}
>
{user.userInfo.activity.sevenDayMsgCount}
</div>
<div className={styles.statLabel}>7</div>
</div>
<div className={styles.statItem}>
<div
className={styles.statValue}
style={{ color: "#722ed1" }}
>
{user.userInfo.activity.thirtyDayMsgCount}
</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="restrictions">
<div className={styles.tabContent}>
{/* 限制记录 */}
<Card title="限制记录" className={styles.infoCard}>
{user.restrictions && user.restrictions.length > 0 ? (
<List>
{user.restrictions.map((restriction) => (
{user.restrictions.map(restriction => (
<List.Item
key={restriction.id}
prefix={
<ExclamationCircleOutlined
style={{ color: "#ff4d4f" }}
/>
}
title={
<div className={styles.restrictionTitle}>
<span>{restriction.reason || "未知原因"}</span>
@@ -354,23 +518,161 @@ const TrafficPoolDetail: React.FC = () => {
</List>
) : (
<div className={styles.emptyState}>
<div className={styles.emptyIcon}>
<UserOutlined style={{ fontSize: 48, color: "#ccc" }} />
</div>
<div className={styles.emptyText}></div>
<div className={styles.emptyDesc}>
</div>
</div>
)}
</Card>
</div>
</Tabs.Tab>
)}
<Tabs.Tab title="操作记录" key="actions">
{activeTab === "journey" && (
<div className={styles.tabContent}>
<Card title="操作记录" className={styles.infoCard}>
<div className={styles.emptyState}>
<div className={styles.emptyText}></div>
</div>
<Card title="互动记录" className={styles.infoCard}>
{journeyLoading && journeyList.length === 0 ? (
<div className={styles.loadingContainer}>
<SpinLoading color="primary" style={{ fontSize: 24 }} />
<div className={styles.loadingText}>...</div>
</div>
) : journeyList.length === 0 ? (
<div className={styles.emptyState}>
<div className={styles.emptyIcon}>
<EyeOutlined style={{ fontSize: 48, color: "#ccc" }} />
</div>
<div className={styles.emptyText}></div>
<div className={styles.emptyDesc}>
</div>
</div>
) : (
<List>
{journeyList.map(record => (
<List.Item
key={record.id}
prefix={getJourneyTypeIcon(record.type)}
title={getJourneyTypeText(record.type)}
description={
<div className={styles.journeyItem}>
<span>{record.remark}</span>
<span className={styles.timestamp}>
{formatDateTime(record.createTime)}
</span>
</div>
}
/>
))}
{journeyLoading && journeyList.length > 0 && (
<div className={styles.loadingMore}>
<SpinLoading color="primary" style={{ fontSize: 16 }} />
<span>...</span>
</div>
)}
{!journeyLoading && journeyList.length < journeyTotal && (
<div className={styles.loadMoreBtn}>
<Button
size="small"
fill="outline"
onClick={() => fetchUserJourney(journeyPage + 1)}
>
</Button>
</div>
)}
</List>
)}
</Card>
</div>
</Tabs.Tab>
</Tabs>
)}
{activeTab === "tags" && (
<div className={styles.tabContent}>
{/* 用户标签 */}
<Card title="用户标签" className={styles.infoCard}>
{user.userTags && user.userTags.length > 0 ? (
<div className={styles.tagsSection}>
{user.userTags.map(tag => (
<Tag
key={tag.id}
color={tag.color}
fill="outline"
className={styles.tagItem}
>
{tag.name}
</Tag>
))}
</div>
) : (
<div className={styles.emptyState}>
<div className={styles.emptyIcon}>
<TagOutlined style={{ fontSize: 48, color: "#ccc" }} />
</div>
<div className={styles.emptyText}></div>
<div className={styles.emptyDesc}></div>
</div>
)}
</Card>
{/* 价值标签 */}
<Card title="价值标签" className={styles.infoCard}>
{user.valueTags && user.valueTags.length > 0 ? (
<div className={styles.valueTagsSection}>
{user.valueTags.map(tag => (
<div key={tag.id} className={styles.valueTagContainer}>
<div className={styles.valueTagRow}>
<Tag
color={tag.color}
fill="outline"
className={styles.tagItem}
>
{tag.icon === "crown" && <CrownOutlined />}
{tag.name}
</Tag>
<span className={styles.rfmScoreText}>
RFM总分: {tag.rfmScore}/15
</span>
</div>
<div className={styles.valueTagRow}>
<span className={styles.valueLevelLabel}>
:
</span>
<Tag color="danger" fill="outline">
{tag.valueLevel}
</Tag>
</div>
</div>
))}
</div>
) : (
<div className={styles.emptyState}>
<div className={styles.emptyIcon}>
<CrownOutlined style={{ fontSize: 48, color: "#ccc" }} />
</div>
<div className={styles.emptyText}></div>
<div className={styles.emptyDesc}>
</div>
</div>
)}
</Card>
{/* 添加新标签按钮 */}
<Button
block
color="primary"
size="large"
className={styles.addTagBtn}
>
<TagOutlined />
</Button>
</div>
)}
</div>
</div>
</Layout>
);

View File

@@ -192,7 +192,7 @@ const TrafficPoolList: React.FC = () => {
className={styles.card}
style={{ cursor: "pointer" }}
onClick={() =>
navigate(`/traffic-pool/detail/${item.sourceId}`)
navigate(`/traffic-pool/detail/${item.sourceId}/${item.id}`)
}
>
<div className={styles.cardContent}>

View File

@@ -29,7 +29,7 @@ const routes = [
auth: true,
},
{
path: "/traffic-pool/detail/:id",
path: "/traffic-pool/detail/:wxid/:userId",
element: <TrafficPoolDetail />,
auth: true,
},