新增功能

master
chris 1 week ago
parent c7a70e04ac
commit 01da3ace7d

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.1 KiB

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1757643604157" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4711" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M226.27123582 48.32594789v336.1867332L115.80296699 48.32594789H10.24337477v491.05202298h83.77013812V195.39035959l111.85099465 343.98761128h104.1653437V48.32594789h-83.75861542zM429.87761009 48.32594789v187.93548186h106.82709089V48.32594789h91.31751648v491.05202298h-91.31751648V321.34515739h-106.82709089v218.03281348h-91.31751647V48.32594789h91.31751647zM792.00197481 341.18727266a24.19770152 24.19770152 0 0 1-25.00429158-14.24207577 108.68224801 108.68224801 0 0 1-5.16217631-42.19618238h-54.77898718a103.70443511 103.70443511 0 0 0 20.22236486 68.39883632c16.24702817 17.12275451 38.54348172 25.89154064 61.15104857 24.03638351a115.69958144 115.69958144 0 0 0 47.669472-8.18112765 72.3050367 72.3050367 0 0 0 28.16151549-21.4207272 73.81451237 73.81451237 0 0 0 13.91943972-29.72860474 146.33848066 146.33848066 0 0 0 3.52595081-31.69898901 91.84756137 91.84756137 0 0 0-11.08485184-48.17647146 52.35921702 52.35921702 0 0 0-39.68423052-24.12856524 49.85878787 49.85878787 0 0 0 31.30721669-21.89315854 80.49768709 80.49768709 0 0 0 11.522715-43.01429515c1.70536181-22.29645356-6.29140242-44.15504392-21.40920447-59.2613233A86.73147589 86.73147589 0 0 0 792.77399673 70.680015a80.86641396 80.86641396 0 0 0-61.53129818 22.29645355 95.25828502 95.25828502 0 0 0-20.97134132 67.52310999h52.33617158a187.53218682 187.53218682 0 0 1 0-19.7153654 48.47606207 48.47606207 0 0 1 4.41319986-15.49805169 24.19770152 24.19770152 0 0 1 8.7572634-10.3704435 27.51624344 27.51624344 0 0 1 14.66841622-3.45681451 24.10551981 24.10551981 0 0 1 20.59109174 9.06837671 53.77651096 53.77651096 0 0 1 6.80992456 30.92696708 94.64758111 94.64758111 0 0 1 0 18.06761715 53.7304201 53.7304201 0 0 1-5.6115622 15.06018853 30.9961034 30.9961034 0 0 1-9.8864895 10.70460225 27.19360743 27.19360743 0 0 1-15.42891538 4.34406355H768.14995474V240.06392569h19.08161605a37.09161964 37.09161964 0 0 1 19.08161607 4.22883642 30.07428618 30.07428618 0 0 1 11.03876097 11.7416466 48.59128921 48.59128921 0 0 1 5.20826718 16.29311902 128.70872669 128.70872669 0 0 1 0 19.77297897 156.70892419 156.70892419 0 0 1 0 18.89725261 45.37645173 45.37645173 0 0 1-4.34406354 15.92439216 26.13351765 26.13351765 0 0 1-9.4371036 10.37044351 30.92696709 30.92696709 0 0 1-16.77707306 3.89467768zM906.01923987 221.45474095h111.90860821V170.69718132h-111.97774451v50.75755963h0.0691363zM846.37766697 533.46681807v-38.35911828h-77.84746264v327.68296955h79.5067336V605.26485531a45.79126948 45.79126948 0 0 1 7.15560602-37.95582324 38.55500443 38.55500443 0 0 1 32.49405634-16.24702817 29.95905904 29.95905904 0 0 1 25.34997302 9.82887589 59.68766377 59.68766377 0 0 1 7.93915065 35.72041656v226.12175941h79.34541559V579.88031415a104.8567066 104.8567066 0 0 0-19.83059255-71.44083309A73.49187635 73.49187635 0 0 0 923.36092597 486.65002697a89.53149563 89.53149563 0 0 0-44.93858856 11.21160173 83.07877524 83.07877524 0 0 0-32.11380674 35.72041653z" fill="#0EAD5A" p-id="4712"></path><path d="M57.2330066 753.00910721h563.28792337c25.94915421 0 46.92049553-24.55490569 46.92049554-54.79050989s-21.09809118-54.7328963-46.92049554-54.7328963h-563.23030981c-25.94915421 0-46.9781091 24.5664284-46.97810913 54.8020326s21.02895491 54.79050988 46.97810913 54.79050987zM263.10935572 972.05591961h-101.20400595c-27.90801577 0-50.57319621-24.55490569-50.5731962-54.79050989s22.66518041-54.79050988 50.5731962-54.79050989H263.10935572c27.89649305 0 50.50405991 24.55490569 50.50405989 54.79050989s-22.722794 54.79050988-50.50405989 54.79050989zM869.67659672 972.05591961h-404.44729695c-27.89649305 0-50.57319621-24.55490569-50.57319619-54.79050989s22.67670314-54.79050988 50.57319619-54.79050989h404.44729695c27.89649305 0 50.50405991 24.55490569 50.50405989 54.79050989s-22.66518041 54.79050988-50.50405989 54.79050989z" fill="#1F2836" p-id="4713"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1757643574356" class="icon" viewBox="0 0 1045 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3899" xmlns:xlink="http://www.w3.org/1999/xlink" width="130.625" height="128"><path d="M106.68219072 595.97468432h115.27433939V436.93172199h56.05946311c101.12587783 0 189.37563699-50.25757583 189.375637-164.25957231 0-118.50609201-86.97741506-155.88755035-192.60738961-155.88755036H106.68219072v479.24097915z m115.27433939-249.88830266v-138.50733617h47.02582322c55.39784518 0 86.31579713 16.76949039 86.31579594 65.0676577 0 47.02582322-27.05002671 73.43967848-83.08404333 73.43967847h-50.25757583zM548.89796695 595.97468432h115.2743394v-197.08603981h157.82151358v197.11148629h114.00199529V116.75915283h-114.00199529v182.30140562H664.17230635V116.75915283h-115.2743394v479.24097797z" fill="#000000" p-id="3900"></path><path d="M363.56838414 276.81998974c2.39200576-90.05648762-88.3006533-74.05040345-144.05475393-74.05040345v150.31468494c54.86346083 0 141.81542941 7.58316806 144.02930624-76.26428149z" fill="#5BB0FF" p-id="3901"></path><path d="M1045.34103256 698.22022382H5.20005626v-39.49354811h1040.1409763v39.49354811z" fill="#000000" p-id="3902"></path><path d="M253.91780311 698.27111796l-239.20061556 237.44478125-18.57621766-18.67800475 239.25150972-237.41933476 18.5253235 18.67800473zM346.08638166 698.27111796l-239.20061674 237.44478125-18.57621765-18.67800475 239.22606322-237.41933476 18.57621766 18.67800473zM438.25495905 698.27111796l-239.20061558 237.44478125-18.57621882-18.67800475 239.22606322-237.41933476 18.57621764 18.67800473zM530.42353641 698.27111796l-239.22606204 237.44478125-18.55077118-18.67800475 239.22606205-237.41933476 18.55077117 18.67800473zM622.59211498 698.27111796l-239.22606324 237.44478125-18.57621765-18.67800475 239.25150971-237.41933476 18.55077118 18.67800473zM714.76069234 698.27111796l-239.22606324 237.44478125-18.57621764-18.67800475 239.2515097-237.41933476 18.55077118 18.67800473zM806.9292697 698.27111796l-239.2515097 237.44478125-18.55077118-18.67800475 239.25150971-237.41933476 18.52532469 18.67800473zM899.09784827 698.27111796l-239.25150972 237.44478125-18.55077117-18.67800475 239.25150972-237.41933476 18.55077117 18.67800473zM991.24097915 698.27111796l-239.20061674 237.44478125-18.57621766-18.67800475 239.22606322-237.41933476 18.57621767 18.67800473z" fill="#000000" p-id="3903"></path><path d="M5.20005626 815.37763228l121.2543556-118.88779511h37.78860793l-159.04296353 155.8621027v-36.97430759zM926.65681284 907.24084717l118.68421972-117.89536702v37.55958607l-80.51390865 80.33578095h-38.17031107zM5.09826916 725.11757046l31.40144217-28.62773329h39.77346413L5.09826916 760.89587457v-35.77830411zM833.80116869 907.24084717l211.53986387-209.1733046v37.40690484l-173.75125592 171.76639976h-37.78860795z" fill="#000000" p-id="3904"></path></svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.6 KiB

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1757643585830" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4228" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M1015.19356904 540.64734097A469.68150734 469.68150734 0 0 0 213.39496573 208.02703687a469.61860658 469.61860658 0 0 0-137.81601542 332.17999675A469.68150734 469.68150734 0 0 0 545.26045764 1009.88854096 469.68150734 469.68150734 0 0 0 1015.19356904 540.64734097zM391.65630115 358.23454106h62.71226186V539.38932165L632.75570096 358.23454106h79.12941355l-168.32298254 164.17152001 193.92367595 219.02116026h-85.04210547l-151.46552446-177.38072172-46.60961498 45.28869491v132.09202681h-62.71226186V358.23454106z" fill="#999999" p-id="4229"></path><path d="M197.54392192 380.87888851a188.13678656 188.13678656 0 0 0 188.82869796-187.82228173V192.17599306A188.70289544 188.70289544 0 0 0 64.06807427 58.76304619 188.63999467 188.63999467 0 0 0 197.54392192 380.87888851z" fill="#18B356" p-id="4230"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@ -109,6 +109,16 @@ export const constantRoutes = [
component: () => import("@/views/weather/history"), component: () => import("@/views/weather/history"),
name: "WeatherHistory", name: "WeatherHistory",
}, },
{
path: "/soil/monitor",
component: () => import("@/views/soil/monitor"),
name: "SoilMonitor",
},
{
path: "/soil/history",
component: () => import("@/views/soil/history"),
name: "SoilHistory",
},
// { // {
// path: "/orchard-screen", // path: "/orchard-screen",
// component: () => import("@/views/orchardScreen"), // component: () => import("@/views/orchardScreen"),

