feat: 移动式检修车间系统前端完成

- 完成系统日志页面,优化表格滚动和样式
- 完成报警记录页面,优化表格滚动和报警级别显示
- 完成环境参数页面,优化参数显示和监控画面
- 完成参数记录页面,优化图表样式和简洁设计
- 集成MQTT配置,支持实时数据对接
- 统一UI设计风格,采用现代化卡片式布局
- 添加响应式设计,适配不同屏幕尺寸
- 预留MQTT数据接口,支持AC空调和WSD温湿度设备
This commit is contained in:
吉浩茹
2025-09-26 10:34:00 +08:00
commit 8f6dcca19f
35 changed files with 17474 additions and 0 deletions

21
.gitignore vendored Normal file
View 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
View 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
View 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>

View 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">&times;</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

File diff suppressed because it is too large Load Diff

69
package.json Normal file
View 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
View 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
View 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>

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

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

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

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

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

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

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,6 @@
export {};
declare module "vue" {
type Hooks = App.AppInstance & Page.PageInstance;
interface ComponentCustomOptions extends Hooks {}
}

View File

@ -0,0 +1 @@
<!-- 这是一个占位符文件,实际项目中需要替换为真实的图片文件 -->

View File

@ -0,0 +1 @@
<!-- 摄像头图标占位符 -->

View File

@ -0,0 +1 @@
<!-- 洁净度图标占位符 -->

View File

@ -0,0 +1 @@
<!-- 湿度图标占位符 -->

View File

@ -0,0 +1 @@
<!-- 设置图标占位符 -->

View File

@ -0,0 +1 @@
<!-- 温度图标占位符 -->

View File

@ -0,0 +1 @@
<!-- 检修车间图片占位符 -->

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
View 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
View 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
View 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
View 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(),
],
})