Эх сурвалжийг харах

add: 性能验证功能模块初始化

cfort 1 жил өмнө
parent
commit
42af1c0f36

+ 195 - 0
src/api/business/pv.js

@@ -0,0 +1,195 @@
+import request from '@/utils/request'
+import { BUSINESS_BASE_URL } from '@/api/baseUrl'
+
+/**
+ * 获取性能验证实验数据
+ * @param {*} params
+ */
+export function queryExperimental (data) {
+    return request({
+        url: BUSINESS_BASE_URL() + '/pv/experimental/query',
+        method: 'post',
+        data
+    })
+}
+
+/**
+ * 获取性能验证实验数据详情
+ * @param {*} params
+ */
+export function getExperimental (params) {
+    return request({
+        url: BUSINESS_BASE_URL() + '/pv/experimental/get',
+        method: 'get',
+        params
+    })
+}
+
+/**
+ * 保存性能验证实验数据
+ * @param {*} params
+ */
+export function saveExperimental (data) {
+    return request({
+        url: BUSINESS_BASE_URL() + '/pv/experimental/save',
+        method: 'post',
+        data
+    })
+}
+
+/**
+ * 删除性能验证实验数据
+ * @param {*} params
+ */
+export function removeExperimental (params) {
+    return request({
+        url: BUSINESS_BASE_URL() + '/pv/experimental/remove',
+        method: 'post',
+        params
+    })
+}
+
+/**
+ * 获取性能验证实验试剂数据
+ * @param {*} params
+ */
+export function getReagentList (data) {
+    return request({
+        url: BUSINESS_BASE_URL() + '/pv/reagent/query',
+        method: 'post',
+        data
+    })
+}
+
+/**
+ * 获取性能验证实验试剂数据详情
+ * @param {*} params
+ */
+export function getReagentDetail (params) {
+    return request({
+        url: BUSINESS_BASE_URL() + '/pv/reagent/get',
+        method: 'get',
+        params
+    })
+}
+
+/**
+ * 保存性能验证实验数据
+ * @param {*} params
+ */
+export function saveReagent (data) {
+    return request({
+        url: BUSINESS_BASE_URL() + '/pv/reagent/save',
+        method: 'post',
+        data
+    })
+}
+
+/**
+ * 删除性能验证实验数据
+ * @param {*} params
+ */
+export function removeReagent (data) {
+    return request({
+        url: BUSINESS_BASE_URL() + '/pv/reagent/remove',
+        method: 'post',
+        data
+    })
+}
+
+/**
+ * 获取性能验证配置数据
+ * @param {*} params
+ */
+export function getConfigList (data) {
+    return request({
+        url: BUSINESS_BASE_URL() + '/pv/experimentalConfig/query',
+        method: 'post',
+        data
+    })
+}
+
+/**
+ * 获取性能验证配置数据详情
+ * @param {*} params
+ */
+export function getConfigDetail (params) {
+    return request({
+        url: BUSINESS_BASE_URL() + '/pv/experimentalConfig/get',
+        method: 'get',
+        params
+    })
+}
+
+/**
+ * 保存性能验证配置数据
+ * @param {*} params
+ */
+export function saveConfig (data) {
+    return request({
+        url: BUSINESS_BASE_URL() + '/pv/experimentalConfig/save',
+        method: 'post',
+        data
+    })
+}
+
+/**
+ * 删除性能验证配置数据
+ * @param {*} params
+ */
+export function removeConfig (data) {
+    return request({
+        url: BUSINESS_BASE_URL() + '/pv/experimentalConfig/remove',
+        method: 'post',
+        data
+    })
+}
+
+/**
+ * 导出数据模板
+ * @param {*} params
+ */
+export function exportTemplate (params) {
+    return request({
+        url: BUSINESS_BASE_URL() + '/pv/exportExcelTemplate',
+        responseType: 'arraybuffer',
+        method: 'post',
+        params
+    })
+}
+
+/**
+ * 导入实验数据
+ * @param {*} params
+ */
+export function importTemplate (data) {
+    return request({
+        url: BUSINESS_BASE_URL() + '/pv/importExcelRecord',
+        method: 'post',
+        data
+    })
+}
+
+/**
+ * 实验数据计算
+ * @param {*} params
+ */
+export function recalculate (params) {
+    return request({
+        url: BUSINESS_BASE_URL() + '/pv/recalculate',
+        method: 'post',
+        params
+    })
+}
+
+/**
+ * 导出数据报告
+ * @param {*} params
+ */
+export function exportReport (data) {
+    return request({
+        url: BUSINESS_BASE_URL() + '/pv/exportExcelReport',
+        method: 'post',
+        data
+    })
+}

+ 5 - 0
src/assets/styles/fixed/element.scss

@@ -135,4 +135,9 @@
 .ibps-signature-canvas {
   width: 100%;
   height:97%;
+}
+
+.el-message-box > .el-message-box__content {
+    max-height: 320px;
+    overflow: auto;
 }

+ 315 - 0
src/views/business/performance/components/basic-info.vue

