Bläddra i källkod

feat(wenshidp): 新增通道设置弹窗及进度条,版本更新至1.15.31

allen 1 dag sedan
förälder
incheckning
e2f3cf532a

+ 2 - 2
manifest.json

@@ -2,8 +2,8 @@
     "name" : "云飞智控",
     "appid" : "__UNI__DBA6730",
     "description" : "",
-    "versionName" : "1.15.30",
-    "versionCode" : 11530,
+    "versionName" : "1.15.31",
+    "versionCode" : 11531,
     "transformPx" : false,
     "sassImplementationName" : "node-sass",
     /* 5+App特有相关 */

+ 363 - 12
pages/cb/wenshidp/components/OperationCard.vue

@@ -1,5 +1,9 @@
 <template>
   <view class="operation">
+    <!-- 右上角设置入口:打开设置弹窗 -->
+    <view class="operation__setting" @tap.stop="openSetting">
+      <u-icon name="setting" size="36" color="#909399"></u-icon>
+    </view>
     <view class="operation__header">
       <image class="operation__icon" :src="iconSrc" mode="aspectFit" />
       <view class="operation__name">{{
@@ -7,16 +11,34 @@
       }}</view>
     </view>
     <view class="operation__body">
-      <!-- 三状态:按钮形式(0 停止 1 正向 2 反向) -->
+      <!-- 三状态:按钮形式(0 停止 1 正向 2 反向)+ 行进进度条 -->
       <block v-if="hasPos">
-        <view
-          class="operation__btn"
-          :class="btnClass(opt.value)"
-          v-for="opt in options"
-          :key="opt.value"
-          @click="handleClick(opt.value)"
-          >{{ opt.label }}</view
-        >
+        <view class="operation__btns">
+          <view
+            class="operation__btn"
+            :class="btnClass(opt.value)"
+            v-for="opt in options"
+            :key="opt.value"
+            @click="handleClick(opt.value)"
+            >{{ opt.label }}</view
+          >
+        </view>
+        <!-- 行进进度条:0 / 当前值 / 100 刻度在条上方,当前值 0/100 时与端点刻度重合隐藏 -->
+        <view class="operation__progress">
+          <view class="operation__scale">
+            <view class="operation__num">0</view>
+            <view
+              v-if="posValue > 0 && posValue < 100"
+              class="operation__num operation__num--pos"
+              :style="{ left: posLeft }"
+              >{{ posValue }}</view
+            >
+            <view class="operation__num operation__num--end">100</view>
+          </view>
+          <view class="operation__track">
+            <view class="operation__fill" :style="{ width: posWidth }"></view>
+          </view>
+        </view>
       </block>
       <!-- 两状态:单个开关(1 开 0 关) -->
       <view v-else class="operation__switch-row">
@@ -34,6 +56,74 @@
         ></u-switch>
       </view>
     </view>
