This commit is contained in:
2025-09-04 10:56:28 +08:00
13 changed files with 847 additions and 311 deletions

View File

@ -0,0 +1,66 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
// 查询生项目天气
export const getScreenWeather = (projectId: number | string) => {
return request({
url: '/project/big/screen/weather/' + projectId,
method: 'get',
});
};
// 查询项目安全天数
export const getScreenSafetyDay = (projectId: number | string) => {
return request({
url: '/project/big/screen/safetyDay/' + projectId,
method: 'get',
});
};
// 查询项目公告
export const getScreenNews = (projectId: number | string) => {
return request({
url: '/project/big/screen/news/' + projectId,
method: 'get',
});
};
// 查询项目土地统计
export const getScreenLand = (projectId: number | string) => {
return request({
url: '/project/big/screen/' + projectId,
method: 'get',
});
};
// 查询项目形象进度
export const getScreenImgProcess = (projectId: number | string) => {
return request({
url: '/project/big/screen/imageProgress/' + projectId,
method: 'get',
});
};
// 查询项目人员情况
export const getScreenPeople = (projectId: number | string) => {
return request({
url: '/project/big/screen/people/' + projectId,
method: 'get',
});
};
// 查询项目AI安全巡检
export const getScreenSafetyInspection = (projectId: number | string) => {
return request({
url: '/project/big/screen/safetyInspection/' + projectId,
method: 'get',
});
};
// 查询项目概况
export const getScreenGeneralize = (projectId: number | string) => {
return request({
url: '/project/big/screen/generalize/' + projectId,
method: 'get',
});
};

View File

