- 完成系统日志页面,优化表格滚动和样式 - 完成报警记录页面,优化表格滚动和报警级别显示 - 完成环境参数页面,优化参数显示和监控画面 - 完成参数记录页面,优化图表样式和简洁设计 - 集成MQTT配置,支持实时数据对接 - 统一UI设计风格,采用现代化卡片式布局 - 添加响应式设计,适配不同屏幕尺寸 - 预留MQTT数据接口,支持AC空调和WSD温湿度设备
675 lines
16 KiB
Vue
675 lines
16 KiB
Vue
<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> |