import * as echarts from 'echarts' import { getAllUnitList } from '@/api/auditEntityManage.js' // import { getAuditTaskList } from '@/api/auditInitiation.js' // , getAuditTaskList import { analyzeStatistics, getAuditTaskList } from '@/api/comprehensive' // 综合分析页面的通用mixin export const comprehensiveMixin = { data() { return { // 通用加载状态 loading: false, projectOptions: [], auditedUnitOptions: [], // 指标树基础配置 treeProps: { children: 'children', label: 'label', }, // 图表实例集合 chartInstances: {}, // 组件是否已销毁标志 isDestroyed: false, } }, mounted() { this.getOptions() // 监听窗口大小变化 window.addEventListener('resize', this.handleResize) }, beforeDestroy() { // 标记组件已销毁 this.isDestroyed = true // 销毁所有图表实例 this.destroyCharts() // 移除窗口大小变化监听 window.removeEventListener('resize', this.handleResize) }, methods: { // 根据索引获取颜色 getColorByIndex(index) { const colors = [ '#5470C6', // 蓝色 '#91CC75', // 绿色 '#FAC858', // 黄色 '#EE6666', // 红色 '#73C0DE', // 浅蓝色 '#3BA272', // 深绿色 '#FC8452', // 橙色 '#9A60B4', // 紫色 ] return colors[index % colors.length] }, // 将 costSurveysList 组装为树结构(用于左侧勾选) buildIndicatorTree(costSurveysList = []) { const processNode = (item) => { const nodeId = item.uniqueId || item.id || item.rowid || item.rowId || `${item.number || item.name || ''}` const node = { ...item, id: nodeId, label: this.buildIndicatorLabel(item), children: [], } // 如果有子项,递归处理 if (Array.isArray(item.costSurveysVos) && item.costSurveysVos.length) { node.children = item.costSurveysVos.map(processNode) // 对子节点按照orderNum从小到大排序 node.children.sort((a, b) => { const orderA = parseInt(a.orderNum || 0) const orderB = parseInt(b.orderNum || 0) return orderA - orderB }) } return node } // 处理根节点并按照orderNum从小到大排序 return costSurveysList.map(processNode).sort((a, b) => { const orderA = parseInt(a.orderNum || 0) const orderB = parseInt(b.orderNum || 0) return orderA - orderB }) }, // label 规则:parentId 为 -1 时,用 “number、name”,否则 “number.name” buildIndicatorLabel(item) { const num = item.number || '' const nm = item.name || '' if (!num) return nm const isRoot = !item.parentId || item.parentId === '-1' return isRoot ? `${num}、${nm}` : `${num}.${nm}` }, // 聚合某节点及其后代的年度与汇总数据 aggregateNodeData(node) { const yearMap = new Map() let surveysSum = 0 const dfs = (n) => { if (!n) return // 累加叶子上的 surveysVos(年度值通常在这里) if (Array.isArray(n.surveysVos) && n.surveysVos.length) { n.surveysVos.forEach((sv) => { const yearKey = String(sv.name || sv.year || '').trim() const val = Number(sv.value) || 0 if (yearKey) { yearMap.set(yearKey, (yearMap.get(yearKey) || 0) + val) } }) surveysSum += n.surveysVos.reduce( (sum, sv) => sum + (Number(sv.value) || 0), 0 ) } // 若有子节点,继续向下汇总 if (Array.isArray(n.costSurveysVos) && n.costSurveysVos.length) { n.costSurveysVos.forEach((child) => { // 如果子项有surveysVos,先处理这些数据 if (Array.isArray(child.surveysVos) && child.surveysVos.length) { child.surveysVos.forEach((sv) => { const yearKey = String(sv.name || sv.year || '').trim() const val = Number(sv.value) || 0 if (yearKey) { yearMap.set(yearKey, (yearMap.get(yearKey) || 0) + val) } }) surveysSum += child.surveysVos.reduce( (sum, sv) => sum + (Number(sv.value) || 0), 0 ) } // 如果子项仍有子节点,递归;否则视为叶子年度数据 if ( Array.isArray(child.costSurveysVos) && child.costSurveysVos.length ) { dfs(child) } else if ( !Array.isArray(child.surveysVos) || child.surveysVos.length === 0 ) { // 只有当没有surveysVos时,才使用name/year和value const yearKey = String(child.name || child.year || '').trim() const val = Number(child.value) || 0 if (yearKey) { yearMap.set(yearKey, (yearMap.get(yearKey) || 0) + val) } } }) } // 汇总 surveysVos(构成图用) if (Array.isArray(n.surveysVos) && n.surveysVos.length) { surveysSum += n.surveysVos.reduce( (sum, sv) => sum + (Number(sv.value) || 0), 0 ) } } dfs(node) // 将年度数据转换为 surveysVos 格式,保留完整的年度信息 const annualSurveysVos = Array.from(yearMap.entries()).map( ([name, value]) => ({ name, value, }) ) return { costSurveysVos: annualSurveysVos, surveysVos: annualSurveysVos, // 返回完整的年度数据而不仅仅是合计值 } }, // 获取树的叶子 id 列表 getLeafIds(tree = []) { const ids = [] const dfs = (node) => { if (!node.children || node.children.length === 0) { ids.push(node.id) } else { node.children.forEach(dfs) } } tree.forEach(dfs) return ids }, // 获取树的父级(拥有子节点)id 列表和节点列表 getParentIds(tree = []) { const ids = [] const items = [] const dfs = (node) => { if ( node && (node.parentId === undefined || node.parentId === null || String(node.parentId) === '-1') ) { ids.push(node.id) items.push(node) } if (node && node.children && node.children.length > 0) { node.children.forEach(dfs) } } tree.forEach(dfs) return { ids, items } }, // 查找节点到根的路径 findPathByKey( id, tree = this.indicatorData || this.leftDataItems || this.rightDataItems || [] ) { const getKeys = (item) => [ item.id, item.uniqueId, item.rowid, item.rowId, item.number, item.name, ].filter(Boolean) const dfs = (items, path = []) => { for (const item of items) { const currentPath = [...path, item] if (getKeys(item).includes(id)) { return currentPath } if (item.children && item.children.length > 0) { const childPath = dfs(item.children, currentPath) if (childPath) return childPath } } return null } return dfs(tree) }, // 过滤选中项: // - 点击根节点:只保留当前根节点及其已勾选的子节点,其它根与节点取消 // - 点击子/孙节点:只保留当前节点及其已勾选的子节点,其它节点取消 sanitizeCheckedKeys( data, checkedKeys = [], tree = this.leftDataItems || this.rightDataItems || this.indicatorData || [] ) { if (!checkedKeys || checkedKeys.length === 0) return [] const useTree = Array.isArray(tree) && tree.length > 0 ? tree : [] const paths = [] checkedKeys.forEach((key) => { const path = this.findPathByKey(key, useTree) if (path && path.length > 0) { paths.push(path) } }) const sanitized = new Set() if (data && String(data.parentId ?? '').trim() === '-1') { // 根节点:只保留当前根及其已勾选的子节点 paths .filter((path) => path[0] && path[0].id === data.id) .forEach((path) => { path.forEach((node) => sanitized.add(node.id)) }) sanitized.add(data.id) } else if (data) { // 子/孙节点:保留当前节点及其已勾选的子节点,并保留已勾选的根节点 paths .filter((path) => path[0] && path[0].rowid === data.parentId) .forEach((root) => { root.forEach((node) => { sanitized.add(node.id) node.children.forEach((child) => { if (checkedKeys.includes(child.id)) { sanitized.add(child.id) } }) }) }) } return Array.from(sanitized) }, // 过滤数据:选什么就返回什么(父子可同时保留各自曲线) filterCostSurveysListForHistoryAnalysis( costSurveysList = [], selectedItems = [] ) { if (!selectedItems || selectedItems.length === 0) { return [] } const toKey = (v) => v === undefined || v === null ? null : String(v).trim() // 兼容 id / uniqueId / rowid,统一转成字符串避免类型不一致 const selectedIdSet = new Set( selectedItems .flatMap((item) => [item.id, item.uniqueId, item.rowid, item.rowId]) .map(toKey) .filter(Boolean) ) // 递归过滤节点,父子都可保留 const filterNode = (node) => { if (!node) return null const keys = [node.id, node.uniqueId, node.rowid, node.rowId].map(toKey) const isSelected = keys.some((k) => selectedIdSet.has(k)) let filteredChildren = [] if (Array.isArray(node.costSurveysVos) && node.costSurveysVos.length) { filteredChildren = node.costSurveysVos.map(filterNode).filter(Boolean) } if (isSelected) { // 选中的节点保留自身;不再用子节点汇总填充,避免父子数据重复 const baseNode = { ...node, surveysVos: Array.isArray(node.surveysVos) ? node.surveysVos : [], costSurveysVos: filteredChildren, // 若子节点也选中则一并保留 } return baseNode } // 未选中但有选中子节点,则只保留这些子节点 if (filteredChildren.length > 0) { return { ...node, costSurveysVos: filteredChildren, } } return null } return costSurveysList.map(filterNode).filter(Boolean) }, // 统一请求统计数据 async requestStatistics(params = {}) { // 如果组件已销毁,直接返回 mock 数据 if (this.isDestroyed) { return [] } try { const res = await analyzeStatistics(params) // 再次检查组件是否已销毁 if (this.isDestroyed) { return [] } if (res && res.value) { return this.attachUniqueId(res.value) } } catch (e) { // 如果是请求中止错误,静默处理 if (e && e.message && e.message.includes('aborted')) { return [] } // 其他错误才打印警告 if (!this.isDestroyed) { console.warn('analyzeStatistics 调用失败,使用 mock 数据', e) } } return [] }, // 为接口返回的列表生成唯一标识 uniqueId,兼容数组或 { costSurveysList } 结构 attachUniqueId(res) { const markList = (list = [], parentKey = 'root', seen = new Set()) => { return (list || []).map((item, idx) => { // 使用多种可能的ID字段作为基础,但不修改原始ID const base = item.id || item.rowid || item.rowId || `${parentKey}-${item.number || item.name || idx}` let key = base let suffix = 1 while (seen.has(key)) { key = `${base}-${suffix++}` } seen.add(key) // 创建新对象并添加uniqueId,不修改原始item的id const cloned = { ...item, uniqueId: key, } if ( Array.isArray(item.costSurveysVos) && item.costSurveysVos.length ) { cloned.costSurveysVos = markList(item.costSurveysVos, key, seen) } return cloned }) } if (Array.isArray(res)) { return markList(res) } if (res && Array.isArray(res.costSurveysList)) { return { ...res, costSurveysList: markList(res.costSurveysList), } } return res }, getOptions() { // 获取监审任务列表 getAuditTaskList() .then((res) => { if (!this.isDestroyed && res && res.value) { this.projectOptions = res.value } }) .catch((e) => { // 如果是请求中止错误,静默处理 if (e && e.message && !e.message.includes('aborted')) { console.warn('获取项目列表失败', e) } }) getAllUnitList() .then((res) => { if (!this.isDestroyed && res && res.value) { this.auditedUnitOptions = res.value } }) .catch((e) => { // 如果是请求中止错误,静默处理 if (e && e.message && !e.message.includes('aborted')) { console.warn('获取单位列表失败', e) } }) }, // 根据选中的指标 ID 过滤 costSurveysList filterCostSurveysListBySelectedIds(costSurveysList = [], selectedIds = []) { if (!selectedIds || selectedIds.length === 0) { return [] } // 统一转成字符串,避免数字/字符串类型不一致导致无法匹配 const toKey = (v) => v === undefined || v === null ? null : String(v).trim() const selectedIdSet = new Set(selectedIds.map(toKey).filter(Boolean)) const isSelected = (node) => { const keys = [ node?.id, node?.uniqueId, node?.rowid, node?.rowId, node?.number, node?.name, ] .map(toKey) .filter(Boolean) return keys.some((k) => selectedIdSet.has(k)) } const filterNode = (node) => { if (!node) return null // 当前节点命中选中,直接保留,子节点也带上(用于饼图父级展示子项/趋势展示自身) if (isSelected(node)) { const filteredNode = { ...node } if ( Array.isArray(node.costSurveysVos) && node.costSurveysVos.length ) { filteredNode.costSurveysVos = node.costSurveysVos .map(filterNode) .filter(Boolean) } return filteredNode } // 未命中,检查子节点 if (Array.isArray(node.costSurveysVos) && node.costSurveysVos.length) { const filteredChildren = node.costSurveysVos .map(filterNode) .filter(Boolean) if (filteredChildren.length > 0) { return { ...node, costSurveysVos: filteredChildren, } } } return null } return costSurveysList.map(filterNode).filter(Boolean) }, // 将 costSurveysList 展平为趋势数据 [{indicatorName, year, value}] // 规则:只展示选中节点对应的年度数据(surveysVos),不追加“合计年”;若无则递归子节点 transformTrendFromCostList(costSurveysList = []) { const trendArr = [] const dfs = (node) => { if (!node) return if (Array.isArray(node.surveysVos) && node.surveysVos.length) { node.surveysVos.forEach((sv) => { const yearKey = String(sv.name || sv.year || '').trim() if (!yearKey) return const indicatorId = node.uniqueId || node.id || node.rowid || node.rowId || node.name trendArr.push({ indicatorId: indicatorId, indicatorName: node.name, year: yearKey, value: Number( sv.value !== undefined && sv.value !== null ? sv.value : sv.rvalue ) || 0, }) }) } if (Array.isArray(node.costSurveysVos) && node.costSurveysVos.length) { node.costSurveysVos.forEach(dfs) } } costSurveysList.forEach(dfs) return trendArr }, // 将 costSurveysList 汇总为构成数据 [{name, value}](显示所有surveysVos) transformCompositionFromCostList( costSurveysList = [], selectedItemsOrder = [] ) { const result = [] const dfs = (node) => { if (!node) return // 获取节点的唯一标识 const indicatorId = node.uniqueId || node.id || node.rowid || node.rowId || node.name if ( Array.isArray(node.costSurveysVos) && node.costSurveysVos.length > 0 ) { result.push({ name: node.name, indicatorId: indicatorId, // 添加唯一标识 value: Number( node.value !== undefined && node.value !== null ? node.value : node.rvalue ) || 0, surveysVos: node.surveysVos || [], }) node.costSurveysVos.forEach((sv) => { const surveysList = Array.isArray(sv.surveysVos) ? sv.surveysVos : [] // 子节点使用自己的唯一标识 const childIndicatorId = sv.uniqueId || sv.id || sv.rowid || sv.rowId || sv.name result.push({ name: sv.name, indicatorId: childIndicatorId, // 添加子节点的唯一标识 value: surveysList.reduce((sum, item) => { const v = item.value !== undefined && item.value !== null ? item.value : item.rvalue return sum + (Number(v) || 0) }, 0) || 0, surveysVos: surveysList, }) }) } else { result.push({ name: node.name, indicatorId: indicatorId, // 添加唯一标识 value: Number( node.value !== undefined && node.value !== null ? node.value : node.rvalue ) || 0, surveysVos: node.surveysVos || [], }) } } costSurveysList.forEach(dfs) // 统计相同名称的数量,用于区分显示 const nameCountMap = {} result.forEach((item) => { nameCountMap[item.name] = (nameCountMap[item.name] || 0) + 1 }) const nameIndexMap = {} let orderedResult = [...result] // 为相同名称的项添加序号 return orderedResult.map((item) => { const nameCount = nameCountMap[item.name] || 1 let displayName = item.name if (nameCount > 1) { if (!nameIndexMap[item.name]) { nameIndexMap[item.name] = 0 } nameIndexMap[item.name]++ displayName = `${item.name}(${nameIndexMap[item.name]})` } return { ...item, name: displayName, // 使用带序号的名字 originalName: item.name, // 保留原始名称 } }) }, // 构建趋势图数据结构(xAxis + series) // selectedItemsOrder: 可选的选中项顺序数组,用于保持图例顺序与选中项顺序一致 buildTrendSeries(trendArr = [], selectedItemsOrder = []) { const years = [ ...new Set( trendArr .map((item) => String(item.year || item.name || '').replace('年', '') ) .filter(Boolean) ), ] .sort() .map((y) => `${y}年`) const indicatorMap = {} trendArr.forEach((item) => { const yearKey = String(item.year || item.name || '').replace('年', '') if (!yearKey) return // 优先使用 indicatorId(uniqueId/id/rowid),确保相同名称但不同ID的项能区分 // 如果 indicatorId 为空,则使用 indicatorName 作为key(这种情况下相同名称会合并) const key = item.indicatorId || item.indicatorName if (!indicatorMap[key]) { indicatorMap[key] = { id: key, name: item.indicatorName, data: {}, color: this.getColorByIndex(Object.keys(indicatorMap).length), } } // 如果同一年份已有数据,累加而不是覆盖(避免相同名称的项数据丢失) const existingValue = indicatorMap[key].data[yearKey] || 0 indicatorMap[key].data[yearKey] = existingValue + Number(item.value) || 0 }) // 统计相同名称的数量,用于区分图例显示 const nameCountMap = {} Object.values(indicatorMap).forEach((indicator) => { nameCountMap[indicator.name] = (nameCountMap[indicator.name] || 0) + 1 }) const nameIndexMap = {} // 按照选中项顺序来生成 series,如果没有提供顺序则使用默认顺序 let orderedIndicators = [] if (selectedItemsOrder && selectedItemsOrder.length > 0) { // 按照 selectedItemsOrder 的顺序来构建 series const indicatorIdToItem = {} selectedItemsOrder.forEach((item) => { const key = item.uniqueId || item.id || item.rowid || item.rowId || item.name if (key && indicatorMap[key]) { indicatorIdToItem[key] = indicatorMap[key] } }) // 先添加按顺序的项 orderedIndicators = selectedItemsOrder .map((item) => { const key = item.uniqueId || item.id || item.rowid || item.rowId || item.name return indicatorMap[key] }) .filter(Boolean) // 再添加不在选中项顺序中的项(以防有遗漏) Object.keys(indicatorMap).forEach((key) => { if (!indicatorIdToItem[key]) { orderedIndicators.push(indicatorMap[key]) } }) } else { // 如果没有提供顺序,使用默认顺序 orderedIndicators = Object.values(indicatorMap) } // 按照顺序重新分配颜色,确保每个项(即使名称相同)都使用不同的颜色 const series = orderedIndicators.map((indicator, index) => { // 如果存在相同名称的项,在名称后添加序号来区分 const nameCount = nameCountMap[indicator.name] || 1 let displayName = indicator.name if (nameCount > 1) { if (!nameIndexMap[indicator.name]) { nameIndexMap[indicator.name] = 0 } nameIndexMap[indicator.name]++ displayName = `${indicator.name}(${nameIndexMap[indicator.name]})` } return { name: displayName, indicatorId: indicator.id, // 保留原始ID用于区分 originalName: indicator.name, // 保留原始名称 data: years.map((y) => { const key = y.replace('年', '') return indicator.data[key] !== undefined ? indicator.data[key] : 0 }), color: this.getColorByIndex(index), // 按照顺序重新分配颜色,确保每个项都不同 } }) return { xAxis: years, series } }, // 初始化图表 initChart(chartId, containerId) { const chartDom = document.getElementById(containerId) if (chartDom) { this.chartInstances[chartId] = echarts.init(chartDom) return this.chartInstances[chartId] } return null }, // 销毁指定图表实例 destroyChart(chartId) { if (this.chartInstances[chartId]) { this.chartInstances[chartId].dispose() delete this.chartInstances[chartId] } }, // 销毁所有图表实例 destroyCharts() { Object.keys(this.chartInstances).forEach((chartId) => { this.destroyChart(chartId) }) }, // 处理窗口大小变化 handleResize() { // 调整所有图表大小 Object.values(this.chartInstances).forEach((chart) => { if (chart) { chart.resize() } }) }, // 更新图表数据 updateChart(chartId, option) { if (this.chartInstances[chartId]) { this.chartInstances[chartId].setOption(option) this.$nextTick(() => { this.chartInstances[chartId].resize() }) } }, // 获取趋势图表基础配置 getBaseTrendChartOption( seriesData = [], xAxisData = ['2022年', '2023年', '2024年'] ) { return { tooltip: { trigger: 'axis', axisPointer: { type: 'cross', }, }, legend: { show: true, data: seriesData.map((item) => item.name), bottom: 0, left: 'center', itemGap: 15, textStyle: { fontSize: 12, color: '#333', }, type: 'plain', // 改为 plain 去掉分页控件 width: 'auto', // 自动宽度,确保能显示所有图例 formatter: function (name) { return name // 确保显示完整的名称 }, }, grid: { left: '3%', right: '4%', bottom: '15%', top: '10%', containLabel: true, }, xAxis: { type: 'category', boundaryGap: false, data: xAxisData, show: true, axisLine: { show: true, lineStyle: { color: '#333', }, }, axisTick: { show: true, }, axisLabel: { show: true, fontSize: 12, color: '#333', }, }, yAxis: { type: 'value', show: true, axisLine: { show: true, lineStyle: { color: '#333', }, }, axisTick: { show: false, }, axisLabel: { show: true, fontSize: 12, color: '#333', }, splitLine: { show: true, lineStyle: { type: 'dashed', color: '#e0e0e0', }, }, }, series: seriesData.map((item) => ({ name: item.name, type: 'line', data: item.data, symbol: 'circle', symbolSize: 8, smooth: false, lineStyle: { width: 2, color: item.color || '#37A2DA', }, itemStyle: { color: item.color || '#37A2DA', }, emphasis: { focus: 'series', }, })), } }, // 获取构成图表基础配置(historyAnalysis.vue 使用) getBaseCompositionChartOption(data = []) { // 为每个数据项分配不同的颜色 const dataWithColors = data.map((item, index) => ({ ...item, itemStyle: { color: this.getColorByIndex(index), }, })) return { tooltip: { show: true, trigger: 'item', formatter: (p) => { const surveysVos = Array.isArray(p.data?.surveysVos) ? p.data.surveysVos : [] // 计算所有年度值的总和 const totalSum = surveysVos.reduce((sum, sv) => { const hasValue = sv.value !== undefined && sv.value !== null const hasRvalue = sv.rvalue !== undefined && sv.rvalue !== null const mainValue = hasValue ? Number(sv.value) || 0 : hasRvalue ? Number(sv.rvalue) || 0 : 0 return sum + mainValue }, 0) // 使用总和作为显示值,如果没有年度数据则使用原始值 const displayValue = totalSum > 0 ? totalSum : p.value const header = `${p.name}: ${displayValue} (${p.percent}%)` // 只展示年度和值,避免重复的字段名与分隔符 const details = surveysVos .map((sv) => { const yearLabel = sv.name || sv.year || '-' const hasValue = sv.value !== undefined && sv.value !== null const hasRvalue = sv.rvalue !== undefined && sv.rvalue !== null const mainValue = hasValue ? sv.value : hasRvalue ? sv.rvalue : '-' // 如果 value 和 rvalue 同时存在且不同,附加 rvalue 以显示所有值 const extra = hasValue && hasRvalue && sv.value !== sv.rvalue ? ` (rvalue: ${sv.rvalue})` : '' return `${yearLabel}: ${mainValue}${extra}` }) .filter(Boolean) return details.length > 0 ? `${header}
${details.join('
')}` : header }, }, legend: { show: true, orient: 'vertical', right: 10, top: 'top', data: data.map((item) => item.name), formatter: function (name) { return name }, itemGap: 8, textStyle: { fontSize: 11, color: '#333', }, itemWidth: 14, itemHeight: 14, type: 'scroll', pageButtonPosition: 'end', pageButtonItemGap: 5, pageButtonGap: 5, pageTextStyle: { fontSize: 10, }, }, series: [ { name: '成本构成', type: 'pie', radius: '60%', center: ['35%', '50%'], avoidLabelOverlap: true, itemStyle: { borderRadius: 0, borderColor: '#fff', borderWidth: 1, }, label: { show: true, position: 'outside', formatter: '{b}', fontSize: 12, color: '#333', }, labelLine: { show: true, length: 10, length2: 5, lineStyle: { width: 1, color: 'auto', // 线条跟随扇区颜色 }, }, emphasis: { label: { show: true, fontSize: 12, fontWeight: 'normal', }, itemStyle: { shadowBlur: 0, shadowOffsetX: 0, shadowColor: 'transparent', }, labelLine: { lineStyle: { color: 'auto', }, }, }, data: dataWithColors, }, ], } }, // 获取构成图表基础配置(industryAnalysis.vue 使用) getIndustryCompositionChartOption(data = []) { // 为每个数据项分配不同的颜色,按照顺序使用 getColorByIndex const dataWithColors = data.map((item, index) => ({ ...item, itemStyle: { color: this.getColorByIndex(index), }, })) return { tooltip: { show: true, trigger: 'item', formatter: (p) => { const surveysVos = Array.isArray(p.data?.surveysVos) ? p.data.surveysVos : [] // 计算所有年度值的总和 const totalSum = surveysVos.reduce((sum, sv) => { const hasValue = sv.value !== undefined && sv.value !== null const hasRvalue = sv.rvalue !== undefined && sv.rvalue !== null const mainValue = hasValue ? Number(sv.value) || 0 : hasRvalue ? Number(sv.rvalue) || 0 : 0 return sum + mainValue }, 0) // 使用总和作为显示值,如果没有年度数据则使用原始值 const displayValue = totalSum > 0 ? totalSum : p.value const header = `${p.name}: ${displayValue} (${p.percent}%)` // 只展示年度和值,避免重复的字段名与分隔符 const details = surveysVos .map((sv) => { const yearLabel = sv.name || sv.year || '-' const hasValue = sv.value !== undefined && sv.value !== null const hasRvalue = sv.rvalue !== undefined && sv.rvalue !== null const mainValue = hasValue ? sv.value : hasRvalue ? sv.rvalue : '-' // 如果 value 和 rvalue 同时存在且不同,附加 rvalue 以显示所有值 const extra = hasValue && hasRvalue && sv.value !== sv.rvalue ? ` (rvalue: ${sv.rvalue})` : '' return `${yearLabel}: ${mainValue}${extra}` }) .filter(Boolean) return details.length > 0 ? `${header}
${details.join('
')}` : header }, }, legend: { show: true, orient: 'horizontal', bottom: 10, left: 'center', data: data.map((item) => item.name), icon: 'circle', itemGap: 20, textStyle: { fontSize: 12, color: '#333', }, itemWidth: 10, itemHeight: 10, }, series: [ { name: '成本构成', type: 'pie', radius: ['40%', '60%'], // 环形图 center: ['50%', '45%'], avoidLabelOverlap: true, silent: false, itemStyle: { borderRadius: 0, borderColor: '#fff', borderWidth: 1, }, label: { show: true, position: 'outside', formatter: '{b}: {d}%', // 名称: 百分比 fontSize: 12, color: '#333', }, labelLine: { show: true, length: 10, length2: 5, lineStyle: { width: 1, color: 'auto', // 线条跟随扇区颜色 }, }, emphasis: { label: { show: true, fontSize: 12, fontWeight: 'normal', }, itemStyle: { shadowBlur: 0, shadowOffsetX: 0, shadowColor: 'transparent', }, labelLine: { lineStyle: { color: 'auto', }, }, }, data: dataWithColors, }, ], } }, // 轮播功能基础配置(仅historyAnalysis.vue使用) initCarousel(chartId, totalPages = 1, onPageChange) { // 若已有轮播,先清理旧定时器,避免重复计时导致数据持续跳动 if (this.carouselConfigs && this.carouselConfigs[chartId]) { this.pauseCarousel(chartId) } const carouselConfig = { totalPages, currentPage: 0, timer: null, interval: 5000, onPageChange, } // 保存轮播配置 this.carouselConfigs = this.carouselConfigs || {} this.carouselConfigs[chartId] = carouselConfig // 启动轮播 if (totalPages > 1) { this.startCarousel(chartId) } else { // 单页不需要轮播,但仍确保跳转到第一页 this.goToCarouselPage(chartId, 0) } }, // 启动轮播 startCarousel(chartId) { const config = this.carouselConfigs?.[chartId] if (!config) return // 清除现有定时器 this.pauseCarousel(chartId) // 设置新定时器 config.timer = setInterval(() => { config.currentPage = (config.currentPage + 1) % config.totalPages this.goToCarouselPage(chartId, config.currentPage) }, config.interval) }, // 暂停轮播 pauseCarousel(chartId) { const config = this.carouselConfigs?.[chartId] if (config?.timer) { clearInterval(config.timer) config.timer = null } }, // 恢复轮播 resumeCarousel(chartId) { this.startCarousel(chartId) }, // 跳转到指定轮播页 goToCarouselPage(chartId, pageIndex) { const config = this.carouselConfigs?.[chartId] if (!config) return config.currentPage = pageIndex if (typeof config.onPageChange === 'function') { config.onPageChange(pageIndex) } }, }, } // mock 数据 export const mockStatisticsData = { costSurveysList: [ { id: '1997964654884069376', name: '人数', orderNum: '1', rowid: 'ce2c23fd-0a60-4d98-a27e-d5344c7f2f7b', number: '一', rvalue: '2', taskId: '1995015537499938816', nianfen: null, parentId: '-1', surveysVos: [ { name: '2024', value: '30' }, { name: '2025', value: '35' }, ], costSurveysVos: [ { id: '1997964655014092800', name: '1班', orderNum: '2', rowid: '0a075d08-1805-4ce0-a5e1-c4f264db76bd', number: '1', rvalue: '4', taskId: '1995015537499938816', nianfen: null, parentId: 'ce2c23fd-0a60-4d98-a27e-d5344c7f2f7b', surveysVos: [ { name: '2024', value: '12' }, { name: '2025', value: '15' }, ], costSurveysVos: null, }, { id: '1997964655131533312', name: '2班', orderNum: '3', rowid: 'ab3efd32-9a10-4355-8d6e-19d99ebdbe4a', number: '2', rvalue: '7', taskId: '1995015537499938816', nianfen: null, parentId: 'ce2c23fd-0a60-4d98-a27e-d5344c7f2f7b', surveysVos: [ { name: '2024', value: '18' }, { name: '2025', value: '20' }, ], costSurveysVos: null, }, ], }, { id: '1997964655244779520', name: '1班费用', orderNum: '4', rowid: '6a81c1d3-ea8b-48d5-bd11-27e5b150cea1', number: '二', rvalue: '53', taskId: '1995015537499938816', nianfen: null, parentId: '-1', surveysVos: [ { name: '2024', value: '120' }, { name: '2025', value: '140' }, ], costSurveysVos: [ { id: '1997964655387385856', name: '押金', orderNum: '5', rowid: 'bf17169e-9d03-49f0-b7c7-7502879a935b', number: '1', rvalue: '124', taskId: '1995015537499938816', nianfen: null, parentId: '6a81c1d3-ea8b-48d5-bd11-27e5b150cea1', surveysVos: [ { name: '2024', value: '50' }, { name: '2025', value: '60' }, ], costSurveysVos: null, }, { id: '1997964655559352320', name: '缴纳', orderNum: '6', rowid: 'e130564b-dc73-4fa9-a004-01c3af418021', number: '2', rvalue: '146', taskId: '1995015537499938816', nianfen: null, parentId: '6a81c1d3-ea8b-48d5-bd11-27e5b150cea1', surveysVos: [ { name: '2024', value: '70' }, { name: '2025', value: '80' }, ], costSurveysVos: null, }, ], }, { id: '1997964655680987136', name: '班级乘法', orderNum: '7', rowid: 'aec785e0-86fa-436e-be7b-bb9d2d9eaf32', number: '三', rvalue: '246', taskId: '1995015537499938816', nianfen: null, parentId: '-1', surveysVos: [ { name: '2024', value: '300' }, { name: '2025', value: '320' }, ], costSurveysVos: [], }, { id: '1997964655794233344', name: '人均费用', orderNum: '8', rowid: '857ba697-aaf0-4e26-97a6-4bd97d8ff6f7', number: '四', rvalue: '246', taskId: '1995015537499938816', nianfen: null, parentId: '-1', surveysVos: [ { name: '2024', value: '15' }, { name: '2025', value: '16' }, ], costSurveysVos: [], }, { id: '1997964655903279104', name: '材料成本', orderNum: '9', rowid: 'f3a8b2c1-5d6e-4f7a-8b9c-0d1e2f3a4b5c', number: '五', rvalue: '580', taskId: '1995015537499938816', nianfen: null, parentId: '-1', surveysVos: [ { name: '2022', value: '450' }, { name: '2023', value: '520' }, { name: '2024', value: '580' }, { name: '2025', value: '620' }, ], costSurveysVos: [ { id: '1997964656012324864', name: '原材料', orderNum: '10', rowid: 'a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d', number: '1', rvalue: '320', taskId: '1995015537499938816', nianfen: null, parentId: 'f3a8b2c1-5d6e-4f7a-8b9c-0d1e2f3a4b5c', surveysVos: [ { name: '2022', value: '250' }, { name: '2023', value: '290' }, { name: '2024', value: '320' }, { name: '2025', value: '350' }, ], costSurveysVos: null, }, { id: '1997964656121370624', name: '辅助材料', orderNum: '11', rowid: 'b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e', number: '2', rvalue: '180', taskId: '1995015537499938816', nianfen: null, parentId: 'f3a8b2c1-5d6e-4f7a-8b9c-0d1e2f3a4b5c', surveysVos: [ { name: '2022', value: '140' }, { name: '2023', value: '160' }, { name: '2024', value: '180' }, { name: '2025', value: '200' }, ], costSurveysVos: null, }, { id: '1997964656230416384', name: '包装材料', orderNum: '12', rowid: 'c3d4e5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f', number: '3', rvalue: '80', taskId: '1995015537499938816', nianfen: null, parentId: 'f3a8b2c1-5d6e-4f7a-8b9c-0d1e2f3a4b5c', surveysVos: [ { name: '2022', value: '60' }, { name: '2023', value: '70' }, { name: '2024', value: '80' }, { name: '2025', value: '70' }, ], costSurveysVos: null, }, ], }, { id: '1997964656339462144', name: '人工成本', orderNum: '13', rowid: 'd4e5f6a7-b8c9-4d0e-1f2a-3b4c5d6e7f8a', number: '六', rvalue: '850', taskId: '1995015537499938816', nianfen: null, parentId: '-1', surveysVos: [ { name: '2022', value: '680' }, { name: '2023', value: '750' }, { name: '2024', value: '850' }, { name: '2025', value: '920' }, ], costSurveysVos: [ { id: '1997964656448507904', name: '基本工资', orderNum: '14', rowid: 'e5f6a7b8-c9d0-4e1f-2a3b-4c5d6e7f8a9b', number: '1', rvalue: '520', taskId: '1995015537499938816', nianfen: null, parentId: 'd4e5f6a7-b8c9-4d0e-1f2a-3b4c5d6e7f8a', surveysVos: [ { name: '2022', value: '420' }, { name: '2023', value: '460' }, { name: '2024', value: '520' }, { name: '2025', value: '580' }, ], costSurveysVos: null, }, { id: '1997964656557553664', name: '绩效奖金', orderNum: '15', rowid: 'f6a7b8c9-d0e1-4f2a-3b4c-5d6e7f8a9b0c', number: '2', rvalue: '200', taskId: '1995015537499938816', nianfen: null, parentId: 'd4e5f6a7-b8c9-4d0e-1f2a-3b4c5d6e7f8a', surveysVos: [ { name: '2022', value: '150' }, { name: '2023', value: '180' }, { name: '2024', value: '200' }, { name: '2025', value: '220' }, ], costSurveysVos: null, }, { id: '1997964656666599424', name: '社保公积金', orderNum: '16', rowid: 'a7b8c9d0-e1f2-4a3b-4c5d-6e7f8a9b0c1d', number: '3', rvalue: '130', taskId: '1995015537499938816', nianfen: null, parentId: 'd4e5f6a7-b8c9-4d0e-1f2a-3b4c5d6e7f8a', surveysVos: [ { name: '2022', value: '110' }, { name: '2023', value: '120' }, { name: '2024', value: '130' }, { name: '2025', value: '120' }, ], costSurveysVos: null, }, ], }, { id: '1997964656775645184', name: '设备成本', orderNum: '17', rowid: 'b8c9d0e1-f2a3-4b4c-5d6e-7f8a9b0c1d2e', number: '七', rvalue: '420', taskId: '1995015537499938816', nianfen: null, parentId: '-1', surveysVos: [ { name: '2022', value: '380' }, { name: '2023', value: '400' }, { name: '2024', value: '420' }, { name: '2025', value: '450' }, ], costSurveysVos: [ { id: '1997964656884690944', name: '设备折旧', orderNum: '18', rowid: 'c9d0e1f2-a3b4-4c5d-6e7f-8a9b0c1d2e3f', number: '1', rvalue: '280', taskId: '1995015537499938816', nianfen: null, parentId: 'b8c9d0e1-f2a3-4b4c-5d6e-7f8a9b0c1d2e', surveysVos: [ { name: '2022', value: '260' }, { name: '2023', value: '270' }, { name: '2024', value: '280' }, { name: '2025', value: '300' }, ], costSurveysVos: null, }, { id: '1997964656993736704', name: '设备维护', orderNum: '19', rowid: 'd0e1f2a3-b4c5-4d6e-7f8a-9b0c-1d2e3f4a5b4a', number: '2', rvalue: '90', taskId: '1995015537499938816', nianfen: null, parentId: 'b8c9d0e1-f2a3-4b4c-5d6e-7f8a9b0c1d2e', surveysVos: [ { name: '2022', value: '80' }, { name: '2023', value: '85' }, { name: '2024', value: '90' }, { name: '2025', value: '100' }, ], costSurveysVos: null, }, { id: '1997964657102782464', name: '设备租赁', orderNum: '20', rowid: 'e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b', number: '3', rvalue: '50', taskId: '1995015537499938816', nianfen: null, parentId: 'b8c9d0e1-f2a3-4b4c-5d6e-7f8a9b0c1d2e', surveysVos: [ { name: '2022', value: '40' }, { name: '2023', value: '45' }, { name: '2024', value: '50' }, { name: '2025', value: '50' }, ], costSurveysVos: null, }, ], }, { id: '1997964657211828224', name: '管理费用', orderNum: '21', rowid: 'f2a3b4c5-d6e7-4f8a-9b0c-1d2e3f4a5b6c', number: '八', rvalue: '360', taskId: '1995015537499938816', nianfen: null, parentId: '-1', surveysVos: [ { name: '2022', value: '320' }, { name: '2023', value: '340' }, { name: '2024', value: '360' }, { name: '2025', value: '380' }, ], costSurveysVos: [ { id: '1997964657320873984', name: '办公费用', orderNum: '22', rowid: 'a3b4c5d6-e7f8-4a9b-0c1d-2e3f4a5b6c7d', number: '1', rvalue: '150', taskId: '1995015537499938816', nianfen: null, parentId: 'f2a3b4c5-d6e7-4f8a-9b0c-1d2e3f4a5b6c', surveysVos: [ { name: '2022', value: '130' }, { name: '2023', value: '140' }, { name: '2024', value: '150' }, { name: '2025', value: '160' }, ], costSurveysVos: null, }, { id: '1997964657429919744', name: '差旅费用', orderNum: '23', rowid: 'b4c5d6e7-f8a9-4b0c-1d2e-3f4a5b6c7d8e', number: '2', rvalue: '120', taskId: '1995015537499938816', nianfen: null, parentId: 'f2a3b4c5-d6e7-4f8a-9b0c-1d2e3f4a5b6c', surveysVos: [ { name: '2022', value: '110' }, { name: '2023', value: '115' }, { name: '2024', value: '120' }, { name: '2025', value: '130' }, ], costSurveysVos: null, }, { id: '1997964657538965504', name: '培训费用', orderNum: '24', rowid: 'c5d6e7f8-a9b0-4c1d-2e3f-4a5b6c7d8e9f', number: '3', rvalue: '90', taskId: '1995015537499938816', nianfen: null, parentId: 'f2a3b4c5-d6e7-4f8a-9b0c-1d2e3f4a5b6c', surveysVos: [ { name: '2022', value: '80' }, { name: '2023', value: '85' }, { name: '2024', value: '90' }, { name: '2025', value: '90' }, ], costSurveysVos: null, }, ], }, { id: '1997964657648011264', name: '能源成本', orderNum: '25', rowid: 'd6e7f8a9-b0c1-4d2e-3f4a-5b6c7d8e9f0a', number: '九', rvalue: '280', taskId: '1995015537499938816', nianfen: null, parentId: '-1', surveysVos: [ { name: '2022', value: '250' }, { name: '2023', value: '270' }, { name: '2024', value: '280' }, { name: '2025', value: '290' }, ], costSurveysVos: [ { id: '1997964657757057024', name: '电费', orderNum: '26', rowid: 'e7f8a9b0-c1d2-4e3f-4a5b-6c7d8e9f0a1b', number: '1', rvalue: '180', taskId: '1995015537499938816', nianfen: null, parentId: 'd6e7f8a9-b0c1-4d2e-3f4a-5b6c7d8e9f0a', surveysVos: [ { name: '2022', value: '160' }, { name: '2023', value: '170' }, { name: '2024', value: '180' }, { name: '2025', value: '190' }, ], costSurveysVos: null, }, { id: '1997964657866102784', name: '水费', orderNum: '27', rowid: 'f8a9b0c1-d2e3-4f4a-5b6c-7d8e9f0a1b2c', number: '2', rvalue: '60', taskId: '1995015537499938816', nianfen: null, parentId: 'd6e7f8a9-b0c1-4d2e-3f4a-5b6c7d8e9f0a', surveysVos: [ { name: '2022', value: '55' }, { name: '2023', value: '58' }, { name: '2024', value: '60' }, { name: '2025', value: '62' }, ], costSurveysVos: null, }, { id: '1997964657975148544', name: '燃气费', orderNum: '28', rowid: 'a9b0c1d2-e3f4-4a5b-6c7d-8e9f0a1b2c3d', number: '3', rvalue: '40', taskId: '1995015537499938816', nianfen: null, parentId: 'd6e7f8a9-b0c1-4d2e-3f4a-5b6c7d8e9f0a', surveysVos: [ { name: '2022', value: '35' }, { name: '2023', value: '38' }, { name: '2024', value: '40' }, { name: '2025', value: '38' }, ], costSurveysVos: null, }, ], }, ], }