功能:边缘节点页面重写为卡片列表
- VxeGrid 表格改为 Card 卡片布局 - 新增 getDeviceList() 调用 WVP list 接口 - 每张卡片展示:设备ID、状态Tag、最后心跳、运行时长、摄像头数、配置版本 - data.ts 精简为状态配置和 formatUptime 工具函数 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -31,26 +31,34 @@ export namespace AiotEdgeApi {
|
||||
}
|
||||
|
||||
// ==================== 边缘设备 API ====================
|
||||
// 数据源:WVP 数据库 wvp_ai_edge_device 表
|
||||
// 路径经 vite proxy: /admin-api/aiot/device/* → rewrite → /api/ai/* → WVP:18080
|
||||
|
||||
/** 获取全部边缘设备列表 */
|
||||
export function getDeviceList() {
|
||||
return wvpRequestClient.get<AiotEdgeApi.Device[]>(
|
||||
'/aiot/device/device/list',
|
||||
);
|
||||
}
|
||||
|
||||
/** 分页查询边缘设备列表 */
|
||||
export function getDevicePage(params: PageParam) {
|
||||
return wvpRequestClient.get<PageResult<AiotEdgeApi.Device>>(
|
||||
'/api/ai/device/page',
|
||||
'/aiot/device/device/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 获取设备详情 */
|
||||
export function getDevice(deviceId: string) {
|
||||
return wvpRequestClient.get<AiotEdgeApi.Device>(
|
||||
'/api/ai/device/get',
|
||||
{ params: { deviceId } },
|
||||
);
|
||||
return wvpRequestClient.get<AiotEdgeApi.Device>('/aiot/device/device/get', {
|
||||
params: { deviceId },
|
||||
});
|
||||
}
|
||||
|
||||
/** 获取设备统计 */
|
||||
export function getDeviceStatistics() {
|
||||
return wvpRequestClient.get<AiotEdgeApi.DeviceStatistics>(
|
||||
'/api/ai/device/statistics',
|
||||
'/aiot/device/device/statistics',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,77 +1,16 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
/** 设备状态配置 */
|
||||
export const STATUS_CONFIG: Record<string, { color: string; label: string }> = {
|
||||
online: { color: 'success', label: '在线' },
|
||||
offline: { color: 'default', label: '离线' },
|
||||
};
|
||||
|
||||
/** 设备状态选项 */
|
||||
export const DEVICE_STATUS_OPTIONS = [
|
||||
{ label: '在线', value: 'online' },
|
||||
{ label: '离线', value: 'offline' },
|
||||
{ label: '异常', value: 'error' },
|
||||
];
|
||||
|
||||
/** 边缘设备搜索表单 */
|
||||
export function useEdgeGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '设备状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: DEVICE_STATUS_OPTIONS,
|
||||
placeholder: '请选择设备状态',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 边缘设备列表字段 */
|
||||
export function useEdgeGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'deviceId',
|
||||
title: '设备ID',
|
||||
minWidth: 160,
|
||||
},
|
||||
{
|
||||
field: 'deviceName',
|
||||
title: '设备名称',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '状态',
|
||||
minWidth: 90,
|
||||
slots: { default: 'status' },
|
||||
},
|
||||
{
|
||||
field: 'lastHeartbeat',
|
||||
title: '最后心跳',
|
||||
minWidth: 170,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'uptimeSeconds',
|
||||
title: '运行时长',
|
||||
minWidth: 100,
|
||||
slots: { default: 'uptime' },
|
||||
},
|
||||
{
|
||||
field: 'framesProcessed',
|
||||
title: '处理帧数',
|
||||
minWidth: 100,
|
||||
slots: { default: 'frames' },
|
||||
},
|
||||
{
|
||||
field: 'alertsGenerated',
|
||||
title: '告警数',
|
||||
minWidth: 90,
|
||||
slots: { default: 'alerts' },
|
||||
},
|
||||
{
|
||||
field: 'updatedAt',
|
||||
title: '更新时间',
|
||||
minWidth: 170,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
];
|
||||
/** 格式化运行时长 */
|
||||
export function formatUptime(seconds?: number): string {
|
||||
if (seconds == null) return '-';
|
||||
const d = Math.floor(seconds / 86400);
|
||||
const h = Math.floor((seconds % 86400) / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
if (d > 0) return `${d}天 ${h}小时`;
|
||||
if (h > 0) return `${h}小时 ${m}分钟`;
|
||||
return `${m}分钟`;
|
||||
}
|
||||
|
||||
@@ -1,92 +1,134 @@
|
||||
<script setup lang="ts">
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { AiotEdgeApi } from '#/api/aiot/edge';
|
||||
import { ref, onMounted } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Tag } from 'ant-design-vue';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Descriptions,
|
||||
DescriptionsItem,
|
||||
Empty,
|
||||
Row,
|
||||
Spin,
|
||||
Tag,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getDevicePage } from '#/api/aiot/edge';
|
||||
import { getDeviceList } from '#/api/aiot/edge';
|
||||
import type { AiotEdgeApi } from '#/api/aiot/edge';
|
||||
|
||||
import { useEdgeGridColumns, useEdgeGridFormSchema } from './data';
|
||||
import { formatUptime, STATUS_CONFIG } from './data';
|
||||
|
||||
defineOptions({ name: 'AiotEdgeNode' });
|
||||
|
||||
/** 获取状态颜色 */
|
||||
function getStatusColor(status?: string) {
|
||||
const colorMap: Record<string, string> = {
|
||||
online: 'success',
|
||||
offline: 'default',
|
||||
error: 'error',
|
||||
};
|
||||
return status ? colorMap[status] || 'default' : 'default';
|
||||
const loading = ref(false);
|
||||
const devices = ref<AiotEdgeApi.Device[]>([]);
|
||||
|
||||
async function fetchDevices() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const list = await getDeviceList();
|
||||
devices.value = Array.isArray(list) ? list : [];
|
||||
} catch {
|
||||
devices.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 格式化运行时长 */
|
||||
function formatUptime(seconds?: number) {
|
||||
if (seconds == null) return '-';
|
||||
const h = Math.floor(seconds / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
if (h > 0) return `${h}h ${m}m`;
|
||||
return `${m}m`;
|
||||
function formatTime(time?: string) {
|
||||
if (!time) return '-';
|
||||
return time;
|
||||
}
|
||||
|
||||
const [Grid] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useEdgeGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useEdgeGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getDevicePage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
status: formValues?.status,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<AiotEdgeApi.Device>,
|
||||
});
|
||||
onMounted(fetchDevices);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<Grid table-title="边缘节点管理">
|
||||
<!-- 状态列 -->
|
||||
<template #status="{ row }">
|
||||
<Tag :color="getStatusColor(row.status)">
|
||||
{{ row.statusName || row.status || '-' }}
|
||||
</Tag>
|
||||
</template>
|
||||
<div class="edge-node-page">
|
||||
<div class="page-header">
|
||||
<h3 class="page-title">边缘节点管理</h3>
|
||||
<Button type="primary" :loading="loading" @click="fetchDevices">
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- 运行时长列 -->
|
||||
<template #uptime="{ row }">
|
||||
<span>{{ formatUptime(row.uptimeSeconds) }}</span>
|
||||
</template>
|
||||
<Spin :spinning="loading">
|
||||
<Empty v-if="!loading && devices.length === 0" description="暂无边缘节点数据" />
|
||||
|
||||
<!-- 处理帧数列 -->
|
||||
<template #frames="{ row }">
|
||||
<span>{{ row.framesProcessed?.toLocaleString() ?? '-' }}</span>
|
||||
</template>
|
||||
<Row v-else :gutter="[16, 16]">
|
||||
<Col
|
||||
v-for="device in devices"
|
||||
:key="device.deviceId"
|
||||
:xs="24"
|
||||
:sm="24"
|
||||
:md="12"
|
||||
:lg="8"
|
||||
:xl="8"
|
||||
>
|
||||
<Card hoverable>
|
||||
<template #title>
|
||||
<div class="card-title">
|
||||
<span class="device-id">{{ device.deviceId || '-' }}</span>
|
||||
<Tag
|
||||
:color="STATUS_CONFIG[device.status || '']?.color || 'default'"
|
||||
>
|
||||
{{ STATUS_CONFIG[device.status || '']?.label || device.status || '未知' }}
|
||||
</Tag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 告警数列 -->
|
||||
<template #alerts="{ row }">
|
||||
<span>{{ row.alertsGenerated?.toLocaleString() ?? '-' }}</span>
|
||||
</template>
|
||||
</Grid>
|
||||
<Descriptions :column="1" size="small">
|
||||
<DescriptionsItem label="最后心跳">
|
||||
{{ formatTime(device.lastHeartbeat) }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="运行时长">
|
||||
{{ formatUptime(device.uptimeSeconds) }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="摄像头数">
|
||||
{{ device.streamCount ?? '-' }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="配置版本">
|
||||
{{ device.configVersion || '-' }}
|
||||
</DescriptionsItem>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Spin>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.edge-node-page {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.device-id {
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user