diff --git a/Cunkebao/dist/.vite/manifest.json b/Cunkebao/dist/.vite/manifest.json index 1fc771a83..454623cd5 100644 --- a/Cunkebao/dist/.vite/manifest.json +++ b/Cunkebao/dist/.vite/manifest.json @@ -1,14 +1,18 @@ { - "_charts-CLRTJ7Uf.js": { - "file": "assets/charts-CLRTJ7Uf.js", + "_charts-DRkEUjsu.js": { + "file": "assets/charts-DRkEUjsu.js", "name": "charts", "imports": [ - "_ui-BFvqeNzU.js", + "_ui-ltFujOOi.js", "_vendor-2vc8h_ct.js" ] }, - "_ui-BFvqeNzU.js": { - "file": "assets/ui-BFvqeNzU.js", + "_ui-D0C0OGrH.css": { + "file": "assets/ui-D0C0OGrH.css", + "src": "_ui-D0C0OGrH.css" + }, + "_ui-ltFujOOi.js": { + "file": "assets/ui-ltFujOOi.js", "name": "ui", "imports": [ "_vendor-2vc8h_ct.js" @@ -17,10 +21,6 @@ "assets/ui-D0C0OGrH.css" ] }, - "_ui-D0C0OGrH.css": { - "file": "assets/ui-D0C0OGrH.css", - "src": "_ui-D0C0OGrH.css" - }, "_utils-6WF66_dS.js": { "file": "assets/utils-6WF66_dS.js", "name": "utils", @@ -33,18 +33,18 @@ "name": "vendor" }, "index.html": { - "file": "assets/index-C48GlG01.js", + "file": "assets/index-DV2QF8R9.js", "name": "index", "src": "index.html", "isEntry": true, "imports": [ "_vendor-2vc8h_ct.js", - "_ui-BFvqeNzU.js", + "_ui-ltFujOOi.js", "_utils-6WF66_dS.js", - "_charts-CLRTJ7Uf.js" + "_charts-DRkEUjsu.js" ], "css": [ - "assets/index-Ta4vyxDJ.css" + "assets/index-DqyE1UEk.css" ] } } \ No newline at end of file diff --git a/Cunkebao/dist/index.html b/Cunkebao/dist/index.html index 8a58e165a..76889c669 100644 --- a/Cunkebao/dist/index.html +++ b/Cunkebao/dist/index.html @@ -11,13 +11,13 @@ - + - + - + - +
diff --git a/Cunkebao/src/components/PoolSelection/api.ts b/Cunkebao/src/components/PoolSelection/api.ts new file mode 100644 index 000000000..552be7e6e --- /dev/null +++ b/Cunkebao/src/components/PoolSelection/api.ts @@ -0,0 +1,34 @@ +import request from "@/api/request"; + +// 请求参数接口 +export interface Request { + keyword: string; + /** + * 条数 + */ + limit: string; + /** + * 分页 + */ + page: string; + [property: string]: any; +} + +// 获取流量池包列表 +export function getPoolPackages(params: Request) { + return request("/v1/traffic/pool/getPackage", params, "GET"); +} + +// 保留原接口以兼容现有代码 +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 000000000..95ff55d4f --- /dev/null +++ b/Cunkebao/src/components/PoolSelection/data.ts @@ -0,0 +1,61 @@ +// 流量池包接口类型 +export interface PoolPackageItem { + id: number; + name: string; + description: string; + createTime: string; + num: number; +} + +// 原流量池接口类型(保留以兼容现有代码) +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 PoolSelectionItem { + id: string; + avatar?: string; + name: string; + wechatId?: string; + mobile?: string; + nickname?: string; + createTime?: string; + description?: string; + num?: number; + [key: string]: any; +} + +// 组件属性接口 +export interface PoolSelectionProps { + selectedOptions: PoolSelectionItem[]; + onSelect: (Pools: PoolSelectionItem[]) => void; + onSelectDetail?: (Pools: PoolPackageItem[]) => void; + placeholder?: string; + className?: string; + visible?: boolean; + onVisibleChange?: (visible: boolean) => void; + selectedListMaxHeight?: number; + showInput?: boolean; + showSelectedList?: boolean; + readonly?: boolean; + onConfirm?: ( + selectedIds: string[], + selectedItems: PoolSelectionItem[], + ) => void; +} diff --git a/Cunkebao/src/components/PoolSelection/index.module.scss b/Cunkebao/src/components/PoolSelection/index.module.scss new file mode 100644 index 000000000..f0421ca1f --- /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: 8px; + 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 000000000..5118bacde --- /dev/null +++ b/Cunkebao/src/components/PoolSelection/index.tsx @@ -0,0 +1,127 @@ +import React, { useState } from "react"; +import { SearchOutlined, DeleteOutlined } from "@ant-design/icons"; +import { Button, Input } from "antd"; +import style from "./index.module.scss"; +import SelectionPopup from "./selectionPopup"; +import { PoolSelectionProps } from "./data"; +export default function PoolSelection({ + selectedOptions, + onSelect, + onSelectDetail, + placeholder = "选择流量池", + className = "", + visible, + onVisibleChange, + selectedListMaxHeight = 300, + showInput = true, + showSelectedList = true, + readonly = false, + onConfirm, +}: PoolSelectionProps) { + 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 || "").charAt(0)} +
+
+
{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 000000000..d952efc8a --- /dev/null +++ b/Cunkebao/src/components/PoolSelection/selectionPopup.tsx @@ -0,0 +1,213 @@ +import React, { useState, useEffect } from "react"; +import { Popup, Checkbox } from "antd-mobile"; + +import { getPoolPackages, Request } 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 { PoolSelectionItem, PoolPackageItem } from "./data"; + +// 弹窗属性接口 +interface SelectionPopupProps { + visible: boolean; + onVisibleChange: (visible: boolean) => void; + selectedOptions: PoolSelectionItem[]; + onSelect: (items: PoolSelectionItem[]) => void; + onSelectDetail?: (items: PoolPackageItem[]) => void; + readonly?: boolean; + onConfirm?: ( + selectedIds: string[], + selectedItems: PoolSelectionItem[], + ) => void; +} + +export default function SelectionPopup({ + visible, + onVisibleChange, + selectedOptions, + onSelect, + onSelectDetail, + readonly = false, + onConfirm, +}: SelectionPopupProps) { + const [poolPackages, setPoolPackages] = 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 fetchPoolPackages = async (page: number, keyword: string = "") => { + setLoading(true); + try { + const params: Request = { + page: String(page), + limit: "20", + keyword: keyword.trim(), + }; + + const response = await getPoolPackages(params); + if (response && response.list) { + setPoolPackages(response.list); + setTotalItems(response.total || 0); + setTotalPages(Math.ceil((response.total || 0) / 20)); + } + } catch (error) { + console.error("获取流量池包列表失败:", error); + } finally { + setLoading(false); + } + }; + + // 处理流量池包选择 + const handlePackageToggle = (item: PoolPackageItem) => { + if (readonly) return; + + // 将PoolPackageItem转换为GroupSelectionItem格式 + const selectionItem: PoolSelectionItem = { + id: String(item.id), + name: item.name, + description: item.description, + createTime: item.createTime, + num: item.num, + // 保留原始数据 + 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 = poolPackages.filter(packageItem => + newSelectedItems.some(g => g.id === String(packageItem.id)), + ); + onSelectDetail(selectedItemObjs); + } + }; + + // 确认选择 + const handleConfirm = () => { + if (onConfirm) { + onConfirm( + selectedOptions.map(item => item.id), + selectedOptions, + ); + } + onVisibleChange(false); + }; + + // 弹窗打开时初始化数据(只执行一次) + useEffect(() => { + if (visible) { + setCurrentPage(1); + setSearchQuery(""); + fetchPoolPackages(1, ""); + } + }, [visible]); + + // 搜索防抖(只在弹窗打开且搜索词变化时执行) + useEffect(() => { + if (!visible || searchQuery === "") return; + + const timer = setTimeout(() => { + setCurrentPage(1); + fetchPoolPackages(1, searchQuery); + }, 500); + + return () => clearTimeout(timer); + }, [searchQuery, visible]); + + // 页码变化时请求数据(只在弹窗打开且页码不是1时执行) + useEffect(() => { + if (!visible || currentPage === 1) return; + fetchPoolPackages(currentPage, searchQuery); + }, [currentPage, visible, searchQuery]); + + return ( + onVisibleChange(false)} + position="bottom" + bodyStyle={{ height: "100vh" }} + > + fetchPoolPackages(currentPage, searchQuery)} + /> + } + footer={ + onVisibleChange(false)} + onConfirm={handleConfirm} + /> + } + > +
+ {loading ? ( +
+
加载中...
+
+ ) : poolPackages.length > 0 ? ( +
+ {poolPackages.map(item => ( +
+ g.id === String(item.id), + )} + onChange={() => !readonly && handlePackageToggle(item)} + disabled={readonly} + style={{ marginRight: 12 }} + /> +
+
+ {item.name ? item.name.charAt(0) : "?"} +
+
+
{item.name}
+
+ 描述: {item.description || "无描述"} +
+
+ 创建时间: {item.createTime} +
+
+ 包含数量: {item.num} +
+
+
+
+ ))} +
+ ) : ( +
+
+ {searchQuery + ? `没有找到包含"${searchQuery}"的流量池包` + : "没有找到流量池包"} +
+
+ )} +
+
+
+ ); +} diff --git a/Cunkebao/src/pages/mobile/test/select.tsx b/Cunkebao/src/pages/mobile/test/select.tsx index d7c73a695..f7c1edd7f 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 { 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,58 @@ const ComponentTest: React.FC = () => {
+ + +
+

GroupSelection 组件测试

+ +
+ 已选群组: {selectedGroups.length} 个 +
+ 群组ID:{" "} + {selectedGroups.map(g => g.id).join(", ") || "无"} +
+
+
+ + +
+

PoolSelection 组件测试

+ +
+ 已选流量池: {selectedPools.length} 个 +
+ 流量池ID:{" "} + {selectedPools.map(p => p.id).join(", ") || "无"} +
+
+
diff --git a/Cunkebao/src/pages/mobile/workspace/auto-group/form/components/BasicSettings.tsx b/Cunkebao/src/pages/mobile/workspace/auto-group/form/components/BasicSettings.tsx new file mode 100644 index 000000000..39712e2dd --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/auto-group/form/components/BasicSettings.tsx @@ -0,0 +1,336 @@ +import React, { useImperativeHandle, forwardRef, useEffect } from "react"; +import { Button, Card, Switch, Form, InputNumber } from "antd"; +import { Input } from "antd"; + +const { TextArea } = Input; + +interface BasicSettingsProps { + initialValues?: { + name: string; + startTime: string; + endTime: string; + groupSizeMin: number; + groupSizeMax: number; + maxGroupsPerDay: number; + groupNameTemplate: string; + groupDescription: string; + status: number; + }; +} + +export interface BasicSettingsRef { + validate: () => Promise; + getValues: () => any; +} + +const BasicSettings = forwardRef( + ( + { + initialValues = { + name: "", + startTime: "06:00", + endTime: "23:59", + groupSizeMin: 20, + groupSizeMax: 50, + maxGroupsPerDay: 10, + groupNameTemplate: "", + groupDescription: "", + status: 1, + }, + }, + ref, + ) => { + const [form] = Form.useForm(); + + // 当initialValues变化时,重新设置表单值 + useEffect(() => { + form.setFieldsValue(initialValues); + }, [form, initialValues]); + + // 暴露方法给父组件 + useImperativeHandle(ref, () => ({ + validate: async () => { + try { + await form.validateFields(); + return true; + } catch (error) { + console.log("BasicSettings 表单验证失败:", error); + return false; + } + }, + getValues: () => { + return form.getFieldsValue(); + }, + })); + + return ( +
+ +
{ + // 可以在这里处理表单值变化 + }} + > + {/* 任务名称 */} + + + + + {/* 允许建群的时间段 */} + +
+ + + + + + + +
+
+ + {/* 每日最大建群数 */} + { + const numValue = Number(value); + if (value && (numValue < 1 || numValue > 100)) { + return Promise.reject( + new Error("每日最大建群数在1-100之间"), + ); + } + return Promise.resolve(); + }, + }, + ]} + > + form.setFieldValue("maxGroupsPerDay", value)} + addonBefore={ + + } + addonAfter={ + + } + /> + + + {/* 群组最小人数 */} + { + const numValue = Number(value); + if (value && (numValue < 1 || numValue > 500)) { + return Promise.reject( + new Error("群组最小人数在1-500之间"), + ); + } + return Promise.resolve(); + }, + }, + ]} + > + form.setFieldValue("groupSizeMin", value)} + addonBefore={ + + } + addonAfter={ + + } + /> + + + {/* 群组最大人数 */} + { + const numValue = Number(value); + if (value && (numValue < 1 || numValue > 500)) { + return Promise.reject( + new Error("群组最大人数在1-500之间"), + ); + } + return Promise.resolve(); + }, + }, + ]} + > + form.setFieldValue("groupSizeMax", value)} + addonBefore={ + + } + addonAfter={ + + } + /> + + + {/* 群名称模板 */} + + + + + {/* 群描述 */} + +