From 0568fa140511a5df539dbc87759bb2040e7d8b10 Mon Sep 17 00:00:00 2001
From: houzhongjian <houzhongyi@126.com>
Date: 星期二, 06 五月 2025 15:06:52 +0800
Subject: [PATCH] 模型管理数据分析页面增加授权功能,以供第三方嵌入

---
 src/router/modules/remaining.ts        |    9 
 src/views/model/pre/analysis/third.vue | 1428 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/permission.ts                      |    4 
 3 files changed, 1,440 insertions(+), 1 deletions(-)

diff --git a/src/permission.ts b/src/permission.ts
index 9120c1a..5e01684 100644
--- a/src/permission.ts
+++ b/src/permission.ts
@@ -49,11 +49,13 @@
 // 路由不重定向白名单
 const whiteList = [
   '/login',
+  '/sso',
   '/social-login',
   '/auth-redirect',
   '/bind',
   '/register',
-  '/oauthLogin/gitee'
+  '/oauthLogin/gitee',
+  '/model/analysis'
 ]
 
 // 路由加载前
diff --git a/src/router/modules/remaining.ts b/src/router/modules/remaining.ts
index c47f1a7..6c21655 100644
--- a/src/router/modules/remaining.ts
+++ b/src/router/modules/remaining.ts
@@ -470,6 +470,15 @@
       }
     ]
   },
+  {
+    path: '/model/analysis',
+    name: 'AnalysisformDataThird',
+    component: () => import('@/views/model/pre/analysis/third.vue'),
+    meta: {
+      hidden: true,
+      noTagsView: true
+    },
+  }
 ]
 
 export default remainingRouter
