Merge branch 'yongpxu-dev' into yongpxu-dev2

This commit is contained in:
2025-08-14 10:13:24 +08:00
19 changed files with 748 additions and 317 deletions

View File

@@ -1,9 +1,9 @@
{ {
"_charts-TuAbbBZ5.js": { "_charts-D0fT04H8.js": {
"file": "assets/charts-TuAbbBZ5.js", "file": "assets/charts-D0fT04H8.js",
"name": "charts", "name": "charts",
"imports": [ "imports": [
"_ui-D1w-jetn.js", "_ui-qLeQLv1F.js",
"_vendor-2vc8h_ct.js" "_vendor-2vc8h_ct.js"
] ]
}, },
@@ -11,8 +11,8 @@
"file": "assets/ui-D0C0OGrH.css", "file": "assets/ui-D0C0OGrH.css",
"src": "_ui-D0C0OGrH.css" "src": "_ui-D0C0OGrH.css"
}, },
"_ui-D1w-jetn.js": { "_ui-qLeQLv1F.js": {
"file": "assets/ui-D1w-jetn.js", "file": "assets/ui-qLeQLv1F.js",
"name": "ui", "name": "ui",
"imports": [ "imports": [
"_vendor-2vc8h_ct.js" "_vendor-2vc8h_ct.js"
@@ -33,18 +33,18 @@
"name": "vendor" "name": "vendor"
}, },
"index.html": { "index.html": {
"file": "assets/index-D3HSx5Yt.js", "file": "assets/index-Cp05akVy.js",
"name": "index", "name": "index",
"src": "index.html", "src": "index.html",
"isEntry": true, "isEntry": true,
"imports": [ "imports": [
"_vendor-2vc8h_ct.js", "_vendor-2vc8h_ct.js",
"_ui-D1w-jetn.js", "_ui-qLeQLv1F.js",
"_utils-6WF66_dS.js", "_utils-6WF66_dS.js",
"_charts-TuAbbBZ5.js" "_charts-D0fT04H8.js"
], ],
"css": [ "css": [
"assets/index-B0SB167P.css" "assets/index-Eg_DAu9e.css"
] ]
} }
} }

View File

@@ -10,14 +10,14 @@
} }
</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-D3HSx5Yt.js"></script> <script type="module" crossorigin src="/assets/index-Cp05akVy.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-D1w-jetn.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-TuAbbBZ5.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-B0SB167P.css"> <link rel="stylesheet" crossorigin href="/assets/index-Eg_DAu9e.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -10,7 +10,7 @@
} }
</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>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -8,6 +8,8 @@ export interface DeviceSelectionItem {
wxid?: string; wxid?: string;
nickname?: string; nickname?: string;
usedInPlans?: number; usedInPlans?: number;
avatar?: string;
totalFriend?: number;
} }
// 组件属性接口 // 组件属性接口

View File

