Files
fishery-web/src/views/fishery/monitorHistory/index.vue
2026-01-10 01:59:53 +08:00

592 lines
17 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>
<div class="p-2">
<!-- 查询表单 -->
<el-card shadow="hover" class="mb-[10px]">
<el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="100px">
<el-form-item label="设备" prop="deviceId">
<el-input
v-model="selectedDeviceDisplay"
placeholder="请选择设备"
readonly
clearable
@clear="handleClearDevice"
style="width: 280px; cursor: pointer;"
@click="openDeviceSelect"
>
<template #suffix>
<el-icon style="cursor: pointer;" @click.stop="openDeviceSelect"><Search /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item label="监测时间" prop="dateRange">
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 280px"
/>
</el-form-item>
<el-form-item label="时间间隔" prop="intervalType">
<el-select v-model="queryParams.intervalType" placeholder="请选择时间间隔" clearable style="width: 150px">
<el-option
v-for="dict in interval_type"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 设备选择对话框 -->
<el-dialog title="选择设备" v-model="deviceSelectVisible" width="1200px" append-to-body>
<!-- 搜索条件 -->
<el-form :model="deviceQueryParams" :inline="true" class="mb-4">
<el-form-item label="用户信息">
<el-input
v-model="deviceQueryParams.params.userKeyword"
placeholder="请输入用户名或手机号"
clearable
style="width: 200px"
@keyup.enter="handleDeviceQuery"
/>
</el-form-item>
<el-form-item label="设备编号">
<el-input
v-model="deviceQueryParams.serialNum"
placeholder="请输入设备编号"
clearable
style="width: 180px"
@keyup.enter="handleDeviceQuery"
/>
</el-form-item>
<el-form-item label="设备名称">
<el-input
v-model="deviceQueryParams.deviceName"
placeholder="请输入设备名称"
clearable
style="width: 180px"
@keyup.enter="handleDeviceQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleDeviceQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetDeviceQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 设备表格 -->
<el-table
:data="deviceList"
highlight-current-row
height="400px"
border
>
<el-table-column label="用户名" align="center" prop="userName" />
<el-table-column label="手机号" align="center" prop="mobilePhone" />
<el-table-column label="设备编号" align="center" prop="serialNum" />
<el-table-column label="设备名称" align="center" prop="deviceName" />
<el-table-column label="塘口名称" align="center" prop="pondName" />
<el-table-column label="绑定时间" align="center" prop="bindTime" width="120">
<template #default="scope">
<span>{{ parseTime(scope.row.bindTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="服务到期时间" align="center" prop="deadTime" width="120">
<template #default="scope">
<span>{{ parseTime(scope.row.deadTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="100">
<template #default="scope">
<el-button
type="primary"
size="small"
@click="handleDeviceSelect(scope.row)"
>
选择
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<pagination
v-show="deviceTotal > 0"
:total="deviceTotal"
v-model:page="deviceQueryParams.pageNum"
v-model:limit="deviceQueryParams.pageSize"
@pagination="handleDevicePaginationChange"
class="mt-4"
/>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancelDeviceSelect"> </el-button>
</div>
</template>
</el-dialog>
<!-- 图表区域 -->
<el-card shadow="never">
<el-tabs v-model="activeTab" @tab-change="handleTabChange">
<el-tab-pane label="溶解氧(Mg/L)" name="dissolvedOxygen"></el-tab-pane>
<el-tab-pane label="水温(℃)" name="temperature"></el-tab-pane>
<el-tab-pane label="饱和度(%)" name="saturability"></el-tab-pane>
<el-tab-pane label="PH(Ph)" name="ph"></el-tab-pane>
<el-tab-pane label="盐度(%)" name="salinity"></el-tab-pane>
<el-tab-pane label="参比值" name="treference"></el-tab-pane>
<el-tab-pane label="荧光值" name="tfluorescence"></el-tab-pane>
<el-tab-pane label="电量(%)" name="battery"></el-tab-pane>
</el-tabs>
<div
ref="chartRef"
v-loading="loading"
style="width: 100%; height: 450px; margin-top: 20px;"
></div>
<!-- 图表工具栏 -->
<div class="chart-toolbar">
<el-button-group>
<el-tooltip content="折线图" placement="top">
<el-button
:type="chartType === 'line' ? 'primary' : ''"
icon="TrendCharts"
@click="changeChartType('line')"
/>
</el-tooltip>
<el-tooltip content="柱状图" placement="top">
<el-button
:type="chartType === 'bar' ? 'primary' : ''"
icon="Histogram"
@click="changeChartType('bar')"
/>
</el-tooltip>
<el-tooltip content="下载" placement="top">
<el-button icon="Download" @click="downloadChart" />
</el-tooltip>
</el-button-group>
</div>
</el-card>
</div>
</template>
<script setup name="MonitorHistory" lang="ts">
import { ref, reactive, onMounted, nextTick, computed } from 'vue';
import * as echarts from 'echarts';
import type { EChartsOption } from 'echarts';
import { getHistoryData } from '@/api/fishery/monitorHistory';
import type { DeviceSensorDataQueryBo, DeviceSensorData } from '@/api/fishery/monitorHistory/types';
import { listDevice } from '@/api/fishery/device';
import type { DeviceVO } from '@/api/fishery/device/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { interval_type } = toRefs<any>(proxy?.useDict('interval_type'));
// 查询参数
const queryParams = ref<DeviceSensorDataQueryBo>({
serialNum: '',
startTime: '',
endTime: '',
intervalType: '3'
});
// 日期范围
const dateRange = ref<string[]>([]);
// 设备选择相关
const deviceList = ref<DeviceVO[]>([]);
const selectedDevice = ref<DeviceVO | null>(null);
const deviceSelectVisible = ref(false);
const deviceQueryParams = reactive<{
pageNum: number;
pageSize: number;
serialNum?: string;
deviceName?: string;
params: {
userKeyword?: string;
};
}>({
pageNum: 1,
pageSize: 10,
serialNum: undefined,
deviceName: undefined,
params: {
userKeyword: undefined
}
});
const deviceTotal = ref(0);
// 设备显示文本
const selectedDeviceDisplay = computed(() => {
if (selectedDevice.value) {
return `${selectedDevice.value.deviceName} (${selectedDevice.value.serialNum})`;
}
return '';
});
// 当前激活的标签页
const activeTab = ref<string>('dissolvedOxygen');
// 图表类型
const chartType = ref<string>('line');
// 加载状态
const loading = ref<boolean>(false);
// 图表实例
const chartRef = ref<HTMLElement>();
let chartInstance: echarts.ECharts | null = null;
// 历史数据
const historyData = ref<DeviceSensorData[]>([]);
// 标签页配置
const tabConfig = {
dissolvedOxygen: { label: '溶解氧', unit: 'Mg/L', color: '#5470c6' },
temperature: { label: '水温', unit: '℃', color: '#91cc75' },
saturability: { label: '饱和度', unit: '%', color: '#fac858' },
ph: { label: 'PH', unit: 'Ph', color: '#ee6666' },
salinity: { label: '盐度', unit: '%', color: '#73c0de' },
treference: { label: '参比值', unit: '', color: '#3ba272' },
tfluorescence: { label: '荧光值', unit: '', color: '#fc8452' },
battery: { label: '电量', unit: '%', color: '#9a60b4' }
};
/** 查询历史数据 */
const getHistoryList = async () => {
// 验证是否选择了设备
if (!queryParams.value.serialNum) {
proxy?.$modal.msgWarning('请先选择设备');
return;
}
loading.value = true;
try {
// 设置时间范围
if (dateRange.value && dateRange.value.length === 2) {
queryParams.value.startTime = dateRange.value[0];
queryParams.value.endTime = dateRange.value[1];
}
const res = await getHistoryData(queryParams.value);
// 处理不同的响应格式
if (Array.isArray(res)) {
// 如果res本身就是数组
historyData.value = res;
} else if (res.data && Array.isArray(res.data)) {
// 如果res.data是数组
historyData.value = res.data;
} else {
// 其他情况
historyData.value = [];
}
// 更新图表
await nextTick();
if (historyData.value.length > 0) {
updateChart();
} else {
// 清空图表显示
updateChart();
proxy?.$modal.msgWarning('未查询到数据,请检查日期范围是否正确');
}
} catch (error) {
proxy?.$modal.msgError('查询历史数据失败');
} finally {
loading.value = false;
}
};
/** 搜索按钮操作 */
const handleQuery = () => {
getHistoryList();
};
/** 重置按钮操作 */
const resetQuery = () => {
// 重置为最近1周
const endDate = new Date();
const startDate = new Date();
startDate.setDate(startDate.getDate() - 7);
const startStr = `${startDate.getFullYear()}-${String(startDate.getMonth() + 1).padStart(2, '0')}-${String(startDate.getDate()).padStart(2, '0')}`;
const endStr = `${endDate.getFullYear()}-${String(endDate.getMonth() + 1).padStart(2, '0')}-${String(endDate.getDate()).padStart(2, '0')}`;
dateRange.value = [startStr, endStr];
selectedDevice.value = null;
queryParams.value = {
serialNum: '',
startTime: '',
endTime: '',
intervalType: '3'
};
};
/** 标签页切换 */
const handleTabChange = () => {
updateChart();
};
/** 图表类型切换 */
const changeChartType = (type: string) => {
chartType.value = type;
updateChart();
};
/** 初始化图表 */
const initChart = () => {
if (!chartRef.value) {
return;
}
if (chartInstance) {
chartInstance.dispose();
}
chartInstance = echarts.init(chartRef.value);
// 监听窗口大小变化
window.addEventListener('resize', () => {
chartInstance?.resize();
});
};
/** 更新图表 */
const updateChart = () => {
// 如果图表实例不存在,先初始化
if (!chartInstance) {
initChart();
if (!chartInstance) {
return;
}
}
if (historyData.value.length === 0) {
// 显示空数据提示,完全清空之前的配置
chartInstance.setOption({
title: {
text: '暂无数据',
left: 'center',
top: 'center',
textStyle: {
color: '#999',
fontSize: 20
}
},
tooltip: {},
grid: {},
xAxis: { show: false },
yAxis: { show: false },
series: []
}, true); // true 表示 notMerge不合并之前的配置
return;
}
const config = tabConfig[activeTab.value as keyof typeof tabConfig];
// 准备数据
const xData = historyData.value.map(item => {
// 处理时间字符串 "2025-10-28 00:00:28.639"
const timeStr = item.time.replace(' ', 'T'); // 转换为ISO格式
const date = new Date(timeStr);
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${month}-${day} ${hours}:${minutes}:${seconds}`;
});
const yData = historyData.value.map(item => {
const value = item[activeTab.value as keyof DeviceSensorData];
// 确保返回数字类型
return typeof value === 'number' ? value : 0;
});
// 图表配置
const option: EChartsOption = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross'
},
formatter: (params: any) => {
const param = params[0];
return `${param.axisValue}<br/>${config.label}: ${param.value} ${config.unit}`;
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: chartType.value === 'bar',
data: xData,
axisLabel: {
interval: Math.floor(xData.length / 20), // 控制显示密度
rotate: 20, // 倾斜45度
fontSize: 11
}
},
yAxis: {
type: 'value',
name: `${config.label}(${config.unit})`,
axisLabel: {
formatter: `{value} ${config.unit}`
}
},
series: [
{
name: config.label,
type: chartType.value,
data: yData,
smooth: chartType.value === 'line',
itemStyle: {
color: config.color
},
lineStyle: chartType.value === 'line' ? {
width: 2
} : undefined,
areaStyle: chartType.value === 'line' ? {
opacity: 0.1
} : undefined
}
]
};
chartInstance.setOption(option, true);
};
/** 下载图表 */
const downloadChart = () => {
if (!chartInstance) return;
const url = chartInstance.getDataURL({
type: 'png',
pixelRatio: 2,
backgroundColor: '#fff'
});
const link = document.createElement('a');
link.download = `${tabConfig[activeTab.value as keyof typeof tabConfig].label}_${queryParams.value.serialNum}_${new Date().getTime()}.png`;
link.href = url;
link.click();
};
/** 获取设备列表 */
const getDeviceList = async () => {
const res = await listDevice(deviceQueryParams);
deviceList.value = res.rows || res.data || [];
deviceTotal.value = res.total || 0;
};
/** 打开设备选择对话框 */
const openDeviceSelect = () => {
deviceQueryParams.pageNum = 1;
deviceQueryParams.serialNum = undefined;
deviceQueryParams.deviceName = undefined;
deviceQueryParams.params.userKeyword = undefined;
getDeviceList();
deviceSelectVisible.value = true;
};
/** 搜索设备 */
const handleDeviceQuery = () => {
deviceQueryParams.pageNum = 1;
getDeviceList();
};
/** 重置设备搜索 */
const resetDeviceQuery = () => {
deviceQueryParams.pageNum = 1;
deviceQueryParams.serialNum = undefined;
deviceQueryParams.deviceName = undefined;
deviceQueryParams.params.userKeyword = undefined;
getDeviceList();
};
/** 设备分页改变 */
const handleDevicePaginationChange = () => {
getDeviceList();
};
/** 选择设备 */
const handleDeviceSelect = (device: DeviceVO) => {
selectedDevice.value = device;
queryParams.value.serialNum = device.serialNum;
deviceSelectVisible.value = false;
// 选择设备后自动查询
getHistoryList();
};
/** 取消设备选择 */
const cancelDeviceSelect = () => {
deviceSelectVisible.value = false;
};
/** 清除设备选择 */
const handleClearDevice = () => {
selectedDevice.value = null;
queryParams.value.serialNum = '';
};
onMounted(() => {
// 设置默认日期为最近1周
const endDate = new Date();
const startDate = new Date();
startDate.setDate(startDate.getDate() - 7);
const startStr = `${startDate.getFullYear()}-${String(startDate.getMonth() + 1).padStart(2, '0')}-${String(startDate.getDate()).padStart(2, '0')}`;
const endStr = `${endDate.getFullYear()}-${String(endDate.getMonth() + 1).padStart(2, '0')}-${String(endDate.getDate()).padStart(2, '0')}`;
dateRange.value = [startStr, endStr];
// 初始化图表 - 确保DOM已经渲染
nextTick(() => {
initChart();
});
});
// 组件卸载时销毁图表
onBeforeUnmount(() => {
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
window.removeEventListener('resize', () => {
chartInstance?.resize();
});
});
</script>
<style scoped lang="scss">
.chart-toolbar {
position: absolute;
top: 20px;
right: 20px;
z-index: 10;
}
:deep(.el-tabs__nav-wrap::after) {
height: 1px;
}
:deep(.el-radio-button__inner) {
padding: 8px 15px;
}
</style>