新增流量分發記錄功能,更新樣式以改善搜索條和分發統計彈窗的顯示效果,並在列表中整合相應的功能邏輯。

This commit is contained in:
超级老白兔
2025-08-16 17:34:36 +08:00
parent 7dd20b0a9b
commit 8af3ad3549
4 changed files with 299 additions and 1 deletions

View File

@@ -31,3 +31,13 @@ export function deleteDistributionRule(id: number): Promise<any> {
export function fetchDistributionRuleDetail(id: number): Promise<any> {
return request(`/v1/workbench/detail?id=${id}`, {}, "GET");
}
//流量分发记录
export function fetchTransferFriends(params: {
page?: number;
limit?: number;
keyword?: string;
workbenchId: number;
}) {
return request("/v1/workbench/transfer-friends", params, "GET");
}

View File

@@ -0,0 +1,232 @@
import React, { useEffect, useState } from "react";
import { Popup, Avatar, SpinLoading, Input } from "antd-mobile";
import { Button, message, Pagination } from "antd";
import { CloseOutlined, SearchOutlined } from "@ant-design/icons";
import style from "../index.module.scss";
import { fetchTransferFriends } from "../api";
interface SendRecordItem {
id: string | number;
nickname?: string;
wechatId?: string;
avatar?: string;
status?: string;
isRecycle?: number;
sendTime?: string;
sendCount?: number;
}
interface SendRcrodModalProps {
visible: boolean;
onClose: () => void;
ruleId?: number;
ruleName?: string;
}
const SendRcrodModal: React.FC<SendRcrodModalProps> = ({
visible,
onClose,
ruleId,
ruleName,
}) => {
const [sendRecords, setSendRecords] = useState<SendRecordItem[]>([]);
const [loading, setLoading] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const [searchKeyword, setSearchKeyword] = useState("");
const [currentPage, setCurrentPage] = useState(1);
const [total, setTotal] = useState(0);
const pageSize = 20;
// 获取分发记录数据
const fetchSendRecords = async (page = 1, keyword = "") => {
if (!ruleId || !visible) return;
setLoading(true);
try {
const detailRes = await fetchTransferFriends({
workbenchId: ruleId,
page,
limit: pageSize,
keyword,
});
console.log(detailRes);
const recordData = detailRes.list || [];
setSendRecords(recordData);
setTotal(detailRes.total || 0);
} catch (error) {
console.error("获取分发记录失败:", error);
message.error("获取分发记录失败");
} finally {
setLoading(false);
}
};
// 当弹窗打开且有ruleId时获取数据
useEffect(() => {
if (visible && ruleId) {
setCurrentPage(1);
setSearchQuery("");
setSearchKeyword("");
fetchSendRecords(1, "");
}
}, [visible, ruleId]);
// 搜索关键词变化时触发搜索
useEffect(() => {
if (!visible || !ruleId) return;
setCurrentPage(1);
fetchSendRecords(1, searchKeyword);
}, [searchKeyword, visible, ruleId]);
// 页码变化
useEffect(() => {
if (!visible || !ruleId || currentPage === 1) return;
fetchSendRecords(currentPage, searchKeyword);
}, [currentPage, visible, ruleId]);
// 处理页码变化
const handlePageChange = (page: number) => {
setCurrentPage(page);
};
// 处理搜索回车
const handleSearchEnter = () => {
setSearchKeyword(searchQuery);
};
// 处理搜索输入
const handleSearchChange = (value: string) => {
setSearchQuery(value);
};
const title = ruleName ? `${ruleName} - 分发统计` : "分发统计";
const getRecycleColor = (isRecycle?: number) => {
switch (isRecycle) {
case 0:
return "#52c41a"; // 绿色 - 未回收
case 1:
return "#ff4d4f"; // 红色 - 已回收
default:
return "#d9d9d9"; // 灰色 - 未知状态
}
};
const getRecycleText = (isRecycle?: number) => {
switch (isRecycle) {
case 0:
return "未回收";
case 1:
return "已回收";
default:
return "未知";
}
};
return (
<Popup
visible={visible}
onMaskClick={onClose}
position="bottom"
bodyStyle={{
height: "100vh",
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
}}
>
<div className={style.accountModal}>
{/* 头部 */}
<div className={style.accountModalHeader}>
<h3 className={style.accountModalTitle}>{title}</h3>
<Button
type="text"
icon={<CloseOutlined />}
onClick={onClose}
className={style.accountModalClose}
/>
</div>
{/* 搜索栏 */}
<div className={style.searchBar}>
<div className={style.searchInputWrapper}>
<SearchOutlined className={style.searchIcon} />
<Input
placeholder="搜索分发记录(回车搜索)"
value={searchQuery}
onChange={handleSearchChange}
onEnterPress={handleSearchEnter}
clearable
/>
</div>
</div>
{/* 分发记录列表 */}
<div className={style.accountList}>
{loading ? (
<div className={style.accountLoading}>
<SpinLoading color="primary" />
<div className={style.accountLoadingText}>
...
</div>
</div>
) : sendRecords.length > 0 ? (
sendRecords.map((record, index) => (
<div key={record.id || index} className={style.accountItem}>
<div className={style.accountAvatar}>
<Avatar
src={record.avatar}
style={{ "--size": "48px" }}
fallback={(record.nickname || record.wechatId || "账号")[0]}
/>
</div>
<div className={style.accountInfo}>
<div className={style.accountName}>
{record.nickname || record.wechatId || `账号${record.id}`}
</div>
<div className={style.accountWechatId}>
{record.wechatId || "未绑定微信号"}
</div>
</div>
<div className={style.accountStatus}>
<span
className={style.statusDot}
style={{
backgroundColor: getRecycleColor(record.isRecycle),
}}
/>
<span className={style.statusText}>
{getRecycleText(record.isRecycle)}
</span>
</div>
</div>
))
) : (
<div className={style.accountEmpty}>
<div className={style.accountEmptyText}></div>
</div>
)}
</div>
{/* 底部统计和分页 */}
<div className={style.accountModalFooter}>
<div className={style.accountStats}>
<span> {total} </span>
</div>
<div className={style.paginationContainer}>
<Pagination
current={currentPage}
pageSize={pageSize}
total={total}
onChange={handlePageChange}
showSizeChanger={false}
showQuickJumper={false}
size="small"
/>
</div>
</div>
</div>
</Popup>
);
};
export default SendRcrodModal;

View File

@@ -255,6 +255,9 @@
padding: 16px 20px;
border-top: 1px solid #f0f0f0;
background: #fff;
display: flex;
justify-content: space-between;
align-items: center;
}
.accountStats {
@@ -263,6 +266,39 @@
color: #666;
}
.searchBar {
padding: 16px 20px;
border-bottom: 1px solid #f0f0f0;
background: #fff;
}
.searchInputWrapper {
position: relative;
display: flex;
align-items: center;
}
.searchIcon {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: #999;
font-size: 16px;
z-index: 1;
}
.searchInputWrapper :global(.adm-input) {
padding-left: 40px;
border-radius: 8px;
height: 40px;
}
.paginationContainer {
display: flex;
justify-content: center;
}
// 设备列表弹窗样式
.deviceModal {
height: 100%;

View File

@@ -34,6 +34,7 @@ import { useNavigate } from "react-router-dom";
import AccountListModal from "./components/AccountListModal";
import DeviceListModal from "./components/DeviceListModal";
import PoolListModal from "./components/PoolListModal";
import SendRcrodModal from "./components/SendRcrodModal";
const PAGE_SIZE = 10;
@@ -57,6 +58,7 @@ const TrafficDistributionList: React.FC = () => {
const [accountModalVisible, setAccountModalVisible] = useState(false);
const [deviceModalVisible, setDeviceModalVisible] = useState(false);
const [poolModalVisible, setPoolModalVisible] = useState(false);
const [sendRecordModalVisible, setSendRecordModalVisible] = useState(false);
const [currentRule, setCurrentRule] = useState<DistributionRule | null>(null);
const navigate = useNavigate();
@@ -153,6 +155,12 @@ const TrafficDistributionList: React.FC = () => {
setPoolModalVisible(true);
};
// 显示分发统计弹窗
const showSendRecord = (item: DistributionRule) => {
setCurrentRule(item);
setSendRecordModalVisible(true);
};
const renderCard = (item: DistributionRule) => {
const menu = (
<Menu onClick={({ key }) => handleMenuClick(key, item)}>
@@ -287,7 +295,11 @@ const TrafficDistributionList: React.FC = () => {
</div>
</div>
<div className={style.ruleStatsItem}>
<div
className={style.ruleStatsItem}
style={{ cursor: "pointer" }}
onClick={() => showSendRecord(item)}
>
<span style={{ fontSize: 16, fontWeight: 600 }}>
{item.config?.total?.totalUsers || 0}
</span>
@@ -394,6 +406,14 @@ const TrafficDistributionList: React.FC = () => {
ruleId={currentRule?.id}
ruleName={currentRule?.name}
/>
{/* 分发统计弹窗 */}
<SendRcrodModal
visible={sendRecordModalVisible}
onClose={() => setSendRecordModalVisible(false)}
ruleId={currentRule?.id}
ruleName={currentRule?.name}
/>
</Layout>
);
};