feat: 添加设备分页和搜索功能
- 引入 `ListTable` 和 `AppPagination` 组件 - 添加分页状态和处理函数 - 更新搜索逻辑以重置分页 - 重构表格列定义和数据加载逻辑dev_na
parent
b9ec41095f
commit
a49888587f
|
|
@ -1,14 +1,15 @@
|
||||||
import { Button, Card, Col, Drawer, Form, Input, Popconfirm, Row, Select, Space, Table, Tag, Typography, message } from "antd";
|
import { Button, Card, Col, Drawer, Form, Input, Popconfirm, Row, Select, Space, Tag, Typography, message } from "antd";
|
||||||
import { CheckCircleOutlined, DesktopOutlined, DisconnectOutlined, EditOutlined, ReloadOutlined, SearchOutlined, ThunderboltOutlined, UserOutlined } from "@ant-design/icons";
|
import { CheckCircleOutlined, DesktopOutlined, DisconnectOutlined, EditOutlined, ReloadOutlined, SearchOutlined, ThunderboltOutlined, UserOutlined } from "@ant-design/icons";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { kickManagedDevice, listManagedDevices, updateManagedDevice } from "@/api";
|
import { kickManagedDevice, listManagedDevices, updateManagedDevice } from "@/api";
|
||||||
import PageHeader from "@/components/shared/PageHeader";
|
import PageHeader from "@/components/shared/PageHeader";
|
||||||
import PageContainer from "@/components/shared/PageContainer";
|
import PageContainer from "@/components/shared/PageContainer";
|
||||||
|
import ListTable from "@/components/shared/ListTable/ListTable";
|
||||||
|
import AppPagination from "@/components/shared/AppPagination";
|
||||||
import { useDict } from "@/hooks/useDict";
|
import { useDict } from "@/hooks/useDict";
|
||||||
import { usePermission } from "@/hooks/usePermission";
|
import { usePermission } from "@/hooks/usePermission";
|
||||||
import type { DeviceInfo } from "@/types";
|
import type { DeviceInfo } from "@/types";
|
||||||
import { getStandardPagination } from "@/utils/pagination";
|
|
||||||
import "./index.less";
|
import "./index.less";
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
@ -29,6 +30,7 @@ export default function Devices() {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [editing, setEditing] = useState<DeviceInfo | null>(null);
|
const [editing, setEditing] = useState<DeviceInfo | null>(null);
|
||||||
const [form] = Form.useForm<DeviceFormValues>();
|
const [form] = Form.useForm<DeviceFormValues>();
|
||||||
|
const [pagination, setPagination] = useState({ current: 1, pageSize: 20 });
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
@ -63,6 +65,12 @@ export default function Devices() {
|
||||||
});
|
});
|
||||||
}, [devices, searchText]);
|
}, [devices, searchText]);
|
||||||
|
|
||||||
|
const pagedData = useMemo(() => {
|
||||||
|
const start = (pagination.current - 1) * pagination.pageSize;
|
||||||
|
const end = start + pagination.pageSize;
|
||||||
|
return filteredData.slice(start, end);
|
||||||
|
}, [filteredData, pagination]);
|
||||||
|
|
||||||
const stats = useMemo(() => {
|
const stats = useMemo(() => {
|
||||||
const total = devices.length;
|
const total = devices.length;
|
||||||
const online = devices.filter((device) => device.online).length;
|
const online = devices.filter((device) => device.online).length;
|
||||||
|
|
@ -104,6 +112,125 @@ export default function Devices() {
|
||||||
await loadData();
|
await loadData();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handlePageChange = (page: number, pageSize: number) => {
|
||||||
|
setPagination({ current: page, pageSize });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchChange = (value: string) => {
|
||||||
|
setSearchText(value);
|
||||||
|
setPagination((prev) => ({ ...prev, current: 1 }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: t("devicesExt.device"),
|
||||||
|
key: "device",
|
||||||
|
width: 280,
|
||||||
|
render: (_value: unknown, record: DeviceInfo) => (
|
||||||
|
<Space>
|
||||||
|
<div className="device-icon-placeholder">
|
||||||
|
<DesktopOutlined aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="device-name font-medium">{record.deviceName || t("devicesExt.unnamedDevice")}</div>
|
||||||
|
<div className="device-code text-xs text-gray-400 tabular-nums">{record.deviceCode}</div>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("devices.owner"),
|
||||||
|
key: "user",
|
||||||
|
width: 220,
|
||||||
|
render: (_value: unknown, record: DeviceInfo) => {
|
||||||
|
if (!record.userId) {
|
||||||
|
return <Text type="secondary">{t("devicesExt.unboundAccount")}</Text>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Space>
|
||||||
|
<UserOutlined aria-hidden="true" style={{ color: "#8c8c8c" }} />
|
||||||
|
<span>{record.displayName || record.username || `#${record.userId}`}</span>
|
||||||
|
<Text type="secondary" style={{ fontSize: "12px" }} className="tabular-nums">
|
||||||
|
({t("devicesExt.ownerId")}: {record.userId})
|
||||||
|
</Text>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("devicesExt.terminalType"),
|
||||||
|
dataIndex: "terminalType",
|
||||||
|
width: 140,
|
||||||
|
render: (text: string) => <Tag>{text || "-"}</Tag>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("devicesExt.terminalVersion"),
|
||||||
|
dataIndex: "terminalVersion",
|
||||||
|
width: 160,
|
||||||
|
render: (text: string) => <Text className="tabular-nums">{text || "-"}</Text>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("devicesExt.onlineStatus"),
|
||||||
|
dataIndex: "online",
|
||||||
|
width: 120,
|
||||||
|
render: (online: boolean) => (
|
||||||
|
<Tag color={online ? "green" : "default"}>{online ? t("devicesExt.online") : t("devicesExt.offline")}</Tag>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("devicesExt.lastOnlineAt"),
|
||||||
|
dataIndex: "lastOnlineAt",
|
||||||
|
width: 180,
|
||||||
|
render: (text: string) => (
|
||||||
|
<Text type="secondary" className="tabular-nums">
|
||||||
|
{text ? text.replace("T", " ").substring(0, 19) : "-"}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("common.status"),
|
||||||
|
dataIndex: "status",
|
||||||
|
width: 100,
|
||||||
|
render: (status: number) => {
|
||||||
|
const item = statusDict.find((dictItem) => dictItem.itemValue === String(status));
|
||||||
|
return <Tag color={status === 1 ? "green" : "red"}>{item?.itemLabel || (status === 1 ? t("devicesExt.enabled") : t("devicesExt.disabled"))}</Tag>;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("devices.updateTime"),
|
||||||
|
dataIndex: "updatedAt",
|
||||||
|
width: 180,
|
||||||
|
render: (text: string) => (
|
||||||
|
<Text type="secondary" className="tabular-nums">
|
||||||
|
{text?.replace("T", " ").substring(0, 19)}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("common.action"),
|
||||||
|
key: "action",
|
||||||
|
width: 140,
|
||||||
|
fixed: "right" as const,
|
||||||
|
render: (_value: unknown, record: DeviceInfo) => (
|
||||||
|
<Space>
|
||||||
|
{can("device:update") ? (
|
||||||
|
<Button type="text" icon={<EditOutlined aria-hidden="true" />} onClick={() => openEdit(record)} aria-label={t("devicesExt.editDevice")} />
|
||||||
|
) : null}
|
||||||
|
{can("device:update") && record.online ? (
|
||||||
|
<Popconfirm title={t("devicesExt.kickDeviceConfirm")} onConfirm={() => kick(record)}>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
danger
|
||||||
|
icon={<DisconnectOutlined aria-hidden="true" />}
|
||||||
|
aria-label={t("devicesExt.kickDevice")}
|
||||||
|
/>
|
||||||
|
</Popconfirm>
|
||||||
|
) : null}
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer
|
<PageContainer
|
||||||
title={t("devices.title")}
|
title={t("devices.title")}
|
||||||
|
|
@ -119,7 +246,7 @@ export default function Devices() {
|
||||||
prefix={<SearchOutlined aria-hidden="true" />}
|
prefix={<SearchOutlined aria-hidden="true" />}
|
||||||
style={{ width: 420 }}
|
style={{ width: 420 }}
|
||||||
value={searchText}
|
value={searchText}
|
||||||
onChange={(event) => setSearchText(event.target.value)}
|
onChange={(event) => handleSearchChange(event.target.value)}
|
||||||
allowClear
|
allowClear
|
||||||
aria-label={t("devicesExt.searchLabel")}
|
aria-label={t("devicesExt.searchLabel")}
|
||||||
/>
|
/>
|
||||||
|
|
@ -164,123 +291,25 @@ export default function Devices() {
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Table<DeviceInfo>
|
<Card className="app-page__content-card" styles={{ body: { padding: 0, flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }} style={{ flex: 1, minHeight: 0, marginTop: 16 }}>
|
||||||
rowKey="deviceId"
|
<div className="app-page__table-wrap" style={{ flex: 1, minHeight: 0, overflow: "auto", padding: "0 24px" }}>
|
||||||
dataSource={filteredData}
|
<ListTable<DeviceInfo>
|
||||||
loading={loading}
|
rowKey="deviceId"
|
||||||
size="middle"
|
dataSource={pagedData}
|
||||||
scroll={{ y: "calc(100vh - 350px)", x: 1200 }}
|
loading={loading}
|
||||||
pagination={getStandardPagination(filteredData.length, 1, 1000)}
|
columns={columns}
|
||||||
columns={[
|
scroll={{ y: "calc(100vh - 520px)", x: 1200 }}
|
||||||
{
|
pagination={false}
|
||||||
title: t("devicesExt.device"),
|
/>
|
||||||
key: "device",
|
</div>
|
||||||
width: 280,
|
<AppPagination
|
||||||
render: (_value: unknown, record) => (
|
current={pagination.current}
|
||||||
<Space>
|
pageSize={pagination.pageSize}
|
||||||
<div className="device-icon-placeholder">
|
total={filteredData.length}
|
||||||
<DesktopOutlined aria-hidden="true" />
|
onChange={handlePageChange}
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="device-name font-medium">{record.deviceName || t("devicesExt.unnamedDevice")}</div>
|
|
||||||
<div className="device-code text-xs text-gray-400 tabular-nums">{record.deviceCode}</div>
|
|
||||||
</div>
|
|
||||||
</Space>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("devices.owner"),
|
|
||||||
key: "user",
|
|
||||||
width: 220,
|
|
||||||
render: (_value: unknown, record) => {
|
|
||||||
if (!record.userId) {
|
|
||||||
return <Text type="secondary">{t("devicesExt.unboundAccount")}</Text>;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Space>
|
|
||||||
<UserOutlined aria-hidden="true" style={{ color: "#8c8c8c" }} />
|
|
||||||
<span>{record.displayName || record.username || `#${record.userId}`}</span>
|
|
||||||
<Text type="secondary" style={{ fontSize: "12px" }} className="tabular-nums">
|
|
||||||
({t("devicesExt.ownerId")}: {record.userId})
|
|
||||||
</Text>
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("devicesExt.terminalType"),
|
|
||||||
dataIndex: "terminalType",
|
|
||||||
width: 140,
|
|
||||||
render: (text: string) => <Tag>{text || "-"}</Tag>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("devicesExt.terminalVersion"),
|
|
||||||
dataIndex: "terminalVersion",
|
|
||||||
width: 160,
|
|
||||||
render: (text: string) => <Text className="tabular-nums">{text || "-"}</Text>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("devicesExt.onlineStatus"),
|
|
||||||
dataIndex: "online",
|
|
||||||
width: 120,
|
|
||||||
render: (online: boolean) => (
|
|
||||||
<Tag color={online ? "green" : "default"}>{online ? t("devicesExt.online") : t("devicesExt.offline")}</Tag>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("devicesExt.lastOnlineAt"),
|
|
||||||
dataIndex: "lastOnlineAt",
|
|
||||||
width: 180,
|
|
||||||
render: (text: string) => (
|
|
||||||
<Text type="secondary" className="tabular-nums">
|
|
||||||
{text ? text.replace("T", " ").substring(0, 19) : "-"}
|
|
||||||
</Text>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("common.status"),
|
|
||||||
dataIndex: "status",
|
|
||||||
width: 100,
|
|
||||||
render: (status: number) => {
|
|
||||||
const item = statusDict.find((dictItem) => dictItem.itemValue === String(status));
|
|
||||||
return <Tag color={status === 1 ? "green" : "red"}>{item?.itemLabel || (status === 1 ? t("devicesExt.enabled") : t("devicesExt.disabled"))}</Tag>;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("devices.updateTime"),
|
|
||||||
dataIndex: "updatedAt",
|
|
||||||
width: 180,
|
|
||||||
render: (text: string) => (
|
|
||||||
<Text type="secondary" className="tabular-nums">
|
|
||||||
{text?.replace("T", " ").substring(0, 19)}
|
|
||||||
</Text>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("common.action"),
|
|
||||||
key: "action",
|
|
||||||
width: 140,
|
|
||||||
fixed: "right",
|
|
||||||
render: (_value: unknown, record) => (
|
|
||||||
<Space>
|
|
||||||
{can("device:update") ? (
|
|
||||||
<Button type="text" icon={<EditOutlined aria-hidden="true" />} onClick={() => openEdit(record)} aria-label={t("devicesExt.editDevice")} />
|
|
||||||
) : null}
|
|
||||||
{can("device:update") && record.online ? (
|
|
||||||
<Popconfirm title={t("devicesExt.kickDeviceConfirm")} onConfirm={() => kick(record)}>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
danger
|
|
||||||
icon={<DisconnectOutlined aria-hidden="true" />}
|
|
||||||
aria-label={t("devicesExt.kickDevice")}
|
|
||||||
/>
|
|
||||||
</Popconfirm>
|
|
||||||
) : null}
|
|
||||||
</Space>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Drawer
|
<Drawer
|
||||||
title={
|
title={
|
||||||
<div className="device-drawer-title">
|
<div className="device-drawer-title">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue