FEAT => 本次更新项目为:

This commit is contained in:
超级老白兔
2025-08-14 15:04:03 +08:00
parent 0e71a6701d
commit 566ca54fcf
7 changed files with 668 additions and 18 deletions

View File

@@ -33,7 +33,7 @@
"name": "vendor" "name": "vendor"
}, },
"index.html": { "index.html": {
"file": "assets/index-Cp05akVy.js", "file": "assets/index-AbKWTcUz.js",
"name": "index", "name": "index",
"src": "index.html", "src": "index.html",
"isEntry": true, "isEntry": true,
@@ -44,7 +44,7 @@
"_charts-D0fT04H8.js" "_charts-D0fT04H8.js"
], ],
"css": [ "css": [
"assets/index-Eg_DAu9e.css" "assets/index-V1Q-fxX3.css"
] ]
} }
} }

View File

@@ -11,13 +11,13 @@
</style> </style>
<!-- 引入 uni-app web-view SDK必须 --> <!-- 引入 uni-app web-view SDK必须 -->
<script type="text/javascript" src="/websdk.js"></script> <script type="text/javascript" src="/websdk.js"></script>
<script type="module" crossorigin src="/assets/index-Cp05akVy.js"></script> <script type="module" crossorigin src="/assets/index-AbKWTcUz.js"></script>
<link rel="modulepreload" crossorigin href="/assets/vendor-2vc8h_ct.js"> <link rel="modulepreload" crossorigin href="/assets/vendor-2vc8h_ct.js">
<link rel="modulepreload" crossorigin href="/assets/ui-qLeQLv1F.js"> <link rel="modulepreload" crossorigin href="/assets/ui-qLeQLv1F.js">
<link rel="modulepreload" crossorigin href="/assets/utils-6WF66_dS.js"> <link rel="modulepreload" crossorigin href="/assets/utils-6WF66_dS.js">
<link rel="modulepreload" crossorigin href="/assets/charts-D0fT04H8.js"> <link rel="modulepreload" crossorigin href="/assets/charts-D0fT04H8.js">
<link rel="stylesheet" crossorigin href="/assets/ui-D0C0OGrH.css"> <link rel="stylesheet" crossorigin href="/assets/ui-D0C0OGrH.css">
<link rel="stylesheet" crossorigin href="/assets/index-Eg_DAu9e.css"> <link rel="stylesheet" crossorigin href="/assets/index-V1Q-fxX3.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -87,18 +87,14 @@ const FriendRequestSettings: React.FC<FriendRequestSettingsProps> = ({
return ( return (
<div className={styles["friend-container"]}> <div className={styles["friend-container"]}>
{/* 选择设备区块 */} {/* 选择设备区块 */}
{![7].includes(formData.scenario) && ( <div className={styles["friend-label"]}></div>
<> <div className={styles["friend-block"]}>
<div className={styles["friend-label"]}></div> <DeviceSelection
<div className={styles["friend-block"]}> selectedOptions={formData.deveiceGroupsOptions}
<DeviceSelection onSelect={handleDevicesChange}
selectedOptions={formData.deveiceGroupsOptions} placeholder="选择设备"
onSelect={handleDevicesChange} />
placeholder="选择设备" </div>
/>
</div>
</>
)}
{/* 好友备注区块 */} {/* 好友备注区块 */}
<div className={styles["friend-label"]}></div> <div className={styles["friend-label"]}></div>

View File

@@ -0,0 +1,126 @@
import React from "react";
import { Popup, Avatar } from "antd-mobile";
import { Button } from "antd";
import { CloseOutlined } from "@ant-design/icons";
import style from "./index.module.scss";
interface AccountItem {
id: string | number;
nickname?: string;
wechatId?: string;
avatar?: string;
status?: string;
}
interface AccountListModalProps {
visible: boolean;
onClose: () => void;
accounts: AccountItem[];
title?: string;
}
const AccountListModal: React.FC<AccountListModalProps> = ({
visible,
onClose,
accounts,
title = "分发账号列表",
}) => {
const getStatusColor = (status?: string) => {
switch (status) {
case "normal":
return "#52c41a";
case "limited":
return "#faad14";
case "blocked":
return "#ff4d4f";
default:
return "#d9d9d9";
}
};
const getStatusText = (status?: string) => {
switch (status) {
case "normal":
return "正常";
case "limited":
return "受限";
case "blocked":
return "封禁";
default:
return "未知";
}
};
return (
<Popup
visible={visible}
onMaskClick={onClose}
position="bottom"
bodyStyle={{
height: "70vh",
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.accountList}>
{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>
</div>
<div className={style.accountInfo}>
<div className={style.accountName}>
{account.nickname ||
account.wechatId ||
`账号${account.id}`}
</div>
<div className={style.accountWechatId}>
{account.wechatId || "未绑定微信号"}
</div>
</div>
<div className={style.accountStatus}>
<span
className={style.statusDot}
style={{ backgroundColor: getStatusColor(account.status) }}
/>
<span className={style.statusText}>
{getStatusText(account.status)}
</span>
</div>
</div>
))
) : (
<div className={style.accountEmpty}>
<div className={style.accountEmptyText}></div>
</div>
)}
</div>
{/* 底部统计 */}
<div className={style.accountModalFooter}>
<div className={style.accountStats}>
<span> {accounts.length} </span>
</div>
</div>
</div>
</Popup>
);
};
export default AccountListModal;

