liling 1 yıl önce
ebeveyn
işleme
778a08c496

+ 2 - 2
public/config.js

@@ -3,8 +3,8 @@ STATIC_CONFIG = {}
 STATIC_CONFIG.lowpowerUrl = 'http://192.168.1.224:9910/#/detection'
 STATIC_CONFIG.golang = 'http://127.0.0.1:8006'
 // 后台服务接口地址
-STATIC_CONFIG.proxyUrl = 'http://192.168.1.236:8080'
-//STATIC_CONFIG.proxyUrl = 'http://1.14.75.75:8080'
+//STATIC_CONFIG.proxyUrl = 'http://192.168.1.236:8080'
+STATIC_CONFIG.proxyUrl = 'http://1.14.75.75:7070'
 // 添加配置代理ip
 // 首页资源
 // STATIC_CONFIG.logo = ''

+ 45 - 17
src/views/device/check.vue

@@ -55,7 +55,17 @@
         <div class="top">
           <!-- 检测参数 和右侧按钮一起 移入 CheckParames.vue 原来的先保留,测试通过删除 -->
           <el-row>
+            <CheckParamesMaster104
+              v-if="isMaster104 == true"
+              ref="checkparames"
+              :cur-plan-detail="deviceDetail.planDetail"
+              :device-status="deviceStatus"
+              :play-data-id="deviceDetail.curPlanId"
+              @updateDisable="getDisable"
+              @updateDeviceStatus="getDeviceStatus"
+            />
             <CheckParames
+              v-else
               ref="checkparames"
               :cur-plan-detail="deviceDetail.planDetail"
               :device-status="deviceStatus"
@@ -109,7 +119,19 @@
                 }"
               />
               <!-- 采样模块 -->
+              <SampleMaster104
+                v-if="isMaster104==true"
+                :key="sampleUpdate1"
+                :component-parames="{
+                  'deviceName':deviceGet.name,
+                  'curPlanId':deviceDetail.curPlanId,
+                  'productId':deviceDetail.planDetail.productId,
+                  'status':sampleStatus,
+                  'isDisable':isDisable
+                }"
+              />
               <Sample
+                v-if="isMaster104==false"
                 :key="sampleUpdate"
                 :component-parames="{
                   'deviceName':deviceGet.name,
@@ -119,6 +141,14 @@
                   'isDisable':isDisable
                 }"
               />
+              <ModelReportMaster104
+                v-if="isMaster104"
+                :report-parames="{
+                  'deviceName':'',
+                  'curPlanId':deviceDetail.curPlanId
+                }"
+                @toggleModel="closeModel"
+              />
             </div>
           </el-col>
         </el-row>
@@ -134,6 +164,7 @@ import { mapGetters } from 'vuex'
 import { editRow, httpGet, httpPostJson, getOptionIndex } from '@/api/common-action'
 import { singleAction } from '@/api/device'
 // 当前检测参数
+import CheckParamesMaster104 from './components/CheckParames_master104'
 import CheckParames from './components/CheckParames'
 // 步骤组件
 import CheckSteps from './components/CheckSteps.vue'
@@ -141,6 +172,10 @@ import CheckSteps from './components/CheckSteps.vue'
 import Example from './tables/Example'
 // 采样表格
 import Sample from './tables/Sample'
+// 采样表格(仅主站104检测才显示)
+import SampleMaster104 from './tables/Sample_master104'
+// 报文列表(仅主站104检测才显示)
+import ModelReportMaster104 from './components/ModelReport_master104'
 // 导入总线
 import { EventBus } from '@/main.js'
 
