FEAT => 本次更新项目为:

This commit is contained in:
超级老白兔
2025-09-22 10:49:21 +08:00
parent a9c41aa9c3
commit ce1eddad68
5 changed files with 279 additions and 290 deletions

View File

@@ -1,28 +1,54 @@
import React from "react";
import React, { useState, useEffect } from "react";
import { Popup, Selector, Button } from "antd-mobile";
import { fetchPackageOptions } from "./api";
import type { PackageOption } from "./data";
interface BatchAddModalProps {
visible: boolean;
onClose: () => void;
packageOptions: PackageOption[];
batchTarget: string;
setBatchTarget: (v: string) => void;
selectedCount: number;
onConfirm: (options) => void;
onConfirm: (data: {
packageOptions: PackageOption[];
selectedPackageId: string;
}) => void;
}
const BatchAddModal: React.FC<BatchAddModalProps> = ({
visible,
onClose,
packageOptions = [],
batchTarget,
setBatchTarget,
selectedCount,
onConfirm,
}) => {
const handSubmit = () => {
onConfirm(packageOptions);
const [packageOptions, setPackageOptions] = useState<PackageOption[]>([]);
const [selectedPackageId, setSelectedPackageId] = useState<string>("");
const [loading, setLoading] = useState(false);
// 获取分组选项
useEffect(() => {
if (visible) {
setLoading(true);
fetchPackageOptions()
.then(res => {
setPackageOptions(res.list || []);
})
.catch(error => {
console.error("获取分组选项失败:", error);
})
.finally(() => {
setLoading(false);
});
}
}, [visible]);
const handleSubmit = () => {
if (!selectedPackageId) {
// 可以添加提示
return;
}
onConfirm({
packageOptions,
selectedPackageId,
});
};
return (
<Popup
@@ -33,11 +59,15 @@ const BatchAddModal: React.FC<BatchAddModalProps> = ({
>
<div style={{ marginBottom: 12, padding: 10 }}>
<div style={{ marginBottom: 12 }}></div>
<Selector
options={packageOptions.map(p => ({ label: p.name, value: p.id }))}
value={[batchTarget]}
onChange={v => setBatchTarget(v[0])}
/>
{loading ? (
<div style={{ textAlign: "center", padding: 20 }}>...</div>
) : (
<Selector
options={packageOptions.map(p => ({ label: p.name, value: p.id }))}
value={[selectedPackageId]}
onChange={v => setSelectedPackageId(v[0])}
/>
)}
<div
style={{
color: "#888",
@@ -48,7 +78,12 @@ const BatchAddModal: React.FC<BatchAddModalProps> = ({
>
{selectedCount}
</div>
<Button onClick={handSubmit} color="primary" block>
<Button
onClick={handleSubmit}
color="primary"
block
disabled={!selectedPackageId || loading}
>
</Button>
</div>

View File

@@ -1,24 +1,77 @@
import React from "react";
import React, { useState, useEffect, useMemo } from "react";
import { Card, Button } from "antd-mobile";
import { fetchTrafficPoolList } from "./api";
import type { TrafficPoolUser } from "./data";
interface DataAnalysisPanelProps {
stats: {
showStats: boolean;
setShowStats: (v: boolean) => void;
onConfirm: (stats: {
total: number;
highValue: number;
added: number;
pending: number;
failed: number;
addSuccessRate: number;
};
showStats: boolean;
setShowStats: (v: boolean) => void;
}) => void;
}
const DataAnalysisPanel: React.FC<DataAnalysisPanelProps> = ({
stats,
showStats,
setShowStats,
onConfirm,
}) => {
const [list, setList] = useState<TrafficPoolUser[]>([]);
const [loading, setLoading] = useState(false);
// 计算统计数据
const stats = useMemo(() => {
const total = list.length;
const highValue = list.filter(
u => u.tags && u.tags.includes("高价值客户池"),
).length;
const added = list.filter(u => u.status === 1).length;
const pending = list.filter(u => u.status === 0).length;
const failed = list.filter(u => u.status === -1).length;
const addSuccessRate = total ? Math.round((added / total) * 100) : 0;
return { total, highValue, added, pending, failed, addSuccessRate };
}, [list]);
// 获取数据
useEffect(() => {
if (showStats) {
setLoading(true);
fetchTrafficPoolList({ page: 1, pageSize: 1000 }) // 获取所有数据进行统计
.then(res => {
setList(res.list || []);
// 通过 onConfirm 抛出统计数据
const total = res.list?.length || 0;
const highValue =
res.list?.filter(u => u.tags && u.tags.includes("高价值客户池"))
.length || 0;
const added = res.list?.filter(u => u.status === 1).length || 0;
const pending = res.list?.filter(u => u.status === 0).length || 0;
const failed = res.list?.filter(u => u.status === -1).length || 0;
const addSuccessRate = total ? Math.round((added / total) * 100) : 0;
onConfirm({
total,
highValue,
added,
pending,
failed,
addSuccessRate,
});
})
.catch(error => {
console.error("获取统计数据失败:", error);
})
.finally(() => {
setLoading(false);
});
}
}, [showStats, onConfirm]);
if (!showStats) return null;
return (
<div
@@ -30,46 +83,54 @@ const DataAnalysisPanel: React.FC<DataAnalysisPanelProps> = ({
boxShadow: "0 2px 8px rgba(0,0,0,0.04)",
}}
>
<div style={{ display: "flex", gap: 16, marginBottom: 12 }}>
<Card style={{ flex: 1 }}>
<div style={{ fontSize: 18, fontWeight: 600, color: "#1677ff" }}>
{stats.total}
{loading ? (
<div style={{ textAlign: "center", padding: 20 }}>
...
</div>
) : (
<>
<div style={{ display: "flex", gap: 16, marginBottom: 12 }}>
<Card style={{ flex: 1 }}>
<div style={{ fontSize: 18, fontWeight: 600, color: "#1677ff" }}>
{stats.total}
</div>
<div style={{ fontSize: 13, color: "#888" }}></div>
</Card>
<Card style={{ flex: 1 }}>
<div style={{ fontSize: 18, fontWeight: 600, color: "#eb2f96" }}>
{stats.highValue}
</div>
<div style={{ fontSize: 13, color: "#888" }}></div>
</Card>
</div>
<div style={{ fontSize: 13, color: "#888" }}></div>
</Card>
<Card style={{ flex: 1 }}>
<div style={{ fontSize: 18, fontWeight: 600, color: "#eb2f96" }}>
{stats.highValue}
<div style={{ display: "flex", gap: 16 }}>
<Card style={{ flex: 1 }}>
<div style={{ fontSize: 18, fontWeight: 600, color: "#52c41a" }}>
{stats.addSuccessRate}%
</div>
<div style={{ fontSize: 13, color: "#888" }}></div>
</Card>
<Card style={{ flex: 1 }}>
<div style={{ fontSize: 18, fontWeight: 600, color: "#faad14" }}>
{stats.added}
</div>
<div style={{ fontSize: 13, color: "#888" }}></div>
</Card>
<Card style={{ flex: 1 }}>
<div style={{ fontSize: 18, fontWeight: 600, color: "#bfbfbf" }}>
{stats.pending}
</div>
<div style={{ fontSize: 13, color: "#888" }}></div>
</Card>
<Card style={{ flex: 1 }}>
<div style={{ fontSize: 18, fontWeight: 600, color: "#ff4d4f" }}>
{stats.failed}
</div>
<div style={{ fontSize: 13, color: "#888" }}></div>
</Card>
</div>
<div style={{ fontSize: 13, color: "#888" }}></div>
</Card>
</div>
<div style={{ display: "flex", gap: 16 }}>
<Card style={{ flex: 1 }}>
<div style={{ fontSize: 18, fontWeight: 600, color: "#52c41a" }}>
{stats.addSuccessRate}%
</div>
<div style={{ fontSize: 13, color: "#888" }}></div>
</Card>
<Card style={{ flex: 1 }}>
<div style={{ fontSize: 18, fontWeight: 600, color: "#faad14" }}>
{stats.added}
</div>
<div style={{ fontSize: 13, color: "#888" }}></div>
</Card>
<Card style={{ flex: 1 }}>
<div style={{ fontSize: 18, fontWeight: 600, color: "#bfbfbf" }}>
{stats.pending}
</div>
<div style={{ fontSize: 13, color: "#888" }}></div>
</Card>
<Card style={{ flex: 1 }}>
<div style={{ fontSize: 18, fontWeight: 600, color: "#ff4d4f" }}>
{stats.failed}
</div>
<div style={{ fontSize: 13, color: "#888" }}></div>
</Card>
</div>
</>
)}
<Button
size="small"
style={{ marginTop: 12 }}

View File

@@ -2,7 +2,7 @@ import React, { useState, useEffect } from "react";
import { Popup } from "antd-mobile";
import { Select, Button } from "antd";
import DeviceSelection from "@/components/DeviceSelection";
import type { UserStatus, ScenarioOption } from "./data";
import type { ScenarioOption } from "./data";
import { fetchScenarioOptions, fetchPackageOptions } from "./api";
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
@@ -10,11 +10,11 @@ interface FilterModalProps {
visible: boolean;
onClose: () => void;
onConfirm: (filters: {
deviceIds: string[];
packageId: string;
scenarioId: string;
deviceld: string[]; // 更新为 deviceld
packageld: string; // 更新为 packageld
taskId: string; // 更新为 taskId
userValue: number;
userStatus: number;
addStatus: number; // 更新为 addStatus
}) => void;
scenarioOptions: ScenarioOption[];
}
@@ -72,11 +72,11 @@ const FilterModal: React.FC<FilterModalProps> = ({
const handleApply = () => {
const params = {
deviceIds: selectedDevices.map(d => d.id.toString()),
packageId,
scenarioId,
deviceld: selectedDevices.map(d => d.id.toString()), // 更新为 deviceld
packageld: packageId, // 更新为 packageld
taskId: scenarioId, // 更新为 taskId
userValue,
userStatus,
addStatus: userStatus, // 更新为 addStatus
};
console.log(params);

View File

@@ -1,183 +0,0 @@
import { useState, useEffect, useMemo } from "react";
import {
fetchTrafficPoolList,
fetchPackageOptions,
fetchScenarioOptions,
} from "./api";
import type { TrafficPoolUser, PackageOption, ScenarioOption } from "./data";
export function useTrafficPoolListLogic() {
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 [packageOptions, setPackageOptions] = useState<PackageOption[]>([]);
const [scenarioOptions, setScenarioOptions] = useState<ScenarioOption[]>([]);
const [selectedDevices, setSelectedDevices] = useState<any[]>([]);
const [packageId, setPackageId] = useState<number>(0);
const [scenarioId, setScenarioId] = useState<number>(0);
const [userValue, setUserValue] = useState<number>(0);
const [userStatus, setUserStatus] = useState<number>(0);
// 批量相关
const [selectedIds, setSelectedIds] = useState<number[]>([]);
const [batchModal, setBatchModal] = useState(false);
const [batchTarget, setBatchTarget] = useState<string>("");
// 数据分析
const [showStats, setShowStats] = useState(false);
const stats = useMemo(() => {
const total = list.length;
const highValue = list.filter(
u => u.tags && u.tags.includes("高价值客户池"),
).length;
const added = list.filter(u => u.status === 1).length;
const pending = list.filter(u => u.status === 0).length;
const failed = list.filter(u => u.status === -1).length;
const addSuccessRate = total ? Math.round((added / total) * 100) : 0;
return { total, highValue, added, pending, failed, addSuccessRate };
}, [list]);
// 获取列表
const getList = async () => {
setLoading(true);
try {
const params: any = {
page,
pageSize,
keyword: search,
packageId,
taskId: scenarioId,
userValue,
addStatus: userStatus,
};
// 添加筛选参数
if (selectedDevices.length > 0) {
params.deviceId = selectedDevices.map(d => d.id).join(",");
}
const res = await fetchTrafficPoolList(params);
setList(res.list || []);
setTotal(res.total || 0);
} catch (error) {
// 忽略请求过于频繁的错误,避免页面崩溃
if (error !== "请求过于频繁,请稍后再试") {
console.error("获取列表失败:", error);
}
} finally {
setLoading(false);
}
};
// 获取筛选项
useEffect(() => {
fetchPackageOptions().then(res => {
setPackageOptions(res.list || []);
});
fetchScenarioOptions().then(res => {
setScenarioOptions(res.list || []);
});
}, []);
// 筛选条件变化时刷新列表
useEffect(() => {
getList();
// eslint-disable-next-line
}, [
page,
search,
selectedDevices,
packageId,
scenarioId,
userValue,
userStatus,
]);
// 全选/反选
const handleSelectAll = (checked: boolean) => {
if (checked) {
setSelectedIds(list.map(item => item.id));
} else {
setSelectedIds([]);
}
};
// 单选
const handleSelect = (id: number, checked: boolean) => {
setSelectedIds(prev =>
checked ? [...prev, id] : prev.filter(i => i !== id),
);
};
// 批量加入分组/流量池
const handleBatchAdd = options => {
console.log("批量加入分组", options);
// if (!batchTarget) {
// Toast.show({ content: "请选择目标分组", position: "top" });
// return;
// }
// // TODO: 调用后端批量接口,这里仅模拟
// Toast.show({
// content: `已将${selectedIds.length}个用户加入${packageOptions.find(p => p.id === batchTarget)?.name || ""}`,
// position: "top",
// });
// setBatchModal(false);
// setSelectedIds([]);
// setBatchTarget("");
// 可刷新列表
};
// 筛选重置
const resetFilter = () => {
setSelectedDevices([]);
setPackageId(0);
setScenarioId(0);
setUserValue(0);
setUserStatus(0);
};
return {
loading,
list,
page,
setPage,
pageSize,
total,
search,
setSearch,
showFilter,
setShowFilter,
packageOptions,
scenarioOptions,
selectedDevices,
setSelectedDevices,
packageId,
setPackageId,
scenarioId,
setScenarioId,
userValue,
setUserValue,
userStatus,
setUserStatus,
selectedIds,
setSelectedIds,
handleSelectAll,
handleSelect,
batchModal,
setBatchModal,
batchTarget,
setBatchTarget,
handleBatchAdd,
showStats,
setShowStats,
stats,
getList,
resetFilter,
};
}

View File

@@ -10,7 +10,8 @@ import styles from "./index.module.scss";
import { Empty, Avatar } from "antd-mobile";
import { useNavigate } from "react-router-dom";
import NavCommon from "@/components/NavCommon";
import { useTrafficPoolListLogic } from "./dataAnyx";
import { fetchTrafficPoolList, fetchScenarioOptions } from "./api";
import type { TrafficPoolUser, ScenarioOption } from "./data";
import DataAnalysisPanel from "./DataAnalysisPanel";
import FilterModal from "./FilterModal";
import BatchAddModal from "./BatchAddModal";
@@ -20,36 +21,107 @@ const defaultAvatar =
const TrafficPoolList: React.FC = () => {
const navigate = useNavigate();
const {
loading,
list,
// 基础状态
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 [selectedDevices, setSelectedDevices] = useState<any[]>([]);
const [packageId, setPackageId] = useState<number>(0);
const [scenarioId, setScenarioId] = useState<number>(0);
const [userValue, setUserValue] = useState<number>(0);
const [userStatus, setUserStatus] = useState<number>(0);
// 批量相关
const [selectedIds, setSelectedIds] = useState<number[]>([]);
const [batchModal, setBatchModal] = useState(false);
// 数据分析
const [showStats, setShowStats] = useState(false);
// 获取列表
const getList = async () => {
setLoading(true);
try {
const params: any = {
page,
pageSize,
keyword: search,
packageld: packageId, // 更新为 packageld
taskId: scenarioId,
userValue,
addStatus: userStatus,
};
// 添加筛选参数
if (selectedDevices.length > 0) {
params.deviceld = selectedDevices.map(d => d.id).join(","); // 更新为 deviceld
}
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 || []);
});
}, []);
// 筛选条件变化时刷新列表
useEffect(() => {
getList();
// eslint-disable-next-line
}, [
page,
setPage,
total,
search,
setSearch,
showFilter,
setShowFilter,
packageOptions,
scenarioOptions,
setSelectedDevices,
setPackageId,
setScenarioId,
setUserValue,
setUserStatus,
selectedIds,
handleSelectAll,
handleSelect,
batchModal,
setBatchModal,
batchTarget,
setBatchTarget,
handleBatchAdd,
showStats,
setShowStats,
stats,
getList,
} = useTrafficPoolListLogic();
selectedDevices,
packageId,
scenarioId,
userValue,
userStatus,
]);
// 全选/反选
const handleSelectAll = (checked: boolean) => {
if (checked) {
setSelectedIds(list.map(item => item.id));
} else {
setSelectedIds([]);
}
};
// 单选
const handleSelect = (id: number, checked: boolean) => {
setSelectedIds(prev =>
checked ? [...prev, id] : prev.filter(i => i !== id),
);
};
// 批量加入分组/流量池
const handleBatchAdd = options => {
console.log("批量加入分组", options);
// TODO: 实现批量加入逻辑
setBatchModal(false);
setSelectedIds([]);
};
// 搜索防抖处理
const [searchInput, setSearchInput] = useState(search);
@@ -104,9 +176,12 @@ const TrafficPoolList: React.FC = () => {
</div>
{/* 数据分析面板 */}
<DataAnalysisPanel
stats={stats}
showStats={showStats}
setShowStats={setShowStats}
onConfirm={statsData => {
// 可以在这里处理统计数据,比如更新本地状态或发送到父组件
console.log("收到统计数据:", statsData);
}}
/>
{/* 批量操作栏 */}
@@ -167,11 +242,12 @@ const TrafficPoolList: React.FC = () => {
<BatchAddModal
visible={batchModal}
onClose={() => setBatchModal(false)}
packageOptions={packageOptions}
batchTarget={batchTarget}
setBatchTarget={setBatchTarget}
selectedCount={selectedIds.length}
onConfirm={handleBatchAdd}
onConfirm={data => {
console.log("收到批量操作数据:", data);
// 处理批量加入逻辑
handleBatchAdd(data);
}}
/>
{/* 筛选弹窗 */}
<FilterModal
@@ -180,7 +256,7 @@ const TrafficPoolList: React.FC = () => {
onConfirm={filters => {
// 更新筛选条件
setSelectedDevices(
filters.deviceIds.map(id => ({
filters.deviceld.map(id => ({
id: parseInt(id),
memo: "",
imei: "",
@@ -188,10 +264,10 @@ const TrafficPoolList: React.FC = () => {
status: "offline" as const,
})),
);
setPackageId(filters.packageId ? parseInt(filters.packageId) : 0);
setScenarioId(filters.scenarioId ? parseInt(filters.scenarioId) : 0);
setPackageId(filters.packageld ? parseInt(filters.packageld) : 0);
setScenarioId(filters.taskId ? parseInt(filters.taskId) : 0);
setUserValue(filters.userValue);
setUserStatus(filters.userStatus);
setUserStatus(filters.addStatus);
// 重新获取列表
getList();
}}