From 235a109b3b461c7acbd1d7bb4f7e920075de2b9e Mon Sep 17 00:00:00 2001 From: 马宇豪 <978517621@qq.com> Date: 星期四, 24 四月 2025 15:40:51 +0800 Subject: [PATCH] 修改大屏 --- src/views/hazardousChemicals/bigScreen/index.vue | 35 ++ src/views/hazardousChemicals/bigScreen/components/midTop.vue | 42 +++ index.html | 1 package.json | 1 src/views/hazardousChemicals/bigScreen/components/midTop2.vue | 490 ++++++++++++++++++++++++++++++++++++++++ src/router/index.js | 5 src/views/hazardousChemicals/bigScreen/components/leftTop.vue | 2 src/views/hazardousChemicals/bigScreen/components/midBottom.vue | 142 +++++++++++ 8 files changed, 705 insertions(+), 13 deletions(-) diff --git a/index.html b/index.html index 08775e0..e5adce6 100644 --- a/index.html +++ b/index.html @@ -211,6 +211,7 @@ </div> </div> <script type="module" src="/src/main.js"></script> + <script type="text/javascript" src="https://api.map.baidu.com/api?v=1.0&&type=webgl&ak=SHPNPo8VSiNTeyuec9nEbMmJXTS2StiW"></script> </body> </html> diff --git a/package.json b/package.json index d551c1a..a608fb6 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,6 @@ "tinymce": "^5.10.2", "video.js": "^8.12.0", "vue": "3.2.45", - "vue-baidu-map-3x": "^1.0.35", "vue-cropper": "1.0.3", "vue-qr": "^4.0.9", "vue-quill-editor": "^3.0.6", diff --git a/src/router/index.js b/src/router/index.js index f088d84..c255cba 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -53,6 +53,11 @@ hidden: true }, { + path: '/bigMap', + component: () => import('@/views/hazardousChemicals/bigScreen/index.vue'), + hidden: true + }, + { path: '/warehouseManage', component: Layout, redirect: '/warehouseManage', diff --git a/src/views/hazardousChemicals/bigScreen/components/leftTop.vue b/src/views/hazardousChemicals/bigScreen/components/leftTop.vue index d787bd5..e6a8327 100644 --- a/src/views/hazardousChemicals/bigScreen/components/leftTop.vue +++ b/src/views/hazardousChemicals/bigScreen/components/leftTop.vue @@ -64,7 +64,7 @@ if (res.code == 200) { data.companyList = res.data.list if(data.queryParams.companyId == null){ - data.queryParams.companyId = data.companyList[0].id + data.queryParams.companyId = data.companyList[data.companyList.length - 1].id } } else { ElMessage.warning(res.message) diff --git a/src/views/hazardousChemicals/bigScreen/components/midBottom.vue b/src/views/hazardousChemicals/bigScreen/components/midBottom.vue index eca8b81..7f9b1f3 100644 --- a/src/views/hazardousChemicals/bigScreen/components/midBottom.vue +++ b/src/views/hazardousChemicals/bigScreen/components/midBottom.vue @@ -1,19 +1,99 @@ <template> <div class="charts-container"> <div id="preWarning"></div> + <div class="table-wrapper"> + <table class="scrollable-table"> + <thead> + <tr> + <th>预警信息</th> + <th>预警时间</th> + </tr> + </thead> + </table> + <div class="scroll-viewport" ref="viewport"> + <div class="scroll-content" :style="contentStyle"> + <table class="scrollable-table"> + <tbody> + <tr v-for="(item,index) in warningData" :key="item.id"> + <td>{{ item.warningInfo }}</td> + <td>{{ item.warningTime }}</td> + </tr> + <tr v-for="(item,index) in loopData" :key="`loop-${item.id}`"> + <td>{{ item.warningInfo }}</td> + <td>{{ item.warningTime }}</td> + </tr> + </tbody> + </table> + </div> + </div> + </div> </div> </template> <script setup> import * as echarts from 'echarts'; -import {onMounted} from "vue"; +import {computed, onBeforeUnmount, onMounted, ref} from "vue"; import {getCompanyMessage, getDailywarningCount} from "@/api/monitor/screenCharts"; import {ElMessage} from "element-plus"; +const warningData = [ + {warningInfo: '超期预警',warningTime: '2025-04-24 13:18:41' }, + {warningInfo: '超期预警',warningTime: '2025-04-23 13:00:21' }, + {warningInfo: '超期预警',warningTime: '2025-04-20 10:11:34' }, + {warningInfo: '超期预警',warningTime: '2025-04-18 09:28:51' }, + {warningInfo: '超期预警',warningTime: '2025-04-16 08:18:21' }, + {warningInfo: '超期预警',warningTime: '2025-04-15 05:12:21' }, + {warningInfo: '超期预警',warningTime: '2025-04-14 04:11:41' } +] +// 配置参数 +const visibleRows = 5 // 显示的行数 +const scrollSpeed = 0.5 // 每次滚动的像素数 +const rowHeight = 36 // 行高,与CSS一致 +const viewport = ref(null) +const scrollPosition = ref(0) +let animationFrame = null + onMounted(()=>{ getList() - +// 设置视口高度 + if (viewport.value) { + viewport.value.style.height = `${visibleRows * rowHeight}px` + } + // 延迟启动滚动,确保初始渲染完成 + setTimeout(() => { + scrollAnimation() + }, 100) }) +onBeforeUnmount(() => { + if (animationFrame) { + cancelAnimationFrame(animationFrame) + } +}) + +// 复制前几行数据用于循环 +const loopData = computed(() => { + return warningData.slice(0, visibleRows) +}) + +// 内容区域样式 +const contentStyle = computed(() => { + return { + transform: `translateY(-${scrollPosition.value}px)` + } +}) + +// 滚动动画 +const scrollAnimation = () => { + const totalHeight = warningData.length * rowHeight + const loopHeight = loopData.value.length * rowHeight + // 更新滚动位置 + scrollPosition.value += scrollSpeed + // 当滚动到循环数据部分时,重置位置实现无缝衔接 + if (scrollPosition.value >= totalHeight) { + scrollPosition.value -= totalHeight + } + animationFrame = requestAnimationFrame(scrollAnimation) +} const getList = async () => { const res = await getDailywarningCount() @@ -105,10 +185,66 @@ .charts-container{ width: 100%; height: 100%; + display: flex; } #preWarning{ - width: 100%; + flex: 2; height: 100%; } + +.table-wrapper { + position: relative; + flex: 1; + height: 100%; + border: 1px solid rgba(255,255,255,.1); + border-radius: 2px; + overflow: hidden; + + .scrollable-table { + width: 100%; + border-collapse: collapse; + + th,td { + padding: 12px 15px; + color: #fff; + text-align: left; + border-bottom: 1px solid rgba(255,255,255,.1); + height: 36px; /* 与rowHeight一致 */ + box-sizing: border-box; + font-size: 12px; + font-weight: normal; + } + th { + position: sticky; + top: 0; + z-index: 10; /* 确保表头在内容之上 */ + } + tr{ + background: rgb(6,38,87); + + &:nth-of-type(2n){ + background: rgb(19,72,127); + } + } + thead tr{ + background: rgba(0,0,0,0); + } + } + .scroll-viewport { + position: relative; + overflow: hidden; + .scroll-content { + will-change: transform; /* 优化性能 */ + } + .danger { + color: #ff2f2f; + animation: blink 1s infinite; + } + .warning { + color: yellow; + animation: blink 1s infinite; + } + } +} </style> diff --git a/src/views/hazardousChemicals/bigScreen/components/midTop.vue b/src/views/hazardousChemicals/bigScreen/components/midTop.vue index 1c3d309..99e6443 100644 --- a/src/views/hazardousChemicals/bigScreen/components/midTop.vue +++ b/src/views/hazardousChemicals/bigScreen/components/midTop.vue @@ -8,13 +8,31 @@ v-model="companyType" filterable placeholder="请选择企业类型" - style="width: 100%" + style="flex: 1" remote remote-show-suffix :remote-method="getList" > <el-option v-for="item in typeList" + :key="item.id" + :label="item.name" + :value="item.id" + /> + </el-select> + <el-select + clearable + :teleported="false" + v-model="warningType" + filterable + placeholder="请选择风险等级" + style="flex: 1" + remote + remote-show-suffix + :remote-method="getList" + > + <el-option + v-for="item in warningList" :key="item.id" :label="item.name" :value="item.id" @@ -94,6 +112,7 @@ }) const companyType = ref('') +const warningType = ref('') const typeList = [ { id: 0, @@ -108,13 +127,30 @@ name: '中试类' } ] +const warningList = [ + { + id: 1, + name: '红' + }, + { + id: 2, + name: '橙' + }, + { + id: 3, + name: '黄' + }, + { + id: 4, + name: '蓝' + } +] const getList = async () => { const res = await getCompanyMessage(companyType.value) if(res.code == 200){ if(res.data && Array.isArray(res.data) && res.data.length>0){ companyData.value = res.data - console.log(companyData.value,555) const mapData = companyData.value.map(i=>{ return { name: i.companyName + '(' + i.warningCount + ')', @@ -293,6 +329,8 @@ .filter{ width: 300px; margin-top: 50px; + display: flex; + align-items: center; } :deep(.el-input__wrapper){ height: 28px; diff --git a/src/views/hazardousChemicals/bigScreen/components/midTop2.vue b/src/views/hazardousChemicals/bigScreen/components/midTop2.vue new file mode 100644 index 0000000..40d3749 --- /dev/null +++ b/src/views/hazardousChemicals/bigScreen/components/midTop2.vue @@ -0,0 +1,490 @@ +<template> + <div class="charts-container"> + <div class="container-left"> + <div class="filter"> + <el-select + clearable + :teleported="false" + v-model="companyType" + filterable + placeholder="请选择企业类型" + style="flex: 1" + remote + remote-show-suffix + :remote-method="getList" + > + <el-option + v-for="item in typeList" + :key="item.id" + :label="item.name" + :value="item.id" + /> + </el-select> + <el-select + clearable + :teleported="false" + v-model="warningType" + filterable + placeholder="请选择风险等级" + style="flex: 1" + remote + remote-show-suffix + :remote-method="getList" + > + <el-option + v-for="item in warningList" + :key="item.id" + :label="item.name" + :value="item.id" + /> + </el-select> + </div> + <div class="table-wrapper"> + <table class="scrollable-table"> + <thead> + <tr> + <th>序号</th> + <th>企业名称</th> + <th>危化品仓库</th> + <th>预警信息</th> + </tr> + </thead> + </table> + <div class="scroll-viewport" ref="viewport"> + <div class="scroll-content" :style="contentStyle"> + <table class="scrollable-table"> + <tbody> + <tr v-for="(item,index) in companyData" :key="item.id"> + <td>{{ index + 1 }}</td> + <td>{{ item.companyName }}</td> + <td>{{ item.warehouseCount }}</td> + <td>{{ item.warningCount }}</td> + </tr> + <tr v-for="(item,index) in loopData" :key="`loop-${item.id}`"> + <td>{{ index + 1 }}</td> + <td>{{ item.companyName }}</td> + <td>{{ item.warehouseCount }}</td> + <td>{{ item.warningCount }}</td> + </tr> + </tbody> + </table> + </div> + </div> + </div> + </div> + <div id="areaMap"></div> + </div> +</template> +<script setup> +import * as echarts from 'echarts'; +import {onMounted, onBeforeUnmount, ref, computed, reactive} from "vue"; +import SUZHOU from './map.json' +import {getAvoidList} from "@/api/hazardousChemicals/avoid"; +import {ElMessage} from "element-plus"; +import {getCompanyMessage} from "@/api/monitor/screenCharts"; + +// 表格数据 +const companyData = ref([]) +// 配置参数 +const visibleRows = 8 // 显示的行数 +const scrollSpeed = 0.5 // 每次滚动的像素数 +const rowHeight = 40 // 行高,与CSS一致 +const viewport = ref(null) +const scrollPosition = ref(0) +let animationFrame = null + +onMounted(()=>{ + getList() + // 设置视口高度 + if (viewport.value) { + viewport.value.style.height = `${visibleRows * rowHeight}px` + } + // 延迟启动滚动,确保初始渲染完成 + setTimeout(() => { + scrollAnimation() + }, 100) + initMap() + +}) + +onBeforeUnmount(() => { + if (animationFrame) { + cancelAnimationFrame(animationFrame) + } +}) + +const companyType = ref('') +const warningType = ref('') +const typeList = [ + { + id: 0, + name: '研发类' + }, + { + id: 1, + name: '生产类' + }, + { + id: 2, + name: '中试类' + } +] +const warningList = [ + { + id: 1, + name: '红' + }, + { + id: 2, + name: '橙' + }, + { + id: 3, + name: '黄' + }, + { + id: 4, + name: '蓝' + } +] + +const initMap=()=>{ + var map = new BMapGL.Map("areaMap"); + map.setMapType(BMAP_NORMAL_MAP); + var point = new BMapGL.Point(120.752833, 31.333439); + map.centerAndZoom(point, 12); + map.enableScrollWheelZoom(true); + map.setTilt(40); + map.setMapStyleV2({ + styleId: 'c66f44df4e55ce8f0fa90205997df335' + }); + // var bd = new BMapGL.Boundary(); + // bd.get('苏州市工业园区', function (rs) { + // // console.log('外轮廓:', rs.boundaries[0]) + // // console.log('内镂空:', rs.boundaries[1]) + // var hole = new BMapGL.Polygon(rs.boundaries, { + // fillColor: 'blue', + // fillOpacity: 0.2 + // }); + // map.addOverlay(hole); + // }); + var bd1 = new BMapGL.Boundary(); + bd1.get('苏州市工业园区', function (rs) { + let count = rs.boundaries.length; + for (let i = 0; i < count; i++) { + let path = []; + let str = rs.boundaries[i].replace(' ', ''); + let points = str.split(';'); + for (let j = 0; j < points.length; j++) { + let lng = points[j].split(',')[0]; + let lat = points[j].split(',')[1]; + path.push(new BMapGL.Point(lng, lat)); + } + let prism = new BMapGL.Prism(path, 200, { + topFillColor: '#5679ea', + topFillOpacity: 0.6, + sideFillColor: '#5679ea', + sideFillOpacity: 0.9 + }); + map.addOverlay(prism); + } + }); + // var circle = new BMapGL.Circle(new BMapGL.Point(120.742833,31.333439),1000,{strokeColor:"blue", strokeWeight:2, strokeOpacity:0.5}); + // map.addOverlay(circle); + var point = new BMapGL.Point(120.742833,31.333439); + var content = '企业一(10)'; + var label = new BMapGL.Label(content, { + position: point, + offset: new BMapGL.Size(-20, -10) + }) + map.addOverlay(label); + label.setStyle({ + color: '#11FEEE', + fontSize: '14px', + border: 'none', + backgroundColor: '#293075' + }) + label.setPosition(point) +} +const getList = async () => { + const res = await getCompanyMessage(companyType.value) + if(res.code == 200){ + if(res.data && Array.isArray(res.data) && res.data.length>0){ + companyData.value = res.data + const mapData = companyData.value.map(i=>{ + return { + name: i.companyName + '(' + i.warningCount + ')', + value: [i.longitude,i.latitude] + } + }) + // initChart(mapData) + } + + }else{ + ElMessage.warning(res.message) + } +} + +// 复制前几行数据用于循环 +const loopData = computed(() => { + return companyData.value.slice(0, visibleRows) +}) + +// 内容区域样式 +const contentStyle = computed(() => { + return { + transform: `translateY(-${scrollPosition.value}px)` + } +}) + +// 滚动动画 +const scrollAnimation = () => { + const totalHeight = companyData.value.length * rowHeight + const loopHeight = loopData.value.length * rowHeight + // 更新滚动位置 + scrollPosition.value += scrollSpeed + // 当滚动到循环数据部分时,重置位置实现无缝衔接 + if (scrollPosition.value >= totalHeight) { + scrollPosition.value -= totalHeight + } + animationFrame = requestAnimationFrame(scrollAnimation) +} + +const initChart =(mapData)=>{ + //获取echart对象 + let dom = document.getElementById('areaMap') + if (dom) { + //初始化 + let myEchart = echarts.init(dom) + //注册地图 + echarts.registerMap('苏州市', SUZHOU) + let option = { + geo: { + map: '苏州市', + aspectScale: 0.8, + layoutCenter: ['50%', '50%'], //地图位置 + layoutSize: '75%', + itemStyle: { + normal: { + shadowColor: '#000', + shadowOffsetX: 0, + shadowOffsetY: 40, + opacity: 0.1 + }, + emphasis: { + areaColor: '#fff' + } + } + }, + tooltip: { + trigger: 'item', + backgroundColor: 'rgba(166, 200, 76, 0.82)', + borderColor: '#FFFFCC', + showDelay: 0, + hideDelay: 0, + enterable: true, + transitionDuration: 0, + extraCssText: 'z-index:100', + formatter: function (params, ticket, callback) { + console.log('params', params.value); + //根据业务自己拓展要显示的内容 + var res = '' + var name = params.name + var value = params.value[params.seriesIndex + 1] || params.value + res = "<span style='color:#fff;'>" + name + '</span><br/>数据:' + value + return res + } + }, + + series: [ + { + tooltip: { + trigger: 'item', + }, + name: '苏州市数据', + type: 'map', + map: '苏州市', // 自定义扩展图表类型 + label: { // 文字 + show: true, + color: '#fff', + fontSize: 10 + }, + itemStyle: { // 地图样式 + shadowBlur: 10, + shadowColor: '#000', + areaColor: 'rgb(1,95,176)', //区域颜色 + normal: { + areaColor: 'rgb(1,95,176)', + borderColor: '#02CDE6', + borderWidth: 1 + }, + emphasis: { + areaColor: 'rgb(3,26,65)', + label: { + color: '#fff' + } + } + }, + }, + // 区域散点图 + { + type: 'effectScatter', + coordinateSystem: 'geo', + symbolSize: 10, + rippleEffect: { + period: 3, + scale: 10, + brushType: 'fill' + }, + label: { + normal: { + show: true, + position: 'right', + formatter: '{b}', + color: 'yellow', + fontSize: 12 + } + }, + data: mapData, + itemStyle: { + //坐标点颜色 + normal: { + show: true, + color: 'skyblue', + shadowBlur: 20, + shadowColor: '#fff' + }, + emphasis: { + areaColor: '#fff' + } + } + } + ], + } + myEchart.setOption(option); + window.addEventListener('resize', function () { + myEchart.resize(); + }); + } +} + +</script> + +<style lang="postcss" scoped> +.charts-container{ + width: 100%; + height: 100%; + display: flex; + align-items: flex-start; + gap: 10px +} + +.container-left{ + display: flex; + flex-direction: column; + align-items: center; + width: 300px; +} + +.filter{ + width: 300px; + margin-top: 50px; + display: flex; + align-items: center; +} +:deep(.el-input__wrapper){ + height: 28px; + box-shadow: none; + border: 1px solid #11FEEE; + background: rgba(6,24,88,.6); + color: #fff; +} +:deep(.el-input__inner){ + color: #02CDE6; +} +:deep(.el-input .el-select__caret){ + color: #02CDE6 +} +:deep(.el-popper.is-light){ + background: rgb(8,44,97); + .el-select-dropdown__item{ + color: #fff; + &:hover{ + background: #015fb0; + } + } +} + + +.table-wrapper { + position: relative; + width: 300px; + margin-top: 10px; + border: 1px solid rgba(255,255,255,.1); + border-radius: 2px; + overflow: hidden; + + .scrollable-table { + width: 100%; + border-collapse: collapse; + + th,td { + padding: 12px 15px; + color: #fff; + text-align: left; + border-bottom: 1px solid rgba(255,255,255,.1); + height: 40px; /* 与rowHeight一致 */ + box-sizing: border-box; + font-size: 12px; + font-weight: normal; + } + th { + position: sticky; + top: 0; + z-index: 10; /* 确保表头在内容之上 */ + } + tr{ + background: rgb(6,38,87); + + &:nth-of-type(2n){ + background: rgb(19,72,127); + } + } + thead tr{ + background: rgba(0,0,0,0); + } + } + .scroll-viewport { + position: relative; + overflow: hidden; + .scroll-content { + will-change: transform; /* 优化性能 */ + } + .danger { + color: #ff2f2f; + animation: blink 1s infinite; + } + .warning { + color: yellow; + animation: blink 1s infinite; + } + } +} + +@keyframes blink { + 0% { opacity: 1; } + 50% { opacity: 0.8; } + 100% { opacity: 1; } +} + + #areaMap{ + flex: 1; + height: 100%; + } +:deep(.BMap_cpyCtrl) { + display: none !important; +} +:deep(.anchorBL) { + display: none !important; +} +</style> diff --git a/src/views/hazardousChemicals/bigScreen/index.vue b/src/views/hazardousChemicals/bigScreen/index.vue index d052e83..865342c 100644 --- a/src/views/hazardousChemicals/bigScreen/index.vue +++ b/src/views/hazardousChemicals/bigScreen/index.vue @@ -9,10 +9,12 @@ <h1 class="dashboard-title">独墅湖科教创新区危化品智慧管控平台</h1> </div> <div class="header-right"> -<!-- <div class="weather-info">--> + <div class="weather-info"> <!-- <span class="weather-icon">☀</span>--> <!-- <span class="weather-text">晴 26°C</span>--> -<!-- </div>--> + <div class="exit-btn" v-show="!isFull" @click="toFull">全屏</div> + <div class="exit-btn" v-show="isFull" @click="exitFull">退出全屏</div> + </div> </div> </header> @@ -84,12 +86,16 @@ import LeftBottom from "@/views/hazardousChemicals/bigScreen/components/leftBottom"; import MidBottom from "@/views/hazardousChemicals/bigScreen/components/midBottom"; import RightTop from "@/views/hazardousChemicals/bigScreen/components/rightTop"; -import MidTop from "@/views/hazardousChemicals/bigScreen/components/midTop"; +import MidTop from "@/views/hazardousChemicals/bigScreen/components/midTop2"; import RightBottom from "@/views/hazardousChemicals/bigScreen/components/rightBottom"; +import {useRoute, useRouter} from "vue-router"; +const router = useRouter() +const route = useRoute() + const currentTime = ref(''); const currentDate = ref(''); let timer = ref(null); - +const isFull = ref(false) const updateTime = () => { const now = new Date(); currentTime.value = now.toLocaleTimeString(); @@ -106,6 +112,11 @@ window.onresize = () => { setRem();/* 改变窗口大小时重新设置 rem */ } + if(route.path == '/bigScreen'){ + isFull.value = false + }else{ + isFull.value = true + } updateTime(); timer.value = setInterval(updateTime, 1000); }); @@ -113,12 +124,20 @@ onBeforeUnmount(() => { if (timer.value) clearInterval(timer.value); }); + +const toFull = ()=>{ + router.push('/bigMap') +} +const exitFull = ()=>{ + router.push('/bigScreen') +} + </script> <style lang="postcss"> .data-dashboard { width: 100%; - height: calc(100vh - 70px); + height: 100%; display: flex; flex-direction: column; background-color: #031A41; @@ -175,7 +194,11 @@ background: rgba(255, 255, 255, 0.1); padding: 10px 20px; border-radius: 30px; - + .exit-btn{ + cursor: pointer; + color: #02CDE6; + font-size: 14px; + } .weather-icon { font-size: 16px; margin-right: 10px; -- Gitblit v1.9.2