feat: 新增土壤及气象检测页面

style: 调整主页样式
master
chris 5 days ago
parent 8b2663daa2
commit c8363875d3

@ -2,7 +2,7 @@
* @Author: chris
* @Date: 2025-09-15 09:51:59
* @LastEditors: chris
* @LastEditTime: 2025-10-22 11:12:44
* @LastEditTime: 2025-10-22 17:03:42
-->
<script setup>
import bug from '@/static/svg/bug.svg'
@ -61,7 +61,7 @@ const modules = [
title: '气象监测',
icon: tianqiyujing,
desc: '温度、湿度、光照等数据',
route: '/pages/weather/index',
route: '/pages/weather/weather',
},
{
id: 'drone',
@ -97,20 +97,22 @@ function handleModuleClick(module) {
<view class="home-content">
<!-- 果园基本信息卡片 -->
<view class="info-section">
<wd-card hoverable :shadow="false" class="basic-card">
<wd-card :shadow="false" class="basic-card">
<!-- 基本信息网格 -->
<wd-grid :column="2" :border="false" :gutter="12">
<wd-grid-item
<wd-row :gutter="12">
<wd-col
v-for="(item, key) in itemDict"
:key="key"
class="info-item-wrapper"
:span="12"
>
<view class="info-item">
<text class="info-label">{{ item.text }}</text>
<text class="info-value">{{ item.value }}</text>
<view class="info-item-wrapper">
<view class="info-item">
<text class="info-label">{{ item.text }}</text>
<text class="info-value">{{ item.value }}</text>
</view>
</view>
</wd-grid-item>
</wd-grid>
</wd-col>
</wd-row>
</wd-card>
</view>
@ -119,38 +121,48 @@ function handleModuleClick(module) {
<text class="section-title">实时监控</text>
<!-- 模块网格 -->
<wd-grid :column="2" :border="false" bg-color="transparent" :gutter="15">
<wd-grid-item v-for="module in modules" :key="module.id" class="module-item-wrapper" custom-class="module-item">
<view
class="module-card"
@click="handleModuleClick(module)"
>
<view class="module-icon-container" :class="`module-${module.id}`">
<image :src="module.icon" mode="aspectFit" class="module-icon" />
</view>
<view class="module-info">
<text class="module-title">{{ module.title }}</text>
<text class="module-desc">{{ module.desc }}</text>
<wd-row :gutter="15" class="module-row">
<wd-col
v-for="module in modules"
:key="module.id"
:span="12"
>
<view class="module-item-wrapper">
<view
class="module-card"
@tap="handleModuleClick(module)"
>
<view class="module-icon-container" :class="`module-${module.id}`">
<image :src="module.icon" mode="aspectFit" class="module-icon" />
</view>
<view class="module-info">
<text class="module-title">{{ module.title }}</text>
<text class="module-desc">{{ module.desc }}</text>
</view>
</view>
</view>
</wd-grid-item>
</wd-grid>
</wd-col>
</wd-row>
</view>
</view>
</view>
</template>
<style lang="scss" scoped>
// -
$primary-color: #165dff; //
$bg-color: #f5f7fa; //
// -
$primary-color: #00c6ff; //
$primary-color-dark: #0072ff; //
$bg-color: #f0f2f5; //
$card-bg: #ffffff; //
$text-primary: #1d2129; //
$text-secondary: #4e5969; //
$text-tertiary: #86909c; //
$border-radius: 12px; //
$box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); //
$transition: all 0.3s ease; //
$card-bg-glass: rgba(255, 255, 255, 0.85); //
$text-primary: #0f1419; //
$text-secondary: #333f51; //
$text-tertiary: #6e7683; //
$border-radius: 16px; //
$box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06); //
$box-shadow-hover: 0 8px 32px rgba(0, 198, 255, 0.15); //
$transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1); //
$glow-effect: 0 0 15px rgba(0, 198, 255, 0.3); //
//
.home-page {
@ -160,48 +172,94 @@ $transition: all 0.3s ease; // 简单的过渡效果
//
.home-title {
background: linear-gradient(135deg, $primary-color, #4080ff);
padding: 24px 0;
background: linear-gradient(135deg, $primary-color, $primary-color-dark);
padding: 22px 0;
position: relative;
overflow: hidden;
box-shadow: 0 3px 12px rgba($primary-color, 0.25);
box-shadow:
0 4px 20px rgba($primary-color, 0.35),
$glow-effect;
//
//
&::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%);
transform: rotate(30deg);
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image:
linear-gradient(rgba(255, 255, 255, 0.1) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.1) 1px, transparent 1px);
background-size: 20px 20px;
background-position: -1px -1px;
z-index: 1;
}
//
&::after {
content: '';
position: absolute;
top: 20%;
right: 10%;
width: 120px;
height: 120px;
background: radial-gradient(circle, rgba(255, 255, 255, 0.3) 0%, transparent 70%);
filter: blur(20px);
z-index: 0;
animation: glowPulse 4s infinite alternate;
}
.title-container {
position: relative;
z-index: 1;
z-index: 2;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
gap: 10px;
}
.title-text {
font-size: 24px;
font-weight: 700;
font-size: 28px;
font-weight: 600;
color: $card-bg;
text-align: center;
letter-spacing: 0.5px;
letter-spacing: 0.8px;
position: relative;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
// 线
&::after {
content: '';
position: absolute;
bottom: -8px;
left: 50%;
transform: translateX(-50%);
width: 60%;
height: 2px;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.8), transparent);
}
}
.title-accent {
width: 40px;
height: 4px;
background: rgba(255, 255, 255, 0.3);
border-radius: 2px;
opacity: 0.8;
width: 50px;
height: 3px;
background: rgba(255, 255, 255, 0.4);
border-radius: 3px;
opacity: 0.9;
backdrop-filter: blur(2px);
box-shadow: 0 0 10px rgba(255, 255, 255, 0.3);
}
}
//
@keyframes glowPulse {
0% {
opacity: 0.6;
transform: scale(1);
}
100% {
opacity: 0.9;
transform: scale(1.1);
}
}
@ -231,6 +289,21 @@ $transition: all 0.3s ease; // 简单的过渡效果
border-radius: $border-radius;
box-shadow: $box-shadow;
overflow: hidden;
background: $card-bg-glass;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
position: relative;
padding: 10px !important;
//
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: linear-gradient(90deg, $primary-color, $primary-color-dark);
}
}
// -
@ -238,33 +311,84 @@ $transition: all 0.3s ease; // 简单的过渡效果
display: flex;
flex-direction: column;
align-items: center;
// padding: 16px;
height: 100%;
min-height: 64px;
min-height: 80px;
justify-content: center;
text-align: center;
position: relative;
padding: 10px;
//
&::before {
content: '';
position: absolute;
top: 10px;
right: 10px;
width: 30px;
height: 30px;
background: radial-gradient(circle, rgba(0, 198, 255, 0.1) 0%, transparent 70%);
border-radius: 50%;
opacity: 0.6;
}
.info-label {
font-size: 13px;
font-size: 14px;
color: $text-tertiary;
margin-bottom: 6px;
margin-bottom: 8px;
letter-spacing: 0.3px;
position: relative;
z-index: 1;
}
.info-value {
font-size: 18px;
font-size: 20px;
font-weight: 600;
color: $primary-color;
color: $primary-color-dark;
letter-spacing: 0.5px;
position: relative;
z-index: 1;
//
&::after {
content: '';
position: absolute;
bottom: -4px;
left: 50%;
transform: translateX(-50%);
width: 40%;
height: 1px;
background: linear-gradient(90deg, transparent, $primary-color, transparent);
opacity: 0.6;
}
}
}
//
.monitor-section {
.section-title {
font-size: 16px;
font-size: 18px;
font-weight: 600;
color: $text-primary;
display: block;
padding-left: 15px;
margin-bottom: 16px;
position: relative;
//
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 16px;
background: linear-gradient(180deg, $primary-color, $primary-color-dark);
border-radius: 2px;
box-shadow: 0 0 8px rgba($primary-color, 0.3);
}
}
//
.module-row {
// padding: 0 15px;
}
//
@ -272,19 +396,14 @@ $transition: all 0.3s ease; // 简单的过渡效果
height: 100%;
}
:deep(.module-item) {
.wd-grid-item__content {
padding: 0;
}
}
// -
.module-card {
padding: 15px;
background-color: $card-bg;
padding: 20px 15px;
margin: 7.5px 0;
background-color: $card-bg-glass;
border-radius: $border-radius;
box-shadow: $box-shadow;
height: 150px;
height: 160px;
display: flex;
flex-direction: column;
align-items: center;
@ -293,6 +412,43 @@ $transition: all 0.3s ease; // 简单的过渡效果
cursor: pointer;
transition: $transition;
overflow: hidden;
position: relative;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.3);
//
&::before {
content: '';
position: absolute;
top: -1px;
left: -1px;
right: -1px;
bottom: -1px;
border-radius: $border-radius;
padding: 1px;
background: linear-gradient(135deg, rgba(0, 198, 255, 0.2), transparent 50%, rgba(0, 198, 255, 0.2));
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
pointer-events: none;
opacity: 0;
transition: opacity 0.3s ease;
}
//
&::after {
content: '';
position: absolute;
bottom: -20px;
right: -20px;
width: 60px;
height: 60px;
background: radial-gradient(circle, rgba(0, 198, 255, 0.1) 0%, transparent 70%);
border-radius: 50%;
opacity: 0.5;
}
}
// -
@ -300,55 +456,110 @@ $transition: all 0.3s ease; // 简单的过渡效果
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 10px;
width: 56px;
height: 56px;
border-radius: 8px;
margin-bottom: 12px;
width: 64px;
height: 64px;
border-radius: 12px;
position: relative;
overflow: hidden;
transition: transform 0.3s ease;
.module-card:hover & {
transform: scale(1.05);
}
&.module-pest {
background-color: rgba(245, 87, 108, 0.08);
background: linear-gradient(135deg, rgba(245, 87, 108, 0.12), rgba(245, 87, 108, 0.05));
box-shadow: 0 4px 12px rgba(245, 87, 108, 0.1);
}
&.module-soil {
background-color: rgba(82, 196, 26, 0.08);
background: linear-gradient(135deg, rgba(82, 196, 26, 0.12), rgba(82, 196, 26, 0.05));
box-shadow: 0 4px 12px rgba(82, 196, 26, 0.1);
}
&.module-weather {
background-color: rgba(66, 153, 225, 0.08);
background: linear-gradient(135deg, rgba(66, 153, 225, 0.12), rgba(66, 153, 225, 0.05));
box-shadow: 0 4px 12px rgba(66, 153, 225, 0.1);
}
&.module-drone {
background-color: rgba(255, 152, 0, 0.08);
background: linear-gradient(135deg, rgba(255, 152, 0, 0.12), rgba(255, 152, 0, 0.05));
box-shadow: 0 4px 12px rgba(255, 152, 0, 0.1);
}
//
&::before {
content: '';
position: absolute;
top: 0;
left: -50%;
width: 20%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
transform: skewX(-20deg);
animation: shine 3s infinite linear;
}
}
//
@keyframes shine {
0% {
left: -50%;
}
100% {
left: 150%;
}
}
//
.module-icon {
width: 28px;
height: 28px;
width: 32px;
height: 32px;
transition: filter 0.3s ease;
.module-card:hover & {
filter: brightness(1.1);
}
}
//
.module-info {
width: 100%;
position: relative;
z-index: 1;
}
.module-title {
font-size: 15px;
font-size: 16px;
font-weight: 600;
color: $text-primary;
margin-bottom: 4px;
margin-bottom: 6px;
display: block;
letter-spacing: 0.2px;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.module-desc {
font-size: 12px;
font-size: 13px;
color: $text-tertiary;
line-height: 1.4;
line-height: 1.5;
display: block;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
letter-spacing: 0.1px;
//
&::after {
content: '';
position: absolute;
bottom: -4px;
left: 50%;
transform: translateX(-50%);
width: 30%;
height: 1px;
background: linear-gradient(90deg, transparent, $primary-color, transparent);
opacity: 0.3;
}
}
}
</style>