@ -0,0 +1,42 @@
<!--
* @Author: chris
* @Date: 2025-08-08 16:17:54
* @LastEditors: chris
* @LastEditTime: 2025-09-09 16:50:46
-->
<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="name" v-if="columns[0].visible"/>
<el-table-column label="数值" align="center" prop="value" v-if="columns[1].visible" :show-overflow-tooltip="true" />
<el-table-column label="创建时间" align="center" prop="createTime" v-if="columns[2].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', 'view', 'update', 'delete'])
function handleSelectionChange(selection) {
emits('selection-change', selection)
}
</script>

@ -0,0 +1,75 @@
<!--
* @Author: chris
* @Date: 2025-08-18 09:27:23
* @LastEditors: chris
* @LastEditTime: 2025-09-12 14:51:11
-->
<script setup>
const props = defineProps({
params: {
type: Object,
default: () => ({
nodeId: null,
dateRange: []
})
},
soilOptions: {
type: Object,
default: () => ([])
}
})
const emits = defineEmits(['update:params'])
const queryRef = ref()
const queryParams = reactive({...props.params});
watch(() => queryParams, (newVal) => {
emits('update:params', newVal)
})
watch(() => props.params, (newVal) => {
Object.assign(queryParams, newVal)
})
function resetQuery() {
emits('reset')
}
function handleQuery() {
emits('query')
}
function exportData() {
emits('export')
}
</script>
<template>
<el-form :model="queryParams" ref="queryRef" :inline="true" label-width="68px">
<el-form-item label="参数" style="width: 260px;">
<el-select v-model="queryParams.nodeId" placeholder="请选择参数">
<el-option v-for="value in soilOptions" :key="value.id" :label="value.name" :value="value.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="记录时间">
<el-date-picker
style="width: 300px"
v-model="queryParams.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>
<style lang="scss" scoped>
</style>

