Merge remote-tracking branch 'origin/main'

main
suixy 2 months ago
commit 11c4cf37ab

@ -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,177 +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 }">
<el-tag :type="row.onlineStatus === '1' ? 'success' : 'info'" size="small">
{{ row.onlineStatus === '1' ? '在线' : '离线' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="alarmStatus" label="告警状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.alarmStatus === '1' ? 'danger' : 'success'" size="small">
{{ row.alarmStatus === '1' ? '告警' : '正常' }}
</el-tag>
</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 || row.deviceCode || '-' }}</template>
</el-table-column>
<el-table-column prop="recordTime" label="时间" min-width="140">
<template #default="{ row }">{{ formatRecordTime(row.recordTime) }}</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 }">
<el-tag :type="row.alarmLevel === '2' ? 'danger' : 'warning'" size="small">
{{ row.alarmLevel === '2' ? '严重' : (row.alarmLevel === '1' ? '一般' : '-') }}
</el-tag>
<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 { getDashboardStats } from "@/api/rfid/dashboard";
import { getRfidDeviceList } from "@/api/rfid/rfidDevice";
import { getRfidReadRecordList } from "@/api/rfid/rfidReadRecord";
import type { RfidDeviceVO } from "@/api/rfid/rfidDevice/types";
import type { RfidReadRecordVO } from "@/api/rfid/rfidReadRecord/types";
import { onMounted, ref, getCurrentInstance, toRefs, type ComponentInternalInstance, onUnmounted } from 'vue';
import { getHomeStats, type HomeStatsVO } from "@/api/rfid/statistics";
const stats = ref({
totalDevices: 0,
onlineDevices: 0,
offlineDevices: 0,
alarmDevices: 0
});
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { location_type } = toRefs<any>(proxy?.useDict("location_type"));
const deviceList = ref<RfidDeviceVO[]>([]);
const alarmList = ref<RfidReadRecordVO[]>([]);
const stats = ref<Partial<HomeStatsVO>>({});
const currentTime = ref('');
let timer: number | null = null;
const loadStats = async () => {
try {
const res = await getDashboardStats();
if (res.data) {
stats.value = res.data;
}
} catch (e) {
console.error("获取统计数据失败", e);
}
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 loadDeviceList = async () => {
const loadData = async () => {
try {
const res: any = await getRfidDeviceList({ isMarked: "1" } as any);
deviceList.value = res.data || [];
const res = await getHomeStats();
stats.value = res.data;
} catch (e) {
console.error("获取设备列表失败", e);
console.error("获取首页数据失败", e);
}
};
const loadAlarmList = async () => {
try {
const res: any = await getRfidReadRecordList({ alarmFlag: "1" } as any);
alarmList.value = res.data || [];
} catch (e) {
console.error("获取告警记录失败", e);
}
};
const formatRecordTime = (val: any) => {
if (!val) {
return "-";
}
return String(val).replace("T", " ");
};
onMounted(() => {
loadStats();
loadDeviceList();
loadAlarmList();
updateTime();
timer = window.setInterval(updateTime, 1000);
loadData();
});
onUnmounted(() => {
if (timer) clearInterval(timer);
});
</script>
@ -183,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;
}
}
@ -238,7 +245,6 @@ onMounted(() => {
display: flex;
justify-content: space-between;
align-items: center;
span {
font-size: 16px;
font-weight: 600;
@ -246,7 +252,7 @@ onMounted(() => {
}
}
//
//
:deep(.el-card__body) {
padding: 16px;
}

@ -26,7 +26,7 @@
<el-date-picker
v-model="dateRange"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
type="datetimerange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
@ -96,7 +96,7 @@
<el-table-column label="条码信息" align="center" prop="barcode" v-if="columns[3].visible" />
<el-table-column label="记录时间" align="center" prop="recordTime" width="180" v-if="columns[4].visible">
<template #default="scope">
<span>{{ parseTime(scope.row.recordTime, '{y}-{m}-{d}') }}</span>
<span>{{ parseTime(scope.row.recordTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
<el-table-column label="是否告警" align="center" prop="alarmFlag" v-if="columns[5].visible">

Loading…
Cancel
Save