+    <!-- 设置弹窗:参数设置(运行/间隔时间,走 config 接口)+ 行程设置(TargetPos,走 devctl 接口) -->
+    <u-popup v-model="settingVisible" mode="bottom" border-radius="24" :safe-area-inset-bottom="true">
+      <view class="setting">
+        <view class="setting__head">
+          <view class="setting__title">{{ settingName }}设置</view>
+          <view class="setting__close" @click="settingVisible = false">
+            <u-icon name="close" size="32" color="#909399"></u-icon>
+          </view>
+        </view>
+        <view class="setting__section">
+          <view class="setting__section-title">参数设置</view>
+          <view class="setting__item">
+            <view class="setting__label">运行时间</view>
+            <view class="setting__input-box">
+              <input
+                class="setting__input"
+                type="number"
+                maxlength="5"
+                :value="settingForm.runTime"
+                placeholder="0"
+                @input="filterNum('runTime', $event)"
+              />
+              <view class="setting__unit">秒</view>
+            </view>
+          </view>
+          <view class="setting__item">
+            <view class="setting__label">间隔时间</view>
+            <view class="setting__input-box">
+              <input
+                class="setting__input"
+                type="number"
+                maxlength="5"
+                :value="settingForm.intervalTime"
+                placeholder="0"
+                @input="filterNum('intervalTime', $event)"
+              />
+              <view class="setting__unit">秒</view>
+            </view>
+          </view>
+          <view class="setting__acts">
+            <view class="setting__btn" @click="settingVisible = false">取消</view>
+            <view class="setting__btn setting__btn--primary" @click="saveSetting('params')">保存</view>
+          </view>
+        </view>
+        <view class="setting__divider"></view>
+        <view class="setting__section">
+          <view class="setting__section-title">行程设置</view>
+          <view class="setting__item">
+            <view class="setting__label">行程设置</view>
+            <view class="setting__input-box">
+              <input
+                class="setting__input"
+                type="number"
+                maxlength="5"
+                :value="settingForm.stroke"
+                placeholder="0"
+                @input="filterNum('stroke', $event)"
+              />
+              <view class="setting__unit">度</view>
+            </view>
+          </view>
+          <view class="setting__acts">
+            <view class="setting__btn" @click="settingVisible = false">取消</view>
+            <view class="setting__btn setting__btn--primary" @click="saveSetting('stroke')">保存</view>
+          </view>
+        </view>
+      </view>
+    </u-popup>
   </view>
 </template>
 <script>
@@ -59,7 +149,22 @@ export default {
       default: () => ({}),
     },
   },
