FEAT => 本次更新项目为:
用户详情
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user