新增功能

master
chris 16 hours ago
parent 9c4d853f38
commit c7a70e04ac

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.4 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="1757476762111" class="icon" viewBox="0 0 1026 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4628" xmlns:xlink="http://www.w3.org/1999/xlink" width="128.25" height="128"><path d="M514.87164688 506.25670727m-502.53814919 0a502.53814918 502.53814918 0 1 0 1005.07629734 0 502.53814918 502.53814918 0 1 0-1005.07629734 0Z" fill="#3674AD" p-id="4629"></path><path d="M397.13413721 572.30457828c-25.84481881 20.1015261-43.07469803 48.81799179-43.07469803 83.2777502 0 43.07469803 25.84481881 80.40610333 66.047871 97.63598255 14.35823233 5.74329273 31.58811258 11.48658648 48.81799179 11.48658648l-71.79116476-192.40031923z" fill="#FFFFFF" p-id="4630"></path><path d="M503.3850604 891.05734679c-134.96738888 0-244.08995791-109.12256903-244.08995791-244.08995791 0-126.35244927 134.96738888-321.62441539 218.24513908-436.49027819l25.84481883-37.33140529 22.97317193 34.45975841c34.45975843 48.81799179 86.14939709 126.35244927 129.22409615 201.01525989 8.6149396 14.35823233 2.87164688 31.58811258-11.48658648 40.20305218-14.35823233 8.6149396-31.58811258 2.87164688-40.20305218-11.48658649-34.45975843-57.43293139-71.79116372-114.86586279-103.3792763-163.68385457-74.66281061 106.25092318-186.65702652 275.67807048-186.65702652 376.18569991 0 103.3792763 83.27775022 186.65702652 186.65702652 186.65702754 40.20305218 0 80.40610333-11.48658648 109.12257005-34.45975945 11.48658648-8.6149396 31.58811258-5.74329273 40.20305116 5.74329376 8.6149396 11.48658648 5.74329273 31.58811258-5.74329273 40.20305115-37.33140531 25.84481881-89.02104397 43.07469803-140.7106816 43.07469906z" fill="#FFFFFF" p-id="4631"></path><path d="M543.58811258 583.79116372c0-17.22987921 5.74329273-31.58811258 14.35823233-43.07469802s22.97317297-17.22987921 40.20305218-17.22987922 28.7164657 5.74329273 37.33140531 14.35823233 14.35823233 22.97317297 14.35823233 40.20305219-5.74329273 31.58811258-14.35823233 43.07469803-22.97317297 17.22987921-40.20305218 17.22988024c-14.35823233 0-28.7164657-5.74329273-37.33140531-14.35823336s-14.35823233-25.84481881-14.35823233-40.20305219z m28.7164657-2.87164584c0 20.1015261 8.6149396 28.7164657 22.97317194 28.71646569s22.97317297-11.48658648 22.97317296-31.58811257-8.6149396-31.58811258-22.97317296-31.58811257c-14.35823233 2.87164688-22.97317297 14.35823233-22.97317194 34.45975945z m178.0420869-54.56128555l-140.71068161 218.2451391h-34.45975945l140.71068161-218.2451391h34.45975945z m-71.79116476 166.55550146c0-17.22987921 5.74329273-31.58811258 14.35823337-43.07469906s22.97317297-17.22987921 40.20305218-17.22987922 28.7164657 5.74329273 37.33140531 14.35823337 14.35823233 22.97317297 14.35823233 40.20305115-5.74329273 31.58811258-14.35823233 43.07469906-22.97317297 17.22987921-40.20305219 17.22987921c-14.35823233 0-28.7164657-5.74329273-37.3314053-14.35823233-11.48658648-11.48658648-14.35823233-22.97317297-14.35823337-40.20305218z m28.7164657 0c0 20.1015261 8.6149396 31.58811258 22.97317297 31.58811154s22.97317297-11.48658648 22.97317194-31.58811154c0-8.6149396-2.87164688-17.22987921-5.74329273-22.97317297-2.87164688-5.74329273-8.6149396-8.6149396-17.22987921-8.61493961-14.35823233 0-22.97317297 8.6149396-22.97317297 31.58811258z" fill="#FFFFFF" p-id="4632"></path></svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.9 KiB

