Merge branch 'yongpxu-dev2' of https://e.coding.net/g-xtcy5189/cunkebao/cunkebao_v3 into yongpxu-dev2
# Conflicts: # nkebao/.env.development resolved by yongpxu-dev2 version # nkebao/src/pages/mobile/mine/traffic-pool/detail/data.ts resolved by yongpxu-dev2 version
This commit is contained in:
@@ -1,5 +1,31 @@
|
|||||||
import request from "@/api/request";
|
import request from "@/api/request";
|
||||||
|
import type {
|
||||||
|
TrafficPoolUserDetail,
|
||||||
|
UserJourneyResponse,
|
||||||
|
UserTagsResponse,
|
||||||
|
} from "./data";
|
||||||
|
|
||||||
export function getTrafficPoolDetail(id: string): Promise<any> {
|
export function getTrafficPoolDetail(
|
||||||
return request("/v1/workbench/detail", { id }, "GET");
|
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<UserTagsResponse> {
|
||||||
|
return request("/v1/traffic/pool/getUserTags", { userId }, "GET");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加用户标签
|
||||||
|
export function addUserTag(userId: string, tagData: any): Promise<any> {
|
||||||
|
return request("/v1/user/tags", { userId, ...tagData }, "POST");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,122 +1,134 @@
|
|||||||
.container {
|
.container {
|
||||||
padding: 12px;
|
padding: 0;
|
||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.userCard {
|
// 头部样式
|
||||||
margin-bottom: 12px;
|
.header {
|
||||||
border-radius: 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.userInfo {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 16px;
|
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
background: #fff;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
|
||||||
.avatar {
|
.title {
|
||||||
width: 64px;
|
font-size: 18px;
|
||||||
height: 64px;
|
font-weight: 600;
|
||||||
border-radius: 50%;
|
color: #333;
|
||||||
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) {
|
.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;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
color: #666;
|
||||||
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.adm-tabs-tab-active) {
|
.tags {
|
||||||
color: #1677ff;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.adm-tabs-tab-line) {
|
.userTag {
|
||||||
background: #1677ff;
|
font-size: 12px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
:global(.adm-tabs-content) {
|
// 标签导航
|
||||||
background: white;
|
.tabNav {
|
||||||
border-radius: 0 0 12px 12px;
|
display: flex;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
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 {
|
.tabContent {
|
||||||
padding: 16px;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 信息卡片
|
||||||
.infoCard {
|
.infoCard {
|
||||||
margin-bottom: 12px;
|
border-radius: 12px;
|
||||||
border-radius: 8px;
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.adm-card-header) {
|
:global(.adm-card-header) {
|
||||||
padding: 12px 16px;
|
padding: 16px;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid #f0f0f0;
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
@@ -126,6 +138,55 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
.statsGrid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
@@ -135,21 +196,176 @@
|
|||||||
|
|
||||||
.statItem {
|
.statItem {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
padding: 12px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.statValue {
|
.statValue {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
line-height: 1.2;
|
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.statLabel {
|
.statLabel {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #666;
|
color: #666;
|
||||||
line-height: 1.2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 用户旅程
|
||||||
|
.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 {
|
.restrictionTitle {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -178,60 +394,20 @@
|
|||||||
margin-top: 4px;
|
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) {
|
@media (max-width: 375px) {
|
||||||
.container {
|
.rfmGrid,
|
||||||
padding: 8px;
|
.statsGrid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.userInfo {
|
.userInfo {
|
||||||
padding: 12px;
|
flex-direction: column;
|
||||||
gap: 12px;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
width: 56px;
|
align-self: center;
|
||||||
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 {
|
.restrictionTitle {
|
||||||
@@ -242,50 +418,3 @@
|
|||||||
font-size: 11px;
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,47 +1,285 @@
|
|||||||
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,
|
||||||
|
SpinLoading,
|
||||||
|
} from "antd-mobile";
|
||||||
|
import {
|
||||||
|
UserOutlined,
|
||||||
|
CrownOutlined,
|
||||||
|
PlusOutlined,
|
||||||
|
CloseOutlined,
|
||||||
|
EyeOutlined,
|
||||||
|
DollarOutlined,
|
||||||
|
MobileOutlined,
|
||||||
|
TagOutlined,
|
||||||
|
FileTextOutlined,
|
||||||
|
UserAddOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
import Layout from "@/components/Layout/Layout";
|
import Layout from "@/components/Layout/Layout";
|
||||||
import { getTrafficPoolDetail } from "./api";
|
import NavCommon from "@/components/NavCommon";
|
||||||
import type { TrafficPoolUserDetail } from "./data";
|
import { getTrafficPoolDetail, getUserJourney, getUserTags } from "./api";
|
||||||
import { Card, Button, Avatar, Tag, Spin } from "antd";
|
import type {
|
||||||
|
TrafficPoolUserDetail,
|
||||||
const tabList = [
|
ExtendedUserDetail,
|
||||||
{ key: "base", label: "基本信息" },
|
InteractionRecord,
|
||||||
{ key: "journey", label: "用户旅程" },
|
UserJourneyRecord,
|
||||||
{ key: "tags", label: "用户标签" },
|
UserTagsResponse,
|
||||||
];
|
UserTagItem,
|
||||||
|
} from "./data";
|
||||||
|
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<"base" | "journey" | "tags">(
|
const [activeTab, setActiveTab] = useState("basic");
|
||||||
"base"
|
|
||||||
);
|
// 用户旅程相关状态
|
||||||
|
const [journeyLoading, setJourneyLoading] = useState(false);
|
||||||
|
const [journeyList, setJourneyList] = useState<UserJourneyRecord[]>([]);
|
||||||
|
const [journeyPage, setJourneyPage] = useState(1);
|
||||||
|
const [journeyTotal, setJourneyTotal] = useState(0);
|
||||||
|
const pageSize = 10;
|
||||||
|
|
||||||
|
// 用户标签相关状态
|
||||||
|
const [tagsLoading, setTagsLoading] = useState(false);
|
||||||
|
const [userTagsList, setUserTagsList] = useState<UserTagItem[]>([]);
|
||||||
|
|
||||||
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 fetchUserTags = async () => {
|
||||||
|
if (!userId) return;
|
||||||
|
|
||||||
|
setTagsLoading(true);
|
||||||
|
try {
|
||||||
|
const response: UserTagsResponse = await getUserTags(userId);
|
||||||
|
setUserTagsList(response.siteLabels || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取用户标签失败:", error);
|
||||||
|
} finally {
|
||||||
|
setTagsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 标签切换处理
|
||||||
|
const handleTabChange = (tab: string) => {
|
||||||
|
setActiveTab(tab);
|
||||||
|
if (tab === "journey" && journeyList.length === 0) {
|
||||||
|
fetchUserJourney(1);
|
||||||
|
}
|
||||||
|
if (tab === "tags" && userTagsList.length === 0) {
|
||||||
|
fetchUserTags();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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) {
|
||||||
|
case 1:
|
||||||
|
return "男";
|
||||||
|
case 2:
|
||||||
|
return "女";
|
||||||
|
default:
|
||||||
|
return "未知";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getGenderColor = (gender: number) => {
|
||||||
|
switch (gender) {
|
||||||
|
case 1:
|
||||||
|
return "#1677ff";
|
||||||
|
case 2:
|
||||||
|
return "#eb2f96";
|
||||||
|
default:
|
||||||
|
return "#999";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRestrictionLevelText = (level: number) => {
|
||||||
|
switch (level) {
|
||||||
|
case 1:
|
||||||
|
return "轻微";
|
||||||
|
case 2:
|
||||||
|
return "中等";
|
||||||
|
case 3:
|
||||||
|
return "严重";
|
||||||
|
default:
|
||||||
|
return "未知";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 getTagColor = (index: number): string => {
|
||||||
|
const colors = ["primary", "success", "warning", "danger", "default"];
|
||||||
|
return colors[index % colors.length];
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<Layout>
|
|
||||||
<div style={{ textAlign: "center", padding: "64px 0" }}>
|
|
||||||
<Spin size="large" />
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout header={<NavCommon title="用户详情" />} loading={loading}>
|
||||||
<div style={{ textAlign: "center", color: "#aaa", padding: "64px 0" }}>
|
<div className={styles.emptyState}>
|
||||||
未找到该用户
|
<div className={styles.emptyText}>未找到该用户</div>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
@@ -49,249 +287,418 @@ const TrafficPoolDetail: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout
|
<Layout
|
||||||
|
loading={loading}
|
||||||
header={
|
header={
|
||||||
<div
|
<>
|
||||||
style={{
|
<NavCommon title="用户详情" />
|
||||||
display: "flex",
|
{/* 用户基本信息 */}
|
||||||
alignItems: "center",
|
<Card className={styles.userCard}>
|
||||||
height: 48,
|
<div className={styles.userInfo}>
|
||||||
borderBottom: "1px solid #eee",
|
<Avatar
|
||||||
background: "#fff",
|
src={user.userInfo.avatar}
|
||||||
}}
|
className={styles.avatar}
|
||||||
>
|
fallback={<UserOutlined />}
|
||||||
<Button
|
/>
|
||||||
type="link"
|
<div className={styles.userDetails}>
|
||||||
onClick={() => navigate(-1)}
|
<div className={styles.nickname}>{user.userInfo.nickname}</div>
|
||||||
style={{ marginRight: 8 }}
|
<div className={styles.wechatId}>{user.userInfo.wechatId}</div>
|
||||||
>
|
<div className={styles.tags}>
|
||||||
< 返回
|
<Tag
|
||||||
</Button>
|
color="warning"
|
||||||
<div style={{ fontWeight: 600, fontSize: 18 }}>用户详情</div>
|
fill="outline"
|
||||||
</div>
|
className={styles.userTag}
|
||||||
|
>
|
||||||
|
<CrownOutlined />
|
||||||
|
重要价值客户
|
||||||
|
</Tag>
|
||||||
|
<Tag color="danger" fill="outline" className={styles.userTag}>
|
||||||
|
优先添加
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
{/* 导航标签 */}
|
||||||
|
<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 style={{ padding: 16 }}>
|
<div className={styles.container}>
|
||||||
{/* 顶部信息 */}
|
{/* 内容区域 */}
|
||||||
<div
|
<div className={styles.content}>
|
||||||
style={{
|
{activeTab === "basic" && (
|
||||||
display: "flex",
|
<div className={styles.tabContent}>
|
||||||
alignItems: "center",
|
{/* 关联信息 */}
|
||||||
gap: 16,
|
<Card title="关联信息" className={styles.infoCard}>
|
||||||
marginBottom: 16,
|
<List>
|
||||||
}}
|
<List.Item extra="设备4">设备</List.Item>
|
||||||
>
|
<List.Item extra="微信4-1">微信号</List.Item>
|
||||||
<Avatar src={user.avatar} size={64} />
|
<List.Item extra="客服1">客服</List.Item>
|
||||||
<div>
|
<List.Item extra="2025/07/21">添加时间</List.Item>
|
||||||
<div style={{ fontSize: 20, fontWeight: 600 }}>{user.nickname}</div>
|
<List.Item extra="2025/07/25">最近互动</List.Item>
|
||||||
<div style={{ color: "#1677ff", fontSize: 14, margin: "4px 0" }}>
|
</List>
|
||||||
{user.wechatId}
|
</Card>
|
||||||
</div>
|
|
||||||
{user.packages &&
|
{/* RFM评分 */}
|
||||||
user.packages.length > 0 &&
|
{user.rfmScore && (
|
||||||
user.packages.map(pkg => (
|
<Card title="RFM评分" className={styles.infoCard}>
|
||||||
<Tag color="purple" key={pkg} style={{ marginRight: 4 }}>
|
<div className={styles.rfmGrid}>
|
||||||
{pkg}
|
<div className={styles.rfmItem}>
|
||||||
</Tag>
|
<div className={styles.rfmLabel}>最近性(R)</div>
|
||||||
))}
|
<div
|
||||||
</div>
|
className={styles.rfmValue}
|
||||||
</div>
|
style={{ color: "#1677ff" }}
|
||||||
{/* Tab栏 */}
|
>
|
||||||
<div
|
{user.rfmScore.recency}
|
||||||
style={{
|
</div>
|
||||||
display: "flex",
|
|
||||||
gap: 24,
|
|
||||||
borderBottom: "1px solid #eee",
|
|
||||||
marginBottom: 16,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{tabList.map(tab => (
|
|
||||||
<div
|
|
||||||
key={tab.key}
|
|
||||||
style={{
|
|
||||||
padding: "8px 0",
|
|
||||||
fontWeight: 500,
|
|
||||||
color: activeTab === tab.key ? "#1677ff" : "#888",
|
|
||||||
borderBottom:
|
|
||||||
activeTab === tab.key ? "2px solid #1677ff" : "none",
|
|
||||||
cursor: "pointer",
|
|
||||||
fontSize: 16,
|
|
||||||
}}
|
|
||||||
onClick={() => setActiveTab(tab.key as any)}
|
|
||||||
>
|
|
||||||
{tab.label}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{/* Tab内容 */}
|
|
||||||
{activeTab === "base" && (
|
|
||||||
<>
|
|
||||||
<Card style={{ marginBottom: 16 }} title="关键信息">
|
|
||||||
<div style={{ display: "flex", flexWrap: "wrap", gap: 24 }}>
|
|
||||||
<div>设备:{user.deviceName || "--"}</div>
|
|
||||||
<div>微信号:{user.wechatAccountName || "--"}</div>
|
|
||||||
<div>客服:{user.customerServiceName || "--"}</div>
|
|
||||||
<div>添加时间:{user.addTime || "--"}</div>
|
|
||||||
<div>最近互动:{user.lastInteraction || "--"}</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
<Card style={{ marginBottom: 16 }} title="RFM评分">
|
|
||||||
<div style={{ display: "flex", gap: 32 }}>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
style={{ fontSize: 20, fontWeight: 600, color: "#1677ff" }}
|
|
||||||
>
|
|
||||||
{user.rfmScore?.recency ?? "-"}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: 12, color: "#888" }}>最近性(R)</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
style={{ fontSize: 20, fontWeight: 600, color: "#52c41a" }}
|
|
||||||
>
|
|
||||||
{user.rfmScore?.frequency ?? "-"}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: 12, color: "#888" }}>频率(F)</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
style={{ fontSize: 20, fontWeight: 600, color: "#eb2f96" }}
|
|
||||||
>
|
|
||||||
{user.rfmScore?.monetary ?? "-"}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: 12, color: "#888" }}>金额(M)</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
<Card style={{ marginBottom: 16 }} title="统计数据">
|
|
||||||
<div style={{ display: "flex", gap: 32 }}>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
style={{ fontSize: 18, fontWeight: 600, color: "#52c41a" }}
|
|
||||||
>
|
|
||||||
¥{user.totalSpent ?? "-"}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: 12, color: "#888" }}>总消费</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
style={{ fontSize: 18, fontWeight: 600, color: "#1677ff" }}
|
|
||||||
>
|
|
||||||
{user.interactionCount ?? "-"}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: 12, color: "#888" }}>互动次数</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
style={{ fontSize: 18, fontWeight: 600, color: "#faad14" }}
|
|
||||||
>
|
|
||||||
{user.conversionRate ?? "-"}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: 12, color: "#888" }}>转化率</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
style={{ fontSize: 18, fontWeight: 600, color: "#ff4d4f" }}
|
|
||||||
>
|
|
||||||
{user.status === "failed"
|
|
||||||
? "添加失败"
|
|
||||||
: user.status === "added"
|
|
||||||
? "添加成功"
|
|
||||||
: "未添加"}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: 12, color: "#888" }}>添加状态</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{activeTab === "journey" && (
|
|
||||||
<Card title="互动记录">
|
|
||||||
{user.interactions && user.interactions.length > 0 ? (
|
|
||||||
user.interactions.slice(0, 4).map(it => (
|
|
||||||
<div
|
|
||||||
key={it.id}
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: 12,
|
|
||||||
borderBottom: "1px solid #f0f0f0",
|
|
||||||
padding: "12px 0",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ fontSize: 22 }}>
|
|
||||||
{it.type === "click" && "📱"}
|
|
||||||
{it.type === "message" && "💬"}
|
|
||||||
{it.type === "purchase" && "💲"}
|
|
||||||
{it.type === "view" && "👁️"}
|
|
||||||
</div>
|
|
||||||
<div style={{ flex: 1 }}>
|
|
||||||
<div style={{ fontWeight: 500 }}>
|
|
||||||
{it.type === "click" && "点击行为"}
|
|
||||||
{it.type === "message" && "消息互动"}
|
|
||||||
{it.type === "purchase" && "购买行为"}
|
|
||||||
{it.type === "view" && "页面浏览"}
|
|
||||||
</div>
|
</div>
|
||||||
<div style={{ color: "#888", fontSize: 13 }}>
|
<div className={styles.rfmItem}>
|
||||||
{it.content}
|
<div className={styles.rfmLabel}>频率(F)</div>
|
||||||
{it.type === "purchase" && it.value && (
|
<div
|
||||||
<span
|
className={styles.rfmValue}
|
||||||
style={{
|
style={{ color: "#52c41a" }}
|
||||||
color: "#52c41a",
|
>
|
||||||
fontWeight: 600,
|
{user.rfmScore.frequency}
|
||||||
marginLeft: 4,
|
</div>
|
||||||
}}
|
</div>
|
||||||
>
|
<div className={styles.rfmItem}>
|
||||||
¥{it.value}
|
<div className={styles.rfmLabel}>金额(M)</div>
|
||||||
</span>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<div
|
</Card>
|
||||||
style={{
|
|
||||||
fontSize: 12,
|
|
||||||
color: "#aaa",
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{it.timestamp}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
color: "#aaa",
|
|
||||||
textAlign: "center",
|
|
||||||
padding: "24px 0",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
暂无互动记录
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
{activeTab === "tags" && (
|
|
||||||
<Card title="用户标签">
|
|
||||||
<div style={{ marginBottom: 12 }}>
|
|
||||||
{user.tags && user.tags.length > 0 ? (
|
|
||||||
user.tags.map(tag => (
|
|
||||||
<Tag
|
|
||||||
key={tag}
|
|
||||||
color="blue"
|
|
||||||
style={{ marginRight: 8, marginBottom: 8 }}
|
|
||||||
>
|
|
||||||
{tag}
|
|
||||||
</Tag>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<span style={{ color: "#aaa" }}>暂无标签</span>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 流量池 */}
|
||||||
|
{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}>
|
||||||
|
<div className={styles.statItem}>
|
||||||
|
<div
|
||||||
|
className={styles.statValue}
|
||||||
|
style={{ color: "#1677ff" }}
|
||||||
|
>
|
||||||
|
{user.userInfo.friendShip.totalFriend}
|
||||||
|
</div>
|
||||||
|
<div className={styles.statLabel}>总好友</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.statItem}>
|
||||||
|
<div
|
||||||
|
className={styles.statValue}
|
||||||
|
style={{ color: "#1677ff" }}
|
||||||
|
>
|
||||||
|
{user.userInfo.friendShip.maleFriend}
|
||||||
|
</div>
|
||||||
|
<div className={styles.statLabel}>男性好友</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.statItem}>
|
||||||
|
<div
|
||||||
|
className={styles.statValue}
|
||||||
|
style={{ color: "#eb2f96" }}
|
||||||
|
>
|
||||||
|
{user.userInfo.friendShip.femaleFriend}
|
||||||
|
</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>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 限制记录 */}
|
||||||
|
<Card title="限制记录" className={styles.infoCard}>
|
||||||
|
{user.restrictions && user.restrictions.length > 0 ? (
|
||||||
|
<List>
|
||||||
|
{user.restrictions.map(restriction => (
|
||||||
|
<List.Item
|
||||||
|
key={restriction.id}
|
||||||
|
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>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</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>
|
</div>
|
||||||
<Button type="dashed" block>
|
)}
|
||||||
➕ 添加新标签
|
|
||||||
</Button>
|
{activeTab === "journey" && (
|
||||||
</Card>
|
<div className={styles.tabContent}>
|
||||||
)}
|
<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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === "tags" && (
|
||||||
|
<div className={styles.tabContent}>
|
||||||
|
{/* 用户标签 */}
|
||||||
|
<Card title="用户标签" className={styles.infoCard}>
|
||||||
|
{tagsLoading && userTagsList.length === 0 ? (
|
||||||
|
<div className={styles.loadingContainer}>
|
||||||
|
<SpinLoading color="primary" style={{ fontSize: 24 }} />
|
||||||
|
<div className={styles.loadingText}>加载中...</div>
|
||||||
|
</div>
|
||||||
|
) : userTagsList.length === 0 ? (
|
||||||
|
<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>
|
||||||
|
) : (
|
||||||
|
<div className={styles.tagsSection}>
|
||||||
|
{userTagsList.map((tag, index) => (
|
||||||
|
<Tag
|
||||||
|
key={tag.id}
|
||||||
|
color={getTagColor(index)}
|
||||||
|
fill="outline"
|
||||||
|
className={styles.tagItem}
|
||||||
|
>
|
||||||
|
{tag.name}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</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" className={styles.addTagBtn}>
|
||||||
|
<TagOutlined />
|
||||||
|
添加新标签
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user