@@ -0,0 +1,315 @@
+<template>
+    <div class="info-container">
+        <div class="info-item">
+            <div class="title">
+                <i class="ibps-icon-star" />
+                <span>实验基础信息</span>
+            </div>
+            <div class="form-container">
+                <el-row :gutter="20" class="form-row">
+                    <el-col :span="12">
+                        <el-form-item label="部门" prop="bianZhiBuMen" :show-message="false">
+                            <el-select
+                                v-model="pageInfo.bianZhiBuMen"
+                                filterable
+                                clearable
+                                :disabled="readonly"
+                                placeholder="请选择部门"
+                            >
+                                <el-option
+                                    v-for="item in deptList"
+                                    :key="item.positionId"
+                                    :label="item.positionName"
+                                    :value="item.positionId"
+                                />
+                            </el-select>
+                        </el-form-item>
+                    </el-col>
+                    <el-col :span="12">
+                        <el-form-item label="实验项目" prop="shiYanXiangMu" :show-message="false">
+                            <el-input
+                                v-model="pageInfo.shiYanXiangMu"
+                                type="text"
+                                clearable
+                                show-word-limit
+                                :maxlength="64"
+                                :disabled="readonly"
+                                placeholder="请输入实验项目"
+                            />
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+                <el-row :gutter="20" class="form-row">
+                    <el-col :span="12">
+                        <el-form-item label="实验方法" prop="shiYanFangFa" :show-message="false">
+                            <el-input
+                                v-model="pageInfo.shiYanFangFa"
+                                type="text"
+                                clearable
+                                show-word-limit
+                                :maxlength="64"
+                                :disabled="readonly"
+                                placeholder="请输入实验方法"
+                            />
+                        </el-form-item>
+                    </el-col>
+                    <el-col :span="12">
+                        <el-form-item label="样本类型" prop="yangBenLeiXing" :show-message="false">
+                            <el-input
+                                v-model="pageInfo.yangBenLeiXing"
+                                type="text"
+                                clearable
+                                show-word-limit
+                                :maxlength="64"
+                                :disabled="readonly"
+                                placeholder="请输入样本类型"
+                            />
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+                <el-row :gutter="20" class="form-row">
+                    <el-col :span="12">
+                        <el-form-item label="实验仪器" prop="shiYanYiQi" :show-message="false">
+                            <el-input
+                                v-model="pageInfo.shiYanYiQi"
+                                type="text"
+                                clearable
+                                show-word-limit
+                                :maxlength="64"
+                                :disabled="readonly"
+                                placeholder="请输入实验仪器"
+                            />
+                        </el-form-item>
+                    </el-col>
+                    <el-col :span="12">
+                        <el-form-item label="仪器编号" prop="yiQiBianHao" :show-message="false">
+                            <el-input
+                                v-model="pageInfo.yiQiBianHao"
+                                type="text"
+                                clearable
+                                show-word-limit
+                                :maxlength="64"
+                                :disabled="readonly"
+                                placeholder="请输入仪器编号"
+                            />
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+                <el-row :gutter="20" class="form-row">
+                    <el-col :span="12">
+                        <el-form-item label="开始时间" prop="kaiShiShiJian" :show-message="false">
+                            <el-date-picker
+                                v-model="pageInfo.kaiShiShiJian"
+                                type="datetime"
+                                clearable
+                                align="right"
+                                value-format="yyyy-MM-dd HH:mm"
+                                format="yyyy-MM-dd HH:mm"
+                                class="date-picker"
+                                :picker-options="startPickerOptions"
+                                :disabled="readonly"
+                                placeholder="请选择实验开始时间"
+                            />
+                        </el-form-item>
+                    </el-col>
+                    <el-col :span="12">
+                        <el-form-item label="结束时间" prop="jieShuShiJian" :show-message="false">
+                            <el-date-picker
+                                v-model="pageInfo.jieShuShiJian"
+                                type="datetime"
+                                clearable
+                                align="right"
+                                value-format="yyyy-MM-dd HH:mm"
+                                format="yyyy-MM-dd HH:mm"
+                                class="date-picker"
+                                :picker-options="endPickerOptions"
+                                :disabled="readonly"
+                                placeholder="请选择实验结束时间"
+                            />
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+                <el-row :gutter="20" class="form-row">
+                    <el-col :span="12">
+                        <el-form-item label="实验操作者" prop="bianZhiRen" :show-message="false">
+                            <el-select
+                                v-model="pageInfo.bianZhiRen"
+                                filterable
+                                clearable
+                                :disabled="readonly"
+                                placeholder="请选择实验操作者"
+                            >
+                                <el-option
+                                    v-for="item in userList"
+                                    :key="item.userId"
+                                    :label="item.userName"
+                                    :value="item.userId"
+                                />
+                            </el-select>
+                        </el-form-item>
+                    </el-col>
+                    <el-col :span="12">
+                        <el-form-item label="评价创建者" prop="createBy" :show-message="false">
+                            <el-select
+                                v-model="pageInfo.createBy"
+                                filterable
+                                clearable
+                                :disabled="readonly"
+                                placeholder="请选择评价创建者"
+                            >
+                                <el-option
+                                    v-for="item in userList"
+                                    :key="item.userId"
+                                    :label="item.userName"
+                                    :value="item.userId"
+                                />
+                            </el-select>
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+                <el-row :gutter="20" class="form-row">
+                    <el-col :span="12">
+                        <el-form-item label="结果单位" prop="jieGuoDanWei" :show-message="false">
+                            <el-input
+                                v-model="pageInfo.jieGuoDanWei"
+                                type="text"
+                                clearable
+                                show-word-limit
+                                :maxlength="16"
+                                :disabled="readonly"
+                                placeholder="请输入结果单位"
+                            />
+                        </el-form-item>
+                    </el-col>
+                    <el-col :span="12">
+                        <el-form-item label="保留小数位" prop="baoLiuXiaoShu" :show-message="false">
+                            <el-input-number
+                                v-model="pageInfo.baoLiuXiaoShu"
+                                type="number"
+                                :min="1"
+                                :max="4"
+                                :precision="0"
+                                :disabled="readonly"
+                                placeholder="请选择保留小数位"
+                            />
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+                <el-row :gutter="20" class="form-row">
+                    <el-col :span="24">
+                        <el-form-item label="备注" prop="beiZhu" :show-message="false">
+                            <el-input
+                                v-model="pageInfo.beiZhu"
+                                type="textarea"
+                                clearable
+                                show-word-limit
+                                :maxlength="512"
+                                :rows="1"
+                                :autosize="readonly"
+                                :disabled="readonly"
+                                placeholder="请输入其他备注信息"
+                            />
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+            </div>
+        </div>
+    </div>
+</template>
+<script>
+export default {
+    props: {
+        info: {
+            type: Object,
+            default: () => {}
+        },
+        readonly: {
+            type: Boolean,
+            default: false
+        }
+    },
+    data () {
+        const that = this
+        const { userList = [], deptList = [], userId } = this.$store.getters || {}
+        return {
+            userList,
+            deptList: deptList.filter(i => i.depth === 4),
+            pageInfo: this.info,
+            startPickerOptions: {
+                disabledDate (time) {
+                    const t = new Date(time)
+                    // 禁用结束日期之后日期
+                    if (that.info.jieShuShiJian) {
+                        const end = new Date(that.info.jieShuShiJian)
+                        return t.getTime() > end.getTime()
+                    }
+                    return t.getTime() >= Date.now()
+                }
+            },
+            endPickerOptions: {
+                disabledDate (time) {
+                    const t = new Date(time)
+                    // 禁用当前日期之后和开始日期之前的日期
+                    if (that.info.kaiShiShiJian) {
+                        const start = new Date(that.info.kaiShiShiJian)
+                        return t.getTime() < start.getTime() || t.getTime() >= Date.now()
+                    }
+                    return t.getTime() >= Date.now()
+                }
+            }
+        }
+    },
+    watch: {
+        info: {
+            handler (val) {
+                // 浅拷贝,数据父子组件间双向传递
+                this.pageInfo = val || {}
+            },
+            immediate: true,
+            deep: true
+        }
+    }
+}
+</script>
+<style lang="scss" scoped>
+    .info-container {
+        .info-item {
+            .form-container {
+                padding: 10px;
+                background: #f5f5f5;
+                border: 1px solid #e6e6e6;
+                border-radius: 4px;
+                overflow: hidden;
+                .form-row {
+                    padding: 5px 0;
+                    border-top: 1px solid #e6e6e6;
+                    &:first-child {
+                        border-top: none;
+                        padding-top: 0;
+                    }
+                    &:last-child {
+                        padding-bottom: 0;
+                    }
+                    ::v-deep {
+                        .el-form-item {
+                            margin-bottom: 0 !important;
+                            .el-form-item__label {
+                                font-size: 14px !important;
+                                color: #606266;
+                            }
+                            .el-form-item__content {
+                                .el-input, .el-select, .el-input-number {
+                                    width: 100%;
+                                }
+                                .el-textarea .el-input__count {
+                                    padding: 0 5px;
+                                    line-height: initial;
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+</style>

+ 96 - 0
src/views/business/performance/components/chart.vue

@@ -0,0 +1,96 @@
+<template>
+    <div class="info-container">
+        <div class="chart info-item">
+            <div class="title">
+                <i class="ibps-icon-star" />
+                <span>图表</span>
+            </div>
+            <div class="chart-container">
+                <el-row :gutter="20" class="form-row">
+                    <el-col v-for="(chart, index) in chartData" :key="index" :xl="12" :lg="12" :md="24">
+                        <div :id="chart.id" class="chart-item" />
+                        <div class="note">{{ chart.note }}</div>
+                    </el-col>
+                </el-row>
+            </div>
+        </div>
+    </div>
+</template>
+<script>
+import * as echarts from 'echarts/core'
+import {
+    DatasetComponent,
+    TitleComponent,
+    TooltipComponent,
+    GridComponent,
+    TransformComponent,
+    MarkLineComponent
+} from 'echarts/components'
+import { ScatterChart, LineChart } from 'echarts/charts'
+import { UniversalTransition, LabelLayout } from 'echarts/features'
+import { CanvasRenderer } from 'echarts/renderers'
+import ecStat from 'echarts-stat'
+
+echarts.use([
+    DatasetComponent,
+    TitleComponent,
+    TooltipComponent,
+    GridComponent,
+    MarkLineComponent,
+    TransformComponent,
+    ScatterChart,
+    LineChart,
+    CanvasRenderer,
+    UniversalTransition,
+    LabelLayout
+])
+echarts.registerTransform(ecStat.transform.regression)
+import { LROption, SPOption } from '../constants/options'
+
+export default {
+    props: {
+        chartData: {
+            type: Array,
+            default: () => []
+        }
+    },
+    data () {
+        return {
+
+        }
+    },
+    mounted () {
+        this.initChart()
+    },
+    methods: {
+        initChart () {
+            console.log(LROption, SPOption)
+            this.chartData.forEach(item => {
+                const chart = echarts.init(document.getElementById(item.id))
+                console.log(item.option)
+                // const option = JSON.parse(item.option)
+                // eslint-disable-next-line no-eval
+                const option = eval('(' + item.option + ')')
+                option.title.text = item.title
+                chart.setOption(option)
+            })
+        }
+    }
+}
+</script>
+<style lang="scss" scoped>
+    .info-container {
+        .chart {
+            .chart-container {
+                // white-space: pre-wrap;
+                // color: #606266;
+                // font-size: 14px;
+                // line-height: 1.5;
+                .chart-item {
+                    height: 300px;
+                    width: 100%;
+                }
+            }
+        }
+    }
+</style>

+ 111 - 0
src/views/business/performance/components/conclusion.vue

@@ -0,0 +1,111 @@
+<template>
+    <div class="info-container">
+        <div class="conclusion info-item">
+            <div class="title">
+                <i class="ibps-icon-star" />
+                <span>实验结论</span>
+            </div>
+            <div v-if="$utils.isNotEmpty(expResult)" class="form-container">
+                <el-row :gutter="20" class="form-row">
+                    <el-col :span="24">
+                        <el-form-item label="结论" prop="shiYanJieLun" :show-message="false">
+                            <ibps-ueditor v-model="expResult" :config="ueditorConfig" :readonly="true" />
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+                <el-row :gutter="20" class="form-row">
+                    <el-col :span="24">
+                        <el-form-item label="实验附件" prop="fuJian" :show-message="false">
+                            <ibps-attachment
+                                v-model="expFiles"
+                                allow-download
+                                download
+                                multiple
+                                accept="*"
+                                store="id"
+                                :readonly="readonly"
+                            />
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+            </div>
+            <el-empty v-else description="导入实验数据后查看" />
+        </div>
+    </div>
+</template>
+<script>
+export default {
+    components: {
+        IbpsAttachment: () => import('@/business/platform/file/attachment/selector'),
+        IbpsUeditor: () => import('@/components/ibps-ueditor')
+    },
+    props: {
+        result: {
+            type: String,
+            default: ''
+        },
+        files: {
+            type: String,
+            default: ''
+        },
+        readonly: {
+            type: Boolean,
+            default: false
+        }
+    },
+    data () {
+        const { userList = [] } = this.$store.getters || {}
+        return {
+            userList,
+            expResult: this.result,
+            expFiles: this.files,
+            pageInfo: null,
+            ueditorConfig: {
+                autoHeightEnabled: true,
+                initialFrameHeight: 240,
+                initialFrameWidth: '100%',
+                initialStyle: 'body { font-size: 14px; }'
+            }
+        }
+    },
+    // computed: {
+    //     expResult () {
+    //         return this.result
+    //     }
+    // },
+    watch: {
+        result (val) {
+            this.expResult = val
+        },
+        expFiles: {
+            handler (val, oldVal) {
+                this.$emit('updateData', { fuJian: val })
+            }
+        }
+    }
+}
+</script>
+<style lang="scss" scoped>
+    .info-container {
+        .info-item {
+            .form-container {
+                padding: 10px;
+                background: #f5f5f5;
+                border: 1px solid #e6e6e6;
+                border-radius: 4px;
+                overflow: hidden;
+                .form-row {
+                    padding: 5px 0;
+                    border-top: 1px solid #e6e6e6;
+                    &:first-child {
+                        border-top: none;
+                        padding-top: 0;
+                    }
+                    &:last-child {
+                        padding-bottom: 0;
+                    }
+                }
+            }
+        }
+    }
+</style>

+ 232 - 0
src/views/business/performance/components/experimental-data.vue

@@ -0,0 +1,232 @@
+<template>
+    <div class="info-container">
+        <div class="experimental-data info-item">
+            <div class="title">
+                <i class="ibps-icon-star" />
+                <span>实验数据</span>
+            </div>
+            <div class="operate">
+                <template v-for="btn in toolbars">
+                    <el-button
+                        v-if="!btn.hidden"
+                        :key="btn.key"
+                        :type="btn.type"
+                        :icon="btn.icon"
+                        :size="btn.size || 'mini'"
+                        plain
+                        @click="handleActionEvent(btn.key)"
+                    >
+                        {{ btn.label }}
+                    </el-button>
+                </template>
+            </div>
+            <div class="content">
+                <template v-if="expData && $utils.isNotEmpty(expData.dataDTO)">
+                    <div class="note" v-html="expData.dataDTO.note" />
+                    <el-table
+                        :data="expData.dataDTO.list"
+                        border
+                        stripe
+                        highlight-current-row
+                        style="width: 100%"
+                        max-height="250px"
+                    >
+                        <el-table-column
+                            v-for="(h, hIndex) in expData.dataDTO.header"
+                            :key="h.children && h.children.length ? hIndex : h.prop"
+                            :prop="h.prop"
+                            :label="h.label"
+                            width="80"
+                            header-align="center"
+                            align="center"
+                        >
+                            <template slot="header" slot-scope="scope">
+                                <span v-html="scope.column.label" />
+                            </template>
+                            <el-table-column
+                                v-for="c in h.children"
+                                :key="c.prop"
+                                :prop="c.prop"
+                                :label="c.label"
+                                header-align="center"
+                                align="center"
+                            >
+                                <template slot="header" slot-scope="scope">
+                                    <span v-html="scope.column.label" />
+                                </template>
+                            </el-table-column>
+                        </el-table-column>
+                    </el-table>
+                </template>
+                <el-empty v-else description="暂无数据,请导出模板填写后导入" />
+            </div>
+        </div>
+        <!-- <import-table
+            :visible="showImportTable"
+            title="导入"
+            @close="visible => (showImportTable = visible)"
+            @action-event="handleImport"
+        /> -->
+    </div>
+</template>
+<script>
+// import IbpsImport from '@/plugins/import'
+import ActionUtils from '@/utils/action'
+import { exportTemplate, importTemplate } from '@/api/business/pv'
+
+export default {
+    components: {
+        ImportTable: () => import('@/business/platform/form/formrender/dynamic-form/components/import-table')
+    },
+    props: {
+        expData: {
+            type: Object,
+            default: () => {}
+        },
+        formId: {
+            type: String,
+            default: ''
+        },
+        readonly: {
+            type: Boolean,
+            default: false
+        }
+    },
+    data () {
+        return {
+            showImportTable: false,
+            toolbars: [
+                { key: 'export', icon: 'ibps-icon-cloud-download', label: '导出模板', type: 'info', hidden: this.readonly },
+                { key: 'import', icon: 'ibps-icon-cloud-upload', label: '导入数据', type: 'warning', hidden: this.readonly },
+                { key: 'generate', icon: 'ibps-icon-file-text-o', label: '查看实验报告', type: 'success', hidden: true }
+            ]
+        }
+    },
+    methods: {
+        handleActionEvent (key) {
+            switch (key) {
+                case 'generate':
+                    this.handleGenerate()
+                    break
+                case 'export':
+                    // this.handleExport()
+                    this.$emit('export')
+                    break
+                case 'import':
+                    // this.showImportTable = true
+                    // this.handleImport()
+                    this.$emit('import')
+                    break
+            }
+        },
+        handleGenerate () {
+            this.$message.info('waiting...')
+        },
+        handleExport () {
+            if (!this.formId) {
+                return this.$message.error('请先保存数据!')
+            }
+            exportTemplate({ id: this.formId }).then(res => {
+                ActionUtils.download(res.data, '实验数据模板.xlsx')
+            })
+        },
+        objectSpanMethod ({ row, column, rowIndex, columnIndex }) {
+            // 判断当前列是否需要进行同值合并
+            if (column.merge) {
+                const firstSameRowIndex = this.getFirstSameRowIndex(this.tableData, rowIndex, column.property)
+                const firstSameColIndex = this.getFirstSameColIndex(this.tableData, columnIndex, rowIndex)
+
+                return {
+                    rowspan: firstSameRowIndex === -1 ? 1 : rowIndex - firstSameRowIndex + 1,
+                    colspan: firstSameColIndex === -1 ? 1 : columnIndex - firstSameColIndex + 1
+                }
+            }
+        },
+        // 获取行同值合并的起始下标
+        getFirstSameRowIndex (data, rowIndex, prop) {
+            for (let i = rowIndex; i >= 0; i--) {
+                if (data[i][prop] === data[rowIndex][prop]) {
+                    return i
+                }
+            }
+            return -1
+        },
+        // 获取列同值合并的起始下标
+        getFirstSameColIndex (data, colIndex, rowIndex) {
+            for (let i = colIndex; i >= 0; i--) {
+                if (data[rowIndex][i] === data[rowIndex][colIndex]) {
+                    return i
+                }
+            }
+            return -1
+        },
+        // handleImport (file, options) {
+        //     this.loading = false
+        //     IbpsImport.xlsx(file, options).then(res => {
+        //         console.log(res)
+        //     })
+        // },
+        handleImport () {
+            if (!this.formId) {
+                return this.$message.error('请先保存数据!')
+            }
+            const input = document.createElement('input')
+            input.type = 'file'
+            input.accept = '.xlsx'
+            input.onchange = event => {
+                const file = event.target.files[0]
+                const reader = new FileReader()
+                reader.onload = event => {
+                    const data = new FormData()
+                    data.append('id', this.formId)
+                    data.append('applyFiles', file)
+                    importTemplate(data).then(res => {
+                        this.$message.success('实验数据导入成功')
+                        this.expData = res.data
+                    }).catch(({ state, cause }) => {
+                        const errMsg = JSON.parse(cause)
+                        let msgContent = ''
+                        Object.keys(errMsg).forEach(key => {
+                            let msgItem = '<div >'
+                            errMsg[key].forEach(item => {
+                                msgItem += `<div>${item}</div>`
+                            })
+                            msgContent += `<div><div style="font-weight: bold;">${key}:</div>${msgItem}<div>`
+                        })
+                        this.$confirm(`<div style="font-size: 14px;">${msgContent}</div>`, '数据校验失败,请根据以下提示完善您的数据!', {
+                            confirmButtonText: '确认',
+                            showClose: false,
+                            showCancelButton: false,
+                            closeOnClickModal: false,
+                            dangerouslyUseHTMLString: true,
+                            customClass: 'errorTips',
+                            type: 'error'
+                        }).then(() => {}).catch(() => {})
+                    })
+                }
+                reader.readAsBinaryString(file)
+            }
+            input.click()
+        }
+    }
+}
+</script>
+<style lang="scss" scoped>
+    .info-container {
+        .experimental-data {
+            position: relative;
+            .operate {
+                position: absolute;
+                right: 0;
+                top: -2px;
+            }
+            .content {
+                padding: 10px;
+                position: relative;
+                .note {
+                    margin-bottom: 10px;
+                }
+            }
+        }
+    }
+</style>

+ 111 - 0
src/views/business/performance/components/experimental-desc.vue

@@ -0,0 +1,111 @@
+<template>
+    <div class="info-container">
+        <div class="design info-item">
+            <div class="title">
+                <i class="ibps-icon-star" />
+                <span>实验步骤</span>
+            </div>
+            <!-- <div class="step">{{ step }}</div> -->
+            <div class="step" v-html="step" />
+        </div>
+        <div class="design info-item">
+            <div class="title">
+                <i class="ibps-icon-star" />
+                <span>判断标准</span>
+            </div>
+            <div class="step" v-html="criterion" />
+        </div>
+        <div v-if="formulas.length" class="design info-item">
+            <div class="title">
+                <i class="ibps-icon-star" />
+                <span>实验公式</span>
+            </div>
+            <div class="formula-box">
+                <div v-for="item in formulas" :key="item.key" class="formula-item">
+                    <div>{{ item.label }}</div>
+                    <div>{{ item.value }}</div>
+                </div>
+            </div>
+        </div>
+        <div v-if="references" class="design info-item">
+            <div class="title">
+                <i class="ibps-icon-star" />
+                <span>参考资料</span>
+            </div>
+            <ibps-attachment
+                v-model="references"
+                allow-download
+                download
+                multiple
+                accept="*"
+                store="id"
+                readonly
+            />
+        </div>
+    </div>
+</template>
+<script>
+import MathJax from '@/utils/MathJax'
+export default {
+    components: {
+        IbpsAttachment: () => import('@/business/platform/file/attachment/selector')
+    },
+    props: {
+        step: {
+            type: String,
+            default: ''
+        },
+        criterion: {
+            type: String,
+            default: ''
+        },
+        formulas: {
+            type: Array,
+            default: () => []
+        },
+        references: {
+            type: String,
+            default: ''
+        }
+    },
+    mounted () {
+        this.$nextTick(() => {
+            this.formatMath()
+        })
+    },
+    methods: {
+        formatMath () {
+            setTimeout(() => {
+                if (MathJax.isMathjaxConfig) {
+                    MathJax.initMathjaxConfig()
+                }
+                MathJax.MathQueue('.formula-box')
+            }, 500)
+        }
+    }
+}
+</script>
+<style lang="scss" scoped>
+    .info-container {
+        .design {
+            margin-bottom: 20px;
+            &:last-child {
+                margin-bottom: 0;
+            }
+            .step {
+                white-space: pre-wrap;
+                color: #606266;
+                font-size: 14px;
+                line-height: 1.5;
+            }
+            .formula-item {
+                display: flex;
+                justify-content: center;
+                align-items: center;
+                > div:nth-child(2) {
+                    margin: 0 10px;
+                }
+            }
+        }
+    }
+</style>

+ 89 - 0
src/views/business/performance/components/formula-preview.vue

@@ -0,0 +1,89 @@
+<template>
+    <el-dialog
+        title="公式预览"
+        :visible.sync="dialogVisible"
+        :show-close="false"
+        append-to-body
+        width="50%"
+        class="dialog formula-dialog"
+        top="30vh"
+        @close="closeDialog"
+    >
+        <div class="formula">
+            <div>{{ formula.label }}</div>
+            <div>{{ formula.value }}</div>
+        </div>
+        <div slot="footer" class="el-dialog--center">
+            <ibps-toolbar
+                :actions="toolbars"
+                @action-event="handleActionEvent"
+            />
+        </div>
+    </el-dialog>
+</template>
+
+<script>
+import MathJax from '@/utils/MathJax'
+export default {
+    props: {
+        show: {
+            type: Boolean,
+            default: false
+        },
+        formula: {
+            type: Object,
+            default: () => {}
+        }
+    },
+    data () {
+        return {
+            dialogVisible: this.show,
+            toolbars: [
+                { key: 'cancel', icon: 'el-icon-close', label: '关闭', type: 'danger' }
+            ]
+        }
+    },
+    watch: {
+        show: {
+            handler (val, oldVal) {
+                this.dialogVisible = this.show
+            },
+            immediate: true
+        }
+    },
+    mounted () {
+        this.$nextTick(() => {
+            this.formatMath()
+        })
+    },
+    methods: {
+        handleActionEvent ({ key }) {
+            switch (key) {
+                case 'cancel':
+                    this.closeDialog()
+                    break
+                default:
+                    break
+            }
+        },
+        formatMath () {
+            if (MathJax.isMathjaxConfig) {
+                MathJax.initMathjaxConfig()
+            }
+            MathJax.MathQueue('.formula')
+        },
+        closeDialog () {
+            this.$emit('update:show', false)
+        }
+    }
+}
+</script>
+<style lang="scss" scoped>
+    .formula-dialog {
+        .formula {
+            display: flex;
+            justify-content: center;
+            align-items: center;
+        }
+    }
+</style>

+ 518 - 0
src/views/business/performance/components/param-info.vue

@@ -0,0 +1,518 @@
+<template>
+    <div class="info-container">
+        <div class="info-item">
+            <div class="title">
+                <i class="ibps-icon-star" />
+                <span>性能验证实验参数</span>
+            </div>
+            <div v-if="pageInfo" class="form-container">
+                <el-row :gutter="20" class="form-row">
+                    <el-col v-if="isShow('specimensNum')" :span="12">
+                        <el-form-item :label="getAttrs('specimensNum', 'label', false)" prop="shiYanCanShu.specimensNum" :show-message="false">
+                            <el-input-number
+                                v-model="pageInfo.specimensNum"
+                                type="number"
+                                :min="getAttrs('specimensNum', 'min')"
+                                :max="getAttrs('specimensNum', 'max')"
+                                :precision="getAttrs('specimensNum', 'precision')"
+                                :disabled="readonly"
+                                placeholder="请输入"
+                                @change="handleNumChange"
+                            />
+                        </el-form-item>
+                    </el-col>
+                    <el-col v-if="isShow('repeatNum')" :span="12">
+                        <el-form-item :label="getAttrs('repeatNum', 'label', false)" prop="shiYanCanShu.repeatNum" :show-message="false">
+                            <el-input-number
+                                v-model="pageInfo.repeatNum"
+                                type="number"
+                                :min="getAttrs('repeatNum', 'min')"
+                                :max="getAttrs('repeatNum', 'max')"
+                                :precision="getAttrs('repeatNum', 'precision')"
+                                :disabled="readonly"
+                                placeholder="请输入"
+                            />
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+                <el-row :gutter="20" class="form-row">
+                    <el-col v-if="isShow('days')" :span="12">
+                        <el-form-item :label="getAttrs('days', 'label', false)" prop="shiYanCanShu.days" :show-message="false">
+                            <el-input-number
+                                v-model="pageInfo.days"
+                                type="number"
+                                :min="getAttrs('days', 'min')"
+                                :max="getAttrs('days', 'max')"
+                                :precision="getAttrs('days', 'precision')"
+                                :disabled="readonly"
+                                placeholder="请输入"
+                            />
+                        </el-form-item>
+                    </el-col>
+                    <el-col v-if="isShow('isConvert')" :span="12">
+                        <el-form-item :label="getAttrs('isConvert', 'label', false)" prop="shiYanCanShu.isConvert" :show-message="false">
+                            <el-radio-group v-model="pageInfo.isConvert" :disabled="readonly">
+                                <el-radio-button :label="true">转换</el-radio-button>
+                                <el-radio-button :label="false">不转换</el-radio-button>
+                            </el-radio-group>
+                        </el-form-item>
+                    </el-col>
+                    <el-col v-if="isShow('methodNum')" :span="12">
+                        <el-form-item :label="getAttrs('methodNum', 'label', false)" prop="shiYanCanShu.methodNum" :show-message="false">
+                            <el-input-number
+                                v-model="pageInfo.methodNum"
+                                type="number"
+                                :min="getAttrs('methodNum', 'min')"
+                                :max="getAttrs('methodNum', 'max')"
+                                :precision="getAttrs('methodNum', 'precision')"
+                                :disabled="readonly"
+                                placeholder="请输入"
+                            />
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+                <el-row v-if="isShow('specimensName')" :gutter="20" class="form-row">
+                    <el-col :span="24">
+                        <el-form-item prop="shiYanCanShu.specimensName" :show-message="false" class="inline-item">
+                            <template slot="label">
+                                <span>{{ getAttrs('specimensName', 'label', false) }}</span>
+                                <el-tooltip
+                                    class="item"
+                                    effect="dark"
+                                    content="与浓度水平数对应"
+                                    placement="top"
+                                >
+                                    <i class="el-icon-question" />
+                                </el-tooltip>
+                            </template>
+                            <el-input
+                                v-for="(item, index) in pageInfo.specimensNum"
+                                :key="index"
+                                v-model="pageInfo.specimensName[index]"
+                                class="inline-input"
+                                size="small"
+                                :disabled="readonly"
+                            />
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+                <el-row v-if="isShow('targetValue')" :gutter="20" class="form-row">
+                    <el-col :span="24">
+                        <el-form-item prop="targetValue" :show-message="false" class="inline-item">
+                            <template slot="label">
+                                <span>{{ getAttrs('targetValue', 'label', false) }}</span>
+                                <el-tooltip
+                                    class="item"
+                                    effect="dark"
+                                    content="注:若只填写高低浓度靶值,其余靶值则由系统自动计算。若填写全部浓度靶值,则按具体填写数值计算。"
+                                    placement="top"
+                                >
+                                    <i class="el-icon-question" />
+                                </el-tooltip>
+                            </template>
+                            <el-input-number
+                                v-for="(item, index) in pageInfo.specimensNum"
+                                :key="index"
+                                v-model="pageInfo.targetValue[index]"
+                                type="number"
+                                class="inline-number"
+                                size="small"
+                                :min="0"
+                                :disabled="readonly"
+                            />
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+                <el-row v-if="isShow('claimValue')" :gutter="20" class="form-row">
+                    <el-col :span="24">
+                        <el-form-item prop="claimValue" :show-message="false" class="inline-item">
+                            <template slot="label">
+                                <span>{{ getAttrs('claimValue', 'label', false) }}</span>
+                                <el-tooltip
+                                    class="item"
+                                    effect="dark"
+                                    content="与浓度水平名一一对应"
+                                    placement="top"
+                                >
+                                    <i class="el-icon-question" />
+                                </el-tooltip>
+                            </template>
+                            <el-input-number
+                                v-for="(item, index) in pageInfo.specimensNum"
+                                :key="index"
+                                v-model="pageInfo.claimValue[index]"
+                                type="number"
+                                class="inline-number"
+                                size="small"
+                                :disabled="readonly"
+                            />
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+                <el-row v-if="isShow('model')" :gutter="20" class="form-row">
+                    <el-col :span="24">
+                        <el-form-item :label="getAttrs('model', 'label', false)" prop="shiYanCanShu.model" :show-message="false">
+                            <el-checkbox-group v-model="pageInfo.model" :disabled="readonly">
+                                <el-checkbox label="批内不精密度">批内不精密度</el-checkbox>
+                                <el-checkbox label="总不精密度">总不精密度</el-checkbox>
+                            </el-checkbox-group>
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+                <el-row :gutter="20" class="form-row">
+                    <el-col v-if="isShow('rejectionRate')" :span="12">
+                        <el-form-item :label="getAttrs('rejectionRate', 'label', false)" prop="rejectionRate" :show-message="false">
+                            <el-select
+                                v-model="pageInfo.rejectionRate"
+                                clearable
+                                :disabled="readonly"
+                                placeholder="请选择"
+                            >
+                                <el-option
+                                    v-for="(item, index) in rateOption"
+                                    :key="index"
+                                    :label="item.label"
+                                    :value="item.value"
+                                />
+                            </el-select>
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+                <el-row :gutter="20" class="form-row">
+                    <el-col v-if="isShow('standard')" :span="12">
+                        <el-form-item :label="getAttrs('standard', 'label', false)" prop="shiYanCanShu.standard" :show-message="false">
+                            <el-select
+                                v-model="pageInfo.standard"
+                                filterable
+                                clearable
+                                :disabled="readonly"
+                                placeholder="请选择"
+                            >
+                                <el-option
+                                    v-for="(item, index) in standardOption"
+                                    :key="index"
+                                    :label="item.label"
+                                    :value="item.value"
+                                />
+                            </el-select>
+                        </el-form-item>
+                    </el-col>
+                    <el-col v-if="pageInfo.standard === '基于允许总误差TEa' && isShow('tea')" :span="12">
+                        <el-form-item label="TEa数值" prop="tea" :show-message="false">
+                            <el-input-number
+                                v-model="pageInfo.tea"
+                                type="number"
+                                :min="0"
+                                :precision="2"
+                                :disabled="readonly"
+                                placeholder="请输入"
+                                @change="handleCvsChange"
+                            />
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+                <el-row v-if="['基于允许总误差TEa'].includes(pageInfo.standard)" :gutter="20" class="form-row">
+                    <el-col v-if="isShow('batchCVS')" :span="12">
+                        <el-form-item :label="getAttrs('batchCVS', 'label', false)" prop="shiYanCanShu.batchCVS" :show-message="false">
+                            <el-select
+                                v-model="pageInfo.batchCVS"
+                                filterable
+                                clearable
+                                :disabled="readonly"
+                                placeholder="请选择"
+                                @change="handleCvsChange"
+                            >
+                                <el-option
+                                    v-for="(item, index) in batchOption"
+                                    :key="index"
+                                    :label="item.label"
+                                    :value="item.value"
+                                />
+                            </el-select>
+                        </el-form-item>
+                    </el-col>
+                    <el-col v-if="isShow('dailyCVS')" :span="12">
+                        <el-form-item :label="getAttrs('dailyCVS', 'label', false)" prop="shiYanCanShu.dailyCVS" :show-message="false">
+                            <el-select
+                                v-model="pageInfo.dailyCVS"
+                                filterable
+                                clearable
+                                :disabled="readonly"
+                                placeholder="请选择"
+                                @change="handleCvsChange"
+                            >
+                                <el-option
+                                    v-for="(item, index) in batchOption"
+                                    :key="index"
+                                    :label="item.label"
+                                    :value="item.value"
+                                />
+                            </el-select>
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+                <el-row v-if="pageInfo.standard === '基于厂商声明参数'" :gutter="20" class="form-row">
+                    <el-col :span="24">
+                        <el-form-item prop="shiYanCanShu.allowableSDr" :show-message="false" class="inline-item">
+                            <template slot="label">
+                                <!-- <span>{{ getAttrs('allowableSDr', 'label', false) }}</span> -->
+                                <span>重复(批内)</span>
+                                <el-tooltip
+                                    class="item"
+                                    effect="dark"
+                                    content="与浓度水平名一一对应"
+                                    placement="top"
+                                >
+                                    <i class="el-icon-question" />
+                                </el-tooltip>
+                            </template>
+                            <el-input-number
+                                v-for="(item, index) in pageInfo.specimensNum"
+                                :key="index"
+                                v-model="pageInfo.allowableSDr[index]"
+                                type="number"
+                                class="inline-number"
+                                size="small"
+                                :disabled="readonly"
+                            />
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+                <el-row v-if="pageInfo.standard === '基于厂商声明参数'" :gutter="20" class="form-row">
+                    <el-col :span="24">
+                        <el-form-item prop="shiYanCanShu.allowableSDl" :show-message="false" class="inline-item">
+                            <template slot="label">
+                                <!-- <span>{{ getAttrs('allowableSDl', 'label', false) }}</span> -->
+                                <span>期间(批间)</span>
+                                <el-tooltip
+                                    class="item"
+                                    effect="dark"
+                                    content="与浓度水平名一一对应"
+                                    placement="top"
+                                >
+                                    <i class="el-icon-question" />
+                                </el-tooltip>
+                            </template>
+                            <el-input-number
+                                v-for="(item, index) in pageInfo.specimensNum"
+                                :key="index"
+                                v-model="pageInfo.allowableSDl[index]"
+                                type="number"
+                                class="inline-number"
+                                size="small"
+                                :disabled="readonly"
+                            />
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+                <el-row v-show="0" :gutter="20" class="form-row">
+                    <el-col :span="12">
+                        <el-form-item label="数值" prop="shiYanCanShu.batchCVSValue" :show-message="false">
+                            <el-input-number
+                                v-model="pageInfo.batchCVSValue"
+                                type="number"
+                                :min="0"
+                                :precision="2"
+                                :disabled="readonly"
+                                placeholder="请输入"
+                            />
+                        </el-form-item>
+                    </el-col>
+                    <el-col :span="12">
+                        <el-form-item label="数值" prop="shiYanCanShu.dailyCVSValue" :show-message="false">
+                            <el-input-number
+                                v-model="pageInfo.dailyCVSValue"
+                                type="number"
+                                :min="0"
+                                :precision="2"
+                                :disabled="readonly"
+                                placeholder="请输入"
+                            />
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+                <el-row :gutter="20" class="form-row">
+                    <el-col v-if="isShow('allowableR2')" :span="12">
+                        <el-form-item :label="getAttrs('allowableR2', 'label', false)" prop="shiYanCanShu.allowableR2" :show-message="false">
+                            <el-input-number
+                                v-model="pageInfo.allowableR2"
+                                type="number"
+                                :min="getAttrs('allowableR2', 'min')"
+                                :max="getAttrs('allowableR2', 'max')"
+                                :step="0.001"
+                                :precision="getAttrs('allowableR2', 'precision')"
+                                :disabled="readonly"
+                                placeholder="请输入"
+                            />
+                        </el-form-item>
+                    </el-col>
+                    <el-col v-if="isShow('range')" :span="12">
+                        <el-form-item prop="shiYanCanShu.range" :show-message="false">
+                            <template slot="label">
+                                <span>{{ getAttrs('range', 'label', false) }}</span>
+                                <el-tooltip
+                                    class="item"
+                                    effect="dark"
+                                    content="注:填写范围为0.00~1.00,若值为0.10,则可接受范围为:90%~110%"
+                                    placement="top"
+                                >
+                                    <i class="el-icon-question" />
+                                </el-tooltip>
+                            </template>
+                            <el-input-number
+                                v-model="pageInfo.range"
+                                type="number"
+                                :min="getAttrs('range', 'min')"
+                                :max="getAttrs('range', 'max')"
+                                :step="0.01"
+                                :precision="getAttrs('range', 'precision')"
+                                :disabled="readonly"
+                                placeholder="请输入"
+                            />
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+            </div>
+        </div>
+    </div>
+</template>
+<script>
+import { standardOption, batchOption, rangeOption, rateOption } from '../constants/index'
+export default {
+    props: {
+        formId: {
+            type: String,
+            default: ''
+        },
+        info: {
+            type: Object,
+            default: () => {}
+        },
+        readonly: {
+            type: Boolean,
+            default: false
+        },
+        configData: {
+            type: Array,
+            default: () => []
+        }
+    },
+    data () {
+        return {
+            standardOption,
+            batchOption,
+            rangeOption,
+            rateOption,
+            pageInfo: null,
+            nameTagValue: '',
+            targetTagValue: '',
+            nameTagVisible: false,
+            targetTagVisible: false,
+            paramsList: this.configData.map(i => ({ key: i.key, visible: i.isVisible }))
+        }
+    },
+    watch: {
+        pageInfo: {
+            handler (val, oldVal) {
+                this.$emit('updateParams', val)
+            },
+            deep: true
+        }
+    },
+    mounted () {
+        const temp = JSON.parse(JSON.stringify(this.info))
+        // 填充默认值
+        this.configData.forEach(item => {
+            if (this.$utils.isNotEmpty(item.default) && this.$utils.isEmpty(temp[item.key])) {
+                temp[item.key] = item.default
+            }
+        })
+        if (!this.formId) {
+            temp.specimensName = temp.specimensNum ? Array.from({ length: temp.specimensNum }, (_, index) => `水平${index + 1}`) : []
+            temp.allowableSDr = []
+            temp.allowableSDl = []
+            temp.targetValue = []
+            temp.claimValue = []
+            temp.rangeValue = temp.range ? [100 - parseFloat(temp.range) * 100, 100 + parseFloat(temp.range) * 100] : []
+        }
+        this.pageInfo = temp || { model: [], targetValue: [], specimensName: [] }
+    },
+    methods: {
+        isShow (props) {
+            const t = this.paramsList.find(i => i.key === props)
+            return t && t.visible
+        },
+        getAttrs (props, attr, isNumber = true) {
+            const t = this.configData.find(i => i.key === props)
+            const res = t ? t[attr] : ''
+            return isNumber ? Number(res) : res
+        },
+        handleNumChange (v) {
+            const { specimensName: s = [] } = this.pageInfo || {}
+            if (v > s.length) {
+                this.pageInfo.specimensName = s.concat(Array.from({ length: v - s.length }, (_, index) => `水平${index + s.length + 1}`))
+            } else {
+                this.pageInfo.specimensName.splice(v)
+            }
+        },
+        handleCvsChange () {
+            const { batchCVS, dailyCVS, tea } = this.pageInfo
+            this.pageInfo.batchCVSValue = parseFloat(tea * batchCVS)
+            this.pageInfo.dailyCVSValue = parseFloat(tea * dailyCVS)
+        }
+    }
+}
+</script>
+<style lang="scss" scoped>
+    .info-container {
+        .info-item {
+            .form-container {
+                padding: 10px;
+                background: #f5f5f5;
+                border: 1px solid #e6e6e6;
+                border-radius: 4px;
+                overflow: hidden;
+                .form-row {
+                    padding: 5px 0;
+                    border-top: 1px solid #e6e6e6;
+                    &:first-child {
+                        border-top: none;
+                        padding-top: 0;
+                    }
+                    &:last-child {
+                        padding-bottom: 0;
+                    }
+                    &:empty {
+                        display: none;
+                    }
+                    .inline-item {
+                        ::v-deep {
+                            .el-form-item__content {
+                                display: flex;
+                                justify-content: flex-start;
+                                > .el-input {
+                                    max-width: 120px;
+                                    margin-right: 10px;
+                                    &:last-of-type {
+                                        margin-right: 0;
+                                    }
+                                }
+                            }
+                        }
+                        .inline-input {
+                            width: 100px;
+                            vertical-align: bottom;
+                        }
+                        .inline-number {
+                            width: 140px;
+                            margin-right: 10px;
+                            &:last-of-type {
+                                margin-right: 0;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+</style>

+ 203 - 0
src/views/business/performance/components/reagent-edit.vue

@@ -0,0 +1,203 @@
+<template>
+    <el-dialog
+        :title="title"
+        :visible.sync="dialogVisible"
+        :close-on-click-modal="false"
+        :close-on-press-escape="false"
+        :show-close="false"
+        append-to-body
+        width="60%"
+        class="dialog reagent-dialog"
+        top="5vh"
+        @close="closeDialog"
+    >
+        <el-form
+            ref="form"
+            :label-width="formLabelWidth"
+            :model="reagentForm"
+            :rules="rules"
+            class="reagent-form"
+            @submit.native.prevent
+        >
+            <el-form-item label="类别" prop="leiBie">
+                <el-select
+                    v-model="reagentForm.leiBie"
+                    filterable
+                    clearable
+                    :disabled="readonly"
+                    placeholder="请选择"
+                >
+                    <el-option
+                        v-for="item in reagentType"
+                        :key="item.value"
+                        :label="item.label"
+                        :value="item.value"
+                    />
+                </el-select>
+            </el-form-item>
+            <el-form-item label="试剂名称" prop="shiJiMingCheng">
+                <el-input
+                    v-model="reagentForm.shiJiMingCheng"
+                    type="text"
+                    clearable
+                    show-word-limit
+                    :maxlength="64"
+                    :disabled="readonly"
+                    placeholder="请输入"
+                />
+            </el-form-item>
+            <el-form-item label="批号" prop="piHao">
+                <el-input
+                    v-model="reagentForm.piHao"
+                    type="text"
+                    clearable
+                    show-word-limit
+                    :maxlength="64"
+                    :disabled="readonly"
+                    placeholder="请输入"
+                />
+            </el-form-item>
+            <el-form-item label="厂家" prop="changJia">
+                <el-input
+                    v-model="reagentForm.changJia"
+                    type="text"
+                    clearable
+                    show-word-limit
+                    :maxlength="64"
+                    :disabled="readonly"
+                    placeholder="请输入"
+                />
+            </el-form-item>
+            <el-form-item label="有效期" prop="youXiaoQi">
+                <el-date-picker
+                    v-model="reagentForm.youXiaoQi"
+                    type="date"
+                    clearable
+                    align="right"
+                    class="date-picker"
+                    value-format="yyyy-MM-dd"
+                    :picker-options="pickerOptions"
+                    placeholder="请选择"
+                />
+            </el-form-item>
+        </el-form>
+        <div slot="footer" class="el-dialog--center">
+            <ibps-toolbar
+                :actions="toolbars"
+                @action-event="handleActionEvent"
+            />
+        </div>
+    </el-dialog>
+</template>
+
+<script>
+import { reagentFormRules, reagentType } from '../constants/index'
+export default {
+    props: {
+        show: {
+            type: Boolean,
+            default: false
+        },
+        readonly: {
+            type: Boolean,
+            default: false
+        },
+        pageData: {
+            type: Object,
+            default: () => {}
+        },
+        dataIndex: {
+            type: Number,
+            default: null
+        }
+    },
+    data () {
+        const isCreate = this.$utils.isEmpty(this.pageData)
+        return {
+            isCreate,
+            reagentType,
+            dialogVisible: this.show,
+            title: isCreate ? '新增试剂' : '编辑试剂',
+            formLabelWidth: '100px',
+            reagentForm: !isCreate ? JSON.parse(JSON.stringify(this.pageData)) : {},
+            rules: reagentFormRules,
+            pickerOptions: {
+                disabledDate (time) {
+                    // 禁用当前日期之前的日期
+                    return time.getTime() < Date.now() - 8.64e7
+                }
+            },
+            toolbars: [
+                { key: 'save', icon: 'ibps-icon-save', label: '保存', type: 'primary', hidden: () => { return this.readonly } },
+                { key: 'continue', icon: 'ibps-icon-send', label: '保存并继续', type: 'success', hidden: () => { return this.readonly || !this.isCreate } },
+                { key: 'cancel', icon: 'el-icon-close', label: '关闭', type: 'danger' }
+            ]
+        }
+    },
+    watch: {
+        show: {
+            handler (val, oldVal) {
+                this.dialogVisible = val
+            },
+            immediate: true
+        }
+    },
+    mounted () {
+
+    },
+    methods: {
+        handleActionEvent ({ key }) {
+            switch (key) {
+                case 'save':
+                    this.handleSave(key)
+                    break
+                case 'continue':
+                    this.handleSave(key)
+                    break
+                case 'cancel':
+                    this.closeDialog()
+                    break
+                default:
+                    break
+            }
+        },
+        handleSave (key) {
+            this.$refs.form.validate((valid) => {
+                if (!valid) {
+                    return
+                }
+                this.submitForm(this.reagentForm, key)
+            })
+        },
+        submitForm (data, key) {
+            this.$emit('callback', { data, index: this.dataIndex })
+            if (key === 'save') {
+                return this.closeDialog()
+            }
+            // this.$refs.form.resetFields()
+            this.reagentForm = {}
+        },
+        closeDialog () {
+            this.$emit('update:show', false)
+        }
+    }
+}
+</script>
+<style lang="scss" scoped>
+    .reagent-dialog {
+        .reagent-form {
+            padding: 20px;
+            ::v-deep {
+                .el-form-item {
+                    margin-bottom: 16px !important;
+                    &:last-child {
+                        margin-bottom: 0 !important;
+                    }
+                    .el-form-item__error {
+                        padding-top: 8px !important;
+                    }
+                }
+            }
+        }
+    }
+</style>

+ 238 - 0
src/views/business/performance/components/reagent-info.vue

@@ -0,0 +1,238 @@
+<template>
+    <div class="info-container">
+        <div class="info-item">
+            <div class="title">
+                <i class="ibps-icon-star" />
+                <span>实验试剂信息</span>
+            </div>
+            <div class="table-container">
+                <div class="table-operate">
+                    <template v-for="btn in toolbars">
+                        <el-button
+                            v-if="!btn.hidden"
+                            :key="btn.key"
+                            :type="btn.type"
+                            :icon="btn.icon"
+                            :size="btn.size || 'mini'"
+                            plain
+                            @click="handleActionEvent(btn.key)"
+                        >
+                            {{ btn.label }}
+                        </el-button>
+                    </template>
+                </div>
+                <el-table
+                    ref="reagentTable"
+                    :data="reagentData"
+                    border
+                    stripe
+                    highlight-current-row
+                    style="width: 100%"
+                    max-height="250px"
+                    class="reagent-table"
+                    @selection-change="handleSelectionChange"
+                >
+                    <el-table-column type="selection" width="40" header-align="center" align="center" />
+                    <el-table-column type="index" label="序号" width="50" header-align="center" align="center" />
+                    <el-table-column prop="leiBie" label="类别" width="80" header-align="center" align="center" />
+                    <el-table-column prop="shiJiMingCheng" label="试剂名称" min-width="120" header-align="center" />
+                    <el-table-column prop="piHao" label="批号" width="75" header-align="center" align="center" sortable />
+                    <el-table-column prop="changJia" label="厂家" width="100" header-align="center" />
+                    <el-table-column prop="youXiaoQi" label="有效期" width="100" header-align="center" align="center" sortable />
+                    <el-table-column v-if="!readonly" fixed="right" label="操作" width="70" header-align="center" align="center">
+                        <template slot-scope="scope">
+                            <div class="inline-operate">
+                                <a><i class="el-icon-edit" @click="handleEdit('edit', scope.row, scope.$index)" /></a>
+                                <a><i class="el-icon-delete" @click="handleRemove(scope.$index)" /></a>
+                            </div>
+                        </template>
+                    </el-table-column>
+                </el-table>
+            </div>
+        </div>
+        <reagent-edit
+            v-if="showReagent"
+            :show.sync="showReagent"
+            :page-data="rowData"
+            :data-index="rowIndex"
+            @close="() => showReagent = false"
+            @callback="updates"
+        />
+        <import-table
+            :visible="showImportTable"
+            title="导入"
+            @close="visible => (showImportTable = visible)"
+            @action-event="handleImport"
+        />
+    </div>
+</template>
+<script>
+import IbpsExport from '@/plugins/export'
+import IbpsImport from '@/plugins/import'
+import ActionUtils from '@/utils/action'
+export default {
+    components: {
+        ReagentEdit: () => import('./reagent-edit'),
+        ImportTable: () => import('@/business/platform/form/formrender/dynamic-form/components/import-table')
+    },
+    props: {
+        info: {
+            type: Array,
+            default: () => []
+        },
+        readonly: {
+            type: Boolean,
+            default: false
+        }
+    },
+    data () {
+        return {
+            reagentData: [],
+            showReagent: false,
+            showImportTable: false,
+            selectionIndex: '',
+            rowData: {},
+            toolbars: [
+                { key: 'export', icon: 'ibps-icon-cloud-download', label: '导出', type: 'info', hidden: this.readonly },
+                { key: 'import', icon: 'ibps-icon-cloud-upload', label: '导入', type: 'warning', hidden: this.readonly },
+                { key: 'create', icon: 'ibps-icon-plus', label: '添加', type: 'success', hidden: this.readonly },
+                // { key: 'edit', icon: 'ibps-icon-edit', label: '编辑', type: 'primary', hidden: this.readonly },
+                { key: 'remove', icon: 'ibps-icon-trash', label: '删除', type: 'danger', hidden: this.readonly }
+            ]
+        }
+    },
+    watch: {
+        info: {
+            handler (val) {
+                this.reagentData = val || []
+            },
+            immediate: true,
+            deep: true
+        }
+    },
+    mounted () {
+
+    },
+    methods: {
+        handleSelectionChange (val) {
+            this.selectionIndex = val.map(item => this.reagentData.indexOf(item))
+        },
+        handleActionEvent (key) {
+            switch (key) {
+                case 'export':
+                    this.handleExport()
+                    break
+                case 'import':
+                    this.showImportTable = true
+                    break
+                case 'create':
+                    this.handleEdit(key)
+                    break
+                case 'remove':
+                    if (!this.selectionIndex) {
+                        return this.$message.warning('请选择要删除的试剂')
+                    }
+                    this.handleRemove(this.selectionIndex)
+                    break
+            }
+        },
+        getColumns () {
+            return [
+                { label: '类别', field_name: 'leiBie', name: 'leiBie' },
+                { label: '试剂名称', field_name: 'shiJiMingCheng', name: 'shiJiMingCheng' },
+                { label: '批号', field_name: 'piHao', name: 'piHao' },
+                { label: '厂家', field_name: 'changJia', name: 'changJia' },
+                { label: '有效期', field_name: 'youXiaoQi', name: 'youXiaoQi' }
+            ]
+        },
+        handleExport () {
+            IbpsExport.excel({
+                columns: this.getColumns(),
+                data: this.reagentData,
+                nameKey: 'name',
+                title: '实验试剂信息'
+            }).then(() => {
+                ActionUtils.success('导出成功')
+            })
+        },
+        handleImport (file, options) {
+            this.loading = false
+            IbpsImport.xlsx(file, options).then(({ header, results }) => {
+                const keys = this.getKeys(this.getColumns())
+                const list = []
+                results.forEach(item => {
+                    const obj = {}
+                    Object.keys(item).forEach(key => {
+                        console.log(key)
+                        if (keys[key]) {
+                            obj[keys[key]] = item[key]
+                        }
+                    })
+                    list.push(obj)
+                })
+                this.reagentData = Array.from(this.reagentData.concat(list))
+                this.showImportTable = false
+            })
+        },
+        getKeys (data) {
+            return Array.isArray(data) ? data.reduce((acc, item) => ({ ...acc, [item.label]: item.name }), {}) : {}
+        },
+        handleEdit (key, selection, index) {
+            this.showReagent = true
+            this.rowData = key === 'edit' ? selection : {}
+            this.rowIndex = key === 'edit' ? index : null
+        },
+        handleRemove (index) {
+            let indexList = []
+            if (typeof index === 'number') {
+                indexList = [index]
+            } else {
+                indexList = index
+            }
+            indexList.sort((a, b) => b - a)
+            this.$confirm('确定要删除选中试剂吗?', '提示', {
+                confirmButtonText: '确定',
+                cancelButtonText: '取消',
+                type: 'warning'
+            }).then(() => {
+                indexList.forEach(i => {
+                    this.reagentData.splice(i, 1)
+                })
+            }).catch(() => {})
+        },
+        updates (params) {
+            console.log(params)
+            if (params.index === null) {
+                this.reagentData.push(params.data)
+            } else {
+                this.reagentData.splice(params.index, 1, params.data)
+            }
+        }
+    }
+}
+</script>
+<style lang="scss" scoped>
+    .info-container {
+        .info-item {
+            .table-container {
+                padding: 10px;
+                background: #f5f5f5;
+                border: 1px solid #e6e6e6;
+                border-radius: 4px;
+                overflow: hidden;
+                .table-operate {
+                    margin-bottom: 10px;
+                    text-align: right;
+                }
+                .inline-operate {
+                    display: flex;
+                    align-items: center;
+                    justify-content: space-around;
+                    a:hover {
+                        color: #409eff;
+                    }
+                }
+            }
+        }
+    }
+</style>

+ 863 - 0
src/views/business/performance/config.vue

@@ -0,0 +1,863 @@
+<template>
+    <el-dialog
+        v-loading="loading"
+        :visible.sync="dialogVisible"
+        :close-on-click-modal="false"
+        :close-on-press-escape="false"
+        :show-close="false"
+        append-to-body
+        fullscreen
+        class="dialog method-config-dialog"
+        top="0"
+        @open="loadData"
+        @close="closeDialog"
+    >
+        <div slot="title" class="config-dialog-header">
+            <div class="title">{{ formData.target + '配置' }}</div>
+            <div class="operate">
+                <template v-for="btn in toolbars">
+                    <el-button
+                        v-if="!btn.hidden"
+                        :key="btn.key"
+                        :type="btn.type"
+                        :icon="btn.icon"
+                        :size="btn.size || 'mini'"
+                        @click="handleActionEvent(btn.key)"
+                    >
+                        {{ btn.label }}
+                    </el-button>
+                </template>
+            </div>
+        </div>
+        <el-form
+            v-if="loadCompleted"
+            ref="configForm"
+            :label-width="formLabelWidth"
+            :model.sync="formData"
+            :rules="rules"
+            class="config-form"
+            @submit.native.prevent
+        >
+            <div class="config-item">
+                <div class="title">
+                    <i class="ibps-icon-star" />
+                    <span>指标配置</span>
+                </div>
+                <div class="form-container">
+                    <el-row :gutter="20" class="form-row">
+                        <el-col :span="12">
+                            <el-form-item label="指标名称" prop="target" :show-message="false">
+                                <el-input
+                                    v-model="formData.target"
+                                    type="text"
+                                    clearable
+                                    show-word-limit
+                                    :maxlength="64"
+                                    :disabled="readonly"
+                                    placeholder="请输入"
+                                />
+                            </el-form-item>
+                        </el-col>
+                        <el-col :span="4">
+                            <el-form-item label="指标类型" prop="targetKey" :show-message="false">
+                                <el-input
+                                    v-model="formData.targetKey"
+                                    type="text"
+                                    clearable
+                                    show-word-limit
+                                    :maxlength="32"
+                                    :disabled="readonly"
+                                    placeholder="请输入"
+                                />
+                            </el-form-item>
+                        </el-col>
+                        <el-col :span="4">
+                            <el-form-item label="排序" prop="sn" :show-message="false">
+                                <el-input-number
+                                    v-model="formData.sn"
+                                    type="number"
+                                    :min="1"
+                                    :max="99"
+                                    :disabled="readonly"
+                                    :precision="0"
+                                />
+                            </el-form-item>
+                        </el-col>
+                        <el-col :span="4">
+                            <el-form-item label="图标" prop="icon">
+                                <el-input
+                                    v-model="formData.icon"
+                                    type="text"
+                                    clearable
+                                    :maxlength="32"
+                                    :disabled="readonly"
+                                    placeholder="请输入"
+                                />
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                </div>
+            </div>
+            <div class="config-item">
+                <div class="title">
+                    <i class="ibps-icon-star" />
+                    <span>方法配置</span>
+                </div>
+                <el-button
+                    class="add-btn"
+                    size="mini"
+                    type="primary"
+                    icon="el-icon-plus"
+                    @click="addMethod"
+                >新增</el-button>
+                <el-tabs v-model="activeTab" type="border-card" class="outer-tabs" @tab-click="handleTabClick">
+                    <el-tab-pane
+                        v-for="(method, mIndex) in formData.methods"
+                        :key="mIndex"
+                        :label="method.methodName"
+                        :name="method.methodName"
+                    >
+                        <template #label>
+                            <span>{{ method.methodName }}</span>
+                            <el-dropdown @command="(command) => handleCommand(mIndex, command)">
+                                <i class="el-icon-setting el-dropdown-link" />
+                                <el-dropdown-menu slot="dropdown">
+                                    <el-dropdown-item command="copy">复制</el-dropdown-item>
+                                    <el-dropdown-item :disabled="readonly && method.isBasic === 'Y'" command="delete">删除</el-dropdown-item>
+                                </el-dropdown-menu>
+                            </el-dropdown>
+                        </template>
+                        <el-row :gutter="20" class="form-row">
+                            <el-col :span="8">
+                                <el-form-item label="方法名称" :prop="`methods[${mIndex}].methodName`" :show-message="false">
+                                    <el-input
+                                        v-model="method.methodName"
+                                        type="text"
+                                        show-word-limit
+                                        :maxlength="64"
+                                        :disabled="readonly && method.isBasic === 'Y'"
+                                        @input="handleNameChange"
+                                    />
+                                </el-form-item>
+                            </el-col>
+                            <el-col :span="8">
+                                <el-form-item label="方法类型" :prop="`methods[${mIndex}].methodType`" :show-message="false">
+                                    <el-select
+                                        v-model="method.methodType"
+                                        :disabled="readonly && method.isBasic === 'Y'"
+                                        placeholder="请选择"
+                                    >
+                                        <el-option
+                                            v-for="(item, index) in methodTypeOption"
+                                            :key="index"
+                                            :label="item.label"
+                                            :value="item.value"
+                                        />
+                                    </el-select>
+                                </el-form-item>
+                            </el-col>
+                            <el-col :span="8">
+                                <el-form-item label="方法KEY" :prop="`methods[${mIndex}].methodKey`" :show-message="false">
+                                    <el-select
+                                        v-model="method.methodKey"
+                                        :disabled="readonly && method.isBasic === 'Y'"
+                                        placeholder="请选择"
+                                    >
+                                        <el-option
+                                            v-for="(item, index) in methodKeyOption.filter(i => i.type === formData.targetKey)"
+                                            :key="index"
+                                            :label="item.label"
+                                            :value="item.value"
+                                        />
+                                    </el-select>
+                                </el-form-item>
+                            </el-col>
+                            <el-col :span="8">
+                                <el-form-item label="排序" :prop="`methods[${mIndex}].sn`" :show-message="false">
+                                    <el-input-number
+                                        v-model="method.sn"
+                                        type="number"
+                                        :min="1"
+                                        :max="99"
+                                        :precision="0"
+                                        :disabled="readonly && method.isBasic === 'Y'"
+                                    />
+                                </el-form-item>
+                            </el-col>
+                        </el-row>
+                        <el-row :gutter="20" class="form-row">
+                            <el-col v-if="isSuper" :span="8">
+                                <el-form-item label="是否基础" :prop="`methods[${mIndex}].isBasic`" :show-message="false">
+                                    <el-switch v-model="method.isBasic" active-value="Y" inactive-value="N" />
+                                </el-form-item>
+                            </el-col>
+                            <template v-if="method.isBasic === 'N' || isSuper">
+                                <el-col :span="8">
+                                    <el-form-item label="是否禁用" :prop="`methods[${mIndex}].isDisabled`" :show-message="false">
+                                        <el-switch v-model="method.isDisabled" active-value="Y" inactive-value="N" />
+                                    </el-form-item>
+                                </el-col>
+                                <el-col :span="8">
+                                    <el-form-item label="是否公开" :prop="`methods[${mIndex}].isPublic`" :show-message="false">
+                                        <el-switch v-model="method.isPublic" active-value="Y" inactive-value="N" />
+                                    </el-form-item>
+                                </el-col>
+                            </template>
+                        </el-row>
+                        <el-tabs tab-position="left" class="inner-tabs">
+                            <el-tab-pane label="实验步骤">
+                                <el-input
+                                    v-model="method.step"
+                                    type="textarea"
+                                    :maxlength="2000"
+                                    show-word-limit
+                                    :rows="16"
+                                    :disabled="readonly && method.isBasic === 'Y'"
+                                />
+                            </el-tab-pane>
+                            <el-tab-pane label="判定标准">
+                                <el-input
+                                    v-model="method.criterion"
+                                    type="textarea"
+                                    :maxlength="2000"
+                                    show-word-limit
+                                    :rows="16"
+                                    :disabled="readonly && method.isBasic === 'Y'"
+                                />
+                            </el-tab-pane>
+                            <el-tab-pane label="参考资料">
+                                <ibps-attachment
+                                    v-model="method.references"
+                                    allow-download
+                                    download
+                                    multiple
+                                    accept="*"
+                                    store="id"
+                                    :readonly="readonly && method.isBasic === 'Y'"
+                                />
+                            </el-tab-pane>
+                            <el-tab-pane label="实验参数">
+                                <div v-if="!readonly || method.isBasic === 'N'" class="operate-btn">
+                                    <el-button
+                                        v-for="btn in tableToolbars"
+                                        :key="btn.key"
+                                        :type="btn.type"
+                                        :icon="btn.icon"
+                                        :size="btn.size || 'mini'"
+                                        plain
+                                        @click="handleActionEvent(btn.key, 'params', mIndex)"
+                                    >
+                                        {{ btn.label }}
+                                    </el-button>
+                                </div>
+                                <el-table
+                                    :ref="`configTable${mIndex}`"
+                                    :data="method.params"
+                                    border
+                                    stripe
+                                    highlight-current-row
+                                    style="width: 100%"
+                                    :max-height="maxHeight"
+                                    class="config-table"
+                                    @selection-change="selection => handleSelectionChange(selection, method.params, 'params')"
+                                >
+                                    <el-table-column type="selection" width="45" header-align="center" align="center" />
+                                    <el-table-column type="index" label="序号" width="50" header-align="center" align="center" />
+                                    <el-table-column
+                                        v-for="(item, pIndex) in paramColumn"
+                                        :key="pIndex"
+                                        :prop="item.key"
+                                        :label="item.label"
+                                        :width="item.width"
+                                        :min-width="item.minWidth"
+                                        header-align="center"
+                                        align="center"
+                                    >
+                                        <template slot-scope="scope">
+                                            <el-switch
+                                                v-if="item.type === 'switch'"
+                                                v-model="scope.row[item.key]"
+                                                :disabled="readonly && method.isBasic === 'Y'"
+                                            />
+                                            <el-input-number
+                                                v-else-if="item.type === 'number'"
+                                                v-model="scope.row[item.key]"
+                                                type="number"
+                                                :disabled="readonly && method.isBasic === 'Y'"
+                                                :min="item.min"
+                                                :max="item.max"
+                                                :precision="item.precision"
+                                            />
+                                            <el-input
+                                                v-else
+                                                v-model="scope.row[item.key]"
+                                                :disabled="readonly && method.isBasic === 'Y'"
+                                            />
+                                        </template>
+                                    </el-table-column>
+                                    <el-table-column v-if="!readonly" fixed="right" label="操作" width="50" header-align="center" align="center">
+                                        <template slot-scope="scope">
+                                            <a><i class="el-icon-delete" @click="handleRemove(scope.$index, 'params', mIndex)" /></a>
+                                        </template>
+                                    </el-table-column>
+                                </el-table>
+                            </el-tab-pane>
+                            <el-tab-pane label="实验公式">
+                                <div v-if="!readonly || method.isBasic === 'N'" class="operate-btn">
+                                    <el-button
+                                        v-for="btn in tableToolbars"
+                                        :key="btn.key"
+                                        :type="btn.type"
+                                        :icon="btn.icon"
+                                        :size="btn.size || 'mini'"
+                                        plain
+                                        @click="handleActionEvent(btn.key, 'formulas', mIndex)"
+                                    >
+                                        {{ btn.label }}
+                                    </el-button>
+                                </div>
+                                <el-table
+                                    :ref="`formulaTable${mIndex}`"
+                                    :data="method.formulas"
+                                    border
+                                    stripe
+                                    highlight-current-row
+                                    style="width: 100%"
+                                    :max-height="maxHeight"
+                                    class="formula-table"
+                                    @selection-change="selection => handleSelectionChange(selection, method.formulas, 'formulas')"
+                                >
+                                    <el-table-column type="selection" width="45" header-align="center" align="center" />
+                                    <el-table-column type="index" label="序号" width="50" header-align="center" align="center" />
+                                    <el-table-column
+                                        v-for="(item, fIndex) in formulaColumn"
+                                        :key="fIndex"
+                                        :prop="item.key"
+                                        :label="item.label"
+                                        :width="item.width"
+                                        :min-width="item.minWidth"
+                                        :style="item.visible === false ? 'display: none;' : ''"
+                                        header-align="center"
+                                        align="center"
+                                    >
+                                        <template slot-scope="scope">
+                                            <el-switch
+                                                v-if="item.type === 'switch'"
+                                                v-model="scope.row[item.key]"
+                                                :disabled="readonly && method.isBasic === 'Y'"
+                                            />
+                                            <el-select
+                                                v-if="item.type === 'select'"
+                                                v-model="scope.row[item.key]"
+                                                :disabled="readonly && method.isBasic === 'Y'"
+                                                placeholder="请选择"
+                                                allow-create
+                                                filterable
+                                                @change="handleFormulaChange(mIndex, scope, item.key, 'formulas', ['key', 'value'])"
+                                            >
+                                                <el-option
+                                                    v-for="(f, index) in item.options"
+                                                    :key="index"
+                                                    :label="f.label"
+                                                    :value="f.label"
+                                                />
+                                            </el-select>
+                                            <el-input
+                                                v-else
+                                                v-model="scope.row[item.key]"
+                                                :disabled="readonly && method.isBasic === 'Y'"
+                                            />
+                                        </template>
+                                    </el-table-column>
+                                    <el-table-column v-if="!readonly || method.isBasic !== 'Y'" fixed="right" label="操作" width="70" header-align="center" align="center">
+                                        <template slot-scope="scope">
+                                            <div class="inline-operate">
+                                                <a><i class="el-icon-view" @click="handlePreview(scope.row)" /></a>
+                                                <a><i class="el-icon-delete" @click="handleRemove(scope.$index, 'formulas', mIndex)" /></a>
+                                            </div>
+                                        </template>
+                                    </el-table-column>
+                                </el-table>
+                            </el-tab-pane>
+                            <el-tab-pane label="结论模板">
+                                <codemirror v-model="method.template" :options="cmConfg" />
+                                <!-- <ibps-ueditor v-model="method.templateDesc" class="editor" :config="ueditorConfig" /> -->
+                            </el-tab-pane>
+                            <el-tab-pane label="图表配置">
+                                <div v-if="!readonly || method.isBasic === 'N'" class="operate-btn">
+                                    <el-button
+                                        v-for="btn in tableToolbars"
+                                        :key="btn.key"
+                                        :type="btn.type"
+                                        :icon="btn.icon"
+                                        :size="btn.size || 'mini'"
+                                        plain
+                                        @click="handleActionEvent(btn.key, 'chartOption', mIndex)"
+                                    >
+                                        {{ btn.label }}
+                                    </el-button>
+                                </div>
+                                <el-table
+                                    :ref="`chartTable${mIndex}`"
+                                    :data="method.chartOption"
+                                    border
+                                    stripe
+                                    highlight-current-row
+                                    style="width: 100%"
+                                    :max-height="maxHeight"
+                                    class="formula-table"
+                                    @selection-change="selection => handleSelectionChange(selection, method.chartOption, 'chartOption')"
+                                >
+                                    <el-table-column type="selection" width="45" header-align="center" align="center" />
+                                    <el-table-column type="index" label="序号" width="50" header-align="center" align="center" />
+                                    <el-table-column
+                                        v-for="(item, cIndex) in chartColumn"
+                                        :key="cIndex"
+                                        :prop="item.key"
+                                        :label="item.label"
+                                        :width="item.width"
+                                        :min-width="item.minWidth"
+                                        :style="item.visible === false ? 'display: none;' : ''"
+                                        header-align="center"
+                                        align="center"
+                                    >
+                                        <template slot-scope="scope">
+                                            <el-input
+                                                v-if="item.type === 'textarea'"
+                                                v-model="scope.row[item.key]"
+                                                type="textarea"
+                                                :rows="4"
+                                                :disabled="readonly && method.isBasic === 'Y'"
+                                            />
+                                            <el-input
+                                                v-else
+                                                v-model="scope.row[item.key]"
+                                                :disabled="readonly && method.isBasic === 'Y'"
+                                            />
+                                        </template>
+                                    </el-table-column>
+                                    <el-table-column v-if="!readonly || method.isBasic !== 'Y'" fixed="right" label="操作" width="50" header-align="center" align="center">
+                                        <template slot-scope="scope">
+                                            <a><i class="el-icon-delete" @click="handleRemove(scope.$index, 'chartOption', mIndex)" /></a>
+                                        </template>
+                                    </el-table-column>
+                                </el-table>
+                            </el-tab-pane>
+                        </el-tabs>
+                    </el-tab-pane>
+                </el-tabs>
+            </div>
+        </el-form>
+        <formula-preview
+            v-if="showFormula"
+            :show.sync="showFormula"
+            :formula="formulaInfo"
+            @close="() => showFormula = false"
+        />
+    </el-dialog>
+</template>
+
+<script>
+import { configFormRules, paramColumn, formulaColumn, chartColumn, paramList, formulaList, chartList, methodTypeOption, methodKeyOption, cmConfg } from './constants/index'
+import { getConfigDetail, saveConfig } from '@/api/business/pv'
+import { codemirror } from 'vue-codemirror'
+import 'codemirror/lib/codemirror.css'
+import 'codemirror/theme/eclipse.css'
+import 'codemirror/mode/xml/xml.js'
+import 'codemirror/addon/selection/active-line.js'
+export default {
+    components: {
+        IbpsAttachment: () => import('@/business/platform/file/attachment/selector'),
+        IbpsUeditor: () => import('@/components/ibps-ueditor'),
+        FormulaPreview: () => import('./components/formula-preview'),
+        codemirror
+    },
+    props: {
+        visible: {
+            type: Boolean,
+            default: false
+        },
+        targetId: {
+            type: String,
+            default: ''
+        }
+    },
+    data () {
+        const { isSuper } = this.$store.getters || {}
+        return {
+            isSuper,
+            paramColumn,
+            paramList,
+            formulaColumn,
+            formulaList,
+            chartColumn,
+            chartList,
+            methodTypeOption,
+            methodKeyOption,
+            cmConfg,
+            maxHeight: document.body.clientHeight - 438 + 'px',
+            dialogVisible: this.visible,
+            formLabelWidth: '90px',
+            formData: {},
+            rules: configFormRules,
+            activeTab: '',
+            activeTabIndex: 0,
+            methodTabs: [],
+            loading: false,
+            loadCompleted: false,
+            readonly: !isSuper,
+            selectionIndex: {
+                params: [],
+                formulas: [],
+                chartOption: []
+            },
+            showFormula: false,
+            formulaInfo: {},
+            toolbars: [
+                { key: 'save', icon: 'ibps-icon-save', label: '保存', type: 'success' },
+                { key: 'cancel', icon: 'el-icon-close', label: '关闭', type: 'danger' }
+            ],
+            tableToolbars: [
+                { key: 'add', icon: 'ibps-icon-plus', label: '添加', type: 'success' },
+                { key: 'remove', icon: 'ibps-icon-trash', label: '删除', type: 'danger' }
+            ],
+            ueditorConfig: {
+                autoHeightEnabled: false,
+                initialFrameHeight: 300,
+                initialFrameWidth: '100%',
+                initialStyle: 'body { font-size: 14px; }'
+            },
+            initMethod: {
+                methodName: '方法',
+                methodType: '',
+                methodKey: '',
+                sn: '',
+                isBasic: 'N',
+                isDisabled: 'N',
+                isPublic: 'N',
+                step: '',
+                criterion: '',
+                references: '',
+                params: [],
+                formulas: [],
+                template: '',
+                templateDesc: '',
+                chartOption: []
+            }
+        }
+    },
+    watch: {
+        visible: {
+            handler (val, oldVal) {
+                this.dialogVisible = this.visible
+            }
+        }
+    },
+    mounted () {
+        this.loadData()
+    },
+    methods: {
+        // 获取数据
+        async loadData () {
+            this.loading = true
+            getConfigDetail({ id: this.targetId }).then(res => {
+                this.loading = false
+                const { config, experimentalConfigDetailPoList: methods, icon, sn, target, targetKey } = res.data
+                methods.forEach(item => {
+                    item.params = this.$utils.isNotEmpty(item.params) ? JSON.parse(item.params) : []
+                    item.formulas = this.$utils.isNotEmpty(item.formulas) ? JSON.parse(item.formulas) : []
+                    item.chartOption = this.$utils.isNotEmpty(item.chartOption) ? JSON.parse(item.chartOption) : []
+                })
+                this.formData = { icon, sn, target, targetKey, id: this.targetId, methods }
+                this.methodTabs = methods.sort((a, b) => a.sn - b.sn)
+                this.activeTab = this.methodTabs[0].methodName
+                this.loadCompleted = true
+            }).catch(() => {
+                this.loading = false
+            })
+        },
+        handleTabClick (tab) {
+            const t = this.methodTabs.findIndex(item => item.methodName === tab.methodName)
+            // 外层tab切换清除选中数据
+            if (t !== this.activeTabIndex) {
+                this.$nextTick(() => {
+                    this.activeTabIndex = t
+                    this.selectionIndex = {
+                        params: [],
+                        formulas: [],
+                        chartOption: []
+                    }
+                    this.$refs[`configTable${this.activeTabIndex}`].clearSelection()
+                    this.$refs[`formulaTable${this.activeTabIndex}`].clearSelection()
+                    this.$refs[`chartOption${this.activeTabIndex}`].clearSelection()
+                })
+            }
+        },
+        handleNameChange (v) {
+            this.activeTab = v
+        },
+        handleCommand (index, command) {
+            switch (command) {
+                case 'copy':
+                    this.copyMethod(index)
+                    break
+                case 'delete':
+                    this.deleteMethod(index)
+                    break
+                default:
+                    break
+            }
+        },
+        copyMethod (index) {
+            const copyData = JSON.parse(JSON.stringify(this.formData.methods[index]))
+            copyData.sn = this.methodTabs.length + 1
+            copyData.id = ''
+            copyData.methodName += ' (复制)'
+            copyData.isBasic = 'N'
+            copyData.isDisabled = 'N'
+            this.formData.methods.push(copyData)
+        },
+        deleteMethod (index) {
+            const { methods = [] } = this.formData || {}
+            this.$confirm('确定要删除方法吗?', '提示', {
+                confirmButtonText: '确定',
+                cancelButtonText: '取消',
+                type: 'warning'
+            }).then(() => {
+                methods.splice(index, 1)
+                this.formData.method = methods
+                this.activeTab = methods.length ? methods[0].methodName : ''
+            }).catch(() => {})
+        },
+        addMethod () {
+            const data = JSON.parse(JSON.stringify(this.initMethod))
+            data.sn = this.methodTabs.length + 1
+            data.methodName += data.sn
+            this.formData.methods.push(data)
+            this.activeTab = data.methodName
+        },
+        handleActionEvent (key, type, index) {
+            switch (key) {
+                case 'save':
+                    this.handleSave()
+                    break
+                case 'cancel':
+                    this.handleCancel()
+                    break
+                case 'add':
+                    this.handleAddParam(type, index)
+                    break
+                case 'remove':
+                    if (!this.selectionIndex[type].length) {
+                        return this.$message.warning('请选择要删除的数据')
+                    }
+                    this.handleRemove(this.selectionIndex[type], type, index)
+                    break
+                default:
+                    break
+            }
+        },
+        handleSave () {
+            this.$refs.configForm.validate((valid) => {
+                if (!valid) {
+                    return this.$message.warning('请完善表单必填项信息!')
+                }
+                const submitData = JSON.parse(JSON.stringify(this.formData))
+                submitData.methods.forEach(item => {
+                    item.params = JSON.stringify(item.params)
+                    item.formulas = JSON.stringify(item.formulas)
+                    item.chartOption = JSON.stringify(item.chartOption)
+                })
+                submitData.experimentalConfigDetailPoList = submitData.methods
+                // 方法数据同时存储于主子表,便于列表获取
+                submitData.config = JSON.stringify(submitData.methods)
+                delete submitData.methods
+                console.log(submitData)
+                this.submitForm(submitData)
+            })
+        },
+        handleAddParam (type, index) {
+            const obj = type === 'config' ? {
+                key: '',
+                label: '',
+                default: '',
+                max: '',
+                min: '',
+                precision: '',
+                isVisible: true
+            } : {
+                key: '',
+                label: '',
+                value: ''
+            }
+            const temp = this.formData.methods[index][type] || []
+            temp.push(obj)
+            this.formData.methods[index][type] = temp
+            this.methodTabs = this.formData.methods
+        },
+        handleDelParam (type, index, cIndex) {
+            this.formData.methods[index][type].splice(cIndex, 1)
+        },
+        handleSelectionChange (v, data, type) {
+            this.selectionIndex[type] = v.map(item => data.indexOf(item))
+        },
+        handleFormulaChange (methodIndex, { $index, row }, key, type, args) {
+            const t = formulaList.find(i => i[key] === row[key])
+            args.forEach(i => {
+                this.methodTabs[methodIndex][type][$index][i] = t ? t[i] : ''
+            })
+        },
+        handlePreview (row) {
+            this.formulaInfo = row
+            this.showFormula = true
+        },
+        handleRemove (removeIndex, type, methodIndex) {
+            let indexList = []
+            if (typeof removeIndex === 'number') {
+                indexList = [removeIndex]
+            } else {
+                indexList = removeIndex
+            }
+            indexList.sort((a, b) => b - a)
+            this.$confirm('确定要删除选中数据吗?', '提示', {
+                confirmButtonText: '确定',
+                cancelButtonText: '取消',
+                type: 'warning'
+            }).then(() => {
+                indexList.forEach(i => {
+                    this.formData.methods[methodIndex][type].splice(i, 1)
+                })
+            }).catch(() => {})
+        },
+        // 提交数据
+        submitForm (data) {
+            saveConfig(data).then(res => {
+                this.$message.success('保存成功')
+                this.closeDialog()
+                this.$emit('callback')
+            })
+        },
+        handleCancel () {
+            this.closeDialog()
+        },
+        closeDialog () {
+            this.$emit('close', false)
+        }
+    }
+}
+</script>
+<style lang="scss" scoped>
+    .method-config-dialog {
+        ::v-deep {
+            .el-dialog__header {
+                padding: 15px 20px 16px;
+            }
+        }
+        .config-dialog-header {
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+            .title {
+                line-height: 24px;
+                font-size: 18px;
+                color: #303133;
+            }
+        }
+        .config-form {
+            padding: 20px;
+            background: #f5f5f5;
+            border-radius: 4px;
+            overflow: hidden;
+            height: calc(100vh - 100px);
+            .config-item {
+                position: relative;
+                margin-bottom: 20px;
+                .title {
+                    height: 20px;
+                    line-height: 20px;
+                    font-size: 16px;
+                    font-weight: bold;
+                    margin-bottom: 10px;
+                    .ibps-icon-star {
+                        color: #FB9600;
+                        margin-right: 5px;
+                    }
+                }
+                .operate-btn {
+                    text-align: left;
+                }
+                .inline-operate {
+                    display: flex;
+                    align-items: center;
+                    justify-content: space-around;
+                    a:hover {
+                        color: #409eff;
+                    }
+                }
+                ::v-deep {
+                    .el-form-item {
+                        margin-bottom: 10 !important;
+                        &__label {
+                            font-size: 14px !important;
+                            color: #606266;
+                        }
+                        &__content {
+                            .el-input, .el-select, .el-input-number {
+                                width: 100%;
+                            }
+                            .el-textarea .el-input__count {
+                                padding: 0 5px;
+                                line-height: initial;
+                            }
+                        }
+                    }
+                    .el-table th.el-table__cell > .cell, .el-table td.el-table__cell {
+                        color: #606266;
+                        font-size: 14px;
+                    }
+                    .el-dropdown-link {
+                        margin-left: 8px;
+                        cursor: pointer;
+                        font-size: 16px;
+                        color: #409EFF;
+                    }
+                    .el-tabs__nav-wrap.is-scrollable {
+                        width: calc(100% - 100px);
+                    }
+                    .outer-tabs {
+                        .el-tabs__content {
+                            height: calc(100vh - 290px);
+                            overflow: auto;
+                        }
+                        .inner-tabs {
+                            .el-tabs__header.is-left {
+                                width: 90px;
+                            }
+                            .el-tabs__item {
+                                padding: 0 12px 0 0;
+                            }
+                            .el-tabs__content {
+                                height: calc(100vh - 410px);
+                                overflow: auto;
+                            }
+                            .el-input-number--small {
+                                width: 100%;
+                            }
+                            .editor {
+                                .edui-editor {
+                                    width: auto !important;
+                                }
+                            }
+                        }
+                    }
+                }
+                .add-btn {
+                    position: absolute;
+                    top: 36px;
+                    right: 18px;
+                    z-index: 99;
+                }
+            }
+        }
+    }
+</style>

+ 406 - 0
src/views/business/performance/constants/index.js

@@ -0,0 +1,406 @@
+
+export const formRules = {
+    bianZhiBuMen: [{
+        required: true,
+        message: '请选择部门',
+        trigger: 'change'
+    }],
+    shiYanXiangMu: [{
+        required: true,
+        message: '请选择实验项目',
+        trigger: 'change'
+    }],
+    shiYanFangFa: [{
+        required: true,
+        message: '请选择实验方法',
+        trigger: 'change'
+    }],
+    yangBenLeiXing: [{
+        required: true,
+        message: '请选择样品',
+        trigger: 'change'
+    }],
+    shiYanYiQi: [{
+        required: true,
+        message: '请选择设备',
+        trigger: 'change'
+    }],
+    yiQiBianHao: [{
+        required: true,
+        message: '请输入设备编号',
+        trigger: 'blur'
+    }],
+    kaiShiShiJian: [{
+        required: true,
+        message: '请选择实验开始时间',
+        trigger: 'change'
+    }],
+    jieShuShiJian: [{
+        required: true,
+        message: '请选择实验结束时间',
+        trigger: 'change'
+    }],
+    bianZhiRen: [{
+        required: true,
+        message: '请选择操作员',
+        trigger: 'change'
+    }],
+    createBy: [{
+        required: true,
+        message: '请选择评价人',
+        trigger: 'change'
+    }],
+    jieGuoDanWei: [{
+        required: true,
+        message: '请选择结果单位',
+        trigger: 'change'
+    }],
+    baoLiuXiaoShu: [{
+        required: true,
+        message: '请选择小数位数',
+        trigger: 'blur'
+    }],
+    shenHeRen: [{
+        required: true,
+        message: '请选择',
+        trigger: 'change'
+    }],
+    shiYanJieLun: [{
+        required: false,
+        message: '请输入',
+        trigger: 'change'
+    }],
+    'shiYanCanShu.specimensNum': [{
+        required: true,
+        message: '请输入',
+        trigger: 'change'
+    }],
+    'shiYanCanShu.repeatNum': [{
+        required: true,
+        message: '请输入',
+        trigger: 'change'
+    }],
+    'shiYanCanShu.days': [{
+        required: true,
+        message: '请输入',
+        trigger: 'change'
+    }],
+    'shiYanCanShu.model': [{
+        required: true,
+        message: '请选择',
+        trigger: 'change'
+    }],
+    'shiYanCanShu.allowableR2': [{
+        required: true,
+        message: '请输入',
+        trigger: 'change'
+    }],
+    'shiYanCanShu.range': [{
+        required: false,
+        message: '请输入',
+        trigger: 'change'
+    }],
+    'shiYanCanShu.standard': [{
+        required: true,
+        message: '请选择',
+        trigger: 'blur'
+    }]
+}
+
+export const reagentFormRules = {
+    leiBie: [{
+        required: true,
+        message: '请选择类别',
+        trigger: 'change'
+    }],
+    shiJiMingCheng: [{
+        required: true,
+        message: '请填写试剂名称',
+        trigger: 'blur'
+    }],
+    piHao: [{
+        required: true,
+        message: '请填写试剂批号',
+        trigger: 'blur'
+    }],
+    changJia: [{
+        required: true,
+        message: '请填写试剂厂家',
+        trigger: 'blur'
+    }],
+    youXiaoQi: [{
+        required: true,
+        message: '请选择试剂有效期',
+        trigger: 'change'
+    }]
+}
+
+export const configFormRules = {
+    target: [{
+        required: true,
+        message: '请输入',
+        trigger: 'change'
+    }],
+    targetKey: [{
+        required: true,
+        message: '请输入',
+        trigger: 'change'
+    }],
+    sn: [{
+        required: true,
+        message: '请输入',
+        trigger: 'change'
+    }],
+    icon: [{
+        required: false,
+        message: '请输入',
+        trigger: 'change'
+    }],
+    'methods[].name': [{
+        required: true,
+        message: '请输入',
+        trigger: 'change'
+    }],
+    'methods[].methodName': [{
+        required: true,
+        message: '请输入',
+        trigger: 'change'
+    }],
+    'methods[].methodType': [{
+        required: true,
+        message: '请输入',
+        trigger: 'change'
+    }],
+    'methods[].sn': [{
+        required: true,
+        message: '请输入',
+        trigger: 'change'
+    }],
+    'methods[].isBasic': [{
+        required: true,
+        message: '请输入',
+        trigger: 'change'
+    }],
+    'methods[].isDisabled': [{
+        required: true,
+        message: '请输入',
+        trigger: 'change'
+    }],
+    'methods[].isPublic': [{
+        required: true,
+        message: '请输入',
+        trigger: 'change'
+    }],
+    'methods[].step': [{
+        required: true,
+        message: '请输入',
+        trigger: 'change'
+    }],
+    'methods[].criterion': [{
+        required: true,
+        message: '请输入',
+        trigger: 'change'
+    }],
+    'methods[].params': [{
+        required: true,
+        message: '请输入',
+        trigger: 'change'
+    }]
+}
+
+export const reagentType = [
+    {
+        value: '质控品',
+        label: '质控品'
+    },
+    {
+        value: '标准品',
+        label: '标准品'
+    },
+    {
+        value: '试剂',
+        label: '试剂'
+    }
+]
+
+export const standardOption = [
+    // {
+    //     value: '实验室指定参数',
+    //     label: '基于实验室指定'
+    // },
+    {
+        value: '基于允许总误差TEa',
+        label: '基于允许总误差TEa'
+    },
+    {
+        value: '基于厂商声明参数',
+        label: '基于厂商声明参数'
+    }
+    // {
+    //     value: '生物学变异参数',
+    //     label: '基于生物学变异'
+    // }
+]
+
+export const batchOption = [
+    {
+        value: '0.5000',
+        label: '1/2TEa'
+    },
+    {
+        value: '0.3333',
+        label: '1/3TEa'
+    },
+    {
+        value: '0.2500',
+        label: '1/4TEa'
+    },
+    {
+        value: '0.200',
+        label: '1/5TEa'
+    }
+]
+
+export const rangeOption = [
+    {
+        value: '99',
+        label: '99%'
+    },
+    {
+        value: '95',
+        label: '95%'
+    },
+    {
+        value: '90',
+        label: '90%'
+    }
+]
+
+export const rateOption = [
+    {
+        value: '1',
+        label: '1%'
+    },
+    {
+        value: '5',
+        label: '5%'
+    },
+    {
+        value: '10',
+        label: '10%'
+    }
+]
+
+export const methodTypeOption = [
+    {
+        value: '定性',
+        label: '定性'
+    },
+    {
+        value: '定量',
+        label: '定量'
+    }
+]
+
+export const methodKeyOption = [
+    {
+        type: 'precision',
+        label: '重复性验证',
+        value: '精密度:重复性验证'
+    },
+    {
+        type: 'precision',
+        label: 'EP15精密度验证',
+        value: '精密度:EP15精密度验证'
+    },
+    {
+        type: 'trueness',
+        label: '偏倚评估',
+        value: '正确度:偏倚评估'
+    },
+    {
+        type: 'trueness',
+        label: '使用患者样品验证',
+        value: '正确度:使用患者样品验证'
+    },
+    {
+        type: 'trueness',
+        label: '使用定值参考物质验证',
+        value: '正确度:使用定值参考物质验证'
+    },
+    {
+        type: 'linearRange',
+        label: '平均斜率评价法',
+        value: '线性区间:平均斜率评价法'
+    },
+    {
+        type: 'linearRange',
+        label: 'EP6线性评价',
+        value: '线性区间:EP6线性评价'
+    },
+    {
+        type: 'linearRange',
+        label: '线性稀释回收法',
+        value: '线性区间:线性稀释回收法'
+    }
+]
+
+export const paramColumn = [
+    { label: '名称', key: 'label', width: '180px' },
+    { label: '编码', key: 'key', width: '150px' },
+    { label: '默认值', key: 'default', minWidth: '120px' },
+    { label: '最小值', key: 'min', type: 'number', min: 0, precision: 0, width: '150px' },
+    { label: '最大值', key: 'max', type: 'number', min: 0, precision: 0, width: '150px' },
+    { label: '精度', key: 'precision', type: 'number', min: 0, max: 4, width: '120px' },
+    { label: '是否显示', key: 'isVisible', type: 'switch', width: '100px' },
+    { label: '是否只读', key: 'isReadonly', type: 'switch', width: '100px' }
+]
+
+export const paramList = []
+
+export const formulaList = [
+    {
+        label: '室内标准差',
+        key: 'sl',
+        value: '$$S_1=\sqrt{\frac{n-1}{n}·S_r^2+S_b^2}$$'
+    },
+    {
+        label: '批内标准差',
+        key: 'sr',
+        value: '$$S_r=\sqrt{\frac{\sum_{d=1}^D\sum_{i-1}^n(x_{di}-\overline{x_d})^2}{D(n-1)}}$$'
+    },
+    {
+        label: '批间方差',
+        key: 'vb',
+        value: '$$S_b^2=\frac{\sum_{d=1}^D(\overline{x_d}-\overset{=}{x})^2}{D-1}$$'
+    }
+]
+
+export const formulaColumn = [
+    { label: '名称', key: 'label', width: '200px', type: 'select', options: formulaList },
+    { label: '编码', key: 'key', width: '150px' },
+    { label: '表达式', key: 'value', minWidth: '220px' }
+]
+
+export const chartColumn = [
+    { label: '名称', key: 'label', width: '200px' },
+    { label: '编码', key: 'key', width: '150px', visible: false },
+    { label: '配置项', key: 'value', minWidth: '220px', type: 'textarea' }
+]
+
+export const chartList = []
+
+export const cmConfg = {
+    mode: 'text/html',
+    theme: 'eclipse',
+    // 是否显示行号
+    lineNumbers: true,
+    indentWithTabs: false,
+    smartIndent: true,
+    matchBrackets: true,
+    styleActiveLine: true,
+    lineWrapping: true,
+    indentUnit: 4,
+    tabSize: 4,
+    lineWiseCopyCut: true
+}

+ 179 - 0
src/views/business/performance/constants/options.js

@@ -0,0 +1,179 @@
+const grid = {
+    left: '2%',
+    right: '2%',
+    bottom: '2%',
+    top: '12%',
+    containLabel: true
+}
+
+const titleStyle = {
+    fontSize: 14,
+    fontWeight: 'bold',
+    color: '#333'
+}
+
+export const LROption = {
+    grid,
+    dataset: [
+        {
+            source: [
+                [96.24, 11.35],
+                [33.09, 85.11],
+                [57.6, 36.61],
+                [36.77, 27.26],
+                [20.1, 6.72],
+                [45.53, 36.37],
+                [110.07, 80.13],
+                [72.05, 20.88],
+                [39.82, 37.15],
+                [48.05, 70.5],
+                [0.85, 2.57],
+                [51.66, 63.7],
+                [61.07, 127.13],
+                [64.54, 33.59],
+                [35.5, 25.01],
+                [226.55, 664.02],
+                [188.6, 175.31],
+                [81.31, 108.68]
+            ]
+        },
+        {
+            transform: {
+                type: 'ecStat:regression',
+                config: { method: 'polynomial', order: 1 }
+            }
+        },
+        {
+            transform: {
+                type: 'ecStat:regression',
+                config: { method: 'polynomial', order: 2 }
+            }
+        }
+    ],
+    title: {
+        text: '线性回归图',
+        subtext: '',
+        sublink: '',
+        left: 'center',
+        top: 8,
+        textStyle: titleStyle
+    },
+    tooltip: {
+        trigger: 'axis',
+        axisPointer: {
+            type: 'cross'
+        }
+    },
+    xAxis: {
+        name: 'x轴',
+        nameLocation: 'center',
+        nameTextStyle: {
+            fontSize: 14
+        },
+        nameGap: 20,
+        splitLine: {
+            show: false
+        }
+    },
+    yAxis: {
+        name: 'y轴',
+        nameLocation: 'center',
+        nameTextStyle: {
+            fontSize: 14
+        },
+        nameGap: 20,
+        min: 0,
+        splitLine: {
+            lineStyle: {
+                type: 'dashed'
+            }
+        }
+    },
+    series: [
+        {
+            name: 'scatter',
+            type: 'scatter'
+        },
+        {
+            name: 'line',
+            type: 'line',
+            smooth: true,
+            datasetIndex: 1,
+            symbolSize: 0.1,
+            symbol: 'circle',
+            label: { show: true, fontSize: 14 },
+            labelLayout: { dx: -20 },
+            encode: { label: 2, tooltip: 1 }
+        },
+        {
+            name: 'line2',
+            type: 'line',
+            smooth: true,
+            datasetIndex: 2,
+            symbolSize: 0.1,
+            symbol: 'circle',
+            label: { show: true, fontSize: 14 },
+            labelLayout: { dx: -20 },
+            encode: { label: 2, tooltip: 1 }
+        }
+    ]
+}
+
+export const SPOption = {
+    xAxis: {},
+    yAxis: {},
+    grid,
+    title: {
+        text: '散点图',
+        left: 'center',
+        top: 8,
+        textStyle: titleStyle
+    },
+    series: [
+        {
+            symbolSize: 10,
+            data: [
+                [10.0, 8.04],
+                [8.07, 6.95],
+                [13.0, 7.58],
+                [9.05, 8.81],
+                [11.0, 8.33],
+                [14.0, 7.66],
+                [13.4, 6.81],
+                [10.0, 6.33],
+                [14.0, 8.96],
+                [12.5, 6.82],
+                [9.15, 7.2],
+                [11.5, 7.2],
+                [3.03, 4.23],
+                [12.2, 7.83],
+                [2.02, 4.47],
+                [1.05, 3.33],
+                [4.05, 4.96],
+                [6.03, 7.24],
+                [12.0, 6.26],
+                [12.0, 8.84],
+                [7.08, 5.82],
+                [5.02, 5.68]
+            ],
+            type: 'scatter',
+            markLine: {
+                symbol: ['none', 'none'], // 去掉箭头
+                itemStyle: {
+                    normal: { lineStyle: { type: 'solid', color: 'blue' },
+                        label: { show: false, position: 'left' }}
+                },
+                data: [{
+                    name: 'Y 轴值为 100 的水平线',
+                    yAxis: 7.24
+                    // valueDim: 'close'
+                }, {
+                    name: 'Y 轴值为 100 的水平线',
+                    yAxis: 1.24
+                    // valueDim: 'close'
+                }
+                ]
+            }
+        }
+    ]
+}

+ 513 - 0
src/views/business/performance/experimental.vue

@@ -0,0 +1,513 @@
+<template>
+    <el-dialog
+        v-loading="loading"
+        :visible.sync="dialogVisible"
+        :close-on-click-modal="false"
+        :close-on-press-escape="false"
+        :show-close="false"
+        append-to-body
+        fullscreen
+        class="dialog experimental-dialog"
+        top="0"
+        @open="loadData"
+        @close="closeDialog"
+    >
+        <el-form
+            ref="form"
+            :label-width="formLabelWidth"
+            :model.sync="form"
+            :rules="rules"
+            class="config-form"
+            :class="readonly ? 'readonly-form' : ''"
+            @submit.native.prevent
+        >
+            <div v-if="loadCompleted" class="config-form-container">
+                <div class="left">
+                    <experimental-desc
+                        :step="configData.step"
+                        :criterion="configData.criterion"
+                        :formulas="configData.formulas"
+                        :references="configData.references"
+                        :readonly="readonly"
+                    />
+                    <basic-info :info="form" :readonly="readonly" />
+                    <reagent-info :info="form.reagentPoList" :readonly="readonly" />
+                    <param-info
+                        v-if="$utils.isNotEmpty(configData.params)"
+                        :form-id="formId"
+                        :info="form.shiYanCanShu"
+                        :config-data="configData.params"
+                        :readonly="readonly"
+                        @updateParams="handleUpdateParams"
+                    />
+                </div>
+                <div class="right">
+                    <experimental-data
+                        :exp-data="form.jiSuanJieGuo"
+                        :form-id="formId"
+                        :readonly="readonly"
+                        @export="handleExport"
+                        @import="handleImport"
+                    />
+                    <precision
+                        v-if="$utils.isNotEmpty(form.jiSuanJieGuo)"
+                        :info="form.jiSuanJieGuo"
+                        :readonly="readonly"
+                        @recalculate="handleRecalculate"
+                    />
+                    <conclusion
+                        :result="form.shiYanJieLun"
+                        :files="form.fuJian"
+                        :readonly="readonly"
+                        @updateData="handleUpdateData"
+                    />
+                </div>
+            </div>
+        </el-form>
+        <div slot="title" class="config-dialog-header">
+            <div class="title">{{ configData.methodName }}</div>
+            <div class="operate">
+                <template v-for="btn in toolbars">
+                    <el-button
+                        v-if="!btn.hidden"
+                        :key="btn.key"
+                        :type="btn.type"
+                        :icon="btn.icon"
+                        :size="btn.size || 'mini'"
+                        @click="handleActionEvent(btn.key)"
+                    >
+                        {{ btn.label }}
+                    </el-button>
+                </template>
+            </div>
+        </div>
+    </el-dialog>
+</template>
+
+<script>
+import { formRules } from './constants/index'
+import ActionUtils from '@/utils/action'
+import { getExperimental, saveExperimental, getConfigDetail, recalculate, exportTemplate, importTemplate } from '@/api/business/pv'
+export default {
+    components: {
+        ExperimentalDesc: () => import('./components/experimental-desc'),
+        BasicInfo: () => import('./components/basic-info'),
+        ReagentInfo: () => import('./components/reagent-info'),
+        ParamInfo: () => import('./components/param-info'),
+        ExperimentalData: () => import('./components/experimental-data'),
+        Conclusion: () => import('./components/conclusion'),
+        Precision: () => import('./report/precision')
+    },
+    props: {
+        visible: {
+            type: Boolean,
+            default: false
+        },
+        readonly: {
+            type: Boolean,
+            default: false
+        },
+        params: {
+            type: Object,
+            default: () => {}
+        }
+    },
+    data () {
+        const { userId } = this.$store.getters || {}
+        return {
+            dialogVisible: this.visible,
+            formLabelWidth: '110px',
+            configData: {},
+            formId: this.params.recordId,
+            form: {
+                xingNengZhiBia: '',
+                fangAnLeiXing: '',
+                bianZhiBuMen: '',
+                shiYanXiangMu: '',
+                shiYanFangFa: '',
+                yangBenLeiXing: '',
+                shiYanYiQi: '',
+                yiQiBianHao: '',
+                kaiShiShiJian: '',
+                jieShuShiJian: '',
+                bianZhiRen: '',
+                createBy: userId,
+                jieGuoDanWei: '',
+                baoLiuXiaoShu: 3,
+                beiZhu: '',
+                reagentPoList: [],
+                shiYanCanShu: {},
+                shiYanShuJu: [],
+                jiSuanJieGuo: {},
+                shiYanJieLun: '',
+                fuJian: ''
+            },
+            rules: formRules,
+            loading: false,
+            loadCompleted: false,
+            toolbars: [
+                { key: 'test', icon: 'ibps-icon-gg', label: '测试', type: 'warning', hidden: this.readonly },
+                { key: 'save', icon: 'ibps-icon-save', label: '保存', type: 'success', hidden: this.readonly },
+                // { key: 'submit', icon: 'ibps-icon-send', label: '提交', type: 'primary', hidden: this.readonly },
+                // { key: 'generate', icon: 'ibps-icon-cube', label: '生成报告', type: 'success', hidden: this.readonly },
+                { key: 'cancel', icon: 'el-icon-close', label: '关闭', type: 'danger' }
+            ]
+        }
+    },
+    watch: {
+        visible: {
+            handler (val, oldVal) {
+                this.dialogVisible = this.visible
+            }
+            // immediate: true
+        }
+    },
+    created () {
+        this.getConfigData(this.params)
+        if (!this.params.recordId) {
+            this.loadCompleted = true
+            return
+        }
+        this.loadData()
+    },
+    methods: {
+        // 获取数据
+        loadData () {
+            this.loading = true
+            getExperimental({ id: this.params.recordId }).then(res => {
+                this.loading = false
+                const data = res.data
+                if (!data) {
+                    return
+                }
+                data.shiYanCanShu = data.shiYanCanShu ? JSON.parse(data.shiYanCanShu) : {}
+                data.shiYanShuJu = data.shiYanShuJu ? JSON.parse(data.shiYanShuJu) : []
+                data.jiSuanJieGuo = data.jiSuanJieGuo ? JSON.parse(data.jiSuanJieGuo) : {}
+                this.form = Object.assign(this.form, data)
+                this.loadCompleted = true
+            }).catch(() => {
+                this.loading = false
+            })
+        },
+        getConfigData ({ targetId, methodId }) {
+            getConfigDetail({ id: targetId }).then(res => {
+                const { target, targetKey, experimentalConfigDetailPoList: methods } = res.data || {}
+                const method = methods.find(i => i.id === methodId) || {}
+                this.configData = {
+                    target,
+                    targetKey,
+                    ...method,
+                    params: this.$utils.isNotEmpty(method.params) ? JSON.parse(method.params) : [],
+                    formulas: this.$utils.isNotEmpty(method.formulas) ? JSON.parse(method.formulas) : []
+                }
+                console.log(this.configData)
+            })
+        },
+        handleActionEvent (key) {
+            switch (key) {
+                case 'save':
+                case 'submit':
+                    this.handleSubmit('submit', true)
+                    break
+                case 'generate':
+                    this.handleGenerate()
+                    break
+                case 'cancel':
+                    this.handleCancel()
+                    break
+                case 'test':
+                    this.handleTest()
+                    break
+                default:
+                    break
+            }
+        },
+        handleSave (key, callback) {
+            this.submitForm(key, callback)
+        },
+        handleSubmit (key, showMsg, callback) {
+            this.$refs.form.validate((valid) => {
+                if (!valid) {
+                    return this.$message.warning('请完善表单必填项后再进行操作!')
+                }
+                this.submitForm(key, showMsg, callback)
+                // this.$confirm('确定要提交数据吗?', '提示', {
+                //     confirmButtonText: '确定',
+                //     cancelButtonText: '取消',
+                //     type: 'warning',
+                //     showClose: false,
+                //     closeOnClickModal: false,
+                //     closeOnPressEscape: false
+                // }).then(() => {
+                //     this.submitForm('submit', callback)
+                // })
+            })
+        },
+        handleGenerate () {
+            this.$message.info('waiting...')
+        },
+        submitForm (key, showMsg, callback) {
+            const { shiYanCanShu, shiYanShuJu, jiSuanJieGuo, ...rest } = this.form || {}
+            const submitData = {
+                ...rest,
+                shiYanCanShu: this.$utils.isNotEmpty(shiYanCanShu) ? JSON.stringify(shiYanCanShu) : null,
+                shiYanShuJu: this.$utils.isNotEmpty(shiYanShuJu) ? JSON.stringify(shiYanShuJu) : null,
+                jiSuanJieGuo: this.$utils.isNotEmpty(jiSuanJieGuo) ? JSON.stringify(jiSuanJieGuo) : null,
+                xingNengZhiBia: this.configData.target,
+                fangAnLeiXing: this.configData.methodName,
+                zhiBiaoId: this.params.targetId,
+                fangFaKey: this.params.methodKey,
+                fangFaId: this.params.methodId,
+                id: this.formId
+            }
+            const isEdit = !!this.formId
+            // 提交数据
+            saveExperimental(submitData).then(res => {
+                this.formId = res.data
+                if (showMsg) {
+                    this.$message.success('保存成功')
+                }
+                if (callback) {
+                    callback()
+                }
+                // 提交时且有实验数据时,重新计算
+                if (key === 'submit' && isEdit && this.$utils.isNotEmpty(shiYanShuJu)) {
+                    recalculate({ id: this.formId }).then(res => {
+                        this.form.jiSuanJieGuo = res.data
+                        this.form.shiYanJieLun = res.data.reportResult
+                    })
+                }
+            })
+        },
+        handleCancel () {
+            this.closeDialog()
+        },
+        closeDialog () {
+            this.$emit('update:visible', false)
+            this.$emit('refresh')
+        },
+        handleUpdateParams (value) {
+            this.form.shiYanCanShu = value
+        },
+        handleUpdateData (value) {
+            this.form = {
+                ...this.form,
+                ...value
+            }
+        },
+        handleExport () {
+            this.handleSubmit('beforeExport', false, () => {
+                exportTemplate({ id: this.formId }).then(res => {
+                    ActionUtils.download(res.data, `${this.form.fangAnLeiXing}-${this.form.shiYanXiangMu}.xlsx`)
+                })
+            })
+        },
+        handleImport () {
+            this.handleSubmit('beforeImport', false, () => {
+                this.importData()
+            })
+        },
+        importData () {
+            const input = document.createElement('input')
+            input.type = 'file'
+            input.accept = '.xlsx'
+            input.onchange = event => {
+                const file = event.target.files[0]
+                const reader = new FileReader()
+                reader.onload = event => {
+                    const data = new FormData()
+                    data.append('id', this.formId)
+                    data.append('applyFiles', file)
+                    importTemplate(data).then(res => {
+                        this.$message.success('实验数据导入成功')
+                        this.form.jiSuanJieGuo = res.data
+                    }).catch(({ state, cause }) => {
+                        const errMsg = JSON.parse(cause)
+                        let msgContent = ''
+                        Object.keys(errMsg).forEach(key => {
+                            let msgItem = '<div >'
+                            errMsg[key].forEach(item => {
+                                msgItem += `<div>${item}</div>`
+                            })
+                            msgContent += `<div><div style="font-weight: bold;">${key}:</div>${msgItem}<div>`
+                        })
+                        this.$confirm(`<div style="font-size: 14px;">${msgContent}</div>`, '数据校验失败,请根据以下提示完善您的数据!', {
+                            confirmButtonText: '确认',
+                            showClose: false,
+                            showCancelButton: false,
+                            closeOnClickModal: false,
+                            dangerouslyUseHTMLString: true,
+                            customClass: 'errorTips',
+                            type: 'error'
+                        }).then(() => {}).catch(() => {})
+                    })
+                }
+                reader.readAsBinaryString(file)
+            }
+            input.click()
+        },
+        handleRecalculate () {
+            this.submitForm('save', () => {
+                recalculate({ id: this.formId }).then(res => {
+                    this.$message.success('重新计算成功')
+                    this.form.jiSuanJieGuo = res.data
+                })
+            })
+        },
+        handleTest () {
+            const o = {
+                xingNengZhiBia: '精密度',
+                fangAnLeiXing: 'EP15-A3精密度评价',
+                bianZhiBuMen: '1166703356459089920',
+                shiYanXiangMu: '测试项目',
+                shiYanFangFa: '测试方法',
+                yangBenLeiXing: '测试样本类型',
+                shiYanYiQi: '测试实验仪器',
+                yiQiBianHao: 'jyk-test-001',
+                kaiShiShiJian: '2024-05-01 09:00',
+                jieShuShiJian: '2024-05-05 17:00',
+                bianZhiRen: '1166772479054577664',
+                createBy: '1166771426615623680',
+                jieGuoDanWei: 'mmol/L',
+                baoLiuXiaoShu: 3,
+                beiZhu: '测试数据',
+                reagentPoList: [
+                    {
+                        changJia: 'BIO-RIO',
+                        leiBie: '质控品',
+                        piHao: 'test001',
+                        shiJiMingCheng: '生化质控品',
+                        youXiaoQi: '2025-05-01'
+                    },
+                    {
+                        changJia: 'BIO-RIO',
+                        leiBie: '校准品',
+                        piHao: 'test002',
+                        shiJiMingCheng: '生化校准品',
+                        youXiaoQi: '2025-06-01'
+                    },
+                    {
+                        changJia: 'BIO-RIO',
+                        leiBie: '标准物',
+                        piHao: 'test001',
+                        shiJiMingCheng: '标准物',
+                        youXiaoQi: '2025-05-01'
+                    }
+                ],
+                shiYanCanShu: {
+                    specimensNum: 2,
+                    repeatNum: 3,
+                    days: 5,
+                    isConvert: false,
+                    standard: '基于允许总误差TEa',
+                    tea: 10,
+                    batchCVS: 0.25,
+                    batchCVSValue: 2.50,
+                    dailyCVS: 0.33,
+                    dailyCVSValue: 3.33
+                },
+                shiYanShuJu: [],
+                shiYanJieLun: '测试达标',
+                shenHeRen: '1166673437578493952',
+                baoGaoShiJian: '2024-05-06',
+                fuJian: '1239940596743798784'
+            }
+            this.form = JSON.parse(JSON.stringify(o))
+        }
+    }
+}
+</script>
+<style lang="scss" scoped>
+    .experimental-dialog {
+        ::v-deep {
+            .el-dialog__header {
+                padding: 15px 20px 16px;
+            }
+        }
+        .config-dialog-header {
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+            .title {
+                line-height: 24px;
+                font-size: 18px;
+                color: #303133;
+            }
+        }
+        .config-form {
+            &-container {
+                position: relative;
+                width: 100%;
+                height: calc(100vh - 60px);
+                overflow: auto;
+                display: flex;
+                .left, .right {
+                    width: 50%;
+                    // min-height: 100%;
+                    height: 100%;
+                    overflow-y: auto;
+                    padding: 15px 20px;
+                    box-sizing: border-box;
+                    .info-container {
+                        margin-bottom: 20px;
+                        &:last-child {
+                            margin-bottom: 0;
+                        }
+                    }
+                    ::v-deep {
+                        .info-item {
+                            .title {
+                                height: 20px;
+                                line-height: 20px;
+                                font-size: 16px;
+                                font-weight: bold;
+                                margin-bottom: 10px;
+                                .ibps-icon-star {
+                                    color: #FB9600;
+                                    margin-right: 5px;
+                                }
+                            }
+                            .el-form-item {
+                                margin-bottom: 0 !important;
+                                &__label {
+                                    font-size: 14px !important;
+                                    color: #606266;
+                                }
+                                &__content {
+                                    .el-input, .el-select, .el-input-number {
+                                        width: 100%;
+                                    }
+                                    .el-textarea .el-input__count {
+                                        padding: 0 5px;
+                                        line-height: initial;
+                                    }
+                                    .el-radio, .el-checkbox {
+                                        margin-right: 10px;
+                                    }
+                                }
+                            }
+                            .el-table th.el-table__cell > .cell, .el-table td.el-table__cell {
+                                color: #606266;
+                                font-size: 14px !important;
+                            }
+                            .el-button--mini {
+                                padding: 5px 12px;
+                            }
+                        }
+                    }
+                }
+                .left {
+                    &::before {
+                        content: '';
+                        width: 0;
+                        height: 100%;
+                        border-left: 1px dashed #dcdfe6;
+                        position: absolute;
+                        top: 0;
+                        left: 50%;
+                    }
+                }
+            }
+        }
+    }
+</style>

+ 214 - 0
src/views/business/performance/index.vue

@@ -0,0 +1,214 @@
+<template>
+    <div class="performance-config">
+        <div class="page-title">性能验证</div>
+        <!-- <div class="page-toolbar">
+            <el-button type="primary">配置 <i class="ibps-icon-cogs" /></el-button>
+        </div> -->
+        <div class="page-container">
+            <el-card
+                v-for="(item, index) in performanceList"
+                :key="index"
+                :header="item.target"
+                class="performance-card"
+            >
+                <template slot="header">
+                    <div class="card-title">
+                        <span>{{ item.target }}</span>
+                        <el-button
+                            size="mini"
+                            icon="ibps-icon-cogs"
+                            circle
+                            @click="handleConfig(item.id)"
+                        />
+                    </div>
+                </template>
+                <div
+                    v-for="t in item.methods"
+                    :key="t.sn"
+                    class="card-item"
+                >
+                    <div
+                        :class="t.isDisabled === 'Y' ? 'method-btn disabled' : 'method-btn'"
+                        @click="handleEdit(item.id, t)"
+                    >{{ t.methodName }}</div>
+                </div>
+            </el-card>
+        </div>
+        <config
+            v-if="showConfig"
+            :visible.sync="showConfig"
+            :target-id="targetId"
+            @close="() => showConfig = false"
+            @callback="loadData"
+        />
+        <experimental
+            v-if="showExperimental"
+            :visible.sync="showExperimental"
+            :params="params"
+            @close="() => showExperimental = false"
+        />
+    </div>
+</template>
+
+<script>
+import { getConfigList } from '@/api/business/pv'
+export default {
+    components: {
+        Config: () => import('./config'),
+        Experimental: () => import('./experimental')
+    },
+    data () {
+        return {
+            showConfig: false,
+            showExperimental: false,
+            params: {},
+            targetId: '',
+            performanceList: []
+        }
+    },
+    created () {
+        this.loadData()
+    },
+    methods: {
+        loadData () {
+            const params = {
+                parameters: [],
+                requestPage: {
+                    pageNo: 1,
+                    limit: 200
+                },
+                sorts: []
+            }
+            const dataList = []
+            getConfigList(params).then(res => {
+                const { dataResult = [] } = res.data || {}
+                dataResult.forEach(item => {
+                    const config = item.config ? JSON.parse(item.config) : []
+                    dataList.push({
+                        id: item.id,
+                        sn: item.sn,
+                        target: item.target,
+                        targetKey: item.targetKey,
+                        icon: item.icon,
+                        methods: config.sort((a, b) => a.sn - b.sn)
+                    })
+                })
+                this.performanceList = dataList.sort((a, b) => a.sn - b.sn)
+            })
+        },
+        handleConfig (id) {
+            this.targetId = id
+            this.showConfig = true
+        },
+        handleEdit (targetId, { id, methodKey, isDisabled }) {
+            if (isDisabled === 'Y') {
+                return
+            }
+            this.params = {
+                targetId,
+                methodId: id,
+                methodKey
+            }
+            this.showExperimental = true
+        }
+    }
+}
+</script>
+<style lang="scss" scoped>
+    .performance-config {
+        .page-title {
+            font-size: 18px;
+            font-weight: bold;
+            text-align: center;
+            margin-top: 10px;
+        }
+        .page-toolbar {
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+            margin-top: 10px;
+            .toolbar-left {
+                display: flex;
+                align-items: center;
+                .toolbar-title {
+                    font-size: 16px;
+                    font-weight: bold;
+                    margin-right: 10px;
+                }
+            }
+            .toolbar-right {
+                display: flex;
+            }
+        }
+        .page-container {
+            display: flex;
+            flex-wrap: wrap;
+            justify-content: flex-start;
+            max-height: calc(100vh - 90px);
+            overflow: auto;
+            .performance-card {
+                width: 350px;
+                margin: 20px;
+                transition: all 0.3s ease;
+                &:hover {
+                    // transform: scale(1.05);
+                    box-shadow: 0 0 15px rgba(0, 0, 0, 0.2);
+                }
+                ::v-deep {
+                    .el-card__header {
+                        padding: 16px 20px;
+                        font-weight: bold;
+                        .card-title {
+                            display: flex;
+                            justify-content: space-between;
+                            align-items: center;
+                        }
+                    }
+                    .el-card__body {
+                        padding: 16px;
+                        display: flex;
+                        flex-wrap: wrap;
+                        justify-content: flex-start;
+                        .card-item {
+                            margin-bottom: 16px;
+                            margin-right: 20px;
+                            &:nth-child(even) {
+                                margin-right: 0;
+                            }
+                            &:nth-last-child(-n+2) {
+                                margin-bottom: 0;
+                            }
+                            .method-btn {
+                                display: flex;
+                                justify-content: center;
+                                align-items: center;
+                                min-height: 32px;
+                                width: 120px;
+                                padding: 10px 14px;
+                                font-size: 14px;
+                                background-color: #409eff;
+                                color: #fff;
+                                border: none;
+                                border-radius: 5px;
+                                cursor: pointer;
+                                background-image: linear-gradient(to right, #409eff 0%, #409eff 50%, #4CAF50 50%, #4CAF50 100%);
+                                background-size: 200% 100%;
+                                transition: background-position 0.5s ease;
+                                &:hover {
+                                    background-position: 100% 0;
+                                }
+                            }
+                            .disabled {
+                                cursor: not-allowed;
+                                background-image: linear-gradient(to right, #909399 0%, #909399 50%, #C0C4CC 50%, #C0C4CC 100%);
+                                &:hover {
+                                    background-position: 0 0
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+</style>

+ 211 - 0
src/views/business/performance/record.vue

@@ -0,0 +1,211 @@
+<template>
+    <div class="main-container">
+        <ibps-crud
+            ref="crud"
+            :display-field="title"
+            :height="height"
+            :data="listData"
+            :toolbars="listConfig.toolbars"
+            :search-form="listConfig.searchForm"
+            :pk-key="pkKey"
+            :columns="listConfig.columns"
+            :row-handle="listConfig.rowHandle"
+            :pagination="pagination"
+            :loading="loading"
+            @action-event="handleAction"
+            @sort-change="handleSortChange"
+            @pagination-change="handlePaginationChange"
+            @row-dblclick="handleRowDblclick"
+        >
+            <template slot="time" slot-scope="scope">
+                <div>起:{{ scope.row.kaiShiShiJian }}</div>
+                <div>止:{{ scope.row.jieShuShiJian }}</div>
+            </template>
+        </ibps-crud>
+        <Experimental
+            v-if="showConfig"
+            :visible.sync="showConfig"
+            :params="params"
+            :readonly="readonly"
+            @refresh="loadData"
+            @close="() => showConfig = false"
+        />
+    </div>
+</template>
+
+<script>
+import { queryExperimental, removeExperimental } from '@/api/business/pv'
+import ActionUtils from '@/utils/action'
+import FixHeight from '@/mixins/height'
+
+export default {
+    components: {
+        Experimental: () => import('./experimental')
+    },
+    mixins: [FixHeight],
+    data () {
+        const { userList = [] } = this.$store.getters || {}
+        const userOption = userList.map(item => ({ label: item.userName, value: item.userId }))
+        return {
+            userOption,
+            title: '性能验证记录',
+            pkKey: 'id', // 主键  如果主键不是pk需要传主键
+            loading: true,
+            height: document.clientHeight,
+            listData: [],
+            pagination: {},
+            sorts: {},
+            showConfig: false,
+            readonly: false,
+            params: {},
+            targetOption: [],
+            methodOption: [],
+            listConfig: {
+                toolbars: [
+                    { key: 'search' },
+                    { key: 'remove' }
+                ],
+                searchForm: {
+                    forms: [
+                        // { prop: 'Q^name_^SL', label: '性能指标', fieldType: 'select', options: this.targetOption },
+                        // { prop: 'Q^fang_an_lei_xing_^SL', label: '方案类型', fieldType: 'select', options: this.methodOption },
+                        { prop: 'Q^name_^SL', label: '性能指标' },
+                        { prop: 'Q^fang_an_lei_xing_^SL', label: '方案类型' },
+                        { prop: 'Q^shi_yan_xiang_mu_^SL', label: '实验项目' },
+                        { prop: 'Q^shi_yan_fang_fa_^SL', label: '实验方法' },
+                        { prop: 'Q^yang_ben_lei_xing^SL', label: '样本类型' },
+                        { prop: 'Q^shi_yan_yi_qi_^SL', label: '实验仪器' },
+                        { prop: ['Q^create_time_^DL', 'Q^create_time_^DG'], label: '创建时间', fieldType: 'daterange' }
+                    ]
+                },
+                // 表格字段配置
+                columns: [
+                    { prop: 'xingNengZhiBia', label: '性能指标', tags: [], minWidth: 110 },
+                    { prop: 'fangAnLeiXing', label: '方案类型', tags: [], width: 125 },
+                    { prop: 'shiYanXiangMu', label: '实验项目', width: 120 },
+                    { prop: 'shiYanFangFa', label: '实验方法', width: 120 },
+                    { prop: 'yangBenLeiXing', label: '样本类型', width: 100 },
+                    { prop: 'shiYanYiQi', label: '实验仪器', width: 120 },
+                    { prop: 'timeRange', label: '实验时间', slotName: 'time', width: 140 },
+                    { prop: 'dataStatus', label: '状态', width: 80 },
+                    { prop: 'bianZhiRen', label: '实验人', tags: userOption, width: 90 },
+                    { prop: 'createBy', label: '评价人', tags: userOption, width: 90 },
+                    { prop: 'createTime', label: '创建时间', dateFormat: 'yyyy-MM-dd HH:mm', sortable: 'custom', width: 130 }
+                ],
+                rowHandle: {
+                    effect: 'display',
+                    actions: [
+                        { key: 'edit', label: '编辑', type: 'primary', icon: 'ibps-icon-edit' }
+                        // { key: 'report', label: '实验报告', type: 'success', icon: 'ibps-icon-file-text-o' }
+                    ]
+                }
+            }
+        }
+    },
+    created () {
+        this.loadData()
+    },
+    methods: {
+        // 加载数据
+        loadData () {
+            this.loading = true
+            queryExperimental(this.getSearchFormData()).then(res => {
+                ActionUtils.handleListData(this, res.data)
+                this.loading = false
+            }).catch(() => {
+                this.loading = false
+            })
+        },
+        /**
+         * 获取格式化参数
+         */
+        getSearchFormData () {
+            return ActionUtils.formatParams(
+                this.$refs['crud'] ? this.$refs['crud'].getSearcFormData() : {},
+                this.pagination,
+                this.sorts
+            )
+        },
+        /**
+         * 处理分页事件
+         */
+        handlePaginationChange (page) {
+            ActionUtils.setPagination(this.pagination, page)
+            this.loadData()
+        },
+        /**
+         * 处理排序
+         */
+        handleSortChange (sort) {
+            ActionUtils.setSorts(this.sorts, sort)
+            this.loadData()
+        },
+        /**
+         * 查询
+         */
+        search () {
+            this.loadData()
+        },
+        /**
+         * 处理按钮事件
+         */
+        handleAction (command, position, selection, data) {
+            switch (command) {
+                case 'search':
+                    ActionUtils.setFirstPagination(this.pagination)
+                    this.search()
+                    break
+                case 'edit':
+                    this.handleEdit(data, command)
+                    break
+                case 'report':
+                    this.handleReport(data)
+                    break
+                case 'remove':
+                    ActionUtils.removeRecord(selection).then((ids) => {
+                        this.handleRemove(ids)
+                    }).catch(() => {})
+                    break
+                default:
+                    break
+            }
+        },
+        /**
+         * 处理编辑
+         */
+        async handleEdit ({ id, zhiBiaoId, fangFaId, fangFaKey }, key) {
+            this.params = {
+                targetId: zhiBiaoId,
+                methodId: fangFaId,
+                methodKey: fangFaKey,
+                recordId: id
+            }
+            this.readonly = key === 'detail'
+            this.showConfig = true
+        },
+        handleReport (data) {
+            console.log('wwww')
+        },
+        /**
+         * 处理删除
+         */
+        handleRemove (ids) {
+            // return this.$message.warning('避免误删测试数据,联系开发删除')
+            removeExperimental({ ids }).then(() => {
+                ActionUtils.removeSuccessMessage()
+                this.search()
+            }).catch(() => {})
+        },
+        handleRowDblclick (row) {
+            this.handleEdit(row, 'detail')
+        }
+    }
+}
+</script>
+<style lang="scss">
+    .attachment-uploader-dialog {
+        .el-dialog__body {
+            height: calc(57vh - 100px) !important;
+        }
+    }
+</style>

+ 245 - 0
src/views/business/performance/report/precision.vue

@@ -0,0 +1,245 @@
+<template>
+    <div class="info-container">
+        <div class="experimental-result info-item">
+            <div class="title">
+                <i class="ibps-icon-star" />
+                <span>计算结果</span>
+                <!-- <el-button
+                    key="recalculate"
+                    type="primary"
+                    icon="refresh"
+                    size="mini"
+                    plain
+                    class="right-btn"
+                    @click="handleRecalculate"
+                >重新计算</el-button> -->
+            </div>
+            <el-tabs v-model="activeTab" @tab-click="handleClick">
+                <el-tab-pane
+                    v-for="(tab, tabIndex) in tabs"
+                    :key="tabIndex"
+                    :label="tab"
+                    :name="tab"
+                >
+                    <div class="content">
+                        <div v-for="(table, tableIndex) in reportData" :key="tableIndex" class="table-item">
+                            <div class="table-title">
+                                <span>{{ table.title }}</span>
+                            </div>
+                            <el-table
+                                :data="table.list"
+                                border
+                                stripe
+                                highlight-current-row
+                                style="width: 100%"
+                                max-height="250px"
+                                :show-header="!table.hideHeader"
+                                :span-method="getSpanMethod(table)"
+                            >
+                                <el-table-column
+                                    v-for="h in table.header"
+                                    :key="h.prop"
+                                    :prop="h.children && h.children.length ? '' : h.prop"
+                                    :label="h.label"
+                                    header-align="center"
+                                    align="center"
+                                >
+                                    <template slot="header" slot-scope="scope">
+                                        <span v-html="scope.column.label" />
+                                    </template>
+                                    <template slot-scope="scope">
+                                        <span v-if="h.slot" v-html="scope.row[h.prop]" />
+                                        <span v-else>{{ scope.row[h.prop] }}</span>
+                                    </template>
+                                    <el-table-column
+                                        v-for="c in h.children"
+                                        :key="c.prop"
+                                        :prop="c.prop"
+                                        :label="c.label"
+                                        header-align="center"
+                                        align="center"
+                                    >
+                                        <template slot="header" slot-scope="scope">
+                                            <span v-html="scope.column.label" />
+                                        </template>
+                                        <template slot-scope="scope">
+                                            <span v-if="c.slot" v-html="scope.row[c.prop]" />
+                                            <span v-else>{{ scope.row[c.prop] }}</span>
+                                        </template>
+                                    </el-table-column>
+                                </el-table-column>
+                            </el-table>
+                        </div>
+                    </div>
+                </el-tab-pane>
+            </el-tabs>
+        </div>
+        <chart
+            v-if="chartData.length"
+            :chart-data="chartData"
+            :readonly="readonly"
+        />
+    </div>
+</template>
+<script>
+export default {
+    components: {
+        chart: () => import('../components/chart.vue')
+    },
+    props: {
+        info: {
+            type: Object,
+            default: () => {}
+        },
+        formula: {
+            type: Array,
+            default: () => []
+        },
+        readonly: {
+            type: Boolean,
+            default: false
+        }
+    },
+    data () {
+        return {
+            tabs: [],
+            activeTab: '',
+            activeTabIndex: 0,
+            reportData: [],
+            chartData: []
+        }
+    },
+    watch: {
+        info: {
+            handler (v) {
+                if (v.sheetDTO && v.sheetDTO.length) {
+                    this.tabs = v.sheetDTO.map(item => item.title)
+                    this.activeTab = this.tabs[this.activeTabIndex] || ''
+                    this.initData(this.activeTabIndex)
+                }
+            },
+            immediate: true,
+            deep: true
+        },
+        activeTabIndex (v) {
+            this.initData(v)
+        }
+    },
+    mounted () {
+        // this.tabs = this.info.sheetDTO && this.info.sheetDTO.length ? this.info.sheetDTO.map(item => item.title) : []
+        // this.activeTab = this.tabs[this.activeTabIndex] || ''
+        // this.initData(this.activeTabIndex)
+    },
+    methods: {
+        handleClick (v) {
+            this.activeTab = v.name
+            this.activeTabIndex = this.tabs.findIndex(item => item === v.name) || 0
+        },
+        initData (index) {
+            const { sheetDTO } = this.info || {}
+            const { reportDataDTO, chartDataDTO } = sheetDTO[index] || {}
+            const reportData = Object.keys(reportDataDTO).map(k => ({
+                title: k,
+                header: reportDataDTO[k].header || this.getTableHeader(reportDataDTO[k].list),
+                hideHeader: this.$utils.isEmpty(reportDataDTO[k].header),
+                list: reportDataDTO[k].list
+            })).sort((a, b) => a.title.localeCompare(b.title))
+            const chartData = chartDataDTO ? Object.keys(chartDataDTO).map(k => ({
+                title: k,
+                id: chartDataDTO[k].name,
+                data: chartDataDTO[k].data,
+                note: chartDataDTO[k].note,
+                option: chartDataDTO[k].option
+            })).sort((a, b) => a.title.localeCompare(b.title)) : []
+            this.chartData = chartData
+            this.reportData = reportData
+        },
+        getTableHeader (data) {
+            return data.length ? Object.keys(data[0]).map(key => ({ label: key, prop: key, slot: true })) : []
+        },
+        getSpanMethod (table) {
+            return (params) => {
+                return this.objectSpanMethod(params, table)
+            }
+        },
+        objectSpanMethod ({ row, column, rowIndex, columnIndex }, { list, header }) {
+            // 判断当前列是否需要进行同值合并
+            if (header[columnIndex].merge) {
+                const firstSameRowIndex = this.getFirstSameRowIndex(list, rowIndex, column.property)
+                const firstSameColIndex = this.getFirstSameColIndex(list, columnIndex, rowIndex)
+                return {
+                    rowspan: rowIndex === firstSameRowIndex ? this.getRowSpanCount(list, rowIndex, column.property) : 0,
+                    colspan: 1
+                }
+            }
+            return {
+                rowspan: 1,
+                colspan: 1
+            }
+        },
+        // 获取行同值合并的起始下标
+        getFirstSameRowIndex (data, rowIndex, prop) {
+            for (let i = rowIndex; i >= 0; i--) {
+                if (data[i][prop] !== data[rowIndex][prop]) {
+                    return i + 1
+                }
+            }
+            return 0
+        },
+        // 获取列同值合并的起始下标
+        getFirstSameColIndex (data, colIndex, rowIndex) {
+            for (let i = colIndex; i >= 0; i--) {
+                if (data[rowIndex][i] !== data[rowIndex][colIndex]) {
+                    return i
+                }
+            }
+            return 0
+        },
+        getRowSpanCount (data, rowIndex, prop) {
+            let count = 1
+            for (let i = rowIndex + 1; i < data.length; i++) {
+                if (data[i][prop] === data[rowIndex][prop]) {
+                    count++
+                } else {
+                    break
+                }
+            }
+            return count
+        },
+        handleRecalculate () {
+            this.$emit('recalculate')
+        }
+    }
+}
+</script>
+<style lang="scss" scoped>
+    .info-container {
+        .experimental-result {
+            position: relative;
+            .title {
+                position: relative;
+                .right-btn {
+                    position: absolute;
+                    top: -2px;
+                    right: 0;
+                }
+            }
+            .content {
+                padding: 10px;
+                position: relative;
+                .table-item {
+                    .table-title {
+                        margin: 15px 0 10px 0;
+                        font-size: 14px;
+                        font-weight: bold;
+                    }
+                    &:first-child {
+                        .table-title {
+                            margin-top: 0;
+                        }
+                    }
+                }
+            }
+        }
+    }
+</style>