@ -97,7 +97,7 @@ export const pestDeviceParamsDict = {
},
};
import worm from "@/assets/images/devices/worm.png";
import worm from "@/assets/images/devices/pest.png";
// import greenhouse from "@/assets/images/devices/greenhouse.jpg";
import weather from "@/assets/images/devices/weather.png";
import soil from "@/assets/images/devices/soil.png";

@ -79,6 +79,11 @@ export const constantRoutes = [
component: () => import("@/views/orchard"),
name: "Orchard",
},
{
path: "/pest",
component: () => import("@/views/pest"),
name: "Pest",
},
{
path: "/pest/imgAnalysis",
component: () => import("@/views/pest/imgAnalysis"),
@ -89,6 +94,21 @@ export const constantRoutes = [
component: () => import("@/views/pest/pestStatistics"),
name: "PestStatistics",
},
{
path: "/pest/trendAnalysis",
component: () => import("@/views/pest/trendAnalysis"),
name: "TrendAnalysis",
},
{
path: "/weather/monitor",
component: () => import("@/views/weather/monitor"),
name: "WeatherMonitor",
},
{
path: "/weather/history",
component: () => import("@/views/weather/history"),
name: "WeatherHistory",
},
// {
// path: "/orchard-screen",
// component: () => import("@/views/orchardScreen"),

@ -2,7 +2,7 @@
* @Author: chris
* @Date: 2025-08-08 16:17:54
* @LastEditors: chris
* @LastEditTime: 2025-09-05 11:15:56
* @LastEditTime: 2025-09-09 09:50:07
-->
<template>
<el-table
@ -19,14 +19,14 @@
<el-table-column label="设备名称" align="center" prop="deviceName" v-if="columns[1].visible" :show-overflow-tooltip="true" />
<el-table-column label="型号" align="center" prop="model" v-if="columns[2].visible" />
<el-table-column label="ip地址" align="center" prop="address" v-if="columns[3].visible" />
<el-table-column label="状态" align="center" v-if="columns[6].visible">
<el-table-column label="状态" align="center" v-if="columns[4].visible">
<template #default="scope">
<el-tag :type="statusColorMap[scope.row.status]">
{{ scope.row.status === '0' ? '正常' : '停用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" v-if="columns[7].visible">
<el-table-column label="创建时间" align="center" prop="createTime" v-if="columns[5].visible">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>

@ -2,7 +2,7 @@
* @Author: chris
* @Date: 2025-09-05 10:12:41
* @LastEditors: chris
* @LastEditTime: 2025-09-05 10:36:37
* @LastEditTime: 2025-09-09 09:50:16
*/
// 列配置
export const columnsConfig = [
@ -10,7 +10,8 @@ export const columnsConfig = [
{ key: 1, label: "设备名称", visible: true },
{ key: 2, label: "型号", visible: true },
{ key: 3, label: "ip地址", visible: true },
{ key: 6, label: "状态", visible: true },
{ key: 4, label: "状态", visible: true },
{ key: 5, label: "创建时间", visible: true },
];
// 状态颜色映射

@ -1,11 +1,24 @@
<script name="DeviceList">
<!--
* @Author: chris
* @Date: 2025-09-05 09:39:28
* @LastEditors: chris
* @LastEditTime: 2025-09-08 09:37:55
-->
<script setup name="DeviceList">
import DeviceFlatList from '@/components/deviceFlatList'
</script>
<template>
<el-card class="device-tree-card" title="设备列表">
设备列表
<el-card class="device-tree-card">
<template #header>
<div class="list-header">
<span>设备列表</span>
</div>
</template>
<device-flat-list />
</el-card>
</template>
<style lang="scss" scoped>
</style>

@ -173,7 +173,7 @@ function handleDelete(item) {
}
.info-item {
@apply flex justify-between;
@apply flex justify-between items-center;
}
.label {

@ -2,7 +2,7 @@
* @Author: chris
* @Date: 2025-08-12 11:20:07
* @LastEditors: chris
* @LastEditTime: 2025-08-14 17:15:18
* @LastEditTime: 2025-09-05 15:07:36
-->
<template>
<div :class="['img-analysis-page', 'page-height', { 'hasTagsView': hasTags }]">
@ -15,11 +15,11 @@
<!-- <action-module @handleQuery="handleQuery" @handleDownload="handleDownload"
@handleDelete="handleDelete" /> -->
<el-row :gutter="20">
<el-col :span="15">
<el-col :span="16">
<image-list :imageList="imageList" v-model="selectedRows" @handleDelete="handleDelete" @handleAnalyze="handleAnalyze" @handleReport="handleReport" />
</el-col>
<el-col :span="1" class="line"></el-col>
<el-col :span="6">
<el-col :span="7">
<image-detail :info="checkItem" />
</el-col>
</el-row>

@ -1,7 +1,7 @@
<template>
<div class="app-container">
<!-- 搜索表单 -->
<search-form
<pest-search-form
v-show="showSearch"
:query-params="queryParams"
v-model:dateRange="dateRange"
@ -54,11 +54,11 @@
import { listPest, delPest } from '@/api/pest'
import { columnsConfig } from './config.js'
import Pagination from '@/components/Pagination'
import SearchForm from './components/SearchForm'
import PestSearchForm from './components/PestSearchForm'
import ActionButtons from './components/ActionButtons'
import pestTable from './components/pestTable'
import pestFormDialog from './components/pestFormDialog'
import pestViewDialog from './components/pestViewDialog'
import PestTable from './components/PestTable'
import PestFormDialog from './components/PestFormDialog'
import PestViewDialog from './components/PestViewDialog'
//
const { proxy } = getCurrentInstance()

@ -0,0 +1,39 @@
<!--
* @Author: chris
* @Date: 2025-08-18 09:24:37
* @LastEditors: chris
* @LastEditTime: 2025-08-18 14:41:04
-->
<script setup name="DeviceList">
const props = defineProps({
devices: {
type: Array,
default: () => []
}
})
const emits = defineEmits(['selected'])
const checkList = ref([]);
watch(() => checkList, (val) => {
emits('selected', val);
})
</script>
<template>
<div class="device-list">
<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-group>
</el-card>
</div>
</template>
<style lang="scss" scoped>
:deep(.el-checkbox-group) {
@apply flex flex-col
}
</style>

@ -0,0 +1,67 @@
<!--
* @Author: chris
* @Date: 2025-08-18 09:27:23
* @LastEditors: chris
* @LastEditTime: 2025-08-18 17:18:54
-->
<script setup>
const props = defineProps({
params: {
type: Object,
default: () => ({
deviceId: null,
dateRange: []
})
}
})
const emits = defineEmits(['update:params'])
const queryRef = ref()
const queryParams = reactive({...props.params});
const pestOpts = ref([])
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="创建时间">
<el-date-picker
style="width: 240px"
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,125 @@
<!--
* @Author: chris
* @Date: 2025-08-18 09:36:11
* @LastEditors: chris
* @LastEditTime: 2025-09-08 15:40:07
-->
<script setup>
import { LineChart } from 'echarts/charts';
// Component
import {
TitleComponent,
TooltipComponent,
GridComponent,
DatasetComponent,
TransformComponent
} from 'echarts/components';
//
import { LabelLayout, UniversalTransition } from 'echarts/features';
// Canvas CanvasRenderer SVGRenderer
import { CanvasRenderer } from 'echarts/renderers';
const props = defineProps({
chartData: {
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]}]
})
}
});
const { proxy } = getCurrentInstance()
const lineChartRef = ref(null)
let lineChart = null
registerComponents()
onMounted(() => {
initLineChart()
})
onUnmounted(() => {
if (lineChart) {
lineChart.dispose();
lineChart = null;
}
});
watch(() => props.chartData, (newData) => {
if (lineChart) {
lineChart.setOption(createOption(newData));
}
}, { deep: true });
function initLineChart() {
if (lineChartRef.value) {
lineChart = proxy.$echarts.init(lineChartRef.value);
lineChart.setOption(createOption(props.chartData));
}
}
function createSeries () {
return props.chartData.series.map((item) => {
return {
name: item.name,
type: 'line',
stack: 'Total',
data: item.data
}
})
}
function createOption (data) {
const series = createSeries();
const option = {
title: {
text: '虫害趋势'
},
tooltip: {
trigger: 'axis'
},
legend: {
data: data.legend
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: data.xAxis
},
yAxis: {
type: 'value'
},
series: series
};
return option;
}
function registerComponents() {
//
proxy.$echarts.use([TitleComponent, TooltipComponent, GridComponent, DatasetComponent, TransformComponent, LineChart, LabelLayout, UniversalTransition, CanvasRenderer]);
}
function calcBoxSize () {}
</script>
<template>
<div class="trend-chart">
<div class="line-chart" ref="lineChartRef"></div>
</div>
</template>
<style lang="scss" scoped>
.trend-chart, .line-chart {
@apply h-100% w-100%;
}
</style>

