Files
fishery-app/src/components/other/dataQuery.vue

729 lines
19 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="data-query-page" :style="pageStyle">
<!-- 顶部标题栏 -->
<view class="header">
<view class="back-btn" @click="goBack">
<text class="icon"></text>
</view>
<view class="title">{{ chartTitle }}</view>
</view>
<!-- 查询条件区域 -->
<view class="query-section">
<view class="query-container">
<view class="query-item">
<view class="query-label">开始时间</view>
<view class="query-value" @click="showStartPicker = true">
{{ startDate || '请选择' }}
</view>
</view>
<view class="query-item">
<view class="query-label">结束时间</view>
<view class="query-value" @click="showEndPicker = true">
{{ endDate || '请选择' }}
</view>
</view>
<nut-button
type="primary"
size="small"
class="query-btn"
@click="queryData"
>
查询
</nut-button>
</view>
</view>
<!-- 图表容器 -->
<view class="chart-container" :style="chartContainerStyle">
<view class="loading-container" v-show="loading">
<text>加载中...</text>
</view>
<canvas
v-if="!loading"
class="chart-canvas"
type="2d"
id="data-query-chart"
:style="canvasStyle"
@touchend="touchEnd"
@touchstart="touchStart"
@touchmove="touchMove"
></canvas>
<view v-if="!loading && (!chartData || chartData.length === 0)" class="empty-data">
<text>暂无数据</text>
</view>
</view>
<!-- 开始时间选择器 -->
<nut-popup v-model:visible="showStartPicker" position="bottom">
<nut-date-picker
v-model="startDateTime"
type="datetime"
title="选择开始时间"
:min-date="minDate"
:max-date="maxDate"
@confirm="confirmStartDate"
@cancel="showStartPicker = false"
/>
</nut-popup>
<!-- 结束时间选择器 -->
<nut-popup v-model:visible="showEndPicker" position="bottom">
<nut-date-picker
v-model="endDateTime"
type="datetime"
title="选择结束时间"
:min-date="minDate"
:max-date="maxDate"
@confirm="confirmEndDate"
@cancel="showEndPicker = false"
/>
</nut-popup>
</view>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue';
import Taro from '@tarojs/taro';
import { deviceHistory } from '@/api/device';
import { formatDateMin } from '@/utils/tools';
import uCharts from '@/utils/js-sdk/u-charts/u-charts.js';
const chartTitle = ref('数据查询');
const chartData = ref<any[]>([]);
const loading = ref(false);
let chartInstance: any = null;
// 接收的参数
const serialNum = ref('');
const dataType = ref(1); // 1-溶解氧, 2-水温, 3-饱和度, 4-PH, 5-盐度
const deviceName = ref('');
const oxyWarnLower = ref<number | null>(null);
const tempWarnUpper = ref<number | null>(null);
const tempWarnLower = ref<number | null>(null);
// 时间选择
const showStartPicker = ref(false);
const showEndPicker = ref(false);
const startDate = ref('');
const endDate = ref('');
const startDateTime = ref(new Date(new Date().getTime() - 30 * 24 * 60 * 60 * 1000));
const endDateTime = ref(new Date());
// 日期范围限制最近90天到当前
const minDate = computed(() => new Date(new Date().getTime() - 90 * 24 * 60 * 60 * 1000));
const maxDate = computed(() => new Date());
// 屏幕尺寸
const screenWidth = ref(375);
const screenHeight = ref(667);
// 页面样式
const pageStyle = computed(() => {
return {
width: '100%',
height: '100%',
background: '#fff',
display: 'flex',
flexDirection: 'column' as const,
};
});
const chartContainerStyle = computed(() => {
return {
flex: '1',
padding: '10px 15px',
background: '#fff',
overflow: 'hidden',
position: 'relative' as const,
};
});
const canvasStyle = computed(() => {
return {
width: chartWidth.value + 'px',
height: chartHeight.value + 'px',
};
});
onMounted(() => {
// 获取屏幕尺寸
const sysInfo = Taro.getSystemInfoSync();
screenWidth.value = sysInfo.windowWidth;
screenHeight.value = sysInfo.windowHeight;
// 获取页面参数
const instance = Taro.getCurrentInstance();
const params = instance.router?.params || {};
serialNum.value = params.serialNum || '';
dataType.value = Number(params.dataType) || 1;
deviceName.value = params.deviceName || '';
oxyWarnLower.value = params.oxyWarnLower ? Number(params.oxyWarnLower) : null;
tempWarnUpper.value = params.tempWarnUpper ? Number(params.tempWarnUpper) : null;
tempWarnLower.value = params.tempWarnLower ? Number(params.tempWarnLower) : null;
// 设置标题
const titles = ['', '溶解氧数据查询', '水温数据查询', '饱和度数据查询', 'PH数据查询', '盐度数据查询'];
chartTitle.value = titles[dataType.value] || '数据查询';
// 初始化默认时间最近30天
const now = new Date();
const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
startDate.value = formatDateMin(monthAgo);
endDate.value = formatDateMin(now);
// 初始化图表尺寸
setTimeout(() => {
initChart();
}, 500);
});
onUnmounted(() => {
if (chartInstance) {
chartInstance = null;
}
});
// 图表尺寸
const chartWidth = ref(0);
const chartHeight = ref(0);
const chartPixelRatio = ref(1);
const chartTop = ref(0);
const chartLeft = ref(0);
// 初始化图表
function initChart() {
const sysInfo = Taro.getSystemInfoSync();
// 横屏模式header(44px) + query-section(约50px) = 94px
chartWidth.value = sysInfo.windowWidth - 30;
chartHeight.value = sysInfo.windowHeight - 110;
chartPixelRatio.value = sysInfo.pixelRatio || 1;
// 自动加载默认数据最近30天
queryData();
}
// 返回上一页
function goBack() {
Taro.navigateBack();
}
// 确认开始时间
function confirmStartDate({ selectedValue }) {
startDateTime.value = selectedValue[0];
startDate.value = formatDateMin(selectedValue[0]);
showStartPicker.value = false;
}
// 确认结束时间
function confirmEndDate({ selectedValue }) {
endDateTime.value = selectedValue[0];
endDate.value = formatDateMin(selectedValue[0]);
showEndPicker.value = false;
}
// 查询数据
function queryData() {
if (!serialNum.value) {
Taro.showToast({ title: '设备序列号不存在', icon: 'none' });
return;
}
if (!startDate.value || !endDate.value) {
Taro.showToast({ title: '请选择查询时间范围', icon: 'none' });
return;
}
// 验证时间范围
const start = new Date(startDate.value);
const end = new Date(endDate.value);
if (start >= end) {
Taro.showToast({ title: '开始时间必须小于结束时间', icon: 'none' });
return;
}
// 计算天数差
const dayDiff = Math.ceil((end.getTime() - start.getTime()) / (24 * 60 * 60 * 1000));
if (dayDiff > 90) {
Taro.showToast({ title: '查询范围不能超过3个月90天', icon: 'none' });
return;
}
// 根据天数差确定 intervalType
// ≤7天: intervalType = 2
// 7-14天: intervalType = 3
// 14-30天: intervalType = 4
// 30-60天: intervalType = 5
// >60天: intervalType = 6
let intervalType = 2;
if (dayDiff > 7 && dayDiff <= 14) {
intervalType = 3;
} else if (dayDiff > 14 && dayDiff <= 30) {
intervalType = 4;
} else if (dayDiff > 30 && dayDiff <= 60) {
intervalType = 5;
} else if (dayDiff > 60) {
intervalType = 6;
}
const params = {
serialNum: serialNum.value,
startTime: startDate.value,
endTime: endDate.value,
intervalType: intervalType,
};
loading.value = true;
deviceHistory(params).then((res: any) => {
loading.value = false;
if (Array.isArray(res)) {
chartData.value = res;
} else if (res.code == 200) {
chartData.value = res.data || [];
}
if (chartData.value.length === 0) {
Taro.showToast({ title: '该时间段暂无数据', icon: 'none' });
} else {
// 绘制图表
drawChart();
}
}).catch(() => {
loading.value = false;
Taro.showToast({ title: '查询失败,请重试', icon: 'none' });
});
}
// 绘制图表
function drawChart() {
if (!chartData.value || chartData.value.length === 0) {
return;
}
const cWidth = chartWidth.value;
const cHeight = chartHeight.value;
const pixelRatio = chartPixelRatio.value;
const fieldMap: any = {
1: 'dissolvedOxygen',
2: 'temperature',
3: 'saturability',
4: 'ph',
5: 'salinity',
};
const nameMap: any = {
1: '溶解氧',
2: '水温',
3: '饱和度',
4: 'PH',
5: '盐度',
};
const fieldName = fieldMap[dataType.value] || 'dissolvedOxygen';
const seriesName = nameMap[dataType.value] || '溶解氧';
const xAxisData: string[] = [];
const seriesData: number[] = [];
let series: any[] = [];
// 根据数据类型添加告警线
if (dataType.value === 1 && oxyWarnLower.value) {
series.push({
name: '告警下限',
data: [],
color: '#c12e34',
lineType: 'dash',
});
} else if (dataType.value === 2) {
if (tempWarnLower.value) {
series.push({
name: '告警下限',
data: [],
color: '#c12e34',
lineType: 'dash',
});
}
if (tempWarnUpper.value) {
series.push({
name: '告警上限',
data: [],
color: '#e098c7',
lineType: 'dash',
});
}
}
chartData.value.forEach((item: any) => {
const timeStr = item.time || item.createTime || '';
const timeParts = timeStr.split(' ');
if (timeParts.length > 1) {
const datePart = timeParts[0];
const timePart = timeParts[1];
const dateMatch = datePart.match(/\d{4}-(\d{2})-(\d{2})/);
const monthDay = dateMatch ? `${dateMatch[1]}-${dateMatch[2]}` : '';
const hourMin = timePart.substring(0, 5);
const formattedTime = `${monthDay} ${hourMin}`;
xAxisData.push(formattedTime);
}
const value = item[fieldName];
seriesData.push(value !== null && value !== undefined ? Number(value) : 0);
// 填充告警线数据
if (dataType.value === 1 && oxyWarnLower.value) {
series[0].data.push(oxyWarnLower.value);
} else if (dataType.value === 2) {
let idx = 0;
if (tempWarnLower.value) {
series[idx].data.push(tempWarnLower.value);
idx++;
}
if (tempWarnUpper.value) {
series[idx].data.push(tempWarnUpper.value);
}
}
});
// 添加主数据系列
series.unshift({ name: seriesName, data: seriesData });
Taro.nextTick(() => {
const env = Taro.getEnv();
if (env === Taro.ENV_TYPE.WEB) {
// H5 环境
setTimeout(() => {
const canvasElement = document.querySelector('#data-query-chart canvas') as HTMLCanvasElement;
if (!canvasElement) return;
const ctx = canvasElement.getContext('2d');
if (!ctx) return;
chartInstance = new uCharts({
type: 'line',
context: ctx,
width: cWidth,
height: cHeight,
categories: xAxisData,
series: series,
pixelRatio: 1,
dataPointShape: false,
animation: true,
background: '#FFFFFF',
color: ['#17B4B2'],
padding: [15, 15, 30, 40],
enableScroll: false,
boundaryGap: 'justify',
legend: { show: false },
dataLabel: false,
xAxis: {
disableGrid: true,
axisLine: true,
type: 'grid',
gridType: 'dash',
scrollShow: false,
labelCount: 8,
rotateLabel: true,
rotateAngle: 30,
},
yAxis: {
disableGrid: false,
gridType: 'dash',
tofix: 2,
},
extra: {
tooltip: {
showBox: true,
showArrow: true,
showCategory: true,
borderWidth: 0,
borderRadius: 0,
borderColor: '#000000',
borderOpacity: 0.7,
bgColor: '#000000',
bgOpacity: 0.7,
gridType: 'solid',
dashLength: 4,
gridColor: '#CCCCCC',
boxPadding: 3,
fontSize: 13,
lineHeight: 20,
fontColor: '#FFFFFF',
legendShow: true,
legendShape: 'auto',
splitLine: true,
labelBgColor: '#FFFFFF',
labelBgOpacity: 0.7,
labelFontColor: '#666666',
},
line: { type: 'curve', width: 2, activeType: 'hollow' },
},
});
// 获取 canvas 位置
const query = Taro.createSelectorQuery();
query.select('#data-query-chart').boundingClientRect();
query.exec((res) => {
if (res.length > 0 && res[0]) {
chartTop.value = res[0].top || 0;
chartLeft.value = res[0].left || 0;
}
});
}, 300);
} else {
// 小程序环境
const query = Taro.createSelectorQuery();
query.select('#data-query-chart')
.fields({ node: true, size: true })
.exec((res) => {
if (!res || !res[0] || !res[0].node) return;
const canvas = res[0].node;
const ctx = canvas.getContext('2d');
if (!ctx) return;
canvas.width = cWidth * pixelRatio;
canvas.height = cHeight * pixelRatio;
chartInstance = new uCharts({
type: 'line',
context: ctx,
width: cWidth * pixelRatio,
height: cHeight * pixelRatio,
categories: xAxisData,
series: series,
pixelRatio: pixelRatio,
dataPointShape: false,
animation: true,
background: '#FFFFFF',
color: ['#17B4B2'],
padding: [15, 15, 30, 40],
enableScroll: false,
boundaryGap: 'justify',
legend: { show: false },
dataLabel: false,
xAxis: {
disableGrid: true,
axisLine: true,
type: 'grid',
gridType: 'dash',
scrollShow: false,
labelCount: 6,
rotateLabel: true,
rotateAngle: 20,
marginTop: 5,
},
yAxis: {
disableGrid: false,
gridType: 'dash',
tofix: 2,
},
extra: {
tooltip: {
showBox: true,
showArrow: true,
showCategory: true,
borderWidth: 0,
borderRadius: 0,
borderColor: '#000000',
borderOpacity: 0.7,
bgColor: '#000000',
bgOpacity: 0.7,
gridType: 'solid',
dashLength: 4,
gridColor: '#CCCCCC',
boxPadding: 3,
fontSize: 13,
lineHeight: 20,
fontColor: '#FFFFFF',
legendShow: true,
legendShape: 'auto',
splitLine: true,
labelBgColor: '#FFFFFF',
labelBgOpacity: 0.7,
labelFontColor: '#666666',
},
line: { type: 'curve', width: 2, activeType: 'hollow' },
},
});
// 获取 canvas 位置
const query = Taro.createSelectorQuery();
query.select('#data-query-chart').boundingClientRect();
query.exec((res) => {
if (res.length > 0 && res[0]) {
chartTop.value = res[0].top || 0;
chartLeft.value = res[0].left || 0;
}
});
});
}
});
}
// 触摸结束事件 - 显示提示框
function touchEnd(e: any) {
if (!chartInstance) return;
const x = e.mpEvent.changedTouches[0].clientX;
const y = e.mpEvent.changedTouches[0].clientY;
const tmpe = { x: x - chartLeft.value, y: y - chartTop.value };
e.changedTouches = [tmpe];
if (e.touches.length == 0) {
chartInstance.touchLegend(e);
chartInstance.showToolTip(e, {
formatter: (item: any) => {
return item.name + ':' + item.data;
},
});
}
}
// 触摸开始事件
function touchStart(e: any) {
if (!chartInstance) return;
const x = e.mpEvent.changedTouches[0].clientX;
const y = e.mpEvent.changedTouches[0].clientY;
const tmpe = { x: x - chartLeft.value, y: y - chartTop.value };
e.changedTouches = [tmpe];
chartInstance.scrollStart(e);
}
// 触摸移动事件
function touchMove(e: any) {
if (!chartInstance) return;
const x = e.mpEvent.changedTouches[0].clientX;
const y = e.mpEvent.changedTouches[0].clientY;
const tmpe = { x: x - chartLeft.value, y: y - chartTop.value };
e.changedTouches = [tmpe];
chartInstance.scroll(e);
}
</script>
<style lang="scss">
.data-query-page {
background: #fff;
overflow: hidden;
}
.header {
height: 44px;
background: #fff;
display: flex;
align-items: center;
padding: 0 15px;
border-bottom: 1px solid #eee;
flex-shrink: 0;
}
.back-btn {
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
}
.back-btn .icon {
font-size: 24px;
color: #333;
}
.title {
flex: 1;
text-align: center;
font-size: 16px;
font-weight: 600;
color: #333;
margin-right: 44px;
}
.query-section {
background: #fff;
padding: 6px 15px;
border-bottom: 1px solid #eee;
flex-shrink: 0;
}
.query-container {
display: flex;
align-items: center;
justify-content: center;
gap: 15px;
}
.query-item {
display: flex;
align-items: center;
gap: 8px;
}
.query-label {
font-size: 13px;
color: #666;
white-space: nowrap;
}
.query-value {
font-size: 13px;
color: #333;
padding: 4px 12px;
background: #f5f5f5;
border-radius: 4px;
white-space: nowrap;
min-width: 150px;
text-align: center;
}
.query-btn {
height: 28px;
padding: 0 20px;
background: linear-gradient(135deg, #09b8c2 0%, #06a0a9 100%);
border: none;
border-radius: 4px;
font-size: 13px;
}
.chart-container {
background: #fff;
position: relative;
}
.chart-canvas {
display: block;
}
.loading-container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
color: #999;
}
.empty-data {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 14px;
color: #999;
}
</style>