新增意见反馈列表查询;气象视频新增控制模块;将果园大屏无人机模块替换为果园地图;完善部分导出功能;调整部分界面表单文字

master
chris 4 weeks ago
parent b00aa151a2
commit 21f18080c5

@ -0,0 +1,7 @@
<!--
* @Author: chris
* @Date: 2026-01-19 09:23:38
* @LastEditors: chris
* @LastEditTime: 2026-01-19 09:25:56
-->
-这个一个基于若依vue3框架开发采用vue+vite+element-plus开发的果园管理系统。

@ -2,7 +2,7 @@
* @Author: chris
* @Date: 2025-09-05 11:34:53
* @LastEditors: chris
* @LastEditTime: 2025-09-18 14:45:50
* @LastEditTime: 2026-01-19 10:13:37
*/
import request from "@/utils/request";
@ -67,3 +67,23 @@ export function exportDeviceData(query) {
responseType: "blob",
});
}
// 导出虫情分析数据
export function exportPestAnalysis(params) {
return request({
url: "/business/device-data/insect/analysis/export",
method: "post",
params,
responseType: "blob",
});
}
// 导出虫情趋势数据
export function exportPestTrend(params) {
return request({
url: "/business/device-data/insect/trend/export",
method: "post",
params,
responseType: "blob",
});
}

@ -0,0 +1,16 @@
/*
* @Author: chris
* @Date: 2026-01-16 15:11:26
* @LastEditors: chris
* @LastEditTime: 2026-01-16 15:13:04
*/
import request from "@/utils/request";
// 获取反馈信息列表(分页)
export function listFeedBack(query) {
return request({
url: "/business/feedback/list",
method: "get",
params: query,
});
}

