Files
movecheck/src/components/ParameterRecord.vue

675 lines
16 KiB
Vue
Raw Normal View History

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