From 34df010769820acd474d72ca6e04d4a05fc03a21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=AC=94=E8=AE=B0=E6=9C=AC=E9=87=8C=E7=9A=84=E6=B0=B8?= =?UTF-8?q?=E5=B9=B3?= Date: Tue, 22 Jul 2025 13:57:24 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=E6=9C=AC=E6=AC=A1=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E6=9B=B4=E6=96=B0=E5=86=85=E5=AE=B9=E5=A6=82=E4=B8=8B?= =?UTF-8?q?=20=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nkebao/src/components/DeviceSelection/api.ts | 10 ++++++++++ nkebao/src/components/DeviceSelection/index.tsx | 16 ++++++---------- .../src/components/DeviceSelectionDialog/api.ts | 10 ++++++++++ .../components/DeviceSelectionDialog/index.tsx | 16 ++++++---------- nkebao/src/components/FriendSelection/api.ts | 11 +++++++++++ nkebao/src/components/FriendSelection/index.tsx | 4 ++-- nkebao/src/components/GroupSelection/api.ts | 10 ++++++++++ nkebao/src/components/GroupSelection/index.tsx | 4 ++-- nkebao/src/vite-env.d.ts | 10 ++++++++++ 9 files changed, 67 insertions(+), 24 deletions(-) create mode 100644 nkebao/src/components/DeviceSelection/api.ts create mode 100644 nkebao/src/components/DeviceSelectionDialog/api.ts create mode 100644 nkebao/src/components/FriendSelection/api.ts create mode 100644 nkebao/src/components/GroupSelection/api.ts diff --git a/nkebao/src/components/DeviceSelection/api.ts b/nkebao/src/components/DeviceSelection/api.ts new file mode 100644 index 00000000..90572df2 --- /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/device/list", params, "GET"); +} diff --git a/nkebao/src/components/DeviceSelection/index.tsx b/nkebao/src/components/DeviceSelection/index.tsx index 67d5072c..673a8c28 100644 --- a/nkebao/src/components/DeviceSelection/index.tsx +++ b/nkebao/src/components/DeviceSelection/index.tsx @@ -1,7 +1,7 @@ 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 { getDeviceList } from "./api"; import style from "./module.scss"; // 设备选择项接口 @@ -37,15 +37,11 @@ export default function DeviceSelection({ const fetchDevices = async (keyword: string = "") => { setLoading(true); try { - const res = await request( - "/v1/device/list", - { - page: 1, - limit: 100, - keyword: keyword.trim() || undefined, - }, - "GET" - ); + const res = await getDeviceList({ + page: 1, + limit: 100, + keyword: keyword.trim() || undefined, + }); if (res && Array.isArray(res.list)) { setDevices( diff --git a/nkebao/src/components/DeviceSelectionDialog/api.ts b/nkebao/src/components/DeviceSelectionDialog/api.ts new file mode 100644 index 00000000..90572df2 --- /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/device/list", params, "GET"); +} diff --git a/nkebao/src/components/DeviceSelectionDialog/index.tsx b/nkebao/src/components/DeviceSelectionDialog/index.tsx index 4e741e15..5e24cad9 100644 --- a/nkebao/src/components/DeviceSelectionDialog/index.tsx +++ b/nkebao/src/components/DeviceSelectionDialog/index.tsx @@ -1,7 +1,7 @@ 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 { getDeviceList } from "./api"; import style from "./module.scss"; interface Device { @@ -36,15 +36,11 @@ export function DeviceSelectionDialog({ const fetchDevices = useCallback(async (keyword: string = "") => { setLoading(true); try { - const response = await request( - "/v1/device/list", - { - page: 1, - limit: 100, - keyword: keyword.trim() || undefined, - }, - "GET" - ); + const response = await getDeviceList({ + page: 1, + limit: 100, + keyword: keyword.trim() || undefined, + }); if (response && Array.isArray(response.list)) { // 转换服务端数据格式为组件需要的格式 diff --git a/nkebao/src/components/FriendSelection/api.ts b/nkebao/src/components/FriendSelection/api.ts new file mode 100644 index 00000000..f94ab93b --- /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/index.tsx b/nkebao/src/components/FriendSelection/index.tsx index 43f37ce5..4bdbd12d 100644 --- a/nkebao/src/components/FriendSelection/index.tsx +++ b/nkebao/src/components/FriendSelection/index.tsx @@ -6,7 +6,7 @@ import { RightOutlined, } from "@ant-design/icons"; import { Input, Button, Popup, Toast } from "antd-mobile"; -import request from "@/api/request"; +import { getFriendList } from "./api"; import style from "./module.scss"; // 微信好友接口类型 @@ -97,7 +97,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( diff --git a/nkebao/src/components/GroupSelection/api.ts b/nkebao/src/components/GroupSelection/api.ts new file mode 100644 index 00000000..af1ea70c --- /dev/null +++ b/nkebao/src/components/GroupSelection/api.ts @@ -0,0 +1,10 @@ +import request from "@/api/request"; + +// 获取群组列表 +export function getGroupList(params: { + page: number; + limit: number; + keyword?: string; +}) { + return request("/v1/chatroom", params, "GET"); +} diff --git a/nkebao/src/components/GroupSelection/index.tsx b/nkebao/src/components/GroupSelection/index.tsx index 5e871e86..e2d3b4fe 100644 --- a/nkebao/src/components/GroupSelection/index.tsx +++ b/nkebao/src/components/GroupSelection/index.tsx @@ -6,7 +6,7 @@ import { RightOutlined, } from "@ant-design/icons"; import { Input, Button, Popup, Toast } from "antd-mobile"; -import request from "@/api/request"; +import { getGroupList } from "./api"; import style from "./module.scss"; // 群组接口类型 @@ -81,7 +81,7 @@ export default function GroupSelection({ params.keyword = keyword.trim(); } - const res = await request("/v1/chatroom", params, "GET"); + const res = await getGroupList(params); if (res && Array.isArray(res.list)) { setGroups( 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; +} From f33bdf42e252131f3e08f8aec73403d94c8b7241 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=AC=94=E8=AE=B0=E6=9C=AC=E9=87=8C=E7=9A=84=E6=B0=B8?= =?UTF-8?q?=E5=B9=B3?= Date: Tue, 22 Jul 2025 14:37:57 +0800 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=E6=9C=AC=E6=AC=A1=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E6=9B=B4=E6=96=B0=E5=86=85=E5=AE=B9=E5=A6=82=E4=B8=8B?= =?UTF-8?q?=20=E5=8A=9F=E8=83=BD=E3=80=81=E5=92=8C=E6=A0=B7=E5=BC=8F?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nkebao/src/api/request.ts | 44 +++++----- nkebao/src/components/DeviceSelection/api.ts | 2 +- .../{module.scss => index.module.scss} | 2 +- .../src/components/DeviceSelection/index.tsx | 36 ++++++--- .../components/DeviceSelectionDialog/api.ts | 2 +- .../{module.scss => index.module.scss} | 7 +- .../DeviceSelectionDialog/index.tsx | 20 ++--- nkebao/src/components/FriendSelection/api.ts | 2 +- .../{module.scss => index.module.scss} | 2 +- .../src/components/FriendSelection/index.tsx | 36 ++++++--- .../{module.scss => index.module.scss} | 2 +- .../src/components/GroupSelection/index.tsx | 38 ++++++--- nkebao/src/components/SelectionTest.tsx | 80 +++++++++++++++++++ nkebao/src/router/module/other.tsx | 6 ++ 14 files changed, 205 insertions(+), 74 deletions(-) rename nkebao/src/components/DeviceSelection/{module.scss => index.module.scss} (93%) rename nkebao/src/components/DeviceSelectionDialog/{module.scss => index.module.scss} (91%) rename nkebao/src/components/FriendSelection/{module.scss => index.module.scss} (93%) rename nkebao/src/components/GroupSelection/{module.scss => index.module.scss} (93%) create mode 100644 nkebao/src/components/SelectionTest.tsx 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 index 90572df2..1f28ce04 100644 --- a/nkebao/src/components/DeviceSelection/api.ts +++ b/nkebao/src/components/DeviceSelection/api.ts @@ -6,5 +6,5 @@ export function getDeviceList(params: { limit: number; keyword?: string; }) { - return request("/v1/device/list", params, "GET"); + 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 93% rename from nkebao/src/components/DeviceSelection/module.scss rename to nkebao/src/components/DeviceSelection/index.module.scss index 34510d81..89a396c9 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 { diff --git a/nkebao/src/components/DeviceSelection/index.tsx b/nkebao/src/components/DeviceSelection/index.tsx index 673a8c28..b4a63ac7 100644 --- a/nkebao/src/components/DeviceSelection/index.tsx +++ b/nkebao/src/components/DeviceSelection/index.tsx @@ -1,8 +1,9 @@ import React, { useState, useEffect } from "react"; import { SearchOutlined, ReloadOutlined } from "@ant-design/icons"; -import { Input, Button, Checkbox, Popup, Toast } from "antd-mobile"; +import { Checkbox, Popup, Toast } from "antd-mobile"; +import { Input, Button } from "antd"; import { getDeviceList } from "./api"; -import style from "./module.scss"; +import style from "./index.module.scss"; // 设备选择项接口 interface DeviceSelectionItem { @@ -56,7 +57,6 @@ export default function DeviceSelection({ } } catch (error) { console.error("获取设备列表失败:", error); - Toast.show({ content: "获取设备列表失败", position: "top" }); } finally { setLoading(false); } @@ -106,13 +106,13 @@ export default function DeviceSelection({ <> {/* 输入框 */}
- } + allowClear + size="large" />
@@ -121,7 +121,7 @@ export default function DeviceSelection({ visible={popupVisible} onMaskClick={() => setPopupVisible(false)} position="bottom" - bodyStyle={{ height: "80vh" }} + bodyStyle={{ height: "100vh" }} >
@@ -129,12 +129,13 @@ export default function DeviceSelection({
- setSearchQuery(val)} - className={style.popupSearchInput} + onChange={(e) => setSearchQuery(e.target.value)} + prefix={} + allowClear + size="large" />
+
{loading ? ( diff --git a/nkebao/src/components/DeviceSelectionDialog/api.ts b/nkebao/src/components/DeviceSelectionDialog/api.ts index 90572df2..1f28ce04 100644 --- a/nkebao/src/components/DeviceSelectionDialog/api.ts +++ b/nkebao/src/components/DeviceSelectionDialog/api.ts @@ -6,5 +6,5 @@ export function getDeviceList(params: { limit: number; keyword?: string; }) { - return request("/v1/device/list", params, "GET"); + 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 91% rename from nkebao/src/components/DeviceSelectionDialog/module.scss rename to nkebao/src/components/DeviceSelectionDialog/index.module.scss index 2a9cd2de..bfde5d72 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; diff --git a/nkebao/src/components/DeviceSelectionDialog/index.tsx b/nkebao/src/components/DeviceSelectionDialog/index.tsx index 5e24cad9..1c9ba4ae 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 { Checkbox, Popup, Toast } from "antd-mobile"; +import { Input, Button } from "antd"; import { getDeviceList } from "./api"; -import style from "./module.scss"; +import style from "./index.module.scss"; interface Device { id: string; @@ -106,7 +107,7 @@ export function DeviceSelectionDialog({ visible={open} onMaskClick={() => onOpenChange(false)} position="bottom" - bodyStyle={{ height: "80vh" }} + bodyStyle={{ height: "100vh" }} >
@@ -114,12 +115,13 @@ export function DeviceSelectionDialog({
- setSearchQuery(val)} - className={style.popupSearchInput} + onChange={(e) => setSearchQuery(e.target.value)} + prefix={} + allowClear + size="large" />
+ +
+
+ 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; From 40c53c2cfe6fa34093da5a9eb9fded0ac0711272 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=AC=94=E8=AE=B0=E6=9C=AC=E9=87=8C=E7=9A=84=E6=B0=B8?= =?UTF-8?q?=E5=B9=B3?= Date: Tue, 22 Jul 2025 14:49:39 +0800 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=E6=9C=AC=E6=AC=A1=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E6=9B=B4=E6=96=B0=E5=86=85=E5=AE=B9=E5=A6=82=E4=B8=8B?= =?UTF-8?q?=20=E5=81=9A=E5=A5=BD=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DeviceSelection/index.module.scss | 32 +++++ .../src/components/DeviceSelection/index.tsx | 80 +++++++---- .../DeviceSelectionDialog/index.module.scss | 32 +++++ .../DeviceSelectionDialog/index.tsx | 136 ++++++++++++------ 4 files changed, 209 insertions(+), 71 deletions(-) diff --git a/nkebao/src/components/DeviceSelection/index.module.scss b/nkebao/src/components/DeviceSelection/index.module.scss index 89a396c9..d457fa24 100644 --- a/nkebao/src/components/DeviceSelection/index.module.scss +++ b/nkebao/src/components/DeviceSelection/index.module.scss @@ -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 b4a63ac7..b59e8104 100644 --- a/nkebao/src/components/DeviceSelection/index.tsx +++ b/nkebao/src/components/DeviceSelection/index.tsx @@ -33,17 +33,19 @@ export default function DeviceSelection({ const [searchQuery, setSearchQuery] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); const [loading, setLoading] = useState(false); + const [currentPage, setCurrentPage] = useState(1); // 新增 + const [total, setTotal] = useState(0); // 新增 + const pageSize = 20; // 每页条数 - // 获取设备列表,支持keyword - const fetchDevices = async (keyword: string = "") => { + // 获取设备列表,支持keyword和分页 + const fetchDevices = async (keyword: string = "", page: number = 1) => { setLoading(true); try { const res = await getDeviceList({ - page: 1, - limit: 100, + page, + limit: pageSize, keyword: keyword.trim() || undefined, }); - if (res && Array.isArray(res.list)) { setDevices( res.list.map((d: any) => ({ @@ -54,30 +56,41 @@ export default function DeviceSelection({ status: d.alive === 1 ? "online" : "offline", })) ); + setTotal(res.total || 0); } } catch (error) { console.error("获取设备列表失败:", error); + Toast.show({ content: "获取设备列表失败", position: "top" }); } finally { setLoading(false); } }; - // 打开弹窗时获取设备列表 + // 打开弹窗时获取第一页 const openPopup = () => { setSearchQuery(""); + setCurrentPage(1); setPopupVisible(true); - fetchDevices(""); + fetchDevices("", 1); }; // 搜索防抖 useEffect(() => { if (!popupVisible) return; const timer = setTimeout(() => { - fetchDevices(searchQuery); + setCurrentPage(1); + fetchDevices(searchQuery, 1); }, 500); return () => clearTimeout(timer); }, [searchQuery, popupVisible]); + // 翻页时重新请求 + useEffect(() => { + if (!popupVisible) return; + fetchDevices(searchQuery, currentPage); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentPage]); + // 过滤设备(只保留状态过滤) const filteredDevices = devices.filter((device) => { const matchesStatus = @@ -87,6 +100,8 @@ export default function DeviceSelection({ return matchesStatus; }); + const totalPages = Math.max(1, Math.ceil(total / pageSize)); + // 处理设备选择 const handleDeviceToggle = (deviceId: string) => { if (selectedDevices.includes(deviceId)) { @@ -129,13 +144,12 @@ export default function DeviceSelection({
+ setSearchQuery(e.target.value)} - prefix={} - allowClear - size="large" + onChange={(val) => setSearchQuery(val)} + className={style.popupSearchInput} />
-
{loading ? ( @@ -198,6 +199,35 @@ export default function DeviceSelection({
)}
+ {/* 分页栏 */} +
+
总计 {total} 个设备
+
+ + + {currentPage} / {totalPages} + + +
+
已选择 {selectedDevices.length} 个设备 diff --git a/nkebao/src/components/DeviceSelectionDialog/index.module.scss b/nkebao/src/components/DeviceSelectionDialog/index.module.scss index bfde5d72..e0961f10 100644 --- a/nkebao/src/components/DeviceSelectionDialog/index.module.scss +++ b/nkebao/src/components/DeviceSelectionDialog/index.module.scss @@ -163,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 1c9ba4ae..588d774a 100644 --- a/nkebao/src/components/DeviceSelectionDialog/index.tsx +++ b/nkebao/src/components/DeviceSelectionDialog/index.tsx @@ -32,47 +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 getDeviceList({ - page: 1, - limit: 100, - keyword: keyword.trim() || undefined, - }); - - 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); + // 获取设备列表,支持keyword和分页 + const fetchDevices = useCallback( + async (keyword: string = "", page: number = 1) => { + setLoading(true); + try { + const response = await getDeviceList({ + page, + limit: pageSize, + keyword: keyword.trim() || undefined, + }); + 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]); @@ -80,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 = @@ -102,6 +116,8 @@ export function DeviceSelectionDialog({ } }; + const totalPages = Math.max(1, Math.ceil(total / pageSize)); + return (
+ setSearchQuery(e.target.value)} - prefix={} - allowClear - size="large" + onChange={(val) => setSearchQuery(val)} + className={style.popupSearchInput} />
IMEI: {device.imei}
-
微信号: {device.wxid || "-"}
+
微信号: {device.wxid || "-"}
昵称: {device.nickname || "-"}
{device.usedInPlans > 0 && ( @@ -194,6 +209,35 @@ export function DeviceSelectionDialog({
)}
+ {/* 分页栏 */} +
+
总计 {total} 个设备
+
+ + + {currentPage} / {totalPages} + + +
+
已选择 {selectedDevices.length} 个设备 From 3b45abe69b96e986286f2e75166e461614fb0f62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=AC=94=E8=AE=B0=E6=9C=AC=E9=87=8C=E7=9A=84=E6=B0=B8?= =?UTF-8?q?=E5=B9=B3?= Date: Tue, 22 Jul 2025 16:27:45 +0800 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20=E6=9C=AC=E6=AC=A1=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E6=9B=B4=E6=96=B0=E5=86=85=E5=AE=B9=E5=A6=82=E4=B8=8B?= =?UTF-8?q?=20=E5=BC=B9=E7=AA=97=E6=9A=82=E6=97=B6=E5=B0=81=E8=A3=85?= =?UTF-8?q?=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nkebao/.env.development | 2 +- .../src/components/DeviceSelection/index.tsx | 617 +++++++++++------- .../DeviceSelection/selectionPopup.tsx | 197 ++++++ 3 files changed, 567 insertions(+), 249 deletions(-) create mode 100644 nkebao/src/components/DeviceSelection/selectionPopup.tsx 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/components/DeviceSelection/index.tsx b/nkebao/src/components/DeviceSelection/index.tsx index b59e8104..126d43fc 100644 --- a/nkebao/src/components/DeviceSelection/index.tsx +++ b/nkebao/src/components/DeviceSelection/index.tsx @@ -1,248 +1,369 @@ -import React, { useState, useEffect } 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"; - -// 设备选择项接口 -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); - const [currentPage, setCurrentPage] = useState(1); // 新增 - const [total, setTotal] = useState(0); // 新增 - const pageSize = 20; // 每页条数 - - // 获取设备列表,支持keyword和分页 - const fetchDevices = async (keyword: string = "", page: number = 1) => { - setLoading(true); - try { - const res = await getDeviceList({ - page, - limit: pageSize, - 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", - })) - ); - setTotal(res.total || 0); - } - } catch (error) { - console.error("获取设备列表失败:", error); - Toast.show({ content: "获取设备列表失败", position: "top" }); - } finally { - setLoading(false); - } - }; - - // 打开弹窗时获取第一页 - const openPopup = () => { - setSearchQuery(""); - setCurrentPage(1); - setPopupVisible(true); - fetchDevices("", 1); - }; - - // 搜索防抖 - useEffect(() => { - if (!popupVisible) return; - const timer = setTimeout(() => { - setCurrentPage(1); - fetchDevices(searchQuery, 1); - }, 500); - return () => clearTimeout(timer); - }, [searchQuery, popupVisible]); - - // 翻页时重新请求 - useEffect(() => { - if (!popupVisible) 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 / pageSize)); - - // 处理设备选择 - 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 ( - <> - {/* 输入框 */} -
- } - allowClear - size="large" - /> -
- - {/* 设备选择弹窗 */} - setPopupVisible(false)} - position="bottom" - bodyStyle={{ height: "100vh" }} - > -
-
-
选择设备
-
-
-
- - setSearchQuery(val)} - className={style.popupSearchInput} - /> -
- -
-
- {loading ? ( -
-
加载中...
-
- ) : ( -
- {filteredDevices.map((device) => ( - - ))} -
- )} -
- {/* 分页栏 */} -
-
总计 {total} 个设备
-
- - - {currentPage} / {totalPages} - - -
-
-
-
- 已选择 {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; // 新增 +} + +const PAGE_SIZE = 20; + +const DeviceSelection: React.FC = ({ + selectedDevices, + onSelect, + placeholder = "选择设备", + className = "", + mode = "input", + open, + onOpenChange, + selectedListMaxHeight = 300, // 默认300 + showInput = true, + showSelectedList = true, +}) => { + // 弹窗控制 + 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 = () => { + 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) => { + 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 + size="large" + /> +
+ )} + {/* 已选设备列表窗口 */} + {mode === "input" && + showSelectedList && + selectedDeviceObjs.length > 0 && ( +
+ {selectedDeviceObjs.map((device) => ( +
+
+ {device.name} +
+
+ ))} +
+ )} + {/* 弹窗 */} + 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; From fceb787924c717f3e6404908e50d995360877b46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=AC=94=E8=AE=B0=E6=9C=AC=E9=87=8C=E7=9A=84=E6=B0=B8?= =?UTF-8?q?=E5=B9=B3?= Date: Tue, 22 Jul 2025 16:47:03 +0800 Subject: [PATCH 5/5] =?UTF-8?q?feat:=20=E6=9C=AC=E6=AC=A1=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E6=9B=B4=E6=96=B0=E5=86=85=E5=AE=B9=E5=A6=82=E4=B8=8B?= =?UTF-8?q?=20=E9=80=89=E6=8B=A9=E7=BB=84=E4=BB=B6=E6=9E=84=E5=BB=BA?= =?UTF-8?q?=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/DeviceSelection/index.tsx | 49 ++++++++++++------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/nkebao/src/components/DeviceSelection/index.tsx b/nkebao/src/components/DeviceSelection/index.tsx index 126d43fc..a62625d2 100644 --- a/nkebao/src/components/DeviceSelection/index.tsx +++ b/nkebao/src/components/DeviceSelection/index.tsx @@ -30,6 +30,7 @@ interface DeviceSelectionProps { selectedListMaxHeight?: number; // 新增,已选列表最大高度,默认500 showInput?: boolean; // 新增 showSelectedList?: boolean; // 新增 + readonly?: boolean; // 新增 } const PAGE_SIZE = 20; @@ -45,6 +46,7 @@ const DeviceSelection: React.FC = ({ selectedListMaxHeight = 300, // 默认300 showInput = true, showSelectedList = true, + readonly = false, }) => { // 弹窗控制 const [popupVisible, setPopupVisible] = useState(false); @@ -99,6 +101,7 @@ const DeviceSelection: React.FC = ({ // 打开弹窗时获取第一页 const openPopup = () => { + if (readonly) return; setSearchQuery(""); setCurrentPage(1); setRealVisible(true); @@ -155,6 +158,7 @@ const DeviceSelection: React.FC = ({ // 删除已选设备 const handleRemoveDevice = (id: string) => { + if (readonly) return; onSelect(selectedDevices.filter((d) => d !== id)); }; @@ -289,8 +293,13 @@ const DeviceSelection: React.FC = ({ value={getDisplayText()} onClick={openPopup} prefix={} - allowClear + allowClear={!readonly} size="large" + readOnly={readonly} + disabled={readonly} + style={ + readonly ? { background: "#f5f5f5", cursor: "not-allowed" } : {} + } />
)} @@ -332,30 +341,32 @@ const DeviceSelection: React.FC = ({ > {device.name}
-