@ -0,0 +1,184 @@
<!--
* @Author: chris
* @Date: 2025-10-22 11:42:40
* @LastEditors: chris
* @LastEditTime: 2025-10-22 14:59:49
-->
<script setup name="line-chart">
import LEchart from '@/uni_modules/lime-echart_1.0.5/components/l-echart/l-echart.vue'
const props = defineProps({
echarts: {
type: Object,
required: true,
},
chartData: {
type: Object,
default: () => ({
times: [],
values: [],
title: '',
unit: '',
color: '#4dabf7',
}),
},
})
const lineChartRef = ref(null)
let lineChart = null
onMounted(async () => {
nextTick(async () => {
await initChart()
})
})
onUnmounted(() => {
if (lineChart)
lineChart.dispose()
})
async function initChart() {
if (!lineChartRef.value || !props.echarts)
return
lineChart = await lineChartRef.value.init(props.echarts)
refreshChart()
}
function refreshChart() {
if (!lineChart) {
nextTick(async () => {
await initChart()
})
return
}
lineChart.showLoading()
lineChart.setOption(createOptions())
lineChart.hideLoading()
}
function createOptions() {
const gradient = new props.echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: `${props.chartData.color}80` }, // 80
{ offset: 1, color: `${props.chartData.color}20` }, // 20
])
const option = {
title: {
text: props.chartData.title,
top: '2%',
textStyle: {
color: '#333',
fontSize: 16,
fontWeight: 'bold',
},
left: 'center',
},
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(0, 0, 0, 0.7)',
borderColor: '#333',
textStyle: {
color: '#fff',
},
formatter: (params) => {
const data = params[0]
return `${data.axisValue} ${data.value} ${props.chartData.unit}`
},
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985',
},
},
},
grid: {
left: '3%',
right: '3%',
bottom: '15%', //
top: '20%',
containLabel: true,
},
xAxis: {
type: 'category',
boundaryGap: false,
data: props.chartData.times,
axisLine: {
lineStyle: {
color: '#464646',
},
},
axisLabel: {
color: '#666',
rotate: 45,
fontSize: 10,
},
},
yAxis: [
{
type: 'value',
name: props.chartData.unit,
nameTextStyle: {
color: '#666',
},
axisLine: {
lineStyle: {
color: '#464646',
},
},
axisLabel: {
color: '#666',
},
splitLine: {
lineStyle: {
color: '#f0f0f0',
type: 'dashed',
},
},
},
],
series: [
{
name: props.chartData.title,
type: 'line',
data: props.chartData.values,
smooth: true,
symbol: 'circle',
symbolSize: 6,
itemStyle: {
color: props.chartData.color,
},
lineStyle: {
width: 2,
},
areaStyle: {
color: gradient,
},
emphasis: {
focus: 'series',
},
},
],
}
return option
}
//
watch(() => props.chartData, () => {
refreshChart()
}, { deep: true })
</script>
<template>
<view class="line-chart">
<l-echart id="soilLineChart" ref="lineChartRef" type="2d" />
</view>
</template>
<style lang="scss" scoped>
.line-chart {
width: 100%;
height: 300px;
}
</style>

@ -2,9 +2,14 @@
* @Author: chris
* @Date: 2025-10-22 10:46:12
* @LastEditors: chris
* @LastEditTime: 2025-10-22 11:24:24
* @LastEditTime: 2025-10-22 15:29:18
-->
<script setup lang="ts">
//
const echarts = require('../../uni_modules/lime-echart_1.0.5/static/echarts.min2.js')
import LineChart from './components/lineChart.vue'
//
definePage({
navigationBarTitleText: '土壤检测',
@ -121,8 +126,14 @@ function generateTimeSeriesData(indicatorId: string): TimeSeriesData[] {
//
const timeSeriesData = ref<TimeSeriesData[]>([])
//
const chartOption = ref<any>({})
//
const chartData = ref({
times: [],
values: [],
title: '',
unit: '',
color: '#4dabf7',
})
//
function handleIndicatorClick(indicatorId: string) {
@ -130,7 +141,7 @@ function handleIndicatorClick(indicatorId: string) {
updateChart()
};
//
//
function updateChart() {
const currentIndicator = soilIndicators.find(ind => ind.id === selectedIndicator.value)
if (!currentIndicator)
@ -138,77 +149,12 @@ function updateChart() {
timeSeriesData.value = generateTimeSeriesData(selectedIndicator.value)
chartOption.value = {
title: {
text: `${currentIndicator.name} 24小时变化趋势`,
left: 'center',
textStyle: {
fontSize: 16,
fontWeight: 'normal',
},
},
tooltip: {
trigger: 'axis',
formatter: (params: any) => {
const data = params[0]
return `${data.axisValue}<br/>${currentIndicator.name}: ${data.value} ${currentIndicator.unit}`
},
},
grid: {
left: '3%',
right: '4%',
bottom: '10%',
top: '20%',
containLabel: true,
},
xAxis: {
type: 'category',
data: timeSeriesData.value.map(item => item.time),
axisLabel: {
rotate: 45,
fontSize: 10,
},
},
yAxis: {
type: 'value',
name: currentIndicator.unit,
nameLocation: 'middle',
nameGap: 30,
},
series: [
{
data: timeSeriesData.value.map(item => item.value),
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 6,
itemStyle: {
color: currentIndicator.color,
},
lineStyle: {
width: 2,
},
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: `${currentIndicator.color}80`, // 80
},
{
offset: 1,
color: `${currentIndicator.color}20`, // 20
},
],
},
},
},
],
chartData.value = {
times: timeSeriesData.value.map(item => item.time),
values: timeSeriesData.value.map(item => item.value),
title: `${currentIndicator.name} 24小时变化趋势`,
unit: currentIndicator.unit,
color: currentIndicator.color,
}
};
@ -250,63 +196,34 @@ onMounted(() => {
<!-- 曲线图区域 -->
<view class="chart-container">
<lime-echart
class="soil-chart"
:option="chartOption"
:canvas-id="`soil-chart-${selectedIndicator}`"
:lazy-load="true"
/>
<line-chart id="soilChart" :echarts="echarts" :chart-data="chartData" />
</view>
</view>
</template>
<style lang="scss" scoped>
.soil-monitor-page {
padding: 20rpx;
background-color: #f8f8f8;
min-height: 100vh;
@apply p-20rpx bg-[#f8f8f8] min-h-100vh;
}
.page-header {
text-align: center;
margin-bottom: 30rpx;
@apply text-center mb-30rpx;
}
.header-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
@apply text-36rpx font-bold text-[#333];
}
.header-subtitle {
font-size: 24rpx;
color: #666;
display: block;
margin-top: 10rpx;
@apply text-24rpx text-[#666] mt-10rpx block;
}
.indicators-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300rpx, 1fr));
gap: 20rpx;
margin-bottom: 30rpx;
justify-content: center;
@apply grid grid-cols-[repeat(auto-fit,minmax(300rpx,1fr))] gap-20rpx justify-center mb-30rpx;
}
.indicator-card {
background-color: #fff;
padding: 16rpx 20rpx;
border-radius: 16rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
border-left: 8rpx solid #e0e0e0;
transition: all 0.3s ease;
cursor: pointer;
min-width: 280rpx;
display: flex;
flex-direction: column;
justify-content: center;
min-height: 120rpx;
height: auto;
@apply bg-white px-20rpx py-16rpx rounded-16rpx shadow-[0_2rpx_10rpx_rgba(0,0,0,0.05)] border-l-[#e0e0e0] border-l-8rpx border-l-solid min-w-[280rpx] min-h-[120rpx] h-auto cursor-pointer transition-all duration-300 ease-in-out flex justify-center flex-col min-h-[120rpx];
}
.indicator-card.active {
@ -315,119 +232,22 @@ onMounted(() => {
}
.indicator-name {
font-size: 26rpx;
color: #666;
margin-bottom: 4rpx;
@apply text-26rpx color-[#666] mb-4rpx;
}
.indicator-value {
font-size: 34rpx;
font-weight: bold;
margin-bottom: 2rpx;
@apply text-34rpx font-bold mb-2rpx;
}
.indicator-unit {
font-size: 22rpx;
font-weight: normal;
margin-left: 4rpx;
@apply text-22rpx ml-4rpx;
}
.indicator-description {
font-size: 18rpx;
color: #999;
margin-top: 4rpx;
@apply text-18rpx color-[#999] mt-4rpx;
}
.chart-container {
background-color: #fff;
border-radius: 16rpx;
padding: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
height: 600rpx;
}
.soil-chart {
width: 100%;
height: 100%;
}
//
@media screen and (min-width: 768px) {
.indicators-grid {
grid-template-columns: repeat(3, 1fr);
max-width: 900px;
margin-left: auto;
margin-right: auto;
}
// 7 - 使auto-fit7
.indicators-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280rpx, 1fr));
gap: 24rpx;
justify-items: center;
}
// 7
.indicators-grid {
--auto-grid-min-size: 280rpx;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(var(--auto-grid-min-size), 1fr));
grid-gap: 24rpx;
justify-content: center;
}
//
.indicator-card {
min-width: 280rpx;
width: 100%;
max-width: 320rpx;
}
}
@media screen and (min-width: 1024px) {
.indicators-grid {
max-width: 1200px;
--auto-grid-min-size: 260rpx;
}
// 7
.indicators-grid {
grid-template-columns: repeat(4, 1fr);
grid-template-rows: auto auto;
}
// 5-7使
.indicators-grid > .indicator-card:nth-child(5) {
grid-column: 1;
grid-row: 2;
}
.indicators-grid > .indicator-card:nth-child(6) {
grid-column: 2;
grid-row: 2;
}
.indicators-grid > .indicator-card:nth-child(7) {
grid-column: 3;
grid-row: 2;
grid-column-end: 5; /* 让第7个卡片占据更宽的空间平衡布局 */
max-width: none;
}
// 7使
.indicators-grid > .indicator-card:nth-child(7) {
display: grid;
grid-template-columns: 1fr 1fr;
align-items: center;
text-align: center;
}
.indicators-grid > .indicator-card:nth-child(7) .indicator-info {
grid-column: 1 / -1;
}
.indicators-grid > .indicator-card:nth-child(7) .indicator-value-container {
grid-column: 1 / -1;
margin: 10rpx 0;
}
@apply bg-[#fff] rounded-16rpx p-20rpx shadow-[0_2rpx_10rpx_rgba(0,0,0,0.05)] mt-20rpx;
}
</style>

@ -0,0 +1,169 @@
<template>
<l-echart
ref="chartRef"
type="2d"
class="line-chart"
/>
</template>
<script setup lang="ts">
interface TimeSeriesData {
time: string
value: number
}
interface ChartData {
title: string
unit: string
color: string
data: TimeSeriesData[]
}
const props = defineProps<{
echarts: any
chartData: ChartData
}>()
const emit = defineEmits(['init'])
const chartRef = ref<any>(null)
let chart: any = null
const chartOption = ref<any>({})
//
async function initChart() {
await nextTick()
if (!chartRef.value || !props.chartData.data.length)
return
try {
chart = await chartRef.value.init(props.echarts)
createOptions()
refreshChart()
emit('init', chart)
}
catch (error) {
console.error('Chart initialization error:', error)
}
}
//
function refreshChart() {
if (!chart)
return
chart.setOption(chartOption.value, true)
}
//
function createOptions() {
const { title, unit, color, data } = props.chartData
chartOption.value = {
title: {
text: title,
left: 'center',
textStyle: {
fontSize: 16,
fontWeight: 'normal',
},
},
tooltip: {
trigger: 'axis',
formatter: (params: any) => {
const param = params[0]
return `${param.axisValue} ${param.value} ${unit}`
},
},
grid: {
left: '3%',
right: '3%',
bottom: '3%',
top: '20%',
containLabel: true,
},
xAxis: {
type: 'category',
data: data.map(item => item.time),
axisLabel: {
rotate: 45,
fontSize: 10,
},
},
yAxis: {
type: 'value',
name: unit,
nameGap: 15,
},
series: [
{
data: data.map(item => item.value),
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 6,
itemStyle: {
color,
},
lineStyle: {
width: 2,
},
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: `${color}80`, // 80
},
{
offset: 1,
color: `${color}20`, // 20
},
],
},
},
},
],
}
}
//
watch(() => props.chartData, () => {
if (chart) {
createOptions()
refreshChart()
}
else {
initChart()
}
}, { deep: true })
//
onMounted(() => {
initChart()
})
//
onUnmounted(() => {
if (chart) {
chart.dispose()
chart = null
}
})
//
defineExpose({
initChart,
refreshChart,
})
</script>
<style lang="scss" scoped>
.line-chart {
@apply w-full h-[300px];
}
</style>

@ -0,0 +1,269 @@
<!--
* @Author: chris
* @Date: 2025-10-22 16:00:00
* @LastEditors: chris
* @LastEditTime: 2025-10-22 16:00:00
-->
<script setup lang="ts">
const echarts = require('../../uni_modules/lime-echart_1.0.5/static/echarts.min2.js')
import LineChart from './components/lineChart.vue'
//
definePage({
navigationBarTitleText: '气象监测',
})
//
interface WeatherIndicator {
id: string
name: string
value: number
unit: string
color: string
description: string
}
//
interface TimeSeriesData {
time: string
value: number
}
//
const weatherIndicators: WeatherIndicator[] = [
{ id: 'temperature', name: '温度', value: 28.5, unit: '°C', color: '#FF6B6B', description: '空气温度' },
{ id: 'light', name: '光照', value: 850, unit: 'lux', color: '#FFD93D', description: '光照强度' },
{ id: 'windSpeed', name: '风速', value: 3.2, unit: 'm/s', color: '#4ECDC4', description: '平均风速' },
{ id: 'windDirection', name: '风向', value: 135, unit: '°', color: '#45B7D1', description: '风向角度' },
{ id: 'rainfall', name: '降雨量', value: 0, unit: 'mm', color: '#6C5CE7', description: '累计降雨量' },
]
//
const selectedIndicator = ref<string>('temperature')
// 24
function generateTimeSeriesData(indicatorId: string): TimeSeriesData[] {
const data: TimeSeriesData[] = []
const now = new Date()
//
let baseValue = 0
let min = 0
let max = 0
switch (indicatorId) {
case 'temperature':
baseValue = 28
min = 18
max = 35
break
case 'light':
baseValue = 800
min = 0
max = 1200
break
case 'windSpeed':
baseValue = 3.0
min = 0
max = 8.0
break
case 'windDirection':
baseValue = 135
min = 0
max = 360
break
case 'rainfall':
baseValue = 0
min = 0
max = 20
break
}
// 24
for (let i = 23; i >= 0; i--) {
const time = new Date(now.getTime() - i * 60 * 60 * 1000)
const hour = time.getHours()
//
let timeFactor = 0
//
if (indicatorId === 'temperature') {
timeFactor = Math.sin(((hour - 6) / 24) * Math.PI * 2) * 0.3
}
else if (indicatorId === 'light') {
//
if (hour >= 6 && hour <= 18) {
timeFactor = Math.sin(((hour - 6) / 12) * Math.PI) * 0.8
}
else {
timeFactor = -0.9
}
}
else if (indicatorId === 'windDirection') {
//
timeFactor = (Math.random() - 0.5) * 0.5
}
else if (indicatorId === 'rainfall') {
//
timeFactor = Math.random() > 0.8 ? Math.random() * 0.5 : -0.1
}
else {
timeFactor = (Math.random() - 0.5) * 0.2
}
//
const randomFactor = (Math.random() - 0.5) * 0.2
let value = baseValue * (1 + timeFactor + randomFactor)
//
value = Math.max(min, Math.min(max, value))
//
if (indicatorId === 'temperature' || indicatorId === 'windSpeed') {
value = Number.parseFloat(value.toFixed(1))
}
else if (indicatorId === 'windDirection') {
value = Math.round(value)
}
else if (indicatorId === 'light') {
value = Math.round(value)
}
else {
value = Number.parseFloat(value.toFixed(1))
}
data.push({
time: `${hour.toString().padStart(2, '0')}:00`,
value,
})
}
return data
};
//
const timeSeriesData = ref<TimeSeriesData[]>([])
//
const chartData = ref<any>({})
//
function handleIndicatorClick(indicatorId: string) {
selectedIndicator.value = indicatorId
updateChart()
}
//
function updateChart() {
const currentIndicator = weatherIndicators.find(ind => ind.id === selectedIndicator.value)
if (!currentIndicator)
return
timeSeriesData.value = generateTimeSeriesData(selectedIndicator.value)
chartData.value = {
title: `${currentIndicator.name} 24小时变化趋势`,
unit: currentIndicator.unit,
color: currentIndicator.color,
data: timeSeriesData.value,
}
}
//
onMounted(() => {
updateChart()
})
</script>
<template>
<view class="weather-monitor-page">
<!-- 页面标题 -->
<view class="page-header">
<text class="header-title">气象监测数据</text>
<text class="header-subtitle">实时监测环境气象指标</text>
</view>
<!-- 气象指标卡片网格 -->
<view class="indicators-grid">
<view
v-for="indicator in weatherIndicators"
:key="indicator.id"
class="indicator-card"
:class="{ active: selectedIndicator === indicator.id }"
:style="{ borderColor: indicator.color }"
@click="handleIndicatorClick(indicator.id)"
>
<view class="indicator-name">
{{ indicator.name }}
</view>
<view class="indicator-value" :style="{ color: indicator.color }">
{{ indicator.value }}<span class="indicator-unit">{{ indicator.unit }}</span>
</view>
<view class="indicator-description">
{{ indicator.description }}
</view>
</view>
</view>
<!-- 曲线图区域 -->
<view class="chart-container">
<line-chart
:echarts="echarts"
:chart-data="chartData"
:canvas-id="`weather-chart-${selectedIndicator}`"
/>
</view>
</view>
</template>
<style lang="scss" scoped>
.weather-monitor-page {
@apply p-20rpx bg-[#f8f8f8] min-h-100vh;
}
.page-header {
@apply text-center mb-30rpx;
}
.header-title {
@apply text-36rpx font-bold text-[#333];
}
.header-subtitle {
@apply text-24rpx text-[#666] mt-10rpx block;
}
.indicators-grid {
@apply grid grid-cols-[repeat(auto-fit,minmax(300rpx,1fr))] gap-20rpx justify-center mb-30rpx;
}
.indicator-card {
@apply bg-white px-20rpx py-16rpx rounded-16rpx shadow-[0_2rpx_10rpx_rgba(0,0,0,0.05)] border-l-[#e0e0e0] border-l-8rpx border-l-solid min-w-[280rpx] min-h-[120rpx] h-auto cursor-pointer transition-all duration-300 ease-in-out flex justify-center flex-col;
}
.indicator-card.active {
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
transform: translateY(-2rpx);
}
.indicator-name {
@apply text-26rpx color-[#666] mb-4rpx;
}
.indicator-value {
@apply text-34rpx font-bold mb-2rpx;
}
.indicator-unit {
@apply text-22rpx ml-4rpx;
}
.indicator-description {
@apply text-18rpx color-[#999] mt-4rpx;
}
.chart-container {
@apply bg-[#fff] rounded-16rpx p-20rpx shadow-[0_2rpx_10rpx_rgba(0,0,0,0.05)] mt-20rpx;
}
</style>
Loading…
Cancel
Save