@ -0,0 +1,18 @@
/*
* @Author: chris
* @Date: 2025-09-05 10:12:41
* @LastEditors: chris
* @LastEditTime: 2025-09-09 16:17:59
*/
// 列配置
export const columnsConfig = [
{ key: 0, label: "参数名称", visible: true },
{ key: 1, label: "数值", visible: true },
{ key: 2, label: "记录时间", visible: true },
];
// 状态颜色映射
export const statusColorMap = {
0: "success",
1: "danger",
};

@ -0,0 +1,39 @@
<!--
* @Author: chris
* @Date: 2025-09-11 17:11:16
* @LastEditors: chris
* @LastEditTime: 2025-09-12 14:48:24
-->
<script setup>
import HistoryTable from './components/HistoryTable.vue';
import SearchForm from './components/SearchForm.vue';
import { columnsConfig } from './config';
import { useSettings } from '@/hooks/useSettings';
const { hasTags } = useSettings();
const historyList = ref([]);
const queryParams = ref({});
getHistoryList();
function getHistoryList() {
}
function handleSelectionChange(selection) {
console.log(selection)
}
</script>
<template>
<div :class="['soil-history-page', 'page-height', { 'hasTagsView': hasTags }]">
<search-form v-model:params="queryParams" class="mb-12px" />
<history-table :dataList="historyList" :columns="columnsConfig" @selection-change="handleSelectionChange" />
</div>
</template>
<style lang="scss" scoped>
.soil-history-page {
@apply p-20px;
}
</style>

