Merge branch 'main' of http://192.168.110.2:3000/taoge/mk_system into szq
This commit is contained in:
275
src/views/projectLarge/ProjectScreen/components/centerPage.vue
Normal file
275
src/views/projectLarge/ProjectScreen/components/centerPage.vue
Normal file
@ -0,0 +1,275 @@
|
||||
<template>
|
||||
<div class="centerPage">
|
||||
<div class="topPage">
|
||||
<newMap :isHide="isHide"></newMap>
|
||||
</div>
|
||||
<div class="endPage" :class="{ 'slide-out-down': isHide }">
|
||||
<Title title="AI安全巡检">
|
||||
<img src="@/assets/projectLarge/robot.svg" alt="" height="20px" width="20px">
|
||||
</Title>
|
||||
<div class="swiper" v-if="inspectionList.length">
|
||||
<div class="arrow" :class="{ 'canUse': canLeft }" @click="swiperClick('left')">
|
||||
<el-icon size="16" :color="canLeft ? 'rgba(29, 214, 255, 1)' : 'rgba(29, 214, 255, 0.3)'">
|
||||
<ArrowLeft />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="swiper_content" ref="swiperContent">
|
||||
<div class="swiper_item" v-for="(item, i) in inspectionList" :key="i">
|
||||
<img :src="item.picture" alt="安全巡检" class="swiper_img">
|
||||
<div class="swiper_date">{{ item.createTime.slice(5, 16) }}</div>
|
||||
<div class="swiper_tip">{{ item.label || '未佩戴安全帽' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="arrow" :class="{ 'canUse': canRight }" @click="swiperClick('right')">
|
||||
<el-icon size="16" :color="canRight ? 'rgba(29, 214, 255, 1)' : 'rgba(29, 214, 255, 0.3)'">
|
||||
<ArrowRight />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, toRefs, getCurrentInstance } from "vue"
|
||||
import Title from './title.vue'
|
||||
import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue'
|
||||
import { getScreenSafetyInspection } from '@/api/projectScreen'
|
||||
import newMap from "./newmap.vue"
|
||||
const { proxy } = getCurrentInstance();
|
||||
const { violation_level_type } = toRefs(proxy?.useDict('violation_level_type'));
|
||||
|
||||
const props = defineProps({
|
||||
isHide: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
projectId: {
|
||||
type: String,
|
||||
default: ""
|
||||
}
|
||||
})
|
||||
|
||||
const inspectionList = ref([{
|
||||
id: "",
|
||||
label: "",
|
||||
picture: "",
|
||||
createTime: ""
|
||||
}])
|
||||
const swiperContent = ref()
|
||||
const swiperItemWidth = ref(100)
|
||||
const canLeft = ref(false)
|
||||
const canRight = ref(true)
|
||||
|
||||
const swiperClick = (direction) => {
|
||||
if (!swiperContent.value) return
|
||||
|
||||
if (direction === 'right') {
|
||||
if (swiperContent.value.scrollLeft >= swiperContent.value.scrollWidth - swiperContent.value.clientWidth) {
|
||||
canRight.value = false
|
||||
canLeft.value = true
|
||||
return
|
||||
}
|
||||
swiperContent.value.scrollLeft += swiperItemWidth.value
|
||||
} else {
|
||||
if (swiperContent.value.scrollLeft <= 0) {
|
||||
canLeft.value = false
|
||||
canRight.value = true
|
||||
return
|
||||
}
|
||||
swiperContent.value.scrollLeft -= swiperItemWidth.value
|
||||
}
|
||||
|
||||
// 更新箭头状态
|
||||
canLeft.value = swiperContent.value.scrollLeft > 0
|
||||
canRight.value = swiperContent.value.scrollLeft < swiperContent.value.scrollWidth - swiperContent.value.clientWidth
|
||||
}
|
||||
|
||||
const getInspectionList = async () => {
|
||||
const res = await getScreenSafetyInspection(props.projectId)
|
||||
const { code, data } = res
|
||||
if (code === 200) {
|
||||
console.log(violation_level_type.value)
|
||||
data.map(item => {
|
||||
item.label = violation_level_type.value.find((i) => i.value === item.violationType)?.label
|
||||
})
|
||||
inspectionList.value = data
|
||||
}
|
||||
}
|
||||
// 创建地球
|
||||
const createEarth = () => {
|
||||
window.YJ.on({
|
||||
ws: true,
|
||||
// host: getIP(), //资源所在服务器地址
|
||||
// username: this.loginForm.username, //用户名 可以不登录(不填写用户名),不登录时无法加载服务端的数据
|
||||
// password: md5pass, //密码 生成方式:md5(用户名_密码)
|
||||
}).then((res) => {
|
||||
let earth = new YJ.YJEarth("earth");
|
||||
window.Earth1 = earth;
|
||||
YJ.Global.openRightClick(window.Earth1);
|
||||
YJ.Global.openLeftClick(window.Earth1);
|
||||
let view = {
|
||||
"position": {
|
||||
"lng": 102.03643298211526,
|
||||
"lat": 34.393586474501,
|
||||
"alt": 11298179.51993155
|
||||
},
|
||||
"orientation": {
|
||||
"heading": 360,
|
||||
"pitch": -89.94481747201486,
|
||||
"roll": 0
|
||||
}
|
||||
}
|
||||
loadBaseMap(earth.viewer)
|
||||
YJ.Global.CesiumContainer(window.Earth1, {
|
||||
compass: false, //罗盘
|
||||
});
|
||||
// YJ.Global.flyTo(earth, view);
|
||||
// YJ.Global.setDefaultView(earth.viewer, view)
|
||||
})
|
||||
}
|
||||
// 加载底图
|
||||
const loadBaseMap = (viewer) => {
|
||||
// 创建瓦片提供器
|
||||
const imageryProvider = new Cesium.UrlTemplateImageryProvider({
|
||||
url: 'https://webst01.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}',
|
||||
// 可选:设置瓦片的格式
|
||||
fileExtension: 'png',
|
||||
// 可选:设置瓦片的范围和级别
|
||||
minimumLevel: 0,
|
||||
maximumLevel: 18,
|
||||
// 可选:设置瓦片的投影(默认为Web Mercator)
|
||||
projection: Cesium.WebMercatorProjection,
|
||||
// 可选:如果瓦片服务需要跨域请求,设置请求头部
|
||||
credit: new Cesium.Credit('卫星图数据来源')
|
||||
});
|
||||
|
||||
// 添加图层到视图
|
||||
const layer = viewer.imageryLayers.addImageryProvider(imageryProvider);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getInspectionList()
|
||||
if (swiperContent.value && swiperContent.value.children.length > 0) {
|
||||
swiperItemWidth.value = swiperContent.value.children[0].clientWidth + 20
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.centerPage {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.topPage,
|
||||
.endPage {
|
||||
width: 100%;
|
||||
padding: 15px 0;
|
||||
border: 1px solid rgba(230, 247, 255, 0.1);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.topPage {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 23px;
|
||||
transition: flex 0.5s ease;
|
||||
}
|
||||
|
||||
.endPage {
|
||||
height: 170px;
|
||||
opacity: 1;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
/* 向下滑出动画 */
|
||||
.slide-out-down {
|
||||
transform: translateY(100%);
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.swiper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
padding: 12px 20px 10px 20px;
|
||||
|
||||
.swiper_content {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
transition: all 0.3s ease-in-out;
|
||||
overflow-x: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.swiper_item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 133px;
|
||||
height: 84px;
|
||||
|
||||
.swiper_img {
|
||||
width: 133px;
|
||||
height: 84px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.swiper_date {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: rgba(230, 247, 255, 1);
|
||||
}
|
||||
|
||||
.swiper_tip {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
padding: 5px 0;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: rgba(230, 247, 255, 1);
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.arrow {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgba(29, 214, 255, 0.3);
|
||||
color: skyblue;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.canUse {
|
||||
border: 1px solid rgba(29, 214, 255, 1);
|
||||
}
|
||||
|
||||
&:hover:not(.canUse) {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
328
src/views/projectLarge/ProjectScreen/components/header.vue
Normal file
328
src/views/projectLarge/ProjectScreen/components/header.vue
Normal file
@ -0,0 +1,328 @@
|
||||
<template>
|
||||
<div class="header">
|
||||
<div class="header_left">
|
||||
<div class="header_left_img">
|
||||
<img src="@/assets/large/secure.png" style="width: 100%; height: 100%" />
|
||||
</div>
|
||||
<div style="font-size: 0.83vw; padding-left: 10px">安全生产天数:</div>
|
||||
<div class="header_left_text">
|
||||
{{ safetyDay }}
|
||||
<span style="font-size: 12px">天</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="title">
|
||||
<div>新能源项目级管理平台</div>
|
||||
<div>Coal Science Construction Management - New Energy Project Level Management Platform</div>
|
||||
</div>
|
||||
<div class="header_right">
|
||||
<div class="top-bar">
|
||||
<!-- 左侧:天气图标 + 日期文字 -->
|
||||
<div class="left-section">
|
||||
<div class="weather-list" @mouseenter="requestPause" @mouseleave="resumeScroll">
|
||||
<div
|
||||
v-for="(item, i) in weatherList"
|
||||
:key="i"
|
||||
class="weather-item"
|
||||
:style="{ transform: `translateY(-${offsetY}px)`, transition: transition }"
|
||||
>
|
||||
<img :src="`/assets/demo/${item.icon}.png`" alt="" />
|
||||
<div>{{ item.weather }}{{ item.tempMin }}°/{{ item.tempMax }}°</div>
|
||||
<div>{{ item.week }}({{ item.date }})</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 分割线 -->
|
||||
<div class="divider">
|
||||
<div class="top-block"></div>
|
||||
<div class="bottom-block"></div>
|
||||
</div>
|
||||
<!-- 右侧:管理系统图标 + 文字 -->
|
||||
<div class="right-section">
|
||||
<img src="@/assets/large/setting.png" alt="设置图标" />
|
||||
<span>管理系统</span>
|
||||
</div>
|
||||
<!-- 分割线 -->
|
||||
<div class="divider">
|
||||
<div class="top-block"></div>
|
||||
<div class="bottom-block"></div>
|
||||
</div>
|
||||
<!-- -->
|
||||
<div class="change" @click="emit('changePage')">
|
||||
<el-icon size="20" v-if="!isFull">
|
||||
<Expand />
|
||||
</el-icon>
|
||||
<el-icon size="20" v-else>
|
||||
<Fold />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import { getScreenSafetyDay, getScreenWeather } from '@/api/projectScreen';
|
||||
|
||||
interface Weather {
|
||||
week: string;
|
||||
date: string;
|
||||
icon: string;
|
||||
weather: string;
|
||||
tempMax: string;
|
||||
tempMin: string;
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
projectId: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
isFull: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['changePage']);
|
||||
|
||||
const safetyDay = ref<number>(0);
|
||||
const weatherList = ref<Weather[]>([]);
|
||||
const timer = ref<number | null>(0);
|
||||
const offsetY = ref<number>(0);
|
||||
const curIndex = ref(0);
|
||||
const transition = ref('transform 0.5s ease');
|
||||
const pendingPause = ref(false);
|
||||
|
||||
/**
|
||||
* 判断当前时间是白天/夜晚
|
||||
*/
|
||||
function judgeDayOrNight(sunRise: string, sunSet: string) {
|
||||
// 将 "HH:MM" 格式转为分钟数(便于计算)
|
||||
const timeToMinutes = (timeStr: any) => {
|
||||
const [hours, minutes] = timeStr.split(':').map(Number);
|
||||
return isNaN(hours) || isNaN(minutes) ? 0 : hours * 60 + minutes;
|
||||
};
|
||||
// 转换日出、日落时间为分钟数
|
||||
const sunRiseMinutes = timeToMinutes(sunRise);
|
||||
const sunSetMinutes = timeToMinutes(sunSet);
|
||||
// 获取当前时间并转为分钟数
|
||||
const now = new Date();
|
||||
const currentMinutes = now.getHours() * 60 + now.getMinutes();
|
||||
// true 白天 false 夜晚
|
||||
return currentMinutes >= sunRiseMinutes && currentMinutes <= sunSetMinutes ? true : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置天气周期滑动
|
||||
*/
|
||||
const setWeatherScroll = () => {
|
||||
curIndex.value += 1;
|
||||
transition.value = 'transform 0.3s ease';
|
||||
offsetY.value = curIndex.value * 60;
|
||||
|
||||
if (curIndex.value === weatherList.value.length - 1) {
|
||||
setTimeout(() => {
|
||||
transition.value = 'none';
|
||||
curIndex.value = 0;
|
||||
offsetY.value = 0;
|
||||
}, 350);
|
||||
}
|
||||
};
|
||||
|
||||
function startScroll() {
|
||||
if (timer.value) clearInterval(timer.value);
|
||||
timer.value = window.setInterval(setWeatherScroll, 5000);
|
||||
}
|
||||
|
||||
function requestPause() {
|
||||
if (timer.value) {
|
||||
clearInterval(timer.value);
|
||||
timer.value = null;
|
||||
}
|
||||
pendingPause.value = true;
|
||||
}
|
||||
|
||||
function resumeScroll() {
|
||||
console.log('resumeScroll');
|
||||
pendingPause.value = false;
|
||||
startScroll();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
/**
|
||||
* 获取安全生产天数
|
||||
*/
|
||||
getScreenSafetyDay(props.projectId).then((res) => {
|
||||
const { data, code } = res;
|
||||
if (code === 200) {
|
||||
safetyDay.value = data.safetyDay;
|
||||
}
|
||||
});
|
||||
/**
|
||||
* 获取近三天天气
|
||||
*/
|
||||
getScreenWeather(props.projectId).then((res) => {
|
||||
const { data, code } = res;
|
||||
if (code === 200) {
|
||||
data.forEach((item) => {
|
||||
if (judgeDayOrNight(item.sunRise, item.sunSet)) {
|
||||
item.weather = item.dayStatus;
|
||||
item.icon = item.dayIcon;
|
||||
} else {
|
||||
item.weather = item.nightStatus;
|
||||
item.icon = item.nightIcon;
|
||||
}
|
||||
});
|
||||
weatherList.value = data;
|
||||
// 多添加第一项 实现无缝衔接
|
||||
weatherList.value = [...weatherList.value, weatherList.value[0]];
|
||||
startScroll();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer.value) {
|
||||
clearInterval(timer.value);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.header {
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
box-sizing: border-box;
|
||||
padding: 10px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.header_left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.header_left_img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
box-sizing: border-box;
|
||||
// padding-right: 10px;
|
||||
}
|
||||
|
||||
.header_left_text {
|
||||
font-size: 1.45vw;
|
||||
font-weight: 700;
|
||||
text-shadow: 0px 1.24px 6.21px rgba(25, 179, 250, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.header_right {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #fff;
|
||||
font-family: 'AlimamaShuHeiTi', sans-serif;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.title > div:first-child {
|
||||
/* 第一个子元素的样式 */
|
||||
font-size: 38px;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.title > div:last-child {
|
||||
/* 最后一个子元素的样式 */
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 顶部栏容器:Flex 水平布局 + 垂直居中 */
|
||||
.top-bar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
color: #fff;
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 左侧区域(天气 + 日期):自身也用 Flex 水平排列,确保元素在一行 */
|
||||
.left-section {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.weather-list {
|
||||
height: 60px;
|
||||
overflow: hidden;
|
||||
|
||||
.weather-item {
|
||||
height: 60px;
|
||||
line-height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
& > div:last-child {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 分割线(视觉分隔,可根据需求调整样式) */
|
||||
.divider {
|
||||
display: grid;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
gap: 2px;
|
||||
padding: 14px 10px;
|
||||
}
|
||||
|
||||
.divider .top-block {
|
||||
width: 2px;
|
||||
height: 7px;
|
||||
background: #19b5fb;
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
.divider .bottom-block {
|
||||
width: 2px;
|
||||
height: 7px;
|
||||
background: #19b5fb;
|
||||
align-self: end;
|
||||
}
|
||||
|
||||
/* 右侧区域(管理系统):图标 + 文字水平排列 */
|
||||
.right-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-family: 'AlimamaShuHeiTi', sans-serif;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.right-section img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 6px;
|
||||
/* 图标与文字间距 */
|
||||
}
|
||||
|
||||
.change {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
margin-right: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
370
src/views/projectLarge/ProjectScreen/components/leftPage.vue
Normal file
370
src/views/projectLarge/ProjectScreen/components/leftPage.vue
Normal file
@ -0,0 +1,370 @@
|
||||
<template>
|
||||
<div class="leftPage">
|
||||
<div class="topPage">
|
||||
<Title title="项目公告" />
|
||||
<div class="content" ref="contentRef" id="event_scroll" @mouseenter="pauseScroll" @mouseleave="resumeScroll">
|
||||
<div class="content_item" v-for="item in news" :key="item.id" @click="showNewsDetail(item)">
|
||||
<img src="@/assets/projectLarge/round.svg" alt="">
|
||||
<div class="ellipsis">
|
||||
{{ item.title }}
|
||||
<span style="color: rgba(138, 149, 165, 1);">{{ item.id === newId ? '关闭' :
|
||||
'查看' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detailBox" :class="{ 'show': newId }">
|
||||
<!-- <div class="detail_title">{{ newDetail.title }}</div> -->
|
||||
<div class="detail_content" v-html="newDetail.content"></div>
|
||||
<div class="close" @click="newId = ''">
|
||||
<CircleClose style="width: 1.2em; height: 1.2em;" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="endPage">
|
||||
<Title title="人员情况" />
|
||||
<div class="map">
|
||||
<img src="@/assets/projectLarge/map.svg" alt="">
|
||||
<!-- <div ref="mapChartRef"></div> -->
|
||||
</div>
|
||||
|
||||
<div class="attendance_tag">
|
||||
<div class="tag_item">
|
||||
<img src="@/assets/projectLarge/people.svg" alt="">
|
||||
<div class="tag_title">出勤人</div>
|
||||
<div class="tag_info">
|
||||
{{ attendanceCount }}
|
||||
<span style="font-size: 14px;">人</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tag_item">
|
||||
<img src="@/assets/projectLarge/people.svg" alt="">
|
||||
<div class="tag_title">在岗人</div>
|
||||
<div class="tag_info">
|
||||
{{ peopleCount }}
|
||||
<span style="font-size: 14px;">人</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tag_item">
|
||||
<img src="@/assets/projectLarge/people.svg" alt="">
|
||||
<div class="tag_title">出勤率</div>
|
||||
<div class="tag_info">
|
||||
{{ attendanceRate }}
|
||||
<span style="font-size: 14px;">%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="attendance_list">
|
||||
<div class="attendance_item subfont">
|
||||
<div class="attendance_item_title"></div>
|
||||
<div class="attendance_item_title">在岗人数</div>
|
||||
<div class="attendance_item_title">出勤率</div>
|
||||
<div class="attendance_item_title">出勤时间</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="attendance_list scroll">
|
||||
<div v-for="item in teamAttendanceList" :key="item.id" class="attendance_item">
|
||||
<div class="attendance_item_title">{{ item.teamName }}</div>
|
||||
<div class="attendance_item_number">{{ item.attendanceNumber }} <span class="subfont">人/{{ item.allNumber
|
||||
}}</span></div>
|
||||
<div class="attendance_item_rate">{{ item.attendanceRate.toFixed(2) }} %</div>
|
||||
<div class="attendance_item_date subfont">{{ item.attendanceTime }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue"
|
||||
import Title from './title.vue'
|
||||
import { getScreenNews, getScreenPeople } from '@/api/projectScreen';
|
||||
|
||||
const props = defineProps({
|
||||
projectId: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const contentRef = ref<HTMLDivElement | null>(null);
|
||||
const news = ref([])
|
||||
const newDetail = ref({
|
||||
title: '',
|
||||
content: ''
|
||||
})
|
||||
const newId = ref('')
|
||||
const attendanceCount = ref(0)
|
||||
const attendanceRate = ref(0)
|
||||
const peopleCount = ref(0)
|
||||
const teamAttendanceList = ref([
|
||||
{ id: "", teamName: "", attendanceNumber: 0, allNumber: 0, attendanceRate: 0, attendanceTime: "" },
|
||||
])
|
||||
|
||||
/**
|
||||
* 显示新闻详情
|
||||
*/
|
||||
const showNewsDetail = (item: any) => {
|
||||
if (newId.value === item.id) {
|
||||
newId.value = ''
|
||||
return
|
||||
}
|
||||
newDetail.value = item
|
||||
newId.value = item.id
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目人员出勤数据
|
||||
*/
|
||||
const getPeopleData = async () => {
|
||||
const res = await getScreenPeople(props.projectId);
|
||||
const { data, code } = res
|
||||
if (code === 200) {
|
||||
attendanceCount.value = data.attendanceCount
|
||||
attendanceRate.value = data.attendanceRate
|
||||
peopleCount.value = data.peopleCount
|
||||
teamAttendanceList.value = data.teamAttendanceList
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目新闻数据
|
||||
*/
|
||||
const getNewsData = async () => {
|
||||
const res = await getScreenNews(props.projectId);
|
||||
const { data, code } = res
|
||||
if (code === 200) {
|
||||
news.value = data
|
||||
requestAnimationFrame((timestamp) => autoScrollTable(timestamp));
|
||||
}
|
||||
}
|
||||
|
||||
var lastTime = 0;
|
||||
var scrolltimerTable = null
|
||||
var rotate = false
|
||||
|
||||
const autoScrollTable = (time: number) => {
|
||||
const divData = document.getElementById('event_scroll');
|
||||
|
||||
if (time - lastTime < 25) {
|
||||
scrolltimerTable = requestAnimationFrame(autoScrollTable);
|
||||
return; // 如果时间未到,则返回,不执行动画更新
|
||||
}
|
||||
lastTime = time;
|
||||
if (rotate) {
|
||||
divData.scrollTop -= 1;
|
||||
} else {
|
||||
divData.scrollTop += 1;
|
||||
}
|
||||
if (divData.clientHeight + divData.scrollTop == divData.scrollHeight) {
|
||||
rotate = true
|
||||
setTimeout(() => {
|
||||
scrolltimerTable = requestAnimationFrame(autoScrollTable);
|
||||
}, 1000);
|
||||
} else if (divData.scrollTop == 0) {
|
||||
rotate = false
|
||||
setTimeout(() => {
|
||||
scrolltimerTable = requestAnimationFrame(autoScrollTable);
|
||||
}, 1000);
|
||||
} else {
|
||||
scrolltimerTable = requestAnimationFrame(autoScrollTable);
|
||||
}
|
||||
};
|
||||
|
||||
// 暂停滚动
|
||||
const pauseScroll = () => {
|
||||
if (scrolltimerTable) {
|
||||
cancelAnimationFrame(scrolltimerTable);
|
||||
scrolltimerTable = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 恢复滚动
|
||||
const resumeScroll = () => {
|
||||
if (!scrolltimerTable) {
|
||||
requestAnimationFrame((timestamp) => autoScrollTable(timestamp));
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getPeopleData()
|
||||
getNewsData()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.leftPage {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.topPage,
|
||||
.endPage {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 15px 0;
|
||||
border: 1px solid rgba(29, 214, 255, 0.1);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.endPage {
|
||||
flex: 1;
|
||||
margin-top: 23px;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
max-height: 100px;
|
||||
margin: 0 15px;
|
||||
padding: 0 10px;
|
||||
margin-top: 15px;
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: rgba(204, 204, 204, 0.1);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(29, 214, 255, 0.78);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.content_item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: rgba(230, 247, 255, 1);
|
||||
cursor: pointer;
|
||||
|
||||
.ellipsis {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
img {
|
||||
margin-top: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.map {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.attendance_tag {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 30px;
|
||||
margin-top: 15px;
|
||||
|
||||
.tag_item {
|
||||
width: 28%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
border: 1px dashed rgba(29, 214, 255, 0.3);
|
||||
padding: 10px;
|
||||
|
||||
.tag_info {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: rgba(230, 247, 255, 1);
|
||||
text-shadow: 0px 1.24px 6.21px rgba(0, 190, 247, 1);
|
||||
}
|
||||
|
||||
.tag_title {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: rgba(230, 247, 255, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.attendance_list {
|
||||
padding: 0px 30px;
|
||||
font-size: 14px;
|
||||
|
||||
.attendance_item {
|
||||
display: grid;
|
||||
grid-template-columns: 3fr 2fr 2fr 3fr;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
&.scroll {
|
||||
margin-top: 10px;
|
||||
height: 280px;
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: rgba(204, 204, 204, 0.1);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(29, 214, 255, 0.78);
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.subfont {
|
||||
color: rgba(138, 149, 165, 1);
|
||||
}
|
||||
|
||||
.detailBox {
|
||||
position: absolute;
|
||||
left: 20vw;
|
||||
top: 0;
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
padding: 10px 15px;
|
||||
box-sizing: border-box;
|
||||
background: rgba(138, 157, 161, 0.5);
|
||||
border: 2px dashed rgba(29, 214, 255, 0.3);
|
||||
border-right: none;
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s ease;
|
||||
opacity: 0;
|
||||
z-index: -1;
|
||||
|
||||
&.show {
|
||||
left: 25vw;
|
||||
opacity: 1;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.close {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
313
src/views/projectLarge/ProjectScreen/components/newmap.vue
Normal file
313
src/views/projectLarge/ProjectScreen/components/newmap.vue
Normal file
@ -0,0 +1,313 @@
|
||||
<script setup>
|
||||
import { onMounted, ref, onUnmounted,defineProps} from 'vue';
|
||||
import CesiumImageLabelEntity from '../js/CesiumImageLabelEntity.js';
|
||||
import CesiumFlyToRoamingController from '../js/CesiumFlyToRoamingController.js';
|
||||
import { setSelect, getSelectList, getGps } from '@/api/projectScreen/index.ts'
|
||||
import videoDialog from "./video.vue"
|
||||
import { getToken } from '@/utils/auth';
|
||||
const defaultExpandedKeys = [1, 2, 3] //默认展开第一级节点
|
||||
const defaultCheckedKeys = ref([]) //默认选中节点
|
||||
const data = ref([]);
|
||||
const deviceId = ref('');
|
||||
const videoDialogRef = ref(null);
|
||||
let token = 'Bearer '+ getToken()
|
||||
let ws = new ReconnectingWebSocket( import.meta.env.VITE_APP_BASE_WS_API + '?Authorization='+token+'&clientid='+import.meta.env.VITE_APP_CLIENT_ID+'&projectId='+'1897160897167638529');
|
||||
// 连接ws
|
||||
const connectWs = () => {
|
||||
ws.onopen = (e) => {
|
||||
// let message ={
|
||||
// projectId:'1897160897167638529',
|
||||
// }
|
||||
// ws.send(JSON.stringify(message));
|
||||
ws.onmessage = (e) => {
|
||||
console.log('ws', e);
|
||||
};
|
||||
};
|
||||
}
|
||||
const props = defineProps({
|
||||
isHide:{
|
||||
type:Boolean,
|
||||
default:true,
|
||||
}
|
||||
})
|
||||
console.log('props', props);
|
||||
|
||||
const defaultProps = {
|
||||
children: 'children',
|
||||
label: 'label',
|
||||
}
|
||||
let entityManager = null;
|
||||
window.deviceMap = new Map();
|
||||
let list = ref([]);
|
||||
// 漫游实例
|
||||
let roamingController = null;
|
||||
// 获取GPS数据
|
||||
function getGpsData() {
|
||||
getGps('1897160897167638529').then(res => {
|
||||
console.log('res', res);
|
||||
if (res.code === 200) {
|
||||
data.value = res.data;
|
||||
if (res.data.length > 0) {
|
||||
res.data.forEach(element => {
|
||||
list.value = [...list.value, ...element.children]
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
// 设置选中节点
|
||||
function setCheckedNode(idList) {
|
||||
let obj = {
|
||||
projectId: '1897160897167638529',
|
||||
idList
|
||||
}
|
||||
setSelect(obj).then(res => {
|
||||
console.log('res', res);
|
||||
})
|
||||
}
|
||||
// 获取选中节点
|
||||
function getCheckedNode() {
|
||||
getSelectList({
|
||||
projectId: '1897160897167638529'
|
||||
}).then(res => {
|
||||
if (res.code == 200) {
|
||||
defaultCheckedKeys.value = res.data || []
|
||||
}
|
||||
})
|
||||
}
|
||||
// 渲染无人机、摄像头、定位设备
|
||||
function renderDevice(item) {
|
||||
const imageEntity = new CesiumImageLabelEntity(Earth1.viewer, {
|
||||
id: item.id,
|
||||
position: {
|
||||
lng: Number(item.lng),
|
||||
lat: Number(item.lat),
|
||||
height: 0
|
||||
},
|
||||
imageWidth: 64,
|
||||
imageHeight: 64,
|
||||
name: item.label || item.id,
|
||||
imageUrl: `/image/${item.type}.png`,
|
||||
onClick: (entity)=>{
|
||||
entityClickHandler(entity,item);
|
||||
}
|
||||
});
|
||||
window.deviceMap.set(item.id, imageEntity);
|
||||
}
|
||||
// 实体的点击事件
|
||||
function entityClickHandler(entity,item) {
|
||||
console.log('entity', entity,item);
|
||||
if (item.type == 'camera') {
|
||||
deviceId.value = 'AE9470016';
|
||||
videoDialogRef.value.show();
|
||||
videoDialogRef.value.videoPlay(deviceId.value);
|
||||
}
|
||||
}
|
||||
// 初始化地球
|
||||
function initEarth() {
|
||||
YJ.on({
|
||||
ws: true,
|
||||
host: '', //资源所在服务器地址
|
||||
username: '', //用户名 可以不登录(不填写用户名),不登录时无法加载服务端的数据
|
||||
password: '', //密码 生成方式:md5(用户名_密码)
|
||||
}).then((res) => {
|
||||
let earth = new YJ.YJEarth("earth");
|
||||
|
||||
window.Earth1 = earth;
|
||||
// 加载底图
|
||||
// earth.viewer.terrainProvider = Cesium.createWorldTerrain();
|
||||
// Earth1.viewer
|
||||
addArcgisLayer(Earth1.viewer, 'img_w')
|
||||
// 添加倾斜数据
|
||||
// loadTiltData(Earth1.viewer)
|
||||
// 获取中心点
|
||||
YJ.Global.CesiumContainer(window.Earth1, {
|
||||
compass: false,//罗盘
|
||||
legend: false, //图例
|
||||
});
|
||||
// 创建实体管理器实例
|
||||
list.value.forEach(item => {
|
||||
if (defaultCheckedKeys.value.includes(item.id)) {
|
||||
console.log("defaultCheckedKeys", item.id);
|
||||
renderDevice(item)
|
||||
}
|
||||
});
|
||||
roamingController = new CesiumFlyToRoamingController(window.Earth1.viewer, {
|
||||
duration: 5, // 每个点之间飞行5秒
|
||||
pitch: -89 // 20度俯角
|
||||
});
|
||||
window.roamingController = roamingController;
|
||||
})
|
||||
}
|
||||
// 加载倾斜数据
|
||||
function loadTiltData(viewer) {
|
||||
viewer.terrainProvider = new Cesium.CesiumTerrainProvider({
|
||||
// url: 'http://192.168.110.2:8895/yjearth4.0/data/pak/e904acb32aaa8b872c64866ebaaaf5e2',
|
||||
// url:"http://58.17.134.85:7363/yjearth4.0/data/pak/e904acb32aaa8b872c64866ebaaaf5e2"
|
||||
url: import.meta.env.VITE_EARTH_URL + "/yjearth4.0/data/pak/4eb21d3fc02873092e75640e261544b3"
|
||||
});
|
||||
}
|
||||
// 获取ArcGIS服务的URL
|
||||
function getArcGisUrlByType(type) {
|
||||
switch (type) {
|
||||
//影像
|
||||
case "img_w":
|
||||
return "https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer";
|
||||
//电子
|
||||
case "vec_w":
|
||||
return "https://services.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer";
|
||||
//蓝色底图
|
||||
case "vec_blue":
|
||||
return "http://map.geoq.cn/arcgis/rest/services/ChinaOnlineStreetPurplishBlue/MapServer";
|
||||
//灰色底图
|
||||
case "vec_gray":
|
||||
return "http://map.geoq.cn/arcgis/rest/services/ChinaOnlineStreetGray/MapServer";
|
||||
//暖色底图
|
||||
case "vec_warm":
|
||||
return "http://map.geoq.cn/arcgis/rest/services/ChinaOnlineStreetWarm/MapServer";
|
||||
}
|
||||
}
|
||||
// 添加ArcGIS图层
|
||||
function addArcgisLayer(viewer, type) {
|
||||
let url = getArcGisUrlByType(type)
|
||||
const layerProvider = new Cesium.ArcGisMapServerImageryProvider({
|
||||
url: url
|
||||
});
|
||||
viewer.imageryLayers.addImageryProvider(layerProvider);
|
||||
}
|
||||
// 节点单击事件
|
||||
function handleNodeClick(data) {
|
||||
console.log('data', data);
|
||||
let entity = window.deviceMap.get(data.id);
|
||||
if (entity) {
|
||||
entity.flyTo();
|
||||
}
|
||||
}
|
||||
// 复选框选中事件
|
||||
function handleCheck(checkedNodes, nodes) {
|
||||
console.log('check', checkedNodes, nodes);
|
||||
// 处理单个节点的通用函数
|
||||
const handleNode = (node) => {
|
||||
if (!window.deviceMap.has(node.id)) {
|
||||
console.log("defaultCheckedKeys", node.id);
|
||||
renderDevice(node);
|
||||
} else {
|
||||
const device = window.deviceMap.get(node.id);
|
||||
// 根据当前显示状态切换显示/隐藏
|
||||
device[device.entity.show ? 'hide' : 'show']();
|
||||
}
|
||||
};
|
||||
|
||||
// 处理选中的节点(可能是单个节点或包含子节点的集合)
|
||||
if (checkedNodes?.children?.length) {
|
||||
console.log('children', checkedNodes.children);
|
||||
checkedNodes.children.forEach(handleNode);
|
||||
} else {
|
||||
handleNode(checkedNodes);
|
||||
}
|
||||
|
||||
setCheckedNode(nodes.checkedKeys);
|
||||
}
|
||||
// 开始漫游
|
||||
function startRoaming() {
|
||||
if (roamingController) {
|
||||
roamingController.startPathRoaming([
|
||||
Cesium.Cartesian3.fromDegrees(106.49556855602525, 29.534393226355515, 200),
|
||||
Cesium.Cartesian3.fromDegrees(106.49142431645038, 29.534472802500083, 200),
|
||||
Cesium.Cartesian3.fromDegrees(106.49142125177437, 29.541881138875755, 200)
|
||||
], 3, false);
|
||||
} else {
|
||||
console.log('请先初始化地球');
|
||||
}
|
||||
}
|
||||
// 停止漫游
|
||||
function stopRoaming() {
|
||||
if (roamingController) {
|
||||
roamingController.stopRoaming();
|
||||
} else {
|
||||
console.log('请先初始化地球');
|
||||
}
|
||||
}
|
||||
onMounted(() => {
|
||||
// 连接ws
|
||||
connectWs();
|
||||
// 获取选中节点
|
||||
getCheckedNode();
|
||||
// 获取GPS数据
|
||||
getGpsData();
|
||||
// 初始化地球
|
||||
initEarth();
|
||||
});
|
||||
onUnmounted(() => {
|
||||
window.deviceMap.forEach((item) => {
|
||||
item.destroy();
|
||||
})
|
||||
window.deviceMap.clear();
|
||||
window.roamingController.destroy();
|
||||
window.Earth1.destroy();
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div class="earth-container-big">
|
||||
<div class="earth" id="earth"></div>
|
||||
<div v-show="isHide" class="left">
|
||||
<div style="width: 100%;height: 100%;">
|
||||
<el-button type="primary" @click="startRoaming">开始漫游</el-button>
|
||||
<el-button type="primary" @click="stopRoaming">停止漫游</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="isHide" class="right">
|
||||
<el-tree show-checkbox :data="data" :props="defaultProps" node-key="id" :expand-on-click-node="false"
|
||||
:check-on-click-node="false" :check-on-click-leaf="false" :default-expanded-keys="defaultExpandedKeys"
|
||||
:default-checked-keys="defaultCheckedKeys" @check="handleCheck" @node-click="handleNodeClick" />
|
||||
</div>
|
||||
<videoDialog :data="deviceId" ref="videoDialogRef"></videoDialog>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
.earth-container-big {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.earth {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.right {
|
||||
top: 50%;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.left {
|
||||
top: 50%;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.right,
|
||||
.left {
|
||||
position: absolute;
|
||||
width: 400px;
|
||||
height: 100%;
|
||||
transform: translateY(-50%);
|
||||
background-color: #00000052;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
z-index: 10;
|
||||
|
||||
.el-tree {
|
||||
background-color: transparent;
|
||||
--el-tree-node-hover-bg-color: transparent;
|
||||
--el-tree-text-color: #fff;
|
||||
.el-text {
|
||||
color: azure;
|
||||
}
|
||||
|
||||
.el-tree-node__content:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
153
src/views/projectLarge/ProjectScreen/components/optionList.ts
Normal file
153
src/views/projectLarge/ProjectScreen/components/optionList.ts
Normal file
@ -0,0 +1,153 @@
|
||||
export let pieOption = {
|
||||
// 定义中心文字
|
||||
graphic: [
|
||||
{
|
||||
type: 'text',
|
||||
left: 'center',
|
||||
top: '40%',
|
||||
style: {
|
||||
// 需要从接口替换
|
||||
text: '70%',
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
fill: '#fff'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
left: 'center',
|
||||
top: '50%',
|
||||
style: {
|
||||
text: '总进度',
|
||||
fontSize: 14,
|
||||
fill: '#fff'
|
||||
}
|
||||
},
|
||||
],
|
||||
legend: {
|
||||
show: true,
|
||||
type: 'plain',
|
||||
bottom: 0,
|
||||
itemWidth: 12,
|
||||
itemHeight: 12,
|
||||
textStyle: {
|
||||
color: '#fff'
|
||||
}
|
||||
},
|
||||
series: {
|
||||
type: 'pie',
|
||||
data: [],
|
||||
radius: [50, 80],
|
||||
center: ['50%', '50%'],
|
||||
itemStyle: {
|
||||
borderColor: '#fff',
|
||||
borderWidth: 1
|
||||
},
|
||||
label: {
|
||||
alignTo: 'edge',
|
||||
formatter: '{name|{b}}\n{percent|{c} %}',
|
||||
minMargin: 10,
|
||||
edgeDistance: 20,
|
||||
lineHeight: 15,
|
||||
rich: {
|
||||
name: {
|
||||
fontSize: 12,
|
||||
color: '#fff'
|
||||
},
|
||||
percent: {
|
||||
fontSize: 12,
|
||||
color: '#fff'
|
||||
}
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
top: 'bottom'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export let barOption = {
|
||||
legend: {
|
||||
icon: 'rect',
|
||||
itemWidth: 12,
|
||||
itemHeight: 12,
|
||||
// 调整文字与图标间距
|
||||
data: ['计划流转面积', '已流转面积'],
|
||||
top: 10,
|
||||
right: 20,
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
}
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['地块1', '地块2', '地块3', '地块4', '地块5', '地块6'],
|
||||
axisLabel: {
|
||||
color: '#fff'
|
||||
},
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
splitLine: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
name: '单位:m²',
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
formatter: '{value}'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
bottom: 0, // 距离容器底部的距离
|
||||
containLabel: true // 确保坐标轴标签不被裁剪
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '计划流转面积',
|
||||
type: 'bar',
|
||||
data: [],
|
||||
barWidth: '20%',
|
||||
itemStyle: {
|
||||
color: 'rgb(29, 253, 253)'
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '已流转面积',
|
||||
type: 'bar',
|
||||
data: [],
|
||||
barWidth: '20%',
|
||||
itemStyle: {
|
||||
color: 'rgb(25, 181, 251)'
|
||||
},
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export let mapOption = {
|
||||
geo: {
|
||||
map: 'ch',
|
||||
roam: true,
|
||||
aspectScale: Math.cos((47 * Math.PI) / 180),
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'graph',
|
||||
coordinateSystem: 'geo',
|
||||
data: [
|
||||
{ name: 'a', value: [7.667821250000001, 46.791734269956265] },
|
||||
{ name: 'b', value: [7.404848750000001, 46.516308805996054] },
|
||||
{ name: 'c', value: [7.376673125000001, 46.24728858538375] },
|
||||
{ name: 'd', value: [8.015320625000001, 46.39460918238572] },
|
||||
{ name: 'e', value: [8.616400625, 46.7020608630855] },
|
||||
{ name: 'f', value: [8.869981250000002, 46.37539345234199] },
|
||||
{ name: 'g', value: [9.546196250000001, 46.58676648282309] },
|
||||
{ name: 'h', value: [9.311399375, 47.182454114178896] },
|
||||
{ name: 'i', value: [9.085994375000002, 47.55395822835779] },
|
||||
{ name: 'j', value: [8.653968125000002, 47.47709530818285] },
|
||||
{ name: 'k', value: [8.203158125000002, 47.44506909144329] }
|
||||
],
|
||||
}
|
||||
]
|
||||
};
|
||||
215
src/views/projectLarge/ProjectScreen/components/rightPage.vue
Normal file
215
src/views/projectLarge/ProjectScreen/components/rightPage.vue
Normal file
@ -0,0 +1,215 @@
|
||||
<template>
|
||||
<div class="leftPage">
|
||||
<div class="topPage">
|
||||
<Title title="项目概况" />
|
||||
<div class="content" v-html="generalize"></div>
|
||||
</div>
|
||||
<div class="midPage">
|
||||
<Title title="形象进度" />
|
||||
<div ref="pieChartRef" class="echart" />
|
||||
</div>
|
||||
<div class="endPage">
|
||||
<Title title="土地流转情况" />
|
||||
<div ref="lineChartRef" class="echart" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, nextTick } from "vue"
|
||||
import Title from './title.vue'
|
||||
import * as echarts from 'echarts';
|
||||
import { pieOption, barOption } from './optionList';
|
||||
import { getScreenLand, getScreenImgProcess, getScreenGeneralize } from '@/api/projectScreen';
|
||||
|
||||
const props = defineProps({
|
||||
projectId: {
|
||||
type: String,
|
||||
default: 0
|
||||
}
|
||||
})
|
||||
|
||||
const generalize = ref()
|
||||
// 饼图相关
|
||||
const pieChartRef = ref<HTMLDivElement | null>(null);
|
||||
let pieChart: any = null;
|
||||
const totalPercent = ref(0)
|
||||
// 折线图相关
|
||||
const lineChartRef = ref<HTMLDivElement | null>(null);
|
||||
let lineChart: any = null;
|
||||
// 土地数据 折线图
|
||||
const designAreaData = ref([])
|
||||
const transferAreaData = ref([])
|
||||
// 饼图数据
|
||||
const pieData = [
|
||||
{ label: 'areaPercentage', name: '厂区', value: 0 },
|
||||
{ label: 'roadPercentage', name: '道路', value: 0 },
|
||||
{ label: 'collectorLinePercentage', name: '集电线路', value: 0 },
|
||||
{ label: 'exportLinePercentage', name: '送出线路', value: 0 },
|
||||
{ label: 'substationPercentage', name: '升压站', value: 0 },
|
||||
{ label: 'boxTransformerPercentage', name: '箱变', value: 0 },
|
||||
]
|
||||
|
||||
// 初始化饼图
|
||||
const initPieChart = () => {
|
||||
if (!pieChartRef.value) {
|
||||
console.error('未找到饼图容器元素');
|
||||
return;
|
||||
}
|
||||
pieOption.series.data = pieData
|
||||
pieOption.graphic[0].style.text = totalPercent.value + '%'
|
||||
pieChart = echarts.init(pieChartRef.value, null, {
|
||||
renderer: 'canvas',
|
||||
useDirtyRect: false
|
||||
});
|
||||
pieChart.setOption(pieOption);
|
||||
}
|
||||
|
||||
// 初始化折线图
|
||||
const initLineChart = () => {
|
||||
if (!lineChartRef.value) {
|
||||
console.error('未找到折线图容器元素');
|
||||
return;
|
||||
}
|
||||
barOption.series[0].data = designAreaData.value
|
||||
barOption.series[1].data = transferAreaData.value
|
||||
lineChart = echarts.init(lineChartRef.value, null, {
|
||||
renderer: 'canvas',
|
||||
useDirtyRect: false
|
||||
});
|
||||
lineChart.setOption(barOption);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目土地统计数据
|
||||
*/
|
||||
const getScreenLandData = async () => {
|
||||
const res = await getScreenLand(props.projectId);
|
||||
const { data, code } = res
|
||||
if (code === 200) {
|
||||
designAreaData.value = data.map((item: any) => Number(item.designArea))
|
||||
transferAreaData.value = data.map((item: any) => Number(item.transferArea))
|
||||
initLineChart();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目形象进度数据
|
||||
*/
|
||||
const getScreenImgProcessData = async () => {
|
||||
const res = await getScreenImgProcess(props.projectId);
|
||||
const { data, code } = res
|
||||
if (code === 200) {
|
||||
totalPercent.value = data.totalPercentage
|
||||
pieData.forEach((item: any) => {
|
||||
item.value = data[item.label]
|
||||
})
|
||||
initPieChart()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目概况数据
|
||||
*/
|
||||
const getScreenGeneralizeData = async () => {
|
||||
const res = await getScreenGeneralize(props.projectId);
|
||||
const { data, code } = res
|
||||
if (code === 200) {
|
||||
generalize.value = data
|
||||
}
|
||||
}
|
||||
|
||||
// 响应窗口大小变化
|
||||
const handleResize = () => {
|
||||
if (pieChart) pieChart.resize();
|
||||
if (lineChart) lineChart.resize();
|
||||
};
|
||||
|
||||
// 组件挂载时初始化图表
|
||||
onMounted(() => {
|
||||
getScreenLandData()
|
||||
getScreenImgProcessData()
|
||||
getScreenGeneralizeData()
|
||||
nextTick(() => {
|
||||
initPieChart();
|
||||
window.addEventListener('resize', handleResize);
|
||||
});
|
||||
});
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
if (pieChart) {
|
||||
pieChart.dispose();
|
||||
pieChart = null;
|
||||
}
|
||||
if (lineChart) {
|
||||
lineChart.dispose();
|
||||
lineChart = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.leftPage {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.topPage,
|
||||
.midPage,
|
||||
.endPage {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 15px 0;
|
||||
border: 1px solid rgba(29, 214, 255, 0.1);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.endPage,
|
||||
.midPage {
|
||||
flex: 1;
|
||||
margin-top: 23px;
|
||||
|
||||
.echart {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
height: 100px;
|
||||
margin: 0 15px;
|
||||
padding: 0 10px;
|
||||
margin-top: 15px;
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: rgba(204, 204, 204, 0.1);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(29, 214, 255, 0.78);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.content_item {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: rgba(230, 247, 255, 1);
|
||||
margin-bottom: 10px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.subfont {
|
||||
color: rgba(138, 149, 165, 1);
|
||||
}
|
||||
</style>
|
||||
45
src/views/projectLarge/ProjectScreen/components/title.vue
Normal file
45
src/views/projectLarge/ProjectScreen/components/title.vue
Normal file
@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div class="title">
|
||||
<div class="title_icon">
|
||||
<img src="@/assets/projectLarge/section.svg" alt="">
|
||||
<img src="@/assets/projectLarge/border.svg" alt="">
|
||||
</div>
|
||||
<div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div>{{ title }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: '标题'
|
||||
},
|
||||
prefix: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.title {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
font-family: 'AlimamaShuHeiTi', sans-serif;
|
||||
|
||||
.title_icon {
|
||||
position: relative;
|
||||
|
||||
&>img:last-child {
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
109
src/views/projectLarge/ProjectScreen/components/video.vue
Normal file
109
src/views/projectLarge/ProjectScreen/components/video.vue
Normal file
@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="摄像头直播" :width="dialogWidth" :height="dialogHeight" :before-close="handleClose"
|
||||
destroy-on-close>
|
||||
<div class="video-container-entity" id="video-container-entity" style="width: 100%; height: 600px"></div>
|
||||
<!-- <template #footer>
|
||||
<el-button type="primary" @click="handlesubmit">确定</el-button>
|
||||
<el-button type="danger" @click="handleClose">关闭</el-button>
|
||||
</template> -->
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, defineEmits, defineProps,onUnmounted } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { getAccessToken } from '@/api/other/ys7Device';
|
||||
import EZUIKit from 'ezuikit-js';
|
||||
const emit = defineEmits(['send-data', 'close']);
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => { }
|
||||
}
|
||||
});
|
||||
// 弹窗控制变量
|
||||
const visible = ref(false);
|
||||
const dialogWidth = ref('950px');
|
||||
const dialogHeight = ref('90%');
|
||||
let loading = ref(true);
|
||||
const flvPlayer = ref(null);
|
||||
const show = () => {
|
||||
visible.value = true;
|
||||
}
|
||||
// 关闭弹窗
|
||||
const handleClose = () => {
|
||||
visible.value = false;
|
||||
};
|
||||
// 处理数据传递
|
||||
const handlesubmit = () => {
|
||||
handleClose();
|
||||
};
|
||||
// 视频播放
|
||||
function videoPlay(deviceSerial) {
|
||||
getAccessToken().then((res) => {
|
||||
if (res.code == 200 && deviceSerial) {
|
||||
flvPlayer.value = new EZUIKit.EZUIKitPlayer({
|
||||
audio: '0',
|
||||
id: 'video-container-entity',
|
||||
accessToken: res.data,
|
||||
url: `ezopen://open.ys7.com/${deviceSerial}/1.hd.live`,
|
||||
template: 'pcLive',
|
||||
width: 870,
|
||||
height: 600,
|
||||
plugin: ['talk'],
|
||||
handleError: function (err) {
|
||||
console.log(err);
|
||||
if (err?.data?.ret === 20020) {
|
||||
// 20020 是并发连接限制的错误码
|
||||
ElMessage.error('当前观看人数已达上限,请稍后再试');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 组件挂载时设置容器ID
|
||||
onMounted(() => {
|
||||
|
||||
});
|
||||
|
||||
// 暴露显示方法给父组件
|
||||
defineExpose({
|
||||
show,
|
||||
videoPlay
|
||||
});
|
||||
|
||||
//
|
||||
onUnmounted(() => {
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.earth-container {
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:deep(.el-dialog__body) {
|
||||
padding: 10px;
|
||||
height: calc(100% - 100px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.el-dialog) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 90vh;
|
||||
}
|
||||
|
||||
:deep(.el-dialog__content) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
107
src/views/projectLarge/ProjectScreen/index.vue
Normal file
107
src/views/projectLarge/ProjectScreen/index.vue
Normal file
@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div class="large_screen">
|
||||
<Header :projectId="projectId" :isFull="isFull" @changePage="handleChangePage" />
|
||||
<div class="nav">
|
||||
<div class="nav_left" :style="{ left: isHideOther ? '-25vw' : '0' }">
|
||||
<leftPage :projectId="projectId" />
|
||||
</div>
|
||||
<div class="nav_center" :style="{ width: isFull ? '100%' : 'calc(50vw - 30px)' }">
|
||||
<centerPage :projectId="projectId" :isHide="isFull" />
|
||||
</div>
|
||||
<div class="nav_right" :style="{ right: isHideOther ? '-25vw' : '0' }">
|
||||
<rightPage :projectId="projectId" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import Header from './components/header.vue';
|
||||
import leftPage from './components/leftPage.vue';
|
||||
import centerPage from './components/centerPage.vue';
|
||||
import rightPage from './components/rightPage.vue';
|
||||
import { useUserStoreHook } from '@/store/modules/user';
|
||||
const userStore = useUserStoreHook();
|
||||
const projectId = computed(() => userStore.selectedProject.id);
|
||||
const isFull = ref(false);
|
||||
const isHideOther = ref(false);
|
||||
|
||||
/**
|
||||
* 切换中心页面全屏
|
||||
*/
|
||||
const handleChangePage = () => {
|
||||
if (isFull.value) {
|
||||
isFull.value = false;
|
||||
setTimeout(() => {
|
||||
isHideOther.value = false;
|
||||
}, 500);
|
||||
} else {
|
||||
isFull.value = true;
|
||||
isHideOther.value = true;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.large_screen {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: url('@/assets/large/bg.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-attachment: fixed;
|
||||
background-color: rgba(4, 7, 17, 1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nav {
|
||||
position: relative;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: calc(100vw - 30px);
|
||||
height: calc(100vh - 90px);
|
||||
margin: 0 auto;
|
||||
box-sizing: border-box;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.nav_left,
|
||||
.nav_right {
|
||||
position: absolute;
|
||||
width: calc(25vw - 15px);
|
||||
height: 100%;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.nav_left {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.nav_right {
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.nav_center {
|
||||
height: 100%;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
/* 中间面板全屏动画 */
|
||||
.full-width {
|
||||
/* 取消flex增长,使用固定宽度 */
|
||||
width: calc(100vw - 30px);
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.slide_left {
|
||||
left: -25vw;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.slide_right {
|
||||
right: -25vw;
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,293 @@
|
||||
export default class CesiumFlyToRoamingController {
|
||||
/**
|
||||
* 构造函数 - 创建基于flyTo的漫游控制器
|
||||
* @param {Cesium.Viewer} viewer - Cesium Viewer实例
|
||||
* @param {Object} [options] - 漫游配置选项
|
||||
* @param {number} [options.duration=3] - 飞行持续时间(秒)
|
||||
* @param {number} [options.pitch=-30] - 俯仰角(度)
|
||||
* @param {number} [options.headingOffset=0] - 航向偏移(度)
|
||||
*/
|
||||
constructor(viewer, options = {}) {
|
||||
if (!viewer || !(viewer instanceof Cesium.Viewer)) {
|
||||
throw new Error('必须提供有效的Cesium Viewer实例');
|
||||
}
|
||||
|
||||
this.viewer = viewer;
|
||||
this.isRoaming = false;
|
||||
this.path = [];
|
||||
this.currentIndex = -1; // 初始为-1,表示尚未开始
|
||||
this.loop = false;
|
||||
this.duration = options.duration || 3;
|
||||
this.pitch = Cesium.Math.toRadians(options.pitch || -30);
|
||||
this.headingOffset = Cesium.Math.toRadians(options.headingOffset || 0);
|
||||
this.flyToOptions = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始路径漫游 - 严格按照路径点顺序,首先飞入第一个点位
|
||||
* @param {Array} path - 路径点数组(按顺序排列)
|
||||
* @param {number} [duration] - 飞行持续时间(秒)
|
||||
* @param {boolean} [loop=false] - 是否循环漫游
|
||||
*/
|
||||
startPathRoaming(path, duration, loop = false) {
|
||||
if (!path || path.length < 1) {
|
||||
throw new Error('路径漫游需要至少1个路径点');
|
||||
}
|
||||
|
||||
// 停止当前可能的漫游
|
||||
this.stopRoaming();
|
||||
|
||||
// 初始化参数
|
||||
this.path = [...path];
|
||||
this.loop = loop;
|
||||
this.currentIndex = -1; // 重置为初始状态
|
||||
|
||||
if (duration !== undefined) {
|
||||
this.duration = duration;
|
||||
}
|
||||
|
||||
this.isRoaming = true;
|
||||
|
||||
// 第一步:飞到第一个点位
|
||||
this.flyToFirstPoint();
|
||||
}
|
||||
|
||||
/**
|
||||
* 专门用于飞到第一个点位的方法
|
||||
*/
|
||||
flyToFirstPoint() {
|
||||
if (!this.isRoaming || this.path.length === 0) return;
|
||||
|
||||
const firstPointIndex = 0;
|
||||
const firstPoint = this.path[firstPointIndex];
|
||||
|
||||
// 计算朝向:如果有第二个点,则面向第二个点,否则保持当前朝向
|
||||
let orientation;
|
||||
if (this.path.length > 1) {
|
||||
orientation = this.calculateOrientation(firstPoint, this.path[1]);
|
||||
} else {
|
||||
orientation = {
|
||||
heading: this.viewer.camera.heading,
|
||||
pitch: this.pitch,
|
||||
roll: 0
|
||||
};
|
||||
}
|
||||
|
||||
// 飞行到第一个点
|
||||
this.flyToOptions = {
|
||||
destination: firstPoint,
|
||||
orientation: orientation,
|
||||
duration: this.duration,
|
||||
complete: () => {
|
||||
// 第一个点到达后更新索引
|
||||
this.currentIndex = firstPointIndex;
|
||||
|
||||
// 如果有更多点,继续飞行到下一个点
|
||||
if (this.path.length > 1) {
|
||||
this.flyToNextPoint();
|
||||
} else {
|
||||
// 只有一个点时,完成后停止漫游
|
||||
this.isRoaming = false;
|
||||
}
|
||||
},
|
||||
cancel: () => {
|
||||
this.isRoaming = false;
|
||||
}
|
||||
};
|
||||
|
||||
this.viewer.camera.flyTo(this.flyToOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 飞行到下一个路径点
|
||||
*/
|
||||
flyToNextPoint() {
|
||||
if (!this.isRoaming || this.currentIndex === -1) return;
|
||||
|
||||
// 计算下一个点的索引
|
||||
const nextIndex = this.currentIndex + 1;
|
||||
|
||||
// 检查是否超出路径范围
|
||||
if (nextIndex >= this.path.length) {
|
||||
if (this.loop) {
|
||||
// 循环模式:回到第一个点
|
||||
this.currentIndex = -1;
|
||||
this.flyToFirstPoint();
|
||||
} else {
|
||||
// 非循环模式:到达终点,停止漫游
|
||||
this.isRoaming = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新当前索引并获取目标点
|
||||
this.currentIndex = nextIndex;
|
||||
const targetPoint = this.path[this.currentIndex];
|
||||
|
||||
// 计算朝向
|
||||
let orientation;
|
||||
if (this.currentIndex < this.path.length - 1) {
|
||||
// 面向下一个点
|
||||
orientation = this.calculateOrientation(targetPoint, this.path[this.currentIndex + 1]);
|
||||
} else if (this.loop) {
|
||||
// 最后一个点且循环模式,面向第一个点
|
||||
orientation = this.calculateOrientation(targetPoint, this.path[0]);
|
||||
} else {
|
||||
// 最后一个点且不循环,保持当前朝向
|
||||
orientation = {
|
||||
heading: this.viewer.camera.heading,
|
||||
pitch: this.pitch,
|
||||
roll: 0
|
||||
};
|
||||
}
|
||||
|
||||
// 执行飞行
|
||||
this.flyToOptions = {
|
||||
destination: targetPoint,
|
||||
orientation: orientation,
|
||||
duration: this.duration,
|
||||
complete: () => this.flyToNextPoint(),
|
||||
cancel: () => { this.isRoaming = false; }
|
||||
};
|
||||
|
||||
this.viewer.camera.flyTo(this.flyToOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算相机朝向
|
||||
* @param {Cesium.Cartesian3} position - 相机位置
|
||||
* @param {Cesium.Cartesian3} lookAtPoint - 看向的点
|
||||
* @returns {Object} 朝向配置
|
||||
*/
|
||||
calculateOrientation(position, lookAtPoint) {
|
||||
const direction = Cesium.Cartesian3.subtract(lookAtPoint, position, new Cesium.Cartesian3());
|
||||
const heading = Math.atan2(direction.x, direction.y) + this.headingOffset;
|
||||
|
||||
return {
|
||||
heading: heading,
|
||||
pitch: this.pitch,
|
||||
roll: 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止漫游
|
||||
*/
|
||||
stopRoaming() {
|
||||
if (this.isRoaming) {
|
||||
this.isRoaming = false;
|
||||
if (this.flyToOptions) {
|
||||
this.viewer.camera.cancelFlight();
|
||||
this.flyToOptions = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停漫游
|
||||
*/
|
||||
pauseRoaming() {
|
||||
if (this.isRoaming) {
|
||||
this.isRoaming = false;
|
||||
if (this.flyToOptions) {
|
||||
this.viewer.camera.cancelFlight();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复漫游
|
||||
*/
|
||||
resumeRoaming() {
|
||||
if (!this.isRoaming && this.path.length > 0) {
|
||||
this.isRoaming = true;
|
||||
if (this.currentIndex === -1) {
|
||||
this.flyToFirstPoint();
|
||||
} else {
|
||||
this.flyToNextPoint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加路径点
|
||||
* @param {Cesium.Cartesian3} point - 路径点
|
||||
*/
|
||||
addPathPoint(point) {
|
||||
if (point instanceof Cesium.Cartesian3) {
|
||||
this.path.push(point);
|
||||
} else {
|
||||
console.warn('路径点必须是Cesium.Cartesian3类型');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除路径
|
||||
*/
|
||||
clearPath() {
|
||||
this.stopRoaming();
|
||||
this.path = [];
|
||||
this.currentIndex = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置飞行持续时间
|
||||
* @param {number} duration - 持续时间(秒)
|
||||
*/
|
||||
setDuration(duration) {
|
||||
if (typeof duration === 'number' && duration > 0) {
|
||||
this.duration = duration;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁控制器
|
||||
*/
|
||||
destroy() {
|
||||
this.stopRoaming();
|
||||
this.viewer = null;
|
||||
this.path = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 使用示例:
|
||||
// 假设已经有一个Cesium Viewer实例叫做viewer
|
||||
//
|
||||
// // 创建漫游控制器
|
||||
// const roamingController = new CesiumRoamingController(viewer, {
|
||||
// speed: 20,
|
||||
// pitch: -20, // 20度俯角
|
||||
// headingOffset: 0
|
||||
// });
|
||||
//
|
||||
// // 示例1: 自由漫游
|
||||
// // 开始自由漫游
|
||||
// document.getElementById('startFreeRoam').addEventListener('click', () => {
|
||||
// roamingController.startFreeRoaming(15); // 速度15米/秒
|
||||
// });
|
||||
//
|
||||
// // 示例2: 路径漫游
|
||||
// // 创建路径点
|
||||
// const pathPoints = [
|
||||
// Cesium.Cartesian3.fromDegrees(116.3, 39.9, 200),
|
||||
// Cesium.Cartesian3.fromDegrees(116.4, 39.9, 200),
|
||||
// Cesium.Cartesian3.fromDegrees(116.4, 40.0, 200),
|
||||
// Cesium.Cartesian3.fromDegrees(116.3, 40.0, 200)
|
||||
// ];
|
||||
//
|
||||
// // 开始路径漫游
|
||||
// document.getElementById('startPathRoam').addEventListener('click', () => {
|
||||
// roamingController.startPathRoaming(pathPoints, 30, true); // 速度30米/秒,循环漫游
|
||||
// });
|
||||
//
|
||||
// // 停止漫游
|
||||
// document.getElementById('stopRoam').addEventListener('click', () => {
|
||||
// roamingController.stopRoaming();
|
||||
// });
|
||||
//
|
||||
// // 调整速度
|
||||
// document.getElementById('speedUp').addEventListener('click', () => {
|
||||
// const currentSpeed = roamingController.speed;
|
||||
// roamingController.setSpeed(currentSpeed + 5);
|
||||
// });
|
||||
|
||||
@ -0,0 +1,298 @@
|
||||
export default class CesiumImageLabelEntity {
|
||||
/**
|
||||
* 构造函数 - 创建带有图片和名称的Cesium Entity
|
||||
* @param {Cesium.Viewer} viewer - Cesium Viewer实例
|
||||
* @param {Object} options - 配置选项
|
||||
* @param {Cesium.Cartesian3} options.position - 实体位置坐标
|
||||
* @param {string} options.name - 实体名称(标签文本)
|
||||
* @param {string} options.imageUrl - 图片URL
|
||||
* @param {Function} [options.onClick] - 左击事件回调函数
|
||||
* @param {string} [options.id] - 实体ID,可选
|
||||
* @param {number} [options.imageWidth=64] - 图片宽度
|
||||
* @param {number} [options.imageHeight=64] - 图片高度
|
||||
* @param {Cesium.Color} [options.imageColor=Cesium.Color.WHITE] - 图片颜色(用于色调调整)
|
||||
* @param {Cesium.Color} [options.labelColor=Cesium.Color.WHITE] - 标签颜色
|
||||
* @param {string} [options.labelFont='16px sans-serif'] - 标签字体
|
||||
* @param {number} [options.labelOffsetY=-70] - 标签Y轴偏移量(相对于图片)
|
||||
* @param {boolean} [options.show=true] - 是否显示实体
|
||||
* @param {Cesium.HorizontalOrigin} [options.horizontalOrigin=Cesium.HorizontalOrigin.CENTER] - 水平对齐方式
|
||||
* @param {Cesium.VerticalOrigin} [options.verticalOrigin=Cesium.VerticalOrigin.BOTTOM] - 垂直对齐方式
|
||||
*/
|
||||
constructor(viewer, options) {
|
||||
// 验证必要参数
|
||||
if (!viewer || !(viewer instanceof Cesium.Viewer)) {
|
||||
throw new Error('必须提供有效的Cesium Viewer实例');
|
||||
}
|
||||
if (!options || !options.position) {
|
||||
throw new Error('必须提供实体位置信息');
|
||||
}
|
||||
if (!options.name) {
|
||||
throw new Error('必须提供实体名称');
|
||||
}
|
||||
if (!options.imageUrl) {
|
||||
throw new Error('必须提供图片URL');
|
||||
}
|
||||
|
||||
this.viewer = viewer;
|
||||
this.onClickCallback = options.onClick || null;
|
||||
this.options = {
|
||||
// 默认配置
|
||||
id: options.id || `image-label-entity-${Date.now()}`,
|
||||
imageWidth: options.imageWidth || 64,
|
||||
imageHeight: options.imageHeight || 64,
|
||||
imageColor: options.imageColor || Cesium.Color.WHITE,
|
||||
labelColor: options.labelColor || Cesium.Color.WHITE,
|
||||
labelFont: options.labelFont || '16px sans-serif',
|
||||
labelOffsetY: options.labelOffsetY || -80,
|
||||
show: options.show !== undefined ? options.show : true,
|
||||
horizontalOrigin: options.horizontalOrigin || Cesium.HorizontalOrigin.CENTER,
|
||||
verticalOrigin: options.verticalOrigin || Cesium.VerticalOrigin.BOTTOM,
|
||||
...options
|
||||
};
|
||||
|
||||
// 创建实体
|
||||
this.entity = this.createEntity();
|
||||
|
||||
// 为实体添加标识,方便后续判断
|
||||
this.entity._isImageLabelEntity = true;
|
||||
this.entity._imageLabelInstance = this;
|
||||
|
||||
// 初始化全局点击事件(确保只注册一次)
|
||||
this.initGlobalClickHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建实体
|
||||
* @returns {Cesium.Entity} 创建的实体对象
|
||||
*/
|
||||
createEntity() {
|
||||
const entity = new Cesium.Entity({
|
||||
id: this.options.id,
|
||||
position: Cesium.Cartesian3.fromDegrees(
|
||||
this.options.position.lng,
|
||||
this.options.position.lat,
|
||||
this.options.position.height || 0 // 修复:使用height而非alt
|
||||
),
|
||||
show: this.options.show,
|
||||
|
||||
// 图片属性
|
||||
billboard: {
|
||||
image: this.options.imageUrl,
|
||||
width: this.options.imageWidth,
|
||||
height: this.options.imageHeight,
|
||||
color: this.options.imageColor,
|
||||
horizontalOrigin: this.options.horizontalOrigin,
|
||||
verticalOrigin: this.options.verticalOrigin,
|
||||
pickable: true // 确保可拾取
|
||||
},
|
||||
|
||||
// 名称标签属性
|
||||
label: {
|
||||
text: this.options.name,
|
||||
font: this.options.labelFont,
|
||||
fillColor: this.options.labelColor,
|
||||
horizontalOrigin: this.options.horizontalOrigin,
|
||||
verticalOrigin: Cesium.VerticalOrigin.TOP,
|
||||
pixelOffset: new Cesium.Cartesian2(0, this.options.labelOffsetY),
|
||||
backgroundColor: new Cesium.Color(0, 0, 0, 0.5), // 修复:半透明背景更易点击
|
||||
backgroundPadding: new Cesium.Cartesian2(5, 5),
|
||||
showBackground: true,
|
||||
pickable: true // 确保可拾取
|
||||
}
|
||||
});
|
||||
|
||||
// 将实体添加到viewer
|
||||
this.viewer.entities.add(entity);
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化全局点击事件处理器(只注册一次)
|
||||
*/
|
||||
initGlobalClickHandler() {
|
||||
// 检查是否已注册全局事件,避免重复注册
|
||||
if (!this.viewer._imageLabelGlobalClickHandler) {
|
||||
const handler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas);
|
||||
|
||||
handler.setInputAction((movement) => {
|
||||
const pickedObject = this.viewer.scene.pick(movement.position);
|
||||
|
||||
// 判断是否点击了我们创建的图片标签实体
|
||||
if (Cesium.defined(pickedObject) &&
|
||||
Cesium.defined(pickedObject.id) &&
|
||||
pickedObject.id._isImageLabelEntity) {
|
||||
// 调用对应实例的回调函数
|
||||
if (pickedObject.id._imageLabelInstance.onClickCallback) {
|
||||
pickedObject.id._imageLabelInstance.onClickCallback(
|
||||
pickedObject.id,
|
||||
movement.position
|
||||
);
|
||||
}
|
||||
}
|
||||
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
|
||||
|
||||
// 存储全局事件处理器引用,避免重复创建
|
||||
this.viewer._imageLabelGlobalClickHandler = handler;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置点击事件回调函数
|
||||
* @param {Function} callback - 回调函数,接收(entity, position)参数
|
||||
*/
|
||||
setOnClick(callback) {
|
||||
if (typeof callback === 'function') {
|
||||
this.onClickCallback = callback;
|
||||
} else {
|
||||
console.warn('回调函数必须是一个函数');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 平滑飞行到实体当前位置
|
||||
* @param {Object} [options] - 飞行参数配置
|
||||
* @param {number} [options.duration=3] - 飞行持续时间(秒)
|
||||
* @param {number} [options.offsetDistance=1000] - 距离目标点的距离(米)
|
||||
* @param {Cesium.HeadingPitchRange} [options.headingPitchRange] - 方向、俯仰和范围,优先级高于offsetDistance
|
||||
* @param {Function} [options.complete] - 飞行完成后的回调函数
|
||||
* @param {Function} [options.cancel] - 飞行被取消后的回调函数
|
||||
*/
|
||||
flyTo(options = {}) {
|
||||
// 获取实体当前位置(考虑可能已更新的情况)
|
||||
const currentPosition = this.entity?.position?.getValue(Cesium.JulianDate.now());
|
||||
|
||||
if (!currentPosition) {
|
||||
console.warn('无法飞行到实体,实体或实体位置不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
// 默认飞行参数
|
||||
const defaultOptions = {
|
||||
duration: 3,
|
||||
offsetDistance: 1000,
|
||||
complete: () => {},
|
||||
cancel: () => {}
|
||||
};
|
||||
|
||||
// 合并用户配置和默认配置
|
||||
const flyOptions = { ...defaultOptions, ...options };
|
||||
|
||||
// 计算飞行视角
|
||||
let headingPitchRange;
|
||||
if (flyOptions.headingPitchRange) {
|
||||
headingPitchRange = flyOptions.headingPitchRange;
|
||||
} else {
|
||||
// 默认视角:从上方稍远处看向实体
|
||||
headingPitchRange = new Cesium.HeadingPitchRange(
|
||||
0, // 方向角(弧度)
|
||||
Cesium.Math.toRadians(-30), // 俯仰角(弧度),负值表示向下看
|
||||
flyOptions.offsetDistance // 距离目标点的距离
|
||||
);
|
||||
}
|
||||
|
||||
// 执行飞行到当前位置
|
||||
this.viewer.flyTo(this.entity, {
|
||||
destination: currentPosition, // 明确指定当前位置作为目标
|
||||
duration: flyOptions.duration,
|
||||
offset: headingPitchRange,
|
||||
complete: flyOptions.complete,
|
||||
cancel: flyOptions.cancel
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新实体位置
|
||||
* @param {Cesium.Cartesian3} position - 新的位置坐标
|
||||
*/
|
||||
updatePosition(position) {
|
||||
if (position && this.entity) {
|
||||
this.entity.position = position;
|
||||
// 更新options中的位置,保持同步
|
||||
this.options.position = position;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新实体名称
|
||||
* @param {string} name - 新的名称
|
||||
*/
|
||||
updateName(name) {
|
||||
if (name && this.entity && this.entity.label) {
|
||||
this.entity.label.text = name;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新实体图片
|
||||
* @param {string} imageUrl - 新的图片URL
|
||||
*/
|
||||
updateImage(imageUrl) {
|
||||
if (imageUrl && this.entity && this.entity.billboard) {
|
||||
this.entity.billboard.image = imageUrl;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示实体
|
||||
*/
|
||||
show() {
|
||||
if (this.entity) {
|
||||
this.entity.show = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏实体
|
||||
*/
|
||||
hide() {
|
||||
if (this.entity) {
|
||||
this.entity.show = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除实体
|
||||
*/
|
||||
remove() {
|
||||
if (this.entity) {
|
||||
this.viewer.entities.remove(this.entity);
|
||||
this.entity = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前实体
|
||||
* @returns {Cesium.Entity} 当前实体对象
|
||||
*/
|
||||
getEntity() {
|
||||
return this.entity;
|
||||
}
|
||||
}
|
||||
|
||||
// 使用示例:
|
||||
// 假设已经有一个Cesium Viewer实例叫做viewer
|
||||
// // 创建实体
|
||||
// const initialPosition = Cesium.Cartesian3.fromDegrees(116.39, 39.9, 100);
|
||||
// const imageEntity = new CesiumImageLabelEntity(viewer, {
|
||||
// position: initialPosition,
|
||||
// name: "可移动点",
|
||||
// imageUrl: "path/to/your/image.png",
|
||||
// onClick: function(entity) {
|
||||
// console.log("点击了实体,飞向当前位置");
|
||||
// entity.flyTo(); // 飞向当前位置
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// // 一段时间后更新位置
|
||||
// setTimeout(() => {
|
||||
// const newPosition = Cesium.Cartesian3.fromDegrees(116.45, 39.92, 100);
|
||||
// imageEntity.updatePosition(newPosition);
|
||||
// console.log("实体位置已更新");
|
||||
// }, 2000);
|
||||
//
|
||||
// // 调用flyTo将飞向最新的位置
|
||||
// setTimeout(() => {
|
||||
// imageEntity.flyTo({duration: 2});
|
||||
// }, 4000);
|
||||
|
||||
Reference in New Issue
Block a user