فهرست منبع

Merge branch 'v1.7.1_EDB'

jwyu 1 سال پیش
والد
کامیت
8a0a24b6f3
39فایلهای تغییر یافته به همراه5476 افزوده شده و 59 حذف شده
  1. 4 0
      README.md
  2. 1 0
      package.json
  3. 98 1
      src/api/dataEDB.js
  4. 28 0
      src/assets/styles/common.scss
  5. 4 0
      src/assets/svg/close.svg
  6. 6 0
      src/assets/svg/edb-history-tag.svg
  7. 5 0
      src/assets/svg/edit.svg
  8. 5 0
      src/assets/svg/error-circle-filled.svg
  9. 8 0
      src/assets/svg/swap.svg
  10. 5 0
      src/assets/svg/warning.svg
  11. 46 0
      src/components/SelectDate.vue
  12. 62 0
      src/components/SelectDateRange.vue
  13. 29 0
      src/components/SvgIcon.vue
  14. 13 23
      src/hooks/edb/useCopyEdbData.js
  15. 5 0
      src/main.js
  16. 32 0
      src/router/dataEDB.js
  17. 3 2
      src/views/dataEDB/AddBaseEDB.vue
  18. 19 16
      src/views/dataEDB/Detail.vue
  19. 32 6
      src/views/dataEDB/Index.vue
  20. 1 1
      src/views/dataEDB/RelationEDB.vue
  21. 143 0
      src/views/dataEDB/ReplaceEDB.vue
  22. 4 2
      src/views/dataEDB/SearchList.vue
  23. 782 0
      src/views/dataEDB/calculate/BatchDetail.vue
  24. 41 0
      src/views/dataEDB/calculate/Detail.vue
  25. 154 0
      src/views/dataEDB/calculate/Index.vue
  26. 571 0
      src/views/dataEDB/calculate/components/DiffusionIndexCalcualate.vue
  27. 568 0
      src/views/dataEDB/calculate/components/FittingResidualsCalculate.vue
  28. 543 0
      src/views/dataEDB/calculate/components/FormulaCalculate.vue
  29. 494 0
      src/views/dataEDB/calculate/components/JointCalculate.vue
  30. 744 0
      src/views/dataEDB/calculate/components/OtherCalculate.vue
  31. 169 0
      src/views/dataEDB/calculate/components/SeeEDBDataList.vue
  32. 341 0
      src/views/dataEDB/calculate/components/SelectEDB.vue
  33. 13 6
      src/views/dataEDB/components/EDBHistory.vue
  34. 88 0
      src/views/dataEDB/components/SelectEDBClassify.vue
  35. 39 0
      src/views/dataEDB/components/SelectEDBFrequency.vue
  36. 39 0
      src/views/dataEDB/components/SelectEDBUnit.vue
  37. 316 0
      src/views/dataEDB/util/config.js
  38. 2 1
      src/views/tabbar/Home.vue
  39. 19 1
      vite.config.js

+ 4 - 0
README.md

@@ -1,3 +1,7 @@
 # 移动ETA
 1. 图片等静态资源如果要放在oss中请放入 oss://hzchart/static/ETA_mobile/ 目录下。
 2. 测试地址:http://8.136.199.33:8611/tabbar/home
+3. 图标尽量使用svg,将svg图标文件放入assets/svg 目录下,使用时直接用<svg-icon name="文件名"/>
+   如果要修改svg颜色将下载的svg中的fill的value改为currentColor(fill="currentColor"),然后在svg-icon 组件上设置class然后设置颜色值
+   或者直接把fill删除 可以直接通过color属性来设置颜色
+4. ios端设置剪切板数据无法在异步方法后设置

+ 1 - 0
package.json

@@ -42,6 +42,7 @@
     "sass": "^1.44.0",
     "unplugin-vue-components": "^0.24.1",
     "vite": "^4.2.0",
+    "vite-plugin-svg-icons": "^2.0.1",
     "vite-plugin-vue-setup-extend": "^0.4.0"
   }
 }

+ 98 - 1
src/api/dataEDB.js

