feat: 本次提交更新内容如下
设备详情搞定
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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<Device[]>([]);
|
||||
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<HTMLDivElement>(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<string | null>(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 (
|
||||
<Layout
|
||||
header={
|
||||
<>
|
||||
<NavBar
|
||||
back={null}
|
||||
left={
|
||||
<div className="nav-title">
|
||||
<ArrowLeftOutlined
|
||||
twoToneColor="#1677ff"
|
||||
onClick={() => navigate(-1)}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
style={{ background: "#fff" }}
|
||||
right={
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={() => setAddVisible(true)}
|
||||
>
|
||||
<AddOutline />
|
||||
添加设备
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<span style={{ color: "var(--primary-color)", fontWeight: 600 }}>
|
||||
设备管理
|
||||
</span>
|
||||
</NavBar>
|
||||
|
||||
<div style={{ padding: "12px 12px 0 12px", background: "#fff" }}>
|
||||
{/* 搜索栏 */}
|
||||
<div style={{ display: "flex", gap: 8, marginBottom: 12 }}>
|
||||
<Input
|
||||
placeholder="搜索设备IMEI/备注"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
prefix={<SearchOutlined />}
|
||||
allowClear
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => loadDevices(true)}
|
||||
icon={<ReloadOutlined />}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
{/* 筛选和删除 */}
|
||||
<div style={{ display: "flex", gap: 8 }}>
|
||||
<Tabs
|
||||
activeKey={status}
|
||||
onChange={(k) => setStatus(k as any)}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<Tabs.Tab title="全部" key="all" />
|
||||
<Tabs.Tab title="在线" key="online" />
|
||||
<Tabs.Tab title="离线" key="offline" />
|
||||
</Tabs>
|
||||
<div style={{ paddingTop: 8 }}>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
danger
|
||||
icon={<DeleteOutline />}
|
||||
disabled={selected.length === 0}
|
||||
onClick={() => setDelVisible(true)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
footer={
|
||||
<div style={{ padding: 16, textAlign: "center", background: "#fff" }}>
|
||||
<Pagination
|
||||
current={page}
|
||||
pageSize={20}
|
||||
total={total}
|
||||
showSizeChanger={false}
|
||||
onChange={handlePageChange}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
loading={loading && devices.length === 0}
|
||||
>
|
||||
<div style={{ padding: 12 }}>
|
||||
{/* 设备列表 */}
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
||||
{filtered.map((device) => (
|
||||
<div
|
||||
key={device.id}
|
||||
style={{
|
||||
background: "#fff",
|
||||
borderRadius: 12,
|
||||
padding: 12,
|
||||
boxShadow: "0 1px 4px #eee",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
cursor: "pointer",
|
||||
border: selected.includes(device.id)
|
||||
? "1.5px solid #1677ff"
|
||||
: "1px solid #f0f0f0",
|
||||
}}
|
||||
onClick={() => goDetail(device.id!)}
|
||||
>
|
||||
<Checkbox
|
||||
checked={selected.includes(device.id)}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelected((prev) =>
|
||||
e.target.checked
|
||||
? [...prev, device.id!]
|
||||
: prev.filter((id) => id !== device.id)
|
||||
);
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{ marginRight: 12 }}
|
||||
/>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontWeight: 600, fontSize: 16 }}>
|
||||
{device.memo || "未命名设备"}
|
||||
</div>
|
||||
<div style={{ fontSize: 14, color: "#999", marginTop: 2 }}>
|
||||
IMEI: {device.imei}
|
||||
</div>
|
||||
<div style={{ fontSize: 14, color: "#999", marginTop: 2 }}>
|
||||
微信号: {device.wechatId || "未绑定"}
|
||||
</div>
|
||||
<div style={{ fontSize: 14, color: "#999", marginTop: 2 }}>
|
||||
好友数: {device.totalFriend ?? "-"}
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color:
|
||||
device.status === "online" || device.alive === 1
|
||||
? "#52c41a"
|
||||
: "#aaa",
|
||||
marginLeft: 8,
|
||||
}}
|
||||
>
|
||||
{device.status === "online" || device.alive === 1
|
||||
? "在线"
|
||||
: "离线"}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* 无限滚动提示(仅在不分页时显示) */}
|
||||
{!usePagination && (
|
||||
<div
|
||||
ref={observerRef}
|
||||
style={{ padding: 12, textAlign: "center", color: "#888" }}
|
||||
>
|
||||
{loading && <SpinLoading style={{ "--size": "24px" }} />}
|
||||
{!hasMore && devices.length > 0 && "没有更多设备了"}
|
||||
{!hasMore && devices.length === 0 && "暂无设备"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* 添加设备弹窗 */}
|
||||
<Popup
|
||||
visible={addVisible}
|
||||
onMaskClick={() => setAddVisible(false)}
|
||||
bodyStyle={{
|
||||
borderTopLeftRadius: 16,
|
||||
borderTopRightRadius: 16,
|
||||
minHeight: 320,
|
||||
}}
|
||||
>
|
||||
<div style={{ padding: 20 }}>
|
||||
<Tabs
|
||||
activeKey={addTab}
|
||||
onChange={setAddTab}
|
||||
style={{ marginBottom: 16 }}
|
||||
>
|
||||
<Tabs.Tab title="扫码添加" key="scan" />
|
||||
<Tabs.Tab title="手动添加" key="manual" />
|
||||
</Tabs>
|
||||
{addTab === "scan" && (
|
||||
<div style={{ textAlign: "center", minHeight: 200 }}>
|
||||
<Button
|
||||
type="error"
|
||||
onClick={handleGetQr}
|
||||
loading={qrLoading}
|
||||
icon={<QrcodeOutlined />}
|
||||
>
|
||||
获取二维码
|
||||
</Button>
|
||||
{qrCode && (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<img
|
||||
src={qrCode}
|
||||
alt="二维码"
|
||||
style={{
|
||||
width: 180,
|
||||
height: 180,
|
||||
background: "#f5f5f5",
|
||||
borderRadius: 8,
|
||||
}}
|
||||
/>
|
||||
<div style={{ color: "#888", fontSize: 12, marginTop: 8 }}>
|
||||
请用手机扫码添加设备
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{addTab === "manual" && (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
||||
<Input
|
||||
placeholder="设备名称"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
allowClear
|
||||
/>
|
||||
<Input
|
||||
placeholder="设备IMEI"
|
||||
value={imei}
|
||||
onChange={(e) => setImei(e.target.value)}
|
||||
allowClear
|
||||
/>
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={handleAddDevice}
|
||||
loading={addLoading}
|
||||
>
|
||||
添加
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Popup>
|
||||
{/* 删除确认弹窗 */}
|
||||
<Dialog
|
||||
visible={delVisible}
|
||||
content={`将删除${selected.length}个设备,删除后本设备配置的计划任务操作也将失效。确认删除?`}
|
||||
confirmText="确认删除"
|
||||
cancelText="取消"
|
||||
onConfirm={handleDelete}
|
||||
onCancel={() => setDelVisible(false)}
|
||||
closeOnAction
|
||||
closeOnMaskClick
|
||||
actions={[
|
||||
{
|
||||
key: "confirm",
|
||||
text: "确认删除",
|
||||
danger: true,
|
||||
// loading: delLoading, // antd-mobile Dialog.Action 不支持 loading 属性,去掉
|
||||
},
|
||||
{ key: "cancel", text: "取消" },
|
||||
]}
|
||||
/>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
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<Device[]>([]);
|
||||
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<HTMLDivElement>(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<string | null>(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 (
|
||||
<Layout
|
||||
header={
|
||||
<>
|
||||
<NavBar
|
||||
back={null}
|
||||
left={
|
||||
<div className="nav-title">
|
||||
<ArrowLeftOutlined
|
||||
twoToneColor="#1677ff"
|
||||
onClick={() => navigate(-1)}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
style={{ background: "#fff" }}
|
||||
right={
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={() => setAddVisible(true)}
|
||||
>
|
||||
<AddOutline />
|
||||
添加设备
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<span style={{ color: "var(--primary-color)", fontWeight: 600 }}>
|
||||
设备管理
|
||||
</span>
|
||||
</NavBar>
|
||||
|
||||
<div style={{ padding: "12px 12px 0 12px", background: "#fff" }}>
|
||||
{/* 搜索栏 */}
|
||||
<div style={{ display: "flex", gap: 8, marginBottom: 12 }}>
|
||||
<Input
|
||||
placeholder="搜索设备IMEI/备注"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
prefix={<SearchOutlined />}
|
||||
allowClear
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => loadDevices(true)}
|
||||
icon={<ReloadOutlined />}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
{/* 筛选和删除 */}
|
||||
<div style={{ display: "flex", gap: 8 }}>
|
||||
<Tabs
|
||||
activeKey={status}
|
||||
onChange={(k) => setStatus(k as any)}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<Tabs.Tab title="全部" key="all" />
|
||||
<Tabs.Tab title="在线" key="online" />
|
||||
<Tabs.Tab title="离线" key="offline" />
|
||||
</Tabs>
|
||||
<div style={{ paddingTop: 8 }}>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
danger
|
||||
icon={<DeleteOutline />}
|
||||
disabled={selected.length === 0}
|
||||
onClick={handleDeleteClick}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
footer={
|
||||
<div style={{ padding: 16, textAlign: "center", background: "#fff" }}>
|
||||
<Pagination
|
||||
current={page}
|
||||
pageSize={20}
|
||||
total={total}
|
||||
showSizeChanger={false}
|
||||
onChange={handlePageChange}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
loading={loading && devices.length === 0}
|
||||
>
|
||||
<div style={{ padding: 12 }}>
|
||||
{/* 设备列表 */}
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
||||
{filtered.map((device) => (
|
||||
<div
|
||||
key={device.id}
|
||||
style={{
|
||||
background: "#fff",
|
||||
borderRadius: 12,
|
||||
padding: 12,
|
||||
boxShadow: "0 1px 4px #eee",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
cursor: "pointer",
|
||||
border: selected.includes(device.id)
|
||||
? "1.5px solid #1677ff"
|
||||
: "1px solid #f0f0f0",
|
||||
}}
|
||||
onClick={() => goDetail(device.id!)}
|
||||
>
|
||||
<Checkbox
|
||||
checked={selected.includes(device.id)}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelected((prev) =>
|
||||
e.target.checked
|
||||
? [...prev, device.id!]
|
||||
: prev.filter((id) => id !== device.id)
|
||||
);
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{ marginRight: 12 }}
|
||||
/>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontWeight: 600, fontSize: 16 }}>
|
||||
{device.memo || "未命名设备"}
|
||||
</div>
|
||||
<div style={{ fontSize: 14, color: "#999", marginTop: 2 }}>
|
||||
IMEI: {device.imei}
|
||||
</div>
|
||||
<div style={{ fontSize: 14, color: "#999", marginTop: 2 }}>
|
||||
微信号: {device.wechatId || "未绑定"}
|
||||
</div>
|
||||
<div style={{ fontSize: 14, color: "#999", marginTop: 2 }}>
|
||||
好友数: {device.totalFriend ?? "-"}
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color:
|
||||
device.status === "online" || device.alive === 1
|
||||
? "#52c41a"
|
||||
: "#aaa",
|
||||
marginLeft: 8,
|
||||
}}
|
||||
>
|
||||
{device.status === "online" || device.alive === 1
|
||||
? "在线"
|
||||
: "离线"}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* 无限滚动提示(仅在不分页时显示) */}
|
||||
{!usePagination && (
|
||||
<div
|
||||
ref={observerRef}
|
||||
style={{ padding: 12, textAlign: "center", color: "#888" }}
|
||||
>
|
||||
{loading && <SpinLoading style={{ "--size": "24px" }} />}
|
||||
{!hasMore && devices.length > 0 && "没有更多设备了"}
|
||||
{!hasMore && devices.length === 0 && "暂无设备"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* 添加设备弹窗 */}
|
||||
<Popup
|
||||
visible={addVisible}
|
||||
onMaskClick={() => setAddVisible(false)}
|
||||
bodyStyle={{
|
||||
borderTopLeftRadius: 16,
|
||||
borderTopRightRadius: 16,
|
||||
minHeight: 320,
|
||||
}}
|
||||
>
|
||||
<div style={{ padding: 20 }}>
|
||||
<Tabs
|
||||
activeKey={addTab}
|
||||
onChange={setAddTab}
|
||||
style={{ marginBottom: 16 }}
|
||||
>
|
||||
<Tabs.Tab title="扫码添加" key="scan" />
|
||||
<Tabs.Tab title="手动添加" key="manual" />
|
||||
</Tabs>
|
||||
{addTab === "scan" && (
|
||||
<div style={{ textAlign: "center", minHeight: 200 }}>
|
||||
<Button
|
||||
type="error"
|
||||
onClick={handleGetQr}
|
||||
loading={qrLoading}
|
||||
icon={<QrcodeOutlined />}
|
||||
>
|
||||
获取二维码
|
||||
</Button>
|
||||
{qrCode && (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<img
|
||||
src={qrCode}
|
||||
alt="二维码"
|
||||
style={{
|
||||
width: 180,
|
||||
height: 180,
|
||||
background: "#f5f5f5",
|
||||
borderRadius: 8,
|
||||
}}
|
||||
/>
|
||||
<div style={{ color: "#888", fontSize: 12, marginTop: 8 }}>
|
||||
请用手机扫码添加设备
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{addTab === "manual" && (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
||||
<Input
|
||||
placeholder="设备名称"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
allowClear
|
||||
/>
|
||||
<Input
|
||||
placeholder="设备IMEI"
|
||||
value={imei}
|
||||
onChange={(e) => setImei(e.target.value)}
|
||||
allowClear
|
||||
/>
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={handleAddDevice}
|
||||
loading={addLoading}
|
||||
>
|
||||
添加
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Popup>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Devices;
|
||||
|
||||
37
nkebao/src/utils/common.ts
Normal file
37
nkebao/src/utils/common.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Modal } from "antd-mobile";
|
||||
|
||||
/**
|
||||
* 通用js调用弹窗,Promise风格
|
||||
* @param content 弹窗内容
|
||||
* @param config 配置项(title, cancelText, confirmText)
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
export const comfirm = (
|
||||
content: string,
|
||||
config?: {
|
||||
title?: string;
|
||||
cancelText?: string;
|
||||
confirmText?: string;
|
||||
}
|
||||
): Promise<void> => {
|
||||
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(),
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user