FEAT => 本次更新项目为:
用户详情搞定
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# 基础环境变量示例
|
||||
VITE_API_BASE_URL=http://www.yishi.com
|
||||
# VITE_API_BASE_URL=https://ckbapi.quwanzhi.com
|
||||
# VITE_API_BASE_URL=http://www.yishi.com
|
||||
VITE_API_BASE_URL=https://ckbapi.quwanzhi.com
|
||||
VITE_APP_TITLE=Nkebao Base
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import React, { useEffect, useState, useCallback, useRef } from "react";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { NavBar, Tabs, Switch, Toast, SpinLoading, Button } from "antd-mobile";
|
||||
import { SettingOutlined, RedoOutlined } from "@ant-design/icons";
|
||||
import {
|
||||
NavBar,
|
||||
Tabs,
|
||||
Switch,
|
||||
Toast,
|
||||
SpinLoading,
|
||||
Button,
|
||||
Avatar,
|
||||
} from "antd-mobile";
|
||||
import { SettingOutlined, RedoOutlined, UserOutlined } from "@ant-design/icons";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import {
|
||||
fetchDeviceDetail,
|
||||
@@ -262,15 +270,32 @@ const DeviceDetail: React.FC = () => {
|
||||
navigate(`/wechat-accounts/detail/${acc.wechatId}`);
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={acc.avatar || "/placeholder.svg"}
|
||||
<Avatar
|
||||
src={acc.avatar}
|
||||
alt={acc.nickname}
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
background: "#eee",
|
||||
}}
|
||||
fallback={
|
||||
<div
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
background:
|
||||
"linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
|
||||
color: "white",
|
||||
fontSize: 16,
|
||||
borderRadius: "50%",
|
||||
}}
|
||||
>
|
||||
<UserOutlined />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontWeight: 500 }}>{acc.nickname}</div>
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import request from "@/api/request";
|
||||
import type {
|
||||
TrafficPoolUserDetail,
|
||||
UserJourneyResponse,
|
||||
UserTagsResponse,
|
||||
} from "./data";
|
||||
import type { UserTagsResponse } from "./data";
|
||||
|
||||
export function getTrafficPoolDetail(
|
||||
wechatId: string,
|
||||
): Promise<TrafficPoolUserDetail> {
|
||||
export function getTrafficPoolDetail(wechatId: string) {
|
||||
return request("/v1/wechats/getWechatInfo", { wechatId }, "GET");
|
||||
}
|
||||
|
||||
@@ -16,7 +10,7 @@ export function getUserJourney(params: {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
userId: string;
|
||||
}): Promise<UserJourneyResponse> {
|
||||
}) {
|
||||
return request("/v1/traffic/pool/getUserJourney", params, "GET");
|
||||
}
|
||||
|
||||
|
||||
@@ -30,3 +30,79 @@ export interface TrafficPoolUserDetail {
|
||||
value?: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
// 扩展的用户详情类型
|
||||
export interface ExtendedUserDetail extends TrafficPoolUserDetail {
|
||||
userInfo: {
|
||||
nickname: string;
|
||||
avatar: string;
|
||||
wechatId: string;
|
||||
friendShip: {
|
||||
totalFriend: number;
|
||||
maleFriend: number;
|
||||
femaleFriend: number;
|
||||
unknowFriend: number;
|
||||
};
|
||||
};
|
||||
rfmScore: {
|
||||
recency: number;
|
||||
frequency: number;
|
||||
monetary: number;
|
||||
totalScore: number;
|
||||
};
|
||||
trafficPools: {
|
||||
currentPool: string;
|
||||
availablePools: string[];
|
||||
};
|
||||
userTags: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
type: string;
|
||||
}>;
|
||||
valueTags: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
icon: string;
|
||||
rfmScore: number;
|
||||
valueLevel: string;
|
||||
}>;
|
||||
restrictions?: Array<{
|
||||
id: string;
|
||||
reason: string;
|
||||
level: number;
|
||||
date: number | null;
|
||||
}>;
|
||||
}
|
||||
|
||||
// 互动记录类型
|
||||
export interface InteractionRecord {
|
||||
id: string;
|
||||
type: string;
|
||||
content: string;
|
||||
timestamp: string;
|
||||
value?: number;
|
||||
}
|
||||
|
||||
// 用户旅程记录类型
|
||||
export interface UserJourneyRecord {
|
||||
id: string;
|
||||
type: number;
|
||||
remark: string;
|
||||
createTime: string;
|
||||
}
|
||||
|
||||
// 用户标签响应类型
|
||||
export interface UserTagsResponse {
|
||||
wechat: string[];
|
||||
siteLabels: UserTagItem[];
|
||||
}
|
||||
|
||||
// 用户标签项类型
|
||||
export interface UserTagItem {
|
||||
id: string;
|
||||
name: string;
|
||||
color?: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
@@ -47,6 +47,18 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.avatarFallback {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.userDetails {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
@@ -343,7 +355,7 @@
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 16px;
|
||||
padding: 20px 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -353,14 +365,14 @@
|
||||
}
|
||||
|
||||
.emptyText {
|
||||
font-size: 16px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.emptyDesc {
|
||||
font-size: 14px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import {
|
||||
Card,
|
||||
Button,
|
||||
Avatar,
|
||||
Tag,
|
||||
Tabs,
|
||||
List,
|
||||
Badge,
|
||||
SpinLoading,
|
||||
} from "antd-mobile";
|
||||
import { Card, Button, Avatar, Tag, List, SpinLoading } from "antd-mobile";
|
||||
import {
|
||||
UserOutlined,
|
||||
CrownOutlined,
|
||||
PlusOutlined,
|
||||
CloseOutlined,
|
||||
EyeOutlined,
|
||||
DollarOutlined,
|
||||
MobileOutlined,
|
||||
@@ -26,9 +15,7 @@ import Layout from "@/components/Layout/Layout";
|
||||
import NavCommon from "@/components/NavCommon";
|
||||
import { getTrafficPoolDetail, getUserJourney, getUserTags } from "./api";
|
||||
import type {
|
||||
TrafficPoolUserDetail,
|
||||
ExtendedUserDetail,
|
||||
InteractionRecord,
|
||||
UserJourneyRecord,
|
||||
UserTagsResponse,
|
||||
UserTagItem,
|
||||
@@ -52,6 +39,7 @@ const TrafficPoolDetail: React.FC = () => {
|
||||
// 用户标签相关状态
|
||||
const [tagsLoading, setTagsLoading] = useState(false);
|
||||
const [userTagsList, setUserTagsList] = useState<UserTagItem[]>([]);
|
||||
const [wechatTagsList, setWechatTagsList] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!wxid) return;
|
||||
@@ -61,6 +49,8 @@ const TrafficPoolDetail: React.FC = () => {
|
||||
// 将API数据转换为扩展的用户详情数据
|
||||
const extendedUser: ExtendedUserDetail = {
|
||||
...res,
|
||||
// 添加userInfo属性
|
||||
userInfo: res.userInfo,
|
||||
// 模拟RFM评分数据
|
||||
rfmScore: {
|
||||
recency: 5,
|
||||
@@ -92,6 +82,8 @@ const TrafficPoolDetail: React.FC = () => {
|
||||
},
|
||||
],
|
||||
};
|
||||
console.log(extendedUser);
|
||||
|
||||
setUser(extendedUser);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
@@ -131,6 +123,7 @@ const TrafficPoolDetail: React.FC = () => {
|
||||
try {
|
||||
const response: UserTagsResponse = await getUserTags(userId);
|
||||
setUserTagsList(response.siteLabels || []);
|
||||
setWechatTagsList(response.wechat || []);
|
||||
} catch (error) {
|
||||
console.error("获取用户标签失败:", error);
|
||||
} finally {
|
||||
@@ -149,10 +142,6 @@ const TrafficPoolDetail: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
navigate(-1);
|
||||
};
|
||||
|
||||
const getJourneyTypeIcon = (type: number) => {
|
||||
switch (type) {
|
||||
case 0: // 浏览
|
||||
@@ -207,32 +196,6 @@ const TrafficPoolDetail: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
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:
|
||||
@@ -297,7 +260,11 @@ const TrafficPoolDetail: React.FC = () => {
|
||||
<Avatar
|
||||
src={user.userInfo.avatar}
|
||||
className={styles.avatar}
|
||||
fallback={<UserOutlined />}
|
||||
fallback={
|
||||
<div className={styles.avatarFallback}>
|
||||
<UserOutlined />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<div className={styles.userDetails}>
|
||||
<div className={styles.nickname}>{user.userInfo.nickname}</div>
|
||||
@@ -617,20 +584,22 @@ const TrafficPoolDetail: React.FC = () => {
|
||||
|
||||
{activeTab === "tags" && (
|
||||
<div className={styles.tabContent}>
|
||||
{/* 用户标签 */}
|
||||
<Card title="用户标签" className={styles.infoCard}>
|
||||
{/* 站内标签 */}
|
||||
<Card title="站内标签" className={styles.infoCard}>
|
||||
{tagsLoading && userTagsList.length === 0 ? (
|
||||
<div className={styles.loadingContainer}>
|
||||
<SpinLoading color="primary" style={{ fontSize: 24 }} />
|
||||
<SpinLoading color="primary" style={{ fontSize: 20 }} />
|
||||
<div className={styles.loadingText}>加载中...</div>
|
||||
</div>
|
||||
) : userTagsList.length === 0 ? (
|
||||
<div className={styles.emptyState}>
|
||||
<div className={styles.emptyIcon}>
|
||||
<TagOutlined style={{ fontSize: 48, color: "#ccc" }} />
|
||||
<TagOutlined style={{ fontSize: 36, color: "#ccc" }} />
|
||||
</div>
|
||||
<div className={styles.emptyText}>暂无站内标签</div>
|
||||
<div className={styles.emptyDesc}>
|
||||
该用户还没有任何站内标签
|
||||
</div>
|
||||
<div className={styles.emptyText}>暂无用户标签</div>
|
||||
<div className={styles.emptyDesc}>该用户还没有任何标签</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.tagsSection}>
|
||||
@@ -648,6 +617,39 @@ const TrafficPoolDetail: React.FC = () => {
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* 微信标签 */}
|
||||
<Card title="微信标签" className={styles.infoCard}>
|
||||
{tagsLoading && wechatTagsList.length === 0 ? (
|
||||
<div className={styles.loadingContainer}>
|
||||
<SpinLoading color="primary" style={{ fontSize: 24 }} />
|
||||
<div className={styles.loadingText}>加载中...</div>
|
||||
</div>
|
||||
) : wechatTagsList.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}>
|
||||
{wechatTagsList.map((tag, index) => (
|
||||
<Tag
|
||||
key={index}
|
||||
color="danger"
|
||||
fill="outline"
|
||||
className={styles.tagItem}
|
||||
>
|
||||
{tag}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* 价值标签 */}
|
||||
<Card title="价值标签" className={styles.infoCard}>
|
||||
{user.valueTags && user.valueTags.length > 0 ? (
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import request from "@/api/request";
|
||||
import type { DeviceOption } from "./data";
|
||||
import { fetchDeviceList } from "@/pages/guide/api";
|
||||
|
||||
// 获取流量池列表
|
||||
export function fetchTrafficPoolList(params: {
|
||||
|
||||
Reference in New Issue
Block a user