feat:carousel-picture and 病虫害监测

This commit is contained in:
wangzenghua 2025-03-17 10:26:17 +01:00
parent 2def02238f
commit 523a745b25
7 changed files with 839 additions and 2 deletions

View File

@ -0,0 +1,233 @@
<template>
<div class="carousel" style="width: 500px">
<el-carousel
v-if="type === 'image'"
ref="carouselRef"
:interval="option.interval"
:arrow="option.arrow"
:indicator-position="option.indicatorPosition"
@change="handleChange"
>
<el-carousel-item v-for="(item, index) in data" :key="index">
<img :src="item.image" style="width: 100%; height: auto" />
</el-carousel-item>
</el-carousel>
<div v-if="type === 'video'" class="carousel-video">
<!-- <img :src="state.videoPicture" class="carousel-video-picture" /> -->
<video ref="videoRef" controls class="carousel-video-video" width="100%" height="100%" @ended="pauseVideo">
<source :src="state.videoUrl" type="video/mp4" />
Your browser does not support the video tag.
</video>
<!-- <span class="carousel-video-btn" @click="handlePlay">
<el-icon><VideoPlay /></el-icon>
</span> -->
</div>
<div class="carousel-container">
<span class="carousel-arrow carousel-arrow-left" @click="handleLeftClick">
<el-icon><ArrowLeftBold /></el-icon>
</span>
<el-scrollbar ref="scrollbarRef" class="carousel-list">
<div
v-for="(item, index) in data"
:key="index"
:class="`carousel-list-item ${state.current === index ? 'active' : ''}`"
@click="handleClick(index)"
>
<el-image style="width: 100px; height: 100px" :src="item.image ?? item" fit="cover" />
</div>
</el-scrollbar>
<span class="carousel-arrow carousel-arrow-right" @click="handleRightClick">
<el-icon><ArrowRightBold /></el-icon>
</span>
</div>
</div>
</template>
<script setup name="custom-carousel-picture">
import { reactive, ref } from 'vue';
const props = defineProps({
data: { type: Array, default: () => [] },
type: { type: String, default: 'image' },
option: {
type: Object,
default: () => {
return {
height: '',
initialIndex: 0,
autoplay: true,
loop: true,
interval: 3000,
arrow: 'never',
indicatorPosition: 'none',
};
},
},
});
const emit = defineEmits(['']);
const carouselRef = ref(null);
const scrollbarRef = ref(null);
const videoRef = ref(null);
const state = reactive({
current: 0,
isReady: false,
videoPicture: '',
videoUrl: '',
});
const handleChange = (cur, last) => {
state.current = cur;
};
const handleLeftClick = () => {
// const index = carouselRef.value.activeIndex;
// carouselRef.value.setActiveItem(index + 1);
scrollbarRef.value.setScrollLeft(scrollbarRef.value.wrapRef.scrollLeft - 120);
};
const handleRightClick = () => {
// const index = carouselRef.value.activeIndex;
// carouselRef.value.setActiveItem(index - 1);
scrollbarRef.value.setScrollLeft(scrollbarRef.value.wrapRef.scrollLeft + 120);
};
const playVideo = () => {
if (videoRef.value) {
videoRef.value.play();
}
};
const pauseVideo = () => {
if (videoRef.value) {
videoRef.value.pause();
}
};
const handleClick = (index) => {
const { type, data } = props;
switch (type) {
case 'image': {
carouselRef.value.setActiveItem(index);
break;
}
case 'video': {
const url = data[index];
state.videoUrl = url;
playVideo();
break;
}
}
};
const handlePlay = () => {
playVideo();
};
</script>
<style lang="scss" scoped>
.carousel {
width: 100%;
display: flex;
flex-direction: column;
&-container {
display: flex;
flex-direction: row;
align-items: center;
margin-top: 30px;
}
&-list {
flex: 3;
:deep(.el-scrollbar__bar) {
display: none !important;
}
:deep(.el-scrollbar__view) {
display: flex;
flex-direction: row;
align-items: center;
}
&-item {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
width: 100px;
height: 100px;
margin: 10px;
border: 2px solid #fff;
text-align: center;
border-radius: 4px;
background: var(--el-color-danger-light-9);
color: var(--el-color-danger);
cursor: pointer;
&.active {
border-color: var(--el-color-primary);
}
}
}
&-arrow {
width: 50px;
text-align: center;
.el-icon {
display: inline-block;
width: 40px;
height: 40px;
margin: 0 auto;
line-height: 40px;
font-size: 32px;
cursor: pointer;
}
}
&-video {
overflow: hidden;
position: relative;
width: 100%;
height: 100%;
min-height: 300px;
&-picture {
position: absolute;
z-index: 10;
}
&-video {
position: absolute;
z-index: 11;
}
&-btn {
position: absolute;
left: 50%;
top: 50%;
z-index: 12;
width: 50px;
height: 50px;
margin-left: -25px;
margin-top: -25px;
border-radius: 25px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
background: rgba(0, 0, 0, 0.2);
cursor: pointer;
.el-icon {
font-size: 32px;
color: #fff;
}
}
}
}
</style>

