diff --git a/nkebao/.env.development b/nkebao/.env.development index da6a111b..f37023e8 100644 --- a/nkebao/.env.development +++ b/nkebao/.env.development @@ -1,4 +1,5 @@ # 基础环境变量示例 VITE_API_BASE_URL=http://www.yishi.com +VITE_API_BASE_URL=https://ckbapi.quwanzhi.com VITE_APP_TITLE=Nkebao Base diff --git a/nkebao/src/pages/devices/Devices.tsx b/nkebao/src/pages/devices/Devices.tsx index 11e11dd6..950c02ce 100644 --- a/nkebao/src/pages/devices/Devices.tsx +++ b/nkebao/src/pages/devices/Devices.tsx @@ -1,439 +1,431 @@ -import React, { useEffect, useRef, useState, useCallback } from "react"; -import { NavBar, Popup, Tabs, Toast, SpinLoading, Dialog } from "antd-mobile"; -import { Button, Input, Pagination, Checkbox } from "antd"; -import { useNavigate } from "react-router-dom"; -import { AddOutline, DeleteOutline } from "antd-mobile-icons"; -import { - ReloadOutlined, - SearchOutlined, - QrcodeOutlined, - ArrowLeftOutlined, -} from "@ant-design/icons"; -import Layout from "@/components/Layout/Layout"; -import MeauMobile from "@/components/MeauMobile/MeauMoible"; -import { - fetchDeviceList, - fetchDeviceQRCode, - addDeviceByImei, - deleteDevice, -} from "@/api/devices"; -import type { Device } from "@/types/device"; - -const Devices: React.FC = () => { - // 设备列表相关 - const [devices, setDevices] = useState([]); - const [loading, setLoading] = useState(false); - const [search, setSearch] = useState(""); - const [status, setStatus] = useState<"all" | "online" | "offline">("all"); - const [page, setPage] = useState(1); - const [hasMore, setHasMore] = useState(true); - const [total, setTotal] = useState(0); - const [selected, setSelected] = useState<(string | number)[]>([]); - const observerRef = useRef(null); - const [usePagination, setUsePagination] = useState(true); // 新增:是否使用分页 - - // 添加设备弹窗 - const [addVisible, setAddVisible] = useState(false); - const [addTab, setAddTab] = useState("scan"); - const [qrLoading, setQrLoading] = useState(false); - const [qrCode, setQrCode] = useState(null); - const [imei, setImei] = useState(""); - const [name, setName] = useState(""); - const [addLoading, setAddLoading] = useState(false); - - // 删除弹窗 - const [delVisible, setDelVisible] = useState(false); - const [delLoading, setDelLoading] = useState(false); - - const navigate = useNavigate(); - // 加载设备列表 - const loadDevices = useCallback( - async (reset = false) => { - if (loading) return; - setLoading(true); - try { - const params: any = { page: reset ? 1 : page, limit: 20 }; - if (search) params.keyword = search; - const res = await fetchDeviceList(params); - const list = Array.isArray(res.list) ? res.list : []; - setDevices((prev) => (reset ? list : [...prev, ...list])); - setTotal(res.total || 0); - setHasMore(list.length === 20); - if (reset) setPage(1); - } catch (e) { - Toast.show({ content: "获取设备列表失败", position: "top" }); - setHasMore(false); // 请求失败后不再继续请求 - } finally { - setLoading(false); - } - }, - [loading, search, page] - ); - - // 首次加载和搜索 - useEffect(() => { - loadDevices(true); - // eslint-disable-next-line - }, [search]); - - // 无限滚动 - useEffect(() => { - if (!hasMore || loading) return; - const observer = new window.IntersectionObserver( - (entries) => { - if (entries[0].isIntersecting && hasMore && !loading) { - setPage((p) => p + 1); - } - }, - { threshold: 0.5 } - ); - if (observerRef.current) observer.observe(observerRef.current); - return () => observer.disconnect(); - }, [hasMore, loading]); - - // 分页加载 - useEffect(() => { - if (page === 1) return; - loadDevices(); - // eslint-disable-next-line - }, [page]); - - // 状态筛选 - const filtered = devices.filter((d) => { - if (status === "all") return true; - if (status === "online") return d.status === "online" || d.alive === 1; - if (status === "offline") return d.status === "offline" || d.alive === 0; - return true; - }); - - // 获取二维码 - const handleGetQr = async () => { - setQrLoading(true); - setQrCode(null); - try { - const accountId = localStorage.getItem("s2_accountId") || ""; - if (!accountId) throw new Error("未获取到用户信息"); - const res = await fetchDeviceQRCode(accountId); - setQrCode(res.qrCode); - } catch (e: any) { - Toast.show({ content: e.message || "获取二维码失败", position: "top" }); - } finally { - setQrLoading(false); - } - }; - - // 手动添加设备 - const handleAddDevice = async () => { - if (!imei.trim() || !name.trim()) { - Toast.show({ content: "请填写完整信息", position: "top" }); - return; - } - setAddLoading(true); - try { - await addDeviceByImei(imei, name); - Toast.show({ content: "添加成功", position: "top" }); - setAddVisible(false); - setImei(""); - setName(""); - loadDevices(true); - } catch (e: any) { - Toast.show({ content: e.message || "添加失败", position: "top" }); - } finally { - setAddLoading(false); - } - }; - - // 删除设备 - const handleDelete = async () => { - if (!selected.length) return; - setDelLoading(true); - try { - for (const id of selected) { - await deleteDevice(Number(id)); - } - Toast.show({ content: `删除成功`, position: "top" }); - setDelVisible(false); - setSelected([]); - loadDevices(true); - } catch (e: any) { - Toast.show({ content: e.message || "删除失败", position: "top" }); - } finally { - setDelLoading(false); - } - }; - - // 跳转详情 - const goDetail = (id: string | number) => { - window.location.href = `/devices/${id}`; - }; - - // 分页切换 - const handlePageChange = (p: number) => { - setPage(p); - loadDevices(true); - }; - - return ( - - - navigate(-1)} - /> - - } - style={{ background: "#fff" }} - right={ - - } - > - - 设备管理 - - - -
- {/* 搜索栏 */} -
- setSearch(e.target.value)} - prefix={} - allowClear - style={{ flex: 1 }} - /> - -
- {/* 筛选和删除 */} -
- setStatus(k as any)} - style={{ flex: 1 }} - > - - - - -
- -
-
-
- - } - footer={ -
- -
- } - loading={loading && devices.length === 0} - > -
- {/* 设备列表 */} -
- {filtered.map((device) => ( -
goDetail(device.id!)} - > - { - e.stopPropagation(); - setSelected((prev) => - e.target.checked - ? [...prev, device.id!] - : prev.filter((id) => id !== device.id) - ); - }} - onClick={(e) => e.stopPropagation()} - style={{ marginRight: 12 }} - /> -
-
- {device.memo || "未命名设备"} -
-
- IMEI: {device.imei} -
-
- 微信号: {device.wechatId || "未绑定"} -
-
- 好友数: {device.totalFriend ?? "-"} -
-
- - {device.status === "online" || device.alive === 1 - ? "在线" - : "离线"} - -
- ))} - - {/* 无限滚动提示(仅在不分页时显示) */} - {!usePagination && ( -
- {loading && } - {!hasMore && devices.length > 0 && "没有更多设备了"} - {!hasMore && devices.length === 0 && "暂无设备"} -
- )} -
-
- {/* 添加设备弹窗 */} - setAddVisible(false)} - bodyStyle={{ - borderTopLeftRadius: 16, - borderTopRightRadius: 16, - minHeight: 320, - }} - > -
- - - - - {addTab === "scan" && ( -
- - {qrCode && ( -
- 二维码 -
- 请用手机扫码添加设备 -
-
- )} -
- )} - {addTab === "manual" && ( -
- setName(e.target.value)} - allowClear - /> - setImei(e.target.value)} - allowClear - /> - -
- )} -
-
- {/* 删除确认弹窗 */} - setDelVisible(false)} - closeOnAction - closeOnMaskClick - actions={[ - { - key: "confirm", - text: "确认删除", - danger: true, - // loading: delLoading, // antd-mobile Dialog.Action 不支持 loading 属性,去掉 - }, - { key: "cancel", text: "取消" }, - ]} - /> -
- ); -}; - -export default Devices; +import React, { useEffect, useRef, useState, useCallback } from "react"; +import { NavBar, Popup, Tabs, Toast, SpinLoading, Dialog } from "antd-mobile"; +import { Button, Input, Pagination, Checkbox } from "antd"; +import { useNavigate } from "react-router-dom"; +import { AddOutline, DeleteOutline } from "antd-mobile-icons"; +import { + ReloadOutlined, + SearchOutlined, + QrcodeOutlined, + ArrowLeftOutlined, +} from "@ant-design/icons"; +import Layout from "@/components/Layout/Layout"; +import MeauMobile from "@/components/MeauMobile/MeauMoible"; +import { + fetchDeviceList, + fetchDeviceQRCode, + addDeviceByImei, + deleteDevice, +} from "@/api/devices"; +import type { Device } from "@/types/device"; +import { comfirm } from "@/utils/common"; + +const Devices: React.FC = () => { + // 设备列表相关 + const [devices, setDevices] = useState([]); + const [loading, setLoading] = useState(false); + const [search, setSearch] = useState(""); + const [status, setStatus] = useState<"all" | "online" | "offline">("all"); + const [page, setPage] = useState(1); + const [hasMore, setHasMore] = useState(true); + const [total, setTotal] = useState(0); + const [selected, setSelected] = useState<(string | number)[]>([]); + const observerRef = useRef(null); + const [usePagination, setUsePagination] = useState(true); // 新增:是否使用分页 + + // 添加设备弹窗 + const [addVisible, setAddVisible] = useState(false); + const [addTab, setAddTab] = useState("scan"); + const [qrLoading, setQrLoading] = useState(false); + const [qrCode, setQrCode] = useState(null); + const [imei, setImei] = useState(""); + const [name, setName] = useState(""); + const [addLoading, setAddLoading] = useState(false); + + // 删除弹窗 + const [delVisible, setDelVisible] = useState(false); + const [delLoading, setDelLoading] = useState(false); + + const navigate = useNavigate(); + // 加载设备列表 + const loadDevices = useCallback( + async (reset = false) => { + if (loading) return; + setLoading(true); + try { + const params: any = { page: reset ? 1 : page, limit: 20 }; + if (search) params.keyword = search; + const res = await fetchDeviceList(params); + const list = Array.isArray(res.list) ? res.list : []; + setDevices((prev) => (reset ? list : [...prev, ...list])); + setTotal(res.total || 0); + setHasMore(list.length === 20); + if (reset) setPage(1); + } catch (e) { + Toast.show({ content: "获取设备列表失败", position: "top" }); + setHasMore(false); // 请求失败后不再继续请求 + } finally { + setLoading(false); + } + }, + [loading, search, page] + ); + + // 首次加载和搜索 + useEffect(() => { + loadDevices(true); + // eslint-disable-next-line + }, [search]); + + // 无限滚动 + useEffect(() => { + if (!hasMore || loading) return; + const observer = new window.IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && hasMore && !loading) { + setPage((p) => p + 1); + } + }, + { threshold: 0.5 } + ); + if (observerRef.current) observer.observe(observerRef.current); + return () => observer.disconnect(); + }, [hasMore, loading]); + + // 分页加载 + useEffect(() => { + if (page === 1) return; + loadDevices(); + // eslint-disable-next-line + }, [page]); + + // 状态筛选 + const filtered = devices.filter((d) => { + if (status === "all") return true; + if (status === "online") return d.status === "online" || d.alive === 1; + if (status === "offline") return d.status === "offline" || d.alive === 0; + return true; + }); + + // 获取二维码 + const handleGetQr = async () => { + setQrLoading(true); + setQrCode(null); + try { + const accountId = localStorage.getItem("s2_accountId") || ""; + if (!accountId) throw new Error("未获取到用户信息"); + const res = await fetchDeviceQRCode(accountId); + setQrCode(res.qrCode); + } catch (e: any) { + Toast.show({ content: e.message || "获取二维码失败", position: "top" }); + } finally { + setQrLoading(false); + } + }; + + // 手动添加设备 + const handleAddDevice = async () => { + if (!imei.trim() || !name.trim()) { + Toast.show({ content: "请填写完整信息", position: "top" }); + return; + } + setAddLoading(true); + try { + await addDeviceByImei(imei, name); + Toast.show({ content: "添加成功", position: "top" }); + setAddVisible(false); + setImei(""); + setName(""); + loadDevices(true); + } catch (e: any) { + Toast.show({ content: e.message || "添加失败", position: "top" }); + } finally { + setAddLoading(false); + } + }; + + // 删除设备 + const handleDelete = async () => { + setDelLoading(true); + try { + for (const id of selected) { + await deleteDevice(Number(id)); + } + Toast.show({ content: `删除成功`, position: "top" }); + setSelected([]); + loadDevices(true); + } catch (e: any) { + if (e) Toast.show({ content: e.message || "删除失败", position: "top" }); + } finally { + setDelLoading(false); + } + }; + + // 删除按钮点击 + const handleDeleteClick = async () => { + try { + await comfirm( + `将删除${selected.length}个设备,删除后本设备配置的计划任务操作也将失效。确认删除?`, + { title: "确认删除", confirmText: "确认删除", cancelText: "取消" } + ); + handleDelete(); + } catch { + // 用户取消,无需处理 + } + }; + + // 跳转详情 + const goDetail = (id: string | number) => { + window.location.href = `/devices/${id}`; + }; + + // 分页切换 + const handlePageChange = (p: number) => { + setPage(p); + loadDevices(true); + }; + + return ( + + + navigate(-1)} + /> + + } + style={{ background: "#fff" }} + right={ + + } + > + + 设备管理 + + + +
+ {/* 搜索栏 */} +
+ setSearch(e.target.value)} + prefix={} + allowClear + style={{ flex: 1 }} + /> + +
+ {/* 筛选和删除 */} +
+ setStatus(k as any)} + style={{ flex: 1 }} + > + + + + +
+ +
+
+
+ + } + footer={ +
+ +
+ } + loading={loading && devices.length === 0} + > +
+ {/* 设备列表 */} +
+ {filtered.map((device) => ( +
goDetail(device.id!)} + > + { + e.stopPropagation(); + setSelected((prev) => + e.target.checked + ? [...prev, device.id!] + : prev.filter((id) => id !== device.id) + ); + }} + onClick={(e) => e.stopPropagation()} + style={{ marginRight: 12 }} + /> +
+
+ {device.memo || "未命名设备"} +
+
+ IMEI: {device.imei} +
+
+ 微信号: {device.wechatId || "未绑定"} +
+
+ 好友数: {device.totalFriend ?? "-"} +
+
+ + {device.status === "online" || device.alive === 1 + ? "在线" + : "离线"} + +
+ ))} + + {/* 无限滚动提示(仅在不分页时显示) */} + {!usePagination && ( +
+ {loading && } + {!hasMore && devices.length > 0 && "没有更多设备了"} + {!hasMore && devices.length === 0 && "暂无设备"} +
+ )} +
+
+ {/* 添加设备弹窗 */} + setAddVisible(false)} + bodyStyle={{ + borderTopLeftRadius: 16, + borderTopRightRadius: 16, + minHeight: 320, + }} + > +
+ + + + + {addTab === "scan" && ( +
+ + {qrCode && ( +
+ 二维码 +
+ 请用手机扫码添加设备 +
+
+ )} +
+ )} + {addTab === "manual" && ( +
+ setName(e.target.value)} + allowClear + /> + setImei(e.target.value)} + allowClear + /> + +
+ )} +
+
+
+ ); +}; + +export default Devices; diff --git a/nkebao/src/utils/common.ts b/nkebao/src/utils/common.ts new file mode 100644 index 00000000..60cbf840 --- /dev/null +++ b/nkebao/src/utils/common.ts @@ -0,0 +1,37 @@ +import { Modal } from "antd-mobile"; + +/** + * 通用js调用弹窗,Promise风格 + * @param content 弹窗内容 + * @param config 配置项(title, cancelText, confirmText) + * @returns Promise + */ +export const comfirm = ( + content: string, + config?: { + title?: string; + cancelText?: string; + confirmText?: string; + } +): Promise => { + return new Promise((resolve, reject) => { + Modal.show({ + title: config?.title || "提示", + content, + closeOnAction: true, + actions: [ + { + key: "cancel", + text: config?.cancelText || "取消", + onClick: () => reject(), + }, + { + key: "confirm", + text: config?.confirmText || "确认", + danger: true, + onClick: () => resolve(), + }, + ], + }); + }); +};