feat: 移动式检修车间系统前端完成
- 完成系统日志页面,优化表格滚动和样式 - 完成报警记录页面,优化表格滚动和报警级别显示 - 完成环境参数页面,优化参数显示和监控画面 - 完成参数记录页面,优化图表样式和简洁设计 - 集成MQTT配置,支持实时数据对接 - 统一UI设计风格,采用现代化卡片式布局 - 添加响应式设计,适配不同屏幕尺寸 - 预留MQTT数据接口,支持AC空调和WSD温湿度设备
This commit is contained in:
21
.gitignore
vendored
Normal file
21
.gitignore
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
115
MQTT对接说明.md
Normal file
115
MQTT对接说明.md
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
# MQTT对接说明
|
||||||
|
|
||||||
|
## 📋 项目信息
|
||||||
|
- **项目地址**: http://101.43.41.9:13000/xzzn/movecheck.git
|
||||||
|
- **EMQX控制台**: http://122.51.194.184:18083
|
||||||
|
- **账号**: admin
|
||||||
|
- **密码**: 8a7c97e5c31c
|
||||||
|
- **MQTT服务器**: 122.51.194.184:1883 (暂时不设置账号密码)
|
||||||
|
|
||||||
|
## 🔧 MQTT配置
|
||||||
|
|
||||||
|
### 服务器信息
|
||||||
|
- **MQTT Broker**: `ws://122.51.194.184:8083/mqtt` (WebSocket)
|
||||||
|
- **标准MQTT**: `mqtt://122.51.194.184:1883`
|
||||||
|
- **订阅主题**: `hdydcj_01_UP`
|
||||||
|
|
||||||
|
### 数据格式
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"Device": "AC", // 设备类型: AC=空调, WSD=温湿度
|
||||||
|
"timestamp": 1758699927, // Unix时间戳
|
||||||
|
"Data": {
|
||||||
|
"BSQWD": 28.9 // 空调温度数据
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 设备类型说明
|
||||||
|
- **AC (空调)**:
|
||||||
|
- `BSQWD`: 温度值
|
||||||
|
- **WSD (温湿度传感器)**:
|
||||||
|
- `WD`: 温度值
|
||||||
|
- `SD`: 湿度值
|
||||||
|
|
||||||
|
## 🚀 使用方法
|
||||||
|
|
||||||
|
### 1. 安装依赖
|
||||||
|
```bash
|
||||||
|
npm install mqtt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 启动项目
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 查看连接状态
|
||||||
|
- 打开浏览器开发者工具
|
||||||
|
- 查看Console日志
|
||||||
|
- 应该看到 "MQTT连接成功" 和 "订阅主题成功" 的日志
|
||||||
|
|
||||||
|
### 4. 测试数据接收
|
||||||
|
- 当有设备数据发送到 `hdydcj_01_UP` 主题时
|
||||||
|
- 页面会自动更新温度、湿度等参数
|
||||||
|
- 控制台会显示 "收到设备数据" 的日志
|
||||||
|
|
||||||
|
## 📊 数据映射
|
||||||
|
|
||||||
|
### 空调设备 (Device: AC)
|
||||||
|
- `BSQWD` → 温度显示
|
||||||
|
- 温度范围: 15-35°C
|
||||||
|
|
||||||
|
### 温湿度传感器 (Device: WSD)
|
||||||
|
- `WD` → 温度显示
|
||||||
|
- `SD` → 湿度显示
|
||||||
|
- 湿度范围: 0-80%
|
||||||
|
|
||||||
|
### PM2.5传感器 (Device: PM)
|
||||||
|
- `PM25` → 洁净度显示
|
||||||
|
- 洁净度范围: 0-100%
|
||||||
|
|
||||||
|
## 🔍 故障排除
|
||||||
|
|
||||||
|
### 连接失败
|
||||||
|
1. 检查网络连接
|
||||||
|
2. 确认MQTT服务器地址正确
|
||||||
|
3. 检查防火墙设置
|
||||||
|
|
||||||
|
### 数据不更新
|
||||||
|
1. 检查主题名称是否正确
|
||||||
|
2. 确认设备数据格式
|
||||||
|
3. 查看控制台错误日志
|
||||||
|
|
||||||
|
### 状态显示异常
|
||||||
|
1. 检查设备类型代码
|
||||||
|
2. 确认数据字段名称
|
||||||
|
3. 验证数据值范围
|
||||||
|
|
||||||
|
## 📝 开发说明
|
||||||
|
|
||||||
|
### 文件结构
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── config/
|
||||||
|
│ └── mqtt.js # MQTT配置
|
||||||
|
├── utils/
|
||||||
|
│ └── mqttClient.js # MQTT客户端
|
||||||
|
└── components/
|
||||||
|
└── EnvironmentParams.vue # 环境参数组件
|
||||||
|
```
|
||||||
|
|
||||||
|
### 主要功能
|
||||||
|
- 自动连接MQTT服务器
|
||||||
|
- 订阅设备数据主题
|
||||||
|
- 实时更新环境参数
|
||||||
|
- 显示连接状态
|
||||||
|
- 错误处理和重连机制
|
||||||
|
|
||||||
|
### 扩展功能
|
||||||
|
- 可以添加更多设备类型
|
||||||
|
- 支持数据历史记录
|
||||||
|
- 可配置报警阈值
|
||||||
|
- 支持数据导出功能
|
||||||
20
index.html
Normal file
20
index.html
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<script>
|
||||||
|
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
|
||||||
|
CSS.supports('top: constant(a)'))
|
||||||
|
document.write(
|
||||||
|
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
|
||||||
|
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
|
||||||
|
</script>
|
||||||
|
<title></title>
|
||||||
|
<!--preload-links-->
|
||||||
|
<!--app-context-->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"><!--app-html--></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
586
mobile-inspection-system.html
Normal file
586
mobile-inspection-system.html
Normal file
@ -0,0 +1,586 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
|
<title>移动式检修车间系统</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: "Microsoft YaHei", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-container {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 200px;
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
border-right: 1px solid #e0e0e0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px 10px;
|
||||||
|
background-color: #3f51b5;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
margin-right: 10px;
|
||||||
|
background-color: #ff9800;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo i {
|
||||||
|
color: white;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
padding: 15px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
position: relative;
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item.active {
|
||||||
|
background-color: #3f51b5;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:hover:not(.active) {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-alert {
|
||||||
|
background-color: #ffc107;
|
||||||
|
padding: 15px;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-title-icon {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
background-color: #ff9800;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-title-text {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-icon {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameters {
|
||||||
|
flex: 2;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 15px;
|
||||||
|
height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter-label {
|
||||||
|
width: 100px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter-value {
|
||||||
|
flex: 1;
|
||||||
|
height: 15px;
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
border-radius: 10px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter-bar {
|
||||||
|
height: 100%;
|
||||||
|
background-color: #3f51b5;
|
||||||
|
width: 70%;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter-arrows {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 5px;
|
||||||
|
color: #3f51b5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-panel {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-feed {
|
||||||
|
flex: 2;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-feed img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-panel {
|
||||||
|
flex: 1;
|
||||||
|
background-color: #3f51b5;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 15px;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-label {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-value {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-button {
|
||||||
|
align-self: flex-end;
|
||||||
|
padding: 10px 30px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: white;
|
||||||
|
font-size: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-button:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹窗样式 */
|
||||||
|
.modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 1000;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background-color: white;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 10px;
|
||||||
|
width: 60%;
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-button {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-button {
|
||||||
|
background-color: #3f51b5;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 已移除设计说明面板相关样式 */
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.sidebar {
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter-label {
|
||||||
|
width: 80px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app-container">
|
||||||
|
<div class="sidebar">
|
||||||
|
<div class="sidebar-header">
|
||||||
|
<div class="logo">
|
||||||
|
<i class="fas fa-tools"></i>
|
||||||
|
</div>
|
||||||
|
<div class="app-title">检修系统</div>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item active">现场环境参数</div>
|
||||||
|
<div class="menu-item">参数记录</div>
|
||||||
|
<div class="menu-item">视觉监控</div>
|
||||||
|
<div class="menu-item">日志</div>
|
||||||
|
<div class="menu-item">报警</div>
|
||||||
|
<div class="current-alert">当前报警项目</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="main-content" style="display: flex; flex-direction: column;">
|
||||||
|
<div class="header">
|
||||||
|
<div class="system-title">
|
||||||
|
<div class="system-title-icon">
|
||||||
|
<i class="fas fa-clipboard-list" style="font-size: 30px; color: white;"></i>
|
||||||
|
</div>
|
||||||
|
<div class="system-title-text">移动式检修车间系统</div>
|
||||||
|
</div>
|
||||||
|
<div class="camera-icon">
|
||||||
|
<i class="fas fa-video" style="font-size: 30px;"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; flex: 1;">
|
||||||
|
<div class="dashboard" style="flex: 2;">
|
||||||
|
<div class="parameters">
|
||||||
|
<div class="parameter-row">
|
||||||
|
<div class="parameter-label">当前</div>
|
||||||
|
<div class="parameter-value">
|
||||||
|
<div class="parameter-bar" style="width: 70%;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="parameter-row">
|
||||||
|
<div class="parameter-label">温度</div>
|
||||||
|
<div class="parameter-value">
|
||||||
|
<div class="parameter-bar" style="width: 50%;"></div>
|
||||||
|
</div>
|
||||||
|
<div class="parameter-arrows">
|
||||||
|
<i class="fas fa-arrow-left"></i>
|
||||||
|
<i class="fas fa-arrow-right"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="parameter-row">
|
||||||
|
<div class="parameter-label">湿度</div>
|
||||||
|
<div class="parameter-value">
|
||||||
|
<div class="parameter-bar" style="width: 60%;"></div>
|
||||||
|
</div>
|
||||||
|
<div class="parameter-arrows">
|
||||||
|
<i class="fas fa-arrow-left"></i>
|
||||||
|
<i class="fas fa-arrow-right"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="parameter-row">
|
||||||
|
<div class="parameter-label">洁净度</div>
|
||||||
|
<div class="parameter-value">
|
||||||
|
<div class="parameter-bar" style="width: 40%;"></div>
|
||||||
|
</div>
|
||||||
|
<div class="parameter-arrows">
|
||||||
|
<i class="fas fa-arrow-left"></i>
|
||||||
|
<i class="fas fa-arrow-right"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="right-panel">
|
||||||
|
<div class="camera-feed">
|
||||||
|
<img src="https://images.unsplash.com/photo-1581094794329-c8112a89af12?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=80" alt="检修车间">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-panel">
|
||||||
|
<div>
|
||||||
|
<div class="settings-row">
|
||||||
|
<div class="settings-label">温度</div>
|
||||||
|
<div class="settings-value">25°C</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-row">
|
||||||
|
<div class="settings-label">湿度</div>
|
||||||
|
<div class="settings-value">45%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="settings-button" id="openSettings">设定</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="flex: 1; background-color: rgba(255, 255, 255, 0.95); margin-left: 20px; padding: 15px; border-radius: 10px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); font-size: 13px; max-height: 100%; overflow-y: auto;">
|
||||||
|
<h3 style="margin-bottom: 10px; color: #3f51b5; text-align: center; font-size: 16px;">设计说明</h3>
|
||||||
|
<div style="margin-bottom: 15px;">
|
||||||
|
<h4 style="margin-bottom: 5px; color: #3f51b5;">整体布局</h4>
|
||||||
|
<p>采用左侧导航栏 + 右侧主内容区的经典布局,符合平板横屏应用的使用习惯</p>
|
||||||
|
</div>
|
||||||
|
<div style="margin-bottom: 15px;">
|
||||||
|
<h4 style="margin-bottom: 5px; color: #3f51b5;">色彩方案</h4>
|
||||||
|
<p>主色调采用深蓝色(#3f51b5),搭配橙色(#ff9800)作为点缀,体现专业性和科技感</p>
|
||||||
|
</div>
|
||||||
|
<div style="margin-bottom: 15px;">
|
||||||
|
<h4 style="margin-bottom: 5px; color: #3f51b5;">交互功能</h4>
|
||||||
|
<ul style="padding-left: 20px; margin: 5px 0;">
|
||||||
|
<li>点击左侧菜单项可切换不同功能模块</li>
|
||||||
|
<li>点击"设定"按钮可打开参数设置弹窗</li>
|
||||||
|
<li>在设置弹窗中可调整各项参数阈值</li>
|
||||||
|
<li>点击"当前报警项目"可查看报警详情</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div style="margin-bottom: 15px;">
|
||||||
|
<h4 style="margin-bottom: 5px; color: #3f51b5;">设计理念</h4>
|
||||||
|
<ul style="padding-left: 20px; margin: 5px 0;">
|
||||||
|
<li><strong>简洁明了:</strong>界面布局清晰,信息层次分明</li>
|
||||||
|
<li><strong>专业可靠:</strong>色彩和元素设计体现工业应用的专业性</li>
|
||||||
|
<li><strong>易于操作:</strong>按钮和交互元素尺寸适中,便于触控</li>
|
||||||
|
<li><strong>信息聚焦:</strong>重要信息突出显示,帮助用户快速获取关键数据</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 设置弹窗 -->
|
||||||
|
<div class="modal" id="settingsModal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-title">环境参数设置</div>
|
||||||
|
<button class="close-button" id="closeModal">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="setting-group">
|
||||||
|
<label class="setting-label">温度阈值 (°C)</label>
|
||||||
|
<input type="number" class="setting-input" value="25" min="10" max="40">
|
||||||
|
</div>
|
||||||
|
<div class="setting-group">
|
||||||
|
<label class="setting-label">湿度阈值 (%)</label>
|
||||||
|
<input type="number" class="setting-input" value="45" min="20" max="80">
|
||||||
|
</div>
|
||||||
|
<div class="setting-group">
|
||||||
|
<label class="setting-label">洁净度阈值</label>
|
||||||
|
<input type="number" class="setting-input" value="100" min="0" max="200">
|
||||||
|
</div>
|
||||||
|
<div class="setting-group">
|
||||||
|
<label class="setting-label">报警启用</label>
|
||||||
|
<select class="setting-input">
|
||||||
|
<option value="all">全部启用</option>
|
||||||
|
<option value="temp">仅温度</option>
|
||||||
|
<option value="humidity">仅湿度</option>
|
||||||
|
<option value="cleanliness">仅洁净度</option>
|
||||||
|
<option value="none">全部禁用</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="modal-button cancel-button" id="cancelSettings">取消</button>
|
||||||
|
<button class="modal-button save-button" id="saveSettings">保存</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 已移除浮动设计说明,直接集成到主界面 -->
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 设置弹窗交互
|
||||||
|
const settingsModal = document.getElementById('settingsModal');
|
||||||
|
const openSettings = document.getElementById('openSettings');
|
||||||
|
const closeModal = document.getElementById('closeModal');
|
||||||
|
const cancelSettings = document.getElementById('cancelSettings');
|
||||||
|
const saveSettings = document.getElementById('saveSettings');
|
||||||
|
|
||||||
|
openSettings.addEventListener('click', () => {
|
||||||
|
settingsModal.style.display = 'flex';
|
||||||
|
});
|
||||||
|
|
||||||
|
closeModal.addEventListener('click', () => {
|
||||||
|
settingsModal.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
cancelSettings.addEventListener('click', () => {
|
||||||
|
settingsModal.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
saveSettings.addEventListener('click', () => {
|
||||||
|
// 这里可以添加保存设置的逻辑
|
||||||
|
settingsModal.style.display = 'none';
|
||||||
|
alert('设置已保存');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 点击其他地方关闭弹窗
|
||||||
|
window.addEventListener('click', (event) => {
|
||||||
|
if (event.target === settingsModal) {
|
||||||
|
settingsModal.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 菜单交互
|
||||||
|
const menuItems = document.querySelectorAll('.menu-item');
|
||||||
|
menuItems.forEach(item => {
|
||||||
|
item.addEventListener('click', () => {
|
||||||
|
menuItems.forEach(i => i.classList.remove('active'));
|
||||||
|
item.classList.add('active');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 已移除设计说明交互代码
|
||||||
|
|
||||||
|
// 模拟参数变化
|
||||||
|
setInterval(() => {
|
||||||
|
const bars = document.querySelectorAll('.parameter-bar');
|
||||||
|
bars.forEach(bar => {
|
||||||
|
const currentWidth = parseInt(bar.style.width);
|
||||||
|
const change = Math.random() > 0.5 ? 2 : -2;
|
||||||
|
let newWidth = currentWidth + change;
|
||||||
|
if (newWidth < 30) newWidth = 30;
|
||||||
|
if (newWidth > 90) newWidth = 90;
|
||||||
|
bar.style.width = newWidth + '%';
|
||||||
|
});
|
||||||
|
}, 3000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
11439
package-lock.json
generated
Normal file
11439
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
69
package.json
Normal file
69
package.json
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
{
|
||||||
|
"name": "uni-preset-vue",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"dev:custom": "uni -p",
|
||||||
|
"dev:h5": "uni",
|
||||||
|
"dev:h5:ssr": "uni --ssr",
|
||||||
|
"dev:mp-alipay": "uni -p mp-alipay",
|
||||||
|
"dev:mp-baidu": "uni -p mp-baidu",
|
||||||
|
"dev:mp-jd": "uni -p mp-jd",
|
||||||
|
"dev:mp-kuaishou": "uni -p mp-kuaishou",
|
||||||
|
"dev:mp-lark": "uni -p mp-lark",
|
||||||
|
"dev:mp-qq": "uni -p mp-qq",
|
||||||
|
"dev:mp-toutiao": "uni -p mp-toutiao",
|
||||||
|
"dev:mp-harmony": "uni -p mp-harmony",
|
||||||
|
"dev:mp-weixin": "uni -p mp-weixin",
|
||||||
|
"dev:mp-xhs": "uni -p mp-xhs",
|
||||||
|
"dev:quickapp-webview": "uni -p quickapp-webview",
|
||||||
|
"dev:quickapp-webview-huawei": "uni -p quickapp-webview-huawei",
|
||||||
|
"dev:quickapp-webview-union": "uni -p quickapp-webview-union",
|
||||||
|
"build:custom": "uni build -p",
|
||||||
|
"build:h5": "uni build",
|
||||||
|
"build:h5:ssr": "uni build --ssr",
|
||||||
|
"build:mp-alipay": "uni build -p mp-alipay",
|
||||||
|
"build:mp-baidu": "uni build -p mp-baidu",
|
||||||
|
"build:mp-jd": "uni build -p mp-jd",
|
||||||
|
"build:mp-kuaishou": "uni build -p mp-kuaishou",
|
||||||
|
"build:mp-lark": "uni build -p mp-lark",
|
||||||
|
"build:mp-qq": "uni build -p mp-qq",
|
||||||
|
"build:mp-toutiao": "uni build -p mp-toutiao",
|
||||||
|
"build:mp-harmony": "uni build -p mp-harmony",
|
||||||
|
"build:mp-weixin": "uni build -p mp-weixin",
|
||||||
|
"build:mp-xhs": "uni build -p mp-xhs",
|
||||||
|
"build:quickapp-webview": "uni build -p quickapp-webview",
|
||||||
|
"build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei",
|
||||||
|
"build:quickapp-webview-union": "uni build -p quickapp-webview-union"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@dcloudio/uni-app": "3.0.0-4070620250821001",
|
||||||
|
"@dcloudio/uni-app-harmony": "3.0.0-4070620250821001",
|
||||||
|
"@dcloudio/uni-app-plus": "3.0.0-4070620250821001",
|
||||||
|
"@dcloudio/uni-components": "3.0.0-4070620250821001",
|
||||||
|
"@dcloudio/uni-h5": "3.0.0-4070620250821001",
|
||||||
|
"@dcloudio/uni-mp-alipay": "3.0.0-4070620250821001",
|
||||||
|
"@dcloudio/uni-mp-baidu": "3.0.0-4070620250821001",
|
||||||
|
"@dcloudio/uni-mp-harmony": "3.0.0-4070620250821001",
|
||||||
|
"@dcloudio/uni-mp-jd": "3.0.0-4070620250821001",
|
||||||
|
"@dcloudio/uni-mp-kuaishou": "3.0.0-4070620250821001",
|
||||||
|
"@dcloudio/uni-mp-lark": "3.0.0-4070620250821001",
|
||||||
|
"@dcloudio/uni-mp-qq": "3.0.0-4070620250821001",
|
||||||
|
"@dcloudio/uni-mp-toutiao": "3.0.0-4070620250821001",
|
||||||
|
"@dcloudio/uni-mp-weixin": "3.0.0-4070620250821001",
|
||||||
|
"@dcloudio/uni-mp-xhs": "3.0.0-4070620250821001",
|
||||||
|
"@dcloudio/uni-quickapp-webview": "3.0.0-4070620250821001",
|
||||||
|
"@dcloudio/uni-ui": "^1.4.28",
|
||||||
|
"vue": "^3.4.21",
|
||||||
|
"vue-i18n": "^9.1.9"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@dcloudio/types": "^3.4.8",
|
||||||
|
"@dcloudio/uni-automator": "3.0.0-4070620250821001",
|
||||||
|
"@dcloudio/uni-cli-shared": "3.0.0-4070620250821001",
|
||||||
|
"@dcloudio/uni-stacktracey": "3.0.0-4070620250821001",
|
||||||
|
"@dcloudio/vite-plugin-uni": "3.0.0-4070620250821001",
|
||||||
|
"@vue/runtime-core": "^3.4.21",
|
||||||
|
"sass": "^1.93.0",
|
||||||
|
"vite": "5.2.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
shims-uni.d.ts
vendored
Normal file
10
shims-uni.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
/// <reference types='@dcloudio/types' />
|
||||||
|
import 'vue'
|
||||||
|
|
||||||
|
declare module '@vue/runtime-core' {
|
||||||
|
type Hooks = App.AppInstance & Page.PageInstance;
|
||||||
|
|
||||||
|
interface ComponentCustomOptions extends Hooks {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
73
src/App.vue
Normal file
73
src/App.vue
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
onLaunch: function () {
|
||||||
|
console.log('App Launch')
|
||||||
|
// 强制设置横屏
|
||||||
|
this.setOrientation()
|
||||||
|
},
|
||||||
|
onShow: function () {
|
||||||
|
console.log('App Show')
|
||||||
|
// 每次显示时确保横屏
|
||||||
|
this.setOrientation()
|
||||||
|
},
|
||||||
|
onHide: function () {
|
||||||
|
console.log('App Hide')
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setOrientation() {
|
||||||
|
// 设置屏幕方向为横屏
|
||||||
|
try {
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
plus.screen.lockOrientation('landscape-primary')
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifdef H5
|
||||||
|
// H5环境下的横屏设置
|
||||||
|
if (screen.orientation && screen.orientation.lock) {
|
||||||
|
screen.orientation.lock('landscape').catch(err => {
|
||||||
|
console.log('横屏锁定失败:', err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
} catch (error) {
|
||||||
|
console.log('设置横屏失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '@/styles/common.scss';
|
||||||
|
|
||||||
|
/*每个页面公共css */
|
||||||
|
page {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica,
|
||||||
|
Segoe UI, Arial, Roboto, 'PingFang SC', 'miui', 'Hiragino Sans GB', 'Microsoft Yahei',
|
||||||
|
sans-serif;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保根元素和页面容器都是100%高度 */
|
||||||
|
#app {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 修复uni-app中的一些默认样式 */
|
||||||
|
button {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
button::after {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹窗样式修复 */
|
||||||
|
.uni-popup .uni-popup__wrapper {
|
||||||
|
border-radius: 10rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
841
src/components/AlarmRecord.vue
Normal file
841
src/components/AlarmRecord.vue
Normal file
@ -0,0 +1,841 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page-container alarm-record-container">
|
||||||
|
<!-- 页面头部 -->
|
||||||
|
<view class="page-header">
|
||||||
|
<view class="header-left">
|
||||||
|
<text class="page-title">报警记录</text>
|
||||||
|
</view>
|
||||||
|
<view class="header-right">
|
||||||
|
<view class="system-title">
|
||||||
|
<view class="system-title-icon">
|
||||||
|
<text class="icon">📋</text>
|
||||||
|
</view>
|
||||||
|
<text class="system-title-text">移动式检修车间系统</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 报警表格 -->
|
||||||
|
<view class="page-content">
|
||||||
|
<view class="alarm-content">
|
||||||
|
<view class="alarm-table">
|
||||||
|
<!-- 表格头部 -->
|
||||||
|
<view class="table-header">
|
||||||
|
<view class="table-cell header-cell content-column">内容</view>
|
||||||
|
<view class="table-cell header-cell type-column">种类</view>
|
||||||
|
<view class="table-cell header-cell time-column">时间</view>
|
||||||
|
<view class="table-cell header-cell level-column">级别</view>
|
||||||
|
<view class="table-cell header-cell action-column">处置</view>
|
||||||
|
<view class="table-cell header-cell action-time-column">时间</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 表格内容 -->
|
||||||
|
<scroll-view
|
||||||
|
class="table-body"
|
||||||
|
scroll-y="true"
|
||||||
|
:scroll-with-animation="true"
|
||||||
|
:scroll-top="scrollTop"
|
||||||
|
@scrolltolower="onScrollToLower"
|
||||||
|
@scroll="onScroll"
|
||||||
|
>
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<view class="table-loading-container" v-if="isLoading">
|
||||||
|
<view class="table-loading-spinner"></view>
|
||||||
|
<text class="table-loading-text">正在加载报警记录...</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 表格数据 -->
|
||||||
|
<template v-else>
|
||||||
|
<view
|
||||||
|
v-for="(alarm, index) in alarmList"
|
||||||
|
:key="index"
|
||||||
|
class="table-row"
|
||||||
|
:class="{ 'even-row': index % 2 === 0 }"
|
||||||
|
>
|
||||||
|
<view class="table-cell content-column">{{ alarm.content }}</view>
|
||||||
|
<view class="table-cell type-column">{{ alarm.type }}</view>
|
||||||
|
<view class="table-cell time-column">{{ alarm.time }}</view>
|
||||||
|
<view class="table-cell level-column" :class="getLevelClass(alarm.level)">
|
||||||
|
{{ alarm.level }}
|
||||||
|
</view>
|
||||||
|
<view class="table-cell action-column">{{ alarm.action }}</view>
|
||||||
|
<view class="table-cell action-time-column">{{ alarm.actionTime }}</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 空数据提示 -->
|
||||||
|
<view class="table-empty-container" v-if="!isLoading && alarmList.length === 0 && hasInitialized">
|
||||||
|
<text class="table-empty-text">暂无报警记录</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 初始状态提示 -->
|
||||||
|
<view class="table-empty-container" v-if="!isLoading && alarmList.length === 0 && !hasInitialized">
|
||||||
|
<text class="table-empty-text">暂无数据</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 底部间距,确保最后一条记录完全显示 -->
|
||||||
|
<view class="table-bottom-spacing"></view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue';
|
||||||
|
|
||||||
|
// 报警数据
|
||||||
|
const alarmList = ref([]);
|
||||||
|
const isLoading = ref(false);
|
||||||
|
const isConnected = ref(false);
|
||||||
|
const hasInitialized = ref(false);
|
||||||
|
|
||||||
|
// 滚动相关
|
||||||
|
const scrollTop = ref(0);
|
||||||
|
const isScrolling = ref(false);
|
||||||
|
|
||||||
|
// 移除空行占位逻辑,没有数据时只显示"暂无数据"
|
||||||
|
|
||||||
|
// MQTT报警服务接口(预留)
|
||||||
|
const mqttAlarmService = {
|
||||||
|
// 连接MQTT服务器
|
||||||
|
connect: async () => {
|
||||||
|
console.log('MQTT报警服务连接中...');
|
||||||
|
try {
|
||||||
|
// 模拟连接延迟
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
isConnected.value = true;
|
||||||
|
console.log('MQTT报警服务连接成功');
|
||||||
|
return Promise.resolve();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('MQTT报警连接失败:', error);
|
||||||
|
isConnected.value = false;
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 订阅报警数据
|
||||||
|
subscribeAlarmData: () => {
|
||||||
|
console.log('订阅系统报警数据');
|
||||||
|
// 这里后期会实现真实的MQTT报警订阅
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取历史报警记录
|
||||||
|
getHistoryAlarms: async (limit = 50) => {
|
||||||
|
console.log(`获取历史报警记录,限制${limit}条`);
|
||||||
|
try {
|
||||||
|
isLoading.value = true;
|
||||||
|
hasInitialized.value = true;
|
||||||
|
// 模拟请求延迟
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 800));
|
||||||
|
|
||||||
|
// 模拟报警数据
|
||||||
|
const mockAlarms = [
|
||||||
|
{
|
||||||
|
content: '湿度45%超标',
|
||||||
|
type: '参数超标',
|
||||||
|
time: '2025-9-3-12:01',
|
||||||
|
level: 'A',
|
||||||
|
action: '恢复',
|
||||||
|
actionTime: '2025-9-3-13:11'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: '温度28℃过高',
|
||||||
|
type: '参数超标',
|
||||||
|
time: '2025-9-3-11:45',
|
||||||
|
level: 'B',
|
||||||
|
action: '调整',
|
||||||
|
actionTime: '2025-9-3-12:30'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: '洁净度异常',
|
||||||
|
type: '环境异常',
|
||||||
|
time: '2025-9-3-11:20',
|
||||||
|
level: 'A',
|
||||||
|
action: '清理',
|
||||||
|
actionTime: '2025-9-3-11:50'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: '设备通讯中断',
|
||||||
|
type: '设备故障',
|
||||||
|
time: '2025-9-3-10:55',
|
||||||
|
level: 'C',
|
||||||
|
action: '重启',
|
||||||
|
actionTime: '2025-9-3-11:05'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: '压力值偏低',
|
||||||
|
type: '参数异常',
|
||||||
|
time: '2025-9-3-10:30',
|
||||||
|
level: 'B',
|
||||||
|
action: '检查',
|
||||||
|
actionTime: '2025-9-3-10:45'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: '电源电压不稳',
|
||||||
|
type: '电气故障',
|
||||||
|
time: '2025-9-3-10:10',
|
||||||
|
level: 'A',
|
||||||
|
action: '更换',
|
||||||
|
actionTime: '2025-9-3-10:25'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: '传感器数据异常',
|
||||||
|
type: '设备异常',
|
||||||
|
time: '2025-9-3-09:50',
|
||||||
|
level: 'B',
|
||||||
|
action: '校准',
|
||||||
|
actionTime: '2025-9-3-10:00'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: '网络连接超时',
|
||||||
|
type: '通讯故障',
|
||||||
|
time: '2025-9-3-09:30',
|
||||||
|
level: 'C',
|
||||||
|
action: '重连',
|
||||||
|
actionTime: '2025-9-3-09:35'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: '内存使用率过高',
|
||||||
|
type: '系统异常',
|
||||||
|
time: '2025-9-3-09:15',
|
||||||
|
level: 'B',
|
||||||
|
action: '清理',
|
||||||
|
actionTime: '2025-9-3-09:20'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: '磁盘空间不足',
|
||||||
|
type: '存储异常',
|
||||||
|
time: '2025-9-3-09:00',
|
||||||
|
level: 'A',
|
||||||
|
action: '扩容',
|
||||||
|
actionTime: '2025-9-3-09:10'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: 'CPU温度过高',
|
||||||
|
type: '硬件故障',
|
||||||
|
time: '2025-9-3-08:45',
|
||||||
|
level: 'A',
|
||||||
|
action: '散热',
|
||||||
|
actionTime: '2025-9-3-08:50'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: '数据库连接失败',
|
||||||
|
type: '数据异常',
|
||||||
|
time: '2025-9-3-08:30',
|
||||||
|
level: 'B',
|
||||||
|
action: '修复',
|
||||||
|
actionTime: '2025-9-3-08:35'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: '配置文件损坏',
|
||||||
|
type: '配置异常',
|
||||||
|
time: '2025-9-3-08:15',
|
||||||
|
level: 'C',
|
||||||
|
action: '恢复',
|
||||||
|
actionTime: '2025-9-3-08:20'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: '服务进程异常',
|
||||||
|
type: '进程故障',
|
||||||
|
time: '2025-9-3-08:00',
|
||||||
|
level: 'B',
|
||||||
|
action: '重启',
|
||||||
|
actionTime: '2025-9-3-08:05'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: '日志文件过大',
|
||||||
|
type: '存储异常',
|
||||||
|
time: '2025-9-3-07:45',
|
||||||
|
level: 'C',
|
||||||
|
action: '压缩',
|
||||||
|
actionTime: '2025-9-3-07:50'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
alarmList.value = mockAlarms;
|
||||||
|
isLoading.value = false;
|
||||||
|
return Promise.resolve(mockAlarms);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取历史报警记录失败:', error);
|
||||||
|
isLoading.value = false;
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取实时报警
|
||||||
|
getRealtimeAlarms: async () => {
|
||||||
|
console.log('获取实时报警');
|
||||||
|
try {
|
||||||
|
// 模拟实时报警数据
|
||||||
|
const contents = ['温度超标', '湿度异常', '压力偏高', '洁净度超标', '设备故障', '通讯中断'];
|
||||||
|
const types = ['参数超标', '环境异常', '设备故障', '电气故障', '通讯故障'];
|
||||||
|
const levels = ['A', 'B', 'C'];
|
||||||
|
const actions = ['处理中', '已恢复', '待处理', '检查中'];
|
||||||
|
|
||||||
|
const newAlarm = {
|
||||||
|
content: contents[Math.floor(Math.random() * contents.length)],
|
||||||
|
type: types[Math.floor(Math.random() * types.length)],
|
||||||
|
time: new Date().toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'numeric',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
}).replace(/\//g, '-').replace(', ', '-'),
|
||||||
|
level: levels[Math.floor(Math.random() * levels.length)],
|
||||||
|
action: actions[Math.floor(Math.random() * actions.length)],
|
||||||
|
actionTime: new Date().toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'numeric',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
}).replace(/\//g, '-').replace(', ', '-')
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加到报警列表顶部
|
||||||
|
alarmList.value.unshift(newAlarm);
|
||||||
|
|
||||||
|
// 限制报警数量,保持最新的50条
|
||||||
|
if (alarmList.value.length > 50) {
|
||||||
|
alarmList.value = alarmList.value.slice(0, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动滚动到顶部显示最新报警
|
||||||
|
setTimeout(() => {
|
||||||
|
scrollToTop();
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
return Promise.resolve(newAlarm);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取实时报警失败:', error);
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 确认报警
|
||||||
|
confirmAlarm: async (alarmId) => {
|
||||||
|
console.log('确认报警:', alarmId);
|
||||||
|
try {
|
||||||
|
// 模拟确认操作
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 300));
|
||||||
|
uni.showToast({
|
||||||
|
title: '报警已确认',
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
|
return Promise.resolve();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('确认报警失败:', error);
|
||||||
|
uni.showToast({
|
||||||
|
title: '确认失败',
|
||||||
|
icon: 'error'
|
||||||
|
});
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 清空报警记录
|
||||||
|
clearAlarms: async () => {
|
||||||
|
console.log('清空报警记录');
|
||||||
|
try {
|
||||||
|
// 模拟清空操作
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 300));
|
||||||
|
alarmList.value = [];
|
||||||
|
uni.showToast({
|
||||||
|
title: '报警记录已清空',
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
|
return Promise.resolve();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('清空报警记录失败:', error);
|
||||||
|
uni.showToast({
|
||||||
|
title: '清空失败',
|
||||||
|
icon: 'error'
|
||||||
|
});
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 断开连接
|
||||||
|
disconnect: () => {
|
||||||
|
console.log('MQTT报警服务断开连接');
|
||||||
|
isConnected.value = false;
|
||||||
|
// 这里后期会实现真实的MQTT断开
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取级别样式类
|
||||||
|
const getLevelClass = (level) => {
|
||||||
|
switch (level) {
|
||||||
|
case 'A':
|
||||||
|
return 'level-a';
|
||||||
|
case 'B':
|
||||||
|
return 'level-b';
|
||||||
|
case 'C':
|
||||||
|
return 'level-c';
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 滚动事件处理
|
||||||
|
let scrollTimer = null;
|
||||||
|
|
||||||
|
const onScroll = (e) => {
|
||||||
|
isScrolling.value = true;
|
||||||
|
|
||||||
|
// 防抖处理,避免频繁触发
|
||||||
|
if (scrollTimer) {
|
||||||
|
clearTimeout(scrollTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollTimer = setTimeout(() => {
|
||||||
|
isScrolling.value = false;
|
||||||
|
}, 150);
|
||||||
|
|
||||||
|
// 可以在这里添加滚动时的逻辑
|
||||||
|
};
|
||||||
|
|
||||||
|
const onScrollToLower = () => {
|
||||||
|
console.log('滚动到底部');
|
||||||
|
// 可以在这里添加加载更多数据的逻辑
|
||||||
|
};
|
||||||
|
|
||||||
|
// 滚动到顶部
|
||||||
|
const scrollToTop = () => {
|
||||||
|
scrollTop.value = scrollTop.value === 0 ? 1 : 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 滚动到底部
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
// 使用nextTick确保DOM更新完成
|
||||||
|
nextTick(() => {
|
||||||
|
// 计算滚动到底部的位置
|
||||||
|
const scrollHeight = alarmList.value.length * 80; // 假设每行80rpx
|
||||||
|
scrollTop.value = scrollHeight + 100; // 额外100rpx确保完全滚动到底部
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 定时获取实时报警
|
||||||
|
let realtimeTimer = null;
|
||||||
|
|
||||||
|
const startRealtimeAlarm = () => {
|
||||||
|
if (realtimeTimer) return;
|
||||||
|
|
||||||
|
realtimeTimer = setInterval(() => {
|
||||||
|
if (isConnected.value && !isLoading.value) {
|
||||||
|
// 20%概率生成新报警
|
||||||
|
if (Math.random() < 0.2) {
|
||||||
|
mqttAlarmService.getRealtimeAlarms();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 8000); // 每8秒检查一次
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopRealtimeAlarm = () => {
|
||||||
|
if (realtimeTimer) {
|
||||||
|
clearInterval(realtimeTimer);
|
||||||
|
realtimeTimer = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 组件生命周期
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
// 连接MQTT并初始化
|
||||||
|
await mqttAlarmService.connect();
|
||||||
|
await mqttAlarmService.subscribeAlarmData();
|
||||||
|
await mqttAlarmService.getHistoryAlarms();
|
||||||
|
|
||||||
|
// 开始实时报警获取
|
||||||
|
startRealtimeAlarm();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('报警系统初始化失败:', error);
|
||||||
|
uni.showToast({
|
||||||
|
title: '连接失败',
|
||||||
|
icon: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopRealtimeAlarm();
|
||||||
|
mqttAlarmService.disconnect();
|
||||||
|
|
||||||
|
// 清理滚动定时器
|
||||||
|
if (scrollTimer) {
|
||||||
|
clearTimeout(scrollTimer);
|
||||||
|
scrollTimer = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '@/styles/common.scss';
|
||||||
|
|
||||||
|
.alarm-record-container {
|
||||||
|
// 继承通用页面容器样式
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alarm-content {
|
||||||
|
// 继承通用内容样式
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alarm-table {
|
||||||
|
// 继承通用表格样式
|
||||||
|
@extend .common-table;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-header {
|
||||||
|
display: flex;
|
||||||
|
background: #f8f9fa;
|
||||||
|
color: #3c4043;
|
||||||
|
border-bottom: 1rpx solid #e8eaed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-cell {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 26rpx;
|
||||||
|
padding: 20rpx 16rpx;
|
||||||
|
text-align: center;
|
||||||
|
letter-spacing: 0.2rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-body {
|
||||||
|
flex: 1;
|
||||||
|
height: 0; /* 重要:配合flex: 1使用,确保正确计算高度 */
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
/* 滚动条样式 */
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #dadce0 #f1f3f4;
|
||||||
|
/* 平滑滚动 */
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Webkit浏览器滚动条样式 */
|
||||||
|
.table-body::-webkit-scrollbar {
|
||||||
|
width: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-body::-webkit-scrollbar-track {
|
||||||
|
background: rgba(241, 243, 244, 0.5);
|
||||||
|
border-radius: 4rpx;
|
||||||
|
margin: 4rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-body::-webkit-scrollbar-thumb {
|
||||||
|
background: linear-gradient(180deg, #dadce0 0%, #bdc1c6 100%);
|
||||||
|
border-radius: 4rpx;
|
||||||
|
border: 1rpx solid rgba(255, 255, 255, 0.2);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-body::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: linear-gradient(180deg, #bdc1c6 0%, #9aa0a6 100%);
|
||||||
|
box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-body::-webkit-scrollbar-thumb:active {
|
||||||
|
background: linear-gradient(180deg, #9aa0a6 0%, #5f6368 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row {
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 1rpx solid #e8eaed;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
min-height: 80rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row.even-row {
|
||||||
|
background-color: rgba(248, 249, 250, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row:hover {
|
||||||
|
background-color: rgba(241, 243, 244, 0.8);
|
||||||
|
transform: translateX(2rpx);
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-cell {
|
||||||
|
padding: 16rpx 16rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #3c4043;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
min-height: 80rpx;
|
||||||
|
line-height: 1.4;
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-all;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-column {
|
||||||
|
flex: 3;
|
||||||
|
justify-content: flex-start;
|
||||||
|
text-align: left;
|
||||||
|
padding-left: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-column {
|
||||||
|
flex: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-column {
|
||||||
|
flex: 2.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-column {
|
||||||
|
flex: 1;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-column {
|
||||||
|
flex: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-time-column {
|
||||||
|
flex: 2.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-a {
|
||||||
|
color: #ea4335;
|
||||||
|
font-weight: 700;
|
||||||
|
position: relative;
|
||||||
|
background: linear-gradient(135deg, rgba(234, 67, 53, 0.1) 0%, rgba(234, 67, 53, 0.05) 100%);
|
||||||
|
border-radius: 8rpx;
|
||||||
|
padding: 8rpx 12rpx;
|
||||||
|
border: 1rpx solid rgba(234, 67, 53, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-a::before {
|
||||||
|
content: '🔴';
|
||||||
|
margin-right: 6rpx;
|
||||||
|
font-size: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-b {
|
||||||
|
color: #fbbc04;
|
||||||
|
font-weight: 700;
|
||||||
|
position: relative;
|
||||||
|
background: linear-gradient(135deg, rgba(251, 188, 4, 0.1) 0%, rgba(251, 188, 4, 0.05) 100%);
|
||||||
|
border-radius: 8rpx;
|
||||||
|
padding: 8rpx 12rpx;
|
||||||
|
border: 1rpx solid rgba(251, 188, 4, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-b::before {
|
||||||
|
content: '🟡';
|
||||||
|
margin-right: 6rpx;
|
||||||
|
font-size: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-c {
|
||||||
|
color: #34a853;
|
||||||
|
font-weight: 700;
|
||||||
|
position: relative;
|
||||||
|
background: linear-gradient(135deg, rgba(52, 168, 83, 0.1) 0%, rgba(52, 168, 83, 0.05) 100%);
|
||||||
|
border-radius: 8rpx;
|
||||||
|
padding: 8rpx 12rpx;
|
||||||
|
border: 1rpx solid rgba(52, 168, 83, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-c::before {
|
||||||
|
content: '🟢';
|
||||||
|
margin-right: 6rpx;
|
||||||
|
font-size: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移除空行样式,不再需要 */
|
||||||
|
|
||||||
|
.table-bottom-spacing {
|
||||||
|
height: 40rpx;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-loading-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 60rpx 20rpx;
|
||||||
|
gap: 20rpx;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-loading-spinner {
|
||||||
|
width: 48rpx;
|
||||||
|
height: 48rpx;
|
||||||
|
border: 3rpx solid #e8eaed;
|
||||||
|
border-top: 3rpx solid #5f6368;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-loading-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #5f6368;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-empty-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 80rpx;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-empty-text {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #9aa0a6;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-loading-spinner {
|
||||||
|
width: 60rpx;
|
||||||
|
height: 60rpx;
|
||||||
|
border: 4rpx solid #e9ecef;
|
||||||
|
border-top: 4rpx solid #6c757d;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-loading-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #6c757d;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-empty-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 100rpx 20rpx;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-empty-text {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #999;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 60rpx;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 60rpx;
|
||||||
|
height: 60rpx;
|
||||||
|
border: 4rpx solid rgba(255, 152, 0, 0.3);
|
||||||
|
border-top: 4rpx solid #ff9800;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 100rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #adb5bd;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计 - 平板设备适配
|
||||||
|
@media (min-width: 768px) and (max-width: 1024px) {
|
||||||
|
.header-cell {
|
||||||
|
font-size: 28rpx;
|
||||||
|
padding: 24rpx 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-cell {
|
||||||
|
font-size: 26rpx;
|
||||||
|
padding: 20rpx 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计 - 手机设备适配
|
||||||
|
@media (max-width: 750rpx) {
|
||||||
|
.header-cell {
|
||||||
|
font-size: 22rpx;
|
||||||
|
padding: 16rpx 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-cell {
|
||||||
|
font-size: 20rpx;
|
||||||
|
padding: 12rpx 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计 - 大屏设备适配
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.header-cell {
|
||||||
|
font-size: 30rpx;
|
||||||
|
padding: 28rpx 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-cell {
|
||||||
|
font-size: 28rpx;
|
||||||
|
padding: 24rpx 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
font-size: 30rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
916
src/components/EnvironmentParams.vue
Normal file
916
src/components/EnvironmentParams.vue
Normal file
@ -0,0 +1,916 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page-container environment-container">
|
||||||
|
<!-- 页面头部 -->
|
||||||
|
<view class="page-header">
|
||||||
|
<view class="header-left">
|
||||||
|
<text class="page-title">现场环境参数</text>
|
||||||
|
</view>
|
||||||
|
<view class="header-right">
|
||||||
|
<view class="system-title">
|
||||||
|
<view class="system-title-icon">
|
||||||
|
<text class="icon">📋</text>
|
||||||
|
</view>
|
||||||
|
<text class="system-title-text">移动式检修车间系统</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="page-content">
|
||||||
|
<view class="content-wrapper">
|
||||||
|
<view class="left-panel">
|
||||||
|
<!-- 参数显示区域 -->
|
||||||
|
<view class="parameters">
|
||||||
|
<!-- 当前状态概览 -->
|
||||||
|
<view class="status-overview">
|
||||||
|
<view class="status-card">
|
||||||
|
<view class="status-icon">🌡️</view>
|
||||||
|
<view class="status-info">
|
||||||
|
<text class="status-label">温度</text>
|
||||||
|
<text class="status-value">{{ temperature }}°C</text>
|
||||||
|
</view>
|
||||||
|
<view class="status-indicator" :class="getTemperatureStatus()"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="status-card">
|
||||||
|
<view class="status-icon">💧</view>
|
||||||
|
<view class="status-info">
|
||||||
|
<text class="status-label">湿度</text>
|
||||||
|
<text class="status-value">{{ humidity }}%</text>
|
||||||
|
</view>
|
||||||
|
<view class="status-indicator" :class="getHumidityStatus()"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="status-card">
|
||||||
|
<view class="status-icon">✨</view>
|
||||||
|
<view class="status-info">
|
||||||
|
<text class="status-label">洁净度</text>
|
||||||
|
<text class="status-value">{{ cleanliness }}%</text>
|
||||||
|
</view>
|
||||||
|
<view class="status-indicator" :class="getCleanlinessStatus()"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 详细参数进度条 -->
|
||||||
|
<view class="detailed-params">
|
||||||
|
<view class="param-item">
|
||||||
|
<view class="param-header">
|
||||||
|
<text class="param-name">温度</text>
|
||||||
|
<text class="param-current">{{ temperature }}°C</text>
|
||||||
|
</view>
|
||||||
|
<view class="param-progress">
|
||||||
|
<view class="progress-bar">
|
||||||
|
<view class="progress-fill temperature-fill" :style="{ width: temperaturePercent + '%' }"></view>
|
||||||
|
</view>
|
||||||
|
<text class="param-range">15°C - 35°C</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="param-item">
|
||||||
|
<view class="param-header">
|
||||||
|
<text class="param-name">湿度</text>
|
||||||
|
<text class="param-current">{{ humidity }}%</text>
|
||||||
|
</view>
|
||||||
|
<view class="param-progress">
|
||||||
|
<view class="progress-bar">
|
||||||
|
<view class="progress-fill humidity-fill" :style="{ width: humidityPercent + '%' }"></view>
|
||||||
|
</view>
|
||||||
|
<text class="param-range">30% - 70%</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="param-item">
|
||||||
|
<view class="param-header">
|
||||||
|
<text class="param-name">洁净度</text>
|
||||||
|
<text class="param-current">{{ cleanliness }}%</text>
|
||||||
|
</view>
|
||||||
|
<view class="param-progress">
|
||||||
|
<view class="progress-bar">
|
||||||
|
<view class="progress-fill cleanliness-fill" :style="{ width: cleanlinessPercent + '%' }"></view>
|
||||||
|
</view>
|
||||||
|
<text class="param-range">60% - 100%</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 设置面板 -->
|
||||||
|
<view class="settings-panel">
|
||||||
|
<view class="settings-header">
|
||||||
|
<text class="settings-title">环境控制</text>
|
||||||
|
<view class="settings-status">
|
||||||
|
<view class="status-dot" :class="isConnected ? getOverallStatus() : 'status-disconnected'"></view>
|
||||||
|
<text class="status-text">{{ getOverallStatusText() }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- MQTT连接状态 -->
|
||||||
|
<view class="mqtt-status" v-if="lastUpdateTime">
|
||||||
|
<text class="mqtt-label">最后更新:</text>
|
||||||
|
<text class="mqtt-time">{{ lastUpdateTime }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="settings-content">
|
||||||
|
<view class="control-item">
|
||||||
|
<text class="control-label">温度控制</text>
|
||||||
|
<view class="control-value">{{ temperature }}°C</view>
|
||||||
|
</view>
|
||||||
|
<view class="control-item">
|
||||||
|
<text class="control-label">湿度控制</text>
|
||||||
|
<view class="control-value">{{ humidity }}%</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="settings-actions">
|
||||||
|
<button class="settings-button" @click="openSettings">
|
||||||
|
<text class="button-icon">⚙️</text>
|
||||||
|
<text class="button-text">参数设定</text>
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 右侧监控画面 -->
|
||||||
|
<view class="right-panel">
|
||||||
|
<view class="camera-feed">
|
||||||
|
<view class="camera-header">
|
||||||
|
<text class="camera-title">实时监控</text>
|
||||||
|
<view class="camera-status" :class="isConnected ? getOverallStatus() : 'status-disconnected'">
|
||||||
|
<view class="status-indicator-dot"></view>
|
||||||
|
<text class="status-text">{{ getOverallStatusText() }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="camera-container">
|
||||||
|
<image class="camera-image" src="/static/inspection-room.jpg" mode="aspectFill"></image>
|
||||||
|
<view class="camera-overlay">
|
||||||
|
<view class="overlay-info">
|
||||||
|
<text class="overlay-time">{{ getCurrentTime() }}</text>
|
||||||
|
<text class="overlay-location">检修车间</text>
|
||||||
|
</view>
|
||||||
|
<view class="camera-controls">
|
||||||
|
<button class="control-btn">
|
||||||
|
<text class="control-icon">📷</text>
|
||||||
|
</button>
|
||||||
|
<button class="control-btn">
|
||||||
|
<text class="control-icon">🔍</text>
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, defineEmits, onMounted, onUnmounted } from 'vue';
|
||||||
|
// import mqttClient from '@/utils/mqttClient';
|
||||||
|
import { MQTT_CONFIG, DataParser } from '@/config/mqtt';
|
||||||
|
|
||||||
|
const emit = defineEmits(['openSettings']);
|
||||||
|
|
||||||
|
// 环境参数数据
|
||||||
|
const temperature = ref(25);
|
||||||
|
const humidity = ref(45);
|
||||||
|
const cleanliness = ref(80);
|
||||||
|
|
||||||
|
// MQTT连接状态
|
||||||
|
const isConnected = ref(false);
|
||||||
|
const lastUpdateTime = ref('');
|
||||||
|
|
||||||
|
// 计算百分比值用于进度条显示
|
||||||
|
const temperaturePercent = ref(50);
|
||||||
|
const humidityPercent = ref(60);
|
||||||
|
const cleanlinessPercent = ref(40);
|
||||||
|
|
||||||
|
// MQTT数据处理
|
||||||
|
const handleDeviceData = (data) => {
|
||||||
|
console.log('收到设备数据:', data);
|
||||||
|
|
||||||
|
// 更新最后更新时间
|
||||||
|
lastUpdateTime.value = DataParser.formatTimestamp(data.timestamp);
|
||||||
|
|
||||||
|
// 根据设备类型处理数据
|
||||||
|
switch (data.Device) {
|
||||||
|
case 'AC': // 空调设备
|
||||||
|
if (data.Data && data.Data.BSQWD !== undefined) {
|
||||||
|
temperature.value = parseFloat(data.Data.BSQWD);
|
||||||
|
temperaturePercent.value = ((temperature.value - 15) / 20) * 100; // 15-35°C范围
|
||||||
|
console.log('更新空调温度:', temperature.value);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'WSD': // 温湿度传感器
|
||||||
|
if (data.Data) {
|
||||||
|
if (data.Data.WD !== undefined) {
|
||||||
|
temperature.value = parseFloat(data.Data.WD);
|
||||||
|
temperaturePercent.value = ((temperature.value - 15) / 20) * 100;
|
||||||
|
console.log('更新温湿度传感器温度:', temperature.value);
|
||||||
|
}
|
||||||
|
if (data.Data.SD !== undefined) {
|
||||||
|
humidity.value = parseFloat(data.Data.SD);
|
||||||
|
humidityPercent.value = (humidity.value / 80) * 100; // 0-80%范围
|
||||||
|
console.log('更新湿度:', humidity.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'PM': // PM2.5传感器
|
||||||
|
if (data.Data && data.Data.PM25 !== undefined) {
|
||||||
|
// 将PM2.5值转换为洁净度百分比 (PM2.5越低,洁净度越高)
|
||||||
|
const pm25Value = parseFloat(data.Data.PM25);
|
||||||
|
cleanliness.value = Math.max(0, Math.min(100, 100 - (pm25Value / 2))); // 简单转换
|
||||||
|
cleanlinessPercent.value = cleanliness.value;
|
||||||
|
console.log('更新PM2.5/洁净度:', pm25Value, cleanliness.value);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.log('未知设备类型:', data.Device);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// // 连接MQTT并订阅数据
|
||||||
|
// const connectMQTT = async () => {
|
||||||
|
// try {
|
||||||
|
// await mqttClient.connect();
|
||||||
|
// isConnected.value = true;
|
||||||
|
|
||||||
|
// // 订阅设备数据主题
|
||||||
|
// const subscribeSuccess = mqttClient.subscribe(MQTT_CONFIG.topics.deviceData, handleDeviceData);
|
||||||
|
|
||||||
|
// if (subscribeSuccess) {
|
||||||
|
// console.log('MQTT订阅成功,等待设备数据...');
|
||||||
|
// } else {
|
||||||
|
// console.error('MQTT订阅失败');
|
||||||
|
// }
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error('MQTT连接失败:', error);
|
||||||
|
// isConnected.value = false;
|
||||||
|
|
||||||
|
// // 显示连接失败提示
|
||||||
|
// uni.showToast({
|
||||||
|
// title: 'MQTT连接失败',
|
||||||
|
// icon: 'error',
|
||||||
|
// duration: 3000
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// 打开设置弹窗
|
||||||
|
const openSettings = () => {
|
||||||
|
emit('openSettings');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取参数状态
|
||||||
|
const getTemperatureStatus = () => {
|
||||||
|
if (temperature.value < 20 || temperature.value > 30) return 'status-warning';
|
||||||
|
return 'status-normal';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHumidityStatus = () => {
|
||||||
|
if (humidity.value < 40 || humidity.value > 60) return 'status-warning';
|
||||||
|
return 'status-normal';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCleanlinessStatus = () => {
|
||||||
|
if (cleanliness.value < 70) return 'status-warning';
|
||||||
|
return 'status-normal';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取整体状态
|
||||||
|
const getOverallStatus = () => {
|
||||||
|
const tempStatus = getTemperatureStatus();
|
||||||
|
const humidityStatus = getHumidityStatus();
|
||||||
|
const cleanlinessStatus = getCleanlinessStatus();
|
||||||
|
|
||||||
|
if (tempStatus === 'status-warning' || humidityStatus === 'status-warning' || cleanlinessStatus === 'status-warning') {
|
||||||
|
return 'status-warning';
|
||||||
|
}
|
||||||
|
return 'status-normal';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOverallStatusText = () => {
|
||||||
|
if (!isConnected.value) {
|
||||||
|
return '未连接';
|
||||||
|
}
|
||||||
|
return getOverallStatus() === 'status-normal' ? '正常' : '异常';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取当前时间
|
||||||
|
const getCurrentTime = () => {
|
||||||
|
const now = new Date();
|
||||||
|
return now.toLocaleTimeString('zh-CN', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 组件挂载时连接MQTT
|
||||||
|
onMounted(() => {
|
||||||
|
// connectMQTT();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 组件卸载时断开连接
|
||||||
|
onUnmounted(() => {
|
||||||
|
// mqttClient.unsubscribe(MQTT_CONFIG.topics.deviceData);
|
||||||
|
// mqttClient.disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 移除模拟数据变化,使用真实MQTT数据
|
||||||
|
// setInterval(() => {
|
||||||
|
// // 温度在22-28之间随机变化
|
||||||
|
// temperature.value = Math.floor(Math.random() * 6) + 22;
|
||||||
|
// temperaturePercent.value = Math.floor(Math.random() * 30) + 40;
|
||||||
|
//
|
||||||
|
// // 湿度在40-60之间随机变化
|
||||||
|
// humidity.value = Math.floor(Math.random() * 20) + 40;
|
||||||
|
// humidityPercent.value = Math.floor(Math.random() * 30) + 40;
|
||||||
|
//
|
||||||
|
// // 洁净度百分比随机变化
|
||||||
|
// cleanlinessPercent.value = Math.floor(Math.random() * 30) + 40;
|
||||||
|
// }, 5000);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '@/styles/common.scss';
|
||||||
|
|
||||||
|
.environment-container {
|
||||||
|
// 继承通用页面容器样式
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
height: calc(100% - 120rpx);
|
||||||
|
padding: 0;
|
||||||
|
// margin: 0 30rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-panel {
|
||||||
|
flex: 3;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 参数显示区域 */
|
||||||
|
.parameters {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 状态概览卡片 */
|
||||||
|
.status-overview {
|
||||||
|
display: flex;
|
||||||
|
gap: 12rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card {
|
||||||
|
flex: 1;
|
||||||
|
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 16rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
border: 1rpx solid #e8eaed;
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card:hover {
|
||||||
|
transform: translateY(-2rpx);
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon {
|
||||||
|
font-size: 32rpx;
|
||||||
|
width: 40rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-info {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-label {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #5f6368;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-value {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #1a73e8;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator {
|
||||||
|
width: 12rpx;
|
||||||
|
height: 12rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator.status-normal {
|
||||||
|
background: linear-gradient(135deg, #34a853 0%, #2e7d32 100%);
|
||||||
|
box-shadow: 0 0 8rpx rgba(52, 168, 83, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator.status-warning {
|
||||||
|
background: linear-gradient(135deg, #ea4335 0%, #d32f2f 100%);
|
||||||
|
box-shadow: 0 0 8rpx rgba(234, 67, 53, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 详细参数进度条 */
|
||||||
|
.detailed-params {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-item {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 20rpx;
|
||||||
|
border: 1rpx solid #e8eaed;
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-item:hover {
|
||||||
|
transform: translateY(-1rpx);
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-name {
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #3c4043;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-current {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1a73e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-progress {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
height: 8rpx;
|
||||||
|
background: #f1f3f4;
|
||||||
|
border-radius: 4rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 4rpx;
|
||||||
|
transition: width 0.5s ease;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.3) 50%, transparent 100%);
|
||||||
|
animation: shimmer 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% { transform: translateX(-100%); }
|
||||||
|
100% { transform: translateX(100%); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.temperature-fill {
|
||||||
|
background: linear-gradient(90deg, #ff6b35 0%, #f7931e 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.humidity-fill {
|
||||||
|
background: linear-gradient(90deg, #4285f4 0%, #34a853 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cleanliness-fill {
|
||||||
|
background: linear-gradient(90deg, #9c27b0 0%, #673ab7 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-range {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #9aa0a6;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 右侧面板 */
|
||||||
|
.right-panel {
|
||||||
|
flex: 2;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-feed {
|
||||||
|
flex: 1;
|
||||||
|
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
|
||||||
|
border-radius: 12rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
border: 1rpx solid #e8eaed;
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16rpx 20rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-bottom: 1rpx solid #e8eaed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #3c4043;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
padding: 6rpx 12rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-status.status-normal {
|
||||||
|
background: rgba(52, 168, 83, 0.1);
|
||||||
|
border: 1rpx solid rgba(52, 168, 83, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-status.status-warning {
|
||||||
|
background: rgba(234, 67, 53, 0.1);
|
||||||
|
border: 1rpx solid rgba(234, 67, 53, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-status.status-disconnected {
|
||||||
|
background: rgba(154, 160, 166, 0.1);
|
||||||
|
border: 1rpx solid rgba(154, 160, 166, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator-dot {
|
||||||
|
width: 8rpx;
|
||||||
|
height: 8rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-status.status-normal .status-indicator-dot {
|
||||||
|
background: #34a853;
|
||||||
|
box-shadow: 0 0 6rpx rgba(52, 168, 83, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-status.status-warning .status-indicator-dot {
|
||||||
|
background: #ea4335;
|
||||||
|
box-shadow: 0 0 6rpx rgba(234, 67, 53, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-status.status-disconnected .status-indicator-dot {
|
||||||
|
background: #9aa0a6;
|
||||||
|
box-shadow: 0 0 6rpx rgba(154, 160, 166, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-container {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(180deg, rgba(0, 0, 0, 0.1) 0%, transparent 30%, transparent 70%, rgba(0, 0, 0, 0.2) 100%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-time {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
font-weight: 600;
|
||||||
|
text-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-location {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
font-weight: 500;
|
||||||
|
text-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 12rpx;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn {
|
||||||
|
width: 48rpx;
|
||||||
|
height: 48rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
border: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
background: rgba(255, 255, 255, 1);
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-icon {
|
||||||
|
font-size: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-panel {
|
||||||
|
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 20rpx;
|
||||||
|
border: 1rpx solid #e8eaed;
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16rpx;
|
||||||
|
// height: 200rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding-bottom: 12rpx;
|
||||||
|
border-bottom: 1rpx solid #e8eaed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #3c4043;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
width: 12rpx;
|
||||||
|
height: 12rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot.status-normal {
|
||||||
|
background: linear-gradient(135deg, #34a853 0%, #2e7d32 100%);
|
||||||
|
box-shadow: 0 0 8rpx rgba(52, 168, 83, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot.status-warning {
|
||||||
|
background: linear-gradient(135deg, #ea4335 0%, #d32f2f 100%);
|
||||||
|
box-shadow: 0 0 8rpx rgba(234, 67, 53, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot.status-disconnected {
|
||||||
|
background: linear-gradient(135deg, #9aa0a6 0%, #5f6368 100%);
|
||||||
|
box-shadow: 0 0 8rpx rgba(154, 160, 166, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #5f6368;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* MQTT状态显示 */
|
||||||
|
.mqtt-status {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8rpx 0;
|
||||||
|
border-bottom: 1rpx solid #e8eaed;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mqtt-label {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #9aa0a6;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mqtt-time {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #1a73e8;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-label {
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #5f6368;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-value {
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1a73e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
padding: 12rpx 24rpx;
|
||||||
|
background: linear-gradient(135deg, #1a73e8 0%, #1557b0 100%);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(26, 115, 232, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-button:hover {
|
||||||
|
transform: translateY(-1rpx);
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(26, 115, 232, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-icon {
|
||||||
|
font-size: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计 - 平板设备适配
|
||||||
|
@media (min-width: 768px) and (max-width: 1024px) {
|
||||||
|
.parameter-label {
|
||||||
|
font-size: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter-value {
|
||||||
|
font-size: 36rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-icon {
|
||||||
|
width: 48rpx;
|
||||||
|
height: 48rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter-row {
|
||||||
|
height: 120rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-label {
|
||||||
|
font-size: 36rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-value {
|
||||||
|
font-size: 36rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计 - 手机设备适配
|
||||||
|
@media (max-width: 750rpx) {
|
||||||
|
.parameter-label {
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter-value {
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-icon {
|
||||||
|
width: 32rpx;
|
||||||
|
height: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter-row {
|
||||||
|
height: 80rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-label {
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-value {
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计 - 大屏设备适配
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.parameter-label {
|
||||||
|
font-size: 36rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter-value {
|
||||||
|
font-size: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-icon {
|
||||||
|
width: 56rpx;
|
||||||
|
height: 56rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter-row {
|
||||||
|
height: 140rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-label {
|
||||||
|
font-size: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-value {
|
||||||
|
font-size: 40rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
675
src/components/ParameterRecord.vue
Normal file
675
src/components/ParameterRecord.vue
Normal file
@ -0,0 +1,675 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page-container parameter-record-container">
|
||||||
|
<!-- 页面头部 -->
|
||||||
|
<view class="page-header">
|
||||||
|
<view class="header-left">
|
||||||
|
<text class="page-title">参数记录</text>
|
||||||
|
<view class="date-selector" @click="showDatePicker">
|
||||||
|
<text class="date-text">{{ currentDate }}</text>
|
||||||
|
<text class="date-icon">▼</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="header-right">
|
||||||
|
<view class="system-title">
|
||||||
|
<view class="system-title-icon">
|
||||||
|
<text class="icon">📋</text>
|
||||||
|
</view>
|
||||||
|
<text class="system-title-text">移动式检修车间系统</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 图表区域 -->
|
||||||
|
<view class="page-content">
|
||||||
|
<scroll-view
|
||||||
|
class="charts-scroll-container"
|
||||||
|
scroll-y="true"
|
||||||
|
:scroll-with-animation="true"
|
||||||
|
:scroll-top="scrollTop"
|
||||||
|
@scrolltolower="onScrollToLower"
|
||||||
|
@scroll="onScroll"
|
||||||
|
>
|
||||||
|
<view class="charts-container">
|
||||||
|
<!-- 温度图表 -->
|
||||||
|
<view class="chart-card">
|
||||||
|
<view class="chart-header">
|
||||||
|
<view class="chart-info">
|
||||||
|
<text class="chart-title">温度趋势</text>
|
||||||
|
<text class="chart-subtitle">24小时数据</text>
|
||||||
|
</view>
|
||||||
|
<view class="chart-status">
|
||||||
|
<view class="status-dot temperature-dot"></view>
|
||||||
|
<text class="status-text">正常</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="chart-content">
|
||||||
|
<canvas
|
||||||
|
id="temperatureChart"
|
||||||
|
canvas-id="temperatureChart"
|
||||||
|
class="chart-canvas"
|
||||||
|
@touchstart="onChartTouch"
|
||||||
|
@touchmove="onChartTouch"
|
||||||
|
@touchend="onChartTouch"
|
||||||
|
></canvas>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 湿度图表 -->
|
||||||
|
<view class="chart-card">
|
||||||
|
<view class="chart-header">
|
||||||
|
<view class="chart-info">
|
||||||
|
<text class="chart-title">湿度趋势</text>
|
||||||
|
<text class="chart-subtitle">24小时数据</text>
|
||||||
|
</view>
|
||||||
|
<view class="chart-status">
|
||||||
|
<view class="status-dot humidity-dot"></view>
|
||||||
|
<text class="status-text">正常</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="chart-content">
|
||||||
|
<canvas
|
||||||
|
id="humidityChart"
|
||||||
|
canvas-id="humidityChart"
|
||||||
|
class="chart-canvas"
|
||||||
|
@touchstart="onChartTouch"
|
||||||
|
@touchmove="onChartTouch"
|
||||||
|
@touchend="onChartTouch"
|
||||||
|
></canvas>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- PM图表 -->
|
||||||
|
<view class="chart-card">
|
||||||
|
<view class="chart-header">
|
||||||
|
<view class="chart-info">
|
||||||
|
<text class="chart-title">PM2.5趋势</text>
|
||||||
|
<text class="chart-subtitle">24小时数据</text>
|
||||||
|
</view>
|
||||||
|
<view class="chart-status">
|
||||||
|
<view class="status-dot pm-dot"></view>
|
||||||
|
<text class="status-text">正常</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="chart-content">
|
||||||
|
<canvas
|
||||||
|
id="pmChart"
|
||||||
|
canvas-id="pmChart"
|
||||||
|
class="chart-canvas"
|
||||||
|
@touchstart="onChartTouch"
|
||||||
|
@touchmove="onChartTouch"
|
||||||
|
@touchend="onChartTouch"
|
||||||
|
></canvas>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 底部间距 -->
|
||||||
|
<view class="content-bottom-spacing"></view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, nextTick } from 'vue';
|
||||||
|
|
||||||
|
// 当前日期
|
||||||
|
const currentDate = ref('2025年9月1日');
|
||||||
|
|
||||||
|
// 滚动相关
|
||||||
|
const scrollTop = ref(0);
|
||||||
|
const isScrolling = ref(false);
|
||||||
|
|
||||||
|
// 模拟数据
|
||||||
|
const temperatureData = ref([]);
|
||||||
|
const humidityData = ref([]);
|
||||||
|
const pmData = ref([]);
|
||||||
|
const pressureData = ref([]);
|
||||||
|
const windSpeedData = ref([]);
|
||||||
|
const lightData = ref([]);
|
||||||
|
|
||||||
|
// MQTT数据请求接口(预留)
|
||||||
|
const mqttService = {
|
||||||
|
// 连接MQTT服务器
|
||||||
|
connect: () => {
|
||||||
|
console.log('MQTT连接中...');
|
||||||
|
// 这里后期会实现真实的MQTT连接
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
|
||||||
|
// 订阅参数数据
|
||||||
|
subscribeParameterData: (date) => {
|
||||||
|
console.log(`订阅${date}的参数数据`);
|
||||||
|
// 这里后期会实现真实的MQTT订阅
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取历史数据
|
||||||
|
getHistoricalData: (date, parameter) => {
|
||||||
|
console.log(`获取${date}的${parameter}历史数据`);
|
||||||
|
// 这里后期会实现真实的MQTT数据请求
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
|
||||||
|
// 断开连接
|
||||||
|
disconnect: () => {
|
||||||
|
console.log('MQTT断开连接');
|
||||||
|
// 这里后期会实现真实的MQTT断开
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化数据
|
||||||
|
const initData = () => {
|
||||||
|
// 清空现有数据
|
||||||
|
temperatureData.value = [];
|
||||||
|
humidityData.value = [];
|
||||||
|
pmData.value = [];
|
||||||
|
pressureData.value = [];
|
||||||
|
windSpeedData.value = [];
|
||||||
|
lightData.value = [];
|
||||||
|
|
||||||
|
// 生成24小时的模拟数据
|
||||||
|
for (let i = 0; i < 24; i++) {
|
||||||
|
temperatureData.value.push({
|
||||||
|
time: i,
|
||||||
|
value: 20 + Math.sin(i * Math.PI / 12) * 5 + Math.random() * 2
|
||||||
|
});
|
||||||
|
|
||||||
|
humidityData.value.push({
|
||||||
|
time: i,
|
||||||
|
value: 50 + Math.sin(i * Math.PI / 8) * 10 + Math.random() * 3
|
||||||
|
});
|
||||||
|
|
||||||
|
pmData.value.push({
|
||||||
|
time: i,
|
||||||
|
value: 30 + Math.sin(i * Math.PI / 6) * 8 + Math.random() * 4
|
||||||
|
});
|
||||||
|
|
||||||
|
pressureData.value.push({
|
||||||
|
time: i,
|
||||||
|
value: 1013 + Math.sin(i * Math.PI / 10) * 5 + Math.random() * 2
|
||||||
|
});
|
||||||
|
|
||||||
|
windSpeedData.value.push({
|
||||||
|
time: i,
|
||||||
|
value: 5 + Math.sin(i * Math.PI / 6) * 3 + Math.random() * 2
|
||||||
|
});
|
||||||
|
|
||||||
|
lightData.value.push({
|
||||||
|
time: i,
|
||||||
|
value: 200 + Math.sin(i * Math.PI / 12) * 100 + Math.random() * 50
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 绘制图表
|
||||||
|
const drawChart = (canvasId, data, color = '#000000') => {
|
||||||
|
const ctx = uni.createCanvasContext(canvasId);
|
||||||
|
// 获取实际canvas尺寸
|
||||||
|
const query = uni.createSelectorQuery();
|
||||||
|
query.select(`#${canvasId}`).boundingClientRect((rect) => {
|
||||||
|
if (rect) {
|
||||||
|
const canvasWidth = rect.width;
|
||||||
|
const canvasHeight = rect.height;
|
||||||
|
drawChartContent(ctx, canvasId, data, color, canvasWidth, canvasHeight);
|
||||||
|
}
|
||||||
|
}).exec();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 绘制图表内容
|
||||||
|
const drawChartContent = (ctx, canvasId, data, color, canvasWidth, canvasHeight) => {
|
||||||
|
// 清空画布
|
||||||
|
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
|
||||||
|
|
||||||
|
// 设置背景 - 透明背景
|
||||||
|
ctx.setFillStyle('transparent');
|
||||||
|
ctx.fillRect(0, 0, canvasWidth, canvasHeight - 40);
|
||||||
|
|
||||||
|
// 绘制网格线
|
||||||
|
ctx.setStrokeStyle('#f1f3f4');
|
||||||
|
ctx.setLineWidth(1);
|
||||||
|
|
||||||
|
// 绘制水平网格线
|
||||||
|
for (let i = 0; i <= 4; i++) {
|
||||||
|
const y = (canvasHeight - 40) * (i / 4);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, y);
|
||||||
|
ctx.lineTo(canvasWidth, y);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绘制垂直网格线
|
||||||
|
for (let i = 0; i <= 6; i++) {
|
||||||
|
const x = (canvasWidth / 6) * i;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, 0);
|
||||||
|
ctx.lineTo(x, canvasHeight - 40);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绘制数据线和填充区域
|
||||||
|
if (data.length > 0) {
|
||||||
|
const points = [];
|
||||||
|
|
||||||
|
// 计算所有点的坐标
|
||||||
|
data.forEach((point, index) => {
|
||||||
|
const x = (canvasWidth / 24) * (point.time + 0.5);
|
||||||
|
let y;
|
||||||
|
if (canvasId === 'temperatureChart') {
|
||||||
|
y = (canvasHeight - 40) - ((point.value - 15) / 15) * (canvasHeight - 40);
|
||||||
|
} else if (canvasId === 'humidityChart') {
|
||||||
|
y = (canvasHeight - 40) - (point.value / 80) * (canvasHeight - 40);
|
||||||
|
} else {
|
||||||
|
y = (canvasHeight - 40) - (point.value / 60) * (canvasHeight - 40);
|
||||||
|
}
|
||||||
|
points.push({ x, y, value: point.value });
|
||||||
|
});
|
||||||
|
|
||||||
|
// 绘制填充区域 - 透明色
|
||||||
|
ctx.setFillStyle('transparent');
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(points[0].x, canvasHeight - 40);
|
||||||
|
points.forEach(point => {
|
||||||
|
ctx.lineTo(point.x, point.y);
|
||||||
|
});
|
||||||
|
ctx.lineTo(points[points.length - 1].x, canvasHeight - 40);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// 绘制数据线
|
||||||
|
ctx.setStrokeStyle(color);
|
||||||
|
ctx.setLineWidth(3);
|
||||||
|
ctx.beginPath();
|
||||||
|
|
||||||
|
points.forEach((point, index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
ctx.moveTo(point.x, point.y);
|
||||||
|
} else {
|
||||||
|
ctx.lineTo(point.x, point.y);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// 绘制数据点
|
||||||
|
ctx.setFillStyle(color);
|
||||||
|
points.forEach((point, index) => {
|
||||||
|
if (index % 3 === 0) { // 每3个点显示一个数据点
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(point.x, point.y, 4, 0, 2 * Math.PI);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// 绘制数据点外圈
|
||||||
|
ctx.setStrokeStyle('#ffffff');
|
||||||
|
ctx.setLineWidth(2);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绘制时间轴背景
|
||||||
|
ctx.setFillStyle('rgba(248, 249, 250, 0.8)');
|
||||||
|
ctx.fillRect(0, canvasHeight - 40, canvasWidth, 40);
|
||||||
|
|
||||||
|
// 绘制时间轴标签 - 显示0-23所有小时
|
||||||
|
ctx.setFillStyle('#5f6368');
|
||||||
|
ctx.setFontSize(20);
|
||||||
|
ctx.setTextAlign('center');
|
||||||
|
for (let i = 0; i <= 23; i += 2) { // 每2小时显示一个标签
|
||||||
|
const x = (canvasWidth / 24) * (i + 0.5);
|
||||||
|
ctx.fillText(i.toString(), x, canvasHeight - 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.draw();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 绘制所有图表
|
||||||
|
const drawAllCharts = () => {
|
||||||
|
nextTick(() => {
|
||||||
|
drawChart('temperatureChart', temperatureData.value, '#ff6b35');
|
||||||
|
drawChart('humidityChart', humidityData.value, '#4285f4');
|
||||||
|
drawChart('pmChart', pmData.value, '#9c27b0');
|
||||||
|
drawChart('pressureChart', pressureData.value, '#34a853');
|
||||||
|
drawChart('windSpeedChart', windSpeedData.value, '#fbbc04');
|
||||||
|
drawChart('lightChart', lightData.value, '#ea4335');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 显示日期选择器
|
||||||
|
const showDatePicker = () => {
|
||||||
|
uni.showActionSheet({
|
||||||
|
itemList: ['2025年9月1日', '2025年8月31日', '2025年8月30日'],
|
||||||
|
success: (res) => {
|
||||||
|
const dates = ['2025年9月1日', '2025年8月31日', '2025年8月30日'];
|
||||||
|
currentDate.value = dates[res.tapIndex];
|
||||||
|
// 通过MQTT获取新日期的数据
|
||||||
|
loadDataByDate(currentDate.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 根据日期加载数据
|
||||||
|
const loadDataByDate = async (date) => {
|
||||||
|
try {
|
||||||
|
// 连接MQTT并获取数据
|
||||||
|
await mqttService.connect();
|
||||||
|
await mqttService.subscribeParameterData(date);
|
||||||
|
|
||||||
|
// 获取各参数的历史数据
|
||||||
|
await Promise.all([
|
||||||
|
mqttService.getHistoricalData(date, 'temperature'),
|
||||||
|
mqttService.getHistoricalData(date, 'humidity'),
|
||||||
|
mqttService.getHistoricalData(date, 'pm')
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 重新生成模拟数据并绘制图表
|
||||||
|
initData();
|
||||||
|
drawAllCharts();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载数据失败:', error);
|
||||||
|
// 如果MQTT失败,使用模拟数据
|
||||||
|
initData();
|
||||||
|
drawAllCharts();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 图表触摸事件
|
||||||
|
const onChartTouch = (e) => {
|
||||||
|
// 可以在这里添加图表交互功能
|
||||||
|
console.log('Chart touched:', e);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 滚动事件处理
|
||||||
|
let scrollTimer = null;
|
||||||
|
|
||||||
|
const onScroll = (e) => {
|
||||||
|
isScrolling.value = true;
|
||||||
|
|
||||||
|
// 防抖处理,避免频繁触发
|
||||||
|
if (scrollTimer) {
|
||||||
|
clearTimeout(scrollTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollTimer = setTimeout(() => {
|
||||||
|
isScrolling.value = false;
|
||||||
|
}, 150);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onScrollToLower = () => {
|
||||||
|
console.log('滚动到底部');
|
||||||
|
// 可以在这里添加加载更多数据的逻辑
|
||||||
|
};
|
||||||
|
|
||||||
|
// 组件挂载后初始化
|
||||||
|
onMounted(() => {
|
||||||
|
loadDataByDate(currentDate.value);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '@/styles/common.scss';
|
||||||
|
|
||||||
|
.parameter-record-container {
|
||||||
|
// 继承通用页面容器样式
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-selector {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16rpx 24rpx;
|
||||||
|
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
|
||||||
|
border-radius: 12rpx;
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
|
||||||
|
border: 1rpx solid #e8eaed;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-selector:hover {
|
||||||
|
transform: translateY(-1rpx);
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-text {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #3c4043;
|
||||||
|
margin-right: 12rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-icon {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #1a73e8;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-selector:hover .date-icon {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.charts-scroll-container {
|
||||||
|
flex: 1;
|
||||||
|
height: 0; /* 重要:配合flex: 1使用,确保正确计算高度 */
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
/* 滚动条样式 */
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #dadce0 #f1f3f4;
|
||||||
|
/* 平滑滚动 */
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Webkit浏览器滚动条样式 */
|
||||||
|
.charts-scroll-container::-webkit-scrollbar {
|
||||||
|
width: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.charts-scroll-container::-webkit-scrollbar-track {
|
||||||
|
background: rgba(241, 243, 244, 0.5);
|
||||||
|
border-radius: 4rpx;
|
||||||
|
margin: 4rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.charts-scroll-container::-webkit-scrollbar-thumb {
|
||||||
|
background: linear-gradient(180deg, #dadce0 0%, #bdc1c6 100%);
|
||||||
|
border-radius: 4rpx;
|
||||||
|
border: 1rpx solid rgba(255, 255, 255, 0.2);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.charts-scroll-container::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: linear-gradient(180deg, #bdc1c6 0%, #9aa0a6 100%);
|
||||||
|
box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.charts-scroll-container::-webkit-scrollbar-thumb:active {
|
||||||
|
background: linear-gradient(180deg, #9aa0a6 0%, #5f6368 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.charts-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20rpx;
|
||||||
|
// padding: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card {
|
||||||
|
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 24rpx;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
|
||||||
|
border: 1rpx solid #e8eaed;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card:hover {
|
||||||
|
transform: translateY(-2rpx);
|
||||||
|
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
padding-bottom: 16rpx;
|
||||||
|
border-bottom: 1rpx solid #e8eaed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #3c4043;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-subtitle {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #9aa0a6;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
padding: 8rpx 16rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
background: rgba(52, 168, 83, 0.1);
|
||||||
|
border: 1rpx solid rgba(52, 168, 83, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
width: 12rpx;
|
||||||
|
height: 12rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.temperature-dot {
|
||||||
|
background: linear-gradient(135deg, #ff6b35 0%, #f7931e 100%);
|
||||||
|
box-shadow: 0 0 8rpx rgba(255, 107, 53, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.humidity-dot {
|
||||||
|
background: linear-gradient(135deg, #4285f4 0%, #34a853 100%);
|
||||||
|
box-shadow: 0 0 8rpx rgba(66, 133, 244, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pm-dot {
|
||||||
|
background: linear-gradient(135deg, #9c27b0 0%, #673ab7 100%);
|
||||||
|
box-shadow: 0 0 8rpx rgba(156, 39, 176, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #34a853;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
border-radius: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-canvas {
|
||||||
|
width: 100%;
|
||||||
|
height: 320rpx;
|
||||||
|
background-color: transparent;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 30rpx;
|
||||||
|
padding: 20rpx 10rpx 10rpx 0;
|
||||||
|
border-top: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-bottom-spacing {
|
||||||
|
height: 40rpx;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计 - 平板设备适配
|
||||||
|
@media (min-width: 768px) and (max-width: 1024px) {
|
||||||
|
.date-text {
|
||||||
|
font-size: 36rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-title {
|
||||||
|
font-size: 36rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-canvas {
|
||||||
|
height: 360rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-text {
|
||||||
|
font-size: 32rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计 - 手机设备适配
|
||||||
|
@media (max-width: 750rpx) {
|
||||||
|
.date-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-canvas {
|
||||||
|
height: 240rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计 - 大屏设备适配
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.date-text {
|
||||||
|
font-size: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-title {
|
||||||
|
font-size: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-canvas {
|
||||||
|
height: 420rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-text {
|
||||||
|
font-size: 36rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
31
src/components/PlaceholderContent.vue
Normal file
31
src/components/PlaceholderContent.vue
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<template>
|
||||||
|
<view class="placeholder-content">
|
||||||
|
<text>{{ title }} 页面内容</text>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineProps } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.placeholder-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
padding: 30rpx;
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #666;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
172
src/components/SettingsModal.vue
Normal file
172
src/components/SettingsModal.vue
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
<template>
|
||||||
|
<view class="modal" v-if="visible">
|
||||||
|
<view class="popup-content">
|
||||||
|
<view class="popup-header">
|
||||||
|
<text class="popup-title">环境参数设置</text>
|
||||||
|
<text class="close-button" @click="closeModal">×</text>
|
||||||
|
</view>
|
||||||
|
<view class="popup-body">
|
||||||
|
<view class="setting-group">
|
||||||
|
<text class="setting-label">温度阈值 (°C)</text>
|
||||||
|
<input type="number" class="setting-input" v-model="tempThreshold" />
|
||||||
|
</view>
|
||||||
|
<view class="setting-group">
|
||||||
|
<text class="setting-label">湿度阈值 (%)</text>
|
||||||
|
<input type="number" class="setting-input" v-model="humidityThreshold" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="popup-footer">
|
||||||
|
<button class="popup-button cancel-button" @click="closeModal">取消</button>
|
||||||
|
<button class="popup-button save-button" @click="saveSettings">保存</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, defineProps, defineEmits } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:visible', 'save']);
|
||||||
|
|
||||||
|
// 设置参数
|
||||||
|
const tempThreshold = ref(25);
|
||||||
|
const humidityThreshold = ref(45);
|
||||||
|
const cleanlinessThreshold = ref(100);
|
||||||
|
|
||||||
|
// 报警选项
|
||||||
|
const alarmOptions = ref(['全部启用', '仅温度', '仅湿度', '仅洁净度', '全部禁用']);
|
||||||
|
const alarmIndex = ref(0);
|
||||||
|
|
||||||
|
// 报警选项变更
|
||||||
|
const onAlarmChange = (e) => {
|
||||||
|
alarmIndex.value = e.detail.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 关闭弹窗
|
||||||
|
const closeModal = () => {
|
||||||
|
emit('update:visible', false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存设置
|
||||||
|
const saveSettings = () => {
|
||||||
|
// 创建设置对象
|
||||||
|
const settings = {
|
||||||
|
tempThreshold: tempThreshold.value,
|
||||||
|
humidityThreshold: humidityThreshold.value,
|
||||||
|
cleanlinessThreshold: cleanlinessThreshold.value,
|
||||||
|
alarmOption: alarmOptions.value[alarmIndex.value]
|
||||||
|
};
|
||||||
|
|
||||||
|
// 发送保存事件
|
||||||
|
emit('save', settings);
|
||||||
|
|
||||||
|
// 关闭弹窗
|
||||||
|
closeModal();
|
||||||
|
|
||||||
|
// 显示保存成功提示
|
||||||
|
uni.showToast({
|
||||||
|
title: '设置已保存',
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
/* 弹窗样式 */
|
||||||
|
.modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-content {
|
||||||
|
background-color: white;
|
||||||
|
padding: 30rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
width: 600rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button {
|
||||||
|
font-size: 40rpx;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-body {
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-group {
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 10rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-input {
|
||||||
|
// width: 100%;
|
||||||
|
padding: 15rpx;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-value {
|
||||||
|
padding: 15rpx;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-button {
|
||||||
|
padding: 15rpx 30rpx;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-button {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-button {
|
||||||
|
background-color: #3f51b5;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
174
src/components/SideMenu.vue
Normal file
174
src/components/SideMenu.vue
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
<template>
|
||||||
|
<view class="sidebar">
|
||||||
|
<view class="logo-container">
|
||||||
|
<!-- <image class="logo" src="/static/logo.png" mode="aspectFit"></image> -->
|
||||||
|
<text class="logo-text">检修系统</text>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
v-for="(item, index) in menuList"
|
||||||
|
:key="index"
|
||||||
|
class="menu-item"
|
||||||
|
:class="{ active: activeMenuIndex === index }"
|
||||||
|
@click="switchMenu(index)"
|
||||||
|
>
|
||||||
|
<text>{{ item.name }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="menu-item alert" @click="showAlert">
|
||||||
|
<text>当前报警项目</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, defineProps, defineEmits } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
activeMenuIndex: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:activeMenuIndex', 'showAlert']);
|
||||||
|
|
||||||
|
// 菜单列表
|
||||||
|
const menuList = ref([
|
||||||
|
{ name: '现场环境参数' },
|
||||||
|
{ name: '参数记录' },
|
||||||
|
{ name: '视觉监控' },
|
||||||
|
{ name: '日志' },
|
||||||
|
{ name: '报警' }
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 切换菜单
|
||||||
|
const switchMenu = (index) => {
|
||||||
|
emit('update:activeMenuIndex', index);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 显示报警信息
|
||||||
|
const showAlert = () => {
|
||||||
|
emit('showAlert');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
/* 左侧菜单栏样式 - 后台管理系统风格 */
|
||||||
|
.sidebar {
|
||||||
|
width: 200rpx;
|
||||||
|
background-color: #ffffff;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
border-right: 1rpx solid #e8eaed;
|
||||||
|
box-shadow: 1rpx 0 3rpx rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 24rpx 0;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
color: #3c4043;
|
||||||
|
flex-direction: column;
|
||||||
|
border-bottom: 1rpx solid #e8eaed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
width: 60rpx;
|
||||||
|
height: 60rpx;
|
||||||
|
background-color: transparent;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
color: #3c4043;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
padding: 20rpx 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
background-color: transparent;
|
||||||
|
color: #5f6368;
|
||||||
|
border-bottom: 1rpx solid #f1f3f4;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 400;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
color: #3c4043;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item.active {
|
||||||
|
background-color: #e8f0fe;
|
||||||
|
color: #1a73e8;
|
||||||
|
border-right: 3rpx solid #1a73e8;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item.alert {
|
||||||
|
background-color: #fef7e0;
|
||||||
|
color: #ea8600;
|
||||||
|
margin-top: auto;
|
||||||
|
border: 1rpx solid #fdd663;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计 - 平板设备适配
|
||||||
|
@media (min-width: 768px) and (max-width: 1024px) {
|
||||||
|
.sidebar {
|
||||||
|
width: 220rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-text {
|
||||||
|
font-size: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
font-size: 26rpx;
|
||||||
|
padding: 24rpx 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计 - 手机设备适配
|
||||||
|
@media (max-width: 750rpx) {
|
||||||
|
.sidebar {
|
||||||
|
width: 160rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-text {
|
||||||
|
font-size: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
font-size: 20rpx;
|
||||||
|
padding: 16rpx 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计 - 大屏设备适配
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.sidebar {
|
||||||
|
width: 240rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
font-size: 28rpx;
|
||||||
|
padding: 28rpx 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
762
src/components/SystemLog.vue
Normal file
762
src/components/SystemLog.vue
Normal file
@ -0,0 +1,762 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page-container system-log-container">
|
||||||
|
<!-- 页面头部 -->
|
||||||
|
<view class="page-header">
|
||||||
|
<view class="header-left">
|
||||||
|
<text class="page-title">日志</text>
|
||||||
|
</view>
|
||||||
|
<view class="header-right">
|
||||||
|
<view class="system-title">
|
||||||
|
<view class="system-title-icon">
|
||||||
|
<text class="icon">📋</text>
|
||||||
|
</view>
|
||||||
|
<text class="system-title-text">移动式检修车间系统</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 日志表格 -->
|
||||||
|
<view class="page-content">
|
||||||
|
<view class="log-content">
|
||||||
|
<view class="log-table">
|
||||||
|
<!-- 表格头部 -->
|
||||||
|
<view class="table-header">
|
||||||
|
<view class="table-cell header-cell event-column">事件</view>
|
||||||
|
<view class="table-cell header-cell time-column">时间</view>
|
||||||
|
<view class="table-cell header-cell status-column">状态</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 表格内容 -->
|
||||||
|
<scroll-view
|
||||||
|
class="table-body"
|
||||||
|
scroll-y="true"
|
||||||
|
:scroll-with-animation="true"
|
||||||
|
:scroll-top="scrollTop"
|
||||||
|
@scrolltolower="onScrollToLower"
|
||||||
|
@scroll="onScroll"
|
||||||
|
>
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<view class="table-loading-container" v-if="isLoading">
|
||||||
|
<view class="table-loading-spinner"></view>
|
||||||
|
<text class="table-loading-text">正在加载日志数据...</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 表格数据 -->
|
||||||
|
<template v-else>
|
||||||
|
<view
|
||||||
|
v-for="(log, index) in logList"
|
||||||
|
:key="index"
|
||||||
|
class="table-row"
|
||||||
|
:class="{ 'even-row': index % 2 === 0 }"
|
||||||
|
>
|
||||||
|
<view class="table-cell event-column">{{ log.event }}</view>
|
||||||
|
<view class="table-cell time-column">{{ log.time }}</view>
|
||||||
|
<view class="table-cell status-column" :class="getStatusClass(log.status)">
|
||||||
|
{{ log.status }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 空数据提示 -->
|
||||||
|
<view class="table-empty-container" v-if="!isLoading && logList.length === 0 && hasInitialized">
|
||||||
|
<text class="table-empty-text">暂无日志数据</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 初始状态提示 -->
|
||||||
|
<view class="table-empty-container" v-if="!isLoading && logList.length === 0 && !hasInitialized">
|
||||||
|
<text class="table-empty-text">暂无数据</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 底部间距,确保最后一条记录完全显示 -->
|
||||||
|
<view class="table-bottom-spacing"></view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue';
|
||||||
|
|
||||||
|
// 日志数据
|
||||||
|
const logList = ref([]);
|
||||||
|
const isLoading = ref(false);
|
||||||
|
const isConnected = ref(false);
|
||||||
|
const hasInitialized = ref(false);
|
||||||
|
|
||||||
|
// 滚动相关
|
||||||
|
const scrollTop = ref(0);
|
||||||
|
const isScrolling = ref(false);
|
||||||
|
|
||||||
|
// 移除空行占位逻辑,没有数据时只显示"暂无数据"
|
||||||
|
|
||||||
|
// MQTT日志服务接口(预留)
|
||||||
|
const mqttLogService = {
|
||||||
|
// 连接MQTT服务器
|
||||||
|
connect: async () => {
|
||||||
|
console.log('MQTT日志服务连接中...');
|
||||||
|
try {
|
||||||
|
// 模拟连接延迟
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
isConnected.value = true;
|
||||||
|
console.log('MQTT日志服务连接成功');
|
||||||
|
return Promise.resolve();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('MQTT日志连接失败:', error);
|
||||||
|
isConnected.value = false;
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 订阅日志数据
|
||||||
|
subscribeLogData: () => {
|
||||||
|
console.log('订阅系统日志数据');
|
||||||
|
// 这里后期会实现真实的MQTT日志订阅
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取历史日志
|
||||||
|
getHistoryLogs: async (limit = 50) => {
|
||||||
|
console.log(`获取历史日志,限制${limit}条`);
|
||||||
|
try {
|
||||||
|
isLoading.value = true;
|
||||||
|
hasInitialized.value = true;
|
||||||
|
// 模拟请求延迟
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 800));
|
||||||
|
|
||||||
|
// 模拟日志数据
|
||||||
|
const mockLogs = [
|
||||||
|
{
|
||||||
|
event: '开机',
|
||||||
|
time: '2025-9-3-12:01',
|
||||||
|
status: '正常'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
event: '报警',
|
||||||
|
time: '2025-9-3-12:01',
|
||||||
|
status: '异常'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
event: '系统启动',
|
||||||
|
time: '2025-9-3-11:58',
|
||||||
|
status: '正常'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
event: '温度检测',
|
||||||
|
time: '2025-9-3-11:55',
|
||||||
|
status: '正常'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
event: '湿度异常',
|
||||||
|
time: '2025-9-3-11:52',
|
||||||
|
status: '异常'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
event: '设备自检',
|
||||||
|
time: '2025-9-3-11:50',
|
||||||
|
status: '正常'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
event: '网络连接',
|
||||||
|
time: '2025-9-3-11:48',
|
||||||
|
status: '正常'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
event: '参数校准',
|
||||||
|
time: '2025-9-3-11:45',
|
||||||
|
status: '正常'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
event: '数据备份',
|
||||||
|
time: '2025-9-3-11:40',
|
||||||
|
status: '正常'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
event: '系统更新',
|
||||||
|
time: '2025-9-3-11:35',
|
||||||
|
status: '正常'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
event: '安全检查',
|
||||||
|
time: '2025-9-3-11:30',
|
||||||
|
status: '正常'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
event: '性能监控',
|
||||||
|
time: '2025-9-3-11:25',
|
||||||
|
status: '正常'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
event: '日志清理',
|
||||||
|
time: '2025-9-3-11:20',
|
||||||
|
status: '正常'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
event: '配置更新',
|
||||||
|
time: '2025-9-3-11:15',
|
||||||
|
status: '正常'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
event: '服务重启',
|
||||||
|
time: '2025-9-3-11:10',
|
||||||
|
status: '正常'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
event: '数据库连接',
|
||||||
|
time: '2025-9-3-11:05',
|
||||||
|
status: '正常'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
event: '缓存清理',
|
||||||
|
time: '2025-9-3-11:00',
|
||||||
|
status: '正常'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
event: '定时任务',
|
||||||
|
time: '2025-9-3-10:55',
|
||||||
|
status: '正常'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
event: '内存检查',
|
||||||
|
time: '2025-9-3-10:50',
|
||||||
|
status: '正常'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
event: '磁盘检查',
|
||||||
|
time: '2025-9-3-10:45',
|
||||||
|
status: '正常'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
logList.value = mockLogs;
|
||||||
|
isLoading.value = false;
|
||||||
|
return Promise.resolve(mockLogs);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取历史日志失败:', error);
|
||||||
|
isLoading.value = false;
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取实时日志
|
||||||
|
getRealtimeLogs: async () => {
|
||||||
|
console.log('获取实时日志');
|
||||||
|
try {
|
||||||
|
// 模拟实时日志数据
|
||||||
|
const events = ['温度监测', '湿度检测', '设备巡检', '数据同步', '状态上报'];
|
||||||
|
const statuses = ['正常', '异常', '警告'];
|
||||||
|
|
||||||
|
const newLog = {
|
||||||
|
event: events[Math.floor(Math.random() * events.length)],
|
||||||
|
time: new Date().toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'numeric',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
}).replace(/\//g, '-').replace(', ', '-'),
|
||||||
|
status: statuses[Math.floor(Math.random() * statuses.length)]
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加到日志列表顶部
|
||||||
|
logList.value.unshift(newLog);
|
||||||
|
|
||||||
|
// 限制日志数量,保持最新的50条
|
||||||
|
if (logList.value.length > 50) {
|
||||||
|
logList.value = logList.value.slice(0, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动滚动到顶部显示最新日志
|
||||||
|
setTimeout(() => {
|
||||||
|
scrollToTop();
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
return Promise.resolve(newLog);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取实时日志失败:', error);
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 清空日志
|
||||||
|
clearLogs: async () => {
|
||||||
|
console.log('清空日志');
|
||||||
|
try {
|
||||||
|
// 模拟清空操作
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 300));
|
||||||
|
logList.value = [];
|
||||||
|
uni.showToast({
|
||||||
|
title: '日志已清空',
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
|
return Promise.resolve();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('清空日志失败:', error);
|
||||||
|
uni.showToast({
|
||||||
|
title: '清空失败',
|
||||||
|
icon: 'error'
|
||||||
|
});
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 断开连接
|
||||||
|
disconnect: () => {
|
||||||
|
console.log('MQTT日志服务断开连接');
|
||||||
|
isConnected.value = false;
|
||||||
|
// 这里后期会实现真实的MQTT断开
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取状态样式类
|
||||||
|
const getStatusClass = (status) => {
|
||||||
|
switch (status) {
|
||||||
|
case '正常':
|
||||||
|
return 'status-normal';
|
||||||
|
case '异常':
|
||||||
|
return 'status-error';
|
||||||
|
case '警告':
|
||||||
|
return 'status-warning';
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 滚动事件处理
|
||||||
|
let scrollTimer = null;
|
||||||
|
|
||||||
|
const onScroll = (e) => {
|
||||||
|
isScrolling.value = true;
|
||||||
|
|
||||||
|
// 防抖处理,避免频繁触发
|
||||||
|
if (scrollTimer) {
|
||||||
|
clearTimeout(scrollTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollTimer = setTimeout(() => {
|
||||||
|
isScrolling.value = false;
|
||||||
|
}, 150);
|
||||||
|
|
||||||
|
// 可以在这里添加滚动时的逻辑
|
||||||
|
};
|
||||||
|
|
||||||
|
const onScrollToLower = () => {
|
||||||
|
console.log('滚动到底部');
|
||||||
|
// 可以在这里添加加载更多数据的逻辑
|
||||||
|
};
|
||||||
|
|
||||||
|
// 滚动到顶部
|
||||||
|
const scrollToTop = () => {
|
||||||
|
scrollTop.value = scrollTop.value === 0 ? 1 : 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 滚动到底部
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
// 使用nextTick确保DOM更新完成
|
||||||
|
nextTick(() => {
|
||||||
|
// 计算滚动到底部的位置
|
||||||
|
const scrollHeight = logList.value.length * 80; // 假设每行80rpx
|
||||||
|
scrollTop.value = scrollHeight + 100; // 额外100rpx确保完全滚动到底部
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 定时获取实时日志
|
||||||
|
let realtimeTimer = null;
|
||||||
|
|
||||||
|
const startRealtimeLog = () => {
|
||||||
|
if (realtimeTimer) return;
|
||||||
|
|
||||||
|
realtimeTimer = setInterval(() => {
|
||||||
|
if (isConnected.value && !isLoading.value) {
|
||||||
|
// 30%概率生成新日志
|
||||||
|
if (Math.random() < 0.3) {
|
||||||
|
mqttLogService.getRealtimeLogs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 5000); // 每5秒检查一次
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopRealtimeLog = () => {
|
||||||
|
if (realtimeTimer) {
|
||||||
|
clearInterval(realtimeTimer);
|
||||||
|
realtimeTimer = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 组件生命周期
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
// 连接MQTT并初始化
|
||||||
|
await mqttLogService.connect();
|
||||||
|
await mqttLogService.subscribeLogData();
|
||||||
|
await mqttLogService.getHistoryLogs();
|
||||||
|
|
||||||
|
// 开始实时日志获取
|
||||||
|
startRealtimeLog();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('日志系统初始化失败:', error);
|
||||||
|
uni.showToast({
|
||||||
|
title: '连接失败',
|
||||||
|
icon: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopRealtimeLog();
|
||||||
|
mqttLogService.disconnect();
|
||||||
|
|
||||||
|
// 清理滚动定时器
|
||||||
|
if (scrollTimer) {
|
||||||
|
clearTimeout(scrollTimer);
|
||||||
|
scrollTimer = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '@/styles/common.scss';
|
||||||
|
|
||||||
|
.system-log-container {
|
||||||
|
// 继承通用页面容器样式
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-content {
|
||||||
|
// 继承通用内容样式
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-table {
|
||||||
|
// 继承通用表格样式
|
||||||
|
@extend .common-table;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-header {
|
||||||
|
display: flex;
|
||||||
|
background: #f8f9fa;
|
||||||
|
color: #3c4043;
|
||||||
|
border-bottom: 1rpx solid #e8eaed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-cell {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 26rpx;
|
||||||
|
padding: 20rpx 16rpx;
|
||||||
|
text-align: center;
|
||||||
|
letter-spacing: 0.2rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-body {
|
||||||
|
flex: 1;
|
||||||
|
height: 0; /* 重要:配合flex: 1使用,确保正确计算高度 */
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
/* 滚动条样式 */
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #dadce0 #f1f3f4;
|
||||||
|
/* 平滑滚动 */
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Webkit浏览器滚动条样式 */
|
||||||
|
.table-body::-webkit-scrollbar {
|
||||||
|
width: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-body::-webkit-scrollbar-track {
|
||||||
|
background: rgba(241, 243, 244, 0.5);
|
||||||
|
border-radius: 4rpx;
|
||||||
|
margin: 4rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-body::-webkit-scrollbar-thumb {
|
||||||
|
background: linear-gradient(180deg, #dadce0 0%, #bdc1c6 100%);
|
||||||
|
border-radius: 4rpx;
|
||||||
|
border: 1rpx solid rgba(255, 255, 255, 0.2);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-body::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: linear-gradient(180deg, #bdc1c6 0%, #9aa0a6 100%);
|
||||||
|
box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-body::-webkit-scrollbar-thumb:active {
|
||||||
|
background: linear-gradient(180deg, #9aa0a6 0%, #5f6368 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row {
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 1rpx solid #e8eaed;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
min-height: 80rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row.even-row {
|
||||||
|
background-color: rgba(248, 249, 250, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row:hover {
|
||||||
|
background-color: rgba(241, 243, 244, 0.8);
|
||||||
|
transform: translateX(2rpx);
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-cell {
|
||||||
|
padding: 16rpx 16rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #3c4043;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
min-height: 80rpx;
|
||||||
|
line-height: 1.4;
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-all;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-column {
|
||||||
|
flex: 2;
|
||||||
|
justify-content: flex-start;
|
||||||
|
text-align: left;
|
||||||
|
padding-left: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-column {
|
||||||
|
flex: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-column {
|
||||||
|
flex: 1;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-normal {
|
||||||
|
color: #34a853;
|
||||||
|
font-weight: 600;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-normal::before {
|
||||||
|
content: '●';
|
||||||
|
color: #34a853;
|
||||||
|
margin-right: 8rpx;
|
||||||
|
font-size: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-error {
|
||||||
|
color: #ea4335;
|
||||||
|
font-weight: 600;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-error::before {
|
||||||
|
content: '●';
|
||||||
|
color: #ea4335;
|
||||||
|
margin-right: 8rpx;
|
||||||
|
font-size: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-warning {
|
||||||
|
color: #fbbc04;
|
||||||
|
font-weight: 600;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-warning::before {
|
||||||
|
content: '●';
|
||||||
|
color: #fbbc04;
|
||||||
|
margin-right: 8rpx;
|
||||||
|
font-size: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移除空行样式,不再需要 */
|
||||||
|
|
||||||
|
.table-bottom-spacing {
|
||||||
|
height: 40rpx;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-loading-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 60rpx 20rpx;
|
||||||
|
gap: 20rpx;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-loading-spinner {
|
||||||
|
width: 48rpx;
|
||||||
|
height: 48rpx;
|
||||||
|
border: 3rpx solid #e8eaed;
|
||||||
|
border-top: 3rpx solid #5f6368;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-loading-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #5f6368;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-empty-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 80rpx;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-empty-text {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #9aa0a6;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-loading-spinner {
|
||||||
|
width: 60rpx;
|
||||||
|
height: 60rpx;
|
||||||
|
border: 4rpx solid #e9ecef;
|
||||||
|
border-top: 4rpx solid #6c757d;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-loading-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #6c757d;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-empty-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 100rpx 20rpx;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-empty-text {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #999;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 60rpx;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 60rpx;
|
||||||
|
height: 60rpx;
|
||||||
|
border: 4rpx solid rgba(255, 152, 0, 0.3);
|
||||||
|
border-top: 4rpx solid #ff9800;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 100rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #adb5bd;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计 - 平板设备适配
|
||||||
|
@media (min-width: 768px) and (max-width: 1024px) {
|
||||||
|
.header-cell {
|
||||||
|
font-size: 28rpx;
|
||||||
|
padding: 24rpx 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-cell {
|
||||||
|
font-size: 26rpx;
|
||||||
|
padding: 20rpx 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计 - 手机设备适配
|
||||||
|
@media (max-width: 750rpx) {
|
||||||
|
.header-cell {
|
||||||
|
font-size: 22rpx;
|
||||||
|
padding: 16rpx 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-cell {
|
||||||
|
font-size: 20rpx;
|
||||||
|
padding: 12rpx 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计 - 大屏设备适配
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.header-cell {
|
||||||
|
font-size: 30rpx;
|
||||||
|
padding: 28rpx 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-cell {
|
||||||
|
font-size: 28rpx;
|
||||||
|
padding: 24rpx 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
font-size: 30rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
488
src/components/VisualMonitoring.vue
Normal file
488
src/components/VisualMonitoring.vue
Normal file
@ -0,0 +1,488 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page-container visual-monitoring-container">
|
||||||
|
<!-- 页面头部 -->
|
||||||
|
<view class="page-header">
|
||||||
|
<view class="header-left">
|
||||||
|
<text class="page-title">视觉监控</text>
|
||||||
|
</view>
|
||||||
|
<view class="header-right">
|
||||||
|
<view class="system-title">
|
||||||
|
<view class="system-title-icon">
|
||||||
|
<text class="icon">📋</text>
|
||||||
|
</view>
|
||||||
|
<text class="system-title-text">移动式检修车间系统</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 监控画面区域 -->
|
||||||
|
<view class="page-content">
|
||||||
|
<view class="monitoring-content">
|
||||||
|
<view class="c">
|
||||||
|
<image
|
||||||
|
class="camera-image"
|
||||||
|
:src="currentImage"
|
||||||
|
mode="aspectFill"
|
||||||
|
@load="onImageLoad"
|
||||||
|
@error="onImageError"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<view class="loading-overlay" v-if="isLoading">
|
||||||
|
<view class="loading-spinner"></view>
|
||||||
|
<text class="loading-text">加载中...</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 连接状态指示器 -->
|
||||||
|
<view class="connection-status" :class="{ connected: isConnected }">
|
||||||
|
<view class="status-dot"></view>
|
||||||
|
<text class="status-text">{{ isConnected ? '已连接' : '未连接' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 底部控制按钮 -->
|
||||||
|
<view class="control-panel">
|
||||||
|
<button
|
||||||
|
class="control-button on-button"
|
||||||
|
:class="{ active: isDeviceOn }"
|
||||||
|
@click="toggleDevice(true)"
|
||||||
|
:disabled="isControlling"
|
||||||
|
>
|
||||||
|
开
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="control-button off-button"
|
||||||
|
:class="{ active: !isDeviceOn }"
|
||||||
|
@click="toggleDevice(false)"
|
||||||
|
:disabled="isControlling"
|
||||||
|
>
|
||||||
|
关
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onUnmounted } from 'vue';
|
||||||
|
|
||||||
|
// 页面状态
|
||||||
|
const currentImage = ref('/static/inspection-room.jpg');
|
||||||
|
const isLoading = ref(false);
|
||||||
|
const isConnected = ref(false);
|
||||||
|
const isDeviceOn = ref(false);
|
||||||
|
const isControlling = ref(false);
|
||||||
|
|
||||||
|
// MQTT服务接口(预留)
|
||||||
|
const mqttService = {
|
||||||
|
// 连接MQTT服务器
|
||||||
|
connect: async () => {
|
||||||
|
console.log('MQTT视觉监控连接中...');
|
||||||
|
try {
|
||||||
|
// 模拟连接延迟
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
isConnected.value = true;
|
||||||
|
console.log('MQTT视觉监控连接成功');
|
||||||
|
return Promise.resolve();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('MQTT连接失败:', error);
|
||||||
|
isConnected.value = false;
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 订阅图像数据
|
||||||
|
subscribeImageData: () => {
|
||||||
|
console.log('订阅视觉监控图像数据');
|
||||||
|
// 这里后期会实现真实的MQTT图像订阅
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取实时图像
|
||||||
|
getRealtimeImage: async () => {
|
||||||
|
console.log('获取实时监控图像');
|
||||||
|
try {
|
||||||
|
// 模拟图像请求
|
||||||
|
isLoading.value = true;
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
|
||||||
|
// 模拟返回不同的图像
|
||||||
|
const images = [
|
||||||
|
'/static/inspection-room.jpg',
|
||||||
|
'/static/camera-placeholder.jpg'
|
||||||
|
];
|
||||||
|
const randomImage = images[Math.floor(Math.random() * images.length)];
|
||||||
|
currentImage.value = randomImage;
|
||||||
|
|
||||||
|
isLoading.value = false;
|
||||||
|
return Promise.resolve(randomImage);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取图像失败:', error);
|
||||||
|
isLoading.value = false;
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 发送设备控制指令
|
||||||
|
sendDeviceControl: async (command) => {
|
||||||
|
console.log(`发送设备控制指令: ${command}`);
|
||||||
|
try {
|
||||||
|
isControlling.value = true;
|
||||||
|
// 模拟控制指令发送延迟
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 800));
|
||||||
|
|
||||||
|
// 更新设备状态
|
||||||
|
isDeviceOn.value = command === 'on';
|
||||||
|
isControlling.value = false;
|
||||||
|
|
||||||
|
uni.showToast({
|
||||||
|
title: `设备已${command === 'on' ? '开启' : '关闭'}`,
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('设备控制失败:', error);
|
||||||
|
isControlling.value = false;
|
||||||
|
uni.showToast({
|
||||||
|
title: '控制失败',
|
||||||
|
icon: 'error'
|
||||||
|
});
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取设备状态
|
||||||
|
getDeviceStatus: async () => {
|
||||||
|
console.log('获取设备状态');
|
||||||
|
try {
|
||||||
|
// 模拟获取设备状态
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 300));
|
||||||
|
const status = Math.random() > 0.5; // 随机状态
|
||||||
|
isDeviceOn.value = status;
|
||||||
|
return Promise.resolve(status);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取设备状态失败:', error);
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 断开连接
|
||||||
|
disconnect: () => {
|
||||||
|
console.log('MQTT视觉监控断开连接');
|
||||||
|
isConnected.value = false;
|
||||||
|
// 这里后期会实现真实的MQTT断开
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 设备控制
|
||||||
|
const toggleDevice = async (turnOn) => {
|
||||||
|
if (isControlling.value || !isConnected.value) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await mqttService.sendDeviceControl(turnOn ? 'on' : 'off');
|
||||||
|
// 控制成功后刷新图像
|
||||||
|
setTimeout(() => {
|
||||||
|
mqttService.getRealtimeImage();
|
||||||
|
}, 1000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('设备控制失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 图像加载事件
|
||||||
|
const onImageLoad = () => {
|
||||||
|
console.log('图像加载成功');
|
||||||
|
isLoading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onImageError = () => {
|
||||||
|
console.error('图像加载失败');
|
||||||
|
isLoading.value = false;
|
||||||
|
uni.showToast({
|
||||||
|
title: '图像加载失败',
|
||||||
|
icon: 'error'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 定时刷新图像
|
||||||
|
let imageRefreshTimer = null;
|
||||||
|
|
||||||
|
const startImageRefresh = () => {
|
||||||
|
if (imageRefreshTimer) return;
|
||||||
|
|
||||||
|
imageRefreshTimer = setInterval(() => {
|
||||||
|
if (isConnected.value && !isLoading.value) {
|
||||||
|
mqttService.getRealtimeImage();
|
||||||
|
}
|
||||||
|
}, 5000); // 每5秒刷新一次图像
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopImageRefresh = () => {
|
||||||
|
if (imageRefreshTimer) {
|
||||||
|
clearInterval(imageRefreshTimer);
|
||||||
|
imageRefreshTimer = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 组件生命周期
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
// 连接MQTT并初始化
|
||||||
|
await mqttService.connect();
|
||||||
|
await mqttService.subscribeImageData();
|
||||||
|
await mqttService.getDeviceStatus();
|
||||||
|
await mqttService.getRealtimeImage();
|
||||||
|
|
||||||
|
// 开始定时刷新图像
|
||||||
|
startImageRefresh();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('视觉监控初始化失败:', error);
|
||||||
|
uni.showToast({
|
||||||
|
title: '连接失败',
|
||||||
|
icon: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopImageRefresh();
|
||||||
|
mqttService.disconnect();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '@/styles/common.scss';
|
||||||
|
|
||||||
|
.visual-monitoring-container {
|
||||||
|
// 继承通用页面容器样式
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitoring-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 500rpx;
|
||||||
|
/* 滚动条样式 */
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #c1c1c1 #f1f1f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Webkit浏览器滚动条样式 */
|
||||||
|
.monitoring-content::-webkit-scrollbar {
|
||||||
|
width: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitoring-content::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitoring-content::-webkit-scrollbar-thumb {
|
||||||
|
background: #c1c1c1;
|
||||||
|
border-radius: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitoring-content::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #a8a8a8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
// margin: 0 30rpx;
|
||||||
|
background-color: #000000;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 60rpx;
|
||||||
|
height: 60rpx;
|
||||||
|
border: 4rpx solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-top: 4rpx solid #ffffff;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
color: white;
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-status {
|
||||||
|
position: absolute;
|
||||||
|
top: 20rpx;
|
||||||
|
right: 20rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
padding: 8rpx 16rpx;
|
||||||
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
|
border-radius: 20rpx;
|
||||||
|
backdrop-filter: blur(10rpx);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
width: 12rpx;
|
||||||
|
height: 12rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #ff4444;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-status.connected .status-dot {
|
||||||
|
background-color: #44ff44;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
color: white;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-panel {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 24rpx;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 24rpx;
|
||||||
|
padding: 16rpx 24rpx;
|
||||||
|
background-color: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 8rpx;
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||||
|
backdrop-filter: blur(10rpx);
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-button {
|
||||||
|
width: 120rpx;
|
||||||
|
height: 60rpx;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: white;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 1rpx 3rpx rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-button::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), transparent);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-button:hover::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.on-button {
|
||||||
|
background: #1a73e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.on-button.active {
|
||||||
|
background: #1557b0;
|
||||||
|
box-shadow: 0 2rpx 6rpx rgba(26, 115, 232, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.off-button {
|
||||||
|
background: #5f6368;
|
||||||
|
}
|
||||||
|
|
||||||
|
.off-button.active {
|
||||||
|
background: #3c4043;
|
||||||
|
box-shadow: 0 2rpx 6rpx rgba(95, 99, 104, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-button:not(:disabled):active {
|
||||||
|
transform: translateY(1rpx);
|
||||||
|
box-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计 - 平板设备适配
|
||||||
|
@media (min-width: 768px) and (max-width: 1024px) {
|
||||||
|
.camera-image {
|
||||||
|
min-height: 500rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-button {
|
||||||
|
font-size: 26rpx;
|
||||||
|
width: 140rpx;
|
||||||
|
height: 70rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计 - 手机设备适配
|
||||||
|
@media (max-width: 750rpx) {
|
||||||
|
.camera-image {
|
||||||
|
min-height: 400rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-button {
|
||||||
|
font-size: 20rpx;
|
||||||
|
width: 100rpx;
|
||||||
|
height: 50rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计 - 大屏设备适配
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.camera-image {
|
||||||
|
min-height: 600rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-button {
|
||||||
|
font-size: 28rpx;
|
||||||
|
width: 160rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
59
src/config/mqtt.js
Normal file
59
src/config/mqtt.js
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
// MQTT配置文件
|
||||||
|
export const MQTT_CONFIG = {
|
||||||
|
// EMQX服务器地址
|
||||||
|
broker: 'ws://122.51.194.184:8083/mqtt', // WebSocket MQTT端口
|
||||||
|
// broker: 'mqtt://122.51.194.184:1883', // 标准MQTT端口
|
||||||
|
|
||||||
|
// 连接选项
|
||||||
|
options: {
|
||||||
|
clientId: 'mobile-inspection-system-' + Math.random().toString(16).substr(2, 8),
|
||||||
|
// 暂时不设置账号密码
|
||||||
|
// username: '',
|
||||||
|
// password: '',
|
||||||
|
keepalive: 60,
|
||||||
|
clean: true,
|
||||||
|
reconnectPeriod: 5000,
|
||||||
|
connectTimeout: 30 * 1000,
|
||||||
|
},
|
||||||
|
|
||||||
|
// 主题配置
|
||||||
|
topics: {
|
||||||
|
// 设备数据主题
|
||||||
|
deviceData: 'hdydcj_01_UP',
|
||||||
|
|
||||||
|
// 设备类型
|
||||||
|
deviceTypes: {
|
||||||
|
WSD: '温湿度', // 温湿度传感器
|
||||||
|
AC: '空调', // 空调设备
|
||||||
|
PM: 'PM2.5', // PM2.5传感器
|
||||||
|
// 可以根据需要添加更多设备类型
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数据解析工具
|
||||||
|
export const DataParser = {
|
||||||
|
// 解析设备数据
|
||||||
|
parseDeviceData(rawData) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(rawData)
|
||||||
|
if (Array.isArray(data) && data.length > 0) {
|
||||||
|
return data[0] // 取第一个设备数据
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析设备数据失败:', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取设备类型名称
|
||||||
|
getDeviceTypeName(deviceCode) {
|
||||||
|
return MQTT_CONFIG.topics.deviceTypes[deviceCode] || deviceCode
|
||||||
|
},
|
||||||
|
|
||||||
|
// 转换Unix时间戳为可读时间
|
||||||
|
formatTimestamp(timestamp) {
|
||||||
|
return new Date(timestamp * 1000).toLocaleString('zh-CN')
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/main.js
Normal file
10
src/main.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import {
|
||||||
|
createSSRApp
|
||||||
|
} from "vue";
|
||||||
|
import App from "./App.vue";
|
||||||
|
export function createApp() {
|
||||||
|
const app = createSSRApp(App);
|
||||||
|
return {
|
||||||
|
app,
|
||||||
|
};
|
||||||
|
}
|
||||||
78
src/manifest.json
Normal file
78
src/manifest.json
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
{
|
||||||
|
"name" : "pad-app",
|
||||||
|
"appid" : "__UNI__5F601D3",
|
||||||
|
"description" : "",
|
||||||
|
"versionName" : "1.0.0",
|
||||||
|
"versionCode" : "100",
|
||||||
|
"transformPx" : false,
|
||||||
|
/* 5+App特有相关 */
|
||||||
|
"app-plus" : {
|
||||||
|
"usingComponents" : true,
|
||||||
|
"nvueStyleCompiler" : "uni-app",
|
||||||
|
"compilerVersion" : 3,
|
||||||
|
"orientation" : "landscape",
|
||||||
|
"splashscreen" : {
|
||||||
|
"alwaysShowBeforeRender" : true,
|
||||||
|
"waiting" : true,
|
||||||
|
"autoclose" : true,
|
||||||
|
"delay" : 0
|
||||||
|
},
|
||||||
|
/* 模块配置 */
|
||||||
|
"modules" : {},
|
||||||
|
/* 应用发布信息 */
|
||||||
|
"distribute" : {
|
||||||
|
/* android打包配置 */
|
||||||
|
"android" : {
|
||||||
|
"permissions" : [
|
||||||
|
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
||||||
|
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
||||||
|
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
|
||||||
|
],
|
||||||
|
"abiFilters" : [ "armeabi-v7a", "arm64-v8a", "x86" ],
|
||||||
|
"screenOrientation" : "landscape"
|
||||||
|
},
|
||||||
|
/* ios打包配置 */
|
||||||
|
"ios" : {
|
||||||
|
"dSYMs" : false,
|
||||||
|
"screenOrientation" : "landscape"
|
||||||
|
},
|
||||||
|
/* SDK配置 */
|
||||||
|
"sdkConfigs" : {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/* 快应用特有相关 */
|
||||||
|
"quickapp" : {},
|
||||||
|
/* 小程序特有相关 */
|
||||||
|
"mp-weixin" : {
|
||||||
|
"appid" : "",
|
||||||
|
"setting" : {
|
||||||
|
"urlCheck" : false
|
||||||
|
},
|
||||||
|
"usingComponents" : true
|
||||||
|
},
|
||||||
|
"mp-alipay" : {
|
||||||
|
"usingComponents" : true
|
||||||
|
},
|
||||||
|
"mp-baidu" : {
|
||||||
|
"usingComponents" : true
|
||||||
|
},
|
||||||
|
"mp-toutiao" : {
|
||||||
|
"usingComponents" : true
|
||||||
|
},
|
||||||
|
"uniStatistics" : {
|
||||||
|
"enable" : false
|
||||||
|
},
|
||||||
|
"vueVersion" : "3"
|
||||||
|
}
|
||||||
30
src/pages.json
Normal file
30
src/pages.json
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"pages": [
|
||||||
|
{
|
||||||
|
"path": "pages/index/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "检修系统首页"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/system/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "检修系统",
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"orientation": "landscape"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"globalStyle": {
|
||||||
|
"navigationBarTextStyle": "white",
|
||||||
|
"navigationBarTitleText": "移动式检修车间系统",
|
||||||
|
"navigationBarBackgroundColor": "#3f51b5",
|
||||||
|
"backgroundColor": "#F8F8F8"
|
||||||
|
},
|
||||||
|
"easycom": {
|
||||||
|
"autoscan": true,
|
||||||
|
"custom": {
|
||||||
|
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
73
src/pages/index/index.vue
Normal file
73
src/pages/index/index.vue
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
<template>
|
||||||
|
<view class="content">
|
||||||
|
<image class="logo" src="/static/logo.png"></image>
|
||||||
|
<view class="text-area">
|
||||||
|
<text class="title">{{ title }}</text>
|
||||||
|
</view>
|
||||||
|
<button class="nav-button" @click="navigateToSystem">进入检修系统</button>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
title: 'Hello',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLoad() {},
|
||||||
|
methods: {
|
||||||
|
navigateToSystem() {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '../system/index',
|
||||||
|
fail: (err) => {
|
||||||
|
console.error('导航失败:', err);
|
||||||
|
// 尝试使用替代方法
|
||||||
|
uni.redirectTo({
|
||||||
|
url: '../system/index'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 200rpx;
|
||||||
|
width: 200rpx;
|
||||||
|
margin-top: 200rpx;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
margin-bottom: 50rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-area {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 50rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 36rpx;
|
||||||
|
color: #8f8f94;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-button {
|
||||||
|
background-color: #3f51b5;
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 20rpx 40rpx;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
font-size: 32rpx;
|
||||||
|
margin-top: 50rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
163
src/pages/system/index.vue
Normal file
163
src/pages/system/index.vue
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
<template>
|
||||||
|
<view class="system-container">
|
||||||
|
<!-- 左侧菜单栏 -->
|
||||||
|
<SideMenu
|
||||||
|
:activeMenuIndex="activeMenuIndex"
|
||||||
|
@update:activeMenuIndex="updateActiveMenu"
|
||||||
|
@showAlert="showAlert"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 右侧内容区 -->
|
||||||
|
<view class="content-area">
|
||||||
|
|
||||||
|
<!-- 内容区域 -->
|
||||||
|
<view class="main-content">
|
||||||
|
<!-- <text class="content-title">{{ currentMenu.name }}</text> -->
|
||||||
|
|
||||||
|
<!-- 根据选中的菜单显示不同内容 -->
|
||||||
|
<EnvironmentParams v-if="activeMenuIndex === 0" @openSettings="openSettings" />
|
||||||
|
<ParameterRecord v-else-if="activeMenuIndex === 1" />
|
||||||
|
<VisualMonitoring v-else-if="activeMenuIndex === 2" />
|
||||||
|
<SystemLog v-else-if="activeMenuIndex === 3" />
|
||||||
|
<AlarmRecord v-else-if="activeMenuIndex === 4" />
|
||||||
|
<PlaceholderContent v-else :title="currentMenu.name" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 设置弹窗 -->
|
||||||
|
<SettingsModal
|
||||||
|
v-model:visible="showSettingsModal"
|
||||||
|
@save="saveSettings"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue';
|
||||||
|
import SideMenu from '@/components/SideMenu.vue';
|
||||||
|
import EnvironmentParams from '@/components/EnvironmentParams.vue';
|
||||||
|
import ParameterRecord from '@/components/ParameterRecord.vue';
|
||||||
|
import VisualMonitoring from '@/components/VisualMonitoring.vue';
|
||||||
|
import SystemLog from '@/components/SystemLog.vue';
|
||||||
|
import AlarmRecord from '@/components/AlarmRecord.vue';
|
||||||
|
import SettingsModal from '@/components/SettingsModal.vue';
|
||||||
|
import PlaceholderContent from '@/components/PlaceholderContent.vue';
|
||||||
|
|
||||||
|
// 菜单列表
|
||||||
|
const menuList = ref([
|
||||||
|
{ name: '现场环境参数' },
|
||||||
|
{ name: '参数记录' },
|
||||||
|
{ name: '视觉监控' },
|
||||||
|
{ name: '日志' },
|
||||||
|
{ name: '报警' }
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 当前选中的菜单索引
|
||||||
|
const activeMenuIndex = ref(0);
|
||||||
|
|
||||||
|
// 当前菜单
|
||||||
|
const currentMenu = computed(() => menuList.value[activeMenuIndex.value]);
|
||||||
|
|
||||||
|
// 不再需要动态组件,直接使用v-if/v-else进行条件渲染
|
||||||
|
|
||||||
|
// 更新当前选中的菜单
|
||||||
|
const updateActiveMenu = (index) => {
|
||||||
|
activeMenuIndex.value = index;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 显示报警信息
|
||||||
|
const showAlert = () => {
|
||||||
|
uni.showToast({
|
||||||
|
title: '暂无报警项目',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 设置弹窗状态
|
||||||
|
const showSettingsModal = ref(false);
|
||||||
|
|
||||||
|
// 打开设置弹窗
|
||||||
|
const openSettings = () => {
|
||||||
|
showSettingsModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存设置
|
||||||
|
const saveSettings = (settings) => {
|
||||||
|
console.log('保存的设置:', settings);
|
||||||
|
// 这里可以添加保存设置的逻辑
|
||||||
|
};
|
||||||
|
|
||||||
|
// 页面加载时强制横屏
|
||||||
|
onMounted(() => {
|
||||||
|
setOrientation();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 设置横屏
|
||||||
|
const setOrientation = () => {
|
||||||
|
try {
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
plus.screen.lockOrientation('landscape-primary');
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifdef H5
|
||||||
|
// H5环境下的横屏设置
|
||||||
|
if (screen.orientation && screen.orientation.lock) {
|
||||||
|
screen.orientation.lock('landscape').catch(err => {
|
||||||
|
console.log('横屏锁定失败:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
} catch (error) {
|
||||||
|
console.log('设置横屏失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.system-container {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 右侧内容区样式 */
|
||||||
|
.content-area {
|
||||||
|
flex: 1;
|
||||||
|
padding: 30rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-icon {
|
||||||
|
width: 80rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主内容区域 */
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
height: calc(100% - 140rpx);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
6
src/shime-uni.d.ts
vendored
Normal file
6
src/shime-uni.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export {};
|
||||||
|
|
||||||
|
declare module "vue" {
|
||||||
|
type Hooks = App.AppInstance & Page.PageInstance;
|
||||||
|
interface ComponentCustomOptions extends Hooks {}
|
||||||
|
}
|
||||||
1
src/static/camera-placeholder.jpg
Normal file
1
src/static/camera-placeholder.jpg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<!-- 这是一个占位符文件,实际项目中需要替换为真实的图片文件 -->
|
||||||
1
src/static/icons/camera.png
Normal file
1
src/static/icons/camera.png
Normal file
@ -0,0 +1 @@
|
|||||||
|
<!-- 摄像头图标占位符 -->
|
||||||
1
src/static/icons/cleanliness.png
Normal file
1
src/static/icons/cleanliness.png
Normal file
@ -0,0 +1 @@
|
|||||||
|
<!-- 洁净度图标占位符 -->
|
||||||
1
src/static/icons/humidity.png
Normal file
1
src/static/icons/humidity.png
Normal file
@ -0,0 +1 @@
|
|||||||
|
<!-- 湿度图标占位符 -->
|
||||||
1
src/static/icons/settings.png
Normal file
1
src/static/icons/settings.png
Normal file
@ -0,0 +1 @@
|
|||||||
|
<!-- 设置图标占位符 -->
|
||||||
1
src/static/icons/temperature.png
Normal file
1
src/static/icons/temperature.png
Normal file
@ -0,0 +1 @@
|
|||||||
|
<!-- 温度图标占位符 -->
|
||||||
1
src/static/inspection-room.jpg
Normal file
1
src/static/inspection-room.jpg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<!-- 检修车间图片占位符 -->
|
||||||
BIN
src/static/logo.png
Normal file
BIN
src/static/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
416
src/styles/common.scss
Normal file
416
src/styles/common.scss
Normal file
@ -0,0 +1,416 @@
|
|||||||
|
// 全局页面通用样式 - 后台管理系统风格
|
||||||
|
// 统一的页面容器样式
|
||||||
|
.page-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background-color: #f5f6fa;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一的页面头部样式
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 24rpx 32rpx;
|
||||||
|
margin: 0;
|
||||||
|
min-height: 88rpx;
|
||||||
|
height: 88rpx;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-bottom: 1rpx solid #e8eaed;
|
||||||
|
box-shadow: 0 1rpx 3rpx rgba(0, 0, 0, 0.1);
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
max-width: 45%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1a1a1a;
|
||||||
|
letter-spacing: 0.3rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
max-width: 50%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一的系统标题样式
|
||||||
|
.system-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-title-icon {
|
||||||
|
width: 28rpx;
|
||||||
|
height: 28rpx;
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-title-icon .icon {
|
||||||
|
font-size: 16rpx;
|
||||||
|
color: #5f6368;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-title-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #3c4043;
|
||||||
|
letter-spacing: 0.3rpx;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 280rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一的页面内容区域样式
|
||||||
|
.page-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 24rpx 0;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一的卡片容器样式
|
||||||
|
.content-card {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
box-shadow: 0 1rpx 3rpx rgba(0, 0, 0, 0.1);
|
||||||
|
border: 1rpx solid #e8eaed;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一的表格样式
|
||||||
|
.common-table {
|
||||||
|
flex: 1;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
box-shadow: 0 1rpx 3rpx rgba(0, 0, 0, 0.1);
|
||||||
|
border: 1rpx solid #e8eaed;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-header {
|
||||||
|
display: flex;
|
||||||
|
background: #f8f9fa;
|
||||||
|
color: #3c4043;
|
||||||
|
border-bottom: 1rpx solid #e8eaed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-cell {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 26rpx;
|
||||||
|
padding: 20rpx 16rpx;
|
||||||
|
text-align: center;
|
||||||
|
letter-spacing: 0.2rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
/* 滚动条样式 */
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #dadce0 #f1f3f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Webkit浏览器滚动条样式 */
|
||||||
|
.table-body::-webkit-scrollbar {
|
||||||
|
width: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-body::-webkit-scrollbar-track {
|
||||||
|
background: #f1f3f4;
|
||||||
|
border-radius: 3rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-body::-webkit-scrollbar-thumb {
|
||||||
|
background: #dadce0;
|
||||||
|
border-radius: 3rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-body::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #bdc1c6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row {
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 1rpx solid #e8eaed;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row.even-row {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row:hover {
|
||||||
|
background-color: #f1f3f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-cell {
|
||||||
|
padding: 16rpx 16rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #3c4043;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
min-height: 80rpx;
|
||||||
|
line-height: 1.4;
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一的加载状态样式
|
||||||
|
.loading-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 60rpx;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 48rpx;
|
||||||
|
height: 48rpx;
|
||||||
|
border: 3rpx solid #e8eaed;
|
||||||
|
border-top: 3rpx solid #5f6368;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #5f6368;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一的空状态样式
|
||||||
|
.empty-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 80rpx;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #9aa0a6;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一的按钮样式
|
||||||
|
.common-button {
|
||||||
|
padding: 16rpx 32rpx;
|
||||||
|
border-radius: 6rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: center;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-button {
|
||||||
|
background: #1a73e8;
|
||||||
|
color: #ffffff;
|
||||||
|
border: 1rpx solid #1a73e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-button:hover {
|
||||||
|
background-color: #1557b0;
|
||||||
|
border-color: #1557b0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-button {
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #5f6368;
|
||||||
|
border: 1rpx solid #dadce0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-button:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-color: #bdc1c6;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一的输入框样式
|
||||||
|
.common-input {
|
||||||
|
padding: 16rpx 20rpx;
|
||||||
|
border: 1rpx solid #dadce0;
|
||||||
|
border-radius: 6rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #3c4043;
|
||||||
|
background-color: #fff;
|
||||||
|
transition: border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.common-input:focus {
|
||||||
|
border-color: #1a73e8;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一的模态框样式
|
||||||
|
.common-modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
padding: 32rpx;
|
||||||
|
margin: 32rpx;
|
||||||
|
max-width: 600rpx;
|
||||||
|
max-height: 80%;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计 - 平板设备适配
|
||||||
|
@media (min-width: 768px) and (max-width: 1024px) {
|
||||||
|
.page-header {
|
||||||
|
padding: 28rpx 32rpx;
|
||||||
|
min-height: 100rpx;
|
||||||
|
height: 100rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-title-text {
|
||||||
|
font-size: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-content {
|
||||||
|
padding: 28rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-cell {
|
||||||
|
font-size: 28rpx;
|
||||||
|
padding: 24rpx 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-cell {
|
||||||
|
font-size: 26rpx;
|
||||||
|
padding: 20rpx 16rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计 - 手机设备适配
|
||||||
|
@media (max-width: 750rpx) {
|
||||||
|
.page-header {
|
||||||
|
padding: 20rpx 24rpx;
|
||||||
|
min-height: 80rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-title-text {
|
||||||
|
font-size: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-content {
|
||||||
|
padding: 20rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-cell {
|
||||||
|
font-size: 22rpx;
|
||||||
|
padding: 16rpx 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-cell {
|
||||||
|
font-size: 20rpx;
|
||||||
|
padding: 12rpx 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 环境参数页面响应式
|
||||||
|
.content-wrapper {
|
||||||
|
margin: 0 20rpx !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 视觉监控页面响应式
|
||||||
|
.camera-container {
|
||||||
|
margin: 0 20rpx !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计 - 大屏设备适配
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.page-header {
|
||||||
|
padding: 32rpx 40rpx;
|
||||||
|
min-height: 120rpx;
|
||||||
|
height: 120rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-title-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-content {
|
||||||
|
padding: 32rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-cell {
|
||||||
|
font-size: 30rpx;
|
||||||
|
padding: 28rpx 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-cell {
|
||||||
|
font-size: 28rpx;
|
||||||
|
padding: 24rpx 20rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
76
src/uni.scss
Normal file
76
src/uni.scss
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* 这里是uni-app内置的常用样式变量
|
||||||
|
*
|
||||||
|
* uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
|
||||||
|
* 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
|
||||||
|
*
|
||||||
|
* 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* 颜色变量 */
|
||||||
|
|
||||||
|
/* 行为相关颜色 */
|
||||||
|
$uni-color-primary: #007aff;
|
||||||
|
$uni-color-success: #4cd964;
|
||||||
|
$uni-color-warning: #f0ad4e;
|
||||||
|
$uni-color-error: #dd524d;
|
||||||
|
|
||||||
|
/* 文字基本颜色 */
|
||||||
|
$uni-text-color: #333; // 基本色
|
||||||
|
$uni-text-color-inverse: #fff; // 反色
|
||||||
|
$uni-text-color-grey: #999; // 辅助灰色,如加载更多的提示信息
|
||||||
|
$uni-text-color-placeholder: #808080;
|
||||||
|
$uni-text-color-disable: #c0c0c0;
|
||||||
|
|
||||||
|
/* 背景颜色 */
|
||||||
|
$uni-bg-color: #fff;
|
||||||
|
$uni-bg-color-grey: #f8f8f8;
|
||||||
|
$uni-bg-color-hover: #f1f1f1; // 点击状态颜色
|
||||||
|
$uni-bg-color-mask: rgba(0, 0, 0, 0.4); // 遮罩颜色
|
||||||
|
|
||||||
|
/* 边框颜色 */
|
||||||
|
$uni-border-color: #c8c7cc;
|
||||||
|
|
||||||
|
/* 尺寸变量 */
|
||||||
|
|
||||||
|
/* 文字尺寸 */
|
||||||
|
$uni-font-size-sm: 12px;
|
||||||
|
$uni-font-size-base: 14px;
|
||||||
|
$uni-font-size-lg: 16;
|
||||||
|
|
||||||
|
/* 图片尺寸 */
|
||||||
|
$uni-img-size-sm: 20px;
|
||||||
|
$uni-img-size-base: 26px;
|
||||||
|
$uni-img-size-lg: 40px;
|
||||||
|
|
||||||
|
/* Border Radius */
|
||||||
|
$uni-border-radius-sm: 2px;
|
||||||
|
$uni-border-radius-base: 3px;
|
||||||
|
$uni-border-radius-lg: 6px;
|
||||||
|
$uni-border-radius-circle: 50%;
|
||||||
|
|
||||||
|
/* 水平间距 */
|
||||||
|
$uni-spacing-row-sm: 5px;
|
||||||
|
$uni-spacing-row-base: 10px;
|
||||||
|
$uni-spacing-row-lg: 15px;
|
||||||
|
|
||||||
|
/* 垂直间距 */
|
||||||
|
$uni-spacing-col-sm: 4px;
|
||||||
|
$uni-spacing-col-base: 8px;
|
||||||
|
$uni-spacing-col-lg: 12px;
|
||||||
|
|
||||||
|
/* 透明度 */
|
||||||
|
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
|
||||||
|
|
||||||
|
/* 文章场景相关 */
|
||||||
|
$uni-color-title: #2c405a; // 文章标题颜色
|
||||||
|
$uni-font-size-title: 20px;
|
||||||
|
$uni-color-subtitle: #555; // 二级标题颜色
|
||||||
|
$uni-font-size-subtitle: 18px;
|
||||||
|
$uni-color-paragraph: #3f536e; // 文章段落颜色
|
||||||
|
$uni-font-size-paragraph: 15px;
|
||||||
156
src/utils/mqttClient.js
Normal file
156
src/utils/mqttClient.js
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
// // MQTT客户端封装
|
||||||
|
// import mqtt from 'mqtt'
|
||||||
|
// import { MQTT_CONFIG, DataParser } from '@/config/mqtt'
|
||||||
|
|
||||||
|
// class MQTTClient {
|
||||||
|
// constructor() {
|
||||||
|
// this.client = null
|
||||||
|
// this.isConnected = false
|
||||||
|
// this.subscriptions = new Map()
|
||||||
|
// this.messageHandlers = new Map()
|
||||||
|
// this.reconnectAttempts = 0
|
||||||
|
// this.maxReconnectAttempts = 5
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 连接MQTT服务器
|
||||||
|
// async connect() {
|
||||||
|
// try {
|
||||||
|
// console.log('正在连接MQTT服务器:', MQTT_CONFIG.broker)
|
||||||
|
// this.client = mqtt.connect(MQTT_CONFIG.broker, MQTT_CONFIG.options)
|
||||||
|
|
||||||
|
// return new Promise((resolve, reject) => {
|
||||||
|
// this.client.on('connect', () => {
|
||||||
|
// console.log('MQTT连接成功')
|
||||||
|
// this.isConnected = true
|
||||||
|
// this.reconnectAttempts = 0
|
||||||
|
// resolve()
|
||||||
|
// })
|
||||||
|
|
||||||
|
// this.client.on('error', (error) => {
|
||||||
|
// console.error('MQTT连接失败:', error)
|
||||||
|
// this.isConnected = false
|
||||||
|
// reject(error)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// this.client.on('message', (topic, message) => {
|
||||||
|
// this.handleMessage(topic, message)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// this.client.on('reconnect', () => {
|
||||||
|
// this.reconnectAttempts++
|
||||||
|
// console.log(`MQTT重连中... (${this.reconnectAttempts}/${this.maxReconnectAttempts})`)
|
||||||
|
|
||||||
|
// if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
||||||
|
// console.error('MQTT重连次数超限,停止重连')
|
||||||
|
// this.client.end()
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
|
// this.client.on('close', () => {
|
||||||
|
// console.log('MQTT连接关闭')
|
||||||
|
// this.isConnected = false
|
||||||
|
// })
|
||||||
|
|
||||||
|
// this.client.on('offline', () => {
|
||||||
|
// console.log('MQTT客户端离线')
|
||||||
|
// this.isConnected = false
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error('MQTT连接异常:', error)
|
||||||
|
// throw error
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 订阅主题
|
||||||
|
// subscribe(topic, handler) {
|
||||||
|
// if (!this.isConnected) {
|
||||||
|
// console.warn('MQTT未连接,无法订阅主题:', topic)
|
||||||
|
// return false
|
||||||
|
// }
|
||||||
|
|
||||||
|
// this.client.subscribe(topic, (error) => {
|
||||||
|
// if (error) {
|
||||||
|
// console.error('订阅主题失败:', topic, error)
|
||||||
|
// return false
|
||||||
|
// } else {
|
||||||
|
// console.log('订阅主题成功:', topic)
|
||||||
|
// this.subscriptions.set(topic, true)
|
||||||
|
// this.messageHandlers.set(topic, handler)
|
||||||
|
// return true
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// return true
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 取消订阅
|
||||||
|
// unsubscribe(topic) {
|
||||||
|
// if (this.subscriptions.has(topic)) {
|
||||||
|
// this.client.unsubscribe(topic)
|
||||||
|
// this.subscriptions.delete(topic)
|
||||||
|
// this.messageHandlers.delete(topic)
|
||||||
|
// console.log('取消订阅主题:', topic)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 发布消息
|
||||||
|
// publish(topic, message) {
|
||||||
|
// if (!this.isConnected) {
|
||||||
|
// console.warn('MQTT未连接,无法发布消息')
|
||||||
|
// return false
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const payload = typeof message === 'object' ? JSON.stringify(message) : message
|
||||||
|
// this.client.publish(topic, payload, (error) => {
|
||||||
|
// if (error) {
|
||||||
|
// console.error('发布消息失败:', topic, error)
|
||||||
|
// return false
|
||||||
|
// } else {
|
||||||
|
// console.log('发布消息成功:', topic, payload)
|
||||||
|
// return true
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// return true
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 处理接收到的消息
|
||||||
|
// handleMessage(topic, message) {
|
||||||
|
// try {
|
||||||
|
// const handler = this.messageHandlers.get(topic)
|
||||||
|
// if (handler) {
|
||||||
|
// const rawData = message.toString()
|
||||||
|
// console.log('收到MQTT消息:', topic, rawData)
|
||||||
|
|
||||||
|
// // 解析数据
|
||||||
|
// const parsedData = DataParser.parseDeviceData(rawData)
|
||||||
|
// if (parsedData) {
|
||||||
|
// handler(parsedData)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error('处理消息失败:', topic, error)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 获取连接状态
|
||||||
|
// getConnectionStatus() {
|
||||||
|
// return {
|
||||||
|
// isConnected: this.isConnected,
|
||||||
|
// reconnectAttempts: this.reconnectAttempts,
|
||||||
|
// subscriptions: Array.from(this.subscriptions.keys())
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 断开连接
|
||||||
|
// disconnect() {
|
||||||
|
// if (this.client) {
|
||||||
|
// this.client.end()
|
||||||
|
// this.isConnected = false
|
||||||
|
// this.subscriptions.clear()
|
||||||
|
// this.messageHandlers.clear()
|
||||||
|
// console.log('MQTT连接已断开')
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export default new MQTTClient()
|
||||||
8
vite.config.js
Normal file
8
vite.config.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import uni from '@dcloudio/vite-plugin-uni'
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
uni(),
|
||||||
|
],
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user