feat: 本次提交更新内容如下

接口对接成功
This commit is contained in:
笔记本里的永平
2025-07-28 14:43:49 +08:00
parent cbc8c4a501
commit 9facfe2ff8
5 changed files with 553 additions and 267 deletions

View File

@@ -1,5 +1,5 @@
import request from "@/api/request";
export function getTrafficPoolDetail(id: string): Promise<any> {
return request("/v1/workbench/detail", { id }, "GET");
export function getTrafficPoolDetail(wechatId: string): Promise<any> {
return request("/v1/wechats/getWechatInfo", { wechatId }, "GET");
}

View File

@@ -0,0 +1,309 @@
.container {
padding: 12px;
background: #f5f5f5;
min-height: 100vh;
}
.userCard {
margin-bottom: 12px;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.userInfo {
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
}
.avatar {
width: 64px;
height: 64px;
border-radius: 50%;
flex-shrink: 0;
}
.userDetails {
flex: 1;
min-width: 0;
}
.nickname {
font-size: 18px;
font-weight: 600;
color: #333;
margin-bottom: 4px;
line-height: 1.2;
}
.wechatId {
font-size: 14px;
color: #1677ff;
margin-bottom: 8px;
line-height: 1.2;
}
.tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.packageTag {
font-size: 12px;
padding: 2px 8px;
border-radius: 12px;
}
.tabs {
background: transparent;
:global(.adm-tabs-header) {
background: white;
border-radius: 12px 12px 0 0;
margin-bottom: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
:global(.adm-tabs-tab) {
font-size: 14px;
font-weight: 500;
}
:global(.adm-tabs-tab-active) {
color: #1677ff;
}
:global(.adm-tabs-tab-line) {
background: #1677ff;
}
:global(.adm-tabs-content) {
background: white;
border-radius: 0 0 12px 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
}
.tabContent {
padding: 16px;
}
.infoCard {
margin-bottom: 12px;
border-radius: 8px;
overflow: hidden;
&:last-child {
margin-bottom: 0;
}
:global(.adm-card-header) {
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
font-size: 14px;
font-weight: 600;
color: #333;
}
:global(.adm-card-body) {
padding: 0;
}
}
.rfmGrid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
padding: 16px;
}
.rfmItem {
text-align: center;
}
.rfmValue {
font-size: 20px;
font-weight: 700;
line-height: 1.2;
margin-bottom: 4px;
}
.rfmLabel {
font-size: 12px;
color: #666;
line-height: 1.2;
}
.statsGrid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
padding: 16px;
}
.statItem {
text-align: center;
}
.statValue {
font-size: 18px;
font-weight: 700;
line-height: 1.2;
margin-bottom: 4px;
}
.statLabel {
font-size: 12px;
color: #666;
line-height: 1.2;
}
.interactionContent {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: #666;
line-height: 1.4;
}
.purchaseValue {
color: #22c55e;
font-weight: 600;
}
.tagsContainer {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 16px;
padding: 16px;
}
.userTag {
font-size: 12px;
padding: 4px 12px;
border-radius: 16px;
}
.addTagButton {
margin: 0 16px 16px;
height: 40px;
border-radius: 8px;
font-size: 14px;
:global(.antd-mobile-icon) {
margin-right: 4px;
}
}
.emptyState {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px 16px;
text-align: center;
}
.emptyText {
color: #999;
font-size: 14px;
line-height: 1.4;
}
// 响应式设计
@media (max-width: 375px) {
.container {
padding: 8px;
}
.userInfo {
padding: 12px;
gap: 12px;
}
.avatar {
width: 56px;
height: 56px;
}
.nickname {
font-size: 16px;
}
.wechatId {
font-size: 13px;
}
.tabContent {
padding: 12px;
}
.rfmGrid {
gap: 12px;
padding: 12px;
}
.rfmValue {
font-size: 18px;
}
.statsGrid {
gap: 12px;
padding: 12px;
}
.statValue {
font-size: 16px;
}
.tagsContainer {
padding: 12px;
}
.addTagButton {
margin: 0 12px 12px;
height: 36px;
font-size: 13px;
}
}
// 暗色模式支持
@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;
}
.infoCard :global(.adm-card-header) {
color: #fff;
border-bottom-color: #3a3a3a;
}
.rfmLabel,
.statLabel {
color: #999;
}
.emptyText {
color: #666;
}
}

