diff --git a/src/api/enterpriseLarge/index.ts b/src/api/enterpriseLarge/index.ts index d94bf2c..da891e7 100644 --- a/src/api/enterpriseLarge/index.ts +++ b/src/api/enterpriseLarge/index.ts @@ -11,3 +11,34 @@ export const keyIndex = () => { method: 'get' }); }; + +/** + * 每个项目的出勤人数 + */ + +export const projectAttendanceCount = () => { + return request({ + url: '/enterprise/big/screen/projectAttendanceCount', + method: 'get' + }); +}; +/** + * 人数统计 + */ + +export const peopleCount = () => { + return request({ + url: '/enterprise/big/screen/peopleCount', + method: 'get' + }); +}; +/** + * 出勤人数统计 + */ + +export const allAttendanceCount = () => { + return request({ + url: '/enterprise/big/screen/allAttendanceCount', + method: 'get' + }); +}; diff --git a/src/assets/images/man.png b/src/assets/images/man.png new file mode 100644 index 0000000..a583fa4 Binary files /dev/null and b/src/assets/images/man.png differ diff --git a/src/assets/images/map.png b/src/assets/images/map.png new file mode 100644 index 0000000..7d3db53 Binary files /dev/null and b/src/assets/images/map.png differ diff --git a/src/views/enterpriseLarge/digitalizationScreen/components/leftPage.vue b/src/views/enterpriseLarge/digitalizationScreen/components/leftPage.vue index 47717e6..fa16dea 100644 --- a/src/views/enterpriseLarge/digitalizationScreen/components/leftPage.vue +++ b/src/views/enterpriseLarge/digitalizationScreen/components/leftPage.vue @@ -18,43 +18,73 @@
- <div class="map"> - <div class="project_attendance_chart"> - <Title style="font-size: 22px" title="各项目人员出勤率" /> - <div class="chart_content" ref="attendanceChartRef"></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 class="people_overview"> + <div class="people_overview_content"> + <div class="people_image"> + <!-- 本地图片占位,后续可替换为实际图片路径 --> + <img src="@/assets/images/man.png" alt="人员总览" class="placeholder_image" /> </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 class="people_stats"> + <div class="stat_item"> + <div class="stat_label">出勤人数</div> + <div class="stat_value">{{ attendanceCount }}</div> </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 class="stat_item"> + <div class="stat_label">出勤率</div> + <div class="stat_rate">{{ attendanceRate }}%</div> </div> </div> </div> </div> + <!-- 点阵地图 --> + <div class="people_map"> + <div class="map_container"> + <img src="@/assets/images/map.png" /> + </div> + </div> - <div class="attendance_tag"></div> + <!-- 人员分类统计 --> + <div class="people_categories"> + <div class="category_item"> + <div class="category_icon"> + <!-- 本地图片占位,后续可替换为实际图片路径 --> + <img src="@/assets/images/constructor.png" alt="施工人员" class="category_image" /> + </div> + <div class="category_info"> + <div class="category_label">施工人员</div> + <div class="category_count">{{ constructionPersonnelCount }}</div> + </div> + </div> + <div class="category_item"> + <div class="category_icon"> + <!-- 本地图片占位,后续可替换为实际图片路径 --> + <img src="@/assets/images/subcontractor.png" alt="分包人员" class="category_image" /> + </div> + <div class="category_info"> + <div class="category_label">分包人员</div> + <div class="category_count">{{ subcontractorsCount }}</div> + </div> + </div> + <div class="category_item"> + <div class="category_icon"> + <!-- 本地图片占位,后续可替换为实际图片路径 --> + <img src="@/assets/images/manager.png" alt="管理人员" class="category_image" /> + </div> + <div class="category_info"> + <div class="category_label">管理人员</div> + <div class="category_count">{{ managersCount }}</div> + </div> + </div> + </div> + + <!-- 项目出勤率柱状图 --> + <div class="project_attendance_chart"> + <Title style="font-size: 22px" title="项目出勤率统计" /> + + <div class="chart_content" ref="attendanceChartRef"></div> + </div> </div> - - <!-- 项目出勤率柱状图 --> </div> </template> @@ -62,7 +92,7 @@ import { ref } from 'vue'; import Title from './title.vue'; import { getScreenNews, getScreenPeople } from '@/api/projectScreen'; -import { keyIndex } from '@/api/enterpriseLarge/index'; +import { keyIndex, projectAttendanceCount, peopleCount, allAttendanceCount } from '@/api/enterpriseLarge/index'; import { mapOption } from './optionList'; import * as echarts from 'echarts'; @@ -114,9 +144,14 @@ const newDetail = ref({ const newId = ref(''); const attendanceCount = ref(0); const attendanceRate = ref(0); -const peopleCount = ref(0); +const totalPeopleCount = ref(0); const teamAttendanceList = ref([{ id: '', teamName: '', attendanceNumber: 0, allNumber: 0, attendanceRate: 0, attendanceTime: '' }]); +// 人员分类统计数据 +const constructionPersonnelCount = ref(0); +const managersCount = ref(0); +const subcontractorsCount = ref(0); + // 项目出勤率数据 const projectAttendanceData = ref([ { name: 'A项目', value: 62 }, @@ -129,6 +164,11 @@ const projectAttendanceData = ref([ let attendanceChart = null; const attendanceChartRef = ref<HTMLDivElement | null>(null); +// 滚动相关状态 +const scrollInterval = ref<number | null>(null); +const currentScrollIndex = ref(0); +const scrollSpeed = 1000; // 滚动间隔时间(ms) + /** * 获取项目人员出勤数据 */ @@ -136,13 +176,189 @@ 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; + totalPeopleCount.value = data.peopleCount; teamAttendanceList.value = data.teamAttendanceList; } }; +/** + * 获取人员分类统计数据 + */ +const getPeopleCategoryData = async () => { + try { + const res = await peopleCount(); + const { data, code } = res; + if (code === 200 && data) { + constructionPersonnelCount.value = data.constructionPersonnelCount || 0; + managersCount.value = data.managersCount || 0; + subcontractorsCount.value = data.subcontractorsCount || 0; + } + } catch (error) { + console.error('获取人员分类统计数据失败:', error); + } +}; + +/** + * 获取人员总览数据 - 通过allAttendanceCount接口 + */ +const getProjectAttendanceCount = async () => { + try { + const res = await allAttendanceCount(); + const { data, code } = res; + if (code === 0 && data) { + // 直接使用接口返回的数据 + attendanceCount.value = data.attendanceCount || 0; + attendanceRate.value = data.attendanceRate || 0; + } + } catch (error) { + console.error('获取人员总览数据失败:', error); + } +}; + +/** + * 滚动到指定项目 + */ +const scrollToProject = (index: number) => { + if (!attendanceChart || projectAttendanceData.value.length === 0) return; + + // 计算实际索引 + const realIndex = index % projectAttendanceData.value.length; + currentScrollIndex.value = realIndex; + + // 获取项目名称 + const projectName = projectAttendanceData.value[realIndex].name; + + // 触发tooltip显示 + attendanceChart.dispatchAction({ + type: 'showTip', + seriesIndex: 0, + dataIndex: realIndex + }); + + // 高亮当前项目的柱子 + attendanceChart.dispatchAction({ + type: 'highlight', + seriesIndex: 0, + dataIndex: realIndex + }); + + // 取消高亮其他项目的柱子 + attendanceChart.dispatchAction({ + type: 'downplay', + seriesIndex: 0, + dataIndex: Array.from({ length: projectAttendanceData.value.length }, (_, i) => i).filter((i) => i !== realIndex) + }); + + // 更新滚动条位置,确保滚动时滚动条同步移动 + // 无论项目数量多少,都应该同步滚动条 + if (projectAttendanceData.value.length > 0) { + // 计算滚动条应该移动到的位置 + // 确保当前项目居中显示 + const totalProjects = projectAttendanceData.value.length; + const visiblePercentage = 15; // 与dataZoom的end值保持一致 + const itemPercentage = 100 / totalProjects; // 每个项目所占总宽度的百分比 + + // 计算新的start值,使当前项目尽量居中显示 + let newStart = realIndex * itemPercentage - visiblePercentage / 2 + itemPercentage / 2; + + // 确保start值在有效范围内 + newStart = Math.max(0, Math.min(100 - visiblePercentage, newStart)); + + // 更新dataZoom组件的位置 + attendanceChart.dispatchAction({ + type: 'dataZoom', + start: newStart, + end: newStart + visiblePercentage + }); + } +}; + +/** + * 开始自动滚动 + */ +const startScroll = () => { + if (scrollInterval.value) return; + + scrollInterval.value = window.setInterval(() => { + currentScrollIndex.value++; + scrollToProject(currentScrollIndex.value); + }, scrollSpeed); +}; + +/** + * 停止自动滚动 + */ +const stopScroll = () => { + if (scrollInterval.value) { + clearInterval(scrollInterval.value); + scrollInterval.value = null; + } +}; + +/** + * 获取项目出勤率统计数据 - 保持项目出勤率图表功能 + */ +const getProjectAttendanceStats = async () => { + try { + const res = await projectAttendanceCount(); + const { data, code } = res; + + // 添加日志信息以便调试 + console.log('projectAttendanceCount接口返回数据:', res); + + if (code === 200 && data && data.length > 0) { + console.log('有效数据:', data); + + // 更新项目出勤率图表数据 + projectAttendanceData.value = data.map((project) => ({ + name: project.projectName, + value: project.attendanceRate + })); + + console.log('处理后的数据:', projectAttendanceData.value); + + // 如果图表已初始化,重新设置数据 + if (attendanceChart) { + console.log('更新图表数据'); + attendanceChart.setOption({ + xAxis: { + data: projectAttendanceData.value.map((item) => item.name) + }, + series: [ + { + data: projectAttendanceData.value.map((item) => item.value) + } + ] + }); + } + } else { + console.warn('数据不符合预期:', { code, data }); + // 使用默认数据确保图表有内容显示 + if (!projectAttendanceData.value.length) { + projectAttendanceData.value = [ + { name: 'A项目', value: 62 }, + { name: 'B项目', value: 56 }, + { name: 'C项目', value: 95 }, + { name: 'D项目', value: 64 }, + { name: 'E项目', value: 97.5 } + ]; + } + } + } catch (error) { + console.error('获取项目出勤率数据失败:', error); + // 发生错误时使用默认数据 + if (!projectAttendanceData.value.length) { + projectAttendanceData.value = [ + { name: 'A项目', value: 62 }, + { name: 'B项目', value: 56 }, + { name: 'C项目', value: 95 }, + { name: 'D项目', value: 64 }, + { name: 'E项目', value: 97.5 } + ]; + } + } +}; + /** * 获取企业关键指标数据 */ @@ -163,8 +379,23 @@ const getKeyIndexData = async () => { */ const initAttendanceChart = () => { if (!attendanceChartRef.value) { + console.warn('attendanceChartRef不存在'); return; } + + // 确保有数据可显示 + if (!projectAttendanceData.value || !projectAttendanceData.value.length) { + console.log('使用默认数据初始化图表'); + projectAttendanceData.value = [ + { name: 'A项目', value: 62 }, + { name: 'B项目', value: 56 }, + { name: 'C项目', value: 95 }, + { name: 'D项目', value: 64 }, + { name: 'E项目', value: 97.5 } + ]; + } + + console.log('开始初始化图表,当前数据:', projectAttendanceData.value); attendanceChart = echarts.init(attendanceChartRef.value); const option = { @@ -191,13 +422,45 @@ const initAttendanceChart = () => { grid: { top: 40, right: 30, - bottom: 40, + bottom: 60, // 增加底部空间以确保标签完全显示 left: 30, containLabel: true, backgroundColor: 'rgba(10, 24, 45, 0.1)', borderColor: 'rgba(29, 214, 255, 0.1)', borderWidth: 1 }, + // 添加dataZoom组件实现水平滚动 + dataZoom: [ + { + type: 'slider', + show: true, + xAxisIndex: [0], + start: 0, + end: 20, // 固定为20%,与visiblePercentage保持一致 + height: 20, + bottom: 10, + backgroundColor: 'rgba(10, 24, 45, 0.2)', + fillerColor: 'rgba(29, 214, 255, 0.2)', + borderColor: 'rgba(29, 214, 255, 0.3)', + textStyle: { + color: '#e6f7ff' + }, + handleStyle: { + color: 'rgba(29, 214, 255, 0.6)', + borderColor: 'rgba(255, 255, 255, 0.3)' + }, + moveHandleStyle: { + color: 'rgba(29, 214, 255, 0.8)' + } + }, + // 支持鼠标滚轮缩放 + { + type: 'inside', + xAxisIndex: [0], + start: 0, + end: 20 // 固定为20%,与visiblePercentage保持一致 + } + ], xAxis: { type: 'category', data: projectAttendanceData.value.map((item) => item.name), @@ -209,9 +472,18 @@ const initAttendanceChart = () => { axisLabel: { color: '#e6f7ff', fontSize: 14, - interval: 0, + interval: 0, // 强制显示所有标签 fontWeight: 'bold', - padding: [10, 0, 0, 0] + padding: [10, 0, 0, 0], + // 防止标签重叠,旋转角度调整 + rotate: 0, + // 添加formatter函数,当名称超过6个汉字时显示省略号 + formatter: function (value) { + if (value.length > 6) { + return value.substring(0, 6) + '...'; + } + return value; + } }, axisTick: { show: true, @@ -321,6 +593,19 @@ const initAttendanceChart = () => { attendanceChart.setOption(option); + // 添加鼠标悬浮事件监听 + if (attendanceChartRef.value) { + // 鼠标进入图表区域时停止滚动 + attendanceChartRef.value.addEventListener('mouseenter', () => { + stopScroll(); + }); + + // 鼠标离开图表区域时重新开始滚动 + attendanceChartRef.value.addEventListener('mouseleave', () => { + startScroll(); + }); + } + // 添加窗口大小变化时的图表更新 const handleResize = () => { if (attendanceChart) { @@ -333,6 +618,12 @@ const initAttendanceChart = () => { // 清理函数 onUnmounted(() => { window.removeEventListener('resize', handleResize); + + // 移除鼠标事件监听 + if (attendanceChartRef.value) { + attendanceChartRef.value.removeEventListener('mouseenter', stopScroll); + attendanceChartRef.value.removeEventListener('mouseleave', startScroll); + } }); }; @@ -347,17 +638,23 @@ const initMapChart = () => { mapChart.setOption(mapOption); }; -onMounted(() => { +onMounted(async () => { // nextTick(() => { // initMapChart(); // }); getPeopleData(); getKeyIndexData(); + getProjectAttendanceCount(); // 获取人员总览数据 + getPeopleCategoryData(); - // 初始化项目出勤率柱状图 - setTimeout(() => { - initAttendanceChart(); - }, 100); + // 先等待获取项目出勤率数据 + await getProjectAttendanceStats(); // 获取项目出勤率图表数据 + + // 再初始化图表 + initAttendanceChart(); + + // 图表初始化后自动开始滚动 + startScroll(); }); onUnmounted(() => { @@ -370,6 +667,9 @@ onUnmounted(() => { attendanceChart.dispose(); attendanceChart = null; } + + // 清理滚动计时器 + stopScroll(); }); </script> @@ -383,37 +683,59 @@ onUnmounted(() => { .endPage { display: flex; flex-direction: column; - align-items: center; width: 100%; - padding: 15px 0; + height: 250; border: 1px solid rgba(29, 214, 255, 0.1); box-sizing: border-box; } - .endPage { + height: auto; flex: 1; - margin-top: 23px; + min-height: 550px; } - .project_attendance_chart { 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; + margin-top: 15px; + padding-bottom: 10px; .chart_title { font-size: 16px; color: #e6f7ff; - margin-bottom: 15px; + margin-bottom: 5px; text-align: left; } + + .scroll_controls { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-bottom: 10px; + padding-right: 10px; + } + + .control_btn { + padding: 5px 15px; + background: rgba(10, 24, 45, 0.8); + border: 1px solid rgba(29, 214, 255, 0.3); + color: #e6f7ff; + font-size: 12px; + cursor: pointer; + border-radius: 4px; + transition: all 0.3s ease; + } + + .control_btn:hover { + background: rgba(29, 214, 255, 0.2); + border-color: rgba(29, 214, 255, 0.6); + } + .chart_content { width: 100%; - height: 200px; + height: 320px; /* 增加高度以容纳滚动条 */ position: relative; + overflow: hidden; } } } @@ -422,20 +744,20 @@ onUnmounted(() => { width: 100%; display: grid; grid-template-columns: repeat(2, 1fr); - gap: 15px; - margin-top: 15px; - padding: 0 15px; + gap: 10px; + margin-bottom: 8px; + padding: 0 10px; box-sizing: border-box; } .indicator-card { position: relative; width: 100%; - height: 100px; + height: 80px; background: rgba(10, 24, 45, 0.7); border: 1px solid rgba(29, 214, 255, 0.2); border-radius: 4px; - padding: 15px; + padding: 10px; box-sizing: border-box; display: flex; flex-direction: column; @@ -488,53 +810,151 @@ onUnmounted(() => { object-fit: contain; } -.map { - margin-top: 15px; -} - -.attendance_tag { +/* 人员总览样式 */ +.people_overview { width: 100%; + margin-top: 5px; + box-sizing: border-box; + border: 1px solid rgba(29, 214, 255, 0.1); + background: rgba(10, 24, 45, 0.3); +} + +.people_overview_title { + font-size: 16px; + color: #e6f7ff; + margin-bottom: 5px; + border-bottom: 1px solid rgba(29, 214, 255, 0.1); +} + +.people_overview_content { 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); - } - } + align-items: center; } -.attendance_list { - padding: 0px 30px; +.people_image { + width: 60px; + height: 60px; + background: linear-gradient(135deg, rgba(29, 214, 255, 0.1) 0%, rgba(29, 214, 255, 0.05) 100%); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.placeholder_image { + width: 100%; + height: 100%; + object-fit: contain; +} + +.people_stats { + flex: 1; + display: flex; + justify-content: space-around; +} + +.stat_item { + display: flex; + flex-direction: column; + align-items: center; +} + +.stat_label { font-size: 14px; - - .attendance_item { - display: grid; - grid-template-columns: 3fr 2fr 2fr 3fr; - margin-top: 20px; - } + color: #8ab2ff; + margin-bottom: 5px; } -.subfont { - color: rgba(138, 149, 165, 1); +.stat_value { + font-size: 28px; + font-weight: bold; + color: #e6f7ff; + text-shadow: 0px 1.24px 6.21px rgba(0, 190, 247, 0.5); +} + +.stat_rate { + font-size: 28px; + font-weight: bold; + color: #00c853; + text-shadow: 0px 1.24px 6.21px rgba(0, 200, 83, 0.5); +} + +/* 点阵地图样式 */ +.people_map { + width: 100%; + height: 120px; + margin-top: 8px; + background: rgba(10, 24, 45, 0.5); + border: 1px solid rgba(29, 214, 255, 0.1); + border-radius: 4px; + overflow: hidden; + position: relative; +} + +.map_background { + width: 100%; + height: 100%; + background-image: radial-gradient(circle, rgba(29, 214, 255, 0.3) 1px, transparent 1px), + radial-gradient(circle, rgba(29, 214, 255, 0.3) 1px, transparent 1px); + background-size: 40px 40px; + background-position: + 0, + 0, + 20px 20px; + position: relative; +} + +/* 人员分类统计样式 */ +.people_categories { + display: flex; + justify-content: space-around; + margin-top: 5px; +} + +.category_item { + display: flex; + flex: 1; + margin: 10px, 30px; +} + +.category_icon { + margin-left: 15px; + margin-top: 10px; + width: 30px; + height: 30px; + background: linear-gradient(135deg, rgba(29, 214, 255, 0.1) 0%, rgba(10, 120, 200, 0.1) 100%); + border: 1px solid rgba(29, 214, 255, 0.2); + border-radius: 5px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.category_image { + width: 45px; + height: 45px; + object-fit: contain; +} + +.category_info { + display: flex; + flex-direction: column; + margin-left: 8px; + margin-top: 8px; + flex: 1; +} + +.category_label { + font-size: 12px; + color: #8ab2ff; + margin-bottom: 1px; +} + +.category_count { + font-size: 16px; + font-weight: bold; + color: #e6f7ff; + text-shadow: 0px 1.24px 6.21px rgba(0, 190, 247, 0.3); } </style>