diff --git a/Cunkebao/src/components/PoolSelection/api.ts b/Cunkebao/src/components/PoolSelection/api.ts new file mode 100644 index 00000000..2e22d353 --- /dev/null +++ b/Cunkebao/src/components/PoolSelection/api.ts @@ -0,0 +1,15 @@ +import request from "@/api/request"; + +// 获取流量池列表 +export function getPoolList(params: { + page?: string; + pageSize?: string; + keyword?: string; + addStatus?: string; + deviceId?: string; + packageId?: string; + userValue?: string; + [property: string]: any; +}) { + return request("/v1/traffic/pool", params, "GET"); +} diff --git a/Cunkebao/src/components/PoolSelection/data.ts b/Cunkebao/src/components/PoolSelection/data.ts new file mode 100644 index 00000000..27294302 --- /dev/null +++ b/Cunkebao/src/components/PoolSelection/data.ts @@ -0,0 +1,50 @@ +// 流量池接口类型 +export interface PoolItem { + id: number; + identifier: string; + mobile: string; + wechatId: string; + fromd: string; + status: number; + createTime: string; + companyId: number; + sourceId: string; + type: number; + nickname: string; + avatar: string; + gender: number; + phone: string; + alias: string; + packages: any[]; + tags: any[]; +} + +export interface GroupSelectionItem { + id: string; + avatar: string; + name: string; + wechatId?: string; + mobile?: string; + nickname?: string; + createTime?: string; + [key: string]: any; +} + +// 组件属性接口 +export interface GroupSelectionProps { + selectedOptions: GroupSelectionItem[]; + onSelect: (groups: GroupSelectionItem[]) => void; + onSelectDetail?: (groups: PoolItem[]) => void; + placeholder?: string; + className?: string; + visible?: boolean; + onVisibleChange?: (visible: boolean) => void; + selectedListMaxHeight?: number; + showInput?: boolean; + showSelectedList?: boolean; + readonly?: boolean; + onConfirm?: ( + selectedIds: string[], + selectedItems: GroupSelectionItem[], + ) => void; +} diff --git a/Cunkebao/src/components/PoolSelection/index.module.scss b/Cunkebao/src/components/PoolSelection/index.module.scss new file mode 100644 index 00000000..bedba3ef --- /dev/null +++ b/Cunkebao/src/components/PoolSelection/index.module.scss @@ -0,0 +1,206 @@ +.inputWrapper { + position: relative; +} +.inputIcon { + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + color: #bdbdbd; + font-size: 20px; +} +.input { + padding-left: 38px !important; + height: 48px; + border-radius: 16px !important; + border: 1px solid #e5e6eb !important; + font-size: 16px; + background: #f8f9fa; +} +.selectedListRow { + padding: 8px; + border-bottom: 1px solid #f0f0f0; + font-size: 14px; +} +.selectedListRowContent { + flex: 1; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} +.selectedListRowContentText { + flex: 1; +} + +.popupContainer { + display: flex; + flex-direction: column; + height: 100vh; + background: #fff; +} +.popupHeader { + padding: 24px; +} +.popupTitle { + text-align: center; + font-size: 20px; + font-weight: 600; + margin-bottom: 24px; +} +.searchWrapper { + position: relative; + margin-bottom: 16px; +} +.searchInput { + padding-left: 40px !important; + padding-top: 8px !important; + padding-bottom: 8px !important; + border-radius: 24px !important; + border: 1px solid #e5e6eb !important; + font-size: 15px; + background: #f8f9fa; +} +.searchIcon { + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + color: #bdbdbd; + font-size: 16px; +} +.clearBtn { + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + height: 24px; + width: 24px; + border-radius: 50%; + min-width: 24px; +} + +.groupList { + flex: 1; + overflow-y: auto; +} +.groupListInner { + border-top: 1px solid #f0f0f0; +} +.groupItem { + display: flex; + align-items: center; + padding: 16px 24px; + border-bottom: 1px solid #f0f0f0; + transition: background 0.2s; + &:hover { + background: #f5f6fa; + } +} +.groupInfo { + display: flex; + align-items: center; + gap: 12px; + flex: 1; +} +.groupAvatar { + width: 40px; + height: 40px; + border-radius: 50%; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + display: flex; + align-items: center; + justify-content: center; + color: #fff; + font-size: 14px; + font-weight: 500; + overflow: hidden; +} +.avatarImg { + width: 100%; + height: 100%; + object-fit: cover; +} +.groupDetail { + flex: 1; +} +.groupName { + font-weight: 500; + font-size: 16px; + color: #222; + margin-bottom: 2px; +} +.groupId { + font-size: 13px; + color: #888; + margin-bottom: 2px; +} +.groupOwner { + font-size: 13px; + color: #bdbdbd; +} + +.loadingBox { + display: flex; + align-items: center; + justify-content: center; + height: 100%; +} +.loadingText { + color: #888; + font-size: 15px; +} +.emptyBox { + display: flex; + align-items: center; + justify-content: center; + height: 100%; +} +.emptyText { + color: #888; + font-size: 15px; +} + +.paginationRow { + border-top: 1px solid #f0f0f0; + padding: 16px; + display: flex; + align-items: center; + justify-content: space-between; + background: #fff; +} +.totalCount { + font-size: 14px; + color: #888; +} +.paginationControls { + display: flex; + align-items: center; + gap: 8px; +} +.pageBtn { + padding: 0 8px; + height: 32px; + min-width: 32px; +} +.pageInfo { + font-size: 14px; + color: #222; +} + +.popupFooter { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px; + border-top: 1px solid #f0f0f0; + background: #fff; +} +.selectedCount { + font-size: 14px; + color: #888; +} +.footerBtnGroup { + display: flex; + gap: 12px; +} diff --git a/Cunkebao/src/components/PoolSelection/index.tsx b/Cunkebao/src/components/PoolSelection/index.tsx new file mode 100644 index 00000000..715c1a13 --- /dev/null +++ b/Cunkebao/src/components/PoolSelection/index.tsx @@ -0,0 +1,126 @@ +import React, { useState } from "react"; +import { SearchOutlined, DeleteOutlined } from "@ant-design/icons"; +import { Button, Input } from "antd"; +import { Avatar } from "antd-mobile"; +import style from "./index.module.scss"; +import SelectionPopup from "./selectionPopup"; +import { GroupSelectionProps } from "./data"; +export default function PoolSelection({ + selectedOptions, + onSelect, + onSelectDetail, + placeholder = "选择流量池", + className = "", + visible, + onVisibleChange, + selectedListMaxHeight = 300, + showInput = true, + showSelectedList = true, + readonly = false, + onConfirm, +}: GroupSelectionProps) { + const [popupVisible, setPopupVisible] = useState(false); + + // 删除已选流量池项 + const handleRemoveItem = (id: string) => { + if (readonly) return; + onSelect(selectedOptions.filter(item => item.id !== id)); + }; + + // 受控弹窗逻辑 + const realVisible = visible !== undefined ? visible : popupVisible; + const setRealVisible = (v: boolean) => { + if (onVisibleChange) onVisibleChange(v); + if (visible === undefined) setPopupVisible(v); + }; + + // 打开弹窗 + const openPopup = () => { + if (readonly) return; + setRealVisible(true); + }; + + // 获取显示文本 + const getDisplayText = () => { + if (selectedOptions.length === 0) return ""; + return `已选择 ${selectedOptions.length} 个流量池项`; + }; + + return ( + <> + {/* 输入框 */} + {showInput && ( +
+ } + allowClear={!readonly} + size="large" + readOnly={readonly} + disabled={readonly} + style={ + readonly ? { background: "#f5f5f5", cursor: "not-allowed" } : {} + } + /> +
+ )} + {/* 已选流量池列表窗口 */} + {showSelectedList && selectedOptions.length > 0 && ( +
+ {selectedOptions.map(item => ( +
+
+ +
+
{item.nickname || item.name}
+
{item.wechatId || item.mobile}
+
+ {!readonly && ( +
+
+ ))} +
+ )} + {/* 弹窗 */} + + + ); +} diff --git a/Cunkebao/src/components/PoolSelection/selectionPopup.tsx b/Cunkebao/src/components/PoolSelection/selectionPopup.tsx new file mode 100644 index 00000000..9eef91e0 --- /dev/null +++ b/Cunkebao/src/components/PoolSelection/selectionPopup.tsx @@ -0,0 +1,227 @@ +import React, { useState, useEffect } from "react"; +import { Popup, Checkbox } from "antd-mobile"; + +import { getPoolList } from "./api"; +import style from "./index.module.scss"; +import Layout from "@/components/Layout/Layout"; +import PopupHeader from "@/components/PopuLayout/header"; +import PopupFooter from "@/components/PopuLayout/footer"; +import { GroupSelectionItem, PoolItem } from "./data"; + +// 弹窗属性接口 +interface SelectionPopupProps { + visible: boolean; + onVisibleChange: (visible: boolean) => void; + selectedOptions: GroupSelectionItem[]; + onSelect: (items: GroupSelectionItem[]) => void; + onSelectDetail?: (items: PoolItem[]) => void; + readonly?: boolean; + onConfirm?: ( + selectedIds: string[], + selectedItems: GroupSelectionItem[], + ) => void; +} + +export default function SelectionPopup({ + visible, + onVisibleChange, + selectedOptions, + onSelect, + onSelectDetail, + readonly = false, + onConfirm, +}: SelectionPopupProps) { + const [poolItems, setPoolItems] = useState([]); + const [searchQuery, setSearchQuery] = useState(""); + const [currentPage, setCurrentPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const [totalItems, setTotalItems] = useState(0); + const [loading, setLoading] = useState(false); + + // 获取流量池列表API + const fetchPoolItems = async (page: number, keyword: string = "") => { + setLoading(true); + try { + const params: any = { + page: String(page), + pageSize: "20", + }; + + if (keyword.trim()) { + params.keyword = keyword.trim(); + } + + const response = await getPoolList(params); + if (response && response.list) { + setPoolItems(response.list); + setTotalItems(response.total || 0); + setTotalPages(Math.ceil((response.total || 0) / 20)); + } + } catch (error) { + console.error("获取流量池列表失败:", error); + } finally { + setLoading(false); + } + }; + + // 处理流量池项选择 + const handleItemToggle = (item: PoolItem) => { + if (readonly) return; + + // 将PoolItem转换为GroupSelectionItem格式 + const selectionItem: GroupSelectionItem = { + id: String(item.id), + name: item.nickname || item.wechatId, + avatar: item.avatar, + wechatId: item.wechatId, + mobile: item.mobile, + nickname: item.nickname, + createTime: item.createTime, + // 保留原始数据 + originalData: item, + }; + + const newSelectedItems = selectedOptions.some(g => g.id === String(item.id)) + ? selectedOptions.filter(g => g.id !== String(item.id)) + : selectedOptions.concat(selectionItem); + + onSelect(newSelectedItems); + + // 如果有 onSelectDetail 回调,传递完整的流量池对象 + if (onSelectDetail) { + const selectedItemObjs = poolItems.filter(poolItem => + newSelectedItems.some(g => g.id === String(poolItem.id)), + ); + onSelectDetail(selectedItemObjs); + } + }; + + // 确认选择 + const handleConfirm = () => { + if (onConfirm) { + onConfirm( + selectedOptions.map(item => item.id), + selectedOptions, + ); + } + onVisibleChange(false); + }; + + // 弹窗打开时初始化数据(只执行一次) + useEffect(() => { + if (visible) { + setCurrentPage(1); + setSearchQuery(""); + fetchPoolItems(1, ""); + } + }, [visible]); + + // 搜索防抖(只在弹窗打开且搜索词变化时执行) + useEffect(() => { + if (!visible || searchQuery === "") return; + + const timer = setTimeout(() => { + setCurrentPage(1); + fetchPoolItems(1, searchQuery); + }, 500); + + return () => clearTimeout(timer); + }, [searchQuery, visible]); + + // 页码变化时请求数据(只在弹窗打开且页码不是1时执行) + useEffect(() => { + if (!visible || currentPage === 1) return; + fetchPoolItems(currentPage, searchQuery); + }, [currentPage, visible, searchQuery]); + + return ( + onVisibleChange(false)} + position="bottom" + bodyStyle={{ height: "100vh" }} + > + fetchPoolItems(currentPage, searchQuery)} + /> + } + footer={ + onVisibleChange(false)} + onConfirm={handleConfirm} + /> + } + > +
+ {loading ? ( +
+
加载中...
+
+ ) : poolItems.length > 0 ? ( +
+ {poolItems.map(item => ( +
+ g.id === String(item.id), + )} + onChange={() => !readonly && handleItemToggle(item)} + disabled={readonly} + style={{ marginRight: 12 }} + /> +
+
+ {item.avatar ? ( + {item.nickname + ) : ( + (item.nickname || item.wechatId || "").charAt(0) + )} +
+
+
+ {item.nickname || item.wechatId} +
+
+ 微信ID: {item.wechatId} +
+ {item.mobile && ( +
+ 手机号: {item.mobile} +
+ )} +
+
+
+ ))} +
+ ) : ( +
+
+ {searchQuery + ? `没有找到包含"${searchQuery}"的流量池项` + : "没有找到流量池项"} +
+
+ )} +
+
+
+ ); +} diff --git a/Cunkebao/src/pages/mobile/test/select.tsx b/Cunkebao/src/pages/mobile/test/select.tsx index d7c73a69..f586895e 100644 --- a/Cunkebao/src/pages/mobile/test/select.tsx +++ b/Cunkebao/src/pages/mobile/test/select.tsx @@ -7,14 +7,16 @@ import FriendSelection from "@/components/FriendSelection"; import GroupSelection from "@/components/GroupSelection"; import ContentSelection from "@/components/ContentSelection"; import AccountSelection from "@/components/AccountSelection"; +import PoolSelection from "@/components/PoolSelection"; import { isDevelopment } from "@/utils/env"; import { GroupSelectionItem } from "@/components/GroupSelection/data"; import { ContentItem } from "@/components/ContentSelection/data"; import { FriendSelectionItem } from "@/components/FriendSelection/data"; import { DeviceSelectionItem } from "@/components/DeviceSelection/data"; import { AccountItem } from "@/components/AccountSelection/data"; +import { GroupSelectionItem as PoolSelectionItem } from "@/components/PoolSelection/data"; const ComponentTest: React.FC = () => { - const [activeTab, setActiveTab] = useState("devices"); + const [activeTab, setActiveTab] = useState("pools"); // 设备选择状态 const [selectedDevices, setSelectedDevices] = useState( @@ -34,6 +36,9 @@ const ComponentTest: React.FC = () => { const [selectedFriendsOptions, setSelectedFriendsOptions] = useState< FriendSelectionItem[] >([]); + + // 流量池选择状态 + const [selectedPools, setSelectedPools] = useState([]); return ( }>
@@ -155,6 +160,32 @@ const ComponentTest: React.FC = () => {
+ + +
+

PoolSelection 组件测试

+ +
+ 已选流量池: {selectedPools.length} 个 +
+ 流量池ID:{" "} + {selectedPools.map(p => p.id).join(", ") || "无"} +
+
+