feat: 首页功能数据渲染;设备管理列表添加设备图;推送历史页面数据渲染;

master
chris 6 days ago
parent af8313de91
commit 4dabcb080e

@ -35,6 +35,7 @@
"unocss": "^0.65.1",
"video.js": "^8.23.4",
"vue": "3.4.31",
"vue-baidu-map-3x": "^1.0.40",
"vue-cropper": "1.1.1",
"vue-router": "4.4.0"
},

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

@ -2,7 +2,7 @@
* @Author: chris
* @Date: 2025-02-06 16:43:54
* @LastEditors: chris
* @LastEditTime: 2025-09-22 17:33:54
* @LastEditTime: 2025-10-29 16:12:03
-->
<template>
<div class="device-list-container">
@ -17,9 +17,9 @@
<div class="device-details">
<div class="device-name font-medium">{{ device.deviceNo }}</div>
<div class="device-model">{{ device.model }}</div>
<!-- <el-tag :type="device.deviceEnabled ? 'success' : 'danger'" size="small">
{{ device.deviceEnabled ? '在线' : '离线' }}
</el-tag> -->
<el-tag :type="device.onlineStatus ? 'success' : 'danger'" size="small">
{{ device.onlineStatus ? '在线' : '离线' }}
</el-tag>
</div>
</div>
</div>

