| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658 |
- <template>
- <ChartsContainer title="整体情况">
- <template #title>
- <div class="download-section">
- <el-button type="primary" size="small" @click="handleDownloadReport" :loading="downloadLoading">
- 下载整体报表
- </el-button>
- </div>
- </template>
- <div class="overview-content">
- <!-- 选择区域 -->
- <div class="selection-section">
- <div class="selection-row" v-for="(row, rowIndex) in selectionRows" :key="rowIndex">
- <div v-for="(item, index) in row" :key="index" class="selection-item">
- <CustomStyleSelect type="tree" :propsConfig="{ value: 'id', showPrefix: false }" v-model="item.selectedId"
- :options="departments" @change="handleSelectionChange(rowIndex * 2 + index, $event)" />
- </div>
- </div>
- </div>
- <!-- 雷达图 -->
- <div class="radar-chart" ref="radarChartRef"></div>
- <!-- 遍历展示的数据 -->
- <div class="data-list" v-if="dataItems.length > 0">
- <div class="data-item" v-for="(item, index) in dataItems" :key="index">
- <div class="data-title" :style="{ color: '#70D3EE' }">{{ item.title }}</div>
- <div class="data-values">
- <div class="value-label" v-for="(data, dataIndex) in item.list" :key="dataIndex">
- <div class="value">{{ data.value }}</div>
- <div class="label">{{ data.label }}</div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </ChartsContainer>
- </template>
- <script setup>
- import { ref, onMounted, onUnmounted, watch, computed } from 'vue'
- import * as echarts from 'echarts'
- import ChartsContainer from '../ChartsContainer.vue'
- import CustomStyleSelect from './CustomStyleSelect.vue'
- import { getDeptUserTree } from '@/api/item/items'
- import { getHomePageDetail, getHomeReportDownload } from '@/api/largeScreen/largeScreen'
- const radarChartRef = ref(null)
- let radarChart = null
- // 下载相关变量
- const downloadLoading = ref(false)
- // 定义props,从父组件接收数据
- const props = defineProps({
- timeRange: {
- type: String,
- default: 'year'
- },
- startDate: {
- type: String,
- default: ''
- },
- endDate: {
- type: String,
- default: ''
- },
- selectedRole: {
- type: String,
- default: 'individual'
- }
- })
- // 遍历展示的数据
- const dataItems = ref([])
- // 选择相关数据
- const selectionItems = ref([
- ])
- // 计算属性,将选择项分成两行,每行两个
- const selectionRows = computed(() => {
- const rows = []
- for (let i = 0; i < selectionItems.value.length; i += 2) {
- rows.push(selectionItems.value.slice(i, i + 2))
- }
- return rows
- })
- const departments = ref([])
- // 计算日期范围(移植自HomePage.vue)
- const calculateDateRange = (timeRange, customStartDate, customEndDate) => {
- const today = new Date()
- const yesterday = new Date(today)
- yesterday.setDate(today.getDate() - 1)
- let startDate = new Date(yesterday)
- let endDate = new Date(yesterday)
- switch (timeRange) {
- case 'week':
- startDate.setDate(yesterday.getDate() - 6)
- break
- case 'month':
- startDate.setDate(yesterday.getDate() - 29)
- break
- case 'quarter':
- startDate.setDate(yesterday.getDate() - 89)
- break
- case 'halfYear':
- startDate.setDate(yesterday.getDate() - 179)
- break
- case 'year':
- startDate.setDate(yesterday.getDate() - 364)
- break
- case 'custom':
- if (customStartDate && customEndDate) {
- startDate = new Date(customStartDate)
- endDate = new Date(customEndDate)
- }
- break
- default:
- startDate.setDate(yesterday.getDate() - 364)
- }
- return {
- startDate: formatDateForInput(startDate),
- endDate: formatDateForInput(endDate)
- }
- }
- // 格式化日期为输入框格式
- const formatDateForInput = (date) => {
- const year = date.getFullYear()
- const month = String(date.getMonth() + 1).padStart(2, '0')
- const day = String(date.getDate()).padStart(2, '0')
- return `${year}-${month}-${day}`
- }
- // 监听时间参数变化
- watch(() => [props.timeRange, props.startDate, props.endDate, props.selectedRole], () => {
- // 当时间参数变化时,重新获取数据
- if (selectionItems.value.some(item => item.selectedId)) {
- fetchHomePageDetailData()
- }
- }, { immediate: true })
- // 更新雷达图数据
- const updateChartWithData = (data) => {
- if (!radarChartRef.value) {
- console.warn('雷达图容器未找到')
- return
- }
- if (!radarChart) {
- radarChart = echarts.init(radarChartRef.value)
- }
- // 根据接口数据更新雷达图
- if (data && data.length > 0) {
- // 使用Graph后缀的数据来显示雷达图
- const seriesData = data.map(item => ({
- name: item.name,
- value: [
- item.seizureCountGraph || 0,
- item.workingHoursGraph || 0,
- item.checkPassRateGraph || 0,
- item.answersAccuracyGraph || 0,
- item.learningGrowthScoreGraph || 0
- ]
- }))
- // 计算每个指标的最大值,用于雷达图的max值
- const indicators = [
- { name: '查获能力', max: Math.max(...seriesData.map(d => d.value[0])) },
- { name: '在岗时长', max: Math.max(...seriesData.map(d => d.value[1])) },
- { name: '巡检合格率', max: Math.max(...seriesData.map(d => d.value[2])) },
- { name: '抽问抽答', max: Math.max(...seriesData.map(d => d.value[3])) },
- { name: '培训答题', max: Math.max(...seriesData.map(d => d.value[4])) }
- ]
- const option = {
- legend: {
- type: 'scroll',
- orient: 'horizontal',
- bottom: 10,
- textStyle: {
- fontSize: 12,
- color: '#fff'
- },
- itemWidth: 12,
- itemHeight: 12,
- data: seriesData.map(item => item.name)
- },
- radar: {
- shape: 'circle',
- indicator: indicators,
- splitNumber: 4,
- center: ['50%', '45%'],
- radius: '65%',
- axisName: {
- color: '#fff',
- fontSize: 12
- },
- splitLine: {
- lineStyle: {
- color: ['rgba(255, 255, 255, 0.3)', 'rgba(255, 255, 255, 0.2)', 'rgba(255, 255, 255, 0.1)']
- }
- },
- splitArea: {
- show: true,
- areaStyle: {
- color: ['rgba(255, 255, 255, 0.1)', 'rgba(255, 255, 255, 0.05)']
- }
- },
- axisLine: {
- lineStyle: {
- color: 'rgba(255, 255, 255, 0.5)'
- }
- }
- },
- series: [
- {
- name: '能力对比',
- type: 'radar',
- data: seriesData.map((item, index) => ({
- value: item.value,
- name: item.name,
- areaStyle: {
- color: getChartColor(index, 0.3)
- },
- lineStyle: {
- color: getChartColor(index),
- width: 2
- },
- itemStyle: {
- color: getChartColor(index)
- }
- }))
- }
- ],
- tooltip: {
- show: false,
- // trigger: 'item',
- // formatter: function (params) {
- // const data = params.data
- // const name = params.name
- // const value = params.value
- // let tooltipHtml = `<div style="font-weight: bold; margin-bottom: 8px;">${name}</div>`
- // const labels = ['查获数量', '在岗时长', '巡检合格率', '抽问抽答', '培训答题']
- // value.forEach((val, index) => {
- // tooltipHtml += `<div style="display: flex; justify-content: space-between; margin: 4px 0;">
- // <span>${labels[index]}:</span>
- // <span style="font-weight: bold;">${val}</span>
- // </div>`
- // })
- // return tooltipHtml
- // }
- }
- }
- radarChart.setOption(option)
- }
- }
- // 获取图表颜色
- const getChartColor = (index, opacity = 1) => {
- const colors = ['#8FA5EC', '#FF9F7F', '#6ECEB2', '#FFD700', '#BA55D3']
- const color = colors[index % colors.length]
- if (opacity < 1) {
- // 转换为RGBA格式
- const r = parseInt(color.slice(1, 3), 16)
- const g = parseInt(color.slice(3, 5), 16)
- const b = parseInt(color.slice(5, 7), 16)
- return `rgba(${r}, ${g}, ${b}, ${opacity})`
- }
- return color
- }
- // 获取部门和人员树数据
- const fetchDepartments = async () => {
- try {
- const res = await getDeptUserTree()
- departments.value = res.data
-
- // 数据加载完成后,查找deptType为BRIGADE的对象
- if (res.data && res.data.length > 0) {
- // 查找deptType为BRIGADE的对象
- const findBrigadeItems = (items) => {
- const brigadeItems = []
- for (const item of items) {
- if (item.deptType === 'BRIGADE') {
- brigadeItems.push(item)
- }
- if (item.children && item.children.length > 0) {
- const childBrigadeItems = findBrigadeItems(item.children)
- brigadeItems.push(...childBrigadeItems)
- }
- }
- return brigadeItems
- }
- const brigadeItems = findBrigadeItems(res.data)
-
- // 设置前4个BRIGADE对象作为默认选项
- if (brigadeItems.length > 0) {
- for (let i = 0; i < Math.min(brigadeItems.length, 4); i++) {
- selectionItems.value[i] = {
- ...brigadeItems[i],
- selectedId: brigadeItems[i].id,
- selectedName: brigadeItems[i].label || brigadeItems[i].name
- }
- }
- // 触发数据请求
- fetchHomePageDetailData()
- }
- }
- } catch (error) {
- console.error('获取部门和人员树失败:', error)
- }
- }
- // 选择变化处理
- const handleSelectionChange = (index, selectedItem) => {
- console.log(selectedItem, "selectedItem")
- if (selectedItem && selectedItem.value) {
- // 查找选中的部门或人员信息
- const findSelectedItem = (items, targetId) => {
- for (const item of items) {
- if (item.id === targetId) {
- return item
- }
- if (item.children && item.children.length > 0) {
- const found = findSelectedItem(item.children, targetId)
- if (found) return found
- }
- }
- return null
- }
- const itemInfo = findSelectedItem(departments.value, selectedItem.value)
- if (itemInfo) {
- selectionItems.value[index] = {
- ...selectionItems.value[index],
- selectedId: selectedItem.value,
- selectedName: itemInfo.label || itemInfo.name
- }
- }
- } else {
- // 清空选择
- selectionItems.value[index] = {
- ...selectionItems.value[index],
- selectedId: '',
- selectedName: ''
- }
- }
- // 触发数据请求
- fetchHomePageDetailData()
- }
- // 获取首页详情数据
- const fetchHomePageDetailData = async () => {
- // 构建参数数组
- const params = []
- // 计算开始和结束日期
- let startDate = props.startDate
- let endDate = props.endDate
- // 如果使用预设时间范围,计算日期
- if (props.timeRange !== 'custom' && (!startDate || !endDate)) {
- const dateRange = calculateDateRange(props.timeRange)
- startDate = dateRange.startDate
- endDate = dateRange.endDate
- }
- for (const item of selectionItems.value) {
- if (item.selectedId) {
- // 查找选中的项目类型
- const findItemType = (items, targetId) => {
- for (const item of items) {
- if (item.id === targetId) {
- return item.nodeType === 'user' ? 'USER' : 'DEPT'
- }
- if (item.children && item.children.length > 0) {
- const type = findItemType(item.children, targetId)
- if (type) return type
- }
- }
- }
- const type = findItemType(departments.value, item.selectedId)
- params.push({
- startDate: startDate,
- endDate: endDate,
- type: type,
- id: item.selectedId,
- name: item.selectedName,
- dataSource: props.selectedRole
- })
- }
- }
- console.log(params)
- if (params.length > 0) {
- try {
- const res = await getHomePageDetail(params)
- console.log('首页详情数据:', res)
- // 处理响应数据,更新图表和数据显示
- handleHomePageDetailResponse(res)
- } catch (error) {
- console.error('获取首页详情数据失败:', error)
- }
- } else {
- // 如果没有选择任何项,清空显示的数据
- dataItems.value = []
- if (radarChart) {
- radarChart.clear()
- }
- }
- }
- // 处理首页详情响应数据
- const handleHomePageDetailResponse = (res) => {
- if (res.data && Array.isArray(res.data)) {
- // 更新数据展示
- dataItems.value = res.data.map(item => {
- return {
- title: item.name + "数据明细",
- list: [
- { label: '人均查获数量', value: item.seizureCount || 0 },
- { label: '人均在岗时长', value: item.workingHours || 0 },
- { label: '巡检合格率', value: `${((item.checkPassRate * 100) || 0).toFixed(2)}%` },
- { label: '抽问抽答正确率', value: `${((item.answersAccuracy * 100) || 0).toFixed(2)}%` },
- { label: '培训答题平均分', value: item.learningGrowthScore || 0 }
- ]
- }
- })
- // 更新雷达图数据
- updateChartWithData(res.data)
- }
- }
- const handleResize = () => {
- if (radarChart) {
- radarChart.resize()
- }
- }
- onMounted(() => {
- window.addEventListener('resize', handleResize)
- // 获取部门和人员树数据
- fetchDepartments()
- })
- onUnmounted(() => {
- if (radarChart) {
- radarChart.dispose()
- }
- window.removeEventListener('resize', handleResize)
- })
- // 下载整体报表
- const handleDownloadReport = async () => {
- try {
- downloadLoading.value = true
- // 构建下载参数
- const downloadParams = {
- startDate: props.startDate,
- endDate: props.endDate,
- }
- // 调用下载接口
- const response = await getHomeReportDownload(downloadParams)
- // 处理下载响应 - 使用项目标准的blob处理方式
- if (response) {
- // 生成文件名
- const fileName = `整体报表_${new Date().toISOString().slice(0, 10)}.xlsx`
- // 使用file-saver的saveAs函数下载文件
- const { saveAs } = await import('file-saver')
- saveAs(response, fileName)
- }
- } catch (error) {
- console.error('下载报表失败:', error)
- // 这里可以添加错误提示
- } finally {
- downloadLoading.value = false
- }
- }
- </script>
- <style lang="scss" scoped>
- .download-section {
- display: flex;
- justify-content: flex-end;
- padding: 0 10px;
- .el-button {
- background: linear-gradient(135deg, #1CB6FF 0%, #0A8CD9 100%);
- border: none;
- border-radius: 4px;
- font-size: 12px;
- padding: 6px 12px;
- &:hover {
- background: linear-gradient(135deg, #0A8CD9 0%, #1CB6FF 100%);
- }
- }
- }
- .overview-content {
- height: 100%;
- display: flex;
- flex-direction: column;
- gap: 15px;
- .selection-section {
- display: flex;
- flex-direction: column;
- gap: 10px;
- margin-bottom: 15px;
- .selection-row {
- display: flex;
- gap: 15px;
- .selection-item {
- flex: 1;
- display: flex;
- flex-direction: column;
- gap: 8px;
- .selection-label {
- font-size: 14px;
- color: #70D3EE;
- font-weight: bold;
- text-align: center;
- }
- :deep(.custom-select) {
- .el-input__wrapper {
- background: #0C4F7A;
- border: none;
- box-shadow: none;
- .el-input__inner {
- color: #fff;
- font-size: 12px;
- }
- }
- }
- }
- }
- }
- .radar-chart {
- height: 350px;
- width: 100%;
- display: flex;
- justify-content: center;
- align-items: center;
- }
- .data-list {
- flex: 1;
- display: flex;
- flex-direction: column;
- gap: 8px;
- overflow-y: auto;
- .data-item {
- background: rgba(255, 255, 255, 0.05);
- border-radius: 6px;
- padding: 11px 25px;
- background: url('../../../../../assets/images/cardBg.png') no-repeat !important;
- background-size: 100% 105% !important;
- .data-title {
- font-size: 16px;
- font-weight: bold;
- margin-bottom: 10px;
- color: #70D3EE;
- }
- .data-values {
- display: flex;
- justify-content: space-around;
- .value-label {
- display: flex;
- flex-direction: column;
- align-items: center;
- .value {
- font-size: 17px;
- // font-weight: bold;
- color: #70D3EE;
- margin-bottom: 2px;
- }
- .label {
- font-size: 13px;
- color: rgba(255, 255, 255, 0.7);
- }
- }
- }
- }
- }
- }
- // /* 响应式设计 */
- // @media (max-width: 1280px) {
- // .overview-content {
- // .data-list {
- // flex-wrap: wrap;
- // .data-item {
- // flex: 0 0 calc(50% - 5px);
- // min-width: 120px;
- // }
- // }
- // }
- // }
- // @media (max-width: 768px) {
- // .overview-content {
- // .data-list {
- // .data-item {
- // flex: 0 0 100%;
- // }
- // }
- // }
- // }</style>
|