@ -0,0 +1,225 @@
<!--
* @Author: chris
* @Date: 2025-09-09 15:30:24
* @LastEditors: chris
* @LastEditTime: 2025-09-12 17:35:34
-->
<script setup>
import soilImg from '@/assets/images/devices/soil.png'
const testDataList = [
{
id: 1,
title: '土壤温度',
type: 'temperature',
value: '22.5',
icon: 'temperature2',
color: '#FF6B6B',
unit: '°C',
},
{
id: 2,
title: '土壤湿度',
type: 'humidity',
value: '65',
icon: 'humidity',
color: '#4ECDC4',
unit: '%',
},
{
id: 3,
title: 'pH值',
type: 'ph',
value: '6.8',
icon: 'ph-color',
color: '#FFD166',
unit: '',
},
{
id: 4,
title: 'EC',
type: 'ec',
value: '1.2',
icon: 'ec',
color: '#6A0572',
unit: 'mS/cm',
},
{
id: 5,
title: '氮含量',
type: 'nitrogen',
value: '250',
icon: 'nitrogen',
color: '#1A535C',
unit: 'mg/kg',
},
{
id: 6,
title: '磷含量',
type: 'phosphorus',
value: '45',
icon: 'phosphorus',
color: '#77DD77',
unit: 'mg/kg',
},
{
id: 7,
title: '钾含量',
type: 'potassium',
value: '180',
icon: 'potassium',
color: '#845EC2',
unit: 'mg/kg',
}
]
const emits = defineEmits([ 'select' ])
function handleSelect(item) {
console.log('Selected item:', item);
emits('select', item)
}
</script>
<template>
<div class="soil-dashboard">
<!-- 顶部标题区域 -->
<div class="dashboard-header">
<h2 class="dashboard-title">土壤监测数据</h2>
</div>
<!-- 数据卡片区域 -->
<div class="data-cards">
<div
v-for="item in testDataList"
:key="item.id"
class="data-card"
:style="{ '--card-color': item.color }"
@click="handleSelect(item)"
>
<div class="card-icon-wrapper">
<svg-icon :icon-class="item.icon" class="card-icon"></svg-icon>
</div>
<div class="card-info">
<div class="card-label">{{ item.title }}</div>
<div class="card-value-container">
<span class="card-value">{{ item.value }}</span>
<span v-if="item.unit" class="card-unit">{{ item.unit }}</span>
</div>
</div>
</div>
</div>
<!-- 图片区域 -->
<div class="soil-photo">
<el-image
:src="soilImg"
fit="contain"
class="soil-photo-img"
lazy
/>
</div>
</div>
</template>
<style lang="scss" scoped>
//
$bg-primary: #ffffff;
$bg-secondary: #f5f7fa;
$text-primary: #333333;
$text-secondary: #666666;
$shadow-normal: 0 2px 12px rgba(0, 0, 0, 0.05);
$transition-base: all 0.3s ease;
.soil-dashboard {
@apply flex flex-col justify-center item-center w-full h-full overflow-hidden;
}
//
.dashboard-header {
@apply text-center mb-24px;
}
.dashboard-title {
@apply text-24px font-weight-600 m-0 tracking-[0.5px];
color: $text-primary;
}
//
.soil-photo {
@apply w-full h-480px rounded-8px overflow-hidden;
box-shadow: $shadow-normal;
transition: $transition-base;
}
.soil-photo:hover {
@apply translate-y-[-2px] shadow-[0_4px_16px_rgba(0,0,0,0.1)];
}
.soil-photo-img {
@apply h-full w-full transition-[transform] duration-500 ease;
}
.soil-photo-img:hover {
transform: scale(1.03);
}
//
.data-cards {
@apply grid grid-cols-[repeat(auto-fill,minmax(160px,1fr))] gap-16px flex-1 mb-24px;
}
// -
.data-card {
@apply rounded-8px p-16px flex items-center relative border-[1px] border-style-solid border-[transparent] min-h-[100px] cursor-pointer;
background: $bg-secondary;
transition: $transition-base;
&:hover {
@apply translate-y-[-3px] shadow-[0_6px_16px_rgba(0,0,0,0.08)] border-[var(--card-color)];
}
//
&::before {
@apply content-[''] absolute top-[-20px] right-[-20px] w-60px h-60px rounded-[50%] bg-[radial-gradient(circle,rgba(var(--card-color-rgb),0.1)_0%,transparent_70%)] z-0;
background: radial-gradient(circle, rgba(var(--card-color-rgb), 0.1) 0%, transparent 70%);
}
}
//
.card-icon-wrapper {
@apply w-48px h-48px rounded-50% flex items-center justify-center flex-shrink-0 mr-16px bg-[rgba(var(--card-color-rgb),0.1)] z-1;
transition: $transition-base;
}
.data-card:hover .card-icon-wrapper {
@apply bg-[rgba(var(--card-color-rgb),0.15)] transform scale-105;
}
.card-icon {
@apply w-36px h-36px color-[var(--card-color)];
}
//
.card-info {
@apply relative z-1 flex-1;
}
.card-label {
@apply text-14px mb-10px font-500;
color: $text-secondary;
}
.card-value-container {
@apply flex items-baseline gap-4px;
}
.card-value {
@apply text-22px font-600 color-[var(--card-color)] line-height-[1] whitespace-nowrap overflow-hidden text-ellipsis;
}
.card-unit {
@apply text-14px font-400 whitespace-nowrap;
color: $text-secondary;
}
</style>

@ -0,0 +1,498 @@
<!--
* @Author: chris
* @Date: 2025-09-12 17:20:00
* @LastEditors: chris
* @LastEditTime: 2025-09-12 14:29:24
* @Description: 土壤数据曲线图组件支持7天/30/90天数据切换
-->
<script setup>
import * as echarts from 'echarts'
// props
const props = defineProps({
//
soilType: {
type: String,
required: true,
validator: (value) => {
//
const validTypes = ['temperature', 'humidity', 'ph', 'ec', 'nitrogen', 'phosphorus', 'potassium']
return validTypes.includes(value)
}
},
// -
chartData: {
type: Object,
required: true,
// : { dates: [], values: [] }
validator: (value) => {
return value &&
Array.isArray(value.dates) &&
Array.isArray(value.values) &&
value.dates.length === value.values.length
}
},
//
selectedTimeRange: {
type: Number,
default: 7,
validator: (value) => [7, 30, 90].includes(value)
},
//
loading: {
type: Boolean,
default: false
}
})
//
const emit = defineEmits(['timeRangeChange'])
//
const selectedDays = ref(props.selectedTimeRange)
//
watch(() => props.selectedTimeRange, (newValue) => {
selectedDays.value = newValue
})
// ECharts
const chartInstance = ref(null)
//
const chartContainer = ref(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 initChart = () => {
if (!chartContainer.value) return
//
if (chartInstance.value) {
chartInstance.value.dispose()
}
//
chartInstance.value = echarts.init(chartContainer.value)
//
updateChart()
//
window.addEventListener('resize', handleResize)
}
//
const updateChart = () => {
if (!chartInstance.value || !props.chartData) return
const config = soilTypeConfig[props.soilType]
const data = props.chartData
const option = {
title: {
text: `${config.name}趋势图 (近${props.selectedTimeRange}天)`,
left: 'center',
textStyle: {
fontSize: 16,
fontWeight: 'normal'
}
},
tooltip: {
trigger: 'item',
show: true,
alwaysShowContent: false,
backgroundColor: 'rgba(0, 0, 0, 0.8)',
borderColor: '#555',
borderWidth: 1,
padding: 10,
textStyle: {
color: '#fff',
fontSize: 12
},
formatter: function(params) {
return `日期: ${params.name}<br/>${params.seriesName}: ${params.value} ${config.unit}`
},
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
},
crossStyle: {
color: '#999'
}
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: data.dates,
axisLabel: {
//
interval: Math.ceil(data.dates.length / 10),
rotate: data.dates.length > 15 ? 45 : 0
}
},
yAxis: {
type: 'value',
...config.yAxis
},
series: [
{
name: config.name,
type: 'line',
data: data.values,
smooth: true,
symbol: 'circle',
symbolSize: 6,
showSymbol: true, //
lineStyle: {
width: 3,
color: config.color
},
itemStyle: {
color: config.color,
borderWidth: 2,
borderColor: '#fff'
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: `${config.color}40`
},
{
offset: 1,
color: `${config.color}10`
}
])
},
emphasis: {
focus: 'item', //
showSymbol: true, //
itemStyle: {
symbolSize: 10,
borderWidth: 3,
borderColor: '#fff'
}
},
//
triggerLineEvent: true
}
]
}
chartInstance.value.setOption(option, true)
}
//
const handleResize = () => {
if (chartInstance.value) {
chartInstance.value.resize()
}
}
//
watch(selectedDays, (newValue) => {
//
emit('timeRangeChange', newValue)
// updateChartprops.chartData
})
// props
watch(
() => [props.chartData, props.selectedTimeRange],
() => {
nextTick(() => {
updateChart()
})
},
{ deep: true }
)
//
onMounted(() => {
initChart()
})
//
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
if (chartInstance.value) {
chartInstance.value.dispose()
}
})
//
defineExpose({
refreshChart: updateChart
})
</script>
<template>
<div class="soil-chart-container">
<!-- 时间选择器 -->
<div class="chart-controls">
<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>
<!-- 加载状态指示器 -->
<div v-if="loading" class="loading-indicator">
<span class="loading-text">数据加载中...</span>
</div>
</div>
<!-- 图表容器 -->
<div
ref="chartContainer"
class="chart-wrapper"
></div>
</div>
</template>
<style lang="scss" scoped>
// 使unocssscss
.soil-chart-container {
@apply w-full h-full bg-white rounded-md p-4 shadow-md;
}
.chart-controls {
@apply mb-4 flex justify-end items-center gap-4;
}
.loading-indicator {
@apply text-xs text-[#606266];
}
.loading-text {
@apply inline-block ml-1;
animation: fadeInOut 1.5s infinite;
}
//
@keyframes fadeInOut {
0%, 100% { opacity: 0.3; }
50% { opacity: 1; }
}
.time-range-selector {
@apply bg-[#f5f7fa] rounded-md p-1;
}
.chart-wrapper {
@apply w-full transition-all duration-300 ease-in-out;
height: calc(100% - 50px);
}
</style>

@ -0,0 +1,71 @@
<!--
* @Author: chris
* @Date: 2025-09-05 09:29:59
* @LastEditors: chris
* @LastEditTime: 2025-09-12 14:20:17
-->
<script setup>
import LiveData from './components/LiveData.vue'
import SoilChart from './components/SoilChart.vue';
import { useSettings } from '@/hooks/useSettings';
import { getFormattedSoilData } from './mock';
const { hasTags } = useSettings()
const liveData = ref({})
const chartData = ref({ dates: [], values: []})
const soilType = ref('temperature')
const selectedTimeRange = ref(7)
const chartLoading = ref(false)
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
}
function handleSelectionChange(selection) {
console.log(selection)
}
function handleSelectSoilItem(item) {
soilType.value = item.type
getChartData()
}
function handleTimeRangeChange(range) {
selectedTimeRange.value = range
getChartData()
}
</script>
<template>
<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"/>
</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" />
<!-- <search-form v-model:params="queryParams" class="mb-12px" />
<history-table :dataList="historyList" :columns="columnsConfig" @selection-change="handleSelectionChange" /> -->
</el-col>
</el-row>
</div>
</template>
<style lang="scss" scoped>
.soil-monitor-page {
@apply p-20px;
> .el-row {
@apply h-full;
}
}
</style>

@ -0,0 +1,218 @@
// 气象数据模拟工具
/**
* 生成日期数组
* @param {number} days - 天数
* @returns {Array} 日期数组
*/
const 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);
dates.push(`${date.getMonth() + 1}/${date.getDate()}`);
}
return dates;
};
/**
* 生成随机数值数组
* @param {number} count - 数据点数量
* @param {number} min - 最小值
* @param {number} max - 最大值
* @param {number} base - 基准值
* @param {number} volatility - 波动率 (0-1)
* @returns {Array} 数值数组
*/
const generateValues = (count, min, max, base = 0, volatility = 0.1) => {
const values = [];
let current = base || min + (max - min) * Math.random();
for (let i = 0; i < count; i++) {
// 添加随机波动
const change = (max - min) * volatility * (Math.random() - 0.5);
current = Math.max(min, Math.min(max, current + change));
// 对于特定类型添加趋势
if (i % 10 === 0 && Math.random() > 0.7) {
current += (max - min) * 0.05 * (Math.random() - 0.5);
}
values.push(parseFloat(current.toFixed(1)));
}
return values;
};
/**
* 生成指定天数的土壤数据
* @param {number} days - 天数
* @returns {Object} 土壤数据对象
*/
const generateSoilData = (days) => {
const dates = generateDates(days);
const count = dates.length;
return {
// 土壤温度数据 (10-30°C)
temperature: {
7: {
dates: generateDates(7),
values: generateValues(7, 15, 28, 22, 0.1),
},
30: {
dates: generateDates(30),
values: generateValues(30, 10, 30, 23, 0.12),
},
90: {
dates: generateDates(90),
values: generateValues(90, 8, 32, 22, 0.15),
},
},
// 土壤湿度数据 (30-90%)
humidity: {
7: {
dates: generateDates(7),
values: generateValues(7, 40, 85, 65, 0.15),
},
30: {
dates: generateDates(30),
values: generateValues(30, 30, 90, 60, 0.2),
},
90: {
dates: generateDates(90),
values: generateValues(90, 25, 95, 62, 0.2),
},
},
// 土壤pH值数据 (4.0-9.0)
ph: {
7: {
dates: generateDates(7),
values: generateValues(7, 6.0, 7.5, 6.8, 0.05),
},
30: {
dates: generateDates(30),
values: generateValues(30, 5.5, 8.0, 6.7, 0.07),
},
90: {
dates: generateDates(90),
values: generateValues(90, 4.0, 9.0, 6.6, 0.1),
},
},
// 土壤电导率(EC)数据 (0.1-5.0 mS/cm)
ec: {
7: {
dates: generateDates(7),
values: generateValues(7, 0.5, 2.0, 1.2, 0.1),
},
30: {
dates: generateDates(30),
values: generateValues(30, 0.3, 3.0, 1.3, 0.15),
},
90: {
dates: generateDates(90),
values: generateValues(90, 0.1, 5.0, 1.2, 0.2),
},
},
// 土壤氮含量数据 (50-500 mg/kg)
nitrogen: {
7: {
dates: generateDates(7),
values: generateValues(7, 150, 350, 250, 0.1),
},
30: {
dates: generateDates(30),
values: generateValues(30, 100, 400, 260, 0.15),
},
90: {
dates: generateDates(90),
values: generateValues(90, 50, 500, 250, 0.2),
},
},
// 土壤磷含量数据 (10-200 mg/kg)
phosphorus: {
7: {
dates: generateDates(7),
values: generateValues(7, 30, 70, 45, 0.08),
},
30: {
dates: generateDates(30),
values: generateValues(30, 20, 100, 48, 0.1),
},
90: {
dates: generateDates(90),
values: generateValues(90, 10, 200, 50, 0.15),
},
},
// 土壤钾含量数据 (50-400 mg/kg)
potassium: {
7: {
dates: generateDates(7),
values: generateValues(7, 120, 250, 180, 0.07),
},
30: {
dates: generateDates(30),
values: generateValues(30, 100, 300, 190, 0.1),
},
90: {
dates: generateDates(90),
values: generateValues(90, 50, 400, 180, 0.15),
},
},
};
};
/**
* 生成实时土壤数据
* @returns {Object} 实时土壤数据
*/
const generateRealtimeSoilData = () => {
return {
temperature: parseFloat((20 + Math.random() * 8).toFixed(1)),
humidity: parseFloat((50 + Math.random() * 30).toFixed(1)),
ph: parseFloat((6.0 + Math.random() * 1.5).toFixed(1)),
ec: parseFloat((0.5 + Math.random() * 1.5).toFixed(1)),
nitrogen: parseFloat((150 + Math.random() * 200).toFixed(1)),
phosphorus: parseFloat((20 + Math.random() * 60).toFixed(1)),
potassium: parseFloat((100 + Math.random() * 200).toFixed(1)),
updateTime: new Date().toLocaleTimeString("zh-CN"),
};
};
// 导出模拟数据
export const soilMockData = generateSoilData();
export const realtimeSoilData = generateRealtimeSoilData();
/**
* 获取指定土壤类型和时间范围的格式化数据用于SoilChart组件
* @param {string} soilType - 土壤类型
* @param {number} days - 时间范围7/30/90
* @returns {Object} 格式化的数据 { dates: [], values: [] }
*/
const getFormattedSoilData = (soilType, days) => {
// 验证参数
if (!soilMockData[soilType] || !soilMockData[soilType][days]) {
console.error(`无效的土壤类型或时间范围: ${soilType}, ${days}`);
return { dates: [], values: [] };
}
return soilMockData[soilType][days];
};
// 导出工具函数供其他组件使用
export {
generateDates,
generateValues,
generateSoilData,
generateRealtimeSoilData,
getFormattedSoilData,
};

@ -2,7 +2,7 @@
* @Author: chris * @Author: chris
* @Date: 2025-09-09 15:30:24 * @Date: 2025-09-09 15:30:24
* @LastEditors: chris * @LastEditors: chris
* @LastEditTime: 2025-09-11 17:27:00 * @LastEditTime: 2025-09-12 17:36:39
--> -->
<script setup> <script setup>
import weatherImg from '@/assets/images/devices/weather.png' import weatherImg from '@/assets/images/devices/weather.png'
@ -176,9 +176,7 @@ $transition-base: all 0.3s ease;
transition: $transition-base; transition: $transition-base;
&:hover { &:hover {
transform: translateY(-3px); @apply translate-y-[-3px] shadow-[0_6px_16px_rgba(0,0,0,0.08)] border-[var(--card-color)];
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08);
border-color: var(--card-color);
} }
// //

Loading…
Cancel
Save