From fbb78ebd509fe4f83a6dc47c27a82dfef7fe2f19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Fri, 17 Oct 2025 18:18:17 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A7=BB=E9=99=A4=E6=B5=81=E9=87=8F=E6=B1=A0?= =?UTF-8?q?=E8=AF=A6=E6=83=85=E5=8F=8A=E7=9B=B8=E5=85=B3=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=EF=BC=9A=E5=88=A0=E9=99=A4=E6=B5=81=E9=87=8F=E6=B1=A0=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E9=A1=B5=E9=9D=A2=E3=80=81API=E3=80=81=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E7=B1=BB=E5=9E=8B=E5=92=8C=E6=A0=B7=E5=BC=8F=E6=96=87?= =?UTF-8?q?=E4=BB=B6=EF=BC=8C=E7=AE=80=E5=8C=96=E4=BB=A3=E7=A0=81=E7=BB=93?= =?UTF-8?q?=E6=9E=84=EF=BC=8C=E6=8F=90=E5=8D=87=E5=8F=AF=E7=BB=B4=E6=8A=A4?= =?UTF-8?q?=E6=80=A7=E5=92=8C=E7=94=A8=E6=88=B7=E4=BD=93=E9=AA=8C=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mobile/mine/traffic-pool/detail/api.ts | 25 - .../mobile/mine/traffic-pool/detail/data.ts | 133 --- .../traffic-pool/detail/index.module.scss | 426 ---------- .../mobile/mine/traffic-pool/detail/index.tsx | 795 ------------------ .../mobile/mine/traffic-pool/form/README.md | 101 +++ .../mobile/mine/traffic-pool/form/api.ts | 75 ++ .../components/AudienceFilter.module.scss | 121 +++ .../form/components/AudienceFilter.tsx | 209 +++++ .../form/components/BasicInfo.module.scss | 60 ++ .../form/components/BasicInfo.tsx | 65 ++ .../form/components/ConditionList.module.scss | 52 ++ .../form/components/ConditionList.tsx | 67 ++ .../CustomConditionModal.module.scss | 80 ++ .../form/components/CustomConditionModal.tsx | 242 ++++++ .../SchemeRecommendation.module.scss | 92 ++ .../form/components/SchemeRecommendation.tsx | 201 +++++ .../components/UserListPreview.module.scss | 126 +++ .../form/components/UserListPreview.tsx | 155 ++++ .../mine/traffic-pool/form/index.module.scss | 49 ++ .../mobile/mine/traffic-pool/form/index.tsx | 180 ++++ .../mobile/mine/traffic-pool/info/api.ts | 34 - .../mine/traffic-pool/info/index.module.scss | 65 -- .../mobile/mine/traffic-pool/list/api.ts | 5 + .../mine/traffic-pool/list/index.module.scss | 25 +- .../mobile/mine/traffic-pool/list/index.tsx | 151 ++-- .../mobile/mine/traffic-pool/userList/api.ts | 11 + .../traffic-pool/{info => userList}/data.ts | 24 - .../traffic-pool/userList/index.module.scss | 40 + .../traffic-pool/{info => userList}/index.tsx | 97 ++- Cunkebao/src/router/module/mine.tsx | 22 +- 30 files changed, 2130 insertions(+), 1598 deletions(-) delete mode 100644 Cunkebao/src/pages/mobile/mine/traffic-pool/detail/api.ts delete mode 100644 Cunkebao/src/pages/mobile/mine/traffic-pool/detail/data.ts delete mode 100644 Cunkebao/src/pages/mobile/mine/traffic-pool/detail/index.module.scss delete mode 100644 Cunkebao/src/pages/mobile/mine/traffic-pool/detail/index.tsx create mode 100644 Cunkebao/src/pages/mobile/mine/traffic-pool/form/README.md create mode 100644 Cunkebao/src/pages/mobile/mine/traffic-pool/form/api.ts create mode 100644 Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/AudienceFilter.module.scss create mode 100644 Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/AudienceFilter.tsx create mode 100644 Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/BasicInfo.module.scss create mode 100644 Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/BasicInfo.tsx create mode 100644 Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/ConditionList.module.scss create mode 100644 Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/ConditionList.tsx create mode 100644 Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/CustomConditionModal.module.scss create mode 100644 Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/CustomConditionModal.tsx create mode 100644 Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/SchemeRecommendation.module.scss create mode 100644 Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/SchemeRecommendation.tsx create mode 100644 Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/UserListPreview.module.scss create mode 100644 Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/UserListPreview.tsx create mode 100644 Cunkebao/src/pages/mobile/mine/traffic-pool/form/index.module.scss create mode 100644 Cunkebao/src/pages/mobile/mine/traffic-pool/form/index.tsx delete mode 100644 Cunkebao/src/pages/mobile/mine/traffic-pool/info/api.ts delete mode 100644 Cunkebao/src/pages/mobile/mine/traffic-pool/info/index.module.scss create mode 100644 Cunkebao/src/pages/mobile/mine/traffic-pool/userList/api.ts rename Cunkebao/src/pages/mobile/mine/traffic-pool/{info => userList}/data.ts (54%) create mode 100644 Cunkebao/src/pages/mobile/mine/traffic-pool/userList/index.module.scss rename Cunkebao/src/pages/mobile/mine/traffic-pool/{info => userList}/index.tsx (63%) diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/detail/api.ts b/Cunkebao/src/pages/mobile/mine/traffic-pool/detail/api.ts deleted file mode 100644 index 3f3341ae..00000000 --- a/Cunkebao/src/pages/mobile/mine/traffic-pool/detail/api.ts +++ /dev/null @@ -1,25 +0,0 @@ -import request from "@/api/request"; -import type { UserTagsResponse } from "./data"; - -export function getTrafficPoolDetail(wechatId: string) { - return request("/v1/traffic/pool/getUserInfo", { wechatId }, "GET"); -} - -// 获取用户旅程记录 -export function getUserJourney(params: { - page: number; - pageSize: number; - userId: string; -}) { - return request("/v1/traffic/pool/getUserJourney", params, "GET"); -} - -// 获取用户标签 -export function getUserTags(userId: string): Promise { - return request("/v1/traffic/pool/getUserTags", { userId }, "GET"); -} - -// 添加用户标签 -export function addUserTag(userId: string, tagData: any): Promise { - return request("/v1/user/tags", { userId, ...tagData }, "POST"); -} diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/detail/data.ts b/Cunkebao/src/pages/mobile/mine/traffic-pool/detail/data.ts deleted file mode 100644 index f32ec073..00000000 --- a/Cunkebao/src/pages/mobile/mine/traffic-pool/detail/data.ts +++ /dev/null @@ -1,133 +0,0 @@ -// 设备信息类型 -export interface DeviceInfo { - id: number; - memo: string; - imei: string; - brand: string; - alive: number; - address: string; -} - -// 来源信息类型 -export interface SourceInfo { - nickname: string; - avatar: string; - gender: number; - phone: string; - wechatId: string; - alias: string; - createTime: string; - friendId: number; - wechatAccountId: number; - lastMsgTime: string; - device: DeviceInfo; -} - -// 统计总计类型 -export interface TotalStats { - msg: number; - money: number; - isFriend: boolean; - percentage: string; -} - -// RMM评分类型 -export interface RmmScore { - r: number; - f: number; - m: number; -} - -// 用户详情类型 -export interface TrafficPoolUserDetail { - id: number; - identifier: string; - wechatId: string; - nickname: string; - avatar: string; - gender: number; - phone: string; - alias: string; - lastMsgTime: string; - source: SourceInfo[]; - packages: any[]; - total: TotalStats; - rmm: RmmScore; -} - -// 扩展的用户详情类型 -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; -} diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/detail/index.module.scss b/Cunkebao/src/pages/mobile/mine/traffic-pool/detail/index.module.scss deleted file mode 100644 index 23fd44d2..00000000 --- a/Cunkebao/src/pages/mobile/mine/traffic-pool/detail/index.module.scss +++ /dev/null @@ -1,426 +0,0 @@ -// 头部样式 -.header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 16px; - background: #fff; - border-bottom: 1px solid #f0f0f0; - - .title { - font-size: 18px; - font-weight: 600; - color: #333; - } - - .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; - } - - .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; - } - - .nickname { - font-size: 18px; - font-weight: 600; - color: #333; - margin-bottom: 4px; - } - - .wechatId { - font-size: 14px; - color: #666; - margin-bottom: 8px; - } - - .tags { - display: flex; - flex-wrap: wrap; - gap: 8px; - } - - .userTag { - font-size: 12px; - padding: 4px 8px; - border-radius: 12px; - } -} - -// 标签导航 -.tabNav { - display: flex; - 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: 10px 10px 10px 16px; -} - -.tabContent { - display: flex; - flex-direction: column; - gap: 16px; -} - -// 信息卡片 -.infoCard { - border-radius: 12px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - overflow: hidden; - - :global(.adm-card-header) { - padding: 16px; - border-bottom: 1px solid #f0f0f0; - font-weight: 600; - color: #333; - } - - :global(.adm-card-body) { - padding: 0; - } -} - -// 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 { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 16px; - padding: 16px; -} - -.statItem { - text-align: center; - padding: 12px; - background: #f8f9fa; - border-radius: 8px; -} - -.statValue { - font-size: 18px; - font-weight: 600; - margin-bottom: 4px; -} - -.statLabel { - font-size: 12px; - color: #666; -} - -// 用户旅程 -.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: 20px 16px; - text-align: center; -} - -.emptyIcon { - margin-bottom: 16px; - opacity: 0.6; -} - -.emptyText { - font-size: 14px; - color: #666; - margin-bottom: 4px; - font-weight: 500; -} - -.emptyDesc { - font-size: 12px; - 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; - } -} diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/detail/index.tsx b/Cunkebao/src/pages/mobile/mine/traffic-pool/detail/index.tsx deleted file mode 100644 index ca0242b4..00000000 --- a/Cunkebao/src/pages/mobile/mine/traffic-pool/detail/index.tsx +++ /dev/null @@ -1,795 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { useParams } from "react-router-dom"; -import { Card, Button, Avatar, Tag, List, SpinLoading } from "antd-mobile"; -import { - UserOutlined, - CrownOutlined, - EyeOutlined, - DollarOutlined, - MobileOutlined, - TagOutlined, - FileTextOutlined, - UserAddOutlined, -} from "@ant-design/icons"; -import Layout from "@/components/Layout/Layout"; -import NavCommon from "@/components/NavCommon"; -import { getTrafficPoolDetail, getUserJourney, getUserTags } from "./api"; -import type { - ExtendedUserDetail, - UserJourneyRecord, - UserTagsResponse, - UserTagItem, -} from "./data"; -import styles from "./index.module.scss"; - -// RMM评分辅助函数 -const getRmmValueLevel = (totalScore: number): string => { - if (totalScore >= 12) return "高价值客户"; - if (totalScore >= 8) return "中等价值客户"; - if (totalScore >= 4) return "低价值客户"; - return "潜在客户"; -}; - -const getRmmColor = (totalScore: number): string => { - if (totalScore >= 12) return "danger"; - if (totalScore >= 8) return "warning"; - if (totalScore >= 4) return "primary"; - return "default"; -}; - -const TrafficPoolDetail: React.FC = () => { - const { wxid, userId } = useParams(); - const [loading, setLoading] = useState(true); - const [user, setUser] = useState(null); - const [activeTab, setActiveTab] = useState("basic"); - - // 用户旅程相关状态 - const [journeyLoading, setJourneyLoading] = useState(false); - const [journeyList, setJourneyList] = useState([]); - const [journeyPage, setJourneyPage] = useState(1); - const [journeyTotal, setJourneyTotal] = useState(0); - const pageSize = 10; - - // 用户标签相关状态 - const [tagsLoading, setTagsLoading] = useState(false); - const [userTagsList, setUserTagsList] = useState([]); - const [wechatTagsList, setWechatTagsList] = useState([]); - - useEffect(() => { - if (!wxid) return; - setLoading(true); - getTrafficPoolDetail(wxid as string) - .then(res => { - // 直接使用API返回的数据结构 - const extendedUser: ExtendedUserDetail = { - ...res, - // 根据新数据结构构建userInfo - userInfo: { - nickname: res.nickname, - avatar: res.avatar, - wechatId: res.wechatId, - friendShip: { - totalFriend: res.source?.length || 0, - maleFriend: res.source?.filter(s => s.gender === 1).length || 0, - femaleFriend: res.source?.filter(s => s.gender === 2).length || 0, - unknowFriend: res.source?.filter(s => s.gender === 0).length || 0, - }, - }, - // 使用API返回的RMM数据 - rfmScore: { - recency: res.rmm.r, - frequency: res.rmm.f, - monetary: res.rmm.m, - totalScore: res.rmm.r + res.rmm.f + res.rmm.m, - }, - // 根据数据推断流量池信息 - trafficPools: { - currentPool: res.total.isFriend ? "已添加好友池" : "待添加池", - availablePools: ["高价值客户池", "活跃用户池", "新用户池"], - }, - // 基于数据生成用户标签 - userTags: [ - ...(res.total.isFriend - ? [ - { - id: "friend", - name: "已添加好友", - color: "success", - type: "status", - }, - ] - : []), - ...(res.total.money > 0 - ? [ - { - id: "paid", - name: "付费用户", - color: "warning", - type: "value", - }, - ] - : []), - ...(res.total.msg > 10 - ? [ - { - id: "active", - name: "高频互动", - color: "primary", - type: "behavior", - }, - ] - : []), - ...(res.source?.length > 1 - ? [ - { - id: "multi", - name: "多设备用户", - color: "danger", - type: "device", - }, - ] - : []), - ], - // 基于RMM评分生成价值标签 - valueTags: [ - { - id: "rmm", - name: getRmmValueLevel(res.rmm.r + res.rmm.f + res.rmm.m), - color: getRmmColor(res.rmm.r + res.rmm.f + res.rmm.m), - icon: "crown", - rfmScore: res.rmm.r + res.rmm.f + res.rmm.m, - valueLevel: getRmmValueLevel(res.rmm.r + res.rmm.f + res.rmm.m), - }, - ], - }; - console.log("用户详情数据:", extendedUser); - - setUser(extendedUser); - }) - .finally(() => setLoading(false)); - }, [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 || []); - setWechatTagsList(response.wechat || []); - } 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 getJourneyTypeIcon = (type: number) => { - switch (type) { - case 0: // 浏览 - return ; - case 2: // 提交订单 - return ; - case 3: // 注册 - return ; - default: - return ; - } - }; - - 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 ; - case "view": - return ; - case "purchase": - return ; - default: - return ; - } - }; - - 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 (!user) { - return ( - } loading={loading}> -
-
未找到该用户
-
-
- ); - } - - return ( - - - {/* 用户基本信息 */} - -
- - -
- } - /> -
-
{user.nickname}
-
{user.wechatId}
-
- {user.valueTags?.map(tag => ( - - - {tag.name} - - ))} - {user.total.isFriend && ( - - 已添加好友 - - )} -
-
- -
- {/* 导航标签 */} -
-
handleTabChange("basic")} - > - 基本信息 -
-
handleTabChange("journey")} - > - 用户旅程 -
-
handleTabChange("tags")} - > - 用户标签 -
-
- - } - > -
- {/* 内容区域 */} -
- {activeTab === "basic" && ( -
- {/* 关联信息 */} - - - - 设备 - - 微信号 - 别名 - - 添加时间 - - - 最近互动 - - - - - {/* RFM评分 */} - {user.rfmScore && ( - -
-
-
最近性(R)
-
- {user.rfmScore.recency} -
-
-
-
频率(F)
-
- {user.rfmScore.frequency} -
-
-
-
金额(M)
-
- {user.rfmScore.monetary} -
-
-
-
总分
-
- {user.rfmScore.totalScore}/15 -
-
-
-
- )} - - {/* 流量池 */} - {user.trafficPools && ( - -
-
- 当前池: - - {user.trafficPools.currentPool} - -
-
- 可选池: - {user.trafficPools.availablePools.map((pool, index) => ( - - {pool} - - ))} -
-
-
- )} - - {/* 统计数据 */} - -
-
-
- ¥{user.total.money || 0} -
-
总消费
-
-
-
- {user.total.msg || 0} -
-
互动次数
-
-
-
- {user.total.percentage || "0"}% -
-
转化率
-
-
-
- {user.total.isFriend ? "已添加" : "未添加"} -
-
添加状态
-
-
-
- - {/* 好友统计 */} - -
-
-
- {user.userInfo?.friendShip.totalFriend || 0} -
-
总好友
-
-
-
- {user.userInfo?.friendShip.maleFriend || 0} -
-
男性好友
-
-
-
- {user.userInfo?.friendShip.femaleFriend || 0} -
-
女性好友
-
-
-
- {user.userInfo?.friendShip.unknowFriend || 0} -
-
未知性别
-
-
-
- - {/* 限制记录 */} - - {user.restrictions && user.restrictions.length > 0 ? ( - - {user.restrictions.map(restriction => ( - - {restriction.reason || "未知原因"} - - {getRestrictionLevelText(restriction.level)} - -
- } - description={ -
- 限制ID: {restriction.id} - {restriction.date && ( - - 限制时间: {formatDate(restriction.date)} - - )} -
- } - /> - ))} - - ) : ( -
-
- -
-
暂无限制记录
-
- 该用户没有任何限制记录 -
-
- )} - -
- )} - - {activeTab === "journey" && ( -
- - {journeyLoading && journeyList.length === 0 ? ( -
- -
加载中...
-
- ) : journeyList.length === 0 ? ( -
-
- -
-
暂无互动记录
-
- 该用户还没有任何互动行为 -
-
- ) : ( - - {journeyList.map(record => ( - - {record.remark} - - {formatDateTime(record.createTime)} - -
- } - /> - ))} - {journeyLoading && journeyList.length > 0 && ( -
- - 加载更多... -
- )} - {!journeyLoading && journeyList.length < journeyTotal && ( -
- -
- )} - - )} - -
- )} - - {activeTab === "tags" && ( -
- {/* 站内标签 */} - - {tagsLoading && userTagsList.length === 0 ? ( -
- -
加载中...
-
- ) : userTagsList.length === 0 ? ( -
-
- -
-
暂无站内标签
-
- 该用户还没有任何站内标签 -
-
- ) : ( -
- {userTagsList.map((tag, index) => ( - - {tag.name} - - ))} -
- )} -
- - {/* 微信标签 */} - - {tagsLoading && wechatTagsList.length === 0 ? ( -
- -
加载中...
-
- ) : wechatTagsList.length === 0 ? ( -
-
- -
-
暂无微信标签
-
- 该用户还没有任何微信标签 -
-
- ) : ( -
- {wechatTagsList.map((tag, index) => ( - - {tag} - - ))} -
- )} -
- - {/* 价值标签 */} - - {user.valueTags && user.valueTags.length > 0 ? ( -
- {user.valueTags.map(tag => ( -
-
- - {tag.icon === "crown" && } - {tag.name} - - - RFM总分: {tag.rfmScore}/15 - -
-
- - 价值等级: - - - {tag.valueLevel} - -
-
- ))} -
- ) : ( -
-
- -
-
暂无价值标签
-
- 该用户还没有任何价值标签 -
-
- )} -
-
- )} - - -
- ); -}; - -export default TrafficPoolDetail; diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/form/README.md b/Cunkebao/src/pages/mobile/mine/traffic-pool/form/README.md new file mode 100644 index 00000000..f8b95a7e --- /dev/null +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/form/README.md @@ -0,0 +1,101 @@ +# 新建流量包功能 + +## 功能概述 + +新建流量包功能是一个完整的用户群体管理工具,允许用户创建和管理基于特定条件的用户分组。 + +## 页面结构 + +### 主页面 (`index.tsx`) + +- 包含三个标签页:基本信息、人群筛选、用户列表 +- 使用 Tabs 组件进行页面切换 +- 底部固定提交按钮 + +### 组件结构 + +#### 1. 基本信息组件 (`BasicInfo.tsx`) + +- **流量包名称**:必填字段 +- **描述**:可选字段 +- **备注**:可选字段,支持多行输入 + +#### 2. 人群筛选组件 (`AudienceFilter.tsx`) + +- **RFM分析**:展示最近消费、消费频率、消费金额 +- **年龄层**:显示年龄范围 +- **消费能力**:显示消费能力等级 +- **标签筛选**:预设的8个标签 +- **自定义条件**:支持添加自定义筛选条件 +- **方案推荐**:提供6个预设方案 + +#### 3. 用户列表预览组件 (`UserListPreview.tsx`) + +- 显示筛选后的用户列表 +- 支持全选和批量操作 +- 显示用户详细信息(RFM评分、活跃度、消费金额等) +- 支持单个用户移除 + +#### 4. 自定义条件弹窗 (`CustomConditionModal.tsx`) + +- 支持10种不同的标签类型 +- 根据标签类型显示不同的输入方式: + - 年龄层:两个数字输入框(范围) + - 其他标签:下拉选择框 +- 支持条件的添加和删除 + +#### 5. 方案推荐弹窗 (`SchemeRecommendation.tsx`) + +- 提供6个预设方案: + - 高价值客户方案 + - 新用户激活方案 + - 用户留存方案 + - 升单转化方案 + - 价格敏感用户方案 + - 忠诚客户维护方案 +- 每个方案包含筛选条件和预估用户数量 + +#### 6. 条件列表组件 (`ConditionList.tsx`) + +- 显示已添加的自定义条件 +- 支持条件的删除和编辑 + +## 数据流 + +1. **基本信息** → 保存到 `formData` 状态 +2. **筛选条件** → 保存到 `formData.filterConditions` +3. **生成用户列表** → 调用模拟API生成用户数据 +4. **提交** → 将所有数据提交到后端 + +## 路由配置 + +- 路径:`/mine/traffic-pool/create` +- 组件:`CreateTrafficPackage` +- 权限:需要登录 + +## 使用流程 + +1. 填写基本信息(流量包名称必填) +2. 在人群筛选页面设置筛选条件: + - 使用预设标签 + - 添加自定义条件 + - 或选择预设方案 +3. 点击"生成用户列表"查看筛选结果 +4. 在用户列表页面预览和调整用户 +5. 点击"创建流量包"完成创建 + +## 技术特点 + +- **模块化设计**:每个功能独立封装为组件 +- **响应式布局**:适配移动端显示 +- **状态管理**:使用React Hooks管理复杂状态 +- **用户体验**:提供丰富的交互反馈 +- **数据模拟**:包含完整的模拟数据用于演示 + +## 扩展性 + +- 支持添加新的标签类型 +- 支持添加新的预设方案 +- 支持自定义筛选逻辑 +- 支持导出用户列表 +- 支持批量操作功能 diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/form/api.ts b/Cunkebao/src/pages/mobile/mine/traffic-pool/form/api.ts new file mode 100644 index 00000000..398ab200 --- /dev/null +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/form/api.ts @@ -0,0 +1,75 @@ +import request from "@/api/request"; + +// 创建流量包 +export interface CreateTrafficPackageParams { + name: string; + description?: string; + remarks?: string; + filterConditions: any[]; + userIds: string[]; +} + +export interface CreateTrafficPackageResponse { + id: string; + name: string; + success: boolean; + message: string; +} + +export async function createTrafficPackage( + params: CreateTrafficPackageParams, +): Promise { + return request("/v1/traffic/pool/create", params, "POST"); +} + +// 获取用户列表(根据筛选条件) +export interface GetUsersByFilterParams { + conditions: any[]; + page?: number; + pageSize?: number; +} + +export interface User { + id: string; + name: string; + avatar: string; + tags: string[]; + rfmScore: number; + lastActive: string; + consumption: number; +} + +export interface GetUsersByFilterResponse { + list: User[]; + total: number; +} + +export async function getUsersByFilter( + params: GetUsersByFilterParams, +): Promise { + return request("/v1/traffic/pool/users/filter", params, "POST"); +} + +// 获取预设方案列表 +export interface PresetScheme { + id: string; + name: string; + description: string; + conditions: any[]; + userCount: number; + color: string; +} + +export async function getPresetSchemes(): Promise { + return request("/v1/traffic/pool/schemes", {}, "GET"); +} + +// 获取行业选项(固定筛选项) +export interface IndustryOption { + label: string; + value: string | number; +} + +export async function getIndustryOptions(): Promise { + return request("/v1/traffic/pool/industries", {}, "GET"); +} diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/AudienceFilter.module.scss b/Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/AudienceFilter.module.scss new file mode 100644 index 00000000..75c1e09e --- /dev/null +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/AudienceFilter.module.scss @@ -0,0 +1,121 @@ +.container { + padding: 0; +} + +.card { + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; +} + +.title { + font-size: 16px; + font-weight: 600; + color: #333; +} + +.schemeBtn { + display: flex; + align-items: center; + gap: 4px; + font-size: 12px; + padding: 4px 8px; + height: 28px; +} + +.section { + margin-bottom: 24px; +} + +.sectionTitle { + font-size: 14px; + font-weight: 500; + color: #333; + margin-bottom: 12px; +} + +.rfmGrid { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 12px; +} + +.rfmItem { + background: #f8f9fa; + border-radius: 6px; + padding: 12px; + text-align: center; +} + +.rfmLabel { + font-size: 12px; + color: #666; + margin-bottom: 4px; +} + +.rfmValue { + font-size: 14px; + font-weight: 500; + color: #333; +} + +.ageRange { + background: #f8f9fa; + border-radius: 6px; + padding: 12px; + text-align: center; + font-size: 14px; + color: #333; +} + +.consumptionLevel { + display: flex; + justify-content: center; +} + +.levelTag { + background: #52c41a; + color: white; + padding: 4px 12px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; +} + +.tagGrid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 8px; +} + +.tag { + padding: 8px 12px; + border-radius: 16px; + color: white; + font-size: 12px; + text-align: center; + font-weight: 500; +} + +.addConditionBtn { + width: 100%; + margin: 16px 0; + border-style: dashed; + border-color: #d9d9d9; + color: #666; + + &:hover { + border-color: #1677ff; + color: #1677ff; + } +} + +.generateBtn { + margin-top: 16px; +} diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/AudienceFilter.tsx b/Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/AudienceFilter.tsx new file mode 100644 index 00000000..6cc023fe --- /dev/null +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/AudienceFilter.tsx @@ -0,0 +1,209 @@ +import React, { useEffect, useState } from "react"; +import { Card, Button } from "antd-mobile"; +import { Select } from "antd"; +import { EditSOutline } from "antd-mobile-icons"; +import CustomConditionModal from "./CustomConditionModal"; +import SchemeRecommendation from "./SchemeRecommendation"; +import ConditionList from "./ConditionList"; +import styles from "./AudienceFilter.module.scss"; +import { getIndustryOptions, IndustryOption } from "../api"; + +interface FilterCondition { + id: string; + type: string; + label: string; + value: any; + operator?: string; +} + +interface AudienceFilterProps { + conditions: FilterCondition[]; + onChange: (conditions: FilterCondition[]) => void; + onGenerate: (users: any[]) => void; +} + +const AudienceFilter: React.FC = ({ + conditions, + onChange, + onGenerate, +}) => { + const [showCustomModal, setShowCustomModal] = useState(false); + const [showSchemeModal, setShowSchemeModal] = useState(false); + const [industryOptions, setIndustryOptions] = useState([]); + const [selectedIndustry, setSelectedIndustry] = useState< + string | number | undefined + >(undefined); + + // 加载行业选项(固定筛选项) + useEffect(() => { + getIndustryOptions() + .then(res => setIndustryOptions(res || [])) + .catch(() => setIndustryOptions([])); + }, []); + + const handleAddCondition = (condition: FilterCondition) => { + const newConditions = [...conditions, condition]; + onChange(newConditions); + }; + + const handleRemoveCondition = (id: string) => { + const newConditions = conditions.filter(c => c.id !== id); + onChange(newConditions); + }; + + const handleUpdateCondition = (id: string, value: any) => { + const newConditions = conditions.map(c => + c.id === id ? { ...c, value } : c, + ); + onChange(newConditions); + }; + + const handleApplyScheme = (schemeConditions: FilterCondition[]) => { + onChange(schemeConditions); + setShowSchemeModal(false); + }; + + const handleGenerate = () => { + // 模拟生成用户数据 + const mockUsers = generateMockUsers(conditions); + onGenerate(mockUsers); + }; + + return ( +
+ +
+
人群筛选
+ +
+ + {/* 行业筛选(固定项,接口获取选项) */} +
+
行业
+ handleChange("name", value)} + className={styles.input} + /> + + + 描述}> + handleChange("description", value)} + className={styles.input} + /> + + + 备注}> + handleChange("remarks", value)} + className={styles.textarea} + rows={3} + /> + + + +
+ ); +}; + +export default BasicInfo; diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/ConditionList.module.scss b/Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/ConditionList.module.scss new file mode 100644 index 00000000..823417b8 --- /dev/null +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/ConditionList.module.scss @@ -0,0 +1,52 @@ +.container { + margin-bottom: 24px; +} + +.title { + font-size: 14px; + font-weight: 500; + color: #333; + margin-bottom: 12px; +} + +.conditionList { + display: flex; + flex-direction: column; + gap: 8px; +} + +.conditionItem { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px; + background: #f8f9fa; + border-radius: 6px; + border: 1px solid #e9ecef; +} + +.conditionContent { + display: flex; + align-items: center; + gap: 8px; +} + +.conditionLabel { + font-size: 14px; + color: #666; +} + +.conditionValue { + font-size: 14px; + font-weight: 500; + color: #333; +} + +.removeBtn { + color: #ff4d4f; + padding: 4px; + + &:hover { + background-color: #fff2f0; + } +} diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/ConditionList.tsx b/Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/ConditionList.tsx new file mode 100644 index 00000000..d0c27e85 --- /dev/null +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/ConditionList.tsx @@ -0,0 +1,67 @@ +import React from "react"; +import { Button } from "antd-mobile"; +import { DeleteOutline } from "antd-mobile-icons"; +import styles from "./ConditionList.module.scss"; + +interface FilterCondition { + id: string; + type: string; + label: string; + value: any; + operator?: string; +} + +interface ConditionListProps { + conditions: FilterCondition[]; + onRemove: (id: string) => void; + onUpdate: (id: string, value: any) => void; +} + +const ConditionList: React.FC = ({ + conditions, + onRemove, + onUpdate, +}) => { + const formatConditionValue = (condition: FilterCondition) => { + switch (condition.type) { + case "range": + return `${condition.value.min || 0}-${condition.value.max || 0}岁`; + case "select": + return condition.value; + default: + return condition.value; + } + }; + + if (conditions.length === 0) { + return null; + } + + return ( +
+
自定义条件
+
+ {conditions.map(condition => ( +
+
+ {condition.label}: + + {formatConditionValue(condition)} + +
+ +
+ ))} +
+
+ ); +}; + +export default ConditionList; diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/CustomConditionModal.module.scss b/Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/CustomConditionModal.module.scss new file mode 100644 index 00000000..41a71d0b --- /dev/null +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/CustomConditionModal.module.scss @@ -0,0 +1,80 @@ +.container { + height: 100%; + display: flex; + flex-direction: column; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px; + border-bottom: 1px solid #f0f0f0; +} + +.title { + font-size: 16px; + font-weight: 600; + color: #333; +} + +.content { + flex: 1; + padding: 16px; + overflow-y: auto; +} + +.section { + margin-bottom: 24px; +} + +.sectionTitle { + font-size: 14px; + font-weight: 500; + color: #333; + margin-bottom: 12px; +} + +.tagList { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 8px; +} + +.tagItem { + padding: 12px; + border: 1px solid #d9d9d9; + border-radius: 6px; + text-align: center; + font-size: 14px; + color: #333; + cursor: pointer; + transition: all 0.2s; + + &:hover { + border-color: #1677ff; + color: #1677ff; + } + + &.selected { + border-color: #1677ff; + background-color: #e6f7ff; + color: #1677ff; + } +} + +.rangeInputs { + display: flex; + align-items: center; + gap: 12px; +} + +.rangeSeparator { + color: #666; + font-weight: 500; +} + +.footer { + padding: 16px; + border-top: 1px solid #f0f0f0; +} diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/CustomConditionModal.tsx b/Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/CustomConditionModal.tsx new file mode 100644 index 00000000..f4ae5e50 --- /dev/null +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/CustomConditionModal.tsx @@ -0,0 +1,242 @@ +import React, { useState } from "react"; +import { Popup, Form, Input, Selector, Button } from "antd-mobile"; +import styles from "./CustomConditionModal.module.scss"; + +interface CustomConditionModalProps { + visible: boolean; + onClose: () => void; + onAdd: (condition: any) => void; +} + +// 模拟标签数据 +const mockTags = [ + { id: "age", name: "年龄层", type: "range", options: [] }, + { + id: "consumption", + name: "消费能力", + type: "select", + options: [ + { label: "高", value: "high" }, + { label: "中", value: "medium" }, + { label: "低", value: "low" }, + ], + }, + { + id: "gender", + name: "性别", + type: "select", + options: [ + { label: "男", value: "male" }, + { label: "女", value: "female" }, + { label: "未知", value: "unknown" }, + ], + }, + { + id: "location", + name: "所在地区", + type: "select", + options: [ + { label: "厦门", value: "xiamen" }, + { label: "泉州", value: "quanzhou" }, + { label: "福州", value: "fuzhou" }, + ], + }, + { + id: "source", + name: "客户来源", + type: "select", + options: [ + { label: "抖音", value: "douyin" }, + { label: "门店扫码", value: "store" }, + { label: "朋友推荐", value: "referral" }, + { label: "广告投放", value: "ad" }, + ], + }, + { + id: "frequency", + name: "消费频率", + type: "select", + options: [ + { label: "高频(>3次/月)", value: "high" }, + { label: "中频", value: "medium" }, + { label: "低频", value: "low" }, + ], + }, + { + id: "sensitivity", + name: "优惠敏感度", + type: "select", + options: [ + { label: "高", value: "high" }, + { label: "中", value: "medium" }, + { label: "低", value: "low" }, + ], + }, + { + id: "category", + name: "品类偏好", + type: "select", + options: [ + { label: "护肤", value: "skincare" }, + { label: "茶饮", value: "tea" }, + { label: "宠物", value: "pet" }, + { label: "课程", value: "course" }, + ], + }, + { + id: "repurchase", + name: "复购行为", + type: "select", + options: [ + { label: "有", value: "yes" }, + { label: "无", value: "no" }, + ], + }, + { + id: "satisfaction", + name: "售后满意度", + type: "select", + options: [ + { label: "好评", value: "good" }, + { label: "一般", value: "average" }, + { label: "差评", value: "bad" }, + ], + }, +]; + +const CustomConditionModal: React.FC = ({ + visible, + onClose, + onAdd, +}) => { + const [selectedTag, setSelectedTag] = useState(null); + const [conditionValue, setConditionValue] = useState(null); + + const handleTagSelect = (tag: any) => { + setSelectedTag(tag); + setConditionValue(null); + }; + + const handleValueChange = (value: any) => { + setConditionValue(value); + }; + + const handleSubmit = () => { + if (!selectedTag || !conditionValue) return; + + const condition = { + id: `${selectedTag.id}_${Date.now()}`, + type: selectedTag.type, + label: selectedTag.name, + value: conditionValue, + }; + + onAdd(condition); + onClose(); + setSelectedTag(null); + setConditionValue(null); + }; + + const renderValueInput = () => { + if (!selectedTag) return null; + + switch (selectedTag.type) { + case "range": + return ( +
+ + setConditionValue(prev => ({ ...prev, min: value })) + } + /> + - + + setConditionValue(prev => ({ ...prev, max: value })) + } + /> +
+ ); + + case "select": + return ( + handleValueChange(value[0])} + multiple={false} + /> + ); + + default: + return ( + + ); + } + }; + + return ( + +
+
+
添加自定义条件
+ +
+ +
+
+
选择标签
+
+ {mockTags.map(tag => ( +
handleTagSelect(tag)} + > + {tag.name} +
+ ))} +
+
+ + {selectedTag && ( +
+
设置条件
+ {renderValueInput()} +
+ )} +
+ +
+ +
+
+
+ ); +}; + +export default CustomConditionModal; diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/SchemeRecommendation.module.scss b/Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/SchemeRecommendation.module.scss new file mode 100644 index 00000000..20b5d157 --- /dev/null +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/SchemeRecommendation.module.scss @@ -0,0 +1,92 @@ +.container { + height: 100%; + display: flex; + flex-direction: column; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px; + border-bottom: 1px solid #f0f0f0; +} + +.title { + font-size: 16px; + font-weight: 600; + color: #333; +} + +.content { + flex: 1; + padding: 16px; + overflow-y: auto; +} + +.schemeList { + display: flex; + flex-direction: column; + gap: 16px; +} + +.schemeCard { + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); +} + +.schemeHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} + +.schemeName { + font-size: 16px; + font-weight: 600; + color: #333; +} + +.schemeBadge { + color: white; + padding: 4px 8px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; +} + +.schemeDescription { + font-size: 14px; + color: #666; + margin-bottom: 12px; + line-height: 1.4; +} + +.schemeConditions { + margin-bottom: 16px; +} + +.conditionsTitle { + font-size: 12px; + color: #999; + margin-bottom: 8px; +} + +.conditionsList { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.conditionTag { + background: #f0f0f0; + color: #666; + padding: 2px 8px; + border-radius: 10px; + font-size: 12px; +} + +.applyBtn { + width: 100%; +} diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/SchemeRecommendation.tsx b/Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/SchemeRecommendation.tsx new file mode 100644 index 00000000..2e43a546 --- /dev/null +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/SchemeRecommendation.tsx @@ -0,0 +1,201 @@ +import React from "react"; +import { Popup, Card, Button } from "antd-mobile"; +import styles from "./SchemeRecommendation.module.scss"; + +interface FilterCondition { + id: string; + type: string; + label: string; + value: any; + operator?: string; +} + +interface SchemeRecommendationProps { + visible: boolean; + onClose: () => void; + onApply: (conditions: FilterCondition[]) => void; +} + +// 预设方案数据 +const presetSchemes = [ + { + id: "high_value", + name: "高价值客户方案", + description: "针对高消费、高活跃度的优质客户", + conditions: [ + { id: "consumption_1", type: "select", label: "消费能力", value: "high" }, + { id: "frequency_1", type: "select", label: "消费频率", value: "high" }, + { + id: "satisfaction_1", + type: "select", + label: "售后满意度", + value: "good", + }, + ], + userCount: 1250, + color: "#1677ff", + }, + { + id: "new_user", + name: "新用户激活方案", + description: "针对新注册用户,提高首次消费转化", + conditions: [ + { + id: "age_2", + type: "range", + label: "年龄层", + value: { min: 18, max: 35 }, + }, + { id: "source_2", type: "select", label: "客户来源", value: "douyin" }, + { id: "frequency_2", type: "select", label: "消费频率", value: "low" }, + ], + userCount: 3200, + color: "#52c41a", + }, + { + id: "retention", + name: "用户留存方案", + description: "针对有流失风险的客户,进行召回激活", + conditions: [ + { id: "frequency_3", type: "select", label: "消费频率", value: "low" }, + { + id: "satisfaction_3", + type: "select", + label: "售后满意度", + value: "average", + }, + { id: "repurchase_3", type: "select", label: "复购行为", value: "no" }, + ], + userCount: 890, + color: "#faad14", + }, + { + id: "upsell", + name: "升单转化方案", + description: "针对有升单潜力的客户,推荐高价值产品", + conditions: [ + { + id: "consumption_4", + type: "select", + label: "消费能力", + value: "medium", + }, + { id: "frequency_4", type: "select", label: "消费频率", value: "medium" }, + { + id: "category_4", + type: "select", + label: "品类偏好", + value: "skincare", + }, + ], + userCount: 1560, + color: "#722ed1", + }, + { + id: "price_sensitive", + name: "价格敏感用户方案", + description: "针对对价格敏感的用户,提供优惠活动", + conditions: [ + { + id: "sensitivity_5", + type: "select", + label: "优惠敏感度", + value: "high", + }, + { id: "consumption_5", type: "select", label: "消费能力", value: "low" }, + { id: "frequency_5", type: "select", label: "消费频率", value: "low" }, + ], + userCount: 2100, + color: "#eb2f96", + }, + { + id: "loyal_customer", + name: "忠诚客户维护方案", + description: "针对高忠诚度客户,提供VIP服务", + conditions: [ + { id: "frequency_6", type: "select", label: "消费频率", value: "high" }, + { id: "repurchase_6", type: "select", label: "复购行为", value: "yes" }, + { + id: "satisfaction_6", + type: "select", + label: "售后满意度", + value: "good", + }, + ], + userCount: 680, + color: "#13c2c2", + }, +]; + +const SchemeRecommendation: React.FC = ({ + visible, + onClose, + onApply, +}) => { + const handleApplyScheme = (scheme: any) => { + onApply(scheme.conditions); + }; + + return ( + +
+
+
方案推荐
+ +
+ +
+
+ {presetSchemes.map(scheme => ( + +
+
{scheme.name}
+
+ {scheme.userCount}人 +
+
+ +
+ {scheme.description} +
+ +
+
筛选条件:
+
+ {scheme.conditions.map((condition, index) => ( + + {condition.label}: {condition.value} + + ))} +
+
+ + +
+ ))} +
+
+
+
+ ); +}; + +export default SchemeRecommendation; diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/UserListPreview.module.scss b/Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/UserListPreview.module.scss new file mode 100644 index 00000000..ff7e0adb --- /dev/null +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/UserListPreview.module.scss @@ -0,0 +1,126 @@ +.container { + padding: 0; +} + +.card { + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; +} + +.title { + font-size: 16px; + font-weight: 600; + color: #333; +} + +.userCount { + font-size: 14px; + color: #1677ff; + font-weight: 500; +} + +.batchActions { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 0; + border-bottom: 1px solid #f0f0f0; + margin-bottom: 16px; +} + +.selectAllCheckbox { + font-size: 14px; + color: #333; +} + +.removeSelectedBtn { + font-size: 12px; + padding: 4px 8px; + height: 28px; +} + +.userList { + display: flex; + flex-direction: column; + gap: 12px; +} + +.userItem { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 12px; + background: #f8f9fa; + border-radius: 8px; + border: 1px solid #e9ecef; +} + +.userCheckbox { + margin-top: 4px; +} + +.userAvatar { + flex-shrink: 0; +} + +.userInfo { + flex: 1; + min-width: 0; +} + +.userName { + font-size: 16px; + font-weight: 600; + color: #333; + margin-bottom: 4px; +} + +.userId { + font-size: 12px; + color: #666; + margin-bottom: 8px; +} + +.userTags { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-bottom: 8px; +} + +.tag { + background: #e6f7ff; + color: #1677ff; + padding: 2px 6px; + border-radius: 8px; + font-size: 10px; + font-weight: 500; +} + +.userStats { + display: flex; + flex-wrap: wrap; + gap: 12px; +} + +.statItem { + font-size: 12px; + color: #666; +} + +.removeBtn { + color: #ff4d4f; + padding: 4px; + flex-shrink: 0; + + &:hover { + background-color: #fff2f0; + } +} diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/UserListPreview.tsx b/Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/UserListPreview.tsx new file mode 100644 index 00000000..7e727948 --- /dev/null +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/UserListPreview.tsx @@ -0,0 +1,155 @@ +import React, { useState } from "react"; +import { Card, Avatar, Button, Checkbox, Empty } from "antd-mobile"; +import { DeleteOutline } from "antd-mobile-icons"; +import styles from "./UserListPreview.module.scss"; + +interface User { + id: string; + name: string; + avatar: string; + tags: string[]; + rfmScore: number; + lastActive: string; + consumption: number; +} + +interface UserListPreviewProps { + users: User[]; + onRemoveUser: (userId: string) => void; +} + +const UserListPreview: React.FC = ({ + users, + onRemoveUser, +}) => { + const [selectedUsers, setSelectedUsers] = useState([]); + + const handleSelectAll = (checked: boolean) => { + if (checked) { + setSelectedUsers(users.map(user => user.id)); + } else { + setSelectedUsers([]); + } + }; + + const handleSelectUser = (userId: string, checked: boolean) => { + if (checked) { + setSelectedUsers(prev => [...prev, userId]); + } else { + setSelectedUsers(prev => prev.filter(id => id !== userId)); + } + }; + + const handleRemoveSelected = () => { + selectedUsers.forEach(userId => onRemoveUser(userId)); + setSelectedUsers([]); + }; + + const getRfmLevel = (score: number) => { + if (score >= 12) return { level: "高价值", color: "#ff4d4f" }; + if (score >= 8) return { level: "中等价值", color: "#faad14" }; + if (score >= 4) return { level: "低价值", color: "#52c41a" }; + return { level: "潜在客户", color: "#bfbfbf" }; + }; + + if (users.length === 0) { + return ( +
+ + + +
+ ); + } + + return ( +
+ +
+
用户列表预览
+
共 {users.length} 个用户
+
+ + {users.length > 0 && ( +
+ 0 + } + onChange={handleSelectAll} + className={styles.selectAllCheckbox} + > + 全选 + + {selectedUsers.length > 0 && ( + + )} +
+ )} + +
+ {users.map(user => { + const rfmInfo = getRfmLevel(user.rfmScore); + + return ( +
+ handleSelectUser(user.id, checked)} + className={styles.userCheckbox} + /> + + + +
+
{user.name}
+
ID: {user.id}
+
+ {user.tags.map((tag, index) => ( + + {tag} + + ))} +
+
+ + RFM:{" "} + + {rfmInfo.level} + + + + 活跃: {user.lastActive} + + + 消费: ¥{user.consumption} + +
+
+ + +
+ ); + })} +
+
+
+ ); +}; + +export default UserListPreview; diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/form/index.module.scss b/Cunkebao/src/pages/mobile/mine/traffic-pool/form/index.module.scss new file mode 100644 index 00000000..1712b8d4 --- /dev/null +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/form/index.module.scss @@ -0,0 +1,49 @@ +.tabsContainer { + background: #fff; + border-bottom: 1px solid #f0f0f0; +} + +.tabs { + :global(.adm-tabs-header) { + border-bottom: none; + } + + :global(.adm-tabs-tab) { + font-size: 14px; + padding: 12px 16px; + } + + :global(.adm-tabs-tab-active) { + color: #1677ff; + font-weight: 500; + } +} + +.content { + padding: 16px; + min-height: calc(100vh - 200px); +} + +.footer { + padding: 16px; + background: #fff; + border-top: 1px solid #f0f0f0; +} + +.buttonGroup { + display: flex; + gap: 12px; + align-items: center; +} + +.prevButton { + flex: 1; +} + +.nextButton { + flex: 1; +} + +.submitButton { + flex: 1; +} diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/form/index.tsx b/Cunkebao/src/pages/mobile/mine/traffic-pool/form/index.tsx new file mode 100644 index 00000000..e6bd5433 --- /dev/null +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/form/index.tsx @@ -0,0 +1,180 @@ +import React, { useState } from "react"; +import { Button } from "antd-mobile"; +import Layout from "@/components/Layout/Layout"; +import NavCommon from "@/components/NavCommon"; +import BasicInfo from "./components/BasicInfo"; +import AudienceFilter from "./components/AudienceFilter"; +import UserListPreview from "./components/UserListPreview"; +import styles from "./index.module.scss"; +import StepIndicator from "@/components/StepIndicator"; + +const CreateTrafficPackage: React.FC = () => { + const [currentStep, setCurrentStep] = useState(1); // 1 基础信息 2 人群筛选 3 用户列表 + const [formData, setFormData] = useState({ + // 基本信息 + name: "", + description: "", + remarks: "", + // 筛选条件 + filterConditions: [], + // 用户列表 + filteredUsers: [], + }); + + const steps = [ + { id: 1, title: "basic", subtitle: "基本信息" }, + { id: 2, title: "filter", subtitle: "人群筛选" }, + { id: 3, title: "users", subtitle: "预览" }, + ]; + + const handleBasicInfoChange = (data: any) => { + setFormData(prev => ({ ...prev, ...data })); + }; + + const handleFilterChange = (conditions: any[]) => { + setFormData(prev => ({ ...prev, filterConditions: conditions })); + }; + + const handleGenerateUsers = (users: any[]) => { + setFormData(prev => ({ ...prev, filteredUsers: users })); + setCurrentStep(3); + }; + + // 初始化模拟数据 + React.useEffect(() => { + if (currentStep === 3 && formData.filteredUsers.length === 0) { + const mockUsers = [ + { + id: "U00000001", + name: "张三", + avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=1", + tags: ["高价值用户", "活跃用户"], + rfmScore: 12, + lastActive: "7天内", + consumption: 2500, + }, + { + id: "U00000002", + name: "李四", + avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=2", + tags: ["新用户", "价格敏感"], + rfmScore: 6, + lastActive: "3天内", + consumption: 800, + }, + { + id: "U00000003", + name: "王五", + avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=3", + tags: ["复购率高", "高潜力"], + rfmScore: 14, + lastActive: "1天内", + consumption: 3200, + }, + { + id: "U00000004", + name: "赵六", + avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=4", + tags: ["已沉睡", "流失风险"], + rfmScore: 3, + lastActive: "30天内", + consumption: 200, + }, + { + id: "U00000005", + name: "钱七", + avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=5", + tags: ["高价值用户", "复购率高"], + rfmScore: 15, + lastActive: "2天内", + consumption: 4500, + }, + ]; + setFormData(prev => ({ ...prev, filteredUsers: mockUsers })); + } + }, [currentStep, formData.filteredUsers.length]); + + const handleSubmit = () => { + // 提交逻辑 + console.log("提交数据:", formData); + }; + + const canSubmit = formData.name && formData.filterConditions.length > 0; + + const renderFooter = () => { + return ( +
+
+ {currentStep > 1 && ( + + )} + {currentStep < 3 ? ( + + ) : ( + + )} +
+
+ ); + }; + + return ( + + + + + } + footer={renderFooter()} + > +
+ {currentStep === 1 && ( + + )} + + {currentStep === 2 && ( + + )} + + {currentStep === 3 && ( + { + setFormData(prev => ({ + ...prev, + filteredUsers: prev.filteredUsers.filter( + (user: any) => user.id !== userId, + ), + })); + }} + /> + )} +
+
+ ); +}; + +export default CreateTrafficPackage; diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/info/api.ts b/Cunkebao/src/pages/mobile/mine/traffic-pool/info/api.ts deleted file mode 100644 index 9b53c030..00000000 --- a/Cunkebao/src/pages/mobile/mine/traffic-pool/info/api.ts +++ /dev/null @@ -1,34 +0,0 @@ -import request from "@/api/request"; - -// 获取流量池列表 -export function fetchTrafficPoolList(params: { - page?: number; - pageSize?: number; - keyword?: string; -}) { - return request("/v1/traffic/pool", params, "GET"); -} - -export async function fetchScenarioOptions() { - return request("/v1/plan/scenes", {}, "GET"); -} - -export async function fetchPackageOptions() { - return request("/v1/traffic/pool/getPackage", {}, "GET"); -} - -export async function addPackage(params: { - type: string; // 类型 1搜索 2选择用户 3文件上传 - addPackageId?: number; - addStatus?: number; - deviceId?: string; - keyword?: string; - packageId?: number; - packageName?: number; // 添加的流量池名称 - tableFile?: number; - taskId?: number; // 任务id j及场景获客id - userIds?: number[]; - userValue?: number; -}) { - return request("/v1/traffic/pool/addPackage", params, "POST"); -} diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/info/index.module.scss b/Cunkebao/src/pages/mobile/mine/traffic-pool/info/index.module.scss deleted file mode 100644 index 2fbee5e3..00000000 --- a/Cunkebao/src/pages/mobile/mine/traffic-pool/info/index.module.scss +++ /dev/null @@ -1,65 +0,0 @@ -.listWrap { - padding: 12px; -} - -.cardContent { - display: flex; - align-items: center; - gap: 12px; - position: relative; -} -.checkbox { - position: absolute; - top: 0; - left: 0; -} -.cardWrap { - background: #fff; - padding: 16px; - border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); - margin-bottom: 12px; -} - -.card { - margin-bottom: 12px; -} - -.title { - font-size: 16px; - font-weight: 600; - color: #222; -} - -.desc { - font-size: 13px; - color: #888; - margin: 6px 0 4px 0; -} - -.count { - font-size: 13px; - color: #1677ff; -} - -.pagination { - display: flex; - align-items: center; - justify-content: center; - gap: 16px; - margin: 16px 0; -} - -.pagination button { - background: #f5f5f5; - border: none; - border-radius: 4px; - padding: 4px 12px; - color: #1677ff; - cursor: pointer; -} - -.pagination button:disabled { - color: #ccc; - cursor: not-allowed; -} diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/list/api.ts b/Cunkebao/src/pages/mobile/mine/traffic-pool/list/api.ts index 98d29231..9171337a 100644 --- a/Cunkebao/src/pages/mobile/mine/traffic-pool/list/api.ts +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/list/api.ts @@ -24,3 +24,8 @@ export async function getPackage(params: { }): Promise { return request("/v1/traffic/pool/getPackage", params, "GET"); } + +// 删除数据包 +export async function deletePackage(id: number): Promise<{ success: boolean }> { + return request("/v1/traffic/pool/deletePackage", { id }, "POST"); +} diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/list/index.module.scss b/Cunkebao/src/pages/mobile/mine/traffic-pool/list/index.module.scss index c0ff2a62..4d493629 100644 --- a/Cunkebao/src/pages/mobile/mine/traffic-pool/list/index.module.scss +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/list/index.module.scss @@ -14,10 +14,33 @@ } .cardBody { - padding: 16px; + padding: 16px 16px 16px 16px; display: flex; align-items: flex-start; gap: 12px; + position: relative; +} + +/* 三点菜单按钮 */ +.menuButton { + position: absolute; + right: 8px; + top: 8px; + z-index: 10; + background: rgba(255, 255, 255, 0.9); + border: 1px solid #f0f0f0; + border-radius: 4px; + padding: 4px 8px; + font-size: 14px; + color: #666; + cursor: pointer; + transition: all 0.2s; + + &:hover { + background: #fff; + border-color: #d9d9d9; + color: #333; + } } /* 左侧图片区域 */ diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/list/index.tsx b/Cunkebao/src/pages/mobile/mine/traffic-pool/list/index.tsx index a4b5af1d..aa92817a 100644 --- a/Cunkebao/src/pages/mobile/mine/traffic-pool/list/index.tsx +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/list/index.tsx @@ -4,13 +4,14 @@ import { SearchOutlined, ReloadOutlined, PlusOutlined, + MoreOutlined, } from "@ant-design/icons"; -import { Input, Button, Pagination, Card } from "antd"; +import { Input, Button, Pagination, Dropdown, message } from "antd"; import styles from "./index.module.scss"; import { Empty } from "antd-mobile"; import { useNavigate } from "react-router-dom"; import NavCommon from "@/components/NavCommon"; -import { getPackage } from "./api"; +import { getPackage, deletePackage } from "./api"; import type { Package, PackageList } from "./api"; // 分组图标映射 @@ -81,6 +82,19 @@ const TrafficPoolList: React.FC = () => { fetchData(); }; + const handleDelete = async (id: number, name: string) => { + try { + // eslint-disable-next-line no-alert + if (!confirm(`确认删除数据包“${name}”吗?`)) return; + await deletePackage(id); + message.success("已删除"); + handleRefresh(); + } catch (e) { + console.error(e); + message.error("删除失败"); + } + }; + useEffect(() => { const fetchData = async () => { setLoading(true); @@ -117,8 +131,7 @@ const TrafficPoolList: React.FC = () => { size="small" icon={} onClick={() => { - // 新建分组逻辑 - console.log("新建分组"); + navigate("/mine/traffic-pool/create"); }} > 新建分组 @@ -164,64 +177,94 @@ const TrafficPoolList: React.FC = () => { ) : (
{list.map(item => ( -
{ - navigate(`/mine/traffic-pool/info/${item.id}`); - }} - > +
- {/* 左侧图片区域(优先展示 pic,缺省时使用假头像) */}
e.stopPropagation()} > - {item.pic ? ( - {item.name} - ) : ( - - {getGroupIcon(item.type, item.name)} - - )} + + navigate( + `/mine/traffic-pool/userList/${item.id}`, + ), + }, + { + key: "delete", + danger: true, + label: "删除数据包", + onClick: () => handleDelete(item.id, item.name), + }, + ], + }} + trigger={["click"]} + > + +
- {/* 右侧仅展示选中字段 */} -
- {/* 标题与人数 */} -
-
{item.name}
-
共{item.num}人
+
+ navigate(`/mine/traffic-pool/userList/${item.id}`) + } + > + {/* 左侧图片区域(优先展示 pic,缺省时使用假头像) */} +
+ {item.pic ? ( + {item.name} + ) : ( + + {getGroupIcon(item.type, item.name)} + + )}
- {/* RFM 汇总 */} -
- RFM:{item.RFM} - - R:{item.R} F:{item.F} M:{item.M} - -
+ {/* 右侧仅展示选中字段 */} +
+ {/* 标题与人数 */} +
+
{item.name}
+
共{item.num}人
+
- {/* 类型与创建时间 */} -
- - 类型: {item.type === 0 ? "自定义" : "系统分组"} - - 创建:{item.createTime} + {/* RFM 汇总 */} +
+ RFM:{item.RFM} + + R:{item.R} F:{item.F} M:{item.M} + +
+ + {/* 类型与创建时间 */} +
+ + 类型: {item.type === 0 ? "自定义" : "系统分组"} + + 创建:{item.createTime || "-"} +
diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/userList/api.ts b/Cunkebao/src/pages/mobile/mine/traffic-pool/userList/api.ts new file mode 100644 index 00000000..7d4efe7d --- /dev/null +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/userList/api.ts @@ -0,0 +1,11 @@ +import request from "@/api/request"; + +// 获取流量包用户列表 +export function fetchTrafficPoolList(params: { + page?: number; + pageSize?: number; + keyword?: string; + packageId?: string; +}) { + return request("/v1/traffic/pool/users", params, "GET"); +} diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/info/data.ts b/Cunkebao/src/pages/mobile/mine/traffic-pool/userList/data.ts similarity index 54% rename from Cunkebao/src/pages/mobile/mine/traffic-pool/info/data.ts rename to Cunkebao/src/pages/mobile/mine/traffic-pool/userList/data.ts index 65ad7f55..204547fa 100644 --- a/Cunkebao/src/pages/mobile/mine/traffic-pool/info/data.ts +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/userList/data.ts @@ -25,27 +25,3 @@ export interface TrafficPoolUserListResponse { page: number; pageSize: number; } - -// 设备类型 -export interface DeviceOption { - id: string; - name: string; -} - -// 分组类型 -export interface PackageOption { - id: string; - name: string; -} - -// 用户价值类型 -export type ValueLevel = "all" | "high" | "medium" | "low"; - -// 状态类型 -export type UserStatus = "all" | "added" | "pending" | "failed" | "duplicate"; - -// 获客场景类型 -export interface ScenarioOption { - id: string; - name: string; -} diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/userList/index.module.scss b/Cunkebao/src/pages/mobile/mine/traffic-pool/userList/index.module.scss new file mode 100644 index 00000000..68ecdca1 --- /dev/null +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/userList/index.module.scss @@ -0,0 +1,40 @@ +.listWrap { + padding: 16px; +} + +.cardWrap { + margin-bottom: 12px; +} + +.card { + background: #fff; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); + overflow: hidden; + transition: all 0.2s; + + &:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + } +} + +.cardContent { + display: flex; + align-items: flex-start; + padding: 16px; + gap: 12px; +} + +.title { + font-size: 16px; + font-weight: 600; + color: #333; + margin-bottom: 8px; +} + +.desc { + font-size: 14px; + color: #666; + margin-bottom: 4px; + line-height: 1.4; +} diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/info/index.tsx b/Cunkebao/src/pages/mobile/mine/traffic-pool/userList/index.tsx similarity index 63% rename from Cunkebao/src/pages/mobile/mine/traffic-pool/info/index.tsx rename to Cunkebao/src/pages/mobile/mine/traffic-pool/userList/index.tsx index 16d2abe7..823ccf65 100644 --- a/Cunkebao/src/pages/mobile/mine/traffic-pool/info/index.tsx +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/userList/index.tsx @@ -1,19 +1,22 @@ -import React, { useEffect, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; +import { useParams, useNavigate } from "react-router-dom"; import Layout from "@/components/Layout/Layout"; import { SearchOutlined, ReloadOutlined } from "@ant-design/icons"; import { Input, Button, Pagination } from "antd"; import styles from "./index.module.scss"; import { Empty, Avatar } from "antd-mobile"; -import { useNavigate } from "react-router-dom"; import NavCommon from "@/components/NavCommon"; import { fetchTrafficPoolList } from "./api"; import type { TrafficPoolUser } from "./data"; + const defaultAvatar = "https://cdn.jsdelivr.net/gh/maokaka/static/avatar-default.png"; -const TrafficPoolList: React.FC = () => { +const TrafficPoolUserList: React.FC = () => { + const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); + // 基础状态 const [loading, setLoading] = useState(false); const [list, setList] = useState([]); const [page, setPage] = useState(1); @@ -21,53 +24,80 @@ const TrafficPoolList: React.FC = () => { const [total, setTotal] = useState(0); const [search, setSearch] = useState(""); - const handleSearch = (value: string) => { - setSearch(value); - setPage(1); + // 获取列表 + const getList = async (customParams?: any) => { + setLoading(true); + try { + const params: any = { + page, + pageSize, + keyword: search, + packageId: id, // 根据流量包ID筛选用户 + ...customParams, // 允许传入自定义参数覆盖 + }; + + const res = await fetchTrafficPoolList(params); + setList(res.list || []); + setTotal(res.total || 0); + } catch (error) { + // 忽略请求过于频繁的错误,避免页面崩溃 + if (error !== "请求过于频繁,请稍后再试") { + console.error("获取列表失败:", error); + } + } finally { + setLoading(false); + } }; + // 搜索防抖处理 + const [searchInput, setSearchInput] = useState(search); + + const debouncedSearch = useCallback(() => { + const timer = setTimeout(() => { + setSearch(searchInput); + // 搜索时重置到第一页并请求列表 + setPage(1); + getList({ keyword: searchInput, page: 1 }); + }, 500); // 500ms 防抖延迟 + + return () => clearTimeout(timer); + }, [searchInput]); + useEffect(() => { - const fetchData = async () => { - setLoading(true); - try { - const params = { - page, - pageSize, - keyword: search, - }; + const cleanup = debouncedSearch(); + return cleanup; + }, [debouncedSearch]); - const res = await fetchTrafficPoolList(params); - setList(res.list || []); - setTotal(res.total || 0); - } catch (error) { - console.error("获取列表失败:", error); - } finally { - setLoading(false); - } - }; + const handSearch = (value: string) => { + setSearchInput(value); + debouncedSearch(); + }; - fetchData(); - }, [page, pageSize, search]); + // 初始加载和参数变化时重新获取数据 + useEffect(() => { + getList(); + }, [page, pageSize, search, id]); return ( - + + {/* 搜索栏 */}
handleSearch(e.target.value)} + value={searchInput} + onChange={e => handSearch(e.target.value)} prefix={} allowClear size="large" />
} >
{list.length === 0 && !loading ? ( - + ) : (
{list.map(item => ( @@ -139,4 +172,4 @@ const TrafficPoolList: React.FC = () => { ); }; -export default TrafficPoolList; +export default TrafficPoolUserList; diff --git a/Cunkebao/src/router/module/mine.tsx b/Cunkebao/src/router/module/mine.tsx index bc53e19d..9346afa6 100644 --- a/Cunkebao/src/router/module/mine.tsx +++ b/Cunkebao/src/router/module/mine.tsx @@ -2,8 +2,9 @@ import Mine from "@/pages/mobile/mine/main/index"; import Devices from "@/pages/mobile/mine/devices/index"; import DeviceDetail from "@/pages/mobile/mine/devices/DeviceDetail"; import TrafficPool from "@/pages/mobile/mine/traffic-pool/list/index"; -import TrafficPoolItem from "@/pages/mobile/mine/traffic-pool/info/index"; -import TrafficPoolDetail from "@/pages/mobile/mine/traffic-pool/detail/index"; +import TrafficPool2 from "@/pages/mobile/mine/traffic-pool/poolList1/index"; +import TrafficPoolUserList from "@/pages/mobile/mine/traffic-pool/userList/index"; +import CreateTrafficPackage from "@/pages/mobile/mine/traffic-pool/form/index"; import WechatAccounts from "@/pages/mobile/mine/wechat-accounts/list/index"; import WechatAccountDetail from "@/pages/mobile/mine/wechat-accounts/detail/index"; import Recharge from "@/pages/mobile/mine/recharge/index"; @@ -13,7 +14,6 @@ import SecuritySetting from "@/pages/mobile/mine/setting/SecuritySetting"; import About from "@/pages/mobile/mine/setting/About"; import Privacy from "@/pages/mobile/mine/setting/Privacy"; import UserSetting from "@/pages/mobile/mine/setting/UserSetting"; - const routes = [ { path: "/mine", @@ -36,16 +36,20 @@ const routes = [ element: , auth: true, }, - //流量池详情页面 { - path: "/mine/traffic-pool/info/:id", - element: , + path: "/mine/traffic-pool/list2", + element: , auth: true, }, - //流量池列表详情页面 + //新建流量包页面 { - path: "/mine/traffic-pool/detail/:wxid/:userId", - element: , + path: "/mine/traffic-pool/create", + element: , + auth: true, + }, + { + path: "/mine/traffic-pool/userList/:id", + element: , auth: true, },