View File

@@ -1,24 +1,24 @@
import React, { useEffect, useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { Card, Button, Avatar, Tag, Tabs, List, Badge } from "antd-mobile";
import {
UserOutlined,
MessageOutlined,
ShoppingOutlined,
EyeOutlined,
PlusOutlined,
} from "@ant-design/icons";
import Layout from "@/components/Layout/Layout";
import NavCommon from "@/components/NavCommon";
import { getTrafficPoolDetail } from "./api";
import type { TrafficPoolUserDetail } from "./data";
import { Card, Button, Avatar, Tag, Spin } from "antd";
const tabList = [
{ key: "base", label: "基本信息" },
{ key: "journey", label: "用户旅程" },
{ key: "tags", label: "用户标签" },
];
import styles from "./index.module.scss";
const TrafficPoolDetail: React.FC = () => {
const { id } = useParams();
const navigate = useNavigate();
const [loading, setLoading] = useState(true);
const [user, setUser] = useState<TrafficPoolUserDetail | null>(null);
const [activeTab, setActiveTab] = useState<"base" | "journey" | "tags">(
"base"
);
useEffect(() => {
if (!id) return;
@@ -28,270 +28,256 @@ const TrafficPoolDetail: React.FC = () => {
.finally(() => setLoading(false));
}, [id]);
if (loading) {
return (
<Layout>
<div style={{ textAlign: "center", padding: "64px 0" }}>
<Spin size="large" />
</div>
</Layout>
);
}
const getInteractionIcon = (type: string) => {
switch (type) {
case "click":
return <UserOutlined style={{ color: "#ff6b35" }} />;
case "message":
return <MessageOutlined style={{ color: "#3b82f6" }} />;
case "purchase":
return <ShoppingOutlined style={{ color: "#22c55e" }} />;
case "view":
return <EyeOutlined style={{ color: "#8b5cf6" }} />;
default:
return <UserOutlined />;
}
};
const getInteractionTitle = (type: string) => {
switch (type) {
case "click":
return "点击行为";
case "message":
return "消息互动";
case "purchase":
return "购买行为";
case "view":
return "页面浏览";
default:
return "未知行为";
}
};
const getStatusText = (status: string | number) => {
if (status === "failed" || status === 0) return "添加失败";
if (status === "added" || status === 1) return "添加成功";
return "未添加";
};
const getStatusColor = (status: string | number) => {
if (status === "failed" || status === 0) return "danger";
if (status === "added" || status === 1) return "success";
return "default";
};
if (!user) {
return (
<Layout>
<div style={{ textAlign: "center", color: "#aaa", padding: "64px 0" }}>
<Layout header={<NavCommon title="用户详情" />} loading={loading}>
<div className={styles.emptyState}>
<div className={styles.emptyText}></div>
</div>
</Layout>
);
}
return (
<Layout
header={
<div
style={{
display: "flex",
alignItems: "center",
height: 48,
borderBottom: "1px solid #eee",
background: "#fff",
}}
>
<Button
type="link"
onClick={() => navigate(-1)}
style={{ marginRight: 8 }}
>
&lt;
</Button>
<div style={{ fontWeight: 600, fontSize: 18 }}></div>
</div>
}
>
<div style={{ padding: 16 }}>
{/* 顶部信息 */}
<div
style={{
display: "flex",
alignItems: "center",
gap: 16,
marginBottom: 16,
}}
>
<Avatar src={user.avatar} size={64} />
<div>
<div style={{ fontSize: 20, fontWeight: 600 }}>{user.nickname}</div>
<div style={{ color: "#1677ff", fontSize: 14, margin: "4px 0" }}>
{user.wechatId}
<Layout header={<NavCommon title="用户详情" />} loading={loading}>
<div className={styles.container}>
{/* 用户基本信息 */}
<Card className={styles.userCard}>
<div className={styles.userInfo}>
<Avatar
src={user.avatar}
className={styles.avatar}
fallback={<UserOutlined />}
/>
<div className={styles.userDetails}>
<div className={styles.nickname}>{user.nickname}</div>
<div className={styles.wechatId}>{user.wechatId}</div>
<div className={styles.tags}>
{user.packages?.map((pkg) => (
<Tag
key={pkg}
color="primary"
fill="outline"
className={styles.packageTag}
>
{pkg}
</Tag>
))}
</div>
</div>
{user.packages &&
user.packages.length > 0 &&
user.packages.map((pkg) => (
<Tag color="purple" key={pkg} style={{ marginRight: 4 }}>
{pkg}
</Tag>
))}
</div>
</div>
{/* Tab栏 */}
<div
style={{
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>
</Card>
{/* 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 ?? "-"}
<Tabs className={styles.tabs}>
<Tabs.Tab title="基本信息" key="base">
<div className={styles.tabContent}>
{/* 关键信息 */}
<Card title="关键信息" className={styles.infoCard}>
<List>
<List.Item extra={user.deviceName || "--"}></List.Item>
<List.Item extra={user.wechatAccountName || "--"}>
</List.Item>
<List.Item extra={user.customerServiceName || "--"}>
</List.Item>
<List.Item extra={user.addTime || "--"}></List.Item>
<List.Item extra={user.lastInteraction || "--"}>
</List.Item>
</List>
</Card>
{/* RFM评分 */}
<Card title="RFM评分" className={styles.infoCard}>
<div className={styles.rfmGrid}>
<div className={styles.rfmItem}>
<div
className={styles.rfmValue}
style={{ color: "#1677ff" }}
>
{user.rfmScore?.recency ?? "-"}
</div>
<div className={styles.rfmLabel}>(R)</div>
</div>
<div style={{ fontSize: 12, color: "#888" }}>(R)</div>
</div>
<div>
<div
style={{ fontSize: 20, fontWeight: 600, color: "#52c41a" }}
>
{user.rfmScore?.frequency ?? "-"}
<div className={styles.rfmItem}>
<div
className={styles.rfmValue}
style={{ color: "#52c41a" }}
>
{user.rfmScore?.frequency ?? "-"}
</div>
<div className={styles.rfmLabel}>(F)</div>
</div>
<div style={{ fontSize: 12, color: "#888" }}>(F)</div>
</div>
<div>
<div
style={{ fontSize: 20, fontWeight: 600, color: "#eb2f96" }}
>
{user.rfmScore?.monetary ?? "-"}
<div className={styles.rfmItem}>
<div
className={styles.rfmValue}
style={{ color: "#eb2f96" }}
>
{user.rfmScore?.monetary ?? "-"}
</div>
<div className={styles.rfmLabel}>(M)</div>
</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 ?? "-"}
</Card>
{/* 统计数据 */}
<Card title="统计数据" className={styles.infoCard}>
<div className={styles.statsGrid}>
<div className={styles.statItem}>
<div
className={styles.statValue}
style={{ color: "#52c41a" }}
>
¥{user.totalSpent ?? "-"}
</div>
<div className={styles.statLabel}></div>
</div>
<div style={{ fontSize: 12, color: "#888" }}></div>
</div>
<div>
<div
style={{ fontSize: 18, fontWeight: 600, color: "#1677ff" }}
>
{user.interactionCount ?? "-"}
<div className={styles.statItem}>
<div
className={styles.statValue}
style={{ color: "#1677ff" }}
>
{user.interactionCount ?? "-"}
</div>
<div className={styles.statLabel}></div>
</div>
<div style={{ fontSize: 12, color: "#888" }}></div>
</div>
<div>
<div
style={{ fontSize: 18, fontWeight: 600, color: "#faad14" }}
>
{user.conversionRate ?? "-"}
<div className={styles.statItem}>
<div
className={styles.statValue}
style={{ color: "#faad14" }}
>
{user.conversionRate ?? "-"}
</div>
<div className={styles.statLabel}></div>
</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 className={styles.statItem}>
<div
className={styles.statValue}
style={{ color: "#ff4d4f" }}
>
{getStatusText(user.status)}
</div>
<div className={styles.statLabel}></div>
</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",
</Card>
</div>
</Tabs.Tab>
<Tabs.Tab title="用户旅程" key="journey">
<div className={styles.tabContent}>
<Card title="互动记录" className={styles.infoCard}>
{user.interactions && user.interactions.length > 0 ? (
<List>
{user.interactions.slice(0, 4).map((interaction) => (
<List.Item
key={interaction.id}
prefix={getInteractionIcon(interaction.type)}
title={getInteractionTitle(interaction.type)}
description={
<div className={styles.interactionContent}>
<span>{interaction.content}</span>
{interaction.type === "purchase" &&
interaction.value && (
<span className={styles.purchaseValue}>
¥{interaction.value}
</span>
)}
</div>
}
extra={interaction.timestamp}
/>
))}
</List>
) : (
<div className={styles.emptyState}>
<div className={styles.emptyText}></div>
</div>
)}
</Card>
</div>
</Tabs.Tab>
<Tabs.Tab title="用户标签" key="tags">
<div className={styles.tabContent}>
<Card title="用户标签" className={styles.infoCard}>
<div className={styles.tagsContainer}>
{user.tags && user.tags.length > 0 ? (
user.tags.map((tag) => (
<Tag
key={tag}
color="primary"
fill="outline"
className={styles.userTag}
>
{tag}
</Tag>
))
) : (
<div className={styles.emptyText}></div>
)}
</div>
<Button
block
color="default"
fill="outline"
className={styles.addTagButton}
onClick={() => {
// TODO: 实现添加标签功能
console.log("添加新标签");
}}
>
<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 style={{ color: "#888", fontSize: 13 }}>
{it.content}
{it.type === "purchase" && it.value && (
<span
style={{
color: "#52c41a",
fontWeight: 600,
marginLeft: 4,
}}
>
¥{it.value}
</span>
)}
</div>
</div>
<div
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>
)}
<PlusOutlined />
</Button>
</Card>
</div>
<Button type="dashed" block>
</Button>
</Card>
)}
</Tabs.Tab>
</Tabs>
</div>
</Layout>
);

View File

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

View File

@@ -73,17 +73,6 @@ const Workspace: React.FC = () => {
path: "/workspace/traffic-distribution",
bgColor: "#e6f7ff",
},
{
id: "ai-assistant",
name: "AI对话助手",
description: "智能回复,提高互动质量",
icon: (
<MessageOutlined className={styles.icon} style={{ color: "#1890ff" }} />
),
path: "/workspace/ai-assistant",
bgColor: "#e6f7ff",
isNew: true,
},
];
// AI智能助手
@@ -176,7 +165,7 @@ const Workspace: React.FC = () => {
</div>
{/* AI智能助手 */}
<div className={styles.section}>
{/* <div className={styles.section}>
<h2 className={styles.sectionTitle}>AI 智能助手</h2>
<div className={styles.featuresGrid}>
{aiFeatures.map((feature) => (
@@ -205,7 +194,7 @@ const Workspace: React.FC = () => {
</Link>
))}
</div>
</div>
</div> */}
</div>
</Layout>
);