Bläddra i källkod

feat: add温湿度数据采集页面及相关功能

1. 新增pages/cb/wenshidp页面及配套组件,实现设备详情、环境数据展示、设备控制和操作记录功能
2. 新增设备列表跳转入口,适配52类型设备
3. 调整API基础URL配置,切换到正式生产地址
4. 注释掉旧的shuHai页面跳转逻辑
allen 1 dag sedan
förälder
incheckning
82d5746c17

+ 13 - 0
pages.json

@@ -485,6 +485,19 @@
       ]
     },
     {
+      "root": "pages/cb/wenshidp",
+      "pages": [
+        {
+          "path": "wenshidp",
+          "style": {
+            "navigationBarTitleText": "",
+            "enablePullDownRefresh": false,
+            "navigationStyle": "custom"
+          }
+        }
+      ]
+    },
+    {
       "root": "pages/cb/zhamenFirst",
       "pages": [
         {

+ 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>

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

@@ -0,0 +1,259 @@
+<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 使 $myRequest resolve 完整响应体(含 data + total)
+          header: {
+            'Content-Type': 'application/json',
+          },
+          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>

+ 20 - 14
pages/equipList2/index.vue

@@ -1045,20 +1045,20 @@ import { wgs84togcj02, bd09togcj02 } from '../../util/coordtransform_utils.js';
 							url: '../cb/xylps/detail/detail?detail=' + JSON.stringify(item),
 						});
 						break;
-					case 51:
-						item.addtime = item.uptime;
-						item.type = item.type_id;
-						uni.navigateTo({
-							url: '../shuHai/detail?info=' + JSON.stringify(item),
-						});
-						break;
-					case 31:
-						item.addtime = item.uptime;
-						item.type = item.type_id;
-						uni.navigateTo({
-							url: '../shuHai1/detail?info=' + JSON.stringify(item),
-						});
-						break;
+					// case 51:
+					// 	item.addtime = item.uptime;
+					// 	item.type = item.type_id;
+					// 	uni.navigateTo({
+					// 		url: '../shuHai/detail?info=' + JSON.stringify(item),
+					// 	});
+					// 	break;
+					// case 31:
+					// 	item.addtime = item.uptime;
+					// 	item.type = item.type_id;
+					// 	uni.navigateTo({
+					// 		url: '../shuHai1/detail?info=' + JSON.stringify(item),
+					// 	});
+					// 	break;
 					case 40:
 						item.addtime = item.uptime;
 						uni.navigateTo({
@@ -1071,6 +1071,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

@@ -1,8 +1,8 @@
 // let BASE_URL = 'http://114.55.0.7:8002';
 // const BASE_URL='http://8.136.98.49:8002'
-let BASE_URL = 'https://wx.hnyfwlw.com'
+// let BASE_URL = 'https://wx.hnyfwlw.com'
 // let BASE_URL = 'http://192.168.1.107:8000';
-// let BASE_URL = 'http://218.28.198.186:10508';
+let BASE_URL = 'http://218.28.198.186:10508';
 // let BASE_URL = 'https://uat.hnyfwlw.com'
 export const myRequest = (options) => {
   // BASE_URL=uni.getStorageSync('http')