Procházet zdrojové kódy

feat: 新增温湿度数据页面,优化资源加载与设备控制逻辑

1.  新增pages/cb/wenshidp相关页面与组件,实现温室环境监测与设备控制功能
2.  将本地图片资源替换为远程CDN链接,减少包体积
3.  修复设备控制页开关绑定逻辑,适配后端字符串格式参数
4.  新增设备类型参数传递,完善设备详情跳转逻辑
5.  新增水肥一体机页面的自动控制模块与灌溉区域组件逻辑
allen před 5 dny
rodič
revize
b8e03e0665

+ 8 - 0
pages.json

@@ -1086,6 +1086,14 @@
       }
     },
     {
+      "path": "pages/cb/wenshidp/wenshidp",
+      "style": {
+        "navigationBarTitleText": "",
+        "enablePullDownRefresh": false,
+        "navigationStyle": "custom"
+      }
+    },
+    {
       "path": "pages/cb/zhamenFirst/controls",
       "style": {
         "navigationBarTitleText": "",

+ 4 - 2
pages/cb/shuifeizs/automation.vue

@@ -302,7 +302,8 @@ uni-page-wrapper {
       left: 0;
       right: 0;
       bottom: 0;
-      background: url('./assets/controlbg.png') no-repeat;
+      background: url('https://webstaticimg.oss-cn-hangzhou.aliyuncs.com/bigdata_app/newindex/controlbg.png')
+        no-repeat;
       background-size: cover;
     }
   }
@@ -317,7 +318,8 @@ uni-page-wrapper {
       left: 0;
       right: 0;
       bottom: 0;
-      background: url('./assets/controlbg.png') no-repeat;
+      background: url('https://webstaticimg.oss-cn-hangzhou.aliyuncs.com/bigdata_app/newindex/controlbg.png')
+        no-repeat;
       background-size: cover;
       transform: rotateY(180deg);
       transform-origin: center center;

+ 14 - 26
pages/cb/shuifeizs/components/facilitystate.vue

@@ -50,21 +50,9 @@
       </view>
       <irrigatedArea :irrigatedAreaList="irrigatedAreaList"/>
     </view>
-  </view>
 </template>
 <script>
-import fertTopBottom from '../assets/fertTopBottom.png';
-import lineTopAndBottom from '../assets/lineTopAndBottom.png';
-import lineTopBottom from '../assets/lineTopBottom.png';
-import fertTopBottomRadius from '../assets/fertTopBottomRadius.png';
-import lastBorder from '../assets/lastBorder.png';
 import irrigatedArea from './irrigatedArea.vue';
-import fan from '../assets/fan.png';
-import fanRun from '../assets/fanRun.png';
-import bucketOpenTop from '../assets/bucketOpenTop.png';
-import bucketCloseTop from '../assets/bucketCloseTop.png';
-import bucketClose from '../assets/bucketClose.png';
-import bucketOpen from '../assets/bucketOpen.png';
 
 export default {
   props:{
@@ -90,19 +78,19 @@ export default {
   },
   data() {
     return {
-      fan,
-      fanRun,
-      lastBorder,
-      lineTopBottom,
-      fertTopBottom,
-      lineTopAndBottom,
-      bucketOpenTop,
+      fan:'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/deviceIcons/shuifei/fan.png',
+      fanRun:'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/deviceIcons/shuifei/fanRun.png',
+      lastBorder:'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/deviceIcons/shuifei/lastBorder.png',
+      lineTopBottom:'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/deviceIcons/shuifei/lineTopBottom.png',
+      fertTopBottom:'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/deviceIcons/shuifei/fertTopBottom.png',
+      lineTopAndBottom:'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/deviceIcons/shuifei/lineTopAndBottom.png',
+      bucketOpenTop:'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/deviceIcons/shuifei/bucketOpenTop.png',
       bucketOpenTopStatus:false,
       bucketOpenTopStatus2:false,
-      bucketCloseTop,
-      bucketClose,
-      bucketOpen,
-      fertTopBottomRadius,
+      bucketCloseTop:'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/deviceIcons/shuifei/bucketCloseTop.png',
+      bucketClose:'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/deviceIcons/shuifei/bucketClose.png',
+      bucketOpen:'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/deviceIcons/shuifei/bucketOpen.png',
+      fertTopBottomRadius:'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/deviceIcons/shuifei/fertTopBottomRadius.png',
     };
   },
   methods:{
@@ -222,7 +210,7 @@ export default {
     }
   }
   .facilitystate-list {
-    background: url('../assets/beijing.png') no-repeat;
+    background: url('https://webstaticimg.oss-cn-hangzhou.aliyuncs.com/bigdata_app/newindex/beijing.png') no-repeat;
     background-size: 100%;
     height: 376rpx;
     width: 690rpx;
@@ -352,8 +340,8 @@ export default {
         width: 70rpx;
         height: 94rpx;
         margin: 0 auto;
-        margin-top: 12rpx;
-        background: url('../assets/bucket.png') no-repeat center center;
+        margin-top: 12rpx;        
+        background: url('https://webstaticimg.oss-cn-hangzhou.aliyuncs.com/bigdata_app/newindex/bucket.png')  no-repeat center center;
         background-size: 100% 100%;
         display: flex;
         flex-direction: column;

+ 2 - 4
pages/cb/shuifeizs/components/irrigatedArea.vue

@@ -34,8 +34,6 @@
   </view>
 </template>
 <script>
-import bucketOpen1 from '../assets/bucketOpen1.png';
-import bucketClose1 from '../assets/bucketClose1.png';
 export default {
   props: {
     irrigatedAreaList: {
@@ -45,8 +43,8 @@ export default {
   },
   data() {
     return {
-      bucketOpen1,
-      bucketClose1,
+      bucketOpen1:'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/deviceIcons/shuifei/bucketOpen1.png',
+      bucketClose1:'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/deviceIcons/shuifei/bucketClose1.png',
     };
   },
   methods: {

+ 30 - 4
pages/cb/shuifeizs/components/rotationBottom.vue

@@ -11,7 +11,7 @@
     <view class="rotation-wraper-left">
       <view
         class="rotation-wraper-left__item"
-        v-for="(item, index) in alreadyList"
+        v-for="(item, index) in listData"
         :key="item.sfBid"
         @click="clickHandler(item, index)"
         :class="{ active: index === currentIndex }"
@@ -32,10 +32,12 @@
         :scroll-into-view="activeMenuId"
       >
         <rotationCard
-          v-for="item in alreadyList"
+          v-for="item in listData"
           :item="item"
           :tktype="tktype"
           :key="item.sfBid"
+          @actionChecked="actionCheckedHandler"
+          @changeTi="changeTiHandler"
           :id="'tab' + item.sfBid"
         />
       </scroll-view>
@@ -44,7 +46,6 @@
 </template>
 <script>
 import rotationCard from './rotationCard.vue';
-import borderBottomGray from '../assets/borderBottomGray.png';
 export default {
   name: 'rotationBottom',
   props: {
@@ -63,7 +64,8 @@ export default {
   },
   data() {
     return {
-      borderBottomGray,
+      listData: [],
+      borderBottomGray:'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/deviceIcons/shuifei/borderBottomGray.png',
       currentIndex: 0,
       currentItem: {},
       activeMenuId: '',
@@ -75,11 +77,35 @@ export default {
       const firstItem = this.alreadyList[0];
       this.activeMenuId = `tab${firstItem?.sfBid}`;
     },
+    alreadyList(value) {
+      this.listData = value;
+    },
   },
   components: {
     rotationCard,
   },
   methods: {
+    changeTiHandler(item) {
+      const index = this.getItemBySfBid(item.sfBid);
+      this.$set(this.listData, index, {
+        ...item,
+        ti: item.ti,
+      });
+      this.$emit('updateList', this.listData);
+    },
+    // 通过sfBid查到某个item的index
+    getItemBySfBid(sfBid) {
+      return this.listData.findIndex((item) => item.sfBid === sfBid);
+    },
+    actionCheckedHandler(item) {
+      const index = this.getItemBySfBid(item.sfBid);
+      this.$set(this.listData, index, {
+        ...item,
+        isChecked: !item.isChecked,
+      });
+      this.$emit('updateList', this.listData);
+    },
+
     clickHandler(item, index) {
       this.currentItem = item;
       this.currentIndex = index;

+ 3 - 6
pages/cb/shuifeizs/control.vue

@@ -57,9 +57,6 @@
   </view>
 </template>
 <script>
-import solenoidValve from './assets/solenoidValve.png';
-import stir from './assets/stir.png';
-import borderRadius from './assets/borderRadius.png';
 
 export default {
   name: 'control',
@@ -67,9 +64,9 @@ export default {
     return {
       activeIndex: 0,
       value: false,
-      solenoidValve,
-      stir,
-      borderRadius,
+      solenoidValve: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/deviceIcons/shuifei/solenoidValve.png',
+      stir: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/deviceIcons/shuifei/stir.png',
+      borderRadius: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/deviceIcons/shuifei/borderRadius.png',
       title: '控制面板',
       leftList: [],
       deviceList: [],

+ 4 - 12
pages/cb/shuifeizs/detail.vue

@@ -32,21 +32,13 @@
 </template>
 
 <script>
-import manualControl from './assets/manualControl.png';
-import wheelIrrigation from './assets/wheelIrrigation.png';
-import timing from './assets/timing.png';
-import masterStop from './assets/masterStop.png';
-import fertilizationFormula from './assets/fertilizationFormula.png';
-import backwashControl from './assets/backwashControl.png';
-import basicSetting from './assets/basicSetting.png';
-import operatingRecord from './assets/operatingRecord.png';
 import Base from './components/base.vue';
 import facilitystate from './components/facilitystate.vue';
 export default {
   data() {
     return {
       title: '水肥一体机',
-      manualControl,
+      manualControl:'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/deviceIcons/shuifei/manualControl.png',
       devBid: '',
       devName: '',
       devStatus: '1',
@@ -55,17 +47,17 @@ export default {
       alreadyfertilizerBucketList: [],
       deviceList: [
         {
-          icon: manualControl,
+          icon: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/deviceIcons/shuifei/manualControl.png',
           title: '手动控制',
           url: '/pages/cb/shuifeizs/control',
         },
         {
-          icon: wheelIrrigation,
+          icon:'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/deviceIcons/shuifei/wheelIrrigation.png',
           title: '自动控制',
           url: '',
         },
         {
-          icon: operatingRecord,
+          icon: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/deviceIcons/shuifei/operatingRecord.png',
           title: '操作记录',
           url: '/pages/cb/shuifeizs/history',
         },

+ 0 - 1
pages/cb/shuifeizs/history.vue

@@ -75,7 +75,6 @@
   </view>
 </template>
 <script>
-import solenoidValve from './assets/solenoidValve.png';
 export default {
   name: 'actionHistory',
   data() {

+ 3 - 7
pages/cb/shuifeizs/rotationflow.vue

@@ -106,18 +106,14 @@
   </view>
 </template>
 <script>
-import rotate from './assets/rotate.png';
-import water from './assets/water.png';
-import autoBtn from './assets/autoBtn.png';
-import json from './test.js';
 
 export default {
   name: 'rotationflow',
   data() {
     return {
-      rotate,
-      water,
-      autoBtn,
+      rotate: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/deviceIcons/shuifei/rotate.png',
+      water: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/deviceIcons/shuifei/water.png',
+      autoBtn: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/deviceIcons/shuifei/autoBtn.png',
       time: '-',
       list: [],
       timer: null,

+ 10 - 18
pages/cb/shuifeizs/shuifeizs.vue

@@ -32,21 +32,13 @@
 </template>
 
 <script>
-import manualControl from './assets/manualControl.png';
-import wheelIrrigation from './assets/wheelIrrigation.png';
-import timing from './assets/timing.png';
-import masterStop from './assets/masterStop.png';
-import fertilizationFormula from './assets/fertilizationFormula.png';
-import backwashControl from './assets/backwashControl.png';
-import basicSetting from './assets/basicSetting.png';
-import operatingRecord from './assets/operatingRecord.png';
 import Base from './components/base.vue';
 import facilitystate from './components/facilitystate.vue';
 export default {
   data() {
     return {
-      title: '水肥一体机',
-      manualControl,
+      title: '水肥一体机', 
+      manualControl: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/deviceIcons/shuifei/manualControl.png',
       devBid: '',
       devName: '',
       devStatus: '1',
@@ -55,42 +47,42 @@ export default {
       alreadyfertilizerBucketList: [],
       deviceList: [
         {
-          icon: manualControl,
+          icon: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/deviceIcons/shuifei/manualControl.png',
           title: '手动控制',
           url: '/pages/cb/shuifeizs/control',
         },
         {
-          icon: wheelIrrigation,
+          icon: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/deviceIcons/shuifei/wheelIrrigation.png',
           title: '自动控制',
           url: '',
         },
         {
-          icon: operatingRecord,
+          icon: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/deviceIcons/shuifei/operatingRecord.png',
           title: '操作记录',
           url: '/pages/cb/shuifeizs/history',
         },
         // {
-        //   icon: timing,
+        //   icon: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/deviceIcons/shuifei/timing.png',
         //   title: '定时计划',
         //   url: '',
         // },
         // {
-        //   icon: masterStop,
+        //   icon: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/deviceIcons/shuifei/masterStop.png',
         //   title: '总停',
         //   url: '',
         // },
         // {
-        //   icon: fertilizationFormula,
+        //   icon: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/deviceIcons/shuifei/fertilizationFormula.png',
         //   title: '施肥配方',
         //   url: '',
         // },
         // {
-        //   icon: backwashControl,
+        //   icon: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/deviceIcons/shuifei/backwashControl.png',
         //   title: '反冲洗控制',
         //   url: '',
         // },
         // {
-        //   icon: basicSetting,
+        //   icon: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/deviceIcons/shuifei/basicSetting.png',
         //   title: '基础设置',
         //   url: '',
         // },

+ 183 - 0
pages/cb/wenshidp/components/DeviceCard.vue

@@ -0,0 +1,183 @@
+<template>
+  <view class="device-card" @click="handleClick">
+    <view class="device-card__status">
+      <image
+        class="img"
+        :src="dataSource.devStatus == '1' ? successImg : errorImg"
+      />
+      <view
+        class="status-text"
+        :class="{ offline: dataSource.devStatus != '1' }"
+        >{{ dataSource.devStatusName || '' }}</view
+      >
+    </view>
+    <view class="device-card__header">
+      <view class="device-card__title u-line-1">
+        <view class="name">设备ID</view>
+        <view class="tips">{{ dataSource.devCode || '' }}</view>
+      </view>
+    </view>
+    <view class="device-card__body">
+      <view class="device-card__row">
+        <view class="device-card__label">上报时间</view>
+        <view class="device-card__value">{{ dataSource.devUpdateddate }}</view>
+      </view>
+      <view class="device-card__row">
+        <view class="device-card__label">设备位置</view>
+        <view class="device-card__value">{{
+          `${dataSource.devProvincealign || dataSource.devProvince || ''}  ${
+            dataSource.devCityalign || dataSource.devCity || ''
+          } ${dataSource.devDistrictalign || dataSource.devDistrict || ''}`
+        }}</view>
+        <!-- <view class="ntoNative" @tap="toNavigation">
+          <image :src="location" class="icon"></image>
+        </view> -->
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  name: 'DeviceCard',
+  props: {
+    dataSource: {
+      type: Object,
+      default() {
+        return {};
+      },
+    },
+  },
+  data() {
+    return {
+      location:
+        'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/image/zhongshui/location.svg',
+      successImg:
+        'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/image/zhongshui/success.png',
+      errorImg:
+        'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/image/zhongshui/error.png',
+    };
+  },
+  methods: {
+    handleClick() {},
+    toNavigation() {
+      this.$emit('navigation');
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.device-card {
+  background: linear-gradient(180deg, #8ae4c93d 0%, #14a47800 25.54%), #fff;
+  position: relative;
+  padding: 28rpx;
+  border-radius: 16rpx;
+  background-color: #fff;
+  margin-bottom: 32rpx;
+  margin: 0 auto;
+  &__status {
+    position: absolute;
+    right: 0;
+    top: 0;
+    width: 150rpx;
+    height: 56rpx;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    .img {
+      position: absolute;
+      width: 100%;
+      height: 100%;
+      right: 0;
+      top: 0;
+      overflow: hidden;
+    }
+
+    .status-text {
+      font-size: 28rpx;
+      color: #14a478;
+
+      &.online {
+        color: #14a478;
+      }
+
+      &.offline {
+        color: #ff3546;
+      }
+    }
+  }
+
+
+  &__title {
+    align-items: center;
+    line-height: 48rpx;
+    display: flex !important;
+    .name {
+      width: 140rpx;
+      color: #9ba6a3;
+      margin-right: 32rpx;
+      // 超出隐藏
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      text-align: left;
+      font-family: 'Source Han Sans CN VF';
+    }
+
+    .tips {
+      width: 410rpx;
+      // 超出隐藏
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      text-align: left;
+    }
+  }
+
+  &__body {
+    padding: 16rpx 0;
+    background-color: #fff;
+    border-radius: 12rpx;
+  }
+
+  &__row {
+    display: flex;
+    align-items: center;
+    margin-bottom: 16rpx;
+    position: relative;
+    &:last-child {
+      margin-bottom: 0;
+    }
+    .ntoNative {
+      position: absolute;
+      right: 16rpx;
+      width: 80rpx;
+      height: 80rpx;
+      display: flex;
+      align-items: center;
+      justify-content: flex-end;
+      .icon {
+        width: 30rpx;
+        height: 30rpx;
+      }
+    }
+  }
+
+  &__label {
+    width: 140rpx;
+    color: #9ba6a3;
+    text-align: left;
+    margin-right: 32rpx;
+  }
+
+  &__value {
+    width: 420rpx;
+    text-align: left;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+  }
+}
+</style>

+ 230 - 0
pages/cb/wenshidp/components/OperationCard.vue

@@ -0,0 +1,230 @@
+<template>
+  <view class="operation">
+    <view class="operation__header">
+      <image class="operation__icon" :src="iconSrc" mode="aspectFit" />
+      <view class="operation__name">{{
+        dourceData.display_name || dourceData.name || dourceData.code
+      }}</view>
+    </view>
+    <view class="operation__body">
+      <!-- 三状态:按钮形式(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
+        >
+      </block>
+      <!-- 两状态:单个开关(1 开 0 关) -->
+      <view v-else class="operation__switch-row">
+        <view
+          class="operation__switch-label"
+          :class="{ on: statusValue == 1 }"
+          >{{ statusValue == 1 ? '开' : '关' }}</view
+        >
+        <u-switch
+          :value="statusValue == 1"
+          active-color="#0bbc58"
+          inactive-color="#C3CAD8"
+          size="36"
+          @input="handleSwitch"
+        ></u-switch>
+      </view>
+    </view>
+  </view>
+</template>
+<script>
+
+const ICONS = {
+  inner_shade_on: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/greenHouseIcon/inner_shade_on.png',
+  inner_shade_off: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/greenHouseIcon/inner_shade_off.png',
+  outer_shade_on: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/greenHouseIcon/outer_shade_on.png',
+  outer_shade_off: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/greenHouseIcon/outer_shade_off.png',
+  roof_window_on: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/greenHouseIcon/roof_window_on.png',
+  roof_window_off: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/greenHouseIcon/roof_window_off.png',
+  side_window_on: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/greenHouseIcon/side_window_on.png',
+  side_window_off: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/greenHouseIcon/side_window_off.png',
+  fan_on: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/greenHouseIcon/fan_on.png',
+  fan_off: 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/greenHouseIcon/fan_off.png',
+};
+
+export default {
+  name: 'OperationCard',
+  props: {
+    dourceData: {
+      type: Object,
+      default: () => ({}),
+    },
+  },
+  computed: {
+    // 存在 CurrentPos 字段即为三状态(0 停止 1 正向 2 反向),否则两状态(0 停止 1 开启)
+    // CurrentPos 可能是明文字段或 ChannelParamSetXX:CurrentPos 平铺属性,只用于判定形态
+    hasPos() {
+      const d = this.dourceData;
+      if ('CurrentPos' in d) {
+        return true;
+      }
+      return Object.keys(d).some((k) =>
+        /^ChannelParamSet\d+:CurrentPos$/.test(k)
+      );
+    },
+    options() {
+      return [
+        { label: '正向', value: 1 },
+        { label: '反向', value: 2 },
+        { label: '停止', value: 0 },
+      ];
+    },
+    // 通道遥测 key:形如 ChannelParamSet00:Status
+    statusKey() {
+      const d = this.dourceData;
+      const code = d.code || d.sfCode;
+      if (code && /^ChannelParamSet\d+:Status$/.test(String(code))) {
+        return String(code);
+      }
+      return (
+        Object.keys(d).find((k) => /^ChannelParamSet\d+:Status$/.test(k)) || ''
+      );
+    },
+    // Status 值存放的属性名:同名属性或 value
+    statusField() {
+      const d = this.dourceData;
+      return this.statusKey && this.statusKey in d ? this.statusKey : 'value';
+    },
+    // 高亮始终取 Status 值:三状态 0 停止 1 正向 2 反向,两状态 0 停止 1 开启
+    statusValue() {
+      const d = this.dourceData;
+      const val = this.statusKey ? d[this.statusKey] : d.value;
+      return val == null ? 0 : Number(val);
+    },
+    // 按通道名匹配图标,随运行状态切换 on/off 变体
+    iconSrc() {
+      const d = this.dourceData;
+      const name = String(d.display_name || d.name || '');
+      let base = '';
+      if (name.includes('内遮阳')) {
+        base = 'inner_shade';
+      } else if (name.includes('外遮阳')) {
+        base = 'outer_shade';
+      } else if (name.includes('顶开窗') || name.includes('顶通风')) {
+        base = 'roof_window';
+      } else if (name.includes('侧开窗') || name.includes('侧通风')) {
+        base = 'side_window';
+      } else if (name.includes('风机')) {
+        base = 'fan';
+      } else {
+        base = this.hasPos ? 'inner_shade' : 'roof_window';
+      }
+      const state = this.statusValue == 0 ? 'off' : 'on';
+      return ICONS[`${base}_${state}`];
+    },
+  },
+  methods: {
+    // 三状态按钮高亮:正/反向绿色,停止橙色
+    btnClass(value) {
+      if (Number(value) !== this.statusValue) {
+        return '';
+      }
+      return Number(value) === 0
+        ? 'operation__btn--stop'
+        : 'operation__btn--run';
+    },
+    handleClick(value) {
+      this.$emit('change', {
+        dourceData: this.dourceData,
+        value,
+        code: this.statusKey || this.dourceData.code || '',
+        field: this.statusField,
+      });
+    },
+    // 两状态开关:切换后 开=1 关=0
+    handleSwitch(newValue) {
+      this.handleClick(newValue ? 1 : 0);
+    },
+  },
+};
+</script>
+<style scoped lang="scss">
+.operation {
+  // 卡片浅灰底,与头部渐变底色衔接;白色按钮直接落在灰底上
+  background: #eff2fa;
+  border-radius: 16rpx;
+  margin-bottom: 24rpx;
+  overflow: hidden;
+  &__header {
+    display: flex;
+    align-items: center;
+    padding: 20rpx 24rpx;
+    background: linear-gradient(180deg, #eff4ff 20.24%, #eff2fa 100%);
+  }
+  &__icon {
+    flex-shrink: 0;
+    width: 44rpx;
+    height: 44rpx;
+    margin-right: 12rpx;
+  }
+  &__name {
+    color: #333333;
+    font-family: 'Source Han Sans CN VF';
+    font-size: 28rpx;
+    font-weight: 500;
+    overflow: hidden;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+  }
+  &__body {
+    display: flex;
+    align-items: center;
+    padding: 20rpx 24rpx 24rpx;
+  }
+  &__btn {
+    flex: 1;
+    height: 72rpx;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    border-radius: 12rpx;
+    background: #ffffff;
+    border: 2rpx solid #eef0f5;
+    box-sizing: border-box;
+    color: #999999;
+    font-family: 'Source Han Sans CN VF';
+    font-size: 26rpx;
+    font-weight: 400;
+    margin-right: 16rpx;
+    &:last-child {
+      margin-right: 0;
+    }
+    &--run {
+      background: #0bbc58;
+      border-color: #0bbc58;
+      color: #ffffff;
+      font-weight: 500;
+    }
+    &--stop {
+      background: #ffa200;
+      border-color: #ffa200;
+      color: #ffffff;
+      font-weight: 500;
+    }
+  }
+  &__switch-row {
+    flex: 1;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+  }
+  &__switch-label {
+    color: #999999;
+    font-family: 'Source Han Sans CN VF';
+    font-size: 26rpx;
+    font-weight: 400;
+    &.on {
+      color: #333333;
+    }
+  }
+}
+</style>

+ 260 - 0
pages/cb/wenshidp/components/RecordList.vue

@@ -0,0 +1,260 @@
+<template>
+  <view class="record">
+    <view class="record__header">
+      <view>操作记录</view>
+    </view>
+    <view class="record__calendar" @click="show = true">
+      <view class="record__calendar-content">
+        <text>{{ startTime }}</text>
+        <text class="separate">至</text>
+        <text>{{ endTime }}</text>
+      </view>
+      <view class="record__calendar-link">
+        <u-icon name="calendar" color="#4e5969" size="34"></u-icon>
+      </view>
+    </view>
+    <view class="record__columns">
+      <view class="record__col">设备名称</view>
+      <view class="record__col">操作内容</view>
+      <view class="record__col">操作人</view>
+      <view class="record__col">操作时间</view>
+    </view>
+    <scroll-view
+      class="record__list"
+      scroll-y
+      v-if="recordList.length"
+      @scrolltolower="scrolltolower"
+    >
+      <view
+        class="record__item"
+        v-for="(item, index) in recordList"
+        :key="item.oprcdBid || index"
+      >
+        <view class="record__col record__name">{{
+          item.oprecdName || '-'
+        }}</view>
+        <view class="record__col">
+          <view
+            class="record__tag"
+            :class="{ active: item.oprecdStatus == 1 || item.oprecdStatus == 2 }"
+            >{{ item.oprecdContent || '-' }}</view
+          >
+        </view>
+        <view class="record__col">{{ item.oprcdCreatorName || '-' }}</view>
+        <view class="record__col record__time">
+          <view class="record__hour">{{ getTime(item.oprcdCreateddate) }}</view>
+          <view class="record__date">{{ getDate(item.oprcdCreateddate) }}</view>
+        </view>
+      </view>
+      <view class="record__nomore" v-if="!hasMore">没有更多了</view>
+    </scroll-view>
+    <view class="record__empty" v-else>
+      <u-empty v-if="!loading" text="暂无数据" />
+    </view>
+    <u-calendar
+      v-model="show"
+      mode="range"
+      @change="handleCalendarConfirm"
+      range-color="#999"
+      btn-type="success"
+      active-bg-color="#0BBC58"
+      range-bg-color="rgba(11,188,88,0.13)"
+    ></u-calendar>
+  </view>
+</template>
+<script>
+export default {
+  name: 'RecordList',
+  props: {
+    devBid: {
+      type: String,
+      default: '',
+    },
+  },
+  data() {
+    return {
+      pageNum: 1,
+      pageSize: 10,
+      show: false,
+      // 默认查询当前日期向前 3 个月
+      startTime: this.formartDate(3),
+      endTime: this.formartDate(),
+      recordList: [],
+      total: 0,
+      loading: false,
+    };
+  },
+  computed: {
+    hasMore() {
+      return this.total > this.pageNum * this.pageSize;
+    },
+  },
+  created() {
+    this.getRecordList();
+  },
+  methods: {
+    async getRecordList() {
+      this.loading = true;
+      try {
+        const res = await this.$myRequest({
+          url: '/api/v2/iot/device/kzg/yunshang/op/record/list/',
+          method: 'post',
+          header: {
+            'Content-Type': 'application/json',
+          },
+          // 传 sfType 使 $myRequest resolve 完整响应体(含 data + total)
+          sfType: true,
+          data: {
+            devBid: this.devBid,
+            startTime: this.startTime + ' 00:00:00',
+            endTime: this.endTime + ' 23:59:59',
+            pageNum: this.pageNum,
+            pageSize: this.pageSize,
+          },
+        });
+        this.recordList = this.recordList.concat(res?.data || []);
+        this.total = res?.total || 0;
+      } finally {
+        this.loading = false;
+      }
+    },
+    scrolltolower() {
+      if (this.loading || !this.hasMore) {
+        return;
+      }
+      this.pageNum++;
+      this.getRecordList();
+    },
+    // 选择日期后重置分页重新查询
+    handleCalendarConfirm(e) {
+      this.pageNum = 1;
+      this.recordList = [];
+      this.total = 0;
+      this.startTime = e.startDate;
+      this.endTime = e.endDate;
+      this.getRecordList();
+    },
+    getDate(date) {
+      return (date || '').split(' ')[0] || '-';
+    },
+    getTime(date) {
+      return (date || '').split(' ')[1] || '-';
+    },
+    formartDate(month = 0) {
+      const time = month * 30 * 24 * 60 * 60 * 1000;
+      const timeStamp = new Date(new Date().getTime() - time);
+      const year = timeStamp.getFullYear();
+      const monthDate = timeStamp.getMonth() + 1;
+      const monthStr = monthDate < 10 ? `0${monthDate}` : monthDate;
+      const day = new Date().getDate();
+      const dayStr = day < 10 ? `0${day}` : day;
+      return `${year}-${monthStr}-${dayStr}`;
+    },
+  },
+};
+</script>
+<style scoped lang="scss">
+::v-deep .u-calendar__action {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+::v-deep .u-hover-class {
+  opacity: 0.6;
+}
+.record {
+  &__header {
+    color: #042118;
+    font-family: 'Source Han Sans CN VF';
+    font-size: 28rpx;
+    font-weight: 700;
+    margin-bottom: 24rpx;
+  }
+  &__calendar {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    height: 64rpx;
+    padding: 0 32rpx;
+    border-radius: 32rpx;
+    background: #f6f7f9;
+    margin-bottom: 24rpx;
+    &-content {
+      font-size: 28rpx;
+      color: #4e5969;
+      .separate {
+        margin: 0 12rpx;
+      }
+    }
+    &-link {
+      display: flex;
+      align-items: center;
+    }
+  }
+  &__columns {
+    display: flex;
+    align-items: center;
+    height: 72rpx;
+    border-bottom: 2rpx solid #e4e7ed;
+  }
+  &__col {
+    flex: 1;
+    text-align: center;
+    color: #042118;
+    font-family: 'Source Han Sans CN VF';
+    font-size: 28rpx;
+    font-weight: 400;
+  }
+  &__list {
+    height: 720rpx;
+  }
+  &__item {
+    display: flex;
+    align-items: center;
+    height: 96rpx;
+    border-bottom: 2rpx solid #f2f3f5;
+    .record__name {
+      // 超出隐藏
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+    .record__tag {
+      display: inline-flex;
+      height: 40rpx;
+      padding: 4rpx 16rpx;
+      justify-content: center;
+      align-items: center;
+      border-radius: 20rpx;
+      background: #7a82911a;
+      color: #7a8291;
+      font-family: 'Source Han Sans CN VF';
+      font-size: 24rpx;
+      // 状态 1/2(启动/正向/反向)绿色,0(停止)灰色
+      &.active {
+        background: #14a47824;
+        color: #14a478;
+      }
+    }
+    .record__time {
+      .record__hour {
+        font-size: 28rpx;
+      }
+      .record__date {
+        font-size: 24rpx;
+        color: #7a8291;
+      }
+    }
+  }
+  &__nomore {
+    padding: 24rpx 0;
+    text-align: center;
+    color: #999999;
+    font-family: 'Source Han Sans CN VF';
+    font-size: 24rpx;
+  }
+  &__empty {
+    padding: 80rpx 0;
+  }
+}
+</style>

+ 185 - 0
pages/cb/wenshidp/components/element.vue

@@ -0,0 +1,185 @@
+<template>
+  <view class="element">
+    <view class="element-header">
+      <view class="element-title">{{ title }}</view>
+      <view class="element-status" v-if="!sensorList">
+        2024-01-25 16:05:05
+        <u-icon name="reload" color="#14A478" style="margin-left: 20rpx" />
+      </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>
+          <view class="element-col-content">{{ item.title }}</view>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+<script>
+
+export default {
+  name: 'Element',
+  props: {
+    // status 接口返回的 sensor_list,为空时展示内置示例
+    sensorList: {
+      type: Array,
+      default: null,
+    },
+    title: {
+      type: String,
+      default: '1号环控',
+    },
+  },
+  computed: {
+    displayList() {
+      if (!this.sensorList || !this.sensorList.length) {
+        return this.list;
+      }
+      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),
+        };
+      });
+    },
+  },
+  data() {
+    return {
+      list: [
+        {
+          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',
+        },
+      ],
+    };
+  },
+  methods: {
+    // 按传感器名称匹配图标
+    iconByName(name) {
+      const n = String(name || '');
+      if (n.includes('土壤') && n.includes('湿')) {
+        return 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/greenHouseIcon/sensor_soil_humidity.png';
+      }
+      if (n.includes('土壤')) {
+        return 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/greenHouseIcon/sensor_soil_temp.png';
+      }
+      if (n.includes('湿')) {
+        return 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/greenHouseIcon/sensor_humidity.png';
+      }
+      if (n.includes('二氧化碳')) {
+        return 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/greenHouseIcon/sensor_co2.png';
+      }
+      if (n.includes('光照') || n.includes('亮度')) {
+        return 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/greenHouseIcon/sensor_sun.png';
+      }
+      if (n.toLowerCase().includes('ph')) {
+        return 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/greenHouseIcon/sensor_ph.png';
+      }
+      return 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/greenHouseIcon/sensor_temp.png';
+    },
+  },
+};
+</script>
+<style lang="less" scoped>
+.element {
+  .element-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 20rpx;
+    color: #042118;
+    font-family: 'Source Han Sans CN VF';
+    font-size: 28rpx;
+    font-weight: 700;
+    .element-status {
+      color: #687a74;
+      text-align: center;
+      font-family: 'Source Han Sans CN VF';
+      font-size: 28rpx;
+      font-weight: 400;
+    }
+  }
+  .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-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: #666;
+        }
+      }
+    }
+  }
+}
+</style>

+ 417 - 0
pages/cb/wenshidp/wenshidp.vue

@@ -0,0 +1,417 @@
+<template>
+  <view class="greenhouse-page">
+    <custom-card>
+      <block slot="backText">
+        <view class="greenhouse-top__title">
+          {{ dataSource.devName }}
+        </view> </block
+      ><view slot="right">
+        <!-- <view class="ntoNative" @tap="toNavigation">
+          <image :src="location" class="icon"></image>
+        </view> -->
+      </view>
+    </custom-card>
+    <view class="greenhouse-top">
+      <device-card :dataSource="dataSource" @navigation="toNavigation" />
+    </view>
+    <view class="tabs">
+      <view
+        class="tab-item"
+        :class="{ active: currentTab === 'env' }"
+        @click="currentTab = 'env'"
+        >环境要素</view
+      >
+      <view
+        class="tab-item"
+        :class="{ active: currentTab === 'channel' }"
+        @click="currentTab = 'channel'"
+        >大棚控制</view
+      >
+      <view
+        class="tab-item"
+        :class="{ active: currentTab === 'record' }"
+        @click="currentTab = 'record'"
+        >操作记录</view
+      >
+    </view>
+    <!-- 大棚控制:通道卡片 -->
+    <view class="greenhouse-bottom" v-if="currentTab === 'channel'">
+      <view class="greenhouse-bottom-header">
+        大棚控制
+      </view>
+      <operation-card
+        @change="channelControlHandler"
+        v-for="(item, index) in juanmoList"
+        :key="index"
+        :dourceData="item"
+      />
+    </view>
+    <!-- 环境要素:传感器数据 -->
+    <view class="greenhouse-bottom" v-else-if="currentTab === 'env'">
+      <Element :sensorList="sensorList" title="环境要素" />
+    </view>
+    <!-- 操作记录 -->
+    <view class="greenhouse-bottom" v-else>
+      <RecordList :devBid="devBid" />
+    </view>
+  </view>
+</template>
+
+<script>
+import Element from './components/element.vue';
+import deviceCard from './components/DeviceCard.vue';
+import OperationCard from './components/OperationCard.vue';
+import RecordList from './components/RecordList.vue';
+
+export default {
+  components: {
+    Element,
+    deviceCard,
+    OperationCard,
+    RecordList,
+  },
+  data() {
+    return {
+      location:
+        'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/image/zhongshui/location.svg',
+      ws: null,
+      sfToken: '',
+      entityId: '',
+      heartbeatTimer: null,
+      dataArray: [],
+      webSockedData: {},
+      reconnectCount: 0,
+      manualClosed: false,
+      currentTab: 'channel',
+      juanmoList: [],
+      sensorList: [],
+      dataSource: {
+        devStatus: '1',
+        devStatusName: '在线',
+        devName: '-',
+        devCode: '-',
+        devUpdateddate: '-',
+        devProvincealign: '-',
+      },
+    };
+  },
+  methods: {
+    init() {
+      this.getKzgYunshangStatus();
+      this.getYunshangInfo();
+    },
+    // 通道控制:三状态 value 0/1/2,两状态 value 0/1
+    channelControlHandler(prop) {
+      const { dourceData, code, value, field } = prop;
+      // 本地先行更新开关状态,稍后拉取接口校正
+      this.$set(dourceData, field || 'value', value);
+      this.optDevctl({
+        devBid: this.devBid,
+        data: {
+          [code]: value,
+        },
+      });
+    },
+    async optDevctl(payload) {
+      const res = await this.$myRequest({
+        url: '/api/v2/iot/device/kzg/yunshang/devctl/',
+        method: 'post',
+        data: payload,
+        header: {
+          'Content-Type': 'application/json',
+        },
+      });
+      // 提示
+      uni.showToast({
+        title: res?.msg || '',
+        icon: 'none',
+        duration: 3000,
+      });
+      setTimeout(() => {
+        this.getKzgYunshangStatus();
+      }, 1000);
+    },
+    // 获取设备详情(sfToken 用于 WebSocket 鉴权)
+    async getYunshangInfo() {
+      const res = await this.$myRequest({
+        url: '/api/v2/iot/device/kzg/yunshang/info/',
+        method: 'post',
+        data: {
+          devBid: this.devBid,
+        },
+      });
+      this.dataSource = res;
+      this.sfToken = res?.sfToken;
+      this.entityId = res?.sfUuid;
+      this.closeWebSocket();
+      this.initWebSocket();
+    },
+    toNavigation() {
+      const roundedLongitude =
+        this.dataSource.devLngalign || this.dataSource.devLng;
+      const roundedLatitude =
+        this.dataSource.devLatalign || this.dataSource.devLat;
+      uni.openLocation({
+        latitude: Number(roundedLatitude || 0),
+        longitude: Number(roundedLongitude || 0),
+        name: '',
+        address: '',
+        scale: 18, // 地图缩放级别
+        success: () => console.log('导航启动成功'),
+        fail: (err) => console.error('导航失败:', err),
+      });
+      // if (!roundedLongitude) {
+      //   uni.showToast({
+      //     title: '当前设备无定位信息',
+      //     icon: 'none',
+      //     duration: 3000,
+      //   });
+      //   return;
+      // }
+      // if (
+      //   plus.runtime.isApplicationExist({
+      //     pname: 'com.autonavi.minimap',
+      //     action: 'iosamap://',
+      //   })
+      // ) {
+      //   let url = `amapuri://route/plan?sourceApplication=yourAppName&dlat=${roundedLatitude}&dlon=${roundedLongitude}&dev=0&t=0&style=0&start=&auto=1&rtdType=1&coordinate=wgs84`;
+      //   plus.runtime.openURL(url);
+      // } else if (
+      //   plus.runtime.isApplicationExist({
+      //     pname: 'com.baidu.BaiduMap',
+      //     action: 'baidumap://',
+      //   })
+      // ) {
+      //   let url = `baidumap://map/direction?destination=${roundedLongitude},${roundedLatitude}&mode=navigation&output=html&coord_type=wgs84`;
+      //   plus.runtime.openURL(url);
+      // } else {
+      //   uni.showToast({
+      //     title: '当前未安装 高德地图或百度地图 无法导航',
+      //     icon: 'none',
+      //     duration: 3000,
+      //   });
+      // }
+    },
+    // 获取控制柜状态信息(只取 channel_list,sensor_list 不处理)
+    async getKzgYunshangStatus() {
+      const res = await this.$myRequest({
+        url: '/api/v2/iot/device/kzg/yunshang/status/',
+        method: 'post',
+        data: {
+          devBid: this.devBid,
+        },
+      });
+      this.dataArray = res?.channel_list || [];
+      this.juanmoList = this.dataArray;
+      this.sensorList = res?.sensor_list || [];
+    },
+    initWebSocket() {
+      this.manualClosed = false;
+      const url = 'wss://things.ysiot.net:18080/api/ws';
+      this.ws = uni.connectSocket({
+        url: url,
+        success: () => {
+          console.log('WebSocket 连接初始化成功');
+        },
+      });
+
+      uni.onSocketOpen(() => {
+        console.log('WebSocket 已连接');
+        // 订阅设备实时遥测,鉴权 token 来自 kzg/yunshang/info 接口
+        const params = {
+          cmds: [
+            {
+              type: 'TIMESERIES',
+              entityType: 'DEVICE',
+              entityId: this.entityId,
+              scope: 'LATEST_TELEMETRY',
+              cmdId: 1,
+            },
+          ],
+          authCmd: {
+            cmdId: 0,
+            token: this.sfToken,
+          },
+        };
+        uni.sendSocketMessage({
+          data: JSON.stringify(params),
+        });
+
+        // 心跳:每 30 秒 ping 一次
+        this.heartbeatTimer = setInterval(() => {
+          uni.sendSocketMessage({
+            data: JSON.stringify({ type: 'ping' }),
+          });
+        }, 30 * 1000);
+      });
+
+      uni.onSocketMessage((evt) => {
+        try {
+          const data = JSON.parse(evt.data);
+          this.webSockedData = data;
+          // 收到实时数据后刷新控制柜状态
+          if (this.dataArray.length) {
+            this.mergeTwoObject(this.dataArray, this.webSockedData?.data);
+          }
+        } catch (e) {
+          console.error('WebSocket 消息解析失败', e);
+        }
+      });
+
+      uni.onSocketError((e) => {
+        console.error('WebSocket 错误', e);
+      });
+
+      uni.onSocketClose(() => {
+        console.warn('WebSocket 已断开,3 秒后尝试重连');
+        clearInterval(this.heartbeatTimer);
+        if (this.manualClosed) {
+          return;
+        }
+        this.reconnectCount = (this.reconnectCount || 0) + 1;
+        if (this.reconnectCount <= 10) {
+          setTimeout(() => this.initWebSocket(), 3000);
+        } else {
+          console.warn('WebSocket 重连次数已达上限,停止重连');
+        }
+      });
+    },
+    // 用 WebSocket 实时数据按 ChannelParamSetXX:Status 匹配 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';
+          }
+          if (statusKey) {
+            codeMap.set(statusKey, { item, statusField });
+          }
+        }
+      };
+      buildMap(firstObject);
+      // 遍历 secondObject 并更新值
+      for (const key in secondObject) {
+        let target = codeMap.get(key);
+        // 兼容不带后缀的上报 key
+        if (!target && /^ChannelParamSet\d+$/.test(key)) {
+          target = codeMap.get(`${key}:Status`);
+        }
+        if (!target) {
+          continue;
+        }
+        const val = secondObject[key]?.[0]?.[1];
+        if (val === undefined) {
+          continue;
+        }
+        this.$set(target.item, target.statusField, val);
+      }
+    },
+    // 关闭 WebSocket
+    closeWebSocket() {
+      this.manualClosed = true;
+      clearInterval(this.heartbeatTimer);
+      if (this.ws) {
+        uni.closeSocket();
+        this.ws = null;
+      }
+    },
+  },
+  onLoad(query) {
+    this.devBid = query.devBid;
+    this.devBid && this.init();
+  },
+  onUnload() {
+    this.manualClosed = true;
+    this.closeWebSocket();
+  },
+};
+</script>
+<style scoped lang="scss">
+uni-page-body {
+  position: relative;
+  height: 100%;
+}
+.icon {
+  width: 40rpx;
+  height: 40rpx;
+}
+.tabs {
+  display: flex;
+  align-items: center;
+  margin: 24rpx 30rpx 0;
+  padding: 8rpx 16rpx;
+  background: #ffffff;
+  border-radius: 24rpx;
+  .tab-item {
+    flex: 1;
+    height: 80rpx;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    position: relative;
+    color: #999999;
+    font-family: 'Source Han Sans CN VF';
+    font-size: 28rpx;
+    font-weight: 400;
+    &.active {
+      color: #042118;
+      font-weight: 700;
+      &::after {
+        content: '';
+        position: absolute;
+        bottom: 8rpx;
+        left: 50%;
+        transform: translateX(-50%);
+        width: 48rpx;
+        height: 8rpx;
+        border-radius: 4rpx;
+        background: #0bbc58;
+      }
+    }
+  }
+}
+.greenhouse-page {
+  background: linear-gradient(
+      180deg,
+      #ffffff00 0%,
+      #eff2fa 23.64%,
+      #eff2fa 100%
+    ),
+    linear-gradient(102deg, #bfeadd 6.77%, #b8f1e7 40.15%, #b9eef5 84.02%);
+  min-height: 100vh;
+  width: 100%;
+  overflow-x: hidden;
+  overflow-y: scroll;
+  .greenhouse-top {
+    text-align: center;
+    margin: 0 auto;
+    margin-top: 18rpx;
+    padding: 0 30rpx;
+  }
+  .greenhouse-bottom {
+    width: calc(100% - 128rpx);
+    padding: 32rpx 32rpx 1rpx 32rpx;
+    background: #ffffff;
+    border-radius: 16rpx;
+    margin: 0 auto;
+    margin-top: 32rpx;
+    .greenhouse-bottom-header {
+      color: #042118;
+      font-family: 'Source Han Sans CN VF';
+      font-size: 28rpx;
+      font-weight: 700;
+      margin-bottom: 24rpx;
+    }
+  }
+}
+</style>

+ 1 - 1
pages/cbd/detail.vue

@@ -290,7 +290,7 @@ export default {
     },
     handleSettingClick(){
       uni.navigateTo({
-        url: '/pages/cbd/deviceControl?deviceId=' + this.deviceInfo.id + '&d_id=' + this.deviceInfo.d_id,
+        url: '/pages/cbd/deviceControl?deviceId=' + this.deviceInfo.id + '&d_id=' + this.deviceInfo.d_id + '&device_model=' + this.deviceInfo.device_model,
       });
     },
     closeOperationHandler(){

+ 9 - 1
pages/cbd/deviceControl.vue

@@ -50,7 +50,7 @@
       <view class="device-detail-content" style="padding-bottom: 50rpx" v-if="activeTab === 'viewImage'">
         <view class="device-detail-viewImage">
           <text class="device-detail-label">设备开关:</text>
-          <u-switch v-model="equipContrlForm.ds" size="40" active-color="#0BBC58" active-value="1" inactive-value="0"></u-switch>
+          <u-switch :value="equipContrlForm.ds == '1'"  @input="setDs" size="40" active-color="#0BBC58" active-value="1" inactive-value="0"></u-switch>
         </view>
         <view class="device-detail-viewImage">
           <text class="device-detail-label">定时模式</text>
@@ -260,6 +260,7 @@ export default {
       imageList: [],
       equipContrlForm: {},
       time: '',
+      device_model: '',
       activeTab: 'pestAnalysis',
       title: '设置控制',
       myuser_type: false,
@@ -272,6 +273,7 @@ export default {
   },
   onLoad(options){
     this.d_id = options.d_id
+    this.device_model = options.device_model || ''
     this.getControlDeviceConfigInfo()
     uni.getStorage({
       key:"myuser_type",
@@ -321,6 +323,11 @@ export default {
       const value = Math.round(min + ratio * (max - min))
       this.$set(this.equipContrlForm, field, value)
     },
+    // u-switch 的 v-model 只会写入布尔值(active-value/inactive-value 仅作用于 change 事件),
+    // 这里手动映射回后端约定的 '1'/'0' 字符串
+    setDs(val) {
+      this.$set(this.equipContrlForm, 'ds', val ? '1' : '0')
+    },
     handleBack() {
       uni.navigateBack({
         delta: 1
@@ -388,6 +395,7 @@ export default {
         method: 'POST',
         data: {
           device_type_id: 3,
+          device_model: this.device_model,
           d_id: this.d_id,
           config: JSON.stringify(newForm)
         }

+ 6 - 0
pages/equipList2/index.vue

@@ -1061,6 +1061,12 @@ import { wgs84togcj02, bd09togcj02 } from '../../util/coordtransform_utils.js';
 							url: `../cb/wenshizs/wenshizs?devBid=${item.d_id}&devName=${item.name}&devStatus=${item.status}`,
 						});
 						break;
+					case 52:
+						item.addtime = item.uptime;
+						uni.navigateTo({
+							url: `../cb/wenshidp/wenshidp?devBid=${item.d_id}&devName=${item.name}&devStatus=${item.status}`,
+						});
+						break;
 					case 45:
 						item.addtime = item.uptime;
 						uni.navigateTo({

+ 2 - 2
util/api.js

@@ -12,8 +12,8 @@ export const myRequest = (options) => {
     // BASE_URL = 'http://218.28.198.186:10508';
     // BASE_URL = 'http://8.136.98.49:8002';
   }
-  BASE_URL = config.productAPI;
-  // BASE_URL = config.developAPI;
+  // BASE_URL = config.productAPI;
+  BASE_URL = config.developAPI;
   var session_key = '';
   session_key = uni.getStorageSync('session_key');
   let url = '';