View File

@ -1,6 +1,7 @@
import SvgIcon from './svg-icon';
import CustomTableOperate from './custom-table-operate';
import CustomTableTree from './custom-table-tree';
import CustomCarouselPicture from './custom-carousel-picture';
import CustomImportExcel from './custom-import-excel';
import CustomRichEditor from './custom-rich-editor';
import CustomEchartBar from './custom-echart-bar';
@ -13,6 +14,7 @@ export {
SvgIcon,
CustomTableOperate,
CustomTableTree,
CustomCarouselPicture,
CustomImportExcel,
CustomRichEditor,
CustomEchartBar,

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@ -1,6 +1,409 @@
<template>
<div class="custom-page">病虫害监测</div>
<div class="custom-page">
<el-row :gutter="20">
<el-col :span="4">
<custom-table-tree title="土地分类" :data="landTypeData" @node-click="onNodeClick" />
</el-col>
<el-col :span="20">
<avue-crud
ref="crudRef"
v-model="state.form"
v-model:search="state.query"
v-model:page="state.pageData"
:table-loading="state.loading"
:data="state.data"
:option="state.options"
@refresh-change="refreshChange"
@search-reset="searchChange"
@search-change="searchChange"
@selection-change="selectionChange"
@current-change="currentChange"
@size-change="sizeChange"
@row-del="rowDel"
@row-save="rowSave"
@row-update="rowUpdate"
>
<template #menu="scope">
<custom-table-operate :actions="state.options.actions" :data="scope" />
</template>
</avue-crud>
</el-col>
</el-row>
</div>
<custom-info ref="customInfoRef" :row="state.currentRow" />
</template>
<script setup>
import { ref, computed, watch } from 'vue';
import { ref, reactive } from 'vue';
import { useRouter } from 'vue-router';
import { useApp } from '@/hooks';
import { useUserStore } from '@/store/modules/user';
import { CRUD_OPTIONS } from '@/config';
import { mockData, sleep } from '@/utils';
import CustomInfo from './info.vue';
const { VITE_APP_BASE_API, VITE_APP_NAME } = import.meta.env;
const app = useApp();
const UserStore = useUserStore();
const router = useRouter();
const crudRef = ref(null);
const landTypeData = ref([
{
label: '农用地',
id: '0',
children: [
{ label: '耕地', id: '01', children: [], pId: '0' },
{ label: '园地', children: [], id: '02', pId: '0' },
],
},
{
label: '建设用地',
id: '1',
children: [{ label: '城乡建设用地', children: [], id: '11', pId: '10' }],
},
{
label: '住宅用地',
id: '2',
children: [],
},
]);
const state = reactive({
loading: false,
query: {
current: 1,
size: 10,
},
form: {},
// selection: [],
options: {
...CRUD_OPTIONS,
addBtnText: '阀值设置',
addBtn: false,
selection: false,
column: [
{
label: '地块名称',
prop: 'p1',
search: true,
width: 200,
rules: {
required: true,
message: '请输入',
trigger: 'blur',
},
},
{
label: '地点',
prop: 'p2',
width: 200,
// type: 'cascader',
// hide: true,
// addDisplay: true,
// editDisplay: true,
// viewDisplay: false,
// props: {
// label: 'areaName',
// value: 'areaCode',
// children: 'areaChildVOS',
// },
// dicUrl: `${VITE_APP_BASE_API}/system/area/region?areaCode=530000`,
// dicHeaders: {
// authorization: UserStore.token,
// },
// dicFormatter: (res) => res.data ?? [],
rules: {
required: true,
message: '请选择',
trigger: 'blur',
},
},
{
label: '土壤环境报告',
children: [
{
label: '酸碱度(PH值)',
prop: 'p3',
width: 150,
},
{
label: '土壤质地',
prop: 'p4',
width: 150,
},
{
label: '养分含量',
prop: 'p5',
width: 150,
},
{
label: '重金属含量',
prop: 'p6',
width: 150,
overHidden: true,
},
],
},
{
label: '水环境报告',
children: [
{
label: '水温',
prop: 'p7',
},
{
label: '浑浊度',
prop: 'p8',
},
{
label: '酸碱度',
prop: 'p9',
},
{
label: '盐分含量',
prop: 'p10',
width: 150,
},
{
label: '重金属和有害物质',
prop: 'p11',
width: 150,
overHidden: true,
},
],
},
{
label: '大气环境报告',
children: [
{
label: '温度',
prop: 'p12',
},
{
label: '湿度',
prop: 'p13',
},
{
label: '风速',
prop: 'p14',
},
{
label: '风向',
prop: 'p15',
},
{
label: '光照强度',
prop: 'p16',
width: 150,
},
{
label: '降雨量',
prop: 'p17',
},
{
label: '有害气体',
prop: 'p18',
width: 150,
},
],
},
{
label: '创建时间',
prop: 'createTime',
width: 200,
search: true,
display: false,
},
{
label: '更新时间',
prop: 'updateTime',
width: 200,
display: false,
},
],
actions: [
{
name: '查看',
icon: 'view',
event: ({ row }) => rowView(row),
},
{
name: '编辑',
icon: 'edit',
event: ({ row }) => rowEdit(row),
},
// {
// type: 'success',
// name: ({ row }) => {
// return row.status === 1 ? '' : '';
// },
// icon: ({ row }) => {
// return row.status === 1 ? 'turnOff' : 'open';
// },
// event: ({ row }) => rowStatus(row),
// },
{
type: 'danger',
name: '删除',
icon: 'delete',
event: ({ row }) => rowDel(row),
},
],
},
pageData: {
total: 0,
currentPage: 1,
pageSize: 10,
},
data: [],
currentRow: {},
});
const loadData = async () => {
//state.loading = true;
// GetEntityList(state.query)
// .then((res) => {
// if (res.code === 200) {
// const { current, size, total, records } = res.data;
// state.data = records;
// state.pageData = {
// currentPage: current || 1,
// pageSize: size || 10,
// total: total,
// };
// }
// })
// .catch((err) => {
// app.$message.error(err.msg);
// state.data = [];
// })
// .finally(() => {
// state.loading = false;
// });
state.loading = true;
await sleep(500);
state.data = mockData(
{
p1: '202501号地',
p2: '耿马县/耿马镇',
p3: '酸性',
p4: '正常',
p5: '正常',
p7: '正常',
p6: '正常',
p8: '正常',
p9: '正常',
p10: '正常',
p11: '铜含量接近阀值',
p12: '正常',
p13: '正常',
p14: '正常',
p15: '东南风',
p16: '正常',
p17: '偏低',
p18: '正常',
// status: 1,
createTime: '2025-01-01',
updateTime: '2025-01-15',
},
10
);
state.loading = false;
};
loadData();
const onNodeClick = (data) => {
console.log('onNodeClick', data);
};
//
const currentChange = (current) => {
state.query.current = current;
loadData();
};
//
const sizeChange = (size) => {
state.query.size = size;
loadData();
};
//
const searchChange = (params, done) => {
if (done) done();
state.query = params;
state.query.current = 1;
loadData();
};
//
const refreshChange = () => {
loadData();
app.$message.success('刷新成功');
};
//
const selectionChange = (rows) => {
state.selection = rows;
};
//
const customInfoRef = ref(null);
const rowView = (row) => {
state.currentRow = row;
customInfoRef.value && customInfoRef.value.show();
};
//
const rowStatus = (row) => {
console.info('操作状态');
};
//
const onAdd = () => {
crudRef.value && crudRef.value.rowAdd();
};
const rowSave = (row, done, loading) => {
// AddEntity(row)
// .then((res) => {
// if (res.code === 200) {
// app.$message.success('');
// done();
// loadData();
// }
// })
// .catch((err) => {
// app.$message.error(err.msg);
// })
// .finally(() => {
// loading();
// });
};
//
const rowEdit = (row) => {
crudRef.value && crudRef.value.rowEdit(row);
};
const rowUpdate = (row, index, done, loading) => {
// UpdateEntity(row)
// .then((res) => {
// if (res.code === 200) {
// app.$message.success('');
// done();
// loadData();
// }
// })
// .catch((err) => {
// app.$message.error(err.msg);
// })
// .finally(() => {
// loading();
// });
};
//
const rowDel = (row) => {};
</script>

View File

@ -0,0 +1,199 @@
<template>
<el-dialog
v-model="state.visible"
draggable
title="查看"
width="70%"
:close-on-click-modal="false"
:close-on-press-escape="false"
@close="state.visible = false"
>
<div class="monitor">
<div class="monitor-tabs">
<el-radio-group v-model="state.currentTab">
<el-radio-button :value="1">虫情监测</el-radio-button>
<el-radio-button :value="2">病情监测</el-radio-button>
</el-radio-group>
</div>
<div v-if="state.currentTab === 1" class="monitor-panel">
<el-row :gutter="16">
<el-col :span="12">
<div class="monitor-panel-item">
<h3>虫情数据</h3>
<custom-echart-line :chart-data="state.pestData" height="500px" :option="state.pestOption" />
</div>
</el-col>
<el-col :span="12">
<div class="monitor-panel-item">
<h3 style="margin-bottom: 20px">数据采集</h3>
<custom-carousel-picture :data="state.pestList" />
</div>
</el-col>
</el-row>
</div>
<div v-if="state.currentTab === 2" class="monitor-panel">
<el-row :gutter="16">
<el-col :span="12">
<div class="monitor-panel-item">
<h3>病情数据</h3>
<custom-echart-bar :chart-data="state.pestData2" height="500px" :option="state.pestOption2" />
</div>
</el-col>
<el-col :span="12">
<div class="monitor-panel-item">
<h3 style="margin-bottom: 20px">数据采集</h3>
<custom-carousel-picture :data="state.pestList" type="video" />
</div>
</el-col>
</el-row>
</div>
</div>
</el-dialog>
</template>
<script setup>
import { reactive } from 'vue';
import { getAssetsFile } from '@/utils';
const props = defineProps({
row: {
type: Object,
default: () => {},
},
});
const state = reactive({
visible: false,
currentTab: 1,
pestOption: {
// color: ['#3685fe', '#41b879', '#fed500'],
title: {
text: '',
textStyle: {
color: '#333',
},
},
xAxis: {
type: 'category',
name: '月份',
},
yAxis: {
type: 'value',
name: '数量',
},
},
pestData: [
{
type: '蝗虫',
value: 40,
name: '1月',
},
{
type: '毛虫',
value: 146,
name: '1月',
},
{
type: '其他昆虫',
value: 81,
name: '1月',
},
{
type: '蝗虫',
value: 60,
name: '2月',
},
{
type: '毛虫',
value: 186,
name: '2月',
},
{
type: '其他昆虫',
value: 101,
name: '2月',
},
{
type: '蝗虫',
value: 230,
name: '3月',
},
{
type: '毛虫',
value: 256,
name: '3月',
},
{
type: '其他昆虫',
value: 301,
name: '3月',
},
],
pestList: [
{
image: getAssetsFile('images/plantingAndBreeding/pic1.png'),
video: '',
},
{
image: getAssetsFile('images/plantingAndBreeding/pic2.png'),
video: '',
},
{
image: getAssetsFile('images/plantingAndBreeding/pic3.png'),
video: '',
},
],
pestOption2: {
// color: ['#3685fe', '#41b879', '#ffd500'],
title: {
text: '',
textStyle: {
color: '#333',
},
},
label: {
color: '#333',
},
},
pestData2: [
{ value: 80, type: '黑星病叶片', name: '1月' },
{ value: 105, type: '火疫病叶片', name: '1月' },
{ value: 187, type: '黑星病叶片', name: '2月' },
{ value: 100, type: '火疫病叶片', name: '2月' },
{ value: 125, type: '黑星病叶片', name: '3月' },
{ value: 217, type: '火疫病叶片', name: '3月' },
],
});
defineExpose({
show: () => {
state.visible = true;
},
hide: () => {
state.visible = false;
},
});
</script>
<style lang="scss" scoped>
.monitor {
padding: 20px;
margin: 0 auto;
&-tabs {
margin-bottom: 20px;
text-align: center;
}
&-panel {
&-item {
margin-bottom: 20px;
h3 {
margin-bottom: 16px;
font-size: 14px;
color: var(--el-text-color-primary);
}
}
}
}
</style>