diff --git a/nkebao/.env.development b/nkebao/.env.development index fe189d22..da6a111b 100644 --- a/nkebao/.env.development +++ b/nkebao/.env.development @@ -1,4 +1,4 @@ # 基础环境变量示例 -VITE_API_BASE_URL=https://ckbapi.quwanzhi.com +VITE_API_BASE_URL=http://www.yishi.com VITE_APP_TITLE=Nkebao Base diff --git a/nkebao/src/api/request.ts b/nkebao/src/api/request.ts index 2c5372bf..b52caa9c 100644 --- a/nkebao/src/api/request.ts +++ b/nkebao/src/api/request.ts @@ -1,22 +1,27 @@ -import axios, { AxiosInstance, AxiosRequestConfig, Method, AxiosResponse } from 'axios'; -import { Toast } from 'antd-mobile'; +import axios, { + AxiosInstance, + AxiosRequestConfig, + Method, + AxiosResponse, +} from "axios"; +import { Toast } from "antd-mobile"; const DEFAULT_DEBOUNCE_GAP = 1000; const debounceMap = new Map(); const instance: AxiosInstance = axios.create({ - baseURL: (import.meta as any).env?.VITE_API_BASE_URL || '/api', + baseURL: (import.meta as any).env?.VITE_API_BASE_URL || "/api", timeout: 10000, headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, }); -instance.interceptors.request.use(config => { - const token = localStorage.getItem('token'); +instance.interceptors.request.use((config) => { + const token = localStorage.getItem("token"); if (token) { config.headers = config.headers || {}; - config.headers['Authorization'] = `Bearer ${token}`; + config.headers["Authorization"] = `Bearer ${token}`; } return config; }); @@ -27,20 +32,20 @@ instance.interceptors.response.use( if (code === 200 || success) { return res.data.data ?? res.data; } - Toast.show({ content: msg || '接口错误', position: 'top' }); + Toast.show({ content: msg || "接口错误", position: "top" }); if (code === 401) { - localStorage.removeItem('token'); + localStorage.removeItem("token"); const currentPath = window.location.pathname + window.location.search; - if (currentPath === '/login') { - window.location.href = '/login'; + if (currentPath === "/login") { + window.location.href = "/login"; } else { window.location.href = `/login?redirect=${encodeURIComponent(currentPath)}`; } } - return Promise.reject(msg || '接口错误'); + return Promise.reject(msg || "接口错误"); }, - err => { - Toast.show({ content: err.message || '网络异常', position: 'top' }); + (err) => { + Toast.show({ content: err.message || "网络异常", position: "top" }); return Promise.reject(err); } ); @@ -48,17 +53,18 @@ instance.interceptors.response.use( export function request( url: string, data?: any, - method: Method = 'GET', + method: Method = "GET", config?: AxiosRequestConfig, debounceGap?: number ): Promise { - const gap = typeof debounceGap === 'number' ? debounceGap : DEFAULT_DEBOUNCE_GAP; + const gap = + typeof debounceGap === "number" ? debounceGap : DEFAULT_DEBOUNCE_GAP; const key = `${method}_${url}_${JSON.stringify(data)}`; const now = Date.now(); const last = debounceMap.get(key) || 0; if (gap > 0 && now - last < gap) { - Toast.show({ content: '请求过于频繁,请稍后再试', position: 'top' }); - return Promise.reject('请求过于频繁,请稍后再试'); + // Toast.show({ content: '请求过于频繁,请稍后再试', position: 'top' }); + return Promise.reject("请求过于频繁,请稍后再试"); } debounceMap.set(key, now); @@ -67,7 +73,7 @@ export function request( method, ...config, }; - if (method.toUpperCase() === 'GET') { + if (method.toUpperCase() === "GET") { axiosConfig.params = data; } else { axiosConfig.data = data; diff --git a/nkebao/src/components/DeviceSelection/api.ts b/nkebao/src/components/DeviceSelection/api.ts new file mode 100644 index 00000000..1f28ce04 --- /dev/null +++ b/nkebao/src/components/DeviceSelection/api.ts @@ -0,0 +1,10 @@ +import request from "@/api/request"; + +// 获取设备列表 +export function getDeviceList(params: { + page: number; + limit: number; + keyword?: string; +}) { + return request("/v1/devices", params, "GET"); +} diff --git a/nkebao/src/components/DeviceSelection/module.scss b/nkebao/src/components/DeviceSelection/index.module.scss similarity index 78% rename from nkebao/src/components/DeviceSelection/module.scss rename to nkebao/src/components/DeviceSelection/index.module.scss index 34510d81..d457fa24 100644 --- a/nkebao/src/components/DeviceSelection/module.scss +++ b/nkebao/src/components/DeviceSelection/index.module.scss @@ -22,7 +22,7 @@ .popupContainer { display: flex; flex-direction: column; - height: 100%; + height: 100vh; background: #fff; } .popupHeader { @@ -154,3 +154,35 @@ display: flex; gap: 12px; } +.refreshBtn { + width: 36px; + height: 36px; +} +.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; + border-radius: 16px; +} +.pageInfo { + font-size: 14px; + color: #222; + margin: 0 8px; +} diff --git a/nkebao/src/components/DeviceSelection/index.tsx b/nkebao/src/components/DeviceSelection/index.tsx index 67d5072c..a62625d2 100644 --- a/nkebao/src/components/DeviceSelection/index.tsx +++ b/nkebao/src/components/DeviceSelection/index.tsx @@ -1,208 +1,380 @@ -import React, { useState, useEffect } from "react"; -import { SearchOutlined, ReloadOutlined } from "@ant-design/icons"; -import { Input, Button, Checkbox, Popup, Toast } from "antd-mobile"; -import request from "@/api/request"; -import style from "./module.scss"; - -// 设备选择项接口 -interface DeviceSelectionItem { - id: string; - name: string; - imei: string; - wechatId: string; - status: "online" | "offline"; -} - -// 组件属性接口 -interface DeviceSelectionProps { - selectedDevices: string[]; - onSelect: (devices: string[]) => void; - placeholder?: string; - className?: string; -} - -export default function DeviceSelection({ - selectedDevices, - onSelect, - placeholder = "选择设备", - className = "", -}: DeviceSelectionProps) { - const [popupVisible, setPopupVisible] = useState(false); - const [devices, setDevices] = useState([]); - const [searchQuery, setSearchQuery] = useState(""); - const [statusFilter, setStatusFilter] = useState("all"); - const [loading, setLoading] = useState(false); - - // 获取设备列表,支持keyword - const fetchDevices = async (keyword: string = "") => { - setLoading(true); - try { - const res = await request( - "/v1/device/list", - { - page: 1, - limit: 100, - keyword: keyword.trim() || undefined, - }, - "GET" - ); - - if (res && Array.isArray(res.list)) { - setDevices( - res.list.map((d: any) => ({ - id: d.id?.toString() || "", - name: d.memo || d.imei || "", - imei: d.imei || "", - wechatId: d.wechatId || "", - status: d.alive === 1 ? "online" : "offline", - })) - ); - } - } catch (error) { - console.error("获取设备列表失败:", error); - Toast.show({ content: "获取设备列表失败", position: "top" }); - } finally { - setLoading(false); - } - }; - - // 打开弹窗时获取设备列表 - const openPopup = () => { - setSearchQuery(""); - setPopupVisible(true); - fetchDevices(""); - }; - - // 搜索防抖 - useEffect(() => { - if (!popupVisible) return; - const timer = setTimeout(() => { - fetchDevices(searchQuery); - }, 500); - return () => clearTimeout(timer); - }, [searchQuery, popupVisible]); - - // 过滤设备(只保留状态过滤) - const filteredDevices = devices.filter((device) => { - const matchesStatus = - statusFilter === "all" || - (statusFilter === "online" && device.status === "online") || - (statusFilter === "offline" && device.status === "offline"); - return matchesStatus; - }); - - // 处理设备选择 - const handleDeviceToggle = (deviceId: string) => { - if (selectedDevices.includes(deviceId)) { - onSelect(selectedDevices.filter((id) => id !== deviceId)); - } else { - onSelect([...selectedDevices, deviceId]); - } - }; - - // 获取显示文本 - const getDisplayText = () => { - if (selectedDevices.length === 0) return ""; - return `已选择 ${selectedDevices.length} 个设备`; - }; - - return ( - <> - {/* 输入框 */} -
- - -
- - {/* 设备选择弹窗 */} - setPopupVisible(false)} - position="bottom" - bodyStyle={{ height: "80vh" }} - > -
-
-
选择设备
-
-
-
- - setSearchQuery(val)} - className={style.popupSearchInput} - /> -
- -
-
- {loading ? ( -
-
加载中...
-
- ) : ( -
- {filteredDevices.map((device) => ( - - ))} -
- )} -
-
-
- 已选择 {selectedDevices.length} 个设备 -
-
- - -
-
-
-
- - ); -} +import React, { useState, useEffect, useCallback } from "react"; +import { SearchOutlined, ReloadOutlined } from "@ant-design/icons"; +import { Checkbox, Popup, Toast } from "antd-mobile"; +import { Input, Button } from "antd"; +import { getDeviceList } from "./api"; +import style from "./index.module.scss"; +import { DeleteOutlined } from "@ant-design/icons"; + +// 设备选择项接口 +interface DeviceSelectionItem { + id: string; + name: string; + imei: string; + wechatId: string; + status: "online" | "offline"; + wxid?: string; + nickname?: string; + usedInPlans?: number; +} + +// 组件属性接口 +interface DeviceSelectionProps { + selectedDevices: string[]; + onSelect: (devices: string[]) => void; + placeholder?: string; + className?: string; + mode?: "input" | "dialog"; // 新增,默认input + open?: boolean; // 仅mode=dialog时生效 + onOpenChange?: (open: boolean) => void; // 仅mode=dialog时生效 + selectedListMaxHeight?: number; // 新增,已选列表最大高度,默认500 + showInput?: boolean; // 新增 + showSelectedList?: boolean; // 新增 + readonly?: boolean; // 新增 +} + +const PAGE_SIZE = 20; + +const DeviceSelection: React.FC = ({ + selectedDevices, + onSelect, + placeholder = "选择设备", + className = "", + mode = "input", + open, + onOpenChange, + selectedListMaxHeight = 300, // 默认300 + showInput = true, + showSelectedList = true, + readonly = false, +}) => { + // 弹窗控制 + const [popupVisible, setPopupVisible] = useState(false); + const isDialog = mode === "dialog"; + const realVisible = isDialog ? !!open : popupVisible; + const setRealVisible = (v: boolean) => { + if (isDialog && onOpenChange) onOpenChange(v); + if (!isDialog) setPopupVisible(v); + }; + + // 设备数据 + const [devices, setDevices] = useState([]); + const [searchQuery, setSearchQuery] = useState(""); + const [statusFilter, setStatusFilter] = useState("all"); + const [loading, setLoading] = useState(false); + const [currentPage, setCurrentPage] = useState(1); + const [total, setTotal] = useState(0); + + // 获取设备列表,支持keyword和分页 + const fetchDevices = useCallback( + async (keyword: string = "", page: number = 1) => { + setLoading(true); + try { + const res = await getDeviceList({ + page, + limit: PAGE_SIZE, + keyword: keyword.trim() || undefined, + }); + if (res && Array.isArray(res.list)) { + setDevices( + res.list.map((d: any) => ({ + id: d.id?.toString() || "", + name: d.memo || d.imei || "", + imei: d.imei || "", + wechatId: d.wechatId || "", + status: d.alive === 1 ? "online" : "offline", + wxid: d.wechatId || "", + nickname: d.nickname || "", + usedInPlans: d.usedInPlans || 0, + })) + ); + setTotal(res.total || 0); + } + } catch (error) { + console.error("获取设备列表失败:", error); + } finally { + setLoading(false); + } + }, + [] + ); + + // 打开弹窗时获取第一页 + const openPopup = () => { + if (readonly) return; + setSearchQuery(""); + setCurrentPage(1); + setRealVisible(true); + fetchDevices("", 1); + }; + + // 搜索防抖 + useEffect(() => { + if (!realVisible) return; + const timer = setTimeout(() => { + setCurrentPage(1); + fetchDevices(searchQuery, 1); + }, 500); + return () => clearTimeout(timer); + }, [searchQuery, realVisible, fetchDevices]); + + // 翻页时重新请求 + useEffect(() => { + if (!realVisible) return; + fetchDevices(searchQuery, currentPage); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentPage]); + + // 过滤设备(只保留状态过滤) + const filteredDevices = devices.filter((device) => { + const matchesStatus = + statusFilter === "all" || + (statusFilter === "online" && device.status === "online") || + (statusFilter === "offline" && device.status === "offline"); + return matchesStatus; + }); + + const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)); + + // 处理设备选择 + const handleDeviceToggle = (deviceId: string) => { + if (selectedDevices.includes(deviceId)) { + onSelect(selectedDevices.filter((id) => id !== deviceId)); + } else { + onSelect([...selectedDevices, deviceId]); + } + }; + + // 获取显示文本 + const getDisplayText = () => { + if (selectedDevices.length === 0) return ""; + return `已选择 ${selectedDevices.length} 个设备`; + }; + + // 获取已选设备详细信息 + const selectedDeviceObjs = selectedDevices + .map((id) => devices.find((d) => d.id === id)) + .filter(Boolean) as DeviceSelectionItem[]; + + // 删除已选设备 + const handleRemoveDevice = (id: string) => { + if (readonly) return; + onSelect(selectedDevices.filter((d) => d !== id)); + }; + + // 弹窗内容 + const popupContent = ( +
+
+
选择设备
+
+
+
+ + setSearchQuery(val)} + className={style.popupSearchInput} + /> +
+ + +
+
+ {loading ? ( +
+
加载中...
+
+ ) : ( +
+ {filteredDevices.map((device) => ( + + ))} +
+ )} +
+ {/* 分页栏 */} +
+
总计 {total} 个设备
+
+ + + {currentPage} / {totalPages} + + +
+
+
+
+ 已选择 {selectedDevices.length} 个设备 +
+
+ + +
+
+
+ ); + + return ( + <> + {/* mode=input 显示输入框,mode=dialog不显示 */} + {mode === "input" && showInput && ( +
+ } + allowClear={!readonly} + size="large" + readOnly={readonly} + disabled={readonly} + style={ + readonly ? { background: "#f5f5f5", cursor: "not-allowed" } : {} + } + /> +
+ )} + {/* 已选设备列表窗口 */} + {mode === "input" && + showSelectedList && + selectedDeviceObjs.length > 0 && ( +
+ {selectedDeviceObjs.map((device) => ( +
+
+ {device.name} +
+ {!readonly && ( +
+ ))} +
+ )} + {/* 弹窗 */} + setRealVisible(false)} + position="bottom" + bodyStyle={{ height: "100vh" }} + > + {popupContent} + + + ); +}; + +export default DeviceSelection; diff --git a/nkebao/src/components/DeviceSelection/selectionPopup.tsx b/nkebao/src/components/DeviceSelection/selectionPopup.tsx new file mode 100644 index 00000000..8f77eabc --- /dev/null +++ b/nkebao/src/components/DeviceSelection/selectionPopup.tsx @@ -0,0 +1,197 @@ +import React from "react"; +import { SearchOutlined, ReloadOutlined } from "@ant-design/icons"; +import { Input, Button, Checkbox, Popup } from "antd-mobile"; +import style from "./index.module.scss"; + +interface DeviceSelectionItem { + id: string; + name: string; + imei: string; + wechatId: string; + status: "online" | "offline"; + wxid?: string; + nickname?: string; + usedInPlans?: number; +} + +interface SelectionPopupProps { + visible: boolean; + onClose: () => void; + selectedDevices: string[]; + onSelect: (devices: string[]) => void; + devices: DeviceSelectionItem[]; + loading: boolean; + searchQuery: string; + setSearchQuery: (v: string) => void; + statusFilter: string; + setStatusFilter: (v: string) => void; + onRefresh: () => void; + filteredDevices: DeviceSelectionItem[]; + total: number; + currentPage: number; + totalPages: number; + setCurrentPage: (v: number) => void; + onCancel: () => void; + onConfirm: () => void; +} + +const SelectionPopup: React.FC = ({ + visible, + onClose, + selectedDevices, + onSelect, + devices, + loading, + searchQuery, + setSearchQuery, + statusFilter, + setStatusFilter, + onRefresh, + filteredDevices, + total, + currentPage, + totalPages, + setCurrentPage, + onCancel, + onConfirm, +}) => { + // 处理设备选择 + const handleDeviceToggle = (deviceId: string) => { + if (selectedDevices.includes(deviceId)) { + onSelect(selectedDevices.filter((id) => id !== deviceId)); + } else { + onSelect([...selectedDevices, deviceId]); + } + }; + + return ( + {}} + position="bottom" + bodyStyle={{ height: "100vh" }} + closeOnMaskClick={false} + > +
+
+
选择设备
+
+
+
+ + +
+ + +
+
+ {loading ? ( +
+
加载中...
+
+ ) : ( +
+ {filteredDevices.map((device) => ( + + ))} +
+ )} +
+ {/* 分页栏 */} +
+
总计 {total} 个设备
+
+ + + {currentPage} / {totalPages} + + +
+
+
+
+ 已选择 {selectedDevices.length} 个设备 +
+
+ + +
+
+
+
+ ); +}; + +export default SelectionPopup; diff --git a/nkebao/src/components/DeviceSelectionDialog/api.ts b/nkebao/src/components/DeviceSelectionDialog/api.ts new file mode 100644 index 00000000..1f28ce04 --- /dev/null +++ b/nkebao/src/components/DeviceSelectionDialog/api.ts @@ -0,0 +1,10 @@ +import request from "@/api/request"; + +// 获取设备列表 +export function getDeviceList(params: { + page: number; + limit: number; + keyword?: string; +}) { + return request("/v1/devices", params, "GET"); +} diff --git a/nkebao/src/components/DeviceSelectionDialog/module.scss b/nkebao/src/components/DeviceSelectionDialog/index.module.scss similarity index 80% rename from nkebao/src/components/DeviceSelectionDialog/module.scss rename to nkebao/src/components/DeviceSelectionDialog/index.module.scss index 2a9cd2de..e0961f10 100644 --- a/nkebao/src/components/DeviceSelectionDialog/module.scss +++ b/nkebao/src/components/DeviceSelectionDialog/index.module.scss @@ -1,7 +1,7 @@ .popupContainer { display: flex; flex-direction: column; - height: 100%; + height: 100vh; background: #fff; } .popupHeader { @@ -49,11 +49,6 @@ padding: 0 12px; background: #fff; } -.refreshBtn { - min-width: 40px; - height: 40px; - border-radius: 8px; -} .loadingIcon { animation: spin 1s linear infinite; font-size: 16px; @@ -168,3 +163,35 @@ display: flex; gap: 12px; } +.refreshBtn { + width: 36px; + height: 36px; +} +.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; + border-radius: 16px; +} +.pageInfo { + font-size: 14px; + color: #222; + margin: 0 8px; +} diff --git a/nkebao/src/components/DeviceSelectionDialog/index.tsx b/nkebao/src/components/DeviceSelectionDialog/index.tsx index 4e741e15..588d774a 100644 --- a/nkebao/src/components/DeviceSelectionDialog/index.tsx +++ b/nkebao/src/components/DeviceSelectionDialog/index.tsx @@ -1,8 +1,9 @@ import React, { useState, useEffect, useCallback } from "react"; import { SearchOutlined, ReloadOutlined } from "@ant-design/icons"; -import { Input, Button, Checkbox, Popup, Toast } from "antd-mobile"; -import request from "@/api/request"; -import style from "./module.scss"; +import { Checkbox, Popup, Toast } from "antd-mobile"; +import { Input, Button } from "antd"; +import { getDeviceList } from "./api"; +import style from "./index.module.scss"; interface Device { id: string; @@ -31,51 +32,53 @@ export function DeviceSelectionDialog({ const [statusFilter, setStatusFilter] = useState("all"); const [loading, setLoading] = useState(false); const [devices, setDevices] = useState([]); + const [currentPage, setCurrentPage] = useState(1); // 新增 + const [total, setTotal] = useState(0); // 新增 + const pageSize = 20; // 每页条数 - // 获取设备列表,支持keyword - const fetchDevices = useCallback(async (keyword: string = "") => { - setLoading(true); - try { - const response = await request( - "/v1/device/list", - { - page: 1, - limit: 100, + // 获取设备列表,支持keyword和分页 + const fetchDevices = useCallback( + async (keyword: string = "", page: number = 1) => { + setLoading(true); + try { + const response = await getDeviceList({ + page, + limit: pageSize, keyword: keyword.trim() || undefined, - }, - "GET" - ); - - if (response && Array.isArray(response.list)) { - // 转换服务端数据格式为组件需要的格式 - const convertedDevices: Device[] = response.list.map( - (serverDevice: any) => ({ - id: serverDevice.id.toString(), - name: serverDevice.memo || `设备 ${serverDevice.id}`, - imei: serverDevice.imei, - wxid: serverDevice.wechatId || "", - status: serverDevice.alive === 1 ? "online" : "offline", - usedInPlans: 0, // 这个字段需要从其他API获取 - nickname: serverDevice.nickname || "", - }) - ); - setDevices(convertedDevices); + }); + if (response && Array.isArray(response.list)) { + const convertedDevices: Device[] = response.list.map( + (serverDevice: any) => ({ + id: serverDevice.id.toString(), + name: serverDevice.memo || `设备 ${serverDevice.id}`, + imei: serverDevice.imei, + wxid: serverDevice.wechatId || "", + status: serverDevice.alive === 1 ? "online" : "offline", + usedInPlans: 0, + nickname: serverDevice.nickname || "", + }) + ); + setDevices(convertedDevices); + setTotal(response.total || 0); + } + } catch (error) { + console.error("获取设备列表失败:", error); + Toast.show({ + content: "获取设备列表失败,请检查网络连接", + position: "top", + }); + } finally { + setLoading(false); } - } catch (error) { - console.error("获取设备列表失败:", error); - Toast.show({ - content: "获取设备列表失败,请检查网络连接", - position: "top", - }); - } finally { - setLoading(false); - } - }, []); + }, + [] + ); - // 打开弹窗时获取设备列表 + // 打开弹窗时获取第一页 useEffect(() => { if (open) { - fetchDevices(""); + setCurrentPage(1); + fetchDevices("", 1); } }, [open, fetchDevices]); @@ -83,11 +86,19 @@ export function DeviceSelectionDialog({ useEffect(() => { if (!open) return; const timer = setTimeout(() => { - fetchDevices(searchQuery); + setCurrentPage(1); + fetchDevices(searchQuery, 1); }, 500); return () => clearTimeout(timer); }, [searchQuery, open, fetchDevices]); + // 翻页时重新请求 + useEffect(() => { + if (!open) return; + fetchDevices(searchQuery, currentPage); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentPage]); + // 过滤设备(只保留状态过滤) const filteredDevices = devices.filter((device) => { const matchesStatus = @@ -105,12 +116,14 @@ export function DeviceSelectionDialog({ } }; + const totalPages = Math.max(1, Math.ceil(total / pageSize)); + return ( onOpenChange(false)} position="bottom" - bodyStyle={{ height: "80vh" }} + bodyStyle={{ height: "100vh" }} >
@@ -138,7 +151,7 @@ export function DeviceSelectionDialog({
IMEI: {device.imei}
-
微信号: {device.wxid || "-"}
+
微信号: {device.wxid || "-"}
昵称: {device.nickname || "-"}
{device.usedInPlans > 0 && ( @@ -196,6 +209,35 @@ export function DeviceSelectionDialog({
)} + {/* 分页栏 */} +
+
总计 {total} 个设备
+
+ + + {currentPage} / {totalPages} + + +
+
已选择 {selectedDevices.length} 个设备 diff --git a/nkebao/src/components/FriendSelection/api.ts b/nkebao/src/components/FriendSelection/api.ts new file mode 100644 index 00000000..56066f31 --- /dev/null +++ b/nkebao/src/components/FriendSelection/api.ts @@ -0,0 +1,11 @@ +import request from "@/api/request"; + +// 获取好友列表 +export function getFriendList(params: { + page: number; + limit: number; + deviceIds?: string; // 逗号分隔 + keyword?: string; +}) { + return request("/v1/friend", params, "GET"); +} diff --git a/nkebao/src/components/FriendSelection/module.scss b/nkebao/src/components/FriendSelection/index.module.scss similarity index 93% rename from nkebao/src/components/FriendSelection/module.scss rename to nkebao/src/components/FriendSelection/index.module.scss index 86535501..0c82db96 100644 --- a/nkebao/src/components/FriendSelection/module.scss +++ b/nkebao/src/components/FriendSelection/index.module.scss @@ -21,7 +21,7 @@ .popupContainer { display: flex; flex-direction: column; - height: 100%; + height: 100vh; background: #fff; } .popupHeader { diff --git a/nkebao/src/components/FriendSelection/index.tsx b/nkebao/src/components/FriendSelection/index.tsx index 43f37ce5..d67aa0ff 100644 --- a/nkebao/src/components/FriendSelection/index.tsx +++ b/nkebao/src/components/FriendSelection/index.tsx @@ -6,8 +6,8 @@ import { RightOutlined, } from "@ant-design/icons"; import { Input, Button, Popup, Toast } from "antd-mobile"; -import request from "@/api/request"; -import style from "./module.scss"; +import { getFriendList } from "./api"; +import style from "./index.module.scss"; // 微信好友接口类型 interface WechatFriend { @@ -27,6 +27,8 @@ interface FriendSelectionProps { enableDeviceFilter?: boolean; placeholder?: string; className?: string; + visible?: boolean; // 新增 + onVisibleChange?: (visible: boolean) => void; // 新增 } export default function FriendSelection({ @@ -37,6 +39,8 @@ export default function FriendSelection({ enableDeviceFilter = true, placeholder = "选择微信好友", className = "", + visible, + onVisibleChange, }: FriendSelectionProps) { const [popupVisible, setPopupVisible] = useState(false); const [friends, setFriends] = useState([]); @@ -46,24 +50,31 @@ export default function FriendSelection({ const [totalFriends, setTotalFriends] = useState(0); const [loading, setLoading] = useState(false); + // 受控弹窗逻辑 + const realVisible = visible !== undefined ? visible : popupVisible; + const setRealVisible = (v: boolean) => { + if (onVisibleChange) onVisibleChange(v); + if (visible === undefined) setPopupVisible(v); + }; + // 打开弹窗并请求第一页好友 const openPopup = () => { setCurrentPage(1); setSearchQuery(""); - setPopupVisible(true); + setRealVisible(true); fetchFriends(1, ""); }; // 当页码变化时,拉取对应页数据(弹窗已打开时) useEffect(() => { - if (popupVisible && currentPage !== 1) { + if (realVisible && currentPage !== 1) { fetchFriends(currentPage, searchQuery); } - }, [currentPage, popupVisible, searchQuery]); + }, [currentPage, realVisible, searchQuery]); // 搜索防抖 useEffect(() => { - if (!popupVisible) return; + if (!realVisible) return; const timer = setTimeout(() => { setCurrentPage(1); @@ -71,7 +82,7 @@ export default function FriendSelection({ }, 500); return () => clearTimeout(timer); - }, [searchQuery, popupVisible]); + }, [searchQuery, realVisible]); // 获取好友列表API - 添加 keyword 参数 const fetchFriends = async (page: number, keyword: string = "") => { @@ -97,7 +108,7 @@ export default function FriendSelection({ params.deviceIds = deviceIds.join(","); } - const res = await request("/v1/friend", params, "GET"); + const res = await getFriendList(params); if (res && Array.isArray(res.list)) { setFriends( @@ -183,10 +194,10 @@ export default function FriendSelection({ {/* 微信好友选择弹窗 */} setPopupVisible(false)} + visible={realVisible} + onMaskClick={() => setRealVisible(false)} position="bottom" - bodyStyle={{ height: "80vh" }} + bodyStyle={{ height: "100vh" }} >
@@ -312,14 +323,17 @@ export default function FriendSelection({
+ +
+
+ FriendSelection + + +
+
+ GroupSelection + + +
+ +
+
已选设备ID: {selectedDevices.join(", ")}
+
已选好友ID: {selectedFriends.join(", ")}
+
已选群组ID: {selectedGroups.join(", ")}
+
+
+ ); +} diff --git a/nkebao/src/router/module/other.tsx b/nkebao/src/router/module/other.tsx index a458b7dc..f222072c 100644 --- a/nkebao/src/router/module/other.tsx +++ b/nkebao/src/router/module/other.tsx @@ -3,6 +3,7 @@ import Plans from "@/pages/plans/Plans"; import PlanDetail from "@/pages/plans/PlanDetail"; import Orders from "@/pages/orders/Orders"; import ContactImport from "@/pages/contact-import/ContactImport"; +import SelectionTest from "@/components/SelectionTest"; const otherRoutes = [ { @@ -35,6 +36,11 @@ const otherRoutes = [ element: , auth: true, }, + { + path: "/selection-test", + element: , + auth: false, + }, ]; export default otherRoutes; diff --git a/nkebao/src/vite-env.d.ts b/nkebao/src/vite-env.d.ts index 7d0ff9ef..422e41c2 100644 --- a/nkebao/src/vite-env.d.ts +++ b/nkebao/src/vite-env.d.ts @@ -1 +1,11 @@ /// + +declare module "*.scss" { + const content: { [className: string]: string }; + export default content; +} + +declare module "*.css" { + const content: { [className: string]: string }; + export default content; +}