@@ -67,60 +67,152 @@
} }
.deviceItem { .deviceItem {
display: flex; display: flex;
align-items: flex-start; flex-direction: column;
gap: 12px; padding: 12px;
padding: 16px;
border-radius: 12px;
border: 1px solid #f0f0f0;
background: #fff; background: #fff;
cursor: pointer; border-radius: 12px;
transition: background 0.2s; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
transition: all 0.2s ease;
border: 1px solid #f5f5f5;
&:hover { &:hover {
background: #f5f6fa; transform: translateY(-1px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
}
.headerRow {
display: flex;
align-items: center;
gap: 8px;
}
.checkboxContainer {
flex-shrink: 0;
}
.imeiText {
font-size: 13px;
color: #666;
font-family: monospace;
flex: 1;
}
.mainContent {
display: flex;
align-items: center;
gap: 12px;
cursor: pointer;
padding: 8px;
border-radius: 8px;
transition: background-color 0.2s ease;
&:hover {
background-color: #f8f9fa;
} }
} }
.deviceCheckbox { .deviceCheckbox {
margin-top: 4px; flex-shrink: 0;
} }
.deviceInfo { .deviceInfo {
flex: 1; flex: 1;
min-width: 0;
display: flex;
align-items: center;
gap: 12px;
} }
.deviceAvatar {
width: 64px;
height: 64px;
border-radius: 6px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.25);
flex-shrink: 0;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatarText {
font-size: 18px;
color: #fff;
font-weight: 700;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
}
.deviceContent {
flex: 1;
min-width: 0;
}
.deviceInfoRow { .deviceInfoRow {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; gap: 6px;
margin-bottom: 6px;
} }
.deviceName { .deviceName {
font-weight: 500;
font-size: 16px; font-size: 16px;
color: #222; font-weight: 600;
color: #1a1a1a;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
.statusOnline { .statusOnline {
width: 56px; font-size: 11px;
height: 24px; padding: 1px 6px;
border-radius: 12px; border-radius: 8px;
background: #52c41a; color: #52c41a;
color: #fff; background: #f6ffed;
font-size: 13px; border: 1px solid #b7eb8f;
display: flex; font-weight: 500;
align-items: center;
justify-content: center;
} }
.statusOffline { .statusOffline {
width: 56px; font-size: 11px;
height: 24px; padding: 1px 6px;
border-radius: 12px; border-radius: 8px;
background: #e5e6eb; color: #ff4d4f;
color: #888; background: #fff2f0;
font-size: 13px; border: 1px solid #ffccc7;
display: flex; font-weight: 500;
align-items: center;
justify-content: center;
} }
.deviceInfoDetail { .deviceInfoDetail {
display: flex;
flex-direction: column;
gap: 4px;
}
.infoItem {
display: flex;
align-items: center;
gap: 8px;
}
.infoLabel {
font-size: 13px; font-size: 13px;
color: #888; color: #666;
margin-top: 4px; min-width: 50px;
}
.infoValue {
font-size: 13px;
color: #333;
&.imei {
font-family: monospace;
}
&.friendCount {
font-weight: 500;
}
} }
.loadingBox { .loadingBox {
display: flex; display: flex;

View File

@@ -46,6 +46,12 @@ const DeviceSelection: React.FC<DeviceSelectionProps> = ({
onSelect(selectedOptions.filter(v => v.id !== id)); onSelect(selectedOptions.filter(v => v.id !== id));
}; };
// 清除所有已选设备
const handleClearAll = () => {
if (readonly) return;
onSelect([]);
};
return ( return (
<> <>
{/* mode=input 显示输入框mode=dialog不显示 */} {/* mode=input 显示输入框mode=dialog不显示 */}
@@ -57,6 +63,7 @@ const DeviceSelection: React.FC<DeviceSelectionProps> = ({
onClick={openPopup} onClick={openPopup}
prefix={<SearchOutlined />} prefix={<SearchOutlined />}
allowClear={!readonly} allowClear={!readonly}
onClear={handleClearAll}
size="large" size="large"
readOnly={readonly} readOnly={readonly}
disabled={readonly} disabled={readonly}
@@ -86,11 +93,52 @@ const DeviceSelection: React.FC<DeviceSelectionProps> = ({
style={{ style={{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
padding: "4px 8px", padding: "8px 12px",
borderBottom: "1px solid #f0f0f0", borderBottom: "1px solid #f0f0f0",
fontSize: 14, fontSize: 14,
}} }}
> >
{/* 头像 */}
<div
style={{
width: 40,
height: 40,
borderRadius: "6px",
background:
"linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
display: "flex",
alignItems: "center",
justifyContent: "center",
overflow: "hidden",
boxShadow: "0 2px 8px rgba(102, 126, 234, 0.25)",
marginRight: "12px",
flexShrink: 0,
}}
>
{device.avatar ? (
<img
src={device.avatar}
alt="头像"
style={{
width: "100%",
height: "100%",
objectFit: "cover",
}}
/>
) : (
<span
style={{
fontSize: 16,
color: "#fff",
fontWeight: 700,
textShadow: "0 1px 3px rgba(0,0,0,0.3)",
}}
>
{(device.memo || device.wechatId || "设")[0]}
</span>
)}
</div>
<div <div
style={{ style={{
flex: 1, flex: 1,
@@ -100,7 +148,7 @@ const DeviceSelection: React.FC<DeviceSelectionProps> = ({
textOverflow: "ellipsis", textOverflow: "ellipsis",
}} }}
> >
{device.memo} - {device.wechatId} {device.memo} - {device.wechatId}
</div> </div>
{!readonly && ( {!readonly && (
<Button <Button

View File

@@ -51,6 +51,8 @@ const SelectionPopup: React.FC<SelectionPopupProps> = ({
wxid: d.wechatId || "", wxid: d.wechatId || "",
nickname: d.nickname || "", nickname: d.nickname || "",
usedInPlans: d.usedInPlans || 0, usedInPlans: d.usedInPlans || 0,
avatar: d.avatar || "",
totalFriend: d.totalFriend || 0,
})), })),
); );
setTotal(res.total || 0); setTotal(res.total || 0);
@@ -161,31 +163,67 @@ const SelectionPopup: React.FC<SelectionPopupProps> = ({
) : ( ) : (
<div className={style.deviceListInner}> <div className={style.deviceListInner}>
{filteredDevices.map(device => ( {filteredDevices.map(device => (
<label key={device.id} className={style.deviceItem}> <div key={device.id} className={style.deviceItem}>
<Checkbox {/* 顶部行选择框和IMEI */}
checked={selectedOptions.some(v => v.id === device.id)} <div className={style.headerRow}>
onChange={() => handleDeviceToggle(device)} <div className={style.checkboxContainer}>
className={style.deviceCheckbox} <Checkbox
/> checked={selectedOptions.some(v => v.id === device.id)}
<div className={style.deviceInfo}> onChange={() => handleDeviceToggle(device)}
<div className={style.deviceInfoRow}> className={style.deviceCheckbox}
<span className={style.deviceName}>{device.memo}</span> />
<div </div>
className={ <span className={style.imeiText}>
device.status === "online" IMEI: {device.imei?.toUpperCase()}
? style.statusOnline </span>
: style.statusOffline </div>
}
> {/* 主要内容区域:头像和详细信息 */}
{device.status === "online" ? "在线" : "离线"} <div className={style.mainContent}>
{/* 头像 */}
<div className={style.deviceAvatar}>
{device.avatar ? (
<img src={device.avatar} alt="头像" />
) : (
<span className={style.avatarText}>
{(device.memo || device.wechatId || "设")[0]}
</span>
)}
</div>
{/* 设备信息 */}
<div className={style.deviceContent}>
<div className={style.deviceInfoRow}>
<span className={style.deviceName}>{device.memo}</span>
<div
className={
device.status === "online"
? style.statusOnline
: style.statusOffline
}
>
{device.status === "online" ? "在线" : "离线"}
</div>
</div>
<div className={style.deviceInfoDetail}>
<div className={style.infoItem}>
<span className={style.infoLabel}>:</span>
<span className={style.infoValue}>
{device.wechatId}
</span>
</div>
<div className={style.infoItem}>
<span className={style.infoLabel}>:</span>
<span
className={`${style.infoValue} ${style.friendCount}`}
>
{device.totalFriend ?? "-"}
</span>
</div>
</div> </div>
</div> </div>
<div className={style.deviceInfoDetail}>
<div>IMEI: {device.imei}</div>
<div>: {device.wechatId}</div>
</div>
</div> </div>
</label> </div>
))} ))}
</div> </div>
)} )}

View File

@@ -62,9 +62,7 @@ export default function ContentForm() {
setSelectedFriends(data.sourceFriends || []); setSelectedFriends(data.sourceFriends || []);
setSelectedGroups(data.selectedGroups || []); setSelectedGroups(data.selectedGroups || []);
setSelectedGroupsOptions(data.selectedGroupsOptions || []); setSelectedGroupsOptions(data.selectedGroupsOptions || []);
setSelectedFriendsOptions(data.friendsGroupsOptions || []);
setSelectedFriendsOptions(data.sourceFriendsOptions || []);
setKeywordsInclude((data.keywordInclude || []).join(",")); setKeywordsInclude((data.keywordInclude || []).join(","));
setKeywordsExclude((data.keywordExclude || []).join(",")); setKeywordsExclude((data.keywordExclude || []).join(","));
setAIPrompt(data.aiPrompt || ""); setAIPrompt(data.aiPrompt || "");

View File

@@ -132,7 +132,7 @@ const ContentLibraryList: React.FC = () => {
}; };
const handleEdit = (id: string) => { const handleEdit = (id: string) => {
navigate(`/content/edit/${id}`); navigate(`/mine/content/edit/${id}`);
}; };
const handleDelete = async (id: string) => { const handleDelete = async (id: string) => {

View File

@@ -0,0 +1,173 @@
.deviceList {
display: flex;
flex-direction: column;
gap: 12px;
}
.deviceCard {
background: #fff;
border-radius: 12px;
padding: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
transition: all 0.2s ease;
position: relative;
overflow: hidden;
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
&.selected {
border: 2px solid #1677ff;
}
&:not(.selected) {
border: 1px solid #f5f5f5;
}
}
.headerRow {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.checkboxContainer {
flex-shrink: 0;
}
.imeiText {
font-size: 13px;
color: #666;
font-family: monospace;
flex: 1;
}
.mainContent {
display: flex;
align-items: center;
gap: 12px;
cursor: pointer;
border-radius: 8px;
transition: background-color 0.2s ease;
&:hover {
background-color: #f8f9fa;
}
}
.avatar {
width: 64px;
height: 64px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.25);
flex-shrink: 0;
border-radius: 6px;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatarText {
font-size: 18px;
color: #fff;
font-weight: 700;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
}
.deviceInfo {
flex: 1;
min-width: 0;
}
.deviceHeader {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 6px;
}
.deviceName {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #1a1a1a;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.statusBadge {
font-size: 11px;
padding: 1px 6px;
border-radius: 8px;
font-weight: 500;
&.online {
color: #52c41a;
background: #f6ffed;
border: 1px solid #b7eb8f;
}
&.offline {
color: #ff4d4f;
background: #fff2f0;
border: 1px solid #ffccc7;
}
}
.infoList {
display: flex;
flex-direction: column;
gap: 4px;
}
.infoItem {
display: flex;
align-items: center;
gap: 8px;
}
.infoLabel {
font-size: 13px;
color: #666;
min-width: 50px;
}
.infoValue {
font-size: 13px;
color: #333;
&.friendCount {
font-weight: 500;
}
}
.arrowIcon {
color: #999;
font-size: 14px;
margin-left: auto;
transition: transform 0.2s ease;
}
.mainContent:hover .arrowIcon {
transform: translateX(3px);
color: #1677ff;
}
.paginationContainer {
padding: 16px;
background: #fff;
border-top: 1px solid #f0f0f0;
display: flex;
justify-content: center;
}

View File

@@ -7,6 +7,7 @@ import {
ReloadOutlined, ReloadOutlined,
SearchOutlined, SearchOutlined,
QrcodeOutlined, QrcodeOutlined,
RightOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import Layout from "@/components/Layout/Layout"; import Layout from "@/components/Layout/Layout";
import { import {
@@ -19,6 +20,7 @@ import type { Device } from "@/types/device";
import { comfirm } from "@/utils/common"; import { comfirm } from "@/utils/common";
import { useUserStore } from "@/store/module/user"; import { useUserStore } from "@/store/module/user";
import NavCommon from "@/components/NavCommon"; import NavCommon from "@/components/NavCommon";
import styles from "./index.module.scss";
const Devices: React.FC = () => { const Devices: React.FC = () => {
// 设备列表相关 // 设备列表相关
@@ -250,7 +252,7 @@ const Devices: React.FC = () => {
</> </>
} }
footer={ footer={
<div className="pagination-container"> <div className={styles.paginationContainer}>
<Pagination <Pagination
current={page} current={page}
pageSize={20} pageSize={20}
@@ -264,65 +266,86 @@ const Devices: React.FC = () => {
> >
<div style={{ padding: 12 }}> <div style={{ padding: 12 }}>
{/* 设备列表 */} {/* 设备列表 */}
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}> <div className={styles.deviceList}>
{filtered.map(device => ( {filtered.map(device => (
<div <div key={device.id} className={styles.deviceCard}>
key={device.id} {/* 顶部行选择框和IMEI */}
style={{ <div className={styles.headerRow}>
background: "#fff", <div className={styles.checkboxContainer}>
borderRadius: 12, <Checkbox
padding: 12, checked={selected.includes(device.id)}
boxShadow: "0 1px 4px #eee", onChange={e => {
display: "flex", e.stopPropagation();
alignItems: "center", setSelected(prev =>
cursor: "pointer", e.target.checked
border: selected.includes(device.id) ? [...prev, device.id!]
? "1.5px solid #1677ff" : prev.filter(id => id !== device.id),
: "1px solid #f0f0f0", );
}} }}
onClick={() => goDetail(device.id!)} onClick={e => e.stopPropagation()}
> />
<Checkbox
checked={selected.includes(device.id)}
onChange={e => {
e.stopPropagation();
setSelected(prev =>
e.target.checked
? [...prev, device.id!]
: prev.filter(id => id !== device.id),
);
}}
onClick={e => e.stopPropagation()}
style={{ marginRight: 12 }}
/>
<div style={{ flex: 1 }}>
<div style={{ fontWeight: 600, fontSize: 16 }}>
{device.memo || "未命名设备"}
</div>
<div style={{ fontSize: 14, color: "#999", marginTop: 2 }}>
IMEI: {device.imei}
</div>
<div style={{ fontSize: 14, color: "#999", marginTop: 2 }}>
: {device.wechatId || "未绑定"}
</div>
<div style={{ fontSize: 14, color: "#999", marginTop: 2 }}>
: {device.totalFriend ?? "-"}
</div> </div>
<span className={styles.imeiText}>
IMEI: {device.imei?.toUpperCase()}
</span>
</div>
{/* 主要内容区域:头像和详细信息 */}
<div className={styles.mainContent}>
{/* 头像 */}
<div className={styles.avatar}>
{device.avatar ? (
<img src={device.avatar} alt="头像" />
) : (
<span className={styles.avatarText}>
{(device.memo || device.wechatId || "设")[0]}
</span>
)}
</div>
{/* 设备信息 */}
<div className={styles.deviceInfo}>
<div className={styles.deviceHeader}>
<h3 className={styles.deviceName}>
{device.memo || "未命名设备"}
</h3>
<span
className={`${styles.statusBadge} ${
device.status === "online" || device.alive === 1
? styles.online
: styles.offline
}`}
>
{device.status === "online" || device.alive === 1
? "在线"
: "离线"}
</span>
</div>
<div className={styles.infoList}>
<div className={styles.infoItem}>
<span className={styles.infoLabel}>:</span>
<span className={styles.infoValue}>
{device.wechatId || "未绑定"}
</span>
</div>
<div className={styles.infoItem}>
<span className={styles.infoLabel}>:</span>
<span
className={`${styles.infoValue} ${styles.friendCount}`}
>
{device.totalFriend ?? "-"}
</span>
</div>
</div>
</div>
{/* 箭头图标 */}
<RightOutlined
className={styles.arrowIcon}
onClick={() => goDetail(device.id!)}
/>
</div> </div>
<span
style={{
fontSize: 12,
color:
device.status === "online" || device.alive === 1
? "#52c41a"
: "#aaa",
marginLeft: 8,
}}
>
{device.status === "online" || device.alive === 1
? "在线"
: "离线"}
</span>
</div> </div>
))} ))}

View File

@@ -64,7 +64,7 @@ const Mine: React.FC = () => {
description: "管理用户流量池和分组", description: "管理用户流量池和分组",
icon: <DatabaseOutlined />, icon: <DatabaseOutlined />,
count: stats.traffic, count: stats.traffic,
path: "/traffic-pool", path: "/mine/traffic-pool",
bgColor: "#f9f0ff", bgColor: "#f9f0ff",
iconColor: "#722ed1", iconColor: "#722ed1",
}, },

View File

@@ -1,118 +1,158 @@
import React from "react"; import React, { useState, useEffect } from "react";
import { Popup } from "antd-mobile"; import { Popup } from "antd-mobile";
import { Select, Button } from "antd"; import { Select, Button } from "antd";
import type { import DeviceSelection from "@/components/DeviceSelection";
DeviceOption, import type { UserStatus, ScenarioOption } from "./data";
PackageOption, import { fetchScenarioOptions, fetchPackageOptions } from "./api";
ValueLevel, import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
UserStatus,
} from "./data";
interface FilterModalProps { interface FilterModalProps {
visible: boolean; visible: boolean;
onClose: () => void; onClose: () => void;
deviceOptions: DeviceOption[]; onConfirm: (filters: {
packageOptions: PackageOption[]; deviceIds: string[];
deviceId: string; packageId: string;
setDeviceId: (v: string) => void; scenarioId: string;
packageId: string; userValue: number;
setPackageId: (v: string) => void; userStatus: number;
valueLevel: ValueLevel; }) => void;
setValueLevel: (v: ValueLevel) => void; scenarioOptions: ScenarioOption[];
userStatus: UserStatus;
setUserStatus: (v: UserStatus) => void;
onReset: () => void;
} }
const valueLevelOptions = [ const valueLevelOptions = [
{ label: "全部价值", value: "all" }, { label: "全部价值", value: 0 },
{ label: "高价值", value: "high" }, { label: "高价值", value: 1 },
{ label: "中价值", value: "medium" }, { label: "中价值", value: 2 },
{ label: "低价值", value: "low" }, { label: "低价值", value: 3 },
]; ];
const statusOptions = [ const statusOptions = [
{ label: "全部状态", value: "all" }, { label: "全部状态", value: 0 },
{ label: "已添加", value: "added" }, { label: "已添加", value: 1 },
{ label: "待添加", value: "pending" }, { label: "待添加", value: 2 },
{ label: "添加失败", value: "failed" }, { label: "重复", value: 3 },
{ label: "重复", value: "duplicate" }, { label: "添加失败", value: -1 },
]; ];
const FilterModal: React.FC<FilterModalProps> = ({ const FilterModal: React.FC<FilterModalProps> = ({
visible, visible,
onClose, onClose,
deviceOptions, onConfirm,
packageOptions, }) => {
deviceId, const [selectedDevices, setSelectedDevices] = useState<DeviceSelectionItem[]>(
setDeviceId, [],
packageId, );
setPackageId, const [packageId, setPackageId] = useState<string>("");
valueLevel, const [scenarioId, setScenarioId] = useState<string>("");
setValueLevel, const [userValue, setUserValue] = useState<number>(0);
userStatus, const [userStatus, setUserStatus] = useState<number>(0);
setUserStatus, const [scenarioOptions, setScenarioOptions] = useState<any[]>([]);
onReset, const [packageOptions, setPackageOptions] = useState<any[]>([]);
}) => (
<Popup useEffect(() => {
visible={visible} if (visible) {
onMaskClick={onClose} fetchScenarioOptions().then(res => {
position="right" setScenarioOptions(res);
bodyStyle={{ width: "80vw", maxWidth: 360, padding: 24 }} });
> fetchPackageOptions().then(res => {
<div style={{ fontWeight: 600, fontSize: 18, marginBottom: 20 }}> setPackageOptions(res);
});
</div> }
<div style={{ marginBottom: 20 }}> }, [visible]);
<div style={{ marginBottom: 6 }}></div>
<Select const handleApply = () => {
style={{ width: "100%" }} const params = {
value={deviceId} deviceIds: selectedDevices.map(d => d.id.toString()),
onChange={setDeviceId} packageId,
options={[ scenarioId,
{ label: "全部设备", value: "all" }, userValue,
...deviceOptions.map(d => ({ label: d.name, value: d.id })), userStatus,
]} };
/> console.log(params);
</div>
<div style={{ marginBottom: 20 }}> onConfirm(params);
<div style={{ marginBottom: 6 }}></div> onClose();
<Select };
style={{ width: "100%" }}
value={packageId} const handleReset = () => {
onChange={setPackageId} setSelectedDevices([]);
options={[ setPackageId("");
{ label: "全部流量池", value: "all" }, setScenarioId("");
...packageOptions.map(p => ({ label: p.name, value: p.id })), setUserValue(0);
]} setUserStatus(0);
/> };
</div>
<div style={{ marginBottom: 20 }}> return (
<div style={{ marginBottom: 6 }}></div> <Popup
<Select visible={visible}
style={{ width: "100%" }} onMaskClick={onClose}
value={valueLevel} position="right"
onChange={v => setValueLevel(v as ValueLevel)} bodyStyle={{ width: "80vw", maxWidth: 360, padding: 24 }}
options={valueLevelOptions} >
/> <div style={{ fontWeight: 600, fontSize: 18, marginBottom: 20 }}>
</div>
<div style={{ marginBottom: 20 }}> </div>
<div style={{ marginBottom: 6 }}></div> <div style={{ marginBottom: 20 }}>
<Select <div style={{ marginBottom: 6 }}></div>
style={{ width: "100%" }} <DeviceSelection
value={userStatus} selectedOptions={selectedDevices}
onChange={v => setUserStatus(v as UserStatus)} onSelect={setSelectedDevices}
options={statusOptions} placeholder="选择设备"
/> showSelectedList={false}
</div> selectedListMaxHeight={120}
<div style={{ display: "flex", gap: 12, marginTop: 32 }}> />
<Button onClick={onReset} style={{ flex: 1 }}> </div>
<div style={{ marginBottom: 20 }}>
</Button> <div style={{ marginBottom: 6 }}></div>
<Button type="primary" onClick={onClose} style={{ flex: 1 }}> <Select
style={{ width: "100%" }}
</Button> value={packageId}
</div> onChange={setPackageId}
</Popup> options={[
); { label: "全部流量池", value: "" },
...packageOptions.map(p => ({ label: p.name, value: p.id })),
]}
/>
</div>
<div style={{ marginBottom: 20 }}>
<div style={{ marginBottom: 6 }}></div>
<Select
style={{ width: "100%" }}
value={scenarioId}
onChange={setScenarioId}
options={[
{ label: "全部场景", value: "" },
...scenarioOptions.map(s => ({ label: s.name, value: s.id })),
]}
/>
</div>
<div style={{ marginBottom: 20 }}>
<div style={{ marginBottom: 6 }}></div>
<Select
style={{ width: "100%" }}
value={userValue}
onChange={v => setUserValue(v as number)}
options={valueLevelOptions}
/>
</div>
<div style={{ marginBottom: 20 }}>
<div style={{ marginBottom: 6 }}></div>
<Select
style={{ width: "100%" }}
value={userStatus}
onChange={v => setUserStatus(v as UserStatus)}
options={statusOptions}
/>
</div>
<div style={{ display: "flex", gap: 12, marginTop: 32 }}>
<Button onClick={handleReset} style={{ flex: 1 }}>
</Button>
<Button type="primary" onClick={handleApply} style={{ flex: 1 }}>
</Button>
</div>
</Popup>
);
};
export default FilterModal; export default FilterModal;

View File

@@ -9,11 +9,10 @@ export function fetchTrafficPoolList(params: {
return request("/v1/traffic/pool", params, "GET"); return request("/v1/traffic/pool", params, "GET");
} }
// 获取分组列表如无真实接口可用mock export async function fetchScenarioOptions(): Promise<any[]> {
export async function fetchPackageOptions(): Promise<any[]> { return request("/v1/plan/scenes", {}, "GET");
// TODO: 替换为真实接口 }
return [
{ id: "pkg-1", name: "高价值客户池" }, export async function fetchPackageOptions(): Promise<any[]> {
{ id: "pkg-2", name: "测试流量池" }, return request("/v1/traffic/pool/getPackage", {}, "GET");
];
} }

View File

@@ -43,3 +43,9 @@ export type ValueLevel = "all" | "high" | "medium" | "low";
// 状态类型 // 状态类型
export type UserStatus = "all" | "added" | "pending" | "failed" | "duplicate"; export type UserStatus = "all" | "added" | "pending" | "failed" | "duplicate";
// 获客场景类型
export interface ScenarioOption {
id: string;
name: string;
}

View File

@@ -1,11 +1,16 @@
import { useState, useEffect, useMemo } from "react"; import { useState, useEffect, useMemo } from "react";
import { fetchTrafficPoolList, fetchPackageOptions } from "./api"; import {
fetchTrafficPoolList,
fetchPackageOptions,
fetchScenarioOptions,
} from "./api";
import type { import type {
TrafficPoolUser, TrafficPoolUser,
DeviceOption, DeviceOption,
PackageOption, PackageOption,
ValueLevel, ValueLevel,
UserStatus, UserStatus,
ScenarioOption,
} from "./data"; } from "./data";
import { Toast } from "antd-mobile"; import { Toast } from "antd-mobile";
@@ -19,12 +24,13 @@ export function useTrafficPoolListLogic() {
// 筛选相关 // 筛选相关
const [showFilter, setShowFilter] = useState(false); const [showFilter, setShowFilter] = useState(false);
const [deviceOptions, setDeviceOptions] = useState<DeviceOption[]>([]);
const [packageOptions, setPackageOptions] = useState<PackageOption[]>([]); const [packageOptions, setPackageOptions] = useState<PackageOption[]>([]);
const [deviceId, setDeviceId] = useState<string>("all"); const [scenarioOptions, setScenarioOptions] = useState<ScenarioOption[]>([]);
const [packageId, setPackageId] = useState<string>("all"); const [selectedDevices, setSelectedDevices] = useState<any[]>([]);
const [valueLevel, setValueLevel] = useState<ValueLevel>("all"); const [packageId, setPackageId] = useState<number>(0);
const [userStatus, setUserStatus] = useState<UserStatus>("all"); const [scenarioId, setScenarioId] = useState<number>(0);
const [userValue, setUserValue] = useState<number>(0);
const [userStatus, setUserStatus] = useState<number>(0);
// 批量相关 // 批量相关
const [selectedIds, setSelectedIds] = useState<number[]>([]); const [selectedIds, setSelectedIds] = useState<number[]>([]);
@@ -47,15 +53,22 @@ export function useTrafficPoolListLogic() {
const getList = async () => { const getList = async () => {
setLoading(true); setLoading(true);
try { try {
const res = await fetchTrafficPoolList({ const params: any = {
page, page,
pageSize, pageSize,
keyword: search, keyword: search,
// deviceId, packageId,
// packageId, taskId: scenarioId,
// valueLevel, userValue,
// userStatus, addStatus: userStatus,
}); };
// 添加筛选参数
if (selectedDevices.length > 0) {
params.deviceId = selectedDevices.map(d => d.id).join(",");
}
const res = await fetchTrafficPoolList(params);
setList(res.list || []); setList(res.list || []);
setTotal(res.total || 0); setTotal(res.total || 0);
} finally { } finally {
@@ -66,13 +79,22 @@ export function useTrafficPoolListLogic() {
// 获取筛选项 // 获取筛选项
useEffect(() => { useEffect(() => {
fetchPackageOptions().then(setPackageOptions); fetchPackageOptions().then(setPackageOptions);
fetchScenarioOptions().then(setScenarioOptions);
}, []); }, []);
// 筛选条件变化时刷新列表 // 筛选条件变化时刷新列表
useEffect(() => { useEffect(() => {
getList(); getList();
// eslint-disable-next-line // eslint-disable-next-line
}, [page, search /*, deviceId, packageId, valueLevel, userStatus*/]); }, [
page,
search,
selectedDevices,
packageId,
scenarioId,
userValue,
userStatus,
]);
// 全选/反选 // 全选/反选
const handleSelectAll = (checked: boolean) => { const handleSelectAll = (checked: boolean) => {
@@ -108,10 +130,11 @@ export function useTrafficPoolListLogic() {
// 筛选重置 // 筛选重置
const resetFilter = () => { const resetFilter = () => {
setDeviceId("all"); setSelectedDevices([]);
setPackageId("all"); setPackageId(0);
setValueLevel("all"); setScenarioId(0);
setUserStatus("all"); setUserValue(0);
setUserStatus(0);
}; };
return { return {
@@ -125,14 +148,16 @@ export function useTrafficPoolListLogic() {
setSearch, setSearch,
showFilter, showFilter,
setShowFilter, setShowFilter,
deviceOptions,
packageOptions, packageOptions,
deviceId, scenarioOptions,
setDeviceId, selectedDevices,
setSelectedDevices,
packageId, packageId,
setPackageId, setPackageId,
valueLevel, scenarioId,
setValueLevel, setScenarioId,
userValue,
setUserValue,
userStatus, userStatus,
setUserStatus, setUserStatus,
selectedIds, selectedIds,

View File

@@ -5,9 +5,9 @@ import {
ReloadOutlined, ReloadOutlined,
BarChartOutlined, BarChartOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { Input, Button, Checkbox } from "antd"; import { Input, Button, Checkbox, Pagination } from "antd";
import styles from "./index.module.scss"; import styles from "./index.module.scss";
import { List, Empty, Avatar, Modal, Selector, Toast, Card } from "antd-mobile"; import { Empty, Avatar } from "antd-mobile";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import NavCommon from "@/components/NavCommon"; import NavCommon from "@/components/NavCommon";
import { useTrafficPoolListLogic } from "./dataAnyx"; import { useTrafficPoolListLogic } from "./dataAnyx";
@@ -18,20 +18,6 @@ import BatchAddModal from "./BatchAddModal";
const defaultAvatar = const defaultAvatar =
"https://cdn.jsdelivr.net/gh/maokaka/static/avatar-default.png"; "https://cdn.jsdelivr.net/gh/maokaka/static/avatar-default.png";
const valueLevelOptions = [
{ label: "全部", value: "all" },
{ label: "高价值", value: "high" },
{ label: "中价值", value: "medium" },
{ label: "低价值", value: "low" },
];
const statusOptions = [
{ label: "全部", value: "all" },
{ label: "已添加", value: "added" },
{ label: "待添加", value: "pending" },
{ label: "添加失败", value: "failed" },
{ label: "重复", value: "duplicate" },
];
const TrafficPoolList: React.FC = () => { const TrafficPoolList: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { const {
@@ -45,15 +31,12 @@ const TrafficPoolList: React.FC = () => {
setSearch, setSearch,
showFilter, showFilter,
setShowFilter, setShowFilter,
deviceOptions,
packageOptions, packageOptions,
deviceId, scenarioOptions,
setDeviceId, setSelectedDevices,
packageId,
setPackageId, setPackageId,
valueLevel, setScenarioId,
setValueLevel, setUserValue,
userStatus,
setUserStatus, setUserStatus,
selectedIds, selectedIds,
handleSelectAll, handleSelectAll,
@@ -67,7 +50,6 @@ const TrafficPoolList: React.FC = () => {
setShowStats, setShowStats,
stats, stats,
getList, getList,
resetFilter,
} = useTrafficPoolListLogic(); } = useTrafficPoolListLogic();
return ( return (
@@ -154,6 +136,17 @@ const TrafficPoolList: React.FC = () => {
</div> </div>
</> </>
} }
footer={
<div className="pagination-container">
<Pagination
current={page}
pageSize={20}
total={total}
showSizeChanger={false}
onChange={setPage}
/>
</div>
}
> >
{/* 批量加入分组弹窗 */} {/* 批量加入分组弹窗 */}
<BatchAddModal <BatchAddModal
@@ -169,17 +162,25 @@ const TrafficPoolList: React.FC = () => {
<FilterModal <FilterModal
visible={showFilter} visible={showFilter}
onClose={() => setShowFilter(false)} onClose={() => setShowFilter(false)}
deviceOptions={deviceOptions} onConfirm={filters => {
packageOptions={packageOptions} // 更新筛选条件
deviceId={deviceId} setSelectedDevices(
setDeviceId={setDeviceId} filters.deviceIds.map(id => ({
packageId={packageId} id: parseInt(id),
setPackageId={setPackageId} memo: "",
valueLevel={valueLevel} imei: "",
setValueLevel={setValueLevel} wechatId: "",
userStatus={userStatus} status: "offline" as const,
setUserStatus={setUserStatus} })),
onReset={resetFilter} );
setPackageId(filters.packageId);
setScenarioId(filters.scenarioId);
setUserValue(filters.userValue);
setUserStatus(filters.userStatus);
// 重新获取列表
getList();
}}
scenarioOptions={scenarioOptions}
/> />
<div className={styles.listWrap}> <div className={styles.listWrap}>
{list.length === 0 && !loading ? ( {list.length === 0 && !loading ? (
@@ -192,7 +193,9 @@ const TrafficPoolList: React.FC = () => {
className={styles.card} className={styles.card}
style={{ cursor: "pointer" }} style={{ cursor: "pointer" }}
onClick={() => onClick={() =>
navigate(`/traffic-pool/detail/${item.sourceId}/${item.id}`) navigate(
`/mine/traffic-pool/detail/${item.sourceId}/${item.id}`,
)
} }
> >
<div className={styles.cardContent}> <div className={styles.cardContent}>
@@ -235,23 +238,6 @@ const TrafficPoolList: React.FC = () => {
</div> </div>
)} )}
</div> </div>
{/* 分页 */}
{total > pageSize && (
<div className={styles.pagination}>
<button disabled={page === 1} onClick={() => setPage(page - 1)}>
</button>
<span>
{page} / {Math.ceil(total / pageSize)}
</span>
<button
disabled={page === Math.ceil(total / pageSize)}
onClick={() => setPage(page + 1)}
>
</button>
</div>
)}
</Layout> </Layout>
); );
}; };

View File

@@ -36,12 +36,12 @@ const routes = [
auth: true, auth: true,
}, },
{ {
path: "/traffic-pool", path: "/mine/traffic-pool",
element: <TrafficPool />, element: <TrafficPool />,
auth: true, auth: true,
}, },
{ {
path: "/traffic-pool/detail/:wxid/:userId", path: "/mine/traffic-pool/detail/:wxid/:userId",
element: <TrafficPoolDetail />, element: <TrafficPoolDetail />,
auth: true, auth: true,
}, },

View File

@@ -11,6 +11,7 @@ export interface Device {
nickname?: string; nickname?: string;
battery?: number; battery?: number;
lastActive?: string; lastActive?: string;
avatar?: string;
features?: { features?: {
autoAddFriend?: boolean; autoAddFriend?: boolean;
autoReply?: boolean; autoReply?: boolean;