View File

@@ -0,0 +1,141 @@
import React from "react";
import { Popup, Avatar } from "antd-mobile";
import { Button } from "antd";
import { CloseOutlined } from "@ant-design/icons";
import style from "./index.module.scss";
interface DeviceItem {
id: string | number;
memo?: string;
imei?: string;
wechatId?: string;
status?: "online" | "offline";
avatar?: string;
totalFriend?: number;
}
interface DeviceListModalProps {
visible: boolean;
onClose: () => void;
devices: DeviceItem[];
title?: string;
}
const DeviceListModal: React.FC<DeviceListModalProps> = ({
visible,
onClose,
devices,
title = "分发设备列表",
}) => {
const getStatusColor = (status?: string) => {
return status === "online" ? "#52c41a" : "#ff4d4f";
};
const getStatusText = (status?: string) => {
return status === "online" ? "在线" : "离线";
};
return (
<Popup
visible={visible}
onMaskClick={onClose}
position="bottom"
bodyStyle={{
height: "70vh",
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
}}
>
<div className={style.deviceModal}>
{/* 头部 */}
<div className={style.deviceModalHeader}>
<h3 className={style.deviceModalTitle}>{title}</h3>
<Button
type="text"
icon={<CloseOutlined />}
onClick={onClose}
className={style.deviceModalClose}
/>
</div>
{/* 设备列表 */}
<div className={style.deviceList}>
{devices.length > 0 ? (
devices.map((device, index) => (
<div key={device.id || index} className={style.deviceItem}>
{/* 顶部行IMEI */}
<div className={style.deviceHeaderRow}>
<span className={style.deviceImeiText}>
IMEI: {device.imei?.toUpperCase() || "-"}
</span>
</div>
{/* 主要内容区域:头像和详细信息 */}
<div className={style.deviceMainContent}>
{/* 头像 */}
<div className={style.deviceAvatar}>
{device.avatar ? (
<img src={device.avatar} alt="头像" />
) : (
<span className={style.deviceAvatarText}>
{(device.memo || device.wechatId || "设")[0]}
</span>
)}
</div>
{/* 设备信息 */}
<div className={style.deviceInfo}>
<div className={style.deviceInfoHeader}>
<h3 className={style.deviceName}>
{device.memo || "未命名设备"}
</h3>
<span
className={`${style.deviceStatusBadge} ${
device.status === "online"
? style.deviceStatusOnline
: style.deviceStatusOffline
}`}
>
{getStatusText(device.status)}
</span>
</div>
<div className={style.deviceInfoList}>
<div className={style.deviceInfoItem}>
<span className={style.deviceInfoLabel}>:</span>
<span className={style.deviceInfoValue}>
{device.wechatId || "未绑定"}
</span>
</div>
<div className={style.deviceInfoItem}>
<span className={style.deviceInfoLabel}>:</span>
<span
className={`${style.deviceInfoValue} ${style.deviceFriendCount}`}
>
{device.totalFriend ?? "-"}
</span>
</div>
</div>
</div>
</div>
</div>
))
) : (
<div className={style.deviceEmpty}>
<div className={style.deviceEmptyText}></div>
</div>
)}
</div>
{/* 底部统计 */}
<div className={style.deviceModalFooter}>
<div className={style.deviceStats}>
<span> {devices.length} </span>
</div>
</div>
</div>
</Popup>
);
};
export default DeviceListModal;