+  data() {
+    return {
+      settingVisible: false,
+      // 设置弹窗回显:LimitRun 运行时间 / StepIdle 间隔时间 / TargetPos 行程
+      settingForm: {
+        runTime: '',
+        intervalTime: '',
+        stroke: '',
+      },
+    };
+  },
   computed: {
+    settingName() {
+      const d = this.dourceData;
+      return d.display_name || d.name || d.code || '设备';
+    },
     // 存在 CurrentPos 字段即为三状态(0 停止 1 正向 2 反向),否则两状态(0 停止 1 开启)
     // CurrentPos 可能是明文字段或 ChannelParamSetXX:CurrentPos 平铺属性,只用于判定形态
     hasPos() {
@@ -100,6 +205,52 @@ export default {
       const val = this.statusKey ? d[this.statusKey] : d.value;
       return val == null ? 0 : Number(val);
     },
+    // 行进位置遥测 key:ChannelParamSetXX:CurrentPos 或明文 CurrentPos,仅三态有
+    posKey() {
+      const d = this.dourceData;
+      if ('CurrentPos' in d) {
+        return 'CurrentPos';
+      }
+      return (
+        Object.keys(d).find((k) => /^ChannelParamSet\d+:CurrentPos$/.test(k)) ||
+        ''
+      );
+    },
+    // 当前行进位置(0~100),无效值按 0 处理
+    posValue() {
+      const d = this.dourceData;
+      const num = Number(this.posKey ? d[this.posKey] : 0);
+      return Number.isFinite(num) ? num : 0;
+    },
+    posWidth() {
+      return `${Math.min(Math.max(this.posValue, 0), 100)}%`;
+    },
+    // 当前值数字跟随填充末端,钳制在 8%~92% 防贴边
+    posLeft() {
+      return `${Math.min(Math.max(this.posValue, 8), 92)}%`;
+    },
+    // 设置弹窗回显/下发 key:ChannelParamSetXX:LimitRun / StepIdle / TargetPos 平铺属性
+    limitRunKey() {
+      return (
+        Object.keys(this.dourceData).find((k) =>
+          /^ChannelParamSet\d+:LimitRun$/.test(k)
+        ) || ''
+      );
+    },
+    stepIdleKey() {
+      return (
+        Object.keys(this.dourceData).find((k) =>
+          /^ChannelParamSet\d+:StepIdle$/.test(k)
+        ) || ''
+      );
+    },
+    targetPosKey() {
+      return (
+        Object.keys(this.dourceData).find((k) =>
+          /^ChannelParamSet\d+:TargetPos$/.test(k)
+        ) || ''
+      );
+    },
     // 按通道名匹配图标,随运行状态切换 on/off 变体
     iconSrc() {
       const d = this.dourceData;
@@ -123,6 +274,65 @@ export default {
     },
   },
   methods: {
+    // 打开设置弹窗:回显通道配置(LimitRun 运行时间 / StepIdle 间隔时间 / TargetPos 行程)
+    openSetting() {
+      const d = this.dourceData;
+      const pick = (key) => (key && d[key] != null ? d[key] : 0);
+      this.settingForm = {
+        runTime: String(pick(this.limitRunKey)),
+        intervalTime: String(pick(this.stepIdleKey)),
+        stroke: String(pick(this.targetPosKey)),
+      };
+      this.settingVisible = true;
+    },
+    // 运行 / 间隔 / 行程只保留数字,最多 5 位(99999),滤掉其余输入
+    filterNum(field, e) {
+      this.settingForm[field] = String(e.detail.value)
+        .replace(/[^\d]/g, '')
+        .slice(0, 5);
+    },
+    // 按分区保存:抛给父级接接口,弹窗保持打开可继续存另一分区
+    saveSetting(section) {
+      const keys = {
+        limitRunKey: this.limitRunKey,
+        stepIdleKey: this.stepIdleKey,
+        targetPosKey: this.targetPosKey,
+      };
+      if (section === 'stroke') {
+        if (!this.targetPosKey) {
+          uni.showToast({
+            title: '该设备不支持行程设置',
+            icon: 'none',
+            duration: 3000,
+          });
+          return;
+        }
+        this.$emit('setting-save', {
+          dourceData: this.dourceData,
+          section,
+          data: { stroke: this.settingForm.stroke },
+          keys,
+        });
+        return;
+      }
+      if (!this.limitRunKey && !this.stepIdleKey) {
+        uni.showToast({
+          title: '该设备不支持参数设置',
+          icon: 'none',
+          duration: 3000,
+        });
+        return;
+      }
+      this.$emit('setting-save', {
+        dourceData: this.dourceData,
+        section,
+        data: {
+          runTime: this.settingForm.runTime,
+          intervalTime: this.settingForm.intervalTime,
+        },
+        keys,
+      });
+    },
     // 三状态按钮高亮:正/反向绿色,停止橙色
     btnClass(value) {
       if (Number(value) !== this.statusValue) {
@@ -149,15 +359,24 @@ export default {
 </script>
 <style scoped lang="scss">
 .operation {
+  position: relative;
   // 卡片浅灰底,与头部渐变底色衔接;白色按钮直接落在灰底上
   background: #eff2fa;
   border-radius: 16rpx;
   margin-bottom: 24rpx;
   overflow: hidden;
+  // 右上角设置按钮(padding 扩大点击热区)
+  &__setting {
+    position: absolute;
+    top: 8rpx;
+    right: 12rpx;
+    z-index: 2;
+    padding: 10rpx;
+  }
   &__header {
     display: flex;
     align-items: center;
-    padding: 20rpx 24rpx;
+    padding: 20rpx 72rpx 20rpx 24rpx;
     background: linear-gradient(180deg, #eff4ff 20.24%, #eff2fa 100%);
   }
   &__icon {
@@ -177,9 +396,14 @@ export default {
   }
   &__body {
     display: flex;
-    align-items: center;
+    flex-direction: column;
+    align-items: stretch;
     padding: 20rpx 24rpx 24rpx;
   }
+  &__btns {
+    display: flex;
+    align-items: center;
+  }
   &__btn {
     flex: 1;
     height: 72rpx;
@@ -211,8 +435,45 @@ export default {
       font-weight: 500;
     }
   }
+  // 行进进度条:绿色填充 + 0 / 当前值 / 100 刻度(刻度在条上方)
+  &__progress {
+    margin-top: 20rpx;
+  }
+  &__scale {
+    position: relative;
+    height: 30rpx;
+    margin-bottom: 4rpx;
+    color: #909399;
+    font-family: 'Source Han Sans CN VF';
+    font-size: 22rpx;
+    line-height: 30rpx;
+  }
+  &__num {
+    &--pos {
+      position: absolute;
+      top: 0;
+      transform: translateX(-50%);
+      color: #0db02b;
+      font-weight: 600;
+    }
+    &--end {
+      position: absolute;
+      top: 0;
+      right: 0;
+    }
+  }
+  &__track {
+    height: 12rpx;
+    border-radius: 6rpx;
+    background: #c3cad8;
+    overflow: hidden;
+  }
+  &__fill {
+    display: block;
+    height: 100%;
+    background: #0bbc58;
+  }
   &__switch-row {
-    flex: 1;
     display: flex;
     align-items: center;
     justify-content: space-between;
@@ -227,4 +488,94 @@ export default {
     }
   }
 }
+// 设置弹窗(底部弹出)
+.setting {
+  padding: 32rpx;
+  &__head {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 32rpx;
+  }
+  &__title {
+    color: #042118;
+    font-family: 'Source Han Sans CN VF';
+    font-size: 32rpx;
+    font-weight: 700;
+  }
+  &__close {
+    padding: 8rpx;
+  }
+  &__section-title {
+    color: #042118;
+    font-family: 'Source Han Sans CN VF';
+    font-size: 28rpx;
+    font-weight: 600;
+    margin-bottom: 24rpx;
+  }
+  &__item {
+    display: flex;
+    align-items: center;
+    margin-bottom: 24rpx;
+  }
+  &__label {
+    flex-shrink: 0;
+    width: 150rpx;
+    color: #666666;
+    font-family: 'Source Han Sans CN VF';
+    font-size: 26rpx;
+  }
+  &__input-box {
+    flex: 1;
+    display: flex;
+    align-items: center;
+    height: 72rpx;
+    padding: 0 20rpx;
+    background: #eff2fa;
+    border-radius: 12rpx;
+  }
+  &__input {
+    flex: 1;
+    height: 100%;
+    color: #333333;
+    font-size: 26rpx;
+  }
+  &__unit {
+    margin-left: 12rpx;
+    color: #999999;
+    font-size: 26rpx;
+  }
+  &__acts {
+    display: flex;
+    align-items: center;
+    justify-content: flex-end;
+  }
+  &__btn {
+    min-width: 140rpx;
+    height: 64rpx;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    padding: 0 28rpx;
+    background: #ffffff;
+    border: 2rpx solid #e3e6ee;
+    border-radius: 12rpx;
+    box-sizing: border-box;
+    color: #666666;
+    font-family: 'Source Han Sans CN VF';
+    font-size: 26rpx;
+    margin-left: 20rpx;
+    &--primary {
+      background: #0bbc58;
+      border-color: #0bbc58;
+      color: #ffffff;
+      font-weight: 500;
+    }
+  }
+  &__divider {
+    height: 2rpx;
+    background: #eef0f5;
+    margin: 32rpx 0;
+  }
+}
 </style>

+ 121 - 86
pages/cb/wenshidp/components/element.vue

@@ -8,14 +8,20 @@
       </view>
     </view>
     <view class="element-body">
-      <view class="element-row" v-for="(item, index) in displayList" :key="index">
-        <image :src="item.url" class="element-col-icon" />
-        <view class="element-col">
-          <view class="element-col-title"
-            >{{ item.content }}
-            <text class="unit">{{ item.unit }}</text>
+      <view class="element-group" v-for="(group, gIndex) in groups" :key="gIndex">
+        <!-- 组名:接口 sensor_list 节点 display_name(对应 PC 屏的 1组/2组 徽标) -->
+        <view class="element-group-name" v-if="group.name">{{ group.name }}</view>
+        <view class="element-grid">
+          <view class="element-row" v-for="(item, index) in group.sensors" :key="index">
+            <image :src="item.url" class="element-col-icon" />
+            <view class="element-col">
+              <view class="element-col-title"
+                >{{ item.content }}
+                <text class="unit">{{ item.unit }}</text>
+              </view>
+              <view class="element-col-content">{{ item.title }}</view>
+            </view>
           </view>
-          <view class="element-col-content">{{ item.title }}</view>
         </view>
       </view>
     </view>
@@ -26,7 +32,7 @@
 export default {
   name: 'Element',
   props: {
-    // status 接口返回的 sensor_list,为空时展示内置示例
+    // status 接口返回的 sensor_list(节点嵌套:display_name + children),为空时展示内置示例
     sensorList: {
       type: Array,
       default: null,
@@ -37,69 +43,79 @@ export default {
     },
   },
   computed: {
-    displayList() {
+    // sensor_list 节点 → 分组:组名取节点 display_name,子传感器值取 SensorDevXX:FieldName 遥测 key
+    groups() {
       if (!this.sensorList || !this.sensorList.length) {
-        return this.list;
+        return this.demoGroups;
       }
-      return this.sensorList.map((item) => {
-        // 取形如 SensorDev00:Temp 的遥测 key 对应的值
-        const key = Object.keys(item).find((k) =>
-          /^SensorDev\d+:\w+$/.test(k)
-        );
-        const val = key ? item[key] : null;
-        return {
-          title: item.display_name || '',
-          unit: item.unit || '',
-          content: val == null ? '--' : val,
-          url: this.iconByName(item.display_name),
-        };
-      });
+      return this.sensorList
+        .map((node) => ({
+          name: node.display_name || '',
+          sensors: (node.children || []).map((item) => {
+            const key = Object.keys(item).find((k) =>
+              /^SensorDev\d+:\w+$/.test(k)
+            );
+            const val = key ? item[key] : null;
+            return {
+              title: item.display_name || '',
+              unit: item.unit || '',
+              content: val == null ? '--' : val,
+              url: this.iconByName(item.display_name),
+            };
+          }),
+        }))
+        .filter((group) => group.sensors.length);
     },
   },
   data() {
     return {
-      list: [
+      demoGroups: [
         {
-          url: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/greenHouseIcon/sensor_temp.png',
-          title: '空气温度',
-          unit: '℃',
-          content: '12',
-        },
-        {
-          url: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/greenHouseIcon/sensor_humidity.png',
-          title: '空气湿度',
-          unit: '%',
-          content: '80',
-        },
-        {
-          url: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/greenHouseIcon/sensor_soil_temp.png',
-          title: '土壤温度',
-          unit: '℃',
-          content: '80',
-        },
-        {
-          url: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/greenHouseIcon/sensor_soil_humidity.png',
-          title: '土壤湿度',
-          unit: '%',
-          content: '80',
-        },
-        {
-          url: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/greenHouseIcon/sensor_co2.png',
-          title: '二氧化碳',
-          unit: 'ppm',
-          content: '80',
-        },
-        {
-          url: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/greenHouseIcon/sensor_sun.png',
-          title: '光照',
-          unit: 'LUX',
-          content: '80',
-        },
-        {
-          url: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/greenHouseIcon/sensor_ph.png',
-          title: '土壤PH值',
-          unit: '',
-          content: '80',
+          name: '1组',
+          sensors: [
+            {
+              url: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/greenHouseIcon/sensor_temp.png',
+              title: '空气温度',
+              unit: '℃',
+              content: '12',
+            },
+            {
+              url: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/greenHouseIcon/sensor_humidity.png',
+              title: '空气湿度',
+              unit: '%',
+              content: '80',
+            },
+            {
+              url: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/greenHouseIcon/sensor_soil_temp.png',
+              title: '土壤温度',
+              unit: '℃',
+              content: '80',
+            },
+            {
+              url: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/greenHouseIcon/sensor_soil_humidity.png',
+              title: '土壤湿度',
+              unit: '%',
+              content: '80',
+            },
+            {
+              url: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/greenHouseIcon/sensor_co2.png',
+              title: '二氧化碳',
+              unit: 'ppm',
+              content: '80',
+            },
+            {
+              url: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/greenHouseIcon/sensor_sun.png',
+              title: '光照',
+              unit: 'LUX',
+              content: '80',
+            },
+            {
+              url: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/greenHouseIcon/sensor_ph.png',
+              title: '土壤PH值',
+              unit: '',
+              content: '80',
+            },
+          ],
         },
       ],
     };
@@ -151,33 +167,52 @@ export default {
     }
   }
   .element-body {
-    display: grid;
-    grid-template-columns: repeat(2, 1fr);
-    grid-gap: 20rpx;
     padding: 20rpx;
-    .element-row {
-      display: flex;
-      margin-bottom: 20rpx;
-      .element-col-icon {
-        width: 80rpx;
-        height: 80rpx;
-        margin-right: 10rpx;
+    .element-group {
+      & + .element-group {
+        margin-top: 32rpx;
       }
-      .element-col {
-        margin-left: 10rpx;
-        .element-col-title {
-          font-size: 28rpx;
-          color: #042118;
-          font-weight: 700;
-          .unit {
+    }
+    // 组名徽标:浅绿底绿字(对应 PC 深色屏的 1组/2组 方块)
+    .element-group-name {
+      display: inline-block;
+      padding: 6rpx 24rpx;
+      margin-bottom: 20rpx;
+      background: #e6f7ee;
+      border-radius: 8rpx;
+      color: #0bbc58;
+      font-family: 'Source Han Sans CN VF';
+      font-size: 24rpx;
+      font-weight: 600;
+    }
+    .element-grid {
+      display: grid;
+      grid-template-columns: repeat(2, 1fr);
+      grid-gap: 20rpx;
+      .element-row {
+        display: flex;
+        margin-bottom: 20rpx;
+        .element-col-icon {
+          width: 80rpx;
+          height: 80rpx;
+          margin-right: 10rpx;
+        }
+        .element-col {
+          margin-left: 10rpx;
+          .element-col-title {
+            font-size: 28rpx;
+            color: #042118;
+            font-weight: 700;
+            .unit {
+              font-size: 24rpx;
+              color: #999;
+            }
+          }
+          .element-col-content {
             font-size: 24rpx;
-            color: #999;
+            color: #666;
           }
         }
-        .element-col-content {
-          font-size: 24rpx;
-          color: #666;
-        }
       }
     }
   }

+ 69 - 16
pages/cb/wenshidp/wenshidp.vue

@@ -41,6 +41,7 @@
       </view>
       <operation-card
         @change="channelControlHandler"
+        @setting-save="channelSettingHandler"
         v-for="(item, index) in juanmoList"
         :key="index"
         :dourceData="item"
@@ -72,6 +73,9 @@ export default {
   },
   data() {
     return {
+      // 必须在 data 中声明:模板 :devBid 传参走视图层数据,
+      // 仅在 onLoad 里挂到 this 上时子组件拿到的是空串
+      devBid: '',
       location:
         'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/image/zhongshui/location.svg',
       ws: null,
@@ -131,6 +135,55 @@ export default {
         this.getKzgYunshangStatus();
       }, 1000);
     },
+    // 通道设置保存:行程走 devctl 下发目标位置;参数走 config 下发限位/步进时间
+    channelSettingHandler(prop) {
+      const { dourceData, section, data, keys } = prop;
+      if (section === 'stroke') {
+        const value = Number(data.stroke) || 0;
+        // 本地先行更新回显值,稍后拉取接口校正
+        this.$set(dourceData, keys.targetPosKey, value);
+        this.optDevctl({
+          devBid: this.devBid,
+          data: {
+            [keys.targetPosKey]: value,
+          },
+        });
+        return;
+      }
+      const runTime = Number(data.runTime) || 0;
+      const intervalTime = Number(data.intervalTime) || 0;
+      // 本地先行更新回显值,稍后拉取接口校正
+      if (keys.limitRunKey) {
+        this.$set(dourceData, keys.limitRunKey, runTime);
+      }
+      if (keys.stepIdleKey) {
+        this.$set(dourceData, keys.stepIdleKey, intervalTime);
+      }
+      this.optDevConfig({
+        devBid: this.devBid,
+        data: {
+          ...(keys.limitRunKey ? { [keys.limitRunKey]: runTime } : {}),
+          ...(keys.stepIdleKey ? { [keys.stepIdleKey]: intervalTime } : {}),
+        },
+      });
+    },
+    // 下发通道配置:{ devBid, data: { LimitRun, StepIdle } }
+    async optDevConfig(payload) {
+      const res = await this.$myRequest({
+        url: '/api/v2/iot/device/kzg/yunshang/config/',
+        method: 'post',
+        data: payload,
+        header: {
+          'Content-Type': 'application/json',
+        },
+      });
+      // 提示
+      uni.showToast({
+        title: res?.msg || '',
+        icon: 'none',
+        duration: 3000,
+      });
+    },
     // 获取设备详情(sfToken 用于 WebSocket 鉴权)
     async getYunshangInfo() {
       const res = await this.$myRequest({
@@ -276,25 +329,24 @@ export default {
         }
       });
     },
-    // 用 WebSocket 实时数据按 ChannelParamSetXX:Status 匹配 channel_list 更新状态
+    // 用 WebSocket 实时数据按 ChannelParamSetXX:XXX 平铺键(Status/CurrentPos 等)匹配 channel_list 更新
     mergeTwoObject(firstObject, secondObject) {
       // 构建 key 到通道的映射,实现 O(1) 查找
       const codeMap = new Map();
       const buildMap = (items) => {
         for (const item of items) {
-          // Status key:优先同名属性,其次 code/sfCode 标识(值在 value 里)
-          let statusKey = Object.keys(item).find((k) =>
-            /^ChannelParamSet\d+:Status$/.test(k)
-          );
-          let statusField = statusKey || '';
-          if (!statusKey) {
-            statusKey = [item.code, item.sfCode].find((v) =>
-              /^ChannelParamSet\d+:Status$/.test(String(v || ''))
-            );
-            statusField = 'value';
+          // 平铺遥测键按同名键更新,覆盖进度条(CurrentPos)等字段
+          for (const k of Object.keys(item)) {
+            if (/^ChannelParamSet\d+:/.test(k)) {
+              codeMap.set(k, { item, field: k });
+            }
           }
-          if (statusKey) {
-            codeMap.set(statusKey, { item, statusField });
+          // Status 兼容:无同名属性时取 code/sfCode 标识(值在 value 里)
+          const statusLike = [item.code, item.sfCode].find((v) =>
+            /^ChannelParamSet\d+:Status$/.test(String(v || ''))
+          );
+          if (statusLike && !codeMap.has(statusLike)) {
+            codeMap.set(statusLike, { item, field: 'value' });
           }
         }
       };
@@ -313,7 +365,7 @@ export default {
         if (val === undefined) {
           continue;
         }
-        this.$set(target.item, target.statusField, val);
+        this.$set(target.item, target.field, val);
       }
     },
     // 关闭 WebSocket
@@ -399,8 +451,9 @@ uni-page-body {
     padding: 0 30rpx;
   }
   .greenhouse-bottom {
-    width: calc(100% - 128rpx);
-    padding: 32rpx 32rpx 1rpx 32rpx;
+    width: calc(100% - 120rpx);
+    /* 底部用真实 padding,不依赖最后一个子卡片的 margin(真机上 margin 会被吞掉,白边消失) */
+    padding: 32rpx;
     background: #ffffff;
     border-radius: 16rpx;
     margin: 0 auto;