|
|
|
|
@ -1,184 +1,163 @@
|
|
|
|
|
<template>
|
|
|
|
|
<div class="app-container home">
|
|
|
|
|
<!-- 统计卡片 -->
|
|
|
|
|
<!-- 欢迎区域 -->
|
|
|
|
|
<el-card shadow="hover" class="welcome-card mb-4">
|
|
|
|
|
<div class="welcome-content">
|
|
|
|
|
<div class="welcome-text">
|
|
|
|
|
<h2>欢迎使用 RFID 中间件管理系统</h2>
|
|
|
|
|
<p>{{ currentTime }}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
|
|
|
<!-- 快捷入口 -->
|
|
|
|
|
<el-row :gutter="16" class="mb-4">
|
|
|
|
|
<el-col :xs="12" :sm="12" :md="6">
|
|
|
|
|
<el-card shadow="hover" class="stat-card">
|
|
|
|
|
<div class="stat-card-inner">
|
|
|
|
|
<div class="stat-icon bg-primary"><el-icon :size="28"><Monitor /></el-icon></div>
|
|
|
|
|
<div class="stat-content">
|
|
|
|
|
<div class="stat-title">设备总数</div>
|
|
|
|
|
<div class="stat-value">{{ stats.totalDevices || 0 }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<el-card shadow="hover" class="shortcut-card" @click="$router.push('/base/rfidDevice')">
|
|
|
|
|
<div class="shortcut-inner">
|
|
|
|
|
<el-icon :size="36" color="#409eff"><Monitor /></el-icon>
|
|
|
|
|
<span>设备管理</span>
|
|
|
|
|
</div>
|
|
|
|
|
</el-card>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :xs="12" :sm="12" :md="6">
|
|
|
|
|
<el-card shadow="hover" class="stat-card">
|
|
|
|
|
<div class="stat-card-inner">
|
|
|
|
|
<div class="stat-icon bg-success"><el-icon :size="28"><CircleCheck /></el-icon></div>
|
|
|
|
|
<div class="stat-content">
|
|
|
|
|
<div class="stat-title">在线设备</div>
|
|
|
|
|
<div class="stat-value text-success">{{ stats.onlineDevices || 0 }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<el-card shadow="hover" class="shortcut-card" @click="$router.push('/base/rfidLocation')">
|
|
|
|
|
<div class="shortcut-inner">
|
|
|
|
|
<el-icon :size="36" color="#67c23a"><Location /></el-icon>
|
|
|
|
|
<span>位置管理</span>
|
|
|
|
|
</div>
|
|
|
|
|
</el-card>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :xs="12" :sm="12" :md="6">
|
|
|
|
|
<el-card shadow="hover" class="stat-card">
|
|
|
|
|
<div class="stat-card-inner">
|
|
|
|
|
<div class="stat-icon bg-warning"><el-icon :size="28"><CircleClose /></el-icon></div>
|
|
|
|
|
<div class="stat-content">
|
|
|
|
|
<div class="stat-title">离线设备</div>
|
|
|
|
|
<div class="stat-value text-warning">{{ stats.offlineDevices || 0 }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<el-card shadow="hover" class="shortcut-card" @click="$router.push('/base/rfidReadRecord')">
|
|
|
|
|
<div class="shortcut-inner">
|
|
|
|
|
<el-icon :size="36" color="#e6a23c"><Document /></el-icon>
|
|
|
|
|
<span>读取记录</span>
|
|
|
|
|
</div>
|
|
|
|
|
</el-card>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :xs="12" :sm="12" :md="6">
|
|
|
|
|
<el-card shadow="hover" class="stat-card">
|
|
|
|
|
<div class="stat-card-inner">
|
|
|
|
|
<div class="stat-icon bg-danger"><el-icon :size="28"><Warning /></el-icon></div>
|
|
|
|
|
<div class="stat-content">
|
|
|
|
|
<div class="stat-title">告警设备</div>
|
|
|
|
|
<div class="stat-value text-danger">{{ stats.alarmDevices || 0 }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<el-card shadow="hover" class="shortcut-card" @click="$router.push('/rfid/dashboard')">
|
|
|
|
|
<div class="shortcut-inner">
|
|
|
|
|
<el-icon :size="36" color="#f56c6c"><DataAnalysis /></el-icon>
|
|
|
|
|
<span>监控看板</span>
|
|
|
|
|
</div>
|
|
|
|
|
</el-card>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
|
|
|
|
|
<!-- 设备列表与告警记录 -->
|
|
|
|
|
<el-row :gutter="16">
|
|
|
|
|
<el-col :xs="24" :md="14">
|
|
|
|
|
<!-- 位置统计与列表 -->
|
|
|
|
|
<el-row :gutter="16" class="mb-4">
|
|
|
|
|
<!-- 位置统计卡片 -->
|
|
|
|
|
<el-col :xs="24" :md="8">
|
|
|
|
|
<el-card shadow="hover">
|
|
|
|
|
<template #header>
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
<span>设备列表</span>
|
|
|
|
|
<el-button type="primary" link @click="$router.push('/base/rfidDevice')">查看更多</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="card-header"><span>位置统计</span></div>
|
|
|
|
|
</template>
|
|
|
|
|
<el-table :data="deviceList" border stripe max-height="400" empty-text="暂无数据">
|
|
|
|
|
<el-table-column type="index" label="序号" width="60" align="center" />
|
|
|
|
|
<el-table-column prop="deviceCode" label="设备编号" min-width="120" />
|
|
|
|
|
<el-table-column prop="deviceName" label="设备名称" min-width="120" />
|
|
|
|
|
<el-table-column prop="locationAlias" label="所在位置" min-width="100">
|
|
|
|
|
<template #default="{ row }">{{ row.locationAlias || '-' }}</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column prop="onlineStatus" label="在线状态" width="100" align="center">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<dict-tag :options="online_status" :value="row.onlineStatus"/>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column prop="alarmStatus" label="告警状态" width="100" align="center">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<dict-tag :options="alarm_status" :value="row.alarmStatus"/>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
<div class="location-stats">
|
|
|
|
|
<div class="stat-item">
|
|
|
|
|
<span class="label">位置总数</span>
|
|
|
|
|
<span class="value">{{ stats.locationStats?.totalLocations || 0 }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<el-divider />
|
|
|
|
|
<div class="stat-item">
|
|
|
|
|
<span class="label">车间数量</span>
|
|
|
|
|
<span class="value text-primary">{{ stats.locationStats?.workshopCount || 0 }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat-item">
|
|
|
|
|
<span class="label">工序数量</span>
|
|
|
|
|
<span class="value text-success">{{ stats.locationStats?.processCount || 0 }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat-item">
|
|
|
|
|
<span class="label">工位数量</span>
|
|
|
|
|
<span class="value text-warning">{{ stats.locationStats?.stationCount || 0 }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</el-card>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :xs="24" :md="10">
|
|
|
|
|
<!-- 顶级位置列表 -->
|
|
|
|
|
<el-col :xs="24" :md="16">
|
|
|
|
|
<el-card shadow="hover">
|
|
|
|
|
<template #header>
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
<span>告警记录</span>
|
|
|
|
|
<el-button type="primary" link @click="$router.push('/base/rfidReadRecord')">查看更多</el-button>
|
|
|
|
|
<span>车间概览</span>
|
|
|
|
|
<el-button type="primary" link @click="$router.push('/base/rfidLocation')">管理位置</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
<el-table :data="alarmList" border stripe max-height="400" empty-text="暂无数据">
|
|
|
|
|
<el-table :data="stats.locations || []" border stripe max-height="260" empty-text="暂无位置数据">
|
|
|
|
|
<el-table-column type="index" label="序号" width="60" align="center" />
|
|
|
|
|
<el-table-column prop="deviceName" label="设备" min-width="100">
|
|
|
|
|
<template #default="{ row }">{{ row.deviceName || '-' }}</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column prop="alarmTime" label="时间" min-width="140">
|
|
|
|
|
<template #default="{ row }">{{ row.alarmTime || '-' }}</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column prop="alarmLevel" label="级别" width="80" align="center">
|
|
|
|
|
<el-table-column prop="locationCode" label="位置编号" min-width="100" />
|
|
|
|
|
<el-table-column prop="locationAlias" label="位置名称" min-width="120" />
|
|
|
|
|
<el-table-column prop="locationType" label="位置类型" width="100" align="center">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<dict-tag :options="alarm_level" :value="row.alarmLevel"/>
|
|
|
|
|
<dict-tag :options="location_type" :value="row.locationType"/>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column prop="alarmAction" label="行为" min-width="80">
|
|
|
|
|
<template #default="{ row }">{{ row.alarmAction || '-' }}</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column prop="childCount" label="子位置数" width="100" align="center" />
|
|
|
|
|
</el-table>
|
|
|
|
|
</el-card>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
|
|
|
|
|
<!-- 系统帮助 -->
|
|
|
|
|
<el-card shadow="hover">
|
|
|
|
|
<template #header>
|
|
|
|
|
<div class="card-header"><span>系统使用指南</span></div>
|
|
|
|
|
</template>
|
|
|
|
|
<el-collapse>
|
|
|
|
|
<el-collapse-item title="1. 如何管理位置?" name="1">
|
|
|
|
|
<p>进入【位置管理】模块,支持创建车间、工序、工位三级位置结构,便于设备归属管理。</p>
|
|
|
|
|
</el-collapse-item>
|
|
|
|
|
<el-collapse-item title="2. 如何管理设备?" name="2">
|
|
|
|
|
<p>进入【设备管理】模块,可新增、编辑、删除 RFID 读写器设备,并关联到指定位置。</p>
|
|
|
|
|
</el-collapse-item>
|
|
|
|
|
<el-collapse-item title="3. 如何查看读取记录?" name="3">
|
|
|
|
|
<p>进入【读取记录】模块,可按时间范围、设备筛选历史读卡数据。</p>
|
|
|
|
|
</el-collapse-item>
|
|
|
|
|
<el-collapse-item title="4. 如何进入监控看板?" name="4">
|
|
|
|
|
<p>点击【监控看板】快捷入口,可实时查看设备状态、成功率趋势和告警信息。</p>
|
|
|
|
|
</el-collapse-item>
|
|
|
|
|
</el-collapse>
|
|
|
|
|
</el-card>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup name="Index" lang="ts">
|
|
|
|
|
import { getRealtimeStats, getLocationTree } from "@/api/rfid/dashboard";
|
|
|
|
|
import type { LocationTreeNode, AlarmStatVO } from "@/api/rfid/dashboard/types";
|
|
|
|
|
import { onMounted, ref, getCurrentInstance, toRefs, type ComponentInternalInstance, onUnmounted } from 'vue';
|
|
|
|
|
import { getHomeStats, type HomeStatsVO } from "@/api/rfid/statistics";
|
|
|
|
|
|
|
|
|
|
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
|
|
|
|
const { online_status, alarm_status, alarm_level } = toRefs<any>(
|
|
|
|
|
proxy?.useDict("online_status", "alarm_status", "alarm_level")
|
|
|
|
|
);
|
|
|
|
|
const { location_type } = toRefs<any>(proxy?.useDict("location_type"));
|
|
|
|
|
|
|
|
|
|
const stats = ref({
|
|
|
|
|
totalDevices: 0,
|
|
|
|
|
onlineDevices: 0,
|
|
|
|
|
offlineDevices: 0,
|
|
|
|
|
alarmDevices: 0
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const deviceList = ref<LocationTreeNode[]>([]);
|
|
|
|
|
const alarmList = ref<AlarmStatVO[]>([]);
|
|
|
|
|
const stats = ref<Partial<HomeStatsVO>>({});
|
|
|
|
|
const currentTime = ref('');
|
|
|
|
|
let timer: number | null = null;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 从位置树中提取设备节点列表(locationType = '3' 且存在 deviceId)
|
|
|
|
|
*/
|
|
|
|
|
const extractDeviceList = (nodes: LocationTreeNode[] = []): LocationTreeNode[] => {
|
|
|
|
|
const result: LocationTreeNode[] = [];
|
|
|
|
|
const stack: LocationTreeNode[] = [...nodes];
|
|
|
|
|
|
|
|
|
|
while (stack.length > 0) {
|
|
|
|
|
const node = stack.pop() as LocationTreeNode;
|
|
|
|
|
if (node.children && node.children.length > 0) {
|
|
|
|
|
stack.push(...node.children);
|
|
|
|
|
}
|
|
|
|
|
if (node.locationType === "3" && node.deviceId != null) {
|
|
|
|
|
result.push(node);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
const updateTime = () => {
|
|
|
|
|
const now = new Date();
|
|
|
|
|
currentTime.value = now.toLocaleString('zh-CN', {
|
|
|
|
|
year: 'numeric', month: '2-digit', day: '2-digit',
|
|
|
|
|
hour: '2-digit', minute: '2-digit', second: '2-digit'
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const loadStats = async () => {
|
|
|
|
|
const loadData = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const res = await getRealtimeStats();
|
|
|
|
|
const data = res.data;
|
|
|
|
|
if (data && data.overview) {
|
|
|
|
|
stats.value = {
|
|
|
|
|
totalDevices: data.overview.deviceTotal ?? 0,
|
|
|
|
|
onlineDevices: data.overview.onlineCount ?? 0,
|
|
|
|
|
offlineDevices: data.overview.offlineCount ?? 0,
|
|
|
|
|
alarmDevices: data.overview.alarmCount ?? 0
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
alarmList.value = data?.alarmStats || [];
|
|
|
|
|
const res = await getHomeStats();
|
|
|
|
|
stats.value = res.data;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error("获取统计数据失败", e);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const loadDeviceList = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const res = await getLocationTree();
|
|
|
|
|
const tree = res.data || [];
|
|
|
|
|
deviceList.value = extractDeviceList(tree);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error("获取设备列表失败", e);
|
|
|
|
|
console.error("获取首页数据失败", e);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
loadStats();
|
|
|
|
|
loadDeviceList();
|
|
|
|
|
updateTime();
|
|
|
|
|
timer = window.setInterval(updateTime, 1000);
|
|
|
|
|
loadData();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
if (timer) clearInterval(timer);
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
@ -190,53 +169,74 @@ onMounted(() => {
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 统计卡片样式
|
|
|
|
|
.stat-card {
|
|
|
|
|
// 欢迎卡片
|
|
|
|
|
.welcome-card {
|
|
|
|
|
background: linear-gradient(135deg, #409eff 0%, #53a8ff 100%);
|
|
|
|
|
:deep(.el-card__body) {
|
|
|
|
|
padding: 16px 20px;
|
|
|
|
|
padding: 24px 32px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.welcome-content {
|
|
|
|
|
.welcome-text {
|
|
|
|
|
h2 {
|
|
|
|
|
color: #fff;
|
|
|
|
|
font-size: 22px;
|
|
|
|
|
margin: 0 0 8px 0;
|
|
|
|
|
}
|
|
|
|
|
p {
|
|
|
|
|
color: rgba(255,255,255,0.85);
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
margin: 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-card-inner {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
// 快捷入口卡片
|
|
|
|
|
.shortcut-card {
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.3s;
|
|
|
|
|
&:hover {
|
|
|
|
|
transform: translateY(-4px);
|
|
|
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
|
|
|
}
|
|
|
|
|
:deep(.el-card__body) {
|
|
|
|
|
padding: 24px 16px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-icon {
|
|
|
|
|
width: 56px;
|
|
|
|
|
height: 56px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
.shortcut-inner {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
color: #fff;
|
|
|
|
|
margin-right: 16px;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
|
|
|
|
&.bg-primary { background: linear-gradient(135deg, #409eff, #53a8ff); }
|
|
|
|
|
&.bg-success { background: linear-gradient(135deg, #67c23a, #85ce61); }
|
|
|
|
|
&.bg-warning { background: linear-gradient(135deg, #e6a23c, #ebb563); }
|
|
|
|
|
&.bg-danger { background: linear-gradient(135deg, #f56c6c, #f78989); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-content {
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
|
|
|
|
.stat-title {
|
|
|
|
|
gap: 12px;
|
|
|
|
|
span {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
color: #909399;
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-value {
|
|
|
|
|
font-size: 28px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
color: #303133;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&.text-success { color: #67c23a; }
|
|
|
|
|
&.text-warning { color: #e6a23c; }
|
|
|
|
|
&.text-danger { color: #f56c6c; }
|
|
|
|
|
// 位置统计
|
|
|
|
|
.location-stats {
|
|
|
|
|
.stat-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
padding: 8px 0;
|
|
|
|
|
.label {
|
|
|
|
|
color: #606266;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
}
|
|
|
|
|
.value {
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: #303133;
|
|
|
|
|
&.text-primary { color: #409eff; }
|
|
|
|
|
&.text-success { color: #67c23a; }
|
|
|
|
|
&.text-warning { color: #e6a23c; }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
:deep(.el-divider) {
|
|
|
|
|
margin: 12px 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -245,7 +245,6 @@ onMounted(() => {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
|
|
|
|
span {
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
@ -253,7 +252,7 @@ onMounted(() => {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 表格容器
|
|
|
|
|
// 通用卡片内边距
|
|
|
|
|
:deep(.el-card__body) {
|
|
|
|
|
padding: 16px;
|
|
|
|
|
}
|
|
|
|
|
|