View File

@@ -69,10 +69,15 @@
.ruleMetaItem { .ruleMetaItem {
flex: 1; flex: 1;
text-align: center; text-align: center;
transition: background-color 0.2s ease;
} }
.ruleMetaItem:not(:last-child) { .ruleMetaItem:not(:last-child) {
border-right: 1px solid #f0f0f0; border-right: 1px solid #f0f0f0;
} }
.ruleMetaItem:hover {
background-color: #f8f9fa;
border-radius: 6px;
}
.ruleDivider { .ruleDivider {
border-top: 1px solid #f0f0f0; border-top: 1px solid #f0f0f0;
@@ -124,3 +129,313 @@
padding: 16px 0; padding: 16px 0;
background: #fff; background: #fff;
} }
// 账号列表弹窗样式
.accountModal {
height: 100%;
display: flex;
flex-direction: column;
}
.accountModalHeader {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid #f0f0f0;
background: #fff;
}
.accountModalTitle {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #222;
}
.accountModalClose {
border: none;
background: none;
color: #888;
font-size: 16px;
}
.accountList {
flex: 1;
overflow-y: auto;
padding: 0 20px;
}
.accountItem {
display: flex;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f5f5f5;
}
.accountItem:last-child {
border-bottom: none;
}
.accountAvatar {
margin-right: 12px;
flex-shrink: 0;
}
.accountInfo {
flex: 1;
min-width: 0;
}
.accountName {
font-size: 16px;
font-weight: 500;
color: #222;
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.accountWechatId {
font-size: 14px;
color: #888;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.accountStatus {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
.statusDot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.statusText {
font-size: 13px;
color: #666;
}
.accountEmpty {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
color: #888;
}
.accountEmptyText {
font-size: 16px;
}
.accountModalFooter {
padding: 16px 20px;
border-top: 1px solid #f0f0f0;
background: #fff;
}
.accountStats {
text-align: center;
font-size: 14px;
color: #666;
}
// 设备列表弹窗样式
.deviceModal {
height: 100%;
display: flex;
flex-direction: column;
}
.deviceModalHeader {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid #f0f0f0;
background: #fff;
}
.deviceModalTitle {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #222;
}
.deviceModalClose {
border: none;
background: none;
color: #888;
font-size: 16px;
}
.deviceList {
flex: 1;
overflow-y: auto;
padding: 0 20px;
}
.deviceItem {
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;
}
.deviceItem:hover {
transform: translateY(-1px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
.deviceHeaderRow {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.deviceImeiText {
font-size: 13px;
color: #888;
font-weight: 500;
}
.deviceMainContent {
display: flex;
align-items: center;
}
.deviceAvatar {
width: 48px;
height: 48px;
border-radius: 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 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);
overflow: hidden;
}
.deviceAvatar img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 12px;
}
.deviceAvatarText {
color: #fff;
font-size: 18px;
font-weight: 600;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.deviceInfo {
flex: 1;
min-width: 0;
}
.deviceInfoHeader {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
}
.deviceName {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #222;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
margin-right: 8px;
}
.deviceStatusBadge {
padding: 2px 8px;
border-radius: 10px;
font-size: 12px;
font-weight: 500;
flex-shrink: 0;
}
.deviceStatusOnline {
background: rgba(82, 196, 26, 0.1);
color: #52c41a;
}
.deviceStatusOffline {
background: rgba(255, 77, 79, 0.1);
color: #ff4d4f;
}
.deviceInfoList {
display: flex;
flex-direction: column;
gap: 4px;
}
.deviceInfoItem {
display: flex;
align-items: center;
font-size: 13px;
}
.deviceInfoLabel {
color: #888;
margin-right: 6px;
min-width: 50px;
}
.deviceInfoValue {
color: #444;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.deviceFriendCount {
color: #1890ff;
font-weight: 500;
}
.deviceEmpty {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
color: #888;
}
.deviceEmptyText {
font-size: 16px;
}
.deviceModalFooter {
padding: 16px 20px;
border-top: 1px solid #f0f0f0;
background: #fff;
}
.deviceStats {
text-align: center;
font-size: 14px;
color: #666;
}

View File

@@ -32,6 +32,8 @@ import {
} from "@ant-design/icons"; } from "@ant-design/icons";
import style from "./index.module.scss"; import style from "./index.module.scss";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import AccountListModal from "./AccountListModal";
import DeviceListModal from "./DeviceListModal";
const PAGE_SIZE = 10; const PAGE_SIZE = 10;
@@ -51,6 +53,14 @@ const TrafficDistributionList: React.FC = () => {
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
// 优化用menuLoadingId标记当前操作的item // 优化用menuLoadingId标记当前操作的item
const [menuLoadingId, setMenuLoadingId] = useState<number | null>(null); 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 navigate = useNavigate(); const navigate = useNavigate();
useEffect(() => { useEffect(() => {
@@ -128,6 +138,44 @@ 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);
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);
setDeviceModalVisible(true);
};
const renderCard = (item: DistributionRule) => { const renderCard = (item: DistributionRule) => {
const menu = ( const menu = (
<Menu onClick={({ key }) => handleMenuClick(key, item)}> <Menu onClick={({ key }) => handleMenuClick(key, item)}>
@@ -213,13 +261,21 @@ const TrafficDistributionList: React.FC = () => {
</div> </div>
</div> </div>
<div className={style.ruleMeta}> <div className={style.ruleMeta}>
<div className={style.ruleMetaItem}> <div
className={style.ruleMetaItem}
style={{ cursor: "pointer" }}
onClick={() => showAccountList(item)}
>
<div style={{ fontSize: 18, fontWeight: 600 }}> <div style={{ fontSize: 18, fontWeight: 600 }}>
{item.config?.account?.length || 0} {item.config?.account?.length || 0}
</div> </div>
<div style={{ fontSize: 13, color: "#888" }}></div> <div style={{ fontSize: 13, color: "#888" }}></div>
</div> </div>
<div className={style.ruleMetaItem}> <div
className={style.ruleMetaItem}
style={{ cursor: "pointer" }}
onClick={() => showDeviceList(item)}
>
<div style={{ fontSize: 18, fontWeight: 600 }}> <div style={{ fontSize: 18, fontWeight: 600 }}>
{item.config?.devices?.length || 0} {item.config?.devices?.length || 0}
</div> </div>
@@ -325,6 +381,22 @@ const TrafficDistributionList: React.FC = () => {
<div className={style.empty}></div> <div className={style.empty}></div>
)} )}
</div> </div>
{/* 账号列表弹窗 */}
<AccountListModal
visible={accountModalVisible}
onClose={() => setAccountModalVisible(false)}
accounts={currentAccounts}
title={`${currentRuleName} - 分发账号列表`}
/>
{/* 设备列表弹窗 */}
<DeviceListModal
visible={deviceModalVisible}
onClose={() => setDeviceModalVisible(false)}
devices={currentDevices}
title={`${currentDeviceRuleName} - 分发设备列表`}
/>
</Layout> </Layout>
); );
}; };