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,
},
],
},
],
}