build: 1、升级sass-embedded版本,意图解决@import/@use 警告问题(暂未解决); 2、引入video.js插件

feat:  1、气象监测页面新增气象视频实时画面;开发虫情相关的交互,与后台接口初步对接
master
chris 12 hours ago
parent 1b510b8cd5
commit 36cf3d7051

@ -33,6 +33,7 @@
"pinia": "2.1.7",
"splitpanes": "3.1.5",
"unocss": "^0.65.1",
"video.js": "^8.23.4",
"vue": "3.4.31",
"vue-cropper": "1.1.1",
"vue-router": "4.4.0"
@ -42,7 +43,7 @@
"@iconify-json/carbon": "^1.2.5",
"@iconify/vue": "^4.2.0",
"@vitejs/plugin-vue": "5.0.5",
"sass-embedded": "^1.83.4",
"sass-embedded": "^1.93.2",
"unplugin-auto-import": "0.17.6",
"unplugin-vue-setup-extend-plus": "1.0.1",
"vite": "5.3.2",

@ -1,4 +1,4 @@
@import './variables.scss';
@import './variables_custom.scss';
:export {
menuText: $menuText;

@ -1,3 +1,9 @@
/*
* @Author: chris
* @Date: 2025-01-13 09:34:10
* @LastEditors: chris
* @LastEditTime: 2025-10-27 14:19:18
*/
import { createApp } from "vue";
import Cookies from "js-cookie";
@ -7,6 +13,7 @@ import "element-plus/dist/index.css";
import "element-plus/theme-chalk/dark/css-vars.css";
import locale from "element-plus/es/locale/lang/zh-cn";
import "virtual:uno.css";
import "video.js/dist/video-js.css";
import "@/assets/styles/index.scss"; // global css

@ -396,3 +396,33 @@ export function isNumberStr(str) {
export function objectToArray(obj) {
return Object.keys(obj).map((key) => obj[key]);
}
/**
* 随机生成暗色
* 确保生成的颜色不是亮色通过控制RGB值在较低范围内
* @returns {string} 十六进制颜色代码
*/
export function getRandomColor() {
// 生成0-255之间的随机RGB值
const r = Math.floor(Math.random() * 255);
const g = Math.floor(Math.random() * 255);
const b = Math.floor(Math.random() * 255);
// 转换为十六进制并格式化
const toHex = (value) => {
const hex = value.toString(16);
return hex.length === 1 ? "0" + hex : hex;
};
return "#" + toHex(r) + toHex(g) + toHex(b);
}
export function getDateRange(days) {
const endDate = new Date();
const startDate = new Date(endDate);
startDate.setDate(endDate.getDate() - days);
return {
beginTime: startDate.toISOString().split("T")[0],
endTime: endDate.toISOString().split("T")[0],
};
}

@ -2,36 +2,11 @@
* @Author: chris
* @Date: 2025-08-25 15:42:45
* @LastEditors: chris
* @LastEditTime: 2025-09-02 16:28:57
* @LastEditTime: 2025-10-15 16:00:17
-->
<script setup>
import ModuleTitle from './moduleTitle.vue';
const itemDict = {
acreage: {
svg: 'nongshi',
text: '种植面积(亩)'
},
terrain: {
svg: 'dixing',
text: '地形'
},
typeNum: {
svg: 'pinzhong',
text: '种植品种(种)'
},
type: {
svg: 'dapengzhongmiaoguanli',
text: '主要品种'
},
lichee: {
svg: 'shuguo',
text: '种植荔枝(棵)'
},
yield: {
svg: 'caizhaiguanli',
text: '去年产量(吨)'
}
}
import { baseInfoDict } from '../config.js';
const prop = defineProps({
info: {
@ -39,17 +14,19 @@ const prop = defineProps({
default: () => {}
}
})
const infoItems = [ 'plantingArea', 'terrain', 'variety', 'plantingNum' ]
</script>
<template>
<div class="orchard-brief-info">
<ModuleTitle title="果园概况" icon="nongchangguanli" />
<div class="list">
<div class="info-item" v-for="key in Object.keys(prop.info)" :key="key">
<div class="info-item" v-for="key in infoItems" :key="key">
<div class="item-icon-box">
<svg-icon class="item-icon" :icon-class="itemDict[key].svg" color="#2b9908"/>
<svg-icon class="item-icon" :icon-class="baseInfoDict[key].svg" color="#2b9908"/>
</div>
<p class="item-text">{{ itemDict[key].text }}</p>
<p class="item-text">{{ baseInfoDict[key]?.text }}</p>
<p class="item-value">{{ prop.info[key] }}</p>
</div>
</div>

@ -2,17 +2,21 @@
* @Author: chris
* @Date: 2025-08-25 15:51:13
* @LastEditors: chris
* @LastEditTime: 2025-09-04 11:04:43
* @LastEditTime: 2025-10-15 10:53:21
-->
<script setup>
import ModuleTitle from './moduleTitle.vue';
import { View } from '@element-plus/icons-vue';
import { pestTypeDict } from '../config';
// 使proxy访$echarts
const { proxy } = getCurrentInstance();
const echarts = proxy?.$echarts;
const props = defineProps({
pestData: {
type: Array,
default: () => []
},
pestInfo: {
type: Object,
default: () => ({
@ -43,9 +47,45 @@ const trendChartRef = ref(null);
const distributionChartRef = ref(null);
let trendChartInstance = null;
let distributionChartInstance = null;
const formateLineData = ref({})
const formatePieData = ref({})
const lineChartData = ref({})
const pieChartData = ref({})
const pestTotal = computed(() => {
return pieChartData.value.values?.reduce((total, cur) => total + cur, 0) || 0
})
watch(() => props.pestData, () => {
const { lineRes, pieRes } = formatData(props.pestData)
formateLineData.value = lineRes
formatePieData.value = pieRes
lineChartData.value = createChartData(formateLineData.value, 'line')
pieChartData.value = createChartData(formatePieData.value, 'pie')
updateTrendChart();
updateDistributionChart();
})
onMounted(() => {
console.log(proxy.$utils)
initTrendChart();
initDistributionChart();
window.addEventListener('resize', handleResize);
});
//
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
if (trendChartInstance) {
trendChartInstance.dispose();
}
if (distributionChartInstance) {
distributionChartInstance.dispose();
}
});
//
const initTrendChart = () => {
function initTrendChart () {
if (trendChartRef.value && echarts) {
//
if (trendChartInstance) {
@ -58,7 +98,7 @@ const initTrendChart = () => {
};
//
const initDistributionChart = () => {
function initDistributionChart () {
if (distributionChartRef.value && echarts) {
//
if (distributionChartInstance) {
@ -71,7 +111,8 @@ const initDistributionChart = () => {
};
//
const updateTrendChart = () => {
function updateTrendChart () {
if (!trendChartInstance || !echarts) return;
const gradient = new echarts.graphic.LinearGradient(0, 0, 0, 1, [
@ -117,7 +158,10 @@ const updateTrendChart = () => {
xAxis: {
type: 'category',
boundaryGap: false,
data: props.pestInfo.trend.map(item => item.date),
data: lineChartData.value.dates?.map(item => {
const arr = item.split('/')
return `${arr[1]}/${arr[2]}`
}) || [],
axisLine: {
lineStyle: {
color: '#464646'
@ -156,7 +200,7 @@ const updateTrendChart = () => {
{
name: '虫情数量',
type: 'line',
data: props.pestInfo.trend.map(item => Math.round(item.count)),
data: lineChartData.value.values || [],
smooth: true,
symbol: 'circle',
symbolSize: 6,
@ -177,7 +221,7 @@ const updateTrendChart = () => {
};
//
const updateDistributionChart = () => {
function updateDistributionChart () {
if (!distributionChartInstance || !echarts) return;
const option = {
@ -234,11 +278,11 @@ const updateDistributionChart = () => {
labelLine: {
show: false
},
data: props.pestInfo.current.types.map(type => ({
name: type.name,
value: type.count,
data: pieChartData.value.names?.map((name, index) => ({
name,
value: pieChartData.value.values?.[index] || 0,
itemStyle: {
color: type.color
color: pestTypeDict[name]?.color || proxy.$utils.getRandomColor()
}
}))
}
@ -248,14 +292,8 @@ const updateDistributionChart = () => {
distributionChartInstance.setOption(option);
};
//
watch(() => props.pestInfo, () => {
updateTrendChart();
updateDistributionChart();
}, { deep: true });
//
const handleResize = () => {
function handleResize () {
if (trendChartInstance) {
trendChartInstance.resize();
}
@ -264,22 +302,29 @@ const handleResize = () => {
}
};
onMounted(() => {
initTrendChart();
initDistributionChart();
window.addEventListener('resize', handleResize);
});
//
const onUnmounted = () => {
window.removeEventListener('resize', handleResize);
if (trendChartInstance) {
trendChartInstance.dispose();
function formatData (data) {
const lineRes = {}
const pieRes = {}
data.forEach(item => {
const date = item.createTime.split(' ')[0].replace(/-/g, '/')
lineRes[item.name] ? lineRes[item.name].push(item) : (lineRes[item.name] = [item])
pieRes[date] ? pieRes[date].push(item) : (pieRes[date] = [item])
})
return {
lineRes,
pieRes
}
if (distributionChartInstance) {
distributionChartInstance.dispose();
}
};
function createChartData(data, type) {
const keys = Object.keys(data)
const values = []
names.forEach(key => {
values.push(data[key].reduce((pre, cur) => cur + (pre.value || 0), 0))
})
return type === 'line' ? { dates: keys, values } : { names: keys, values }
}
</script>
<template>
@ -297,7 +342,7 @@ const onUnmounted = () => {
<module-title title="虫情监测" icon="bug">
<div class="total-count">
<span class="total-label">总虫情数:</span>
<span class="total-value">{{ pestInfo.current.totalCount }}</span>
<span class="total-value">{{ pestTotal }}</span>
</div>
</module-title>
@ -322,10 +367,10 @@ const onUnmounted = () => {
<div class="pest-types-card">
<div class="pest-types-title">害虫类型数量</div>
<div class="pest-types">
<div v-for="type in pestInfo.current.types" :key="type.name" class="type-item">
<div class="color-dot" :style="{ backgroundColor: type.color }" />
<span class="type-name">{{ type.name }}</span>
<span class="type-count">{{ type.count }}</span>
<div v-for="(name, index) in pieChartData.names" :key="name" class="type-item">
<div class="color-dot" :style="{ backgroundColor: pestTypeDict[name]?.color || proxy.$utils.getRandomColor()}" />
<span class="type-name">{{ name }}</span>
<span class="type-count">{{ pieChartData.values[index] || 0 }}</span>
</div>
</div>
</div>
@ -383,17 +428,17 @@ $transition: all 0.3s ease;
//
.left-column {
@apply w-[43%];
@apply w-[45%];
}
//
.middle-column {
@apply w-[32%];
@apply w-[33%];
}
//
.right-column {
@apply w-[25%];
@apply w-[20%];
}
.chart-card {

@ -2,87 +2,11 @@
* @Author: chris
* @Date: 2025-08-25 15:51:13
* @LastEditors: chris
* @LastEditTime: 2025-09-03 14:22:30
* @LastEditTime: 2025-10-14 16:14:16
-->
<script setup>
import ModuleTitle from './moduleTitle.vue';
import {
Sunny
} from '@element-plus/icons-vue';
// - 使7
const soilData = ref([
{
id: 'temperature',
name: '土壤温度',
value: '22.5',
unit: '°C',
icon: 'turangwendu',
color: '#ff7a45',
status: 'normal',
description: '适宜作物生长'
},
{
id: 'moisture',
name: '土壤湿度',
value: '65',
unit: '%',
icon: 'turangshidu',
color: '#4096ff',
status: 'normal',
description: '湿润度良好'
},
{
id: 'ec',
name: '电导率',
value: '0.8',
unit: 'mS/cm',
icon: 'turangdiandaoshuai',
color: '#73d13d',
status: 'normal',
description: '盐分适中'
},
{
id: 'ph',
name: '土壤pH值',
value: '6.8',
unit: '',
icon: 'suanjiandu',
color: '#ff9800',
status: 'normal',
description: '酸碱度适宜'
},
{
id: 'nitrogen',
name: '氮含量',
value: '35',
unit: 'mg/kg',
icon: 'turangdanhanliang',
color: '#fa541c',
status: 'normal',
description: '肥力良好'
},
{
id: 'phosphorus',
name: '磷含量',
value: '22',
unit: 'mg/kg',
icon: 'turanglinhanliang',
color: '#9c27b0',
status: 'normal',
description: '含量适中'
},
{
id: 'potassium',
name: '钾含量',
value: '85',
unit: 'mg/kg',
icon: 'turangjiahanliang',
color: '#2196f3',
status: 'normal',
description: '钾素充足'
}
]);
import { soilDict, soilTypes } from '../config';
//
const statusColors = {
@ -90,6 +14,55 @@ const statusColors = {
warning: '#faad14',
danger: '#ff4d4f'
};
const props = defineProps({
soilData: {
type: Array,
default: () => [],
}
})
const formateData = ref({})
const liveData = computed(() => {
return createLiveData(formateData.value)
});
onMounted(() => {
formateData.value = formatData(props.soilData)
})
function createLiveData (data) {
const liveData = []
Object.keys(data).forEach((type, index) => {
const dictItem = soilDict[type]
liveData.push({
id: index + 1,
title: dictItem.name,
type,
value: data[type][0]?.value || '--',
icon: dictItem.icon,
color: dictItem.color,
unit: dictItem.unit,
})
})
return liveData;
}
function formatData(data) {
const obj = {};
const res = {};
// data.sort((a,b) => soilTypes.indexOf(a.identifier) - soilTypes.indexOf(b.identifier))
data.forEach(item => {
const identifier = item.identifier;
if (!identifier || identifier === 'null') return false;
obj[identifier] ? obj[identifier].push(item) : (obj[identifier] = [item])
})
soilTypes.forEach(type => {
res[type] = obj[type] || []
})
return res;
}
</script>
<template>
@ -97,7 +70,7 @@ const statusColors = {
<ModuleTitle class="soil-title" title="土壤信息" icon="duocengturangshangqing" />
<div class="soil-data-grid">
<div
v-for="item in soilData"
v-for="item in liveData"
:key="item.id"
class="soil-data-card"
>
@ -110,7 +83,7 @@ const statusColors = {
<span class="soil-data-value">{{ item.value }}</span>
<span class="soil-data-unit">{{ item.unit }}</span>
</div>
<div class="soil-data-name">{{ item.name }}</div>
<div class="soil-data-name">{{ item.title }}</div>
<!-- <div class="soil-data-status" :style="{ color: statusColors[item.status] }">
{{ item.description }}
</div> -->

@ -2,119 +2,34 @@
* @Author: chris
* @Date: 2025-08-25 15:51:13
* @LastEditors: chris
* @LastEditTime: 2025-09-04 11:02:47
* @LastEditTime: 2025-10-14 15:36:47
-->
<script setup>
import { Sunny, Pouring, WindPower, Odometer, Sunrise, Guide } from '@element-plus/icons-vue';
import { Sunny, Pouring, WindPower, Sunrise, Guide } from '@element-plus/icons-vue';
import { weatherTypes, weatherDict } from '../config.js';
// 使proxy访$echarts
const { proxy } = getCurrentInstance();
const echarts = proxy?.$echarts;
const props = defineProps({
weatherInfo: {
type: Object,
default: () => ({
//
current: {
temperature: 25.5,
humidity: 65,
windSpeed: 3.2,
windDirection: '东北',
rainfall: 0,
pressure: 1013,
uvIndex: 5,
visibility: 10
},
forecast: Array(24).fill().map((_, index) => ({
time: `${(new Date().getHours() + index) % 24}:00`,
temperature: 20 + Math.sin(index / 3) * 8 + Math.random() * 2,
humidity: 50 + Math.cos(index / 3) * 20 + Math.random() * 5,
rainfall: Math.random() > 0.8 ? Math.random() * 2 : 0,
windSpeed: 2 + Math.sin(index / 2) * 2 + Math.random() * 1,
uvIndex: 3 + Math.sin(index / 4) * 3 + Math.random() * 1
}))
})
},
weatherData: {
type: Array,
default: () => []
}
});
const chartRef = ref(null);
let chartInstance = null;
const formateData = ref({})
const chartData = ref({})
//
const selectedMetric = ref('temperature');
//
const metricConfig = {
temperature: {
name: '温度',
unit: '°C',
color: '#ff6b6b',
dataKey: 'temperature',
min: 0,
max: 40,
interval: 10,
gradient: echarts ? new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(255, 107, 107, 0.5)' },
{ offset: 1, color: 'rgba(255, 107, 107, 0.1)' }
]) : null
},
humidity: {
name: '湿度',
unit: '%',
color: '#4dabf7',
dataKey: 'humidity',
min: 0,
max: 100,
interval: 20,
gradient: echarts ? new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(77, 171, 247, 0.5)' },
{ offset: 1, color: 'rgba(77, 171, 247, 0.1)' }
]) : null
},
windSpeed: {
name: '风速',
unit: 'm/s',
color: '#00b894',
dataKey: 'windSpeed',
min: 0,
max: 10,
interval: 2,
gradient: echarts ? new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(0, 184, 148, 0.5)' },
{ offset: 1, color: 'rgba(0, 184, 148, 0.1)' }
]) : null
},
rainfall: {
name: '降雨量',
unit: 'mm',
color: '#74b9ff',
dataKey: 'rainfall',
min: 0,
max: 10,
interval: 2,
gradient: echarts ? new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(116, 185, 255, 0.5)' },
{ offset: 1, color: 'rgba(116, 185, 255, 0.1)' }
]) : null
},
uvIndex: {
name: '紫外线',
unit: '级',
color: '#fdcb6e',
dataKey: 'uvIndex',
min: 0,
max: 10,
interval: 2,
gradient: echarts ? new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(253, 203, 110, 0.5)' },
{ offset: 1, color: 'rgba(253, 203, 110, 0.1)' }
]) : null
}
};
const selectedMetric = ref('Temperature');
//
const handleMetricClick = (metric) => {
selectedMetric.value = metric;
chartData.value = createChartData(formateData.value, selectedMetric.value)
updateChart();
};
@ -133,9 +48,9 @@ const initChart = () => {
//
const updateChart = () => {
if (!chartInstance || !echarts || !props.weatherInfo.forecast) return;
if (!chartInstance || !echarts) return;
const config = metricConfig[selectedMetric.value];
const config = weatherDict[selectedMetric.value];
const option = {
title: {
@ -175,7 +90,7 @@ const updateChart = () => {
xAxis: {
type: 'category',
boundaryGap: false,
data: props.weatherInfo.forecast.map(item => item.time),
data: chartData.value.dates,
axisLine: {
lineStyle: {
color: '#464646'
@ -216,17 +131,7 @@ const updateChart = () => {
{
name: `${config.name}(${config.unit})`,
type: 'line',
data: props.weatherInfo.forecast.map(item => {
//
if (selectedMetric.value === 'windSpeed' && typeof item[config.dataKey] === 'undefined') {
return (2 + Math.sin(props.weatherInfo.forecast.indexOf(item) / 2) * 2).toFixed(1);
}
// 线
if (selectedMetric.value === 'uvIndex' && typeof item[config.dataKey] === 'undefined') {
return (3 + Math.sin(props.weatherInfo.forecast.indexOf(item) / 4) * 3).toFixed(1);
}
return item[config.dataKey] ? item[config.dataKey].toFixed(1) : '0';
}),
data: chartData.value.values,
smooth: true,
symbol: 'circle',
symbolSize: 6,
@ -234,7 +139,16 @@ const updateChart = () => {
color: config.color
},
areaStyle: {
color: config.gradient
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: `${config.color}40`
},
{
offset: 1,
color: `${config.color}10`
}
])
},
emphasis: {
focus: 'series'
@ -246,10 +160,19 @@ const updateChart = () => {
chartInstance.setOption(option);
};
//
watch(() => props.weatherInfo, () => {
watch(() =>props.weatherData, (value) => {
formateData.value = formatData(value)
chartData.value = createChartData(formateData.value, selectedMetric.value)
updateChart();
}, { deep: true });
})
const liveData = computed(() => {
const res ={}
weatherTypes.forEach(type => {
res[type] = formateData.value[type]?.[0] || '--'
})
return res
})
//
const handleResize = () => {
@ -270,38 +193,54 @@ const onUnmounted = () => {
chartInstance.dispose();
}
};
function createChartData(data, type) {
const dates = []
const values = []
const typeList = data[type] || []
typeList.forEach(item => {
dates.push(proxy.$utils.formatTime(item.createTime, '{h}:{m}'))
values.push(item.value)
})
return { dates, values }
}
function formatData(data) {
const obj = {};
const res = {};
// data.sort((a,b) => soilTypes.indexOf(a.identifier) - soilTypes.indexOf(b.identifier))
data.forEach(item => {
const identifier = item.identifier;
if (!identifier || identifier === 'null') return false;
obj[identifier] ? obj[identifier].push(item) : (obj[identifier] = [item])
})
weatherTypes.forEach(type => {
res[type] = obj[type] || []
})
return res;
}
</script>
<template>
<div class="weather-info">
<div class="info-list">
<div class="info-item" :class="{ 'info-item-selected': selectedMetric === 'temperature' }" @click="handleMetricClick('temperature')">
<div class="info-item" :class="{ 'info-item-selected': selectedMetric === 'Temperature' }" @click="handleMetricClick('Temperature')">
<div class="icon-container">
<Sunrise class="icon" />
</div>
<div class="info-content">
<div class="info-label">温度</div>
<div class="info-value">{{ weatherInfo.current.temperature }}°C</div>
</div>
</div>
<div class="info-item" :class="{ 'info-item-selected': selectedMetric === 'humidity' }" @click="handleMetricClick('humidity')">
<div class="icon-container">
<Odometer class="icon" />
</div>
<div class="info-content">
<div class="info-label">湿度</div>
<div class="info-value">{{ weatherInfo.current.humidity }}%</div>
<div class="info-value">{{ liveData.Temperature }}°C</div>
</div>
</div>
<div class="info-item" :class="{ 'info-item-selected': selectedMetric === 'windSpeed' }" @click="handleMetricClick('windSpeed')">
<div class="info-item" :class="{ 'info-item-selected': selectedMetric === 'WindSpeed' }" @click="handleMetricClick('WindSpeed')">
<div class="icon-container">
<WindPower class="icon" />
</div>
<div class="info-content">
<div class="info-label">风速</div>
<div class="info-value">{{ weatherInfo.current.windSpeed }} m/s</div>
<div class="info-value">{{ liveData.WindSpeed }} m/s</div>
</div>
</div>
@ -311,27 +250,27 @@ const onUnmounted = () => {
</div>
<div class="info-content">
<div class="info-label">风向</div>
<div class="info-value">{{ weatherInfo.current.windDirection }}</div>
<div class="info-value">{{ liveData.WindDirection }}</div>
</div>
</div>
<div class="info-item" :class="{ 'info-item-selected': selectedMetric === 'rainfall' }" @click="handleMetricClick('rainfall')">
<div class="info-item" :class="{ 'info-item-selected': selectedMetric === 'DailyRainfall' }" @click="handleMetricClick('DailyRainfall')">
<div class="icon-container">
<Pouring class="icon" />
</div>
<div class="info-content">
<div class="info-label">降雨量</div>
<div class="info-value">{{ weatherInfo.current.rainfall }} mm</div>
<div class="info-value">{{ liveData.DailyRainfall }} mm</div>
</div>
</div>
<div class="info-item" :class="{ 'info-item-selected': selectedMetric === 'uvIndex' }" @click="handleMetricClick('uvIndex')">
<div class="info-item" :class="{ 'info-item-selected': selectedMetric === 'Illuminance' }" @click="handleMetricClick('Illuminance')">
<div class="icon-container">
<Sunny class="icon" />
</div>
<div class="info-content">
<div class="info-label">紫外线</div>
<div class="info-value">{{ weatherInfo.current.uvIndex }}</div>
<div class="info-label">光照</div>
<div class="info-value">{{ liveData.Illuminance }}</div>
</div>
</div>
</div>

@ -0,0 +1,187 @@
/*
* @Author: chris
* @Date: 2025-10-14 10:12:41
* @LastEditors: chris
* @LastEditTime: 2025-10-15 10:34:32
*/
export const baseInfoDict = {
plantingArea: {
svg: "nongshi",
text: "种植面积(亩)",
},
terrain: {
svg: "dixing",
text: "地形",
},
variety: {
svg: "pinzhong",
text: "种植品种(种)",
},
type: {
svg: "dapengzhongmiaoguanli",
text: "主要品种",
},
plantingNum: {
svg: "shuguo",
text: "种植荔枝(棵)",
},
yield: {
svg: "caizhaiguanli",
text: "去年产量(吨)",
},
};
export const weatherTypes = [
"Temperature",
"Illuminance",
"WindSpeed",
"WindDirection",
"DailyRainfall",
];
export const weatherDict = {
Temperature: {
name: "温度",
unit: "°C",
color: "#FF6B6B",
icon: "temperature2",
min: 0,
max: 40,
interval: 10,
},
Illuminance: {
name: "光照",
unit: "lux",
color: "#FFD166",
icon: "light",
min: 0,
max: 3200,
interval: 800,
},
WindSpeed: {
name: "风速",
unit: "km/h",
color: "#009688",
icon: "windPower",
min: 0,
max: 10,
interval: 2,
},
WindDirection: {
name: "风向",
unit: "°",
color: "#845EC2",
icon: "wind",
},
DailyRainfall: {
name: "降雨量",
unit: "mm",
color: "#6A0572",
icon: "rain",
min: 0,
max: 10,
interval: 2,
},
};
export const soilTypes = [
"SoilTemperature",
"SoilHumidity",
"EC",
"PH",
"Nitrogen",
"Phosphorus",
"Potassium",
];
// 土壤数据 - 使用土壤设备常用的7个参数
export const soilDict = {
SoilTemperature: {
id: "SoilTemperature",
name: "土壤温度",
value: "22.5",
unit: "°C",
icon: "turangwendu",
color: "#ff7a45",
status: "normal",
description: "适宜作物生长",
},
SoilHumidity: {
id: "SoilHumidity",
name: "土壤湿度",
value: "65",
unit: "%",
icon: "turangshidu",
color: "#4096ff",
status: "normal",
description: "湿润度良好",
},
EC: {
id: "EC",
name: "电导率",
value: "0.8",
unit: "μS/cm",
icon: "turangdiandaoshuai",
color: "#73d13d",
status: "normal",
description: "盐分适中",
},
PH: {
id: "PH",
name: "土壤pH值",
value: "6.8",
unit: "",
icon: "suanjiandu",
color: "#ff9800",
status: "normal",
description: "酸碱度适宜",
},
Nitrogen: {
id: "Nitrogen",
name: "氮含量",
value: "35",
unit: "mg/kg",
icon: "turangdanhanliang",
color: "#fa541c",
status: "normal",
description: "肥力良好",
},
Phosphorus: {
id: "Phosphorus",
name: "磷含量",
value: "22",
unit: "mg/kg",
icon: "turanglinhanliang",
color: "#9c27b0",
status: "normal",
description: "含量适中",
},
Potassium: {
id: "Potassium",
name: "钾含量",
value: "85",
unit: "mg/kg",
icon: "turangjiahanliang",
color: "#2196f3",
status: "normal",
description: "钾素充足",
},
};
export const pestTypeDict = {
蚜虫: {
color: "#ff6b6b",
},
红蜘蛛: {
color: "#ffa502",
},
果蝇: {
color: "#5f27cd",
},
菜青虫: {
color: "#10ac84",
},
其他: {
color: "#54a0ff",
},
};

@ -2,25 +2,39 @@
* @Author: chris
* @Date: 2025-08-22 10:10:51
* @LastEditors: chris
* @LastEditTime: 2025-09-04 17:51:53
* @LastEditTime: 2025-10-15 16:02:36
-->
<script setup>
import { useRoute } from 'vue-router'
import ScreenHeader from './components/ScreenHeader.vue'
import OrchardBriefInfo from './components/OrchardBriefInfo.vue'
import WeatherInfo from './components/WeatherInfo.vue';
import PestInfo from './components/PestInfo.vue';
import WarningInfo from './components/WarningInfo.vue';
// import WarningInfo from './components/WarningInfo.vue';
import SoilInfo from './components/SoilInfo.vue';
import AerialPhoto from './components/AerialPhoto.vue';
import TimeBox from './components/TimeBox.vue';
import { BorderBox12, Loading } from '@kjgl77/datav-vue3'
import { BorderBox12, Loading as DvLoading } from '@kjgl77/datav-vue3'
import useDraw from '@/hooks/useDraw';
import mock from './mock';
import { getDeviceDataAnalysis } from '@/api/deviceData';
import { getDept } from '@/api/system/dept'
const { proxy } = getCurrentInstance()
const route = useRoute()
const loading = ref(false)
const hoursRange = getDateRange(1)
const daysRange = getDateRange(7)
const pestList = ref([])
const soilList = ref([])
const weatherList = ref([])
const baseInfo = ref({})
const { screenRef, calcRate, windowDraw, unWindowDraw } = useDraw()
const orchardId = route.query.id || ''
getData()
onMounted(() => {
calcRate()
@ -30,6 +44,86 @@ onMounted(() => {
onUnmounted(() => {
unWindowDraw()
})
function getData() {
loading.value = true
Promise.all([
getPestAnalysisData(),
getSoilAnalysisData(),
getWeatherAnalysisData(),
getBaseInfo()
]).then(res => {
console.log('res', res)
pestList.value = res[0].rows
soilList.value = res[1].rows
weatherList.value = res[2].rows
baseInfo.value = res[3].data
}).finally(() => {
loading.value = false
})
}
async function getPestAnalysisData () {
try {
return await getDeviceDataAnalysis({
beginTime: hoursRange[0],
endTime: hoursRange[1],
type: 1,
deptId: orchardId
})
} catch (error) {
proxy.$modal.msgError('获取虫情数据失败: ' + (error.message || '未知错误'))
console.error('Failed to get soil analysis data:', error)
return error
}
}
async function getSoilAnalysisData () {
try {
return await getDeviceDataAnalysis({
beginTime: daysRange[0],
endTime: daysRange[1],
type: 2,
deptId: orchardId
})
} catch (error) {
proxy.$modal.msgError('获取土壤数据失败: ' + (error.message || '未知错误'))
console.error('Failed to get soil analysis data:', error)
return error
}
}
async function getWeatherAnalysisData () {
try {
return await getDeviceDataAnalysis({
beginTime: hoursRange[0],
endTime: hoursRange[1],
type: 3,
deptId: orchardId
})
} catch (error) {
proxy.$modal.msgError('获取气象数据失败: ' + (error.message || '未知错误'))
console.error('Failed to get soil analysis data:', error)
return error
}
}
async function getBaseInfo () {
try {
return await getDept(orchardId)
} catch (error) {
proxy.$modal.msgError('获取果园信息失败: ' + (error.message || '未知错误'))
console.error('Failed to get orchard info:', error)
return error
}
}
// n
function getDateRange (num) {
const nowDate = new Date().toLocaleString().replace(/\//g, '-')
const preDate = new Date(+new Date(nowDate) - num * 24 * 60 * 60 * 1000).toLocaleString().replace(/\//g, '-')
return [preDate, nowDate]
}
</script>
<template>
@ -37,11 +131,11 @@ onUnmounted(() => {
<!-- 背景 -->
<div class="bg" ref="screenRef">
<!-- 加载动画 -->
<loading v-if="loading">
<dv-loading v-if="loading">
<div color-white>
正在加载...
</div>
</loading>
</dv-loading>
<div class="screen-body" v-else>
<!-- 头部 -->
<div class="screen-header">
@ -53,12 +147,12 @@ onUnmounted(() => {
<div class="screen-content-left">
<div class="content-left-top">
<border-box-12>
<orchard-brief-info :info="mock.info"/>
<orchard-brief-info :info="baseInfo"/>
</border-box-12>
</div>
<div class="content-left-bottom">
<border-box-12>
<weather-info />
<weather-info :weather-data="weatherList" />
</border-box-12>
</div>
</div>
@ -66,13 +160,13 @@ onUnmounted(() => {
<div class="screen-content-right">
<div class="content-right-top">
<border-box-12>
<pest-info class="pest-info-module" />
<warning-info class="warning-info-module" />
<pest-info class="pest-info-module" :pest-data="pestList" />
<!-- <warning-info class="warning-info-module" /> -->
</border-box-12>
</div>
<div class="content-right-bottom">
<border-box-12>
<soil-info />
<soil-info :soil-data="soilList" />
</border-box-12>
<border-box-12>
<aerial-photo />
@ -125,11 +219,11 @@ onUnmounted(() => {
@apply w-25% h-full;
.content-left-top {
@apply h-40%;
@apply h-32%;
}
.content-left-bottom {
@apply h-60%;
@apply h-68%;
}
}
@ -137,7 +231,7 @@ onUnmounted(() => {
@apply flex-1 h-full;
.content-right-top {
@apply h-62%;
@apply h-58%;
:deep(.border-box-content) {
@apply flex flex-col;
@ -145,7 +239,7 @@ onUnmounted(() => {
}
.content-right-bottom {
@apply flex h-38%;
@apply flex h-42%;
> :nth-child(1) {
flex: 4;
@ -158,7 +252,7 @@ onUnmounted(() => {
}
.pest-info-module {
@apply h-60%;
@apply h-100%;
}
.warning-info-module {

@ -2,16 +2,14 @@
* @Author: chris
* @Date: 2025-08-26 16:53:44
* @LastEditors: chris
* @LastEditTime: 2025-09-01 09:22:27
* @LastEditTime: 2025-10-14 10:20:42
*/
export default {
info: {
acreage: 1000,
plantingArea: 1000,
terrain: "山地",
typeNum: "10",
type: "桂味",
lichee: "2000",
yield: "82",
plantingNum: "10",
variety: "桂味",
},
weatherInfo: [
{

@ -2,10 +2,28 @@
* @Author: chris
* @Date: 2025-09-05 09:39:28
* @LastEditors: chris
* @LastEditTime: 2025-09-08 09:37:55
* @LastEditTime: 2025-09-26 17:37:26
-->
<script setup name="DeviceList">
import DeviceFlatList from '@/components/deviceFlatList'
const props = defineProps({
modelValue: {
type: String,
default: null,
},
deviceList: {
type: Array,
default: () => [],
}
})
const emit = defineEmits(['update:modelValue', 'change'])
function handleChangeDevice(device) {
emit('update:modelValue', device.id)
emit('change', device)
}
</script>
<template>
@ -15,7 +33,7 @@ import DeviceFlatList from '@/components/deviceFlatList'
<span>设备列表</span>
</div>
</template>
<device-flat-list />
<device-flat-list :list="deviceList" @change="handleChangeDevice" />
</el-card>
</template>

@ -2,18 +2,27 @@
* @Author: chris
* @Date: 2025-08-12 11:19:45
* @LastEditors: chris
* @LastEditTime: 2025-08-14 17:15:58
* @LastEditTime: 2025-10-15 14:58:52
-->
<template>
<div class="image-detail-module">
<h3 class="image-detail-title">分析报告</h3>
<div class="image-detail-content" v-if="!isEmptyObject(props.info)">
<div class="detail-item">
<span>{{ props.info.name }}:</span> <span class="num">{{ props.info.num }}</span>
<span>{{ props.info.name }}:</span> <span class="num">{{ props.info.value }}</span>
</div>
<div class="detail-item" v-for="item in detailShowKeys" :key="item.key">
<span>{{ item.name }}:</span> <span>{{ props.info[item.key] }}</span>
</div>
<div class="detail-item">
<span>分析员:</span> <span>AI</span>
</div>
<div class="detail-img">
<p>分析图片:</p>
<div class="analysis-img">
<ImagePreview :src="props.info.analyseCoordUrl" width="100%" />
</div>
</div>
</div>
<el-empty description="请点击“查看”按钮查看" v-else></el-empty>
</div>
@ -22,6 +31,7 @@
<script setup>
import { detailShowKeys } from '../config';
import { isEmptyObject } from '@/utils/validate.js'
import ImagePreview from '@/components/ImagePreview/index.vue'
const props = defineProps({
info: {
@ -53,4 +63,10 @@ const props = defineProps({
color: $--color-danger;
}
}
.detail-img {
p {
@apply font-700 mb-20px;
}
}
</style>

@ -10,13 +10,15 @@
:class="{ 'selected': selectedItems.includes(item.id) }"
>
<!-- 复选框 -->
<div class="checkbox-container">
<el-checkbox v-model="selectedItems" :value="item.id" size="large"></el-checkbox>
</div>
<!-- <div class="checkbox-container">
<el-checkbox-group v-model="selectedItems">
<el-checkbox :value="item.id" size="large"></el-checkbox>
</el-checkbox-group>
</div> -->
<!-- 图片预览 -->
<div class="image-container">
<ImagePreview :src="item.imageUrl" width="100%" height="180px" />
<ImagePreview :src="item.analyseCoordUrl" width="100%" height="180px" />
</div>
<!-- 卡片内容 -->
@ -24,15 +26,15 @@
<div class="card-info">
<div class="info-item">
<span class="label">设备编号:</span>
<span class="value">{{ item.deviceId }}</span>
<span class="value">{{ item.deviceNo }}</span>
</div>
<div class="info-item">
<span class="label">拍摄时间:</span>
<span class="value">{{ formatDate(item.captureTime) }}</span>
<span class="value">{{ formatDate(item.createTime) }}</span>
</div>
<div class="info-item">
<span class="label">害虫数量:</span>
<span class="value">{{ item.pestCount || 0 }}</span>
<span class="value">{{ item.value || 0 }}</span>
</div>
</div>
@ -40,12 +42,13 @@
<div class="action-buttons">
<el-button
size="small"
type="primary"
@click="handleReport(item)"
icon="view"
>
查看
</el-button>
<el-button
<!-- <el-button
type="primary"
size="small"
@click="handleAnalyze(item.id)"
@ -60,7 +63,7 @@
icon="Delete"
>
删除
</el-button>
</el-button> -->
</div>
</div>
</el-card>

@ -2,23 +2,23 @@
* @Author: chris
* @Date: 2025-08-13 09:54:21
* @LastEditors: chris
* @LastEditTime: 2025-08-13 10:00:09
* @LastEditTime: 2025-09-26 17:23:42
*/
export const detailShowKeys = [
{
key: "createTime",
name: "采样时间",
},
{
key: "verifyTime",
name: "分析时间",
},
// {
// key: "verifyTime",
// name: "分析时间",
// },
{
key: "remark",
name: "备注",
},
{
key: "analyst",
name: "分析员",
},
// {
// key: "analyst",
// name: "分析员",
// },
];

@ -2,13 +2,13 @@
* @Author: chris
* @Date: 2025-08-12 11:20:07
* @LastEditors: chris
* @LastEditTime: 2025-09-05 15:07:36
* @LastEditTime: 2025-10-15 11:29:04
-->
<template>
<div :class="['img-analysis-page', 'page-height', { 'hasTagsView': hasTags }]">
<el-row :gutter="10" class="h-full">
<el-col :span="4">
<device-list class="device-tree-card" :deviceList="deviceList" v-model="deviceId" />
<device-list class="device-tree-card" :deviceList="deviceList" v-model="queryParams.deviceId" @change="handleChangeDevice" />
</el-col>
<el-col :span="20">
<el-card>
@ -35,15 +35,63 @@ import DeviceList from "./components/DeviceList.vue";
import ImageList from "./components/ImageList.vue";
import ImageDetail from "./components/ImageDetail.vue";
import { imageListData } from "./mockData";
import { listDevice } from '@/api/device'
import { listDeviceData } from '@/api/deviceData'
const { proxy } = getCurrentInstance()
const { hasTags } = useSettings()
const deviceList = ref([])
const deviceId = ref(null)
const imageList = ref(imageListData)
const selectedRows = ref([])
const checkItem = ref({})
const loading = ref(false)
const deviceLoading = ref(false)
const total = ref(0)
const queryParams = ref({
pageNum: 1,
pageSize: 6,
type: 1,
deviceId: null,
beginTime: null,
endTime: null
})
getDeviceList()
getDeviceDataList()
async function getDeviceList () {
const params = {
pageNum: 1,
pageSize: 50,
type: 1
}
deviceLoading.value = true;
try {
const res = await listDevice(params);
deviceList.value = res.rows;
} catch (error) {
proxy.$modal.msgError('获取虫情设备数据失败: ' + (error.message || '未知错误'))
console.error('Failed to get pest list:', error)
} finally {
deviceLoading.value = false;
}
}
async function getDeviceDataList () {
loading.value = true;
try {
const res = await listDeviceData(queryParams.value);
imageList.value = res.rows;
total.value = res.total;
} catch (error) {
proxy.$modal.msgError('获取虫情设备实时数据失败: ' + (error.message || '未知错误'))
console.error('Failed to get device live data:', error)
} finally {
loading.value = false;
}
}
function handleQuery() {
}
@ -65,6 +113,10 @@ function handleReport(item) {
checkItem.value = item
}
function handleChangeDevice() {
getDeviceDataList()
}
</script>
<style scss scoped>

@ -117,7 +117,7 @@ const liveData = ref({})
const queryParams = ref({
pageNum: 1,
pageSize: 20,
deviceType: 1,
type: 1,
deviceId: null,
beginTime: null,
endTime: null
@ -138,6 +138,7 @@ async function getDeviceList () {
const params = {
pageNum: 1,
pageSize: 20,
type: 1
}
loading.value = true;
try {

@ -2,23 +2,36 @@
* @Author: chris
* @Date: 2025-08-18 09:24:37
* @LastEditors: chris
* @LastEditTime: 2025-08-18 14:41:04
* @LastEditTime: 2025-09-29 14:36:40
-->
<script setup name="DeviceList">
const props = defineProps({
modelValue: {
type: Array,
default: () => []
},
devices: {
type: Array,
default: () => []
}
})
const emits = defineEmits(['selected'])
const emits = defineEmits(['selected', 'update:modelValue'])
const checkList = ref([]);
const list = ref([]);
watch(() => checkList, (val) => {
const checkList = computed({
set: (val) => {
list.value = val;
emits('update:modelValue', val)
emits('selected', val);
},
get: () => list.value
})
watch(() => props.modelValue, (val) => {
list.value = val;
}, { immediate: false })
</script>
<template>
@ -26,7 +39,7 @@ watch(() => checkList, (val) => {
<el-card>
<template #header>设备列表</template>
<el-checkbox-group size="large" v-model="checkList">
<el-checkbox v-for="item in props.devices" :label="item.name" :value="item.id" />
<el-checkbox v-for="item in props.devices" :label="item.model" :value="item.id" />
</el-checkbox-group>
</el-card>
</div>

@ -2,32 +2,38 @@
* @Author: chris
* @Date: 2025-08-18 09:27:23
* @LastEditors: chris
* @LastEditTime: 2025-08-18 17:18:54
* @LastEditTime: 2025-09-30 17:14:44
-->
<script setup>
const props = defineProps({
params: {
type: Object,
default: () => ({
deviceId: null,
dateRange: []
})
default: () => ({})
}
})
const emits = defineEmits(['update:params'])
const queryRef = ref()
const queryParams = reactive({...props.params});
const dateRange = ref([])
const pestOpts = ref([])
watch(() => queryParams, (newVal) => {
emits('update:params', newVal)
watch(() => dateRange.value, (newVal) => {
if (!newVal) {
props.params.beginTime = props.params.endTime = ''
return
}
props.params.beginTime = newVal[0] || ''
props.params.endTime = newVal[1] || ''
})
watch(() => props.params, (newVal) => {
Object.assign(queryParams, newVal)
})
console.log('watch', newVal)
dateRange.value = [newVal.beginTime, newVal.endTime]
// nextTick(() => {
// dateRange.value = [newVal.beginTime, newVal.endTime]
// })
}, { immediate: false })
function resetQuery() {
emits('reset')
@ -43,16 +49,18 @@ function exportData() {
</script>
<template>
<el-form :model="queryParams" ref="queryRef" :inline="true" label-width="68px">
<el-form :model="props.params" ref="queryRef" :inline="true" label-width="68px">
<el-form-item label="创建时间">
<el-date-picker
style="width: 240px"
v-model="queryParams.dateRange"
value-format="YYYY-MM-DD"
style="width: 300px"
v-model="dateRange"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date(0,0,0,0,0,0), new Date(0,0,0,23,59,59)]"
></el-date-picker>
</el-form-item>
<el-form-item>

@ -2,7 +2,7 @@
* @Author: chris
* @Date: 2025-08-18 09:36:11
* @LastEditors: chris
* @LastEditTime: 2025-08-21 11:10:48
* @LastEditTime: 2025-09-29 15:19:28
-->
<script setup>
import { BarChart } from 'echarts/charts';
@ -23,10 +23,10 @@ const props = defineProps({
chartData: {
type: Object,
required: true,
default: () => ({
xAxisData: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
seriesData: [120, 200, 150, 80, 70, 110, 130]
})
// default: () => ({
// names: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
// data: [120, 200, 150, 80, 70, 110, 130]
// })
}
});
@ -67,15 +67,22 @@ function createOption (data) {
},
xAxis: {
type: 'category',
data: data.xAxisData
data: data.names
},
yAxis: {
type: 'value'
},
series: [
{
data: data.seriesData,
data: data.data,
type: 'bar',
colorBy: 'series',
itemStyle: {
color: ({ dataIndex }) => {
const colorList = getItemsColorList(data.data?.length || 0)
return colorList[dataIndex]
}
},
}
]
};
@ -87,6 +94,34 @@ function registerComponents() {
proxy.$echarts.use([TitleComponent, TooltipComponent, GridComponent, DatasetComponent, TransformComponent, BarChart, LabelLayout, UniversalTransition, CanvasRenderer]);
}
/**
* 随机生成暗色
* 确保生成的颜色不是亮色通过控制RGB值在较低范围内
* @returns {string} 十六进制颜色代码
*/
function generateDarkColorList() {
// 0-180RGB
const r = Math.floor(Math.random() * 180);
const g = Math.floor(Math.random() * 180);
const b = Math.floor(Math.random() * 180);
//
const toHex = (value) => {
const hex = value.toString(16);
return hex.length === 1 ? '0' + hex : hex;
};
return '#' + toHex(r) + toHex(g) + toHex(b);
}
function getItemsColorList (length) {
const colorList = []
for (let i = 0; i < length; i++) {
colorList.push(generateDarkColorList())
}
return colorList
}
function calcBoxSize () {}
</script>

@ -2,41 +2,38 @@
* @Author: chris
* @Date: 2025-08-20 17:06:41
* @LastEditors: chris
* @LastEditTime: 2025-08-21 10:47:52
* @LastEditTime: 2025-09-29 15:13:15
-->
<script setup>
const props = defineProps({
params: {
type: Object,
default: () => ({
pestTypes: []
})
},
options: {
type: Array,
default: () => []
}
})
const emits = defineEmits(['update:params'])
const emits = defineEmits(['pestChange'])
const queryParams = ref({...props.params});
const pestTypes = ref([])
watch(() => queryParams.value, (newVal) => {
emits('update:params', newVal)
watch(() => props.options, (newVal) => {
pestTypes.value = newVal.map(item => item.name)
})
function handlePestChange(newVal) {
emits('pestChange', newVal)
}
</script>
<template>
<el-row class="chart-action">
<el-col :span="12" :offset="18">
<el-select v-model="queryParams.pestTypes" placeholder="请选择" style="width: 240px;">
<el-select v-model="pestTypes" @change="handlePestChange" multiple placeholder="请选择" style="width: 240px;">
<el-option
v-for="item in options"
multiple
:key="item.value"
:label="item.label"
:value="item.value"
v-for="item in props.options"
:key="item.id"
:label="item.name"
:value="item.name"
/>
</el-select>
</el-col>

@ -0,0 +1,18 @@
/*
* @Author: chris
* @Date: 2025-10-10 16:08:07
* @LastEditors: chris
* @LastEditTime: 2025-10-10 16:08:52
*/
const nowDate = new Date().toLocaleDateString().replace(/\//g, "-");
const nextDate = new Date(+new Date(nowDate) + 24 * 60 * 60 * 1000)
.toLocaleDateString()
.replace(/\//g, "-");
export const defaultParams = {
pageNum: 1,
deviceType: 1,
deviceId: null,
beginTime: `${nowDate} 00:00:00`,
endTime: `${nextDate} 23:59:59`,
};

@ -2,7 +2,7 @@
* @Author: chris
* @Date: 2025-08-18 09:24:26
* @LastEditors: chris
* @LastEditTime: 2025-09-05 09:06:57
* @LastEditTime: 2025-10-10 17:25:04
-->
<script setup name="PestStatistics">
import { useSettings } from "@/hooks/useSettings";
@ -11,27 +11,129 @@ import SearchForm from './components/SearchForm.vue';
import StatisticsChart from './components/StatisticsChart.vue';
import ChartAction from "./components/chartAction.vue";
import { devices } from './mockData'
import { listDevice } from '@/api/device'
import { listDeviceData } from '@/api/deviceData'
import { defaultParams } from './config'
const { hasTags } = useSettings()
const { proxy } = getCurrentInstance();
const nowDate = new Date().toLocaleDateString().replace(/\//g, '-')
const nextDate = new Date(+new Date(nowDate) + 24 * 60 * 60 * 1000).toLocaleDateString().replace(/\//g, '-')
const deviceList = ref(devices);
const tableData = ref([]);
const queryParams = ref({});
const chartData = ref([]);
const formateData = ref({})
const options = ref([]);
const loading = ref(false);
const deviceLoading = ref(false);
const queryParams = ref({
...defaultParams
})
const checkList = ref([])
const deviceIds = computed({
get: () => {
return checkList.value || []
},
set: (value) => {
checkList.value = value
queryParams.value.deviceId = value.join(',')
getList()
}
})
getDeviceList();
function getList () {
async function getList () {
loading.value = true;
try {
const res = await listDeviceData(queryParams.value);
// imageList.value = res.rows;
formateData.value = formatData(res.rows)
chartData.value = createChartData(formateData.value);
options.value = createPestOption(chartData.value.names)
} catch (error) {
proxy.$modal.msgError('获取虫情设备实时数据失败: ' + (error.message || '未知错误'))
console.error('Failed to get device live data:', error)
} finally {
loading.value = false;
}
}
async function getDeviceList () {
const params = {
pageNum: 1,
pageSize: 50,
type: 1
}
deviceLoading.value = true;
try {
const res = await listDevice(params);
deviceList.value = res.rows;
deviceIds.value = res.rows.map(item => item.id)
} catch (error) {
proxy.$modal.msgError('获取虫情设备数据失败: ' + (error.message || '未知错误'))
console.error('Failed to get pest list:', error)
} finally {
deviceLoading.value = false;
}
}
function handleSelect(items) {
console.log(items);
// queryParams.value.deviceId = items.join(',')
// getList()
}
function handlePestChange(names) {
console.log('handlePestChange', names)
const filterRes = {}
names.forEach(name => {
filterRes[name] = formateData.value[name] || []
})
chartData.value = createChartData(filterRes)
}
function exportView() {
console.log('导出视图')
}
function resetQuery() {
console.log('重置查询')
queryParams.value = {
...defaultParams
}
getList()
}
function formatData (data) {
const res = {}
data.forEach(item => {
res[item.name] ? res[item.name].push(item) : res[item.name] = [item]
})
return res
}
function createChartData(data) {
const obj = {}
const names = Object.keys(data)
names.forEach(key => {
obj[key] = data[key].reduce((pre, cur) => pre + (cur.value || 0), 0)
})
// formateData.value = obj
return {
names,
data: Object.values(obj)
}
}
function createPestOption (names) {
return names.map((name, index) => ({
id: index + 1,
name
}))
}
</script>
@ -39,12 +141,12 @@ function resetQuery() {
<div :class="['pest-statistics-page', 'page-height', { 'hasTagsView': hasTags }]">
<el-row :gutter="40" class="h-100%">
<el-col :span="4">
<device-list :devices="deviceList" @select="handleSelect"></device-list>
<device-list v-model="deviceIds" :devices="deviceList" @selected="handleSelect"></device-list>
</el-col>
<el-col :span="20">
<search-form v-model:params="queryParams" @reset="resetQuery" @query="getList" @export="exportView" />
<chart-action v-model:params="queryParams" :options="options" class="action" />
<statistics-chart :tableData="tableData" class="chart"/>
<chart-action :options="options" @pestChange="handlePestChange" class="action" />
<statistics-chart :chartData="chartData" class="chart"/>
</el-col>
</el-row>
</div>

@ -0,0 +1,43 @@
<!--
* @Author: chris
* @Date: 2025-08-08 16:17:54
* @LastEditors: chris
* @LastEditTime: 2025-10-15 17:31:37
-->
<template>
<el-table
v-loading="loading"
:data="dataList"
@selection-change="handleSelectionChange"
row-key="orchardId"
border
highlight-current-row
style="width: 100%"
>
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="内容" align="center" prop="content" v-if="columns[0].visible" :show-overflow-tooltip="true"/>
<el-table-column label="状态" align="center" prop="status" v-if="columns[1].visible" />
<el-table-column label="模板" align="center" prop="template" v-if="columns[2].visible" />
<el-table-column label="推送时间" align="center" prop="createTime" v-if="columns[3].visible">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
</el-table>
</template>
<script setup>
import { parseTime } from '@/utils/ruoyi'
const props = defineProps({
dataList: { type: Array, required: true },
loading: { type: Boolean, required: true },
columns: { type: Array, required: true }
})
const emits = defineEmits(['selection-change'])
function handleSelectionChange(selection) {
emits('selection-change', selection)
}
</script>

@ -0,0 +1,69 @@
<!--
* @Author: chris
* @Date: 2025-08-18 09:27:23
* @LastEditors: chris
* @LastEditTime: 2025-10-15 17:34:07
-->
<script setup>
const props = defineProps({
params: {
type: Object,
default: () => ({})
}
})
const emits = defineEmits(['update:params', 'reset', 'query', 'export'])
const queryRef = ref()
const queryParams = ref({...props.params});
const dateRange = ref(null);
watch(() => dateRange.value, () => {
if (!dateRange.value.length) return;
queryParams.value.beginTime = dateRange.value[0]
queryParams.value.endTime = dateRange.value[1]
})
watch(() => queryParams.value, (newVal) => {
emits('update:params', newVal)
}, {deep: true})
watch(() => props.params, (newVal) => {
Object.assign(queryParams.value, newVal)
})
function resetQuery() {
dateRange.value = [];
emits('reset', queryRef.value)
}
function handleQuery() {
// query
emits('query')
}
function exportData() {
emits('export')
}
</script>
<template>
<el-form :model="queryParams" ref="queryRef" :inline="true" label-width="68px">
<el-form-item label="记录时间">
<el-date-picker
style="width: 300px"
v-model="dateRange"
value-format="YYYY-MM-DD"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
></el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery"></el-button>
<el-button type="warning" icon="Download" @click="exportData"></el-button>
<el-button icon="Refresh" @click="resetQuery"></el-button>
</el-form-item>
</el-form>
</template>

@ -0,0 +1,301 @@
<!--
* @Author: chris
* @Date: 2025-08-18 09:36:11
* @LastEditors: chris
* @LastEditTime: 2025-10-16 10:29:23
-->
<script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue';
import { BarChart } from 'echarts/charts';
//
import {
TitleComponent,
TooltipComponent,
GridComponent,
DatasetComponent,
TransformComponent
} from 'echarts/components';
//
import { LabelLayout, UniversalTransition } from 'echarts/features';
// Canvas
import { CanvasRenderer } from 'echarts/renderers';
const props = defineProps({
chartData: {
type: Object,
default: () => ({
names: [],
data: []
})
}
});
//
const { proxy } = getCurrentInstance();
//
const barChartRef = ref(null);
// ECharts
let barChart = null;
// 715
const selectedDays = ref(7);
const dateRange = ref([]);
const emits = defineEmits(['dayRangeChange']);
// ECharts
function registerComponents() {
proxy.$echarts.use([
TitleComponent,
TooltipComponent,
GridComponent,
DatasetComponent,
TransformComponent,
BarChart,
LabelLayout,
UniversalTransition,
CanvasRenderer
]);
}
//
// function generateDates(days) {
// const dates = [];
// const today = new Date();
// for (let i = days - 1; i >= 0; i--) {
// const date = new Date(today);
// date.setDate(date.getDate() - i);
// // MM/DD
// dates.push(`${date.getMonth() + 1}/${date.getDate()}`);
// }
// return dates;
// }
// //
// function generateMessageCount(days) {
// const counts = [];
// for (let i = 0; i < days; i++) {
// // 10-200
// const count = Math.floor(Math.random() * 190) + 10;
// counts.push(count);
// }
// return counts;
// }
// //
// function initChartData() {
// const dates = generateDates(selectedDays.value);
// const messageCounts = generateMessageCount(selectedDays.value);
// return {
// names: dates,
// data: messageCounts
// };
// }
//
function createOption(data) {
return {
title: {
text: '消息推送统计',
left: 'center',
textStyle: {
fontSize: 16,
fontWeight: 'bold'
}
},
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(0, 0, 0, 0.7)',
borderColor: '#333',
textStyle: {
color: '#fff'
},
formatter: function(params) {
const data = params[0];
return `${data.name}<br/>推送数量: ${data.value}`;
},
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
}
},
grid: {
left: '3%',
right: '4%',
bottom: '10%',
top: '15%',
containLabel: true
},
xAxis: {
type: 'category',
data: data.names,
axisLine: {
lineStyle: {
color: '#464646'
}
},
axisLabel: {
color: '#333'
}
},
yAxis: {
type: 'value',
name: '推送数量',
nameTextStyle: {
color: '#333'
},
min: 0,
axisLine: {
lineStyle: {
color: '#464646'
}
},
axisLabel: {
color: '#333'
},
splitLine: {
lineStyle: {
color: '#f0f0f0',
type: 'dashed'
}
}
},
series: [
{
name: '推送数量',
type: 'bar',
data: data.data,
itemStyle: {
color: new proxy.$echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: '#4dabf7'
},
{
offset: 1,
color: '#228be6'
}
])
},
emphasis: {
itemStyle: {
color: new proxy.$echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: '#339af0'
},
{
offset: 1,
color: '#1c7ed6'
}
])
}
},
//
label: {
show: true,
position: 'top',
color: '#333'
}
}
]
};
}
//
function initBarChart() {
if (barChartRef.value && proxy.$echarts) {
//
if (barChart) {
barChart.dispose();
}
barChart = proxy.$echarts.init(barChartRef.value);
barChart.setOption(createOption(props.chartData));
}
}
//
function handleResize() {
if (barChart) {
barChart.resize();
}
}
//
function handleTimeRangeChange(days) {
selectedDays.value = days;
dateRange.value = proxy.$utils.getDateRange(days);
emits('dayRangeChange', dateRange.value);
}
//
watch(() => props.chartData, (newData) => {
if (barChart) {
barChart.setOption(createOption(newData));
}
}, { immediate: true });
//
onMounted(() => {
registerComponents();
initBarChart();
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
if (barChart) {
barChart.dispose();
barChart = null;
}
});
</script>
<template>
<div class="statistics-container">
<!-- 时间范围选择器 -->
<div class="time-range-selector">
<el-radio-group v-model="selectedDays" @change="handleTimeRangeChange">
<el-radio-button :value="7">近7天</el-radio-button>
<el-radio-button :value="15">近15天</el-radio-button>
<el-radio-button :value="30">近30天</el-radio-button>
</el-radio-group>
</div>
<!-- 柱状图容器 -->
<div class="chart-container">
<div class="bar-chart" style="height: 100%;" ref="barChartRef"></div>
</div>
</div>
</template>
<style lang="scss" scoped>
.statistics-container {
width: 100%;
height: 100%;
.time-range-selector {
margin-bottom: 10px;
}
.chart-container {
width: 100%;
height: calc(100% - 40px);
.bar-chart {
width: 100%;
height: 100%;
min-height: 340px;
}
}
}
</style>

@ -0,0 +1,26 @@
/*
* @Author: chris
* @Date: 2025-09-05 10:12:41
* @LastEditors: chris
* @LastEditTime: 2025-10-15 17:28:32
*/
// 列配置
export const columnsConfig = [
{ key: 0, label: "内容", visible: true },
{ key: 1, label: "状态", visible: true },
{ key: 2, label: "模板", visible: true },
{ key: 3, label: "时间", visible: true },
];
// 状态颜色映射
export const statusColorMap = {
0: "success",
1: "danger",
};
export const defaultQueryParams = {
pageNum: 1,
pageSize: 10,
beginTime: null,
endTime: null,
};

@ -0,0 +1,103 @@
<!--
* @Author: chris
* @Date: 2025-10-15 17:07:16
* @LastEditors: chris
* @LastEditTime: 2025-10-16 10:26:28
-->
<script setup>
import HistoryTable from './components/HistoryTable.vue';
import SearchForm from './components/SearchForm.vue';
import Statistics from './components/Statistics.vue';
import { columnsConfig, defaultQueryParams } from './config';
import { useSettings } from '@/hooks/useSettings';
import { listDeviceData, exportDeviceData } from '@/api/deviceData';
const { hasTags } = useSettings();
const { proxy } = getCurrentInstance();
const historyList = ref([]);
const chartData = ref([]);
const total = ref(0);
const loading = ref(false);
const queryParams = ref({...defaultQueryParams});
getHistoryList();
async function getHistoryList () {
loading.value = true;
try {
const res = await listDeviceData(queryParams.value);
historyList.value = res.rows;
chartData.value = createChartData(res.rows)
total.value = res.total;
} catch (error) {
proxy.$modal.msgError('获取土壤墒情设备实时数据失败: ' + (error.message || '未知错误'))
console.error('Failed to get device live data:', error)
} finally {
loading.value = false;
}
}
function handleSelectionChange(selection) {
console.log(selection)
}
function handleQuery() {
queryParams.value.pageNum = 1
getHistoryList();
}
function handleReset(queryRef) {
queryParams.value = {...defaultQueryParams}
queryRef.resetFields()
getHistoryList()
}
async function handleExport() {
const fileName = await proxy.download('/business/device-data/export', queryParams.value, `土壤墒情设备数据_${new Date().getTime()}.xlsx`)
fileName && proxy.$download.name(fileName)
}
function createChartData(data) {
const res = {}
data.forEach(item => {
const date = item.createTime?.split(' ')[0]
res[date] ? res[date]++ : (res[date] = 1)
})
return {
names: Object.keys(res),
data: Object.values(res)
}
}
</script>
<template>
<div :class="['push-history-page', 'page-height', { 'hasTagsView': hasTags }]">
<el-card class="history-table-card">
<!-- 搜索表单 -->
<search-form v-model:params="queryParams" class="mb-12px" @query="handleQuery" @reset="handleReset" @export="handleExport" />
<!-- 历史记录表格 -->
<history-table :dataList="historyList" :columns="columnsConfig" :loading="loading" @selection-change="handleSelectionChange" />
<!-- 分页 -->
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getHistoryList" />
</el-card>
<el-card class="analysis-card">
<statistics :chartData="chartData" />
</el-card>
</div>
</template>
<style lang="scss" scoped>
.push-history-page {
@apply p-20px flex flex-col gap-15px;
}
.history-table-card {
@apply flex-1;
}
.analysis-card {
@apply h-380px;
}
</style>

@ -1,5 +1,19 @@
<template>
<div class="app-container">
<el-row :gutter="30">
<el-col :span="6">
<el-card class="mb10">
<template #header>
<p>推送平台</p>
</template>
</el-card>
<el-card>
<template #header>
<p>推送模板</p>
</template>
</el-card>
</el-col>
<el-col :span="18">
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="用户昵称" prop="nickname">
<el-input
@ -101,6 +115,8 @@
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</el-col>
</el-row>
<!-- 添加或修改虫情预警推送人员接收列对话框 -->
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
@ -276,10 +292,12 @@ function handleDelete(row) {
}
/** 导出按钮操作 */
function handleExport() {
async function handleExport() {
proxy.download('business/member/export', {
...queryParams.value
}, `member_${new Date().getTime()}.xlsx`)
const fileName = await proxy.download('/business/member/export', queryParams.value, `推送人员数据_${new Date().getTime()}.xlsx`)
fileName && proxy.$download.name(fileName)
}
getList()

@ -2,22 +2,36 @@
* @Author: chris
* @Date: 2025-08-18 09:24:37
* @LastEditors: chris
* @LastEditTime: 2025-08-18 14:41:04
* @LastEditTime: 2025-09-30 17:37:57
-->
<script setup name="DeviceList">
const props = defineProps({
modelValue: {
type: Array,
default: () => []
},
devices: {
type: Array,
default: () => []
}
})
const emits = defineEmits(['selected'])
const emits = defineEmits(['selected', 'update:modelValue'])
const list = ref([]);
const checkList = ref([]);
// const checkList = ref([]);
const checkList = computed({
get:() => list.value,
set:(val) => {
list.value = val
emits('update:modelValue', val)
emits('selected', val)
}
})
watch(() => checkList, (val) => {
emits('selected', val);
watch(() => props.modelValue, (val) => {
list.value = val
})
</script>
@ -26,7 +40,7 @@ watch(() => checkList, (val) => {
<el-card>
<template #header>设备列表</template>
<el-checkbox-group size="large" v-model="checkList">
<el-checkbox v-for="item in props.devices" :label="item.name" :value="item.id" />
<el-checkbox v-for="item in props.devices" :label="item.model" :value="item.id" />
</el-checkbox-group>
</el-card>
</div>

@ -2,32 +2,38 @@
* @Author: chris
* @Date: 2025-08-18 09:27:23
* @LastEditors: chris
* @LastEditTime: 2025-08-18 17:18:54
* @LastEditTime: 2025-09-30 17:58:54
-->
<script setup>
const props = defineProps({
params: {
type: Object,
default: () => ({
deviceId: null,
dateRange: []
})
default: () => ({})
}
})
const emits = defineEmits(['update:params'])
const queryRef = ref()
const queryParams = reactive({...props.params});
const dateRange = ref([])
const pestOpts = ref([])
watch(() => queryParams, (newVal) => {
emits('update:params', newVal)
watch(() => dateRange.value, (newVal) => {
if (!newVal) {
props.params.beginTime = props.params.endTime = ''
return
}
props.params.beginTime = newVal[0] || ''
props.params.endTime = newVal[1] || ''
})
watch(() => props.params, (newVal) => {
Object.assign(queryParams, newVal)
})
console.log('watch', newVal)
dateRange.value = [newVal.beginTime, newVal.endTime]
// nextTick(() => {
// dateRange.value = [newVal.beginTime, newVal.endTime]
// })
}, { immediate: false })
function resetQuery() {
emits('reset')
@ -43,16 +49,18 @@ function exportData() {
</script>
<template>
<el-form :model="queryParams" ref="queryRef" :inline="true" label-width="68px">
<el-form :model="props.params" ref="queryRef" :inline="true" label-width="68px">
<el-form-item label="创建时间">
<el-date-picker
style="width: 240px"
v-model="queryParams.dateRange"
value-format="YYYY-MM-DD"
style="width: 300px"
v-model="dateRange"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date(0,0,0,0,0,0), new Date(0,0,0,23,59,59)]"
></el-date-picker>
</el-form-item>
<el-form-item>

@ -2,7 +2,7 @@
* @Author: chris
* @Date: 2025-08-18 09:36:11
* @LastEditors: chris
* @LastEditTime: 2025-09-08 15:40:07
* @LastEditTime: 2025-10-09 10:58:32
-->
<script setup>
import { LineChart } from 'echarts/charts';
@ -24,10 +24,15 @@ const props = defineProps({
type: Object,
required: true,
default: () => ({
legend: ['蚜虫', '利蚂', '红蜘蛛', '大蚕蛾', '蝎蝽', '黄螟'],
xAxis: ['2025/06/01', '2025/06/02', '2025/06/03', '2025/06/04', '2025/06/05', '2025/06/06', '2025/06/07'],
series: [{ name: '蚜虫', data: [120, 200, 150, 80, 70, 110, 130]}, { name: '利蚂', data: [120, 200, 150, 80, 70, 110, 130]}, { name: '红蜘蛛', data: [120, 200, 150, 80, 70, 110, 130]}, { name: '大蚕蛾', data: [120, 200, 150, 80, 70, 110, 130]}, { name: '蝎蝽', data: [120, 200, 150, 80, 70, 110, 130]}, { name: '黄螟', data: [120, 200, 150, 80, 70, 110, 130]}]
names: [],
dates: [],
values: []
})
// default: () => ({
// legend: ['', '', '', '', '', ''],
// xAxis: ['2025/06/01', '2025/06/02', '2025/06/03', '2025/06/04', '2025/06/05', '2025/06/06', '2025/06/07'],
// series: [{ name: '', data: [120, 200, 150, 80, 70, 110, 130]}, { name: '', data: [120, 200, 150, 80, 70, 110, 130]}, { name: '', data: [120, 200, 150, 80, 70, 110, 130]}, { name: '', data: [120, 200, 150, 80, 70, 110, 130]}, { name: '', data: [120, 200, 150, 80, 70, 110, 130]}, { name: '', data: [120, 200, 150, 80, 70, 110, 130]}]
// })
}
});
@ -62,7 +67,7 @@ function initLineChart() {
}
function createSeries () {
return props.chartData.series.map((item) => {
return props.chartData.values?.map((item) => {
return {
name: item.name,
type: 'line',
@ -83,7 +88,7 @@ function createOption (data) {
trigger: 'axis'
},
legend: {
data: data.legend
data: data.names
},
grid: {
left: '3%',
@ -94,7 +99,7 @@ function createOption (data) {
xAxis: {
type: 'category',
boundaryGap: false,
data: data.xAxis
data: data.dates
},
yAxis: {
type: 'value'

@ -2,41 +2,39 @@
* @Author: chris
* @Date: 2025-08-20 17:06:41
* @LastEditors: chris
* @LastEditTime: 2025-08-21 10:47:52
* @LastEditTime: 2025-10-09 10:19:00
-->
<script setup>
const props = defineProps({
params: {
type: Object,
default: () => ({
pestTypes: []
})
},
options: {
type: Array,
default: () => []
}
})
const emits = defineEmits(['update:params'])
const emit = defineEmits(['pestChange'])
const queryParams = ref({...props.params});
const pestTypes = ref([])
watch(() => queryParams.value, (newVal) => {
emits('update:params', newVal)
watch(() => props.options, (newVal) => {
pestTypes.value = newVal.map(item => item.name)
})
function handleChange(value) {
emit('pestChange', value)
}
</script>
<template>
<el-row class="chart-action">
<el-col :span="12" :offset="18">
<el-select v-model="queryParams.pestTypes" placeholder="请选择" style="width: 240px;">
<el-select v-model="pestTypes" @change="handleChange" multiple placeholder="请选择" style="width: 240px;">
<el-option
v-for="item in options"
multiple
:key="item.value"
:label="item.label"
:value="item.value"
:key="item.id"
:label="item.name"
:value="item.name"
/>
</el-select>
</el-col>

@ -2,7 +2,7 @@
* @Author: chris
* @Date: 2025-08-18 09:24:26
* @LastEditors: chris
* @LastEditTime: 2025-09-08 11:35:38
* @LastEditTime: 2025-10-09 10:53:17
-->
<script setup name="PestStatistics">
import { useSettings } from "@/hooks/useSettings";
@ -11,15 +11,75 @@ import SearchForm from './components/SearchForm.vue';
import TrendChart from './components/TrendChart.vue';
import ChartAction from "./components/chartAction.vue";
import { devices } from './mockData'
import { listDevice } from '@/api/device'
import { listDeviceData } from '@/api/deviceData'
const { hasTags } = useSettings()
const { proxy } = getCurrentInstance();
const nowDate = new Date().toLocaleDateString().replace(/\//g, '-')
const nextDate = new Date(+new Date(nowDate) + 24 * 60 * 60 * 1000).toLocaleDateString().replace(/\//g, '-')
const loading = ref(false);
const deviceLoading = ref(false);
const chartAllData = ref({});
const chartData = ref({});
const deviceList = ref(devices);
const tableData = ref([]);
const queryParams = ref({});
const options = ref([])
const checkList = ref([]);
const queryParams = ref({
pageNum: 1,
deviceType: 1,
deviceId: null,
beginTime: `${nowDate} 00:00:00`,
endTime: `${nextDate} 23:59:59`
});
const deviceIds = computed({
get: () => {
return checkList.value || []
},
set: (value) => {
checkList.value = value
queryParams.value.deviceId = value.join(',')
getList()
}
})
function getList () {
getDeviceList();
async function getList () {
loading.value = true;
try {
const res = await listDeviceData(queryParams.value);
// imageList.value = res.rows;
chartAllData.value = chartData.value = createChartData(res.rows);
options.value = createPestOption(chartData.value.names)
} catch (error) {
proxy.$modal.msgError('获取虫情设备实时数据失败: ' + (error.message || '未知错误'))
console.error('Failed to get device live data:', error)
} finally {
loading.value = false;
}
}
async function getDeviceList () {
const params = {
pageNum: 1,
pageSize: 50,
type: 1
}
deviceLoading.value = true;
try {
const res = await listDevice(params);
deviceList.value = res.rows;
deviceIds.value = res.rows.map(item => item.id)
} catch (error) {
proxy.$modal.msgError('获取虫情设备数据失败: ' + (error.message || '未知错误'))
console.error('Failed to get pest list:', error)
} finally {
deviceLoading.value = false;
}
}
function handleSelect(items) {
@ -33,18 +93,72 @@ function exportView() {
function resetQuery() {
console.log('重置查询')
}
function createChartData(data) {
const res = {}
data.forEach(item => {
const date = item.createTime.split(' ')[0]
const name = item.name
if (!res[date]) {
res[date] = {}
res[date][name] = item.value
} else {
res[date][name] = res[date][name] ? (res[date][name] + item.value) : item.value
}
})
let maxItemKeys = []
Object.keys(res).forEach(dateKey => {
const item = res[dateKey]
const pestKeys = Object.keys(item)
pestKeys.length > maxItemKeys.length ? (maxItemKeys = pestKeys) : null
})
const values = []
maxItemKeys.forEach(pestKey => {
values.push({
name: pestKey,
data: Object.keys(res).map(key => res[key][pestKey] || 0)
})
})
return {
names: maxItemKeys,
dates: Object.keys(res),
values
}
}
function createPestOption (names) {
return names.map((name, index) => ({
id: index + 1,
name
}))
}
function handlePestChange(names) {
const data = JSON.parse(JSON.stringify(chartAllData.value))
Object.keys(data).forEach(key => {
const item = data[key]
key =='names' && (data[key] = item.filter(name => names.includes(name)))
key =='values' && (data[key] = item.filter(item => names.includes(item.name)))
})
chartData.value = data
}
</script>
<template>
<div :class="['pest-statistics-page', 'page-height', { 'hasTagsView': hasTags }]">
<el-row :gutter="40" class="h-100%">
<el-col :span="4">
<device-list :devices="deviceList" @select="handleSelect"></device-list>
<device-list v-model="deviceIds" :devices="deviceList" @select="handleSelect"></device-list>
</el-col>
<el-col :span="20">
<search-form v-model:params="queryParams" @reset="resetQuery" @query="getList" @export="exportView" />
<chart-action v-model:params="queryParams" :options="options" class="action" />
<trend-chart :tableData="tableData" class="chart"/>
<chart-action :options="options" @pestChange="handlePestChange" class="action" />
<trend-chart :chartData="chartData" class="chart"/>
</el-col>
</el-row>
</div>

@ -2,11 +2,19 @@
* @Author: chris
* @Date: 2025-09-09 15:30:24
* @LastEditors: chris
* @LastEditTime: 2025-09-12 17:35:34
* @LastEditTime: 2025-09-25 09:18:24
-->
<script setup>
import soilImg from '@/assets/images/devices/soil.png'
const props = defineProps({
items: {
type: Array,
default: () => []
}
})
const testDataList = [
{
id: 1,
@ -91,7 +99,7 @@ function handleSelect(item) {
<!-- 数据卡片区域 -->
<div class="data-cards">
<div
v-for="item in testDataList"
v-for="item in items"
:key="item.id"
class="data-card"
:style="{ '--card-color': item.color }"

@ -2,11 +2,12 @@
* @Author: chris
* @Date: 2025-09-12 17:20:00
* @LastEditors: chris
* @LastEditTime: 2025-09-12 14:29:24
* @LastEditTime: 2025-09-23 17:39:37
* @Description: 土壤数据曲线图组件支持7天/30/90天数据切换
-->
<script setup>
import * as echarts from 'echarts'
import { soilTypes, soilDict } from '../config'
// props
const props = defineProps({
@ -16,8 +17,7 @@ const props = defineProps({
required: true,
validator: (value) => {
//
const validTypes = ['temperature', 'humidity', 'ph', 'ec', 'nitrogen', 'phosphorus', 'potassium']
const validTypes = soilTypes
return validTypes.includes(value)
}
},
@ -61,210 +61,212 @@ const chartInstance = ref(null)
//
const chartContainer = ref(null)
let soilTypeConfig = null;
//
const soilTypeConfig = {
temperature: {
name: '土壤温度',
unit: '℃',
color: '#FF6B6B',
yAxis: {
name: '土壤温度 (℃)',
min: function(value) { return value.min - 2; },
max: function(value) { return value.max + 2; },
show: true,
axisLabel: {
show: true,
formatter: '{value}',
color: '#333',
fontSize: 12
},
axisTick: {
show: true
},
axisLine: {
show: true
},
splitLine: {
show: true,
lineStyle: {
type: 'dashed'
}
}
}
},
humidity: {
name: '土壤湿度',
unit: '%',
color: '#4ECDC4',
yAxis: {
name: '土壤湿度 (%)',
min: 0,
max: 100,
show: true,
axisLabel: {
show: true,
formatter: '{value}'
},
axisTick: {
show: true
},
axisLine: {
show: true
},
splitLine: {
show: true,
lineStyle: {
type: 'dashed'
}
}
}
},
ph: {
name: 'pH值',
unit: '',
color: '#FFD166',
yAxis: {
name: 'pH值',
min: 4,
max: 9,
show: true,
axisLabel: {
show: true,
formatter: '{value}',
color: '#333',
fontSize: 12
},
axisTick: {
show: true
},
axisLine: {
show: true
},
splitLine: {
show: true,
lineStyle: {
type: 'dashed'
}
}
}
},
ec: {
name: '电导率(EC)',
unit: 'mS/cm',
color: '#6A0572',
yAxis: {
name: '电导率 (mS/cm)',
min: 0,
max: 5,
show: true,
axisLabel: {
show: true,
formatter: '{value}',
color: '#333',
fontSize: 12
},
axisTick: {
show: true
},
axisLine: {
show: true
},
splitLine: {
show: true,
lineStyle: {
type: 'dashed'
}
}
}
},
nitrogen: {
name: '氮含量',
unit: 'mg/kg',
color: '#1A535C',
yAxis: {
name: '氮含量 (mg/kg)',
min: 0,
max: 500,
show: true,
axisLabel: {
show: true,
formatter: '{value}',
color: '#333',
fontSize: 12
},
axisTick: {
show: true
},
axisLine: {
show: true
},
splitLine: {
show: true,
lineStyle: {
type: 'dashed'
}
}
}
},
phosphorus: {
name: '磷含量',
unit: 'mg/kg',
color: '#77DD77',
yAxis: {
name: '磷含量 (mg/kg)',
min: 0,
max: 200,
show: true,
axisLabel: {
show: true,
formatter: '{value}',
color: '#333',
fontSize: 12
},
axisTick: {
show: true
},
axisLine: {
show: true
},
splitLine: {
show: true,
lineStyle: {
type: 'dashed'
}
}
}
},
potassium: {
name: '钾含量',
unit: 'mg/kg',
color: '#845EC2',
yAxis: {
name: '钾含量 (mg/kg)',
min: 0,
max: 400,
show: true,
axisLabel: {
show: true,
formatter: '{value}',
color: '#333',
fontSize: 12
},
axisTick: {
show: true
},
axisLine: {
show: true
},
splitLine: {
show: true,
lineStyle: {
type: 'dashed'
}
}
}
}
}
// const soilTypeConfig = {
// temperature: {
// name: '',
// unit: '',
// color: '#FF6B6B',
// yAxis: {
// name: ' ()',
// min: function(value) { return value.min - 2; },
// max: function(value) { return value.max + 2; },
// show: true,
// axisLabel: {
// show: true,
// formatter: '{value}',
// color: '#333',
// fontSize: 12
// },
// axisTick: {
// show: true
// },
// axisLine: {
// show: true
// },
// splitLine: {
// show: true,
// lineStyle: {
// type: 'dashed'
// }
// }
// }
// },
// humidity: {
// name: '湿',
// unit: '%',
// color: '#4ECDC4',
// yAxis: {
// name: '湿 (%)',
// min: 0,
// max: 100,
// show: true,
// axisLabel: {
// show: true,
// formatter: '{value}'
// },
// axisTick: {
// show: true
// },
// axisLine: {
// show: true
// },
// splitLine: {
// show: true,
// lineStyle: {
// type: 'dashed'
// }
// }
// }
// },
// ph: {
// name: 'pH',
// unit: '',
// color: '#FFD166',
// yAxis: {
// name: 'pH',
// min: 4,
// max: 9,
// show: true,
// axisLabel: {
// show: true,
// formatter: '{value}',
// color: '#333',
// fontSize: 12
// },
// axisTick: {
// show: true
// },
// axisLine: {
// show: true
// },
// splitLine: {
// show: true,
// lineStyle: {
// type: 'dashed'
// }
// }
// }
// },
// ec: {
// name: '(EC)',
// unit: 'mS/cm',
// color: '#6A0572',
// yAxis: {
// name: ' (mS/cm)',
// min: 0,
// max: 5,
// show: true,
// axisLabel: {
// show: true,
// formatter: '{value}',
// color: '#333',
// fontSize: 12
// },
// axisTick: {
// show: true
// },
// axisLine: {
// show: true
// },
// splitLine: {
// show: true,
// lineStyle: {
// type: 'dashed'
// }
// }
// }
// },
// nitrogen: {
// name: '',
// unit: 'mg/kg',
// color: '#1A535C',
// yAxis: {
// name: ' (mg/kg)',
// min: 0,
// max: 500,
// show: true,
// axisLabel: {
// show: true,
// formatter: '{value}',
// color: '#333',
// fontSize: 12
// },
// axisTick: {
// show: true
// },
// axisLine: {
// show: true
// },
// splitLine: {
// show: true,
// lineStyle: {
// type: 'dashed'
// }
// }
// }
// },
// phosphorus: {
// name: '',
// unit: 'mg/kg',
// color: '#77DD77',
// yAxis: {
// name: ' (mg/kg)',
// min: 0,
// max: 200,
// show: true,
// axisLabel: {
// show: true,
// formatter: '{value}',
// color: '#333',
// fontSize: 12
// },
// axisTick: {
// show: true
// },
// axisLine: {
// show: true
// },
// splitLine: {
// show: true,
// lineStyle: {
// type: 'dashed'
// }
// }
// }
// },
// potassium: {
// name: '',
// unit: 'mg/kg',
// color: '#845EC2',
// yAxis: {
// name: ' (mg/kg)',
// min: 0,
// max: 400,
// show: true,
// axisLabel: {
// show: true,
// formatter: '{value}',
// color: '#333',
// fontSize: 12
// },
// axisTick: {
// show: true
// },
// axisLine: {
// show: true
// },
// splitLine: {
// show: true,
// lineStyle: {
// type: 'dashed'
// }
// }
// }
// }
// }
//
const initChart = () => {
@ -294,7 +296,7 @@ const updateChart = () => {
const option = {
title: {
text: `${config.name}趋势图 (近${props.selectedTimeRange})`,
text: `${config.name}趋势图 (近24小时)`,
left: 'center',
textStyle: {
fontSize: 16,
@ -421,6 +423,7 @@ watch(
//
onMounted(() => {
soilTypeConfig = createSoilTypeConfig()
initChart()
})
@ -436,17 +439,56 @@ onUnmounted(() => {
defineExpose({
refreshChart: updateChart
})
function createSoilTypeConfig () {
const soilTypeConfig = {}
soilTypes.forEach(type => {
const typeItem = soilDict[type]
soilTypeConfig[type] = {
name: typeItem.name,
unit: typeItem.unit,
color: typeItem.color,
icon: typeItem.icon,
yAxis: {
name: `${typeItem.name} (${typeItem.unit})`,
min: function(value) { return value.min; },
max: function(value) { return value.max + 2; },
show: true,
axisLabel: {
show: true,
formatter: '{value}',
color: '#333',
fontSize: 12
},
axisTick: {
show: true
},
axisLine: {
show: true
},
splitLine: {
show: true,
lineStyle: {
type: 'dashed'
}
}
}
}
})
return soilTypeConfig
}
</script>
<template>
<div class="soil-chart-container">
<!-- 时间选择器 -->
<div class="chart-controls">
<el-radio-group v-model="selectedDays" class="time-range-selector">
<!-- <el-radio-group v-model="selectedDays" class="time-range-selector">
<el-radio-button :value="7" :disabled="loading">近7天</el-radio-button>
<el-radio-button :value="30" :disabled="loading">近30天</el-radio-button>
<el-radio-button :value="90" :disabled="loading">近90天</el-radio-button>
</el-radio-group>
</el-radio-group> -->
<!-- 加载状态指示器 -->
<div v-if="loading" class="loading-indicator">

@ -0,0 +1,59 @@
/*
* @Author: chris
* @Date: 2025-09-23 09:49:02
* @LastEditors: chris
* @LastEditTime: 2025-09-23 11:57:53
*/
export const soilTypes = [
"SoilTemperature",
"SoilHumidity",
"EC",
"PH",
"Nitrogen",
"Phosphorus",
"Potassium",
];
export const soilDict = {
SoilTemperature: {
name: "土壤温度",
unit: "°C",
color: "#FF6B6B",
icon: "temperature2",
},
SoilHumidity: {
name: "土壤湿度",
unit: "%",
color: "#4ECDC4",
icon: "humidity",
},
EC: {
name: "EC",
unit: "μS/cm",
color: "#6A0572",
icon: "ec",
},
PH: {
name: "PH",
unit: "",
color: "#FFD166",
icon: "ph-color",
},
Nitrogen: {
name: "氮",
unit: "mg/kg",
color: "#1A535C",
icon: "nitrogen",
},
Phosphorus: {
name: "磷",
unit: "mg/kg",
color: "#77DD77",
icon: "phosphorus",
},
Potassium: {
name: "钾",
unit: "mg/kg",
color: "#845EC2",
icon: "potassium",
},
};

@ -2,46 +2,111 @@
* @Author: chris
* @Date: 2025-09-05 09:29:59
* @LastEditors: chris
* @LastEditTime: 2025-09-12 14:20:17
* @LastEditTime: 2025-09-25 09:35:34
-->
<script setup>
import LiveData from './components/LiveData.vue'
import SoilChart from './components/SoilChart.vue';
import { useSettings } from '@/hooks/useSettings';
import { getFormattedSoilData } from './mock';
import { getDeviceDataAnalysis } from '@/api/deviceData';
import { soilTypes, soilDict } from './config';
const { hasTags } = useSettings()
const { proxy } = getCurrentInstance()
const liveData = ref({})
const liveData = ref([])
const chartData = ref({ dates: [], values: []})
const soilType = ref('temperature')
const soilType = ref('SoilTemperature')
const selectedTimeRange = ref(7)
const chartLoading = ref(false)
const analysisData = ref(null)
getChartData()
// getChartData()
function getChartData() {
// function getChartData() {
// chartLoading.value = true
// setTimeout(() => {
// console.log(getFormattedSoilData(soilType.value, selectedTimeRange.value))
// chartData.value = getFormattedSoilData(soilType.value, selectedTimeRange.value)
// chartLoading.value = false
// }, 300)
// return chartData.value
// }
getAnalysisData()
async function getAnalysisData () {
chartLoading.value = true
setTimeout(() => {
console.log(getFormattedSoilData(soilType.value, selectedTimeRange.value))
chartData.value = getFormattedSoilData(soilType.value, selectedTimeRange.value)
try {
const res = await getDeviceDataAnalysis({ type: 2 })
chartLoading.value = false
analysisData.value = formatData(res.rows)
liveData.value = createLiveData(analysisData.value)
chartData.value = createChartData(analysisData.value, soilType.value)
} catch (error) {
proxy.$modal.msgError('获取分析数据失败: ' + (error.message || '未知错误'))
console.error('Failed to get analysis data:', error)
} finally {
chartLoading.value = false
}, 300)
return chartData.value
}
}
function handleSelectionChange(selection) {
console.log(selection)
}
function handleSelectSoilItem(item) {
soilType.value = item.type
getChartData()
chartData.value = createChartData(analysisData.value, soilType.value)
}
function handleTimeRangeChange(range) {
selectedTimeRange.value = range
getChartData()
}
function formatData(data) {
const obj = {};
const res = {};
// data.sort((a,b) => soilTypes.indexOf(a.identifier) - soilTypes.indexOf(b.identifier))
data.forEach(item => {
const identifier = item.identifier;
if (!identifier || identifier === 'null') return false;
obj[identifier] ? obj[identifier].push(item) : (obj[identifier] = [item])
})
soilTypes.forEach(type => {
res[type] = obj[type] || []
})
return res;
}
function createLiveData (data) {
const liveData = []
Object.keys(data).forEach((type, index) => {
const dictItem = soilDict[type]
liveData.push({
id: index + 1,
title: dictItem.name,
type,
value: data[type][0]?.value || '--',
icon: dictItem.icon,
color: dictItem.color,
unit: dictItem.unit,
})
})
return liveData;
}
function createChartData(data, type) {
const dates = []
const values = []
const typeList = data[type] || []
typeList.forEach(item => {
dates.push(item.createTime)
values.push(item.value)
})
return { dates, values }
}
</script>
@ -49,7 +114,7 @@ function handleTimeRangeChange(range) {
<div :class="['soil-monitor-page', 'page-height', { 'hasTagsView': hasTags }]">
<el-row :gutter="20">
<el-col class="real-time-data" :span="6">
<live-data :data="liveData" @select="handleSelectSoilItem"/>
<live-data :items="liveData" @select="handleSelectSoilItem"/>
</el-col>
<el-col class="history-data" :span="18">
<soil-chart :soil-type="soilType" :chart-data="chartData" :selected-time-range="selectedTimeRange" :loading="chartLoading" @time-range-change="handleTimeRangeChange" />

@ -1,17 +1,17 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
<el-form-item label="部门名称" prop="deptName">
<el-form-item label="果园名称" prop="deptName">
<el-input
v-model="queryParams.deptName"
placeholder="请输入部门名称"
placeholder="请输入果园名称"
clearable
style="width: 200px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="部门状态" clearable style="width: 200px">
<el-select v-model="queryParams.status" placeholder="果园状态" clearable style="width: 200px">
<el-option
v-for="dict in sys_normal_disable"
:key="dict.value"
@ -55,7 +55,7 @@
:default-expand-all="isExpandAll"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
>
<el-table-column prop="deptName" label="部门名称" width="260"></el-table-column>
<el-table-column prop="deptName" label="果园名称" width="260"></el-table-column>
<el-table-column prop="orderNum" label="排序" width="200"></el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="scope">
@ -78,7 +78,7 @@
<!-- 添加或修改部门对话框 -->
<el-dialog :title="title" v-model="open" width="600px" append-to-body>
<el-form ref="deptRef" :model="form" :rules="rules" label-width="80px">
<el-form ref="deptRef" :model="form" :rules="rules" label-width="100px">
<el-row>
<el-col :span="24" v-if="form.parentId !== 0">
<el-form-item label="上级部门" prop="parentId">
@ -93,8 +93,8 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="部门名称" prop="deptName">
<el-input v-model="form.deptName" placeholder="请输入部门名称" />
<el-form-item label="果园名称" prop="deptName">
<el-input v-model="form.deptName" placeholder="请输入果园名称" />
</el-form-item>
</el-col>
<el-col :span="12">
@ -118,7 +118,27 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="部门状态">
<el-form-item label="地形" prop="terrain">
<el-input v-model="form.terrain" placeholder="请输入地形" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="品种" prop="variety">
<el-input v-model="form.variety" placeholder="请输入品种" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="种植面积" prop="plantingArea">
<el-input v-model="form.plantingArea" placeholder="请输入种植面积(亩)" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="种植数量" prop="plantingNum">
<el-input v-model="form.plantingNum" placeholder="请输入种植数量" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="果园状态">
<el-radio-group v-model="form.status">
<el-radio
v-for="dict in sys_normal_disable"
@ -163,10 +183,14 @@ const data = reactive({
},
rules: {
parentId: [{ required: true, message: "上级部门不能为空", trigger: "blur" }],
deptName: [{ required: true, message: "部门名称不能为空", trigger: "blur" }],
deptName: [{ required: true, message: "果园名称不能为空", trigger: "blur" }],
orderNum: [{ required: true, message: "显示排序不能为空", trigger: "blur" }],
email: [{ type: "email", message: "请输入正确的邮箱地址", trigger: ["blur", "change"] }],
phone: [{ pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "请输入正确的手机号码", trigger: "blur" }]
phone: [{ pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "请输入正确的手机号码", trigger: "blur" }],
plantingArea: [{ required: true, message: "种植面积不能为空", trigger: "blur" }],
plantingNum: [{ required: true, message: "种植数量不能为空", trigger: "blur" }],
terrain: [{ required: true, message: "地形不能为空", trigger: "blur" }],
variety: [{ required: true, message: "品种不能为空", trigger: "blur" }],
},
});
@ -197,6 +221,10 @@ function reset() {
leader: undefined,
phone: undefined,
email: undefined,
plantingArea: undefined,
plantingNum: undefined,
terrain: undefined,
variety: undefined,
status: "0"
};
proxy.resetForm("deptRef");

@ -0,0 +1,24 @@
<!--
* @Author: chris
* @Date: 2025-10-10 12:03:45
* @LastEditors: chris
* @LastEditTime: 2025-10-10 14:36:11
-->
<script setup>
import { useSettings } from '@/hooks/useSettings'
const { hasTags } = useSettings()
</script>
<template>
<div :class="['uav-page', 'page-height', { 'hasTagsView': hasTags }]">
<iframe src="https://fh.dji.com/organization/6950d95b-1d32-44ef-9b69-816ad1ecdcf6/project/c50567c4-5582-47b3-b6c3-217f25f14281#/wayline" class="uav-iframe"></iframe>
</div>
</template>
<style lang="scss" scoped>
.uav-iframe {
height: 100%;
width: 100%;
}
</style>

@ -2,65 +2,56 @@
* @Author: chris
* @Date: 2025-09-09 15:30:24
* @LastEditors: chris
* @LastEditTime: 2025-09-22 17:25:39
* @LastEditTime: 2025-10-27 14:56:51
-->
<script setup>
import VideoJs from 'video.js'
import weatherImg from '@/assets/images/devices/weather.png'
import videoSrc from '@/assets/video/test.mp4'
const props = defineProps({
items: {
type: Array,
default: () => [],
}
})
const testDataList = [
{
id: 1,
title: '温度',
type: 'Temperature',
value: '25',
icon: 'temperature2',
color: '#FF6B6B',
unit: '°C',
},
{
id: 2,
title: '光照',
type: 'Illuminance',
value: '10000',
icon: 'light',
color: '#FFD166',
unit: 'lux',
},
{
id: 3,
title: '降雨量',
type: 'DailyRainfall',
value: '10',
icon: 'rain',
color: '#6A0572',
unit: 'mm',
},
{
id: 4,
title: '风向',
type: 'WindDirection',
value: '东南风',
icon: 'wind',
color: '#1A535C',
unit: '°',
},
{
id: 5,
title: '风速',
type: 'WindSpeed',
value: '2',
icon: 'windPower',
color: '#77DD77',
unit: 'm/s',
},
]
const emits = defineEmits([ 'select' ])
const playerRef = ref(null)
let player = null
// const weatherItems = createItemObj()
onMounted(() => {
initVideo()
})
onUnmounted(() => {
if (!player) return
player.dispose()
})
async function initVideo () {
player = await VideoJs('player', {
autoplay: true,
preload: 'auto',
fluid: false,
controls: true,
})
player.src({
src: videoSrc,
type: 'video/mp4',
})
}
function handleSelect(item) {
console.log('Selected item:', item);
emits('select', item)
}
</script>
<template>
@ -70,10 +61,15 @@ function handleSelect(item) {
<h2 class="dashboard-title">气象监测数据</h2>
</div>
<div class="video-box position-relative">
<video ref="playerRef" id="player" class="video-player video-js">
</video>
</div>
<!-- 数据卡片区域 -->
<div class="data-cards">
<div
v-for="item in testDataList"
v-for="item in items"
:key="item.id"
class="data-card"
:style="{ '--card-color': item.color }"
@ -129,7 +125,7 @@ $transition-base: all 0.3s ease;
//
.weather-photo {
@apply w-full h-600px rounded-8px overflow-hidden;
@apply w-full h-330px rounded-8px overflow-hidden;
box-shadow: $shadow-normal;
transition: $transition-base;
}
@ -153,7 +149,7 @@ $transition-base: all 0.3s ease;
// -
.data-card {
@apply rounded-8px p-16px flex items-center relative border-[1px] border-style-solid border-[transparent] min-h-[100px] cursor-pointer;
@apply rounded-8px p-16px flex items-center relative border-[1px] border-style-solid border-[transparent] min-h-[90px] cursor-pointer;
background: $bg-secondary;
transition: $transition-base;
@ -204,4 +200,12 @@ $transition-base: all 0.3s ease;
@apply text-14px font-400 whitespace-nowrap;
color: $text-secondary;
}
.video-box {
@apply w-full h-300px overflow-hidden mb-15px rounded-8px;
}
.video-player {
@apply max-h-[100%] max-w-[100%];
}
</style>

@ -10,25 +10,30 @@ export const weatherDict = {
name: "温度",
unit: "°C",
color: "#FF6B6B",
icon: "temperature2",
},
Illuminance: {
name: "光照",
unit: "lux",
color: "#FFD166",
icon: "light",
},
WindSpeed: {
name: "风速",
unit: "km/h",
color: "#009688",
icon: "windPower",
},
WindDirection: {
name: "风向",
unit: "°",
color: "#845EC2",
icon: "wind",
},
DailyRainfall: {
name: "降雨量",
unit: "mm",
color: "#6A0572",
icon: "rain",
},
};

@ -2,7 +2,7 @@
* @Author: chris
* @Date: 2025-09-05 09:29:59
* @LastEditors: chris
* @LastEditTime: 2025-09-22 17:23:27
* @LastEditTime: 2025-09-25 09:35:16
-->
<script setup>
import LiveData from './components/LiveData.vue'
@ -11,15 +11,15 @@ import { useSettings } from '@/hooks/useSettings';
// import { getFormattedWeatherData } from './mock';
import { getDeviceDataAnalysis } from '@/api/deviceData';
import { formatDate } from '@/utils';
import { weatherTypes } from './config';
import { weatherTypes, weatherDict } from './config';
const { hasTags } = useSettings()
const { proxy } = getCurrentInstance()
const liveData = ref({})
const liveData = ref([])
const chartData = ref({ dates: [], values: []})
const weatherType = ref('Temperature')
const weatherData = ref({})
const analysisData = ref({})
const selectedTimeRange = ref(7)
const chartLoading = ref(false)
const loading = ref(false)
@ -33,8 +33,9 @@ async function getDeviceAnalysisData() {
const endTime = formatDate(new Date().getTime())
try {
const res = await getDeviceDataAnalysis({ type: 3 })
weatherData.value = formatData(res.rows)
chartData.value =createChartData(weatherData.value, weatherType.value)
analysisData.value = formatData(res.rows)
liveData.value = createLiveData(analysisData.value)
chartData.value =createChartData(analysisData.value, weatherType.value)
} catch (error) {
proxy.$modal.msgError('获取气象分析数据失败: ' + (error.message || '未知错误'))
console.error('Failed to get device live data:', error)
@ -60,57 +61,63 @@ function handleSelectionChange(selection) {
function handleSelectWeatherItem(item) {
weatherType.value = item.type
chartData.value = createChartData(weatherData.value, weatherType.value)
chartData.value = createChartData(analysisData.value, weatherType.value)
}
function handleTimeRangeChange(range) {
selectedTimeRange.value = range
}
function createChartData(data, type) {
const dateList = getHourList()
return {
dates: dateList,
values: data[type] || []
function createLiveData (data) {
const liveData = []
weatherTypes.forEach((type, index) => {
const dictItem = weatherDict[type]
liveData.push({
id: index + 1,
title: dictItem.name,
type,
value: data[type][0]?.value || '--',
icon: dictItem.icon,
color: dictItem.color,
unit: dictItem.unit,
})
})
return liveData;
}
function createChartData(data, type) {
const dates = []
const values = []
const typeList = data[type] || []
typeList.forEach(item => {
dates.push(item.createTime)
values.push(item.value)
})
return { dates, values }
}
function formatData(data) {
const result = {};
data.sort((a,b) => weatherTypes.indexOf(a.identifier) - weatherTypes.indexOf(b.identifier))
const obj = {};
const res = {};
// data.sort((a,b) => soilTypes.indexOf(a.identifier) - soilTypes.indexOf(b.identifier))
data.forEach(item => {
const identifier = item.identifier;
if (!identifier || identifier === 'null') return false;
result[identifier] ? result[identifier].push(item.value) : (result[identifier] = [item.value])
obj[identifier] ? obj[identifier].push(item) : (obj[identifier] = [item])
})
console.log(result)
// TODO: ,
delete result.Humidity
delete result.WindPower
delete result.DailyRainfall
return result
}
function getHourList () {
const hours = [];
const now = new Date();
for (let i = 23; i >= 0; i--) {
const hourDate = new Date(now);
hourDate.setHours(hourDate.getHours() - i);
const hourStr = hourDate.getHours().toString().padStart(2, '0');
hours.push(`${hourStr}:00`);
weatherTypes.forEach(type => {
res[type] = obj[type] || []
})
return res;
}
return hours;
}
</script>
<template>
<div :class="['weather-monitor-page', 'page-height', { 'hasTagsView': hasTags }]">
<el-row :gutter="20">
<el-col class="real-time-data" :span="6">
<live-data :data="liveData" @select="handleSelectWeatherItem"/>
<live-data :items="liveData" @select="handleSelectWeatherItem"/>
</el-col>
<el-col class="history-data" :span="18">
<weather-chart :weather-type="weatherType" :chart-data="chartData" :selected-time-range="selectedTimeRange" :loading="chartLoading" @time-range-change="handleTimeRangeChange" />

@ -2,7 +2,7 @@
* @Author: chris
* @Date: 2025-01-13 09:33:28
* @LastEditors: chris
* @LastEditTime: 2025-09-16 10:07:43
* @LastEditTime: 2025-10-27 10:24:31
*/
import { defineConfig, loadEnv } from "vite";
import path from "path";
@ -63,7 +63,7 @@ export default defineConfig(({ mode, command }) => {
preprocessorOptions: {
scss: {
silenceDeprecations: ["legacy-js-api"],
additionalData: `@import "@/assets/styles/variables.scss";`,
additionalData: `@import "@/assets/styles/variables_custom.scss";`,
},
},
},

Loading…
Cancel
Save