@@ -149,10 +184,13 @@ import PowerCentent from './protocols/PowerCentent'
 export default {
   components: {
     CheckParames,
+    CheckParamesMaster104,
     Example,
     Sample,
     CheckSteps,
-    PowerCentent
+    PowerCentent,
+    ModelReportMaster104,
+    SampleMaster104
   },
   filters: {
     formartStatus: function(params) {
@@ -163,6 +201,7 @@ export default {
   },
   data() {
     return {
+      isMaster104: false, // 是否主站104
       // 默认数据
       deviceDetail: {
         deviceId: 0,
@@ -240,24 +279,8 @@ export default {
   },
   computed: {
     ...mapGetters(['roles'])
-    /* 使用 computed 获取实时状态测试
-    realTimeStatus: function() {
-      console.log('realTimeStatus=', this.deviceStatus)
-      return {
-        deviceStatus: this.deviceStatus
-      }
-    } */
   },
   watch: {
-    // $router(to, from) {
-    //   console.log('watch curPlanId stated to=', to)
-    //   console.log('watch curPlanId stated from=', from)
-    //   this.deviceDetail.curPlanId = this.$route.query.curPlanId
-    //   this.deviceDetail.suiteId = this.$route.query.suiteId
-    //   this.deviceDetail.deviceId = this.$route.query.deviceId
-    //   this.initFunctions()
-    // }
-    // '$route': 'watchRouter',
     $route: {
       handler() {
         // 路由改变时先清空定时器,避免切换页面时生成多个定时器
@@ -337,6 +360,11 @@ export default {
       httpGet(`/test/plan/${this.deviceDetail.curPlanId}`).then((response) => {
         // console.log('获取当前方案详细信息 res=', response)
         this.deviceDetail.planDetail = response
+        if (response.productType === 'IEC104') {
+          this.isMaster104 = true
+        } else {
+          this.isMaster104 = false
+        }
       })
 
       // 获取当前方案相关步骤

+ 594 - 0
src/views/device/components/CheckParames_master104.vue

@@ -0,0 +1,594 @@
+<!-- 当前检测参数 -->
+<template>
+  <div>
+    <el-col :span="21" class="left">
+      <div>
+        <div class="contorl-title">
+          当前检测参数
+        </div>
+        <div class="contorl-sub-title">
+          <div class="title-item">
+            <div class="title-lable">检测协议:</div>
+            <div class="title-value">{{ curPlanDetail.productType }}</div>
+          </div>
+          <div class="title-item">
+            <div class="title-lable">检测方案:</div>
+            <div class="title-value">{{ curPlanDetail.name }}</div>
+          </div>
+        </div>
+        <div class="contorl-parame">
+          <!-- 动态添加新的输入框/下拉框 -->
+          <div v-for="(field, index) in fields" :key="index" class="parame-item">
+            <div class="parame-lable" :for="field.name">
+              {{ field.label }}:
+            </div>
+            <div class="parame-value">
+              <input v-if="field.type === 'text'" :id="field.name" v-model="field.value" maxlength="64" :placeholder="field.placeholder || '请填写'+field.label" :disabled="isEdit" :type="field.type" :name="field.name" size="mini">
+              <input
+                v-else-if="field.type === 'number'"
+                :id="field.name"
+                v-model="field.value"
+                :min="field.min || 1"
+                :max="field.max || 65535"
+                :disabled="isEdit"
+                :type="field.type"
+                :name="field.name"
+                maxlength="19"
+                size="mini"
+                :placeholder="field.placeholder || '请填写'+field.label"
+                style="bgcolor:red"
+              >
+              <el-select v-else-if="field.type === 'select'" v-model="field.value" :disabled="isEdit" filterable :placeholder="`请选择${field.label}`" size="mini">
+                <el-option
+                  v-for="item in field.options"
+                  :key="item.id"
+                  :label="item.name"
+                  :value="item.id"
+                />
+              </el-select>
+            </div>
+          </div>
+        </div>
+        <div class="contorl-buttons">
+          <div v-if="isEdit==true" class="item">
+            <el-button
+              :disabled="isDisable"
+              type="primary"
+              icon="el-icon-edit"
+              size="mini"
+              @click="editForm"
+            >编辑</el-button>
+          </div>
+        </div>
+      </div>
+    </el-col>
+    <el-col :span="3" class="right">
+      <!-- 运行 -->
+      <div class="play">
+        <el-image
+          :class="clickClass.start"
+          :src="require('@/assets/device/play.png')"
+          fit="contain"
+          @click="controlClick('start')"
+        />
+      </div>
+      <!-- 停止 -->
+      <div class="stop">
+        <el-image
+          :class="clickClass.stop"
+          :src="require('@/assets/device/stop.png')"
+          fit="contain"
+          @click="controlClick('stop')"
+        />
+      </div>
+      <!-- 刷新 -->
+      <div class="refresh">
+        <el-image
+          :class="clickClass.refresh"
+          :src="require('@/assets/device/refresh.png')"
+          fit="contain"
+          @click="controlClick('refresh')"
+        />
+      </div>
+    </el-col>
+    <!-- 编辑弹出的模态框套表单 -->
+    <el-dialog title="检测参数信息" :close-on-click-modal="false" :visible.sync="dialogParamsFormVisible">
+      <div v-for="(field, index) in fields" :key="index" class="parame-item-form">
+        <div class="parame-lable" :for="field.name">
+          {{ field.label }}:
+        </div>
+        <div class="parame-value">
+          <input v-if="field.type === 'text'" :id="field.name" v-model="field.value" maxlength="64" :placeholder="field.placeholder || '请填写'+field.label" :type="field.type" :name="field.name" size="mini">
+          <input
+            v-else-if="field.type === 'number'"
+            :id="field.name"
+            v-model="field.value"
+            :min="field.min || 1"
+            :max="field.max || 65535"
+            :type="field.type"
+            :name="field.name"
+            maxlength="19"
+            size="mini"
+            :placeholder="field.placeholder || '请填写'+field.label"
+            style="bgcolor:red"
+            :disabled="Boolean(field.name=='unitAddr')"
+          >
+          <el-select v-else-if="field.type === 'select'" v-model="field.value" filterable :placeholder="`请选择${field.label}`" size="mini">
+            <el-option
+              v-for="item in field.options"
+              :key="item.id"
+              :label="item.name"
+              :value="item.id"
+            />
+          </el-select>
+        </div>
+      </div>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="cancelForm">取 消</el-button>
+        <el-button type="primary" @click="saveForm">确 定</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { httpPostJson, httpGet, putData } from '@/api/common-action'
+// 导入总线
+import { EventBus } from '@/main.js'
+export default {
+  props: {
+    // 当前检测方案数据
+    curPlanDetail: {
+      type: Object,
+      default: function() {
+        return
+      }
+    },
+    // playData.id
+    playDataId: {
+      type: String,
+      default: '0'
+    },
+    // 设备状态
+    deviceStatus: {
+      type: Object,
+      default: function() {
+        return
+      }
+    }
+  },
+  data() {
+    return {
+      dialogParamsFormVisible: false, // 参数编辑框显示状态
+      // 默认表单数据
+      fields: [
+        // 表单项数组,每个对象表示一个表单项
+      ],
+      formItems: {
+        ipAddr: {
+          type: 'text',
+          name: 'ipAddr',
+          label: '从站IP',
+          value: '127.0.0.1'
+        },
+        ipPort: {
+          type: 'number',
+          name: 'ipPort',
+          label: '从站端口',
+          value: ''
+        },
+        t1: {
+          type: 'number',
+          name: 't1',
+          label: 'T1',
+          value: ''
+        },
+        t2: {
+          type: 'number',
+          name: 't2',
+          label: 'T2',
+          value: ''
+        },
+        t3: {
+          type: 'number',
+          name: 't3',
+          label: 'T3',
+          value: ''
+        },
+        k: {
+          type: 'number',
+          name: 'k',
+          label: 'K',
+          value: ''
+        },
+        w: {
+          type: 'number',
+          name: 'w',
+          label: 'W',
+          value: ''
+        },
+        // #TODO: 以后改为仅整型
+        unitAddr: {
+          type: 'number',
+          name: 'unitAddr',
+          label: '子站链路地址',
+          value: '1'
+        },
+        unitIdentifier: {
+          type: 'number',
+          name: 'unitIdentifier',
+          label: 'ASDU公区地址',
+          value: '0'
+        }
+      },
+      // 是否禁用
+      isDisable: true,
+      // 是否编辑状态
+      isEdit: true,
+      // 点击样式
+      clickClass: {
+        start: 'default',
+        stop: 'default',
+        refresh: 'default'
+      }
+    }
+  },
+  watch: {
+    // 刚打开网页就会执行这里,所以 initFunction 就先不执行了
+    // 当前选中方案发生改变时
+    curPlanDetail: function(_newQuestion, _oldQuestion) {
+      // console.log('watch curPlanDetail=', this.curPlanDetail)
+      // 获取当前方案协议对应的参数,用于生成 当前检测参数表单
+      this.makeParamesForm()
+      // 更新设备运行状态
+      this.updateStatus()
+    }
+  },
+  mounted() {
+    this.$nextTick(() => {
+      // 网页加载完成后执行
+      // console.log('CheckParames mounted curPlanDetail=', this.curPlanDetail)
+      // console.log('CheckParames mounted playDataId=', this.playDataId)
+      // 初始化
+      this.initFunctions()
+    })
+  },
+  methods: {
+    // // 初始化
+    initFunctions() {
+      this.makeParamesForm()
+      // 获取状态,并进行后续相关操作
+      this.updateStatus()
+    },
+    // 点击编辑
+    editForm() {
+      this.dialogParamsFormVisible = true
+    },
+    // 取消编辑
+    cancelForm() {
+      this.makeParamesForm()
+      this.dialogParamsFormVisible = false
+    },
+    // 提交保存
+    saveForm() {
+      if (this.formItems.unitAddr.value > 65535) {
+        this.$message({
+          message: '设备地址不能大于65535',
+          type: 'error',
+          offset: window.screen.height / 3
+        })
+        return
+      }
+      this.isEdit = true
+      // 开始运行前,先保存参数
+      // 接口 修改检测参数 /test/plan
+      // 保存数据
+      const putUrl = `/test/plan`
+      const formData = this.formatFormData(this.formItems)
+      // console.log('saveForm formData=', formData)
+      putData(putUrl, formData).then(_res => {
+        this.$message({
+          message: '保存成功',
+          type: 'success',
+          offset: window.screen.height / 3
+        })
+        this.dialogParamsFormVisible = false
+      })
+    },
+    // 根据状态显示相关数据
+    showByStatus() {
+      /*
+      1:获取当前设备运行状态,是否运行中
+        1.1:是
+          禁用 四处按钮
+        1.2:否
+          1.2.1 显示编辑按钮
+            点击 编辑 按钮 出现 保存 和 取消 按钮
+      */
+      if (this.deviceStatus.state === 'RUNNING') {
+        this.isDisable = true
+      } else {
+        this.isDisable = false
+      }
+      // console.log('showByStatus this.deviceStatus=', this.deviceStatus)
+      // console.log('showByStatus started')
+    },
+    // 生成 当前检测参数表单
+    makeParamesForm() {
+      // 获取状态,并进行后续相关操作
+      this.showByStatus()
+      // 使用循环优化如上赋值
+      // const values = Object.values(this.curPlanDetail)
+      const keys = Object.keys(this.formItems)
+      keys.forEach(key => {
+        this.formItems[key].value = this.curPlanDetail[key]
+      })
+      // console.log('makeParamesForm started ', this.formItems)
+
+      const curProductType = this.curPlanDetail.productType
+      // 先清空一次 fields ,避免造成叠加
+      this.fields = []
+      switch (curProductType) {
+        case 'IEC104':
+          // IP
+          // 端口
+          this.fields.push(this.formItems.ipAddr)
+          this.fields.push(this.formItems.ipPort)
+          this.fields.push(this.formItems.unitAddr)
+          this.fields.push(this.formItems.unitIdentifier)
+          this.fields.push(this.formItems.t1)
+          this.fields.push(this.formItems.t2)
+          this.fields.push(this.formItems.t3)
+          this.fields.push(this.formItems.k)
+          this.fields.push(this.formItems.w)
+          break
+        default:
+          break
+      }
+    },
+    // 格式化当前表单数据
+    formatFormData(obj) {
+      return {
+        id: this.playDataId,
+        productType: this.curPlanDetail.productType,
+        name: this.curPlanDetail.name,
+        ipAddr: obj.ipAddr.value,
+        ipPort: obj.ipPort.value,
+        unitAddr: obj.unitAddr != null ? obj.unitAddr.value : 0,
+        unitIdentifier: obj.unitIdentifier != null ? obj.unitIdentifier.value : 1,
+        t1: obj.t1.value,
+        t2: obj.t2.value,
+        t3: obj.t3.value,
+        k: obj.k.value,
+        w: obj.w.value
+      }
+    },
+    // 控制按钮
+    controlClick(actStr) {
+      // 设置图片点击效果
+      this.clickEffect(actStr)
+      switch (actStr) {
+        case 'start':
+          this.$confirm(
+            '是否开始检测?',
+            `开始`,
+            {
+              confirmButtonText: '确定',
+              cancelButtonText: '取消',
+              type: 'warning'
+            }
+          )
+            .then(() => {
+              // 更新禁用状态
+              this.updateDisable(true)
+              // 触发lowpower的事件 弃用
+              this.playData = this.formatFormData(this.formItems)
+
+              // 接口 开始检测 /test/execute/:runner/start
+              // runner 3200000208
+              const postUrl = `/test/execute/${this.playDataId}/start`
+              httpPostJson(postUrl, this.playData).then(_res => {
+                // 提交保存运行参数
+                // 提交成功后,更新设备状态
+                this.updateStatus()
+                EventBus.$emit('refreshManualFn')
+                // console.log('controlClick this.clickClass=', this.clickClass)
+                this.$message({
+                  type: 'success',
+                  message: '已开始',
+                  offset: window.screen.height / 3
+                })
+              }).catch(_err => {
+                this.updateDisable(false)
+              })
+              // }
+            })
+            .catch(() => {
+              this.$message({
+                type: 'info',
+                message: '已取消',
+                offset: window.screen.height / 3
+              })
+            })
+          break
+        case 'stop':
+          this.$confirm(
+            '是否停止检测?',
+            `停止`,
+            {
+              confirmButtonText: '确定',
+              cancelButtonText: '取消',
+              type: 'warning'
+            }
+          )
+            .then(() => {
+              // 更新禁用状态
+              this.updateDisable(false)
+              // console.log('controlAction 执行开始 actStr=', actStr)
+              httpPostJson(`/test/execute/${this.playDataId}/${actStr}`).then((_response) => {
+                // 停止成功,更新状态
+                this.updateStatus()
+                // console.log('controlAction 执行完成 rs=', response)
+                this.$message({
+                  type: 'success',
+                  message: '已停止',
+                  offset: window.screen.height / 3
+                })
+              })
+            })
+            .catch(() => {
+              this.$message({
+                type: 'info',
+                message: '已取消',
+                offset: window.screen.height / 3
+              })
+            })
+          break
+        case 'refresh':
+          this.updateStatus()
+          break
+
+        default:
+          this.$message({
+            message: '操作参数未知',
+            type: 'error',
+            offset: window.screen.height / 3
+          })
+          break
+      }
+      // 设置点击特效 - 结束
+      // this.clickClass = 'default'
+    },
+    // 点击效果
+    clickEffect(actStr) {
+      // 设置点击特效 - 开始
+      this.clickClass[`${actStr}`] = 'clicked'
+      setTimeout(() => {
+        // 延迟200毫秒更新样式
+        this.clickClass[`${actStr}`] = 'default'
+      }, 200)
+    },
+    // 更新当前状态
+    updateStatus() {
+      this.$emit('updateDeviceStatus', {
+        id: '0',
+        stateName: '读取中...'
+      })
+      // 接口 测试状态 /test/execute/:runner/state
+      // :runner 3200000208
+      const getUrl = `/test/execute/${this.playDataId}/state`
+      httpGet(getUrl).then((response) => {
+        // 更新设备状态
+        this.$emit('updateDeviceStatus', response)
+      })
+    },
+    // 更新禁用状态
+    updateDisable(bVal) {
+      this.isDisable = bVal
+      // 在运行一开始时就要切换运行状态,用于禁用以下:
+      // 设备名称 下 方案选择 下拉框
+      // 检测参数中所有输入框/下拉框
+      // 用例 添加按钮
+      // 采样 添加按钮
+      this.$emit('updateDisable', bVal)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+//
+.contorl-title {
+  font-weight: bold;
+  padding-bottom: 20px;
+  display: flex;
+  justify-content: left;
+
+  .contorl-buttons {
+    padding-left: 40px;
+    // width: 50%;
+    // border: 1px solid #F00;
+    .item {
+      display: flex;
+    }
+  }
+}
+.contorl-sub-title {
+  display: flex;
+  .title-item {
+    display: flex;
+    padding-right: 20px;
+  }
+}
+.contorl-parame {
+  display: flex;
+  flex-wrap: wrap;
+  color: #707070;
+  font-size: 12px;
+  .parame-item {
+    display: flex;
+    padding-right: 10px;
+    line-height: 40px;
+    justify-content: left;
+    .parame-lable {
+      white-space: nowrap;
+    }
+    .parame-value {
+      font-weight: bold;
+      width:70%;
+    }
+    input{
+      width: 100px;
+    }
+  }
+}
+.parame-item-form {
+    width: 100%;
+    text-align: center;
+    display: flex;
+    padding-right: 10px;
+    line-height: 40px;
+    justify-content: left;
+    .parame-lable {
+      float: left;
+      width: 40%;
+      white-space: nowrap;
+      text-align: right;
+    }
+    .parame-value {
+      float: left;
+      font-weight: bold;
+      width:60%;
+      text-align: left;
+    }
+    input{
+      width: 200px;
+    }
+  }
+.right {
+  display: flex;
+  align-items: center;
+  height: 80px;
+  // border: 1px solid #f00;
+  justify-content: right;
+  .play,.stop {
+    padding-right: 10px;
+  }
+  .play,.stop,.refresh {
+    cursor: pointer;
+    .clicked {
+      filter: Alpha(opacity=50);
+    }
+    .default {
+      filter: Alpha(opacity=100);
+    }
+    .el-image {
+      height: 50px;
+    }
+  }
+  .refresh .el-image {
+    height: 44px;
+  }
+}
+:root .clicked { opacity: .5; filter: none; }
+</style>

+ 394 - 0
src/views/device/components/ModelReport_master104.vue

@@ -0,0 +1,394 @@
+<!-- 设备报文 -->
+<template>
+  <div class="table-container" style="padding-top:20px;">
+    <div class="buttons">
+      <div class="title-main">报文</div>
+      <div class="left">
+        <div class="search">
+          <el-form ref="searchForm" :model="searchForm" class="search-form" label-width="80px">
+            <el-form-item class="search-parames">
+              <el-select v-model="searchForm.selectVal" filterable @change="changeSelect">
+                <el-option
+                  v-for="item in searchForm.selectOptions"
+                  :key="item.id"
+                  :label="item.name"
+                  :value="item.id"
+                />
+              </el-select>
+            </el-form-item>
+          </el-form>
+        <!-- 下拉框改变化,自动就刷新数据了,不需要再点击搜索按钮
+          <div class="search-button">
+            <el-button class="dark-button" icon="el-icon-search" size="small" @click="refreshReport()">搜索</el-button>
+          </div> -->
+        </div>
+      </div>
+      <div class="right">
+        <div class="export">
+          <a target="_blank" :href="`${APP_BASE_API}/test/execute/${reportParames.curPlanId}/message.xls`"><el-button icon="el-icon-download" class="light-button" size="small">导出</el-button></a>
+        </div>
+        <div class="refresh">
+          <el-button class="light-button" icon="el-icon-refresh-left" size="small" @click="refreshReport({page:paginationNumber-1,limit:pageLimit},true)">刷新</el-button>
+        </div>
+        <div class="clear">
+          <el-button type="primary" icon="el-icon-delete" class="dark-button" size="small" @click="clearReport()">清空</el-button>
+        </div>
+      </div>
+    </div>
+    <el-row type="flex">
+      <el-col :span="18">
+        <div class="left-report">
+          <!-- 表格 -->
+          <el-table :data="deviceReportData" :height="tableHeight" @row-click="analysisReport">
+            <!-- id隐藏不显示,作为操作数据依据 -->
+            <el-table-column v-if="false" property="id" label="id" />
+            <el-table-column property="occur" label="时间" width="160px" />
+            <el-table-column property="channelName" label="通道号" width="100px" />
+            <el-table-column property="evtName" label="事件" width="75px" />
+            <el-table-column property="hex" label="报文" />
+            <el-table-column
+              fixed="right"
+              label="操作"
+              width="125px"
+            >
+              <template slot-scope="scope">
+                <el-button v-if="scope.row.id" v-clipboard:copy="scope.row.hex" v-clipboard:success="clipboardSuccess" class="table-act" type="text" icon="el-icon-delete" size="small">复制</el-button>
+                <el-button v-if="scope.row.id" class="table-act" type="text" icon="el-icon-edit" size="small">解析</el-button>
+              <!-- <el-button v-clipboard:copy="copyClipData" v-clipboard:success="clipboardSuccess" class="table-act" type="text" icon="el-icon-delete" size="small" @click="copyReport(scope.row)">复制</el-button> -->
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
+      </el-col>
+      <el-col :span="6">
+        <div class="right-report">
+          <!-- 解析结果显示 -->
+          <div class="analysis-result">
+            <div v-if="comonentVar.analysisResult.length<=0" class="empty-result">暂无解析结果</div>
+            <div v-else>
+              <ul
+                v-for="item in comonentVar.analysisResult"
+                :key="item.index"
+                class="result"
+              >
+                <!-- <li class="key"> {{ item.first }} </li>
+                <li class="value"> {{ item.second }} </li> -->
+                <li class="oneBox">
+                  <span>参数:{{ item.first }}</span>
+                  <span>值:{{ item.second }}</span>
+                </li>
+              </ul>
+            </div>
+          </div>
+        </div>
+      </el-col>
+    </el-row>
+    <el-row>
+      <el-col>
+        <pagination
+          background
+          layout="pager"
+          :limit="pageLimit"
+          :total="paginationTotalElements"
+          :current-page.sync="paginationNumber"
+          @pagination="refreshReport"
+        />
+      </el-col>
+    </el-row>
+  </div>
+</template>
+<script>
+import { mapGetters } from 'vuex'
+import { delRecord, download, httpGet } from '@/api/common-action'
+import clipboard from '@/directive/clipboard/index.js'
+import Pagination from '@/components/Pagination'
+
+export default {
+  name: 'ModelReport',
+  directives: {
+    clipboard
+  },
+  components: { Pagination },
+  // report-parames
+  props: {
+    reportParames: {
+      type: Object,
+      default: function() {
+        return {}
+      }
+    }
+  },
+  data() {
+    return {
+      APP_BASE_API: window.STATIC_CONFIG.proxyUrl,
+      // 报文表格数据
+      deviceReportData: [
+        {
+          'id': '读取中...',
+          'occur': '读取中...',
+          'runnerId': '读取中...',
+          'channelId': '读取中...',
+          'channelName': '读取中...',
+          'evtId': '读取中...',
+          'bytes': '读取中...'
+        }
+      ],
+      // 组件变量
+      comonentVar: {
+        deviceReportTitle: '读取中...',
+        analysisResult: ''
+      },
+      // 搜索表单
+      searchForm: {
+        selectOptions: [
+          { id: 'pt15m', name: '最近15分钟' },
+          { id: 'pt4h', name: '最近4小时' },
+          { id: 'pt24h', name: '最近24小时' }
+        ],
+        // 查询时间范围
+        // PT4H是一种时间表示格式,表示持续时间为4小时。其中“PT”代表“持续时间(duration)”,后面的“4H”代表4个小时(hours)的意思。
+        // PT2H 2小时 “PT3M”(3分钟)和“PT30S”(30秒)
+        selectVal: 'pt4h'
+      },
+      copyClipData: {
+        copyTestName: '剪切板复制测试内容-name',
+        copyTestValue: '剪切板复制测试内容-value'
+      },
+      pageLimit: 20,
+      paginationNumber: 0,
+      paginationTotalElements: 0,
+      // 动态设置table高度
+      tableHeight: 0
+    }
+  },
+  computed: {
+    ...mapGetters([
+      'roles'
+    ])
+  },
+  created() {
+    // 获取后端报文数据
+    // 报文查询 接口 /test/execute/:runner/message
+    // 猜测 runner 应该是 planId
+    // 动态设置表格高度
+    const marginBottom = 440
+    this.tableHeight = window.innerHeight - marginBottom
+
+    this.refreshReport()
+  },
+  mounted() {
+    this.comonentVar.deviceReportTitle = `[${this.reportParames.deviceName}]报文详情`
+  },
+  methods: {
+    // 关闭model
+    toggleModel() {
+      // console.log('toggleModel started')
+      this.$emit('toggleModel', 'ModelReport', false)
+    },
+    // 报文时间改变时执行
+    changeSelect(selectVal) {
+      // console.log('changeSelect selectVal=', selectVal)
+      this.refreshReport()
+      // 清空解析结果
+      this.comonentVar.analysisResult = ''
+    },
+    // 清空报文
+    // swagger 接口 删除通信日志 /test/execute/{id}/message
+    // id 3200000252
+    clearReport() {
+      const delUrl = `/test/execute/${this.reportParames.curPlanId}/message`
+      delRecord(delUrl).then(res => {
+        // 删除成功
+        // 刷新表格
+        this.refreshReport()
+        this.$message({
+          message: '清空成功',
+          type: 'success',
+          duration: 1500,
+          offset: window.screen.height / 3
+        })
+      })
+    },
+    // 刷新
+    refreshReport(pageObj, isRe) {
+      pageObj = pageObj || {
+        page: 0,
+        limit: this.pageLimit
+      }
+
+      // 报文查询 接口 /test/execute/:runner/message
+      // #TODO: 这里要使用新标准,需要添加绝对时间参数的判断
+      const getUrl = `/test/execute/${this.reportParames.curPlanId}/message?period=${this.searchForm.selectVal}&page=${pageObj.page}&size=${pageObj.limit}`
+      // console.log('refreshReport getUrl=', getUrl)
+      httpGet(getUrl).then(res => {
+        // console.log('刷新后端报文数据 httpGet res=', res)
+        this.deviceReportData = res.content
+        this.paginationNumber = res.number + 1
+        this.paginationTotalElements = res.totalElements * 1 // *1 强制转换为数字型
+        if (isRe) {
+          this.$message({
+            message: '刷新成功',
+            type: 'success',
+            duration: 500,
+            offset: window.screen.height / 3
+          })
+        }
+
+        // 返回数据示例
+        // "content": [
+        //      {
+        //         "id": "4",
+        //         "occur": "2023-06-11 08:31:11",
+        //         "runnerId": "999",
+        //         "channelId": null,
+        //         "channelName": "389434b4",
+        //         "evtId": "READ",
+        //         "bytes": "000100000003018401"
+        //     }],
+        //     "totalPages": 1,
+        //     "totalElements": "4",
+        //     "size": 20,
+        //     "number": 0,
+        //     "numberOfElements": 4
+        //   }
+      })
+    },
+    // 导出
+    exportReport() {
+      // 接口 报文导出 /test/execute/:runner/message.xls
+      // :runner 3200000315
+
+      download(`/test/execute/${this.reportParames.curPlanId}/message.xls`).then(res => {
+        const link = document.createElement('a')
+        const blob = new Blob([res], {
+          type: 'application/vnd.ms-excel;charset=utf-8'
+        })
+        link.style.display = 'none'
+        link.href = URL.createObjectURL(blob)
+        link.setAttribute('download', `导出报文-${this.componentParames.curPlanId}.xls`)
+        document.body.appendChild(link)
+        link.click()
+        document.body.removeChild(link)
+      })
+    },
+    // 解析报文
+    analysisReport(row) {
+      // 报文解析接口 /test/message/:id
+      // 猜测 这个id 应该是前面 报文查询 接口 返回的id
+      httpGet(`/test/message/${row.id}`).then(res => {
+        if (res.items && res.items.length > 0) {
+          this.comonentVar.analysisResult = res.items
+        }
+      })
+    },
+    // 复制报文 成功回调
+    clipboardSuccess(val) {
+      this.$message({
+        message: '复制成功:' + val.text,
+        type: 'success',
+        duration: 1500,
+        offset: window.screen.height / 3
+      })
+    }
+  }
+}
+</script>
+<style lang="scss" scoped>
+.title-main {
+  color: #111;
+  font-weight: bold;
+}
+.buttons {
+  display: flex;
+  justify-content: space-between;
+  .left {
+    .search{
+      display: flex;
+      justify-content: left;
+      .search-form {
+        .search-parames {
+          text-align: left;
+        }
+      }
+      .search-button {
+        padding-left: 20px;
+      }
+    }
+  }
+  .right{
+    display: flex;
+    justify-content: right;
+    padding-bottom: 20px;
+    .clear,.refresh {
+      padding-left: 20px;
+    }
+  }
+}
+.right-report {
+  height: 100%;
+  // 解析结果
+  .analysis-result {
+    margin-left: 4px;
+    border:1px #eaefef solid;
+    background-color: #fcfdfd;
+    width: 100%;
+    height: 100%;
+    padding: 0;
+    // display: flex;
+    // justify-content: center;
+    // align-items: flex-start;
+    .result {
+      width: 100%;
+      display: flex;
+      justify-content: flex-start;
+      align-items: center;
+      text-align: left;
+      color: #909399;
+      &:nth-child(2n){
+          background-color: #eee;
+        }
+      // .key,.value {
+      //   list-style-type: none;
+      // }
+      // .key {
+      //   width: 80px;
+      //   text-align: right;
+      //   padding-right:10px;
+      //   ::after {
+      //     width:10px;
+      //   }
+      // }
+      .oneBox{
+        width: 100%;
+        list-style-type: none;
+        margin: 0;
+        padding: 0;
+        display: flex;
+        flex-direction: column;
+
+      }
+
+    }
+    .empty-result {
+      text-align: center;
+      color: #ccc;
+    }
+  }
+}
+
+// ::v-deep .diaClass{
+//  position: relative;
+//  top: 10%;
+//  left:25%
+// }
+// 按钮公用样式
+.dark-button {
+  background-color: #00706B;
+  border: 1px #00706B solid;
+  color: #fff;
+}
+.light-button {
+  color: #00706B;
+  border: 1px #00706B solid;
+}
+</style>

+ 528 - 0
src/views/device/tables/Sample_master104.vue

@@ -0,0 +1,528 @@
+<!-- 采样表格
+返回数据示例
+{
+  "id": "24577",
+  "name": "A相电压",
+  "offset": 0,
+  "value": null,
+  "occur": null,
+  "iec104": 24577,
+  "modbus": 40001,
+  "range": "HOLDING_REGISTER"
+},
+无法分页 采样表格无法分页,数据在内存中
+微信聊天记录 边缘代理2开发沟通 - 2023-06-27 采样表格无法分页,数据在内存中
+-->
+<template>
+  <div class="table-container">
+    <!-- 采样模块 -->
+    <div class="device-values">
+      <!-- 采样 -->
+      <div class="title-container">
+        <div class="title-main">采样值</div>
+        <div class="title-right">
+          <div class="check-resaults">
+            <el-button
+              icon="el-icon-refresh-left"
+              class="light-button"
+              size="mini"
+              @click="refreshManual(true)"
+            >刷新</el-button>
+          </div>
+          <div class="check-resaults">
+            <!-- 导入即是上传文件 -->
+            <el-upload
+              ref="upload"
+              :action="importCsvActionStr"
+              :show-file-list="false"
+              :auto-upload="false"
+              accept=".csv"
+              :on-change="submitUpload"
+            >
+              <el-button
+                class="light-button"
+                icon="el-icon-upload2"
+                size="mini"
+              >导入</el-button>
+            </el-upload>
+          </div>
+          <div class="check-resaults">
+            <el-button
+              class="light-button"
+              icon="el-icon-download"
+              size="mini"
+              @click="getPlanExport"
+            >导出</el-button>
+          </div>
+          <div class="check-resaults">
+            <el-button
+              icon="el-icon-plus"
+              type="primary"
+              size="mini"
+              :disabled="componentParames.isDisable"
+              @click="editRow()"
+            >添加</el-button>
+          </div>
+          <div class="check-resaults" style="margin-left: 20px;">
+            <el-button
+              icon="el-icon-plus"
+              type="primary"
+              size="mini"
+              @click="refreshManual(true)"
+            >卡片</el-button>
+          </div>
+          <div class="check-resaults">
+            <el-button
+              icon="el-icon-refresh-left"
+              class="light-button"
+              size="mini"
+              @click="refreshManual(true)"
+            >列表</el-button>
+          </div>
+        </div>
+      </div>
+      <!-- 采样表格 -->
+      <div class="table-container">
+        <el-table :key="tableIsUpdate" v-loading="vLoading" :data="tableData" stripe max-height="300">
+          <el-table-column prop="iec104" label="104点号" width="100px" />
+          <el-table-column prop="name" label="测点名称" />
+          <el-table-column prop="groupingName" label="设备名称" />
+          <el-table-column prop="rangeName" label="点号类型" width="175px">
+            <template slot-scope="scope">
+              <span class="range">{{ scope.row.rangeName }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column prop="offset" label="点号" width="100px" />
+          <el-table-column prop="value" label="值" width="100px" />
+          <el-table-column prop="occur" label="时间" width="175px" />
+          <el-table-column fixed="right" label="操作" width="250px">
+            <template slot-scope="scope">
+              <el-button
+                v-if="scope.row.range=='COIL_STATUS' && scope.row.relationTag!=''"
+                class="table-act"
+                type="text"
+                icon="el-icon-edit"
+                size="small"
+                @click="editRow(scope.$index)"
+              >遥控操作</el-button>
+              <el-button
+                v-if="scope.row.range=='COIL_SHOLDING_REGISTERTATUS' && scope.row.relationTag!=''"
+                class="table-act"
+                type="text"
+                icon="el-icon-edit"
+                size="small"
+                @click="editRow(scope.$index)"
+              >遥调操作</el-button>
+              <el-button
+                class="table-act"
+                type="text"
+                icon="el-icon-edit"
+                size="small"
+                @click="editRow(scope.$index)"
+              >编辑</el-button>
+              <el-button
+                class="table-act"
+                type="text"
+                icon="el-icon-delete"
+                size="small"
+                @click="delRow(scope.$index)"
+              >删除</el-button>
+              <el-button
+                class="table-act"
+                type="text"
+                icon="el-icon-data-line"
+                size="small"
+                @click="sampleEchart(scope.row)"
+              >曲线</el-button>
+            </template>
+          </el-table-column>
+          <!-- 隐藏 列 这里不用显示,但是在编辑的时候需要获取此数据 -->
+          <el-table-column
+            v-if="false"
+            prop="deviceCheckId"
+            label="序号"
+          />
+          <!-- 隐藏 列 -->
+        </el-table>
+      </div>
+      <!-- 采样分页
+      <div class="page-bar">
+        <pagination
+          background
+          layout="pager"
+          :limit="tablePrames.pageLimit"
+          :total="tablePrames.paginationTotalElements"
+          :current-page.sync="tablePrames.paginationNumber"
+          @pagination="getTableData"
+        />
+      </div>
+      -->
+    </div>
+    <!-- 添加/编辑 采样 model -->
+    <ModelFormSample
+      v-if="showModelForm"
+      :model-parames="{
+        'curPlanId':componentParames.curPlanId,
+        'productId':componentParames.productId,
+        'deviceName':componentParames.deviceName,
+        'curSuiteId':componentParames.curSuiteId,
+        'modelFormData':modelFormData
+      }"
+      :sample-data="tableData"
+      @toggleModel="closeModel"
+    />
+    <!-- #TODO: 以后做成公用 dialog ,不再使用 toggleModel 关闭 dialog -->
+    <el-dialog
+      :visible="showEchart"
+      :before-close="closeSampleEchart"
+      :title="curPointName"
+    >
+      <!-- 采样 曲线  model -->
+      <ModelSampleEchart
+        v-if="showEchart"
+        :model-parames="{
+          'curPlanId':componentParames.curPlanId,
+          'curPointId':curPointId,
+          'curPointName':curPointName
+        }"
+      />
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import {
+  httpGet,
+  delRecord,
+  download
+} from '@/api/common-action'
+// 分页
+// import Pagination from '@/components/Pagination'
+// 显示采样曲线
+import ModelSampleEchart from '../components/ModelSampleEchart'
+// 添加/编辑 采样
+import ModelFormSample from '../components/ModelFormSample'
+// 导入总线
+import { EventBus } from '@/main.js'
+export default {
+  name: 'Sample',
+  components: {
+    // Pagination,
+    ModelSampleEchart,
+    ModelFormSample
+  },
+  props: {
+    componentParames: {
+      type: Object,
+      default: function() {
+        return {}
+      }
+    }
+  },
+  data() {
+    return {
+      vLoading: true,
+      tableData: [],
+      // 采样
+      tablePrames: {
+        pageLimit: 5,
+        paginationNumber: 1,
+        paginationTotalElements: 1
+      },
+      // 是否显示 添加/编辑 采样
+      showModelForm: false,
+      // 是否显示曲线
+      showEchart: false,
+      // 当前采样id
+      curPointId: '0',
+      // 当前采样名称
+      curPointName: '0',
+      // 导入csv 请求参数
+      importCsvActionStr: '#',
+      // 表格是否更新标示,用于解决表格内数据变化了不会刷新的问题
+      tableIsUpdate: '0'
+    }
+  },
+  // watch: {
+  //   componentParames: {
+  //     handler() {
+  //       // 监听传入的值 是否有变化 ,如有 刷新表格数据
+  //       // console.log('刷新表格数据 componentParames')
+  //       // this.refreshTableData()
+  //     }
+  //   }
+  // },
+  created() {
+    const envUrl = process.env.VUE_APP_BASE_API
+    // 网页加载完成后执行
+    // 接口 导入测点 /test/plan/:plan/points.csv
+    this.importCsvActionStr = envUrl + `/test/plan/${this.componentParames.curPlanId}/points.csv`
+    // console.log('this.importCsvActionStr=', this.importCsvActionStr)
+    // 在bus上挂载刷新事件
+    EventBus.$on('refreshManualSample', this.refreshManual)
+  },
+  mounted() {
+    this.$nextTick(() => {
+      // 网页加载完成后执行
+      this.initFunctions()
+    })
+  },
+  methods: {
+    //
+    async initFunctions() {
+      // 所有需要加载时初始化的函数都放这里
+      // 获取用例列表数据 ,不传分页信息,即采集默认分页获取第一页数据
+      this.getTableData()
+    },
+    // 导出
+    async getPlanExport() {
+      // 接口 导出测点 /test/plan/:plan/points.csv
+
+      download(`/test/plan/${this.componentParames.curPlanId}/points.csv`).then(res => {
+        const link = document.createElement('a')
+        const blob = new Blob([res], {
+          type: 'application/vnd.ms-excel;charset=utf-8'
+        })
+        link.style.display = 'none'
+        link.href = URL.createObjectURL(blob)
+        link.setAttribute('download', `导出采样数据-${this.componentParames.curPlanId}.csv`)
+        document.body.appendChild(link)
+        link.click()
+        document.body.removeChild(link)
+      })
+    },
+    // 格式化bool值
+    formatBool(val) {
+      const rs = val.not.toString()
+      return rs
+    },
+    // 格式化 测试状态
+    formatState(val) {
+      return '未获取'
+    },
+
+    // 获取采样表格数据
+    getTableData(pageObj) {
+      this.vLoading = true
+      pageObj = pageObj || {
+        page: 0,
+        limit: this.tablePrames.pageLimit
+      }
+      // 获取表格数据
+      // 接口 采样数据查询 /test/execute/:runner/values
+      // :runner 例:3200000208
+      httpGet(`/test/execute/${this.componentParames.curPlanId}/values?page=${pageObj.page}&size=${pageObj.limit}`).then((response) => {
+        this.vLoading = false
+        delete response.code
+        this.tableData = response
+        this.tablePrames.paginationTotalElements = response.totalElements * 1
+        // this.requestData.deviceProtocolOptions = response
+        // 刷新一次状态
+        this.refreshManual(false)
+      })
+    },
+    // 编辑
+    editRow(index) {
+      // console.log('this.tableData[index]=', this.tableData[index].id)
+      // 如果是添加,则index为空,modelFormData也为空
+      this.modelFormData = this.tableData[index]
+      this.showModelForm = true
+    },
+    // 删除
+    delRow(index) {
+      this.$confirm(
+        '此操作将永久删除该记录, 是否继续?',
+        `删除`,
+        {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning'
+        }
+      )
+        .then(() => {
+          // console.log('delRow index=', index)
+          // 接口 删除测点 /product/model/:product/:tag
+          // :product 1100000645
+          // :tag 6386 tag id 为采样列表序号id
+
+          const delUrl = `/product/model/${this.componentParames.productId}/${this.tableData[index].id}`
+          // console.log('delRow delUrl=', delUrl)
+          delRecord(delUrl).then(res => {
+            this.$message({
+              message: '删除成功',
+              type: 'success',
+              offset: window.screen.height / 3
+            })
+            // 后端成功执行后,前端再删除选中行
+            this.tableData.splice(index, 1)
+          })
+        })
+        .catch(() => {
+          this.$message({
+            type: 'info',
+            message: '已取消删除',
+            offset: window.screen.height / 3
+          })
+        })
+    },
+    // 采样值数据获取
+    // 手动刷新时,通过models接口获取实时数据信息
+    // isHand: 是否手动刷新。false表示自动触发 true表示界面刷新按钮操作
+    refreshManual(isHand) {
+      // 默认接口 采样数据(高频刷新) /test/execute/:runner/models
+      var getUrl = `/test/execute/${this.componentParames.curPlanId}/models`
+      httpGet(getUrl).then(res => {
+        if (this.tableData.length > 0) {
+          this.tableData.forEach(item => {
+            res.forEach(ritem => {
+              if (item.id === ritem.id) {
+                item.value = ritem.value
+                item.occur = ritem.occur
+                // 解决tableData无法刷新的问题
+                this.tableIsUpdate = 'sample-' + new Date().getTime()
+              }
+            })
+          })
+        }
+        if (isHand != null && isHand) {
+          this.$message({
+            message: '采样值刷新成功',
+            type: 'success',
+            duration: 500,
+            offset: window.screen.height / 3
+          })
+        }
+        // console.log('this.tableData', this.tableData)
+      })
+    },
+    // 导入
+    submitUpload() {
+      // 上传文件
+      this.$refs.upload.submit()
+      // 上传之后刷新页面查看上传结果
+      this.getTableData()
+    },
+    // 关闭 model
+    closeModel(modelName, modelShow) {
+      // console.log('closeModel modelName=', modelName)
+      // console.log('closeModel modelShow=', modelShow)
+      switch (modelName) {
+        // case 'ModelAddStep':
+        //   this.deviceDetail.showModelAddStep = modelShow
+        //   break
+        case 'ModelFormSample':
+          this.showModelForm = modelShow
+          // 操作过用例模态框 刷新一次表格数据
+          this.getTableData()
+          break
+        case 'ModelSampleEchart':
+          this.showEchart = modelShow
+          break
+        default:
+          console.log('未获取到 modelName')
+          break
+      }
+    },
+    // 曲线
+    sampleEchart(row) {
+      // 接口 /test/execute/:runner/point/:point/chart?period=pt4h
+      // :runner 示例 3200000235
+      // :point 示例 24755
+
+      // const getUrl = `/test/execute/${this.componentParames.curPlanId}/point/0/chart?period=pt4h`
+      // httpGet(getUrl).then(res => {
+      //   // const sampleEchartData = {
+      //   //   xAxis: res.xAxis[0],
+      //   //   yAxis: res.yAxis[0],
+      //   //   series: res.series[0]
+      //   // }
+      //   // const sampleEchartData = {
+      //   //   xAxis: { type: 'category', min: '2023-06-14 06:52:46', max: '2023-06-14 10:52:46' },
+      //   //   yAxis: res.yAxis[0],
+      //   //   series: { data: [820, 932, 901, 934, 1290, 1330, 1320], type: 'line' }
+      //   // }
+      //   const sampleEchartData = res
+
+      //   const chart = this.$refs.chart
+      //   const myChart = echarts.init(chart)
+      //   myChart.setOption(sampleEchartData)
+      //   // window.addEventListener('resize', function() {
+      //   //   myChart.resize()
+      //   // })
+      //   // this.$on('hook:destroyed', () => {
+      //   //   window.removeEventListener('resize', function() {
+      //   //     myChart.resize()
+      //   //   })
+      //   // })
+      //   console.log('sampleEchart 数据获取成功 res=', res)
+      //   console.log('sampleEchart 数据获取成功 sampleEchartData=', sampleEchartData)
+      // })
+      // console.log('sampleEchart started 展示方式待定', row.id)
+      this.curPointId = row.id
+      this.curPointName = row.name
+      this.showEchart = true
+    },
+    // 关闭Echart弹窗
+    closeSampleEchart() {
+      this.showEchart = false
+    }
+  }
+}
+</script>
+<style lang="scss" scoped>
+.title-container {
+  display: flex;
+  justify-content: space-between;
+  padding: 20px 0;
+  .title-right {
+    display: flex;
+    align-items: center;
+    .check-resaults {
+      padding-right: 16px;
+      .success {
+        color: #00706b;
+      }
+      .faild {
+        color: #f00;
+        padding-right: 6px;
+      }
+      span {
+        font-weight: bold;
+      }
+    }
+  }
+}
+.title-main {
+  color: #111;
+  font-weight: bold;
+}
+// 用例表格样式
+.table-container {
+  // padding: 20px 0;
+  .el-table {
+    font-size: 12px;
+  }
+  .range {
+    white-space: nowrap;
+  }
+}
+// 底部按钮
+.bottom-button {
+  display: flex;
+  justify-content: center;
+  padding-bottom: 20px;
+  .cancel-plan {
+    padding-right: 20px;
+  }
+}
+// 按钮公用样式
+.dark-button {
+  background-color: #00706B;
+  border: 1px #00706B solid;
+  color: #fff;
+}
+.light-button {
+  color: #00706B;
+  border: 1px #00706B solid;
+}
+</style>

+ 915 - 0
src/views/plan/components/PlanCheckPoint_master104.vue

@@ -0,0 +1,915 @@
+<!-- 步骤2  -->
+<template>
+  <div v-show="currentStep == 1" :class="className">
+    <div class="title">测点导入</div>
+    <div class="buttons">
+      <div class="left">
+        <!-- 批量删除 -->
+        <div class="clear item">
+          <el-button
+            class="dark-button"
+            icon="el-icon-delete"
+            :disabled="!showDel"
+            @click="multDelete()"
+          >批量删除</el-button>
+        </div>
+      </div>
+      <div class="right">
+        <div class="item">
+          <!-- 添加新测点 -->
+          <el-button
+            class="light-button"
+            icon="el-icon-refresh-right"
+            @click="updataTableData()"
+          >刷新</el-button>
+        </div>
+        <div class="item">
+          <!-- 导入即是上传文件 -->
+          <el-upload
+            ref="upload"
+            :action="importCsvActionStr"
+            :show-file-list="false"
+            :auto-upload="false"
+            accept=".csv"
+            :on-change="submitUpload"
+          >
+            <el-button
+              class="light-button"
+              icon="el-icon-upload2"
+            >导入</el-button>
+          </el-upload>
+        </div>
+        <div class="export item">
+          <el-button
+            class="light-button"
+            icon="el-icon-download"
+            @click="getPlanExport()"
+          >导出</el-button>
+        </div>
+        <div class="item">
+          <!-- 添加新测点 -->
+          <el-button
+            class="dark-button"
+            icon="el-icon-plus"
+            @click="addCheckPoint()"
+          >添加</el-button>
+        </div>
+      </div>
+    </div>
+    <div class="table-container">
+      <el-table ref="multipleTable" v-loading="vloading" class="table-fixed" type="index" :data="tableData" :height="tableHeight" stripe @selection-change="handleSelectionChange">
+        <el-table-column
+          type="selection"
+        />
+        <!-- <el-table-column prop="id" label="序号" width="100" /> -->
+        <el-table-column prop="iec104" label="104点号" width="100">
+          <!-- rs.offset -->
+          <template slot-scope="scope">
+            <el-input
+              v-model="scope.row.iec104"
+              class="table-column-input"
+              type="number"
+              disabled
+            />
+          </template>
+        </el-table-column>
+        <el-table-column prop="name" label="* 测点名称" width="200">
+          <template slot-scope="scope">
+            <el-input
+              v-model="scope.row.name"
+              class="table-column-input"
+              clearable
+              placeholder="请输入名称"
+              maxlength="32"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column prop="name" label="* 设备名称" width="200">
+          <template slot-scope="scope">
+            <el-input
+              v-model="scope.row.groupingName"
+              class="table-column-input"
+              clearable
+              placeholder="请输入设备名称"
+              maxlength="32"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column prop="range" label="* 点号类型">
+          <!-- rs.range -->
+          <template slot-scope="scope">
+            <el-select
+              v-model="scope.row.range"
+              class="table-column-input"
+              placeholder=""
+              :disabled="Boolean(scope.row.id)"
+            >
+              <el-option
+                v-for="item in checkPointFuctionOptions"
+                :key="item.id"
+                :label="item.name"
+                :value="item.id"
+              />
+            </el-select>
+          </template>
+        </el-table-column>
+        <el-table-column prop="offset" label="* 点号">
+          <!-- rs.offset -->
+          <template slot-scope="scope">
+            <el-input
+              v-model="scope.row.offset"
+              class="table-column-input"
+              type="number"
+              :disabled="Boolean(scope.row.id)"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column prop="offset" label="关联遥信点号(仅遥控可关联)">
+          <!-- rs.offset -->
+          <template slot-scope="scope">
+            <el-input
+              v-if="scope.row.range=='COIL_STATUS'"
+              v-model="scope.row.relationTag"
+              class="table-column-input"
+              placeholder="请输入遥信点号"
+              type="number"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column prop="offset" label="关联遥测点号(仅遥调可关联)">
+          <!-- rs.offset -->
+          <template slot-scope="scope">
+            <el-input
+              v-if="scope.row.range=='HOLDING_REGISTER'"
+              v-model="scope.row.relationTag"
+              class="table-column-input"
+              placeholder="请输入遥测点号"
+              type="number"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column prop="scaling" label="倍率" width="80">
+          <!-- scaling -->
+          <template slot-scope="scope">
+            <el-input
+              v-model="scope.row.scaling"
+              class="table-column-input"
+              clearable
+              type="number"
+              maxlength="32"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column prop="adjust" label="偏移量" width="80">
+          <template slot-scope="scope">
+            <el-input
+              v-model="scope.row.adjust"
+              class="table-column-input"
+              clearable
+              type="number"
+              maxlength="32"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="180" fixed="right">
+          <template slot-scope="scope">
+            <el-button
+              class="table-act"
+              type="text"
+              icon="el-icon-edit"
+              size="small"
+              @click="editClick(scope.row, scope.$index)"
+            >
+              编辑
+            </el-button>
+            <el-button
+              class="table-act"
+              type="text"
+              icon="el-icon-s-order"
+              size="small"
+              @click="saveClick(scope.row,scope.$index)"
+            >保存</el-button>
+            <el-button
+              class="table-act"
+              type="text"
+              icon="el-icon-delete"
+              size="small"
+              @click="delClick(scope.row, scope.$index)"
+            >删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+    <!-- 编辑弹出的模态框套表单 -->
+    <el-dialog title="测点信息" :close-on-click-modal="false" :visible.sync="dialogFormVisible">
+      <el-form ref="form" :model="form">
+        <el-form-item label="*测点名称" :label-width="formLabelWidth">
+          <el-input v-model="form.name" autocomplete="off" maxlength="32" placeholder="请输入测点名称" />
+        </el-form-item>
+        <el-form-item label="*设备名称" :label-width="formLabelWidth">
+          <el-input v-model="form.groupingName" maxlength="20" placeholder="请输入设备名称" />
+        </el-form-item>
+        <el-form-item class="modelInput" label="*测点类型" :label-width="formLabelWidth">
+          <template slot-scope="scope">
+            <el-select
+              :key="toString(scope.row)"
+              v-model="form.range"
+              clearable
+              placeholder=""
+            >
+              <el-option
+                v-for="item in checkPointFuctionOptions"
+                :key="item.id"
+                :label="item.name"
+                :value="item.id"
+              />
+            </el-select>
+          </template>
+        </el-form-item>
+        <el-form-item label="*点号" :label-width="formLabelWidth">
+          <el-input v-model="form.offset" maxlength="6" />
+        </el-form-item>
+        <el-form-item label="关联遥信点号 (仅遥控可关联)" :label-width="formLabelWidth">
+          <el-input v-if="form.range=='COIL_STATUS'" v-model="form.relationTag" maxlength="6" placeholder="请输入遥信点号" />
+          <el-input v-else v-model="form.relationTag" disabled />
+        </el-form-item>
+        <el-form-item label="关联遥测点号 (仅遥调可关联)" :label-width="formLabelWidth">
+          <el-input v-if="form.range=='HOLDING_REGISTER'" v-model="form.relationTag" maxlength="6" placeholder="请输入遥测点号" />
+          <el-input v-else v-model="form.relationTag" disabled />
+        </el-form-item>
+        <el-form-item label="倍率" :label-width="formLabelWidth">
+          <el-input v-model="form.scaling" maxlength="5" />
+        </el-form-item>
+        <el-form-item label="偏移量" :label-width="formLabelWidth">
+          <el-input v-model="form.adjust" maxlength="6" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="dialogFormVisible = false">取 消</el-button>
+        <el-button type="primary" @click="saveForm">确 定</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 分页 -->
+    <pagination
+      background
+      layout="pager"
+      :limit="pageLimit"
+      :total="paginationTotalElements"
+      :current-page.sync="paginationNumber"
+      :page="paginationNumber"
+      @pagination="getPlanCheckPoint"
+    />
+    <!-- 底部按钮 -->
+    <div class="bottom-button">
+      <div class="cancel-plan">
+        <el-button class="light-button" @click="backStep">上一步</el-button>
+      </div>
+      <div class="save-plan">
+        <el-button class="dark-button" @click="submitPlan()">下一步</el-button>
+      </div>
+    </div>
+    <!-- 检查本地存储是否成功 -->
+    <!-- <el-button @click="checkStore()">checkStore</el-button> -->
+  </div>
+</template>
+
+<script>
+import { planExport } from '@/api/plan'
+import { dictOptions } from '@/api/dict'
+import Pagination from '@/components/Pagination'
+import { delRecord, editRow, httpGet, putData } from '@/api/common-action'
+export default {
+  components: { Pagination },
+  props: {
+    className: {
+      type: String,
+      default: 'steps-bar-default'
+    },
+    currentStep: {
+      type: Number,
+      default: 1
+    },
+    currentPlanData: {
+      type: Object,
+      default: function() {
+        return
+      }
+    }
+  },
+  data() {
+    return {
+      vloading: true,
+      // 表格默认数据
+      tableData: [],
+      tmpCheckPointId: 0,
+      tmpTableData: [],
+      // 每页显示多少条
+      pageLimit: 20,
+      paginationNumber: 1,
+      // 一共多少条
+      paginationTotalElements: 0,
+      // 一共多少页
+      // paginationTotalPages: 1,
+      // 导入csv 请求参数
+      importCsvActionStr: '#',
+      // 功能码选项
+      checkPointFuctionOptions: [],
+      // 值类型列表
+      valueTypeVale: '',
+      valueTypeOptions: [],
+      // 批量删除显示控制
+      showDel: false,
+      // 选中的arr
+      delarr: [],
+      dialogFormVisible: false,
+      // 模态框内表单
+      form: {},
+      formLabelWidth: '120px',
+      // 模态框表单内数据
+      modalFromArr: [],
+      stepIndex: '',
+      tableHeight: '',
+      // 多选框选中数据
+      multipleSelection: ''
+    }
+  },
+  watch: {
+    multipleSelection(nv, ov) {
+      if (nv.length === 0) {
+        // console.log('nv = ', nv)
+        this.showDel = false
+      }
+    },
+    'tableData': {
+      handler() {
+        this.$nextTick(() => {
+          this.$refs.multipleTable.doLayout()
+        })
+        true
+      },
+      deep: true
+    }
+  },
+  // computed: {
+  //   totalOfEle: () => {
+  //     console.log(this.tableData)
+  //     return this.tableData.length
+  //   }
+  // },
+  created() {
+    const marginBottom = 500
+    this.tableHeight = window.innerHeight - marginBottom
+  },
+  mounted() {
+    this.$nextTick(() => {
+      const envUrl = process.env.VUE_APP_BASE_API
+      // 网页加载完成后执行
+      this.importCsvActionStr = envUrl + `/product/model/0/models.csv`
+      if (this.currentPlanData.id > 0) {
+        // 如果获取到了PlanId再加载详细信息
+        this.getPlanCheckPoint()
+        this.importCsvActionStr = envUrl + `/product/model/${this.currentPlanData.product}/models.csv`
+      }
+      this.initFunctions()
+    })
+  },
+  methods: {
+    // 检查本地存储是否成功
+    // checkStore() {
+    //   console.log('checkStore start ')
+    //   const planFromDataStorage = localStorage.getItem('planFromData')
+    //   console.log('planFromData=',planFromDataStorage)
+    // }
+    // 文件限制
+    // handleExceed(files, fileList) {
+    //   this.$message.warning(`当前限制选择 3 个文件,本次选择了 ${files.length} 个文件,共选择了 ${files.length + fileList.length} 个文件`);
+    // },
+    // 初始化
+    async initFunctions() {
+      // 获取当前
+      // 获取功能码选项 131
+      let res = await dictOptions('131')
+      this.checkPointFuctionOptions = res
+      // 获取值类型列表
+      res = await dictOptions('132')
+      this.valueTypeOptions = res
+      // console.log('获取值类型列表 this.valueTypeOptions=', this.valueTypeOptions)
+      this.valueTypeVale = this.valueTypeOptions[0]
+    },
+    // 上一步
+    backStep() {
+      let currentStep = this.currentStep
+      currentStep--
+      this.$emit('changeStep', currentStep)
+    },
+    // 新加测试点
+    addCheckPoint() {
+      // console.log('addCheckPoint 新加测试点', this.valueTypeOptions)
+      this.tmpCheckPointId++
+      const newTable = {
+        tmpCheckPointId: this.tmpCheckPointId,
+        name: '',
+        groupingName: '',
+        relationTag: '',
+        range: this.checkPointFuctionOptions[0].id,
+        offset: 0,
+        dataType: this.valueTypeOptions[0].id,
+        swapByte: false,
+        swapWord: false,
+        scaling: 1,
+        adjust: 0,
+        value: 0,
+        // point104: 0,
+        id: null
+      }
+      this.tableData.unshift(newTable)
+      // 保存到临时表格数据中
+      this.tmpTableData.push(newTable)
+      this.tmpTableData.forEach((element) => {
+        // console.log(`this.tmpTableData element=${element.tmpCheckPointId}`)
+      })
+    },
+    // 导入
+    submitUpload() {
+      this.$refs.upload.submit()
+      // 上传之后刷新页面查看上传结果
+      this.getPlanCheckPoint()
+    },
+    // 导出
+    async getPlanExport() {
+      // /product/model/:product/models.csv
+      planExport(this.currentPlanData.product).then((response) => {
+        const link = document.createElement('a')
+        const blob = new Blob([response], {
+          type: 'application/vnd.ms-excel;charset=utf-8'
+        })
+        link.style.display = 'none'
+        link.href = URL.createObjectURL(blob)
+        link.setAttribute('download', `导出数据.csv`)
+        document.body.appendChild(link)
+        link.click()
+        document.body.removeChild(link)
+        // this.msgSuccess('导出成功')
+      })
+    },
+    // 清除
+    planClear() {
+      // console.log('planClear 清除')
+      this.tableData = []
+    },
+    // 获取测点表格数据
+    async getPlanCheckPoint(pageObj) {
+      this.vloading = true
+      // 翻页时pageObj对象才会有值,属性分别为page和limit
+      if (pageObj == null) {
+        pageObj = {
+          page: 0,
+          limit: this.pageLimit
+        }
+        this.paginationNumber = 0
+      } else {
+        this.pageLimit = pageObj.limit
+      }
+      // 返回值
+      /* {
+          adjust:0   //偏移量
+          dataType:null  //值类型
+          id:null    //
+          name:"中文" //测点名称
+          offset:0 //104点号
+          range:null //测点类型
+          scaling:1  //倍率
+          swapByte: 0, //字内交换
+          swapWord: 0, //字节交换
+          value:0     //先不用管- 测点查询
+          "manual": null,    //先不用管 -单测点查询
+          "stepSeconds": 0,    //先不用管 -单测点查询
+          "pollingCycle": 0,    //先不用管 -单测点查询
+          "disable": null    //先不用管 -单测点查询
+        }
+        totalPages 总页数
+        number 第几页
+        删除默认 tableData数据 */
+      const getUrl = `product/model/${this.currentPlanData.product}/paging?page=${pageObj.page}&size=${pageObj.limit}`
+      httpGet(getUrl).then((response) => {
+        this.vloading = false
+        if (response == null || response.content == null || response.content.length === 0) {
+          this.tableData = []
+          this.paginationNumber = 0
+          this.paginationTotalPages = 0
+          this.paginationTotalElements = 0
+          return
+        }
+        // 计算104点号
+        // response.content.map((item, index) => {
+        //   item.point104 = this.calPoint104(item.range, item.offset)
+        // })
+        this.tableData = response.content
+        this.paginationTotalPages = response.totalPages
+        this.paginationTotalElements = parseInt(response.totalElements)
+        this.paginationNumber = response.number * 1 + 1
+      })
+    },
+    // 表格重排
+    tableLayout() {
+      this.$nextTick(() => {
+        this.$refs.multipleTable.doLayout()
+      })
+    },
+    // input失焦 添加数据
+    // inputBlur(inputBlurData) {
+
+    //   // 2023-05-29 应后端要求,取消失焦保存
+    //   // console.log('inputBlur inputBlurData=', inputBlurData)
+    //   // // 后端不支持多条数据一起添加,所以这里失焦就添加数据,不需要再判断是否是临时添加数据
+    //   // // 此处 // /product/model/:product 中的 :product 是上一步添加方案时返回的 product 的值
+    //   // const editPath = `/product/model/${this.currentPlanData.product}`
+    //   // const editData = this.getSubmitTableRowData(inputBlurData)
+    //   // console.log('inputBlur editData=', editData)
+    //   // editRow(editPath, editData)
+
+    // },
+    async saveClick(row, index) {
+      // if (row.offset > 65535) {
+      //   this.$message({
+      //     type: 'error',
+      //     message: `寄存器地址不能大于65535`,
+      //     offset: window.screen.height / 3
+      //   })
+      //   return
+      // }
+      const editPath = `/product/model/${this.currentPlanData.product}`
+      const editData = row
+      // const editData = this.getSubmitTableRowData(row)
+      // console.log('saveClick editData=', editData)
+      // 校验必填项目
+      let isNotPassMesage = ''
+      let v = editData.name || ''
+      v = v.toString().replace(/ /gi, '')
+      if (v === '') {
+        isNotPassMesage = '必填项(测点名称)不能为空'
+      }
+      v = editData.groupingName || ''
+      v = v.toString().replace(/ /gi, '')
+      if (v === '') {
+        isNotPassMesage = '必填项(设备名称)不能为空'
+      }
+      v = editData.range || ''
+      v = v.toString().replace(/ /gi, '')
+      if (v === '') {
+        isNotPassMesage = '必填项(点号类型)不能为空'
+      }
+      v = editData.offset || ''
+      v = v.toString().replace(/ /gi, '')
+      if (v === '') {
+        isNotPassMesage = '必填项(点号)不能为空'
+      }
+      if (isNotPassMesage !== '') {
+        this.$message({
+          type: 'error',
+          message: isNotPassMesage,
+          offset: window.screen.height / 3
+        })
+        return
+      }
+      const res = await editRow(editPath, editData)
+      if (res) {
+        this.$set(this.tableData[index], 'id', res.id)
+        this.$set(this.tableData[index], 'modbus', res.modbus)
+        this.$set(this.tableData[index], 'iec104', res.iec104)
+        this.$message({
+          type: 'success',
+          message: `${res.name} 保存成功.`,
+          offset: window.screen.height / 3
+        })
+      }
+    },
+    delClick(row) {
+      // return
+      this.$confirm('此操作将永久删除该记录, 是否继续?', `删除:${row.name}`, {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(async() => {
+        if (row.id != null && row.id > 0) {
+        //   // :product 产品名称
+        //   // :tag 标签编号
+          const delUrl = `/product/model/${this.currentPlanData.product}/${row.id}`
+          await delRecord(delUrl)
+            .then((response) => {
+              // console.log('delRecord rs=', response)
+              // 后端成功执行后,前端再删除选中行
+              // this.tableData.splice(index, 1)
+              // this.tableData = this.tableData.filter(item => item.id !== row.id)
+              const newData = this.tableData.filter(item => item.id !== row.id)
+              this.tableData = [...newData]
+            })
+        } else {
+        // 如果是新增的临时行,直接删除选中行
+          // this.tableData.splice(index, 1)
+          // this.tableData = this.tableData.filter(item => item.tmpCheckPointId !== row.tmpCheckPointId)
+          const newData = this.tableData.filter(item => item.tmpCheckPointId !== row.tmpCheckPointId)
+          // console.log('newData=', newData)
+          this.tableData = [...newData]
+        }
+      }).catch(() => {
+        this.$message({
+          type: 'info',
+          message: '已取消删除',
+          offset: window.screen.height / 3
+        })
+      })
+    },
+    // 刷新
+    updataTableData() {
+      this.planClear()
+      this.getPlanCheckPoint()
+    },
+    // 提交
+    // 提交 下一步
+    submitPlan() {
+      // console.log(`this.tmpTableData=`, this.tmpTableData)
+      let currentStep = this.currentStep
+      const noName = this.tableData.some(item => (!item.groupingName) || (!item.name) || (!item.range) || (!item.offset.toString()))
+      if (noName) {
+        this.$message({
+          type: 'error',
+          message: '必填项不能为空',
+          offset: window.screen.height / 3
+        })
+        return
+      }
+
+      if (currentStep < 3) {
+        // 如果没有数据
+        if (!this.tableData.length) {
+          currentStep = currentStep + 1
+          this.$emit('changeStep', currentStep)
+          return
+        }
+        // 不接受数组提交,只能单条提交,所以使用遍历提交
+        this.tableData.forEach((item, index) => {
+          const tableRowData = this.getSubmitTableRowData(item)
+
+          // 保存步骤
+          const editPath = `/product/model/${this.currentPlanData.product}`
+          editRow(editPath, tableRowData).then(res => {
+            // console.log('PlanCheckPoint submitPlan currentStep=', currentStep)
+            // console.log('PlanCheckPoint submitPlan index=', index)
+            if (index === this.tableData.length - 1) {
+              // 如果是最后一条数据 提交成功,下一步
+              currentStep = currentStep + 1
+              this.$emit('changeStep', currentStep)
+            }
+          })
+        })
+      } else {
+        // 已经是最后一步了
+      }
+    },
+    // 公用函数
+    // 获取提交的行数据
+    getSubmitTableRowData(rowData) {
+      if (rowData.id > 0 || rowData.tmpCheckPointId > 0) {
+        let submitRowData = []
+        // {
+        //   adjust:0   //偏移量
+        //   dataType:null  //值类型
+        //   id:null    //
+        //   name:"中文" //测点名称
+        //   offset:0 //寄存器地址
+        //   range:null //功能码
+        //   scaling:1  //倍率
+        //   swapByte: 0, //字内交换
+        //   swapWord: 0, //字节交换
+        //   value:0     //先不用管- 测点查询
+        //   "manual": null,    //先不用管 -单测点查询
+        //   "stepSeconds": 0,    //先不用管 -单测点查询
+        //   "pollingCycle": 0,    //先不用管 -单测点查询
+        //   "disable": null    //先不用管 -单测点查询
+        // }
+        if (!rowData.id) {
+          rowData.id = null
+        }
+        // 组织出待提交数据
+        // {"tmpCheckPointId":2,"name":"","range":"COIL_STATUS","offset":0,"dataType":"BOOL","swapByte":false,"swapWord":false,"scaling":1,"adjust":0,"value":0,"id":null}
+        submitRowData = {
+          id: rowData.id,
+          groupingName: rowData.groupingName,
+          relationTag: rowData.relationTag,
+          name: rowData.name,
+          range: rowData.range,
+          offset: rowData.offset,
+          dataType: rowData.dataType,
+          swapByte: rowData.swapByte,
+          swapWord: rowData.swapWord,
+          scaling: rowData.scaling,
+          adjust: rowData.adjust,
+          value: rowData.value
+        }
+
+        return submitRowData
+      } else {
+        return []
+      }
+    },
+    // 批量删除
+    async multDelete() {
+      /* const delUrl = `/product/model/${this.currentPlanData.product}/${item.id}`
+      delRecord(delUrl).then((response) => {
+        console.log('delRecord rs=', response)
+        // 后端成功执行后,前端再删除选中行
+        // this.tableData.splice(index, 1)
+        // this.tableData = this.tableData.filter(item => item.id !== row.id)
+        const newData = this.tableData.filter(par => par.id !== item.id)
+        this.tableData = [...newData]
+      }).catch((err) => {
+        console.log('delRecord err rs=', err)
+      }) */
+      // 后端执行完成后,开始前端操作
+      // 遍历 `tableData` 数组
+      for (let i = 0; i < this.tableData.length; i++) {
+        const item = this.tableData[i]
+        // 判断当前行数据是否被选中
+        if (this.multipleSelection.includes(item)) {
+          // 如果选中,则从 开始删除
+
+          if (item.id > 0) {
+            // 如果 id > 0 则表示 是已经保存在后端的数据了,需要向后端发送删除请求
+            const delUrl = `/product/model/${this.currentPlanData.product}/${item.id}`
+            await delRecord(delUrl).then((response) => {
+              // 后端成功执行后,前端再删除选中行
+              this.tableData.splice(i, 1)
+              i-- // 因为 `splice` 方法会修改数组长度,所以需要将 `i` 减去 1,避免漏删下一个元素
+            })
+          } else {
+            // 如果是新增未保存过的数据,直接删除
+            this.tableData.splice(i, 1)
+            i-- // 因为 `splice` 方法会修改数组长度,所以需要将 `i` 减去 1,避免漏删下一个元素
+          }
+        }
+      }
+      // this.showDel = false
+    },
+    // 多选按钮状态改变 handleSelectionChange
+    handleSelectionChange(val) {
+      // 选中行存入临时变量中
+      this.multipleSelection = val
+      // console.log('handleSelectionChange val', val)
+      // 批量删除按钮激活
+      this.showDel = true
+    },
+    editClick(row, index) {
+      // 单测点查询接口 /product/model/:product/:tag
+      // 获取测点详细信息
+
+      if (row.id) {
+        const getUrl = `/product/model/${this.currentPlanData.product}/${row.id}`
+        // console.log('PlanCheckPoint editClick getUrl=', getUrl)
+        httpGet(getUrl).then(res => {
+          this.form = res
+          // this.form.swapByte = Boolean(res.swapByte)
+          // this.form.swapWord = Boolean(res.swapWord)
+
+          // console.log('PlanCheckPoint editClick this.form=', this.form)
+        })
+      } else {
+        this.form = this.tableData[index]
+      }
+      this.stepIndex = index
+      this.dialogFormVisible = true
+    },
+    saveForm() {
+      // 修改测点接口 /product/model/:product
+      if (this.form.id) {
+        let isNotPassMesage = ''
+        let v = this.form.name || ''
+        v = v.toString().replace(/ /gi, '')
+        if (v === '') {
+          isNotPassMesage = '必填项(测点名称)不能为空'
+        }
+        v = this.form.groupingName || ''
+        v = v.toString().replace(/ /gi, '')
+        if (v === '') {
+          isNotPassMesage = '必填项(设备名称)不能为空'
+        }
+        v = this.form.range || ''
+        v = v.toString().replace(/ /gi, '')
+        if (v === '') {
+          isNotPassMesage = '必填项(点号类型)不能为空'
+        }
+        v = this.form.offset || ''
+        v = v.toString().replace(/ /gi, '')
+        if (v === '') {
+          isNotPassMesage = '必填项(点号)不能为空'
+        }
+        if (isNotPassMesage !== '') {
+          this.$message({
+            type: 'error',
+            message: isNotPassMesage,
+            offset: window.screen.height / 3
+          })
+          return
+        }
+        const putUrl = `/product/model/${this.currentPlanData.product}`
+        putData(putUrl, this.form).then(res => {
+          // 保存表单
+          // console.log('saveForm putData 1 tableData=', this.tableData)
+          if (res == null) {
+            return
+          }
+          // res.point104 = this.calPoint104(res.range, res.offset)
+          this.$set(this.tableData, this.stepIndex, res)
+          this.dialogFormVisible = false
+        })
+      } else {
+        this.$set(this.tableData, this.stepIndex, this.form)
+        this.dialogFormVisible = false
+      }
+    }
+  }
+
+}
+</script>
+
+<style lang="scss" scoped>
+.plan-check-point {
+  // border: 1px #f00 solid;
+  text-align: center;
+  font-size: 16px;
+  width: 100%;
+  padding:0 20px;
+  .title {
+    font-size: 20px;
+    font-weight: bold;
+    padding: 10px 0 10px 0;
+  }
+  .buttons {
+    display: flex;
+    justify-content: space-between;
+    text-align: left;
+    padding-bottom: 20px;
+    .left,.right {
+      width: 50%;
+    }
+    .right{
+      display: flex;
+      justify-content: right;
+    }
+    .item {
+      margin: 0 10px;
+    }
+  }
+  .modelInput{
+    text-align: left;
+  }
+  // 分页
+  .page-bar {
+    display: flex;
+    justify-content: center;
+    // padding: 20px;
+    .el-pagination.is-background {
+      .el-pager {
+        .number {
+          color: #00706b;
+          border: 1px #00706b solid;
+          background-color: #fff;
+        }
+      }
+    }
+  }
+  // 底部步骤按钮
+ .bottom-button{
+    margin-top: 20px;
+  }
+  .table-container{
+    height: 100%;
+  }
+
+// ::v-deep .pagination-container{
+//     margin-top: 0;
+//   }
+  ::v-deep {
+    // 表格输入框
+    .table-column-input {
+      .el-input__inner {
+        border: none;
+        background: transparent;
+          padding-left: 0;
+      }
+      .el-input__inner:focus {
+        border: 1px #00706b solid;
+      }
+    }
+  }
+}
+::v-deep input::-webkit-outer-spin-button,
+   ::v-deep input::-webkit-inner-spin-button {
+       -webkit-appearance: none !important;
+     }
+   ::v-deep input[type='number'] {
+       -moz-appearance: textfield !important;
+     }
+
+</style>

+ 1 - 1
src/views/plan/components/PlanConfig.vue

@@ -117,7 +117,7 @@ export default {
       // 获取接收到的表单参数值,判断参数是否合法,是否需要提交
       const res = await searchPlanData({
         page: 0,
-        limit: 200000000
+        limit: 10000
       })
       const allData = res.content
 

+ 5 - 4
src/views/plan/components/PlanList.vue

@@ -166,10 +166,11 @@
       <el-dialog title="模型点表" :visible.sync="showCheckModel">
         <el-table :data="checkModelData" :height="modaltableHeight" stripe>
           <el-table-column property="id" label="序号" />
-          <el-table-column property="name" label="名称" />
-          <el-table-column property="range" label="寄存器范围" />
-          <el-table-column property="offset" label="寄存器地址" />
-          <el-table-column property="dataType" label="值类型" />
+          <el-table-column property="name" label="测点名称" />
+          <el-table-column property="groupingName" label="设备名称" />
+          <el-table-column property="range" label="测点类型" />
+          <el-table-column property="offset" label="点号" />
+          <el-table-column property="relationTag" label="关联点号" />
         </el-table>
         <!-- 分页 -->
         <div class="page-bar">

+ 1 - 1
src/views/plan/components/PlanSteps.vue

@@ -36,7 +36,7 @@
         </el-table-column>
         <el-table-column prop="name" label="* 步骤名称" width="250">
           <template slot-scope="scope">
-            <el-input v-model="scope.row.name" class="table-column-input" type="text" clearable maxlength="32" />
+            <el-input v-model="scope.row.name" class="table-column-input" type="text" clearable maxlength="32" placeholder="请输入步骤名称" />
             <!-- <el-tooltip class="item" effect="light" :content="scope.row.name" placement="top">
               <el-input
                 v-model="scope.row.name"

+ 17 - 2
src/views/plan/save.vue

@@ -7,7 +7,8 @@
     <!-- 1-检测方案配置表单 -->
     <plan-config v-if="mountedFinished" class-name="plan-config" :current-step="currentStep" :current-plan-data="currentPlanData" @updataPlanData="updataCurrentPlanData" @changeStep="getStep" />
     <!-- 2-测点导入 -->
-    <plan-check-point v-if="currentPlanData.id>0" ref="planCheckPoint" class-name="plan-check-point" :current-step="currentStep" :current-plan-data="currentPlanData" @changeStep="getStep" />
+    <plan-check-point v-if="currentPlanData.id>0 && isMaster104===false" ref="planCheckPoint" class-name="plan-check-point" :current-step="currentStep" :current-plan-data="currentPlanData" @changeStep="getStep" />
+    <plan-check-point-master104 v-if="currentPlanData.id>0 && isMaster104===true" ref="planCheckPointMaster104" class-name="plan-check-point" :current-step="currentStep" :current-plan-data="currentPlanData" @changeStep="getStep" />
     <!-- 3-步骤设置 -->
     <plan-steps v-if="currentPlanData.id>0" ref="planSteps" class-name="plan-steps" :current-step="currentStep" :current-plan-data="currentPlanData" @changeStep="getStep" />
     <!-- 4-用例设置 -->
@@ -23,6 +24,8 @@ import StepsBar from './components/StepsBar'
 import PlanConfig from './components/PlanConfig'
 // 测点导入
 import PlanCheckPoint from './components/PlanCheckPoint'
+// 主站104检测方案-测点导入
+import PlanCheckPointMaster104 from './components/PlanCheckPoint_master104'
 // 步骤设置
 import PlanSteps from './components/PlanSteps'
 // 用例设置
@@ -35,11 +38,13 @@ export default {
     StepsBar,
     PlanConfig,
     PlanCheckPoint,
+    PlanCheckPointMaster104, // 主站104检测时的测点导入组件
     PlanSteps,
     PlanExample
   },
   data() {
     return {
+      isMaster104: false, // 是否是主站104检测方案
       currentStep: 0,
       currentPlanData: {},
       mountedFinished: false,
@@ -78,6 +83,11 @@ export default {
         // 通过planId 获取 方案配置 数据
         const res = await httpGet(`/test/suite/${this.currentPlanData.id}`)
         // console.log('save initFunctions httpGet res=', res)
+        if (res.productType === 'IEC104') {
+          this.isMaster104 = true
+        } else {
+          this.isMaster104 = false
+        }
         this.currentPlanData = res
         // if (res.productType === 'LOW_POWER' || res.protocolValue === 'MICRO_POWER') {
         //   this.className = 'one-steps-bar'
@@ -96,7 +106,12 @@ export default {
         this.currentPlanData = currentPlanData
       }
       if (this.currentStep === 1) {
-        this.$refs.planCheckPoint.getPlanCheckPoint()
+        // 当前方案是否是主站104方案
+        if (this.isMaster104) {
+          this.$refs.planCheckPointMaster104.getPlanCheckPoint()
+        } else {
+          this.$refs.planCheckPoint.getPlanCheckPoint()
+        }
       } else if (this.currentStep === 2) {
         this.$refs.planSteps.getPlanSteps()
       } else if (this.currentStep === 3) {