@ -2,11 +2,12 @@
* @Author: chris
* @Date: 2025-01-13 09:34:10
* @LastEditors: chris
* @LastEditTime: 2025-10-27 14:19:18
* @LastEditTime: 2025-10-30 17:40:44
*/
import { createApp } from "vue";
import Cookies from "js-cookie";
import BaiduMap from "vue-baidu-map-3x";
import ElementPlus from "element-plus";
import "element-plus/dist/index.css";
@ -93,6 +94,12 @@ app.component("Icon", Icon);
directive(app);
app.use(BaiduMap, {
// ak 是在百度地图开发者平台申请的密钥 详见 http://lbsyun.baidu.com/apiconsole/key */
ak: "cAIqAWTNEUud6dBKXuuXOcv3z8tebyeO",
type: "WebGL", // ||API 默认API (使用此模式 BMap=BMapGL)
});
// 使用element-plus 并且设置全局的大小
app.use(ElementPlus, {
locale: locale,

@ -41,6 +41,11 @@
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="24" v-if="props.form.type === '4'">
<el-form-item label="视频流" prop="videoPlayUrl">
<el-input v-model="props.form.videoPlayUrl" placeholder="请输入视频流地址" maxlength="200" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注">
<el-input v-model="props.form.remark" type="textarea" placeholder="请输入备注内容"></el-input>
@ -79,6 +84,10 @@ watch(() => uploadImg.value, (newVal) => {
props.form.imgUrl = newVal
})
watch(() => props.form.imgUrl, (newVal) => {
uploadImg.value = newVal
})
//
const rules = {
imgUrl: [{ required: true, message: '请上传设备图片', trigger: 'blur' }],
@ -88,6 +97,7 @@ const rules = {
model: [{ required: true, message: '设备型号不能为空', trigger: 'blur' }],
status: [{ required: true, message: '请选择状态', trigger: 'blur' }],
installPointDes: [{ required: true, message: '安装点描述不能为空', trigger: 'blur' }],
videoPlayUrl: [{ required: true, message: '视频流地址不能为空', trigger: 'blur' }],
}
//

@ -2,7 +2,7 @@
* @Author: chris
* @Date: 2025-08-08 16:17:54
* @LastEditors: chris
* @LastEditTime: 2025-09-17 16:59:45
* @LastEditTime: 2025-10-27 16:45:32
-->
<template>
<el-table
@ -15,6 +15,11 @@
style="width: 100%"
>
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="设备图" align="center" prop="imgUrl">
<template #default="scope">
<ImagePreview :src="scope.row.imgUrl" fit="contain" width="100px" height="100px" />
</template>
</el-table-column>
<el-table-column label="设备号" align="center" prop="deviceNo" v-if="columns[0].visible" :show-overflow-tooltip="true" />
<el-table-column label="型号" align="center" prop="model" v-if="columns[1].visible" />
<el-table-column label="类型" align="center" prop="type" v-if="columns[2].visible">

@ -95,7 +95,8 @@ const data = reactive({
installPointDes: '',
imgUrl: '',
type: null,
status: '0'
status: '0',
videoPlayUrl: ''
},
queryParams: {
type: null,
@ -112,7 +113,7 @@ onMounted(() => {
getList()
})
/** 查询果园列表 */
/** 查询果园设备列表 */
async function getList() {
loading.value = true
try {

@ -2,34 +2,35 @@
* @Author: chris
* @Date: 2025-01-13 09:34:10
* @LastEditors: chris
* @LastEditTime: 2025-08-06 16:12:04
* @LastEditTime: 2025-10-31 14:18:26
-->
<template>
<div class="app-container home" :class="{ 'hasTagsView': hasTags }">
<section class="home-top">
<device-overview />
<device-overview :deviceData="formateData" />
</section>
<section class="home-content">
<el-row :gutter="12" class="h-100%">
<el-col :span="3">
<el-col :span="4">
<el-card class="device-list">
<template #header>
<span class="card-title">设备列表</span>
</template>
<!-- <device-list /> -->
<device-flat-list @change="handleChangeDevice"/>
<device-flat-list @change="handleChangeDevice" :list="deviceList"/>
</el-card>
</el-col>
<el-col :span="21" class="!flex flex-col">
<el-row :gutter="12" class="m-b-12px flex-1">
<el-col :span="14">
<el-col :span="20" class="!flex flex-col">
<el-row :gutter="12" class="flex-1">
<el-col :span="15">
<el-card class="device-map h-100%">
<template #header>
<span class="card-title">设备地图</span>
</template>
<device-map :deviceList="deviceList" :currentDevice="currentDevice"/>
</el-card>
</el-col>
<el-col :span="10">
<el-col :span="9">
<el-card class="device-info h-100%">
<template #header>
<span class="card-title">设备信息</span>
@ -38,12 +39,12 @@
</el-card>
</el-col>
</el-row>
<el-card class="live-data h-350px">
<!-- <el-card class="live-data h-350px">
<template #header>
<span class="card-title">实时数据</span>
</template>
<live-data />
</el-card>
</el-card> -->
</el-col>
</el-row>
</section>
@ -53,17 +54,63 @@
<script setup name="Index">
import useSettingsStore from '@/store/modules/settings'
import deviceOverview from './indexComponents/deviceOverview.vue'
import liveData from './indexComponents/liveData.vue'
// import liveData from './indexComponents/liveData.vue'
import deviceFlatList from '@/components//deviceFlatList'
import deviceMap from './indexComponents/deviceMap'
import deviceInfo from './indexComponents/deviceInfo'
import { listDevice } from '@/api/device'
const settingsStore = useSettingsStore()
const deviceList = ref([])
const formateData = ref({})
const currentDevice = ref(null);
const loading = ref(false)
const queryParams = ref({})
const hasTags = computed(() => settingsStore.tagsView);
getDeviceList()
async function getDeviceList() {
loading.value = true
try {
const res = await listDevice(queryParams.value)
deviceList.value = res.rows || []
formateData.value = formatDeviceData(deviceList.value)
} catch (error) {
proxy.$modal.msgError('获取设备数据失败: ' + (error.message || '未知错误'))
console.error('Failed to get pest list:', error)
} finally {
loading.value = false
}
}
function formatDeviceData (devices) {
const dataOfType = { // 1: 2: 3:
1: [],
2: [],
3: [],
4: []
}
const dataOfStatus = { // 0: 线 1: 线
0: [],
1: [],
}
devices.forEach(device => {
const type = device.type
const status = device.onlineStatus
dataOfType[type] && dataOfType[type].push(device)
dataOfStatus[status] && dataOfStatus[status].push(device)
})
return {
dataOfType,
dataOfStatus,
}
}
const handleChangeDevice = (device) => {
currentDevice.value = device;
};
@ -89,5 +136,11 @@ const handleChangeDevice = (device) => {
.device-list {
height: 100%;
}
:deep(.device-map) {
.el-card__body {
height: calc(100% - 43px);
}
}
</style>

@ -2,40 +2,40 @@
* @Author: chris
* @Date: 2025-01-13 16:31:26
* @LastEditors: chris
* @LastEditTime: 2025-08-07 09:29:31
* @LastEditTime: 2025-10-30 14:40:38
-->
<template>
<div class="device-info" v-if="device">
<div class="card-header">
<div class="header-content">
<el-icon :size="20" class="icon-folder"><Folder /></el-icon>
<h3 class="device-name">{{ device.deviceName }}</h3>
<h3 class="device-name">{{ device.deviceNo }}</h3>
</div>
<el-tag :type="device.deviceEnabled ? 'success' : 'danger'" size="small" class="status-tag">
{{ device.deviceEnabled ? '在线' : '离线' }}
<el-tag :type="device.onlineStatus ? 'success' : 'danger'" size="small" class="status-tag">
{{ device.onlineStatus ? '在线' : '离线' }}
</el-tag>
</div>
<div class="card-body">
<div class="info-left">
<div class="info-item">
<span class="info-label">设备型号:</span>
<span class="info-value">{{ device.model }}</span>
</div>
<div class="info-item">
<span class="info-label">设备地址码:</span>
<span class="info-value">{{ device.deviceAddr }}</span>
</div>
<div class="info-item">
<span class="info-label">设备经纬度:</span>
<span class="info-value">{{ device.devicelat }},{{ device.devicelng }}</span>
</div>
<div class="info-item">
<span class="info-label">离线判断间隔:</span>
<span class="info-value">{{ device.offlineInterval }}分钟</span>
<span class="info-value">{{ device.installPointDes }}</span>
</div>
<el-button type="primary" class="detail-button" @click="handleViewDetail(device)">
<!-- <el-button type="primary" class="detail-button" @click="handleViewDetail(device)">
查看详情
</el-button>
</el-button> -->
</div>
<div class="info-right">
<el-image class="device-image" :src="deviceImageDict[device.deviceType]" fit="contain" />
<el-image class="device-image" :src="deviceImage" fit="contain" />
</div>
</div>
</div>
@ -49,7 +49,7 @@ import { Folder } from '@element-plus/icons-vue';
import { deviceImageDict } from '@/dict/index.js';
defineProps({
const props = defineProps({
device: {
type: Object,
required: true,
@ -59,6 +59,12 @@ defineProps({
defineEmits(['viewDetail']);
const deviceImage = computed(() => {
return import.meta.env.VITE_APP_BASE_API + props.device.imgUrl;
});
const handleViewDetail = (device) => {
emit('viewDetail', device);
};

@ -0,0 +1,83 @@
<!--
* @Author: chris
* @Date: 2025-01-13 16:31:10
* @LastEditors: chris
* @LastEditTime: 2025-10-31 15:33:18
-->
<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 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)
}
</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}" >
</bm-marker>
</baidu-map>
</div>
</template>
<style lang="scss" scoped>
.device-map {
// @apply mx-[-19px] my-[-15px];
}
.device-map, .map {
@apply w-full h-full;
}
:deep(.anchorBL) {
display: none;
}
</style>

@ -1,25 +1,41 @@
<template>
<div class="sort-info" ref="sortInfoRef">
<el-row :gutter="8">
<el-col :span="index == 0 ? 6 : 3" v-for="(item, index) in infoItems" :key="index">
<div class="info-item" data-type="infoItem" :style="`background-color: ${colorList[item.type].color}`" @mouseover.self="handleHoverItem($event, item)" @mouseleave.self="winVisible = false">
<Icon :icon="colorList[item.type].icon" class="info-item__icon"/>
<el-col :span="6">
<div class="info-item" data-type="infoItem" :style="`background-color: #329874`" @mouseover.self="handleHoverItem($event, allDeviceList)" @mouseleave.self="winVisible = false">
<Icon icon="carbon:devices" class="info-item__icon"/>
<div class="info-msg">
<p class="msg-title">{{ item.title }}</p>
<span class="msg-num">{{ item.num }}</span>
<p class="msg-title">设备总数</p>
<span class="msg-num">{{ allDeviceList.length }}</span>
</div>
</div>
</el-col>
<el-col :span="typeDict[key].span" v-for="(key, index) in Object.keys(props.deviceData?.dataOfType || {})" :key="index">
<div class="info-item" data-type="infoItem" :style="`background-color: ${typeDict[key].color}`" @mouseover.self="handleHoverItem($event, props.deviceData?.dataOfType[key])" @mouseleave.self="winVisible = false">
<Icon :icon="typeDict[key].icon" class="info-item__icon"/>
<div class="info-msg">
<p class="msg-title">{{ typeDict[key].title }}</p>
<span class="msg-num">{{ props.deviceData?.dataOfType[key].length }}</span>
</div>
</div>
</el-col>
<el-col :span="statusDict[key].span" v-for="(key, index) in Object.keys(props.deviceData?.dataOfStatus || {})" :key="index">
<div class="info-item" data-type="infoItem" :style="`background-color: ${statusDict[key].color}`" @mouseover.self="handleHoverItem($event, props.deviceData?.dataOfStatus[key])" @mouseleave.self="winVisible = false">
<Icon :icon="statusDict[key].icon" class="info-item__icon"/>
<div class="info-msg">
<p class="msg-title">{{ statusDict[key].title }}</p>
<span class="msg-num">{{ props.deviceData?.dataOfStatus[key].length }}</span>
</div>
</div>
</el-col>
</el-row>
<div class="device-win" ref="deviceWinRef" :style="`width: ${deviceWinWidth}px;`" v-show="winVisible" @mouseover="winVisible = true" @mouseleave="winVisible = false">
<div class="search-box">
<!-- <div class="search-box">
<el-input placeholder="请输入设备名称" class="search-input" v-model="searchValue" />
<el-button type="primary" class="search-btn" @click="searchDevices"></el-button>
</div>
</div> -->
<el-table :data="tableData" stripe style="width: 100%">
<template v-for="key in Object.keys(deviceData)">
<el-table-column :prop="key" label="deviceData[key]" />
</template>
<el-table-column prop="deviceAddr" label="设备地址" />
</el-table>
</div>
</div>
@ -28,74 +44,65 @@
<script setup>
const { proxy } = getCurrentInstance()
const colorList = readonly({
device:{
icon: 'carbon:devices',
color: '#329874'
},
pest:{
const typeDict = readonly({ // 1: 2: 3:
1:{
icon: 'carbon:pest',
color: '#b47a3e'
},
environment:{
icon: 'carbon:radar-weather',
color: '#396a98'
title: '虫情检测设备',
color: '#b47a3e',
span: 3
},
soil:{
2:{
icon: 'carbon:soil-moisture',
color: '#45969f'
title: '墒情监测设备',
color: '#45969f',
span: 3
},
online:{
icon: 'carbon:link',
color: '#3aa534'
},
offline:{
icon: 'carbon:unlink',
color: '#808890'
3:{
icon: 'carbon:radar-weather',
title: '气象监测设备',
color: '#396a78',
span: 3
},
abnormal:{
icon: 'carbon:warning',
color: '#c04747'
4:{
icon: 'carbon:camera',
title: '监控设备',
color: '#233c62',
span: 3
}
})
const infoItems = reactive([
{
type: 'device',
title: '设备总数',
num: 12
},
{
type: 'pest',
title: '虫情监测设备',
num: 6
},
{
type: 'environment',
title: '环境监测设备',
num: 2
},
{
type: 'soil',
title: '土壤监测设备',
num: 2
},
{
type: 'online',
const statusDict = readonly({
1:{
icon: 'carbon:link',
color: '#3aa534',
title: '在线设备',
num: 18
span: 3
},
{
type: 'offline',
0:{
icon: 'carbon:unlink',
color: '#808890',
title: '离线设备',
num: 2
},
{
type: 'abnormal',
title: '异常设备',
num: 2
span: 3
}
})
const props = defineProps({
deviceData: {
type: Object,
default: () => ({
dataOfType: {
1: [],
2: [],
3: [],
4: []
},
dataOfStatus: {
0: [],
1: []
}
})
}
])
})
const deviceWinRef = ref(null)
const sortInfoRef = ref(null)
@ -104,22 +111,25 @@ const winVisible = ref(false)
const searchValue = ref('')
const deviceType = ref(null)
const deviceWinWidth = ref(460)
const deviceData = reactive({})
const tableData = computed(() => {
return proxy.$utils.objectToArray(deviceData)
const tableData = ref([])
const allDeviceList = computed(() => {
const list = []
Object.values(props.deviceData?.dataOfType || {}).forEach(items => {
list.push(...items)
})
return list;
})
function searchDevices () {
console.log('searchValue:', searchValue.value)
}
function handleHoverItem (e, item) {
function handleHoverItem (e, list) {
const target = e.target;
const { offsetLeft, offsetWidth } = target;
const parentWidth = sortInfoRef.value.offsetWidth;
console.log(e, item, sortInfoRef)
tableData.value = list;
winVisible.value = true;
// nextTick(() => {
// const isLeft = (parentWidth- offsetLeft) > deviceWinWidth.value;

@ -2,7 +2,7 @@
* @Author: chris
* @Date: 2025-08-18 09:27:23
* @LastEditors: chris
* @LastEditTime: 2025-10-15 17:34:07
* @LastEditTime: 2025-10-27 17:07:49
-->
<script setup>
const props = defineProps({
@ -62,7 +62,7 @@ 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="exportData"></el-button> -->
<el-button icon="Refresh" @click="resetQuery"></el-button>
</el-form-item>
</el-form>

@ -2,7 +2,7 @@
* @Author: chris
* @Date: 2025-10-15 17:07:16
* @LastEditors: chris
* @LastEditTime: 2025-10-16 10:26:28
* @LastEditTime: 2025-10-27 17:07:07
-->
<script setup>
import HistoryTable from './components/HistoryTable.vue';
@ -10,7 +10,7 @@ import SearchForm from './components/SearchForm.vue';
import Statistics from './components/Statistics.vue';
import { columnsConfig, defaultQueryParams } from './config';
import { useSettings } from '@/hooks/useSettings';
import { listDeviceData, exportDeviceData } from '@/api/deviceData';
import { listNotify } from '@/api/pest/pushHistory';
const { hasTags } = useSettings();
const { proxy } = getCurrentInstance();
@ -26,13 +26,13 @@ getHistoryList();
async function getHistoryList () {
loading.value = true;
try {
const res = await listDeviceData(queryParams.value);
const res = await listNotify(queryParams.value);
historyList.value = res.rows;
chartData.value = createChartData(res.rows)
total.value = res.total;
} catch (error) {
proxy.$modal.msgError('获取土壤墒情设备实时数据失败: ' + (error.message || '未知错误'))
console.error('Failed to get device live data:', error)
proxy.$modal.msgError('获取推送历史数据失败: ' + (error.message || '未知错误'))
console.error('Failed to get push history data:', error)
} finally {
loading.value = false;
}

Loading…
Cancel
Save