From 3b0b83aae180274be6393fbd5ee90a05a1433d1b 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: Wed, 15 Oct 2025 10:16:12 +0800 Subject: [PATCH 01/19] =?UTF-8?q?=E5=88=A0=E9=99=A4=E6=B5=81=E9=87=8F?= =?UTF-8?q?=E6=B1=A0=E7=9B=B8=E5=85=B3=E7=BB=84=E4=BB=B6=EF=BC=9A=E7=A7=BB?= =?UTF-8?q?=E9=99=A4BatchAddModal=E3=80=81DataAnalysisPanel=E3=80=81Filter?= =?UTF-8?q?Modal=E5=8F=8A=E5=85=B6=E7=9B=B8=E5=85=B3=E9=80=BB=E8=BE=91?= =?UTF-8?q?=EF=BC=8C=E7=AE=80=E5=8C=96=E4=BB=A3=E7=A0=81=E7=BB=93=E6=9E=84?= =?UTF-8?q?=EF=BC=8C=E6=8F=90=E5=8D=87=E7=BB=B4=E6=8A=A4=E6=80=A7=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mobile/mine/traffic-pool/list/index.tsx | 322 ++------------ .../{list => poolList}/BatchAddModal.tsx | 0 .../{list => poolList}/DataAnalysisPanel.tsx | 0 .../{list => poolList}/FilterModal.tsx | 0 .../mobile/mine/traffic-pool/poolList/api.ts | 34 ++ .../mobile/mine/traffic-pool/poolList/data.ts | 51 +++ .../traffic-pool/poolList/index.module.scss | 65 +++ .../mine/traffic-pool/poolList/index.tsx | 396 ++++++++++++++++++ Cunkebao/src/router/module/mine.tsx | 6 + 9 files changed, 586 insertions(+), 288 deletions(-) rename Cunkebao/src/pages/mobile/mine/traffic-pool/{list => poolList}/BatchAddModal.tsx (100%) rename Cunkebao/src/pages/mobile/mine/traffic-pool/{list => poolList}/DataAnalysisPanel.tsx (100%) rename Cunkebao/src/pages/mobile/mine/traffic-pool/{list => poolList}/FilterModal.tsx (100%) create mode 100644 Cunkebao/src/pages/mobile/mine/traffic-pool/poolList/api.ts create mode 100644 Cunkebao/src/pages/mobile/mine/traffic-pool/poolList/data.ts create mode 100644 Cunkebao/src/pages/mobile/mine/traffic-pool/poolList/index.module.scss create mode 100644 Cunkebao/src/pages/mobile/mine/traffic-pool/poolList/index.tsx 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 c5c6306da..16d2abe7b 100644 --- a/Cunkebao/src/pages/mobile/mine/traffic-pool/list/index.tsx +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/list/index.tsx @@ -1,29 +1,19 @@ -import React, { useCallback, useEffect, useState } from "react"; +import React, { useEffect, useState } from "react"; import Layout from "@/components/Layout/Layout"; -import { - SearchOutlined, - ReloadOutlined, - BarChartOutlined, -} from "@ant-design/icons"; -import { Toast } from "antd-mobile"; -import { Input, Button, Checkbox, Pagination } from "antd"; +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, fetchScenarioOptions, addPackage } from "./api"; -import type { TrafficPoolUser, ScenarioOption } from "./data"; -import DataAnalysisPanel from "./DataAnalysisPanel"; -import FilterModal from "./FilterModal"; -import BatchAddModal from "./BatchAddModal"; -import { DeviceSelectionItem } from "@/components/DeviceSelection/data"; +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 navigate = useNavigate(); - // 基础状态 const [loading, setLoading] = useState(false); const [list, setList] = useState([]); const [page, setPage] = useState(1); @@ -31,251 +21,57 @@ const TrafficPoolList: React.FC = () => { const [total, setTotal] = useState(0); const [search, setSearch] = useState(""); - // 筛选相关 - const [showFilter, setShowFilter] = useState(false); - const [scenarioOptions, setScenarioOptions] = useState([]); + const handleSearch = (value: string) => { + setSearch(value); + setPage(1); + }; - // 公共筛选条件状态 - const [filterParams, setFilterParams] = useState({ - selectedDevices: [] as DeviceSelectionItem[], - packageId: 0, - scenarioId: 0, - userValue: 0, - userStatus: 0, - }); + useEffect(() => { + const fetchData = async () => { + setLoading(true); + try { + const params = { + page, + pageSize, + keyword: search, + }; - // 批量相关 - const [selectedIds, setSelectedIds] = useState([]); - const [batchModal, setBatchModal] = useState(false); - - // 数据分析 - const [showStats, setShowStats] = useState(false); - - // 获取列表 - const getList = async (customParams?: any) => { - setLoading(true); - try { - const params: any = { - page, - pageSize, - keyword: search, - packageld: filterParams.packageId, - sceneId: filterParams.scenarioId, - userValue: filterParams.userValue, - addStatus: filterParams.userStatus, - deviceld: filterParams.selectedDevices.map(d => d.id).join(), - ...customParams, // 允许传入自定义参数覆盖 - }; - - const res = await fetchTrafficPoolList(params); - setList(res.list || []); - setTotal(res.total || 0); - } catch (error) { - // 忽略请求过于频繁的错误,避免页面崩溃 - if (error !== "请求过于频繁,请稍后再试") { + const res = await fetchTrafficPoolList(params); + setList(res.list || []); + setTotal(res.total || 0); + } catch (error) { console.error("获取列表失败:", error); + } finally { + setLoading(false); } - } finally { - setLoading(false); - } - }; + }; - // 获取筛选项 - useEffect(() => { - fetchScenarioOptions().then(res => { - setScenarioOptions(res.list || []); - }); - }, []); - - // 全选/反选 - 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 = async options => { - try { - // 构建请求参数 - const params = { - type: "2", // 2选择用户 - addPackageId: options.selectedPackageId, // 目标分组ID - userIds: selectedIds.map(id => id), // 选中的用户ID数组 - // 如果有当前筛选条件,也可以传递 - ...(filterParams.packageId && { - packageId: filterParams.packageId, - }), - ...(filterParams.scenarioId && { - taskId: filterParams.scenarioId, - }), - ...(filterParams.userValue && { - userValue: filterParams.userValue, - }), - ...(filterParams.userStatus && { - addStatus: filterParams.userStatus, - }), - ...(filterParams.selectedDevices.length > 0 && { - deviceId: filterParams.selectedDevices.map(d => d.id).join(","), - }), - ...(search && { keyword: search }), - }; - - console.log("批量加入请求参数:", params); - - // 调用接口 - const result = await addPackage(params); - console.log("批量加入结果:", result); - - // 成功后刷新列表 - getList(); - - // 关闭弹窗并清空选择 - setBatchModal(false); - setSelectedIds([]); - - // 可以添加成功提示 - Toast.show({ - content: `成功将用户加入分组`, - position: "top", - }); - } catch (error) { - console.error("批量加入失败:", error); - // 可以添加错误提示 - Toast.show({ content: "批量加入失败,请重试", position: "top" }); - } - }; - - // 搜索防抖处理 - 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 cleanup = debouncedSearch(); - return cleanup; - }, [debouncedSearch]); - - const handSearch = (value: string) => { - setSearchInput(value); - setSelectedIds([]); - debouncedSearch(); - }; + fetchData(); + }, [page, pageSize, search]); return ( - setShowStats(s => !s)} - style={{ marginLeft: 8 }} - > - {showStats ? "收起分析" : "数据分析"} - - } - /> - {/* 搜索栏 */} +
handSearch(e.target.value)} + placeholder="搜索用户" + value={search} + onChange={e => handleSearch(e.target.value)} prefix={} allowClear size="large" />
-
- {/* 数据分析面板 */} - { - // 可以在这里处理统计数据,比如更新本地状态或发送到父组件 - console.log("收到统计数据:", statsData); - }} - /> - - {/* 批量操作栏 */} -
- 0} - onChange={e => handleSelectAll(e.target.checked)} - style={{ marginRight: 8 }} /> - 全选 - {selectedIds.length > 0 && ( - <> - {`已选${selectedIds.length}项`} - - - )} - {searchInput.length > 0 && ( - <> - - - )} -
-
} @@ -283,56 +79,14 @@ const TrafficPoolList: React.FC = () => {
{ - setPage(newPage); - getList({ page: newPage }); - }} + onChange={setPage} />
} > - {/* 批量加入分组弹窗 */} - setBatchModal(false)} - selectedCount={selectedIds.length} - onConfirm={data => { - // 处理批量加入逻辑 - handleBatchAdd(data); - }} - /> - {/* 筛选弹窗 */} - setShowFilter(false)} - onConfirm={filters => { - // 更新公共筛选条件状态 - const newFilterParams = { - selectedDevices: filters.selectedDevices, - packageId: filters.packageld, - scenarioId: filters.sceneId, - userValue: filters.userValue, - userStatus: filters.addStatus, - }; - - setFilterParams(newFilterParams); - // 重置到第一页并请求列表 - setPage(1); - getList({ - page: 1, - packageld: newFilterParams.packageId, - sceneId: newFilterParams.scenarioId, - userValue: newFilterParams.userValue, - addStatus: newFilterParams.userStatus, - deviceld: newFilterParams.selectedDevices.map(d => d.id).join(), - }); - }} - scenarioOptions={scenarioOptions} - initialFilters={filterParams} - />
{list.length === 0 && !loading ? ( @@ -350,13 +104,6 @@ const TrafficPoolList: React.FC = () => { } >
- handleSelect(item.id, e.target.checked)} - style={{ marginRight: 8 }} - onClick={e => e.stopPropagation()} - className={styles.checkbox} - /> {
{item.nickname || item.identifier} - {/* 性别icon可自行封装 */}
微信号:{item.wechatId || "-"} diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/list/BatchAddModal.tsx b/Cunkebao/src/pages/mobile/mine/traffic-pool/poolList/BatchAddModal.tsx similarity index 100% rename from Cunkebao/src/pages/mobile/mine/traffic-pool/list/BatchAddModal.tsx rename to Cunkebao/src/pages/mobile/mine/traffic-pool/poolList/BatchAddModal.tsx diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/list/DataAnalysisPanel.tsx b/Cunkebao/src/pages/mobile/mine/traffic-pool/poolList/DataAnalysisPanel.tsx similarity index 100% rename from Cunkebao/src/pages/mobile/mine/traffic-pool/list/DataAnalysisPanel.tsx rename to Cunkebao/src/pages/mobile/mine/traffic-pool/poolList/DataAnalysisPanel.tsx diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/list/FilterModal.tsx b/Cunkebao/src/pages/mobile/mine/traffic-pool/poolList/FilterModal.tsx similarity index 100% rename from Cunkebao/src/pages/mobile/mine/traffic-pool/list/FilterModal.tsx rename to Cunkebao/src/pages/mobile/mine/traffic-pool/poolList/FilterModal.tsx diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/poolList/api.ts b/Cunkebao/src/pages/mobile/mine/traffic-pool/poolList/api.ts new file mode 100644 index 000000000..9b53c030a --- /dev/null +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/poolList/api.ts @@ -0,0 +1,34 @@ +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/poolList/data.ts b/Cunkebao/src/pages/mobile/mine/traffic-pool/poolList/data.ts new file mode 100644 index 000000000..65ad7f559 --- /dev/null +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/poolList/data.ts @@ -0,0 +1,51 @@ +// 流量池用户类型 +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"; + +// 获客场景类型 +export interface ScenarioOption { + id: string; + name: string; +} diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/poolList/index.module.scss b/Cunkebao/src/pages/mobile/mine/traffic-pool/poolList/index.module.scss new file mode 100644 index 000000000..2fbee5e38 --- /dev/null +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/poolList/index.module.scss @@ -0,0 +1,65 @@ +.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/poolList/index.tsx b/Cunkebao/src/pages/mobile/mine/traffic-pool/poolList/index.tsx new file mode 100644 index 000000000..c5c6306da --- /dev/null +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/poolList/index.tsx @@ -0,0 +1,396 @@ +import React, { useCallback, useEffect, useState } from "react"; +import Layout from "@/components/Layout/Layout"; +import { + SearchOutlined, + ReloadOutlined, + BarChartOutlined, +} from "@ant-design/icons"; +import { Toast } from "antd-mobile"; +import { Input, Button, Checkbox, 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, fetchScenarioOptions, addPackage } from "./api"; +import type { TrafficPoolUser, ScenarioOption } from "./data"; +import DataAnalysisPanel from "./DataAnalysisPanel"; +import FilterModal from "./FilterModal"; +import BatchAddModal from "./BatchAddModal"; +import { DeviceSelectionItem } from "@/components/DeviceSelection/data"; +const defaultAvatar = + "https://cdn.jsdelivr.net/gh/maokaka/static/avatar-default.png"; + +const TrafficPoolList: React.FC = () => { + const navigate = useNavigate(); + + // 基础状态 + 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 [scenarioOptions, setScenarioOptions] = useState([]); + + // 公共筛选条件状态 + const [filterParams, setFilterParams] = useState({ + selectedDevices: [] as DeviceSelectionItem[], + packageId: 0, + scenarioId: 0, + userValue: 0, + userStatus: 0, + }); + + // 批量相关 + const [selectedIds, setSelectedIds] = useState([]); + const [batchModal, setBatchModal] = useState(false); + + // 数据分析 + const [showStats, setShowStats] = useState(false); + + // 获取列表 + const getList = async (customParams?: any) => { + setLoading(true); + try { + const params: any = { + page, + pageSize, + keyword: search, + packageld: filterParams.packageId, + sceneId: filterParams.scenarioId, + userValue: filterParams.userValue, + addStatus: filterParams.userStatus, + deviceld: filterParams.selectedDevices.map(d => d.id).join(), + ...customParams, // 允许传入自定义参数覆盖 + }; + + const res = await fetchTrafficPoolList(params); + setList(res.list || []); + setTotal(res.total || 0); + } catch (error) { + // 忽略请求过于频繁的错误,避免页面崩溃 + if (error !== "请求过于频繁,请稍后再试") { + console.error("获取列表失败:", error); + } + } finally { + setLoading(false); + } + }; + + // 获取筛选项 + useEffect(() => { + fetchScenarioOptions().then(res => { + setScenarioOptions(res.list || []); + }); + }, []); + + // 全选/反选 + 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 = async options => { + try { + // 构建请求参数 + const params = { + type: "2", // 2选择用户 + addPackageId: options.selectedPackageId, // 目标分组ID + userIds: selectedIds.map(id => id), // 选中的用户ID数组 + // 如果有当前筛选条件,也可以传递 + ...(filterParams.packageId && { + packageId: filterParams.packageId, + }), + ...(filterParams.scenarioId && { + taskId: filterParams.scenarioId, + }), + ...(filterParams.userValue && { + userValue: filterParams.userValue, + }), + ...(filterParams.userStatus && { + addStatus: filterParams.userStatus, + }), + ...(filterParams.selectedDevices.length > 0 && { + deviceId: filterParams.selectedDevices.map(d => d.id).join(","), + }), + ...(search && { keyword: search }), + }; + + console.log("批量加入请求参数:", params); + + // 调用接口 + const result = await addPackage(params); + console.log("批量加入结果:", result); + + // 成功后刷新列表 + getList(); + + // 关闭弹窗并清空选择 + setBatchModal(false); + setSelectedIds([]); + + // 可以添加成功提示 + Toast.show({ + content: `成功将用户加入分组`, + position: "top", + }); + } catch (error) { + console.error("批量加入失败:", error); + // 可以添加错误提示 + Toast.show({ content: "批量加入失败,请重试", position: "top" }); + } + }; + + // 搜索防抖处理 + 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 cleanup = debouncedSearch(); + return cleanup; + }, [debouncedSearch]); + + const handSearch = (value: string) => { + setSearchInput(value); + setSelectedIds([]); + debouncedSearch(); + }; + + return ( + + setShowStats(s => !s)} + style={{ marginLeft: 8 }} + > + {showStats ? "收起分析" : "数据分析"} + + } + /> + {/* 搜索栏 */} +
+
+ handSearch(e.target.value)} + prefix={} + allowClear + size="large" + /> +
+ +
+ {/* 数据分析面板 */} + { + // 可以在这里处理统计数据,比如更新本地状态或发送到父组件 + console.log("收到统计数据:", statsData); + }} + /> + + {/* 批量操作栏 */} +
+ 0} + onChange={e => handleSelectAll(e.target.checked)} + style={{ marginRight: 8 }} + /> + 全选 + {selectedIds.length > 0 && ( + <> + {`已选${selectedIds.length}项`} + + + )} + {searchInput.length > 0 && ( + <> + + + )} +
+ +
+ + } + footer={ +
+ { + setPage(newPage); + getList({ page: newPage }); + }} + /> +
+ } + > + {/* 批量加入分组弹窗 */} + setBatchModal(false)} + selectedCount={selectedIds.length} + onConfirm={data => { + // 处理批量加入逻辑 + handleBatchAdd(data); + }} + /> + {/* 筛选弹窗 */} + setShowFilter(false)} + onConfirm={filters => { + // 更新公共筛选条件状态 + const newFilterParams = { + selectedDevices: filters.selectedDevices, + packageId: filters.packageld, + scenarioId: filters.sceneId, + userValue: filters.userValue, + userStatus: filters.addStatus, + }; + + setFilterParams(newFilterParams); + // 重置到第一页并请求列表 + setPage(1); + getList({ + page: 1, + packageld: newFilterParams.packageId, + sceneId: newFilterParams.scenarioId, + userValue: newFilterParams.userValue, + addStatus: newFilterParams.userStatus, + deviceld: newFilterParams.selectedDevices.map(d => d.id).join(), + }); + }} + scenarioOptions={scenarioOptions} + initialFilters={filterParams} + /> +
+ {list.length === 0 && !loading ? ( + + ) : ( +
+ {list.map(item => ( +
+
+ navigate( + `/mine/traffic-pool/detail/${item.wechatId}/${item.id}`, + ) + } + > +
+ handleSelect(item.id, e.target.checked)} + style={{ marginRight: 8 }} + onClick={e => e.stopPropagation()} + className={styles.checkbox} + /> + +
+
+ {item.nickname || item.identifier} + {/* 性别icon可自行封装 */} +
+
+ 微信号:{item.wechatId || "-"} +
+
+ 来源:{item.fromd || "-"} +
+
+ 分组: + {item.packages && item.packages.length + ? item.packages.join(",") + : "-"} +
+
+ 创建时间:{item.createTime} +
+
+
+
+
+ ))} +
+ )} +
+ + ); +}; + +export default TrafficPoolList; diff --git a/Cunkebao/src/router/module/mine.tsx b/Cunkebao/src/router/module/mine.tsx index 0017d157e..e7038ddde 100644 --- a/Cunkebao/src/router/module/mine.tsx +++ b/Cunkebao/src/router/module/mine.tsx @@ -2,6 +2,7 @@ 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 TrafficPoolList from "@/pages/mobile/mine/traffic-pool/poolList/index"; import TrafficPoolDetail from "@/pages/mobile/mine/traffic-pool/detail/index"; import WechatAccounts from "@/pages/mobile/mine/wechat-accounts/list/index"; import WechatAccountDetail from "@/pages/mobile/mine/wechat-accounts/detail/index"; @@ -34,6 +35,11 @@ const routes = [ element: , auth: true, }, + { + path: "/mine/traffic-pool/list", + element: , + auth: true, + }, { path: "/mine/traffic-pool/detail/:wxid/:userId", element: , From ceffea4c5bb5d6aa51193a82244594bd130ad3f0 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: Wed, 15 Oct 2025 15:47:34 +0800 Subject: [PATCH 02/19] =?UTF-8?q?=E9=87=8D=E6=9E=84=E6=B5=81=E9=87=8F?= =?UTF-8?q?=E6=B1=A0=E6=A8=A1=E5=9D=97=EF=BC=9A=E6=9B=B4=E6=96=B0API?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=EF=BC=8C=E4=BC=98=E5=8C=96=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E7=BB=93=E6=9E=84=EF=BC=8C=E7=A7=BB=E9=99=A4=E4=B8=8D=E5=BF=85?= =?UTF-8?q?=E8=A6=81=E7=9A=84=E7=BB=84=E4=BB=B6=EF=BC=8C=E7=AE=80=E5=8C=96?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E9=80=BB=E8=BE=91=EF=BC=8C=E6=8F=90=E5=8D=87?= =?UTF-8?q?=E5=8F=AF=E7=BB=B4=E6=8A=A4=E6=80=A7=E5=92=8C=E7=94=A8=E6=88=B7?= =?UTF-8?q?=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/list/api.ts | 50 +-- .../mobile/mine/traffic-pool/list/index.tsx | 222 ++++++++-- .../mine/traffic-pool/poolList/index.tsx | 322 ++------------ .../{poolList => poolList1}/BatchAddModal.tsx | 0 .../DataAnalysisPanel.tsx | 0 .../{poolList => poolList1}/FilterModal.tsx | 0 .../mobile/mine/traffic-pool/poolList1/api.ts | 34 ++ .../mine/traffic-pool/poolList1/data.ts | 51 +++ .../traffic-pool/poolList1/index.module.scss | 65 +++ .../mine/traffic-pool/poolList1/index.tsx | 396 ++++++++++++++++++ Cunkebao/src/router/module/mine.tsx | 2 +- 11 files changed, 778 insertions(+), 364 deletions(-) rename Cunkebao/src/pages/mobile/mine/traffic-pool/{poolList => poolList1}/BatchAddModal.tsx (100%) rename Cunkebao/src/pages/mobile/mine/traffic-pool/{poolList => poolList1}/DataAnalysisPanel.tsx (100%) rename Cunkebao/src/pages/mobile/mine/traffic-pool/{poolList => poolList1}/FilterModal.tsx (100%) create mode 100644 Cunkebao/src/pages/mobile/mine/traffic-pool/poolList1/api.ts create mode 100644 Cunkebao/src/pages/mobile/mine/traffic-pool/poolList1/data.ts create mode 100644 Cunkebao/src/pages/mobile/mine/traffic-pool/poolList1/index.module.scss create mode 100644 Cunkebao/src/pages/mobile/mine/traffic-pool/poolList1/index.tsx 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 9b53c030a..98d292314 100644 --- a/Cunkebao/src/pages/mobile/mine/traffic-pool/list/api.ts +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/list/api.ts @@ -1,34 +1,26 @@ import request from "@/api/request"; -// 获取流量池列表 -export function fetchTrafficPoolList(params: { - page?: number; - pageSize?: number; - keyword?: string; -}) { - return request("/v1/traffic/pool", params, "GET"); +export interface Package { + id: number; + name: string; + description: string; + pic: string; + type: number; + createTime: string; + num: number; + R: number; + F: number; + M: number; + RFM: number; } - -export async function fetchScenarioOptions() { - return request("/v1/plan/scenes", {}, "GET"); +export interface PackageList { + list: Package[]; + total: number; } - -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"); +export async function getPackage(params: { + page: number; + pageSize: number; + keyword: string; +}): Promise { + return request("/v1/traffic/pool/getPackage", params, "GET"); } 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 16d2abe7b..c805ba752 100644 --- a/Cunkebao/src/pages/mobile/mine/traffic-pool/list/index.tsx +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/list/index.tsx @@ -1,21 +1,52 @@ import React, { useEffect, useState } from "react"; import Layout from "@/components/Layout/Layout"; -import { SearchOutlined, ReloadOutlined } from "@ant-design/icons"; -import { Input, Button, Pagination } from "antd"; +import { + SearchOutlined, + ReloadOutlined, + PlusOutlined, + RightOutlined, +} from "@ant-design/icons"; +import { Input, Button, Pagination, Card, Tag } from "antd"; import styles from "./index.module.scss"; -import { Empty, Avatar } from "antd-mobile"; +import { Empty } 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"; +import { getPackage } from "./api"; +import type { Package, PackageList } from "./api"; + +// 分组图标映射 +const getGroupIcon = (type: number, name?: string) => { + if (type === 0 && name) { + // type=0时使用分组名称首个字符 + return name.charAt(0).toUpperCase(); + } + + const icons = { + 1: "👥", // 高价值客户池 + 2: "📈", // 潜在客户池 + 3: "💬", // 高互动客户池 + 4: "⭐", // 自定义分组 + }; + return icons[type] || "👥"; +}; + +// 分组颜色映射 +const getGroupColor = (type: number) => { + const colors = { + 0: "#f0f0f0", // 灰色 - 自定义分组(使用名称首字符) + 1: "#ff4d4f", // 红色 + 2: "#1890ff", // 蓝色 + 3: "#52c41a", // 绿色 + 4: "#722ed1", // 紫色 + }; + return colors[type] || "#1890ff"; +}; const TrafficPoolList: React.FC = () => { const navigate = useNavigate(); const [loading, setLoading] = useState(false); - const [list, setList] = useState([]); + const [list, setList] = useState([]); const [page, setPage] = useState(1); const [pageSize] = useState(10); const [total, setTotal] = useState(0); @@ -36,9 +67,9 @@ const TrafficPoolList: React.FC = () => { keyword: search, }; - const res = await fetchTrafficPoolList(params); - setList(res.list || []); - setTotal(res.total || 0); + const res: PackageList = await getPackage(params); + setList(res?.list || []); + setTotal(res?.total || 0); } catch (error) { console.error("获取列表失败:", error); } finally { @@ -54,11 +85,27 @@ const TrafficPoolList: React.FC = () => { loading={loading} header={ <> - + } + onClick={() => { + // 新建分组逻辑 + console.log("新建分组"); + }} + > + 新建分组 + + } + /> +
handleSearch(e.target.value)} prefix={} @@ -89,48 +136,131 @@ const TrafficPoolList: React.FC = () => { >
{list.length === 0 && !loading ? ( - + ) : (
{list.map(item => ( -
-
- navigate( - `/mine/traffic-pool/detail/${item.wechatId}/${item.id}`, - ) - } - > -
- -
-
- {item.nickname || item.identifier} + { + // 进入分组管理 + navigate(`/mine/traffic-pool/group/${item.id}`); + }} + > +
+ {/* 分组图标 */} +
+ {getGroupIcon(item.type, item.name)} +
+ + {/* 分组信息 */} +
+
+ {item.name} +
+ +
+ {item.description} +
+ +
+ {item.num}人 +
+ + {/* RFM数据 */} + {item.RFM > 0 ? ( +
+ RFM: {item.RFM} | R:{item.R} F:{item.F} M:{item.M}
-
- 微信号:{item.wechatId || "-"} + ) : ( +
+ 暂无RFM数据
-
- 来源:{item.fromd || "-"} -
-
- 分组: - {item.packages && item.packages.length - ? item.packages.join(",") - : "-"} -
-
- 创建时间:{item.createTime} + )} + + {/* 标签和创建时间 */} +
+ + {item.type === 4 ? "自定义" : "系统分组"} + +
+ {item.createTime}
+ + {/* 右侧箭头 */} +
+ +
-
+ ))}
)} diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/poolList/index.tsx b/Cunkebao/src/pages/mobile/mine/traffic-pool/poolList/index.tsx index c5c6306da..16d2abe7b 100644 --- a/Cunkebao/src/pages/mobile/mine/traffic-pool/poolList/index.tsx +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/poolList/index.tsx @@ -1,29 +1,19 @@ -import React, { useCallback, useEffect, useState } from "react"; +import React, { useEffect, useState } from "react"; import Layout from "@/components/Layout/Layout"; -import { - SearchOutlined, - ReloadOutlined, - BarChartOutlined, -} from "@ant-design/icons"; -import { Toast } from "antd-mobile"; -import { Input, Button, Checkbox, Pagination } from "antd"; +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, fetchScenarioOptions, addPackage } from "./api"; -import type { TrafficPoolUser, ScenarioOption } from "./data"; -import DataAnalysisPanel from "./DataAnalysisPanel"; -import FilterModal from "./FilterModal"; -import BatchAddModal from "./BatchAddModal"; -import { DeviceSelectionItem } from "@/components/DeviceSelection/data"; +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 navigate = useNavigate(); - // 基础状态 const [loading, setLoading] = useState(false); const [list, setList] = useState([]); const [page, setPage] = useState(1); @@ -31,251 +21,57 @@ const TrafficPoolList: React.FC = () => { const [total, setTotal] = useState(0); const [search, setSearch] = useState(""); - // 筛选相关 - const [showFilter, setShowFilter] = useState(false); - const [scenarioOptions, setScenarioOptions] = useState([]); + const handleSearch = (value: string) => { + setSearch(value); + setPage(1); + }; - // 公共筛选条件状态 - const [filterParams, setFilterParams] = useState({ - selectedDevices: [] as DeviceSelectionItem[], - packageId: 0, - scenarioId: 0, - userValue: 0, - userStatus: 0, - }); + useEffect(() => { + const fetchData = async () => { + setLoading(true); + try { + const params = { + page, + pageSize, + keyword: search, + }; - // 批量相关 - const [selectedIds, setSelectedIds] = useState([]); - const [batchModal, setBatchModal] = useState(false); - - // 数据分析 - const [showStats, setShowStats] = useState(false); - - // 获取列表 - const getList = async (customParams?: any) => { - setLoading(true); - try { - const params: any = { - page, - pageSize, - keyword: search, - packageld: filterParams.packageId, - sceneId: filterParams.scenarioId, - userValue: filterParams.userValue, - addStatus: filterParams.userStatus, - deviceld: filterParams.selectedDevices.map(d => d.id).join(), - ...customParams, // 允许传入自定义参数覆盖 - }; - - const res = await fetchTrafficPoolList(params); - setList(res.list || []); - setTotal(res.total || 0); - } catch (error) { - // 忽略请求过于频繁的错误,避免页面崩溃 - if (error !== "请求过于频繁,请稍后再试") { + const res = await fetchTrafficPoolList(params); + setList(res.list || []); + setTotal(res.total || 0); + } catch (error) { console.error("获取列表失败:", error); + } finally { + setLoading(false); } - } finally { - setLoading(false); - } - }; + }; - // 获取筛选项 - useEffect(() => { - fetchScenarioOptions().then(res => { - setScenarioOptions(res.list || []); - }); - }, []); - - // 全选/反选 - 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 = async options => { - try { - // 构建请求参数 - const params = { - type: "2", // 2选择用户 - addPackageId: options.selectedPackageId, // 目标分组ID - userIds: selectedIds.map(id => id), // 选中的用户ID数组 - // 如果有当前筛选条件,也可以传递 - ...(filterParams.packageId && { - packageId: filterParams.packageId, - }), - ...(filterParams.scenarioId && { - taskId: filterParams.scenarioId, - }), - ...(filterParams.userValue && { - userValue: filterParams.userValue, - }), - ...(filterParams.userStatus && { - addStatus: filterParams.userStatus, - }), - ...(filterParams.selectedDevices.length > 0 && { - deviceId: filterParams.selectedDevices.map(d => d.id).join(","), - }), - ...(search && { keyword: search }), - }; - - console.log("批量加入请求参数:", params); - - // 调用接口 - const result = await addPackage(params); - console.log("批量加入结果:", result); - - // 成功后刷新列表 - getList(); - - // 关闭弹窗并清空选择 - setBatchModal(false); - setSelectedIds([]); - - // 可以添加成功提示 - Toast.show({ - content: `成功将用户加入分组`, - position: "top", - }); - } catch (error) { - console.error("批量加入失败:", error); - // 可以添加错误提示 - Toast.show({ content: "批量加入失败,请重试", position: "top" }); - } - }; - - // 搜索防抖处理 - 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 cleanup = debouncedSearch(); - return cleanup; - }, [debouncedSearch]); - - const handSearch = (value: string) => { - setSearchInput(value); - setSelectedIds([]); - debouncedSearch(); - }; + fetchData(); + }, [page, pageSize, search]); return ( - setShowStats(s => !s)} - style={{ marginLeft: 8 }} - > - {showStats ? "收起分析" : "数据分析"} - - } - /> - {/* 搜索栏 */} +
handSearch(e.target.value)} + placeholder="搜索用户" + value={search} + onChange={e => handleSearch(e.target.value)} prefix={} allowClear size="large" />
-
- {/* 数据分析面板 */} - { - // 可以在这里处理统计数据,比如更新本地状态或发送到父组件 - console.log("收到统计数据:", statsData); - }} - /> - - {/* 批量操作栏 */} -
- 0} - onChange={e => handleSelectAll(e.target.checked)} - style={{ marginRight: 8 }} /> - 全选 - {selectedIds.length > 0 && ( - <> - {`已选${selectedIds.length}项`} - - - )} - {searchInput.length > 0 && ( - <> - - - )} -
-
} @@ -283,56 +79,14 @@ const TrafficPoolList: React.FC = () => {
{ - setPage(newPage); - getList({ page: newPage }); - }} + onChange={setPage} />
} > - {/* 批量加入分组弹窗 */} - setBatchModal(false)} - selectedCount={selectedIds.length} - onConfirm={data => { - // 处理批量加入逻辑 - handleBatchAdd(data); - }} - /> - {/* 筛选弹窗 */} - setShowFilter(false)} - onConfirm={filters => { - // 更新公共筛选条件状态 - const newFilterParams = { - selectedDevices: filters.selectedDevices, - packageId: filters.packageld, - scenarioId: filters.sceneId, - userValue: filters.userValue, - userStatus: filters.addStatus, - }; - - setFilterParams(newFilterParams); - // 重置到第一页并请求列表 - setPage(1); - getList({ - page: 1, - packageld: newFilterParams.packageId, - sceneId: newFilterParams.scenarioId, - userValue: newFilterParams.userValue, - addStatus: newFilterParams.userStatus, - deviceld: newFilterParams.selectedDevices.map(d => d.id).join(), - }); - }} - scenarioOptions={scenarioOptions} - initialFilters={filterParams} - />
{list.length === 0 && !loading ? ( @@ -350,13 +104,6 @@ const TrafficPoolList: React.FC = () => { } >
- handleSelect(item.id, e.target.checked)} - style={{ marginRight: 8 }} - onClick={e => e.stopPropagation()} - className={styles.checkbox} - /> {
{item.nickname || item.identifier} - {/* 性别icon可自行封装 */}
微信号:{item.wechatId || "-"} diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/poolList/BatchAddModal.tsx b/Cunkebao/src/pages/mobile/mine/traffic-pool/poolList1/BatchAddModal.tsx similarity index 100% rename from Cunkebao/src/pages/mobile/mine/traffic-pool/poolList/BatchAddModal.tsx rename to Cunkebao/src/pages/mobile/mine/traffic-pool/poolList1/BatchAddModal.tsx diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/poolList/DataAnalysisPanel.tsx b/Cunkebao/src/pages/mobile/mine/traffic-pool/poolList1/DataAnalysisPanel.tsx similarity index 100% rename from Cunkebao/src/pages/mobile/mine/traffic-pool/poolList/DataAnalysisPanel.tsx rename to Cunkebao/src/pages/mobile/mine/traffic-pool/poolList1/DataAnalysisPanel.tsx diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/poolList/FilterModal.tsx b/Cunkebao/src/pages/mobile/mine/traffic-pool/poolList1/FilterModal.tsx similarity index 100% rename from Cunkebao/src/pages/mobile/mine/traffic-pool/poolList/FilterModal.tsx rename to Cunkebao/src/pages/mobile/mine/traffic-pool/poolList1/FilterModal.tsx diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/poolList1/api.ts b/Cunkebao/src/pages/mobile/mine/traffic-pool/poolList1/api.ts new file mode 100644 index 000000000..9b53c030a --- /dev/null +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/poolList1/api.ts @@ -0,0 +1,34 @@ +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/poolList1/data.ts b/Cunkebao/src/pages/mobile/mine/traffic-pool/poolList1/data.ts new file mode 100644 index 000000000..65ad7f559 --- /dev/null +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/poolList1/data.ts @@ -0,0 +1,51 @@ +// 流量池用户类型 +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"; + +// 获客场景类型 +export interface ScenarioOption { + id: string; + name: string; +} diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/poolList1/index.module.scss b/Cunkebao/src/pages/mobile/mine/traffic-pool/poolList1/index.module.scss new file mode 100644 index 000000000..2fbee5e38 --- /dev/null +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/poolList1/index.module.scss @@ -0,0 +1,65 @@ +.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/poolList1/index.tsx b/Cunkebao/src/pages/mobile/mine/traffic-pool/poolList1/index.tsx new file mode 100644 index 000000000..c5c6306da --- /dev/null +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/poolList1/index.tsx @@ -0,0 +1,396 @@ +import React, { useCallback, useEffect, useState } from "react"; +import Layout from "@/components/Layout/Layout"; +import { + SearchOutlined, + ReloadOutlined, + BarChartOutlined, +} from "@ant-design/icons"; +import { Toast } from "antd-mobile"; +import { Input, Button, Checkbox, 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, fetchScenarioOptions, addPackage } from "./api"; +import type { TrafficPoolUser, ScenarioOption } from "./data"; +import DataAnalysisPanel from "./DataAnalysisPanel"; +import FilterModal from "./FilterModal"; +import BatchAddModal from "./BatchAddModal"; +import { DeviceSelectionItem } from "@/components/DeviceSelection/data"; +const defaultAvatar = + "https://cdn.jsdelivr.net/gh/maokaka/static/avatar-default.png"; + +const TrafficPoolList: React.FC = () => { + const navigate = useNavigate(); + + // 基础状态 + 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 [scenarioOptions, setScenarioOptions] = useState([]); + + // 公共筛选条件状态 + const [filterParams, setFilterParams] = useState({ + selectedDevices: [] as DeviceSelectionItem[], + packageId: 0, + scenarioId: 0, + userValue: 0, + userStatus: 0, + }); + + // 批量相关 + const [selectedIds, setSelectedIds] = useState([]); + const [batchModal, setBatchModal] = useState(false); + + // 数据分析 + const [showStats, setShowStats] = useState(false); + + // 获取列表 + const getList = async (customParams?: any) => { + setLoading(true); + try { + const params: any = { + page, + pageSize, + keyword: search, + packageld: filterParams.packageId, + sceneId: filterParams.scenarioId, + userValue: filterParams.userValue, + addStatus: filterParams.userStatus, + deviceld: filterParams.selectedDevices.map(d => d.id).join(), + ...customParams, // 允许传入自定义参数覆盖 + }; + + const res = await fetchTrafficPoolList(params); + setList(res.list || []); + setTotal(res.total || 0); + } catch (error) { + // 忽略请求过于频繁的错误,避免页面崩溃 + if (error !== "请求过于频繁,请稍后再试") { + console.error("获取列表失败:", error); + } + } finally { + setLoading(false); + } + }; + + // 获取筛选项 + useEffect(() => { + fetchScenarioOptions().then(res => { + setScenarioOptions(res.list || []); + }); + }, []); + + // 全选/反选 + 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 = async options => { + try { + // 构建请求参数 + const params = { + type: "2", // 2选择用户 + addPackageId: options.selectedPackageId, // 目标分组ID + userIds: selectedIds.map(id => id), // 选中的用户ID数组 + // 如果有当前筛选条件,也可以传递 + ...(filterParams.packageId && { + packageId: filterParams.packageId, + }), + ...(filterParams.scenarioId && { + taskId: filterParams.scenarioId, + }), + ...(filterParams.userValue && { + userValue: filterParams.userValue, + }), + ...(filterParams.userStatus && { + addStatus: filterParams.userStatus, + }), + ...(filterParams.selectedDevices.length > 0 && { + deviceId: filterParams.selectedDevices.map(d => d.id).join(","), + }), + ...(search && { keyword: search }), + }; + + console.log("批量加入请求参数:", params); + + // 调用接口 + const result = await addPackage(params); + console.log("批量加入结果:", result); + + // 成功后刷新列表 + getList(); + + // 关闭弹窗并清空选择 + setBatchModal(false); + setSelectedIds([]); + + // 可以添加成功提示 + Toast.show({ + content: `成功将用户加入分组`, + position: "top", + }); + } catch (error) { + console.error("批量加入失败:", error); + // 可以添加错误提示 + Toast.show({ content: "批量加入失败,请重试", position: "top" }); + } + }; + + // 搜索防抖处理 + 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 cleanup = debouncedSearch(); + return cleanup; + }, [debouncedSearch]); + + const handSearch = (value: string) => { + setSearchInput(value); + setSelectedIds([]); + debouncedSearch(); + }; + + return ( + + setShowStats(s => !s)} + style={{ marginLeft: 8 }} + > + {showStats ? "收起分析" : "数据分析"} + + } + /> + {/* 搜索栏 */} +
+
+ handSearch(e.target.value)} + prefix={} + allowClear + size="large" + /> +
+ +
+ {/* 数据分析面板 */} + { + // 可以在这里处理统计数据,比如更新本地状态或发送到父组件 + console.log("收到统计数据:", statsData); + }} + /> + + {/* 批量操作栏 */} +
+ 0} + onChange={e => handleSelectAll(e.target.checked)} + style={{ marginRight: 8 }} + /> + 全选 + {selectedIds.length > 0 && ( + <> + {`已选${selectedIds.length}项`} + + + )} + {searchInput.length > 0 && ( + <> + + + )} +
+ +
+ + } + footer={ +
+ { + setPage(newPage); + getList({ page: newPage }); + }} + /> +
+ } + > + {/* 批量加入分组弹窗 */} + setBatchModal(false)} + selectedCount={selectedIds.length} + onConfirm={data => { + // 处理批量加入逻辑 + handleBatchAdd(data); + }} + /> + {/* 筛选弹窗 */} + setShowFilter(false)} + onConfirm={filters => { + // 更新公共筛选条件状态 + const newFilterParams = { + selectedDevices: filters.selectedDevices, + packageId: filters.packageld, + scenarioId: filters.sceneId, + userValue: filters.userValue, + userStatus: filters.addStatus, + }; + + setFilterParams(newFilterParams); + // 重置到第一页并请求列表 + setPage(1); + getList({ + page: 1, + packageld: newFilterParams.packageId, + sceneId: newFilterParams.scenarioId, + userValue: newFilterParams.userValue, + addStatus: newFilterParams.userStatus, + deviceld: newFilterParams.selectedDevices.map(d => d.id).join(), + }); + }} + scenarioOptions={scenarioOptions} + initialFilters={filterParams} + /> +
+ {list.length === 0 && !loading ? ( + + ) : ( +
+ {list.map(item => ( +
+
+ navigate( + `/mine/traffic-pool/detail/${item.wechatId}/${item.id}`, + ) + } + > +
+ handleSelect(item.id, e.target.checked)} + style={{ marginRight: 8 }} + onClick={e => e.stopPropagation()} + className={styles.checkbox} + /> + +
+
+ {item.nickname || item.identifier} + {/* 性别icon可自行封装 */} +
+
+ 微信号:{item.wechatId || "-"} +
+
+ 来源:{item.fromd || "-"} +
+
+ 分组: + {item.packages && item.packages.length + ? item.packages.join(",") + : "-"} +
+
+ 创建时间:{item.createTime} +
+
+
+
+
+ ))} +
+ )} +
+ + ); +}; + +export default TrafficPoolList; diff --git a/Cunkebao/src/router/module/mine.tsx b/Cunkebao/src/router/module/mine.tsx index e7038ddde..b0d6f6e21 100644 --- a/Cunkebao/src/router/module/mine.tsx +++ b/Cunkebao/src/router/module/mine.tsx @@ -2,7 +2,7 @@ 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 TrafficPoolList from "@/pages/mobile/mine/traffic-pool/poolList/index"; +import TrafficPoolList from "@/pages/mobile/mine/traffic-pool/poolList1/index"; import TrafficPoolDetail from "@/pages/mobile/mine/traffic-pool/detail/index"; import WechatAccounts from "@/pages/mobile/mine/wechat-accounts/list/index"; import WechatAccountDetail from "@/pages/mobile/mine/wechat-accounts/detail/index"; From fc5ae31b2b203ec98adbe0b39b76c9107e7b5835 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: Thu, 16 Oct 2025 14:11:26 +0800 Subject: [PATCH 03/19] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=B5=81=E9=87=8F?= =?UTF-8?q?=E6=B1=A0=E5=88=97=E8=A1=A8=E6=A0=B7=E5=BC=8F=EF=BC=9A=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E6=A0=B7=E5=BC=8F=E6=96=87=E4=BB=B6=EF=BC=8C=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E5=8D=A1=E7=89=87=E6=A0=B7=E5=BC=8F=E5=92=8C=E5=B8=83?= =?UTF-8?q?=E5=B1=80=EF=BC=8C=E6=8F=90=E5=8D=87=E8=A7=86=E8=A7=89=E6=95=88?= =?UTF-8?q?=E6=9E=9C=E5=92=8C=E7=94=A8=E6=88=B7=E4=BD=93=E9=AA=8C=EF=BC=9B?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E7=BB=84=E4=BB=B6=E9=80=BB=E8=BE=91=EF=BC=8C?= =?UTF-8?q?=E7=AE=80=E5=8C=96=E4=BB=A3=E7=A0=81=E7=BB=93=E6=9E=84=EF=BC=8C?= =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E5=8F=AF=E7=BB=B4=E6=8A=A4=E6=80=A7=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mine/traffic-pool/list/index.module.scss | 182 ++++++++++++++---- .../mobile/mine/traffic-pool/list/index.tsx | 177 +++++++---------- 2 files changed, 217 insertions(+), 142 deletions(-) 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 2fbee5e38..c0ff2a620 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 @@ -1,65 +1,175 @@ .listWrap { padding: 12px; + background: #f5f5f5; } -.cardContent { +/* 美团风格卡片样式 */ +.cardCompact { + margin: 0 0 12px 0; + border: none; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + overflow: hidden; + background: #fff; +} + +.cardBody { + padding: 16px; + display: flex; + align-items: flex-start; + gap: 12px; +} + +/* 左侧图片区域 */ +.imageBox { + width: 80px; + height: 80px; + border-radius: 6px; display: flex; align-items: center; - gap: 12px; + justify-content: center; + flex-shrink: 0; 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; + overflow: hidden; } -.card { - margin-bottom: 12px; +/* 右侧内容区域 */ +.contentArea { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 8px; +} + +/* 标题行 */ +.titleRow { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 8px; } .title { font-size: 16px; font-weight: 600; - color: #222; + color: #1a1a1a; + line-height: 1.3; + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } -.desc { - font-size: 13px; - color: #888; - margin: 6px 0 4px 0; +/* 右侧标签区域 */ +.rightTags { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 4px; + flex-shrink: 0; } -.count { - font-size: 13px; - color: #1677ff; +.deliveryTag { + background: #fff7e6; + color: #d46b08; + font-size: 10px; + padding: 2px 6px; + border-radius: 4px; + border: 1px solid #ffd591; } -.pagination { +.timeTag { + color: #ff6b35; + font-size: 11px; + font-weight: 500; display: flex; align-items: center; + gap: 2px; +} + +/* 评分和销售信息 */ +.ratingRow { + display: flex; + align-items: center; + gap: 12px; + font-size: 12px; +} + +.rating { + color: #ff6b35; + font-weight: 600; +} + +.sales { + color: #8c8c8c; +} + +.price { + color: #8c8c8c; +} + +/* 配送信息 */ +.deliveryInfo { + display: flex; + align-items: center; + gap: 12px; + font-size: 11px; + color: #8c8c8c; +} + +/* 中间标签 */ +.middleTag { + display: flex; justify-content: center; - gap: 16px; - margin: 16px 0; + margin: 4px 0; } -.pagination button { - background: #f5f5f5; - border: none; +.highScoreTag { + background: linear-gradient(135deg, #ff6b35, #ff8c42); + color: white; + font-size: 10px; + padding: 4px 8px; border-radius: 4px; - padding: 4px 12px; - color: #1677ff; - cursor: pointer; + font-weight: 500; } -.pagination button:disabled { - color: #ccc; - cursor: not-allowed; +/* 底部按钮区域 */ +.bottomActions { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + margin-top: 8px; +} + +.dineInBtn { + background: transparent; + border: 1px solid #52c41a; + color: #52c41a; + font-size: 11px; + padding: 4px 8px; + border-radius: 4px; + display: flex; + align-items: center; + gap: 2px; +} + +.couponBtn { + background: linear-gradient(135deg, #ff6b35, #ff8c42); + color: white; + font-size: 11px; + padding: 4px 8px; + border-radius: 4px; + display: flex; + align-items: center; + gap: 2px; + flex: 1; + justify-content: center; +} + +.couponText { + font-size: 10px; + color: rgba(255, 255, 255, 0.8); + margin-left: 4px; } 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 c805ba752..2ed29bfad 100644 --- a/Cunkebao/src/pages/mobile/mine/traffic-pool/list/index.tsx +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/list/index.tsx @@ -4,9 +4,8 @@ import { SearchOutlined, ReloadOutlined, PlusOutlined, - RightOutlined, } from "@ant-design/icons"; -import { Input, Button, Pagination, Card, Tag } from "antd"; +import { Input, Button, Pagination, Card } from "antd"; import styles from "./index.module.scss"; import { Empty } from "antd-mobile"; import { useNavigate } from "react-router-dom"; @@ -57,6 +56,31 @@ const TrafficPoolList: React.FC = () => { setPage(1); }; + const handleRefresh = () => { + setPage(1); + // 触发数据重新获取 + const fetchData = async () => { + setLoading(true); + try { + const params = { + page: 1, + pageSize, + keyword: search, + }; + + const res: PackageList = await getPackage(params); + setList(res?.list || []); + setTotal(res?.total || 0); + } catch (error) { + console.error("获取列表失败:", error); + } finally { + setLoading(false); + } + }; + + fetchData(); + }; + useEffect(() => { const fetchData = async () => { setLoading(true); @@ -114,7 +138,7 @@ const TrafficPoolList: React.FC = () => { />
-
- )} - - )} - -
- )} - - {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 000000000..f8b95a7ef --- /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 000000000..398ab2007 --- /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 000000000..75c1e09ed --- /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 000000000..6cc023fe9 --- /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 000000000..823417b81 --- /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 000000000..d0c27e852 --- /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 000000000..41a71d0b3 --- /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 000000000..f4ae5e50d --- /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 000000000..20b5d1575 --- /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 000000000..2e43a5463 --- /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 000000000..ff7e0adbf --- /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 000000000..7e7279481 --- /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 000000000..1712b8d45 --- /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 000000000..e6bd5433b --- /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 9b53c030a..000000000 --- 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 2fbee5e38..000000000 --- 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 98d292314..9171337aa 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 c0ff2a620..4d4936292 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 a4b5af1d0..aa92817aa 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 000000000..7d4efe7d8 --- /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 65ad7f559..204547faf 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 000000000..68ecdca15 --- /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 16d2abe7b..823ccf655 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 bc53e19d0..9346afa6c 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, }, From 3fbf54df9912645742af5afa16f1996109f15f33 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:22:38 +0800 Subject: [PATCH 06/19] =?UTF-8?q?=E7=A7=BB=E9=99=A4=E6=B5=81=E9=87=8F?= =?UTF-8?q?=E6=B1=A0=E5=88=97=E8=A1=A8=E6=A0=B7=E5=BC=8F=EF=BC=9A=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E4=B8=8D=E5=BF=85=E8=A6=81=E7=9A=84=E6=A0=B7=E5=BC=8F?= =?UTF-8?q?=E8=A7=84=E5=88=99=EF=BC=8C=E7=AE=80=E5=8C=96=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E7=BB=93=E6=9E=84=EF=BC=8C=E6=8F=90=E5=8D=87=E5=8F=AF=E7=BB=B4?= =?UTF-8?q?=E6=8A=A4=E6=80=A7=E5=92=8C=E7=94=A8=E6=88=B7=E4=BD=93=E9=AA=8C?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mobile/mine/traffic-pool/list/index.module.scss | 13 ------------- 1 file changed, 13 deletions(-) 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 4d4936292..fb01dcec5 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 @@ -27,20 +27,7 @@ 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; - } } /* 左侧图片区域 */ From a775596719f91db2eefd101b80811de8ca55ea66 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:24:50 +0800 Subject: [PATCH 07/19] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=B5=81=E9=87=8F?= =?UTF-8?q?=E6=B1=A0=E5=88=97=E8=A1=A8=E6=A0=B7=E5=BC=8F=EF=BC=9A=E8=B0=83?= =?UTF-8?q?=E6=95=B4=E5=8D=A1=E7=89=87=E5=86=85=E8=BE=B9=E8=B7=9D=E5=92=8C?= =?UTF-8?q?=E8=8F=9C=E5=8D=95=E6=8C=89=E9=92=AE=E4=BD=8D=E7=BD=AE=EF=BC=8C?= =?UTF-8?q?=E6=8F=90=E5=8D=87=E5=B8=83=E5=B1=80=E7=BE=8E=E8=A7=82=E6=80=A7?= =?UTF-8?q?=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 --- .../src/pages/mobile/mine/traffic-pool/list/index.module.scss | 4 ++-- Cunkebao/src/pages/mobile/mine/traffic-pool/list/index.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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 fb01dcec5..c24427b2f 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,7 +14,7 @@ } .cardBody { - padding: 16px 16px 16px 16px; + padding: 16px 30px 16px 16px; display: flex; align-items: flex-start; gap: 12px; @@ -25,7 +25,7 @@ .menuButton { position: absolute; right: 8px; - top: 8px; + top: 10px; z-index: 10; cursor: pointer; } 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 aa92817aa..34f162129 100644 --- a/Cunkebao/src/pages/mobile/mine/traffic-pool/list/index.tsx +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/list/index.tsx @@ -209,7 +209,7 @@ const TrafficPoolList: React.FC = () => {
navigate(`/mine/traffic-pool/userList/${item.id}`) } From 4df670d1c946565d8b67cd788f4e6363593d500d 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: Mon, 20 Oct 2025 15:04:41 +0800 Subject: [PATCH 08/19] =?UTF-8?q?=E9=87=8D=E6=9E=84=E6=B5=81=E9=87=8F?= =?UTF-8?q?=E6=B1=A0=E8=A1=A8=E5=8D=95=E7=BB=84=E4=BB=B6=EF=BC=9A=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E9=A2=84=E8=AE=BE=E6=96=B9=E6=A1=88=E9=80=89=E6=8B=A9?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=8C=E4=BC=98=E5=8C=96=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E7=94=9F=E6=88=90=E9=80=BB=E8=BE=91=EF=BC=8C?= =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E6=8F=90=E4=BA=A4=E7=8A=B6=E6=80=81=E7=AE=A1?= =?UTF-8?q?=E7=90=86=EF=BC=8C=E6=8F=90=E5=8D=87=E7=94=A8=E6=88=B7=E4=BD=93?= =?UTF-8?q?=E9=AA=8C=E5=92=8C=E4=BB=A3=E7=A0=81=E5=8F=AF=E7=BB=B4=E6=8A=A4?= =?UTF-8?q?=E6=80=A7=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mobile/mine/traffic-pool/form/api.ts | 57 ++++- .../components/AudienceFilter.module.scss | 12 +- .../form/components/AudienceFilter.tsx | 239 +++++++++--------- .../mobile/mine/traffic-pool/form/index.tsx | 67 ++++- .../pages/mobile/scenarios/plan/new/index.tsx | 31 ++- 5 files changed, 266 insertions(+), 140 deletions(-) diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/form/api.ts b/Cunkebao/src/pages/mobile/mine/traffic-pool/form/api.ts index 398ab2007..781ebe281 100644 --- a/Cunkebao/src/pages/mobile/mine/traffic-pool/form/api.ts +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/form/api.ts @@ -61,7 +61,62 @@ export interface PresetScheme { } export async function getPresetSchemes(): Promise { - return request("/v1/traffic/pool/schemes", {}, "GET"); + // 模拟数据 + return new Promise(resolve => { + setTimeout(() => { + resolve([ + { + id: "scheme_1", + name: "高价值客户方案", + description: "针对高消费、高活跃度的客户群体", + conditions: [ + { id: "rfm_high", type: "rfm", label: "RFM评分", value: "high" }, + { + id: "consumption_high", + type: "consumption", + label: "消费能力", + value: "high", + }, + ], + userCount: 1250, + color: "#ff4d4f", + }, + { + id: "scheme_2", + name: "新用户激活方案", + description: "针对新注册用户的激活策略", + conditions: [ + { id: "new_user", type: "tag", label: "新用户", value: true }, + { + id: "low_activity", + type: "activity", + label: "活跃度", + value: "low", + }, + ], + userCount: 890, + color: "#52c41a", + }, + { + id: "scheme_3", + name: "流失挽回方案", + description: "针对流失风险用户的挽回策略", + conditions: [ + { id: "churn_risk", type: "tag", label: "流失风险", value: true }, + { + id: "last_active", + type: "time", + label: "最后活跃", + value: "30天前", + }, + ], + userCount: 567, + color: "#faad14", + }, + ]); + }, 500); + }); + // return request("/v1/traffic/pool/schemes", {}, "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 index 75c1e09ed..c1c7e292c 100644 --- 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 @@ -20,13 +20,21 @@ color: #333; } -.schemeBtn { +.schemeRow { + display: flex; + gap: 12px; + align-items: center; +} + +.addSchemeBtn { display: flex; align-items: center; gap: 4px; font-size: 12px; padding: 4px 8px; - height: 28px; + height: 32px; + white-space: nowrap; + flex-shrink: 0; } .section { 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 index 6cc023fe9..c126041c5 100644 --- a/Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/AudienceFilter.tsx +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/form/components/AudienceFilter.tsx @@ -1,12 +1,16 @@ import React, { useEffect, useState } from "react"; import { Card, Button } from "antd-mobile"; import { Select } from "antd"; -import { EditSOutline } from "antd-mobile-icons"; +import { PlusOutlined } from "@ant-design/icons"; import CustomConditionModal from "./CustomConditionModal"; -import SchemeRecommendation from "./SchemeRecommendation"; import ConditionList from "./ConditionList"; import styles from "./AudienceFilter.module.scss"; -import { getIndustryOptions, IndustryOption } from "../api"; +import { + getIndustryOptions, + getPresetSchemes, + IndustryOption, + PresetScheme, +} from "../api"; interface FilterCondition { id: string; @@ -19,26 +23,31 @@ interface FilterCondition { 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 [presetSchemes, setPresetSchemes] = useState([]); const [selectedIndustry, setSelectedIndustry] = useState< string | number | undefined >(undefined); + const [selectedScheme, setSelectedScheme] = useState( + undefined, + ); - // 加载行业选项(固定筛选项) + // 加载行业选项和方案列表 useEffect(() => { getIndustryOptions() .then(res => setIndustryOptions(res || [])) .catch(() => setIndustryOptions([])); + + getPresetSchemes() + .then(res => setPresetSchemes(res || [])) + .catch(() => setPresetSchemes([])); }, []); const handleAddCondition = (condition: FilterCondition) => { @@ -58,15 +67,23 @@ const AudienceFilter: React.FC = ({ onChange(newConditions); }; - const handleApplyScheme = (schemeConditions: FilterCondition[]) => { - onChange(schemeConditions); - setShowSchemeModal(false); + const handleSchemeChange = (schemeId: string) => { + setSelectedScheme(schemeId); + if (schemeId) { + // 找到选中的方案并应用其条件 + const scheme = presetSchemes.find(s => s.id === schemeId); + if (scheme) { + onChange(scheme.conditions); + } + } else { + // 清空方案选择时,清空条件 + onChange([]); + } }; - const handleGenerate = () => { - // 模拟生成用户数据 - const mockUsers = generateMockUsers(conditions); - onGenerate(mockUsers); + const handleAddScheme = () => { + // 这里可以打开添加方案的弹窗或跳转到方案管理页面 + console.log("添加新方案"); }; return ( @@ -74,99 +91,96 @@ const AudienceFilter: React.FC = ({
人群筛选
-
- {/* 行业筛选(固定项,接口获取选项) */} + {/* 方案推荐选择 */}
-
行业
- ({ + label: `${scheme.name} (${scheme.userCount}人)`, + value: scheme.id, + }))} + allowClear + /> +
- {/* 自定义条件列表 */} - + {/* 条件筛选区域 - 当未选择方案时显示 */} + {!selectedScheme && ( + <> + {/* 行业筛选(固定项,接口获取选项) */} +
+
行业
+ setCustomAmount(e.target.value)} + style={{ + width: "100%", + padding: "12px 16px", + border: "1px solid #e5e5e5", + borderRadius: "8px", + fontSize: "16px", + outline: "none", + }} + min="1" + max="100000" + step="0.01" + />
-
- {selected.name} - {selected.isRecommend === 1 && ( - - 推荐 - - )} - {selected.isHot === 1 && ( - - 热门 - - )} -
-
- 包含 {selected.tokens} Tokens -
- {selected.originalPrice && ( -
+ • 单次充值上限:100,000元 +
• 支持微信支付 +
+
+ + {customAmount && parseFloat(customAmount) > 0 && ( +
+
+ 充值金额: + - 原价: ¥{selected.originalPrice / 100} -
- )} + ¥{parseFloat(customAmount).toFixed(2)} + +
+
+ 到账金额:¥{parseFloat(customAmount).toFixed(2)} +
)} + + -
服务消耗
+
充值说明
- 使用以下服务将从余额中扣除相应费用。 + • 充值金额实时到账,可用于购买AI服务和版本套餐 +
+ • 使用AI服务将从余额中扣除相应费用 +
• 余额支持退款,详情请联系客服
+ {balance < 10 && (
@@ -349,63 +392,106 @@ const Recharge: React.FC = () => {
- 存客宝版本套餐 + 充值套餐
- 选择适合的版本,享受不同级别的AI服务 + 选择合适的充值套餐,享受更多优惠
- {versionPackages.map(pkg => ( + {taocanList.map(pkg => (
-
{pkg.icon}
+
💰
{pkg.name} - {pkg.tag && ( + {pkg.isRecommend === 1 && ( - {pkg.tag} + 推荐 + + )} + {pkg.isHot === 1 && ( + + 热门 )}
-
{pkg.price}
+
+ ¥{pkg.price / 100} +
-
- {pkg.description} -
+ {pkg.description && ( +
+ {pkg.description} +
+ )}
-
包含功能:
- {pkg.features.map((feature, index) => ( -
- - {feature} +
套餐内容:
+
+ + 包含 {pkg.tokens} Tokens +
+ {pkg.originalPrice && ( +
+ 原价: ¥{pkg.originalPrice / 100}
- ))} + )}
- {pkg.status && ( -
{pkg.status}
- )} - {pkg.buttonText && ( - - )} + ))}
+ + {selected && ( +
+ +
+ )}
); diff --git a/Cunkebao/src/pages/mobile/workspace/ai-knowledge/api 文档 b/Cunkebao/src/pages/mobile/workspace/ai-knowledge/api 文档 new file mode 100644 index 000000000..1659a0c3c --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/ai-knowledge/api 文档 @@ -0,0 +1,168 @@ +初始化AI功能(每次都得执行) + GET /v1/knowledge/init + + + + +发布并应用AI工具(修改知识库需要重新发布) + GET /v1/knowledge/release + 传参: + { + id:number + } + + 返回参数: + { + "id": 1, + "companyId": 2130, + "userId": 128, + "config": { + "name": "魔兽世界", + "model_id": "1737521813", + "prompt_info": "# 角色\r\n你是一位全能知识客服,作为专业的客服智能体,具备全面的知识储备,能够回答用户提出的各类问题。在回答问题前,会仔细查阅知识库内容,并且始终严格遵守中国法律法规。\r\n\r\n## 技能\r\n### 技能 1: 回答用户问题\r\n1. 当用户提出问题时,首先在知识库中进行搜索查找相关信息。\r\n2. 依据知识库中的内容,为用户提供准确、清晰、完整的回答。\r\n \r\n## 限制\r\n- 仅依据知识库内容回答问题,对于知识库中没有的信息,如实告知用户无法回答。\r\n- 回答必须严格遵循中国法律法规,不得出现任何违法违规内容。\r\n- 回答需简洁明了,避免冗长复杂的表述。" + }, + "createTime": "2025-10-24 16:55:08", + "updateTime": "2025-10-24 16:56:28", + "isRelease": 1, + "releaseTime": 1761296188, + "botId": "7564707767488610345", + "datasetId": "7564708881499619366" + } + + + +知识库类型 - 列表 + GET /v1/knowledge/typeList + 传参: + { + page:number + limit:number + } + 返回参数: + "total": 5, + "per_page": 20, + "current_page": 1, + "last_page": 1, + "data": [ + { + "id": 1, + "type": 0, + "name": "产品介绍库", + "description": "包含所有产品相关的介绍文档、图片和视频资料", + "label": [ + "产品", + "营销" + ], + "prompt": null, + "companyId": 0, + "userId": 0, + "createTime": null, + "updateTime": null, + "isDel": 0, + "delTime": 0 + }, + + ] + + +知识库类型 - 添加 + POST /v1/knowledge/addType + 传参: + { + name:string + description:string + label:string[] + prompt:string + } + + +知识库类型 - 编辑 + POST /v1/knowledge/editType + 传参: + { + id:number + name:string + description:string + label:string[] + prompt:string + } + + +知识库类型 - 删除 + DELETE /v1/knowledge/deleteType + 传参: + { + id:number + } + + +知识库 - 列表 + GET /v1/knowledge/getList + 传参: + { + name:number + typeId:number + label:string[] + fileUrl:string + } + 返回参数: + { + "total": 1, + "per_page": 20, + "current_page": 1, + "last_page": 1, + "data": [ + { + "id": 1, + "typeId": 1, + "name": "存客宝项目介绍(面向开发人员).docx", + "label": [ + "1231", + "3453" + ], + "companyId": 2130, + "userId": 128, + "createTime": 1761296164, + "updateTime": 1761296165, + "isDel": 0, + "delTime": 0, + "documentId": "7564706328503189558", + "fileUrl": "http://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/2025/10/22/9de59fc8723f10973ade586650dfb235.docx", + "type": { + "id": 1, + "type": 0, + "name": "产品介绍库", + "description": "包含所有产品相关的介绍文档、图片和视频资料", + "label": [ + "产品", + "营销" + ], + "prompt": null, + "companyId": 0, + "userId": 0, + "createTime": null, + "updateTime": null, + "isDel": 0, + "delTime": 0 + } + } + ] + } + + +知识库 - 添加 + POST /v1/knowledge/add + 传参: + { + name:number + typeId:number + label:string[] + fileUrl:string + } + +知识库 - 删除 + DELETE /v1/knowledge/delete + 传参: + { + id:number + } diff --git a/Cunkebao/src/pages/mobile/workspace/ai-knowledge/detail/api.ts b/Cunkebao/src/pages/mobile/workspace/ai-knowledge/detail/api.ts new file mode 100644 index 000000000..aecda9a04 --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/ai-knowledge/detail/api.ts @@ -0,0 +1,109 @@ +import request from "@/api/request"; +import type { + KnowledgeBaseDetailResponse, + MaterialListResponse, + CallerListResponse, +} from "./data"; + +// 获取知识库类型详情(复用列表接口) +export function getKnowledgeBaseDetail( + id: number, +): Promise { + // 接口文档中没有单独的详情接口,通过列表接口获取 + return request("/v1/knowledge/typeList", { page: 1, limit: 100 }, "GET").then( + (res: any) => { + const item = res.data?.find((item: any) => item.id === id); + if (!item) { + throw new Error("知识库不存在"); + } + // 转换数据格式 + return { + ...item, + tags: item.label || [], + useIndependentPrompt: !!item.prompt, + independentPrompt: item.prompt || "", + materials: [], // 需要单独获取 + callers: [], // 暂无接口 + }; + }, + ); +} + +// 获取知识库素材列表(对应接口的 knowledge/getList) +export function getMaterialList(params: { + knowledgeBaseId: number; + page?: number; + limit?: number; + name?: string; + label?: string[]; +}): Promise { + return request( + "/v1/knowledge/getList", + { + typeId: params.knowledgeBaseId, + name: params.name, + label: params.label, + page: params.page || 1, + limit: params.limit || 20, + }, + "GET", + ).then((res: any) => ({ + list: res.data || [], + total: res.total || 0, + })); +} + +// 添加素材 +export function uploadMaterial(data: { + typeId: number; + name: string; + label: string[]; + fileUrl: string; +}): Promise { + return request("/v1/knowledge/add", data, "POST"); +} + +// 删除素材 +export function deleteMaterial(id: number): Promise { + return request("/v1/knowledge/delete", { id }, "DELETE"); +} + +// 获取调用者列表(接口未提供) +export function getCallerList( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + params: { + knowledgeBaseId: number; + page?: number; + limit?: number; + }, +): Promise { + // 注意:实际接口未提供,需要后端补充 + console.warn("getCallerList 接口未提供"); + return Promise.resolve({ + list: [], + total: 0, + }); +} + +// 更新知识库配置(使用编辑接口) +export function updateKnowledgeBaseConfig(data: { + id: number; + name?: string; + description?: string; + label?: string[]; + aiCallEnabled?: boolean; + useIndependentPrompt?: boolean; + independentPrompt?: string; +}): Promise { + return request( + "/v1/knowledge/editType", + { + id: data.id, + name: data.name || "", + description: data.description || "", + label: data.label || [], + prompt: data.useIndependentPrompt ? data.independentPrompt || "" : "", + }, + "POST", + ); +} diff --git a/Cunkebao/src/pages/mobile/workspace/ai-knowledge/detail/data.ts b/Cunkebao/src/pages/mobile/workspace/ai-knowledge/detail/data.ts new file mode 100644 index 000000000..b477d105d --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/ai-knowledge/detail/data.ts @@ -0,0 +1,48 @@ +// AI知识库详情相关类型定义 +import type { KnowledgeBase, Caller } from "../list/data"; + +export type { KnowledgeBase, Caller }; + +// 素材类型(对应接口的 knowledge) +export interface Material { + id: number; + typeId: number; // 知识库类型ID + name: string; // 文件名 + label: string[]; // 标签 + companyId: number; + userId: number; + createTime: number; + updateTime: number; + isDel: number; + delTime: number; + documentId: string; // 文档ID + fileUrl: string; // 文件URL + type?: KnowledgeBase; // 关联的知识库类型信息 + // 前端扩展字段 + fileName?: string; // 映射自 name + fileSize?: number; // 文件大小(前端计算) + fileType?: string; // 文件类型(从 name 提取) + filePath?: string; // 映射自 fileUrl + tags?: string[]; // 映射自 label + uploadTime?: string; // 映射自 createTime + uploaderId?: number; // 映射自 userId + uploaderName?: string; +} + +// 知识库详情响应 +export interface KnowledgeBaseDetailResponse extends KnowledgeBase { + materials: Material[]; + callers: Caller[]; +} + +// 素材列表响应 +export interface MaterialListResponse { + list: Material[]; + total: number; +} + +// 调用者列表响应 +export interface CallerListResponse { + list: Caller[]; + total: number; +} diff --git a/Cunkebao/src/pages/mobile/workspace/ai-knowledge/detail/index.module.scss b/Cunkebao/src/pages/mobile/workspace/ai-knowledge/detail/index.module.scss new file mode 100644 index 000000000..ed1fbafa1 --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/ai-knowledge/detail/index.module.scss @@ -0,0 +1,481 @@ +// 详情页容器 +.detailPage { + background: #f5f5f5; + min-height: 100vh; +} + +// Tab容器 +.tabContainer { + background: #fff; + border-bottom: 1px solid #f0f0f0; +} + +.tabs { + display: flex; + padding: 0 16px; +} + +.tab { + flex: 1; + padding: 14px 0; + text-align: center; + font-size: 15px; + color: #666; + border-bottom: 2px solid transparent; + cursor: pointer; + transition: all 0.2s; + background: none; + border: none; + outline: none; + + &:active { + opacity: 0.7; + } +} + +.tabActive { + color: #1890ff; + font-weight: 600; + border-bottom-color: #1890ff; +} + +// 知识库信息卡片 +.infoCard { + background: #fff; + margin: 12px 16px; + border-radius: 12px; + padding: 16px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); +} + +.infoHeader { + display: flex; + align-items: flex-start; + gap: 12px; + margin-bottom: 16px; +} + +.infoIcon { + width: 56px; + height: 56px; + border-radius: 12px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + display: flex; + align-items: center; + justify-content: center; + color: #fff; + font-size: 28px; + flex-shrink: 0; +} + +.infoContent { + flex: 1; + min-width: 0; +} + +.infoName { + font-size: 18px; + font-weight: 600; + color: #222; + margin-bottom: 6px; +} + +.infoDescription { + font-size: 13px; + color: #888; + line-height: 1.5; +} + +.infoStats { + display: flex; + justify-content: space-between; + padding: 12px 0; + border-top: 1px solid #f0f0f0; + border-bottom: 1px solid #f0f0f0; + margin-bottom: 16px; +} + +.statItem { + flex: 1; + text-align: center; +} + +.statValue { + font-size: 20px; + font-weight: 600; + color: #1890ff; + margin-bottom: 4px; +} + +.statValueSuccess { + color: #52c41a; +} + +.statLabel { + font-size: 12px; + color: #888; +} + +.infoTags { + margin-bottom: 16px; +} + +.tagTitle { + font-size: 13px; + color: #666; + margin-bottom: 8px; + font-weight: 500; +} + +.tags { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.tag { + padding: 4px 12px; + border-radius: 10px; + font-size: 12px; + background: rgba(24, 144, 255, 0.1); + color: #1890ff; + border: 1px solid rgba(24, 144, 255, 0.2); +} + +// 配置区域 +.configSection { + margin-bottom: 16px; +} + +.configItem { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 0; + border-bottom: 1px solid #f5f5f5; + + &:last-child { + border-bottom: none; + } +} + +.configLabel { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + color: #333; +} + +.configIcon { + font-size: 16px; + color: #1890ff; +} + +.configDescription { + font-size: 12px; + color: #888; + margin-top: 4px; +} + +// 功能说明列表 +.featureList { + background: #f9f9f9; + border-radius: 8px; + padding: 12px; +} + +.featureItem { + display: flex; + align-items: flex-start; + gap: 8px; + font-size: 13px; + color: #666; + line-height: 1.6; + margin-bottom: 8px; + + &:last-child { + margin-bottom: 0; + } +} + +.featureIcon { + color: #52c41a; + margin-top: 2px; + flex-shrink: 0; +} + +// 调用者名单 +.callerSection { + margin-top: 16px; +} + +.sectionHeader { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; +} + +.sectionTitle { + display: flex; + align-items: center; + gap: 6px; + font-size: 14px; + font-weight: 500; + color: #333; +} + +.sectionCount { + font-size: 13px; + color: #888; + font-weight: normal; +} + +.callerList { + background: #f9f9f9; + border-radius: 8px; + padding: 8px; +} + +.callerItem { + display: flex; + align-items: center; + gap: 10px; + padding: 8px; + background: #fff; + border-radius: 6px; + margin-bottom: 6px; + + &:last-child { + margin-bottom: 0; + } +} + +.callerAvatar { + width: 40px; + height: 40px; + border-radius: 50%; + flex-shrink: 0; +} + +.callerInfo { + flex: 1; + min-width: 0; +} + +.callerName { + font-size: 14px; + font-weight: 500; + color: #333; + margin-bottom: 2px; +} + +.callerRole { + font-size: 12px; + color: #888; +} + +.callerTime { + font-size: 11px; + color: #999; + white-space: nowrap; +} + +// 素材列表 +.materialSection { + padding: 12px 16px; +} + +.uploadButton { + width: 100%; + margin-bottom: 16px; +} + +.materialList { + display: flex; + flex-direction: column; + gap: 12px; +} + +.materialItem { + background: #fff; + border-radius: 12px; + padding: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); + border: 1px solid #ececec; + display: flex; + align-items: center; + gap: 12px; +} + +.materialIcon { + width: 48px; + height: 48px; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + flex-shrink: 0; +} + +.fileIcon { + background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%); + color: #fff; +} + +.videoIcon { + background: linear-gradient(135deg, #a855f7 0%, #9333ea 100%); + color: #fff; +} + +.docIcon { + background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); + color: #fff; +} + +.materialContent { + flex: 1; + min-width: 0; +} + +.materialHeader { + display: flex; + align-items: flex-start; + justify-content: space-between; + margin-bottom: 6px; +} + +.materialName { + font-size: 14px; + font-weight: 500; + color: #333; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex: 1; + margin-right: 8px; +} + +.materialMenu { + font-size: 16px; + color: #888; + cursor: pointer; + padding: 2px; + flex-shrink: 0; +} + +.materialMeta { + display: flex; + align-items: center; + gap: 12px; + font-size: 12px; + color: #888; +} + +.materialSize { + display: flex; + align-items: center; + gap: 4px; +} + +.materialDate { + display: flex; + align-items: center; + gap: 4px; +} + +.materialTags { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-top: 6px; +} + +.materialTag { + padding: 2px 8px; + border-radius: 8px; + font-size: 11px; + background: rgba(0, 0, 0, 0.05); + color: #666; +} + +// 底部按钮组 +.bottomActions { + display: flex; + gap: 12px; + padding: 16px; + background: #fff; + border-top: 1px solid #f0f0f0; +} + +.actionButton { + flex: 1; + padding: 12px; + border: 1px solid #d9d9d9; + border-radius: 8px; + background: #fff; + font-size: 14px; + cursor: pointer; + transition: all 0.2s; + + &:active { + opacity: 0.7; + } +} + +.editButton { + color: #1890ff; + border-color: #1890ff; +} + +.deleteButton { + color: #ff4d4f; + border-color: #ff4d4f; +} + +// 空状态 +.empty { + text-align: center; + padding: 60px 20px; + color: #bbb; +} + +.emptyIcon { + font-size: 64px; + color: #d9d9d9; + margin-bottom: 16px; +} + +.emptyText { + font-size: 14px; + color: #999; +} + +// 编辑提示词弹窗 +.promptEditModal { + .promptTextarea { + width: 100%; + min-height: 200px; + padding: 12px; + border: 1px solid #d9d9d9; + border-radius: 8px; + font-size: 14px; + line-height: 1.6; + resize: vertical; + font-family: inherit; + + &:focus { + outline: none; + border-color: #1890ff; + } + } + + .promptHint { + font-size: 12px; + color: #888; + margin-top: 8px; + line-height: 1.5; + } +} diff --git a/Cunkebao/src/pages/mobile/workspace/ai-knowledge/detail/index.tsx b/Cunkebao/src/pages/mobile/workspace/ai-knowledge/detail/index.tsx new file mode 100644 index 000000000..4d2705ed5 --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/ai-knowledge/detail/index.tsx @@ -0,0 +1,648 @@ +import React, { useEffect, useState } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { + Button, + Switch, + message, + Spin, + Dropdown, + Modal, + Input, + Upload, +} from "antd"; +import { + BookOutlined, + CheckCircleOutlined, + UserOutlined, + UploadOutlined, + FileOutlined, + VideoCameraOutlined, + FileTextOutlined, + MoreOutlined, + EditOutlined, + DeleteOutlined, + SettingOutlined, + ApiOutlined, + BulbOutlined, + CalendarOutlined, + DatabaseOutlined, +} from "@ant-design/icons"; +import Layout from "@/components/Layout/Layout"; +import NavCommon from "@/components/NavCommon"; +import style from "./index.module.scss"; +import { + getKnowledgeBaseDetail, + getMaterialList, + deleteMaterial, + updateKnowledgeBaseConfig, + uploadMaterial, +} from "./api"; +import { deleteKnowledgeBase } from "../list/api"; +import type { KnowledgeBase, Material, Caller } from "./data"; + +type TabType = "info" | "materials"; + +const AIKnowledgeDetail: React.FC = () => { + const navigate = useNavigate(); + const { id } = useParams<{ id: string }>(); + const [activeTab, setActiveTab] = useState("info"); + const [loading, setLoading] = useState(false); + const [knowledgeBase, setKnowledgeBase] = useState( + null, + ); + const [materials, setMaterials] = useState([]); + const [callers, setCallers] = useState([]); + const [promptEditVisible, setPromptEditVisible] = useState(false); + const [independentPrompt, setIndependentPrompt] = useState(""); + + useEffect(() => { + if (id) { + fetchDetail(); + } + }, [id]); + + const fetchDetail = async () => { + if (!id) return; + setLoading(true); + try { + const detail = await getKnowledgeBaseDetail(Number(id)); + setKnowledgeBase(detail); + setCallers(detail.callers || []); + setIndependentPrompt(detail.independentPrompt || ""); + + // 获取素材列表 + const materialRes = await getMaterialList({ + knowledgeBaseId: Number(id), + page: 1, + limit: 100, + }); + + // 转换素材数据格式 + const transformedMaterials = (materialRes.list || []).map( + (item: any) => ({ + ...item, + fileName: item.name, + tags: item.label || [], + filePath: item.fileUrl, + uploadTime: item.createTime + ? new Date(item.createTime * 1000).toLocaleDateString("zh-CN") + : "-", + uploaderId: item.userId, + fileType: item.name?.split(".").pop() || "file", + fileSize: 0, // 接口未返回,需要前端计算或后端补充 + }), + ); + + setMaterials(transformedMaterials); + + // 更新知识库的素材数量 + if (detail) { + setKnowledgeBase({ + ...detail, + materialCount: transformedMaterials.length, + }); + } + } catch (error) { + message.error("获取详情失败"); + navigate(-1); + } finally { + setLoading(false); + } + }; + + const handleAICallToggle = async (checked: boolean) => { + if (!id || !knowledgeBase) return; + + // 系统预设不允许修改 + if (knowledgeBase.type === 0) { + message.warning("系统预设知识库不可修改"); + return; + } + + try { + await updateKnowledgeBaseConfig({ + id: Number(id), + name: knowledgeBase.name, + description: knowledgeBase.description, + label: knowledgeBase.tags || knowledgeBase.label || [], + aiCallEnabled: checked, + useIndependentPrompt: knowledgeBase.useIndependentPrompt, + independentPrompt: knowledgeBase.independentPrompt || "", + }); + message.success(checked ? "已启用AI调用" : "已关闭AI调用"); + setKnowledgeBase(prev => + prev ? { ...prev, aiCallEnabled: checked } : null, + ); + } catch (error) { + message.error("操作失败"); + } + }; + + const handleIndependentPromptToggle = async (checked: boolean) => { + if (!id || !knowledgeBase) return; + + // 系统预设不允许修改 + if (knowledgeBase.type === 0) { + message.warning("系统预设知识库不可修改"); + return; + } + + if (checked) { + // 启用时打开编辑弹窗 + setPromptEditVisible(true); + } else { + // 禁用时直接更新 + try { + await updateKnowledgeBaseConfig({ + id: Number(id), + name: knowledgeBase.name, + description: knowledgeBase.description, + label: knowledgeBase.tags || knowledgeBase.label || [], + useIndependentPrompt: false, + independentPrompt: "", + }); + message.success("已关闭独立提示词"); + setKnowledgeBase(prev => + prev + ? { + ...prev, + useIndependentPrompt: false, + independentPrompt: "", + prompt: null, + } + : null, + ); + } catch (error) { + message.error("操作失败"); + } + } + }; + + const handlePromptSave = async () => { + if (!id || !knowledgeBase) return; + if (!independentPrompt.trim()) { + message.error("请输入提示词内容"); + return; + } + + try { + await updateKnowledgeBaseConfig({ + id: Number(id), + name: knowledgeBase.name, + description: knowledgeBase.description, + label: knowledgeBase.tags || knowledgeBase.label || [], + useIndependentPrompt: true, + independentPrompt: independentPrompt.trim(), + }); + message.success("保存成功"); + setKnowledgeBase(prev => + prev + ? { + ...prev, + useIndependentPrompt: true, + independentPrompt: independentPrompt.trim(), + prompt: independentPrompt.trim(), + } + : null, + ); + setPromptEditVisible(false); + } catch (error) { + message.error("保存失败"); + } + }; + + const handleDeleteKnowledge = async () => { + if (!id || !knowledgeBase) return; + + // 系统预设不允许删除 + if (knowledgeBase.type === 0) { + message.warning("系统预设知识库不可删除"); + return; + } + + Modal.confirm({ + title: "确认删除", + content: "删除后数据无法恢复,确定要删除该知识库吗?", + okText: "确定", + cancelText: "取消", + okButtonProps: { danger: true }, + onOk: async () => { + try { + await deleteKnowledgeBase(Number(id)); + message.success("删除成功"); + navigate(-1); + } catch (error) { + message.error("删除失败"); + } + }, + }); + }; + + const handleDeleteMaterial = async (materialId: number) => { + Modal.confirm({ + title: "确认删除", + content: "确定要删除该素材吗?", + okText: "确定", + cancelText: "取消", + okButtonProps: { danger: true }, + onOk: async () => { + try { + await deleteMaterial(materialId); + message.success("删除成功"); + setMaterials(prev => prev.filter(m => m.id !== materialId)); + } catch (error) { + message.error("删除失败"); + } + }, + }); + }; + + const handleUpload = async (file: File) => { + if (!id) return; + + try { + // 注意:这里需要先上传文件获取 fileUrl + // 实际项目中应该有单独的文件上传接口 + // 这里暂时使用占位实现 + message.loading("正在上传文件...", 0); + + // TODO: 调用文件上传接口获取 fileUrl + // const fileUrl = await uploadFile(file); + + // 临时方案:直接使用文件名作为占位 + const fileUrl = `temp://${file.name}`; + + await uploadMaterial({ + typeId: Number(id), + name: file.name, + label: [], // 可以后续添加标签编辑功能 + fileUrl: fileUrl, + }); + + message.destroy(); + message.success("上传成功"); + fetchDetail(); + } catch (error) { + message.destroy(); + message.error("上传失败"); + } + }; + + const getFileIcon = (fileType: string) => { + const type = fileType.toLowerCase(); + if (["mp4", "avi", "mov", "wmv"].includes(type)) { + return ( +
+ +
+ ); + } else if (["doc", "docx", "pdf", "txt"].includes(type)) { + return ( +
+ +
+ ); + } else { + return ( +
+ +
+ ); + } + }; + + const formatFileSize = (bytes: number) => { + if (bytes < 1024) return bytes + " B"; + if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB"; + return (bytes / (1024 * 1024)).toFixed(1) + " MB"; + }; + + const renderInfoTab = () => { + if (!knowledgeBase) return null; + + const isSystemPreset = knowledgeBase.type === 0; // 系统预设只读 + + return ( + <> +
+
+
+ +
+
+
+ {knowledgeBase.name} + {isSystemPreset && ( + + (系统预设) + + )} +
+ {knowledgeBase.description && ( +
+ {knowledgeBase.description} +
+ )} +
+
+ +
+
+
+ {knowledgeBase.materialCount || 0} +
+
素材总数
+
+
+
+ {knowledgeBase.aiCallEnabled ? "启用" : "关闭"} +
+
AI状态
+
+
+
+ {knowledgeBase.tags?.length || 0} +
+
标签数
+
+
+ + {knowledgeBase.tags && knowledgeBase.tags.length > 0 && ( +
+
内容库标签
+
+ {knowledgeBase.tags.map((tag, index) => ( + + {tag} + + ))} +
+
+ )} + +
+
+
+
+ + AI调用配置 +
+
+ AI助手可以使用此内容库的素材 +
+
+ +
+ +
+
+
+ + 支持智能应答和推荐 +
+
+
+ +
+
+
+ + 实时响应用户查询 +
+
+
+
+ +
+
+ + 与各知识库的回复基本合用 +
+
+ + 确保所有知识库回复的一致性和专业度 +
+
+ + {callers.length > 0 && ( +
+
+
+ + 调用者名单 + {callers.length} +
+
+
+ {callers.slice(0, 3).map(caller => ( +
+ {caller.name} +
+
{caller.name}
+
{caller.role}
+
+
+ 调用 {caller.callCount} 次 · {caller.lastCallTime} +
+
+ ))} +
+
+ )} +
+ + {/* 系统预设不显示编辑和删除按钮 */} + {!isSystemPreset && ( +
+ + +
+ )} + + ); + }; + + const renderMaterialsTab = () => { + const isSystemPreset = knowledgeBase?.type === 0; // 系统预设只读 + + return ( +
+ {/* 系统预设不显示上传按钮 */} + {!isSystemPreset && ( + { + handleUpload(file); + return false; + }} + > + + + )} + +
+ {materials.length > 0 ? ( + materials.map(material => ( +
+ {getFileIcon(material.fileType)} +
+
+
+ {material.fileName} +
+ {/* 系统预设不显示删除按钮 */} + {!isSystemPreset && ( + , + label: "删除", + danger: true, + }, + ], + onClick: () => handleDeleteMaterial(material.id), + }} + trigger={["click"]} + placement="bottomRight" + > + + + )} +
+
+
+ + {formatFileSize(material.fileSize)} +
+
+ + {material.uploadTime} +
+
+ {material.tags && material.tags.length > 0 && ( +
+ {material.tags.map((tag, index) => ( + + {tag} + + ))} +
+ )} +
+
+ )) + ) : ( +
+
+ +
+
暂无素材
+
+ )} +
+
+ ); + }; + + return ( + + navigate("/workspace/ai-knowledge")} + /> +
+
+ + +
+
+ + } + > +
+ {loading ? ( +
+ +
+ ) : ( + <> + {activeTab === "info" && renderInfoTab()} + {activeTab === "materials" && renderMaterialsTab()} + + )} +
+ + {/* 编辑独立提示词弹窗 */} + setPromptEditVisible(false)} + onOk={handlePromptSave} + okText="保存" + cancelText="取消" + className={style.promptEditModal} + > +