Selaa lähdekoodia

fix:修改行业分析和历史分析页面图表显示问题

luzhixia 2 viikkoa sitten
vanhempi
commit
2b31f2f53c

+ 393 - 0
src/views/costAudit/baseInfo/statistics/components/IndustryPanel.vue

@@ -0,0 +1,393 @@
+<template>
+  <div class="comparison-panel" :class="panelClass">
+    <!-- 搜索区域 -->
+    <div class="search-section">
+      <el-form :inline="true" :model="localSearchForm" class="search-form">
+        <el-form-item label="项目名称">
+          <el-select
+            v-model="localSearchForm.projectId"
+            placeholder="请选择"
+            style="width: 200px"
+            clearable
+            filterable
+            allow-create
+            default-first-option
+            @change="handleProjectChange"
+          >
+            <el-option
+              v-for="item in projectOptions"
+              :key="item.projectId"
+              :label="item.projectName"
+              :value="item.projectId"
+            ></el-option>
+          </el-select>
+        </el-form-item>
+
+        <el-form-item label="监审对象">
+          <el-select
+            v-model="localSearchForm.auditedUnitId"
+            placeholder="请选择"
+            style="width: 200px"
+            clearable
+            filterable
+            allow-create
+            default-first-option
+            :disabled="!localSearchForm.projectId"
+            @change="emitSearchForm"
+          >
+            <el-option
+              v-for="item in auditedUnitOptions"
+              :key="item.unitId"
+              :label="item.unitName"
+              :value="item.unitId"
+            ></el-option>
+          </el-select>
+        </el-form-item>
+
+        <el-form-item label="计划年度">
+          <el-date-picker
+            v-model="localSearchForm.startYear"
+            type="year"
+            placeholder="开始年份"
+            format="yyyy年"
+            value-format="yyyy"
+            style="width: 140px; margin-right: 10px"
+            @change="emitSearchForm"
+          ></el-date-picker>
+          <span style="margin: 0 5px">~</span>
+          <el-date-picker
+            v-model="localSearchForm.endYear"
+            type="year"
+            placeholder="结束年份"
+            format="yyyy年"
+            value-format="yyyy"
+            style="width: 140px"
+            @change="emitSearchForm"
+          ></el-date-picker>
+        </el-form-item>
+
+        <el-form-item>
+          <el-tooltip class="item" :content="tooltipContent" placement="bottom">
+            <el-button
+              :icon="queryIcon"
+              type="primary"
+              :disabled="queryDisabled"
+              @click="emitQuery"
+            >
+              查询
+            </el-button>
+          </el-tooltip>
+
+          <el-button
+            :icon="resetIcon"
+            style="margin-left: 10px"
+            @click="emitReset"
+          >
+            重置
+          </el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+
+    <!-- 数据项选择和图表 -->
+    <div class="panel-content">
+      <div class="data-selector">
+        <div class="panel-title">请选择数据项后查看分析:</div>
+        <div class="custom-list">
+          <div class="list-header">项目</div>
+          <div class="list-content">
+            <el-tree
+              v-if="dataItems && dataItems.length > 0"
+              ref="indicatorTree"
+              class="indicator-tree"
+              :data="dataItems"
+              show-checkbox
+              :node-key="nodeKey"
+              :props="treeProps"
+              :check-strictly="true"
+              @check="onCheck"
+            ></el-tree>
+            <Empty v-else description="暂无数据"></Empty>
+          </div>
+        </div>
+      </div>
+
+      <div class="chart-section">
+        <div class="chart-container">
+          <h3 class="chart-title">指标趋势分析</h3>
+          <!-- 指标趋势分析图表 -->
+          <div v-show="hasTrendData" :id="trendChartId" class="chart"></div>
+          <Empty v-show="!hasTrendData" description="暂无数据"></Empty>
+          <!-- 趋势图轮播指示灯 -->
+          <div v-if="trendTotalPages > 1" class="carousel-pagination">
+            <div class="carousel-indicators">
+              <span
+                v-for="index in trendTotalPages"
+                :key="'trend-' + index"
+                class="indicator-dot"
+                :class="{ active: trendCurrentPage === index }"
+                @click="handleTrendPageClick(index - 1)"
+              ></span>
+            </div>
+          </div>
+        </div>
+
+        <div class="chart-container">
+          <h3 class="chart-title">指标占比分析</h3>
+          <div
+            v-show="hasProportionData"
+            :id="proportionChartId"
+            class="chart"
+          ></div>
+          <Empty v-show="!hasProportionData" description="暂无数据"></Empty>
+          <!-- 饼图轮播指示灯 -->
+          <div v-if="proportionTotalPages > 1" class="carousel-pagination">
+            <div class="carousel-indicators">
+              <span
+                v-for="index in proportionTotalPages"
+                :key="'proportion-' + index"
+                class="indicator-dot"
+                :class="{ active: proportionCurrentPage === index }"
+                @click="handleProportionPageClick(index - 1)"
+              ></span>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+  export default {
+    name: 'IndustryPanel',
+    model: {
+      prop: 'searchForm',
+      event: 'update:searchForm',
+    },
+    props: {
+      panelClass: {
+        type: String,
+        default: '',
+      },
+      searchForm: {
+        type: Object,
+        default: () => ({}),
+      },
+      projectOptions: {
+        type: Array,
+        default: () => [],
+      },
+      auditedUnitOptions: {
+        type: Array,
+        default: () => [],
+      },
+      treeProps: {
+        type: Object,
+        default: () => ({}),
+      },
+      dataItems: {
+        type: Array,
+        default: () => [],
+      },
+      hasTrendData: {
+        type: Boolean,
+        default: false,
+      },
+      hasProportionData: {
+        type: Boolean,
+        default: false,
+      },
+      trendChartId: {
+        type: String,
+        required: true,
+      },
+      proportionChartId: {
+        type: String,
+        required: true,
+      },
+      nodeKey: {
+        type: String,
+        default: 'id',
+      },
+      tooltipContent: {
+        type: String,
+        default: '查询条件:项目名称、监审对象都需要选择,否则无法查询',
+      },
+      queryDisabled: {
+        type: Boolean,
+        default: false,
+      },
+      queryIcon: {
+        type: String,
+        default: 'iconfont-5039297 icon-chaxun',
+      },
+      resetIcon: {
+        type: String,
+        default: 'iconfont-5039297 icon-zhongzhi',
+      },
+      // 趋势图分页信息
+      trendCurrentPage: {
+        type: Number,
+        default: 1,
+      },
+      trendTotalPages: {
+        type: Number,
+        default: 0,
+      },
+      // 饼图分页信息
+      proportionCurrentPage: {
+        type: Number,
+        default: 1,
+      },
+      proportionTotalPages: {
+        type: Number,
+        default: 0,
+      },
+    },
+    data() {
+      return {
+        localSearchForm: { ...this.searchForm },
+      }
+    },
+    watch: {
+      searchForm: {
+        deep: true,
+        handler(val) {
+          this.localSearchForm = { ...val }
+        },
+      },
+    },
+    methods: {
+      emitSearchForm() {
+        this.$emit('update:searchForm', { ...this.localSearchForm })
+      },
+      handleProjectChange(val) {
+        this.emitSearchForm()
+        this.$emit('project-change', val)
+      },
+      emitQuery() {
+        this.emitSearchForm()
+        this.$emit('query')
+      },
+      emitReset() {
+        this.$emit('reset')
+      },
+      onCheck(data, info) {
+        this.$emit('check', data, info)
+      },
+      setCheckedKeys(keys = []) {
+        this.$refs.indicatorTree?.setCheckedKeys?.(keys)
+      },
+      getCheckedKeys() {
+        return this.$refs.indicatorTree?.getCheckedKeys?.() || []
+      },
+      handleTrendPrev() {
+        if (this.trendCurrentPage > 1) {
+          this.$emit('trend-page-change', this.trendCurrentPage - 1)
+        }
+      },
+      handleTrendNext() {
+        if (this.trendCurrentPage < this.trendTotalPages) {
+          this.$emit('trend-page-change', this.trendCurrentPage + 1)
+        }
+      },
+      handleProportionPrev() {
+        if (this.proportionCurrentPage > 1) {
+          this.$emit('proportion-page-change', this.proportionCurrentPage - 1)
+        }
+      },
+      handleProportionNext() {
+        if (this.proportionCurrentPage < this.proportionTotalPages) {
+          this.$emit('proportion-page-change', this.proportionCurrentPage + 1)
+        }
+      },
+      handleTrendPageClick(pageIndex) {
+        this.$emit('trend-page-click', pageIndex)
+      },
+      handleProportionPageClick(pageIndex) {
+        this.$emit('proportion-page-click', pageIndex)
+      },
+    },
+  }
+</script>
+
+<style scoped lang="scss">
+  .comparison-panel {
+    display: flex;
+    flex-direction: column;
+    width: 100%;
+  }
+  .panel-content {
+    display: flex;
+    gap: 20px;
+    flex: 1;
+    padding: 20px;
+    overflow-x: hidden;
+    min-width: 0;
+    box-sizing: border-box;
+  }
+  .chart-section {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    gap: 30px;
+    min-width: 0;
+    overflow-x: hidden;
+  }
+  .chart-container {
+    background-color: #fff;
+    padding: 20px;
+    overflow: hidden; // 防止空状态时出现滚动条
+    min-width: 0;
+    box-sizing: border-box;
+  }
+
+  .chart-title {
+    font-size: 16px;
+    font-weight: bold;
+    margin-bottom: 15px;
+    color: #333;
+    text-align: center;
+    padding-bottom: 10px;
+  }
+
+  .chart {
+    width: 100%;
+    height: 400px;
+  }
+
+  // 轮播指示灯样式
+  .carousel-pagination {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    margin-top: 15px;
+  }
+
+  .carousel-indicators {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+
+  .indicator-dot {
+    width: 8px;
+    height: 8px;
+    border-radius: 50%;
+    background-color: #d9d9d9;
+    margin: 0 4px;
+    cursor: pointer;
+    transition: all 0.3s;
+  }
+
+  .indicator-dot:hover {
+    background-color: #409eff;
+  }
+
+  .indicator-dot.active {
+    width: 16px;
+    border-radius: 4px;
+    background-color: #409eff;
+  }
+</style>

+ 42 - 25
src/views/costAudit/baseInfo/statistics/historyAnalysis.vue

@@ -63,7 +63,7 @@
         <el-tooltip
           class="item"
           :content="'查询条件:项目名称、被监审单位都需要选择,否则无法查询'"
-          placement="top"
+          placement="bottom"
         >
           <el-button
             type="primary"
@@ -245,8 +245,12 @@
               const unit = selectedProject.auditedUnitId.split(',')
               if (unit.length === 1) {
                 this.searchForm.auditedUnitId = unit[0]
-                this.searchForm.auditedUnitName =
-                  selectedProject.auditedUnitName
+                this.searchForm.auditedUnitName = selectedProject.unitName
+                this.auditedUnitOptions.forEach((item) => {
+                  if (unit.includes(item.unitId)) {
+                    this.projectAuditedUnitOptions.push(item)
+                  }
+                })
               } else if (unit.length > 1) {
                 this.auditedUnitOptions.forEach((item) => {
                   if (unit.includes(item.unitId)) {
@@ -295,9 +299,9 @@
             trigger: 'axis',
           },
           legend: {
-            data: [],
             bottom: 0,
             left: 'center',
+            show: true,
           },
           grid: {
             left: '3%',
@@ -310,11 +314,11 @@
             type: 'category',
             boundaryGap: false,
             data: [],
-            show: false, // 隐藏坐标轴
+            show: true, // 显示坐标轴
           },
           yAxis: {
             type: 'value',
-            show: false, // 隐藏坐标轴
+            show: true, // 显示坐标轴
           },
           series: [],
         }
@@ -339,8 +343,7 @@
             orient: 'vertical',
             right: 10,
             top: 'center',
-            data: [],
-            show: false, // 隐藏图例
+            show: true,
           },
           series: [
             {
@@ -376,9 +379,9 @@
               trigger: 'axis',
             },
             legend: {
-              data: [],
               bottom: 0,
               left: 'center',
+              show: true,
             },
             grid: {
               left: '3%',
@@ -391,11 +394,11 @@
               type: 'category',
               boundaryGap: false,
               data: [],
-              show: false, // 隐藏坐标轴
+              show: true, // 显示坐标轴
             },
             yAxis: {
               type: 'value',
-              show: false, // 隐藏坐标轴
+              show: true, // 显示坐标轴
             },
             series: [],
           }
@@ -445,8 +448,7 @@
               orient: 'vertical',
               right: 10,
               top: 'center',
-              data: [],
-              show: false, // 隐藏图例
+              show: true,
             },
             series: [
               {
@@ -607,11 +609,16 @@
       getParentIds(tree = []) {
         const ids = []
         const dfs = (node) => {
-          // 仅收集 parentId === '-1' 的根节点
-          if (String(node.parentId) === '-1') {
+          // 仅收集 parentId === '-1' 的根节点,兼容 undefined/null
+          if (
+            node &&
+            (node.parentId === undefined ||
+              node.parentId === null ||
+              String(node.parentId) === '-1')
+          ) {
             ids.push(node.id)
           }
-          if (node.children && node.children.length > 0) {
+          if (node && node.children && node.children.length > 0) {
             node.children.forEach(dfs)
           }
         }
@@ -713,13 +720,22 @@
         if (!selectedItems || selectedItems.length === 0) {
           return []
         }
-        const selectedIdSet = new Set(selectedItems.map((item) => item.id))
+        // 兼容 id / uniqueId / rowid
+        const selectedIdSet = new Set(
+          selectedItems
+            .flatMap((item) => [item.id, item.uniqueId, item.rowid, item.rowId])
+            .filter(Boolean)
+        )
 
         // 递归过滤节点,父子都可保留
         const filterNode = (node) => {
           if (!node) return null
 
-          const isSelected = selectedIdSet.has(node.id)
+          const isSelected =
+            selectedIdSet.has(node.id) ||
+            selectedIdSet.has(node.uniqueId) ||
+            selectedIdSet.has(node.rowid) ||
+            selectedIdSet.has(node.rowId)
           let filteredChildren = []
 
           if (
@@ -732,7 +748,7 @@
           }
 
           if (isSelected) {
-            // 选中的节点保留自身,并附上已过滤的子节点(若有)。
+            // 选中的节点只保留自身(折线仅展示选中的项,不强制带子项)。
             // 若自身没有年度数据,则用子节点汇总填充,以保证有折线数据。
             const hasSurvey =
               Array.isArray(node.surveysVos) && node.surveysVos.length
@@ -747,9 +763,8 @@
             return {
               ...node,
               surveysVos: hasSurvey ? node.surveysVos : fallback.surveysVos,
-              costSurveysVos: filteredChildren.length
-                ? filteredChildren
-                : fallback.costSurveysVos || [],
+              // 折线不展示未选中的子节点
+              costSurveysVos: [],
             }
           }
 
@@ -863,7 +878,7 @@
 
         // 处理趋势图数据
         const trendArr = this.transformTrendFromCostList(trendList)
-        const { xAxis, series } = this.buildTrendSeries(trendArr)
+        const { xAxis, series } = this.buildTrendSeries(trendArr, selectedItems)
         this.trendData = { xAxis, series }
         // 保存所有趋势图数据
         this.trendAllSeries = series
@@ -876,8 +891,10 @@
           dataSource.costSurveysList || [],
           effectiveSelectedIds
         )
-        const compositionData =
-          this.transformCompositionFromCostList(compositionList)
+        const compositionData = this.transformCompositionFromCostList(
+          compositionList,
+          selectedItems
+        )
         // 保存所有构成图数据
         this.compositionAllData = compositionData
         this.compositionTotalCount = compositionData.length

+ 416 - 85
src/views/costAudit/baseInfo/statistics/index.js

@@ -1,8 +1,8 @@
 import * as echarts from 'echarts'
 import { getAllUnitList } from '@/api/auditEntityManage.js'
-import { getAuditTaskList } from '@/api/auditInitiation.js'
+// import { getAuditTaskList } from '@/api/auditInitiation.js'
 // , getAuditTaskList
-import { analyzeStatistics } from '@/api/comprehensive'
+import { analyzeStatistics, getAuditTaskList } from '@/api/comprehensive'
 // 综合分析页面的通用mixin
 export const comprehensiveMixin = {
   data() {
@@ -54,8 +54,15 @@ export const comprehensiveMixin = {
     // 将 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: [],
         }
@@ -89,6 +96,88 @@ export const comprehensiveMixin = {
       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 = []
@@ -108,9 +197,16 @@ export const comprehensiveMixin = {
       const ids = []
       const items = []
       const dfs = (node) => {
-        if (node.parentId == '-1') {
+        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)
         }
       }
@@ -130,7 +226,9 @@ export const comprehensiveMixin = {
         if (this.isDestroyed) {
           return []
         }
-        if (res && res.value) return res.value
+        if (res && res.value) {
+          return this.attachUniqueId(res.value)
+        }
       } catch (e) {
         // 如果是请求中止错误,静默处理
         if (e && e.message && e.message.includes('aborted')) {
@@ -144,6 +242,51 @@ export const comprehensiveMixin = {
       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()
@@ -178,33 +321,41 @@ export const comprehensiveMixin = {
         return []
       }
       const selectedIdSet = new Set(selectedIds)
+      const isSelected = (node) => {
+        const keys = [
+          node?.id,
+          node?.uniqueId,
+          node?.rowid,
+          node?.rowId,
+          node?.number,
+          node?.name,
+        ].filter(Boolean)
+        return keys.some((k) => selectedIdSet.has(k))
+      }
 
-      // 递归过滤节点
       const filterNode = (node) => {
         if (!node) return null
 
-        // 如果当前节点被选中,保留它及其所有子节点
-        if (selectedIdSet.has(node.id)) {
+        // 当前节点命中选中,直接保留,子节点也带上(用于饼图父级展示子项/趋势展示自身)
+        if (isSelected(node)) {
           const filteredNode = { ...node }
           if (
             Array.isArray(node.costSurveysVos) &&
             node.costSurveysVos.length
           ) {
-            // 如果父节点被选中,保留所有子节点
-            filteredNode.costSurveysVos = node.costSurveysVos.map((child) => ({
-              ...child,
-            }))
+            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,
@@ -229,10 +380,18 @@ export const comprehensiveMixin = {
           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({
-              indicatorName: node.name,
+              indicatorId: indicatorId,
+              indicatorName: node.label || node.name,
               year: yearKey,
-              value: Number(sv.value) || 0,
+              value:
+                Number(
+                  sv.value !== undefined && sv.value !== null
+                    ? sv.value
+                    : sv.rvalue
+                ) || 0,
             })
           })
         }
@@ -247,10 +406,17 @@ export const comprehensiveMixin = {
     },
 
     // 将 costSurveysList 汇总为构成数据 [{name, value}](显示所有surveysVos)
-    transformCompositionFromCostList(costSurveysList = []) {
+    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
@@ -261,28 +427,86 @@ export const comprehensiveMixin = {
               : []
             result.push({
               name: sv.name,
+              indicatorId: indicatorId, // 添加唯一标识
               value:
-                surveysList.reduce(
-                  (sum, item) => sum + Number(item.value) || 0,
-                  0
-                ) || 0,
+                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,
-            value: Number(node.rvalue) || 0,
+            indicatorId: indicatorId, // 添加唯一标识
+            value:
+              Number(
+                node.value !== undefined && node.value !== null
+                  ? node.value
+                  : node.rvalue
+              ) || 0,
             surveysVos: node.surveysVos || [],
           })
         }
       }
       costSurveysList.forEach(dfs)
-      return result
+
+      // 统计相同名称的数量,用于区分显示
+      const nameCountMap = {}
+      result.forEach((item) => {
+        nameCountMap[item.name] = (nameCountMap[item.name] || 0) + 1
+      })
+      const nameIndexMap = {}
+
+      // 按照选中项顺序来排序结果
+      let orderedResult = result
+      if (selectedItemsOrder && selectedItemsOrder.length > 0) {
+        const indicatorIdToItem = {}
+        result.forEach((item) => {
+          indicatorIdToItem[item.indicatorId] = item
+        })
+        // 先添加按顺序的项
+        orderedResult = selectedItemsOrder
+          .map((item) => {
+            const key =
+              item.uniqueId || item.id || item.rowid || item.rowId || item.name
+            return indicatorIdToItem[key]
+          })
+          .filter(Boolean)
+        // 再添加不在选中项顺序中的项(以防有遗漏)
+        result.forEach((item) => {
+          if (!orderedResult.find((r) => r.indicatorId === item.indicatorId)) {
+            orderedResult.push(item)
+          }
+        })
+      }
+
+      // 为相同名称的项添加序号
+      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)
-    buildTrendSeries(trendArr = []) {
+    // selectedItemsOrder: 可选的选中项顺序数组,用于保持图例顺序与选中项顺序一致
+    buildTrendSeries(trendArr = [], selectedItemsOrder = []) {
       const years = [
         ...new Set(
           trendArr
@@ -299,24 +523,94 @@ export const comprehensiveMixin = {
       trendArr.forEach((item) => {
         const yearKey = String(item.year || item.name || '').replace('年', '')
         if (!yearKey) return
-        if (!indicatorMap[item.indicatorName]) {
-          indicatorMap[item.indicatorName] = {
+        // 优先使用 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),
           }
         }
-        indicatorMap[item.indicatorName].data[yearKey] = Number(item.value) || 0
+        // 如果同一年份已有数据,累加而不是覆盖(避免相同名称的项数据丢失)
+        const existingValue = indicatorMap[key].data[yearKey] || 0
+        indicatorMap[key].data[yearKey] =
+          existingValue + Number(item.value) || 0
       })
 
-      const series = Object.values(indicatorMap).map((indicator) => ({
-        name: indicator.name,
-        data: years.map((y) => {
-          const key = y.replace('年', '')
-          return indicator.data[key] || 0
-        }),
-        color: indicator.color,
-      }))
+      // 统计相同名称的数量,用于区分图例显示
+      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), // 按照顺序重新分配颜色,确保每个项都不同
+        }
+      })
+
+      // 确保至少有一个空系列,以便图表可以正确初始化
+      if (series.length === 0) {
+        series.push({
+          name: '',
+          data: years.map(() => 0),
+          color: '#ccc',
+        })
+      }
 
       return { xAxis: years, series }
     },
@@ -382,11 +676,16 @@ export const comprehensiveMixin = {
           data: seriesData.map((item) => item.name),
           bottom: 0,
           left: 'center',
-          itemGap: 20,
+          itemGap: 15,
           textStyle: {
             fontSize: 12,
             color: '#333',
           },
+          type: 'plain', // 改为 plain 去掉分页控件
+          width: 'auto', // 自动宽度,确保能显示所有图例
+          formatter: function (name) {
+            return name // 确保显示完整的名称
+          },
         },
         grid: {
           left: '3%',
@@ -463,15 +762,36 @@ export const comprehensiveMixin = {
 
     // 获取构成图表基础配置(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 header = `${p.name}: ${p.value} (${p.percent}%)`
             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) => {
@@ -500,18 +820,25 @@ export const comprehensiveMixin = {
           show: true,
           orient: 'vertical',
           right: 10,
-          top: 'center',
+          top: 'top',
           data: data.map((item) => item.name),
           formatter: function (name) {
             return name
           },
-          itemGap: 10,
+          itemGap: 8,
           textStyle: {
-            fontSize: 12,
+            fontSize: 11,
             color: '#333',
           },
           itemWidth: 14,
           itemHeight: 14,
+          type: 'scroll',
+          pageButtonPosition: 'end',
+          pageButtonItemGap: 5,
+          pageButtonGap: 5,
+          pageTextStyle: {
+            fontSize: 10,
+          },
         },
         series: [
           {
@@ -558,7 +885,7 @@ export const comprehensiveMixin = {
                 },
               },
             },
-            data: data,
+            data: dataWithColors,
           },
         ],
       }
@@ -566,54 +893,58 @@ export const comprehensiveMixin = {
 
     // 获取构成图表基础配置(industryAnalysis.vue 使用)
     getIndustryCompositionChartOption(data = []) {
-      const colorPalette = [
-        '#5470C6', // 蓝色 - 基本工资
-        '#73C0DE', // 青色/蓝绿色 - 津贴
-        '#91CC75', // 绿色 - 奖金
-        '#FAC858', // 黄色 - 福利费
-        '#FF85C0', // 粉色 - 其他人员支出
-        '#EE6666', // 红色
-        '#3BA272', // 深绿色
-        '#FC8452', // 橙色
-        '#9A60B4', // 紫色
-        '#E71D36', // 深红
-      ]
+      // 为每个数据项分配不同的颜色,按照顺序使用 getColorByIndex
+      const dataWithColors = data.map((item, index) => ({
+        ...item,
+        itemStyle: {
+          color: this.getColorByIndex(index),
+        },
+      }))
+
       return {
-        color: colorPalette,
         tooltip: {
           show: true,
           trigger: 'item',
           formatter: (p) => {
-            let tooltipContent = `${p.name}: ${p.value} (${p.percent}%)`
-            // 如果有surveysVos信息,显示完整的surveysVos
-            if (p.data && p.data.surveysVos) {
-              const surveysVos = p.data.surveysVos
-              tooltipContent += '<br/>'
-              if (Array.isArray(surveysVos)) {
-                // 如果是数组,遍历显示每个元素
-                surveysVos.forEach((sv, index) => {
-                  if (index > 0) {
-                    tooltipContent += '<br/>---<br/>'
-                  }
-                  for (const key in sv) {
-                    if (sv[key] !== undefined && sv[key] !== null) {
-                      tooltipContent += `${key}: ${sv[key]}<br/>`
-                    }
-                  }
-                })
-              } else if (typeof surveysVos === 'object') {
-                // 如果是对象,直接显示所有属性
-                for (const key in surveysVos) {
-                  if (
-                    surveysVos[key] !== undefined &&
-                    surveysVos[key] !== null
-                  ) {
-                    tooltipContent += `${key}: ${surveysVos[key]}<br/>`
-                  }
-                }
-              }
-            }
-            return tooltipContent
+            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}<br/>${details.join('<br/>')}`
+              : header
           },
         },
         legend: {
@@ -677,7 +1008,7 @@ export const comprehensiveMixin = {
                 },
               },
             },
-            data: data,
+            data: dataWithColors,
           },
         ],
       }
@@ -1070,7 +1401,7 @@ export const mockStatisticsData = {
           id: '1997964656993736704',
           name: '设备维护',
           orderNum: '19',
-          rowid: 'd0e1f2a3-b4c5-4d6e-7f8a-9b0c1d2e3f4a',
+          rowid: 'd0e1f2a3-b4c5-4d6e-7f8a-9b0c-1d2e3f4a5b4a',
           number: '2',
           rvalue: '90',
           taskId: '1995015537499938816',

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 435 - 468
src/views/costAudit/baseInfo/statistics/industryAnalysis.vue


Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä