刪除帳號和設備列表彈窗組件,新增流量池列表彈窗組件,並更新樣式以支持新的彈窗功能,優化流量分配列表的顯示邏輯。

This commit is contained in:
超级老白兔
2025-08-14 16:53:16 +08:00
parent 27b9ef910c
commit 7be8ff2e94
6 changed files with 524 additions and 62 deletions

View File

@@ -26,3 +26,8 @@ export function toggleDistributionRuleStatus(
export function deleteDistributionRule(id: number): Promise<any> {
return request("/v1/workbench/delete", { id }, "POST");
}
// 获取流量分发规则详情
export function fetchDistributionRuleDetail(id: number): Promise<any> {
return request(`/v1/workbench/detail?id=${id}`, {}, "GET");
}

View File

@@ -1,8 +1,9 @@
import React from "react";
import { Popup, Avatar } from "antd-mobile";
import { Button } from "antd";
import React, { useEffect, useState } from "react";
import { Popup, Avatar, SpinLoading } from "antd-mobile";
import { Button, message } from "antd";
import { CloseOutlined } from "@ant-design/icons";
import style from "./index.module.scss";
import style from "../index.module.scss";
import { fetchDistributionRuleDetail } from "../api";
interface AccountItem {
id: string | number;
@@ -15,16 +16,44 @@ interface AccountItem {
interface AccountListModalProps {
visible: boolean;
onClose: () => void;
accounts: AccountItem[];
title?: string;
ruleId?: number;
ruleName?: string;
}
const AccountListModal: React.FC<AccountListModalProps> = ({
visible,
onClose,
accounts,
title = "分发账号列表",
ruleId,
ruleName,
}) => {
const [accounts, setAccounts] = useState<AccountItem[]>([]);
const [loading, setLoading] = useState(false);
// 获取账号数据
const fetchAccounts = async () => {
if (!ruleId) return;
setLoading(true);
try {
const detailRes = await fetchDistributionRuleDetail(ruleId);
const accountData = detailRes?.config?.accountGroupsOptions || [];
setAccounts(accountData);
} catch (error) {
console.error("获取账号详情失败:", error);
message.error("获取账号详情失败");
} finally {
setLoading(false);
}
};
// 当弹窗打开且有ruleId时获取数据
useEffect(() => {
if (visible && ruleId) {
fetchAccounts();
}
}, [visible, ruleId]);
const title = ruleName ? `${ruleName} - 分发账号列表` : "分发账号列表";
const getStatusColor = (status?: string) => {
switch (status) {
case "normal":
@@ -76,13 +105,24 @@ const AccountListModal: React.FC<AccountListModalProps> = ({
{/* 账号列表 */}
<div className={style.accountList}>
{accounts.length > 0 ? (
{loading ? (
<div className={style.accountLoading}>
<SpinLoading color="primary" />
<div className={style.accountLoadingText}>
...
</div>
</div>
) : accounts.length > 0 ? (
accounts.map((account, index) => (
<div key={account.id || index} className={style.accountItem}>
<div className={style.accountAvatar}>
<Avatar src={account.avatar} style={{ "--size": "48px" }}>
{(account.nickname || account.wechatId || "账号")[0]}
</Avatar>
<Avatar
src={account.avatar}
style={{ "--size": "48px" }}
fallback={
(account.nickname || account.wechatId || "账号")[0]
}
/>
</div>
<div className={style.accountInfo}>
<div className={style.accountName}>

View File

@@ -1,8 +1,9 @@
import React from "react";
import { Popup, Avatar } from "antd-mobile";
import { Button } from "antd";
import React, { useEffect, useState } from "react";
import { Popup, Avatar, SpinLoading } from "antd-mobile";
import { Button, message } from "antd";
import { CloseOutlined } from "@ant-design/icons";
import style from "./index.module.scss";
import style from "../index.module.scss";
import { fetchDistributionRuleDetail } from "../api";
interface DeviceItem {
id: string | number;
@@ -17,16 +18,44 @@ interface DeviceItem {
interface DeviceListModalProps {
visible: boolean;
onClose: () => void;
devices: DeviceItem[];
title?: string;
ruleId?: number;
ruleName?: string;
}
const DeviceListModal: React.FC<DeviceListModalProps> = ({
visible,
onClose,
devices,
title = "分发设备列表",
ruleId,
ruleName,
}) => {
const [devices, setDevices] = useState<DeviceItem[]>([]);
const [loading, setLoading] = useState(false);
// 获取设备数据
const fetchDevices = async () => {
if (!ruleId) return;
setLoading(true);
try {
const detailRes = await fetchDistributionRuleDetail(ruleId);
const deviceData = detailRes?.config?.deveiceGroupsOptions || [];
setDevices(deviceData);
} catch (error) {
console.error("获取设备详情失败:", error);
message.error("获取设备详情失败");
} finally {
setLoading(false);
}
};
// 当弹窗打开且有ruleId时获取数据
useEffect(() => {
if (visible && ruleId) {
fetchDevices();
}
}, [visible, ruleId]);
const title = ruleName ? `${ruleName} - 分发设备列表` : "分发设备列表";
const getStatusColor = (status?: string) => {
return status === "online" ? "#52c41a" : "#ff4d4f";
};
@@ -60,7 +89,12 @@ const DeviceListModal: React.FC<DeviceListModalProps> = ({
{/* 设备列表 */}
<div className={style.deviceList}>
{devices.length > 0 ? (
{loading ? (
<div className={style.deviceLoading}>
<SpinLoading color="primary" />
<div className={style.deviceLoadingText}>...</div>
</div>
) : devices.length > 0 ? (
devices.map((device, index) => (
<div key={device.id || index} className={style.deviceItem}>
{/* 顶部行IMEI */}

View File

@@ -0,0 +1,170 @@
import React, { useEffect, useState } from "react";
import { Popup, Avatar, SpinLoading } from "antd-mobile";
import { Button, message } from "antd";
import { CloseOutlined } from "@ant-design/icons";
import style from "../index.module.scss";
import { fetchDistributionRuleDetail } from "../api";
interface PoolItem {
id: string | number;
name?: string;
description?: string;
userCount?: number;
tags?: string[];
createdAt?: string;
deviceIds?: string[];
}
interface PoolListModalProps {
visible: boolean;
onClose: () => void;
ruleId?: number;
ruleName?: string;
}
const PoolListModal: React.FC<PoolListModalProps> = ({
visible,
onClose,
ruleId,
ruleName,
}) => {
const [pools, setPools] = useState<PoolItem[]>([]);
const [loading, setLoading] = useState(false);
// 获取流量池数据
const fetchPools = async () => {
if (!ruleId) return;
setLoading(true);
try {
const detailRes = await fetchDistributionRuleDetail(ruleId);
const poolData = detailRes?.config?.pools || [];
const formattedPools = poolData.map((pool: any) => ({
id: pool.id || pool.poolId,
name: pool.name || pool.poolName || `流量池${pool.id}`,
description: pool.description || pool.desc || "",
userCount: pool.userCount || pool.count || 0,
tags: pool.tags || [],
createdAt: pool.createdAt || pool.createTime || "",
deviceIds: pool.deviceIds || [],
}));
setPools(formattedPools);
} catch (error) {
console.error("获取流量池详情失败:", error);
message.error("获取流量池详情失败");
} finally {
setLoading(false);
}
};
// 当弹窗打开且有ruleId时获取数据
useEffect(() => {
if (visible && ruleId) {
fetchPools();
}
}, [visible, ruleId]);
const title = ruleName ? `${ruleName} - 流量池列表` : "流量池列表";
return (
<Popup
visible={visible}
onMaskClick={onClose}
position="bottom"
bodyStyle={{
height: "70vh",
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
}}
>
<div className={style.poolModal}>
{/* 头部 */}
<div className={style.poolModalHeader}>
<h3 className={style.poolModalTitle}>{title}</h3>
<Button
type="text"
icon={<CloseOutlined />}
onClick={onClose}
className={style.poolModalClose}
/>
</div>
{/* 流量池列表 */}
<div className={style.poolList}>
{loading ? (
<div className={style.poolLoading}>
<SpinLoading color="primary" />
<div className={style.poolLoadingText}>...</div>
</div>
) : pools.length > 0 ? (
pools.map((pool, index) => (
<div key={pool.id || index} className={style.poolItem}>
{/* 流量池信息 */}
<div className={style.poolMainContent}>
{/* 图标 */}
<div className={style.poolIcon}>
<span className={style.poolIconText}>
{(pool.name || "池")[0]}
</span>
</div>
{/* 流量池信息 */}
<div className={style.poolInfo}>
<div className={style.poolInfoHeader}>
<h3 className={style.poolName}>
{pool.name || `流量池${pool.id}`}
</h3>
<span className={style.poolUserCount}>
{pool.userCount || 0}
</span>
</div>
<div className={style.poolInfoList}>
<div className={style.poolInfoItem}>
<span className={style.poolInfoLabel}>:</span>
<span className={style.poolInfoValue}>
{pool.description || "暂无描述"}
</span>
</div>
<div className={style.poolInfoItem}>
<span className={style.poolInfoLabel}>:</span>
<span className={style.poolInfoValue}>
{pool.createdAt || "-"}
</span>
</div>
</div>
{/* 标签 */}
{pool.tags && pool.tags.length > 0 && (
<div className={style.poolTags}>
{pool.tags.map((tag, tagIndex) => (
<span key={tagIndex} className={style.poolTag}>
{tag}
</span>
))}
</div>
)}
</div>
</div>
</div>
))
) : (
<div className={style.poolEmpty}>
<div className={style.poolEmptyText}></div>
</div>
)}
</div>
{/* 底部统计 */}
<div className={style.poolModalFooter}>
<div className={style.poolStats}>
<span> {pools.length} </span>
</div>
</div>
</div>
</Popup>
);
};
export default PoolListModal;

View File

@@ -235,6 +235,22 @@
font-size: 16px;
}
.accountLoading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
gap: 16px;
padding: 20px;
}
.accountLoadingText {
font-size: 15px;
color: #666;
font-weight: 500;
}
.accountModalFooter {
padding: 16px 20px;
border-top: 1px solid #f0f0f0;
@@ -428,6 +444,22 @@
font-size: 16px;
}
.deviceLoading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
gap: 16px;
padding: 20px;
}
.deviceLoadingText {
font-size: 15px;
color: #666;
font-weight: 500;
}
.deviceModalFooter {
padding: 16px 20px;
border-top: 1px solid #f0f0f0;
@@ -439,3 +471,194 @@
font-size: 14px;
color: #666;
}
// 流量池列表弹窗样式
.poolModal {
height: 100%;
display: flex;
flex-direction: column;
}
.poolModalHeader {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid #f0f0f0;
background: #fff;
}
.poolModalTitle {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #222;
}
.poolModalClose {
border: none;
background: none;
color: #888;
font-size: 16px;
}
.poolList {
flex: 1;
overflow-y: auto;
padding: 0 20px;
}
.poolItem {
background: #fff;
border-radius: 12px;
padding: 12px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
border: 1px solid #ececec;
transition: all 0.2s ease;
}
.poolItem:hover {
transform: translateY(-1px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
.poolMainContent {
display: flex;
align-items: flex-start;
}
.poolIcon {
width: 48px;
height: 48px;
border-radius: 12px;
background: linear-gradient(135deg, #1890ff 0%, #722ed1 100%);
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
flex-shrink: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.poolIconText {
color: #fff;
font-size: 18px;
font-weight: 600;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.poolInfo {
flex: 1;
min-width: 0;
}
.poolInfoHeader {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
}
.poolName {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #222;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
margin-right: 8px;
}
.poolUserCount {
padding: 2px 8px;
border-radius: 10px;
font-size: 12px;
font-weight: 500;
background: rgba(24, 144, 255, 0.1);
color: #1890ff;
flex-shrink: 0;
}
.poolInfoList {
display: flex;
flex-direction: column;
gap: 4px;
margin-bottom: 8px;
}
.poolInfoItem {
display: flex;
align-items: center;
font-size: 13px;
}
.poolInfoLabel {
color: #888;
margin-right: 6px;
min-width: 60px;
}
.poolInfoValue {
color: #444;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.poolTags {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.poolTag {
padding: 2px 8px;
border-radius: 10px;
font-size: 11px;
background: rgba(0, 0, 0, 0.05);
color: #666;
border: 1px solid rgba(0, 0, 0, 0.1);
}
.poolLoading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
gap: 16px;
padding: 20px;
}
.poolLoadingText {
font-size: 15px;
color: #666;
font-weight: 500;
}
.poolEmpty {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
color: #888;
}
.poolEmptyText {
font-size: 16px;
}
.poolModalFooter {
padding: 16px 20px;
border-top: 1px solid #f0f0f0;
background: #fff;
}
.poolStats {
text-align: center;
font-size: 14px;
color: #666;
}

View File

@@ -32,8 +32,9 @@ import {
} from "@ant-design/icons";
import style from "./index.module.scss";
import { useNavigate } from "react-router-dom";
import AccountListModal from "./AccountListModal";
import DeviceListModal from "./DeviceListModal";
import AccountListModal from "./components/AccountListModal";
import DeviceListModal from "./components/DeviceListModal";
import PoolListModal from "./components/PoolListModal";
const PAGE_SIZE = 10;
@@ -53,14 +54,11 @@ const TrafficDistributionList: React.FC = () => {
const [searchQuery, setSearchQuery] = useState("");
// 优化用menuLoadingId标记当前操作的item
const [menuLoadingId, setMenuLoadingId] = useState<number | null>(null);
// 账号列表弹窗
// 弹窗控制
const [accountModalVisible, setAccountModalVisible] = useState(false);
const [currentAccounts, setCurrentAccounts] = useState<any[]>([]);
const [currentRuleName, setCurrentRuleName] = useState("");
// 设备列表弹窗
const [deviceModalVisible, setDeviceModalVisible] = useState(false);
const [currentDevices, setCurrentDevices] = useState<any[]>([]);
const [currentDeviceRuleName, setCurrentDeviceRuleName] = useState("");
const [poolModalVisible, setPoolModalVisible] = useState(false);
const [currentRule, setCurrentRule] = useState<DistributionRule | null>(null);
const navigate = useNavigate();
useEffect(() => {
@@ -140,42 +138,22 @@ const TrafficDistributionList: React.FC = () => {
// 显示账号列表弹窗
const showAccountList = (item: DistributionRule) => {
// 这里需要根据实际的账号数据结构来转换
// 假设 item.config.account 是账号ID数组需要转换为账号对象数组
const accounts = (item.config?.account || []).map(
(accountId: string | number) => ({
id: accountId,
nickname: `账号${accountId}`,
wechatId: `wx_${accountId}`,
avatar: "",
status: "normal",
}),
);
setCurrentAccounts(accounts);
setCurrentRuleName(item.name);
setCurrentRule(item);
setAccountModalVisible(true);
};
// 显示设备列表弹窗
const showDeviceList = (item: DistributionRule) => {
// 这里需要根据实际的设备数据结构来转换
// 假设 item.config.devices 是设备ID数组需要转换为设备对象数组
const devices = (item.config?.devices || []).map((deviceId: string) => ({
id: deviceId,
memo: `设备${deviceId}`,
imei: `IMEI${deviceId}`,
wechatId: `wx_${deviceId}`,
status: "online" as const,
avatar: "",
totalFriend: Math.floor(Math.random() * 1000) + 100,
}));
setCurrentDevices(devices);
setCurrentDeviceRuleName(item.name);
setCurrentRule(item);
setDeviceModalVisible(true);
};
// 显示流量池列表弹窗
const showPoolList = (item: DistributionRule) => {
setCurrentRule(item);
setPoolModalVisible(true);
};
const renderCard = (item: DistributionRule) => {
const menu = (
<Menu onClick={({ key }) => handleMenuClick(key, item)}>
@@ -281,7 +259,11 @@ const TrafficDistributionList: React.FC = () => {
</div>
<div style={{ fontSize: 13, color: "#888" }}></div>
</div>
<div className={style.ruleMetaItem}>
<div
className={style.ruleMetaItem}
style={{ cursor: "pointer" }}
onClick={() => showPoolList(item)}
>
<div style={{ fontSize: 18, fontWeight: 600 }}>
{item.config?.pools?.length || 0}
</div>
@@ -386,16 +368,24 @@ const TrafficDistributionList: React.FC = () => {
<AccountListModal
visible={accountModalVisible}
onClose={() => setAccountModalVisible(false)}
accounts={currentAccounts}
title={`${currentRuleName} - 分发账号列表`}
ruleId={currentRule?.id}
ruleName={currentRule?.name}
/>
{/* 设备列表弹窗 */}
<DeviceListModal
visible={deviceModalVisible}
onClose={() => setDeviceModalVisible(false)}
devices={currentDevices}
title={`${currentDeviceRuleName} - 分发设备列表`}
ruleId={currentRule?.id}
ruleName={currentRule?.name}
/>
{/* 流量池列表弹窗 */}
<PoolListModal
visible={poolModalVisible}
onClose={() => setPoolModalVisible(false)}
ruleId={currentRule?.id}
ruleName={currentRule?.name}
/>
</Layout>
);