Files
movecheck/src/components/ParameterRecord.vue
吉浩茹 8f6dcca19f feat: 移动式检修车间系统前端完成
- 完成系统日志页面,优化表格滚动和样式
- 完成报警记录页面,优化表格滚动和报警级别显示
- 完成环境参数页面,优化参数显示和监控画面
- 完成参数记录页面,优化图表样式和简洁设计
- 集成MQTT配置,支持实时数据对接
- 统一UI设计风格,采用现代化卡片式布局
- 添加响应式设计,适配不同屏幕尺寸
- 预留MQTT数据接口,支持AC空调和WSD温湿度设备
2025-09-26 10:34:00 +08:00

675 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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