功能:边缘节点页面重写为卡片列表

- 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:
2026-03-17 17:47:31 +08:00
parent 4cd07c3fef
commit 694c5c7af1
3 changed files with 140 additions and 151 deletions

View File

@@ -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',
);
}

View File

@@ -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}分钟`;
}

View File

@@ -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>