删除流量池相关组件:移除BatchAddModal、DataAnalysisPanel、FilterModal及其相关逻辑,简化代码结构,提升维护性。

This commit is contained in:
超级老白兔
2025-10-15 10:16:12 +08:00
parent 077f4fd488
commit 3b0b83aae1
9 changed files with 586 additions and 288 deletions

View File

@@ -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<TrafficPoolUser[]>([]);
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<ScenarioOption[]>([]);
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<number[]>([]);
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 (
<Layout
loading={loading}
header={
<>
<NavCommon
title="流量池用户列表"
right={
<Button
onClick={() => setShowStats(s => !s)}
style={{ marginLeft: 8 }}
>
<BarChartOutlined /> {showStats ? "收起分析" : "数据分析"}
</Button>
}
/>
{/* 搜索栏 */}
<NavCommon title="流量池用户列表" />
<div className="search-bar">
<div className="search-input-wrapper">
<Input
placeholder="搜索计划名称"
value={searchInput}
onChange={e => handSearch(e.target.value)}
placeholder="搜索用户"
value={search}
onChange={e => handleSearch(e.target.value)}
prefix={<SearchOutlined />}
allowClear
size="large"
/>
</div>
<Button
onClick={() => getList()}
onClick={() => setPage(1)}
loading={loading}
size="large"
icon={<ReloadOutlined />}
></Button>
</div>
{/* 数据分析面板 */}
<DataAnalysisPanel
showStats={showStats}
setShowStats={setShowStats}
onConfirm={statsData => {
// 可以在这里处理统计数据,比如更新本地状态或发送到父组件
console.log("收到统计数据:", statsData);
}}
/>
{/* 批量操作栏 */}
<div
style={{
display: "flex",
alignItems: "center",
padding: "8px 12px 8px 26px",
background: "#fff",
borderBottom: "1px solid #f0f0f0",
}}
>
<Checkbox
checked={selectedIds.length === list.length && list.length > 0}
onChange={e => handleSelectAll(e.target.checked)}
style={{ marginRight: 8 }}
/>
<span></span>
{selectedIds.length > 0 && (
<>
<span
style={{ marginLeft: 16, color: "#1677ff" }}
>{`已选${selectedIds.length}`}</span>
<Button
size="small"
color="primary"
style={{ marginLeft: 16 }}
onClick={() => setBatchModal(true)}
>
</Button>
</>
)}
{searchInput.length > 0 && (
<>
<Button
size="small"
type="primary"
style={{ marginLeft: 16 }}
onClick={() => setBatchModal(true)}
>
</Button>
</>
)}
<div style={{ flex: 1 }} />
<Button
size="small"
style={{ marginLeft: 8 }}
onClick={() => setShowFilter(true)}
>
</Button>
</div>
</>
}
@@ -283,56 +79,14 @@ const TrafficPoolList: React.FC = () => {
<div className="pagination-container">
<Pagination
current={page}
pageSize={20}
pageSize={pageSize}
total={total}
showSizeChanger={false}
onChange={newPage => {
setPage(newPage);
getList({ page: newPage });
}}
onChange={setPage}
/>
</div>
}
>
{/* 批量加入分组弹窗 */}
<BatchAddModal
visible={batchModal}
onClose={() => setBatchModal(false)}
selectedCount={selectedIds.length}
onConfirm={data => {
// 处理批量加入逻辑
handleBatchAdd(data);
}}
/>
{/* 筛选弹窗 */}
<FilterModal
visible={showFilter}
onClose={() => 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}
/>
<div className={styles.listWrap}>
{list.length === 0 && !loading ? (
<Empty description="暂无数据" />
@@ -350,13 +104,6 @@ const TrafficPoolList: React.FC = () => {
}
>
<div className={styles.cardContent}>
<Checkbox
checked={selectedIds.includes(item.id)}
onChange={e => handleSelect(item.id, e.target.checked)}
style={{ marginRight: 8 }}
onClick={e => e.stopPropagation()}
className={styles.checkbox}
/>
<Avatar
src={item.avatar || defaultAvatar}
style={{ "--size": "60px" }}
@@ -364,7 +111,6 @@ const TrafficPoolList: React.FC = () => {
<div style={{ flex: 1 }}>
<div className={styles.title}>
{item.nickname || item.identifier}
{/* 性别icon可自行封装 */}
</div>
<div className={styles.desc}>
{item.wechatId || "-"}

View File

@@ -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");
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<TrafficPoolUser[]>([]);
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<ScenarioOption[]>([]);
// 公共筛选条件状态
const [filterParams, setFilterParams] = useState({
selectedDevices: [] as DeviceSelectionItem[],
packageId: 0,
scenarioId: 0,
userValue: 0,
userStatus: 0,
});
// 批量相关
const [selectedIds, setSelectedIds] = useState<number[]>([]);
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 (
<Layout
loading={loading}
header={
<>
<NavCommon
title="流量池用户列表"
right={
<Button
onClick={() => setShowStats(s => !s)}
style={{ marginLeft: 8 }}
>
<BarChartOutlined /> {showStats ? "收起分析" : "数据分析"}
</Button>
}
/>
{/* 搜索栏 */}
<div className="search-bar">
<div className="search-input-wrapper">
<Input
placeholder="搜索计划名称"
value={searchInput}
onChange={e => handSearch(e.target.value)}
prefix={<SearchOutlined />}
allowClear
size="large"
/>
</div>
<Button
onClick={() => getList()}
loading={loading}
size="large"
icon={<ReloadOutlined />}
></Button>
</div>
{/* 数据分析面板 */}
<DataAnalysisPanel
showStats={showStats}
setShowStats={setShowStats}
onConfirm={statsData => {
// 可以在这里处理统计数据,比如更新本地状态或发送到父组件
console.log("收到统计数据:", statsData);
}}
/>
{/* 批量操作栏 */}
<div
style={{
display: "flex",
alignItems: "center",
padding: "8px 12px 8px 26px",
background: "#fff",
borderBottom: "1px solid #f0f0f0",
}}
>
<Checkbox
checked={selectedIds.length === list.length && list.length > 0}
onChange={e => handleSelectAll(e.target.checked)}
style={{ marginRight: 8 }}
/>
<span></span>
{selectedIds.length > 0 && (
<>
<span
style={{ marginLeft: 16, color: "#1677ff" }}
>{`已选${selectedIds.length}`}</span>
<Button
size="small"
color="primary"
style={{ marginLeft: 16 }}
onClick={() => setBatchModal(true)}
>
</Button>
</>
)}
{searchInput.length > 0 && (
<>
<Button
size="small"
type="primary"
style={{ marginLeft: 16 }}
onClick={() => setBatchModal(true)}
>
</Button>
</>
)}
<div style={{ flex: 1 }} />
<Button
size="small"
style={{ marginLeft: 8 }}
onClick={() => setShowFilter(true)}
>
</Button>
</div>
</>
}
footer={
<div className="pagination-container">
<Pagination
current={page}
pageSize={20}
total={total}
showSizeChanger={false}
onChange={newPage => {
setPage(newPage);
getList({ page: newPage });
}}
/>
</div>
}
>
{/* 批量加入分组弹窗 */}
<BatchAddModal
visible={batchModal}
onClose={() => setBatchModal(false)}
selectedCount={selectedIds.length}
onConfirm={data => {
// 处理批量加入逻辑
handleBatchAdd(data);
}}
/>
{/* 筛选弹窗 */}
<FilterModal
visible={showFilter}
onClose={() => 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}
/>
<div className={styles.listWrap}>
{list.length === 0 && !loading ? (
<Empty description="暂无数据" />
) : (
<div>
{list.map(item => (
<div key={item.id} className={styles.cardWrap}>
<div
className={styles.card}
style={{ cursor: "pointer" }}
onClick={() =>
navigate(
`/mine/traffic-pool/detail/${item.wechatId}/${item.id}`,
)
}
>
<div className={styles.cardContent}>
<Checkbox
checked={selectedIds.includes(item.id)}
onChange={e => handleSelect(item.id, e.target.checked)}
style={{ marginRight: 8 }}
onClick={e => e.stopPropagation()}
className={styles.checkbox}
/>
<Avatar
src={item.avatar || defaultAvatar}
style={{ "--size": "60px" }}
/>
<div style={{ flex: 1 }}>
<div className={styles.title}>
{item.nickname || item.identifier}
{/* 性别icon可自行封装 */}
</div>
<div className={styles.desc}>
{item.wechatId || "-"}
</div>
<div className={styles.desc}>
{item.fromd || "-"}
</div>
<div className={styles.desc}>
{item.packages && item.packages.length
? item.packages.join("")
: "-"}
</div>
<div className={styles.desc}>
{item.createTime}
</div>
</div>
</div>
</div>
</div>
))}
</div>
)}
</div>
</Layout>
);
};
export default TrafficPoolList;

View File

@@ -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: <TrafficPool />,
auth: true,
},
{
path: "/mine/traffic-pool/list",
element: <TrafficPoolList />,
auth: true,
},
{
path: "/mine/traffic-pool/detail/:wxid/:userId",
element: <TrafficPoolDetail />,