Merge branch 'yongpxu-dev' into yongpxu-dev2
This commit is contained in:
18
Cunkebao/dist/.vite/manifest.json
vendored
18
Cunkebao/dist/.vite/manifest.json
vendored
@@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
10
Cunkebao/dist/index.html
vendored
10
Cunkebao/dist/index.html
vendored
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ export interface DeviceSelectionItem {
|
|||||||
wxid?: string;
|
wxid?: string;
|
||||||
nickname?: string;
|
nickname?: string;
|
||||||
usedInPlans?: number;
|
usedInPlans?: number;
|
||||||
|
avatar?: string;
|
||||||
|
totalFriend?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 组件属性接口
|
// 组件属性接口
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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 || "");
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
173
Cunkebao/src/pages/mobile/mine/devices/index.module.scss
Normal file
173
Cunkebao/src/pages/mobile/mine/devices/index.module.scss
Normal 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;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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");
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user