feat(index): 重构首页视图及新增系统使用指南组件

- 新增欢迎区域显示当前时间,强化用户欢迎体验
- 替换统计卡片为快捷入口卡片,支持快速导航至设备管理、位置管理等模块
- 增加位置统计卡片,展示位置总数及车间、工序、工位数量
- 新增车间概览表格,展示顶级位置以及其编号、名称、类型和子位置数
- 替换和优化首页数据获取接口,统一调用新的getHomeStats接口
- 移除旧的设备列表和告警记录,整体界面布局调整更清晰
- 添加系统使用指南折叠面板,帮助用户快速了解各模块功能与操作流程
- 实现首页当前时间实时刷新功能,提升动态交互体验
- 调整样式,优化卡片、图标和表格的视觉效果与响应交互
main
zangch@mesnac.com 3 weeks ago
parent 898b9de165
commit e827489726

@ -1,20 +1,6 @@
## 平台简介
- 本仓库为前端技术栈 [Vue3](https://v3.cn.vuejs.org) + [TS](https://www.typescriptlang.org/) + [Element Plus](https://element-plus.org/zh-CN) + [Vite](https://cn.vitejs.dev) 版本。
- 成员项目: 基于 vben5(ant-design-vue) 的前端项目 [ruoyi-plus-vben5](https://gitee.com/dapppp/ruoyi-plus-vben5)
- 成员项目: 基于soybean 的前端项目 [ruoyi-plus-soybean](https://gitee.com/xlsea/ruoyi-plus-soybean)
## 配套后端代码仓库地址
| 介绍 | 项目名 | 项目地址 |
|------------|:-----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 🔥 分布式集群框架 | RuoYi-Vue-Plus | - [Gitee](https://gitee.com/dromara/RuoYi-Vue-Plus)<br> - [GitHub](https://github.com/dromara/RuoYi-Vue-Plus)<br> - [GitCode](https://gitcode.com/dromara/RuoYi-Vue-Plus) |
| 🔥 微服务框架 | RuoYi-Cloud-Plus | - [Gitee](https://gitee.com/dromara/RuoYi-Cloud-Plus)<br>- [GitHub](https://github.com/dromara/RuoYi-Cloud-Plus)<br> - [GitCode](https://gitcode.com/dromara/RuoYi-Cloud-Plus) |
## 分支说明
- ts分支(稳定发布主分支 生产可用)
- dev分支(开发分支 开发过程中使用)
## 前端运行

@ -0,0 +1,34 @@
import request from '@/utils/request';
/**
*
*/
export interface HomeStatsVO {
locationStats: LocationStats;
locations: LocationItem[];
}
export interface LocationStats {
totalLocations: number;
workshopCount: number;
processCount: number;
stationCount: number;
}
export interface LocationItem {
id: number;
locationCode: string;
locationAlias: string;
locationType: string;
childCount: number;
}
/**
*
*/
export function getHomeStats() {
return request({
url: '/rfid/statistics/home',
method: 'get'
});
}

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

Loading…
Cancel
Save