@@ -15,7 +15,7 @@ export default{
      * @param CurrentIndex
      * @param PageSize
      */
-    edbChartSearchList(params){
+    edbSearchList(params){
         return get('/datamanage/edb_info/filter_by_es',params)
     },
 
@@ -270,5 +270,102 @@ export default{
      */
     getEDBSourceOpts(params){
         return get('/datamanage/edb_source/list',params)
+    },
+
+    /**
+     * 计算指标(指标运算新增)
+     * @param CalculateFormula 计算公式
+     * @param ClassifyId
+     * @param EdbName
+     * @param Frequency
+     * @param Unit
+     * @param EdbInfoIdArr eg:[{EdbInfoId:100,FromTag:A}]
+     */
+    addCalculateFormula(params){
+        return post('/datamanage/edb_info/calculate/save',params)
+    },
+
+    /**
+     * 计算指标(指标运算编辑)
+     * @param EdbInfoId
+     * @param CalculateFormula 计算公式
+     * @param ClassifyId
+     * @param EdbName
+     * @param Frequency
+     * @param Unit
+     * @param EdbInfoIdArr eg:[{EdbInfoId:100,FromTag:A}]
+     */
+    editCalculateFormula(params){
+        return post('/datamanage/edb_info/calculate/edit',params)
+    },
+
+    /**
+     * 指标计算新增
+     * @param FromEdbInfoId
+     * @param Source
+     * @param ClassifyId
+     * @param EdbName
+     * @param Frequency
+     * @param Unit
+     * @param Formula
+     * @param MoveFrequency
+     * @param MoveType
+     * @param Calendar
+     */
+    addCalculateEDB(params){
+        return post('/datamanage/edb_info/calculate/batch/save',params)
+    },
+
+    /**
+     * 指标计算编辑
+     * @param EdbInfoId
+     * @param FromEdbInfoId
+     * @param Source
+     * @param ClassifyId
+     * @param EdbName
+     * @param Frequency
+     * @param Unit
+     * @param Formula
+     * @param MoveFrequency
+     * @param MoveType
+     * @param Calendar
+     */
+    editCalculateEDB(params){
+        return post('/datamanage/edb_info/calculate/batch/edit',params)
+    },
+
+    /**
+     * 拟合残差两个指标相关系数
+     * @param Formula 传的日期eg:‘2023-08-10,2023-09-10’
+     * @param EdbInfoIdArr
+     */
+    edbCorrelationIndex(params){
+        return post('/datamanage/edb_info/calculate/compute_correlation',params)
+    },
+
+    /**
+     * 批量添加指标进行计算
+     * @params [{CalculateId,CalculateInfo:{}}]
+     */
+    batchAddCalculateEDB(params){
+        return post('/datamanage/edb_info/calculate/batch/save/batch',params)
+    },
+
+    /**
+     * 指标替换前校验
+     */
+    replaceEDBCheck(){
+        return post('/datamanage/edb_info/replace/check',{})
+    },
+
+    /**
+     * 指标替换
+     * @param NewEdbInfoId
+     * @param OldEdbInfoId
+     */
+    replaceEDB(params){
+        return post('/datamanage/edb_info/replace',params)
     }
+
+
 }

+ 28 - 0
src/assets/styles/common.scss

@@ -62,6 +62,12 @@ img {
     -khtml-user-select:none;
 }
 
+//英文数字强制换行
+.en-text-wrap{
+    word-break: break-all;
+    word-wrap: break-word;
+}
+
 // 列表无数据占位图
 .list-empty-img{
     width: 400px;
@@ -85,6 +91,13 @@ img {
     }
 }
 
+// vant按钮次级的
+.primary2{
+    background-color: #F2F3FF !important;
+    border-color: #F2F3FF !important;
+    color: $theme-color;
+}
+
 // 报告详情样式
 .report-html-wrap{
     line-height: 1.8;
@@ -156,4 +169,19 @@ a[href="https://froala.com/wysiwyg-editor"], a[href="https://www.froala.com/wysi
             margin-bottom: 16px;
         }
     }
+}
+
+//edb计算指标公式说明弹窗样式
+.edb-formula-tips-html-box{
+    font-size: var(--van-dialog-font-size);
+    color: $font-grey;
+    line-height: 1.5;
+    padding: 36px;
+    max-height: 60vh;
+    overflow-y: auto;
+}
+@media screen and (min-width:650px){
+    .edb-formula-tips-html-box{
+        padding: 18px;
+    }
 }

+ 4 - 0
src/assets/svg/close.svg

@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28" fill="none">
+  <rect width="28" height="28" rx="14" fill="#999999"/>
+  <path d="M14 15.6106L19.3894 21L21 19.3893L15.6106 14L21 8.61066L19.3894 7.00005L14 12.3894L8.61062 7L7 8.61061L12.3894 14L7 19.3894L8.61062 21L14 15.6106Z" fill="white"/>
+</svg>

+ 6 - 0
src/assets/svg/edb-history-tag.svg

@@ -0,0 +1,6 @@
+<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="app">
+<rect width="48" height="48" rx="4" fill="#F2F3FF"/>
+<path id="Vector" d="M23.5 8C32.0608 8 39 14.9392 39 23.5C39 24.6462 38.8752 25.7765 38.6312 26.8766L38.5334 27.2876L36.2203 26.7073C36.4826 25.6676 36.6154 24.5929 36.6154 23.5C36.6154 16.2563 30.7437 10.3846 23.5 10.3846C16.2563 10.3846 10.3846 16.2563 10.3846 23.5C10.3846 30.7437 16.2563 36.6154 23.5 36.6154C26.57 36.6203 29.5435 35.5435 31.8986 33.5742L31.9845 33.5003L22.6662 24.3513L24.3338 22.6479L33.9669 32.1053L35.3945 33.5098L34.5598 34.3603C33.1186 35.8317 31.3977 37.0001 29.4984 37.7968C27.5991 38.5936 25.5597 39.0027 23.5 39C14.9392 39 8 32.0608 8 23.5C8 14.9392 14.9392 8 23.5 8ZM23.5 15.9487C24.6198 15.9485 25.7256 16.1974 26.7374 16.6772C27.7492 17.1571 28.6415 17.856 29.3499 18.7233C30.0582 19.5906 30.5648 20.6046 30.833 21.6918C31.1011 22.779 31.1241 23.9123 30.9003 25.0095L30.8343 25.3036L28.518 24.736C28.7852 23.6506 28.6931 22.5079 28.2557 21.4793C27.8183 20.4507 27.0591 19.5916 26.0921 19.0311C25.125 18.4706 24.0022 18.2388 22.8923 18.3705C21.7823 18.5022 20.745 18.9903 19.9359 19.7616C19.1269 20.5329 18.5899 21.5458 18.4053 22.6482C18.2208 23.7506 18.3988 24.8832 18.9125 25.8759C19.4263 26.8686 20.2481 27.6679 21.2547 28.1539C22.2613 28.6399 23.3983 28.7863 24.4952 28.5713L24.7376 28.518L25.306 30.8335C24.2688 31.0881 23.1894 31.1199 22.1391 30.9266C21.0887 30.7334 20.0913 30.3195 19.2127 29.7124C18.3341 29.1053 17.5943 28.3187 17.042 27.4046C16.4898 26.4905 16.1378 25.4697 16.0091 24.4095C15.8805 23.3493 15.9782 22.2739 16.2958 21.2543C16.6134 20.2346 17.1437 19.294 17.8517 18.4943C18.5596 17.6947 19.4291 17.0544 20.4027 16.6156C21.3764 16.1768 22.432 15.9495 23.5 15.9487Z" fill="#0052D9"/>
+</g>
+</svg>

+ 5 - 0
src/assets/svg/edit.svg

@@ -0,0 +1,5 @@
+<svg width="48" height="48" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
+<g id="&#231;&#188;&#150;&#232;&#190;&#145; 1">
+<path id="Vector" d="M23.4 13.2L34.8 24.6L19.4 40H8V28.6L23.4 13.2ZM23.4 19L12 30.4V36H17.6L29 24.6L23.4 19ZM28.8 8L40 19.4L36.6 22.8L25.2 11.4L28.8 8Z"/>
+</g>
+</svg>

+ 5 - 0
src/assets/svg/error-circle-filled.svg

@@ -0,0 +1,5 @@
+<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="error-circle-filled">
+<path id="union" d="M30 16C30 23.732 23.732 30 16 30C8.26801 30 2 23.732 2 16C2 8.26801 8.26802 2 16 2C23.732 2 30 8.26802 30 16ZM17 8.00061H15V19H17V8.00061ZM14.7886 22V24.4H17.1886V22H14.7886Z" fill="#999999"/>
+</g>
+</svg>

+ 8 - 0
src/assets/svg/swap.svg

@@ -0,0 +1,8 @@
+<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="swap">
+<g id="union">
+<path d="M2.50049 11.9999L26.0165 11.9999L18.3281 4.50217L19.7244 3.07031L29.0668 12.181C29.7475 12.8447 29.2776 13.9999 28.3268 13.9999L2.50049 13.9999V11.9999Z" fill="#0052D9"/>
+<path d="M29.5 19.9999L6.04053 19.9999L13.6611 27.2007L12.2874 28.6544L2.94908 19.8303C2.2522 19.1718 2.71812 17.9999 3.6771 17.9999L29.5 17.9999V19.9999Z" fill="#0052D9"/>
+</g>
+</g>
+</svg>

+ 5 - 0
src/assets/svg/warning.svg

@@ -0,0 +1,5 @@
+<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="error-circle-filled">
+<path id="union" d="M22.5 12.5C22.5 18.299 17.799 23 12 23C6.20101 23 1.5 18.299 1.5 12.5C1.5 6.70101 6.20101 2 12 2C17.799 2 22.5 6.70101 22.5 12.5ZM12.75 6.50045H11.25V14.75H12.75V6.50045ZM11.0914 17V18.8H12.8914V17H11.0914Z" fill="#0052D9"/>
+</g>
+</svg>

+ 46 - 0
src/components/SelectDate.vue

@@ -0,0 +1,46 @@
+<script setup>
+import moment from 'moment'
+import {ref} from 'vue'
+
+const props=defineProps({
+    show:{
+        type:Boolean,
+        default:false
+    }
+})
+const emits=defineEmits(['update:show','select'])
+
+function handleClose(){
+    emits('update:show',false)
+}
+
+const minDate=new Date(1970, 0, 1)
+const maxDate=moment().add(10,'years')._d
+const currentDate=ref(moment().format('YYYY-MM-DD').split('-'))
+
+function handleConfirmDate({selectedValues, selectedOptions}){
+    // console.log(selectedValues, selectedOptions);
+    emits('select',selectedValues.join('-'))
+    handleClose()
+}
+
+</script>
+
+<template>
+    <van-popup 
+        :show="props.show"
+        position="bottom"
+        round
+        @click-overlay="handleClose"
+        @click-close-icon="handleClose"
+    >
+        <van-date-picker
+            v-model="currentDate"
+            title="选择日期"
+            :min-date="minDate"
+            :max-date="maxDate"
+            @confirm="handleConfirmDate"
+            @cancel="handleClose"
+        />
+    </van-popup>
+</template>

+ 62 - 0
src/components/SelectDateRange.vue

@@ -0,0 +1,62 @@
+<script setup>
+import moment from 'moment'
+import { showToast } from 'vant'
+import {ref} from 'vue'
+
+const props=defineProps({
+    show:{
+        type:Boolean,
+        default:false
+    }
+})
+const emits=defineEmits(['update:show','select'])
+
+function handleClose(){
+    emits('update:show',false)
+}
+
+const minDate=new Date(1970, 0, 1)
+const maxDate=moment().add(10,'years')._d
+const startDate=ref(moment().format('YYYY-MM-DD').split('-'))
+const endDate=ref(moment().format('YYYY-MM-DD').split('-'))
+
+function handleConfirmDate(){
+    const isBefore=moment(endDate.value).isBefore(startDate.value,'day')
+    if(isBefore){
+        showToast('开始日期不能晚于结束日期')
+        return
+    }
+    emits('select',[startDate.value.join('-'),endDate.value.join('-')])
+    handleClose()
+}
+
+</script>
+
+<template>
+    <van-popup 
+        :show="props.show"
+        position="bottom"
+        round
+        next-step-text="下一步"
+        @click-overlay="handleClose"
+        @click-close-icon="handleClose"
+    >
+        <van-picker-group
+            title="选择日期"
+            :tabs="['开始日期', '结束日期']"
+            @confirm="handleConfirmDate"
+            @cancel="handleClose"
+        >
+            <van-date-picker
+                v-model="startDate"
+                :min-date="minDate"
+                :max-date="maxDate"
+            />
+            <van-date-picker 
+                v-model="endDate" 
+                :min-date="minDate" 
+                :max-date="maxDate" 
+            />
+        </van-picker-group>
+    </van-popup>
+</template>

+ 29 - 0
src/components/SvgIcon.vue

@@ -0,0 +1,29 @@
+<script setup>
+import { computed } from 'vue'
+const props = defineProps({
+  // prefix: {
+  //   type: String,
+  //   default: 'icon'
+  // },
+  name: {
+    type: String,
+    required: true
+  },
+  color: {
+    type: String,
+    default: '#333'
+  },
+  size: {
+    type: String,
+    default: '1em'
+  }
+})
+
+const symbolId = computed(() => `#${props.name}`)
+</script>
+
+<template>
+  <svg aria-hidden="true" class="svg-icon" :width="props.size" :height="props.size">
+    <use :xlink:href="symbolId" :fill="props.color" />
+  </svg>
+</template>

+ 13 - 23
src/hooks/edb/useCopyEdbData.js

@@ -1,32 +1,22 @@
 // 复制指标数据
-import apiDataEDB from '@/api/dataEDB'
-import apiDataPredictEDB from '@/api/dataPredictEDB'
 import { showToast } from 'vant'
 import { copyText } from 'vue3-clipboard'
 
 export function useCopyEdbData(){
 
-    async function copyData(e){
-        const params={
-            PageSize: 100000,
-            CurrentIndex: 1,
-            EdbInfoId:e.EdbInfoId
-        }
-        const res=params.EdbInfoCategoryType===1?await apiDataPredictEDB.edbDataList(params):await apiDataEDB.edbDataList(params)
-        if(res.Ret===200){
-            const arr=res.Data.Item.DataList || [];
-            let str = '日期\t 值\n';
-            arr.forEach((item) => (str += `${item.DataTime}\t${item.Value}\n`));
-            copyText(str,undefined,(error,event)=>{
-                if(error){
-                    showToast('复制失败')
-                    console.log('复制数据失败',error);
-                    throw new Error(error)
-                }else{
-                    showToast('复制成功')
-                }
-            })
-        }
+    function copyData(data){
+        let str = '日期\t 值\n';
+        data.forEach((item) => (str += `${item.DataTime}\t${item.Value}\n`));
+        copyText(str,undefined,(error,event)=>{
+            if(error){
+                showToast('复制失败')
+                console.log('复制数据失败',error);
+                throw new Error('复制数据失败'+JSON.stringify(error))
+            }else{
+                showToast('复制成功')
+            }
+        })
+        
     }
 
     return {

+ 5 - 0
src/main.js

@@ -12,6 +12,10 @@ import {setupStore} from '@/store'
 import VConsole from 'vconsole';
 import vue3TreeOrg from 'vue3-tree-org';
 import "vue3-tree-org/lib/vue3-tree-org.css";
+// svg图标组件
+import svgIcon from "@/components/SvgIcon.vue";
+//引入注册脚本
+import 'virtual:svg-icons-register'
 
 
 if(import.meta.env.MODE==='test'){
@@ -26,6 +30,7 @@ reportErr(app)//设置全局错误上报
 registerVant(app)
 RegisterDirective(app)
 setupStore(app)
+app.component('svg-icon', svgIcon)
 app.use(vue3TreeOrg)
 app.use(router)
 app.mount('#app')

+ 32 - 0
src/router/dataEDB.js

@@ -56,4 +56,36 @@ export const dataEDBRoutes=[
             title: "添加指标"
         },
     },
+    {
+        path:"/dataEDB/calculate/index",
+        name:"DataEDBCalculateIndex",
+        component: () => import("@/views/dataEDB/calculate/Index.vue"),
+        meta: { 
+            title: "计算指标"
+        },
+    },
+    {
+        path:"/dataEDB/calculate/detail",
+        name:"DataEDBCalculateDetail",
+        component: () => import("@/views/dataEDB/calculate/Detail.vue"),
+        meta: { 
+            title: "指标运算"
+        },
+    },
+    {
+        path:"/dataEDB/calculate/batchDetail",
+        name:"DataEDBCalculateBatchDetail",
+        component: () => import("@/views/dataEDB/calculate/BatchDetail.vue"),
+        meta: { 
+            title: "指标批量运算"
+        },
+    },
+    {
+        path:"/dataEDB/replaceEDB",
+        name:"DataEDBReplaceEDB",
+        component: () => import("@/views/dataEDB/ReplaceEDB.vue"),
+        meta: { 
+            title: "替换指标"
+        },
+    },
 ]

+ 3 - 2
src/views/dataEDB/AddBaseEDB.vue

@@ -174,8 +174,8 @@ function handleClearPage(){
         </template>
 
         <div class="btns-wrap">
-            <van-button block type="primary" v-if="result?.Status===2" :disabled="!result?.SearchItem.DataList?.length" @click="handleShowEditBaseEDB">下一步</van-button>
-            <van-button block type="primary" v-else-if="[1,3].includes(result?.Status)" @click="handleClearPage">知道了</van-button>
+            <van-button block type="primary" style="max-width:300px;margin: 0 auto" v-if="result?.Status===2" :disabled="!result?.SearchItem.DataList?.length" @click="handleShowEditBaseEDB">下一步</van-button>
+            <van-button block type="primary" style="max-width:300px;margin: 0 auto" v-else-if="[1,3].includes(result?.Status)" @click="handleClearPage">知道了</van-button>
         </div>
     </div>
 
@@ -296,6 +296,7 @@ function handleClearPage(){
     padding: 20PX;
     z-index: 99;
     background-color: #fff;
+    text-align: center;
 }
 
 @media screen and (min-width:$media-width){

+ 19 - 16
src/views/dataEDB/Detail.vue

@@ -13,7 +13,9 @@ import {showLoadingToast,showToast} from 'vant'
 import {useCopyEdbData} from '@/hooks/edb/useCopyEdbData'
 import {useEDBDelete} from './hooks/useEDBDelete'
 import {useCachedViewsStore} from '@/store/modules/cachedViews'
+import { useWindowSize } from '@vueuse/core'
 
+const { width } = useWindowSize()
 const cachedViewsStore=useCachedViewsStore()
 const {edbClassifyDelete} =useEDBDelete()
 const {copyData} =useCopyEdbData()
@@ -36,28 +38,20 @@ async function getLangType(){
 }
 getLangType()
 
-// 获取指标详情
+// 指标详情
 const edbInfo=ref(null)
-// async function getEDBInfo(){
-//     const res=route.query.edbType==1
-//         ? await apiDataEDB.getBaseEdbInfo({EdbInfoId:Number(route.query.edbInfoId)})
-//         : await apiDataEDB.getCalculateEdbInfo({EdbInfoId:Number(route.query.edbInfoId)})
-//     if(res.Ret===200){
-//         edbInfo.value=res.Data
-//     }
-// }
-// getEDBInfo()
-
+const edbDataAll=ref([])//指标的所有数据
 //获取指标的按钮操作权限和详情
 //由于指标详情接口中没有按钮权限 只能获取一下指标数据接口了
 async function getEDBOptAuth(){
     const res=await apiDataEDB.edbDataList({
         EdbInfoId:route.query.edbInfoId,
-        PageSize: 1,
+        PageSize: 100000,
         CurrentIndex: 1,
     })
     if(res.Ret===200){
         edbInfo.value=res.Data.Item
+        edbDataAll.value=res.Data.Item.DataList || []
     }
 }
 getEDBOptAuth()
@@ -69,8 +63,10 @@ function handleShowLimit(){
 }
 
 //点击复制
+// 提前获取指标的所有数据用于复制(在getEDBOptAuth方法中一起获取)
+// ios端无法在异步方法后设置剪切板内容
 function handleCopyData(){
-    copyData({EdbInfoId:edbInfo.value.EdbInfoId})
+    copyData(edbDataAll.value)
 }
 
 //点击刷新
@@ -101,7 +97,15 @@ function handleEdit(){
     }
     // 计算指标
     if(data.EdbType === 2 && ![27,40,58,59].includes(data.Source)){
-        showToast('待开发,计算指标请在pc端操作')
+        router.push({
+            path:'/dataEDB/calculate/detail',
+            query:{
+                source:data.Source,
+                name:data.SourceName,
+                edbInfoId:data.EdbInfoId,
+                type:'edit'
+            }
+        })
     }
     //代码运算
     if(data.Source===27){
@@ -175,7 +179,6 @@ function handleSave(){
     chartDetailIns?.value.handleSaveChartLimit()
 }
 
-
 </script>
 
 <template>
@@ -183,7 +186,7 @@ function handleSave(){
         <div class="top-box">
             <div class="van-multi-ellipsis--l2 edb-name-box">{{langType==='zh'?edbInfo.EdbName:edbInfo.EdbNameEn||edbInfo.EdbName}}</div>
         </div>
-        <van-tabs v-model:active="activeType" sticky line-width="16" title-active-color="#0052D9" title-inactive-color="#333">
+        <van-tabs v-model:active="activeType" :offset-top="width>650?60:0" sticky line-width="16" title-active-color="#0052D9" title-inactive-color="#333">
             <van-tab title="图表详情" name="chart">
                 <EDBChartDetailVue
                     ref="chartDetailIns" 

+ 32 - 6
src/views/dataEDB/Index.vue

@@ -3,7 +3,7 @@ import {reactive,ref, watch} from 'vue'
 import apiDataEDB from '@/api/dataEDB'
 import {apiCommonSetSysConfig,apiGetLanguageConfig} from '@/api/common'
 import EDBClassify from './components/EDBClassify.vue'
-import { showSuccessToast, showToast } from 'vant'
+import { showDialog, showSuccessToast, showToast } from 'vant'
 import {useRouter} from 'vue-router'
 import {useCachedViewsStore} from '@/store/modules/cachedViews'
 import langIconZH from '@/assets/imgs/chartETA/lang-icon.png'
@@ -78,9 +78,20 @@ function refreshEBDList(){
 
 //基础操作
 const showActionPop=ref(false)
-function handleSelectActionOpt(e){
-    console.log(e);
+async function handleSelectActionOpt(e){
     if(e.path){
+        // 替换指标要做校验
+        if(e.name==='替换指标'){
+            const res=await apiDataEDB.replaceEDBCheck()
+            if(res.Ret!==200) return
+            if(res.Data!==2){
+                showDialog({
+                    title:'提示',
+                    message: '当前正在替换中,请耐心等待',
+                })
+                return
+            }
+        }
         router.push(e.path)
         return
     }
@@ -158,6 +169,19 @@ function handleEDBOpt(type,data){
         })
     }
 
+    //查看计算指标
+    if(type==='see'){
+        router.push({
+            path:'/dataEDB/calculate/detail',
+            query:{
+                source:data.Source,
+                name:data.SourceName,
+                edbInfoId:data.EdbInfoId,
+                type:'preview'
+            }
+        })
+    }
+
     edbOptState.show=false
 }
 
@@ -297,10 +321,12 @@ async function goSearch(){
                 path:'/dataEDB/addbaseEDB'
             },
             {
-                name:'计算指标'
+                name:'计算指标',
+                path:'/dataEDB/calculate/index'
             },
             {
-                name:'替换指标'
+                name:'替换指标',
+                path:'/dataEDB/replaceEDB'
             }
         ]"
         @select="handleSelectActionOpt"
@@ -320,7 +346,7 @@ async function goSearch(){
         <template #default>
             <ul class="edb-opt-list">
                 <!-- 计算指标查看 -->
-                <!-- <li class="item" v-if="seeComputeEDBInfo(edbOptState.data)">查看</li> -->
+                <li class="item" v-if="seeComputeEDBInfo(edbOptState.data)" @click="handleEDBOpt('see',edbOptState.data)">查看</li>
                 <li class="item" v-if="edbOptState.data?.Button.ShowEdbRelation" @click="handleEDBOpt('relationEDB',edbOptState.data)">关联指标</li>
                 <li class="item" v-if="edbOptState.data?.Button.ShowChartRelation" @click="handleEDBOpt('relationChart',edbOptState.data)">关联图表</li>
                 <li class="item" v-if="edbOptState.data?.Button.MoveButton" @click="handleEDBOpt('move',edbOptState.data)">移动至</li>

+ 1 - 1
src/views/dataEDB/RelationEDB.vue

@@ -48,7 +48,7 @@ function onLoad(){
 
 //复制数据
 function handleCopyEDBData(e){
-    copyData({EdbInfoId:e.EdbInfoId})
+    copyData(e.DataList||[])
 }
 
 // 查看数据

+ 143 - 0
src/views/dataEDB/ReplaceEDB.vue

@@ -0,0 +1,143 @@
+<script setup name="DataEDBReplaceEDB">
+import {ref} from 'vue'
+import apiDataEDB from '@/api/dataEDB'
+import SelectEDB from './calculate/components/SelectEDB.vue'
+import { useRouter } from 'vue-router'
+import { showDialog, showToast } from 'vant'
+
+const router=useRouter()
+
+const beforeEDB=ref(null)
+const afterEDB=ref(null)
+const showSelectEDB=ref(false)
+let selectType='before'
+function handleShowSelectEDB(type){
+    selectType=type
+    showSelectEDB.value=true
+}
+function handleConfirmSelectEDB(e){
+    if(selectType==='before'){
+        beforeEDB.value=e
+    }else{
+        afterEDB.value=e
+    }
+}
+
+const saveBtnLoading=ref(false)
+function handleSave(){
+    if(!beforeEDB.value){
+        showToast('原指标不能为空')
+        return
+    }
+    if(!afterEDB.value){
+        showToast('替换指标不能为空')
+        return
+    }
+    showDialog({
+        title:'提示',
+        message:'本次替换将持续较长时间,是否确认替换?',
+        showCancelButton:true
+    }).then(()=>{
+        apiDataEDB.replaceEDB({
+            OldEdbInfoId:beforeEDB.value.EdbInfoId,
+            NewEdbInfoId:afterEDB.value.EdbInfoId,
+        }).then(res=>{
+            if(res.Ret===200){
+                setTimeout(() => {
+                    router.back()
+                }, 500);
+            }
+        })
+    })
+
+}
+
+</script>
+
+<template>
+    <div class="replace-edb-page">
+        <van-field 
+            :modelValue="beforeEDB?.EdbName"
+            readonly
+            label="原指标" 
+            placeholder="请选择指标"
+            input-align="right"
+            right-icon="arrow"
+            required
+            @click-input="handleShowSelectEDB('before')"
+        />
+        <van-field 
+            :modelValue="afterEDB?.EdbName"
+            readonly
+            label="替换为" 
+            placeholder="请选择指标"
+            input-align="right"
+            right-icon="arrow"
+            required
+            @click-input="handleShowSelectEDB('after')"
+        />
+        <p class="tips">提示:替换后,图表和计算指标引用到原指标的会全部由替换指标替代。</p>
+
+        <div class="opt-btns">
+            <van-button class="primary2" @click="$router.back()">取消</van-button>
+            <van-button 
+                type="primary" 
+                :loading="saveBtnLoading"
+                loading-text="替换中..."
+                @click="handleSave"
+            >全部替换</van-button>
+        </div>
+    </div>
+
+    <!-- 选择指标 -->
+    <SelectEDB v-model:show="showSelectEDB" @select="handleConfirmSelectEDB"/>
+</template>
+
+<style lang="scss" scoped>
+.replace-edb-page{
+    min-height: 90vh;
+    background-color: $page-bg-grey;
+    padding-bottom: 210px ;
+    .tips{
+        color: $font-grey;
+        line-height: 1.7;
+        padding: var(--van-cell-horizontal-padding);
+    }
+}
+.opt-btns{
+    position: fixed;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    background-color: #fff;
+    z-index: 99;
+    padding: 48px;
+    display: flex;
+    justify-content: space-between;
+    .van-button{
+        width: 48%;
+        max-width: 300PX;
+    }
+}
+
+@media screen and (min-width:$media-width){
+    .replace-edb-page{
+        padding-bottom: 105px;
+    }
+    .formula-intro-btn{
+        width: 90px;
+        height: 30px;
+        gap: 2px;
+        border-radius: 16px;
+        .icon{
+            width: 12px;
+            height: 12px;
+        }
+    }
+    .opt-btns{
+        padding: 24px;
+        justify-content: center;
+        gap: 10px;
+    }
+}
+</style>

+ 4 - 2
src/views/dataEDB/SearchList.vue

@@ -17,13 +17,13 @@ const listState = reactive({
 })
 async function getList(){
     listState.loading=true
-    const res=await apiDataEDB.edbChartSearchList({
+    const res=await apiDataEDB.edbSearchList({
         CurrentIndex:listState.page,
         PageSize:listState.pageSize,
         KeyWord:keyword.value
     })
+    listState.loading=false
     if(res.Ret===200){
-        listState.loading=false
         if(!res.Data){
             listState.finished=true
             return
@@ -72,6 +72,7 @@ function goDetail(item){
             />
         </div>
         <img v-if="listState.list.length==0&&listState.finished&&keyword" class="list-empty-img" src="https://hzstatic.hzinsights.com/static/ETA_mobile/empty_img.png" alt="">
+        <template v-if="keyword">
         <van-list
             v-model:loading="listState.loading"
             :finished="listState.finished"
@@ -93,6 +94,7 @@ function goDetail(item){
                 </li>
             </ul>
         </van-list>
+        </template>
     </div>
 </template>
 

+ 782 - 0
src/views/dataEDB/calculate/BatchDetail.vue

@@ -0,0 +1,782 @@
+<script setup>
+import {ref,reactive,watch, computed} from 'vue'
+import { useRoute, useRouter } from "vue-router";
+import SelectEDBClassify from '../components/SelectEDBClassify.vue'
+import SelectEDBUnit from '../components/SelectEDBUnit.vue'
+import SelectEDBFrequency from '../components/SelectEDBFrequency.vue'
+import SeeEDBDataList from './components/SeeEDBDataList.vue'
+import SelectEDB from './components/SelectEDB.vue'
+import EDBHistory from '@/views/dataEDB/components/EDBHistory.vue'
+import {calculateTypeTipsMap} from '../util/config'
+import { showToast,showDialog  } from 'vant';
+import apiDataEDB from '@/api/dataEDB'
+
+const route=useRoute()
+const router=useRouter()
+
+const letterOpts = [];//字母数据
+function initLetterOpt(){
+    for(let i=0;i<26;i++){
+        letterOpts.push(String.fromCharCode(65+i));
+    }
+}
+initLetterOpt()
+
+//公式说明
+const showTips=ref(false)
+const tipsContent=ref(calculateTypeTipsMap.get(['toMonthSeason','accumulate'].includes(route.query.source)? route.query.source: Number(route.query.source))||'')
+
+//提交计算按钮文字
+function getCalculateBtnText(){
+    let str='生成计算指标'
+    const btnTextMap=new Map([
+		[5,'转月值计算'],
+		[6,'同比值计算'],
+		[7,'同差值计算'],
+		[8,'移动平均计算'],
+		[12,'环比值计算'],
+		[13,'环差值计算'],
+		[14,'升频计算'],
+		[61,'转季值计算'],
+		[62,'累计值计算'],
+		[63,'年初至今计算'],
+        [72,'指数修匀计算'],
+	])
+    str=btnTextMap.get(source.value)
+
+    return str
+}
+
+const source=ref(0)//计算类型
+const tabsArr=ref([])
+// tab切换
+function handleTabChange(){
+    edbList.value=[
+        {
+            tag:letterOpts[0],
+            edbData:null,
+            name:'',
+            unit:'',
+            classify:'',
+            classifyName:'',
+            frequency:'',
+            numberN:1,
+            alphaVal:'',
+        },
+        {
+            tag:letterOpts[1],
+            edbData:null,
+            name:'',
+            unit:'',
+            classify:'',
+            classifyName:'',
+            frequency:'',
+            numberN:1,
+            alphaVal:'',
+        }
+    ]
+}
+// 初始化
+function init(){
+    // 累计值转月/季值
+    if(route.query.source==='toMonthSeason'){
+        tabsArr.value=[{ label: '累计值转月值',key: 5 },{ label: '累计值转季值',key: 61 }]
+        source.value=5
+    }else if(route.query.source==='accumulate'){//累计值
+        tabsArr.value=[{ label: '累计值',key: 62 },{ label: '年初至今累计值',key: 63 }]
+        source.value=62
+    }else{
+        tabsArr.value=[]
+        source.value=Number(route.query.source)
+    }
+
+}
+init()
+
+// 指标列表
+const activeTag=ref(letterOpts[0])
+const edbList=ref([
+    {
+        tag:letterOpts[0],
+        edbData:null,
+        name:'',
+        unit:'',
+        classify:'',
+        classifyName:'',
+        frequency:'',
+        numberN:1,
+        alphaVal:'',
+    },
+    {
+        tag:letterOpts[1],
+        edbData:null,
+        name:'',
+        unit:'',
+        classify:'',
+        classifyName:'',
+        frequency:'',
+        numberN:1,
+        alphaVal:'',
+    }
+])
+function handleAddEdbList(){
+    if(edbList.value.length>=26){
+        showToast('添加指标个数已达上限')
+        return
+    }
+    let tag = edbList.value[edbList.value.length-1].tag;
+	let index = letterOpts.findIndex(item => item === tag);
+	const item = {
+		tag: letterOpts[index+1],
+        edbData:null,
+        name:'',
+        unit:'',
+        classify:'',
+        classifyName:'',
+        frequency:'',
+        numberN:1,
+        alphaVal:'',
+	};
+	edbList.value.push(item);
+}
+function handleDeleteEDBItem(index){
+    if(index===currentEdbInfoIndex.value){
+        activeTag.value=edbList.value[0].tag
+    }
+    edbList.value.splice(index, 1)
+
+}
+// 当前操作指标的下标
+const currentEdbInfoIndex=computed(()=>{
+    return edbList.value.findIndex(item=>item.tag===activeTag.value)
+})
+
+// 选择指标
+const showSelectEDB=ref(false)
+function handleConfirmSelectEDB(e){
+    edbList.value[currentEdbInfoIndex.value].edbData=e
+    const obj=setBaseInfo(e,edbList.value[currentEdbInfoIndex.value])
+    edbList.value[currentEdbInfoIndex.value].name=obj.targetName
+    edbList.value[currentEdbInfoIndex.value].unit=obj.unit
+    edbList.value[currentEdbInfoIndex.value].frequency=obj.frequency
+
+    if([72].includes(source.value)){//指数修匀设置默认指标目录为选择的指标目录
+        selectEDBClassifyINS.value?.getSelectClassifyOpt(e.ClassifyId)//获取选择的分类目录
+    }
+}
+
+// 选择指标后设置基础信息
+function setBaseInfo(obj,list){
+    switch(source.value){
+        case 6:
+        case 32:
+            return {
+                targetName: `${obj.EdbName}同比`,
+				unit: '无',
+				frequency: obj.Frequency
+            }
+        case 7: 
+		case 33: 
+			return {
+				targetName: `${obj.EdbName}同差`,
+				unit: obj.Unit,
+				frequency: obj.Frequency
+			}
+		case 8: 
+		case 39: 
+			return {
+				targetName: obj.EdbName,
+				unit: obj.Unit,
+				frequency: obj.Frequency
+			}
+		case 14:
+		case 45: 
+            return {
+				targetName: obj.EdbName,
+				unit: obj.Unit,
+				frequency: '日度'
+			}
+		case 12:
+		case 43: 
+			return{
+				targetName: `${obj.EdbName}${list.n_num}${obj.Frequency.slice(0,1)}环比`,
+				unit: '无',
+				frequency: obj.Frequency
+			}
+		case 13: 
+		case 44: 
+			return {
+				targetName: `${obj.EdbName}${list.n_num}${obj.Frequency.slice(0,1)}环差`,
+				unit: '无',
+				frequency: obj.Frequency
+			}
+		case 5:
+		case 42: 
+			return {
+				targetName: obj.EdbName,
+				unit: obj.Unit,
+				frequency: obj.Frequency
+			}
+		case 61:
+		case 64: 
+			return {
+			    targetName: obj.EdbName,
+				unit: '无',
+				frequency: '季度'
+			}
+		case 62:
+		case 65: 
+			return {
+				targetName: obj.EdbName,
+				unit: '无',
+				frequency: obj.Frequency
+			}
+		case 63:
+		case 66: 
+			return {
+				targetName: obj.EdbName,
+				unit: '无',
+				frequency: obj.Frequency
+			}
+        case 72:
+            return {
+                targetName:`${obj.EdbName}指数修匀`,
+                unit: obj.Unit,
+				frequency: obj.Frequency
+            }
+    }
+}
+
+//查看指标数据详情
+const showSeeEDBDataList=ref(false)
+
+const edbNameInputFocus=ref(false)//指标名称输入框聚焦
+const numberNInputFocus=ref(false)//N值输入框聚焦
+const alphaValInputFocus=ref(false)//alpha值输入框聚焦
+
+// 选择单位
+const showSelectUnit=ref(false)
+function onConfirmSelectUnit(value){
+    edbList.value[currentEdbInfoIndex.value].unit=value
+}
+
+//选择分类
+const showSelectClassify=ref(false)
+const selectEDBClassifyINS=ref(null)
+function handleConfirmClassify({value,selectedOptions}){
+    if(selectedOptions.length===0){
+        edbList.value[currentEdbInfoIndex.value].classify=''
+        edbList.value[currentEdbInfoIndex.value].classifyName=''
+        return
+    }
+    edbList.value[currentEdbInfoIndex.value].classify=value
+    edbList.value[currentEdbInfoIndex.value].classifyName=`${selectedOptions[0].ClassifyName}/${selectedOptions[1].ClassifyName}/${selectedOptions[2].ClassifyName}`
+}
+
+//选择频度
+const showSelectFrequency=ref(false)
+function handleConfirmFrequency(value){
+    edbList.value[currentEdbInfoIndex.value].frequency=value
+}
+
+
+// 提交计算
+const saveBtnLoading=ref(false)
+async function handleSave(){
+    const filterArr = edbList.value.filter(item=> item.edbData);
+    if(!filterArr.length){
+        showToast('请选择指标')
+        return
+    }
+    const isEnough=filterArr.every(item => item.name&&item.unit&&item.classify&&item.frequency)
+    console.log(isEnough);
+    if(!isEnough){
+        showToast('请填写完整信息')
+        return
+    }
+    const params=filterArr.map(item => {
+        return {
+            CalculateId: item.tag,
+            CalculateInfo: {
+                ClassifyId: item.classify,
+                EdbName: item.name,
+                Formula: [72].includes(source.value)?String(item.alphaVal) : String(item.numberN),
+                Frequency:item.frequency,
+                FromEdbInfoId: item.edbData.EdbInfoId,
+                MoveFrequency: "天",
+                MoveType: 1,
+                Source: source.value,
+                Unit: item.unit
+            }
+        }
+    })
+    console.log(params);
+    // 判断alpha的值
+    if([72].includes(source.value)){
+        let msg=[]
+        params.forEach(item=>{
+            if(item.CalculateInfo.Formula<=0||item.CalculateInfo.Formula>=1){
+                msg.push(item.CalculateId)
+            }
+
+        })
+        if(msg.length){
+            showDialog({
+                allowHtml:true,
+                message:'指标'+msg.join('、')+'的alpha值不合法'
+            })
+            return
+        }
+    }
+
+    saveBtnLoading.value=true
+    const res=await apiDataEDB.batchAddCalculateEDB(params)
+    saveBtnLoading.value=false
+    if(res.Ret===200){
+        const { Fail,Success } = res.Data;
+        if(Fail.length){
+            let message=''
+            Fail.forEach(item=>{
+                message+=`${item.CalculateId}:${item.Msg}\n`
+            })
+            showDialog({
+                allowHtml:true,
+                message
+            }).then(()=>{})
+            // 处理失败的
+            const failTagArr = Fail.map(_ =>_.CalculateId);
+            const arr=edbList.value.filter(_ => failTagArr.includes(_.tag))
+            console.log(arr);
+            edbList.value=edbList.value.filter(_ => failTagArr.includes(_.tag))
+            activeTag.value=edbList.value[0].tag
+        }else{
+            showToast('添加成功')
+            setTimeout(() => {
+                router.replace({
+                    path:'/dataEDB/detail',
+                    query:{
+                        edbInfoId:Success[0].EdbInfoId
+                    }
+                })
+            }, 1500);
+        }
+    }
+}
+
+// 显示指标溯源
+const showEDBHistory=ref(false)
+const edbHistoryId=ref(0)
+function handleShowEDBHistory(item){
+    //计算指标打开弹窗,基础指标打开新页面
+    if(item.EdbType===2){
+        edbHistoryId.value=item.EdbInfoId
+        showEDBHistory.value=true
+    }else{
+        const routerEl=router.resolve({
+            path:'/dataEDB/detail',
+            query:{
+                edbInfoId:item.EdbInfoId
+            }
+        })
+        window.open(routerEl.href,'_blank')
+    }
+}
+
+
+</script>
+
+<template>
+    <div class="batch-calculate-wrap">
+        <div class="sticky-top-wrap">
+        <van-tabs 
+            v-model:active="source" 
+            border 
+            title-active-color="#0052D9" 
+            title-inactive-color="#333" 
+            line-width="16px"
+            @change="handleTabChange"
+            v-if="tabsArr.length"
+        >
+            <van-tab 
+                :title="tab.label" 
+                :name="tab.key" 
+                v-for="tab in tabsArr" 
+                :key="tab.key"
+            />
+        </van-tabs>
+        <section class="section edblist-tag-list">
+            <ul class="tag-list">
+                <li
+                    :class="['item-tag',activeTag===item.tag?'item-tag_active':'']"
+                    v-for="(item,index) in edbList" 
+                    :key="item.tag"
+                    @click="activeTag=item.tag"
+                >
+                    <span>{{item.tag}}</span>
+                    <svg-icon name="close" @click.stop="handleDeleteEDBItem(index)" v-if="index>1"></svg-icon>
+                </li>
+            </ul>
+            <van-button type="primary" size="small" @click="handleAddEdbList">添加指标</van-button>
+        </section>
+        </div>
+        <section class="section form-wrap">
+            <van-field
+                label="选择指标"
+                right-icon="arrow"
+                @click-input="showSelectEDB=true"
+            >
+                <template #left-icon>
+                    <div class="left-icon">
+                        <svg-icon name="edb-history-tag" size="24px" v-if="edbList[currentEdbInfoIndex].edbData" @click="handleShowEDBHistory(edbList[currentEdbInfoIndex].edbData)"/>
+                    </div>
+                </template>
+                <template #input>
+                    <div class="select-edbinfo-box">
+                        <div class="edb-info" v-if="edbList[currentEdbInfoIndex].edbData">
+                            <span class="name">{{edbList[currentEdbInfoIndex].edbData.EdbName}}</span>
+                            <span class="time">{{edbList[currentEdbInfoIndex].edbData.StartDate}}至{{edbList[currentEdbInfoIndex].edbData.EndDate}}</span>
+                        </div>
+                        <span class="placeholder" v-else>请选择指标</span>
+                    </div>
+                </template>
+            </van-field>
+            <div class="edbinfo-box">
+                <div class="van-cell__title">已选指标</div>
+                <div v-if="!edbList[currentEdbInfoIndex].edbData">
+                    <img class="list-empty-img" src="https://hzstatic.hzinsights.com/static/ETA_mobile/empty_img.png" alt="">
+                    <p style="text-align:center;color:#999">暂无指标</p>
+                </div>
+                <div class="info-box" v-else>
+                    <h2 class="name">{{edbList[currentEdbInfoIndex].edbData?.EdbName}}</h2>
+                    <ul class="info-list">
+                        <li class="info-item">ID:{{edbList[currentEdbInfoIndex].edbData.EdbCode}}</li>
+                        <li class="info-item">起始时间:{{edbList[currentEdbInfoIndex].edbData.StartDate}}</li>
+                        <li class="info-item">频度:{{edbList[currentEdbInfoIndex].edbData.Frequency}}</li>
+                        <li class="info-item">更新时间:{{edbList[currentEdbInfoIndex].edbData.ModifyTime}}</li>
+                        <li class="info-item">单位:{{edbList[currentEdbInfoIndex].edbData.Unit}}</li>
+                        <li class="info-item">数据来源:{{edbList[currentEdbInfoIndex].edbData.SourceName}}</li>
+                    </ul>
+                    <div style="text-align:right">
+                        <van-button color="#0052D9" size="small" @click="showSeeEDBDataList=true">查看数据</van-button>
+                    </div>
+                </div>
+            </div>
+            <div>
+                <van-field 
+                    v-model="edbList[currentEdbInfoIndex].name" 
+                    label="指标名称" 
+                    placeholder="指标名称"
+                    input-align="right"
+                    required
+                    @focus="edbNameInputFocus=true"
+                    @blur="edbNameInputFocus=false"
+                >
+                    <template #right-icon>
+                        <svg-icon class="edit-icon" name="edit" :color="edbNameInputFocus?'#0052D9':'#333333'"/>
+                    </template>
+                </van-field>
+                <van-field 
+                    :modelValue="edbList[currentEdbInfoIndex].unit"
+                    readonly
+                    label="单位" 
+                    placeholder="请选择单位"
+                    input-align="right"
+                    right-icon="arrow"
+                    required
+                    @click-input="showSelectUnit=true"
+                    :disabled="[6,7,32,33].includes(source)"
+                />
+                <van-field 
+                    :modelValue="edbList[currentEdbInfoIndex].classifyName"
+                    readonly
+                    label="指标目录" 
+                    placeholder="请选择指标目录"
+                    input-align="right"
+                    right-icon="arrow"
+                    required
+                    @click-input="showSelectClassify=true"
+                />
+                <van-field 
+                    :modelValue="edbList[currentEdbInfoIndex].frequency"
+                    readonly
+                    label="频度" 
+                    placeholder="请选择指标频度"
+                    input-align="right"
+                    right-icon="arrow"
+                    required
+                    @click-input="showSelectFrequency=true"
+                    :disabled="[6,7,32,33,5,42,61,64,63,66].includes(source)"
+                />
+                <van-field
+                    v-if="[8,12,13,39,43,44].includes(source)"
+                    v-model.number="edbList[currentEdbInfoIndex].numberN" 
+                    label="N等于" 
+                    placeholder="请输入N数值"
+                    input-align="right"
+                    required
+                    @focus="numberNInputFocus=true"
+                    @blur="numberNInputFocus=false"
+                >
+                    <template #right-icon>
+                        <svg-icon class="edit-icon" name="edit" :color="numberNInputFocus?'#0052D9':'#333333'"/>
+                    </template>
+                </van-field>
+                <!-- 指数修匀 alpha 值 -->
+                <van-field
+                    v-if="[72].includes(source)"
+                    v-model.number="edbList[currentEdbInfoIndex].alphaVal"
+                    type="number"
+                    label="alpha值" 
+                    placeholder="请输入alpha值"
+                    input-align="right"
+                    required
+                    @focus="alphaValInputFocus=true"
+                    @blur="alphaValInputFocus=false"
+                    :disabled="isPreview"
+                >
+                    <template #right-icon>
+                        <svg-icon v-if="!isPreview" class="edit-icon" name="edit" :color="alphaValInputFocus?'#0052D9':'#333333'"/>
+                    </template>
+                </van-field>
+            </div>
+        </section>
+
+        <div class="formula-intro-btn" @click="showTips=true">
+            <svg-icon class="icon" name="warning"></svg-icon>
+            <span>公式说明</span>
+        </div>
+        <div class="opt-btns">
+            <van-button class="primary2" @click="$router.back()">取消</van-button>
+            <van-button 
+                type="primary" 
+                :loading="saveBtnLoading"
+                loading-text="计算中..."
+                @click="handleSave"
+            >{{getCalculateBtnText()}}</van-button>
+        </div>
+    </div>
+
+    <!-- 选择指标 -->
+    <SelectEDB v-model:show="showSelectEDB" @select="handleConfirmSelectEDB"/>
+
+    <!-- 选择单位 -->
+    <SelectEDBUnit v-model:show="showSelectUnit" @select="onConfirmSelectUnit"/>
+
+    <!-- 选择分类 -->
+    <SelectEDBClassify ref="selectEDBClassifyINS" :defaultId="edbList[currentEdbInfoIndex]?.classify" v-model:show="showSelectClassify" @select="handleConfirmClassify" />
+
+    <!-- 选择频度 -->
+    <SelectEDBFrequency v-model:show="showSelectFrequency" @select="handleConfirmFrequency"/>
+
+    <!-- 查看指标数据 -->
+    <SeeEDBDataList v-model:show="showSeeEDBDataList" :edbInfoId="edbList[currentEdbInfoIndex].edbData?.EdbInfoId" />
+
+    <!-- 指标溯源 -->
+    <EDBHistory v-model:show="showEDBHistory" :edbInfoId="edbHistoryId"/>
+
+    <!-- 公式说明 -->
+    <van-dialog 
+        v-model:show="showTips" 
+        :title="$route.query.name"
+        confirmButtonText='知道啦'
+    >
+        <div class="edb-formula-tips-html-box" v-html="tipsContent"></div>
+    </van-dialog>
+
+</template>
+
+<style lang="scss" scoped>
+.batch-calculate-wrap{
+    min-height: 90vh;
+    background-color: $page-bg-grey;
+    padding-bottom: 210px ;
+    .sticky-top-wrap{
+        position: sticky;
+        top: 0;
+        z-index: 10;
+    }
+}
+.section{
+    background-color: #fff;
+    margin-bottom: 32px;
+}
+.edblist-tag-list{
+    padding: 16px 32px;
+    display: flex;
+    .tag-list{
+        margin-right: 20px;
+        flex: 1;
+        overflow-x: auto;
+        display: flex;
+        gap: 0 16px;
+        &::-webkit-scrollbar{
+            width: 0;
+            height: 0;
+        }
+        .item-tag{
+            display: flex;
+            gap: 12px;
+            align-items: center;
+            box-sizing: border-box;
+            padding: 6px 32px;
+            border-radius: 1998px;
+            background-color: #F3F3F3;
+            span{
+                line-height: 1;
+            }
+        }
+        .item-tag_active{
+            background-color: #F2F3FF;
+            color: $theme-color;
+        }
+    }
+}
+.form-wrap{
+    // .left-icon{
+    //     width: 48px;
+    //     height: 48px;
+    // }
+    :deep(.van-cell__right-icon){
+        align-self: center;
+        color: #333;
+    }
+    .select-edbinfo-box{
+        width: 100%;
+        text-align: right;
+        .placeholder{
+            color: var(--van-text-color-3);
+        }
+        .edb-info{
+            display: flex;
+            flex-direction: column;
+        }
+        .time{
+            color: $font-grey_999;
+            font-size: 24px;
+        }
+    }
+    .edbinfo-box{
+        padding: var(--van-cell-horizontal-padding);
+        .info-box{
+            margin-top: 14px;
+            border-radius: 8px;
+            border: 1px solid $border-color;
+            box-shadow: $box-shadow;
+            padding: 20px;
+            .name{
+                font-size: 32px;
+                margin: 0 0 10px 0;
+            }
+            .info-list{
+                display: flex;
+                flex-wrap: wrap;
+                gap: 10px 0;
+                color: $font-grey;
+                margin-bottom: 10px;
+                .info-item{
+                    width: 100%;
+                    margin-bottom: 10px;
+                }
+            }
+        }
+    }
+}
+
+.formula-intro-btn{
+    width: 180px;
+    height: 60px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: 5px;
+    color: $theme-color;
+    line-height: 1;
+    background-color: #fff;
+    border-radius: 32px;
+    margin-left: auto;
+    margin-right: var(--van-cell-horizontal-padding);
+    .icon{
+        width: 24px;
+        height: 24px;
+    }
+}
+.opt-btns{
+    position: fixed;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    background-color: #fff;
+    z-index: 99;
+    padding: 48px;
+    display: flex;
+    justify-content: space-between;
+    .van-button{
+        width: 48%;
+        max-width: 300PX;
+    }
+}
+@media screen and (min-width:$media-width){
+    .batch-calculate-wrap{
+        padding-bottom: 105px;
+    }
+    .section{
+        margin-bottom: 16px;
+    }
+    .edblist-tag-list{
+        padding: 8px 16px;
+        .tag-list{
+            margin-right: 10px;
+            gap: 0 8px;
+            .item-tag{
+                gap: 6px;
+                padding: 3px 16px;
+            }
+        }
+    }
+
+    .form-wrap{
+        // .left-icon{
+        //     width: 24px;
+        //     height: 24px;
+        // }
+        .select-edbinfo-box{
+            .time{
+                font-size: 12px;
+            }
+        }
+        .edbinfo-box{
+            .info-box{
+                margin-top: 7px;
+                border-radius: 4px;
+                padding: 10px;
+                .name{
+                    font-size: 16px;
+                    margin: 0 0 5px 0;
+                }
+                .info-list{
+                    gap: 5px 0;
+                    margin-bottom: 5px;
+                    .info-item{
+                        margin-bottom: 5px;
+                    }
+                }
+            }
+        }
+    }
+    .formula-intro-btn{
+        width: 90px;
+        height: 30px;
+        gap: 2px;
+        border-radius: 16px;
+        .icon{
+            width: 12px;
+            height: 12px;
+        }
+    }
+    .opt-btns{
+        padding: 24px;
+        justify-content: center;
+        gap: 10px;
+    }
+}
+</style>

+ 41 - 0
src/views/dataEDB/calculate/Detail.vue

@@ -0,0 +1,41 @@
+<script setup name="DataEDBCalculateDetail">
+import {ref} from 'vue'
+import apiDataEDB from '@/api/dataEDB'
+import { useRoute } from "vue-router";
+import DiffusionIndexCalcualate from './components/DiffusionIndexCalcualate.vue';
+import FittingResidualsCalculate from './components/FittingResidualsCalculate.vue';
+import FormulaCalculate from './components/FormulaCalculate.vue';
+import JointCalculate from './components/JointCalculate.vue';
+import OtherCalculate from './components/OtherCalculate.vue';
+
+const route=useRoute()
+const source=ref(route.query.source||'')//计算类型
+document.title=route.query.name||'指标运算'
+
+// 获取计算指标详情
+const edbInfo=ref(null)
+async function getCalculateInfo(){
+    const res=await apiDataEDB.getCalculateEdbInfo({EdbInfoId:Number(route.query.edbInfoId)})
+    if(res.Ret===200){
+        edbInfo.value=res.Data
+    }
+}
+if(['edit','preview'].includes(route.query.type)){
+    getCalculateInfo()
+}
+
+ 
+</script>
+
+<template>
+    <!-- 指标运算 -->
+    <FormulaCalculate v-if="['4'].includes(source)" :edbInfo="edbInfo"/>
+    <!-- 其他的运算 编辑时toMonthSeason=5\61 accumulate=62\63 -->
+    <OtherCalculate v-if="['toMonthSeason','5','6','7','8','12','13','14','22','35','51','52','61','62','63','accumulate','72'].includes(source)" :edbInfo="edbInfo"/>
+    <!-- 拼接计算 编辑时则为 23\24 -->
+    <JointCalculate v-if="['joint','23','24'].includes(source)" :edbInfo="edbInfo"/>
+    <!-- 拟合残差计算 -->
+    <FittingResidualsCalculate v-if="source==='37'" :edbInfo="edbInfo"/>
+    <!-- 扩散指数计算 -->
+    <DiffusionIndexCalcualate v-if="source==='53'" :edbInfo="edbInfo"/>
+</template>

+ 154 - 0
src/views/dataEDB/calculate/Index.vue

@@ -0,0 +1,154 @@
+<script setup name="DataEDBCalculateIndex">
+import {ref} from 'vue'
+import { Dialog } from 'vant';
+import { useRouter } from 'vue-router';
+import {calculateTypeTipsMap,calculateType,calculateBatchType} from '../util/config'
+import { useWindowSize } from '@vueuse/core'
+
+
+const { width } = useWindowSize()
+
+const router=useRouter()
+
+const showTips=ref(false)
+const tipsTitle=ref('')
+const tipsContent=ref('')
+function handleShowTips(item){
+    tipsTitle.value=`【${item.name}】公式说明`
+    tipsContent.value=calculateTypeTipsMap.get(item.type)
+    showTips.value=true
+}
+
+function handleGoDetail(item){
+    router.push({
+        path:'/dataEDB/calculate/detail',
+        query:{
+            source:item.type,
+            name:item.name
+        }
+    })
+}
+
+function handleGoBatchDetail(item){
+    router.push({
+        path:'/dataEDB/calculate/batchDetail',
+        query:{
+            source:item.type,
+            name:item.name
+        }
+    })
+}
+
+</script>
+
+<template>
+    <div class="calculate-index-page">
+        <van-tabs sticky :offset-top="width>650?60:0" swipeable border title-active-color="#0052D9" title-inactive-color="#333" line-width="16px">
+            <van-tab title="常规计算">
+                <ul class="type-list-wrap">
+                    <li 
+                        class="type-item" 
+                        v-for="item in calculateType" 
+                        :key="item.type"
+                        @click="handleGoDetail(item)"
+                    >
+                        <div class="tips-box" @click.stop="handleShowTips(item)"> 
+                            <svg xmlns="http://www.w3.org/2000/svg" width="20" height="21" viewBox="0 0 20 21" fill="none">
+                                <path d="M18.75 10.5C18.75 15.3325 14.8325 19.25 10 19.25C5.16751 19.25 1.25 15.3325 1.25 10.5C1.25 5.66751 5.16751 1.75 10 1.75C14.8325 1.75 18.75 5.66751 18.75 10.5ZM10.625 5.50038H9.375V12.375H10.625V5.50038ZM9.24286 14.25V15.75H10.7429V14.25H9.24286Z" fill="#0052D9"/>
+                            </svg>
+                            <span>公式说明</span>
+                        </div>
+                        <h3 class="name">{{item.name}}</h3>
+                    </li>
+                </ul>
+            </van-tab>
+            <van-tab title="批量计算">
+                <ul class="type-list-wrap">
+                    <li 
+                        class="type-item" 
+                        v-for="item in calculateBatchType" 
+                        :key="item.type"
+                        @click="handleGoBatchDetail(item)"
+                    >
+                        <div class="tips-box" @click.stop="handleShowTips(item)"> 
+                            <svg xmlns="http://www.w3.org/2000/svg" width="20" height="21" viewBox="0 0 20 21" fill="none">
+                                <path d="M18.75 10.5C18.75 15.3325 14.8325 19.25 10 19.25C5.16751 19.25 1.25 15.3325 1.25 10.5C1.25 5.66751 5.16751 1.75 10 1.75C14.8325 1.75 18.75 5.66751 18.75 10.5ZM10.625 5.50038H9.375V12.375H10.625V5.50038ZM9.24286 14.25V15.75H10.7429V14.25H9.24286Z" fill="#0052D9"/>
+                            </svg>
+                            <span>公式说明</span>
+                        </div>
+                        <h3 class="name">{{item.name}}</h3>
+                    </li>
+                </ul>
+            </van-tab>
+        </van-tabs>
+    </div>
+    <!-- 公式说明 -->
+    <van-dialog 
+        v-model:show="showTips" 
+        :title="tipsTitle"
+        confirmButtonText='知道啦'
+    >
+        <div class="edb-formula-tips-html-box" v-html="tipsContent"></div>
+    </van-dialog>
+</template>
+
+<style lang="scss" scoped>
+.type-list-wrap{
+    padding: $page-padding;
+    display: flex;
+    flex-wrap: wrap;
+    gap: 20px;
+    .type-item{
+        padding: 30px 20px;
+        border-radius: 16px;
+        background: #F8F9FF;
+        width: 331px;
+        .tips-box{
+            width: 140px;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            background-color: #fff;
+            height: 31px;
+            border-radius: 42px;
+            font-size: 22px;
+            color: $theme-color;
+            line-height: 1;
+            svg{
+                width: 20px;
+                height: 20px;
+            }
+        }
+        .name{
+            margin-top: 9px;
+            margin-bottom: 0;
+            font-size: 28px;
+        }
+    }
+}
+@media screen and (min-width:$media-width){
+    .type-list-wrap{
+        padding: $page-padding;
+        gap: 10px;
+        .type-item{
+            padding: 15px 10px;
+            border-radius: 8px;
+            width: 166px;
+            .tips-box{
+                width: 70px;
+                height: 15px;
+                border-radius: 21px;
+                font-size: 11px;
+                svg{
+                    width: 10px;
+                    height: 10px;
+                }
+            }
+            .name{
+                margin-top: 5px;
+                font-size: 14px;
+            }
+        }
+    }
+}
+</style>

+ 571 - 0
src/views/dataEDB/calculate/components/DiffusionIndexCalcualate.vue

@@ -0,0 +1,571 @@
+<script setup>
+import apiDataEDB from '@/api/dataEDB'
+import { showToast } from 'vant';
+import {computed, nextTick, reactive, ref, watch} from 'vue'
+import SelectEDB from './SelectEDB.vue'
+import SelectEDBClassify from '../../components/SelectEDBClassify.vue'
+import SelectEDBUnit from '../../components/SelectEDBUnit.vue'
+import SelectEDBFrequency from '../../components/SelectEDBFrequency.vue'
+import EDBHistory from '@/views/dataEDB/components/EDBHistory.vue'
+import {calculateTypeTipsMap} from '../../util/config'
+import { useRoute, useRouter } from 'vue-router';
+import moment from 'moment';
+
+const props=defineProps({
+    edbInfo:{
+        type:Object,
+        default:null
+    }
+})
+
+watch(
+    ()=>props.edbInfo,
+    ()=>{
+        if(['edit','preview'].includes(route.query.type)){
+            edbList.value=props.edbInfo.CalculateList.map(item=>{
+                return {
+                    tag:item.FromTag,
+                    target:item.FromEdbInfoId,
+                    startDate:item.StartDate,
+                    endDate:item.EndDate,
+                    name:item.FromEdbName
+                }
+            })
+            const obj=JSON.parse(props.edbInfo.EdbInfoDetail.CalculateFormula)
+            dateContactState.type=obj.DateType//会触发radio的change事件 所有下面用个nextTick
+            nextTick(()=>{
+                dateContactState.list=obj.CheckList
+            })
+            baseInfo.name=props.edbInfo.EdbInfoDetail.EdbName
+            baseInfo.unit=props.edbInfo.EdbInfoDetail.Unit
+            baseInfo.classify=props.edbInfo.EdbInfoDetail.ClassifyId
+            baseInfo.frequency=props.edbInfo.EdbInfoDetail.Frequency
+            setTimeout(() => {
+                selectEDBClassifyINS.value?.getSelectClassifyOpt(props.edbInfo.EdbInfoDetail.ClassifyId)//获取选择的分类目录
+            }, 1000);
+        }
+    }
+)
+
+const route=useRoute()
+const router=useRouter()
+
+// 预览页面
+const isPreview=ref(route.query.type==='preview'||false)
+
+const source=ref(Number(route.query.source)||0)//计算类型
+
+//公式说明
+const showTips=ref(false)
+const tipsContent=ref(calculateTypeTipsMap.get(Number(route.query.source))||'')
+
+
+const letterOpts = [];//字母数据
+function initLetterOpt(){
+    for(let i=0;i<26;i++){
+        letterOpts.push(String.fromCharCode(65+i));
+    }
+}
+initLetterOpt()
+
+//选择的指标集合
+const edbList=ref([
+    {
+        tag:letterOpts[0],
+        target:'',
+        startDate:'',
+        endDate:'',
+        name:'',
+        EdbType:0
+    },
+    {
+        tag:letterOpts[1],
+        target:'',
+        startDate:'',
+        endDate:'',
+        name:'',
+        EdbType:0
+    }
+])
+function handleAddEdbList(){
+    if(edbList.value.length>=26){
+        showToast('添加指标个数已达上限')
+        return
+    }
+    let tag = edbList.value[edbList.value.length-1].tag;
+	let index = letterOpts.findIndex(item => item === tag);
+	const item = {
+		tag: letterOpts[index+1],
+		target: '',
+		start_date: '',
+		end_date: '',
+        name:'',
+        EdbType:0
+	};
+	edbList.value.push(item);
+}
+function handleDeleteEDBItem(index){
+    // 检测dateContactState.list是否有改项有的话删除掉
+    const tag=edbList.value[index].tag
+    const flagIndex=dateContactState.list.indexOf(tag)
+    if(flagIndex!=-1){
+        dateContactState.list.splice(flagIndex,1)
+    }
+
+
+    edbList.value.splice(index, 1)
+}
+// 选择指标
+const showSelectEDB=ref(false)
+let whichIndex=0
+function handleShowSelectEDB(index){
+    if(isPreview.value) return
+    whichIndex=index
+    showSelectEDB.value=true
+}
+function handleConfirmSelectEDB(e){
+    edbList.value[whichIndex].target=e.EdbInfoId
+    edbList.value[whichIndex].startDate=e.StartDate
+    edbList.value[whichIndex].endDate=e.EndDate
+    edbList.value[whichIndex].name=e.EdbName
+    edbList.value[whichIndex].EdbType=e.EdbType
+}
+
+// 日期合并类型
+const dateContactState=reactive({
+    type:1,
+    list:[],//部分日期并集选择的指标
+})
+// 显示日期
+const dateRange=computed(()=>{
+    const arr=edbList.value.filter(_=>dateContactState.list.includes(_.tag)&&_.target)
+                     .map(_=>[new Date(_.startDate),new Date(_.endDate)]).flat(Infinity)
+    const start=arr.length&&moment(Math.min(...arr)).format('YYYY-MM-DD')
+    const end=arr.length&&moment(Math.max(...arr)).format('YYYY-MM-DD')
+    if(start&&end) return start+'至'+end
+
+    return ''
+})
+
+
+// 基础信息
+const edbNameInputFocus=ref(false)
+const baseInfo=reactive({
+    name:'',
+    unit:'',
+    classify:'',
+    frequency:''
+})
+
+// 选择单位
+const showSelectUnit=ref(false)
+function onConfirmSelectUnit(value){
+    baseInfo.unit=value
+}
+
+//选择分类
+const showSelectClassify=ref(false)
+const classifyStr=ref('')
+const selectEDBClassifyINS=ref(null)
+function handleConfirmClassify({value,selectedOptions}){
+    if(selectedOptions.length===0){
+        baseInfo.classify=''
+        classifyStr.value=''
+        return
+    }
+    baseInfo.classify=value
+    classifyStr.value=`${selectedOptions[0].ClassifyName}/${selectedOptions[1].ClassifyName}/${selectedOptions[2].ClassifyName}`
+}
+
+//选择频度
+const showSelectFrequency=ref(false)
+function handleConfirmFrequency(value){
+    baseInfo.frequency=value
+}
+
+// 提交计算
+const saveBtnLoading=ref(false)
+async function handleSave(){
+    const arr=edbList.value.filter(item=>item.target).map(item=>{
+        return {
+            EdbInfoId: item.target,
+            FromTag: item.tag,
+        }
+    })
+    if(!arr.length){
+        showToast('请选择指标')
+        return
+    }
+    if(!baseInfo.name){
+        showToast('指标名称不能为空')
+        return
+    }
+    if(!baseInfo.unit){
+        showToast('指标单位不能为空')
+        return
+    }
+    if(!baseInfo.classify){
+        showToast('指标目录不能为空')
+        return
+    }
+    if(!baseInfo.frequency){
+        showToast('指标频度不能为空')
+        return
+    }
+    const params={
+        Source: source.value,
+		EdbName: baseInfo.name,
+		Unit: baseInfo.unit,
+		ClassifyId: baseInfo.classify,
+		Frequency: baseInfo.frequency,
+		Formula: JSON.stringify({DateType:dateContactState.type,CheckList:dateContactState.list}),
+        EdbInfoIdArr:arr
+    }
+    saveBtnLoading.value=true
+    const res=route.query.type==='edit'?await apiDataEDB.editCalculateEDB({...params,EdbInfoId:Number(route.query.edbInfoId)}) : await apiDataEDB.addCalculateEDB(params)
+    saveBtnLoading.value=false
+    if(res.Ret===200){
+        showToast(res.Msg)
+        setTimeout(() => {
+            if(route.query.type==='edit'){
+                router.back()
+            }else{
+                router.replace({
+                    path:'/dataEDB/detail',
+                    query:{
+                        edbInfoId:res.Data.EdbInfoId
+                    }
+                })
+            }
+        }, 1500);
+    }
+}
+
+// 显示指标溯源
+const showEDBHistory=ref(false)
+const edbHistoryId=ref(0)
+function handleShowEDBHistory(item){
+    //计算指标打开弹窗,基础指标打开新页面
+    if(item.EdbType===2){
+        edbHistoryId.value=item.target
+        showEDBHistory.value=true
+    }else{
+        const routerEl=router.resolve({
+            path:'/dataEDB/detail',
+            query:{
+                edbInfoId:item.target
+            }
+        })
+        window.open(routerEl.href,'_blank')
+    }
+}
+
+
+</script>
+
+<template>
+    <div class="diffusionIndex-wrap">
+        <section class="section select-edb-box">
+            <van-swipe-cell  v-for="(item,index) in edbList" :key="item.tag" :disabled="index<2||isPreview">
+                <van-field 
+                    :label="item.tag"
+                    :right-icon="!isPreview?'arrow':''"
+                    @click-input="handleShowSelectEDB(index)"
+                    :disabled="isPreview"
+                >
+                    <template #left-icon>
+                        <div class="left-icon">
+                            <svg-icon name="edb-history-tag" size="24px" v-if="item.target" @click="handleShowEDBHistory(item)"/>
+                        </div>
+                    </template>
+                    <template #input>
+                        <div class="edb-info-box">
+                            <div class="edb-info" v-if="item.target">
+                                <span class="name">{{item.name}}</span>
+                                <span class="time">{{item.startDate}}至{{item.endDate}}</span>
+                            </div>
+                            <span class="placeholder" v-else>请选择指标</span>
+                        </div>
+                    </template>
+                </van-field>
+                <template #right>
+                    <van-button square type="danger" text="删除" @click="handleDeleteEDBItem(index)"/>
+                </template>
+            </van-swipe-cell>
+            <div class="van-cell add-edb-box" @click="handleAddEdbList" v-if="!isPreview">
+                <img src="@/assets/imgs/icon01.png" alt="">
+                <span>添加更多参数</span>
+            </div>
+            <van-field label="指标扩散日期">
+                <template #input>
+                    <div class="contact-date-box">
+                        <van-radio-group :disabled="isPreview" v-model="dateContactState.type" shape="dot" @change="dateContactState.list=[]">
+                            <van-radio :name="1">全部日期并集</van-radio>
+                            <van-radio :name="2">部分日期并集</van-radio>
+                        </van-radio-group>
+                        <van-checkbox-group 
+                            v-model="dateContactState.list"
+                            direction="horizontal"
+                            shape="square" 
+                            v-if="dateContactState.type===2"
+                            :disabled="isPreview"
+                        >
+                            <van-checkbox
+                                v-for="item in edbList"
+                                :key="item.tag"
+                                :name="item.tag"
+                            >{{item.tag}}</van-checkbox>
+                        </van-checkbox-group>
+                        <div class="time" v-if="dateContactState.type===2">{{dateRange}}</div>
+                    </div>
+                </template>
+            </van-field>
+        </section>
+
+        <section class="section baseinfo-box">
+            <van-field 
+                v-model="baseInfo.name" 
+                label="指标名称" 
+                placeholder="指标名称"
+                input-align="right"
+                required
+                @focus="edbNameInputFocus=true"
+                @blur="edbNameInputFocus=false"
+                :disabled="isPreview"
+            >
+                <template #right-icon>
+                    <svg-icon v-if="!isPreview" class="edit-icon" name="edit" :color="edbNameInputFocus?'#0052D9':'#333333'"/>
+                </template>
+            </van-field>
+            <van-field 
+                :modelValue="baseInfo.unit"
+                readonly
+                label="单位" 
+                placeholder="请选择单位"
+                input-align="right"
+                :right-icon="!isPreview?'arrow':''"
+                required
+                @click-input="showSelectUnit=true"
+                :disabled="isPreview"
+            />
+            <van-field 
+                :modelValue="classifyStr"
+                readonly
+                label="指标目录" 
+                placeholder="请选择指标目录"
+                input-align="right"
+                :right-icon="!isPreview?'arrow':''"
+                required
+                @click-input="showSelectClassify=true"
+                :disabled="isPreview"
+            />
+            <van-field 
+                :modelValue="baseInfo.frequency"
+                readonly
+                label="频度" 
+                placeholder="请选择指标频度"
+                input-align="right"
+                :right-icon="!isPreview?'arrow':''"
+                required
+                @click-input="showSelectFrequency=true"
+                :disabled="isPreview"
+            />
+        </section>
+
+        <div class="formula-intro-btn" @click="showTips=true">
+            <svg-icon class="icon" name="warning"></svg-icon>
+            <span>公式说明</span>
+        </div>
+
+        <div class="opt-btns" v-if="!isPreview">
+            <van-button class="primary2" @click="$router.back()">取消</van-button>
+            <van-button 
+                type="primary" 
+                :loading="saveBtnLoading"
+                loading-text="计算中..."
+                @click="handleSave"
+            >扩散指数计算</van-button>
+        </div>
+    </div>
+
+    <!-- 选择指标 -->
+    <SelectEDB v-model:show="showSelectEDB" @select="handleConfirmSelectEDB"/>
+
+    <!-- 选择单位 -->
+    <SelectEDBUnit v-model:show="showSelectUnit" @select="onConfirmSelectUnit"/>
+
+    <!-- 选择分类 -->
+    <SelectEDBClassify ref="selectEDBClassifyINS" :defaultId="baseInfo.classify" v-model:show="showSelectClassify" @select="handleConfirmClassify" />
+
+    <!-- 选择频度 -->
+    <SelectEDBFrequency v-model:show="showSelectFrequency" @select="handleConfirmFrequency"/>
+
+    <!-- 指标溯源 -->
+    <EDBHistory v-model:show="showEDBHistory" :edbInfoId="edbHistoryId"/>
+
+    <!-- 公式说明 -->
+    <van-dialog 
+        v-model:show="showTips" 
+        :title="$route.query.name"
+        confirmButtonText='知道啦'
+    >
+        <div class="edb-formula-tips-html-box" v-html="tipsContent"></div>
+    </van-dialog>
+
+</template>
+
+<style lang="scss" scoped>
+.diffusionIndex-wrap{
+    min-height: 90vh;
+    background-color: $page-bg-grey;
+    padding-bottom: 210px ;
+}
+
+.section{
+    background-color: #fff;
+    margin-bottom: 32px;
+}
+
+.select-edb-box{
+    .left-icon{
+        width: 48px;
+        height: 48px;
+    }
+    :deep(.van-cell__right-icon){
+        align-self: center;
+        color: #333;
+    }
+    .edb-info-box{
+        width: 100%;
+        text-align: right;
+        .placeholder{
+            color: var(--van-text-color-3);
+        }
+        .edb-info{
+            display: flex;
+            flex-direction: column;
+        }
+        .time{
+            color: $font-grey_999;
+            font-size: 24px;
+        }
+    }
+    .add-edb-box{
+        display: flex;
+        justify-content: flex-end;
+        align-items: center;
+        color: $theme-color;
+        font-size: 32px;
+        padding: 32px var(--van-cell-horizontal-padding);
+        img{
+            width: 48px;
+            height: 48px;
+        }
+    }
+
+}
+
+.contact-date-box{
+    width: 100%;
+    .van-radio{
+        justify-content: flex-end;
+        margin-bottom: 10px;
+    }
+    .van-checkbox-group--horizontal{
+        justify-content: flex-end;
+        gap: 8px;
+    }
+    .time{
+        color: $theme-color;
+        text-align: right;
+        margin-top: 10px;
+    }
+}
+
+.formula-intro-btn{
+    width: 180px;
+    height: 60px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: 5px;
+    color: $theme-color;
+    line-height: 1;
+    background-color: #fff;
+    border-radius: 32px;
+    margin-left: auto;
+    margin-right: var(--van-cell-horizontal-padding);
+    .icon{
+        width: 24px;
+        height: 24px;
+    }
+}
+.opt-btns{
+    position: fixed;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    background-color: #fff;
+    z-index: 99;
+    padding: 48px;
+    display: flex;
+    justify-content: space-between;
+    .van-button{
+        width: 48%;
+        max-width: 300PX;
+    }
+}
+@media screen and (min-width:$media-width){
+    .diffusionIndex-wrap{
+        padding-bottom: 105px;
+    }
+    .section{
+        margin-bottom: 16px;
+    }
+    .select-edb-box{
+        .left-icon{
+            width: 24px;
+            height: 24px;
+        }
+        .edb-info-box{
+            .time{
+                font-size: 12px;
+            }
+        }
+        .add-edb-box{
+            font-size: 16px;
+            padding: 16px var(--van-cell-horizontal-padding);
+            img{
+                width: 24px;
+                height: 24px;
+            }
+        }
+    }
+    .contact-date-box{
+        .van-radio{
+            margin-bottom: 5px;
+        }
+        .van-checkbox-group--horizontal{
+            gap: 4px;
+        }
+        .time{
+            margin-top: 5px;
+        }
+    }
+    .formula-intro-btn{
+        width: 90px;
+        height: 30px;
+        gap: 2px;
+        border-radius: 16px;
+        .icon{
+            width: 12px;
+            height: 12px;
+        }
+    }
+    .opt-btns{
+        padding: 24px;
+        justify-content: center;
+        gap: 10px;
+    }
+}
+</style>

+ 568 - 0
src/views/dataEDB/calculate/components/FittingResidualsCalculate.vue

@@ -0,0 +1,568 @@
+<script setup>
+import apiDataEDB from '@/api/dataEDB'
+import {calculateTypeTipsMap} from '../../util/config'
+import {ref,reactive,watch} from 'vue'
+import SelectEDB from './SelectEDB.vue'
+import SelectEDBClassify from '../../components/SelectEDBClassify.vue'
+import SelectEDBUnit from '../../components/SelectEDBUnit.vue'
+import SelectEDBFrequency from '../../components/SelectEDBFrequency.vue'
+import SelectDateRange from '@/components/SelectDateRange.vue'
+import EDBHistory from '@/views/dataEDB/components/EDBHistory.vue'
+import { showToast } from 'vant'
+import { useRoute, useRouter } from 'vue-router'
+import moment from 'moment'
+
+const route=useRoute()
+const router=useRouter()
+
+const props=defineProps({
+    edbInfo:{
+        type:Object,
+        default:null
+    }
+})
+
+watch(
+    ()=>props.edbInfo,
+    ()=>{
+        if(['edit','preview'].includes(route.query.type)){
+            props.edbInfo.CalculateList.forEach(item=>{
+                if(item.FromTag==='A'){
+                    independentEDBInfo.value={
+                        EdbInfoId:item.FromEdbInfoId,
+                        EdbName:item.FromEdbName,
+                        StartDate:item.StartDate,
+                        EndDate:item.EndDate
+                    }
+                    selfMoveType.value=item.MoveValue===0?0:1
+                    selfMoveVal.value=item.MoveValue
+                }
+                if(item.FromTag==='B'){
+                    dependentEDBInfo.value={
+                        EdbInfoId:item.FromEdbInfoId,
+                        EdbName:item.FromEdbName,
+                        StartDate:item.StartDate,
+                        EndDate:item.EndDate
+                    }
+                }
+            })
+
+            fittingDate.value=props.edbInfo.EdbInfoDetail.CalculateFormula.split(',')
+
+            baseInfo.name=props.edbInfo.EdbInfoDetail.EdbName
+            baseInfo.unit=props.edbInfo.EdbInfoDetail.Unit
+            baseInfo.classify=props.edbInfo.EdbInfoDetail.ClassifyId
+            baseInfo.frequency=props.edbInfo.EdbInfoDetail.Frequency
+
+            setTimeout(() => {
+                selectEDBClassifyINS.value?.getSelectClassifyOpt(props.edbInfo.EdbInfoDetail.ClassifyId)//获取选择的分类目录
+            }, 1000);
+
+            getEDBCorrelationIndex()
+        }
+    }
+)
+
+// 预览页面
+const isPreview=ref(route.query.type==='preview'||false)
+
+const source=ref(Number(route.query.source)||0)//计算类型
+
+//公式说明
+const showTips=ref(false)
+const tipsContent=ref(calculateTypeTipsMap.get(['toMonthSeason','accumulate'].includes(route.query.source)? route.query.source: Number(route.query.source))||'')
+
+// 获取两个指标的相关系数
+const correlationIndex=ref('')
+async function getEDBCorrelationIndex(){
+    if(!independentEDBInfo.value||!dependentEDBInfo.value||fittingDate.value.length===0) return correlationIndex.value=''
+    if(selfMoveType.value===1&&!selfMoveVal.value) return correlationIndex.value=''
+    const params={
+        Formula:fittingDate.value.join(','),
+        EdbInfoIdArr:[
+            {
+                EdbInfoId:independentEDBInfo.value.EdbInfoId,
+                FromTag: 'A',
+                MoveValue:selfMoveType.value===0?0:Number(selfMoveVal.value)
+            },
+            {
+                EdbInfoId:dependentEDBInfo.value.EdbInfoId,
+                FromTag: 'B',
+                MoveValue:0
+            }
+        ]
+    }
+    const res=await apiDataEDB.edbCorrelationIndex(params)
+    if(res.Ret===200){
+        correlationIndex.value=res.Data
+    }
+}
+
+// 选择指标
+const showSelectEDB=ref(false)
+const independentEDBInfo=ref(null)//自变量指标
+const dependentEDBInfo=ref(null)//因变量指标
+let currentSelectEDBType=''
+function handleShowSelectEDB(type){
+    if(isPreview.value) return
+    currentSelectEDBType=type
+    showSelectEDB.value=true
+}
+function handleConfirmSelectEDB(e){
+    if(currentSelectEDBType==='independent'){
+        independentEDBInfo.value=e
+    }else{
+        dependentEDBInfo.value=e
+    }
+    if(independentEDBInfo.value&&dependentEDBInfo.value){
+        baseInfo.name=`${dependentEDBInfo.value.EdbName}拟合残差/${independentEDBInfo.value.EdbName}`
+    }
+    getEDBCorrelationIndex()
+}
+
+// 类型
+const selfMoveType=ref(0)
+const selfMoveVal=ref('')
+function handleSelfMoveTypeChange(){
+    selfMoveVal.value=''
+    getEDBCorrelationIndex()
+}
+
+//拟合时间段
+const showFittingDate=ref(false)
+const fittingDate=ref([])
+function handleConfirmFittingDate(e){
+    // 日期间隔不得少于两天
+    const diff=moment(e[1]).diff(e[0],'days')
+    if(diff<2){
+        showToast('日期间隔不得少于两天')
+        return
+    }
+    fittingDate.value=e
+    getEDBCorrelationIndex()
+}
+
+// 基础信息
+const edbNameInputFocus=ref(false)
+const baseInfo=reactive({
+    name:'',
+    unit:'',
+    classify:'',
+    frequency:''
+})
+
+// 选择单位
+const showSelectUnit=ref(false)
+function onConfirmSelectUnit(value){
+    baseInfo.unit=value
+}
+
+//选择分类
+const showSelectClassify=ref(false)
+const classifyStr=ref('')
+const selectEDBClassifyINS=ref(null)
+function handleConfirmClassify({value,selectedOptions}){
+    if(selectedOptions.length===0){
+        baseInfo.classify=''
+        classifyStr.value=''
+        return
+    }
+    baseInfo.classify=value
+    classifyStr.value=`${selectedOptions[0].ClassifyName}/${selectedOptions[1].ClassifyName}/${selectedOptions[2].ClassifyName}`
+}
+
+//选择频度
+const showSelectFrequency=ref(false)
+function handleConfirmFrequency(value){
+    baseInfo.frequency=value
+}
+
+// 提交计算
+const saveBtnLoading=ref(false)
+async function handleSave(){
+    if(!independentEDBInfo.value){
+        showToast('自变量不能为空')
+        return
+    }
+    if(!dependentEDBInfo.value){
+        showToast('因变量不能为空')
+        return
+    }
+    if(fittingDate.value.length===0){
+        showToast('拟合时间段不能为空')
+        return
+    }
+    if(!baseInfo.name){
+        showToast('指标名称不能为空')
+        return
+    }
+    if(!baseInfo.unit){
+        showToast('指标单位不能为空')
+        return
+    }
+    if(!baseInfo.classify){
+        showToast('指标目录不能为空')
+        return
+    }
+    if(!baseInfo.frequency){
+        showToast('指标频度不能为空')
+        return
+    }
+    const params={
+        Source: source.value,
+		EdbName: baseInfo.name,
+		Unit: baseInfo.unit,
+		ClassifyId: baseInfo.classify,
+		Frequency: baseInfo.frequency,
+        Formula:fittingDate.value.join(','),
+        EdbInfoIdArr:[
+            {
+                EdbInfoId:independentEDBInfo.value.EdbInfoId,
+                FromTag: 'A',
+                MoveValue:selfMoveType.value===0?0:Number(selfMoveVal.value)
+            },
+            {
+                EdbInfoId:dependentEDBInfo.value.EdbInfoId,
+                FromTag: 'B',
+                MoveValue:0
+            }
+        ]
+    }
+    saveBtnLoading.value=true
+    const res=route.query.type==='edit'?await apiDataEDB.editCalculateEDB({...params,EdbInfoId:Number(route.query.edbInfoId)}) : await apiDataEDB.addCalculateEDB(params)
+    saveBtnLoading.value=false
+    if(res.Ret===200){
+        showToast(res.Msg)
+        setTimeout(() => {
+            if(route.query.type==='edit'){
+                router.back()
+            }else{
+                router.replace({
+                    path:'/dataEDB/detail',
+                    query:{
+                        edbInfoId:res.Data.EdbInfoId
+                    }
+                })
+            }
+        }, 1500);
+    }
+}
+
+//点击选择的指标左侧图标查看指标详情
+const showEDBHistory=ref(false)// 显示指标溯源
+const edbHistoryId=ref(0)
+function handleShowEDBHistory(data){
+    //计算指标打开弹窗,基础指标打开新页面
+    if(data.EdbType===2){
+        edbHistoryId.value=data.EdbInfoId
+        showEDBHistory.value=true
+    }else{
+        const routerEl=router.resolve({
+            path:'/dataEDB/detail',
+            query:{
+                edbInfoId:data.EdbInfoId
+            }
+        })
+        window.open(routerEl.href,'_blank')
+    }
+}
+
+</script>
+
+<template>
+    <div class="fitting-residuals-wrap">
+        <section class="section select-edb-box">
+            <van-field 
+                label="自变量"
+                required
+                :right-icon="!isPreview?'arrow':''"
+                @click-input="handleShowSelectEDB('independent')"
+                :disabled="isPreview"
+            >
+                <template #left-icon>
+                    <div class="left-icon" v-if="independentEDBInfo" @click="handleShowEDBHistory(independentEDBInfo)">
+                        <svg-icon name="edb-history-tag" size="24px"/>
+                    </div>
+                </template>
+                <template #input>
+                    <div class="edb-info-box">
+                        <div class="edb-info" v-if="independentEDBInfo">
+                            <span class="name">{{independentEDBInfo.EdbName}}</span>
+                            <span class="time">{{independentEDBInfo.StartDate}}至{{independentEDBInfo.EndDate}}</span>
+                        </div>
+                        <span class="placeholder" v-else>请选择指标</span>
+                    </div>
+                </template>
+            </van-field>
+            <van-cell>
+                <div class="self-move-type-box">
+                    <van-radio-group :disabled="isPreview" v-model="selfMoveType" shape="dot" direction="horizontal" @change="handleSelfMoveTypeChange">
+                        <van-radio :name="0">标准指标</van-radio>
+                        <van-radio :name="1">领先天数</van-radio>
+                    </van-radio-group>
+                    <div class="day-box" v-show="selfMoveType===1">
+                        <input class="input" :disabled="isPreview" type="number" :min="0" v-model="selfMoveVal" @change="getEDBCorrelationIndex()">
+                        <span>天</span>
+                    </div>
+                </div>
+            </van-cell>
+            <van-field 
+                label="因变量"
+                required
+                :right-icon="!isPreview?'arrow':''"
+                @click-input="handleShowSelectEDB('dependent')"
+                :disabled="isPreview"
+            >
+                <template #left-icon>
+                    <div class="left-icon" v-if="dependentEDBInfo" @click="handleShowEDBHistory(dependentEDBInfo)">
+                        <svg-icon name="edb-history-tag" size="24px"/>
+                    </div>
+                </template>
+                <template #input>
+                    <div class="edb-info-box">
+                        <div class="edb-info" v-if="dependentEDBInfo">
+                            <span class="name">{{dependentEDBInfo.EdbName}}</span>
+                            <span class="time">{{dependentEDBInfo.StartDate}}至{{dependentEDBInfo.EndDate}}</span>
+                        </div>
+                        <span class="placeholder" v-else>请选择指标</span>
+                    </div>
+                </template>
+            </van-field>
+            <van-field
+                label="拟合时间段"
+                :right-icon="!isPreview?'arrow':''"
+                required
+                @click-input="()=>{if(isPreview) return false ;showFittingDate=true}"
+                :disabled="isPreview"
+            >
+                <template #input>
+                    <div class="edb-info-box">
+                        <div class="edb-info" v-if="fittingDate.length>0">
+                            <span class="name">{{fittingDate[0]}}~{{fittingDate[1]}}</span>
+                            <span class="time" v-if="correlationIndex">相关系数:{{correlationIndex}}</span>
+                        </div>
+                        <span class="placeholder" v-else>请选择时间段</span>
+                    </div>
+                </template>
+            </van-field>
+        </section>
+        <section class="section baseinfo-box">
+            <van-field 
+                v-model="baseInfo.name" 
+                label="指标名称" 
+                placeholder="指标名称"
+                input-align="right"
+                required
+                @focus="edbNameInputFocus=true"
+                @blur="edbNameInputFocus=false"
+                :disabled="isPreview"
+            >
+                <template #right-icon>
+                    <svg-icon v-if="!isPreview" class="edit-icon" name="edit" :color="edbNameInputFocus?'#0052D9':'#333333'"/>
+                </template>
+            </van-field>
+            <van-field 
+                :modelValue="baseInfo.unit"
+                readonly
+                label="单位" 
+                placeholder="请选择单位"
+                input-align="right"
+                :right-icon="!isPreview?'arrow':''"
+                required
+                @click-input="showSelectUnit=true"
+                :disabled="isPreview"
+            />
+            <van-field 
+                :modelValue="classifyStr"
+                readonly
+                label="指标目录" 
+                placeholder="请选择指标目录"
+                input-align="right"
+                :right-icon="!isPreview?'arrow':''"
+                required
+                @click-input="showSelectClassify=true"
+                :disabled="isPreview"
+            />
+            <van-field 
+                :modelValue="baseInfo.frequency"
+                readonly
+                label="频度" 
+                placeholder="请选择指标频度"
+                input-align="right"
+                :right-icon="!isPreview?'arrow':''"
+                required
+                @click-input="showSelectFrequency=true"
+                :disabled="isPreview"
+            />
+        </section>
+
+        <div class="formula-intro-btn" @click="showTips=true">
+            <svg-icon class="icon" name="warning"></svg-icon>
+            <span>公式说明</span>
+        </div>
+
+        <div class="opt-btns" v-if="!isPreview">
+            <van-button class="primary2" @click="$router.back()">取消</van-button>
+            <van-button 
+                type="primary" 
+                :loading="saveBtnLoading"
+                loading-text="计算中..."
+                @click="handleSave"
+            >拟合残差计算</van-button>
+        </div>
+    </div>
+
+    <!-- 选择指标 -->
+    <SelectEDB v-model:show="showSelectEDB" :params="{FilterSource:1}" @select="handleConfirmSelectEDB"/>
+
+    <!-- 选择时间段 -->
+    <SelectDateRange v-model:show="showFittingDate" @select="handleConfirmFittingDate"/>
+
+    <!-- 选择单位 -->
+    <SelectEDBUnit v-model:show="showSelectUnit" @select="onConfirmSelectUnit"/>
+
+    <!-- 选择分类 -->
+    <SelectEDBClassify ref="selectEDBClassifyINS" :defaultId="baseInfo.classify" v-model:show="showSelectClassify" @select="handleConfirmClassify" />
+
+    <!-- 选择频度 -->
+    <SelectEDBFrequency v-model:show="showSelectFrequency" @select="handleConfirmFrequency"/>
+
+    <!-- 指标溯源 -->
+    <EDBHistory v-model:show="showEDBHistory" :edbInfoId="edbHistoryId"/>
+
+    <!-- 公式说明 -->
+    <van-dialog 
+        v-model:show="showTips" 
+        :title="$route.query.name"
+        confirmButtonText='知道啦'
+    >
+        <div class="edb-formula-tips-html-box" v-html="tipsContent"></div>
+    </van-dialog>
+</template>
+
+<style lang="scss" scoped>
+.fitting-residuals-wrap{
+    min-height: 90vh;
+    background-color: $page-bg-grey;
+    padding-bottom: 210px ;
+}
+.section{
+    background-color: #fff;
+    margin-bottom: 32px;
+}
+.self-move-type-box{
+    padding: 20px 0;
+    display: flex;
+    justify-content: space-between;
+    .day-box{
+        display: flex;
+        align-items: center;
+        gap: 5px;
+        .input{
+            width: 80px;
+            height: 48px;
+            border: 1px solid #DCDCDC;
+            border-radius: 6px;
+            text-align: center;
+            padding: 0 10px;
+            color: #333;
+        }
+    }
+}
+.select-edb-box{
+    :deep(.van-cell__right-icon){
+        align-self: center;
+        color: #333;
+    }
+    .edb-info-box{
+        width: 100%;
+        text-align: right;
+        .placeholder{
+            color: var(--van-text-color-3);
+        }
+        .edb-info{
+            display: flex;
+            flex-direction: column;
+        }
+        .time{
+            color: $font-grey_999;
+            font-size: 24px;
+        }
+    }
+}
+.formula-intro-btn{
+    width: 180px;
+    height: 60px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: 5px;
+    color: $theme-color;
+    line-height: 1;
+    background-color: #fff;
+    border-radius: 32px;
+    margin-left: auto;
+    margin-right: var(--van-cell-horizontal-padding);
+    .icon{
+        width: 24px;
+        height: 24px;
+    }
+}
+.opt-btns{
+    position: fixed;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    background-color: #fff;
+    z-index: 99;
+    padding: 48px;
+    display: flex;
+    justify-content: space-between;
+    .van-button{
+        width: 48%;
+        max-width: 300PX;
+    }
+}
+@media screen and (min-width:$media-width){
+    .fitting-residuals-wrap{
+        padding-bottom: 105px;
+    }
+    .section{
+        margin-bottom: 16px;
+    }
+    .self-move-type-box{
+        padding: 10px 0;
+        .day-box{
+            gap: 5px;
+            .input{
+                width: 40px;
+                height: 24px;
+                border-radius: 3px;
+                padding: 0 5px;
+            }
+        }
+    }
+    .select-edb-box{
+        .edb-info-box{
+            .time{
+                font-size: 12px;
+            }
+        }
+    }
+    .formula-intro-btn{
+        width: 90px;
+        height: 30px;
+        gap: 2px;
+        border-radius: 16px;
+        .icon{
+            width: 12px;
+            height: 12px;
+        }
+    }
+    .opt-btns{
+        padding: 24px;
+        justify-content: center;
+        gap: 10px;
+    }
+}
+</style>

+ 543 - 0
src/views/dataEDB/calculate/components/FormulaCalculate.vue

@@ -0,0 +1,543 @@
+<script setup>
+import apiDataEDB from '@/api/dataEDB'
+import { showToast } from 'vant';
+import {reactive, ref,watch} from 'vue'
+import SelectEDB from './SelectEDB.vue'
+import EDBHistory from '@/views/dataEDB/components/EDBHistory.vue'
+import SelectEDBClassify from '../../components/SelectEDBClassify.vue'
+import SelectEDBUnit from '../../components/SelectEDBUnit.vue'
+import SelectEDBFrequency from '../../components/SelectEDBFrequency.vue'
+import {calculateTypeTipsMap} from '../../util/config'
+import { useRoute, useRouter } from 'vue-router';
+
+const route=useRoute()
+const router=useRouter()
+
+
+const props=defineProps({
+    edbInfo:{
+        type:Object,
+        default:null
+    }
+})
+
+watch(
+    ()=>props.edbInfo,
+    ()=>{
+        if(['edit','preview'].includes(route.query.type)){
+            edbList.value=props.edbInfo.CalculateList.map(item=>{
+                return {
+                    tag:item.FromTag,
+                    target:item.FromEdbInfoId,
+                    startDate:item.StartDate,
+                    endDate:item.EndDate,
+                    name:item.FromEdbName
+                }
+            })
+            formulaVal.value=props.edbInfo.EdbInfoDetail.CalculateFormula
+            baseInfo.name=props.edbInfo.EdbInfoDetail.EdbName
+            baseInfo.unit=props.edbInfo.EdbInfoDetail.Unit
+            baseInfo.classify=props.edbInfo.EdbInfoDetail.ClassifyId
+            baseInfo.frequency=props.edbInfo.EdbInfoDetail.Frequency
+            setTimeout(() => {
+                selectEDBClassifyINS.value?.getSelectClassifyOpt(props.edbInfo.EdbInfoDetail.ClassifyId)//获取选择的分类目录
+            }, 1000);
+        }
+    }
+)
+
+// 预览页面
+const isPreview=ref(route.query.type==='preview'||false)
+
+const letterOpts = [];//字母数据
+function initLetterOpt(){
+    for(let i=0;i<26;i++){
+        letterOpts.push(String.fromCharCode(65+i));
+    }
+}
+initLetterOpt()
+
+//公式说明
+const showTips=ref(false)
+const tipsContent=ref(calculateTypeTipsMap.get(Number(route.query.source))||'')
+
+//选择的指标集合
+const edbList=ref([
+    {
+        tag:letterOpts[0],
+        target:'',
+        startDate:'',
+        endDate:'',
+        name:'',
+        EdbType:0
+    },
+    {
+        tag:letterOpts[1],
+        target:'',
+        startDate:'',
+        endDate:'',
+        name:'',
+        EdbType:0
+    },
+    {
+        tag:letterOpts[2],
+        target:'',
+        startDate:'',
+        endDate:'',
+        name:'',
+        EdbType:0
+    },
+    {
+        tag:letterOpts[3],
+        target:'',
+        startDate:'',
+        endDate:'',
+        name:'',
+        EdbType:0
+    }
+])
+function handleAddEdbList(){
+    if(edbList.value.length>=26){
+        showToast('添加指标个数已达上限')
+        return
+    }
+    let tag = edbList.value[edbList.value.length-1].tag;
+	let index = letterOpts.findIndex(item => item === tag);
+	const item = {
+		tag: letterOpts[index+1],
+		target: '',
+		start_date: '',
+		end_date: '',
+        name:'',
+        EdbType:0
+	};
+	edbList.value.push(item);
+}
+function handleDeleteEDBItem(index){
+    edbList.value.splice(index, 1)
+}
+// 选择指标
+const showSelectEDB=ref(false)
+let whichIndex=0
+function handleShowSelectEDB(index){
+    if(isPreview.value) return
+    whichIndex=index
+    showSelectEDB.value=true
+}
+function handleConfirmSelectEDB(e){
+    edbList.value[whichIndex].target=e.EdbInfoId
+    edbList.value[whichIndex].startDate=e.StartDate
+    edbList.value[whichIndex].endDate=e.EndDate
+    edbList.value[whichIndex].name=e.EdbName
+    edbList.value[whichIndex].EdbType=e.EdbType
+}
+
+// 显示指标溯源
+const showEDBHistory=ref(false)
+const edbHistoryId=ref(0)
+function handleShowEDBHistory(item){
+    //计算指标打开弹窗,基础指标打开新页面
+    if(item.EdbType===2){
+        edbHistoryId.value=item.target
+        showEDBHistory.value=true
+    }else{
+        const routerEl=router.resolve({
+            path:'/dataEDB/detail',
+            query:{
+                edbInfoId:item.target
+            }
+        })
+        window.open(routerEl.href,'_blank')
+    }
+}
+
+// 计算公式
+const formulaVal=ref('')
+
+// 基础信息
+const edbNameInputFocus=ref(false)
+const baseInfo=reactive({
+    name:'',
+    unit:'',
+    classify:'',
+    frequency:''
+})
+
+// 选择单位
+const showSelectUnit=ref(false)
+function onConfirmSelectUnit(value){
+    baseInfo.unit=value
+}
+
+//选择分类
+const showSelectClassify=ref(false)
+const classifyStr=ref('')
+const selectEDBClassifyINS=ref(null)
+function handleConfirmClassify({value,selectedOptions}){
+    if(selectedOptions.length===0){
+        baseInfo.classify=''
+        classifyStr.value=''
+        return
+    }
+    baseInfo.classify=value
+    classifyStr.value=`${selectedOptions[0].ClassifyName}/${selectedOptions[1].ClassifyName}/${selectedOptions[2].ClassifyName}`
+}
+
+//选择频度
+const showSelectFrequency=ref(false)
+function handleConfirmFrequency(value){
+    baseInfo.frequency=value
+}
+
+// 提交计算
+async function handleSave(){
+    if(!formulaVal.value){
+        showToast('计算公式不能为空')
+        return
+    }
+    if(!baseInfo.name){
+        showToast('指标名称不能为空')
+        return
+    }
+    if(!baseInfo.unit){
+        showToast('指标单位不能为空')
+        return
+    }
+    if(!baseInfo.classify){
+        showToast('指标目录不能为空')
+        return
+    }
+    if(!baseInfo.frequency){
+        showToast('指标频度不能为空')
+        return
+    }
+    const arr=edbList.value.filter(item=>item.target).map(item=>{
+        return {
+            EdbInfoId: item.target,
+            FromTag: item.tag,
+        }
+    })
+    const params={
+        CalculateFormula:formulaVal.value,
+        ClassifyId:baseInfo.classify,
+        EdbName:baseInfo.name,
+        Frequency:baseInfo.frequency,
+        Unit:baseInfo.unit,
+        EdbInfoIdArr:arr
+    }
+    const edbInfoId=route.query.edbInfoId
+    const res=edbInfoId?await apiDataEDB.editCalculateFormula({...params,EdbInfoId:Number(edbInfoId)}):await apiDataEDB.addCalculateFormula(params)
+    if(res.Ret===200){
+        showToast(res.Msg)
+        setTimeout(() => {
+            if(edbInfoId){
+                router.back()
+            }else{
+                router.replace({
+                    path:'/dataEDB/detail',
+                    query:{
+                        edbInfoId:res.Data.EdbInfoId
+                    }
+                })
+            }
+        }, 1500);
+    }
+}
+
+</script>
+
+<template>
+    <div class="formula-calculate-wrap">
+        <section class="section select-edb-box">
+            <van-swipe-cell  v-for="(item,index) in edbList" :key="item.tag" :disabled="index<4||isPreview">
+                <van-field 
+                    :label="item.tag"
+                    :right-icon="!isPreview?'arrow':''"
+                    @click-input="handleShowSelectEDB(index)"
+                    :disabled="isPreview"
+                >
+                    <template #left-icon>
+                        <div class="left-icon">
+                            <svg-icon name="edb-history-tag" size="24px" v-if="item.target" @click="handleShowEDBHistory(item)"/>
+                        </div>
+                    </template>
+                    <template #input>
+                        <div class="edb-info-box">
+                            <div class="edb-info" v-if="item.target">
+                                <span class="name">{{item.name}}</span>
+                                <span class="time">{{item.startDate}}至{{item.endDate}}</span>
+                            </div>
+                            <span class="placeholder" v-else>请选择指标</span>
+                        </div>
+                    </template>
+                </van-field>
+                <template #right>
+                    <van-button square type="danger" text="删除" @click="handleDeleteEDBItem(index)"/>
+                </template>
+            </van-swipe-cell>
+            <div class="add-edb-box" @click="handleAddEdbList" v-if="!isPreview">
+                <img src="@/assets/imgs/icon01.png" alt="">
+                <span>添加更多参数</span>
+            </div>
+        </section>
+        <section class="section formula-box">
+            <van-field  
+                label="计算公式"
+                required
+            >
+                <template #input>
+                    <div style="margin-left:auto">
+                    <input class="formula-input" :disabled="isPreview" type="text" placeholder="请输入公式" v-model="formulaVal">
+                    <div class="formula-tips">
+                        <p class="en-text-wrap">公式示例:A*0.5+B*C*1.2+120-MAX(A,B,C)</p>
+                        <p class="en-text-wrap">函数支持:MAX(),MIN(),ln(A),log(a,A)</p>
+                    </div>
+                    </div>
+                </template>
+            </van-field>
+        </section>
+        <section class="section baseinfo-box">
+            <van-field 
+                v-model="baseInfo.name" 
+                label="指标名称" 
+                placeholder="指标名称"
+                input-align="right"
+                required
+                @focus="edbNameInputFocus=true"
+                @blur="edbNameInputFocus=false"
+                :disabled="isPreview"
+            >
+                <template #right-icon>
+                    <svg-icon v-if="!isPreview" class="edit-icon" name="edit" :color="edbNameInputFocus?'#0052D9':'#333333'"/>
+                </template>
+            </van-field>
+            <van-field 
+                :modelValue="baseInfo.unit"
+                readonly
+                label="单位" 
+                placeholder="请选择单位"
+                input-align="right"
+                :right-icon="!isPreview?'arrow':''"
+                required
+                @click-input="showSelectUnit=true"
+                :disabled="isPreview"
+            />
+            <van-field 
+                :modelValue="classifyStr"
+                readonly
+                label="指标目录" 
+                placeholder="请选择指标目录"
+                input-align="right"
+                :right-icon="!isPreview?'arrow':''"
+                required
+                @click-input="showSelectClassify=true"
+                :disabled="isPreview"
+            />
+            <van-field 
+                :modelValue="baseInfo.frequency"
+                readonly
+                label="频度" 
+                placeholder="请选择指标频度"
+                input-align="right"
+                :right-icon="!isPreview?'arrow':''"
+                required
+                @click-input="showSelectFrequency=true"
+                :disabled="isPreview"
+            />
+        </section>
+        <div class="formula-intro-btn" @click="showTips=true">
+            <svg-icon class="icon" name="warning"></svg-icon>
+            <span>公式说明</span>
+        </div>
+        <div class="opt-btns" v-if="!isPreview">
+            <van-button class="primary2" @click="$router.back()">取消</van-button>
+            <van-button type="primary" @click="handleSave">生成计算指标</van-button>
+        </div>
+    </div>
+
+    <!-- 选择指标 -->
+    <SelectEDB v-model:show="showSelectEDB" @select="handleConfirmSelectEDB"/>
+
+    <!-- 指标溯源 -->
+    <EDBHistory v-model:show="showEDBHistory" :edbInfoId="edbHistoryId"/>
+
+    <!-- 选择单位 -->
+    <SelectEDBUnit v-model:show="showSelectUnit" @select="onConfirmSelectUnit"/>
+
+    <!-- 选择分类 -->
+    <SelectEDBClassify ref="selectEDBClassifyINS" :defaultId="baseInfo.classify" v-model:show="showSelectClassify" @select="handleConfirmClassify" />
+
+    <!-- 选择频度 -->
+    <SelectEDBFrequency v-model:show="showSelectFrequency" @select="handleConfirmFrequency"/>
+
+    <!-- 公式说明 -->
+    <van-dialog 
+        v-model:show="showTips" 
+        :title="$route.query.name"
+        confirmButtonText='知道啦'
+    >
+        <div class="edb-formula-tips-html-box" v-html="tipsContent"></div>
+    </van-dialog>
+</template>
+
+<style lang="scss" scoped>
+.formula-calculate-wrap{
+    min-height: 90vh;
+    background-color: $page-bg-grey;
+    padding-bottom: 210px ;
+}
+.section{
+    background-color: #fff;
+    margin-bottom: 32px;
+}
+.select-edb-box{
+    .left-icon{
+        width: 48px;
+        height: 48px;
+    }
+    :deep(.van-cell__right-icon){
+        align-self: center;
+        color: #333;
+    }
+    .edb-info-box{
+        width: 100%;
+        text-align: right;
+        .placeholder{
+            color: var(--van-text-color-3);
+        }
+        .edb-info{
+            display: flex;
+            flex-direction: column;
+        }
+        .time{
+            color: $font-grey_999;
+            font-size: 24px;
+        }
+    }
+    .add-edb-box{
+        display: flex;
+        justify-content: flex-end;
+        align-items: center;
+        color: $theme-color;
+        font-size: 32px;
+        padding: 32px var(--van-cell-horizontal-padding);
+        img{
+            width: 48px;
+            height: 48px;
+        }
+    }
+
+}
+.formula-box{
+    .formula-input{
+        display: block;
+        box-sizing: border-box;
+        background-color: $page-bg-grey;
+        padding: 12px 32px;
+        border-radius: 12px;
+        line-height: 1.7;
+        width: 100%;
+    }
+    .formula-tips{
+        font-size: 24px;
+        color: $font-grey_999;
+    }
+}
+.baseinfo-box{
+    .edit-icon{
+        width: 48px;
+        height: 48px;
+    }
+}
+.formula-intro-btn{
+    width: 180px;
+    height: 60px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: 5px;
+    color: $theme-color;
+    line-height: 1;
+    background-color: #fff;
+    border-radius: 32px;
+    margin-left: auto;
+    margin-right: var(--van-cell-horizontal-padding);
+    .icon{
+        width: 24px;
+        height: 24px;
+    }
+}
+.opt-btns{
+    position: fixed;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    background-color: #fff;
+    z-index: 99;
+    padding: 48px;
+    display: flex;
+    justify-content: space-between;
+    .van-button{
+        width: 48%;
+        max-width: 300PX;
+    }
+}
+@media screen and (min-width:$media-width){
+    .formula-calculate-wrap{
+        padding-bottom: 105px;
+    }
+    .section{
+        margin-bottom: 16px;
+    }
+    .select-edb-box{
+        .left-icon{
+            width: 24px;
+            height: 24px;
+        }
+        .edb-info-box{
+            .time{
+                font-size: 12px;
+            }
+        }
+        .add-edb-box{
+            font-size: 16px;
+            padding: 16px var(--van-cell-horizontal-padding);
+            img{
+                width: 24px;
+                height: 24px;
+            }
+        }
+    }
+    .formula-box{
+        .formula-input{
+            padding: 6px 16px;
+            border-radius: 6px;
+        }
+        .formula-tips{
+            font-size: 12px;
+        }
+    }
+    .baseinfo-box{
+        .edit-icon{
+            width: 24px;
+            height: 24px;
+        }
+    }
+    .formula-intro-btn{
+        width: 90px;
+        height: 30px;
+        gap: 2px;
+        border-radius: 16px;
+        .icon{
+            width: 12px;
+            height: 12px;
+        }
+    }
+    .opt-btns{
+        padding: 24px;
+        justify-content: center;
+        gap: 10px;
+    }
+    
+}
+</style>

+ 494 - 0
src/views/dataEDB/calculate/components/JointCalculate.vue

@@ -0,0 +1,494 @@
+<script setup>
+import apiDataEDB from '@/api/dataEDB'
+import {ref,reactive, computed,watch} from 'vue'
+import SelectEDB from './SelectEDB.vue'
+import SelectEDBClassify from '../../components/SelectEDBClassify.vue'
+import SelectEDBUnit from '../../components/SelectEDBUnit.vue'
+import SelectEDBFrequency from '../../components/SelectEDBFrequency.vue'
+import SelectDate from '@/components/SelectDate.vue'
+import { showToast } from 'vant'
+import { useRoute, useRouter } from 'vue-router'
+
+const route=useRoute()
+const router=useRouter()
+
+const props=defineProps({
+    edbInfo:{
+        type:Object,
+        default:null
+    }
+})
+watch(
+    ()=>props.edbInfo,
+    ()=>{
+        if(['edit','preview'].includes(route.query.type)){
+            const edbInfoData=props.edbInfo.EdbInfoDetail
+            if(edbInfoData.Source===24){//累计同比拼接
+                tabActive.value=2
+            }else{
+                tabActive.value=1
+                jointDate.value=edbInfoData.CalculateFormula
+            }
+            const temBeforeEDBInfo=props.edbInfo.CalculateList.find(item => item.FromTag === 'A')
+            const temAfterEDBInfo=props.edbInfo.CalculateList.find(item => item.FromTag === 'B')
+            beforeEDBInfo.value={
+                EdbName:temBeforeEDBInfo.FromEdbName,
+                EdbInfoId:temBeforeEDBInfo.FromEdbInfoId,
+                EndDate:temBeforeEDBInfo.EndDate,
+                StartDate:temBeforeEDBInfo.StartDate
+            }
+            afterEBDInfo.value={
+                EdbName:temAfterEDBInfo.FromEdbName,
+                EdbInfoId:temAfterEDBInfo.FromEdbInfoId,
+                EndDate:temAfterEDBInfo.EndDate,
+                StartDate:temAfterEDBInfo.StartDate
+            }
+            
+            baseInfo.name=edbInfoData.EdbName
+            baseInfo.unit=edbInfoData.Unit
+            baseInfo.classify=edbInfoData.ClassifyId
+            baseInfo.frequency=edbInfoData.Frequency
+
+
+            setTimeout(() => {
+                selectEDBClassifyINS.value?.getSelectClassifyOpt(props.edbInfo.EdbInfoDetail.ClassifyId)//获取选择的分类目录
+            }, 1000);
+        }
+    }
+)
+
+// 预览页面
+const isPreview=ref(route.query.type==='preview'||false)
+
+//公式说明
+const tipsConfig=new Map([
+	[1,`
+		直接拼接说明:<br>
+		1、选取拼接日期<br>
+		2、在拼接日期之前的数据选择指标A<br>
+		3、拼接日期之后的数据选择指标B<br>
+		4、新指标的起始日期为A的开始日期,更新时间跟随指标B进行更新<br>
+		5、指标A和B可以是原始指标,也可以是计算指标
+	`],
+	[2,`
+		累计值同比拼接说明:<br>
+		1、只支持月频度的指标进行累计值同比拼接<br>
+		2、如果出现有空值运算,则返回空值<br>
+		3、选取待拼接指标A和同比值指标B<br>
+		4、搜索到指标A最后一个12月31日有值的年份,并且向前回溯12个值(12个月),分别乘以下一年同期对应的同比增长率(B),公式为【A*(1+同期增长率(B)/100)】,得到下一年每个月的绝对值,再用新得到的下一年的12个月的值再乘以再下一年同期对应的同比增长率,得到再下一年每个月的绝对值<br>
+		5、以此类推,直到运算至指标B的最新值,得到新的序列C<br>
+		6、新指标是将序列C和指标A进行直接拼接,拼接日期选取指标A中有值的年份的年末最后一天即12月31日,再与序列C的起始日期做拼接<br>
+		7、新指标跟随指标B进行更新
+	`],
+])
+const showTips=ref(false)
+const tipsContent=computed(()=>{
+    return tipsConfig.get(tabActive.value)
+})
+
+const tabsArr=ref([{ label: '直接拼接',key: 1 },{ label: '累计同比拼接',key: 2 }])
+const tabActive=ref(1)
+function handleTabChange(){
+    jointDate.value=''
+    beforeEDBInfo.value=null
+    afterEBDInfo.value=null
+    baseInfo.name=''
+    baseInfo.unit=''
+    baseInfo.classify=''
+    baseInfo.frequency=''
+}
+
+//拼接日期
+const jointDate=ref('')
+const showSelectJointDate=ref(false)
+function handleConfirmJointDate(e){
+    jointDate.value=e
+}
+
+
+// 选择指标
+const selectEDBSearchParams=ref({})
+const beforeEDBInfo=ref(null)
+const afterEBDInfo=ref(null)
+const showSelectEDB=ref(false)
+let currentSelectEDBType=''//当前是选择哪个指标
+function handleShowSelectEDB(type){
+    if(isPreview.value) return
+    currentSelectEDBType=type
+    if(tabActive.value===2&&type==='before'){//累计同比值拼接选择待拼接指标搜索指标时Frequency传月度
+        selectEDBSearchParams.value={Frequency:'月度'}
+    }else{
+        selectEDBSearchParams.value={}
+    }
+    showSelectEDB.value=true
+}
+function handleConfirmSelectEDB(e){
+    if(currentSelectEDBType==='before'){
+        beforeEDBInfo.value=e
+    }else{
+        afterEBDInfo.value=e
+    }
+}
+
+// 基础信息
+const edbNameInputFocus=ref(false)
+const baseInfo=reactive({
+    name:'',
+    unit:'',
+    classify:'',
+    frequency:''
+})
+
+// 选择单位
+const showSelectUnit=ref(false)
+function onConfirmSelectUnit(value){
+    baseInfo.unit=value
+}
+
+//选择分类
+const showSelectClassify=ref(false)
+const classifyStr=ref('')
+const selectEDBClassifyINS=ref(null)
+function handleConfirmClassify({value,selectedOptions}){
+    if(selectedOptions.length===0){
+        baseInfo.classify=''
+        classifyStr.value=''
+        return
+    }
+    baseInfo.classify=value
+    classifyStr.value=`${selectedOptions[0].ClassifyName}/${selectedOptions[1].ClassifyName}/${selectedOptions[2].ClassifyName}`
+}
+
+//选择频度
+const showSelectFrequency=ref(false)
+function handleConfirmFrequency(value){
+    baseInfo.frequency=value
+}
+
+// 提交计算
+const saveBtnLoading=ref(false)
+async function handleSave(){
+    if(tabActive.value===1&&!jointDate.value){
+        showToast('拼接日期不能为空')
+        return
+    }
+    if(!beforeEDBInfo.value){
+        showToast(tabActive.value===1?'指标不能为空':'待拼接指标不能为空')
+        return
+    }
+    if(!afterEBDInfo.value){
+        showToast(tabActive.value===1?'指标不能为空':'指标不能为空')
+        return
+    }
+    if(!baseInfo.name){
+        showToast('指标名称不能为空')
+        return
+    }
+    if(!baseInfo.unit){
+        showToast('指标单位不能为空')
+        return
+    }
+    if(!baseInfo.classify){
+        showToast('指标目录不能为空')
+        return
+    }
+    if(!baseInfo.frequency){
+        showToast('指标频度不能为空')
+        return
+    }
+    const baseParams={
+        ClassifyId:baseInfo.classify,
+        EdbName:baseInfo.name,
+        Frequency:baseInfo.frequency,
+        Unit:baseInfo.unit,
+    }
+    const params=tabActive.value===1?{
+        ...baseParams,
+        EdbInfoIdArr:[
+            {EdbInfoId:afterEBDInfo.value.EdbInfoId}
+        ],
+        Formula:jointDate.value,
+        FromEdbInfoId:beforeEDBInfo.value.EdbInfoId,
+        Source:23
+    }:{
+        ...baseParams,
+        EdbInfoIdArr:[
+            {EdbInfoId:afterEBDInfo.value.EdbInfoId}
+        ],
+        FromEdbInfoId:beforeEDBInfo.value.EdbInfoId,
+        Source:24
+    }
+    saveBtnLoading.value=true
+    const res=route.query.type==='edit'?await apiDataEDB.editCalculateEDB({...params,EdbInfoId:Number(route.query.edbInfoId)}) : await apiDataEDB.addCalculateEDB(params)
+    saveBtnLoading.value=false
+    if(res.Ret===200){
+        showToast(route.query.type==='edit'?'编辑成功':'新增成功')
+        setTimeout(() => {
+            if(route.query.type==='edit'){
+                router.back()
+            }else{
+                router.replace({
+                    path:'/dataEDB/detail',
+                    query:{
+                        edbInfoId:res.Data.EdbInfoId
+                    }
+                })
+            }
+        }, 1500);
+    }
+}
+
+
+</script>
+
+<template>
+    <div class="joint-calculate-wrap">
+        <van-tabs 
+            v-model:active="tabActive" 
+            sticky 
+            border 
+            title-active-color="#0052D9" 
+            title-inactive-color="#333" 
+            line-width="16px"
+            @change="handleTabChange"
+            v-if="!['edit','preview'].includes($route.query.type)"
+        >
+            <van-tab 
+                :title="tab.label" 
+                :name="tab.key" 
+                v-for="tab in tabsArr" 
+                :key="tab.key"
+            />
+        </van-tabs>
+        <section class="section select-edb-box">
+            <van-field 
+                :modelValue="jointDate"
+                readonly
+                label="拼接日期" 
+                placeholder="请选择日期"
+                input-align="right"
+                :right-icon="!isPreview?'arrow':''"
+                required
+                @click-input="showSelectJointDate=true"
+                v-if="tabActive===1"
+                :disabled="isPreview"
+            />
+            <van-field
+                label-width="7em"
+                :label="tabActive===1?'拼接日期之前':'待拼接指标'"
+                required
+                :right-icon="!isPreview?'arrow':''"
+                @click-input="handleShowSelectEDB('before')"
+                :disabled="isPreview"
+            >
+                <template #input>
+                    <div class="edb-info-box">
+                        <div class="edb-info" v-if="beforeEDBInfo">
+                            <span class="name">{{beforeEDBInfo.EdbName}}</span>
+                            <span class="time" v-if="tabActive===1">(起始日期:{{beforeEDBInfo.StartDate}})</span>
+                            <span class="time" v-if="tabActive===2">(截至日期:{{beforeEDBInfo.EndDate}})</span>
+                        </div>
+                        <span class="placeholder" v-else>请选择指标</span>
+                    </div>
+                </template>
+            </van-field>
+            <van-field
+                label-width="7em"
+                :label="tabActive===1?'拼接日期之后':'同比值指标'"
+                :right-icon="!isPreview?'arrow':''"
+                required
+                @click-input="handleShowSelectEDB('after')"
+                :disabled="isPreview"
+            >
+                <template #input>
+                    <div class="edb-info-box">
+                        <div class="edb-info" v-if="afterEBDInfo">
+                            <span class="name">{{afterEBDInfo.EdbName}}</span>
+                            <span class="time" v-if="tabActive===1">(最新日期:{{afterEBDInfo.EndDate}})</span>
+                        </div>
+                        <span class="placeholder" v-else>请选择指标</span>
+                    </div>
+                </template>
+            </van-field>
+        </section>
+
+        <section class="section baseinfo-box">
+            <van-field 
+                v-model="baseInfo.name" 
+                label="指标名称" 
+                placeholder="指标名称"
+                input-align="right"
+                required
+                @focus="edbNameInputFocus=true"
+                @blur="edbNameInputFocus=false"
+                :disabled="isPreview"
+            >
+                <template #right-icon>
+                    <svg-icon v-if="!isPreview" class="edit-icon" name="edit" :color="edbNameInputFocus?'#0052D9':'#333333'"/>
+                </template>
+            </van-field>
+            <van-field 
+                :modelValue="baseInfo.unit"
+                readonly
+                label="单位" 
+                placeholder="请选择单位"
+                input-align="right"
+                :right-icon="!isPreview?'arrow':''"
+                required
+                @click-input="showSelectUnit=true"
+                :disabled="isPreview"
+            />
+            <van-field 
+                :modelValue="classifyStr"
+                readonly
+                label="指标目录" 
+                placeholder="请选择指标目录"
+                input-align="right"
+                :right-icon="!isPreview?'arrow':''"
+                required
+                @click-input="showSelectClassify=true"
+                :disabled="isPreview"
+            />
+            <van-field 
+                :modelValue="baseInfo.frequency"
+                readonly
+                label="频度" 
+                placeholder="请选择指标频度"
+                input-align="right"
+                :right-icon="!isPreview?'arrow':''"
+                required
+                @click-input="showSelectFrequency=true"
+                :disabled="isPreview"
+            />
+        </section>
+
+        <div class="formula-intro-btn" @click="showTips=true">
+            <svg-icon class="icon" name="warning"></svg-icon>
+            <span>公式说明</span>
+        </div>
+
+        <div class="opt-btns" v-if="!isPreview">
+            <van-button class="primary2" @click="$router.back()">取消</van-button>
+            <van-button 
+                type="primary" 
+                :loading="saveBtnLoading"
+                loading-text="计算中..."
+                @click="handleSave"
+            >保存</van-button>
+        </div>
+    </div>
+
+    <!-- 选择拼接日期 -->
+    <SelectDate v-model:show="showSelectJointDate" @select="handleConfirmJointDate"/>
+
+    <!-- 选择指标 -->
+    <SelectEDB v-model:show="showSelectEDB" :params="selectEDBSearchParams" @select="handleConfirmSelectEDB"/>
+
+    <!-- 选择单位 -->
+    <SelectEDBUnit v-model:show="showSelectUnit" @select="onConfirmSelectUnit"/>
+
+    <!-- 选择分类 -->
+    <SelectEDBClassify ref="selectEDBClassifyINS" :defaultId="baseInfo.classify" v-model:show="showSelectClassify" @select="handleConfirmClassify" />
+
+    <!-- 选择频度 -->
+    <SelectEDBFrequency v-model:show="showSelectFrequency" @select="handleConfirmFrequency"/>
+
+    <!-- 公式说明 -->
+    <van-dialog 
+        v-model:show="showTips" 
+        :title="$route.query.name"
+        confirmButtonText='知道啦'
+    >
+        <div class="edb-formula-tips-html-box" v-html="tipsContent"></div>
+    </van-dialog>
+</template>
+
+<style lang="scss" scoped>
+.joint-calculate-wrap{
+    min-height: 90vh;
+    background-color: $page-bg-grey;
+    padding-bottom: 210px ;
+}
+.section{
+    background-color: #fff;
+    margin-bottom: 32px;
+}
+.select-edb-box{
+    :deep(.van-cell__right-icon){
+        align-self: center;
+        color: #333;
+    }
+    .edb-info-box{
+        width: 100%;
+        text-align: right;
+        .placeholder{
+            color: var(--van-text-color-3);
+        }
+        .edb-info{
+            display: flex;
+            flex-direction: column;
+        }
+        .time{
+            color: $font-grey_999;
+            font-size: 24px;
+        }
+    }
+}
+.formula-intro-btn{
+    width: 180px;
+    height: 60px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: 5px;
+    color: $theme-color;
+    line-height: 1;
+    background-color: #fff;
+    border-radius: 32px;
+    margin-left: auto;
+    margin-right: var(--van-cell-horizontal-padding);
+    .icon{
+        width: 24px;
+        height: 24px;
+    }
+}
+.opt-btns{
+    position: fixed;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    background-color: #fff;
+    z-index: 99;
+    padding: 48px;
+    display: flex;
+    justify-content: space-between;
+    .van-button{
+        width: 48%;
+        max-width: 300PX;
+    }
+}
+
+@media screen and (min-width:$media-width){
+    .joint-calculate-wrap{
+        padding-bottom: 105px;
+    }
+    .section{
+        margin-bottom: 16px;
+    }
+    .formula-intro-btn{
+        width: 90px;
+        height: 30px;
+        gap: 2px;
+        border-radius: 16px;
+        .icon{
+            width: 12px;
+            height: 12px;
+        }
+    }
+    .opt-btns{
+        padding: 24px;
+        justify-content: center;
+        gap: 10px;
+    }
+}
+</style>

+ 744 - 0
src/views/dataEDB/calculate/components/OtherCalculate.vue

@@ -0,0 +1,744 @@
+<script setup>
+import {ref,reactive,watch} from 'vue'
+import { useRoute, useRouter } from "vue-router";
+import EDBHistory from '@/views/dataEDB/components/EDBHistory.vue'
+import SelectEDB from './SelectEDB.vue'
+import SelectEDBClassify from '../../components/SelectEDBClassify.vue'
+import SelectEDBUnit from '../../components/SelectEDBUnit.vue'
+import SelectEDBFrequency from '../../components/SelectEDBFrequency.vue'
+import SeeEDBDataList from './SeeEDBDataList.vue'
+import {calculateTypeTipsMap} from '../../util/config'
+import { showToast } from 'vant';
+import apiDataEDB from '@/api/dataEDB'
+import { useWindowSize } from '@vueuse/core'
+
+const { width } = useWindowSize()
+const route=useRoute()
+const router=useRouter()
+
+const props=defineProps({
+    edbInfo:{
+        type:Object,
+        default:null
+    }
+})
+
+watch(
+    ()=>props.edbInfo,
+    ()=>{
+        if(['edit','preview'].includes(route.query.type)){
+            const fromEdbInfo=props.edbInfo.CalculateList[0]
+            const edbInfoData=props.edbInfo.EdbInfoDetail
+            searchEDBInfoByCode(fromEdbInfo.FromEdbCode)
+            baseInfo.name=edbInfoData.EdbName
+            baseInfo.unit=edbInfoData.Unit
+            baseInfo.classify=edbInfoData.ClassifyId
+            baseInfo.frequency=edbInfoData.Frequency
+            if(edbInfoData.Source===51){//降频
+                baseInfo.valueType=edbInfoData.CalculateFormula
+            }
+            if(edbInfoData.Source===35){//超季节性
+                baseInfo.calendarType=edbInfoData.Calendar
+            }
+            if(edbInfoData.Source===22){//时间位移
+                baseInfo.moveType=edbInfoData.MoveType
+                baseInfo.moveUnit=edbInfoData.MoveFrequency
+                baseInfo.moveVal=edbInfoData.CalculateFormula
+            }
+            if([8,12,13,35].includes(edbInfoData.Source)){
+                baseInfo.numberN=Number(edbInfoData.CalculateFormula)
+            }
+            if([72].includes(edbInfoData.Source)){//指数修匀
+                baseInfo.alphaVal=Number(edbInfoData.CalculateFormula)
+            }
+
+            setTimeout(() => {
+                selectEDBClassifyINS.value?.getSelectClassifyOpt(props.edbInfo.EdbInfoDetail.ClassifyId)//获取选择的分类目录
+            }, 1000);
+        }
+    }
+)
+
+// 预览页面
+const isPreview=ref(route.query.type==='preview'||false)
+
+const moveTypeOpts=[{name:'领先',key:1},{name:'滞后',key:2}]
+const moveUnitOpts=[
+    {name:'天',key:'天'},
+    {name:'周',key:'周'},
+    {name:'月',key:'月'},
+    {name:'季',key:'季'},
+    {name:'年',key:'年'},
+]
+
+//公式说明
+const showTips=ref(false)
+const tipsContent=ref(calculateTypeTipsMap.get(['toMonthSeason','accumulate'].includes(route.query.source)? route.query.source: Number(route.query.source))||'')
+
+//提交计算按钮文字
+function getCalculateBtnText(){
+    let str='生成计算指标'
+    const btnTextMap=new Map([
+		[5,'转月值计算'],
+		[6,'同比值计算'],
+		[7,'同差值计算'],
+		[8,'移动平均计算'],
+		[12,'环比值计算'],
+		[13,'环差值计算'],
+		[14,'升频计算'],
+		[22,'保存'],
+		[35,'超季节性计算'],
+		[52,'年化计算'],
+		[51,'降频计算'],
+		[61,'转季值计算'],
+		[62,'累计值计算'],
+		[63,'年初至今计算'],
+        [72,'指数修匀计算'],
+	])
+    str=btnTextMap.get(source.value)
+
+    return str
+}
+
+const editEdbInfoId=ref(route.query.edbInfoId||0)//编辑时的指标id
+const source=ref(0)//计算类型
+const tabsArr=ref([])
+// 初始化
+function init(){
+    // 累计值转月/季值
+    if(route.query.source==='toMonthSeason'){
+        tabsArr.value=[{ label: '累计值转月值',key: 5 },{ label: '累计值转季值',key: 61 }]
+        source.value=5
+    }else if(route.query.source==='accumulate'){//累计值
+        tabsArr.value=[{ label: '累计值',key: 62 },{ label: '年初至今累计值',key: 63 }]
+        source.value=62
+    }else{
+        tabsArr.value=[]
+        source.value=Number(route.query.source)
+    }
+}
+init()
+
+// 选择指标
+const showSelectEDB=ref(false)
+const selectEDBinfo=ref(null)
+function handleConfirmSelectEDB(e){
+    selectEDBinfo.value=e
+    updateBaseInfoData(e)
+}
+// 编辑时回显选择的指标 通过code去搜索
+function searchEDBInfoByCode(code){
+    apiDataEDB.edbSearchList({KeyWord:code,CurrentIndex:1,PageSize:1}).then(res=>{
+        if(res.Ret===200){
+            selectEDBinfo.value=res.Data?.List[0]
+        }
+    })
+}
+
+// 选择指标更新基础信息
+function updateBaseInfoData(data){
+    const tMap=new Map([
+		['日度','D'],
+		['周度','W'],
+		['旬度','T'],
+		['月度','M'],
+		['季度','Q'],
+		['年度','Y'],
+	])
+	const name_map = {
+		5: data.EdbName,
+		8: `${data.EdbName}/${baseInfo.numberN}${tMap.get(data.Frequency)}MA`,
+		14: `${data.EdbName}/${data.Frequency}升频`,
+		6: `${data.EdbName}同比`,
+		7: `${data.EdbName}同差`,
+		12: `${data.EdbName}${baseInfo.numberN}${data.Frequency.slice(0,1)}环比`,
+		13: `${data.EdbName}${baseInfo.numberN}${data.Frequency.slice(0,1)}环差`,
+		35: `${data.EdbName}超季节性/${baseInfo.numberN}年${baseInfo.calendarType==='公历'?'':'/'+baseInfo.calendarType}`,
+		52: `${data.EdbName}年化`,
+		51: `${data.EdbName}/${data.Frequency}降频`,
+		61:  data.EdbName,
+		62:  data.EdbName,
+		63:  data.EdbName,
+        72: `${data.EdbName}指数修匀`,
+	}
+    baseInfo.name=name_map[source.value]||''
+    baseInfo.unit=[5,8,14,7,35,72].includes(source.value) ? data.Unit : '无',
+    baseInfo.frequency=source.value === 14 ? '日度' : source.value === 61 ? '季度' : source.value === 62 ? '' : data.Frequency
+
+    if(source.value===72){//指数修匀目录默认和选择的指标在同一个目录
+        selectEDBClassifyINS.value?.getSelectClassifyOpt(data.ClassifyId)//获取选择的分类目录
+    }
+    
+}
+
+// 基础信息
+const edbNameInputFocus=ref(false)
+const numberNInputFocus=ref(false)
+const alphaValInputFocus=ref(false)
+const baseInfo=reactive({
+    name:'',
+    unit:'',
+    classify:'',
+    frequency:'',
+    numberN:1,//N值
+    moveVal:'',
+    moveType:1,
+    moveUnit:'天',
+    calendarType:'公历',
+    valueType:'期末值',
+    alphaVal:'',
+})
+
+// 选择单位
+const showSelectUnit=ref(false)
+function onConfirmSelectUnit(value){
+    baseInfo.unit=value
+}
+
+//选择分类
+const showSelectClassify=ref(false)
+const classifyStr=ref('')
+const selectEDBClassifyINS=ref(null)
+function handleConfirmClassify({value,selectedOptions}){
+    if(selectedOptions.length===0){
+        baseInfo.classify=''
+        classifyStr.value=''
+        return
+    }
+    baseInfo.classify=value
+    classifyStr.value=`${selectedOptions[0].ClassifyName}/${selectedOptions[1].ClassifyName}/${selectedOptions[2].ClassifyName}`
+}
+
+//选择频度
+const showSelectFrequency=ref(false)
+function handleConfirmFrequency(value){
+    baseInfo.frequency=value
+}
+
+//移动方式类型选择
+const showMoveType=ref(false)
+function onSelectMoveType(e){
+    baseInfo.moveType=e.key
+}
+function getMoveTypeName(e){
+    return moveTypeOpts.filter(item=>item.key===e)[0].name
+}
+
+//移动方式单位选择
+const showMoveUnit=ref(false)
+function onSelectMoveUnit(e){
+    baseInfo.moveUnit=e.key
+}
+
+//超季节性日历选择
+const showSelectCalendar=ref(false)
+
+//降频数据取值选择
+const showDataValSelect=ref(false)
+
+//查看指标数据详情
+const showSeeEDBDataList=ref(false)
+
+// 提交计算
+const saveBtnLoading=ref(false)
+async function handleSave(){
+    if(!selectEDBinfo.value){
+        showToast('指标不能为空')
+        return
+    }
+    if(!baseInfo.name){
+        showToast('指标名称不能为空')
+        return
+    }
+    if(!baseInfo.unit){
+        showToast('指标单位不能为空')
+        return
+    }
+    if(!baseInfo.classify){
+        showToast('指标目录不能为空')
+        return
+    }
+    if(!baseInfo.frequency){
+        showToast('指标频度不能为空')
+        return
+    }
+    if([72].includes(source.value)){
+        if(baseInfo.alphaVal<=0||baseInfo.alphaVal>=1){
+            showToast('请输入>0,<1的alpha数值')
+            return
+        }
+    }
+
+    const valueMap = {
+		22: 'moveVal',
+		51: 'valueType',
+        72: 'alphaVal'
+	}
+    const params={
+        FromEdbInfoId: selectEDBinfo.value.EdbInfoId,
+        Source: source.value,
+		EdbName: baseInfo.name,
+		Unit: baseInfo.unit,
+		ClassifyId: baseInfo.classify,
+		Frequency: baseInfo.frequency,
+		Formula: valueMap[source.value] ? String(baseInfo[valueMap[source.value]]) : String(baseInfo.numberN),
+		MoveFrequency: baseInfo.moveUnit,
+		MoveType: baseInfo.moveType, 
+		Calendar: baseInfo.calendarType,
+    }
+
+    saveBtnLoading.value=true
+    const res=route.query.type==='edit'?await apiDataEDB.editCalculateEDB({...params,EdbInfoId:Number(route.query.edbInfoId)}) : await apiDataEDB.addCalculateEDB(params)
+    saveBtnLoading.value=false
+    if(res.Ret===200){
+        showToast(res.Msg)
+        setTimeout(() => {
+            if(route.query.type==='edit'){
+                router.back()
+            }else{
+                router.replace({
+                    path:'/dataEDB/detail',
+                    query:{
+                        edbInfoId:res.Data.EdbInfoId
+                    }
+                })
+            }
+        }, 1500);
+    }
+}
+
+// tab切换重置表单
+function handleTabChange(){
+    selectEDBinfo.value=null
+    baseInfo.name=''
+    baseInfo.unit=''
+    baseInfo.classify=''
+    baseInfo.frequency=''
+    baseInfo.numberN=1
+    baseInfo.moveVal=''
+    baseInfo.moveType=1
+    baseInfo.moveUnit='天'
+    baseInfo.calendarType='公历'
+    baseInfo.valueType='期末值'
+    classifyStr.value=''
+
+}
+
+//点击选择的指标左侧图标查看指标详情
+const showEDBHistory=ref(false)// 显示指标溯源
+const edbHistoryId=ref(0)
+function handleShowEDBHistory(data){
+    //计算指标打开弹窗,基础指标打开新页面
+    if(data.EdbType===2){
+        edbHistoryId.value=data.EdbInfoId
+        showEDBHistory.value=true
+    }else{
+        const routerEl=router.resolve({
+            path:'/dataEDB/detail',
+            query:{
+                edbInfoId:data.EdbInfoId
+            }
+        })
+        window.open(routerEl.href,'_blank')
+    }
+}
+
+</script>
+
+<template>
+    <div class="other-calculate-wrap">
+        <van-tabs 
+            v-model:active="source" 
+            sticky
+            :offset-top="width>650?60:0"
+            border 
+            title-active-color="#0052D9" 
+            title-inactive-color="#333" 
+            line-width="16px"
+            @change="handleTabChange"
+            v-if="tabsArr.length"
+        >
+            <van-tab 
+                :title="tab.label" 
+                :name="tab.key" 
+                v-for="tab in tabsArr" 
+                :key="tab.key"
+            />
+        </van-tabs>
+        <section class="section select-edb-box">
+            <van-field
+                :modelValue="selectEDBinfo?.EdbName"
+                label="选择指标"
+                :right-icon="!isPreview?'arrow':''"
+                readonly
+                placeholder="请选择指标"
+                input-align="right"
+                @click-input="showSelectEDB=true"
+                :disabled="isPreview"
+            >
+                <template #left-icon>
+                    <div class="left-icon" v-if="selectEDBinfo" @click="handleShowEDBHistory(selectEDBinfo)">
+                        <svg-icon name="edb-history-tag" size="24px"/>
+                    </div>
+                </template>
+            </van-field>
+        </section>
+        <section class="section edbinfo-box">
+            <div class="van-cell__title">已选指标</div>
+            <div v-if="!selectEDBinfo">
+                <img class="list-empty-img" src="https://hzstatic.hzinsights.com/static/ETA_mobile/empty_img.png" alt="">
+                <p style="text-align:center;color:#999">暂无指标</p>
+            </div>
+            <div class="info-box" v-else>
+                <h2 class="name">{{selectEDBinfo?.EdbName}}</h2>
+                <ul class="info-list">
+                    <li class="info-item">ID:{{selectEDBinfo.EdbCode}}</li>
+                    <li class="info-item">起始时间:{{selectEDBinfo.StartDate}}</li>
+                    <li class="info-item">频度:{{selectEDBinfo.Frequency}}</li>
+                    <li class="info-item">更新时间:{{selectEDBinfo.ModifyTime}}</li>
+                    <li class="info-item">单位:{{selectEDBinfo.Unit}}</li>
+                    <li class="info-item">数据来源:{{selectEDBinfo.SourceName}}</li>
+                </ul>
+                <div style="text-align:right">
+                    <van-button color="#0052D9" size="small" @click="showSeeEDBDataList=true">查看数据</van-button>
+                </div>
+            </div>
+        </section>
+        <section class="section baseinfo-box">
+            <!-- 时间位移(移动方式) -->
+            <van-field
+                label="移动方式"
+                required
+                v-if="source===22"
+                :disabled="isPreview"
+            >
+                <template #input>
+                    <div class="move-type-box">
+                        <div :class="['btn',isPreview?'disabled':'']" @click="()=>{if(isPreview)return false;showMoveType=true}">
+                            <svg-icon name="swap" v-if="!isPreview"></svg-icon>
+                            <span>{{getMoveTypeName(baseInfo.moveType)}}</span>
+                        </div>
+                        <input :disabled="isPreview" class="input" type="number" :min="0" v-model="baseInfo.moveVal">
+                        <div :class="['btn',isPreview?'disabled':'']" @click="()=>{if(isPreview)return false;showMoveUnit=true}">
+                            <svg-icon name="swap" v-if="!isPreview"></svg-icon>
+                            <span>{{baseInfo.moveUnit}}</span>
+                        </div>
+                    </div>
+                </template>
+            </van-field>
+            <van-field 
+                v-model="baseInfo.name" 
+                label="指标名称" 
+                placeholder="指标名称"
+                input-align="right"
+                required
+                @focus="edbNameInputFocus=true"
+                @blur="edbNameInputFocus=false"
+                :disabled="isPreview"
+            >
+                <template #right-icon>
+                    <svg-icon v-if="!isPreview" class="edit-icon" name="edit" :color="edbNameInputFocus?'#0052D9':'#333333'"/>
+                </template>
+            </van-field>
+            <van-field 
+                :modelValue="baseInfo.unit"
+                readonly
+                label="单位" 
+                placeholder="请选择单位"
+                input-align="right"
+                :right-icon="!isPreview?'arrow':''"
+                required
+                @click-input="showSelectUnit=true"
+                :disabled="!editEdbInfoId&&[6,7].includes(source)||isPreview"
+            />
+            <van-field 
+                :modelValue="classifyStr"
+                readonly
+                label="指标目录" 
+                placeholder="请选择指标目录"
+                input-align="right"
+                :right-icon="!isPreview?'arrow':''"
+                required
+                @click-input="showSelectClassify=true"
+                :disabled="isPreview"
+            />
+            <van-field 
+                :modelValue="baseInfo.frequency"
+                readonly
+                label="频度" 
+                placeholder="请选择指标频度"
+                input-align="right"
+                :right-icon="!isPreview?'arrow':''"
+                required
+                @click-input="showSelectFrequency=true"
+                :disabled="[5,14,61,63].includes(source)||(!editEdbInfoId&&[6,7].includes(source))||isPreview"
+            />
+            <van-field
+                v-if="[8,12,13,35].includes(source)"
+                v-model.number="baseInfo.numberN" 
+                label="N等于" 
+                placeholder="请输入N数值"
+                input-align="right"
+                required
+                @focus="numberNInputFocus=true"
+                @blur="numberNInputFocus=false"
+                :disabled="isPreview"
+            >
+                <template #right-icon>
+                    <svg-icon v-if="!isPreview" class="edit-icon" name="edit" :color="numberNInputFocus?'#0052D9':'#333333'"/>
+                </template>
+            </van-field>
+            <!-- 超季节性日历 -->
+            <van-field
+                v-if="source===35"
+                :modelValue="baseInfo.calendarType"
+                readonly
+                label="日历" 
+                placeholder="请选择日历"
+                input-align="right"
+                :right-icon="!isPreview?'arrow':''"
+                required
+                @click-input="showSelectCalendar=true"
+                :disabled="isPreview"
+            />
+            <!-- 降频数据取值 -->
+            <van-field
+                v-if="source===51"
+                :modelValue="baseInfo.valueType"
+                readonly
+                label="数据取值" 
+                placeholder="请选择"
+                input-align="right"
+                :right-icon="!isPreview?'arrow':''"
+                @click-input="showDataValSelect=true"
+                :disabled="isPreview"
+            />
+            <!-- 指数修匀 alpha 值 -->
+            <van-field
+                v-if="[72].includes(source)"
+                v-model.number="baseInfo.alphaVal"
+                type="number"
+                label="alpha值" 
+                placeholder="请输入alpha值"
+                input-align="right"
+                required
+                @focus="alphaValInputFocus=true"
+                @blur="alphaValInputFocus=false"
+                :disabled="isPreview"
+            >
+                <template #right-icon>
+                    <svg-icon v-if="!isPreview" class="edit-icon" name="edit" :color="alphaValInputFocus?'#0052D9':'#333333'"/>
+                </template>
+            </van-field>
+        </section>
+
+        <div class="formula-intro-btn" @click="showTips=true">
+            <svg-icon class="icon" name="warning"></svg-icon>
+            <span>公式说明</span>
+        </div>
+        <div class="opt-btns" v-if="!isPreview">
+            <van-button class="primary2" @click="$router.back()">取消</van-button>
+            <van-button 
+                type="primary" 
+                :loading="saveBtnLoading"
+                loading-text="计算中..."
+                @click="handleSave"
+            >{{getCalculateBtnText()}}</van-button>
+        </div>
+    </div>
+    
+    <!-- 选择指标 -->
+    <SelectEDB v-model:show="showSelectEDB" :source='source' @select="handleConfirmSelectEDB"/>
+
+    <!-- 选择单位 -->
+    <SelectEDBUnit v-model:show="showSelectUnit" @select="onConfirmSelectUnit"/>
+
+    <!-- 选择分类 -->
+    <SelectEDBClassify ref="selectEDBClassifyINS" v-model:show="showSelectClassify" :defaultId="baseInfo.classify" @select="handleConfirmClassify" />
+
+    <!-- 选择频度 -->
+    <SelectEDBFrequency v-model:show="showSelectFrequency" @select="handleConfirmFrequency"/>
+
+    <!-- 查看指标数据 -->
+    <SeeEDBDataList v-model:show="showSeeEDBDataList" :edbInfoId="selectEDBinfo?.EdbInfoId" />
+
+    <!-- 移动方式类型选择 -->
+    <van-action-sheet v-model:show="showMoveType" close-on-click-action :actions="moveTypeOpts" @select="onSelectMoveType" />
+    
+    <!-- 移动方式单位选择 -->
+    <van-action-sheet v-model:show="showMoveUnit" close-on-click-action :actions="moveUnitOpts" @select="onSelectMoveUnit" />
+
+    <!-- 超季节性日历选择 -->
+    <van-action-sheet v-model:show="showSelectCalendar" close-on-click-action :actions="[{name:'公历'},{name:'农历'}]" @select="e=>baseInfo.calendarType=e.name" />
+
+    <!-- 降频数据取值 -->
+    <van-action-sheet v-model:show="showDataValSelect" close-on-click-action :actions="[{name:'期末值'},{name:'平均值'}]" @select="e=>baseInfo.valueType=e.name" />
+    
+    <!-- 指标溯源 -->
+    <EDBHistory v-model:show="showEDBHistory" :edbInfoId="edbHistoryId"/>
+
+    <!-- 公式说明 -->
+    <van-dialog 
+        v-model:show="showTips" 
+        :title="$route.query.name"
+        confirmButtonText='知道啦'
+    >
+        <div class="edb-formula-tips-html-box" v-html="tipsContent"></div>
+    </van-dialog>
+</template>
+
+<style lang="scss" scoped>
+.other-calculate-wrap{
+    min-height: 90vh;
+    background-color: $page-bg-grey;
+    padding-bottom: 210px ;
+}
+.section{
+    background-color: #fff;
+    margin-bottom: 32px;
+}
+.edbinfo-box{
+    padding: var(--van-cell-horizontal-padding);
+    .info-box{
+        margin-top: 14px;
+        border-radius: 8px;
+        border: 1px solid $border-color;
+        box-shadow: $box-shadow;
+        padding: 20px;
+        .name{
+            font-size: 32px;
+            margin: 0 0 10px 0;
+        }
+        .info-list{
+            display: flex;
+            flex-wrap: wrap;
+            gap: 10px 0;
+            color: $font-grey;
+            margin-bottom: 10px;
+            .info-item{
+                width: 100%;
+                margin-bottom: 10px;
+            }
+        }
+    }
+}
+.move-type-box{
+    width: 100%;
+    display: flex;
+    align-items: center;
+    justify-content: flex-end;
+    gap: 20px;
+    .input{
+        box-sizing: border-box;
+        display: block;
+        border-radius: 12px;
+        width: 150px;
+        height: 72px;
+        padding: 0 32px;
+        background-color: #f6f6f6;
+    }
+    .btn{
+        flex-shrink: 0;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        width: 130px;
+        height: 72px;
+        border-radius: 12px;
+        background-color: #F2F3FF;
+        color: $theme-color;
+    }
+    .disabled{
+        background-color: #F6F6F6;
+        color: #333;
+    }
+}
+.formula-intro-btn{
+    width: 180px;
+    height: 60px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: 5px;
+    color: $theme-color;
+    line-height: 1;
+    background-color: #fff;
+    border-radius: 32px;
+    margin-left: auto;
+    margin-right: var(--van-cell-horizontal-padding);
+    .icon{
+        width: 24px;
+        height: 24px;
+    }
+}
+.opt-btns{
+    position: fixed;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    background-color: #fff;
+    z-index: 99;
+    padding: 48px;
+    display: flex;
+    justify-content: space-between;
+    .van-button{
+        width: 48%;
+        max-width: 300PX;
+    }
+}
+@media screen and (min-width:$media-width){
+    .other-calculate-wrap{
+        padding-bottom: 105px;
+    }
+    .section{
+        margin-bottom: 16px;
+    }
+    .edbinfo-box{
+        .info-box{
+            margin-top: 7px;
+            border-radius: 4px;
+            padding: 10px;
+            .name{
+                font-size: 16px;
+                margin: 0 0 5px 0;
+            }
+            .info-list{
+                gap: 5px 0;
+                margin-bottom: 5px;
+                .info-item{
+                    margin-bottom: 5px;
+                }
+            }
+        }
+    }
+    .move-type-box{
+        gap: 10px;
+        .input{
+            border-radius: 6px;
+            width: 65px;
+            height: 36px;
+            padding: 0 16px;
+        }
+        .btn{
+            width: 65px;
+            height: 36px;
+            border-radius: 6px;
+        }
+    }
+    .formula-intro-btn{
+        width: 90px;
+        height: 30px;
+        gap: 2px;
+        border-radius: 16px;
+        .icon{
+            width: 12px;
+            height: 12px;
+        }
+    }
+    .opt-btns{
+        padding: 24px;
+        justify-content: center;
+        gap: 10px;
+    }
+}
+</style>

+ 169 - 0
src/views/dataEDB/calculate/components/SeeEDBDataList.vue

@@ -0,0 +1,169 @@
+<script setup>
+import { reactive, watch } from "vue"
+import apiDataEDB from '@/api/dataEDB'
+
+
+const props=defineProps({
+    show:{
+        type:Boolean,
+        default:false
+    },
+    edbInfoId:{
+        type:Number,
+        default:0,
+    }
+})
+const emits=defineEmits(['update:show'])
+
+function handleClose(){
+    emits('update:show',false)
+}
+
+const listState=reactive({
+    title:'',
+    list:[],
+    page:1,
+    pageSize:20,
+    loading:false,
+    finished:false
+})
+async function getEDBDataList(){
+    listState.loading=true
+    const res=await apiDataEDB.edbDataList({
+        PageSize:listState.pageSize,
+        CurrentIndex:listState.page,
+        EdbInfoId:props.edbInfoId
+    })
+    listState.loading=false
+    if(res.Ret===200){
+        listState.title=res.Data?.Item.EdbName
+        const arr=res.Data?.Item.DataList||[]
+        listState.list=[...listState.list,...arr]
+        listState.finished=res.Data.Paging.IsEnd
+    }
+}
+function onLoad(){
+    listState.page++
+    getEDBDataList()
+}
+
+watch(
+    ()=>props.show,
+    (n)=>{
+        if(n){
+            listState.title=''
+            listState.list=[]
+            listState.finished=false
+            getEDBDataList()
+        }
+    }
+)
+
+</script>
+
+<template>
+    <van-popup 
+        :show="props.show"
+        round
+        closeable
+        position="bottom"
+        @click-close-icon="handleClose"
+        @click-overlay="handleClose"
+    >
+        <div class="title">{{listState.title}}</div>
+        <div class="nodata-box" style="padding-bottom:30px" v-if="listState.list.length==0&&listState.finished">
+            <img  class="list-empty-img" src="https://hzstatic.hzinsights.com/static/ETA_mobile/empty_img.png" alt="">
+            <p style="text-align:center;color:#999">暂无数据</p>
+        </div>
+        <div class="data-list" v-else>
+            <li class="data-item">
+                <div class="label time">时间</div>
+                <div class="label">值</div>
+            </li>
+            <van-list
+                v-model:loading="listState.loading"
+                :finished="listState.finished"
+                :finished-text="listState.list.length>0?'没有更多了':'暂无数据'"
+                :immediate-check="false"
+                @load="onLoad"
+            >
+                <li class="data-item" v-for="item in listState.list" :key="item">
+                    <div class="time">{{item.DataTime}}</div>
+                    <div class="val">
+                        <span>{{item.Value}}</span>
+                    </div>
+                </li>
+            </van-list>
+        </div>
+    </van-popup>
+</template>
+
+<style lang="scss" scoped>
+.title{
+    font-size: 36px;
+    font-weight: 600;
+    padding: 34px;
+    border-bottom: 1px solid $border-color;
+}
+.data-list{
+    max-height: 60vh;
+    overflow-y: auto;
+    .data-item{
+        display: flex;
+        border-bottom: 1px solid $border-color;
+        div{
+            flex-shrink: 0;
+            width: 50%;
+            padding: 0 32px;
+            height: 112px;
+            display: flex;
+            align-items: center;
+        }
+        .lable{
+            font-size: 34px;
+            font-weight: 500;
+        }
+        .time{
+            border-right: 1px solid $border-color;
+        }
+        .val{
+            span{
+                display: inline-block;
+                padding: 0 10px;
+                line-height: 48px;
+                background-color: #F2F3FF;
+                color: $theme-color;
+                border-radius: 4px;
+                min-width: 60px;
+                text-align: center;
+            }
+        }
+    }
+}
+@media screen and (min-width:$media-width){
+    .title{
+        font-size: 18px;
+        padding: 17px;
+    }
+    .data-list{
+        .data-item{
+            div{
+                padding: 0 16px;
+                height: 56px;
+            }
+            .lable{
+                font-size: 17px;
+            }
+            .val{
+                span{
+                    padding: 0 5px;
+                    line-height: 24px;
+                    border-radius: 2px;
+                    min-width: 30px;
+                }
+                
+            }
+        }
+    }
+}
+</style>

+ 341 - 0
src/views/dataEDB/calculate/components/SelectEDB.vue

@@ -0,0 +1,341 @@
+<script setup>
+import {reactive, ref, watch} from 'vue'
+import apiDataEDB from '@/api/dataEDB'
+import { showToast } from 'vant'
+
+const props=defineProps({
+    show:{
+        type:Boolean,
+        default:false
+    },
+    source:{//计算类型
+        type:Number,
+        default:0
+    },
+    params:{},//传来的参数
+})
+const emits=defineEmits(['update:show','select'])
+
+function handleClose(){
+    emits('update:show',false)
+}
+
+const listState=reactive({
+    list:[],
+    page:1,
+    pageSize:20,
+    finished:false,
+    loading:false,
+})
+async function getEDBList(){
+    listState.loading=true
+    const filterMap = {
+		5: 2,
+		14: 3,
+		63: 6
+	}
+    const params=props.source?{
+        FilterSource:filterMap[props.source] ? filterMap[props.source] : 1,
+        Frequency:props.source===61?'季度':''
+    }:{}
+    const res=await apiDataEDB.edbSearchList({
+        KeyWord:searchEDBTxt.value,
+        CurrentIndex:listState.page,
+        PageSize:listState.pageSize,
+        ...params,
+        ...props.params,
+    })
+    listState.loading=false
+    if(res.Ret===200){
+        if(!res.Data){
+            listState.finished=true
+            return
+        }
+        
+        listState.finished=res.Data.Paging.IsEnd
+        const arr=res.Data.List||[]
+        listState.list=[...listState.list,...arr]
+    }
+}
+function onLoad(){
+    listState.page++
+    getEDBList()
+}
+const searchEDBTxt=ref('')
+function onSearch(){
+    listState.page=1
+    listState.list=[]
+    listState.finished=false
+    getEDBList()
+}
+
+
+const checked=ref('')
+function handleSave(){
+    let selectItem=null
+    listState.list.forEach(item=>{
+        if(item.EdbInfoId===checked.value){
+            selectItem=item
+        }
+    })
+    if(!selectItem){
+        showToast('请选择指标')
+        return
+    }
+    emits('select',selectItem)
+    handleClose()
+}
+
+watch(
+    ()=>props.show,
+    ()=>{
+        if(props.show){
+            listState.page=1
+            listState.list=[]
+            listState.finished=false
+            checked.value=''
+            searchEDBTxt.value=''
+        }
+    }
+)
+
+
+// 指标详情
+const showEDBInfo=ref(false)
+const edbInfo=ref(null)
+function handleShowEDBInfo(item){
+    edbInfo.value=item
+    showEDBInfo.value=true
+}
+
+</script>
+
+<template>
+    <van-popup 
+        :show="props.show"
+        position="bottom"
+        :style="{ height: '100%' }"
+    >
+        <div class="select-edb-box">
+            <div class="search-box">
+                <van-search
+                    v-model="searchEDBTxt"
+                    show-action
+                    shape="round"
+                    placeholder="指标ID/名称"
+                    @clear="searchEDBTxt=''"
+                    @search="onSearch"
+                >
+                    <template #action>
+                        <span class="right-btn" @click="onSearch">查询</span>
+                    </template>
+                </van-search>
+            </div>
+            <div class="list-wrap" v-if="props.show">
+                <img v-if="listState.list.length==0&&listState.finished&&keyword" class="list-empty-img" src="https://hzstatic.hzinsights.com/static/ETA_mobile/empty_img.png" alt="">
+                <van-list
+                    v-model:loading="listState.loading"
+                    :finished="listState.finished"
+                    :finished-text="listState.list.length>0?'没有更多了':'暂无数据'"
+                    :immediate-check="false"
+                    @load="onLoad"
+                >   
+                    <van-radio-group v-model="checked">
+                        <ul class="list-box">
+                            <li class="item" v-for="item in listState.list" :key="item.EdbInfoId">
+                                <van-radio :name="item.EdbInfoId">
+                                    <div class="con">
+                                        <div class="name">{{item.EdbName}}</div>
+                                        <svg-icon @click.stop="handleShowEDBInfo(item)" class="icon" name="error-circle-filled" size="16px" color="#999"/>
+                                    </div>
+                                </van-radio>
+                            </li>
+                        </ul>
+                    </van-radio-group>
+                </van-list>
+            </div>
+            <div class="btns-box">
+                <van-button @click="handleClose">取消</van-button>
+                <van-button type="primary" @click="handleSave">保存</van-button>
+            </div>
+        </div>
+    </van-popup>
+    <!-- 指标信息详情 -->
+    <van-popup 
+        v-model:show="showEDBInfo"
+        round
+    >
+        <div class="edb-info-wrap" v-if="edbInfo">
+            <h3 class="van-multi-ellipsis--l2 name">{{edbInfo.EdbName}}</h3>
+            <ul class="info-list">
+                <li class="item">
+                    <span class="label">指标ID:</span>
+                    <span class="con">{{edbInfo.EdbCode}}</span>
+                </li>
+                <li class="item">
+                    <span class="label">频度:</span>
+                    <span class="con">{{edbInfo.Frequency}}</span>
+                </li>
+                <li class="item">
+                    <span class="label">单位:</span>
+                    <span class="con">{{edbInfo.Unit}}</span>
+                </li>
+                <li class="item">
+                    <span class="label">起始时间:</span>
+                    <span class="con">{{edbInfo.StartDate}}</span>
+                </li>
+                <li class="item">
+                    <span class="label">更新日期:</span>
+                    <span class="con">{{edbInfo.LatestDate}}</span>
+                </li>
+                <li class="item">
+                    <span class="label">最新值:</span>
+                    <span class="con">{{edbInfo.LatestValue}}</span>
+                </li>
+                <li class="item">
+                    <span class="label">最近更新:</span>
+                    <span class="con">{{edbInfo.ModifyTime}}</span>
+                </li>
+                <li class="item">
+                    <span class="label">数据来源:</span>
+                    <span class="con">{{edbInfo.SourceName}}</span>
+                </li>
+            </ul>
+            <van-button block type="primary" @click="showEDBInfo=false">知道了</van-button>
+        </div>
+    </van-popup>
+</template>
+
+<style lang="scss" scoped>
+.select-edb-box{
+    display: flex;
+    flex-direction: column;
+    height: 100%;
+    .list-wrap{
+        flex: 1;
+        overflow-y: auto;
+        padding-left: var(--van-padding-md);
+        .item{
+            :deep(.van-radio__label){
+                flex: 1;
+            }
+            .con{
+                padding: 32px 32px 32px 0;
+                border-bottom: 1PX solid $border-color;
+                display: flex;
+                .name{
+                    flex: 1;
+                }
+                .icon{
+                    margin-left: 20px;
+                }
+            }
+        }
+    }
+    .btns-box{
+        padding: 20px var(--van-padding-md);
+        display: flex;
+        justify-content: space-between;
+        .van-button{
+            width: 48%;
+            max-width: 300PX;
+        }
+    }
+}
+.search-box{
+    padding: var(--van-padding-md);
+    .van-search{
+        padding: 6px;
+        background-color: #F0F2F6;
+        border-radius: var(--van-radius-max);
+    }
+    :deep(.van-search__content){
+        background: transparent !important;
+        border: none;
+    }
+    :deep(.van-search__field .van-field__left-icon){
+        color: #333;
+    }
+    .right-btn{
+        display: inline-block;
+        color: #fff;
+        background-color: $theme-color;
+        width: 112px;
+        height: 56px;
+        text-align: center;
+        line-height: 56px;
+        border-radius: var(--van-radius-max);
+    }
+}
+.edb-info-wrap{
+    padding: 48px;
+    width: 80vw;
+    max-width: 500PX;
+    .name{
+        text-align: center;
+        margin-top: 0;
+        font-size: 36px;
+    }
+    .info-list{
+        max-height: 60vh;
+        overflow-y: auto;
+        .item{
+            margin-bottom: 32px;
+            display: flex;
+            .label{
+                width: 50%;
+                flex-shrink: 0;
+            }
+            .con{
+                width: 50%;
+                flex-shrink: 0;
+                color: $font-grey;
+                word-wrap: break-word;
+                word-break: break-all;
+            }
+        }
+    }
+}
+
+@media screen and (min-width:$media-width){
+    .select-edb-box{
+        .list-wrap{
+            .item{
+                .con{
+                    padding: 16px 16px 16px 0;
+                    .icon{
+                        margin-left: 10px;
+                    }
+                }
+            }
+        }
+        .btns-box{
+            padding: 10px var(--van-padding-md);
+            justify-content: center;
+            gap: 10px;
+        }
+    }
+    .search-box{
+        .van-search{
+            padding: 3px;
+        }
+        .right-btn{
+            width: 56px;
+            height: 28px;
+            line-height: 28px;
+        }
+    }
+    .edb-info-wrap{
+        padding: 24px;
+        .name{
+            font-size: 18px;
+        }
+        .info-list{
+            .item{
+                margin-bottom: 16px;
+            }
+        }
+    }
+}
+
+</style>

+ 13 - 6
src/views/dataEDB/components/EDBHistory.vue

@@ -28,11 +28,14 @@ function getImg(){
                 useCORS: true, // 允许图片跨域
                 allowTaint: true, // 在渲染前测试图片
                 imageTimeout: 0, // 加载延时
-                scale:3,
+                scale:1,
             }).then(res=>{
                 let img = res.toDataURL("image/png");
                 // console.log(img);
                 imgUrl.value=img
+                nextTick(()=>{
+                    $('.img-box').scrollLeft(700)
+                })
             })
         }
 }
@@ -69,7 +72,9 @@ watch(
     >
         <div class="img-wrap">
             <h2 class="name">指标溯源</h2>
-            <img class="img" :src="imgUrl" alt="">
+            <div class="img-box">
+                <img class="img" :src="imgUrl" alt="">
+            </div>
         </div>
     </van-popup>
     <div class="render-wrap" id="render-wrap" v-if="props.show">
@@ -90,23 +95,25 @@ watch(
 .img-wrap{
     width: 90vw;
     max-width: 600PX;
-    max-height: 80vh;
-    overflow-y: auto;
     .name{
         line-height: 30PX;
         font-size: 16PX;
         text-align: center;
     }
+    .img-box{
+        width: 100%;
+        height: 80vh;
+        overflow: auto;
+    }
     .img{
         display: block;
-        width: 100%;
         margin: 0 auto;
     }
 }
 .render-wrap{
     position: absolute;
     z-index: -1;
-    width: 400PX;
+    width: 2000PX;
     height: 1000PX;
     :deep(.tree-org-node__content){
         background-color: #409EFF;

+ 88 - 0
src/views/dataEDB/components/SelectEDBClassify.vue

@@ -0,0 +1,88 @@
+<script setup>
+import {ref, watch} from 'vue'
+import apiDataEDB from '@/api/dataEDB'
+
+const props=defineProps({
+    show:{
+        type:Boolean,
+        default:false
+    },
+    defaultId:{//初始选中的分类
+        type:[Number,String],
+        default:0
+    }
+})
+
+const emits=defineEmits(['update:show','select'])
+
+function handleClose(){
+    emits('update:show',false)
+}
+
+watch(
+    ()=>props.defaultId,
+    (n)=>{
+        // if(n){
+            classify.value=n
+        // }
+    }
+)
+
+// 获取指标库分类
+const classify=ref('')
+const edbClassifyList=ref([])
+function getEdbClassifyList(){
+    apiDataEDB.edbClassifyList().then(res=>{
+        if(res.Ret===200){
+            edbClassifyList.value=res.Data.AllNodes||[]
+        }
+    })
+}
+getEdbClassifyList()
+
+function handleFinish({value,selectedOptions,tabIndex}){
+    emits('select',{value,selectedOptions})
+    handleClose()
+}
+
+// 通过classifyid 获取当前目录
+function getSelectClassifyOpt(id){
+    let arr=[]
+    edbClassifyList.value.forEach(e1=>{
+        e1.Children?.forEach(e2=>{
+            e2.Children?.forEach(e3=>{
+                if(e3.ClassifyId===id){
+                    arr=[e1,e1,e3]
+                }
+            })
+        })
+    })
+    if(arr.length===0){
+        classify.value=''
+    }
+
+    emits('select',{value:id,selectedOptions:arr})
+    return arr
+}
+
+defineExpose({getSelectClassifyOpt})
+
+
+</script>
+
+<template>
+    <van-popup 
+        :show="props.show" 
+        round
+        position="bottom"
+    >
+        <van-cascader
+            v-model="classify"
+            title="选择目录"
+            :options="edbClassifyList"
+            :field-names="{text:'ClassifyName',value:'ClassifyId',children:'Children'}"
+            @close="handleClose"
+            @finish="handleFinish"
+        />
+    </van-popup>
+</template>

+ 39 - 0
src/views/dataEDB/components/SelectEDBFrequency.vue

@@ -0,0 +1,39 @@
+<script setup>
+import {edbFrequencyOpts} from '../util/config'
+
+const frequencyOpts=edbFrequencyOpts.map(item=>{return{text:item,value:item}})
+
+const props=defineProps({
+    show:{
+        type:Boolean,
+        default:false
+    },
+})
+
+const emits=defineEmits(['update:show','select'])
+
+function handleClose(){
+    emits('update:show',false)
+}
+function onConfirmSelectFrequency(e){
+    const value=e.selectedValues[0]
+    emits('select',value)
+    handleClose()
+}
+
+</script>
+
+<template>
+    <van-popup 
+        :show="props.show" 
+        round 
+        position="bottom"
+    >
+        <van-picker
+            title="请选择频度"
+            :columns="frequencyOpts"
+            @cancel="handleClose"
+            @confirm="onConfirmSelectFrequency"
+        />
+    </van-popup>
+</template>

+ 39 - 0
src/views/dataEDB/components/SelectEDBUnit.vue

@@ -0,0 +1,39 @@
+<script setup>
+import {edbUnitOpts} from '../util/config'
+
+const unitOpts=edbUnitOpts.map(item=>{return{text:item,value:item}})
+
+const props=defineProps({
+    show:{
+        type:Boolean,
+        default:false
+    },
+})
+
+const emits=defineEmits(['update:show','select'])
+
+function handleClose(){
+    emits('update:show',false)
+}
+function onConfirmSelectUnit(e){
+    const value=e.selectedValues[0]
+    emits('select',value)
+    handleClose()
+}
+
+</script>
+
+<template>
+    <van-popup 
+        :show="props.show" 
+        round 
+        position="bottom"
+    >
+        <van-picker
+            title="选择单位"
+            :columns="unitOpts"
+            @cancel="handleClose"
+            @confirm="onConfirmSelectUnit"
+        />
+    </van-popup>
+</template>

+ 316 - 0
src/views/dataEDB/util/config.js

@@ -18,3 +18,319 @@ export const edbUnitOpts=[
     '美分/加仑',
     '手'
 ]
+
+//计算类型说明tips
+export const calculateTypeTipsMap=new Map([
+	[4,`指标运算:选择指标参数,按照输入的计算公式进行计算,生成新的指标<br>
+	在参与计算的日期序列上某指标无值时,该指标往前/往后找距离最近的值作为当天的值进行计算,遍历允许跨年,往前最多35天,往后最多35天`],
+	[5,`累计值转月值计算方法:<br>
+	<p style="display:flex;justify-content: space-between;">
+		<span style="width:45%">1月无值:1月=2月/2</span>
+		<span style="width:40%">1月有值:1月=1月</span>
+	</p>
+	<p style="display:flex;justify-content: space-between;">
+		<span style="width:40%">2月=2月/2</span>
+		<span style="width:40%">2月=2月-1月</span>
+	</p>
+	<p style="display:flex;justify-content: space-between;">
+		<span style="width:40%">3月=3月-2月</span>
+		<span style="width:40%">3月=3月-2月</span>
+	</p>
+	<p style="display:flex;justify-content: space-between;">
+		<span style="width:40%">4月=4月-3月</span>
+		<span style="width:40%">4月=4月-3月</span>
+	</p>
+	<p style="display:flex;justify-content: space-between;">
+		<span style="width:40%">以此类推</span>
+		<span style="width:40%">以此类推</span>
+	</p>
+	<p>特别说明:若1月和2月均无值,则该年不做计算</p>`],
+	['toMonthSeason',`1、累计值转月值计算方法:<br>
+	<p style="display:flex;justify-content: space-between;">
+		<span style="width:45%">1月无值:1月=2月/2</span>
+		<span style="width:40%">1月有值:1月=1月</span>
+	</p>
+	<p style="display:flex;justify-content: space-between;">
+		<span style="width:40%">2月=2月/2</span>
+		<span style="width:40%">2月=2月-1月</span>
+	</p>
+	<p style="display:flex;justify-content: space-between;">
+		<span style="width:40%">3月=3月-2月</span>
+		<span style="width:40%">3月=3月-2月</span>
+	</p>
+	<p style="display:flex;justify-content: space-between;">
+		<span style="width:40%">4月=4月-3月</span>
+		<span style="width:40%">4月=4月-3月</span>
+	</p>
+	<p style="display:flex;justify-content: space-between;">
+		<span style="width:40%">以此类推</span>
+		<span style="width:40%">以此类推</span>
+	</p>
+	<p>特别说明:若1月和2月均无值,则该年不做计算</p>
+	2、累计值转季值计算方法:<br>
+	<p style="display:flex;justify-content: space-between;">
+		<span style="width:45%">1季度无值:1季度=2季度/2 </span>
+		<span style="width:40%">1季度有值:1季度=1季度</span>
+	</p>
+	<p style="display:flex;justify-content: space-between;">
+		<span style="width:40%">2季度=2季度/2</span>
+		<span style="width:40%">2季度=2季度-1季度</span>
+	</p>
+	<p style="display:flex;justify-content: space-between;">
+		<span style="width:40%">3季度=3季度-2季度</span>
+		<span style="width:40%">3季度=3季度-2季度</span>
+	</p>
+	<p style="display:flex;justify-content: space-between;">
+		<span style="width:40%">4季度=4季度-3季度</span>
+		<span style="width:40%">4季度=4季度-3季度</span>
+	</p>
+	<p style="display:flex;justify-content: space-between;">
+		<span style="width:40%">以此类推</span>
+		<span style="width:40%">以此类推</span>
+	</p>
+	<p>特别说明:若1季度和2季度均无值,则该年不做计算</p>`],
+	[6,`同比公式:今年同期/去年同期-1<br>
+	1、锁定当前数值对应的日期<br>
+	2、匹配上一年同期:寻找过去一年有数值的对应日期中,
+	与当前数值对应日期相等或者最为接近的那一天,如果有
+	两个日期与当前数值对应日期的距离相等,则取降序日期
+	排列下的第一个日期。<br>
+	3、取到匹配的上一年同期对应的数值<br>
+	4、将当期的数值与上一年匹配的同期的值计算比例<br>
+	5、将计算得到的比例填充至当前数值对应的日期,生成
+	新的数据序列<br>
+	6、遍历允许跨年,对于日度/周度/季度数据,遍历往前
+	最多35天,往后最多35天`],
+	[7,`同差公式:今年同期-去年同期<br>
+	1、锁定当前数值对应的日期<br>
+	2、匹配上一年同期:寻找过去一年有数值的对应日期中,
+	与当前数值对应日期相等或者最为接近的那一天,如果有
+	两个日期与当前数值对应日期的距离相等,则取降序日期
+	排列下的第一个日期。<br>
+	3、取到匹配的上一年同期对应的数值<br>
+	4、将当期的数值与上一年匹配的同期的值计算差值<br>
+	5、将计算得到的差值填充至当前数值对应的日期,生成
+	新的数据序列<br>
+	6、遍历允许跨年,对于日度/周度/季度数据,遍历往前
+	最多35天,往后最多35天`],
+	[8,`计算公式:=AVERAGE(N个数值的和),N为取数个数<br>
+	1、计算当前的数值的N值移动平均,则时间向前追溯
+	N个值(包括当前值),如果遇到空格值的日期,则自
+	动跳过空格数值,继续往前追溯<br>
+	2、如果当前日期对应的原始数据为空值,则N值移动
+	平均也为空值`],
+	[12,`环比公式:(当期-上期)/上期<br>
+	1、选择当期对应值,若是当期值为空,则环比值为空;若当期有值,上期值往 前遍历查询,默认N等于1,找最近的一个上期值;可根据设置的N值选择最 近的第N个上期值 <br>
+	2、将当期的数值与匹配到的上期值按照公式进行计算 <br>
+	3、将计算得到值填充至当期数值对应的日期,生成新的 数据序列<br>
+	4、原始数据中出现0和负值时,提示该指标不能进行环比运算`],
+	[13,`环差公式:当期-上期<br>
+	1、选择当期对应值,若是当期值为空,则环差值为空;若当期有值,上期值往 前遍历查询,默认N等于1,找最近的一个上期值;可根据设置的N值选择最 近的第N个上期值找到最近的一个上期值即可<br>
+	2、将当期的数值与匹配到的上期值按照公式进行计算<br>
+	3、将计算得到值填充至当期数值对应的日期,生成新的数据序列`],
+	[14,`升频:支持转换所有频度指标为日度指标<br>
+	1、规则:若将月度指标转换为日度指标,如当前月为9月,但9月数据还未更新,假设9月数据是9.30更新,则9.1~9.29的数据等于8.31的数据<br>
+	2、其他频度规则相同
+	`],
+	['joint',`1、直接拼接说明:将指标A和指标B按照选取的拼接日期进行拼接;<br>
+	2、累计值同比拼接说明:指标A最后一个12月31日有值的年份数据乘以指标B的同比增长率得到指标A下一年的数据,从指标A最后一个12月31日有值的日期开始拼接计算得到指标A下一年的数据;`],
+	[22,`时间移位:把数据的时间序列加(领先)减(滞后)相应的时间,形成新的数据系列。<br>例:<br>
+	<p>
+		领先10天,即将指标每个数据点的日期加10天。原始数据点(2022-1-1,100)将转化成(2022-1-11,100)
+	</p>
+	<p>
+		滞后1月,即将日期减30天,原始数据点(2022-1-1,100)将转化成(2021-12-2,100)
+	</p>`],
+	[35,`计算公式:现值 - AVERAGE(过去N年同期数值和),N为取数个数<br>
+	1、计算过去N年同期数值和,则时间向前追溯 N年(包括最新一年)<br>
+	2、参与计算的指标数据通过线性方程补全为日度的数据(包括周末)<br>
+	3、遇到闰二月,如2.29,去掉该天数据<br>
+	4、进行农历计算时,只计算11月--次年5月,如果当前月为12月,则取第二年的春节为时间位移标准点<br>
+	5、计算生成的结果的数据频度与原始指标频度保持一致
+	`],
+	[37,`拟合残差:计算一个指标(B)的实际值和拟合值(B’)的差值。拟合值B’由指标A(自变量)和指标B(因变量)通过线性回归拟合得到,具体算法如下:<br>
+	根据指标A(自变量)和指标B(因变量)过去一个时间段内(这个N期可以是从最新值往前倒退N期,包含最新,也可以是选取历史数据的一个时间段内的数据),生成线性回归方程 Y=aX+b<br>
+	由指标A(自变量)和拟合方程的系数a,b,计算得到拟合出来的系列B’=aA b 再计算拟合系列B’和原始系列B的差值得到新的数据系列Delta,Delta=B-B'
+	`],
+	[40,`数据调整:将所选指标的历史数据经过调整后保存,该指标更新时将在调整数据的基础上更新最新数据。<br>
+	注:系统只取A、B列数据,且调整数据的日期不得晚于指标的实际最新日期
+	`],
+	[52,`年化值=S / a (S表示指标数值,a表示年化平均占比)<br>
+	1、读取指标最新值对应的日期T和指标数值S<br>
+	2、计算该指标在过去三年日期T对应的值与当年最后一个日期对应的值的比值,即截止到日期T的累计值占全年值的比重<br>
+	3、计算三年的占比平均值,即过去三年平均占比a<br>
+	4、若历史数据不足三年,则至少按两年计算,少于两年不生成年化值<br>
+	5、如果某一年日期T没有值,则通过日期T前后的两个值,用线性插值法【(Y-Y1)/(X-X1)=(Y2-Y1)/(X2-X1)】计算得到日期T的值
+	`],
+	[51,`降频:将高频指标转换为低频指标,需选择转换的频度<br>
+	1、选择转换的频度,日度可以选择降频成周度、旬度、月度、季度、年度;周度可以选择降频成旬度、月度、季度、年度;旬度可以选择降频成月度、季度、年度;月度可降成季度、年度,季度可降成年度<br>
+	2、降频后数据日期,周度的降频数据系列日期取每周五;旬度取每月10日、20日、最后一日;月度取每月最后一天,季度取季度最后一天,年度取年度最后一天<br>
+	3、数据点取值提供两种选择:a、期末值,取区间最后一个日期的数据值。b、取区间平均值。<br>
+	4、最新值的处理:最新的高频数据,正好处于所要降频的低频的两个日期之间时,例如当前1月3号,1月已经有了数据,但1月31号还未到来,则降成月频数据时最新数据日期为1月31号`],
+	[53,`扩散指数:AVERAGE(所选指标环差指数和)<br>
+	1、选择多个指标,设定扩散指标日期并集<br>
+	2、所选指标在日期并集内有缺失值的用前期值填充<br>
+	3、计算日期并集内所选指标的每期环差指数(环差>0,取1,环差=0,取0.5,环差<0,取0),并计算环差指数均值`],
+	['accumulate',`1、累计值计算方法:<br>
+	日度转周度:日期选周五,计算上周六到本周五的日度值的加总,最新日期为最新值对应的周五。<br>
+	日度转月度:日期选每个月最后一天,计算当月所有日度值的加总,最新日期为最新值对应当月最后一天。<br>
+	日度转季度、年度:方法类似转月度。<br>
+	周度转月度/季度/年度:将周度值转成日度,空值用插值法插值,计算当月/当季/当年所有值的加总,然后除以7。<br>
+	月度转季度/年度: 当季/当年月度值相加。<br>
+	以此类推 特别说明:旬度指标可以转成更低频指标,更高频指标不能转成旬度<br>
+	2、年初至今计算方法:<br>
+	日度数据年初至今:日期同原日度数据。将每年1月1日(含)到日度数据所在日期(含)之间的日度值,进行加总。<br>
+	周度数据年初至今:日期同原周度数据。将周度值转成日度频率,空值用插值法插值,然后算法同日度年度至今,再除以7<br>
+	月度/季度数据年初至今:日期同原月度/季度数据,将每年1月1日(含)到月度数据所在日期(含)之间的月度/季度值,进行加总<br>
+	以此类推`],
+	[61,`累计值转季值计算方法:<br>
+	<p style="display:flex;justify-content: space-between;">
+		<span style="width:45%">1季度无值:1季度=2季度/2 </span>
+		<span style="width:40%">1季度有值:1季度=1季度</span>
+	</p>
+	<p style="display:flex;justify-content: space-between;">
+		<span style="width:40%">2季度=2季度/2</span>
+		<span style="width:40%">2季度=2季度-1季度</span>
+	</p>
+	<p style="display:flex;justify-content: space-between;">
+		<span style="width:40%">3季度=3季度-2季度</span>
+		<span style="width:40%">3季度=3季度-2季度</span>
+	</p>
+	<p style="display:flex;justify-content: space-between;">
+		<span style="width:40%">4季度=4季度-3季度</span>
+		<span style="width:40%">4季度=4季度-3季度</span>
+	</p>
+	<p style="display:flex;justify-content: space-between;">
+		<span style="width:40%">以此类推</span>
+		<span style="width:40%">以此类推</span>
+	</p>
+	<p>特别说明:若1季度和2季度均无值,则该年不做计算</p>`],
+	[62,`累计值计算方法:<br>
+	日度转周度:日期选周五,计算上周六到本周五的日度值的加总,最新日期为最新值对应的周五。<br>
+	日度转月度:日期选每个月最后一天,计算当月所有日度值的加总,最新日期为最新值对应当月最后一天。<br>
+	日度转季度、年度:方法类似转月度。<br>
+	周度转月度/季度/年度:将周度值转成日度,空值用插值法插值,计算当月/当季/当年所有值的加总,然后除以7。<br>
+	月度转季度/年度: 当季/当年月度值相加。<br>
+	以此类推 特别说明:旬度指标可以转成更低频指标,更高频指标不能转成旬度<br>`],
+	[63,`年初至今计算方法:<br>
+	日度数据年初至今:日期同原日度数据。将每年1月1日(含)到日度数据所在日期(含)之间的日度值,进行加总。<br>
+	周度数据年初至今:日期同原周度数据。将周度值转成日度频率,空值用插值法插值,然后算法同日度年度至今,再除以7<br>
+	月度/季度数据年初至今:日期同原月度/季度数据,将每年1月1日(含)到月度数据所在日期(含)之间的月度/季度值,进行加总<br>
+	以此类推`],
+	[72,`指数修匀计算公式:<br>
+	1、设定指数修匀值序列的初始值=原来时间序列的初始值 <br>
+	2、选择平滑系数alpha值:在0-1之间,开区间 <br>
+	3、本期指数修匀值=alpha*本期实际值+(1-alpha)*上期指数修匀值`]
+])
+
+// 常规计算类型
+export const calculateType=[
+    {
+		name:'指标运算',
+		type:4,
+	},
+	{
+		name:'累计值转月/季值',
+		type:'toMonthSeason',
+    },
+	{
+		name:'同比值',
+		type:6
+	},
+	{
+		name:'同差值',
+		type:7
+	},
+	{
+		name:'N数值移动平均计算',
+		type:8
+	},
+	{
+		name:'N数值环比值',
+		type:12
+	},
+	{
+		name:'N数值环差值',
+		type:13
+	},
+	{
+		name:'升频',
+		type:14
+	},
+	{
+		name:'指标拼接',
+		type: 'joint'
+	},
+	{
+		name: '时间移位',
+		type: 22
+	},
+	{
+		name: '超季节性',
+		type: 35
+	},
+	{
+		name: '拟合残差',
+		type: 37
+	},
+	{
+		name: '年化',
+		type: 52
+	},
+	{
+		name: '降频',
+		type: 51
+	},
+	{
+		name: '扩散指数',
+		type: 53
+	},
+	{
+		name: '累计值',
+		type: 'accumulate'
+	},
+	{
+		name: '指数修匀',
+		type: 72
+	},
+]
+
+// 批量计算类型
+export const calculateBatchType=[
+    {
+		name:'同比值',
+		type:6
+	},
+	{
+		name:'同差值',
+		type:7
+	},
+	{
+		name:'N数值移动平均计算',
+		type:8
+	},
+	{
+		name:'N数值环比值',
+		type:12
+	},
+	{
+		name:'N数值环差值',
+		type:13
+	},
+	{
+		name:'升频',
+		type:14
+	},
+	{
+		name:'累计值转月/季值',
+		type:'toMonthSeason'
+	},
+	{
+		name: '累计值',
+		type: 'accumulate'
+	},
+	{
+		name: '指数修匀',
+		type: 72
+	},
+]

+ 2 - 1
src/views/tabbar/Home.vue

@@ -184,7 +184,8 @@ const menuOpts=computed(()=>{
         }
         if(item.level===2){
             resMenuList.value.forEach(f=>{
-                f.children.forEach(s=>{
+                const arr=f.children||[]
+                arr.forEach(s=>{
                     if(s.name.trim()===item.key){
                         item.show=true
                     }

+ 19 - 1
vite.config.js

@@ -4,6 +4,7 @@ import path from "path";
 import Components from "unplugin-vue-components/vite";
 import { VantResolver } from "unplugin-vue-components/resolvers";
 import VueSetupExtend from 'vite-plugin-vue-setup-extend'
+import {createSvgIconsPlugin} from 'vite-plugin-svg-icons'
 
 // https://vitejs.dev/config/
 export default ({ mode }) =>
@@ -14,7 +15,24 @@ export default ({ mode }) =>
       Components({
         resolvers: [VantResolver()],
       }),
-      VueSetupExtend()
+      VueSetupExtend(),
+      createSvgIconsPlugin({
+        // 指定需要缓存的图标文件夹
+        iconDirs: [path.resolve(process.cwd(), 'src/assets/svg')],
+        // 指定symbolId格式
+        symbolId: '[name]',
+        /**
+         * 自定义插入位置
+         * @default: body-last
+         */
+        // inject?: 'body-last' | 'body-first'
+  
+        /**
+         * custom dom id
+         * @default: __svg__icons__dom__
+         */
+        // customDomId: '__svg__icons__dom__'
+      })
     ],
     css: {
       // css预处理器