数字化大屏

This commit is contained in:
ljx
2025-09-09 15:26:15 +08:00
parent 460472b29a
commit 16eb852d9d
16 changed files with 1467 additions and 8 deletions

View File

@ -5,8 +5,8 @@ VITE_APP_TITLE = 煤科建管平台
VITE_APP_ENV = 'development'
# 开发环境
VITE_APP_BASE_API = 'http://192.168.110.149:8899'
# VITE_APP_BASE_API = 'http://192.168.110.209:8899'
VITE_APP_BASE_API = 'http://192.168.110.209:8899'
# 李陈杰 209
# VITE_APP_BASE_API = 'http://192.168.110.209:8899'
# 曾涛
@ -14,7 +14,7 @@ VITE_APP_BASE_API = 'http://192.168.110.149:8899'
# 罗成
# VITE_APP_BASE_API = 'http://192.168.110.188:8899'
# 朱银
VITE_APP_BASE_API = 'http://192.168.110.149:8899'
# VITE_APP_BASE_API = 'http://192.168.110.149:8899'
#曾涛
# VITE_APP_BASE_API = 'http://192.168.110.171:8899'

View File

@ -72,9 +72,15 @@ export const constantRoutes: RouteRecordRaw[] = [
component: () => import('@/views/error/401.vue'),
hidden: true
},
{
path: '/ProjectScreen',
component: () => import('@/views/ProjectScreen/index.vue'),
component: () => import('@/views/projectLarge/ProjectScreen/index.vue'),
hidden: true
},
{
path: '/digitalizationScreen',
component: () => import('@/views/projectLarge/digitalizationScreen/index.vue'),
hidden: true
},
{

View File

@ -22,11 +22,10 @@ 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 isFull = ref(false);
const isHideOther = ref(false);
/**
* 切换中心页面全屏
@ -41,7 +40,7 @@ const handleChangePage = () => {
isFull.value = true;
isHideOther.value = true;
}
}
};
</script>
<style lang="scss" scoped>

View File

@ -0,0 +1,275 @@
<template>
<div class="centerPage">
<div class="topPage">
<div id="earth" style="width: 100%;height: 100%;"></div>
</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) }}</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'
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) {
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()
createEarth()
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 {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
padding: 15px 0;
border: 1px solid rgba(230, 247, 255, 0.1);
box-sizing: border-box;
}
.topPage {
flex: 1;
margin-bottom: 23px;
transition: flex 0.5s ease;
}
.endPage {
max-height: 300px;
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: 20px 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: 14px;
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>

View File

@ -0,0 +1,327 @@
<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: 12px; 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 Digital Intelligent 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="`../../../src/assets/images/${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 2fr 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-weight: 500;
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>

View File

@ -0,0 +1,326 @@
<template>
<div class="leftPage">
<div class="topPage">
<Title title="项目公告" />
<div class="content">
<div class="content_item" v-for="item in news" :key="item.id">
<img src="@/assets/projectLarge/round.svg" alt="" />
<div class="ellipsis">
{{ item.title }}
<span @click="showNewsDetail(item)" 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>
<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 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 }} %</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';
import { mapOption } from './optionList';
import * as echarts from 'echarts';
const props = defineProps({
projectId: {
type: String,
default: ''
}
});
let mapChart = null;
const mapChartRef = 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;
}
};
/**
* 初始化地图
*/
const initMapChart = () => {
if (!mapChartRef.value) {
return;
}
mapChart = echarts.init(mapChartRef.value);
mapChart.setOption(mapOption);
};
onMounted(() => {
// nextTick(() => {
// initMapChart();
// });
getPeopleData();
getNewsData();
});
onUnmounted(() => {
// if (mapChart) {
// mapChart.dispose();
// mapChart = null;
// }
});
</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;
}
}
.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: 0 15px;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.2);
border-radius: 4px;
transition: all 0.3s ease;
opacity: 0;
z-index: -1;
&.show {
left: 25vw;
opacity: 1;
z-index: 1;
}
}
.detailBox::before {
content: '';
/* 绝对定位相对于父元素 */
position: absolute;
/* 定位到左侧中间位置 */
left: -10px;
top: 50%;
/* 垂直居中 */
transform: translateY(-50%);
/* 利用边框创建三角形 */
border-width: 10px 10px 10px 0;
border-style: solid;
/* 三角形颜色与背景匹配,左侧边框透明 */
border-color: transparent rgba(255, 255, 255, 0.2) transparent transparent;
/* 确保三角形在内容下方 */
z-index: -1;
}
</style>

View 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: 20,
itemWidth: 12,
itemHeight: 12,
textStyle: {
color: '#fff'
}
},
series: {
type: 'pie',
data: [],
radius: [50, 80],
center: ['50%', '45%'],
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: 0,
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] }
],
}
]
};

View File

@ -0,0 +1,221 @@
<template>
<div class="leftPage">
<div class="topPage">
<Title title="项目概况" />
<div class="content" v-html="generalize"></div>
</div>
<div class="endPage">
<Title title="形象进度" />
<div class="chart_container">
<div ref="pieChartRef" class="echart" />
<div ref="lineChartRef" class="echart" />
</div>
</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 handleResize = () => {
if (pieChart) pieChart.resize();
if (lineChart) lineChart.resize();
};
/**
* 获取项目土地统计数据
*/
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
}
}
// 组件挂载时初始化图表
onMounted(() => {
getScreenLandData()
getScreenImgProcessData()
getScreenGeneralizeData()
nextTick(() => {
initPieChart();
window.addEventListener('resize', handleResize);
});
});
// 组件卸载时清理
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
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,
.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;
.chart_container {
display: flex;
flex-direction: column;
gap: 5px;
width: 100%;
height: 100%;
}
.echart {
height: 48%;
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>

View 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>

View 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>