@ -0,0 +1,47 @@
<!--
* @Author: chris
* @Date: 2025-08-20 17:06:41
* @LastEditors: chris
* @LastEditTime: 2025-08-21 10:47:52
-->
<script setup>
const props = defineProps({
params: {
type: Object,
default: () => ({
pestTypes: []
})
},
options: {
type: Array,
default: () => []
}
})
const emits = defineEmits(['update:params'])
const queryParams = ref({...props.params});
watch(() => queryParams.value, (newVal) => {
emits('update:params', 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-option
v-for="item in options"
multiple
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-col>
</el-row>
</template>
<style lang="scss" scoped>
</style>

@ -0,0 +1,63 @@
<!--
* @Author: chris
* @Date: 2025-08-18 09:24:26
* @LastEditors: chris
* @LastEditTime: 2025-09-08 11:35:38
-->
<script setup name="PestStatistics">
import { useSettings } from "@/hooks/useSettings";
import DeviceList from './components/DeviceList.vue';
import SearchForm from './components/SearchForm.vue';
import TrendChart from './components/TrendChart.vue';
import ChartAction from "./components/chartAction.vue";
import { devices } from './mockData'
const { hasTags } = useSettings()
const deviceList = ref(devices);
const tableData = ref([]);
const queryParams = ref({});
function getList () {
}
function handleSelect(items) {
console.log(items);
}
function exportView() {
console.log('导出视图')
}
function resetQuery() {
console.log('重置查询')
}
</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>
</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"/>
</el-col>
</el-row>
</div>
</template>
<style lang="scss" scoped>
.pest-statistics-page {
@apply p-20px
}
.action {
@apply mt-20px;
}
.chart {
@apply h-88% w-100% mt-10px;
}
</style>

@ -0,0 +1,10 @@
export const devices = [
{
id: "1",
name: "设备1",
},
{
id: "2",
name: "设备2",
},
];

@ -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-09 16:54:23
-->
<script setup>
const props = defineProps({
params: {
type: Object,
default: () => ({
nodeId: null,
dateRange: []
})
},
weatherOptions: {
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 weatherOptions" :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-11 17:21:28
-->
<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="['weather-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>
.weather-history-page {
@apply p-20px;
}
</style>

@ -0,0 +1,227 @@
<!--
* @Author: chris
* @Date: 2025-09-09 15:30:24
* @LastEditors: chris
* @LastEditTime: 2025-09-11 17:27:00
-->
<script setup>
import weatherImg from '@/assets/images/devices/weather.png'
const testDataList = [
{
id: 1,
title: '温度',
type: 'temperature',
value: '25',
icon: 'temperature2',
color: '#FF6B6B',
unit: '°C',
},
{
id: 2,
title: '湿度',
type: 'humidity',
value: '60',
icon: 'humidity',
color: '#4ECDC4',
unit: '%',
},
{
id: 3,
title: '光照',
type: 'light',
value: '10000',
icon: 'light',
color: '#FFD166',
unit: 'lux',
},
{
id: 4,
title: '降雨量',
type: 'rainfall',
value: '10',
icon: 'rain',
color: '#6A0572',
unit: 'mm',
},
{
id: 5,
title: '风向',
type: 'windDirection',
value: '东南风',
icon: 'wind',
color: '#1A535C',
unit: '°',
},
{
id: 6,
title: '风速',
type: 'windSpeed',
value: '2',
icon: 'windPower',
color: '#77DD77',
unit: 'm/s',
},
{
id: 7,
title: '气压',
type: 'pressure',
value: '1013',
icon: 'hpa',
color: '#845EC2',
unit: 'hPa',
}
]
const emits = defineEmits([ 'select' ])
function handleSelect(item) {
console.log('Selected item:', item);
emits('select', item)
}
</script>
<template>
<div class="weather-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="weather-photo">
<el-image
:src="weatherImg"
fit="contain"
class="weather-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;
.weather-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;
}
//
.weather-photo {
@apply w-full h-480px rounded-8px overflow-hidden;
box-shadow: $shadow-normal;
transition: $transition-base;
}
.weather-photo:hover {
@apply translate-y-[-2px] shadow-[0_4px_16px_rgba(0,0,0,0.1)];
}
.weather-photo-img {
@apply h-full w-full transition-[transform] duration-500 ease;
}
.weather-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 {
transform: translateY(-3px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08);
border-color: 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,460 @@
<!--
* @Author: chris
* @Date: 2025-09-12 17:20:00
* @LastEditors: chris
* @LastEditTime: 2025-09-11 17:35:22
* @Description: 气象曲线图组件支持7天/30/90天数据切换
-->
<script setup>
import { ref, watch, onMounted, nextTick } from 'vue'
import * as echarts from 'echarts'
// props
const props = defineProps({
//
weatherType: {
type: String,
required: true,
validator: (value) => {
//
const validTypes = ['temperature', 'humidity', 'rainfall', 'windSpeed', 'pressure', 'light']
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 weatherTypeConfig = {
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'
}
}
}
},
rainfall: {
name: '降雨量',
unit: 'mm',
color: '#6A0572',
yAxis: {
name: '降雨量 (mm)',
min: 0,
show: true,
axisLabel: {
show: true,
formatter: '{value}'
},
axisTick: {
show: true
},
axisLine: {
show: true
},
splitLine: {
show: true,
lineStyle: {
type: 'dashed'
}
}
}
},
windSpeed: {
name: '风速',
unit: 'm/s',
color: '#77DD77',
yAxis: {
name: '风速 (m/s)',
min: 0,
show: true,
axisLabel: {
show: true,
formatter: '{value}'
},
axisTick: {
show: true
},
axisLine: {
show: true
},
splitLine: {
show: true,
lineStyle: {
type: 'dashed'
}
}
}
},
pressure: {
name: '气压',
unit: 'hPa',
color: '#845EC2',
yAxis: {
name: '气压 (hPa)',
min: function(value) { return value.min - 5; },
max: function(value) { return value.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'
}
}
}
},
light: {
name: '光照',
unit: 'lux',
color: '#FFD166',
yAxis: {
name: '光照 (lux)',
min: 0,
show: true, // Y
axisLabel: {
show: true, //
formatter: '{value}'
},
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 = weatherTypeConfig[props.weatherType]
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="weather-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
.weather-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-11 17:11:53
-->
<script setup>
import LiveData from './components/LiveData.vue'
import WeatherChart from './components/WeatherChart.vue';
import { useSettings } from '@/hooks/useSettings';
import { getFormattedWeatherData } from './mock';
const { hasTags } = useSettings()
const liveData = ref({})
const chartData = ref({ dates: [], values: []})
const weatherType = ref('temperature')
const selectedTimeRange = ref(7)
const chartLoading = ref(false)
getChartData()
function getChartData() {
chartLoading.value = true
setTimeout(() => {
console.log(getFormattedWeatherData(weatherType.value, selectedTimeRange.value))
chartData.value = getFormattedWeatherData(weatherType.value, selectedTimeRange.value)
chartLoading.value = false
}, 300)
return chartData.value
}
function handleSelectionChange(selection) {
console.log(selection)
}
function handleSelectWeatherItem(item) {
weatherType.value = item.type
getChartData()
}
function handleTimeRangeChange(range) {
selectedTimeRange.value = range
getChartData()
}
</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"/>
</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" />
<!-- <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>
.weather-monitor-page {
@apply p-20px;
> .el-row {
@apply h-full;
}
}
</style>

@ -0,0 +1,240 @@
// 气象数据模拟工具
/**
* 生成日期数组
* @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 generateWeatherData = (days) => {
const dates = generateDates(days);
const count = dates.length;
return {
// 温度数据 (10-35°C)
temperature: {
7: {
dates: generateDates(7),
values: generateValues(7, 15, 30, 22, 0.15),
},
30: {
dates: generateDates(30),
values: generateValues(30, 10, 35, 23, 0.15),
},
90: {
dates: generateDates(90),
values: generateValues(90, 5, 38, 22, 0.2),
},
},
// 湿度数据 (30-95%)
humidity: {
7: {
dates: generateDates(7),
values: generateValues(7, 40, 90, 65, 0.2),
},
30: {
dates: generateDates(30),
values: generateValues(30, 30, 95, 60, 0.25),
},
90: {
dates: generateDates(90),
values: generateValues(90, 25, 98, 62, 0.25),
},
},
// 降雨量数据 (0-50mm)
rainfall: {
7: { dates: generateDates(7), values: generateRainfallValues(7) },
30: { dates: generateDates(30), values: generateRainfallValues(30) },
90: { dates: generateDates(90), values: generateRainfallValues(90) },
},
// 风速数据 (0-15m/s)
windSpeed: {
7: { dates: generateDates(7), values: generateValues(7, 0, 10, 3, 0.3) },
30: {
dates: generateDates(30),
values: generateValues(30, 0, 15, 3.5, 0.3),
},
90: {
dates: generateDates(90),
values: generateValues(90, 0, 20, 4, 0.35),
},
},
// 气压数据 (980-1040hPa)
pressure: {
7: {
dates: generateDates(7),
values: generateValues(7, 990, 1030, 1013, 0.05),
},
30: {
dates: generateDates(30),
values: generateValues(30, 985, 1035, 1015, 0.05),
},
90: {
dates: generateDates(90),
values: generateValues(90, 980, 1040, 1012, 0.07),
},
},
// 光照数据 (0-100000lux)
light: {
7: { dates: generateDates(7), values: generateLightValues(7) },
30: { dates: generateDates(30), values: generateLightValues(30) },
90: { dates: generateDates(90), values: generateLightValues(90) },
},
};
};
/**
* 生成降雨量数据特殊处理大部分为0
* @param {number} count - 数据点数量
* @returns {Array} 降雨量数组
*/
const generateRainfallValues = (count) => {
const values = [];
for (let i = 0; i < count; i++) {
// 80%的概率无雨
if (Math.random() < 0.8) {
values.push(0);
} else {
// 小雨到大雨
const rain =
Math.random() < 0.7
? parseFloat((Math.random() * 10).toFixed(1)) // 小雨 (0-10mm)
: parseFloat((10 + Math.random() * 40).toFixed(1)); // 大雨 (10-50mm)
values.push(rain);
}
}
return values;
};
/**
* 生成光照数据模拟昼夜变化
* @param {number} count - 数据点数量
* @returns {Array} 光照数组
*/
const generateLightValues = (count) => {
const values = [];
for (let i = 0; i < count; i++) {
// 模拟白天光照强,晚上光照弱
const hour = Math.floor(Math.random() * 24);
let light;
if (hour >= 6 && hour <= 18) {
// 白天 5000-100000lux
light = 5000 + Math.random() * 95000;
} else if ((hour >= 5 && hour < 6) || (hour > 18 && hour <= 19)) {
// 晨昏 100-5000lux
light = 100 + Math.random() * 4900;
} else {
// 夜晚 0-100lux
light = Math.random() * 100;
}
// 添加天气影响(阴天光照减弱)
if (Math.random() < 0.3) {
light *= 0.3 + Math.random() * 0.5;
}
values.push(parseFloat(light.toFixed(0)));
}
return values;
};
/**
* 生成实时气象数据
* @returns {Object} 实时气象数据
*/
const generateRealtimeWeatherData = () => {
return {
temperature: parseFloat((15 + Math.random() * 20).toFixed(1)),
humidity: parseFloat((30 + Math.random() * 65).toFixed(1)),
rainfall: parseFloat((Math.random() * 10).toFixed(1)),
windSpeed: parseFloat((Math.random() * 15).toFixed(1)),
pressure: parseFloat((990 + Math.random() * 40).toFixed(1)),
light: parseFloat((Math.random() * 100000).toFixed(0)),
updateTime: new Date().toLocaleTimeString("zh-CN"),
};
};
// 导出模拟数据
export const weatherMockData = generateWeatherData();
export const realtimeWeatherData = generateRealtimeWeatherData();
/**
* 获取指定气象类型和时间范围的格式化数据用于WeatherChart组件
* @param {string} weatherType - 气象类型
* @param {number} days - 时间范围7/30/90
* @returns {Object} 格式化的数据 { dates: [], values: [] }
*/
const getFormattedWeatherData = (weatherType, days) => {
// 验证参数
if (!weatherMockData[weatherType] || !weatherMockData[weatherType][days]) {
console.error(`无效的气象类型或时间范围: ${weatherType}, ${days}`);
return { dates: [], values: [] };
}
return weatherMockData[weatherType][days];
};
// 导出工具函数供其他组件使用
export {
generateDates,
generateValues,
generateWeatherData,
generateRealtimeWeatherData,
getFormattedWeatherData,
};
Loading…
Cancel
Save