From 8749a5995792a58342f7ebede90fadafd7b3f6e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Fri, 8 Aug 2025 14:59:59 +0800 Subject: [PATCH] =?UTF-8?q?FEAT=20=3D>=20=E6=9C=AC=E6=AC=A1=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E9=A1=B9=E7=9B=AE=E4=B8=BA=EF=BC=9A=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E5=B8=B3=E8=99=9F=E9=81=B8=E6=93=87=E7=B5=84=E4=BB=B6=EF=BC=8C?= =?UTF-8?q?=E7=B0=A1=E5=8C=96=E7=8B=80=E6=85=8B=E7=AE=A1=E7=90=86=EF=BC=8C?= =?UTF-8?q?=E4=B8=A6=E6=9B=B4=E6=96=B0=E6=B8=AC=E8=A9=A6=E9=A0=81=E9=9D=A2?= =?UTF-8?q?=E4=BB=A5=E4=BD=BF=E7=94=A8=E6=96=B0=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/AccountSelection/data.ts | 34 +++ .../src/components/AccountSelection/index.tsx | 244 ++---------------- .../AccountSelection/selectionPopup.tsx | 202 +++++++++++++++ nkebao/src/pages/mobile/test/select.tsx | 33 ++- .../traffic-distribution/form/index.tsx | 23 +- 5 files changed, 289 insertions(+), 247 deletions(-) create mode 100644 nkebao/src/components/AccountSelection/data.ts create mode 100644 nkebao/src/components/AccountSelection/selectionPopup.tsx diff --git a/nkebao/src/components/AccountSelection/data.ts b/nkebao/src/components/AccountSelection/data.ts new file mode 100644 index 000000000..8c65ee8d3 --- /dev/null +++ b/nkebao/src/components/AccountSelection/data.ts @@ -0,0 +1,34 @@ +// 账号对象类型 +export interface AccountItem { + id: number; + userName: string; + realName: string; + departmentName: string; + avatar?: string; + [key: string]: any; +} +//弹窗的 +export interface SelectionPopupProps { + visible: boolean; + onVisibleChange: (visible: boolean) => void; + selectedOptions: AccountItem[]; + onSelect: (options: AccountItem[]) => void; + readonly?: boolean; + onConfirm?: (selectedOptions: AccountItem[]) => void; +} + +// 组件属性接口 +export interface AccountSelectionProps { + selectedOptions: AccountItem[]; + onSelect: (options: AccountItem[]) => void; + accounts?: AccountItem[]; // 可选:用于在外层显示已选账号详情 + placeholder?: string; + className?: string; + visible?: boolean; + onVisibleChange?: (visible: boolean) => void; + selectedListMaxHeight?: number; + showInput?: boolean; + showSelectedList?: boolean; + readonly?: boolean; + onConfirm?: (selectedOptions: AccountItem[]) => void; +} diff --git a/nkebao/src/components/AccountSelection/index.tsx b/nkebao/src/components/AccountSelection/index.tsx index 3f8aace6c..96def66be 100644 --- a/nkebao/src/components/AccountSelection/index.tsx +++ b/nkebao/src/components/AccountSelection/index.tsx @@ -1,42 +1,13 @@ -import React, { useState, useEffect } from "react"; +import React, { useState } from "react"; import { SearchOutlined, DeleteOutlined } from "@ant-design/icons"; -import { Popup } from "antd-mobile"; import { Button, Input } from "antd"; -import { getAccountList } 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"; - -// 账号对象类型 -export interface AccountItem { - id: number; - userName: string; - realName: string; - departmentName: string; - avatar?: string; - [key: string]: any; -} - -// 组件属性接口 -interface AccountSelectionProps { - value: number[]; - onChange: (ids: number[]) => void; - accounts?: AccountItem[]; - placeholder?: string; - className?: string; - visible?: boolean; - onVisibleChange?: (visible: boolean) => void; - selectedListMaxHeight?: number; - showInput?: boolean; - showSelectedList?: boolean; - readonly?: boolean; - onConfirm?: (selectedIds: number[], selectedItems: AccountItem[]) => void; -} +import SelectionPopup from "./selectionPopup"; +import { AccountItem, AccountSelectionProps } from "./data"; export default function AccountSelection({ - value, - onChange, + selectedOptions, + onSelect, accounts: propAccounts = [], placeholder = "选择账号", className = "", @@ -49,10 +20,6 @@ export default function AccountSelection({ onConfirm, }: AccountSelectionProps) { const [popupVisible, setPopupVisible] = useState(false); - const [accountsList, setAccountsList] = useState(propAccounts); - const [searchQuery, setSearchQuery] = useState(""); - const [currentPage, setCurrentPage] = useState(1); - const [loading, setLoading] = useState(false); // 受控弹窗逻辑 const realVisible = visible !== undefined ? visible : popupVisible; @@ -61,110 +28,22 @@ export default function AccountSelection({ if (visible === undefined) setPopupVisible(v); }; - // 打开弹窗时先显示弹窗再请求账号数据 + // 打开弹窗 const openPopup = () => { if (readonly) return; - setCurrentPage(1); - setSearchQuery(""); setRealVisible(true); - setTimeout(async () => { - if (typeof getAccountList === "function") { - setLoading(true); - try { - const response = await getAccountList({ - page: 1, - limit: 100, - keyword: "", - }); - if (response && response.list) { - setAccountsList(response.list); - } - } catch (e) { - } finally { - setLoading(false); - } - } - }, 0); - }; - - // 搜索时请求账号数据 - useEffect(() => { - if (!realVisible) return; - const timer = setTimeout(async () => { - setCurrentPage(1); - if (typeof getAccountList === "function") { - setLoading(true); - try { - const response = await getAccountList({ - page: 1, - limit: 100, - keyword: searchQuery, - }); - if (response && response.list) { - setAccountsList(response.list); - } - } catch (e) { - } finally { - setLoading(false); - } - } - }, 400); - return () => clearTimeout(timer); - }, [searchQuery, realVisible]); - - // 渲染和过滤都依赖内部accountsList - const filteredAccounts = accountsList.filter( - acc => - acc.userName.includes(searchQuery) || - acc.realName.includes(searchQuery) || - acc.departmentName.includes(searchQuery), - ); - - // 处理账号选择 - const handleAccountToggle = (accountId: number) => { - if (readonly) return; - const uniqueValue = [...new Set(value)]; - const newSelected = uniqueValue.includes(accountId) - ? uniqueValue.filter(id => id !== accountId) - : [...uniqueValue, accountId]; - onChange(newSelected); }; // 获取显示文本 const getDisplayText = () => { - const uniqueValue = [...new Set(value)]; - if (uniqueValue.length === 0) return ""; - return `已选择 ${uniqueValue.length} 个账号`; + if (selectedOptions.length === 0) return ""; + return `已选择 ${selectedOptions.length} 个账号`; }; - // 获取已选账号详细信息 - 去重处理 - const uniqueValue = [...new Set(value)]; - const selectedAccountObjs = [ - ...accountsList.filter(acc => uniqueValue.includes(acc.id)), - ...uniqueValue - .filter(id => !accountsList.some(acc => acc.id === id)) - .map(id => ({ - id, - userName: String(id), - realName: "", - departmentName: "", - })), - ]; - // 删除已选账号 const handleRemoveAccount = (id: number) => { if (readonly) return; - const uniqueValue = [...new Set(value)]; - onChange(uniqueValue.filter(d => d !== id)); - }; - - // 确认选择 - const handleConfirm = () => { - if (onConfirm) { - const uniqueValue = [...new Set(value)]; - onConfirm(uniqueValue, selectedAccountObjs); - } - setRealVisible(false); + onSelect(selectedOptions.filter(d => d.id !== id)); }; return ( @@ -188,7 +67,7 @@ export default function AccountSelection({ )} {/* 已选账号列表窗口 */} - {showSelectedList && selectedAccountObjs.length > 0 && ( + {showSelectedList && selectedOptions.length > 0 && (
- {selectedAccountObjs.map(acc => ( + {selectedOptions.map(acc => (
)} {/* 弹窗 */} - setRealVisible(false)} - position="bottom" - bodyStyle={{ height: "100vh" }} - > - {}} - /> - } - footer={ - setRealVisible(false)} - onConfirm={handleConfirm} - /> - } - > -
- {loading ? ( -
-
加载中...
-
- ) : filteredAccounts.length > 0 ? ( -
- {filteredAccounts.map(acc => ( - - ))} -
- ) : ( -
-
- {searchQuery - ? `没有找到包含"${searchQuery}"的账号` - : "没有找到账号"} -
-
- )} -
-
-
+ ); } diff --git a/nkebao/src/components/AccountSelection/selectionPopup.tsx b/nkebao/src/components/AccountSelection/selectionPopup.tsx new file mode 100644 index 000000000..830215583 --- /dev/null +++ b/nkebao/src/components/AccountSelection/selectionPopup.tsx @@ -0,0 +1,202 @@ +import React, { useEffect, useMemo, useRef, useState } from "react"; +import { Popup } from "antd-mobile"; +import Layout from "@/components/Layout/Layout"; +import PopupHeader from "@/components/PopuLayout/header"; +import PopupFooter from "@/components/PopuLayout/footer"; +import style from "./index.module.scss"; +import { getAccountList } from "./api"; +import { AccountItem, SelectionPopupProps } from "./data"; + +export default function SelectionPopup({ + visible, + onVisibleChange, + selectedOptions, + onSelect, + readonly = false, + onConfirm, +}: SelectionPopupProps) { + const [accounts, setAccounts] = useState([]); + const [searchQuery, setSearchQuery] = useState(""); + const [currentPage, setCurrentPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const [totalAccounts, setTotalAccounts] = useState(0); + const [loading, setLoading] = useState(false); + + // 累积已加载过的账号,确保确认时能返回更完整的对象 + const loadedAccountMapRef = useRef>(new Map()); + + const pageSize = 20; + + const fetchAccounts = async (page: number, keyword: string = "") => { + setLoading(true); + try { + const params: any = { page, limit: pageSize }; + if (keyword.trim()) params.keyword = keyword.trim(); + + const response = await getAccountList(params); + if (response && response.list) { + setAccounts(response.list); + const total: number = response.total || response.list.length || 0; + setTotalAccounts(total); + setTotalPages(Math.max(1, Math.ceil(total / pageSize))); + + // 累积到映射表 + response.list.forEach((acc: AccountItem) => { + loadedAccountMapRef.current.set(acc.id, acc); + }); + } else { + setAccounts([]); + setTotalAccounts(0); + setTotalPages(1); + } + } catch (error) { + console.error("获取账号列表失败:", error); + } finally { + setLoading(false); + } + }; + + const handleAccountToggle = (account: AccountItem) => { + if (readonly || !onSelect) return; + const isSelected = selectedOptions.some(opt => opt.id === account.id); + const next = isSelected + ? selectedOptions.filter(opt => opt.id !== account.id) + : selectedOptions.concat(account); + onSelect(next); + }; + + const handleConfirm = () => { + if (onConfirm) { + onConfirm(selectedOptions); + } + onVisibleChange(false); + }; + + // 弹窗打开时初始化数据 + useEffect(() => { + if (visible) { + setCurrentPage(1); + setSearchQuery(""); + loadedAccountMapRef.current.clear(); + fetchAccounts(1, ""); + } + }, [visible]); + + // 搜索防抖 + useEffect(() => { + if (!visible) return; + if (searchQuery === "") return; + const timer = setTimeout(() => { + setCurrentPage(1); + fetchAccounts(1, searchQuery); + }, 500); + return () => clearTimeout(timer); + }, [searchQuery, visible]); + + // 页码变化 + useEffect(() => { + if (!visible || currentPage === 1) return; + fetchAccounts(currentPage, searchQuery); + }, [currentPage, visible, searchQuery]); + + const selectedIdSet = useMemo( + () => new Set(selectedOptions.map(opt => opt.id)), + [selectedOptions], + ); + + return ( + onVisibleChange(false)} + position="bottom" + bodyStyle={{ height: "100vh" }} + > + fetchAccounts(currentPage, searchQuery)} + /> + } + footer={ + onVisibleChange(false)} + onConfirm={handleConfirm} + /> + } + > +
+ {loading ? ( +
+
加载中...
+
+ ) : accounts.length > 0 ? ( +
+ {accounts.map(acc => ( + + ))} +
+ ) : ( +
+
+ {searchQuery + ? `没有找到包含"${searchQuery}"的账号` + : "没有找到账号"} +
+
+ )} +
+
+
+ ); +} diff --git a/nkebao/src/pages/mobile/test/select.tsx b/nkebao/src/pages/mobile/test/select.tsx index 65dc43e76..c4989299a 100644 --- a/nkebao/src/pages/mobile/test/select.tsx +++ b/nkebao/src/pages/mobile/test/select.tsx @@ -11,15 +11,13 @@ import { isDevelopment } from "@/utils/env"; import { GroupSelectionItem } from "@/components/GroupSelection/data"; import { ContentItem } from "@/components/ContentSelection/data"; import { FriendSelectionItem } from "@/components/FriendSelection/data"; +import { AccountItem } from "@/components/AccountSelection/data"; const ComponentTest: React.FC = () => { - const [activeTab, setActiveTab] = useState("libraries"); + const [activeTab, setActiveTab] = useState("accounts"); // 设备选择状态 const [selectedDevices, setSelectedDevices] = useState([]); - // 好友选择状态 - const [selectedFriends, setSelectedFriends] = useState([]); - // 群组选择状态 const [selectedGroups, setSelectedGroups] = useState( [], @@ -28,7 +26,7 @@ const ComponentTest: React.FC = () => { // 内容库选择状态 const [selectedContent, setSelectedContent] = useState([]); - const [selectedAccounts, setSelectedAccounts] = useState([]); + const [selectedAccounts, setSelectedAccounts] = useState([]); const [selectedFriendsOptions, setSelectedFriendsOptions] = useState< FriendSelectionItem[] >([]); @@ -137,15 +135,24 @@ const ComponentTest: React.FC = () => {

AccountSelection 组件测试

-
- 已选账号: - {selectedAccounts.length > 0 - ? selectedAccounts.join(", ") - : "无"} +
+ 已选账号: {selectedAccounts.length} 个 +
+ 账号ID:{" "} + {selectedAccounts.map(a => a.id).join(", ") || "无"}
diff --git a/nkebao/src/pages/mobile/workspace/traffic-distribution/form/index.tsx b/nkebao/src/pages/mobile/workspace/traffic-distribution/form/index.tsx index 5155245cc..bf82ae584 100644 --- a/nkebao/src/pages/mobile/workspace/traffic-distribution/form/index.tsx +++ b/nkebao/src/pages/mobile/workspace/traffic-distribution/form/index.tsx @@ -17,6 +17,7 @@ import Layout from "@/components/Layout/Layout"; import NavCommon from "@/components/NavCommon"; import AccountSelection from "@/components/AccountSelection"; import { getAccountList } from "@/components/AccountSelection/api"; +import { AccountItem } from "@/components/AccountSelection/data"; import { getTrafficDistributionDetail, updateTrafficDistribution, @@ -62,7 +63,7 @@ const TrafficDistributionForm: React.FC = () => { const isEdit = !!id; const [current, setCurrent] = useState(0); - const [selectedAccountIds, setSelectedAccountIds] = useState([]); + const [selectedAccounts, setSelectedAccounts] = useState([]); const [distributeType, setDistributeType] = useState(1); const [maxPerDay, setMaxPerDay] = useState(50); const [timeType, setTimeType] = useState(1); @@ -117,11 +118,15 @@ const TrafficDistributionForm: React.FC = () => { setMaxPerDay(config.maxPerDay); setTimeType(config.timeType); - // 去重账号ID - const uniqueAccountIds = [ - ...new Set(config.account.map(id => Number(id))), - ]; - setSelectedAccountIds(uniqueAccountIds); + // 设置账号信息 - 这里需要根据实际API返回的数据结构调整 + // 暂时使用ID创建简单的AccountItem对象 + const accountItems = config.account.map((id: any) => ({ + id: Number(id), + userName: `账号${id}`, + realName: "", + departmentName: "", + })); + setSelectedAccounts(accountItems); // 设置时间范围 - 使用dayjs格式 if (config.timeType === 2 && config.startTime && config.endTime) { @@ -167,7 +172,7 @@ const TrafficDistributionForm: React.FC = () => { endTime: timeType === 2 && timeRange?.[1] ? timeRange[1].format("HH:mm") : "", devices: detailData?.config.devices || [], - account: selectedAccountIds, + account: selectedAccounts.map(acc => acc.id), pools: selectedPools, enabled: true, }; @@ -236,8 +241,8 @@ const TrafficDistributionForm: React.FC = () => { className={style.accountSelectItem} >