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 { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { kickManagedDevice, listManagedDevices, updateManagedDevice } from "@/api";
|
||||
import PageHeader from "@/components/shared/PageHeader";
|
||||
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 { usePermission } from "@/hooks/usePermission";
|
||||
import type { DeviceInfo } from "@/types";
|
||||
import { getStandardPagination } from "@/utils/pagination";
|
||||
import "./index.less";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
|
@ -29,6 +30,7 @@ export default function Devices() {
|
|||
const [open, setOpen] = useState(false);
|
||||
const [editing, setEditing] = useState<DeviceInfo | null>(null);
|
||||
const [form] = Form.useForm<DeviceFormValues>();
|
||||
const [pagination, setPagination] = useState({ current: 1, pageSize: 20 });
|
||||
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
|
|
@ -63,6 +65,12 @@ export default function Devices() {
|
|||
});
|
||||
}, [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 total = devices.length;
|
||||
const online = devices.filter((device) => device.online).length;
|
||||
|
|
@ -104,79 +112,21 @@ export default function Devices() {
|
|||
await loadData();
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
title={t("devices.title")}
|
||||
subtitle={t("devices.subtitle")}
|
||||
headerExtra={
|
||||
<Button icon={<ReloadOutlined />} onClick={loadData}>
|
||||
{t("common.refresh")}
|
||||
</Button>
|
||||
}
|
||||
toolbar={
|
||||
<Input
|
||||
placeholder={t("devicesExt.searchPlaceholder")}
|
||||
prefix={<SearchOutlined aria-hidden="true" />}
|
||||
style={{ width: 420 }}
|
||||
value={searchText}
|
||||
onChange={(event) => setSearchText(event.target.value)}
|
||||
allowClear
|
||||
aria-label={t("devicesExt.searchLabel")}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Row gutter={[16, 16]} className="devices-metrics">
|
||||
<Col xs={24} md={8}>
|
||||
<Card className="devices-metric-card devices-metric-card--total" bordered={false}>
|
||||
<div className="devices-metric-card__icon">
|
||||
<DesktopOutlined aria-hidden="true" />
|
||||
</div>
|
||||
<div className="devices-metric-card__content">
|
||||
<div className="devices-metric-card__label">{t("devicesExt.totalDevices")}</div>
|
||||
<div className="devices-metric-card__value tabular-nums">{stats.total}</div>
|
||||
<div className="devices-metric-card__hint">{t("devicesExt.totalDevicesHint")}</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} md={8}>
|
||||
<Card className="devices-metric-card devices-metric-card--online" bordered={false}>
|
||||
<div className="devices-metric-card__icon">
|
||||
<ThunderboltOutlined aria-hidden="true" />
|
||||
</div>
|
||||
<div className="devices-metric-card__content">
|
||||
<div className="devices-metric-card__label">{t("devicesExt.onlineDevices")}</div>
|
||||
<div className="devices-metric-card__value tabular-nums">{stats.online}</div>
|
||||
<div className="devices-metric-card__hint">{t("devicesExt.onlineDevicesHint")}</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} md={8}>
|
||||
<Card className="devices-metric-card devices-metric-card--enabled" bordered={false}>
|
||||
<div className="devices-metric-card__icon">
|
||||
<CheckCircleOutlined aria-hidden="true" />
|
||||
</div>
|
||||
<div className="devices-metric-card__content">
|
||||
<div className="devices-metric-card__label">{t("devicesExt.enabledDevices")}</div>
|
||||
<div className="devices-metric-card__value tabular-nums">{stats.enabled}</div>
|
||||
<div className="devices-metric-card__hint">{t("devicesExt.enabledDevicesHint")}</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
const handlePageChange = (page: number, pageSize: number) => {
|
||||
setPagination({ current: page, pageSize });
|
||||
};
|
||||
|
||||
<Table<DeviceInfo>
|
||||
rowKey="deviceId"
|
||||
dataSource={filteredData}
|
||||
loading={loading}
|
||||
size="middle"
|
||||
scroll={{ y: "calc(100vh - 350px)", x: 1200 }}
|
||||
pagination={getStandardPagination(filteredData.length, 1, 1000)}
|
||||
columns={[
|
||||
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) => (
|
||||
render: (_value: unknown, record: DeviceInfo) => (
|
||||
<Space>
|
||||
<div className="device-icon-placeholder">
|
||||
<DesktopOutlined aria-hidden="true" />
|
||||
|
|
@ -192,7 +142,7 @@ export default function Devices() {
|
|||
title: t("devices.owner"),
|
||||
key: "user",
|
||||
width: 220,
|
||||
render: (_value: unknown, record) => {
|
||||
render: (_value: unknown, record: DeviceInfo) => {
|
||||
if (!record.userId) {
|
||||
return <Text type="secondary">{t("devicesExt.unboundAccount")}</Text>;
|
||||
}
|
||||
|
|
@ -260,8 +210,8 @@ export default function Devices() {
|
|||
title: t("common.action"),
|
||||
key: "action",
|
||||
width: 140,
|
||||
fixed: "right",
|
||||
render: (_value: unknown, record) => (
|
||||
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")} />
|
||||
|
|
@ -279,8 +229,87 @@ export default function Devices() {
|
|||
</Space>
|
||||
)
|
||||
}
|
||||
]}
|
||||
];
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
title={t("devices.title")}
|
||||
subtitle={t("devices.subtitle")}
|
||||
headerExtra={
|
||||
<Button icon={<ReloadOutlined />} onClick={loadData}>
|
||||
{t("common.refresh")}
|
||||
</Button>
|
||||
}
|
||||
toolbar={
|
||||
<Input
|
||||
placeholder={t("devicesExt.searchPlaceholder")}
|
||||
prefix={<SearchOutlined aria-hidden="true" />}
|
||||
style={{ width: 420 }}
|
||||
value={searchText}
|
||||
onChange={(event) => handleSearchChange(event.target.value)}
|
||||
allowClear
|
||||
aria-label={t("devicesExt.searchLabel")}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Row gutter={[16, 16]} className="devices-metrics">
|
||||
<Col xs={24} md={8}>
|
||||
<Card className="devices-metric-card devices-metric-card--total" bordered={false}>
|
||||
<div className="devices-metric-card__icon">
|
||||
<DesktopOutlined aria-hidden="true" />
|
||||
</div>
|
||||
<div className="devices-metric-card__content">
|
||||
<div className="devices-metric-card__label">{t("devicesExt.totalDevices")}</div>
|
||||
<div className="devices-metric-card__value tabular-nums">{stats.total}</div>
|
||||
<div className="devices-metric-card__hint">{t("devicesExt.totalDevicesHint")}</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} md={8}>
|
||||
<Card className="devices-metric-card devices-metric-card--online" bordered={false}>
|
||||
<div className="devices-metric-card__icon">
|
||||
<ThunderboltOutlined aria-hidden="true" />
|
||||
</div>
|
||||
<div className="devices-metric-card__content">
|
||||
<div className="devices-metric-card__label">{t("devicesExt.onlineDevices")}</div>
|
||||
<div className="devices-metric-card__value tabular-nums">{stats.online}</div>
|
||||
<div className="devices-metric-card__hint">{t("devicesExt.onlineDevicesHint")}</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} md={8}>
|
||||
<Card className="devices-metric-card devices-metric-card--enabled" bordered={false}>
|
||||
<div className="devices-metric-card__icon">
|
||||
<CheckCircleOutlined aria-hidden="true" />
|
||||
</div>
|
||||
<div className="devices-metric-card__content">
|
||||
<div className="devices-metric-card__label">{t("devicesExt.enabledDevices")}</div>
|
||||
<div className="devices-metric-card__value tabular-nums">{stats.enabled}</div>
|
||||
<div className="devices-metric-card__hint">{t("devicesExt.enabledDevicesHint")}</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<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 }}>
|
||||
<div className="app-page__table-wrap" style={{ flex: 1, minHeight: 0, overflow: "auto", padding: "0 24px" }}>
|
||||
<ListTable<DeviceInfo>
|
||||
rowKey="deviceId"
|
||||
dataSource={pagedData}
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
scroll={{ y: "calc(100vh - 520px)", x: 1200 }}
|
||||
pagination={false}
|
||||
/>
|
||||
</div>
|
||||
<AppPagination
|
||||
current={pagination.current}
|
||||
pageSize={pagination.pageSize}
|
||||
total={filteredData.length}
|
||||
onChange={handlePageChange}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Drawer
|
||||
title={
|
||||
<div className="device-drawer-title">
|
||||
|
|
|
|||
Loading…
Reference in New Issue