@ -0,0 +1,5 @@
export interface TableQuery extends PageQuery {
tableName: string;
tableComment: string;
dataName: string;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 652 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 665 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

View File

@ -1,26 +1,27 @@
<template> <template>
<div class="centerPage"> <div class="centerPage">
<div class="topPage"> <div class="topPage">
<!-- 暂无 --> <img src="@/assets/projectLarge/center.png" alt="">
</div> </div>
<div class="endPage"> <div class="endPage" :class="{ 'slide-out-down': isHide }">
<Title title="AI安全巡检" :prefix="true" /> <Title title="AI安全巡检">
<img src="@/assets/projectLarge/robot.svg" alt="" height="20px" width="20px">
<div class="swiper"> </Title>
<div class="swiper" v-if="inspectionList.length">
<div class="arrow" :class="{ 'canUse': canLeft }" @click="swiperClick('left')"> <div class="arrow" :class="{ 'canUse': canLeft }" @click="swiperClick('left')">
<el-icon size="16" color="skyblue"> <el-icon size="16" :color="canLeft ? 'rgba(29, 214, 255, 1)' : 'rgba(29, 214, 255, 0.3)'">
<ArrowLeft /> <ArrowLeft />
</el-icon> </el-icon>
</div> </div>
<div class="swiper_content" ref="swiperContent"> <div class="swiper_content" ref="swiperContent">
<div class="swiper_item" v-for="(item, index) in swiperList" :key="index"> <div class="swiper_item" v-for="(item, i) in inspectionList" :key="i">
<img src="@/assets/projectLarge/swiper.png" alt="" class="swiper_img"> <img :src="item.picture" alt="安全巡检" class="swiper_img">
<div class="swiper_date">{{ item.date }}</div> <div class="swiper_date">{{ item.createTime.slice(5) }}</div>
<div class="swiper_tip">{{ item.tip }}</div> <div class="swiper_tip">{{ item.label }}</div>
</div> </div>
</div> </div>
<div class="arrow" :class="{ 'canUse': canRight }" @click="swiperClick('right')"> <div class="arrow" :class="{ 'canUse': canRight }" @click="swiperClick('right')">
<el-icon size="16"> <el-icon size="16" :color="canRight ? 'rgba(29, 214, 255, 1)' : 'rgba(29, 214, 255, 0.3)'">
<ArrowRight /> <ArrowRight />
</el-icon> </el-icon>
</div> </div>
@ -30,30 +31,38 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue" import { ref, onMounted, toRefs, getCurrentInstance } from "vue"
import Title from './title.vue' import Title from './title.vue'
import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue'
import { getScreenSafetyInspection } from '@/api/projectScreen'
const swiperList = ref([ const { proxy } = getCurrentInstance();
{ date: '03-18 15:00', tip: '未佩戴安全帽1' }, const { violation_level_type } = toRefs(proxy?.useDict('violation_level_type'));
{ date: '03-18 15:00', tip: '未佩戴安全帽2' },
{ date: '03-18 15:00', tip: '未佩戴安全帽3' },
{ date: '03-18 15:00', tip: '未佩戴安全帽4' },
{ date: '03-18 15:00', tip: '未佩戴安全帽5' },
{ date: '03-18 15:00', tip: '未佩戴安全帽6' },
{ date: '03-18 15:00', tip: '未佩戴安全帽7' },
{ date: '03-18 15:00', tip: '未佩戴安全帽8' },
{ date: '03-18 15:00', tip: '未佩戴安全帽9' },
{ date: '03-18 15:00', tip: '未佩戴安全帽10' },
{ date: '03-18 15:00', tip: '未佩戴安全帽11' },
{ date: '03-18 15:00', tip: '未佩戴安全帽12' },
])
const props = defineProps({
isHide: {
type: Boolean,
default: false
},
projectId: {
type: String,
default: ""
}
})
const inspectionList = ref([{
id: "",
label: "",
picture: "",
createTime: ""
}])
const swiperContent = ref<HTMLDivElement>() const swiperContent = ref<HTMLDivElement>()
const swiperItemWidth = ref(100) const swiperItemWidth = ref(100)
const canLeft = ref(false) const canLeft = ref(false)
const canRight = ref(true) const canRight = ref(true)
const swiperClick = (direction: 'left' | 'right') => { const swiperClick = (direction: 'left' | 'right') => {
if (!swiperContent.value) return
if (direction === 'right') { if (direction === 'right') {
if (swiperContent.value.scrollLeft >= swiperContent.value.scrollWidth - swiperContent.value.clientWidth) { if (swiperContent.value.scrollLeft >= swiperContent.value.scrollWidth - swiperContent.value.clientWidth) {
@ -70,36 +79,70 @@ const swiperClick = (direction: 'left' | 'right') => {
} }
swiperContent.value.scrollLeft -= swiperItemWidth.value 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: any) => i.value === item.violationType)?.label
})
inspectionList.value = data
}
} }
onMounted(() => { onMounted(() => {
getInspectionList()
if (swiperContent.value && swiperContent.value.children.length > 0) {
swiperItemWidth.value = swiperContent.value.children[0].clientWidth + 20 swiperItemWidth.value = swiperContent.value.children[0].clientWidth + 20
}
}) })
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.centerPage { .centerPage {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 50vw;
height: 100%; height: 100%;
}
.topPage, .topPage,
.endPage { .endPage {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center;
width: 100%; width: 100%;
padding: 15px 0; padding: 15px 0;
border: 1px solid rgba(29, 214, 255, 0.1); border: 1px solid rgba(230, 247, 255, 0.1);
box-sizing: border-box; box-sizing: border-box;
} }
.topPage { .topPage {
flex: 1; flex: 1;
margin-bottom: 23px; 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 { .swiper {
@ -161,14 +204,20 @@ onMounted(() => {
.arrow { .arrow {
display: grid; display: grid;
place-items: center; place-items: center;
width: 20px; width: 24px;
height: 20px; height: 24px;
border-radius: 50%; border-radius: 50%;
border: 1px solid skyblue; border: 1px solid rgba(29, 214, 255, 0.3);
color: skyblue; color: skyblue;
cursor: pointer;
transition: all 0.3s ease;
&:canUse { &.canUse {
color: #000 !important; border: 1px solid rgba(29, 214, 255, 1);
}
&:hover:not(.canUse) {
opacity: 0.7;
} }
} }
</style> </style>

View File

@ -6,7 +6,7 @@
</div> </div>
<div style="font-size: 12px; padding-left: 10px">安全生产天数</div> <div style="font-size: 12px; padding-left: 10px">安全生产天数</div>
<div class="header_left_text"> <div class="header_left_text">
1,235 {{ safetyDay }}
<span style="font-size: 12px"></span> <span style="font-size: 12px"></span>
</div> </div>
</div> </div>
@ -14,16 +14,18 @@
<div>XXX智慧工地管理平台</div> <div>XXX智慧工地管理平台</div>
<div>XXX Smart Construction Stic Management Dashboard</div> <div>XXX Smart Construction Stic Management Dashboard</div>
</div> </div>
<div class="right"> <div class="header_right">
<div class="top-bar"> <div class="top-bar">
<!-- 左侧天气图标 + 日期文字 --> <!-- 左侧天气图标 + 日期文字 -->
<div class="left-section"> <div class="left-section">
<img src="@/assets/large/weather.png" alt="天气图标" /> <div class="weather-list" @mouseenter="requestPause" @mouseleave="resumeScroll">
<div v-for="(item, i) in weatherList" :key="i" class="weather-item"
<span> :style="{ transform: `translateY(-${offsetY}px)`, transition: transition }">
<span>多云 9°/18°</span> <img :src="`../../../src/assets/images/${item.icon}.png`" alt="" />
<span style="padding-left: 20px"> {{ week[date.week] }} ({{ date.ymd }})</span> <div>{{ item.weather }}{{ item.tempMin }}°/{{ item.tempMax }}°</div>
</span> <div>{{ item.week }}({{ item.date }})</div>
</div>
</div>
</div> </div>
<!-- 分割线 --> <!-- 分割线 -->
<div class="divider"> <div class="divider">
@ -35,48 +37,155 @@
<img src="@/assets/large/setting.png" alt="设置图标" /> <img src="@/assets/large/setting.png" alt="设置图标" />
<span>管理系统</span> <span>管理系统</span>
</div> </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> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const week = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']; import { ref, onMounted, onUnmounted } from 'vue';
const date = ref({ import { getScreenSafetyDay, getScreenWeather } from '@/api/projectScreen';
ymd: '',
hms: '', interface Weather {
week: 0 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()
}
})
}); });
const setTime = () => {
let date1 = new Date();
let year: any = date1.getFullYear();
let month: any = date1.getMonth() + 1;
let day: any = date1.getDate();
let hours: any = date1.getHours();
if (hours < 10) {
hours = '0' + hours;
}
let minutes: any = date1.getMinutes();
if (minutes < 10) {
minutes = '0' + minutes;
}
let seconds: any = date1.getSeconds();
if (seconds < 10) {
seconds = '0' + seconds;
}
date.value.ymd = year + '-' + month + '-' + day;
date.value.hms = hours + ':' + minutes + ':' + seconds;
date.value.week = date1.getDay();
};
// 添加定时器,每秒更新一次时间
const timer = setInterval(setTime, 1000);
// 组件卸载时清除定时器
onUnmounted(() => { onUnmounted(() => {
clearInterval(timer); if (timer.value) {
}); clearInterval(timer.value)
}
})
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@ -107,6 +216,12 @@ onUnmounted(() => {
} }
} }
.header_right {
width: 100%;
height: 100%;
display: flex;
}
.title { .title {
color: #fff; color: #fff;
font-family: 'AlimamaShuHeiTi', sans-serif; font-family: 'AlimamaShuHeiTi', sans-serif;
@ -124,12 +239,6 @@ onUnmounted(() => {
font-size: 14px; font-size: 14px;
} }
.right {
width: 100%;
height: 100%;
display: flex;
}
/* 顶部栏容器Flex 水平布局 + 垂直居中 */ /* 顶部栏容器Flex 水平布局 + 垂直居中 */
.top-bar { .top-bar {
width: 100%; width: 100%;
@ -137,7 +246,6 @@ onUnmounted(() => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
// background-color: #1e2128;
color: #fff; color: #fff;
padding: 8px 16px; padding: 8px 16px;
font-size: 14px; font-size: 14px;
@ -145,24 +253,37 @@ onUnmounted(() => {
/* 左侧区域(天气 + 日期):自身也用 Flex 水平排列,确保元素在一行 */ /* 左侧区域(天气 + 日期):自身也用 Flex 水平排列,确保元素在一行 */
.left-section { .left-section {
height: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
// margin-right: auto; /* 让右侧元素(管理系统)居右 */
}
.left-section img { .weather-list {
width: 32px; height: 60px;
height: 32px; overflow: hidden;
margin-right: 8px;
/* 图标与文字间距 */ .weather-item {
height: 60px;
line-height: 60px;
display: flex;
align-items: center;
&>div:last-child {
margin-left: 10px;
}
img {
width: 50px;
height: 50px;
}
}
}
} }
/* 分割线(视觉分隔,可根据需求调整样式) */ /* 分割线(视觉分隔,可根据需求调整样式) */
.divider { .divider {
display: grid; display: grid;
grid-template-rows: 1fr 1fr; grid-template-rows: 1fr 1fr;
height: 100%; gap: 2px;
/* 根据需要调整高度 */
padding: 14px 10px; padding: 14px 10px;
} }
@ -195,4 +316,11 @@ onUnmounted(() => {
margin-right: 6px; margin-right: 6px;
/* 图标与文字间距 */ /* 图标与文字间距 */
} }
.change {
display: grid;
place-items: center;
margin-right: 10px;
cursor: pointer;
}
</style> </style>

View File

@ -2,30 +2,53 @@
<div class="leftPage"> <div class="leftPage">
<div class="topPage"> <div class="topPage">
<Title title="项目公告" /> <Title title="项目公告" />
<div class="content"> <div class="content">
<div class="content_item" v-for="item in 6" :key="item"> <div class="content_item" v-for="item in news" :key="item.id">
<div class="round"> <img src="@/assets/projectLarge/round.svg" alt="">
<div class="sub_round"></div> <div class="ellipsis">
</div> {{ item.title }}
<div class="ellipsis">2025年6月23日 重庆市两江新区广场前期准备与审批完毕区广场前期准备与审批完毕前期准备与审批完毕区广场前期准备与审批完毕</div> <span @click="showNewsDetail(item)" style="color: rgba(138, 149, 165, 1);">{{ item.id === newId ? '关闭' :
'查看' }}</span>
</div> </div>
</div> </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"> <div class="endPage">
<Title title="人员情况" /> <Title title="人员情况" />
<div class="map"> <div class="map">
<img src="@/assets/projectLarge/map.svg" alt=""> <img src="@/assets/projectLarge/map.svg" alt="">
<!-- <div ref="mapChartRef"></div> -->
</div> </div>
<div class="attendance_tag"> <div class="attendance_tag">
<div class="tag_item" v-for="(item, index) in tagList" :key="index"> <div class="tag_item">
<img src="@/assets/projectLarge/people.svg" alt=""> <img src="@/assets/projectLarge/people.svg" alt="">
<div class="tag_title">{{ item.title }}</div> <div class="tag_title">出勤人</div>
<div class="tag_info"> <div class="tag_info">
{{ item.number }} {{ attendanceCount }}
<span style="font-size: 14px;">{{ index === 2 ? '%' : '人' }}</span> <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>
</div> </div>
@ -37,11 +60,11 @@
<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 v-for="item in list" :key="item.title" class="attendance_item"> <div v-for="item in teamAttendanceList" :key="item.id" class="attendance_item">
<div class="attendance_item_title">{{ item.title }}</div> <div class="attendance_item_title">{{ item.teamName }}</div>
<div class="attendance_item_number">{{ item.number }} <span class="subfont">/{{ item.number }}</span></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_rate">{{ item.attendanceRate }} %</div>
<div class="attendance_item_date subfont">{{ item.date }}</div> <div class="attendance_item_date subfont">{{ item.attendanceTime }}</div>
</div> </div>
</div> </div>
</div> </div>
@ -51,29 +74,101 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue" import { ref } from "vue"
import Title from './title.vue' import Title from './title.vue'
import { getScreenNews, getScreenPeople } from '@/api/projectScreen';
import { mapOption } from './optionList'
import * as echarts from 'echarts';
const list = ref([ const props = defineProps({
{ title: '智慧系统运维', number: 30, attendanceRate: 100, date: '2025-08-05 08:10' }, projectId: {
{ title: '智慧系统运维', number: 30, attendanceRate: 100, date: '2025-08-05 08:10' }, type: String,
{ title: '智慧系统运维', number: 30, attendanceRate: 100, date: '2025-08-05 08:10' }, default: ''
{ title: '智慧系统运维', number: 30, attendanceRate: 100, date: '2025-08-05 08:10' }, }
{ title: '智慧系统运维', number: 30, attendanceRate: 100, date: '2025-08-05 08:10' }, })
{ title: '智慧系统运维', number: 30, attendanceRate: 100, date: '2025-08-05 08:10' },
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 tagList = ref([ /**
{ title: '出勤人数', number: 259 }, * 显示新闻详情
{ title: '在岗人数', number: 100 }, */
{ title: '出勤率', number: 100 }, 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> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.leftPage { .leftPage {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: calc(25vw - 30px);
margin: 0 15px;
height: 100%; height: 100%;
.topPage, .topPage,
@ -115,11 +210,11 @@ const tagList = ref([
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
gap: 10px; gap: 10px;
// position: relative;
margin-bottom: 20px; margin-bottom: 20px;
font-size: 14px; font-size: 14px;
font-weight: 400; font-weight: 400;
color: rgba(230, 247, 255, 1); color: rgba(230, 247, 255, 1);
cursor: pointer;
.ellipsis { .ellipsis {
display: -webkit-box; display: -webkit-box;
@ -134,21 +229,8 @@ const tagList = ref([
margin-bottom: 0; margin-bottom: 0;
} }
.round { img {
display: grid;
place-items: center;
margin-top: 3px; margin-top: 3px;
width: 12px;
height: 12px;
border-radius: 50%;
background: rgba(29, 214, 255, 0.3);
.sub_round {
width: 6px;
height: 6px;
border-radius: 50%;
background: #1DD6FF;
}
} }
} }
} }
@ -201,4 +283,45 @@ const tagList = ref([
.subfont { .subfont {
color: rgba(138, 149, 165, 1); 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> </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

@ -2,172 +2,61 @@
<div class="leftPage"> <div class="leftPage">
<div class="topPage"> <div class="topPage">
<Title title="项目概况" /> <Title title="项目概况" />
<div class="content" v-html="generalize"></div>
<div class="content">
<div class="content_item">项目名称智慧生态工地社区开发项目</div>
<div class="content_item">项目位置贵州省贵阳市乌当区具体地块编号01-123-11</div>
<div class="content_item">占地面积约10000亩</div>
<div class="content_item"> 土地性质城镇住宅用地兼容商业用地容积率2.5</div>
</div>
</div> </div>
<div class="endPage"> <div class="endPage">
<!-- 饼图容器 -->
<Title title="形象进度" /> <Title title="形象进度" />
<div class="chart_container">
<div ref="pieChartRef" class="echart" /> <div ref="pieChartRef" class="echart" />
<!-- 折线图容器 -->
<div ref="lineChartRef" class="echart" /> <div ref="lineChartRef" class="echart" />
</div> </div>
</div> </div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onUnmounted, nextTick } from "vue" import { ref, onMounted, onUnmounted, nextTick } from "vue"
import Title from './title.vue' import Title from './title.vue'
import * as echarts from 'echarts'; 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); const pieChartRef = ref<HTMLDivElement | null>(null);
let pieChart: any = null; let pieChart: any = null;
const totalPercent = ref(0)
// 折线图相关 // 折线图相关
const lineChartRef = ref<HTMLDivElement | null>(null); const lineChartRef = ref<HTMLDivElement | null>(null);
let lineChart: any = null; let lineChart: any = null;
// 土地数据 折线图
const designAreaData = ref([])
const transferAreaData = ref([])
// 饼图数据 // 饼图数据
const pieData = [ const pieData = [
{ name: '桩点浇筑', value: 13 }, { label: 'areaPercentage', name: '厂区', value: 0 },
{ name: '水泥灌注', value: 7 }, { label: 'roadPercentage', name: '道路', value: 0 },
{ name: '箱变安装', value: 40 }, { label: 'collectorLinePercentage', name: '集电线路', value: 0 },
{ name: '支架安装', value: 20 }, { label: 'exportLinePercentage', name: '送出线路', value: 0 },
{ name: '组件安装', value: 20 }, { label: 'substationPercentage', name: '升压站', value: 0 },
{ label: 'boxTransformerPercentage', name: '箱变', value: 0 },
] ]
// 折线图数据
const barData = {
xAxis: ['地块1', '地块2', '地块3', '地块4', '地块5', '地块6'],
series: [
{
name: '计划流转面积',
data: [70, 25, 45, 115, 70, 85]
},
{
name: '已流转面积',
data: [105, 30, 150, 65, 80, 200]
}
]
}
// 饼图配置
const pieOption = {
series: {
type: 'pie',
data: pieData,
radius: [50, 80],
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'
},
}
};
// 柱状图配置
const barOption = {
legend: {
data: ['计划流转面积', '已流转面积'],
top: 0
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: barData.xAxis
},
yAxis: {
name: '单位m²',
type: 'value',
axisLabel: {
formatter: '{value}'
}
},
series: [
{
type: 'bar',
data: [], // 空数据仅用于承载markArea
markArea: {
silent: true, // 背景不响应交互
data: (() => {
const groupCount = 3; // 共3组6个月 ÷ 2
const groupWidth = 1.8; // 每组背景宽度覆盖2根柱子
const bgData = [];
for (let i = 0; i < groupCount; i++) {
const startX = i * 2 - 0.9; // 每组起始位置
const endX = startX + groupWidth; // 每组结束位置
bgData.push([
{ xAxis: startX, yAxis: 0 },
{ xAxis: endX, yAxis: 100 }
]);
}
return bgData;
})(),
itemStyle: {
color: 'rgba(255, 255, 255, 0.05)',
borderRadius: 4
}
}
},
{
name: '计划流转面积',
type: 'bar',
data: barData.series[0].data,
barWidth: 15, // 柱形宽度
itemStyle: {
color: 'rgb(29, 253, 253)'
},
},
{
name: '已流转面积',
type: 'bar',
data: barData.series[1].data,
barWidth: 15,
itemStyle: {
color: '#rgb(25, 181, 251)'
},
}
]
};
// 初始化饼图 // 初始化饼图
const initPieChart = () => { const initPieChart = () => {
if (!pieChartRef.value) { if (!pieChartRef.value) {
console.error('未找到饼图容器元素'); console.error('未找到饼图容器元素');
return; return;
} }
pieOption.series.data = pieData
pieOption.graphic[0].style.text = totalPercent.value + '%'
pieChart = echarts.init(pieChartRef.value, null, { pieChart = echarts.init(pieChartRef.value, null, {
renderer: 'canvas', renderer: 'canvas',
useDirtyRect: false useDirtyRect: false
@ -181,6 +70,8 @@ const initLineChart = () => {
console.error('未找到折线图容器元素'); console.error('未找到折线图容器元素');
return; return;
} }
barOption.series[0].data = designAreaData.value
barOption.series[1].data = transferAreaData.value
lineChart = echarts.init(lineChartRef.value, null, { lineChart = echarts.init(lineChartRef.value, null, {
renderer: 'canvas', renderer: 'canvas',
useDirtyRect: false useDirtyRect: false
@ -194,11 +85,52 @@ const handleResize = () => {
if (lineChart) lineChart.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(() => { onMounted(() => {
getScreenLandData()
getScreenImgProcessData()
getScreenGeneralizeData()
nextTick(() => { nextTick(() => {
initPieChart(); initPieChart();
initLineChart();
window.addEventListener('resize', handleResize); window.addEventListener('resize', handleResize);
}); });
}); });
@ -221,8 +153,6 @@ onUnmounted(() => {
.leftPage { .leftPage {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: calc(25vw - 30px);
margin: 0 15px;
height: 100%; height: 100%;
.topPage, .topPage,
@ -240,15 +170,37 @@ onUnmounted(() => {
flex: 1; flex: 1;
margin-top: 23px; margin-top: 23px;
.echart { .chart_container {
display: flex;
flex-direction: column;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.echart {
height: 50%;
width: 100%;
}
} }
} }
.content { .content {
margin: 10px 35px; 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 { .content_item {
font-size: 14px; font-size: 14px;
@ -262,12 +214,6 @@ onUnmounted(() => {
} }
} }
.ellipse {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.subfont { .subfont {
color: rgba(138, 149, 165, 1); color: rgba(138, 149, 165, 1);
} }

View File

@ -4,8 +4,8 @@
<img src="@/assets/projectLarge/section.svg" alt=""> <img src="@/assets/projectLarge/section.svg" alt="">
<img src="@/assets/projectLarge/border.svg" alt=""> <img src="@/assets/projectLarge/border.svg" alt="">
</div> </div>
<div v-if="prefix"> <div>
<img src="@/assets/projectLarge/robot.svg" alt="" style="width: 20px; height: 20px;margin-right: 5px;"> <slot></slot>
</div> </div>
<div>{{ title }}</div> <div>{{ title }}</div>
</div> </div>

View File

@ -1,45 +1,111 @@
<template> <template>
<div class="large-screen"> <div class="large_screen">
<Header /> <Header :projectId="projectId" :isFull="isFull" @changePage="handleChangePage" />
<div class="nav"> <div class="nav">
<leftPage /> <div class="nav_left" :style="{ left: isHideOther ? '-25vw' : '0' }">
<centerPage /> <leftPage :projectId="projectId" />
<rightPage /> </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>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue';
import Header from './components/header.vue'; import Header from './components/header.vue';
import leftPage from './components/leftPage.vue'; import leftPage from './components/leftPage.vue';
import centerPage from './components/centerPage.vue'; import centerPage from './components/centerPage.vue';
import rightPage from './components/rightPage.vue'; import rightPage from './components/rightPage.vue';
<<<<<<< HEAD
=======
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;
}
}
>>>>>>> 290fc16c320aeeee8ecb31af446b0ad7d555e954
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.large-screen { .large_screen {
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
background: url('@/assets/large/bg.png') no-repeat; background: url('@/assets/large/bg.png') no-repeat;
background-size: 100% 100%; background-size: 100% 100%;
background-attachment: fixed;
background-color: rgba(4, 7, 17, 1); background-color: rgba(4, 7, 17, 1);
overflow: hidden;
} }
.nav { .nav {
display: flex; position: relative;
gap: 15rpx; display: grid;
width: 100%; place-items: center;
height: calc(100vh - 100px); width: calc(100vw - 30px);
height: calc(100vh - 90px);
margin: 0 auto;
box-sizing: border-box; box-sizing: border-box;
color: #fff; color: #fff;
} }
.nav_left, .nav_left,
.nav_right { .nav_right {
margin: 0 15px 15px 15px; 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 { .nav_center {
margin-bottom: 15px; 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> </style>