diff --git a/src/views/model/pre/analysis/third.vue b/src/views/model/pre/analysis/third.vue
new file mode 100644
index 0000000..1f4c1da
--- /dev/null
+++ b/src/views/model/pre/analysis/third.vue
@@ -0,0 +1,1428 @@
+<template>
+  <el-card shadow="never" class="aui-card--fill">
+    <div class="mod-his__index">
+      <el-form :inline="true" :model="formData" label-width="70px">
+        <el-form-item label="开始时间">
+          <el-date-picker
+            v-model="formData.startTime"
+            type="datetime"
+            format="YYYY-MM-DD HH:mm:00"
+            value-format="YYYY-MM-DD HH:mm:00"
+            placeholder="选择日期时间"/>
+        </el-form-item>
+        <el-form-item label="结束时间">
+          <el-date-picker
+            v-model="formData.endTime"
+            type="datetime"
+            format="YYYY-MM-DD HH:mm:00"
+            value-format="YYYY-MM-DD HH:mm:00"
+            placeholder="选择日期时间"/>
+        </el-form-item>
+        <el-form-item label="预测时间">
+          <el-date-picker
+            v-model="formData.predictTime"
+            type="datetime"
+            format="YYYY-MM-DD HH:mm:00"
+            value-format="YYYY-MM-DD HH:mm:00"
+            placeholder="选择日期时间"/>
+        </el-form-item>
+        <el-form-item label="预测频率">
+          <el-input-number v-model="formData.predictFreq" controls-position="right"
+                           :min="1"
+                           :max="10"/>
+        </el-form-item>
+        <el-form-item>
+          <el-button-group>
+            <el-button type="primary" plain :icon="DArrowLeft"
+                       :loading="loading1" @click="leftSearchDataByRange()"/>
+            <el-button type="primary" plain :icon="Search"
+                       :loading="loading1" @click="getList()">查询
+            </el-button>
+            <el-button type="primary" plain :icon="DArrowRight"
+                       :loading="loading1" @click="rightSearchDataByRange()"/>
+          </el-button-group>
+        </el-form-item>
+        <el-form-item>
+          <el-button-group>
+            <el-button type="primary" plain :icon="CaretLeft"
+                       @click="playChart(true)"/>
+            <el-button type="primary" plain :icon="VideoPlay" v-if="!isPlay"
+                       @click="playHandle('play')"/>
+            <el-button type="primary" plain :icon="VideoPause" v-if="isPlay"
+                       @click="playHandle('pause')"/>
+            <el-button type="primary" plain :icon="CaretRight"
+                       @click="playChart(false)"/>
+          </el-button-group>
+        </el-form-item>
+
+        <div class="his-body">
+          <div class="his-body-left">
+            <div class="his-body-tree">
+              <el-input
+                v-model="filterText"
+                class="mb-2"
+                placeholder="Filter"
+              />
+              <el-tree
+                :data="treeData"
+                show-checkbox
+                node-key="id"
+                ref="treeRef"
+                highlight-current
+                check-strictly
+                :filter-node-method="filterNode"
+                @check="onCheckTree"/>
+            </div>
+          </div>
+          <div class="his-body-right">
+            <div class="his-body-chart">
+              <el-form :inline="true" :model="calRateForm" :rules="formRules" ref="calRateFormRef"
+                       label-width="108px">
+                <el-row>
+<!--                  <el-col :span="6" style="display: flex;align-items: center;justify-content: center">-->
+<!--                    <span>预测项:{{formData.checkedItemData?.label || ''}}</span>-->
+<!--                  </el-col>-->
+                  <el-col :span="8">
+                    <el-form-item label="精准度偏差" prop="IN_DEVIATION">
+                      <el-input-number size="small" v-model="calRateForm.IN_DEVIATION"
+                                       controls-position="right" :min="0"/>
+                    </el-form-item>
+                  </el-col>
+                  <el-col :span="8">
+                    <el-form-item label="不可信率偏差" prop="OUT_DEVIATION">
+                      <el-input-number size="small" v-model="calRateForm.OUT_DEVIATION"
+                                       controls-position="right"
+                                       :min="1"/>
+                    </el-form-item>
+                  </el-col>
+                  <el-col :span="8">
+                    <el-form-item>
+                      <el-button size="small" type="primary" plain :loading="loading2"
+                                 @click="calAccuracyRate">计算精准度
+                      </el-button>
+                    </el-form-item>
+                  </el-col>
+                </el-row>
+                <el-row>
+                  <el-col :span="4">
+                    <el-form-item label="精准度:">
+                      {{ calRateForm.IN_ACCURACY_RATE }}%
+                    </el-form-item>
+                  </el-col>
+                  <el-col :span="4">
+                    <el-form-item label="预测平均值:">
+                      {{ calRateForm.itemPreAvg }}
+                    </el-form-item>
+                  </el-col>
+                  <el-col :span="4">
+                    <el-form-item label="预测最大值:">
+                      {{ calRateForm.itemPreMax }}
+                    </el-form-item>
+                  </el-col>
+                  <el-col :span="4">
+                    <el-form-item label="预测最小值:">
+                      {{ calRateForm.itemPreMin }}
+                    </el-form-item>
+                  </el-col>
+                  <el-col :span="4">
+                    <el-form-item label="预测累积量:">
+                      {{ calRateForm.preCumulant }}
+                    </el-form-item>
+                  </el-col>
+                  <el-col :span="4">
+                    <el-form-item label="平均绝对误差:" label-width="110px">
+                      {{ calRateForm.deviation }}
+                    </el-form-item>
+                  </el-col>
+                </el-row>
+                <el-row>
+                  <el-col :span="4">
+                    <el-form-item label="不可信率:">
+                      {{ calRateForm.OUT_ACCURACY_RATE }}%
+                    </el-form-item>
+                  </el-col>
+                  <el-col :span="4">
+                    <el-form-item label="真实平均值:">
+                      {{ calRateForm.itemAvg }}
+                    </el-form-item>
+                  </el-col>
+                  <el-col :span="4">
+                    <el-form-item label="真实最大值:">
+                      {{ calRateForm.itemMax }}
+                    </el-form-item>
+                  </el-col>
+                  <el-col :span="4">
+                    <el-form-item label="真实最小值:">
+                      {{ calRateForm.itemMin }}
+                    </el-form-item>
+                  </el-col>
+                  <el-col :span="4">
+                    <el-form-item label="真实累积量:">
+                      {{ calRateForm.realCumulant }}
+                    </el-form-item>
+                  </el-col>
+                  <el-col :span="4">
+                    <el-form-item label="累积量平均绝对误差:" label-width="152px">
+                      {{ calRateForm.deviationCumulant }}
+                    </el-form-item>
+                  </el-col>
+                </el-row>
+              </el-form>
+              <el-form :inline="true" :model="formData" label-width="100px">
+                <el-row>
+                  <el-col :span="18">
+                    <el-form-item label="数据类型">
+                      <el-checkbox-group v-model="formData.chartCheck" @change="changeChartCheck">
+                        <el-checkbox v-for="item in formData.chartOptions" :label="item"
+                                     :key="item">{{ item }}
+                        </el-checkbox>
+                      </el-checkbox-group>
+                    </el-form-item>
+                  </el-col>
+                  <el-col :span="6">
+                    <el-form-item>
+                      <el-radio v-model="formData.isMultipleY" :label="false"
+                                @input="onChangeMultipleY">单坐标轴
+                      </el-radio>
+                      <el-radio v-model="formData.isMultipleY" :label="true"
+                                @input="onChangeMultipleY">多坐标轴
+                      </el-radio>
+                    </el-form-item>
+                  </el-col>
+                </el-row>
+              </el-form>
+              <div style="width: 100%;height: 700px;display: flex;flex-direction: row;">
+                <div style="height: 100%;width: 80%">
+                  <div ref="dataAnalysisChart" style="height: 50%;width: 100%"></div>
+                  <div ref="influenceFactorChart" style="height: 50%;width: 100%"></div>
+                </div>
+                <div style="width: 20%;height: 100%;">
+                  <div style="display: flex;flex-direction: row;align-items: center;margin-bottom: 4px;">
+                    <div style="font-weight: bold;font-size: 14px">影响因素:</div>
+                    <div style="width: calc(100% - 80px);">
+                      <el-select v-model="influenceFactor" placeholder="请选择" size="small" @change="changeInfluenceFactor">
+                        <el-option
+                          v-for="(influenceFactor,index) in influenceFactorList"
+                          :key="index"
+                          :label="influenceFactor.factorOutputName"
+                          :value="influenceFactor.factorOutputId"
+                        />
+                      </el-select>
+                    </div>
+                  </div>
+                  <div class="chart-foot-table p-2" style="width: 100%;height: calc(100% - 20px);overflow-x: hidden;overflow-y: auto;">
+                    <div style="display: flex;flex-direction: column;align-items: center;margin-bottom: 4px;">
+                      <span>影响时间</span>
+                      <span style="font-size: 16px;font-weight: bold">{{influenceFactorResultTime}}</span>
+                    </div>
+                    <div v-for="(result, index) in influenceFactorResult" :key="index" style="display: flex;flex-direction: row">
+                      <span>{{result.factorOutputName}}:</span>
+                      <span>{{result.value}}</span>
+                    </div>
+                  </div>
+                </div>
+              </div>
+              <div class="chart-foot">
+                <div class="chart-foot-content">
+                  <h3 class="chart-foot-title">预警信息</h3>
+                  <div class="chart-foot-table">
+                    <el-table :data="alarmList" style="width: 100%" v-loading="loadingAlarm" height="100px">
+                      <el-table-column prop="content" header-align="center" align="left" label="消息内容" min-width="240" />
+                      <el-table-column prop="alarmType" label="预警类型" header-align="center" align="left" min-width="150"/>
+                      <el-table-column prop="alarmTime" label="预警时间" header-align="center" align="left" min-width="150"/>
+                    </el-table>
+                  </div>
+                </div>
+                <div class="chart-foot-content">
+                  <h3 class="chart-foot-title">调度建议</h3>
+                  <div class="chart-foot-table">
+                    <el-table :data="suggestList" style="width: 100%" v-loading="loadingAdjust" height="100px">
+                      <el-table-column
+                        prop="scheduleTime"
+                        label="调度时间"
+                        header-align="center"
+                        align="left"
+                        min-width="160"
+                      />
+                      <el-table-column
+                        prop="content"
+                        label="内容"
+                        min-width="300"
+                        header-align="center" align="left"
+                      />
+                      <el-table-column
+                        prop="adjustValue"
+                        label="调整值"
+                        header-align="center"
+                        align="center"
+                        min-width="100"
+                      />
+                    </el-table>
+                  </div>
+
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </el-form>
+    </div>
+  </el-card>
+</template>
+<script lang="ts" setup>
+import {getYMDHMS,formatToDateTime} from "@/utils/dateUtil"
+import * as McsApi from '@/api/model/mcs'
+import * as influenceFactorApi from '@/api/model/pre/influenceFactor/influenceFactorApi'
+import * as AlarmMessageApi from '@/api/model/pre/alarm/message'
+import * as ScheSuggestApi from '@/api/model/sche/suggest'
+import * as echarts from "echarts";
+import {Search, DArrowLeft, DArrowRight, VideoPlay, VideoPause, CaretLeft, CaretRight} from '@element-plus/icons-vue'
+import {lighten} from "@/utils/color";
+import * as LoginApi from '@/api/login'
+import * as authUtil from '@/utils/auth'
+
+defineOptions({name: 'AnalysisformDataThird'})
+
+const message = useMessage() // 消息弹窗
+const {t} = useI18n() // 国际化
+
+const loading1 = ref(false) // 列表的加载中
+const loading2 = ref(false) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 字典表格数据
+let formData = ref({
+  rangeDate: '',
+  startTime: '',
+  endTime: '',
+  predictTime: '',
+  predictTimeStr: '',
+  startTimeStr: '',
+  endTimeStr: '',
+  predictTimeStamp: 0,
+  startTimeStamp: 0,
+  endTimeStamp: 0,
+  currentStamp: '',
+  currentStamp60: '',
+  predictStamp: '',
+  chartCheck: ['T+L', '真实值'],
+  chartOptions: ['T+N', 'T+L','T+L(未调整)', '当时', '真实值', '调整值', '预测累计', '真实累计'],
+  checkedItemData: undefined,
+  backItem: '',
+  backValue: 0,
+  backCoe: 1,
+  preCumulant: 0,
+  realCumulant: 0,
+  queryStep: 2,
+  isMultipleYRadio: '单坐标轴',
+  isMultipleY: false,
+  predictFreq: 2,
+})
+const calRateFormRef = ref()
+const calRateForm = ref({
+  calItem: undefined,
+  IN_DEVIATION: 10,
+  OUT_DEVIATION: 50,
+  IN_ACCURACY_RATE: 0,
+  OUT_ACCURACY_RATE: 0,
+  itemAvg: 0,
+  itemMax: 0,
+  itemMin: 0,
+  itemPreAvg: 0,
+  itemPreMax: 0,
+  itemPreMin: 0,
+  preCumulant: 0,
+  realCumulant: 0,
+  deviation: 0, //平均绝对误差
+  deviationCumulant: 0, //累积量平均绝对误差
+})
+let itemData = ref({
+  currentTreeList: [],
+  chart: {},
+  option: {}
+})
+const treeData = ref([])
+const itemDataObject = ref()
+const timer = ref()
+const isPlay = ref(false)
+const alarmList = ref([])
+const suggestList = ref([])
+const loadingAlarm = ref(false)
+const loadingAdjust = ref(false)
+
+// 影响因素结果列表
+const influenceFactorResultList = ref([])
+// 影响因素列表
+const influenceFactorList = ref([])
+// 选中影响因素
+const influenceFactor = ref()
+
+const formRules = reactive({
+  IN_DEVIATION: [{required: true, message: '精准度偏差不能为空', trigger: 'blur'}],
+  OUT_DEVIATION: [{required: true, message: '不可信率偏差不能为空', trigger: 'blur'}],
+})
+
+// 树形过滤
+const filterText = ref('')
+const treeRef = ref()
+watch(filterText, (val) => {
+  treeRef.value!.filter(val)
+})
+const filterNode = (value: string, data) => {
+  if (!value) return true
+  return data.label.includes(value)
+}
+
+let xAxisData = []
+
+/** 查询列表 */
+const getList = async (isClear = true) => {
+  loading1.value = true
+  try {
+    if (!formData.value.chartCheck) {
+      formData.value.chartCheck = ['真实值']
+    }
+    let chartCheckArray = formData.value.chartCheck;
+    if (!formData.value.checkedItemData) {
+      itemData.value.option = {};
+      return;
+    }
+    let outIds = [formData.value.checkedItemData.id]
+    const params = reactive({
+      outIds: outIds,
+      predictTime: formData.value.predictTime,
+      startTime: formData.value.startTime,
+      endTime: formData.value.endTime
+    })
+
+
+    const data = await McsApi.getPreDataCharts(params)
+    formData.value.predictTime = data.predictTime;
+    formData.value.startTime = data.startTime
+    formData.value.endTime = data.endTime
+
+    // 默认影响时间
+    changeInfluenceFactorTime(data.predictTime);
+
+    // 获取影响因素结果列表
+    influenceFactorResultList.value = await influenceFactorApi.getResultList({
+      outIds: outIds,
+      startTime: data.startTime,
+      endTime: data.endTime
+    })
+
+    // 获取影响因素结果列表
+    influenceFactorList.value = await influenceFactorApi.getListByOutId(formData.value.checkedItemData.id)
+    if (influenceFactorList.value && influenceFactorList.value.length > 0) {
+      // 根据factorOutputId去重,因为不同的统计规则会有重复的影响因素
+      influenceFactorList.value = Array.from(new Map(influenceFactorList.value.map(item => [item.factorOutputId, item])).values());
+      // 默认选中第一个影响因素
+      influenceFactor.value = influenceFactorList.value?.[0]?.factorOutputId
+      getInfluenceFactorChart(influenceFactorList.value?.[0]?.factorOutputId)
+    }
+
+
+    const paramsAlarm = reactive({
+      outIds: outIds,
+      predictTime: formData.value.predictTime
+    })
+
+    loadingAlarm.value = true
+    alarmList.value = await AlarmMessageApi.getListByOut(paramsAlarm)
+    loadingAlarm.value = false
+
+    loadingAdjust.value = true
+    suggestList.value = await ScheSuggestApi.getListByOut(paramsAlarm)
+    loadingAdjust.value = false
+
+    xAxisData = data.categories;
+    let defaultYAxis = [
+      {
+        type: 'value',
+        name: "累计值",
+        splitLine: {show: false},
+        axisLine: {show: true},
+        position: 'right'
+      },
+      {
+        type: 'value',
+        name: "",
+        splitLine: {show: false},
+        axisLine: {show: true},
+        position: 'left'
+      }
+    ];
+    let yAxisData = [];
+    let offset = 0;
+    let yAxisIndex = 0;
+    let legendData = [];
+    let yMaxArr = [];
+    let seriesData = [];
+    seriesData.push({
+      name: '',
+      data: [null],
+      type: 'line',
+      smooth: true,
+      color: 'green',
+      markLine: {
+        silent: true,
+        lineStyle: {
+          color: '#32a487',
+          width: 2
+        },
+        data: [{
+          xAxis: formData.value.predictTime
+        }],
+        label: {
+          normal: {
+            formatter: formData.value.predictTime
+          }
+        },
+        symbol: ['circle', 'none'],
+      },
+    });
+    itemDataObject.value = {}
+    yAxisData.push({
+      type: 'value',
+      name: "累计值",
+      position: 'right',
+      splitLine: {
+        show: false
+      },
+      axisLine: {
+        show: true,
+        lineStyle: {}
+      },
+      axisLabel: {
+        formatter: '{value}'
+      }
+    })
+    for (let i = 0; i < data.dataViewList.length; i++) {
+      let dataView = data.dataViewList[i]
+      itemDataObject.value[dataView.outId] = dataView;
+      let maxValue = dataView.maxValue;
+      let minValue = dataView.minValue;
+      yAxisIndex = (formData.value.isMultipleY ? i : 0) + 1;
+      let yMax = maxValue;
+      if (maxValue < 0) {
+        maxValue = 1;
+      } else if (maxValue < 10) {
+        yMax = (Math.ceil(maxValue * 11) / 10).toFixed(1);
+      } else if (maxValue < 100) {
+        yMax = (Math.ceil(maxValue * 1.1 / 5) * 5);
+      } else {
+        yMax = (Math.ceil(maxValue * 1.1 / 10) * 10);
+      }
+      yMaxArr.push(yMax);
+      let yMin = minValue;
+      if (minValue >= 0) {
+        yMin = 0;
+      } else if (minValue > -10) {
+        yMin = (Math.floor(minValue * 11) / 10).toFixed(1);
+      } else if (minValue > -100) {
+        yMin = (Math.floor(minValue * 1.1 / 5) * 5);
+      } else {
+        yMin = (Math.floor(minValue * 1.1 / 10) * 10);
+      }
+      yAxisData.push({
+        type: 'value',
+        name: "",
+        min: yMin,
+        max: yMax,
+        position: 'left',
+        offset: offset,
+        splitLine: {
+          show: false
+        },
+        axisLine: {
+          show: true,
+          lineStyle: {}
+        },
+        axisLabel: {
+          formatter: '{value}'
+        }
+      })
+      offset = offset + 40
+      if (chartCheckArray.indexOf('真实值') !== -1) {
+        let legendName = dataView.resultName + '(真实)';
+        legendData.push(legendName);
+        seriesData.push({
+          name: legendName,
+          data: dataView.realData || [],
+          type: 'line',
+          yAxisIndex: yAxisIndex,
+          showSymbol: false,
+          smooth: false,
+          lineStyle: {
+            width: 2
+          }
+        });
+      }
+      if (chartCheckArray.indexOf('T+N') !== -1) {
+        let legendName = dataView.resultName + '(T+N)';
+        legendData.push(legendName);
+        seriesData.push({
+          name: legendName,
+          data: dataView.preDataN || [],
+          type: 'line',
+          yAxisIndex: yAxisIndex,
+          showSymbol: false,
+          smooth: false,
+          lineStyle: {
+            width: 2
+          }
+        });
+      }
+      if (chartCheckArray.indexOf('T+L') !== -1) {
+        let legendName = dataView.resultName + '(T+L)';
+        legendData.push(legendName);
+        seriesData.push({
+          name: legendName,
+          data: dataView.preDataL || [],
+          type: 'line',
+          showSymbol: false,
+          connectNulls: true,
+          yAxisIndex: yAxisIndex,
+          smooth: false,
+          lineStyle: {
+            width: 2
+          }
+        });
+      }
+      if (chartCheckArray.indexOf('T+L(未调整)') !== -1) {
+        let legendName = dataView.resultName + '(T+L(未调整))';
+        legendData.push(legendName);
+        seriesData.push({
+          name: legendName,
+          data: dataView.preDataLOriginal
+            || [],
+          type: 'line',
+          showSymbol: false,
+          connectNulls: true,
+          yAxisIndex: yAxisIndex,
+          smooth: false,
+          lineStyle: {
+            width: 2
+          }
+        });
+      }
+      if (chartCheckArray.indexOf('当时') !== -1) {
+        let legendName = dataView.resultName + '(当时)';
+        legendData.push(legendName);
+        seriesData.push({
+          name: legendName,
+          data: dataView.curData || [],
+          type: 'line',
+          yAxisIndex: yAxisIndex,
+          showSymbol: true,
+          smooth: false,
+          lineStyle: {
+            width: 3
+          }
+        });
+      }
+      if (chartCheckArray.indexOf('调整值') !== -1) {
+        let legendName = dataView.resultName + '(调整值)';
+        legendData.push(legendName);
+        seriesData.push({
+          name: legendName,
+          data: dataView.adjData || [],
+          type: 'line',
+          yAxisIndex: yAxisIndex,
+          showSymbol: false,
+          connectNulls: true,
+          smooth: false,
+          lineStyle: {
+            width: 2,
+            type: 'dashed'
+          }
+        });
+      }
+
+      if (chartCheckArray.indexOf('预测累计') !== -1) {
+        let legendName = dataView.resultName + '(预测累计)';
+        legendData.push(legendName);
+        let seriesLeiJiData = []
+        if (dataView.cumulantPreData) {
+          seriesLeiJiData = dataView.cumulantPreData
+        }
+        seriesData.push({
+          name: legendName,
+          data: seriesLeiJiData,
+          type: 'line',
+          yAxisIndex: 0,
+          showSymbol: false,
+          connectNulls: true,
+          smooth: false,
+          lineStyle: {
+            width: 2,
+            type: 'dashed'
+          }
+        });
+      }
+
+      if (chartCheckArray.indexOf('真实累计') !== -1) {
+        let legendName = dataView.resultName + '(真实累计)';
+        legendData.push(legendName);
+        let seriesLeiJiData = []
+        if (dataView.cumulantRealData) {
+          seriesLeiJiData = dataView.cumulantRealData
+        }
+        seriesData.push({
+          name: legendName,
+          data: seriesLeiJiData,
+          type: 'line',
+          yAxisIndex: 0,
+          showSymbol: false,
+          connectNulls: true,
+          smooth: false,
+          lineStyle: {
+            width: 2,
+            type: 'dashed'
+          }
+        });
+      }
+    }
+    //如果最大值相差不大,改成一致大小
+    if (yMaxArr.length > 1) {
+      let max = Math.max.apply(null, yMaxArr);
+      let min = Math.min.apply(null, yMaxArr);
+      if (Math.abs((max - min) / max) <= 0.2) {
+        for (let i = 0; i < yAxisData.length; i++) {
+          yAxisData[i].max = max;
+        }
+      }
+    }
+
+    let option = {
+      title: {
+        text: ''
+      },
+      tooltip: {
+        trigger: 'axis'
+      },
+      toolbox: {
+        show: true,
+        feature: {
+          saveAsImage: {}
+        }
+      },
+      legend: {
+        show: true,
+        data: legendData,
+        top: 10
+      },
+      grid: {
+        top: '20%',
+        left: '5%',
+        right: '5%',
+        bottom: '3%',
+        containLabel: true
+      },
+      xAxis: {
+        type: 'category',
+        boundaryGap: false,
+        data: xAxisData
+      },
+      yAxis: formData.value.isMultipleY ? yAxisData : defaultYAxis,
+      dataZoom: [
+        {
+          type: 'inside',
+          start: 0,
+          end: 100
+        },
+        {
+          start: 0,
+          end: 10
+        }
+      ],
+      series: seriesData
+    }
+    if (isClear) {
+      myChart.clear()
+    }
+
+    myChart.setOption(option)
+
+
+
+
+  } finally {
+    loading1.value = false
+  }
+
+  calItemBaseVale()
+}
+
+// 查询影响因素chart
+const getInfluenceFactorChart = async (outId) => {
+  loading1.value = true
+  try {
+
+    let outIds = [outId]
+    const params = reactive({
+      outIds: outIds,
+      predictTime: formData.value.predictTime,
+      startTime: formData.value.startTime,
+      endTime: formData.value.endTime
+    })
+
+    const data = await McsApi.getPreDataCharts(params)
+
+    if (!data?.dataViewList || data.dataViewList.length === 0) {
+      myInfluenceFactorChart.clear()
+      return
+    }
+
+    xAxisData = data.categories;
+    let defaultYAxis = [
+      {
+        type: 'value',
+        name: "累计值",
+        splitLine: {show: false},
+        axisLine: {show: true},
+        position: 'right'
+      },
+      {
+        type: 'value',
+        name: "",
+        splitLine: {show: false},
+        axisLine: {show: true},
+        position: 'left'
+      }
+    ];
+    let yAxisData = [];
+    let offset = 0;
+    let yAxisIndex = 0;
+    let legendData = [];
+    let yMaxArr = [];
+    let seriesData = [];
+    seriesData.push({
+      name: '',
+      data: [null],
+      type: 'line',
+      smooth: true,
+      color: 'green',
+      markLine: {
+        silent: true,
+        lineStyle: {
+          color: '#32a487',
+          width: 2
+        },
+        data: [{
+          xAxis: formData.value.predictTime
+        }],
+        label: {
+          normal: {
+            formatter: formData.value.predictTime
+          }
+        },
+        symbol: ['circle', 'none'],
+      },
+    });
+    itemDataObject.value = {}
+    yAxisData.push({
+      type: 'value',
+      name: "累计值",
+      position: 'right',
+      splitLine: {
+        show: false
+      },
+      axisLine: {
+        show: true,
+        lineStyle: {}
+      },
+      axisLabel: {
+        formatter: '{value}'
+      }
+    })
+    for (let i = 0; i < data.dataViewList.length; i++) {
+      let dataView = data.dataViewList[i]
+      itemDataObject.value[dataView.outId] = dataView;
+      let maxValue = dataView.maxValue;
+      let minValue = dataView.minValue;
+      yAxisIndex = (formData.value.isMultipleY ? i : 0) + 1;
+      let yMax = maxValue;
+      if (maxValue < 0) {
+        maxValue = 1;
+      } else if (maxValue < 10) {
+        yMax = (Math.ceil(maxValue * 11) / 10).toFixed(1);
+      } else if (maxValue < 100) {
+        yMax = (Math.ceil(maxValue * 1.1 / 5) * 5);
+      } else {
+        yMax = (Math.ceil(maxValue * 1.1 / 10) * 10);
+      }
+      yMaxArr.push(yMax);
+      let yMin = minValue;
+      if (minValue >= 0) {
+        yMin = 0;
+      } else if (minValue > -10) {
+        yMin = (Math.floor(minValue * 11) / 10).toFixed(1);
+      } else if (minValue > -100) {
+        yMin = (Math.floor(minValue * 1.1 / 5) * 5);
+      } else {
+        yMin = (Math.floor(minValue * 1.1 / 10) * 10);
+      }
+      yAxisData.push({
+        type: 'value',
+        name: "",
+        min: yMin,
+        max: yMax,
+        position: 'left',
+        offset: offset,
+        splitLine: {
+          show: false
+        },
+        axisLine: {
+          show: true,
+          lineStyle: {}
+        },
+        axisLabel: {
+          formatter: '{value}'
+        }
+      })
+      offset = offset + 40
+      //真实值
+      legendData.push(dataView.resultName + '(真实)');
+      seriesData.push({
+        name: dataView.resultName + '(真实)',
+        data: dataView.realData || [],
+        type: 'line',
+        yAxisIndex: yAxisIndex,
+        showSymbol: false,
+        smooth: false,
+        lineStyle: {
+          width: 2
+        }
+      });
+      //T+L
+      legendData.push(dataView.resultName + '(T+L)');
+      seriesData.push({
+        name: dataView.resultName + '(T+L)',
+        data: dataView.preDataL || [],
+        type: 'line',
+        showSymbol: false,
+        connectNulls: true,
+        yAxisIndex: yAxisIndex,
+        smooth: false,
+        lineStyle: {
+          width: 2
+        }
+      });
+      // 当时
+      legendData.push(dataView.resultName + '(当时)');
+      seriesData.push({
+        name: dataView.resultName + '(当时)',
+        data: dataView.curData || [],
+        type: 'line',
+        yAxisIndex: yAxisIndex,
+        showSymbol: true,
+        smooth: false,
+        lineStyle: {
+          width: 3
+        }
+      });
+      //预测累计
+      legendData.push(dataView.resultName + '(预测累计)');
+      seriesData.push({
+        name: dataView.resultName + '(预测累计)',
+        data: dataView.cumulantPreData || [],
+        type: 'line',
+        yAxisIndex: 0,
+        showSymbol: false,
+        connectNulls: true,
+        smooth: false,
+        lineStyle: {
+          width: 2,
+          type: 'dashed'
+        }
+      });
+      // 真实累计
+      legendData.push(dataView.resultName + '(真实累计)');
+      seriesData.push({
+        name: dataView.resultName + '(真实累计)',
+        data: dataView.cumulantRealData || [],
+        type: 'line',
+        yAxisIndex: 0,
+        showSymbol: false,
+        connectNulls: true,
+        smooth: false,
+        lineStyle: {
+          width: 2,
+          type: 'dashed'
+        }
+      });
+    }
+    //如果最大值相差不大,改成一致大小
+    if (yMaxArr.length > 1) {
+      let max = Math.max.apply(null, yMaxArr);
+      let min = Math.min.apply(null, yMaxArr);
+      if (Math.abs((max - min) / max) <= 0.2) {
+        for (let i = 0; i < yAxisData.length; i++) {
+          yAxisData[i].max = max;
+        }
+      }
+    }
+
+    let option = {
+      title: {
+        text: ''
+      },
+      tooltip: {
+        trigger: 'axis'
+      },
+      toolbox: {
+        show: true,
+        feature: {
+          saveAsImage: {}
+        }
+      },
+      legend: {
+        show: true,
+        data: legendData,
+        top: 10
+      },
+      grid: {
+        top: '20%',
+        left: '5%',
+        right: '5%',
+        bottom: '3%',
+        containLabel: true
+      },
+      xAxis: {
+        type: 'category',
+        boundaryGap: false,
+        data: xAxisData
+      },
+      yAxis: formData.value.isMultipleY ? yAxisData : defaultYAxis,
+      dataZoom: [
+        {
+          type: 'inside',
+          start: 0,
+          end: 100
+        },
+        {
+          start: 0,
+          end: 10
+        }
+      ],
+      series: seriesData
+    }
+    myInfluenceFactorChart.clear()
+    myInfluenceFactorChart.setOption(option)
+  } finally {
+    loading1.value = false
+  }
+}
+
+
+
+onMounted(() => {
+  if(!authUtil.getTenantId()) {
+    handleLogin()
+  }
+  initChart()
+  resetForm()
+  getPreItemTree()
+})
+
+const loginData = reactive({
+  isShowPassword: false,
+  captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE,
+  tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE,
+  loginForm: {
+    tenantName: 'ansteel',
+    username: 'model',
+    password: 'anxin123456!@',
+    captchaVerification: '',
+    rememberMe: true // 默认记录我。如果不需要,可手动修改
+  }
+})
+
+// 登录
+const handleLogin = async () => {
+  try {
+    await getTenantId()
+    const res = await LoginApi.login(loginData.loginForm)
+    if (!res) {
+      return
+    }
+    if (loginData.loginForm.rememberMe) {
+      authUtil.setLoginForm(loginData.loginForm)
+    } else {
+      authUtil.removeLoginForm()
+    }
+    authUtil.setToken(res)
+  } catch (e) {
+    message.error("对不起,您没有权限,请联系管理员")
+  }
+}
+
+// 获取租户 ID
+const getTenantId = async () => {
+  if (loginData.tenantEnable === 'true') {
+    const res = await LoginApi.getTenantIdByName(loginData.loginForm.tenantName)
+    authUtil.setTenantId(res)
+  }
+}
+
+const dataAnalysisChart = ref();
+const influenceFactorChart = ref();
+let myChart = ref({});
+let myInfluenceFactorChart = ref({});
+
+function initChart() {
+  myChart = echarts.init(dataAnalysisChart.value)
+  myInfluenceFactorChart = echarts.init(influenceFactorChart.value)
+  // 监听点击事件
+  myChart.getZr().on('click', 'series.line',function (params) {
+    var pointInPixel = [params.offsetX, params.offsetY];
+    var pointInData = myChart.convertFromPixel('grid', pointInPixel);
+    const time = xAxisData[pointInData[0]];
+    changeInfluenceFactorTime(time)
+  });
+}
+
+let influenceFactorResult = ref([])
+let influenceFactorResultTime = ref('')
+// 影响因素时间改变
+function changeInfluenceFactorTime(time) {
+  if (time && new Date(time)?.getTime()) {
+    influenceFactorResultTime.value = time
+    influenceFactorResult.value = influenceFactorResultList.value?.[formData.value.checkedItemData?.id]?.filter(e => e.time === new Date(time).getTime()).sort((a, b) => b.value - a.value) || [];
+  }
+}// 选择影响因素
+function changeInfluenceFactor(value) {
+  getInfluenceFactorChart(value)
+}
+
+async function getPreItemTree() {
+  treeData.value = await McsApi.getPredictItemTree()
+}
+
+function changeChartCheck(value) {
+  getList(true)
+}
+
+function onChangeMultipleY() {
+  getList(true)
+}
+
+function playChart(isBack = false) {
+  let mins = isBack ? formData.value.predictFreq * -1 : formData.value.predictFreq
+  let startTime = formData.value.startTime;
+  let endTime = formData.value.endTime;
+  let predictTime = formData.value.predictTime;
+  if (predictTime) {
+    predictTime = getYMDHMS(new Date(predictTime).getTime() + 1000 * 60 * mins);
+    formData.value.predictTime = predictTime;
+  }
+  if (startTime) {
+    startTime = getYMDHMS(new Date(startTime).getTime() + 1000 * 60 * mins);
+    formData.value.startTime = startTime;
+  }
+  if (endTime) {
+    endTime = getYMDHMS(new Date(endTime).getTime() + 1000 * 60 * mins);
+    formData.value.endTime = endTime;
+  }
+  getList(false);
+}
+
+function playHandle(type) {
+  isPlay.value = 'play' === type
+  let doPlay = setInterval(function () {
+    if (isPlay.value) {
+      playChart()
+    } else {
+      clearInterval(doPlay);
+    }
+    if (new Date().getTime() - new Date(formData.value.predictTime).getTime() < 1000 * 60 ) {
+      isPlay.value = false
+      clearInterval(doPlay);
+    }
+  }, 1000)
+}
+
+function leftSearchDataByRange() {
+  let mins = getRangeMins();
+  let startTime = formData.value.startTime;
+  let endTime = formData.value.endTime;
+  let predictTime = formData.value.predictTime;
+  if (predictTime) {
+    predictTime = getYMDHMS(new Date(predictTime).getTime() - 1000 * 60 * mins);
+    formData.value.predictTime = predictTime;
+  }
+  if (startTime) {
+    startTime = getYMDHMS(new Date(startTime).getTime() - 1000 * 60 * mins);
+    formData.value.startTime = startTime;
+  }
+  if (endTime) {
+    endTime = getYMDHMS(new Date(endTime).getTime() - 1000 * 60 * mins);
+    formData.value.endTime = endTime;
+  }
+  getList(false);
+}
+
+function getRangeMins() {
+  let result: string | number = 0;
+  if (formData.value.startTime && formData.value.endTime) {
+    let startStamp = new Date(formData.value.startTime).getTime();
+    let endStamp = new Date(formData.value.endTime).getTime();
+    let queryStep = ((endStamp - startStamp) / (1000 * 60)).toFixed(0);
+    result = queryStep >= 0 ? queryStep : 0;
+  }
+  return result;
+}
+
+function onCheckTree(data, checked, indeterminate) {
+  // 单选
+  treeRef.value.setCheckedKeys([])
+  treeRef.value.setCheckedNodes([data])
+
+  formData.value.checkedItemData = data
+  calRateForm.value.calItem = data.id
+  // if (checked.checkedNodes) {
+  //   let cns = [...checked.checkedNodes]
+  //   for (let i = 0; i < cns.length; i++) {
+  //     if (cns[i].id.indexOf('-') !== -1) {
+  //       continue
+  //     }
+  //     formData.value.checkedItemData.push(cns[i])
+  //   }
+  // }
+  debounce(getList, 1000);
+}
+
+function debounce(func, wait) {
+  let args = [];
+  if (timer.value) {
+    clearTimeout(timer.value);
+  }
+  timer.value = setTimeout(() => {
+    func.apply(this, args);
+    timer.value = null;
+  }, wait)
+}
+
+function calItemBaseVale() {
+  if (!calRateForm.value.calItem) {
+    calRateForm.value.itemPreMax = 0;
+    calRateForm.value.itemPreMin = 0;
+    calRateForm.value.itemPreAvg = 0;
+    calRateForm.value.preCumulant = 0;
+    calRateForm.value.itemMax = 0;
+    calRateForm.value.itemMin = 0;
+    calRateForm.value.itemAvg = 0;
+    calRateForm.value.realCumulant = 0;
+  } else {
+    let dataView = itemDataObject.value[calRateForm.value.calItem]
+    calRateForm.value.itemPreMax = dataView.preMax;
+    calRateForm.value.itemPreMin = dataView.preMin;
+    calRateForm.value.itemPreAvg = dataView.preAvg;
+    calRateForm.value.preCumulant = dataView.preCumulant;
+    calRateForm.value.itemMax = dataView.hisMax;
+    calRateForm.value.itemMin = dataView.hisMin;
+    calRateForm.value.itemAvg = dataView.hisAvg;
+    calRateForm.value.realCumulant = dataView.hisCumulant;
+    calDeviation(dataView.realData,dataView.preDataL,'deviation')
+    calDeviation(dataView.cumulantRealData,dataView.cumulantPreData,'deviationCumulant')
+    calAccuracyRate()
+  }
+}
+
+function calAccuracyRate() {
+  const valid = calRateFormRef.value.validate()
+  if (!valid) return
+
+  let dataView = itemDataObject.value[calRateForm.value.calItem]
+  let seriesReaData = dataView.realData;
+  let seriesPreData = dataView.preDataL;
+  if (seriesReaData == null || seriesPreData == null ||
+    seriesReaData.length === 0 || seriesPreData.length === 0) {
+    loading2.value = false;
+    return;
+  }
+  let predictValueMap = {};
+  seriesPreData.forEach(function (item) {
+    predictValueMap[item[0]] = item[1];
+  })
+  let pointValueMap = {};
+  seriesReaData.forEach(function (item) {
+    pointValueMap[item[0]] = item[1];
+  })
+  let inDeviation = Number(calRateForm.value.IN_DEVIATION);
+  let outDeviation = Number(calRateForm.value.OUT_DEVIATION);
+  if (inDeviation === 0 && outDeviation === 0) {
+    loading2.value = false;
+    return;
+  }
+  let inDeviationCount = 0;
+  let outDeviationCount = 0;
+  let totalCount = 0;
+  for (let key in predictValueMap) {
+    let predictValue = predictValueMap[key];
+    let pointValue = pointValueMap[key];
+    if (pointValue == null || "" === pointValue || predictValue == null || "" === predictValue) {
+      continue;
+    }
+    let deviationAbs = (predictValue - pointValue) >= 0 ? (predictValue - pointValue) : (predictValue - pointValue) * -1;
+    if (deviationAbs < inDeviation) {
+      inDeviationCount = inDeviationCount + 1;
+    }
+    if (deviationAbs > outDeviation) {
+      outDeviationCount = outDeviationCount + 1;
+    }
+    totalCount = totalCount + 1;
+  }
+
+  let rateIn = (inDeviationCount / totalCount * 100).toFixed(2);
+  let rateOut = (outDeviationCount / totalCount * 100).toFixed(2);
+  calRateForm.value.IN_ACCURACY_RATE = Number(rateIn);
+  calRateForm.value.OUT_ACCURACY_RATE = Number(rateOut);
+  loading2.value = false;
+}
+
+function calDeviation(realData,preDataL,key) {
+  if (realData == null || preDataL == null || realData.length === 0 || preDataL.length === 0) {
+    return;
+  }
+  const realObj = {}
+  realData.map(e => realObj[e[0]] = e[1])
+
+  let sum = 0;
+  let index = 0;
+  preDataL.forEach(e => {
+    if (realObj[e[0]] != undefined) {
+      sum += Math.abs(e[1] - realObj[e[0]])
+      index++
+    }
+  })
+  calRateForm.value[key] = Number((sum / index).toFixed(2))
+}
+
+function rightSearchDataByRange() {
+  let mins = getRangeMins();
+  let startTime = formData.value.startTime;
+  let endTime = formData.value.endTime;
+  let predictTime = formData.value.predictTime;
+  if (predictTime) {
+    predictTime = getYMDHMS(new Date(predictTime).getTime() + 1000 * 60 * mins);
+    formData.value.predictTime = predictTime;
+  }
+  if (startTime) {
+    startTime = getYMDHMS(new Date(startTime).getTime() + 1000 * 60 * mins);
+    formData.value.startTime = startTime;
+  }
+  if (endTime) {
+    endTime = getYMDHMS(new Date(endTime).getTime() + 1000 * 60 * mins);
+    formData.value.endTime = endTime;
+  }
+  getList(false);
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    rangeDate: '',
+    startTime: '',
+    endTime: '',
+    predictTime: '',
+    predictTimeStr: '',
+    startTimeStr: '',
+    endTimeStr: '',
+    predictTimeStamp: 0,
+    startTimeStamp: 0,
+    endTimeStamp: 0,
+    currentStamp: '',
+    currentStamp60: '',
+    predictStamp: '',
+    chartCheck: ['T+L', '真实值'],
+    chartOptions: ['T+N', 'T+L','T+L(未调整)', '当时', '真实值', '调整值', '预测累计', '真实累计'],
+    checkedItemData: undefined,
+    backItem: '',
+    backValue: 0,
+    backCoe: 1,
+    preCumulant: 0,
+    realCumulant: 0,
+    queryStep: 2,
+    isMultipleYRadio: '单坐标轴',
+    isMultipleY: false,
+    predictFreq: 2,
+  }
+  calRateForm.value = {
+    calItem: undefined,
+    IN_DEVIATION: 10,
+    OUT_DEVIATION: 50,
+    IN_ACCURACY_RATE: 0,
+    OUT_ACCURACY_RATE: 0,
+    itemAvg: 0,
+    itemMax: 0,
+    itemMin: 0,
+    itemPreAvg: 0,
+    itemPreMax: 0,
+    itemPreMin: 0,
+    preCumulant: 0,
+    realCumulant: 0,
+    deviation: 0, //平均绝对误差
+    deviationCumulant: 0, //累积量平均绝对误差
+  }
+  calRateFormRef.value?.resetFields()
+}
+</script>
+<style scoped>
+.chart-foot-table {
+  border: 1px solid #bababa;
+}
+.chart-foot-title {
+  font-size: 14px;
+}
+.chart-foot-content {
+  height: 100%;
+  width: 50%;
+  padding: 5px;
+}
+.chart-foot {
+  height: 120px;
+  width: 100%;
+  display: flex;
+  flex-direction: row;
+}
+.el-form-item {
+  margin-bottom: 0 !important;
+}
+
+.his-body-chart {
+  height: 100%;
+  border: 1px solid lightgray;
+  padding: 10px;
+  overflow-y: auto;
+  overflow-x: hidden;
+}
+
+.his-body-tree {
+  height: 100%;
+  border: 1px solid lightgray;
+  padding: 10px 10px 20px 10px;
+  overflow-y: auto;
+}
+
+.his-body-right {
+  width: 80%;
+  height: 100%;
+  padding-top: 10px;
+}
+
+.his-body-left {
+  width: 20%;
+  height: 100%;
+  padding: 10px 10px 0 0;
+}
+
+.his-body {
+  width: 100%;
+  height: calc(calc(100vh - 68px - 38px - 60px));
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-start;
+  align-content: flex-start;
+}
+</style>

--
Gitblit v1.9.3