From 67e95fb698edc5732c10f83a87a6c03b3bd63e69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=AC=94=E8=AE=B0=E6=9C=AC=E9=87=8C=E7=9A=84=E6=B0=B8?= =?UTF-8?q?=E5=B9=B3?= Date: Thu, 24 Jul 2025 15:07:06 +0800 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20=E6=9C=AC=E6=AC=A1=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E6=9B=B4=E6=96=B0=E5=86=85=E5=AE=B9=E5=A6=82=E4=B8=8B?= =?UTF-8?q?=20=E5=BC=80=E5=A7=8B=E5=81=9A=E7=AD=9B=E9=80=89=E5=92=8C?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=88=86=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/mine/traffic-pool/TrafficPool.tsx | 10 - .../mine/traffic-pool/TrafficPoolDetail.tsx | 8 - .../src/pages/mine/traffic-pool/list/api.ts | 30 ++ .../src/pages/mine/traffic-pool/list/data.ts | 45 ++ .../mine/traffic-pool/list/index.module.scss | 50 ++ .../pages/mine/traffic-pool/list/index.tsx | 442 +++++++++++++++++- 6 files changed, 564 insertions(+), 21 deletions(-) delete mode 100644 nkebao/src/pages/mine/traffic-pool/TrafficPool.tsx delete mode 100644 nkebao/src/pages/mine/traffic-pool/TrafficPoolDetail.tsx diff --git a/nkebao/src/pages/mine/traffic-pool/TrafficPool.tsx b/nkebao/src/pages/mine/traffic-pool/TrafficPool.tsx deleted file mode 100644 index d64714d2..00000000 --- a/nkebao/src/pages/mine/traffic-pool/TrafficPool.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from "react"; -import PlaceholderPage from "@/components/PlaceholderPage"; - -const TrafficPool: React.FC = () => { - return ( - - ); -}; - -export default TrafficPool; diff --git a/nkebao/src/pages/mine/traffic-pool/TrafficPoolDetail.tsx b/nkebao/src/pages/mine/traffic-pool/TrafficPoolDetail.tsx deleted file mode 100644 index d4c1ceef..00000000 --- a/nkebao/src/pages/mine/traffic-pool/TrafficPoolDetail.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import React from "react"; -import PlaceholderPage from "@/components/PlaceholderPage"; - -const TrafficPoolDetail: React.FC = () => { - return ; -}; - -export default TrafficPoolDetail; diff --git a/nkebao/src/pages/mine/traffic-pool/list/api.ts b/nkebao/src/pages/mine/traffic-pool/list/api.ts index e69de29b..71c2e5c2 100644 --- a/nkebao/src/pages/mine/traffic-pool/list/api.ts +++ b/nkebao/src/pages/mine/traffic-pool/list/api.ts @@ -0,0 +1,30 @@ +import request from "@/api/request"; +import type { TrafficPoolListResponse } from "./data"; + +// 获取流量池列表 +export function fetchTrafficPoolList(params: { + page?: number; + pageSize?: number; + keyword?: string; +}) { + return request("/v1/traffic/pool", params, "GET"); +} + +// 获取设备列表(如无真实接口可用mock) +export async function fetchDeviceOptions(): Promise { + // TODO: 替换为真实接口 + return [ + { id: "device-1", name: "设备1" }, + { id: "device-2", name: "设备2" }, + { id: "device-3", name: "设备3" }, + ]; +} + +// 获取分组列表(如无真实接口可用mock) +export async function fetchPackageOptions(): Promise { + // TODO: 替换为真实接口 + return [ + { id: "pkg-1", name: "高价值客户池" }, + { id: "pkg-2", name: "测试流量池" }, + ]; +} diff --git a/nkebao/src/pages/mine/traffic-pool/list/data.ts b/nkebao/src/pages/mine/traffic-pool/list/data.ts index e69de29b..17e8face 100644 --- a/nkebao/src/pages/mine/traffic-pool/list/data.ts +++ b/nkebao/src/pages/mine/traffic-pool/list/data.ts @@ -0,0 +1,45 @@ +// 流量池用户类型 +export interface TrafficPoolUser { + id: number; + identifier: string; + mobile: string; + wechatId: string; + fromd: string; + status: number; + createTime: string; + companyId: number; + sourceId: string; + type: number; + nickname: string; + avatar: string; + gender: number; + phone: string; + packages: string[]; + tags: string[]; +} + +// 列表响应类型 +export interface TrafficPoolUserListResponse { + list: TrafficPoolUser[]; + total: number; + 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"; diff --git a/nkebao/src/pages/mine/traffic-pool/list/index.module.scss b/nkebao/src/pages/mine/traffic-pool/list/index.module.scss index e69de29b..f37ab2a7 100644 --- a/nkebao/src/pages/mine/traffic-pool/list/index.module.scss +++ b/nkebao/src/pages/mine/traffic-pool/list/index.module.scss @@ -0,0 +1,50 @@ +.listWrap { + padding: 12px; +} + +.card { + background: #fff; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0,0,0,0.04); + padding: 16px; + 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/nkebao/src/pages/mine/traffic-pool/list/index.tsx b/nkebao/src/pages/mine/traffic-pool/list/index.tsx index 94006840..3f8bf42c 100644 --- a/nkebao/src/pages/mine/traffic-pool/list/index.tsx +++ b/nkebao/src/pages/mine/traffic-pool/list/index.tsx @@ -1,3 +1,439 @@ -export default function TrafficPoolList() { - return
TrafficPoolList
; -} +import React, { useEffect, useState } from "react"; +import Layout from "@/components/Layout/Layout"; +import { SearchOutlined, ReloadOutlined } from "@ant-design/icons"; +import { Input } from "antd"; +import { fetchTrafficPoolList } from "./api"; +import type { TrafficPoolUser } from "./data"; +import styles from "./index.module.scss"; +import { + List, + Empty, + Avatar, + Button, + Modal, + Selector, + Toast, + Card, +} from "antd-mobile"; +import { fetchDeviceOptions, fetchPackageOptions } from "./api"; +import type { + DeviceOption, + PackageOption, + ValueLevel, + UserStatus, +} from "./data"; +import { useNavigate } from "react-router-dom"; +import NavCommon from "@/components/NavCommon"; + +const defaultAvatar = + "https://cdn.jsdelivr.net/gh/maokaka/static/avatar-default.png"; + +const valueLevelOptions = [ + { label: "全部", value: "all" }, + { label: "高价值", value: "high" }, + { label: "中价值", value: "medium" }, + { label: "低价值", value: "low" }, +]; +const statusOptions = [ + { label: "全部", value: "all" }, + { label: "已添加", value: "added" }, + { label: "待添加", value: "pending" }, + { label: "添加失败", value: "failed" }, + { label: "重复", value: "duplicate" }, +]; + +const TrafficPoolList: React.FC = () => { + const [loading, setLoading] = useState(false); + const [list, setList] = useState([]); + const [page, setPage] = useState(1); + const [pageSize] = useState(10); + const [total, setTotal] = useState(0); + const [search, setSearch] = useState(""); + const [showFilter, setShowFilter] = useState(false); + const [deviceOptions, setDeviceOptions] = useState([]); + const [packageOptions, setPackageOptions] = useState([]); + const [deviceId, setDeviceId] = useState("all"); + const [packageId, setPackageId] = useState("all"); + const [valueLevel, setValueLevel] = useState("all"); + const [userStatus, setUserStatus] = useState("all"); + const [selectedIds, setSelectedIds] = useState([]); + const [batchModal, setBatchModal] = useState(false); + const [batchTarget, setBatchTarget] = useState(""); + const [showStats, setShowStats] = useState(false); + const navigate = useNavigate(); + + // 统计数据 + const stats = React.useMemo(() => { + const total = list.length; + const highValue = list.filter((u) => + u.tags.includes("高价值客户池") + ).length; + const added = list.filter((u) => u.status === 1).length; + const pending = list.filter((u) => u.status === 0).length; + const failed = list.filter((u) => u.status === -1).length; + const addSuccessRate = total ? Math.round((added / total) * 100) : 0; + return { total, highValue, added, pending, failed, addSuccessRate }; + }, [list]); + + const getList = async () => { + setLoading(true); + try { + const res = await fetchTrafficPoolList({ + page, + pageSize, + keyword: search, + }); + setList(res.list || []); + setTotal(res.total || 0); + } finally { + setLoading(false); + } + }; + + // 获取筛选项 + useEffect(() => { + fetchDeviceOptions().then(setDeviceOptions); + fetchPackageOptions().then(setPackageOptions); + }, []); + + // 筛选条件变化时刷新列表 + useEffect(() => { + getList(); + // eslint-disable-next-line + }, [page, search, deviceId, packageId, valueLevel, userStatus]); + + // 全选/反选 + const handleSelectAll = (checked: boolean) => { + if (checked) { + setSelectedIds(list.map((item) => item.id)); + } else { + setSelectedIds([]); + } + }; + // 单选 + const handleSelect = (id: number, checked: boolean) => { + setSelectedIds((prev) => + checked ? [...prev, id] : prev.filter((i) => i !== id) + ); + }; + + // 批量加入分组/流量池 + const handleBatchAdd = () => { + if (!batchTarget) { + Toast.show({ content: "请选择目标分组", position: "top" }); + return; + } + // TODO: 调用后端批量接口,这里仅模拟 + Toast.show({ + content: `已将${selectedIds.length}个用户加入${packageOptions.find((p) => p.id === batchTarget)?.name || ""}`, + position: "top", + }); + setBatchModal(false); + setSelectedIds([]); + setBatchTarget(""); + // 可刷新列表 + }; + + // 性别icon + const renderGender = (gender: number) => { + if (gender === 1) + return ; + if (gender === 2) + return ; + return ?; + }; + + return ( + + setShowStats((s) => !s)} + style={{ marginLeft: 8 }} + > + {showStats ? "收起分析" : "数据分析"} + + } + /> + {/* 搜索栏 */} +
+
+ setSearch(e.target.value)} + prefix={} + allowClear + size="large" + /> +
+ +
+ + } + > + {/* 数据分析面板 */} + {showStats && ( +
+
+ +
+ {stats.total} +
+
总用户数
+
+ +
+ {stats.highValue} +
+
高价值用户
+
+
+
+ +
+ {stats.addSuccessRate}% +
+
添加成功率
+
+ +
+ {stats.added} +
+
已添加
+
+ +
+ {stats.pending} +
+
待添加
+
+ +
+ {stats.failed} +
+
添加失败
+
+
+
+ )} + {/* 批量操作栏 */} +
+ 0} + onChange={(e) => handleSelectAll(e.target.checked)} + style={{ marginRight: 8 }} + /> + 全选 + {selectedIds.length > 0 && ( + <> + {`已选${selectedIds.length}项`} + + + )} + +
+ {/* 批量加入分组弹窗 */} + setBatchModal(false)} + footer={[ + { text: "取消", onClick: () => setBatchModal(false) }, + { text: "确定", onClick: handleBatchAdd }, + ]} + > +
+
选择目标分组
+ ({ + label: p.name, + value: p.id, + }))} + value={[batchTarget]} + onChange={(v) => setBatchTarget(v[0])} + /> +
+
+ 将选中的{selectedIds.length}个用户加入所选分组 +
+
+ {/* 筛选弹窗 */} + setShowFilter(false)} + footer={[ + { + text: "重置", + onClick: () => { + setDeviceId("all"); + setPackageId("all"); + setValueLevel("all"); + setUserStatus("all"); + }, + }, + { text: "确定", onClick: () => setShowFilter(false) }, + ]} + > +
+
设备
+ ({ label: d.name, value: d.id })), + ]} + value={[deviceId]} + onChange={(v) => setDeviceId(v[0])} + /> +
+
+
分组
+ ({ label: p.name, value: p.id })), + ]} + value={[packageId]} + onChange={(v) => setPackageId(v[0])} + /> +
+
+
用户价值
+ setValueLevel(v[0] as ValueLevel)} + /> +
+
+
状态
+ setUserStatus(v[0] as UserStatus)} + /> +
+
+
+ {list.length === 0 && !loading ? ( + + ) : ( + + {list.map((item) => ( + +
+ navigate(`/mine/traffic-pool/detail/${item.id}`) + } + > +
+ handleSelect(item.id, e.target.checked)} + style={{ marginRight: 8 }} + onClick={(e) => e.stopPropagation()} + /> + +
+
+ {item.nickname || item.identifier} + {renderGender(item.gender)} +
+
+ 微信号:{item.wechatId || "-"} +
+
+ 来源:{item.fromd || "-"} +
+
+ 分组: + {item.packages && item.packages.length + ? item.packages.join(",") + : "-"} +
+
+ 创建时间:{item.createTime} +
+
+
+
+
+ ))} +
+ )} +
+ {/* 分页 */} + {total > pageSize && ( +
+ + + {page} / {Math.ceil(total / pageSize)} + + +
+ )} +
+ ); +}; + +export default TrafficPoolList; From 19a7cd687e226c18c6c670eda918b164ad307f51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=AC=94=E8=AE=B0=E6=9C=AC=E9=87=8C=E7=9A=84=E6=B0=B8?= =?UTF-8?q?=E5=B9=B3?= Date: Thu, 24 Jul 2025 16:14:35 +0800 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20=E6=9C=AC=E6=AC=A1=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E6=9B=B4=E6=96=B0=E5=86=85=E5=AE=B9=E5=A6=82=E4=B8=8B?= =?UTF-8?q?=20Ai=E5=8A=A9=E6=89=8B=E7=95=8C=E9=9D=A2=E6=9E=84=E5=BB=BA?= =?UTF-8?q?=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ai-assistant/AIAssistant.module.scss | 139 ++++++++++ .../workspace/ai-assistant/AIAssistant.tsx | 262 +++++++++++++++++- 2 files changed, 398 insertions(+), 3 deletions(-) create mode 100644 nkebao/src/pages/workspace/ai-assistant/AIAssistant.module.scss diff --git a/nkebao/src/pages/workspace/ai-assistant/AIAssistant.module.scss b/nkebao/src/pages/workspace/ai-assistant/AIAssistant.module.scss new file mode 100644 index 00000000..d5a7cd33 --- /dev/null +++ b/nkebao/src/pages/workspace/ai-assistant/AIAssistant.module.scss @@ -0,0 +1,139 @@ +.chatContainer { + display: flex; + flex-direction: column; +} + +.messageList { + flex: 1; + overflow-y: auto; + padding: 16px 12px 80px 12px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.userMessage { + display: flex; + flex-direction: column; + align-items: flex-end; +} + +.aiMessage { + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.bubble { + max-width: 80%; + padding: 10px 14px; + border-radius: 18px; + font-size: 15px; + line-height: 1.6; + word-break: break-word; + background: #fff; + color: #222; + box-shadow: 0 2px 8px rgba(0,0,0,0.04); +} + +.userMessage .bubble { + background: linear-gradient(135deg, #a7e0ff 0%, #5bbcff 100%); + color: #222; + border-bottom-right-radius: 6px; +} + +.aiMessage .bubble { + background: #fff; + color: #222; + border-bottom-left-radius: 6px; +} + +.time { + font-size: 11px; + color: #aaa; + margin: 4px 8px 0 8px; + align-self: flex-end; +} + +.inputBar { + position: fixed; + left: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + background: #fff; + padding: 10px 12px 10px 12px; + box-shadow: 0 -2px 8px rgba(0,0,0,0.04); + z-index: 10; +} + +.input { + flex: 1; + border: none; + outline: none; + background: #f3f4f6; + border-radius: 18px; + padding: 10px 14px; + font-size: 15px; + margin-right: 8px; +} + +.sendButton { + background: var(--primary-gradient, linear-gradient(135deg, #a7e0ff 0%, #5bbcff 100%)); + color: #fff; + border: none; + border-radius: 18px; + padding: 8px 18px; + font-size: 15px; + font-weight: 600; + cursor: pointer; + transition: background 0.2s; +} + +.sendButton:disabled { + background: #e5e7eb; + color: #aaa; + cursor: not-allowed; +} + +.iconBtn { + background: none; + border: none; + outline: none; + margin-right: 6px; + font-size: 20px; + color: #888; + cursor: pointer; + padding: 4px; + border-radius: 50%; + transition: background 0.2s, color 0.2s; +} + +.iconBtn:hover, .iconBtn:active { + background: #f3f4f6; + color: #5bbcff; +} + +.image { + max-width: 180px; + max-height: 180px; + border-radius: 10px; + display: block; +} + +.fileLink { + color: #5bbcff; + text-decoration: none; + font-size: 15px; + word-break: break-all; + display: flex; + align-items: center; +} + +.nav-title { + color: var(--primary-color); + font-weight: 700; + font-size: 18px; + text-shadow: 0 2px 4px rgba(24, 142, 238, 0.2); +} \ No newline at end of file diff --git a/nkebao/src/pages/workspace/ai-assistant/AIAssistant.tsx b/nkebao/src/pages/workspace/ai-assistant/AIAssistant.tsx index 2727f453..4e60367e 100644 --- a/nkebao/src/pages/workspace/ai-assistant/AIAssistant.tsx +++ b/nkebao/src/pages/workspace/ai-assistant/AIAssistant.tsx @@ -1,8 +1,264 @@ -import React from "react"; -import PlaceholderPage from "@/components/PlaceholderPage"; +import React, { useRef, useState, useEffect } from "react"; +import Layout from "@/components/Layout/Layout"; +import NavCommon from "@/components/NavCommon"; +import { + PictureOutlined, + PaperClipOutlined, + AudioOutlined, +} from "@ant-design/icons"; +import styles from "./AIAssistant.module.scss"; + +interface Message { + id: string; + content: string; + from: "user" | "ai"; + time: string; + type?: "text" | "image" | "file" | "audio"; + fileName?: string; + fileUrl?: string; +} + +const initialMessages: Message[] = [ + { + id: "1", + content: "你好!我是你的AI助手,有什么可以帮助你的吗?", + from: "ai", + time: "15:29", + type: "text", + }, +]; const AIAssistant: React.FC = () => { - return ; + const [messages, setMessages] = useState(initialMessages); + const [input, setInput] = useState(""); + const [loading, setLoading] = useState(false); + const messagesEndRef = useRef(null); + const fileInputRef = useRef(null); + const imageInputRef = useRef(null); + const [recognizing, setRecognizing] = useState(false); + const recognitionRef = useRef(null); + + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [messages]); + + // 语音识别初始化 + useEffect(() => { + if (!("webkitSpeechRecognition" in window)) return; + const SpeechRecognition = (window as any).webkitSpeechRecognition; + recognitionRef.current = new SpeechRecognition(); + recognitionRef.current.continuous = false; + recognitionRef.current.interimResults = false; + recognitionRef.current.lang = "zh-CN"; + recognitionRef.current.onresult = (event: any) => { + const transcript = event.results[0][0].transcript; + setInput((prev) => prev + transcript); + setRecognizing(false); + }; + recognitionRef.current.onerror = () => setRecognizing(false); + recognitionRef.current.onend = () => setRecognizing(false); + }, []); + + const handleSend = async () => { + if (!input.trim()) return; + const userMsg: Message = { + id: Date.now().toString(), + content: input, + from: "user", + time: new Date().toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + }), + type: "text", + }; + setMessages((prev) => [...prev, userMsg]); + setInput(""); + setLoading(true); + setTimeout(() => { + setMessages((prev) => [ + ...prev, + { + id: Date.now().toString() + "-ai", + content: "AI正在思考...(此处可接入真实API)", + from: "ai", + time: new Date().toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + }), + type: "text", + }, + ]); + setLoading(false); + }, 1200); + }; + + // 图片上传 + const handleImageChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + const url = URL.createObjectURL(file); + setMessages((prev) => [ + ...prev, + { + id: Date.now().toString(), + content: url, + from: "user", + time: new Date().toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + }), + type: "image", + fileName: file.name, + fileUrl: url, + }, + ]); + } + e.target.value = ""; + }; + + // 文件上传 + const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + const url = URL.createObjectURL(file); + setMessages((prev) => [ + ...prev, + { + id: Date.now().toString(), + content: file.name, + from: "user", + time: new Date().toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + }), + type: "file", + fileName: file.name, + fileUrl: url, + }, + ]); + } + e.target.value = ""; + }; + + // 语音输入 + const handleVoiceInput = () => { + if (!recognitionRef.current) return alert("当前浏览器不支持语音输入"); + if (recognizing) { + recognitionRef.current.stop(); + setRecognizing(false); + } else { + recognitionRef.current.start(); + setRecognizing(true); + } + }; + + return ( + } loading={false}> +
+
+ {messages.map((msg) => ( +
+ {msg.type === "text" && ( +
{msg.content}
+ )} + {msg.type === "image" && ( +
+ {msg.fileName} +
+ )} + {msg.type === "file" && ( + + )} + {/* 语音消息可后续扩展 */} +
{msg.time}
+
+ ))} + {loading && ( +
+
AI正在输入...
+
+ )} +
+
+
+ + + + + + setInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") handleSend(); + }} + disabled={loading} + /> + +
+
+ + ); }; export default AIAssistant; From aadc6d9a9e14a4f47b4a3699fa66f247ab2c4d28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=AC=94=E8=AE=B0=E6=9C=AC=E9=87=8C=E7=9A=84=E6=B0=B8?= =?UTF-8?q?=E5=B9=B3?= Date: Thu, 24 Jul 2025 17:35:10 +0800 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20=E6=9C=AC=E6=AC=A1=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E6=9B=B4=E6=96=B0=E5=86=85=E5=AE=B9=E5=A6=82=E4=B8=8B?= =?UTF-8?q?=20Ai=E5=88=86=E6=9E=90=E6=95=B0=E6=8D=AE=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E6=9E=84=E5=BB=BA=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workspace/ai-analyzer/index.module.scss | 96 ++++++++++++ .../src/pages/workspace/ai-analyzer/index.tsx | 141 ++++++++++++++++++ nkebao/src/router/module/workspace.tsx | 3 +- 3 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 nkebao/src/pages/workspace/ai-analyzer/index.module.scss create mode 100644 nkebao/src/pages/workspace/ai-analyzer/index.tsx diff --git a/nkebao/src/pages/workspace/ai-analyzer/index.module.scss b/nkebao/src/pages/workspace/ai-analyzer/index.module.scss new file mode 100644 index 00000000..1fe010a2 --- /dev/null +++ b/nkebao/src/pages/workspace/ai-analyzer/index.module.scss @@ -0,0 +1,96 @@ +.analyzerPage { + + +} + +.tabs { + background: #fff; + padding: 0 12px; + border-radius: 0 0 12px 12px; + margin-bottom: 8px; +} + +.planList { + display: flex; + flex-direction: column; + gap: 16px; + padding: 0 12px 16px 12px; +} + +.planCard { + background: #fff; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0,0,0,0.06); + padding: 16px 14px 12px 14px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.cardHeader { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 6px; +} + +.cardTitle { + font-size: 16px; + font-weight: 700; + color: #222; +} + +.statusDone { + background: #e6f9e6; + color: #22c55e; + font-size: 12px; + border-radius: 8px; + padding: 2px 10px; + font-weight: 600; +} + +.statusDoing { + background: #e0f2fe; + color: #1677ff; + font-size: 12px; + border-radius: 8px; + padding: 2px 10px; + font-weight: 600; +} + +.cardInfo { + font-size: 13px; + color: #444; + display: flex; + flex-direction: column; + gap: 4px; +} + +.label { + color: #888; + font-size: 12px; + margin-right: 2px; +} + +.keyword { + display: inline-block; + background: #f3f4f6; + color: #1677ff; + border-radius: 6px; + padding: 2px 8px; + font-size: 12px; + margin-right: 6px; + margin-bottom: 2px; +} + +.cardActions { + display: flex; + gap: 10px; + margin-top: 8px; +} + +.actionBtn { + border-radius: 6px !important; + font-size: 13px !important; + padding: 0 12px !important; +} \ No newline at end of file diff --git a/nkebao/src/pages/workspace/ai-analyzer/index.tsx b/nkebao/src/pages/workspace/ai-analyzer/index.tsx new file mode 100644 index 00000000..09224bd6 --- /dev/null +++ b/nkebao/src/pages/workspace/ai-analyzer/index.tsx @@ -0,0 +1,141 @@ +import React, { useState } from "react"; +import NavCommon from "@/components/NavCommon"; +import Layout from "@/components/Layout/Layout"; +import { Tabs } from "antd-mobile"; +import { Button } from "antd"; +import styles from "./index.module.scss"; +import { PlusOutlined } from "@ant-design/icons"; + +const mockPlans = [ + { + id: "1", + title: "美妆用户分析", + status: "done", + device: "设备1", + wechat: "wxid_abc123", + type: "综合分析", + keywords: ["美妆", "护肤", "彩妆"], + createTime: "2023/12/15 18:30:00", + finishTime: "2023/12/15 19:45:00", + }, + { + id: "2", + title: "健身爱好者分析", + status: "doing", + device: "设备2", + wechat: "wxid_fit456", + type: "好友信息分析", + keywords: ["健身", "运动", "健康"], + createTime: "2023/12/16 17:15:00", + finishTime: "", + }, +]; + +const statusMap = { + all: "全部计划", + doing: "进行中", + done: "已完成", +}; + +const statusTag = { + done: 已完成, + doing: 分析中, +}; + +const AiAnalyzer: React.FC = () => { + const [tab, setTab] = useState<"all" | "doing" | "done">("all"); + + const filteredPlans = + tab === "all" ? mockPlans : mockPlans.filter((p) => p.status === tab); + + return ( + + 新建计划 + + } + /> + } + > +
+ setTab(key as any)} + className={styles.tabs} + > + + + + +
+ {filteredPlans.map((plan) => ( +
+
+ {plan.title} + {statusTag[plan.status as "done" | "doing"]} +
+
+
+ 设备: + {plan.device} | 微信号: {plan.wechat} +
+
+ 分析类型: + {plan.type} +
+
+ 关键词: + {plan.keywords.map((k) => ( + + {k} + + ))} +
+
+ 创建时间: + {plan.createTime} +
+ {plan.status === "done" && ( +
+ 完成时间: + {plan.finishTime} +
+ )} +
+
+ {plan.status === "done" ? ( + <> + + + + ) : ( + + )} +
+
+ ))} +
+
+
+ ); +}; + +export default AiAnalyzer; diff --git a/nkebao/src/router/module/workspace.tsx b/nkebao/src/router/module/workspace.tsx index ee4ca0c5..036d2811 100644 --- a/nkebao/src/router/module/workspace.tsx +++ b/nkebao/src/router/module/workspace.tsx @@ -16,6 +16,7 @@ import TrafficDistribution from "@/pages/workspace/traffic-distribution/TrafficD import TrafficDistributionDetail from "@/pages/workspace/traffic-distribution/Detail"; import NewDistribution from "@/pages/workspace/traffic-distribution/NewDistribution"; import PlaceholderPage from "@/components/PlaceholderPage"; +import AiAnalyzer from "@/pages/workspace/ai-analyzer"; const workspaceRoutes = [ { @@ -116,7 +117,7 @@ const workspaceRoutes = [ // AI数据分析 { path: "/workspace/ai-analyzer", - element: , + element: , auth: true, }, // AI策略优化 From 02049ed91d3a302c173efeef56111de65eecb363 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=AC=94=E8=AE=B0=E6=9C=AC=E9=87=8C=E7=9A=84=E6=B0=B8?= =?UTF-8?q?=E5=B9=B3?= Date: Thu, 24 Jul 2025 20:30:40 +0800 Subject: [PATCH 4/6] =?UTF-8?q?feat:=20=E6=9C=AC=E6=AC=A1=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E6=9B=B4=E6=96=B0=E5=86=85=E5=AE=B9=E5=A6=82=E4=B8=8B?= =?UTF-8?q?=20=E6=B5=81=E9=87=8F=E6=B1=A0=E6=A0=B7=E5=BC=8F=E5=AE=8C?= =?UTF-8?q?=E6=88=90=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nkebao/.env.development | 3 +- nkebao/src/api/request.ts | 2 +- .../src/pages/content/list/index.module.scss | 11 +- .../content/materials/list/index.module.scss | 10 - .../mine/traffic-pool/list/BatchAddModal.tsx | 47 ++ .../traffic-pool/list/DataAnalysisPanel.tsx | 84 +++ .../mine/traffic-pool/list/FilterModal.tsx | 118 +++ .../src/pages/mine/traffic-pool/list/api.ts | 61 +- .../pages/mine/traffic-pool/list/dataAnyx.tsx | 160 ++++ .../mine/traffic-pool/list/index.module.scss | 103 +-- .../pages/mine/traffic-pool/list/index.tsx | 699 +++++++----------- .../scenarios/plan/list/index.module.scss | 6 - .../src/pages/scenarios/plan/list/index.tsx | 2 +- 13 files changed, 758 insertions(+), 548 deletions(-) create mode 100644 nkebao/src/pages/mine/traffic-pool/list/BatchAddModal.tsx create mode 100644 nkebao/src/pages/mine/traffic-pool/list/DataAnalysisPanel.tsx create mode 100644 nkebao/src/pages/mine/traffic-pool/list/FilterModal.tsx create mode 100644 nkebao/src/pages/mine/traffic-pool/list/dataAnyx.tsx diff --git a/nkebao/.env.development b/nkebao/.env.development index da6a111b..05c62ec4 100644 --- a/nkebao/.env.development +++ b/nkebao/.env.development @@ -1,4 +1,5 @@ # 基础环境变量示例 -VITE_API_BASE_URL=http://www.yishi.com +# VITE_API_BASE_URL=http://www.yishi.com +VITE_API_BASE_URL=https://ckbapi.quwanzhi.com VITE_APP_TITLE=Nkebao Base diff --git a/nkebao/src/api/request.ts b/nkebao/src/api/request.ts index b52caa9c..1e57fac9 100644 --- a/nkebao/src/api/request.ts +++ b/nkebao/src/api/request.ts @@ -11,7 +11,7 @@ const debounceMap = new Map(); const instance: AxiosInstance = axios.create({ baseURL: (import.meta as any).env?.VITE_API_BASE_URL || "/api", - timeout: 10000, + timeout: 20000, headers: { "Content-Type": "application/json", }, diff --git a/nkebao/src/pages/content/list/index.module.scss b/nkebao/src/pages/content/list/index.module.scss index d7600b53..101d4e19 100644 --- a/nkebao/src/pages/content/list/index.module.scss +++ b/nkebao/src/pages/content/list/index.module.scss @@ -40,16 +40,7 @@ } } -.refresh-btn { - border-radius: 20px; - border: 1px solid #e0e0e0; - background: white; - - &:hover { - border-color: #1677ff; - color: #1677ff; - } -} + .create-btn { border-radius: 20px; diff --git a/nkebao/src/pages/content/materials/list/index.module.scss b/nkebao/src/pages/content/materials/list/index.module.scss index bebcd3f4..3abd8d22 100644 --- a/nkebao/src/pages/content/materials/list/index.module.scss +++ b/nkebao/src/pages/content/materials/list/index.module.scss @@ -40,16 +40,6 @@ } } -.refresh-btn { - border-radius: 20px; - border: 1px solid #e0e0e0; - background: white; - - &:hover { - border-color: #1677ff; - color: #1677ff; - } -} .create-btn { border-radius: 20px; diff --git a/nkebao/src/pages/mine/traffic-pool/list/BatchAddModal.tsx b/nkebao/src/pages/mine/traffic-pool/list/BatchAddModal.tsx new file mode 100644 index 00000000..6fa79bfc --- /dev/null +++ b/nkebao/src/pages/mine/traffic-pool/list/BatchAddModal.tsx @@ -0,0 +1,47 @@ +import React from "react"; +import { Modal, Selector } from "antd-mobile"; +import type { PackageOption } from "./data"; + +interface BatchAddModalProps { + visible: boolean; + onClose: () => void; + packageOptions: PackageOption[]; + batchTarget: string; + setBatchTarget: (v: string) => void; + selectedCount: number; + onConfirm: () => void; +} + +const BatchAddModal: React.FC = ({ + visible, + onClose, + packageOptions, + batchTarget, + setBatchTarget, + selectedCount, + onConfirm, +}) => ( + +
+
选择目标分组
+ ({ label: p.name, value: p.id }))} + value={[batchTarget]} + onChange={(v) => setBatchTarget(v[0])} + /> +
+
+ 将选中的{selectedCount}个用户加入所选分组 +
+
+); + +export default BatchAddModal; diff --git a/nkebao/src/pages/mine/traffic-pool/list/DataAnalysisPanel.tsx b/nkebao/src/pages/mine/traffic-pool/list/DataAnalysisPanel.tsx new file mode 100644 index 00000000..9ba4fead --- /dev/null +++ b/nkebao/src/pages/mine/traffic-pool/list/DataAnalysisPanel.tsx @@ -0,0 +1,84 @@ +import React from "react"; +import { Card, Button } from "antd-mobile"; + +interface DataAnalysisPanelProps { + stats: { + total: number; + highValue: number; + added: number; + pending: number; + failed: number; + addSuccessRate: number; + }; + showStats: boolean; + setShowStats: (v: boolean) => void; +} + +const DataAnalysisPanel: React.FC = ({ + stats, + showStats, + setShowStats, +}) => { + if (!showStats) return null; + return ( +
+
+ +
+ {stats.total} +
+
总用户数
+
+ +
+ {stats.highValue} +
+
高价值用户
+
+
+
+ +
+ {stats.addSuccessRate}% +
+
添加成功率
+
+ +
+ {stats.added} +
+
已添加
+
+ +
+ {stats.pending} +
+
待添加
+
+ +
+ {stats.failed} +
+
添加失败
+
+
+ +
+ ); +}; + +export default DataAnalysisPanel; diff --git a/nkebao/src/pages/mine/traffic-pool/list/FilterModal.tsx b/nkebao/src/pages/mine/traffic-pool/list/FilterModal.tsx new file mode 100644 index 00000000..ab4badc4 --- /dev/null +++ b/nkebao/src/pages/mine/traffic-pool/list/FilterModal.tsx @@ -0,0 +1,118 @@ +import React from "react"; +import { Popup } from "antd-mobile"; +import { Select, Button } from "antd"; +import type { + DeviceOption, + PackageOption, + ValueLevel, + UserStatus, +} from "./data"; + +interface FilterModalProps { + visible: boolean; + onClose: () => void; + deviceOptions: DeviceOption[]; + packageOptions: PackageOption[]; + deviceId: string; + setDeviceId: (v: string) => void; + packageId: string; + setPackageId: (v: string) => void; + valueLevel: ValueLevel; + setValueLevel: (v: ValueLevel) => void; + userStatus: UserStatus; + setUserStatus: (v: UserStatus) => void; + onReset: () => void; +} + +const valueLevelOptions = [ + { label: "全部价值", value: "all" }, + { label: "高价值", value: "high" }, + { label: "中价值", value: "medium" }, + { label: "低价值", value: "low" }, +]; +const statusOptions = [ + { label: "全部状态", value: "all" }, + { label: "已添加", value: "added" }, + { label: "待添加", value: "pending" }, + { label: "添加失败", value: "failed" }, + { label: "重复", value: "duplicate" }, +]; + +const FilterModal: React.FC = ({ + visible, + onClose, + deviceOptions, + packageOptions, + deviceId, + setDeviceId, + packageId, + setPackageId, + valueLevel, + setValueLevel, + userStatus, + setUserStatus, + onReset, +}) => ( + +
+ 筛选选项 +
+
+
设备
+ ({ label: p.name, value: p.id })), + ]} + /> +
+
+
用户价值
+ setUserStatus(v as UserStatus)} + options={statusOptions} + /> +
+
+ + +
+
+); + +export default FilterModal; diff --git a/nkebao/src/pages/mine/traffic-pool/list/api.ts b/nkebao/src/pages/mine/traffic-pool/list/api.ts index 71c2e5c2..b5b9bc36 100644 --- a/nkebao/src/pages/mine/traffic-pool/list/api.ts +++ b/nkebao/src/pages/mine/traffic-pool/list/api.ts @@ -1,30 +1,31 @@ -import request from "@/api/request"; -import type { TrafficPoolListResponse } from "./data"; - -// 获取流量池列表 -export function fetchTrafficPoolList(params: { - page?: number; - pageSize?: number; - keyword?: string; -}) { - return request("/v1/traffic/pool", params, "GET"); -} - -// 获取设备列表(如无真实接口可用mock) -export async function fetchDeviceOptions(): Promise { - // TODO: 替换为真实接口 - return [ - { id: "device-1", name: "设备1" }, - { id: "device-2", name: "设备2" }, - { id: "device-3", name: "设备3" }, - ]; -} - -// 获取分组列表(如无真实接口可用mock) -export async function fetchPackageOptions(): Promise { - // TODO: 替换为真实接口 - return [ - { id: "pkg-1", name: "高价值客户池" }, - { id: "pkg-2", name: "测试流量池" }, - ]; -} +import request from "@/api/request"; +import type { TrafficPoolListResponse, DeviceOption } from "./data"; +import { fetchDeviceList } from "@/api/devices"; + +// 获取流量池列表 +export function fetchTrafficPoolList(params: { + page?: number; + pageSize?: number; + keyword?: string; +}) { + return request("/v1/traffic/pool", params, "GET"); +} + +// 获取设备列表(真实接口) +export async function fetchDeviceOptions(): Promise { + const res = await fetchDeviceList({ page: 1, limit: 100 }); + // 假设返回 { list: [{ id, name, ... }], ... } + return (res.list || []).map((item: any) => ({ + id: String(item.id), + name: item.name, + })); +} + +// 获取分组列表(如无真实接口可用mock) +export async function fetchPackageOptions(): Promise { + // TODO: 替换为真实接口 + return [ + { id: "pkg-1", name: "高价值客户池" }, + { id: "pkg-2", name: "测试流量池" }, + ]; +} diff --git a/nkebao/src/pages/mine/traffic-pool/list/dataAnyx.tsx b/nkebao/src/pages/mine/traffic-pool/list/dataAnyx.tsx new file mode 100644 index 00000000..2bf1ad85 --- /dev/null +++ b/nkebao/src/pages/mine/traffic-pool/list/dataAnyx.tsx @@ -0,0 +1,160 @@ +import { useState, useEffect, useMemo } from "react"; +import { + fetchTrafficPoolList, + fetchDeviceOptions, + fetchPackageOptions, +} from "./api"; +import type { + TrafficPoolUser, + DeviceOption, + PackageOption, + ValueLevel, + UserStatus, +} from "./data"; +import { Toast } from "antd-mobile"; + +export function useTrafficPoolListLogic() { + const [loading, setLoading] = useState(false); + const [list, setList] = useState([]); + const [page, setPage] = useState(1); + const [pageSize] = useState(10); + const [total, setTotal] = useState(0); + const [search, setSearch] = useState(""); + + // 筛选相关 + const [showFilter, setShowFilter] = useState(false); + const [deviceOptions, setDeviceOptions] = useState([]); + const [packageOptions, setPackageOptions] = useState([]); + const [deviceId, setDeviceId] = useState("all"); + const [packageId, setPackageId] = useState("all"); + const [valueLevel, setValueLevel] = useState("all"); + const [userStatus, setUserStatus] = useState("all"); + + // 批量相关 + const [selectedIds, setSelectedIds] = useState([]); + const [batchModal, setBatchModal] = useState(false); + const [batchTarget, setBatchTarget] = useState(""); + + // 数据分析 + const [showStats, setShowStats] = useState(false); + const stats = useMemo(() => { + const total = list.length; + const highValue = list.filter((u) => + u.tags.includes("高价值客户池") + ).length; + const added = list.filter((u) => u.status === 1).length; + const pending = list.filter((u) => u.status === 0).length; + const failed = list.filter((u) => u.status === -1).length; + const addSuccessRate = total ? Math.round((added / total) * 100) : 0; + return { total, highValue, added, pending, failed, addSuccessRate }; + }, [list]); + + // 获取列表 + const getList = async () => { + setLoading(true); + try { + const res = await fetchTrafficPoolList({ + page, + pageSize, + keyword: search, + // deviceId, + // packageId, + // valueLevel, + // userStatus, + }); + setList(res.list || []); + setTotal(res.total || 0); + } finally { + setLoading(false); + } + }; + + // 获取筛选项 + useEffect(() => { + fetchDeviceOptions().then(setDeviceOptions); + fetchPackageOptions().then(setPackageOptions); + }, []); + + // 筛选条件变化时刷新列表 + useEffect(() => { + getList(); + // eslint-disable-next-line + }, [page, search /*, deviceId, packageId, valueLevel, userStatus*/]); + + // 全选/反选 + const handleSelectAll = (checked: boolean) => { + if (checked) { + setSelectedIds(list.map((item) => item.id)); + } else { + setSelectedIds([]); + } + }; + // 单选 + const handleSelect = (id: number, checked: boolean) => { + setSelectedIds((prev) => + checked ? [...prev, id] : prev.filter((i) => i !== id) + ); + }; + + // 批量加入分组/流量池 + const handleBatchAdd = () => { + if (!batchTarget) { + Toast.show({ content: "请选择目标分组", position: "top" }); + return; + } + // TODO: 调用后端批量接口,这里仅模拟 + Toast.show({ + content: `已将${selectedIds.length}个用户加入${packageOptions.find((p) => p.id === batchTarget)?.name || ""}`, + position: "top", + }); + setBatchModal(false); + setSelectedIds([]); + setBatchTarget(""); + // 可刷新列表 + }; + + // 筛选重置 + const resetFilter = () => { + setDeviceId("all"); + setPackageId("all"); + setValueLevel("all"); + setUserStatus("all"); + }; + + return { + loading, + list, + page, + setPage, + pageSize, + total, + search, + setSearch, + showFilter, + setShowFilter, + deviceOptions, + packageOptions, + deviceId, + setDeviceId, + packageId, + setPackageId, + valueLevel, + setValueLevel, + userStatus, + setUserStatus, + selectedIds, + setSelectedIds, + handleSelectAll, + handleSelect, + batchModal, + setBatchModal, + batchTarget, + setBatchTarget, + handleBatchAdd, + showStats, + setShowStats, + stats, + getList, + resetFilter, + }; +} diff --git a/nkebao/src/pages/mine/traffic-pool/list/index.module.scss b/nkebao/src/pages/mine/traffic-pool/list/index.module.scss index f37ab2a7..275503da 100644 --- a/nkebao/src/pages/mine/traffic-pool/list/index.module.scss +++ b/nkebao/src/pages/mine/traffic-pool/list/index.module.scss @@ -1,50 +1,53 @@ -.listWrap { - padding: 12px; -} - -.card { - background: #fff; - border-radius: 8px; - box-shadow: 0 2px 8px rgba(0,0,0,0.04); - padding: 16px; - 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; -} +.listWrap { +padding: 12px; +} +.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/nkebao/src/pages/mine/traffic-pool/list/index.tsx b/nkebao/src/pages/mine/traffic-pool/list/index.tsx index 3f8bf42c..aee21e7e 100644 --- a/nkebao/src/pages/mine/traffic-pool/list/index.tsx +++ b/nkebao/src/pages/mine/traffic-pool/list/index.tsx @@ -1,439 +1,260 @@ -import React, { useEffect, useState } from "react"; -import Layout from "@/components/Layout/Layout"; -import { SearchOutlined, ReloadOutlined } from "@ant-design/icons"; -import { Input } from "antd"; -import { fetchTrafficPoolList } from "./api"; -import type { TrafficPoolUser } from "./data"; -import styles from "./index.module.scss"; -import { - List, - Empty, - Avatar, - Button, - Modal, - Selector, - Toast, - Card, -} from "antd-mobile"; -import { fetchDeviceOptions, fetchPackageOptions } from "./api"; -import type { - DeviceOption, - PackageOption, - ValueLevel, - UserStatus, -} from "./data"; -import { useNavigate } from "react-router-dom"; -import NavCommon from "@/components/NavCommon"; - -const defaultAvatar = - "https://cdn.jsdelivr.net/gh/maokaka/static/avatar-default.png"; - -const valueLevelOptions = [ - { label: "全部", value: "all" }, - { label: "高价值", value: "high" }, - { label: "中价值", value: "medium" }, - { label: "低价值", value: "low" }, -]; -const statusOptions = [ - { label: "全部", value: "all" }, - { label: "已添加", value: "added" }, - { label: "待添加", value: "pending" }, - { label: "添加失败", value: "failed" }, - { label: "重复", value: "duplicate" }, -]; - -const TrafficPoolList: React.FC = () => { - const [loading, setLoading] = useState(false); - const [list, setList] = useState([]); - const [page, setPage] = useState(1); - const [pageSize] = useState(10); - const [total, setTotal] = useState(0); - const [search, setSearch] = useState(""); - const [showFilter, setShowFilter] = useState(false); - const [deviceOptions, setDeviceOptions] = useState([]); - const [packageOptions, setPackageOptions] = useState([]); - const [deviceId, setDeviceId] = useState("all"); - const [packageId, setPackageId] = useState("all"); - const [valueLevel, setValueLevel] = useState("all"); - const [userStatus, setUserStatus] = useState("all"); - const [selectedIds, setSelectedIds] = useState([]); - const [batchModal, setBatchModal] = useState(false); - const [batchTarget, setBatchTarget] = useState(""); - const [showStats, setShowStats] = useState(false); - const navigate = useNavigate(); - - // 统计数据 - const stats = React.useMemo(() => { - const total = list.length; - const highValue = list.filter((u) => - u.tags.includes("高价值客户池") - ).length; - const added = list.filter((u) => u.status === 1).length; - const pending = list.filter((u) => u.status === 0).length; - const failed = list.filter((u) => u.status === -1).length; - const addSuccessRate = total ? Math.round((added / total) * 100) : 0; - return { total, highValue, added, pending, failed, addSuccessRate }; - }, [list]); - - const getList = async () => { - setLoading(true); - try { - const res = await fetchTrafficPoolList({ - page, - pageSize, - keyword: search, - }); - setList(res.list || []); - setTotal(res.total || 0); - } finally { - setLoading(false); - } - }; - - // 获取筛选项 - useEffect(() => { - fetchDeviceOptions().then(setDeviceOptions); - fetchPackageOptions().then(setPackageOptions); - }, []); - - // 筛选条件变化时刷新列表 - useEffect(() => { - getList(); - // eslint-disable-next-line - }, [page, search, deviceId, packageId, valueLevel, userStatus]); - - // 全选/反选 - const handleSelectAll = (checked: boolean) => { - if (checked) { - setSelectedIds(list.map((item) => item.id)); - } else { - setSelectedIds([]); - } - }; - // 单选 - const handleSelect = (id: number, checked: boolean) => { - setSelectedIds((prev) => - checked ? [...prev, id] : prev.filter((i) => i !== id) - ); - }; - - // 批量加入分组/流量池 - const handleBatchAdd = () => { - if (!batchTarget) { - Toast.show({ content: "请选择目标分组", position: "top" }); - return; - } - // TODO: 调用后端批量接口,这里仅模拟 - Toast.show({ - content: `已将${selectedIds.length}个用户加入${packageOptions.find((p) => p.id === batchTarget)?.name || ""}`, - position: "top", - }); - setBatchModal(false); - setSelectedIds([]); - setBatchTarget(""); - // 可刷新列表 - }; - - // 性别icon - const renderGender = (gender: number) => { - if (gender === 1) - return ; - if (gender === 2) - return ; - return ?; - }; - - return ( - - setShowStats((s) => !s)} - style={{ marginLeft: 8 }} - > - {showStats ? "收起分析" : "数据分析"} - - } - /> - {/* 搜索栏 */} -
-
- setSearch(e.target.value)} - prefix={} - allowClear - size="large" - /> -
- -
- - } - > - {/* 数据分析面板 */} - {showStats && ( -
-
- -
- {stats.total} -
-
总用户数
-
- -
- {stats.highValue} -
-
高价值用户
-
-
-
- -
- {stats.addSuccessRate}% -
-
添加成功率
-
- -
- {stats.added} -
-
已添加
-
- -
- {stats.pending} -
-
待添加
-
- -
- {stats.failed} -
-
添加失败
-
-
-
- )} - {/* 批量操作栏 */} -
- 0} - onChange={(e) => handleSelectAll(e.target.checked)} - style={{ marginRight: 8 }} - /> - 全选 - {selectedIds.length > 0 && ( - <> - {`已选${selectedIds.length}项`} - - - )} - -
- {/* 批量加入分组弹窗 */} - setBatchModal(false)} - footer={[ - { text: "取消", onClick: () => setBatchModal(false) }, - { text: "确定", onClick: handleBatchAdd }, - ]} - > -
-
选择目标分组
- ({ - label: p.name, - value: p.id, - }))} - value={[batchTarget]} - onChange={(v) => setBatchTarget(v[0])} - /> -
-
- 将选中的{selectedIds.length}个用户加入所选分组 -
-
- {/* 筛选弹窗 */} - setShowFilter(false)} - footer={[ - { - text: "重置", - onClick: () => { - setDeviceId("all"); - setPackageId("all"); - setValueLevel("all"); - setUserStatus("all"); - }, - }, - { text: "确定", onClick: () => setShowFilter(false) }, - ]} - > -
-
设备
- ({ label: d.name, value: d.id })), - ]} - value={[deviceId]} - onChange={(v) => setDeviceId(v[0])} - /> -
-
-
分组
- ({ label: p.name, value: p.id })), - ]} - value={[packageId]} - onChange={(v) => setPackageId(v[0])} - /> -
-
-
用户价值
- setValueLevel(v[0] as ValueLevel)} - /> -
-
-
状态
- setUserStatus(v[0] as UserStatus)} - /> -
-
-
- {list.length === 0 && !loading ? ( - - ) : ( - - {list.map((item) => ( - -
- navigate(`/mine/traffic-pool/detail/${item.id}`) - } - > -
- handleSelect(item.id, e.target.checked)} - style={{ marginRight: 8 }} - onClick={(e) => e.stopPropagation()} - /> - -
-
- {item.nickname || item.identifier} - {renderGender(item.gender)} -
-
- 微信号:{item.wechatId || "-"} -
-
- 来源:{item.fromd || "-"} -
-
- 分组: - {item.packages && item.packages.length - ? item.packages.join(",") - : "-"} -
-
- 创建时间:{item.createTime} -
-
-
-
-
- ))} -
- )} -
- {/* 分页 */} - {total > pageSize && ( -
- - - {page} / {Math.ceil(total / pageSize)} - - -
- )} -
- ); -}; - -export default TrafficPoolList; +import React from "react"; +import Layout from "@/components/Layout/Layout"; +import { + SearchOutlined, + ReloadOutlined, + BarChartOutlined, +} from "@ant-design/icons"; +import { Input, Button, Checkbox } from "antd"; +import styles from "./index.module.scss"; +import { List, Empty, Avatar, Modal, Selector, Toast, Card } from "antd-mobile"; +import { useNavigate } from "react-router-dom"; +import NavCommon from "@/components/NavCommon"; +import { useTrafficPoolListLogic } from "./dataAnyx"; +import DataAnalysisPanel from "./DataAnalysisPanel"; +import FilterModal from "./FilterModal"; +import BatchAddModal from "./BatchAddModal"; + +const defaultAvatar = + "https://cdn.jsdelivr.net/gh/maokaka/static/avatar-default.png"; + +const valueLevelOptions = [ + { label: "全部", value: "all" }, + { label: "高价值", value: "high" }, + { label: "中价值", value: "medium" }, + { label: "低价值", value: "low" }, +]; +const statusOptions = [ + { label: "全部", value: "all" }, + { label: "已添加", value: "added" }, + { label: "待添加", value: "pending" }, + { label: "添加失败", value: "failed" }, + { label: "重复", value: "duplicate" }, +]; + +const TrafficPoolList: React.FC = () => { + const navigate = useNavigate(); + const { + loading, + list, + page, + setPage, + pageSize, + total, + search, + setSearch, + showFilter, + setShowFilter, + deviceOptions, + packageOptions, + deviceId, + setDeviceId, + packageId, + setPackageId, + valueLevel, + setValueLevel, + userStatus, + setUserStatus, + selectedIds, + handleSelectAll, + handleSelect, + batchModal, + setBatchModal, + batchTarget, + setBatchTarget, + handleBatchAdd, + showStats, + setShowStats, + stats, + getList, + resetFilter, + } = useTrafficPoolListLogic(); + + return ( + + setShowStats((s) => !s)} + style={{ marginLeft: 8 }} + > + {showStats ? "收起分析" : "数据分析"} + + } + /> + {/* 搜索栏 */} +
+
+ setSearch(e.target.value)} + prefix={} + allowClear + size="large" + /> +
+ +
+ {/* 数据分析面板 */} + + + {/* 批量操作栏 */} +
+ 0} + onChange={(e) => handleSelectAll(e.target.checked)} + style={{ marginRight: 8 }} + /> + 全选 + {selectedIds.length > 0 && ( + <> + {`已选${selectedIds.length}项`} + + + )} +
+ +
+ + } + > + {/* 批量加入分组弹窗 */} + setBatchModal(false)} + packageOptions={packageOptions} + batchTarget={batchTarget} + setBatchTarget={setBatchTarget} + selectedCount={selectedIds.length} + onConfirm={handleBatchAdd} + /> + {/* 筛选弹窗 */} + setShowFilter(false)} + deviceOptions={deviceOptions} + packageOptions={packageOptions} + deviceId={deviceId} + setDeviceId={setDeviceId} + packageId={packageId} + setPackageId={setPackageId} + valueLevel={valueLevel} + setValueLevel={setValueLevel} + userStatus={userStatus} + setUserStatus={setUserStatus} + onReset={resetFilter} + /> +
+ {list.length === 0 && !loading ? ( + + ) : ( +
+ {list.map((item) => ( +
+
+ navigate(`/mine/traffic-pool/detail/${item.id}`) + } + > +
+ handleSelect(item.id, e.target.checked)} + style={{ marginRight: 8 }} + onClick={(e) => e.stopPropagation()} + /> + +
+
+ {item.nickname || item.identifier} + {/* 性别icon可自行封装 */} +
+
+ 微信号:{item.wechatId || "-"} +
+
+ 来源:{item.fromd || "-"} +
+
+ 分组: + {item.packages && item.packages.length + ? item.packages.join(",") + : "-"} +
+
+ 创建时间:{item.createTime} +
+
+
+
+
+ ))} +
+ )} +
+ {/* 分页 */} + {total > pageSize && ( +
+ + + {page} / {Math.ceil(total / pageSize)} + + +
+ )} + + ); +}; + +export default TrafficPoolList; diff --git a/nkebao/src/pages/scenarios/plan/list/index.module.scss b/nkebao/src/pages/scenarios/plan/list/index.module.scss index 4df03dc4..cfd64be7 100644 --- a/nkebao/src/pages/scenarios/plan/list/index.module.scss +++ b/nkebao/src/pages/scenarios/plan/list/index.module.scss @@ -33,12 +33,6 @@ } } -.refresh-btn { - height: 40px; - width: 40px; - padding: 0; - border-radius: 8px; -} .plan-list { display: flex; diff --git a/nkebao/src/pages/scenarios/plan/list/index.tsx b/nkebao/src/pages/scenarios/plan/list/index.tsx index 82daa1dd..552f7347 100644 --- a/nkebao/src/pages/scenarios/plan/list/index.tsx +++ b/nkebao/src/pages/scenarios/plan/list/index.tsx @@ -381,7 +381,7 @@ const ScenarioList: React.FC = () => { size="small" onClick={handleRefresh} loading={loadingTasks} - className={style["refresh-btn"]} + className="refresh-btn" > From d46ff6e7773acbd72ff220d148e64dc5b4882d95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=AC=94=E8=AE=B0=E6=9C=AC=E9=87=8C=E7=9A=84=E6=B0=B8?= =?UTF-8?q?=E5=B9=B3?= Date: Thu, 24 Jul 2025 20:35:08 +0800 Subject: [PATCH 5/6] =?UTF-8?q?feat:=20=E6=9C=AC=E6=AC=A1=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E6=9B=B4=E6=96=B0=E5=86=85=E5=AE=B9=E5=A6=82=E4=B8=8B?= =?UTF-8?q?=20=E5=88=97=E8=A1=A8=E9=80=89=E9=A1=B9=E5=8D=A1=E6=A0=B7?= =?UTF-8?q?=E5=BC=8F=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/mine/traffic-pool/detail/data.ts | 32 +++++++++++++++++++ .../mine/traffic-pool/list/index.module.scss | 12 +++++++ .../pages/mine/traffic-pool/list/index.tsx | 7 ++-- 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/nkebao/src/pages/mine/traffic-pool/detail/data.ts b/nkebao/src/pages/mine/traffic-pool/detail/data.ts index e69de29b..f1c30d00 100644 --- a/nkebao/src/pages/mine/traffic-pool/detail/data.ts +++ b/nkebao/src/pages/mine/traffic-pool/detail/data.ts @@ -0,0 +1,32 @@ +// 用户详情类型 +export interface TrafficPoolUserDetail { + id: number; + nickname: string; + avatar: string; + wechatId: string; + status: number | string; + addTime: string; + lastInteraction: string; + deviceName?: string; + wechatAccountName?: string; + customerServiceName?: string; + poolNames?: string[]; + rfmScore?: { + recency: number; + frequency: number; + monetary: number; + segment?: string; + }; + totalSpent?: number; + interactionCount?: number; + conversionRate?: number; + tags?: string[]; + packages?: string[]; + interactions?: Array<{ + id: string; + type: string; + content: string; + timestamp: string; + value?: number; + }>; +} diff --git a/nkebao/src/pages/mine/traffic-pool/list/index.module.scss b/nkebao/src/pages/mine/traffic-pool/list/index.module.scss index 275503da..c37654d8 100644 --- a/nkebao/src/pages/mine/traffic-pool/list/index.module.scss +++ b/nkebao/src/pages/mine/traffic-pool/list/index.module.scss @@ -1,6 +1,18 @@ .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; diff --git a/nkebao/src/pages/mine/traffic-pool/list/index.tsx b/nkebao/src/pages/mine/traffic-pool/list/index.tsx index aee21e7e..64698a6e 100644 --- a/nkebao/src/pages/mine/traffic-pool/list/index.tsx +++ b/nkebao/src/pages/mine/traffic-pool/list/index.tsx @@ -195,18 +195,17 @@ const TrafficPoolList: React.FC = () => { navigate(`/mine/traffic-pool/detail/${item.id}`) } > -
+
handleSelect(item.id, e.target.checked)} style={{ marginRight: 8 }} onClick={(e) => e.stopPropagation()} + className={styles.checkbox} />
From fd2cf3149e4c5155a1c1f78319c24c3a3b8b335d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=AC=94=E8=AE=B0=E6=9C=AC=E9=87=8C=E7=9A=84=E6=B0=B8?= =?UTF-8?q?=E5=B9=B3?= Date: Thu, 24 Jul 2025 20:57:18 +0800 Subject: [PATCH 6/6] =?UTF-8?q?feat:=20=E6=9C=AC=E6=AC=A1=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E6=9B=B4=E6=96=B0=E5=86=85=E5=AE=B9=E5=A6=82=E4=B8=8B?= =?UTF-8?q?=20=E6=B5=81=E9=87=8F=E6=B1=A0=E5=8A=9F=E8=83=BD=E7=BC=BA?= =?UTF-8?q?=E5=A4=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/mine/traffic-pool/detail/api.ts | 2 +- .../pages/mine/traffic-pool/detail/index.tsx | 303 +++++++++++++++++- .../pages/mine/traffic-pool/list/index.tsx | 4 +- nkebao/src/router/module/mine.tsx | 2 +- 4 files changed, 303 insertions(+), 8 deletions(-) diff --git a/nkebao/src/pages/mine/traffic-pool/detail/api.ts b/nkebao/src/pages/mine/traffic-pool/detail/api.ts index 6b6377fe..13e3d535 100644 --- a/nkebao/src/pages/mine/traffic-pool/detail/api.ts +++ b/nkebao/src/pages/mine/traffic-pool/detail/api.ts @@ -1,5 +1,5 @@ import request from "@/api/request"; export function getTrafficPoolDetail(id: string): Promise { - return request("/v1/traffic/pool/detail", { id }, "GET"); + return request("/v1/workbench/detail", { id }, "GET"); } diff --git a/nkebao/src/pages/mine/traffic-pool/detail/index.tsx b/nkebao/src/pages/mine/traffic-pool/detail/index.tsx index bc908842..6a3b983c 100644 --- a/nkebao/src/pages/mine/traffic-pool/detail/index.tsx +++ b/nkebao/src/pages/mine/traffic-pool/detail/index.tsx @@ -1,3 +1,300 @@ -export default function TrafficPoolDetail() { - return
TrafficPoolDetail
; -} +import React, { useEffect, useState } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import Layout from "@/components/Layout/Layout"; +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: "用户标签" }, +]; + +const TrafficPoolDetail: React.FC = () => { + const { id } = useParams(); + const navigate = useNavigate(); + const [loading, setLoading] = useState(true); + const [user, setUser] = useState(null); + const [activeTab, setActiveTab] = useState<"base" | "journey" | "tags">( + "base" + ); + + useEffect(() => { + if (!id) return; + setLoading(true); + getTrafficPoolDetail(id as string) + .then((res) => setUser(res)) + .finally(() => setLoading(false)); + }, [id]); + + if (loading) { + return ( + +
+ +
+
+ ); + } + if (!user) { + return ( + +
+ 未找到该用户 +
+
+ ); + } + + return ( + + +
用户详情
+
+ } + > +
+ {/* 顶部信息 */} +
+ +
+
{user.nickname}
+
+ {user.wechatId} +
+ {user.packages && + user.packages.length > 0 && + user.packages.map((pkg) => ( + + {pkg} + + ))} +
+
+ {/* Tab栏 */} +
+ {tabList.map((tab) => ( +
setActiveTab(tab.key as any)} + > + {tab.label} +
+ ))} +
+ {/* Tab内容 */} + {activeTab === "base" && ( + <> + +
+
设备:{user.deviceName || "--"}
+
微信号:{user.wechatAccountName || "--"}
+
客服:{user.customerServiceName || "--"}
+
添加时间:{user.addTime || "--"}
+
最近互动:{user.lastInteraction || "--"}
+
+
+ +
+
+
+ {user.rfmScore?.recency ?? "-"} +
+
最近性(R)
+
+
+
+ {user.rfmScore?.frequency ?? "-"} +
+
频率(F)
+
+
+
+ {user.rfmScore?.monetary ?? "-"} +
+
金额(M)
+
+
+
+ +
+
+
+ ¥{user.totalSpent ?? "-"} +
+
总消费
+
+
+
+ {user.interactionCount ?? "-"} +
+
互动次数
+
+
+
+ {user.conversionRate ?? "-"} +
+
转化率
+
+
+
+ {user.status === "failed" + ? "添加失败" + : user.status === "added" + ? "添加成功" + : "未添加"} +
+
添加状态
+
+
+
+ + )} + {activeTab === "journey" && ( + + {user.interactions && user.interactions.length > 0 ? ( + user.interactions.slice(0, 4).map((it) => ( +
+
+ {it.type === "click" && "📱"} + {it.type === "message" && "💬"} + {it.type === "purchase" && "💲"} + {it.type === "view" && "👁️"} +
+
+
+ {it.type === "click" && "点击行为"} + {it.type === "message" && "消息互动"} + {it.type === "purchase" && "购买行为"} + {it.type === "view" && "页面浏览"} +
+
+ {it.content} + {it.type === "purchase" && it.value && ( + + ¥{it.value} + + )} +
+
+
+ {it.timestamp} +
+
+ )) + ) : ( +
+ 暂无互动记录 +
+ )} +
+ )} + {activeTab === "tags" && ( + +
+ {user.tags && user.tags.length > 0 ? ( + user.tags.map((tag) => ( + + {tag} + + )) + ) : ( + 暂无标签 + )} +
+ +
+ )} +
+ + ); +}; + +export default TrafficPoolDetail; diff --git a/nkebao/src/pages/mine/traffic-pool/list/index.tsx b/nkebao/src/pages/mine/traffic-pool/list/index.tsx index 64698a6e..afa631e4 100644 --- a/nkebao/src/pages/mine/traffic-pool/list/index.tsx +++ b/nkebao/src/pages/mine/traffic-pool/list/index.tsx @@ -191,9 +191,7 @@ const TrafficPoolList: React.FC = () => {
- navigate(`/mine/traffic-pool/detail/${item.id}`) - } + onClick={() => navigate(`/traffic-pool/detail/${item.id}`)} >
, auth: true, },