@ -2,7 +2,7 @@
* @Author: chris
* @Date: 2025-09-05 11:34:53
* @LastEditors: chris
* @LastEditTime: 2025-09-17 11:53:48
* @LastEditTime: 2026-01-19 10:13:52
*/
import request from "@/utils/request";
@ -49,7 +49,7 @@ export function delPest(id) {
});
}
// 导出果园数据
// 导出虫情设数据
export function exportPest(query) {
return request({
url: "/leilinglitchi/business/device/export",

@ -2,14 +2,14 @@
* @Author: chris
* @Date: 2025-09-05 11:34:53
* @LastEditors: chris
* @LastEditTime: 2025-10-27 17:05:10
* @LastEditTime: 2025-12-31 10:40:10
*/
import request from "@/utils/request";
// 查询推送历史列表
export function listNotify(query) {
return request({
url: "/leilinglitchi/business/device/list",
url: "/leilinglitchi/business/notify-log/list",
method: "get",
params: Object.assign({ type: 1 }, query),
});
@ -18,7 +18,7 @@ export function listNotify(query) {
// 查询推送历史详细
export function getNotify(id) {
return request({
url: "/leilinglitchi/business/device/" + id,
url: "/leilinglitchi/business/notify-log/" + id,
method: "get",
});
}

@ -57,6 +57,11 @@ export const constantRoutes = [
component: () => import("@/views/error/401"),
hidden: true,
},
{
path: "/orchard-screen",
component: () => import("@/views/orchardScreen"),
name: "OrchardScreen",
},
{
path: "",
component: Layout,
@ -119,11 +124,6 @@ export const constantRoutes = [
// component: () => import("@/views/soil/history"),
// name: "SoilHistory",
// },
// // {
// // path: "/orchard-screen",
// // component: () => import("@/views/orchardScreen"),
// // name: "OrchardScreen",
// // },
// TODO 测试结束, 后续删除
],
},

@ -34,6 +34,11 @@
<el-input v-model="form.installPointDes" placeholder="请输入设备点位" maxlength="100" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="卡号" prop="cardNumber">
<el-input v-model="form.cardNumber" placeholder="请输入卡号" maxlength="100" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="状态">
<el-radio-group v-model="form.status">
@ -97,6 +102,7 @@ watch(() => form.value.imgUrl, (newVal) => {
//
const rules = {
cardNumber: [{ required: true, message: '请输入卡号', trigger: 'blur' }],
imgUrl: [{ required: true, message: '请上传设备图片', trigger: 'blur' }],
type: [{ required: true, message: '请选择设备类型', trigger: 'change' }],
deviceNo: [{ required: true, message: '设备号不能为空', trigger: 'blur' }],
@ -144,10 +150,10 @@ async function submitForm() {
await deviceRef.value.validate()
if (form.id) {
await updateDevice(form)
await updateDevice(form.value)
proxy.$modal.msgSuccess('修改成功')
} else {
await addDevice(form)
await addDevice(form.value)
proxy.$modal.msgSuccess('新增成功')
}

@ -2,7 +2,7 @@
* @Author: chris
* @Date: 2025-09-05 10:12:41
* @LastEditors: chris
* @LastEditTime: 2025-12-10 09:35:30
* @LastEditTime: 2026-01-06 10:55:13
*/
// 列配置
export const columnsConfig = [
@ -20,6 +20,7 @@ export const statusColorMap = {
};
export const defaultForm = {
cardNumber: "",
deviceNo: "",
deviceAddr: "",
model: "",

@ -135,14 +135,14 @@ function resetQuery() {
/** 新增按钮操作 */
function handleAdd() {
open.value = true
title.value = '新增虫情设备'
title.value = '新增设备'
formDialogRef.value.resetForm()
}
/** 修改按钮操作 */
function handleUpdate(row) {
open.value = true
title.value = '修改虫情设备信息'
title.value = '修改设备信息'
// row
const targetRow = row || (selectedRows.value.length ? selectedRows.value[0] : null)
if (targetRow) {

@ -0,0 +1,47 @@
<!--
* @Author: chris
* @Date: 2026-01-16 14:59:10
* @LastEditors: chris
* @LastEditTime: 2026-01-16 18:01:57
-->
<script setup>
const props = defineProps({
loading: {
type: Boolean,
default: false,
},
columns: {
type: Array,
default: () => [],
},
tableData: {
type: Array,
default: () => [],
},
})
</script>
<template>
<el-table
v-loading="props.loading"
:data="props.tableData"
border
highlight-current-row
style="width: 100%"
>
<el-table-column label="内容" align="center" prop="content" :show-overflow-tooltip="true" v-if="columns[0].visible" />
<el-table-column label="状态" align="center" prop="status" v-if="columns[1].visible">
<template #default="scope">
<span>{{ scope.row.status === 0 ? '已读' : '未读' }}</span>
</template>
</el-table-column>
<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>
<style lang="scss" scoped>
</style>

@ -0,0 +1,58 @@
<!--
* @Author: chris
* @Date: 2026-01-16 14:58:35
* @LastEditors: chris
* @LastEditTime: 2026-01-16 18:01:36
-->
<script setup>
const emits = defineEmits(['query', 'reset']);
const queryRef = ref(null);
const queryParams = reactive({
startTime: '',
endTime: ''
});
const dateRange = ref([]);
watch(() => dateRange.value, (val) => {
if (val && val.length === 2) {
queryParams.startTime = val[0];
queryParams.endTime = val[1];
} else {
queryParams.startTime = queryParams.endTime = '';
}
});
const handleQuery = () => {
emits('query', queryParams);
};
const resetQuery = () => {
dateRange.value = [];
emits('reset', queryRef.value);
};
</script>
<template>
<el-form :model="queryParams" ref="queryRef" :inline="true" label-width="68px">
<el-form-item label="时间范围">
<el-date-picker
style="width: 300px"
v-model="dateRange"
value-format="YYYY-MM-DD"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
></el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery"></el-button>
<el-button icon="Refresh" @click="resetQuery"></el-button>
</el-form-item>
</el-form>
</template>
<style lang="scss" scoped>
</style>

@ -0,0 +1,13 @@
export const defaultQueryParams = {
pageNum: 1,
pageSize: 10,
beginTime: "",
endTime: "",
};
// 列配置
export const columnsConfig = [
{ key: 0, label: "内容", visible: true },
{ key: 1, label: "状态", visible: true },
{ key: 2, label: "创建时间", visible: true },
];

@ -0,0 +1,55 @@
<!--
* @Author: chris
* @Date: 2026-01-16 14:47:23
* @LastEditors: chris
* @LastEditTime: 2026-01-16 18:03:50
-->
<script setup>
import SearchModule from './components/SearchModule.vue';
import FeedBackTable from './components/FeedBackTable.vue';
import { defaultQueryParams, columnsConfig } from './config.js'
import { listFeedBack } from '@/api/feedback/index.js'
const feedBackList = ref([]);
const loading = ref(false);
const total = ref(0);
const queryParams = ref ({
...defaultQueryParams
})
getFeedBackList()
function getFeedBackList() {
loading.value = true;
return listFeedBack(queryParams.value).then((res) => {
feedBackList.value = res.data;
}).finally(() => {
loading.value = false;
})
}
function handleQuery(query) {
queryParams.value = Object.assign(queryParams.value, query);
getFeedBackList();
}
function handleReset(formRef) {
queryParams.value = {
...defaultQueryParams
}
formRef.resetFields();
getFeedBackList();
}
</script>
<template>
<div class="app-container">
<SearchModule @query="handleQuery" @reset="handleReset" />
<FeedBackTable :tableData="feedBackList" :loading="loading" :columns="columnsConfig" />
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getFeedBackList" />
</div>
</template>
<style lang="scss" scoped>
</style>

@ -0,0 +1,115 @@
<!--
* @Author: chris
* @Date: 2025-01-13 16:31:10
* @LastEditors: chris
* @LastEditTime: 2026-01-19 15:07:11
-->
<script setup>
import { BmMarker, BmInfoWindow } from 'vue-baidu-map-3x'
const props = defineProps({
deviceList: {
type: Array,
default: () => []
},
currentDevice: {
type: Object,
default: () => null
}
})
const Map = ref(null)
const pestMap = ref(null)
const baiduMapConfig = {
center: '汕头市潮南区雷岭镇',
zoom: 12,
}
const winInfo = ref({
open: false,
lat: 0,
lng: 0,
content: ''
})
const mapDevices = computed(() => {
if (!Map.value) return []
return props.deviceList.map(item => {
const [lng, lat] = item.installPointDes.split(',')
return Object.assign({}, item, {
point: new Map.value.Point(lng, lat),
open: false
})
})
})
watch(() => props.currentDevice, (newVal) => {
if(!pestMap.value || !props.currentDevice) return
const device = mapDevices.value.find(item => item.deviceAddr === newVal.deviceAddr)
device && pestMap.value.flyTo(device.point, 16)
})
function mapReady({BMap, map}) {
console.log(BMap, map)
pestMap.value = map;
Map.value = BMap;
map.enableScrollWheelZoom(true)
// const overlays = map.getOverlays()
// const points = overlays.map(item => item.latLng)
// const viewport = map.getViewport(points)
// map.setViewport(viewport)
}
function mapZoomEnd({target}) {
target.setTilt(45)
}
function markerClick(e, item) {
console.log(item, e)
const deviceType = item.deviceType === '1' ? '虫情监测' : item.deviceType === '2' ? '土壤墒情监测' : '气象视频监测'
// 1: 2: 3:
winInfo.value = {
open: true,
lat: e.latLng.lat,
lng: e.latLng.lng,
content: `<p style="font-size: 16px; margin-bottom: 10px;font-weight: bold; color: #333;">${deviceType}设备</p>
<p style="font-size: 14px; color: #666;">设备编号${item.deviceNo}</p>
<p style="font-size: 14px;">设备状态<span style="color: ${item.status === '1' ? 'green' : 'red'};">${item.status === '1' ? '在线' : '离线'}</span></p>`
}
}
</script>
<template>
<div class="device-map" >
<baidu-map class="map" :center="baiduMapConfig.center" :zoom="baiduMapConfig.zoom" @ready="mapReady" @zoomend="mapZoomEnd">
<bm-marker v-for="item in mapDevices" :key="item.deviceAddr" :position="{lng: item.point.lng, lat: item.point.lat}" @click="markerClick($event, item)" >
</bm-marker>
<bm-info-window :show="winInfo.open" :position="{lng: winInfo.lng, lat: winInfo.lat}" :offset="{width: -20, height: -25}">
<div class="device-info" v-html="winInfo.content || '暂无数据'"></div>
</bm-info-window>
</baidu-map>
</div>
</template>
<style lang="scss" scoped>
.device-map {
// @apply mx-[-19px] my-[-15px];
@apply p-18px;
}
.device-map, .map {
@apply w-full h-full;
}
.map {
@apply rounded-8px overflow-hidden;
}
:deep(.anchorBL) {
display: none;
}
.device-info {
@apply p-12px;
}
</style>

@ -2,7 +2,7 @@
* @Author: chris
* @Date: 2025-08-22 10:10:51
* @LastEditors: chris
* @LastEditTime: 2025-10-15 16:02:36
* @LastEditTime: 2026-01-19 11:23:10
-->
<script setup>
import { useRoute } from 'vue-router'
@ -14,11 +14,13 @@ import PestInfo from './components/PestInfo.vue';
import SoilInfo from './components/SoilInfo.vue';
import AerialPhoto from './components/AerialPhoto.vue';
import TimeBox from './components/TimeBox.vue';
import OrchardMap from './components/OrchardMap.vue';
import { BorderBox12, Loading as DvLoading } from '@kjgl77/datav-vue3'
import useDraw from '@/hooks/useDraw';
import mock from './mock';
import { getDeviceDataAnalysis } from '@/api/deviceData';
import { getDept } from '@/api/system/dept'
import { listDevice } from '@/api/device';
const { proxy } = getCurrentInstance()
const route = useRoute()
@ -26,6 +28,7 @@ const route = useRoute()
const loading = ref(false)
const hoursRange = getDateRange(1)
const daysRange = getDateRange(7)
const deviceList = ref([])
const pestList = ref([])
const soilList = ref([])
const weatherList = ref([])
@ -48,21 +51,35 @@ onUnmounted(() => {
function getData() {
loading.value = true
Promise.all([
getDevices(),
getPestAnalysisData(),
getSoilAnalysisData(),
getWeatherAnalysisData(),
getBaseInfo()
]).then(res => {
console.log('res', res)
pestList.value = res[0].rows
soilList.value = res[1].rows
weatherList.value = res[2].rows
baseInfo.value = res[3].data
deviceList.value = res[0].rows
pestList.value = res[1].rows
soilList.value = res[2].rows
weatherList.value = res[3].rows
baseInfo.value = res[4].data
}).finally(() => {
loading.value = false
})
}
async function getDevices() {
try {
return await listDevice({
deptId: orchardId
})
} catch (error) {
proxy.$modal.msgError('获取设备数据失败: ' + (error.message || '未知错误'))
console.error('Failed to get device data:', error)
return error
}
}
async function getPestAnalysisData () {
try {
return await getDeviceDataAnalysis({
@ -169,7 +186,8 @@ function getDateRange (num) {
<soil-info :soil-data="soilList" />
</border-box-12>
<border-box-12>
<aerial-photo />
<!-- <aerial-photo /> -->
<orchard-map :deviceList="deviceList" />
</border-box-12>
</div>
</div>

@ -0,0 +1,58 @@
<!--
* @Author: chris
* @Date: 2026-01-06 11:50:02
* @LastEditors: chris
* @LastEditTime: 2026-01-15 17:52:16
-->
<script setup>
const defaultQueryParams = {
dateRange: [],
}
const queryRef = ref(null)
const queryParams = ref({
dateRange: [],
})
const emits = defineEmits(['export', 'query', 'reset'])
function handleQuery () {
emits('query', {
beginTime: queryParams.value.dateRange[0],
endTime: queryParams.value.dateRange[1],
})
}
function handleReset () {
queryParams.value = {...defaultQueryParams}
queryParams.value.dateRange = []
emits('reset', queryRef.value)
}
function handleExport () {
console.log('export', queryRef)
emits('export', queryRef.value)
}
</script>
<template>
<el-form :model="queryParams" ref="queryRef" :inline="true" label-width="68px">
<el-form-item label="记录时间">
<el-date-picker
style="width: 300px"
v-model="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="handleExport"></el-button>
<el-button icon="Refresh" @click="handleReset"></el-button>
</el-form-item>
</el-form>
</template>

@ -116,7 +116,7 @@ watch(selectedItems, (newVal) => {
function handlePagination(data) {
currentPage.value = data.page
pageSize.value = data.limit
getImageList()
emit('pageChange', data)
}
function handleReport(item) {
@ -127,11 +127,6 @@ function handleReport(item) {
function handleAnalyze(item) {
emit('handleAnalyze', item)
}
//
function handleDelete(item) {
emit('handleDelete', item)
}
</script>
//

@ -2,7 +2,7 @@
* @Author: chris
* @Date: 2025-08-12 11:20:07
* @LastEditors: chris
* @LastEditTime: 2025-10-15 11:29:04
* @LastEditTime: 2026-01-16 18:34:35
-->
<template>
<div :class="['img-analysis-page', 'page-height', { 'hasTagsView': hasTags }]">
@ -12,11 +12,11 @@
</el-col>
<el-col :span="20">
<el-card>
<!-- <action-module @handleQuery="handleQuery" @handleDownload="handleDownload"
@handleDelete="handleDelete" /> -->
<action-module @query="handleQuery" @export="handleExport"
@reset="handleReset" class="mb-10px" />
<el-row :gutter="20">
<el-col :span="16">
<image-list :imageList="imageList" v-model="selectedRows" @handleDelete="handleDelete" @handleAnalyze="handleAnalyze" @handleReport="handleReport" />
<image-list :imageList="imageList" v-model="selectedRows" @handleAnalyze="handleAnalyze" @handleReport="handleReport" @pageChange="handlePageChange" />
</el-col>
<el-col :span="1" class="line"></el-col>
<el-col :span="7">
@ -34,10 +34,20 @@ import { useSettings } from "@/hooks/useSettings";
import DeviceList from "./components/DeviceList.vue";
import ImageList from "./components/ImageList.vue";
import ImageDetail from "./components/ImageDetail.vue";
import ActionModule from "./components/ActionModule.vue";
import { imageListData } from "./mockData";
import { listDevice } from '@/api/device'
import { listDeviceData } from '@/api/deviceData'
const defaultQueryParams = {
pageNum: 1,
pageSize: 6,
type: 1,
deviceId: null,
beginTime: null,
endTime: null
}
const { proxy } = getCurrentInstance()
const { hasTags } = useSettings()
@ -50,12 +60,7 @@ const loading = ref(false)
const deviceLoading = ref(false)
const total = ref(0)
const queryParams = ref({
pageNum: 1,
pageSize: 6,
type: 1,
deviceId: null,
beginTime: null,
endTime: null
...defaultQueryParams
})
getDeviceList()
@ -93,15 +98,28 @@ async function getDeviceDataList () {
}
}
function handleQuery() {
function handleQuery(query) {
queryParams.value = Object.assign(queryParams.value, query)
getDeviceDataList()
}
function handleDownload() {
function handlePageChange(page) {
queryParams.value.pageNum = page.page
queryParams.value.pageSize = page.limit
getDeviceDataList()
}
function handleDelete() {
async function handleExport() {
const fileName = await proxy.download('/business/device-data/export', queryParams.value, `抓拍图片数据_${new Date().getTime()}.xlsx`)
fileName && proxy.$download.name(fileName)
}
function handleReset(queryFormRef) {
queryParams.value = {
...defaultQueryParams
}
queryFormRef.resetFields()
getDeviceDataList()
}
function handleAnalyze() {

@ -58,6 +58,7 @@
value-format="YYYY-MM-DD"
/>
<el-button type="primary" @click="getDeviceDataList"></el-button>
<el-button type="warning" @click="exportHistory"></el-button>
</el-col>
</el-row>
</template>
@ -174,6 +175,11 @@ function handleChangeDevice(device) {
function handleModelChange(val) {
console.log(val)
}
async function exportHistory() {
const fileName = await proxy.download('/business/device-data/export', queryParams.value, `虫情历史数据_${new Date().getTime()}.xlsx`)
fileName && proxy.$download.name(fileName)
}
</script>
<style lang="scss" scoped>

@ -2,7 +2,7 @@
* @Author: chris
* @Date: 2025-08-18 09:27:23
* @LastEditors: chris
* @LastEditTime: 2025-09-30 17:14:44
* @LastEditTime: 2026-01-16 18:49:16
-->
<script setup>
const props = defineProps({
@ -43,8 +43,12 @@ function handleQuery() {
emits('query')
}
function exportData() {
emits('export')
function exportAnalysis() {
emits('exportAnalysis')
}
function exportTrend() {
emits('exportTrend')
}
</script>
@ -65,7 +69,8 @@ function exportData() {
</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 type="warning" icon="Download" @click="exportAnalysis"></el-button>
<el-button type="warning" icon="Download" @click="exportTrend"></el-button>
<el-button icon="Refresh" @click="resetQuery"></el-button>
</el-form-item>
</el-form>

@ -2,7 +2,7 @@
* @Author: chris
* @Date: 2025-08-18 09:24:26
* @LastEditors: chris
* @LastEditTime: 2025-10-10 17:25:04
* @LastEditTime: 2026-01-16 18:48:30
-->
<script setup name="PestStatistics">
import { useSettings } from "@/hooks/useSettings";
@ -96,8 +96,14 @@ function handlePestChange(names) {
}
function exportView() {
console.log('导出视图')
async function exportAnalysis() {
const fileName = await proxy.download('/business/device-data/insect/analysis/export', queryParams.value, `分析导出_${new Date().getTime()}.xlsx`)
fileName && proxy.$download.name(fileName)
}
async function exportTrend() {
const fileName = await proxy.download('/business/device-data/insect/trend/export', queryParams.value, `统计导出_${new Date().getTime()}.xlsx`)
fileName && proxy.$download.name(fileName)
}
function resetQuery() {
@ -144,7 +150,7 @@ function createPestOption (names) {
<device-list v-model="deviceIds" :devices="deviceList" @selected="handleSelect"></device-list>
</el-col>
<el-col :span="20">
<search-form v-model:params="queryParams" @reset="resetQuery" @query="getList" @export="exportView" />
<search-form v-model:params="queryParams" @reset="resetQuery" @query="getList" @exportAnalysis="exportAnalysis" @exportTrend="exportTrend" />
<chart-action :options="options" @pestChange="handlePestChange" class="action" />
<statistics-chart :chartData="chartData" class="chart"/>
</el-col>

@ -6,11 +6,19 @@
<template #header>
<p>推送平台</p>
</template>
<el-checkbox-group v-model="platformGroup">
<el-checkbox label="公众号" :value="1" />
<el-checkbox label="短信" :value="2" />
</el-checkbox-group>
</el-card>
<el-card>
<template #header>
<p>推送模板</p>
</template>
<el-radio-group v-model="tmplGroup">
<el-radio :value="1">模板1</el-radio>
<el-radio :value="2">模板2</el-radio>
</el-radio-group>
</el-card>
</el-col>
<el-col :span="18">
@ -158,6 +166,8 @@ const single = ref(true)
const multiple = ref(true)
const total = ref(0)
const title = ref("")
const platformGroup = ref([])
const tmplGroup = ref([])
const data = reactive({
form: {},
@ -293,9 +303,9 @@ function handleDelete(row) {
/** 导出按钮操作 */
async function handleExport() {
proxy.download('business/member/export', {
...queryParams.value
}, `member_${new Date().getTime()}.xlsx`)
// proxy.download('business/member/export', {
// ...queryParams.value
// }, `_${new Date().getTime()}.xlsx`)
const fileName = await proxy.download('/business/member/export', queryParams.value, `推送人员数据_${new Date().getTime()}.xlsx`)
fileName && proxy.$download.name(fileName)
}

@ -2,7 +2,7 @@
* @Author: chris
* @Date: 2025-09-05 09:29:59
* @LastEditors: chris
* @LastEditTime: 2025-09-25 09:35:34
* @LastEditTime: 2026-01-19 09:04:28
-->
<script setup>
import LiveData from './components/LiveData.vue'
@ -108,6 +108,11 @@ function createChartData(data, type) {
})
return { dates, values }
}
async function exportAnalysis() {
const fileName = await proxy.download('/business/device-data/soil/analysis/export',{ type: 2 } , `分析导出_${new Date().getTime()}.xlsx`)
fileName && proxy.$download.name(fileName)
}
</script>
<template>
@ -117,6 +122,7 @@ function createChartData(data, type) {
<live-data :items="liveData" @select="handleSelectSoilItem"/>
</el-col>
<el-col class="history-data" :span="18">
<el-button type="warning" @click="exportAnalysis" class="export-btn">导出</el-button>
<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" /> -->
@ -132,5 +138,8 @@ function createChartData(data, type) {
> .el-row {
@apply h-full;
}
.export-btn {
@apply float-right mr-10px mt-10px;
}
}
</style>

@ -23,6 +23,7 @@
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery"></el-button>
<el-button icon="Refresh" @click="resetQuery"></el-button>
<el-button icon="Download" type="warning" @click="handleExport"></el-button>
</el-form-item>
</el-form>
@ -81,13 +82,13 @@
<el-form ref="deptRef" :model="form" :rules="rules" label-width="100px">
<el-row>
<el-col :span="24" v-if="form.parentId !== 0">
<el-form-item label="上级部门" prop="parentId">
<el-form-item label="上级单位" prop="parentId">
<el-tree-select
v-model="form.parentId"
:data="deptOptions"
:props="{ value: 'deptId', label: 'deptName', children: 'children' }"
value-key="deptId"
placeholder="选择上级部门"
placeholder="选择上级单位"
check-strictly
/>
</el-form-item>
@ -182,7 +183,7 @@ const data = reactive({
status: undefined
},
rules: {
parentId: [{ required: true, message: "上级部门不能为空", trigger: "blur" }],
parentId: [{ required: true, message: "上级单位不能为空", trigger: "blur" }],
deptName: [{ required: true, message: "果园名称不能为空", trigger: "blur" }],
orderNum: [{ required: true, message: "显示排序不能为空", trigger: "blur" }],
email: [{ type: "email", message: "请输入正确的邮箱地址", trigger: ["blur", "change"] }],
@ -251,7 +252,7 @@ function handleAdd(row) {
form.value.parentId = row.deptId;
}
open.value = true;
title.value = "添加部门";
title.value = "添加合作社/果园";
}
/** 展开/折叠操作 */
@ -272,7 +273,7 @@ function handleUpdate(row) {
getDept(row.deptId).then(response => {
form.value = response.data;
open.value = true;
title.value = "修改部门";
title.value = "修改合作社/果园";
});
}
@ -307,5 +308,9 @@ function handleDelete(row) {
}).catch(() => {});
}
function handleExport () {
}
getList();
</script>

@ -6,7 +6,7 @@
<pane size="16">
<el-col>
<div class="head-container">
<el-input v-model="deptName" placeholder="请输入部门名称" clearable prefix-icon="Search" style="margin-bottom: 20px" />
<el-input v-model="deptName" placeholder="请输入单位名称" clearable prefix-icon="Search" style="margin-bottom: 20px" />
</div>
<div class="head-container">
<el-tree :data="deptOptions" :props="{ label: 'label', children: 'children' }" :expand-on-click-node="false" :filter-node-method="filterNode" ref="deptTreeRef" node-key="id" highlight-current default-expand-all @node-click="handleNodeClick" />
@ -61,7 +61,7 @@
<el-table-column label="用户编号" align="center" key="userId" prop="userId" v-if="columns[0].visible" />
<el-table-column label="用户名称" align="center" key="userName" prop="userName" v-if="columns[1].visible" :show-overflow-tooltip="true" />
<el-table-column label="用户昵称" align="center" key="nickName" prop="nickName" v-if="columns[2].visible" :show-overflow-tooltip="true" />
<el-table-column label="部门" align="center" key="deptName" prop="dept.deptName" v-if="columns[3].visible" :show-overflow-tooltip="true" />
<el-table-column label="单位" align="center" key="deptName" prop="dept.deptName" v-if="columns[3].visible" :show-overflow-tooltip="true" />
<el-table-column label="手机号码" align="center" key="phonenumber" prop="phonenumber" v-if="columns[4].visible" width="120" />
<el-table-column label="状态" align="center" key="status" v-if="columns[5].visible">
<template #default="scope">
@ -111,8 +111,8 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="归属部门" prop="deptId">
<el-tree-select v-model="form.deptId" :data="deptOptions" :props="{ value: 'id', label: 'label', children: 'children' }" value-key="id" placeholder="请选择归属部门" check-strictly />
<el-form-item label="归属单位" prop="deptId">
<el-tree-select v-model="form.deptId" :data="deptOptions" :props="{ value: 'id', label: 'label', children: 'children' }" value-key="id" placeholder="请选择归属单位" check-strictly />
</el-form-item>
</el-col>
</el-row>

@ -2,7 +2,7 @@
* @Author: chris
* @Date: 2025-09-11 17:11:16
* @LastEditors: chris
* @LastEditTime: 2025-09-19 11:12:17
* @LastEditTime: 2026-01-19 09:52:54
-->
<script setup>
import HistoryTable from './components/HistoryTable.vue';
@ -54,7 +54,7 @@ function handleReset(queryRef) {
}
async function handleExport() {
const fileName = await proxy.download('/business/device-data/export', queryParams.value, `土壤墒情设备数据_${new Date().getTime()}.xlsx`)
const fileName = await proxy.download('/business/device-data/export', queryParams.value, `气象设备历史数据_${new Date().getTime()}.xlsx`)
fileName && proxy.$download.name(fileName)
}
</script>

@ -2,12 +2,14 @@
* @Author: chris
* @Date: 2025-09-09 15:30:24
* @LastEditors: chris
* @LastEditTime: 2025-10-27 14:56:51
* @LastEditTime: 2026-01-19 09:47:04
-->
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import VideoJs from 'video.js'
import weatherImg from '@/assets/images/devices/weather.png'
import videoSrc from '@/assets/video/test.mp4'
import { ArrowUp, ArrowDown, ArrowLeft, ArrowRight, ZoomIn, ZoomOut } from '@element-plus/icons-vue'
const props = defineProps({
items: {
@ -52,6 +54,33 @@ function handleSelect(item) {
emits('select', item)
}
function controlCamera(action) {
console.log('Camera control action:', action);
//
switch (action) {
case 'up':
console.log('Move camera up');
break;
case 'down':
console.log('Move camera down');
break;
case 'left':
console.log('Move camera left');
break;
case 'right':
console.log('Move camera right');
break;
case 'zoomIn':
console.log('Zoom in camera');
break;
case 'zoomOut':
console.log('Zoom out camera');
break;
default:
break;
}
}
</script>
<template>
@ -66,6 +95,39 @@ function handleSelect(item) {
</video>
</div>
<!-- 视频控制面板 -->
<div class="video-control">
<div class="control-buttons">
<!-- 方向控制按钮 -->
<div class="direction-controls">
<button class="control-btn" @click="controlCamera('up')">
<el-icon class="control-icon" size="20"><ArrowUp /></el-icon>
</button>
<div class="horizontal-controls">
<button class="control-btn" @click="controlCamera('left')">
<el-icon class="control-icon" size="20"><ArrowLeft /></el-icon>
</button>
<button class="control-btn" @click="controlCamera('right')">
<el-icon class="control-icon" size="20"><ArrowRight /></el-icon>
</button>
</div>
<button class="control-btn" @click="controlCamera('down')">
<el-icon class="control-icon" size="20"><ArrowDown /></el-icon>
</button>
</div>
<!-- 缩放控制按钮 -->
<div class="zoom-controls">
<button class="control-btn" @click="controlCamera('zoomIn')">
<el-icon class="control-icon" size="20"><ZoomIn /></el-icon>
</button>
<button class="control-btn" @click="controlCamera('zoomOut')">
<el-icon class="control-icon" size="20"><ZoomOut /></el-icon>
</button>
</div>
</div>
</div>
<!-- 数据卡片区域 -->
<div class="data-cards">
<div
@ -89,14 +151,14 @@ function handleSelect(item) {
</div>
<!-- 图片区域 -->
<div class="weather-photo">
<!-- <div class="weather-photo">
<el-image
:src="weatherImg"
fit="contain"
class="weather-photo-img"
lazy
/>
</div>
</div> -->
</div>
</template>
@ -208,4 +270,42 @@ $transition-base: all 0.3s ease;
.video-player {
@apply max-h-[100%] max-w-[100%];
}
//
.video-control {
@apply mb-24px;
}
.control-buttons {
@apply flex items-center gap-24px;
}
//
.direction-controls {
@apply flex flex-col items-center gap-8px flex-1;
}
.horizontal-controls {
@apply flex gap-40px;
}
//
.zoom-controls {
@apply flex gap-8px flex-1;
}
//
.control-btn {
@apply w-60px h-50px rounded-8px flex items-center justify-center border-[1px] border-style-solid border-[#e4e7ed];
background: $bg-secondary;
transition: $transition-base;
&:hover {
@apply bg-[#ecf5ff] border-[#c6e2ff] shadow-[0_2px_8px_rgba(0,0,0,0.1)];
}
&:active {
@apply bg-[#d9ecff] border-[#91d5ff];
}
}
</style>

@ -2,7 +2,7 @@
* @Author: chris
* @Date: 2025-09-05 09:29:59
* @LastEditors: chris
* @LastEditTime: 2025-09-25 09:35:16
* @LastEditTime: 2026-01-19 09:08:55
-->
<script setup>
import LiveData from './components/LiveData.vue'
@ -135,5 +135,9 @@ function formatData(data) {
> .el-row {
@apply h-full;
}
.export-btn {
@apply float-right mr-10px mt-10px;
}
}
</style>

Loading…
Cancel
Save