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,6 +1,5 @@
# 基础环境变量示例 # 基础环境变量示例
VITE_API_BASE_URL=http://www.yishi.com # VITE_API_BASE_URL=http://www.yishi.com
# VITE_API_BASE_URL=https://ckbapi.quwanzhi.com VITE_API_BASE_URL=https://ckbapi.quwanzhi.com
VITE_APP_TITLE=Nkebao Base VITE_APP_TITLE=Nkebao Base

View File

@@ -1,5 +1,27 @@
import request from "@/api/request"; 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"); 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 { export interface TrafficPoolUserDetail {
userInfo: { userInfo: {
wechatId: string; wechatId: string;
@@ -44,3 +44,67 @@ export interface TrafficPoolUserDetail {
reason: string; 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 { .container {
padding: 12px; padding: 0;
background: #f5f5f5; background: #f5f5f5;
min-height: 100vh; min-height: 100vh;
} }
.userCard { // 头部样式
margin-bottom: 12px; .header {
border-radius: 12px; display: flex;
overflow: hidden; justify-content: space-between;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); align-items: center;
} padding: 16px;
background: #fff;
.userInfo { border-bottom: 1px solid #f0f0f0;
display: flex;
align-items: center; .title {
gap: 16px; font-size: 18px;
padding: 16px; font-weight: 600;
} color: #333;
}
.avatar {
width: 64px; .closeBtn {
height: 64px; padding: 8px;
border-radius: 50%; border: none;
flex-shrink: 0; background: transparent;
} color: #999;
font-size: 16px;
.userDetails { }
flex: 1; }
min-width: 0;
} // 用户卡片
.userCard {
.nickname { margin: 16px;
font-size: 18px; border-radius: 12px;
font-weight: 600; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
color: #333;
margin-bottom: 4px; .userInfo {
line-height: 1.2; display: flex;
} align-items: flex-start;
gap: 16px;
.wechatId { }
font-size: 14px;
color: #1677ff; .avatar {
margin-bottom: 4px; width: 60px;
line-height: 1.2; height: 60px;
} border-radius: 50%;
flex-shrink: 0;
.alias { }
font-size: 12px;
color: #666; .userDetails {
margin-bottom: 8px; flex: 1;
line-height: 1.2; min-width: 0;
} }
.tags { .nickname {
display: flex; font-size: 18px;
flex-wrap: wrap; font-weight: 600;
gap: 6px; color: #333;
} margin-bottom: 4px;
}
.genderTag {
font-size: 12px; .wechatId {
padding: 2px 8px; font-size: 14px;
border-radius: 12px; color: #666;
} margin-bottom: 8px;
}
.weightTag {
font-size: 12px; .tags {
padding: 2px 8px; display: flex;
border-radius: 12px; flex-wrap: wrap;
} gap: 8px;
}
.tabs {
background: transparent; .userTag {
font-size: 12px;
:global(.adm-tabs-header) { padding: 4px 8px;
background: white; border-radius: 12px;
border-radius: 12px 12px 0 0; }
margin-bottom: 0; }
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
} // 标签导航
.tabNav {
:global(.adm-tabs-tab) { display: flex;
font-size: 14px; background: #fff;
font-weight: 500; margin: 0 16px;
} border-radius: 8px;
overflow: hidden;
:global(.adm-tabs-tab-active) { box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
color: #1677ff;
} .tabItem {
flex: 1;
:global(.adm-tabs-tab-line) { padding: 12px 16px;
background: #1677ff; text-align: center;
} font-size: 14px;
color: #666;
:global(.adm-tabs-content) { cursor: pointer;
background: white; transition: all 0.3s ease;
border-radius: 0 0 12px 12px; border-bottom: 2px solid transparent;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
} &.active {
} color: var(--primary-color);
border-bottom-color: var(--primary-color);
.tabContent { background: rgba(24, 142, 238, 0.05);
padding: 16px; }
}
&:hover {
.infoCard { background: rgba(24, 142, 238, 0.05);
margin-bottom: 12px; }
border-radius: 8px; }
overflow: hidden; }
&:last-child { // 内容区域
margin-bottom: 0; .content {
} padding: 16px;
}
:global(.adm-card-header) {
padding: 12px 16px; .tabContent {
border-bottom: 1px solid #f0f0f0; display: flex;
font-size: 14px; flex-direction: column;
font-weight: 600; gap: 16px;
color: #333; }
}
// 信息卡片
:global(.adm-card-body) { .infoCard {
padding: 0; border-radius: 12px;
} box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
} overflow: hidden;
.statsGrid { :global(.adm-card-header) {
display: grid; padding: 16px;
grid-template-columns: repeat(2, 1fr); border-bottom: 1px solid #f0f0f0;
gap: 16px; font-weight: 600;
padding: 16px; color: #333;
} }
.statItem { :global(.adm-card-body) {
text-align: center; padding: 0;
} }
}
.statValue {
font-size: 18px; // RFM评分网格
font-weight: 700; .rfmGrid {
line-height: 1.2; display: grid;
margin-bottom: 4px; grid-template-columns: repeat(2, 1fr);
} gap: 16px;
padding: 16px;
.statLabel { }
font-size: 12px;
color: #666; .rfmItem {
line-height: 1.2; text-align: center;
} padding: 12px;
background: #f8f9fa;
.restrictionTitle { border-radius: 8px;
display: flex; }
align-items: center;
justify-content: space-between; .rfmLabel {
gap: 8px; font-size: 12px;
font-size: 14px; color: #666;
font-weight: 500; margin-bottom: 4px;
color: #333; }
line-height: 1.4;
} .rfmValue {
font-size: 18px;
.restrictionLevel { font-weight: 600;
font-size: 10px; }
padding: 2px 6px;
border-radius: 8px; // 流量池区域
flex-shrink: 0; .poolSection {
} padding: 16px;
display: flex;
.restrictionContent { flex-direction: column;
display: flex; gap: 12px;
flex-direction: column; }
gap: 4px;
font-size: 12px; .currentPool,
color: #666; .availablePools {
line-height: 1.4; display: flex;
margin-top: 4px; align-items: center;
} gap: 8px;
flex-wrap: wrap;
.emptyState { }
display: flex;
flex-direction: column; .poolLabel {
align-items: center; font-size: 14px;
justify-content: center; color: #666;
padding: 48px 16px; white-space: nowrap;
text-align: center; }
}
// 统计数据网格
.emptyText { .statsGrid {
color: #999; display: grid;
font-size: 14px; grid-template-columns: repeat(2, 1fr);
line-height: 1.4; gap: 16px;
} padding: 16px;
}
// 响应式设计
@media (max-width: 375px) { .statItem {
.container { text-align: center;
padding: 8px; padding: 12px;
} background: #f8f9fa;
border-radius: 8px;
.userInfo { }
padding: 12px;
gap: 12px; .statValue {
} font-size: 18px;
font-weight: 600;
.avatar { margin-bottom: 4px;
width: 56px; }
height: 56px;
} .statLabel {
font-size: 12px;
.nickname { color: #666;
font-size: 16px; }
}
// 用户旅程
.wechatId { .journeyItem {
font-size: 13px; display: flex;
} justify-content: space-between;
align-items: center;
.alias { font-size: 12px;
font-size: 11px; color: #666;
} margin-top: 4px;
}
.tabContent {
padding: 12px; .timestamp {
} color: #999;
}
.statsGrid {
gap: 12px; // 加载状态
padding: 12px; .loadingContainer {
} display: flex;
flex-direction: column;
.statValue { align-items: center;
font-size: 16px; justify-content: center;
} padding: 40px 16px;
text-align: center;
.restrictionTitle { }
font-size: 13px;
} .loadingText {
font-size: 14px;
.restrictionContent { color: #999;
font-size: 11px; margin-top: 8px;
} }
}
.loadingMore {
// 暗色模式支持 display: flex;
@media (prefers-color-scheme: dark) { align-items: center;
.container { justify-content: center;
background: #1a1a1a; gap: 8px;
} padding: 16px;
color: #666;
.userCard, font-size: 14px;
.tabs :global(.adm-tabs-header), }
.tabs :global(.adm-tabs-content) {
background: #2a2a2a; .loadMoreBtn {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); display: flex;
} justify-content: center;
padding: 16px;
.nickname { }
color: #fff;
} // 标签区域
.tagsSection {
.wechatId { padding: 16px;
color: #4a9eff; display: flex;
} flex-wrap: wrap;
gap: 8px;
.alias { }
color: #999;
} .valueTagsSection {
padding: 16px;
.infoCard :global(.adm-card-header) { display: flex;
color: #fff; flex-direction: column;
border-bottom-color: #3a3a3a; gap: 12px;
} }
.statLabel { .tagItem {
color: #999; font-size: 12px;
} padding: 6px 12px;
border-radius: 16px;
.restrictionTitle { }
color: #fff;
} .valueTagContainer {
display: flex;
.restrictionContent { flex-direction: column;
color: #999; gap: 8px;
} }
.emptyText { .valueTagRow {
color: #666; 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 React, { useEffect, useState } from "react";
import { useParams, useNavigate } from "react-router-dom"; 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 { import {
UserOutlined, UserOutlined,
MessageOutlined, CrownOutlined,
TeamOutlined,
ClockCircleOutlined,
ExclamationCircleOutlined,
PlusOutlined, PlusOutlined,
CloseOutlined,
EyeOutlined,
DollarOutlined,
MobileOutlined,
TagOutlined,
FileTextOutlined,
UserAddOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import Layout from "@/components/Layout/Layout"; import Layout from "@/components/Layout/Layout";
import NavCommon from "@/components/NavCommon"; import NavCommon from "@/components/NavCommon";
import { getTrafficPoolDetail } from "./api"; import { getTrafficPoolDetail, getUserJourney } from "./api";
import type { TrafficPoolUserDetail } from "./data"; import type {
TrafficPoolUserDetail,
ExtendedUserDetail,
InteractionRecord,
UserJourneyRecord,
} from "./data";
import styles from "./index.module.scss"; import styles from "./index.module.scss";
const TrafficPoolDetail: React.FC = () => { const TrafficPoolDetail: React.FC = () => {
const { id } = useParams(); const { wxid, userId } = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
const [loading, setLoading] = useState(true); 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(() => { useEffect(() => {
if (!id) return; if (!wxid) return;
setLoading(true); setLoading(true);
getTrafficPoolDetail(id as string) getTrafficPoolDetail(wxid as string)
.then((res) => setUser(res)) .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)); .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) => { const getGenderText = (gender: number) => {
switch (gender) { 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) { if (!user) {
return ( return (
<Layout header={<NavCommon title="用户详情" />} loading={loading}> <Layout header={<NavCommon title="用户详情" />} loading={loading}>
@@ -108,8 +256,21 @@ const TrafficPoolDetail: React.FC = () => {
} }
return ( return (
<Layout header={<NavCommon title="用户详情" />} loading={loading}> <Layout loading={loading}>
<div className={styles.container}> <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}> <Card className={styles.userCard}>
<div className={styles.userInfo}> <div className={styles.userInfo}>
@@ -121,54 +282,167 @@ const TrafficPoolDetail: React.FC = () => {
<div className={styles.userDetails}> <div className={styles.userDetails}>
<div className={styles.nickname}>{user.userInfo.nickname}</div> <div className={styles.nickname}>{user.userInfo.nickname}</div>
<div className={styles.wechatId}>{user.userInfo.wechatId}</div> <div className={styles.wechatId}>{user.userInfo.wechatId}</div>
<div className={styles.alias}>{user.userInfo.alias}</div>
<div className={styles.tags}> <div className={styles.tags}>
<Tag <Tag color="warning" fill="outline" className={styles.userTag}>
color="primary" <CrownOutlined />
fill="outline"
className={styles.genderTag} </Tag>
style={{ color: getGenderColor(user.userInfo.gender) }} <Tag color="danger" fill="outline" className={styles.userTag}>
>
{getGenderText(user.userInfo.gender)}
</Tag> </Tag>
{user.userInfo.weight && (
<Tag
color="success"
fill="outline"
className={styles.weightTag}
>
: {user.userInfo.weight}
</Tag>
)}
</div> </div>
</div> </div>
</div> </div>
</Card> </Card>
{/* Tab内容 */} {/* 导航标签 */}
<Tabs className={styles.tabs}> <div className={styles.tabNav}>
<Tabs.Tab title="基本信息" key="base"> <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}> <div className={styles.tabContent}>
{/* 账户信息 */} {/* 关联信息 */}
<Card title="账户信息" className={styles.infoCard}> <Card title="关联信息" className={styles.infoCard}>
<List> <List>
<List.Item extra={formatAccountAge(user.accountAge)}> <List.Item extra="设备4"></List.Item>
<List.Item extra="微信4-1"></List.Item>
</List.Item> <List.Item extra="客服1"></List.Item>
<List.Item <List.Item extra="2025/07/21"></List.Item>
extra={`${user.statistics.todayAdded}/${user.statistics.addLimit}`} <List.Item extra="2025/07/25"></List.Item>
>
</List.Item>
<List.Item extra={user.activityLevel.allTimes}>
</List.Item>
<List.Item extra={user.activityLevel.dayTimes}>
</List.Item>
</List> </List>
</Card> </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}> <Card title="好友统计" className={styles.infoCard}>
<div className={styles.statsGrid}> <div className={styles.statsGrid}>
@@ -205,126 +479,16 @@ const TrafficPoolDetail: React.FC = () => {
</div> </div>
<div className={styles.statLabel}></div> <div className={styles.statLabel}></div>
</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> </div>
</Card> </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}> <Card title="限制记录" className={styles.infoCard}>
{user.restrictions && user.restrictions.length > 0 ? ( {user.restrictions && user.restrictions.length > 0 ? (
<List> <List>
{user.restrictions.map((restriction) => ( {user.restrictions.map(restriction => (
<List.Item <List.Item
key={restriction.id} key={restriction.id}
prefix={
<ExclamationCircleOutlined
style={{ color: "#ff4d4f" }}
/>
}
title={ title={
<div className={styles.restrictionTitle}> <div className={styles.restrictionTitle}>
<span>{restriction.reason || "未知原因"}</span> <span>{restriction.reason || "未知原因"}</span>
@@ -354,23 +518,161 @@ const TrafficPoolDetail: React.FC = () => {
</List> </List>
) : ( ) : (
<div className={styles.emptyState}> <div className={styles.emptyState}>
<div className={styles.emptyIcon}>
<UserOutlined style={{ fontSize: 48, color: "#ccc" }} />
</div>
<div className={styles.emptyText}></div> <div className={styles.emptyText}></div>
<div className={styles.emptyDesc}>
</div>
</div> </div>
)} )}
</Card> </Card>
</div> </div>
</Tabs.Tab> )}
<Tabs.Tab title="操作记录" key="actions"> {activeTab === "journey" && (
<div className={styles.tabContent}> <div className={styles.tabContent}>
<Card title="操作记录" className={styles.infoCard}> <Card title="互动记录" className={styles.infoCard}>
<div className={styles.emptyState}> {journeyLoading && journeyList.length === 0 ? (
<div className={styles.emptyText}></div> <div className={styles.loadingContainer}>
</div> <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> </Card>
</div> </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> </div>
</Layout> </Layout>
); );

View File

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

View File

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

View File

@@ -1,24 +1,24 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ESNext", "target": "ESNext",
"lib": ["DOM", "DOM.Iterable", "ESNext"], "lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"esModuleInterop": true, "esModuleInterop": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"strict": false, "strict": false,
"noImplicitAny": false, "noImplicitAny": false,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"module": "ESNext", "module": "ESNext",
"moduleResolution": "Node", "moduleResolution": "Node",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"jsx": "react-jsx", "jsx": "react-jsx",
"baseUrl": "./", "baseUrl": "./",
"paths": { "paths": {
"@/*": ["src/*"] "@/*": ["src/*"]
} }
}, },
"include": ["src"] "include": ["src"]
} }